개발 slecs

회원 활동 내역에 결제·쿠폰·잔액 상태명 표시 추가

목차

관리자가 특정 회원의 행동 이력을 한 화면에서 조회할 수 있게 하는 작업이었다. 로그인·결제·쿠폰 사용·잔액 변동을 타임라인으로 보여주는 건데, 기능 자체보다 상태명 표시 처리가 이번 작업의 핵심이었다.

왜 이 기능이 필요했나

운영을 하다 보면 "이 회원 어제 쿠폰 썼나요?", "결제가 왜 취소됐는지 봐주세요" 같은 문의가 끊임없이 들어온다. 이걸 매번 DB 쿼리로 직접 뒤지거나, 결제·쿠폰·잔액 테이블을 각각 열어서 맞춰보는 건 비효율 그 자체다. CS 대응 속도도 느려지고, 중간에 실수도 생긴다.

활동 내역 기능은 그 분산된 이력을 한 곳에 모으는 역할이다. 기록 항목은 아래처럼 정리했다.

활동 타입 설명
로그인/로그아웃 접속 이력
결제/취소 거래 이력
쿠폰 사용 쿠폰 상태 변경
잔액 변동 충전/차감 이력

각 기능에서 처리가 완료될 때 기록 메서드를 호출하는 방식이라 중앙 로그 테이블에 자연스럽게 모인다. 구조 자체는 단순한데, 이렇게 하면 앞으로 활동 타입이 늘어날 때도 기록 계층을 건드리지 않고 호출만 추가하면 된다.

상태명 매핑이 생각보다 중요한 이유

DB에는 COUPON_STATUS = 'USED', PAY_STATUS = 'CANCEL' 같은 코드값이 저장된다. 이걸 그대로 화면에 뿌리면 관리자도 어색하고, CS 팀은 더 모른다. 읽기 좋은 레이블로 변환하는 매핑 로직이 필요하다.

문제는 이 매핑이 여러 도메인에 흩어지기 쉽다는 거다. 결제 쪽에 하나, 쿠폰 쪽에 하나, 잔액 쪽에 하나... 비슷한 로직이 각 서비스 레이어나 JSP에 복사되면서 나중에 레이블 하나 바꾸려면 파일 세 곳을 다 열어야 하는 상황이 된다.

이번에는 공통 브랜드 매핑 로직을 도입해서 일관되게 처리했다. 패턴은 대략 이렇다.

// StatusLabelMapper.java (예시 구조)
public class StatusLabelMapper {

    private static final Map<String, String> PAY_STATUS_MAP = Map.of(
        "COMPLETE", "결제완료",
        "CANCEL",   "취소",
        "PENDING",  "처리중"
    );

    private static final Map<String, String> COUPON_STATUS_MAP = Map.of(
        "UNUSED", "미사용",
        "USED",   "사용완료",
        "EXPIRED","만료"
    );

    public static String payStatus(String code) {
        return PAY_STATUS_MAP.getOrDefault(code, code); // 모르는 코드는 원값 그대로
    }

    public static String couponStatus(String code) {
        return COUPON_STATUS_MAP.getOrDefault(code, code);
    }
}

getOrDefault(code, code) 패턴을 쓴 건 이유가 있다. 매핑에 없는 코드가 들어왔을 때 예외를 던지거나 빈 문자열을 내려보내는 것보다, 원래 코드값을 그대로 보여주는 게 운영 상황에서 훨씬 안전하다. 적어도 뭔가 들어왔다는 건 알 수 있으니까.

개발하면서 챙긴 것들

Spring MVC + MyBatis 구조라서 내부 클래스는 요청 처리와 화면 제어, SQL은 XML에서 관리, JSP가 뷰 레이어로 HTML을 렌더링한다. 이 구조에서 이번 작업을 할 때 특히 신경 쓴 부분들을 정리해두자면:

트랜잭션 범위. 로그 기록 메서드를 기존 서비스 흐름 안에 끼워넣다 보면 트랜잭션 범위가 꼬이기 쉽다. 결제 처리와 활동 로그 기록이 같은 트랜잭션 안에 묶여 있으면, 로그 기록 실패가 결제 롤백을 일으킬 수 있다. 반대로 분리하면 결제는 성공했는데 로그가 빠지는 케이스가 생긴다. 어느 쪽을 선택할지는 "로그 누락 vs 주요 트랜잭션 영향" 중 뭘 더 감수하겠냐의 문제인데, 이번엔 주요 흐름 보호를 우선했다.

예외 처리. RuntimeException을 그대로 전파할지, 잡아서 응답 변환할지 선택이 필요했다. 매핑 로직이 실패해도 화면에 에러 페이지를 던지는 건 과하다. 레이블 변환 실패는 원값 그대로 보여주는 걸로 폴백했고, 예외는 로그로만 남겼다.

로그. 운영 환경에서 디버깅이 가능하도록 중요 입출력값은 INFO 레벨로 남겼다. 어떤 회원 ID로 어떤 활동 타입이 기록됐는지 정도는 남겨야 나중에 "왜 이 이력이 없냐"는 질문에 대응할 수 있다.

기존 흐름 영향. 기록 메서드를 추가한 지점들이 기존 로직에 사이드 이펙트를 주지 않는지 코드 리뷰 때 따라가봤다. 작업 규모가 크지 않을수록 이 추적이 허술해지기 쉬운데, 작은 변경이 오히려 더 조심해야 한다.

작업 후 로컬 기동 확인 → 개발 DB 반영 → 배포 순서로 처리했다. 뻔한 순서지만 이게 제일 안전하다.

규모가 큰 기능은 아니었는데, 이런 운영 편의성 작업이 쌓이면 실제로 CS 대응 속도가 달라진다. 매핑 로직 하나도 공통으로 뽑아두는 습관이 나중에 유지보수 비용을 줄인다는 걸 다시 확인한 작업이었다.

댓글 0

첫 댓글 달아줘.