Gukin Han
티스토리에서 새 글은 이곳에 쓰기로 결정했습니다. 점진적으로 이전할 예정이며, 기존 글은 티스토리에 그대로 있습니다.
개별 테스트는 통과, 전체 실행시 Too many connections 발생 여러 기능 브랜치가 개발 브랜치로 머지된 후, 그동안 기능 개발하면서 놓친 실패 테스트들을 점검하기 위해 전체 테스트를 실행했다. Too many connections 에러로그가 발생하면서 통합테스트들 일부가 실패하였다. 로그의 일부는 아래와 같다: Error creating bean with name 'liquibase': Too many connections → Error creating bean with name 'databaseCleanupUtil' → Failed to load ApplicationContext 하지만, 각각 테스트 클래스들을 개별로 실행했을때는 모두 통과했다. 도대체 왜 동일한 코드에서 실행 범위만 다를 뿐인데 결과가 다를까? ...
Virtual Thread 적용 이후 대량 이벤트 발생시 DB 저장 로직 실패 진행중인 개인 프로젝트에서는 DART 전자공시를 이용한다. 매일 수백 개의 전자공시가 DART 전자공시를 통해 게시되며 이를 수집해서 필터링하고 시장의 반응과 LLM의 감성분석을 집계하는 목표를 갖고 있다. 공시 자료 폴링(Polling)이 중단되었다가 재시작 하는 경우 다량의 공시자료를 조회하고 이벤트를 발행하게 된다. 이때 분석된 결과를 DB에 저장하는 과정에서 이슈가 발생했다. 이 과정의 비즈니스 로직은 아래와 같이 정리할 수 있다. Polling으로 새 공시를 수집한다. 수집된 공시의 제목으로 후속 처리가 필요한지 필터링한다. 후속 처리가 필요하면 NewDisclosureEvent를 발행한다. 이벤트 메시지에 담긴 공시 번호로 공시 문서를 조회한다. 조회된 공시 문서를 LLM에게 넘겨서 감성 분석(Sentiment Analysis)을 실시한다. 분석 결과를 DB에 저장한다. 문제가 발생한 코드는 다음과 같다. ...
시간당 34GB의 로그 발생 사내 인프라 담당자가 우리가 담당하고 있는 서버의 로깅 설정 관련된 내용을 공유해주었다. 간단하게 정리하면 다음과 같다: Cloudwatch는 로그 GB마다 과금되는 형태 불필요한 로그로 인해 비용이 발생하는것으로 확인됨 현재 서비스에서 발생하는 로그(특히 info)를 점검할 필요가 있음 도메인 개발자라서 인프라 업무를 직접 수행할 기회와 권한이 없었다. 이번에 인프라팀이 공유해준 걸 계기로 개인적으로 살펴보았다. 먼저 Cloudwatch에 들어가 직접 눈으로 로그 발생량을 확인했다. 우리 서비스는 HR 서비스이기 때문에 대부분의 트래픽은 9시부터 18시 사이에 발생한다. 시간당 대략 34GB(2.7억건)의 로그가 발생하며 그 중에 44% 정도가 jdbc.resultset=INFO 에서 발생함을 확인했다. 여기서 몇 가지 의문이 들었다. ...
들어가며 이전 포스팅들은 스택 영역에서의 바이트코드의 상호작용을 다뤘다. 런타임 데이터 영역에는 스택 영역 뿐만 아니라 힙 영역이 존재한다. JVM 스펙에 따르면 다음 내용을 확인할 수 있다: “자바 가상 머신은 모든 스레드가 공유하는 힙(Heap) 영역을 가지고 있습니다. 힙은 모든 클래스 인스턴스와 배열의 메모리가 할당되는 런타임 데이터 영역입니다.” 따라서, 이 글에서는 객체와 힙 영역에 관련된 여러 요소들을 탐험하고 실험하는데 목적이 있다. Object 클래스의 객체 생성 (new 키워드) 처음으로 시도해볼 실험은 가장 순수한 객체를 생성해보는 것이다. 아무런 필드도, 로직도 없는 가장 순수한 형태의 객체를 생성해 볼 수 있는 클래스는 java.lang.Object이다. Object 객체를 생성할때 과연 몇 번의 CPU 명령어를 소모할까? 어떤 과정을 거칠까? ...
들어가며 이전 포스팅에서는 컴파일된 증감연산자(i++, ++i)의 정적인 바이트코드를 분석했다. 해당 실험에서는 load, store 등의 명령어를 피연산자 스택을 통해 어떻게 연산이 이뤄지는지, iinc 명령어의 최적화 방식을 이해할 수 있었다. 이번에는 단순한 연산을 벗어나, 프로그램의 실행 순서를 바꾸고 제어하는 **제어 흐름(Control Flow)**을 다뤄보려고 한다. 코드의 흐름을 나누는 분기문(if-else), 특정 구간을 반복하는 반복문(for-loop, while), 조건에 따라 특정 점프하는 스위치문(switch)에 해당된다. 각각 바이트코드 레벨에서 어떻게 흐름을 제어하는지 실험을 통해 살펴본다. 실험 java 코드를 작성하고 javac ControlFlowTest.java 와 javap -c -p -v ControlFlowTest > result.txt 명령어를 이용해서 컴파일과 역어셈블을 하였다. 3개의 섹션을 나눠서 각각 분기문, 반복문, 스위치문을 실험 및 분석하였다. ...
들어가며 이 글은 거창하게 바이트코드를 분석하려는 의도로 시작하지 않았다. 단지 JVM을 학습하는 과정에서 제시된 간단한 실습 예제를 직접 실험해보고 있었다. 작성된 자바 파일을 javac와 javap 명령어로 컴파일하고 역어셈블(Disassemble)하는 과정은 생각보다 많은 질문을 던져주었다. “우리가 작성한 소스코드는 실제로 JVM 위에서 어떻게 돌아갈까?” “단순한 i++ 연산은 CPU와 메모리 관점에서 어떤 비용을 지불하는가?” 이러한 질문을 가지고 실험을 하면서, 컴파일러의 최적화와 JVM의 스택 머신 아키텍처 특징을 바이트코드와 그 연산 플로우에서 확인할 수 있었다. 이 글에서는 간단한 증감 연산자(i++) 실험을 통해 소스코드가 바이트코드로 변환될 때 발생하는 차이와 그 원리를 정리해본다. ...
요약 가상 스레드는 CPU성능을 높이는 기술이 아닌, 대기 중인 스레드의 점유 비용을 낮추는 동시성 모델이다. 스파이크 트래픽 환경에서 I/O-bound 작업은 가상 스레드로 인해 큐 대기와 p95 지연이 크게 줄어들었다. CPU-bound 작업에서는 스레드 모델 변경만으로 유의미한 성능 차이가 나타나지 않았다. 가상 스레드는 병목을 제거하기보다, 병목의 위치를 스레드에서 다른 계층(DB, 외부 API 등)으로 이동시킨다. 따라서 가상 스레드 도입은 성능 최적화 문제가 아니라, 백프레셔/타임아웃/제한 등의 설계를 포함한 시스템 설계 문제로 여겨야한다. 문제 ...
요약 문제 운영 환경에서 조회 API 응답 지연(1초 이상) 이슈 발생 상관 서브쿼리로 인한 N+1 패턴이 DB 레벨에서 발생 과정 실행 계획 분석을 통해 Nested Loop 반복 실행과 옵티마이저 통계 오류를 확인 상관 서브쿼리를 CTE + Hash Join 구조로 리팩토링 성과 서브쿼리 실행 92회에서 1회로 감소 총 실행 비용 246.0에서 40.6로 감소 (약 83% 개선, 6배 성능 향상) 운영 데이터 기준으로 실행 계획 변화와 성능 개선을 수치로 검증 문제 배경 운영 중인 서비스에서 특정 테넌트 기준 조회 API의 응답지연이 보고되었다. 슬로우 쿼리 분석 결과, 직원을 그룹화하는 테이블의 조회 쿼리 내부에 상관 서브쿼리(Correleated Subquery)가 포함되어있었다. 그 영향으로 메인 쿼리 결과 행 수만큼 서브쿼리가 반복 실행되는 문제가 발생했다. ...
문제 현상: 8개 Fargate Task 중 1개만 메모리 사용률이 지속 상승 (60% → 97%) 결과: 메모리 알람 후 해당 Task 재시작 특이점: 동일 코드 기반의 다른 Task들은 GC 정상 작동 환경: Java 21, Spring Boot, Kafka Consumer, MyBatis Batch, AWS Fargate 증상 분석 CloudWatch 메모리 그래프: 특정 Task만 계단식 상승 덤프 서버: 동일 이벤트 처리에도 톱니형 GC 패턴 (정상) GC 자체 문제라기보다는 객체 참조 유지로 인한 회수 불가 상황에 가까움 원인 분석 대량 청크 메시지 권한 갱신 이벤트를 한 번에 2,500건 단위로 Kafka에 게시 특정 Task가 해당 청크를 독점 처리하면 순간적으로 수만 개 객체 생성 중복 역직렬화 Consumer 내부에서 Map → DTO → VO 변환을 최대 세 번 반복 일시적 객체 폭증, Eden → Old Gen 승격 가속 MyBatis Batch 누적 ExecutorType.BATCH 사용 시 flushStatements() 전까지 파라미터 참조 유지 GC 입장에서는 여전히 “사용 중” 객체로 인식 컨슈머 병렬성 부족 단일 스레드 리스너로 동작 특정 파티션이 한 Task에 몰리면 부하 편향 및 메모리 사용량 집중 GC 관점에서 정리 구분 설명 GC는 참조가 끊긴 객체만 수거 리스트, 세션 등에서 참조 중이면 회수 불가 부하가 높으면 safepoint 진입 지연 GC 실행 타이밍이 밀리면서 Old Gen 누적 Old Gen 승격 가속 장수 객체가 많아질수록 Old Gen 압박 증가 덤프 서버에서 정상인 이유 부하가 낮아 GC 개입 여유가 충분 단기 개선 해결책 조치 설명 기대 효과 DTO 직접 바인딩 @KafkaListener에서 Map 대신 DTO로 수신 중복 역직렬화 제거 청크 분할 처리 2,500건 → 500건 단위로 분할 처리 동시 생성 객체 수 감소, 생존 시간 단축 리스트 참조 해제 처리 후 리스트 clear() 또는 참조 null GC 회수 가능 시점 앞당김 Batch flush 주기 조정 200건마다 flushStatements() 호출 MyBatis 내부 파라미터 참조 조기 해제 필드 누락 복구 누락된 accessType 필드 복원 불필요한 대량 delete 방지 장기 개선 해결책 Kafka Consumer 병렬성 향상 (factory.setConcurrency(2) 등) Partition assignment 전략을 cooperative-sticky로 조정 DLQ(Dead Letter Topic) 구성으로 재시도 루프 제거 Micrometer로 배치 크기, 처리 시간, lag 메트릭 수집 및 모니터링