자금흐름 투명성 증빙 기능 추가
목차
자금흐름 투명성 증빙 기능이라는 이름만 봐도 감이 오는 작업이다. 운영 중인 플랫폼에서 자금이 어떻게 이동했는지 추적 가능한 증빙 데이터를 남기는 기능 - 내부 감사, 정산 오류 추적, 분쟁 대응 어느 쪽에서 봐도 있어야 하는 기능인데 어느 순간 빠져 있는 경우가 많다. Spring MVC + MyBatis 스택에서 내부 클래스를 건드리는 작업이었고, 요청 파라미터 검증부터 SQL 쿼리, JSP 화면까지 일반적인 레이어를 전부 거쳤음.
자금흐름 증빙이 왜 별도 기능이 되는가
자금의 이동은 대부분 트랜잭션 레코드로 남는다. 그런데 "증빙"은 그보다 한 층 위에 있다. 트랜잭션 로그가 존재한다는 것과, 그 로그가 특정 시점 기준으로 누가 무엇을 승인했고 어떤 조건에서 집행됐는지를 구조화된 형태로 확인 가능하다는 건 다른 얘기다.
실운영에서 이런 요구가 생기는 경로는 대체로 세 가지다.
- 내부 감사팀이 특정 기간의 자금 이동 내역을 추적 가능한 형태로 요구할 때
- 정산 오류가 발생했고 어느 시점에 어떤 값이 들어왔는지 소급 확인이 필요할 때
- 외부 기관 제출용 자료가 필요한데 기존 화면이나 API만으로는 구성이 안 될 때
세 경우 모두 공통점이 있다 - 기능이 없으면 개발자가 직접 쿼리 짜서 뽑아주는 식으로 임시 대응하다가, 그게 반복되면 그때야 "기능으로 만들자"는 얘기가 나온다. 결국 기능은 기획 단계에서 생기는 게 아니라 운영 현장에서 필요성이 증명된 후 만들어지는 경우가 많고, 이번 작업도 그 흐름이었을 것.
구현할 때 실제로 신경 쓴 지점
파라미터 검증이 생각보다 중요하다. 자금 관련 기능에서 날짜 범위, 금액 범위, 계정 식별자 같은 파라미터는 null 체크 수준을 넘어서 의미적 유효성까지 봐야 함. 시작일이 종료일보다 나중이거나, 조회 범위가 비상식적으로 넓으면 그냥 통과시키면 안 된다. 운영 DB에 무거운 쿼리가 날아가는 상황을 막는 것도 검증 레이어의 역할이다.
MyBatis SQL XML에서 자금흐름 조회처럼 조건이 가변적인 경우 <if> 태그 남용이 시작된다. 이게 쌓이면 어떤 조건 조합에서 어떤 SQL이 실행되는지 추적이 힘들어짐. 조건이 4개를 넘기 시작하면 <choose>로 분기를 명시적으로 관리하거나, 아예 Mapper 메서드를 나누는 게 낫다.
<!-- 조건이 늘어날수록 실제 실행 SQL 추적이 어려워지는 구조 -->
<select id="selectFundFlowList" resultType="FundFlowDto">
SELECT * FROM fund_flow
<where>
<if test="startDate != null">AND txn_date >= #{startDate}</if>
<if test="endDate != null">AND txn_date <= #{endDate}</if>
<if test="accountId != null">AND account_id = #{accountId}</if>
<if test="minAmount != null">AND amount >= #{minAmount}</if>
</where>
</select>
트랜잭션 범위는 조회 기능이라면 크게 고민할 게 없는데, 증빙 요청 자체를 이력으로 남기는 로직이 붙으면 달라진다. "누가 언제 어떤 조건으로 조회했는지"를 별도 테이블에 insert하는 경우, 조회 트랜잭션과 이력 insert가 같은 트랜잭션에 묶이면 조회가 실패했을 때 이력도 안 남는다. 반대로 분리하면 조회 실패 후에도 이력만 남는 상황이 생길 수 있다. 어느 쪽이 맞냐는 요구사항에 달린 문제지만, 이 선택을 명시적으로 안 하면 나중에 "왜 이 케이스에서 이력이 없냐"는 문의가 꼭 들어온다.
예외 처리도 마찬가지. RuntimeException을 타고 올라가게 놔두면 컨트롤러에서 500으로 뭉개지는데, 자금 관련 기능에서 사용자에게 500 그대로 돌려주는 건 좋지 않다. 그렇다고 모든 예외를 잡아서 "처리 실패" 메시지로 바꾸면 운영에서 원인 추적이 힘들어진다. 최소한 로그에는 스택트레이스를 남기고, 사용자에게는 정제된 메시지를 돌려주는 구조가 필요함.
try {
List<FundFlowDto> result = fundFlowService.selectFundFlowList(param);
return ResponseEntity.ok(result);
} catch (InvalidParameterException e) {
log.warn("[FundFlow] 잘못된 파라미터 - {}", e.getMessage());
return ResponseEntity.badRequest().body(e.getMessage());
} catch (Exception e) {
log.error("[FundFlow] 조회 실패", e); // 스택트레이스 보존
return ResponseEntity.internalServerError().body("조회 중 오류가 발생했습니다.");
}
로그는 INFO 레벨로 중요한 입출력값을 남겨뒀다. 운영 환경에서 DEBUG는 대부분 꺼져 있어서, 재현이 어려운 케이스를 디버깅할 때 INFO 로그 외에 기댈 수 있는 게 없는 경우가 많다. 어떤 파라미터가 들어왔고 결과가 몇 건이었는지 정도만 남겨도 나중에 도움이 됨.
CI/CD 파이프라인에서 자주 걸리는 부분
로컬 기동 확인 → 개발 DB 반영 → 배포 순서는 맞는데, CI/CD 파이프라인이 끼면 변수가 늘어난다.
| 문제 | 원인 | 대응 |
|---|---|---|
| SSH 타임아웃 | 빌드 시간이 기본 연결 유지 시간 초과 | command_timeout 또는 timeout-minutes 명시 |
| Gradle 캐시 충돌 | 브랜치 간 캐시 키 공유 | 브랜치별 캐시 키 격리 또는 --no-daemon |
| SSH 키 인증 오류 | 키 등록 시 개행 문자 포함 여부 불일치 | 키 값 개행 처리 확인 |
| 배포 후 앱 미기동 | 환경변수 또는 Secrets 누락 | 기동 시 필수 변수 검증 로직 추가 |
SSH 타임아웃은 빌드가 길어질수록 자주 발생한다. Gradle 빌드가 의존성 다운로드부터 시작하면 10분을 넘기기 쉽고, 그 사이 SSH 연결이 끊김. timeout-minutes를 명시하지 않으면 기본값에 의존하게 되는데, 기본값이 환경마다 달라서 로컬에서는 안 터지고 CI에서만 터지는 상황이 생긴다.
환경변수 누락은 배포 성공 로그를 믿다가 당하는 케이스다. 빌드와 배포 단계는 정상적으로 완료됐는데 앱이 뜨지 않는 상황. 대부분 Secrets 설정이 빠져 있음. 배포 파이프라인에서 환경변수 목록을 관리하거나, 앱 기동 시 필수 변수 누락을 초기화 단계에서 명시적으로 예외로 던지는 로직을 두는 게 안전하다. 조용히 null로 뜨는 것보다 기동 실패로 즉시 알려주는 게 낫다.
작업 규모 자체는 크지 않았지만, 자금 관련 기능이라 검증과 예외 처리를 꼼꼼하게 챙겼다. 조회 기능 하나가 잘못 동작하면 정산 오류 추적이나 감사 대응 전체가 흔들릴 수 있어서다. 이런 기능일수록 예외 케이스를 뭉개지 않고 명시적으로 처리해두는 게 나중에 운영 안정성으로 돌아옴. 기존 흐름과의 호환성 확인도 수정한 로직이 다른 어디에 영향을 주지는 않는지 커밋 전에 한 번 더 추적하는 습관이 결국 불필요한 핫픽스를 줄여준다.
댓글 0
첫 댓글 달아줘.