카카오페이 결제 승인(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가지이다.
- 승인 처리 URL과 결과 화면 URL을 분리한다.
- 승인 결과는 딱 한 번만 보여주는
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에 결과만 보여주면 되지'
라고 생각했는데 결제 기능은 정상 동작보다
‘중복/재요청/새로고침’ 상황에서
어떻게 버티는지가 더 중요하다는 것을 체감했다.
이번 개선은 단순 리팩토링이 아니라,
'승인 처리 흐름'을 운영 관점에서 구조적으로
더 안전하게 만든 경험이 되었다.
'개주 훈련일지 > 🏋️ 전집중 호흡 훈련' 카테고리의 다른 글
| Git) Eclipse에서 Git 연결 후 커밋까지 진행해보기 (0) | 2026.02.03 |
|---|---|
| AniMale 서블릿 프로젝트를 Spring Boot(War)로 전환하는 과정 정리 (0) | 2026.02.03 |
| 이메일 인증 완료 후 비밀번호 변경까지 - Ajax 3단계 + 최종 ResetAction 적용기 (0) | 2025.12.31 |
| 비밀번호 찾기 UX 개선: 아이디 확인 → 이메일 자동 세팅 → 인증코드 발송(AJAX/Servlet) (0) | 2025.12.31 |
| 동기 검색/이동+비동기 페이지네이션 적용기 (0) | 2025.12.30 |