그때는 미처 알지 못했고, 지금은 조금 알 듯하고, 내일은 더 알았으면 싶고
안녕하세요, 넷마블 인프라실 인프라자동화팀 안현지입니다.
부푼 마음으로 넷마블에 입사했던 2021년 1월이 엊그제 같은데, 시간이 빠르게 흘러 사계절이 지나 다시 봄이 돌아왔습니다. 어느덧 저는 “신입” 딱지를 떼고 2년 차가 됐고, 짧은 지난 1년간 겪었던 희로애락의 순간이 빠르게 흐른 시간과 달리 잔뜩 떠오릅니다. 학생 시절에는 겪지 못했던 경험으로 깨달은 몇 가지 내용을 머릿속에서 꺼내 보겠습니다.
OJT
제가 속한 인프라자동화팀은 넷마블 사내 클라우드 서비스를 개발하는 팀입니다.
인프라자동화팀은 크게 에이전트(Agent), 백엔드(Back-end), 프론트엔드(Front-end) 파트로 나뉘어 있습니다. 저는 약 3개월간 백엔드와 프론트엔드를 오가며 OJT를 보냈습니다. 이후에는 백엔드 개발을 맡아 왔습니다.
백엔드 개발에서는 스프링부트(Spring Boot)를 사용하고 있었습니다. 반면 저는 스프링부트를 사용해본 적이 없었던 탓에, 잘 적응할 수 있을지 걱정도 했었습니다. 하지만, OJT 기간 동안 매일 멘토님께 궁금한 점을 해소하면서, 빠르게 팀에서 사용하는 기술 스택에 익숙해질 수 있었습니다. 입사 초기 제가 어려움에 부닥칠 때마다 나타나서 같이 고민해주시고, 해결할 수 있도록 많은 도움 주신 멘토님께 다시 한번 감사드립니다.
설정 자동화, Conf 개발 시작
OJT를 마친 지난 1년 동안 레거시 프로젝트를 처음부터 다시 개발하는 업무를 담당했습니다. “Conf”라는 이름을 가진 서비스로, 이름에서 유추할 수 있듯이 넷마블 클라우드 내에 설정 자동화 기능을 제공하는 서비스입니다. 시중에서 많이 사용하는 유사 도구 중에는 앤서블(Ansible)이 있습니다.
방화벽 설정 확인 습관화
개발을 시작한 후, 학생 때와 달랐던 가장 첫 경험은 방화벽 설정이었습니다. 특정 서버나 특정 사이트에 접속하지 못하거나, 팀 내 배포 시스템에 접근하지 못해 혼자 끙끙거렸던 입사 초기는 아직도 생생하게 기억납니다. 혹시라도 기본 설정도 할 줄 모르고, 배포 명령어도 모르는 신입으로 비칠질까봐 조마조마하기도 했습니다.
멘토님께서는 입사 초기에 겪는 흔한 권한 문제라며, 방화벽 오픈을 신청하면 쉽게 해결할 수 있다는 것을 알려주셨습니다. 전에는 고려해본 적 없던 “방화벽” 문제가 회사에서는 매우 중요한 보안규정이라는 걸 깨달은 중요한 순간이었습니다.
‘호환성’이라는 단어가 주는 무게감
개발 단계를 거의 마무리할 즈음, 다른 서비스와 연동하기 위한 추가 작업이 생겼습니다. 그로 인해 코드를 새로 고치던 중 서버 API 규격이 바뀌었고, 바뀐 규격은 다시 다른 분들께 고스란히 영향을 끼쳤습니다. Conf API를 사용하는 분들께 2차 수정을 요청드리면서 마음이 무거웠습니다. 무거워진 마음이 가시지 않은 채, 이런 수정 요청이 그 후로도 여러 번 생겼습니다. 추가 수정이 필요하지 않도록 최대한 신경 썼다고 생각했었는데, 생각과 현실은 다르더군요. ‘불가피하다’라는 수식어가 마음의 짐을 덜어주진 않았습니다.
이미 많이 사용하는 서비스라면 변경 사항 단 1개도 2차 수정, 3차 수정 등 많은 파급력을 가지고 있습니다. 그래서 쉽게 구조를 변경하기 힘들다는 것을 몸소 느낄 수밖에 없었습니다.
직접 겪어봐야 이해한다고 하던가요? 이전에는 코드를 짤 때 자주 사용하는 함수에서 ‘Deprecated’ 문구가 붙은 걸 보면서 “왜 변경했지?”라며 투정을 부리곤 했었는데, 이제는 “음. 그만한 이유가 있겠구나.”라며 이해하게 됐습니다.
예상치 못했던 웹서버 트래픽 폭주
설렘과 두려움을 품고 실 서버에 프로젝트를 론칭했습니다. 론칭 후 한 달간 베타 서비스를 운영하는 동안은 특별한 문제가 발생하지 않았습니다. 그러나 베타 배지를 뗀 후, 그날이 왔습니다. 연동한 서비스 업데이트와 함께 대량 요청건이 발생한 날, 웹서버 트래픽이 폭주하기 시작했습니다. 연신 ‘Too many connections’ 에러를 내뿜던 서버는 다운되기 일보 직전까지 갔습니다.
1분도 안 되는 시간 동안 400 에러가 수십 개씩 쌓이는 걸 보면서, 저의 심장 박동 소리도 요동쳤습니다. 서버를 재기동하면 잠시 잘 되는 것처럼 보이다가, 이내 커넥션 부족으로 또다시 400 에러가 비처럼 쏟아 내렸습니다. 급하게 팀장님께 SOS를 보내고 나서야 문제 원인이 커넥션 부족이라는 걸 알게 됐습니다. 커넥션 설정값을 변경하고 재기동하자, 문제가 해결됐습니다.
DB로 사용하던 MySQL은 커넥션 최댓값(max_connections)을 따로 수정하지 않으면 기본으로 151개가 설정돼 있습니다. 개발 환경에서도 커넥션 최댓값을 변경했었고, 실 서버에서도 반영해야 한다고 분명히 머리로는 알고 있었는데 왜 이 부분을 놓쳤던 걸까요. 자책감이 들며, 개발자로서 일할 역량이 부족한 것이 아닐까 하는 고민이 들기도 했습니다. 이날은 많은 생각이 들던 하루였습니다.
학생 때도 다양한 프로젝트에 참여해 개발하고 배포하는 과정까지 마무리하는 경험이 몇 번 있었습니다. 그땐 기본 설정값으로 충분하고도 남을 트래픽이었기 때문에 설정값 튜닝은 한 번도 생각해볼 일이 없었습니다. 문제가 되지도 않았죠. 그렇지만 이번 사건을 통해 아파치 톰캣(Apache Tomcat)과 DB 설정값에 대해서 자세히 살펴볼 수 있는 계기가 생겼고, 덕분에 개발자로서 시야를 넓힐 수 있었습니다.
유지보수는 계속된다!
Conf 서비스 QA와 보완점 개선을 거쳐 베타 기간까지 마친 후에는 “아! 이제 끝났다.”라는 생각이 들었습니다. 하지만 그건 오산이었습니다. 계속해서 개선을 해야하는 이슈가 나타났습니다. 데이터가 쌓이면서 속도는 느려지고 성능은 저하되고 데이터 저장 공간은 부족해지는 등 여러 문제가 발생했습니다.
요청을 더 빠르게 처리하기
Conf 서비스는 크게 “시나리오”와 “실행”으로 나눌 수 있습니다.
- 시나리오: YAML 문법을 사용해 설정 파일 작성
- 실행: 사전에 작성한 설정 파일을 복수 서버에서 실행
기존 Conf 서비스는 실행 요청을 받을 때 동기식으로 작동했습니다. 예를 들면 서버 10대에서 실행할 항목을 리스트로 요청받아 각 서버 에이전트로 보내면, 해당 요청을 동기식으로 처리하는 방식입니다. 미세한 시간 차이라 생각할 수도 있지만, 이런 요청이 한 번에 몇십건씩 들어오면 실행 속도에 딜레이가 발생할 수 있습니다. 그래서 이 부분을 비동기로 처리하도록 로직을 변경했습니다.
스프링(Spring)에서는 비동기를 구현하기 위해 @async 어노테이션을 사용합니다. 이 때 @async는 프록시(Proxy)를 사용하기 때문에 동일한 서비스에서 호출하면 적용되지 않습니다. 또한 @async를 사용하면 현재 스레드(Thread)에서 분리돼 새로운 스레드로 수행되기 때문에 @Transactional을 적용한 곳에서 사용하더라도 하이버네이트(Hibernate)에 포함되지 않습니다. 그래서 저는 DB 작업을 먼저 진행하고 비동기 작업을 실행하도록 수정했습니다.
데이터 조회 시간 단축하기
위 화면은 Conf 서비스 실행 목록 화면입니다. 서비스 런칭 초기에는 지연 없이 빠르게 목록을 조회할 수 있었습니다. 하지만 데이터가 쌓이면서 조금씩 느려지다가, 10만 건 이상 데이터가 쌓이면 페이지가 멈춰버리는 현상이 나타났습니다.
이 문제는 DB 테이블에 인덱스(Index)를 추가해서 해결했습니다.
일반적으로 DB 테이블을 생성할 경우, 기본키(PK)에는 자동으로 인덱스가 생성됩니다. 인덱스가 적용되면 인덱스에 걸린 컬럼을 기준으로 B-tree 구조를 생성하기 때문에 매우 빠르게 조회할 수 있습니다. 반면 인덱스가 걸리지 않은 컬럼값을 기준으로 조회하면 데이터양이 1만 건 정도만 쌓여도 매우 느려질 수 있습니다. 그래서 조회할 때 사용하는 컬럼에는 기본키가 아니더라도 인덱스를 추가해서 기존 대비 매우 빠른 속도로 목록을 조회할 수 있게 됐습니다.
인덱스를 추가하면 그만큼 메모리 공간을 많이 사용하고, DB 성능 저하를 초래할 수 있습니다. 그래서 꼭 필요한 컬럼값에만 적용하는 것이 좋습니다. 수정 빈도가 많은 컬럼보다는 값을 한번 추가한 후에 바뀔 일이 거의 없거나, 값 중복도가 낮을수록 인덱스를 추가하기 좋습니다.
DB 구조를 변경하는 업데이트
기존에는 실행 목록에서 조회하는 서버 정보를 타 서비스에서 직접 가져와서 보여주고 있었습니다. Conf 서비스용 DB에 따로 저장하지 않고 있어서, 모든 페이지를 정렬하는 기능을 제공하지 못했습니다. 조회 로직도 복잡하게 엉켜있어서 성능 저하 요소도 있었습니다.
이 문제를 해결하기 위해 주기적인 동기화 작업으로 서버 정보를 DB에 직접 저장하기로 했습니다. 설계 단계에서는 DB ERD를 변경했고, 구현 단계에서는 배치 작업을 추가했습니다.
아무래도 DB 구조를 변경하는 만큼 리스크가 큰 작업이었습니다. 이미 성능이 검증되고 안정화된 서비스 구조를 수정하는 것이기 때문에, 필요성은 알지만 걱정이 앞서는 건 어쩔 수 없었습니다. 설계 작업이 너무나도 중요하다는 것이 가장 크게 와닿았던 순간이었습니다. 학생 때는 대부분 과정이 “설계-구현-구현-구현-구현-구현-약간 테스트-제출-끝”이라, 빈약한 설계로 인해 코드가 점점 산으로 가도 제출을 끝으로 “안녕~”이었지만, 더 이상 그런 끝은 저에게 허락되지 않았습니다. 제가 잘못 짠 코드는 어떤 경로로든 다시 저에게 돌아와 저를 괴롭힙니다. 어제의 저를 원망하는 오늘의 저를 보게 되고, 또 그걸 반복할 내일의 저를 예상하게 되니까요.
OutOfMemoryError
동기화 작업을 진행할 때 외부 데이터를 한꺼번에 가져오는 경우, 메모리 부족으로 ‘OutOfMemoryError’가 발생할 수 있습니다. JVM을 사용한다면 힙 메모리(Heap Memory) 공간을 참고해 데이터를 가져올 양을 대략 추정할 수 있습니다.
저의 경우에는 외부 데이터를 한 번에 전부 가져와도 힙 메모리에 여유 있었기 때문에, 한 번에 받아와 일괄 처리했습니다. 만약 큰 규모의 데이터를 가져오는 경우라면 페이지 단위나 일정량으로 나눠 반복 조회를 하는 방식으로 대응하면 메모리 부족 사태를 막을 수 있습니다.
DB 메모리 공간 부족
성능 개선 작업을 통해 이제 안정화가 되었다고 안심하려던 때, 새로운 이슈가 또 등장했습니다. 데이터가 계속 쌓이고 쌓여 100만개를 훌쩍 넘어섰고, 아찔한 마음에 확인한 가용 공간은 얼마 남아 있지 않았다며 제 손길을 기다리고 있었습니다. 그래서 생성 시각을 기준으로 주기적인 삭제 배치 작업을 추가했습니다.
삭제 배치 작업으로 가용 공간이 넉넉하게 남아있는 것을 보고 마음에 안정을 찾았습니다.
멀티 서버 환경일 경우에는 예약한 배치 작업을 처리할 때 해당 작업이 서버 개수만큼 중복돼 처리될 수 있습니다. 그래서 Lock을 사용해 1번만 실행하도록 조치해야 합니다.
DB에서는 ACID를 보장하기 위해 Lock을 기본적으로 사용하므로, 별다른 조치를 하지 않아도 문제가 발생하지 않을 수 있습니다. 그와 달리, JPA에서는 스케줄 작업에 @Transactional 어노테이션이 들어가는 경우, 추가로 Lock을 설정하지 않으면 SQL 에러가 발생할 수 있습니다. 스케줄 작업이 많다면 스케줄용 서버를 따로 운영하는 방법도 있습니다.
데이터 종류에 맞는 DB
기존에는 시나리오 실행 로그 데이터를 에이전트 서버에서 조회하는 형태로 받아왔었습니다. 이 경우, 서버가 삭제되거나 일시적으로 다운되면 로그 데이터를 정상적으로 가지고 오지 못하는 문제가 발생합니다. 따라서 로그 데이터를 자체 저장하는 것이 필요했습니다.
Conf 서비스용 DB로는 MySQL을 사용하고 있었습니다. 관계형 데이터베이스(RDB) 특성상 로그 데이터를 텍스트(Text)형으로 저장하면 속도가 느려지고, 데이터양이 많기 때문에 전체적으로 성능 저하를 유발할 수 있었습니다. 그래서 로그 데이터를 MySQL이 아니라, 엘라스틱서치(Elasticsearch)에 저장하기로 했습니다.
팀 내에서 사용하는 엘라스틱서치 클러스터가 이미 있었던 덕분에 인덱스(Index, RDB에서는 Database)만 추가하는 정도로 수월하게 환경을 구성할 수 있었습니다. 이제 데이터 CRUD 작업을 할 때, SQL(혹은 JPA) 대신 RESTful API 형태로 요청하면 됩니다.
MySQL에서는 워크벤치(Workbench)로 데이터를 관리할 수 있었는데, 엘라스틱서치에 수집한 데이터는 키바나(Kibana)를 활용해 검색하고 시각화할 수 있습니다.
엘라스틱서치는 아파치 루씬(Apache Lucene)기반 오픈소스 분산 검색 엔진입니다. 검색 엔진은 DB와는 다른 것이라는 생각이 들 수 있지만, 엘라스틱서치는 대용량 스토리지로도 사용할 수 있습니다. 그래서 RDB에서 처리하기 힘든 대용량 비정형 데이터를 저장하기 위한 DB로도 활용할 수 있습니다.
엘라스틱서치는 RDBMS와 달리 역색인 구조로 돼 있어, 데이터양이 많아지더라도 매우 빠른 속도로 데이터를 조회할 수 있다는 장점이 있습니다. 또한, HTTP 기반 RESTful API를 지원하고 있어서 JSON형식 데이터를 요청하고 응답할 때 유용하게 사용할 수 있습니다.
반면, 실시간 처리가 불가능하다는 단점도 있습니다. 색인된 데이터는 내부적으로 커밋과 플러시 과정을 거쳐야 해서, 일반적으로 약 1초가 지난 뒤에 검색할 수 있습니다. 엘라스틱서치 공식 홈페이지에서는 이 지연 현상을 ‘NRT(Near Real Time)’라고 표현합니다. 또한 시스템적으로 비용 소모가 크기 때문에, 트랜잭션이나 롤백 기능을 지원하지 않습니다. 그래서 예상치 못한 문제 발생으로 데이터를 유연하게 관리할 수 없다는 부분을 대비해야 합니다.
이외에도 지난 1년 동안 크고 작은 사건을 거쳐, 현재는 안정적으로 Conf 서비스를 운영하고 있습니다. 어쩌면 지극히 개인적일 수도 있는 아찔한 경험과 실수일지도 모르겠지만, 이제 막 실무를 처음 경험한 신입 혹은 주니어 개발자들에게 공감과 위안이 됐으면 좋겠습니다. 선배 개발자들께서도 귀엽게 봐주셨으면 좋겠습니다. (혹은 봐주시기를 바랍니다.)
1년을 돌아보며
지난 1년을 돌아보면 학생 신분에서 직장인 신분으로 바뀌었고, 새로운 경험으로 떨리기도 했지만 동시에 설렘의 연속이었습니다. 잘하고 싶은 마음에 각종 서적을 구매하고 업무 관련 기술 강의를 찾아서 공부하며 실력을 키울 수 있다면 뭐라도 해보고 싶었습니다. 그렇지만 이론적으로 배우는 것과 실제 해보는 것 사이에는 큰 간극이 있었고, 제 마음과는 달리 새로운 일을 시작하는 과정에는 난관도 많았습니다. 멋있는 시니어 개발자분들과 스스로를 비교하면서 “왜 나는 이것밖에 할 수 없을까?” 하는 고민도 하고 말이죠. 그분들도 주니어 시절을 보내며 고난과 시련의 기간이 있었다는 것을 까먹은 채 말입니다. 아마 이제 막 업무를 시작한 모든 사회초년생이 겪는 고민이 아닐까 싶습니다.
그러나 이런 고민과 불안이 우리를 감싸는 시기가 있기에 다음 단계로 성장할 수 있다고 믿습니다. 아직도 모르는 것이 많고 배워야 할 것이 무척이나 많지만, 지금처럼 한 단계 한 단계 나아가다 보면 언젠가는 제가 기대하는 모습으로 성장할 수 있지 않을까요? 그날까지 꾸준히 전진하겠습니다.
부족한 제가 하나의 독립된 서비스를 개발하고 운영할 수 있도록 많은 도움을 주신 인프라실과 인프라자동화팀에 계신 모든 분께 감사드립니다.