CASE STUDY
한눈에 보는 변화
본사와 유럽법인에서 따로 운영하던 다운로드 기능을 Spring Boot 기반 단일 프로젝트로 합쳤습니다. 이 과정에서 다른 시스템과 고객이 사용하던 URL·파라미터·파일 구성은 유지하고, 요청마다 생성되던 DB 객체는 애플리케이션이 공용으로 관리하도록 바꿨습니다.
본사와 유럽법인에 비슷한 다운로드 기능이 서로 다른 방식으로 중복 구현되어 있었습니다. 요청마다 Connection Pool과 SessionFactory가 생성됐고, DB Link와 법인별 배포 방식에도 크게 의존했습니다. 이를 개선하면서 고객이 사용하던 URL·파라미터·파일명·압축 구성은 그대로 유지해야 했습니다.
직접 담당한 범위
- 본사 Model2와 유럽법인 Model1 구조의 AS-IS/TO-BE 및 Gap Analysis 작성
- 공통 구조 설계와 핵심 다운로드·검증·예외 처리 개발
- 기능테스트와 본사·유럽법인 별도 통합테스트, 단계별 배포와 운영 안정화 주도
| 구분 | 변경 전 | 변경 후 | 확인된 변화 |
|---|---|---|---|
| DB 객체 생명주기 | 요청마다 Connection Pool을 만들고 쿼리 경로에서 SqlSessionFactory를 반복 생성 | DB별 DataSource와 SqlSessionFactory를 애플리케이션 공용 객체로 관리 | 요청마다 생성되던 Connection Pool 제거 |
| DB 접근 경로 | 구형·신형 LIMS 접근과 DB Link가 코드 안에서 혼재 | 용도별 DataSource를 분리하고 필요한 구간은 직접 DB 접근으로 전환 | 접근 대상과 Pool 소유 관계 명확화 |
| 법인별 코드·배포 | 본사 Model2와 유럽법인 Model1에 유사 기능이 중복 | 애플리케이션 코드는 통합하고 Build Profile·설정·배포 흐름은 분리 | 본사·유럽법인 단일 프로젝트 |
| 성공·실패 처리 | 기능과 법인에 따라 성공·실패 화면 16개가 분산 | 메시지와 예외 전달 규칙을 정리해 공통 화면 2개로 통합 | 16개 → 2개 |
| 요청 호환·다국어 | 레거시 URL·법인별 언어 규칙과 하드코딩 메시지가 혼재 | 원 URL·법인 Profile로 Locale을 먼저 확정하고 DB MessageSource와 내부 Forward에 연결 | 기존 호출 URL을 유지한 Spring 요청 생명주기 |
| 입력·접근 검증 | Service마다 필수값·경로 검증이 반복되고 호출별 차이가 코드에 분산 | Validation Group을 메서드별로 조합하고 경로·주문 고객 검증은 별도 책임으로 분리 | 호출별 Validation Group을 선언적으로 구성 |
EVIDENCE CHAIN
문제를 좁힌 과정
- 01 · 현행 비교
Connection을 소유한 Pool의 생성 횟수까지 추적했습니다
DNA2에서 DB Connection 232개가 유지되는 상태를 확인하고, 본사 Model2·유럽법인 Model1의 DB 접근과 요청 처리를 기능별로 비교했습니다. 개별 쿼리 종료 여부만으로는 이 현상을 설명할 수 없었습니다.
판단Connection 한 개의 close 누락과 Connection을 소유하는 Pool 자체의 반복 생성을 분리해 확인해야 했습니다.
- 02 · 호출 경로 추적
요청과 쿼리 코드가 인프라 객체까지 만들고 있었습니다
요청 진입점부터 설정 로드, DataSource 생성, MyBatis 설정, SqlSessionFactory 생성과 쿼리 실행까지 역추적했습니다. 요청마다 Pool이 만들어지고 쿼리 경로에서 SqlSessionFactory가 다시 생성되는 것을 확인했습니다.
판단SqlSession을 닫아도 Connection은 새로 만든 Pool로 돌아갈 뿐이므로, Pool의 생명주기를 애플리케이션 단위로 올려야 했습니다.
- 03 · 변경 제약 수집
기존 호출 방식을 회귀 테스트 기준으로 만들었습니다
LIMS·COMS와 메일 링크가 사용하던 URL, 축약 파라미터, 파일명, 경로, 압축 구성과 예외 응답을 기능별로 목록화했습니다. 내부 명칭을 정리하더라도 외부에서 보내는 요청 형식은 그대로 유지해야 했습니다.
판단내부 구현이 바뀌어도 같은 요청에 같은 결과를 돌려주는지를 성공 기준으로 삼았습니다.
- 04 · 기능 검증
최초 548건 성공에서 멈추지 않고 차이가 난 9건을 분리했습니다
20개 기능테스트 시나리오, 557개 테스트 케이스를 실행해 최초 548건 성공과 9건 실패를 확인했습니다. 실패 항목을 기능별 결함으로 분리해 수정한 뒤 전체 557건을 다시 검증했습니다. 본사·유럽법인 통합테스트는 별도 시나리오로 수행했습니다.
판단기능테스트 557개 수치와 별도 통합테스트를 구분하고, 두 검증 범위를 모두 통과해야 전환을 완료할 수 있었습니다.
- 05 · 운영 안정화
배포 뒤 나타난 세 가지 문제를 따로 재현했습니다
대용량 ZIP 처리, 동일 파일명 충돌, 구형 JDBC Driver 선로딩이 차례로 나타났습니다. 압축 로직·파일 구성·JVM Class Loading을 각각 확인해 원인을 분리했습니다.
판단코드 전환을 마친 뒤에도 실제 실행 환경을 확인하고 운영 문제를 안정화해야 했습니다.
DECISION LOG
검토한 대안과 선택 근거
Connection close 경로만 보완
개별 세션이 반환돼도 요청마다 새 Pool이 만들어지는 생명주기 문제는 남기 때문입니다.
기존 DB Link 중심 접근 유지
실제로 어느 DB에 접근하는지 코드에서 알아보기 어렵고 법인별 의존성도 계속 남기 때문입니다.
법인별 Branch 분리
법인별 최적화는 쉽지만 공통 기능 Merge 충돌과 법인 추가 시 Branch 증가가 예상됐고, 현행 분석에서 본사·유럽 기능의 공통성이 더 높았기 때문입니다.
단일 소스 + Active Profile·Build Profile
공통 코드는 한 곳에서 유지하고 법인별 분기는 Active Profile로, 접속정보와 배포 산출물은 Build Profile로 분리할 수 있었습니다.
단일 코드 + 명시적 복수 DataSource
공통 로직을 한 곳에서 관리하면서 구형·신형 LIMS의 접근 대상과 인프라 객체 소유 관계를 명확히 할 수 있었습니다.
IMPLEMENTATION
설계와 구현
DB 인프라 생명주기
- 구형·신형 LIMS의 XA DataSource와 MyBatis 접근 계층을 각각 분리
- 조건에 따라 다른 Mapper를 사용하는 Service가 같은 @Transactional 선언을 쓰도록 Atomikos JTA 구성
- DB Link 의존 구간을 직접 DataSource 접근으로 전환하고 유럽 Profile에서는 미사용 LIMS 2.0 구성을 비활성화
URL·요청 호환 계층
- 레거시 화면 요청을 내부 Controller로 Forward하고 정적 리소스 경로를 신규 위치에 매핑
- HandlerMethodArgumentResolver가 레거시 축약 파라미터를 의미가 드러나는 DTO 필드로 변환
- 기존 파일명·압축 구성·다운로드 결과와 오류 응답을 회귀 기준으로 고정
Locale·메시지 생명주기
- 원 요청 URL과 법인 Profile로 Locale을 먼저 확정해 내부 Forward 전 언어 정보 보존
- LocaleContextHolder와 DB MessageSource를 Controller·Service·예외 처리·Bean Validation에 연결
- 운영은 메시지 사전 적재, 개발·로컬은 지연 적재를 적용하고 요청 종료 시 커스텀 ThreadLocal 제거
호출 맥락별 검증
- 동일 DTO의 필드 제약에 의미 단위 Validation Group을 부여하고 호출 메서드별로 조합
- Spring AOP가 Controller 인자에서 대상 DTO를 찾아 표준 Validator를 Group 순서대로 실행
- 파일 경로의 '..' 검사와 요청 고객·실제 주문 고객 일치 검증은 구조·필수값 검증과 분리
법인별 설정·배포
- 본사와 유럽법인 애플리케이션 코드를 하나의 프로젝트로 통합
- 법인별 설정, Build Profile과 Jenkins 배포 흐름은 독립적으로 유지
- 각 법인의 DB 접근 경로와 환경 차이는 설정으로 분리
다운로드·운영 안정화
- 파일명·압축 구성·시퀀스 길이 계산과 분석 결과 예외 기준 개선
- 유럽법인 자동발송 결과 압축에 필요한 결과 파일이 포함되도록 보완
- 대용량 압축 처리와 ZIP 내부 동일 파일명 충돌 보완
- JVM Class Loading 로그로 애플리케이션 의존성과 실제 JDBC Driver 차이 확인
- 본사 자동발송 모듈의 불필요한 구형 JDBC Driver 의존 제거
VERIFICATION
테스트 매트릭스
| 검증 영역 | 검증 방법 | 완료 기준 |
|---|---|---|
| 기존 호출 방식 | 기존 .jsp URL·축약 파라미터와 언어 경로를 동일하게 호출 | Forward 이후에도 본사·유럽법인별 다운로드·Locale·오류 결과 유지 |
| 입력·접근 검증 | API별 필수 Group, '..' 경로와 주문 고객 불일치 요청 확인 | 호출별 선택 필드는 허용하고 비정상 경로·고객 불일치는 업무 처리 전 차단 |
| 파일 결과 | 파일명·압축 구성·분석 결과 예외를 전후 비교 | 기존 사용자가 받는 결과 형식 유지 |
| DB 접근 | 구형·신형 LIMS 직접 연결과 기존 조회 결과 대조 | 조회 결과가 같고 요청을 반복해도 Pool이 추가로 생성되지 않음 |
| 기능테스트 | 20개 시나리오·557개 테스트 케이스 수행 | 최초 실패 9건 수정 후 557건 전체 통과 |
| 법인 통합 흐름 | 본사·유럽법인 통합테스트를 별도 시나리오로 수행 | 557개 기능테스트 수치와 분리해 연계·다운로드 흐름 확인 |
| 운영 안정화 | 단계별 배포 후 연결 수·로그·대용량 다운로드 관찰 | 요청마다 Pool을 만들지 않고 주요 다운로드 기능이 정상 동작 |
| 자동발송 모듈 후속 안정화 | 필수 결과 파일 포함과 JDBC 패키징 변경 관련 검증 | 자동발송 결과 구성 유지 및 불필요한 구형 JDBC Driver 의존 제거 |
OPERATION
운영 반영과 후속 안정화
대용량 ZIP 처리 지연
- 분리한 원인
- 일반 다운로드와 달리 압축 시간에 영향을 받는 타임아웃 문제로 분리
- 대응
- 대용량 압축 시간을 고려하도록 처리 흐름 보완
ZIP 내부 동일 파일명 충돌
- 분리한 원인
- 서로 다른 경로의 파일이 같은 이름으로 압축될 때 발생하는 충돌 재현
- 대응
- 압축 구성 단계의 파일명 충돌 처리 보완
JDBC API 실행 불일치
- 분리한 원인
- JVM Class Loading 로그로 JRE Extension 경로의 구형 Driver 선로딩 확인
- 대응
- 코드 결함과 운영 런타임 Driver 충돌을 분리해 대응
- 요청마다 만들던 Connection Pool과 SessionFactory를 애플리케이션 공용 객체로 전환
- 성공·실패 화면 16개를 공통 화면 2개로 통합
- 본사·유럽법인 유사 기능을 단일 프로젝트와 공통 코드로 재구성
- 20개 기능테스트 시나리오, 총 557개 테스트 케이스 통과
- 다운로드 시스템과 자동발송 모듈의 JDBC Driver 로딩·패키징 충돌 요인을 함께 제거
TAKEAWAYS
이 사례에서 드러난 역량
- Connection 수가 많을 때 close()만 찾지 않고 Pool과 SessionFactory가 몇 번 생성되는지 확인해야 합니다.
- 레거시 현대화에서는 내부 코드가 바뀐 뒤에도 기존 호출 방식과 결과가 유지되는지 확인해야 합니다.
- 공통 코드는 통합하더라도 법인별 설정과 배포 책임까지 억지로 하나로 묶을 필요는 없습니다.
- 운영 배포 뒤 나타난 문제는 전환 실패로 뭉뚱그리지 않고 압축·파일·런타임 문제로 나눠 확인해야 합니다.