<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>hoon.log</title>
        <link>https://velog.io/</link>
        <description>긍정적이고 항상 즐겁게</description>
        <lastBuildDate>Wed, 18 Sep 2024 04:17:23 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <copyright>Copyright (C) 2019. hoon.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/jhlee_" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[MSA에서 서비스 간 원자성을 보장하는 트랜잭션 아웃박스 패턴]]></title>
            <link>https://velog.io/@jhlee_/MSA%EC%97%90%EC%84%9C-%EC%84%9C%EB%B9%84%EC%8A%A4-%EA%B0%84-%EC%9B%90%EC%9E%90%EC%84%B1%EC%9D%84-%EB%B3%B4%EC%9E%A5%ED%95%98%EB%8A%94-%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98-%EC%95%84%EC%9B%83%EB%B0%95%EC%8A%A4-%ED%8C%A8%ED%84%B4</link>
            <guid>https://velog.io/@jhlee_/MSA%EC%97%90%EC%84%9C-%EC%84%9C%EB%B9%84%EC%8A%A4-%EA%B0%84-%EC%9B%90%EC%9E%90%EC%84%B1%EC%9D%84-%EB%B3%B4%EC%9E%A5%ED%95%98%EB%8A%94-%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98-%EC%95%84%EC%9B%83%EB%B0%95%EC%8A%A4-%ED%8C%A8%ED%84%B4</guid>
            <pubDate>Wed, 18 Sep 2024 04:17:23 GMT</pubDate>
            <description><![CDATA[<h1 id="msa에서-서비스-간-원자성을-보장하는-트랜잭션-아웃박스-패턴">MSA에서 서비스 간 원자성을 보장하는 트랜잭션 아웃박스 패턴</h1>
<p>마이크로서비스 아키텍처(MSA)에서는 여러 서비스가 독립적으로 배포되고 운영되기 때문에 서비스 간의 데이터 일관성을 유지하는 것이 중요한 과제입니다. 특히, 한 서비스의 트랜잭션이 다른 서비스와 연동될 때 원자성을 보장하기 위한 방안이 필요합니다. 이를 해결하기 위해 널리 사용되는 패턴 중 하나가 <strong>트랜잭션 아웃박스(Transactional Outbox)</strong> 패턴입니다.</p>
<h2 id="트랜잭션-아웃박스-패턴이란">트랜잭션 아웃박스 패턴이란?</h2>
<p>트랜잭션 아웃박스 패턴은 <strong>로컬 트랜잭션과 메시지 전송 간의 일관성 문제</strong>를 해결하는 데 사용됩니다. 일반적으로 서비스 간 통신에서 데이터 일관성을 유지하기 위해 트랜잭션을 확장하는 방법이 필요하지만, MSA에서는 서비스별 데이터베이스가 분리되어 있어 분산 트랜잭션을 적용하기 어렵습니다. 트랜잭션 아웃박스 패턴은 로컬 트랜잭션 내에서 데이터를 저장하면서, 비동기적으로 메시지를 전달하는 방식으로 일관성을 보장합니다.</p>
<h3 id="트랜잭션-아웃박스-패턴의-구조">트랜잭션 아웃박스 패턴의 구조</h3>
<p>트랜잭션 아웃박스 패턴은 크게 세 단계로 나뉩니다:</p>
<ol>
<li><strong>Outbox 테이블에 이벤트 저장</strong>: 서비스 내 트랜잭션이 성공적으로 처리될 때, 해당 트랜잭션과 함께 Outbox 테이블에 이벤트를 저장합니다.</li>
<li><strong>비동기 이벤트 처리</strong>: Outbox 테이블에 저장된 이벤트는 별도의 프로세스에 의해 비동기로 처리되며, 메시지 브로커(Kafka, RabbitMQ 등)에 전송됩니다.</li>
<li><strong>이벤트 상태 업데이트</strong>: 이벤트가 성공적으로 처리되면 Outbox 테이블의 상태가 업데이트됩니다.</li>
</ol>
<p>이 방식은 <strong>로컬 트랜잭션</strong>과 <strong>비동기 메시지 전송</strong>을 분리하여 서비스 간 데이터 일관성을 보장합니다.</p>
<h3 id="트랜잭션-아웃박스-패턴의-장점">트랜잭션 아웃박스 패턴의 장점</h3>
<ul>
<li><strong>원자성 보장</strong>: 데이터베이스 트랜잭션과 메시지 전송을 하나의 트랜잭션으로 처리하지 않아도, 같은 로컬 트랜잭션 내에서 Outbox에 이벤트를 기록하여 원자성을 보장할 수 있습니다.</li>
<li><strong>확장성</strong>: 각 서비스는 독립적으로 이벤트를 처리하므로, 비동기 처리와 재시도 로직을 구현하여 안정성을 높일 수 있습니다.</li>
<li><strong>데이터 일관성 보장</strong>: 메시지 전송 중 실패하더라도 Outbox 테이블의 데이터를 기반으로 재처리가 가능해 데이터 손실을 방지할 수 있습니다.</li>
</ul>
<h3 id="트랜잭션-아웃박스-패턴의-단점">트랜잭션 아웃박스 패턴의 단점</h3>
<ul>
<li><strong>복잡성 증가</strong>: 별도의 Outbox 테이블을 관리하고, 메시지 전송 로직을 구현해야 하기 때문에 시스템의 복잡도가 증가할 수 있습니다.</li>
<li><strong>지연 발생 가능성</strong>: 이벤트를 비동기로 처리하기 때문에 실시간 응답이 필요한 시나리오에서는 추가적인 지연이 발생할 수 있습니다.</li>
</ul>
<h2 id="spring-boot로-트랜잭션-아웃박스-패턴-구현">Spring Boot로 트랜잭션 아웃박스 패턴 구현</h2>
<p>Spring Boot에서 트랜잭션 아웃박스 패턴을 적용하는 예시를 살펴보겠습니다.</p>
<h3 id="1-outbox-테이블-생성">1. Outbox 테이블 생성</h3>
<p>먼저 각 서비스의 데이터베이스에 Outbox 테이블을 생성합니다. 이 테이블은 전송할 이벤트 데이터를 저장하는 역할을 합니다.</p>
<pre><code class="language-sql">CREATE TABLE outbox (
  id BIGINT AUTO_INCREMENT PRIMARY KEY,
  aggregate_id VARCHAR(255),
  event_type VARCHAR(255),
  payload TEXT,
  status VARCHAR(255),
  created_at TIMESTAMP
);</code></pre>
<h3 id="2-로컬-트랜잭션에서-outbox에-이벤트-저장">2. 로컬 트랜잭션에서 Outbox에 이벤트 저장</h3>
<p>트랜잭션이 성공적으로 완료되면, 같은 트랜잭션 내에서 Outbox 테이블에 이벤트를 저장합니다. 이렇게 하면 데이터베이스와 이벤트 기록이 모두 하나의 트랜잭션으로 묶여 원자성이 보장됩니다.</p>
<pre><code class="language-java">@Service
public class OrderService {

    @Transactional
    public void createOrder(Order order) {
        orderRepository.save(order);

        // Outbox에 이벤트 저장
        OutboxEvent event = new OutboxEvent(order.getId(), &quot;OrderCreated&quot;, order);
        outboxRepository.save(event);
    }
}</code></pre>
<h3 id="3-스케줄러로-outbox-이벤트-처리">3. 스케줄러로 Outbox 이벤트 처리</h3>
<p>이벤트는 별도의 프로세스나 스케줄러를 통해 비동기로 처리됩니다. Outbox 테이블에서 PENDING 상태인 이벤트를 주기적으로 읽어, 메시지 브로커(Kafka, RabbitMQ 등)에 전송한 후 상태를 업데이트합니다.</p>
<pre><code class="language-java">@Scheduled(fixedDelay = 1000)
public void processOutboxEvents() {
    List&lt;OutboxEvent&gt; events = outboxRepository.findPendingEvents();
    for (OutboxEvent event : events) {
        try {
            // 메시지 브로커로 전송
            messageBroker.send(event);
            event.markAsSent();  // 이벤트 상태 업데이트
            outboxRepository.save(event);
        } catch (Exception e) {
            // 실패 시 재시도 로직 등 예외 처리
        }
    }
}</code></pre>
<h3 id="4-메시지-브로커와-통신">4. 메시지 브로커와 통신</h3>
<p>메시지 브로커(Kafka, RabbitMQ 등)를 통해 다른 서비스로 이벤트를 전달하고, 해당 서비스는 이를 처리합니다. Spring Kafka 또는 Spring AMQP와 같은 라이브러리를 사용하여 메시지를 전송할 수 있습니다.</p>
<h3 id="스케줄러가-이벤트를-정상적으로-처리하지-못한-경우">스케줄러가 이벤트를 정상적으로 처리하지 못한 경우!</h3>
<p>여러가지 원인이 존재하지만 트랜잭션 아웃박스 패턴을 도입하는 이유가 원자성을 보장하기 위함입니다.</p>
<ul>
<li>재시도 로직 구현 (이벤트 상태를 FAIL 로 저장 후 해당 이벤트 재시도</li>
<li>Dead Letter Queue (DLQ) 사용</li>
<li>알림 및 모니터링을 통해 수동 재시도</li>
</ul>
<p>간략하게 위와 같은 대처방법으로 원자성을 보장할 수 있습니다.</p>
<p>더 나아가 멱등성 또한 보장해야 합니다.</p>
<p>이벤트 간 event id 와 같은 Key 값을 공유하여 해당 Key 에 대한 이벤트 처리 여부를 체크하여 
동일 이벤트에 대해 멱등성을 보장하는 방법이 존재합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[elasticsearch, kibana docker-compose 로 올리기]]></title>
            <link>https://velog.io/@jhlee_/elasticsearch-kibana-docker-compose-%EB%A1%9C-%EC%98%AC%EB%A6%AC%EA%B8%B0</link>
            <guid>https://velog.io/@jhlee_/elasticsearch-kibana-docker-compose-%EB%A1%9C-%EC%98%AC%EB%A6%AC%EA%B8%B0</guid>
            <pubDate>Thu, 18 Jul 2024 12:17:18 GMT</pubDate>
            <description><![CDATA[<h4 id="docker-compose-로-elasticsearch-kibana-구동">docker-compose 로 Elasticsearch, Kibana 구동</h4>
<blockquote>
<p>docker-compose.yml</p>
</blockquote>
<pre><code>services:
  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:8.5.2
    container_name: elasticsearch
    environment:
      - node.name=es01
      - cluster.name=es-docker-cluster
      - discovery.type=single-node // 멀티 노드로 구성할 경우 추가해야 함
      - bootstrap.memory_lock=true
      - xpack.security.enabled=false
      - &quot;ES_JAVA_OPTS=-Xms1g -Xmx1g&quot;
    ulimits:
      memlock:
        soft: -1
        hard: -1
    volumes:
      - ./data:/usr/share/elasticsearch/data
    ports:
      - 9200:9200
    networks:
      - elastic

  kibana:
    container_name: kibana
    image: docker.elastic.co/kibana/kibana:8.5.2
    environment:
      - ELASTICSEARCH_HOSTS=http://elasticsearch:9200
    ports:
      - 5601:5601
    depends_on:
      - elasticsearch
    networks:
      - elastic

networks:
  elastic:
    external: true</code></pre><blockquote>
<p>docker-compose up -d | docker-compose down 명령어로 실행 및 중지</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/jhlee_/post/83309fcd-6251-451c-91ce-93152ce33276/image.png" alt=""></p>
<p>위와 같이 잘 실행되면 localhost:5601 로 접속하면 됩니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring Boot] Slack 외부 API 연동]]></title>
            <link>https://velog.io/@jhlee_/Spring-Boot-Slack-%EC%99%B8%EB%B6%80-API-%EC%97%B0%EB%8F%99</link>
            <guid>https://velog.io/@jhlee_/Spring-Boot-Slack-%EC%99%B8%EB%B6%80-API-%EC%97%B0%EB%8F%99</guid>
            <pubDate>Thu, 11 Apr 2024 08:45:31 GMT</pubDate>
            <description><![CDATA[<p>SQS를 이용하여 데이터 동기화 작업 중 리스너 모듈에서 데이터를 정확히 받지 못하거나
데이터 동기화 중 에러가 발생하여 실패할 경우 모니터링 및 빠른 정상화(?)를 위해 Slacp API를 통해
Notification 을 적용하려고 합니다.</p>
<hr>
<p>보통은 DLQ(dead letter queue)라고 해서 별도 queue를 구성하여 컨슘 또는 구독에 실패한 데이터를 저장하는 용도를 구성하는데 해당 방법보다 Slack 메시지로 즉각적으로 대비할 수 있도록 하기 위해 Slack을 사용했습니다.</p>
<hr>
<p>1.webhook 앱 추가
<img src="https://velog.velcdn.com/images/jhlee_/post/93f19874-2281-45ce-acb8-cfc69dd57ed6/image.png" alt=""></p>
<p>먼저 자신의 Slack 앱에서 Incoming Webhooks 을 검색 후 slack에 추가 버튼을 통해 Incoming Webhooks 을 추가합니다.</p>
<p>2.알림받을 채널 생성 
<img src="https://velog.velcdn.com/images/jhlee_/post/6bf7023f-23af-411e-bfb4-24d0927407e5/image.png" alt="">
알림받을 기존 채널을 선택하거나 생성 후 앱 추가</p>
<p>3.webhook url 을 통해 알림 전송
<img src="https://velog.velcdn.com/images/jhlee_/post/b84184ae-e5d9-4d0b-b03d-1a95c763a067/image.png" alt=""></p>
<p>해당 url에 post 요청, json 문자열을 payload로 보내면 처음 선택한 채널로 메시지가 전송됩니다.</p>
<p>Slack Block Kit Builder</p>
<blockquote>
<p><a href="https://app.slack.com/block-kit-builder">https://app.slack.com/block-kit-builder</a></p>
</blockquote>
<p>위 주소에서 커스텀 메시지를 만들 수 있습니다. 메시지를 커스터마이징 하고 싶은 분은 참고하시길 바랍니다.</p>
<p>이상 간단하게 Slack notificaion 구현을 마치겠습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring Boot] Event Handler]]></title>
            <link>https://velog.io/@jhlee_/Spring-Boot-Event-Handler</link>
            <guid>https://velog.io/@jhlee_/Spring-Boot-Event-Handler</guid>
            <pubDate>Sat, 23 Mar 2024 14:36:49 GMT</pubDate>
            <description><![CDATA[<h4 id="spring-boot-event-기반-동작-구현">Spring Boot Event 기반 동작 구현</h4>
<hr>
<blockquote>
<p>적용 예시 (회원가입)
회원가입 후 DB 회원정보 저장
회원정보로 받은 Email에 회원가입 메일 전송</p>
</blockquote>
<p>여기서 제가 진행 중인 MSA 프로젝트에서는 회원 도메인과 이메일 Notificaion 도메인이 분리가 되어 있어 회원 서비스에서는 회원 가입만 수행하고
이메일 전송은 notification 서비스에서 수행합니다.</p>
<p>이 과정을 통해 간단한 코드를 작성하자면</p>
<pre><code>@Slf4j
@RequiredArgsConstructor
@Transactional
@Service
public class AuthService {

    private final AccountRepository accountRepository;
    private final NotificationSNSPublisher notificationSNSPublisher;

    public void join(JoinRequest request) {
        log.info(&quot;join start&quot;);

        Account account = Account.create(request);
        accountRepository.save(account);
        log.info(&quot;account save&quot;);

        // send email
        notificationSNSPublisher.sendEmail(request.getEmail());

        log.info(&quot;join end&quot;);
    }
}</code></pre><p>해당 Service 는 request 로 회원 정보를 입력받고 DB에 회원정보 저장 후 가입회원에게 email 전송하는 간단한 비즈니스 로직입니다.
email을 전송하는 notificaion 서비스와 통신은 비동기 message queue 를 통해 진행하고 있습니다.</p>
<p>해당 join 메소드에 대해 테스트 코드를 작성하고 진행하면 다음과 같은 log 가 출력됩니다.</p>
<pre><code>[nio-8002-exec-3] com.authserver.service.AuthService       : join start
Hibernate: 
    /* insert for
        com.authserver.entity.Account */insert 
    into
        account (name,password,user_id) 
    values
        (?,?,?)
[nio-8002-exec-3] com.authserver.service.AuthService       : account save
[nio-8002-exec-3] com.authserver.service.AuthService       : email listener start
[nio-8002-exec-3] com.authserver.service.AuthService       : email send
[nio-8002-exec-3] com.authserver.service.AuthService       : email listener end
[nio-8002-exec-3] com.authserver.service.AuthService       : join end</code></pre><p>여기서 join 메소드는 문제점이 있습니다.</p>
<ol>
<li>해당 프로젝트는 MSA 구조로 작성되어 Notification 서비스는 회원 서비스와 분리되어있습니다.
회원 서비스는 도메인에 맞게 회원가입 로직만 수행하고 회원가입 이메일 전송은 Notificaion 서비스에서 회원가입한 회원의 이메일 정보만 
message queue AWS SNS, SQS 를 통해 전송합니다
따라서 join 메소드를 실행하면 Transaction이 적용되는 범위에는 실제 이메일이 전송되는 비즈니스 로직이 포함되어 있지않습니다.
만약에라도 Account Entity를 생성하는 코드나 DB에 저장하는 도중 Exception이 발생한다면 이메일 전송도 Transaction에 적용되야 합니다.</li>
</ol>
<p>이 문제를 해결하기 위해 Spring Event에서 제공하는 TransactionalEventListener 를 적용하려고 합니다.</p>
<hr>
<h3 id="spring-event-발행">Spring Event 발행</h3>
<pre><code>@Slf4j
@RequiredArgsConstructor
@Transactional
@Service
public class AuthService {

    private final AccountRepository accountRepository;
    private final ApplicationEventPublisher applicationEventPublisher;

    public void join(JoinRequest request) {
        log.info(&quot;join start&quot;);

        Account account = Account.create(request);
        accountRepository.save(account);
        log.info(&quot;account save&quot;);

        // send join Email
        applicationEventPublisher.publishEvent(request);

        log.info(&quot;join end&quot;);
    }
}</code></pre><p>ApplicationEventPublisher 를 통해 해당 이벤트를 발행시킵니다.
이벤트를 발생하는 방법은 간단하게 publishEvent 메소드만 호출하면됩니다.</p>
<h3 id="spring-event-listener">Spring Event Listener</h3>
<p>publishEvent에 파라미터로 전달된 객체를 받는 Listener를 구현해야 합니다.</p>
<pre><code>@Slf4j
@Component
public class EventHandler {

    private final NotificaionSNSPublisher notificaionSNSPublisher; 

    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void sendEmail(JoinRequest request){
        log.info(&quot;email listener start&quot;);
        log.info(&quot;email send&quot;);
        notificaionSNSPublisher.send(request.getEmail());
        log.info(&quot;email listener end&quot;);
    }
}</code></pre><p>EventHandler 를 @Component 적용하여 Bean으로 등록하고
실제 Listener에는 @TransactionalEventListener 작성합니다. @TransactionalEventListener 옵션으로 phase를 설정해야 하는데
4가지 BEFORE_COMMIT, AFTER_COMMIT, AFTER_ROLLBACK, AFTER_COMPLETION 가 존재합니다.
저는 여기서 AFTER_COMMIT 을 적용하겠습니다. 위 phase 작동원리는 따로 찾아보시길 바랍니다.</p>
<p>AFTER_COMMIT 의 작동원리는 이벤트를 발생한 트렌젝션이 commit이 된 후 해당 이벤트 리스너가 실행되는 원리입니다.</p>
<p>Event를 적용시킨 후 service 테스트 코드를 실행해보겠습니다.</p>
<pre><code>[nio-8002-exec-3] com.authserver.service.AuthService       : join start
Hibernate: 
    /* insert for
        com.authserver.entity.Account */insert 
    into
        account (name,password,user_id) 
    values
        (?,?,?)
[nio-8002-exec-3] com.authserver.service.AuthService       : account save
[nio-8002-exec-3] com.authserver.service.AuthService       : join end
[nio-8002-exec-3] com.authserver.service.AuthService       : email listener start
[nio-8002-exec-3] com.authserver.service.AuthService       : email send
[nio-8002-exec-3] com.authserver.service.AuthService       : email listener end</code></pre><p>아까와는 다른 log가 출력됩니다.
join 메소드 commit이 이루어진 후에 listener log가 출력됩니다.
join 메소드가 정상적으로 수행되지 않고 exception이 발생된다면 listener 또한 실행되지 않습니다.
이로서 문제되었던 Transaction이 적용되어 join 메소드와 listener 가 하나의 Transaction으로 묶여 작동하게됩니다.</p>
<p><strong><em>!!!!! 하지만 또하나의 문제점이 있습니다.</em></strong>
join 메소드가 정상적으로 commit이 되면 DB에는 회원정보가 정상적으로 저장되고 listener가 수행된다고 했습니다.
만약 listener에서 exception이 발생된다면 어떻게 될까요...?
앞서 말씀드렸겠지만 회원 서비스와 알림 서비스는 분리가 되어있습니다. 따라서 알림 서비스에서 exception이 발생하거나 로직상 문제가 있어
이메일 전송이 실패했다면..
우리는 message queue 를 사용하여 서비스간 통신을 하기 때문에 SQS subscription 이 실패하는 상황에 대비해야 합니다.</p>
<p>다음 포스트에서는 message subscription 이 실패하는 상황에 대비하는 방법을 작성하겠습니다.</p>
<p>작성된 글에서 잘못된 부분이 있을경우 많은 지적바랍니다</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring Boot] Google In App Purchase 인앱결제]]></title>
            <link>https://velog.io/@jhlee_/Spring-Boot-Google-In-App-Purchase-%EC%9D%B8%EC%95%B1%EA%B2%B0%EC%A0%9C</link>
            <guid>https://velog.io/@jhlee_/Spring-Boot-Google-In-App-Purchase-%EC%9D%B8%EC%95%B1%EA%B2%B0%EC%A0%9C</guid>
            <pubDate>Tue, 02 May 2023 12:22:14 GMT</pubDate>
            <description><![CDATA[<p>ios 앱과 마찬가지로 andorid 인앱결제에 대한 서버 작업에 대해 정리</p>
<hr>
<p>andorid 인앱결제 상품에 대해 파악해야 합니다.
소모성 상품인 일회용 상품이 있고, 정기 결제(매 월 결제) 하는 정기 결제 상품이 존재하니 
해당 프로젝트의 설계에 맞는 상품을 google 에 등록하여 해당 상품을 준비하면 됩니다.</p>
<p>자세한 내용은 <a href="https://developer.android.com/google/play/billing?hl=ko">google play</a>에서 확인 가능합니다.</p>
<p><img src="https://velog.velcdn.com/images/jhlee_/post/ed5cd633-a70f-4c69-ba19-14ce3ed1c309/image.png" alt="google 정기결제 상품">
<a href="https://play.google.com/console">google play console</a> 에서 실제 상품을 등록한 내용</p>
<p>인앱결제에 대해 서버에서 구현해야 할 비즈니스 로직은 ios와 마찬가지로 간단합니다.
앱내 결제가 이루어지고 해당 결제 건에 대한 영수증(receipt) 데이터를 서버에 던져 검증(verify)만 통과한다면 그 이후
db에 데이터를 저장하고 해당 상품 구입에 대한 권한? 만 사용자에게 부여하면 됩니다.</p>
<p><a href="https://console.cloud.google.com/">google cloud console</a> 에서 oauth2 클라이언트 등록 후 api 사용 key(json) 다운로드
해당 key(json)파일을 프로젝트 res 폴더에 복사</p>
<pre><code>{
  &quot;type&quot;: &quot;service_account&quot;,
  &quot;project_id&quot;: &quot;---&quot;,
  &quot;private_key_id&quot;: &quot;---&quot;,
  &quot;private_key&quot;: &quot;-----BEGIN PRIVATE KEY-----\nMblahblah=\n-----END PRIVATE KEY-----\n&quot;,
  &quot;client_email&quot;: &quot;email&quot;,
  &quot;client_id&quot;: &quot;123&quot;,
  &quot;auth_uri&quot;: &quot;https://accounts.google.com/o/oauth2/auth&quot;,
  &quot;token_uri&quot;: &quot;https://oauth2.googleapis.com/token&quot;,
  &quot;auth_provider_x509_cert_url&quot;: &quot;https://www.googleapis.com/oauth2/v1/certs&quot;,
  &quot;client_x509_cert_url&quot;: &quot;?&quot;
}</code></pre><p>해당 json key 를 이용하여 google iap(in app purchase) api 호출</p>
<h1 id="gradle-dependency-추가">gradle dependency 추가</h1>
<pre><code>implementation &#39;com.google.apis:google-api-services-androidpublisher:v3-rev20230313-2.0.0&#39;</code></pre><h1 id="영수증-데이터purchasetoken-검증">영수증 데이터(purchaseToken) 검증</h1>
<pre><code>    /**
     * 영수증 데이터(purchase token)으로 api 호출하여 정상적인 응답이 내려올 경우 responseDto에 매핑
     * @param token
     * @return
     */
    public GoogleIAPResponseDto verifyReceipt(String token) {

        AndroidPublisher publisher = null;
        GoogleIAPResponseDto googleIAPResponseDto;
        try {
            publisher = googleCredentialsConfig.androidPublisher();
            AccessToken accessToken = googleCredentialsConfig.getAccessToken();
            AndroidPublisher.Purchases.Subscriptionsv2.Get get = publisher.purchases().subscriptionsv2()
                    .get(packageName, token);
            get.setAccessToken(accessToken.getTokenValue());
            SubscriptionPurchaseV2 subscriptionPurchaseV2 = get.execute();

            /*
                subscriptionPurchaseV2 객체를 dto에 매핑하는 작업 각자의 db 설계에 맞도록
            */

        } catch (IOException | GeneralSecurityException e) {
            throw new InternalServerErrorException(&quot;IAP.VALIDATE_RECEIPT_FAIL&quot;);
        }
        return googleIAPResponseDto;
    }</code></pre><p>이후 정상적인 영수증 데이터일 경우 해당 상품의 정보를 db에 저장했습니다. (결제일, 금액, 상품에 대한 내용 등)</p>
<p>저 같은 경우 정기 결제(30일 이용권)을 등록하여 결제를 진행 했고 해당 상품의 내용도 api에 잘 담겨서 내려왔습니다.</p>
<p>ios, android 인앱결제에 대한 구현을 처음 경험했지만 생각보다 서버에서 처리할 작업이 많지는 않아 쉽게 해결했고</p>
<p>다음 포스팅은 ios, android 인앱결제 상품에 대해 환불(refund)에 대한 내용을 공유하겠습니다.</p>
<p>해당 내용은 <a href="https://github.com/LeeJeongHun1/spring-in-app-purchase">git</a>에 올렸습니다.</p>
]]></description>
        </item>
    </channel>
</rss>