기록은 가장 쉬운 명상입니다.
지표 결재 승인 기준일 계산 로직 – 단위 테스트 도입으로 검증 사이클 단축
10 min read · 2025년 12월 07일
“복잡한 결재 기준일 계산 로직을 단위 테스트로 전환하여, 기존 15분 걸리던 수동·배포 기반 테스트를 32초로 단축해 테스트 시간을 약 96% 절감했어요.”
-
배경 및 시스템 구조
- 이상징후조기경보시스템(EWS)에서 지표 수정에 대한 결재요청을 올리면, 내부통제시스템(ICS)에서 승인/반려를 처리하는 구조.
- 결재 승인 후 실제 상태 변경·집계 기준일 계산 로직은 EWS에 존재하지만, 결재 화면은 ICS에만 존재해 로컬 개발 환경에서는
[결재요청 → 승인/반려 → 결과 확인]전체 플로우를 직접 돌려볼 수 없는 상황이었어요. - 검증을 위해서는 항상
git push → PR/merge → Jenkins 빌드 → 서버 반영(8분) → 개발 서버에서 수동 테스트를 거쳐야 했고,
복잡한 “해당 월 다섯 번째 영업일” 계산 로직을 자주 수정·검증하기에는 비용이 너무 컸어요.
-
문제 정의
- 결재 승인 시점 기준으로 “데이터 집계 기준일(적용일)”을 계산해야 하는데,
- 해당 월의 다섯 번째 영업일 계산
- 1일이 영업일인지/휴일인지에 따라 카운트 방식이 달라지는 예외 케이스
- 이 로직이 서버에 배포된 후에야 실제 ICS 화면과 붙여 테스트할 수 있어,
작은 수정에도 배포 전체 사이클을 반복해야 하는 비효율이 있었어요. - 폐쇄망 환경이라 외부 라이브러리 의존(Mock 프레임워크 도입 등)도 쉽지 않았어요.
- 결재 승인 시점 기준으로 “데이터 집계 기준일(적용일)”을 계산해야 하는데,
-
해결 전략
- 결재 플로우 전체를 E2E로 돌려보지 못하더라도,
“결재 승인 이후 EWS 내부에서 돌아가는 계산 로직”만큼은 로컬 단위 테스트로 완전히 검증하는 것을 목표로 설정했어요. - ICS→EWS로 들어오는 입력 값과 외부 시스템 연동을 모두 가짜 객체로 대체하고,
EWS 서비스 레이어의 핵심 메서드를 직접 호출하는 JUnit 테스트를 작성했어요. - 외부 의존성 처리 방식
- 영업일 조회 API(WebClient 호출)는
FakeRestCallUtil로 mocking - 결재 건 유효성 검사 및 결재 대상 지표 조회(MyBatis Mapper)는
FakeAppvAcwMapper로 mocking - 폐쇄망이라 Mockito를 쓸 수 없어서, 기존
RestCallUtil, Mapper 인터페이스를 상속/구현한 Fake 클래스를 직접 작성해 동작을 시뮬레이션했어요.
- 영업일 조회 API(WebClient 호출)는
- 결재 플로우 전체를 E2E로 돌려보지 못하더라도,
-
구현 상세
- 결재 승인(코드 “02”) 시나리오를 대상으로 JUnit 테스트 작성
- 입력: 결재요청 종류 코드(“02”), 결재등록번호, 지표 변경 Task ID 등
- 처리:
- FakeMapper가 “결재 대상 지표 존재” 응답 반환
calculateAndSetAppltDate(..)실행 시- FakeRestCallUtil이 영업일 조회 API를 대신해
/retrieve-service-day호출 시: “해당 월 1일이 영업일인지 여부”를 응답/retrieve-service-before-after호출 시: “다섯 번째 영업일 날짜” 문자열 응답
- FakeRestCallUtil이 영업일 조회 API를 대신해
- 최종적으로 지표의
AnmlyIndicAppltBasDd필드가 기대한 기준일로 세팅되는지 검증
- FakeRestCallUtil 구현 과정에서의 기술적 포인트
- 기존
RestCallUtil이 생성자에서WebClient를 주입받는 구조라,
Fake 클래스에서super(WebClient.builder().baseUrl("http://dummy").build())로 더미 WebClient를 생성해 전달.
→ 실제 HTTP 호출은 하지 않지만, 부모 클래스의 의존성 요구사항을 만족시키는 형태로 이해하고 설계. callApi메서드를 오버라이드하여 URL 패턴에 따라 서로 다른ResponseMessage<T>를 반환.
제네릭 타입 소거 때문에 컴파일러 경고가 발생해@SuppressWarnings("unchecked")를 사용했고,
이를 통해 “테스트 코드 내부에서만 허용되는 제네릭 캐스팅”이라는 점을 명시적으로 표시했어요.
- 기존
- 결재 승인(코드 “02”) 시나리오를 대상으로 JUnit 테스트 작성
-
단위 테스트로 발견한 버그
-
기준일 계산 함수의 인자 형식 문제
- 함수가
String fifthServiceDay인자에"yyyyMMdd"포맷 값을 받도록 설계되어 있었는데,
내부에서LocalDate.parse(..)시 포맷터를 잘못 지정해 parse 에러와 잘못된 날짜 계산이 발생 가능함을 테스트 중에 발견. - 포맷터를
"yyyy-MM-dd"에서"yyyyMMdd"로 맞추고, 변환 책임을 한 곳으로 모아 재구성.
- 함수가
-
다섯 번째 영업일 계산 로직 오류
- 초기 구현: “기준월 1일로부터 항상 4영업일 후”를 다섯 번째 영업일로 가정.
- 문제: 기준월 1일이 비영업일(주말/공휴일)인 경우, 실제 다섯 번째 영업일이 달라진다는 점을 간과.
- 테스트에서 “1일이 휴일인 달” 케이스를 추가하면서,
“1일이 영업일인지 여부를 먼저 검사한 뒤 영업일 카운트를 시작해야 한다”는 요구사항을 도출하고 로직을 수정.
-
-
배포 이후 추가로 마주친 문제와 개선
- 운영 배포 후, WebClient 응답을 DTO로 바로 캐스팅하는 부분에서
java.util.LinkedHashMap cannot be cast to ServiceDayOutDTO예외 발생. - 원인 분석
- Spring WebClient + Jackson이 제네릭 타입 정보를 잃고 JSON을 역직렬화하면서
ResponseMessage<List<ServiceDayOutDTO>>대신ResponseMessage<List<LinkedHashMap>>로 파싱.
- Spring WebClient + Jackson이 제네릭 타입 정보를 잃고 JSON을 역직렬화하면서
- 해결
callGatehub대신 제네릭 타입을 명시할 수 있는callApi(HttpMethod, String, Object, ParameterizedTypeReference<T>)를 사용하도록 수정.new ParameterizedTypeReference<ResponseMessage<List<ServiceDayOutDTO>>>() {}와 같이
제네릭 정보를 런타임까지 유지하도록 변경해 LinkedHashMap 캐스팅 문제를 근본적으로 해결.
- 운영 배포 후, WebClient 응답을 DTO로 바로 캐스팅하는 부분에서
-
성과
- 기존 검증 방식
- 코드 수정 후 Jenkins 빌드 + 서버 반영 + 수동 시나리오 테스트까지 약 15분 소요.
- 단위 테스트 도입 이후
- 기준일 계산 관련 파라미터를 수정하고 테스트 실행까지 약 32초
- 테스트 케이스 값 수정: 약 30초
- 테스트 실행 시간: 약 1초
- 약 96% 절감
- 기준일 계산 관련 파라미터를 수정하고 테스트 실행까지 약 32초
- 복잡한 날짜/영업일 계산 로직을 안전하게 리팩터링할 수 있는 기반이 생겼고,
ICS–EWS 간 분리된 시스템 구조에서도 핵심 도메인 로직을 로컬에서 검증할 수 있는 패턴을 정립했어요.
- 기존 검증 방식
-
배운 점
- 시스템이 여러 프래그먼트(ICS/EWS)로 분리되어 있고, 실제 화면은 다른 시스템에 있더라도
“한 시스템 내부에서 완결되는 도메인 로직”은 단위 테스트로 충분히 검증할 수 있다는 확신을 얻었어요. - WebClient, 제네릭, ParameterizedTypeReference, 타입 소거, @SuppressWarnings("unchecked") 등
평소 잘 의식하지 못했던 Java/Spring의 타입 시스템 동작 방식을 실전 문제 해결 과정에서 자연스럽게 이해하게 되었어요. - 폐쇄망, 외부 라이브러리 제한 환경에서도
“직접 Fake 객체를 만들어 기존 코드를 상속/구현하여 테스트하는 패턴”이 유효한 대안이 될 수 있음을 경험했어요.
- 시스템이 여러 프래그먼트(ICS/EWS)로 분리되어 있고, 실제 화면은 다른 시스템에 있더라도