<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>제이의 기억 저장소</title>
    <link>https://traeper.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Sat, 11 Jul 2026 15:27:55 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>jay.pg</managingEditor>
    <item>
      <title>OCP를 만족하는 Layered Architecture 개발하기</title>
      <link>https://traeper.tistory.com/226</link>
      <description>&lt;h1 style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot;&gt;Layered (계층형) 아키텍처&lt;/h1&gt;
&lt;p style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Layered 아키텍처는 웹서비스 개발 시 가장 널리 쓰이며 역할에 따라 여러 레이어로 구성되는 특징이 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Presentation 레이어 : 클라이언트에 상호작용을 위한 View를 제공하고 API 요청을 처리한다.&lt;/li&gt;
&lt;li&gt;Application 레이어 : Persistence, 기타 서비스 등에 의존하고 비즈니스의 흐름을 제어한다.&lt;/li&gt;
&lt;li&gt;Persistence 레이어 : DB에 접근하여 영구적으로 데이터를 쓰고 읽는 각종 인터페이스를 제공한다.&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Layered 아키텍처는 의존성의 방향이 위에서 아래로의 한 방향으로만 흘러가므로 구현 난이도가 쉬운 것이 장점이지만 객체지향적인 측면에서 조금 부족할 수 있다. 글을 진행하기에 앞서 용어가 혼동될 수 있는데 Application == api, Persistence == domain-jpa 라고 이해하면서 읽어주시면 충분할듯 하다.  &amp;zwj; &amp;zwj;️&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;비침투성 개발을 지향하는 Spring Framework 관련 이야기라기 보단 Layered Architecture에서 Application Layer와 Persistence Layer 간 관계에 한정하여 글을 작성했음을 양해 부탁드린다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;a id=&quot;user-content-layered-아키텍처&quot; style=&quot;color: #000000;&quot; href=&quot;https://github.com/traeper/msa_architecture_study#layered-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;Layered 아키텍처&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;125&quot; data-origin-height=&quot;260&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/t4Ftm/btr8XxWdRry/nAcYtJCNgKyyMxKTXVBBx1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/t4Ftm/btr8XxWdRry/nAcYtJCNgKyyMxKTXVBBx1/img.png&quot; data-alt=&quot;OCP를 만족시키지 못하는 Layered 아키텍처&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/t4Ftm/btr8XxWdRry/nAcYtJCNgKyyMxKTXVBBx1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Ft4Ftm%2Fbtr8XxWdRry%2FnAcYtJCNgKyyMxKTXVBBx1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;125&quot; height=&quot;260&quot; data-origin-width=&quot;125&quot; data-origin-height=&quot;260&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;OCP를 만족시키지 못하는 Layered 아키텍처&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;위 아키텍처 그림을 보면 Application 레이어는 Persistence 레이어에 의존한다. 즉 Application은 Persistence의 구체적인 구현에 의존한다. 물론 Persistence 레이어에서 인터페이스를 정의하고 그 인터페이스에 의존시켜 DIP를 만족시킬 수는 있지만 Persistence의 인터페이스가 변경될 때마다 Application 레이어도 변경될 수 있어 OCP를 만족시키지 못한다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;준비해둔 코드 샘플을 보자.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;a id=&quot;user-content-모듈-구성&quot; style=&quot;color: #000000;&quot; href=&quot;https://github.com/traeper/msa_architecture_study#%EB%AA%A8%EB%93%88-%EA%B5%AC%EC%84%B1&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;모듈 구성&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://github.com/traeper/msa_architecture_study/blob/main/layered-api%2Fsrc%2Fmain%2Fkotlin%2Fcom%2Ftraeper%2Fmsa%2Flayered%2Fapi&quot;&gt;layered-api&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;: Presentation, Application 레이어를 갖는 모듈&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://github.com/traeper/msa_architecture_study/blob/main/layered-domain-jpa%2Fsrc%2Fmain%2Fkotlin%2Fcom%2Ftraeper%2Fmsa%2Flayered%2Fdomain%2Fjpa&quot;&gt;layered-domain-jpa&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;: Persistence 레이어 모듈이며 JPA의 구체적인 기술에 의존하는 모듈&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;a id=&quot;user-content-dip를-만족하지만-ocp는-만족하지-않는-예시&quot; style=&quot;color: #000000;&quot; href=&quot;https://github.com/traeper/msa_architecture_study#dip%EB%A5%BC-%EB%A7%8C%EC%A1%B1%ED%95%98%EC%A7%80%EB%A7%8C-ocp%EB%8A%94-%EB%A7%8C%EC%A1%B1%ED%95%98%EC%A7%80-%EC%95%8A%EB%8A%94-%EC%98%88%EC%8B%9C&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;DIP를 만족하지만 OCP는 만족하지 않는 예시&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;layered-api의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://github.com/traeper/msa_architecture_study/blob/main/layered-api%2Fsrc%2Fmain%2Fkotlin%2Fcom%2Ftraeper%2Fmsa%2Flayered%2Fapi%2Fapplication%2FCarMarketService.kt&quot;&gt;CarMarketService 코드&lt;/a&gt;를 보자.&lt;/p&gt;
&lt;div style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot;&gt;
&lt;pre class=&quot;kotlin&quot; style=&quot;background-color: #000000; color: #000000;&quot;&gt;&lt;code&gt;package com.traeper.msa.layered.api.application

/* .. */
import com.traeper.msa.layered.domain.jpa.car.port.CarDomainService

@Service
class CarMarketService(
    private val carDomainService: CarDomainService,
) {
    fun getCar(carId: Long): CarResponse =
        carDomainService.getCar(carId)
            .toCarResponse()
}

private fun Car.toCarResponse(): CarResponse =
    CarResponse(this.corpName, this.modelName)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;DB에 접근하는 부분을&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://github.com/traeper/msa_architecture_study/blob/main/layered-domain-jpa%2Fsrc%2Fmain%2Fkotlin%2Fcom%2Ftraeper%2Fmsa%2Flayered%2Fdomain%2Fjpa%2Fcar%2Fport%2FCarDomainService.kt&quot;&gt;CarDomainService 인터페이스&lt;/a&gt;에 의존하므로 JPA와 같은 구체적인 기술에 의존하지 않는 것을 볼 수 있다. 그렇지만 import 문을 보면 알 수 있듯 jpa라는 단어가 조금 거슬린다. 기술에 대한 의존성도 함께 가지고 있는데 과연 OCP를 만족시킬 수 있을까?&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot;&gt;
&lt;pre class=&quot;kotlin&quot; style=&quot;background-color: #000000; color: #000000;&quot;&gt;&lt;code&gt;// CarDomainService 인터페이스
package com.traeper.msa.layered.api.domain.port.car

interface CarDomainService {
    fun getCar(carId: Long): Car
}

// CarDomainServiceImpl 구현체
package com.traeper.msa.layered.domain.jpa.car

@Component
class CarDomainServiceImpl(
    val repository: CarMarketRepository,
) : CarDomainService {
    override fun getCar(carId: Long): Car =
        repository.findByIdOrNull(carId)!!.toCar()
}

private fun CarEntity.toCar(): Car =
    Car(
        corpName = this.corpName,
        modelName = this.modelName,
    )&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;모듈 단위에서 보면 여전히 api는 domain-jpa가 변경될 때마다 직접적인 영향을 받는다. domain 모듈에서 인터페이스를 제공해서 DIP를 만족시키더라도 결국 상위 레이어인 api 모듈 입장에선 저수준의 인터페이스에 종속되는 것이므로 OCP에 위배된다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;a id=&quot;user-content-ocp를-만족시키지-못하는-구체적인-이유&quot; style=&quot;color: #000000;&quot; href=&quot;https://github.com/traeper/msa_architecture_study#ocp%EB%A5%BC-%EB%A7%8C%EC%A1%B1%EC%8B%9C%ED%82%A4%EC%A7%80-%EB%AA%BB%ED%95%98%EB%8A%94-%EA%B5%AC%EC%B2%B4%EC%A0%81%EC%9D%B8-%EC%9D%B4%EC%9C%A0&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;OCP를 만족시키지 못하는 구체적인 이유&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JPA의 특정 스펙이 변경되어 인터페이스가 변경되는 경우, 상위 api 모듈의 코드도 변경되어야 한다.&lt;/li&gt;
&lt;li&gt;JPA가 아닌 다른 기술을 이용한 모듈을 새로 개발해야 하는 경우, 상위 api 모듈에서도 새로운 인터페이스를 참조하도록 변경해야 하며 Persistence 레이어 간 인터페이스가 중복으로 존재하게 되는 단점이 추가로 생긴다.&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이런 문제들이 생겨나는 이유를 쉽게 설명해보자면 갑(api)과 을(domain-jpa)이 있다고 칠 때, 을의 변경에 갑이 영향을 받는 이상한 모양이 되기 때문이다. 을이 변하더라도 갑은 변하지 않는 즉 수정에 닫히는 방식으로 개발을 하는게 좋겠다. OCP를 만족시키려면 어떻게 해야할까?&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-9730478914470663&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;a id=&quot;user-content-ocp를-만족하는-layered-아키텍처&quot; style=&quot;color: #000000;&quot; href=&quot;https://github.com/traeper/msa_architecture_study#ocp%EB%A5%BC-%EB%A7%8C%EC%A1%B1%ED%95%98%EB%8A%94-layered-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;OCP를 만족하는 Layered 아키텍처&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;OCP의 핵심은 상위 모듈이 하위 모듈의 변경에 대해 자유로워야 한다는 것인데 구현 방법은 생각보다 간단하다. 하위 모듈의 인터페이스를 상위 모듈의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;요구사항 명세&lt;/b&gt;를 담는 별도의 인터페이스 전용 모듈로 추출하면 되는 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;264&quot; data-origin-height=&quot;431&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b99b57/btr8Nceh18c/UbH3cfNeA82S3FB5PCsNGk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b99b57/btr8Nceh18c/UbH3cfNeA82S3FB5PCsNGk/img.png&quot; data-alt=&quot;OCP를 만족하는 Layered 아키텍처&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b99b57/btr8Nceh18c/UbH3cfNeA82S3FB5PCsNGk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb99b57%2Fbtr8Nceh18c%2FUbH3cfNeA82S3FB5PCsNGk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;264&quot; height=&quot;431&quot; data-origin-width=&quot;264&quot; data-origin-height=&quot;431&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;OCP를 만족하는 Layered 아키텍처&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;위는 OCP를 충족시키는 아키텍처이다. 기존 layered 아키텍처와 다르게 Persistence Port 인터페이스 모듈을 Application 레이어에 추가하였다. Application은 Persistence Port 모듈에 자신의 요구사항을 명시하고 의존한다. Persistence는 해당 인터페이스를 구현하며 실행시간에 빈을 제공하도록 한다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;a id=&quot;user-content-모듈-구성-1&quot; style=&quot;color: #000000;&quot; href=&quot;https://github.com/traeper/msa_architecture_study#%EB%AA%A8%EB%93%88-%EA%B5%AC%EC%84%B1-1&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;모듈 구성&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://github.com/traeper/msa_architecture_study/blob/main/ocp-layered-api%2Fsrc%2Fmain%2Fkotlin%2Fcom%2Ftraeper%2Fmsa%2Flayered%2Fapi&quot;&gt;ocp-layered-api&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;: Presentation, Application 레이어를 갖는 모듈, 기존 layered 구조와 거의 차이가 없다.&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://github.com/traeper/msa_architecture_study/blob/main/ocp-layered-api-domain-port%2Fsrc%2Fmain%2Fkotlin%2Fcom%2Ftraeper%2Fmsa%2Flayered%2Fapi%2Fdomain%2Fport&quot;&gt;ocp-layered-api-domain-port&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;: Persistence Port 모듈이며 Application이 Persistence 레이어에 원하는 요구사항을 명세한 인터페이스 모듈&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://github.com/traeper/msa_architecture_study/blob/main/ocp-layered-domain-jpa%2Fsrc%2Fmain%2Fkotlin%2Fcom%2Ftraeper%2Fmsa%2Flayered%2Fdomain%2Fjpa&quot;&gt;ocp-layered-domain-jpa&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;: Persistence 레이어 모듈이며 Persistence Port의 인터페이스를 따르는 JPA의 구현체를 작성하는 모듈&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;모듈 구성을 보면 ocp-layered-api-domain-port이 추가된 것을 볼 수 있다. Application이 Persistence 레이어에 원하는 요구사항을 명세한 인터페이스 모듈이라고 이해하면 된다. 갑(api)의 요구사항을 명시하고 을(domain-jpa)이 요구사항을 충실히 구현하는 방향으로 이해하자.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;a id=&quot;user-content-dip-뿐만-아니라-ocp를-만족하는-layered-아키텍처&quot; style=&quot;color: #000000;&quot; href=&quot;https://github.com/traeper/msa_architecture_study#dip-%EB%BF%90%EB%A7%8C-%EC%95%84%EB%8B%88%EB%9D%BC-ocp%EB%A5%BC-%EB%A7%8C%EC%A1%B1%ED%95%98%EB%8A%94-layered-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;DIP 뿐만 아니라 OCP를 만족하는 Layered 아키텍처&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;ocp-layered-api의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://github.com/traeper/msa_architecture_study/blob/main/ocp-layered-api%2Fsrc%2Fmain%2Fkotlin%2Fcom%2Ftraeper%2Fmsa%2Flayered%2Fapi%2Fapplication%2FCarMarketService.kt&quot;&gt;CarMarketService 코드&lt;/a&gt;를 보자.&lt;/p&gt;
&lt;div style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot;&gt;
&lt;pre class=&quot;kotlin&quot; style=&quot;background-color: #000000; color: #000000;&quot;&gt;&lt;code&gt;package com.traeper.msa.layered.api.application

/* .. */
import com.traeper.msa.layered.api.domain.port.car.CarDomainService

@Service
class CarMarketService(
    private val carDomainService: CarDomainService,
) {
    fun getCar(carId: Long): CarResponse =
        carDomainService.getCar(carId).toCarResponse()
}

private fun Car.toCarResponse(): CarResponse =
    CarResponse(this.corpName, this.modelName)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;별도의 모듈로 추출한&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://github.com/traeper/msa_architecture_study/blob/main/layered-domain-jpa%2Fsrc%2Fmain%2Fkotlin%2Fcom%2Ftraeper%2Fmsa%2Flayered%2Fdomain%2Fjpa%2Fcar%2Fport%2FCarDomainService.kt&quot;&gt;CarDomainService 인터페이스&lt;/a&gt;에 의존하는 것을 볼 수 있다. 여기까지 봐서는 OCP를 만족시키지 못하는 예제와 다를 것이 거의 없어보이지만 이미 차이는 생겼다. import를 보면 jpa가 더는 보이지 않으므로 구체적인 기술에 의존하지 않는 방향으로 추상화가 되었음을 알 수 있다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot;&gt;
&lt;pre class=&quot;kotlin&quot; style=&quot;background-color: #000000; color: #000000;&quot;&gt;&lt;code&gt;// ocp-layered-api-domain-port의 CarDomainService 인터페이스
package com.traeper.msa.layered.api.domain.port.car

interface CarDomainService {
    fun getCar(carId: Long): Car
}

// ocp-layered-domain-jpa의 CarDomainServiceImpl 구현체
package com.traeper.msa.layered.domain.jpa.car

@Component
class CarDomainServiceImpl(
    val repository: CarMarketRepository,
) : CarDomainService {
    override fun getCar(carId: Long): Car =
        repository.findByIdOrNull(carId)!!.toCar()
}

private fun CarEntity.toCar(): Car =
    Car(
        corpName = this.corpName,
        modelName = this.modelName,
    )&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;CarDomainService와 CarDomainServiceImpl이 서로 다른 모듈에 위치하는 것을 볼 수 있다. 이젠 CarDomainService에 대해 JPA가 아닌 다른 구현체를 추가하더라도 독립적인 인터페이스 전용 모듈에 의존하고 있으므로 api 모듈에서 수정이 발생할 확률이 줄었다고 볼 수 있다. api 모듈은 컴파일 시간에는 인터페이스에 의존하고 실행 시간에는 구현체의 빈에 의존하여 작동하게 된다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;물론 이렇게만 작성한다고 자동으로 OCP가 100% 충족이 되는 것은 아니다. 인터페이스가 특정 기술이나 구현체에 종속적으로 변하지 않도록 갑(api)의 입장에서 인터페이스를 바라보고 오염되지 않도록 경계하는 습관을 가져야 한다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;a id=&quot;user-content-요약&quot; style=&quot;color: #000000;&quot; href=&quot;https://github.com/traeper/msa_architecture_study#%EC%9A%94%EC%95%BD&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;요약&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JPA, Redis, 기타 시스템 등 구체적인 기술이나 스펙에 구체적으로 의존하는 모듈이 있다면 인터페이스는 외부에 전용 모듈로 추출하고 기술보단 서비스의 요구사항을 담도록 노력하자.&lt;/li&gt;
&lt;li&gt;위 말이 어렵다면 인터페이스는 갑(api)의 입장에서 작성하고 을(domain-jpa)은 철저히 따르도록 구현하는 것을 기억하자.&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #444444; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;보충 의견&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JPA Entity를 Persistence 레이어에서 DTO로 변환시켜 리턴하고 있는데 이는 트랜잭션 관리, 코드 양의 증가 등 현실에서는 다소 오버스펙의 구현이 될 수 있음을 참고해주시기 바란다. 반면 Server To Server 통신을 하는 경우에는 적용할만한 예시일듯 하다.&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #444444; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;링크&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #444444; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;깃헙 프로젝트 :&lt;span&gt; &lt;a href=&quot;https://github.com/traeper/msa_architecture_study&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/traeper/msa_architecture_study&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;</description>
      <category>Architecture</category>
      <category>architecture</category>
      <category>DIP</category>
      <category>Interface</category>
      <category>Layered</category>
      <category>MSA</category>
      <category>OCP</category>
      <category>solid</category>
      <category>아키텍처</category>
      <category>인터페이스</category>
      <author>jay.pg</author>
      <guid isPermaLink="true">https://traeper.tistory.com/226</guid>
      <comments>https://traeper.tistory.com/226#entry226comment</comments>
      <pubDate>Mon, 10 Apr 2023 00:37:38 +0900</pubDate>
    </item>
    <item>
      <title>ChatGPT와 개발자에 대해 이야기해보기</title>
      <link>https://traeper.tistory.com/225</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;chat-gpt-logo-360600.webp&quot; data-origin-width=&quot;1600&quot; data-origin-height=&quot;900&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/At1av/btr4ykHr79B/C2zOGmdbP3eBrK00QZySxk/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/At1av/btr4ykHr79B/C2zOGmdbP3eBrK00QZySxk/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/At1av/btr4ykHr79B/C2zOGmdbP3eBrK00QZySxk/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAt1av%2Fbtr4ykHr79B%2FC2zOGmdbP3eBrK00QZySxk%2Fimg.webp&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;617&quot; height=&quot;347&quot; data-filename=&quot;chat-gpt-logo-360600.webp&quot; data-origin-width=&quot;1600&quot; data-origin-height=&quot;900&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;요즘 많은 사람의 입에 ChatGPT가 많이 오르내리고 있는데 도대체 얼마나 대단한지 궁금해졌다. 개발자에 대해 간단히 물어봤는데 생각보다 친절하고 상세하게 답(가르침)을 줘서 감명받아 블로그를 써보게 됐다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot;&gt;ChatGPT, 든든한 코딩 비서의 가능성&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot;&gt;요즘 블로깅을 하면서 공부하고 있는 개념들(Spring Framework, Domain Driven Design)에 대해 ChatGPT에 대해 물어보다가 대답을 너무 잘해주고 심지어 코드 샘플까지 꽤 잘 만들어줘서 깜짝 놀랐다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;물론 AI가 개발자를 당장 대체하긴 어려워 보이며 당분간 코드 샘플 생성, 코드 리뷰 등 프로그래밍을 서포트해 주는 정도로 사용될 것 같다. 아직까진 생체 컴퓨터인 인간이 다른 동료들과 집단지성으로 의사소통하고 통합적인 사고를 통해 제품을 완성시키는 역량이 AI보단 나을 것이기 때문이다.&lt;br /&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #333333;&quot;&gt;ChatGPT에 접속해서 개발자를 AI가 대체할 수 있는지 대충 물어봤는데 막연했던 내 생각보다 훨씬 일목요연하게 정리해 줘서 깜짝 놀랐다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-03-20 오전 12.04.09.png&quot; data-origin-width=&quot;717&quot; data-origin-height=&quot;560&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/YQNti/btr4xKe9awt/p0xwmqbrAkFh85lXxzy9IK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/YQNti/btr4xKe9awt/p0xwmqbrAkFh85lXxzy9IK/img.png&quot; data-alt=&quot;자신(AI)의 용도부터 소프트웨어 개발에 대한 깊은 이해까지.. 당신은 도대체..?!&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YQNti/btr4xKe9awt/p0xwmqbrAkFh85lXxzy9IK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYQNti%2Fbtr4xKe9awt%2Fp0xwmqbrAkFh85lXxzy9IK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;717&quot; height=&quot;560&quot; data-filename=&quot;스크린샷 2023-03-20 오전 12.04.09.png&quot; data-origin-width=&quot;717&quot; data-origin-height=&quot;560&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;자신(AI)의 용도부터 소프트웨어 개발에 대한 깊은 이해까지.. 당신은 도대체..?!&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;어떤 질문을 하더라도 상세하게 알려주는 것 같다. 특히 ChatGPT는 대화의 맥락까지 고려하기 때문에 대화형식으로 질문을 구체화시킬수록 더 정교한 답변을 얻을 수 있다고 한다. 앞으로는 일할 때 잘 모르는 개념이 나오면 ChatGPT에게 물어보면 될 것 같다. 간단한 소스코드 생성도 꽤 잘해서 얼마든지 맡길 수 있을 것 같아 일의 효율을 대폭 올릴 수 있을 것 같다.&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 ChatGPT가 Spring Security Configuration 코드를 작성하는 것을 화면 캡처한 것이다.&lt;/p&gt;

            &lt;figure class=&quot;unsupported component-kakaotv&quot; contenteditable=&quot;false&quot; style=&quot;background:#000;margin:16px 0;min-height:72px;padding:10px 16px;display:flex;align-items:center;justify-content:center;text-align:center;box-sizing:border-box;width:100%;max-width:100%;&quot;&gt;
                &lt;p contenteditable=&quot;false&quot; style=&quot;margin:0;color:#8a8a8a;font-size:13px;line-height:1.6;user-select:none;pointer-events:none;&quot;&gt;동영상 서비스가 종료되어 해당 콘텐츠를 재생할 수 없습니다.&lt;/p&gt;
            &lt;/figure&gt;
        
&lt;p data-ke-size=&quot;size16&quot;&gt;OAuth 2.0 authorization server, 액세스 토큰 발급이 가능한 환경 설정의 Spring Security 코드 생성을 부탁했다. 위 영상은 편집된 거라 무척 빠르지만 실제로 15초 정도 기다렸다가 코드를 생성하기 시작했다. 아마 밤에 트래픽이 몰려서 그런 것 같고 빠를 때는 3~5초만 있어도 바로 코드를 작성하기 시작한다.&lt;br /&gt;&lt;br /&gt;ChatGPT가 똑똑하다고 생각되는 것은 토큰 발급 URI (/oauth/token)에 대해서는 permitAll() 처리를 하고 나머지 요청에는 authenticated()을 걸어주는 등  endpoint까지 구분해서 보안 설정을 하는 부분이었다. formLogin()을 통해 로그인이 성공한 유저에 대해 토큰을 발급하도록 하였는데 영상에서는 잘려서 보이지 않지만 In-Memory tokenStore로 액세스 토큰을 관리하는  부분까지 생성되어 조금만 수정해 주면 실행도 가능한 코드 샘플이었다.&lt;br /&gt;&lt;br /&gt;물론 이 코드를 바로 사용하기는 어렵지만 영상 마지막에서 알 수 있듯이 애플리케이션의 요구사항에 대해 커스터마이징을 하라고  친절하게 설명하며 답변은 끝이 난다. 실무를 할 때는 In-Memory 부분을 JPA 구현체로 생성해달라고 또 부탁하면 그만일 테고 토큰의 타입과 만료 시간 정도만 설정해 주면 더는 할 게 없어 보일 정도로 쓸만했다. ☺️&lt;/p&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-9730478914470663&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;개발자 부업에 대한 고민 (feat. 블로그)&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;개발자로 9년 가까이 일하면서 어느덧 30대 중반이 되었는데 앞으로 얼마나 더 일할 수 있을까? 얼마 전 내 가정을 꾸리게 되면서 현실적인 수입과 개발자의 짧은 수명에 대한 걱정을 가끔 하는 편이다. 당장 돈이 되는 과외, 강의를 고민하지만 체력소모가 큰 부업을 하는 것은 당장 회사 업무를 잘 해내는 것도 벅차서 엄두를 못 내겠더라. 회사 업무에 지장을 주지 않는 선에서 실력을 키우고 추가적인 수입도 해결하는 방법은 블로그가 유일한 것 같아서 블로그에 다시 도전해보고 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;고민되는 것은 ChatGPT에게 물어보자. 개발자가 할 수 있는 부업은?&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-03-20 오전 12.41.35.png&quot; data-origin-width=&quot;723&quot; data-origin-height=&quot;643&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lTskh/btr4zcP3HTF/lZHWegnXxyNnl11XeocFJ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lTskh/btr4zcP3HTF/lZHWegnXxyNnl11XeocFJ1/img.png&quot; data-alt=&quot;개발자가 할 수 있는 부업들.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lTskh/btr4zcP3HTF/lZHWegnXxyNnl11XeocFJ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlTskh%2Fbtr4zcP3HTF%2FlZHWegnXxyNnl11XeocFJ1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;723&quot; height=&quot;643&quot; data-filename=&quot;스크린샷 2023-03-20 오전 12.41.35.png&quot; data-origin-width=&quot;723&quot; data-origin-height=&quot;643&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;개발자가 할 수 있는 부업들.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;1번, 4번은 근로계약 위반 or 체력소모가 큰 방법이라고 생각한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;2번은 프로그래밍 과외나 인터넷 강의인데 과외는 할 수 있지만 일회성에 체력소모가 제법 된다. 인터넷 강의는 아직 내가 할 레벨이 아니라고 생각한다. 5번 컨설팅도 마찬가지로 깜냥과 경험이 부족하다. 아직 수직적인 성장이 필요한 나에겐 3번 블로깅이 적절한 방법이라 생각한다. 블로깅하면 좋은 게 혹여나 틀린 글을 쓰더라도 피드백을 주시는 감사한 분들이 있기 때문에 내 성장에 도움이 되는 방법인 것 같다.&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;br /&gt;자 이제 정말 궁금한.. 블로그 수입은 어떤 구조로 되는 걸까? ChatGPT에게 애드센스의 수입에 대해 물어봤다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-03-20 오전 1.19.17.png&quot; data-origin-width=&quot;719&quot; data-origin-height=&quot;600&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bukcjP/btr4RAvm00X/n0AELHk6Vs9qs9qDuy2al0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bukcjP/btr4RAvm00X/n0AELHk6Vs9qs9qDuy2al0/img.png&quot; data-alt=&quot;대충 질문해도 상세한 답을 주는 우문현답 그 자체  &quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bukcjP/btr4RAvm00X/n0AELHk6Vs9qs9qDuy2al0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbukcjP%2Fbtr4RAvm00X%2Fn0AELHk6Vs9qs9qDuy2al0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;719&quot; height=&quot;600&quot; data-filename=&quot;스크린샷 2023-03-20 오전 1.19.17.png&quot; data-origin-width=&quot;719&quot; data-origin-height=&quot;600&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;대충 질문해도 상세한 답을 주는 우문현답 그 자체  &lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;수입에 영향을 주는 요소를 정리해 보자면 아래와 같다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;블로그의 주제&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;방문자수, 방문자의 지역&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;광고가 노출된 페이지뷰(횟수)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;광고 포맷과 배치된 위치&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;광고 방식은 CPC(cost per click) 방식이며 구글의 엄격한 정책을 잘 따라야 한다고 적혀있다. 구글은 평균적으로 광고 수입의 68%를 공유한다고 하는데 그중 제일 중요한 건 역시 고품질의 콘텐츠로 진솔한 방문 트래픽을 끌어올리는 것..! 참 짧은 질문에 현답을 이리도 주다니 미안하고 고맙다. 참선생 GPT.. 아참. 프로그래밍 토픽에 대해서도 물어봤는데 거의 비슷한 답변이 돌아왔다. 역시 중요한 것은 &lt;b&gt;고품질의 컨텐츠&lt;/b&gt;!&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;개발자가 실력을 키우는 방법&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;개발자가 실력을 키우는 방법에 대해서도 물어봤는데 뻔한 질문에 교과서적인 대답이지만 개발을 잘 모르는 초심자에게는 이 정도의 설명만으로도 많은 도움이 될 것 같다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-03-20 오전 1.33.02.png&quot; data-origin-width=&quot;721&quot; data-origin-height=&quot;801&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CgprU/btr4ykHr7QQ/eCTwRIQGQLjCR2l74nKUI1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CgprU/btr4ykHr7QQ/eCTwRIQGQLjCR2l74nKUI1/img.png&quot; data-alt=&quot;좋은 개발자 되는 방법은 만국공통인가보다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CgprU/btr4ykHr7QQ/eCTwRIQGQLjCR2l74nKUI1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCgprU%2Fbtr4ykHr7QQ%2FeCTwRIQGQLjCR2l74nKUI1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;721&quot; height=&quot;801&quot; data-filename=&quot;스크린샷 2023-03-20 오전 1.33.02.png&quot; data-origin-width=&quot;721&quot; data-origin-height=&quot;801&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;좋은 개발자 되는 방법은 만국공통인가보다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리해 보자면&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;정기적인 코딩 : 일, 주 단위로 주기를 정해서 새로운 도구나 코딩 챌린지등을 하기.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;컨퍼런스, 밋업 참여 : 개발자 네트워크에 참여하여 다른 개발자에게서 배우고 최신 기술 트렌드를 분석한다. 국내에도 많은 개발자 컨퍼런스가 있는데 리프레쉬도 되고 공부도 되고 아주 좋은 것 같다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;온라인 강의, 튜토리얼 수강 : 나 또한 강추한다. Coursera, 인프런 등 다양한 온라인 강의 사이트에서 수강을 했었는데 업계 전문가의 질 좋은 강의를 들을 수 있는 게 정말 큰 장점이다. 가끔 무료이거나 저렴한 강의도 있으니 수시로 찾아보면 도움이 될듯하다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;기술 블로그 글 읽기 : 구글링 하면서 늘 경험하는 것인데 ChatGPT에게 물어봐도 충분할 것 같은데 내 회사에서도 사용할 수 있는 날이 얼른 오면 좋겠다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;오픈소스 프로젝트 참여 : 글로벌하게 오픈된 장소에서 코드 리뷰를 받고 좋은 이미지를 곳곳에 심어놓으면 채용까지도 염두할 수 있는 좋은 방법.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;개인프로젝트 작성 : &lt;a style=&quot;color: #333333;&quot; href=&quot;https://github.com/traeper&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;내 개인 깃헙&lt;/a&gt;에 많이 하는 방법이다. 잘 모르거나 삽질한 경험을 코드로 남겨서 체득하는 데에 도움이 되는 것 같다. 그리고 어느 정도 퀄리티에도 신경을 쓴다면 개인의 포트폴리오로서도 활용가능하며 스타를 받는다면 꽤 쏠쏠한 동기부여가 된다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;마지막으로 꾸준히 공부하는 것을 강조하는데 정말 맞는 말이다. ChatGPT 그는 참선생..&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;ChatGPT는  잠깐 써봤는데도 많은 가르침을 주며 제법 빠른 응답속도까지 사용하기 너무 좋았다. 앞으로 인생을 좀 더 효과적으로 살기 위해 여러 방면으로 애용하게 될 것 같다.&lt;/span&gt;&lt;/p&gt;</description>
      <category>인공지능(AI) 살펴보기</category>
      <category>AI</category>
      <category>chatGPT</category>
      <category>개발자</category>
      <category>부업</category>
      <category>소프트웨어</category>
      <category>인공지능</category>
      <author>jay.pg</author>
      <guid isPermaLink="true">https://traeper.tistory.com/225</guid>
      <comments>https://traeper.tistory.com/225#entry225comment</comments>
      <pubDate>Mon, 20 Mar 2023 01:56:42 +0900</pubDate>
    </item>
    <item>
      <title>Spring Security 기초 공부</title>
      <link>https://traeper.tistory.com/224</link>
      <description>&lt;h1 style=&quot;background-color: #ffffff; color: #24292f; text-align: start;&quot;&gt;Spring Security Basic Practice&lt;/h1&gt;
&lt;p style=&quot;background-color: #ffffff; color: #24292f; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Spring Security는 인증(Authentication) 및 인가(Authorization)를 담당하는 스프링 프레임워크이다. 먼저 ChatGPT에게 Spring Security에 대해 물어봤는데 정확하고 상세하게 대답해줘서 놀랐다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;721&quot; data-origin-height=&quot;431&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bxBAdP/btr4RzDcOxr/tPkypjGXwPJRkIfRNJV0Nk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bxBAdP/btr4RzDcOxr/tPkypjGXwPJRkIfRNJV0Nk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bxBAdP/btr4RzDcOxr/tPkypjGXwPJRkIfRNJV0Nk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbxBAdP%2Fbtr4RzDcOxr%2FtPkypjGXwPJRkIfRNJV0Nk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;721&quot; height=&quot;431&quot; data-origin-width=&quot;721&quot; data-origin-height=&quot;431&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #24292f; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;누군가 Spring Security에 대해 물어본다면 ChatGPT처럼 말하면 아주 좋을 것 같다. (대단;;) 이번 블로깅에서는 Spring Security의 기초 구조에 대해 정리해보려고 한다.&lt;/p&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #24292f; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;a id=&quot;user-content-basic-web-security&quot; style=&quot;color: #000000;&quot; href=&quot;https://github.com/traeper/spring-security-basic-practice#basic-web-security&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;Basic Web Security&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;638&quot; data-origin-height=&quot;450&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ba6wiO/btr3egma4A0/efL53yrdCkux9dXoHjZ680/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ba6wiO/btr3egma4A0/efL53yrdCkux9dXoHjZ680/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ba6wiO/btr3egma4A0/efL53yrdCkux9dXoHjZ680/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fba6wiO%2Fbtr3egma4A0%2FefL53yrdCkux9dXoHjZ680%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;638&quot; height=&quot;450&quot; data-origin-width=&quot;638&quot; data-origin-height=&quot;450&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #24292f; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Servlet 기반으로 구동되는 웹서비스는 인증, 인가, 변환, 압축, 암호화 등 비즈니스 로직이 수행되기 전에 공통으로 사용되는 로직을 Servlet Filter로 구현한다. 위 그림처럼 Servlet이 웹 요청을 처리하기 앞서 여러 Filter가 순서에 맞춰 수행되는 것이 Servlet 서버의 기본 동작 흐름이다.&lt;/p&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #24292f; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;a id=&quot;user-content-spring-security&quot; style=&quot;color: #000000;&quot; href=&quot;https://github.com/traeper/spring-security-basic-practice#spring-security&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;Spring Security&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;702&quot; data-origin-height=&quot;495&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tVEaA/btr3lETZbDO/B68OpKKNGSmHS5MTkpXg0k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tVEaA/btr3lETZbDO/B68OpKKNGSmHS5MTkpXg0k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tVEaA/btr3lETZbDO/B68OpKKNGSmHS5MTkpXg0k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtVEaA%2Fbtr3lETZbDO%2FB68OpKKNGSmHS5MTkpXg0k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;702&quot; height=&quot;495&quot; data-origin-width=&quot;702&quot; data-origin-height=&quot;495&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #24292f; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Spring Security는 FilterChainProxy라는 물리 Filter 한 개를 필터 흐름에 끼워 넣는데, 이 프록시는 내부적으로 FilterChain이라는 Filter 복합체 여러개를 가지며 FilterChain 한 개는 많은 Filter를 가진다.&lt;/p&gt;
&lt;div style=&quot;background-color: #ffffff; color: #24292f; text-align: start;&quot;&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;background-color: #000000; color: #000000;&quot;&gt;&lt;code&gt;public interface SecurityFilterChain {
    boolean matches(HttpServletRequest request);
    List&amp;lt;Filter&amp;gt; getFilters();
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p style=&quot;background-color: #ffffff; color: #24292f; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;FilterChainProxy는 가지고 있는 FilterChain 중 현재 웹 요청(HttpServletRequest)을 지원하는 첫 번째 chain을 선택하여 해당 chain의 Filter들을 수행한다. SecurityFilterChain interface의 matches 메서드를 이용해서다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #24292f; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;개발자는 Spring Security에서 기본적으로 제공하는 FilterChain 리스트 외에 커스텀 FilterChain을 추가하여 서비스의 보안 요구사항을 만족시키는 작업을 해야한다.&lt;/p&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-9730478914470663&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #24292f; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;a id=&quot;user-content-thread-bounded-security-context&quot; style=&quot;color: #000000;&quot; href=&quot;https://github.com/traeper/spring-security-basic-practice#thread-bounded-security-context&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;Thread Bounded Security Context&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #24292f; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Servlet은 웹 요청마다 하나의 thread로 동작하는데, Spring Security에서도 thread 단위로 현재의 인증된 Principal(인증 주체)를 저장한다. 인증된 principal을 Authentication interface로 지칭하며 이 객체는 SecurityContext에 포함된다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #24292f; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;인증된 객체의 정보를 접근하는 방법은 3가지 정도 있다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #24292f; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;1. SecurityContextHolder (흔히 쓰이지는 않음)&lt;/p&gt;
&lt;div style=&quot;background-color: #ffffff; color: #24292f; text-align: start;&quot;&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;background-color: #000000; color: #000000;&quot;&gt;&lt;code&gt;SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();
User = (User) authentication.getPrincipal();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. &lt;span style=&quot;color: #333333; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;@AuthenticationPrincipal&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #24292f; text-align: start;&quot;&gt;
&lt;pre class=&quot;less&quot; style=&quot;background-color: #000000; color: #000000;&quot;&gt;&lt;code&gt;@RequestMapping(&quot;/foo&quot;)
public String foo(@AuthenticationPrincipal User user) {
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. &lt;span style=&quot;color: #333333; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;Principal&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #24292f; text-align: start;&quot;&gt;
&lt;pre class=&quot;crmsh&quot; style=&quot;background-color: #000000; color: #000000;&quot;&gt;&lt;code&gt;@RequestMapping(&quot;/foo&quot;)
public String foo(Principal principal) {
    Authentication authentication = (Authentication) principal;
    User user = (User) authentication.getPrincipal();
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #24292f; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;a id=&quot;user-content-references&quot; style=&quot;color: #000000;&quot; href=&quot;https://github.com/traeper/spring-security-basic-practice#references&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;References&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #24292f; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Spring Security Architecture :&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://spring.io/guides/topicals/spring-security-architecture&quot;&gt;https://spring.io/guides/topicals/spring-security-architecture&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #24292f; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;링크&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://spring.io/guides/topicals/spring-security-architecture&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #444444; text-align: start;&quot;&gt;깃헙 프로젝트 :&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;a href=&quot;https://github.com/traeper/spring-security-basic-practice&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/traeper/spring-security-basic-practice&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Spring/Spring Security</category>
      <category>Filter</category>
      <category>filterChainProxy</category>
      <category>httpServletRequest</category>
      <category>Servlet</category>
      <category>Spring Security</category>
      <author>jay.pg</author>
      <guid isPermaLink="true">https://traeper.tistory.com/224</guid>
      <comments>https://traeper.tistory.com/224#entry224comment</comments>
      <pubDate>Sun, 12 Mar 2023 23:18:48 +0900</pubDate>
    </item>
    <item>
      <title>OAuth 2.0 - Spring Authorization Server 프랙티스</title>
      <link>https://traeper.tistory.com/223</link>
      <description>&lt;h1&gt;Spring Authorization Server Practice&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Authorization Server 프레임워크는 OAuth 2.0 중 Authorization Server 부분을 쉽게 구현할 수 있게 해준다. 우선 OAuth 2.0에 대해 간략히 살펴보자.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;a id=&quot;user-content-oauth-20&quot; href=&quot;https://github.com/traeper/spring-authorization-server-practice#oauth-20&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;OAuth 2.0&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;450&quot; data-origin-height=&quot;253&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yePZP/btr1YJWZIbP/yk9Esm78xpSdRmyNXWbWlk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yePZP/btr1YJWZIbP/yk9Esm78xpSdRmyNXWbWlk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yePZP/btr1YJWZIbP/yk9Esm78xpSdRmyNXWbWlk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyePZP%2Fbtr1YJWZIbP%2Fyk9Esm78xpSdRmyNXWbWlk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;450&quot; height=&quot;253&quot; data-origin-width=&quot;450&quot; data-origin-height=&quot;253&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;rfc6749 문서에 따르면 'OAuth 2.0 인가(인증) 프레임워크'라는 말로 OAuth 2.0을 풀이한다. OAuth 2.0은 서드파티 앱이 특정 서비스의 제한이 걸린 자원에 접근하기 위해 리소스 오너(사용자)의 허락을 받아 자원에 접근을 하게되는 일련의 플로우를 제공한다. 구글, 페이스북 등 유수의 IT 기업들은 자신의 우수한 회원 체계, 여러 코어 서비스를 바탕으로 OAuth 2.0 서비스를 제공하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;rfc문서에서는 프레임워크라고 말하는데 알고리즘, 프로토콜 같은 조그만 범위를 넘어 일종의 체계화된 틀이며 이 틀에 맞춰 충실히 스펙을 구현하면 된다는 말로도 이해할 수 있다. 이어서 말하겠지만 OAuth 2.0의 구조는 단순한 편이 결코 아니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;a id=&quot;user-content-protocol-flow&quot; href=&quot;https://github.com/traeper/spring-authorization-server-practice#protocol-flow&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;Protocol Flow&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;538&quot; data-origin-height=&quot;336&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b9TQoD/btr1Wloz0vf/m08dSOyv2gzwWPKWRMt5FK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b9TQoD/btr1Wloz0vf/m08dSOyv2gzwWPKWRMt5FK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b9TQoD/btr1Wloz0vf/m08dSOyv2gzwWPKWRMt5FK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb9TQoD%2Fbtr1Wloz0vf%2Fm08dSOyv2gzwWPKWRMt5FK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;538&quot; height=&quot;336&quot; data-origin-width=&quot;538&quot; data-origin-height=&quot;336&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OAuth 2.0의 구성요소를 살펴보자.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Client : Protected Resource에 접근하고 싶어하는 주체이며 웹앱, 모바일앱, 기타 프로그램 중 어떤 것도 될 수 있다.&lt;/li&gt;
&lt;li&gt;Resource Owner : Protected Resource에 대한 접근을 허용하는 주체이며 대부분의 경우 end-user, 즉 사용자를 의미한다.&lt;/li&gt;
&lt;li&gt;Authorization Server : Resource Owner를 인증하고 Access Token을 발급하는 서버.&lt;/li&gt;
&lt;li&gt;Resource Server : Access Token을 파라미터로 받아 검증하고 Protected Resource을 제공하는 서버.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Protocol Flow를 간단히 살펴보면 아래와 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;(A), (B), (C), (D) : 인증을 통한 Access Token 획득&lt;/li&gt;
&lt;li&gt;(E), (F) : Access Token을 이용한 Protected Resource을 이용&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;a id=&quot;user-content-client-credentials-grant&quot; href=&quot;https://github.com/traeper/spring-authorization-server-practice#client-credentials-grant&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;Client Credentials Grant&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OAuth 2.0 여러 인증 방식 중 Server to Server 구현에 해당하며 가장 간단한&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Client Credentials Grant&lt;/b&gt;을 구현해보고자 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;525&quot; data-origin-height=&quot;154&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vertm/btr1MkdepRi/e5GxF2FrYGBkn1mkHsKfq1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vertm/btr1MkdepRi/e5GxF2FrYGBkn1mkHsKfq1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vertm/btr1MkdepRi/e5GxF2FrYGBkn1mkHsKfq1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fvertm%2Fbtr1MkdepRi%2Fe5GxF2FrYGBkn1mkHsKfq1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;525&quot; height=&quot;154&quot; data-origin-width=&quot;525&quot; data-origin-height=&quot;154&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식은 Client와 Authorization Server 간 보안 환경(SSL) 등 신뢰할 수 있는 구간에서 활용할 수 있는 방식이다. Client는 Access Token을 얻은 다음 Resource Server에 접근하여 Protected Resource에 접근할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 Protocol Flow가 동작하기 전에 Authorization Server에서 ClientId, ClientSecret을 생성한 다음 Client가 이를 갖고 있는 상황에서 진행해야 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;a id=&quot;user-content-authorization-server-practice&quot; href=&quot;https://github.com/traeper/spring-authorization-server-practice#authorization-server-practice&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;Authorization Server Practice&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OAuth 2.0에 대해 여러 구현이 있지만 23년 3월 기준으로 최신 구현인 Spring Authorization Server 프레임워크를 이용하고자 한다. 이 프레임워크도 Spring Security의 SecurityFilterChain 기반으로 동작하므로 Spring Security에 대한 이해가 선행되어 있어야 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;792&quot; data-origin-height=&quot;533&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dGLoNl/btr133UVeuz/3HXm8vy3VRCMAKJ4dJlOh1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dGLoNl/btr133UVeuz/3HXm8vy3VRCMAKJ4dJlOh1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dGLoNl/btr133UVeuz/3HXm8vy3VRCMAKJ4dJlOh1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdGLoNl%2Fbtr133UVeuz%2F3HXm8vy3VRCMAKJ4dJlOh1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;792&quot; height=&quot;533&quot; data-origin-width=&quot;792&quot; data-origin-height=&quot;533&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;위 이미지는 공식 Reference 문서이다. Access Token을 발급하는 부분을 보고 싶으면 Protocol Endpoints -&amp;gt; OAuth2 Token Endpoint를 참조하면 된다. 해당 가이드에서는 AccessTokenRequestConverter, AuthenticationProvider, AccessTokenResponseHandler 등 SecurityFilterChain 구현체에 참여하는 다양한 요소들에 대해 커스터마이저를 제공하지만 커스터마이징을 할 필요는 거의 없을 것이다. 프레임워크가 rfc 문서에 적혀있는 OAuth 2.0 프레임워크의 스펙을 대부분 커버하므로 그대로 활용하면 될 뿐이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ClientId, ClientSecret 등 Client Authentication 동작에 필요한 저장소는 RegisteredClientRepository을 구현하면 되는데, In-Memory, Jdbc 구현체를 기본적으로 제공하며&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://docs.spring.io/spring-authorization-server/docs/current/reference/html/guides/how-to-jpa.html&quot;&gt;JPA 기반으로 작성된 예제&lt;/a&gt;도 충분히 제공하고 있다. JPA로 구현을 해봤었는데 이번에는 간단하게 In-Memory로 구현해보겠다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;a id=&quot;user-content-security-configuration&quot; href=&quot;https://github.com/traeper/spring-authorization-server-practice#security-configuration&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;Security Configuration&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/traeper/spring-authorization-server-practice/blob/main/src/main/kotlin/com/traeper/oauth2/authorization/configuration/SecurityConfiguration.kt&quot;&gt;SecurityConfiguration&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;파일을 보면 심플하게 구현된 SecurityFilterChain을 볼 수 있다. Reference가 워낙 잘 나와서 보고 따라 적으면 되는 수준이긴 하다.&lt;span&gt;&amp;nbsp;&lt;/span&gt; &lt;br /&gt;Client Credentials Grant 방식에 Authorization Basic Header를 이용한 인증 방식을 지원하기 위해 RegisteredClientRepository의 구현체는 아래처럼 커스터마이징 했다. 참고로 Access Token, Refresh Token 등의 유효시간도 설정할 수 있다.&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@Bean
fun registeredClientRepository(): RegisteredClientRepository {
    val registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
        // ..
        .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) // Authorization: Basic {base64 encoded String}
        .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) // client_credentials 이용
        .tokenSettings(TokenSettings.builder().accessTokenTimeToLive(Duration.ofHours(2)).build()) // 토큰 2시간 유효
        .build()
    return InMemoryRegisteredClientRepository(registeredClient)
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간편한 테스트를 위해 Resource Server의 역할도 겸하게 하였으며 구현이 간편한 jwt를 이용하도록 설정을 잡았다. 만약 Opaque Token을 이용하려 한다면 프레임워크 기본 구현체가 Resource Server에서 RestTemplate로 Authorization Server을 호출해야하므로 두 서버를 따로 뛰워야 하는 불편함이 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;a id=&quot;user-content-간단한-api-코드-작성&quot; href=&quot;https://github.com/traeper/spring-authorization-server-practice#%EA%B0%84%EB%8B%A8%ED%95%9C-api-%EC%BD%94%EB%93%9C-%EC%9E%91%EC%84%B1&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;간단한 API 코드 작성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Security 설정이 잘 되었는지 확인하기 위해 샘플 API를 작성했다.&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@RequestMapping(&quot;/v1/users&quot;)
@RestController
class UserController {

    @GetMapping(&quot;/me&quot;)
    fun getMyInfo(): UserInfoResponse =
        UserInfoResponse(&quot;traeper&quot;)

    data class UserInfoResponse(
        val nickname: String,
    )
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;a id=&quot;user-content-테스트&quot; href=&quot;https://github.com/traeper/spring-authorization-server-practice#%ED%85%8C%EC%8A%A4%ED%8A%B8&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;테스트하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버를 띄우고 이것저것 테스트를 해보자. (불필요한 헤더는 생략)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 인증 없이 API 호출&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;curl -X GET http://localhost:8080/v1/users/me -v
&amp;lt; HTTP/1.1 401
&amp;lt; WWW-Authenticate: Bearer&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인증 없이 API를 호출하면 401 응답을 받게 되며&lt;span&gt;&amp;nbsp;&lt;/span&gt;WWW-Authenticate: Bearer를 받게 된다. client가 유효하지 않을 때 내려주는 헤더값인데 Bearer로 인증하라는 것을 알 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. Access Token 발급 시도 (client_secret_basic 헤더 없이)&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;curl -X POST http://localhost:8080/custom-uri/oauth2/token\?grant_type\=client_credentials \
--header 'Content-Type: application/x-www-form-urlencoded' -v
&amp;lt; HTTP/1.1 401&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;client_secret_basic 헤더가 없어서 401이 발생하지만 WWW-Authenticate는 보이지 않는다. Basic 헤더를 잘 넘겨주자..!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. Access Token 발급 시도 (client_secret_basic 헤더와 함께)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;client_secret_basic 헤더의 포맷은&lt;span&gt;&amp;nbsp;&lt;/span&gt;Authorization: Basic {base64 encoded string}이다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;{clientId}:{clientSecret}의 복합 문장을 base64 인코딩하면 끝이다. 예시는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://github.com/traeper/spring-authorization-server-practice/blob/main/src/test/kotlin/com/traeper/oauth2/authorization/ApplicationTests.kt&quot;&gt;테스트케이스&lt;/a&gt;로 구현해뒀다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;clientSecret을 잘못 입력한 경우 401헤더와 함께&lt;span&gt;&amp;nbsp;&lt;/span&gt;{&quot;error&quot;:&quot;invalid_client&quot;}&lt;span&gt;&amp;nbsp;&lt;/span&gt;같은 응답이 내려오기도 한다. clientSecret은 spring security에서 제공하는 DelegatingPasswordEncoder을 이용하므로 테스트 시 참고하자. 본 예제에서는 {noop}으로 했으니 평문 비교를 한다. 현업에서 활용할 때는 해싱을 지원하는 bcrypt 알고리즘 등으로 clientSecret을 인코딩해서 저장하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;올바른 스펙으로 토큰 발급 API를 호출하면 마침내 Access Token을 얻을 수 있다.&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;curl -X POST http://localhost:8080/custom-uri/oauth2/token\?grant_type\=client_credentials \
--header 'Content-Type: application/x-www-form-urlencoded' \
--header 'Authorization: Basic YWFhYTpiYmJi' -v
&amp;gt; Content-Type: application/x-www-form-urlencoded
&amp;gt; Authorization: Basic YWFhYTpiYmJi
* Mark bundle as not supporting multiuse
&amp;lt; HTTP/1.1 200
{&quot;access_token&quot;:&quot;eyJraWQiOiIzZTk2OGI0YS04Zjk2LTRkMjMtYjc5MS02MmYzMGRiYWE1M2MiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJhYWFhIiwiYXVkIjoiYWFhYSIsIm5iZiI6MTY3Nzk1MzQzOSwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwIiwiZXhwIjoxNjc3OTYwNjM5LCJpYXQiOjE2Nzc5NTM0Mzl9.BApI_joRvSMBcQE4c3U86oJxKwCTf3EGgC-6pNGQ7sL3PvZ6FF1S5sNUC9RqqV5qRtDxLVV1CL3h9Elzz-AzyuH3Dnvky-VGbbtnl01fqlvHX33ovEmLDLWR_wWwC4NXiClD1ad-jamOO4bvd_TVPj84W7-Ok9Sza74X5jlAYK-l0Zca8J-GpRhF92wr7UDCdPdde_FKk99dO3LWf-qbklQBgitnbgUYc1but-fRypKoTfa8uT0NCd8pw3OkDSLuJ8rVvbqCWH5ugHZtt0Z1ZasVnMKz9XFjkpcpifIr-zT9-g807zoCFktS0pFN5aMcWpENV30EQDvyOCr94-nRwA&quot;,&quot;token_type&quot;:&quot;Bearer&quot;,&quot;expires_in&quot;:7199}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;access_token 값은 JWT라서 길이가 제법 긴데 Opaque Token이였으면 많이 짧을 것이다. token_type은 Bearer, expires_in:7199초로 2시간인 것을 알 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. Access Token으로 API 호출&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;curl -X GET localhost:8080/v1/users/me \
--header 'Authorization: Bearer eyJraWQiOiIzZTk2OGI0YS04Zjk2LTRkMjMtYjc5MS02MmYzMGRiYWE1M2MiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJhYWFhIiwiYXVkIjoiYWFhYSIsIm5iZiI6MTY3Nzk1MzQzOSwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwIiwiZXhwIjoxNjc3OTYwNjM5LCJpYXQiOjE2Nzc5NTM0Mzl9.BApI_joRvSMBcQE4c3U86oJxKwCTf3EGgC-6pNGQ7sL3PvZ6FF1S5sNUC9RqqV5qRtDxLVV1CL3h9Elzz-AzyuH3Dnvky-VGbbtnl01fqlvHX33ovEmLDLWR_wWwC4NXiClD1ad-jamOO4bvd_TVPj84W7-Ok9Sza74X5jlAYK-l0Zca8J-GpRhF92wr7UDCdPdde_FKk99dO3LWf-qbklQBgitnbgUYc1but-fRypKoTfa8uT0NCd8pw3OkDSLuJ8rVvbqCWH5ugHZtt0Z1ZasVnMKz9XFjkpcpifIr-zT9-g807zoCFktS0pFN5aMcWpENV30EQDvyOCr94-nRwA
' -v
&amp;gt; Authorization: Bearer eyJraWQiOiIzZTk2OGI0YS04Zjk2LTRkMjMtYjc5MS02MmYzMGRiYWE1M2MiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJhYWFhIiwiYXVkIjoiYWFhYSIsIm5iZiI6MTY3Nzk1MzQzOSwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwIiwiZXhwIjoxNjc3OTYwNjM5LCJpYXQiOjE2Nzc5NTM0Mzl9.BApI_joRvSMBcQE4c3U86oJxKwCTf3EGgC-6pNGQ7sL3PvZ6FF1S5sNUC9RqqV5qRtDxLVV1CL3h9Elzz-AzyuH3Dnvky-VGbbtnl01fqlvHX33ovEmLDLWR_wWwC4NXiClD1ad-jamOO4bvd_TVPj84W7-Ok9Sza74X5jlAYK-l0Zca8J-GpRhF92wr7UDCdPdde_FKk99dO3LWf-qbklQBgitnbgUYc1but-fRypKoTfa8uT0NCd8pw3OkDSLuJ8rVvbqCWH5ugHZtt0Z1ZasVnMKz9XFjkpcpifIr-zT9-g807zoCFktS0pFN5aMcWpENV30EQDvyOCr94-nRwA
&amp;lt; HTTP/1.1 200
{&quot;nickname&quot;:&quot;traeper&quot;}%&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소량의 코드만으로 OAuth 2.0 Authorization Server 구현 성공!&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;a id=&quot;user-content-spring-authorization-framework-파헤쳐-보기&quot; href=&quot;https://github.com/traeper/spring-authorization-server-practice#spring-authorization-framework-%ED%8C%8C%ED%97%A4%EC%B3%90-%EB%B3%B4%EA%B8%B0&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;Spring Authorization Framework 파헤쳐 보기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프레임워크가 동작하는 원리를 좀더 파보자.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;a id=&quot;user-content-filterchainproxy-중-사용된-filter-살펴보기&quot; href=&quot;https://github.com/traeper/spring-authorization-server-practice#filterchainproxy-%EC%A4%91-%EC%82%AC%EC%9A%A9%EB%90%9C-filter-%EC%82%B4%ED%8E%B4%EB%B3%B4%EA%B8%B0&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;FilterChainProxy 중 사용된 Filter 살펴보기&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;515&quot; data-origin-height=&quot;567&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bOckQZ/btr1UTZ98L1/KGs3R3Zg5MVbhOrfyijqi1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bOckQZ/btr1UTZ98L1/KGs3R3Zg5MVbhOrfyijqi1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bOckQZ/btr1UTZ98L1/KGs3R3Zg5MVbhOrfyijqi1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbOckQZ%2Fbtr1UTZ98L1%2FKGs3R3Zg5MVbhOrfyijqi1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;515&quot; height=&quot;567&quot; data-origin-width=&quot;515&quot; data-origin-height=&quot;567&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 이미지는 FilterChainProxy의 filterChains 필드에 등록된 필터들을 보여준다. Spring Security가 제공하는 10개 정도의 기본 필터 외에도 OAuth 2.0 Authorization Server 구현을 위한 필터도 약 10개 정도 추가되어 총 19개나 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 테스트에 사용된 필터는 빨갛게 칠해진 3개다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;OAuth2ClientAuthenticationFilter : Client로부터 받은 인증 요청을 처리하는 필터
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ClientSecretBasicAuthenticationConverter : 테스트에 사용된 client_secret_basic Authorization 헤더에서 clientId, clientSecret을 추출한다.&lt;/li&gt;
&lt;li&gt;ClientSecretAuthenticationProvider : 입력 받은 clientId, clientSecret을 검증한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;OAuth2TokenEndpointFilter : 토큰 발급 endpoint 필터
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;OAuth2ClientCredentialsAuthenticationConverter : client_credentials 방식을 지원하는 OAuth2ClientCredentialsAuthenticationToken 토큰 생성&lt;/li&gt;
&lt;li&gt;OAuth2ClientCredentialsAuthenticationProvider : OAuth2ClientCredentialsAuthenticationToken 토큰을 읽고 AccessToken을 생성&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;BearerTokenAuthenticationFilter : Bearer Token을 인증하는 필터
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JwtAuthenticationProvider : jwt를 검증한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 다른 인증 방식, endpoint, 토큰 타입 등을 활용한다면 사용되는 구현체가 달라질 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;a id=&quot;user-content-references&quot; href=&quot;https://github.com/traeper/spring-authorization-server-practice#references&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;References&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;The OAuth 2.0 Authorization Framework :&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://www.rfc-editor.org/rfc/rfc6749&quot;&gt;https://www.rfc-editor.org/rfc/rfc6749&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;The OAuth 2.1 Authorization Framework :&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-07&quot;&gt;https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-07&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;프로젝트 깃헙 링크 :&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://github.com/spring-projects/spring-authorization-server&quot;&gt;https://github.com/spring-projects/spring-authorization-server&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Spring Authorization Server Reference :&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://docs.spring.io/spring-authorization-server/docs/current/reference/html/index.html&quot;&gt;https://docs.spring.io/spring-authorization-server/docs/current/reference/html/index.html&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;링크&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;깃헙 프로젝트 : &lt;a href=&quot;https://github.com/traeper/spring-authorization-server-practice&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/traeper/spring-authorization-server-practice&lt;/a&gt;&lt;/p&gt;</description>
      <category>Spring/Spring Security OAuth 2.0</category>
      <category>authorization</category>
      <category>BASIC</category>
      <category>Bearer</category>
      <category>client_crendentials</category>
      <category>Grant</category>
      <category>OAuth</category>
      <category>OAuth 2.0</category>
      <category>Security</category>
      <category>Spring</category>
      <author>jay.pg</author>
      <guid isPermaLink="true">https://traeper.tistory.com/223</guid>
      <comments>https://traeper.tistory.com/223#entry223comment</comments>
      <pubDate>Sun, 5 Mar 2023 16:34:06 +0900</pubDate>
    </item>
    <item>
      <title>DDD 프랙티스 - 자동차 공장</title>
      <link>https://traeper.tistory.com/222</link>
      <description>&lt;h1&gt;DDD 프랙티스 #1 자동차 공장&lt;/h1&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;447&quot; data-origin-height=&quot;251&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b4TBHW/btr1WzAdzA8/aEZxJhVgR3KGqFyyWQGxEK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b4TBHW/btr1WzAdzA8/aEZxJhVgR3KGqFyyWQGxEK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b4TBHW/btr1WzAdzA8/aEZxJhVgR3KGqFyyWQGxEK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb4TBHW%2Fbtr1WzAdzA8%2FaEZxJhVgR3KGqFyyWQGxEK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;447&quot; height=&quot;251&quot; data-origin-width=&quot;447&quot; data-origin-height=&quot;251&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DDD스러운 코드를 작성하는 방법을 탐구하는 자동차 공장 예제. JPA 기반으로 작성한 예제이며 보다 핵심 도메인에 집중할 수 있는 장점이 있음을 확인할 수 있었다. 맨 아래 링크로 가서 README.md와 소스코드를 각각 켜놓고 읽는 것도 괜찮다. (테스트코드 포함)&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;a id=&quot;user-content-자동차-공장-설명&quot; href=&quot;https://github.com/traeper/ddd-practice/tree/main/car_factory#%EC%9E%90%EB%8F%99%EC%B0%A8-%EA%B3%B5%EC%9E%A5-%EC%84%A4%EB%AA%85&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;자동차 공장 설명&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Domain 간 유기적인 관계를 크게 이용하지 않는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://github.com/traeper/ddd-practice/blob/main/car_factory/src/main/kotlin/com/traeper/car_factory/legacy&quot;&gt;legacy&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;예제와 JPA의 여러 옵션을 활용하여 Aggregate 내부의 일관성을 유지하고 도메인에서 핵심이 되는 Aggregate Root등을 파악하기 쉬운&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://github.com/traeper/ddd-practice/blob/main/car_factory/src/main/kotlin/com/traeper/car_factory/ddd&quot;&gt;ddd&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;예제를 작성하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자동차 공장에는 자동차, 바퀴 개념이 존재하는데 자동차 없는 바퀴는 존재하지 않는 것을 전제로 하여 자동차는 Aggregate Root, 바퀴는 자동차에 포함되어 생성 및 삭제되는 종속적인 개념으로 가정한다. 즉 시스템의 핵심을 자동차에 두어 코드를 작성하였다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;a id=&quot;user-content-legacy-vs-ddd-비교-미리보기&quot; href=&quot;https://github.com/traeper/ddd-practice/tree/main/car_factory#legacy-vs-ddd-%EB%B9%84%EA%B5%90-%EB%AF%B8%EB%A6%AC%EB%B3%B4%EA%B8%B0&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;legacy vs ddd 비교 미리보기&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;301&quot; data-origin-height=&quot;415&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bXFBF7/btr1YKH06HK/m3P40AvOGDL8pRm0TpYDC0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bXFBF7/btr1YKH06HK/m3P40AvOGDL8pRm0TpYDC0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bXFBF7/btr1YKH06HK/m3P40AvOGDL8pRm0TpYDC0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbXFBF7%2Fbtr1YKH06HK%2Fm3P40AvOGDL8pRm0TpYDC0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;301&quot; height=&quot;415&quot; data-origin-width=&quot;301&quot; data-origin-height=&quot;415&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;legacy가 ddd에 비해 WheelRepository를 하나 더 가지고 있는 것을 볼 수 있다. 잘 모르는 사람이 위 파일 목록을 봤을 때는 바퀴의 지위와 쓰임새가 자동차와 동등한 수준일 수도 있다는 생각이 든다. 물론 바퀴와 자동차를 보면 누구나 자동차가 핵심이라고 생각할 것이지만 실제로 도메인이 복잡한 현업에서는 어떤 객체가 더 핵심인지 한 눈에 살펴보기 힘들 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;a id=&quot;user-content-legacy-예제&quot; href=&quot;https://github.com/traeper/ddd-practice/tree/main/car_factory#legacy-%EC%98%88%EC%A0%9C&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;legacy 예제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자동차(&lt;a href=&quot;https://github.com/traeper/ddd-practice/blob/main/car_factory/src/main/kotlin/com/traeper/car_factory/legacy/domain/car/LegacyCarEntity.kt&quot;&gt;LegacyCarEntity&lt;/a&gt;)에 바퀴를 OneToMany로 달아서 fetch join까지는 활용하도록 했지만 자동차와 바퀴는 엄연히 독립적으로 관리되는 것을 알 수 있다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://github.com/traeper/ddd-practice/blob/main/car_factory/src/test/kotlin/com/traeper/car_factory/legacy/domain/car/LegacyCarFactoryServiceTest.kt&quot;&gt;테스트코드&lt;/a&gt;를 보면 바퀴 Repository가 독립적으로 존재하여 관리되므로 바퀴가 자동차에 완전히 종속적인 개념인지 알기 어렵다. 즉 어떤 개념이 좀 더 핵심이 되는지 한 눈에 보기 어렵다.&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Entity
class LegacyCarEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    var carId: Long = 0

    @OneToMany
    @JoinColumn(name = &quot;carId&quot;, referencedColumnName = &quot;carId&quot;)
    var wheels: MutableList&amp;lt;LegacyWheelEntity&amp;gt; = mutableListOf()

    companion object {
        fun of(): LegacyCarEntity = LegacyCarEntity()
    }
}

@Entity
class LegacyWheelEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(nullable = false)
    var wheelId: Long = 0

    @Column(nullable = false)
    var carId: Long = 0

    companion object {
        fun of(carId: Long): LegacyWheelEntity =
            LegacyWheelEntity().apply {
                this.carId = carId
            }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바퀴와 연관 관계는 있지만 바퀴는 독립적으로 조작(CRUD)될 수 있는 가능성이 있다. 이런 코드라면 프로그램 실행 중에 자동차 객체 자신도 모르게 바퀴가 하나 더 달려버리는 문제가 생길 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/traeper/ddd-practice/blob/main/car_factory/src/main/kotlin/com/traeper/car_factory/legacy/domain/car/LegacyCarFactoryService.kt&quot;&gt;LegacyCarFactoryService&lt;/a&gt;를 보면 자동차와 바퀴를 따로 생성해야 하는 것을 볼 수 있고 Service의 의존성이 2개인 것을 알 수 있다. 비즈니스가 복잡해지면 의존하는 리포지토리나 빈이 훨씬 많아질텐데 이대로라면 핵심을 파악하기 훨씬 어려워지지 않을까?&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Service
class LegacyCarFactoryService(
    private val carRepository: LegacyCarRepository,
    private val wheelRepository: LegacyWheelRepository,
) {
    @Transactional
    fun createCar() {
        val car = LegacyCarEntity.of()
        carRepository.save(car)

        repeat((1..4).count()) {
            val wheel = LegacyWheelEntity.of(car.carId)
            wheelRepository.save(wheel)
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;a id=&quot;user-content-ddd-예제&quot; href=&quot;https://github.com/traeper/ddd-practice/tree/main/car_factory#ddd-%EC%98%88%EC%A0%9C&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;ddd 예제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ddd 예제에서도 자동차(&lt;a href=&quot;https://github.com/traeper/ddd-practice/blob/main/car_factory/src/main/kotlin/com/traeper/car_factory/ddd/domain/car/CarEntity.kt&quot;&gt;CarEntity&lt;/a&gt;)는 OneToMany로 바퀴를 관리하는데 달라진 점은 Cascade 옵션을 주어 자동차가 생성, 삭제될 때 바퀴도 따라 생성, 삭제되도록 하여 바퀴의 생애가 자동차에 종속적인 것임을 알 수 있게 된 것이다. 이제는 바퀴 Repository가 존재하지 않아 바퀴를 독립적으로 제어할 수 없다. 그리고 자동차와 바퀴에 AggregateRoot, DomainEntity interface로 수식하여 둘의 관계와 중요도를 좀 더 명확하게 표현하였다.&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Entity
class CarEntity : AggregateRoot {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    var carId: Long = 0

    // 양방향 매핑, car 생성/삭제할 때 wheel도 함께 생성, 삭제
    @OneToMany(cascade = [CascadeType.PERSIST, CascadeType.REMOVE], mappedBy = &quot;car&quot;)
    var wheels: MutableList&amp;lt;WheelEntity&amp;gt; = mutableListOf()

    // 바퀴의 추가를 자동차가 관리한다.
    fun addWheel(wheel: WheelEntity) {
        wheels.add(wheel)
    }

    // 바퀴의 제거를 자동차가 관리한다.
    fun removeWheel(wheel: WheelEntity) {
        wheels.remove(wheel)
    }

    companion object {
        fun of(): CarEntity =
            CarEntity()
    }
}

@Entity
class WheelEntity : DomainEntity&amp;lt;CarEntity&amp;gt; {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(nullable = false)
    var wheelId: Long = 0

    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    @JoinColumn(name = &quot;carId&quot;, nullable = false)
    lateinit var car: CarEntity
        protected set

    companion object {
        fun of(car: CarEntity): WheelEntity =
            WheelEntity().apply {
                this.car = car
            }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;legacy와 다르게 자동차가 바퀴를 직접 관리한다. 즉 자동차와 바퀴의 상태는 Aggregate Root인 자동차를 통해서만 변경할 수 있으며 프로그램 실행 중에 자동차 객체는 자신에게 바퀴가 달리는지, 없어지는지 항상 알 수 있다. 즉 자동차와 바퀴의 집합이 일관성 있게 변경되는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/traeper/ddd-practice/blob/main/car_factory/src/main/kotlin/com/traeper/car_factory/ddd/domain/car/CarFactoryService.kt&quot;&gt;CarFactoryService&lt;/a&gt;를 보면 legacy와 다르게 WheelRepository가 더는 존재하지 않으며 자동차를 통해서만 바퀴 추가가 가능하다. 의존성도 legacy에 비해 줄었으며 로직에서 핵심이 무엇인지 알기 더욱 쉬워졌다.&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@DomainService
class CarFactoryService(
    private val carRepository: CarRepository,
) {
    @Transactional
    fun createCar() {
        val car = CarEntity.of()

        repeat((1..4).count()) {
            val newWheel = WheelEntity.of(car)
            car.addWheel(newWheel)
        }

        carRepository.save(car)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;a id=&quot;user-content-질문&quot; href=&quot;https://github.com/traeper/ddd-practice/tree/main/car_factory#%EC%A7%88%EB%AC%B8&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;질문&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;새로운 요구사항으로 인해 만약 바퀴가 Aggregate Root로 승격하는 경우는 없을까? 이런 경우 자동차와 다른 Aggregate로 개발되어야 되는데 그럴 때는 자동차와 바퀴 관계를 어떻게 개발해야 할까?&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;링크&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;깃헙 프로젝트 :&lt;span&gt; &lt;a href=&quot;https://github.com/traeper/ddd-practice/tree/main/car_factory&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/traeper/ddd-practice/tree/main/car_factory&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;</description>
      <category>방법론</category>
      <category>aggregate</category>
      <category>DDD</category>
      <category>의존성</category>
      <category>핵심 도메인</category>
      <author>jay.pg</author>
      <guid isPermaLink="true">https://traeper.tistory.com/222</guid>
      <comments>https://traeper.tistory.com/222#entry222comment</comments>
      <pubDate>Sat, 4 Mar 2023 19:17:44 +0900</pubDate>
    </item>
    <item>
      <title>DDD 프랙티스 - DDD 코어</title>
      <link>https://traeper.tistory.com/221</link>
      <description>&lt;h1&gt;DDD Core&lt;/h1&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;447&quot; data-origin-height=&quot;251&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c9xlob/btr1URnpPYP/KkmOj7C0eXMmt0D9UQzvDK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c9xlob/btr1URnpPYP/KkmOj7C0eXMmt0D9UQzvDK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c9xlob/btr1URnpPYP/KkmOj7C0eXMmt0D9UQzvDK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc9xlob%2Fbtr1URnpPYP%2FKkmOj7C0eXMmt0D9UQzvDK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;447&quot; height=&quot;251&quot; data-origin-width=&quot;447&quot; data-origin-height=&quot;251&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DDD에서 언급되는 Aggregate Root, Domain Service, Domain Event 등을 코드로 녹여냈다. 예전에 함께 일했던 동료가 작성한 것을 보고 감명 깊었던 적이 있어서 나도 이렇게 코드를 작성해본다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Aggregate Root&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 Aggregate(애그리거트)에 포함된 Entity끼리는 변경에 대해 일관성(같은 Root를 통해 함께 변경됨)을 가져야 한다. Aggregate의 Root를 담당하는 Entity만 Domain에서 Repository를 유일하게 소유할 수 있어 제어의 창구가 되어 Aggregate 내의 다른 Entity들을 관리한다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://github.com/traeper/ddd-practice/blob/main/ddd_core/src/main/kotlin/com/traeper/ddd/core/AggregateRoot.kt&quot;&gt;AggregateRoot interface&lt;/a&gt;를 정의하였는데 이것을 Aggregate Root가 되는 Entity에 붙여서 Repository를 유일하게 가질 수 있음을 명시적으로 알려주도록 하였다.&lt;/p&gt;
&lt;pre id=&quot;code_1677919120964&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package com.traeper.ddd.core

/**
 * AggregateRoot를 명시적으로 표시하기 위해 정의
 */
interface AggregateRoot&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용 예시 :&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://github.com/traeper/ddd-practice/blob/main/car_factory/src/main/kotlin/com/traeper/car_factory/ddd/domain/car/CarEntity.kt&quot;&gt;CarEntity&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1677919181933&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
class CarEntity : AggregateRoot {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    var carId: Long = 0

    // 양방향 매핑, car 생성/삭제할 때 wheel도 함께 생성, 삭제
    @OneToMany(cascade = [CascadeType.PERSIST, CascadeType.REMOVE], mappedBy = &quot;car&quot;)
    var wheels: MutableList&amp;lt;WheelEntity&amp;gt; = mutableListOf()
    
    // ..
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Domain Entity&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Aggregate Root가 아닌 Domain 객체는 Repository를 가질 수 없어야 하며 연관된 AggregateRoot가 존재한다. 이를 코드로 구현하기 위해&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://github.com/traeper/ddd-practice/blob/main/ddd_core/src/main/kotlin/com/traeper/ddd/core/DomainEntity.kt&quot;&gt;DomainEntity interface&lt;/a&gt;를 정의하였고 연관된 AggregateRoot를 명시하도록 하였다.&lt;/p&gt;
&lt;pre id=&quot;code_1677918919349&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package com.traeper.ddd.core

interface DomainEntity&amp;lt;T : AggregateRoot&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용 예시 :&amp;nbsp;&lt;a href=&quot;https://github.com/traeper/ddd-practice/blob/main/car_factory/src/main/kotlin/com/traeper/car_factory/ddd/domain/car/WheelEntity.kt&quot;&gt;WheelEntity&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1677919214984&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
class WheelEntity : DomainEntity&amp;lt;CarEntity&amp;gt; {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(nullable = false)
    var wheelId: Long = 0

    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    @JoinColumn(name = &quot;carId&quot;, nullable = false)
    lateinit var car: CarEntity
        protected set
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;DomainService&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/traeper/ddd-practice/blob/main/ddd_core/src/main/kotlin/com/traeper/ddd/core/DomainService.kt&quot;&gt;DomainService&lt;/a&gt;를 추가하여 Domain Layer의 DomainService임을 명시해준다. 이 개념을 추가한 이유는 Layered Architecture 기반에서 프로그래밍하는 경우 상위 레이어인 ApplicationService와 구분하기 위함이다. DomainService는 단일 Repository만으로 해결할 수 없거나 인접한 Domain 간 복잡도가 높은 비즈니스를 수행하기 위해 이용하는 용도이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인접한 Domain을 DomainService로 묶어 처리하여 응집도가 증가하며 가독성이 증가하게 된다. 추후 복잡도가 올라가는 예제를 작성하면서 활용할 예정이다.&lt;/p&gt;
&lt;pre id=&quot;code_1677919238182&quot; class=&quot;crystal&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package com.traeper.ddd.core

import org.springframework.stereotype.Service

@Service
annotation class DomainService&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용 예시 :&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://github.com/traeper/ddd-practice/blob/main/car_factory/src/main/kotlin/com/traeper/car_factory/ddd/domain/car/CarFactoryService.kt&quot;&gt;CarFactoryService&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1677919272714&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@DomainService
class CarFactoryService(
    private val carRepository: CarRepository,
) {
    @Transactional
    fun createCar() { 
	    // 자동차를 만들고 바퀴를 달아준다.
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;a id=&quot;user-content-domainevent&quot; href=&quot;https://github.com/traeper/ddd-practice/blob/main/ddd_core/README.md#domainevent&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;DomainEvent&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Aggregate 간 결합도를 낮추기 위해 활용하는 개념이다. A Aggregate가 비즈니스를 수행한 뒤 B Aggregate의 동작이 수행되어야 할 때 A Aggregate는 DomainEvent를 발행하며 B는 그 DomainEvent를 구독하여 동기 혹은 비동기로 자신의 비즈니스를 수행할 수 있다. Aggregate 간 결합도는 낮추면서 비즈니스를 연결하는 방법이다. 이것 역시 추후 프랙티스를 작성할 예정이다.&lt;/p&gt;
&lt;pre id=&quot;code_1677915911551&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package com.traeper.ddd.core

/**
 * 도메인 객체가 발생시키는 DomainEvent를 가리킨다.
 */
interface DomainEvent&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;링크&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;깃헙 프로젝트 : &lt;a href=&quot;https://github.com/traeper/ddd-practice/tree/main/ddd_core&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/traeper/ddd-practice/tree/main/ddd_core&lt;/a&gt;&lt;/p&gt;</description>
      <category>방법론</category>
      <category>aggregate</category>
      <category>DDD</category>
      <category>domain</category>
      <category>DomainService</category>
      <category>event</category>
      <author>jay.pg</author>
      <guid isPermaLink="true">https://traeper.tistory.com/221</guid>
      <comments>https://traeper.tistory.com/221#entry221comment</comments>
      <pubDate>Sat, 4 Mar 2023 17:23:31 +0900</pubDate>
    </item>
    <item>
      <title>DDD 직접 구현하며 익혀보기</title>
      <link>https://traeper.tistory.com/220</link>
      <description>&lt;h1&gt;DDD (Domain Driven Design)&lt;/h1&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;447&quot; data-origin-height=&quot;251&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/o3Rqc/btr1XOXXUvR/KpXQ5SSQPoDuu5dNh0Jy7k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/o3Rqc/btr1XOXXUvR/KpXQ5SSQPoDuu5dNh0Jy7k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/o3Rqc/btr1XOXXUvR/KpXQ5SSQPoDuu5dNh0Jy7k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fo3Rqc%2Fbtr1XOXXUvR%2FKpXQ5SSQPoDuu5dNh0Jy7k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;447&quot; height=&quot;251&quot; data-origin-width=&quot;447&quot; data-origin-height=&quot;251&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;a id=&quot;user-content-ddd-vs-oop&quot; href=&quot;https://github.com/traeper/ddd-practice#ddd-vs-oop&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;DDD vs OOP?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OOP : 패러다임(함수형 등)&lt;br /&gt;DDD : 방법론(TDD, BDD 등)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DDD와 다른 TDD, BDD의 차이점으로 DDD는 개발자 외에도 기획자, 도메인 전문가 등 프로덕트 팀 단위의 역량이 필요한 협업 방법론이라고 할 수 있음. 객체지향의 역사는 길지만 개인적인 느낌으로는 best practice가 DDD라고 생각되어 정리하게 되었다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;a id=&quot;user-content-ddd-특징&quot; href=&quot;https://github.com/traeper/ddd-practice#ddd-%ED%8A%B9%EC%A7%95&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;DDD 특징&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;도메인의 모델과 로직에 집중
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;System, Infrastructure 등 기술적인 부분이 아닌 핵심 도메인 비즈니스에 집중하는 방법론&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;보편적 언어 사용
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프로덕트 팀 구성원이 동일한 언어를 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Software Entity와 Domain 간 개념의 일치
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;코드가 곧 설계를 의미&lt;/li&gt;
&lt;li&gt;Entity 클래스만 봐도 Domain 간 연관 관계, 중요도의 차이(Aggregate), 비즈니스 복잡도를 이해하는데에 도움을 받을 수 있음.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;a id=&quot;user-content-ddd-주요-개념&quot; href=&quot;https://github.com/traeper/ddd-practice#ddd-%EC%A3%BC%EC%9A%94-%EA%B0%9C%EB%85%90&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;DDD 주요 개념&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Bounded Context
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;범위를 구분하는 하위 도메인 개념.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Context Map
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Bounded Context 간 관계를 보여준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Aggregate
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터 변경 단위, 일관성 있게 변경되어야 하는 Entity 집합체로 볼 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;a id=&quot;user-content-프랙티스&quot; href=&quot;https://github.com/traeper/ddd-practice#%ED%94%84%EB%9E%99%ED%8B%B0%EC%8A%A4&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;프랙티스&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://traeper.tistory.com/221&quot;&gt;DDD 코어&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;: DDD 개념을 코드로 옮긴다.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://traeper.tistory.com/222&quot;&gt;자동차 공장&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;: 자동차 공장을 예로 들어 작성한 DDD 예시&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;a id=&quot;user-content-reference&quot; href=&quot;https://github.com/traeper/ddd-practice#reference&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;Reference&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;도메인 주도 설계 핵심 - 반 버논 (번역본) :&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://product.kyobobook.co.kr/detail/S000001804574&quot;&gt;https://product.kyobobook.co.kr/detail/S000001804574&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;ㄷㄷㄷ: Domain Driven Design과 적용 사례공유 / if(kakao)2022&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://youtu.be/4QHvTeeTsj0&quot;&gt;https://youtu.be/4QHvTeeTsj0&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>방법론</category>
      <category>DDD</category>
      <category>Domain Driven Design</category>
      <author>jay.pg</author>
      <guid isPermaLink="true">https://traeper.tistory.com/220</guid>
      <comments>https://traeper.tistory.com/220#entry220comment</comments>
      <pubDate>Sat, 4 Mar 2023 16:41:25 +0900</pubDate>
    </item>
    <item>
      <title>OpenApiSpec.을 이용한 더욱 효과적인 API 문서화</title>
      <link>https://traeper.tistory.com/219</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1100&quot; data-origin-height=&quot;288&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lOq94/btrtKGr6DKI/ZQRYD0GrKOqSxSNakKpkOK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lOq94/btrtKGr6DKI/ZQRYD0GrKOqSxSNakKpkOK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lOq94/btrtKGr6DKI/ZQRYD0GrKOqSxSNakKpkOK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlOq94%2FbtrtKGr6DKI%2FZQRYD0GrKOqSxSNakKpkOK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1100&quot; height=&quot;288&quot; data-origin-width=&quot;1100&quot; data-origin-height=&quot;288&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API 서버를 개발하게 되면 App, Frontend 그리고 동료 Backend 개발자와 API 형상을 공유하고 편하게 테스트할 수 있는 API 문서를 제공해주는 것이 협업의 첫 걸음이라고 할 수 있을 것 같습니다. API 문서를 만들기 위해 Swagger나 REST Docs를 많이 사용하는데요. 본 글에서는 자주 비교되곤 하는 Swagger와 REST Docs의 장점만을 뽑아낸 OpenApi Spec.(OAS) 기반의 API 문서화 방법을 공유드리고자 합니다. 블로그에 작성된 모든 예제는 &lt;a href=&quot;https://github.com/traeper/api_documentation&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;github&lt;/a&gt;에 있으니 실제로 돌려보실 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;a id=&quot;user-content-1-api-문서화-도구&quot; href=&quot;https://github.com/traeper/api_documentation/blob/master/README.md#1-api-%EB%AC%B8%EC%84%9C%ED%99%94-%EB%8F%84%EA%B5%AC&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;1. API 문서화 도구&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;a id=&quot;user-content-11-swagger&quot; href=&quot;https://github.com/traeper/api_documentation/blob/master/README.md#11-swagger&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;1.1. Swagger&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Swagger는 Controller에 몇 가지의 Annotation을 달기만 해도 API 문서가 만들어지는 편리함이 있지만 Test를 강제하지는 않기에 문서의 신뢰도를 높게 유지하기 어려운 문제가 있습니다. 그럼에도 문서가 워낙 예쁘고 테스트를 돌리지 않아도 되는 간편함이 있어 많이 사용되고 있습니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 76px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style4&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;장점&lt;/td&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;단점&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;예쁜 문서&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;어노테이션으로 소스코드가 복잡해짐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;API Test 기능 지원&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;높지 않은 신뢰도(테스트 X)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;a id=&quot;user-content-12-spring-rest-docs&quot; href=&quot;https://github.com/traeper/api_documentation/blob/master/README.md#12-spring-rest-docs&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;1.2. Spring REST Docs&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;REST Docs는 Integration 테스트를 작성해서 통과해야만 문서가 작성되므로 문서의 신뢰도가 높게 유지된다는 장점이 있습니다. Swagger와 다르게 소스코드에 변경을 주지 않아 비즈니스 로직과 테스트케이스를 분리하는 장점이 있고 테스트를 강제하므로 유지보수에도 도움을 줍니다. 반면 문서가 Swagger에 비해 덜(?) 예쁘고 API Test를 지원하지는 않는 단점이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;장점단점&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 60px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style4&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;장점&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;단점&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;테스트 기반의 높은 신뢰도&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;문서가 예쁘지 않음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;소스코드에 영향 없음&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;API Test 기능 미지원&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;a id=&quot;user-content-2-openapi-specificationoas-기반-api-문서화&quot; href=&quot;https://github.com/traeper/api_documentation/blob/master/README.md#2-openapi-specificationoas-%EA%B8%B0%EB%B0%98-api-%EB%AC%B8%EC%84%9C%ED%99%94&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;2. OpenApi Specification(OAS) 기반 API 문서화&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Swagger 팀이 SmartBear에 합류하면서 Swagger Spec.이 OpenApi Spec.으로 명칭이 바뀌었고 오늘날에는 RESTful API 스펙에 대한 사실상의 표준으로서 활용되고 있다고 합니다. Swagger-UI는 이 OAS를 해석하여 API 스펙을 시각화해줍니다. 또한 Postman, Paw 같은 REST Client들도 OAS를 지원하고 있어 OAS의 활용도가 무척 다양한 것을 알 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;a id=&quot;user-content-21-rest-docs-api-specification-integration&quot; href=&quot;https://github.com/traeper/api_documentation/blob/master/README.md#21-rest-docs-api-specification-integration&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;2.1. REST Docs API specification Integration&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;REST Docs와 Swagger의 장점을 어떻게 합칠 수 있을까요? 운이 좋게도 독일 기업 epages에서 Spring REST Docs를 연동하여&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://github.com/ePages-de/restdocs-api-spec&quot;&gt;OAS 파일을 만들어주는 오픈소스(restdocs-api-spec)&lt;/a&gt;를 제공하고 있습니다. 이 오픈소스를 이용해서 OAS 파일을 생성하고 Swagger-UI로 띄우면 되는 것입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;a id=&quot;user-content-22-구현&quot; href=&quot;https://github.com/traeper/api_documentation/blob/master/README.md#22-%EA%B5%AC%ED%98%84&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;2.2. 구현&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구현 순서는 다음과 같습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Swagger-UI standalone, Static Routing 셋팅&lt;/li&gt;
&lt;li&gt;restdocs-api/spec을 이용한 OAS 파일을 생성하는 빌드 환경 구축&lt;/li&gt;
&lt;li&gt;생성된 OAS 파일을 swagger 디렉토리로 복사하는 스크립트 작성(copyOasToSwagger)&lt;/li&gt;
&lt;li&gt;MockMvc REST Docs Test 코드 작성&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;a id=&quot;user-content-221-swagger-ui-standalone-static-routing-셋팅&quot; href=&quot;https://github.com/traeper/api_documentation/blob/master/README.md#221-swagger-ui-standalone-static-routing-%EC%85%8B%ED%8C%85&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;2.2.1. Swagger-UI standalone, Static Routing 셋팅&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Swagger-UI gradle 의존성은 불필요하며 standalone (HTML/CSS/JS) 파일들만 있으면 됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;a id=&quot;user-content-2211-swagger-ui-installation&quot; href=&quot;https://github.com/traeper/api_documentation/blob/master/README.md#2211-swagger-ui-installation&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;2.2.1.1.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://swagger.io/docs/open-source-tools/swagger-ui/usage/installation/&quot;&gt;Swagger UI Installation&lt;/a&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;'Static files without HTTP or HTML' 부분에서 latest release를 다운 받아 /dist 디렉토리만 복사해줍니다.&lt;/p&gt;
&lt;p&gt;&lt;img style=&quot;text-align: center; caret-color: transparent; font-family: -apple-system, BlinkMacSystemFont, AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; letter-spacing: 0px;&quot; src=&quot;https://blog.kakaocdn.net/dn/c9un9F/btrtIQoqTDY/Zu8whGKYoIHcfybBo4deQk/img.png&quot; data-image-src=&quot;https://blog.kakaocdn.net/dn/c9un9F/btrtIQoqTDY/Zu8whGKYoIHcfybBo4deQk/img.png&quot; data-origin-width=&quot;253&quot; data-origin-height=&quot;313&quot; /&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;a id=&quot;user-content-2212-static-routing-셋팅&quot; href=&quot;https://github.com/traeper/api_documentation/blob/master/README.md#2212-static-routing-%EC%85%8B%ED%8C%85&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;2.2.1.2. Static Routing 셋팅&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WebMvcConfigurer의 addResourceHandlers() 메서드를 구현합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/traeper/api_documentation/blob/master/src/main/kotlin/com/traeper/api_documentation/configuration/StaticRoutingConfiguration.kt&quot;&gt;Static Routing Configuration&lt;/a&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;a id=&quot;user-content-2213-swagger-file-수정&quot; href=&quot;https://github.com/traeper/api_documentation/blob/master/README.md#2213-swagger-file-%EC%88%98%EC%A0%95&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;2.2.1.3. Swagger file 수정&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;index.html
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;swagger-ui.html으로 이름 변경&lt;/li&gt;
&lt;li&gt;내부 js, css 경로를 static routing으로 적용&lt;/li&gt;
&lt;li&gt;SwaggerUIBundle 경로는 생성될 yaml 파일의 경로로 입력&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;불필요한 파일 삭제
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;oauth2-redirect.html&lt;/li&gt;
&lt;li&gt;swagger-ui.js&lt;/li&gt;
&lt;li&gt;swagger-ui-es-bundle-core.js&lt;/li&gt;
&lt;li&gt;swagger-ui-es-bundle.js&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;a id=&quot;user-content-222-epages-derestdocs-apispec을-이용한-oas-출력&quot; href=&quot;https://github.com/traeper/api_documentation/blob/master/README.md#222-epages-derestdocs-apispec%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%9C-oas-%EC%B6%9C%EB%A0%A5&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;2.2.2. ePages-de/restdocs-api/spec을 이용한 OAS 출력&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring REST Docs의 extension을 포함하는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;restdocs-api-spec&lt;/b&gt;과 MockMvc wrapper를 제공하는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;restdocs-api-spec-mockmvc&lt;/b&gt;를 이용해서 OAS 파일을 생성하고자 합니다. RestAssured도 지원되니 필요하신 분은 restdocs-api-spec-restassured를 사용하셔도 됩니다. build tool은 gradle로 진행하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;build.gradle.kts 파일에 아래처럼 plugins와 dependency를 추가해주면 사용할 수 있습니다. 깃헙에&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://github.com/ePages-de/restdocs-api-spec#gradle&quot;&gt;legacy plugin 예시&lt;/a&gt;도 함께 제공되니 편한 것을 이용해서 환경을 잡아주면 됩니다. openapi, openapi3, postman 등 다양한 output format이 제공되고 커스터마이징할 수 있는데 본 포스팅에서는 swagger 연동을 위해 openapi3 설정을 커스터마이징해보곘습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OAS 파일 기본 생성 경로는&lt;span&gt;&amp;nbsp;&lt;/span&gt;/build/api-spec&lt;span&gt;&amp;nbsp;&lt;/span&gt;입니다.&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;plugins {
    id(&quot;com.epages.restdocs-api-spec&quot;) version &quot;0.15.3&quot;
}

repositories {
    mavenCentral()
}

dependencies {
    testImplementation(&quot;com.epages:restdocs-api-spec-mockmvc:0.15.3&quot;)
}

openapi3 {
    this.setServer(&quot;https://localhost:8080&quot;) // list로 넣을 수 있어 각종 환경의 URL들을 넣을 수 있음!
    title = &quot;My API&quot;
    description = &quot;My API description&quot;
    version = &quot;0.1.0&quot;
    format = &quot;yaml&quot; // or json
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;gradle Tasks - documentation에 openapi, openapi3, postman Task가 기본으로 추가된 모습을 확인할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;163&quot; data-origin-height=&quot;260&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1zhyS/btrtH0ZYC18/lnxNKbkinwUSfekUXB76d1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1zhyS/btrtH0ZYC18/lnxNKbkinwUSfekUXB76d1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1zhyS/btrtH0ZYC18/lnxNKbkinwUSfekUXB76d1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1zhyS%2FbtrtH0ZYC18%2FlnxNKbkinwUSfekUXB76d1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;163&quot; height=&quot;260&quot; data-origin-width=&quot;163&quot; data-origin-height=&quot;260&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지만 작업하고 Test 코드 없이 openapi3을 실행하면 /build/api-spec 디렉토리에 별 내용이 없는 openapi3.yaml이 생성됩니다.&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;# /build/api-spec
openapi: 3.0.1
info:
  title: My API
  description: My API description
  version: 0.1.0
servers:
- url: http://localhost:8080
tags: []
paths: {}
components:
  schemas: {}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;a id=&quot;user-content-223-생성된-oas-파일을-swagger-디렉토리로-복사하는-스크립트-작성copyoastoswagger&quot; href=&quot;https://github.com/traeper/api_documentation/blob/master/README.md#223-%EC%83%9D%EC%84%B1%EB%90%9C-oas-%ED%8C%8C%EC%9D%BC%EC%9D%84-swagger-%EB%94%94%EB%A0%89%ED%86%A0%EB%A6%AC%EB%A1%9C-%EB%B3%B5%EC%82%AC%ED%95%98%EB%8A%94-%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%EC%9E%91%EC%84%B1copyoastoswagger&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;2.2.3. 생성된 OAS 파일을 swagger 디렉토리로 복사하는 스크립트 작성(copyOasToSwagger)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;/build 디렉토리에 생성된 yaml 파일을 swagger 디렉토리로 복사하는 Task를 작성합니다. Task를 실행시키면 swagger 디렉토리로 파일이 복사되는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;gradle&quot;&gt;&lt;code&gt;// build.gradle.kts
tasks.register&amp;lt;Copy&amp;gt;(&quot;copyOasToSwagger&quot;) {
    delete(&quot;src/main/resources/static/swagger-ui/openapi3.yaml&quot;) // 기존 yaml 파일 삭제
    from(&quot;$buildDir/api-spec/openapi3.yaml&quot;) // 복제할 yaml 파일 타겟팅
    into(&quot;src/main/resources/static/swagger-ui/.&quot;) // 타겟 디렉토리로 파일 복제
    dependsOn(&quot;openapi3&quot;) // openapi3 task가 먼저 실행되도록 설정
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;a id=&quot;user-content-224-mockmvc-rest-docs-test-코드-작성&quot; href=&quot;https://github.com/traeper/api_documentation/blob/master/README.md#224-mockmvc-rest-docs-test-%EC%BD%94%EB%93%9C-%EC%9E%91%EC%84%B1&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;2.2.4. MockMvc REST Docs Test 코드 작성&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MockMvc + MockMvcRestDocumentationWrapper를 이용하여 REST Docs&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://github.com/traeper/api_documentation/blob/master/src/test/kotlin/com/traeper/api_documentation/SampleControllerTest.kt&quot;&gt;테스트케이스 샘플&lt;/a&gt;을 작성하였습니다. Wrapper와 Snippet을 epages 라이브러리로 대신 사용하는 것 외에는 REST Docs와 큰 차이가 없으며 모양새도 거의 동일합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;a id=&quot;user-content-2241-기존-rest-docs로-작성한-코드를-활용하려면&quot; href=&quot;https://github.com/traeper/api_documentation/blob/master/README.md#2241-%EA%B8%B0%EC%A1%B4-rest-docs%EB%A1%9C-%EC%9E%91%EC%84%B1%ED%95%9C-%EC%BD%94%EB%93%9C%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%98%EB%A0%A4%EB%A9%B4&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;2.2.4.1. 기존 REST Docs로 작성한 코드를 활용하려면&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring의 MockMvcRestDocumentation을 MockMvcRestDocumentationWrapper로 바꿔주기만 하면 됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;a id=&quot;user-content-3-결과&quot; href=&quot;https://github.com/traeper/api_documentation/blob/master/README.md#3-%EA%B2%B0%EA%B3%BC&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;3. 결과&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;copyOasToSwagger task를 수행한 다음 서버를 띄우면 Swagger가 잘 뜬 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Swagger-UI가 성공적으로 뜬 모습&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;617&quot; data-origin-height=&quot;644&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Hghf3/btrtExDtWLz/AaJN1KqwxYS8BSz7eCjmW1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Hghf3/btrtExDtWLz/AaJN1KqwxYS8BSz7eCjmW1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Hghf3/btrtExDtWLz/AaJN1KqwxYS8BSz7eCjmW1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHghf3%2FbtrtExDtWLz%2FAaJN1KqwxYS8BSz7eCjmW1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;617&quot; height=&quot;644&quot; data-origin-width=&quot;617&quot; data-origin-height=&quot;644&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;API Test도 잘 지원합니다. :)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;608&quot; data-origin-height=&quot;834&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eh7OTq/btrtMJWIEno/dLLPo70feKLi3z8TBZOMV0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eh7OTq/btrtMJWIEno/dLLPo70feKLi3z8TBZOMV0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eh7OTq/btrtMJWIEno/dLLPo70feKLi3z8TBZOMV0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Feh7OTq%2FbtrtMJWIEno%2FdLLPo70feKLi3z8TBZOMV0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;608&quot; height=&quot;834&quot; data-origin-width=&quot;608&quot; data-origin-height=&quot;834&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;OAS 파일이 잘 생성되었습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;openapi: 3.0.1
info:
  title: My API
  description: My API description
  version: 0.1.0
servers:
- url: http://localhost:8080
tags: []
paths:
  /api/v1/samples/{sampleId}:
    get:
      tags:
      - Sample
      summary: Get a sample by id
      description: Get a sample by id
      operationId: sample
      parameters:
      - name: sampleId
        in: path
        description: &quot;&quot;
        required: true
        schema:
          type: string
      responses:
        &quot;200&quot;:
          description: &quot;200&quot;
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/api-v1-samples-sampleId486549215'
              examples:
                sample:
                  value: &quot;{\&quot;sampleId\&quot;:\&quot;aaa\&quot;,\&quot;name\&quot;:\&quot;sample-aaa\&quot;}&quot;
components:
  schemas:
    api-v1-samples-sampleId486549215:
      type: object
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;a id=&quot;user-content-4-결론&quot; href=&quot;https://github.com/traeper/api_documentation/blob/master/README.md#4-%EA%B2%B0%EB%A1%A0&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;4. 결론&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;본 포스팅에서는 자주 비교되고 있는 REST Docs와 Swagger에서 장점만을 뽑아내어 API 문서화를 만들어봤습니다. OAS 파일은 Postman, Paw, Insomnia 같은 API Client에서 Import하여 사용할 수 있는 장점도 있습니다. 옆자리 동료에게 공유해보세요. :) 마지막으로 아래 주의사항을 확인해주시기 바랍니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;a id=&quot;user-content-41-주의사항&quot; href=&quot;https://github.com/traeper/api_documentation/blob/master/README.md#41-%EC%A3%BC%EC%9D%98%EC%82%AC%ED%95%AD&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;4.1. 주의사항&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;배포 환경에 따라 바로 사용하기 어렵거나 수정이 필요할 수 있습니다. 제가 다니는 회사의 배포 파이프라인에 맞춰 구성한 예제이므로 참고 부탁드립니다.&lt;/li&gt;
&lt;li&gt;epages의 restdocs-api-spec이 아직 모든 REST Docs의 스펙을 100% 커버하지는 못하므로 깃헙에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://github.com/ePages-de/restdocs-api-spec/issues/172&quot;&gt;문의글을 남겨야 하는 경우&lt;/a&gt;가 있습니다. 물론 핵심 기능은 이상 없이 제공되니 큰 문제는 아닌 듯 하네요. :)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;a id=&quot;user-content-links&quot; href=&quot;https://github.com/traeper/api_documentation/blob/master/README.md#links&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;Links&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;원문 : &lt;a href=&quot;https://github.com/traeper/api_documentation&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;github&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://swagger.io/resources/articles/documenting-apis-with-swagger/&quot;&gt;https://swagger.io/resources/articles/documenting-apis-with-swagger/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://swagger.io/docs/open-source-tools/swagger-ui/usage/installation/&quot;&gt;Swagger UI Installation - Standalone Download&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/ePages-de/restdocs-api-spec&quot;&gt;Spring REST Docs 기반 OAS format generator - ePages-de/restdocs-api-spec&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://taetaetae.github.io/posts/a-combination-of-swagger-and-spring-restdocs/&quot;&gt;Swagger와 Spring Restdocs의 우아한 조합 (by OpenAPI Spec) - 꿈꾸는 태태태의 공간&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>API Documentation</category>
      <category>API Documentation</category>
      <category>ePages</category>
      <category>OAS</category>
      <category>openapi</category>
      <category>rest docs</category>
      <category>Restdocs</category>
      <category>Swagger</category>
      <author>jay.pg</author>
      <guid isPermaLink="true">https://traeper.tistory.com/219</guid>
      <comments>https://traeper.tistory.com/219#entry219comment</comments>
      <pubDate>Sun, 20 Feb 2022 00:47:41 +0900</pubDate>
    </item>
    <item>
      <title>[JPA] Inheritance bulk-id delete</title>
      <link>https://traeper.tistory.com/218</link>
      <description>&lt;h1&gt;JPA inheritance bulk-id strategies&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA에서 Inheritance를 사용한 경우 batch delete 구현 방법을 다뤄봅니다. Links에 있는 Hibernate post를 보시면 좀더 상세한 글을 확인할 수 있습니다. 깃헙 원문으로 가시면 실제 소스코드도 확인하실 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;테스트 환경
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;H2 DB&lt;/li&gt;
&lt;li&gt;MySQL 5.7.24&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;a id=&quot;user-content-class-상속-구성&quot; href=&quot;https://github.com/traeper/jpa_practice/blob/main/src/main/kotlin/com/traeper/jpa_practice/inheritance/bulk_delete/README.md#class-%EC%83%81%EC%86%8D-%EA%B5%AC%EC%84%B1&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;class 상속 구성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Person 클래스를 상속하는 Doctor, Student가 있습니다. JOINED Inheritance를 사용하였습니다.&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Entity(name = &quot;person&quot;)
@Inheritance(strategy = InheritanceType.JOINED)
open class PersonEntity(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long? = null,

    @Column(nullable = false)
    var name: String,

    @Column(nullable = false)
    var employed: Boolean,
)

@Entity(name = &quot;doctor&quot;)
class DoctorEntity(
    id: Long,
    name: String,
    employed: Boolean,
    @Column(nullable = false)
    val major: String,
) : PersonEntity(id, name, employed)

@Entity(name = &quot;student&quot;)
class StudentEntity(
    id: Long,
    name: String,
    employed: Boolean,
    var creditAverage: Double,
) : PersonEntity(id, name, employed)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;a id=&quot;user-content-bulk-deletion&quot; href=&quot;https://github.com/traeper/jpa_practice/blob/main/src/main/kotlin/com/traeper/jpa_practice/inheritance/bulk_delete/README.md#bulk-deletion&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;bulk deletion&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래처럼 쿼리문을 작성하면 batch delete를 수행하는데요. 이렇게 부모 클래스를 타겟으로 작성하면 bulk-id delete를 수행하게 됩니다.&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;entityManager.createQuery(&quot;delete from person where employed = :employed&quot;)
    .setParameter(&quot;employed&quot;, false)
    .executeUpdate()&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;a id=&quot;user-content-h2-테스트-결과-로그&quot; href=&quot;https://github.com/traeper/jpa_practice/blob/main/src/main/kotlin/com/traeper/jpa_practice/inheritance/bulk_delete/README.md#h2-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EA%B2%B0%EA%B3%BC-%EB%A1%9C%EA%B7%B8&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;H2 테스트 결과 로그&lt;/h3&gt;
&lt;div&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;// HT_person 테이블 생성 후 조회된 identifier 삽입
Hibernate: create cached local temporary table if not exists HT_person (id bigint not null) on commit drop transactional
Hibernate: insert into HT_person select personenti0_.id as id from person personenti0_ where personenti0_.employed=?
// batch delete 시작
Hibernate: delete from student where (id) IN (select id from HT_person)
Hibernate: delete from doctor where (id) IN (select id from HT_person)
Hibernate: delete from person where (id) IN (select id from HT_person)
Hibernate: delete from HT_person
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hibernate에서는 상속 관계에서 최적화를 위해 HT_를 prefix로 하는 identifier holding 용도의 임시 테이블을 생성하는 것이 기본 전략입니다. (global temporary table) 실제로 수행되는 쿼리들을 살펴보면 HT_person 임시 테이블을 만든 다음 테이블 갯수 만큼의 bulk delete를 수행하는 것을 볼 수 있습니다. 마지막으로 임시 테이블을 삭제하면서 끝나네요.&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;// H2Dialect.class
public MultiTableBulkIdStrategy getDefaultMultiTableBulkIdStrategy() {
    return new LocalTemporaryTableBulkIdStrategy(new IdTableSupportStandardImpl() {
        public String getCreateIdTableCommand() {
            return &quot;create cached local temporary table if not exists&quot;;
        }

        public String getCreateIdTableStatementOptions() {
            return &quot;on commit drop transactional&quot;;
        }
    }, AfterUseAction.CLEAN, TempTableDdlTransactionHandling.NONE);
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;create cache local temporary table ~은 H2Dialect에 포함되어 있음을 확인할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;a id=&quot;user-content-mysql-테스트-결과-로그&quot; href=&quot;https://github.com/traeper/jpa_practice/blob/main/src/main/kotlin/com/traeper/jpa_practice/inheritance/bulk_delete/README.md#mysql-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EA%B2%B0%EA%B3%BC-%EB%A1%9C%EA%B7%B8&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;mysql 테스트 결과 로그&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 mysql(5.7.24)에서도 global temporary table 전략은 잘 돌아가는 것을 확인헀습니다. 다만 데이터가 존재하지 않아도 delete 쿼리들은 항상 수행이 되네요.&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;Hibernate: create temporary table if not exists HT_person (id bigint not null) 
Hibernate: insert into HT_person select personenti0_.id as id from person personenti0_ where personenti0_.employed=?
Hibernate: delete from student where (id) IN (select id from HT_person)
Hibernate: delete from doctor where (id) IN (select id from HT_person)
Hibernate: delete from person where (id) IN (select id from HT_person)
Hibernate: drop temporary table HT_person
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;a id=&quot;user-content-임시-테이블을-만들-수-없는-경우-선택-가능한-방법&quot; href=&quot;https://github.com/traeper/jpa_practice/blob/main/src/main/kotlin/com/traeper/jpa_practice/inheritance/bulk_delete/README.md#%EC%9E%84%EC%8B%9C-%ED%85%8C%EC%9D%B4%EB%B8%94%EC%9D%84-%EB%A7%8C%EB%93%A4-%EC%88%98-%EC%97%86%EB%8A%94-%EA%B2%BD%EC%9A%B0-%EC%84%A0%ED%83%9D-%EA%B0%80%EB%8A%A5%ED%95%9C-%EB%B0%A9%EB%B2%95&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;임시 테이블을 만들 수 없는 경우 선택 가능한 방법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 DBMS에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://hibernate.atlassian.net/browse/HHH-11262&quot;&gt;HHH-11262 - Insufficient Privilege&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;등의 이유로 임시 테이블을 생성할 수 없을 때 사용가능한 전략 4가지가 있습니다. 다만 'IN' 절을 사용하는 전략은 Composite Identifiers만 지원하므로 PostgreSQL에서만 지원된다고 하네요. 실제로 사용하시는 DBMS에서 지원하는 전략을 선택해서 활용하시면 될 것 같습니다. 전략과 DBMS 궁합은 Reference 링크를 참고해주세요.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;InlineIdsInClauseBulkIdStrategy : PostgreSQL에서만 작동&lt;/li&gt;
&lt;li&gt;InlineIdsSubSelectValueListBulkIdStrategy : PostgreSQL에서만 작동&lt;/li&gt;
&lt;li&gt;InlineIdsOrClauseBulkIdStrategy : Oracle, SQL Server, MySQL, and PostgreSQL 모두 작동한다는 장점이 있음.&lt;/li&gt;
&lt;li&gt;CteValuesListBulkIdStrategy : CTE(Common Table Expressions) 지원해야하며 PostgreSQL에서만 작동&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;696&quot; data-origin-height=&quot;286&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bqcYLy/btrtfFtifvG/DacWSwLKRVag0D5F7TdHlk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bqcYLy/btrtfFtifvG/DacWSwLKRVag0D5F7TdHlk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bqcYLy/btrtfFtifvG/DacWSwLKRVag0D5F7TdHlk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbqcYLy%2FbtrtfFtifvG%2FDacWSwLKRVag0D5F7TdHlk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;696&quot; height=&quot;286&quot; data-origin-width=&quot;696&quot; data-origin-height=&quot;286&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 전략들은 모두 MultiTableBulkIdStrategy을 구현(implementation)하고 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;a id=&quot;user-content-inlineidsinclausebulkidstrategy&quot; href=&quot;https://github.com/traeper/jpa_practice/blob/main/src/main/kotlin/com/traeper/jpa_practice/inheritance/bulk_delete/README.md#inlineidsinclausebulkidstrategy&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;InlineIdsInClauseBulkIdStrategy&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Composite Identifiers가 아니다보니 H2 DB와 제 로컬 mysql 모두 아래처럼 UnsupportedOperationException이 발생해서 사용할 수 없네요.&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;The InlineIdsIdsInClauseDeleteHandlerImpl can only be used with Dialects that support IN clause row-value expressions (for composite identifiers)!
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;a id=&quot;user-content-inlineidssubselectvaluelistbulkidstrategy&quot; href=&quot;https://github.com/traeper/jpa_practice/blob/main/src/main/kotlin/com/traeper/jpa_practice/inheritance/bulk_delete/README.md#inlineidssubselectvaluelistbulkidstrategy&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;InlineIdsSubSelectValueListBulkIdStrategy&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;InlineIdsInClauseBulkIdStrategy와 마찬가지로 Composite Identifiers가 아니여서 동작하지 않습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;a id=&quot;user-content-inlineidsorclausebulkidstrategy&quot; href=&quot;https://github.com/traeper/jpa_practice/blob/main/src/main/kotlin/com/traeper/jpa_practice/inheritance/bulk_delete/README.md#inlineidsorclausebulkidstrategy&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;InlineIdsOrClauseBulkIdStrategy&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래처럼 select로 먼저 조건을 만족하는 id를 찾은 이후 delete를 수행하는 것을 확인할 수 있습니다. 만약 select 결과가 존재하지 않는다면 delete문은 수행하지 않는 것을 확인했습니다.&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;Hibernate: select personenti0_.id as id from person personenti0_ where personenti0_.employed=?
Hibernate: delete from student where (id = 2) or (id = 3)
Hibernate: delete from doctor where (id = 2) or (id = 3)
Hibernate: delete from person where (id = 2) or (id = 3)
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;a id=&quot;user-content-ctevalueslistbulkidstrategy&quot; href=&quot;https://github.com/traeper/jpa_practice/blob/main/src/main/kotlin/com/traeper/jpa_practice/inheritance/bulk_delete/README.md#ctevalueslistbulkidstrategy&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;CteValuesListBulkIdStrategy&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL5InnoDBDialect에서는 CTE가 지원되지 않아서 역시 UnsupportedOperationException이 발생하네요. 그런데 reference를 보니 역시 'IN' 절을 사용하기 때문에 PostgreSQL에서만 작동한다고 합니다.&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;mizar&quot;&gt;&lt;code&gt;UnsupportedOperationException: The CteValuesListDeleteHandlerImpl can only be used with Dialects that support CTE that can take UPDATE or DELETE statements as well!
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;a id=&quot;user-content-reference&quot; href=&quot;https://github.com/traeper/jpa_practice/blob/main/src/main/kotlin/com/traeper/jpa_practice/inheritance/bulk_delete/README.md#reference&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;links&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;원문 : &lt;a href=&quot;https://github.com/traeper/jpa_practice/blob/main/src/main/kotlin/com/traeper/jpa_practice/inheritance/bulk_delete/README.md&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;github&lt;/a&gt;&amp;nbsp;&lt;/li&gt;
&lt;li&gt;Hibernate post:&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://in.relation.to/2017/02/01/non-temporary-table-bulk-id-strategies/&quot;&gt;https://in.relation.to/2017/02/01/non-temporary-table-bulk-id-strategies/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>JPA(Hibernate)</category>
      <category>batch delete</category>
      <category>bulk-id</category>
      <category>inheritance</category>
      <category>joined</category>
      <category>JPA</category>
      <author>jay.pg</author>
      <guid isPermaLink="true">https://traeper.tistory.com/218</guid>
      <comments>https://traeper.tistory.com/218#entry218comment</comments>
      <pubDate>Mon, 14 Feb 2022 02:13:43 +0900</pubDate>
    </item>
    <item>
      <title>2020.11 순천만 습지</title>
      <link>https://traeper.tistory.com/217</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthContent&quot; data-filename=&quot;20201111_160910.jpg&quot; data-origin-width=&quot;4608&quot; data-origin-height=&quot;3456&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mjuHc/btqRI8Ny9vq/BR7bXov2wF657jJVPQh7xk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mjuHc/btqRI8Ny9vq/BR7bXov2wF657jJVPQh7xk/img.jpg&quot; data-alt=&quot;용산 전망대에서 촬영한 순천만 습지 사진&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mjuHc/btqRI8Ny9vq/BR7bXov2wF657jJVPQh7xk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmjuHc%2FbtqRI8Ny9vq%2FBR7bXov2wF657jJVPQh7xk%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;4608&quot; height=&quot;3456&quot; data-filename=&quot;20201111_160910.jpg&quot; data-origin-width=&quot;4608&quot; data-origin-height=&quot;3456&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;용산 전망대에서 촬영한 순천만 습지 사진&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthContent&quot; data-filename=&quot;20201111_154104.jpg&quot; data-origin-width=&quot;4032&quot; data-origin-height=&quot;3024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cu9Kxu/btqRTSpewqa/Hkt1fNOYoxCCWB8wodLM0k/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cu9Kxu/btqRTSpewqa/Hkt1fNOYoxCCWB8wodLM0k/img.jpg&quot; data-alt=&quot;순천만습지 - 용산 전망대가 있는 곳&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cu9Kxu/btqRTSpewqa/Hkt1fNOYoxCCWB8wodLM0k/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcu9Kxu%2FbtqRTSpewqa%2FHkt1fNOYoxCCWB8wodLM0k%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;4032&quot; height=&quot;3024&quot; data-filename=&quot;20201111_154104.jpg&quot; data-origin-width=&quot;4032&quot; data-origin-height=&quot;3024&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;순천만습지 - 용산 전망대가 있는 곳&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthContent&quot; data-filename=&quot;20201111_152453.jpg&quot; data-origin-width=&quot;4032&quot; data-origin-height=&quot;3024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/blH9lO/btqRL36dZOw/Q8WsQVB6RDWQNw2K07KuIK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/blH9lO/btqRL36dZOw/Q8WsQVB6RDWQNw2K07KuIK/img.jpg&quot; data-alt=&quot;순천만 습지 갈대숲&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/blH9lO/btqRL36dZOw/Q8WsQVB6RDWQNw2K07KuIK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FblH9lO%2FbtqRL36dZOw%2FQ8WsQVB6RDWQNw2K07KuIK%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;4032&quot; height=&quot;3024&quot; data-filename=&quot;20201111_152453.jpg&quot; data-origin-width=&quot;4032&quot; data-origin-height=&quot;3024&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;순천만 습지 갈대숲&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>잡동사니</category>
      <category>순천</category>
      <category>순천만 습지</category>
      <author>jay.pg</author>
      <guid isPermaLink="true">https://traeper.tistory.com/217</guid>
      <comments>https://traeper.tistory.com/217#entry217comment</comments>
      <pubDate>Wed, 30 Dec 2020 02:38:13 +0900</pubDate>
    </item>
  </channel>
</rss>