카카오 페이 (1)

MeteorLee·2023년 6월 7일
0

Team 프로젝트

목록 보기
2/6
post-thumbnail

카카오 페이 구현

이번 기업 연계 프로젝트 기간에 클라이언트(CEO)분께서 카카오 페이의 구현을 강하게 요구하셨다. 그리고 UI/UX 분들 또한 이번 프로젝트에서 결제 기능의 구현을 백엔드 팀과 상의하게 되었다. 사실 처음에는 결제 기능이 백엔드 팀원들에게 다루지 않았던 부분이었기에 진행하지 않는 방향으로 얘기가 되었다. 하지만 클라이언트 분께서 조금은 강하게 결제 부분이 있었으면 좋겠다고 명확하게 요구하셨고 UI/UX 팀 또한 이 부분을 강하게 어필하였다. 이런 요구 사항을 받아 백엔드 팀에서 상의 결과, 자신이 잘 알지 못하는 개발을 해야하는 과정에서 필연적으로 공부할 내용이 많아질 것이고 만약 구현이 어려울 경우 굉장히 많은 시간을 투자해야할 것이라고 판단했다. 따라서 결제 기능을 구현하고자 하는 인원이 지원할 경우에만 결제 기능을 구현하기로 했다.

내가 지원한 이유

사실 처음 결제 기능에 지원할 생각은 없었다. 이번 프로젝트를 진행하며 처음 생각한 부분은 프로젝트를 배포하는 AWS와 관련된 기능과 스프링 시큐리티와 JWT 관련된 기능을 담당하고 싶었다. 하지만 클라이언트분이 결제 기능의 구현을 크게 원하고 계셨기에 생각을 바꾸게 되었습니다.

외부 API를 이용한 구현은 쉽지 않다.

이제까지 많은 API나 라이브러리 등을 이용하여 기능을 구현할 때 정말 매우 많이 친절한 방식을 제공 받았고 조금만 구글링 하거나 chatGPT와 같은 AI 도움으로도 쉽게 해결할 수 있는 내용이 대부분이었다. 즉, 강의나 책에서 나온 내용은 내가 조금만 고민해도 구현할 수 있었고 내가 막히는 부분에서 쉽게 해결할 수 있었고 정답과 같은 코드도 많이 존재했다.

하지만 이제는 예시로 주어진 코드가 아닌 내가 외부 API에서 제공해주는 문서를 보고 나만의 코드를 통해 API를 구현하는 방식을 익혀야 했고 많은 시행착오 끝에 문서를 보고 코드를 만드는 능력을 기르는데 성공했다.

결제 기능 구현을 위한 과정

카카오 페이 문서

카카오 developers

Step 1.결제 준비
서버에서 결제 준비 API를 호출합니다. 응답이 오면 요청한 결제와 TID를 매핑(Mapping)하여 저장하고, 추후 결제 승인 API 호출 및 대사 작업에 사용합니다. 보안을 위해 TID가 사용자에게 노출되지 않도록 주의합니다. 응답 본문으로 받은 next_redirect_pc_url 값으로 결제 대기 화면을 팝업(Popup) 혹은 레이어(Layer) 방식으로 띄웁니다.

Step 2.사용자 정보 입력
사용자는 전화번호 및 생년월일을 입력합니다. 올바른 정보가 입력되면 카카오톡으로 결제 요청 메시지가 전송되고, 사용자 정보 입력 화면은 결제 대기 화면으로 변경됩니다. 사용자는 결제 요청 메시지를 통해 결제 화면으로 이동해 결제 수단을 선택합니다.

Step 3.결제 대기
결제 대기 화면은 결제 요청 결과에 따라 각각 다른 URL로 리다이렉트(redirect)됩니다. 사용자가 결제를 취소한 경우, 보안을 위해 주문 상세 조회 API를 호출하고 결제 과정을 중단해야 합니다. 조회 시 상태 값이 사용자가 결제를 중단한 상태임을 나타내는 QUIT_PAYMENT인 것을 확인하고 결제 중단 처리를 해야 합니다.

요청 결과별 리다이렉트 URL
요청 성공: approval_url로 리다이렉트
요청 취소: cancel_url로 리다이렉트
요청 유효 시간(15분) 경과: fail_url로 리다이렉트

approval_url, cancel_url, fail_url은 카카오페이 API 요청 응답을 받아 처리할 주소입니다. 이 값들의 도메인(Domain)은 앱 정보에 등록된 웹 플랫폼 도메인과 일치해야 합니다.

Step 4.결제 승인
결제 대기 단계에서 결제 요청에 성공했다면 결제 승인 API를 호출합니다. 이때 결제 승인 요청을 인증하는 pg_token을 전달해야 합니다. pg_token은 사용자가 결제 수단을 선택하고 결제 버튼을 눌러 approval_url로 리다이렉트될 때, 리다이렉트 요청의 approval_url에 포함된 query string으로 전달 받습니다.

응답을 받으면 결제 결과를 저장하고 사용자에게 결제 완료 화면을 보여줍니다. 결제 승인 API의 동작에 따라 사용자도 결제 완료 메시지를 받습니다.

내가 겪은 어려움

카카오 페이 단건 결제에 관련된 문서를 읽으면서 가장 크게 느꼈던 점은 생소하다는 느낌이었다. 내가 이때까지 만났던 API들은 99% 완성되어 내가 원하는 매개 변수만 입력하면 되었었다. 하지만 카카오 개발자 문서에서는 주고 받는 방식과 주고 받는 정보의 2가지만을 기술하고 있었기에 '어떻게 구현을 시작해야하는 거지?'는 생각에 쉽게 개발을 시작하지 못 했었다.

거기다가 이때까지 내가 구현한 API는 전부 하나의 요청과 하나의 응답으로 이루어져 있었지만 카카오 페이의 결제 API는 준비와 승인의 2번의 요청과 응답으로 이루어져 있었기에 더욱 이해하는 데 힘이 들게 되었다.

그리고 카카오 서버에서 응답받는 정보들을 어떤 방식으로 방식으로 받고 이름이나 객체는 어떻게 처리할 것인지에 대한 내용도 쉽지 않았다.

단일 요청, 응답 따라하기

내가 직접 문서를 보고 이해하며 코드를 작성할 수 있다면 좋겠지만 내 실력이 부족한 것을 알기에 클론 코딩으로 일단 따라서 구현해보려고 했다. 다행히 카카오 페이의 구현을 다룬 곳이 꽤 있었기에 다행이라고 생각한다.

일단 유튜브의 스프링 카카오 페이 API 구현을 보면서 카카오 서버에 요청을 보내는 로직을 구현했다.

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import java.io.*;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;

@Controller
@RequestMapping("/pay")
public class PayController {

    @ResponseBody
    @RequestMapping("/kakao/single")
    public String singleKakaopay() {

        try {
            URL url = new URL("https://kapi.kakao.com/v1/payment/ready");
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
            conn.setRequestMethod("POST");

            // 카카오 인증 APP_ADMIN_KEY 입력
            conn.setRequestProperty("Authorization", "KakaoAK " + ADMIN키);
            // content-type
            conn.setRequestProperty("Content-type", "application/x-www-form-urlencoded;charset=utf-8");
            // 연결에서 받을 정보가 있다
            conn.setDoOutput(true);

			// 카카오 서버로 보낼 정보
            String parameter = "cid=TC0ONETIME" // 가맹점 코드
                    + "&partner_order_id=partner_order_id" // 가맹점 주문번호
                    + "&partner_user_id=partner_user_id" // 가맹점 회원 id
                    + "&item_name=초코파이" // 상품명
                    + "&quantity=1" // 상품 수량
                    + "&total_amount=5000" // 총 금액
                    + "&vat_amount=200" // 부가세
                    + "&tax_free_amount=0" // 상품 비과세 금액
                    + "&approval_url=http://localhost/" // 결제 성공 시
                    + "&fail_url=http://localhost/" // 결제 실패 시
                    + "&cancel_url=http://localhost/"; // 결제 취소 시


            OutputStream outputStream = conn.getOutputStream();
            DataOutputStream dataOutputStream = new DataOutputStream(outputStream);
            dataOutputStream.writeBytes(parameter);
            dataOutputStream.close();

            int result = conn.getResponseCode();

            InputStream inputStream;
            if (result == 200) {
                inputStream = conn.getInputStream();
            } else {
                inputStream = conn.getErrorStream();
            }

            InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
            BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
            String s = bufferedReader.readLine();

            System.out.println(s);

            return s;

        } catch (MalformedURLException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }


    }

}
  • https://kapi.kakao.com/v1/payment/ready 주소로 요청
  • POST 방식
  • 요청 헤더에 문서에서 지정한 헤더 붙이기
    • Authorization : "KakaoAK " + ADMIN키
    • Content-type : "application/x-www-form-urlencoded;charset=utf-8"
  • 문서에서 필수로 입력되어 있는 파라미터들 입력
  • 요청을 받아서 출력해보면 문서에서 명시해준 변수명으로 응답함 (tid, next_redirect_app_url, next_redirect_pc_url ...)

카카오 개발자 문서에서 적힌 정보들을 통해 이제는 내가 요청을 할 때 어떤 형식을 취해야 하는지 명확하게 이해하게 되었다. 응답에서는 어떤 내용의 응답이 돌아오고 응답에 담긴 정보를 다룰 수 있는 방법에 대한 이해도도 높아졌다.

요청 방식을 스프링 방식으로 변경

위의 방식은 자바의 스트림을 이용한 방식이다. 물론 이번 프로젝트에서 자바를 사용하기에 따라 사용해도 되지만 스프링에서 제공하는 RestTemplate을 사용하여 더 좋은 방식으로 요청을 보내는 방식을 선택했다.

@Service
@RequiredArgsConstructor
@Transactional
public class KakaoPayService {

    private final OrderRepository orderRepository;

	static final String cid = "TC0ONETIME"; // 가맹점 테스트 코드
    static final String admin_Key = ADMIN 키; // ADMIN 키

    /**
     * 결제 요청
     * 
     * @param requsetDTO
     * @return
     */
    public KakaoReadyResponse kakaoPayReady(KakaoPayRequsetDTO requsetDTO) {

        // 카카오페이 요청 양식
        MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();

        String partner_order_id = requsetDTO.getPartner_order_id();
        Orders order = this.getOrdersByPartnerOrderId(partner_order_id);


        // 서버와 주고 받을 정보
        parameters.add("cid", cid);
        parameters.add("partner_order_id", partner_order_id);
        parameters.add("partner_user_id", order.getUser().getEmail());
        parameters.add("total_amount", String.valueOf(order.getTotalPrice()));
        parameters.add("item_name", "아이템 이름");
        parameters.add("quantity", "5"); // 아이템 갯수

        // 부가세, 비과세 금액으로 현재는 0으로 설정
        parameters.add("tax_free_amount", "0"); // 상품 비과세 금액 일단 0으로 설정
//        parameters.add("vat_amount", "0"); // 상품 부가세 금액 필수 아님, 없으면 0 설정
        
        parameters.add("approval_url", "http://52.78.88.121:8080/account/pay/kakao/success"); // 성공 시 redirect url
        parameters.add("cancel_url", "http://52.78.88.121:8080/account/pay/kakao/cancel"); // 취소 시 redirect url
        parameters.add("fail_url", "http://52.78.88.121:8080/account/pay/kakao/fail"); // 실패 시 redirect url

        // 파라미터, 헤더
        HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<>(parameters, this.getHeaders());

        // 외부에 보낼 url
        RestTemplate restTemplate = new RestTemplate();

        KakaoReadyResponse response = null;
        try {
            response = restTemplate.postForObject(
                    "https://kapi.kakao.com/v1/payment/ready",
                    requestEntity,
                    KakaoReadyResponse.class);
        } catch (RestClientException e) {
            throw new KakaoSinglePaymentReadyException();
        }


        return response;
    }
    
    
	/**
     * 결제 승인
     * 
     * @param pgToken
     * @param partner_order_id
     */
    public void approveResponse(String pgToken, String partner_order_id) {

        // 카카오 요청
        MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
        parameters.add("cid", cid);
        
        // DB 접근하여 주문 정보 가져오기
        Orders orders = this.getOrdersByPartnerOrderId(partner_order_id);

        parameters.add("tid", orders.getPgUid());

        parameters.add("partner_order_id", String.valueOf(orders.getNumber()));
        parameters.add("partner_user_id", orders.getUser().getEmail());
        parameters.add("pg_token", pgToken);

        // 파라미터, 헤더
        HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<>(parameters, this.getHeaders());

        // 외부에 보낼 url
        RestTemplate restTemplate = new RestTemplate();

        try {
            KakaoApproveResponse approveResponse = restTemplate.postForObject(
                    "https://kapi.kakao.com/v1/payment/approve",
                    requestEntity,
                    KakaoApproveResponse.class);
        } catch (RestClientException e) {
            throw new KakaoSinglePaymentApproveException();
        }

    }
    
    
    /**
     * 카카오 요청 header 생성
     *
     * @return
     */
    private HttpHeaders getHeaders() {
        HttpHeaders httpHeaders = new HttpHeaders();

        String auth = "KakaoAK " + admin_Key;

        httpHeaders.set("Authorization", auth);
        httpHeaders.set("Content-type", "application/x-www-form-urlencoded;charset=utf-8");

        return httpHeaders;
    }
}

별개의 요청이 가지는 문제점

카카오 개발자 문서에 있는 결제 준비와 결제 승인의 두 가지 요청을 보내고 응답을 받아 정보를 이용하는 로직을 구현하는데는 성공했지만 결국 두 가지의 요청은 별개의 요청인 것이다. Http요청은 이전의 요청을 저장하지 않기에 두가지의 요청을 하나의 흐름에 올려두는 일이 필요했다.

그리고 결제는 단순하게 요청 두 번을 프런트에서 받아서 시행하는 방식이 아니기에 결제에대한 전체적인 흐름을 이해하는 것이 필요했다. 유저 - 프런트 - 백엔드 - DB - 백엔드 - 프런트 - 유저의 단순한 흐름이 아닌 결제만의 고유한 흐름을 명확하게 이해하는 것이 중요했다.

구현

글이 너무 길어져서 결제 흐름부터는 다음 글에서 작성할 예정이다.

구현

느낀점

문서를 통해 API를 구현하는 어려움을 극복하며 느낀 점은 컸따. 하지만 단순한 어려움을 극복한 것뿐만 아니라 실제 고객(CEO)분이 존재하고 요구하신 요구 사항과 기한을 생각한 협업을 진행하며 느낀 점도 적지 않았다.

문서만을 통한 구현

외부 API를 이용하기 위해 이제까지 해왔던 연습이나 클론 코딩없이 구현하는 것은 쉽지 않다는 것을 크게 느꼈다. 웹을 다루는 백엔드 개발자를 꿈꾸는 만큼 잘 해야하는 분야이기에 이번에 확실하게 이해하기 위해 많은 노력을 했다. 하지만 그럼에도 카카오에서 제공하는 문서만을 보고 실제 구현까지의 과정에 다를 때까지는 나의 실력으로 시간과 노력이 너무 많이 들 것이라고 생각했다.

처음 프로젝트를 진행하며 내가 지금까지 강의, 책 등을 통한 클론 코딩으로 공부한 방식에서 문제가 있다는 생각을 하게 되었다. 실제 클라이언트 분이 요구하는 요구 사항을 만족하기 위해서는 클론 코딩을 벗어나서 내가 찾아보고 내가 처음부터 구현해야만 했기 때문이다. 이런 문제점을 보완하기 위해 프로젝트를 시작한 지 얼마 안된 시점에 강의, 책, 구글링을 상당히 배제하고 스스로 공부하여 이번 프로젝트를 진행하려고 했다.

'그러면 위의 동영상을 본건 뭐지?'라고 생각할 수 있다. 내가 왜 동영상을 보고 공부하는 방식, 문제가 있다고 생각한 방식을 다시 시도한 이유는 명확한 이유가 있었다.

✅ 팀 프로젝트는 공부가 아니다!!!!!!!

내가 진행한 프로젝트는 팀원이 공통된 목표를 달성하기 위해 노력하는 프로젝트였다. 하지만 내가 처음 프로젝트를 진행하는 방향성은 나의 발전이 상당히 큰 부분을 차지하고 있었다. 팀원과 프로젝트를 완성하는 것만큼이나 나의 발전도 중요하다고 생각했기에 공부와 카카오 개발 문서만을 보고 구현을 하려고 노력했었다.

그러다가 한 유튜브를 보면서 생각을 바꾸게 되었다. 시간이 좀 지나 전체적인 내용이 희미해졌지만 대략적으로 이제 막 취직을 한 신입 개발자가 자신이 잘 모르는 일을 하면서 겪은 문제와 해결한 방법 그리고 선배 개발자의 피드백이었다.

신입 개발자가 잘 모르는 기능에 대한 구현을 해야하는 상황에 직면했다. 신입 개발자인 만큼 부족한 부분도 많았기에 자신의 모르는 부분을 열심히 공부해서 3~4일에 걸쳐서 이 문제를 해결했다. 그리고 선배 개발자 분에게 자신이 어려움을 겪었지만 시간이 걸리더라도 자기 스스로 공부를 하여 문제를 해결했다고 말했다. 선배 개발자의 피드백이 중요했는데 "팀 프로젝트에서 너가 3~4일에 걸쳐서 해결한 문제는 나에게 질문했을 경우 3~4 시간에 해결될 문제였다. 따라서 너에게 남은 3~4일은 다른 기능을 구현할 수 있었고 프로젝트의 더 많은 기능의 구현을 맡아 진행할 수 있었고 프로젝트가 더 많이 진행됐을 거다." 였다.

물론 실제 기업에서 근무하고 있는 신입 개발자를 위한 피드백이기에 나에게는 조금 다를 수 있었지만 결국 프로젝트의 성격이 중요한 것이었다. 이번 프로젝트는 나의 성장과 발전이 중요한 것이 아닌 UI/UX, 프런트 엔드, 백엔드 팀원들과 함께 공통된 목표를 달성하기 위한 프로젝트였다. 내가 프로젝트를 대하는 태도가 잘못된 것이었다.

기한이 존재하고 모든 팀원이 클라이언트의 요구사항을 만족하는 웹 사이트를 개발하기 개발하는 팀 프로젝트는 내가 공부하는 곳이 아니다!!!

그렇기에 카카오 페이 결제 시스템을 구현하는 동영상을 보고 클론 코딩을 하며 개발을 진행했다. 그리고 이런 클론 코딩을 또 다시 한번 하면서 내 생각도 다시 바뀌게 되었다.

클론 코딩, 강의, 책을 통한 공부

사실 많은 유튜브에서 클론 코딩, 강의, 책을 통해서 하는 공부는 한계가 명확하다 좋지 않다는 내용을 다룬 영상을 많이 봤다. 나 또한 이런 따라하는 공부에 대해 경계를 하고 있지만 혼자 공부하는 상황에서는 클론 코딩을 많이 선택하게 되었다. 그리고 이번 팀 프로젝트를 시작하면서 뭔가 크게 잘못 된 방법이라는 생각을 하게 되었는데 카카오 페이 구현을 클론 코딩하면서 생각이 다시 한번 더 바뀌게 되었다.

내가 너무나도 모르기에 처음부터 공부하여 하나씩 혼자 힘으로 구현하는 것이야 말로 진짜 제대로 된 개발 공부다! 따라서 프로젝트를 시작하며 하나씩 전부 다 공부하려고 했다. 위에서 말했듯 중간에 생각이 변하게 되어 공부하는 과정이 아닌 팀 프로젝트를 위한 개발을 하려고 생각을 바꾸게 되어 다시 한번 더 유튜브 동영상을 보며 클론 코딩을 진행하며 이 생각은 바뀌게 되었다.

'모방은 과연 잘못 된 것일까?' 모방은 창조의 어머니라는 말이 있듯이 클론 코딩이 잘못된 방식이라는 생각을 조금 다시 생각하게 되었다. 내가 전부다 하나씩 알면서 구현할 수 있다면 그것이 맞는 일이겠지만 나는 그런 실력이 되기 어렵다는 것을 잘 알게 되었다. 사실 공부라는 것이 모르는 것을 알아가는 과정이고 이 과정에서 따라하는 방식은 잘못되지 않았다고 다시 한번 느끼게 되었다. 그리고 많은 선배 개발자 분들이 '클론 코딩은 진짜 코딩이 아니다'라는 말을 하신 것이 어떤 의미인지를 명확하게 알게 되었다.

내가 모르는 것을 알기 위해 클론 코딩을 통해 공부를 하는 것은 좋은 공부 방식의 한 방식이라고 생가한다. 하지만 클론 코딩만을 하는 것이 큰 문제였던 것이다. 내가 어떤 문제를 만났고 이를 해결하기 위해 문제점을 파악하고 구현 방식을 결정하고 어떤 API, 라이브러리, 함수 등을 사용할 지 결정하고 구현 과정에서 만나는 에러와 문제점을 해결하는 과정은 굉장히 가변적이다. 따라서 내가 스스로 생각하여 문제를 해결, 구현 방식 결정 ... 등 스스로 생각해야만 하는 것이다. 책과 강의 등은 이런 과정의 한가지 예만을 보여주는 것이었다. 따라서 공부를 진행하며 코드를 바라보는 것이 아닌 문제와 구현을 바라보는 관점을 더 중요시 하면서 공부를 진행해야만 했다.

앞으로도 나는 계속해서 강의와 책을 통해서 공부를 진행할 것이다. 하지만 이제는 책이나 강의에서 나오는 코드가 아닌 필자가 문제를 해결하는 과정, 생각하는 사고 방식에 집중하며 공부를 진행할 것이다.

마무리

단순한 개발이 아닌 UI/UX, 프런트 엔드, 다른 백엔드 팀원과의 소통하며 진행하는 팀 프로젝트는 적응하기 쉽지 않았다. 그렇기에 프로젝트를 진행하면서 계속해서 많은 변화가 있었다. 단순한 나의 공부 방식이나 개발 방법에 관한 생각이 아닌 팀 프로젝트를 대하는 생각과 팀원들과 함께 공통된 목표를 달성하기 위해 노력하는 방식, 태도, 생각 또한 많이 바뀌게 되었다.

profile
코딩 시작

0개의 댓글