입금 시스템 운영 안정성 개선
목차
입금 시스템 운영 안정성 개선 작업이었다. 규모가 크진 않았지만, 이런 류의 작업이 제일 신경 쓰인다. 기능 추가야 새 코드를 붙이면 그만인데, 기존 흐름 한가운데를 건드리는 수정은 영향 범위를 잘못 보면 운영에서 터진다.
이번엔 Spring MVC + MyBatis 구조에서 내부 클래스 쪽을 손댔다. 요청 파라미터 검증과 바인딩, 비즈니스 로직, SQL 매핑, JSP 응답 구성까지 이어지는 전형적인 레이어 흐름이다. 건드린 파일은 내부 클래스 하나였지만, 그 클래스가 요청 처리와 화면 제어를 같이 쥐고 있어서 양쪽 다 확인해야 했다.
이번에 실제로 챙긴 것들
트랜잭션 범위가 제일 먼저였다. DB를 건드리는 로직이 있으면 트랜잭션 어노테이션 범위를 명확히 잡아야 하는데, 레거시 코드에서 이게 흐릿하게 돼 있는 경우가 많다. 롤백이 필요한 단위와 실제 @Transactional이 걸린 단위가 다르면 부분 커밋이 발생한다. 특히 입금 도메인은 데이터 정합성이 민감하기 때문에 이 부분을 먼저 짚었다.
@Transactional(rollbackFor = Exception.class)
public DepositResult processDeposit(DepositRequest req) {
validate(req); // 검증 실패 시 예외 → 롤백
insertDepositLog(req); // 로그 먼저
updateBalance(req); // 잔액 갱신
return buildResult(req);
}
위처럼 범위를 명시적으로 잡지 않으면, RuntimeException 기본 롤백 정책만 믿게 되는데, checked exception을 던지는 외부 의존이 섞이면 조용히 커밋되고 넘어간다. rollbackFor = Exception.class를 달아두는 게 습관이 됐다.
예외 처리 전략은 항상 선택이다. RuntimeException을 위로 전파할지, 잡아서 응답으로 변환할지. 이번엔 사용자 요청 흐름에서 직접 노출되는 API였기 때문에 잡아서 변환하는 쪽을 택했다. 예외가 그대로 올라가면 500 응답이 내려가고 프론트에서 핸들링이 안 된다.
try {
return depositService.processDeposit(req);
} catch (DepositValidationException e) {
log.warn("입금 검증 실패 | userId={} reason={}", req.getUserId(), e.getMessage());
return DepositResult.fail(e.getMessage());
} catch (Exception e) {
log.error("입금 처리 중 예외 | userId={}", req.getUserId(), e);
return DepositResult.error("처리 중 오류가 발생했습니다.");
}
에러 메시지를 그대로 내리는 건 피했다. 내부 예외 메시지에 테이블명이나 쿼리 힌트가 담기는 경우가 있어서, 사용자한테 노출되면 안 된다.
로그는 운영 디버깅용으로 중요 입출력값을 INFO 레벨로 남겼다. WARN/ERROR만 챙기다 보면 정상 흐름 추적이 안 돼서 장애 발생 시 타임라인을 복원하기 어렵다. 다만 로그에 민감 데이터(계좌번호 전체, 금액 원본)가 그대로 박히면 보안 이슈가 된다. 마스킹하거나 ID 기반으로만 남기는 게 맞다.
SQL XML 관리와 MyBatis 매핑 주의점
MyBatis는 SQL을 XML로 관리하는 구조다 보니, 쿼리 변경이 컴파일 타임에 검증이 안 된다. 타입 오류, 컬럼명 오탈자는 런타임에서야 터진다. 이번 작업에서도 결과 매핑 쪽을 손댔는데, resultMap에 컬럼 추가 후 실제 DTO 필드가 없으면 그냥 null로 들어온다. 에러도 안 난다.
| 위험 포인트 | 증상 | 대응 |
|---|---|---|
| resultMap 컬럼 누락 | 필드가 null로 조용히 매핑됨 | DTO 필드명 대조 필수 |
| 파라미터 타입 불일치 | 런타임 BindingException | 로컬 전 케이스 돌려봐야 함 |
| 동적 쿼리 조건 누락 | 의도치 않은 전체 조회 | 조건 없는 경로 테스트 |
| 캐시 상태 오염 | 이전 쿼리 결과 재사용 | flushCache="true" 검토 |
MyBatis 쓸 때는 단위 테스트로 각 쿼리를 커버하는 게 이상적인데, 현실적으로 XML 쿼리 전체를 테스트로 감싸기 어려운 구조가 많다. 최소한 로컬에서 직접 케이스를 태워보는 수밖에 없다.
배포 순서와 CI/CD에서 자주 걸리는 것들
작업 후 순서는 늘 같다. 로컬 기동 확인 → 개발 DB 반영 → 배포. 이 순서를 건너뛰고 개발 DB 먼저 때린 뒤 로컬에서 맞춰보는 식으로 역방향으로 가다 보면 나중에 꼬인다.
CI/CD 파이프라인에서는 반복적으로 걸리는 지점들이 있다.
- SSH 타임아웃: 빌드가 길어지면 연결이 끊긴다.
ServerAliveInterval이나command_timeout명시 필요 - 키 인증 오류: 개행 문자가 붙어서 키가 깨지는 경우.
echo대신printf나 파일로 직접 넣어야 함 - Gradle 캐시 충돌: CI 환경에서 꼬이면
--no-daemon --rerun-tasks로 강제 재실행하거나 캐시 디렉토리 날려야 함 - 환경변수 누락: Secrets 설정 빠뜨리면 빌드는 성공해도 앱이 뜨지 않는다. 배포 성공 로그만 보고 넘어가면 뒤늦게 발견
배포 성공 여부를 프로세스 기동 로그까지 확인하는 습관을 들이는 게 중요하다. systemctl status나 앱 헬스체크 엔드포인트까지 찍어봐야 진짜 배포 성공이다.
작은 수정이라도 입금 같은 금전 도메인은 검증, 트랜잭션, 예외, 로그 네 가지를 빠뜨리지 않는 게 기본이다. 이게 습관으로 박히면 코드 리뷰할 때도 이 네 축으로 먼저 보게 된다.
댓글 0
첫 댓글 달아줘.