포스트

아이디어 실현에 도움이 될 소프트웨어 기술의 학습과 깊이 있는 경험을 기록하는 공간입니다.

  • 스프링 부트 JVM 핫 스와핑 (devtools, JRebel)

    스프링 부트(Spring Boot) 애플리케이션 개발을 진행할 때, 코드를 변경하고 결과 확인을 위해 서버를 재시작하는 번거로움이 있어요. 이런 번거로움을 해소하기 위한 핫 스와핑(hot-swapping) 기능을 할 수 있는 솔루션이 있는데, Devtools와 JRebel이 있어요. Devtools와 JRebel에 대해 알아보고, macOS와 인텔리제이(IntelliJ IDEA) 환경에서 실행하는 방법을 확인해 볼게요. 1. Devtools 많은 분들이 스프링 부트 애플리케이션을 개발할 때 대표적으로 spring-boot-devtools 의존성을 추가하여 사용하고 있어요. devtools를 사용하면 클래스, 리소스 파일을 변경하면 애플리케이션이 자동으로 재시작 돼요. 그리고 HTML, CSS, JavaScript 등 정적 리소스를 수정하고 브라우저를 새로고침 하면 변경 사항을 즉시 반영할 수 있어요. 그뿐만 아니라 properties, yml 파일의 설정 정보를 자동으로 적용해 줘요. devtools를 적용하고, 실행하는 방법을 알아볼게요. 본인이 사용하고 있는 빌드 도구(Gradle, Maven)에 맞춰 의존성을 추가해 주세요. devtools는 의존성만 추가하면 별도의 설정은 필요가 없어요. devtools가 동작하는지 확인을 위해 서버를 실행하고, 클래스 파일을 수정해 주세요. 클래스 파일 변경이 감지되면 자동으로 서버가 재시작되는 걸 확인할 수 있어요. 인텔리제이에서 포커스가 벗어나는 경우에 변경 감지가 이뤄지기 때문에 개인적으로 이 부분이 불편하다고 생각해요. 그래서 수동으로 Command + Shift + F9 단축키로 명확하게 변경된 코드를 적용하여, devtools의 변경 감지가 이뤄져서 서버가 재시작되는 걸 확인할 수 있어요. [1] devtools 실행 결과 코드 변경이 일어날 때마다 자동으로 애플리케이션이 재시작되기 때문에 그 시간 동안 딜레이가 발생해요. 하지만 서버를 아예 멈췄다가 실행하는 콜드 스타트에 비해 빠르기 때문에 보다 쾌적한 개발을 진행할 수 있어요. devtools 사용할 때 한 가지 주의할 점은 개발할 때만 사용하고, 운영 환경에서는 비활성화 해줘야 해요. 운영 환경에서 사용하게 되면 불필요한 자원이 낭비되고, 성능 저하 등 발생할 수 있어요. 그렇기 때문에 운영 환경에서는 spring-boot-devtools 의존성을 제거하거나 properties 또는 yml 파일에서 spring.devtools.restart.enabled = false로 설정해 주세요. 2. JRebel JRebel도 devtools와 마찬가지로 클래스, 리소스 파일, 정적 파일 그리고 설정 파일에 대한 변경을 실시간으로 적용해 줘요. devtools와 가장 큰 차이점으로는 애플리케이션을 재시작하지 않고, 즉시 반영해 준다는 점이에요. devtools는 스프링 부트에서 사용되는 오픈 소스로 의존성만 추가하면 무료로 사용할 수 있지만, JRebel은 상용 소프트웨어로 플러그인을 설치하고 라이선스를 등록해야 사용할 수 있어요. JRebel을 설정하고, 실행하는 방법을 알아볼게요. 라이선스를 받기 위해 JRebel 사이트에 접속 후 정보를 입력해 주세요. [2-1] JRebel 사이트에서 정보 등록 입력한 이메일로 라이선스 정보를 받으면 라이선스 키를 복사해 주세요. [2-2] 메일로 라이선스 키 수신 인텔리제이를 실행하고, Settings > Plugins > Marketplace에서 JRebel and XRebel 플러그인을 설치해 주세요. [2-3] JRebel 플러그인 설치 Help > JRebel > Activation을 클릭 후, 메일로 전달받은 라이선스 키를 입력하고 활성화해 주세요. [2-4] JRebel 라이선스 활성화 우측 상단에 JRebel 실행 버튼을 클릭하여 서버를 시작해 주세요. [2-5] JRebel 서버 실행 JRebel이 동작하는지 확인을 위해 클래스 파일을 수정하고, Command + Shift + F9 단축키로 Recompile 해주세요. (devtools와 동일하게 변경 감지가 이뤄지면 리컴파일 단축키를 누르지 않더라도 자동으로 실행돼요) 그러면 애플리케이션 재시작 없이 변경된 파일이 적용되는 걸 확인할 수 있어요. [2-6] 변경된 클래스 파일 리로딩 JRebel에서 제공받은 평가판 라이선스는 14일 동안 유효하게 사용할 수 있어요. 제한 없이 라이선스를 사용하기 위해 유료 라이선스 가격을 문의해 봤더니, 연간 $655 라는 작지 않은 금액을 답변 받았어요. [2-7] 유료 라이선스 가격 개인이 구매하기에는 부담되는 금액으로 판단되고, 대신에 약간의 불편함을 감수하고 평가판 라이선스를 지속해서 사용할 수 있는 방법이 있어요. 홈 디렉터리로 이동 후 .jrebel 디렉터리를 제거해 주세요. (파인더(Finder)에서 직접 제거해도 돼요) 그리고 인텔리제이를 재실행 후 [2-4] 이미지 과정을 다시 진행해 주세요. 그러면 평가판 라이선스 기간이 14일로 초기화되어 사용할 수 있어요. [2-8] .jrebel 디렉터리 삭제 기간이 만료될 때마다 해당 방법을 반복해야 하는 수고로움이 있지만, 그래도 JRebel 유료 라이선스 구매 없이 평가판 라이선스를 계속 사용할 수 있어요. 개인적으로 devtools 보다 JRebel을 추천해요. 이유는 devtools는 변경 시점마다 애플리케이션이 자동으로 재시작되지만, JRebel은 재시작 없이 변경된 파일을 바로 적용 해줘요. 그렇기 때문에 속도도 빠르고, JVM 핫 스와핑도 안정적으로 동작해요. [3] 스프링 부트 핫 스와핑에 대한 공식 문서 또한 서버가 내려가지 않기 때문에 Memory DB를 사용하고 있다면 테스트 중이던 데이터가 유지돼요. devtools는 서버가 재시작되면서 메모리가 날아가기 때문에 테스트 중이던 데이터를 다시 등록해야 한다는 불편한 점도 있거든요. 그리고 프로젝트에 의존성을 추가하지 않고, 인텔리제이 플러그인으로 설정하여 동작하기 때문에 devtools 처럼 의존성을 신경 쓰지 않아도 돼요. JRebel로 디버깅을 켠 상태로 개발을 진행하면 쾌적한 개발 환경을 경험할 수 있어요! JRebel 플러그인 설치 및 라이선스 활성화 "Settings > Keymap"에서 Rebel Debug 단축키를 Control + D로 설정 Control + D 단축키로 Debug with Rebel로 서버 실행 Command + Shift + F9 단축키로 Recompile 하여 변경된 코드 반영 이상으로 JVM 핫 스와핑 기능을 위한 Devtools와 JRebel에 대해 알아봤어요.

  • 스프링 프로젝트에 애플 인 앱 결제(IAP, In-App Purchase) 서버 개발

    애플(Apple)의 인 앱 결제(In-App Purchase)를 통해 구입한 상품 데이터를 스프링(Spring) 프로젝트로 구현한 서버(Server)에서 검증을 하고 데이터베이스에 저장하여 구입이 완료된 상품을 관리할 수 있도록 프로세스를 확인해보도록 하겠습니다. 상품 구매에 대한 인 앱 결제 프로세스를 확인해보도록 하겠습니다. App은 Server에서 상품 목록 API를 호출합니다. Server는 관리자가 실제로 App Store Connect에서 등록한 상품을 DB에서 관리하며, 목록을 App에게 제공합니다. App은 선택한 상품의 구매 가능 여부를 Server에 요청합니다. Server는 유저가 선택한 상품이 구매한 적이 있는 상품인지 확인하여 App에게 제공합니다. (상품 구매가 가능한 경우, Server에서 고유한 ID를 생성해서 [3 ~ 5-2] 과정을 Server 입장에서 하나의 트랜잭션으로 관리하여 데이터 안정성을 관리할 수 있지 않을까..? 고유한 ID가 생성되면 DB에 저장 - 서버 입장에서의 구매 트랜잭션 시작) App은 구매 가능한 상품인 경우에 App Store에 결제를 진행합니다. 결제가 완료되면 App은 receipt-data를 생성합니다. (트랜잭션 시작) App은 생성한 receipt-data로 Server에 상품 결제 검증 요청 API를 호출합니다. 4-1. Server는 App에서 전달받은 receipt-data로 App Store에게 영수증 검증(/verifyReceipt) API를 호출합니다. 4-2. Server는 App Store에게 JSON 형태의 영수증 데이터를 응답받습니다. 4-3. Server는 응답받은 영수증 데이터에서 필요한 값을 파싱 하여 DB에 저장하도록 합니다. (유저가 구입한 상품을 DB에 저장) 4-4. 유저가 구입한 상품을 DB에 저장 완료했다면 Server는 성공 응답 값을 App에게 전달합니다. App은 Server에게 성공 응답 값을 받으면 트랜잭션을 종료시키고 Server에게 구매 완료 API를 호출합니다. 5-1. Server는 App에게 구매 완료 요청을 받으면 [4-3]과정에서 DB에 저장한 영수증 데이터에 완료에 대한 업데이트를 합니다. ([2-2] 과정에서 Server가 생성한 고유한 ID로 구매 완료된 상품에 대한 데이터를 찾아서 업데이트 - 서버 입장에서의 구매 트랜잭션 종료) 5-2. App은 인 앱 결제에 대한 프로세스가 최종적으로 종료됩니다. 서버(Server)는 인 앱 결제 프로세스에서 "상품 목록, 상품 구매 가능 여부, 상품 결제 검증, 상품 구매 완료" 4개의 API를 App에게 제공해주면 됩니다. "상품 목록, 상품 구매 가능 여부, 상품 구매 완료" API는 해당 서비스 정책에 따라서 다르기 때문에 플로우(Flow)에서만 설명하도록 하고, 해당 글에서는 [Step 03 ~ Step 04] 과정에 대한 코드를 확인하고 발생할 수 있는 이슈 사항에 대해 체크해보도록 하겠습니다. 인 앱 결제에 대한 서버 코드에 대해 확인하기 전, 애플에서 제공하는 상품 종류에 따른 몇 가지 특징에 대해서는 확인해보도록 하겠습니다. 애플에서 제공하는 인 앱 결제에 대한 상품 종류는 총 4가지입니다. Ref. In-app purchase types | 상품명 | 설명 | 특징 | | --- | --- | --- | | 소모품(Consumable) | 앱 내에서 사용하면 소모되는 일회성 상품입니다. | 정상적으로 구매가 완료(App에서 트랜잭션을 종료) 되었다면 "/verifyReceipt"를 호출하였을 때 소모품에 대한 응답 값은 확인할 수 없습니다. | | 비소모품(Non-Consumable) | 한 번 구매하면 기간 제한없이 계속 사용할 수 있는 상품입니다. | \- "/verifyReceipt"를 호출하면 비소모품에 대한 데이터를 포함한 응답 값을 반환합니다. \- 복원(Restore)되면 transaction\_id는 변경됩니다. | | 자동 갱신 구독(Auto-Renewable Subscription) | 구매 후 고객이 취소하기 전까지 자동으로 결제가 이루어지는 상품입니다. | \- "/verifyReceipt"를 호출할 때 "receipt-data, password" 2개의 값이 필요합니다. \- "/verifyReceipt"를 호출하면 자동 갱신 구독에 대한 데이터를 포함한 응답 값을 반환합니다. \- 갱신되면 transaction\_id는 변경되며 상품에 대한 receipt 데이터는 응답 값에 추가됩니다. | | 비자동 갱신 구독(Non-Renewable Subscription) | 구매 후 특정 기간동안 사용할 수 있으며, 자동으로 결제가 이루어지지 않습니다. 고객이 재구매를 통해 기간을 연장할 수 있는 상품입니다. | \- "/verifyReceipt"를 호출하면 비자동 갱신 구독에 대한 데이터를 포함한 응답 값을 반환합니다. \- 갱신되면 transaction\_id는 변경되며 상품에 대한 receipt 데이터는 응답 값에 추가됩니다. | 각 상품 특징에 대해 확인했습니다. 그렇다면 상품에 대한 receipt-data는 어떻게 얻을까요? App 개발자와 협업을 통해 테스트 앱 혹은 데이터를 얻을 수 있다면 큰 문제는 되지 않습니다. 하지만 상황이 여의치 않다면 직접 테스트 앱을 만들어서 필요한 데이터를 얻는 수밖에.. 저는 여의치 않은 상황이었고, 앱 쪽에서 상품 구매와 완료 처리되는 게 궁금했기 때문에 테스트 앱을 실행 후 데이터를 얻었습니다. Developer와 App Store Connect 설정과 테스트 앱 실행까지 설명하면 좋겠지만, 인 앱 결제 처리에 대한 서버 프로세스에 집중하도록 하겠습니다. 테스트 앱에서 제가 얻은 각 상품에 대한 receipt-data는 아래와 같습니다. 서버 프로세스를 간략하게 이해하고, 애플에서 제공하는 상품 4가지에 대해서 알아봤습니다. receipt-data까지 준비가 완료되었다면 스프링 프로젝트의 로직을 확인하도록 하겠습니다. (receipt-data 준비가 어렵다면 위에 제가 제공해드린 receipt-data를 사용해도 상관없습니다.) 글에서 설명드리는 코드가 필요하신 분은 GitHub을 통해서 다운로드하시면 됩니다. application.properties는 따로 설정할 필요가 없을 거 같습니다. "APPLE.PASSWORD"는 자동 갱신 구독 상품에 대한 서비스를 제공하고 있다면 App Store Connect에서 생성한 공유 암호를 넣어주시면 됩니다. 하지만 이 글에서는 소모품 상품에 대해서만 테스트해 보도록 하겠습니다. (소모품 상품에 대해서만 테스트를 진행한 이유는 아래에서 설명드리도록 하겠습니다.) 소모품 외에도 "비소모품, 비자동 갱신 구독" 상품은 따로 password가 필요하지 않기 때문에 문제없이 진행할 수 있습니다. (자동 갱신 구독 상품에 대한 구매 이력이 포함되어 있는 receipt-data는 password가 필요합니다.) 1. App에서 "receipt-data" 요청받기 [그림 1]에서 "4. 상품 결제 검증 요청" 프로세스 부분입니다. 유저가 구매한 상품을 서버에서 관리하기 위해서는 App에게 receipt-data 외에도 유저(구매자)에 대한 데이터도 받아야 합니다. 실제 서비스를 구현할 때는 UserReceipt.java 파일에서 유저 관리에 필요한 Fields를 적절하게 정의해주도록 하세요. 테스트에서는 receipt-data만 받아서 진행하도록 하겠습니다. 2. App Store에게 receipt-data 검증 요청 (/verfityReceipt) [그림 1]에서 "4-1. 영수증 검증 요청" 프로세스 부분입니다. receipt-data 데이터와 함께 "POST https://buy.itunes.apple.com/verifyReceipt" 를 호출합니다. 호출 후 전달받은 JSON 응답 값에서 status 코드 값이 21007인 경우에는 "POST https://sandbox.itunes.apple.com/verifyReceipt" 를 다시 호출하도록 합니다. (password는 자동 구독 갱신 상품에 대한 receipt-data 혹은 이력이 포함된 receipt-data를 검증하는 경우에 필요합니다.) 인 앱 결제 테스트를 진행하는 경우에는 Sandbox Test URL을 호출하여야 합니다. 애플 공식 문서에 따르면 Production URL을 먼저 호출하고 21007 코드를 받으면 Sandbox URL을 호출하여 계속 진행하라고 합니다. 3. 영수증 검증 후 응답받은 데이터를 DB에 저장 [그림 1]에서 "4-2. 영수증 검증 응답" 및 "4-3. 영수증 응답 데이터 저장" 프로세스 부분입니다. "/verifyReceipt"을 호출 후 응답받은 JSON 형태의 영수증(receipt) 데이터에서 필요한 값을 파싱 하고 데이터베이스에 저장하여 유저가 구매한 상품 이력을 관리해주어야 합니다. 저는 소모품 상품에 대한 영수증 데이터를 파싱 하여 데이터베이스에 저장하는 대신 logger로 찍었습니다. 실제 서비스에서 상품에 대한 정책과 DB에서 관리할 영수증 데이터가 정해지지 않았기 때문에 각 상품에 대한 상황을 체크하여 프로세스를 확인하는 게 목적이기 때문에 소모품 상품에 대한 영수증을 기반으로 로직을 작성하였습니다. 응답받은 영수증 데이터에서 소모품의 경우에는 in_app에 1건의 데이터만 존재합니다. ("비소모품, 자동 갱신 구독, 비자동 갱신 구독" 상품의 결제를 진행했다면 소모품 상품 외 이력이 in_app에 존재합니다. 또한 트랜잭션이 종료되지 않은 상품의 경우에도 이력이 존재합니다.) Apple에게 "/verifyReceipt" API 요청 후 응답받은 상품 별 영수증 데이터(JSON)는 아래와 같습니다. JSON의 Properties 설명은 공식 문서를 참고해주세요. 이제부터는 해당 로직에서 발생할 수 있는 이슈를 확인해 보도록 하겠습니다. 구매한 상품의 트랜잭션이 정상적으로 종료되지 않는 문제에 대해 아래와 같은 케이스를 생각해볼 수 있습니다. App에서 발생한 문제 1-1. App에서 receipt-data를 Server에 전달하기 전 비정상적으로 종료된 경우 1-2. App에서 receipt-data를 Server에 전달한 후 비정상적으로 종료된 경우 1-3. App에서 구매 완료(트랜잭션 종료) 후 Server에게 구매 완료 요청 API를 호출하기 전 종료된 경우 Server에서 발생한 문제 2-1. 데이터베이스에 영수증 데이터를 저장 후 Server 문제 발생으로 App에게 응답 값을 전달하지 못한 경우 2-2. 영수증 검증 오류와 같은 이슈로 데이터베이스에 저장하지 못하고 App에게 실패(fail)의 응답 값을 전달한 경우 위 5가지의 문제에 대해 제가 생각해본 해결 포인트는 아래와 같습니다. 1-1. App이 재구동될 때 종료되지 않은 트랜잭션의 receipt-data를 재생성하여 Server에 재요청하기 때문에 Server 쪽에서는 신경 쓸 부분이 없다. ‣ App에서 처리해야하는 이슈 1-2. App이 재구동 되면서 종료되지 않은 트랜잭션의 receipt-data를 재생성하여 Server에 재요청 한다. 이 경우에 Server는 기존 로직을 동일하게 진행하나 데이터베이스에 중복으로 유저의 상품 정보가 존재하는지 확인해야 한다. ‣ App & Server에서 처리해야하는 이슈 1-3. App에서 유저의 구매 이력과 DB에 저장된 유저의 구매 이력을 비교하여 DB를 현행화 해주는 Server 로직이 필요하다. 이 경우에는 어떻게 처리해야 할까.. 댓글로 의견 부탁드리겠습니다. ‣ 고객이 결제를 했으나, 상품 구매가 정상적으로 이루어지지 않았기 때문에 고객센터를 통해 문의를 한다. (?) 2-1. App은 일정시간 동안 응답 값을 받지 못하면 Server에 재요청한다. 일정시간 혹은 재요청 후에도 응답 값을 받지 못하면 App은 결제 실패에 대한 트랜잭션을 처리할 로직을 구현해야 한다. (서버에 문제가 발생한 경우에는 서비스 전체 장애이기 때문에 절대 발생하는 일이 없기를..) ‣ App & Server에서 처리해야하는 이슈 2-2. App은 실패에 대한 응답 값을 전달받았기 때문에 Server에 재요청 한다. 이 경우에는 Apple 쪽 문제 혹은 Server에서 데이터 처리 과정에서 발생한 문제이기 때문에 관련 로그를 적절하게 남기고 빠른 시간 내에 장애를 처리해야 한다. (마찬가지로 발생해서는 안되는 문제이지만 만약 발생한 경우 App 쪽에서 결제 실패에 대한 트랜잭션을 처리할 로직을 구현해야한다.) ‣ App & Server에서 처리해야하는 이슈 로직에서 고려해야 할 사항은 (1) "비소모품, 자동 갱신 구독, 비자동 갱신 구독" 상품의 구매 이력이 존재하는 영수증 데이터에 대한 처리와 (2) 1-2 이슈 상황에 대한 부분일 거라고 생각합니다. 아래에서 두 가지 상황에 대해서 얘기를 해보겠습니다. 1. 구매 이력이 존재하는 영수증 in_app 프로퍼티에 상품의 구매 이력이 존재하는 영수증 데이터를 처리하는 로직은 현재 코드에서 구현하지 않았습니다. 이유는 서비스 정책이 정해지지 않은 상황이지만, 앱 내에서 사용하는 "기간제 & 무기한"의 상품을 다룰 예정이고 유저의 구매 상품과 기간을 서버에서 관리할 것이기 때문에 transaction_id가 일관성 있게 유지되는 소모품(Consumable) 옵션이 적절하다고 판단하였기 때문입니다. "비소모품, 자동 갱신 구독, 비자동 갱신 구독" 상품의 경우에는 App에서 복원(Restore) 기능을 구현해야 합니다. 복원 기능을 구현하지 않는다면 App Store에서 거절(reject) 사유에 해당되기 때문입니다. 또한 복원을 하는 경우 transaction_id가 변경되어 구매 이력이 추가적으로 생성되기 때문에 앱 내에서 사용하는 상품을 관리하기에는 적절하지 않다고 판단하였습니다. 해당 부분에서 본인의 서비스 정책에 따른 상품의 영수증(receipt)에서 필요한 데이터를 파싱 하는 로직을 구현하면 됩니다. (추후에 정책이 정해지면 적절한 프로세스를 구현해보고 좀 더 자세하게 설명을 추가해보도록 하겠습니다.) 2. App에서 receipt-data를 Server에 전달한 후 비정상적으로 종료된 경우 App에서는 receipt-data를 Server에 전달하였고 Server에서는 영수증 검증까지 완료하고 유저의 구매 상품의 정보를 데이터베이스에 정상적으로 저장까지 완료하고 성공(Success)에 대한 응답 값을 App에게 전달하였으나, App이 비정상적으로 종료되어서 Server에서 보낸 성공에 대한 응답 값을 못 받았기 때문에 구매 트랜잭션을 종료하지 못한 상황입니다. 이 경우에는 App이 재 구동된 시점에 종료되지 않은 트랜잭션에 대한 receipt-data를 재생성하여 Server에 다시 요청합니다. 하지만 유저가 구매한 상품에 대한 정보가 데이터베이스에 저장된 상황이기 때문에 Server는 transaction_id를 이용하여 데이터베이스에 저장된 데이터가 있는지 확인하고 데이터가 존재한다면 App에게 성공(Success)에 대한 응답 값을 전달해주면 됩니다. 데이터베이스에 저장하는 로직은 현재 구현하지 않았고 logger로 필요한 데이터를 출력하였습니다. 이미 존재하는 데이터에 대한 중복 저장에 대해서는 INSERT IGNORE를 이용하여 처리하면 어떨까 하는 의견입니다. 마찬가지로 데이터베이스에서 관리할 상품 정보가 정해지지 않아서 몇 가지의 데이터만 뽑아서 확인했습니다. 애플 공식 문서에서는 transaction_id, original_transaction_id, product_id를 기준으로 데이터베이스에서 상품을 관리하길 권장합니다. 저는 데이터베이스에서 "기간제 & 무기한" 상품을 관리할 예정이기 때문에 "transaction_id, original_transaction_id, product_id" 외에도 상품을 구매한 유저의 정보와 구매한 상품에 대한 기간으로 계산한 만료 기간 등 추가로 넣어줄 예정입니다. 4. 데이터베이스에 유저가 구매한 상품 정보를 정상적으로 저장하였다면 App에게 성공(Success)에 대한 응답 값 반환 [그림 1]에서 "4-4. 상품 결제 검증 응답" 프로세스 부분입니다. 이상으로 애플(Apple) 인 앱 결제(In-App Purchase)에 대한 프로세스를 알아봤습니다. 가장 중요한 것은 서비스에 대한 정책을 어떻게 정하느냐에 따라 로직이 달라질 것입니다. 인 앱 결제에서 관리할 데이터와 프로세스가 정해진다면 프로토타입으로 간단하게 작성한 로직을 좀 더 구체화시킬 수 있을 거 같습니다. 해당 글 내용은 개인적인 판단으로 임의의 서비스 정책을 정한 후 작성한 글입니다. 또한 코드를 작성하는 실력이 아직 많이 부족하기 때문에 부족함이 많은 프로토타입의 코드입니다. 애플의 인 앱 결제(In-App Purchase) 프로세스를 이해하는 부분에서 도움이 되었으면 합니다. 인 앱 결제에서 사용되는 4가지 종류의 상품과 다양한 방식의 예외 사항을 테스트해봤습니다. 프로세스 흐름에 따라 글을 작성하다 보니 제가 경험한 전부를 내용에 넣지는 못했습니다.

  • 스프링 프로젝트에 애플 로그인 API 연동하기

    이전 글에서 "Sign in with Apple" 연동을 위한 Apple Developer 3가지 설정을 진행하였습니다. 설정을 통해서 필요한 데이터 준비가 끝났으므로 프로젝트에 설정하여 확인해보도록 하겠습니다. 먼저, 깃허브에서 샘플 코드를 다운로드 하도록 합니다. macOS Catalina 버전 10.15.6 운영체제에서 IntelliJ IDEA Ultimate 환경에서 Spring Boot 프로젝트로 설명하도록 하겠습니다. JWT 관련 라이브러리는 "nimbus-jose-jwt(v3.10)"를 이용했습니다. 해당 버전의 라이브러리를 이용한 이유는 애플 로그인 API를 적용시킬 프로젝트에서 사용하고 있기 때문에 그대로 유지하여 사용하기로 결정하였기 때문입니다. 프로젝트 다운로드하여 실행하였다면 application.properties 파일로 이동해주세요. 이전 글에서 설정 후 얻은 값을 아래와 동일한 위치에 입력해주면 됩니다. 프로젝트를 실행 후 "localhost:8080/"으로 접속하면 Sign in with Apple 로그인 화면이 정상적으로 실행됩니다. 그리고 앞서 application.properties에 설정이 정상적이라면 로그인을 진행하고 값을 반환받을 수 있습니다. 프로젝트 실행은 설정 값만 적용한다면 쉽게 진행할 수 있습니다. 이어서 애플 로그인 프로세스와 코드에 대해서 설명드리도록 하겠습니다. [1] 애플 로그인 버튼 페이지 Ref. configuring_your_webpage_for_sign_in_with_apple 위 코드는 "localhost:8080/"으로 접속하면 Sign in with Apple 애플 로그인 버튼이 보이는 화면입니다. 유저가 버튼을 클릭하면 로그인이 진행되는데 이때 메타정보와 유저 아이디, 비밀번호가 애플에게 요청됩니다. | 필드 | 설명 | | --- | --- | | ID | 유저 아이디 | | Password | 유저 비밀번호 | | appleid-signin-client-id | Services ID - Identifier 값 | | appleid-signin-scope | 애플에게 전달받을 유저 정보 - name email | | appleid-signin-redirect-uri | Services ID - Return URLs 값 | | appleid-signin-state | 상태 값 | | appleid-signin-nonce | 임시 값 | [2] 유저 로그인 후 정보 받기 Ref. sign_in_with_apple_rest_api/authenticating_users_with_sign_in_with_apple 정의된 7개의 데이터와 함께 "https://appleid.apple.com/auth/authorize" 호출되고, 애플은 Services ID에 정의된 Return URLs로 JSON 데이터를 반환합니다. 반환받은 JSON 데이터는 "state, code, id_token, user" 4개의 키로 이루어져 있습니다. 여기서 알고 있어야 할 부분은 user 키는 유저가 서비스 최초 가입할 때만 받을 수 있습니다. 또한, 유저는 자신의 email을 공유할 수도 있고, 하지 않을 수도 있습니다. (JSON 데이터는 유저가 email을 공유하지 않은 데이터이며, "code" 키의 값은 5분 동안 유효합니다.) [3] id_token 5가지 유효성 검증 Ref. sign_in_with_apple_rest_api/verifying_a_user Ref. sign_in_with_apple/fetch_apple_s_public_key_for_verifying_token_signature 애플에게 로그인 유저에 대한 정보를 JSON 데이터로 받은 후 "id_token" 값을 decode 하여 "RSA, exp, nonce, iss, aud" 5가지의 검증 절차를 진행합니다. "exp, nonce, iss, aud"의 값은 "id_token" 값을 decode 하면 PAYLOAD 영역에 존재합니다. jwt.io에서 전달받은 JSON을 decode 할 수 있습니다. RSA 검증은 "GET https://appleid.apple.com/auth/keys" 를 호출하여 공개키 리스트를 받은 후 "id_token" 값의 HEADER 영역의 kid와 동일한 공개키 데이터로 서명 확인을 진행합니다. | 키 | 값 | | --- | --- | | exp | id\_token 만료 시간 (10분) | | iss | https://appleid.apple.com | | aud | Services ID - Identifier 값 | | nonce | 생성된 임의 값 | | RSA | Apple에서 제공받은 Public Key | [4] client_secret 생성 Ref. sign_in_with_apple/generate_and_validate_tokens [3]에서 5가지의 검증 절차가 정상적으로 완료되었다면 client_secret을 생성해주도록 합니다. client_secret은 JWT로 생성되며 필요한 값은 아래와 같습니다. | 키 | 값 | | --- | --- | | kid | 애플에서 생성한 Private Key에 대한 Key ID | | alg | ES256 | | iss | App ID 생성에 사용된 Team ID | | iat | 생성 시간 | | exp | 만료 시간 | | aud | https://appleid.apple.com | | sub | Services ID - Identifier 값 | 위의 데이터로 client_secret의 JWT가 생성되었다면, 마지막으로 애플에서 다운로드한 Key 파일 안에 들어있는 Private Key로 서명을 해주면 client_secret이 정상적으로 생성 완료됩니다. [5] 토큰 검증 및 발급 Ref. sign_in_with_apple/generate_and_validate_tokens Ref. sign_in_with_apple/tokenresponse [2]에서 전달받은 code와 [4]에서 생성한 client_secret의 값 그리고 "client_id, grant_type, redirect_uri" 값으로 "POST https://appleid.apple.com/auth/token" 을 호출하여 권한 부여를 위한 토큰 검증을 진행하도록 합니다. ("code"는 5분간 유효한 값이므로 주의하도록 한다.) | 키 | 값 | | --- | --- | | client\_id | Services ID - Identifier 값 | | client\_secret | eyJraWQiOiJWTTJOOFMzN1RSIiwiYWxnIjoiRVMyNT ... 생략 | | code | c3944a20072b7446b97633646556204f8.0.rruy.Gjgud84EqqpCvP31MrudDw | | grant\_type | authorization\_code | | redirect\_uri | Services ID - Return URLs 값 | "POST https://appleid.apple.com/auth/token" 호출이 정상적으로 완료되면 JSON 데이터를 반환받습니다. 반환받은 JSON 데이터에서 "id_token"을 decode 하여 필요한 유저 정보를 얻을 수 있습니다. [6] refresh_token 검증 및 토근 재발급 Ref. sign_in_with_apple/generate_and_validate_tokens Ref. sign_in_with_apple/tokenresponse [5]에서 전달받은 "refresh_token"에 대한 유효성 검증을 하고 싶다면 "client_id, client_secret, grant_type, refresh_token"의 값으로 "POST https://appleid.apple.com/auth/token" 호출하여 검증을 진행합니다. | 키 | 값 | | --- | --- | | client\_id | Services ID - Identifier 값 | | client\_secret | eyJraWQiOiJWTTJOOFMzN1RSIiwiYWxnIjoiRVMyNTYifQ ... 생략 | | grant\_type | refresh\_token | | refresh\_token | r8e88bc9f62bc496398b71117610c5aeb.0.mruy.UuuL5tpwnWaof86XPErqJg | "refresh_token"에 대한 "POST https://appleid.apple.com/auth/token" 호출이 정상적으로 완료되면 JSON 데이터를 반환받습니다. 반환받은 JSON 데이터에서 "id_token"을 decode 하여 필요한 유저 정보를 얻을 수 있습니다. 대략적인 애플 로그인(Sign in with Apple) 연동에 대해서 설명했습니다. 코드와 같이 풀어서 쉽게 설명하려고 했으나 생각보다 더 복잡하고 헷갈릴 수 있을 거 같습니다. Apple Developer Documentation을 참고하며 코드를 보면서 이해하신다면 포스트보다 이해하기가 좀 더 수월하실 거라 생각합니다. Sign in with Apple 흐름은 아래와 같습니다. "Sign in with Apple" 버튼이 있는 애플 로그인 페이지 "https://appleid.apple.com/auth/authorize" 호출 - (유저 로그인) "https://appleid.apple.com/auth/keys" 공개 키 호출 및 검증 - (rsa, exp, nonce, iss, aud) "client_secret" 생성 - (jwt + private key) "https://appleid.apple.com/auth/token" 호출 - (authorization_code) "https://appleid.apple.com/auth/token" 호출 - (refresh_token) 내용 추가 1. 애플 로그인 페이지 추가적으로 앱(App)에 Sign in with Apple 버튼이 존재하는 페이지가 아닌 애플 로그인 페이지 화면을 제공해야 하는 경우에는 "https://appleid.apple.com/auth/authorize"를 redirect 해주면 ID와 Password를 입력하는 화면으로 바로 이동됩니다. 내용 추가 2. 이메일 변경, 서비스 해지, 애플 계정 탈퇴 이벤트가 발생한 경우 유저의 애플 계정에 대한 이벤트가 발생하면 body 안에 payload 키로 jwt 형태의 데이터가 담겨서 "App ID에 등록된 Endpoint URL"로 전송됩니다. payload의 값은 jwt이므로 decode 하면 HEADER와 PAYLOAD 데이터 영역으로 나뉩니다. 유저가 서비스 해지를 한 경우, 전달된 payload의 값을 decode 한 결과입니다.

  • 스프링 프로젝트에 애플 로그인 API 연동을 위한 Apple Developer 설정

    Spring API Server에서 Apple Login API를 연동하여 앱(App)에 제공하기 위한 개발을 해봤습니다. 애플에게 사용자 인증 토큰을 발급받는 로직뿐 아니라 Android와 ios 12 이하 버전에서는 로그인 페이지를 앱에게 제공해주어야 합니다. (ios 13 버전 이상에서는 SDK가 제공되는 걸로 알고 있습니다.) 좋은 코드를 작성할 실력은 부족하지만 Apple Developer Documentation을 보면서 프로세스를 이해하고 글을 작성해볼 기회를 가졌다는 것에 의미를 두도록 하겠습니다. 애플 로그인 API(Sign In with Apple) 연동을 하기 위해서는 Apple Developer 사이트에서 필요한 설정을 먼저 해줘야 합니다. (참고로 Apple Developer에서 필요한 API 서비스를 이용하기 위해서는 매년 결제를 진행해야 이용할 수 있습니다.) Account - Apple Developer 사이트로 이동하여 3가지 설정을 진행하도록 하겠습니다. [1] App ID 등록하기 Account - Apple Developer 사이트로 이동 후 "Certificates, Identifiers & Profiles"를 클릭해주세요. "Identifiers" 메뉴로 이동 후 "+" 버튼을 클릭해주세요. "App IDs"를 선택 후 진행해주세요. "App" 타입을 선택 후 진행해주세요. "Description"에 해당 App ID에 대한 간략한 설명을 적고, "Bundle ID"는 네이밍 예시처럼 입력해주세요. (ex. Bundle ID : com.domainname.appname) 아래로 스크롤하여 "Sign In with Apple"를 선택 후 "Edit"을 클릭해주세요. "Enable as a primary App ID"를 선택하고, "Endpoint" URL를 작성해주세요. Endpoint URL는 도메인 형식이며 기본 443 포트로 SSL 적용이 되어 있어야 합니다. 애플에서 유저가 "이메일 변경, 앱 서비스 해지, 애플 계정 탈퇴"를 했을 경우, 입력한 Endpoint URL로 유저 정보와 이벤트에 대한 PAYLOAD 데이터를 전송합니다. 만약, 상황이 여의치 않아서 URI를 갖추지 못하였다면 형식을 맞춰서 임의로 작성해주도록 합니다. 유저의 "이메일 변경, 앱 서비스 해지, 애플 계정 탈퇴"에 대해서는 테스트하지 못하겠지만 애플 로그인에 연동은 문제없이 진행할 수 있습니다. App ID가 등록 완료되었습니다. Team ID는 보안상의 이유로 이미지에서 지웠지만, 자신의 "Team ID"를 알고 있어 주세요. Team ID는 클라이언트 시크릿(Client secret)을 생성할 때 필요한 정보입니다. [2] Services ID 등록하기 이번에는 애플 로그인(Sign In with Apple)을 진행한 유저의 정보를 전달받기 위한 Services ID를 등록하겠습니다. [그림 1.2]와 동일하게 "Identifiers" 메뉴에서 "+" 버튼을 클릭 후 "Services IDs"를 선택하여 진행해주세요. "Description"에 Services ID에 대한 간략한 설명을 입력하고 "Identifier"에 Services ID를 식별할 수 있는 도메인을 작성해주세요. (ex. Identifier : com.domainname.appname) Services ID가 등록되었다면 Identifiers 메뉴 리스트에서 등록한 Services ID를 클릭하여 설정 페이지로 이동해주세요. Services ID 설정 페이지에서 "Sign In with Apple"를 선택 후 "Configure" 버튼을 클릭해주세요. 앞서 등록을 진행했던 App ID를 선택 후, "Domain"과 "Return URLs"를 입력해주세요. 도메인이 준비가 안되었다면 임의의 도메인을 입력해주셔도 테스트에는 문제가 없습니다. 하지만, Return URLs는 현재 자신의 IP/Port, URL를 정확하게 입력해줘야 합니다. 등록한 Return URLs로 애플 로그인을 진행한 유저의 정보가 전달되기 때문입니다. (Domain과 Return URLs은 여러 개 등록할 수 있습니다.) Services ID 등록이 완료되었습니다. 자신의 Services ID의 "Identifier"를 기억하고 있어야 합니다. Services ID의 식별자(Identifier)는 client_id, aud의 값으로 사용됩니다. [3] Private Key 생성하기 클라이언트 시크릿(client_secret)을 생성할 때 사용할 비밀 키(private key)를 생성하도록 하겠습니다. "Keys" 메뉴에서 "+" 버튼을 클릭해주세요. Key Name을 입력 후, "Sign in with Apple"를 선택 후 "Configure" 버튼을 클릭해주세요. 앞서 생성했던 App ID를 선택해주세요. Private Key 생성을 위한 "Key" 등록은 비교적 쉽게 끝냈습니다. 자신의 "Key ID"를 기억해주세요. 그리고 "Download" 버튼을 클릭하여 Private Key 정보가 들어있는 "AuthKey_[KeyID].p8" 파일을 다운로드하여주세요. 주의할 점은 다운로드는 최초 1회만 가능하고 이후에는 다시 다운로드하지 못하니 파일을 잘 보관해주도록 합니다. (혹시, 파일을 잃어버렸다면 [그림 3.1]부터 다시 진행해주세요.) Key ID와 파일은 클라이언트 시크릿(client secret)을 생성할 때 필요한 데이터입니다. 이상으로 애플 로그인 API 연동을 위한 Apple Developer의 3가지 설정을 완료했습니다. 설정에 대해서 다시 한번 정리해보도록 하겠습니다. [1] App ID - 자신의 App 서비스를 생성하고, 서비스에 애플 로그인으로 가입한 유저에 대한 이벤트를 전달받기 위한 설정입니다. 클라이언트 시크릿을 생성할 때 사용될 Team ID를 기억해주세요. (유저에 대한 이벤트는 "이메일 변경, 서비스 해지, 애플 계정 탈퇴"에 대한 것입니다. Endpoint URL에 등록한 URL로 PAYLOAD라는 키로 JWT 데이터를 전달받을 수 있습니다.) [2] Services ID - 애플 로그인을 진행한 유저의 정보를 전달받기 위한 설정입니다. client_id와 aud로 사용될 Identifier를 기억해주세요. (Return URLs에 등록된 URL로 유저 정보에 대한 JSON 데이터를 전달받을 수 있습니다.) [3] Key - 클라이언트 시크릿을 생성할 때 필요한 Private Key를 생성하기 위한 설정입니다. Key ID와 .p8 파일은 기억해주세요. 모든 설정을 완료했으니, 다음 글에서 코드를 참고하여 확인해보도록 하겠습니다.