

🌻 각 파일의 역할 정리 🌻
KakaoPayConfig파일 : Admin Key, CID 등 설정 관리
KakaoPayReadyAction : 구매 요청을 생성하고 카카오페이 구매 페이지로 이동
KakaoPaySuccessAction : 구매 완료 후 승인 처리를 시작하는 역할
KakaoPayApproveUtil : 카카오페이 구매 승인 API를 실제로 호출
KakaoPayFailAction : 구매 실패 시 페이지 이동
🌻 KakaoPayConfig.java 🌻
package controller.kakaopay;
// 카카오페이 연동에 필요한 설정 정보 관리 : Admin Key, CID 등
public class KakaoPayConfig {
// 우리 서버의 기본 주소
public static final String BASE_URL = "http://localhost:8088";
// 테스트용 상점 코드 : 테스트 CID (1회성 결제)
public static final String CID = [ 상점 코드 ];
// Secret Key (개발용)
public static final String SECRET_KEY_DEV = [ Secret Key ];
// 결제 준비(Ready) API 주소
// 결제 요청 정보를 카카오페이에 전달하여
// TID와 결제 페이지 URL을 발급받기 위한 API
public static final String READY_URL = "https://kapi.kakaopay.com/v1/payment/ready";
// 결제 성공 시 카카오페이가 리다이렉트하는 우리 서버의 URL
// 사용자가 결제 완료/실패/취소 시 이동하는 주소
public static final String SUCCESS_URL = BASE_URL + "/BugSandwichOrnamentMall/kakaoPaySuccess.do";
public static final String FAIL_URL = BASE_URL + "/BugSandwichOrnamentMall/kakaoPayFail.do";
public static final String CANCEL_URL = BASE_URL + "/BugSandwichOrnamentMall/kakaoPayCancel.do";
}
Admin Key, CID 등 설정 관리를 하는 파일
🌻 KakaoPayReadyAction.java 🌻
package controller.kakaopay;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.util.ArrayList;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import javax.websocket.Session;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import controller.common.Action;
import controller.common.ActionForward;
import model.dao.CartDAO;
import model.dao.ItemDAO;
import model.dao.OrderDAO;
import model.dto.CartDTO;
import model.dto.ItemDTO;
import model.dto.OrderDTO;
// 결제 준비 (ready API 호출)
// 카카오페이 API 호출 (결제 준비)
// 성공적인 응답을 받으면 TID와 함께 리다이렉트 URL 반환
// 클라이언트에게 결제 페이지로 리다이렉션
//// 필요한 정보: Admin Key, CID, 결제 금액, 주문번호, 상품명 등
public class KakaoPayReadyAction implements Action {
@Override
public ActionForward execute(HttpServletRequest request, HttpServletResponse response) {
// 카카오페이 API 호출 코드
// 결제 준비 요청
// 응답에서 TID 받아서 리다이렉트 URL 생성
System.out.println("[로그] KakaoPayReadyAction 시작");
HttpSession session = request.getSession();
ActionForward forward = new ActionForward();
try {
// 세션과 요청에서 정보 가져오기
int accountPk = (Integer) session.getAttribute("accountPk");
int addressPk = Integer.parseInt(request.getParameter("addressPk"));
int totalAmount = Integer.parseInt(request.getParameter("totalAmount"));
System.out.println("accountPk" + accountPk);
System.out.println("addressPk" + addressPk);
System.out.println("totalAmount" + totalAmount);
if (totalAmount <= 0) {
throw new RuntimeException("결제 금액 오류");
}
ItemDAO itemDAO = new ItemDAO();
CartDAO cartDAO = new CartDAO();
// 즉시 구매 여부
CartDTO tempCartDTO = (CartDTO) session.getAttribute("tempCartDTO");
// 상품명 설정
String itemName = "";
// =========================
// 즉시 구매
// =========================
if (tempCartDTO != null) {
int itemPk = tempCartDTO.getItemPk();
int count = tempCartDTO.getCount();
// 1. 재고 체크
ItemDTO itemDTO = new ItemDTO();
itemDTO.setItemPk(itemPk);
itemDTO.setItemCount(count);
itemDTO.setCondition("ITEM_STOCK_ENOUGH");
ItemDTO itemData = itemDAO.selectOne(itemDTO);
if (itemDAO.selectOne(itemDTO) == null) {
System.out.println("[로그] 즉시 구매 재고 부족");
request.setAttribute("message", "해당 상품의 재고가 부족합니다.");
request.setAttribute("location", "ornamentDetailPage.do?itemPk=" + itemDTO.getItemPk());
forward.setPath("message.jsp");
forward.setRedirect(false);
return forward;
}
System.out.println("[로그] 즉시 구매 재고 감소 성공");
itemName = itemData.getItemName(); // itemName
// 2. 재고 임시 감소
itemDTO.setCondition("BUY_ITEM");
if (!itemDAO.update(itemDTO)) {
throw new RuntimeException("재고 선점 실패");
}
// 3. 선점 정보 세션 저장 (복구용)
session.setAttribute("reservedItemPk", itemPk);
session.setAttribute("reservedCount", count);
}
// =========================
// 장바구니 구매
// =========================
else {
CartDTO cartDTO = new CartDTO();
cartDTO.setAccountPk(accountPk);
cartDTO.setCondition("SELECT_ALL_ACCOUNT_CART");
// 장바구니 상품
ArrayList<CartDTO> cartDatas = cartDAO.selectAll(cartDTO);
// 장바구니에 재고가 부족한 상품
ArrayList<String> outOfStockItems = new ArrayList<>();
// 1. 재고 체크
for (CartDTO cart : cartDatas) {
ItemDTO itemDTO = new ItemDTO();
itemDTO.setItemPk(cart.getItemPk());
itemDTO.setItemCount(cart.getCount());
itemDTO.setCondition("ITEM_STOCK_ENOUGH");
if (itemDAO.selectOne(itemDTO) == null) {
// 재고 부족이면 리스트에 상품명 추가
outOfStockItems.add(cart.getItemName()); // CartDTO에 itemName이 있어야 함
continue; // 재고 부족 상품은 스킵
}
}
// 2. 재고 부족 체크
if (!outOfStockItems.isEmpty()) {
itemName = outOfStockItems.get(0);
if (outOfStockItems.size() > 1) {
itemName += " 외 " + (outOfStockItems.size() - 1) + "건";
}
request.setAttribute("outOfStockItems", outOfStockItems);
forward.setPath("paymentStockFail.jsp"); // 재고 부족 모달 표시용 JSP
forward.setRedirect(false);
return forward;
}
// itemName 설정
if (!cartDatas.isEmpty()) {
// 장바구니 상품이 1개면 그냥 상품명
itemName = cartDatas.get(0).getItemName();
// 장바구니 상품이 2개 이상이면 "첫번째 상품명 외 N건"
if (cartDatas.size() > 1) {
itemName += " 외 " + (cartDatas.size() - 1) + "건";
}
}
System.out.println("[로그] 출력 상품명 확인 : " + itemName);
// 3. 재고 충분하면 감소
for (CartDTO cart : cartDatas) {
ItemDTO itemDTO = new ItemDTO();
itemDTO.setItemPk(cart.getItemPk());
itemDTO.setItemCount(cart.getCount());
itemDTO.setCondition("BUY_ITEM");
itemDAO.update(itemDTO);
// 재고 감소 실패
if (!itemDAO.update(itemDTO)) {
throw new RuntimeException("재고 감소 실패");
}
}
// 복구용 저장
session.setAttribute("reservedCartDatas", cartDatas);
}
// =========================
// 주문 생성
// =========================
OrderDTO orderDTO = new OrderDTO();
orderDTO.setAccountPk(accountPk);
orderDTO.setAddressPk(addressPk);
orderDTO.setCondition("PREPARING");
OrderDAO orderDAO = new OrderDAO();
orderDAO.insert(orderDTO);
// 방금 생성된 orderPk 가져오기
orderDTO.setCondition("SELECT_ONE_ORDER_PK");
int orderPk = orderDAO.selectOne(orderDTO).getOrderPk();
session.setAttribute("orderPk", orderPk); // 세션 저장
System.out.println("[로그] orderPk 값 확인 : " + orderPk );
System.out.println("[로그] CID 값 확인 : " + KakaoPayConfig.CID );
System.out.println("[로그] SECRET_KEY_DEV 값 확인 : " + KakaoPayConfig.SECRET_KEY_DEV);
// 카카오페이 API 준비
JsonObject body = new JsonObject();
body.addProperty("cid", KakaoPayConfig.CID);
body.addProperty("partner_order_id", orderPk);
body.addProperty("partner_user_id", accountPk);
body.addProperty("item_name", itemName);
body.addProperty("quantity", 1);
body.addProperty("total_amount", totalAmount);
body.addProperty("tax_free_amount", 0);
body.addProperty("approval_url", KakaoPayConfig.SUCCESS_URL);
body.addProperty("cancel_url", KakaoPayConfig.CANCEL_URL);
body.addProperty("fail_url", KakaoPayConfig.FAIL_URL);
// Open API 호출
URL url = new URL("https://open-api.kakaopay.com/online/v1/payment/ready");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("POST");
conn.setRequestProperty("Authorization", "SECRET_KEY " + KakaoPayConfig.SECRET_KEY_DEV);
conn.setRequestProperty("Content-Type", "application/json;charset=UTF-8");
conn.setDoOutput(true);
OutputStream os = conn.getOutputStream();
os.write(body.toString().getBytes("UTF-8"));
os.flush();
os.close();
// 응답 처리
BufferedReader br;
if (conn.getResponseCode() == 200) {
br = new BufferedReader(new InputStreamReader(conn.getInputStream(), "UTF-8"));
} else {
br = new BufferedReader(new InputStreamReader(conn.getErrorStream(), "UTF-8"));
}
StringBuilder sb = new StringBuilder();
String line;
while((line = br.readLine()) != null) {
sb.append(line); // 줄바꿈 없이 연결
}
br.close();
System.out.println("응답 JSON: " + sb.toString());
JsonObject json = JsonParser.parseString(sb.toString()).getAsJsonObject();
if (json.has("tid") && json.has("next_redirect_pc_url")) {
String tid = json.get("tid").getAsString(); // tid 발급
String redirectUrl = json.get("next_redirect_pc_url").getAsString();
session.setAttribute("tid", tid); // 세션에 저장
forward.setPath(redirectUrl);
forward.setRedirect(true);
System.out.println("<<로그>> tid : " + tid);
System.out.println("<<로그>> redirectUrl : " + redirectUrl);
} else {
System.out.println("<<로그>> Ready API 호출 실패: " + json.toString());
forward.setPath(KakaoPayConfig.BASE_URL + "/kakaoPayFail.do");
forward.setRedirect(true);
}
} catch (Exception e) {
e.printStackTrace();
forward.setPath(KakaoPayConfig.FAIL_URL);
forward.setRedirect(true);
}
return forward;
}
}
KakaoPayReadyAction 클래스는 카카오페이 결제 준비(Ready API) 를 담당하는 컨트롤러
사용자가 결제 버튼을 눌렀을 때 실행되어 재고 선점 ➡ 주문 생성 ➡ 카카오페이 결제 페이지로 리다이렉트까지 하는 역할
이 액션이 시작되면 가장 먼저 세션과 요청 파라미터에서 결제에 필요한 핵심 정보를 가져온다
accountPk : 로그인한 사용자 식별자
addressPk : 배송지
totalAmount : 최종 결제 금액
결제 금액이 0 이하인 경우는 정상적인 결제가 아니기 때문에 바로 에외를 발생시켜 흐름을 중단한다
이후 상품 및 장바구니 처리를 위해 ItemDAO, CartDAO를 중비하고
즉시 구매인지/장바구니 구매인지를 구분하기 위해 세션에 저장된 tampCart를 확인한다
이 분기점이 이 코드의 핵심이다
즉시 구매인 경우 tempCartDAO != null
단일 상품에 대해서만 처리한다
상품 PK의 수량을 가져온 뒤,
ITEM_STOCK_ENOUGH 조건으로 재고가 충분한지 먼저 조회한다
여기서 재고가 부족하면 메시지 JSP로 포워딩하면서 결제를 중단한다
재고가 충분하다면 상품명을 itemName으로 설정하고,
BUY_ITEM 조건으로 재고를 임시 감소 시킨다
이 단계는 "결제 중 재고 선점" 역할을 하며,
결제 도중 사용자가 이탕하거나 실패했을 때를 대비해
reservedItemPk, reservedCount를 세션에 저장해 복구 가능하도록 설계되어 있다
이 부분은 실무 기준으로도 좋은 흐름이다
즉시 구매가 아니라면 장바구니 구매 로직으로 들어간다
현재 로그인한 계정의 장바구니 전체를 조회한 뒤
각 상품마다 ITEM_STOCK_ENOUGH 조건으로 재고를 하나씩 확인한다
이때 재고가 부족한 상품이 하나라도 있으면
그 상품명들을 outOfStockItems 리스트에 담고,
결제를 진행하지 않고 paymentStockFail.jsp로 포워딩한다
여기서 첫 번째 상품명 + "외 N건" 형태로 표시한다
모든 상품의 재고가 충분한 경우에만
카카오페이에 전달할 itemName을
1개이면 : 상품명 그대로
2개 이상이면 : "첫 상품명 외 N건"
으로 표시한다
그 다음 장바구니에 들어 있는 모든 상품에 대해 BUY_ITEM 조건으로 재고를 감소시키고,
감소한 장바구니 목록 전체를 세션에 reservedCartDatas로 저장한다
이 역시 결제 실패 시 재고 롤백을 염두에 둔 구조다
재고 선점이 끝나면 이제 주문 생성 단계로 넘어간다.
OrderDTO에 계정, 주소를 세팅하고 상태를 PREPARING으로 둔 채 주문을 insert 한다.
이후 SELECT_ONE_ORDER_PK 조건으로 방금 생성된 주문의 PK를 다시 조회해
세션에 orderPk로 저장한다.
이 orderPk는 이후 카카오페이 승인 단계에서
partner_order_id로 재사용되기 때문에 매우 중요한 값이다.
이제 실제 카카오페이 Ready API 호출을 준비한다.
JSON Body에는 다음 정보들이 들어간다.
- CID: 가맹점 식별자
- partner_order_id: 주문 PK
- partner_user_id: 사용자 PK
- item_name: 앞에서 구성한 상품명
- quantity: 1 (실제 수량이 아닌 결제 단위 개념)
- total_amount: 총 결제 금액
- tax_free_amount: 0
- success / cancel / fail URL
이후 HttpURLConnection을 사용해
https://open-api.kakaopay.com/online/v1/payment/ready로 POST 요청을 보내며,
Authorization 헤더에 SECRET_KEY를 포함시킨다.
응답은 HTTP 상태 코드에 따라 InputStream 또는 ErrorStream에서 읽고,
JSON으로 파싱한다.
응답에 tid와 next_redirect_pc_url이 모두 존재하면
- tid를 세션에 저장하고
- 사용자를 카카오페이 결제 페이지로 redirect 시킨다.
이 단계까지 오면 결제 준비는 성공이다.
반대로 응답에 필요한 값이 없거나 예외가 발생하면
실패 URL로 리다이렉트되며,
catch 블록에서도 동일하게 실패 URL로 보내도록 되어 있다.
'🎅 오너먼트 프로젝트' 카테고리의 다른 글
| 테이블에 JSON 형식으로 값을 저장 with EVENT Table (1) | 2026.01.26 |
|---|---|
| 오너먼트 중간 프로젝트 / 중간 마무리 (0) | 2026.01.09 |
| [ 스크립트 공유 ] 장바구니 비동기 처리와 커뮤니티 일화 (0) | 2026.01.06 |
| jQuery의 이벤트 위임 방식 $(document).on()과 AJAX (1) | 2026.01.05 |
| 장바구니 개수 변경 / 장바구니 상품 삭제 전체 코드 (0) | 2026.01.05 |