[요즘 우아한 개발] 백엔드 개발자로 성장하기 : 개발자 머피의 법칙 ❷

이 글은 《우아한 요즘 개발》에서 발췌했습니다.

골든래빗 출판사

#개발문화        #데이터베이스        #보안  

손권남 2019.09.19

머피의 법칙은 ‘항상 나한테만 재수 없는 일이 일어난다’는 의미가 아닙니다. ‘어떤 일을 하는 데에 둘 이상의 방법이 있고 그것 중 하나가 나쁜 결과(disaster)를 불러온다면 누군가가 꼭 그 방법을 사용한다’에서 시작해 ‘잘못되는 것은 꼭 잘못되게 마련이다’라는 의미입니다. 특정한 개인의 불행과는 상관없는 것이지요. 어차피 잘못될 것이니 자포자기 하라는 의미가 아니라 잘못될 가능성이 있다고 생각된다면 미리 대비하라는 말입니다.

프로그래머로 산 지 좀 되다보니 ‘어, 이거 이렇게 하면 저렇게 잘못될 거 같지만, 에이 설마…’ 이런 생각을 하고 넘어 갔다가 호되게 당한 경험이 꽤 많고 다른 사람이 그런 경우도 많이 봤습니다. 주로 백엔드 서버 개발자로서 어떤 일들을 겪었는지 저 자신의 경험과 주위에서 본 것을 정리해보았습니다.

 

직원 PC에서 운영 서버 API, DB 저장소 접근을 통제한다

머피네 회사는 아직 초창기였습니다. DBA(Database Administration)도 부족했고 보안에 신경 쓸 여력도 부족했습니다.

 

서비스용 DB 접근 계정에서는 DDL 권한이 제거돼야 한다

 

개발자들이 수시로 실 서비스 DB에 테이블 수정 명령 alter table을 날렸고, update/delete/insert도 했습니다. 개발자의 PC에서요. ‘설마 개발자들이 바보가 아닌 이상 alter table을 잘못 날리겠어?’라고 생각했죠. 다행히도 아직까지 실수한 개발자는 없긴 했습니다. JPA/Hibernate를 사용하던 머피네 개발팀은 hibernate.hbm2ddl.auto=create라는 옵션을 알게 됩니다. 이 옵션을 사용하면 개발자가 DB 구조를 변경할 때 매번 alter table을 실행하지 않아도 자동으로 기존 테이블을 모두 날리고 새로 만들어줍니다.

로컬 개발 환경에서 잘 사용하면 개발이 매우 편리해지지만, 문제는 저옵션을 켠 채로 운영에 배포했을 때 발생합니다. 모든 테이블을 날리고 새로 생성하고 데이터가 초기화되는 일이 발생합니다.

이 실수는 hibernate.hbm2ddl.auto로 검색하면 매우 많이 나옵니다.

단순히 production 프로필 설정에서 이 값을 none으로 한다고 해결되는 게 아닙니다. 로컬 PC 환경에서 production 서버에 접근이 가능하면 똑같은 일이 발생할 수 있습니다.

운영 DB에 접근하는 계정(개발자 계정이든 애플리케이션 계정이든)은 통제된 몇 명(DBA)이 아니면 DDL(가급적 DML까지) 권한을 제거해야 합니다. hibernate.hbm2ddl.auto는 그냥 사용하지 말고 Flyway 등을 사용하세요. Flyway, LiquidBase, 혹은 각 언어/프레임워크에서 제공하는 DB 히스토리 관리 도구를 사용해도 같은 일이 발생할 수 있습니다. 머피는 삭제된 DB를 복구하느라 상당히 애를 먹었습니다. 다행히 복구가 되긴 했습니다.

DB만 저장소인줄 알았지? MQ도 저장소다

 

머피는 DB를 날려먹는 경험을 한 뒤로 모든 DB DDL 권한을 제거하고 로컬에서는 운영 DB에 접근할 수 없게 막았습니다. 그리고는 ‘이제 안심해도 되겠구나’ 했습니다. ‘데이터 저장하는 DB나 NoSQL 계열은 다 막았으니 괜찮겠지?’ 안심하긴 일렀습니다.

주문 시스템을 맡았던 머피는 옆 팀인 배달팀 팀원으로부터 갑작스런 호출을 받았습니다. 갑자기 주문 완료 후 배달 요청하는 MQ(Message Queue) 메시지가 뭔가 매우 적게 오는 것 같다는 겁니다. ‘무슨일이지?’ 머피가 주문 완료 후 배달 쪽으로 MQ 메시지 전송하는 것이 잘 이뤄지는지 확인해보고, 잘 된다고 옆 팀에 전달하자마자 옆 팀 개발자 한 명이 지르는 비명을 듣게 됩니다. 옆 팀의 다른 개발자가 production 서버에 붙어서 확인할 게 있다고 별 생각 없이 로컬 개발 환경에서 production 프로필로 배달 애플리케이션 서버를 띄운 것이었습니다. 본인은 제 딴에 ‘데이터를 수정하는 기능을 호출하지는 않고 잘 붙는지만 볼 거니 괜찮겠지?’라고 생각했던 것이지요.

이는 로컬 환경에서 운영 MQ 서버에 접속해서 운영 MQ의 데이터를 읽어서 처리하게 된 것과 같습니다. 원래 MQ는 메시지 전송 후 예외가 발생하면 메시지를 다시 MQ에 집어넣게 돼 있는데, 이럴 수가! 배달 애플리케이션에서 예외가 발생한 것을 모두 무시하고 로그만 간단히 남기게 했나봅니다.

하필 엄청나게 주문이 몰리는 시간이었네요. 수천 건의 주문 완료 후 배달 요청으로 넘어가는 데이터가 운영 배달 애플리케이션이 아니라 개발자의 로컬 PC에 떠 있는 서버로 빨려들어가 버렸습니다.

옆 팀 개발자는 로컬에서 띄운 애플리케이션을 종료하고, 그 기간 중 주문된 데이터와 배달 요청 메시지가 오지 않은 것을 뽑아내어 한 땀 한 땀 데이터를 복원해야만 했습니다. 그 사이 배달은 지연되었죠.

API 서버도 접근 통제해야 한다

 

머피네 회사는 앞서의 DB, MQ 관련 사고를 겪으면서 개발자 PC에서 운영 저장소로의 접근을 모두 통제하고, 불필요한 DDL, DML 권한을 모두 제거해버렸습니다. ‘이 정도 했으면 설마 무슨 일 생기겠어?’ 네, 생기죠. 머피네 회사는 운영 API 서버의 접근 권한을 회사 망 내의 개발자들에게 열어주었습니다. 머피는 데이터 변경 요청을 받았는데 API를 호출하면 쉽게 된다는 것을 알았습니다. 그래서 안전하게 테스트 환경 API 서버에 변경 요청을 날려보기로 했습니다. 그렇게 테스트를 모두 한 뒤에 운영 API 서버를 호출하기로 했습니다.

요즘 나오는 REST Client 애플리케이션은 개발, 테스트 환경, 운영 환경별 프로필을 설정하고 프로필 별로 접속 주소를 따로 설정하고, API 요청 시에 이 프로필을 선택할 수 있게 합니다. 즉, 같은 /api/update라는 API도 어떨 때는 로컬 PC에서 어떨 때는 운영 API 서버에서 손쉽게 실행하게 해줍니다.

머피는 나름 조심스럽게 프로필을 선택한다고 했는데, 프로필 선택 순간 옆에서 누가 잠깐 말을 걸었고, 터치패드의 손이 미끄러졌는지 production 프로필을 선택하고 실행해버리고 맙니다.

그 뒤는 얘기하지 않겠습니다. 이처럼 API 서버를 직접 호출해야만 하는 일이 자주 생긴다면, 해당 역할을 하는 인증과 권한 관리가 된 내부 관리자용 어드민 서비스를 만들어야 합니다.

모든 서버는 프라이빗하게, 회사망조차도 모두 접근 통제

 

개발자에게 로컬 PC에서 모든 쓰기 권한을 삭제하고 읽기 권한만 지급해서 DB나 API에 접근하게 했습니다. 그런데 보안은 평균으로 작동하지 않고 가장 약한 부분에 의해 뚫리게 됩니다. 쇠사슬처럼요. 100개중 99개가 튼튼해도 1개의 사슬만 고장나 있으면 그 쇠사슬은 끊어집니다.

개발자들은 다른 비개발자들과 같은 망에서 개발을 하고, 운영 서버에 읽기 권한으로 접근할 수 있습니다. 즉, 개발자가 아니라 동일 망 내의 비개발자도 운영 DB, API에 읽기 권한에 접근할 수 있습니다.

개발자든 아니든 직원 중 한 명이 가짜 자동차 보험료 할인 이메일에 속아서 ActiveX를 깔거나 한다면 회사망 내의 포트를 샅샅히 스캔해서 데이터를 빼갈 수도 있을 겁니다.

따라서 모든 서버는 프라이빗 망에서만 접근 가능해야 하고, 꼭 필요한 것만 프록시(Proxy, AWS에서는 ELB/ALB 등)를 통해서만 퍼블릭에서 접근 가능해야 합니다. 회사 직원이 사용하는 서비스는 회사망 내에서만 접근 가능해야 합니다. 어쩔 수 없이 필요하다면, 모든 운영 저장소와 API는 회사망을 기준으로 하지 않고 꼭 필요한 특정 직원을 기준으로, 되도록 쓰기 없이 읽기 권한만 접근 통제 상태로 권한을 부여해야 합니다. 요즘에는 ActiveX 대신*.exe파일과 안드로이드 앱 수동 설치 등으로 보안이 뚫릴 수도 있어 보입니다.

사실 보안은 스타트업에게는 너무 어려운 일입니다. 당장 기능을 개선하기에도 바쁘거든요. 그리고 보안의 강화는 개발의 불편을 의미합니다. 하지만 서비스가 정말로 성장할 것 같다면 성장세에 맞추어 계속 보완해 나가야 합니다.

중요 배치는 실행 여부를 제3의 시스템에서 검증해야 한다

머피네 서비스의 인기가 너무 좋아 데이터양이 너무 많아지자 매일 매일 다음날치 데이터를 미리 생성하는 배치Batch 애플리케이션으로 데이터를 말아놓는 방식을 사용하기로 했습니다. 머피는 열심히 개발했고 배치가 며칠 잘 도는 것도 확인했습니다. 꼭 실행해야 하는 중요한 배치이므로 배치 애플리케이션 코드 내에서 오류 발생 시 오류 메시지도 메신저와 이메일로 전송해주게 만들었습니다.

‘배치 애플리케이션 실패에 관해서도 알림을 걸었으니 설마 실행되지 않는 경우는 없겠지. 설마 배치 서버가 죽지는 않겠지.’ 네, 다행히 배치를 실행하는 서버는 안죽었습니다. 그런데 누가 배치 실행 시간이 오래 걸리니 스케줄러를 변경해서 실행 시간을 좀 앞당기자고 했습니다. 그래서 스케줄을 앞당겼는데 Cron 표현식을 약간 잘못 작성한 걸 모른 채 지나갔습니다.

그리고 배치 애플리케이션은 아예 실행조차 되지 않았습니다. 실행이 안됐으므로 오류도 나지 않았고, 오류가 안 나니 알림도 오지 않았습니다. 다음날 출근했더니 데이터가 없습니다.

중요한 배치 잡(Batch Job)들은 별개의 모니터링 솔루션을 사용해 특정 시간 범위에 성공적으로 실행됐는지를 확인해 그렇지 못한 경우 알림을 주게 만들어야만 합니다. influxDB와 그라파나Grafana를 사용하면 가능할 것 같네요.

로그를 외부 서버로 수집하는 것은 별도 프로세스에서 비동기로

10년도 전 얘기입니다. 로그 수집 서버 같은 게 없을 때요. 로그를 디스크에 잘 쌓고 있었습니다. 그런데 오류가 발생해도 개발자들이 서버에 들어가서 확인해보기 전에는 모두 모르고 넘어가고 있었죠. 더 나은 프로그램을 만들고 싶고 문제를 빨리 인식하고 싶었습니다.

그래서 이메일로 로그를 발송해주는 SMTPAppender를 자바 로거(Logger)에 설정해서 오류 로그가 발생하면 이메일로 전송하게 했습니다. 그러면서 ‘설마 오류가 그렇게 많이 나려고…’라고 생각했죠. 몇 달 동안 날라온 오류 로그 이메일을 보면서 열심히 개선했습니다. 뿌듯했습니다.

어느 날 전사 이메일이 먹통이 돼버렸습니다. 직원이 3천 명은 넘었던 것 같은데 말이죠. 어떤 개발자가 해당 서비스의 메인 페이지에 버그를 심었고, 하필 이벤트로 인해서 메인 페이지는 어마무시한 폭탄급 트래픽을 받게 됩니다. 버그는 Stacktrace가 포함된 수많은 오류를 발생시켰고, 그 오류는 모두 이메일로 전송되면서 SMTP 서버를 감당할 수 없는 지경에 빠뜨린 겁니다.

나중에 메일함에서 이메일을 30만 개 정도 지웠던 것 같습니다. 그리고 그 기능을 제거했습니다. 그 뒤에 머피는 다른 시스템을 맡아 일하면서 또 비슷한 상황을 맞이합니다. 거기서도 개발자들이 똑같이 서버 오류를 잘 안보고 있습니다. 그래서 오류 로그가 발생하면 그것을 네트워크 통신으로 접속해서 전송하는 Log Appender를 설정했습니다. 또 오류가 많이 날 수도 있다고는 생각했습니다. 하지만 ‘설마 오류가 나서 엄청나게 오류 로그를 전송한다고 뭔일이 나겠어?’라고 생각했죠. 즉, ‘뭔 일이 나는지 모르는 상태’였습니다.

또 다시 누군가가 메인 페이지에 버그를 심었고… 당연히 SMTP 서버는 멀쩡했습니다. 그런데 애플리케이션이 멈춰버리고, 서버에 접근할 수가 없었습니다. 오류 로그 폭탄은 네트워크를 타고 전송됐고, 네트워크 대역폭을 모두 잡아먹었습니다. 로그 전송에 고갈된 네트워크는 엄청나게 느려져서 장애 대응을 위해 서버에 접속하는 것이 불가능했습니다. 아마 해당 서버의 디스크 IO/CPU 점유율도 문제가 돼서 접속되지 않은 것 같습니다.

요즘처럼 AWS 같은 클라우드를 사용한다면 새로운 서버를 쉽게 띄우고 트래픽을 옮겼겠지만, 당시에는 클라우드를 사용하지 않고 고정된 하드웨어에서 애플리케이션 버전을 갈아끼우는 방식으로 배포했기 때문에 다른 신규 서버를 띄워서 트래픽을 전환하기도 매우 힘든 상황이었습니다.

여기서는 세 가지 문제가 있습니다.

  • 지금은 좋은 로그 수집 시스템이 많이 나와 있죠. 그런 솔루션을 사용하면 됩니다. 어설프게 애플리케이션 프로세스에서 네트워크로 전송하는 방식을 사용해서는 안 됩니다. 애플리케이션 프로세스와 독립된 로그 수집/전송 데몬을 사용해 로그를 비 동기로 CPU와 디스크/네트워크 IO에 영향을 최소화하는 방식으로 차등 전송해야 합니다.
  • 애플리케이션 자체에서도 비동기로 디스크 쓰기 작업을 하게 AsyncAppender를 설정하는 것이 좋습니다. 부하가 심하면 불필요한 로그를 버리거나, 혹은 쓰기 작업 을 늦춥니다.
  • 애플리케이션 자체에서 처리하려고 했다면 성능 테스트를 해야만 했습니다.

 

기본 키는 Integer가 아니라 Long으로

스타트업 개발자들의 흔한 착각 중에 ‘설마 우리 서비스가 성공할 리 없어~’라는 게 있는 것 같습니다. 비용을 아낀다는 명목으로 혹은 별 생각 없이 순차 증가하는 기본 키(Primary Key)를 Integer로 만듭니다. 이는 갑자기 폭발적으로 성장하는 서비스의 발목을 잡는 주범이 됩니다. 단순히 순차 증가 값을 Long으로 하는 것만으로 엄청난 장애를 막고 비용을 아낄 수도 있습니다. Integer로 매핑된 DB 컬럼을 변경하고, 애플리케이션 코드의 매핑 필드 타입을 변경하는 작업은 엄청난 비용을 초래하며, 수많은 연계 시스템 중에서 한 개만 Integer 매핑 상태로 있어도 전면 장애로 이어질 수 있습니다. 특히 기본 키 값은 외래 키로 서비스 곳곳으로 전파되기 때문에 한 군데만 고친다고 문제가 해결되는 게 아닙니다. 저장소 비용을 아끼려다 개발 비용/장애 처리 비용 및 신뢰도 하락이라는 더 큰 비용을 초래합니다. 게다가 성공하지 못할 서비스에게 Long형 데이터 몇 개는 비용으로 볼 수도 없는 수준일 겁니다.

너무도 명명백백하게 어떤 상황에서도 Integer 범위를 넘길 수 없는 경우를 빼고는 그냥 순차 증가 기본 키는 Long으로 매핑하는 게 이득인 거 같습니다. 어려운 일도 아니니까요. 여기서 주의할 것이 있습니다. API로 외부 연동을 할 경우 클라이언트 애플리케이션에서도 확실히 Long으로 매핑하고 저장하는지 확인해야만 합니다. 초반 테스트용 데이터의 작은 숫자만 보고 Integer로 API 결과를 매핑하는 일도 부지기수입니다.

 

오류 로그는 일상적인 것과 크리티컬한 것을 구분한다

‘설마 우리 개발자들이 알림을 놓치는 일은 없을거야! 모든 알림을 같은 채널에 두자!’ 모든 알림을 다 중요하게 취급하든, 모든 알림을 다 사소하게 취급하든 중요한 알림은 놓칠 수 있습니다. 일상적으로 오는 알람과 중요 이벤트 알림을 구분하고, 중요 이벤트는 관련 팀의 응답을 필수적으로 체크하는 것이 좋습니다.

어느 정도 규모 있는 서비스를 개발해본 사람 중에서 마케팅팀에서 주요 포털 메인 광고를 붙이는 공유를 못 받아서 서버 증설 대응을 못해 장애가 나거나, 오류 로그 알림 중 중요한 것을 무시하고 넘어가서 나중에야 알게 되는 일은 드물지 않으리라 생각합니다. 그때 많이 듣는 말이 ‘우리 회사는 정보 공유가 잘 안돼’라는 푸념인데, 사실은 공유했지만 중요도를 인지하지 못한 경우가 더 많았던 것 같습니다.

롤백 가능한 배포

‘나의 배포는 완벽해! 뒤로 되돌릴 일은 없다고!’라는 자신만만한 분은 없죠? 웹 애플리케이션뿐만 아니라 배치 애플리케이션도 롤백 가능하게 만들어야 합니다.

 

당신의 서버는 무조건 죽는다 : SPoF를 제거하자

‘설마 내가 구축한 서버가 죽겠어?’ 네,죽습니다. 안 죽는 서버를 만드는 게 아니라 ‘죽어도 괜찮은 서버’를 구축해야 합니다. 단일 장애점 SPoF(Single Point of Failure)제거하라는 의미입니다. 또한 내가 만든 애플리케이션이 아니라 내가 호출하는 애플리케이션이 죽는 경우도 염두에 두고 서킷 브레이커 등을 도입하는 것도 고려하면 좋습니다.

 

◆◆◆

 

저의 모든 경험을 여기 적을 수도 없고, 그렇다 쳐도 저의 경험은 개발자들이 하는 모든 상황 중 1%도 안 되는, 한 줌도 안 되는 수준에 불과할 겁니다. 그리고 이 모든 내용은 제가 다 겪은 게 아니라 여전히 ‘설마…’하는 상태인 것도 있습니다. 제가 하고 싶은 말의 핵심은 이겁니다. ‘이런저런 일이 생길 수도 있겠네, 그러면 큰 문제가 되겠는데?’라는 생각이 드는 순간 실제로 그 일이 발생했다고 가정해야 한다는 겁니다. 그리고 ‘실제로 그 일이 발생하면 여파가 어느 정도될까? 감당할 수준인가?’라고 판단해 봅니다.판단 결과 장애 정도가 너무 크다면 그 장애는 이미 발생한 것으로 간주합니다. 그리고 실제 장애가 발생했을 때처럼 최우선 처리를 합니다.

그러나 감당할 수 있는 수준이라면 그것은 그것대로 넘어가야 합니다. 세상의모든 일에 미리 대비한다면 아무 일도 못하고, 어떤 서비스도 배포해보지 못할 테니까요. 저는 서비스의 성장에 맞추어 대비를 해나가야 하지 않나 싶습니다. 서비스의 성장 단계에 따라 장애 발생 시 여파가 달라지기 때문입니다.

제가 일하면서 제일 많이 한 생각이 ‘아… 설마 설마 했지만 알고는 있는데… 계획도 세워뒀는데…’였는데요, 다시 한번 상기시켜드립니다. ‘잘못되는 것은 꼭 잘못되게 마련이다.’

저자 우아한 형제들

우아한형제들은 배달이 일상을 조금 더 행복하게 하도록 오늘도 달리고 있습니다. 평범한 사람들이 모여 비범한 성과를 만들어 내는 곳이될 수 있도록 건강한 조직문화를 만드는 일에 진심을 다합니다. 2016년부터 ‘우아한형제들 기술블로그’를 운영하며 개발 조직의 성장 과정을 기록하고 있습니다.

1 Comment

Leave a Reply

©2020 GoldenRabbit. All rights reserved.
상호명 : 골든래빗 주식회사
(04051) 서울특별시 마포구 양화로 186, 5층 512호, 514호 (동교동, LC타워)
TEL : 0505-398-0505 / FAX : 0505-537-0505
대표이사 : 최현우
사업자등록번호 : 475-87-01581
통신판매업신고 : 2023-서울마포-2391호
master@goldenrabbit.co.kr
개인정보처리방침
배송/반품/환불/교환 안내