외부 API·시스템 연계 · Architecture

반응마다 JVM을 종료하면 Connection Pool을 재사용할 수 없습니다

Spring Boot·MyBatis 커넥션 풀·직접 JDBC를 같은 조건에서 구현해 반복 비용을 비교했습니다. 반응마다 JVM이 종료되는 문제를 확인하고 연계를 상시 기동 REST API로 바꿨습니다.

JavaHikariCPREST APIProcess Lifecycle

쿼리보다 JVM 초기화 비용이 반복됐습니다

분석 시스템과 LIMS를 연결하는 초기안은 분석 반응마다 Java 모듈을 새로 실행하는 방식이었습니다. 모듈은 실행될 때 설정을 읽고 DB에 연결해 데이터를 처리한 뒤 종료됐습니다. 기존 분석 자체는 짧게 끝나는데 JVM 기동과 DB 연결 비용이 반응마다 반복되면 전체 처리량이 낮아질 수 있었습니다.

처음에는 커넥션 풀을 적용하면 연결 비용을 줄일 수 있다고 예상했습니다. 하지만 풀의 효과는 연결을 다음 요청에서도 재사용할 수 있을 때만 생깁니다. 프로세스가 한 번 처리한 뒤 종료된다면 풀도 함께 사라집니다.

세 가지 구현의 생명주기를 비교했습니다

세 가지 방식을 실제 서버에서 구현하고 실행 구간을 나눠 비교했습니다.

  • Spring Boot와 HikariCP
  • 비 Spring MyBatis 커넥션 풀
  • 직접 JDBC 연결

비교 구간은 애플리케이션 초기화, 연결 획득, 쿼리 실행, 결과 매핑으로 나눴습니다. 쿼리 시간만 보는 대신 요청 한 건이 프로세스 시작부터 결과 반환까지 거치는 단계를 확인했습니다.

커넥션 풀이 해결하지 못한 이유

반응마다 새로운 JVM이 시작되면 풀은 빈 상태에서 생성됩니다. 첫 연결을 만든 뒤 재사용할 다음 요청이 오기 전에 프로세스가 종료되므로, 풀 초기화 비용은 추가되지만 재사용 이점은 얻기 어렵습니다.

반응마다 실행되는 모듈은 요청이 끝날 때 JVM과 풀도 함께 종료됐고, 상시 기동 애플리케이션에서는 여러 요청이 같은 풀을 재사용할 수 있었습니다. 특정 성능 배율 대신 이 생명주기 차이를 설계 기준으로 삼았습니다.

비 Spring 모듈은 정적 SqlSessionFactoryPooledDataSource를, Spring Boot 애플리케이션은 HikariDataSource 빈을 사용했습니다. 두 구조의 생명주기를 단순화하면 다음과 같습니다.

[반응마다 실행되는 모듈]
JVM 시작
  → static SqlSessionFactory / PooledDataSource 생성
  → Connection 획득 → Query → Result Mapping
  → 모듈 종료와 함께 JVM·Pool 종료

[상시 기동 REST 애플리케이션]
애플리케이션 시작
  → HikariDataSource Bean 생성
  → 요청 1: Pool에서 Connection 대여·반환
  → 요청 2: 같은 Pool에서 Connection 대여·반환
  → 애플리케이션 종료 시 Pool 종료

static이나 싱글턴은 같은 JVM 안에서만 생명주기를 공유합니다. 반응마다 JVM 자체가 끝나는 구조에서는 정적 풀도 다음 반응까지 남지 않습니다. 반대로 상시 기동 구조에서는 풀이 요청보다 오래 살아야 여러 요청이 같은 연결을 재사용할 수 있습니다.

판단을 바꾼 지점

Spring Boot, MyBatis 풀, 직접 JDBC 중 어느 구현이 조금 더 빠른지를 고르는 것으로는 더 근본적인 반복 초기화 비용을 제거할 수 없었습니다. 그래서 라이브러리 선택 문제가 아니라 프로세스 생명주기 문제로 다시 정의했습니다.

선택한 방향은 LIMS 연계 기능을 상시 기동 애플리케이션으로 운영하고, 분석 시스템이 REST API를 호출하도록 바꾸는 것이었습니다. JVM과 풀을 유지한 채 여러 요청을 처리하므로 초기화와 연결 생성 비용을 요청마다 반복하지 않습니다.

연계를 상시 기동 REST API로 바꿨습니다

API는 다음 기능을 제공하도록 구성했습니다.

  • 분석 결과와 관련 데이터를 저장하는 API
  • 분석에 필요한 샘플 정보를 조회하는 API
  • 분석 실패 상태와 원인을 전달하는 API
  • 요청값 검증, 토큰 인증, 표준 예외 응답과 요청·응답 이력 저장
  • 본사·유럽·일본 프로필과 DBMS별 매퍼 분리

결과, 결과 파일, 신호 정보, 반응 상태, 주문별 집계, 리포트와 변경 이력은 하나의 서비스 트랜잭션에서 처리했습니다. 기존 데이터 존재 여부에 따른 삽입·수정 분기도 API 안으로 모았습니다.

아래 코드는 성공 결과를 저장하는 서비스의 핵심 처리 순서를 축약한 예시입니다. 결과 묶음에는 결과, 결과 파일과 추가 결과 정보가 포함됩니다.

@Transactional(rollbackFor = Exception.class)
public void saveAnalysisResult(ResultRequest request) {
    if (resultRepository.exists(request.resultKey())) {
        updateResultBundle(request);
    } else {
        insertResultBundle(request);
    }

    reactionRepository.updateStatus(request.reaction());
    orderAggregateRepository.update(request.orderKey());
    reportRepository.delete(request.orderKey());
    reportRepository.insert(request.orderKey());
    historyRepository.record(request.history());
}

실패 결과는 같은 서비스의 별도 트랜잭션 메서드에서 상태와 원인을 기록했습니다. 토큰은 컨트롤러에 도달하기 전에 필터에서 확인했고, DTO 형식은 요청을 받는 단계에서 검증했습니다. 결과·상태·집계·리포트·이력의 원자성은 서비스 트랜잭션이 보장했습니다. 외부 분석 시스템의 처리까지 같은 트랜잭션으로 묶은 것은 아닙니다.

적용 결과

반응마다 JVM과 DB 연결을 다시 준비하던 방식을 없애고, 분석 시스템과 LIMS 사이의 결과·오류·코멘트 전달을 상시 기동 REST API로 모았습니다. AS-IS 분석부터 연계 방식 검증, LIMS API와 오류 처리 구현, 법인 환경 적용까지 이어서 진행했습니다.

정리

커넥션 풀은 자신을 만든 프로세스보다 오래 살 수 없습니다. JVM이 매번 종료되면 풀 구현을 바꿔도 다음 요청에 연결을 재사용할 수 없습니다. 쿼리와 함께 프로세스 시작부터 종료까지 측정해야 실행 방식을 바꿔야 할 지점을 찾을 수 있습니다.

TECHNICAL SERIES

CIWER 연계 구조와 운영 안정화

일회성 프로세스를 상시 기동 API로 바꾸고 호출자가 실패를 구분할 수 있도록 오류 응답을 보완한 기록입니다.

시리즈 전체 보기

현재 1/2

  1. 1반응마다 JVM을 종료하면 Connection Pool을 재사용할 수 없습니다
  2. 2실패했는데 HTTP 200을 반환하면 안 되는 이유