Showing Posts From

시간

API 응답 시간 최적화로 밤새 격렬한 논쟁

API 응답 시간 최적화로 밤새 격렬한 논쟁

화요일 밤 10시 배포가 끝났다. 그런데 퇴근을 못 한다. 민수가 슬랙에 글을 올렸다. "API 응답 속도 개선안 공유합니다." PDF 파일 하나. 23페이지다. 10시에 이걸 올리나. 열어봤다. 캐싱 전략을 Redis에서 로컬 메모리로 바꾸자는 내용이다. 응답 시간을 300ms에서 50ms로 줄일 수 있다고 한다. 벤치마크 결과까지 첨부했다. 성실하긴 하다. 문제는 이게 아니다. "민수야, 잠깐." "네 형님." "이거 좋긴 한데, 메모리 동기화 이슈는?" "Caffeine 쓰면 괜찮을 것 같은데요." 시작됐다.250ms의 의미 우리 API는 평균 300ms다. 나쁘지 않다. 대부분의 유저는 느끼지 못한다. 그런데 민수는 이게 문제라고 한다. 모바일에서 체감된다고. 특히 3G 환경에서. "형님, 요즘 누가 3G 써요." "통계 봐. 아직 12%야." "그 12%를 위해 아키텍처를 바꿔요?" 할 말은 없다. 그래도 12%는 유저다. 민수의 안은 이렇다. Redis 대신 로컬 메모리 캐시를 쓴다. Caffeine 라이브러리로 TTL 5분. 서버 3대 각각 독립적으로 캐시를 가진다. 장점은 명확하다. 네트워크 홉이 사라진다. Redis까지 왕복 50ms가 없어진다. 데이터 직렬화도 필요 없다. 객체를 그냥 메모리에 둔다. 단점도 명확하다. 캐시 불일치. 서버 A에서 업데이트해도 서버 B는 모른다. 최대 5분간 다른 데이터를 준다. "그 정도는 괜찮지 않아요? Eventually Consistent면 되는데." "민수야, 우리 결제 API도 이거 쓰는데." "아." 침묵. 밤 11시, 화이트보드 회의실로 갔다. 화이트보드를 꺼냈다. 민수가 왼쪽에 그렸다. 현재 아키텍처. WAS 3대, Redis 1대. 모든 요청이 Redis를 거친다. 단순하다. 데이터 일관성도 보장된다. 내가 오른쪽에 그렸다. 민수의 안. WAS 3대, 각각 로컬 캐시. Redis는 그대로 두되 폴백용. 복잡하다. "형님, Redis는 왜 남겨둬요?" "세션이랑 Rate Limiting은 어떻게 할 건데." "아, 맞다." 우리는 30분간 화이트보드를 채웠다. 장단점을 표로 만들었다. 로컬 캐시 장점:응답 속도 250ms 단축 Redis 부하 70% 감소 네트워크 장애 영향 최소화로컬 캐시 단점:캐시 불일치 최대 5분 메모리 사용량 서버당 2GB 증가 배포 시 캐시 워밍업 필요 모니터링 복잡도 증가민수가 펜을 내려놨다. "그래도 해볼 만한데요." 나도 펜을 내려놨다. "어떤 API부터?"데이터를 보자 감이 아니라 숫자로 얘기하기로 했다. Grafana를 켰다. 지난 한 달 API 통계. 상품 조회 API일 호출 500만 건 평균 응답 320ms 데이터 변경 주기: 10분에 1번 캐시 히트율: 89%장바구니 API일 호출 120만 건 평균 응답 280ms 데이터 변경 주기: 실시간 캐시 히트율: 45%주문 내역 API일 호출 80만 건 평균 응답 350ms 데이터 변경 주기: 하루 1~2번 캐시 히트율: 92%민수가 화면을 가리켰다. "이거 보세요. 상품 조회는 10분에 한 번만 바뀌잖아요. 5분 불일치도 괜찮아요." 맞는 말이다. 상품 가격이나 재고가 5분 늦게 반영돼도 큰 문제는 없다. "주문 내역도요. 하루에 한두 번만 바뀌는데 5분이 뭐가 중요해요." 이것도 맞다. 이미 완료된 주문은 거의 안 바뀐다. "그럼 장바구니는?" "이건 Redis 그대로 쓰죠." 합리적이다. 자정 넘어서 계산을 해봤다. 현재 비용:EC2 t3.large 3대: 월 50만원 ElastiCache r6g.large 1대: 월 35만원 총 85만원로컬 캐시 적용 시:EC2 t3.xlarge 3대 필요 (메모리 증가): 월 95만원 ElastiCache 그대로: 월 35만원 총 130만원월 45만원 증가. 연 540만원. "형님, 근데 Redis 스케일 아웃 안 해도 되잖아요." 민수 말대로 현재 Redis CPU가 60%다. 트래픽 더 늘면 스케일 아웃 해야 한다. r6g.xlarge로 올리면 월 70만원. 35만원 증가. 결국 비슷하다. "그럼 속도 개선만 남네." "네. 250ms요." "250ms에 얼마를 지불할 건데." 민수가 계산기를 두드렸다. "개발 2주, 테스트 1주, 모니터링 구축 1주. 한 달이요." 한 달이면 내 인건비 500만원, 민수 인건비 400만원. 총 900만원. "첫 해에 1500만원 쓰는 거네." "네." "250ms를 위해." 침묵이 길어졌다.기획팀 메일 그때 메일이 왔다. 기획팀장이다. "개발팀 귀띔 하나 해드려요. 다음 분기에 모바일 앱 리뉴얼 들어갑니다. API 속도 개선 필요하면 지금 하세요. 예산 나와요." 민수가 웃었다. "타이밍 좋네요." 나도 웃었다. "그러게." 예산이 나온다. 그럼 얘기가 다르다. "민수야, 그럼 이렇게 하자." "네." "1단계는 상품 조회만. 리스크 낮으니까." "좋아요." "2단계는 주문 내역. 한 달 모니터링 후에." "네." "장바구니는 그대로." "알겠습니다." 계획이 나왔다. 단계적 적용. 리스크 분산. 새벽 1시 민수가 일어났다. "형님, 그럼 내일부터 시작할게요." "아니, 모레부터. 내일은 쉬어." "괜찮은데요." "민수야, 지금 몇 시야." "아, 맞다." 민수가 가방을 챙겼다. 나도 컴퓨터를 껐다. 엘리베이터에서 민수가 물었다. "형님, 제 안이 맞았어요?" "반은." "반만요?" "기술적으론 맞았어. 근데 비용 계산을 안 했잖아." 민수가 고개를 끄덕였다. "그게 시니어의 차이군요." "뭔 시니어. 나도 3년 전엔 너처럼 했어." "진짜요?" "응. 캐시를 Memcached에서 Redis로 바꾸자고 했다가 팀장한테 혼났지." 엘리베이터가 1층에 도착했다. 최적화의 함정 집에 가는 택시 안에서 생각했다. 개발자는 최적화를 좋아한다. 300ms를 50ms로 만드는 게 즐겁다. 코드가 빨라지면 기분이 좋다. 근데 회사는 다르다. 비용을 본다. 1500만원으로 250ms를 사는 게 합리적인지 묻는다. 답은 없다. 상황마다 다르다. 트래픽이 많으면 최적화가 돈이 된다. 서버 한 대를 줄일 수 있으면 연 1200만원이 남는다. 그럼 해야 한다. 근데 우리는 아직 그 단계가 아니다. 트래픽은 안정적이다. 서버를 줄일 수준도 아니다. 그래도 하기로 했다. 예산이 나오니까. 결국 타이밍이다. 수요일 아침 출근했다. 민수가 먼저 와 있다. "형님, 어제 생각해봤는데요." "응." "1단계 전에 PoC 먼저 돌려보면 어때요?" "좋지." 민수가 노트북을 열었다. 이미 로컬 환경에 구축했다. Caffeine 설정, TTL 5분, 최대 10000개 캐시. "JMeter로 부하 테스트 돌렸어요." "결과는?" "300ms에서 48ms로 떨어졌어요." 오. 벤치마크보다 좋다. "근데요." "뭔데." "CPU가 15% 올라갔어요. GC 때문에." 역시. 로컬 메모리는 GC 압력을 높인다. 객체가 많아지면 Stop-the-World 시간이 늘어난다. "JVM 옵션 튜닝 필요하겠네." "G1GC를 ZGC로 바꿔볼까요?" "Java 버전은?" "11이요." "그럼 안 돼. 17로 올려야 하는데." 또 일이 늘었다. 결국 민수와 나는 계획을 다시 짰다. 1차: 준비 (2주)Java 17 업그레이드 JVM 옵션 튜닝 PoC 환경 구축 모니터링 대시보드2차: 적용 (2주)상품 조회 API 로컬 캐시 적용 카나리 배포 (트래픽 10%부터) 에러율, 응답시간 모니터링3차: 확장 (4주)주문 내역 API 적용 전체 트래픽 전환 성능 리포트 작성총 8주. 2개월이다. 민수가 Jira 티켓을 만들었다. 나는 팀장에게 보고 메일을 썼다. 기술 부채와 개선의 경계 점심을 먹으면서 민수에게 물었다. "어제 힘들었지?" "괜찮았어요. 재밌었어요." "뭐가?" "형님이랑 같이 고민하는 거요." 민수는 솔직하다. "민수야, 근데 하나 물어볼게." "네." "왜 이 최적화를 하고 싶었어?" 민수가 잠깐 생각했다. "느린 게 싫었어요. 우리가 만든 API가 300ms씩 걸리는 게." "유저는 못 느끼는데?" "그래도요. 더 빠르게 할 수 있잖아요." 이해한다. 나도 7년 전엔 그랬다. "근데 형님은 왜 반대하셨어요?" "반대한 게 아니라, 계산을 하자는 거였어." "비용이요?" "응. 시간, 돈, 리스크." 민수가 고개를 끄덕였다. "시니어가 되면 그런 거 자동으로 생각돼요?" "아니. 실수 많이 해봐야 돼." 나는 김치찌개를 한 숟가락 떴다. 논쟁의 가치 어제 밤은 소모적이지 않았다. 민수는 비용 개념을 배웠다. 나는 새로운 기술을 배웠다. Caffeine 라이브러리는 몰랐다. ZGC 얘기도 민수가 꺼냈다. 좋은 논쟁이었다. 나쁜 논쟁도 있다. 자존심 싸움. 누가 옳은지 증명하려는 싸움. 그런 건 시간 낭비다. 좋은 논쟁은 다르다. 서로 배운다. 더 나은 답을 찾는다. 완벽한 답은 없어도 합리적인 답은 나온다. 어제 우리는 합리적인 답을 찾았다. 로컬 캐시를 쓰되, 단계적으로. 비용을 쓰되, 예산 범위 안에서. 리스크를 지되, 최소화하면서. 목요일 오후 민수가 PR을 올렸다. Java 17 업그레이드 작업. 리뷰했다. Gradle 버전도 올렸다. 라이브러리 호환성도 체크했다. 꼼꼼하다. "Approve." 민수가 머지했다. CI/CD가 돌아간다. 테스트 통과. 빌드 성공. "형님, 개발 서버에 배포할게요." "ㄱㄱ." 10분 후. 슬랙 알람. "개발 서버 배포 완료. Java 17 정상 동작." 시작이다. 2주 후 PoC가 끝났다. 결과는 좋았다. 상품 조회 API (로컬 캐시 적용)평균 응답: 45ms (기존 320ms) CPU 증가: 12% (예상 15%) 메모리 증가: 1.8GB (예상 2GB) 캐시 히트율: 94% (기존 89%) 에러율: 변화 없음민수가 보고서를 만들었다. 그래프도 예뻤다. 기획팀에 보냈다. 답장이 왔다. "좋습니다. 프로덕션 일정 잡아주세요." 금요일 저녁 회식이다. 민수 제안으로 고깃집. 고기를 굽는다. 민수가 소주를 땄다. "형님, 건배." "응." 잔을 부딪쳤다. "형님, 고마워요." "뭐가." "어제 밤에요. 같이 고민해줘서." 나는 소주를 마셨다. "민수야, 나도 고마워." "네?" "좋은 아이디어였어. 덕분에 나도 배웠어." 민수가 웃었다. 우리는 고기를 먹었다. 야근 얘기, 레거시 얘기, 신입 얘기. 개발자들이 모이면 하는 얘기. 편했다. 최적화의 끝 최적화는 끝이 없다. 45ms를 20ms로 만들 수도 있다. CDN을 쓰면 된다. 10ms로 만들 수도 있다. Edge Computing을 쓰면 된다. 근데 안 한다. 비용 때문이다. 어디선가 멈춰야 한다. 합리적인 지점에서. 45ms면 충분하다. 유저도 못 느낀다. 비용도 적당하다. 더 빠르게 할 수 있지만, 하지 않는다. 그게 어른이 되는 거다. 민수도 곧 알게 될 거다. 월요일 이번 주에 프로덕션 배포다. 월요일 밤 10시. 카나리 배포. 트래픽 10%. 화요일 새벽 2시. 모니터링 체크. 수요일 오후. 50%로 확대. 목요일 오전. 100% 전환. 계획은 완벽하다. 근데 계획대로 안 될 거다. 항상 그랬으니까. 괜찮다. 우리는 준비했다. 롤백 플랜도 있다. 모니터링도 빡빡하다. 민수가 물었다. "형님, 떨려요." "나도." "진짜요?" "응. 매번 떨려." 배포는 항상 떨린다. 7년 차도 마찬가지다. 근데 해야 한다. 안 하면 아무것도 안 바뀐다.결국 둘 다 옳았다. 민수의 기술안도, 내 비용 계산도. 그래서 절충했다. 개발은 원래 그런 거다. 완벽한 답은 없고 합리적인 타협만 있다. 어제 밤 논쟁은 시간 낭비가 아니었다. 더 나은 방향을 찾았으니까. 다음 주엔 배포다. 또 떨리겠지.