기록은 가장 쉬운 명상입니다.
Memodian

지표 결재 승인 기준일 계산 로직 – 단위 테스트 도입으로 검증 사이클 단축

10 min read · 2025년 12월 07일

“복잡한 결재 기준일 계산 로직을 단위 테스트로 전환하여, 기존 15분 걸리던 수동·배포 기반 테스트를 32초로 단축해 테스트 시간을 약 96% 절감했어요.”

  1. 배경 및 시스템 구조

    • 이상징후조기경보시스템(EWS)에서 지표 수정에 대한 결재요청을 올리면, 내부통제시스템(ICS)에서 승인/반려를 처리하는 구조.
    • 결재 승인 후 실제 상태 변경·집계 기준일 계산 로직은 EWS에 존재하지만, 결재 화면은 ICS에만 존재해 로컬 개발 환경에서는
      [결재요청 → 승인/반려 → 결과 확인] 전체 플로우를 직접 돌려볼 수 없는 상황이었어요.
    • 검증을 위해서는 항상 git push → PR/merge → Jenkins 빌드 → 서버 반영(8분) → 개발 서버에서 수동 테스트 를 거쳐야 했고,
      복잡한 “해당 월 다섯 번째 영업일” 계산 로직을 자주 수정·검증하기에는 비용이 너무 컸어요.
  2. 문제 정의

    • 결재 승인 시점 기준으로 “데이터 집계 기준일(적용일)”을 계산해야 하는데,
      • 해당 월의 다섯 번째 영업일 계산
      • 1일이 영업일인지/휴일인지에 따라 카운트 방식이 달라지는 예외 케이스
    • 이 로직이 서버에 배포된 후에야 실제 ICS 화면과 붙여 테스트할 수 있어,
      작은 수정에도 배포 전체 사이클을 반복해야 하는 비효율이 있었어요.
    • 폐쇄망 환경이라 외부 라이브러리 의존(Mock 프레임워크 도입 등)도 쉽지 않았어요.
  3. 해결 전략

    • 결재 플로우 전체를 E2E로 돌려보지 못하더라도,
      “결재 승인 이후 EWS 내부에서 돌아가는 계산 로직”만큼은 로컬 단위 테스트로 완전히 검증하는 것을 목표로 설정했어요.
    • ICS→EWS로 들어오는 입력 값과 외부 시스템 연동을 모두 가짜 객체로 대체하고,
      EWS 서비스 레이어의 핵심 메서드를 직접 호출하는 JUnit 테스트를 작성했어요.
    • 외부 의존성 처리 방식
      • 영업일 조회 API(WebClient 호출)는 FakeRestCallUtil로 mocking
      • 결재 건 유효성 검사 및 결재 대상 지표 조회(MyBatis Mapper)는 FakeAppvAcwMapper로 mocking
      • 폐쇄망이라 Mockito를 쓸 수 없어서, 기존 RestCallUtil, Mapper 인터페이스를 상속/구현한 Fake 클래스를 직접 작성해 동작을 시뮬레이션했어요.
  4. 구현 상세

    • 결재 승인(코드 “02”) 시나리오를 대상으로 JUnit 테스트 작성
      • 입력: 결재요청 종류 코드(“02”), 결재등록번호, 지표 변경 Task ID 등
      • 처리:
        1. FakeMapper가 “결재 대상 지표 존재” 응답 반환
        2. calculateAndSetAppltDate(..) 실행 시
          • FakeRestCallUtil이 영업일 조회 API를 대신해
            • /retrieve-service-day 호출 시: “해당 월 1일이 영업일인지 여부”를 응답
            • /retrieve-service-before-after 호출 시: “다섯 번째 영업일 날짜” 문자열 응답
        3. 최종적으로 지표의 AnmlyIndicAppltBasDd 필드가 기대한 기준일로 세팅되는지 검증
    • FakeRestCallUtil 구현 과정에서의 기술적 포인트
      • 기존 RestCallUtil이 생성자에서 WebClient를 주입받는 구조라,
        Fake 클래스에서 super(WebClient.builder().baseUrl("http://dummy").build()) 로 더미 WebClient를 생성해 전달.
        → 실제 HTTP 호출은 하지 않지만, 부모 클래스의 의존성 요구사항을 만족시키는 형태로 이해하고 설계.
      • callApi 메서드를 오버라이드하여 URL 패턴에 따라 서로 다른 ResponseMessage<T>를 반환.
        제네릭 타입 소거 때문에 컴파일러 경고가 발생해 @SuppressWarnings("unchecked") 를 사용했고,
        이를 통해 “테스트 코드 내부에서만 허용되는 제네릭 캐스팅”이라는 점을 명시적으로 표시했어요.
  5. 단위 테스트로 발견한 버그

    1. 기준일 계산 함수의 인자 형식 문제

      • 함수가 String fifthServiceDay 인자에 "yyyyMMdd" 포맷 값을 받도록 설계되어 있었는데,
        내부에서 LocalDate.parse(..) 시 포맷터를 잘못 지정해 parse 에러와 잘못된 날짜 계산이 발생 가능함을 테스트 중에 발견.
      • 포맷터를 "yyyy-MM-dd"에서 "yyyyMMdd"로 맞추고, 변환 책임을 한 곳으로 모아 재구성.
    2. 다섯 번째 영업일 계산 로직 오류

      • 초기 구현: “기준월 1일로부터 항상 4영업일 후”를 다섯 번째 영업일로 가정.
      • 문제: 기준월 1일이 비영업일(주말/공휴일)인 경우, 실제 다섯 번째 영업일이 달라진다는 점을 간과.
      • 테스트에서 “1일이 휴일인 달” 케이스를 추가하면서,
        “1일이 영업일인지 여부를 먼저 검사한 뒤 영업일 카운트를 시작해야 한다”는 요구사항을 도출하고 로직을 수정.
  6. 배포 이후 추가로 마주친 문제와 개선

    • 운영 배포 후, WebClient 응답을 DTO로 바로 캐스팅하는 부분에서
      java.util.LinkedHashMap cannot be cast to ServiceDayOutDTO 예외 발생.
    • 원인 분석
      • Spring WebClient + Jackson이 제네릭 타입 정보를 잃고 JSON을 역직렬화하면서
        ResponseMessage<List<ServiceDayOutDTO>> 대신 ResponseMessage<List<LinkedHashMap>>로 파싱.
    • 해결
      • callGatehub 대신 제네릭 타입을 명시할 수 있는 callApi(HttpMethod, String, Object, ParameterizedTypeReference<T>)를 사용하도록 수정.
      • new ParameterizedTypeReference<ResponseMessage<List<ServiceDayOutDTO>>>() {} 와 같이
        제네릭 정보를 런타임까지 유지하도록 변경해 LinkedHashMap 캐스팅 문제를 근본적으로 해결.
  7. 성과

    • 기존 검증 방식
      • 코드 수정 후 Jenkins 빌드 + 서버 반영 + 수동 시나리오 테스트까지 약 15분 소요.
    • 단위 테스트 도입 이후
      • 기준일 계산 관련 파라미터를 수정하고 테스트 실행까지 약 32초
        • 테스트 케이스 값 수정: 약 30초
        • 테스트 실행 시간: 약 1초
        • 약 96% 절감
    • 복잡한 날짜/영업일 계산 로직을 안전하게 리팩터링할 수 있는 기반이 생겼고,
      ICS–EWS 간 분리된 시스템 구조에서도 핵심 도메인 로직을 로컬에서 검증할 수 있는 패턴을 정립했어요.
  8. 배운 점

    • 시스템이 여러 프래그먼트(ICS/EWS)로 분리되어 있고, 실제 화면은 다른 시스템에 있더라도
      “한 시스템 내부에서 완결되는 도메인 로직”은 단위 테스트로 충분히 검증할 수 있다는 확신을 얻었어요.
    • WebClient, 제네릭, ParameterizedTypeReference, 타입 소거, @SuppressWarnings("unchecked") 등
      평소 잘 의식하지 못했던 Java/Spring의 타입 시스템 동작 방식을 실전 문제 해결 과정에서 자연스럽게 이해하게 되었어요.
    • 폐쇄망, 외부 라이브러리 제한 환경에서도
      “직접 Fake 객체를 만들어 기존 코드를 상속/구현하여 테스트하는 패턴”이 유효한 대안이 될 수 있음을 경험했어요.