<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>빈&amp;rsquo;s 개발일기</title>
    <link>https://beaniejoy.tistory.com/</link>
    <description>잘못알고 있는 내용이 있을 수 있습니다. 언제나 좋은 피드백 감사합니다.</description>
    <language>ko</language>
    <pubDate>Fri, 8 May 2026 17:30:30 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>beaniejoy</managingEditor>
    <image>
      <title>빈&amp;rsquo;s 개발일기</title>
      <url>https://tistory1.daumcdn.net/tistory/3454935/attach/2183e50f7e6e4cafa6f31433cc5eb3f7</url>
      <link>https://beaniejoy.tistory.com</link>
    </image>
    <item>
      <title>Spring Boot 3 &amp;amp; Batch 5 버전에서 multi datasource 설정하기(batch meta table 분리하기)</title>
      <link>https://beaniejoy.tistory.com/110</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;개인프로젝트로 Spring Boot 3버전의 Spring Batch를 사용해보다가 최근에 실무에서 같은 3버전대의 스프링 신규 프로젝트에도 배치모듈을 적용할 일이 있어 신규 모듈을 만들게 되었습니다.&lt;br /&gt;&lt;br /&gt;개인프로젝트와 실무에서 Spring Boot 3 버전에서의 Spring Batch를 적용한 것들을 블로그에 정리하려고 합니다. 이번 게시글에서는 Spring Boot3 &amp;amp; Spring Batch 5버전에서 multi datasource를 어떻게 설정하는지에 대해 작성해보고자 합니다.&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;b&gt;1. Spring Boot 3 &amp;amp; Spring Batch 5 적용시 알아야할 내용&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2024-03-29 at 5.26.03 PM.png&quot; data-origin-width=&quot;1900&quot; data-origin-height=&quot;434&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yvrS6/btsGdP4GJ9L/y4DcAqsaJBkKayclvEVLzk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yvrS6/btsGdP4GJ9L/y4DcAqsaJBkKayclvEVLzk/img.png&quot; data-alt=&quot;spring boot 3.1.11-SNAPSHOT 버전에서의 spring batch 버전&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yvrS6/btsGdP4GJ9L/y4DcAqsaJBkKayclvEVLzk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyvrS6%2FbtsGdP4GJ9L%2Fy4DcAqsaJBkKayclvEVLzk%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;760&quot; height=&quot;174&quot; data-filename=&quot;Screenshot 2024-03-29 at 5.26.03 PM.png&quot; data-origin-width=&quot;1900&quot; data-origin-height=&quot;434&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;spring boot 3.1.11-SNAPSHOT 버전에서의 spring batch 버전&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot 3버전대에서는 Spring Batch가 5버전을 기본으로 사용하게 되었습니다. 이에 따라 기존에 사용하던 Spring Batch 방식에서 꽤나 많은 부분이 변경이 되었는데요. 그 중에 중요한 몇 개만 언급하려고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1-1. @EnableBatchProcessing은 이제 사용하지 않아요~&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Sprinb Boot 2.x 버전에서 spring batch를 사용했다면 &lt;b&gt;@EnableBatchProcessing&lt;/b&gt; 어노테이션은 필수로 붙여서 사용했습니다. 하지만 &lt;u&gt;&lt;b&gt;Spring Batch 5버전으로 올라오면서 해당 어노테이션 없이도 자동으로 batch 관련 설정이 되도록 바뀌었습니다.&lt;/b&gt;&lt;/u&gt;&lt;br /&gt;&lt;a href=&quot;https://docs.spring.io/spring-batch/reference/whatsnew.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.spring.io/spring-batch/reference/whatsnew.html&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1711701664949&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;What&amp;rsquo;s New in Spring Batch 5.1 :: Spring Batch&quot; data-og-description=&quot;Embracing JDK 21 LTS is one of the main themes for Spring Batch 5.1, especially the support of virtual threads from Project Loom. In this release, virtual threads can be used in all areas of the framework, like running a concurrent step with virtual thread&quot; data-og-host=&quot;docs.spring.io&quot; data-og-source-url=&quot;https://docs.spring.io/spring-batch/reference/whatsnew.html&quot; data-og-url=&quot;https://docs.spring.io/spring-batch/reference/whatsnew.html&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-batch/reference/whatsnew.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.spring.io/spring-batch/reference/whatsnew.html&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;What&amp;rsquo;s New in Spring Batch 5.1 :: Spring Batch&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Embracing JDK 21 LTS is one of the main themes for Spring Batch 5.1, especially the support of virtual threads from Project Loom. In this release, virtual threads can be used in all areas of the framework, like running a concurrent step with virtual thread&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.spring.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 custom한 설정을 통해 multi datasource 환경에서 어떤 datasource와 transactionManager를 batch 기본으로 설정해줄지에 대해서 고민이 될 수 있는데요. 그 때 &lt;b&gt;@EnableBatchProcessing 어노테이션의 관련 옵션들을 사용하라고 가이드해줍니다.&lt;/b&gt; &lt;br /&gt;(Spring Boot 3 버전대에서 이 설정도 사용하기 힘들어졌는데요 조금 있다가 언급하도록 하겠습니다.)&lt;/p&gt;
&lt;pre id=&quot;code_1711701828156&quot; class=&quot;kotlin&quot; data-ke-language=&quot;highlight.js&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
@EnableBatchProcessing(
    dataSourceRef = &quot;batchDataSource&quot;, 
    transactionManagerRef = &quot;batchTransactionManager&quot;
)
class BatchConfig {
	//...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 해당 어노테이션에서 제공해주는 properties 중에 dataSourceRef, transactionManagerRef를 사용하라고 가이드해주고 있습니다.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;그런데 Spring Boot에서 batch 관련 자동으로 설정해주는 BatchAutoConfiguration을 보면 @EnableBatchProcessing 어노테이션 관련 설정이 있는데요. 이부분을 잘 봐야 합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1713839734214&quot; class=&quot;java&quot; data-ke-language=&quot;highlight.js&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// BatchAutoConfiguration.java
@ConditionalOnMissingBean(value = DefaultBatchConfiguration.class, annotation = EnableBatchProcessing.class)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot autoconfigure에 있는 Batch 관련 자동 설정 클래스내용입니다. ConditionalMissingBean 어노테이션이 추가됐는데요. &lt;b&gt;@EnableBatchProcessing 혹은 DefaultBatchConfiguration을 사용해서 스프링 배치를 설정하게 되면 Spring Boot에서 batch 관련 자동설정을 해주지 않게 됩니다.&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;즉 @EnableBatchProcessing 어노테이션을 설정하면 스프링 부트는 BatchAutoConfiguration에 있는 Bean들을 자동으로 등록해주지 않습니다. 스프링 배치 실행을 위한 관련 Bean들(특히 Batch ApplicationRunner)이 등록되지 못했기 때문에 스프링 부트를 통해 배치를 실행해도 해당 배치는 실행되지 않게 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2024-04-24 at 4.52.46 PM.png&quot; data-origin-width=&quot;2950&quot; data-origin-height=&quot;522&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uUKSz/btsGTYUcdoB/APeewx8YeKgav7TyqYIVQ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uUKSz/btsGTYUcdoB/APeewx8YeKgav7TyqYIVQ0/img.png&quot; data-alt=&quot;@EnableBatchProcessing을 BatchConfig에 설정하고 배치 실행하면 위와 같이 스프링 배치는 실행되지 않습니다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uUKSz/btsGTYUcdoB/APeewx8YeKgav7TyqYIVQ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuUKSz%2FbtsGTYUcdoB%2FAPeewx8YeKgav7TyqYIVQ0%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;760&quot; height=&quot;134&quot; data-filename=&quot;Screenshot 2024-04-24 at 4.52.46 PM.png&quot; data-origin-width=&quot;2950&quot; data-origin-height=&quot;522&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;@EnableBatchProcessing을 BatchConfig에 설정하고 배치 실행하면 위와 같이 스프링 배치는 실행되지 않습니다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이와 관련해서 정보를 찾아보니&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;JobLauncherApplicationRunner을 따로 Bean으로 설정하면 된다는 글을 보긴 했는데요.&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;multi datasource로 구성된 환경에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;JobLauncherApplicationRunner 따로 등록하는 방식으로 해봐도 제대로 동작하지 않았습니다.&lt;br /&gt;&lt;br /&gt;아마 제가 잘 못 알고 있거나 잘 못 설정했을 가능성이 크긴 한데요. 무엇보다 BatchAutoConfiguration에 있는 Bean들을 따로 등록하고 하는 것들이 복잡하기도 하고 스프링 배치가 의도한 대로 동작하지 않을 있겠다는 생각을 하게 되어 저는 &lt;u&gt;Spring Batch 5 가이드에서 언급한 대로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;@EnableBatchProcessing&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;어노테이션을 사용하지 않았습니다.&lt;/u&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div&gt;&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1-2. spring batch meta table은 사용하는 것으로,,,&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;spring batch를 사용하면 당연히 meta table을 사용하는 거지 무조건 사용해야된다? 무슨 얘기인가 싶은 생각을 하셨을 수도 있습니다. 그런데 실무에서 spring batch 사용할 때 meta table을 아예 사용하지 않는 프로젝트들이 많이 있습니다. &lt;br /&gt;&lt;br /&gt;관리의 이슈와 spring batch meta table에서 데이터가 꼬이는 일로 배치 오류가 발생하지 않도록 본연의 배치 잡 실행에 집중하고자 meta table을 아예 사용하지 않은 것으로 생각했는데요.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;Spring Batch 5버전 이전에서는 meta table이 생성되지도, 사용하지도 않도록 하는 설정이 가능했습니다.&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1711702627938&quot; class=&quot;kotlin&quot; data-ke-language=&quot;highlight.js&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
@EnableBatchProcessing
class BatchConfig : DefaultBatchConfigurer() {
	
    override fun getTransactionManager(): PlatformTransactionManager {
        return ResourcelessTransactionManager()
    }

    override fun setDataSource(dataSource: DataSource) {
    }
    
    @Bean
    fun mapJobRepositoryFactory(
    	txManager: ResourcelessTransactionManager?
    ): MapJobRepositoryFactoryBean {
        val factory = MapJobRepositoryFactoryBean(txManager)
        factory.afterPropertiesSet()
        return factory
    }
    
    //...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DefaultBatchConfigurer를 상속받아 transactionManager를 ResourcelessTransactionManager로 설정하고 DataSource setter에는 아무것도 설정을 하지 않습니다. 그리고 &lt;b&gt;MapJobRepositoryFactoryBean&lt;/b&gt;을 제공했는데요.&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;MapJobRepositoryFactoryBean&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;A FactoryBean that automates the creation of a SimpleJobRepository using non-persistent in-memory DAO implementations.&amp;nbsp;&lt;br /&gt;This&amp;nbsp;repository&amp;nbsp;is&amp;nbsp;only&amp;nbsp;really&amp;nbsp;intended&amp;nbsp;for&amp;nbsp;use&amp;nbsp;in&amp;nbsp;testing&amp;nbsp;and&amp;nbsp;rapid&amp;nbsp;prototyping.&lt;br /&gt;...&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영속화되지 않는 인메모리 구현체라고 소개하고 있습니다. 이걸 통해 spring batch에 DataSource를 설정하지 않을 수 있었고 MapJobRepositoryFactoryBean를 통해 생성된 JobRepository는 batch job이 실행되어도 따로 spring batch metatable에 관련 정보들을 영속화하지 않게 됩니다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;하지만 Spring Batch 5버전부터는 이러한 것들이 deprecated 되면서 사실상 spring batch metatable은 필수 사항이 되었습니다.&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Deprecated as of v4.3 in favor or using the JobRepositoryFactoryBean with an in-memory database. &lt;br /&gt;Scheduled for removal in v5.0.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://stackoverflow.com/questions/76245029/how-to-disable-spring-batch-meta-data-in-spring-5-0-1&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://stackoverflow.com/questions/76245029/how-to-disable-spring-batch-meta-data-in-spring-5-0-1&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1713946508325&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;How to Disable Spring Batch Meta Data in spring 5.0.1&quot; data-og-description=&quot;I googled a lot and trying to follow this doc https://docs.spring.io/spring-batch/docs/5.0.1/reference/html/whatsnew.html#whatsNew But not able to find a way to disable spring batch meta data. Even I&quot; data-og-host=&quot;stackoverflow.com&quot; data-og-source-url=&quot;https://stackoverflow.com/questions/76245029/how-to-disable-spring-batch-meta-data-in-spring-5-0-1&quot; data-og-url=&quot;https://stackoverflow.com/questions/76245029/how-to-disable-spring-batch-meta-data-in-spring-5-0-1&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bq9HTX/hyVS2g5Qbj/c0IBOMfVFWatnFpFokvae1/img.png?width=316&amp;amp;height=316&amp;amp;face=0_0_316_316&quot;&gt;&lt;a href=&quot;https://stackoverflow.com/questions/76245029/how-to-disable-spring-batch-meta-data-in-spring-5-0-1&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://stackoverflow.com/questions/76245029/how-to-disable-spring-batch-meta-data-in-spring-5-0-1&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bq9HTX/hyVS2g5Qbj/c0IBOMfVFWatnFpFokvae1/img.png?width=316&amp;amp;height=316&amp;amp;face=0_0_316_316');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;How to Disable Spring Batch Meta Data in spring 5.0.1&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;I googled a lot and trying to follow this doc https://docs.spring.io/spring-batch/docs/5.0.1/reference/html/whatsnew.html#whatsNew But not able to find a way to disable spring batch meta data. Even I&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;stackoverflow.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;stackoverflow에도 이와 관련한 내용이 있었는데요. in-memory Map-based는 deprecated 되었고 JDBC 구현체를 사용해야 한다고 나와있는데요. 결국 spring batch meta table을 사용할 수 밖에 없다는 내용 같습니다.&lt;br /&gt;&lt;br /&gt;그럼에도 meta table을 사용하고 싶지 않다면 h2 database 같은 in memory db datasource를 따로 설정해서 spring batch 기본 datasource로 설정해서 사용하라는 글도 봤습니다. 방법이 될 수 있으나 제 개인적으로는 이렇게 사용하고 싶지 않다는 생각을 했습니다. (배치 실행때마다 in-memory h2 database를 띄워야 하고 관련 테이블 DDL 수행 후 배치를 실행해야 하는게 좀 걸렸습니다.)&lt;br /&gt;&lt;br /&gt;&lt;u&gt;&lt;b&gt;저는 그래서 meta table를 담는 batch용 database와 서비스 용도의 database를 따로 구성해서 2개의 datasource를 가지고 설정하게 되었습니다.&lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;div&gt;&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&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;b&gt;2. 프로젝트 설정&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배치 테스트용 프로젝트는 Spring Boot 3.2.2 버전, kotlin 1.9.22 버전, java 17 베이스로 구성했습니다.&lt;br /&gt;(&lt;a href=&quot;https://github.com/beaniejoy/back-overall-repository/blob/main/spring-boot-v3-batch-demo/build.gradle.kts&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;build.gradle.kts 설정 내용은 repository에서 확인하시면 될 것 같습니다.&lt;/a&gt;)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2-1. datasource 관련 설정&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2개의 DB를 사용할 것이기에 datasource 설정은 2개로 구분지어야 합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1713947200647&quot; class=&quot;yaml&quot; data-ke-language=&quot;highlight.js&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# application-local.yml
spring:
  datasource:
    service:
      pool-name: service-db
      driver-class-name: com.mysql.cj.jdbc.Driver
      jdbc-url: jdbc:mysql://localhost:3306/cafe?characterEncoding=utf8&amp;amp;serverTimezone=Asia/Seoul
      username: root
      password: beaniejoy
    batch:
      pool-name: batch-db
      driver-class-name: com.mysql.cj.jdbc.Driver
      jdbc-url: jdbc:mysql://localhost:3307/cafe?characterEncoding=utf8&amp;amp;serverTimezone=Asia/Seoul
      username: root
      password: beaniejoy&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;service용 db와 batch용 db에 대한 connection 정보를 설정하고 Config 파일에 Bean으로 등록해줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1713947592665&quot; class=&quot;kotlin&quot; data-ke-language=&quot;highlight.js&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration(proxyBeanMethods = false)
class DataSourceConfig {
    @Primary
    @Bean(BATCH_DATASOURCE)
    @ConfigurationProperties(prefix = &quot;spring.datasource.batch&quot;)
    fun batchDataSource(): DataSource {
        return DataSourceBuilder
            .create()
            .type(HikariDataSource::class.java)
            .build()
    }

    @Bean(SERVICE_DATASOURCE)
    @ConfigurationProperties(prefix = &quot;spring.datasource.service&quot;)
    fun serviceDataSource(): DataSource {
        return DataSourceBuilder
            .create()
            .type(HikariDataSource::class.java)
            .build()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 중요한 것이 &lt;b&gt;batch용 datasource bean에 @Primary를 붙여야 합니다.&lt;/b&gt; 안그러면 DataSource 타입으로 두 개의 빈이 등록되기 때문에 중복등록으로 실행과정에서 오류가 발생할 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2-2. TransactionManager 설정&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스용 datasource는 JpaTransactionManager에 설정해야하고 batch용 datasource는 JdbcTransactionManager에 설정했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1713948132982&quot; class=&quot;kotlin&quot; data-ke-language=&quot;highlight.js&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// BatchConfig
@Primary
@Bean(BATCH_TRANSACTION_MANAGER)
fun batchTransactionManager(
    @Qualifier(BATCH_DATASOURCE) batchDataSource: DataSource
): PlatformTransactionManager {
    return JdbcTransactionManager(batchDataSource)
}

// JpaConfig
// LocalContainerEntityManagerFactoryBean 따로 Bean 등록 필수
@Bean(SERVICE_TRANSACTION_MANAGER)
fun serviceTransactionManager(
    @Qualifier(SERVICE_ENTITY_MANAGER) serviceEntityManager: LocalContainerEntityManagerFactoryBean
): PlatformTransactionManager {
    return JpaTransactionManager().apply {
        this.entityManagerFactory = serviceEntityManager.`object`
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TransactionManager도 마찬가지로 batch에서 사용하는 Bean에 &lt;b&gt;@Primary&lt;/b&gt;를 붙여햐 합니다.&lt;/p&gt;
&lt;div&gt;&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2-3. Batch Job 등록 및 실행&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;datasource, transactionManager 관련 설정이 다 됐고 스프링 배치를 한 번 실행해보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1713948820401&quot; class=&quot;yaml&quot; data-ke-language=&quot;highlight.js&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# application.yml
spring:
  profiles:
    active: local

  batch:
    job:
      enabled: true
      name: ${job.name:NONE}
    jdbc:
      initialize-schema: always&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;yml 파일에 spring batch 관련 기본 설정을 먼저 해줍니다. &lt;br /&gt;&lt;b&gt;spring.batch.jdbc.initialize-schema&lt;/b&gt;는 spring batch meta table들을 실행할 때 자동으로 등록되도록 할 것인지에 대해 설정해주는 옵션입니다. 운영 환경에서는 되도록 never로 하는 것이 좋을 것 같습니다. (테스트를 위해 always로 하겠습니다.)&lt;/p&gt;
&lt;pre id=&quot;code_1713949585472&quot; class=&quot;kotlin&quot; data-ke-language=&quot;highlight.js&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
class TestJobConfig(
    private val jobRepository: JobRepository,
    private val transactionManager: PlatformTransactionManager
) {
    companion object : KLogging()

    @Bean
    fun testJob(
        simpleStep1: Step,
        simpleStep2: Step
    ): Job {
        return JobBuilder(&quot;testJob&quot;, jobRepository)
            .incrementer(RunIdIncrementer())
            .start(simpleStep1)
            .next(simpleStep2)
            .build()
    }

    @Bean
    fun simpleStep1(): Step {
        val testTasklet = Tasklet { _, _ -&amp;gt;
            logger.info { &quot;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; this is step1&quot; }
            RepeatStatus.FINISHED
        }

        return StepBuilder(&quot;simpleStep1&quot;, jobRepository)
            .tasklet(testTasklet, transactionManager)
            .build()
    }

    @Bean
    fun simpleStep2(): Step {
        val testTasklet = Tasklet { _, _ -&amp;gt;
            logger.info { &quot;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; this is step2&quot; }
            RepeatStatus.FINISHED
        }

        return StepBuilder(&quot;simpleStep2&quot;, jobRepository)
            .tasklet(testTasklet, transactionManager)
            .build()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 테스트용 배치 잡하나를 등록하고 program arguments에 &lt;b&gt;--job.name=testJob &lt;/b&gt;와 함께 애플리케이션 실행하면 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2024-04-24 at 6.11.21 PM.png&quot; data-origin-width=&quot;3118&quot; data-origin-height=&quot;734&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cUKVUe/btsGVbTay0U/hqEEKRTXtu2MQQdcH56KoK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cUKVUe/btsGVbTay0U/hqEEKRTXtu2MQQdcH56KoK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cUKVUe/btsGVbTay0U/hqEEKRTXtu2MQQdcH56KoK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcUKVUe%2FbtsGVbTay0U%2FhqEEKRTXtu2MQQdcH56KoK%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;760&quot; height=&quot;179&quot; data-filename=&quot;Screenshot 2024-04-24 at 6.11.21 PM.png&quot; data-origin-width=&quot;3118&quot; data-origin-height=&quot;734&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(저는 테스트한다고 배치 실행한 이력이 있어서 run.id가 6으로 나오고 있습니다.)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2024-04-24 at 6.12.20 PM.png&quot; data-origin-width=&quot;982&quot; data-origin-height=&quot;988&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bC9ffe/btsGT8P8flP/vJbVi0rTNqQN8PhAA5DQYK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bC9ffe/btsGT8P8flP/vJbVi0rTNqQN8PhAA5DQYK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bC9ffe/btsGT8P8flP/vJbVi0rTNqQN8PhAA5DQYK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbC9ffe%2FbtsGT8P8flP%2FvJbVi0rTNqQN8PhAA5DQYK%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;760&quot; height=&quot;765&quot; data-filename=&quot;Screenshot 2024-04-24 at 6.12.20 PM.png&quot; data-origin-width=&quot;982&quot; data-origin-height=&quot;988&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의도한 대로 batch용 DB에 spring batch meta table들이 생성되어 있고, 서비스용 DB에는 JPA Entity 내용이 그대로 테이블로 등록된 것을 볼 수 있습니다.&lt;br /&gt;&lt;br /&gt;(위의 코드에 대한 &lt;a href=&quot;https://github.com/beaniejoy/back-overall-repository/tree/main/spring-boot-v3-batch-demo&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Github Repository 링크&lt;/a&gt; 드립니다. 참고하세요.)&lt;/p&gt;
&lt;figure id=&quot;og_1713950475078&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;back-overall-repository/spring-boot-v3-batch-demo at main &amp;middot; beaniejoy/back-overall-repository&quot; data-og-description=&quot;  Back-end Study &amp;amp; Test Repository, which manages and tests various frameworks, libraries and modules, that consists of directories named by each topic. - beaniejoy/back-overall-repository&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/beaniejoy/back-overall-repository/tree/main/spring-boot-v3-batch-demo&quot; data-og-url=&quot;https://github.com/beaniejoy/back-overall-repository/tree/main/spring-boot-v3-batch-demo&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/LqSvP/hyVSYFMWoy/XGGmIYcD19BFpQ6s852Je1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/beaniejoy/back-overall-repository/tree/main/spring-boot-v3-batch-demo&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/beaniejoy/back-overall-repository/tree/main/spring-boot-v3-batch-demo&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/LqSvP/hyVSYFMWoy/XGGmIYcD19BFpQ6s852Je1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;back-overall-repository/spring-boot-v3-batch-demo at main &amp;middot; beaniejoy/back-overall-repository&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;  Back-end Study &amp;amp; Test Repository, which manages and tests various frameworks, libraries and modules, that consists of directories named by each topic. - beaniejoy/back-overall-repository&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&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>Spring</category>
      <category>SpringBatch</category>
      <category>springboot3</category>
      <author>beaniejoy</author>
      <guid isPermaLink="true">https://beaniejoy.tistory.com/110</guid>
      <comments>https://beaniejoy.tistory.com/110#entry110comment</comments>
      <pubDate>Wed, 24 Apr 2024 18:30:25 +0900</pubDate>
    </item>
    <item>
      <title>Spring Boot에 MySQL Replication datasource 설정하기(관련 property 병합하기)</title>
      <link>https://beaniejoy.tistory.com/109</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트에 DB Replication을 적용할 일이 생겼는데요. 여러 블로그 글을 참고하여 Spring Boot에 적용해보았던 내용을 기록해보고자 합니다. 이미 Spring Boot에 DB Replication을 적용하는 방법에 대해 자세하게 알려주는 글들이 많아서 의미가 있을지는 모르겠습니다만 약간 다른 방식으로 적용한 부분도 있어서 '지나가다가 참고해봐야지'하는 생각으로 봐주시면 좋을 것 같습니다.&lt;br /&gt;&lt;br /&gt;환경은 kotiln에 Spring Boot 3버전이고 DB는 MySQL을 사용하였습니다.&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;b&gt;1. 필요한 gradle 설정&lt;/b&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1706776353961&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;plugins {
    // noarg, allOpen
    kotlin(&quot;plugin.jpa&quot;).version(&quot;1.9.20&quot;)
}

noArg {
    annotation(&quot;jakarta.persistence.Entity&quot;)
    annotation(&quot;jakarta.persistence.Embeddable&quot;)
    annotation(&quot;jakarta.persistence.MappedSuperclass&quot;)
}

allOpen {
    annotation(&quot;jakarta.persistence.Entity&quot;)
    annotation(&quot;jakarta.persistence.Embeddable&quot;)
    annotation(&quot;jakarta.persistence.MappedSuperclass&quot;)
}

dependencies {

    implementation(&quot;org.springframework.boot:spring-boot-starter-data-jpa&quot;)

    runtimeOnly(&quot;com.h2database:h2&quot;)
    runtimeOnly(&quot;com.mysql:mysql-connector-j:${Version.Deps.MYSQL}&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 spring data jpa를 사용할 것이기에 관련 gradle 설정을 해두었고 당연히 MySQL 드라이버 설정도 있어야 합니다. (H2는 덤으로)&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;b&gt;2. property yaml 파일 설정&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 가장 중요한데요. source, replica database 두 개를 사용할 것이기에 당연히 datasource 설정도 두 개 해야합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1707792428603&quot; class=&quot;yaml&quot; data-ke-language=&quot;highlight.js&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# application-local.yaml

spring:
  datasource:
    source:
      hikari:
        poolName: source-pool
        driverClassName: com.mysql.cj.jdbc.Driver
        jdbcUrl: jdbc:mysql://localhost:3306/prayerhouse?characterEncoding=utf8&amp;amp;serverTimezone=Asia/Seoul
        username: copebble
        password: copebble
        maximumPoolSize: 5
        minimumIdle: 5
    replica:
      hikari:
        poolName: replica-pool
        driverClassName: com.mysql.cj.jdbc.Driver
        jdbcUrl: jdbc:mysql://localhost:3307/prayerhouse?characterEncoding=utf8&amp;amp;serverTimezone=Asia/Seoul
        username: copebble
        password: copebble
        maximumPoolSize: 5
        minimumIdle: 5&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;local 환경에 대한 설정을 할 것이라서요. local profile에만 적용되도록 &lt;b&gt;application-local.yaml&lt;/b&gt; 파일에 두 개의 datasource 연결 설정을 해두었습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1707792506440&quot; class=&quot;yaml&quot; data-ke-language=&quot;highlight.js&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# application.yaml

spring:
  datasource:
    common:
      hikari:
        connectionTimeout: 3000
        validationTimeout: 1000
        maxLifetime: 1800000 # 30 minutes (default)
        connectionTestQuery: SELECT 1
        dataSource:
          autoReconnect: true
          cachePrepStmts: true
          prepStmtCacheSize: 250
          prepStmtCacheSqlLimit: 2048
          useServerPrepStmts: true
          connectTimeout: 2000
          socketTimeout: 5000
          
  jpa:
    open-in-view: true
    hibernate:
      ddl-auto: validate
    properties:
      hibernate:
        format_sql: true
        show_sql: false
        default_batch_fetch_size: 100
        use_sql_comments: true&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 외에 공통적으로 datasource에 설정할 옵션들을 설정했는데요. common이라는 이름으로 했습니다. Spring Boot는 기본적으로 HikariCP를 datasource로 사용하고 있기 때문에 관련 설정 옵션들은 HikariCP github repo에서 확인할 수 있습니다. &lt;br /&gt;(이부분은 이번 주제에서 넘어선 부분이라 생략하겠습니다.)&lt;br /&gt;&lt;br /&gt;그리고 위에 공통적으로 확인할 수 있는&amp;nbsp;&lt;b&gt;hikari:&lt;/b&gt; 이름은 제가 hikariCP를 사용하고 있음을 명시적으로 보여주기 위해 그냥 넣어둔 것입니다. 아래에 config file 설정내용이 나오겠지만 사실 없어도 되는 부분입니다. 이부분은 자유롭게 뺄지 말지 결정하시면 될 것 같습니다.&lt;br /&gt;&lt;br /&gt;hikari 설정 말고도 jpa 관련 기본 설정들도 해뒀습니다. 이부분은 아래 Jpa 설정에서 다시 언급될 예정입니다.&lt;/p&gt;
&lt;div&gt;&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;&lt;/div&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;b&gt;3. Configuration 설정&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 우선 위의 datasource 설정 내용들을 클래스 파일로 load하기 위해 &lt;b&gt;ConfigurationProperties&lt;/b&gt;를 사용했습니다. 여기서 필드로 받아야하는 부분은 세 부분이 될 것 같습니다(&lt;b&gt;common, source, replica&lt;/b&gt;).&lt;/p&gt;
&lt;pre id=&quot;code_1707792904801&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@ConfigurationProperties(prefix = &quot;spring.datasource&quot;)
data class CustomDataSourceProperties(
    val common: HikariProperties,
    val source: HikariProperties,
    val replica: HikariProperties
) {
	
    fun getSourceWithCommon(): Properties {
        return ConfigUtil.mergeProperties(
            common.hikari,
            source.hikari
        )
    }

    fun getReplicaWithCommon(): Properties {
        return ConfigUtil.mergeProperties(
            common.hikari,
            replica.hikari
        )
    }
}

data class HikariProperties(
    val hikari: Properties
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각각 필드명을 common, source, replica으로 하고 (common, source) / (common, replica) 이렇게 쌍으로 properties를 merge하는 메소드는 만들어 둡니다.&lt;/p&gt;
&lt;pre id=&quot;code_1707890705727&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(CustomDataSourceProperties::class)
class DataSourceConfig(
    private val dataSourceProperties: CustomDataSourceProperties
) {

    @Bean(SOURCE_DATASOURCE)
    fun sourceDataSource(): DataSource {
        val sourceConfig = HikariConfig(dataSourceProperties.getSourceWithCommon())

        return HikariDataSource(sourceConfig)
    }

    @Bean(REPLICA_DATASOURCE)
    fun replicaDataSource(): DataSource {
        val replicaConfig = HikariConfig(dataSourceProperties.getReplicaWithCommon())

        return HikariDataSource(replicaConfig)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;@EnableConfigurationProperties&lt;/b&gt;를 통해 따로 만들어둔 Properties 클래스 파일로 설정정보들을 가져올 수 있도록 합니다.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;source와 replica에 대한 datasource bean을 각각 HikariDataSource 객체로 만들어 설정합니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1707891297056&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 흔히 사용하는 datasource 설정 방식(여기서는 사용하지 않음)
@Bean(SOURCE_DATASOURCE)
@ConfigurationProperties(prefix = &quot;spring.datasource.source.hikari&quot;)
public DataSource sourceDataSource() {
return DataSourceBuilder
    .create()
    .type(HikariDataSource.class)
    .build();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 코드는 Spring Boot Replication으로 검색하면 가장 많이 나오는 DataSource 설정 내용인데요. 저는 hikari 설정 property에서 &lt;b&gt;common 내용과 source, replica 설정 내용을 merge를 해야하는 상황인데&lt;/b&gt;&amp;nbsp;위의 방식으로는 원하는 방식으로 datasource 설정이 안되거나 이상하게 설정이 되는 현상이 발생되어 따로 &lt;b&gt;DataSourceBuilder 도움 없이 직접 HikariDataSource 객체를 생성하는 방식을 사용하였습니다.&lt;/b&gt;&amp;nbsp;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1707891588475&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean(ROUTING_DATASOURCE)
fun routingDataSource(
    @Qualifier(SOURCE_DATASOURCE) sourceDataSource: DataSource,
    @Qualifier(REPLICA_DATASOURCE) replicaDataSource: DataSource
): DataSource {
    val dataSourceMap = mapOf(
        DbType.SOURCE to sourceDataSource,
        DbType.REPLICA to replicaDataSource
    )

    return RoutingDataSource().apply {
        this.setTargetDataSources(dataSourceMap.toMap())
        this.setDefaultTargetDataSource(sourceDataSource)
    }
}

class RoutingDataSource : AbstractRoutingDataSource() {
    override fun determineCurrentLookupKey(): Any {
        val isTxReadOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly()

        return DbType.valueOfReadOnly(isTxReadOnly)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각각 설정한 source, replica datasource bean 객체를 가져와서 Map 형식으로 구조체를 하나 만들어둡니다.&lt;br /&gt;그 다음 AbstractRoutingDataSource를 이용해 어떤 datasource를 사용할 지 결정해주는 메소드를 overriding하여 구현합니다.&lt;br /&gt;&lt;br /&gt;구현내용은 Transactional에 readOnly 속성이 true인 경우에 slave를 바라보도록 하는 내용입니다. DBType은 &lt;b&gt;SOURCE, REPLICA&lt;/b&gt; 구분을 위해 Enum 클래스로 따로 만들었습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1708503059997&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private enum class DbType(
    private val readOnly: Boolean
) {
    SOURCE(false), REPLICA(true);

    companion object {
        fun valueOfReadOnly(isReadOnly: Boolean): DbType {
            return entries.find { it.readOnly == isReadOnly }
                ?: throw DatabaseException(&quot;Not Found DB(${entries.joinToString { it.name }}) type&quot;)
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 source, replica에 대한 dataSource와 두 개를 라우팅해주는 routingDataSource bean까지 생성했다면 실제 &lt;b&gt;@Transactional(readOnly=true)&lt;/b&gt;인 function을 호출했을 때 SLAVE를 바라보도록 해야되는데요. 이를 위해 &lt;b&gt;LazyConnectionDataSourceProxy &lt;/b&gt;클래스에 위의 routingDataSource bean을 등록해야 합니다. 이것에 대해 자세히 설명해주는 글이 있어 링크를 드리겠습니다. (&lt;a href=&quot;https://chagokx2.tistory.com/103&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://chagokx2.tistory.com/103&lt;/a&gt;)&lt;/p&gt;
&lt;figure id=&quot;og_1708505181152&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;프록시 객체와 지연 로딩으로 DataSource 분기 처리 실패 해결하기&quot; data-og-description=&quot;프록시 객체와 지연 로딩으로 DataSource 분기 처리 실패 해결하기 Overview 저번 글에서는 부하 분산을 위해 MySQL Replication 구성을 사용했고, 어플리케이션에서는 쿼리 요청에 따라 마스터 서버와 슬&quot; data-og-host=&quot;chagokx2.tistory.com&quot; data-og-source-url=&quot;https://chagokx2.tistory.com/103&quot; data-og-url=&quot;https://chagokx2.tistory.com/103&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/c7vFpo/hyVm3UFvYG/AtwD8AZx09CjIjD2B0XxJK/img.png?width=800&amp;amp;height=216&amp;amp;face=0_0_800_216,https://scrap.kakaocdn.net/dn/bgKYnW/hyVmZrcXJ6/NaquLAaJcHHQgrxjFBgUh1/img.png?width=800&amp;amp;height=216&amp;amp;face=0_0_800_216&quot;&gt;&lt;a href=&quot;https://chagokx2.tistory.com/103&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://chagokx2.tistory.com/103&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/c7vFpo/hyVm3UFvYG/AtwD8AZx09CjIjD2B0XxJK/img.png?width=800&amp;amp;height=216&amp;amp;face=0_0_800_216,https://scrap.kakaocdn.net/dn/bgKYnW/hyVmZrcXJ6/NaquLAaJcHHQgrxjFBgUh1/img.png?width=800&amp;amp;height=216&amp;amp;face=0_0_800_216');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;프록시 객체와 지연 로딩으로 DataSource 분기 처리 실패 해결하기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;프록시 객체와 지연 로딩으로 DataSource 분기 처리 실패 해결하기 Overview 저번 글에서는 부하 분산을 위해 MySQL Replication 구성을 사용했고, 어플리케이션에서는 쿼리 요청에 따라 마스터 서버와 슬&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;chagokx2.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론은 외부 클래스에서 다른 클래스의 @Transactional 어노테이션이 있는 function 호출시 DataSource 설정된 Connection Pool에서 connection 을 가져오게 되는데요. SOURCE, REPLICA를 결정할 readOnly 옵션 값이 동기화 안된 상태로 먼저 connection을 가져오게 되는 문제가 발생하게 됩니다. &lt;br /&gt;&lt;br /&gt;readOnly 옵션 체크 이후 실제 getConnection 하도록 Proxy를 이용해서 지연해주는 것이 LazyConnectionDataSourceProxy라고 합니다. (자세한 설명은 위의 링크에 있으니 참고하시면 될 것 같습니다.)&lt;/p&gt;
&lt;pre id=&quot;code_1708505084990&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Primary
@Bean(APPLICATION_DATA_SOURCE)
fun applicationDataSource(
    @Qualifier(ROUTING_DATA_SOURCE) routingDataSource: DataSource
): DataSource {
    return LazyConnectionDataSourceProxy(routingDataSource)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 DataSource 설정 완료입니다.&lt;/p&gt;
&lt;div&gt;&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;&lt;/div&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;b&gt;4. JPA 관련 설정&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에 DataSource 설정까지 했고 @Primary를 LazyConnectionDataSourceProxy로 생성된 DataSource bean에 설정했다면 의도한대로 동작할 것입니다. &lt;br /&gt;여기서 더 나아가 프로젝트에서 관리하고 있는 모든 JPA Repository에 대해 위에서 설정한 ApplicationDataSource를 바라보도록 transactionManager를 설정하고자 합니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1708590827520&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration(proxyBeanMethods = false)
@EnableJpaRepositories(
    basePackages = [&quot;io.copebble.prayerhouse.db.**.repository&quot;],
    entityManagerFactoryRef = APP_ENTITY_MANAGER
)
class JpaConfig(
    private val jpaProperties: JpaProperties,
    private val hibernateProperties: HibernateProperties
) {

    @Primary
    @Bean(APP_ENTITY_MANAGER)
    fun appEntityManager(
        @Qualifier(APPLICATION_DATA_SOURCE) applicationDataSource: DataSource
    ): LocalContainerEntityManagerFactoryBean {
        val mergedJpaProperties = Properties().apply {
            this.putAll(jpaProperties.properties)
            this[SchemaToolingSettings.HBM2DDL_AUTO] = hibernateProperties.ddlAuto
        }

        return LocalContainerEntityManagerFactoryBean().apply {
            this.jpaVendorAdapter = HibernateJpaVendorAdapter()
            this.setJpaProperties(mergedJpaProperties)
            this.dataSource = applicationDataSource
            this.persistenceUnitName = &quot;prayerApp&quot;
            this.setPackagesToScan(&quot;io.copebble.prayerhouse.db.*.entity&quot;)
        }
    }
    
    //...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 jpa의 근간이 되는 EntityManager를 설정하기 위해 EntityManagerFactory를 등록해줍니다.&lt;br /&gt;등록되었던 &lt;b&gt;applicationDataSource&lt;/b&gt; bean을 EntityManagerFactory에 적용하기 위해 인자로 가져옵니다.&lt;br /&gt;&lt;br /&gt;LocalContainerEntityManagerFactoryBean을 &lt;b&gt;appEntityManager&lt;/b&gt;라는 custom한 이름의 빈으로 등록할 것이기에 위에 &lt;b&gt;@EnableJpaRepositories&lt;/b&gt; 어노테이션을 이용해 jpa repository가 있는 패키지에 해당 bean 이름으로 EntityManager를 등록하겠다는 설정을 따로 해야 합니다.&lt;br /&gt;(만약에 entityManagerFactoryRef를 따로 설정하지 않는다면 기본 값으로 entityManager라는 이름의 빈을 찾게 되기 때문에 위에 설정내용에서는 런타임 오류가 발생하게 될 것입니다.)&lt;br /&gt;&lt;br /&gt;또한 JpaConfig의 생성자 인자로 JpaProperties와 HibernateProperties를 받고 있는데요.&lt;/p&gt;
&lt;pre id=&quot;code_1708592074301&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@ConfigurationProperties(prefix = &quot;spring.jpa&quot;)
@ConfigurationProperties(&quot;spring.jpa.hibernate&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;게시글 상단에서 &lt;b&gt;application.yaml 설정파일에 적어두었던 spring.jpa, spring.jpa.hibernate 속성값들을 가져오고 있습니다.&lt;/b&gt; custom하게 LocalContainerEntityManagerFactoryBean를 등록할 때 jpa 관련 properties도 따로 등록해야 했기 때문에 인자로 받아서 위의 내용대로 properties merge한 후 설정해줬습니다.&lt;br /&gt;&lt;br /&gt;주의해야할 점은 ddl-auto 설정은 우리가 익히 알고있던 spring.jpa.hibernate.ddl-auto 그대로 properties에 담으면 안되고 &lt;b&gt;hibernate.hbm2ddl.auto&lt;/b&gt; 이름으로 등록해야 합니다. (SchemaToolingSettings.HBM2DDL_AUTO 상수값을 사용했습니다.)&lt;/p&gt;
&lt;pre id=&quot;code_1708591815272&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean(APP_TRANSACTION_MANAGER)
fun appTransactionManager(
    @Qualifier(APP_ENTITY_MANAGER)
    appEntityManager: LocalContainerEntityManagerFactoryBean
): PlatformTransactionManager {
    return JpaTransactionManager().apply {
        this.entityManagerFactory = appEntityManager.`object`
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 다음 같은 설정 클래스에 TransactionManager bean을 생성해주는데요. 방금 bean으로 따로 등록했던 EntityManagerFactory에 대한 bean을 인자로 주입받아 설정해줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1708594385686&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration(proxyBeanMethods = false)
@EnableJpaRepositories(
    basePackages = [&quot;io.copebble.prayerhouse.db.**.repository&quot;],
    entityManagerFactoryRef = APP_ENTITY_MANAGER,
    transactionManagerRef = APP_TRANSACTION_MANAGER
)
class JpaConfig(
    private val jpaProperties: JpaProperties,
    private val hibernateProperties: HibernateProperties
) {

	//...
    
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JpaConfig @EnableJpaRepositories로 돌아와서 transactionManagerRef에 방금 등록했던 TransactionManager bean을 등록해줍니다.&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;b&gt;5. 확인&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2024-02-22 at 6.47.58 PM.png&quot; data-origin-width=&quot;2908&quot; data-origin-height=&quot;1038&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjXh8b/btsFdV5J5GL/yxcFqp0Yvd8InkK0l6ARP1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjXh8b/btsFdV5J5GL/yxcFqp0Yvd8InkK0l6ARP1/img.png&quot; data-alt=&quot;source datasource&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjXh8b/btsFdV5J5GL/yxcFqp0Yvd8InkK0l6ARP1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbjXh8b%2FbtsFdV5J5GL%2FyxcFqp0Yvd8InkK0l6ARP1%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;760&quot; height=&quot;271&quot; data-filename=&quot;Screenshot 2024-02-22 at 6.47.58 PM.png&quot; data-origin-width=&quot;2908&quot; data-origin-height=&quot;1038&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;source datasource&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2024-02-22 at 6.48.35 PM.png&quot; data-origin-width=&quot;2810&quot; data-origin-height=&quot;1058&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/btphez/btsFcEcgPhz/GUkulwkk6PXN81bEGMq5PK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/btphez/btsFcEcgPhz/GUkulwkk6PXN81bEGMq5PK/img.png&quot; data-alt=&quot;replica datasource&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/btphez/btsFcEcgPhz/GUkulwkk6PXN81bEGMq5PK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbtphez%2FbtsFcEcgPhz%2FGUkulwkk6PXN81bEGMq5PK%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;760&quot; height=&quot;286&quot; data-filename=&quot;Screenshot 2024-02-22 at 6.48.35 PM.png&quot; data-origin-width=&quot;2810&quot; data-origin-height=&quot;1058&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;replica datasource&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션 실행시 나오는 HikrariConfig 내용을 보면 두 개의 dataSource가 정상적으로 생성된 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2024-02-22 at 6.47.01 PM.png&quot; data-origin-width=&quot;2002&quot; data-origin-height=&quot;566&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dq69OY/btsFaM94Uoc/V02skvHYLtxVkMub3CSKk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dq69OY/btsFaM94Uoc/V02skvHYLtxVkMub3CSKk1/img.png&quot; data-alt=&quot;@Transactional 서비스 로직 수행시&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dq69OY/btsFaM94Uoc/V02skvHYLtxVkMub3CSKk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdq69OY%2FbtsFaM94Uoc%2FV02skvHYLtxVkMub3CSKk1%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;760&quot; height=&quot;215&quot; data-filename=&quot;Screenshot 2024-02-22 at 6.47.01 PM.png&quot; data-origin-width=&quot;2002&quot; data-origin-height=&quot;566&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;@Transactional 서비스 로직 수행시&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2024-02-22 at 6.46.27 PM.png&quot; data-origin-width=&quot;1946&quot; data-origin-height=&quot;606&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/SP9gP/btsE96nvufR/Opab0tviMSfTk1elZkBqL1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/SP9gP/btsE96nvufR/Opab0tviMSfTk1elZkBqL1/img.png&quot; data-alt=&quot;@Transactional(readOnly=true) 서비스 로직 수행시&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/SP9gP/btsE96nvufR/Opab0tviMSfTk1elZkBqL1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSP9gP%2FbtsE96nvufR%2FOpab0tviMSfTk1elZkBqL1%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;760&quot; height=&quot;237&quot; data-filename=&quot;Screenshot 2024-02-22 at 6.46.27 PM.png&quot; data-origin-width=&quot;1946&quot; data-origin-height=&quot;606&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;@Transactional(readOnly=true) 서비스 로직 수행시&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Transactional과 @Transactional(readOnly=true)에 대해 어떤 Connection Pool에서 DataSource를 가져오게 되는지 설정클래스에서 로그를 찍어봤을 때 위와 같이 의도한 대로 잘 수행된 것을 확인할 수 있습니다.&lt;/p&gt;</description>
      <category>Spring</category>
      <category>db</category>
      <category>hikraicp</category>
      <category>JPA</category>
      <category>MySQL</category>
      <category>Replication</category>
      <category>Spring</category>
      <category>Spring Boot</category>
      <author>beaniejoy</author>
      <guid isPermaLink="true">https://beaniejoy.tistory.com/109</guid>
      <comments>https://beaniejoy.tistory.com/109#entry109comment</comments>
      <pubDate>Fri, 23 Feb 2024 13:55:10 +0900</pubDate>
    </item>
    <item>
      <title>Spring Boot + kotlin 프로젝트에 ktlint 적용하기 (Multi module 통합 관리하기)</title>
      <link>https://beaniejoy.tistory.com/108</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이번 게시글에서는 ktlint를 사용해보고 적용했던 내용들을 간단하게 정리해보고자 합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;  간단한 Overview&lt;/li&gt;
&lt;li&gt;  spring boot, kotlin 프로젝트에서 ktlint 설정&lt;/li&gt;
&lt;li&gt;  멀티모듈 환경에서 ktlint 설정&lt;/li&gt;
&lt;li&gt;  git commit시 ktlint check 설정&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 내용을 가지고 ktlint를 어떻게 적용했는지 한 번 알아보겠습니다.&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;b&gt;1. 간단한 Overview&lt;/b&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;KtLint is a linter tool for checking and enforcing code style conventions for Kotlin programming language.&lt;br /&gt;(https://medium.com/@naeem0313/configuring-and-running-ktlin-on-android-studio-752413e7f6c2)&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ktlint는 개발자들이 코틀린 코드에 대한 컨벤션을 지키도록 체크하고 강제할 수 있도록 도와주는 툴입니다.&lt;/b&gt;&lt;br /&gt;코드 컨벤션은 왜 필요할까요. 이 부분에 대해서 개인적으로 크게 생각을 하지 않았었는데 최근들어서 필요성을 느끼고 있습니다. &lt;br /&gt;&lt;br /&gt;여러 개발자들이 하나의 프로젝트에 개발한 내용을 가지고 커밋하고 머지하는 과정에서 코드가 지저분해지는 것은 당연합니다. 처음 프로젝트를 구성하고 개발을 시작할 때에는 팀 내에 나름 코드 작성 규칙도 정해보고 잘 지켜지는 것처럼 보일 수 있습니다. &lt;br /&gt;&lt;br /&gt;하지만 시간이 지날 수록 코드에 먼지(?)가 쌓이기 시작합니다. 간편하게 코드 보라고 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;한 줄에&lt;span&gt; 코드를 압축해놓아&lt;/span&gt;&lt;/span&gt; 가로 스크롤 해야 뒷 부분을 볼 수 있는 것들도 있고, 과거의 추억을 회상하고자 몇 년째 안쓰는 코드들 그대로 남겨놓아서 IntelliJ로 보면 곳곳에 회색처리된 부분들도 많이 보이기 시작합니다. 이외에도 먼지가 쌓인 코드들이 많아지게 되는데요.&lt;br /&gt;&lt;br /&gt;얼핏 생각해보면 이런 것들 뭐 별거 아니라고 생각이 들 수도 있습니다. 그런데 프로젝트가 정말 복잡해지고 오류 찾아내려고 역으로 코드를 추적하거나 로직 파악하는데 있어서 이러한 먼지들이 상당히 거슬립니다. 여러 개발자들이 각자의 스타일을 가지고 개발을 하게 되면 코드만 가지고 분석할 때에도 꽤나 시간을 잡아먹게 됩니다.&lt;br /&gt;&lt;br /&gt;이러한 것들을 방지해주고 일관성있게 프로젝트를 관리할 수 있게 해주는 것이 바로 Linter인데요. Linter 뜻을 찾아보니 보푸라기 제거제라고 나오더라고요. &lt;b&gt;코드에 붙어있는 지저분한 먼지나 보푸라기들을 제거해준다고 해서 Linter라고 부르는 것 같습니다.&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;여러 언어들을 지원해주는 Linter들이 존재하는데요. 그 중 kotlin 언어의 linter, ktlint에 대해서 어떻게 설정하는지 알아보도록 하겠습니다.&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;b&gt;2. spring boot, kotlin 프로젝트에서 ktlint 설정&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ktlint는 기본적으로 &lt;a href=&quot;https://github.com/pinterest/ktlint&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;pinterest의 오픈소스 프로젝트&lt;/a&gt;를 가장 많이 사용합니다.&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1705449433464&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - pinterest/ktlint: An anti-bikeshedding Kotlin linter with built-in formatter&quot; data-og-description=&quot;An anti-bikeshedding Kotlin linter with built-in formatter - GitHub - pinterest/ktlint: An anti-bikeshedding Kotlin linter with built-in formatter&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/pinterest/ktlint&quot; data-og-url=&quot;https://github.com/pinterest/ktlint&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bcLFCw/hyU5E1RcLM/jkeAkFZqtKg9V9Rd9QRonk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/pinterest/ktlint&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/pinterest/ktlint&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bcLFCw/hyU5E1RcLM/jkeAkFZqtKg9V9Rd9QRonk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - pinterest/ktlint: An anti-bikeshedding Kotlin linter with built-in formatter&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;An anti-bikeshedding Kotlin linter with built-in formatter - GitHub - pinterest/ktlint: An anti-bikeshedding Kotlin linter with built-in formatter&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;github readme 페이지에서 여러 장점들을 소개하고 있는데요. 그 중에 개인적으로 큰 장점으로 다가왔던 두 부분에 대해서 말씀드리자면, &lt;b&gt;첫 번째로, &lt;a href=&quot;https://kotlinlang.org/docs/coding-conventions.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;코틀린 공식 document의 code convention&lt;/a&gt;을 기반으로 한다는 점이 있습니다.&lt;/b&gt; ktlint는 공식 문서의 code convention보다 좀 더 타이트한 컨벤션 rule을 가지고 있는 것 같은데요. 이 부분이 저에게는 큰 장점으로 다가왔습니다.&lt;br /&gt;&lt;br /&gt;두 번째로 &lt;b&gt;.editorconfig&lt;/b&gt; 파일을 지원한다는 것입니다. ktlint에 내장되어 있는 built in rule들이 존재하는데요. 개인 프로젝트를 하거나 팀 내에서 코드 컨벤션을 정할 때 원치 않은 rule들이 있거나 설정된 숫자값들이 분명 있습니다. 그 때 해당 프로젝트 루트 경로에 &lt;b&gt;.editorconfig&lt;/b&gt; 파일을 만들고 거기에 커스텀하게 rule들을 설정할 수 있습니다. ktlint는 이러한 것도 같이 적용할 수 있도록 지원해주고 있어서 이부분도 큰 장점으로 다가왔습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2-1. gradle plugin 설정&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 kotlin DSL이 적용된 build.gradle.kts 파일에 ktlint를 적용해보았습니다. &lt;br /&gt;(groovy DSL에 대한 ktlint 설정 내용도 아주 친절하게 나와있으니 구글링 조금만 해보시면 바로 나올 거예요)&lt;br /&gt;&lt;br /&gt;gradle plugin으로 적용한 것은 &lt;a href=&quot;https://github.com/JLLeitschuh/ktlint-gradle&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;b&gt;JLLeitschuh/ktlint-gradle&lt;/b&gt;&lt;/a&gt; 플러그인 입니다.&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1705454253664&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - JLLeitschuh/ktlint-gradle: A ktlint gradle plugin&quot; data-og-description=&quot;A ktlint gradle plugin. Contribute to JLLeitschuh/ktlint-gradle development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/JLLeitschuh/ktlint-gradle&quot; data-og-url=&quot;https://github.com/JLLeitschuh/ktlint-gradle&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/qysx2/hyU5UDCUlS/CKK1t054Y4kYGevMhFFq01/img.png?width=1200&amp;amp;height=600&amp;amp;face=975_123_1036_190&quot;&gt;&lt;a href=&quot;https://github.com/JLLeitschuh/ktlint-gradle&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/JLLeitschuh/ktlint-gradle&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/qysx2/hyU5UDCUlS/CKK1t054Y4kYGevMhFFq01/img.png?width=1200&amp;amp;height=600&amp;amp;face=975_123_1036_190');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - JLLeitschuh/ktlint-gradle: A ktlint gradle plugin&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;A ktlint gradle plugin. Contribute to JLLeitschuh/ktlint-gradle development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;pinterest/ktlint&lt;/b&gt;를 gradle 기반 프로젝트에서 간편하게 사용할 수 있도록 감싼(wrapping) gradle plugin입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1705454334180&quot; class=&quot;kotlin&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;highlight.js&quot;&gt;&lt;code&gt;// build.gradle.kts
plugin {
	id(&quot;org.jlleitschuh.gradle.ktlint&quot;).version(&quot;12.1.0&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정도 정말 간편합니다. Spring Boot 프로젝트 루트 경로에 있는 &lt;b&gt;build.gradle.kts&lt;/b&gt; 파일에서 jlleitschuh plugin 하나만 추가하면 기본적인 ktlint 기능을 사용해볼 수 있습니다.&lt;br /&gt;&lt;br /&gt;위의 plugin 하나만 추가하면 gradle build task에 ktlint check가 포함됩니다. 그래서 &lt;b&gt;프로젝트 build시 ktlint check 과정도 진행&lt;/b&gt;됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1705455250691&quot; class=&quot;shell&quot; data-ke-language=&quot;highlight.js&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ ./gradlew ktlintCheck&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;plugin ktlint 기능만 사용하고 싶다면 &lt;b&gt;ktlintCheck&lt;/b&gt; 입력하면 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2024-01-17 at 10.41.42 AM.png&quot; data-origin-width=&quot;3320&quot; data-origin-height=&quot;896&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qrM5b/btsDAkztkBh/b6UdrVChaghoJz7WjTqCc0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qrM5b/btsDAkztkBh/b6UdrVChaghoJz7WjTqCc0/img.png&quot; data-alt=&quot;ktlintCheck 수행 결과 콘솔 내용(오류 내용)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qrM5b/btsDAkztkBh/b6UdrVChaghoJz7WjTqCc0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqrM5b%2FbtsDAkztkBh%2Fb6UdrVChaghoJz7WjTqCc0%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;760&quot; height=&quot;205&quot; data-filename=&quot;Screenshot 2024-01-17 at 10.41.42 AM.png&quot; data-origin-width=&quot;3320&quot; data-origin-height=&quot;896&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;ktlintCheck 수행 결과 콘솔 내용(오류 내용)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 ktlint rule에 어긋난 코드 내용이 있다면 위의 캡쳐화면처럼 오류내용이 나옵니다. 오류 내용만 보더라도 대강 어떤  rule 내용을 위반했는지 나오긴 하지만 더 자세히 확인해보려면&amp;nbsp;&amp;nbsp;&lt;span style=&quot;background-color: #f3c000;&quot;&gt;&lt;b&gt;${project_root_dir}/build/reports/ktlint/..&lt;/b&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;Screenshot 2024-01-17 at 11.07.40 AM.png&quot; data-origin-width=&quot;3050&quot; data-origin-height=&quot;768&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kwE7Q/btsDuQNewHW/mwaS7gSMLDHPHl9j97vFXK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kwE7Q/btsDuQNewHW/mwaS7gSMLDHPHl9j97vFXK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kwE7Q/btsDuQNewHW/mwaS7gSMLDHPHl9j97vFXK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkwE7Q%2FbtsDuQNewHW%2FmwaS7gSMLDHPHl9j97vFXK%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;760&quot; height=&quot;191&quot; data-filename=&quot;Screenshot 2024-01-17 at 11.07.40 AM.png&quot; data-origin-width=&quot;3050&quot; data-origin-height=&quot;768&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;standard:no-empty-first-line-in-method-block&lt;/b&gt; rule을 위반했다고 나오네요. SpringBootKtlintDemoApplication.kt 파일에서 발생한 것이니 가서 수정하도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2024-01-17 at 11.09.36 AM.png&quot; data-origin-width=&quot;1602&quot; data-origin-height=&quot;606&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cPlPJb/btsDuRrQpT9/qb5uCC7rsBKl2mbfPM4M6K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cPlPJb/btsDuRrQpT9/qb5uCC7rsBKl2mbfPM4M6K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cPlPJb/btsDuRrQpT9/qb5uCC7rsBKl2mbfPM4M6K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcPlPJb%2FbtsDuRrQpT9%2Fqb5uCC7rsBKl2mbfPM4M6K%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;760&quot; height=&quot;287&quot; data-filename=&quot;Screenshot 2024-01-17 at 11.09.36 AM.png&quot; data-origin-width=&quot;1602&quot; data-origin-height=&quot;606&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확인해보니 10번째 라인에 main method block 맨 첫 번째 줄이 비어있어서 발생한 위반내용이었습니다. 해당 라인을 제거하고 다시 ktlintCheck를 실행해보겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2024-01-17 at 11.11.20 AM.png&quot; data-origin-width=&quot;2130&quot; data-origin-height=&quot;232&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bcKehA/btsDAsYnZA3/AnkVsHPOZ3YbnL6HJKfcWK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bcKehA/btsDAsYnZA3/AnkVsHPOZ3YbnL6HJKfcWK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bcKehA/btsDAsYnZA3/AnkVsHPOZ3YbnL6HJKfcWK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbcKehA%2FbtsDAsYnZA3%2FAnkVsHPOZ3YbnL6HJKfcWK%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;760&quot; height=&quot;83&quot; data-filename=&quot;Screenshot 2024-01-17 at 11.11.20 AM.png&quot; data-origin-width=&quot;2130&quot; data-origin-height=&quot;232&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ktlintCheck 다시 한 번 실행해보니 성공적으로 작업을 수행했다고 나옵니다. 이러면 프로젝트 내 모든 kotlin 코드들이 ktlint 규칙에 맞는 것이라 할 수 있습니다.&lt;/p&gt;
&lt;div&gt;&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2-2. ktlint report 형식 바꾸기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 위 과정에서 하나 걸리는 것이 있습니다. ktlint 수행 결과로 나온 오류 내용 report 파일이 txt 파일로 되어있는데, 막상 보면 규칙 위반 내용이 눈에 띄질 않습니다. 어떤 파일에 몇 번째 라인에서 어떠한 rule을 위반했는지 읽기가 어렵습니다. 이럴 때는 ktlint의 report 파일 형식을 바꾸면 되는데요. txt 파일로 되어있는 것을 JSON 파일로 바꾸면 만족 할만한 내용들을 받아볼 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1705458492556&quot; class=&quot;kotlin&quot; data-ke-language=&quot;highlight.js&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ktlint {
    reporters {
        reporter(ReporterType.JSON)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ktlint은 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;kotlin DSL에서&lt;span&gt; KtlintExtension을 설정할 수 있도록 람다형식으로 제공하고 있는 function입니다. KtlintExtension에 대한 설정 옵션 내용들은 &lt;a href=&quot;https://github.com/JLLeitschuh/ktlint-gradle?tab=readme-ov-file#configuration&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;b&gt;JLLeitschuh/ktlint-gradle&lt;/b&gt; github readme 페이지&lt;/a&gt;에서 확인해볼 수 있습니다. 이 중에 저는 ReporterExtension의 ReporterType을 JSON 타입으로 설정했습니다. 이렇게 설정하고 다시 ktlint check 작업을 수행해보겠습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1705464387058&quot; class=&quot;json&quot; data-ke-language=&quot;highlight.js&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[
    {
        &quot;file&quot;: &quot;.../spring-boot-ktlint-demo/src/main/kotlin/io/beaniejoy/ktlintdemo/SpringBootKtlintDemoApplication.kt&quot;,
        &quot;errors&quot;: [
            {
                &quot;line&quot;: 10,
                &quot;column&quot;: 1,
                &quot;message&quot;: &quot;First line in a method block should not be empty&quot;,
                &quot;rule&quot;: &quot;standard:no-empty-first-line-in-method-block&quot;
            }
        ]
    }
]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;txt 파일로 받아봤을 때보다 훨씬 더 좋네요. 몇 번째 라인에서 어떤 rule을 위반했고 그 내용까지 한 번에 확인할 수 있어서 오류 내용이 눈에 확 띄는 것 같습니다.&lt;br /&gt;&lt;br /&gt;이것 말고도 추가 editor config 파일 설정, 특정 경로 ktlint 수행하지 않도록 하는 filter 설정 등, ktlint 관련 커스텀하게 설정할 수 있는 여러 옵션들이 있으니 github repo 문서 내용 보시고 적용해보시면 좋을 것 같습니다.&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;b&gt;3. Multi module 프로젝트에서 ktlint 설정&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 내용은 monolithic project에서 간단하게 ktlint를 적용해본 것입니다. 보통 spring boot project를 구성하게 되면 multi module로 진행하는 경우가 많은데요. multi module 환경에서는 ktlint를 어떻게 적용할 수 있는지 알아보겠습니다.&lt;br /&gt;&lt;br /&gt;멀티모듈 프로젝트에 ktlint를 적용하기 위해 테스트 프로젝트로 3개의 모듈이 설정된 프로젝트를 만들어보았습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2024-01-17 at 10.42.01 PM.png&quot; data-origin-width=&quot;778&quot; data-origin-height=&quot;572&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bv7aC6/btsDD2dPJ8s/d0YvoZv2MPSbygX1WEDn3K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bv7aC6/btsDD2dPJ8s/d0YvoZv2MPSbygX1WEDn3K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bv7aC6/btsDD2dPJ8s/d0YvoZv2MPSbygX1WEDn3K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbv7aC6%2FbtsDD2dPJ8s%2Fd0YvoZv2MPSbygX1WEDn3K%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;760&quot; height=&quot;559&quot; data-filename=&quot;Screenshot 2024-01-17 at 10.42.01 PM.png&quot; data-origin-width=&quot;778&quot; data-origin-height=&quot;572&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 모듈간에 의존성 설정을 해두었는데요. 다음과 같습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1705536858002&quot; class=&quot;kotlin&quot; data-ke-language=&quot;highlight.js&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// app/build.gradle.kts
dependencies {
    implementation(project(&quot;:domain&quot;))
    implementation(project(&quot;:persistence&quot;))

	//...
}

// persistence/build.gradle.kts
dependencies {
    implementation(project(&quot;:domain&quot;))
    
	//...
}

// domain/build.gradle.kts
dependencies {
	//...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;app 모듈은 api controller 기능을 모아둔 곳으로 endpoint 역할을 담당하게 됩니다. domain 모듈은 비즈니스 로직을 담고 있고, persistence는 database 관련 영속화작업을 수행하고 관련 datasource를 설정하는 모듈입니다. 간단하게 요정도로 프로젝트 구성을 해보았고 본격적으로 ktlint를 설정해봅시다.&lt;/p&gt;
&lt;div&gt;&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3-1. 프로젝트 루트 경로의 build.gradle.kts 설정&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;루트 경로의 build.gradle.kts부터 살펴보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1705499076071&quot; class=&quot;kotlin&quot; data-ke-language=&quot;highlight.js&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;plugins {
    //... 기본 springboot + kotlin gradle plugin 설정

    id(&quot;org.jlleitschuh.gradle.ktlint&quot;).version(&quot;12.1.0&quot;)
}

allprojects {
    group = &quot;io.beaniejoy&quot;

    repositories {
        mavenCentral()
    }

    apply {
        plugin(&quot;org.jlleitschuh.gradle.ktlint&quot;)
    }

    ktlint {
        reporters {
            reporter(ReporterType.JSON)
        }
    }
}

subprojects {
	//...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모놀로틱 구조의 프로젝트에서 ktlint 설정과 조금 다른 점은 &lt;u&gt;&lt;b&gt;allprojects에 리포트파일 형식 설정을 적용했다는 것입니다.&lt;/b&gt;&lt;/u&gt; &lt;br /&gt;&lt;br /&gt;만약 subprojects에 적용하게 되면 app, domain, persistence 모듈 안에 있는 내용만 ktlint 리포트 파일 형식이 json으로 바뀌게 됩니다. 루트 경로에도 &lt;b&gt;build.gradle.kts, settings.gradle.kts, 추가로&lt;/b&gt;&amp;nbsp;&lt;b&gt;buildSrc&lt;/b&gt;를 적용해볼 수 있는데 여기서 존재할 수 있는 코틀린 코드에 대해 수행한 ktlint 리포트 결과 형식은 &lt;b&gt;txt파일(default)로 나오게 됩니다. &lt;br /&gt;&lt;/b&gt;(subprojects에 ReporerType.JSON 적용하고 &lt;b&gt;./gradlew :ktlintCheck&lt;/b&gt; 실행해보면 바로 알 수 있습니다.)&lt;br /&gt;&lt;br /&gt;즉, 루트 경로에 대해서도 ktlint 리포트 파일 형식을 json으로 똑같이 적용하고 싶다면 &lt;b&gt;allprojects에 적용해야합니다.&lt;br /&gt;&lt;br /&gt;&lt;/b&gt;위와 같이 설정하고 다음 명령어를 실행해보면 결과파일들이 json형태로 잘 나오는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1705543602862&quot; class=&quot;shell&quot; data-ke-language=&quot;highlight.js&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ ./gradlew clean ktlintCheck&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2024-01-18 at 11.07.12 AM.png&quot; data-origin-width=&quot;1996&quot; data-origin-height=&quot;1550&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/B15f3/btsDBuoJRfn/9bTfZnlAq9vZfxNsKZK3Bk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/B15f3/btsDBuoJRfn/9bTfZnlAq9vZfxNsKZK3Bk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/B15f3/btsDBuoJRfn/9bTfZnlAq9vZfxNsKZK3Bk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FB15f3%2FbtsDBuoJRfn%2F9bTfZnlAq9vZfxNsKZK3Bk%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;760&quot; height=&quot;590&quot; data-filename=&quot;Screenshot 2024-01-18 at 11.07.12 AM.png&quot; data-origin-width=&quot;1996&quot; data-origin-height=&quot;1550&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3-2. ktlint report 파일 한 곳에서 관리하기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 각 모듈의 &lt;b&gt;build/reports/ktlint&lt;/b&gt; 디렉토리 안에서 ktlint 오류에 대한 결과파일을 볼 수 있는데요. 한 가지 불편한 점은 각 모듈마다 결과파일이 산재해 있다보니 각 모듈의 build 디렉토리 안을 확인해야 해서 확인하기가 꽤나 버겁고 정신이 없는 것 같습니다.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;루트 프로젝트와 각 서브 모듈에 대한 ktlint 결과 파일들을 하나의 디렉토리로 모을 수 있다면 보기에도 편할 것 같습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1705552466887&quot; class=&quot;kotlin&quot; data-ke-language=&quot;highlight.js&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;allprojects {
	//...

    apply {
        plugin(&quot;org.jlleitschuh.gradle.ktlint&quot;)
    }

    ktlint {
        reporters {
            reporter(ReporterType.JSON)
        }
    }

    // ktlint report directory location setting
    tasks.withType&amp;lt;GenerateReportsTask&amp;gt; {
        reportsOutputDirectory.set(
            rootProject.layout.buildDirectory.dir(&quot;reports/ktlint/${project.name}&quot;)
        )
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/JLLeitschuh/ktlint-gradle?tab=readme-ov-file#setting-reports-output-directory&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;jlleitschuh plugin github readme&lt;/a&gt;에 보면 reports output file에 대한 디렉토리를 custom하게 설정할 수 있도록 가이드한 내용이 있습니다. 여기서 저는 프로젝트 루트 경로에 &lt;b&gt;build/reports/ktlint&lt;/b&gt; 디렉토리 안에서 모듈 이름 별로 관리하고 싶어서 위와 같이 설정했습니다. rootProject,,buildDirectory에 &lt;b&gt;reports/ktlint/${project.name}&lt;/b&gt; 으로 설정하면 각 모듈 이름 별로 결과파일을 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2024-01-18 at 1.39.30 PM.png&quot; data-origin-width=&quot;1688&quot; data-origin-height=&quot;870&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbeaOS/btsDz9Tp9zb/YxnmqmN0KchKaNf6AQpDK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbeaOS/btsDz9Tp9zb/YxnmqmN0KchKaNf6AQpDK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbeaOS/btsDz9Tp9zb/YxnmqmN0KchKaNf6AQpDK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbbeaOS%2FbtsDz9Tp9zb%2FYxnmqmN0KchKaNf6AQpDK1%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;760&quot; height=&quot;392&quot; data-filename=&quot;Screenshot 2024-01-18 at 1.39.30 PM.png&quot; data-origin-width=&quot;1688&quot; data-origin-height=&quot;870&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하니 하나의 디렉토리에서 모든 모듈에 대한 rule 위반 내용을 확인할 수 있어서 확실히 좋아진 것 같습니다.&lt;/p&gt;
&lt;div&gt;&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3-3. 모든 rule 위반 내용을 한 번에 확인할 수는 없는 것인가요?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;app, domain, persistence 모듈에 있는 코틀린 코드에 임의로 rule에 맞지 않는 내용을 넣고 ktlint 작업을 수행해보겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2024-01-18 at 1.49.41 PM.png&quot; data-origin-width=&quot;788&quot; data-origin-height=&quot;386&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cKUOlj/btsDFXcwwLF/R65r9TnY5EQo66z7QJkSAk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cKUOlj/btsDFXcwwLF/R65r9TnY5EQo66z7QJkSAk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cKUOlj/btsDFXcwwLF/R65r9TnY5EQo66z7QJkSAk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcKUOlj%2FbtsDFXcwwLF%2FR65r9TnY5EQo66z7QJkSAk%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;760&quot; height=&quot;372&quot; data-filename=&quot;Screenshot 2024-01-18 at 1.49.41 PM.png&quot; data-origin-width=&quot;788&quot; data-origin-height=&quot;386&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분명 모든 서브 모듈에 위반한 내용이 있는데 실제 ktlint 결과 파일 보면 domain에 있는 json 파일은 비어있고 persistence에 대한 위반 내용만 나와 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1705553820288&quot; class=&quot;shell&quot; data-ke-language=&quot;highlight.js&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ ./gradlew clean ktlintCheck --info | grep &quot;Task :&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;콘솔에 해당 프로젝트에 대해서 위의 gradle task를 실행해서 로그를 확인해보면 어느정도 이유를 알 수 있는데요.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2024-01-18 at 7.28.53 PM.png&quot; data-origin-width=&quot;1692&quot; data-origin-height=&quot;1224&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mKh4O/btsDBzK4ePM/awDZ4KNKgoLVGjpNBq08RK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mKh4O/btsDBzK4ePM/awDZ4KNKgoLVGjpNBq08RK/img.png&quot; data-alt=&quot;gradle ktlint check task 성공한 경우&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mKh4O/btsDBzK4ePM/awDZ4KNKgoLVGjpNBq08RK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmKh4O%2FbtsDBzK4ePM%2FawDZ4KNKgoLVGjpNBq08RK%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;760&quot; height=&quot;550&quot; data-filename=&quot;Screenshot 2024-01-18 at 7.28.53 PM.png&quot; data-origin-width=&quot;1692&quot; data-origin-height=&quot;1224&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;gradle ktlint check task 성공한 경우&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2024-01-18 at 7.29.47 PM.png&quot; data-origin-width=&quot;1652&quot; data-origin-height=&quot;1002&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IbW3z/btsDEQSwvVL/oNTkcCGyDiUI1L3q9mvud1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IbW3z/btsDEQSwvVL/oNTkcCGyDiUI1L3q9mvud1/img.png&quot; data-alt=&quot;gradle ktlint check task 실패한 경우 (중간에 작업이 끊기고 예외를 반환한다)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IbW3z/btsDEQSwvVL/oNTkcCGyDiUI1L3q9mvud1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIbW3z%2FbtsDEQSwvVL%2FoNTkcCGyDiUI1L3q9mvud1%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;760&quot; height=&quot;461&quot; data-filename=&quot;Screenshot 2024-01-18 at 7.29.47 PM.png&quot; data-origin-width=&quot;1652&quot; data-origin-height=&quot;1002&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;gradle ktlint check task 실패한 경우 (중간에 작업이 끊기고 예외를 반환한다)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;gradle ktlintCheck 작업을 수행했을 때 실패한 경우와 성공한 경우의 로그를 살펴보면 차이점을 바로 알 수 있습니다. &lt;br /&gt;성공했을 때는 모든 모듈과 루트 프로젝트에 대해서 ktlintCheck를 모두 잘 수행된 것을 확인할 수 있습니다. 하지만 실패한 경우 실패한 task 기점으로 더이상 작업이 진행되지 않는 것을 알 수 있습니다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;그래서 gradle ktlintCheck를 여러 번 수행을 하며 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;rule 오류 내용을 고쳐나가야 하는 경우가 많이 발생합니다. &lt;/span&gt;&lt;/b&gt;예를 들어, ktlintCheck를 처음 수행하고 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;domain, persistence에서 rule 위반 내용이 나왔는데, 해당 위반 내용을 모두 고치고나서 다시 ktlintCheck를 수행하면 이전에 안나왔던 app 모듈에서 rule 위반 내용이 나올 수 있습니다.&lt;/span&gt; 이부분 참고하시면 좋을 것 같습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;+ 위의 ktlint check 오류 캡쳐화면에 보면 :ktlintKotlinScriptCheck에서 FAILED가 나왔는데 거기서 task가 끝나는 것이 아니라 그 이후에도 2개의 task가 더 수행된 것을 볼 수 있습니다. 이 부분은 저도 정확하게 확인해보지는 못했지만 gradle task는 parallel execution 기능이 있는데요. 여러 모듈의 gradle task를 병행해서 수행하는 기능으로 보입니다. 추측이지만 ktlint plugin에 대한 task도 parallel execution으로 수행되어서 여러 task에서 FAILED된 것이 아닐까 생각합니다. &lt;br /&gt;gradle document를 확인했는데 default로 parallel mode를 사용하고 있지 않다고 봐서 왜 병행해서 수행되는지 알지는 못했네요,, ㅜ 이부분에 대해서 아시는 분 계시면 댓글 부탁드리겠습니다~&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3-4. 멀티모듈 build 수행할 때 ktlint task 설정&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;멀티모듈 Spring Boot를 개발하고 build 작업을 수행할 때 시작점이 되는 서브 모듈이 있을 것입니다. 예를 들어 위의 예시 프로젝트를 가지고 말씀드리자면 app 모듈이 될 것 같습니다. app 모듈은 애플리케이션의 endpoint를 담당하고 있고 실제 프로젝트를 실행할 수 있는 bootstrap class(AppApplication.kt)를 가지고 있기 때문입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1705576014201&quot; class=&quot;shell&quot; data-ke-language=&quot;highlight.js&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ ./gradlew clean :app:build -x test&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션을 서버에 배포할 때 프로젝트 build 과정을 거쳐야 하는데요. 위의 gradle 명령을 수행하여 app 모듈을 build하게 되면 implement 하고 있는 domain, persistence 모듈도 같이 빌드를 진행하게 됩니다.&lt;br /&gt;&lt;br /&gt;여기서 위에 &lt;b&gt;2-1. gradle plugin&lt;/b&gt; &lt;b&gt;설정&lt;/b&gt; 섹션에서 ktlint gradle plugin 설정하기만 하면 gradle build 작업 수행시 ktlint check 과정을 진행하게 된다고 했습니다. 멀티모듈에도 그대로 적용되는데 위 설정 그대로 적용하면 app 모듈만 ktlintCheck가 진행되는 것을 볼 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2024-01-18 at 8.28.46 PM.png&quot; data-origin-width=&quot;1810&quot; data-origin-height=&quot;272&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Ey7PX/btsDFUOaezP/ra0BLxJmCUkqVaPMnokxb1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Ey7PX/btsDFUOaezP/ra0BLxJmCUkqVaPMnokxb1/img.png&quot; data-alt=&quot;build 수행시 app 모듈에 대해서만 ktlint 작업 수행&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Ey7PX/btsDFUOaezP/ra0BLxJmCUkqVaPMnokxb1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEy7PX%2FbtsDFUOaezP%2Fra0BLxJmCUkqVaPMnokxb1%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;760&quot; height=&quot;114&quot; data-filename=&quot;Screenshot 2024-01-18 at 8.28.46 PM.png&quot; data-origin-width=&quot;1810&quot; data-origin-height=&quot;272&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;build 수행시 app 모듈에 대해서만 ktlint 작업 수행&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;build 할 때 모든 모듈과 루트 프로젝트까지 ktlint를 수행하게 하고 싶다면 afterEvaluate 설정을 &lt;b&gt;app/build.gradle.kts&lt;/b&gt;에 추가해야 합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1705577604147&quot; class=&quot;kotlin&quot; data-ke-language=&quot;highlight.js&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// app/build.gradle.kts

import org.jlleitschuh.gradle.ktlint.tasks.KtLintCheckTask

afterEvaluate {
    project.tasks.apply {
        // check all root &amp;amp; sub-modules ktLint
        this.withType&amp;lt;KtLintCheckTask&amp;gt; {
            dependsOn(
                listOf(
                    &quot;:ktlintCheck&quot;,
                    &quot;:domain:ktlintCheck&quot;,
                    &quot;:persistence:ktlintCheck&quot;
                )
            )
        }
    }
}

dependencies {
    implementation(project(&quot;:domain&quot;))
    implementation(project(&quot;:persistence&quot;))

	//...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;KtLintCheckTask에 의존성을 부여하는 방식입니다. app 모듈의 ktlintCheck task를 수행할 때 dependsOn에 등록한 task들을 먼저 수행하겠다는 의미로 생각하면 편할 것 같습니다. 위와 같이 설정하고 다시 :app:build 작업을 실행해보겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2024-01-18 at 8.29.59 PM.png&quot; data-origin-width=&quot;1846&quot; data-origin-height=&quot;1028&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bZLjud/btsDC8Gsl7t/ffFg1MVBEDg7ffnOzshHd0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bZLjud/btsDC8Gsl7t/ffFg1MVBEDg7ffnOzshHd0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bZLjud/btsDC8Gsl7t/ffFg1MVBEDg7ffnOzshHd0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbZLjud%2FbtsDC8Gsl7t%2FffFg1MVBEDg7ffnOzshHd0%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;760&quot; height=&quot;423&quot; data-filename=&quot;Screenshot 2024-01-18 at 8.29.59 PM.png&quot; data-origin-width=&quot;1846&quot; data-origin-height=&quot;1028&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;루트 프로젝트(:ktlintCheck)까지 포함해서 모든 모듈에서 ktlintCheck 작업이 수행된 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;+ 참고로 app 모듈의 KtLintCheckTask에 다른 모듈의 ktlintCheck 작업을 dependsOn 걸어버리면 &lt;b&gt;./gradlew ktlintCheck&lt;/b&gt;를 따로 수행했을 때 domain, persistence의 ktlintCheck가 실행되고 app의 ktlintCheck도 실행될텐데 app 모듈에 dependsOn 걸려있으면 &lt;b&gt;domain, persistence의 ktlintCheck가 불필요하게 두 번 수행&lt;/b&gt;하는 것은 아닌가 하는 의문이 들 수 있습니다. &lt;br /&gt;&lt;br /&gt;&lt;b&gt;결론은 그렇지 않습니다.&lt;/b&gt; 직접 테스트해봤을 때는 의도한 대로 모든 모듈들의 ktlintCheck는 한 번씩 실행되는 것을 확인했는데요. 이부분에 대해서 명확한 이유를 잘은 모르겠네요,, 찾아보다가 답이 나오면 여기에 추가해놓도록 하겠습니다.&lt;br /&gt;&lt;br /&gt;$ ./gradlew ktlintCheck&lt;br /&gt;$ ./gradlew :app:ktlintCheck&lt;br /&gt;app 모듈에 dependsOn을 설정했을 때는 두 개의 gradle 명령은 똑같이 동작하게 됩니다. 이 점 참고하세요.&lt;/blockquote&gt;
&lt;div&gt;&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3-5. ktlint의 특정 rule을 비활성화 하고 싶을 때 .editorconfig 활용하기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코틀린 코드로 개발하다가 ktlint에서 기본적으로 적용되어 있는 rule이 마음에 안드는 경우가 있을 수 있습니다. 혹은 팀 내에서 코드 컨벤션을 정했는데 ktlint의 rule과 충돌하는 경우가 발생하게 됩니다. 이런 경우에는 특정 rule을 비활성화해야될텐데요. 이 때 &lt;b&gt;.editorconfig&lt;/b&gt; 파일을 사용하면 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1705578667379&quot; class=&quot;json&quot; data-ke-language=&quot;highlight.js&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[
    {
        &quot;file&quot;: &quot;.../OrderReaderAdapter.kt&quot;,
        &quot;errors&quot;: [
            {
                &quot;line&quot;: 12,
                &quot;column&quot;: 49,
                &quot;message&quot;: &quot;Missing trailing comma before \&quot;)\&quot;&quot;,
                &quot;rule&quot;: &quot;standard:trailing-comma-on-declaration-site&quot;
            }
        ]
    }
]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ktlintCheck를 수행하고 받은 오류 report 내용입니다. &lt;b&gt;trailing-comma-on-declaration-site&lt;/b&gt; rule을 위반했다고 나오는데요. ktlint의 rule에 대한 내용을 보고싶으면 &lt;a href=&quot;https://pinterest.github.io/ktlint/1.1.0/rules/code-styles/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;pinterest github 페이지에 있는 document&lt;/a&gt;를 참고하면 됩니다.&lt;/p&gt;
&lt;figure id=&quot;og_1705578848920&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Code styles - Ktlint&quot; data-og-description=&quot;Code styles Starting from version 1.0, ktlint_official is the default code style. If you want to revert to another code style, then set the .editorconfig property ktlint_code_style. [*.{kt,kts}] ktlint_code_style = intellij_idea # or android_studio or ktli&quot; data-og-host=&quot;pinterest.github.io&quot; data-og-source-url=&quot;https://pinterest.github.io/ktlint/1.1.0/rules/code-styles/&quot; data-og-url=&quot;https://pinterest.github.io/ktlint/1.1.0/rules/code-styles/&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://pinterest.github.io/ktlint/1.1.0/rules/code-styles/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://pinterest.github.io/ktlint/1.1.0/rules/code-styles/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Code styles - Ktlint&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Code styles Starting from version 1.0, ktlint_official is the default code style. If you want to revert to another code style, then set the .editorconfig property ktlint_code_style. [*.{kt,kts}] ktlint_code_style = intellij_idea # or android_studio or ktli&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;pinterest.github.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;trailing-comma-on-declaration-site를 찾아보면 선언부에 괄호안의 인자 끝부분에 comma를 넣으라는 것 같습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1705579102601&quot; class=&quot;kotlin&quot; data-ke-language=&quot;highlight.js&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
class OrderReaderAdapter(
    private val orderRepository: OrderRepository, // 마지막 인자 끝부분에 comma(,)를 넣어야 합니다
) : OrderReaderPort {
    override fun getOrder(orderId: Long): Order {
        return orderRepository.findByIdOrNull(orderId)
            ?: throw RuntimeException(&quot;order not found&quot;)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;엇 그런데 저는 콤마 넣기가 싫어서 해당 rule을 비활성화하고 싶습니다. 이 때 루트 디렉토리에 &lt;b&gt;.editorconfig&lt;/b&gt; 파일을 만들고 다음과 같이 내용을 기입합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1705579186267&quot; class=&quot;markdown&quot; data-ke-language=&quot;highlight.js&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;root = true

[*]
charset = utf-8
end_of_line = lf
indent_style = space
trim_trailing_whitespace = true
insert_final_newline = true
max_line_length = 120

[*.{kt,kts}]
indent_size = 4
tab_width = 4

# ktlint_[standard/experimental]_[rule_name]=disabled
ktlint_standard_trailing-comma-on-declaration-site=disabled&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;root = true&lt;/b&gt;를 맨 위에 넣어줌으로써 해당 프로젝트 안에 &lt;b&gt;.editorconfig&lt;/b&gt; 파일을 인식하는 경계지점을 설정해줍니다. &lt;br /&gt;(프로젝트 root 경로에 root = true로 된 .editorconfig가 있으면 프로젝트 root 경로를 끝으로 상위 폴더에 가서 .editorconfig를 찾지 않겠다는 것입니다.)&lt;br /&gt;&lt;br /&gt;위에서 다른 부분은 제치고 &lt;b&gt;[*.{kt,kts}]&lt;/b&gt; 기준으로 kt, kts 파일에 적용할 editing 형식을 지정하게 되는데요. disabled하고 싶은 ktlint 규칙 설정도 그 아래에 넣어주면 됩니다. 위 내용처럼 disabled 처리하고 ktlintCheck 작업을 수행하면 해당 rule 위반 내용이 안나오게 됩니다.&lt;br /&gt;&lt;br /&gt;특정 ktlint rule disabled 처리하는 것과 관련해서 pinterest document에서 친절하게 가이드해주고 있는데요. 링크 참고하셔서 사용해보시면 좋을 것 같습니다. &lt;a href=&quot;https://pinterest.github.io/ktlint/0.49.0/rules/configuration-ktlint/#disabled-rules&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://pinterest.github.io/ktlint/0.49.0/rules/configuration-ktlint/#disabled-rules&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1705580375098&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;KtLint configuration - Ktlint&quot; data-og-description=&quot;KtLint configuration Ktlint uses a limited set of .editorconfig properties for additional configuration. A sensible default value is provided for each property when not explicitly defined. Properties can be overridden, provided they are specified under [*.&quot; data-og-host=&quot;pinterest.github.io&quot; data-og-source-url=&quot;https://pinterest.github.io/ktlint/0.49.0/rules/configuration-ktlint/#disabled-rules&quot; data-og-url=&quot;https://pinterest.github.io/ktlint/0.49.0/rules/configuration-ktlint/#disabled-rules&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://pinterest.github.io/ktlint/0.49.0/rules/configuration-ktlint/#disabled-rules&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://pinterest.github.io/ktlint/0.49.0/rules/configuration-ktlint/#disabled-rules&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;KtLint configuration - Ktlint&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;KtLint configuration Ktlint uses a limited set of .editorconfig properties for additional configuration. A sensible default value is provided for each property when not explicitly defined. Properties can be overridden, provided they are specified under [*.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;pinterest.github.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;.editorconfig는 참고로 코틀린 파일외의 여러 파일들에 대한 스타일을 설정할 수 있어서 어떤 환경에서 작업을 하든 해당 프로젝트를 받기만해도 형식을 맞출 수 있어서 잘 활용하면 좋을 것 같습니다.&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;b&gt;4. git commit 시 ktlint 연동하기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발작업을 다하고 git commit을 하게 되는데요. 이 때에도 ktlint check 작업을 연동해서 commit 하기 전에 형식 관련해서 rule 위반 사항이 있는지 자동으로 체크할 수 있도록 설정할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1705580874257&quot; class=&quot;shell&quot; data-ke-language=&quot;highlight.js&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ ./gradlew addKtlintCheckGitPreCommitHook&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ktlint gradle plugin이 적용되어 있는 상태로 프로젝트 루트 경로에서 위의 명령어만 입력하면 설정 끝입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2024-01-18 at 9.31.36 PM.png&quot; data-origin-width=&quot;1834&quot; data-origin-height=&quot;252&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OyOS1/btsDHuaap8l/YFduy0ihmZiyVqdSyrk8qk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OyOS1/btsDHuaap8l/YFduy0ihmZiyVqdSyrk8qk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OyOS1/btsDHuaap8l/YFduy0ihmZiyVqdSyrk8qk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOyOS1%2FbtsDHuaap8l%2FYFduy0ihmZiyVqdSyrk8qk%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;760&quot; height=&quot;104&quot; data-filename=&quot;Screenshot 2024-01-18 at 9.31.36 PM.png&quot; data-origin-width=&quot;1834&quot; data-origin-height=&quot;252&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2024-01-18 at 9.31.49 PM.png&quot; data-origin-width=&quot;1792&quot; data-origin-height=&quot;260&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vObf5/btsDC69HDww/pgDCsCxEgKaFYUusCrEXKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vObf5/btsDC69HDww/pgDCsCxEgKaFYUusCrEXKK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vObf5/btsDC69HDww/pgDCsCxEgKaFYUusCrEXKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvObf5%2FbtsDC69HDww%2FpgDCsCxEgKaFYUusCrEXKK%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;760&quot; height=&quot;110&quot; data-filename=&quot;Screenshot 2024-01-18 at 9.31.49 PM.png&quot; data-origin-width=&quot;1792&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;git commit을 시도하면 gradle ktlint 작업을 진행하게 됩니다. ktlintCheck를 통과했을 때 commit이 제대로 되고 만약 rule 위반사항이 있다면 commit이 되지 않고 에러를 반환하게 됩니다. 프로젝트에 gradle ktlint plugin을 적용했다면 이러한 설정을 적용해서 git commit시 놓쳤던 ktlint 규칙 위반내용들을 알아서 걸러낼 수 있도록 하면 좋을 것 같네요.&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;위의 모든 내용에 대해서 적용해보았던 테스트 프로젝트 github repository 링크를 첨부해놓겠습니다. 참고하세요~&lt;/p&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/beaniejoy/back-overall-repository/tree/main/spring-boot-ktlint-demo&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;모놀로틱 프로젝트 ktlint 적용 프로젝트&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/beaniejoy/back-overall-repository/tree/main/spring-boot-ktlint-multi-demo&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;멀티모듈 프로젝트 ktlint 적용 프로젝트&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Spring Boot + kotlin 프로젝트 환경에서 ktlint 적용하는데 있어서 조금이나마 도움이 되었으면 좋겠습니다. &lt;br /&gt;혹시나 틀린내용이 있거나 제가 알지 못했던 내용을 알고 계시다면 언제든 댓글 주세요. 감사합니다~!&lt;/blockquote&gt;</description>
      <category>Spring</category>
      <category>Gradle</category>
      <category>Kotlin</category>
      <category>ktlint</category>
      <category>Spring</category>
      <category>Spring Boot</category>
      <author>beaniejoy</author>
      <guid isPermaLink="true">https://beaniejoy.tistory.com/108</guid>
      <comments>https://beaniejoy.tistory.com/108#entry108comment</comments>
      <pubDate>Thu, 18 Jan 2024 21:55:55 +0900</pubDate>
    </item>
    <item>
      <title>[Ansible] 다른 OS 환경의 host들을 범용적으로 관리할 수 있는 playbook 작성해보자(패키지 설치)</title>
      <link>https://beaniejoy.tistory.com/107</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;개인 프로젝트의 배포 툴로 Jenkins와 Provisioning 도구로 Ansible(이하 앤서블)을 사용해보고 있습니다.&lt;br /&gt;배포 대상이 되는 서버에는 애플리케이션 실행에 필요한 패키지들이 설치되어 있어야 하는데요. 앤서블은 이러한 패키지들이 설치되어 있지 않다면 설치하고 이후 애플리케이션 배포 프로세스가 진행되도록 자동화할 수 있게 해줍니다.&lt;br /&gt;&lt;br /&gt;여기서 문제는 기존의 배포 서버를 다른 운영체제의 서버로 migration 한다거나 추가했을 때 기존 앤서블 playbook 스크립트에 설정된 내용들을 고쳐야 하는 번거로움이 생길 수 있습니다. 이게 어떤 문제 상황인지 구체적으로 알아보고 어떻게 하면 해결을 할 수 있을지 알아보겠습니다.&lt;br /&gt;&lt;br /&gt;(기본적인 앤서블 사용방법에 대해서 알고계셔야 합니다. ansible roles, ansible 사용법에 대해서 알고 계신 상태로 아래 게시글을 읽으시기를 권장드립니다.)&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;b&gt;1. 문제 상황&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션 서버에 reverse proxy로 nginx를 사용한다면 배포 전에 nginx가 설치되어 실행되고 있어야 합니다. &lt;br /&gt;또한 spring boot 기반의 애플리케이션이므로 build된 jar파일을 실행하려면 버전에 맞는 java 패키지가 설치되어 있어야 합니다.&lt;br /&gt;만약 없다면 이에 대해서 대비하는 설정이 배포 프로세스에 있어야 합니다. 앤서블은 이러한 패키지 관리를 깔끔하게 해줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1704530750419&quot; class=&quot;yaml&quot; data-ke-language=&quot;highlight.js&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# roles/package/tasks/main.yaml
---
- name: Install java
  ansible.builtin.dnf:
    name: &quot;{{ java_package }}&quot;
    state: present
  tags:
    - init

- name: Install nginx
  ansible.builtin.dnf:
    name: &quot;{{ nginx_package }}&quot;
    state: present
  tags:
    - init
    
# roles/package/vars/main.yaml
java_package: java-17-amazon-corretto
nginx_package: nginx&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;package role 단계에서 앤서블에 등록된 배포서버에 java와 nginx 패키지가 설치되어 있지 않다면 install 실행을 하게 됩니다.&lt;br /&gt;&lt;br /&gt;여기서 주목해야할 점은 &lt;a href=&quot;https://docs.ansible.com/ansible/latest/collections/ansible/builtin/dnf_module.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;패키지 관리 툴로 dnf&lt;/a&gt;를 사용했습니다. 앤서블은 참고로 dnf 뿐만 아니라 Debian계열의 운영체제에서 사용되는 apt, RedHat 진영에서 사용되는 yum 등 여러 패키지 관리 툴을 지원하고 있습니다.&lt;br /&gt;&lt;br /&gt;저는 배포서버로 AWS EC2를 사용했고 운영체제는 Amazon Linux 2023을 사용하고 있는 상황입니다. 그렇기에 ansible의 dnf 모듈을 사용해서 install을 진행했는데요. 문제 상황은 바로 여기서 발생하게 됩니다.&lt;br /&gt;&lt;br /&gt;해당 앤서블 스크립트를 가지고 다른 운영체제의 배포서버에 배포를 진행하게 된다면 어떻게 될까요. 예를 들어 Ubuntu 운영체제를 가진 배포서버에 해당 앤서블 스크립트를 가지고 배포 진행을 한다면 당연히 에러가 발생하게 될 것입니다.&lt;br /&gt;&lt;br /&gt;또한 Amazon Linux 2023 운영체제에서는 java 패키지로 java-17-amazon-corretto를 사용해야 하지만 다른 운영체제의 서버에서는 openjdk-17을 사용할 수 있습니다. &lt;u&gt;&lt;b&gt;다시 말하면&amp;nbsp;위의 앤서블 스크립트는 오로지 Amazon Linux를 사용하는 EC2 서버에서만 사용가능한 파일이라고 할 수 있습니다.&lt;br /&gt;&lt;br /&gt;&lt;/b&gt;&lt;/u&gt;앤서블을 사용하는 여러 이유 중 하나는 &lt;u&gt;&lt;b&gt;범용성&lt;/b&gt;&lt;/u&gt;에 있다고 생각합니다. 특정 운영체제와 환경을 가진 서버 한 대에만 동작하는 배포 스크립트는 추후에 변경될 여지가 많습니다. 물론 특정 운영체제의 서버로 고정해서 사용할 거라면 별 문제는 되지 않겠지만 다른 운영체제의 서버에 대해서도 적용이 필요하다면 위의 스크립트는 수정해서 따로 관리를 해야하는 이슈가 생기게 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1704532266975&quot; class=&quot;yaml&quot; data-ke-language=&quot;highlight.js&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# roles/package/tasks/main.yaml
---
- name: Install java
  ansible.builtin.dnf:
    name: &quot;{{ java_package }}&quot;
    state: present
  tags:
    - init

- name: Install nginx
  ansible.builtin.dnf:
    name: &quot;{{ nginx_package }}&quot;
    state: present
  tags:
    - init
    
# ubuntu에서 사용하려면 ansible.builtin.apt 모듈을 사용해야 하는데 
# 스크립트 파일을 분리해서 따로 관리??&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;&lt;/div&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;b&gt;2. 해결 방법&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앤서블은 &lt;b&gt;ansible facts&lt;/b&gt;를 사용하여 위와 같은 상황에서도 하나의 스크립트 파일로 관리할 수 있도록 할 수 있습니다.&lt;br /&gt;&lt;br /&gt;ansible에는 facts라는 변수들이 있습니다. ansible에 설정된 remote server에 대한 여러 정보들을 facts라는 변수들로 제공해주고 있습니다. &lt;a href=&quot;https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_vars_facts.html#discovering-variables-facts-and-magic-variables&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;ansible 공식 문서에 facts 관련 내용&lt;/a&gt;을 살펴보면 원격 서버에 대한 다양한 정보들이 담긴 변수 내용들을 확인해볼 수 있습니다.&lt;/p&gt;
&lt;figure id=&quot;og_1704532789619&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Discovering variables: facts and magic variables &amp;mdash; Ansible Documentation&quot; data-og-description=&quot;Ansible facts are data related to your remote systems, including operating systems, IP addresses, attached filesystems, and more. You can access this data in the ansible_facts variable. By default, you can also access some Ansible facts as top-level variab&quot; data-og-host=&quot;docs.ansible.com&quot; data-og-source-url=&quot;https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_vars_facts.html#discovering-variables-facts-and-magic-variables&quot; data-og-url=&quot;https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_vars_facts.html#discovering-variables-facts-and-magic-variables&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_vars_facts.html#discovering-variables-facts-and-magic-variables&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_vars_facts.html#discovering-variables-facts-and-magic-variables&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Discovering variables: facts and magic variables &amp;mdash; Ansible Documentation&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Ansible facts are data related to your remote systems, including operating systems, IP addresses, attached filesystems, and more. You can access this data in the ansible_facts variable. By default, you can also access some Ansible facts as top-level variab&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.ansible.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수 많은 facts 내용 중에 ansible_distribution이라는 변수를 사용해보려 합니다. remote server의 운영체제를 알려주는 변수입니다. 이를 활용하면 원격 서버의 운영체제 종류에 따른 패키지 내용과 패키지 관리 툴을 설정할 수 있을 것 같습니다.&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;b&gt;3. 적용해보기&lt;/b&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1704532866204&quot; class=&quot;yaml&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;highlight.js&quot;&gt;&lt;code&gt;---
- name: Check OS
  ansible.builtin.debug:
    msg: &quot;OS: {{ ansible_distribution }} / OS version: {{ ansible_distribution_version }}&quot;
  tags:
    - always

- name: Install java
  action: &amp;gt;
    {{ pkg_mgr[ansible_distribution] }} name={{ java_package[ansible_distribution] }} state=present
  tags:
    - init

- name: Install nginx
  action: &amp;gt;
    {{ pkg_mgr[ansible_distribution] }} name={{ nginx_package[ansible_distribution] }} state=present
  tags:
    - init&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Check 단계에서 OS 배포판과 배포판 버젼을 체크하여 로그를 남겼습니다. 그 다음에 패키지 설치를 해야하는데, 운영체제별 패키지 관리 툴 설정이 필요합니다. ansible_facts에 &lt;b&gt;ansible_pkg_mgr&lt;/b&gt;이라는 것이 있어서 remote server의 운영체제 맞는 패키지 관리 툴을 설정할 수 있을 것 같습니다. 다만 저는 직접 관리하고 싶어서 ansible playbook vars에 따로 패키지 관리 툴을 OS별로 설정했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1704768198984&quot; class=&quot;yaml&quot; data-ke-language=&quot;highlight.js&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# roles/package/vars/main.yml
pkg_mgr:
  Amazon: dnf
  Debian: apt

java_package:
  Amazon: java-17-amazon-corretto
  Debian: openjdk-17-jre
nginx_package:
  Amazon: nginx
  Debian: nginx&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;package roles에 &lt;b&gt;vars/main.yml&lt;/b&gt; 파일에다가 위와 같이 변수를 등록하면 package tasks 안에서 해당 변수를 사용할 수 있게 되는데요.&lt;/p&gt;
&lt;pre id=&quot;code_1704768353535&quot; class=&quot;yaml&quot; data-ke-language=&quot;highlight.js&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;- name: Install java
  action: &amp;gt;
    {{ pkg_mgr[ansible_distribution] }} name={{ java_package[ansible_distribution] }} state=present
  tags:
    - init&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 &lt;b&gt;{{ }}&lt;/b&gt; 안에 pkg_mgr을 사용해서 ansible_distribution facts에 따라 OS에 맞는 패키지 관리툴을 지정할 수 있습니다. java_package도 마찬가지로 ansible_distribution에 따라 위에서 vars로 미리 설정한 java package 이름이 적용됩니다.&lt;/p&gt;
&lt;div&gt;&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;&lt;/div&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;b&gt;4. 참고 사항&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 위와 같이 ansible_facts를 사용하려면 gather_facts 모드를 활성화해야 하는데요. ansible playbook에 따로 설정한 것이 없으면 default로 활성화되어 있어서 playbook 스크립트 파일 내에 ansible_facts을 사용할 수 있긴합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1704768686796&quot; class=&quot;yaml&quot; data-ke-language=&quot;highlight.js&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;- name: &quot;Deploy service-api({{ spring_profile }})&quot;
  hosts: ec2
  gather_facts: false
  ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 혹여나 &lt;b&gt;gather_facts를 false 처리&lt;/b&gt;했다면 위와 같이 facts를 사용할 수 없게 되니 facts를 사용하고 싶으시다면 &lt;b&gt;해당 값을 true로 설정하거나 제거하시면 됩니다.&lt;/b&gt; 이 점 참고하세요.&lt;br /&gt;&lt;br /&gt;그리고 해당 게시글에 대한 전체 ansible 스크립트 내용은 &lt;a href=&quot;https://github.com/beaniejoy/ansible-deploy-script&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;github repo&lt;/a&gt;를 참고하시면 될 것 같습니다.&lt;/p&gt;
&lt;figure id=&quot;og_1704768870402&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - beaniejoy/ansible-deploy-script: ansible playbook for application(Spring Boot) deployment&quot; data-og-description=&quot;ansible playbook for application(Spring Boot) deployment - GitHub - beaniejoy/ansible-deploy-script: ansible playbook for application(Spring Boot) deployment&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/beaniejoy/ansible-deploy-script&quot; data-og-url=&quot;https://github.com/beaniejoy/ansible-deploy-script&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/b7kndt/hyU2en4GJj/BI4AwHD38evYO50VEfzF8k/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/beaniejoy/ansible-deploy-script&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/beaniejoy/ansible-deploy-script&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/b7kndt/hyU2en4GJj/BI4AwHD38evYO50VEfzF8k/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - beaniejoy/ansible-deploy-script: ansible playbook for application(Spring Boot) deployment&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;ansible playbook for application(Spring Boot) deployment - GitHub - beaniejoy/ansible-deploy-script: ansible playbook for application(Spring Boot) deployment&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Infra</category>
      <category>ansible</category>
      <category>Playbook</category>
      <author>beaniejoy</author>
      <guid isPermaLink="true">https://beaniejoy.tistory.com/107</guid>
      <comments>https://beaniejoy.tistory.com/107#entry107comment</comments>
      <pubDate>Tue, 9 Jan 2024 12:07:55 +0900</pubDate>
    </item>
    <item>
      <title>토큰 방식의 인증/인가 프로세스에 대한 개념과 생각 정리(with 세션 방식)</title>
      <link>https://beaniejoy.tistory.com/106</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;cyber-security-1923446_1280.png&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;819&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/4Dycx/btsA4S7MPB1/sYoBXoKpteQDdgO3pUIzwk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/4Dycx/btsA4S7MPB1/sYoBXoKpteQDdgO3pUIzwk/img.png&quot; data-alt=&quot;Image by&amp;amp;amp;nbsp; Darwin Laganzon &amp;amp;amp;nbsp;from&amp;amp;amp;nbsp; Pixabay&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/4Dycx/btsA4S7MPB1/sYoBXoKpteQDdgO3pUIzwk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F4Dycx%2FbtsA4S7MPB1%2FsYoBXoKpteQDdgO3pUIzwk%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;760&quot; height=&quot;486&quot; data-filename=&quot;cyber-security-1923446_1280.png&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;819&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Image by&amp;amp;nbsp; Darwin Laganzon &amp;amp;nbsp;from&amp;amp;nbsp; Pixabay&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. Overview&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백엔드 개발에 있어서 인증/인가 처리는 빼놓을 수 없는 부분입니다. 사용자가 어떤 웹서비스를 이용하려면 기본적으로 로그인이라는 인증과정을 거쳐야하는데요. 그래야 쇼핑이든, 서비스 예약이든 간에 할 수 있겠죠(인증된 정보를 기반으로 해야하기 때문에) &lt;br /&gt;&lt;br /&gt;개인 프로젝트를 통해 JWT 토큰으로 access token과 이를 보완하는 refresh token을 적용해보는 작업을 해보았는데요. 토큰 방식의 인증/인가를 구현해보면서 몇 가지 들었던 의문점들을 저의 의식이 흐르는 대로 두서없이 작성해보고자 합니다.&lt;br /&gt;&lt;br /&gt;특히나 이번 게시글은 JWT 토큰방식으로 인증/인가를 적용한 내용들을 정리해보는 내용이 아니라, 토큰 방식의 인증/인가에 대해 마주했던 의문점들, 한계에 대한 내용을 세션 방식부터 설명하면서 풀어보고자 합니다.&lt;br /&gt;(이 다음 게시글에서 access, refresh token을 이용한 인증/인가 구현에 대해서 정리해보도록 하겠습니다.)&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;b&gt;2. 세션에 의한 인증/인가 방식&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인증, 인가는 왜 있어야할까요. 서비스를 이용하다보면 사용자 본인만 가능한 행위들이 존재합니다. 예를 들어 온라인 쇼핑몰에서 구매하는 행위, 내 게시판에 나의 글을 게시하는 행위, 내 정보를 수정하는 행위등이 있습니다. 이러한 행위들에 내가 아닌 다른 사용자가 마음대로 접근할 수 있다면 서비스에 대한 신뢰도는 0가 될 것이고 안전하지 못한 그 서비스를 절대로 이용하지 않을 것입니다.&lt;br /&gt;&lt;br /&gt;즉, 안전한 서비스를 제공하기 위해 나와 다른 사용자를 안전하고 확실하게 분리하고 나 아닌 다른 사람들을 배제할 수 있어야 합니다. 여기서 인증/인가 기능이 필수가 된 것입니다.&lt;br /&gt;&lt;br /&gt;그런데 오늘날 거의 모든 웹서비스는 HTTP 프로토콜에 기반하고 있습니다. HTTP의 중요한 특징 중 하나는 &lt;b&gt;Stateless(무상태)하다는 것&lt;/b&gt;입니다. 즉, &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;HTTP 프로토콜 기반의 통신만으로는 클라이언트의 상태를 저장할 수 없기 때문에&lt;span&gt; 사용자가 인증을 해도 해당 사용자가 인증을 했던 사용자인지 구분할 수 없어서 매번 인증 과정을 거쳐야 합니다. 비효율의 극치입니다.&lt;br /&gt;&lt;br /&gt;이를 보완하기 위해 &lt;b&gt;세션과 쿠키를 이용해 인증/인가 프로세스&lt;/b&gt;를 적용해 볼 수 있습니다.&lt;/span&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;세션방식의인증과정+.png&quot; data-origin-width=&quot;940&quot; data-origin-height=&quot;530&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ehlq2p/btsA4SNtuDv/0eCRfIxjcG84rkyM9mufSk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ehlq2p/btsA4SNtuDv/0eCRfIxjcG84rkyM9mufSk/img.png&quot; data-alt=&quot;출처: 본인 작성&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ehlq2p/btsA4SNtuDv/0eCRfIxjcG84rkyM9mufSk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fehlq2p%2FbtsA4SNtuDv%2F0eCRfIxjcG84rkyM9mufSk%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;760&quot; height=&quot;429&quot; data-filename=&quot;세션방식의인증과정+.png&quot; data-origin-width=&quot;940&quot; data-origin-height=&quot;530&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;위의 그림에서 볼 수 있듯이 사용자는 email(혹은 계정ID), password를 입력해 서버에서 타당한 사용자라고 인증이 되면 해당 인증 정보를 서버 메모리에 있는 세션에 저장하고 그 세션의 ID 값을 클라이언트의 쿠키로 설정하도록 응답을 보내게 됩니다.&lt;br /&gt;&lt;br /&gt;이렇게 되면 인가가 필요한 어떤 기능을 이용할 때 해당 세션ID 값을 서버에 보내주기만 하면 다시 인증과정을 거치지 않고도 사용자는 바로 서비스를 이용할 수 있게 됩니다.&lt;br /&gt;&lt;br /&gt;과정을 보면 괜찮은 방법 같아 보이지만 여기에는 한계가 존재합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2-1.  확장성의 한계&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 서비스를 구현하고 서버에 배포를 하게 되면 애플리케이션 한 개만 띄어놓는 경우는 없다고 보시면 됩니다. 애플리케이션 인스턴스를 8개, 10개씩 띄어두고 load balancer를 통해 트래픽 분산처리되도록 인프라를 구축해두는데요. &lt;br /&gt;(scale out 방식으로 서버 확장을 하는 경우가 대부분입니다.)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;session_scaleout.png&quot; data-origin-width=&quot;1410&quot; data-origin-height=&quot;700&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wwtkI/btsBcaZTK0S/4jobKPX5WskAefTAkkTMY0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wwtkI/btsBcaZTK0S/4jobKPX5WskAefTAkkTMY0/img.png&quot; data-alt=&quot;출처: 본인 작성&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wwtkI/btsBcaZTK0S/4jobKPX5WskAefTAkkTMY0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwwtkI%2FbtsBcaZTK0S%2F4jobKPX5WskAefTAkkTMY0%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;760&quot; height=&quot;377&quot; data-filename=&quot;session_scaleout.png&quot; data-origin-width=&quot;1410&quot; data-origin-height=&quot;700&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;b&gt;인스턴스마다 세션 안에 있는 데이터는 기본적으로 공유할 수 없습니다.&lt;/b&gt; App 1 인스턴스에 인증된 정보가 세션으로 저장되어 있는데 다음  api 요청에 대해 App 2 인스턴스에서 처리가 이루어진다면 같은 사용자이지만 인증된 정보는 확인할 수 없어 다시 인증과정을 거쳐야 하는 상황이 발생할 수 있습니다.&lt;br /&gt;&lt;br /&gt;물론 대안도 있습니다. 인증 세션이 발생한 첫 요청을 처리한 서버로만 고정하는 &lt;b&gt;sticky session&lt;/b&gt;, 여러 개 분산되어 있는 서버 인스턴스의 세션들을 클러스터링해 하나로 관리하는 &lt;b&gt;session clustering&lt;/b&gt;, 아예 구분된 메모리 서버를 두어 모든 인스턴스들이 해당 메모리 서버를 바라보게 하는 &lt;b&gt;session server&lt;/b&gt; 등이 있습니다. &lt;br /&gt;&lt;br /&gt;하지만 이를 구현하는 것도 만만치 않아서 어드민 서버나 간단한 서비스 구현을 제외하고는 세션 방식의 인증/인가 방식은 잘 사용되지 않는 것 같습니다.&lt;/p&gt;
&lt;div&gt;&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&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;b&gt;3. 토큰(JWT)에 의한 인증/인가 방식&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토큰 인증/인가 방식은 Stateless한 HTTP 프로토콜과 잘 맞는 방식입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;토큰방식의인증과정.png&quot; data-origin-width=&quot;940&quot; data-origin-height=&quot;530&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/edjYLO/btsA8pRjmR4/W2POQiBNjtabk52jeMncxk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/edjYLO/btsA8pRjmR4/W2POQiBNjtabk52jeMncxk/img.png&quot; data-alt=&quot;출처: 본인 작성&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/edjYLO/btsA8pRjmR4/W2POQiBNjtabk52jeMncxk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FedjYLO%2FbtsA8pRjmR4%2FW2POQiBNjtabk52jeMncxk%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;760&quot; height=&quot;429&quot; data-filename=&quot;토큰방식의인증과정.png&quot; data-origin-width=&quot;940&quot; data-origin-height=&quot;530&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;인증 후 세션에 인증정보를 저장한 것과 다르게 토큰 방식은 인증정보를 토대로 문자열로 이루어진 토큰을 생성해서 사용자에게 응답합니다. 사용자는 해당 토큰을 가지고 LocalStorage, Cookie와 같은 클라이언트 저장소에 저장해두었다가 인가가 필요한 기능을 요청할 때 해당 토큰을 header에 같이 담아 보내게 됩니다.&lt;br /&gt;&lt;br /&gt;Stateless한 방식으로 매요청마다 들고 있는 header의 토큰값을 가지고 인증여부를 체크하기 때문에 별도의 서버쪽 세션이 필요가 없게 됩니다. 이렇게 되면 scale out과 같은 분산된 인스턴스 환경에서 일관되게 인증된 상태임을 검증할 수 있게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3-1. 보안 취약성&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토큰 인증/인가 방식에도 단점은 존재합니다. 바로 보안에 취약하다는 점입니다. &lt;br /&gt;토큰은 인증 정보를 바탕으로 암호화된 문자열입니다. 해커가 토큰을 획득했다 하더라도 이를 복호화하기는 힘듭니다.&lt;br /&gt;그런데 서버쪽에서는 보통 요청으로 같이 들어온 인증 토큰을 가지고 인가 여부만 판단해 클라이언트가 요청한 api에 대해 응답을 내려주게 되는데요. 이를 충분히 악용할 수 있습니다.&lt;br /&gt;&lt;br /&gt;사용자가 가지고 있는 토큰을 복호화할 필요없이 사용할 수만 있다면 언제든 해커는 인증된 사용자로 위장해 마음껏 기능을 사용할 수 있게 됩니다. 대표적으로 이러한 취약점 공격으로 잘 알려진 것이 &lt;b&gt;Cross-Site Scripting(XSS)&lt;/b&gt;과 &lt;b&gt;Cross Site Request Fogery(CSRF)&lt;/b&gt;가 있습니다. &lt;br /&gt;(영어이긴 한데 &lt;a href=&quot;https://portswigger.net/web-security/cross-site-scripting&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;XSS&lt;/a&gt;와 &lt;a href=&quot;https://portswigger.net/web-security/csrf&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;CSRF&lt;/a&gt;를 잘 설명해주는 글이 있어 읽어보시는 것을 추천드립니다.)&lt;/p&gt;
&lt;figure id=&quot;og_1701165136094&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;What is cross-site scripting (XSS) and how to prevent it? | Web Security Academy&quot; data-og-description=&quot;In this section, we'll explain what cross-site scripting is, describe the different varieties of cross-site scripting vulnerabilities, and spell out how to ...&quot; data-og-host=&quot;portswigger.net&quot; data-og-source-url=&quot;https://portswigger.net/web-security/cross-site-scripting&quot; data-og-url=&quot;https://portswigger.net/web-security/cross-site-scripting&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/mRixS/hyUFbrmnn8/oCFAaggKz9nlCK7SLLmZzk/img.png?width=1201&amp;amp;height=603&amp;amp;face=0_0_1201_603,https://scrap.kakaocdn.net/dn/uzhpF/hyUE37ULRH/N0ESkTtiMvmG9QqkUWBHMK/img.png?width=1201&amp;amp;height=603&amp;amp;face=0_0_1201_603&quot;&gt;&lt;a href=&quot;https://portswigger.net/web-security/cross-site-scripting&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://portswigger.net/web-security/cross-site-scripting&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/mRixS/hyUFbrmnn8/oCFAaggKz9nlCK7SLLmZzk/img.png?width=1201&amp;amp;height=603&amp;amp;face=0_0_1201_603,https://scrap.kakaocdn.net/dn/uzhpF/hyUE37ULRH/N0ESkTtiMvmG9QqkUWBHMK/img.png?width=1201&amp;amp;height=603&amp;amp;face=0_0_1201_603');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;What is cross-site scripting (XSS) and how to prevent it? | Web Security Academy&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;In this section, we'll explain what cross-site scripting is, describe the different varieties of cross-site scripting vulnerabilities, and spell out how to ...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;portswigger.net&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;figure id=&quot;og_1701165121641&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;What is CSRF (Cross-site request forgery)? Tutorial &amp;amp; Examples | Web Security Academy&quot; data-og-description=&quot;In this section, we'll explain what cross-site request forgery is, describe some examples of common CSRF vulnerabilities, and explain how to prevent CSRF ...&quot; data-og-host=&quot;portswigger.net&quot; data-og-source-url=&quot;https://portswigger.net/web-security/csrf&quot; data-og-url=&quot;https://portswigger.net/web-security/csrf&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bByJDe/hyUFalFCoC/b8BpzZyTPt1cBuSjPBazFK/img.png?width=1201&amp;amp;height=603&amp;amp;face=0_0_1201_603,https://scrap.kakaocdn.net/dn/bRczZ9/hyUE3tjgVt/dajtkHxY7xEKsHkuyBNeH0/img.png?width=1201&amp;amp;height=603&amp;amp;face=0_0_1201_603&quot;&gt;&lt;a href=&quot;https://portswigger.net/web-security/csrf&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://portswigger.net/web-security/csrf&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bByJDe/hyUFalFCoC/b8BpzZyTPt1cBuSjPBazFK/img.png?width=1201&amp;amp;height=603&amp;amp;face=0_0_1201_603,https://scrap.kakaocdn.net/dn/bRczZ9/hyUE3tjgVt/dajtkHxY7xEKsHkuyBNeH0/img.png?width=1201&amp;amp;height=603&amp;amp;face=0_0_1201_603');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;What is CSRF (Cross-site request forgery)? Tutorial &amp;amp; Examples | Web Security Academy&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;In this section, we'll explain what cross-site request forgery is, describe some examples of common CSRF vulnerabilities, and explain how to prevent CSRF ...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;portswigger.net&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 토큰은 한 번 발급하면 만료되지 않는 이상 발급한 사람이 토큰을 수정하거나, 조작할 수 없게 됩니다. &lt;br /&gt;세션 방식에서는 쿠키에 저장되어 있는 세션ID가 탈취되거나 악용되고 있음을 감지하면 해당 사용자에 대한 세션을 바로 비활성화 처리라도 할 수 있는데요. &lt;b&gt;토큰은 이러한 비활성화 처리가 불가능합니다.&lt;/b&gt; JWT 같은 토큰을 사용한다면 최초 토큰 발행시 설정했던 만료시간이 지날 때까지 제거할 수도 없고 무효화 처리같은 조작도 불가능하기 때문에 속수무책으로 당할 수 밖에 없습니다.&lt;br /&gt;&lt;br /&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;b&gt;4. 해결방안 생각해보기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토큰 방식의 인증/인가의 보안 취약성을 보완하기 위해서는 쿠키와 같은 클라이언트 저장공간 조차도 이용하지 않고 클라이언트 단에서 javascript 같은 코드상에 &lt;b&gt;인증 토큰을&amp;nbsp;변수에 할당하는 방식&lt;/b&gt;을 생각해볼 수는 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1701166121624&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;let accessToken

const requestAuth = async (email, password) =&amp;gt; {
    try {
        const loginRequest = { email: 'xxxx', password: 'xxxx' }
        const response = await axios.post('/auth/login', loginRequest)
        accessToken = response.data
    } catch (e) {
        console.debug(error)
        alert('로그인 실패')
    }
}

const getUserInfo = async () =&amp;gt; {
    const headers = { 
    	Authorization: `Bearer ${accessToken}`
    }
    
    try {
    	const response = await axios.get(`/api/users/me`, headers)
        const userInfo = response.data
        
        //...
    } catch (e) {
	    //...
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1701166129640&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 인가가 필요한 api 호출 때마다 accessToken 획득 필요
const email = params.email
const password = params.password

await requestAuth(email, password)
await getUserInfo()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 결정적으로 문제가 되는 부분은 매번 api 호출때마다 로그인을 해야한다는 것입니다. 대부분의 웹서비스는 한 번 로그인을 하면 브라우저를 종료했다가 다시 접속해도 인증된 상태를 유지하도록 되어 있습니다. 위의 방식으로는 페이지 이동할 때마다 매번 로그인 인증과정을 거쳐야 합니다.&lt;br /&gt;&lt;br /&gt;인증 상태를 유지하려면 결국은 &lt;b&gt;클라이언트 저장공간을 활용&lt;/b&gt;해야 하는데요. 좀 더 안정적으로 관리하기 위해서는 토큰 자체를 바꿀 수 없으니 &lt;u&gt;&lt;b&gt;발급할 때 Access Token 유효기간을 아주 짧게 설정하는 방식&lt;/b&gt;&lt;/u&gt;을 많이들 사용하는 것 같습니다.&lt;br /&gt;&lt;br /&gt;그런데 유효기간을 짧게 설정해도 번거로운 것은 마찬가지일 것입니다. 예를 들어, 1시간 유효기간이 설정된 인증토큰을 발급받으면 1시간 이후에는 해당 토큰이 만료되어 결국 1시간 마다 로그인을 새로 해야하는 상황이 발생하게 되는데요. &lt;br /&gt;이를 위해 &lt;b&gt;Refresh Token&lt;/b&gt;을 가져와 사용하는 것 같습니다.&lt;/p&gt;
&lt;div&gt;&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4-1. Refresh Token 사용하여 토큰 기반의 인증/인가 보완하기&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;토큰갱신과정.png&quot; data-origin-width=&quot;970&quot; data-origin-height=&quot;710&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bd7TAo/btsA8K8IFxK/eeC3jcBEyXWO6A6ha0kht1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bd7TAo/btsA8K8IFxK/eeC3jcBEyXWO6A6ha0kht1/img.png&quot; data-alt=&quot;출처: 본인 작성&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bd7TAo/btsA8K8IFxK/eeC3jcBEyXWO6A6ha0kht1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbd7TAo%2FbtsA8K8IFxK%2FeeC3jcBEyXWO6A6ha0kht1%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;800&quot; height=&quot;586&quot; data-filename=&quot;토큰갱신과정.png&quot; data-origin-width=&quot;970&quot; data-origin-height=&quot;710&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;b&gt;Access Token은 ResponseBody로 반환, Refresh Token은 Cookie에 저장&lt;/b&gt;하고, 이후 API 요청시 Access Token을 header에 담아 요청합니다. 여기서 Access Token의 만료시간을 아주 짧게 설정해주었기에 만료응답(Token Expired)을 받더라도 Cookie에 Refresh Token만 있으면 언제든 토큰을 갱신해서 사용할 수 있습니다. 유효한 Refresh Token만 있다면 인증된 상태를 계속 유지하면서 서비스를 이용할 수 있게 됩니다.&lt;br /&gt;&lt;br /&gt;만료시간이 긴 Refresh Token을 이용해서 갱신할 때만 이용하고 나머지 API 요청에 대해서는 Access Token을 사용함으로써 좀 더 토큰에 대한 보안을 높일 수 있습니다. 왜냐하면 Access Token은 만료시간 짧기에 해커가 토큰을 가로챌 여지를 줄이고, Refresh Token은 갱신시에만 사용되기에 이 또한 해커에게 여지를 줄일 수 있기 때문입니다.&lt;br /&gt;&lt;br /&gt;이와 관련된 &lt;a href=&quot;https://velog.io/@yaytomato/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%90%EC%84%9C-%EC%95%88%EC%A0%84%ED%95%98%EA%B2%8C-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EC%B2%98%EB%A6%AC%ED%95%98%EA%B8%B0&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;좋은 글이 있어 공유&lt;/a&gt;드립니다. (위의 제가 작성한 시퀀스 다이어그램도 해당 글을 참고하여 작성한 것입니다.)&lt;/p&gt;
&lt;figure id=&quot;og_1701221866539&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;  프론트에서 안전하게 로그인 처리하기 (ft. React)&quot; data-og-description=&quot;localStorage냐 쿠키냐 그것이 문제로다&quot; data-og-host=&quot;velog.io&quot; data-og-source-url=&quot;https://velog.io/@yaytomato/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%90%EC%84%9C-%EC%95%88%EC%A0%84%ED%95%98%EA%B2%8C-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EC%B2%98%EB%A6%AC%ED%95%98%EA%B8%B0&quot; data-og-url=&quot;https://velog.io/@yaytomato/프론트에서-안전하게-로그인-처리하기&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/d2W2xI/hyUB148vsw/qgezz2FWkTRIPy2jcWuG2K/img.gif?width=480&amp;amp;height=360&amp;amp;face=0_0_480_360,https://scrap.kakaocdn.net/dn/cb9acL/hyUB8iPVK7/6kpVwUFlRXKKYMQJpBtSEk/img.gif?width=480&amp;amp;height=360&amp;amp;face=0_0_480_360,https://scrap.kakaocdn.net/dn/cVKzCC/hyUE7bwsu3/9BEewLMEhxFj42MTxm1VKK/img.png?width=2400&amp;amp;height=1584&amp;amp;face=0_0_2400_1584&quot;&gt;&lt;a href=&quot;https://velog.io/@yaytomato/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%90%EC%84%9C-%EC%95%88%EC%A0%84%ED%95%98%EA%B2%8C-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EC%B2%98%EB%A6%AC%ED%95%98%EA%B8%B0&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://velog.io/@yaytomato/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%90%EC%84%9C-%EC%95%88%EC%A0%84%ED%95%98%EA%B2%8C-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EC%B2%98%EB%A6%AC%ED%95%98%EA%B8%B0&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/d2W2xI/hyUB148vsw/qgezz2FWkTRIPy2jcWuG2K/img.gif?width=480&amp;amp;height=360&amp;amp;face=0_0_480_360,https://scrap.kakaocdn.net/dn/cb9acL/hyUB8iPVK7/6kpVwUFlRXKKYMQJpBtSEk/img.gif?width=480&amp;amp;height=360&amp;amp;face=0_0_480_360,https://scrap.kakaocdn.net/dn/cVKzCC/hyUE7bwsu3/9BEewLMEhxFj42MTxm1VKK/img.png?width=2400&amp;amp;height=1584&amp;amp;face=0_0_2400_1584');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;  프론트에서 안전하게 로그인 처리하기 (ft. React)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;localStorage냐 쿠키냐 그것이 문제로다&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;velog.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4-2. Refresh Token는 결국 세션 방식처럼 별도의 저장공간을 필요로 한다.&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Refresh Token을 가지고 토큰을 갱신하기 위해서는 서버에서 Refresh Token을 별도의 공간에 저장해서 관리를 해야합니다. 그래야 해당 토큰으로 요청이 들어왔을 때 저장된 내용과 비교를 통해 검증을 할 수 있게 됩니다. 다음 게시글에 구현 부분에서 설명하겠지만 저는 Redis를 사용해 한 곳에서 데이터가 관리되도록 하였습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1701246974571&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RedisHash(&quot;auth_tokens&quot;)
class AuthToken protected constructor(
    id: Long,
    accessToken: String,
    refreshToken: String,
    expiration: Long
) {
    @Id
    var id: Long = id
        protected set

    var accessToken: String = accessToken
        protected set

    var refreshToken: String = refreshToken
        protected set

    @TimeToLive
    var expiration: Long = expiration
        protected set

	//...
 }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;세션 방식의 인증/인가 프로세스와 마찬가지로 scale out 환경에서 데이터 일관성을 고려할 수 밖에 없습니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;또한 Refresh Token은 클라이언트에서도 쿠키로 저장되어 관리해야 하기 때문에 Session ID를 쿠키에 저장하는 세션 방식과 비슷하다고 볼 수 있습니다.&lt;br /&gt;&lt;br /&gt;결국 stateless한 HTTP 프로토콜에 가장 잘 맞다고 생각했던 &lt;u&gt;&lt;b&gt;토큰 방식도 결국에는 세션처럼 stateful한 성격도 가지게 될 수 밖에 없습니다.&lt;/b&gt;&lt;/u&gt; 이부분에 있어서 토큰 방식이 결국 세션 방식하고 비슷해졌는데 Access Token, Refresh Token 관리대상도 더 많아졌고, 구현도 오히려 더 복잡해진 느낌이 들긴 했습니다. 세션 방식에서 session server로 Redis 적용하는 것과 별차이도 없어 보인다는 생각까지 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4-3. Refresh Token 적용하기 위해 신경써야할 부분이 많다.&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 게시글에서는 Refresh Token만 쿠키에 저장하고 Access Token는 javascript단에서 local variable에 할당해서 사용하는 방식을 소개하고 있습니다. 하지만 딱 이정도만으로는 완전히 XSS, CSRF를 방지할 수는 없습니다.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;위의 게시글에서 언급한 대로 클라이언트, 서버 둘 다 토큰 관리에 대해서 신경써야 한다는 내용에는 저 또한 적극 공감합니다. &lt;br /&gt;Refresh Token만 쿠키에 저장하고 Access Token을 response로 받아서 처리한다해도 Access Token에 대해서 XSS에 결국 취약한 것 아닌가 하는 의문이 들기는 했지만 클라이언트 단에서 추가적으로 XSS 방어처리를 할 수 있다고 언급된 것을 보니 클라이언트 단에서 어떤 방어 수단이 여럿 존재하는 것 같습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;예:&amp;nbsp;&amp;lt;input&amp;gt;에서 입력된 값이 html / Javascript로 인식되지 않도록 서버에서 escape 처리를 해준다. 또 url을 통해 Javascript를 수행할 수 없도록 라우팅을 꼼꼼하게 관리한다. 다행인 것은 React는 공격자가 string에 html / Javascript를 담아 JSX에 삽입할 경우 자동으로 escape 처리한다. (XSS 방어 처리는 또 다른 주제기에 여기서는 이 정도에서 마무리한다.&lt;br /&gt;&lt;br /&gt;- 출처: 프론트에서 안전하게 로그인 처리하기(ft. React) (링크는 바로 위 상단 참고)&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 쿠키에 저장할 때 쿠키에 HttpOnly, Secure 속성을 설정하고 domain 속성까지 고려해볼 수 있을 것 같습니다.&lt;br /&gt;&lt;br /&gt;서버단에서는 CSRF 공격을 방지하기 위해 Referer header 값 검증, CSRF 토큰 적용 등을 고려해볼 수 있을 것 같습니다.&lt;br /&gt;(쿠키를 사용하는 것 자체가 CSRF 공격 수단으로 사용될 여지가 있다는 것이기에 필히 신경 써야할 것입니다.)&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;b&gt;5. 결론 및 생각정리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세션 방식부터 토큰 방식까지 간략하게 내용을 정리해보았는데요. &lt;br /&gt;구글링 조금만 해봐도 인증/인가 프로세스 개발할 때 세션 방식보다 토큰 방식을 더 선호하고 훨씬 더 많이 사용하고 있는 것 같습니다. &lt;br /&gt;저 또한 개인 프로젝트 진행하면서 JWT 토큰 방식으로 적용했고 실무에서조차 JWT 토큰 방식으로 인증 과정을 적용한 것을 쉽게 볼 수 있었습니다.&lt;br /&gt;&lt;br /&gt;여기서 문득 들었던 생각은 &lt;b&gt;'왜 토큰 방식을 선호하는 것일까'&lt;/b&gt; 입니다. Access Token 만으로 보안에 취약하기에 Refresh Token 까지 적용해서 보완하는 것까지 좋았지만 결국 &lt;b&gt;세션과 같이 토큰을 저장 관리해야되는 이슈&lt;/b&gt;가 생기게 되고 &lt;b&gt;쿠키에 저장된 토큰은 결국 Session ID&lt;/b&gt;처럼 탈취될 여지 또한 있다고 생각이 들었습니다.&lt;br /&gt;&lt;br /&gt;무엇보다 토큰이 어떻게 탈취가 될 수 있는지, XSS, CSRF 공격은 구체적으로 어떻게 이루어지는지에 대해 공부해봐야될 것 같습니다.&lt;br /&gt;세션 방식보다 토큰 방식을 더 많이 사용하는 이유에 대해서 명확하게 아시는 분이 계시다면 댓글로 알려주시면 정말 감사하겠습니다!&lt;br /&gt;&lt;br /&gt;이 다음 게시글에서 위에서 언급했던 Access Token, Refresh Token을 활용한 인증/인가 프로세스에 대해 개인 프로젝트에서 구현했던 내용을 프론트와 백엔드로 나눠 정리해보도록 하겠습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;틀린 내용이 있을 수 있습니다. 언제나 피드백 환영합니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Spring/Security</category>
      <category>JWT</category>
      <category>Security</category>
      <category>보안</category>
      <category>세션</category>
      <category>인가</category>
      <category>인증</category>
      <category>토큰</category>
      <author>beaniejoy</author>
      <guid isPermaLink="true">https://beaniejoy.tistory.com/106</guid>
      <comments>https://beaniejoy.tistory.com/106#entry106comment</comments>
      <pubDate>Thu, 30 Nov 2023 09:30:47 +0900</pubDate>
    </item>
    <item>
      <title>[CI/CD] Rolling 배포 전략을 이용해 무중단 배포해보기(jenkins, ansible)</title>
      <link>https://beaniejoy.tistory.com/104</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. 무중단 배포?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;nbsp;무중단 배포(zero-downtime deployment)&lt;/b&gt;는 백엔드 개발자에 있어서 절대 놓치면 안되는 중요한 내용입니다. 어떤 서비스를 배포할 때 실제 사용하고 있는 사용자들에게 영향을 미치지 않으면서 새로운 버전의 애플리케이션을 끊김 없이 잘 적용하는 것이 중요합니다. &lt;br /&gt;&lt;br /&gt;&amp;nbsp;만약 새로운 버전의 애플리케이션을 배포하는 과정에서 잠깐이어도 서비스가 멈추는 일이 발생하면 단순히 사용자가 불편을 겪는 것을 넘어서서 결제 같은 중요한 프로세스가 꼬이거나 하는 대형사고가 펼쳐질 수 있습니다.&lt;br /&gt;&amp;nbsp;그만큼 백엔드 애플리케이션 개발자라고 한다면 서비스 개발을 잘하는 것도 중요하지만 애플리케이션을 장애없이 돌아가게끔 하는 인프라적인 요소들도 잘 챙겨야 합니다.&lt;br /&gt;&lt;br /&gt;&amp;nbsp;무중단 배포에서 개인적으로 생각하기에 중요하게 고려해야하는 것은 &lt;b&gt;가장 먼저 배포과정에서 구버전에서 신버전으로 넘어가면서 끊김이 발생하면 안된다는 것입니다.&lt;/b&gt; (이것을 순단이라고 부르기도 하더라고요.)&lt;br /&gt;&amp;nbsp;두 번째로는 &lt;b&gt;구버전을 중단할 때 이미 들어온 요청들에 대해서 어떻게 할 것인지에 대해 고려해야 합니다.&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;크게 두 부분을 생각하면서 한 번 무중단 배포를 적용해보려고 합니다.&lt;/p&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&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;b&gt;2. 테스트할 시스템 구조와 Rolling 배포 과정&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무중단 배포를 적용하기에 앞서서 이번에 적용할 배포 전략에 대해 간략하게나마 언급을 해야될 것 같습니다.&lt;br /&gt;&lt;br /&gt;무중단 배포에는 여러 전략들이 존재하는데요. 크게 &lt;b&gt;Rolling, Blue-Green, Canary&lt;/b&gt; 전략이 있습니다. 실제로는 3개의 전략을 적절히 혼합해서 사용하는 경우도 있는 것 같은데 저는 무중단 배포의 대표 3인방에 대해서만 적용해보려고 하고 이번 게시글에서는 &lt;b&gt;Rolling 배포 전략&lt;/b&gt;에 대해 중점적으로 적용해보고자 합니다.&lt;br /&gt;&lt;br /&gt;Rolling 배포 전략의 개념에 대해서는 구&amp;nbsp; 글에 검색하면 아주 자세하게 소개한 멋진 글들이 많은데요. 그것을 참고하시는 것이 베스트일 것 같습니다. 개념을 모르신다면 먼저 해당 배포전략에 대해 알아보시고 해당 글을 읽으시는 것을 추천드립니다.&lt;br /&gt;&lt;br /&gt;개인 프로젝트에 적용된 서버 구조는 다음과 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_연습-1.jpg&quot; data-origin-width=&quot;2157&quot; data-origin-height=&quot;1171&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c2jJkp/btsytvM6kZ1/iOBR1K3Inu5evLw18LIGdk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c2jJkp/btsytvM6kZ1/iOBR1K3Inu5evLw18LIGdk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c2jJkp/btsytvM6kZ1/iOBR1K3Inu5evLw18LIGdk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc2jJkp%2FbtsytvM6kZ1%2FiOBR1K3Inu5evLw18LIGdk%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;760&quot; height=&quot;588&quot; data-filename=&quot;edited_연습-1.jpg&quot; data-origin-width=&quot;2157&quot; data-origin-height=&quot;1171&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라우드 서버 리소스 비용이 무지막지해서 개인적으로 구매한 라즈베리파이 하나에 포트로 구분된 애플리케이션 2개를 띄어서 nginx를 이용해 load balancing하는 아주 간략한 구조로 세팅했습니다.&lt;br /&gt;&lt;br /&gt;Rolling 배포전략이기 때문에 배포시 포트별로 하나하나 적용하게 되는데요. 과정은 다음과 같습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;nginx 단에서 upstream에 연결되어 있는 8080포트에 대해 연결 중단(down)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;8080포트 구버전 애플리케이션 shutdown&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;8080포트 신버전 애플리케이션 run&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;8080포트 신버전 애플리케이션 health checking(실행완료되었는지 체크)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;nginx 단에서 upstream에 다시 8080포트 연결&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;... 8081 포트도 동일하게 진행&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 절차대로 진행하는 것을 목표로 한 번 배포 프로세스를 구성해보도록 하겠습니다.&lt;/p&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&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;b&gt;3. Jenkins pipeline 빌드 프로세스&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CI/CD 과정은 Jenkins와 ansible을 사용했습니다. 이전에 작성한 글 중 jenkins와 ansible로 CI/CD 프로세스 만들어보는 내용으로 정리한 것이 있습니다. &lt;b&gt;Jenkins나 ansible에 대해서 처음 접하신 분들은 이 글을 읽으시기 전에 첨부해드린 제 글을 읽으시고, 공식 document도 참고하시면서 꼭 한 번 만들어보는 것을 추천드립니다.&amp;nbsp;&lt;br /&gt;&lt;/b&gt;(&lt;a href=&quot;https://beaniejoy.tistory.com/103&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://beaniejoy.tistory.com/103&lt;/a&gt;)&lt;/p&gt;
&lt;figure id=&quot;og_1697278401263&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Jenkins와 Ansible을 이용한 CI/CD 프로세스 만들어보기 (내가 만든 서비스 배포까지)&quot; data-og-description=&quot;Jenkins와 Ansible을 이용해 Spring Boot application을 빌드하고 서버에 배포하는 모든 과정(CI/CD)을 자동화해보는 내용을 정리해보려고 합니다. 이 글을 통해 Jenkins와 Ansible을 통해 빌드, 배포 과정을 처음&quot; data-og-host=&quot;beaniejoy.tistory.com&quot; data-og-source-url=&quot;https://beaniejoy.tistory.com/103&quot; data-og-url=&quot;https://beaniejoy.tistory.com/103&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/oJvqa/hyUd0KG7zy/g5fGUdQ5kAkDAJTfujD431/img.png?width=720&amp;amp;height=405&amp;amp;face=0_0_720_405,https://scrap.kakaocdn.net/dn/bw8Ina/hyT9Dp4BQC/j4LpgilssrRVK2vdMqcKV0/img.png?width=720&amp;amp;height=405&amp;amp;face=0_0_720_405,https://scrap.kakaocdn.net/dn/ErlEZ/hyT9KirZ0n/gStBTWd1x5RNnhThV3gfi1/img.png?width=1516&amp;amp;height=1680&amp;amp;face=0_0_1516_1680&quot;&gt;&lt;a href=&quot;https://beaniejoy.tistory.com/103&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://beaniejoy.tistory.com/103&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/oJvqa/hyUd0KG7zy/g5fGUdQ5kAkDAJTfujD431/img.png?width=720&amp;amp;height=405&amp;amp;face=0_0_720_405,https://scrap.kakaocdn.net/dn/bw8Ina/hyT9Dp4BQC/j4LpgilssrRVK2vdMqcKV0/img.png?width=720&amp;amp;height=405&amp;amp;face=0_0_720_405,https://scrap.kakaocdn.net/dn/ErlEZ/hyT9KirZ0n/gStBTWd1x5RNnhThV3gfi1/img.png?width=1516&amp;amp;height=1680&amp;amp;face=0_0_1516_1680');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Jenkins와 Ansible을 이용한 CI/CD 프로세스 만들어보기 (내가 만든 서비스 배포까지)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Jenkins와 Ansible을 이용해 Spring Boot application을 빌드하고 서버에 배포하는 모든 과정(CI/CD)을 자동화해보는 내용을 정리해보려고 합니다. 이 글을 통해 Jenkins와 Ansible을 통해 빌드, 배포 과정을 처음&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;beaniejoy.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(이번 글에서는 Rolling 배포전략에 대해서 직접 적용해보는 것을 목표로 하는 글이기 때문에 jenkins, ansible에 대해 기본적인 사용법을 알고 있다는 것을 전제로 정리해보려 합니다.)&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;b&gt;4. Rolling 배포 전략을 적용하기 전 고려해야할 사항(중요)&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;맨 위의 문단에서 무중단 배포에서 크게 고려해야될 부분이 2가지가 있음을 언급드렸습니다. &lt;br /&gt;가장 먼저 &lt;b&gt;배포과정에서 구버전에서 새로운 버전으로 마이그레이션하는 상황에서 끊김이 발생하면 안된다는 내용입니다.&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1697281618422&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# nginx config 변경 후 단순히 restart하면 문제는 없을까?
sudo systemctl restart nginx&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포과정에서 nginx 설정중에 upstream 내용을 수정해서 적용해야 하는데요. 이 때 단순히 설정을 바꾸고 nginx를 restart하면 예기치 않은 문제(nginx config syntax 오류 등)로 nginx가 종료되어버리는 상황이 발생할 수 있습니다. 이는 서비스 중단을 초래할 수 있는데요. 이러한 부분들을 신중하게 고려해서 배포 스크립트를 작성해야 합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1697281796937&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# nginx config에 대한 기본적인 syntax 에러 체크
if ! sudo nginx -t;
then
echo &quot;Invalid nginx conf &amp;gt; upstream(${PORT}}) ${toggle_status}&quot;
exit 40
fi

# nginx에 downtime이 발생하지 않도록 reload를 통해 바뀐 config 적용
sudo systemctl reload nginx&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;nginx -t&lt;/b&gt;를 통해 기본적인 syntax를 체크한 다음 restart 대신 &lt;b&gt;reload&lt;/b&gt;로 바뀐 config 내용을 nginx에 적용함으로써 안전하게 nginx 설정내용을 변경할 수 있습니다.&lt;br /&gt;&lt;br /&gt;두 번째로, &lt;b&gt;구버전을 중단할 때 이미 들어온 요청들에 대해서 어떻게 할 것인지에 대해 고려해야 합니다.&lt;br /&gt;&lt;/b&gt;새로운 버전의 애플리케이션 실행에만 관심을 가지면 안됩니다. 이전 버전에 대해서 어떻게 처리해야할지도 신중히 생각을 해야합니다.&lt;br /&gt;만약 이전 버전에서 요청을 처리하는 중에 있는데 배포 과정에서 이전 버전의 애플리케이션을 무턱대고 shutdown 해버리면 해당 요청은 영문도 모르는 상태로 실패처리가 될 것입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1697284577826&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ kill -TERM [PID] (or kill -15 [PID])&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로세스 종료할 때 signal로 SIGKILL(9), SIGTERM(15)을 주로 비교하는데요. 관련 내용에 대해서 꼭 찾아서 보시는 것을 추천드립니다. (&lt;a href=&quot;https://www.springcloud.io/post/2022-02/spring-boot-graceful-shutdown/#gsc.tab=0&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;영어이긴 하지만 추천하는 글 하나 링크드립니다.&lt;/a&gt;)&lt;/p&gt;
&lt;figure id=&quot;og_1697294855505&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;A Study of Graceful Shutdown for Spring Boot Applications&quot; data-og-description=&quot;Recently, I took a look at the restart scripts of the project and found that Ops has been using kill-9&amp;lt;pid&amp;gt; to restart springboot embedded tomcat, in fact, we almost unanimously agree that kill-9&amp;lt;pid&amp;gt; is a more violent way, but few people can analyze what &quot; data-og-host=&quot;www.springcloud.io&quot; data-og-source-url=&quot;https://www.springcloud.io/post/2022-02/spring-boot-graceful-shutdown/#gsc.tab=0&quot; data-og-url=&quot;https://www.springcloud.io/post/2022-02/spring-boot-graceful-shutdown/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/xAS4P/hyT9AfSKLr/2y0bnLZAt5NdbGwXLsjskK/img.png?width=192&amp;amp;height=192&amp;amp;face=0_0_192_192&quot;&gt;&lt;a href=&quot;https://www.springcloud.io/post/2022-02/spring-boot-graceful-shutdown/#gsc.tab=0&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.springcloud.io/post/2022-02/spring-boot-graceful-shutdown/#gsc.tab=0&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/xAS4P/hyT9AfSKLr/2y0bnLZAt5NdbGwXLsjskK/img.png?width=192&amp;amp;height=192&amp;amp;face=0_0_192_192');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;A Study of Graceful Shutdown for Spring Boot Applications&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Recently, I took a look at the restart scripts of the project and found that Ops has been using kill-9&amp;lt;pid&amp;gt; to restart springboot embedded tomcat, in fact, we almost unanimously agree that kill-9&amp;lt;pid&amp;gt; is a more violent way, but few people can analyze what&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.springcloud.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단하게 요약하자면 &lt;u&gt;&lt;b&gt;kill -9보다는 kill -15를 사용해서 애플리케이션을 종료하라는 것입니다.&lt;/b&gt;&lt;/u&gt; &lt;br /&gt;&amp;gt; &lt;b&gt;SIGKILL(9)&lt;/b&gt;을 통해 프로세스를 종료하면 컴퓨터 사용 중에 플러그인을 뽑으면 죽어버리듯이 애플리케이션을 즉시 종료시켜버려 애플리케이션 입장에서는 상당히 불친절합니다. &lt;br /&gt;&amp;gt; &lt;b&gt;SIGTERM(15)&lt;/b&gt;은 JVM단에서 해당 시그널을 받고 애플리케이션에 등록된 shutdown hook을 실행하게 됩니다. 이 때 실행되고 있는 모든 thread가 종료될 때까지 기다렸다가 안전하게 애플리케이션을 down시키는 그런 후작업들을 진행할 수 있게 된다고 보시면 될 것 같습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1697295503983&quot; class=&quot;go&quot; data-ke-language=&quot;go&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// application.yaml 설정 파일
server:
  shutdown: graceful
  
spring:
  lifecycle:
    timeout-per-shutdown-phase: 30s # default 30s (graceful shutdown)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot application에 기본적으로 제공하고 있는 graceful shutdown 기능이 있는데요. 해당 기능을 꼭 설정해야 내장 톰캣이 SIGTERM에 반응해서 말그대로 우아하게 애플리케이션을 종료할 수 있습니다.&lt;/p&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&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;b&gt;5. Ansible playbook, 배포 스크립트 작성하기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;5-1. Ansible playbook 살짝 살펴보기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포를 위한 기본적인 ansible playbook 스크립트 내용은 위에 첨부해드린 게시글에서 정리했는데요. 해당 글을 참고하시면 좋을 것 같습니다. 여기서는 rolling 배포와 관련된 내용들만 언급드리려 합니다. (&lt;a href=&quot;https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_reuse_roles.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;ansible roles&lt;/a&gt; 사용하여 스크립트를 적용했습니다.)&lt;/p&gt;
&lt;pre id=&quot;code_1697299208538&quot; class=&quot;scala&quot; data-ke-language=&quot;scala&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;- name: &quot;[Rolling Update] Run all tasks&quot;
  ansible.builtin.include_tasks:
    file: rolling_update/main.yml
    apply:
      tags:
        - rolling_update
  tags:
    - rolling_update&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;rolling update를 위한 전용 script 파일(&lt;b&gt;rolling_update/main.yml&lt;/b&gt;)을 실행해줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1697299577774&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# rolling_update/main.yml
- name: &quot;[Rolling Update] Copy service script file to remote server&quot;
  ansible.builtin.template:
    src: rolling-update-deploy.sh.j2
    dest: &quot;{{ remote_app_path }}/service.sh&quot;
    owner: &quot;{{ remote_server_user[ansible_distribution] }}&quot;
    group: &quot;{{ remote_server_group[ansible_distribution] }}&quot;
    mode: 0755

- name: &quot;[Rolling Update] Run service script&quot;
  ansible.builtin.include_tasks:
    file: deploy.yml
    apply:
      tags:
        - rolling_update
  loop:
    - 8080
    - 8081&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;rolling 배포전략을 위한 실행 스크립트 파일을 원격서버로 전송한 다음에 해당 스크립트 파일을 실행하게 됩니다.&lt;br /&gt;위의 스크립트 내용에서 중요한 것은 ansible playbook의 &lt;b&gt;loop&lt;/b&gt;를 이용해서 8080, 8081 포트를 타깃으로 배포를 &lt;b&gt;차례대로 진행하게 된다는 것입니다.&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;nginx 단에서 upstream에 연결되어 있는 8080포트에 대해 연결 중단(down)&lt;/li&gt;
&lt;li&gt;8080포트 구버전 애플리케이션 shutdown&lt;/li&gt;
&lt;li&gt;8080포트 신버전 애플리케이션 run&lt;/li&gt;
&lt;li&gt;8080포트 신버전 애플리케이션 health checking(실행완료되었는지 체크)&lt;/li&gt;
&lt;li&gt;nginx 단에서 upstream에 다시 8080포트 연결&lt;/li&gt;
&lt;li&gt;... 8081 포트도 동일하게 진행&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 내용을 다시 적어보았는데요. 8080 포트부터 배포를 진행하고, 8081 포트도 동일한 과정으로 배포를 진행하게 됩니다. 그림으로 간략하게 묘사하자면 다음과 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_연습-2.jpg&quot; data-origin-width=&quot;2131&quot; data-origin-height=&quot;1094&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/byNQMc/btsytRh4Hxm/EfwMTKqT9QnOZiDxjaloM1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/byNQMc/btsytRh4Hxm/EfwMTKqT9QnOZiDxjaloM1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/byNQMc/btsytRh4Hxm/EfwMTKqT9QnOZiDxjaloM1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbyNQMc%2FbtsytRh4Hxm%2FEfwMTKqT9QnOZiDxjaloM1%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;760&quot; height=&quot;588&quot; data-filename=&quot;edited_연습-2.jpg&quot; data-origin-width=&quot;2131&quot; data-origin-height=&quot;1094&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1697300620976&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# rolling_update/deploy.yml
- name: &quot;[Rolling Update] Shutdown old application - {{ item }}&quot;
  ansible.builtin.command: &quot;{{ remote_app_path }}/service.sh shutdown {{ item }}&quot;
  register: result
  become: false

- ansible.builtin.debug:
    msg: &quot;{{ result.stdout_lines }}&quot;

- name: &quot;[Rolling Update] Run service script - {{ item }}&quot;
  ansible.builtin.command: &quot;{{ remote_app_path }}/service.sh run {{ item }}&quot;
  register: result
  become: false

- ansible.builtin.debug:
    msg: &quot;{{ result.stdout_lines }}&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ansible playbook loop를 통해 실행되는 deploy.yml 파일 내용입니다. 원격 서버로 전송된 실행 스크립트를 사용해서 먼저 해당 port의 이전 버전 애플리케이션을 shutdown 시키고 새로운 버전의 애플리케이션을 실행(run)하는 과정으로 배포가 진행됩니다.&lt;br /&gt;&lt;br /&gt;다음으로 실행 스크립트를 한 번 살펴볼까요.&lt;/p&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;5-2. 실행 스크립트 살펴보기&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;5-2-1. 이전 버전의 애플리케이션 종료&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1697301163890&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;case &quot;${COMMAND}&quot; in
  shutdown)
    shutdown
    ;;
  run)
    run_app
    ;;
  *)
    echo &quot;${COMMAND} &amp;gt; Not supported command&quot;
    exit 40;
esac&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;shutdown과 run에 대한 진행을 각각 하기 위해 별도의 function으로 구분을 지었습니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1697301228841&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;shutdown() {
  echo &quot;-----------------------------------------&quot;
  echo &quot;[SHUTDOWN] ${MODULE_NAME}:${PORT}&quot;
  echo &quot;-----------------------------------------&quot;

  private_toggle_proxy_upstream down

  # ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;shutdown 부터 살펴보면 가장 먼저 proxy(nginx)에 설정된 upstream 내용 중 포트 하나에 대해 down 설정을 통해서 nginx와 애플리케이션의 연결을 끊습니다. 8080부터 시작할테니 8080포트로 실행되고 있는 애플리케이션과 nginx 연결을 끊게 될 것입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1697301463185&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private_toggle_proxy_upstream() {

  # ... from_upstream_status, to_upstream_status 설정하는 내용 생략 ...
  
  if [ -n &quot;${target_upstream_status}&quot; ]
  then
    echo &quot;proxy upstream ${PORT} status - already ${toggle_status}&quot;
  else
    echo &quot;proxy upstream ${PORT} setting ${toggle_status}&quot;
    sudo sed -i &quot;s/\(${from_upstream_status}\)/${to_upstream_status}/g&quot; &quot;${PROXY_CONF_FILE}&quot;
  fi

  if ! sudo nginx -t;
  then
    echo &quot;Invalid nginx conf &amp;gt; upstream(${PORT}}) ${toggle_status}&quot;
    exit 40
  fi

  sudo systemctl reload nginx
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스크립트 내용이 너무 길어서 중요한 내용만 축약해보았습니다. sed 명령어를 통해서 파일에 정규표현식으로 nginx config 파일의 내용을 변경하는 부분이 있습니다. 위의 스크립트 내용을 8080 포트에 대해서 실행하게 되면 다음과 같이 nginx가 설정됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1697302652539&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;upstream service {
  least_conn;
  server 127.0.0.1:8080 down;
  server 127.0.0.1:8081;
  keepalive 16;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;8080포트에 down 설정이 되고나서 nginx -t를 통해 syntax 검사를 하고 nginx reload를 통해 실제 nginx에 적용하게 됩니다. &lt;b&gt;이렇게 되면 8081포트만 서비스가 이루어지고 있는 상태가 됩니다.&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1697302822917&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  private_toggle_proxy_upstream down
  
  echo &quot;&amp;gt;&amp;gt; kill current running application(${PORT})&quot;
  current_pid=$(pgrep -f &quot;java.*${PORT}&quot;)
  if [ -z &quot;${current_pid}&quot; ];
  then
    echo &quot;no current running application &amp;gt;&amp;gt; pass killing process&quot;
  else
    echo &quot;stop java application(${PORT}) - ${current_pid}&quot;
    sudo kill -TERM &quot;${current_pid}&quot;
    sleep 2
  fi&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션 하나를 nginx와 연결 끊고 난 이후에 해당 포트로 실행되고 있는 java 애플리케이션의 PID 값을 알아냅니다. 그리고 graceful shutdown을 위한 SIGTERM을 통해 프로세스 종료를 진행합니다. 이렇게 되면 특정 포트의 애플리케이션 종료가 완료됩니다.&lt;/p&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;5-2-2. 신규 버전의 애플리케이션 실행&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1697303060568&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;run_app() {
  cd &quot;${APP_WORKSPACE}&quot; || exit 40

  echo &quot;&amp;gt;&amp;gt; run java application&quot;
  if ! java --version;
  then
    echo &quot;no JRE setup &amp;gt; JRE is required!!&quot;
    exit 50
  fi

  # ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션 실행 전 원격 서버에 jre가 제대로 설치되어 있는지 체크해줍니다. 여기서 더 나아가서 Sprign Boot application에서 사용된 java 버전까지 일치하는지도 체크하면 좋을 것 같습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1697303088706&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;run_app() {
  # ...
  
  nohup java -jar \
    -Dspring.profiles.active=&quot;${SPRING_PROFILE}&quot; \
    -Dserver.port=&quot;${PORT}&quot; \
    &quot;${APP_WORKSPACE}&quot;/&quot;${MODULE_NAME}&quot;.jar 1&amp;gt;&quot;${STD_OUT}&quot; 2&amp;gt;&quot;${STD_ERR}&quot; &amp;amp;

  # ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;nohup을 통해 백그라운드에서 java가 실행되도록 해줍니다. 여기에는 spring boot profile, port에 대한 환경변수도 등록해줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1697303110194&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;run_app() {
  # ...
  
  echo &quot;&amp;gt;&amp;gt; application(${PORT}) health check&quot;
  private_app_health_check

  # ...
}

CHECK_MAX_COUNT=10
CHECK_COUNT=0
private_app_health_check() {
  if [ $CHECK_COUNT -gt $CHECK_MAX_COUNT ];
  then
    echo &quot;Failure of running java application(${PORT})&quot;
    exit 50
  fi

  status_code=$(curl --connect-timeout 1 --max-time 3 -i -X GET http://localhost:&quot;${PORT}&quot;/monitoring/health | awk '/HTTP\/1/{print $2}')
  if [[ $status_code =~ ^2[0-9]{2}$ ]];
  then
    echo &quot;application(${PORT}) health check &amp;gt; UP&quot;
  else
    echo &quot;application(${PORT}) health check &amp;gt; DOWN&quot;
    sleep 3
    CHECK_COUNT=$(expr ${CHECK_COUNT} + 1)
    private_app_health_check
  fi
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;java 애플리케이션이 백그라운드로 실행되면서 완전히 up된 상태인지 체크를 해줘야 합니다. curl을 통해 java application에 등록해준 health check용 api를 3초 텀으로 계속 상태 체크를 하게 됩니다.&lt;br /&gt;&lt;br /&gt;특히, &lt;b&gt;curl 요청시 i option&lt;/b&gt;을 부여하면 HTTP response 전문을 확인할 수 있는데 가장 첫 번째 줄에 &lt;b&gt;HTTP/1.1 200&lt;/b&gt; 응답중 200 status code를 확인함으로써 애플리케이션이 제대로 떠있는지 확인할 수 있습니다.&lt;br /&gt;만약 애플리케이션의 health check api 응답이 200대의 성공응답으로 아직 받지 못했다면 recursive 형식으로 다시 해당 funciton을 호출하게 됩니다.&lt;br /&gt;&lt;br /&gt;Spring Boot 애플리케이션이 실행완료되는 시점까지 오래걸린다면 CHECK_MAX_COUNT 값을 상향 조정해서 health check를 더 많이 잡을 수 있습니다. &lt;b&gt;개발하고 있는 프로젝트의 규모에 따라 health check 횟수를 조정하면 될 것 같습니다.&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1697303234969&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;run_app() {
  # ...
  
  private_toggle_proxy_upstream up
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;health check까지 수행해서 애플리케이션이 제대로 뜬 상태임을 확인했다면 nginx config에서 upstream down시켰던 것을 다시 원복을 시킴으로써 해당 포트와 nginx를 다시 연결해야 합니다. 위에서 nginx upstream 연결을 down할 때 사용했던 function을 사용해서 처리하면 됩니다. 위의 과정까지 성공적으로 마쳤다면 nginx upstream에는 이제 8080, 8081 포트가 둘 다 연결된 상태가 됩니다.&lt;br /&gt;&lt;br /&gt;실행 스크립트에서 rolling 배포 방식에 있어 중요한 부분만 일부 가져와서 배포 과정을 정리해보았는데요. 위의 과정을 8080, 8081 포트에 대해서 각각 수행한다고 보시면 됩니다.&lt;/p&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&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;b&gt;6. Rolling 배포방식에 대한 생각과 정리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;rolling 배포방식과 실행스크립트를 통해서 구체적으로 어떻게 애플리케이션이 적용되는지 알아보았습니다. rolling 배포방식은 다른 배포방식에 비해 가장 간단한 방식이라 할 수 있습니다. 서비스 중인 여러 개의 애플리케이션을 차례차례로 구버전에서 신버전으로 갈아끼우는 방식으로 진행되다보니 스크립트 작성하는 것도 크게 어렵진 않았는데요.&lt;b&gt; 하지만 제가 몸을 담고 있는 실무에서는 rolling 방식보다 blue-green 방식을 선호하는 것 같습니다.&lt;/b&gt; 이유는 다음과 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;6-1. rolling 배포과정에서 특정 애플리케이션에 트래픽이 몰리는 상황 발생&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_연습-3.jpg&quot; data-origin-width=&quot;2157&quot; data-origin-height=&quot;1357&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/PCnjD/btsyuKwe1dx/p2FEMfDORpjUgfz7mZPS80/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/PCnjD/btsyuKwe1dx/p2FEMfDORpjUgfz7mZPS80/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PCnjD/btsyuKwe1dx/p2FEMfDORpjUgfz7mZPS80/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPCnjD%2FbtsyuKwe1dx%2Fp2FEMfDORpjUgfz7mZPS80%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;760&quot; height=&quot;588&quot; data-filename=&quot;edited_연습-3.jpg&quot; data-origin-width=&quot;2157&quot; data-origin-height=&quot;1357&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 예시를 가지고 보면 8080포트에 대해서 v2 적용중인 상황에서는 8081포트의 애플리케이션만 온전히 모든 트래픽들을 감당해야 합니다. 트래픽을 분산 처리했다가 하나로 집중된다면 애플리케이션이 감당 못할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;6-2. 이전 버전과 새로운 버전이 혼합된 상태 발생&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;rolling 방식은 배포 과정에서 두 개의 버전이 혼용된 상태가 될 수 있는데요. 이부분도 rolling 방식의 단점이라고 많은 곳에서 언급하고 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_연습-4 2.jpg&quot; data-origin-width=&quot;2153&quot; data-origin-height=&quot;1378&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eNhIHU/btsyth9qMz9/7bOnPO9aRDcWZDQOzRpiak/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eNhIHU/btsyth9qMz9/7bOnPO9aRDcWZDQOzRpiak/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eNhIHU/btsyth9qMz9/7bOnPO9aRDcWZDQOzRpiak/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeNhIHU%2Fbtsyth9qMz9%2F7bOnPO9aRDcWZDQOzRpiak%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;760&quot; height=&quot;588&quot; data-filename=&quot;edited_연습-4 2.jpg&quot; data-origin-width=&quot;2153&quot; data-origin-height=&quot;1378&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 4개의 애플리케이션이 서비스되고 있는 상태에서 rolling 배포를 적용한다면 배포 과정에서 아주 잠깐이더라도 위 그림과 같이 &lt;b&gt;v1, v2 버전이 혼용되어 트래픽을 처리하는 상태가 될 수 있습니다.&lt;/b&gt; 버전이 혼용되어 적용되어있다는 것 자체가 어떤 사이드 이펙트를 초래할지 모르기 때문에 rolling 배포 방식을 선택하는데 있어서 이러한 부분도 충분히 고려해야할 것 같습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;rolling 방식에 대해서 정리해보았는데요. 다음 글에서 &lt;b&gt;Blue Green 배포 전략&lt;/b&gt;을 이용한 무중단 배포에 대해 작성해보도록 하겠습니다.&lt;br /&gt;&lt;br /&gt;위의 내용들에 대한 실행 스크립트 내용은 github repo에서 관리하고 있는데 참고용으로 링크걸어두겠습니다.&lt;br /&gt;(&lt;a href=&quot;https://github.com/beaniejoy/ansible-deploy-script/blob/main/roles/run-app/templates/rolling-update-deploy.sh.j2&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/beaniejoy/ansible-deploy-script/blob/main/roles/run-app/templates/rolling-update-deploy.sh.j2&lt;/a&gt;)&lt;br /&gt;&lt;br /&gt;위에서 설정한 ansible role 관련 내용은 다음의 github repo 참고하시면 될 것 같습니다.&lt;br /&gt;(&lt;a href=&quot;https://github.com/beaniejoy/ansible-deploy-script&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/beaniejoy/ansible-deploy-script&lt;/a&gt;)&lt;/p&gt;
&lt;figure id=&quot;og_1697384206137&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - beaniejoy/ansible-deploy-script: ansible playbook for application(Spring Boot) deployment&quot; data-og-description=&quot;ansible playbook for application(Spring Boot) deployment - GitHub - beaniejoy/ansible-deploy-script: ansible playbook for application(Spring Boot) deployment&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/beaniejoy/ansible-deploy-script&quot; data-og-url=&quot;https://github.com/beaniejoy/ansible-deploy-script&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/zf49Q/hyUdPQmyn7/QHiGpoZs1kvdPTOwRTdwLK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/beaniejoy/ansible-deploy-script&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/beaniejoy/ansible-deploy-script&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/zf49Q/hyUdPQmyn7/QHiGpoZs1kvdPTOwRTdwLK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - beaniejoy/ansible-deploy-script: ansible playbook for application(Spring Boot) deployment&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;ansible playbook for application(Spring Boot) deployment - GitHub - beaniejoy/ansible-deploy-script: ansible playbook for application(Spring Boot) deployment&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;틀린 내용이 있을 수 있습니다. 언제나 건강한 피드백 환영합니다. 감사합니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Infra</category>
      <category>ansible</category>
      <category>deploy</category>
      <category>jenkins</category>
      <category>Rolling</category>
      <category>배포전략</category>
      <author>beaniejoy</author>
      <guid isPermaLink="true">https://beaniejoy.tistory.com/104</guid>
      <comments>https://beaniejoy.tistory.com/104#entry104comment</comments>
      <pubDate>Mon, 16 Oct 2023 00:41:27 +0900</pubDate>
    </item>
    <item>
      <title>Jenkins와 Ansible을 이용한 CI/CD 프로세스 만들어보기 (내가 만든 서비스 배포까지)</title>
      <link>https://beaniejoy.tistory.com/103</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Jenkins와 Ansible을 이용해 Spring Boot application을 빌드하고 서버에 배포하는 모든 과정(CI/CD)을 자동화해보는 내용을 정리해보려고 합니다. 이 글을 통해 Jenkins와 Ansible을 통해 빌드, 배포 과정을 처음 만들어보려는 분들에게 작게나마 도움이 되셨으면 좋겠습니다. &lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;본론으로 바로 들어가보겠습니다.&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;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;  1. 준비물&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;CI/CD 프로세르를 만들어보기 전에 준비물이 있는데요. Spring Boot 프로젝트, Jenkins가 설치된 서버(로컬, 클라우드 서버 상관없습니다), Ansible이 필요한데요. 지난 저의 게시글 중에 Jenkins, Ansible 간단하게 구성해보는 글이 있으니 참고하셔도 좋을 것 같습니다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;또한 배포할 대상이 되는 간단한 Spring Boot 프로젝트도 하나 있으면 좋을 것 같습니다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;추가로 저는 Spring Boot에 vault secret을 연동을 한 상태입니다. 그래서 Jenins pipeline에 vault secret 정보를 가져오는 내용도 있는데요. vault 서버도 하나 있으면 좋을 것 같지만 선택사항이라 프로젝트에 vault를 연동하고 있지 않다면 넘어가셔도 좋습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;- Ansible 구성&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;a href=&quot;https://beaniejoy.tistory.com/101&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://beaniejoy.tistory.com/101&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1694706319710&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Jenkins] Ansible plugin 사용해보기(ansible 설치부터 pipeline까지 작업)&quot; data-og-description=&quot;지난 게시글 중에 Jenkins 서버를 설치하고 Spring boot project 대상으로 간단하게 테스트, 빌드까지 해보는 Jenkins pipeline을 적용해보는 글이 있었습니다. 이번 게시글을 읽기 전에 먼저 읽어보시는 것&quot; data-og-host=&quot;beaniejoy.tistory.com&quot; data-og-source-url=&quot;https://beaniejoy.tistory.com/101&quot; data-og-url=&quot;https://beaniejoy.tistory.com/101&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/1vLqa/hyTVXHD0bP/OCsXuci0deapd8Uwsi3Mu0/img.png?width=800&amp;amp;height=596&amp;amp;face=531_242_622_342,https://scrap.kakaocdn.net/dn/kihxe/hyTV0K8TRK/Ws8qofeqdOxzCB9G611Ov0/img.png?width=800&amp;amp;height=596&amp;amp;face=531_242_622_342,https://scrap.kakaocdn.net/dn/cHtW1Y/hyTV29Z9kk/SMsAG7as91GrmzI8cbPHkK/img.png?width=1728&amp;amp;height=1418&amp;amp;face=0_0_1728_1418&quot;&gt;&lt;a href=&quot;https://beaniejoy.tistory.com/101&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://beaniejoy.tistory.com/101&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/1vLqa/hyTVXHD0bP/OCsXuci0deapd8Uwsi3Mu0/img.png?width=800&amp;amp;height=596&amp;amp;face=531_242_622_342,https://scrap.kakaocdn.net/dn/kihxe/hyTV0K8TRK/Ws8qofeqdOxzCB9G611Ov0/img.png?width=800&amp;amp;height=596&amp;amp;face=531_242_622_342,https://scrap.kakaocdn.net/dn/cHtW1Y/hyTV29Z9kk/SMsAG7as91GrmzI8cbPHkK/img.png?width=1728&amp;amp;height=1418&amp;amp;face=0_0_1728_1418');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[Jenkins] Ansible plugin 사용해보기(ansible 설치부터 pipeline까지 작업)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;지난 게시글 중에 Jenkins 서버를 설치하고 Spring boot project 대상으로 간단하게 테스트, 빌드까지 해보는 Jenkins pipeline을 적용해보는 글이 있었습니다. 이번 게시글을 읽기 전에 먼저 읽어보시는 것&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;beaniejoy.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;- Jenkins 구성&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;a href=&quot;https://beaniejoy.tistory.com/95&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://beaniejoy.tistory.com/95&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1694706347460&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Jenkins] Lightsail에 Jenkins server 구축해보고 Spring project 빌드해보기&quot; data-og-description=&quot;목적 - AWS Lightsail을 통해 생성한 instance에 jenkins server 구축한 내용 기록용 목표 - AWS Lightsail에 띄운 instance에 jenkins server 띄우기 - jenkins server 기본적인 설정 - jenkins item 생성 후 spring project build 해&quot; data-og-host=&quot;beaniejoy.tistory.com&quot; data-og-source-url=&quot;https://beaniejoy.tistory.com/95&quot; data-og-url=&quot;https://beaniejoy.tistory.com/95&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bOw9C4/hyTVRglBEW/sl764cyqaQyS4jFapFV4R0/img.png?width=676&amp;amp;height=411&amp;amp;face=318_36_400_125,https://scrap.kakaocdn.net/dn/czjleB/hyTVZFsklF/vkGeuC4Mt37bA5VslaWkP0/img.png?width=676&amp;amp;height=411&amp;amp;face=318_36_400_125,https://scrap.kakaocdn.net/dn/KC58e/hyTVPixab1/g9JfUtFfBszPsR8wBkjdW0/img.png?width=1980&amp;amp;height=1366&amp;amp;face=0_0_1980_1366&quot;&gt;&lt;a href=&quot;https://beaniejoy.tistory.com/95&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://beaniejoy.tistory.com/95&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bOw9C4/hyTVRglBEW/sl764cyqaQyS4jFapFV4R0/img.png?width=676&amp;amp;height=411&amp;amp;face=318_36_400_125,https://scrap.kakaocdn.net/dn/czjleB/hyTVZFsklF/vkGeuC4Mt37bA5VslaWkP0/img.png?width=676&amp;amp;height=411&amp;amp;face=318_36_400_125,https://scrap.kakaocdn.net/dn/KC58e/hyTVPixab1/g9JfUtFfBszPsR8wBkjdW0/img.png?width=1980&amp;amp;height=1366&amp;amp;face=0_0_1980_1366');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[Jenkins] Lightsail에 Jenkins server 구축해보고 Spring project 빌드해보기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;목적 - AWS Lightsail을 통해 생성한 instance에 jenkins server 구축한 내용 기록용 목표 - AWS Lightsail에 띄운 instance에 jenkins server 띄우기 - jenkins server 기본적인 설정 - jenkins item 생성 후 spring project build 해&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;beaniejoy.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;- Vault 구성&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;a href=&quot;https://beaniejoy.tistory.com/100&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://beaniejoy.tistory.com/100&lt;/a&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1694962474867&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Vault 서버를 설치해보자(AWS, Lightsail에 vault 서버 구축해보기)&quot; data-og-description=&quot;Spring Boot 애플리케이션을 개발하다보면 민감한 정보들을 설정해야할 때가 있습니다. DB 연동시 필수적으로 입력해야 하는 jdbc url, username, password 정보도 있고 Security 인증 관련해서 토큰 발급을 &quot; data-og-host=&quot;beaniejoy.tistory.com&quot; data-og-source-url=&quot;https://beaniejoy.tistory.com/100&quot; data-og-url=&quot;https://beaniejoy.tistory.com/100&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bIgE2x/hyTVXIzmZP/xfBraiSN8yJGzsh9RZGioK/img.png?width=800&amp;amp;height=418&amp;amp;face=0_0_800_418,https://scrap.kakaocdn.net/dn/hADUe/hyTV3Py664/458vQeNG12YUilsJoEdgD0/img.png?width=800&amp;amp;height=418&amp;amp;face=0_0_800_418,https://scrap.kakaocdn.net/dn/Uqsk9/hyTVY8yoWh/82JLsQ4oSG3apY4YWKQbk0/img.png?width=3490&amp;amp;height=1956&amp;amp;face=0_0_3490_1956&quot;&gt;&lt;a href=&quot;https://beaniejoy.tistory.com/100&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://beaniejoy.tistory.com/100&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bIgE2x/hyTVXIzmZP/xfBraiSN8yJGzsh9RZGioK/img.png?width=800&amp;amp;height=418&amp;amp;face=0_0_800_418,https://scrap.kakaocdn.net/dn/hADUe/hyTV3Py664/458vQeNG12YUilsJoEdgD0/img.png?width=800&amp;amp;height=418&amp;amp;face=0_0_800_418,https://scrap.kakaocdn.net/dn/Uqsk9/hyTVY8yoWh/82JLsQ4oSG3apY4YWKQbk0/img.png?width=3490&amp;amp;height=1956&amp;amp;face=0_0_3490_1956');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Vault 서버를 설치해보자(AWS, Lightsail에 vault 서버 구축해보기)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Spring Boot 애플리케이션을 개발하다보면 민감한 정보들을 설정해야할 때가 있습니다. DB 연동시 필수적으로 입력해야 하는 jdbc url, username, password 정보도 있고 Security 인증 관련해서 토큰 발급을&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;beaniejoy.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&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;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;  2. 배포 전 빌드 순서 Overview&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&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;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;DB Validate (flyway)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Test&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Build&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Deploy (ssh publisher)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Run Application (by ansible playbook)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;2-1. Flyway 사용한 DB Validate&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;가장 첫번째로 flyway를 사용한 DB Validate를 진행합니다. 이 과정은 제가 빌드 시작단계에 적용한 단계인데요.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;과거 실무에서 일하면서 실환경 DB에 테이블 변경(DDL) 내용이 적용이 안되었는데 배포가 진행되었던 경험이 있는데요.이것이 생각이 나서 개인 프로젝트에서 DB Migration 용도로 사용하고 있는 flyway를 통해 DB에 제대로 변경분이 모두 적용된 상태인지 체크하는 validate 과정을 넣게 되었습니다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;이런 식으로 빌드 및 테스트 전에 DB 확인부터 하니 안전성도 챙기고 쓸데 없는 &lt;b&gt;리소스 낭비도 줄이고&lt;/b&gt; 꽤 괜찮게 사용하고 있습니다. 이러한 부분도 고려해봐도 좋을 거 같네요.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;(&lt;b&gt;리소스 낭비를 줄인다는 것&lt;/b&gt;은 실컷 테스트 다 수행하고 빌드, 배포까지 잘 이루어졌는데 애플리케이션 실행단계에서 DB 내용이 달라서 오류가 발생한다면 그 이전의 모든 작업들이 헛수고가 되어버리는 것 같았습니다. &lt;b&gt;사전에 이러한 일들을 방지한다는 의미에서 리소스 낭비를 줄인다고 표현을 했습니다.&lt;/b&gt;)&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;여기서는 Jenkins의 &lt;b&gt;Flyway Runner&lt;/b&gt; 플러그인을 적용해보려 합니다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;제가 쓴 게시글 중에 Jenkins - flyway runner 플러그인 적용해보는 글이 있는데요. 참고하시면 좋을 것 같습니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;a href=&quot;https://beaniejoy.tistory.com/98&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://beaniejoy.tistory.com/98&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1694957640175&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Jenkins의 flywayrunner plugin을 통해 DB migration 자동화하기(jenkins pipeline)&quot; data-og-description=&quot;목표 - DB migration 용도의 Jenkinsfile 작성해보기 - Jenkins pipeline을 통해 flyway migration 자동화 적용 - Jenkins에서 item에 버튼 하나 누르면 알아서 migration 작업 진행하도록 적용 - DB 테이블에 대한 migration &quot; data-og-host=&quot;beaniejoy.tistory.com&quot; data-og-source-url=&quot;https://beaniejoy.tistory.com/98&quot; data-og-url=&quot;https://beaniejoy.tistory.com/98&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dtQLqF/hyTVWiAcWt/JkJKwby4rgqGl3lckVGKIK/img.png?width=800&amp;amp;height=368&amp;amp;face=0_0_800_368,https://scrap.kakaocdn.net/dn/iTng8/hyTV2JR2Cn/Oy8p2nVfLNNff4MWH8f4Ck/img.png?width=800&amp;amp;height=368&amp;amp;face=0_0_800_368,https://scrap.kakaocdn.net/dn/XL0t5/hyTVYU13tl/VKkmCvrDmvqRinO4kfvVjk/img.png?width=1014&amp;amp;height=832&amp;amp;face=0_0_1014_832&quot;&gt;&lt;a href=&quot;https://beaniejoy.tistory.com/98&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://beaniejoy.tistory.com/98&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dtQLqF/hyTVWiAcWt/JkJKwby4rgqGl3lckVGKIK/img.png?width=800&amp;amp;height=368&amp;amp;face=0_0_800_368,https://scrap.kakaocdn.net/dn/iTng8/hyTV2JR2Cn/Oy8p2nVfLNNff4MWH8f4Ck/img.png?width=800&amp;amp;height=368&amp;amp;face=0_0_800_368,https://scrap.kakaocdn.net/dn/XL0t5/hyTVYU13tl/VKkmCvrDmvqRinO4kfvVjk/img.png?width=1014&amp;amp;height=832&amp;amp;face=0_0_1014_832');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Jenkins의 flywayrunner plugin을 통해 DB migration 자동화하기(jenkins pipeline)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;목표 - DB migration 용도의 Jenkinsfile 작성해보기 - Jenkins pipeline을 통해 flyway migration 자동화 적용 - Jenkins에서 item에 버튼 하나 누르면 알아서 migration 작업 진행하도록 적용 - DB 테이블에 대한 migration&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;beaniejoy.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;2-2. Test 및 Build&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;DB까지 잘 적용된 것을 확인했으면 그 다음에 테스트와 빌드를 수행하게 됩니다. 이를 통해 서버에 배포할 jar 파일을 생성하게 되는데요. Spring Boot 프로젝트에 있는 gradle wrapper를 통해 수행하게 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;2-3. Deploy&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;테스트와 빌드를 성공적으로 마치게 되면 jar 파일이 생성되는데요. 해당 파일은 실행가능한 파일이기 때문에 java 명령어를 통해 바로 애플리케이션을 동작하게 할 수 있습니다. 그 전에 애플리케이션을 돌릴 서버에 해당 jar파일을 건네줘야 하는데요. Deploy 단계에서 서버로 ssh를 이용해 파일 전송을 하게 됩니다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;여기서 Jenkins의 &lt;b&gt;Publish over SSH&lt;/b&gt;라는 플러그인을 적용해서 사용해보려 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;2-4. Run Application&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;배포한 애플리케이션 jar 파일을 절차에 따라 실행을 단계입니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;애플리케이션 실행 단계에서는 Ansible을 적극 활용하려 합니다. Jenkins에서도 &lt;b&gt;Ansible plugin&lt;/b&gt;을 제공하는데 해당 플러그인을 적용해서 ansible playbook으로 애플리케이션 실행을 해보겠습니다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Jenkins에 Ansible plugin 적용해보는 내용은 이전 게시글에서 확인할 수 있는데요. 이것도 역시 위의 준비물에서 Ansible 링크 드렸습니다. 참고하시면 좋을 것 같네요.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&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;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;  3. pipeline 구성하기&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;pipeline은 여러 개의 stage를 기반으로 순차적으로 작업이 이루어지게 됩니다. 위에서 소개한 빌드 순서를 기반으로 jenkins pipeline의 stage들 구성해보려 합니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;프로젝트 빌드, 배포를 위한 stage를 위에서 간략히 소개했던 것처럼, 개인적으로 CI/CD를 구성해보고자 할 때 먼저 전체적인 stage들을 구상해보고 pipeline script를 작성하면 수월하게 하실 수 있을 거라 생각합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;3-1. Inititalize 단계(필요한 변수 선언 및 할당)&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;각 stage 단계에서 필요한 변수들을 선언하고 할당하는 작업을 진행합니다. 예를 들어 Spring Boot 프로젝트를 test, build를 하기 위한 MODULE_NAME, 그리고 flyway를 수행하기 위한 conf 파일이나 ansible을 수행하기 위한 playbook, inventory hosts 파일 위치나 이름 등등 각 stage에서 필요한 변수들을 선언하고 할당해줍니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1694960831113&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;stage('Init') {
    steps {
        script {
            PROJECT_PROFILE = 'prod'
            PROJECT_NAME = 'service-api'
            PARENT_MODULE_NAME = 'app'
            MODULE_NAME = &quot;${PARENT_MODULE_NAME}:${PROJECT_NAME}&quot;

            // flyway migration
            MIGRATION_WORKSPACE = &quot;${WORKSPACE}/flyway&quot;

            // ansible playbook
            ANSIBLE_INVENTORY = &quot;${HOME}/ansible/inventory&quot;
            ANSIBLE_PLAYBOOK = &quot;${WORKSPACE}/scripts/deploy/${PROJECT_NAME}.yml&quot;
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;또한 변수를 위의 내용처럼 직접 할당하는 것들도 있지만 &lt;b&gt;vault secret 혹은 jenkins credentials&lt;/b&gt; 같은 곳에서 필요한 정보들을 가져와야 하는 경우도 있을 수 있습니다. 저 같은 경우 vault secret에서 필요한 정보들이 있어서 직접 가져와 값을 할당했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1694962282330&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;def secrets = [
    [
        path: &quot;secret/dongne/${PROJECT_PROFILE}&quot;,
        engineVersion: 2,
        secretValues: [
            [envVar: 'jdbcUrl', vaultKey: 'spring.datasource.hikari.jdbc-url']
        ]
    ],
    [
        path: &quot;vault/config&quot;,
        engineVersion: 2,
        secretValues: [
            [envVar: 'vaultAddr', vaultKey: 'VAULT_ADDR'],
            [envVar: 'vaultToken', vaultKey: 'VAULT_TOKEN']
        ]
    ],
]

withVault([
    vaultSecrets: secrets,
    configuration: [
        engineVersion: 2,
        timeout: 10
    ]
]) {
    JDBC_URL = &quot;${jdbcUrl}&quot;
    VAULT_ADDR = &quot;${vaultAddr}&quot;
    VAULT_TOKEN = &quot;${vaultToken}&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Jenkins pipeline에 vault secret 내용을 가져오려면 &lt;b&gt;vault plugin(&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;HashiCorp Vault&lt;/span&gt;)&lt;/b&gt;이 따로 필요한데요. 연동하는 방법은 아주 간단한데 아직 관련 글을 작성해보진 않았네요. Jenkins - Vault 연동 방법에 대해 간략하게라도 글 작성 완료되면 여기에도 업데이트를 해두겠습니다.&lt;br /&gt;(여기서는 vault가 중요한 내용이 아니라 이런 게 있구나라고만 생각하시고 가볍게 넘어가면 좋을 것 같습니다.)&lt;br /&gt;&lt;br /&gt;위의 내용대로 직접 할당하든 vault나 jenkins credentials에서 값을 가져와 할당하든 필요한 정보들을 선언하고 할당하는 내용이 Initialize stage에서 이루어진다고 보면 될 것 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;3-2. DB Validate 단계&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;flyway migration 내역이 실제 DB에 잘 적용이 되어있는지 체크하는 stage입니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1694966031941&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;stage('DB Validate') {
    steps {
        flywayrunner installationName: 'flyway-jenkins',
                flywayCommand: 'info validate',
                commandLineArgs: &quot;-configFiles=${MIGRATION_WORKSPACE}/flyway-${PROJECT_PROFILE}.conf&quot;,
                credentialsId: &quot;VAULT_DB_CONNECTION_${PROJECT_PROFILE.toString().toUpperCase()}&quot;,
                url: &quot;${JDBC_URL}&quot;,
                locations: &quot;filesystem:${MIGRATION_WORKSPACE}/migration&quot;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;(flywayrunner에 대한 pipeline syntax는 위에 제가 링크드린 flyway runner 관련 글을 참고하시면 됩니다.)&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;gradle에서도 따로 plugin을 제공해주고 있어서 gradle wrapper를 통해 flyway 관련 작업을 할 수 있는데요. gradle로 flyway 진행하는 것이 flyway를 직접 설치해서 수행하는 것보다 빌드 진행속도가 조금 느리게 느껴졌습니다.&lt;br /&gt;&lt;br /&gt;그래서 저 같은 경우 &lt;b&gt;flyway runner&lt;/b&gt;라는 jenkins plugin을 이용해 서버에 설치된 flyway 패키지와 연동해서 사용하고 있습니다. 물론 각각에 장단점이 있는 것 같습니다. &lt;br /&gt;&lt;br /&gt;gradle plugin을 통한 flyway 작업은 따로 jenkins에 플러그인 설치나 패키지 설치 및 연동 작업을 하지 않고도 실행할 수 있다는 점에서 이식성이 좋다고 느꼈습니다. 즉 jenkins 서버를 다른 곳에 새로 구축하거나 확장하거나 할 때 이러한 방식이 괜찮을 것 같습니다. &lt;br /&gt;Jenkins 서버를 한 번 구축하고 굳이 확장하거나 옮기지 않을 거라면 플러그인 연동해서 사용해도 무방합니다. 각자의 환경에 맞는 방식을 선택하면 될 것 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;3-3. Test, Build&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1694966967004&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;stage('Test') {
    steps {
        sh &quot;./gradlew clean :${MODULE_NAME}:test&quot;
    }
}

stage('Build') {
    steps {
        sh &quot;./gradlew clean :${MODULE_NAME}:build -x test&quot;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Test, Build 단계는 따로 언급할 것이 없어 보입니다. spring boot project root 경로에 있는 gradle wrapper(./gradlew)를 통해 대상이 되는 모듈에 대한 test와 build를 각각 실행하면 됩니다.&lt;/p&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3-4. Deploy&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;build 단계를 통해 생성된 실행가능한 jar 파일을 서버로 배포하는 단계입니다.&lt;br /&gt;Publish over SSH 플러그인을 설치하면 Jenkins에서 pipeline syntax 페이지를 통해 간편하게 script를 작성할 수 있습니다.&lt;br /&gt;&lt;br /&gt;ssh publisher 스크립트를 구성하기 전에 jenkins 설정을 해야하는데요. &lt;br /&gt;&lt;b&gt;Jenkins 관리 &amp;gt; System&lt;/b&gt;에 들어가면 &lt;b&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;Publish over SSH&lt;/span&gt;&lt;/b&gt; 항목이 있습니다. &lt;br /&gt;(Jenkins 관리 &amp;gt; Plugins 에서 Publish over SSH 설치하셔야 합니다.)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2023-09-18 at 1.14.17 AM.png&quot; data-origin-width=&quot;1516&quot; data-origin-height=&quot;1680&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1FW5X/btst85smJSM/aCqgNxwNqK4Bs9lEbjf5dK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1FW5X/btst85smJSM/aCqgNxwNqK4Bs9lEbjf5dK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1FW5X/btst85smJSM/aCqgNxwNqK4Bs9lEbjf5dK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1FW5X%2Fbtst85smJSM%2FaCqgNxwNqK4Bs9lEbjf5dK%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;760&quot; height=&quot;842&quot; data-filename=&quot;Screenshot 2023-09-18 at 1.14.17 AM.png&quot; data-origin-width=&quot;1516&quot; data-origin-height=&quot;1680&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 항목에서 &lt;b&gt;SSH Servers&lt;/b&gt; 설정할 수 있는 칸들이 나오는데요. SSH Server는 배포 대상이 되는 서버이자 애플리케이션이 실행되는 서버라고 생각하시면 됩니다. 여기에 해당 서버에 대한 내용을 기입하면 됩니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Hostname&lt;/b&gt;: ssh server의 ip 주소를 기입하면 됩니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Username&lt;/b&gt;: ssh server의 사용자 이름(aws ec2는 ec2-user)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Remote Directory&lt;/b&gt;: ssh server에 전송이 될 파일들이 위치하는 기준이 되는 path입니다. 기본 workspace 경로라고 생각하면 됩니다. (aws ec2는 ec2-user 유저의 user directory로 설정)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;authentication&lt;/b&gt;: ssh를 위한 key 설정을 해야 합니다. 설정방식은 제각각이지만 저는 그냥 jenkins서버에 있는 &lt;b&gt;.ssh&lt;/b&gt; 디렉토리 안에 원격 서버에 대한 pem key 파일을 저장해두고 해당 경로를 설정했습니다. &lt;br /&gt;(Path to key &amp;gt; jenkins 서버의 경로를 기준으로 설정해야 합니다.)&lt;br /&gt;참고로, 위의 캡쳐본에서 &lt;b&gt;/home/ec2-user/.ssh/app-server.pem &lt;/b&gt;경로에서 &lt;b&gt;/home/ec2-user&lt;/b&gt;는 ssh server의 디렉토리가 아니라 &lt;b&gt;jenkins 서버의 디렉토리&lt;/b&gt;입니다. (저는 jenkins 서버도 ec2에 올려서 상용하고 있습니다.)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 내용과 같이 설정하고 저장합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정이 완료되면 ssh publisher pipeline script를 작성해야 하는데요. Jenkins에서 제공해주는 &lt;b&gt;Pipeline Syntax&lt;/b&gt; 기능을 사용합시다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2023-09-20 at 1.17.12 PM.png&quot; data-origin-width=&quot;2572&quot; data-origin-height=&quot;1406&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cFjSNh/btsuG3HhQaX/m37rDMvGYKVT4kEeaXgVKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cFjSNh/btsuG3HhQaX/m37rDMvGYKVT4kEeaXgVKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cFjSNh/btsuG3HhQaX/m37rDMvGYKVT4kEeaXgVKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcFjSNh%2FbtsuG3HhQaX%2Fm37rDMvGYKVT4kEeaXgVKk%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;760&quot; height=&quot;415&quot; data-filename=&quot;Screenshot 2023-09-20 at 1.17.12 PM.png&quot; data-origin-width=&quot;2572&quot; data-origin-height=&quot;1406&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Pipeline Syntax에 가시면 sshPublisher 선택하면 아래 관련 설정필드들이 나옵니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2023-09-20 at 1.18.24 PM.png&quot; data-origin-width=&quot;2374&quot; data-origin-height=&quot;1576&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c0x2Hm/btsuJa0gTZI/8s9mLE86q8oiKsVL8kmqHk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c0x2Hm/btsuJa0gTZI/8s9mLE86q8oiKsVL8kmqHk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c0x2Hm/btsuJa0gTZI/8s9mLE86q8oiKsVL8kmqHk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc0x2Hm%2FbtsuJa0gTZI%2F8s9mLE86q8oiKsVL8kmqHk%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;760&quot; height=&quot;505&quot; data-filename=&quot;Screenshot 2023-09-20 at 1.18.24 PM.png&quot; data-origin-width=&quot;2374&quot; data-origin-height=&quot;1576&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Source files, Remove prefix, Remote directory 내용만 입력했습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Source files&lt;/b&gt;: Jenkins 서버에서 빌드했던 jar파일의 절대경로&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Remove prefix&lt;/b&gt;: 원격 서버로 보낼 때 Source files에 지정했던 절대경로가 포함이되어 전송이 되는데요. Jenkins 서버와 원격서버의 파일위치는 독립적으로 가져가야 하기에 절대경로 부분을 제거하고 딱 파일만 지정되도록 remove prefix를 지정합니다.&lt;br /&gt;(ex. /this/path/to/application.jar ... remove prefix: /this/path/to &amp;gt;&amp;gt; &lt;b&gt;application.jar&lt;/b&gt; )&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Remote directory&lt;/b&gt;: 위에 SSH Server 설정할 때 Remote Directory를 &lt;b&gt;/home/ec2-user&lt;/b&gt;로 지정했는데요. 여기서 지정한 디렉토리를 기준으로 어느 위치에 source file을 보내겠느냐하는 내용입니다. 저는 &lt;b&gt;deploy&lt;/b&gt; 디렉토리를 설정했고 &lt;b&gt;/home/ec2-user/deploy&lt;/b&gt; 위치에 배포파일이 보내지게끔 했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2023-09-20 at 1.18.42 PM.png&quot; data-origin-width=&quot;2856&quot; data-origin-height=&quot;864&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bxrmGk/btsuQhZF84A/VldO8TZsboZA5pRsSNK5Y0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bxrmGk/btsuQhZF84A/VldO8TZsboZA5pRsSNK5Y0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bxrmGk/btsuQhZF84A/VldO8TZsboZA5pRsSNK5Y0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbxrmGk%2FbtsuQhZF84A%2FVldO8TZsboZA5pRsSNK5Y0%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;760&quot; height=&quot;230&quot; data-filename=&quot;Screenshot 2023-09-20 at 1.18.42 PM.png&quot; data-origin-width=&quot;2856&quot; data-origin-height=&quot;864&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정 필드들을 채우고 맨아래에 Generate Pipeline Script 버튼을 클릭하면 위와 같이 ssh publisher에 대한 완성된 pipeline script 코드가 나오게 됩니다. 위 내용을 복사해서 Jenkinsfile에 stage 구문 안에 집어넣으면 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1694968031781&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;stage('Deploy') {
    steps {
        echo 'Deploy JAR file'
        sshPublisher(publishers: [
                sshPublisherDesc(
                        configName: 'app-server',
                        transfers: [
                                sshTransfer(
                                        cleanRemote: false,
                                        excludes: '',
                                        execCommand: '',
                                        execTimeout: 120000,
                                        flatten: false,
                                        makeEmptyDirs: false,
                                        noDefaultExcludes: false,
                                        patternSeparator: '[, ]+',
                                        remoteDirectory: 'deploy',
                                        remoteDirectorySDF: false,
                                        removePrefix: &quot;${PARENT_MODULE_NAME}/${PROJECT_NAME}/build/libs&quot;,
                                        sourceFiles: &quot;${PARENT_MODULE_NAME}/${PROJECT_NAME}/build/libs/${PROJECT_NAME}.jar&quot;
                                )
                        ],
                        usePromotionTimestamp: false,
                        useWorkspaceInPromotion: false,
                        verbose: false
                )
        ])
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3-5. &lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Run Application&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Ansible playbook을 이용해 원격서버에서 application을 실행하는 단계입니다. Jenkins에 Ansible 관련 설정은 맨위에 제가 작성했던 Ansible 관련 글을 첨부해두었는데요. 그것을 참고하시면 될 것 같습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1695228597337&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;stage('Run Application with Ansible') {
    steps {
        ansiblePlaybook(
                installation: 'ansible',
                playbook: &quot;${ANSIBLE_PLAYBOOK}&quot;,
                inventory: &quot;${ANSIBLE_INVENTORY}&quot;,
                extraVars: [
                    module_name: &quot;${PROJECT_NAME}&quot;,
                    jenkins_user_home: &quot;${HOME}&quot;,
                    spring_profile: &quot;${PROJECT_PROFILE}&quot;,
                    vault_address: &quot;${VAULT_ADDR}&quot;,
                    vault_token: &quot;${VAULT_TOKEN}&quot;
                ]
        )
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;installation, playbook, inventory부분은 이전에 제가 작성했던 글에서 설명하고 있어서 넘어가도록 하겠습니다. &lt;br /&gt;&lt;b&gt;extraVars&lt;/b&gt; 부분은 ansible playbook에서 사용할 수 있도록 제공해주는 일종의 환경변수 같은 것들입니다. spring boot를 실행시키기 위한 값들을 위주로 설정했습니다.&lt;br /&gt;&lt;br /&gt;Jenkinsfile 전체 내용에 대해서 링크 따로 남겨놓겠습니다. 위의 내용하고 다른 점들이 있긴 한데요. 참고하시면 좋을 것 같습니다.&lt;br /&gt;&lt;a href=&quot;https://github.com/beaniejoy/dongne-cafe-api/blob/01a9f61f44f3fa0f6a918727178c2dcaeae1da33/Jenkinsfile-service&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/beaniejoy/dongne-cafe-api/blob/01a9f61f44f3fa0f6a918727178c2dcaeae1da33/Jenkinsfile-service&lt;/a&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;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;  4. ansible playbook 작성하기&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Jenkins pipeline이 작성이 완료되었으면 다음으로 ansible을 실행할 playbook을 작성해야합니다. 여기서는 playbook을 아주 간단하게 작성해보려고 합니다. 또한 shell script를 통해 무중단 배포 전략 없이 간단하게 중단(?) 배포를 통해 application을 실행시키는 것까지 정리해보겠습니다.&lt;br /&gt;(ansible playbook에 대한 사용법, 문법같은 경우 너무나 방대한 내용을 담고 있기 때문에 document를 참고하면서 아래 내용을 살펴보시는 걸 추천드립니다. &lt;a href=&quot;https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_intro.html#ansible-playbooks&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;ansible playbook doc&lt;/a&gt;)&lt;/p&gt;
&lt;figure id=&quot;og_1695230359848&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Ansible playbooks &amp;mdash; Ansible Documentation&quot; data-og-description=&quot;A playbook runs in order from top to bottom. Within each play, tasks also run in order from top to bottom. Playbooks with multiple &amp;lsquo;plays&amp;rsquo; can orchestrate multi-machine deployments, running one play on your webservers, then another play on your databas&quot; data-og-host=&quot;docs.ansible.com&quot; data-og-source-url=&quot;https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_intro.html#ansible-playbooks&quot; data-og-url=&quot;https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_intro.html#ansible-playbooks&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_intro.html#ansible-playbooks&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_intro.html#ansible-playbooks&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Ansible playbooks &amp;mdash; Ansible Documentation&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;A playbook runs in order from top to bottom. Within each play, tasks also run in order from top to bottom. Playbooks with multiple &amp;lsquo;plays&amp;rsquo; can orchestrate multi-machine deployments, running one play on your webservers, then another play on your databas&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.ansible.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4-1. 변수 설정하기&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1695229546827&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# run-app.yml (ansible playbook)
---
- name: Run java application
  hosts: ec2

  vars_files:
    - vars/all.yml
    - &quot;{{ jenkins_user_home }}/ansible/vars/app_config.yml&quot;

  tasks:
    ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;hosts&lt;/b&gt;는 inventory/hosts 파일에 지정한 대상 서버를 설정하게 됩니다. ec2 이름으로 설정된 내용이 inventory 안에 있는데 그것을 가리키도록 작성한 것입니다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;var_files&lt;/b&gt;는 해당 playbook에서 사용할 변수들을 어떤 파일에서 가져올 것인지 지정하는 것입니다. 해당 playbook을 기준으로 상대 경로로 하거나 절대경로로 설정할 수 있습니다. 그럼 변수들을 담은 파일에는 어떤 것들이 있는지 간단하게 살펴보도록 하겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1695229986787&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# ./vars/all.yml

remote_server_user: ec2-user
remote_server_group: ec2-user
remote_user_workspace: &quot;/home/{{ remote_server_user }}&quot;

remote_deploy_path: &quot;{{ remote_user_workspace }}/deploy&quot;
remote_app_path: &quot;{{ remote_user_workspace }}/app&quot;
remote_env_path: &quot;{{ remote_user_workspace }}/app/env&quot;
remote_script_path: &quot;{{ remote_user_workspace }}/app/scripts&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주로 remote server에 대한 내용들입니다. 사용자명, 그룹명을 작성했고 application을 실행하기 위해 필요한 directory들을 미리 변수로 만들어두었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4-2. 배포된 jar 파일 체크&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1695230139456&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# run-app.yml (ansible playbook)
tasks:
    - name: Register JAR file existence status
      ansible.builtin.stat:
        path: &quot;{{ remote_deploy_path }}/{{ module_name }}.jar&quot;
      register: file_status

    - name: Check if JAR file exists
      block:
        - name: Check 'file_status'
          ansible.builtin.fail:
            msg: &quot;{{ module_name }}.jar does not exist&quot;
          when: not file_status.stat.exists

        - name: Print success message
          ansible.builtin.debug:
            msg: &quot;{{ module_name }}.jar exists!!&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 playbook으로 돌아와 이제 ansible을 통해 어떤 작업을 지시할 것인지 tasks에 설정해야 합니다.&lt;br /&gt;&lt;br /&gt;처음에는 Jenkins Deploy 단계에서 배포한 jar 파일이 잘 있는지부터 체크합니다. file_status를 매개로 해서 jar 파일 존재유무 체크를 하게 되는데요. (&lt;b&gt;file_status.stat.exists&lt;/b&gt;)&lt;br /&gt;만약 존재하지 않으면 fail 처리해서 다음 단계 진행이 안되도록 하면 됩니다.&lt;/p&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4-3. 파일 이동 및 필요한 template 파일 전송&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1695230565653&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    - name: Copy JAR file for running application
      ansible.builtin.copy:
        src: &quot;{{ remote_deploy_path }}/{{ module_name }}.jar&quot;
        remote_src: yes
        dest: &quot;{{ remote_app_path }}/{{ module_name }}.jar&quot;
        owner: &quot;{{ remote_server_user }}&quot;
        group: &quot;{{ remote_server_group }}&quot;
        mode: 0644

    - name: Remove deployed JAR file
      ansible.builtin.file:
        state: absent
        path: &quot;{{ remote_deploy_path }}/{{ module_name }}.jar&quot;

    - name: Copy env file to remote server
      ansible.builtin.template:
        src: .env.j2
        dest: &quot;{{ remote_env_path }}/.env&quot;
        owner: &quot;{{ remote_server_user }}&quot;
        group: &quot;{{ remote_server_group }}&quot;
        mode: 0644

    - name: Copy service script file to remote server
      ansible.builtin.template:
        src: service.sh.j2
        dest: &quot;{{ remote_script_path }}/service.sh&quot;
        owner: &quot;{{ remote_server_user }}&quot;
        group: &quot;{{ remote_server_group }}&quot;
        mode: 0755&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ansible은 원격 서버 내부에 있는 파일을 다른 디렉토리로 이동시키는 것도 가능하고 ansible을 실행하는 서버에서 원격서버로 파일을 전송하는 것도 가능합니다.&lt;br /&gt;&lt;br /&gt;먼저 원격 서버에 배포된 jar 파일을 원하는 위치로 이동을 시킵니다. (copy 후 제거 처리) 그 다음 java 애플리케이션 실행시 필요한 환경변수를 담은 환경변수 파일(&lt;b&gt;.env&lt;/b&gt;)과 애플리케이션 실행을 위한 실행 스크립트 파일(&lt;b&gt;service.sh&lt;/b&gt;)을 원격서버로 보냅니다.&lt;br /&gt;&lt;br /&gt;기본적인 ansible playbook 작성법에 대해서는 링크드린 &lt;b&gt;ansible playbook doc&lt;/b&gt;을 참고하는 걸 추천드렸는데요. 그 중 &lt;b&gt;j2 template&lt;/b&gt;에 대해서는 간단하게 언급하고 넘어가는 것이 좋을 것 같습니다. 위의 스크립트를 보면 src에 이상한 j2 확장자로 끝나는 파일이 있습니다.&lt;br /&gt;(&lt;b&gt;service.sh.j2, .env.j2&lt;/b&gt;)&lt;br /&gt;&lt;br /&gt;&lt;a href=&quot;https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_templating.html#templating-jinja2&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;j2 template&lt;/a&gt;은 ansible playbook에서 제공하는 templating 기능인데요. j2 확장자로 끝나는 파일에 대해서 ansible playbook에 등록한 여러 변수들(&lt;b&gt;extraVars, vars_files&lt;/b&gt; 등 여러 방식을 통해 설정된 변수들)을 접근할 수 있게 됩니다. 이게 무슨 소리인지는 다음 예시를 보면 한 번에 이해되실 겁니다.&lt;/p&gt;
&lt;pre id=&quot;code_1695310089029&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# .env.js
DATABASE_HOST={{ database_host }}
DATABASE_USERNAME={{ database_username }}
DATABASE_PASSWORD={{ database_password }}

# run-app.yml (ansible playbook)
...
vars_files:
    - vars/all.yml
    - &quot;{{ jenkins_user_home }}/ansible/vars/app_config.yml&quot;
...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;.env.j2&amp;nbsp;&lt;/b&gt;파일입니다. spring boot application을 실행하기 위해 필요한 환경변수들을 담은 파일을 원격서버로 보내야 하는데요.&lt;br /&gt;위의 파일 내용 처럼 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;{{ 변수명 }}&lt;/b&gt;&lt;/span&gt; 이렇게 지정하면 ansible playbook으로 가져온 변수 내용으로 교체되어 원격서버로 전송이 된다고 생각하시면 됩니다. &lt;b&gt;{{ database_host }}&lt;/b&gt;은 그 아래 playbook에서 vars_files로 가져온 app_config.yml 파일에서 가져오게 됩니다.&lt;br /&gt;&lt;br /&gt;이렇게 j2 template은 playbook에 가져온 변수들에 접근할 수 있다는 점에서 유용합니다. ansible playbook은 이것을 이용해 &lt;b&gt;.env.j2&lt;/b&gt;와 애플리케이션 실행을 위한 shell script인 &lt;b&gt;service.sh.j2&lt;/b&gt; 파일을 원격 서버로 보내게 됩니다.&lt;/p&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4-4. 애플리케이션 실행 및 로그 확인&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1695311500971&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    - name: Run Application
      ansible.builtin.command: &quot;{{ remote_script_path }}/service.sh&quot;
      register: result
      environment:
        VAULT_DOMAIN_ADDR: &quot;{{ vault_address }}&quot;
        VAULT_AUTH_TOKEN: &quot;{{ vault_token }}&quot;
        
    - name: Show all prints while running service script
      ansible.builtin.debug:
        msg: &quot;{{ result.stdout_lines }}&quot;

    - name: Finish
      ansible.builtin.debug:
        msg: &quot;All tasks completed!!&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원격 서버로 보내진 service.sh 스크립트 파일을 통해 애플리케이션을 실행하게 되고(&lt;b&gt;ansible.builtin.command&lt;/b&gt;) 해당 스크립트 파일을 실행하면서 기록된 로그들을 result에 담아 다음 단계에서 출력(&lt;b&gt;ansible.builtin.&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;b&gt;debug&lt;/b&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;하게 됩니다. command로 스크립트 파일 실행할 때 &lt;b&gt;environment&lt;/b&gt; 통해 vault 관련 domain과 token 값을 환경변수로 등록합니다. (vault 내용은 스킵하셔도 됩니다.)&lt;br /&gt;&lt;br /&gt;여기까지 실행이 완료되면 배포를 위한 ansible playbook 완성입니다.&lt;br /&gt;&lt;br /&gt;위 내용에 대한 ansible playbook 전체 파일도 링크드리겠습니다. 내용이 조금 다를 순 있어서 참고만 하시면 좋을 것 같습니다.&lt;br /&gt;&lt;a href=&quot;https://github.com/beaniejoy/dongne-cafe-api/blob/01a9f61f44f3fa0f6a918727178c2dcaeae1da33/scripts/deploy/run-app.yml&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/beaniejoy/dongne-cafe-api/blob/01a9f61f44f3fa0f6a918727178c2dcaeae1da33/scripts/deploy/run-app.yml&lt;/a&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;b&gt;  5. Shell Script를 통한 애플리케이션 실행&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 애플리케이션 실행을 위한 shell script를 작성해야 하는데요. 이번 게시글에서는 정말 간단하게 애플리케이션 실행만 해보는 것을 목표로 작성을 해보았습니다. &lt;b&gt;그래서 배포전략도 없이 기존 java process 중단 &amp;gt; 신규 애플리케이션 실행하는 방식으로 작성했습니다. (&lt;b&gt;중단 배포 방식)&lt;/b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1695311829766&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#!/bin/bash

ENV_LOCATION={{ remote_env_path }}
APP_WORKSPACE={{ remote_app_path }}
MODULE_NAME={{ module_name }}
PORT=8080

SPRING_PROFILE={{ spring_profile }}

STD_OUT=${APP_WORKSPACE}/logs/stdout_${PORT}.log
STD_ERR=${APP_WORKSPACE}/logs/stderr_${PORT}.log

echo &quot;## move to application workspace&quot;
cd $APP_WORKSPACE

echo &quot;## kill current running application&quot;
CURRENT_PID = $(pgrep -f ${MODULE_NAME})

if [ -z &quot;${CURRENT_PID}&quot; ];
then
  echo &quot;no current running application &amp;gt;&amp;gt; pass killing process&quot;
else
  echo &quot;kill -TERM CURRENT_PID&quot;
  kill -TERM ${CURRENT_PID}
  sleep 2
fi

echo &quot;## run java application&quot;
java --version
if [ $? -ne 0 ];
then
  echo &quot;no JRE setup &amp;gt; JRE is required!!&quot;
  exit 1
fi

nohup java -jar \
  -Dspring.config.import=optional:file:${ENV_LOCATION}/.env[.properties] \
  -Dspring.profiles.active=${SPRING_PROFILE} \
  ${APP_WORKSPACE}/${MODULE_NAME}.jar 1&amp;gt;${STD_OUT} 2&amp;gt;${STD_ERR} &amp;amp;

echo &quot;## run application completed!!&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 script 내용입니다. &lt;b&gt;service.sh.j2&lt;/b&gt; 파일이름에서 알 수 있듯이 shell script도 ansible의 j2 template을 이용했습니다.&lt;br /&gt;내용이 워낙 간단해서 일부분만 짚고 넘어가도록 하겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1695338757921&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;echo &quot;## kill current running application&quot;
CURRENT_PID = $(pgrep -f ${MODULE_NAME})

if [ -z &quot;${CURRENT_PID}&quot; ];
then
  echo &quot;no current running application &amp;gt;&amp;gt; pass killing process&quot;
else
  echo &quot;kill -TERM CURRENT_PID&quot;
  kill -TERM ${CURRENT_PID}
  sleep 2
fi&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- pgrep -f ${MODULE_NAME}&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 linux 계열 OS에서 process 확인 할 때 &lt;b&gt;&quot;ps -ef | grep [process name]&quot;&lt;/b&gt; 이런 식으로 많이들 확인하셨을텐데 pgrep은 저 두 개를 합친 것으로 생각하시면 됩니다. pgrep의 &lt;b&gt;f 옵션(-f)&lt;/b&gt;은 실행중인 프로세스가 있는 경우 해당 프로세스 ID를 반환하도록 하는 옵션입니다. 즉, 저기서 하려는 액션은 실행 중인 java application의 PID 값을 받아서 종료해주는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- kill -TERM ${CURRENT_PID}&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포할 때 가장 중요하게 고려해야 하는 것 중에 하나가 바로 프로세스를 올바르게 종료하는 것입니다. &lt;br /&gt;&lt;br /&gt;배포때 마다 애플리케이션을 중단해야 하는데 이미 요청이 들어오고 나서 처리되지 않았는데 중간에 애플리케이션을 강제 종료하면 그 요청은 제대로 처리되지 않은 상태로 응답도 못 내려주고 끝나게 됩니다. 요청 처리하는 중간에 중단이 되어버리면 데이터가 꼬일 수 있는 상황도 발생할 수 있는 만큼 프로세스 종료에 대해서 신경써서 스크립트를 작성해야 합니다.&lt;br /&gt;&lt;br /&gt;Spring Boot에서는 &lt;b&gt;graceful shutdown&lt;/b&gt; 기능을 제공하고 있고 서버단에서는 &lt;b&gt;kill SIGTERM&lt;/b&gt;을 사용해서 안전하게 프로세스를 종료할 수 있는데요. 이부분에 대해서 아주 잘 설명하고 있는 글이 있어서 소개를 해드리고자 합니다. 시간 되실 때 읽어보시는 걸 추천드립니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #fefefe; color: #000c34; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;b&gt;&amp;gt; kill 명령어로 안전하게 프로세스 종료 시키는 방법&lt;/b&gt;&lt;br /&gt;&lt;/span&gt;&lt;a href=&quot;https://www.lesstif.com/system-admin/unix-linux-kill-12943674.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://www.lesstif.com/system-admin/unix-linux-kill-12943674.html&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1695339662651&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Unix, Linux 에서 kill 명령어로 안전하게 프로세스 종료 시키는 방법&quot; data-og-description=&quot;&amp;nbsp;여기를 클릭하여 펼치기... 위 쓰레드에서 발췌한 killtree.sh #!/bin/bash killtree() { local _pid=$1 local _sig=${2:-TERM} kill -stop ${_pid} # needed to stop quickly forking parent from producing child between child killing and parent ki&quot; data-og-host=&quot;www.lesstif.com&quot; data-og-source-url=&quot;https://www.lesstif.com/system-admin/unix-linux-kill-12943674.html&quot; data-og-url=&quot;https://www.lesstif.com/system-admin/unix-linux-kill-12943674.html&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://www.lesstif.com/system-admin/unix-linux-kill-12943674.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.lesstif.com/system-admin/unix-linux-kill-12943674.html&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Unix, Linux 에서 kill 명령어로 안전하게 프로세스 종료 시키는 방법&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;여기를 클릭하여 펼치기... 위 쓰레드에서 발췌한 killtree.sh #!/bin/bash killtree() { local _pid=$1 local _sig=${2:-TERM} kill -stop ${_pid} # needed to stop quickly forking parent from producing child between child killing and parent ki&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.lesstif.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;gt; SpringBoot Graceful Shutdown&lt;/b&gt;&lt;br /&gt;&lt;a href=&quot;https://hudi.blog/springboot-graceful-shutdown/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://hudi.blog/springboot-graceful-shutdown/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1695340224804&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;SpringBoot Graceful Shutdown&quot; data-og-description=&quot;이전 포스팅인 무중단 배포 중 구버전 프로세스를 그냥 종료해도 괜찮을까? (feat. Kill, Graceful Shutdown) 에서 스프링부트의 Graceful Shutdown에 대해서 간단히 다루어보았다. 이 내용을 조금 더 자세히 &quot; data-og-host=&quot;hudi.blog&quot; data-og-source-url=&quot;https://hudi.blog/springboot-graceful-shutdown/&quot; data-og-url=&quot;https://hudi.blog/springboot-graceful-shutdown/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bHPKC4/hyTY9V0E8s/16tkUp0rFzXAppcP1iiPR0/img.png?width=800&amp;amp;height=400&amp;amp;face=0_0_800_400,https://scrap.kakaocdn.net/dn/bFdIDS/hyTZbTOEA6/JPqZuhyKCxLi43Rpo5gy7K/img.png?width=680&amp;amp;height=312&amp;amp;face=0_0_680_312&quot;&gt;&lt;a href=&quot;https://hudi.blog/springboot-graceful-shutdown/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://hudi.blog/springboot-graceful-shutdown/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bHPKC4/hyTY9V0E8s/16tkUp0rFzXAppcP1iiPR0/img.png?width=800&amp;amp;height=400&amp;amp;face=0_0_800_400,https://scrap.kakaocdn.net/dn/bFdIDS/hyTZbTOEA6/JPqZuhyKCxLi43Rpo5gy7K/img.png?width=680&amp;amp;height=312&amp;amp;face=0_0_680_312');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;SpringBoot Graceful Shutdown&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;이전 포스팅인 무중단 배포 중 구버전 프로세스를 그냥 종료해도 괜찮을까? (feat. Kill, Graceful Shutdown) 에서 스프링부트의 Graceful Shutdown에 대해서 간단히 다루어보았다. 이 내용을 조금 더 자세히&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;hudi.blog&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1695340237075&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;echo &quot;## run java application&quot;
java --version
if [ $? -ne 0 ];
then
  echo &quot;no JRE setup &amp;gt; JRE is required!!&quot;
  exit 1
fi

nohup java -jar \
  -Dspring.config.import=optional:file:${ENV_LOCATION}/.env[.properties] \
  -Dspring.profiles.active=${SPRING_PROFILE} \
  ${APP_WORKSPACE}/${MODULE_NAME}.jar 1&amp;gt;${STD_OUT} 2&amp;gt;${STD_ERR} &amp;amp;

echo &quot;## run application completed!!&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로세스를 안전하게 종료했다면 새로 배포된 빌드파일로 애플리케이션을 실행하면 됩니다. 가장 먼저 jdk가 잘 설치되었는지 체크합니다. 여기서 더 추가해볼 수 있는 것은 Spring Boot 프로젝트에 설정된 jdk 버전과 일치하는 버전이 있는지도 체크할 수 있을 것 같습니다.&lt;br /&gt;&lt;br /&gt;다음으로 java application 실행단계입니다. &lt;b&gt;nohup&lt;/b&gt;은 뒤에 나오는 명령프로그램을 데몬 형태로 실행시켜줍니다. 로그아웃으로 세션이 끊어져도 해당 명령프로그램은 중지되지 않고 계속 실행이 된다고 보시면 됩니다. java 실행시 ansible playbook에서 가져온 변수들을 토대로 env파일을 import하고 spring profile도 설정해주었습니다.&lt;/p&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&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;b&gt;  6. 정리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Jenkinsfile과 ansible playbook, shell script까지 작성하고 Jenkins에 item 등록해서 빌드를 하게 되면 아래와 같이 pipeline으로 등록해둔 모든 stage가 나오면서 성공, 실패여부가 나오게 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2023-09-22 at 8.59.37 AM.png&quot; data-origin-width=&quot;2368&quot; data-origin-height=&quot;1730&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/daqxzj/btsvbFScvoD/OWhpIktuUzlrK9RmFE34r1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/daqxzj/btsvbFScvoD/OWhpIktuUzlrK9RmFE34r1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/daqxzj/btsvbFScvoD/OWhpIktuUzlrK9RmFE34r1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdaqxzj%2FbtsvbFScvoD%2FOWhpIktuUzlrK9RmFE34r1%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;760&quot; height=&quot;555&quot; data-filename=&quot;Screenshot 2023-09-22 at 8.59.37 AM.png&quot; data-origin-width=&quot;2368&quot; data-origin-height=&quot;1730&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 캡쳐 사진에서는 제가 이것저것 테스트하느라 몇몇 stage를 스킵해서 중간에 비어있는 채로 나왔는데요. 처음부터 끝까지 jenkins build를 성공적으로 마치게되면 모두 초록불로 나오게 됩니다.&lt;br /&gt;&lt;br /&gt;위에서 Jenkins pipeline의 stage 순서 구성부터 pipeline script, ansible playbook, 실행 script 작성까지 정리해보았는데요. 다시 읽어보니 두서없이 정리한 것 같네요,, 혹시 궁금한 점이 있으면 언제든 댓글 주세요!&lt;br /&gt;&lt;br /&gt;이번 게시글에서 정리한 내용들은 참고용입니다. ci, cd 프로세스는 개발하는 팀과 사람마다 다 다르고 인프라 구조도 다르기 때문에 각자의 상황에 맞게 사용하는 것이 중요합니다. &lt;br /&gt;이번 글을 통해 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;조금이나마&lt;span&gt; &lt;/span&gt;&lt;/span&gt;도움을 받으셨으면 하는 바람은 있습니다.&lt;br /&gt;&lt;br /&gt;다음 글에서는 &lt;b&gt;무중단 배포전략(Rolling, Blue Green)을 사용해서 배포&lt;/b&gt;하는&amp;nbsp;내용을 정리해보려합니다. 감사합니다.&lt;/p&gt;</description>
      <category>Infra</category>
      <category>ansible</category>
      <category>CI/CD</category>
      <category>jenkins</category>
      <category>Shell</category>
      <category>Vault</category>
      <author>beaniejoy</author>
      <guid isPermaLink="true">https://beaniejoy.tistory.com/103</guid>
      <comments>https://beaniejoy.tistory.com/103#entry103comment</comments>
      <pubDate>Fri, 22 Sep 2023 09:50:15 +0900</pubDate>
    </item>
    <item>
      <title>[Vault] Spring Boot에 vault secret 정보를 적용해보자</title>
      <link>https://beaniejoy.tistory.com/102</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이번 게시글은 Spring Boot Applicaiton에 vault secret 데이터들을 적용했던 내용을 정리하는 글입니다. vault 서버가 준비가 안되어있다면 이전에 제가 작성한 글이나 구글링을 통해 vault 설치를 먼저하시는 것을 추천드립니다. &lt;br /&gt;&lt;a href=&quot;https://beaniejoy.tistory.com/100&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://beaniejoy.tistory.com/100&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1692201201804&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Vault 서버를 설치해보자(AWS, Lightsail에 vault 서버 구축해보기)&quot; data-og-description=&quot;Spring Boot 애플리케이션을 개발하다보면 민감한 정보들을 설정해야할 때가 있습니다. DB 연동시 필수적으로 입력해야 하는 jdbc url, username, password 정보도 있고 Security 인증 관련해서 토큰 발급을 &quot; data-og-host=&quot;beaniejoy.tistory.com&quot; data-og-source-url=&quot;https://beaniejoy.tistory.com/100&quot; data-og-url=&quot;https://beaniejoy.tistory.com/100&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/UC5NR/hyTCz2KVZh/M2R8KOeCuOF6iU5dgfXIXk/img.png?width=800&amp;amp;height=418&amp;amp;face=0_0_800_418,https://scrap.kakaocdn.net/dn/eXOC4/hyTFkQqwmm/GCHLUTm9dNu9zTkGk6ierk/img.png?width=800&amp;amp;height=418&amp;amp;face=0_0_800_418,https://scrap.kakaocdn.net/dn/3jyWR/hyTCGt3mfV/4GzuCiblfKlPIjx1CBBDl0/img.png?width=3490&amp;amp;height=1956&amp;amp;face=0_0_3490_1956&quot;&gt;&lt;a href=&quot;https://beaniejoy.tistory.com/100&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://beaniejoy.tistory.com/100&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/UC5NR/hyTCz2KVZh/M2R8KOeCuOF6iU5dgfXIXk/img.png?width=800&amp;amp;height=418&amp;amp;face=0_0_800_418,https://scrap.kakaocdn.net/dn/eXOC4/hyTFkQqwmm/GCHLUTm9dNu9zTkGk6ierk/img.png?width=800&amp;amp;height=418&amp;amp;face=0_0_800_418,https://scrap.kakaocdn.net/dn/3jyWR/hyTCGt3mfV/4GzuCiblfKlPIjx1CBBDl0/img.png?width=3490&amp;amp;height=1956&amp;amp;face=0_0_3490_1956');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Vault 서버를 설치해보자(AWS, Lightsail에 vault 서버 구축해보기)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Spring Boot 애플리케이션을 개발하다보면 민감한 정보들을 설정해야할 때가 있습니다. DB 연동시 필수적으로 입력해야 하는 jdbc url, username, password 정보도 있고 Security 인증 관련해서 토큰 발급을&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;beaniejoy.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&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;b&gt;  1. Spring Boot 프로젝트에 vault secret 관리 대상이 무엇이 있을까요?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떻게 보면 Spring Boot 애플리케이션 개발에 있어서 vault를 왜 도입해야하는지에 대한 이유하고 연결될 것 같습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1693540589248&quot; class=&quot;go&quot; data-ke-language=&quot;go&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    hikari:
      jdbc-url: jdbc:mysql://localhost:3306/dongne?autoreconnect=true&amp;amp;characterEncoding=utf8&amp;amp;serverTimezone=Asia/Seoul
      username: root
      password: beaniejoy
      maximum-pool-size: 5
      minimum-idle: 5

jwt:
  secret_key: ZG9uZ25lLWNhZmUtcHJvamVjdC1rZXktZm9yLXRlc3QtY29kZQo
  validity_time_in_sec: 60&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;secret 관리 대상은 인증 관련 데이터 혹은 DB connection 정보 등 &lt;b&gt;주로 외부 공개에 있어 민감한 데이터들&lt;/b&gt;입니다. 이러한 데이터들은 프로젝트 설정파일에 입력을 해두는 것이 아니라 별도로 관리가 되어야 하는데요. 여러 가지 방안이 있을 수 있지만 실무에서도 가장 많이 사용되고 있고 그만큼 안전하고 사용하기 간편한&amp;nbsp;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;vault로 관리를 해보고자 합니다.&lt;br /&gt;&lt;br /&gt;vault가 왜 안전한지에 대해서는 내부 구조를 알아야하는데 저는 vault document 보고 내부구조 파악에는 실패하였습니다. 혹시 vault를 빠삭하게 아시는분이 계신다면 알려주시거나 vault를 이해하는데 있어 좋은 자료 공유해주시면 정말 감사하겠습니다. (꾸벅)&lt;br /&gt;&lt;br /&gt;vault 원리보다 간편하게 적용하는 방법에 대해서 정리해보고자 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&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;b&gt;  2. vault에 해당 데이터들을 저장하자&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;준비물로 vault 서버가 있어야 합니다. 준비해온 vault 서버에 들어가서 가장 먼저 secret을 설정해야합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2023-09-01 at 6.16.05 PM.png&quot; data-origin-width=&quot;2202&quot; data-origin-height=&quot;922&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lNvld/btssTev9t0H/xwkuosKRkWJTXdKlk3xKT0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lNvld/btssTev9t0H/xwkuosKRkWJTXdKlk3xKT0/img.png&quot; data-alt=&quot;vault 인증 후 메인 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lNvld/btssTev9t0H/xwkuosKRkWJTXdKlk3xKT0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlNvld%2FbtssTev9t0H%2FxwkuosKRkWJTXdKlk3xKT0%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;760&quot; height=&quot;318&quot; data-filename=&quot;Screenshot 2023-09-01 at 6.16.05 PM.png&quot; data-origin-width=&quot;2202&quot; data-origin-height=&quot;922&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;vault 인증 후 메인 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;vault 서버 인증 후 진입하는 메인 화면에서 secret engine 내역을 볼 수 있는데요. Spring Boot application 전용 secret을 위해 새로운 engine을&amp;nbsp; 우측에 사진과 같이 Enable new engine 버튼을 누릅니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2023-09-01 at 6.24.04 PM.png&quot; data-origin-width=&quot;2302&quot; data-origin-height=&quot;1338&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bKy2ta/btssMF2VtB8/iUcBLxddkZJZKbBj7Os2m0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bKy2ta/btssMF2VtB8/iUcBLxddkZJZKbBj7Os2m0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bKy2ta/btssMF2VtB8/iUcBLxddkZJZKbBj7Os2m0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbKy2ta%2FbtssMF2VtB8%2FiUcBLxddkZJZKbBj7Os2m0%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;760&quot; height=&quot;442&quot; data-filename=&quot;Screenshot 2023-09-01 at 6.24.04 PM.png&quot; data-origin-width=&quot;2302&quot; data-origin-height=&quot;1338&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Generic &amp;gt; KV를 선택하시고 다음으로 넘어갑니다. KV는 vault의 Key-Value 형식 저장소를 의미하고 임의의 secret 정보들을 key value 형태로 저장하게 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2023-09-01 at 6.24.18 PM.png&quot; data-origin-width=&quot;2304&quot; data-origin-height=&quot;1134&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bs1sHU/btssOsaUNxM/qq2HhKnCAMFlJXAhNpk5mK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bs1sHU/btssOsaUNxM/qq2HhKnCAMFlJXAhNpk5mK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bs1sHU/btssOsaUNxM/qq2HhKnCAMFlJXAhNpk5mK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbs1sHU%2FbtssOsaUNxM%2Fqq2HhKnCAMFlJXAhNpk5mK%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;760&quot; height=&quot;374&quot; data-filename=&quot;Screenshot 2023-09-01 at 6.24.18 PM.png&quot; data-origin-width=&quot;2304&quot; data-origin-height=&quot;1134&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;secret engine의 이름이 되는 path를 지정해야 하는데요. 원하는 이름으로 설정하시면 됩니다.&lt;br /&gt;(저는 이미 secret 이라는 이름의 engine을 생성했는데요. 다른 이름으로 하셔도 됩니다.)&lt;br /&gt;&lt;br /&gt;여기까지 되셨다면 secret engine 생성은 완료입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2023-09-01 at 6.30.49 PM.png&quot; data-origin-width=&quot;2168&quot; data-origin-height=&quot;618&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/XeJ2O/btssTd45A8b/4kW1q4ZrYbuLGOQQRpQTyK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/XeJ2O/btssTd45A8b/4kW1q4ZrYbuLGOQQRpQTyK/img.png&quot; data-alt=&quot;생성된 secret engine 내부 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/XeJ2O/btssTd45A8b/4kW1q4ZrYbuLGOQQRpQTyK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXeJ2O%2FbtssTd45A8b%2F4kW1q4ZrYbuLGOQQRpQTyK%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;2168&quot; height=&quot;618&quot; data-filename=&quot;Screenshot 2023-09-01 at 6.30.49 PM.png&quot; data-origin-width=&quot;2168&quot; data-origin-height=&quot;618&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;생성된 secret engine 내부 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;secret engine을 생성했으면 그 안에 들어가서 application에서 사용할 secret 내용을 입력해야 하는데요. 여기서도 경로 설정이 중요합니다.&lt;br /&gt;&lt;br /&gt;Spring Boot는 기본적으로 profile로 설정을 환경별로 구분해서 관리하는데요. 각각의 환경에 맞게 secret 정보들을 구성해야 하기 때문에 secret의 path를 잘 만드는 것이 중요합니다.&lt;br /&gt;&lt;br /&gt;저는 이미 생성해두었지만 dongne라는 application 이름을 따고 환경별로 local, prod를 구성하고자 합니다. local 환경부터 구성하면 다음과 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2023-09-01 at 6.42.37 PM.png&quot; data-origin-width=&quot;2290&quot; data-origin-height=&quot;1270&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yFaUh/btssN3PKxPn/TouHDXSEDIKVzjocrn164k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yFaUh/btssN3PKxPn/TouHDXSEDIKVzjocrn164k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yFaUh/btssN3PKxPn/TouHDXSEDIKVzjocrn164k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyFaUh%2FbtssN3PKxPn%2FTouHDXSEDIKVzjocrn164k%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;760&quot; height=&quot;421&quot; data-filename=&quot;Screenshot 2023-09-01 at 6.42.37 PM.png&quot; data-origin-width=&quot;2290&quot; data-origin-height=&quot;1270&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;path에는 dongne/local로 지정하고 secret data에는 key value 형식에 맞춰 application.yml에 적용하고자 하는 설정내용들을 입력하면 됩니다.&lt;br /&gt;&lt;br /&gt;특징은 yml 형식이 아닌 properties 파일 형식과 같이 입력하면 됩니다. 다 입력하고 저장하면 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2023-09-01 at 6.45.07 PM.png&quot; data-origin-width=&quot;2180&quot; data-origin-height=&quot;754&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/24WIp/btssMQJ9CGR/eSVDdsrLrITx7KRK7AX8Rk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/24WIp/btssMQJ9CGR/eSVDdsrLrITx7KRK7AX8Rk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/24WIp/btssMQJ9CGR/eSVDdsrLrITx7KRK7AX8Rk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F24WIp%2FbtssMQJ9CGR%2FeSVDdsrLrITx7KRK7AX8Rk%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;760&quot; height=&quot;263&quot; data-filename=&quot;Screenshot 2023-09-01 at 6.45.07 PM.png&quot; data-origin-width=&quot;2180&quot; data-origin-height=&quot;754&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;local, prod 둘 다 저장하면 위와 같이 환경별로 secret 정보들을 깔끔하게 관리할 수 있습니다.&lt;/p&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&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;b&gt;  3. Spring Boot 내에 vault 설정&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;vault에 secret 정보들을 저장했고 이제 Spring Boot application에 vault를 연동해야 합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1693756535497&quot; class=&quot;go&quot; data-ke-language=&quot;go&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;dependencies {
	//...
    
    implementation(&quot;org.springframework.cloud:spring-cloud-starter-vault-config:3.1.3&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;spring-cloud-starter-vault-config 의존성을 추가해줘야 하는데요. 제 개인 프로젝트의 Spring Boot 버전은 2.7.x 인데요. vault-config 버전을 3.1.3으로 해야 제대로 적용이 되었습니다. Spring Boot 버전하고 vault-config 버전하고 호환성 이슈가 있는지 모르겠지만 버전에 따라 제대로 적용되는지 체크해야 할 것 같습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1693757524440&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring:
    config:
        import:
          - optional:file:./env/.env.${spring.profiles.active}[.properties] # local
          - vault://
    cloud:
        vault:
            uri: ${VAULT_DOMAIN_ADDR} # vault domain uri (port: 8200)
            authentication: TOKEN
            token: ${VAULT_AUTH_TOKEN} # vault root token
            connection-timeout: 3000
            read-timeout: 10000
            kv:
                backend: secret
                application-name: dongne
                default-context:
                profiles: ${spring.profiles.active}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 설정내용을 vault를 적용하고자 하는 모듈이나 프로젝트의 application.yml에 작성합니다.&lt;br /&gt;위에서 중요한 항목은 uri, token 정보입니다. 해당 정보들도 외부에 노출되면 안되는 내용이기 때문에 저같은 경우 환경 변수를 통해 주입받도록 했습니다. (uri, token을 가져오는 방식은 여러가지라서 원하는 방식으로 하셔도 상관없습니다.)&lt;br /&gt;&lt;br /&gt;그리고 kv 필드에 backend, application-name, profiles 내용들이 있는데요. Spring Boot 애플리케이션을 실행하게되면 vault-config가 해당 항목들을 참조해서 secret path를 조합하고 그 path에 있는 secret 데이터들을 가져오게 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2023-09-01 at 6.22.33 PM.png&quot; data-origin-width=&quot;1906&quot; data-origin-height=&quot;480&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/skWyO/btssYTryOg0/yGahyQNKzBAVMnXmRxZYs1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/skWyO/btssYTryOg0/yGahyQNKzBAVMnXmRxZYs1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/skWyO/btssYTryOg0/yGahyQNKzBAVMnXmRxZYs1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FskWyO%2FbtssYTryOg0%2FyGahyQNKzBAVMnXmRxZYs1%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;1906&quot; height=&quot;480&quot; data-filename=&quot;Screenshot 2023-09-01 at 6.22.33 PM.png&quot; data-origin-width=&quot;1906&quot; data-origin-height=&quot;480&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 &lt;b&gt;backend: secret, application-name: hello, profiles: local&lt;/b&gt;로 한다면 위와 같이 애플리케이션 실행시점에 &lt;b&gt;secret/hello/local&lt;/b&gt;과 &lt;b&gt;secret/hello&lt;/b&gt; path에 있는 secret 정보들을 가져오게 됩니다. &lt;br /&gt;&lt;br /&gt;위에서 vault 설정할 때 vault에 저장해두었던 secret 데이터의 path는 &lt;b&gt;secret/dongne/local&lt;/b&gt; 이었습니다. &lt;br /&gt;그렇기 때문에 &lt;u&gt;&lt;b&gt;application-name: dongne&lt;/b&gt;&lt;/u&gt;라고 설정해두어야 하고, &lt;u&gt;&lt;b&gt;profiles:&lt;/b&gt;&lt;/u&gt;&lt;b&gt;&lt;u&gt; ${spring.profiles.active}&lt;/u&gt;를&lt;/b&gt; 통해 현재 실행되고 있는 profile 환경에 따라 vault의 secret 데이터를 가져오도록 설정해두면 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2023-09-04 at 1.28.38 AM.png&quot; data-origin-width=&quot;1062&quot; data-origin-height=&quot;95&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/C9DZt/btssTmVQPrF/xsv0FyuCpuLswKkgFagXu1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/C9DZt/btssTmVQPrF/xsv0FyuCpuLswKkgFagXu1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/C9DZt/btssTmVQPrF/xsv0FyuCpuLswKkgFagXu1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FC9DZt%2FbtssTmVQPrF%2Fxsv0FyuCpuLswKkgFagXu1%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;1062&quot; height=&quot;95&quot; data-filename=&quot;Screenshot 2023-09-04 at 1.28.38 AM.png&quot; data-origin-width=&quot;1062&quot; data-origin-height=&quot;95&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정을 마치고 애플리케이션을 실행하면 프로젝트에 따로 jdbc-url을 설정한 적이 없는데 vault에 있는 secret 내용을 가져와서 설정이 된 것을 확인할 수 있습니다.&lt;br /&gt;&lt;br /&gt;이렇게 vault를 사용하게 되면 local 환경에서 실행할 때 뿐만 아니라 어떤 spring boot profile 환경에서든, 관리하기 어려운 민감데이터들을 vault 하나로 쉽게 관리할 수 있다는 점에서 실무에서 많이 사용되고 있는 것 같습니다.&lt;/p&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&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;b&gt;  (번외) 적용해보면서 직면했던 또 다른 애로사항(테스트 실행시 vault 비활성화)&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;vault를 사용하는 것은 좋은데 테스트 실행시 vault를 사용하지 않고 테스트용 secret 데이터들은 application.yml 파일에다가 설정해두고 싶은 경우가 있을 수 있는데요. 이 때 조금 애를 먹었던 기억이 있습니다.&lt;br /&gt;&lt;br /&gt;예를 들어 local profile에서는 vault를 활성화하고 test profile에서는 vault 비활성화해두고 싶은 경우 아래와 같이 설정하면 에러가 발생합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1693759237820&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# application-test.yml
spring:
    cloud:
    	vault:
            enabled: false
            
# application-local.yml
spring:
    config:
        import:
          - optional:file:./env/.env.${spring.profiles.active}[.properties] # local
          - vault://
    cloud:
        vault:
            uri: ${VAULT_DOMAIN_ADDR} # vault domain uri (port: 8200)
            authentication: TOKEN
            token: ${VAULT_AUTH_TOKEN} # vault root token
            connection-timeout: 3000
            read-timeout: 10000
            kv:
                backend: secret
                application-name: dongne
                default-context:
                profiles: ${spring.profiles.active}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 profile에 해당하는 yml 설정파일에 위와 같이 설정하고 local 환경에서 애플리케이션을 실행하면 다음과 같은 에러가 발생합니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Caused by: java.net.URISyntaxException: Illegal character in path at index 1: ${VAULT_DOMAIN_ADDR}&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;env 파일이 분명 있음에도 해당 파일을 가져오지 못해서 그런 것인지 모르겠지만 정확한 원인은 아직 파악을 못했습니다. 특이한 것은 &lt;b&gt;application-local.yml&lt;/b&gt; 이 아닌 &lt;b&gt;application.yml&lt;/b&gt;에 설정하면 env 파일을 잘 가져온다는 것입니다.&lt;br /&gt;&lt;br /&gt;그럼 기존대로 application.yml에 vault 관련 설정을 하고 test profile로 &lt;b&gt;@SpringBootTest&lt;/b&gt;가 있는 테스트코드를 실행해보면 위와 똑같은 에러가 발생하게 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1693761615482&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# application.yml
spring:
  config:
    import:
      - optional:file:./env/.env.${spring.profiles.active}[.properties] # local
      - optional:vault://
  cloud:
    vault:
      uri: ${VAULT_DOMAIN_ADDR} # vault domain uri (port: 8200)
      authentication: TOKEN
      token: ${VAULT_AUTH_TOKEN} # vault root token
      connection-timeout: 3000
      read-timeout: 10000
      kv:
        backend: secret
        application-name: dongne
        default-context:
        profiles: ${spring.profiles.active}

---
spring:
  config:
    activate:
      on-profile: test

  cloud:
    vault:
      enabled: false&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;@SpringBootTest&lt;/b&gt;를 통한 테스트에서는 vault 비활성화, 애플리케이션 실행시에는 vault 적용하기 위해 결국 위와 같이 수정을 했습니다.&lt;br /&gt;&lt;br /&gt;기존에 &lt;b&gt;spring.config.import&lt;/b&gt; 부분에 &lt;b&gt;vault://&lt;/b&gt; 적용했던 부분을 &lt;b&gt;optional&lt;/b&gt;로 바꾸고 같은 application.yml 파일에 test profile을 따로 만들어서 거기에 vault를 비활성화하였습니다. 이렇게 하니 테스트도, 애플리케이션 실행도 잘 이루어 지긴 했습니다.&lt;br /&gt;(이런 방식 말고 테스트와 애플리케이션 실행시 vault 설정을 격리해서 관리하는 더 좋은 방법이 있을 것 같네요)&lt;/p&gt;</description>
      <category>Spring</category>
      <category>secret 설정</category>
      <category>Spring Boot</category>
      <category>Vault</category>
      <author>beaniejoy</author>
      <guid isPermaLink="true">https://beaniejoy.tistory.com/102</guid>
      <comments>https://beaniejoy.tistory.com/102#entry102comment</comments>
      <pubDate>Tue, 5 Sep 2023 23:57:30 +0900</pubDate>
    </item>
    <item>
      <title>[Jenkins] Ansible plugin 사용해보기(ansible 설치부터 pipeline까지 작업)</title>
      <link>https://beaniejoy.tistory.com/101</link>
      <description>&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;widthOrigin&quot; data-filename=&quot;ansible-jenkins.png&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;954&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HSYTA/btsrq9JyCAK/9KtaIGdqBblbOq98u0K1ek/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HSYTA/btsrq9JyCAK/9KtaIGdqBblbOq98u0K1ek/img.png&quot; data-alt=&quot;https://codingtricks.io/how-to-run-ansible-playbook-from-jenkins&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HSYTA/btsrq9JyCAK/9KtaIGdqBblbOq98u0K1ek/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHSYTA%2Fbtsrq9JyCAK%2F9KtaIGdqBblbOq98u0K1ek%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;760&quot; height=&quot;566&quot; data-filename=&quot;ansible-jenkins.png&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;954&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://codingtricks.io/how-to-run-ansible-playbook-from-jenkins&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;지난 게시글 중에 Jenkins 서버를 설치하고 Spring boot project 대상으로 간단하게 테스트, 빌드까지 해보는 Jenkins pipeline을 적용해보는 글이 있었습니다. 이번 게시글을 읽기 전에 먼저 읽어보시는 것을 추천드립니다.&lt;br /&gt;(&lt;a href=&quot;https://beaniejoy.tistory.com/95&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://beaniejoy.tistory.com/95&lt;/a&gt;)&lt;/p&gt;
&lt;figure id=&quot;og_1691856203816&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Jenkins] Lightsail에 Jenkins server 구축해보고 Spring project 빌드해보기&quot; data-og-description=&quot;목적 - AWS Lightsail을 통해 생성한 instance에 jenkins server 구축한 내용 기록용 목표 - AWS Lightsail에 띄운 instance에 jenkins server 띄우기 - jenkins server 기본적인 설정 - jenkins item 생성 후 spring project build 해&quot; data-og-host=&quot;beaniejoy.tistory.com&quot; data-og-source-url=&quot;https://beaniejoy.tistory.com/95&quot; data-og-url=&quot;https://beaniejoy.tistory.com/95&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bZuN3a/hyTCz8oUdt/u9KTHkVIweF7sXlRJG5CCk/img.png?width=676&amp;amp;height=411&amp;amp;face=318_36_400_125,https://scrap.kakaocdn.net/dn/Edi0K/hyTCKWm35g/N6tZQK7u1kMxNpTFb0ah81/img.png?width=676&amp;amp;height=411&amp;amp;face=318_36_400_125,https://scrap.kakaocdn.net/dn/3ZPZL/hyTCNr2rnL/N6cGuge62hYdvjgVUgiDb1/img.png?width=1938&amp;amp;height=1330&amp;amp;face=0_0_1938_1330&quot;&gt;&lt;a href=&quot;https://beaniejoy.tistory.com/95&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://beaniejoy.tistory.com/95&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bZuN3a/hyTCz8oUdt/u9KTHkVIweF7sXlRJG5CCk/img.png?width=676&amp;amp;height=411&amp;amp;face=318_36_400_125,https://scrap.kakaocdn.net/dn/Edi0K/hyTCKWm35g/N6tZQK7u1kMxNpTFb0ah81/img.png?width=676&amp;amp;height=411&amp;amp;face=318_36_400_125,https://scrap.kakaocdn.net/dn/3ZPZL/hyTCNr2rnL/N6cGuge62hYdvjgVUgiDb1/img.png?width=1938&amp;amp;height=1330&amp;amp;face=0_0_1938_1330');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[Jenkins] Lightsail에 Jenkins server 구축해보고 Spring project 빌드해보기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;목적 - AWS Lightsail을 통해 생성한 instance에 jenkins server 구축한 내용 기록용 목표 - AWS Lightsail에 띄운 instance에 jenkins server 띄우기 - jenkins server 기본적인 설정 - jenkins item 생성 후 spring project build 해&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;beaniejoy.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 게시글에는 설치된 Jenkins에 Ansible plugin을 적용해보는 내용을 정리해보고자 합니다.&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;b&gt;  1. Ansible을 왜 적용하려고 하나요? (장점이 무엇인가요?)&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Ansible에 대한 원론적인 개념을 언급하지는 않으려고 합니다. 구글에 ansible만 쳐도 개념은 아주 자세하게 잘 나와있기 때문에 혹시 ansible를 완전히 처음인 분들은 개념 한 번 보시고 아주 기본적인 내용을 가지고 테스트로 ansible 실행해보시는 것을 추천드립니다.&lt;br /&gt;&lt;br /&gt;저는 ansible을 사용하게 된 직접적인 계기는 실무에서 사용하고 있기 때문입니다. 그래서 로컬에서 여러 vm을 설치해 ansible을 테스트로 돌려보면서 이번 개인프로젝트 CI/CD 프로세스에 적용해보기로 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1-1.&amp;nbsp; 개인적으로 느낀 ansible의 장점&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ansible을 아주 기본적인 형태로밖에 사용하지 못했지만 개인 프로젝트에 적용해보면서 느낀 장점은 제가 관리하고 있는 개발 프로젝트 내부에서 ansible script 파일을 컨트롤할 수 있다는 점입니다. &lt;br /&gt;&lt;br /&gt;개인적으로 인프라적인 요소들을 접목하는데 있어 중요하게 생각하는 것 중 하나는 설정 관련된 내용들이 뿔뿔이 흩어져있지 않고 되도록 한 곳에서 관리될 수 있게 하는 것입니다. 물론 모든 것을 한 곳에 관리하는 것은 불가능하긴 합니다. 개발은 개발 따로, CI 툴 따로, 배포 관련 서버 따로, 모든 설정들이 흩어져 관리된다면 수정하는 것도 힘들어지고 오류 발생시 확인하는 과정도 더 번잡해질 가능성이 커집니다.&lt;br /&gt;&lt;br /&gt;ansible은 이러한 저의 니즈를 아주 적절하게 충족해주는 도구라 할 수 있습니다. &lt;br /&gt;ansible은 playbook이라는 script를 통해 대상이 되는 서버에 여러 설정을 지시할 수 있습니다. 디렉토리, 파일 생성과 같은 아주 기본적인 것들부터 필요한 패키지 설치, shell script 실행 등 파일 실행도 할 수 있고 심지어 직접 작성한 파일을 대상이 되는 서버로 전송할 수도 있습니다. 이렇게 되면 애플리케이션 실행을 하기 위한 shell script와 같은 실행파일도 하나의 개발프로젝트에서 관리가 가능해집니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2023-08-14 at 4.36.13 PM.png&quot; data-origin-width=&quot;1728&quot; data-origin-height=&quot;1418&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/di0Hak/btsrgqkoEX5/nCPPhRhGPnvBaWsqM5oVJk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/di0Hak/btsrgqkoEX5/nCPPhRhGPnvBaWsqM5oVJk/img.png&quot; data-alt=&quot;개발프로젝트에서 ansible 관련 스크립트 파일도 관리가 가능&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/di0Hak/btsrgqkoEX5/nCPPhRhGPnvBaWsqM5oVJk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdi0Hak%2FbtsrgqkoEX5%2FnCPPhRhGPnvBaWsqM5oVJk%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;760&quot; height=&quot;624&quot; data-filename=&quot;Screenshot 2023-08-14 at 4.36.13 PM.png&quot; data-origin-width=&quot;1728&quot; data-origin-height=&quot;1418&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;개발프로젝트에서 ansible 관련 스크립트 파일도 관리가 가능&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발 프로젝트에다 관리하는 것이 오히려 번잡하다 생각하시면 ansible 관련 설정내용만 따로 git repository로 관리하셔도 되고 편한 방식으로 하셔도 됩니다. 정답은 없지만 개발하는 입장에서 인프라적인 요소도 같이 관리를 해야 한다면 최대한 보기 쉽고 오류에 대응하기에도 편리한 방식을 고민하는 것이 중요한 것 같습니다.&lt;/p&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&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;b&gt;  2. Jenkins 서버에 Ansible 패키지 설치하기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 Jenkins를 AWS에 설치를 했는데요. 리소스 비용 문제로 Jenkins가 설치된 동일 서버에 ansible도 같이 설치하였습니다.&lt;br /&gt;&lt;br /&gt;AWS OS는 최근에 나온 Amazon Linux 2023을 사용하고 있는데요. 해당 OS에서 ansible 설치하는 방법을 자세하게 소개하는 블로그이 있어 첨부해드리겠습니다. &lt;a href=&quot;https://cloudkatha.com/how-to-install-ansible-on-amazon-linux-2023/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://cloudkatha.com/how-to-install-ansible-on-amazon-linux-2023/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1691999817429&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;How to Install Ansible on Amazon Linux 2023 - CloudKatha&quot; data-og-description=&quot;discussed How to Install Ansible on Amazon Linux 2023 Instance. We learnt that Amazon Linux Extras and EPEL are not available on Amazon Linux&quot; data-og-host=&quot;cloudkatha.com&quot; data-og-source-url=&quot;https://cloudkatha.com/how-to-install-ansible-on-amazon-linux-2023/&quot; data-og-url=&quot;https://cloudkatha.com/how-to-install-ansible-on-amazon-linux-2023/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/ma5m0/hyTFjDrXr1/crwsHqsM3sh4WKCYIKydTk/img.jpg?width=1200&amp;amp;height=675&amp;amp;face=0_0_1200_675&quot;&gt;&lt;a href=&quot;https://cloudkatha.com/how-to-install-ansible-on-amazon-linux-2023/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://cloudkatha.com/how-to-install-ansible-on-amazon-linux-2023/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/ma5m0/hyTFjDrXr1/crwsHqsM3sh4WKCYIKydTk/img.jpg?width=1200&amp;amp;height=675&amp;amp;face=0_0_1200_675');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;How to Install Ansible on Amazon Linux 2023 - CloudKatha&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;discussed How to Install Ansible on Amazon Linux 2023 Instance. We learnt that Amazon Linux Extras and EPEL are not available on Amazon Linux&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;cloudkatha.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(블로그 내용만 잘 따라서 입력하시기만 하셔도 설치하실 수 있습니다.)&lt;/p&gt;
&lt;pre id=&quot;code_1691999839289&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ pip install ansible

$ ansible --version
ansible [core 2.15.2]
  config file = None
  configured module search path = ['/home/ec2-user/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /home/ec2-user/.local/lib/python3.9/site-packages/ansible
  ansible collection location = /home/ec2-user/.ansible/collections:/usr/share/ansible/collections
  executable location = /home/ec2-user/.local/bin/ansible
  python version = 3.9.16 (main, Feb 23 2023, 00:00:00) [GCC 11.3.1 20221121 (Red Hat 11.3.1-4)] (/usr/bin/python3)
  jinja version = 3.1.2
  libyaml = True&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ansible 버전을 확인했을 때 위와 같이 콘솔창에 메시지가 나오면 설치 완료된 것입니다. 위 내용 중에 중요하게 기억해두고 있어야할 내용은 &lt;b&gt;executable location&lt;/b&gt;입니다. pip로 설치했을 때 &lt;b&gt;/usr/bin&lt;/b&gt; 에 설치가 되는 것이 아닌 &lt;b&gt;/home/ec2-user/.local/bin&lt;/b&gt; 에 설치가 된다는 것입니다. 조금 있다가 Jenkins에 ansible plugin을 적용할 때 고려해야하는 부분이니 그 때 다시 한 번 언급하도록 하겠습니다.&lt;/p&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&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;b&gt;  3. Jenkins에 Ansible plugin 적용해보자&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Jenkins에 접속하고&lt;b&gt; Jenkins 관리 &amp;gt; Plugins &amp;gt; 왼쪽 탭 Available plugins&lt;/b&gt; 이동합니다. 거기에 ansible이라고만 검색해도 Ansible plugin이라고 나옵니다. 바로 설치 진행해줍니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2023-08-14 at 5.06.55 PM.png&quot; data-origin-width=&quot;3192&quot; data-origin-height=&quot;934&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bEWGXY/btsq5Z2ACq3/xB6x6z6HK3KAk9c2mlu2u1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bEWGXY/btsq5Z2ACq3/xB6x6z6HK3KAk9c2mlu2u1/img.png&quot; data-alt=&quot;저는 이미 ansible plugin 설치된 상태이기 때문에 Installed에 해당 플러그인이 나오고 있습니다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bEWGXY/btsq5Z2ACq3/xB6x6z6HK3KAk9c2mlu2u1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbEWGXY%2Fbtsq5Z2ACq3%2FxB6x6z6HK3KAk9c2mlu2u1%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;760&quot; height=&quot;222&quot; data-filename=&quot;Screenshot 2023-08-14 at 5.06.55 PM.png&quot; data-origin-width=&quot;3192&quot; data-origin-height=&quot;934&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;저는 이미 ansible plugin 설치된 상태이기 때문에 Installed에 해당 플러그인이 나오고 있습니다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Ansible plugin을 설치하고 나서 확인해봐야하는 중요한 내용이 있습니다. &lt;b&gt;Jenkins 관리 &amp;gt; System Information&lt;/b&gt;에서 환경 변수를 확인해줍니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2023-08-14 at 5.11.03 PM.png&quot; data-origin-width=&quot;3358&quot; data-origin-height=&quot;1740&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Oe89h/btsrgkR41hB/Sysxu01xGx0P7OH2zbDz30/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Oe89h/btsrgkR41hB/Sysxu01xGx0P7OH2zbDz30/img.png&quot; data-alt=&quot;jenkins에 등록된 PATH 환경변수 확인&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Oe89h/btsrgkR41hB/Sysxu01xGx0P7OH2zbDz30/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOe89h%2FbtsrgkR41hB%2FSysxu01xGx0P7OH2zbDz30%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;760&quot; height=&quot;394&quot; data-filename=&quot;Screenshot 2023-08-14 at 5.11.03 PM.png&quot; data-origin-width=&quot;3358&quot; data-origin-height=&quot;1740&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;jenkins에 등록된 PATH 환경변수 확인&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;거기에 PATH 환경변수가 어떻게 등록이 되어있는지 확인을 해야합니다. 만약 &lt;b&gt;/home/ec2-user/.local/bin&lt;/b&gt;이 등록안되어 있다면 Jenkins에 PATH를 다시 등록하거나 ansible plugin installation 관련 설정을 해야 jenkins에서 ansible을 사용할 수 있습니다.&lt;br /&gt;&lt;br /&gt;Jenkins는 PATH에 등록된 경로를 기준으로 설치된 플러그인들에 대한 실행을 프로그램을 찾게 됩니다. 위에서 pip를 통해 ansible 설치했을 때 &lt;b&gt;/home/ec2-user/.local/bin &lt;/b&gt;경로에 설치된 것을 확인했는데요. Jenkins에서는 해당 경로를 인식 못하기 때문에 Jenkins 상에서 ansible을 실행하려고 한다면 에러가 발생하게 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2023-08-14 at 5.15.37 PM.png&quot; data-origin-width=&quot;1254&quot; data-origin-height=&quot;360&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b2OR7z/btsrglKdpxH/xx11UoN8zqAutjjxOGu1o0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b2OR7z/btsrglKdpxH/xx11UoN8zqAutjjxOGu1o0/img.png&quot; data-alt=&quot;jenkins에 등록된 PATH에 ansible 실행파일 없을시 위와 같이 에러가 발생&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b2OR7z/btsrglKdpxH/xx11UoN8zqAutjjxOGu1o0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb2OR7z%2FbtsrglKdpxH%2Fxx11UoN8zqAutjjxOGu1o0%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;760&quot; height=&quot;218&quot; data-filename=&quot;Screenshot 2023-08-14 at 5.15.37 PM.png&quot; data-origin-width=&quot;1254&quot; data-origin-height=&quot;360&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;jenkins에 등록된 PATH에 ansible 실행파일 없을시 위와 같이 에러가 발생&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 하나 주의해야할 점이 AWS에 명령프롬프트로 PATH를 출력했을 때하고 Jenkins에 등록된 PATH 내용이 다를 수 있기 때문에 되도록 위에서 보여드렸던 것처럼 jenkins에 설정된 PATH 내용을 확인하셔야 합니다.&lt;/p&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 jenkins PATH 확인했을 때 ansible 실행파일이 있는 경로가 설정이 안되어있을 때 Jenkins에 ansible 관련 설정을 해야 합니다.&lt;br /&gt;&lt;b&gt;Jenkins 관리 &amp;gt; Tools&lt;/b&gt;에 들어갑니다. Ansible plugin을 설치하면 Ansible 항목이 보일텐데요. 거기에 pip를 통해 설치된 ansible 실행프로그램이 있는 경로를 지정해주면 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2023-08-14 at 5.25.04 PM.png&quot; data-origin-width=&quot;2128&quot; data-origin-height=&quot;1162&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/A0fOb/btsq8JZtpqP/7AOiwyGdAzFTpPyVidKfV0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/A0fOb/btsq8JZtpqP/7AOiwyGdAzFTpPyVidKfV0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/A0fOb/btsq8JZtpqP/7AOiwyGdAzFTpPyVidKfV0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FA0fOb%2Fbtsq8JZtpqP%2F7AOiwyGdAzFTpPyVidKfV0%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;760&quot; height=&quot;415&quot; data-filename=&quot;Screenshot 2023-08-14 at 5.25.04 PM.png&quot; data-origin-width=&quot;2128&quot; data-origin-height=&quot;1162&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지 하면 Jenkins에 기본적인 Ansible 설정이 완료된 것이고 Jenkins를 통해 ansible을 실행할 수 있게 됩니다.&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;b&gt;  4. Jenkins pipeline에 ansible-playbook 테스트해보자&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4-1. ansiblePlaybook에 대한 pipeline script 작성하기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;jenkins pipeline을 통해 ansible playbook을 한 번 실행해보겠습니다.&lt;br /&gt;&lt;br /&gt;우선 젠킨스에 새로운 item 하나를 pipeline으로 생성하고 기본 설정을 해줍니다. &lt;br /&gt;(맨 위에 링크드린 제 게시글에서 젠킨스 item 설정하는 방법있으니 참고바랍니다!)&lt;br /&gt;&lt;br /&gt;주의할 점은 item 설정할 때 대상이 되는 Jenkinsfile 스크립트 파일 path(파일이름까지)를 잘 설정하셔야 합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2023-08-16 at 1.05.23 PM.png&quot; data-origin-width=&quot;3450&quot; data-origin-height=&quot;1838&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bYxQWl/btsro9JjQ0R/zHdu7UIbkIvSl0Y7AA7y4K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bYxQWl/btsro9JjQ0R/zHdu7UIbkIvSl0Y7AA7y4K/img.png&quot; data-alt=&quot;jenkins의 pipeline item 생성하면 설정 맨 아래에 Pipeline Syntax가 있습니다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bYxQWl/btsro9JjQ0R/zHdu7UIbkIvSl0Y7AA7y4K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbYxQWl%2Fbtsro9JjQ0R%2FzHdu7UIbkIvSl0Y7AA7y4K%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;760&quot; height=&quot;405&quot; data-filename=&quot;Screenshot 2023-08-16 at 1.05.23 PM.png&quot; data-origin-width=&quot;3450&quot; data-origin-height=&quot;1838&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;jenkins의 pipeline item 생성하면 설정 맨 아래에 Pipeline Syntax가 있습니다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 다음 item 설정 맨아래에 보시면 Pipeline Syntax가 있습니다. 해당 링크를 클릭하면 pipeline script를 간편하게 설정할 수 있도록 각종 샘플을 제공하는 페이지가 나올 것입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2023-08-16 at 1.07.37 PM.png&quot; data-origin-width=&quot;2860&quot; data-origin-height=&quot;1614&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bdGXCh/btsrgTAyTk7/TtZ8tIsgCklBaSt3ojKJ11/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bdGXCh/btsrgTAyTk7/TtZ8tIsgCklBaSt3ojKJ11/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bdGXCh/btsrgTAyTk7/TtZ8tIsgCklBaSt3ojKJ11/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbdGXCh%2FbtsrgTAyTk7%2FTtZ8tIsgCklBaSt3ojKJ11%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;760&quot; height=&quot;429&quot; data-filename=&quot;Screenshot 2023-08-16 at 1.07.37 PM.png&quot; data-origin-width=&quot;2860&quot; data-origin-height=&quot;1614&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Ansible plugin을 설치했다면 Sample Step에 ansiblePlaybook이라고 나올 것입니다. 해당 샘플을 클릭하면 아래에 관련 설정들이 나오게 됩니다. 모든 설정을 다 기입할 필요는 없는데요. 테스트를 위해 간단하게 기입해보겠습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;installation&lt;/b&gt;: &lt;b&gt;Jenkins 관리 &amp;gt; Tools&lt;/b&gt;에서 Ansible Installation을 설정했으면 해당 내용이 나올 것입니다. 그 설정을 지정해주면 됩니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;playbook file path&lt;/b&gt; : ansible-playbook을 실행하기 위한 스크립트 파일인 playbook 파일의 경로를 설정해줍니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;inventory file path&lt;/b&gt;: ansible-playbook 실행의 대상이 되는 서버에 대한 내용이 설정된 ansible용도의 hosts 파일의 경로를 설정해줍니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2023-08-16 at 1.27.08 PM.png&quot; data-origin-width=&quot;2664&quot; data-origin-height=&quot;980&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EbPoI/btsrhZHn8oE/0zfH9FYPqWoCsl0DVcHfJk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EbPoI/btsrhZHn8oE/0zfH9FYPqWoCsl0DVcHfJk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EbPoI/btsrhZHn8oE/0zfH9FYPqWoCsl0DVcHfJk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEbPoI%2FbtsrhZHn8oE%2F0zfH9FYPqWoCsl0DVcHfJk%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;760&quot; height=&quot;280&quot; data-filename=&quot;Screenshot 2023-08-16 at 1.27.08 PM.png&quot; data-origin-width=&quot;2664&quot; data-origin-height=&quot;980&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정내용들을 기입하고 맨 아래에 Generate 어쩌구 버튼이 있는데요. 클릭하면 설정 내용들을 토대로 pipeline script 내용을 생성해줍니다. 생성된 script 내용을 빌드할 Jenkinsfile에 기입해주면 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1692191205550&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;pipeline {
    agent any

    stages {
        stage('Init') {
            steps {
                script {
                    // ansible playbook
                    ANSIBLE_INVENTORY = &quot;${HOME}/ansible/inventory&quot;
                    ANSIBLE_PLAYBOOK = &quot;${WORKSPACE}/scripts/deploy/test.yml&quot;
                }
            }
        }

        stage('Run Application with Ansible') {
            steps {
                ansiblePlaybook(
                        installation: 'ansible',
                        playbook: &quot;${ANSIBLE_PLAYBOOK}&quot;,
                        inventory: &quot;${ANSIBLE_INVENTORY}&quot;,
                        extraVars: [
                                hello: &quot;world&quot;,
                                beanie: &quot;joy&quot;
                        ]
                )
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 Jenkinsfile-test 파일을 개발 프로젝트의 root 경로(프로젝트 최상단 위치)에 만들어두고 github repo에 푸시하시면 됩니다.&lt;br /&gt;(참고로 &lt;b&gt;extraVars&lt;/b&gt;는 playbook으로 전달할 외부 변수들을 지정한 것입니다. 뒤에 playbook 만들 때 출력용도로 사용될 예정입니다.)&lt;/p&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4-2. 서버에 ansible 관련 파일 생성하기(hosts, playbook)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 ansible에서 사용될 hosts파일과 playbook을 만들어야 합니다.&lt;br /&gt;&lt;br /&gt;hosts 파일은 위의 script에서 inventory에 지정한 &lt;b&gt;${HOME}/ansible/inventory&lt;/b&gt; 경로에 만들어야 합니다. ${HOME}은 jenksins 프로그램이 돌아가고 있는 서버의 사용자 디렉토리로 AWS에서 &lt;b&gt;/home/ec2-user&lt;/b&gt;를 가리킵니다. &lt;br /&gt;즉 &lt;b&gt;/home/ec2-user/ansible/inventory&lt;/b&gt; 경로에 hosts 파일을 생성합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1692191542669&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[ec2]
[ssh_server_ip_addr]	ansible_user=ec2-user	ansible_ssh_private_key_file=/home/ec2-user/.ssh/[ssh_server_private_key]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;hosts 파일에는 ansible이 프로비저닝할 대상 인프라 정보를 담으면 ansible이 실행할 때 해당 파일을 참조해 관련 프로세스(여기서는 playbook에 지정한 절차)를 진행하게 됩니다.&lt;br /&gt;&lt;br /&gt;위의 내용 처럼 &lt;b&gt;[ssh_server_ip_addr]&lt;/b&gt;에는 대상 서버의 ip address를 적으면 되고, &lt;b&gt;ansible_user&lt;/b&gt;에는 대상 서버의 사용자를, &lt;b&gt;ansible_ssh_private_key&lt;/b&gt;는 대상 서버의 private key를 지정하면 되는데 여기서는 AWS ec2 설정시 받았던 pem key file을 지정하면 됩니다. (저같은 경우 jenkins 서버 내의 사용자 디렉토리의 .ssh에다가 저장을 해두었습니다.)&lt;br /&gt;&lt;br /&gt;이렇게 되면 ansible 실행시 ec2에 대해서는 해당 설정 내용들을 참조해 ssh 연결을 하게 됩니다.&lt;br /&gt;&lt;br /&gt;그 다음 간단하게 대상 서버에 지시할 playbook을 만들어보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1692191975780&quot; class=&quot;vbnet&quot; data-ke-language=&quot;vbnet&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;---
- name: Test Ansible
  hosts: ec2

  tasks:
    - name: Debug extra vars
      ansible.builtin.debug:
        msg: &quot;{{ hello }}, {{ beanie }}&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;엄청 간단하게 pipeline script에서 지정한 extraVars의 내용들을 출력해보는 작업을 만들어보았습니다.&lt;br /&gt;위의 playbook은 yaml 파일 형식으로 만들어야 하고요. 경로는 역시 pipeline script에 지정한 &lt;b&gt;${WORKSPACE}/scripts/deploy&lt;/b&gt;에 &lt;b&gt;test.yml&lt;/b&gt; 이름으로 파일을 만들면 됩니다.&lt;br /&gt;참고로 jenkins에서 &lt;b&gt;${WORKSPACE}&lt;/b&gt;는 해당 jenkins item의 작업이 이루어지는 공간으로 쉽게 말해서 github repository의 root directory라고 생각하시면 됩니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;위의 pipeline script에서 inventory, playbook 경로로 지정한 내용은 개발하시는 입장에서 관리하기 편한 곳에 임의로 설정하면 됩니다. 위 내용은 예시일 뿐입니다.&lt;/blockquote&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하고 jenkins item을 빌드 실행해볼까요?&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2023-08-16 at 10.24.57 PM.png&quot; data-origin-width=&quot;2018&quot; data-origin-height=&quot;1402&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/n8itB/btsrsQbGzN3/aIitTsiklaWsY0Qr2njEj1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/n8itB/btsrsQbGzN3/aIitTsiklaWsY0Qr2njEj1/img.png&quot; data-alt=&quot;pipeline script에 설정한 stage들이 성공적으로 잘 수행된 것을 확인할 수 있습니다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/n8itB/btsrsQbGzN3/aIitTsiklaWsY0Qr2njEj1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fn8itB%2FbtsrsQbGzN3%2FaIitTsiklaWsY0Qr2njEj1%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;760&quot; height=&quot;528&quot; data-filename=&quot;Screenshot 2023-08-16 at 10.24.57 PM.png&quot; data-origin-width=&quot;2018&quot; data-origin-height=&quot;1402&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;pipeline script에 설정한 stage들이 성공적으로 잘 수행된 것을 확인할 수 있습니다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2023-08-16 at 10.25.30 PM.png&quot; data-origin-width=&quot;2654&quot; data-origin-height=&quot;524&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/u65Jm/btsrqDKUjWk/3C7p47dRTRyhTMOOdTwklK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/u65Jm/btsrqDKUjWk/3C7p47dRTRyhTMOOdTwklK/img.png&quot; data-alt=&quot;playbook에 설정한 debug부분에 extraVars로 지정한 변수값들이 잘 출력된 것을 확인할 수 있습니다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/u65Jm/btsrqDKUjWk/3C7p47dRTRyhTMOOdTwklK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fu65Jm%2FbtsrqDKUjWk%2F3C7p47dRTRyhTMOOdTwklK%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;2654&quot; height=&quot;524&quot; data-filename=&quot;Screenshot 2023-08-16 at 10.25.30 PM.png&quot; data-origin-width=&quot;2654&quot; data-origin-height=&quot;524&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;playbook에 설정한 debug부분에 extraVars로 지정한 변수값들이 잘 출력된 것을 확인할 수 있습니다.&lt;/figcaption&gt;
&lt;/figure&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;b&gt;  5. 정리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ansible을 적용하게된 이유부터 해서 제가 개인적으로 생각하는 장점에 대해서 언급해드렸는데요. 다른 생각을 가지신 분들도 많을 거라 생각합니다.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;그 이후에 Jenkins가 돌아가고 있는 서버에 ansible 패키지를 설치했고, Jenkins에 Ansible plugin 설치하고 관련 설정들을 작업하였습니다.&lt;br /&gt;&lt;br /&gt;ansible-playbook에 대한 Jenkins pipeline script를 작성해보았고요. playbook과 ansible hosts 파일을 작성하였고 jenkins item 빌드를 통해 제가 테스트로 설정한 extraVars가 잘 출력된 것을 확인할 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ansible playbook에 대해서 정말 방대한 정보들을 담고 있는데요. 만약 playbook을 통해 다양한 방식으로 기능들을 적용해보고 싶으시다면 공식 document를 꼭 읽어보시는 것을 추천드립니다. &lt;a href=&quot;https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_intro.html#playbook-syntax&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_intro.html#playbook-syntax&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1692192799818&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Ansible playbooks &amp;mdash; Ansible Documentation&quot; data-og-description=&quot;A playbook runs in order from top to bottom. Within each play, tasks also run in order from top to bottom. Playbooks with multiple &amp;lsquo;plays&amp;rsquo; can orchestrate multi-machine deployments, running one play on your webservers, then another play on your databas&quot; data-og-host=&quot;docs.ansible.com&quot; data-og-source-url=&quot;https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_intro.html#playbook-syntax&quot; data-og-url=&quot;https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_intro.html#playbook-syntax&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_intro.html#playbook-syntax&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_intro.html#playbook-syntax&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Ansible playbooks &amp;mdash; Ansible Documentation&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;A playbook runs in order from top to bottom. Within each play, tasks also run in order from top to bottom. Playbooks with multiple &amp;lsquo;plays&amp;rsquo; can orchestrate multi-machine deployments, running one play on your webservers, then another play on your databas&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.ansible.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 ansible + jenkins 적용된 제 개인 프로젝트 github repository 링크입니다.&lt;br /&gt;&lt;a href=&quot;https://github.com/beaniejoy/dongne-cafe-api&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/beaniejoy/dongne-cafe-api&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1692199779160&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - beaniejoy/dongne-cafe-api: ☕️ kotlin &amp;amp; spring boot application (toy project) / siren order service fro local cafe&quot; data-og-description=&quot;☕️ kotlin &amp;amp; spring boot application (toy project) / siren order service fro local cafe - GitHub - beaniejoy/dongne-cafe-api: ☕️ kotlin &amp;amp; spring boot application (toy project) / siren order ...&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/beaniejoy/dongne-cafe-api&quot; data-og-url=&quot;https://github.com/beaniejoy/dongne-cafe-api&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bo7PJb/hyTFbMIgAP/Nw7MIZoyXU9gBRwpNC0GoK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/beaniejoy/dongne-cafe-api&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/beaniejoy/dongne-cafe-api&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bo7PJb/hyTFbMIgAP/Nw7MIZoyXU9gBRwpNC0GoK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - beaniejoy/dongne-cafe-api: ☕️ kotlin &amp;amp; spring boot application (toy project) / siren order service fro local cafe&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;☕️ kotlin &amp;amp; spring boot application (toy project) / siren order service fro local cafe - GitHub - beaniejoy/dongne-cafe-api: ☕️ kotlin &amp;amp; spring boot application (toy project) / siren order ...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;틀린 내용이 있을 수 있습니다~ 언제나 피드백 환영합니다!&lt;/blockquote&gt;</description>
      <category>Infra</category>
      <category>ansible</category>
      <category>jenkins</category>
      <author>beaniejoy</author>
      <guid isPermaLink="true">https://beaniejoy.tistory.com/101</guid>
      <comments>https://beaniejoy.tistory.com/101#entry101comment</comments>
      <pubDate>Thu, 17 Aug 2023 00:21:14 +0900</pubDate>
    </item>
    <item>
      <title>Vault 서버를 설치해보자(AWS, Lightsail에 vault 서버 구축해보기)</title>
      <link>https://beaniejoy.tistory.com/100</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot 애플리케이션을 개발하다보면 민감한 정보들을 설정해야할 때가 있습니다. &lt;br /&gt;DB 연동시 필수적으로 입력해야 하는 jdbc url, username, password 정보도 있고 Security 인증 관련해서 토큰 발급을 위한 secret_key도 있을 수 있습니다.&lt;br /&gt;&lt;br /&gt;이러한 민감 정보들을 Spring 프로젝트에서 application.yml 파일에 작성해놓고 github origin repository에 push하는 순간 외부에 DB 서버 접근 정보를 노출하게 되는 심각한 상황에 마주하게 될 것입니다.&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;b&gt;  0. Vault를 도입하게 된 직접적인 계기...&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot를 이용한 개인 애플리케이션 개발을 진행하면서 Jenkins를 통해 CI/CD 자동화 프로세스를 적용해보았는데요. 그 과정에서 DB 정보나, 민감 데이터에 대한 처리가 상당히 번거로웠습니다.&lt;br /&gt;&lt;br /&gt;위에서 언급한 &lt;b&gt;DB 정보 설정&lt;/b&gt;을 예시로 보겠습니다.&lt;br /&gt;&lt;br /&gt;Jenkins pipeline step들 중 DB 설정 내용과 관련있는 step만 보면 다음과 같습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&amp;gt; Init&lt;br /&gt;&amp;nbsp; - 이 때 Jenkins에 설정된 DB Connection Credential 정보를 가져와 username, password을 가져옵니다.&lt;br /&gt;&lt;br /&gt;&amp;gt; DB Validate&lt;br /&gt;&amp;nbsp; - flyway migration을 통해 DB에 이미 적용된 flyway version 정보들과 비교해 최신화된 상태인지 체크&lt;br /&gt;&amp;nbsp; - DATABASE_HOST 정보는 Jenkins 환경 설정에서 환경변수로 따로 등록한 내용을 가져옵니다.&lt;br /&gt;&amp;nbsp; - username, password 데이터 적용&lt;br /&gt;&lt;br /&gt;...&lt;br /&gt;&lt;br /&gt;&amp;gt; Run Application with Ansible&lt;br /&gt;&amp;nbsp; - ansible을 이용해 실제 애플리케이션을 배포 및 실행하는 단계입니다.&lt;br /&gt;&amp;nbsp; - 여기서도 DB 정보들이 필요하기 때문에 extraVars를 이용해 ansible로 DB Connection 정보들을 전달합니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Jenkins pipeline script로 살펴보면 다음과 같습니다.&lt;/p&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&gt;
&lt;pre id=&quot;code_1690557039164&quot; class=&quot;go&quot; data-ke-language=&quot;go&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;stage('Init') {
    steps {
        script {
            //...

            // DB Connection Credential
            DB_CONNECTION_CREDENTIAL = 'b873f9bf-03cc-4daf-be4f-7e00194aa2a0'
            withCredentials([
                    usernamePassword(
                            credentialsId: &quot;${DB_CONNECTION_CREDENTIAL}&quot;,
                            usernameVariable: 'username',
                            passwordVariable: 'password'
                    )
            ]) {
                DATABASE_USERNAME = &quot;${username}&quot;
                DATABASE_PASSWORD = &quot;${password}&quot;
            }

            //...
        }
    }
}

stage('DB Validate') {
    steps {
        flywayrunner installationName: 'flywaytool-jenkins',
                flywayCommand: 'info validate',
                commandLineArgs: &quot;-configFiles=${MIGRATION_WORKSPACE}/flyway-${PROJECT_PROFILE}.conf&quot;,
                credentialsId: &quot;${DB_CONNECTION_CREDENTIAL}&quot;,
                url: &quot;jdbc:mysql://${DATABASE_HOST}:3306/dongne&quot;,
                locations: &quot;filesystem:${MIGRATION_WORKSPACE}/migration&quot;
    }
}

//...

stage('Run Application with Ansible') {
    steps {
        ansiblePlaybook inventory: &quot;${ANSIBLE_INVENTORY}&quot;,
                playbook: &quot;${ANSIBLE_PLAYBOOK}&quot;,
                extraVars: [
                		//...
                        database_host: &quot;${DATABASE_HOST}&quot;,
                        database_username: &quot;${DATABASE_USERNAME}&quot;,
                        database_password: &quot;${DATABASE_PASSWORD}&quot;
                ]
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB 연결정보를 필요로 하는 곳을 정리해보면 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DB Validate시 flyway 설정&lt;/li&gt;
&lt;li&gt;ansible에 외부 변수로 전달&lt;/li&gt;
&lt;li&gt;ansible 내에서 전달받은 DB Connection 정보를 가지고 애플리케이션 실행시 필요&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 위와 같은 방식은 secret 정보들에 대한 관리가 어려워진다는 단점이 존재합니다.&lt;br /&gt;&lt;br /&gt;DB 연결정보의 변경이 발생한다면 수정해야할 곳들이 여러 군데 발생하게 됩니다. Jenkins에 들어가서 환경변수 내용을 건드려야하고요. credential 내용도 수정해야할 것 입니다.&lt;br /&gt;&lt;br /&gt;또한 DB 연결정보 뿐만 아니라 인증토큰, 모니터링 채널정보 등 외부에는 공개가 되면 안되는 secret 정보들을 추가로 적용하려면 이곳 저곳에 덕지덕지 secret 값들을 붙여야 할 것 입니다. &lt;br /&gt;이러한 secret 정보들을 보안 안정성과 편리한 관리, 두 마리 토끼를 잡을 수 있는 방법이 있을까요? &lt;br /&gt;&lt;br /&gt;여기서 vault를 주목하게 됩니다.&lt;/p&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&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;b&gt;  1. Vault의 개념? 보다는 바로 설치해보자&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;vault란 무엇일까는 한 번 &lt;a href=&quot;https://developer.hashicorp.com/vault/tutorials?product_intent=vault&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;공식사이트(튜토리얼)&lt;/a&gt;나 다른 블로그를 참고하실 것을 추천드립니다. 왜냐하면 저는 vault를 단순히 제 개인 프로젝트의 secret 정보들을 편리하게 관리하기 위해서 적용해보기만 한 것이지 이것에 대한 정확한 개념이나 사용법을 알지 못합니다.&lt;/p&gt;
&lt;figure id=&quot;og_1690690395826&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Tutorials | Vault | HashiCorp Developer&quot; data-og-description=&quot;Explore Vault product documentation, tutorials, and examples.&quot; data-og-host=&quot;developer.hashicorp.com&quot; data-og-source-url=&quot;https://developer.hashicorp.com/vault/tutorials?product_intent=vault&quot; data-og-url=&quot;https://developer.hashicorp.com/vault/tutorials?product_intent=vault&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cML8DC/hyTtshFwO9/QtJlcwVNrjeRDQGOlbfSmk/img.jpg?width=3200&amp;amp;height=1800&amp;amp;face=0_0_3200_1800,https://scrap.kakaocdn.net/dn/Rx161/hyTtkqns03/ivxByFewegFF5EhbPGnSRk/img.jpg?width=3200&amp;amp;height=1800&amp;amp;face=0_0_3200_1800&quot;&gt;&lt;a href=&quot;https://developer.hashicorp.com/vault/tutorials?product_intent=vault&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.hashicorp.com/vault/tutorials?product_intent=vault&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cML8DC/hyTtshFwO9/QtJlcwVNrjeRDQGOlbfSmk/img.jpg?width=3200&amp;amp;height=1800&amp;amp;face=0_0_3200_1800,https://scrap.kakaocdn.net/dn/Rx161/hyTtkqns03/ivxByFewegFF5EhbPGnSRk/img.jpg?width=3200&amp;amp;height=1800&amp;amp;face=0_0_3200_1800');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Tutorials | Vault | HashiCorp Developer&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Explore Vault product documentation, tutorials, and examples.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.hashicorp.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼에도 vault에 대한 블로그를 왜 작성했냐라고 의문을 가지실 수 있을텐데요. 단순히 개발하는데 있어서 이러한 불편함이 있었고 이렇게 해결해보았습니다를 저도 다시 참고할 수 있고 다른 사람들에게도 공유할 수 있고 추후에 혹여나 면접을 보게 될 때 고민의 발자취를 남기고자 글을 작성하게 되었습니다.&lt;br /&gt;&lt;br /&gt;그래서 이번 글도 보실 때 &quot;아, 저 사람은 개인적으로 애플리케이션 개발할 때 이런 불편함과 고민들이 있었고 이런 방식으로도 해결을 해보았구나&quot;라고 귀엽게 봐주시면 될 것 같습니다. 덤으로 거기서 아이디어를 얻어가셨다면 저에게는 더할나위없이 보람찰 것 같네요.&lt;br /&gt;(그리고 블로그 글에 작성하는 내용은 대부분 이미 제가 속한 실무 뿐만아니라 여러 곳에서 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;보편적으로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt; 사용하고 있는 기술이기도 합니다.)&lt;br /&gt;&lt;br /&gt;이번 주제에서 살짝 벗어난 얘기를 길게 늘어놓았는데요. 위에서 vault를 사용하게 된 계기를 어느정도 알게 되었으니 본격적으로 vault를 사용해보고자 합니다. 테스트 단계에서 vault를 사용하는 방식은 여러가지가 있을 수 있는데요. 저는 클라우드 서버 인스턴스를 따로 생성해서 해당 서버에 vault 서버를 설치하는 방식으로 진행하였습니다.&lt;br /&gt;&lt;br /&gt;어디서 개발하든 어떤 로컬환경에서 개발하든 바로바로 적용해볼 수 있고 관리가 아주 편하기 때문에 사비가 살짝 들더라도 클라우드 서버를 이용하게 되었습니다. (상대적으로 저렴한 AWS Lightsail을 사용하였습니다.)&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;vault를 서버를 야매로 적용해본 것이기에 이를 감안해서 봐주시면 될 것 같습니다.&lt;/blockquote&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;  2. &lt;/b&gt;&lt;b&gt;vault 서버 설치하기(공식사이트의 Tutorials를 보면 다 나오는,,,)&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Vault는 dev 모드의 in-memory 방식으로 설치하는 방식이 있고 production 모드의 실제 서버를 실행해서 설치하는 방식이 있습니다. 저는 클라우드 서버에서 상시로 접근이 가능한 상태로 운영해야하기에 production 모드로 설치를 진행하였습니다.&lt;br /&gt;&lt;br /&gt;설치하는 방법은 정말 간단합니다. &lt;b&gt;&lt;a href=&quot;https://developer.hashicorp.com/vault/tutorials/getting-started-ui/getting-started-install#install-vault&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Vault 공식사이트의 듀토리얼 내용&lt;/a&gt;을 그대로 따라하시면 됩니다.&lt;/b&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1690691357039&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Install Vault | Vault | HashiCorp Developer&quot; data-og-description=&quot;The first step to using Vault is to get it installed.&quot; data-og-host=&quot;developer.hashicorp.com&quot; data-og-source-url=&quot;https://developer.hashicorp.com/vault/tutorials/getting-started-ui/getting-started-install#install-vault&quot; data-og-url=&quot;https://developer.hashicorp.com/vault/tutorials/getting-started-ui/getting-started-install#install-vault&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bLdw78/hyTtrXnxA9/5Bc17n91vi11JwJ52ptdxK/img.jpg?width=3200&amp;amp;height=1800&amp;amp;face=0_0_3200_1800,https://scrap.kakaocdn.net/dn/bfSPxT/hyTtvSZ7BI/8fmatcHItakakHrxZGAZFk/img.jpg?width=3200&amp;amp;height=1800&amp;amp;face=0_0_3200_1800&quot;&gt;&lt;a href=&quot;https://developer.hashicorp.com/vault/tutorials/getting-started-ui/getting-started-install#install-vault&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.hashicorp.com/vault/tutorials/getting-started-ui/getting-started-install#install-vault&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bLdw78/hyTtrXnxA9/5Bc17n91vi11JwJ52ptdxK/img.jpg?width=3200&amp;amp;height=1800&amp;amp;face=0_0_3200_1800,https://scrap.kakaocdn.net/dn/bfSPxT/hyTtvSZ7BI/8fmatcHItakakHrxZGAZFk/img.jpg?width=3200&amp;amp;height=1800&amp;amp;face=0_0_3200_1800');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Install Vault | Vault | HashiCorp Developer&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;The first step to using Vault is to get it installed.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.hashicorp.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치 방법은 끝입니다. 네 정말 끝입니다.&lt;br /&gt;&lt;br /&gt;그런데 저는 멍청이라서 친절한 듀토리얼 설명에도 설치하다가 막혔던 것들이 있었는데요. 그 내용만 짚고 넘어가도록 하겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1690691506900&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ sudo yum -y install vault&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Linux에서 Amazon Linux 방식으로 vault 패키지를 설치해주면 다음의 디렉토리에서 vault 내용이 설치된 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1690691653155&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;1. /opt/vault/
2. /etc/vault.d/&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1번 디렉토리안에는 &lt;b&gt;data&lt;/b&gt;, &lt;b&gt;tls&lt;/b&gt; 디렉토리가 담겨있고, 2번 디렉토리 안에는 환경변수를 담은 파일과 기본 설정 파일(&lt;b&gt;vault.hcl&lt;/b&gt;)이 담겨있습니다. 우선 이 정도만 알면 될 것 같습니다.&lt;/p&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;  3.&lt;/b&gt;&lt;b&gt;&amp;nbsp;설정파일 만들고 실행해보자&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;vault 서버를 실행하기 위해서는 실행을 위한 &lt;b&gt;설정파일&lt;/b&gt;이 있어야합니다. 설정파일은 주로 hcl 확장자로 이루어진 파일을 사용하게 되는데요. hcl은 &lt;a href=&quot;https://github.com/hashicorp/hcl&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Hashicorp Configuration Language&lt;/a&gt;의 약자라고 하네요. hashicorp는 vault를 개발하고 관리하는 회사이고 그 회사에서 자체적으로 개발한 설정용 언어인 것 같습니다. 심심하시면 링크 걸어드린 github 내용 참고해보시거나 다른 글 참고해보시면 될 것 같습니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1690692513996&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# /etc/vault.d/config.hcl
ui = true

#mlock = true
disable_mlock = true

storage &quot;file&quot; {
  path = &quot;/opt/vault/data&quot;
}

# HTTPS listener
listener &quot;tcp&quot; {
  address       = &quot;0.0.0.0:8200&quot;
  tls_disable 	= &quot;true&quot;
#  tls_cert_file = &quot;/opt/vault/tls/tls.crt&quot;
#  tls_key_file  = &quot;/opt/vault/tls/tls.key&quot;
}

api_addr = &quot;http://127.0.0.1:8200&quot;
cluster_addr = &quot;http://127.0.0.1:8201&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;/etc/vault.d/&lt;/b&gt; 디렉토리에 새로운 설정파일(&lt;b&gt;config.hcl&lt;/b&gt;)을 만들었습니다. 위의 내용들을 공식사이트 듀토리얼에 다 나오는 내용인데요. 몇 가지 주의할 점이 있습니다. storage 설정에서는 secret 내용이나 authentication 정보를 어떻게 저장하고 관리할 것인지 설정하는데요. file 방식으로 했고 디렉토리는 기본적으로 생성된 &lt;b&gt;/opt/vault/data&lt;/b&gt;를 바라보게 하였습니다. &lt;br /&gt;(raft 방식도 있는데 정확히 어떤 것인지 몰라 가장 간단해보이는 file 방식으로 하였습니다. raft 방식으로 해보신 분 있으시면 이게 무슨 개념인지 알려주시면 감사하겠습니다)&lt;br /&gt;&lt;br /&gt;listener는 어떤 요청방식에 응답할 것인지 설정하는 것인데요. tcp, unix 두 개 방식이 있고 보편적으로 사용되는 것 같아보이는 tcp 방식으로 설정했습니다. tcp에는 tls 인증서도 적용할 수 있는데요. 저는 따로 하지않고 &lt;b&gt;tls 자체를 비활성화 처리했습니다.&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;추후에 글을 작성하겠지만 저는 돈을 조금이라도 더 아끼고자 하나의 AWS instance에 vault와 jenkins 두 개의 서버를 적용했는데요. &lt;b&gt;nginx를 통해 https를 적용할 것이기 때문에 vault 자체적으로는 따로 tls 설정을 하지 않았습니다.&lt;/b&gt; &lt;br /&gt;(혹시 vault의 tls 설정이 정확하게 어떤 것을 의미하는지 아시는 분 계시면 알려주시면 감사하겠습니다.)&lt;br /&gt;&lt;br /&gt;요건 좀 중요한데요. listener에서 address를 &lt;b&gt;&quot;0.0.0.0:8200&quot;&lt;/b&gt;로 하지 않으면 외부에서 해당 포트로 vault 접속이 안됩니다. 누락되지 않도록 주의해주세요. (8200번 포트는 vault의 기본 포트입니다.)&lt;/p&gt;
&lt;pre id=&quot;code_1690693122653&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ vault server -config=/etc/vault.d/config.hcl&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 설정파일을 가지고 vault를 실행해줍니다. 그러면 뭐라고 콘솔에 나오면서 &lt;b&gt;Vault server started!&lt;/b&gt; 라고 나오면 서버가 잘 실행된 것입니다.&lt;/p&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;  4.&lt;/b&gt;&lt;b&gt; vault 서버 초기화(Unsealing Vault server) 및 접속&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;vault는 서버를 실행하면 끝이 아니라 초기화 작업을 해주어야 합니다. vault 공식문서를 보면 내부 동작원리를 설명하고 있는데요. 저는 아직 이부분에 대해서 이해를 하지 못해 아쉽게도 자세하게 설명을 드릴 순 없을 것 같습니다.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;그 중에 알고 넘어가야 하는 부분이 있는데요. &lt;a href=&quot;https://youtu.be/ae72pKpXe-s?t=1719&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Vault Seal &amp;amp; Unseal&lt;/a&gt; 개념입니다.&lt;br /&gt;(제가 참고했던 유투브 강의가 있는데 시간 되시는 분들은 보시는 걸 추천드립니다.)&lt;/p&gt;
&lt;figure data-ke-type=&quot;video&quot; data-ke-style=&quot;alignCenter&quot; data-video-host=&quot;youtube&quot; data-video-url=&quot;https://www.youtube.com/watch?v=ae72pKpXe-s&quot; data-video-thumbnail=&quot;https://scrap.kakaocdn.net/dn/meByV/hyTtvrXcEf/6BmK9zBNEPksxM0QdmfiL0/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=816_196_1032_432&quot; data-video-width=&quot;760&quot; data-video-height=&quot;428&quot; data-video-origin-width=&quot;860&quot; data-video-origin-height=&quot;484&quot; data-ke-mobilestyle=&quot;widthContent&quot; data-original-url=&quot;&quot; data-video-title=&quot;&quot;&gt;&lt;iframe src=&quot;https://www.youtube.com/embed/ae72pKpXe-s&quot; width=&quot;760&quot; height=&quot;428&quot; frameborder=&quot;&quot; allowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Vault는 sealed, 즉 봉인된 상태로 서버가 실행이 됩니다. Vault 보안을 특히 중시하고 있어서 vault로 물리저장소에 저장된 secret 데이터에 접근은 할 수 있지만 encrypted된 상태이기에 그 자체로 데이터를 사용할 수 없게 되어있습니다.&lt;br /&gt;&lt;br /&gt;물리저장소에 암호화된 데이터를 꺼내서 사용할 수 있는 유일한 방법은 &lt;b&gt;vault 자체를 unseal, 즉 봉인해제를 하는 방법인데요.&lt;/b&gt; 여기서 &lt;b&gt;shared shamir keys&lt;/b&gt;가 사용됩니다.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;shared shamir keys 개념은 아주아주 복잡하지만 정말 간단하게 얘기하면 &lt;span style=&quot;background-color: #ffffff; color: #121212; text-align: left;&quot;&gt;비밀내용을 여러 조각으로 쪼개서 일정 갯수 이상의 조각이 모였을때만 비밀내용을 다시 복원할 수 있도록 하는 방법입니다. vault를 unsealing하기 위해 unseal key가 필요한데 그 키를 여러 조각으로 쪼개서 보관한다고 생각하시면 됩니다. &lt;br /&gt;&lt;br /&gt;&lt;u&gt;&lt;b&gt;vault 서버 초기화는 바로 shared shamir keys를 생성하고 unsealing 작업을 해주는 것이라 보면 됩니다.&lt;/b&gt;&lt;/u&gt;&lt;br /&gt;(shared shamir keys에 대한 내용, 그리고 관련한 암호학에 대해서도 공부해보면 좋겠다는 생각도 들었네요.)&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1690694422968&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ export VAULT_ADDR='http://127.0.0.1:8200'

$ vault operator init&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 먼저 linux 서버의 환경변수로 &lt;b&gt;VAULT_ADDR&lt;/b&gt;를 설정해줍니다. 이걸 해야 초기화 작업을 진행할 수 있습니다. 그 다음 vault init을 시작합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1690694489406&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Unseal Key 1: 4jYbl2CBIv6SpkKj6Hos9iD32k5RfGkLzlosrrq/JgOm
Unseal Key 2: B05G1DRtfYckFV5BbdBvXq0wkK5HFqB9g2jcDmNfTQiS
Unseal Key 3: Arig0N9rN9ezkTRo7qTB7gsIZDaonOcc53EHo83F5chA
Unseal Key 4: 0cZE0C/gEk3YHaKjIWxhyyfs8REhqkRW/CSXTnmTilv+
Unseal Key 5: fYhZOseRgzxmJCmIqUdxEm9C3jB5Q27AowER9w4FC2Ck

Initial Root Token: s.KkNJYWF5g0pomcCLEmDdOVCW&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;init을 실행하면 위와 같이 unseal key에 대한 5개의 shared shamir key를 생성해주고 root token도 생성해줍니다. 해당 정보를 꼭 메모장이든 어디든 기록해두시고 지워지지 않도록 은밀한 곳에 잘 보관하셔야 합니다.&lt;/p&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&gt;
&lt;pre id=&quot;code_1690694585273&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; vault operator unseal

Unseal Key (will be hidden): [unseal key 입력]
Key                Value
---                -----
Seal Type          shamir
Initialized        true
Sealed             true
Total Shares       5
Threshold          3
Unseal Progress    1/3
Unseal Nonce       d3d06528-aafd-c63d-a93c-e63ddb34b2a9
Version            1.7.0
Storage Type       raft
HA Enabled         true&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;vault operator 명령어로 unseal 작업을 진행합니다. 위와 같이 unseal 명령어를 입력하면 unseal key를 입력하라고 나오는데요. 거기에 init할 때 받았던 5개의 unseal key 조각들 중 아무거나 입력하면 됩니다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;한 번에 하나씩만 입력되기 때문에 위 작업을 3번 해주어야 unseal 작업이 완료됩니다.&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;unseal 작업이 완료되면 이제 브라우저 화면을 띄워서 &lt;b&gt;http://[AWS Instance hostname]:8200&lt;/b&gt;에 접속해줍니다.&lt;br /&gt;(외부에서 AWS instance 접속하실 때 방화벽 설정에서 8200번 포트가 적절하게 open되어 있는지 꼭 체크해주세요.)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2023-07-30 at 2.27.27 PM.png&quot; data-origin-width=&quot;3490&quot; data-origin-height=&quot;1956&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nnFPg/btsplLxWaYU/po1jCCS0TPF1jxdkkJQgHk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nnFPg/btsplLxWaYU/po1jCCS0TPF1jxdkkJQgHk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nnFPg/btsplLxWaYU/po1jCCS0TPF1jxdkkJQgHk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnnFPg%2FbtsplLxWaYU%2Fpo1jCCS0TPF1jxdkkJQgHk%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;760&quot; height=&quot;426&quot; data-filename=&quot;Screenshot 2023-07-30 at 2.27.27 PM.png&quot; data-origin-width=&quot;3490&quot; data-origin-height=&quot;1956&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;vault에 접속하면 위와 같이 vault 로그인 화면이 나옵니다. 기본 method가 Token으로 나오는데요. 여기에 init할 때 받은 Initial Root Token 값을 입력해줍니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2023-07-30 at 2.29.59 PM.png&quot; data-origin-width=&quot;3570&quot; data-origin-height=&quot;1332&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zzVpv/btsplPN6ngW/WdQo6HF99f3l97FvBkOXCK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zzVpv/btsplPN6ngW/WdQo6HF99f3l97FvBkOXCK/img.png&quot; data-alt=&quot;인증하고 접속한 vault 관리 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zzVpv/btsplPN6ngW/WdQo6HF99f3l97FvBkOXCK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzzVpv%2FbtsplPN6ngW%2FWdQo6HF99f3l97FvBkOXCK%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;760&quot; height=&quot;284&quot; data-filename=&quot;Screenshot 2023-07-30 at 2.29.59 PM.png&quot; data-origin-width=&quot;3570&quot; data-origin-height=&quot;1332&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;인증하고 접속한 vault 관리 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인증이 완료되면 위와 같이 vault 관리 화면이 나오게 됩니다. 축하드립니다. 여기까지 성공하셨다면 정말 기본적인 vault 서버 설치를 완료하신 겁니다.&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;b&gt;  5. 마무리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Vault는 실무에서도 거의 필수로 사용되는 secret 정보 관리 툴입니다. 그만큼 뛰어난 보안과 관리의 편리함을 제공해주는데요. 위의 구축된 내용을 기반으로 Spring Boot Application에 어떻게 연동을 하는지에 대해서 따로 게시글을 작성해보려 합니다.&lt;br /&gt;&lt;br /&gt;vault root token, unseal keys는 아주아주 중요한 token 값들이기 때문에 외부에 노출되거나 잃어버리는 일이 없도록 잘 관리하셔야 합니다.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;vault의 개념을 좀더 알고 싶으신 분들은 이번 게시글 중간에 링크드린 유투브 강의 꼭 보시는 것을 추천드립니다. 1시간 분량이고 영어로 되어있긴 하지만 자막으로 충분히 커버 가능하고, 처음 vault를 접해보는 분들에게 정말 유익한 강의라고 생각합니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;틀린 내용 있을 수 있습니다. 피드백 언제나 환영합니다. 감사합니다.&lt;/blockquote&gt;</description>
      <category>Infra</category>
      <category>Infra</category>
      <category>Vault</category>
      <author>beaniejoy</author>
      <guid isPermaLink="true">https://beaniejoy.tistory.com/100</guid>
      <comments>https://beaniejoy.tistory.com/100#entry100comment</comments>
      <pubDate>Sun, 30 Jul 2023 14:41:28 +0900</pubDate>
    </item>
    <item>
      <title>[JPA] UnexpectedRollbackException과 AOP에서 상황별 롤백(rollback)여부에 대해 알아보자</title>
      <link>https://beaniejoy.tistory.com/99</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이번 게시글은 Spring에서 자주 사용하는 &lt;b&gt;@Transactional&lt;/b&gt;이 예외를 마주하게 되었을 때 발생하는 롤백에 대해서 정리해보고자 합니다.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;@Transactional&lt;/b&gt;에서 여러 propagation 옵션이 있는데 이번 게시글에서는&amp;nbsp;&lt;b&gt;REQUIRED&lt;/b&gt;, &lt;b&gt;REQUIRES_NEW&lt;/b&gt;에 대해서만 다룰 예정입니다. 두 개의 propagation 상황에서 어떤 Exception이 언제 발생하는지에 따라 롤백이 일어나는지 아닌지에 대해서도 다루려고 합니다. 그리고 또한 Custom AOP를 적용했을 때 그 안에서 발생하는 예외는 어떻게 처리가 되는지 정리해보고자 합니다.&lt;br /&gt;&lt;br /&gt;지금부터 여러 케이스를 통해 롤백 발생여부를 알아보려 하는데요. 기본적인 테스트를 위한 로직 틀은 다음과 같습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1687186563112&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Slf4j
@Service
@RequiredArgsConstructor
public class ParentService {
    private final ChildService childService;
    private final ChildRepository childRepository;
    
    @Transactional
    public void justCallChildService() {
        Cafe cafe = Cafe.builder()
                .name(&quot;joy's cafe&quot;)
                .description(&quot;joy cafe desc&quot;)
                .phoneNumber(&quot;01033334444&quot;)
                .address(&quot;joy cafe's address&quot;)
                .build();

        cafeRepository.save(cafe);

        childService.justSave();
    }
}

@Slf4j
@Service
@RequiredArgsConstructor
public class ChildService {
    private final ChildRepository childRepository;
    
    @Transactional
    public void justSave() {
        Cafe cafe = Cafe.builder()
                .name(&quot;beanie's cafe&quot;)
                .description(&quot;beanie cafe desc&quot;)
                .phoneNumber(&quot;01023450981&quot;)
                .address(&quot;beanie cafe's address&quot;)
                .build();

        cafeRepository.save(cafe);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ParentService에서 joy cafe를 JPA Repository를 통해 저장하고, 이후에 ChildService의 메소드를 호출합니다. 그 안에서는 마찬가지로 beanie cafe를 JPA Repository를 통해 저장하는 아주 단순한 로직입니다.&lt;br /&gt;&lt;br /&gt;이 로직을 기반으로 해서 한 번 여러 케이스를 통해 롤백여부를 체크해봅시다.&lt;/p&gt;
&lt;div&gt;&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;  1. Method 내부에 또 다른 Method 호출하는 상황&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1-1. parent, child 둘 다 @Transactional 기본 propagation&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1687186789466&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ParentService
@Transactional
public void justCallChildService() {
    Cafe cafe = Cafe.builder()
            .name(&quot;joy's cafe&quot;)
            .description(&quot;joy cafe desc&quot;)
            .phoneNumber(&quot;01033334444&quot;)
            .address(&quot;joy cafe's address&quot;)
            .build();

    cafeRepository.save(cafe);

    childService.justSave();
}

//ChildService
@Transactional
public void justSave() {
    Cafe cafe = Cafe.builder()
            .name(&quot;beanie's cafe&quot;)
            .description(&quot;beanie cafe desc&quot;)
            .phoneNumber(&quot;01023450981&quot;)
            .address(&quot;beanie cafe's address&quot;)
            .build();

    cafeRepository.save(cafe);
    throw new RuntimeException(&quot;child exception&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Parent와 Child에 있는 Method 모두 기본 @Transactional 입니다. 해당 어노테이션 안에는 propagation을 지정할 수 있는데요. 기본 값은 &lt;b&gt;&quot;REQUIRED&quot;&lt;/b&gt; 입니다.&lt;br /&gt;&lt;b&gt;&lt;br /&gt;REQUIRED&lt;/b&gt;는 &lt;u&gt;이전에 트랜잭션이 시작해서 현재 트랜잭션이 활성화된 상태면 그대로 사용하고 만약 트랜잭션이 없다면 새로 시작하는 전략입니다.&lt;/u&gt; 위의 상황에서는 ParentService에서 트랜잭션을 시작했고 이후 ChildService를 호출하고 있기 때문에 &lt;b&gt;Parent, Child 모두 하나의 트랜잭션으로 묶여있다고 보면 됩니다.&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;Child에서 RuntimeException을 의도적으로 발생시키면 모두가 예상한 대로 해당 트랜잭션은 롤백이 되면서 &lt;b&gt;Parent, Child 모두 롤백됩니다.(하나의 트랜잭션으로 묶여 있었기 때문에)&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1687187253522&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ParentService
@Transactional
public void justCallChildService() {
    Cafe cafe = Cafe.builder()
        .name(&quot;joy's cafe&quot;)
        .description(&quot;joy cafe desc&quot;)
        .phoneNumber(&quot;01033334444&quot;)
        .address(&quot;joy cafe's address&quot;)
        .build();

    cafeRepository.save(cafe);

    try {	
        childService.justSave();
    } catch (Exception e) {
        log.error(e.getMessage());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 RuntimeException도 해당 메소드에서 try ~ catch로 처리해주면 롤백되지 않고 괜찮지 않을까 생각할 수 있는데요. 그래도 &lt;b&gt;Parent, Child 모두 롤백&lt;/b&gt;됩니다. Child에서 RuntimeException이 발생한 상황에서 Transaction은 따로 롤백마크를 지정해두었다가 Parent 메소드가 끝나면서 롤백마크를 근거로 전체 롤백을 하게 됩니다.&lt;br /&gt;&lt;br /&gt;&lt;u&gt;이 때 발생하는 에러가 &lt;b&gt;UnexpectedRollbackException&lt;/b&gt; 입니다.&lt;/u&gt; &lt;br /&gt;(메소드는 끝났는데 말 그대로 정말 예기치 않은 롤백을 마주하게 됐다는 늬앙스가 있는 것 같네요.)&lt;br /&gt;&lt;br /&gt;만약 ParentService에서 호출하고 있는 &lt;b&gt;ChildSerivce&lt;/b&gt; method(&quot;justSave()&quot;)에서도 RuntimeException 발생하는 부분을 try ~ catch로 처리한다면 이 때는 &lt;b&gt;전체 커밋&lt;/b&gt;이 될 것입니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;뒤에서 다시 언급드리겠지만 노파심에서 먼저 말씀드리자면 Transaction과 java의 checked, unchecked Exception(특히 RuntimeException)는 아무런 관련이 없습니다.&lt;br /&gt;&lt;br /&gt;Spring의 @Transactional 어노테이션 코드를 보면 rollback 마크를 default로 RuntimeException이 설정된 것을 확인할 수 있습니다. 우리는 이를 그대로 별생각 없이 사용했었기 때문에 RuntimeException 발생하면 롤백되는구나라고 생각해왔을 수 있습니다. &lt;br /&gt;&lt;br /&gt;즉 RuntimeException은 Spring Transactional에서 기본적으로 롤백마크 설정한 예외이지,&amp;nbsp;둘 사이 관계(트랜잭션과 RuntimeException)는 전혀 관련 없다는 것을 인지하시고 이어서 게시글을 보시면 좋을 것 같습니다.&lt;/blockquote&gt;
&lt;div&gt;&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1-2. parent: &quot;REQUIRED&quot; / child: &quot;REQUIRES_NEW&quot; 상황&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1687348391392&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ParentService
@Transactional
public void callChildServiceWithNewTx() {
    Cafe cafe = Cafe.builder()
            .name(&quot;joy's cafe&quot;)
            .description(&quot;joy cafe desc&quot;)
            .phoneNumber(&quot;01033334444&quot;)
            .address(&quot;joy cafe's address&quot;)
            .build();

    cafeRepository.save(cafe);

    childService.saveAndThrowRuntimeExceptionWithNewTx();
}

// ChildService
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveAndThrowRuntimeExceptionWithNewTx() {
    Cafe cafe = Cafe.builder()
            .name(&quot;beanie's cafe&quot;)
            .description(&quot;beanie cafe desc&quot;)
            .phoneNumber(&quot;01023450981&quot;)
            .address(&quot;beanie cafe's address&quot;)
            .build();

    cafeRepository.save(cafe);
    throw new RuntimeException(&quot;test&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 케이스와 다른 점은 ChildService의 메소드에 @Transactional 전파(propagation)전략을 &lt;b&gt;&quot;REQUIRES_NEW&quot;&lt;/b&gt;로 설정한 점입니다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;REQUIRES_NEW&lt;/b&gt;는 &lt;u&gt;이전에 생성된 트랜잭션이 있어도 해당 메소드 안에서는 새로운 트랜잭션을 만들어 사용한다는 전파 전략입니다.&lt;/u&gt;&lt;br /&gt;즉, A에서 B로 호출했을 때 A에 이미 트랜잭션 시작을 한 상황이라면 B로 들어가기 전에 기존의 트랜잭션을 잠시 멈추고, 새로운 트랜잭션을 생성해 B에 적용하는 것 같습니다. &lt;br /&gt;물론, B가 끝나면 새로운 트랜잭션은 commit 되고 기존의 트랜잭션에서 수행하고 있던 로직을 다시 진행할 것입니다.&lt;br /&gt;&lt;br /&gt;이 상황에서 기대하는 결과는 Parent와 Child가 서로 다른 트랜젹션으로 움직이고 있기 때문에 Child에서 예외가 발생하면 Child 내용만 롤백되고 Parent 내용은 커밋될 것이라고 생각할 수 있습니다.&lt;br /&gt;&lt;br /&gt;하지만 예상은 보기 좋게 빗나가는데요. &lt;b&gt;둘 다 롤백 됩니다.&lt;/b&gt; 이유는 단순합니다. Exception은 throw 하는 순간 try ~ catch로 처리하지 않는 이상 호출한 상위 메소드로 예외가 전가되기 때문입니다.&lt;br /&gt;&lt;b&gt;즉, Child에서 발생한 예외를 따로 처리하지 않았기 때문에 Child를 호출했던 Parent의 메소드로 예외가 전가가 되는 것이고 Parent에도 예외를 따로 처리하지 않았기 때문에 전체 롤백이 되는 것이라 생각할 수 있습이다.&lt;br /&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1687349398523&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ParentService
try {
    childService.saveAndThrowRuntimeExceptionWithNewTx();
} catch (Exception e) {
    log.error(e.getMessage());
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 똑같은 상황에서 ParentService에서 Child 메소드 호출한 부분을 try ~ catch로 예외처리하게 되면 어떻게 될까요?&lt;br /&gt;이 때는 우리가 예상한 대로 &lt;b&gt;Parent는 커밋이 되고, Child는 롤백이 됩니다. &lt;br /&gt;REQUIRES_NEW &lt;/b&gt;전략을 사용해서 새로운 트랜잭션을 제대로 사용하고 싶다면 예외 처리에 대해서 잘 생각을 해야할 것 같습니다.&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;div&gt;&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&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;b&gt;  2. Checked Exception&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 RuntimeException, 다시 말하면 Unchecked Exception에 대해서 테스트를 해보았는데요. 이번에는 Checked Exception에 대해서 트랜잭션 롤백이 발생하는지 다른 케이스를 통해 알아보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1687349711660&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ParentService
@Transactional
public void callChildServiceThrowChecked() throws IOException {
    Cafe cafe = Cafe.builder()
            .name(&quot;joy's cafe&quot;)
            .description(&quot;joy cafe desc&quot;)
            .phoneNumber(&quot;01033334444&quot;)
            .address(&quot;joy cafe's address&quot;)
            .build();

    cafeRepository.save(cafe);

    childService.justSave();

    throw new IOException(&quot;test&quot;);
}

// ChildService
@Transactional
public void justSave() {
    Cafe cafe = Cafe.builder()
            .name(&quot;beanie's cafe&quot;)
            .description(&quot;beanie cafe desc&quot;)
            .phoneNumber(&quot;01023450981&quot;)
            .address(&quot;beanie cafe's address&quot;)
            .build();

    cafeRepository.save(cafe);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 Parent method 마지막에 IOException을 발생시켜보았습니다. Child는 정상 동작하도록 했습니다. 여기서 전파 전략은 둘 다 기본 전략(&lt;b&gt;REQUIRED&lt;/b&gt;)으로 설정했습니다. &lt;br /&gt;&lt;br /&gt;예상하는 결과는 Parent 마지막에 예외가 발생했고 Parent, Child 둘 다 동일한 트랜잭션 안에 묶여있기 때문에, 둘 다 롤백이 될 것이라 생각하는데요. 실제로 실행하면 &lt;b&gt;둘 다 커밋이 됩니다.&lt;br /&gt;&lt;br /&gt;&lt;/b&gt;오잉 예외가 발생했는데 둘 다 커밋이라니 이상할 수 있는데요. &lt;u&gt;&lt;b&gt;RuntimeExcepti&lt;/b&gt;&lt;b&gt;on&lt;/b&gt;&lt;b&gt;,&lt;/b&gt;&lt;b&gt; E&lt;/b&gt;&lt;b&gt;rror과 다르게 Checked Exception은 롤백 마킹 대상이 아니기 때문입니다.&lt;/b&gt;&lt;b&gt;이것은 Spring에서의 @Transactional에서 해당하는 내용입니다.&lt;/b&gt;&lt;/u&gt; &lt;br /&gt;&lt;br /&gt;@Transactional에서 rollback 관련된 설정을  할 수 있는데요. rollbackFor 옵션에 다음과 같은 설명이 있습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;By default, a transaction will be rolling back on RuntimeException and Error but not on checked exceptions (business exceptions)&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 docs에도 이에 대한 설명이 있는데요. (&lt;a href=&quot;https://docs.spring.io/spring-framework/docs/4.2.x/spring-framework-reference/html/transaction.html#transaction-declarative-attransactional-settings&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;링크 참고&lt;/a&gt;)&lt;br /&gt;&lt;br /&gt;즉, Spring에서의 &lt;b&gt;@Transactional&lt;/b&gt;은 rollback 타겟에 대해 default로 &lt;b&gt;RuntimeException, Error&lt;/b&gt;에 대해서만 적용한 것을 확인할 수 있습니다. 그래서 방금의 &lt;b&gt;IOException&lt;/b&gt;이 발생하는 상황에서 모두 커밋이 된 것입니다.&lt;/p&gt;
&lt;div&gt;&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&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;b&gt;  3. AOP에서 발생한 Exception&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 Custom한 AOP를 ChildService 메소드에 적용했을 때 AOP에서 발생한 Exception은 어떻게 처리가 될까요? 상황을 통해 한 번 알아봅시다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3-1. AOP에서 발생한 Checked, Unchecked Exception&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1687967250641&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface CustomAnnotation {
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface CustomAnnotation2 {
}

@Slf4j
@Aspect
@Component
public class CustomAspect {
    @Around(&quot;@annotation(io.beaniejoy.springdatajpa.common.CustomAnnotation)&quot;)
    public Object testAround(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info(&quot;&amp;gt;&amp;gt;&amp;gt; TestAspect START&quot;);
        Object result = joinPoint.proceed();
        log.info(&quot;&amp;gt;&amp;gt;&amp;gt; TestAspect END&quot;);

        throw new RuntimeException(&quot;aop unchecked exception&quot;);

//        return result;
    }
    
    @Around(&quot;@annotation(io.beaniejoy.springdatajpa.common.CustomAnnotation2)&quot;)
    public Object testAround2(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info(&quot;&amp;gt;&amp;gt;&amp;gt; TestAspect START&quot;);
        Object result = joinPoint.proceed();
        log.info(&quot;&amp;gt;&amp;gt;&amp;gt; TestAspect END&quot;);

        throw new IOException(&quot;aop checked exception&quot;);

//        return result;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 간단하게 2개의 CustomAnnotation을 만들고 해당 어노테이션들에 Aspect를 설정했습니다. &lt;br /&gt;그리고 AOP에는 실제 메소드가 처리된 이후에 Exception을 의도적으로 발생하게 했는데요. 하나는 &lt;b&gt;RuntimeException&lt;/b&gt;으로 다른 하나는 &lt;b&gt;IOException&lt;/b&gt;으로 설정했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1688080714694&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ChildService.java
@Transactional
@CustomAnnotation
public void saveWithCustomAspectThrowRuntimeException() {
    Cafe cafe = Cafe.builder()
            .name(&quot;beanie's cafe&quot;)
            .description(&quot;beanie cafe desc&quot;)
            .phoneNumber(&quot;01023450981&quot;)
            .address(&quot;beanie cafe's address&quot;)
            .build();

    cafeRepository.save(cafe);
}

@Transactional
@CustomAnnotation2
public void saveWithCustomAspectThrowCheckedException() {
    Cafe cafe = Cafe.builder()
            .name(&quot;beanie's cafe&quot;)
            .description(&quot;beanie cafe desc&quot;)
            .phoneNumber(&quot;01023450981&quot;)
            .address(&quot;beanie cafe's address&quot;)
            .build();

    cafeRepository.save(cafe);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CustomAnnotation 두 개를 각각 적용한 메소드를 &lt;b&gt;ChildService&lt;/b&gt;에 만들었습니다. 먼저 해당 메소드들을 직접 호출하면 어떻게 되는지 테스트코드로 실행해보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1688080843684&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
public void child_with_tx_REQUIRED_and_custom_aspect_exception_test() {
    // 둘 다 RuntimeException 으로 떨어짐
    
    // RuntimeException
    assertThrows(RuntimeException.class, () -&amp;gt; {
        childService.saveWithCustomAspectThrowRuntimeException();
    });

    // checked exception &amp;gt; UndeclaredThrowableException로 던져짐
    assertThrows(UndeclaredThrowableException.class, () -&amp;gt; {
        childService.saveWithCustomAspectThrowCheckedException();
    });
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AOP에서 RuntimeException, IOException 예외가 발생하는 상황에서 &lt;u&gt;&lt;b&gt;두 개의 메소드를 직접 호출하면 위와 같이 RuntimeException 예외가 발생하게 됩니다.&lt;/b&gt;&lt;/u&gt;&lt;br /&gt;&lt;br /&gt;RuntimeException을 던진 &lt;b&gt;@CustomAnnotation&lt;/b&gt;에서는 납득이 되는데 &lt;u&gt;IOException을 던지는 &lt;b&gt;@CustomAnnotation2&lt;/b&gt;에서도 RuntimeException이 발생한다는 것은 납득이 안가는데요.&lt;/u&gt; 이부분에 대해서 잘 설명해주는 글이 있는데요.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;If the proxy itself throws a checked exception, from the caller's perspective, the&amp;nbsp;save&amp;nbsp;method throws that checked exception.&lt;/b&gt;&amp;nbsp;The caller probably doesn't know anything about that proxy and will blame the&amp;nbsp;save&amp;nbsp;for this exception.&lt;br /&gt;In such circumstances, &lt;b&gt;Java will wrap the actual checked exception inside an&amp;nbsp;UndeclaredThrowableException&amp;nbsp;and throw the&amp;nbsp;UndeclaredThrowableException&amp;nbsp;instead.&lt;/b&gt;&amp;nbsp;It's worth mentioning that the&amp;nbsp;UndeclaredThrowableException&amp;nbsp;itself is an unchecked exception.&lt;br /&gt;&lt;br /&gt;# 출처 Baeldung's Post [When Does Java Throw UndeclaredThrowableException?]&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제가 예시로 들었던 상황으로 해석하자면 &lt;b&gt;@CustomAnnotation2&lt;/b&gt;이 적용된 ChildService의 &lt;b&gt;saveWithCustomAspectThrowCheckedException&lt;/b&gt; 메소드를 호출하는 쪽(caller)에서는 AOP에서 checked exception이 발생했다는 것을 모르기 때문에 caller 입장에서는 saveWithCustomAspectThrowCheckedException 메소드에서 checked exception을 던진 것이라 책임을 전가할 수 있습니다. 사실 해당 메소드에서는 아무런 에러가 발생하지 않았는데 말이죠.&lt;br /&gt;&lt;br /&gt;즉, 쉽게 말해 실제 호출한 메소드는 잘못한 것이 없고 뒤에 숨어있는 AOP라는 친구가 잘못한 것인데 호출한 쪽에서는 호출한 메소드한테 뭐라고 하는 억울한 상황(?)이라고 할 수 있을 것 같네요.&lt;br /&gt;&lt;br /&gt;Java에서는 이것을 명확하게 구분하기 위해 위의 설명에 나와있듯이 &lt;b&gt;UndeclaredThrowableException&lt;/b&gt;으로 AOP에서 발생한 checked exception을 한 번 감싸서(wrapping) 던집니다. 그리고 UndeclaredThrowableException는 &lt;b&gt;RuntimeException&lt;/b&gt;을 상속받은 클래스입니다. &lt;u&gt;테스트코드에서 실행결과 둘 다 RuntimeException으로 떨어진다는 것&lt;/u&gt;이 이제야 납득이 되네요.&lt;/p&gt;
&lt;div&gt;&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3-2. AOP 순서(Order)에 따른 롤백여부&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 &lt;b&gt;@Transactional&lt;/b&gt;하고 같이 사용된다면 AOP에서 발생한 exception(unchecked, checked Exception 둘 다)의 종류와 상관없이 모두 롤백이 됩니다. 본래 checked exception은 롤백 마크 대상이 아니지만 위에서 확인했듯이 checked exception은 RuntimeException으로 감싸져서 throw되기 때문입니다.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;하지만 특이한 점이 있습니다. AOP의 우선순위를 최상위로 설정하고 롤백테스트를 하면 어떻게 되는지 보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1688082914905&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Slf4j
@Aspect
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class CustomAspect {

    @Around(&quot;@annotation(io.beaniejoy.springdatajpa.common.CustomAnnotation2)&quot;)
    public Object testAround2(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info(&quot;&amp;gt;&amp;gt;&amp;gt; TestAspect START&quot;);
        Object result = joinPoint.proceed();
        log.info(&quot;&amp;gt;&amp;gt;&amp;gt; TestAspect END&quot;);

        throw new IOException(&quot;aop exception&quot;);

//        return result;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 Aspect 클래스 차원에서 &lt;b&gt;@Order(Ordered.HIGHEST_PRECEDENCE)&lt;/b&gt;를 적용합니다. 말 그대로 최우선 순위로 설정하겠다는 것인데 이렇게 되면 @Transactional 보다 먼저 해당 AOP가 수행하게 됩니다. (@Transactional도 AOP)&lt;br /&gt;&lt;br /&gt;이 상황에서 한 번 롤백 테스트를 해보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1688082828625&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
public void child_with_tx_REQUIRED_and_custom_aspect_exception_test() {
    // checked exception 에도 롤백이 되어야 할 것 같은데 커밋이 된다??
    assertThrows(UndeclaredThrowableException.class, () -&amp;gt; {
        childService.saveWithCustomAspectThrowCheckedException();
    });

    List&amp;lt;Cafe&amp;gt; cafes = cafeService.getAllCafes();
    
    // 테스트를 위해 기본으로 insert한 3개의 데이터 + childService에서 insert 커밋된 1개의 데이터
    // = 4개
    assertEquals(4, cafes.size());
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예상대로 UndeclaredThrowableException이 발생했습니다. 그런데 결과를 보면 테스트 수행 전에 DB에 저장했던 3개의 테스트 데이터를 제외하고 &lt;u&gt;AOP가 적용된 ChildService의 메소드에서 insert가 커밋된 것을 확인할 수 있습니다.&lt;/u&gt; &lt;br /&gt;RuntimeException이기 때문에 본래 롤백이 되어야 하는데 커밋이 된 것입니다. 어떻게 된 것일까요. 해당 메소드에 적용된 AOP 실행 순서를 고려하면 납득이 됩니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;CustomAOP &amp;gt; @Transactional(tx 시작) &amp;gt; &lt;b&gt;childService save(insert 수행)&lt;/b&gt; &amp;gt; &lt;b&gt;@Transactional(commit)&lt;/b&gt; &amp;gt; CustomAOP(&lt;b&gt;exception 발생&lt;/b&gt;)&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CustomAOP를 가장 먼저 실행되도록 설정했기 때문에 @Transactional 보다 먼저 수행됩니다. childService에서 insert를 수행하고 메소드가 끝났을 때는 @Transactional이 먼저 수행됩니다.(끝마치고 나갈 때는 Filter와 같이 역순으로 수행됩니다.) &lt;br /&gt;이 때 commit이 먼저 이루어지게 되고 그 다음 CustomAOP에서 exception이 발생하게 됩니다. &lt;b&gt;&lt;u&gt;커밋이 이루어지고 난 다음에 RuntimeException이 발생했기 때문에 롤백이 되지 않고 커밋이 되었다고 볼 수 있습니다.&lt;/u&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2023-06-30 at 9.08.16 AM.png&quot; data-origin-width=&quot;1307&quot; data-origin-height=&quot;342&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/q00L3/btslXmf4F4l/JthS079dN3sf6IaNMqRtC0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/q00L3/btslXmf4F4l/JthS079dN3sf6IaNMqRtC0/img.png&quot; data-alt=&quot;CustomAOP &amp;amp;gt; @Order(Ordered.HIGHEST_PRECEDENCE) 적용하지 않았을 때&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/q00L3/btslXmf4F4l/JthS079dN3sf6IaNMqRtC0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fq00L3%2FbtslXmf4F4l%2FJthS079dN3sf6IaNMqRtC0%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;760&quot; height=&quot;199&quot; data-filename=&quot;Screenshot 2023-06-30 at 9.08.16 AM.png&quot; data-origin-width=&quot;1307&quot; data-origin-height=&quot;342&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;CustomAOP &amp;gt; @Order(Ordered.HIGHEST_PRECEDENCE) 적용하지 않았을 때&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2023-06-30 at 9.12.23 AM.png&quot; data-origin-width=&quot;1296&quot; data-origin-height=&quot;344&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dQ51os/btslR6y3dJt/6hqmsRUwo751JXkJbVZLD0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dQ51os/btslR6y3dJt/6hqmsRUwo751JXkJbVZLD0/img.png&quot; data-alt=&quot;CustomAOP &amp;amp;gt; @Order(Ordered.HIGHEST_PRECEDENCE) 적용&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dQ51os/btslR6y3dJt/6hqmsRUwo751JXkJbVZLD0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdQ51os%2FbtslR6y3dJt%2F6hqmsRUwo751JXkJbVZLD0%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;760&quot; height=&quot;202&quot; data-filename=&quot;Screenshot 2023-06-30 at 9.12.23 AM.png&quot; data-origin-width=&quot;1296&quot; data-origin-height=&quot;344&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;CustomAOP &amp;gt; @Order(Ordered.HIGHEST_PRECEDENCE) 적용&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;div&gt;&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&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;b&gt;  4. 마무리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에서도 CustomAOP 사용을 아주 많이 하게 됩니다. 주로 Redis 관련한 내용이 많은데요. CustomAOP에서 예외가 발생했을 때 &lt;b&gt;@Transactional&lt;/b&gt;하고 같이 사용하는 상황에서는 트랜잭션이 어떻게 처리가 되는지에 대한 궁금증에서 시작이 되었습니다.&lt;br /&gt;&lt;br /&gt;위의 정리한 내용 말고도 아주 다양한 상황들이 존재하는데요. 핵심만 잘 기억한다면 CustomAOP와 트랜잭션 처리에 대해 어려움을 겪지 않으실 거라 생각합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;@Transactional에는 다양한 propagation 설정들이 존재(우선 두 개만 기억)&lt;br /&gt;- &lt;b&gt;REQUIRED&lt;/b&gt;: 이전에 트랜잭션 존재시 그대로 사용, 없다면 새로 시작하는 전략&lt;br /&gt;- &lt;b&gt;REQUIRES_NEW&lt;/b&gt;: 이전의 트랜잭션 존재 유무 상관없이 새로운 트랜잭션 생성하는 전략&lt;/li&gt;
&lt;li&gt;&lt;b&gt;REQUIRES_NEW&lt;/b&gt;에도 호출하는 쪽(ParentService)에서 예외 처리(try catch)하지 않으면 같이 롤백이 되기에 주의&lt;/li&gt;
&lt;li&gt;Spring의 @Transactional rollback 대상은 default로 &lt;b&gt;RuntimeException&lt;/b&gt;이다.&lt;br /&gt;즉 @Transactional를 기본으로 사용하면 checked exception에 대해서는 롤백이 되지 않고 커밋됨&lt;/li&gt;
&lt;li&gt;CustomAOP에서 발생한 Exception은 종류 상관없이 RuntimeException으로 throwing된다.&lt;br /&gt;(&lt;b&gt;UndeclaredThrowableException&lt;/b&gt; 참고)&lt;/li&gt;
&lt;li&gt;CustomAOP의 Order에 따라 @Transactional하고 같이 사용하는 상황에서 커밋이 될 수도, 안 될 수도 있다.&lt;br /&gt;(게시글 참고)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요정도로 요약할 수 있겠네요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;틀린 내용이 있을 수 있습니다. 피드백 언제나 환영합니다. 감사합니다.&lt;/blockquote&gt;</description>
      <category>Spring/JPA</category>
      <category>AOP</category>
      <category>Rollback</category>
      <category>Spring</category>
      <category>transaction</category>
      <author>beaniejoy</author>
      <guid isPermaLink="true">https://beaniejoy.tistory.com/99</guid>
      <comments>https://beaniejoy.tistory.com/99#entry99comment</comments>
      <pubDate>Fri, 30 Jun 2023 09:32:04 +0900</pubDate>
    </item>
    <item>
      <title>Jenkins의 flywayrunner plugin을 통해 DB migration 자동화하기(jenkins pipeline)</title>
      <link>https://beaniejoy.tistory.com/98</link>
      <description>&lt;blockquote data-ke-style=&quot;style3&quot;&gt;목표&lt;br /&gt;- DB migration 용도의 Jenkinsfile 작성해보기&lt;br /&gt;- Jenkins pipeline을 통해 flyway migration 자동화 적용&lt;br /&gt;- Jenkins에서 item에 버튼 하나 누르면 알아서 migration 작업 진행하도록 적용&lt;br /&gt;- DB 테이블에 대한 migration 이력을 프로젝트에서 파일(.sql)로 관리 가능&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 글을 보시기 전에 Jenkins 서버 설치가 되어있어야 합니다. Jenkins 설치 방법은 여러 블로그 글에 자세하고 친절하게 소개를 하고 계시더라구요. (&lt;s&gt;아니면 ChatGPT로,,,&lt;/s&gt;)&lt;br /&gt;AWS 클라우드 서버 내 Jenkins 서버 설치에 대해서 이전에 제가 작성했던 글도 있는데요. 허접하지만 이거 참고하셔도 좋습니다.&lt;br /&gt;(&lt;a href=&quot;https://beaniejoy.tistory.com/95&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://beaniejoy.tistory.com/95&lt;/a&gt;)&lt;/p&gt;
&lt;figure id=&quot;og_1684598317129&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Jenkins] Lightsail에 Jenkins server 구축해보고 Spring project 빌드해보기&quot; data-og-description=&quot;목적 - AWS Lightsail을 통해 생성한 instance에 jenkins server 구축한 내용 기록용 목표 - AWS Lightsail에 띄운 instance에 jenkins server 띄우기 - jenkins server 기본적인 설정 - jenkins item 생성 후 spring project build 해&quot; data-og-host=&quot;beaniejoy.tistory.com&quot; data-og-source-url=&quot;https://beaniejoy.tistory.com/95&quot; data-og-url=&quot;https://beaniejoy.tistory.com/95&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/fOjBm/hySGcAGde4/HYVDDmLuhbE3txzlqbXGmK/img.png?width=676&amp;amp;height=411&amp;amp;face=318_36_400_125,https://scrap.kakaocdn.net/dn/dbzCOW/hySF0tsMQs/Gnbvf7HtZEnfEr1tmNnM7k/img.png?width=676&amp;amp;height=411&amp;amp;face=318_36_400_125,https://scrap.kakaocdn.net/dn/bWB8Yq/hySG415SdK/t5oYRr6P0WzWHxxFrSvGYk/img.png?width=1980&amp;amp;height=1366&amp;amp;face=0_0_1980_1366&quot;&gt;&lt;a href=&quot;https://beaniejoy.tistory.com/95&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://beaniejoy.tistory.com/95&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/fOjBm/hySGcAGde4/HYVDDmLuhbE3txzlqbXGmK/img.png?width=676&amp;amp;height=411&amp;amp;face=318_36_400_125,https://scrap.kakaocdn.net/dn/dbzCOW/hySF0tsMQs/Gnbvf7HtZEnfEr1tmNnM7k/img.png?width=676&amp;amp;height=411&amp;amp;face=318_36_400_125,https://scrap.kakaocdn.net/dn/bWB8Yq/hySG415SdK/t5oYRr6P0WzWHxxFrSvGYk/img.png?width=1980&amp;amp;height=1366&amp;amp;face=0_0_1980_1366');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[Jenkins] Lightsail에 Jenkins server 구축해보고 Spring project 빌드해보기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;목적 - AWS Lightsail을 통해 생성한 instance에 jenkins server 구축한 내용 기록용 목표 - AWS Lightsail에 띄운 instance에 jenkins server 띄우기 - jenkins server 기본적인 설정 - jenkins item 생성 후 spring project build 해&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;beaniejoy.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 저는 개인 프로젝트를 위해 Jenkins 서버를 처음에 AWS에서 설치하고 CI/CD 프로세스까지 적용해보았는데요. &lt;br /&gt;슬프게도 돈이 없는 저는 매달 3~4만원의 비용이 크게 작용해서 local에서 Virtual Box로 CentOS를 설치한 다음 거기에 Jenkins 서버를 설치했습니다 ㅜㅜ&lt;br /&gt;&lt;br /&gt;어떠한 방식이든 좋으니 Jenkins 서버를 설치하시기만 하면 됩니다.&lt;br /&gt;&lt;br /&gt;이번 게시글을 통해 최종적으로 적용된 프로젝트 구조는 다음과 같습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1684681345888&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;dongne-cafe-api
 ├ ...
 ├ ⭐️db/				&amp;gt; flyway migration 관련 내용 관리
    ├ migration/		&amp;gt; migration 적용될 sql 파일 관리
    ├ seed/			&amp;gt; seed data insert를 위한 repeatable migration 관리
    ├ flyway-local.conf	&amp;gt; local profile용 flyway config 파일
    ├ flyway-prod.conf	&amp;gt; prod profile(배포)용 flyway config 파일
    └ ...
 ├ dongne-account-api/
 ├ dongne-common/
 ├ dongne-service-api/
 ├ ...
 ├ scripts/	&amp;gt; Jenkins 배포 관련 ansible &amp;amp; bash script 관리
 ├ ...
 ├ ⭐️Jenkinsfile-db-migration	&amp;gt; &quot;db migration&quot;(flyway)용 Jenkins pipeline
 ├ Jenkinsfile-service		&amp;gt; &quot;service-api&quot; build 및 deploy 용 Jenkins pipeline
 └ ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;db 디렉토리와 Jenkinsfile-db-migration이 이번 게시글에서 살펴볼 내용입니다.&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;b&gt;  1.  DB migration 작업을 애플리케이션 CI/CD pipeline과 분리한 이유가 무엇인가요?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Jenkins에 flyway runner plugin을 적용해보기 전에 바로 위에 언급된 프로젝트 구조를 한 번 살펴보겠습니다.&lt;br /&gt;&lt;br /&gt;살펴보면 Jenkins pipeline을 두 개 적용한 것이 있는데요. DB migration(Jenkinsfile-db-migration)용도와 service-api CI/CD용도(Jenkinsfile-service-api) 두 가지의 pipeline이 있습니다.&lt;br /&gt;&lt;br /&gt;생각해보면 두 개의 pipeline으로 구성하기보다 CI/CD 과정에 DB migration stage를 포함해서 한꺼번에 적용하는 것이 더 좋지 않겠나는 궁금증도 있을 수 있습니다.&lt;br /&gt;&lt;br /&gt;하지만 DB migration은 애플리케이션 CI/CD와 분리되어야 하는 이유는 명확합니다. &lt;b&gt;성격이 다르고 둘다 동시에 진행하는 것은 큰 리스크가 있다고 생각했습니다.&lt;/b&gt; CI/CD는 애플리케이션 빌드와 배포 프로세스 자체에 의미를 두고 있고 애플리케이션 코드와 관련이 있습니다. DB table 구조와 데이터에 대한 이력을 관리하고 적용하는 DB migration과 애플리케이션 코드는 성격 자체가 서로 맞질 않다고 생각했습니다.&lt;br /&gt;&lt;br /&gt;또한 CI/CD 안에 DB migration 과정을 포함하게 되면 예기치 않은 위험한 상황에 놓일 수 있습니다. DB migration위한 sql을 작성하는 것은 결국 개발자인데 테이블 구조를 잘못 수정하였거나 잘못된 데이터를 기입할 수 있습니다. CI/CD pipeline 실행 과정에서 DB migration 수행시 version 체크와 sql 문법 오류 같은 부분만 체크하게 되는데, 잘못 수정된 내용에 대해서는 검출을 하지 못할 수 있습니다. &lt;br /&gt;이렇게 되면 개발자 입장에서 실환경 배포를 나가는데 잘못된 DB 정보에 대해서 인지하지 못하는 것이고 그대로 적용이 되어 심각한 오류 상황을 마주하게 될 수 있습니다.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;이러한 이유들로 DB migration 과정을 따로 구별된 pipeline으로 구성하였습니다. &lt;br /&gt;이를 통해 DB 테이블 구조 변경이 필요할 때 Jenkins에서 해당 pipeline item으로 먼저 migration 작업을 진행하고 실환경 나가기 전에 제대로 DB 수정이 이루어졌는지에 대해서 충분히 확인 및 검토할 수 있을 것이라 생각했습니다.&lt;/p&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&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;b&gt;  2. Jenkins에서 flyway runner plugin 설치 및 기본 설정&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 본격적으로 Jenkins에 flyway runner plugin를 적용해보겠습니다.&lt;br /&gt;&lt;br /&gt;먼저 flyway runner plugin을 설치해보려합니다. &lt;br /&gt;&lt;b&gt;Jenkins에 접속 &amp;gt; Jenkins 관리 &amp;gt; 플러그인 관리 &amp;gt; Available Plugins &amp;gt; flyway 검색&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;위의 과정을 통해 flyway 검색하면 flyway runner plugin이 나올 것입니다. 바로 restart 없이 install 진행 버튼을 클릭합니다.&lt;br /&gt;설치 진행이 완료되면 다음과 같이 적용된 것을 확인 할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2023-05-22 at 12.44.20 AM.png&quot; data-origin-width=&quot;941&quot; data-origin-height=&quot;380&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bHt2fc/btsgEdclJZ6/RnKPdizktAGfr1BtLJwGtK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bHt2fc/btsgEdclJZ6/RnKPdizktAGfr1BtLJwGtK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bHt2fc/btsgEdclJZ6/RnKPdizktAGfr1BtLJwGtK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbHt2fc%2FbtsgEdclJZ6%2FRnKPdizktAGfr1BtLJwGtK%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;760&quot; height=&quot;307&quot; data-filename=&quot;Screenshot 2023-05-22 at 12.44.20 AM.png&quot; data-origin-width=&quot;941&quot; data-origin-height=&quot;380&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FlywayRunner 플러그인 설치 이후 &lt;b&gt;Jenkins 관리 &amp;gt; Global Tool Configuration&lt;/b&gt;에 들어갑니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2023-05-22 at 12.46.33 AM.png&quot; data-origin-width=&quot;1311&quot; data-origin-height=&quot;796&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oFCk2/btsgEf9xoru/cPvdKkD1RqL7vlMTcw8DK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oFCk2/btsgEf9xoru/cPvdKkD1RqL7vlMTcw8DK0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oFCk2/btsgEf9xoru/cPvdKkD1RqL7vlMTcw8DK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoFCk2%2FbtsgEf9xoru%2FcPvdKkD1RqL7vlMTcw8DK0%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;760&quot; height=&quot;461&quot; data-filename=&quot;Screenshot 2023-05-22 at 12.46.33 AM.png&quot; data-origin-width=&quot;1311&quot; data-origin-height=&quot;796&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Tool 설정에 들어가서 flyway 모듈 설정을 위와 같이 하면 됩니다. Version 같은 경우 사용하고 싶은 버전을 Jenkins가 설치된 OS에 따라서 적절하게 설정하면 됩니다.&lt;br /&gt;&lt;br /&gt;여기까지 완료되면 기본적인 flyway runner plugin 설치 및 설정은 완료입니다.&lt;/p&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&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;b&gt;  3. Jenkins Pipeline 적용하기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;  3-1. item 생성&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2023-05-22 at 12.50.21 AM.png&quot; data-origin-width=&quot;1115&quot; data-origin-height=&quot;871&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bc8zvW/btsgUtEAuSA/Uc1REzQ5UIANoPWgBkWIGk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bc8zvW/btsgUtEAuSA/Uc1REzQ5UIANoPWgBkWIGk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bc8zvW/btsgUtEAuSA/Uc1REzQ5UIANoPWgBkWIGk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbc8zvW%2FbtsgUtEAuSA%2FUc1REzQ5UIANoPWgBkWIGk%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;760&quot; height=&quot;594&quot; data-filename=&quot;Screenshot 2023-05-22 at 12.50.21 AM.png&quot; data-origin-width=&quot;1115&quot; data-origin-height=&quot;871&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB migration용 Jenkins pipline을 적용하기 위해 새로운 item 생성을 해줍시다. 저 같은 경우는 &quot;dongne-db-migration&quot; 이름으로 이미 생성된 pipeline item이 있기 때문에 위와 같이 빨간색 주의문구가 나오고 있습니다. 적절한 이름으로 생성하시면 됩니다. 생성하실 때 item 종류로 Pipeline으로 체크하고 생성하시면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;  3-2. item 기본 설정&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2023-05-22 at 12.54.18 AM.png&quot; data-origin-width=&quot;1014&quot; data-origin-height=&quot;832&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c1V3VN/btsgELGGYKw/7kPebieVlKYnSEWwxnSLxk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c1V3VN/btsgELGGYKw/7kPebieVlKYnSEWwxnSLxk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c1V3VN/btsgELGGYKw/7kPebieVlKYnSEWwxnSLxk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc1V3VN%2FbtsgELGGYKw%2F7kPebieVlKYnSEWwxnSLxk%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;760&quot; height=&quot;624&quot; data-filename=&quot;Screenshot 2023-05-22 at 12.54.18 AM.png&quot; data-origin-width=&quot;1014&quot; data-origin-height=&quot;832&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성한 item에 대해서 기본 설정을 진행해줍시다.&lt;br /&gt;Do not allow concurrent builds: 같은 item에 대해 동시에 여러 번 build를 진행하지 못하도록 방지&lt;br /&gt;오래된 빌드 삭제: 5개 정도로 해서 보관할 최대갯수를 설정해줍시다. (각자 원하는 방향으로 설정)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2023-05-22 at 12.56.41 AM.png&quot; data-origin-width=&quot;1137&quot; data-origin-height=&quot;569&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QcWa0/btsgC4fV1tI/Y8Pw76Q0KXTO4PUGHnTgz0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QcWa0/btsgC4fV1tI/Y8Pw76Q0KXTO4PUGHnTgz0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QcWa0/btsgC4fV1tI/Y8Pw76Q0KXTO4PUGHnTgz0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQcWa0%2FbtsgC4fV1tI%2FY8Pw76Q0KXTO4PUGHnTgz0%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;760&quot; height=&quot;380&quot; data-filename=&quot;Screenshot 2023-05-22 at 12.56.41 AM.png&quot; data-origin-width=&quot;1137&quot; data-origin-height=&quot;569&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매개변수(parameter)로 branch 이름의 파라미터를 설정해줍니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2023-05-22 at 12.58.03 AM.png&quot; data-origin-width=&quot;1204&quot; data-origin-height=&quot;799&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zJXD6/btsgDM7aVda/kJSy5RF8lgrDtCfyNHjyV0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zJXD6/btsgDM7aVda/kJSy5RF8lgrDtCfyNHjyV0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zJXD6/btsgDM7aVda/kJSy5RF8lgrDtCfyNHjyV0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzJXD6%2FbtsgDM7aVda%2FkJSy5RF8lgrDtCfyNHjyV0%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;760&quot; height=&quot;504&quot; data-filename=&quot;Screenshot 2023-05-22 at 12.58.03 AM.png&quot; data-origin-width=&quot;1204&quot; data-origin-height=&quot;799&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pipeline script를 어디껄로 참조할 것인지에 대한 설정입니다. git을 통해 가져온 repository 내에 있는 script 파일을 참조하고자 합니다. repository의 url 설정해주고 credentials 정보를 설정해줍니다. &lt;br /&gt;(여기서는 git repository 자체가 public 성격이라 따로 credentials 내용을 설정하지 않았습니다. 하지만 보통 실무에서는 private repository나 github enterprise 같은 구축형 플랫폼을 통해서 관리되기 때문에 거의 필수로 설정하고 있습니다.)&lt;br /&gt;&lt;br /&gt;repository 설정 이후 어느 특정 branch에서 가져올지에 대해서도 설정해줍니다. 위에서 적용했던 &lt;b&gt;branch parameter&lt;/b&gt;에서 빌드시 입력한 브랜치에서 pipeline script를 가져오겠다는 의미입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2023-05-22 at 3.54.16 PM.png&quot; data-origin-width=&quot;735&quot; data-origin-height=&quot;378&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bqsrUd/btsgG5MIA1G/v3WvF6pGwH8Ggtj2HA5fJK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bqsrUd/btsgG5MIA1G/v3WvF6pGwH8Ggtj2HA5fJK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bqsrUd/btsgG5MIA1G/v3WvF6pGwH8Ggtj2HA5fJK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbqsrUd%2FbtsgG5MIA1G%2Fv3WvF6pGwH8Ggtj2HA5fJK%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;760&quot; height=&quot;391&quot; data-filename=&quot;Screenshot 2023-05-22 at 3.54.16 PM.png&quot; data-origin-width=&quot;735&quot; data-origin-height=&quot;378&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Script Path는 pipeline 빌드할 script의 위치를 설정하는 것인데요. 저는 Jenkinsfile script 파일 위치가 repository root path에 있기 때문에 단순히 파일명만 입력하였습니다.&lt;br /&gt;&lt;br /&gt;여기까지 하면 pipeline을 build 해볼 수 있는 item 기본 설정까지 완료입니다.&lt;/p&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&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;b&gt;&lt;b&gt;  &lt;/b&gt;4. pipeline script 작성하기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 pipeline을 작동시킬 script를 작성해보겠습니다.&lt;br /&gt;&lt;br /&gt;stage를 크게 4단계로 잡았습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Init&lt;/b&gt;&lt;br /&gt;- 기본 설정 내용 및 환경 변수 내용 확인&lt;br /&gt;- DB Connection 정보 가져오기(username, password)&lt;br /&gt;- 필요한 정보에 대한 변수 설정&lt;/li&gt;
&lt;li&gt;&lt;b&gt;DB Version Info&lt;/b&gt;&lt;br /&gt;- flyway &lt;b&gt;info&lt;/b&gt;&amp;nbsp;명령 수행&lt;br /&gt;- target이 되는 DB에 적용된 migration version 정보를 테이블 형태로 보여줌&lt;/li&gt;
&lt;li&gt;&lt;b&gt;DB Migrate&lt;/b&gt;&lt;br /&gt;- flyway &lt;b&gt;migrate&lt;/b&gt; 명령 수행&lt;br /&gt;- 테이블에 대한 DDL 정보들을 담은 migration용 sql 파일을 참고하여 실제 migrate 수행&lt;/li&gt;
&lt;li&gt;&lt;b&gt;DB Validate&lt;/b&gt;&lt;br /&gt;- flyway validate 명령 수행&lt;br /&gt;- 프로젝트에서 지정한 migration sql 내용들과 DB에 적용된 migration 정보를 비교해 불일치 여부 체크&lt;br /&gt;- migration 수행한 이후 validate를 수행함으로써 migration이 성공적으로 잘 이루어졌는지 체크하기 위함&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;flyway 수행을 위해서 위에서 설치했던 FlywayRunner Jenkins 플러그인을 사용하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;  4-1. pipeline syntax를 통해 flywayrunner 플러그인 적용하기&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1684846683157&quot; class=&quot;scala&quot; data-ke-language=&quot;scala&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;enum Stage {
    ALL, INFO, MIGRATE, VALIDATE;
    Stage() {}
}

def executeConditionAllOr(stage) {
    return params.FLYWAY_COMMAND == Stage.ALL.name() || params.FLYWAY_COMMAND == stage.name()
}

// Run flyway runner Jenkins Plugin
def executeFlywayRunner(stage) {
    if (stage == Stage.ALL) {
        return
    }

    flywayrunner installationName: 'flywaytool-jenkins',
            flywayCommand: &quot;$stage&quot;.toLowerCase(),
            commandLineArgs: &quot;-configFiles=${MIGRATION_WORKSPACE}/flyway-${PROJECT_PROFILE}.conf&quot;,
            credentialsId: &quot;${DB_CONNECTION_CREDENTIAL}&quot;,
            url: &quot;jdbc:mysql://${DATABASE_HOST}:3306/dongne&quot;,
            locations: &quot;filesystem:${MIGRATION_WORKSPACE}/migration&quot;
}

pipeline {
	agent any
    
    //...
    
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Jenkinsfile은 groovy 문법으로 이루어져있습니다. 위의 코드는 pipeline에서 stage block을 구성하기 위한 공통 내용들을 메소드와 enum class로 먼저 정의를 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;executeFlywayRunner&quot; &lt;/b&gt;function은 각 stage별로 수행하는 flyway 명령을 실질적으로 flywayRunner 플러그인이 처리해주는 부분입니다.&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;installationName&lt;/b&gt;&lt;br /&gt;- 위에서 &lt;b&gt;Jenkins 관리 &amp;gt; Global Tool Configuration&lt;/b&gt;에서 설정했던 flyway installation 이름을 기입합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;flywayCommand&lt;/b&gt;&lt;br /&gt;- flyway를 통해 수행할 명령어를 기입합니다.&lt;br /&gt;- 여기서 적용된 flyway command는 &lt;b&gt;info, migrate, validate&lt;/b&gt; 세 가지입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;commandLineArgs&lt;/b&gt;&lt;br /&gt;- flyway command를 수행할 때 같이 적용할 args(명령어 옵션)내용을 기입합니다.&lt;br /&gt;- 여기서는 configFiles 옵션을 적용했습니다. flyway를 수행할 때 참고하는 설정 내용입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;credentialsId&lt;/b&gt;&lt;br /&gt;- Jenkins에서 DB Connection에 대한 정보를 저장한 credentials id값을 넣어줍니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;url&lt;/b&gt;&lt;br /&gt;- 연결할 DB url 내용을 기입합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;locations&lt;/b&gt;&lt;br /&gt;- flyway 명령을 수행할 때 기준이되는 migration 정보들을 담고있는 디렉토리 경로를 입력합니다.&lt;br /&gt;- filesystem으로 하고 Jenkins가 돌아가고 있는 서버에서 migration 정보가 있는 디렉토리의 절대경로를 기입했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 내용을 무작정 따라서 타입핑할 필요 전혀없습니다. Jenkins는 &lt;u&gt;&lt;b&gt;pipeline syntax&lt;/b&gt;&lt;/u&gt;라는 것을 제공해주는데 위와 같은 plugin들을 pipeline script에 적용하기 쉽게 일종의 템플릿을 제공해줍니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2023-05-23 at 10.11.04 PM.png&quot; data-origin-width=&quot;2260&quot; data-origin-height=&quot;1156&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wvjsL/btsg7IwCa9w/lKLKHUJ8R8n9TheflNXyQk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wvjsL/btsg7IwCa9w/lKLKHUJ8R8n9TheflNXyQk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wvjsL/btsg7IwCa9w/lKLKHUJ8R8n9TheflNXyQk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwvjsL%2Fbtsg7IwCa9w%2FlKLKHUJ8R8n9TheflNXyQk%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;760&quot; height=&quot;389&quot; data-filename=&quot;Screenshot 2023-05-23 at 10.11.04 PM.png&quot; data-origin-width=&quot;2260&quot; data-origin-height=&quot;1156&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2023-05-23 at 10.12.09 PM.png&quot; data-origin-width=&quot;3072&quot; data-origin-height=&quot;1494&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uMXAO/btsg9MLpscm/VfQFQiWmV5IHTAw8iMO8Zk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uMXAO/btsg9MLpscm/VfQFQiWmV5IHTAw8iMO8Zk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uMXAO/btsg9MLpscm/VfQFQiWmV5IHTAw8iMO8Zk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuMXAO%2Fbtsg9MLpscm%2FVfQFQiWmV5IHTAw8iMO8Zk%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;760&quot; height=&quot;370&quot; data-filename=&quot;Screenshot 2023-05-23 at 10.12.09 PM.png&quot; data-origin-width=&quot;3072&quot; data-origin-height=&quot;1494&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Pipeline Syntax에 들어가서 flywayrunner 플러그인을 선택하고 위의 항목대로 내용을 입력해줍니다.&lt;br /&gt;입력을 완료하면 &lt;b&gt;Generate Pipeline Script&lt;/b&gt; 버튼이 있는데요. 버튼을 클릭하면 위의 script 코드 처럼 flywayrunner pipeline용 script 완성본을 보여줍니다. 복사 붙여넣기해서 넣으면 됩니다.&lt;br /&gt;&lt;br /&gt;그리고 바로 위의 사진과 같이 Pipeline Syntax에서 flywayrunner script를 클릭하면 credentials 정보 기입하는 칸이 나오는데요. 아무것도 등록하지 않았다면 Add 버튼을 눌러 새로 등록하면 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2023-05-23 at 10.12.56 PM.png&quot; data-origin-width=&quot;3066&quot; data-origin-height=&quot;1636&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ods7R/btshc4RWEmI/gvcvaHhod9tqUi5lL9CQ3K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ods7R/btshc4RWEmI/gvcvaHhod9tqUi5lL9CQ3K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ods7R/btshc4RWEmI/gvcvaHhod9tqUi5lL9CQ3K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fods7R%2Fbtshc4RWEmI%2FgvcvaHhod9tqUi5lL9CQ3K%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;760&quot; height=&quot;406&quot; data-filename=&quot;Screenshot 2023-05-23 at 10.12.56 PM.png&quot; data-origin-width=&quot;3066&quot; data-origin-height=&quot;1636&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Add 버튼을 누르면 위와 같이 Credentials 정보 기입하는 항목들이 나오는데요. DB Connection을 위해 DB에 대한 username, password만 기입하셔도 됩니다.&amp;nbsp;&lt;/p&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;  4-2. pipeline stage 적용하기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 본격적으로 4단계의 stage를 적용해보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1684848658212&quot; class=&quot;scala&quot; data-ke-language=&quot;scala&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;pipeline {
    agent any

    // Disallow concurrent executions of the Pipeline.
    options { disableConcurrentBuilds() }

    parameters {
        // Choice of flyway target command
        choice(
                name: 'FLYWAY_COMMAND',
                choices: [&quot;${Stage.ALL}&quot;, &quot;${Stage.INFO}&quot;, &quot;${Stage.MIGRATE}&quot;, &quot;${Stage.VALIDATE}&quot;],
                description: 'target flyway command'
        )
    }
    
    //...
    
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 Jenkins에서 해당 pipeline이 동시에 build되지 않도록 options 부분에 disableConcurrentBuilds를 적용합니다.&lt;br /&gt;&lt;br /&gt;그다음 Jenkins parameter하나를 설정했는데요. pipeline build를 시작하기 전에 특정 stage만 실행하고 싶을 때를 위해 선택지를 만들어두었습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1684848931124&quot; class=&quot;scala&quot; data-ke-language=&quot;scala&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;pipeline {

    //...
    stages {
        stage('Init') {
            steps {
                script {
                    sh 'whoami'
                    sh 'printenv'

                    PROJECT_PROFILE = 'prod'

                    // DB Connection Credential
                    DB_CONNECTION_CREDENTIAL = 'b873f9bf-03cc-4daf-be4f-7e00194aa2a0'
                    withCredentials([
                            usernamePassword(
                                    credentialsId: &quot;${DB_CONNECTION_CREDENTIAL}&quot;,
                                    usernameVariable: 'username',
                                    passwordVariable: 'password'
                            )
                    ]) {
                        DATABASE_USERNAME = &quot;${username}&quot;
                        DATABASE_PASSWORD = &quot;${password}&quot;
                    }

                    // flyway migration directory
                    MIGRATION_WORKSPACE = &quot;${WORKSPACE}/db&quot;
                }
            }
        }

        stage('DB Version Info') {
            when {
                expression { executeConditionAllOr(Stage.INFO) }
            }
            steps { executeFlywayRunner(Stage.INFO) }
        }

        stage('DB Migrate') {
            when {
                expression { executeConditionAllOr(Stage.MIGRATE) }
            }
            steps { executeFlywayRunner(Stage.MIGRATE) }
        }

        stage('DB Validate') {
            when {
                expression { executeConditionAllOr(Stage.VALIDATE) }
            }
            steps { executeFlywayRunner(Stage.VALIDATE) }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 첫 단계인 Init에서 중요한 것은 Jenkins에 저장되어 있는 credentials 정보를 가져옵니다. DB Connection을 위한 username과 password를 가져오기 위함입니다. 이렇게 해야 github에 올릴 프로젝트의 pipeline script에서 DB connection 정보와 같은 민감한 데이터를 노출하지 않고 관리할 수 있습니다.&lt;br /&gt;&lt;br /&gt;그다음 &lt;b&gt;DB Info, Migrate, Validate&lt;/b&gt; stage를 차례대로 적용합니다. 위에서 만들어두었던 &lt;b&gt;executeFlywayRunner&lt;/b&gt; function을 그대로 활용하면 됩니다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;when&lt;/b&gt; 구절은 choice parameter(ALL, INFO, MIGRATE, VALIDATE)에서 선택한 값을 기준으로 특정 stage만 실행되도록 하는 실행조건절입니다.&lt;/p&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&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;b&gt;  5. 결론&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 내용은 참고만 하셔도 됩니다.&lt;br /&gt;&lt;br /&gt;중요한 것은 &lt;b&gt;애플리케이션을 실제 배포하기 전에 꼭 DB migration 내용이 잘 적용되었는지 체크하는 것이 중요한데요.&lt;/b&gt; &lt;br /&gt;작년에 실무에서 한 번 DDL 작업 요청을 깜빡하고 실환경 배포를 나간 적이 있었습니다. 실무에서는 실환경 배포 프로세스에서 DB 정보가 제대로 반영이 되었는지에 대해서 체크하지 않고 진행되기 때문에 누락된 내용을 아예 인지하지 못했습니다. &lt;br /&gt;그나마 다행히 어드민쪽 배포였고 그리고 베타환경에서 이를 감지해서 뒤늦게 DDL ALTER 쿼리를 신청해서 잘 처리했습니다. 이 때를 기점으로 꼼꼼하게 보는 습관을 들였고 또한 애플리케이션 배포 전에 DB에 대한 변경지점 체크의 중요성도 알게 되었습니다.&lt;br /&gt;&lt;br /&gt;단순 칼럼 추가와 같이 명확하게 구분이 되는 내용에 대해서는 애플리케이션 빌드와 실행시 JPA 같은 persistence 단에서 감지가 가능할 수도 있겠으나 이를 감지하지 못하고 배포가 완료가 되는 경우 사용자가 실제 서비스를 사용할 때 충분히 오류가 발생할 수도 있습니다.&lt;br /&gt;&lt;br /&gt;이러한 개발자의 깜빡(?)이슈같은 실수를 방지하고자 제 개인 프로젝트에서는 이러한 부분을 충분히 고려했고 flyway를 사용하기에 이르렀는데요. Jenkins를 이용해 CI/CD 프로세스를 만들면서 애플리케이션 빌드 전에 &lt;b&gt;DB migration 체크 과정&lt;/b&gt;을 넣었고 &lt;b&gt;DB migration 전용 pipeline을 따로 구성&lt;/b&gt;함으로써 개발자의 실수를 알아서 잡아주고 관리도 명확하게 할 수 있게끔 pipeline을 구성하게 되었습니다.&lt;br /&gt;(실무에서 flyway를 사용할 수 있으면 좋겠지만 그렇지 못해 아쉬운 마음을 제 개인 프로젝트에서나마 풀었네요,,, ㅎㅎ)&lt;br /&gt;&lt;br /&gt;이 글을 보시는 분들도 DB migration을 어떻게 하면 효율적으로 관리할 수 있는지에 대해 고민해보시면 좋을 것 같고 위의 허접한 내용보다 훨씬 더 좋은 방법이 분명 있을거라 생각합니다. 있으면 저한테도 공유해주시면 감사하겠습니다 ㅎㅎ&lt;br /&gt;&lt;br /&gt;위에 적용한 script 내용이 있는 &lt;a href=&quot;https://github.com/beaniejoy/dongne-cafe-api/pull/52/files#diff-9af49ac1cc084a720015bad88bf8b69e2a3a3a3229757edfad3d5919a006dc70&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;github repository 링크&lt;/a&gt; 남겨놓겠습니다.&lt;/p&gt;
&lt;figure id=&quot;og_1684850849292&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;Flyway migrate, validate 용도의 Jenkinsfile 구분 by beaniejoy &amp;middot; Pull Request #52 &amp;middot; beaniejoy/dongne-cafe-api&quot; data-og-description=&quot;&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/beaniejoy/dongne-cafe-api/pull/52/files#diff-9af49ac1cc084a720015bad88bf8b69e2a3a3a3229757edfad3d5919a006dc70&quot; data-og-url=&quot;https://github.com/beaniejoy/dongne-cafe-api/pull/52&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/crApRu/hySJmbDeoS/sW7tkyeSrxvY8J4xW4cjc1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/beaniejoy/dongne-cafe-api/pull/52/files#diff-9af49ac1cc084a720015bad88bf8b69e2a3a3a3229757edfad3d5919a006dc70&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/beaniejoy/dongne-cafe-api/pull/52/files#diff-9af49ac1cc084a720015bad88bf8b69e2a3a3a3229757edfad3d5919a006dc70&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/crApRu/hySJmbDeoS/sW7tkyeSrxvY8J4xW4cjc1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Flyway migrate, validate 용도의 Jenkinsfile 구분 by beaniejoy &amp;middot; Pull Request #52 &amp;middot; beaniejoy/dongne-cafe-api&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;틀린 내용에 대한 피드백 언제나 환영합니다. 긴 글 읽어주셔서 감사합니다.&lt;/blockquote&gt;</description>
      <category>Infra</category>
      <author>beaniejoy</author>
      <guid isPermaLink="true">https://beaniejoy.tistory.com/98</guid>
      <comments>https://beaniejoy.tistory.com/98#entry98comment</comments>
      <pubDate>Tue, 23 May 2023 23:20:48 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] HTTP Request, Response 내용을 logging 적용해보기</title>
      <link>https://beaniejoy.tistory.com/97</link>
      <description>&lt;blockquote data-ke-style=&quot;style3&quot;&gt;목표&lt;br /&gt;- Spring Boot Application에서 HTTP Request, Response 내용에 대해서 logging을 적용해본다.&lt;br /&gt;&lt;br /&gt;효과&lt;br /&gt;- logging 적용을 통해 HTTP 요청마다 요청 전문과 응답 전문 내용 확인이 가능해진다.&lt;br /&gt;- logging을 통해 오류에 대한 디버깅과 원인 추적을 더욱 쉽고 빠르게 할 수 있다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무든 개인 프로젝트든 애플리케이션을 개발하거나 운용할 때 여러 에러를 마주하게 됩니다. 에러를 마주하게 되면 원인을 알아야 해결방법을 생각할 수 있기 때문에 에러 발생 원인을 찾는 것이 아주 중요합니다.&lt;br /&gt;&lt;br /&gt;실제 업무하면서도 에러 발생 원인을 찾는데에 많은 시간을 할애하게 됩니다. 여러 단서들을 이곳 저곳에서 확인하고 원인을 추적해나가는데요. http request, response 내용도 중요한 단서가 됩니다. 이번 게시글에서는 단서가 되는 request, response 내용이 무엇이 있는지, 그 내용들을 가지고 어떻게 logging을 적용할 것인지, 적용하는 데 있어서 필요한 설정이 무엇이 있을지에 대해서 정리해보고자 합니다.&lt;/p&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&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;b&gt;  1. Logging할 내용 목록화하기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;request, response 내용을 로깅할 때 어떤 것을 로그에 남길 것인지를 목록화하면 좋습니다.&lt;br /&gt;웹서비스는 거의 HTTP 프로토콜로 움직이기 때문에 HTTP 프로토콜에서 중요한 요소들을 뽑아보았습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;request&lt;/b&gt;&lt;br /&gt;- &lt;b&gt;http method&lt;/b&gt;: GET, POST, PUT, PATCH, DELETE 등...&lt;br /&gt;- &lt;b&gt;request uri&lt;/b&gt;: 요청 api에 대한 정보(ex. /api/cafes)&lt;br /&gt;- &lt;b&gt;client ip&lt;/b&gt;: 요청한 client의 ip 주소&lt;br /&gt;- &lt;b&gt;headers&lt;/b&gt;: 요청 내용에 포함된 http header 값들(ex. content-type:application/json)&lt;br /&gt;- &lt;b&gt;request param&lt;/b&gt;: 요청시 uri에 포함되어 있는 query string 내용&lt;br /&gt;- &lt;b&gt;request body&lt;/b&gt;: 요청 본문(body)에 해당하는 내용&lt;/li&gt;
&lt;li&gt;&lt;b&gt;response&lt;/b&gt;&lt;br /&gt;- &lt;b&gt;http status code&lt;/b&gt;: http 응답에 대한 응답코드(200, 400, 401, 500 등...)&lt;br /&gt;- &lt;b&gt;response body&lt;/b&gt;: 응답 본문(body)에 해당하는 내용&lt;/li&gt;
&lt;li&gt;&lt;b&gt;elasped time&lt;/b&gt;: 요청 ~ 응답 처리하는데 걸린 시간&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1682765648815&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;data class HttpLogMessage(
    val httpMethod: String,
    val requestUri: String,
    val httpStatus: HttpStatus,
    val clientIp: String,
    val elapsedTime: Double,
    val headers: String?,
    val requestParam: String?,
    val requestBody: String?,
    val responseBody: String?,
) {
//...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로깅의 대상이 되는 http 프로토콜 관련 정보들을 Spring Application에 사용하기 위해 위와 같이 하나의 클래스로 정의하는 것이 좋은 것 같습니다. (이부분은 아래에서 다시 언급하도록 하겠습니다.)&lt;/p&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&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;b&gt;  2. Filter 구성하기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;본격적으로 logging 처리할 Filter를 구성해보려고 합니다. &lt;br /&gt;그 전에 왜 Filter에서 구현하려고 하는지에 대해서 나름의 이유를 정리해보았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;  2-1. Filter에서 구현하는 이유&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;항상 개발할 때는 어떤 방식으로 구현할 것인지에 대한 &lt;b&gt;선택과 이유가 명확하면 좋습니다.&lt;/b&gt; &lt;br /&gt;logging을 구현하기 위해서 생각해볼 수 있는 구간이 filter와 interceptor가 있을 것 같은데요. 이 중 filter를 선택했습니다. 이유는 대부분의 프로젝트에서 filter로 logging을 처리하고 있기 때문입니다라고 하면 사실 공부하는 입장에서 별로 좋지 않은 접근이라고 생각합니다. 다른 사람들의 코드를 그냥 무지성으로 따라한 느낌이 강하기 때문이죠.&lt;br /&gt;&lt;br /&gt;실제로 대부분의 logging 처리를 filter에서 구현하고 있고 저희 실무에서조차 filter에서 구현하고 있습니다. 왜 그런지에 대해서 생각해보면 이유를 filter와 interceptor의 구조 차이에서 찾아볼 수 있을 것 같습니다.&lt;br /&gt;&lt;a href=&quot;https://www.baeldung.com/spring-mvc-handlerinterceptor-vs-filter&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://www.baeldung.com/spring-mvc-handlerinterceptor-vs-filter&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(구글링을 조금만 해봐도 filter와 interceptor의 차이에 대해서 설명한 글들이 많습니다. 먼저 차이점에 대해서 찾아보시고 이 글을 이어가시면 좋을 것 같습니다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제가 생각한 logging을 filter에서 구현하는 이유는 다음과 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Filter 단계에서 가장 먼저 request 내용을 받고, response 내보낼 때 가장 마지막에서 처리&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;request, response logging은 client에서 보낸 요청과 client로 보낼 응답 내용을 날 것(?) 그 자체로 확인하는 것이 좋기 때문에 client 요청 이후 가장 먼저 받아볼 수 있는 구간에서 request 내용을 logging하는 것이 좋고, client에 최종 응답을 보내기 직전인 구간에서 response 내용을 logging 하는 것이 좋다고 생각했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Filter 단계에서는 요청, 응답 header의 조작이 가능합니다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Filter 단계에서는 request, response header에 대한 내용들을 충분히 조작할 수 있습니다. logging의 목적은 client의 요청내용과 최종 응답내용에 대해서 그 자체로 확인하고자 하는 것입니다. 그렇기 때문에 interceptor보다 Filter에서 처리함으로써 client로부터 온 request header 그대로를 확인해볼 수 있고 client에 최종 응답을 보내기 직전에 최종 response 내용을 확인할 수 있습니다. (물론 LoggingFilter에 대해서 우선순위를 최상위로 설정해야할 것입니다. 이부분도 아래 코드 구현부분에서 살펴보겠습니다.)&lt;/p&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;  2-2. 기본적인 Logging Filter 구현&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1683283030807&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
class ReqResLoggingFilter : OncePerRequestFilter() {
    private val log = KotlinLogging.logger {}

    override fun doFilterInternal(
        request: HttpServletRequest,
        response: HttpServletResponse,
        filterChain: FilterChain,
    ) {
        val cachingRequestWrapper = ContentCachingRequestWrapper(request)
        val cachingResponseWrapper = ContentCachingResponseWrapper(response)

        val startTime = System.currentTimeMillis()
        filterChain.doFilter(cachingRequestWrapper, cachingResponseWrapper)
        val end = System.currentTimeMillis()

        try {
            log.info {
                HttpLogMessage.createInstance(
                    requestWrapper = cachingRequestWrapper,
                    responseWrapper = cachingResponseWrapper,
                    elapsedTime = (end - startTime) / 1000.0
                ).toPrettierLog()
            }

            cachingResponseWrapper.copyBodyToResponse()
        } catch (e: Exception) {
            log.error(e) { &quot;[${this::class.simpleName}] Logging 실패&quot; }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot에서 Filter를 적용하는 방법에는 여러가지가 있는데요. Filter도 Spring Bean 등록을 통해 적용이 가능해지면서 저도 Bean 등록 방식으로 구현했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;OncePerRequestFilter&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;OncePerRequestFilter&lt;/b&gt;를 구현했는데요. 이부분에 대한 설명도 상당히 길어지기 때문에 왜 이 Filter를 구현했는지 찾아보시면 좋을 것 같습니다.  forwarding 등 여러 이유들로 하나의 요청에 Filter가 불필요하게 두 번 호출되는 경우가 존재하는데요. 이러한 중복 호출을 방지하고자 사용되는 Filter가 &lt;b&gt;OncePerRequestFilter&lt;/b&gt;라고 할 수 있습니다.&lt;br /&gt;(이와 관련된 내용을 따로 정리했던 글이 있는데요. 참고하시면 될 것 같습니다. &lt;a href=&quot;https://beaniejoy.tistory.com/96&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;OncePerRequestFilter 관련 이전 글 내용&lt;/a&gt;)&lt;/p&gt;
&lt;figure id=&quot;og_1683450957564&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Spring] Filter 중복 호출되는 경우와 OncePerRequestFilter를 통한 처리&quot; data-og-description=&quot;목표 - 하나의 request에서 Filter의 중복 호출되는 사례들을 알아보자 - OncePerRequestFilter를 사용함으로써 Filter 중복 호출 방지 Spring Boot를 사용해 web application을 개발하다보면 Filter를 구현해서 적용&quot; data-og-host=&quot;beaniejoy.tistory.com&quot; data-og-source-url=&quot;https://beaniejoy.tistory.com/96&quot; data-og-url=&quot;https://beaniejoy.tistory.com/96&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bkiO0n/hySwMhgYiF/htvzOyg6cxoK5OPYRiS1Ck/img.png?width=800&amp;amp;height=400&amp;amp;face=0_0_800_400,https://scrap.kakaocdn.net/dn/brwgkp/hySwQRuqyo/5huxkqm1GEqFK1Bh4rOwe0/img.png?width=800&amp;amp;height=400&amp;amp;face=0_0_800_400&quot;&gt;&lt;a href=&quot;https://beaniejoy.tistory.com/96&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://beaniejoy.tistory.com/96&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bkiO0n/hySwMhgYiF/htvzOyg6cxoK5OPYRiS1Ck/img.png?width=800&amp;amp;height=400&amp;amp;face=0_0_800_400,https://scrap.kakaocdn.net/dn/brwgkp/hySwQRuqyo/5huxkqm1GEqFK1Bh4rOwe0/img.png?width=800&amp;amp;height=400&amp;amp;face=0_0_800_400');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[Spring] Filter 중복 호출되는 경우와 OncePerRequestFilter를 통한 처리&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;목표 - 하나의 request에서 Filter의 중복 호출되는 사례들을 알아보자 - OncePerRequestFilter를 사용함으로써 Filter 중복 호출 방지 Spring Boot를 사용해 web application을 개발하다보면 Filter를 구현해서 적용&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;beaniejoy.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;CotentCachingRequestWrapper, &lt;b&gt;CotentCachingResponseWrapper&lt;/b&gt;&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 주목할 것이 &lt;b&gt;CotentCachingRequestWrapper&lt;/b&gt;, &lt;b&gt;CotentCachingResponseWrapper&lt;/b&gt; 내용입니다. Logging Filter에서 Request, Response 데이터 내용을 확인해야 하는데요. 데이터 확인을 위해 Stream에서 데이터 read 과정을 거치게 되는데 이 때 Stream은 한 번 데이터를 읽으면 다시 해당 데이터를 읽을 수 없게 됩니다. &lt;b&gt;쉽게 말해서 한 번 읽은 데이터에 대해서 재사용이 불가능하다는 것입니다.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;&lt;/b&gt;InputStream으로 설명을 드리자면 read, write에 대한 pointer가 존재하는데 데이터의 끝자락(End Of File, EOF)에 도달하면 더이상 해당 Stream 내용을 읽을 수 없게 됩니다. &lt;br /&gt;(이부분에 대해서 설명한 글이 있는데요. 영어로 되어있긴 하지만 한 번 읽어보시면 좋을 것 같습니다. &lt;a href=&quot;https://www.programmersought.com/article/80902025083/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://www.programmersought.com/article/80902025083/&lt;/a&gt;)&lt;/p&gt;
&lt;figure id=&quot;og_1683286978243&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;How to reuse inputStream? - Programmer Sought&quot; data-og-description=&quot;Quote: Do the project when it came before a question is read from the network to upload pictures to oss, but also to cut and compress images, which have to use to upload and crop images to inputStream, but also because they can not duplicate read inputstre&quot; data-og-host=&quot;www.programmersought.com&quot; data-og-source-url=&quot;https://www.programmersought.com/article/80902025083/&quot; data-og-url=&quot;https://www.programmersought.com/article/80902025083/&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://www.programmersought.com/article/80902025083/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.programmersought.com/article/80902025083/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;How to reuse inputStream? - Programmer Sought&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Quote: Do the project when it came before a question is read from the network to upload pictures to oss, but also to cut and compress images, which have to use to upload and crop images to inputStream, but also because they can not duplicate read inputstre&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.programmersought.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;request logging을 위해 request 내부의 Stream에서 데이터를 읽는 순간 실제 request 내용 가지고 처리를 해야하는 Controller단에서 request 데이터를 가져올 수 없게 됩니다. response도 마찬가지로 client에 보낼 최종 응답 데이터를 보낼 수 없게 됩니다. &lt;br /&gt;&lt;br /&gt;이를 해결하기 위해 나온 것이 &lt;b&gt;CotentCachingRequestWrapper&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;CotentCachingResponseWrapper&amp;nbsp;&lt;/b&gt;입니다. Stream에서 데이터를 읽을 때 내부에서 해당 데이터들을 임시 저장(caching 처리)해놓았다가 실제 request, response 데이터를 가져올 때 caching해두었던 데이터들을 반환해주는 처리를 알아서 해줍니다.&lt;br /&gt;&lt;br /&gt;response는 &lt;b&gt;copyBodyToResponse&lt;/b&gt;를 최종적으로 호출해야하는데요. 해당 메소드를 호출함으로써 실제 response에 caching 해두었던 데이터들을 담아 보내게 됩니다.&lt;/p&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;실제 logging 처리&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1683287543081&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Logging Filter
try {
    log.info {
        HttpLogMessage.createInstance(
            requestWrapper = cachingRequestWrapper,
            responseWrapper = cachingResponseWrapper,
            elapsedTime = (end - startTime) / 1000.0
        ).toPrettierLog()
    }

    cachingResponseWrapper.copyBodyToResponse()
} catch (e: Exception) {
    log.error(e) { &quot;[${this::class.simpleName}] Logging 실패&quot; }
}

// HttpLogMessage
data class HttpLogMessage(
	//...
) {
	companion object {
        fun createInstance(
            requestWrapper: ContentCachingRequestWrapper,
            responseWrapper: ContentCachingResponseWrapper,
            elapsedTime: Double
        ): HttpLogMessage {
            return HttpLogMessage(
                httpMethod = requestWrapper.method,
                requestUri = requestWrapper.requestURI,
                httpStatus = HttpStatus.valueOf(responseWrapper.status),
                clientIp = requestWrapper.getClientIp(),
                elapsedTime = elapsedTime,
                headers = requestWrapper.getRequestHeaders(),
                requestParam = requestWrapper.getRequestParams(),
                requestBody = requestWrapper.getRequestBody(),
                responseBody = responseWrapper.getResponseBody(),
            )
        }
    }

    // 이부분은 각자 취향대로 포멧 정하는 것으로,,,
    fun toPrettierLog(): String {
        return &quot;&quot;&quot;
        |
        |[REQUEST] ${this.httpMethod} ${this.requestUri} ${this.httpStatus} (${this.elapsedTime})
        |&amp;gt;&amp;gt; CLIENT_IP: ${this.clientIp}
        |&amp;gt;&amp;gt; HEADERS: ${this.headers}
        |&amp;gt;&amp;gt; REQUEST_PARAM: ${this.requestParam}
        |&amp;gt;&amp;gt; REQUEST_BODY: ${this.requestBody}
        |&amp;gt;&amp;gt; RESPONSE_BODY: ${this.responseBody}
        &quot;&quot;&quot;.trimMargin()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 Logging 처리를 위한 코드를 가져왔습니다. INFO 레벨로 처리하였고 logging할 내용을 만들어내는 코드는 &lt;b&gt;HttpLogMessage&lt;/b&gt; 클래스에서 담당하도록 했습니다. logging 내용을 만들어내는 부분은 구현하는 개발자 마음이기 때문에 원하는 방식으로 적용하면 됩니다.&lt;br /&gt;&lt;br /&gt;LoggingFilter 클래스를&amp;nbsp;&lt;b&gt;@Component&lt;/b&gt;로 bean 등록을 했고 Spring Boot에서 따로 설정하지 않아도 알아서 Filter로 등록을 해줄 것입니다. 바로 애플리케이션을 실행해서 설정한 로그들을 확인해보면 다음과 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2023-05-16 at 10.59.25 PM.png&quot; data-origin-width=&quot;1332&quot; data-origin-height=&quot;187&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/N3TZt/btsgcpRjFJN/zSgMDpbExUNqubjZxLNMzk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/N3TZt/btsgcpRjFJN/zSgMDpbExUNqubjZxLNMzk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/N3TZt/btsgcpRjFJN/zSgMDpbExUNqubjZxLNMzk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FN3TZt%2FbtsgcpRjFJN%2FzSgMDpbExUNqubjZxLNMzk%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;760&quot; height=&quot;107&quot; data-filename=&quot;Screenshot 2023-05-16 at 10.59.25 PM.png&quot; data-origin-width=&quot;1332&quot; data-origin-height=&quot;187&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&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;b&gt;  3. Request 식별자 및 logging format 설정하기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 각 요청마다 HTTP 내용을 로그로 기록하는 작업을 진행했습니다. 여기서 살짝 부족한 부분이 있는데요, 각 request를 구별지을 수 있는 식별자가 없습니다.&lt;br /&gt;&lt;br /&gt;각 request에 대한 식별자(&lt;b&gt;request_id&lt;/b&gt;)가 필요한 이유는 여러가지가 있겠지만 멀티쓰레드 환경에서 여러 request들을 구분지을 수 있기 때문입니다. 멀티쓰레드 환경에서 Spring Boot는 각 request들에 쓰레드를 할당하고 이들을 동시에 처리하게 됩니다. 사실 말은 동시처리지만 OS단에서의 context switching 기법에 의해 여러 쓰레드들을 번갈아가면서 처리를 하게 됩니다.&lt;br /&gt;&lt;br /&gt;이렇게 되면 여러 요청들이 동시에 들어왔을 때 번갈아가며 요청들을 처리하기 때문에 로그들이 섞여서 출력이 되는 경우가 대부분입니다. 각 로그들이 어떤 request에 의해 발생된 것인지 구분하기 위해 request 식별자를 사용하는 것입니다.&lt;br /&gt;&lt;br /&gt;그리고 추가로 키바나 같은 로그 관리 툴에서 로그를 추적할 때 검색조건으로 자주 사용됩니다. 실무에서도 에러에 대한 원인을 분석할 때, 에러가 발생한 request_id를 가지고 키바나에서 로그데이터를 검색을 하곤합니다.&lt;br /&gt;&lt;br /&gt;이번에는 지금까지 설정한 내용을 기반으로 request 식별자를 적용하고 logback 설정파일을 통해 logging format을 따로 설정해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;  3-1. Request 식별자 설정&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1684247898028&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ReqResLoggingFilter
class ReqResLoggingFilter : OncePerRequestFilter() {
    private val log = KotlinLogging.logger {}

    companion object {
        const val REQUEST_ID = &quot;request_id&quot;
    }

    override fun doFilterInternal(
        request: HttpServletRequest,
        response: HttpServletResponse,
        filterChain: FilterChain,
    ) {
    	//...

        val requestId = UUID.randomUUID().toString().substring(0, 8)

        MDC.put(REQUEST_ID, requestId)

        //...

        try {
            //...
        } catch (e: Exception) {
            log.error(e) { &quot;[${this::class.simpleName}] Logging 실패&quot; }
        }

        MDC.remove(REQUEST_ID)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;식별자는 여러 방법으로 적용할 수 있습니다. 구글링했을 때 가장 많이 검색되는 &lt;b&gt;UUID&lt;/b&gt;를 가지고 random 값을 생성해서 적용했습니다.&lt;br /&gt;&lt;br /&gt;여기서 MDC를 이용했는데요. 이와 관련된 자세한 설명은 제가 참고했던 링크로 보시면 될 것 같습니다.&lt;br /&gt;(&lt;a href=&quot;https://www.baeldung.com/mdc-in-log4j-2-logback&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Improved Java Logging with Mapped Diagnostic Context (MDC)&lt;/a&gt;)&lt;br /&gt;핵심은 MDC는 내부에서 ThreadLocal 방식으로 동작하고 logback appender에서 접근가능하다는 것입니다.&lt;br /&gt;ThreadLocal은 각 쓰레드별로 관리되는 임시저장소 같은 것이라서 request_id와 같은 쓰레드별로 관리되어야 하는 데이터에 적격입니다. &lt;br /&gt;&lt;br /&gt;&lt;b&gt;logback appender&lt;/b&gt; 내용은 다음으로 넘어가서 보겠습니다.&lt;/p&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;  3-2. logback 설정파일 적용&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;resources 디렉토리 안에 &lt;b&gt;logback-spring.xml&lt;/b&gt; 파일을 생성해보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1684249287312&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;lt;configuration&amp;gt;
    &amp;lt;include resource=&quot;org/springframework/boot/logging/logback/defaults.xml&quot;/&amp;gt;
    &amp;lt;include resource=&quot;org/springframework/boot/logging/logback/console-appender.xml&quot;/&amp;gt;

    &amp;lt;!-- Pattern --&amp;gt;
    &amp;lt;property name=&quot;LOG_PATTERN&quot; value=&quot;%d{yyyy-MM-dd HH:mm:ss.SSS} %clr(%5level) [%15.15t] [%X{request_id}] %clr(%-40.40logger{39}){cyan} : %m%n%wEx&quot;/&amp;gt;
    &amp;lt;!-- Request Thread Console Appender --&amp;gt;
    &amp;lt;appender name=&quot;THREAD_CONSOLE&quot; class=&quot;ch.qos.logback.core.ConsoleAppender&quot;&amp;gt;
        &amp;lt;encoder class=&quot;ch.qos.logback.classic.encoder.PatternLayoutEncoder&quot;&amp;gt;
            &amp;lt;pattern&amp;gt;${LOG_PATTERN}&amp;lt;/pattern&amp;gt;
        &amp;lt;/encoder&amp;gt;
    &amp;lt;/appender&amp;gt;
&amp;lt;/configuration&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;custom한 logging format을 적용하기 위해 logback 설정파일을 통해 appender를 추가해줍니다. appender는 logback 구성요소중 하나인데요. logging 대상들을 어디에 쓸 것인지(파일, 콘솔 등등), 어떤 포멧을 적용할지 등을 설정할 수 있습니다.&lt;br /&gt;&lt;br /&gt;custom appender를 하나 추가했고 name을 &lt;b&gt;THREAD_CONSOLE&lt;/b&gt;로 하였습니다.&lt;br /&gt;&lt;br /&gt;여기서 주목할 점은 &lt;b&gt;LOG_PATTERN&lt;/b&gt;인데요. 여기에 &lt;b&gt;request_id&lt;/b&gt;를 사용하고 있습니다. 위에서 MDC 사용 이유 중 하나가 logback appender에서 접근가능하다는 특성이 있다고 설명했는데요. 이러한 특징이 여기서 빛을 발한다고 볼 수 있습니다.&lt;br /&gt;&lt;br /&gt;별개로&lt;b&gt; &amp;lt;include&amp;gt;&lt;/b&gt;로 추가 적용한 내용이 있는데요. Spring Boot에서 기본적으로 포함되어 있는 logback 관련 기본 appender를 가져오기 위한 설정내용입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1684249874435&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;!-- org/springframework/boot/logging/logback/console-appender.xml --&amp;gt;
&amp;lt;included&amp;gt;
	&amp;lt;appender name=&quot;CONSOLE&quot; class=&quot;ch.qos.logback.core.ConsoleAppender&quot;&amp;gt;
		&amp;lt;encoder&amp;gt;
			&amp;lt;pattern&amp;gt;${CONSOLE_LOG_PATTERN}&amp;lt;/pattern&amp;gt;
			&amp;lt;charset&amp;gt;${CONSOLE_LOG_CHARSET}&amp;lt;/charset&amp;gt;
		&amp;lt;/encoder&amp;gt;
	&amp;lt;/appender&amp;gt;
&amp;lt;/included&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 포함하고 있는 &lt;b&gt;ConsoleAppender&lt;/b&gt;를 가져왔습니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1684250331061&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;springProfile name=&quot;local&quot;&amp;gt;
    &amp;lt;logger additivity=&quot;false&quot; level=&quot;INFO&quot; name=&quot;io.beaniejoy.dongnecafe&quot;&amp;gt;
        &amp;lt;appender-ref ref=&quot;THREAD_CONSOLE&quot;/&amp;gt;
    &amp;lt;/logger&amp;gt;

    &amp;lt;!-- Bootstrap class file --&amp;gt;
    &amp;lt;logger additivity=&quot;false&quot; level=&quot;INFO&quot; name=&quot;io.beaniejoy.dongnecafe.DongneServiceApiApplicationKt&quot;&amp;gt;
        &amp;lt;appender-ref ref=&quot;CONSOLE&quot;/&amp;gt;
    &amp;lt;/logger&amp;gt;

    &amp;lt;root level=&quot;INFO&quot;&amp;gt;
        &amp;lt;appender-ref ref=&quot;CONSOLE&quot;/&amp;gt;
    &amp;lt;/root&amp;gt;
&amp;lt;/springProfile&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;lt;springProfile&amp;gt;&lt;/b&gt;를 통해 특정 profile에서 appender를 적용할 수 있는데요. 저는 일단 local 환경에서만 테스트를 할 것이기에 local profile로 설정했습니다.&lt;br /&gt;&lt;br /&gt;제 개인 프로젝트 기본 패키지가 &lt;b&gt;io.beaniejoy.dongnecafe&lt;/b&gt;이기에 해당 패키지에 custom appender(&lt;b&gt;THREAD_CONSOLE&lt;/b&gt;)을 등록했습니다. 한 가지 유의해야할 사항은 Bootstrap class file도 제가 지정한 custom appender로 적용이 된다는 점입니다. 그래서 따로 Bootstrap class file에 대해서는 기본 &lt;b&gt;CONSOLE&lt;/b&gt; appender를 적용했습니다.&lt;br /&gt;&lt;br /&gt;그 외에 나머지 부분(&amp;lt;root&amp;gt;)에 대해서도 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;기본&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;CONSOLE&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;appender를 적용했습니다.&lt;br /&gt;&lt;br /&gt;여기까지 완료되면 웬만한 Logging Filter 관련 설정이 마무리됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;  4. Filter 순서 조정&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 고려해야할 것이 있는데요. Filter의 순서를 조정해야 합니다.&lt;br /&gt;프로젝트를 개발하다보면 LoggingFilter 뿐만 아니라 Security Filter 같은 여러 Filter들을 같이 적용해 사용하게 됩니다. 지금까지 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Logging Filter&lt;span&gt;&amp;nbsp;관련&amp;nbsp;&lt;/span&gt;&lt;/span&gt;설정한 내용들을 가지고 Security Filter가 같이 적용이 된 상황에서 request를 요청해보면 어떻게 될까요.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2023-05-17 at 12.35.04 AM.png&quot; data-origin-width=&quot;1025&quot; data-origin-height=&quot;235&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cwj1L4/btsgex2jAgc/kUF8rE1aGKBiVR4qGopFGK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cwj1L4/btsgex2jAgc/kUF8rE1aGKBiVR4qGopFGK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cwj1L4/btsgex2jAgc/kUF8rE1aGKBiVR4qGopFGK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcwj1L4%2Fbtsgex2jAgc%2FkUF8rE1aGKBiVR4qGopFGK%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;760&quot; height=&quot;174&quot; data-filename=&quot;Screenshot 2023-05-17 at 12.35.04 AM.png&quot; data-origin-width=&quot;1025&quot; data-origin-height=&quot;235&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 사진과 같이 Spring Security에 적용된 Filter에 대해서는 &lt;u&gt;request_id 값이 로그에 제대로 적용이 안 된 것을 확인&lt;/u&gt;할 수 있습니다.&lt;br /&gt;왜 그럴까요.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2023-05-17 at 12.39.07 AM.png&quot; data-origin-width=&quot;777&quot; data-origin-height=&quot;339&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bFL4zZ/btsgbJJJygW/k4ABGjpOcOdbhRSHXzGgBk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bFL4zZ/btsgbJJJygW/k4ABGjpOcOdbhRSHXzGgBk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bFL4zZ/btsgbJJJygW/k4ABGjpOcOdbhRSHXzGgBk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbFL4zZ%2FbtsgbJJJygW%2Fk4ABGjpOcOdbhRSHXzGgBk%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;760&quot; height=&quot;332&quot; data-filename=&quot;Screenshot 2023-05-17 at 12.39.07 AM.png&quot; data-origin-width=&quot;777&quot; data-origin-height=&quot;339&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Security Filter의 기본 순서값은 -100입니다. &lt;u&gt;다시 말하면 Spring Security에 적용된 Filter들은 기본적으로 모든 Filter보다 선행한다는 것입니다.&lt;br /&gt;&lt;/u&gt;&lt;br /&gt;Logging Filter보다도 먼저 Security 관련 Filter들이 동작하기 때문에 MDC에 request_id 값을 저장하기 전이라서 아무런 값이 없는 상태입니다. Security Filter도 MDC에 저장한 request_id 값에 접근하기 위해서 Filter의 순서 조정이 필요해보입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1684296562461&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
class ReqResLoggingFilter : OncePerRequestFilter() {
//...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring에서 제공해주는 Order 어노테이션을 이용해서 LoggingFilter를 최상위 우선순위로 설정해줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1684296735825&quot; class=&quot;go&quot; data-ke-language=&quot;go&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring:
  security:
    filter:
    order: 10 # for logging filter&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;application.yml 파일에서 spring security에 대한 filter 순서값을 대략 10으로 설정해줍니다.&lt;br /&gt;이렇게 되면 LoggingFilter가 가장 먼저 동작하게 되고 MDC에 저장된 request_id 값을 이후에 Spring Security 관련 Filter에서도 접근할 수 있게 됩니다.&lt;/p&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&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;b&gt;  5. 결과&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2023-05-17 at 12.25.45 AM.png&quot; data-origin-width=&quot;1279&quot; data-origin-height=&quot;51&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bNqwYM/btsf6GsSJcO/83MC616sAE44KoHiAA3OTK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bNqwYM/btsf6GsSJcO/83MC616sAE44KoHiAA3OTK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bNqwYM/btsf6GsSJcO/83MC616sAE44KoHiAA3OTK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbNqwYM%2Fbtsf6GsSJcO%2F83MC616sAE44KoHiAA3OTK%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;760&quot; height=&quot;30&quot; data-filename=&quot;Screenshot 2023-05-17 at 12.25.45 AM.png&quot; data-origin-width=&quot;1279&quot; data-origin-height=&quot;51&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션 최초 실행시 bootstrap class file(여기서는 DongneServiceApiApplicationKt)에 대해서는 기본 Console appender가 적용이 된 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2023-05-17 at 1.28.47 PM.png&quot; data-origin-width=&quot;2190&quot; data-origin-height=&quot;380&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d7HRlr/btsgcrCwXix/vPG8vu93fmSzFXKO4ejOfk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d7HRlr/btsgcrCwXix/vPG8vu93fmSzFXKO4ejOfk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d7HRlr/btsgcrCwXix/vPG8vu93fmSzFXKO4ejOfk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd7HRlr%2FbtsgcrCwXix%2FvPG8vu93fmSzFXKO4ejOfk%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;760&quot; height=&quot;132&quot; data-filename=&quot;Screenshot 2023-05-17 at 1.28.47 PM.png&quot; data-origin-width=&quot;2190&quot; data-origin-height=&quot;380&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2023-05-17 at 1.29.09 PM.png&quot; data-origin-width=&quot;1806&quot; data-origin-height=&quot;386&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c5mc2C/btsgaL2lNAy/6m1AG6v0WKEoDL7cVUy580/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c5mc2C/btsgaL2lNAy/6m1AG6v0WKEoDL7cVUy580/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c5mc2C/btsgaL2lNAy/6m1AG6v0WKEoDL7cVUy580/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc5mc2C%2FbtsgaL2lNAy%2F6m1AG6v0WKEoDL7cVUy580%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;760&quot; height=&quot;162&quot; data-filename=&quot;Screenshot 2023-05-17 at 1.29.09 PM.png&quot; data-origin-width=&quot;1806&quot; data-origin-height=&quot;386&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Security에 등록된 Filter에도 request_id가 잘 나오고 있고, LoggingFilter에서의 로그도 request_id와 함께 제가 지정한 로그 형식대로 잘 출력된 것을 볼 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;LoggingFilter 작성시 고려사항&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;로그로 기록할 HTTP 프로토콜 내용 추리기&lt;/li&gt;
&lt;li&gt;구현할 Filter 내용 설정&lt;br /&gt;- OncePerRequestFilter 사용 이유&lt;br /&gt;- CotentCachingRequestWrapper,&lt;span&gt;&amp;nbsp;&lt;/span&gt;CotentCachingResponseWrapper 등 내용 고려&lt;br /&gt;- request_id 식별자 값 적용&lt;/li&gt;
&lt;li&gt;logback 설정파일 적용&lt;br /&gt;- logback-spring.xml 파일 생성&lt;br /&gt;- custom appender, 기본 logback console appender 등 원하는 appender 적용&lt;/li&gt;
&lt;li&gt;LoggingFilter에 대한 최상위 순서로 조정&lt;br /&gt;- 여러 Filter들이 존재하기 때문에 logging filter 순서를 고려해야함&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이정도일 것 같네요 :)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;틀린 내용이 있을 수 있습니다. 건강한 피드백 언제나 환영합니다!&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Spring</category>
      <category>Filter</category>
      <category>http</category>
      <category>log</category>
      <category>Spring</category>
      <category>Spring Boot</category>
      <author>beaniejoy</author>
      <guid isPermaLink="true">https://beaniejoy.tistory.com/97</guid>
      <comments>https://beaniejoy.tistory.com/97#entry97comment</comments>
      <pubDate>Wed, 17 May 2023 18:05:40 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] Filter 중복 호출되는 경우와 OncePerRequestFilter를 통한 처리</title>
      <link>https://beaniejoy.tistory.com/96</link>
      <description>&lt;blockquote data-ke-style=&quot;style3&quot;&gt;목표&lt;br /&gt;- 하나의 request에서 Filter의 중복 호출되는 사례들을 알아보자&lt;br /&gt;- OncePerRequestFilter를 사용함으로써 Filter 중복 호출 방지&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot를 사용해 web application을 개발하다보면 Filter를 구현해서 적용하는 일이 반드시 생기게 됩니다. Filter를 사용하다보면 Filter가 중복 호출되는 경우가 발생하게 되는데요. 어떤 경우에 이런 Filter 중복 호출 현상이 발생하는지에 대해 알아보고 OncePerRequestFilter를 통해서 이러한 현상을 방지하는 것까지 정리해보려합니다.&lt;br /&gt;(참고로 예시 코드는 kotlin 입니다.)&lt;/p&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&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;b&gt;  1. 기본적인 Filter 구성&lt;/b&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1683444400476&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class FirstFilter : Filter {
    companion object : KLogging()

    override fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) {
        val req = request as HttpServletRequest
        logger.info { &quot;#### [FirstFilter] [${req.dispatcherType}] request uri ${req.requestURI} ####&quot; }

        chain.doFilter(request, response)
        logger.info { &quot;#### [FirstFilter] after doFilter ####&quot; }
    }
}

class SecondFilter : Filter {
    companion object : KLogging()

    override fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) {
        val req = request as HttpServletRequest
        logger.info { &quot;#### [SecondFilter] [${req.dispatcherType}] request uri ${req.requestURI} ####&quot; }

        chain.doFilter(request, response)
        logger.info { &quot;#### [SecondFilter] after doFilter ####&quot; }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적인 서블릿 Filter 인터페이스를 구현하였고 테스트를 위해 두 개의 Filter를 만들었습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1683444482127&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
class FilterConfig {

    @Bean
    fun firstFilter(): FilterRegistrationBean&amp;lt;Filter&amp;gt; {
        return FilterRegistrationBean&amp;lt;Filter&amp;gt;().apply {
            this.filter = FirstFilter()
            this.order = 1
        }
    }

    @Bean
    fun secondFilter(): FilterRegistrationBean&amp;lt;Filter&amp;gt; {
        return FilterRegistrationBean&amp;lt;Filter&amp;gt;().apply {
            this.filter = SecondFilter()
            this.order = 2
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따로 Config file 구성해서 따로 Spring Bean으로 두 개의 필터를 등록했습니다. &quot;setOrder&quot;를 통해 순서까지만 설정하였습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1683445403695&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Controller
@RequestMapping(&quot;/api/filter&quot;)
class SpringFilterController {
    @ResponseBody
    @GetMapping(&quot;/test&quot;)
    fun filterTest(): String {
        return &quot;ok&quot;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트를 위한 api 호출을 위해 test controller를 만들고 테스트용 handler도 하나 적용했습니다.(GET Method)&lt;br /&gt;&lt;br /&gt;위와 같이 세팅하고 application을 실행하여 api를 호출해보면 다음과 같이 로그 결과가 나와야 합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2023-05-07 at 4.45.14 PM.png&quot; data-origin-width=&quot;2254&quot; data-origin-height=&quot;374&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cZohA7/btsd0vmwJt9/bYEV0hqxOmRedrLbKSqvck/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cZohA7/btsd0vmwJt9/bYEV0hqxOmRedrLbKSqvck/img.png&quot; data-alt=&quot;적용한 2개의 필터 로그 결과&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cZohA7/btsd0vmwJt9/bYEV0hqxOmRedrLbKSqvck/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcZohA7%2Fbtsd0vmwJt9%2FbYEV0hqxOmRedrLbKSqvck%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;780&quot; height=&quot;129&quot; data-filename=&quot;Screenshot 2023-05-07 at 4.45.14 PM.png&quot; data-origin-width=&quot;2254&quot; data-origin-height=&quot;374&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;적용한 2개의 필터 로그 결과&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;test api 호출시 request 유입에서는 config file에 적용했던 순서대로 FirstFilter &amp;gt; SecondFilter 순으로 호출이 되고 response 내보내는 과정에서는 반대로 SecondFilter &amp;gt; FirstFilter 순으로 호출되는 것을 확인할 수 있습니다.&lt;br /&gt;&lt;br /&gt;기본적인 Filter의 호출되는 과정을 봤습니다. &lt;br /&gt;이렇게 일반적인 케이스에서는 Filter가 한 번씩만 호출되지만 하나의 request에서 하나의 Filter가 중복으로 여러 번 호출되는 경우가 존재합니다. 이러한 케이스에 대해서 한 번 알아보겠습니다.&lt;/p&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&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;b&gt;  2. Filter 중복 호출 사례&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;  2-1. forward 처리시&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;forward는 redirect와 비교내용으로 많이 언급되는 페이지 전환 기법입니다. 둘의 차이점에 대해서는 구글에 검색해보시면 자세한 내용 확인할 수 있을 것입니다.(&lt;s&gt;요즘은 ChatGPT로&lt;/s&gt;...)&lt;br /&gt;&lt;br /&gt;forward 방식은 쉽게 말해서 client 최초 요청 그대로 다른 url로 바로 전달하는 방식입니다. 새로운 url로 새로운 요청정보를 가지고 client에서 다시 request를 보내는 redirect 방식과 다르게 서버 내부에서 요청정보 그대로 새로운 url로 요청을 알아서 전달해준다는 차이점이 있습니다.&lt;br /&gt;&lt;br /&gt;여기에서 하나의 request에서 Filter 중복 호출 이슈가 발생할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1683446855868&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
fun firstFilter(): FilterRegistrationBean&amp;lt;Filter&amp;gt; {
    return FilterRegistrationBean&amp;lt;Filter&amp;gt;().apply {
        this.filter = FirstFilter()
        this.order = 1
        this.setDispatcherTypes(DispatcherType.REQUEST)
    }
}

@Bean
fun secondFilter(): FilterRegistrationBean&amp;lt;Filter&amp;gt; {
    return FilterRegistrationBean&amp;lt;Filter&amp;gt;().apply {
        this.filter = SecondFilter()
        this.order = 2
        this.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.FORWARD)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Filter를 등록했던 config file에서 SecondFilter에는 DispatcherType으로 FORWARD 타입을 추가해서 등록합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1683446945099&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@GetMapping(&quot;/forward&quot;)
fun forward(): String {
    return &quot;forward:/api/filter/test&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 다음에 위에서 만들었던 Controller에 Spring의 forward 기능을 적용한 api handler를 추가합니다.&lt;br /&gt;이렇게 되면 &lt;b&gt;하나의 요청&lt;/b&gt;에서&amp;nbsp;&lt;b&gt;GET /api/filter/forward&lt;/b&gt; 요청시 &amp;gt; &lt;b&gt;GET /api/filter/test&lt;/b&gt;로 forwarding 될 것입니다. &lt;br /&gt;한 번 결과를 확인해볼까요.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2023-05-07 at 5.11.06 PM.png&quot; data-origin-width=&quot;2552&quot; data-origin-height=&quot;330&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dpq15y/btsdZ3RlsUv/YdzkU0O3LK14glL0Id3Lok/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dpq15y/btsdZ3RlsUv/YdzkU0O3LK14glL0Id3Lok/img.png&quot; data-alt=&quot;forwarding 처리된 api 호출시 로그 결과&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dpq15y/btsdZ3RlsUv/YdzkU0O3LK14glL0Id3Lok/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdpq15y%2FbtsdZ3RlsUv%2FYdzkU0O3LK14glL0Id3Lok%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;780&quot; height=&quot;101&quot; data-filename=&quot;Screenshot 2023-05-07 at 5.11.06 PM.png&quot; data-origin-width=&quot;2552&quot; data-origin-height=&quot;330&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;forwarding 처리된 api 호출시 로그 결과&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;SecondFilter가 두 번 호출&lt;/b&gt;된 것을 확인할 수 있습니다.&lt;br /&gt;forwarding시 filter들이 다음과 같이 처리됨을 알 수 있습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;1. GET /api/filter/forward 요청&lt;br /&gt;2. FirstFilter (REQUEST)&lt;br /&gt;3. SecondFilter (REQUEST)&lt;br /&gt;4. api 내부에서 /api/filter/test api로 forwarding 처리&lt;br /&gt;&lt;b&gt;5. SecondFilter (FORWARD)&lt;/b&gt;&lt;br /&gt;6. GET /api/filter/test handler 내부에 &quot;ok&quot; return&lt;br /&gt;&lt;b&gt;7. SecondFilter (FORWARD) - filterChain doFilter 이후 프로세스 진행&lt;/b&gt;&lt;br /&gt;8. SecondFilter (REQUEST) - filterChain doFilter 이후 프로세스 진행&lt;br /&gt;9. FirstFilter (REQUEST) - filterChain doFilter 이후 프로세스 진행&lt;br /&gt;10. client에 response 반환&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FORWARD dispatcherType을 등록한 SecondFilter에 대해서 하나의 요청에 대해 두 번 호출된 것을 확인할 수 있었습니다.&lt;/p&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;  2-2. Spring MVC error 처리시&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring MVC는 구현한 api 내부 로직에서 exception 발생시 try catch로 따로 잡아내지 않으면 tomcat까지 에러내용이 전달됩니다.&lt;br /&gt;&lt;a href=&quot;https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-2/dashboard&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;김영한님의 스프링 MVC 2편 강의&lt;/a&gt;에서 Spring MVC의 error handling에 대해서 자세하게 설명해주고 있는데요.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;1. WAS(여기까지 전파) &amp;lt;- 필터 &amp;lt;- 서블릿 &amp;lt;- 인터셉터 &amp;lt;- 컨트롤러(예외발생)&lt;br /&gt;2. WAS `/error` 다시 요청 -&amp;gt; 필터 -&amp;gt; 서블릿 -&amp;gt; 인터셉터 -&amp;gt; 컨트롤러(/error) -&amp;gt; View&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨트롤러 내부 로직에서 Exception 발생시 따로 handling하지 않으면 tomcat(WAS)까지 해당 예외가 전달이되고 default로 설정되어 있는 &lt;b&gt;/error&lt;/b&gt; url을 다시 요청하게 됩니다. 이 과정에서 같은 필터가 중복 호출이 될 수 있습니다.&lt;br /&gt;이부분에 대해서 한 번 실제로 확인해보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1683448766477&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
fun secondFilter(): FilterRegistrationBean&amp;lt;Filter&amp;gt; {
    return FilterRegistrationBean&amp;lt;Filter&amp;gt;().apply {
        this.filter = SecondFilter()
        this.order = 2
        this.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.FORWARD, DispatcherType.ERROR)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SecondFilter에 dispatcherType으로 &lt;b&gt;ERROR&lt;/b&gt; type을 추가 적용해보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1683448829413&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@GetMapping(&quot;/error&quot;)
fun error() {
    throw RuntimeException(&quot;error test&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RuntimeException을 던지는  api handler를 하나 추가해보겠습니다. 해당 api를 요청하면 다음과 같은 결과가 나옵니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2023-05-07 at 5.41.10 PM.png&quot; data-origin-width=&quot;2818&quot; data-origin-height=&quot;580&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FLPhJ/btsd9dEUvfj/SYtTM8ToMnTkomoilNcBC1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FLPhJ/btsd9dEUvfj/SYtTM8ToMnTkomoilNcBC1/img.png&quot; data-alt=&quot;error 처리 과정에서의 Filter 중복 호출&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FLPhJ/btsd9dEUvfj/SYtTM8ToMnTkomoilNcBC1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFLPhJ%2Fbtsd9dEUvfj%2FSYtTM8ToMnTkomoilNcBC1%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;780&quot; height=&quot;161&quot; data-filename=&quot;Screenshot 2023-05-07 at 5.41.10 PM.png&quot; data-origin-width=&quot;2818&quot; data-origin-height=&quot;580&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;error 처리 과정에서의 Filter 중복 호출&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SecondFilter가 REQUEST 때 한 번 호출되고 Exception 발생 이후에 &lt;b&gt;ERROR&lt;/b&gt; type으로 한 번 더 호출되는 것을 확인할 수 있습니다.&lt;br /&gt;&lt;br /&gt;위의 두 개의 사례로 하나의 요청에서 같은 Filter가 중복 호출될 수 있음을 알 수 있었습니다. 위의 사례는 테스트용으로 보여드리기 위해 간단한 상황을 코드로 적용해본 것인데요. 실제로는 아주 복잡한 상황에서 복잡한 프로세스에 의해 Filter가 중복 호출될 수 있기 때문에 개발시 이부분에 대해서 따로 체크를 하시는 것이 좋습니다.&lt;br /&gt;&lt;br /&gt;불필요한 중복 호출을 방지하기 위해 &lt;b&gt;OncePerRequestFilter&lt;/b&gt;을 제공하고 있는데요. 이것을 한 번 적용해보겠습니다.&lt;/p&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;  3. OncePerRequestFilter를 통한 중복 호출 방지 적용해보기&lt;/b&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1683449488587&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class OnceFilter : OncePerRequestFilter() {
    private val log = KotlinLogging.logger {}

    override fun doFilterInternal(
        request: HttpServletRequest,
        response: HttpServletResponse,
        filterChain: FilterChain,
    ) {
        log.info { &quot;#### [*OnceFilter*] [${request.dispatcherType}] request uri ${request.requestURI} ####&quot; }
        filterChain.doFilter(request, response)
        log.info { &quot;#### [*OnceFilter*] after doFilter ####&quot; }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 개의 Filter(FirstFilter, SecondFilter)와 다르게 &lt;b&gt;OncePerRequestFilter&lt;/b&gt;를 구현하고 있습니다. 위와 같이 간단하게 새로운 필터를 만들고 다음과 같이 설정파일에 Bean으로 적용해줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1683449578160&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
fun onceFilter(): FilterRegistrationBean&amp;lt;Filter&amp;gt; {
    return FilterRegistrationBean&amp;lt;Filter&amp;gt;().apply {
        this.filter = OnceFilter()
        this.order = 3
        this.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.FORWARD, DispatcherType.ERROR)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SecondFilter와 똑같이 dispatcherType으로 REQUEST, FORWARD, ERROR 모두 적용해줍니다. &lt;br /&gt;이런 상태에서 forward, exception 발생 상황에 대해서 중복 호출이 발생하는지 확인해보겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2023-05-07 at 5.54.25 PM.png&quot; data-origin-width=&quot;2546&quot; data-origin-height=&quot;362&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bBS7IE/btsd5YnG10M/Z8LCjSktgXLiFM3wRuHZw0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bBS7IE/btsd5YnG10M/Z8LCjSktgXLiFM3wRuHZw0/img.png&quot; data-alt=&quot;forward 상황에서 OnceFilter 중복호출 여부 확인&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bBS7IE/btsd5YnG10M/Z8LCjSktgXLiFM3wRuHZw0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbBS7IE%2Fbtsd5YnG10M%2FZ8LCjSktgXLiFM3wRuHZw0%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;780&quot; height=&quot;111&quot; data-filename=&quot;Screenshot 2023-05-07 at 5.54.25 PM.png&quot; data-origin-width=&quot;2546&quot; data-origin-height=&quot;362&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;forward 상황에서 OnceFilter 중복호출 여부 확인&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2023-05-07 at 5.55.06 PM.png&quot; data-origin-width=&quot;2854&quot; data-origin-height=&quot;670&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhRCU2/btsedclnqac/TO2v4p3GuSYFkc5FbxNKt0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhRCU2/btsedclnqac/TO2v4p3GuSYFkc5FbxNKt0/img.png&quot; data-alt=&quot;Exception 발생 상황에서 OnceFilter 중복호출 여부 확인&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhRCU2/btsedclnqac/TO2v4p3GuSYFkc5FbxNKt0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbhRCU2%2Fbtsedclnqac%2FTO2v4p3GuSYFkc5FbxNKt0%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;780&quot; height=&quot;183&quot; data-filename=&quot;Screenshot 2023-05-07 at 5.55.06 PM.png&quot; data-origin-width=&quot;2854&quot; data-origin-height=&quot;670&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Exception 발생 상황에서 OnceFilter 중복호출 여부 확인&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SecondFilter와 똑같은 dispatcherType을 적용했음에도 forward, exception 발생상황에서 OncePerRequestFilter를 구현한 Filter는 한 번만 호출된 것을 확인했습니다.&lt;/p&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&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;b&gt;  정리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;OncePerRequestFilter&lt;/b&gt;가 Filter 중복 호출을 방지하고 클래스 이름 그대로 하나의 Request에 한 번만 호출되도록 하는 필터인 것은 전에도 알고 있던 내용이었고 그대로 적용했었는데요.&lt;br /&gt;&lt;br /&gt;중요한 것은 어떤 상황에서 하나의 요청에서 같은 Filter가 중복호출이 되는지에 대해서 막연하게만 알았는데 이번을 계기로 좀더 구체적으로 알게 되었습니다. &lt;u&gt;간단하게 구글링해서 나온 코드를 단순히 복붙으로 적용하는 것보다는 왜 이러한 것이 나오게 됐는지에 대한 배경과 이유를 알고 사용하는 것&lt;/u&gt;이 좋다고 생각해서 장황하게 정리를 해보았습니다.&lt;br /&gt;&lt;br /&gt;필터 중복 호출은 불필요한 리소스 낭비차원에서 방지해야한다고 생각할 수 있지만, &lt;br /&gt;그것보다 인증, 인가 과정에서 하나의 요청에 대해 불필요한 인증 작업을 두 번이상 진행할 수도 있는 점을 고려해보았을 때 요청 처리 과정에서 치명적인 결함이 발생할 수 있기 때문에 중복 호출 이슈는 가볍게 넘기고 가야할 이슈는 아니라고 생각합니다.&lt;br /&gt;&lt;br /&gt;상황에 따라 필수적으로 하나의 요청에 단 한 번만 필터가 적용해야할 필요가 있다면 &lt;b&gt;OncePerRequestFilter&lt;/b&gt;를 구현해서 적용하시면 될 것 같습니다.&lt;/p&gt;</description>
      <category>Spring</category>
      <category>Filter</category>
      <category>Spring</category>
      <category>Spring MVC</category>
      <author>beaniejoy</author>
      <guid isPermaLink="true">https://beaniejoy.tistory.com/96</guid>
      <comments>https://beaniejoy.tistory.com/96#entry96comment</comments>
      <pubDate>Sun, 7 May 2023 18:13:28 +0900</pubDate>
    </item>
    <item>
      <title>[Jenkins] Lightsail에 Jenkins server 구축해보고 Spring project 빌드해보기</title>
      <link>https://beaniejoy.tistory.com/95</link>
      <description>&lt;blockquote data-ke-style=&quot;style3&quot;&gt;목적&lt;br /&gt;- AWS Lightsail을 통해 생성한 instance에 jenkins server 구축한 내용 기록용&lt;br /&gt;&lt;br /&gt;목표&lt;br /&gt;- AWS Lightsail에 띄운 instance에 jenkins server 띄우기&lt;br /&gt;- jenkins server 기본적인 설정&lt;br /&gt;- jenkins item 생성 후 spring project build 해보기&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개인 프로젝트에 대한 빌드 자동화를 위해 공부할 겸 Jenkins를 사용해보았습니다.&lt;br /&gt;Jenkins를 설치하는 내용보다 설치한 이후 기본적인 설정들과 pipeline을 사용한 item 생성 및 Spring project의 빌드 프로세스 구축한 내용이 주를 이룰 것 같습니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;  1. AWS Lightsail 네트워크 설정 변경&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://beaniejoy.tistory.com/94&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;AWS Lightsail을 이용한 instance 설정 내용에 대한 게시글&lt;/a&gt;을 참고해서 먼저 instance를 할당받아야 합니다.&lt;/p&gt;
&lt;figure id=&quot;og_1678619511010&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[AWS] AWS Lightsail을 이용해 간단하고 저렴하게 클라우드 서버 이용해보기&quot; data-og-description=&quot;목적 - AWS Lightsail을 이용해 AWS 클라우드 서비스를 이용한 내용을 정리 및 기록 - AWS Lightsail 간편한 설정과 상대적으로 가성비 있는 클라우드 서비스라는 것을 알리기 위함 (다른 더 좋은 것이 있&quot; data-og-host=&quot;beaniejoy.tistory.com&quot; data-og-source-url=&quot;https://beaniejoy.tistory.com/94&quot; data-og-url=&quot;https://beaniejoy.tistory.com/94&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bX5ojB/hyRTU1nKNN/MTi4C07gY8r9ZV3B8POZ9K/img.jpg?width=800&amp;amp;height=377&amp;amp;face=0_0_800_377,https://scrap.kakaocdn.net/dn/nmoDI/hyRVdSygC6/g0mf4KnTRktjqyGlnhVce1/img.jpg?width=800&amp;amp;height=377&amp;amp;face=0_0_800_377,https://scrap.kakaocdn.net/dn/cM5g9p/hyRTSvJwJT/RfRGIOIUwhycO1JppxRcW1/img.png?width=1374&amp;amp;height=914&amp;amp;face=0_0_1374_914&quot;&gt;&lt;a href=&quot;https://beaniejoy.tistory.com/94&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://beaniejoy.tistory.com/94&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bX5ojB/hyRTU1nKNN/MTi4C07gY8r9ZV3B8POZ9K/img.jpg?width=800&amp;amp;height=377&amp;amp;face=0_0_800_377,https://scrap.kakaocdn.net/dn/nmoDI/hyRVdSygC6/g0mf4KnTRktjqyGlnhVce1/img.jpg?width=800&amp;amp;height=377&amp;amp;face=0_0_800_377,https://scrap.kakaocdn.net/dn/cM5g9p/hyRTSvJwJT/RfRGIOIUwhycO1JppxRcW1/img.png?width=1374&amp;amp;height=914&amp;amp;face=0_0_1374_914');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[AWS] AWS Lightsail을 이용해 간단하고 저렴하게 클라우드 서버 이용해보기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;목적 - AWS Lightsail을 이용해 AWS 클라우드 서비스를 이용한 내용을 정리 및 기록 - AWS Lightsail 간편한 설정과 상대적으로 가성비 있는 클라우드 서비스라는 것을 알리기 위함 (다른 더 좋은 것이 있&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;beaniejoy.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;instance를 생성하고 가장 먼저 해야할 것이 lightsail에서 할당받은 instance의 네트워크 설정을 변경해주어야 합니다.&lt;br /&gt;&lt;br /&gt;Jenkins를 설치하면 default port로 8080을 부여받게 됩니다. 제 개인 노트북에서 AWS cloud server에서 실행중인 Jenkins 프로세스에 접근하려면 해당 포트번호를 통해 접속해야하는데요. 중요한 것은 바로 8080번에 접속할 수 없습니다.&lt;br /&gt;&lt;br /&gt;lightsail에서 네트워크 설정에 들어가면 방화벽 설정이 있는데요. 제 컴퓨터가 연결하고 있는 인터넷의 ip address에 대해서 instance의 8080 포트를 열어주어야 접속할 수 있습니다. 해당 내용은 위에 첨부해드린 링크에 들어가셔서 방화벽 설정부분 참고하시면 됩니다. &lt;br /&gt;(22번 ssh 포트에 대한 설정이 나올텐데 8080번 포트에 대해서 같은 방법으로 설정하면 됩니다.)&lt;/p&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&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;b&gt;  2. AWS instance에 Jenkins 구축해보기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;  2-1. JDK 설치하기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS instance에 jenkins 설치하기 전에 기본적인 jdk가 설치되어 있어야 합니다. &lt;br /&gt;저 같은 경우 java 17버전을 설치했고 lightsail instance에 Amazon Linux2 운영체제 기반으로 할당을 받았기 때문에 AWS에서 지원하는 JDK를 설치했습니다. (참고로 jenkins 공식 홈에서는 특정 jenkins 버전 이후로 java 11, 17버전을 사용하게끔 하고 있습니다.)&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Jenkins requires Java 11 or 17 since Jenkins 2.357 and LTS 2.361.1&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Amazon에서 지원하는 openjdk인 &lt;a href=&quot;https://docs.aws.amazon.com/corretto/latest/corretto-17-ug/what-is-corretto-17.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Amazon Corretto 17&lt;/a&gt;을 설치하면 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1678604574968&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ sudo yum install java-17-amazon-corretto-devel&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Amazon Linux 운영체제 기반에서 Amazon Corretto 17를 설치하는 내용에 대한 자세한 내용은 document를 참고하시면 됩니다.&lt;br /&gt;(&lt;a href=&quot;https://docs.aws.amazon.com/corretto/latest/corretto-17-ug/amazon-linux-install.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Amazon Corretto 17 Installation Instructions&lt;/a&gt;)&lt;/p&gt;
&lt;figure id=&quot;og_1678604722840&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Amazon Corretto 17 Installation Instructions for Amazon Linux 2 and Amazon Linux 2022 - Amazon Corretto 17&quot; data-og-description=&quot;Amazon Corretto 17 Installation Instructions for Amazon Linux 2 and Amazon Linux 2022 This topic describes how to install and uninstall Amazon Corretto 17 on a host or container running the Amazon Linux 2 or Amazon Linux 2022 operating systems. Install usi&quot; data-og-host=&quot;docs.aws.amazon.com&quot; data-og-source-url=&quot;https://docs.aws.amazon.com/corretto/latest/corretto-17-ug/amazon-linux-install.html&quot; data-og-url=&quot;https://docs.aws.amazon.com/corretto/latest/corretto-17-ug/amazon-linux-install.html&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://docs.aws.amazon.com/corretto/latest/corretto-17-ug/amazon-linux-install.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.aws.amazon.com/corretto/latest/corretto-17-ug/amazon-linux-install.html&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Amazon Corretto 17 Installation Instructions for Amazon Linux 2 and Amazon Linux 2022 - Amazon Corretto 17&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Amazon Corretto 17 Installation Instructions for Amazon Linux 2 and Amazon Linux 2022 This topic describes how to install and uninstall Amazon Corretto 17 on a host or container running the Amazon Linux 2 or Amazon Linux 2022 operating systems. Install usi&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.aws.amazon.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치가 완료되고 JAVA_HOME 환경 변수를 설정하고 기본 PATH에도 지정해줍시다.&lt;/p&gt;
&lt;pre id=&quot;code_1678606173237&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ vi ~/.bashrc&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1678606152537&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export JAVA_HOME=/usr/lib/jvm/java-17-amazon-corretto.x86_64

export PATH=/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/ec2-user/.local/bin:/home/ec2-user/bin
export PATH=$JAVA_HOME:$PATH&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1678606189375&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ source ~/.bashrc&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저 같은 경우 &lt;b&gt;.bashrc&lt;/b&gt; 설정파일에 환경 변수를 설정하였습니다. JAVA_HOME을 좀 전에 설치한 amazon corretto 17 내용의 경로로 지정하고 이 내용을 PATH에 등록해줍니다.&lt;/p&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;  2-2. Jenkins 설치하기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;instance에 jenkins 설치하는 방법 자체에 대해서 친절하게 소개하는 블로그 글이 있는데요. 해당 내용을 참고하셔서 설치하시면 될 것 같습니다. (&lt;a href=&quot;https://green-joo.tistory.com/12&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;AWS EC2에 Jenkins 설치하기&lt;/a&gt;)&lt;/p&gt;
&lt;figure id=&quot;og_1678603917890&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;AWS EC2(Amazon Linux 2)에 Jenkins 설치하기 ( AWS / EC2 / Linux / Jenkins ) _220713_업데이트&quot; data-og-description=&quot;안녕하세요. 그린주입니다 ๑'ٮ'๑ 오늘도 힘차게 시작해보겠습니다! 개요 이번 글에서는 AWS EC2(Amazon Linux 2)에 Jenkins 설치하는 방법을 공유하고자 합니다. 참고로 JAVA가 설치되어있고 Jenkins 인바&quot; data-og-host=&quot;green-joo.tistory.com&quot; data-og-source-url=&quot;https://green-joo.tistory.com/12&quot; data-og-url=&quot;https://green-joo.tistory.com/12&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/d88Ddy/hyRTQkd1jG/5VNpDxyeXo0pUggAXRRKc1/img.png?width=564&amp;amp;height=564&amp;amp;face=0_0_564_564,https://scrap.kakaocdn.net/dn/dZo4KL/hyRTV6VeFP/ro3KYYRm4UIFLGt3ZD3ko0/img.png?width=564&amp;amp;height=564&amp;amp;face=0_0_564_564,https://scrap.kakaocdn.net/dn/7rJZ6/hyRTSPRCGI/Jq3qUJVUIK1ekyHZSdQAvK/img.png?width=974&amp;amp;height=296&amp;amp;face=0_0_974_296&quot;&gt;&lt;a href=&quot;https://green-joo.tistory.com/12&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://green-joo.tistory.com/12&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/d88Ddy/hyRTQkd1jG/5VNpDxyeXo0pUggAXRRKc1/img.png?width=564&amp;amp;height=564&amp;amp;face=0_0_564_564,https://scrap.kakaocdn.net/dn/dZo4KL/hyRTV6VeFP/ro3KYYRm4UIFLGt3ZD3ko0/img.png?width=564&amp;amp;height=564&amp;amp;face=0_0_564_564,https://scrap.kakaocdn.net/dn/7rJZ6/hyRTSPRCGI/Jq3qUJVUIK1ekyHZSdQAvK/img.png?width=974&amp;amp;height=296&amp;amp;face=0_0_974_296');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;AWS EC2(Amazon Linux 2)에 Jenkins 설치하기 ( AWS / EC2 / Linux / Jenkins ) _220713_업데이트&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;안녕하세요. 그린주입니다 ๑'ٮ'๑ 오늘도 힘차게 시작해보겠습니다! 개요 이번 글에서는 AWS EC2(Amazon Linux 2)에 Jenkins 설치하는 방법을 공유하고자 합니다. 참고로 JAVA가 설치되어있고 Jenkins 인바&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;green-joo.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;pre id=&quot;code_1678604892406&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;==================================================================================================================================
 Package                       Arch                         Version                           Repository                     Size
==================================================================================================================================
Installing:
 jenkins                       noarch                       2.375.3-1.1                       jenkins                        90 M

Transaction Summary
==================================================================================================================================&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치가 완료되면 위와 같은 내용이 출력될 것 입니다. 버전 내용 확인하시면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;  2-3. Jenkins 설정 변경하기 - 1. 권한&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치 이후에 몇 가지 변경할 내용이 있는데요. 우선 Jenkins의 기본 사용자 권한, 그룹 권한을 변경해줍니다.&lt;br /&gt;설치시 default로 지정된 사용자, 그룹은 &lt;b&gt;jenkins&lt;/b&gt;로 되어 있는데요. Amazon Linux2의 기본 사용자(그룹)인 &lt;b&gt;ec2-user&lt;/b&gt;로 바꾸도록 하겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1678605159705&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ sudo vi /usr/lib/systemd/system/jenkins.service&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1678605181092&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# Unix account that runs the Jenkins daemon
# Be careful when you change this, as you need to update the permissions of
# $JENKINS_HOME, $JENKINS_LOG, and (if you have already run Jenkins)
# $JENKINS_WEBROOT.
User=ec2-user
Group=ec2-user&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;User와 Group 이름을 ec2-user로 변경하고 저장합니다.&lt;br /&gt;&lt;br /&gt;위에 링크 달아드린 Jenkins 설치방법에 대해서 소개하는 블로그 글에도 이 내용이 나와 있는데요. 구버전(2.335, 2.332.1 LTS 이전)에 대해서는 다음과 같이 설정하면 된다고 합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1678605402509&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ sudo vi /etc/sysconfig/jenkins&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1678605451100&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# Unix user account that runs the Jenkins daemon
# Be careful when you change this, as you need to update
# permissions of $JENKINS_HOME and /var/log/jenkins,
# and if you have already run Jenkins, potentially other
# directories such as /var/cache/jenkins .
#
JENKINS_USER=&quot;ec2-user&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 &lt;b&gt;JENKINS_USER&lt;/b&gt; 부분을 기존 jenkins에서 &lt;b&gt;ec2-user&lt;/b&gt;로 변경하고 저장합니다.&lt;br /&gt;&lt;br /&gt;jenkins의 기본 권한을 변경한 다음에 jenkins 관련 디렉토리의 사용자 및 그룹 권한도 변경해주어야 합니다. (기본 jenkins로 되어 있음)&lt;/p&gt;
&lt;pre id=&quot;code_1678605545814&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ chown -R ec2-user:ec2-user /var/lib/jenkins 
$ chown -R ec2-user:ec2-user /var/cache/jenkins
$ chown -R ec2-user:ec2-user /var/log/jenkins&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 세 개의 디렉토리를 &lt;b&gt;ec2-user:ec2-user&lt;/b&gt;로 변경해줍니다.&lt;br /&gt;설정이 완료되었으면 jenkins를 다시 시작하면 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1678605588915&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ sudo systemctl daemon-reload
$ sudo systemctl restart jenkins&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;  2-4. Jenkins 설정 변경하기 - 2. encoding&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;jenkins 기본 encoding이 UTF-8이 아니라서 젠킨스 빌드 실행시 console output에 나오는 로그내용이 깨져서 나올 수 있습니다. 이를 방지하기 위해 encoding 방식을 UTF-8로 변경했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1678606279559&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ vi ~/.bashrc&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1678606297633&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export JAVA_TOOL_OPTIONS=-Dfile.encoding=UTF-8
export LANG=en_US.UTF-8&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1678606313979&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ source ~/.bashrc&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 JAVA_TOOL_OPTIONS, LANG 이름의 환경변수를 설정해줍니다.&lt;br /&gt;그리고 jenkins에 접속해서 왼쪽 탭에 &lt;b&gt;Manage Jenkins&lt;/b&gt; &amp;gt; &lt;b&gt;Configure System&lt;/b&gt; &amp;gt; &lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #14141f; text-align: start;&quot;&gt;Global properties&lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;background-color: #ffffff; color: #14141f; text-align: start;&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;Screenshot 2023-03-12 at 4.33.59 PM.png&quot; data-origin-width=&quot;2462&quot; data-origin-height=&quot;1304&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dwYbpz/btr3cCQTpu5/6gwm24kixNtehnPyTl7kX1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dwYbpz/btr3cCQTpu5/6gwm24kixNtehnPyTl7kX1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dwYbpz/btr3cCQTpu5/6gwm24kixNtehnPyTl7kX1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdwYbpz%2Fbtr3cCQTpu5%2F6gwm24kixNtehnPyTl7kX1%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;750&quot; height=&quot;397&quot; data-filename=&quot;Screenshot 2023-03-12 at 4.33.59 PM.png&quot; data-origin-width=&quot;2462&quot; data-origin-height=&quot;1304&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #14141f; text-align: start;&quot;&gt;위와 같이 설정을 마치고 나서 jenkins를 다시 restart 합니다.&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;Screenshot 2023-03-12 at 4.36.35 PM.png&quot; data-origin-width=&quot;2126&quot; data-origin-height=&quot;540&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bZKe4r/btr3fs7uTGR/xMQtnASzbvfzuCxCPiIut1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bZKe4r/btr3fs7uTGR/xMQtnASzbvfzuCxCPiIut1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bZKe4r/btr3fs7uTGR/xMQtnASzbvfzuCxCPiIut1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbZKe4r%2Fbtr3fs7uTGR%2FxMQtnASzbvfzuCxCPiIut1%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;750&quot; height=&quot;190&quot; data-filename=&quot;Screenshot 2023-03-12 at 4.36.35 PM.png&quot; data-origin-width=&quot;2126&quot; data-origin-height=&quot;540&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #14141f; text-align: start;&quot;&gt;restart하고 jenkins의 System Information을 확인하면 위와 같이 file.encoding이 UTF-8로 되어 있을 것입니다.&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;Screenshot 2023-03-12 at 4.38.42 PM.png&quot; data-origin-width=&quot;1128&quot; data-origin-height=&quot;270&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bOMZKf/btr3dTExSFJ/9KCkzCDkhuKB60cUEopwiK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bOMZKf/btr3dTExSFJ/9KCkzCDkhuKB60cUEopwiK/img.png&quot; data-alt=&quot;encoding 설정 전&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bOMZKf/btr3dTExSFJ/9KCkzCDkhuKB60cUEopwiK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbOMZKf%2Fbtr3dTExSFJ%2F9KCkzCDkhuKB60cUEopwiK%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;750&quot; height=&quot;180&quot; data-filename=&quot;Screenshot 2023-03-12 at 4.38.42 PM.png&quot; data-origin-width=&quot;1128&quot; data-origin-height=&quot;270&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;encoding 설정 전&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2023-03-12 at 4.38.55 PM.png&quot; data-origin-width=&quot;1056&quot; data-origin-height=&quot;244&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dGSag5/btr3kxHeHxv/KudEaot8h64kNhQk77Oohk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dGSag5/btr3kxHeHxv/KudEaot8h64kNhQk77Oohk/img.png&quot; data-alt=&quot;encoding UTF-8 설정 후&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dGSag5/btr3kxHeHxv/KudEaot8h64kNhQk77Oohk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdGSag5%2Fbtr3kxHeHxv%2FKudEaot8h64kNhQk77Oohk%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;750&quot; height=&quot;173&quot; data-filename=&quot;Screenshot 2023-03-12 at 4.38.55 PM.png&quot; data-origin-width=&quot;1056&quot; data-origin-height=&quot;244&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;encoding UTF-8 설정 후&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&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;b&gt;  3. 기본적인 Pipeline 설정&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Jenkins 설정까지 마무리 되고 본격적으로 Spring project에 대한 빌드를 jenkins에 적용해보겠습니다. 저는 jenkins의 &lt;b&gt;pipeline&lt;/b&gt; 방식으로 진행하였습니다.&lt;br /&gt;&lt;br /&gt;pipeline 방식으로 빌드 프로세스를 구축하기 위해 spring project에 &lt;b&gt;Jenkinsfile&lt;/b&gt;을 만들어야 합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1678607038725&quot; class=&quot;go&quot; data-ke-language=&quot;go&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;pipeline {
    agent any

    stages {
        stage('Init') {
            steps {
                script {
                    sh 'whoami'
                    sh 'printenv'
                }
            }
        }

        stage('Test') {
            steps {
                sh './gradlew clean :dongne-service-api:test'
            }
        }

        stage('Build') {
            steps {
                sh './gradlew clean :dongne-service-api:build -x test'
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단한 pipeline을 작성해보았습니다. 빌드 절차로 간단하게 &lt;b&gt;Init &amp;gt; Test &amp;gt; Build&lt;/b&gt; 스테이지를 설정했습니다. &lt;br /&gt;Init에는 서버의 사용자 이름(&lt;b&gt;whoami&lt;/b&gt;)과 지정된 모든 환경변수 내용(&lt;b&gt;printenv&lt;/b&gt;)을 출력해서 로그로 확인할 수 있도록 하였습니다.&lt;br /&gt;Test와 Build는 gradle wrapper를 통해 실행되도록 하였습니다.&lt;/p&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&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;b&gt;  4. Jenkins Item 생성&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트에 pipeline을 위한 Jenkinsfile까지 생성하였다면 이제 jenkins에 적용해볼 차례입니다. &lt;br /&gt;jenkins에 접속하면 왼쪽 탭에 &lt;b&gt;New Item&lt;/b&gt;을 클릭합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2023-03-12 at 4.51.31 PM.png&quot; data-origin-width=&quot;2032&quot; data-origin-height=&quot;878&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bxcIus/btr3fs7u8AD/eDyQSqIOyiA3QtVkjSuEck/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bxcIus/btr3fs7u8AD/eDyQSqIOyiA3QtVkjSuEck/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bxcIus/btr3fs7u8AD/eDyQSqIOyiA3QtVkjSuEck/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbxcIus%2Fbtr3fs7u8AD%2FeDyQSqIOyiA3QtVkjSuEck%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;750&quot; height=&quot;324&quot; data-filename=&quot;Screenshot 2023-03-12 at 4.51.31 PM.png&quot; data-origin-width=&quot;2032&quot; data-origin-height=&quot;878&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;적절한 item 이름을 입력하고 Pipeline을 체크합니다. &lt;br /&gt;(dongne-service라는 이름으로 이미 item을 만들어둔 상태라 위와 같이 빨간색 글씨로 이미 존재한다는 경고 메세지가 나오고 있습니다. 첫 item 생성이면 이런 메세지는 안 나올 겁니다.)&lt;br /&gt;&lt;br /&gt;생성하자마자 바로 설정페이지로 진입할 것입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2023-03-12 at 4.55.50 PM.png&quot; data-origin-width=&quot;1938&quot; data-origin-height=&quot;1330&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dK0gd0/btr3cBYLF30/SxaLH2AdL7lOpDSn8BYETk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dK0gd0/btr3cBYLF30/SxaLH2AdL7lOpDSn8BYETk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dK0gd0/btr3cBYLF30/SxaLH2AdL7lOpDSn8BYETk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdK0gd0%2Fbtr3cBYLF30%2FSxaLH2AdL7lOpDSn8BYETk%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;750&quot; height=&quot;515&quot; data-filename=&quot;Screenshot 2023-03-12 at 4.55.50 PM.png&quot; data-origin-width=&quot;1938&quot; data-origin-height=&quot;1330&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;&lt;b&gt;Description&lt;/b&gt;: 해당 item에 대한 적절한 설명을 입력합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Discard old builds&lt;/b&gt;: jenkins item에서 빌드를 수행하면 빌드 이력이 남게되는데요. 저장공간을 절약해야하기 때문에 최대 5개까지만 이력을 저장하도록 하였습니다. 원하는 개수 입력하시면 됩니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Do not allow concurrent builds&lt;/b&gt;: 말 그대로 동시에 여러개 빌드가 진행되지 않도록 허용하지 않겠다고 설정하는 것입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2023-03-12 at 4.56.10 PM.png&quot; data-origin-width=&quot;1934&quot; data-origin-height=&quot;1088&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/VjNWW/btr3fsNcFM0/UHa86g9zc90hVZt1GiVNQ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/VjNWW/btr3fsNcFM0/UHa86g9zc90hVZt1GiVNQ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/VjNWW/btr3fsNcFM0/UHa86g9zc90hVZt1GiVNQ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVjNWW%2Fbtr3fsNcFM0%2FUHa86g9zc90hVZt1GiVNQ0%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;750&quot; height=&quot;422&quot; data-filename=&quot;Screenshot 2023-03-12 at 4.56.10 PM.png&quot; data-origin-width=&quot;1934&quot; data-origin-height=&quot;1088&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;item 안에서 사용할 파라미터 변수를 하나 지정합니다. &lt;br /&gt;brach라는 이름으로 파라미터를 등록했는데요. github repository의 &lt;b&gt;어떤 브랜치&lt;/b&gt;를 clone 받아서 빌드를 진행할지 결정하는 변수로 사용할 것입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2023-03-12 at 4.56.37 PM.png&quot; data-origin-width=&quot;1980&quot; data-origin-height=&quot;1366&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bij757/btr3przUk3l/e9XwSCyzXB3nTgZTSkNTwK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bij757/btr3przUk3l/e9XwSCyzXB3nTgZTSkNTwK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bij757/btr3przUk3l/e9XwSCyzXB3nTgZTSkNTwK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbij757%2Fbtr3przUk3l%2Fe9XwSCyzXB3nTgZTSkNTwK%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;750&quot; height=&quot;517&quot; data-filename=&quot;Screenshot 2023-03-12 at 4.56.37 PM.png&quot; data-origin-width=&quot;1980&quot; data-origin-height=&quot;1366&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pipeline script로 사용할 프로젝트를 지정합니다. github repository로 관리되고 있는 spring project를 대상으로 합니다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;본래 Credentials에서 github access token을 발급받아 설정해야합니다.&lt;/b&gt; &lt;br /&gt;저 같은 경우 public하게 오픈되어 있는 repository를 대상으로 하고 있기 때문에 따로 Credientials를 설정하지 않았는데요. &lt;br /&gt;이부분은 웬만하면 보안 안정성을 위해 token을 발급받아 인증된 token이 있을 때에만 github repository를 가져올 수 있게끔 해야합니다.&lt;br /&gt;&lt;br /&gt;Branches to build부분에는 위에 지정한 파라미터 변수인 &lt;b&gt;branch&lt;/b&gt;로 설정해줍니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2023-03-12 at 4.56.44 PM.png&quot; data-origin-width=&quot;1922&quot; data-origin-height=&quot;1058&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/A5GKD/btr3ofsMc7j/JK8AZXP8czJU3CkokXrZhk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/A5GKD/btr3ofsMc7j/JK8AZXP8czJU3CkokXrZhk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/A5GKD/btr3ofsMc7j/JK8AZXP8czJU3CkokXrZhk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FA5GKD%2Fbtr3ofsMc7j%2FJK8AZXP8czJU3CkokXrZhk%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;750&quot; height=&quot;413&quot; data-filename=&quot;Screenshot 2023-03-12 at 4.56.44 PM.png&quot; data-origin-width=&quot;1922&quot; data-origin-height=&quot;1058&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Script Path로 Jenkinsfile이 있는 경로와 이름을 지정해줍니다. project root 경로에 있는 &lt;b&gt;Jenkinsfile-service&lt;/b&gt; 이름의 pipeline script 파일로 지정했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 설정해주고 Apply &amp;gt; Save 해주면 기본적인 item 설정은 완료입니다.&lt;/p&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&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;b&gt;  5. Item 실행해보기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정을 마치면 item의 메인 페이지로 들어가게 되는데요 왼쪽 탭에 &lt;b&gt;Build with Parameters&lt;/b&gt;를 클릭합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2023-03-12 at 7.56.42 PM.png&quot; data-origin-width=&quot;2684&quot; data-origin-height=&quot;1074&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lmgQX/btr3fuj145I/bqlzE2k9x8TQD7AaXK8N9K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lmgQX/btr3fuj145I/bqlzE2k9x8TQD7AaXK8N9K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lmgQX/btr3fuj145I/bqlzE2k9x8TQD7AaXK8N9K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlmgQX%2Fbtr3fuj145I%2FbqlzE2k9x8TQD7AaXK8N9K%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;750&quot; height=&quot;300&quot; data-filename=&quot;Screenshot 2023-03-12 at 7.56.42 PM.png&quot; data-origin-width=&quot;2684&quot; data-origin-height=&quot;1074&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정에서 branch 파라미터 변수를 등록했었는데요. default value로 설정한 alpha 브랜치를 가지고 build 해보겠습니다. 당연히 git repository로 등록한 프로젝트의 alpha 브랜치에 Jenkinsfile 내용이 반영되어 있어야 합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2023-03-12 at 8.00.16 PM.png&quot; data-origin-width=&quot;2314&quot; data-origin-height=&quot;1156&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GAJ7T/btr3gPBt3ep/Md2F3B2QqhKkNETVwuVpC0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GAJ7T/btr3gPBt3ep/Md2F3B2QqhKkNETVwuVpC0/img.png&quot; data-alt=&quot;build 완료된 후&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GAJ7T/btr3gPBt3ep/Md2F3B2QqhKkNETVwuVpC0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGAJ7T%2Fbtr3gPBt3ep%2FMd2F3B2QqhKkNETVwuVpC0%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;750&quot; height=&quot;375&quot; data-filename=&quot;Screenshot 2023-03-12 at 8.00.16 PM.png&quot; data-origin-width=&quot;2314&quot; data-origin-height=&quot;1156&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;build 완료된 후&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;build가 완료되면 Jenkinsfile에 설정했던 stage 내용과 각 단계에서 소요된 시간이 함께 표시됩니다. 그리고 Build History 영역에 build된 이력이 남겨져 있습니다. build 이력을 클릭해봅시다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2023-03-12 at 8.02.55 PM.png&quot; data-origin-width=&quot;2294&quot; data-origin-height=&quot;1054&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bgc5zU/btr3gMSiARU/DigF2fPqZ0beACVXsGPhx0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bgc5zU/btr3gMSiARU/DigF2fPqZ0beACVXsGPhx0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bgc5zU/btr3gMSiARU/DigF2fPqZ0beACVXsGPhx0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbgc5zU%2Fbtr3gMSiARU%2FDigF2fPqZ0beACVXsGPhx0%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;750&quot; height=&quot;345&quot; data-filename=&quot;Screenshot 2023-03-12 at 8.02.55 PM.png&quot; data-origin-width=&quot;2294&quot; data-origin-height=&quot;1054&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;build 이력에 대한 상세 페이지가 나오는데요. 여기서 자세한 내용들을 확인할 수 있습니다. 그 중 &lt;b&gt;console output&lt;/b&gt;에서 Jenkins build 과정에 대한 로그내용을 확인할 수 있습니다. Jenkins build 실패시 console output에서 Failed 원인에 대한 단서를 찾아볼 수 있기 때문에 로그 내용을 잘 살펴보는 것이 중요합니다.&lt;/p&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&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;b&gt;  6. 정리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS lightsail에 올린 instance에 Jenkins를 설치하고 기본적인 설정 후 item 생성하여 spring project를 build 해보았습니다.&amp;nbsp;&lt;br /&gt;Jenkins pipeline 방식 말고도 다양한 방법으로 build를 할 수 있는데요. 실무에서도 pipeline 방식을 사용하고 있는 만큼 공부하는 차원에서 pipeline 방식으로 하였습니다.&lt;br /&gt;&lt;br /&gt;이제는 여기서 더 나아가 다른 서버에 build된 jar 파일을 Deploy하고 해당 서버에서 애플리케이션 실행까지 pipeline을 통해 단계를 추가할 수 있습니다. 이 부분에 대해서도 구축해보고 블로그에 정리해보려 합니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;틀린 내용이 있을 수 있습니다. 피드백 언제나 환영합니다. 감사합니다.&lt;/blockquote&gt;</description>
      <category>Infra</category>
      <category>AWS</category>
      <category>aws lightsail</category>
      <category>CI/CD</category>
      <category>Infra</category>
      <category>jenkins</category>
      <author>beaniejoy</author>
      <guid isPermaLink="true">https://beaniejoy.tistory.com/95</guid>
      <comments>https://beaniejoy.tistory.com/95#entry95comment</comments>
      <pubDate>Sun, 12 Mar 2023 20:19:51 +0900</pubDate>
    </item>
    <item>
      <title>[AWS] AWS Lightsail을 이용해 간단하고 저렴하게 클라우드 서버 이용해보기</title>
      <link>https://beaniejoy.tistory.com/94</link>
      <description>&lt;blockquote data-ke-style=&quot;style3&quot;&gt;목적&lt;br /&gt;- AWS Lightsail을 이용해 AWS 클라우드 서비스를 이용한 내용을 정리 및 기록&lt;br /&gt;- AWS Lightsail 간편한 설정과 상대적으로 가성비 있는 클라우드 서비스라는 것을 알리기 위함&lt;br /&gt;(다른 더 좋은 것이 있을수도...)&lt;br /&gt;&lt;br /&gt;목표&lt;br /&gt;- AWS Lightsail을 이용해 인스턴스 생성해본다.&lt;br /&gt;- 인스턴스에 대한 기본 설정에 대해서 알아보자.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개인 프로젝트하면서 나만의 서버를 구축을 해야할 필요성이 생겨서 클라우드 서비스를 이용하기로 했습니다. 클라우드 서버의 가장 큰 장점은 내 개인 노트북으로 까딱까딱 몇 개만 하면 나만의 서버를 구축하고 이용할 수 있다는 것이라 생각합니다. 개인 프로젝트를 빌드하고 배포함으로써 어디서든 내가 만든 서비스를 사용해 볼 수 있다는 점에서 아주 매력적입니다.&lt;br /&gt;&lt;br /&gt;이번 시간에는 클라우드 서비스 중 하나인 AWS Lightsail을 통해 인스턴스 생성에 대해서 정리해보고 이에 대한 간단한 평가 및 소감을 끄적끄적 적어보고자 합니다.&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;b&gt;  1. AWS Lightsail??&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원래 AWS EC2를 이용해 클라우스 서버를 구축하려고 했으나 과거에 막 가져다 쓰다가 요금 폭탄에 직면했던 경험이 있어서 다른 대체제를 찾아보게 되었습니다. 사실 AWS를 저렴하게 이용하는 방법이 있다면 EC2를 통해 제대로 사용해보고 싶지만 아직 AWS 자체에 대한 지식이 없어서 잠시 접어두기로 했습니다.&lt;br /&gt;&lt;br /&gt;그 중 AWS에 Lightsail이란 서비스를 알게 되었고 이름에서도 느껴지는 무언가 가벼운(light)느낌에 이끌려 이것을 사용하기로 결심했습니다.&lt;br /&gt;&lt;br /&gt;AWS Lightsail과 EC2의 차이점은 AWS에서 아주 친절하게 표까지해서 설명해주고 있는데요. (&lt;a href=&quot;https://aws.amazon.com/ko/premiumsupport/knowledge-center/lightsail-differences-from-ec2/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;설명 링크&lt;/a&gt;)&lt;br /&gt;저한테 가장 눈에 띄었던 것은&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Lightsail&lt;/td&gt;
&lt;td&gt;EC2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;가격이 저렴하고&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://aws.amazon.com/lightsail/pricing/&quot;&gt;고정 가격 모델&lt;/a&gt;이 있습니다.&lt;/td&gt;
&lt;td&gt;요금은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://aws.amazon.com/ec2/pricing/&quot;&gt;사용한 만큼 지불&lt;/a&gt;하는 모델을 따릅니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요금에 대한 내용이었습니다.&lt;br /&gt;&lt;br /&gt;Lightsail을 이용해 간단하게 instance 생성해보겠습니다.&lt;/p&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&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;b&gt;  2. Lightsail을 이용한 Instance 생성&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS lightsail을 이용하기 위해 AWS 계정이 필요합니다. 계정을 생성할 때 결제연동이 가능한 신용카드 등록하는 부분도 있는데요. 이부분에 대해서는 구글이 아주 친절하게 알려줄 것입니다. 여기서는 기본적으로 AWS 계정이 있다는 전제조건을 가지고 이어가겠습니다.&lt;br /&gt;&lt;br /&gt;&lt;a href=&quot;https://lightsail.aws.amazon.com/ls/webapp/home/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://lightsail.aws.amazon.com/ls/webapp/home/instances&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1677837879588&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;https://lightsail.aws.amazon.com/ls/webapp/home/instances&quot; data-og-description=&quot;&quot; data-og-host=&quot;lightsail.aws.amazon.com&quot; data-og-source-url=&quot;https://lightsail.aws.amazon.com/ls/webapp/home/instances&quot; data-og-url=&quot;https://lightsail.aws.amazon.com/ls/webapp/home/instances&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://lightsail.aws.amazon.com/ls/webapp/home/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://lightsail.aws.amazon.com/ls/webapp/home/instances&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;https://lightsail.aws.amazon.com/ls/webapp/home/instances&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;lightsail.aws.amazon.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 링크를 타고 lightsail dashboard에 들어갑니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_Screenshot 2023-03-03 at 7.05.24 PM.png&quot; data-origin-width=&quot;2016&quot; data-origin-height=&quot;586&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bPX4OK/btr1Kwc4T9D/8PrCFpmpoFWkJudpFNiWTk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bPX4OK/btr1Kwc4T9D/8PrCFpmpoFWkJudpFNiWTk/img.png&quot; data-alt=&quot;lightsail dashboard&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bPX4OK/btr1Kwc4T9D/8PrCFpmpoFWkJudpFNiWTk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbPX4OK%2Fbtr1Kwc4T9D%2F8PrCFpmpoFWkJudpFNiWTk%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;740&quot; height=&quot;215&quot; data-filename=&quot;edited_Screenshot 2023-03-03 at 7.05.24 PM.png&quot; data-origin-width=&quot;2016&quot; data-origin-height=&quot;586&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;lightsail dashboard&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대시보드에 들어가서 오른쪽에 보이는 create instance 주황색 버튼을 클릭합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_Screenshot 2023-03-03 at 7.05.47 PM.png&quot; data-origin-width=&quot;1594&quot; data-origin-height=&quot;1346&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xVVWZ/btr1MiS9OrU/IzMXMznyNuCHLRTXixyQqk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xVVWZ/btr1MiS9OrU/IzMXMznyNuCHLRTXixyQqk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xVVWZ/btr1MiS9OrU/IzMXMznyNuCHLRTXixyQqk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxVVWZ%2Fbtr1MiS9OrU%2FIzMXMznyNuCHLRTXixyQqk%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;740&quot; height=&quot;625&quot; data-filename=&quot;edited_Screenshot 2023-03-03 at 7.05.47 PM.png&quot; data-origin-width=&quot;1594&quot; data-origin-height=&quot;1346&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영체제를 선택해야합니다. 저같은 경우는 OS Only로 Amazon Linux2 OS를 선택하였습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_Screenshot 2023-03-03 at 7.06.04 PM.png&quot; data-origin-width=&quot;1374&quot; data-origin-height=&quot;914&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zsU2Y/btr1KO6iLJq/ThzaTaz6wKae8KMAWAxXg0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zsU2Y/btr1KO6iLJq/ThzaTaz6wKae8KMAWAxXg0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zsU2Y/btr1KO6iLJq/ThzaTaz6wKae8KMAWAxXg0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzsU2Y%2Fbtr1KO6iLJq%2FThzaTaz6wKae8KMAWAxXg0%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;740&quot; height=&quot;492&quot; data-filename=&quot;edited_Screenshot 2023-03-03 at 7.06.04 PM.png&quot; data-origin-width=&quot;1374&quot; data-origin-height=&quot;914&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSH 접속을 위한 key pair 설정이 있습니다. 저는 이미 생성했던 key pair가 있지만 처음에는 아무것도 없을 것입니다. create new를 통해 key pair를 생성합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_Screenshot 2023-03-03 at 7.06.35 PM.png&quot; data-origin-width=&quot;1452&quot; data-origin-height=&quot;762&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bHr0DY/btr1Vdjj9PM/Rr00PaeJGg5uQRTqULF3P1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bHr0DY/btr1Vdjj9PM/Rr00PaeJGg5uQRTqULF3P1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bHr0DY/btr1Vdjj9PM/Rr00PaeJGg5uQRTqULF3P1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbHr0DY%2Fbtr1Vdjj9PM%2FRr00PaeJGg5uQRTqULF3P1%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;740&quot; height=&quot;388&quot; data-filename=&quot;edited_Screenshot 2023-03-03 at 7.06.35 PM.png&quot; data-origin-width=&quot;1452&quot; data-origin-height=&quot;762&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원하는 kay pair 이름을 작성하고 generate key pair 버튼을 클릭합니다. 그러면 생성된 &lt;b&gt;private key&lt;/b&gt;를 다운받아질 것입니다. &lt;br /&gt;다운 받은 private key 파일은 절대 외부에 노출되어서도 안되고 공유해서도 안되고 잃어버리는 것도 안됩니다. 내 인스턴스가 비트코인 채굴용으로 사용되고 싶지 않나면 key 파일을 잘 보관하시길 바랍니다. &lt;br /&gt;(실제로 github에 aws에서 다운받은 pem 파일을 업로드했다가 천 만원 단위의 요금 폭탄을 받았던 사례가 있었다고 하더라구요.)&lt;/p&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&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;widthOrigin&quot; data-filename=&quot;edited_Screenshot 2023-03-03 at 7.06.58 PM.png&quot; data-origin-width=&quot;1852&quot; data-origin-height=&quot;884&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cZfAL1/btr1UUjJcB9/A6IAjopvKemASEekRQ9QLK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cZfAL1/btr1UUjJcB9/A6IAjopvKemASEekRQ9QLK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cZfAL1/btr1UUjJcB9/A6IAjopvKemASEekRQ9QLK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcZfAL1%2Fbtr1UUjJcB9%2FA6IAjopvKemASEekRQ9QLK%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;740&quot; height=&quot;353&quot; data-filename=&quot;edited_Screenshot 2023-03-03 at 7.06.58 PM.png&quot; data-origin-width=&quot;1852&quot; data-origin-height=&quot;884&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아주아주 중요한 plan 관련 선택입니다. First 3 months free는 AWS 프리티어 정책과 동일하다고 볼 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_Screenshot 2023-03-03 at 7.21.41 PM.png&quot; data-origin-width=&quot;2542&quot; data-origin-height=&quot;826&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sQiLS/btr1Uur8rB6/SKDDCzEs9uWuvbdzOS2uEK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sQiLS/btr1Uur8rB6/SKDDCzEs9uWuvbdzOS2uEK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sQiLS/btr1Uur8rB6/SKDDCzEs9uWuvbdzOS2uEK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsQiLS%2Fbtr1Uur8rB6%2FSKDDCzEs9uWuvbdzOS2uEK%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;740&quot; height=&quot;240&quot; data-filename=&quot;edited_Screenshot 2023-03-03 at 7.21.41 PM.png&quot; data-origin-width=&quot;2542&quot; data-origin-height=&quot;826&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라우드 서버를 750시간 사용한 이후 표준 요금표대로 요금이 부과됨을 알려주고 있는데요. 한달은 24 x 30이니 720시간 정도되기 때문에 instance가 24시간 작동되어도 750시간을 넘지는 않습니다. 사실상 3개월간 무료라고 보시면 됩니다.&lt;br /&gt;&lt;br /&gt;만약 정말 간단한 프로그램을 클라우드 서버에서 구동해보고 싶다하시면 프리티어 적용 대상 Plan을 사용하셔도 무방합니다. &lt;br /&gt;&lt;br /&gt;&lt;u&gt;&lt;b&gt;본인이 instance 사용목적이 무엇인지에 따라 판단하시면 되겠습니다.&lt;/b&gt;&lt;/u&gt; &lt;br /&gt;&lt;br /&gt;저의 사용 사례를 예시로 들자면 저는 lightsail 이용하는 목적이 jenkins 서버 구축하는 것과 Spring Boot 어플리케이션을 클라우드 서버에서 실행해보고자 함입니다. jenkins 서버를 구축하고 실행하는데 2GB 용량의 메모리는 턱없이 부족했기 때문에 4GB 이상의 메모리를 가진 plan을 선택했습니다. &lt;br /&gt;(사실 프리티어를 100프로 활용하기 위해 처음에 2GB 메모리 플랜으로 했었는데요. jenkins 프로그램에서 item 하나 실행했다가 메모리 부족으로 jenkins 서버가 죽어버리더라구요,,,)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_Screenshot 2023-03-03 at 7.07.25 PM.png&quot; data-origin-width=&quot;1914&quot; data-origin-height=&quot;1160&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cfdi1B/btr1VdwTzyX/U3Jg4OaIkxsgkT8kheIcDk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cfdi1B/btr1VdwTzyX/U3Jg4OaIkxsgkT8kheIcDk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cfdi1B/btr1VdwTzyX/U3Jg4OaIkxsgkT8kheIcDk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcfdi1B%2Fbtr1VdwTzyX%2FU3Jg4OaIkxsgkT8kheIcDk%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;740&quot; height=&quot;448&quot; data-filename=&quot;edited_Screenshot 2023-03-03 at 7.07.25 PM.png&quot; data-origin-width=&quot;1914&quot; data-origin-height=&quot;1160&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;적절한 instance 이름을 지어주시고 create instance를 눌러줍니다. (key-only tags, key-value tags는 생략하셔도 됩니다.)&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_Screenshot 2023-03-03 at 7.33.11 PM.png&quot; data-origin-width=&quot;2038&quot; data-origin-height=&quot;928&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/beQIp8/btr1UuTanfS/kWxAWbkiFCxcrHIVjMMMXK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/beQIp8/btr1UuTanfS/kWxAWbkiFCxcrHIVjMMMXK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/beQIp8/btr1UuTanfS/kWxAWbkiFCxcrHIVjMMMXK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbeQIp8%2Fbtr1UuTanfS%2FkWxAWbkiFCxcrHIVjMMMXK%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;740&quot; height=&quot;337&quot; data-filename=&quot;edited_Screenshot 2023-03-03 at 7.33.11 PM.png&quot; data-origin-width=&quot;2038&quot; data-origin-height=&quot;928&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성한 instance가 pending(준비중) 상태로 나오는데요. 몇 초 ~ 몇 분 기다리시면 오른쪽 인스턴스처럼 Running 상태가 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_Screenshot 2023-03-03 at 7.34.23 PM.png&quot; data-origin-width=&quot;1020&quot; data-origin-height=&quot;380&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/454Ve/btr1KQXlWJa/ZRn4OXxkXGcL8LPpAoYrr1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/454Ve/btr1KQXlWJa/ZRn4OXxkXGcL8LPpAoYrr1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/454Ve/btr1KQXlWJa/ZRn4OXxkXGcL8LPpAoYrr1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F454Ve%2Fbtr1KQXlWJa%2FZRn4OXxkXGcL8LPpAoYrr1%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;740&quot; height=&quot;276&quot; data-filename=&quot;edited_Screenshot 2023-03-03 at 7.34.23 PM.png&quot; data-origin-width=&quot;1020&quot; data-origin-height=&quot;380&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이로써 인스턴스 생성을 완료하였습니다. (정말 간단합니다.)&lt;/p&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&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;b&gt;  3. 인스턴스 설정하기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인스턴스를 생성하고 또 해야할 일이 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_Screenshot 2023-03-03 at 7.37.16 PM.png&quot; data-origin-width=&quot;982&quot; data-origin-height=&quot;480&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c4udM3/btr1T6LsIUM/u9g5Vh3YUPPUiDi2hKlApk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c4udM3/btr1T6LsIUM/u9g5Vh3YUPPUiDi2hKlApk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c4udM3/btr1T6LsIUM/u9g5Vh3YUPPUiDi2hKlApk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc4udM3%2Fbtr1T6LsIUM%2Fu9g5Vh3YUPPUiDi2hKlApk%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;740&quot; height=&quot;362&quot; data-filename=&quot;edited_Screenshot 2023-03-03 at 7.37.16 PM.png&quot; data-origin-width=&quot;982&quot; data-origin-height=&quot;480&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성한 인스턴스에 위 사진처럼 manage 버튼을 클릭합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_Screenshot 2023-03-03 at 7.38.11 PM.png&quot; data-origin-width=&quot;2116&quot; data-origin-height=&quot;964&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/F3W24/btr1KQQyKdM/SFBPy8XwwZCZWF9ZmR5S01/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/F3W24/btr1KQQyKdM/SFBPy8XwwZCZWF9ZmR5S01/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/F3W24/btr1KQQyKdM/SFBPy8XwwZCZWF9ZmR5S01/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FF3W24%2Fbtr1KQQyKdM%2FSFBPy8XwwZCZWF9ZmR5S01%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;740&quot; height=&quot;337&quot; data-filename=&quot;edited_Screenshot 2023-03-03 at 7.38.11 PM.png&quot; data-origin-width=&quot;2116&quot; data-origin-height=&quot;964&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Networking에 들어갑니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_Screenshot 2023-03-03 at 7.38.56 PM.png&quot; data-origin-width=&quot;1858&quot; data-origin-height=&quot;754&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/q8sLJ/btr1I9JjGyX/wPcKkydQpNzqBm1ZH4BdtK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/q8sLJ/btr1I9JjGyX/wPcKkydQpNzqBm1ZH4BdtK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/q8sLJ/btr1I9JjGyX/wPcKkydQpNzqBm1ZH4BdtK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fq8sLJ%2Fbtr1I9JjGyX%2FwPcKkydQpNzqBm1ZH4BdtK%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;740&quot; height=&quot;300&quot; data-filename=&quot;edited_Screenshot 2023-03-03 at 7.38.56 PM.png&quot; data-origin-width=&quot;1858&quot; data-origin-height=&quot;754&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네트워킹에 들어가면 public, private ip를 확인할 수 있습니다. public ip에 static ip를 적용해줍니다.&lt;br /&gt;static ip는 AWS 계정에 할당하는 고정형 ip 입니다. 계정을 삭제하거나 static ip를 삭제하기 전까지 말 그대로 고정된 ip를 사용할 수 있다는 장점이 있습니다. (&lt;a href=&quot;https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/elastic-ip-addresses-eip.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;AWS 설명 링크&lt;/a&gt;)&lt;/p&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_Screenshot 2023-03-03 at 7.40.39 PM.png&quot; data-origin-width=&quot;1218&quot; data-origin-height=&quot;798&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lFKS9/btr1Vd4IsWB/KnXkTzG4K7FDuZTDDDjAb1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lFKS9/btr1Vd4IsWB/KnXkTzG4K7FDuZTDDDjAb1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lFKS9/btr1Vd4IsWB/KnXkTzG4K7FDuZTDDDjAb1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlFKS9%2Fbtr1Vd4IsWB%2FKnXkTzG4K7FDuZTDDDjAb1%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;740&quot; height=&quot;485&quot; data-filename=&quot;edited_Screenshot 2023-03-03 at 7.40.39 PM.png&quot; data-origin-width=&quot;1218&quot; data-origin-height=&quot;798&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;static ip 이름을 지어주고 create and attach 버튼 클릭하면 생성과 동시에 해당 instance public ip에 attach 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_Screenshot 2023-03-03 at 7.41.07 PM.png&quot; data-origin-width=&quot;894&quot; data-origin-height=&quot;288&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2rcbL/btr1XPWuSko/ji00kohihbQcnnoYPqKm71/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2rcbL/btr1XPWuSko/ji00kohihbQcnnoYPqKm71/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2rcbL/btr1XPWuSko/ji00kohihbQcnnoYPqKm71/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2rcbL%2Fbtr1XPWuSko%2Fji00kohihbQcnnoYPqKm71%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;700&quot; height=&quot;226&quot; data-filename=&quot;edited_Screenshot 2023-03-03 at 7.41.07 PM.png&quot; data-origin-width=&quot;894&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;이런식으로 attach 되었다는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_Screenshot 2023-03-03 at 7.43.35 PM.png&quot; data-origin-width=&quot;1592&quot; data-origin-height=&quot;588&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bnr11Y/btr1KO6iL6e/gkjxUK9Sg77BTQ9QdUgBF0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bnr11Y/btr1KO6iL6e/gkjxUK9Sg77BTQ9QdUgBF0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bnr11Y/btr1KO6iL6e/gkjxUK9Sg77BTQ9QdUgBF0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbnr11Y%2Fbtr1KO6iL6e%2FgkjxUK9Sg77BTQ9QdUgBF0%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;740&quot; height=&quot;273&quot; data-filename=&quot;edited_Screenshot 2023-03-03 at 7.43.35 PM.png&quot; data-origin-width=&quot;1592&quot; data-origin-height=&quot;588&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 다음 SSH를 통한 shell 접속을 위해 방화벽을 열어주어야 합니다. 위 사진에 빨간색 네모박스로 표시한 버튼을 클릭합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_Screenshot 2023-03-03 at 7.50.53 PM.png&quot; data-origin-width=&quot;1548&quot; data-origin-height=&quot;796&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kYrEt/btr1WlH8qoF/6kkcL2kKfKcX70xyhKODMk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kYrEt/btr1WlH8qoF/6kkcL2kKfKcX70xyhKODMk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kYrEt/btr1WlH8qoF/6kkcL2kKfKcX70xyhKODMk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkYrEt%2Fbtr1WlH8qoF%2F6kkcL2kKfKcX70xyhKODMk%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;740&quot; height=&quot;381&quot; data-filename=&quot;edited_Screenshot 2023-03-03 at 7.50.53 PM.png&quot; data-origin-width=&quot;1548&quot; data-origin-height=&quot;796&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSH(22 포트)에 대해서 특정 ip로 제한을 두려고 합니다. Restrict to IP address를 체크하고 Source IP Address를 지정해줍니다. &lt;br /&gt;&lt;br /&gt;개인 노트북에서 인스턴스로 SSH 접속을 하고 싶다하면 개인 노트북이 연결하고 있는 네트워크의 ip 주소를 설정해주어야 합니다. private ip로 하면 안되고 public ip 주소이어야 합니다. 확인은 구글에 ip address 확인이라고 검색하시면 쉽게 확인할 수 있습니다.&lt;br /&gt;(&lt;a href=&quot;https://nordvpn.com/ko/ip-lookup/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;ip 주소 확인 링크&lt;/a&gt;)&lt;br /&gt;&lt;br /&gt;카페에서 SSH 접속하고자 할 때 카페의 public ip 주소를 지정해주어야 하는데요. 사실 안전하지 않는 방법이기 때문에 되도록 집에서 접속하는 것이 좋습니다.&lt;br /&gt;&lt;br /&gt;여기까지 하면 기본적인 인스턴스 설정을 마치게 됩니다.&lt;/p&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&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;b&gt;  4. SSH를 통한 인스턴스 원격 접속&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개인 노트북에서 콘솔화면에 들어갑니다.&lt;br /&gt;(macOS 환경으로 작성하였습니다. window 환경에서는 콘솔이 아닌 putty 같은 ssh 접속 프로그램을 따로 이용해야 합니다.)&lt;/p&gt;
&lt;pre id=&quot;code_1677841900238&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ssh -i &quot;aws-cloud-server.pem&quot; ec2-user@[instance_ip_address]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인스턴스 생성할 때 다운받은 pem 키파일이 있는 디렉토리로 이동(cd)한 다음 ssh 명령어를 통해 인스턴스에 SSH 접속을 할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2023-03-03 at 8.13.48 PM.png&quot; data-origin-width=&quot;1506&quot; data-origin-height=&quot;358&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/8e4yC/btr1USzqEYm/zfC0CwBTPicskhRC6upok0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/8e4yC/btr1USzqEYm/zfC0CwBTPicskhRC6upok0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/8e4yC/btr1USzqEYm/zfC0CwBTPicskhRC6upok0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F8e4yC%2Fbtr1USzqEYm%2FzfC0CwBTPicskhRC6upok0%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;740&quot; height=&quot;176&quot; data-filename=&quot;Screenshot 2023-03-03 at 8.13.48 PM.png&quot; data-origin-width=&quot;1506&quot; data-origin-height=&quot;358&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 접속 성공해야 합니다. &lt;br /&gt;접속이 실패하는 경우가 있는데요. 위에서 설정한 SSH 포트에 대한 방화벽 설정이 제대로 동작하는지 확인해야 합니다. 방화벽 설정할 때 허용 ip 주소로 제대로 된 ip 주소를 지정했는지 체크해야 합니다.&lt;/p&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&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;b&gt;  5. 정리 및 소감&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS에서 lightsail을 소개할 때 &quot;소규모에서 중간 규모에 이르는 워크로드가 있는 애플리케이션에 사용됩니다.&quot; 라고 소개하고 있습니다. 저와 같이 개인 프로젝트를 클라우드에 올려보고 서비스해보려 할 때 좋은 대안이 될 수 있을 거라 생각합니다.&lt;br /&gt;&lt;br /&gt;lightsail의 장점을 적어보자면 위에 설명한 내용이 초기 인스턴스 설정의 거의 모든 것이라 할정도로 인스턴스 생성이 아주아주 쉽고 UI가 직관적입니다. AWS EC2는 UI가 무언가 난잡하고 처음 사용하는 사용자 입장에서 복잡할 수 있지만 lightsail은 처음 사용해도 바로바로 적용해볼 수 있을 정도로 간단합니다. 저처럼 빠르게 클라우드 서버를 올리고 적용해보고자 할 때 추천드립니다.&lt;br /&gt;&lt;br /&gt;단점은 customizing의 제약이 있습니다. 인스턴스의 스펙이 마음에 안들어 스케일 업을 하고자 한다면 기존의 instance를 엎고 새로운 instance를 생성해서 사용해야 합니다. 유동적으로 스펙을 조정할 수 없습니다. &lt;br /&gt;그리고 제 개인적으로 아직까지 요금이 비싸게 느껴집니다. EC2에 비해서 상당히 저렴하다고 할 수 있지만 제가 원하는 구성의 플랜을 사용하려고 하면 가격이 확 올라갑니다. &lt;br /&gt;인스턴스를 여러 개 생성해서 어플리케이션 용도, 젠킨스 서버 용도, monitoring 용도로 사용해보고 싶은 마음이 있는데 하나당 20달러, 40달러씩 돈이 나가면 마음이 아플 것 같네요,,,&lt;br /&gt;&lt;br /&gt;어찌됐든 저는 AWS lightsail을 조만간 계속 이용해보려고 합니다. 조만간 lightsail을 이용해 jenkins 구축해보고 실제 item 생성해서 어플리케이션 테스트, 빌드 자동화한 내용도 정리해보겠습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;틀린 내용이 있으면 언제든 피드백 주세요~ 감사합니다&lt;/blockquote&gt;</description>
      <category>Infra</category>
      <category>AWS</category>
      <category>aws lightsail</category>
      <category>Cloud</category>
      <category>Infra</category>
      <author>beaniejoy</author>
      <guid isPermaLink="true">https://beaniejoy.tistory.com/94</guid>
      <comments>https://beaniejoy.tistory.com/94#entry94comment</comments>
      <pubDate>Fri, 3 Mar 2023 20:38:51 +0900</pubDate>
    </item>
    <item>
      <title>[Security] 인증, 인가 예외를 @ExceptionHandler로 적용해보기(ExceptionTranslationHandler)</title>
      <link>https://beaniejoy.tistory.com/93</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Security에서 인증 및 인가 예외가 발생할 수 있는데 이를 Spring의 @ExceptionHandler로 적용해보는 과정을 정리해보고자 합니다. 해당 글을 이해하기 위해서는 기본적인 Spring Security filter 진행 순서와 과정, 메커니즘을 어느정도 알고 있어야 읽기에 수월할 것 같습니다. (그렇다고 어려운 내용도 아니고 쉽습니다.)&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;b&gt;  1. 인증 과정에서의 인증 예외 처리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Security에서 인증 과정을 처리해주는 인증 필터가 존재합니다. 예를 들어, 기본적인 form login 방식에서는 &lt;b&gt;UsernamePasswordAuthenticationFilter&lt;/b&gt;가 있습니다.&lt;br /&gt;&lt;br /&gt;그리고 custom한 인증 필터를 만들어서 적용해보고 싶다하면 여러가지 방식이 있겠지만 &lt;b&gt;UsernamePasswordAuthenticationFilter&lt;/b&gt;의 상위 상속 클래스인 &lt;b&gt;AbstractAuthenticationProcessingFilter&lt;/b&gt;를 상속받아서 원하는 방식대로 코드를 doFilter에 구현하고 이를 Security Config 파일에 적용할 수도 있고 따로 Filter를 구성해서 직접 Config에 적용할 수 있습니다.&lt;br /&gt;&lt;br /&gt;제가 테스트했던 프로젝트에서는 custom한 인증 filter를 적용했고 실제 인증을 처리해주는 &lt;b&gt;AuthenticationProvider&lt;/b&gt;와 사용자정보를 조회하는 &lt;b&gt;UserDetailsService&lt;/b&gt;를 따로 구현하여 적용했습니다. &lt;br /&gt;이에 대한 내용은 생략하겠습니다. (테스트 프로젝트는 github url로 남겨두겠습니다.)&lt;br /&gt;&lt;br /&gt;이야기가 산으로 갔는데 본론으로 들어가면 인증 과정을 처리해주는 Security filter가 따로 있고 이 과정에서 인증 예외가 발생할 수 있습니다. 예를 들어 사용자가 입력한 email(username) 정보가 DB에 없다거나 입력한 비밀번호가 DB에 저장된 비밀번호와 불일치하는 경우 인증 예외가 발생합니다. &lt;br /&gt;&lt;br /&gt;여기서 발생하는 인증 예외는 &lt;b&gt;AuthenticationException&lt;/b&gt;이고 이를 상속하는 클래스가 여러 개 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2023-01-20 at 1.22.40 PM.png&quot; data-origin-width=&quot;2122&quot; data-origin-height=&quot;1090&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kMDBV/btrWNaSTww9/ylMvHu2i14ySAeZDDTOeY1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kMDBV/btrWNaSTww9/ylMvHu2i14ySAeZDDTOeY1/img.png&quot; data-alt=&quot;AuthenticationException을 상속하고 있는 클래스들&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kMDBV/btrWNaSTww9/ylMvHu2i14ySAeZDDTOeY1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkMDBV%2FbtrWNaSTww9%2FylMvHu2i14ySAeZDDTOeY1%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;760&quot; height=&quot;390&quot; data-filename=&quot;Screenshot 2023-01-20 at 1.22.40 PM.png&quot; data-origin-width=&quot;2122&quot; data-origin-height=&quot;1090&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;AuthenticationException을 상속하고 있는 클래스들&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&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;b&gt;  2. 인증 예외에 대한 ExceptionHandler 적용하기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 &lt;b&gt;AuthenticationException&lt;/b&gt;에 대한 ExceptionHandler를 정의해서 ControllerAdvice 설정된 클래스에서 예외를 관리하도록 구현합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1674190271517&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RestControllerAdvice
class BasicControllerAdvice {
	
    companion object: KLogging()
    
    /**
     * 인증, 인가 관련 에러 처리
     */
    @ResponseStatus(HttpStatus.OK)
    @ExceptionHandler(AuthenticationException::class)
    fun handleAuthException(e: AuthenticationException): ApplicationResponse&amp;lt;Nothing&amp;gt; {
        logger.error { &quot;[${e::class.simpleName}] &amp;lt;ErrorCode&amp;gt;: ${errorCode.name}, &amp;lt;ErrorMessage&amp;gt;: ${e.message}&quot; }
        
        return ApplicationResponse.fail(errorCode = ErrorCode.AUTH_UNAUTHORIZED).build()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 코드대로 AuthenticationException에 대한 handling하는 핸들러를 만들어두었습니다. &lt;br /&gt;제가 구현한 AuthenticationProvider, UserDetailsService에서의 인증 절차 내용은 Controller 진입 후 처리되도록 구현했습니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1674191133542&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// AuthController
@PostMapping(&quot;/authenticate&quot;)
fun signIn(@RequestBody signInRequest: SignInRequest): ApplicationResponse&amp;lt;TokenResponse&amp;gt; {
    val authentication = authService.signIn(
        email = signInRequest.email,
        password = signInRequest.password
    )

    val accessToken = jwtTokenUtils.createToken(authentication)

    return ApplicationResponse
        .success(&quot;success authenticate&quot;)
        .data(TokenResponse(accessToken))
}

// AuthService
fun signIn(email: String, password: String): Authentication {
    val authenticationToken = UsernamePasswordAuthenticationToken(email, password)

    return authenticationManager.authenticate(authenticationToken)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Controller 진입 이후 인증 절차가 진행되기에 이 과정에서 발생한 &lt;b&gt;AuthenticationException&lt;/b&gt;에 대해서 &lt;b&gt;@ExceptionHandler&lt;/b&gt;가 잘 catch해서 처리해줄 것입니다. &lt;br /&gt;(실질적으로 Controller 단에서 발생한 에러를 throw 했을 때 &lt;b&gt;DispatcherServlet&lt;/b&gt;까지 에러가 전달이 되고 여기서 &lt;b&gt;ExceptionResolver&lt;/b&gt;에 의해서 @ExceptionHandler에 적용된 핸들러로 에러를 전달하게 됩니다.)&lt;/p&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&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;b&gt;  3. 인가 과정에서의 AuthenticationException과 AccessDeniedException&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 인증 요청이 아닌 어떤 API를 요청했을 때 Spring Security는 해당 요청자가 요청한 API에 접근가능한 권한을 가지고 있는지 판단해서 거르는 과정을 진행해줍니다.&lt;br /&gt;&lt;br /&gt;이것이 바로 인가 프로세스인데요. &lt;b&gt;FilterSecurityInterceptor&lt;/b&gt;라는 필터에서 진행합니다. 그리고 이 필터에서는 &lt;b&gt;AuthenticationException&lt;/b&gt;과 &lt;b&gt;AccessDeniedException&lt;/b&gt; 두 개의 예외가 발생할 수 있습니다. 예외가 발생하면 ExceptionTranslationFilter에서 이 오류들을 catch해서 처리하는 부분이 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;code.png&quot; data-origin-width=&quot;1742&quot; data-origin-height=&quot;616&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/XlnLY/btrWPlTZ8Kk/So6gO39kmHIYHA5tyQ6fnk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/XlnLY/btrWPlTZ8Kk/So6gO39kmHIYHA5tyQ6fnk/img.png&quot; data-alt=&quot;ExceptionTranslationFilter에서 두 개의 Exception을 처리하는 부분&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/XlnLY/btrWPlTZ8Kk/So6gO39kmHIYHA5tyQ6fnk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXlnLY%2FbtrWPlTZ8Kk%2FSo6gO39kmHIYHA5tyQ6fnk%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;760&quot; height=&quot;269&quot; data-filename=&quot;code.png&quot; data-origin-width=&quot;1742&quot; data-origin-height=&quot;616&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;ExceptionTranslationFilter에서 두 개의 Exception을 처리하는 부분&lt;/figcaption&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;&lt;b&gt;AuthenticationException -&amp;gt; AuthenticationEntryPoint&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;AccessDeniedException -&amp;gt; AccessDeniedHandler&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각각의 Exception을 처리해주는 녀석들이 있는데 바로위의 &lt;b&gt;AuthenticationEntryPoint,&lt;/b&gt; &lt;b&gt;AccessDeniedHandler&lt;/b&gt;입니다.&lt;br /&gt;저는 위 두 개의 인터페이스를 따로 구현해서 제가 원하는 에러를 던져보고자 합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1674192356347&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
class CustomAuthenticationEntryPoint : AuthenticationEntryPoint {
    companion object: KLogging()

    override fun commence(
        request: HttpServletRequest,
        response: HttpServletResponse,
        authException: AuthenticationException
    ) {
        logger.info { &quot;Unauthorized Error!!&quot; }
        throw authException
    }
}

@Component
class CustomAccessDeniedHandler : AccessDeniedHandler {
    companion object : KLogging()

    override fun handle(
        request: HttpServletRequest,
        response: HttpServletResponse,
        accessDeniedException: AccessDeniedException
    ) {
        logger.info { &quot;Access Denied!!!!!&quot; }
        throw accessDeniedException
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 custom 구현체 두 개를 만들고 SecurityConfig에 적용합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1674192673030&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@ResponseStatus(HttpStatus.OK)
@ExceptionHandler(*arrayOf(AuthenticationException::class, AccessDeniedException::class))
fun handleAuthException(e: Exception): ApplicationResponse&amp;lt;Nothing&amp;gt; {
    val errorCode = when (e) {
        is AuthenticationException -&amp;gt; ErrorCode.AUTH_UNAUTHORIZED
        is AccessDeniedException -&amp;gt; ErrorCode.AUTH_ACCESS_DENIED
        else -&amp;gt; ErrorCode.DEFAULT
    }

    logger.error { &quot;[${e::class.simpleName}] &amp;lt;ErrorCode&amp;gt;: ${errorCode.name}, &amp;lt;ErrorMessage&amp;gt;: ${e.message}&quot; }
    return ApplicationResponse.fail(errorCode = errorCode).build()
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 ExceptionHandler 내용도 수정해줍니다. AccessDeniedHandler도 처리해줘야 하기 때문에&amp;nbsp; 두 개의 Exception을 한꺼번에 처리하는 handler로 수정했습니다.&lt;/p&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 만들어놓고 authenticated 설정된 API 하나 호출해보겠습니다. 기대하는 결과는 바로 위의 handleAuthException 핸들러가 동작하면서 제가 정의한 json 형태로 응답값을 받는 것입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1674192889577&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ curl -X GET localhost:9090/auth/check | jq ''                                                             ✔  base 
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  7250    0  7250    0     0   643k      0 --:--:-- --:--:-- --:--:--  643k
{
  &quot;timestamp&quot;: &quot;2023-01-20T05:29:51.885+00:00&quot;,
  &quot;status&quot;: 500,
  &quot;error&quot;: &quot;Internal Server Error&quot;,
  &quot;trace&quot;: &quot;org.springframework.security.authentication.InsufficientAuthenticationException: Full authentication is required to access this resource\n\tat org.springframewo&quot;,
  &quot;message&quot;: &quot;Full authentication is required to access this resource&quot;,
  &quot;path&quot;: &quot;/auth/check&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오잉 의도치 않는 형태로 응답이 나왔습니다.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;정확한 내용인지는 모르겠지만 &lt;b&gt;ExceptionTranslationFilter&lt;/b&gt;를 통해 &lt;b&gt;AuthenticationEntryPoint&lt;/b&gt;, &lt;b&gt;AccessDeniedHandler&lt;/b&gt;가 동작하는 구간은 Filter 구간입니다. 즉 &lt;b&gt;DispatcherServlet&lt;/b&gt; 전에 동작하는 부분입니다. 하지만 @ExceptionHandler는 &lt;b&gt;DispatcherServlet&lt;/b&gt;에서 &lt;b&gt;ExceptionResolver&lt;/b&gt;를 통해 예외 해결 시도할 때 동작하는 부분입니다. &lt;br /&gt;&lt;br /&gt;즉 ExceptionResolver가 처리를 기대하기도 전에 Security Filter 단계에서 Exception을 throw한 셈이 됩니다.&lt;br /&gt;&lt;br /&gt;이를 동기화하기 위해 &lt;b&gt;AuthenticationEntryPoint&lt;/b&gt;&lt;span&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;AccessDeniedHandler&lt;/b&gt;에 HandlerExceptionResolver를 통해서 ExceptionResolver가 해당 인증, 인가 예외를 다룰 수 있도록 설정할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1674193225202&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
class CustomAuthenticationEntryPoint(
    private val handlerExceptionResolver: HandlerExceptionResolver
): AuthenticationEntryPoint {
    companion object: KLogging()

    override fun commence(
        request: HttpServletRequest,
        response: HttpServletResponse,
        authException: AuthenticationException
    ) {
        logger.info { &quot;Unauthorized Error!!&quot; }
        handlerExceptionResolver.resolveException(request, response, null, authException)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AccessDeniedHandler 내용도 거의 똑같습니다. &lt;br /&gt;이렇게 하면 ExceptionResolver에게 명시적으로 Exception을 전달해주어 @ExceptionHandler에서 처리되도록 할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1674193372954&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ curl -X GET localhost:9090/auth/check | jq ''                                                             ✔  base 
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    49    0    49    0     0    188      0 --:--:-- --:--:-- --:--:--   187
{
  &quot;result&quot;: &quot;FAIL&quot;,
  &quot;errorCode&quot;: &quot;AUTH_UNAUTHORIZED&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의도한 형태로 응답값을 받았습니다. 너무 좋네요.&lt;/p&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&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;b&gt;  4. 정리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제 테스트 프로젝트의 경우 AuthenticationException, AccessDeniedException 발생 케이스를 아래와 같이 나눌 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;인증(로그인) 과정&lt;/b&gt;&lt;br /&gt;- 여기서 발생한 에러는 AuthenticationException&lt;br /&gt;- Controller 단 내부에서 인증 과정이 진행되도록 구성&lt;br /&gt;- 여기서 발생한 에러는 &lt;b&gt;ExceptionResolver&lt;/b&gt;가 감지하게 되어 @ExceptionHandler로 처리 가능&lt;/li&gt;
&lt;li&gt;&lt;b&gt;FilterSecurityInterceptor&lt;/b&gt;&lt;br /&gt;- 여기서는 AuthenticationException, AccessDeniedException 둘 다 발생 가능&lt;br /&gt;- ExceptionTranslationFilter가 해당 오류들을 catch해서 각각 &lt;b&gt;AuthenticationEntryPoint&lt;/b&gt;, &lt;b&gt;AccessDeniedHandler&lt;/b&gt;에 처리 위임&lt;br /&gt;- 위 과정은 Filter 단에서 이루어지는 내용이므로 여기서 &lt;b&gt;throw e&lt;/b&gt;을 해도 &lt;b&gt;ExceptionResolver가&lt;/b&gt; 감지를 못함&lt;br /&gt;(DispatcherServlet 과정 체크)&lt;br /&gt;- 명시적으로 ExceptionResolver에게 에러를 전달해야 함&lt;br /&gt;(&lt;b&gt;HandlerExceptionResolver resolveException&lt;/b&gt;)&lt;/li&gt;
&lt;/ul&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;b&gt;  5. References&lt;/b&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://www.baeldung.com/spring-security-exceptionhandler&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Handle Spring Security Exceptions With @ExceptionHandler&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;제가 구현한 &lt;a href=&quot;https://github.com/beaniejoy/dongne-cafe-api/blob/main/dongne-account-api/src/main/kotlin/io/beaniejoy/dongnecafe/security/handler/CustomAuthenticationEntryPoint.kt&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;github repository&lt;/a&gt; 입니다. 참고하세요&lt;/li&gt;
&lt;/ul&gt;
&lt;figure id=&quot;og_1674194310279&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - beaniejoy/dongne-cafe-api: ☕️ 동네 카페를 위한 사이렌 오더 토이 프로젝트 (~ing)&quot; data-og-description=&quot;☕️ 동네 카페를 위한 사이렌 오더 토이 프로젝트 (~ing). Contribute to beaniejoy/dongne-cafe-api development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/beaniejoy/dongne-cafe-api/blob/main/dongne-account-api/src/main/kotlin/io/beaniejoy/dongnecafe/security/handler/CustomAuthenticationEntryPoint.kt&quot; data-og-url=&quot;https://github.com/beaniejoy/dongne-cafe-api&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bwGIRb/hyRlC60Gvh/fmiXHz3bwUaMAkcDHa6FNK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/beaniejoy/dongne-cafe-api/blob/main/dongne-account-api/src/main/kotlin/io/beaniejoy/dongnecafe/security/handler/CustomAuthenticationEntryPoint.kt&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/beaniejoy/dongne-cafe-api/blob/main/dongne-account-api/src/main/kotlin/io/beaniejoy/dongnecafe/security/handler/CustomAuthenticationEntryPoint.kt&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bwGIRb/hyRlC60Gvh/fmiXHz3bwUaMAkcDHa6FNK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - beaniejoy/dongne-cafe-api: ☕️ 동네 카페를 위한 사이렌 오더 토이 프로젝트 (~ing)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;☕️ 동네 카페를 위한 사이렌 오더 토이 프로젝트 (~ing). Contribute to beaniejoy/dongne-cafe-api development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Spring/Security</category>
      <category>authentication</category>
      <category>Authorization</category>
      <category>exception</category>
      <category>Filter</category>
      <category>spring security</category>
      <author>beaniejoy</author>
      <guid isPermaLink="true">https://beaniejoy.tistory.com/93</guid>
      <comments>https://beaniejoy.tistory.com/93#entry93comment</comments>
      <pubDate>Fri, 20 Jan 2023 14:55:23 +0900</pubDate>
    </item>
    <item>
      <title>결국은 Call by Reference가 아닌 Call by Value</title>
      <link>https://beaniejoy.tistory.com/92</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;프로그래밍 언어를 배우면 Call by Reference와 Call by Value에 대한 내용이 나옵니다. 최근에 이 부분에 대한 오해(?)가 있었는데 이를 해소하면서 배운 점들을 블로그에 정리해보고자 합니다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;  Call by Reference?&lt;/span&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1672996587463&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class CallByReferenceAndValue {

    static void changeObjectValue(Person target) {
        target.setName(&quot;changed&quot;);
    }

    public static void main(String[] args) {
        Person person = new Person();
        person.setName(&quot;hello&quot;);

        System.out.println(&quot;before method - Person name: &quot; + person.getName());
        changeObjectValue(person);

        System.out.println(&quot;after method - Person name: &quot; + person.getName());
    }

    static class Person {
        private String name;

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;간단하게 조잡한 예시 코드를 짜봤는데요. 딱봐도 너무 쉬워보입니다. 프로그래밍 입문할 때 call by reference 예시로 들면서 메소드에 인자로 객체 reference를 받아서 해당 내부정보를 수정하는 경우 인자로 넣은 객체의 정보가 바뀐다는 내용입니다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;color: #333333;&quot;&gt;이게 무슨 말인지는 저 위의 실행로그를 보면 명료해집니다.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;before method - Person name: hello&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #333333;&quot;&gt;after method - Person name: changed&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;처음 Person name 값으로 &quot;hello&quot; 값을 넣었고 메소드로 해당 Person 객체를 받아서 메소드 내부에서 &quot;changed&quot;값으로 넣었습니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #333333;&quot;&gt;메소드 호출한 이후에 인자로 넣었던 person 객체의 name 값을 출력해보면 메소드 내부에서 수정했던 &quot;changed&quot; 값으로 변경되어 있는 것을 확인할 수 있습니다. 즉 메소드 내부에서 수정한 내용이 호출한 쪽에서도 반영이 된다는 내용입니다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;color: #333333;&quot;&gt;reference 내용을 메소드에 전달했을 때 내부 데이터값이 변경될 수 있다는 예시를 들어 call by reference라고 들었을 것입니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #333333;&quot;&gt;하지만 사실 &lt;b&gt;call by reference&lt;/b&gt;가 아닌 &lt;b&gt;call by value&lt;/b&gt;입니다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;  Call by Value&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;call by value는 말 그대로 값 자체가 전달이 된다는 내용입니다. 주로 int, long, float와 같은 primitive type에 해당하는 내용입니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1672997306049&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class CallByReferenceAndValue2 {

    static void changeValue(int target) {
    	target++;
    }

    public static void main(String[] args) {
    	int target = 1;

        System.out.println(&quot;before method - target: &quot; + target);
        changeValue(target);

        System.out.println(&quot;after method - target: &quot; + target);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;target은 primitive type으로 값 그자체입니다. 메소드에 전달해서 내부적으로 +1 을 해도 메소드 호출한 쪽에서는 값이 바뀌지 않습니다. 왜냐하면 메소드로 값을 복사해서 전달한 것이기 때문입니다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;color: #333333;&quot;&gt;메소드는 값을 복사해서 인자로 전달받는다는 관점에서 위의 call by reference를 보면 Person의 name이 바뀌는 예시도 결국 call by value임을 알 수 있습니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1672997576198&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class CallByReferenceAndValue {

    // Person 객체의 주소값이 복사되어 전달받은 것일 뿐
    static void changeObjectValue(Person target) {
        System.out.println(&quot;call by reference(method): &quot; + target);
        target.setName(&quot;changed&quot;);
    }

    public static void main(String[] args) {
        Person person = new Person();
        person.setName(&quot;hello&quot;);

        System.out.println(&quot;before method - Person name: &quot; + person.getName());
        System.out.println(&quot;call by reference: &quot; + person);
        changeObjectValue(person);

        System.out.println(&quot;after method - Person name: &quot; + person.getName());
    }

    static class Person {
        private String name;

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;Person 예시에서 객체의 주소값을 출력해보겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;before method - Person name: hello &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #333333;&quot;&gt;call by reference: io.beaniejoy.call.CallByReferenceAndValue$Person@7344699f &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #333333;&quot;&gt;call by reference(method): io.beaniejoy.call.CallByReferenceAndValue$Person@7344699f after method - Person name: changed&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;객체 참조값이 method 안에 인자로 복사되어 전달된다는 것을 알 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;stack 영역&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #333333;&quot;&gt;- main method &amp;gt; person 객체 생성 (참조값: 7344699f)&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #333333;&quot;&gt;- changeObjectValue 호출&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #333333;&quot;&gt;- changeObjectValue(7344699f) &amp;gt; 파라미터에 인자값을 전달, 참조값 &quot;7344699f&quot; 복사해서 전달&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #333333;&quot;&gt;- target.setName(&quot;...&quot;) &amp;gt; 7344699f 주소의 객체 내용 중 name 값 변경&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;color: #333333;&quot;&gt;heap 영역&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #333333;&quot;&gt;- main method 단계 &amp;gt;7344699f 참조에 대한 객체 내용 저장(name: &quot;hello&quot; 저장)&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #333333;&quot;&gt;- changeObjectValue 단계 &amp;gt; 7344699f 참조에 대한 객체 내용 변경(name: &quot;hello&quot; -&amp;gt; &quot;changed&quot;)&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;메모리 구조와 접목했을 때 위와 같이 정리해볼 수 있을 것 같습니다. &lt;br /&gt;참조값 &lt;span style=&quot;background-color: #fcfcfc;&quot;&gt;7344699f 자체가 복사되어 메소드에 전달되고 메소드 안에서 해당 참조값의 객체 내용을 수정하면 참조값이 바라보고 있는 heap 영역의 객체 정보를 수정하게 됩니다. 그래서 changeObjectValue 메소드 호출 이후에 person의 name을 출력했을 때 바뀐 값으로 출력된 것을 확인할 수 있었습니다.&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;정리하자면, method 파라미터에 전달되는 인자는 &lt;b&gt;call by value&lt;/b&gt;에 의해 값 자체가 복사되어 전달되고 그 값은 primitive type의 데이터뿐만 아니라 객체의 참조값 자체도 인자로 복사해서 전달됩니다.&lt;/span&gt;&lt;/p&gt;</description>
      <category>Java</category>
      <category>Call by reference</category>
      <category>Call by Value</category>
      <category>java</category>
      <author>beaniejoy</author>
      <guid isPermaLink="true">https://beaniejoy.tistory.com/92</guid>
      <comments>https://beaniejoy.tistory.com/92#entry92comment</comments>
      <pubDate>Tue, 17 Jan 2023 05:22:26 +0900</pubDate>
    </item>
    <item>
      <title>[JPA] Transactional과 쓰기지연 쿼리에 대한 간단한 이슈 정리(updatedAt 관련)</title>
      <link>https://beaniejoy.tistory.com/91</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;JPA를 사용하면서 마주했던 이슈에 대해서 기록하고자 블로그에 정리하게 되었습니다.&lt;br /&gt;&lt;br /&gt;A Entity가 있고 B Entity가 있는데 A Entity에 대한 내용을 수정하고 A Entity의 필드 값들을 B Entity에 담아서 DB에 insert 요청을 하기 위한 save 작업을 하는 내용이었습니다. 코드 상으로 보면 다음과 같습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1671637386009&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Transactional
fun saveHistoryAfterCafeInfoUpdated(cafeId: Long, request: UpdateDto) {
    // 대상 cafe 조회
    val cafe = cafeRepository.findByIdOrNull(cafeid) 
    	?: throw RuntimeException(&quot;cafe not found&quot;)
        
    // cafe 내용 변경
    cafe.updateInfo(request)
    // db에 update 쿼리 작성
    cafeRepository.save(cafe)
    
    logger.info { &quot;cafe name ${cafe.name}, address ${cafe.address}&quot; }
    logger.info { &quot;cafe updatedAt: ${cafe.updatedAt}&quot; }
    
    // cafe_histories에 변경된 cafe 내용을 db에 저장
    cafeHistoryService.saveHistory(cafe)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Cafe 내용을 조회하고 cafe의 name, address 등의 필드 내용을 변경하고 Repository를 이용해 save를 함으로써 update 쿼리를 작성하게 됩니다. 그 이후에 CafeHistory에 변경된 Cafe 정보들을 가지고 반영하여 신규 save(insert)를 하게 되는 프로세스입니다.&lt;/p&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&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;b&gt;  쓰기 지연으로 인한 AuditingEntityListener 적용된 updatedAt의 시점에 따른 값의 차이&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 메소드에&amp;nbsp;&lt;b&gt;@Transactional&lt;/b&gt; 어노테이션이 붙어있는 상황에서 로직을 수행하면 어떤 일이 발생하는지 알아보겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2022-12-22 at 12.54.09 AM.png&quot; data-origin-width=&quot;1221&quot; data-origin-height=&quot;641&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eNrKcX/btrUfLhodVb/Y4txgkQo8PZFQ9kDo3bKj1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eNrKcX/btrUfLhodVb/Y4txgkQo8PZFQ9kDo3bKj1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eNrKcX/btrUfLhodVb/Y4txgkQo8PZFQ9kDo3bKj1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeNrKcX%2FbtrUfLhodVb%2FY4txgkQo8PZFQ9kDo3bKj1%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;760&quot; height=&quot;399&quot; data-filename=&quot;Screenshot 2022-12-22 at 12.54.09 AM.png&quot; data-origin-width=&quot;1221&quot; data-origin-height=&quot;641&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;findByIdOrNull&lt;/b&gt;에 의해 우선 select 쿼리가 날라가서 cafe 데이터를 조회해오는 것을 볼 수 있습니다. 그런데 그 이후를 보면 &lt;b&gt;cafeRepository.save(cafe)&lt;/b&gt;를 통해 update가 된 줄 알았던 &lt;b&gt;Cafe&lt;/b&gt; Entity 내용을 그대로 &lt;b&gt;CafeHistory&lt;/b&gt;에 반영하려고 보았더니 &lt;b&gt;updatedAt&lt;/b&gt;이 &lt;b&gt;null&lt;/b&gt;입니다.&lt;br /&gt;&lt;br /&gt;변경된 cafe를 Repository에 save하면서 updatedAt도 현재시간으로 업데이트 될 줄 알았는데 null이 나온 것입니다. 위의 로그를 보면 명확하게 알 수 있는데요. 로그를 찍고나서 그 다음에 실제 update 쿼리가 나간 것을 알 수 있습니다. 왜 그럴까요?&lt;br /&gt;&lt;br /&gt;이게 바로 영속성 컨텍스트의 쓰기 지연 작업 때문이라 할 수 있습니다. 쓰기 지연은 말 그대로 insert, update 같이 데이터를 생성하거나 수정할 때 지연되어 요청보낸다는 내용입니다.&lt;br /&gt;&lt;br /&gt;쓰기 지연에서 가장 중요한 설정은 &lt;b&gt;Transactional&lt;/b&gt; 입니다. 해당 로직에 &lt;b&gt;@Transactional&lt;/b&gt;로 묶여 있어야 쓰기지연이 제대로 동작합니다.&lt;br /&gt;JPA에서 Repository에 의해 save 같은 메소드로 호출하거나 필드 변경을 통한 변경감지를 이용해 i&lt;span&gt;nsert, update를&lt;span&gt; 요청할 수 있는데요. 메소드 호출시점에 바로 이러한 쿼리를 요청해서 DB에 반영하는 것이 아니라 쿼리만 작성해두고 &lt;u&gt;모아두었다가 Transactional로 메소드가 끝나는 시점에 한꺼번에 호출하게 됩니다.&lt;/u&gt; &lt;br /&gt;(정확히는 &lt;b&gt;transaction commit&lt;/b&gt;이 발생하는 시점에 쿼리들이 실제 날라가게 됩니다.)&lt;br /&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1671638762760&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Transactional
fun saveHistoryAfterCafeInfoUpdated(cafeId: Long, request: UpdateDto) {
    // 대상 cafe 조회
    val cafe = cafeRepository.findByIdOrNull(cafeid) 
    	?: throw RuntimeException(&quot;cafe not found&quot;)
        
    // cafe 내용 변경
    cafe.updateInfo(request)
    // db에 update 쿼리 작성
    cafeRepository.save(cafe)
    
    logger.info { &quot;cafe name ${cafe.name}, address ${cafe.address}&quot; }
    logger.info { &quot;cafe updatedAt: ${cafe.updatedAt}&quot; }
    
    // 메소드가 끝나는 시점(Transactional commit 되는 순간)
    // 이 때 update cafe 쿼리가 날라가게 된다.
    // 그렇기 때문에 위의 updatedAt은 기존 cafe 데이터의 값인 null로 나오게 된다.
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;b&gt;updatedAt&lt;/b&gt;은 &lt;b&gt;AuditingEntityListener&lt;/b&gt;에 의해서 실제 update 쿼리가 실행될 때 업데이트된 시간을 넣게 되는데요. &lt;br /&gt;즉, logger에 의해 &lt;u&gt;logging 되는 시점에서 &lt;b&gt;updatedAt&lt;/b&gt;은 메소드가 끝나기 전이기 때문에 쓰기 지연에 의한 update 쿼리가 실행되기 전이고 update 되기 전인 본래의 cafe 데이터의 &lt;b&gt;updatedAt&lt;/b&gt; 값이 로그에 찍히는 것을 확인할 수 있습니다.&lt;/u&gt; &lt;br /&gt;(만약 update 된적이 한 번도 없고 기존에 &lt;b&gt;updatedAt&lt;/b&gt;에 null로 저장되어있었다면 null이 로그에 나올 것입니다.)&lt;br /&gt;&lt;br /&gt;위 내용들을 정리하면 다음과 같습니다.&lt;br /&gt;&lt;/span&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;하나의 Transaction으로 묶인(@Transactional) 메소드 안에서 Entity 조회후 해당 Entity 내용을 수정&lt;/li&gt;
&lt;li&gt;수정한 Entity를 그대로 Repository(JpaRepository) save 메소드를 통해 update 수행&lt;/li&gt;
&lt;li&gt;JPA 쓰기 지연 특징으로 실제 쿼리가 실행되는 것은 메소드가 끝나는 지점(&lt;b&gt;메소드 끝나는 지점이 tx commit 되는 시점&lt;/b&gt;)&lt;/li&gt;
&lt;li&gt;메소드 끝나기전에는 실제 update 쿼리가 실행된 것이 아니기 때문에 &lt;b&gt;updatedAt은 update되기 전의 값으로 나오게 됨&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&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;b&gt;  @Transactional이 없는 상황에서의 updatedAt&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;@Transactional&lt;/b&gt; 어노테이션을 제거하고 같은 요청을 한 번 진행해보았습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2022-12-23 at 2.13.08 PM.png&quot; data-origin-width=&quot;2326&quot; data-origin-height=&quot;1272&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Hb7Z4/btrUqaVCAsc/2AZkXjH7u5yWi33EtCVdQK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Hb7Z4/btrUqaVCAsc/2AZkXjH7u5yWi33EtCVdQK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Hb7Z4/btrUqaVCAsc/2AZkXjH7u5yWi33EtCVdQK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHb7Z4%2FbtrUqaVCAsc%2F2AZkXjH7u5yWi33EtCVdQK%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;760&quot; height=&quot;416&quot; data-filename=&quot;Screenshot 2022-12-23 at 2.13.08 PM.png&quot; data-origin-width=&quot;2326&quot; data-origin-height=&quot;1272&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 updatedAt에 날짜 시간이 잘 찍힌 것을 볼 수 있습니다.&lt;br /&gt;Transactional 어노테이션이 없이 실행되면 해당 메소드는 트랜잭션으로 묶이지 않게 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1671772574415&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun saveHistoryAfterCafeInfoUpdated(cafeId: Long, request: UpdateDto) {
    // 대상 cafe 조회
    val cafe = cafeRepository.findByIdOrNull(cafeid) 
    	?: throw RuntimeException(&quot;cafe not found&quot;)
        
    // cafe 내용 변경
    cafe.updateInfo(request)
    // db에 update 쿼리 작성
    // 여기서는 JpaRepository 구현체에 의해 Transactional 적용됨
    cafeRepository.save(cafe)
    
    logger.info { &quot;cafe name ${cafe.name}, address ${cafe.address}&quot; }
    logger.info { &quot;cafe updatedAt: ${cafe.updatedAt}&quot; }
    
    // cafe_histories에 변경된 cafe 내용을 db에 저장
    cafeHistoryService.saveHistory(cafe)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JpaRepository를 상속받은 cafeRepository 인터페이스를 통해 Spring Data JPA의 기본 제공 메소드인 save를 중간에 실행하게 됩니다. 여기에는 Transactional이 적용되어 있는데요. 이렇게 되면 &lt;b&gt;cafeRepository.save(cafe)&lt;/b&gt;를 실행했을 때 transaction이 시작하고 save 메소드가 끝날 때 transaction이 끝나게 됩니다.&lt;br /&gt;&lt;br /&gt;즉 cafeRepository의 save 작업이 끝나는 동시에 transaction commit이 발생하고 JPA에서는 flush가 발생하게 되어 쓰기지연 쿼리들이 실제 DB요청으로 날라가게 됩니다. DB에 쿼리를 실행하게 되는 것이므로 updatedAt에도 값이 담겨져서 데이터에 저장이 된 상태가 됩니다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;cafeRepository.save(cafe)&lt;/b&gt;의 인자로 들어간 cafe의 updatedAt에는 &lt;b&gt;AuditingEntityListener&lt;/b&gt;에 의해 현재날짜 값이 담겨질 것이고 그 아래의 log에 찍히게 되는 것이라 할 수 있습니다.&lt;/p&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&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;b&gt;  정리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 JPA에서는 쓰기 관련 쿼리(insert, update)들이 임시저장되어 있다가 flush 시점에서 한 번에 DB 요청을 보내게 됩니다. 이를 쓰기지연 쿼리 기능이라 하고 JPA에서 중요한 내용 중 하나입니다.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;flush는 여러 상황에서 발생할 수 있지만 그 중에서 transaction commit 될 때 flush가 발생합니다. 그래서 @Transactional이 있을 때와 없을 때의 상황을 통해 updatedAt이라는 데이터는 어떻게 담겨지는지를 보았던 것입니다. JPA에서 가장 기초적인 부분이기도 하고 이러한 부분들을 알고 개발하면 좋을 것 같네요.&lt;/p&gt;</description>
      <category>Spring/JPA</category>
      <category>Hibernate</category>
      <category>JPA</category>
      <category>Spring</category>
      <category>transaction</category>
      <author>beaniejoy</author>
      <guid isPermaLink="true">https://beaniejoy.tistory.com/91</guid>
      <comments>https://beaniejoy.tistory.com/91#entry91comment</comments>
      <pubDate>Fri, 23 Dec 2022 14:29:36 +0900</pubDate>
    </item>
    <item>
      <title>Classful IP Address 개념 정리</title>
      <link>https://beaniejoy.tistory.com/90</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;832&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cHewDO/btrTvjESGyN/JkWZhgEUTtQk7zzeoIL7Uk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cHewDO/btrTvjESGyN/JkWZhgEUTtQk7zzeoIL7Uk/img.png&quot; data-alt=&quot;Image by jeferrb from Pixabay&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cHewDO/btrTvjESGyN/JkWZhgEUTtQk7zzeoIL7Uk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcHewDO%2FbtrTvjESGyN%2FJkWZhgEUTtQk7zzeoIL7Uk%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;740&quot; height=&quot;481&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;832&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Image by jeferrb from Pixabay&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네트워크 공부하면서 알게된 Classful, Classless IP 주소에 대해서 정리해보고자 합니다. 우선 &lt;b&gt;Classful IP&lt;/b&gt; 주소에 대해서 알아봅시다.&lt;br /&gt;&lt;br /&gt;네트워크 OSI 7 Layer 중 3계층은 네트워크 계층으로, 주로 다른 네트워크 대역에 있는 컴퓨터 장비와 통신하기 위해 사용되는 프로토콜들이 있습니다. IP, ICMP, ARP등 여러 3계층 프로토콜들이 존재하는데요. 그 중에 IP 프로토콜에 대한 내용입니다.&lt;br /&gt;&lt;br /&gt;네트워크 통신을 하기 위해서 어떤 장비하고 통신하고 싶은지 구분할 수 있는 식별자가 있어야 합니다.&lt;br /&gt;예를 들어, 2계층 프로토콜인 이더넷(Ethernet) 프로토콜에서는 MAC 주소를 통해 컴퓨터 장비를 식별하고 이 주소를 통해 통신하기 원하는 컴퓨터 장비를 특정할 수 있게 됩니다.&lt;br /&gt;&lt;br /&gt;하지만 MAC 주소는 같은 네트워크 대역에 있는 컴퓨터 장비들끼리만 식별할 수 있기 때문에 인터넷과 같은 외부 네트워크로 통신하는데 사용될 수 없습니다. &lt;br /&gt;&lt;br /&gt;3계층 프로토콜인 IP 프로토콜은 이를 가능하게 해주는데요. IP 프로토콜에서 식별자로 사용되는 주소가 바로 &lt;b&gt;IP 주소(IP Address)&lt;/b&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;b&gt;  1. IP 주소&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IP 주소는 다른 네트워크 대역에서 데이터 통신할 수 있게 식별자 역할로서 사용됩니다. &lt;br /&gt;32 bit (4 bytes) 크기를 가지고 있고 표시는 다음과 같이 합니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;172.50.3.152&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;0.0.0.0&lt;/b&gt; 부터 &lt;b&gt;255.255.255.255&lt;/b&gt;까지 범위를 가지고 있고 네트워크와 연결되어 있는 각각의 컴퓨터 장비들은 안에서 고유 주소를 할당받게 됩니다.&lt;br /&gt;&lt;br /&gt;IP주소는 16진수로 표현되기도 하는데요. 보편적으로 10진수 표기법을 사용합니다.&lt;br /&gt;그리고 ip 주소를 표시할 때 각 유효한 숫자 앞에 &lt;b&gt;0&lt;/b&gt;을 기입하면 안됩니다. 예를 들어, 172.50.3.152 주소에서 3자리로 맞춘다고 172.050.003.152 이렇게 표기하면 안됩니다.&lt;/p&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&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;b&gt;  2. Classful IP?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IP주소는 처음 나왔을 때, 일종의 계층별로 ip주소를 나눠서 사용하였습니다. 이게 Classful IP Addres입니다. &lt;br /&gt;&lt;br /&gt;클래스는 5개로 나누어서 사용했고 A, B, C, D, E 클래스라는 명칭으로 나누었습니다.&lt;br /&gt;클래스별로 사용할 수 있는 주소 범위를 할당했는데요. 범위를 나누는 방식은 다음과 같습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Class A: 0_______.xxxxxxxx.xxxxxxxx.xxxxxxxx &lt;br /&gt;Class B: 10______.xxxxxxxx.xxxxxxxx.xxxxxxxx &lt;br /&gt;Class C: 110_____.xxxxxxxx.xxxxxxxx.xxxxxxxx &lt;br /&gt;Class D: 1110____.xxxxxxxx.xxxxxxxx.xxxxxxxx &lt;br /&gt;Class E: 1111____.xxxxxxxx.xxxxxxxx.xxxxxxxx&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 무슨 내용일까요,, 쉽게 말하자면 각 클래스마다 &lt;b&gt;2진수로 표현한 ip 주소 맨앞&lt;/b&gt;에 일정한 규칙을 가지고 값을 고정하고 있습니다.&lt;br /&gt;A 클래스부터 살펴보면 ip주소 맨 앞에 0을 고정해서 사용하겠다는 것이고, B 클래스는 10을, C 클래스는 110, D 클래스는 1110, 마지막으리로 E 클래스는 1111을 고정하겠다는 내용입니다.&lt;br /&gt;(이렇게 되면 각 클래스에서 사용할 수 있는 ip 주소 범위의 시작지점과 끝 지점을 알 수 있습니다.)&lt;br /&gt;&lt;br /&gt;위 내용을 정리하면 다음과 같습니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 104px;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 25%; height: 19px;&quot;&gt;&lt;b&gt;클래스 구분&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 19px;&quot;&gt;&lt;b&gt;주소 맨 앞의 고정값&lt;/b&gt;&lt;br /&gt;&lt;b&gt;(Leading Bits)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 19px;&quot;&gt;&lt;b&gt;시작 주소&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 19px;&quot;&gt;마지막 주소&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;&lt;b&gt;Class A&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;&lt;span style=&quot;background-color: #fcfcfc;&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;0 ***&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;0.0.0.0&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;127.255.255.255&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;&lt;b&gt;Class B&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;10 ***&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;128.0.0.0&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;191.255.255.255&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;&lt;b&gt;Class C&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;110 ***&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;192.0.0.0&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;223.255.255.255&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;&lt;b&gt;Class D&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;1110 ***&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;224.0.0.0&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;239.255.255.255&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;&lt;b&gt;Class E&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;1111 ***&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;240.0.0.0&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;255.255.255.255&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&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;b&gt;  3. Classful IP 주소에서 네트워크 대역 구분&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Classful IP 주소 체계에서는 ip 주소를 네트워크 대역을 구분하는 값과 호스트를 구분하는 값 두 부분으로 나누었습니다. ip 주소를 두 부분으로 쪼개서 하나는 &lt;b&gt;네트워크를 구분지을 수 있는 식별자&lt;/b&gt;로 다른 하나는 &lt;b&gt;해당 네트워크 안에 연결된 호스트(컴퓨터 장비들)들을 구분지을 수 있는 식별자&lt;/b&gt;로 사용했습니다.&lt;br /&gt;&lt;br /&gt;여기서 각 Class 마다 네트워크 식별자와 호스트 식별자 두 부분으로 나누는 경계지점이 다른데 이 부분은 제가 참고했던 블로그 그림 내용을 참조해보겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;715&quot; data-origin-height=&quot;375&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bAn4Mv/btrThexWr3Z/FshqmVx7TwpzFyUymUGgVK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bAn4Mv/btrThexWr3Z/FshqmVx7TwpzFyUymUGgVK/img.jpg&quot; data-alt=&quot;https://www.geeksforgeeks.org/introduction-of-classful-ip-addressing/&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bAn4Mv/btrThexWr3Z/FshqmVx7TwpzFyUymUGgVK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbAn4Mv%2FbtrThexWr3Z%2FFshqmVx7TwpzFyUymUGgVK%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;715&quot; height=&quot;375&quot; data-origin-width=&quot;715&quot; data-origin-height=&quot;375&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://www.geeksforgeeks.org/introduction-of-classful-ip-addressing/&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 그림에서 볼 수 있듯이 Classful IP 주소에서는 &lt;b&gt;점을 기준으로&lt;/b&gt; 네트워크 ID와 호스트 ID를 나누고 있습니다. &lt;br /&gt;&lt;br /&gt;ip 주소는 32 bit로 4 바이트이고 xxxx.xxxx.xxxx.xxxx 이렇게 점을 기준으로 4부분으로 나누어 표시합니다. A 클래스는 첫번째 byte 지점을 네트워크 구분하는데 사용하겠다는 것이고, B 클래스는 두 번째 지점까지, C 클래스는 세 번째 지점까지 사용하겠다는 내용입니다. &lt;br /&gt;&lt;br /&gt;D, E 클래스는 네트워크 ID를 따로 할당받지 않고 있는데요. 이 두 개 클래스는 reserved 클래스라고 해서 각각 특정 목적을 가지고 사용되는 주소라 따로 네트워크 구분값을 지정하지 않습니다.&lt;br /&gt;&lt;br /&gt;A 클래스를 사용해서 ip 주소를 한 번 만들어보겠습니다.&lt;br /&gt;A 클래스를 사용한 네트워크를 구성할 때 &lt;b&gt;0.0.0.0 ~ 127.255.255.255&lt;/b&gt; 범위를 가지고 ip 주소를 할당해야 합니다. 이 사이의 주소값 중 &lt;b&gt;120.0.0.xxx&lt;/b&gt; 을 선택했다고 해봅시다. 여기서 네트워크 식별자 값은 120이고, 120이라는 하나의 네트워크 대역 안에서 할당받을 수 있는 컴퓨터 장비들의 식별자 값은 0.0.1 ~ 255.255.254 범위 내에서 가능합니다. &lt;br /&gt;(하나의 네트워크 ID안의 호스트 ID중 모든 비트의 값이 0인 주소는 네트워크 주소, 모든 비트의 값이 1인 주소는 broadcast 주소로 할당하고 있기 때문에 &lt;b&gt;0.0.0과 255.255.255는 제외&lt;/b&gt;하였습니다.) &lt;br /&gt;&lt;br /&gt;개수로 따지면 &lt;b&gt;2의 24승 개(대략 1670만 개)&lt;/b&gt;정도 됩니다. 그 말인즉슨, A 클래스에서 하나의 네트워크에 할당가능한 컴퓨터 장비 대수는 1670만 개정도 가능하다는 얘기가 됩니다.&lt;br /&gt;&lt;br /&gt;여기서 제가 임의로 네트워크 내에 컴퓨터 장비 24대를 연결하고자 한다고 했을 때, 각각의 컴퓨터 장비들의 ip 주소는 120.0.0.1 ~ 120.0.0.24로 할당할 수 있을 것 같네요.&lt;/p&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&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;b&gt;  4. Classful IP Address의 한계&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ip 주소가 나왔을 때 적용했던 Classful한 방식의 ip 주소 체계에는 어떤 한계가 있을까요.&lt;br /&gt;&lt;br /&gt;방금 A 클래스에서 ip 주소를 할당하는 내용을 보시면 알 수 있습니다. 120.0.0.1 ~ 120.0.0.24 로 하나의 네트워크 대역에서 24대의 컴퓨터 장비에 ip 주소를 할당했었는데요. 이렇게 되면, 나머지 &lt;b&gt;120.0.0.25 ~ 120.255.255.254 까지의 주소는 그대로 낭비&lt;/b&gt;하게 됩니다.&lt;br /&gt;(120.255.255.255는 해당 네트워크 대역의 broadcast 주소로 사용될 것이라 제외하였습니다.)&lt;br /&gt;&lt;br /&gt;호스트 식별 영역이 가장 작은 C 클래스에서 조차 많은 낭비가 발생합니다. 192.100.0.1 ~ 192.100.0.24로 24대 장비에 ip 주소를 할당했으면 &lt;b&gt;192.100.0.25 ~ 192.100.0.254까지 229대 정도가 낭비&lt;/b&gt;되는 것입니다. (네트워크 식별자로 192.100.0 이 사용되었기에)&lt;br /&gt;&lt;br /&gt;ip 주소를 처음 사용하기 시작했을 때 당시에는 컴퓨터 자체가 지금처럼 보급이 안되던 시대라서 이러한 낭비에 대해서 전혀 상관하지 않았습니다. (상관할 필요조차 없었을 것 같습니다.)&lt;br /&gt;그런데 지금은 전세계적으로 컴퓨터는 각 가정당 하나씩, 심지어 개인당 하나씩 당연하게 가지고 있는 시대입니다. 여기에 IoT 기기까지 보급되면서 인터넷에 연결된 장비가 폭발적으로 증가했고 ip 주소가 부족한 상황에 놓여져 있습니다.&lt;br /&gt;&lt;br /&gt;이에 대한 보완책으로 &lt;b&gt;Classless IP Address&lt;/b&gt;가 나오게 되었는데요. 이부분은 다음 게시글에서 정리해보도록 하겠습니다.&lt;br /&gt;&lt;br /&gt;Classful IP Address에 대해 잘 정리한 해외 게시글이 있어서 공유드리겠습니다.&lt;br /&gt;&lt;a href=&quot;https://www.geeksforgeeks.org/introduction-of-classful-ip-addressing/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;https://www.geeksforgeeks.org/introduction-of-classful-ip-addressing/&lt;/span&gt;&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;figure data-ke-type=&quot;opengraph&quot; data-og-title=&quot;Introduction of Classful IP Addressing - GeeksforGeeks&quot; data-ke-align=&quot;alignCenter&quot; data-og-description=&quot;A Computer Science portal for geeks. It contains well written, well thought and well explained computer science and programming articles, quizzes and practice/competitive programming/company interview Questions.&quot; data-og-host=&quot;www.geeksforgeeks.org&quot; data-og-source-url=&quot;https://www.geeksforgeeks.org/introduction-of-classful-ip-addressing/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cuNlPY/hyQQz5qn22/HVqkP3vjGrLXv7Ak2nYVq0/img.png?width=200&amp;amp;height=200&amp;amp;face=0_0_200_200,https://scrap.kakaocdn.net/dn/8e1Dv/hyQRXDDEnN/SJt9osFU4rQ5erV4g3OST0/img.jpg?width=634&amp;amp;height=214&amp;amp;face=0_0_634_214,https://scrap.kakaocdn.net/dn/sJk9B/hyQQCuk89D/HM9ZQCUiyNpEuQrjL3BjRk/img.jpg?width=715&amp;amp;height=375&amp;amp;face=0_0_715_375&quot; data-og-url=&quot;https://www.geeksforgeeks.org/introduction-of-classful-ip-addressing/&quot;&gt;&lt;a href=&quot;https://www.geeksforgeeks.org/introduction-of-classful-ip-addressing/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.geeksforgeeks.org/introduction-of-classful-ip-addressing/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cuNlPY/hyQQz5qn22/HVqkP3vjGrLXv7Ak2nYVq0/img.png?width=200&amp;amp;height=200&amp;amp;face=0_0_200_200,https://scrap.kakaocdn.net/dn/8e1Dv/hyQRXDDEnN/SJt9osFU4rQ5erV4g3OST0/img.jpg?width=634&amp;amp;height=214&amp;amp;face=0_0_634_214,https://scrap.kakaocdn.net/dn/sJk9B/hyQQCuk89D/HM9ZQCUiyNpEuQrjL3BjRk/img.jpg?width=715&amp;amp;height=375&amp;amp;face=0_0_715_375');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Introduction of Classful IP Addressing - GeeksforGeeks&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;A Computer Science portal for geeks. It contains well written, well thought and well explained computer science and programming articles, quizzes and practice/competitive programming/company interview Questions.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.geeksforgeeks.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>CS/Network</category>
      <category>classful ip</category>
      <category>IPv4</category>
      <category>Network</category>
      <author>beaniejoy</author>
      <guid isPermaLink="true">https://beaniejoy.tistory.com/90</guid>
      <comments>https://beaniejoy.tistory.com/90#entry90comment</comments>
      <pubDate>Mon, 12 Dec 2022 22:41:12 +0900</pubDate>
    </item>
    <item>
      <title>[Git] 다른 프로젝트의 원격 repository에서 특정 branch 내용 merge 하기</title>
      <link>https://beaniejoy.tistory.com/89</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;강의 수강을 위해 v1 내용을 v2로 옮겨야 하는 상황이 있었는데 git 기능으로 해결했던 내용 기록하고자 하는 내용입니다.&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;b&gt;  1. 원격 repository 추가 등록&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;project-v1&lt;/b&gt;의 특정 branch(beanie)에서 , &lt;b&gt;project-v2&lt;/b&gt;의 특정 branch(beanie)로 merge해서 합병해야 하는 상황입니다.&lt;br /&gt;이를 위해 먼저 옮기고자 하는 내용이 있는 project-v1 원격 branch를 가져와야 합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1670166762621&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;project-v2&amp;gt; git remote -v
origin  git@github.com:[account]/project-v2.git (fetch)
origin  git@github.com:[account]/project-v2.git (push)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 remote에 origin 이름으로 project-v2 repository가 등록되어 있습니다. 여기에 project-v1 repository를 다른 remote 이름으로 등록해야 합니다. 그래야 해당 remote 이름으로 fetch를 받아 merge를 해볼 수 있을 겁니다.&lt;/p&gt;
&lt;pre id=&quot;code_1670166984194&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;project-v2&amp;gt; git remote add temp git@github.com:[account]/project-v1.git
project-v2&amp;gt; git remote -v
origin  git@github.com:[account]/project-v2.git (fetch)
origin  git@github.com:[account]/project-v2.git (push)
temp    git@github.com:[account]/project-v1.git (fetch)
temp    git@github.com:[account]/project-v1.git (push)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;temp라는 이름으로 remote가 추가 등록된 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;div&gt;
&lt;script src=&quot;https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-8328273903441798&quot;&gt;&lt;/script&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&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;b&gt;  2. 전혀 관련 없는 repository merge fatal 이슈&lt;/b&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1670167169127&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;project-v2&amp;gt; git fetch temp
remote: Enumerating objects: 495, done.
remote: Counting objects: 100% (495/495), done.
remote: Compressing objects: 100% (237/237), done.
remote: Total 495 (delta 171), reused 434 (delta 110), pack-reused 0
Receiving objects: 100% (495/495), 492.23 KiB | 687.00 KiB/s, done.
Resolving deltas: 100% (171/171), done.
From github.com:[account]/project-v1
 * [new branch]      beanie     -&amp;gt; temp/beanie
 * [new branch]      main     	-&amp;gt; temp/main
 * [new branch]      other      -&amp;gt; temp/other&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;fetch로 remote temp의 모든 원격 branch들을 가져옵니다. merge하려는 target인 &lt;b&gt;beanie&lt;/b&gt; 브랜치가 &lt;b&gt;temp/beanie&lt;/b&gt;로 잘 가져온 것을 확인 할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1670167251091&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;project-v2&amp;gt; git checkout -b beanie&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;merge 하기 전에 project-v2에도 beanie 브랜치를 하나 만들어줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1670167353313&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;project-v2&amp;gt;beanie&amp;gt; git merge temp/beanie
fatal: refusing to merge unrelated histories&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타겟으로 하고 있는 temp/beanie를 project-v2의 beanie로 merge를 시도했는데 error가 나옵니다.&lt;br /&gt;내용을 확인해보면 &lt;b&gt;연관이 없는 이력(unrelated histories)을 merge하는 것을 금지&lt;/b&gt;한다고 나오네요.&lt;br /&gt;&lt;br /&gt;해당 내용은 구글링하면 바로 나옵니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2022-12-05 at 12.25.54 AM.png&quot; data-origin-width=&quot;1158&quot; data-origin-height=&quot;988&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bNvA0k/btrSI8Z9eLQ/feMD4hktp79RFYobJsXzFK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bNvA0k/btrSI8Z9eLQ/feMD4hktp79RFYobJsXzFK/img.png&quot; data-alt=&quot;출처: https://www.educative.io/answers/the-fatal-refusing-to-merge-unrelated-histories-git-error&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bNvA0k/btrSI8Z9eLQ/feMD4hktp79RFYobJsXzFK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbNvA0k%2FbtrSI8Z9eLQ%2FfeMD4hktp79RFYobJsXzFK%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;600&quot; height=&quot;512&quot; data-filename=&quot;Screenshot 2022-12-05 at 12.25.54 AM.png&quot; data-origin-width=&quot;1158&quot; data-origin-height=&quot;988&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처: https://www.educative.io/answers/the-fatal-refusing-to-merge-unrelated-histories-git-error&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.educative.io/answers/the-fatal-refusing-to-merge-unrelated-histories-git-error&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;The &amp;ldquo;fatal: refusing to merge unrelated histories&amp;rdquo; Git error&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1670167797051&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;The &amp;ldquo;fatal: refusing to merge unrelated histories&amp;rdquo; Git error&quot; data-og-description=&quot;Contributor: Educative Answers Team&quot; data-og-host=&quot;www.educative.io&quot; data-og-source-url=&quot;https://www.educative.io/answers/the-fatal-refusing-to-merge-unrelated-histories-git-error&quot; data-og-url=&quot;https://www.educative.io/answers/the-fatal-refusing-to-merge-unrelated-histories-git-error&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/yhSFu/hyQMCOpR6q/lmQXYmwJV6oiZDKPlQFjO0/img.png?width=1667&amp;amp;height=1667&amp;amp;face=0_0_1667_1667&quot;&gt;&lt;a href=&quot;https://www.educative.io/answers/the-fatal-refusing-to-merge-unrelated-histories-git-error&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.educative.io/answers/the-fatal-refusing-to-merge-unrelated-histories-git-error&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/yhSFu/hyQMCOpR6q/lmQXYmwJV6oiZDKPlQFjO0/img.png?width=1667&amp;amp;height=1667&amp;amp;face=0_0_1667_1667');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;The &amp;ldquo;fatal: refusing to merge unrelated histories&amp;rdquo; Git error&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Contributor: Educative Answers Team&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.educative.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 글을 보면 전혀 관련없는 커밋 이력을 가진 repository들을 가지고 merge를 하려고하면 git은 error를 반환한다고 알려주고 있는데요. 아무래도 merge하려는 두 개의 서로 다른 repository 간에 커밋 이력 연결점을 찾을 수 없기 때문에 기본적으로 error 처리한 것 같습니다. (개인적인 생각)&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;You have created a new repository, added a few&amp;nbsp;commits&amp;nbsp;to it, and now you are trying to&amp;nbsp;pull&amp;nbsp;from a remote repository that already has some commits of its own. Git will also throw the error in this case, since it has no idea how the two projects are related.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저같은 경우 이 상황에 해당한다고 볼 수 있겠네요. &lt;br /&gt;새로운 프로젝트(project-v2)를 만들고 beanie 브랜치 만든 상황에서 커밋을 한 이력이 있었고 다른 원격 브랜치를 pull할 때 발생한 에러라고 볼 수 있습니다.(pull은 기본적으로 fetch &amp;gt; merge 과정을 한 번에 수행하기에)&lt;/p&gt;
&lt;div&gt;
&lt;script src=&quot;https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-8328273903441798&quot;&gt;&lt;/script&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&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;b&gt;  3. 해결책&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해결책은 위의 첨부해드린 글에서 나와있는대로 간단하게 해결 가능합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1670168173483&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;project-v2&amp;gt;beanie&amp;gt; git merge temp/beanie --allow-unrelated-histories&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;&lt;b&gt;--allow-unrelated-histories&lt;/b&gt;&lt;/span&gt; 해당 옵션을 넣어서 merge 해주면 됩니다.&lt;br /&gt;전혀 관련 없는 repository에서 merge를 한 것이기 때문에 merge conflict가 많이 나오긴 했었는데 적절하게 잘 resolve하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원하는 branch에 무조건 merge하고자 할 때 사용하시면 될 것 같고 실무에서는 사용하지 않는 것으로,,,&lt;br /&gt;(실무에서는 있어서는 안될 상황이긴 합니다.)&lt;/p&gt;</description>
      <category>Git</category>
      <category>Git</category>
      <category>merge</category>
      <author>beaniejoy</author>
      <guid isPermaLink="true">https://beaniejoy.tistory.com/89</guid>
      <comments>https://beaniejoy.tistory.com/89#entry89comment</comments>
      <pubDate>Mon, 5 Dec 2022 00:40:03 +0900</pubDate>
    </item>
    <item>
      <title>[JPA] Hibernate dialect와 H2 데이터베이스 호환 이슈</title>
      <link>https://beaniejoy.tistory.com/88</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;김영한님 JPA 강의 듣다가 &lt;code&gt;hibernate.dialect&lt;/code&gt;내용을 바꿔서 코드 실행을 하는 부분이 있었는데 이 과정에 겪었던 에러 이슈와 처리했던 내용을 기억하고자 이 곳에 기록하게 되었습니다.&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h1&gt;  문제 상황&lt;/h1&gt;
&lt;pre id=&quot;code_1669117649598&quot; class=&quot;go&quot; data-ke-language=&quot;go&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;properties&amp;gt;
    &amp;lt;property name=&quot;javax.persistence.jdbc.driver&quot; value=&quot;org.h2.Driver&quot;/&amp;gt;
    &amp;lt;property name=&quot;javax.persistence.jdbc.user&quot; value=&quot;sa&quot;/&amp;gt;
    &amp;lt;property name=&quot;javax.persistence.jdbc.password&quot; value=&quot;&quot;/&amp;gt;
    &amp;lt;property name=&quot;javax.persistence.jdbc.url&quot; value=&quot;jdbc:h2:tcp://localhost/~/test&quot;/&amp;gt;
    &amp;lt;property name=&quot;hibernate.dialect&quot; value=&quot;org.hibernate.dialect.H2Dialect&quot;/&amp;gt;
    
    &amp;lt;property name=&quot;hibernate.show_sql&quot; value=&quot;true&quot;/&amp;gt;
    &amp;lt;property name=&quot;hibernate.format_sql&quot; value=&quot;true&quot;/&amp;gt;
    &amp;lt;property name=&quot;hibernate.use_sql_comments&quot; value=&quot;true&quot;/&amp;gt;
    &amp;lt;property name=&quot;hibernate.hbm2ddl.auto&quot; value=&quot;create&quot; /&amp;gt;
&amp;lt;/properties&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;jpa 관련 설정은 위의 내용과 같습니다. h2 database를 사용했기 때문에 h2 driver와 db dialect(방언)는 &lt;code&gt;H2Dialect&lt;/code&gt;로 하였습니다.&lt;br /&gt;이렇게 설정하고 jpa 코드 실행하면 아무 문제 없이 잘 수행합니다.&lt;br /&gt;&lt;br /&gt;여기서 oracle 쿼리는 어떻게 적용이 되는지 보기 위해 dialect 설정을 &lt;code&gt;Oracle12Dialect&lt;/code&gt;로 변경했는데요. 여기서 문제가 발생합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2022-11-22 at 8.41.14 PM.png&quot; data-origin-width=&quot;2746&quot; data-origin-height=&quot;1784&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eE7IvA/btrROrT2OMs/fsZ19ZWyc59fk52MOTWekk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eE7IvA/btrROrT2OMs/fsZ19ZWyc59fk52MOTWekk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eE7IvA/btrROrT2OMs/fsZ19ZWyc59fk52MOTWekk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeE7IvA%2FbtrROrT2OMs%2FfsZ19ZWyc59fk52MOTWekk%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;2746&quot; height=&quot;1784&quot; data-filename=&quot;Screenshot 2022-11-22 at 8.41.14 PM.png&quot; data-origin-width=&quot;2746&quot; data-origin-height=&quot;1784&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;ddl-auto&lt;/code&gt;를 &lt;code&gt;create&lt;/code&gt;로 설정한 상황에서 코드 실행하면 JPA Entity 내용대로 drop, create 순으로 쿼리가 동작하게 됩니다.&lt;br /&gt;여기서 위와 같은 에러가 발생했는데요. 대략 create 문의 query syntax 오류가 발생했다고 나옵니다.&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h1&gt;  해결방법&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해결방법은 간단합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1669117719184&quot; class=&quot;go&quot; data-ke-language=&quot;go&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;property name=&quot;javax.persistence.jdbc.url&quot; value=&quot;jdbc:h2:tcp://localhost/~/test;Mode=Oracle&quot;/&amp;gt;
&amp;lt;property name=&quot;hibernate.dialect&quot; value=&quot;org.hibernate.dialect.Oracle12cDialect&quot;/&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 jdbc url에서 맨 끝에 &lt;code&gt;;Mode=Oracle&lt;/code&gt;를 추가해주면 됩니다.&lt;br /&gt;h2 database document를 보면 compatibility 내용이 있는 것을 볼 수 있습니다.&lt;br /&gt;(&lt;a href=&quot;http://www.h2database.com/html/features.html#compatibility&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;h2 database compatibility&lt;/a&gt;)&lt;/p&gt;
&lt;figure id=&quot;og_1669117756983&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Features&quot; data-og-description=&quot;&amp;nbsp; Features Feature List H2 in Use Connection Modes Database URL Overview Connecting to an Embedded (Local) Database In-Memory Databases Database Files Encryption Database File Locking Opening a Database Only if it Already Exists Closing a Database Ignore &quot; data-og-host=&quot;www.h2database.com&quot; data-og-source-url=&quot;http://www.h2database.com/html/features.html#compatibility&quot; data-og-url=&quot;http://www.h2database.com/html/features.html#compatibility&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dkgIjA/hyQE5vKCuE/iHzWYbqK5AhVuyfFPDtp6k/img.png?width=806&amp;amp;height=480&amp;amp;face=0_0_806_480,https://scrap.kakaocdn.net/dn/v8HAA/hyQE9StjBe/ggoBcmDR9x6ebj3KAkM4f1/img.png?width=752&amp;amp;height=436&amp;amp;face=0_0_752_436,https://scrap.kakaocdn.net/dn/btaUAr/hyQEZ99qVv/QGEBcZPEgTZbYV45Lo5YIk/img.png?width=416&amp;amp;height=519&amp;amp;face=112_213_310_411&quot;&gt;&lt;a href=&quot;http://www.h2database.com/html/features.html#compatibility&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;http://www.h2database.com/html/features.html#compatibility&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dkgIjA/hyQE5vKCuE/iHzWYbqK5AhVuyfFPDtp6k/img.png?width=806&amp;amp;height=480&amp;amp;face=0_0_806_480,https://scrap.kakaocdn.net/dn/v8HAA/hyQE9StjBe/ggoBcmDR9x6ebj3KAkM4f1/img.png?width=752&amp;amp;height=436&amp;amp;face=0_0_752_436,https://scrap.kakaocdn.net/dn/btaUAr/hyQEZ99qVv/QGEBcZPEgTZbYV45Lo5YIk/img.png?width=416&amp;amp;height=519&amp;amp;face=112_213_310_411');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Features&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; Features Feature List H2 in Use Connection Modes Database URL Overview Connecting to an Embedded (Local) Database In-Memory Databases Database Files Encryption Database File Locking Opening a Database Only if it Already Exists Closing a Database Ignore&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.h2database.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 설정없이 잘 동작했던 것이 지금은 에러가 발생하는 것으로 보아서 h2 버전업되면서 DB간 호환 관련 설정이 추가된 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 내용으로 설정하고 코드를 다시 실행하면 잘 동작하는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2022-11-22 at 8.50.08 PM.png&quot; data-origin-width=&quot;2126&quot; data-origin-height=&quot;1710&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cj4RDR/btrRSPyVW0V/g3bdDElwTe8KodGJxLCZUK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cj4RDR/btrRSPyVW0V/g3bdDElwTe8KodGJxLCZUK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cj4RDR/btrRSPyVW0V/g3bdDElwTe8KodGJxLCZUK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcj4RDR%2FbtrRSPyVW0V%2Fg3bdDElwTe8KodGJxLCZUK%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;2126&quot; height=&quot;1710&quot; data-filename=&quot;Screenshot 2022-11-22 at 8.50.08 PM.png&quot; data-origin-width=&quot;2126&quot; data-origin-height=&quot;1710&quot;/&gt;&lt;/span&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;이를 응용해서 MySQL dialect로 하고 싶으면 다음과 같이 설정하면 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1669117919825&quot; class=&quot;go&quot; data-ke-language=&quot;go&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;property name=&quot;javax.persistence.jdbc.url&quot; value=&quot;jdbc:h2:tcp://localhost/~/test;Mode=MySQL&quot;/&amp;gt;
&amp;lt;property name=&quot;hibernate.dialect&quot; value=&quot;org.hibernate.dialect.MySQL8Dialect&quot;/&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Spring/JPA</category>
      <category>db</category>
      <category>H2</category>
      <category>JPA</category>
      <category>Spring</category>
      <author>beaniejoy</author>
      <guid isPermaLink="true">https://beaniejoy.tistory.com/88</guid>
      <comments>https://beaniejoy.tistory.com/88#entry88comment</comments>
      <pubDate>Tue, 22 Nov 2022 20:53:50 +0900</pubDate>
    </item>
    <item>
      <title>[Spring Security] API 방식의 인증 프로세스 개발해보기(custom filter, provider 적용)</title>
      <link>https://beaniejoy.tistory.com/87</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;security.jpg&quot; data-origin-width=&quot;7952&quot; data-origin-height=&quot;5304&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/btPkdt/btrRqFFcDX7/e4SeKfVvvhZdUyoEi52OB1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/btPkdt/btrRqFFcDX7/e4SeKfVvvhZdUyoEi52OB1/img.jpg&quot; data-alt=&quot;Photo by FLY:D on Unsplash&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/btPkdt/btrRqFFcDX7/e4SeKfVvvhZdUyoEi52OB1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbtPkdt%2FbtrRqFFcDX7%2Fe4SeKfVvvhZdUyoEi52OB1%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;7952&quot; height=&quot;5304&quot; data-filename=&quot;security.jpg&quot; data-origin-width=&quot;7952&quot; data-origin-height=&quot;5304&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Photo by FLY:D on Unsplash&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;br /&gt;저번 게시글에서 Spring Security의 기본 인증 방식인 form login 인증 방식의 간략한 프로세스를 정리해보았습니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;a href=&quot;https://beaniejoy.tistory.com/86&quot;&gt;Spring Security의 인증 프로세스 정리(form login 인증 방식)&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1668666446211&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Spring Security의 인증 프로세스 정리(form login 인증 방식)&quot; data-og-description=&quot;개인 프로젝트 하면서 적용했던 내용을 정리해보는 글입니다. 이번 내용은 로그인(인증) 프로세스를 순수 Spring Security만을 가지고 개발해보았던 내용을 두 번에 나누어 정리해보고자 합니다. 이&quot; data-og-host=&quot;beaniejoy.tistory.com&quot; data-og-source-url=&quot;https://beaniejoy.tistory.com/86&quot; data-og-url=&quot;https://beaniejoy.tistory.com/86&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dLLyfL/hyQB3qsD1j/MzpCAXG4qhoSJQRgkpk7b0/img.jpg?width=800&amp;amp;height=545&amp;amp;face=0_0_800_545,https://scrap.kakaocdn.net/dn/GepaA/hyQA8NIiQE/3EH6ybAXsvkpSUKBmZx780/img.jpg?width=800&amp;amp;height=545&amp;amp;face=0_0_800_545,https://scrap.kakaocdn.net/dn/cMclvD/hyQA5wFcIZ/Xe2OjMme8fxhxNSd08yxfK/img.png?width=1728&amp;amp;height=1768&amp;amp;face=0_0_1728_1768&quot;&gt;&lt;a href=&quot;https://beaniejoy.tistory.com/86&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://beaniejoy.tistory.com/86&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dLLyfL/hyQB3qsD1j/MzpCAXG4qhoSJQRgkpk7b0/img.jpg?width=800&amp;amp;height=545&amp;amp;face=0_0_800_545,https://scrap.kakaocdn.net/dn/GepaA/hyQA8NIiQE/3EH6ybAXsvkpSUKBmZx780/img.jpg?width=800&amp;amp;height=545&amp;amp;face=0_0_800_545,https://scrap.kakaocdn.net/dn/cMclvD/hyQA5wFcIZ/Xe2OjMme8fxhxNSd08yxfK/img.png?width=1728&amp;amp;height=1768&amp;amp;face=0_0_1728_1768');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Spring Security의 인증 프로세스 정리(form login 인증 방식)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;개인 프로젝트 하면서 적용했던 내용을 정리해보는 글입니다. 이번 내용은 로그인(인증) 프로세스를 순수 Spring Security만을 가지고 개발해보았던 내용을 두 번에 나누어 정리해보고자 합니다. 이&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;beaniejoy.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;해당 게시글에서 정리했던 내용들을 토대로 이번에는 api 인증 방식에 대해서 Spring Security를 이용해 구현해보고자 합니다.&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;api 방식의 인증 프로세스를 구현하기 위해서는 기존의 Spring Security form login 방식으로는 적용할 수 없습니다. 왜냐하면 이전 게시글에서도 정리했듯이 &lt;code&gt;UsernamePasswordAuthenticationFilter&lt;/code&gt;의 &lt;code&gt;username&lt;/code&gt;, &lt;code&gt;password&lt;/code&gt;라는 parameter를 받아서 처리하는 부분은 api 방식과 맞지 않기 때문입니다. &lt;b&gt;request body에 JSON 형태로 담겨져서 들어온 요청 내용에 대해서 처리할 수 있는 Filter를 따로 만드는 것이 중요합니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&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;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;  1. API 전용 인증 Filter 적용(ApiAuthenticationFilter)&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;이제 API 전용 custom한 인증 Filter를 만들어서 Spring Security 설정에다가 적용해보겠습니다. Filter는 &lt;code&gt;UsernamePasswordAuthenticationFilter&lt;/code&gt;의 추상클래스인 &lt;code&gt;AbstractAuthenticationProcessingFilter&lt;/code&gt;를 상속받아서 구현하면 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1668666729321&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class ApiAuthenticationFilter(requestMatcher: AntPathRequestMatcher) :
    AbstractAuthenticationProcessingFilter(requestMatcher) {

    private val objectMapper = jacksonObjectMapper()

    override fun attemptAuthentication(
        request: HttpServletRequest,
        response: HttpServletResponse,
    ): Authentication {
        if (isValidRequest(request).not()) {
            throw IllegalStateException(&quot;request is not supported. check request method and content-type&quot;)
        }

        val signInRequest = objectMapper.readValue(request.reader, SignInRequest::class.java)
        request.setAttribute(&quot;email&quot;, signInRequest.email)

        val token = signInRequest.let {
            if (StringUtils.hasText(it.email).not() || StringUtils.hasText(it.password).not()) {
                throw IllegalArgumentException(&quot;Email &amp;amp; Password are not empty!!&quot;)
            }

            UsernamePasswordAuthenticationToken(it.email, it.password)
        }

        return authenticationManager.authenticate(token)
    }

    private fun isValidRequest(request: HttpServletRequest): Boolean {
        if (request.method != HttpMethod.POST.name) {
            return false
        }

        if (request.contentType != MediaType.APPLICATION_JSON_VALUE) {
            return false
        }

        return true
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;code&gt;attemptAuthentication&lt;/code&gt; 메소드를 overriding해서 원하는 방식으로 구현하면 됩니다. 위에 작성한 코드는 예시기 때문에 참고만 하시면 될 것 같습니다. 위 코드 프로세스를 살펴보면 다음과 같습니다.&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;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;code&gt;isValidRequest&lt;/code&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;request의 method가 &lt;code&gt;POST&lt;/code&gt; 방식이면서 &lt;code&gt;Content-Type&lt;/code&gt;이 JSON 타입인지 체크합니다. 만약 해당 내용과 맞지않는 request라면 에러를 반환하게 됩니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;code&gt;ObjectMapper&lt;/code&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;kotlin module을 적용한 ObjectMapper를 이용해 request의 body내용을 원하는 형식의 DTO 클래스로 변환합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;code&gt;Authentication&lt;/code&gt; 객체로 변환&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;email, password내용이 잘 들어왔는지 체크한 후 &lt;code&gt;UsernamePasswordAuthenticationToken&lt;/code&gt; 객체를 생성합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;code&gt;AuthenticationManager&lt;/code&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;authenticate 진행을 위해 Manager에 이전 단계에서 생성했던 Authentication 객체를 담아 다음 인증과정을 진행합니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;여기서 중요한 것은 어떤 api와 해당 Filter를 매핑할 것인지를 설정해야합니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;code&gt;UsernamePasswordAuthenticationFilter&lt;/code&gt;도 어떤 url과 매핑할 것인지 설정하는 부분이 있는데요.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1668666844159&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

    private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER 
    	= new AntPathRequestMatcher(&quot;/login&quot;, &quot;POST&quot;);
            
    //...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;위와 같이 &lt;code&gt;POST&lt;/code&gt; method의 &lt;code&gt;/login&lt;/code&gt; url에 대해서 해당 Filter가 받겠다는 설정내용이 있습니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;제가 구현한 &lt;code&gt;ApiAuthenticationFilter&lt;/code&gt;도 이와 같이 method와 인증 api url을 매핑하는 설정이 필요합니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;이 부분은 생성자로 &lt;code&gt;AntPathRequestMatcher&lt;/code&gt;를 인자로 받아서 설정할 예정인데요. &lt;b&gt;Spring Security&lt;/b&gt; 설정 부분에서 다시 언급하도록 하겠습니다.&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;그 다음은 실제 인증을 수행할 &lt;code&gt;AuthenticationProvider&lt;/code&gt;를 구현해서 custom Provider를 생성해보겠습니다.&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&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;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;  2. Custom Provider 적용(ApiAuthenticationProvider)&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;code&gt;AuthenticationProvider&lt;/code&gt;는 실제 인증 처리를 담당하는 곳입니다. Spring Security에서는 &lt;code&gt;AbstractUserDetailsAuthenticationProvider&lt;/code&gt;에서 &lt;code&gt;AuthenticationProvider&lt;/code&gt;를 구현해서 인증작업을 처리했었는데요. 이부분도 따로 custom Provider를 구현해서 적용해보고자 합니다.&lt;br /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1668667180744&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
class ApiAuthenticationProvider(
    private val userDetailsService: UserDetailsService,
    private val passwordEncoder: PasswordEncoder
) : AuthenticationProvider {
    companion object: KLogging()

    override fun authenticate(authentication: Authentication): Authentication {
        logger.info { &quot;start authentication&quot; }

        val email = authentication.name
        val password = authentication.credentials as String?

        val user = userDetailsService.loadUserByUsername(email)
        if (!passwordEncoder.matches(password, user.password)) {
            throw BadCredentialsException(&quot;Input password does not match stored password&quot;)
        }

        logger.info { &quot;User password ${user.password}&quot; }

        // password null로 반환
        return UsernamePasswordAuthenticationToken(user, null, user.authorities)
    }

    override fun supports(authentication: Class&amp;lt;*&amp;gt;): Boolean {
        return UsernamePasswordAuthenticationToken::class.java.isAssignableFrom(authentication)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&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;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;code&gt;email&lt;/code&gt;, &lt;code&gt;password&lt;/code&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Filter에서 생성했던 Authentication 객체를 전달받아서 &lt;code&gt;email&lt;/code&gt;, &lt;code&gt;password&lt;/code&gt;를 추출합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;code&gt;UserDetailsService&lt;/code&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;여기서 email 값을 가지고 User 객체를 조회합니다. 이 부분은 DB 연동을 통해 따로 구현 클래스를 적용해보고자 합니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;(아래에서 자세히 설명하겠습니다.)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;password 비교&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;PasswordEncoder 객체를 통해서 조회해온 User 객체의 password 값과 request로 들어온 password 값을 비교합니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;여기서 불일치가 발생하면 Spring Security 내용대로 &lt;code&gt;BadCredentialsException&lt;/code&gt; 예외를 던지도록 하였습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Authentication 객체 반환&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;유저 조회 및 패스워드 검증 과정을 모두 통과하게 되면 해당 User객체를 &lt;code&gt;Authetication&lt;/code&gt; 객체(&lt;code&gt;UsernamePasswordAuthenticationToken&lt;/code&gt;)에 담아 최종 반환하게 됩니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;여기서 반환할 때 &lt;code&gt;credentials&lt;/code&gt;부분에 password 값을 담아야하는데 이미 인증 완료된 객체에 대해서는 null 값을 넣어줍니다. 이미 인증 성공된 객체이기 때문에 불필요하게 password 값을 담을 필요가 없기 때문입니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;추가로 &lt;code&gt;supports&lt;/code&gt; 메소드도 구현을 해야합니다. &lt;code&gt;AuthenticationManager&lt;/code&gt;(&lt;code&gt;ProviderManager&lt;/code&gt;)에서 여러 Provider들 중 인증 처리할 수 있는 Authentication 타입인지 아닌지 체크해서 수행가능한 타입이면 해당 Provider에게 인증처리를 넘기는 로직이 있는데요. 이를 위해 &lt;b&gt;supports&lt;/b&gt; 메소드에 처리할 수 있는 Authentication type을 지정해야 합니다.&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;code&gt;UsernamePasswordAuthenticationToken&lt;/code&gt;객체를 custom Filter에서 반환하고 있기 때문에 여기서는 해당 클래스 타입을 지원하도록 설정하였습니다.&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&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;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;  3. UserDetailsService 구현&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;위의 Provider에서 인증 과정을 수행하기 위해 User를 받아와야 하는데 조건에 맞는 User 객체를 조회해서 반환해주는 곳이 &lt;code&gt;UserDetailsService&lt;/code&gt; 인터페이스입니다. 해당 인터페이스의 구현체를 직접 만들어서 적용해보았습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1668667363766&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component(&quot;userDetailsService&quot;)
class UserDetailsServiceImpl(
    private val memberRepository: MemberRepository
) : UserDetailsService {
    companion object: KLogging()

    override fun loadUserByUsername(email: String): UserDetails {
        return memberRepository.findByEmail(email)?.let {
            logger.info { &quot;[LOAD MEMBER] email: ${it.email}, role: ${it.roleType}, activated: ${it.activated}&quot; }
            createSecurityUser(it)
        } ?: throw UsernameNotFoundException(email)
    }

    private fun createSecurityUser(member: Member): User {
        if (member.activated.not()) {
            throw MemberDeactivatedException(member.email)
        }

        return User(
            /* username = */ member.email,
            /* password = */ member.password,
            /* authorities = */ listOf(SimpleGrantedAuthority(member.roleType.name))
        )
    }
}&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 style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;code&gt;loadUserByUsername&lt;/code&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;메소드를 구현해야 하는데요. 여기서 JPA를 통해 DB와 연동하여 테이블에 있는 유저 데이터를 조회하고 UserDetails 구현체인 User 객체를 생성해서 반환해줍니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;code&gt;UsernameNotFoundException&lt;/code&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;DB 테이블에서 email로 조건 조회했을 때 데이터가 없는 경우 던지는 예외입니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;code&gt;MemberDeactivatedException&lt;/code&gt;&lt;br /&gt;&lt;span&gt;Member Entity에서&lt;b&gt;&lt;span&gt; activated&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;를 통해 활성화 여부를 체크하게 됩니다. &lt;br /&gt;비활성화(탈퇴 등의 이유로)된 맴버에 대해서 인증 시도시 따로 만들어둔 예외를 던지게 했습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;code&gt;SimpleGrantedAuthority&lt;/code&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;조회한 Member가 부여받은 권한에 대한 정보를 User 객체에 전달하기 위해 생성하는 객체입니다. &lt;br /&gt;여기서 여러 개 부여 받은 권한을 list로 전달할 수 도 있고 혹은 권한별로 레벨이 존재해서 상위, 하위로 인가 구분이 가능하다면 하나의 권한만 전달할 수도 있을 것 같습니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;(이 부분은 권한에 대한 인가 부분을 구현하는 사람이 어떻게 구현하느냐에 따라 나뉠 것 같네요.)&lt;br /&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;  4. AuthenticationSuccessHandler, AuthenticationFailureHandler 구현&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;위의 내용들은 api 방식의 인증 프로세스 내용을 적용한 것들이었습니다. 이번에는 인증 과정을 모두 통과한 경우와 인증을 통과하지 못한 경우에 대해서 처리해주는 handler를 구현해보고자 합니다.&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;위에서 언급했던 &lt;code&gt;AbstractAuthenticationProcessingFilter&lt;/code&gt;에서는 요청이 들어왔을 때 request body에서 인증을 위한 &lt;b&gt;email&lt;/b&gt;, &lt;b&gt;password&lt;/b&gt;를 추출하고 Manager에게 인증 위임을 하고 그 결과(인증 성공시 &lt;code&gt;Authentication&lt;/code&gt; 객체 반환)를 받아오게 됩니다.&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;인증 성공하게 되면 &lt;code&gt;successfulAuthentication&lt;/code&gt;를 통해 인증 성공에 대한 handler를 호출하게 됩니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;인증 실패시 &lt;code&gt;unsuccessfulAuthentication&lt;/code&gt;를 통해 인증 실패에 대한 handler를 호출하게 됩니다.&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;  4-1. SuccessHandler custom 구현체&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1668667432637&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
class ApiAuthenticationSuccessHandler :AuthenticationSuccessHandler {
    private val objectMapper = jacksonObjectMapper()

    companion object: KLogging()

    override fun onAuthenticationSuccess(
        request: HttpServletRequest,
        response: HttpServletResponse,
        authentication: Authentication,
    ) {
        val user = authentication.principal as User
        logger.info { &quot;[AUTH SUCCESS] email: ${user.username}, authorities: ${user.authorities}&quot; }

        response.apply {
            this.status = HttpStatus.OK.value()
            this.contentType = MediaType.APPLICATION_JSON_VALUE
        }

        objectMapper.writeValue(
            response.writer,
            AuthenticationResult(
                email = user.username,
                authorities = user.authorities,
                msg = &quot;authentication success&quot;
            )
        )
    }
}&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 style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;code&gt;onAuthenticationSuccess&lt;/code&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;인증 성공시 해당 메소드 인자로 인증 성공한 &lt;code&gt;Authentication&lt;/code&gt; 객체를 받게 됩니다. api 방식에 맞게 인증 성공한 객체를 가지고 json 형태로 반환하면 됩니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;code&gt;response&lt;/code&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;성공하였기 때문에 &lt;code&gt;200 OK&lt;/code&gt; 응답코드를 설정하고 응답데이터에 대한 타입으로 &lt;code&gt;application/json&lt;/code&gt;을 설정합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;응답데이터&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;code&gt;ObjectMapper&lt;/code&gt;를 통해서 인증 성공한 &lt;code&gt;Authentication&lt;/code&gt; 객체 정보를 json 형태로 변환해서 response &lt;code&gt;PrintWriter&lt;/code&gt;에 담아주면 됩니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;

&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;  4-2. FailureHandler custom 구현체&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;위의 &lt;b&gt;SuccessHandler&lt;/b&gt;와 거의 유사합니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1668667468009&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
class ApiAuthenticationFailureHandler : AuthenticationFailureHandler {
    private val objectMapper = jacksonObjectMapper()

    companion object : KLogging()

    override fun onAuthenticationFailure(
        request: HttpServletRequest,
        response: HttpServletResponse,
        exception: AuthenticationException,
    ) {
        val email = request.getAttribute(&quot;email&quot;) as String
        logger.error { &quot;[AUTH FAILED] $email&quot; }

        response.apply {
            this.status = HttpStatus.UNAUTHORIZED.value()
            this.contentType = MediaType.APPLICATION_JSON_VALUE
        }

        objectMapper.writeValue(
            response.writer,
            AuthenticationResult(
                email = email,
                msg = &quot;authentication failed&quot;
            )
        )
    }
}&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 style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;code&gt;onAuthenticationFailure&lt;/code&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;인증 실패시 해당 메소드로 인증 과정 중 발생한 예외를 인자로 받게 됩니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;code&gt;request&lt;/code&gt; &amp;gt; &lt;code&gt;email&lt;/code&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;code&gt;ApiAuthenticationFilter&lt;/code&gt;에서 request에 parameter로 담았던 email 정보를 꺼냅니다. 사실 이부분은 응답데이터로 어떤 email 계정이 인증 실패하였는지 보내주기 위해서 Filter에서 parameter로 담았는데 좋은 방법은 아닌 것 같아보입니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;(Exception 정보에 email 내용을 담아서 FailureHandler에 전달하는 방법도 있을 것 같네요.)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;응답데이터&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;위의 인증 성공 때와 마찬가지로 email 내용과 에러메시지를 가지고 json 형태로 변환해서 응답으로 내보내면 됩니다.&lt;br /&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;  5. SecurityConfig&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;API 방식의 인증 프로세스 관련해서 구현할만한 내용들은 모두 마쳤습니다. 이제 위의 구현체들을 &lt;b&gt;SpringSecurity&lt;/b&gt;에 등록을 해야 하는데요. 이번에는 &lt;b&gt;SecurityConfig&lt;/b&gt; 설정 내용에 위의 custom 구현체들을 한 번 적용해보겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1668667527256&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
@EnableWebSecurity
class SecurityConfig {
    //...
    
    @Bean
    fun filterChain(http: HttpSecurity): SecurityFilterChain {
        return http
            .csrf().disable()

            .authorizeRequests()
            .antMatchers(&quot;/auth/members/sign-up&quot;).permitAll()
            .antMatchers(&quot;/test&quot;).hasRole(&quot;USER&quot;)   // 임시 인가 테스트용
            .anyRequest().authenticated()
            .and()
            .addFilterBefore(apiAuthenticationFilter(), UsernamePasswordAuthenticationFilter::class.java)

            .build()
    }
    
    //...
}&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 style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;code&gt;csrf().disable()&lt;/code&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Spring Security를 적용하면 default로 csrf 기능이 활성화됩니다. &lt;code&gt;CsrfFilter&lt;/code&gt;에서 request header에 담겨져온 csrf token 값을 가지고 오게 되는데요. api 방식에서는 csrf token값을 따로 request에서 설정하지 않습니다. token 값이 없이 filter에 들어오게 되면 &lt;code&gt;AccessDeniedHandler&lt;/code&gt;에 의해 &lt;code&gt;403 Forbidden&lt;/code&gt;호출하게 되는데요. 이를 방지하기 위해 비활성화처리를 해야 합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;허용 api&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;회원가입 api는 인증받지 않은 사용자에게도 열어두어야 하기 때문에 &lt;code&gt;/auth/members/sign-up&lt;/code&gt; url은 &lt;code&gt;permitAll&lt;/code&gt; 처리합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;code&gt;addFilterBefore&lt;/code&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;custom filter를 기존 인증 처리를 담당했던 &lt;code&gt;UsernamePasswordAuthenticationFilter&lt;/code&gt;보다 앞에 필터가 등록되도록 합니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1668667572091&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
fun apiAuthenticationFilter(): ApiAuthenticationFilter {
    return ApiAuthenticationFilter(
        AntPathRequestMatcher(&quot;/auth/authenticate&quot;, HttpMethod.POST.name)
    ).apply {
        this.setAuthenticationManager(authenticationConfiguration.authenticationManager)
        this.setAuthenticationSuccessHandler(apiAuthenticationSuccessHandler)
        this.setAuthenticationFailureHandler(apiAuthenticationFailureHandler)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;위에서 만들어 두었던 API 전용 인증 필터(&lt;b&gt;ApiAuthenticationFilter&lt;/b&gt;)를 Bean 객체로 설정하는 부분입니다.&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;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;code&gt;AuthenticationManager&lt;/code&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;이 부분이 저한테는 아직까지 어려운 부분인데요. 애플리케이션이 실행될 때 default로 설정된 Security 관련 인증 필터들이 등록이 되면서 AuthenticationManager 객체를 주입시켜주는데요. Custom Filter를 등록할 때에는 이 부분을 따로 주입시켜줘야 합니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;그리고 Spring Security v5.7.x 버전부터 Security 설정방식이 바뀌었습니다. 이후 버전부터는 &lt;code&gt;AuthenticationConfiguration&lt;/code&gt;에서 가져온 &lt;code&gt;AuthenticationManager&lt;/code&gt;를 custom filter에 주입시켜줍니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Success, Failure Handler 등록&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;apiAuthenticationFilter bean 등록할 때 위에서 따로 만들어 두었던 success, failure handler도 같이 주입시켜줍니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;code&gt;AntPathRequestMatcher&lt;/code&gt; 주입&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;어떤 요청 api url, method와 해당 apiAuthenticationFilter를 매핑할 것인지 설정하는 부분입니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;저는 &lt;code&gt;POST /auth/authenticate&lt;/code&gt; api를 인증 api로 설정하였습니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;  6. Reference&lt;/span&gt;&lt;/b&gt;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;위의 내용을 적용했던 제 개인 프로젝트 pull request 내용에 대해서 링크걸어두겠습니다. (&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;a href=&quot;https://github.com/beaniejoy/dongne-cafe-api/pull/18&quot;&gt;참고 Github Repo PR&lt;/a&gt;)&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1668666417408&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;Spring Security를 사용한 인증 프로세스 적용 by beaniejoy &amp;middot; Pull Request #18 &amp;middot; beaniejoy/dongne-cafe-api&quot; data-og-description=&quot;내용 인증 전용 프로젝트 분리를 위한 multi module project로 전환 service-api, account-api, common 분리 작업 진행 Spring Security만을 사용한 api 인증프로세스 적용 api 인증을 위한 Custom Filter 적용 AuthenticationPr&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/beaniejoy/dongne-cafe-api/pull/18&quot; data-og-url=&quot;https://github.com/beaniejoy/dongne-cafe-api/pull/18&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cK42V1/hyQA783Tup/3f2QrFmnK2EeuZwTs9nmo1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/beaniejoy/dongne-cafe-api/pull/18&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/beaniejoy/dongne-cafe-api/pull/18&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cK42V1/hyQA783Tup/3f2QrFmnK2EeuZwTs9nmo1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Spring Security를 사용한 인증 프로세스 적용 by beaniejoy &amp;middot; Pull Request #18 &amp;middot; beaniejoy/dongne-cafe-api&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;내용 인증 전용 프로젝트 분리를 위한 multi module project로 전환 service-api, account-api, common 분리 작업 진행 Spring Security만을 사용한 api 인증프로세스 적용 api 인증을 위한 Custom Filter 적용 AuthenticationPr&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Spring/Security</category>
      <category>Spring</category>
      <category>spring security</category>
      <category>인증</category>
      <author>beaniejoy</author>
      <guid isPermaLink="true">https://beaniejoy.tistory.com/87</guid>
      <comments>https://beaniejoy.tistory.com/87#entry87comment</comments>
      <pubDate>Thu, 17 Nov 2022 21:46:05 +0900</pubDate>
    </item>
    <item>
      <title>Spring Security의 인증 프로세스 정리(form login 인증 방식)</title>
      <link>https://beaniejoy.tistory.com/86</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;개인 프로젝트 하면서 적용했던 내용을 정리해보는 글입니다. 이번 내용은 로그인(인증) 프로세스를 순수 Spring Security만을 가지고 개발해보았던 내용을 두 번에 나누어 정리해보고자 합니다.&lt;br /&gt;&lt;br /&gt;이번 게시글에서는 api 인증 프로세스 개발 적용해보기 전에 Spring Security의 기본 인증 전략에 대해서 간단하게 정리해보고자 합니다.&lt;br /&gt;(이부분을 알고 있어야 api용 인증 프로세스 개발할 때 사용되는 Filter, Provider, UserDetailsService의 custom 구현체들이 어떻게 적용되는지 알 수 있습니다.)&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;b&gt;  1. 간단한 Spring Security 인증 과정&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Security에서 간단한 인증 과정을 살펴보면 다음과 같습니다. (자세한 내용은 생략하도록 하겠습니다.)&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;1. 사용자 Form 로그인 화면 &lt;br /&gt;-&amp;gt; 2. AbstractAuthenticationProcessingFilter (doFilter)&lt;br /&gt;&amp;nbsp; &amp;nbsp; -&amp;gt; UsernamePasswordAuthenticationFilter (attemptAuthentication 호출)&lt;br /&gt;-&amp;gt; 3. AuthenticationManager(ProviderManager)&lt;br /&gt;-&amp;gt; 4. AuthenticationProvider&lt;br /&gt;&amp;nbsp; &amp;nbsp; -&amp;gt; UserDetailsService (loadUserByUsername 호출)&lt;br /&gt;-&amp;gt; 5. AbstractAuthenticationProcessingFilter (UsernamePasswordAuthenticationFilter의 추상&amp;nbsp;클래스)&lt;br /&gt;&amp;nbsp; &amp;nbsp; -&amp;gt; 여기서 인증 성공시 AuthenticationSuccessHandler&amp;nbsp;호출&lt;br /&gt;&amp;nbsp; &amp;nbsp; -&amp;gt; 인증 실패 시 AuthenticationFailureHandler 호출&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 Spring Security의 인증 과정은 위의 과정보다 훨씬 더 많은 Filter들을 거쳐서 이루어지고 중간에 많은 과정을 거치게 되는데요. 이번에 인증 과정을 개발하면서 중요한 지점들만 추려보았습니다. 각 과정에 대해서 간단하게 정리해보고자 합니다.&lt;/p&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&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;b&gt;  2. 사용자 Form 로그인 화면&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Security를 프로젝트에 적용하면 기본적으로 Form Login 방식을 사용하게 됩니다. 모든 요청에 대해서 authenticated 설정을 하게 되면 &lt;span&gt;애플리케이션을 실행하고&lt;span&gt; 브라우저에서 요청을 하게 되면 Spring Security 기본 로그인 Form 화면으로 이동하게 됩니다.&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;여기서 사용자 ID와 비밀번호 입력 후 확인 버튼을 누르게 되면 각각 &lt;i&gt;username&lt;/i&gt;, &lt;i&gt;password&lt;/i&gt;라는 parameter 이름으로 인증을 요청하게 됩니다.&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&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;b&gt;&lt;span&gt;&lt;span&gt;  3. UsernamePasswordAuthenticationFilter&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;i&gt;AbstractAuthenticationProcessingFilter&lt;/i&gt;&lt;/b&gt;를 상속받은 Spring Security Filter 클래스입니다. Form Login 방식으로 인증 요청을 하면 여기서 요청을 받게 됩니다. &lt;br /&gt;(정확히는 &lt;i&gt;AbstractAuthenticationProcessingFilter&lt;/i&gt;로 들어오게 되고 doFilter에서 인증 시도하는 메소드를 호출하게 되는데 이 때 사용되는 클래스가&amp;nbsp;&lt;u&gt;&lt;b&gt;&lt;i&gt;UsernamePasswordAuthenticationFilter&lt;/i&gt;&lt;/b&gt;&lt;/u&gt;입니다.)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;221101_security_01.png&quot; data-origin-width=&quot;1874&quot; data-origin-height=&quot;904&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cn1VKp/btrP8LlJQAD/l3tkKQLBui6lOirgUXF1v0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cn1VKp/btrP8LlJQAD/l3tkKQLBui6lOirgUXF1v0/img.png&quot; data-alt=&quot;UsernamePasswordAuthenticationFilter.java 코드 일부분&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cn1VKp/btrP8LlJQAD/l3tkKQLBui6lOirgUXF1v0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcn1VKp%2FbtrP8LlJQAD%2Fl3tkKQLBui6lOirgUXF1v0%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;760&quot; height=&quot;367&quot; data-filename=&quot;221101_security_01.png&quot; data-origin-width=&quot;1874&quot; data-origin-height=&quot;904&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;UsernamePasswordAuthenticationFilter.java 코드 일부분&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;obtainUsername, obtainPassword를 통해 &lt;i&gt;username&lt;/i&gt;, &lt;i&gt;password&lt;/i&gt;라는 parameter를 request에서 받아 옵니다. 그리고 새로 만든 &lt;i&gt;Authentication&lt;/i&gt; 객체에 두 개의 값을 담아 &lt;i&gt;AuthenticationManager&lt;/i&gt;로 넘깁니다.&lt;/p&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&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;b&gt;  4. AuthenticationManager(ProviderManager)&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서는 실제 인증 처리를 담당할 Provider들을 list로 가지고 있는데요. 반복문 수행을 통해 list를 돌면서 Provider에게 인증처리를 위임하게 됩니다. &lt;u&gt;&lt;b&gt;&lt;i&gt;AuthenticationManager&lt;/i&gt;&lt;/b&gt;&lt;/u&gt;는 인터페이스고 실제로는 &lt;u&gt;&lt;b&gt;&lt;i&gt;ProviderManager&lt;/i&gt;&lt;/b&gt;&lt;/u&gt; 구현체가 담당하게 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;221101_security_02.png&quot; data-origin-width=&quot;1728&quot; data-origin-height=&quot;1768&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HB6Uc/btrP8O3PcS2/QgwD2PPJCuOBc046bxEkx0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HB6Uc/btrP8O3PcS2/QgwD2PPJCuOBc046bxEkx0/img.png&quot; data-alt=&quot;AuthenticationManager(ProviderManager)의 코드 일부분&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HB6Uc/btrP8O3PcS2/QgwD2PPJCuOBc046bxEkx0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHB6Uc%2FbtrP8O3PcS2%2FQgwD2PPJCuOBc046bxEkx0%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;760&quot; height=&quot;778&quot; data-filename=&quot;221101_security_02.png&quot; data-origin-width=&quot;1728&quot; data-origin-height=&quot;1768&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;AuthenticationManager(ProviderManager)의 코드 일부분&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;getProviders()&lt;/i&gt;를 통해서 등록된 &lt;i&gt;AuthenticationProvider&lt;/i&gt; list를 가져옵니다. 가져온 Provider list를 반복문으로 돌면서 인증 처리를 위임하게 됩니다.&lt;br /&gt;&lt;br /&gt;중요한 것은 인증 처리를 위임했는데 결과가 null(지원하지 않는 Authentication class type 등의 사유로)인 경우에 parent &lt;b&gt;&lt;i&gt;ProviderManager&lt;/i&gt;&lt;/b&gt;를 계속 탐색할 수 있다는 점입니다. Security 설정시 원할 경우 parent ProviderManager를 등록해서 인증 처리를 위한 Provider 탐색을 연장할 수 있습니다.&amp;nbsp;&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;b&gt;&lt;span&gt;  5.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;AuthenticationProvider&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 &lt;b&gt;&lt;u&gt;실제 인증처리를 담당&lt;/u&gt;&lt;/b&gt;하게 됩니다. 코드 내용은 다음과 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;221101_security_03.png&quot; data-origin-width=&quot;1786&quot; data-origin-height=&quot;1516&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/P6X2b/btrQaub5dj0/jtyBrqcw3oNY3728isREg0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/P6X2b/btrQaub5dj0/jtyBrqcw3oNY3728isREg0/img.png&quot; data-alt=&quot;AbstractUserDetailsAuthenticationProvider 코드 일부분, 여기서 실질적인 인증 과정을 거치게 된다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/P6X2b/btrQaub5dj0/jtyBrqcw3oNY3728isREg0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FP6X2b%2FbtrQaub5dj0%2FjtyBrqcw3oNY3728isREg0%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;760&quot; height=&quot;645&quot; data-filename=&quot;221101_security_03.png&quot; data-origin-width=&quot;1786&quot; data-origin-height=&quot;1516&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;AbstractUserDetailsAuthenticationProvider 코드 일부분, 여기서 실질적인 인증 과정을 거치게 된다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;먼저 &lt;i&gt;retrieveUser&lt;/i&gt; 메소드를 통해&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;u&gt;&lt;b&gt;&lt;i&gt;UserDetailsService &lt;/i&gt;&lt;/b&gt;&lt;/u&gt;구현체에서&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;i&gt;username&lt;/i&gt;&lt;span&gt;을 가지고 사용자를 조회해&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;i&gt;UserDetails&lt;/i&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;객체를 받아옵니다. &lt;i&gt;username&lt;/i&gt;으로 User 조회에 실패하면 &lt;b&gt;&lt;i&gt;UsernameNotFoundException&lt;/i&gt;&lt;/b&gt;을 반환하게 됩니다.&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;span&gt;그다음 &lt;i&gt;additionalAuthenticationChecks&lt;/i&gt;를 통해서 input 받은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;i&gt;password&lt;/i&gt;&lt;span&gt;와 조회한&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;i&gt;UserDetails&lt;/i&gt;&lt;span&gt;의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;i&gt;password&lt;/i&gt;의 일치여부를 체크하게 됩니다. 여기서 &lt;i&gt;password&lt;/i&gt; 값이 일치하지 않으면 &lt;b&gt;&lt;i&gt;BadCredentialsException&lt;/i&gt;&lt;/b&gt; 에러를 throw하게 됩니다.&lt;br /&gt;&lt;br /&gt;인증 과정에 이상이 없으면 &lt;i&gt;UsernamePasswordAuthenticationToken(Authentication)&lt;/i&gt; 객체를 만들어 반환하게 됩니다.&lt;/p&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&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;b&gt;&lt;span&gt;  6.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;AbstractAuthenticationProcessingFilter&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;221101_security_04.png&quot; data-origin-width=&quot;1714&quot; data-origin-height=&quot;1048&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cX9Ecs/btrP7U4nHnu/RvXT36MQDh7VSkqhy8XyZK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cX9Ecs/btrP7U4nHnu/RvXT36MQDh7VSkqhy8XyZK/img.png&quot; data-alt=&quot;AbstractAuthenticationProcessingFilter 코드 일부분(doFilter)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cX9Ecs/btrP7U4nHnu/RvXT36MQDh7VSkqhy8XyZK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcX9Ecs%2FbtrP7U4nHnu%2FRvXT36MQDh7VSkqhy8XyZK%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;760&quot; height=&quot;465&quot; data-filename=&quot;221101_security_04.png&quot; data-origin-width=&quot;1714&quot; data-origin-height=&quot;1048&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;AbstractAuthenticationProcessingFilter 코드 일부분(doFilter)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;i&gt;UsernamePasswordAuthenticationFilter&lt;/i&gt;&lt;/b&gt;의 추상 클래스입니다. &lt;b&gt;&lt;i&gt;attemptAuthentication&lt;/i&gt;&lt;/b&gt; 메소드는 위에 2번 과정에서 설명드렸던 &lt;i&gt;UsernamePasswordAuthenticationFilter&lt;/i&gt;가 &lt;i&gt;username&lt;/i&gt;, &lt;i&gt;password&lt;/i&gt; parameter를 받아 &lt;i&gt;AuthenticationManager&lt;/i&gt;에게 Authentication 객체를 넘기는 과정입니다.&lt;br /&gt;&lt;br /&gt;4번 과정까지 인증 과정을 거쳐 &lt;i&gt;Authentication&lt;/i&gt; 객체를 받아오고 만약 인증을 성공하게 되면 &lt;b&gt;&lt;i&gt;successfulAuthentication&lt;/i&gt;&lt;/b&gt; 메소드를 호출하는데요. 여기서&amp;nbsp;&lt;u&gt;&lt;b&gt;&lt;i&gt;AuthenticationSuccessHandler&lt;/i&gt;&lt;/b&gt;&lt;/u&gt;를 호출하게 됩니다.&lt;br /&gt;&lt;br /&gt;인증 과정 중 &lt;i&gt;username&lt;/i&gt;으로 &lt;i&gt;UserDetails&lt;/i&gt; 조회에 실패하거나 &lt;i&gt;password&lt;/i&gt; 불일치 등으로 인증을 실패하게 되면 &lt;b&gt;&lt;i&gt;unsuccessfulAuthentication&lt;/i&gt;&lt;/b&gt;를 호출하게 되고 여기서 &lt;u&gt;&lt;b&gt;&lt;i&gt;AuthenticationFailureHandler&lt;/i&gt;&lt;/b&gt;&lt;/u&gt;를 호출하게 됩니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;위의 과정은 개발하면서 적용되었던 부분들에 대해서만 언급한 내용입니다. 인증을 통과하게 되면 Session에 SecurityContext 저장하는 과정도 있고 인증 처리 전 후로 더 디테일한 과정들이 있지만 모두 생략하였습니다.&lt;/blockquote&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;b&gt;  7. 정리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Security에서 form login 인증 적용시 기본적으로 수행하는 인증 프로세스에 대해서 정말 간략하게 정리해보았습니다.&lt;br /&gt;&lt;br /&gt;form login 방식에서 위에서는 생략했지만 알아야하는 중요한 내용은 인증 성공시 반환받은 &lt;i&gt;Authentication&lt;/i&gt; 객체를 &lt;b&gt;&lt;i&gt;SecurityContext&lt;/i&gt;&lt;/b&gt;에 저장하고 최종적으로 &lt;b&gt;Session&lt;/b&gt;에 저장된다는 내용입니다. Spring Security는 기본적으로 session 정책을 사용하게 되는데요. 이로 인해 한계점도 분명 있어서 OAuth2, JWT같은 stateless 인증 방식도 등장했습니다.&lt;br /&gt;&lt;br /&gt;이부분은 제쳐두고 위의 form login 인증 방식은 말 그대로 form tag에 POST method로 parameter로 username, password를 담아 요청한 내용에 대해서 다루고 있습니다.&lt;br /&gt;&lt;br /&gt;Spring Security는 api 방식(json body)의 인증 프로세스를 지원하지 않기 때문에 이를 적용하려면 따로 Security 설정에 적용을 해야하는데요. 다음 게시글에서 api 인증 과정 적용을 위한 custom 구현체들을 Spring Security에 어떻게 적용하는지 이어서 정리해보고자 합니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;틀린 내용이 있을 수 있습니다. 이에 대한 코멘트 언제나 환영합니다.&lt;/blockquote&gt;</description>
      <category>Spring/Security</category>
      <category>Spring</category>
      <category>spring security</category>
      <category>인증</category>
      <author>beaniejoy</author>
      <guid isPermaLink="true">https://beaniejoy.tistory.com/86</guid>
      <comments>https://beaniejoy.tistory.com/86#entry86comment</comments>
      <pubDate>Tue, 1 Nov 2022 21:26:13 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] 설정파일과 Bean 사이의 순환참조(circular references) 이슈 및 해결</title>
      <link>https://beaniejoy.tistory.com/85</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Security만을 사용해서 개인 프로젝트에 간단한 회원가입과 인증 프로세스를 개발하면서 부딪혔던 내용 중 하나를 정리하고자 합니다.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;Spring Security 설정파일 작성 후 애플리케이션 실행 시 발생했던 &lt;b&gt;순환참조&lt;/b&gt;(circular references, dependency cycle)에 대해 기록한 내용입니다.&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;b&gt;  1. 개발했던 내용&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오직 Spring Security 내용을 가지고 인증 프로세스를 구현했던 내용을 정말 간단하게 요약하고 문제상황을 보여드리는게 좋을 것 같습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1667064634199&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/**
 * 실제 인증 절차 수행
 * @property userDetailsService email로 계정 찾기
 */
@Component
class ApiAuthenticationProvider(
    private val userDetailsService: UserDetailsService,
    private val passwordEncoder: PasswordEncoder
) : AuthenticationProvider {
    companion object: KLogging()

    override fun authenticate(authentication: Authentication): Authentication {
        logger.info { &quot;start authentication&quot; }

        val email = authentication.name
        val password = authentication.credentials as String?

        val user = userDetailsService.loadUserByUsername(email)
        if (!passwordEncoder.matches(password, user.password)) {
            throw BadCredentialsException(&quot;Input password does not match stored password&quot;)
        }

        // password null로 반환
        return UsernamePasswordAuthenticationToken(email, null, user.authorities)
    }

    override fun supports(authentication: Class&amp;lt;*&amp;gt;): Boolean {
        return UsernamePasswordAuthenticationToken::class.java.isAssignableFrom(authentication)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Api 방식의 &lt;b&gt;AuthenticationFilter&lt;/b&gt;를 적용하면서 authenticate를 실제 수행하는 Provider를 custom하기 위한 구현체를 적용했습니다. 여기서 &lt;b&gt;@Component&lt;/b&gt;로 bean 객체 등록을 했었습니다. (이것이 순환참조 발생의 발단이 됩니다.)&lt;br /&gt;&lt;br /&gt;해당 Provider는 &lt;b&gt;UserDetailsService&lt;/b&gt; 구현체에서 실제 인증 대상 사용자 객체를 받아 password 비교를 통한 인증 과정을 수행하고 있습니다.&lt;br /&gt;password 비교는 주입 받은 PasswordEncoder 객체가 수행합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1667064888364&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
@EnableWebSecurity
class SecurityConfig {
	//...
    
    @Bean
    fun passwordEncoder(): PasswordEncoder {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;PasswordEncoder&lt;/b&gt; bean은 따로 Security Config 파일에서 등록해줬습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Spring Security를 활용한 회원가입, 로그인(인증) 프로세스에 대해서는 따로 정리하고자 합니다.&lt;/blockquote&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;  2. 초기 설정 내용&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1667065005046&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
@EnableWebSecurity
class SecurityConfig {

    @Autowired
    lateinit var apiAuthenticationProvider: apiAuthenticationProvider
    
    //...
    
    @Bean
    fun authenticationManager(): AuthenticationManager {
        val authenticationManager = authenticationConfiguration.authenticationManager as ProviderManager
        authenticationManager.providers.add(apiAuthenticationProvider)
        return authenticationManager
    }
    
    //...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;api 전용 custom filter를 설정하기 위해서 &lt;b&gt;AuthenticationManager&lt;/b&gt;를 따로 custom filter에 등록을 해야하는 과정이 필요해서 &lt;b&gt;AuthenticationManager&lt;/b&gt;를 따로 bean 등록했습니다. 여기에는 따로 개발해두었던 AuthenticationProvider 구현체를 직접 필드 주입을 통해 주입받아서 &lt;b&gt;AuthenticationManager&lt;/b&gt;에 추가했습니다.&lt;/p&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;  3. 문제 발생&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 설정을 가지고 애플리케이션 실행하면 다음과 같은 에러가 발생하면서 실행이 중지됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1667065351317&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;The dependencies of some of the beans in the application context form a cycle:

┌─────┐
|  securityConfig (field public io.beaniejoy.dongnecafe.common.security.ApiAuthenticationProvider io.beaniejoy.dongnecafe.common.config.SecurityConfig.apiAuthenticationProvider)
&amp;uarr;     &amp;darr;
|  apiAuthenticationProvider defined in file [/Users/beanie.joy/Dev/project/dongne-cafe-api/dongne-account-api/build/classes/kotlin/main/io/beaniejoy/dongnecafe/common/security/ApiAuthenticationProvider.class]
└─────┘

Action:

Relying upon circular references is discouraged and they are prohibited by default. Update your application to remove the dependency cycle between beans.&lt;/code&gt;&lt;/pre&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;b&gt;@Component&lt;/b&gt;로 등록한 &lt;b&gt;ApiAuthenticationProvider&lt;/b&gt;에서 &lt;b&gt;PasswordEncoder&lt;/b&gt; bean 객체를 주입받고 있는데 이를 위해 &lt;b&gt;SecurityConfig&lt;/b&gt; 파일을 참조하게 됩니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;SecurityConfig&lt;/b&gt; 클래스에서는 &lt;b&gt;ApiAuthenticationProvider&lt;/b&gt;를 주입받고 있습니다. 그렇기 때문에 &lt;b&gt;ApiAuthenticationProvider&lt;/b&gt;를 다시 한 번 참조하게 됩니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ApiAuthenticationProvider&lt;/b&gt;는 &lt;b&gt;PasswordEncoder&lt;/b&gt;를 참조하고 있어서 다시 &lt;b&gt;SecurityConfig&lt;/b&gt; 파일을 참조하게 됩니다.&lt;/li&gt;
&lt;li&gt;이렇게 순환참조가 발생해서 애플리케이션 실행자체가 안되었습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(친절하게 에러로그에서 Action으로 어떻게 해결해야할지도 설명해주고 있습니다.)&lt;/p&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;  4. 해결책&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 순환참조 고리를 끊기 위한 여러가지 해결책이 있을 것 같습니다.&lt;br /&gt;&lt;br /&gt;우선 순환참조되는 Config 파일을 분리시켜서 &lt;b&gt;PasswordEncoder&lt;/b&gt; bean 등록을 따로 떼어 놓는 것입니다.&lt;br /&gt;두 번째로는 &lt;b&gt;ApiAuthenticationProvider&lt;/b&gt;를 &lt;b&gt;@Component&lt;/b&gt; annotation bean 등록이 아닌 설정파일을 통한 bean 설정하는 방법이 있습니다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;두 번째 방법&lt;/b&gt;을 사용해서 한 번 순환참조 문제를 해결해보고자 합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1667146911129&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//@Component
class ApiAuthenticationProvider(...) {...)&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1667065544229&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
@EnableWebSecurity
class SecurityConfig {
    //...
    
    @Autowired
    lateinit var userDetailsService: UserDetailsService
    
    //...
    
    @Bean
    fun passwordEncoder(): PasswordEncoder {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder()
    }
    
    @Bean
    fun apiAuthenticationProvider(): AuthenticationProvider {
        return ApiAuthenticationProvider(
            userDetailsService = userDetailsService,
            passwordEncoder = passwordEncoder()
        )
    }
    
    @Bean
    fun authenticationManager(): AuthenticationManager {
        val authenticationManager = authenticationConfiguration.authenticationManager as ProviderManager
        authenticationManager.providers.add(apiAuthenticationProvider())
        return authenticationManager
    }
    
    //...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;@Component&lt;/b&gt;를 제거한 &lt;b&gt;ApiAuthenticationProvider&lt;/b&gt;를 Config 파일에서 직접 Bean 설정해줍니다. 여기에서 주입시켜야하는 &lt;b&gt;UserDetailsService&lt;/b&gt; Bean을 필드 주입받습니다.&lt;br /&gt;&lt;br /&gt;이렇게 되면 오로지 &lt;b&gt;SecurityConfig&lt;/b&gt; 파일 내에서 &lt;b&gt;ApiAuthenticationProvider&lt;/b&gt;와 &lt;b&gt;PasswordEncoder&lt;/b&gt; bean 객체 둘다 관리하고 있기 때문에 순환참조의 고리를 끊을 수 있게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;  5. 정리&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 예시는 Spring Security를 다루다가 발생했던 순환참조를 들었었는데요. 사실 순환참조는 Spring 프레임워크를 사용하다보면 어떤 상황에서도 발생할 수 있는 내용입니다.&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;순환참조가 발생하면 애플리케이션 실행시 위에 에러로그처럼 순환참조 발생지점과 어떻게 해결하면 되는지를 친절한 설명이 나올 것입니다. 로그를 보고 순환참조 발생지점을 수정하면 해당 문제는 쉽게 해결할 수 있을 것입니다.&lt;/span&gt;&lt;/p&gt;</description>
      <category>Spring</category>
      <category>Spring</category>
      <category>spring security</category>
      <category>순환참조</category>
      <category>인증</category>
      <author>beaniejoy</author>
      <guid isPermaLink="true">https://beaniejoy.tistory.com/85</guid>
      <comments>https://beaniejoy.tistory.com/85#entry85comment</comments>
      <pubDate>Mon, 31 Oct 2022 01:32:14 +0900</pubDate>
    </item>
    <item>
      <title>[JUnit] mock을 이용한 신규 생성 로직 단위 테스트(JUnit5, mockito)</title>
      <link>https://beaniejoy.tistory.com/84</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #dddddd;&quot;&gt;(메인 사진 출처: &lt;a style=&quot;color: #dddddd;&quot; href=&quot;https://famunity.net/kotlin-junit5/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://famunity.net/kotlin-junit5/&lt;/a&gt;)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개인프로젝트 진행하며 카페 도메인 신규 생성 서비스 로직에 대한 단위테스트를 적용했던 내용을 기록겸 작성한 글입니다. 서비스 단위 테스트를 위해 &lt;b&gt;JUnit5&lt;/b&gt;, &lt;b&gt;mockito&lt;/b&gt;를 이용하여 진행하였습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&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;b&gt;  1. 서비스 테스트를 위한 기본 구성&lt;/b&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1660310539358&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@ExtendWith(MockitoExtension::class)
@TestMethodOrder(MethodOrderer.DisplayName::class)
internal class CafeServiceTest {
    @InjectMocks
    lateinit var mockCafeService: CafeService

    @Mock
    lateinit var mockCafeRepository: CafeRepository
    
    //...
 }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CafeService에 대한 테스트 클래스 구성 내용입니다. Mockito를 이용한 Mock 객체를 활용한 단위 테스트를 진행하고자 &lt;b&gt;MockitoExtension&lt;/b&gt;을 적용하였습니다.&lt;br /&gt;&lt;br /&gt;CafeService 실제 로직 안에서는 CafeRepository를 사용하기 때문에 이에 대해서 mock 객체를 주입받습니다. 적용하고자 하는 테스트는 CafeService 단위에서 수행하는 로직이 잘 작동하는지에 대한 테스트이지 CafeRepository에 대한 테스트는 아닙니다.&lt;br /&gt;&lt;br /&gt;이에 따라 CafeRepository는 mock 객체로 적용해서 return 값에 대해 미리 설정하고 CafeService 로직 테스트에 집중하고자 mock을 적용하였습니다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;@InjectMocks&lt;/b&gt;는 해당 어노테이션이 붙은 객체에 &lt;b&gt;@Mock&lt;/b&gt; 가짜 객체를 주입시켜줍니다. 다시 말해서 가짜 CafeService bean을 생성하기 위해 의존성이 있는 CafeRepository를 &lt;b&gt;@Mock&lt;/b&gt;을 통해 가짜 객체를 받아와 알아서 주입시켜 준다고 생각하시면 됩니다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;@TestMethodOrder&lt;/b&gt;는 테스트 결과 내용을 표시할 때 &lt;b&gt;@DisplayName&lt;/b&gt;에 지정한 테스트 이름을 기준으로 순서대로 표현하기 위해 설정한 내용입니다.&lt;/p&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&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;b&gt;  2. 기본적인 카페 신규 생성 테스트 코드 구성&lt;/b&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1660311347638&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
@DisplayName(&quot;카페 신규 생성 테스트&quot;)
fun create_cafe_test() {
    // given
    // 카페 생성을 위한 CafeRequestDto 구성 &amp;amp; mock 객체의 return 내용 지정
    
    // when
    // 실제 카페 생성 서비스 로직 실행(mockCafeService.createCafe)
    
    // then
    // 카페 생성 서비스 내부 로직이 실행됐는지 여부 체크(verify)
    // 생성 서비스 로직 결과로 return하는 id 값 일치 여부 체크(assertEquals)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단위 테스트에서 가장 기본적으로 사용하는 &lt;b&gt;given - when - then&lt;/b&gt; 틀을 사용하였습니다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;given&lt;/b&gt;에는 카페 생성을 위한 재료가 되는 CafeRequestDto를 구성하고 카페 생성 서비스 로직에서 호출되는 mockCafeRepository 메소드에 대한 return 내용을 설정하였습니다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;when&lt;/b&gt;에서는 실제 테스트하려는 대상 메소드가 실행되어야 하기 때문에 given에서 구성했던 requestDto 정보들을 가지고 &lt;b&gt;mockCafeService.createCafe(...)&lt;/b&gt;를 직접 호출합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;br /&gt;then&lt;/b&gt;에서는 그 결과값이 예상하는 값과 일치하는지 체크하는 것과 테스트 대상이 되는 생성 서비스 내부 로직들이 잘 호출되고 있는지 검증하는 코드를 구성하였습니다.&lt;/p&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&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;b&gt;  3. given&lt;/b&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1660311612118&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// given
val cafeRequestDto = CafeTestUtils.createCafeRequestDto()
val savedMockCafeId = 100L

`when`(mockCafeRepository.findByName(cafeRequestDto.name!!)).thenReturn(null)
`when`(mockCafeRepository.save(any(Cafe::class.java))).thenAnswer {
    injectCafeId(it.getArgument(0), savedMockCafeId)
}

//...

private fun injectCafeId(
    cafe: Cafe,
    newCafeId: Long,
): Cafe {
    val idField = cafe.javaClass.declaredFields
        .find { f -&amp;gt;
            f.getAnnotation(GeneratedValue::class.java) != null
        } ?: return cafe

    idField.isAccessible = true
    idField.set(cafe, newCafeId)

    return cafe
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CafeRequestDto 객체를 생성하는 내용은 다른 곳에서도 자주 사용되기 때문에 utils 클래스를 따로 두어 메소드화 하였습니다.&lt;br /&gt;Cafe Entity를 생성하고 반환되는 Cafe id 값을 미리 설정하였습니다. (&lt;b&gt;savedMockCafeId&lt;/b&gt;)&lt;br /&gt;&lt;br /&gt;카페 생성 서비스 로직에서 호출되는 것들 중에 따로 return을 지정해야 하는 부분은 크게 두 부분인데요. CafeRepository의 &lt;b&gt;findByName&lt;/b&gt;과 &lt;b&gt;save&lt;/b&gt;입니다.&lt;br /&gt;&lt;br /&gt;&lt;u&gt;findByName은 null 로 return하게 해서 신규 생성하려는 카페의 이름이 기존에 없는 것으로 가정하였습니다.&lt;/u&gt; &lt;br /&gt;(findByName에서 반환되는 Cafe Entity가 존재한다는 것은 기존에 이미 저장된 카페 정보가 있다는 의미이므로 Exception을 반환하게 됩니다. 이에 대해서 따로 Test 코드를 구성하여야 합니다.)&lt;br /&gt;&lt;br /&gt;&lt;u&gt;save 메소드에 대해서는 return하는 Cafe Entity가 미리 설정한 cafe id인 &lt;b&gt;savedMockCafeId&lt;/b&gt; 값을 가지고 있어야 합니다.&lt;/u&gt; 이를 위해서 &lt;b&gt;thenAnswer&lt;/b&gt;를 통해 save의 argument로 들어오는 Cafe Entity 내용을 가로채서 기대하는 &lt;b&gt;savedMockCafeId&lt;/b&gt; 값을 주입하면 됩니다.&lt;br /&gt;(&lt;b&gt;thenAnswer&lt;/b&gt;는 when에서 설정한 메소드에 들어가는 인자를 가로채서 해당 내용을 조작하여 return하도록 할 수 있게 해줍니다.)&lt;/p&gt;
&lt;pre id=&quot;code_1660312220828&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long = 0L&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Cafe의 id 필드는 val로 지정되어 있기 때문에 기본적으로 setter 접근이 어렵습니다. 원하는 값으로 주입하기 위해서 &lt;b&gt;Reflection&lt;/b&gt;을 이용해 id 필드값에 주입한 것을 테스트 코드에서 확인할 수 있습니다.&lt;br /&gt;(&lt;b&gt;isAccessible = true&lt;/b&gt; 를 통해 setter가 없는 private 필드에 접근하여 값을 주입할 수 있습니다.)&lt;/p&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&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;b&gt;  4. when&lt;/b&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1660312503058&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// when
val savedCafeId = mockCafeService.createCafe(
    name = cafeRequestDto.name!!,
    address = cafeRequestDto.address!!,
    phoneNumber = cafeRequestDto.phoneNumber!!,
    description = cafeRequestDto.description!!,
    cafeMenuRequestList = cafeRequestDto.cafeMenuList
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;when&lt;/b&gt;에서는 테스트 대상이 되는 카페 생성 서비스 메소드를 직접 호출합니다. 인자 값으로는 given에서 미리 설정했던 CafeRequestDto 내용으로 설정하면 됩니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&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;b&gt;  5. then&lt;/b&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1660312592721&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// then
verify(mockCafeRepository).findByName(cafeRequestDto.name!!)
verify(mockCafeRepository).save(any(Cafe::class.java))

assertEquals(savedCafeId, savedMockCafeId)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;then에서는 카페 생성 서비스 내부 로직들이 잘 호출되었는지와 생성 로직의 최종 결과값이 기대했던 값과 일치하는지를 체크하게 됩니다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;verify&lt;/b&gt;를 통해 생성 서비스 로직에서 호출되는 내용들이 잘 호출되었는지 체크합니다. 여기서 체크해볼 대상들은 CafeRepository의 &lt;b&gt;findByName&lt;/b&gt;과 &lt;b&gt;save&lt;/b&gt; 메소드입니다.&lt;br /&gt;&lt;br /&gt;그리고 카페 생성 서비스 로직의 최종 결과값으로 save 이후 반환되는 cafe id 값을 반환하게 되는데 given에서 미리 설정했던 기대값인 &lt;b&gt;savedMockCafeId&lt;/b&gt;와 값을 비교하면 됩니다.&lt;/p&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&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;b&gt;  6. (참고) Cafe Entity 객체를 create하는 메소드&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CafeService createCafe 메소드 안에는 인자로 받은 RequestDto 내용들을 가지고 Cafe Entity 객체를 생성합니다. 생성한 Cafe Entity를 JpaRepository save를 통해 영속화시키고 DB에 데이터들을 저장하게 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1660319280484&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;val cafe = Cafe.createCafe(
    name = name,
    address = address,
    phoneNumber = phoneNumber,
    description = description,
    cafeMenuRequestList = cafeMenuRequestList
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Cafe class의 companion object을 통해 Cafe Entity 객체 생성 로직을 메소드화하였습니다.&lt;br /&gt;&lt;br /&gt;하지만 CafeServiceTest에서는 이에 대한 테스트 검증을 따로 하지 않았는데요. Cafe Entity에 대한 Test 코드를 따로 구성하여 &lt;b&gt;Cafe.createCafe&lt;/b&gt;를 테스트를 따로 진행하고 있기 때문에 CafeServiceTest에서는 검증할 필요를 느끼지 못하여 제외하였습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&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;widthOrigin&quot; data-filename=&quot;Screen Shot 2022-08-12 at 10.59.19 PM.png&quot; data-origin-width=&quot;2326&quot; data-origin-height=&quot;416&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wpzv8/btrJC4cJ2vG/f1b6vUNC26KPOLlixupFAK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wpzv8/btrJC4cJ2vG/f1b6vUNC26KPOLlixupFAK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wpzv8/btrJC4cJ2vG/f1b6vUNC26KPOLlixupFAK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fwpzv8%2FbtrJC4cJ2vG%2Ff1b6vUNC26KPOLlixupFAK%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;2326&quot; height=&quot;416&quot; data-filename=&quot;Screen Shot 2022-08-12 at 10.59.19 PM.png&quot; data-origin-width=&quot;2326&quot; data-origin-height=&quot;416&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 테스트 코드를 구성하고 테스트를 진행하면 기분 좋게 테스트를 성공하는 것을 확인할 수 있습니다.&lt;br /&gt;&lt;br /&gt;Service 단계에서 단위 테스트를 구성할 때 테스트를 해야하는 대상들을 선별하는 것과 Mock 객체를 활용해 Service 단위의 로직들만을 가지고 테스트하도록 하는 것이 중요하다는 것을 알 수 있었습니다.&lt;br /&gt;&lt;br /&gt;kotlin 관련 테스트 모듈을 가지고 더 세련된 코드를 구성할 수도 있을 것 같네요.&lt;br /&gt;&lt;br /&gt;(단위 테스트 구성에 대한 저의 얕은 지식으로 인해 틀린내용이 있을 수 있고 코드가 상당히 저급할 수 있습니다. 이에 대한 코멘트 언제나 환영합니다.)&lt;/p&gt;</description>
      <category>Spring/Test</category>
      <category>JUnit</category>
      <category>Kotlin</category>
      <category>mock</category>
      <author>beaniejoy</author>
      <guid isPermaLink="true">https://beaniejoy.tistory.com/84</guid>
      <comments>https://beaniejoy.tistory.com/84#entry84comment</comments>
      <pubDate>Sat, 13 Aug 2022 00:52:17 +0900</pubDate>
    </item>
    <item>
      <title>[Kotlin] &amp;quot;companion object of enum class is uninitialized&amp;quot; 이슈 해결</title>
      <link>https://beaniejoy.tistory.com/83</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;실무에서 자바 to 코틀린 변환 작업을 하면서 자바 enum class를 코틀린으로 변환하는 과정에서 있었던 이슈 하나를 기록해보고자 합니다.&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;b&gt;  1. Java Enum static method&lt;/b&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1659098542588&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public enum HelloJavaStrategy {
    KOREAN(convertLang(Locale.KOREAN)),
    ENGLISH(convertLang(Locale.ENGLISH)),
    JAPANESE(convertLang(Locale.JAPANESE));

    private final String hello;

    HelloJavaStrategy(String hello) {
        this.hello = hello;
    }

    public String getHello() {
        return hello;
    }

    private static String convertLang(Locale locale) {
        if (Locale.KOREAN.equals(locale)) {
            return &quot;안녕하세요.&quot;;
        } else if (Locale.JAPANESE.equals(locale)) {
            return &quot;おはよう。&quot;;
        } else if (Locale.ENGLISH.equals(locale)) {
            return &quot;hello&quot;;
        } else {
            return &quot;no lang&quot;;
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에서 실제 사용되는 코드를 가져올 수는 없어서 조잡한 예시로 정리해보고자 합니다. &lt;br /&gt;말도 안되는 enum class이긴 하지만 enum instance 초기화 과정에서 hello 필드에 각각의 나라에 해당하는 인사말을 주입하기 위해 enum 내 static method를 사용하는 예시입니다.&lt;br /&gt;&lt;br /&gt;Java에서는 위와 같이 enum class 내의 static method를 가지고 enum instance 초기화할 때 생성자에서 사용할 수 있습니다.&lt;/p&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&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;b&gt;   2. Kotlin Enum companion function&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코틀린에서는 자바와 다르게 동작합니다. 위의 자바 클래스를 코틀린 코드로 바꾼 내용은 다음과 같습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1659098862298&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;enum class HelloStrategy(
    val hello: String
) {
    KOREAN(convertLang(Locale.KOREAN)),
    ENGLISH(convertLang(Locale.ENGLISH)),
    JAPANESE(convertLang(Locale.JAPANESE))
    ;
    
    companion object {
        fun convertLang(locale: Locale): String {
            return when (locale) {
                Locale.KOREAN -&amp;gt; &quot;안녕하세요&quot;
                Locale.JAPANESE -&amp;gt; &quot;おはよう。&quot;
                Locale.ENGLISH -&amp;gt; &quot;hello&quot;
                else -&amp;gt; &quot;no lang&quot;
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;static method를 companion object function으로 만들어 변환해보았습니다. 하지만 위와 같이 코드를 작성하면 컴파일 단계에서 다음과 같은 에러가 발생합니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Companion&amp;nbsp;object&amp;nbsp;of&amp;nbsp;enum&amp;nbsp;class&amp;nbsp;'HelloStrategy'&amp;nbsp;is&amp;nbsp;uninitialized&amp;nbsp;here&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직역하면 HelloStrategy 안에 있는 companion object가 enum instance 초기화하는 생성자안에서 uninitialized(초기화 X)된 상황이라는 뜻이 됩니다.&lt;br /&gt;&lt;br /&gt;자바의 static과 코틀린의 companion object 차이에서 이러한 에러가 발생하는데요. &lt;u&gt;자바 static은 애플리케이션 실행시 객체 생성보다 먼저 메모리에 올라가게 됩니다.&lt;/u&gt; 즉 자바에서는 enum instance 생성 단계에서 static method를 당연하게 사용할 수 있게 됩니다.&lt;br /&gt;&lt;br /&gt;코틀린의 companion은 자바의 static과 다른 개념인데요. &lt;u&gt;companion object 자체가 instance화 되는 시점은 속해있는 class가 instance화 되는 시점에 같이 이루어집니다.&lt;/u&gt; 즉 코틀린 enum instance 생성자 시점은 enum class가 instance화 되기 전이기 때문에 companion object가 아직 initialize 되지 않은 상태라 enum instance 생성자 안에 companion object function을 사용할 수 없게 됩니다.&lt;/p&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&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;b&gt;  3. 해결 방법&lt;/b&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1659100674627&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;enum class HelloStrategy(
    val hello: String
) {
    KOREAN(HelloStrategy.convertLang(Locale.KOREAN)),
    ENGLISH(HelloStrategy.convertLang(Locale.ENGLISH)),
    JAPANESE(HelloStrategy.convertLang(Locale.JAPANESE))
    ;
    
    //...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구글링하다 보면 위와 같은 방식으로 companion object를 품고있는 클래스를 직접 명시하면 된다고 언급하는 곳이 있었습니다. 하지만 위와 같은 방식으로 해도 다음과 같은 warning이 나오는데요.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Companion&amp;nbsp;object&amp;nbsp;of&amp;nbsp;enum&amp;nbsp;class&amp;nbsp;'HelloStrategy'&amp;nbsp;is&amp;nbsp;uninitialized&amp;nbsp;here.&amp;nbsp;This&amp;nbsp;warning&amp;nbsp;will&amp;nbsp;become&amp;nbsp;an&amp;nbsp;error&amp;nbsp;in&amp;nbsp;future&amp;nbsp;releases&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 문구가 나오면서 현재 에러가 발생하지 않지만 추후에 어떠한 에러가 발생할지 모른다고 IDE가 경고하고 있습니다. 실제로 해당 enum 값을 가지고 테스트해보면 &lt;b&gt;KOREAN(...)&lt;/b&gt; 부분에 &lt;b&gt;NullPointerException&lt;/b&gt;이 발생하게 됩니다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;enum instance를 먼저 생성하려고 하는데 companion object가 생성되기 전에 companion object 맴버 함수에 접근하려고 하니 NullPointerException이 발생하는 것은 당연한 일입니다.&lt;br /&gt;&lt;/b&gt;(해당 내용은 Jetbrains에서도 언급하고 있습니다. &lt;a href=&quot;https://youtrack.jetbrains.com/issue/KT-49110&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://youtrack.jetbrains.com/issue/KT-49110&lt;/a&gt;)&lt;/p&gt;
&lt;figure id=&quot;og_1659101189794&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Prohibit access to members of companion of enum class from initializers of entries of this enum : KT-49110&quot; data-og-description=&quot;Classification Type of change: * New errors are introduced Motivation types: * User code fails with exception(s) * The implementation does not abide by a published spec or documentation * Type safety guarantees are not met (including fail-fast behavior for&quot; data-og-host=&quot;youtrack.jetbrains.com&quot; data-og-source-url=&quot;https://youtrack.jetbrains.com/issue/KT-49110&quot; data-og-url=&quot;https://youtrack.jetbrains.com/issue/KT-49110&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://youtrack.jetbrains.com/issue/KT-49110&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://youtrack.jetbrains.com/issue/KT-49110&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Prohibit access to members of companion of enum class from initializers of entries of this enum : KT-49110&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Classification Type of change: * New errors are introduced Motivation types: * User code fails with exception(s) * The implementation does not abide by a published spec or documentation * Type safety guarantees are not met (including fail-fast behavior for&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;youtrack.jetbrains.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1659101215955&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;enum class HelloStrategy(
    val hello: String
) {
    KOREAN(LangConverter.convertLang(Locale.KOREAN)),
    ENGLISH(LangConverter.convertLang(Locale.ENGLISH)),
    JAPANESE(LangConverter.convertLang(Locale.JAPANESE))
    ;
}

class LangConverter{
    companion object {
        fun convertLang(locale: Locale): String {
            return when (locale) {
                Locale.KOREAN -&amp;gt; &quot;안녕하세요&quot;
                Locale.JAPANESE -&amp;gt; &quot;おはよう。&quot;
                Locale.ENGLISH -&amp;gt; &quot;hello&quot;
                else -&amp;gt; &quot;no lang&quot;
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 그냥 companion object 부분을 별도의 클래스를 생성해서 관리하게 하고 HelloStrategy는 해당 클래스에서 companion object function을 가져와 사용하게끔 작성을 했습니다. (다른 해결방법이 있을 수도 있습니다.)&lt;/p&gt;
&lt;pre id=&quot;code_1659101330192&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import example.LangConverter.Companion.convertLang
import java.util.Locale

enum class HelloStrategy(
    val hello: String
) {
    KOREAN(convertLang(Locale.KOREAN)),
    ENGLISH(convertLang(Locale.ENGLISH)),
    JAPANESE(convertLang(Locale.JAPANESE))
    ;
}

class LangConverter{
    companion object {
        fun convertLang(locale: Locale): String {
            return when (locale) {
                Locale.KOREAN -&amp;gt; &quot;안녕하세요&quot;
                Locale.JAPANESE -&amp;gt; &quot;おはよう。&quot;
                Locale.ENGLISH -&amp;gt; &quot;hello&quot;
                else -&amp;gt; &quot;no lang&quot;
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LangConverter를 중복으로 명시해야되는 부분이 있기에 import를 통해 간결하게 사용할 수도 있습니다. &lt;br /&gt;(import를 보시면 LangConverter의 Companion을 통해 convertLang function을 가져오고 있습니다.)&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;b&gt;  4. 정리&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Java static은 객체 생성과 무관하게 애플리케이션 실행시 메모리에 먼저 올라가는 영역이기 때문에 &lt;u&gt;&lt;b&gt;enum instance하는 과정에서 해당 enum class 안에 있는 static method를 사용할 수 있습니다.&lt;/b&gt;&lt;/u&gt;&lt;/li&gt;
&lt;li&gt;Kotlin companion object는 java static과 다르게 속해있는 클래스가 instance화 되는 시점에 companion object가 생성이 됩니다. 그래서 &lt;u&gt;&lt;b&gt;enum instance 생성자 시점에서 compaion object는 생성이 되기 전 상태이기 때문에 생성자안에 companion object function을 사용할 수 없게 됩니다.&lt;/b&gt;&lt;/u&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Kotlin</category>
      <category>compaion</category>
      <category>kotiln</category>
      <author>beaniejoy</author>
      <guid isPermaLink="true">https://beaniejoy.tistory.com/83</guid>
      <comments>https://beaniejoy.tistory.com/83#entry83comment</comments>
      <pubDate>Fri, 29 Jul 2022 22:34:27 +0900</pubDate>
    </item>
    <item>
      <title>[Spring Core #2] 스프링 컨테이너와 스프링 빈 (스프링 핵심 원리 강의정리)</title>
      <link>https://beaniejoy.tistory.com/82</link>
      <description>&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Index&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;a class=&quot;index_anchor_tag&quot; href=&quot;#c1&quot;&gt;  스프링 컨테이너 생성&lt;/a&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&amp;nbsp; - 스프링 컨테이너 생성 과정&lt;br /&gt;&lt;a class=&quot;index_anchor_tag&quot; href=&quot;#c2&quot;&gt;  컨테이너에 등록된 모든 빈 조회&lt;/a&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;a class=&quot;index_anchor_tag&quot; href=&quot;#c3&quot;&gt;  스프링 빈 조회&lt;/a&gt; &lt;br /&gt;&amp;nbsp; - 기본&lt;br /&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;&amp;nbsp; - 스프링 빈 조회 (동일한 타입 둘 이상인 경우)&lt;/span&gt;&lt;br /&gt;&lt;/span&gt;&lt;/span&gt;&amp;nbsp; - 스프링 빈 조회 (상속 관계)&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;a class=&quot;index_anchor_tag&quot; href=&quot;#c4&quot;&gt;  BeanFactory와 ApplicationContext&lt;/a&gt; &lt;br /&gt;&amp;nbsp; - BeanFactory&lt;br /&gt;&lt;/span&gt;&amp;nbsp; - ApplicationContext&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;a class=&quot;index_anchor_tag&quot; href=&quot;#c5&quot;&gt;  다양한 설정 형식 지원 - XML, 설정 클래스 파일&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;a class=&quot;index_anchor_tag&quot; href=&quot;#c6&quot;&gt;  스프링 빈 설정 메타 정보 - BeanDefinition&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;a class=&quot;index_anchor_tag&quot; href=&quot;#c7&quot;&gt;  정리&lt;/a&gt;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;해당 내용은 강의 내용을 기억하기 위한 정리글입니다.&lt;/b&gt;&amp;nbsp;자세한 내용은 강의에서 확인하실 수 있습니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;(저는 코틀린 베이스로 강의를 진행하였고 게시글의 코드 예시는 대부분 코틀린으로 이루어져 있습니다.)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;a href=&quot;https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8&quot;&gt;스프링 핵심 원리 - 기본편(김영한님)&lt;/a&gt;&amp;nbsp;&lt;b&gt;#광고아님&lt;/b&gt;,&amp;nbsp;&lt;b&gt;#내돈내산&lt;/b&gt;,&amp;nbsp;&lt;b&gt;#적극추천&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1656608120805&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;스프링 핵심 원리 - 기본편 - 인프런 | 강의&quot; data-og-description=&quot;스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., - 강의 소개 | 인프런...&quot; data-og-host=&quot;www.inflearn.com&quot; data-og-source-url=&quot;https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8&quot; data-og-url=&quot;https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dDUXEJ/hyOV5LVVeJ/BAwRergHMTD72l9UQ92ss0/img.png?width=768&amp;amp;height=500&amp;amp;face=0_0_768_500,https://scrap.kakaocdn.net/dn/oWGcS/hyOVYFZ3n4/YO2o6PKSZeXTrHKSkeKrm0/img.png?width=768&amp;amp;height=500&amp;amp;face=0_0_768_500,https://scrap.kakaocdn.net/dn/DOxQQ/hyOW2z2LLW/uiPHK3HDIGqrVmFgPv0S0k/img.png?width=1805&amp;amp;height=1044&amp;amp;face=0_0_1805_1044&quot;&gt;&lt;a href=&quot;https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dDUXEJ/hyOV5LVVeJ/BAwRergHMTD72l9UQ92ss0/img.png?width=768&amp;amp;height=500&amp;amp;face=0_0_768_500,https://scrap.kakaocdn.net/dn/oWGcS/hyOVYFZ3n4/YO2o6PKSZeXTrHKSkeKrm0/img.png?width=768&amp;amp;height=500&amp;amp;face=0_0_768_500,https://scrap.kakaocdn.net/dn/DOxQQ/hyOW2z2LLW/uiPHK3HDIGqrVmFgPv0S0k/img.png?width=1805&amp;amp;height=1044&amp;amp;face=0_0_1805_1044');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;스프링 핵심 원리 - 기본편 - 인프런 | 강의&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., - 강의 소개 | 인프런...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.inflearn.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&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;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;  스프링 컨테이너 생성&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;이번 강의에서는 스프링 컨테이너와 스프링 빈의 원리를 설명하고 있습니다. 스프링 컨테이너는 스프링 빈을 관리하는 주체이자 저장소라고 할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;스프링 컨테이너는 &lt;b&gt;XML 설정 방식&lt;/b&gt;, &lt;b&gt;Annotation 기반 자바 설정 클래스(AppConfig)&lt;/b&gt;로 구성 가능합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;스프링 컨테이너 생성 과정&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;애노테이션 기반의 설정 클래스 기준&lt;/b&gt;으로 먼저 예시를 들고 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1656668615217&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;val context: ApplicationContext = AnnotationConfigApplicationContext(AppConfig::class.java)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;AnnotationConfigApplicationContext&lt;/b&gt;를 통해 애노테이션 기반 설정 클래스로 구성한 스프링 컨테이너를 가져올 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1656668723326&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
class AppConfig {
    @Bean
    fun memberService(): MemberService {
        return MemberServiceImpl(memberRepository())
    }

    @Bean
    fun memberRepository(): MemberRepository {
        return MemoryMemberRepository()
    }

    @Bean
    fun orderService(): OrderService {
        return OrderServiceImpl(memberRepository(), discountPolicy())
    }

    @Bean
    fun discountPolicy(): DiscountPolicy {
        return FixDiscountPolicy()
//        return RateDiscountPolicy()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;설정 클래스 안에는 &lt;b&gt;@Bean&lt;/b&gt; 애노테이션으로 빈을 정의하고 있습니다. 여기서 따로 빈 이름을 설정하지 않으면 스프링은 &lt;b&gt;메소드 이름&lt;/b&gt;으로 스프링 빈 이름을 설정합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;물론 &lt;b&gt;빈 이름을 직접 지정&lt;/b&gt;할 수도 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1656668818595&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean(name = [&quot;memberCustomService&quot;])
fun memberService(): MemberService {
    return MemberServiceImpl(memberRepository())
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;코틀린&lt;/b&gt;에서는 Bean 애노테이션의 name 옵션 값이 Array로 되어 있어 이를 명시적으로 Array 형태로 입력해야 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&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;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;스프링 컨테이너 생성&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;스프링 빈 구성 정보 지정(AppConfig.class)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;스프링 빈 등록&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;스프링 컨테이너 내부의 빈 저장소는 일종의 key, value로 이루어져 있음. 여기에 설정파일의 메소드 이름과 반환 객체를 가지고 각각 key, value로 객체를 저장소에 저장.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;스프링 빈 의존관계 설정&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;원래는 스프링 빈들을 다 생성하고 그다음 의존관계를 주입하는 단계가 나누어져 있지만 자바 설정파일로 한 경우에는 빈 등록시 생성자가 호출되면서 해당 생성자 인자를 가지고 의존관계 주입도 같이 이루어지게 됨&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;주의할 점은 빈 이름은 항상 다른 이름으로 설정해야 한다는 점입니다.&lt;/b&gt; 여러 빈들에 같은 이름으로 중복 설정하면 의도치 않게 다른 빈이 무시되거나 기존의 빈을 overwrite할 수 있습니다. (Spring Boot는 기본적으로 이러한 빈 중복을 아예 원천 차단한다네요.)&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;애노테이션 기반의 설정 클래스로 스프링 빈을 등록하면 그 안에 &lt;b&gt;생성자를 호출하면서 스프링 빈 생성과 의존관계 주입이 한 번에 처리&lt;/b&gt;된다는 점도 중요해보였습니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&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;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;  컨테이너 등록된 모든 빈 조회&lt;/span&gt;&lt;/h2&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;applicationContext.getBeanDefinitionNames()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;스프링 컨테이너에 등록된 모든 빈들을 조회할 수 있는데 위와 같이 등록된 모든 빈들의 이름 목록을 가져올 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1656671964507&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
@DisplayName(&quot;모든 빈 출력하기&quot;)
fun findAllBean() {
    val beanDefinitionNames = ac.beanDefinitionNames
    for (beanDefinitionName in beanDefinitionNames) {
        val bean = ac.getBean(beanDefinitionName)
        println(&quot;name = ${beanDefinitionName}, object = ${bean}&quot;)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;스프링 빈 이름으로 빈 객체를 조회(getBean) 할 수 있습니다. 위와 같은 코드로 스프링에 등록된 &lt;b&gt;모든 빈들의 이름&lt;/b&gt;과 &lt;b&gt;빈 인스턴스&lt;/b&gt;를 한 꺼번에 확인할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;그런데 스프링에 등록된 모든 빈들 중에는 우리가 AppConfig를 통해 직접 설정한 빈들 뿐만 아니라 &lt;b&gt;스프링 내부적으로 사용하기 위해 등록한 빈&lt;/b&gt;들도 있습니다. 이를 따로 구분해서 조회할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1656672137229&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
@DisplayName(&quot;애플리케이션 빈 출력하기&quot;)
fun findApplicationBean() {
    val beanDefinitionNames = ac.beanDefinitionNames
    for (beanDefinitionName in beanDefinitionNames) {
        val beanDefinition = ac.getBeanDefinition(beanDefinitionName)

        if (beanDefinition.role == BeanDefinition.ROLE_APPLICATION) {
            val bean = ac.getBean(beanDefinitionName)
            println(&quot;name = ${beanDefinitionName}, object = ${bean}&quot;)
        }
    }
}&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 style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;span style=&quot;color: #ef5369; background-color: #dddddd;&quot;&gt;ROLE_APPLICATION&lt;/span&gt;: 사용자가 설정한 일반적인 스프링 빈&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;span style=&quot;background-color: #dddddd; color: #ef5369;&quot;&gt;ROLE_INFRASTRUCTURE&lt;/span&gt;: 스프링이 내부적으로 사용하기 위해 등록한 스프링 빈&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;우리가 직접 등록한 스프링 빈만 확인하고 싶을 때는 &lt;span style=&quot;background-color: #dddddd; color: #ef5369;&quot;&gt;ROLE_APPLICATION&lt;/span&gt;로 조회하면 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&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;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;  스프링 빈 조회&lt;/span&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;기본&lt;/span&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1656672359177&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;applicationContext.getBean([BeanName], [type])
applicationContext.getBean([type])&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;기본적으로 스프링 빈 이름과 타입을 가지고 스프링 빈을 조회할 수 있습니다. (&lt;b&gt;ApplicationContext getBean&lt;/b&gt; 사용)&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1656672484199&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
@DisplayName(&quot;빈 이름으로 조회&quot;)
fun findBeanByName() {
    val memberService = ac.getBean(&quot;memberService&quot;, MemberService::class.java)

    assertThat(memberService).isInstanceOf(MemberServiceImpl::class.java)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;span style=&quot;background-color: #dddddd; color: #ef5369;&quot;&gt;&lt;b&gt;applicationContext.getBean([BeanName],&amp;nbsp;[type])&lt;/b&gt;&lt;/span&gt; 이걸 사용해서 스프링 빈을 조회한 것입니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1656672529594&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
@DisplayName(&quot;구체 타입으로 조회&quot;)
fun findBeanByName2() {
    val memberService = ac.getBean(&quot;memberService&quot;, MemberServiceImpl::class.java)

    assertThat(memberService).isInstanceOf(MemberServiceImpl::class.java)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;첫 번째에서는 &lt;b&gt;MemberService 인터페이스&lt;/b&gt; 타입으로 조회했는데 이번에는 &lt;b&gt;MemberService의 구현체 타입(MemberServiceImpl)&lt;/b&gt;으로 스프링 빈을 조회한 것입니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1656672590123&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
@DisplayName(&quot;이름 없이 타입으로만 조회&quot;)
fun findBeanByType() {
    val memberService = ac.getBean(MemberService::class.java)

    assertThat(memberService).isInstanceOf(MemberServiceImpl::class.java)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;스프링 빈 이름 없이 타입만으로도 스프링 빈을 조회할 수 있습니다. 당연하게 MemberService 뿐만 아니라 MemberServiceImpl로도 조회 가능합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;그런데 구체(구현체) 타입으로 조회하는 것은 바람직하지 못합니다. 객체 지향적으로 설계했는데 &lt;b&gt;구체 타입으로 조회하면 유연성이 떨어지고 확장하는데 많은 변경지점이 발생하게 됩니다.&lt;/b&gt; (OCP 위반)&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Test
@DisplayName(&quot;빈 이름으로 조회 X&quot;)
fun findBeanByNameX() {
    assertThrows(NoSuchBeanDefinitionException::class.java) {
        val memberService = ac.getBean(&quot;xxxxx&quot;, MemberService::class.java)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;등록이 안 된 빈 이름으로 조회를 하려고 하면 &lt;b&gt;NoSuchBeanDefinitionException&lt;/b&gt; 예외가 발생합니다. 참고해두면 좋을 것 같네요.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;스프링 빈 조회 (동일한 타입 둘 이상인 경우)&lt;/span&gt;&lt;/h3&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Configuration
private class SameBeanConfig {
    @Bean
    fun memberRepository1(): MemberRepository {
        return MemoryMemberRepository()
    }

    @Bean
    fun memberRepository2(): MemberRepository {
        return MemoryMemberRepository()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;이렇게 같은 타입으로 스프링 빈을 2개 중복 등록하면 어떻게 되는지도 강의에서 설명하고 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Test
@DisplayName(&quot;타입으로 조회시 같은 타입이 둘 이상 있으면, 중복 오류가 발생한다.&quot;)
fun findBeanByTypeDuplicate() {
    assertThrows(NoUniqueBeanDefinitionException::class.java) {
         val memberRepository = ac.getBean(MemberRepository::class.java)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;기본적으로 getBean 조회시 이런 상황에서는 &lt;b&gt;NoUniqueBeanDefinitionException&lt;/b&gt; 예외가 발생하게 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;val memberRepository = ac.getBean(&quot;memberRepository1&quot;, MemberRepository::class.java)
assertThat(memberRepository).isInstanceOf(MemberRepository::class.java)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;두 개 이상의 같은 타입으로 중복된 빈들 중 하나를 조회하려면 &lt;b&gt;빈 이름&lt;/b&gt;을 특정해서 조회하면 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Test
@DisplayName(&quot;특정 타입을 모두 조회한다.&quot;)
fun findAllBeansByType() {
    val beansOfType = ac.getBeansOfType(MemberRepository::class.java)
    beansOfType.keys.forEach {
        println(&quot;key = ${it}, value = ${beansOfType[it]}&quot;)
    }

    println(&quot;beansOfType = ${beansOfType}&quot;)
    assertEquals(beansOfType.size, 2)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;span style=&quot;color: #ef5369; background-color: #dddddd;&quot;&gt;&lt;b&gt;ac.getBeansOfType()&lt;/b&gt;&lt;/span&gt; 을 통해서 특정 타입으로 등록된 모든 스프링 빈들을 조회할 수 있습니다. &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Map 형식으로 반환해주는데 &lt;b&gt;key는 빈 이름, value는 해당 빈 인스턴스&lt;/b&gt;입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;그런데 특정타입 하나로 여러 개의 스프링 빈을 등록한 경우는 실무에서 잘 사용되지 않습니다. (저는 아직까지 본 적이 없네요.)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;스프링 빈 조회 (상속 관계)&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;스프링 빈 조회에 있어서 상속 관계가 적용이 됩니다. &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;즉, 상위 타입으로 스프링 빈을 조회하면 그에 해당하는 모든 하위 타입들을 같이 조회하게 됩니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Configuration
private class TestConfig {
    @Bean
    fun rateDiscountPolicy(): DiscountPolicy {
        return RateDiscountPolicy()
    }

    @Bean
    fun fixDiscountPolicy(): DiscountPolicy {
        return FixDiscountPolicy()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;이번에는 DiscountPolicy 구현체 두 개를 다른 이름으로 하여 빈으로 등록했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1656673777094&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
@DisplayName(&quot;부모 타입으로 조회시, 자식이 둘 이상 있으면, 중복 오류가 발생한다.&quot;)
fun findBeanByParentTypeDuplicate() {
    assertThrows(NoUniqueBeanDefinitionException::class.java) {
        ac.getBean(DiscountPolicy::class.java)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;위에 동일한 타입 둘 이상인 경우와 비슷하게 &lt;b&gt;DiscountPolicy&lt;/b&gt;로 다른 구현체 두 개가 스프링 빈으로 등록되었기 때문에 &lt;b&gt;getBean&lt;/b&gt; 조회시 중복 에러가 발생합니다. 즉, &lt;b&gt;DiscountPolicy&lt;/b&gt; 타입으로 조회하면 그의 하위 타입인 &lt;b&gt;FixDiscountPolicy&lt;/b&gt;, &lt;b&gt;RateDiscountPolicy&lt;/b&gt; 둘다 조회하게 되는 것을 알 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1656673870915&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
@DisplayName(&quot;부모 타입으로 조회시, 자식이 둘 이상 있으면, 빈 이름을 지정하면 된다..&quot;)
fun findBeanByParentTypeBeanName() {
    val discountPolicy = ac.getBean(&quot;rateDiscountPolicy&quot;, DiscountPolicy::class.java)
    assertThat(discountPolicy).isInstanceOf(RateDiscountPolicy::class.java)
}

@Test
@DisplayName(&quot;특정 하위 타입으로 조회&quot;)
fun findBeanBySubType() {
    val rateDiscountPolicy = ac.getBean(RateDiscountPolicy::class.java)
    assertThat(rateDiscountPolicy).isInstanceOf(RateDiscountPolicy::class.java)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;마찬가지로 등록된 스프링 빈 타입으로 조회하는 방법이 있습니다. 또한 상위 타입이 아닌 &lt;b&gt;구현체 타입&lt;/b&gt;으로 특정 지어 조회하는 방법도 있습니다. 하지만 앞에서 봤듯이 유연성이 없어진다는 문제가 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Test
@DisplayName(&quot;부모 타입으로 모두 조회하기&quot;)
fun findAllBeansByParentType() {
    val beansOfType = ac.getBeansOfType(DiscountPolicy::class.java)
    assertEquals(beansOfType.size, 2)

    beansOfType.keys.forEach {
        println(&quot;key = ${it}, value = ${beansOfType[it]}&quot;)
    }
}

@Test
@DisplayName(&quot;부모 타입으로 모두 조회하기 - Object&quot;)
fun findAllBeansByObjectType() {
    val beansOfType = ac.getBeansOfType(Object::class.java)
    beansOfType.keys.forEach {
        println(&quot;key = ${it}, value = ${beansOfType[it]}&quot;)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;span style=&quot;color: #ef5369; background-color: #dddddd;&quot;&gt;&lt;b&gt;ac.getBeansOfType()&lt;/b&gt;&lt;/span&gt;을 통해 상위 타입으로 모든 관련 하위 타입들을 한꺼번에 조회할 수 있습니다. 여기서 &lt;b&gt;Object&lt;/b&gt; 타입으로 조회하게 되면 모든 스프링 빈들을 조회하게 되는데요. &lt;b&gt;Object&lt;/b&gt;는 모든 클래스들의 최상위 타입이기 때문입니다.&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;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;  BeanFactory와 ApplicationContext&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;스프링 컨테이너로 사용되는 &lt;b&gt;ApplicationContext&lt;/b&gt;는 사실 &lt;b&gt;BeanFactory&lt;/b&gt;를 상속받고 있습니다. 즉, 스프링 컨테이너의 최상위 인터페이스가 바로 &lt;b&gt;BeanFactory&lt;/b&gt;라 할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;BeanFactory&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;BeanFactory는 getBean 메소드 등 기본적인 스프링 빈 조회 기능과 빈이 singleton, prototype인지 체크하는 기능 등 스프링 컨테이너의 베이스가 되는 기본 기능들을 제공해줍니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;ApplicationContext&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;BeanFactory&lt;/b&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;Screen Shot 2022-07-01 at 8.18.37 PM.png&quot; data-origin-width=&quot;1760&quot; data-origin-height=&quot;602&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b8pSyM/btrGgrwvLSF/Kd8BvnuVQ6Bkb9nK7ODDu0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b8pSyM/btrGgrwvLSF/Kd8BvnuVQ6Bkb9nK7ODDu0/img.png&quot; data-alt=&quot;출처: 스프링 핵심 원리 기본편 (김영한님) 인프런 강의&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b8pSyM/btrGgrwvLSF/Kd8BvnuVQ6Bkb9nK7ODDu0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb8pSyM%2FbtrGgrwvLSF%2FKd8BvnuVQ6Bkb9nK7ODDu0%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;1760&quot; height=&quot;602&quot; data-filename=&quot;Screen Shot 2022-07-01 at 8.18.37 PM.png&quot; data-origin-width=&quot;1760&quot; data-origin-height=&quot;602&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처: 스프링 핵심 원리 기본편 (김영한님) 인프런 강의&lt;/figcaption&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;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;MessageSource&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;- 국제화(i18n) 기능을 제공하는 인터페이스&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;EnvironmentCapable&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;- 프로파일과 프로퍼티를 다루는 인터페이스&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;- 로컬, 개발, 운영 등의 애플리케이션 구동 환경을 설정할 수 있음&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;ApplicationEventPublisher&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;- 이벤트를 발행, 구독하는 모델을 편리하게 지원&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;- 이벤트 프로그래밍에 필요한 인터페이스&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;ResourceLoader&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;- 파일, 클래스패스, 외부에서 리소스를 편리하게 조회 &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;- 리소스를 읽어오는 기능을 제공하는 인터페이스&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;ApplicationContext&lt;/b&gt;는 기본 스프링 빈 조회 기능 뿐만 아니라 그 외 편리한 기능들을 확장한 인터페이스라고 보시면 됩니다.&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;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;  다양한 설정 형식 지원 - XML, 설정 클래스 파일&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screen Shot 2022-07-12 at 8.30.12 PM.png&quot; data-origin-width=&quot;1390&quot; data-origin-height=&quot;820&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GQTAs/btrG8H6rXfG/lpRcfCd08yHimFC4OUT95k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GQTAs/btrG8H6rXfG/lpRcfCd08yHimFC4OUT95k/img.png&quot; data-alt=&quot;출처: 스프링 핵심 원리 기본편 (김영한님) 인프런 강의&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GQTAs/btrG8H6rXfG/lpRcfCd08yHimFC4OUT95k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGQTAs%2FbtrG8H6rXfG%2FlpRcfCd08yHimFC4OUT95k%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;1390&quot; height=&quot;820&quot; data-filename=&quot;Screen Shot 2022-07-12 at 8.30.12 PM.png&quot; data-origin-width=&quot;1390&quot; data-origin-height=&quot;820&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;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;스프링은 다양한 스프링 빈 설정 방식을 지원합니다.&amp;nbsp;위의 Annotation 기반의 설정파일(&lt;b&gt;AppConfig&lt;/b&gt;) 방식은 &lt;b&gt;AnnotationConfigApplicationContext&lt;/b&gt;를 통해 사용할 수 있습니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Annotation 기반 뿐만 아니라 xml 설정 파일 방식도 지원하는데 &lt;b&gt;GenericXmlApplicationContext&lt;/b&gt;를 사용합니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1657625700536&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;lt;beans xmlns=&quot;http://www.springframework.org/schema/beans&quot;
	xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;
	xsi:schemaLocation=&quot;http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd&quot;&amp;gt;
    
    &amp;lt;bean id=&quot;memberService&quot; class=&quot;hello.core.member.MemberServiceImpl&quot;&amp;gt;
	    &amp;lt;constructor-arg name=&quot;memberRepository&quot; ref=&quot;memberRepository&quot; /&amp;gt;
    &amp;lt;/bean&amp;gt;
    
    &amp;lt;bean id=&quot;memberRepository&quot; class=&quot;hello.core.member.MemoryMemberRepository&quot; /&amp;gt;
&amp;lt;/beans&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;xml 설정 방식은 위와 같이 bean 태그 안에 지정하고자 하는 빈 class 정보, id 값, 그리고 (생성자, setter)주입 받을 빈 내용을 지정할 수 있습니다. xml 설정 방식은 요즘에도 사용하고 있는 곳이 있지만 거의 사용되지 않는 방식이고 거의 대부분은 Annotation 기반으로 설정한다고 보면 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;이렇듯 스프링은 빈 설정 방식까지 추상화를 통해 유연하게 여러 가지 방식을 적용할 수 있도록 지원하고 있습니다.&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;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;  스프링 빈 설정 메타 정보 - BeanDefinition&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;바로 위의 내용과 같이 스프링은 다양한 스프링 빈 설정 방식을 유연성 있게 지원하고 있습니다. 추상화를 통해 유연성을 가질 수 있었는데요. 여기서 &lt;b&gt;BeanDefinition&lt;/b&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;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;AnnotationConfigApplicationContext &amp;gt; AnnotatedBeanDefinitionReader &amp;gt; &lt;b&gt;BeanDefinition&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;GenericXmlApplicationContext &amp;gt; XmlBeanDefinitionReader &amp;gt; &lt;b&gt;BeanDefinition&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;XxxApplicationContext &amp;gt; XxxBeanDefinitionReader &amp;gt; &lt;b&gt;BeanDefinition&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;여러 방식으로 빈을 설정하면 해당 설정파일을 ApplicationContext 여러 구현체들이 읽어들이는데요. ApplicationContext 구현체들은 &lt;b&gt;BeanDefinitionReader&lt;/b&gt;를 사용해서 &lt;b&gt;BeanDefinition&lt;/b&gt;을 생성하게 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1657626959432&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
@DisplayName(&quot;빈 설정 메타정보 확인&quot;)
fun findApplicationBean() {
    val beanDefinitionNames = ac.beanDefinitionNames
    for (beanDefinitionName in beanDefinitionNames) {
        val beanDefinition = ac.getBeanDefinition(beanDefinitionName)

        if (beanDefinition.role == BeanDefinition.ROLE_APPLICATION) {
            println(&quot;beanDefinitionName = ${beanDefinitionName}, beanDefinition = ${beanDefinition}&quot;)
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screen Shot 2022-07-12 at 8.57.04 PM.png&quot; data-origin-width=&quot;2656&quot; data-origin-height=&quot;534&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bw7TG7/btrG8matUnu/tHVfIVP6DSIgri9QtqK0LK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bw7TG7/btrG8matUnu/tHVfIVP6DSIgri9QtqK0LK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bw7TG7/btrG8matUnu/tHVfIVP6DSIgri9QtqK0LK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbw7TG7%2FbtrG8matUnu%2FtHVfIVP6DSIgri9QtqK0LK%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;2656&quot; height=&quot;534&quot; data-filename=&quot;Screen Shot 2022-07-12 at 8.57.04 PM.png&quot; data-origin-width=&quot;2656&quot; data-origin-height=&quot;534&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;ac.getBeanDefinition&lt;/b&gt;을 통해 설정된 BeanDefinition 정보들을 전부 조회할 수 있습니다. (안의 정보들은 참고)&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;BeanDefinition는 스프링이 다양한 형태의 설정 정보를 추상화해서 사용하는 것 정도만 이해하면 됩니다.&lt;/span&gt;&lt;/blockquote&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;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;  정리&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;이번 강의 섹션에서는 스프링 컨테이너와 설정된 스프링 빈을 조회하는 방법, 그리고 마지막으로 추상화를 통한 여러 스프링 빈 설정 방식 지원한다는 내용과 BeanDefinition까지 살펴보았습니다. 정리하면 다음과 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;1. 스프링 생성 과정을 스프링 컨테이너 생성 &amp;gt; 스프링 빈 등록 &amp;gt; 스프링 빈 의존관계 설정 순서로 설명했습니다.&lt;/span&gt;&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;특히 애노테이션 기반의 설정 클래스로 스프링 빈을 등록하면 그 안에&amp;nbsp;생성자를 호출하면서&amp;nbsp;스프링 빈 생성과 의존관계 주입이 한 번에&amp;nbsp;처리된다는 점이 중요해 보였습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;2. 컨테이너에 등록된 빈 객체들을 조회하는 방법에는 여러가지가 존재했습니다.&lt;/blockquote&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;하나의 타입으로 여러 개 빈들을 생성하면 예기치 않은 스프링 에러가 발생할 수 있다는 점에서 주의해야 할 점으로 보았습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;3. BeanFactory와 ApplicationContext&amp;nbsp;관계를 확인할 수 있었습니다.&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;BeanFactory&lt;/b&gt;는 getBean등 빈 조회하는 가장 기본적인 기능들을 담고 있고 이를 구현한 것이 &lt;b&gt;ApplicationContext&lt;/b&gt; 입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ApplicationContext&lt;/b&gt;는 &lt;b&gt;BeanFactory&lt;/b&gt; 뿐만 아니라 &lt;b&gt;MessageSource&lt;/b&gt;, &lt;b&gt;EnvironmentCapable&lt;/b&gt;, &lt;b&gt;ApplicationEventPublisher&lt;/b&gt;, &lt;b&gt;ResourceLoader&lt;/b&gt; 등 여러 인터페이스들을 상속받아 여러 다양한 기능들을 제공해준다는 점이 있었습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;4. 스프링은 다양한 스프링 빈 설정 방식을 지원합니다.&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;ApplicationContext&lt;/b&gt;를 구현한 구현체 중에서는 xml 방식, annotation 설정 방식 등 지원하는 구현체가 존재합니다.&lt;/li&gt;
&lt;li&gt;이렇게 여러 스프링 빈 설정 방식을 지원할 수 있는 것은 &lt;b&gt;BeanDefinitionReader&lt;/b&gt;를 사용해 &lt;b&gt;BeanDefinition&lt;/b&gt; 인터페이스 타입으로 반환하도록 설계했기 때문에 가능합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;5. 스프링은 스프링 빈 생성 과정부터 설정 방식까지 OOP에 입각해 유연성 있고 확장가능하도록 설계한 프레임워크라는 점에서 인상 깊었습니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Spring</category>
      <author>beaniejoy</author>
      <guid isPermaLink="true">https://beaniejoy.tistory.com/82</guid>
      <comments>https://beaniejoy.tistory.com/82#entry82comment</comments>
      <pubDate>Tue, 12 Jul 2022 21:30:49 +0900</pubDate>
    </item>
    <item>
      <title>[Spring Core #1] 스프링의 객체 지향 원리 적용 (스프링 핵심 원리 강의정리)</title>
      <link>https://beaniejoy.tistory.com/81</link>
      <description>&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Index&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;a class=&quot;index_anchor_tag&quot; href=&quot;#c1&quot;&gt;  새로운 요구사항의 추가&lt;/a&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;a class=&quot;index_anchor_tag&quot; href=&quot;#c2&quot;&gt;  관심사 분리&lt;/a&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;a class=&quot;index_anchor_tag&quot; href=&quot;#c3&quot;&gt;  AppConfig 리팩토링&lt;/a&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;a class=&quot;index_anchor_tag&quot; href=&quot;#c4&quot;&gt;  좋은 객체 지향 설계 5가지 원칙 적용&lt;/a&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;a class=&quot;index_anchor_tag&quot; href=&quot;#c5&quot;&gt;  IoC, DI, 컨테이너&lt;/a&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;a class=&quot;index_anchor_tag&quot; href=&quot;#c6&quot;&gt;  정리&lt;/a&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;해당 내용은 강의 내용을 기억하기 위한 정리글입니다.&lt;/b&gt; 자세한 내용은 강의에서 확인하실 수 있습니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;(저는 코틀린 베이스로 강의를 진행하였고 게시글의 코드 예시는 대부분 코틀린으로 이루어져 있습니다.)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;a href=&quot;https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;스프링 핵심 원리 - 기본편(김영한님)&lt;/a&gt; &lt;b&gt;#광고아님&lt;/b&gt;, &lt;b&gt;#내돈내산&lt;/b&gt;, &lt;b&gt;#적극추천&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1655985119174&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;스프링 핵심 원리 - 기본편 - 인프런 | 강의&quot; data-og-description=&quot;스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., - 강의 소개 | 인프런...&quot; data-og-host=&quot;www.inflearn.com&quot; data-og-source-url=&quot;https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8&quot; data-og-url=&quot;https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/b2lAQM/hyORDgsaOo/uKYOGZ1t8jdcD7uXvDt02K/img.png?width=768&amp;amp;height=500&amp;amp;face=0_0_768_500,https://scrap.kakaocdn.net/dn/JHETz/hyORDHuoH5/UMpTXrhtYVUYitf2m2y9ok/img.png?width=768&amp;amp;height=500&amp;amp;face=0_0_768_500,https://scrap.kakaocdn.net/dn/eanfNa/hyORNQPHZs/YqbKTHSUFbUbelbZCe13N0/img.png?width=1805&amp;amp;height=1044&amp;amp;face=0_0_1805_1044&quot;&gt;&lt;a href=&quot;https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/b2lAQM/hyORDgsaOo/uKYOGZ1t8jdcD7uXvDt02K/img.png?width=768&amp;amp;height=500&amp;amp;face=0_0_768_500,https://scrap.kakaocdn.net/dn/JHETz/hyORDHuoH5/UMpTXrhtYVUYitf2m2y9ok/img.png?width=768&amp;amp;height=500&amp;amp;face=0_0_768_500,https://scrap.kakaocdn.net/dn/eanfNa/hyORNQPHZs/YqbKTHSUFbUbelbZCe13N0/img.png?width=1805&amp;amp;height=1044&amp;amp;face=0_0_1805_1044');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;스프링 핵심 원리 - 기본편 - 인프런 | 강의&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., - 강의 소개 | 인프런...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.inflearn.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 id=&quot;c1&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;  새로운 요구사항의 추가&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;주문서비스가 있고 주문에 따른 할인 정책을 적용해야 하는 비즈니스 요구사항이 있습니다. 할인 정책에는 고정할인 정책만 적용해보려합니다. 다음과 같이 서비스 클래스를 구성할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1655985464990&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class OrderServiceImpl: OrderService {
    private val discountPolicy: DiscountPolicy = FixDiscountPolicy()
    
    //...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;위와 같이 &lt;b&gt;OrderServiceImpl&lt;/b&gt; 서비스 코드는 &lt;b&gt;DiscountPolicy&lt;/b&gt;에 의존하고 있고 해당 정책에는 고정할인정책 구현체를 적용하였습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&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;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;역할과 구현을 충실하게 분리&lt;br /&gt;&lt;/b&gt;&lt;b&gt;DiscountPolicy&lt;/b&gt;라는 역할과 &lt;b&gt;FixDiscountPolicy&lt;/b&gt;라는 구현을 분리하였습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;다형성 활용, 인터페이스/구현체 분리&lt;br /&gt;&lt;/b&gt;역할과 구현 내용과 비슷하게 &lt;b&gt;DiscountPolicy&lt;/b&gt;는 인터페이스 &lt;b&gt;FixDiscountPolicy&lt;/b&gt;는 구현체입니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;역할과 구현, 다형성 활용, 인터페이스 구현체 분리 등 객체 지향 원리를 잘 활용했다고 볼 수 있습니다. 하지만 또 생각해볼 것들이 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1655986010332&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class OrderServiceImpl: OrderService {
    //private val discountPolicy: DiscountPolicy = FixDiscountPolicy()
    private val discountPolicy: DiscountPolicy = RateDiscountPolicy()
    
    //...
}&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 style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;DIP(의존 역전 원칙) 준수 X&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;OrderServiceImpl&lt;/b&gt;은 추상(인터페이스, &lt;b&gt;DiscountPolicy&lt;/b&gt;)과 구현 클래스(&lt;b&gt;FixDiscountPolicy&lt;/b&gt;) 둘다 의존하고 있습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;OCP(개방 폐쇄 원칙) 준수 X&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;기능을 확장하면(또 다른 할인 정책 적용) 이것을 사용하는 클라이언트 코드(&lt;b&gt;OrderServiceImpl&lt;/b&gt;)에 영향을 주게 됩니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;즉 새로운 요구사항의 추가로 인해 기능을 확장해야 하는 상황에서는 한계에 부딪히게 됩니다. &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;(확장성을 고려못한 설계, 객체지향스럽지 않은 냄새나는 코드라 할 수 있습니다.)&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 id=&quot;c2&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;  관심사 분리&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;위의 서비스 코드(&lt;b&gt;OrderServiceImpl&lt;/b&gt;)에서는 두 가지 책임과 역할을 수행하고 있습니다. 하나는 서비스 코드 자체의 비즈니스 로직 수행 역할, 또 하나는 객체 생성과 참조변수 할당의 역할입니다. 이러한 설계는 객체지향스럽지 못합니다. 객체 지향은 기본적으로 하나의 코드에 하나의 책임과 역할을 수행해야 한다는 원칙을 가지고 있습니다. 즉 여기서 &lt;b&gt;관심사를 분리&lt;/b&gt;해야 될 필요가 생긴 것입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;여기서 설정파일을 통해 객체 생성과 할당하는 역할을 분리해보겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1655986545572&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
class AppConfig {
    @Bean
    fun orderService(): OrderService {
        return OrderServiceImpl(MemoryMemberRepository(), FixDiscountPolicy())
    }
}

class OrderServiceImpl(
    private val memberRepository: MemberRepository,
    private val discountPolicy: DiscountPolicy
) : OrderService {
	//...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;스프링 IoC와 DI를 통해서 관심사 분리를 간단하게 구현할 수 있습니다. &lt;b&gt;AppConfig&lt;/b&gt; 설정파일을 만들고 &lt;b&gt;OrderService&lt;/b&gt; 구현체를 스프링 빈으로 등록합니다. 이 때 생성자를 통해 &lt;b&gt;MemberRepository&lt;/b&gt;와 &lt;b&gt;DiscountPolicy&lt;/b&gt; 인터페이스의 구현체를 주입합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;스프링 IoC는 &lt;b&gt;AppConfig&lt;/b&gt; 설정 내용을 토대로 대신 객체를 생성해주고 필요한 구현체들을 주입시켜줍니다. 서비스 코드인 &lt;b&gt;OrderServiceImpl&lt;/b&gt;에서는 바로 위 코드처럼 순수하게 비즈니스 로직만 수행하면 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;만약 &lt;b&gt;FixDiscountPolicy&lt;/b&gt;에서 &lt;b&gt;RateDiscountPolicy&lt;/b&gt;로 기능을 변경해야 한다면 &lt;b&gt;AppConfig&lt;/b&gt;만 코드를 수정하면 됩니다. 서비스 코드에서는 변경지점이 사라지게 됩니다. (OCP, DIP 원칙을 준수하게 됩니다.)&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;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;AppConfig&lt;/b&gt;: 구현체 클래스 선택 및 전체 구성 책임만 수행&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;OrderServiceImpl&lt;/b&gt;: 기능을 실행하는 책임만 수행&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;이렇게 관심사를 확실하게 분리할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 id=&quot;c3&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;  AppConfig 리팩토링&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;지금은 Bean 생성시 필요한 구성 클래스들을 직접 생성자를 통해서 주입을 했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1656525279890&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
class AppConfig {
    @Bean
    fun orderService(): OrderService {
        return OrderServiceImpl(MemoryMemberRepository(), FixDiscountPolicy())
    }
    
    @Bean
    fun memberService(): MemberService {
        return MemberServiceImpl(MemoryMemberRepository())
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;위와 같이 MemberRepository 구현체가 여러 bean 생성 때 필요로 한다면 생성자를 통한 객체 생성 부분이 중복이 될 수 있습니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;이를 하나로 묶을 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1656525462656&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
class AppConfig {
    @Bean
    fun orderService(): OrderService {
        return OrderServiceImpl(memberRepository(), discountPolicy())
    }
    
    @Bean
    fun memberService(): MemberService {
        return MemberServiceImpl(memberRepository())
    }
    
    @Bean
    fun memberRepository(): MemberRepository {
        return MemoryMemberRepository()
    }
    
    @Bean
    fun discountPolicy(): DiscountPolicy {
    	return FixDiscountPolicy()
    }
}&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 style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;@Configuration&lt;/b&gt;: 스프링에게 해당 클래스가 설정파일이라는 것을 알려줍니다. (Bean들의 설계도)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;@Bean&lt;/b&gt;: 스프링 컨테이너에 스프링 빈으로 등록을 시켜줍니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 id=&quot;c4&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;  좋은 객체 지향 설계 5가지 원칙 적용&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;5가지 중 SRP, DIP, OCP에 대해서 생각해볼 수 있습니다.&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;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;SRP(단일 책임 원칙)&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;- 구현 객체 생성 및 연결은 &lt;b&gt;AppConfig&lt;/b&gt;, 실행에 대한 책임은 클라이언트 객체(&lt;b&gt;OrderServiceImpl&lt;/b&gt;)가 담당&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;- 관심사 분리&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;DIP(의존 관계 역전 원칙)&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;- 의존성 주입을 통해 추상화에 의존하고 구체화에 의존하지 않는 구조를 따름&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;- &lt;b&gt;OrderServiceImpl&lt;/b&gt; &amp;gt; &lt;b&gt;DiscountPolicy&lt;/b&gt;(인터페이스)에 의존, &lt;b&gt;FixDiscountPolicy&lt;/b&gt;에 의존 X&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;OCP(개방 폐쇄 원칙)&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;- 확장에는 열려있고 변경에는 닫혀있음&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;- &lt;b&gt;FixDiscountPolicy&lt;/b&gt; -&amp;gt; &lt;b&gt;RateDiscountPolicy&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;- 다른 할인정책으로 확장할 때 클라이언트 코드(&lt;b&gt;OrderServiceImpl&lt;/b&gt;)는 변경지점이 없음&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;위의 사례를 통해 객체 지향 설계를 잘 준수하고 있음을 확인할 수 있었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 id=&quot;c5&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;  IoC, DI, 컨테이너&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;IoC, DI, 컨테이너는 강의 내용을 다음과 같이 요약할 수 있습니다.&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;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;IoC, Inversion of Control, 제어의 역전&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;- 기존에는 클라이언트 코드에서 구현 객체 생성, 연결, 실행 모든 역할을 수행&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;- 클라이언트 코드가 제어의 흐름을 컨트롤한다고 볼 수 있음&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;- &lt;b&gt;AppConfig&lt;/b&gt; 등장으로 클라이언트 코드에서는 로직을 실행하는 역할만 담당&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;- 제어의 흐름에 대한 권한은 &lt;b&gt;AppConfig&lt;/b&gt;가 가지게 됨&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;- 프로그램 제어의 흐름을 외부에서 관리하는 것이 IoC 핵심&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;DI, Dependency Injection, 의존관계 주입&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;- &lt;b&gt;OrderServiceImpl&lt;/b&gt;은 &lt;b&gt;DiscountPolicy&lt;/b&gt;에 의존&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;- 의존관계는 &lt;b&gt;정적인 클래스 의존관계&lt;/b&gt;, &lt;b&gt;동적인 객체 의존 관계&lt;/b&gt; 둘로 나뉨&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&amp;nbsp; &lt;b&gt;&amp;gt; 정적인 클래스 의존관계&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; import만 보고 쉽게 파악 가능, 실제 실행시점에서 어떤 구현체가 주입되는지 알 수 없음&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;b&gt;OrderServiceImpl&lt;/b&gt; &amp;gt; &lt;b&gt;MemberRepository&lt;/b&gt;, &lt;b&gt;DiscountPolicy&lt;/b&gt; &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 두 개 인터페이스의 실제 구현체가 어떤게 주입되는 지 알 수 없음&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&amp;nbsp; &lt;b&gt;&amp;gt; 동적인 객체 의존관계&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/b&gt;애플리케이션 실행시점에 실제 생성된 객체 참조가 연결된 의존관계&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;IoC, DI 컨테이너&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;- AppConfig 같이 객체 생성, 관리, 의존관계 연결해주는 역할 수행&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;- 최근에는 &lt;b&gt;DI 컨테이너&lt;/b&gt;라 불림(어샘블러, 오브젝트 팩토리 등으로도 불림)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;의존관계 주입(DI)을 사용하면 정적인 클래스 의존관계 변경하지 않고 동적인 객체 인스턴스 의존관계를 쉽게 변경 가능&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;이게 이번 챕터 강의 내용 핵심인 것 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 id=&quot;c6&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;  정리&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;이번 강의 내용은 스프링 핵심 개념인 IoC, DI를 코드로 예시를 보여주며 잘 습득할 수 있어서 좋았습니다. 강의 핵심은 다음과 같습니다.&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;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;스프링은 &lt;b&gt;객체지향 5가지 원칙&lt;/b&gt;을 잘 준수하는 프레임워크 그 중 &lt;b&gt;SRP, DIP, OCP&lt;/b&gt;에 주목&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;관심사 분리&lt;/b&gt;를 통해 &lt;b&gt;SRP&lt;/b&gt; 준수&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;- 객체 생성, 연결하는 제어 관점 &amp;amp; 기능을 실행하는 사용 관점&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;- &lt;b&gt;AppConfig&lt;/b&gt; (설정파일), &lt;b&gt;IoC&lt;/b&gt; 개념 등장&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;의존성 주입(DI)&lt;/b&gt;를 통해 &lt;b&gt;DIP&lt;/b&gt; 준수&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;- 추상화에 의존하고 구체화에 의존하지 않는 코드 구성 (OrderServiceImpl과 DiscountPolicy 관계 생각)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;IoC와 DI를 통해 &lt;b&gt;OCP&lt;/b&gt; 준수&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;- 다른 할인 정책을 적용할 때(확장) AppConfig 코드만 수정하면 됨&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #dddddd; color: #ef5369; font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;의존관계 주입(DI)을 사용하면 정적인 클래스 의존관계 변경하지 않고 동적인 객체 인스턴스 의존관계를 쉽게 변경 가능&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Spring</category>
      <category>course</category>
      <category>OOP</category>
      <category>Spring</category>
      <category>spring-core-basic</category>
      <author>beaniejoy</author>
      <guid isPermaLink="true">https://beaniejoy.tistory.com/81</guid>
      <comments>https://beaniejoy.tistory.com/81#entry81comment</comments>
      <pubDate>Fri, 1 Jul 2022 01:16:48 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] 객체 지향 설계를 극대화한 스프링의 핵심 개념 정리</title>
      <link>https://beaniejoy.tistory.com/78</link>
      <description>&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Index&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;a class=&quot;index_anchor_tag&quot; href=&quot;#c1&quot;&gt;  스프링 탄생 배경&lt;/a&gt;&lt;br /&gt;&amp;nbsp; - EJB의 한계&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&amp;nbsp; - 스프링의 등장&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;a class=&quot;index_anchor_tag&quot; href=&quot;#c2&quot;&gt;  스프링의 핵심&lt;/a&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;a class=&quot;index_anchor_tag&quot; href=&quot;#c3&quot;&gt;  객체 지향 5가지 원칙과 스프링&lt;/a&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&amp;nbsp; - 스프링 IoC와 DI&lt;br /&gt;&amp;nbsp; - 스프링은 객체지향 원칙을 잘 준수해요&lt;br /&gt;&lt;a class=&quot;index_anchor_tag&quot; href=&quot;#c4&quot;&gt;  정리&lt;/a&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 id=&quot;c1&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;  1. 스프링의 탄생 배경&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;1-1. EJB 한계&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;지금은 알 필요가 없는 EJB읜 한계를 극복하기 위해 로드 존슨 형님이 스프링을 제안하였고 이를 개발했습니다. (유겐 휠러 형님이 사실상 스프링 대부분의 코드 지분을 가지고 있음)&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;EJB(Enterprise Java Beans)에 대해서 본인은 이쪽 세대도 아니었고 한 번도 사용해본 적이 없었기 때문에 EJB 개념조차 제대로 알지 못합니다. 하지만 스프링이 왜 태어났는지에 대해서 찾아보거나 강의를 듣게 되면 EJB는 빠짐없이 등장합니다. 대략적으로 스프링이 탄생하기까지의 과정을 정리해보겠습니다.&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;옛날(?)에 Java로 애플리케이션을 개발할 때 EJB 스펙이 널리 보급되던 시절이 있었다고 합니다. EJB로 개발할 때 가장 큰 문제점이 배보다 배꼽이 더 커진다는 것이었는데요. 쉽게 말하자면 나는 고객의 요구대로 어떤 로직을 개발하고 적용하고 싶은데 로직 개발하는 것보다 그 외의 요소들(환경적인 요소들), 설정해야 되는 것들이 대부분의 비중을 차지했습니다.&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;다른 단점도 있는데요, 아마 이게 가장 치명적이지 않나 싶습니다. 코드를 짤 때에 EJB 인터페이스에 의존해서 개발을 했기 때문에 &lt;b&gt;EJB에 종속적인 코드가 될 수 밖에 없다는 단점&lt;/b&gt;이 있습니다. 그래서 특정 기술, 스펙에 의존적으로 개발할 수 밖에 없기에 유연한 개발은 애초에 불가능했다고 합니다. 그런 측면에서 &lt;b&gt;EJB는 객체지향적이지 않다고 할 수 있습니다.&lt;br /&gt;&lt;br /&gt;&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;그래서 EJB에 얽매인 코드에서 순수한 Java 코드로 돌아가자 해서 &lt;b&gt;POJO(Plain Old Java Object)&lt;/b&gt; 개념이 등장하게 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;옛날 Java가 그립다! 복잡한 EJB에서 벗어나서 순수한 Java로 돌아가자!&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;1-2. 스프링의 등장&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;로드 존슨 형님이 EJB 못 써먹겠다, 여기서 탈피하자고 선언하면서 책을 출간하는데(그게 바로 전설적인 J2EE Development Without EJB) 이것이 바로 스프링의 모태가 됩니다. &lt;b&gt;로드 존슨 형님이 주목했던 것은 EJB없이 개발하면서 확장가능한, 유연한 개발 방법론이었습니다.&lt;/b&gt; 이 내용이 유명해지면서 다양하게 확장되는데 지금의 스프링으로 이어지게 된다고 보시면 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;a href=&quot;http://www.yes24.com/Product/Goods/798826&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;http://www.yes24.com/Product/Goods/798826&lt;/a&gt;(&lt;b&gt;전설의 그 책 참고&lt;/b&gt;)&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1655652200058&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;book&quot; data-og-title=&quot;Expert One-On-One J2EE Development Without EJB - YES24&quot; data-og-description=&quot;What is this book about? &amp;#96;Expert One-on-One J2EE Development without EJB shows Java developers and architects how to build robust J2EE applicati...&quot; data-og-host=&quot;www.yes24.com&quot; data-og-source-url=&quot;http://www.yes24.com/Product/Goods/798826&quot; data-og-url=&quot;http://www.yes24.com/Product/Goods/798826&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/0d9Oy/hyOOIIJi8u/cNk2vUjdL9c1YEZMlff1fk/img.jpg?width=318&amp;amp;height=400&amp;amp;face=182_60_277_164,https://scrap.kakaocdn.net/dn/cfatIf/hyOOsMGqm1/5oXgIkycOYBHfHRr6lvqKK/img.jpg?width=318&amp;amp;height=400&amp;amp;face=182_60_277_164,https://scrap.kakaocdn.net/dn/vMxl4/hyOOHC2m4y/sxGzuRQf23W6mQzQHVQqE1/img.jpg?width=318&amp;amp;height=400&amp;amp;face=182_60_277_164&quot;&gt;&lt;a href=&quot;http://www.yes24.com/Product/Goods/798826&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;http://www.yes24.com/Product/Goods/798826&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/0d9Oy/hyOOIIJi8u/cNk2vUjdL9c1YEZMlff1fk/img.jpg?width=318&amp;amp;height=400&amp;amp;face=182_60_277_164,https://scrap.kakaocdn.net/dn/cfatIf/hyOOsMGqm1/5oXgIkycOYBHfHRr6lvqKK/img.jpg?width=318&amp;amp;height=400&amp;amp;face=182_60_277_164,https://scrap.kakaocdn.net/dn/vMxl4/hyOOHC2m4y/sxGzuRQf23W6mQzQHVQqE1/img.jpg?width=318&amp;amp;height=400&amp;amp;face=182_60_277_164');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Expert One-On-One J2EE Development Without EJB - YES24&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;What is this book about? `Expert One-on-One J2EE Development without EJB shows Java developers and architects how to build robust J2EE applicati...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.yes24.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 id=&quot;c2&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;  2. 스프링의 핵심&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;스프링의 핵심은 &lt;u&gt;&lt;b&gt;자바 언어 기반의 OOP(객체 지향 프로그래밍)의 강점을 극대화한 프레임워크&lt;/b&gt;&lt;/u&gt;입니다. 즉 EJB 녀석이 할 수 없었던 객체 지향 프로그래밍 방식으로 개발할 수 있게 스프링이 개발 방법의 틀을 만들어준 것입니다.&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;그럼 객체 지향 프로그래밍이란 무엇인지 알아야 하는데 이 내용은 예전에 제가 작성했던 게시글을 참고하시면 될 것 같습니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;a href=&quot;https://beaniejoy.tistory.com/47&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://beaniejoy.tistory.com/47&lt;/a&gt; &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;(나의 영원한 마스터, 구글에게 &lt;b&gt;&quot;객체지향 프로그래밍이란&quot;&lt;/b&gt; 검색하셔도 됩니다.)&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1655652529078&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[OOP] 객체 지향 프로그래밍은 무엇일까&quot; data-og-description=&quot;Spring은 객체 지향의 꽃이라고 들었는데... 면접에서도 단골 질문 메뉴인 객체 지향은 무엇일까.   객체 지향(Object Oriented) 개발자들이 더욱 편하게 개발할 수 있는 방법론을 고안하는 과정에서 &quot; data-og-host=&quot;beaniejoy.tistory.com&quot; data-og-source-url=&quot;https://beaniejoy.tistory.com/47&quot; data-og-url=&quot;https://beaniejoy.tistory.com/47&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cTzS6v/hyOOFrHcrO/YqnypmhfSF0Ncm1ai86iEk/img.jpg?width=800&amp;amp;height=569&amp;amp;face=0_0_800_569,https://scrap.kakaocdn.net/dn/dJheRS/hyOOxUIpnI/UZKR43rTR5310UrvFNuOAK/img.jpg?width=800&amp;amp;height=569&amp;amp;face=0_0_800_569&quot;&gt;&lt;a href=&quot;https://beaniejoy.tistory.com/47&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://beaniejoy.tistory.com/47&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cTzS6v/hyOOFrHcrO/YqnypmhfSF0Ncm1ai86iEk/img.jpg?width=800&amp;amp;height=569&amp;amp;face=0_0_800_569,https://scrap.kakaocdn.net/dn/dJheRS/hyOOxUIpnI/UZKR43rTR5310UrvFNuOAK/img.jpg?width=800&amp;amp;height=569&amp;amp;face=0_0_800_569');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[OOP] 객체 지향 프로그래밍은 무엇일까&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Spring은 객체 지향의 꽃이라고 들었는데... 면접에서도 단골 질문 메뉴인 객체 지향은 무엇일까.   객체 지향(Object Oriented) 개발자들이 더욱 편하게 개발할 수 있는 방법론을 고안하는 과정에서&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;beaniejoy.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;객체지향 프로그래밍 특징에는 네 가지가 있는데 &lt;b&gt;캡슐화, 추상화, 상속, 다형성&lt;/b&gt;이 있습니다. 그 중에 스프링에서 가장 중요한 객체 지향 특징은 &lt;b&gt;다형성(Polymorphism)&lt;/b&gt;입니다.&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;br /&gt;&lt;br /&gt;다형성은 &lt;b&gt;인터페이스와 구현체&lt;/b&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;Screen Shot 2022-06-20 at 12.44.31 AM.png&quot; data-origin-width=&quot;719&quot; data-origin-height=&quot;348&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dwIxbA/btrFfYHZw1u/iEjT5GoiIcNONYsTfHsw61/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dwIxbA/btrFfYHZw1u/iEjT5GoiIcNONYsTfHsw61/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dwIxbA/btrFfYHZw1u/iEjT5GoiIcNONYsTfHsw61/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdwIxbA%2FbtrFfYHZw1u%2FiEjT5GoiIcNONYsTfHsw61%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;348&quot; data-filename=&quot;Screen Shot 2022-06-20 at 12.44.31 AM.png&quot; data-origin-width=&quot;719&quot; data-origin-height=&quot;348&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;서로 다른 두 장치 혹은 사용자와 장치 사이에 이어주는 부분이라고 정의되어 있습니다. &lt;b&gt;&quot;연결&quot;&lt;/b&gt;에 초점을 맞추면 될 것 같습니다. 실제 세계에서 예시를 들면, 우리가 살아가면서 어떤 장치를 사용하는데 대부분 그 작동 원리를 몰라도 기능을 잘 사용합니다. 자동차를 가장 많이 예시로 드는데요. 우리가 자동차의 엑셀, 브레이크, 기어 변경 등 여러 기능들이 어떻게 작동하는지 몰라도 해당 기능들을 잘 사용할 수 있습니다. (혹여나 차종이 바뀌어도 잘 사용합니다.)&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1655654172389&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface Car {
    void accelerate();
    
    void stop();
    
    //...
}

public class BenZGClass implements Car { /*...*/ }

public class VolvoXC implements Car { /*...*/ }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;저의 드림카인 벤츠 G class든 볼보 XC든 저는 엑셀과 브레이크 기능을 이용하기만 하면 됩니다. 여기서 &lt;b&gt;확장 가능한 프로그래밍&lt;/b&gt;을 가능하게 해줍니다. &lt;b&gt;스프링은 OOP의 특징, 특히 다형성을 잘 활용해서 확장 가능하고 유연한 프로그래밍을 가능하도록 설계&lt;/b&gt;되어 있습니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1655654571477&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class CarService {
	private CarRepository carRepository = new JdbcCarRepository();
//	private CarRepository carRepository = new MyBatisCarRepository();
//	private CarRepository carRepository = new JpaCarRepository();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;사용자 입장에서 어떤 DB 접근 기술을 사용할 것인지 유연하게 선택할 수 있습니다. &lt;b&gt;인터페이스 참조가 해당 기술 구현체를 받아서 로직을 수행&lt;/b&gt;할 것이기 때문에 구현체만 잘 주입시키면 됩니다. &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;(심지어 스프링에서 DI를 사용하면 CarService에서 코드 변경 지점이 하나도 없이 DB 접근기술을 유연하게 변경할 수 있습니다.)&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 id=&quot;c3&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;  3. 객체 지향 5가지 원칙과 스프링&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;SOLID&lt;/b&gt;라 불리는 객체 지향의 5가지 원칙이 있습니다.&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;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;SRP&lt;/b&gt; - Single Responsibility Principle, 단일 책임 원칙&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt; OCP&lt;/b&gt; - Open Closed Principle, 개방 폐쇄 원칙&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;LSP&lt;/b&gt; - Liskov Substitution Principle, 리스코프 치환 원칙&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;ISP&lt;/b&gt; - Interface Segregation Principle, 인터페이스 분리 원칙&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;DIP&lt;/b&gt; - Dependency Inversion Principle, 의존 역전 원칙&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;주니어 기술 면접 단골 질문인데요. 이부분은 따로 공부 정리할겸 게시글을 작성해보도록 하겠습니다. &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;(이것도 구글 형님한테 &lt;b&gt;&quot;객체 지향 5가지 원칙&quot;&lt;/b&gt;이란 검색하시면 됩니다.)&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;스프링은 IoC, DI 개념을 통해 위의 원칙들을 가능하게 해줍니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;3-1. 스프링 IoC와 DI&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;IoC는 IOC 위원이 아닌 Inversion of Control, 즉 &lt;b&gt;제어의 역전&lt;/b&gt;이라고 직역할 수 있습니다. 스프링은 애플리케이션을 처음 실행할 때 자바 설정 파일, xml 설정파일, 혹은 어노테이션을 통해 정의한 Bean 객체들을 생성하고 특정 공간에 저장합니다. 이것이 바로 &lt;b&gt;스프링 빈(Spring Bean)&lt;/b&gt;입니다.&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;쉽게 말해서 어떤 설계 도면이 있는데 그 설계 도면의 재료가 바로 스프링 빈이 된다고 보시면 됩니다. &lt;b&gt;객체 지향 프로그래밍에서의 재료는 객체이고 객체단위로 움직이기 때문에&lt;/b&gt; 재료가 될 클래스를 도면에 적고 스프링이 이 도면을 보고 재료들을 객체로 생성하고 가지고 있게 됩니다.&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;여기서 눈치 채셨겠지만 스프링이 도면을 보고 직접 객체 생성한다고 되어 있는데 여기서 &lt;b&gt;IoC 개념&lt;/b&gt;이 나오게 된다고 보시면 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1655655919995&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Example {
	
    public String run() {
    	
        // 사용자가 직접 객체를 생성
        Source source = new ConcreteSource();
        Instrument instrument = new Hammer();
        
        instrument.process(source);
        
        //...
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;자바 코드로 개발할 때 사용자가 직접 객체를 생성하고 그 객체를 가지고 조리하는데 이는&amp;nbsp;&lt;b&gt;사용자에게 제어권이 있다고 볼 수 있습니다.&lt;/b&gt; 하지만 객체 지향 원칙에서 &lt;b&gt;단일 책임 원칙&lt;/b&gt;이 있는데 하나의 클래스 안에 메소드 로직뿐만 아니라 객체 생성이라는 역할까지 수행한다는 점에서 단일 책임 원칙을 위반한다고 볼 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1655656046015&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Example {
    // 아래 설정파일에서 스프링이 생성해준 빈 객체를 가져다 사용하기만 하면된다.
    @Autowired
    private Source source;
    @Autowired
    private Instrument instrument;
    
    public String run() {
        instrument.process(source);
        
        //...
    }
}

@Configuration
public class Config {
	@Bean
    public Source source() {
    	return new ConcreteSource();
    }
    
    @Bean
    public Instrument instrument() {
    	return new Hammer();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;스프링은 사용자가 직접 했던 객체 생성 역할을 대신 수행해줍니다. 설정파일에 설계도면의 재료가 될 Bean들을 정의만 해주면 스프링은 source, instrument 구현 객체를 대신 생성해서 따로 저장해둡니다. &lt;b&gt;객체 생성 역할을 따로 분리&lt;/b&gt;했다는 점에서 단일 책임 원칙을 준수한다고 볼 수 있습니다.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;사용자한테 제어권이 있었던 것이 제 3자한테 넘어갔다 해서 &lt;b&gt;제어의 역전(Inversion of Control)&lt;/b&gt;이라고 부르기도 합니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;(왜 역전이라는 표현을 썼는지는 모르겠지만 원래 우리가 가지고 있던 제어권이 뒤집어져서 다른 사람에게 넘어갔으니 역전됐다는 표현을 사용한 것 같네요,,,)&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;참고로 객체 생성한 스프링 빈들을 특정 공간에 저장해둔다고 했는데 이 특정 공간을 가리켜 &lt;b&gt;IoC 컨테이너(스프링 컨테이너)&lt;/b&gt;라고 부릅니다.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;그럼 &lt;b&gt;DI&lt;/b&gt;는 뭘까요?&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;DI는 Dependency Injection, 의존성 주입&lt;/b&gt;이라고 하는데요. 의존성이라는 개념이 무언가 어디에 의존하고 있다라고 생각이 드는데 맞습니다. DI는 위의 예시에서도 찾아 볼 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1655656924525&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Example {
    @Autowired
    private Source source;
    @Autowired
    private Instrument instrument;
    
    //...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Example이라는 클래스는 Source, Instrument를 가져다 로직에 사용하고 있습니다. 즉 Example 클래스 혼자서는 아무것도 할 수 없고 Source, Instrument가 있어야만 제대로된 로직을 수행할 수 있습니다. 이런 점에서 혼자서 수행할 수 없고 다른 것에 의존적이라고 표현할 수 있고 &lt;b&gt;&quot;Example은 Source, Intrument에 의존한다&quot;&lt;/b&gt; 라고 표현합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;스프링은 어떤 설계 도면을 읽어서 재료들을 적절히 사용한다고 되어있는데 설계 도면은 바로 &lt;b&gt;의존성&lt;/b&gt;을 참고합니다. 스프링은 빈 대상으로 설정한 클래스 전부 스캔해서 해당 클래스가 의존하고 있는 클래스들을 싹다 알아냅니다. 앞서 스프링은 스프링 빈 객체를 미리 생성해서 스프링 컨테이너에 저장해둔다고 했는데, 생성해둔 빈 객체들을 의존하고 있는 클래스에 스프링이 알아서 주입시켜줍니다. 이를 DI, 의존성 주입이라고 보시면 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;!-- 티스토리 / 본문 / 수평형 / 반응형 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-8328273903441798&quot; data-ad-slot=&quot;6574113568&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;3-2. 스프링은 객체지향 원칙을 잘 준수해요&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;스프링은 IoC, DI 개념을 통해 위의 원칙들을 가능하게 해줍니다. 몇 개만 보자면 확장에는 열려있고 변경에 대해서는 닫혀 있어야 한다는 OCP를 스프링은 DI를 통해 준수하고 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1655657821610&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Example {
    @Autowired
    private Source source;
    @Autowired
    private Instrument instrument;
    
    //...
}

@Configuration
public class Config {
	//...
    
    @Bean
    public Instrument instrument() {
    	return new Sledgehammer();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Instrument를 망치에서 오함마로 구현체를 변경해야 할 때 Example 클래스는 코드를 변경할 필요가 없습니다. 즉 변경은 하지 않으면서 다른 Instrument 구현체로 확장해서 교체할 수 있다는 점에서 &lt;b&gt;OCP(개방 폐쇄 원칙)&lt;/b&gt;를 아주 잘 수행하고 있습니다.&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;또한 위에서 언급했듯이 스프링은 &lt;b&gt;SRP(단일 책임 원칙)&lt;/b&gt;도 준수하고 있음을 알 수 있었고, &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Example 클래스는 Source, Instrument 인터페이스에 의존하고 있다는 점에서 구체적인 것이 아닌 추상화된 것에 의존한다는 원칙인 &lt;b&gt;DIP(의존 역전 원칙)&lt;/b&gt;도 준수하고 있다고 볼 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 id=&quot;c4&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;  정리&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;스프링은 자바 기반의 객체 지향 프로그래밍의 꽃&lt;/b&gt;이라고 많이들 부릅니다. 객체 지향 프로그래밍의 특징을 극대화하고 객체 지향 원칙들을 준수하면서 개발하게끔 해준다는 점에서 그렇게 부르는 것 같습니다. EJB의 기나긴 겨울이 지나 따뜻한 봄이 왔다해서 스프링이라고 이름을 지었다고 하던데, 저는 스프링 부트를 사용하면서 진짜 이게 봄이라는 생각을 혼자 했던 것 같습니다.&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;자바라는 것이 옛날에는 딱딱하고 무겁고 어렵게만 여겨졌는데 스프링이 나오고 스프링 부트가 나오면서 개발하는 사용자 측면에서 가볍고 쉽게 애플리케이션을 개발할 수 있게 되었고 우리나라는 특히 스프링을 아주 많이 사용하는 듯 보입니다. &lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;스프링은 객체 지향의 막강한 파워를 기반으로 해서 넓디 넓은 스프링만의 생태계를 구축했고 지금도 오픈소스로 계속해서 기능을 확장하고 있습니다. 스프링의 넓은 생태계에 같이 동참하면 좋을 것 같네요.&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;스프링 개발자로서 기저에 숨겨져있는 객체 지향 프로그래밍에 대해서 잘 알아야겠다고 생각해서 강의도 듣고 책도 읽으며 블로그로 한 번 정리해보며 다시 기억하는 시간을 가져보았네요. 틀린 부분 있으면 언제든 코멘트 부탁드리겠습니다. 감사합니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;</description>
      <category>Spring</category>
      <category>course</category>
      <category>OOP</category>
      <category>Spring</category>
      <category>spring-core-basic</category>
      <author>beaniejoy</author>
      <guid isPermaLink="true">https://beaniejoy.tistory.com/78</guid>
      <comments>https://beaniejoy.tistory.com/78#entry78comment</comments>
      <pubDate>Mon, 20 Jun 2022 01:53:49 +0900</pubDate>
    </item>
  </channel>
</rss>