<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>im_ssu.log</title>
        <link>https://velog.io/</link>
        <description>Android Developer</description>
        <lastBuildDate>Wed, 15 Jun 2022 03:53:59 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>im_ssu.log</title>
            <url>https://images.velog.io/images/im_ssu/profile/52725613-7089-43e3-9d01-00a27904c26d/KakaoTalk_20220321_140004646.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. im_ssu.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/im_ssu" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[안드로이드에 클린아키텍처 적용하기]]></title>
            <link>https://velog.io/@im_ssu/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C%EC%97%90-%ED%81%B4%EB%A6%B0%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@im_ssu/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C%EC%97%90-%ED%81%B4%EB%A6%B0%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 15 Jun 2022 03:53:59 GMT</pubDate>
            <description><![CDATA[<h2 id="클린-아키텍처란">클린 아키텍처란?</h2>
<p><strong>클린 아키텍처(Clean Architecture)</strong> 는 2012년 _로버트 C 마틴 (aka 밥아저씨)_가 탄생시킨(?) 개념이다.</p>
<p>이전의 시스템 아키텍쳐들(알리스테어 코번의 헥사고날 아키텍쳐, 제프리 팔레모르의 어니언 아키텍쳐, 밥아저씨의 스크리밍 아키텍쳐 등)은 세세한 부분은 다르지만 모두 같은 목적을 갖고 있는데, 바로 <strong>관심사의 분리</strong>이다.</p>
<p>관심사의 분리를 위해 공통적으로 추구하는 내용은 아래와 같다.</p>
<ol>
<li>UI, 데이터베이스, 프레임워크, 외부기능 등의 독립성</li>
<li>테스트의 용이성</li>
<li>기능 변경 및 확장의 용이성</li>
</ol>
<p>이런 내용들을 단일 개념으로 통합하여 나온 그림이 아래의 그림이다.
<img src="https://velog.velcdn.com/images/im_ssu/post/eb5294cd-e537-4a47-9797-4ae2814ab15a/image.png" alt=""></p>
<h4 id="의존-규칙">의존 규칙</h4>
<p>각 동심원은 서로 다른 영역을 표현하며 안으로 향할수록 고수준, 바깥으로 갈수록 저수준의 계층이다. 의존 규칙에 의해서 소스 코드는 안쪽을 향해서만 의존할 수 있다. 즉, 반드시 저수준에서 고수준으로 향해야 한다. 안쪽의 원은 바깥쪽의 원에 대해 알지 못하며 참조해서는 안된다.</p>
<h4 id="엔티티-entity">엔티티 (Entity)</h4>
<p>엔티티는 대규모 프로젝트 레벨의 핵심 비즈니스 규칙을 캡슐화 한다. 메서드를 가진 객체 일 수도 있지만 데이터 구조와 함수의 집합 일 수 도 있다.
바깥쪽에서 무엇이 변경되더라도 바뀌지 않는다.</p>
<h4 id="유즈케이스-usecase">유즈케이스 (UseCase)</h4>
<p>애플리케이션 고유 비지니스 규칙을 포함한다. 엔티티로 들어오고 나가는 데이터의 흐름을 조정한다.
유즈케이스에서 발생한 변경이 엔티티에 영향을 주어서는 안되며 또한 유즈케이스 바깥쪽에서 발생한 변경이 유즈케이스에 영향을 주어서는 안된다.</p>
<h4 id="인터페이스-어댑터-interface-adapter">인터페이스 어댑터 (Interface Adapter)</h4>
<p>인터페이스 어댑터 계층은 일련의 어댑터들로 구성되는데, 어댑터는 유스케이스와 엔티티의 데이터를 외부 계층에 적용할 수 있는 형식으로 변환한다. MVC, MVP, MVVM 등의 Controller, Presenter, ViewModel 등이 이 계층에 속한다.</p>
<h4 id="프레임워크과-드라이버">프레임워크과 드라이버</h4>
<p>가장 바깥쪽의 계층은 데이터베이스나 웹 프레임워크, UI 등으로 구성되는데 일반적으로 안쪽원과 통신하기 위한 코드외에 특별히 작성할 코드가 많지 않다.</p>
<h4 id="원은-네개여야만-할까">원은 네개여야만 할까?</h4>
<p>아니다! 원은 컨셉을 전하기 위한 수단일뿐, 지켜야한다는 규칙은 없다. 다만 의존 규칙은 항상 지켜야한다. (반드시 저수준 -&gt; 고수준)</p>
<p>&#x2001;</p>
<h2 id="안드로이드에-적용하기">안드로이드에 적용하기</h2>
<p>아래는 안드로이드 뿐 아니라 모바일에서 적용되는 클린 아키텍처의 개념을 설명한 그림이다.
<img src="https://velog.velcdn.com/images/im_ssu/post/278c691d-2995-4c01-a12f-e4388eb77ba7/image.png" alt=""></p>
<p>3개의 레이어로 이루어져 있으며, 클린 아키텍쳐상의 Entity 개념은 채택하지 않는다 (Data Layer의 Entity는 클린 아키텍처의 Entity의 개념과 다름)</p>
<p>의존성의 방향은 Presentation(UI) -&gt; Data -&gt; Domain 이다.
(Repository의 위치에 따라 의존성 방향이 Presentation(UI) -&gt; Domain -&gt; Data 로 될 수 있다)</p>
<p>각각의 레이어들을 이미지 검색 앱으로 예를 들어 설명해보겠다. <a href="https://github.com/im-sujeong/Coroutine-SearchImage">(코드 전문)</a></p>
<h4 id="presentation-layer-ui-layer">Presentation Layer (UI Layer)</h4>
<p>화면과 관련된 모든 것들이 이 레이어에 포함된다.</p>
<ul>
<li><p><img src="https://velog.velcdn.com/images/im_ssu/post/f7a46b09-db05-4f46-8e42-e19dd57c19e5/image.png" alt=""></p>
</li>
<li><p><strong>UI</strong> : UI를 표시하는 부분 (HostActivity, SearchImageFragment 등)</p>
</li>
<li><p><strong>Presenter</strong> : UI 업데이트와 관련된 로직을 구현 (SearchImageViewModel 등)</p>
<pre><code class="language-kotlin">@HiltViewModel
class SearchImageViewModel @Inject constructor(
  private val useCases: UseCases
): BaseViewModel() {
  private val _state = MutableLiveData&lt;SearchImageState&gt;()
  val state : LiveData&lt;SearchImageState&gt; = _state

  private val queryFlow = MutableSharedFlow&lt;String&gt;()

  override fun fetchData() = viewModelScope.launch{
      queryFlow
          .flatMapLatest {
              searchImage(it)
          }
          .cachedIn(viewModelScope)
          .collectLatest {
              setState(SearchImageState.Success(it))
          }
  }

  private suspend fun searchImage(query: String): Flow&lt;PagingData&lt;ImageModel&gt;&gt; = useCases.searchImageUseCase(query)

  fun handleQuery(query: String) = viewModelScope.launch(errorHandler) {
      queryFlow.emit(query)
  }

  fun favoriteImage(imageModel: ImageModel) = viewModelScope.launch(errorHandler){
      useCases.insertFavoriteImageUseCase(imageModel)
  }

  private fun setState(state: SearchImageState) {
      _state.value = state
  }
}</code></pre>
</li>
</ul>
<h4 id="domain-layer">Domain Layer</h4>
<p>독립적인 계층으로, 다른 계층의 변경이 Domain Layer에 영향을 끼쳐서는 안된다.</p>
<ul>
<li><img src="https://velog.velcdn.com/images/im_ssu/post/cfae78fe-b613-4d9b-93a4-f9e64a30ea1c/image.png" alt=""></li>
<li><strong>UseCase</strong> : 행동들의 최소 단위<ul>
<li>이름만 보고도 무슨 기능을 수행하는지 알 수 있어야한다 (마치 요구사항 명세서 같다..!)<pre><code class="language-kotlin">class SearchImageUseCase(
private val imageRepository: ImageRepository
) {
suspend operator fun invoke(query: String): Flow&lt;PagingData&lt;Image&gt;&gt; {
  return imageRepository.searchImages(query)
}
}</code></pre>
</li>
</ul>
</li>
<li><strong>Model</strong> : 필요한 데이터<pre><code class="language-kotlin">data class ImageModel(
  val title: String,
  val link: String,
  val thumbnail: String,
  val sizeHeight: Int,
  val sizeWidth: Int,
  val isFavorite: Boolean = false
)</code></pre>
</li>
<li><strong>Repository (Interface)</strong> : 관련된 행동들을 정의<ul>
<li>Domain Layer 를 독립적으로 만들기 위해 Repository를 Interface와 구현체로 분리해야함</li>
</ul>
</li>
</ul>
<pre><code class="language-kotlin">interface ImageRepository {
    fun getFavoriteImages(): Flow&lt;List&lt;ImageEntity&gt;&gt;

    suspend fun searchImages(query: String): Flow&lt;PagingData&lt;ImageModel&gt;&gt;

    suspend fun insertFavoriteImage(imageEntity: ImageEntity)

    suspend fun deleteFavoriteImage(imageEntity: ImageEntity)
}</code></pre>
<h4 id="data-layer">Data Layer</h4>
<ul>
<li><p><img src="https://velog.velcdn.com/images/im_ssu/post/4f76f7de-22c9-4dc8-baf3-62279a3d8522/image.png" alt=""> </p>
</li>
<li><p><strong>DataStore</strong> : 로컬 DB 또는 REST API 통신과 관련된 내용</p>
<pre><code class="language-kotlin">@Database(
  entities = [ImageEntity::class],
  version = 1
)
abstract class ImageDatabase: RoomDatabase() {
  abstract val imageDao: ImageDao
}</code></pre>
<pre><code class="language-kotlin">@Dao
interface ImageDao {
  @Insert(onConflict = OnConflictStrategy.REPLACE)
  suspend fun insertImage(imageEntity: ImageEntity)

  @Query(&quot;SELECT * FROM imageentity&quot;)
  fun getImages(): Flow&lt;List&lt;ImageEntity&gt;&gt;

  @Delete
  suspend fun deleteImage(imageEntity: ImageEntity)
}</code></pre>
</li>
<li><p><strong>Entity</strong> : 로컬 DB의 테이블을 만들기 위한 Entity와 서버 통신을 위한 Dto가 포함된다.</p>
<pre><code class="language-kotlin">@Entity
data class ImageEntity(
  @PrimaryKey val title: String,
  val link: String,
  val thumbnail: String,
  val sizeHeight: Int,
  val sizeWidth: Int
)</code></pre>
<pre><code class="language-kotlin">data class ImageDto(
  val title: String,
  val link: String,
  val thumbnail: String,
  @SerializedName(&quot;sizeheight&quot;) val sizeHeight: Int,
  @SerializedName(&quot;sizewidth&quot;) val sizeWidth: Int
)</code></pre>
</li>
<li><p><strong>Repository (구현체)</strong> : Domain Layer의 Repository Interface의 실제 구현을 담당한다.</p>
<pre><code class="language-kotlin">class ImageRepositoryImpl(
  private val ioDispatcher: CoroutineDispatcher,
  private val searchApi: SearchApi,
  private val imageDao: ImageDao
): ImageRepository {
  override fun getFavoriteImages(): Flow&lt;List&lt;ImageEntity&gt;&gt; =
      imageDao.getImages().flowOn(ioDispatcher)

  override suspend fun searchImages(query: String): Flow&lt;PagingData&lt;ImageModel&gt;&gt; {
      return Pager(
          config = PagingConfig(
              pageSize = SearchImagesDataSource.defaultDisplay,
              enablePlaceholders = false
          ),
          pagingSourceFactory = {
              SearchImagesDataSource(query, searchApi)
          }
      ).flow
  }

  override suspend fun insertFavoriteImage(imageEntity: ImageEntity) {
      imageDao.insertImage(imageEntity)
  }

  override suspend fun deleteFavoriteImage(imageEntity: ImageEntity) {
      imageDao.deleteImage(imageEntity)
  }
}</code></pre>
</li>
<li><p><strong>Mapper</strong> : Entity -&gt; Model,  Dto -&gt; Entity, Dto -&gt; Model 등과 같이 데이터들의 형식을 변환한다.</p>
<pre><code class="language-kotlin">fun ImageEntity.toImageModel() : ImageModel =
  ImageModel(
      title = title,
      link = link,
      thumbnail = thumbnail,
      sizeHeight = sizeHeight,
      sizeWidth = sizeWidth
  )
</code></pre>
</li>
</ul>
<p>fun List<ImageEntity>.toImageModels(): List<ImageModel> =
    map {
        it.toImageModel()
    }</p>
<p>fun ImageDto.toImageModel(): ImageModel =
    ImageModel(
        title = title,
        link = link,
        thumbnail = thumbnail,
        sizeHeight = sizeHeight,
        sizeWidth = sizeWidth
    )</p>
<p>fun ImageModel.toImageEntity(): ImageEntity =
    ImageEntity(
        title = title,
        link = link,
        thumbnail = thumbnail,
        sizeHeight = sizeHeight,
        sizeWidth = sizeWidth
    )</p>
<p>```</p>
<h4 id="-참고">* 참고</h4>
<ul>
<li><a href="https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html">The Clean Architecture - 원문</a></li>
<li><a href="https://blog.coderifleman.com/2017/12/18/the-clean-architecture/">The Clean Architecture - 번역</a></li>
<li><a href="https://uchanlee.dev/clean-architecture/book/ch22/">[클린아키텍처] 22장 - 클린 아키텍처</a></li>
<li><a href="https://medium.com/@justfaceit/clean-architecture%EB%8A%94-%EB%AA%A8%EB%B0%94%EC%9D%BC-%EA%B0%9C%EB%B0%9C%EC%9D%84-%EC%96%B4%EB%96%BB%EA%B2%8C-%EB%8F%84%EC%99%80%EC%A3%BC%EB%8A%94%EA%B0%80-1-%EA%B2%BD%EA%B3%84%EC%84%A0-%EA%B3%84%EC%B8%B5%EC%9D%84-%EC%A0%95%EC%9D%98%ED%95%B4%EC%A4%80%EB%8B%A4-b77496744616">Clean Architecture는 모바일 개발을 어떻게 도와주는가? - (1) 경계선: 계층 나누기</a></li>
<li><a href="https://developer.android.com/topic/architecture">앱 아키텍쳐 가이드</a></li>
<li><a href="https://vagabond95.me/posts/clean-architecture-1/">[안드로이드] Clean Architecture 를 도입하며</a></li>
<li><a href="https://jungwoon.github.io/android/2021/04/12/Android-CleanArchitecture.html">안드로이드에서 클린 아키텍처 구현하기</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[안드로이드 구글 인앱 결제 V4]]></title>
            <link>https://velog.io/@im_ssu/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-%EA%B5%AC%EA%B8%80-%EC%9D%B8%EC%95%B1-%EA%B2%B0%EC%A0%9C-V3</link>
            <guid>https://velog.io/@im_ssu/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-%EA%B5%AC%EA%B8%80-%EC%9D%B8%EC%95%B1-%EA%B2%B0%EC%A0%9C-V3</guid>
            <pubDate>Wed, 11 May 2022 02:32:58 GMT</pubDate>
            <description><![CDATA[<p>구글의 인앱결제 정책에 따라 현재 서비스 하고 있는 어플에 인앱 결제 기능을 추가해야한다.
결제 정책은 아래 링크에서 확인 할 수 있다.</p>
<ul>
<li><a href="https://support.google.com/googleplay/android-developer/answer/10281818?hl=ko">구글 인앱결제 정책 이해</a></li>
<li><a href="https://support.google.com/googleplay/android-developer/answer/9858738?visit_id=637879144016212766-2594404329&amp;rd=1">구글 인앱결제 정책</a></li>
</ul>
<p>&#x2001; 
<em>하.. 발등에 <del>또ㅇ.. 아니</del> 불이 떨어졌다. 😇</em></p>
<p>&#x2001;
얼른 인앱 결제를 구현해보자
<em>(아래 내용은 <a href="https://developer.android.com/google/play/billing?hl=ko">Google Play 결제 시스템</a> 문서를 참고함)</em></p>
<h2 id="준비하기">준비하기</h2>
<h4 id="결제-프로필-만들기">결제 프로필 만들기</h4>
<ul>
<li>구글 플레이콘솔 -&gt; 설정 -&gt; 결제 프로필에서 결제 프로필 만들기를 클릭하여 일련의 과정에 따라 결제 프로필을 만들어주면 된다.
<img src="https://velog.velcdn.com/images/im_ssu/post/528b6e5c-f379-41c1-975b-a42530a24641/image.png" alt="">
&#x2001;<h4 id="google-play-console에서-결제-관련-기능-사용-설정">Google Play Console에서 결제 관련 기능 사용 설정</h4>
</li>
<li>인앱 상품을 등록하기 위해서는 Googole Play 결제 라이브러리가 포함된 앱 버전을 게시해야 한다.
앱의 build.gradle 에 아래 종속 항목을 추가하여 앱을 빌드한다.</li>
</ul>
<pre><code class="language-kotlin">dependencies {
    implementation &#39;com.android.billingclient:billing:4.1.0&#39;
}</code></pre>
<ul>
<li>콘솔에 등록된 앱의 출시 -&gt; 테스트 -&gt; 내부 테스트 -&gt; 새 버전 만들기를 클릭하여 앱을 게시한다.
<img src="https://velog.velcdn.com/images/im_ssu/post/6ed35bb5-ea83-4c48-a3f9-85f6577a0c92/image.png" alt="">
&#x2001;<h4 id="제품-생성-및-구성-소모성-or-비소모성-상품--일회성-청구-상품">제품 생성 및 구성 (소모성 or 비소모성 상품 = 일회성 청구 상품)</h4>
</li>
<li>앱 게시를 완료하였다면 이제 등록된 앱의 수익 창출 -&gt; 인앱 상품 -&gt; 상품 만들기를 클릭하여 상품을 만들 수 있다.
고유한 제품 ID, 이름, 설명 및 가격 정보를 제공한다.
<img src="https://velog.velcdn.com/images/im_ssu/post/95cc9267-be56-4dd5-bcd6-7d0fca6ab478/image.png" alt=""><img src="https://velog.velcdn.com/images/im_ssu/post/754dac3b-f7d0-4935-8ed8-d297cda4748f/image.png" alt="">
&#x2001;<h2 id="구현하기">구현하기</h2>
<h4 id="구매-진행-과정">구매 진행 과정</h4>
</li>
<li>일회성 구매의 일반적인 구매 흐름은 아래와 같다.
<img src="https://velog.velcdn.com/images/im_ssu/post/8cb80d3a-1be8-481b-b2d5-3ca3578d4b6b/image.png" alt="">
&#x2001;<h4 id="billingclient-초기화">BillingClient 초기화</h4>
</li>
<li>구매 관련 업데이트를 수신하기 위하여 setListener()에 PurchasesUpdatedListener를 추가한다.
해당 리스너는 앱의 모든 구매 관련 업데이트를 수신한다.</li>
</ul>
<pre><code class="language-kotlin">val billingClient = BillingClient.newBuilder(this)
    .setListener(PurchasesUpdatedListener { billingResult, purchases -&gt;
        //모든 구매 관련 업데이트를 수신한다.
    })
    .enablePendingPurchases()
    .build()</code></pre>
<p>&#x2001;</p>
<h4 id="google-play-연결-설정">Google Play 연결 설정</h4>
<ul>
<li><p>Google Play에 연결하기 위해 <strong>startConnection()</strong> 을 호출한다. 비동기적이므로 <strong>BillingClientStateListener</strong> 를 구현하여 콜백을 수신해야 한다.</p>
<pre><code class="language-kotlin">billingClient.startConnection(object : BillingClientStateListener {
  override fun onBillingServiceDisconnected() {
      // 연결 실패 시 재시도 로직을 구현.
  }
  override fun onBillingSetupFinished(billingResult: BillingResult) {
      if (billingResult.responseCode ==  BillingClient.BillingResponseCode.OK) {
          // 준비 완료가 되면 상품 쿼리를 처리 할 수 있다!
      }
  }
})</code></pre>
<p>&#x2001;</p>
<h4 id="상품-정보-가져오기">상품 정보 가져오기</h4>
</li>
<li><p>인앱 상품 정보를 쿼리하려면 <strong>querySkuDetailsAsync()</strong> 를 호출한다.
해당 메소드는 SkuType (정기 : SkuType.SUBS / 일회성 : SkuType.INAPP)과 상품 ID 문자열 목록 포함한 <strong>SkuDetailsParams</strong> 를 매개변수로 사용한다.</p>
<pre><code class="language-kotlin">private fun querySkuDetails() {
  val skuList = ArrayList&lt;String&gt;()

  skuList.add(&quot;item_id_1&quot;)
  skuList.add(&quot;item_id_2&quot;)
  skuList.add(&quot;item_id_3&quot;)

  val params = SkuDetailsParams.newBuilder().apply {
      setSkusList(skuList)
      setType(BillingClient.SkuType.INAPP)        //정기 구독일 경우 BillingClient.SkuType.SUBS
  }.build()

  billingClient.querySkuDetailsAsync(params) { billingResult, skuDetailsList -&gt;
      // 완료되면 SkuDetails(상품 상세 정보)를 List 형태로 반환한다.
  }
}</code></pre>
<p>&#x2001;</p>
<h4 id="구매-흐름-시작">구매 흐름 시작</h4>
</li>
<li><p>구매 요청을 시작하려면 <strong>launchBillingFlow()</strong> 를 호출한다.
해당 메소드는 querySkuDetailsAsync() 호출로 반환받은 SkuDetails가 포함된 <strong>BillingFlowParams</strong> 를 매개변수로 사용한다.
그리고 launchBillingFlow()는 BillingResponseCode를 반환하며 이 결과를 검토하여 오류를 처리한다.</p>
<pre><code class="language-kotlin">val flowParams = BillingFlowParams.newBuilder()
  .setSkuDetails(skuDetails)
  .build()
</code></pre>
</li>
</ul>
<p>val billingResult = billingClient.launchBillingFlow(
    this,
    flowParams
)</p>
<p>//launchBillingFlow()는 BillingResponseCode를 반환한다. 
if( billingResult.responseCode != BillingClient.BillingResponseCode.OK ) {
    //오류가 발생 할 경우 여기서 처리
}</p>
<pre><code>- 호출에 성공하면 다음과 같은 구매 화면이 표시된다. (테스트 결제는 다음 챕터에서 설명) 
![](https://velog.velcdn.com/images/im_ssu/post/8b423538-813e-4fbd-81bf-2f0bb9c5b1fa/image.png)

- 구매 결과는 BillingClient 초기화 시 추가했던 PurchasesUpdatedListener 리스너에 전송된다.
```kotlin
val billingClient = BillingClient.newBuilder(this)
    .setListener(PurchasesUpdatedListener { billingResult, purchases -&gt;
        //모든 구매 관련 업데이트를 수신한다.
        purchasesUpdated(billingResult, purchases)
    })
    .enablePendingPurchases()
    .build()

private fun purchasesUpdated(billingResult: BillingResult, purchases: List&lt;Purchase&gt;?) {
    if (billingResult.responseCode == BillingClient.BillingResponseCode.OK &amp;&amp; purchases != null) {
        for (purchase in purchases) {
            //구매 성공 시 처리
        }
    } else if (billingResult.responseCode == BillingClient.BillingResponseCode.USER_CANCELED) {
        // 사용자가 구매를 취소했을 경우 처리
    } else {
        // 이외의 오류 처리
    }
}</code></pre><p>&#x2001;</p>
<h4 id="상품-소비-처리">상품 소비 처리</h4>
<ul>
<li><p>구매(결제)에 성공하면 서비스 중인 서버에 구매(결제) 검증을 요청하고(현재 내용에서는 생략) 검증 성공 시 소비 처리를 해준다.
소비성 상품이 경우는 소비 처리를 해주어야 상품을 다시 구매 할 수 있는데, <strong>consumeAsync()</strong> 를 호출하여 소비 처리를 해준다.
해당 메소드는 <strong>구매 토큰(purchaseToken)</strong>이 포함된 <strong>ConsumeParams</strong>를 매개변수로 사용한다.
(<em>만약 3일 이내(테스트일 경우 3분)에 소비 처리를 하지 않을 경우, 자동 환불 및 구매 취소가 된다.</em>)</p>
<pre><code class="language-kotlin">private fun handlePurchase(purchase: Purchase) {
  // 소비 처리 이전에 구매 검증 필요

  // 소비 처리
  val consumeParams =
      ConsumeParams.newBuilder()
          .setPurchaseToken(purchase.purchaseToken)
          .build()

  billingClient.consumeAsync(consumeParams) { billingResult, str -&gt;
      //소비 처리에 대한 결과 처리 
  }
}</code></pre>
</li>
<li><p>미처 소비 처리 되지 못한 구매 내역이 있을 수 있으므로 onResume() 에서 <strong>queryPurchasesAsync()</strong> 를 호출하여 내역이 있다면 구매 재검증 및 구매 처리를 해주어야 한다.</p>
<pre><code class="language-kotlin">billingClient.queryPurchasesAsync(BillingClient.SkuType.INAPP) { billingResult, purchaseList -&gt;
  if( billingResult.responseCode == BillingClient.BillingResponseCode.OK ) {
      purchaseList.forEach {
          handlePurchase(it)
      }
}
</code></pre>
</li>
</ul>
<pre><code>&amp;#x2001;
## 결제 테스트하기
#### 라이선스 테스트 설정
- 구글 플레이 콘솔 설정 -&gt; 라이선스 테스트에 테스터의 Google 이메일 계정을 등록한다.
![](https://velog.velcdn.com/images/im_ssu/post/aae648c9-1fab-4d5b-bd1b-a3241a11a473/image.png)
&amp;#x2001;
#### 내부 테스트 설정
- 내부 테스트로 앱을 게시한 경우에는 등록된 앱의 테스트 -&gt; 내부 테스트 -&gt; 테스터 -&gt; 이메일 목록 만들기를 클릭하여 테스터의 Google 이메일 계정을 추가한다.
![](https://velog.velcdn.com/images/im_ssu/post/ce36b179-083e-46cc-8a9e-1510c38d995c/image.png)
&amp;#x2001;
## 구매(결제) 검증
- 구매(결제) 검증은 민감한 데이터 로직이므로 서버 사이드에서 처리해야한다.
사용자가 구매 완료시 반환 받는 purchaseToken을 서버로 전송하여 구매(결제) 검증을 해야하는데,
Google Developer API 의 **Purchases.products.get**(일회성 구매 상품 일 경우) 또는 **Purchases.subscriptions:get**(정기 구독 상품 일 경우) 를 사용하여 구매(결제) 검증을 진행한다. 
더 자세한 내용은 아래 링크를 참고하자.

   - [자격을 부여하기 전에 구매 확인](https://developer.android.com/google/play/billing/security?hl=ko#verify)
   - [Google Developer API](https://developers.google.com/android-publisher/getting_started?hl=ko)
       - [Purchases.products.get](https://developers.google.com/android-publisher/api-ref/rest/v3/purchases.products?hl=ko)
    - [Purchases.subscriptions:get](https://developers.google.com/android-publisher/api-ref/rest/v3/purchases.subscriptions?hl=ko)

&amp;#x2001;
## 환불처리
- 아래에서 짧게 설명할 실시간 개발자 알림은 일회성 구매 상품에 대한 환불을 처리하기에는 부적절하므로 일회성 구매 상품의 환불은 Google Developer API 의 **Voided Purchases** 를 사용하여 처리해야한다. 해당 API를 사용해 무효화된 구매에 대해 확인하고, 무효화된 구매와 관련된 제품(또는 콘텐츠)에 엑세스 하지 못하도록 하는 시스템을 구현해야한다. 
더 자세한 내용은 아래 링크를 참고하자.

   - [무효화된 구매 감지 및 처리
](https://developer.android.com/google/play/billing/security?hl=ko#voided)
   - [Voided Purchases API](https://developers.google.com/android-publisher/voided-purchases)


&amp;#x2001;
## 실시간 개발자 알림(RTDN) 이란?
- **실시간 개발자 알림(RTDN)** 은 앱 내에서 사용자의 사용 권한이 변경될 때마다 Google의 알림을 수신하는 메커니즘으로 Google Cloud의 Pub/Sub을 활용한다.
다만 해당 알림으로는 정기 구독에 대한 알림과 일회성 구매 중에서도 지연된 결제건에 대한 알림만 받을 수 있어 이번 내용에서는 다루지 않았다. 더 자세한 내용은 아래 링크를 참고하자.

   - [실시간 개발자 알림 구성 준비하기](https://developer.android.com/google/play/billing/getting-ready?hl=ko#configure-rtdn)
   - [실시간 개발자 알림 참조 가이드](https://developer.android.com/google/play/billing/rtdn-reference)
   - [Pub/Sub](https://cloud.google.com/pubsub/docs/overview)</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[Android Studio Bumblebee에서 Lombok Plugin 설치하기]]></title>
            <link>https://velog.io/@im_ssu/Android-Studio-Bumblebee%EC%97%90%EC%84%9C-Lombok-Plugin-%EC%84%A4%EC%B9%98%EA%B0%80-%EC%95%88%EB%90%A0%EB%95%8C</link>
            <guid>https://velog.io/@im_ssu/Android-Studio-Bumblebee%EC%97%90%EC%84%9C-Lombok-Plugin-%EC%84%A4%EC%B9%98%EA%B0%80-%EC%95%88%EB%90%A0%EB%95%8C</guid>
            <pubDate>Tue, 05 Apr 2022 04:43:56 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/cloudflare/im_ssu/9f29adf6-a1e8-452b-a6dc-b9b11f6d01d8/AVvXsEhQ7R2ySipHb8y5jNJeiIj3pE8dZfWAV7EF0wQZ4rQ65lB4MsZroAT4R_7rSfznMZ30xBMLx9_dwnt05V6I0Du0EfI7mvLicK6LwdkuZsF_Gc3sPqrZGxkojTJpHCXFI3Kvr3bLyoSjElldtt1NUpGSBzHgG3O1pvS9BR02L9R2_FYTUgPLfUoNLWYQ%20(1).png" alt=""></p>
<p>회사 컴퓨터를 윈도우에서 맥으로 옮기면서 자연스럽게 안드로이드 스튜디오를 업데이트 하게 되었다.</p>
<p>작업 중이던 프로젝트를 새로운 안드로이드 스튜디오에서 빌드했더니 역시나 많은 오류들이 발생하였다.. 하지만 난 머찐 개발자니까 손쉽게(아님) 해결하였다..! 라고 생각했는데.....!</p>
<p>회사 프로젝트는 JAVA 로 개발이 되어있고, Model 클래스에 Getter/Setter.. 를 사용하기 위해 Lombok을 사용하고 있었는데, 안드로이드 스튜디오 범블비에서는 Lombok Plugin이 아예 검색 조차 되지 않았다..!
물론 빌드는 정상적으로 되었지만 앞으로 계속 이(<del>거지같은</del>) JAVA 코드로 작업을 해야하므로 난 Lombok이 꼭 필요했다. </p>
<p>구구절절,,,</p>
<p>암튼간에 그래서 <strong>안드로이드 스튜디오 범블비에서 Lombok Plugin 설치하기</strong>를 간단히 설명해볼까 한다.
(<a href="https://github.com/mplushnikov/lombok-intellij-plugin/issues/1028#issuecomment-937485680">Lombok Plugin Github Issues</a> 댓글에 있는 내용을 조금 쉽게 바꾸어 봄!)</p>
<p>&#x2001;</p>
<ol>
<li><p>아래 링크에서 Lombok Plugin 을 다운받는다.
<a href="https://plugins.jetbrains.com/plugin/6317-lombok/versions/stable/108259">https://plugins.jetbrains.com/plugin/6317-lombok/versions/stable/108259</a>
<img src="https://velog.velcdn.com/cloudflare/im_ssu/7d8fed30-7067-4a6d-94a8-95201ca0169d/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202022-04-05%20%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE%201.10.51.png" alt=""></p>
</li>
<li><p>다운받은 파일의 압축을 푼다.</p>
</li>
<li><p>lomok-plugin -&gt; lib -&gt; lombok-plugin-0.34.1-2019.1.jar 파일의 META-INF 폴더의 plugin.xml의 내용을 아래와 같이 변경한다.</p>
<pre><code class="language-xml">//변경 전
&lt;idea-version since-build=&quot;191.6183&quot; until-build=&quot;191.*&quot;/&gt;
</code></pre>
</li>
</ol>
<p>//변경 후
<idea-version since-build="191.6183" until-build="IE-212.*"/></p>
<pre><code>4. 하지만 이 과정이 조금 번거로우므로 변경된 META-INF/plugin.xml 을 [이곳에서 다운](https://github.com/mplushnikov/lombok-intellij-plugin/files/7299286/META-INF.zip)받는다.

5. 4에서 다운받은 파일의 압축을 푼다.

6. 터미널에서 아래와 같이 입력하여 lombok-plugin-0.34.1-2019.1.jar파일의 plugin.xml을 업데이트 해준다.</code></pre><p>$ jar uf {경로}/lombok-plugin-0.34.1-2019.1.jar {경로}/META-INF/plugin.xml</p>
<p>```
7. lombok-plugin 폴더를 <strong>Android Studio 폴더 내 Plugin 폴더</strong>에 옮겨준다.
(맥 기준으로 <em>/Users/{사용자이름}/Library/Application Support/Google</em> 폴더 내에 존재함)</p>
<ol start="8">
<li>안드로이드 스튜디오를 실행(실행되어 있다면 재실행)한다. 그럼 이제 Lombok을 정상적으로 사용할 수 있다!
&#x2001;
&#x2001;</li>
</ol>
<p>모두 안드로이드 스튜디오 범블비에서 Lombok을 정상적으로 쓸 수 있길 바란다!</p>
<p><del><em>이 문제를 해결하면서 느낀 점.. kotlin 쓰고 싶다.. data class 쓰면 이런 문제는 만나지도 않았을 텐데...</em></del></p>
<p><em>* 2022.09.06 추가
범블비 이상 버전에서 잘 안된다면 아래 링크를 확인해보세욧
<a href="https://github.com/mplushnikov/lombok-intellij-plugin/issues/1111">https://github.com/mplushnikov/lombok-intellij-plugin/issues/1111</a></em></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Kotlin을 알아보자]]></title>
            <link>https://velog.io/@im_ssu/kotlinbasics</link>
            <guid>https://velog.io/@im_ssu/kotlinbasics</guid>
            <pubDate>Wed, 30 Mar 2022 13:44:28 GMT</pubDate>
            <description><![CDATA[<h2 id="kotlin이란">kotlin이란?</h2>
<p>2011년 IntlliJ IDEA 개발사인 JetBrains에서 공개한 프로그래밍 언어.
코틀린 공식 문서에서는 아래와 같이 코틀린을 소개하고 있다!</p>
<blockquote>
<p><strong>A modern programming language
that makes developers happier.</strong>
(개발자를 행복하게 하는 현대적인 프로그래밍 언어 .....<em><del>땡큐파파고</del></em>)</p>
</blockquote>
<p>일반적인 코드 오류를 줄이고 기존 앱에 쉽게 통합되는, <strong>간결하면서도 안전하고 표현력이 높은 프로그래밍 언어</strong>로 2017년 구글 <strong>안드로이드의 공식 언어</strong>로 채택되었다.
<em>(안드로이드 뿐 아니라 다양한 서버 프레임워크(Spring, Vert.x, Ktor..)와 JavaScript등의 개발에서도 사용 가능하다)</em></p>
<h2 id="kotlin-훑어보기">Kotlin 훑어보기</h2>
<h3 id="-변수-선언">* 변수 선언</h3>
<ul>
<li><strong>val</strong> : 값이 변경 되지 않는 변수 선언 시 사용</li>
<li><strong>var</strong> : 값이 변경되는 변수 선언 시 사용</li>
<li><strong>타입 추론</strong> : 변수 타입을 명시 하지 않아도 할당된 값을 통해 타입 추론이 가능하다.</li>
<li><strong>lateinit</strong> : 원하는 시점에 초기화</li>
<li><strong>lazy</strong> : 변수를 미리 선언하고 사용 시점에 값을 할당<pre><code class="language-kotlin">val name: String = &quot;호랑이&quot;    // 값이 변경 되지 않는 변수는 val
var numberA: Int = 10        // 값이 변경 되는 변수는 var
var numberB = 1              // 타입 추론
</code></pre>
</li>
</ul>
<p>lateinit var lateinitNumber: Int
//추후 초기화
lateinitNumber = 10</p>
<p>val lazyString: String by lazy {
    &quot;호랑이&quot;
}
//사용 시에 값이 할당됨
lazyString.contains(&quot;호랑&quot;)</p>
<pre><code>
- java👇
```java
final String name = &quot;호랑이&quot;;
int numberA = 10;
int numberB = 1;</code></pre><h3 id="-type">* Type</h3>
<p>kotlin 기본 타입은 아래와 같다.</p>
<ul>
<li>정수형 : Byte, Short, Int, Long</li>
<li>실수형 : Float, Double,</li>
<li>그외 : Char, String, Boolean
&#x2001;&#x2001;<h3 id="-null-safety">* Null Safety</h3>
kotlin 변수는 기본적으로 null 값을 가질 수 없으며, null 값을 포함하는 변수는 nullable 로 타입 뒤에 <strong>?</strong> 를 붙여 지정한다.<pre><code class="language-kotlin">var nameA: String = null     // 잘못된 코드!!!
var nameB: String? = null    // 타입의 접미사로 ?를 붙이면 nullable 완성~
</code></pre>
</li>
</ul>
<p>//null 체크하기
nameB.contains(&quot;가&quot;)            // 잘못된 코드!!!
nameB?.contains(&quot;가&quot;)        // nameB가 null일 경우 해당 코드는 실행되지 않음</p>
<pre><code>- java👇
```java
String nameA = null;            // 기본적으로 null 가능

nameA.contains(&quot;가&quot;);            // NullPointException 에러가 발생 할 수 있음

//null 체크하기
if(nameA != null) {
    nameA.contains(&quot;가&quot;);
}</code></pre><h3 id="-조건부">* 조건부</h3>
<p>if-else 사용법은 java와 유사하지만 java와 다르게 아래와 같이(<strong>Expression. 표현식</strong>) 사용 가능하다.</p>
<pre><code class="language-kotlin">val answer = if(numberA &lt; 5) {
    &quot;numberA는 5보다 작다.&quot;
}else if( numberA == 5 ) {
    &quot;numberA는 5이다.&quot;
}else {
    &quot;numberA는 5보다 크다.&quot;
}</code></pre>
<p>switch문과 비슷한 when이 존재한다. </p>
<pre><code class="language-kotlin">//Expression
val answer = when() {
    numberA &lt; 5 -&gt; &quot;numberA는 5보다 작다.&quot;
    numberA == 5 -&gt; &quot;numberA는 5이다.&quot;
    else -&gt; &quot;numberA는 5보다 크다.&quot;
}

when(numberA) {
    in 0..5 -&gt; &quot;numberA는 0~5이다&quot;
    else -&gt; &quot;numberA는 5보다 크다.&quot;
}</code></pre>
<ul>
<li>java👇<pre><code class="language-java">String answer = &quot;&quot;;
</code></pre>
</li>
</ul>
<p>if(numberA &lt; 5) {
    answer = &quot;numberA는 5보다 작다.&quot;;
}else if( numberA == 5 ) {
    answer = &quot;numberA는 5이다.&quot;;
}else {
    answer = &quot;numberA는 5보다 크다.&quot;;
}</p>
<p>switch(numberA) {
    case 0:
    case 1:
    case 2:
    case 3:
    case 4:
    case 5:
        answer = &quot;numberA는 0~5이다&quot;;
        break;
    default:
        answer = &quot;numberA는 5보다 크다&quot;;
        break;
}</p>
<pre><code>### * For문
java와 형태가 조금 다르다.
``` kotlin
//1 ~ 5 까지
for (i in 1..5) {
    print(&quot;$i &quot;)
}
//1 2 3 4 5 

//6 ~ 0 까지 +=2
for (i in 6 downTo 0 step 2) {
    print(&quot;$i &quot;)
}
//6 4 2 0

//1 ~ 5 까지 +=3
for (i in 1..5 step 3) {
    print(&quot;$i &quot;)
}
//1 4</code></pre><ul>
<li>java👇<pre><code class="language-java">for (int i = 1; i &lt;= 5; i++) {
  System.out.print(i + &quot; &quot;);
}
</code></pre>
</li>
</ul>
<p>for (int i = 6; i &gt;= 0; i-=2) {
    System.out.print(i + &quot; &quot;);
}</p>
<p>for (int i = 1; i &lt;= 5; i+=3) {
    System.out.print(i + &quot; &quot;);
}</p>
<pre><code>### * 함수
kotlin에서는 함수를 다음과 같이 작성한다.
```kotlin
//인수에 기본 값 지정이 가능하다.
//generateAnswer(6) or generateAnswer() 모두 호출 가능!
fun generateAnswer(number: Int = 1): String {
    val answer = when {
        number &lt; 5 -&gt; {
            &quot;number는 5보다 작다.&quot;
        }
        number == 5 -&gt; {
            &quot;number는 5이다.&quot;
        }
        else -&gt; {
            &quot;number는 5보다 크다.&quot;
        }
    }

    return answer
}

//단순화 1
fun generateAnswer(number: Int = 1): String {
    return when {
        number &lt; 5 -&gt; {
            &quot;number는 5보다 작다.&quot;
        }
        number == 5 -&gt; {
            &quot;number는 5이다.&quot;
        }
        else -&gt; {
            &quot;number는 5보다 크다.&quot;
        }
    }
}

//단순화 2
fun generateAnswer(number: Int = 1): String = when {
    number &lt; 5 -&gt; {
        &quot;number는 5보다 작다.&quot;
    }
    number == 5 -&gt; {
        &quot;number는 5이다.&quot;
    }
    else -&gt; {
        &quot;number는 5보다 크다.&quot;
    }
}</code></pre><h3 id="-익명함수">* 익명함수</h3>
<ul>
<li>말 그대로 이름이 없는 함수!<pre><code class="language-kotlin">//stringLengthFunc에 할당 되는 값이 익명함수. String - 매개변수, Int - 반환값
val stringLengthFunc: (String) -&gt; Int = { input -&gt;
  input.length
}
</code></pre>
</li>
</ul>
<p>val stringLength: Int = stringLengthFunc(&quot;Android&quot;)
//stringLength 값은 7</p>
<pre><code>
### * 고차함수
함수를 인자로 사용 할 수 있는데, 이렇게 다른 함수를 인자로 사용하는 함수를 고차 함수라고 한다. 콜백 인터페이스처럼 사용하면 아주 유용하다!
```kotlin
//사실 아주 좋은 예시는 아님😅
fun sum(a: Int, b: Int, sumCallback: (Int) -&gt; Unit ) {
    sumCallback(a+b)
}

//아래와 같이 사용
sum(1, 2, { sum -&gt;
    //action
})</code></pre><p>&#x2001;</p>
<h4 id="🤓-갑자기-다음-시간에-계속">🤓 (갑자기?) 다음 시간에 계속...</h4>
<p>&#x2001;
&#x2001;
** 참고 : <a href="https://kotlinlang.org/">Kotlin 공식 문서</a>, <a href="https://developer.android.com/kotlin/learn">Google Developer - Kotlin</a>, 패스트 캠퍼스 강의</p>
]]></description>
        </item>
    </channel>
</rss>