NerdVana
  • 홈
  • About
  • 아카이브
  • 메인으로
블로그로 돌아가기

**Spring Boot 3 + Java 21로 “다시” 백엔드 실무를 정리했다: MSA 전환, Docker·Kubernetes, OpenTelemetry, 성능 튜닝까지**

**Spring Boot 3와 Java 21 기준으로 MSA 전환에서 Docker·Kubernetes 배포, OpenTelemetry 기반 Observability, 그리고 성능 튜닝까지 실무에서 부딪혔던 포인트를 한 번에 정리했다. “이론”보다 “운영에서 살아남는 방법”에 초점을 맞췄다.**
2025년 12월 27일
조회 7
# Spring Boot 3와 Java 21, 실무에서 마주친 진짜 이야기 ![대표 이미지: 도입부 첫 번째 문단 직후 배치하여 전체 주제를 상징적으로 보여주는 이미지](https://paycurr.com/download?f=20251227_045347_Tu39sXId.png) 기술 스택을 업그레이드한다는 건 생각보다 단순하지 않다. 버전 숫자 몇 개 올리면 끝날 줄 알았던 일이 어느새 아키텍처 전체를 흔드는 여정이 되어버린다. 올해 유독 많은 팀이 레거시 정리와 MSA 전환, 클라우드 네이티브 배포를 동시에 추진하고 있다. 나 역시 그 흐름 한가운데 서 있었다. --- ## 업그레이드는 시작일 뿐이었다 처음에는 간단해 보였다. Spring Boot 2.x에서 3.x로, Java 17에서 21로 올리면 끝이라고 생각했다. 그런데 현실은 늘 그렇듯 예상을 빗나갔다. MSA로 서비스를 쪼개면 각각의 덩어리는 작아진다. 하지만 운영 난이도는 오히려 커진다. 컨테이너에 올리면 배포 자체는 쉬워지지만, 관측 체계가 없으면 장애 원인을 찾는 일이 지옥이 된다. 트래픽이 조금만 요동쳐도 GC, 스레드, 커넥션 풀, 쿼리가 한꺼번에 발목을 잡는다. 그래서 이 글은 내가 실무에서 직접 겪은 순서대로 정리했다. MSA 전환에서 시작해 Docker와 Kubernetes 배포를 거쳐 OpenTelemetry 기반 관측 체계를 구축하고, 마지막으로 성능 튜닝까지. 이 순서가 고통을 완전히 없애주진 않지만, 적어도 덜어준다. --- ![설명 이미지: 'Virtual Threads의 실체' 소제목 직후 배치하여 개념 설명 보강](https://paycurr.com/download?f=20251227_045402_gGpyLchb.png) ## MSA 전환에서 진짜 중요한 것 ### 서비스를 나누기 전에 경계부터 확정하라 MSA 전환에서 흔히 저지르는 실수가 있다. 서비스를 나누는 작업 자체에만 집중하는 것이다. 정작 중요한 건 경계다. 경계가 흐릿하면 서비스가 분리되어도 문제가 끊이지 않는다. API 호출이 폭발적으로 늘어난다. 마치 채팅하듯 서비스끼리 끊임없이 호출을 주고받게 된다. 트랜잭션을 억지로 분산 트랜잭션으로 끌고 가려 하고, 한 서비스가 죽으면 장애가 전체로 퍼져나간다. 내가 효과를 본 접근법은 도메인 기준과 데이터 소유권을 먼저 확정하는 것이었다. 각 서비스는 자기 데이터의 소유권을 가진다. 다른 서비스의 데이터가 필요하면 DB 조인이 아니라 API나 이벤트를 통해 받는다. 조회 성능이 필요하면 CQRS 스타일로 읽기 모델을 비정규화한다. 실제로 주문, 결제, 정산이 복잡하게 얽힌 프로젝트에서 이 원칙을 적용했다. 주문 서비스가 결제 테이블을 직접 읽던 구조를 끊어냈다. 처음에는 팀원들이 불편해했다. "조인 한 번이면 되는데 왜 돌아가느냐"는 말이 나왔고, 솔직히 나도 속으로는 공감했다. 그런데 운영을 겪고 나면 결론이 달라진다. 조인이 편한 건 개발 초기뿐이다. 장애 대응, 확장성, 보안은 운영에 들어가야 진면목이 드러난다. ### 동기 호출과 이벤트, 정답은 없다 ![예시 이미지: '실제 장애 사례' 문단 직후 배치하여 사례 시각화](https://paycurr.com/download?f=20251227_045431_hx2VoGS9.png) 동기 호출과 비동기 이벤트 중 무엇을 선택할지는 정답이 없다. 대신 어떤 방향의 기술 부채를 감당할 것인지 선택하는 문제다. 동기 호출은 REST나 gRPC 같은 방식을 말한다. 즉시 일관성에 가깝지만 장애와 지연이 빠르게 전파된다. 반면 Kafka 같은 이벤트 기반 방식은 느슨한 결합과 확장성을 제공하지만, 중복 처리, 순서 보장, 재처리라는 숙제가 따라붙는다. 내 경험상 결제 승인처럼 반드시 즉각적인 결과가 필요한 경우는 동기로 두는 게 맞다. 반면 정산, 포인트 적립, 알림처럼 조금 늦어도 되는 것들은 이벤트로 넘기는 편이 팀의 스트레스를 줄였다. --- ## Spring Boot 3와 Java 21, 습관을 바꿔야 한다 Spring Boot 3는 Jakarta 전환이 이미 완료된 세계다. 그래서 컴파일 에러만 잡으면 된다고 생각하기 쉽다. 하지만 그런 안일함은 운영 환경에서 뒤통수를 맞게 된다. ![설명 이미지: '자주 마주치는 병목 다섯 가지' 문단 직후 배치하여 요약 보강](https://paycurr.com/download?f=20251227_045453_m8sOeISz.png) ### Virtual Threads의 실체 Java 21의 Virtual Threads는 가볍게 볼 기능이 아니다. 특히 외부 API 호출이 잦거나 DB 응답 지연이 튀는 서비스에서 스레드 대기 비용이 눈에 띄게 줄어든다. 기존에는 톰캣 스레드가 막히면 대기열이 길어졌다. Virtual Threads를 적용하면 막히는 동안 OS 스레드를 덜 점유하기 때문에 동시성에 여유가 생긴다. 다만 무조건 빨라진다는 식으로 적용하면 위험하다. JDBC 드라이버, 커넥션 풀, 외부 SDK가 Virtual Thread 환경에서 어떤 병목을 만드는지 반드시 확인해야 한다. 내 경우 Virtual Thread를 적용한 뒤 오히려 DB 커넥션 풀이 먼저 바닥나서 장애 직전까지 간 적이 있다. 스레드 수가 늘어난 게 아니라 동시 요청 처리량이 늘어난 것이기 때문에, 풀 크기 조정과 쿼리 최적화가 함께 이루어져야 했다. ### 컨테이너 환경에서의 메모리 관리 Kubernetes에서 메모리 제한을 걸어두면 JVM이 그 제한을 제대로 인식하는지 옵션과 버전 조합을 확인해야 한다. 요즘은 많이 개선되었지만 힙만 보고 안심하면 안 된다. Metaspace, Direct memory(Netty 등에서 사용), Thread stack, Native memory까지 함께 봐야 한다. OOMKilled는 힙만의 문제가 아니다. --- ## Docker, 빌드는 가볍게 실행은 예측 가능하게 Docker는 이제 선택이 아니라 실무의 전제 조건이다. 내가 선호하는 방향은 명확하다. 빌드 이미지와 런타임 이미지를 멀티 스테이지로 분리한다. 런타임은 distroless나 최소화된 JRE 이미지를 사용한다. 컨테이너 안에서 JVM이 자신에게 할당된 자원 한계를 정확히 인지하도록 설정한다. 그런데 이미지 최적화보다 더 중요한 게 있다. 로그와 진단 가능성이다. 이미지를 너무 미니멀하게 줄이다가 장애 상황에서 컨테이너 안에서 확인할 도구가 하나도 없으면, 그때부터는 운영팀과 서로 눈치만 보게 된다. 나는 distroless를 좋아하지만 팀이 아직 익숙하지 않다면 슬림 이미지에 최소한의 디버깅 도구를 넣는 타협을 한다. 운영의 목적은 멋진 Dockerfile을 만드는 게 아니라 새벽 3시에 살아남는 것이다. --- ## Kubernetes 배포, 오토스케일보다 리소스 모델이 먼저다 Kubernetes로 넘어오면 대부분 HPA(Horizontal Pod Autoscaler)부터 활성화한다. 하지만 리소스 request와 limit을 대충 잡으면 오토스케일은 자동으로 망하는 기능이 되기도 한다. ### 리소스 설정의 감각 CPU request는 평균이 아니라 안정적으로 버티는 최소치를 기준으로 잡는다. CPU limit은 너무 타이트하게 걸면 스로틀링 때문에 오히려 지연이 튄다. 메모리 limit은 OOMKilled로 직결되므로 여유를 주고 관측 데이터를 보면서 점진적으로 줄인다. readiness와 liveness probe를 대충 넣으면 배포가 안정적인 것처럼 보이다가 트래픽 피크에 무너진다. readiness는 트래픽을 받아도 되는 상태인지, liveness는 프로세스가 죽었는지를 판단한다. 부팅이 오래 걸리는 앱이라면 startup probe를 추가하는 게 좋다. 이것 하나로 삶이 훨씬 편해졌다. --- ## Observability, OpenTelemetry는 도구가 아니라 언어다 MSA로 전환하면 장애 원인 분석이 어려워진다. 로그만으로는 부족하고 메트릭만으로도 부족하다. 트레이스가 있어야 서비스 간 흐름이 연결된다. 요즘 실무에서 가장 무난한 선택은 OpenTelemetry다. 특정 벤더에 덜 묶이면서도 생태계가 탄탄하다. ### 관측에서 우선순위를 두는 세 가지 첫째는 분산 트레이싱이다. 이 요청이 어디서 느려졌는지 파악하는 데 핵심이다. 둘째는 메트릭이다. 얼마나 많은 요청이, 얼마나 느리게, 얼마나 많이 실패하는지를 보여주는 RED나 USE 같은 지표를 활용한다. 셋째는 구조화된 로그와 traceId 연계다. 왜 그런 결과가 나왔는지 원인을 추적하는 데 필수적이다. 특히 로그에 traceId를 자동으로 심는 것만으로도 체감 효과가 크다. 예전에는 장애가 나면 각 서비스 로그를 시간대에 맞춰 grep으로 뒤졌다. 지금은 traceId 하나로 흐름 파악이 거의 끝난다. 이건 정말로 삶의 질이 바뀌는 수준이다. ### 실제 장애 사례 결제는 정상인데 주문이 실패했던 날이 있었다. 장애 알람은 주문 서비스에서만 울렸고 결제 서비스는 멀쩡했다. 처음에는 주문 DB가 느린 건가 싶었다. 트레이스를 열어보니 상황이 달랐다. 주문에서 결제 승인까지는 정상이었다. 문제는 그다음이었다. 주문에서 재고 확인으로 가는 호출에서 지연이 발생했다. 재고 서비스에서 특정 조건에 캐시 미스가 터지면서 DB 커넥션 풀이 고갈되었고 타임아웃으로 이어졌다. 결제는 잘 됐는데 주문이 실패한 이유가 깔끔하게 보였다. 이때 깨달았다. MSA 환경의 장애는 한 군데의 문제가 아니라 흐름 전체의 문제라는 것을. --- ## 성능 튜닝, 빠르게보다 예측 가능하게 성능 튜닝을 할 때 TPS 최대치에만 집착하면 운영이 불안해진다. 실무에서 더 중요한 건 다른 데 있다. 피크 상황에서 응답 시간이 급격히 무너지는 구간을 없애는 것. 장애가 났을 때 복구 가능한 형태로 시스템을 설계하는 것. 노드, DB, 캐시 같은 비용을 합리적으로 쓰는 것. ### 자주 마주치는 병목 다섯 가지 DB 쿼리에서는 인덱스, 조인, 락, 커넥션 풀을 본다. 스레드 모델에서는 플랫폼 스레드와 Virtual Threads, 블로킹 I/O 여부를 확인한다. 캐시 전략에서는 동시 갱신 상황에서 캐시 미스가 폭탄처럼 터지는지 점검한다. GC와 메모리는 힙뿐 아니라 native 영역까지 살펴야 한다. 외부 API 호출에서는 타임아웃, 리트라이, 서킷브레이커 설정을 검토한다. ### 리트라이는 부하 증폭기다 타임아웃이 발생해서 리트라이를 걸면 순간적으로 트래픽이 두세 배가 된다. 장애 상황에서는 성공 확률이 낮은데 부하만 늘어나는 최악의 루프에 빠진다. 그래서 나는 보통 이렇게 설정한다. 타임아웃은 짧게 걸어서 기다리지 말고 빨리 실패하게 한다. 리트라이는 횟수를 제한하고 지수 백오프를 적용한다. 서킷브레이커로 문제가 되는 구간을 격리한다. 그리고 실패했을 때 사용자 경험을 미리 설계해둔다. 대체 응답을 보여주거나 지연 처리로 안내하는 식이다. --- ## AI 시대의 백엔드 실무 요즘 팀에서 AI 도구를 쓰는 건 이상한 일이 아니다. 나도 설계 초안이나 마이그레이션 체크리스트는 AI의 도움을 자주 받는다. 흥미로운 건 AI가 코드를 빨리 만들어줄수록 개발자에게 남는 일이 더 명확해진다는 점이다. 이 변경이 운영에서 어떤 리스크를 만들지, 관측 지표로 무엇을 봐야 하는지, 장애가 나면 어디부터 확인할지, 비용은 어떻게 달라지는지. 결국 실무 백엔드는 코드를 짜는 일이 아니라 시스템을 설명하고 예측하고 책임지는 일에 가까워지고 있다. Spring Boot 3와 Java 21은 좋은 도구다. 하지만 도구가 방향을 대신 정해주지는 않는다. --- ## 순서를 지키면 덜 다친다 정리하면 내가 추천하는 진행 순서는 이렇다. 먼저 도메인 경계와 데이터 소유권을 확정한다. 이것이 MSA의 뼈대다. 그다음 컨테이너와 Kubernetes는 배포 자동화보다 리소스 모델을 먼저 잡는다. OpenTelemetry로 trace, metric, log의 연결 고리를 만든다. 성능 튜닝은 최고 속도가 아니라 급락 구간을 제거하는 데 집중한다. 이 흐름대로 가면 열심히 했는데 운영에서 무너지는 확률이 줄어든다. 나도 몇 번은 무너져봤고, 그래서 더 확신한다. --- ## 다음에 다루고 싶은 이야기 Spring Boot 3에서 OpenTelemetry 수집 파이프라인을 팀 규모별로 어떻게 구성할지, Java 21 Virtual Threads를 언제 쓰고 언제 쓰지 말아야 하는지, Kubernetes에서 HPA와 VPA, KEDA를 섞을 때의 운영 감각 같은 주제들이다. 팀 상황에 따라 트래픽 규모, DB 종류, 배포 환경, 관측 도구가 다를 것이다. 구체적인 맥락을 알려주면 그에 맞는 현실적인 체크리스트로 다시 정리해볼 수 있다.
7
조회수
0
좋아요
0
공유

© 2025 NerdVana. All rights reserved.

홈으로 블로그