온라인 쇼핑몰 프로젝트에서 결제 시스템을 구축해야 정말 완성도 높은 프로젝트가 되겠다라는 생각이 있었지만, 막상 적절한 API 및 실제 결제를 어떻게 진행해야 할 지 막막 했었습니다...
하지만 실제 연동으로 결제를 진행할 수 있지만, 테스트 용도로 따로 구분짓게 만들어 자정이 지나면 자동 환불이 되도록 개발 연습에만 도움을 줄 수 있도록 Iamport에서 다행히 이를 구현을 해놓았기에 이것을 활용하여 결제 시스템을 구축 할 수 있었고 이번 포스팅에서는 제가 어떻게 했으며 각 개념정리를 다루어 보려합니다
아임포트는 다양한 결제 게이트웨이와 연동을 쉽게 할 수 있도록 해주는 플랫폼으로, 복잡한 결제 시스템을 간편하게 구현할 수 있게 해줍니다. 저는 프론트엔드와 백엔드를 연동하여서 해당 기능을 제공하고자 하였습니다. 백엔드 웹 개발자를 바라고 있지만 리액트도 어느정도는 다루어보았기에 도전해봤지만 상당히 오래 걸렸습니다 ㅜㅜ
초기 설정
먼저 아임포트를 사용하기 위해서는 아임포트 사이트에서 회원가입 후, 테스트 란으로 들어가면 쉽게 API Key와 Secret을 받을 수 있습니다 이것을 활용하여 하는데, 테스트 용은 잘 모르겠지만 다른 블로그 글 포스팅 및 사이트에서는 API Key와 Secret은 외부에 노출되지 않도록 주의하라고 합니다
그러니 환경 변수나 설정 파일에 안전하게 보관하도록 하는데 추후에 알고보니 인텔리제이에서 API 키를 따로 등록하여서 안전하게 사용할 수 있는 방법이 있었습니다.. 저는 이것을 늦게 알아서 처음에는 application.yml에 생으로 등록해두었지만 나중에 완성되고 깃에 올리며 aws로 배포할 때에는 인텔리제이를 활용할 계획입니다
SDK와 merchantUid의 개념
제가 처음에 애를 먹었던 이유는, 결제 플랫폼을 사용하기에 앞서 생소한 개념과 코드를 다루어야 하기 때문입니다. 아임포트를 이해하고 코드를 구성하기 위해서는 두 가지 중요한 개념이 있는데 "SDK" 와 "merchantUid" 입니다
SDK는 특정 플랫폼이나 서비스를 사용하기 위해 제공되는 도구 모음으로 아임포트에서 사용되는 것은 웹 애플리케이션에 결제 기능을 통합할 수 있도록 도와주는 키트라고 생각하시면 됩니다. 이 SDK를 통해 다양한 결제 방법을 쉽게 구현할 수 있고 결제 요청부터 결제 완료 후 검증까지의 전체 프로세스를 손 쉽게 다룰 수 있게 됩니다
SDK를 사용함으로써 개발자는 복잡한 결제 로직을 직접 구현할 필요 없이, 간단한 메서드 호출만으로 결제 기능을 구현할 수 있으니 빡구현에 앞서 이러한 것들을 검색해보는게 많이 유용한 것을 알았습니다 ㅎㅎ
merchantUid는 각 결제 건을 고유하게 식별하기 위한 값입니다. 이 값은 결제 요청 시에 생성되며, 결제 완료 후 결제 검증 및 주문 상태 업데이트 등에 사용됩니다. 여기서 해당 기능을 이용하는 이유가 분명히 드러나는 데 개발자는 각 주문에 대해 유일한 merchantUid를 생성하여 아임포트와 통신을 하기에 이것을 통해 결제 내역을 식별하고 관리할 수 있습니다.
만일 요청 시, 고유한 merchantUid를 생성하여 결제를 요청하구, 이후 결제 검증 및 완료 시 이 값을 사용하여 해당 주문을 식별하게 됩니다. 해당 기능은 어떻게 보면 각 주문을 고유하게 만들기도 하지만 보안에 있어서도 한 몫합니다. 저는 이 점을 이용하여서 결제 내역을 안전하고 신뢰성 있게 관리할 수 있도록 하면 좋겠다고 생각하였고, 아래 코드를 통해 이 과정이 어떻게 구성을 하였는 지 보여드리겠습니다:
String merchantUid = generateMerchantUid();
Order order = Order.builder()
.member(member)
.status(OrderStatus.PENDING)
.merchantUid(merchantUid)
.totalPrice(tempOrderDTO.getTotalPrice())
.requestMessage(tempOrderDTO.getRequestMessage())
.build();
orderRepository.save(order);
결제 요청 시, merchantUid는 주문을 고유하게 식별하기 위해 생성이 되기에, 이 값은 주문과 결제의 일치성을 유지하며, 보안 측면에서도 중요하게 여겨집니당. 이것을 통해 각 주문이 고유하게 식별되고, 중복이나 위조된 결제를 방지할 수 있겠다고 생각 하게 되었습니다
Payment payment = portOneService.verifyPayment(impUid);
if (!payment.getMerchantUid().equals(merchantUid)) {
log.error("결제 merchantUid가 일치하지 않습니다: 예상 {}, 실제 {}", merchantUid, payment.getMerchantUid());
return;
}
Order order = orderRepository.findOrderWithAllByMerchantUid(merchantUid)
.orElseThrow(() -> new OrderNotFoundException("주문을 찾을 수 없습니다."));
order.setStatus(OrderStatus.ORDERED);
orderRepository.save(order);
코드를 보면 알 수 있듯이, merchantUid는 결제와 주문의 통합 관리에 필수적인 역할을 하고 안전하고 신뢰할 수 있는 결제 프로세스를 구현하게 도움을 줍니다
프론트엔드는 결제 요청 및 처리
처음 결제 기능을 구상할 때, 스프링으로 결제 기능을 구현할지, 아니면 리액트로 구현할지 고민했습니다. 그러나 최종적으로는 스프링에서는 결제 검증과 주문 상태 관리를 담당하고, 리액트는 결제 요청 및 사용자 인터페이스를 처리하는 역할로 분리하도록 결정하였고 결정한 이유는 이렇습니다 ->
스프링에서 결제 검증을 담당한 이유:
스프링은 서버 측에서의 비즈니스 로직을 처리하고 데이터의 무결성을 유지하는 데 최적화된 프레임워크라고 책과 강의를 통해서 배웠습니다. 그리고 결제 검증은 보안이 매우 중요한 작업이므로, 이에 최적화된 스프링 서버 측에서 처리함으로써 결제 데이터의 신뢰성을 보장할 수 있다고 생각이 들었습니다. 또한, 서버에서 결제 검증과 주문 상태를 관리함으로써 결제 과정이 안전하게 처리되고, 클라이언트 측에서 발생할 수 있는 보안 위험을 줄일 수 있겠다고 생각하였습니다
말을 이어가자면 먼저 프론트엔드에서는 아임포트 SDK를 사용하여 결제 요청을 처리합니다. 그러기에 앞서, 아임포트 SDK를 로드하고 초기화하는 과정이 필요합니다. 이는 결제 요청을 처리할 준비를 마치는 단계로, 아래와 같이 스크립트를 동적으로 로드하여 아임포트를 초기화합니다
const loadIamportScript = () => {
const script = document.createElement('script');
script.src = "https://cdn.iamport.kr/js/iamport.payment-1.2.0.js";
script.onload = () => {
if (window.IMP) {
window.IMP.init(' 본인의 아임포트 가맹점 식별코드);
} else {
console.error('Iamport SDK 로딩 실패');
}
};
script.onerror = () => {
console.error('Iamport SDK 작동하지 않음');
};
document.head.appendChild(script);
};
해당 코드를 통해 아임포트 SDK가 진행되며, 초기화 후 결제 요청을 처리할 준비를 합니다. 결제 요청은 사용자가 결제 버튼을 눌렀을 때 시작되도록 onClike으로 처리하였고, 사용자가 선택한 상품 정보와 결제 금액을 백엔드로 전송하여 주문을 생성하고, 아임포트에 결제 요청을 보냅니다. 결제가 성공적으로 이루어지면, 결제 검증 절차로 넘어가도록 구성하였습니당
const handlePlaceOrder = async () => {
try {
const tempOrderDTO = {
tempOrderItems: orderItems.map(item => ({
id: item.id,
quantity: item.quantity,
productOption: {
id: item.productOption.id,
color: item.productOption.color,
size: item.productOption.size,
},
productImg: item.productImg,
price: item.price,
})),
totalPrice: totalAmount,
};
const response = await axios.post('http://localhost:8080/api/order/place', tempOrderDTO, {
withCredentials: true,
});
const merchantUid = response.data;
requestPay(merchantUid);
} catch (error) {
alert(error.response.data.error);
}
};
const requestPay = (merchantUid) => {
const { IMP } = window;
IMP.request_pay({
pg: 'html5_inicis',
pay_method: 'card',
merchant_uid: merchantUid,
name: `주문명: ${orderItems.map(item => item.productName).join(', ')}`,
amount: totalAmount,
}, rsp => {
if (rsp.success) {
handlePostVerification(rsp.imp_uid, merchantUid);
} else {
console.error(rsp.error_msg);
}
});
};
"handlePlaceOrder" 함수는 사용자가 결제 버튼을 클릭할 때 실행되는 함수로 주요 역할은 사용자가 선택한 상품 정보와 결제 금액을 백엔드 서버로 보내는 것입니다. 이 정보를 바탕으로 백엔드 서버에서는 위에서 설명한 merchantUid를 생성하여 결제 요청을 준비합니다
그 다음, requestPay 함수가 실행됩니다. 이 함수는 아임포트에 결제 요청을 보내는 역할을 합니다. 해당 함수를 통해 아임포트는 결제 정보를 받아 처리하게 되고 결제가 성공적으로 이루어지게 되면 아임포트는 결제 결과를 돌려주게 됩니다
이제 결제 자체의 프로세스가 끝나고 나면 최종적으로 검증 절차가 진행됩니다. 아임포트로부터 받은 결제 정보가 백엔드에서 생성한 merchantUid와 일치하는지 확인하고, 결제가 정상적으로 처리되었는지 확인하는 것으로 마무리 합니다
백엔드는 주문 처리 및 결제 검증
백엔드에서는 프론트엔드에서 넘어온 결제 요청을 받아 주문을 생성하고, 결제를 검증합니다. 이 과정에서 주문 데이터를 통해서 Order 엔티티를 생성하고 데이터베이스에 저장하도록 합니다, 생성된 주문 정보는 아임포트에 결제를 준비시키는 데 사용이 되고 그 이후, 결제 요청 시 사용할 merchantUid를 반환하게 됩니다.
public String placeOrder(String userEmail, TempOrderDTO tempOrderDTO) {
Member member = memberRepository.findMemberWithOrdersByUserEmail(userEmail);
memberValidator.validateMember(member);
String merchantUid = generateMerchantUid();
Order order = Order.builder()
.member(member)
.status(OrderStatus.PENDING)
.merchantUid(merchantUid)
.totalPrice(tempOrderDTO.getTotalPrice())
.build();
for (TempOrderItemDTO tempOrderItemDTO : tempOrderDTO.getTempOrderItems()) {
ProductOption productOption = productOptionRepository.findById(tempOrderItemDTO.getProductOption().getId())
.orElseThrow(() -> new ProductOptionNotFoundException("해당 상품 옵션 정보를 찾을 수 없습니다"));
productOption.decreaseStock(tempOrderItemDTO.getQuantity());
OrderItem orderItem = tempOrderItemDTO.toEntity(productOption.getProduct(), productOption);
order.addOrderItem(orderItem);
}
orderRepository.save(order);
tempOrderDTO.setId(order.getId());
// 만약 결제 과정에서 문제가 발생하거나 추가 작업이 필요할 때,
// tempOrderDTO에 설정된 주문 ID를 사용해 해당 주문을 쉽게 찾고 처리하기 위함
portOneService.preparePayment(merchantUid, tempOrderDTO.getTotalPrice());
return merchantUid;
}
주문 데이터를 바탕으로 Order 엔티티를 생성하고 이를 데이터베이스에 저장합니다. 이후 아임포트에 결제를 준비시키며, 결제 검증을 위한 merchantUid를 반환합니다.
결제가 성공적으로 이루어진 후에는 백엔드에서 결제의 유효성을 검증하고, 주문 상태를 업데이트를 진행하구, completeOrder 메서드는 결제 성공 후 호출되며, 아임포트로부터 결제 정보를 검증하고, 주문 상태를 ORDERED로 변경한 후 저장합니다. 주문 상태를 바꾸는 이유는, 추후에 MYPAGE에서 사용자의 결제 정보를 띄우기 위함입니다.
이러한 결제와 검증을 분리한 뒤, 프로세스를 순서에 맞게 진행하게 되면 주문이 정상적으로 완료되었는지 확인하고, 주문 상태를 업데이트합니다
마무리
이번에 아임포트를 활용하여 쇼핑몰의 결제 시스템을 구현하고 어떻게 프로세스를 나누었는지를 소개하였는데요, 아임포트를 사용하면 개발자 입장에선 손 쉽게 결제 과정을 쉽게 처리할 수 있으며, 프론트엔드와 백엔드가 연동되어 결제부터 검증까지의 과정을 매끄럽게 진행할 수 있었습니다
SDK와 merchantUid를 활용하여 각 결제 건을 관리하고, 안전하게 결제를 처리할 수 있는 방법을 이해하고 응용해보았고, 이 과정을 통해 사용자에게 개발자 입장에서도 손쉽게 보안성을 제공하게 되었네용
'spring' 카테고리의 다른 글
| AOP와 스프링 AOP란? (0) | 2024.09.23 |
|---|---|
| 스프링 세션과 Redis 응용 (1) | 2024.09.18 |
| [MileStone 프로젝트] 계층형 데이터베이스 구축 - JPA (0) | 2024.08.14 |
| [MileStone 프로젝트] EAGER // LAZY (로딩 전략) (0) | 2024.08.11 |
| [MileStone 프로젝트] SQL 쿼리 최적화와 성능 향상 (0) | 2024.08.07 |