PortOne 사용
카카오 결제 연동하기
테스트 채널 추가
결제 연동 -> 연동 정보 -> 테스트 -> 채널 추가
테스트 선택 후
결제대행사 : 카카오페이 선택
결제 모듈 : 간편결제 선택 후 다음 누르기
채널 이름 입력하고 채널 속성 = 결제 선택
PG상점아이디에 카카오페이 일반결제 선택 후 저장
저장을 누르면 테스트 결제 채널이 생긴다.
결제 연동하기
결제 연동하기 공식 문서
https://developers.portone.io/opi/ko/integration/start/v2/checkout?v=v2
1. 포트원 SDK 설치
<script src="https://cdn.portone.io/v2/browser-sdk.js"></script>
2. 결제 요청
storeId와 channelKey에 자신의 상점 ID와 채널 키를 입력한다.
카카오페이로 결제할 것이기 때문에 payMethod를 EASY_PAY로 바꿔준다.
const response = await PortOne.requestPayment({
// Store ID 설정
storeId: "store-4ff4af41-85e3-4559-8eb8-0d08a2c6ceec",
// 채널 키 설정
channelKey: "channel-key-893597d6-e62d-410f-83f9-119f530b4b11",
paymentId: `payment-${crypto.randomUUID()}`,
orderName: "나이키 와플 트레이너 2 SD",
totalAmount: 1000,
currency: "CURRENCY_KRW",
payMethod: "EASY_PAY",
});
상점 아이디와 채널 키는 연동 정보에서 확인할 수 있다.
JSON 데이터 처리
maven repository 에서 설치
Jackson Databind, Jackson Core, Jackson Annotations
세 개를 같은 버전으로 설치해야 한다. (나는 2.17.3 버전으로 설치했다.)
jar 파일도 다운로드 받아서 tomcat 폴더의 lib 폴더 안에도 넣어줘야 한다.
<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.17.3</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.17.3</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.17.3</version>
</dependency>
입력 받은 데이터를 JSON 형식의 string으로 바꾸기
doPost 메소드
String json = JsonParser.parse(req);
JsonParser
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;
public class JsonParser {
private static final StringBuilder jsonBuilder = new StringBuilder();
private static final ObjectMapper mapper = new ObjectMapper();
// HttpServletRequest 객체를 받아 요청 본문을 JSON 형식의 문자열로 반환
public static String parse(HttpServletRequest req) {
try (BufferedReader reader = req.getReader()) {
String line;
// 요청 본문 데이터를 한 줄씨 꺼내서 읽음
// 더 이상 읽을 데이터가 없을 때까지 반복
while ((line = reader.readLine()) != null) {
// 현재 읽은 줄 내용을 jsonBuilder에 추가
jsonBuilder.append(line);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
// 문자열 데이터 반환
String json = jsonBuilder.toString();
return json;
}
}
JSON 형식의 문자열을 자바 객체로 변환
ObjectMapper 객체를 생성한 후
readValue 메소드에 첫 번째 매개변수로 JSON 형식의 문자열을 넣고,
두 번째 매개변수로 바꾸고 싶은 객체 클래스를 넣어준다.
ObjectMapper objectMapper = new ObjectMapper(); // JSON 글자를 자바 객체로 바꾸는 법
Orders orders = objectMapper.readValue(json, Orders.class);
장바구니 기능 구현
장바구니 상품 담기
<h1>상품 목록</h1>
<ul>
<li>
상품01 : 1000
<button onclick="saveToStorage(1, '상품01', 1000)">장바구니</button>
</li>
<li>
상품02 : 2000
<button onclick="saveToStorage(2, '상품02', 2000)">장바구니</button>
</li>
<li>
상품03 : 3000
<button onclick="saveToStorage(3, '상품03', 3000)">장바구니</button>
</li>
</ul>
<button onclick="pay()">결제하기</button>
세션 스토리지에 장바구니 상품 저장
const saveToStorage = (productId, productName, productPrice) => {
// 세션 스토리지에서 cart 값을 가져오기
const cart = JSON.parse(sessionStorage.getItem('cart')) || [];
let exists = false;
for (let i = 0; i < cart.length; i++) {
// 저장하려는 상품이 cart에 이미 있다면 exists를 true로 바꾸기
if (cart[i].id === productId) {
exists = true;
break;
}
}
// 상품이 cart 안에 들어있지 않으면 넣기
if (!exists) {
cart.push({id: productId, name: productName, price: productPrice});
}
sessionStorage.setItem('cart', JSON.stringify(cart));
}
결제하기 구현 (totalAmount, PortOne)
cart 안에 있는 상품들을 가져와서 금액 합계를 계산한 후, PortOne 결제 메소드의 totalAmount 부분에 넣어준다.
<script src="https://cdn.portone.io/v2/browser-sdk.js"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script>
const pay = async () => {
// cart 상품 가져오기
const cart = JSON.parse(sessionStorage.getItem('cart'));
let orderName = cart[0].name;
if (cart.length > 1) {
orderName += ' 외 ' + (cart.length - 1);
}
// 장바구니 안의 상품들 금액 합계 저장
let totalAmount = cart.reduce((sum, product) => sum + product.price, 0);
const response = await PortOne.requestPayment({
// Store ID 설정
storeId: "상점 ID",
// 채널 키 설정
channelKey: "채널 키",
paymentId: 'payment-' + crypto.randomUUID(),
orderName: orderName,
totalAmount: totalAmount,
currency: "CURRENCY_KRW",
payMethod: "EASY_PAY",
});
console.log(response);
if (response.code !== undefined) {
// 오류 발생
return alert(response.message);
}
// JSON 객체 생성
const data = JSON.stringify({
paymentId: response.paymentId,
cart: sessionStorage.getItem('cart')
})
await axios.post('http://localhost:8080/orders/validation', data, {headers: {'Content-Type': 'application/json'}});
}
</script>
OrdersController (POST 요청 받기)
OrdersController에서 위에서 axios.post로 받은 요청을 doPost 메소드로 받는다.
받은 데이터를 JSON 형식의 문자열로 바꾼 다음, Orders 객체로 변환한다.
cookie를 확인하고 만약 ATOKEN이 있다면 userId를 가져온다.
ordersService로 결제 검증을 한다.
만약 결제 검증이 성공했다면 product/list 페이지로 보내주고,
검증이 실패했다면 장바구니 페이지로 보내준다.
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String action = req.getPathInfo();
if ("/validation".equals(action)) {
String json = JsonParser.parse(req); // JSON 데이터 받는 법
ObjectMapper objectMapper = new ObjectMapper(); // JSON 글자를 자바 객체로 바꾸는 법
Orders orders = objectMapper.readValue(json, Orders.class); // JSON 글자를 자바 객체로 바꾸는 법
// userIdx 가져오기
int userIdx = 0;
for (Cookie cookie : req.getCookies()) {
System.out.println(cookie.getName());
if(cookie.getName().equals("ATOKEN")) {
userIdx = JwtUtil.getIdx(cookie.getValue());
}
}
// 결제 검증
boolean result = ordersService.validate(orders, Integer.parseInt(req.getSession().getAttribute("userIdx").toString()));
if (result) {
resp.sendRedirect("/product/list");
} else {
resp.sendRedirect("/orders/cart");
}
}
}
받아온 데이터
{
"paymentId": "결제 id",
"cart": [
{
"id": 1,
"name": "상품01",
"price": 1000
},
{
"id": 2,
"name": "상품02",
"price": 2000
},
{
"id": 3,
"name": "상품03",
"price": 3000
}
]
}
OrdersService (실제 주문 금액 검증)
validate 메소드
PortOne에 결제 조회 API (아래에 있음)를 요청하고,
DB에서 가져온 상품 금액 합계와 결제 요청한 금액 합계가 일치하지 않으면
실제 주문 금액과 다른 것이기 때문에 결제가 되게 해서는 안 된다.
일치하면 DB에 주문 내용 데이터를 저장 후 true를 반환하고,
일치하지 않으면 PortOne에게 환불 처리 API를 요청해서 환불 처리를 해준 후 false를 반환한다.
public boolean validate(Orders orders, int userIdx) {
// PortOne에 결제 조회 API 요청
int totalAmount = HttpClientUtil.getTotalAmount(Constants.PORTONE_SECRET, orders.getPaymentId());
int totalPrice = 0;
// DB에 있는 상품의 금액 조회해서 총 합계 금액 계산
for (Product cartProduct : orders.getProducts()) {
Product product = productDao.getProductByIdx(cartProduct.getIdx());
totalPrice += product.getPrice();
}
// 결제 요청한 총 합계 금액과 실제 DB에서 조회한 총 합계 금액이 일치하면
if(totalAmount == totalPrice) {
System.out.println("주문 완료!!");
int ordersIdx = ordersDao.save(userIdx);
// 주문 데이터 DB에 저장
ordersDao.saveDetails(ordersIdx, orders.getProducts());
return true;
} else {
System.out.println("나쁜놈 결제 안됨!!");
// 환불 처리 API
return false;
}
}
실제 주문 금액이 상품 금액 합계랑 일치하지 않으면
결제 안 됨 출력
HttpClientUtil (PortOne 결제 조회 API)
API들은 아래 링크에서 확인할 수 있다.
https://developers.portone.io/api/rest-v2/auth?v=v2
시크릿 키 발급
V2 API를 사용하기 위해서는 시크릿 키가 필요하다.
연동 정보 -> 식별 코드 , API Keys -> V2 API -> 새로운 API Secret 발급
Secret 이름을 입력하고 만료 기한을 선택 후 생성을 누르면 시크릿 키가 만들어진다.
만들어지고 나서는 시크릿 키가 뜨는데, 페이지를 벗어나면 확인할 수 없기 때문에
반드시 만들고 나서 바로 복사한 다음에 갖고 있어야 한다.
실제 주문 금액 조회
아래 링크에서 확인할 수 있다.
https://developers.portone.io/api/rest-v2/payment?v=v2#get%20%2Fpayments%2F%7BpaymentId%7D
java.net을 사용하고 있기 때문에 Request Sample 부분에 Java - java.net.http 선택 후 아래 샘플대로 API를 요청한다.
// 샘플 코드
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.portone.io/payments/paymentId"))
.header("Content-Type", "application/json")
.method("GET", HttpRequest.BodyPublishers.ofString("{}"))
.build();
HttpResponse<String> response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
// 실제 코드
client = HttpClient.newHttpClient();
// GET 요청 생성
httpRequest = HttpRequest.newBuilder()
.uri(URI.create("https://api.portone.io/payments/" + paymentId))
.header("Authorization", "PortOne " + secret)
.GET()
.build();
httpResponse = client.send(httpRequest, HttpResponse.BodyHandlers.ofString());
String responseBody = httpResponse.body();
Response 부분에 보면 응답 오는 내용들을 확인할 수 있는데,
총 결제 금액을 가져와야 하기 때문에 여기서 amount 안에 있는 total 값을 가져올 것이다.
// amount 가져오기
ObjectMapper objectMapper = new ObjectMapper();
JsonNode rootNode = objectMapper.readTree(responseBody);
JsonNode amountNode = rootNode.get("amount");
// 주문 금액 가져오기
int total = amountNode.get("total").asInt();
전체 코드
public static int getTotalAmount(String secret, String paymentId) {
try {
client = HttpClient.newHttpClient();
// GET 요청 생성
httpRequest = HttpRequest.newBuilder()
.uri(URI.create("https://api.portone.io/payments/" + paymentId))
.header("Authorization", "PortOne " + secret)
.GET()
.build();
httpResponse = client.send(httpRequest, HttpResponse.BodyHandlers.ofString());
String responseBody = httpResponse.body();
// amount 가져오기
ObjectMapper objectMapper = new ObjectMapper();
JsonNode rootNode = objectMapper.readTree(responseBody);
JsonNode amountNode = rootNode.get("amount");
// 주문 금액 가져오기
int total = amountNode.get("total").asInt();
return total;
} catch (IOException e) {
throw new RuntimeException(e);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
받아온 데이터
{
"paymentId": "결제 id",
"cart": [
{
"id": 1,
"name": "상품01",
"price": 1000
},
{
"id": 2,
"name": "상품02",
"price": 2000
},
{
"id": 3,
"name": "상품03",
"price": 3000
}
]
}
'BE > Java' 카테고리의 다른 글
[Java] JWT란? / JWT 생성, 검증 (0) | 2025.01.17 |
---|---|
[Java] 카카오 로그인 API 연동 (1) | 2025.01.10 |
[Java] DTO 설계 시 고려사항 (0) | 2025.01.10 |
[Java] multipart/form-data란? / form 태그로 서버에 파일 전달하기 (0) | 2025.01.09 |
[Java] SQL Injection, PreparedStatement (2) | 2025.01.09 |