메모리 사용률 그래프

Fat 메시지로 인한 Consumer 메모리 누수 케이스

문제 현상: 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 메트릭 수집 및 모니터링

November 23, 2025 · 2 min · 309 words · Gukin Han

근태 서비스에 이벤트 기반 아키텍처를 적용한다면 - Polling Adapter + Kafka

우리는 매일 아침 출근을 한다. 근태관리에서 출퇴근은 결국 돈과 관련되기 때문에 고용주/고용인 양쪽 입장에서 모두 중요하며 민감하게 생각한다. 과거에는 종이에 구멍을 뚫어(punch) 출퇴근 관리를 하였다. 언어란 신기하게도 현대 사회에서는 더 이상 사용하게 되지 않은 사물이나 행위를 나타내는 용어를 관습적으로 사용하는 경우가 많다. 여기서 얘기하는 펀치(punch)도 유사하게 출퇴근을 나타내는 용어로 여전히 사용 중이다. 펀치 인은 출근, 펀치 아웃은 퇴근. 아마 일부 외국 e-HR 시스템에서는 여전히 이러한 용어들을 사용하지 않을까? ...

September 5, 2025 · 7 min · 1441 words · Gukin Han
REDIS_SCHED 모드 아키텍처

트랜잭션에서 이벤트로 - Sync / Async / Redis 성능 비교와 TTV 분석

문제 @Transactional public void like(String loginId, Long productId) { // 1. 유저 조회 User user = userService.getByLoginId(loginId); // 2. 상품-좋아요 Insert boolean isInserted = productLikeRepository .insertIgnoreDuplicateKey(user.getUserId(), productId); if (!isInserted) return; // 3. 좋아요 수 집계 Update productRepository.incrementLikeCount(productId); } 좋아요 기능에서 처음엔 모든 로직을 하나의 트랜잭션 안에서 동기적(Sync)으로 처리 안정적이지만 트랜잭션 크기가 커지고, 상품 핫키에 경합으로 인한 병목이 발생 그래서, 이벤트 기반 설계를 통해 결합도를 낮추고(loosely coupled), 책임을 분리하려는 시도 Github PR : https://github.com/gukin-han/commercial-service/pull/7 ...

August 29, 2025 · 4 min · 743 words · Gukin Han
OFFSET 임계점에서 플랜 전환이 발생하는 그래프

정규화부터 캐싱까지 - 상품 목록 페이지네이션 최적화 과정

요약 정규화 쿼리(집계 + 정렬) → 풀스캔 + filesort로 ~17초 소요. 역정규화 + 정렬 포함 인덱스 → ~227ms (약 76배 향상). OFFSET 페이지네이션 → 인덱스 순차 스캔 시작 → 임계점에서 Table Scan + Filesort로 전환 → 이후 응답시간 급등. 전환 전 일정한 속도는 InnoDB 버퍼 풀/캐시 히트율 덕분. Keyset(Seek) 페이지네이션 → 깊은 페이지에서도 일정 성능 (421ms → 31ms, 92.6% 단축). 인덱스 설계는 핫 트래픽 패턴(브랜드 + 인기순/최신순/최저가순)에 맞춘 3개의 복합 인덱스 유지가 가성비 최적. 설계 원칙: 필터 선두 → 정렬키 → tie-breaker(id) 캐시(Cache-Aside) 적용 시 200RPS에서도 DB I/O 부하 없이 ms 단위 응답 가능. 단, TTL·Evict 정책·실시간성 요구사항·정합성 유지·스탬피드 방지 등 고려 필수. 결론: 정규화/역정규화 → 인덱스 설계 → 페이지네이션 전략 → 캐싱이 단계적으로 맞물려 성능을 좌우함. 문제 정의와 가설 웹 서비스에서는 쓰기 보다 읽기 트래픽이 압도적으로 많다는 글을 읽거나 실제로 경험해봤을 것이다. ...

August 15, 2025 · 16 min · 3283 words · Gukin Han

MySQL InnoDB에서 읽기·쓰기 충돌부터 deadlock 로그 분석까지

요약 MVVC는 트랜잭션 시작점을 기준으로 데이터 버저닝을 한다 락 종류에 따라 충돌 상황을 테스트 공유락은 말그대로 트랜잭션끼리 공유할 수 있다 베타락은 말그대로 하나의 트랜잭션만 가진다 공유락을 가진 상태로 업데이트, 삭제 등을 하면 베타락을 획득하려는 시도를 한다 락 획득 대기 사이클이 생기면 데드락이 발생한다 데드락을 최소화하기 위한 락 설계 방법: 락을 잡는 순서를 트랜잭션 마다 동일하게 유지 락 범위를 축소 첫 쿼리 부터 for update로 가져오는 방법 배경 이번 주차는 낙관적 락과 비관적 락을 학습하게 되었다. 비관적 락은 흔히 느리지만 정합성이 중요할때 사용하며, 낙관적 락은 충돌 가능성이 낮을 때 상대적으로 빠른 상황을 요구할때 사용한다고 한다. ...

August 8, 2025 · 15 min · 3089 words · Gukin Han

멱등성 키(Idempotency Key)의 이해와 기업 사례 정리

배경 멱등성(idempotency)은 단순히 “두 번 호출해도 한 번만 처리된다(Exactly-Once)“를 벗어나 시스템에 큰 이점을 제공한다. 우리 서비스에는 급여계산을 위한 근무시간을 산출할 때, 반복적으로 수행되어도 괜찮게 설계되어 있다. 반복적으로 요청이 들어왔을 때 결괏값이 addUp 하는 게 아니라 refresh 된다. 휴가계산도 동일하게 적용된다. 회사의 연차규정과 직원의 입사일에 따라 연차가 계산되는데 새로운 요구사항이 전달되어 수정하게 되었을때 계산결과가 잘못되어도 올바르게 계산되는 로직으로 수정해서 실행시키면 덮어씌워지게 된다. 물론 돈과 관련된 일이라 항상 조심하고 테스트를 잘 작성해야 한다. ...

July 25, 2025 · 4 min · 654 words · Gukin Han

좋은 테스트는 무엇인가? 레거시 시스템에 테스트 환경 구축

요약 반복적인 수동테스트로 불편함을 느껴서 테스트 환경을 구축하였습니다 구축하고 테스트를 작성하는 과정에서 구성 방식, 데이터 클린업 등에 대한 고민을 하였습니다 결과적으로 어떻게 설계하고 테스트를 작성할지 자신만의 기준을 정할 수 있게 되었습니다 수동 테스트의 반복 비용 마이클 페더는 “레거시 코드 활용 전략”에서 레거시 코드를 다음과 같이 정의했다: “테스트가 없는 코드는 곧 레거시 코드다” 테스트 코드가 없는 조직은 신기능 개발이나 이슈로 인한 코드 수정이 발생하면 일부 기능에 대한 수동 테스트를 진행하게 된다. 예를 들어, 연차를 생성하는 옵션에 요구사항이 바뀌면 연차 생성을 위한 옵션 설정을 하고 여러 직원, 근무형태 케이스에 대해 모두 테스트를 진행하게 된다. 아무래도 다양한 경우의 수를 확인하기 어렵기 때문에 숨어있는 모든 버그를 찾아내기란 쉽지 않았다. ...

July 18, 2025 · 6 min · 1251 words · Gukin Han

Java 스레드의 메모리 관리와 할당에 대한 이해

배경 Spring Boot에서는 Async 어노테이션으로 비동기 메서드를 쉽게 작성할 수 있다. 우리 팀에서 운영 중인 서비스에는 알림 생성 로직을 비동기로 실행하고 있다. 그 알림들 자체는 미래에 발생하기 때문에 동기적으로 작성할 필요가 없고, 유저들에게 응답속도를 높이기 위한 결정으로 보였다. Spring Boot의 코어 기능인 AutoConfiguration 덕분에 ThreadPoolTaskExecutor 빈도 자동으로 등록되지만, 실무에서는 IO-Bound, CPU-Bound 등 목적에 맞는 스레드 풀을 만들어 운영할 필요가 있다. 서로 다른 특성을 가진 작업을 혼합해서 사용하면 비효율이 발생하기 때문이다. CPU 사용이 낮은 IO-Bound 작업은 코어보다 많은 수의 스레드를 풀에 담아 사용하고, CPU 사용률이 높아 컨텍스트 스위칭 비용이 높은 CPU-Bound 작업은 코어 수에 비례해서 제한해야 한다. ...

May 4, 2025 · 5 min · 965 words · Gukin Han

Java enum을 활용한 전략 패턴(Strategy Pattern) 실무 적용기

B2B 서비스에서 새로운 고객사가 들어오거나 기존 기능을 수정 및 이슈 픽스 해야하는 상황일 때 복잡한 if-else 레거시가 유지보수 효율성 문제를 자주 발생시켰다. enum 방식의 전략 패턴을 도입했고, 각 로직에는 단위테스트를 쉽게 도입할 수 있었다. 결과적으로 회귀테스트 비용이 제로로 떨어졌고, 나중에 팀원이 동일한 작업을 진행하면서 추가 작업이 쉬워졌음을 느꼈다고 공유해주었다. 이 글에 사용된 코드는 실제 프로덕션 코드가 아닌, 핵심 구조만 재구성한 예시입니다. 유지보수 관점에서 if-else의 문제점 모든 if-else 구조가 나쁜 것은 아니다. 때론 간결하고 단순한 코드 방식이 더욱 유지보수에 유리한 경우가 있다. ...

April 25, 2025 · 4 min · 847 words · Gukin Han

DELETE-INSERT 패턴에서 발생하는 InnoDB Deadlock 분석

배경 최근 우리 서비스는 Sentry를 통해 데드락 알람을 자주 받고 있다 처음에는 중복 인덱스를 발견해서 제거하는 작업을 진행했다 그럼에도 불구하고 동일한 로직에서 데드락이 발생하는 중이다 조금은 빈도가 감소했을 수 있는데, 데드락 모니터링과 수집 및 통계화를 다른 회사에서는 어떻게 하는지 리서치가 필요해 보인다 아무튼 데드락을 분석해보니, 동일한 레코드의 DELETE-INSERT가 하나의 트랜잭션 내 수행되는 API에서 발생하였다 명확한 원인을 파악하고 전략을 세우기 위해 분석을 시도하였다 InnoDB Deadlock 분석 InnoDB 엔진 내부 상태를 확인하는 방법 RDMBS 클라이언트에서 위 명령어를 입력: ...

April 21, 2025 · 6 min · 1106 words · Gukin Han