patMent 프로젝트를 만들었다


패키지 구조
▪ controller.common
Action.java : 공통 Action 인터페이스 또는 추상 클래스
ActionFactory.java : 요청 URL과 Action 매핑 처리
ActionForward.java : 페이지 이동 정보를 담는 DTO
FrontController : 모든 요청을 처리하는 중앙 서블릿
▪ controller.page
KakaoPayReadyAction.java : 카카오페이 결제 준비 처리
KakaoPayApproveAction.java : 카카오페이 경제 승인 처리
OrderHistoryAction.java : 주문 내역 조회 처리
KakaoPayService.java : 카카오페이 API 호출 서비스
▪ model.common
JDBCUtil.java : DB 연결/자원 관리
▪ model.dao
OrderDAO.java : 주문 관련 DB 처리
ProductDAO.java : 상품 관련 DB 처리
▪ model.dto
OrderDTO.java : 주문 데이터 객체
ProductDTO.java : 상품 데이터 객체
웹 리소스(webapp)
▪ images : 상품이미지
▪ WEB-INF/lib : 라이브러리
▪ JSP 페이지
index.jsp : 메인페이지
ordermentFail.jsp : 주문 내역 페이지
paymentFail.jsp : 결제 실패 페이지
paymentSuccess.jsp : 결제 성공 페이지
🥦 전체 코드 흐름
프로젝트는 FrontController 패턴이기 때문에, 무조건 모든 요청이 아래 순서로 흘러간다
1. 클라이언트가 URL 호출
2. FrontController.java 실행
3. ActionFactory ➡ URL에 맞는 Action 찾기
4. 해당 Action 실행
5. DAO 호출 ➡ DB 처리
6. JSP로 이동 (ActionForward)
그래서 FrontController ➡ ActionFactory➡ Action 구조를 이해해야 한다
FrontController.java
주의할 점
어떤 URL이 들어올 때 어떻게 매칭되는지
forward / redirect 처리 방식
ActionFactory 호출 방식
➡ 왜 tid가 null이 되는지 같은 문제도 이부분 떄문에 자주 발생함
package controller.common;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@WebServlet("*.do")
public class FrontController extends HttpServlet {
private static final long serialVersionUID = 1L;
private ActionFactory factory;
@Override
public void init() throws ServletException {
// factory 초기화!!
factory = new ActionFactory();
}
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doAction(request, response);
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doAction(request, response);
}
private void doAction(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String uri = request.getRequestURI(); // /프로젝트명/payMent/KakaoPayReady.do
String context = request.getContextPath(); // /프로젝트명
String command = uri.substring(context.length()); // /payMent/KakaoPayReady.do
System.out.println("[로그] FrontController, command : "+command);
Action action = factory.getAction(command);
if (action == null) {
response.getWriter().println("[로그] Action이 null입니다.. (" + command + ")");
return;
}
// 3. Action 실행 후 ActionForward 반환
ActionForward forward = null;
try {
forward = action.execute(request, response);
} catch (Exception e) {
e.printStackTrace();
response.getWriter().println("[로그] Action 실행 중 예외 발생 : " + e.getMessage());
return;
}
if (forward != null) {
if (forward.isRedirect()) {
// Redirect 시 contextPath 포함
response.sendRedirect(request.getContextPath() + forward.getPath());
} else {
// Forward
RequestDispatcher rd = request.getRequestDispatcher(forward.getPath());
rd.forward(request, response);
}
} else {
// forward가 null이면 아무 작업도 안 함
System.out.println("[로그] ActionForward가 null이므로 처리하지 않습니다.");
}
}
}
factory = new ActionFactory();
요청 URL에 맞는 Action 객체를 생성, 관리하는 "액션 생성기(매핑 담당자)"를 준비하는 것
즉, FrontController는 모든 요청을 받는 "문지기"
ActionFactory는 그 요청을 처리할 "사람(Action 객체)"을 연결해주는 "스위치"
✅ 왜 init() 안에서 실행하는가?
서블릿의 init()은 서버 시작 시 단 한 번만 실행됨
ActionFactory는 서버 켜질 때 한 번만 만들면 됨
요청할 때마다 새로 만들 필요 없음
모든 요청에서 공통으로 사용됨
String uri = request.getRequestURI(); // /payMent/KakaoPayReady.do
String context = request.getContextPath(); // /payMent
String command = uri.substring(context.length()); // /KakaoPayReady.do
FrontController 패턴에서 URL에서 실제 요청 명령만 뽑아내기 위한 표준 코드
브라우저는 URL을 http://localhost:8088/프로젝트명/payMent/KakaoPayReady.do
그런데 우리가 실제로 받고 싶은 건 /KakaoPayReady.do 이거 하나이다
왜냐? ActionFactory에서 매핑하는 key가 /KakaoPayReady.do 이니까
❗ 문제 ) forward 경로 오타로 페이지 안 열림 주의
ex) /patmentSuccess.jsp vs /payMent/paymentSuccess.jsp
대부분 /patMent를 빠뜨리거나, 넣지 말아야 할 곳에 넣음
ActionFactory.java 분석
ActionFactory는 URL(command)와 Action 클래스를 연결해주는 "라우팅 테이블"이다
URL을 Action 객체와 매핑함
여기서 매핑을 잘 못하면 action = null ➡ NULL 오류 발생
package controller.common;
import java.util.HashMap;
import java.util.Map;
import controller.page.KakaoPayApproveAction;
import controller.page.KakaoPayReadyAction;
import controller.page.OrderHistoryAction;
public class ActionFactory {
private Map<String, Action> map;
public ActionFactory() {
map = new HashMap<>();
map.put("/OrderHistory.do", new OrderHistoryAction()); // 결제 내역 보기
map.put("/KakaoPayReady.do", new KakaoPayReadyAction()); // 카카오페이 결제 준비
map.put("/KakaoPayApproveAction.do", new KakaoPayApproveAction()); // 카카오페이 결제 승인
}
public Action getAction(String command) {
return map.get(command);
}
}
✅ map을 사용하는 이유
주소록과 같은 것
URL(문자열)을 key로 하고 해당 URL을 처리할 Action 객체를 value로 넣으면
나중에 URL이 나왔을 때 해당 Action 값을 넘겨줌
각각의 Action 로직 분석
KakaoPayReadyAction.java
상품 정보를 받아와 카카오페이 결제 준비(ready)를 호출하고, 결제 페이지로 보내는 Action이다
1. pid로 상품 조회
2. 주문번호 생성
3. 카카오페이 결제 준비 API 호출
4. 생성된 tid와 상품 정보를 세션에 저장
5. 카카오 결제창 URL로 이동
package controller.page;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.OutputStream;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import controller.common.Action;
import controller.common.ActionForward;
import model.dto.ProductDTO;
import model.dao.ProductDAO;
public class KakaoPayReadyAction implements Action {
@Override
public ActionForward execute(HttpServletRequest request, HttpServletResponse response) throws Exception {
// 1) JSP에서 넘어온 pid 받기
String pid = request.getParameter("pid");
System.out.println("[로그] pid : " + pid);
// 2) Product 조회
ProductDTO product = ProductDAO.getProduct(pid);
if (product == null) {
System.out.println("[로그] product null");
return null;
}
// 3) 주문번호 생성 (unique)
String orderId = "ORDER_" + System.currentTimeMillis();
// 4) KakaoPayService ready 호출
String redirectUrl = KakaoPayService.ready(orderId, product.getName(), product.getPrice());
// 5) 세션에 tid 저장
HttpSession session = request.getSession();
session.setAttribute("tid", KakaoPayService.getTid(orderId)); // 결제 승인 단계에서 필요
session.setAttribute("product", product); // 결제하려는 상품 저장
session.setAttribute("pid", product.getPid()); // 결제 완료 후 DB 저장용
// 6) 결제 페이지로 redirect
response.sendRedirect(redirectUrl);
return null;
}
}
✅ 세션에 tid 저장?
사용자가 결제창으로 이동했다가 돌아오면 request가 새로 생김
tid는 Ready 단계에서만 오니까, 이후 요청에서 다시 받을 수 없음
따라서 세션에 저장해둬야 다음 요청(approve)에서 꺼내 쓸 수 있음
📌 한 문장으로 요약
카카오페이는 승인 단계에서 tid를 안 보내주기 때문에,
Ready 단계에서 받은 tid를 세션에 저장해 두었다가 Approve 단계에서 사용하려고 저장하는 것
✅ redirect_url 셋팅? 파라미터 준비?
String redirectUrl = KakaoPayService.ready(orderId, product.getName(), product.getPrice());
KakaoPayService.ready() 메서드가 리턴한 값을 redirectUrl 변수에 담는다
KakaoPayService.ready() → 카카오페이 서버에 "결제 준비 요청"을 보냄
카카오페이가 응답으로 결제 페이지 URL(tid 포함) 을 보내줌
그 URL을 redirectUrl 변수에 저장(세팅)
orderId, product.getName(), product.getPrice()
주문번호, 상품명, 가격은 카카오 API에 전달하는 정보들이다
/
KakaoPayApproveAction.java
카카오페이 결제 성공 후 돌아온 요청을 받아서 ➡ 결제 승인 API 호출하고
➡ DB에 주문 저장하고 ➡ 성공/실패 페이지로 이동시키는 역할
package controller.page;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import controller.common.Action;
import controller.common.ActionForward;
import model.dao.OrderDAO;
import model.dao.ProductDAO;
import model.dto.OrderDTO;
import model.dto.ProductDTO;
public class KakaoPayApproveAction implements Action {
@Override
public ActionForward execute(HttpServletRequest request, HttpServletResponse response) throws Exception {
// GET 파라미터로 전달된 tid와 pg_token 읽기
HttpSession session = request.getSession();
// 1) 세션에서 tid와 pid 꺼내기
// tid : 카카오페이 결제 고유 번호
// pid : 어떤 상품을 결제했는지
String tid = (String) session.getAttribute("tid");
String pid = (String) session.getAttribute("pid");
System.out.println("tid: " + tid + ", pid: " + pid);
// pg_token : 카카오페이가 결제 성공하면 URL에 붙여주는 값
String pgToken = request.getParameter("pg_token"); // URL에서 전달됨
if (tid == null || pgToken == null || pid == null) {
System.out.println("[로그] tid, pgToken, pid 없음");
System.out.println("tid: " + tid + ", pid: " + pid + ", pgToken:" + pgToken);
ActionForward forward = new ActionForward();
forward.setPath("/paymentFail.jsp");
forward.setRedirect(false);
return forward;
}
// 2) 카카오페이 승인 API 호출 (테스트용 true)
boolean success = KakaoPayService.approve(tid, pgToken);
if (success) {
// 3) DB에 실제 상품 정보 저장
ProductDTO product = ProductDAO.getProduct(pid);
OrderDTO orderDTO = new OrderDTO();
orderDTO.setTid(tid);
orderDTO.setProductName(product.getName());
orderDTO.setOrderPrice(product.getPrice());
orderDTO.setImgURL(product.getImgURL());
// DB insert
OrderDAO dao = new OrderDAO();
boolean inserted = dao.insert(orderDTO);
System.out.println("DB 저장 성공 여부: " + inserted);
}
ActionForward forward = new ActionForward();
if (success) {
forward.setPath("/paymentSuccess.jsp");
} else {
forward.setPath("/paymentFail.jsp");
}
forward.setRedirect(success);
return forward;
}
}
/
OrderHistoryAction.java
주문 내역 데이터를 가져와서 JSP로 보내는 Controller 역할
package controller.page;
import java.util.List;
import javax.servlet.http.*;
import controller.common.Action;
import controller.common.ActionForward;
import model.dao.OrderDAO;
import model.dto.OrderDTO;
public class OrderHistoryAction implements Action {
@Override
public ActionForward execute(HttpServletRequest request, HttpServletResponse response) throws Exception {
// 주문 내역 조회
OrderDAO dao = new OrderDAO();
List<OrderDTO> orderList = dao.selectAll();
// selectAll() 메소드는 모든 주문 정보를 리스트 형태로 가져옴
// JSP에 데이터 전달
request.setAttribute("order", orderList);
// 페이지 이동 설정
ActionForward forward = new ActionForward(); // 어떤 방식으로 이동할지
forward.setPath("/orderHistory.jsp"); // 보여줄 JSP 지정
forward.setRedirect(false); // forward 방식
return forward;
}
}
▪ execute() 메소드
FrontController 패턴에서 호출되는 핵심 메소드
요청과 응답 객체를 받아 비즈니스 로직 처리 ➡ 데이터 전달 ➡ 페이지 이동을 수행
✅ 전체 흐름 요약
1. 클라이언트가 주문 내역 페이지 요청 ➡ /OrderHistory.do
2. FrontController가 OrderHistoryAction.execute() 호출
3. DAO를 통해 DB에서 모든 주문 정보 조회
4. 조회한 주문 목록을 request 객체에 담아 JSP로 전달
5. ActionForward를 통해 forward 방식으로 orderHistory.jsp로 이동
6. JSP 에서 order 속성을 받아 주문 내역 화면 출력
DAO 분석
OrderDAO.java
insert
select
delete
package model.dao;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
import model.common.JDBCUtil;
import model.dto.OrderDTO;
public class OrderDAO {
// 상품 구매
private static final String INSERT = "INSERT INTO ORDERS (ORDER_PK, NAME, ORDER_PRICE, PRODUCT_IMAGE_URL) VALUES (?, ?, ?, ?)";
// 주문 내역 조회
private static final String SELECT_ALL = "SELECT * FROM ORDERS";
public ArrayList<OrderDTO> selectAll() {
ArrayList<OrderDTO> datas = new ArrayList<>();
Connection conn = JDBCUtil.connect();
PreparedStatement pstmt = null;
try {
pstmt = conn.prepareStatement(SELECT_ALL);
ResultSet rs = pstmt.executeQuery();
while (rs.next()) {
OrderDTO order = new OrderDTO();
order.setTid(rs.getString("ORDER_PK"));
order.setProductName(rs.getString("NAME"));
order.setOrderPrice(rs.getInt("ORDER_PRICE"));
order.setOrderDate(rs.getTimestamp("ORDER_DATE"));
order.setImgURL(rs.getString("PRODUCT_IMAGE_URL"));
datas.add(order);
}
} catch (SQLException e) {
e.printStackTrace();
}
JDBCUtil.disconnect(conn, pstmt);
System.out.println(datas);
return datas;
}
public boolean insert(OrderDTO orderDTO) {
Connection conn = JDBCUtil.connect();
PreparedStatement pstmt = null;
try {
pstmt = conn.prepareStatement(INSERT);
pstmt.setString(1, orderDTO.getTid());
pstmt.setString(2, orderDTO.getProductName());
pstmt.setInt(3, orderDTO.getOrderPrice());
pstmt.setString(4, orderDTO.getImgURL());
int result = pstmt.executeUpdate();
if(result <= 0) {
return false;
}
} catch (SQLException e) {
e.printStackTrace();
return false;
}
JDBCUtil.disconnect(conn, pstmt);
return true;
}
}
오류 포인트 확인
PK 생성 방식
SQL 파라미터 순서
DB 연결 close
ProductDAO.java
상품 목록/정보 가져오는 부분
package model.dao;
import java.util.HashMap;
import java.util.Map;
import model.dto.ProductDTO;
// DB 대신 상품 목록을 미리 저장해두는 클래스 (메모리 DB 역할)
public class ProductDAO {
// 상품 저장용 Map
private static Map<String, ProductDTO> productMap = createProductMap();
private static Map<String, ProductDTO> createProductMap() {
Map<String, ProductDTO> map = new HashMap<>();
map.put("P001", new ProductDTO("P001", "오리", 4500, "images/kiki.jpg"));
map.put("P002", new ProductDTO("P002", "병아리", 5000, "images/kiki2.jpg"));
map.put("P003", new ProductDTO("P003", "강아지", 7000, "images/kiki3.jpg"));
map.put("P004", new ProductDTO("P004", "우유", 6000, "images/kiki4.jpg"));
return map;
}
public static ProductDTO getProduct(String pid) {
return productMap.get(pid);
}
}
DB 대신 메모리 내 상품 데이터를 관리
ProductDTO 객체를 Map에 저장 ➡ 키는 상품 ID(pid)
▪ 상품 저장용 Map
private static Map<String, ProductDTO> productMap = createProductMap();
static ➡ 클래스 로딩 시 한 번만 생성, 모든 객체에서 공유
키(String) ➡ 상품 Id (P1001, P1002 등)
값(ProductDTO) ➡ 상품 이름, 가격, 이미지 URL 등
▪ 상품 조회 메서드
public static ProductDTO getProduct(String pid) {
return productMap.get(pid);
}
입력 : 상품 ID (pid)
반환 : 해당 상품의 ProductDTO 객체
존재하지 않는 ID 입력 시 ➡ null 반환
DTO 분석
데이터가 JSP에 넘어갈 때 어떻게 들어가는지 확인
OrderDTO.java
package model.dto;
import java.sql.Timestamp;
public class OrderDTO {
private String tid; // 주문 번호 (TID)
private String productName; // 상품명
private int orderPrice; // 주문 금액
private Timestamp orderDate; // 주문 날짜
private String imgURL; // 상품 이미지 URL
public String getTid() {
return tid;
}
public void setTid(String tid) {
this.tid = tid;
}
public String getProductName() {
return productName;
}
public void setProductName(String productName) {
this.productName = productName;
}
public int getOrderPrice() {
return orderPrice;
}
public void setOrderPrice(int orderPrice) {
this.orderPrice = orderPrice;
}
public Timestamp getOrderDate() {
return orderDate;
}
public void setOrderDate(Timestamp orderDate) {
this.orderDate = orderDate;
}
public String getImgURL() {
return imgURL;
}
public void setImgURL(String imgURL) {
this.imgURL = imgURL;
}
@Override
public String toString() {
return "OrderDTO [tid=" + tid + ", productName=" + productName + ", orderPrice=" + orderPrice
+ ", orderDate=" + orderDate + ", imgURL=" + imgURL + "]";
}
}
tid : 카카오페이에서 발급한 주문 고유 번호
productName : 주문한 상품 이름
orderPrice : 결제한 가격
orderDate : 주문이 발생한 날짜/시간 (java.sql.Timestamp 사용)
imgURL : 상품 이미지를 JSP에서 보여주기 위한 이미지 경로
▶ 주문과 관련된 데이터
ProductDTO.java
package model.dto;
public class ProductDTO {
private String pid; // 상품 ID
private String name; // 상품명
private int price; // 가격
private String imgURL; // 상품이미지
public ProductDTO(String pid, String name, int price, String imgURL) {
this.pid = pid;
this.name = name;
this.price = price;
this.imgURL = imgURL;
}
public String getImgURL() {
return imgURL;
}
public void setImgURL(String imgURL) {
this.imgURL = imgURL;
}
public String getPid() {
return pid;
}
public void setPid(String pid) {
this.pid = pid;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getPrice() {
return price;
}
public void setPrice(int price) {
this.price = price;
}
@Override
public String toString() {
return "ProductDTO [pid=" + pid + ", name=" + name + ", price=" + price + ", imgURL=" + imgURL + "]";
}
}
pid : 상품 ID
name : 상품 이름
price : 상품 가격
imgURL : JSP 등에서 사용할 상품 이미지 경로
ProductDTO 생성자
: 객체 생성 시 모든 필드를 한 번에 초기화 가능
▶ 상품 관련 필드만 존재
JSP 분석
각 JSP가 어떤 데이터를 request attribute로 받는지 확인
index.jsp ➡ 결제 버튼 / 상품 선택
paymentSuccess.jsp ➡ 결제 완료 페이지
orderHistory ➡ 주문 내역 출력
paymentFail.jsp ➡ 주문 실패 페이지
web.xml
FrontController mapping 확인
CharacterEncodingFilter 있는지
KakaoPay redirect_url 설정 영향 확인
🔥 우선순위 순서 요약
- FrontController.java
- ActionFactory.java
- 각 Action (Ready → Approve → History)
- Dao (OrderDAO / ProductDAO)
- DTO
- JSP
- web.xml
'JSP' 카테고리의 다른 글
| 결제 API : ngrok 실행시키기 (0) | 2025.12.04 |
|---|---|
| 카카오톡 결제 API를 사용하여 작은 결제 프로젝트 만들어보기 - 2 (0) | 2025.12.03 |
| a태그와 JSP 내장객체 pageContext (0) | 2025.12.02 |
| Servlet vs Action의 역할 (0) | 2025.12.02 |
| XML 설정과 어노테이션 (0) | 2025.12.01 |