결제시스템은 결제를 한다고 다가 아닙니다.
아이엠포트를 결제를 한다고 치면 프론트에서 결제 창이 뜨고 결제 완료가 됩니다.
그리고 그 결제완료된 내용을 서버로 보내고 서버에선 디비에 저장하게 됩니다.
그런데 프론트에서 결제 승인이 났는데 뜻밖의 이슈로 서버가 다운됐다고 쳐봅시다.
그럼 유저 입장에서 돈은 빠졌나갔는데 자신의 결제기록을 볼 수 없죠?
그러므로 안전하게 이를 핸들링하기 위해 상태값 중 결제 대기 상태를 만듭니다.
그래서 유저가 프론트에서 아이엠포트를 이용하기 전에 결제 대기 상태로 서버에 데이터를 보내줍니다.
결제 대기를 포함한 아이엠포트 서버 코드 예시들을 보여드리겠습니다.
우선 결제 대기 로직입니다.
// 결재 대기 로직
WaitAccept : async (req, res) => {
try {
let {payment, user_id} = req.body;
const rows = await pay_report.create({
user_id : user_id,
pay_method : payment.pay_method,
merchant_uid : payment.merchant_uid,
paid_amount : payment.amount,
buyer_name : payment.buyer_name,
buyer_email : payment.buyer_email,
buyer_tel : payment.buyer_tel,
buyer_addr : payment.buyer_addr,
buyer_postcode : payment.buyer_postcode,
card_name : payment.card_name,
bank_name : payment.bank_name,
card_quota : payment.card_quota,
card_number : payment.card_number,
is_complete : 1
})
if(rows) return res.code(200).send({result : true});
else throw "에러";
} catch (error) {
return res.code(200).send({error : error});
}
},
그리고 결제가 정상적으로 완료되면 아래의 예시 코드가 되겠죠
// 결재 로직
Accept : async (req, res) => {
try {
let {payment, user_id} = req.body;
const getToken = await axios.post('https://api.iamport.kr/users/getToken', {
imp_key : process.env.imp_key,
imp_secret : process.env.imp_secret
})
// 실제로 결제내역이 있는지 아이엠포트 서버에서 확인
const retval = await axios.post(util.format('https://api.iamport.kr/payments/%s',payment.imp_uid),{
}, {
headers: {
Authorization: getToken.data.response.access_token,
}
});
// 아이엠포트 서버에 결제내역이 없을 경우
if (retval.data.response.status != "paid") {
throw "에러";
}
// 아이엠포트 서버상의 가격과 유저의 요청 가격 맞는지 비교
if(retval.data.response.amount != payment.paid_amount){
throw "에러";
}
// 이전 결재대기 로직 불러와서 결재완료 시키기
const rows = await pay_report.update({
is_complete : 0,
imp_uid : payment.imp_uid
},{
where :{user_id : user_id, merchant_uid : payment.merchant_uid}
})
console.log(rows)
if(rows) return res.code(200).send({result : true});
else throw "에러";
} catch (error) {
return res.code(200).send({error : error});
}
},
결제 취소 신청은 이렇게 됩니다.
Cancel : async (req, res) => {
try {
let {merchant_uid, reason} = req.body;
const rows = await pay_report.findOne({
where : {merchant_uid : merchant_uid}
})
if (!rows) throw "에러";
const getToken = await axios.post('https://api.iamport.kr/users/getToken', {
imp_key : process.env.imp_key,
imp_secret : process.env.imp_secret
})
const getCancelData = await axios.post('https://api.iamport.kr/payments/cancel', {
reason : reason, // 가맹점 클라이언트로부터 받은 환불사유
imp_uid : rows.imp_uid, // imp_uid를 환불 `unique key`로 입력
amount: rows.paid_amount, // 가맹점 클라이언트로부터 받은 환불금액
checksum: rows.paid_amount // [권장] 환불 가능 금액 입력
},
{
headers: {
Authorization: getToken.data.response.access_token,
},
})
if(getCancelData.data.code == 0){
// 업데이트 실패 시, 기록 따로 저장하는 로직 필요
await pay_report.update({
is_complete: 2,
cancel_reason : reason
}, {
where : { merchant_uid : merchant_uid}
})
return res.code(200).send({result : true});
}else{
return res.code(200).send({result : false});
}
} catch (error) {
return res.code(200).send({error : error});
}
},
결제 기록 가져오기입니다.
GetPayment : async (req, res) => {
try {
let {user_id} = req.query;
const rows = await pay_report.findAll({
where : {user_id : user_id,is_complete : 0},
order: [["createdAt", "desc"]]
})
if (!rows) throw "에러";
return res.code(200).send(rows);
} catch (error) {
return res.code(200).send({error : error});
}
},
자 이제 결제 대기가 되어있는 상태에서 프론트에서 결제가 완료되었고 서버에 데이터를 보내주기 전에 서버가 다운되어 있는 상태라고 가정해봅시다.
서버에 데이터가 넘어오지는 않았지만 실제로 결제가 완료된 내용은 결제 대기가 아닌 결제 완료로 변경해줘야 합니다.
이럴땐 아이엠포트 api 를 이용하여 결제된 내용을 조회할 수 있습니다.
방법은 두가지 인데요.
첫번째는 디비에서 결제 대기 상태인 데이터들만 가져와서 아이엠포트 api 로 요청을 날려서 결제 완료인 결제건이 있으면 결제 완료로 바꿔주는 배치 파일을 만들어주는 것입니다.
두번째는 프론트에서 서버로의 요청이 실패하면 사이드 서버로 결제 내역을 보내준 뒤, 사이드 서버에서 카프카에 그 내용을 저장합니다. 그리고 카프카에 있는 데이터를 지속적으로 소비하면서 본서버에 결제완료 api 요청을 보내고 요청이 실패할 시, 다시 카프카에 담는 방법입니다.
어느 방법으로 할지는 개발자가 어떻게 대응할지에 따라 다른데요. 첫번째 방식으로 하려면 해당 에러를 개발자가 알림받을 수 있게 세팅을 해서 에러가 발생하면 바로 배치 파일을 돌리는 방법이고, 두번째는 개발자가 즉각적으로 대응하지 않아도 주기적으로 카프카가 데이터를 관리해주므로 편리하지만 서버를 따로 배포해야하므로 리소스가 요구됩니다.
코드 예시를 보여드리겠습니다.
(모듈이나 자세한 세팅방법은 생략했습니다. 자세한 내용은 본문 하단에 링크된 깃에서 확인해주세요)
우선 배치파일 코드 예시입니다.
const getInfo = async () => {
const getToken = await axios.post('https://api.iamport.kr/users/getToken', {
imp_key : process.env.imp_key,
imp_secret : process.env.imp_secret
})
const rows = await pay_report.findAll({
where : {is_complete : 1},
})
for(const element of rows){
const retval = await axios.post(util.format('https://api.iamport.kr/payments/find/%s/paid',element.merchant_uid),{
}, {
headers: {
Authorization: getToken.data.response.access_token,
}
});
if (retval.data.response.status == "paid" && retval.data.response.amount == element.paid_amount) {
await pay_report.update({
is_complete : 0,
imp_uid : retval.data.response.imp_uid
},{
where :{user_id : element.user_id, merchant_uid : element.merchant_uid}
})
}else{
console.log("해당 없음")
}
};
}
getInfo();
사이드 서버 예시입니다.
app.post('/api/v1/payment/sendside', async (req, res) => {
try {
let {payment, user_id} = req.body;
// user_id 토큰 해쉬하는 로직 있어야 함
const data = JSON.stringify(payment)
await producer.send({
topic: "iamport_kafka",
messages: [
{
value : Buffer.from({"data":data,"user_id":user_id}),
}],
});
return res.code(200).send({result : "success"});
} catch (error) {
return res.code(200).send({error : "error"});
}
})
const consumerRun = async () => {
await consumer.connect();
await consumer.subscribe({ topic: "iamport_kafka", fromBeginning: true });
await consumer.run({
eachMessage: async({topic, partition, message}) => {
let data = await JSON.parse(message.value.toString())
try {
// 본 서버로 카프카 데이터 꺼내서 요청
await axios.post('http://localhost:8081/api/v1/payment/accept', {
payment : data.data,
user_id : data.user_id
})
} catch (err) {
// 통신 실패하면 다시 카프카에 담기
await producer.send({
topic: "iamport_kafka",
messages: [
{
value : message.value
}],
});
}
}
})
}
app.listen({ port: 8082 }, (err) => { if (err) throw err }, () => {
console.log("running on port 8082");
});
kafkaConnect();
consumerRun().catch(err => console.log("kafka err : ", err))
이렇게 하면 결제 도중 서버가 다운되어도 결제 기록이 날아가지 않고 안전하게 유저에게 결제 기록을 보여줄 수 있는 결제 시스템을 만들 수 있습니다.
프론트에서 바로 카프카에 안담고 왜 서버로 보내는가?
- 카프카에 JWT같은 인증시스템이 없으니까 보안이 안좋다. 프론트에서 바로 MYSQL접근 안하게 하는거랑 같은 원리이므로 서버로 보내줘야 하는 함
왜 사이드 서버에서 바로 디비에 업데이트하면 안되고 다시 본 서버로 api 요청 날려서 데이터 내용 수정해야하는가?
- 일반적으로 디비에 대한 정보를 많은 프로젝트가 가지고 있는건 보안에 안좋다. 싱크서버같이 어쩔수없는 경우가 있겠지만, 저런건 굳이 없어도 되니 차라리 API요청을 하는게 좋다.
감사합니다.
전체 코드는 제 깃에서 확인해주세요. : https://github.com/fkwsur/Node_Iamport_Payment
'Node.js' 카테고리의 다른 글
[Node.js] prototype 사용예시 (0) | 2023.04.26 |
---|---|
[Node.js] prototype 이란 (0) | 2023.04.26 |
[Node.js] RabbitMQ (0) | 2021.07.25 |
[Node.js] artillery, pm2, morgan 모듈을 이용한 로드밸런싱 (0) | 2021.07.09 |