개발 slecs

자금흐름 증빙 기능 개선

목차

자금흐름 증빙 쪽 작업은 볼륨만 보면 별거 아니다 싶은데, 막상 손대면 생각보다 챙길 게 많다. 증빙이라는 도메인 특성상 데이터가 한 건이라도 꼬이면 재무 처리나 감사 대응 시점에 문제가 터지기 때문에, 코드 수정 자체보다 "운영에서 어떤 케이스가 실제로 발생하는가"를 먼저 파악하는 데 시간을 더 썼다.

이번 작업의 중심은 내부 클래스 수정이었다. Spring MVC 구조에서 내부 클래스는 컨트롤러 안에 요청 바인딩용 커맨드 객체를 두거나, 특정 처리 흐름을 묶어두는 용도로 자주 쓰이는 패턴인데, 시간이 지나면서 비즈니스 로직이 조금씩 섞여드는 경우가 많다. 이번도 그랬다. 뷰 바인딩 코드와 조회 로직이 같은 계층에 붙어 있어서, 어디서 어디까지가 책임인지 경계를 추적하는 것부터 시작했다.

파라미터 검증과 동적 SQL 매핑

요청 파라미터 검증은 단순해 보여도 자금 관련 화면에서는 빈 값이나 null을 그냥 흘려보내면 안 된다. 특히 기간 조건이 있는 쿼리는 시작일·종료일 두 값이 모두 없을 때의 기본값 처리를 명확히 해야 한다. 넣지 않으면 쿼리가 전체 데이터를 긁거나, 반대로 아무 결과도 안 나오거나 둘 중 하나다.

MyBatis XML에서 <if test="..."> 조건 분기로 동적 쿼리를 구성하는 건 흔한 방식인데, 분기가 쌓이면 실제로 실행되는 SQL이 어떻게 생겼는지 눈으로 추적하기 어려워진다. 로컬에서 MyBatis 로그 레벨을 DEBUG로 내리고 바인딩된 SQL이 의도대로 나오는지 확인하는 습관이 나중을 위해 중요하다.

<select id="selectFundFlowList" parameterType="map" resultType="FundFlowVO">
    SELECT
        flow_id, flow_date, amount, description, evidence_yn
    FROM fund_flow
    WHERE 1=1
    <if test="startDate != null and startDate != ''">
        AND flow_date &gt;= #{startDate}
    </if>
    <if test="endDate != null and endDate != ''">
        AND flow_date &lt;= #{endDate}
    </if>
    <if test="evidenceYn != null and evidenceYn != ''">
        AND evidence_yn = #{evidenceYn}
    </if>
    ORDER BY flow_date DESC
</select>

증빙 여부 필터(evidence_yn)가 없을 때 전체 조회가 되도록 설계한 게 이번 포인트였다. 운영에서 실제 사용 패턴을 확인해보면 "미증빙 건만 보기"가 주된 흐름이라서, 기본값을 Y나 N 중 하나로 고정하면 필터를 안 건 사용자는 원하지 않는 결과를 보게 된다. 파라미터가 없으면 전체를 주고, 값을 넣으면 필터링하는 방식이 UX상 자연스럽다.

결과 매핑 쪽도 확인했다. VO 필드명과 쿼리 컬럼명이 맞지 않을 때 MyBatis가 조용히 null로 채우는 케이스가 있어서, 신규 컬럼이 추가됐거나 alias를 건드린 경우엔 매핑 결과를 한 번 로그로 찍어보는 게 낫다.

트랜잭션 범위와 예외 처리 결정

이번에 가장 고민한 부분이다. 자금흐름 데이터는 조회 전용으로 보여도, 증빙 여부를 업데이트하는 흐름이 같은 서비스 안에 들어있을 수 있다. 조회와 업데이트가 혼재하는 서비스라면 @Transactional 범위를 어디까지 잡을지 명확히 해야 한다.

상황 적용 이유
순수 조회 메서드 @Transactional(readOnly = true) 불필요한 쓰기 락 방지
업데이트 포함 메서드 @Transactional 기본값 롤백 보장
외부 API 호출 포함 트랜잭션 밖으로 분리 DB 롤백해도 외부 상태는 되돌릴 수 없음

예외 처리는 두 갈래였다. RuntimeException을 그대로 위로 전파해서 글로벌 ExceptionHandler에서 통일 처리하거나, 서비스 레이어에서 잡아서 응답 코드로 변환하거나. 이번엔 후자를 택했다. 자금 도메인 특성상 에러 메시지가 화면에 그대로 노출되면 안 되고, 사용자에게는 정제된 문구만 보여줘야 했기 때문이다. 내부 로그엔 full stack trace를 남기고, 응답 객체엔 메시지만 담는 구조다.

try {
    fundFlowService.updateEvidenceYn(flowId, evidenceYn);
    result.put("result", "success");
} catch (Exception e) {
    log.error("[자금흐름 증빙 업데이트 실패] flowId={}, evidenceYn={}", flowId, evidenceYn, e);
    result.put("result", "fail");
    result.put("msg", "처리 중 오류가 발생했습니다.");
}
return result;

로그에 flowIdevidenceYn을 찍어두는 게 나중에 실제로 도움이 됐다. "몇 시에 어떤 건이 실패했는지" 추적이 가능해지니까. 입출력값을 INFO로 남기는 건 귀찮아 보여도 운영에서 디버깅 시간을 크게 줄여준다. 반대로 로그를 과하게 남기면 용량 문제가 생기니까, 핵심 식별자와 분기 결과값 정도만 찍는 게 균형이 맞다.

JSP 뷰 레이어는 이번에 응답 구성 쪽만 건드렸다. Spring MVC에서 JSP가 뷰를 담당하는 구조에서는 컨트롤러가 ModelAndView나 Model에 값을 담아 넘기는데, 화면에서 쓰는 변수명과 컨트롤러에서 addAttribute로 설정한 키가 일치해야 한다. 리팩토링하다가 키 이름이 바뀌면 JSP에서 EL 표현식이 조용히 빈 값을 출력하기 때문에, 변경 후 화면에서 실제로 값이 찍히는지 직접 확인하는 게 빠르다.

배포까지 가면서 겪은 잡음

코드 수정 자체보다 배포 과정에서 잡음이 있었다. 정리해두면 나중에 비슷한 상황에서 순서를 찾는 시간을 아낄 수 있으니.

  • SSH 타임아웃: Gradle 빌드가 길어지면 연결이 끊긴다. CI 설정에 command_timeout을 명시하지 않으면 기본값에 걸리는데, 빌드 로그가 어중간하게 잘려서 원인 파악하는 데 시간이 더 걸린다.
  • 키 인증 오류: SSH 키에 개행 문자가 들어가 있으면 인증이 안 된다. Secrets에 키를 붙여넣을 때 말미 개행이 포함됐는지 확인.
  • Gradle 캐시 충돌: 로컬에서 잘 돌다가 CI에서만 빌드가 깨지면 캐시 문제인 경우가 많다. --no-daemon 옵션이나 캐시 클리어로 대부분 해결된다. 근본적으론 캐시 키 전략을 명확히 잡는 게 맞지만.
  • 환경변수 누락: 배포 성공인데 앱이 뜨지 않으면 Secrets 설정을 먼저 본다. 에러 메시지 없이 조용히 죽어 있는 경우가 이 케이스다.

작업 후 로컬 기동 확인 → 개발 DB 반영 → 배포 순서로 진행했는데, 개발 DB 반영 단계에서 실제 데이터 형태가 로컬 더미 데이터와 달라서 쿼리 결과가 예상과 달랐던 게 한 번 있었다. 로컬 테스트가 전부가 아니라는 걸 이런 작업에서 다시 확인하게 된다.

작업 규모는 작았다. 그래도 자금 도메인 코드는 작은 수정이라도 검토 범위를 좁히면 안 된다고 생각한다. 기존 흐름과 호환이 되는지, 실제 운영 케이스에서 엣지 조건이 빠진 건 없는지, 로그가 나중에 디버깅에 충분한지. 이런 작은 확인들이 쌓여서 나중에 문제가 생겼을 때 추적이 가능해진다.

댓글 0

첫 댓글 달아줘.