개주 훈련일지/🏋️ 전집중 호흡 훈련

카카오페이 결제 승인 응답 전달 방식 개선(PRG)

lshfood2 2026. 1. 6. 22:39

카카오페이 결제 승인(Approve) 기능을 구현할 때,

“성공 결과를 어떻게 화면에 전달할 것인가”는

단순 UI 문제가 아니라 운영 안정성과 직결되는 문제이다.


이번 포스팅에서는 기존에 ApproveAction에서 성공 시

cashResult.jsp로 바로 forward 하던 방식을,

 

Session(Flash) 저장 + Redirect(PRG)

+ 결과 전용 .do 재진입 구조로 개선한 과정을 정리한다.


1. 기존 방식(Before) ApproveAction

→ request.setAttribute → cashResult.jsp forward

 

기존 흐름은 다음과 같았음.

  • /KakaoPayApprove.do 진입
  • 승인 처리(카카오 승인 API 호출, DB 반영 등)
  • request.setAttribute("result", ...) 로 결과 담기
  • cashResult.jsp 로 forward

장점

  • 구현이 빠르고 직관적임
  • request scope로 JSP에 값 전달이 간단함

문제점(운영 관점)

가장 큰 문제는 승인 처리 엔드포인트와

성공 화면이 같은 URL 컨텍스트에 묶여버린다는 점이다.

  • 사용자가 성공 화면에서 새로고침(F5) 하면,
    브라우저는 현재 주소(/KakaoPayApprove.do)를 다시 요청함
  • 즉 승인 로직이 다시 실행될 위험이 생김
    (중복 승인/중복 DB 반영/중복 로그 등)
  • “승인 처리(비즈니스)”와
    “결과 화면(뷰)”가 섞여 책임 분리가 약해짐
  • 결과 화면 진입 경로가 /KakaoPayApprove.do로 남아
    UX/디버깅 관점에서도 깔끔하지 않음

결제 같은 유료 기능에서는
“한 번만 처리되어야 하는 요청”이기 때문에,
새로고침/뒤로가기/재시도 같은 사용자 행동에 대해
구조적으로 방어하는 편이 안전하다.


2. 개선 목표: PRG(Post/Redirect/Get)
+ Flash(Session)로 “1회성 결과 전달”

 

개선 목표는 다음 2가지이다.

  1. 승인 처리 URL과 결과 화면 URL을 분리한다.
  2. 승인 결과는 딱 한 번만 보여주는
    1회성 데이터(Flash)로 전달한다.

3. 개선 방식(After): ApproveAction → session flash 저장
→ redirect → ResultAction에서 꺼내서 forward

 

▼ Before

/KakaoPayApprove.do (승인 처리)
  └ request.setAttribute(result)
  └ forward /cashResult.jsp
  └ (주소창은 여전히 /KakaoPayApprove.do)
  └ F5 → 승인 처리 재실행 위험

 

▼ After

/KakaoPayApprove.do (승인 처리)
  └ session.setAttribute(flash_result)
  └ redirect /cashResult.do

/cashResult.do (결과 화면 라우팅)
  └ session.getAttribute(flash_result)
  └ session.removeAttribute(flash_result)  // 1회성 보장
  └ request.setAttribute(result)
  └ forward /cashResult.jsp

핵심은 아래와 같다.

  • 승인 처리 결과는 request가 아니라 session(Flash)에 담는다.
  • 승인 처리 후에는 반드시 redirect로 결과 페이지로 이동한다.
  • 결과 페이지는 session에서 값을 꺼내 즉시 제거(remove) 한다.

4. 구현 코드 예시

 

4-1) ApproveAction

: 성공 결과를 “Flash 세션”에 담고 Redirect

// KakaoPayApproveAction.java (개선 후 예시)
HttpSession session = request.getSession();

// (1) 승인 처리 로직 수행
// - 카카오 승인 API 호출
// - DB 반영
// - 금액/주문 검증
// - (중복 승인 방지는 별도 Idempotency 로직으로 처리 가능)

// (2) JSP에 보여줄 결과 모델(가벼운 DTO/Map 권장)
CashResultDTO result = new CashResultDTO();
result.setStatus("SUCCESS");
result.setPartnerOrderId(partnerOrderId);
result.setApprovedAt(approvedAt);
result.setAmount(totalAmount);

// (3) 결과를 request가 아니라 "세션 Flash"로 전달
session.setAttribute("flash_cash_result", result);

// (4) PRG: 결과 화면은 Redirect로 분리
ActionForward forward = new ActionForward();
forward.setRedirect(true);
forward.setPath(request.getContextPath() + "/cashResult.do");
return forward;
여기서 포인트는 성공 화면을 보여주기 위해
승인 엔드포인트에 머무르지 않는다는 점이다.

4-2) CashResultAction

: Flash를 꺼내고 즉시 제거 → request로 옮겨 JSP forward

// CashResultAction.java
HttpSession session = request.getSession();

// (1) Flash 데이터 조회
CashResultDTO flash = (CashResultDTO) session.getAttribute("flash_cash_result");

// (2) 1회성 보장을 위해 즉시 제거
session.removeAttribute("flash_cash_result");

// (3) Flash가 없으면 직접 URL 접근/만료/중복 접근이므로 방어
if (flash == null) {
    // 선택 1) 마이페이지로 리다이렉트
    ActionForward f = new ActionForward();
    f.setRedirect(true);
    f.setPath(request.getContextPath() + "/myPage.do");
    return f;

    // 선택 2) 안내 페이지/에러 페이지로 이동도 가능
}

// (4) JSP 출력용으로는 request에 담는다
request.setAttribute("result", flash);

// (5) JSP로 forward (여기서는 단순 화면 렌더링만 담당)
ActionForward forward = new ActionForward();
forward.setRedirect(false);
forward.setPath("/cashResult.jsp");
return forward;

4-3) cashResult.jsp

: request에서만 읽기 (직접 접근 방어도 가능)

<%-- cashResult.jsp --%>
<c:if test="${empty result}">
  <script>
    alert('유효하지 않은 접근입니다.');
    location.href='${ctx}/myPage.do';
  </script>
</c:if>

<div>승인 상태: ${result.status}</div>
<div>주문번호: ${result.partnerOrderId}</div>
<div>결제금액: ${result.amount}</div>
<div>승인시간: ${result.approvedAt}</div>

5. 이 구조의 효과(정리)

1) 새로고침(F5) 안전성 상승

성공 화면의 URL이 /cashResult.do가 되므로,

새로고침은 “승인 재실행”이 아니라 “결과 화면 GET 재요청”이 된다.

 

게다가 Flash는 이미 제거되었기 때문에,

결과가 없으면 자연스럽게 방어 로직으로 빠진다.

 

2) 책임 분리

  • /KakaoPayApprove.do : 결제 승인 처리(비즈니스)
  • /cashResult.do : 결과 화면 라우팅(뷰 전용)

결제 안정성에서 이 분리는 생각보다 큰 차이를 만든다.

 

3) “1회성 결과” 보장

승인 결과는 본질적으로

“한 번만 보여주면 되는 데이터”인 경우가 많다.
Flash(Session) 패턴은 이 경우에 적합하다.


마무리

처음에는 '승인 성공하면 JSP에 결과만 보여주면 되지'

라고 생각했는데 결제 기능은 정상 동작보다

‘중복/재요청/새로고침’ 상황에서 

어떻게 버티는지가 더 중요하다는 것을 체감했다.


이번 개선은 단순 리팩토링이 아니라,

'승인 처리 흐름'을 운영 관점에서 구조적으로

더 안전하게 만든 경험이 되었다.