되자!백엔드개발자

[Spring] 카카오페이 연동 구현 본문

개발공부/Spring

[Spring] 카카오페이 연동 구현

HyunJng 2024. 2. 10. 16:35

회사업무로 카카오페이 인증쪽 연동하는 일을 맡게되었다.

한번 복습해볼겸 집에서 정말 간단화해서 구현한걸 기록 겸 작성해본다.

정말 간단하게 해서 DB 아예 안썼다. 실무에서 이러면 큰일난다.

서버간통신 예제 겸 보면 좋겠다.

목표

- 카카오페이 인증창 띄우기

개발환경

- JAVA 17

- Spring 3.2.2

 

필요 Gradle

    implementation 'org.springframework.boot:spring-boot-starter-web'
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
    implementation 'com.google.code.gson:gson:2.9.0'

 


결제 페이지 : kakaopay.html

<html>
<meta charset="UTF-8" />
<body>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<div>
    <h2>카카오페이연습</h2>
    <form id="payInfo">
        <input type="text" name="ordId" value="20240210">
        <input type="text" name="userId" value="한장">
        <input type="text" name="itemNm" value="휴지">
        <input type="text" name="quantity" value="3">
        <input type="text" name="itemAmt" value="1004">
        <input type="text" name="freeAmt" value="0">
        <button id="submitBtn" type="button">결제</button>
    </form>
</div>
</body>

<script>

    $(document).ready(function(){
        $("#submitBtn").on("click", function () {
            var formData = $("#payInfo").serialize();

            $.ajax({
                type: "POST",
                url: "/pay/kakao",
                data: formData,
                dataType: "json",
                success: function (result) {
                    window.open(result.next_redirect_pc_url, "kakaopay_pop", "width = 500, height = 500, top = 100, left = 200, location = no");
                },
                error: function (error) {
                    console.log("결제 실패");
                    console.log(error);
                },
                complete: function (xhr, status) {
                    console.log("Request completed with status: " + status);
                }
            });
        });
    })

- 결제 버튼을 누르면 ajax를 통해 /pay/kakao로 데이터를 전달한다.

- 데이터가 받아지면 popup창을 띄워 next_redirect_pc_url을 띄우게 된다.

 

ModelAttribute용 : KakaopayReq

@ToString
@Getter @Setter
@NoArgsConstructor
public class KakaopayReq {
    private String ordId;
    private String userId;
    private String itemNm;
    private int quantity;
    private int itemAmt;
    private int freeAmt;

    @Builder
    public KakaopayReq(String ordId, String userId, String itemNm, int quantity, int itemAmt, int freeAmt) {
        this.ordId = ordId;
        this.userId = userId;
        this.itemNm = itemNm;
        this.quantity = quantity;
        this.itemAmt = itemAmt;
        this.freeAmt = freeAmt;
    }
}

- kakaopay.html 에서 전달한 데이터를 받기 위해 DTO 객체이다. 

 

카카오 전달용 VO : KakaopayReqVo

@Getter
@ToString
public class KakaopayReqVo {
    private String cid;
    private String partner_order_id;
    private String partner_user_id;
    private String item_name;
    private Integer quantity;
    private Integer total_amount;
    private Integer tax_free_amount;
    private String approval_url;
    private String cancel_url;
    private String fail_url;

    public KakaopayReqVo(String cid, String receiveUrl, KakaopayReq kakaopayReq) {
        this.cid = cid;
        this.partner_order_id = kakaopayReq.getOrdId();
        this.partner_user_id = kakaopayReq.getUserId();
        this.item_name = kakaopayReq.getItemNm();
        this.quantity = kakaopayReq.getQuantity();
        this.total_amount = kakaopayReq.getItemAmt();
        this.tax_free_amount = kakaopayReq.getFreeAmt();
        this.approval_url = receiveUrl;
        this.cancel_url = receiveUrl;
        this.fail_url = receiveUrl;
    }
}

- 카카오에게 전달하기 위해서는 카카오가 사용하는 변수명으로 변경해야 하므로 사용한다..

- 또한 사용자에게 받는 데이터 외에도 개발자가 설정해야하는 데이터도 존재하는데 여기서 설정해준다.

  . 카카오페이는 취소, 승인, 실패일 경우 호출할 URL을 줘야하는데 나는 "/pay/kakao/receive"로 통합해서 처리하도록 할 예정이기에 receiveURL하나만 받도록 구현했다.

 

 

PayService:toKakaoServer

@Slf4j
@Service
public class PayService {
    @Autowired
    Gson gson;

    public <T> T toKakaoServer(String secretKey, String url, Object reqBody, Class<T> resp) {
        URI uri = UriComponentsBuilder
                .fromUriString("https://open-api.kakaopay.com")
                .path(url)
                .build()
                .toUri();

        HttpHeaders httpHeaders = new HttpHeaders();
        httpHeaders.set("Authorization", "SECRET_KEY " + secretKey);
        httpHeaders.setContentType(MediaType.APPLICATION_JSON);
        HttpEntity<Object> requestsMessage = new HttpEntity<>(reqBody, httpHeaders);

        RestTemplate restTemplate = new RestTemplate();
        ResponseEntity<T> tResponseEntity = restTemplate.postForEntity(uri, requestsMessage, resp);
        if (!tResponseEntity.getStatusCode().equals(HttpStatus.OK)) {
            throw new RuntimeException("서버간 통신 에러발생 = " + tResponseEntity);
        }
        return tResponseEntity.getBody();
    }
}

- 카카오 서버에 서버간 통신을 하는 메서드이다.

- RestTemplate을 사용했고 JSON형태로 전달하고 매개변수인 resp의 클래스 형으로 데이터를 전달받도록 구현했다.

카카오서버가 전달하는 정보 받는 VO: KakaopayAuthResVo

@AllArgsConstructor
@NoArgsConstructor
@Setter @Getter
public class KakaopayAuthResVo {
    private String tid;
    private String next_redirect_mobile_url;
    private String next_redirect_pc_url;
    private LocalDateTime created_at;
}

- 카카오페이가 요청에 대한 답으로 보내는 정보를 담을 객체이다.

 

PayController: kakaopayAuth

@Slf4j
@Controller
public class PayController {
    private final String CID = "TC0ONETIME";
    private final String SECRETKEY = "{본인의 secretKey}";
    private final String SERVER_DOMAIN = "http://127.0.0.1:8080";

    @Autowired
    PayService payService;

    @ResponseBody
    @PostMapping("/pay/kakao")
    public ResponseEntity<KakaopayAuthResVo> kakaopayAuth(KakaopayReq kakaopayReq) {
        KakaopayReqVo kakaopayReqVo = new KakaopayReqVo(CID, SERVER_DOMAIN + "/pay/kakao/receive", kakaopayReq);
        KakaopayAuthResVo response = payService.toKakaoServer(SECRETKEY, "/online/v1/payment/ready", kakaopayReqVo, KakaopayAuthResVo.class);
        return ResponseEntity.ok(response);
    }
}

- CID는 카카오가 테스트용으로 제공해주는 CID를 사용했다. SECRETKEY는 본인의 것을 사용하면 된다.

- 실제 CID, SECRETKEY는 DB, 혹은 application.properties에 안전하게 보관을 해야겠지만 나는 귀찮...기 때문에 하드코딩했다.

여기까지 하면 KakaopayAuthResVo에는 이런 정보를 받를 담게 된다.

HTTP/1.1 200 OK
Content-type: application/json;charset=UTF-8
{
  "tid": "T1234567890123456789",
  "next_redirect_app_url": "https://mockup-pg-web.kakao.com/v1/xxxxxxxxxx/aInfo",
  "next_redirect_mobile_url": "https://mockup-pg-web.kakao.com/v1/xxxxxxxxxx/mInfo",
  "next_redirect_pc_url": "https://mockup-pg-web.kakao.com/v1/xxxxxxxxxx/info",
  "android_app_scheme": "kakaotalk://kakaopay/pg?url=https://mockup-pg-web.kakao.com/v1/xxxxxxxxxx/order",
  "ios_app_scheme": "kakaotalk://kakaopay/pg?url=https://mockup-pg-web.kakao.com/v1/xxxxxxxxxx/order",
  "created_at": "2023-07-15T21:18:22"
}

나는 지금 PC로 결제창을 띄우는 것을 구현하고 있기에 next_redirect_pc_url을 Popup창으로 띄우게 된다.

여기까지만 하면 아래와 같이 화면이 띄워지는걸 확인할 수 있다.

이제 좀만 더 보충해보겠다.

보충1. TID 보관

- tid는 승인을 보낼 때 필요한 정보이므로 DB에 저장하여 보관할 필요가 있다.

- 실제로 구현했을 때는 DB에 보관했는데 지금은 DB를 사용하지 않으므로 프론트에 보관하도록 하겠다.

 

결제 페이지 : kakaopay.html에 tid저장 추가

          $.ajax({
                .... (생략)
                success: function (result) {
                    window.open(result.next_redirect_pc_url, "kakaopay_pop", "width = 500, height = 500, top = 100, left = 200, location = no");
                    var newInput = $("<input>").attr({ // 추가
                        type: "hidden",
                        name: "tid",
                        value: result.tid
                    });
                    $("#payInfo").append(newInput);

                },
                .... (생략)
              });

 

- 결제창을 띄우고 나면 tid가 저장된 것을 확인할 수 있다.

보충2: 취소를 누를 경우, QR을 진행할 경우

위에서 카카오에게 전달한 approval_url, cancel_url에 똑같이 /pay/kakao/receive를 담아서 전달했었다.

저 두 경우는 카카오가 pg_token을 전달했느냐로 구분할 수 있다.

PayController

// PayController
    @GetMapping("/pay/kakao/receive")
    public String kakaoReceive(@RequestParam(name = "pg_token", required = false)String token, Model model) {
        model.addAttribute("resultCd", "9999");
        model.addAttribute("token", "null");
        if (!Objects.isNull(token)) {
            model.addAttribute("token", token);
            model.addAttribute("resultCd", "0000");
        }
        return "kakaoReceive";
    }

- 사실 토큰도 화면단에 전달하면 안될 것 같은 정보고 실전에서는 DB에 저장해서 사용했지만 여기서는 DB를 전혀 사용하지 않기에 Mode에 저장하여 전달했다.

kakaoReceive.html

<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<meta charset="UTF-8" />
<body>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
</body>
<script>
    $(document).ready(function(){
        let resultCd = "[[${resultCd}]]"
        let token = "[[${token}]]";
        let data = {"token": token, "resultCd" : resultCd};
        window.opener.postMessage(data, "*");
        window.close();
    })
</script>
</html>

- 부모창인 kakaopay.html에 취소에 대한 정봅 혹은 승인(token)에 대한 정보를 전달한다.

kakaopay.html

...(생략)
    window.addEventListener("message", function (result) {
        if (result.data.resultCd == "0000") {
            let _form = $("#payInfo");
            let newInput = $("<input>").attr({
                type: "hidden",
                name: "token",
                value: result.data.token
            });
            _form.append(newInput);
            _form.attr({
                method: "post",
                action: "/pay/kakao/approve"
            });
            _form.submit();
        } else {
            $("#payInfo > input[name='tid']").remove();
        }
    }, false);

- 결제 결과가 성공코드이면 token을 저장하여 승인 로직으로 전송하도록 한다.

- 위 방식대로 하면 재시도 할 때 tid가 중복될 수 있으므로 취소일 경우 tid를 삭제해주는 코드도 넣어주었다.

 

PayController:kakaoApprove

    @PostMapping("/pay/kakao/approve")
    public String kakaoApprove(KakaopayApproveReq approveReq, Model model) {
        KakaoApprovReqVO kakaoApprovReqVO = new KakaoApprovReqVO(CID, approveReq);
        Map response = payService.toKakaoServer(SECRETKEY, "/online/v1/payment/approve", kakaoApprovReqVO, Map.class);
        model.addAttribute("res", response);
        return "kakaoResult";
    }

 

- 카카오에 전송하는 메서드는 이전에 만들어 둔 것을 재사용할 수 있어서 Controller코드만 짜면 끝이다.

- 카드를 사용하느냐,카카오페이머니를 사용하느냐에 따라 결과값이 달라지므로 Map으로 받을 수 있도록 하였다.

- 결과 페이지는 ... 귀찮아서 안 만들었다.

완성본