[1부]에 이어
스카우터에 찍힌 쿼리와 코드를 계속해서 탐색했다. 몇 가지 개선해야 할 것이 눈에 보이기 시작했다. 코드가 변경이 쉬운 구조는 아니지만 J 개발자에게 코드의 구조와 동작 방식을 들으면서 변경 방법을 조금씩 찾기 시작했다.
이 와중에
J 개발자와 코드를 이리저리 보고 있는데 다급한 표정으로 누군가 다가온다. 푸시 발송에 문제가 있단다. 개선안 찾던 걸 멈추고 푸시 문제로 넘어갔다. 스카우터를 보니 푸시 서버 관련 XLog가 제일 위에 붙어 있다.
일단 푸시를 내렸다. 코드를 봤더니 푸시 목록을 DB에서 조회하고 푸시를 발송하고 발송 상태로 변경하는 작업이 한 트랜잭션으로 묶여 있다. 이를 주기적으로 실행해서 푸시를 발송하고 있는데 발송 대상이 늘어나면서 실행 주기 안에 푸시 발송을 끝내지 못했고 이로 인해 이슈가 발생하기 시작했다. 아직 푸시 처리를 하고 있는데 다시 푸시 처리를 요청하면서 락이 발생했고 그러면서 전체 푸시 발송이 먹통이 되었다.
당장 구조를 변경할 수 없어 중복 실행을 방지하는 쪽으로 방향을 잡았다. 푸시 발송 상태 필드를 추가하고 아직 발송 중이면 실행 요청을 무시하는 코드를 넣었다. 수정한 코드를 배포하고 푸시를 재개했다. 다행히 문제가 재발하지 않는다.
급한 불을 끄고 J 개발자와 옥상에 올라갔다. J 개발자는 담배 한 모금과 긴 한 숨을 내 쉬며 마음을 가라 앉힌다. 판교 하늘이 어두워지기 시작했다.
다시 안 도출
저녁을 먹고 자리로 돌아와 다시 안을 도출하기 시작했다. 한 번에 모두 바꿀 수 없으니 조회 성능을 높이는데 초점을 맞췄다. 크게 4개 정도 안을 도출했다.
- 자주 불리는 기능에 대해 조회 전용 모델 개발. 현재는 하나의 모델을 그것도 복잡하게 연관을 맺은 모델을 명령과 조회에 함께 사용하고 있어 조회 시 불필요하게 많은 쿼리를 실행하고 이로 인해 응답 시간이 길어지고 있다.
- 집계용 테이블 추가. 게시물의 댓글 개수를 댓글 테이블에 count 쿼리를 날려서 구하고 있다. 좋아요 수도 동일한 방식으로 구하고 있다. 이 또한 응답 시간을 증가시키고 있다.
- 조회 기능 일부에 캐시 적용. 조회 전용 모델을 만들어내는데 성공하면 일부 기능에 캐시를 적용할 수 있게 된다. 아직은 바로 할 수 없다.
- 채팅 용도 서버 분리. 지금은 API 서버가 모든 요청을 처리하고 있다. 채팅 트래픽이 많지는 않지만 응답 시간이 증가해 톰캣 쓰레드를 잡고 있으면 다른 기능도 영향을 받게 된다. 당장 DB 분리는 못 하더라도 채팅 전용 서버라도 따로 분리해 영향을 줄이고 싶다.
안을 정리하고 나니 벌써 금요일 저녁 10시다. 집에 가야겠다.
구현 시작
3월 9일. 주말을 쉬고 돌아온 월요일. M사에 해당 서비스를 책임지는 서버 개발자는 J 혼자다. M사의 다른 조직에도 서버 개발자가 있긴 한데 적극적으로 돕지는 않고 있다. 이대로 가만있으면 도출한 안을 실행하지 못할 것 같다. 일부라도 실행하려면 M사에 가서 밀어 붙여야 한다. 점심을 먹고 M사로 이동했다.
M사에 도착했다. 여전히 다들 정신이 없다. 한가지 다행이라면 J 개발자가 채팅 용도 서버는 분리했다는 것이다. 하나를 했으니 다른 하나를 진행하면 된다. 오늘의 목표는 게시글 조회 전용 모델을 만드는 것이다. 이걸 만들지 못하면 다음 단계로 넘어갈 수 없다.
기존 게시글 모델을 복사해서 새 모델을 만들었다. 조회에 불필요한 사용자 엔티티, 첨부 파일 엔티티 등 대부분 연관을 삭제했다. 첨부파일의 썸네일 이미지 경로는 코드에서 조합했다. 새 모델을 위한 조회 API는 별도 클래스로 직접 구현했다. 나중에 일부라도 캐시를 적용하고 싶어서다.
현재 게시글 목록 조회 API는 페이지 단위로 조회하고 있다. 여기에 like 검색도 섞여 있다. 페이지 개수를 구하기 위해 count 쿼리도 날린다. 이대로는 성능 개선이 어렵다. 마침 등록 시간에 인덱스가 걸려 있는 걸 확인했다. 다행이다. 이걸 쓰면 되겠다. 조회 전용 게시글 모델 목록 API는 등록 시간 기준 from - to 조건과 조회 개수 제한 limit을 사용해서 목록을 조회하게 구현했다. 이를 이용하면 일정 시간 내에 목록을 제공할 수 있을 것 같다.
새 게시글 목록 API를 다 만들고 개발에서 기능을 확인했다. 끝나고 나니 저녁 10시가 되었다. 월요일부터 힘들다. 못하겠다. 퇴근이다.
서버 다운
3월 10일 화요일. 오전부터 응답 시간이 폭증한다. 일부 서버가 풀GC를 미친듯이 하고 있다. 특정 게시글을 조회할 때 발생한 것으로 보인다. 거미줄처럼 엮어 있는 연관 관계 때문에 조회 결과에 연관된 엔티티 개수가 많은 게시글이 포함되어 응답 시간이 느려진 것이다. 0.01초 걸리는 쿼리를 1000번 돌리면 10초가 걸리는 것과 같은 효과다. 응답이 빨리 안 오자 새로고침을 계속하고 그러면서 메모리 사용이 증가했고 이것이 풀GC를 유발했으며 그러다 어느 순간 죽은 거다.
당장 살아야 하니 급한대로 톰캣이 사용하는 메모리를 늘려서 재시작하자고 했다. 조회 전용 모델을 빨리 운영에 반영해야 한다. 안 그러면 또 같은 일이 벌어질 것이다.
작은 실랑이
3월 11일. 수요일. 앱 개발자/앱 기획자와 만났다. API 변경에 대해 얘기하기 위함이다. 서버를 M사에서 진행했다면 앱은 I사에서 진행했다. I사는 내가 다니는 회사다. 그렇다. 같은 회사 직원을 만나 회의를 진행했다.
앱 쪽과 먼저 협의하지 않고 서버 API 변경을 진행하는 것이 서운했는지 반응이 좋지 않다. 당장 시작해도 늦었는데 이런 반응이라니. 순간 목소리가 높아졌다. 몇 차례 언성을 높였다가 정신을 차리고 다시 논의를 했다. 사실 앱 개발자도 처음부터 서버 API가 많이 이상해서 바꿔달라고 요구했지만 그 프리랜서 개발자가 고집을 피우며 바꾸지 않았다고 한다. 아...... 마음을 가라 앉히고 다시 API 변경에 대해 논의했다. 안을 정리했고 회의 말미에는 언성을 높인 것도 사과했다.
J 개발자에게 회의 내용을 공유하고 조회 전용 모델을 더 추가해 달라고 말했다.
일주일
3월 12일. 목요일이다. 처음 연락을 받은 뒤로 일주일이 지났다. 그 사이에 J 개발자가 인덱스 몇 개를 더 추가했다. 그 덕에 당장은 버티고 있다. 하지만 이걸로는 안 된다. 트래픽이 조금만 증가해도 무너질 것이다. 마음이 급하다. 오후에 다시 M사로 이동했다.
오늘은 게시글 관련 집계 정보를 별도 테이블로 구현하는 것이 목표다. 댓글 수, 좋아요 수를 담을 테이블을 구성하고 개발을 시작했다. 댓글 생성 기능에 이벤트 발생 코드를 추가했다. 이 이벤트를 수신할 핸들러를 만들고 핸들러에서 댓글 수 증가 기능을 호출하게 구현했다. 핸들러는 비동기로 실행했다. 카운트 증가 감소는 JdbcTrmplate을 이용해서 쿼리로 구현했다. JPA를 써야 할 이유가 없다. 게시글 조회수 증가도 변경했다. 기존에는 게시글 엔티티의 readCount 값을 1 증가하는 방식이었는데 이를 댓글 개수와 동일하게 이벤트+비동기 핸들러+쿼리로 바꿨다.
새로 만든 게시글 목록/상세 API에 카운트 테이블 연동을 추가했다. 목표를 달성했다. 끝내고 나니 10시가 넘었다. 퇴근이다.
초조
3월 13일. 금요일. J 개발자와 통화를 했다. 게시글 관련 집계 정보를 별도 테이블이 아니라 게시글 테이블에 칼럼을 추가한 방식으로 구현을 바꿨단다. 음... 분리한 이유가 있는데. 지금은 그 이유를 설명하고 있을 여유가 없다. 일단 하나를 했으니 다음을 진행해야 한다.
이후로 몇 일이 지났다. J 개발자의 노력으로 몇 개 기능에 대해 조회 전용 모델이 완성됐다. 하지만 웹과 앱에는 아직 반영되지 않고 있다. 초조하다. 빨리 적용해야 하는데...
[3부]에서 계속 ......