트러블 슈팅

HikariCP 커넥션 고갈과 Broken Pipe 해결기 (@TransactionalEventListener활용)

7lyoung 2026. 2. 18. 16:21

오늘은 HikariCP 커넥션 풀에 관련하여 내가 마주한 문제에 대해 정리해볼 생각입니다.

 

배경 및 문제 상황

가상 화폐 거래소(Upbit) 클론 코딩 프로젝트를 진행하며, 핵심 로직인 [주문 -> 체결 -> 웹소켓 전송] 구간의 부하 테스트를 수행중에 대량의 트래픽을 발생시키자 서버 로그에 다음과 같은 경고와 에러가 발생했습니다.

  • Error Log: java.io.IOException: Broken pipe (클라이언트 연결 끊김)
  • Symptom: HikariCP 커넥션 풀 고갈 및 서버 응답 지연

 

 

원인 분석

로그 분석 및 코드 리뷰 결과, processTradeResults 메서드의 트랜잭션 범위가 지나치게 넓은 것이 원인이었습니다

  • 기존 로직: 체결 데이터 DB 저장 + 자산 정산(Update) + Redis 갱신 + 웹소켓 브로드캐스팅(전체 전송)
  • 문제점: 이 모든 과정이 단일 트랜잭션(@Transactional)으로 묶여 있음.
    1. 웹소켓 전송이나 Redis I/O가 지연되면 DB 커넥션을 반납하지 못하고 계속 점유
    2. 대기 중인 다른 요청들이 커넥션을 얻지 못해 HikariCP 풀이 고갈
    3. 응답이 늦어지자 클라이언트가 연결을 끊어버려 Broken pipe가 발생

 

해결 전략: 트랜잭션과 부가 기능의 분리

"DB 데이터 정합성이 중요한 작업(저장/정산)만 트랜잭션으로 묶고, 나머지는 트랜잭션이 끝난 뒤에 처리하자"는 전략을 세웠습니다.

이를 위해 스프링의 ApplicationEventPublisher@TransactionalEventListener를 도입했습니다.

 

리팩토링 과정

1. 이벤트를 담을 DTO 생성 (TradesCommitedEvent)

체결이 확정된 후, 후속 처리에 필요한 데이터(종목 ID, 체결 리스트)를 전달할 불변 객체(Record)를 생성.

public record TradesCommitedEvent(Long categoryId, List<TradeResponse> tradeResults) {
}

 

2. 트랜잭션 커밋 후 실행될 리스너 구현 (TradeAfterCommitHandler)

@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) 옵션을 사용하여, DB 트랜잭션이 성공적으로 커밋된 직후에만 실행되도록 설정했다.

  • 역할: Redis 캐시 갱신, 웹소켓 전송 트리거, 가격 변동 알림 발행
  • 장점: 이 로직들이 실패하거나 지연되어도, 이미 커밋된 체결/자산 데이터는 롤백안됨.(데이터 정합성 보장).
@Component
@RequiredArgsConstructor
public class TradeAfterCommitHandler {
    // ... 의존성 주입 ...

    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void onTradesCommitted(TradesCommitedEvent event) {
        // 1. Redis 최신가 업데이트 (실패해도 핵심 로직 영향 X)
        // 2. 웹소켓 전송 (Broadcaster 위임)
        // 3. 가격 알림 이벤트 발행
        
    }
}

 

3. 웹소켓 전송 로직 분리 (TradeMarketBroadcaster)

기존 서비스(TradeService)에 혼재되어 있던 시세 계산(고가/저가/거래량 등) 및 웹소켓 전송 로직을 별도의 컴포넌트로 분리했다.

  • 메모리 관리: ConcurrentHashMap을 사용하여 DB 조회 없이 실시간 시세를 메모리에서 관리하여 성능을 극대화.
  • 책임 분리: 서비스 계층은 '비즈니스 로직'에, 브로드캐스터는 '실시간 통신'에 집중.
@Component
@RequiredArgsConstructor
public class TradeMarketBroadcaster {
    // ... 메모리 맵 및 WebSocket 템플릿 ...

    public void updateMarketAndBroadcast(Long categoryId, TradeResponse response) {
        // 메모리 상에서 시세/캔들/호가 데이터 계산 후 웹소켓 전송
    }
}

 

결과 및 기대 효과

이 리팩토링을 통해 얻을 수 있는 이점은 명확하다.

  1. DB 커넥션 점유 시간 최소화: 트랜잭션 내부에는 오직 DB INSERT/UPDATE만 남게 되어, 커넥션이 매우 빠르게 반납, HikariCP 고갈 문제도 해소
  2. 응답 속도 향상: 클라이언트는 체결 처리가 끝나자마자 응답을 받게 되므로(웹소켓 전송은 비동기/후속 처리), Broken pipe 에러가 감소
  3. 결합도 감소: 핵심 비즈니스 로직과 부가 기능(알림, 소켓)이 분리되어 유지보수가 용이