<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>함께 자라기</title>
    <link>https://wbluke.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Sat, 7 Mar 2026 17:58:36 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>wbluke</managingEditor>
    <image>
      <title>함께 자라기</title>
      <url>https://tistory1.daumcdn.net/tistory/3243776/attach/d9de03a78e9c47e1840ab3fd87fbf02e</url>
      <link>https://wbluke.tistory.com</link>
    </image>
    <item>
      <title>블로그 이전</title>
      <link>https://wbluke.tistory.com/70</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;블로그를 깃헙으로 이전했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;티스토리의 이전 글을 마이그레이션 할까도 생각했지만, 큰 효용가치는 없다고 생각해서 그대로 두고 새롭게 구성하기로 했습니다.&lt;br /&gt;자유도가 높아진 만큼 천천히 여러가지 블로그 기능도 붙여보고, 꾸준하게 포스팅 해보려고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;감사합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://wbluke.github.io/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://wbluke.github.io/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>블로그</category>
      <author>wbluke</author>
      <guid isPermaLink="true">https://wbluke.tistory.com/70</guid>
      <comments>https://wbluke.tistory.com/70#entry70comment</comments>
      <pubDate>Sun, 13 Mar 2022 14:02:40 +0900</pubDate>
    </item>
    <item>
      <title>1일 1잔디 19개월 회고</title>
      <link>https://wbluke.tistory.com/69</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;농부 졸업&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작년에 &lt;a href=&quot;https://wbluke.tistory.com/52&quot;&gt;1일 1잔디 8개월 회고&lt;/a&gt;라는 제목으로 1일 1커밋에 대한 포스팅을 한 적이 있다.&lt;br /&gt;일일커밋을 잔디 심는 농부에 빗대어 장점을 정리하고 글을 읽는 다른 분들에게도 일일커밋이 성향에 맞다면 시도해볼 것을 추천하는 내용이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;이제는 졸업할 시기가 온 것 같다.&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코로나 2차 백신을 맞고 3일 째 누워있다가, 오늘도 오늘 치 잔디를 심어야겠다는 생각에 (코딩할 힘은 없고) 미뤄둔 책을 꺼내서 몇 장 읽다가 문득 든 생각이었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1858&quot; data-origin-height=&quot;462&quot; data-filename=&quot;스크린샷 2021-10-16 오후 11.13.14.png&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbfEPO/btripFcUeW3/b5Ad249yYMvkYBltrO1tx1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbfEPO/btripFcUeW3/b5Ad249yYMvkYBltrO1tx1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbfEPO/btripFcUeW3/b5Ad249yYMvkYBltrO1tx1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbbfEPO%2FbtripFcUeW3%2Fb5Ad249yYMvkYBltrO1tx1%2Fimg.png&quot; data-origin-width=&quot;1858&quot; data-origin-height=&quot;462&quot; data-filename=&quot;스크린샷 2021-10-16 오후 11.13.14.png&quot; data-ke-mobilestyle=&quot;widthOrigin&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;돌아보니 2020년 3월 중순부터 이 글을 작성하는 2021년 10월 중순까지 19개월 정도 일일커밋을 진행했다. (오래도 했다)&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;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&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;/li&gt;
&lt;li&gt;지난 날의 행적을 시각적으로 확인할 수 있다.&lt;/li&gt;
&lt;li&gt;비교의 대상을 남이 아닌 나 자신으로 한정할 수 있다.&lt;/li&gt;
&lt;li&gt;지금 내가 잘하는 것, 해왔던 것, 이룬 것, 당장 필요한 것, 나중에 필요한 것들을 수시로 객관화할 수 있다.&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;그리고 졸업을 고민한 이유는 다음과 같다.&lt;/p&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;코드를 짜다가 커밋 1개를 만들어서 남기는 순간 그 날 하루 치 양을 채웠다는 인간적인 생각에 집중이 풀리곤 했다. 충분히 더 진행할 수 있는 상태였음에도.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;시간 내어 기술서적을 읽을 때도 내용 정리 커밋을 남기기 위한 시각으로 책을 읽어야만 했다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;내용에 푹 빠져서 집중해서 읽었으면 했는데, 어떻게 글로 정리할까를 고민하면서 읽었다.&lt;/li&gt;
&lt;li&gt;진심으로 표시하고 기억에 남기고 싶었던 내용에 밑줄을 치는 것이 아니라, 2차 창작물을 위한 밑줄을 쳤다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;현재 시기적으로 하는 일이 여러가지로 너무 많은 시기여서, 일일커밋을 진행할 물리적인 시간이 부족해 겨우겨우 잔디를 채우고 있었다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;내가 어느정도 조정하여 쉴 수 있는 영역에서는 쉬는 것이 좋겠다는 생각이 들었다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;가용 시간을 조금 더 세부적으로 분할하여 다른 활동에 투자하고 싶었다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프로그래밍 외에 책을 읽는다던가, 새로운 취미를 가진다던가, 운동을 한다던가. 재테크 공부도 좀 하고싶고.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/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;단점으로 인한 &lt;code&gt;중단&lt;/code&gt;이 아니라 &lt;code&gt;졸업&lt;/code&gt;이라고 표현하는 이유도 위와 같은 이유 때문이다.&lt;br /&gt;처음에는 제도를 강제함으로써 오는 유익이 있는데, 이제는 오래 해봤기 때문에 제도가 없어도 충분히 유익을 주도적으로 만들 수 있겠다는 생각이 들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다고 컨트리뷰션 판을 아예 비워두겠다는 건 당연히 아니고, 결과물로 구체화할 수 있는 학습 기록들은 꾸준히 만들어서 올릴 생각이다.&lt;br /&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;글을 작성하는 시점에 벌써 4일 정도의 빈 칸이 생겼다.&lt;br /&gt;막상 숨 쉬듯이 하던 일을 그만두니 더 할 수 있는데 일치감찌 포기해버리는건 아닌가 싶은 생각도 들고, 괜시리 불안하기도 하다.&lt;br /&gt;언젠가 그만두는 날이 올거라고 예상했지만 사실 더 오래 2~3년 정도는 채울거라고 생각했어서 더 싱숭생숭 한가보다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래도 너무나 바빴던 일상이 조금은 숨 쉴 구멍이 생긴 것 같아 비교적 살만해진 것 같다.&lt;br /&gt;취미가 책 사기에요, 라고 말하면서 구입하기만 했던 밀린 책도 하나씩 꺼내보고, 스트레스 해소를 위한 운동과 취미에도 골고루 시간 투자를 해야겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래도 여전히 일일커밋 운동을 추천하고자 하는 마음은 변하지 않으니, 많은 분들이 한번쯤은 꼭 시도해 보셨으면 좋겠다.&lt;/p&gt;</description>
      <category>회고</category>
      <category>일일잔디</category>
      <category>회고</category>
      <author>wbluke</author>
      <guid isPermaLink="true">https://wbluke.tistory.com/69</guid>
      <comments>https://wbluke.tistory.com/69#entry69comment</comments>
      <pubDate>Wed, 20 Oct 2021 23:12:26 +0900</pubDate>
    </item>
    <item>
      <title>[RTFM] Spring Core - 05. Aspect Oriented Programming with Spring</title>
      <link>https://wbluke.tistory.com/68</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;아래 포스팅에 대한 상위 메타 문서는&amp;nbsp;&lt;a href=&quot;https://wbluke.tistory.com/62&quot;&gt;[RTFM] 매일 읽는 공식 문서&lt;/a&gt;&amp;nbsp;를 참고해 주세요.&lt;br /&gt;원문 : &lt;a href=&quot;https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#aop&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#aop&lt;/a&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관점 지향 프로그래밍(AOP)는 프로그램 구조에 대한 다른 생각을 제공함으로써 객체 지향 프로그래밍(OOP)을 보완한다.&lt;br /&gt;OOP에서 모듈화의 핵심 단위는 클래스인 반면, AOP에서 모듈화의 단위는 애스펙트이다.&lt;br /&gt;애스펙트는 여러 타입과 객체를 관통하는 (트랜잭션 관리 같은) 개념의 모듈화를 가능하게 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링의 핵심 컴포넌트 중 하나가 바로 AOP 프레임워크이다.&lt;br /&gt;스프링 IoC 컨테이너가 AOP에 의존하지 않는 반면에 (즉, AOP를 사용하고 싶지 않으면 사용하지 않을 수 있다), AOP는 아주 유용한 미들웨어 해결책으로 스프링 IoC를 보완한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AOP는 스프링 프레임워크에서 다음을 위해 사용된다.&lt;/p&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;사용자가 커스텀한 애스펙트를 구현하여 AOP와 함께 OOP 사용을 보완할 수 있다.&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;AOP 개념&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;몇 가지 중심적인 AOP 개념과 용어를 정의해 보자.&lt;br /&gt;이 용어들은 스프링에 국한된 용어는 아니다.&lt;br /&gt;불행히도 AOP 용어가 직관적이지는 않지만, 스프링이 자체 용어를 사용하는 것보다는 덜 혼란스러울 것이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;애스펙트(Aspect)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여러 클래스를 관통하는 개념의 모듈화이다.&lt;br /&gt;트랜잭션 관리가 크로스컷팅 개념의 좋은 예시다.&lt;br /&gt;스프링 AOP에서 애스펙트는 일반 클래스에 의해 구현되거나 &lt;code&gt;@Aspect&lt;/code&gt; 어노테이션에 의해 마킹된 일반 클래스에 의해 구현된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;조인 포인트(Join point)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메서드의 실행이나 예외 처리 같은 프로그램 실행 포인트.&lt;br /&gt;스프링 AOP에서 조인 포인트는 항상 메서드 실행을 의미한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;어드바이스(Advice)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;특정 조인 포인트에서 애스펙트에 의해 행해지는 행위.&lt;br /&gt;어드바이스의 다양한 타입은 &quot;around&quot;, &quot;before&quot;, &quot;after&quot; 어드바이스를 포함한다.&lt;br /&gt;스프링을 포함한 많은 AOP 프레임워크는 어드바이스를 인터셉터로 모델링하고 조인 포인트에 여러 인터셉터의 체인을 유지한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;포인트컷(Pointcut)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;조인 포인트에 매칭되는 서술부.&lt;br /&gt;어드바이스는 포인트컷 표현식과 연관되며 포인트컷과 매칭되는 모든 조인 포인트에서 실행된다.&lt;br /&gt;포인트컷 표현식에 의해 매칭되는 조인 포인트의 개념은 AOP의 핵심이고 스프링은 기본적으로 AspectJ 포인트컷 표현식 언어를 사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;인트로덕션(Introduction)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;타입을 대신하여 추가 메서드나 필드를 선언한다.&lt;br /&gt;스프링 AOP를 사용하면 어드바이스된 객체에 추가적인 인터페이스(및 해당 구현)를 도입할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;타깃 객체
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;하나 이상의 애스펙트에게 어드바이스를 받는 객체.&lt;br /&gt;&quot;어드바이스된 객체&quot;라고도 한다.&lt;br /&gt;스프링 AOP가 런타임 프록시를 사용하여 구현되기 때문에, 이 객체는 항상 프록시 객체이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;AOP 프록시
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;애스펙트 계약을 구현하기 위해 AOP 프레임워크에 의해 생성된 객체.&lt;br /&gt;스프링 프레임워크에서 AOP 프록시는 JDK 다이나믹 프록시이거나 CGLIB 프록시이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;위빙(Weaving)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;애스펙트를 다른 애플리케이션 타입 또는 객체와 연결해서 새로운 어드바이스된 객체를 만든다.&lt;br /&gt;이는 컴파일 타임, 로드 타임, 런타임에 수행할 수 있다.&lt;br /&gt;스프링 AOP는 다른 순수 자바 AOP 프레임워크와 마찬가지로 런타임에 위빙을 수행한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 AOP는 다음과 같은 어드바이스 타입을 가진다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Before 어드바이스
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;조인 포인트 전에 수행되는 어드바이스.&lt;br /&gt;그러나 (예외를 던지지 않는 한) 조인 포인트로 진행되는 흐름을 막을 능력은 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;After returning 어드바이스
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;조인 포인트가 일반적으로 완료된 후에 실행되는 어드바이스 (예외를 던지지 않고 메서드가 완료된 경우)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;After throwing 어드바이스
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메서드가 예뢰를 발생시킨 경우 실행되는 어드바이스&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;After (finally) 어드바이스
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;조인 포인트가 종료되는 방식(정상이거나 예외를 던지거나)에 상관 없이 실행하는 어드바이스&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Around 어드바이스
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;조인 포인트를 감싼 어드바이스.&lt;br /&gt;이 방식이 가장 강력한 어드바이스이다.&lt;br /&gt;Around 어드바이스는 메서드 호출 전후에 커스텀한 행동을 할 수 있다.&lt;br /&gt;또한 자체 반환 값을 반환하거나 예외를 던져서 조인 포인트로 진행할지 어드바이스된 메서드 실행을 단축할지 여부를 선택할 책임을 가지고 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Around 어드바이스는 가장 일반적인 경우의 어드바이스이다.&lt;br /&gt;AspectJ와 같은 스프링 AOP에서 모든 범위의 어드바이스 타입을 제공하는 반면, 우리는 당신이 요구되는 행위를 구현할 수 있는 최소한의 강력한 어드바이스 타입을 사용하기를 권장한다.&lt;br /&gt;예를 들어, 만약 메서드의 반환 값으로 캐시를 갱신하고 싶다면, 비록 around 어드바이스가 같은 기능을 해낼 수 있을지라도 around 어드바이스 보다는 after returning 어드바이스를 사용하는 것이 좋다.&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;스프링 AOP의 효용성과 목적&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 AOP는 순수 자바로 구현된다.&lt;br /&gt;특별한 컴파일 과정이 필요하지 않다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 AOP는 메서드 실행 시점의 조인 포인트만 지원한다.&lt;br /&gt;필드 액세스 등의 추가 조인 포인트를 사용해야 하는 경우 AspectJ를 고려하는 것이 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AOP에 대한 스프링 AOP의 접근 방식은 다른 AOP 프레임워크의 접근 방식과 다르다.&lt;br /&gt;스프링 AOP의 목표는 완전한 AOP 구현을 제공하는 것이 아니라, AOP 구현과 스프링 IoC 간의 긴밀한 통합을 제공해서 엔터프라이즈 애플리케이션의 문제를 해결하는 데 도움이 되는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 스프링 프레임워크의 AOP 기능은 일반적으로 스프링 IoC 컨테이너와 함께 사용된다.&lt;br /&gt;애스펙트는 일반 빈 정의 구문을 사용해서 구성된다.&lt;br /&gt;이것이 다른 AOP 구현과의 결정적인 차이점이다.&lt;br /&gt;스프링 AOP로는 매우 세분화된 객체에 애스펙트를 제공할 수 없고, 이런 경우에는 AspectJ가 최선의 선택이다.&lt;br /&gt;하지만 스프링 AOP는 AOP가 적용되는 엔터프라이즈 자바 애플리케이션의 대부분의 문제에 대한 탁월한 해결책을 제공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 AOP는 포괄적인 AOP 해결책을 제공하기 위해 AspectJ와 경쟁하지 않는다.&lt;br /&gt;우리는 스프링 AOP와 같은 프록시 기반 프레임워크와 AspectJ와 같은 완전한 프레임워크가 모두 가치 있고 상호 보완적이라고 믿는다.&lt;br /&gt;스프링은 AspectJ와 스프링 AOP 및 IoC를 잘 통합하여 일관된 스프링 기반 애플리케이션 아키텍처 내에서 AOP의 모든 사용을 가능하게 한다.&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;AOP 프록시&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 AOP는 기본적으로 AOP 프록시에 표준 JDK 다이나믹 프록시를 사용한다.&lt;br /&gt;이를 통해 모든 인터페이스는 프록싱될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 AOP는 CGLIB 프록시도 사용할 수 있다.&lt;br /&gt;이는 인터페이스가 아닌 프록시 클래스에 필요하다.&lt;br /&gt;기본적으로 비즈니스 객체가 인터페이스를 구현하지 않는 경우에 CGLIB가 사용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 AOP가 프록시 기반이라는 사실을 파악하는 것이 중요하다.&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;@AspectJ 지원&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@AspectJ 는 어노테이션이 붙은 일반 자바 클래스로 애스펙트를 선언하는 스타일을 말한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;@AspectJ 지원 허용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 설정에서 @AspectJ 애스펙트를 사용하기 위해서는 @AspectJ 애스펙트에 기반한 스프링 AOP 설정과 이러한 애스펙트에 의해 어드바이스 되는지 아닌지를 판단하는 자동 프록시 빈의 스프링 지원을 활성화해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Configuration 으로 @AspectJ 지원을 활성화하려면 다음과 같이 할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Configuration
@EnableAspectJAutoProxy
public class AppConfig {

}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Aspect 선언&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@AspectJ 지원이 활성화되면 @AspectJ 의 애스펙트와 함께 애플리케이션 컨텍스트에 정의된 모든 빈이 스프링에 의해 자동으로 감지되고 스프링 AOP를 구성하는 데 사용된다.&lt;/p&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;package org.xyz;
import org.aspectj.lang.annotation.Aspect;

@Aspect
public class NotVeryUsefulAspect {

}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;포인트컷 선언&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;포인트컷은 관심사의 조인 포인트를 결정하고, 어드바이스가 언제 실행될지를 조절할 수 있게 한다.&lt;br /&gt;스프링 AOP는 스프링 빈을 위한 메서드 실행 조인 포인트만을 지원하며, 그러므로 당신은 포인트컷이 스프링 빈에서 메서드 실행과 매칭된다고 생각할 수 있다.&lt;br /&gt;포인트컷 선언은 두 가지로 나뉘는데, 이름과 모든 파라미터를 포함하는 시그니처와 관심사가 있는 메서드 실행부를 결정하는 포인트컷 표현식이다.&lt;br /&gt;AOP의 @AspectJ 어노테이션 스타일에 따르면, 포인트컷 시그니처는 표준 메서드 정의에 의해 제공되며, 포인트컷 표현식은 &lt;code&gt;@Pointcut&lt;/code&gt; 어노테이션을 사용해서 표현된다.&lt;br /&gt;(포인트컷 시그니처를 표현하는 메서드는 반드시 void 반환을 해야 한다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;@Pointcut(&quot;execution(* transfer(..))&quot;) // 포인트컷 표현식
private void anyOldTransfer() {} // 포인트컷 시그니처&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;@Pointcut&lt;/code&gt; 어노테이션에 의한 포인트컷 표현식은 표준 AspectJ 포인트컷 표현식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 AOP는 다음 AspectJ 포인트컷 지정자를 지원한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;execution&lt;/code&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메서드 실행 조인 포인트를 매칭한다. 스프링 AOP에서 가장 우선되는 지정자이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;within&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;this&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;target&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;args&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@target&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@args&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@within&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@annotation&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 AOP는 또한 &lt;code&gt;bean&lt;/code&gt; 이라는 추가 지정자를 지원한다.&lt;br /&gt;이 지정자를 사용하면 특정 이름의 스프링 빈 또는 스프링 빈 세트(와일드카드 사용 시)에 대한 조인 포인트 일치를 제한할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당신은 &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt;, &lt;code&gt;||&lt;/code&gt;, &lt;code&gt;!&lt;/code&gt; 를 사용하여 여러 포인트컷 표현식을 조합할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Pointcut(&quot;execution(public * *(..))&quot;) // public 메서드
private void anyPublicOperation() {} 

@Pointcut(&quot;within(com.xyz.myapp.trading..*)&quot;) // trading 모듈 내의 메서드
private void inTrading() {} 

@Pointcut(&quot;anyPublicOperation() &amp;amp;&amp;amp; inTrading()&quot;) // trading 모듈 내의 public 메서드
private void tradingOperation() {}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;엔터프라이즈급 애플리케이션을 설계하다보면, 개발자들은 특정 연산 집합이나 애플리케이션 모듈을 몇 가지 애스펙트로 관리하기를 원한다.&lt;br /&gt;이러한 목적을 위해 &lt;code&gt;CommonPointcuts&lt;/code&gt; 애스펙트를 정의해서 일반적인 포인트컷 표현식을 사용하기를 원한다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;package com.xyz.myapp;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;

@Aspect
public class CommonPointcuts {

    @Pointcut(&quot;within(com.xyz.myapp.web..*)&quot;)
    public void inWebLayer() {}

    @Pointcut(&quot;within(com.xyz.myapp.service..*)&quot;)
    public void inServiceLayer() {}

    @Pointcut(&quot;within(com.xyz.myapp.dao..*)&quot;)
    public void inDataAccessLayer() {}

    @Pointcut(&quot;execution(* com.xyz.myapp..service.*.*(..))&quot;)
    public void businessService() {}

    @Pointcut(&quot;execution(* com.xyz.myapp.dao.*.*(..))&quot;)
    public void dataAccessOperation() {}

}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;어드바이스 선언&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어드바이스는 포인트컷 표현식과 관련이 있고, 포인트컷에 의해 매칭되는 메서드 실행 전, 후, 혹은 전후 시점에 동작한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;@Before&lt;/code&gt; 어노테이션을 통해 before 어드바이스를 선언할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

@Aspect
public class BeforeExample {

    @Before(&quot;com.xyz.myapp.CommonPointcuts.dataAccessOperation()&quot;)
    public void doAccessCheck() {
        // ...
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;in-place 포인트컷 표현식을 사용한다면 위 예제는 다음과 같이 사용할 수 있을 것이다.&lt;/p&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

@Aspect
public class BeforeExample {

    @Before(&quot;execution(* com.xyz.myapp.dao.*.*(..))&quot;)
    public void doAccessCheck() {
        // ...
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;After returning 어드바이스는 메서드 실행부가 일반적인 반환을 했을 때 수행된다.&lt;br /&gt;&lt;code&gt;@AfterReturning&lt;/code&gt; 어노테이션을 사용해서 선언할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;

@Aspect
public class AfterReturningExample {

    @AfterReturning(&quot;com.xyz.myapp.CommonPointcuts.dataAccessOperation()&quot;)
    public void doAccessCheck() {
        // ...
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;때때로 반환되는 실제 값에 접근해야 할 경우가 있다.&lt;br /&gt;&lt;code&gt;@AfterReturning&lt;/code&gt; 에 다음과 같이 바인딩해서 접근할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;

@Aspect
public class AfterReturningExample {

    @AfterReturning(
        pointcut=&quot;com.xyz.myapp.CommonPointcuts.dataAccessOperation()&quot;,
        returning=&quot;retVal&quot;)
    public void doAccessCheck(Object retVal) {
        // ...
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;returning&lt;/code&gt; 속성에 사용된 이름은 반드시 어드바이스 메서드의 파라미터 이름과 동일해야만 한다.&lt;br /&gt;메서드 실행부가 반환할 때, 반환값은 어드바이스 메서드의 인자로 넘어오게 된다.&lt;br /&gt;&lt;code&gt;returning&lt;/code&gt; 절은 또한 메서드 실행부의 반환값 타입만 가능하도록 매칭을 제한해야 한다.&lt;br /&gt;(위 예제는 Object로, 모든 반환 값을 다 수용한다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;After throwing 어드바이스는 매칭되는 메서드 실행부가 예외를 던진 경우 수행된다.&lt;br /&gt;&lt;code&gt;@AfterThrowing&lt;/code&gt; 어노테이션으로 선언할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;

@Aspect
public class AfterThrowingExample {

    @AfterThrowing(&quot;com.xyz.myapp.CommonPointcuts.dataAccessOperation()&quot;)
    public void doRecoveryActions() {
        // ...
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;종종 당신은 특정 예외 타입이 던져졌을 때 어드바이스가 수행되기를 원하기도 하고, 어드바이스 바디 안에 있는 던져진 예외에 접근하기를 원할 수도 있다.&lt;br /&gt;&lt;code&gt;throwing&lt;/code&gt; 속성으로 매칭을 제한하고 어드바이스 파라미터에 던져진 예외를 받을 수 있다.&lt;/p&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;

@Aspect
public class AfterThrowingExample {

    @AfterThrowing(
        pointcut=&quot;com.xyz.myapp.CommonPointcuts.dataAccessOperation()&quot;,
        throwing=&quot;ex&quot;)
    public void doRecoveryActions(DataAccessException ex) {
        // ...
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;throwing&lt;/code&gt; 속성에 사용된 이름은 반드시 어드바이스 메서드의 파라미터 이름과 동일해야만 한다.&lt;br /&gt;메서드 실행부가 예외를 던질 때, 예외는 어드바이스 메서드의 인자로 넘어오게 된다.&lt;br /&gt;&lt;code&gt;throwing&lt;/code&gt; 절은 또한 메서드 실행부가 던지는 예외 타입만 가능하도록 매칭을 제한해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;After (Finally) 어드바이스는 메서드 실행부가 끝날 때 수행된다.&lt;br /&gt;&lt;code&gt;@After&lt;/code&gt; 어노테이션을 사용해 선언된다.&lt;br /&gt;After 어드바이스는 일반적인 종료와 예외 상황 모두 다룰 수 있도록 준비되어야 한다.&lt;/p&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.After;

@Aspect
public class AfterFinallyExample {

    @After(&quot;com.xyz.myapp.CommonPointcuts.dataAccessOperation()&quot;)
    public void doReleaseLock() {
        // ...
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어드바이스의 마지막 종류는 Around 어드바이스이다.&lt;br /&gt;Around 어드바이스는 말 그대로 메서드 실행부 &quot;전후&quot;로 수행된다.&lt;br /&gt;메서드 실행 전후에 작업을 수행하고 메서드가 실제로 실행되는 시기와 방법, 실행 여부를 결정할 수 있는 기회를 가졌다.&lt;br /&gt;Around 어드바이스는 메서드 실행부 전후로 스레드 안전한 방식으로 상태를 가져야할 때 많이 사용된다.&lt;br /&gt;(예를 들면, 스탑워치 같은.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Around 어드바이스는 &lt;code&gt;@Around&lt;/code&gt; 어노테이션을 통해 선언된다.&lt;br /&gt;어드바이스 메서드의 첫 번째 인자는 &lt;code&gt;ProceedingJoinPoint&lt;/code&gt; 이다.&lt;br /&gt;어드바이스 바디에서, ProceedingJoinPoint의 &lt;code&gt;proceed()&lt;/code&gt; 메서드를 호출하여 조인 포인트 메서드를 실행할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.ProceedingJoinPoint;

@Aspect
public class AroundExample {

    @Around(&quot;com.xyz.myapp.CommonPointcuts.businessService()&quot;)
    public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
        // start stopwatch
        Object retVal = pjp.proceed();
        // stop stopwatch
        return retVal;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Around 어드바이스의 반환 값은 호출 메서드의 반환 값이다.&lt;br /&gt;proceed() 메서드는 바디에서 한 번, 혹은 여러 번 호출되거나 전혀 호출되지 않을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 어드바이스 메서드는 첫 번째 파라미터로 &lt;code&gt;org.aspectj.lang.JoinPoint&lt;/code&gt; 타입을 받을 수 있다.&lt;br /&gt;JoinPoint 인터페이스는 다음과 같은 유용한 메서드를 제공한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;getArgs()&lt;/code&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메서드의 인자 반환&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;getThis()&lt;/code&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프록시 객체 반환&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;getTarget()&lt;/code&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;타깃 객체 반환&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;getSignature()&lt;/code&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;어드바이스된 메서드의 description 반환&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;toString()&lt;/code&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;어드바이스된 메서드의 유용한 description 출력&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메서드의 인자 값을 어드바이스 바디에서 사용 가능하게 하려면, &lt;code&gt;args&lt;/code&gt; 를 사용할 수 있다.&lt;br /&gt;args 표현식에 지정한 타입 이름을 어드바이스의 파라미터로 받을 수 있다.&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;@Before(&quot;com.xyz.myapp.CommonPointcuts.dataAccessOperation() &amp;amp;&amp;amp; args(account,..)&quot;)
public void validateAccount(Account account) {
    // ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;포인트컷 표현식의 &lt;code&gt;args(account,..)&lt;/code&gt; 부분은 두 가지 역할을 한다.&lt;br /&gt;첫 번째는 메서드가 하나 이상의 파라미터를 사용하고 해당 파라미터가 Account의 인스턴스인 메서드 실행부로만 매칭을 제한하는 것이다.&lt;br /&gt;두 번째는 실제 Account 객체를 account 파라미터를 통해 어드바이스에서 사용할 수 있도록 하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 다른 방법은 조인 포인트를 매칭할 때 Account 객체 값을 &quot;제공&quot;하는 포인트컷을 선언한 다음 어드바이스에서 해당 포인트컷을 참조하는 방법이 있다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Pointcut(&quot;com.xyz.myapp.CommonPointcuts.dataAccessOperation() &amp;amp;&amp;amp; args(account,..)&quot;)
private void accountDataAccessOperation(Account account) {}

@Before(&quot;accountDataAccessOperation(account)&quot;)
public void validateAccount(Account account) {
    // ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프록시 객체(&lt;code&gt;this&lt;/code&gt;), 타깃 객체(&lt;code&gt;target&lt;/code&gt;), 어노테이션들(&lt;code&gt;@within&lt;/code&gt;, &lt;code&gt;@target&lt;/code&gt;, &lt;code&gt;@annotation&lt;/code&gt;, &lt;code&gt;@args&lt;/code&gt;)도 마찬가지로 비슷하게 사용될 수 있다.&lt;br /&gt;다음은 어노테이션에 대한 예제이다.&lt;/p&gt;
&lt;pre class=&quot;oxygene&quot;&gt;&lt;code&gt;@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Auditable {
    AuditCode value();
}

@Before(&quot;com.xyz.lib.Pointcuts.anyPublicMethod() &amp;amp;&amp;amp; @annotation(auditable)&quot;)
public void audit(Auditable auditable) {
    AuditCode code = auditable.value();
    // ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 AOP는 클래스와 메서드 파라미터에 있는 제네릭도 다룰 수 있다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;public interface Sample&amp;lt;T&amp;gt; {
    void sampleGenericMethod(T param);
    void sampleGenericCollectionMethod(Collection&amp;lt;T&amp;gt; param);
}

@Before(&quot;execution(* ..Sample+.sampleGenericMethod(*)) &amp;amp;&amp;amp; args(param)&quot;)
public void beforeSampleMethod(MyType param) {
    // Advice implementation
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단, 제네릭 컬렉션은 다음과 같은 형태로 사용할 수 없다.&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;@Before(&quot;execution(* ..Sample+.sampleGenericCollectionMethod(*)) &amp;amp;&amp;amp; args(param)&quot;)
public void beforeSampleMethod(Collection&amp;lt;MyType&amp;gt; param) {
    // Advice implementation
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 작업을 수행하려면 컬렉션의 모든 요소를 검사해야 한다.&lt;br /&gt;이는 일반적으로 null을 처리하는 방법도 결정할 수 없기 때문에 합리적이지 않다.&lt;br /&gt;이와 유사한 결과를 얻으려면 &lt;code&gt;Collection&amp;lt;?&amp;gt;&lt;/code&gt; 에 파라미터를 입력하고 수동으로 검증해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 조인 포인트에 여러 개의 어드바이스가 수행되기를 원하면 어떻게 해야 할까?&lt;br /&gt;스프링 AOP는 AspectJ와 동일한 우선순위 규칙을 따라 어드바이스 실행 순서를 결정한다.&lt;br /&gt;가장 높은 우선순위의 어드바이스가 &quot;들어오면서&quot; 가장 먼저 실행되고, 반대로 &quot;나갈 때는&quot; 가장 나중에 실행된다.&lt;br /&gt;(즉, 두 개의 After 어드바이스가 있을 경우 우선순위가 가장 높은 어드바이스가 두 번째로 실행된다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 애스펙트로 정의한 두 어드바이스가 하나의 조인 포인트에서 실행되어야 할 때, 특별히 지정하지 않는 한 실행 순서는 정의되지 않는다.&lt;br /&gt;당신은 우선순위를 정해서 실행 순서를 제어할 수 있다.&lt;br /&gt;일반적인 스프링에서와 같이 &lt;code&gt;org.springframework.core.Ordered&lt;/code&gt; 인터페이스를 구현하거나 &lt;code&gt;@Order&lt;/code&gt; 어노테이션으로 순서를 지정할 수 있다.&lt;br /&gt;두 애스펙트가 주어진 경우, Ordered.getOrder()에서 더 낮은 값을 반환하는 애스펙트가 더 높은 우선순위를 갖는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선순위는 @Around, @Before, @After, @AfterReturning, @AfterThrowing 순으로 높다.&lt;br /&gt;단, @After 어드바이스는 동일한 애스펙트에서 @AfterReturning 또는 @AfterThrowing 어드바이스 이후에 효과적으로 호출되며, @After에 대한 AspectJ의 &quot;after finally 어드바이스&quot; 의미를 따른다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Introductions&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인트로덕션을 통해 애스펙트는 어드바이스된 객체가 주어진 인터페이스를 구현한다고 선언하며, 해당 객체를 대신하여 인터페이스의 구현체를 제공할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 &lt;code&gt;@DeclareParents&lt;/code&gt; 어노테이션을 통해 만들 수 있다.&lt;br /&gt;이 어노테이션은 매칭 타입이 새로운 부모를 가지도록 선언하는 데 사용된다.&lt;br /&gt;예를 들어 UsageTracked라는 인터페이스와 구현체인 DefaultUsageTracked가 있을 때, 다음 애스펙트는 서비스 인터페이스의 모든 구현체가 UsageTracked 인터페이스도 구현한다고 선언한다.&lt;/p&gt;
&lt;pre class=&quot;d&quot;&gt;&lt;code&gt;@Aspect
public class UsageTracking {

    @DeclareParents(value=&quot;com.xzy.myapp.service.*+&quot;, defaultImpl=DefaultUsageTracked.class)
    public static UsageTracked mixin;

    @Before(&quot;com.xyz.myapp.CommonPointcuts.businessService() &amp;amp;&amp;amp; this(usageTracked)&quot;)
    public void recordUsage(UsageTracked usageTracked) {
        usageTracked.incrementUseCount();
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구현될 인터페이스는 어노테이션 필드의 타입에 따라 결정된다.&lt;br /&gt;&lt;code&gt;@DeclareParents&lt;/code&gt; 어노테이션의 value 속성은 AspectJ 타입 패턴이다.&lt;br /&gt;일치하는 타입의 빈은 UsageTracked 인터페이스를 구현하게 된다.&lt;br /&gt;위 예제의 before 어드바이스에서 서비스 빈은 UsageTracked 인터페이스의 구현체로 직접 사용될 수 있다.&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;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 AOP는 JDK 다이나믹 프록시 또는 CGLIB를 사용하여 타깃 객체에 대한 프록시를 생성한다.&lt;br /&gt;JDK 다이나믹 프록시는 JDK에 내장되어 있는 반면 CGLIB는 일반적인 오픈 소스 정의 라이브러리이다. (&lt;code&gt;spring-core&lt;/code&gt; 에서 재패키징됨)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프록시 타깃 객체가 하나 이상의 인터페이스를 구현하는 경우 JDK 다이나믹 프록시 방식이 사용된다.&lt;br /&gt;타깃 타입에 의해 구현된 모든 인터페이스는 프록싱된다.&lt;br /&gt;만약 타깃이 인터페이스를 구현하지 않았다면 CGLIB 프록시가 생성된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;AOP 프록시 이해하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 AOP는 프록시 기반이다.&lt;br /&gt;나만의 애스펙트를 만들거나 스프링이 지원하는 스프링 AOP 기반의 애스펙트를 사용하기 전에 반드시 이 말이 무슨 말인지를 이해하고 있어야만 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같이 매우 간단한 객체가 있다고 하자.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;public class SimplePojo implements Pojo {

    public void foo() {
        // 아래 메서드 호출은 'this' 참조를 통해 바로 불려진다.
        this.bar();
    }

    public void bar() {
        // some logic...
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체 참조값에서 메서드를 호출하면 메서드가 해당 객체 참조값에서 직접 호출된다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;public class Main {

    public static void main(String[] args) {
        Pojo pojo = new SimplePojo();
        // 이 메서드는 'pojo' 레퍼런스를 호출한다.
        pojo.foo();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트 코드에 있는 참조가 프록시인 경우 상황이 약간 달라진다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/aop_proxy.png&quot; alt=&quot;&quot; /&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1340&quot; data-origin-height=&quot;540&quot; data-filename=&quot;aop_proxy.png&quot; width=&quot;545&quot; height=&quot;220&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vb1Mw/btrazMy4oOj/c9qhkk3MkTCnWOCk0c8Sf0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vb1Mw/btrazMy4oOj/c9qhkk3MkTCnWOCk0c8Sf0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vb1Mw/btrazMy4oOj/c9qhkk3MkTCnWOCk0c8Sf0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fvb1Mw%2FbtrazMy4oOj%2Fc9qhkk3MkTCnWOCk0c8Sf0%2Fimg.png&quot; data-origin-width=&quot;1340&quot; data-origin-height=&quot;540&quot; data-filename=&quot;aop_proxy.png&quot; width=&quot;545&quot; height=&quot;220&quot; data-ke-mobilestyle=&quot;widthOrigin&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;public class Main {

    public static void main(String[] args) {
        ProxyFactory factory = new ProxyFactory(new SimplePojo());
        factory.addInterface(Pojo.class);
        factory.addAdvice(new RetryAdvice());

        Pojo pojo = (Pojo) factory.getProxy();
        // 이 메서드는 프록시를 호출한다!
        pojo.foo();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서의 핵심은 Main 클래스의 main() 메서드 내부에 있는 클라이언트 코드에 프록시 참조가 있다는 것이다.&lt;br /&gt;이는 해당 객체 참조에 대한 메서드 호출이 프록시에 대한 호출임을 의미하고, 결과적으로 프록시는 특정 메서드 호출과 관련된 모든 인터셉터에 기능을 위임할 수 있다.&lt;br /&gt;그러나 this.bar() 또는 this.foo()와 같이 내부의 자체 메서드를 수행하게 되면, 메서드 호출이 타깃 객체에 의해 일어나기 때문에 프록시가 아니게 된다.&lt;br /&gt;이것은 중요한 의미를 가지고 있는데, 이는 자체적인 호출로 인해서는 메소드 호출과 관련된 어드바이스가 실행 기회를 얻지 못한다는 것을 의미한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 해결하기 위한 가장 좋은 접근은 자기자신의 호출을 하지 않도록 리팩토링하는 것이다.&lt;br /&gt;이것이 가장 좋은 해결책이고, 침투적이지 않은 방식이다.&lt;br /&gt;그 다음 선택으로는 절대적으로 끔찍한 방법인데, 다음 예제외 같이 클래스 내의 로직을 스프링 AOP에 완전히 묶어버릴 수 있다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;public class SimplePojo implements Pojo {

    public void foo() {
        // 끔찍한 일이다.
        ((Pojo) AopContext.currentProxy()).bar();
    }

    public void bar() {
        // some logic...
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드는 당신의 코드와 스프링 AOP의 코드를 완전히 커플링하여 클래스 자체가 스프링 AOP를 사용하고 있다는 사실을 완전히 인식하게 한다.&lt;br /&gt;심지어 다음과 같은 추가 설정이 필요하다.&lt;/p&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;public class Main {

    public static void main(String[] args) {
        ProxyFactory factory = new ProxyFactory(new SimplePojo());
        factory.addInterface(Pojo.class);
        factory.addAdvice(new RetryAdvice());
        factory.setExposeProxy(true);

        Pojo pojo = (Pojo) factory.getProxy();
        // this is a method call on the proxy!
        pojo.foo();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 AspectJ는 프록시 기반 AOP 프레임워크가 아니기 때문에 이러한 호출 문제가 없다는 점을 기억해야 한다.&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;@AspectJ 프록시의 프로그래밍 방식 생성&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;org.springframework.aop.aspectj.annotation.AspectJProxyFactory&lt;/code&gt; 클래스를 사용하여 하나 이상의 @AspectJ 애스펙트에서 어드바이스되는 타깃 객체에 대한 프록시를 생성할 수 있다.&lt;br /&gt;예시는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// 타깃 객체 프록시를 생성할 수 있는 팩토리 생성
AspectJProxyFactory factory = new AspectJProxyFactory(targetObject);

// 애스펙트 추가, 클래스는 @AspectJ 애스펙트일 것
// 여러 애스펙트가 필요한 경우 반복 호출 가능
factory.addAspect(SecurityManager.class);

// 애스펙트 인스턴스도 추가 가능, 제공 객체의 타입은 @AspectJ 애스펙트일 것
factory.addAspect(usageTracker);

// 프록시 객체 얻기
MyInterfaceType proxy = factory.getProxy();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;스프링 애플리케이션에서의 AspectJ&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(문서의 구체적인 내용 대신 간략하게 핵심 개념만 정리합니다.)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 AOP는 런타임 시에 IoC 대상 빈에만 사용할 수 있는 프록시 기반 AOP 기술이다.&lt;br /&gt;반면 AspectJ는 컴파일, 로드 시점 위빙을 제공하는 AOP 기술이다.&lt;br /&gt;필요하다면 스프링에서 추가적인 몇 가지 설정을 통해서 AspectJ의 기능들을 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 AOP는 JDK 다이나믹 프록시나 CGLIB 프록시를 사용한 프록시 기반 AOP이고, 메서드 실행 시점만을 조인 포인트로 가진다.&lt;br /&gt;AspectJ가 사용하기 복잡하기 때문에 IoC 컨테이너를 통한 순수 자바 객체만으로 AOP를 사용하기 위해서 나온 기술이라 볼 수 있다.&lt;br /&gt;반면 AspectJ는 완전한 AOP를 제공하는 기술이며, 스프링 AOP보다 강력하다.&lt;br /&gt;런타임 위빙만 지원하는 스프링 AOP에 비해 컴파일 위빙, 로드 시점 위빙을 지원하며(런타임 시에는 아무것도 하지 않는다), 속도도 훨씬 빠르다.&lt;br /&gt;조인 포인트도 메서드 실행 뿐만 아니라 생성자 호출/실행, 객체 초기화, 필드 참조/변경 등 다양한 시점을 가질 수 있다.&lt;/p&gt;</description>
      <category>RTFM</category>
      <category>RTFM</category>
      <category>SpringCore</category>
      <author>wbluke</author>
      <guid isPermaLink="true">https://wbluke.tistory.com/68</guid>
      <comments>https://wbluke.tistory.com/68#entry68comment</comments>
      <pubDate>Tue, 27 Jul 2021 20:41:56 +0900</pubDate>
    </item>
    <item>
      <title>[RTFM] Spring Core - 04. Spring Expression Language (SpEL)</title>
      <link>https://wbluke.tistory.com/67</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;아래 포스팅에 대한 상위 메타 문서는&amp;nbsp;&lt;a href=&quot;https://wbluke.tistory.com/62&quot;&gt;[RTFM] 매일 읽는 공식 문서&lt;/a&gt;&amp;nbsp;를 참고해 주세요.&lt;br /&gt;원문 :&lt;span&gt; &lt;a href=&quot;https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#expressions&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#expressions&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 표현 언어(SpEL)는 런타임에 객체 그래프를 탐색하고 조작할 수 있도록 해주는 강력한 표현 언어이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바 표현 언어는 여러가지(OGNL, MVEL, JBoss EL 등)가 있지만, SpEL은 스프링 프로덕트 전반적으로 사용할 수 있는 좋은 단일 표현 언어로 스프링 커뮤니티에 제공되었다.&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;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 코드는 Hello World 라는 문자열 표현을 평가하는 SpEL API를 소개한다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression(&quot;'Hello World'&quot;); 
String message = (String) exp.getValue(); // Hello World&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SpEL 클래스와 인터페이스는 &lt;code&gt;org.springframework.expression&lt;/code&gt; 패키지와 그 하위 패키지인 &lt;code&gt;spel.support&lt;/code&gt; 에 위치해 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ExpressionParser 인터페이스는 표현 문자열을 파싱하는 책임을 가진다.&lt;br /&gt;위 예제에서, 표현 문자열은 따옴표로 둘러쌓인 문자열인다.&lt;br /&gt;Expression 인터페이스는 표현 문자열을 평가한다.&lt;br /&gt;평가 시 ParseException이나 EvaluationException이 각각 parser.parseExpression()과 exp.getValue()에서 발생할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SpEL은 메서드를 호출하거나 속성에 접근하거나 생성자를 호출하는 등의 다양한 기능을 지원한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 concat 메서드를 호출하는 예제이다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression(&quot;'Hello World'.concat('!')&quot;); 
String message = (String) exp.getValue(); // Hello World!&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 다음은 문자열의 속성값인 Bytes 를 호출하는 예제이다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;ExpressionParser parser = new SpelExpressionParser();

// 'getBytes()' 호출
Expression exp = parser.parseExpression(&quot;'Hello World'.bytes&quot;); 
byte[] bytes = (byte[]) exp.getValue();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;점을 찍는 방식으로 내부 속성에도 접근할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;ExpressionParser parser = new SpelExpressionParser();

// 'getBytes().length' 호출
Expression exp = parser.parseExpression(&quot;'Hello World'.bytes.length&quot;); 
int length = (Integer) exp.getValue();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같이 생성자도 호출할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression(&quot;new String('hello world').toUpperCase()&quot;); 
String message = exp.getValue(String.class);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;public &amp;lt;T&amp;gt; T getValue(Class&amp;lt;T&amp;gt; desiredResultType)&lt;/code&gt; 제네릭 메서드는 결과 값의 타입 캐스팅을 필요없게 한다.&lt;br /&gt;만약 타입 변환 중 실패한다면 EvaluationException을 던진다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;EvaluationContext 이해&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EvaluationContext 인터페이스는 속성, 메서드 또는 필드를 확인하고 형식 변환을 수행하기 위해 표현식을 평가할 때 사용된다.&lt;br /&gt;스프링은 다음 두 가지 구현체를 제공한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SimpleEvaluationContext
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SpEL의 전체 범위가 필요하지 않은 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;StandardEvaluationContext
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SpEL의 전체 기능 제공&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SimpleEvaluationContext는 SpEL 언어의 일부분만 지원한다.&lt;br /&gt;자바 타입 참조, 생성자, 빈 참조 등은 지원하지 않는다.&lt;br /&gt;또한 표현식에서 속성과 메서드의 지원 레벨을 선택하도록 요구한다.&lt;br /&gt;기본적으로는 create() 정적 팩토리 메서드가 속성에 대한 읽기 권한을 제공한다.&lt;br /&gt;또 다음 중 하나 이상의 조합으로 필요한 지원 수준을 구성하는 빌더를 얻을 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자 지정 PropertyAccessor (reflection 없음)&lt;/li&gt;
&lt;li&gt;읽기 전용 액세스를 위한 데이터 바인딩 속성&lt;/li&gt;
&lt;li&gt;읽기 및 쓰기를 위한 데이터 바인딩 속성&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;class Simple {
    public List&amp;lt;Boolean&amp;gt; booleanList = new ArrayList&amp;lt;Boolean&amp;gt;();
}

Simple simple = new Simple();
simple.booleanList.add(true);

EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();

// &quot;false&quot; 는 문자열로 전달된다.
// SpEL과 변환 서비스는 Boolean임을 인식하고 그에 따라 변환한다.
parser.parseExpression(&quot;booleanList[0]&quot;).setValue(context, simple, &quot;false&quot;);

// b는 false
Boolean b = simple.booleanList.get(0);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Parser 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파서 구성 객체를 사용해서 SpEL 표현식 파서를 구성할 수 있다.&lt;br /&gt;구성 객체는 일부 표현식 컴포넌트의 행동을 제어한다.&lt;br /&gt;예를 들어, 배열 또는 컬렉션으로 인덱싱하고 지정된 인덱스의 요소가 null이면, SpEL은 자동으로 해당 요소를 생성한다.&lt;br /&gt;이는 속성 참조 체인으로 만들어진 표현식을 사용할 때 유용하다.&lt;br /&gt;배열 또는 리스트로 인덱싱하고 현 배열 혹은 리스트의 크기를 초과하는 인덱스를 지정하면 SpEL은 해당 배열 또는 리스트를 자동으로 늘릴 수 있다.&lt;br /&gt;지정된 인덱스에 요소를 추가하기 위해 SpEL은 해당 유형의 기본 생성자를 이용하여 요소를 생성하려고 한다.&lt;br /&gt;요소 유형에 기본 생성자가 없으면 null로 추가된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 예제는 목록을 자동으로 늘리는 방법에 대한 예제이다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;class Demo {
    public List&amp;lt;String&amp;gt; list;
}

// 2가지 기능 on
// 자동 null 참조 초기화
// 자동 컬렉션 증가
SpelParserConfiguration config = new SpelParserConfiguration(true,true);

ExpressionParser parser = new SpelExpressionParser(config);

Expression expression = parser.parseExpression(&quot;list[3]&quot;);

Demo demo = new Demo();

Object o = expression.getValue(demo);

// demo.list는 이제 4개의 요소를 갖는 컬렉션이다.
// 각 요소는 빈 문자열이다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;빈 정의에서의 Expressions&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빈 정의 인스턴스를 정의하기 위해 XML 기반 혹은 어노테이션 기반의 SpEL 표현식을 사용할 수 있다.&lt;br /&gt;두 경우 모두 표현식을 정의하는 구문은 &lt;code&gt;#{ &amp;lt;표현식 문자열&amp;gt; }&lt;/code&gt; 이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;어노테이션 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;@Value&lt;/code&gt; 어노테이션을 사용하여 필드, 메서드, 생성자 파라미터 등에 사용할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;public class MovieRecommender {

    private String defaultLocale;

    private CustomerPreferenceDao customerPreferenceDao;

    public MovieRecommender(CustomerPreferenceDao customerPreferenceDao,
            @Value(&quot;#{systemProperties['user.country']}&quot;) String defaultLocale) {
        this.customerPreferenceDao = customerPreferenceDao;
        this.defaultLocale = defaultLocale;
    }

    // ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Language 문서&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;리터럴 표현식&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리터럴 표현식은 문자열, 숫자값, boolean값 그리고 null을 지원한다.&lt;br /&gt;문자열은 홑따옴표로 구분된다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;ExpressionParser parser = new SpelExpressionParser();

// &quot;Hello World&quot;로 평가
String helloWorld = (String) parser.parseExpression(&quot;'Hello World'&quot;).getValue();

double avogadrosNumber = (Double) parser.parseExpression(&quot;6.0221415E+23&quot;).getValue();

// 2147483647로 평가
int maxValue = (Integer) parser.parseExpression(&quot;0x7FFFFFFF&quot;).getValue();

boolean trueValue = (Boolean) parser.parseExpression(&quot;true&quot;).getValue();

Object nullValue = parser.parseExpression(&quot;null&quot;).getValue();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;숫자는 음수 표기, 지수 표기, 소수점 표기를 지원한다.&lt;br /&gt;기본적으로 실수는 Double.parseDouble()로 파싱된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;속성 값, 배열, 리스트, 맵, 그리고 인덱서&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;속성 참조를 탐색하는 것은 쉽다.&lt;br /&gt;마침표를 통해 내부 속성 값을 조회할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;int year = (Integer) parser.parseExpression(&quot;birthdate.year + 1900&quot;).getValue(context);

String city = (String) parser.parseExpression(&quot;placeOfBirth.city&quot;).getValue(context);&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 PlaceOfBirth.city 처럼 첫글자가 대문자여도 된다.&lt;br /&gt;또한 getPlaceOfBirth().getCity() 와 같이 메서드 참조 형태로도 사용할 수 있다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배열과 리스트는 브라켓으로 표현할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();

// Inventions 배열

// &quot;Induction motor&quot;로 평가
String invention = parser.parseExpression(&quot;inventions[3]&quot;).getValue(
        context, tesla, String.class);

// Members 리스트

// &quot;Nikola Tesla&quot;로 평가
String name = parser.parseExpression(&quot;members[0].name&quot;).getValue(
        context, ieee, String.class);

// 리스트와 배열 탐색
// &quot;Wireless communication&quot;으로 평가
String invention = parser.parseExpression(&quot;members[0].inventions[6]&quot;).getValue(
        context, ieee, String.class);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;맵에서는 특별한 문자열 키를 넣어서 표현할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;// Officer's Dictionary

Inventor pupin = parser.parseExpression(&quot;officers['president']&quot;).getValue(
        societyContext, Inventor.class);

// &quot;Idvor&quot;로 평가
String city = parser.parseExpression(&quot;officers['president'].placeOfBirth.city&quot;).getValue(
        societyContext, String.class);

// 값 세팅
parser.parseExpression(&quot;officers['advisors'][0].placeOfBirth.country&quot;).setValue(
        societyContext, &quot;Croatia&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;인라인 리스트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;{}&lt;/code&gt; 를 사용하여 리스트를 직접적으로 표현할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// 4개의 숫자를 가진 리스트로 평가
List numbers = (List) parser.parseExpression(&quot;{1,2,3,4}&quot;).getValue(context);

List listOfLists = (List) parser.parseExpression(&quot;{{'a','b'},{'x','y'}}&quot;).getValue(context);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;{}&lt;/code&gt; 는 그 자체로 빈 리스트를 의미한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;인라인 맵&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;{key:value}&lt;/code&gt; 형태로 맵을 표현할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// 2개의 요소를 가진 맵으로 평가
Map inventorInfo = (Map) parser.parseExpression(&quot;{name:'Nikola',dob:'10-July-1856'}&quot;).getValue(context);

Map mapOfMaps = (Map) parser.parseExpression(&quot;{name:{first:'Nikola',last:'Tesla'},dob:{day:10,month:'July',year:1856}}&quot;).getValue(context);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;{:}&lt;/code&gt; 는 그 자체로 빈 맵을 의미한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;배열 생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;친숙한 자바 문법으로 배열을 생성할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;int[] numbers1 = (int[]) parser.parseExpression(&quot;new int[4]&quot;).getValue(context);

// 초기값을 포함한 배열
int[] numbers2 = (int[]) parser.parseExpression(&quot;new int[]{1,2,3}&quot;).getValue(context);

// 다중 배열
int[][] numbers3 = (int[][]) parser.parseExpression(&quot;new int[4][5]&quot;).getValue(context);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다중 배열의 경우에는 초기값을 선언할 수 없다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;메서드&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같이 메서드를 호출할 수도 있다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;// 문자열 리터럴, &quot;bc&quot;로 평가
String bc = parser.parseExpression(&quot;'abc'.substring(1, 3)&quot;).getValue(String.class);

// true로 평가
boolean isMember = parser.parseExpression(&quot;isMember('Mihajlo Pupin')&quot;).getValue(
        societyContext, Boolean.class);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;연산자&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관계 연산자도 지원이 된다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;// true로 평가
boolean trueValue = parser.parseExpression(&quot;2 == 2&quot;).getValue(Boolean.class);

// false로 평가
boolean falseValue = parser.parseExpression(&quot;2 &amp;lt; -5.0&quot;).getValue(Boolean.class);

// true로 평가
boolean trueValue = parser.parseExpression(&quot;'black' &amp;lt; 'block'&quot;).getValue(Boolean.class);&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;null과의 비교 시 null은 0이 아니라 없는 값으로 비교된다.&lt;br /&gt;null은 그 어떤 값보다 작은 값으로 평가된다. (X &amp;gt; null 은 항상 true, X &amp;lt; null 은 항상 false)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SpEL은 &lt;code&gt;instanceof&lt;/code&gt; 와 &lt;code&gt;matches&lt;/code&gt; 도 지원한다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;// false로 평가
boolean falseValue = parser.parseExpression(
        &quot;'xyz' instanceof T(Integer)&quot;).getValue(Boolean.class);

// true로 평가
boolean trueValue = parser.parseExpression(
        &quot;'5.00' matches '^-?\\d+(\\.\\d{2})?$'&quot;).getValue(Boolean.class);

// false로 평가
boolean falseValue = parser.parseExpression(
        &quot;'5.0067' matches '^-?\\d+(\\.\\d{2})?$'&quot;).getValue(Boolean.class);&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원시 타입은 그 즉시 래퍼 타입으로 박싱된다.&lt;br /&gt;1 instanceof T(int) 는 false로 평가되고, 1 instanceof T(Integer) 는 true로 평가된다.&lt;/p&gt;
&lt;/blockquote&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;code&gt;lt&lt;/code&gt; (&lt;code&gt;&amp;lt;&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;gt&lt;/code&gt; (&lt;code&gt;&amp;gt;&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;le&lt;/code&gt; (&lt;code&gt;&amp;le;&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ge&lt;/code&gt; (&lt;code&gt;&amp;ge;&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;eq&lt;/code&gt; (&lt;code&gt;==&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ne&lt;/code&gt; (&lt;code&gt;&amp;ne;&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;div&lt;/code&gt; (&lt;code&gt;/&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mod&lt;/code&gt; (&lt;code&gt;%&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;not&lt;/code&gt; (&lt;code&gt;!&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&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;code&gt;and&lt;/code&gt; (&lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;or&lt;/code&gt; (&lt;code&gt;!!&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;not&lt;/code&gt; (&lt;code&gt;!&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;// -- AND --

// false로 평가
boolean falseValue = parser.parseExpression(&quot;true and false&quot;).getValue(Boolean.class);

// true로 평가
String expression = &quot;isMember('Nikola Tesla') and isMember('Mihajlo Pupin')&quot;;
boolean trueValue = parser.parseExpression(expression).getValue(societyContext, Boolean.class);

// -- OR --

// true로 평가
boolean trueValue = parser.parseExpression(&quot;true or false&quot;).getValue(Boolean.class);

// true로 평가
String expression = &quot;isMember('Nikola Tesla') or isMember('Albert Einstein')&quot;;
boolean trueValue = parser.parseExpression(expression).getValue(societyContext, Boolean.class);

// -- NOT --

// false로 평가
boolean falseValue = parser.parseExpression(&quot;!true&quot;).getValue(Boolean.class);

// -- AND and NOT --
String expression = &quot;isMember('Nikola Tesla') and !isMember('Mihajlo Pupin')&quot;;
boolean falseValue = parser.parseExpression(expression).getValue(societyContext, Boolean.class);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수식 연산도 다음과 같이 지원한다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;// 덧셈
int two = parser.parseExpression(&quot;1 + 1&quot;).getValue(Integer.class);  // 2

String testString = parser.parseExpression(
        &quot;'test' + ' ' + 'string'&quot;).getValue(String.class);  // 'test string'

// 뺄셈
int four = parser.parseExpression(&quot;1 - -3&quot;).getValue(Integer.class);  // 4

double d = parser.parseExpression(&quot;1000.00 - 1e4&quot;).getValue(Double.class);  // -9000

// 곱셈
int six = parser.parseExpression(&quot;-2 * -3&quot;).getValue(Integer.class);  // 6

double twentyFour = parser.parseExpression(&quot;2.0 * 3e0 * 4&quot;).getValue(Double.class);  // 24.0

// 나눗셈
int minusTwo = parser.parseExpression(&quot;6 / -3&quot;).getValue(Integer.class);  // -2

double one = parser.parseExpression(&quot;8.0 / 4e0 / 2&quot;).getValue(Double.class);  // 1.0

// mod 연산
int three = parser.parseExpression(&quot;7 % 4&quot;).getValue(Integer.class);  // 3

int one = parser.parseExpression(&quot;8 / 5 % 2&quot;).getValue(Integer.class);  // 1

// 연산자 우선순위
int minusTwentyOne = parser.parseExpression(&quot;1+2-3*8&quot;).getValue(Integer.class);  // -21&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;할당 연산자는 &lt;code&gt;=&lt;/code&gt; 이다.&lt;br /&gt;이는 일반적으로 &lt;code&gt;setValue()&lt;/code&gt; 호출 내에서 수행되지만 &lt;code&gt;getValue()&lt;/code&gt; 호출 내에서도 수행될 수 있다.&lt;/p&gt;
&lt;pre class=&quot;vhdl&quot;&gt;&lt;code&gt;Inventor inventor = new Inventor();
EvaluationContext context = SimpleEvaluationContext.forReadWriteDataBinding().build();

parser.parseExpression(&quot;name&quot;).setValue(context, inventor, &quot;Aleksandar Seovic&quot;);

// 대안
String aleks = parser.parseExpression(
        &quot;name = 'Aleksandar Seovic'&quot;).getValue(context, inventor, String.class);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;타입&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특별히 &lt;code&gt;java.lang.Class&lt;/code&gt; 의 인스턴스인 &lt;code&gt;T&lt;/code&gt; 연산자를 사용할 수 있다.&lt;br /&gt;정적 메서드는 이 연산자를 사용해서도 호출할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;Class dateClass = parser.parseExpression(&quot;T(java.util.Date)&quot;).getValue(Class.class);

Class stringClass = parser.parseExpression(&quot;T(String)&quot;).getValue(Class.class);

boolean trueValue = parser.parseExpression(
        &quot;T(java.math.RoundingMode).CEILING &amp;lt; T(java.math.RoundingMode).FLOOR&quot;)
        .getValue(Boolean.class);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;생성자&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;new&lt;/code&gt; 연산자를 사용하여 생성자를 호출할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;Inventor einstein = p.parseExpression(
        &quot;new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')&quot;)
        .getValue(Inventor.class);

// 리스트의 add() 메서드를 호출하면서 Inventor 인스턴스 생성
p.parseExpression(
        &quot;Members.add(new org.spring.samples.spel.inventor.Inventor(
            'Albert Einstein', 'German'))&quot;).getValue(societyContext);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;변수&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;표현식에서 &lt;code&gt;#변수이름&lt;/code&gt; 과 같은 형식으로 변수를 참조할 수 있다.&lt;br /&gt;변수는 EvaluationContext 구현체에서 setVariable() 메서드로 지정할 수 있다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유효한 변수 이름은 영문자, 숫자, 언더바(_), 달러 표시($)의 조합으로 구성해야 한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;Inventor tesla = new Inventor(&quot;Nikola Tesla&quot;, &quot;Serbian&quot;);

EvaluationContext context = SimpleEvaluationContext.forReadWriteDataBinding().build();
context.setVariable(&quot;newName&quot;, &quot;Mike Tesla&quot;);

parser.parseExpression(&quot;name = #newName&quot;).getValue(context, tesla);
System.out.println(tesla.getName())  // &quot;Mike Tesla&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;#this&lt;/code&gt; 변수는 항상 현재 평가하고 있는 객체를 참조한다.&lt;br /&gt;&lt;code&gt;#root&lt;/code&gt; 변수는 항상 루트 컨텍스트 객체를 참조한다.&lt;br /&gt;&lt;code&gt;#this&lt;/code&gt; 가 평가되는 컴포넌트에 따라 항상 변할 수 있는 반면에, &lt;code&gt;#root&lt;/code&gt; 는 항상 루트 객체이다.&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;// 정수 배열 생성
List&amp;lt;Integer&amp;gt; primes = new ArrayList&amp;lt;Integer&amp;gt;();
primes.addAll(Arrays.asList(2,3,5,7,11,13,17));

// 파서 생성 및 정수 배열로 primes 변수 설정
ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataAccess();
context.setVariable(&quot;primes&quot;, primes);

// 10보다 큰 모든 소수
// [11, 13, 17]로 평가
List&amp;lt;Integer&amp;gt; primesGreaterThanTen = (List&amp;lt;Integer&amp;gt;) parser.parseExpression(
        &quot;#primes.?[#this&amp;gt;10]&quot;).getValue(context);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;함수&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;표현식에서 사용자가 만든 함수를 등록하여 SpEL을 확장할 수 있다.&lt;br /&gt;해당 함수는 EvaluationContext를 통해 등록된다.&lt;/p&gt;
&lt;pre class=&quot;vhdl&quot;&gt;&lt;code&gt;ExpressionParser parser = new SpelExpressionParser();

EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
context.setVariable(&quot;reverseString&quot;,
        StringUtils.class.getDeclaredMethod(&quot;reverseString&quot;, String.class)); // 메서드 등록

String helloWorldReversed = parser.parseExpression(
        &quot;#reverseString('hello')&quot;).getValue(context, String.class);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;빈 참조&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;평가 컨텍스트에 빈 리졸버를 구성하면, &lt;code&gt;@&lt;/code&gt; 표기를 이용해 표현식에서 빈을 참조할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;ExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext();
context.setBeanResolver(new MyBeanResolver());

// 평가 시 MyBeanResolver에서 resolve(context, &quot;something&quot;) 호출
Object bean = parser.parseExpression(&quot;@something&quot;).getValue(context);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팩토리 빈 자체에 접근하고 싶다면, &lt;code&gt;&amp;amp;&lt;/code&gt; 표기를 사용하면 된다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;ExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext();
context.setBeanResolver(new MyBeanResolver());

// 평가 시 MyBeanResolver에서 resolve(context, &quot;&amp;amp;foo&quot;) 호출
Object bean = parser.parseExpression(&quot;&amp;amp;foo&quot;).getValue(context);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;삼항 연산자&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;표현식 내에서 if-then-else 조건식을 구성하기 위해 삼항 연산자를 사용할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;String falseString = parser.parseExpression(
        &quot;false ? 'trueExp' : 'falseExp'&quot;).getValue(String.class);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;엘비스 연산자&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;엘비스 연산자는 groovy 언어에서 사용되는 삼항 연산자의 축약 버전이다.&lt;br /&gt;다음과 같이 사용할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;ExpressionParser parser = new SpelExpressionParser();

String name = parser.parseExpression(&quot;name?:'Unknown'&quot;).getValue(new Inventor(), String.class);
System.out.println(name);  // 'Unknown'&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 엘비스 연산자는 표현식에서 기본값을 설정하는 데에 사용할 수 있다.&lt;br /&gt;@Value(&quot;#{systemProperties['pop3.port'] ?: 25}&quot;)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;안전한 탐색 연산자&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안전한 탐색 연산자는 groovy 언어에서 왔고, NPE를 피하기 위한 연산자이다.&lt;br /&gt;전형적으로, 객체를 참조하면 메서드와 속성 값에 접근하기 위해 null이 아님을 보장해야만 했다.&lt;br /&gt;이를 피하기 위해, 안전한 탐색 연산자는 예외를 던지는 대신 null을 반환한다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();

Inventor tesla = new Inventor(&quot;Nikola Tesla&quot;, &quot;Serbian&quot;);
tesla.setPlaceOfBirth(new PlaceOfBirth(&quot;Smiljan&quot;));

String city = parser.parseExpression(&quot;placeOfBirth?.city&quot;).getValue(context, tesla, String.class);
System.out.println(city);  // Smiljan

tesla.setPlaceOfBirth(null);
city = parser.parseExpression(&quot;placeOfBirth?.city&quot;).getValue(context, tesla, String.class);
System.out.println(city);  // null - NPE를 던지지 않는다!&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;컬렉션 선택자&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;선택자는 소스 컬렉션의 속성을 필터링하여 다른 컬렉션으로 변환할 때 사용하는 아주 강력한 표현 언어이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;선택자는 &lt;code&gt;.?[선택 표현식]&lt;/code&gt; 형태로 사용한다.&lt;br /&gt;이는 기존 컬렉션의 특정 부분집합을 필터링하여 새로운 컬렉션을 반환한다.&lt;/p&gt;
&lt;pre class=&quot;sas&quot;&gt;&lt;code&gt;List&amp;lt;Inventor&amp;gt; list = (List&amp;lt;Inventor&amp;gt;) parser.parseExpression(
        &quot;members.?[nationality == 'Serbian']&quot;).getValue(societyContext);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;선택자는 배열과 &lt;code&gt;java.lang.Iterable&lt;/code&gt; , 혹은 &lt;code&gt;java.util.Map&lt;/code&gt; 을 구현한 어떤 것이든 지원한다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;Map newMap = parser.parseExpression(&quot;map.?[value&amp;lt;27]&quot;).getValue();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;선택한 모든 요소를 반환하는 것 외에도 첫 번째 또는 마지막 요소만 선택할 수 있다.&lt;br /&gt;선택 항목과 일치하는 첫 번째 요소를 얻으려면 &lt;code&gt;.^[선택 표현식]&lt;/code&gt; , 마지막으로 일치하는 항목을 얻으려면 &lt;code&gt;.$[선택 표현식]&lt;/code&gt; 을 사용하면 된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;컬렉션 프로젝션&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝션은 컬렉션이 하위 표현식의 평가를 하도록 하고, 결과는 새 컬렉션이 나오도록 한다.&lt;br /&gt;구문은 &lt;code&gt;.![프로젝션 표현식]&lt;/code&gt; 이다.&lt;/p&gt;
&lt;pre class=&quot;sas&quot;&gt;&lt;code&gt;// returns ['Smiljan', 'Idvor' ]
List placesOfBirth = (List)parser.parseExpression(&quot;members.![placeOfBirth.city]&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝션은 배열과 &lt;code&gt;java.lang.Iterable&lt;/code&gt; , 혹은 &lt;code&gt;java.util.Map&lt;/code&gt; 을 구현한 어떤 것이든 지원한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;표현 템플릿&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;표현 템플릿을 사용하면 리터럴 텍스트를 하나 이상의 평가 블록과 조합할 수 있다.&lt;br /&gt;각 평가 블록은 정의할 수 있는 접두사 및 접미사로 표현한다.&lt;br /&gt;보통은 다음과 같이 &lt;code&gt;#{}&lt;/code&gt; 로 표현하는 것이다.&lt;/p&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;String randomPhrase = parser.parseExpression(
        &quot;random number is #{T(java.lang.Math).random()}&quot;,
        new TemplateParserContext()).getValue(String.class);

// evaluates to &quot;random number is 0.7038186818312008&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문자열은 리터럴 텍스트 'random number is'를 &lt;code&gt;#{}&lt;/code&gt; 구분 기호로 평가한 결과와 연결하여 평가된다.&lt;br /&gt;parseExpression()의 두 번째 파라미터는 ParserContext 유형인데, 이 인터페이스는 표현 템플릿 기능을 지원하기 위해 표현식이 분석되는 방식에 영향을 미치는 데 사용된다.&lt;br /&gt;TemplateParserContext의 정의는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;public class TemplateParserContext implements ParserContext {

    public String getExpressionPrefix() {
        return &quot;#{&quot;;
    }

    public String getExpressionSuffix() {
        return &quot;}&quot;;
    }

    public boolean isTemplate() {
        return true;
    }
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>RTFM</category>
      <category>RTFM</category>
      <category>SpringCore</category>
      <author>wbluke</author>
      <guid isPermaLink="true">https://wbluke.tistory.com/67</guid>
      <comments>https://wbluke.tistory.com/67#entry67comment</comments>
      <pubDate>Wed, 21 Jul 2021 20:44:41 +0900</pubDate>
    </item>
    <item>
      <title>[RTFM] Spring Core - 03. Validation, Data Binding, and Type Conversion</title>
      <link>https://wbluke.tistory.com/66</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;아래 포스팅에 대한 상위 메타 문서는&amp;nbsp;&lt;a href=&quot;https://wbluke.tistory.com/62&quot;&gt;[RTFM] 매일 읽는 공식 문서&lt;/a&gt;&amp;nbsp;를 참고해 주세요.&lt;br /&gt;원문 : &lt;a href=&quot;https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#validation&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#validation&lt;/a&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&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;br /&gt;유효성 검사는 웹 계층에 종속적이어서도 안 되며, 현지화가 쉬워야 하고, 어떤 validator도 적용할 수 있어야 한다.&lt;br /&gt;이러한 것들을 고려하여, 스프링은 애플리케이션의 모든 계층에서 기본적이고 탁월한 사용이 가능한 Validator를 제공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 바인딩은 사용자 입력을 애플리케이션 도메인 모델에 동적으로 바인딩하는 데 유용하다.&lt;br /&gt;스프링은 이를 위해 적절한 이름의 DataBinder를 제공한다.&lt;br /&gt;Validator와 DataBinder는 웹 계층에서 주로 사용되지만 국한되지는 않는 validation 패키지를 구성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BeanWrapper는 스프링의 기본 개념이며 많은 곳에서 사용된다.&lt;br /&gt;하지만 BeanWrapper를 직접 사용할 일이 없을 것인데, 공식 문서이다보니 약간은 설명할 필요를 느꼈다.&lt;br /&gt;만약 BeanWrapper를 사용할 일이 있다면, 데이터를 객체에 반영하려고 할 때일 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링의 DataBinder와 낮은 수준의 BeanWrapper는 둘 다 프로퍼티 값들을 파싱하고 포맷팅하기 위해 PropertyEditorSupport 구현체를 사용한다.&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;스프링 Validator 인터페이스를 사용한 유효성 검사&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링은 객체의 유효성을 검사하기 위해 Validator 인터페이스를 제공한다.&lt;br /&gt;Validator 인터페이스는 Errors 객체를 사용하는데, 유효성 검사 시 validator들은 유효성 검사 실패 건을 Errors 객체에 기록한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 String name과 int age를 가지는 Person 객체가 있을 때, 다음과 같이 적용할 수 있다.&lt;br /&gt;Validator 는 2개의 메서드를 구현해야 한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;supports(Class)&lt;/code&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;특정 Class를 지원하는지 여부&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;validate(Object, org.springframework.validation.Errors)&lt;/code&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;유효성 검사 및 Errors 객체에 에러 등록&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;public class PersonValidator implements Validator {

    /**
     * This Validator validates only Person instances
     */
    public boolean supports(Class clazz) {
        return Person.class.equals(clazz);
    }

    public void validate(Object obj, Errors e) {
        ValidationUtils.rejectIfEmpty(e, &quot;name&quot;, &quot;name.empty&quot;);
        Person p = (Person) obj;
        if (p.getAge() &amp;lt; 0) {
            e.rejectValue(&quot;age&quot;, &quot;negativevalue&quot;);
        } else if (p.getAge() &amp;gt; 110) {
            e.rejectValue(&quot;age&quot;, &quot;too.darn.old&quot;);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ValidationUtils의 rejectIfEmpty() 메서드는 name 속성이 null이거나 빈 문자열이면 거절하는 메서드이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조금 더 복잡한 경우에, 예를 들어 Customer 클래스 내에 Address 객체가 있는 경우 단일 Validator 클래스를 구현해서 해결할 수도 있지만, 각각의 Validator를 만들어서 유효성 검사 로직을 캡슐화하는 것이 더 좋다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;public class CustomerValidator implements Validator {

    private final Validator addressValidator;

    public CustomerValidator(Validator addressValidator) {
        if (addressValidator == null) {
            throw new IllegalArgumentException(&quot;The supplied [Validator] is &quot; +
                &quot;required and must not be null.&quot;);
        }
        if (!addressValidator.supports(Address.class)) {
            throw new IllegalArgumentException(&quot;The supplied [Validator] must &quot; +
                &quot;support the validation of [Address] instances.&quot;);
        }
        this.addressValidator = addressValidator;
    }

    /**
     * 이 Validator는 Customer 인스턴스 뿐만 아니라 그 서브클래스들도 검증한다.
     */
    public boolean supports(Class clazz) {
        return Customer.class.isAssignableFrom(clazz);
    }

    public void validate(Object target, Errors errors) {
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, &quot;firstName&quot;, &quot;field.required&quot;);
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, &quot;surname&quot;, &quot;field.required&quot;);
        Customer customer = (Customer) target;
        try {
            errors.pushNestedPath(&quot;address&quot;);
            ValidationUtils.invokeValidator(this.addressValidator, customer.getAddress(), errors);
        } finally {
            errors.popNestedPath();
        }
    }
}&lt;/code&gt;&lt;/pre&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;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MessageSource를 사용하여 에러 메시지를 출력하려면 필드를 거부할 때 제공하는 에러 코드(위 예시의 name 이나 age)를 사용하면 된다.&lt;br /&gt;예를 들어 ValidationUtils 클래스를 사용하여 rejectValue를 호출하거나 Errors 인터페이스의 다른 reject 메서드 중 하나를 호출하면 우리가 전달한 코드를 등록할 뿐만 아니라 여러 추가적인 에러 코드도 같이 전달한다.&lt;br /&gt;MessageCodesResolver는 Errors 인터페이스가 등록하는 에러 코드를 결정한다.&lt;br /&gt;기본적으로는 DefaultMessageCodesResolver가 사용된다.&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;빈 조작과 BeanWrapper&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Bean 패키지에서 가장 중요한 클래스 중 하나는 BeanWrapper 인터페이스와 그 구현체이다.&lt;br /&gt;BeanWrapper는 속성 값을 설정(set)하거나 가져오고(get), 속성을 쿼리하여 읽기 또는 쓰기 가능 여부를 결정하는 기능을 제공한다.&lt;br /&gt;또한 BeanWrapper는 중첩된 속성에 대한 지원을 제공하여 하위 속성의 속성값을 무제한 깊이로 설정할 수 있게 한다.&lt;br /&gt;BeanWrapper는 일반적으로 애플리케이션 코드에서 직접적으로 사용되지는 않지만 DataBinder 및 BeanFactory에서 사용된다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;BeanWrapper company = new BeanWrapperImpl(new Company());
// 회사 이름 설정
company.setPropertyValue(&quot;name&quot;, &quot;Some Company Inc.&quot;);
// ... 이렇게도 가능하다.
PropertyValue value = new PropertyValue(&quot;name&quot;, &quot;Some Company Inc.&quot;);
company.setPropertyValue(value);

// 좋다, 디렉터를 생성하고 회사에 설정해보자.
BeanWrapper jim = new BeanWrapperImpl(new Employee());
jim.setPropertyValue(&quot;name&quot;, &quot;Jim Stravinsky&quot;);
company.setPropertyValue(&quot;managingDirector&quot;, jim.getWrappedInstance());

// 회사를 통해 managingDirector의 급여를 조회
Float salary = (Float) company.getPropertyValue(&quot;managingDirector.salary&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;내장된 PropertyEditor 구현체들&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링은 Object와 String 간 변환을 위해 PropertyEditor를 사용한다.&lt;br /&gt;이는 객체 자체 대신 다른 방식으로 쉽게 속성 값들을 표현할 수 있도록 한다.&lt;br /&gt;예를 들어, Date 객체는 인간친화적인 방식('2007-12-09'와 같은 String)으로 표현될 수 있고, 그 반대도 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내장된 구현체들은 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ByteArrayPropertyEditor&lt;/li&gt;
&lt;li&gt;ClassEditor&lt;/li&gt;
&lt;li&gt;CustomBooleanEditor&lt;/li&gt;
&lt;li&gt;CustomCollectionEditor&lt;/li&gt;
&lt;li&gt;CustomDateEditor&lt;/li&gt;
&lt;li&gt;CustomNumberEditor&lt;/li&gt;
&lt;li&gt;FileEditor&lt;/li&gt;
&lt;li&gt;InputStreamEditor&lt;/li&gt;
&lt;li&gt;LocaleEditor&lt;/li&gt;
&lt;li&gt;PatternEditor&lt;/li&gt;
&lt;li&gt;PropertiesEditor&lt;/li&gt;
&lt;li&gt;StringTrimmerEditor&lt;/li&gt;
&lt;li&gt;URLEditor&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 것들은 커스텀한 PropertyEditor 타입을 등록해서도 구성할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;scala&quot;&gt;&lt;code&gt;package example;

public class ExoticTypeEditor extends PropertyEditorSupport {

    public void setAsText(String text) {
        setValue(new ExoticType(text.toUpperCase()));
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PropertyEditorRegistrar를 이용해서 다음과 같이 커스텀 속성 에디터를 등록할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;package com.foo.editors.spring;

public final class CustomPropertyEditorRegistrar implements PropertyEditorRegistrar {

    public void registerCustomEditors(PropertyEditorRegistry registry) {

        // it is expected that new PropertyEditor instances are created
        registry.registerCustomEditor(ExoticType.class, new ExoticTypeEditor());

        // you could register as many custom property editors as are required here...
    }
}&lt;/code&gt;&lt;/pre&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;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링3에서는 타입 변환을 위한 &lt;code&gt;core.convert&lt;/code&gt; 패키지가 소개되었다.&lt;br /&gt;스프링 컨테이너 내에서 이 시스템을 PropertyEditor 구현의 대안으로 사용하여 빈 속성 값 문자열을 필수 타입으로 변환할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Converter SPI&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타입 변환을 구현하는 SPI는 간단하고 강력한 형식이다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;package org.springframework.core.convert.converter;

public interface Converter&amp;lt;S, T&amp;gt; {

    T convert(S source);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Converter를 만들고 싶다면, Converter 인터페이스를 구현하고 변환 전 타입인 S와 변환할 타입인 T를 지정해주면 된다.&lt;br /&gt;마찬가지로 S 배열을 컬렉션 T로 변환하고 싶은 경우에도, Converter를 적용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;convert(S)&lt;/code&gt; 호출 시 인자인 S는 null이 아님이 보장되어야만 한다.&lt;br /&gt;Converter는 변환 실패 시 언체크 예외를 던질 것이다.&lt;br /&gt;특별히, 유효하지 않은 값에 대해서는 IllegalArgumentException을 던져야 한다.&lt;br /&gt;또한 Converter는 항상 스레드 안전해야함을 명심하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;core.convert.support&lt;/code&gt; 패키지에 있는 여러 컨버터도 편의를 위해 제공된다.&lt;br /&gt;예를 들어 StringToInteger 같은 경우는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;package org.springframework.core.convert.support;

final class StringToInteger implements Converter&amp;lt;String, Integer&amp;gt; {

    public Integer convert(String source) {
        return Integer.valueOf(source);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ConverterFactory 사용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 계층 구조의 전체 클래스를 대상으로 변환 로직을 중앙화하고 싶은 경우, ConverterFactory를 사용할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;package org.springframework.core.convert.converter;

public interface ConverterFactory&amp;lt;S, R&amp;gt; {

    &amp;lt;T extends R&amp;gt; Converter&amp;lt;S, T&amp;gt; getConverter(Class&amp;lt;T&amp;gt; targetType);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;S는 변환하기 전 타입, R은 변환하고자 하는 클래스의 범위(range)를 지정하면 된다.&lt;br /&gt;&lt;code&gt;getConverter(Class&amp;lt;T&amp;gt;)&lt;/code&gt; 의 T는 R의 서브클래스이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;StringToEnumConverterFactory 예시는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;package org.springframework.core.convert.support;

final class StringToEnumConverterFactory implements ConverterFactory&amp;lt;String, Enum&amp;gt; {

    public &amp;lt;T extends Enum&amp;gt; Converter&amp;lt;String, T&amp;gt; getConverter(Class&amp;lt;T&amp;gt; targetType) {
        return new StringToEnumConverter(targetType);
    }

    private final class StringToEnumConverter&amp;lt;T extends Enum&amp;gt; implements Converter&amp;lt;String, T&amp;gt; {

        private Class&amp;lt;T&amp;gt; enumType;

        public StringToEnumConverter(Class&amp;lt;T&amp;gt; enumType) {
            this.enumType = enumType;
        }

        public T convert(String source) {
            return (T) Enum.valueOf(this.enumType, source.trim());
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;GenericConverter 사용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 정교한 Converter 구현체가 필요하다면, GenericConverter 인터페이스를 고려해보면 좋다.&lt;br /&gt;유연하지만 강한 타입은 아닌 Converter 대신, GenericConverter는 여러 개의 소스와 타겟 간 타입 변환을 지원한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;package org.springframework.core.convert.converter;

public interface GenericConverter {

    public Set&amp;lt;ConvertiblePair&amp;gt; getConvertibleTypes();

    Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GenericConverter를 구현하면, &lt;code&gt;getConvertibleTypes()&lt;/code&gt; 는 지원하는 소스 &amp;rarr; 타겟 타입 페어를 반환한다.&lt;br /&gt;&lt;code&gt;convert(Object, TypeDescriptor, TypeDescriptor)&lt;/code&gt; 에는 변환 로직을 구현하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GenericConverter의 좋은 예시는 자바의 배열과 컬렉션 간 변환이다.&lt;br /&gt;ArrayToCollectionConverter는 소스 배열이 타겟 컬렉션으로 변환되도록 해준다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GenericConverter는 좀 더 복잡한 SPI 인터페이스이기 때문에, 꼭 필요할 때만 사용하면 된다.&lt;br /&gt;보통은 Converter나 ConverterFactory로 사용한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가끔은, Converter가 특정 조건에서만 작동하기를 바라는 경우가 있다.&lt;br /&gt;예를 들어, 어떤 타겟 필드에 특정 어노테이션이 붙어있을 때에만 변환하기를 바라거나, 어떤 클래스에 특정 메서드가 구현되어 있을 경우에만 변환하기를 바랄 수 있다.&lt;br /&gt;ConditionalGenericConverter는 이러한 것들을 가능하게 하는 GenericConverter와 ConditionalConverter 인터페이스의 조합이다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;public interface ConditionalConverter {

    boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType);
}

public interface ConditionalGenericConverter extends GenericConverter, ConditionalConverter {
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ConditionalGenericConverter의 좋은 예시는 영속화된 엔티티의 식별자와 엔티티 참조 간을 변환하는 IdToEntityConverter이다.&lt;br /&gt;IdToEntityConverter는 타겟 엔티티 타입이 정적 finder 메서드를 선언한 경우에만 변환을 진행한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ConversionService API&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ConversionService는 런타임에 타입 변환을 실행하기 위한 통합 API를 제공한다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;package org.springframework.core.convert;

public interface ConversionService {

    boolean canConvert(Class&amp;lt;?&amp;gt; sourceType, Class&amp;lt;?&amp;gt; targetType);

    &amp;lt;T&amp;gt; T convert(Object source, Class&amp;lt;T&amp;gt; targetType);

    boolean canConvert(TypeDescriptor sourceType, TypeDescriptor targetType);

    Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대부분의 ConversionService 구현체는 ConverterRegistry도 구현하는데, converter를 등록하기 위한 SPI를 제공하기 위해서이다.&lt;br /&gt;내부적으로 ConversionService 구현체는 타입 변환 로직을 등록된 converter에 위임한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ConversionService는 &lt;code&gt;core.convert.support&lt;/code&gt; 패키지에서 제공된다.&lt;br /&gt;GenericConversionService는 대부분의 환경에서 사용되기 위한 일반적인 목적의 구현체이다.&lt;br /&gt;ConversionServiceFactory는 ConversionService를 생성하기 위한 편리한 팩토리를 제공한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ConversionService 구성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ConversionService는 애플리케이션 시작 시 초기화되고, 여러 스레드에 의해 공유되도록 만들어진 무상태 객체이다.&lt;br /&gt;스프링 애플리케이션에서는 전형적으로 스프링 컨테이너마다 ConversionService를 구성해야 한다.&lt;br /&gt;스프링은 프레임워크에 의한 타입 변환이 필요한 경우 ConversionService를 사용한다.&lt;br /&gt;또한 ConversionService를 빈에 직접 주입해서 사용할 수도 있다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 어떤 ConversionService도 스프링에 등록되지 않는다면, 기존의 PropertyEditor 기반 시스템이 사용된다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;프로그래밍 방식으로 ConversionService 사용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ConversionService를 프로그래밍 방식으로 사용하고자 한다면, 다음과 같이 빈에 주입해서 사용할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Service
public class MyService {

    public MyService(ConversionService conversionService) {
        this.conversionService = conversionService;
    }

    public void doIt() {
        this.conversionService.convert(...)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대부분의 경우 타겟 타입으로의 변환을 위해 &lt;code&gt;convert()&lt;/code&gt; 메서드를 사용하겠지만, 이는 매개변수화된 컬렉션과 같은 복잡한 타입에서는 동작하지 않는다.&lt;br /&gt;예를 들어 정수 리스트를 문자열 리스트로 변환하고자 할 때, 소스와 타겟 타입에 대한 정의가 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다행히도, TypeDescriptor는 이런 상황을 해결할 수 있는 옵션을 제공한다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;DefaultConversionService cs = new DefaultConversionService();

List&amp;lt;Integer&amp;gt; input = ...
cs.convert(input,
    TypeDescriptor.forObject(input), // List&amp;lt;Integer&amp;gt; type descriptor
    TypeDescriptor.collection(List.class, TypeDescriptor.valueOf(String.class)));&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DefaultConversionService는 대부분의 환경에 적합한 converter를 자동으로 등록한다.&lt;br /&gt;여기에는 컬렉션 converter, 스칼라 converter 및 기본 Object-String converter가 포함된다.&lt;br /&gt;DefaultConversionService 클래스에서 정적 addDefaultConverters 메서드를 사용하여 ConverterRegistry에 converter를 등록 할 수 있다.&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;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 웹이나 데스크탑 환경과 같은 일반적인 클라이언트 환경에서의 타입 변환을 생각해 보자.&lt;br /&gt;이런 환경에서는, 문자열을 필요한 타입으로 변환하고, 다시 뷰 렌터링을 위해 문자열로 변환하기도 한다.&lt;br /&gt;Converter SPI는 이러한 형식 요구 사항을 직접 해결하지 않는다.&lt;br /&gt;이를 해결하기 위해 스프링 3은 클라이언트 환경을 위한 PropertyEditor 구현체의 강력한 대안으로 편리한 Formatter SPI를 도입했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 범용적인 목적의 타입 변환 로직(예를 들어, &lt;code&gt;java.util.Date&lt;/code&gt; 와 Long 간 변환)을 구현해야 할 때 Converter SPI를 사용할 수 있다.&lt;br /&gt;그리고 클라이언트 환경에서 작업하고 현지화된 필드 값을 파싱하고 출력해야 하는 경우 Formatter SPI를 사용할 수 있다.&lt;br /&gt;ConversionService는 두 SPI에 대한 통합 타입 API를 제공한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Formatter SPI&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Formatter SPI는 간단하고 강력한 포맷팅 로직을 제공한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;package org.springframework.format;

public interface Formatter&amp;lt;T&amp;gt; extends Printer&amp;lt;T&amp;gt;, Parser&amp;lt;T&amp;gt; {
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Formatter는 Printer와 Parser를 확장한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;public interface Printer&amp;lt;T&amp;gt; {

    String print(T fieldValue, Locale locale);
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;import java.text.ParseException;

public interface Parser&amp;lt;T&amp;gt; {

    T parse(String clientValue, Locale locale) throws ParseException;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Formatter를 만들려면, Formatter 인터페이스를 구현한다.&lt;br /&gt;매개변수 T는 Date와 같이 포맷팅하려는 타입 객체이다.&lt;br /&gt;print() 메서드는 T를 클라이언트 환경에 보여주기 위한 메서드이고, parse() 메서드는 반대로 포맷팅된 표현을 T 타입 인스턴스로 변환하기 위한 메서드이다.&lt;br /&gt;파싱에 실패하면 ParseException이나 IllegalArgumentException을 던져야 한다.&lt;br /&gt;Formatter 구현체는 스레드 안전해야 함을 명심하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DateFormatter의 예시는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;package org.springframework.format.datetime;

public final class DateFormatter implements Formatter&amp;lt;Date&amp;gt; {

    private String pattern;

    public DateFormatter(String pattern) {
        this.pattern = pattern;
    }

    public String print(Date date, Locale locale) {
        if (date == null) {
            return &quot;&quot;;
        }
        return getDateFormat(locale).format(date);
    }

    public Date parse(String formatted, Locale locale) throws ParseException {
        if (formatted.length() == 0) {
            return null;
        }
        return getDateFormat(locale).parse(formatted);
    }

    protected DateFormat getDateFormat(Locale locale) {
        DateFormat dateFormat = new SimpleDateFormat(this.pattern, locale);
        dateFormat.setLenient(false);
        return dateFormat;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;어노테이션 기반 포맷팅&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필드 포맷팅은 필트 타입이나 어노테이션으로 구성될 수 있다.&lt;br /&gt;어노테이션을 Formatter로 바인딩하려면, AnnotationFormatterFactory를 구현한다.&lt;/p&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;package org.springframework.format;

public interface AnnotationFormatterFactory&amp;lt;A extends Annotation&amp;gt; {

    Set&amp;lt;Class&amp;lt;?&amp;gt;&amp;gt; getFieldTypes();

    Printer&amp;lt;?&amp;gt; getPrinter(A annotation, Class&amp;lt;?&amp;gt; fieldType);

    Parser&amp;lt;?&amp;gt; getParser(A annotation, Class&amp;lt;?&amp;gt; fieldType);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구현체를 만들려면, 매개변수 A에 포맷팅 로직과 관련시킬 어노테이션 타입을 적용하면 된다. (예 : DateTimeFormat)&lt;br /&gt;그리고 getPrinter(), getParser() 각 메서드에서 필요한 Printer와 Parser를 반환하도록 구현하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 예시는 &lt;code&gt;@NumberFormat&lt;/code&gt; 어노테이션에 대한 AnnotationFormatterFactory 구현체이다.&lt;/p&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;public final class NumberFormatAnnotationFormatterFactory
        implements AnnotationFormatterFactory&amp;lt;NumberFormat&amp;gt; {

    public Set&amp;lt;Class&amp;lt;?&amp;gt;&amp;gt; getFieldTypes() {
        return new HashSet&amp;lt;Class&amp;lt;?&amp;gt;&amp;gt;(asList(new Class&amp;lt;?&amp;gt;[] {
            Short.class, Integer.class, Long.class, Float.class,
            Double.class, BigDecimal.class, BigInteger.class }));
    }

    public Printer&amp;lt;Number&amp;gt; getPrinter(NumberFormat annotation, Class&amp;lt;?&amp;gt; fieldType) {
        return configureFormatterFrom(annotation, fieldType);
    }

    public Parser&amp;lt;Number&amp;gt; getParser(NumberFormat annotation, Class&amp;lt;?&amp;gt; fieldType) {
        return configureFormatterFrom(annotation, fieldType);
    }

    private Formatter&amp;lt;Number&amp;gt; configureFormatterFrom(NumberFormat annotation, Class&amp;lt;?&amp;gt; fieldType) {
        if (!annotation.pattern().isEmpty()) {
            return new NumberStyleFormatter(annotation.pattern());
        } else {
            Style style = annotation.style();
            if (style == Style.PERCENT) {
                return new PercentStyleFormatter();
            } else if (style == Style.CURRENCY) {
                return new CurrencyStyleFormatter();
            } else {
                return new NumberStyleFormatter();
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같은 형식으로 사용 가능하다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;public class MyModel {

    @NumberFormat(style=Style.CURRENCY)
    private BigDecimal decimal;

    @DateTimeFormat(iso=ISO.DATE)
    private Date date;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;FormatterRegistry SPI&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FormatterRegistry는 formatter와 converter를 등록하기 위한 SPI이다.&lt;br /&gt;FormatterConversionService는 대부분의 환경에서 사용하기 적합한 FormatterRegistry 구현체이다.&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;package org.springframework.format;

public interface FormatterRegistry extends ConverterRegistry {

    void addPrinter(Printer&amp;lt;?&amp;gt; printer);

    void addParser(Parser&amp;lt;?&amp;gt; parser);

    void addFormatter(Formatter&amp;lt;?&amp;gt; formatter);

    void addFormatterForFieldType(Class&amp;lt;?&amp;gt; fieldType, Formatter&amp;lt;?&amp;gt; formatter);

    void addFormatterForFieldType(Class&amp;lt;?&amp;gt; fieldType, Printer&amp;lt;?&amp;gt; printer, Parser&amp;lt;?&amp;gt; parser);

    void addFormatterForFieldAnnotation(AnnotationFormatterFactory&amp;lt;? extends Annotation&amp;gt; annotationFormatterFactory);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FormatterRegistry SPI를 사용하면 컨트롤러 전체에 중복 설정을 하는 대신, 포맷팅 규칙을 중앙화할 수 있다.&lt;br /&gt;예를 들어 모든 날짜 필드가 특정 방식으로 포맷팅되거나 특정 어노테이션이 있는 필드가 특정 방식으로 포맷팅되도록 할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;FormatterRegistrar SPI&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FormatterRegistrar는 FormatterRegistry를 통해 formatter 및 converter를 등록하기 위한 SPI이다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;package org.springframework.format;

public interface FormatterRegistrar {

    void registerFormatters(FormatterRegistry registry);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FormatterRegistrar는 날짜 형식과 같은 포맷팅 카테고리에 대해 여러 관련된 converter와 formatter를 등록할 때 유용하다.&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;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 &lt;code&gt;@DateTimeFormat&lt;/code&gt; 을 사용하지 않은 날짜와 시간은 DateFormat.SHORT를 사용하여 문자열로 변환된다.&lt;br /&gt;원하는 경우 전역 포맷을 지정하여 변경할 수 있다.&lt;br /&gt;그렇게 하려면, 스프링이 기본 포맷터를 등록하지 않음을 보장해야 한다.&lt;br /&gt;다음 클래스의 도움을 받으면 된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;org.springframework.format.datetime.standard.DateTimeFormatterRegistrar&lt;/li&gt;
&lt;li&gt;org.springframework.format.datetime.DateFormatterRegistrar&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 예제는 전역적인 &lt;code&gt;yyyyMMdd&lt;/code&gt; 포맷을 등록하는 설정 예제이다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@Configuration
public class AppConfig {

    @Bean
    public FormattingConversionService conversionService() {

        // DefaultFormattingConversionService 사용, 기본 등록은 제외
        DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService(false);

        // @NumberFormat 지원은 보장
        conversionService.addFormatterForFieldAnnotation(new NumberFormatAnnotationFormatterFactory());

        // 특정 전역 포맷을 사용한 JSR-310 날짜 변환 등록
        DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar();
        registrar.setDateFormatter(DateTimeFormatter.ofPattern(&quot;yyyyMMdd&quot;));
        registrar.registerFormatters(conversionService);

        // 특정 전역 포맷을 사용한 날짜 변환 등록
        DateFormatterRegistrar registrar = new DateFormatterRegistrar();
        registrar.setFormatter(new DateFormatter(&quot;yyyyMMdd&quot;));
        registrar.registerFormatters(conversionService);

        return conversionService;
    }
}&lt;/code&gt;&lt;/pre&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;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;빈 유효성 검사 개요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빈 유효성 검사는 제약 선언과 메타 데이터를 통한 일반적인 유효성 검사를 제공한다.&lt;br /&gt;이를 사용하려면, 도메인 모델 속성에 런타임 시점에 동작하는 유효성 검사 제약을 선언하면 된다.&lt;br /&gt;기본 제공되는 제약도 있고, 커스텀하게 지정할 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 빈 유효성 검사를 제공한 PersonForm 클래스는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;public class PersonForm {

    @NotNull
    @Size(max=64)
    private String name;

    @Min(0)
    private int age;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;빈 유효성 검사 제공자 구성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링은 빈 유효성 검사 제공자를 포함한 빈 유효성 검사 API를 전격 지원한다.&lt;br /&gt;&lt;code&gt;javax.validation.ValidatorFactory&lt;/code&gt; 또는 &lt;code&gt;javax.validation.Validator&lt;/code&gt; 를 주입해서 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 Validator를 스프링 빈으로 구성하기 위해 LocalValidatorFactoryBean을 사용할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;

@Configuration
public class AppConfig {

    @Bean
    public LocalValidatorFactoryBean validator() {
        return new LocalValidatorFactoryBean();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LocalValidatorFactoryBean은 &lt;code&gt;javax.validation.ValidatorFactory&lt;/code&gt; 와 &lt;code&gt;javax.validation.Validator&lt;/code&gt; 를 구현하고, 스프링의 &lt;code&gt;org.springframework.validation.Validator&lt;/code&gt; 도 구현한다.&lt;br /&gt;유효성 검사 로직이 필요한 빈에 이러한 인터페이스 중 하나에 대한 참조를 주입할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빈 유효성 검사 API 사용을 원한다면, &lt;code&gt;javax.validation.Validator&lt;/code&gt; 를 주입받을 수 있다.&lt;/p&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;import javax.validation.Validator;

@Service
public class MyService {

    @Autowired
    private Validator validator;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 유효성 검사 API가 필요한 빈이라면, &lt;code&gt;org.springframework.validation.Validator&lt;/code&gt; 를 주입할 수도 있다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;import org.springframework.validation.Validator;

@Service
public class MyService {

    @Autowired
    private Validator validator;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각각의 빈 유효성 검사는 다음 2가지로 구성된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;제약과 그 속성값을 선언하는 &lt;code&gt;@Constraint&lt;/code&gt; 어노테이션&lt;/li&gt;
&lt;li&gt;제약의 행동을 구현하는 &lt;code&gt;javax.validation.ConstraintValidator&lt;/code&gt; 구현체&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;선언을 구현부와 연결하기 위해 @Constraint 어노테이션은 해당하는 ConstraintValidator 구현체를 참조한다.&lt;br /&gt;런타임 시 ConstraintValidatorFactory는 도메인 모델에서 해당 어노테이션이 발견될 때 참조된 구현체를 인스턴스화한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 LocalValidatorFactoryBean은 스프링을 사용하여 ConstraintValidator 인스턴스를 만드는 SpringConstraintValidatorFactory를 구성한다.&lt;br /&gt;이렇게 하면 커스텀 ConstraintValidators가 다른 스프링 빈과 마찬가지로 의존성 주입의 이점을 얻을 수 있다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy=MyConstraintValidator.class)
public @interface MyConstraint {
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;import javax.validation.ConstraintValidator;

public class MyConstraintValidator implements ConstraintValidator {

    @Autowired;
    private Foo aDependency;

    // ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;DataBinder 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링3 부터, Validator와 함께 DataBinder 인스턴스를 구성할 수 있다.&lt;br /&gt;구성한 후 binder.validate() 메서드를 호출하여 Validator를 사용할 수 있다.&lt;br /&gt;발생한 유효성 에러는 binder의 BindingResult에 추가된다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;Foo target = new Foo();
DataBinder binder = new DataBinder(target);
binder.setValidator(new FooValidator());

// 타겟 객체 바인딩
binder.bind(propertyValues);

// 타겟 객체 유효성 검사
binder.validate();

// 유효성 에러를 가진 BindingResult
BindingResult results = binder.getBindingResult();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;dataBinder.addValidators(), dataBinder.replaceValidators() 를 사용하여 다수의 Validator 인스턴스를 등록할 수도 있다.&lt;br /&gt;이는 전역적으로 구성된 빈 유효성 검사를 DataBinder 인스턴스에 로컬로 구성된 스프링 Validator와 결합할 때 유용하다.&lt;/p&gt;</description>
      <category>RTFM</category>
      <category>RTFM</category>
      <category>SpringCore</category>
      <author>wbluke</author>
      <guid isPermaLink="true">https://wbluke.tistory.com/66</guid>
      <comments>https://wbluke.tistory.com/66#entry66comment</comments>
      <pubDate>Wed, 14 Jul 2021 20:36:22 +0900</pubDate>
    </item>
    <item>
      <title>[RTFM] Spring Core - 02. Resources</title>
      <link>https://wbluke.tistory.com/65</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;아래 포스팅에 대한 상위 메타 문서는&amp;nbsp;&lt;a href=&quot;https://wbluke.tistory.com/62&quot;&gt;[RTFM] 매일 읽는 공식 문서&lt;/a&gt;&amp;nbsp;를 참고해 주세요.&lt;br /&gt;원문 : &lt;a href=&quot;https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#resources&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#resources&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;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바 표준 &lt;code&gt;java.net.URL&lt;/code&gt; 클래스와 표준 URL 핸들러들은 불행히도, 낮은 레벨의 리소스에 접근하기에는 충분하지 않다.&lt;br /&gt;예를 들어, 클래스 경로나 ServletContext에 관련된 리소스에 접근하는 표준화된 URL 구현체가 없다.&lt;br /&gt;특별한 URL 접두사를 위한 핸들러를 등록할 수 있지만, 일반적으로 꽤 복잡하며, URL 인터페이스는 리소스가 실제로 존재하는지를 체크하는 기능 등이 부족하다.&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;Resource 인터페이스&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;org.springframework.core.io&lt;/code&gt; 패키지에 있는 &lt;code&gt;Resource&lt;/code&gt; 인터페이스는 낮은 레벨의 리소스에 접근하기 위한 유능한 인터페이스이다.&lt;br /&gt;Resource 인터페이스는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;public interface Resource extends InputStreamSource {

    boolean exists();

    boolean isReadable();

    boolean isOpen();

    boolean isFile();

    URL getURL() throws IOException;

    URI getURI() throws IOException;

    File getFile() throws IOException;

    ReadableByteChannel readableChannel() throws IOException;

    long contentLength() throws IOException;

    long lastModified() throws IOException;

    Resource createRelative(String relativePath) throws IOException;

    String getFilename();

    String getDescription();
}&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;code&gt;getInputStream()&lt;/code&gt; (InputStreamSource에 있다.)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;리소스를 읽어서 InputStream으로 반환해준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;exists()&lt;/code&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;리소스가 물리적으로 존재하는지를 확인한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;isOpen()&lt;/code&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;리소스가 stream으로 핸들링할 수 있는지를 나타낸다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 메서드들은 리소스를 표현하는 URL이나 File 객체를 제공한다.&lt;br /&gt;Resource의 어떤 구현체들은 &lt;code&gt;WritableResource&lt;/code&gt; 인터페이스도 구현해서 리소스에 쓰기 작업을 하기도 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링은 자체적으로 Resource가 필요한 경우 메서드 시그니처의 인자 타입 등으로 광범위하게 사용한다.&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;내장된 Resource 구현체&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;UrlResource&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;UrlResource&lt;/code&gt; 는 &lt;code&gt;java.net.URL&lt;/code&gt; 을 포장하며, 파일, HTTPS 타겟, FTP 타겟 등 URL로 접근할 수 있는 어떤 객체든 사용 가능하다.&lt;br /&gt;모든 URL은 표준화된 문자열 표현을 가지고 있는데, 예를 들어 적절한 표준 접두사를 특정 URL 타입을 호출하기 위해 사용하는 식이다.&lt;br /&gt;이는 파일 시스템 경로로 접근하기 위한 &lt;code&gt;file:&lt;/code&gt; , HTTPS 프로토콜을 통해 리소스에 접근하기 위한 &lt;code&gt;https:&lt;/code&gt; , FTP를 통해 리소스에 접근하기 위한 &lt;code&gt;ftp:&lt;/code&gt; 등을 포함한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UrlResource는 명시적으로 생성자를 사용하여 자바 코드에서 생성되지만 경로를 나타내기 위한 문자열 인수를 사용하는 API 메서드를 호출할 때 내부적으로 생성되는 경우가 많다.&lt;br /&gt;JavaBeans의 PropertyEditor는 생성할 리소스의 타입을 결정한다.&lt;br /&gt;접두사가 포함된 경우 적절한 리소스를 생성하고, 접두사를 인식하지 못하는 경우 해당 문자열을 표준 URL로 가정하여 UrlResource를 생성한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ClassPathResource&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 클래스는 클래스패스를 통해 얻어지는 리소스를 표현한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ClassPathResource는 명시적으로 생성자를 사용하여 자바 코드에서 생성되지만 경로를 나타내기 위한 문자열 인수를 사용하는 API 메서드를 호출할 때 내부적으로 생성되는 경우가 많다.&lt;br /&gt;JavaBeans의 PropertyEditor는 &lt;code&gt;classpath:&lt;/code&gt; 접두사를 인식하고 ClassPathResource를 생성한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;FileSystemResouce&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 Resource 구현체는 &lt;code&gt;java.io.File&lt;/code&gt; 을 핸들링하는 구현체이다.&lt;br /&gt;또한 &lt;code&gt;java.nio.file.Path&lt;/code&gt; 핸들링도 지원하여 스프링의 표준 문자열 기반 경로 변환을 적용하지만 &lt;code&gt;java.nio.file.Files&lt;/code&gt; API를 통해 모든 작업을 수행한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;PathResource&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Path&lt;/code&gt; API를 통해 모든 작업과 변환을 수행하는 &lt;code&gt;java.nio.file.Path&lt;/code&gt; 핸들링에 대한 Resource 구현체이다.&lt;br /&gt;&lt;code&gt;File&lt;/code&gt; 과 &lt;code&gt;URL&lt;/code&gt; 에 대한 확인을 지원하고WritableResource 인터페이스도 구현한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ServletContextResource&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 응용 애플리케이션의 루트 디렉터리 내 상대 경로를 해석하는 ServletContext 리소스에 대한 Resource 구현체이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;InputStreamResource&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주어진 InputStream에 대한 Resource 구현체이다.&lt;br /&gt;특정 Resource 구현체가 적용되지 않는 경우에만 사용해야 한다.&lt;br /&gt;특히 가능한 경우 ByteArrayResource나 다른 파일 기반 Resource 구현체를 선호한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 Resource 구현체와 달리 이는 이미 열린 리소스에 대한 설명자이다.&lt;br /&gt;따라서 isOpen()에서 true를 반환한다.&lt;br /&gt;리소스 설명자를 어딘가에 보관해야 하거나 스트림을 여러번 읽어야하는 경우에는 사용하면 안 된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ByteArrayResource&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주어진 바이트 배열에 대한 Resource 구현체이다.&lt;br /&gt;주어진 바이트 배열로 ByteArrayInputStream을 생성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일회용 InputStreamResource에 의존하지 않고도 주어진 바이트 배열에서 콘텐츠를 로드하는 데 유용하다.&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;ResourceLoader 인터페이스&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ResourceLoader 인터페이스는 Resource 인스턴스를 반환하는 객체에 의해 구현된다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;public interface ResourceLoader {

    Resource getResource(String location);

    ClassLoader getClassLoader();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 애플리케이션 컨텍스트는 ResourceLoader 인터페이스를 구현한다.&lt;br /&gt;그러므로 모든 애플리케이션 컨텍스트는 Resource 인스턴스를 얻는 데 사용될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 애플리케이션 컨텍스트에서 getResource()를 호출하면, 그리고 위치 패스에 특별한 접두사가 없으면, 애플리케이션 컨텍스트에 맞는 Resource 타입을 반환받게 된다.&lt;br /&gt;예를 들어, 다음 코드처럼 ClassPathXmlApplicationContext 인스턴스를 통해 Resource를 받는다고 가정해 보자.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;Resource template = ctx.getResource(&quot;some/resource/path/myTemplate.txt&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ClassPathXmlApplicationContext를 통하면 코드는 ClassPathResource를 반환한다.&lt;br /&gt;마찬가지로 FileSystemXmlApplicationContext 인스턴스를 통하면 동일한 메서드는 FileSystemResource를 반환한다.&lt;br /&gt;WebApplicationContext는 ServletContextResource를 반환한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면, ClassPathResource를 반환하도록 강제하고 싶은 경우, &lt;code&gt;classpath:&lt;/code&gt; 라는 접두사를 사용하면 된다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;Resource template = ctx.getResource(&quot;classpath:some/resource/path/myTemplate.txt&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마찬가지로, UrlResource를 반환받고 싶으면 아무 표준 &lt;code&gt;java.net.URL&lt;/code&gt; 접두사 중 하나를 사용하면 된다.&lt;br /&gt;다음 예시는 file과 https 접두사에 대한 예시이다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;Resource template = ctx.getResource(&quot;file:///some/resource/path/myTemplate.txt&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;Resource template = ctx.getResource(&quot;https://myhost.com/resource/path/myTemplate.txt&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;ResourcePatternResolver 인터페이스&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ResourcePatternResolver 인터페이스는 ResourceLoader 인터페이스의 확장이고, 위치 패턴 해석에 대한 전략을 정의한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;public interface ResourcePatternResolver extends ResourceLoader {

    String CLASSPATH_ALL_URL_PREFIX = &quot;classpath*:&quot;;

    Resource[] getResources(String locationPattern) throws IOException;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 예시에서, 이 인터페이스는 &lt;code&gt;classpath*:&lt;/code&gt; 이라는 클래스패스의 전체 리소스를 매칭하는 리소스 접두사를 정의했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PathMatchingResourcePatternResolver는 ApplicationContext 외부에서 사용할 수 있는 독립 실행 구현체이며 Resource[] 빈 속성을 채우기 위해 ResourceArrayPropertyEditor에서도 사용된다.&lt;br /&gt;PathMatchingResourcePatternResolver는 지정된 리소스 위치 경로를 하나 이상의 일치하는 Resource 개체로 확인할 수 있다.&lt;br /&gt;소스 경로는 타깃 Resource에 대한 일대일 매핑이 있는 간단한 경로일 수도 있고, 또는 대안으로 특수 &lt;code&gt;classpath*:&lt;/code&gt; 접두사 또는 내부 Ant 스타일 정규식을 포함할 수 있다.&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;ResourceLoaderAware 인터페이스&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ResourceLoaderAware 인터페이스는 ResourceLoader를 제공받을 것으로 예상되는 구성 요소를 식별하는 콜백 인터페이스이다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;public interface ResourceLoaderAware {

    void setResourceLoader(ResourceLoader resourceLoader);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 클래스가 ResourceLoaderAware를 구현하고 애플리케이션 컨텍스트에서 관리되고 있다면, 애플리케이션 컨텍스트에 의해 ResourceLoaderAware로 인식된다.&lt;br /&gt;그런 다음 애플리케이션 컨텍스트는 setResourceLoader를 호출하여 자신을 인수로 제공한다.&lt;br /&gt;(스프링의 모든 애플리케이션 컨텍스트는 ResourceLoader 인터페이스를 구현한다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ApplicationContext가 ResourceLoader이기 때문에, 빈은 또한 ApplicationContextAware 인터페이스를 구현하고 제공되는 애플리케이션 컨텍스트를 리소스 로드를 위해 사용할 수도 있다.&lt;br /&gt;하지만 일반적으로 필요한 경우 특수한 ResourceLoader 인터페이스를 사용하는 것이 좋다.&lt;br /&gt;코드가 리소스 로딩 인터페이스와만 관련이 있도록 하고, 전체 ApplicationContext 인터페이스와는 관련이 없도록 하는 것이 좋기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴포넌트에서 ResourceLoaderAware 인터페이스를 구현하는 대신 ResourceLoader를 오토와이어링할 수도 있다.&lt;br /&gt;기존 방식인 생성자나 byType 오토와이어링은 각각 생성자 인자나 setter 메서드를 통해 ResourceLoader를 제공할 수 있고, 더 많은 유연성을 원한다면 @Autowired를 사용한 어노테이션 기반 오토와이어링을 사용할 수 있다.&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;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빈 자체가 일종의 동적인 프로세스를 통해 리소스 경로를 결정하고 제공하려는 경우 빈이 ResourceLoader 또는 ResourcePatternResolver 인터페이스를 사용하여 리소르를 로드하는 것이 합리적일 수 있다.&lt;br /&gt;예를 들어 특정 리소스가 사용자의 역할에 따라 달라지는 템플릿 등이 있을 수 있다.&lt;br /&gt;리소스가 정적인 경우는 ResourceLoader 인터페이스 대신 빈이 필요한 Resource 속성을 노출하도록 해서 사용하는 것이 좋다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;package example;

public class MyBean {

    private Resource template;

    public setTemplate(Resource template) {
        this.template = template;
    }

    // ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;&amp;lt;bean id=&quot;myBean&quot; class=&quot;example.MyBean&quot;&amp;gt;
    &amp;lt;property name=&quot;template&quot; value=&quot;some/resource/path/myTemplate.txt&quot;/&amp;gt;
&amp;lt;/bean&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리소스 경로에는 접두사가 없다.&lt;br /&gt;결과적으로 애플리케이션 컨텍스트 자체가 ResourceLoader로 사용되기 때문에 리소스는 컨텍스트 유형에 따라 ClassPathResource, FileSystemResource 또는 ServletContextResource를 통해 로드된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 리소스 유형을 강제로 사용해야하는 경우에는 다음과 같이 클래스패스 혹은 파일 등의 접두사를 사용할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;&amp;lt;property name=&quot;template&quot; value=&quot;classpath:some/resource/path/myTemplate.txt&quot;&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;&amp;lt;property name=&quot;template&quot; value=&quot;file:///some/resource/path/myTemplate.txt&quot;/&amp;gt;&lt;/code&gt;&lt;/pre&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;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;애플리케이션 컨텍스트 구성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션 컨텍스트의 생성자는 문자열이나 문자열 배열로 XML 파일 같은 경로를 받아 컨텍스트를 정의한다.&lt;br /&gt;만약 경로에 접두사가 없다면, 해당 경로에서 특별한 Resource 타입이 만들어지고 이는 애플리케이션 컨텍스트의 구현체에 따라 달라진다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;ApplicationContext ctx = new ClassPathXmlApplicationContext(&quot;conf/appContext.xml&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 예제에서 빈 정의는 클래스패스에 따라 로드되는데, ClassPathResource가 사용되었기 때문이다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;ApplicationContext ctx = new FileSystemXmlApplicationContext(&quot;conf/appContext.xml&quot;);&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;만약 특별한 &lt;code&gt;classpath&lt;/code&gt; 접두사나 표준 URL 접두사가 사용된다면, 다음과 같이 Resource의 기본 타입을 오버라이딩한다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;ApplicationContext ctx = new FileSystemXmlApplicationContext(&quot;classpath:conf/appContext.xml&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FileSystemXmlApplicationContext는 클래스패스에 의해 빈 정의를 로드한다.&lt;br /&gt;하지만 이는 여전히 FileSystemXmlApplicationContext이기 때문에 나중에 ResourceLoader 등으로 사용되는 경우 접두사가 없는 패스는 파일 시스템 기반으로 다뤄질 것이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;FileSystemResource 주의 사항&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FileSystemApplicationContext에 연결되지 않은 FileSystemResource (즉, FileSystemApplicationContext가 ResourceLoader가 아닌 경우)는 절대 경로와 상대 경로를 모두 잘 처리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이전 버전과의 호환성 이유로 인해 FileSystemApplicationContext가 ResourceLoader일 때는 절대 경로, 상대 경로에 상관 없이 모든 위치 경로를 상대 경로로 처리한다.&lt;br /&gt;즉, 다음 두 경우가 같은 방식으로 동작한다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;ApplicationContext ctx = new FileSystemXmlApplicationContext(&quot;conf/context.xml&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;ApplicationContext ctx = new FileSystemXmlApplicationContext(&quot;/conf/context.xml&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 두 예제도 마찬가지로 같은 의미다.&lt;/p&gt;
&lt;pre class=&quot;abnf&quot;&gt;&lt;code&gt;FileSystemXmlApplicationContext ctx = ...;
ctx.getResource(&quot;some/resource/path/myTemplate.txt&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;abnf&quot;&gt;&lt;code&gt;FileSystemXmlApplicationContext ctx = ...;
ctx.getResource(&quot;/some/resource/path/myTemplate.txt&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 파일 절대 경로가 필요한 경우는 &lt;code&gt;file:&lt;/code&gt; 접두사를 사용하여 다음과 같이 UrlResource를 사용하도록 해야 한다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;// context 타입에 상관 없이, Resource는 항상 UrlResource
ctx.getResource(&quot;file:///some/resource/path/myTemplate.txt&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;// UrlResource를 통해 정의를 로드하도록 FileSystemXmlApplicationContext 강제
ApplicationContext ctx = new FileSystemXmlApplicationContext(&quot;file:///conf/context.xml&quot;);&lt;/code&gt;&lt;/pre&gt;</description>
      <category>RTFM</category>
      <category>RTFM</category>
      <category>SpringCore</category>
      <author>wbluke</author>
      <guid isPermaLink="true">https://wbluke.tistory.com/65</guid>
      <comments>https://wbluke.tistory.com/65#entry65comment</comments>
      <pubDate>Sat, 10 Jul 2021 16:51:53 +0900</pubDate>
    </item>
    <item>
      <title>[RTFM] Spring Core - 01. The IoC Container (2)</title>
      <link>https://wbluke.tistory.com/64</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;아래 포스팅에 대한 상위 메타 문서는 &lt;a href=&quot;https://wbluke.tistory.com/62&quot;&gt;[RTFM] 매일 읽는 공식 문서&lt;/a&gt; 를 참고해 주세요.&lt;br /&gt;원문 : &lt;a href=&quot;https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-java&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-java&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;Java 기반 컨테이너 설정&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 섹션에서는 자바 코드에서 스프링 컨테이너 설정을 어떻게 해야하는지를 알아본다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;@Bean과 @Configuration&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링의 새로운 자바 구성 지원의 중심 요소는 &lt;code&gt;@Configuration&lt;/code&gt; 가 적용된 클래스와 &lt;code&gt;@Bean&lt;/code&gt; 이 적용된 메서드이다.&lt;br /&gt;&lt;code&gt;@Bean&lt;/code&gt; 은 메서드에서 스프링 컨테이너에서 관리할 새로운 객체를 생성하고, 구성하고, 초기화할 때 사용한다.&lt;br /&gt;&lt;code&gt;@Bean&lt;/code&gt; 이 달린 메서드는 어느 &lt;code&gt;@Component&lt;/code&gt; 클래스에서도 사용 가능하지만, 보통은 &lt;code&gt;@Configuration&lt;/code&gt; 클래스에서 사용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;@Configuration&lt;/code&gt; 선언의 가장 주된 목적은 빈 정의의 원천임을 나타내는 것이다.&lt;br /&gt;&lt;code&gt;@Configuration&lt;/code&gt; 클래스 내에서 다른 &lt;code&gt;@Bean&lt;/code&gt; 메서드를 호출하여 빈 간 종속성을 정의할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Configuration
public class AppConfig {

    @Bean
    public MyService myService() {
        return new MyServiceImpl();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;@Configuration 이 선언되지 않은 클래스에서 @Bean 메서드가 선언되면 lite 한 모드에서 처리되는 것으로 인식된다.&amp;nbsp; &lt;br /&gt;예를 들어 서비스 컴포넌트는 적용 가능한 각 컴포넌트 클래스에 대한 라이트 @Bean 선언을 통해 컨테이너에 관리 뷰를 노출할 수 있다.&amp;nbsp; &lt;br /&gt;일반적인 @Configuration 내 @Bean 선언과 달리, 라이트 @Bean 메서드는 빈 간 종속성을 선언할 수 없다.&amp;nbsp; &lt;br /&gt;그래서 이런 @Bean 메서드는 다른 @Bean 메서드를 호출해서는 안 된다.&amp;nbsp; &lt;br /&gt;일반적인 시나리오에서는 @Bean 메서드는 @Configuration 클래스 내에서 선언되어 &quot;full&quot; 모드로 사용되고, 컨테이너의 라이프사이클 관리 하에 각 메서드 참조가 동작하도록 하는 것이 좋다.&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;AnnotationConfigApplicationContext를 사용한 스프링 컨테이너 초기화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 스프링 3.0에서 소개된 &lt;code&gt;AnnotationConfigApplicationContext&lt;/code&gt; 이다.&lt;br /&gt;이 다용도의 ApplicationContext 구현체는 &lt;code&gt;@Configuration&lt;/code&gt; 클래스를 받아들일 수 있을 뿐만 아니라 &lt;code&gt;@Component&lt;/code&gt; 클래스나 JSR-330 어노테이션 클래스도 받아들일 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;@Configuration&lt;/code&gt; 클래스가 입력으로 제공되면 해당 클래스 자체가 빈 정의로 등록되고 클래스 내에 선언된 모든 &lt;code&gt;@Bean&lt;/code&gt; 메서드도 빈 정의로 등록된다.&lt;br /&gt;&lt;code&gt;@Component&lt;/code&gt; 및 JSR-330 클래스가 제공되면 빈 정의로 등록되고, 필요한 경우 &lt;code&gt;@Autowired&lt;/code&gt; 나 &lt;code&gt;@Inject&lt;/code&gt; 와 같은 DI 메타 데이터를 사용하는 것으로 가정한다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;public static void main(String[] args) {
    ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
    MyService myService = ctx.getBean(MyService.class);
    myService.doStuff();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또는 다음과 같이 register()로 등록할 수도 있다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;public static void main(String[] args) {
    AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
    ctx.register(AppConfig.class, OtherConfig.class);
    ctx.register(AdditionalConfig.class);
    ctx.refresh();

    MyService myService = ctx.getBean(MyService.class);
    myService.doStuff();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴포넌트 스캔을 원한다면 다음과 같이 패키지 기반 범위를 지정할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Configuration
@ComponentScan(basePackages = &quot;com.acme&quot;) 
public class AppConfig  {
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 예제는 &quot;com.acme&quot; 패키지 하위의 모든 &lt;code&gt;@Component&lt;/code&gt; 클래스를 찾아서 컨테이너에 빈 정의로 등록한다.&lt;br /&gt;&lt;code&gt;AnnotationConfigApplicationContext&lt;/code&gt; 에서도 scan() 메서드로 위 스캐닝을 진행할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;public static void main(String[] args) {
    AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
    ctx.scan(&quot;com.acme&quot;);
    ctx.refresh();
    MyService myService = ctx.getBean(MyService.class);
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;@Bean 어노테이션 사용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;@Bean&lt;/code&gt; 어노테이션으로 메서드 레벨에서 메서드가 반환하는 타입으로 빈 정의를 등록할 수 있는데, 이때 기본적으로 메서드의 이름이 빈의 이름이 된다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Configuration
public class AppConfig {

    @Bean
    public TransferServiceImpl transferService() {
        return new TransferServiceImpl();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 아래와 같이 구현체를 인터페이스 타입으로 반환한다면, 빈 등록은 가능하지만 막상 해당 인터페이스에 여러 구현체가 존재할 경우 어떤 구현체를 빈으로 가질지 문제가 생길 수 있으니 가능하면 구체적인 구현체 타입으로 빈을 등록하는 것이 좋다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Configuration
public class AppConfig {

    @Bean
    public TransferService transferService() {
        return new TransferServiceImpl();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;@Bean&lt;/code&gt; 으로 정의된 클래스는 라이프사이클 콜백을 지원하고, JSR-250의 &lt;code&gt;@PostConstruct&lt;/code&gt; 와 &lt;code&gt;@PreDestroy&lt;/code&gt; 를 사용할 수 있다.&lt;br /&gt;또한 다음과 같이 초기화 메서드와 소멸 메서드를 지정할 수도 있다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;public class BeanOne {

    public void init() {
        // initialization logic
    }
}

public class BeanTwo {

    public void cleanup() {
        // destruction logic
    }
}

@Configuration
public class AppConfig {

    @Bean(initMethod = &quot;init&quot;)
    public BeanOne beanOne() {
        return new BeanOne();
    }

    @Bean(destroyMethod = &quot;cleanup&quot;)
    public BeanTwo beanTwo() {
        return new BeanTwo();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 필요하다면 name 프로퍼티로 이름을 지정하거나, alias를 지정할 수 있고, &lt;code&gt;@Description&lt;/code&gt; 을 통해 빈에 대한 설명도 작성할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;@Configuration 어노테이션 사용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;@Configuration&lt;/code&gt; 메서드는 클래스 레벨에서 사용되며, &lt;code&gt;@Bean&lt;/code&gt; 메서드를 통해 빈을 선언하는 역할을 한다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Configuration
public class AppConfig {

    @Bean
    public BeanOne beanOne() {
        return new BeanOne(beanTwo());
    }

    @Bean
    public BeanTwo beanTwo() {
        return new BeanTwo();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;위와 같이 메서드를 통한 빈 간 종속성 설정은 @Configuration 클래스 내의 @Bean 메서드끼리만 가능하다.&amp;nbsp; &lt;br /&gt;일반 @Component 클래스 내에서는 빈 간 종속성을 선언할 수 없다.&lt;/blockquote&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Configuration
public class AppConfig {

    @Bean
    public ClientService clientService1() {
        ClientServiceImpl clientService = new ClientServiceImpl();
        clientService.setClientDao(clientDao());
        return clientService;
    }

    @Bean
    public ClientService clientService2() {
        ClientServiceImpl clientService = new ClientServiceImpl();
        clientService.setClientDao(clientDao());
        return clientService;
    }

    @Bean
    public ClientDao clientDao() {
        return new ClientDaoImpl();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 위와 같이 인터페이스 타입(ClientDao)을 서로 다른 두 메서드에서 사용하고 있다면 (싱글턴 스코프에서) 두 개의 인스턴스가 생성되어 문제가 될 것 같이 보인다.&lt;br /&gt;실제로는 &lt;code&gt;@Configuration&lt;/code&gt; 클래스는 시작 시 CGLIB를 사용하여 서브클래싱되고, 하위 클래스에서 자식 메서드는 부모 메서드를 호출하고 새 인스턴스를 만들기 전에 캐싱된 빈이 있는지 확인하기 때문에 문제가 발생하지 않는다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;자바 기반 설정 구성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;@Import&lt;/code&gt; 어노테이션은 여러 모듈화된 설정 파일들 간에 사용하는데, 다른 설정 파일을 불러올 때 사용한다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Configuration
public class ConfigA {

    @Bean
    public A a() {
        return new A();
    }
}

@Configuration
@Import(ConfigA.class)
public class ConfigB {

    @Bean
    public B b() {
        return new B();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 예제에서는 ConfigB 파일만 컨텍스트에 로딩해도 A, B 빈들을 모두 사용할 수 있게 된다.&lt;br /&gt;이는 컨테이너 초기화 로직을 훨씬 간단하게 만들 수 있는 방법이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 위 예제는 너무 간단하고, 좀 더 실용적인 시나리오를 살펴보자.&lt;br /&gt;많은 경우 빈들은 서로 의존성을 가지며, 다른 설정 파일에 해당 의존성이 존재하는 경우도 있다.&lt;br /&gt;XML 기반이라면 사실 문제될 것이 없는데, 컴파일러가 개입하지 않고, &lt;code&gt;ref&lt;/code&gt; 속성으로 필요한 빈을 선언해줄 수 있기 때문이다.&lt;br /&gt;하지만 다행히도, &lt;code&gt;@Bean&lt;/code&gt; 메서드는 필요한 빈 의존성을 파라미터로 받을 수 있기 때문에 문제를 해결할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Configuration
public class ServiceConfig {

    @Bean
    public TransferService transferService(AccountRepository accountRepository) {
        return new TransferServiceImpl(accountRepository);
    }
}

@Configuration
public class RepositoryConfig {

    @Bean
    public AccountRepository accountRepository(DataSource dataSource) {
        return new JdbcAccountRepository(dataSource);
    }
}

@Configuration
@Import({ServiceConfig.class, RepositoryConfig.class})
public class SystemTestConfig {

    @Bean
    public DataSource dataSource() {
        // return new DataSource
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시스템 환경에 따라 설정 파일들을 조건적으로 선택할 수도 있다.&lt;br /&gt;&lt;code&gt;@Profile&lt;/code&gt; 어노테이션은 더 유연한 구조의 &lt;code&gt;@Conditional&lt;/code&gt; 어노테이션을 통해 구현되고 있고, 이는 &lt;code&gt;org.springframework.context.annotation.Condition&lt;/code&gt; 클래스를 통해 동작한다.&lt;br /&gt;Condition 인터페이스의 구현부를 보면 다음과 같이 matches() 메서드로 프로파일 조건을 체크하고 있는 것을 볼 수 있다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
    // @Profile 어노테이션 속성 읽어오기
    MultiValueMap&amp;lt;String, Object&amp;gt; attrs = metadata.getAllAnnotationAttributes(Profile.class.getName());
    if (attrs != null) {
        for (Object value : attrs.get(&quot;value&quot;)) {
            if (context.getEnvironment().acceptsProfiles(((String[]) value))) {
                return true;
            }
        }
        return false;
    }
    return true;
}&lt;/code&gt;&lt;/pre&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;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Environment&lt;/code&gt; 인터페이스는 컨테이너 내에서 통합된 추상 개념이고, profiles와 properties라는 두 가지 중요한 관점을 가지고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;profile은 주어진 프로파일에 따라 각각 등록되는 빈 정의들의 논리적 묶음이다.&lt;br /&gt;profile과 관련된 &lt;code&gt;Environment&lt;/code&gt; 객체의 역할은 현재 어떤 프로파일이 동작하고 있는지, 그리고 어떤 프로파일이 기본 설정으로 동작하도록 되어있는지를 결정하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;properties는 거의 모든 애플리케이션에서 중요한 역할을 하고, 아마도 많은 소스(프로퍼티 파일, JVM 시스템 프로퍼티, 시스템 환경 변수, JNDI, 서블릿 컨텍스트 파라미터, 애드혹 프로퍼티 객체, 맵 객체 등)들의 기원이 되는 항목이다.&lt;br /&gt;properties와 관련된 &lt;code&gt;Environment&lt;/code&gt; 객체의 역할은 프로퍼티를 구성하고 적용함으로써 사용자에게 편리한 서비스 인터페이스를 제공하는 것이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;빈 정의 프로파일&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빈 정의 프로파일은 코어 컨테이너에서 서로 다른 환경에 서로 다른 빈이 등록될 수 있는 매커니즘을 제공한다.&lt;br /&gt;&quot;환경&quot;이라는 단어는 &quot;서로 다른 사용자들을 위한 서로 다른 어떤 것&quot;을 의미할 수 있는데, 이 기능은 다음과 같은 상황에 도움을 줄 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;개발 환경에서는 인메모리 데이터소스를 사용하는 반면 QA나 프로덕션 환경에서는 JNDI 데이터소스를 사용하는 것&lt;/li&gt;
&lt;li&gt;퍼포먼스 환경에 배포할 때만 모니터링 인프라스트럭처를 등록하는 것&lt;/li&gt;
&lt;li&gt;AB 테스트 시 각각 서로 다른 빈들을 구성하는 것&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;DataSource&lt;/code&gt; 의 예를 생각해보자.&lt;br /&gt;테스트 환경에서는 다음과 같이 환경을 구성할 수 있을 것이다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;@Bean
public DataSource dataSource() {
    return new EmbeddedDatabaseBuilder()
        .setType(EmbeddedDatabaseType.HSQL)
        .addScript(&quot;my-schema.sql&quot;)
        .addScript(&quot;my-test-data.sql&quot;)
        .build();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로 QA나 프로덕션 환경에서는 데이터소스가 애플리케이션 서버 내 JNDI 디렉토리에 등록되어있다고 가정해보자.&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;@Bean(destroyMethod=&quot;&quot;)
public DataSource dataSource() throws Exception {
    Context ctx = new InitialContext();
    return (DataSource) ctx.lookup(&quot;java:comp/env/jdbc/datasource&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 어떻게 현 환경에 맞게 두 개의 변수를 전환할 것인가이다.&lt;br /&gt;빈 정의 프로파일은 이런 문제에 대한 해결책을 제공하는 핵심 컨테이너 기능이다.&lt;br /&gt;위 사례를 일반화하자면 상황 A에서 빈 정의의 특정 프로파일을 등록하고, 상황 B에서는 다른 프로파일을 등록하고 싶다는 것으로 이야기할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;@Profile&lt;/code&gt; 어노테이션은 컴포넌트가 활성화된 1개 혹은 그 이상의 프로파일들에 맞게 빈을 등록할 수 있도록 한다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Configuration
@Profile(&quot;development&quot;)
public class StandaloneDataConfig {

    @Bean
    public DataSource dataSource() {
        return new EmbeddedDatabaseBuilder()
            .setType(EmbeddedDatabaseType.HSQL)
            .addScript(&quot;classpath:com/bank/config/sql/schema.sql&quot;)
            .addScript(&quot;classpath:com/bank/config/sql/test-data.sql&quot;)
            .build();
    }
}

@Configuration
@Profile(&quot;production&quot;)
public class JndiDataConfig {

    @Bean(destroyMethod=&quot;&quot;)
    public DataSource dataSource() throws Exception {
        Context ctx = new InitialContext();
        return (DataSource) ctx.lookup(&quot;java:comp/env/jdbc/datasource&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로파일 문자열은 &lt;code&gt;!&lt;/code&gt; , &lt;code&gt;&amp;amp;&lt;/code&gt; , &lt;code&gt;|&lt;/code&gt; 와 같은 연산자와 함께 구성할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 다음과 같이 &lt;code&gt;@Profile&lt;/code&gt; 어노테이션을 메타 어노테이션으로 활용해볼 수도 있다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Profile(&quot;production&quot;)
public @interface Production {
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;@Profile&lt;/code&gt; 은 메서드 레벨에서도 등록할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Configuration
public class AppConfig {

    @Bean(&quot;dataSource&quot;)
    @Profile(&quot;development&quot;) 
    public DataSource standaloneDataSource() {
        return new EmbeddedDatabaseBuilder()
            .setType(EmbeddedDatabaseType.HSQL)
            .addScript(&quot;classpath:com/bank/config/sql/schema.sql&quot;)
            .addScript(&quot;classpath:com/bank/config/sql/test-data.sql&quot;)
            .build();
    }

    @Bean(&quot;dataSource&quot;)
    @Profile(&quot;production&quot;) 
    public DataSource jndiDataSource() throws Exception {
        Context ctx = new InitialContext();
        return (DataSource) ctx.lookup(&quot;java:comp/env/jdbc/datasource&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 환경을 구성했으면, 스프링 애플리케이션을 프로파일에 맞게 실행시켜야 한다.&lt;br /&gt;프로파일 활성화는 여러가지 방식으로 수행할 수 있지만, 가장 간단한 방법은 ApplicationContext를 통해 사용할 수 있는 &lt;code&gt;Environment&lt;/code&gt; API에 대해 프로그래밍 방식으로 수행하는 것이다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.getEnvironment().setActiveProfiles(&quot;development&quot;);
ctx.register(SomeConfig.class, StandaloneDataConfig.class, JndiDataConfig.class);
ctx.refresh();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가적으로, 활성화할 프로파일을 &lt;code&gt;spring.profiles.active&lt;/code&gt; 프로퍼티로 선언할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;-Dspring.profiles.active=&quot;profile1,profile2&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로, 프로파일이 지정되지 않았을 때의 기본 프로파일은 다음과 같이 지정할 수 있다.&lt;br /&gt;프로파일이 지정된 상황이라면 기본 프로파일은 적용되지 않는다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Configuration
@Profile(&quot;default&quot;)
public class DefaultDataConfig {

    // ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;PropertySource 추상화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링의 &lt;code&gt;Environment&lt;/code&gt; 추상화는 프로퍼티 소스 계층 구조에 대한 검색을 제공한다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;ApplicationContext ctx = new GenericApplicationContext();
Environment env = ctx.getEnvironment();
boolean containsMyProperty = env.containsProperty(&quot;my-property&quot;);
System.out.println(&quot;Does my environment contain the 'my-property' property? &quot; + containsMyProperty);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 예제에서 현재 환경에 my-property 속성이 정의되어 있는지 스프링에 물어볼 수 있다.&lt;br /&gt;이를 답하기 위해 Environment 객체는 PropertySource 객체들의 집합에서 검색을 수행한다.&lt;br /&gt;&lt;code&gt;PropertySource&lt;/code&gt; 는 키-값 쌍으로 이루어진 단순한 추상이며, 스프링의 &lt;code&gt;StandardEnvironment&lt;/code&gt; 는 두 개의 PropertySource(JVM 환경 프로퍼티, 시스템 환경 프로퍼티)로 구성된다.&lt;br /&gt;결론적으로, &lt;code&gt;StandardEnvironment&lt;/code&gt; 를 사용할 경우 &lt;code&gt;env.containsProperty(&quot;my-property&quot;)&lt;/code&gt; 는 &lt;code&gt;my-property&lt;/code&gt; 시스템 프로퍼티가 있거나 환경 변수가 있는 경우에 참을 반환할 것이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;@PropertySource 사용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;app.properties 파일에서 키-값 쌍을 정의한 경우 다음과 같이 해당 값을 가져올 수 있다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Configuration
@PropertySource(&quot;classpath:/com/myco/app.properties&quot;)
public class AppConfig {

    @Autowired
    Environment env;

    @Bean
    public TestBean testBean() {
        TestBean testBean = new TestBean();
        testBean.setName(env.getProperty(&quot;testbean.name&quot;));
        return testBean;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;ApplicationContext의 추가 기능&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;org.springframework.beans.factory&lt;/code&gt; 패키지는 프로그래밍 방식을 포함하여, 빈을 다루고 관리하는 기능을 제공한다.&lt;br /&gt;&lt;code&gt;org.springframework.context&lt;/code&gt; 패키지는 BeanFactory 인터페이스를 상속하고, 프레임워크-지향 방식의 추가적인 기능들을 제공하는 다른 인터페이스들도 상속한 ApplicationContext 인터페이스를 추가했다.&lt;br /&gt;많은 사람들은 ApplicationContext를 프로그래밍 방식으로 생성하는 대신, ContextLoader와 같은 지원 클래스에 의존하여 자동으로 인스턴스화한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보다 프레임워크-지향 방식으로 BeanFactory의 기능을 향상시키기 위해 컨텍스트 패키지는 다음과 같은 기능도 제공한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;MessageSource&lt;/code&gt; 를 통한 i18n(internationalization)-스타일의 메시지 접근&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ResourceLoader&lt;/code&gt; 를 통한 URL과 파일과 같은 리소스 접근&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ApplicationEventPublisher&lt;/code&gt; 를 통한 이벤트 발행 ( &lt;code&gt;ApplicationListener&lt;/code&gt; 를 구현한 빈들)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;HierarchicalBeanFactory&lt;/code&gt; 인터페이스를 통해 애플리케이션의 웹 계층과 같은 특정 계층에 각각 집중할 수 있도록 여러 계층 구조 컨텍스트를 로딩&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;MessageSource를 통한 국제화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ApplicationContext는 MessageSource 인터페이스를 확장하고 있기 때문에 i18n 메세징 기능을 제공할 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;String getMessage(String code, Object[] args, String default, Locale loc)&lt;/code&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;가장 기본이 되는 메서드로, MessageSource로부터 메시지를 조회한다.&lt;/li&gt;
&lt;li&gt;적절한 지역이 없으면 기본 메시지를 사용한다.&lt;/li&gt;
&lt;li&gt;라이브러리에 의해 제공되는 MessageFormat 기능을 사용해서 어떤 인자든 대체 값으로 넣어줄 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;String getMessage(String code, Object[] args, Locale loc)&lt;/code&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위 메서드와 같으나 기본 메시지가 없어서, 적절한 지역 메시지가 없는 경우 NoSuchMessageException을 던진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;String getMessage(MessageSourceResolvable resolvable, Locale locale)&lt;/code&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위에서 사용된 모든 속성이 MessageSourceResolvable로 묶여서 사용된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ApplicationContext가 로딩될 때, 컨텍스트에 정의된 MessageSource를 자동으로 찾는다.&lt;br /&gt;그 빈은 반드시 이름이 &lt;code&gt;messageSource&lt;/code&gt; 여야만 한다.&lt;br /&gt;빈을 찾으면, 위의 메서드를 통한 모든 호출은 메시지 소스로 위임된다.&lt;br /&gt;메시지 소스 빈이 없으면, ApplicationContext는 해당 빈의 이름을 가진 부모가 있는지 찾기 시작한다.&lt;br /&gt;만약 ApplicationContext가 아무 빈도 찾지 못한다면 비어있는 DeligatingMessageSource가 인스턴스화된다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;public static void main(String[] args) {
    MessageSource resources = new ClassPathXmlApplicationContext(&quot;beans.xml&quot;);
    String message = resources.getMessage(&quot;message&quot;, null, &quot;Default&quot;, Locale.ENGLISH);
    System.out.println(message);
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;표준 이벤트와 커스텀 이벤트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ApplicationContext 의 이벤트 핸들링은 ApplicationEvent 클래스와 ApplicationListener 인터페이스에 의해 제공된다.&lt;br /&gt;만약 빈이 ApplicationListener 인터페이스를 구현하고 컨텍스트 내에 존재한다면, 항상 ApplicationEvent가 ApplicationContext에 발행될 때마다 빈에 알림이 전송된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링에서 제공하는 표준 이벤트는 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ContextRefreshedEvent&lt;/li&gt;
&lt;li&gt;ContextStartedEvent&lt;/li&gt;
&lt;li&gt;ContextStoppedEvent&lt;/li&gt;
&lt;li&gt;ContextClosedEvent&lt;/li&gt;
&lt;li&gt;RequestHandledEvent&lt;/li&gt;
&lt;li&gt;ServletRequestHandledEvent&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커스텀 이벤트를 만들 수도 있다.&lt;/p&gt;
&lt;pre class=&quot;scala&quot;&gt;&lt;code&gt;public class BlockedListEvent extends ApplicationEvent {

    private final String address;
    private final String content;

    public BlockedListEvent(Object source, String address, String content) {
        super(source);
        this.address = address;
        this.content = content;
    }

    // ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ApplicationEvent를 발행하기 위해서는, ApplicationEventPublisher의 publishEvent() 메서드를 호출하면 된다.&lt;br /&gt;전형적으로 이는 ApplicationEventPublisherAware 를 구현한 클래스를 생성하고 빈으로 등록함으로써 수행된다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;public class EmailService implements ApplicationEventPublisherAware {

    private List&amp;lt;String&amp;gt; blockedList;
    private ApplicationEventPublisher publisher;

    public void setBlockedList(List&amp;lt;String&amp;gt; blockedList) {
        this.blockedList = blockedList;
    }

    public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
        this.publisher = publisher;
    }

    public void sendEmail(String address, String content) {
        if (blockedList.contains(address)) {
            publisher.publishEvent(new BlockedListEvent(this, address, content));
            return;
        }
        // 이메일 전송
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 컨테이너는 ApplicationEventPublisherAware를 구현한 EmailService 를 찾아서, setter를 통해 자기 자신을 주입한다.&lt;br /&gt;이 과정에서 ApplicationEventPublisher 인터페이스를 통해 애플리케이션 컨텍스트와 소통할 수 있게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커스텀 ApplicationEvent를 받기 위해서는, ApplicationListener 를 구현한 클래스를 생성하고 스프링 빈으로 등록하면 된다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;public class BlockedListNotifier implements ApplicationListener&amp;lt;BlockedListEvent&amp;gt; {

    private String notificationAddress;

    public void setNotificationAddress(String notificationAddress) {
        this.notificationAddress = notificationAddress;
    }

    public void onApplicationEvent(BlockedListEvent event) {
        // notificationAddress를 통해 적절한 알림 발송
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ApplicationListener는 일반적으로 사용자가 지정한 타입으로 제네릭화된다.&lt;br /&gt;즉, onApplicationEvent() 메서드는 다운 캐스팅이 필요하지 않고 타입 안전하게 유지될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원하는 만큼 이벤트 리스너를 등록할 수 있지만 기본적으로 이벤트 리스너는 이벤트를 동기식으로 수신한다.&lt;br /&gt;즉, 모든 리스너가 이벤트 처리를 완료할 때까지 publishEvent() 메서드가 차단된다.&lt;br /&gt;이런 동기 및 단일 스레드 방식의 장점 중 하나는 리스너가 이벤트를 수신할 때 트랜잭션 컨텍스트를 사용할 수 있는 경우 발행한 곳의 트랜잭션 컨텍스트 내에서 작동한다는 점이다.&lt;br /&gt;이벤트 수신에 대한 다른 전략이 필요하면 ApplicationEventMulticaster, SimpleApplicationEventMulticaster를 참고하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어노테이션 기반으로도 리스너를 등록할 수 있다.&lt;br /&gt;다음과 같은 경우 인터페이스를 구현할 필요도 없고, 메서드 이름도 자유로워진다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;public class BlockedListNotifier {

    private String notificationAddress;

    public void setNotificationAddress(String notificationAddress) {
        this.notificationAddress = notificationAddress;
    }

    @EventListener
    public void processBlockedListEvent(BlockedListEvent event) {
        // notificationAddress를 통해 적절한 알림 발송
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 개의 이벤트를 수신해야하거나 파라미터 없이 메서드를 정의하고 싶은 경우는 다음과 같이 지정할 수도 있다.&lt;/p&gt;
&lt;pre class=&quot;d&quot;&gt;&lt;code&gt;@EventListener({ContextStartedEvent.class, ContextRefreshedEvent.class})
public void handleContextStart() {
    // ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이벤트 수신 후 또 다른 이벤트를 발행해야 한다면, 다음과 같이 반환값으로 이벤트를 줄 수도 있다.&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;@EventListener
public ListUpdateEvent handleBlockedListEvent(BlockedListEvent event) {
    // notificationAddress를 통해 적절한 알림 발송
    // 그리고 ListUpdateEvent 발행
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 리스너가 비동기적으로 이벤트를 받아서 작업해야 한다면 다음과 같은 어노테이션을 사용할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@EventListener
@Async
public void processBlockedListEvent(BlockedListEvent event) {
    // BlockedListEvent가 별도의 스레드에서 수행된다.
}&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;/li&gt;
&lt;li&gt;비동기 이벤트 리스너 메서드는 반환 값으로 다른 이벤트를 주어 새로운 이벤트를 발행할 수 없다.&lt;br /&gt;만약 다른 이벤트를 이어서 발행해야 한다면 수동으로 이벤트를 발행하기 위해 ApplicationEventPublisher를 주입해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이벤트 리스너 간 순서를 지정할 수도 있다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@EventListener
@Order(42)
public void processBlockedListEvent(BlockedListEvent event) {
    // notificationAddress를 통해 적절한 알림 발송
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제네릭을 사용하여 이벤트 구조를 추가로 정의할 수도 있다.&lt;br /&gt;다음과 같이 &lt;code&gt;EventCreatedEvent&amp;lt;T&amp;gt;&lt;/code&gt; 를 사용하면 생성되는 엔티티의 타입을 지정하여, Person에 대한 EntityCreatedEvent만 수신할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;@EventListener
public void onPersonCreated(EntityCreatedEvent&amp;lt;Person&amp;gt; event) {
    // ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;낮은 수준의 리소스에 대한 편리한 접근&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션 컨텍스트는 Resource 객체를 로드할 수 있는 ResourceLoader이다.&lt;br /&gt;Resource는 JDK의 &lt;code&gt;java.net.URL&lt;/code&gt; 클래스보다 더 본질적으로 많은 기능을 제공하는 클래스이며, 사실 Resource 구현체가 URL 클래스를 감싸고 있다.&lt;br /&gt;Resource는 클래스패스, 파일 시스템, 표준 URL 등 거의 모든 위치에 있는 낮은 수준의 리소스를 얻을 수 있다.&lt;br /&gt;만약 리소스의 위치 정보가 아무런 접두사 없이 사용되었다면, 애플리케이션 컨텍스트의 타입을 보고 적절한 리소스를 불러온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당신은 빈 초기화 시점에 애플리케이션 컨텍스트를 ResourceLoader로 주입 받아서 사용하기 위해 ResourceLoaderAware를 사용할 수 있다.&lt;br /&gt;또한 정적 리소스에 접근하기 위해 Resource 타입 속성을 노출할 수도 있으며, 이는 다른 속성과 마찬가지로 주입될 것이다.&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;BeanFactory&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;BeanFactory or ApplicationContext?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특별한 이유가 없는 한 BeanFactory 대신 ApplicationContext를 사용해야 한다.&lt;br /&gt;ApplicationContext는 BeanFactory의 모든 기능을 포함하기 때문에 일반적으로 빈 처리에 대한 완전한 제어가 필요하지 않은 이상 BeanFactory보다 권장된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 BeanFactory에서는 제공하지 않지만 ApplicationContext에서 제공하는 기능들이다.&lt;/p&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;자동 BeanPostProcessor 등록&lt;/li&gt;
&lt;li&gt;자동 BeanFactoryPostProcessor 등록&lt;/li&gt;
&lt;li&gt;편리한 MessageSource 접근&lt;/li&gt;
&lt;li&gt;내재된 ApplicationEvent 발행 메커니즘&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>RTFM</category>
      <category>RTFM</category>
      <category>SpringCore</category>
      <author>wbluke</author>
      <guid isPermaLink="true">https://wbluke.tistory.com/64</guid>
      <comments>https://wbluke.tistory.com/64#entry64comment</comments>
      <pubDate>Wed, 7 Jul 2021 08:39:32 +0900</pubDate>
    </item>
    <item>
      <title>[RTFM] Spring Core - 01. The IoC Container (1)</title>
      <link>https://wbluke.tistory.com/63</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;아래 포스팅에 대한 상위 메타 문서는 &lt;a href=&quot;https://wbluke.tistory.com/62&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[RTFM] 매일 읽는 공식 문서&lt;/a&gt; 를 참고해 주세요.&lt;br /&gt;원문 : &lt;a href=&quot;https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans&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;IoC 컨테이너와 빈&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 챕터에서는 IoC(Inversion of Control)의 원리를 가진 스프링 프레임워크의 구현을 다룬다.&lt;br /&gt;IoC는 DI(Dependency Injection)라고도 알려져 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 컨테이너는 빈을 생성할 때 필요한 의존성을 모두 주입한다.&lt;br /&gt;이 과정은 빈이 자신의 생성이나 의존성에 대해 컨트롤하지 않는 과정이므로 기능적으로 제어의 역전(IoC)인 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;org.springframework.beans&lt;/code&gt; 와 &lt;code&gt;org.springframework.context&lt;/code&gt; 패키지는 스프링 IoC 컨테이너의 기본 패키지이다.&lt;br /&gt;BeanFactory 인터페이스는 모든 타입의 객체들을 수용하는 설정을 제공한다.&lt;br /&gt;ApplicationContext는 BeanFactory의 서브 타입으로, 다음을 포함한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Spring AOP 기능의 손쉬운 통합&lt;/li&gt;
&lt;li&gt;Message Resource 핸들링&lt;/li&gt;
&lt;li&gt;이벤트 발행&lt;/li&gt;
&lt;li&gt;WebApplicationContext와 같이, 웹 애플리케이션에서 사용되는 특정 응용 계층 컨텍스트&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링에서는, 우리의 애플리케이션에 기반을 두고, IoC 컨테이너에 의해 관리되는 객체를 빈이라 부른다.&lt;br /&gt;빈은 스프링 IoC 컨테이너에 의해 생성되고, 응집되고, 관리되는 객체이다.&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;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;org.springframework.context.ApplicationContext&lt;/code&gt; 인터페이스는 스프링 IoC 컨테이너를 의미하고, 이는 빈들을 생성하고, 설정하고, 조립하는 역할을 한다.&lt;br /&gt;컨테이너는 설정 메타데이터를 읽어서 어떤 객체들이 생성되고, 구성되고, 만들어져야 하는지를 알게 된다.&lt;br /&gt;설정 메타데이터는 XML, 자바 애노테이션, 혹은 자바 코드로 표현된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리의 애플리케이션은 컨텍스트가 생성되고 초기화된 이후에, 설정 메타데이터와 병합하게 된다.&lt;br /&gt;그러면 우리는 완전히 구성되고 실행가능한 시스템(애플리케이션)을 얻게 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;694&quot; data-filename=&quot;IoC_Container.png&quot; width=&quot;502&quot; height=&quot;340&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ls39C/btq8OshWZWu/olMJjarrfmRPHyssUGN18k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ls39C/btq8OshWZWu/olMJjarrfmRPHyssUGN18k/img.png&quot; data-alt=&quot;The Spring IoC Container&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ls39C/btq8OshWZWu/olMJjarrfmRPHyssUGN18k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fls39C%2Fbtq8OshWZWu%2FolMJjarrfmRPHyssUGN18k%2Fimg.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;694&quot; data-filename=&quot;IoC_Container.png&quot; width=&quot;502&quot; height=&quot;340&quot; data-ke-mobilestyle=&quot;widthOrigin&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;/&gt;&lt;/span&gt;&lt;figcaption&gt;The Spring IoC Container&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;설정 메타데이터&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 설정 메타데이터는 우리 애플리케이션 개발자들에게 어떻게 스프링 컨테이너가 객체들을 생성하고, 구성할 수 있는지를 명시한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정 메타데이터는 XML 형식만 있는 것이 아니다.&lt;br /&gt;스프링 IoC 컨테이너는 설정 문서 형식과는 완전히 분리되어 있다.&lt;br /&gt;애노테이션 기반 설정은 스프링 2.5부터 시작되었고, &lt;code&gt;@Configuration&lt;/code&gt;, &lt;code&gt;@Bean&lt;/code&gt;, &lt;code&gt;@Import&lt;/code&gt;, &lt;code&gt;@DependsOn&lt;/code&gt; 과 같은 자바 기반 설정은 스프링 3.0부터 도입되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ApplicationContext는 여러 빈들과 그 의존성을 관리하고 유지하기 위한 팩토리 인터페이스이다.&lt;br /&gt;&lt;code&gt;T getBean(String name, Class&amp;lt;T&amp;gt; requiredType)&lt;/code&gt; 메서드를 사용해서 빈 인스턴스를 조회할 수 있다.&lt;br /&gt;ApplicationContext 인터페이스는 빈을 조회하기 위한 여러 다른 메서드들도 제공하지만, 사실 애플리케이션에서 사용할 일은 없다.&lt;br /&gt;실제로 애플리케이션에서 getBean() 메서드를 사용할 일이 없고, 스프링 API에 의존적일 필요도 없다.&lt;br /&gt;예를 들어, 스프링 통합 웹 프레임워크는 다양한 웹 프레임워크 컴포넌트를 위한 DI를 제공하기 때문에, 굳이 직접적인 API를 호출할 필요는 없다.&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;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빈의 정의는 &lt;code&gt;BeanDefinition&lt;/code&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;/li&gt;
&lt;li&gt;빈의 행동 구성 요소(스코프, 라이프사이클 콜백 등)&lt;/li&gt;
&lt;li&gt;해당 빈이 자신의 역할을 수행하기 위한 다른 참조 빈들(의존성)&lt;/li&gt;
&lt;li&gt;새로운 객체를 생성할 때의 설정값(ex. 풀 사이즈, 커넥션 풀의 커넥션 수 등)&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;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;엔터프라이즈급 애플리케이션은 단지 하나의 객체로만 구성되지 않는다.&lt;br /&gt;간단한 애플리케이션조차 끝단 유저가 보는 것들을 표현하기 위해 여러 객체가 협업하고 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;의존성 주입&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의존성 주입(DI)은 생성자, 팩토리 메서드, 혹은 인스턴스 생성 후의 설정된 속성을 통해서 종속성을 정의하는 프로세스다.&lt;br /&gt;컨테이너는 빈을 생성할 때 이런 의존성들을 주입한다.&lt;br /&gt;이 과정은 기본적으로 빈 자신의 역전(제어의 역전)으로, 빈의 의존성을 제어하는 과정이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DI 방식을 사용하면 코드도 훨씬 깔끔하고, 이런 디커플링은 객체가 의존성을 필요로 할 때 더 효과적이다.&lt;br /&gt;객체는 의존성을 찾지도, 해당 객체가 어디에 있는지도 알지 못한다.&lt;br /&gt;결과적으로, 클래스는 테스트하기 쉬워지고, 특히 의존성이 인터페이스이거나 추상클래스인 경우 단위 테스트에서 테스트 대역을 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성자 기반 DI는 필요한 의존성을 나타내는 생성자의 인자들을 기반으로 구성된다.&lt;br /&gt;스태틱 팩토리 메서드도 마찬가지다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;setter 기반 DI는 기본 생성자나 기본 스태틱 팩토리 메서드를 사용해 빈 인스턴스를 만든 후, setter 메서드로 의존성을 주입하는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 팀에서는 일반적으로 setter DI보다 생성자 DI를 선호하는데, 이는 컴포넌트를 불변으로 만들어주고 의존성이 null이 아님을 보장해주기 때문이다.&lt;br /&gt;게다가, 생성자 DI 컴포넌트는 항상 클라이언트에게 완전히 초기화된 상태의 인스턴스를 보장한다.&lt;br /&gt;부작용이라면, 많은 수의 생성자 파라미터는 좋지 않은 코드 스멜을 야기한다는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 생성자 DI를 사용한다면, 순환 참조의 발생 가능성도 존재하게 된다.&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;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빈의 정의를 보고 빈을 생성할 때, 의존성과 설정값들 뿐만 아니라 객체의 생애 주기(scope)도 결정할 수 있다.&lt;br /&gt;이런 접근은 굉장히 강력하고 유연한 기능인데, 자바 클래스 레벨에서 객체의 생애 주기를 지정하지 않고, 그저 스코프를 객체에 알맞게 선택만 하면 되기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 프레임워크는 다음 6가지 스코프를 지원한다.&lt;br /&gt;그 중 4개는 웹 기반 &lt;code&gt;ApplicationContext&lt;/code&gt; 에서 사용 가능한 스코프이다.&lt;br /&gt;물론 커스텀한 스코프도 만들 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;singleton
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;(기본값) IoC 컨테이너마다 딱 하나씩만 존재하는 빈&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;prototype
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여러 개의 인스턴스가 생성될 수 있는 스코프&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;request
&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;웹 기반 &lt;code&gt;ApplicationContext&lt;/code&gt; 에서 사용 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;session
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HTTP Session 단위로 생애주기를 가짐&lt;/li&gt;
&lt;li&gt;웹 기반 &lt;code&gt;ApplicationContext&lt;/code&gt; 에서 사용 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;application
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;ServletContext&lt;/code&gt; 단위로 생애주기를 가짐&lt;/li&gt;
&lt;li&gt;웹 기반 &lt;code&gt;ApplicationContext&lt;/code&gt; 에서 사용 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;websocket
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;WebSocket&lt;/code&gt; 단위로 생애주기를 가짐&lt;/li&gt;
&lt;li&gt;웹 기반 &lt;code&gt;ApplicationContext&lt;/code&gt; 에서 사용 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;싱글턴 스코프&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;싱글턴 빈은 단 하나만 만들어지고 공유되며, 해당 빈을 id로 참조하고 있는 모든 요청이 스프링 컨테이너에 의해 반환되는 1개의 특정 인스턴스를 받게 된다.&lt;br /&gt;즉, 스프링 컨테이너는 싱글턴으로 정의된 빈은 단 1개만 생성한다.&lt;br /&gt;생성된 하나의 인스턴스는 싱글턴 빈으로 캐싱되었다가 모든 하위 요청과 참조 위치에 반환된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;프로토타입 스코프&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;싱글턴이 아닌 프로토타입 스코프는 매 요청 시 새로운 빈 인스턴스를 만들게 된다.&lt;br /&gt;다른 스코프들과 다르게, 스프링은 프로토타입 빈의 생애주기를 완전히 관리하지 않는다.&lt;br /&gt;컨테이너는 프로토타입 빈을 원하는 요청을 받을 때, 생성에만 관여한 이후로는 책임을 클라이언트에게 넘긴다.&lt;br /&gt;그에 따라 클라이언트는 프로토타입 빈을 정리하고, 할당된 높은 비용의 리소스들을 해소해야만 한다.&lt;br /&gt;즉, 스프링 컨테이너의 역할은 프로토타입 빈의 &lt;code&gt;new&lt;/code&gt; 연산자에만 관여하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로토타입 빈을 싱글턴 빈에 주입하게 되면 프로토타입 빈이 단 한번만 생성되게 되니 주의해야 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Request, Session, Application, WebSocket 스코프&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;request, session, application, websocket 스코프는 웹 기반 &lt;code&gt;ApplicationContext&lt;/code&gt; 에서만 사용 가능하다.&lt;br /&gt;이 스코프들은 싱글턴, 프로토타입 스코프와 다르게 약간의 초기 설정이 필요하다.&lt;br /&gt;스프링 MVC를 사용하고 있다면, 즉 &lt;code&gt;DispatcherServlet&lt;/code&gt; 에 의해 다뤄지는 요청이라면, 특별한 세팅은 필요 없다.&lt;br /&gt;그 외 Servlet 2.5 컨테이너를 사용하고 있다면, RequestContextListener 등록을 해줘야 하고, Servlet 3.0 이상의 버전을 사용하고 있다면, WebApplicationInitializer 인터페이스를 사용하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;request 스코프는 HTTP 요청 단위로 빈을 생성하고, session 스코프는 HTTP Session 단위로 빈을 생성한다.&lt;br /&gt;application 스코프는 ServletContext 단위로 빈이 생겨 싱글턴과 유사하지만, ApplicationContext 단위가 아니라 ServletContext 단위라는 점이 다르고, ServletContext의 속성으로 드러난다는 것이 다르다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커스텀한 스코프는 &lt;code&gt;org.springframework.beans.factory.config.Scope&lt;/code&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;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;BeanPostProcessor를 사용한 빈 커스터마이징&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;BeanPostProcessor&lt;/code&gt; (빈 후처리기) 인터페이스는 초기화 로직, 의존성 로직 등을 재정의할 수 있도록 하는 콜백 메서드들이다.&lt;br /&gt;만약 스프링 컨테이너가 빈을 초기화한 후에 특별한 커스텀 로직을 수행하도록 추가하고 싶다면, 필요한 만큼 BeanPostProcessor 구현을 추가하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 개의 BeanPostProcessor 인스턴스를 구성할 때에는, &lt;code&gt;order&lt;/code&gt; 속성으로 인스턴스 간 순서를 지정할 수 있다.&lt;br /&gt;이는 &lt;code&gt;Ordered&lt;/code&gt; 인터페이스로 지정 가능하며, BeanPostProcessor를 사용할 때 항상 같이 고려하는 것이 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BeanPostProcessor는 2개의 콜백 메서드를 가지는데, 각각 빈이 생성되기 전과 후를 제어할 수 있는 메서드들이다.&lt;br /&gt;빈 후처리기는 이 메서드를 통해 빈의 생성에 대한 어떤 처리도 진행할 수 있다.&lt;br /&gt;몇몇 스프링 AOP에서는 빈 후처리기를 사용해 프록시 래핑 로직을 수행하기도 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;BeanFactoryPostProcessor를 사용한 설정 메타데이터 커스터마이징&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 &lt;code&gt;BeanFactoryPostProcessor&lt;/code&gt; 이다.&lt;br /&gt;BeanPostProcessor와 비슷하지만, 가장 큰 차이점은, BeanFactoryPostProcessor는 빈 설정 메타데이터에서 동작한다는 것이다.&lt;br /&gt;이는 스프링 IoC 컨테이너가 BeanFactoryPostProcessor가 설정 메타데이터를 읽고 변경할 수 있도록 허용한다는 뜻이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BeanPostProcessor와 마찬가지로, 여러 개의 BeanFactoryPostProcessor를 사용하는 경우 &lt;code&gt;Ordered&lt;/code&gt; 인터페이스로 순서를 같이 고려하는 것이 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빈 팩토리 후처리기는 ApplicationContext에 선언되어 있는 경우 컨테이너에 정의된 설정 메타데이터를 변경하기 위해 자동으로 동작한다.&lt;br /&gt;스프링에는 이미 정의된 여러 빈 팩토리 후처리기가 존재하며, 커스텀한 빈 팩토리 후처리기를 추가할 수도 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;FactoryBean을 사용한 초기화 로직 커스터마이징&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;FactoryBean&lt;/code&gt; 인터페이스는 빈을 생성할 수 있는 인터페이스이다.&lt;br /&gt;만약 어떤 빈의 초기화 로직이 복잡해서, XML 설정 등으로 구성하기가 어려운 경우 커스텀한 FactoryBean을 구성해서 추가할 수 있다.&lt;br /&gt;FactoryBean은 다음 3가지 메서드를 가진다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;T getObject()&lt;/code&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;/li&gt;
&lt;li&gt;&lt;code&gt;boolean isSingleton()&lt;/code&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;싱글턴 빈을 반환하는지의 여부&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ClasS&amp;lt;?&amp;gt; getObjectType()&lt;/code&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;생성하는 빈의 타입&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/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;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;'어노테이션 기반 설정이 XML 설정 보다 나을까?' 라는 질문에 대한 답은 '개발자에게 달렸다'이다.&amp;nbsp; &lt;br /&gt;어노테이션은 짧고 간결한 설정을 통해 선언만으로 많은 맥락을 제공하는 반면, XML은 본연의 소스 코드를 건드리지 않고 설정을 할 수 있다는 특징을 가지고 있다.&amp;nbsp; &lt;br /&gt;어떤 개발자들은 소스 코드와 설정을 하나로 묶는 반면 또 다른 이들은 어노테이션 기반 클래스들이 더이상 POJO가 아니며, 설정들이 군집화되지 않고 제어하기 힘들다고 주장한다.&amp;nbsp; &lt;br /&gt;스프링은 두 가지 스타일 모두 수용하고 있고, 동시 사용도 허용하고 있으니 상황에 맞게 사용하면 된다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;태그를 사용하는 XML 설정의 대안으로 바이트코드 수준에서 컴포넌트들을 연결하는 어노테이션 기반 설정이 있다.&lt;br /&gt;XML로 빈 와이어링을 설정하는 대신, 어노테이션을 통해 설정값들을 컴포넌트 클래스로 옮기는 방식이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;@Required&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;@Required&lt;/code&gt; 어노테이션은 setter 메서드에 쓰인다.&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;@Required
public void setMovieFinder(MovieFinder movieFinder) {
    this.movieFinder = movieFinder;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 초기 빈 설정 시 빈 정의나 오토와이어링을 통해 필요한 빈 프로퍼티가 있음을 알려주는 어노테이션이다.&lt;br /&gt;Spring 5.1부터는 Deprecated 되었다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;@Autowired&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;@Autowired&lt;/code&gt; 는 다음과 같이 사용한다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;public class MovieRecommender {

    private final CustomerPreferenceDao customerPreferenceDao;

    @Autowired
    public MovieRecommender(CustomerPreferenceDao customerPreferenceDao) {
        this.customerPreferenceDao = customerPreferenceDao;
    }

    // ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성자 뿐만 아니라 setter 메서드, 필드에도 사용할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;@Primary를 사용한 오토와이어링&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오토와이어링 시에는 여러 빈 후보들이 있을 수 있기 때문에, 어떤 빈을 선택할지에 대한 제어가 필요하다.&lt;br /&gt;이를 구성하는 어노테이션 중 하나가 바로 &lt;code&gt;@Primary&lt;/code&gt; 이다.&lt;br /&gt;&lt;code&gt;@Primary&lt;/code&gt; 는 여러 후보 빈 중 특정 빈을 우선적으로 선택할 수 있도록 알려주는 어노테이션이다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Configuration
public class MovieConfiguration {

    @Bean
    @Primary
    public MovieCatalog firstMovieCatalog() { ... }

    @Bean
    public MovieCatalog secondMovieCatalog() { ... }

    // ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;@Qualifier를 사용한 오토와이어링&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좀 더 세밀한 조정을 원한다면, &lt;code&gt;@Qualifier&lt;/code&gt; 어노테이션을 사용해볼 수도 있다.&lt;br /&gt;&lt;code&gt;@Primary&lt;/code&gt; 가 하나의 우선 후보가 존재할 때 사용하면 좋은 반면, &lt;code&gt;@Qualifier&lt;/code&gt; 는 전달한 파라미터 기반으로 어떤 빈을 선택할지 정할 수 있는 어노테이션이다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;public class MovieRecommender {

    @Autowired
    @Qualifier(&quot;main&quot;)
    private MovieCatalog movieCatalog;

    // ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커스텀한 Qualifier 어노테이션을 만들 수도 있다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Genre {

    String value();
}

@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface MovieQualifier {

    String genre();

    Format format(); // Enum
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Qualifier와 제네릭 사용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Qualifer는 제네릭 타입도 감지한다.&lt;br /&gt;&lt;code&gt;Store&amp;lt;String&amp;gt;&lt;/code&gt; 과 &lt;code&gt;Store&amp;lt;Integer&amp;gt;&lt;/code&gt; 를 각각 구현한 Store가 있다고 가정할 경우, 아래와 같이 사용 가능하다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Configuration
public class MyConfiguration {

    @Bean
    public StringStore stringStore() {
        return new StringStore();
    }

    @Bean
    public IntegerStore integerStore() {
        return new IntegerStore();
    }
}
// -----------------------------------------
@Autowired
private Store&amp;lt;String&amp;gt; s1; // &amp;lt;String&amp;gt; qualifier

@Autowired
private Store&amp;lt;Integer&amp;gt; s2; // &amp;lt;Integer&amp;gt; qualifier&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;@Value&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;@Value&lt;/code&gt; 는 외부 프로퍼티를 주입할 수 있는 어노테이션이다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Component
public class MovieRecommender {

    private final String catalog;

    public MovieRecommender(@Value(&quot;${catalog.name}&quot;) String catalog) {
        this.catalog = catalog;
    }
}
// -----------------------------------------
// application.properties
catalog.name = MovieCatalog&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;@PostConstruct와 @PreDestroy&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;@PostConstruct&lt;/code&gt; 는 빈 생성 전 초기화 로직을 추가할 수 있는 어노테이션이고, &lt;code&gt;@PreDestroy&lt;/code&gt; 는 빈 소멸 시에 필요한 로직을 추가할 수 있는 어노테이션이다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;public class CachingMovieLister {

    @PostConstruct
    public void populateMovieCache() {
        // 초기화 로직
    }

    @PreDestroy
    public void clearMovieCache() {
        // 클리어 로직
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Classpath 스캔과 컴포넌트 관리&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 위 예제들에서는 빈에 대한 정의를 XML 기반으로 진행했지만, 이번에는 클래스패스를 스캔하여 후보 컴포넌트를 찾는 방식을 알아보려고 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;@Component와 표준 어노테이션&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링은 &lt;code&gt;@Component&lt;/code&gt; 어노테이션과 &lt;code&gt;@Repository&lt;/code&gt; , &lt;code&gt;@Service&lt;/code&gt; , &lt;code&gt;@Controller&lt;/code&gt; 등의 &lt;code&gt;@Component&lt;/code&gt; 어노테이션의 특수 케이스인 어노테이션들을 제공한다.&lt;br /&gt;&lt;code&gt;@Component&lt;/code&gt; 를 사용하면 스프링에 의해 관리되는 컴포넌트, 혹은 에스펙트(aspect)를 적용하도록 해당 클래스를 사용할 수 있고, &lt;code&gt;@Repository&lt;/code&gt; , &lt;code&gt;@Service&lt;/code&gt; , &lt;code&gt;@Controller&lt;/code&gt; 는 각 레이어에 맞게 사용할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;메타 어노테이션과 합성 어노테이션&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링에서 제공되는 여러 어노테이션들은 코드에서 메타 어노테이션으로 활용된다.&lt;br /&gt;예를 들어, &lt;code&gt;@Service&lt;/code&gt; 어노테이션은 &lt;code&gt;@Component&lt;/code&gt; 어노테이션을 가진 메타 어노테이션이다.&lt;br /&gt;또한 메타 어노테이션은 여러 어노테이션이 합쳐져서 만들어지기도 한다.&lt;br /&gt;&lt;code&gt;@RestController&lt;/code&gt; 는 &lt;code&gt;@Controller&lt;/code&gt; 와 &lt;code&gt;@ResponseBody&lt;/code&gt; 가 합쳐진 메타 어노테이션이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;클래스 자동 탐지 및 빈 정의 등록&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링은 이런 클래스들을 자동으로 찾아서 ApplicationContext를 통해 BeanDefinition 을 만들어 등록할 수 있다.&lt;br /&gt;빈들을 찾고 등록하기 위해서는 &lt;code&gt;@Configuration&lt;/code&gt; 클래스에 &lt;code&gt;@ComponentScan&lt;/code&gt; 어노테이션을 추가해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빈 스캔 시에 원하는 타입을 정해서 필터도 적용할 수 있다.&lt;br /&gt;특정 클래스를 상속받는 어노테이션이라던가, Aspectj 표현식, 정규식, 커스텀한 필터 등으로 필터링하여 스캔할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Configuration
@ComponentScan(basePackages = &quot;org.example&quot;,
        includeFilters = @Filter(type = FilterType.REGEX, pattern = &quot;.*Stub.*Repository&quot;),
        excludeFilters = @Filter(Repository.class))
public class AppConfig {
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;컴포넌트를 통한 빈 메타데이터 정의&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 컴포넌트는 또한 컨테이너에게 빈 정의 메타데이터를 구성해서 넘겨줄 수 있다.&lt;br /&gt;이를 &lt;code&gt;@Configuration&lt;/code&gt; 클래스에서 &lt;code&gt;@Bean&lt;/code&gt; 어노테이션을 사용해 특정 빈을 정의하여 컨테이너에 넘겨줄 수 있다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Component
public class FactoryMethodComponent {

    @Bean
    @Qualifier(&quot;public&quot;)
    public TestBean publicInstance() {
        return new TestBean(&quot;publicInstance&quot;);
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반 스프링 컴포넌트의 &lt;code&gt;@Bean&lt;/code&gt; 메서드와 &lt;code&gt;@Configuration&lt;/code&gt; 내의 &lt;code&gt;@Bean&lt;/code&gt; 메서드는 차이가 있다.&lt;br /&gt;차이점은 &lt;code&gt;@Component&lt;/code&gt; 클래스가 메서드나 필드 호출을 가로채기 위해서 CGLIB를 사용하지 않는다는 것이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;오토디텍팅된 컴포넌트 네이밍&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴포넌트가 디텍팅되면, 빈의 이름은 BeanNameGenerator를 통해 생성된다.&lt;br /&gt;스프링의 기본 제공 어노테이션(&lt;code&gt;@Component&lt;/code&gt; , &lt;code&gt;@Repository&lt;/code&gt; , &lt;code&gt;@Service&lt;/code&gt; , &lt;code&gt;@Controller&lt;/code&gt; )들은 이에 따라 해당 빈 정의에 이름을 제공한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;오토디텍팅된 컴포넌트에 스코프 제공&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링이 관리하는 컴포넌트는 기본적으로 &lt;code&gt;싱글턴&lt;/code&gt; 스코프를 갖는다.&lt;br /&gt;그러나 때로 다른 스코프를 지정해야 할 때가 있는데, 이때는 &lt;code&gt;@Scope&lt;/code&gt; 어노테이션을 사용하면 된다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Scope(&quot;prototype&quot;)
@Repository
public class MovieFinderImpl implements MovieFinder {
    // ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;JSR 330 표준 어노테이션&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 3.0을 사용한다면, 스프링은 JSR-330 표준 어노테이션의 지원을 제공한다.&lt;br /&gt;이 어노테이션들은 스프링 어노테이션과 마찬가지로 스캐닝되며, 사용하려면 관련 jar 파일을 클래스패스에 추가해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;@Autowired&lt;/code&gt; 대신 &lt;code&gt;@Inject&lt;/code&gt; 를 사용해볼 수도 있다.&lt;br /&gt;&lt;code&gt;@Autowired&lt;/code&gt; 와 마찬가지로, &lt;code&gt;@Inject&lt;/code&gt; 도 필드 레벨, 메서드 레벨, 생성자 레벨에 적용할 수 있다.&lt;br /&gt;게다가 주입 포인트에 Provider를 선언하면 주입받은 빈에 대한 지연 로딩을 구현할 수도 있다.&lt;/p&gt;
&lt;pre class=&quot;gradle&quot;&gt;&lt;code&gt;import javax.inject.Inject;

public class SimpleMovieLister {

    private Provider&amp;lt;MovieFinder&amp;gt; movieFinder;

    @Inject
    public void setMovieFinder(Provider&amp;lt;MovieFinder&amp;gt; movieFinder) {
        this.movieFinder = movieFinder;
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;@Named&lt;/code&gt; 와 &lt;code&gt;@ManagedBean&lt;/code&gt; 은 &lt;code&gt;@Component&lt;/code&gt; 대신 사용해볼 수도 있다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Named(&quot;movieListener&quot;)  // @ManagedBean(&quot;movieListener&quot;)
public class SimpleMovieLister {

    private MovieFinder movieFinder;

    @Inject
    public void setMovieFinder(MovieFinder movieFinder) {
        this.movieFinder = movieFinder;
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링의 어노테이션을 대체할 수 있는 나머지 JSR-330 어노테이션에 대한 소개와 제약 사항은 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;@Autowired&lt;/code&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;@Inject&lt;/code&gt; 로 대체 가능&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Inject&lt;/code&gt; 에는 required 필드가 없어서, 필요하다면 Java8의 Optional을 같이 활용해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Component&lt;/code&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;@Named&lt;/code&gt; , &lt;code&gt;@ManagedBean&lt;/code&gt; 으로 대체 가능&lt;/li&gt;
&lt;li&gt;구성 가능한 모델을 제공하지 않고 이름으로 식별하는 방법만 제공&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Scope(&quot;singleton&quot;)&lt;/code&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;@Singleton&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;JSR-330의 기본 스코프는 스프링의 prototype과 비슷하게 동작하지만, 스프링 컨테이너 내에서는 싱글턴으로 동작하도록 선언되었다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Qualifier&lt;/code&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;@Qualifier&lt;/code&gt; , &lt;code&gt;@Named&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Value&lt;/code&gt; , &lt;code&gt;@Required&lt;/code&gt; , &lt;code&gt;@Lazy&lt;/code&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;대체할 수 있는 사항이 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;ObjectFactory
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Provider&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>RTFM</category>
      <category>RTFM</category>
      <category>SpringCore</category>
      <author>wbluke</author>
      <guid isPermaLink="true">https://wbluke.tistory.com/63</guid>
      <comments>https://wbluke.tistory.com/63#entry63comment</comments>
      <pubDate>Tue, 6 Jul 2021 12:41:00 +0900</pubDate>
    </item>
    <item>
      <title>[RTFM] 매일 읽는 공식 문서</title>
      <link>https://wbluke.tistory.com/62</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;개요&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 사용하고 있는 기술에 대해 조금씩 깊이 알게 되거나 알아가야 하는 시점이 도래하면서, 피상적인 정보들만으로는 채울 수 없는, 본질에 대한 욕구가 깊어지고 있었습니다.&lt;br /&gt;마침 제가 몸 담고 있는 커뮤니티에서 &lt;code&gt;[RTFM] 매일 읽는 공식 문서&lt;/code&gt; 라는 이름으로 스터디가 열려서 참석하고 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;[RTFM] 매일 읽는 공식 문서&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 정보의 전달 방식을 &lt;code&gt;지식&lt;/code&gt; 과 &lt;code&gt;경험&lt;/code&gt; 으로 나누었을 때, 경험에 대한 전달은 모두가 쉽게 경험할 수 없고 그 자체로 희소성이 있는 내용이지만, 지식에 대한 전달은 비교적 공통의 출처가 정해져 있고, 그 출처에서 파생된 내용을 2차, 3차 가공을 거쳐 게시하는 경우가 많습니다. (저 역시도 그렇고요.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;경험&lt;/code&gt; 을 제외한 &lt;code&gt;지식&lt;/code&gt; 정보에 한하여, 2차 가공물이라고 볼 수 있는 여러 블로그, Stack OverFlow 등의 게시물 대신 공식 문서를 읽음으로써 정보의 원천에 가까워지자는 취지에서 시작된 스터디입니다.&lt;br /&gt;또한 새로운 내용을 알아간다는 목적보다는, 그동안 알고 있던 지식을 더욱 견고히 하고, 공식 문서에서 제시하는 문맥에 내가 가지고 있는 정보를 일치시킨다는 의도도 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;방식&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OT 모임에서 서로 각자 읽기를 원하는 Top 5 공식문서를 추리고, 비슷한 성격의 그룹으로 나누어 우선순위를 정하고 읽기로 했습니다.&lt;br /&gt;제가 속한 그룹은 Spring Core 문서부터 시작하기로 했고, 매일 하루 분량의 범위를 정해 읽는 것은 각자 자유로운 시간에 읽은 뒤, 해당 내용에 대한 미팅은 출근 전 오전 시간에 진행하는 것으로 결정했습니다. (평일만 하고 주말은 쉽니다.)&lt;br /&gt;그룹 미팅 시간은 오전 8시이고, 15 ~ 20분 정도 당일 리더의 주관 하에 읽은 내용을 정리하고 이해가 안가는 내용을 공유하면서 서로 싱크를 맞추고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저 같은 경우는 평일 아침 6시 ~ 6시 30분 쯤 기상하여 정해진 하루 치 범위를 읽고 정리하고 있습니다.&lt;br /&gt;영어로 된 공식 문서를 언제 어떻게 읽는 게 좋을지 여러가지 방식으로 테스트해 보았는데요.&lt;br /&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;/li&gt;
&lt;li&gt;읽고 정리한 내용으로 1일 1커밋을 진행하고 있는데, 오랜 경험 상 오전에 커밋을 찍으면 그 날 하루가 자유롭다. 저녁 시간을 내 마음대로 사용 가능하다.&lt;/li&gt;
&lt;li&gt;오전 미팅 직전에 읽어야 Time Limit 이 걸린 상태로 읽어서 효율이 좋다. (리딩하는 날이면 더 쪼들린다.)&lt;/li&gt;
&lt;li&gt;한글 + 내 방식으로 정리해 놓지 않으면 쉽게 휘발될 여지가 있기 때문에, 시간이 조금 더 걸리더라도 별도로 정리한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;주의 사항&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2021년 6월 14일부터 진행하기 시작했고, 벌써 4주차네요.&lt;br /&gt;읽으면서 정리한 내용으로 블로그에 족적을 남기려고 합니다.&lt;br /&gt;다만 2차 가공물을 피하기 위해 시작한 스터디에서 제가 2차 가공물을 생산하고 있네요 ㅋㅋ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고하신다면 다음과 같은 특징을 유념해주시기 바랍니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모든 내용을 담지 않았습니다. 읽으면서 필요 없다고 생각한 내용이나 이해가 너무 안가는 내용은 과감히 제외했습니다.&lt;/li&gt;
&lt;li&gt;오역과 의역, 축약 등이 있을 수 있습니다.&lt;/li&gt;
&lt;li&gt;'어 뭔가 좀 이상한데?' 싶으시면 바로 공식 문서를 보시는 것을 추천합니다.&amp;nbsp;&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;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Spring Core&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원문 : &lt;a href=&quot;https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#spring-core&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#spring-core&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://wbluke.tistory.com/63&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;01. The IoC Container (1)&lt;/a&gt;&amp;nbsp;&amp;nbsp;&lt;br /&gt;&lt;a href=&quot;https://wbluke.tistory.com/64&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;01. The IoC Container (2)&lt;/a&gt; &amp;nbsp;&lt;br /&gt;&lt;a href=&quot;https://wbluke.tistory.com/65&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;02. Resources&lt;/a&gt;&amp;nbsp;&amp;nbsp;&lt;br /&gt;&lt;a href=&quot;https://wbluke.tistory.com/66&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;03. Validation, Data Binding, and Type Conversion&lt;/a&gt;&amp;nbsp;&amp;nbsp;&lt;br /&gt;&lt;a href=&quot;https://wbluke.tistory.com/67&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;04. Spring Expression Language (SpEL)&lt;/a&gt;&amp;nbsp;&amp;nbsp;&lt;br /&gt;&lt;a href=&quot;https://wbluke.tistory.com/68&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;05. Aspect Oriented Programming with Spring&lt;/a&gt;&amp;nbsp;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>RTFM</category>
      <category>RTFM</category>
      <author>wbluke</author>
      <guid isPermaLink="true">https://wbluke.tistory.com/62</guid>
      <comments>https://wbluke.tistory.com/62#entry62comment</comments>
      <pubDate>Tue, 6 Jul 2021 12:36:36 +0900</pubDate>
    </item>
    <item>
      <title>제모옥은 젠킨스 조회 로직 개선으로 하겠습니다. 근데 이제 비동기를 곁들인</title>
      <link>https://wbluke.tistory.com/61</link>
      <description>&lt;p&gt;이 글은 &lt;a href=&quot;https://woowabros.github.io/experience/2021/04/06/fetch-jenkins-api-async.html&quot;&gt;우아한형제들 기술블로그에 기고한 글&lt;/a&gt;과 동일한 글입니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;제목의 밈은 &lt;a href=&quot;https://www.youtube.com/watch?v=IuBfmQs9wcA#t=65&quot;&gt;조림요정의 휴먼강록체&lt;/a&gt;입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Intro&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p&gt;평화로운 2020년 9월의 어느 날...&lt;br /&gt;데일리 미팅을 마치고 일감을 정리하던 저에게 한 가지 요청이 들어왔습니다.&lt;br /&gt;&quot;우빈님 여기 로직이 오래 걸리면 90초 넘게 걸리고 있는데 한번 개선할 수 있을지 확인 부탁드려요.&quot;&lt;/p&gt;
&lt;p&gt;'읭 아니 대체 어떤 레거시길래 90초씩이나 걸리는거야'&lt;br /&gt;라고 생각하며 코드를 열어서 확인했는데요.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;01_its_you.png&quot; data-origin-width=&quot;625&quot; data-origin-height=&quot;615&quot; width=&quot;364&quot; height=&quot;NaN&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9NTph/btq16zUn29Z/o5UC3NRtVQFky7xjmKqeK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9NTph/btq16zUn29Z/o5UC3NRtVQFky7xjmKqeK0/img.png&quot; data-alt=&quot;너라고..&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9NTph/btq16zUn29Z/o5UC3NRtVQFky7xjmKqeK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9NTph%2Fbtq16zUn29Z%2Fo5UC3NRtVQFky7xjmKqeK0%2Fimg.png&quot; data-filename=&quot;01_its_you.png&quot; data-origin-width=&quot;625&quot; data-origin-height=&quot;615&quot; width=&quot;364&quot; height=&quot;NaN&quot; data-ke-mobilestyle=&quot;widthContent&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;/&gt;&lt;/span&gt;&lt;figcaption&gt;너라고..&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;범인은 다섯 달 전의 저였습니다.&lt;br /&gt;개발자들에게는 흔히 있는 일이라고 하는데... 저만 겪고 있는 건 아니죠?&lt;/p&gt;
&lt;p&gt;오늘 포스팅에서는 위 레거시를 생산하게 된 배경과, 그 해결과정을 정리해서 공유해보려고 합니다.&lt;br /&gt;크게 어려운 내용은 아니니 해결해가는 과정 자체에 포인트를 두고 가볍게 읽어주시면 감사하겠습니다.  &lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2&gt;5개월 전...&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p&gt;입사 후 &lt;a href=&quot;https://woowabros.github.io/experience/2020/03/02/pilot-project-wbluke.html&quot;&gt;신규입사자 온보딩 과정&lt;/a&gt;을 마무리하고, 팀에서 가장 먼저 받은 첫 번째 과제는 정산시스템 어드민(관리자 페이지)의 최초 진입 화면인 &lt;code&gt;대시보드&lt;/code&gt;를 만드는 과제였습니다.&lt;br /&gt;당시 정산 어드민에 로그인한 후 보이는 첫 화면은 좌측 메뉴 바를 제외하고는 특별하게 제공되는 것이 없는 빈 화면이었는데요.&lt;br /&gt;관리자 페이지를 사용하시는 기획/운영자 분이 수시로 확인하면 좋을 각종 실시간 지표들을 화면에 구축하는 것이 바로 해당 과제의 내용이었습니다.&lt;/p&gt;
&lt;h3&gt;배치 모니터링&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;02_monitoring_batch.png&quot; data-origin-width=&quot;1015&quot; data-origin-height=&quot;764&quot; width=&quot;489&quot; height=&quot;NaN&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bkPSo0/btq10R96TWD/KLEryqNC0ZHskk9Icz9o41/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bkPSo0/btq10R96TWD/KLEryqNC0ZHskk9Icz9o41/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bkPSo0/btq10R96TWD/KLEryqNC0ZHskk9Icz9o41/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbkPSo0%2Fbtq10R96TWD%2FKLEryqNC0ZHskk9Icz9o41%2Fimg.png&quot; data-filename=&quot;02_monitoring_batch.png&quot; data-origin-width=&quot;1015&quot; data-origin-height=&quot;764&quot; width=&quot;489&quot; height=&quot;NaN&quot; data-ke-mobilestyle=&quot;widthContent&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;요구사항에 있는 많은 지표들 중 하나는, 정산시스템의 허리를 담당하고 있는 수십 개의 배치를 개발자가 아닌 운영자 분이 모니터링할 수 있도록 하는 &lt;code&gt;배치 모니터링&lt;/code&gt; 지표였습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;아래에 이어서 나오는 배치 모니터링 분석에 대한 내용은 당시에 &lt;a href=&quot;https://woowabros.github.io/experience/2020/10/08/excel-download.html&quot;&gt;태현님&lt;/a&gt;이 많은 도움을 주셨습니다  &lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;배치 모니터링 지표에서는 그날 수행될 예정인 배치가 어떤 것인지 파악할 수 있어야 했고, 해당 배치의 실시간 상태를 확인할 수 있어야 했습니다.&lt;br /&gt;또한 각 배치의 활성화(on/off) 상태와 더불어 수행 시작/종료 시간도 트래킹할 수 있어야 했습니다. &lt;br /&gt;즉, 정리하자면 다음과 같은 정보들을 어디선가 얻어오거나 필요에 따라 새롭게 정의해야 했습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;배치의 가장 최근 작업 상태 (수행중/성공/실패/수행예정)&lt;/li&gt;
&lt;li&gt;활성화 여부&lt;/li&gt;
&lt;li&gt;작업 시작시간/종료시간&lt;/li&gt;
&lt;li&gt;작업 주기 (배치가 수행될 예정인지를 판별하기 위함)
&lt;ul&gt;
&lt;li&gt;운영자가 매일/영업일/매월 1일/수시 등으로 사전에 정의하여 지정&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;저희 팀은 배치 관리 도구로 젠킨스(Jenkins)를 사용하고 있는데요.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;03_jenkins.png&quot; data-origin-width=&quot;414&quot; data-origin-height=&quot;460&quot; width=&quot;215&quot; height=&quot;NaN&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bWzkLq/btq10SgSEAc/OKvSi2jgEwFzDQoEEx5JN1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bWzkLq/btq10SgSEAc/OKvSi2jgEwFzDQoEEx5JN1/img.png&quot; data-alt=&quot;오늘의 주인공 젠킨스 집사님&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bWzkLq/btq10SgSEAc/OKvSi2jgEwFzDQoEEx5JN1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbWzkLq%2Fbtq10SgSEAc%2FOKvSi2jgEwFzDQoEEx5JN1%2Fimg.png&quot; data-filename=&quot;03_jenkins.png&quot; data-origin-width=&quot;414&quot; data-origin-height=&quot;460&quot; width=&quot;215&quot; height=&quot;NaN&quot; data-ke-mobilestyle=&quot;widthContent&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;/&gt;&lt;/span&gt;&lt;figcaption&gt;오늘의 주인공 젠킨스 집사님&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;정산시스템에서 이 젠킨스를 통해 관리하고 있는 배치의 형태를 파악해보니 다음과 같이 세 종류로 구분할 수 있었습니다. (이름은 임의로 붙여봤습니다.)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;04_jenkins_new_items.png&quot; data-origin-width=&quot;1644&quot; data-origin-height=&quot;1142&quot; width=&quot;709&quot; height=&quot;NaN&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/brKUME/btq10576nZK/aHNK1mj0eEZaAk5n24TGbk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/brKUME/btq10576nZK/aHNK1mj0eEZaAk5n24TGbk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/brKUME/btq10576nZK/aHNK1mj0eEZaAk5n24TGbk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbrKUME%2Fbtq10576nZK%2FaHNK1mj0eEZaAk5n24TGbk%2Fimg.png&quot; data-filename=&quot;04_jenkins_new_items.png&quot; data-origin-width=&quot;1644&quot; data-origin-height=&quot;1142&quot; width=&quot;709&quot; height=&quot;NaN&quot; data-ke-mobilestyle=&quot;widthContent&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Simple Job
&lt;ul&gt;
&lt;li&gt;가장 단순한 형태의 배치로, Spring Batch와 Jenkins Job이 1:1인 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Trigger Job
&lt;ul&gt;
&lt;li&gt;하나의 주요한 배치 A가 있고, A 배치에게 다양한 파라미터를 제공하며 수행 요청만 하고 자기자신은 종료되는 배치&lt;/li&gt;
&lt;li&gt;배치 A와 1:N 관계&lt;/li&gt;
&lt;li&gt;예를 들어, 배치 A에 1번 파라미터를 제공하는 배치, 배치 A에 2번 파라미터를 제공하는 배치 등&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Pipeline Job
&lt;ul&gt;
&lt;li&gt;여러 개의 배치 Job을 순차적으로 실행할 수 있는 파이프라인 배치&lt;/li&gt;
&lt;li&gt;파이프라인 Script로 하위 배치 Job를 단계별로 지정해서 실행&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이렇게 구해야 하는 배치의 정보와, 현재 배치 시스템의 상황을 파악하고 나니 다음과 같은 고민이 있었습니다.&lt;/p&gt;
&lt;p&gt;&lt;b&gt;  수십 개의 배치가 수행된 상태를 실시간으로 조회하기 위해 어떤 방법을 선택해야 하는가?&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;크게는 두 가지 방법을 놓고 고민했는데요.&lt;br /&gt;바로 &lt;code&gt;Spring Batch Metadata Tables&lt;/code&gt;와 &lt;code&gt;젠킨스 API&lt;/code&gt; 였습니다.&lt;/p&gt;
&lt;h3&gt;Spring Batch Metadata Tables&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;05_spring_batch_metadata_tables.png&quot; data-origin-width=&quot;1688&quot; data-origin-height=&quot;1372&quot; width=&quot;703&quot; height=&quot;NaN&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0zsCH/btq150ShLe1/O7AvUJlYDRkCkgWWd943Hk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0zsCH/btq150ShLe1/O7AvUJlYDRkCkgWWd943Hk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0zsCH/btq150ShLe1/O7AvUJlYDRkCkgWWd943Hk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0zsCH%2Fbtq150ShLe1%2FO7AvUJlYDRkCkgWWd943Hk%2Fimg.png&quot; data-filename=&quot;05_spring_batch_metadata_tables.png&quot; data-origin-width=&quot;1688&quot; data-origin-height=&quot;1372&quot; width=&quot;703&quot; height=&quot;NaN&quot; data-ke-mobilestyle=&quot;widthContent&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.spring.io/spring-batch/docs/current/reference/html/schema-appendix.html&quot;&gt;Spring Batch Metadata Tables(Schema)&lt;/a&gt;는 Spring Batch로 수행된 모든 Job에 대한 정보를 가지고 있는 메타데이터 테이블입니다.&lt;br /&gt;위 요구사항과 관련한 정보들을 가져오기 위해서는, 그중에서도 BATCH_JOB_INSTANCE 테이블과 BATCH_JOB_EXECUTION 테이블, 그리고 BATCH_JOB_EXECUTION_PARAMS 테이블을 참고해야 할 것으로 보였습니다.&lt;br /&gt;Spring Batch를 사용하시는 분들은 대부분 아시겠지만, 한번 간단하게 정리해보자면 다음과 같습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;BATCH_JOB_INSTANCE
&lt;ul&gt;
&lt;li&gt;Job Parameter에 따른 배치 수행 정보를 기록&lt;/li&gt;
&lt;li&gt;동일 파라미터로는 데이터가 발생하지 않음 (배치 수행 불가)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;BATCH_JOB_EXECUTION
&lt;ul&gt;
&lt;li&gt;BATCH_JOB_INSTANCE의 자식 테이블로, 모든 성공/실패 내역을 가지고 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;BATCH_JOB_EXECUTION_PARAMS
&lt;ul&gt;
&lt;li&gt;실제 수행되었던 배치의 구체적인 모든 파라미터가 기록되는 테이블&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;더 상세한 내용은 &lt;a href=&quot;https://jojoldu.tistory.com/326&quot;&gt;jojoldu님의 블로그&lt;/a&gt;를 참고해보시면 좋습니다.&lt;/p&gt;
&lt;p&gt;Spring Batch Metadata Tables와 위 요구사항을 매칭해보니, 다음과 같은 문제점들이 보였습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;파이프라인 Job은 메타테이블이 직접적으로 관리하고 있지 않기 때문에 추가 작업이 필요하다.
&lt;ul&gt;
&lt;li&gt;파이프라인 하위 배치들의 순서를 알아야 하고, 순서가 달라지면 코드 변경 및 배포가 필요하다.&lt;/li&gt;
&lt;li&gt;첫 수행 배치 Job과 마지막 수행 배치 Job의 시간을 조회해야 한다.&lt;/li&gt;
&lt;li&gt;여러 테이블의 조인이 필요하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Jenkins Job의 활성화 여부는 Jenkins API로만 얻어올 수 있다.
&lt;ul&gt;
&lt;li&gt;(아래 캡쳐 참조) 배치 활성화 on/off 는 &lt;code&gt;빌드 안함&lt;/code&gt; 체크로 관리하고 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;06_jenkins_build_false.png&quot; data-origin-width=&quot;1638&quot; data-origin-height=&quot;1154&quot; width=&quot;690&quot; height=&quot;NaN&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MhYJr/btq16QuQOkV/RLoLUMZwLD3th77jFjbKu0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MhYJr/btq16QuQOkV/RLoLUMZwLD3th77jFjbKu0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MhYJr/btq16QuQOkV/RLoLUMZwLD3th77jFjbKu0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMhYJr%2Fbtq16QuQOkV%2FRLoLUMZwLD3th77jFjbKu0%2Fimg.png&quot; data-filename=&quot;06_jenkins_build_false.png&quot; data-origin-width=&quot;1638&quot; data-origin-height=&quot;1154&quot; width=&quot;690&quot; height=&quot;NaN&quot; data-ke-mobilestyle=&quot;widthContent&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3&gt;Jenkins API&lt;/h3&gt;
&lt;p&gt;다음으로는 Jenkins의 API에 대해 고민했는데요.&lt;br /&gt;&lt;a href=&quot;https://www.jenkins.io/doc/book/using/remote-access-api/&quot;&gt;Jenkins API wiki&lt;/a&gt;가 있긴 하지만, 어떤 API가 있는지 정확하게 정리해주지는 않고 있습니다.&lt;br /&gt;하지만 우리의 요구사항에 대한 정보를 얻어오기 위해서는 다음 2개의 API면 충분합니다.&lt;/p&gt;
&lt;p&gt;첫 번째로 Job에 대한 정보를 받아오는 API 입니다. &lt;code&gt;${Jenkins_URL}/job/${job_name}/api/json&lt;/code&gt; 으로 요청할 수 있습니다. (Jenkins job 화면에서 &lt;code&gt;/api/json&lt;/code&gt; 을 붙여서 확인해보실 수 있습니다.) &lt;br /&gt;주요한 필드만 기술해보면 아래와 같습니다. (테스트 환경의 IP 정보는 JENKINS_URL로 표시했습니다.)&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;description&quot;: &quot;&quot;,
  &quot;displayName&quot;: &quot;test-01&quot;,
  &quot;fullDisplayName&quot;: &quot;test-01&quot;,
  &quot;fullName&quot;: &quot;test-01&quot;,
  &quot;name&quot;: &quot;test-01&quot;,
  &quot;url&quot;: &quot;http://JENKINS_URL/job/test-01/&quot;,
  &quot;buildable&quot;: true,
  &quot;builds&quot;: [
    {
      &quot;_class&quot;: &quot;hudson.model.FreeStyleBuild&quot;,
      &quot;number&quot;: 3,
      &quot;url&quot;: &quot;http://JENKINS_URL/job/test-01/3/&quot;
    },
    {
      &quot;_class&quot;: &quot;hudson.model.FreeStyleBuild&quot;,
      &quot;number&quot;: 2,
      &quot;url&quot;: &quot;http://JENKINS_URL/job/test-01/2/&quot;
    },
    {
      &quot;_class&quot;: &quot;hudson.model.FreeStyleBuild&quot;,
      &quot;number&quot;: 1,
      &quot;url&quot;: &quot;http://JENKINS_URL/job/test-01/1/&quot;
    }
  ],
  &quot;color&quot;: &quot;blue&quot;,
  &quot;firstBuild&quot;: {
    &quot;_class&quot;: &quot;hudson.model.FreeStyleBuild&quot;,
    &quot;number&quot;: 1,
    &quot;url&quot;: &quot;http://JENKINS_URL/job/test-01/1/&quot;
  },
  &quot;inQueue&quot;: false,
  &quot;keepDependencies&quot;: false,
  &quot;lastBuild&quot;: {
    &quot;_class&quot;: &quot;hudson.model.FreeStyleBuild&quot;,
    &quot;number&quot;: 3,
    &quot;url&quot;: &quot;http://JENKINS_URL/job/test-01/3/&quot;
  },
  &quot;lastCompletedBuild&quot;: {
    &quot;_class&quot;: &quot;hudson.model.FreeStyleBuild&quot;,
    &quot;number&quot;: 3,
    &quot;url&quot;: &quot;http://JENKINS_URL/job/test-01/3/&quot;
  },
  &quot;lastFailedBuild&quot;: {
    &quot;_class&quot;: &quot;hudson.model.FreeStyleBuild&quot;,
    &quot;number&quot;: 2,
    &quot;url&quot;: &quot;http://JENKINS_URL/job/test-01/2/&quot;
  },
  &quot;lastStableBuild&quot;: {
    &quot;_class&quot;: &quot;hudson.model.FreeStyleBuild&quot;,
    &quot;number&quot;: 3,
    &quot;url&quot;: &quot;http://JENKINS_URL/job/test-01/3/&quot;
  },
  &quot;lastSuccessfulBuild&quot;: {
    &quot;_class&quot;: &quot;hudson.model.FreeStyleBuild&quot;,
    &quot;number&quot;: 3,
    &quot;url&quot;: &quot;http://JENKINS_URL/job/test-01/3/&quot;
  },
  &quot;lastUnstableBuild&quot;: null,
  &quot;lastUnsuccessfulBuild&quot;: {
    &quot;_class&quot;: &quot;hudson.model.FreeStyleBuild&quot;,
    &quot;number&quot;: 2,
    &quot;url&quot;: &quot;http://JENKINS_URL/job/test-01/2/&quot;
  },
  &quot;nextBuildNumber&quot;: 4
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Job API에서 눈여겨 볼 정보들은 다음과 같습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;name
&lt;ul&gt;
&lt;li&gt;Job 이름&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;buildable
&lt;ul&gt;
&lt;li&gt;활성화 여부 (&lt;code&gt;빌드 안함&lt;/code&gt; 체크 여부)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;lastBuild, lastCompletedBuild, lastFailedBuild
&lt;ul&gt;
&lt;li&gt;최신 빌드 URL, 최신 성공 빌드 URL, 최신 실패 빌드 URL&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;두 번째로는 Build에 대한 정보를 받아오는 API 입니다. &lt;br /&gt;&lt;code&gt;${Jenkins_URL}/job/${job_name}/${build_id}/api/json&lt;/code&gt; 으로 요청할 수 있습니다. &lt;br /&gt;(상세 Build 화면에서 &lt;code&gt;/api/json&lt;/code&gt; 을 붙여서 확인해보실 수 있습니다.) &lt;br /&gt;마찬가지로 주요한 필드만 기술해보면 아래와 같습니다.&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;building&quot;: false,
  &quot;displayName&quot;: &quot;#3&quot;,
  &quot;duration&quot;: 34,
  &quot;estimatedDuration&quot;: 116,
  &quot;fullDisplayName&quot;: &quot;test-01 #3&quot;,
  &quot;id&quot;: &quot;3&quot;,
  &quot;keepLog&quot;: false,
  &quot;number&quot;: 3,
  &quot;result&quot;: &quot;SUCCESS&quot;,
  &quot;timestamp&quot;: 1616924008157,
  &quot;url&quot;: &quot;http://JENKINS_URL/job/test-01/3/&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Build API에서 눈여겨 볼 정보는 다음과 같습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;building
&lt;ul&gt;
&lt;li&gt;현재 수행중인지 여부&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;duration
&lt;ul&gt;
&lt;li&gt;수행된 시간&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;result
&lt;ul&gt;
&lt;li&gt;빌드 결과&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;timestamp
&lt;ul&gt;
&lt;li&gt;수행 시작 시간&lt;/li&gt;
&lt;li&gt;timestamp에 duration을 더해서 환산하면 수행 종료 시간이 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;(위 2개 API를 제외한 다른 API가 궁금하시면 Jenkins API wrapping 라이브러리인 &lt;a href=&quot;https://github.com/cdancy/jenkins-rest&quot;&gt;Jenkins-rest&lt;/a&gt;를 참고해보셔도 좋습니다!)&lt;/p&gt;
&lt;p&gt;마찬가지로 Jenkins API와 위 요구사항을 매칭해보니, 다음과 같은 문제점들이 보였습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Trigger Job은 다른 배치를 Trigger만 하고 종료되기 때문에, 정확한 수행시간 등을 모니터링할 수 없다.&lt;/li&gt;
&lt;li&gt;하나의 배치 당 2번의 API 조회가 필요하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Jenkins API를 사용하는 방법에서 가장 큰 문제는, 하나의 API만 가지고는 우리가 필요한 모든 정보를 얻을 수 없다는 것입니다. &lt;br /&gt;즉, Job API에서는 어디로 가야 Build 정보가 있는지 Build 번호와 URL만 던져줄 뿐이고, 그렇게 찾아간 Build API에서는 반대로 Job에 대한 상세 정보(활성화 여부 등)를 확인할 수 없습니다. &lt;br /&gt;이렇게 되면 하나의 배치 당 Job API 한번, Build API 한번, 총 2번 API를 찔러야 하고, 결국 모니터링하려는 수십 개의 배치의 2배 만큼 API 호출을 해야하는 상황이었습니다. &lt;br /&gt;아니 집사님... 친절하게 한번에 다 알려주시면 안됩니까...&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;07_jenkins_cute.png&quot; data-origin-width=&quot;484&quot; data-origin-height=&quot;502&quot; width=&quot;270&quot; height=&quot;NaN&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ntzZM/btq18EObhWi/KeO2iNcYfyoJzD8ImIzknK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ntzZM/btq18EObhWi/KeO2iNcYfyoJzD8ImIzknK/img.png&quot; data-alt=&quot;안알랴줌ㅋ&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ntzZM/btq18EObhWi/KeO2iNcYfyoJzD8ImIzknK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FntzZM%2Fbtq18EObhWi%2FKeO2iNcYfyoJzD8ImIzknK%2Fimg.png&quot; data-filename=&quot;07_jenkins_cute.png&quot; data-origin-width=&quot;484&quot; data-origin-height=&quot;502&quot; width=&quot;270&quot; height=&quot;NaN&quot; data-ke-mobilestyle=&quot;widthContent&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;/&gt;&lt;/span&gt;&lt;figcaption&gt;안알랴줌ㅋ&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;Spring Batch Metadata Tables와 Jenkins API 중 고민하다가, 어차피 활성화 여부는 API를 통해 얻어와야 하고, 정보를 조합하기에도 API 쪽이 더 낫다고 생각해 후자를 선택하게 되었습니다.&lt;br /&gt;장단점에 따라 두 방법을 섞어서 쓰기에는 개발 기간이나 복잡도 측면에서 적합하지 않다고 판단하였습니다.&lt;/p&gt;
&lt;p&gt;집사가 일을 대충하면 목마른 주인이 우물을 파야죠. 연장 가져오겠습니다. ⛏&lt;/p&gt;
&lt;h3&gt;해보자, 조회!&lt;/h3&gt;
&lt;p&gt;전체적인 배치 모니터링플로우는 다음과 같습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;운영자가 모니터링하고 싶은 배치를 어드민에 Job 이름/관리용 이름/작업주기/조회유무 정보와 함께 등록한다.&lt;/li&gt;
&lt;li&gt;등록할 때 Job 이름(batchId)을 기준으로 Jenkins API를 찔러서, 실제 운영되고 있는 배치인지 확인한다.&lt;/li&gt;
&lt;li&gt;배치가 존재한다면, monitoring_batch라는 별도의 모니터링용 배치 관리 테이블에 저장한다.&lt;/li&gt;
&lt;li&gt;조회 시에는 등록한 batchId와 작업주기를 기준으로 당일 조회 대상인 배치 리스트를 가져와 일괄 조회한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;먼저, 필요한 API 정보에 맞게 JobInfo와 BuildInfo를 정의했습니다.&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;@ToString
@Getter
@NoArgsConstructor
public class JobInfo {

    private String description;
    private String displayName;
    private String name;
    private boolean buildable;
    private String url;

    private List&amp;lt;BuildUrl&amp;gt; builds;

    private BuildUrl firstBuild;
    private BuildUrl lastBuild;
    private BuildUrl lastCompletedBuild;
    private BuildUrl lastFailedBuild;
    private BuildUrl lastStableBuild;
    private BuildUrl lastSuccessfulBuild;
    private BuildUrl lastUnstableBuild;
    private BuildUrl lastUnsuccessfulBuild;

    public boolean isLastBuildEmpty() {
        return lastBuild == null;
    }

    public Long getLastBuildNumber() {
        return isLastBuildEmpty() ? null : lastBuild.getNumber();
    }

}

@ToString
@Getter
@NoArgsConstructor
public class BuildUrl {

    private String url;
    private long number;

}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Build 조회 API URL에 Job 이름이 포함되어 있기 때문에 너무 당연해서 그런지 API에서 Job의 이름을 제공하고 있지 않아서, BuildInfo에서는 추후 활용도를 높이기 위해 조회 후 이미 가지고 있는 Job 이름 정보를 업데이트하기로 했습니다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;@ToString
@Getter
@NoArgsConstructor
public class BuildInfo {

    private String batchId; // 해당 빌드의 Job Name. API에서 제공하고 있지 않아 수동으로 update 한다.

    private String id;
    private long number;
    private String url;

    private boolean building;
    private String description;
    private String displayName;
    private String result;

    private long timestamp;
    private long duration;

    public void updateBatchId(String batchId) {
        this.batchId = batchId;
    }

    public LocalDateTime getBuildStartTime() {
        return new Timestamp(timestamp).toLocalDateTime();
    }

    public LocalDateTime getBuildEndTime() {
        return new Timestamp(timestamp + duration).toLocalDateTime();
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;정산시스템에서는 이미 Jenkins API를 통해 특정 배치를 단건 실행하는 JenkinsConnector라는 모듈이 있었기 때문에, 해당 모듈에 조회 로직을 추가하기로 했습니다. &lt;br /&gt;RestTemplate을 사용해서 Job의 존재 유무를 확인하는 메서드와 JobInfo, BuildInfo를 조회하는 메서드를 각각 작성했습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;젠킨스 API를 사용하기 위해서는 젠킨스 환경설정에서 사용자별 토큰을 발급하고, Request에 &lt;code&gt;사용자ID:토큰&lt;/code&gt; 의 형태로 인증 헤더를 보내면 됩니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// JenkinsConnector.java

public boolean existsJob(String batchId) {
    try {
        String requestUrl = createJobUrl(batchId); // Job 조회 API URL

        ResponseEntity&amp;lt;JobInfo&amp;gt; responseEntity = this.restTemplate
                .exchange(requestUrl, GET, new HttpEntity&amp;lt;&amp;gt;(&quot;&quot;, createAuthHeaders()), JobInfo.class);

        HttpStatus statusCode = responseEntity.getStatusCode();
        return statusCode.is2xxSuccessful();
    } catch (Exception e) {
        log.error(e.getMessage(), e);
        return false;
    }
}

public JobInfo getJobInfo(String batchId) {
    try {
        log.info(&quot;address: {}, batchId: {}&quot;, address, batchId);
        String requestUrl = createJobUrl(batchId); // Job 조회 API URL

        return this.restTemplate
                .exchange(requestUrl, GET, new HttpEntity&amp;lt;&amp;gt;(&quot;&quot;, createAuthHeaders()), JobInfo.class)
                .getBody();
    } catch (Exception e) {
        throw new JenkinsExecuteException(&quot;Job Info Fetching Exception! &quot;, e);
    }
}

public BuildInfo getBuildInfo(String batchId, long buildNumber) {
    try {
        log.info(&quot;address: {}, batchId: {}, buildNumber: {}&quot;, address, batchId, buildNumber);
        String requestUrl = createBuildUrl(batchId, buildNumber); // Build 조회 API URL

        BuildInfo buildInfo = this.restTemplate
                .exchange(requestUrl, GET, new HttpEntity&amp;lt;&amp;gt;(&quot;&quot;, createAuthHeaders()), BuildInfo.class)
                .getBody();
        if (buildInfo != null) {
            buildInfo.updateBatchId(batchId);
        }

        return buildInfo;
    } catch (Exception e) {
        throw new JenkinsExecuteException(&quot;Build Info Fetching Exception! &quot;, e);
    }
}

private HttpHeaders createAuthHeaders() {
    String credentials = username + &quot;:&quot; + token;
    String base64Credentials = Base64.getEncoder().encodeToString(credentials.getBytes());

    HttpHeaders httpHeaders = new HttpHeaders();
    httpHeaders.set(&quot;Authorization&quot;, &quot;Basic &quot; + base64Credentials);
    return httpHeaders;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그리고 서비스 로직에서 이 JenkinsConnector를 사용하여 배치 하나 당 Job 조회, Build 조회를 순차적으로 진행하도록 했습니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// MonitoringBatchService.java

public List&amp;lt;MonitoringBatchResponse&amp;gt; search(MonitoringBatchSearchRequest request) {
    LocalDate workingDate = request.getWorkingDate();
    List&amp;lt;MonitoringBatchResponse&amp;gt; monitoringBatchResponses = monitoringBatchServerRepository.searchByCondition(request);

    for (MonitoringBatchResponse monitoringBatchResponse : monitoringBatchResponses) {
        updateBuildInfo(monitoringBatchResponse, workingDate); // DB에서 조회한 배치 리스트를 반복문을 돌면서 하나씩 Job, Build 조회
    }

    // ...
}

private void updateBuildInfo(MonitoringBatchResponse monitoringBatchResponse, LocalDate workingDate) {
    String batchId = monitoringBatchResponse.getBatchId();

    try {
        JobInfo jobInfo = jenkinsConnector.getJobInfo(batchId); // Job 조회
        // ...

        BuildUrl lastBuild = jobInfo.getLastBuild(); // 최신 Build 번호 확인
        if (lastBuild != null) {
            BuildInfo buildInfo = jenkinsConnector.getBuildInfo(batchId, lastBuild.getNumber()); // Build 조회

            // ... 상태 업데이트
        }
    } catch (JenkinsExecuteException e) {
        log.error(&quot;Jenkins 에서 조회할 수 없는 배치 Job 입니다. batchId={}&quot;, batchId);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;음, 아주 조회가 잘 되는 것을 확인했습니다. 배포!  &lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2&gt;비동기로 로직 개선&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;08_completable_fetch.png&quot; data-origin-width=&quot;1604&quot; data-origin-height=&quot;1072&quot; width=&quot;645&quot; height=&quot;NaN&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/n7e4A/btq17Zyg9vA/6P8e7gVFKqOEzafpTc8Oj0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/n7e4A/btq17Zyg9vA/6P8e7gVFKqOEzafpTc8Oj0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/n7e4A/btq17Zyg9vA/6P8e7gVFKqOEzafpTc8Oj0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fn7e4A%2Fbtq17Zyg9vA%2F6P8e7gVFKqOEzafpTc8Oj0%2Fimg.png&quot; data-filename=&quot;08_completable_fetch.png&quot; data-origin-width=&quot;1604&quot; data-origin-height=&quot;1072&quot; width=&quot;645&quot; height=&quot;NaN&quot; data-ke-mobilestyle=&quot;widthContent&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;네, 지금까지 과거 회상 씬이었고요.&lt;br /&gt;이제 서문에서의 상황으로 돌아가서 이 무지막지한 API 호출을 손볼 차례입니다.&lt;/p&gt;
&lt;p&gt;사실 배치 모니터링은 대시보드의 여러가지 지표 중 지분이 작고 활용도가 적은 부분이라 뒤늦게 인지한 측면도 있었고, 개선 우선순위가 낮기도 했었습니다. &lt;s&gt;핑계&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;확인한 당시에는 등록한 배치 수도 50여 개 가까이 되면서 1회 진입 시 호출하는 API 횟수가 100회에 가까웠습니다.&lt;br /&gt;API 호출 100회를 동기식으로 조회하니 속도가 안나오는건 당연할수밖에요.&lt;/p&gt;
&lt;p&gt;그래서 비동기 방식으로 슥삭 개선해보기로 했습니다.&lt;br /&gt;비동기 요청을 처리하기 위해 Java8에 등장한 CompletableFuture를 사용하기로 했고, 그전까지 CompletableFuture를 제대로 써본 적이 없었기에 진행에 앞서 &lt;a href=&quot;https://wbluke.tistory.com/50&quot;&gt;개인 블로그&lt;/a&gt;에 정리도 해보았는데요.&lt;br /&gt;아래 나올 CompletableFuture의 API를 몇 개만 간단히 정리해보면 다음과 같습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;supplyAsync
&lt;ul&gt;
&lt;li&gt;Supplier 타입을 파라미터 콜백으로 받아서 비동기 요청을 수행하고 반환값을 리턴하는 메서드입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;join
&lt;ul&gt;
&lt;li&gt;위 supplyAsync는 콜백의 반환값을 CompletableFuture가 감싼 형태로 리턴을 하는데요. 받은 CompletableFuture가 끝나기를 기다리고 싶다면 join()으로 Blocking을 걸어서 비동기 작업이 끝나기를 기다릴 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;handleAsync
&lt;ul&gt;
&lt;li&gt;비동기 작업이 끝난 이후, 해당 결과물과 (혹시나 실패했다면) 예외를 파라미터로 받아 종합적으로 후속 작업을 지정할 수 있는 메서드입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;allOf
&lt;ul&gt;
&lt;li&gt;여러 CompletableFuture를 한번에 Blocking 할 때 사용하는 메서드입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;CompletableFuture까지 훑어봤으니, 이제 동기 로직을 비동기 로직으로 전환해보도록 하겠습니다!&lt;br /&gt;먼저 기존 어드민 시스템에서 비동기 작업을 담당하고 있던 스레드 풀(Executor)의 설정을 변경했습니다.&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;// AsyncConfig.java

public static final String ADMIN_DEFAULT_EXECUTOR_NAME = &quot;threadPoolTaskExecutor&quot;;
private static final int POOL_SIZE = 30;

@Bean(name = ADMIN_DEFAULT_EXECUTOR_NAME)
public Executor threadPoolTaskExecutor() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setCorePoolSize(POOL_SIZE);
    executor.setMaxPoolSize(POOL_SIZE);
    executor.setThreadNamePrefix(&quot;admin-default-async-&quot;);

    // ...
    return executor;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;다들 아시겠지만 위에 나온 threadPoolTaskExecutor의 속성들을 간단하게 정리해보자면 다음과 같습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;corePoolSize
&lt;ul&gt;
&lt;li&gt;스레드의 최소 유지 개수&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;maxPoolSize
&lt;ul&gt;
&lt;li&gt;스레드가 최대로 만들어질 수 있는 개수&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;threadNamePrefix
&lt;ul&gt;
&lt;li&gt;해당 스레드 풀 내의 스레드 이름 prefix&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;원래 어드민에서 사용하던 비동기 작업은 단건 메일 전송, 위의 JenkinsConnector를 이용한 단건 배치 수행 등의 단순 작업만 존재했기에 스레드 풀 설정이 default 값으로 되어 있었습니다.&lt;br /&gt;즉, executor의 corePoolSize는 1, maxPoolSize는 Integer.MAX_VALUE 였습니다.&lt;br /&gt;이를 배치 조회를 위한 다수의 API 호출을 위해 corePoolSize, maxPoolSize를 30개로 상향 조정하였습니다.&lt;/p&gt;
&lt;p&gt;다음으로는 젠킨스의 조회를 담당하는 객체를 두고, CompletableFuture를 사용하여 비동기 조회 방식으로 로직을 구성하였습니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// BatchJobInfoFetcher.java

private final Executor threadPoolTaskExecutor;

public List&amp;lt;BatchJobItem&amp;gt; fetch(List&amp;lt;String&amp;gt; batchIds) {
    List&amp;lt;CompletableFuture&amp;lt;BatchJobItem&amp;gt;&amp;gt; batchJobItemFutures = batchIds.stream()
            .map(this::fetchBatchJobItemFuture)
            .collect(Collectors.toList());

    return CompletableFuture.allOf(batchJobItemFutures.toArray(new CompletableFuture[0])) // (1) 전체 Blocking
            .thenApply(Void -&amp;gt; batchJobItemFutures.stream()
                    .map(CompletableFuture::join)
                    .filter(Objects::nonNull)
                    .collect(Collectors.toList()))
            .join();
}

// 하나의 스레드에서 JobInfo 조회가 끝나면, 다른 스레드가 BuildInfo 조회 작업을 진행할 수 있도록 handleAsync()를 사용
private CompletableFuture&amp;lt;BatchJobItem&amp;gt; fetchBatchJobItemFuture(String batchId) {
    return getJobInfoAsync(batchId) // (2) 비동기로 Job 조회
            .handleAsync((jobInfo, jobInfoException) -&amp;gt; { // (3) Job 조회 후 Build 조회
                Long lastBuildNumber = jobInfo.getLastBuildNumber();
                // ... 검증

                return fetchBatchJobItem(jobInfo, lastBuildNumber);
            }, threadPoolTaskExecutor);
}

private CompletableFuture&amp;lt;JobInfo&amp;gt; getJobInfoAsync(String batchId) {
    return CompletableFuture.supplyAsync(() -&amp;gt; jenkinsConnector.getJobInfo(batchId), threadPoolTaskExecutor)
            .exceptionally(e -&amp;gt; {
                logErrorWithBatchId(batchId);
                return null;
            });
}

private BatchJobItem fetchBatchJobItem(JobInfo jobInfo, Long lastBuildNumber) {
    CompletableFuture&amp;lt;BatchJobItem&amp;gt; batchJobItemFuture = getBuildInfoAsync(jobInfo.getName(), lastBuildNumber) // (4) 비동기로 Build 조회
            .handleAsync((buildInfo, buildInfoException) -&amp;gt; { // (5) Build 조회 후 가공
                // ... 검증

                return new BatchJobItem(jobInfo, buildInfo);
            }, threadPoolTaskExecutor);

    return CompletableFuture.allOf(batchJobItemFuture) // Blocking
            .thenApply(Void -&amp;gt; batchJobItemFuture.join())
            .join();
}

private CompletableFuture&amp;lt;BuildInfo&amp;gt; getBuildInfoAsync(String batchId, long buildNumber) {
    return CompletableFuture.supplyAsync(() -&amp;gt; jenkinsConnector.getBuildInfo(batchId, buildNumber), threadPoolTaskExecutor)
            .exceptionally(e -&amp;gt; {
                logErrorWithBatchId(batchId);
                return null;
            });
}

private void logErrorWithBatchId(String batchId) { // (6)
    log.error(&quot;Jenkins에서 조회할 수 없는 배치 Job입니다. batchId={}&quot;, batchId); // log.error로 슬랙 알람 발송
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;코드가 조금 복잡할 수는 있는데요, 크게는 다음과 같은 흐름입니다. (주석 번호 참조)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;(1) 모든 비동기 요청은 결국 각 요청의 결과값이 모두 다 도착해야 화면 응답을 내려줄 수 있기 때문에, 조회가 끝나고 최종적으로는 CompletableFuture.allOf()로 Blocking을 걸어서 모든 응답이 끝났음을 보장해 줬습니다.&lt;/li&gt;
&lt;li&gt;(2) 먼저 조회해야 하는 batchId 리스트를 보고 순차적으로 Job을 조회하는 비동기 요청을 보냅니다.
&lt;ul&gt;
&lt;li&gt;Supplier 타입을 파라미터로 받는 CompletableFuture.supplyAsync()를 사용했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;(3) Job을 비동기로 조회한 뒤 Build를 조회하는 후속작업을 지정합니다.&lt;/li&gt;
&lt;li&gt;(4) Job 조회에서 얻은 최신 buildId를 사용해 Build를 조회합니다.&lt;/li&gt;
&lt;li&gt;(5) (3)에서와 동일하게 Build를 비동기로 조회한 뒤 Job과 Build를 가공하는 후속작업을 지정합니다.&lt;/li&gt;
&lt;li&gt;(6) 만약 Job이나 Build 조회 시 예외가 발생하면, &lt;code&gt;log.error&lt;/code&gt; (Slf4j)를 남기고 null을 반환하도록 했습니다.
&lt;ul&gt;
&lt;li&gt;정산시스템은 log.error가 발생하면 ELK에서 감지 후 슬랙에 에러 알람을 보내도록 되어있습니다.&lt;/li&gt;
&lt;li&gt;null 반환은 기본적으로 안티 패턴이지만, private 메서드로 내부에서만 사용하기 때문에 에러 시 null을 반환하도록 하고, public 메서드에서 필터링하는 용도로 사용했습니다. 한 건의 조회를 실패하더라도 나머지 정상적인 조회 결과는 보여줘야 하기 때문입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 로직에서 눈여겨봐야 하는 부분은, &lt;b&gt;(2) ~ (5)의 과정에서 모두 같은 스레드 풀(threadPoolTaskExecutor)을 사용하도록 했다&lt;/b&gt;는 점입니다.&lt;br /&gt;이렇게 개선을 마치고, 베타 서버에서 조회 속도의 드라마틱한 향상을 체감한 뒤 운영 배포를 진행했습니다!  &lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2&gt;1차 배포 후&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;09_monitoring_batch_failed.png&quot; data-origin-width=&quot;1018&quot; data-origin-height=&quot;678&quot; width=&quot;510&quot; height=&quot;NaN&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NRGAK/btq14ZlI0ZZ/jpfzUcWsmwLU0GgQlY0glK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NRGAK/btq14ZlI0ZZ/jpfzUcWsmwLU0GgQlY0glK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NRGAK/btq14ZlI0ZZ/jpfzUcWsmwLU0GgQlY0glK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNRGAK%2Fbtq14ZlI0ZZ%2FjpfzUcWsmwLU0GgQlY0glK%2Fimg.png&quot; data-filename=&quot;09_monitoring_batch_failed.png&quot; data-origin-width=&quot;1018&quot; data-origin-height=&quot;678&quot; width=&quot;510&quot; height=&quot;NaN&quot; data-ke-mobilestyle=&quot;widthContent&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;10_noooooo.png&quot; data-origin-width=&quot;1028&quot; data-origin-height=&quot;588&quot; width=&quot;505&quot; height=&quot;NaN&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Il5nf/btq16i6o6Il/5j0zRGp8NAfkbRr1O1us5k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Il5nf/btq16i6o6Il/5j0zRGp8NAfkbRr1O1us5k/img.png&quot; data-alt=&quot;아..안돼!&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Il5nf/btq16i6o6Il/5j0zRGp8NAfkbRr1O1us5k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIl5nf%2Fbtq16i6o6Il%2F5j0zRGp8NAfkbRr1O1us5k%2Fimg.png&quot; data-filename=&quot;10_noooooo.png&quot; data-origin-width=&quot;1028&quot; data-origin-height=&quot;588&quot; width=&quot;505&quot; height=&quot;NaN&quot; data-ke-mobilestyle=&quot;widthContent&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;/&gt;&lt;/span&gt;&lt;figcaption&gt;아..안돼!&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;장애가 났습니다.&lt;br /&gt;배포 직후에는 화면에 반응이 없길래 '왜 속도가 안나오지?' 했는데, 알고보니 아예 동작을 하지 않았던 것이었습니다.&lt;br /&gt;더 큰 문제는, 단순히 배치 모니터링 지표만 안나오면 그나마 괜찮은데 &lt;b&gt;어드민의 다른 기능까지 같이 동작하지 않는&lt;/b&gt; 이슈가 발생하기 시작했습니다.&lt;/p&gt;
&lt;p&gt;이유인즉슨, 스레드 풀에서 데드락이 발생했고, 같은 스레드 풀을 사용하고 있던 몇 없는 비동기 로직(위에서 언급한 메일 전송, 단건 배치 실행 등)까지도 같이 동작하지 않았던 것이었습니다.&lt;br /&gt;위 로직대로라면, &lt;code&gt;조회 배치 수 &amp;gt;= 스레드 수&lt;/code&gt; 의 조건에서는 데드락 현상이 발생합니다.&lt;br /&gt;구체적인 설명을 위해 &lt;s&gt;중복이지만 여러분의 스크롤은 소중하기 때문에&lt;/s&gt;&amp;nbsp;위의 코드를 다시 가져와 보겠습니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// BatchJobInfoFetcher.java

private final Executor threadPoolTaskExecutor;

public List&amp;lt;BatchJobItem&amp;gt; fetch(List&amp;lt;String&amp;gt; batchIds) {
    List&amp;lt;CompletableFuture&amp;lt;BatchJobItem&amp;gt;&amp;gt; batchJobItemFutures = batchIds.stream()
            .map(this::fetchBatchJobItemFuture)
            .collect(Collectors.toList());

    return CompletableFuture.allOf(batchJobItemFutures.toArray(new CompletableFuture[0])) // (1) 전체 Blocking
            .thenApply(Void -&amp;gt; batchJobItemFutures.stream()
                    .map(CompletableFuture::join)
                    .filter(Objects::nonNull)
                    .collect(Collectors.toList())
            )
            .join();
}

// 하나의 스레드에서 JobInfo 조회가 끝나면, 다른 스레드가 BuildInfo 조회 작업을 진행할 수 있도록 handleAsync()를 사용
private CompletableFuture&amp;lt;BatchJobItem&amp;gt; fetchBatchJobItemFuture(String batchId) {
    return getJobInfoAsync(batchId) // (2) 비동기로 Job 조회
            .handleAsync((jobInfo, jobInfoException) -&amp;gt; { // (3) Job 조회 후 Build 조회
                Long lastBuildNumber = jobInfo.getLastBuildNumber();
                // ... 검증

                return fetchBatchJobItem(jobInfo, lastBuildNumber);
            }, threadPoolTaskExecutor);
}

private CompletableFuture&amp;lt;JobInfo&amp;gt; getJobInfoAsync(String batchId) {
    return CompletableFuture.supplyAsync(() -&amp;gt; jenkinsConnector.getJobInfo(batchId), threadPoolTaskExecutor)
            .exceptionally(e -&amp;gt; {
                logErrorWithBatchId(batchId);
                return null;
            });
}

private BatchJobItem fetchBatchJobItem(JobInfo jobInfo, Long lastBuildNumber) {
    CompletableFuture&amp;lt;BatchJobItem&amp;gt; batchJobItemFuture = getBuildInfoAsync(jobInfo.getName(), lastBuildNumber) // (4) 비동기로 Build 조회
            .handleAsync((buildInfo, buildInfoException) -&amp;gt; { // (5) Build 조회 후 가공
                // ... 검증

                return new BatchJobItem(jobInfo, buildInfo);
            }, threadPoolTaskExecutor);

    return CompletableFuture.allOf(batchJobItemFuture) // Blocking
            .thenApply(Void -&amp;gt; batchJobItemFuture.join())
            .join();
}

private CompletableFuture&amp;lt;BuildInfo&amp;gt; getBuildInfoAsync(String batchId, long buildNumber) {
    return CompletableFuture.supplyAsync(() -&amp;gt; jenkinsConnector.getBuildInfo(batchId, buildNumber), threadPoolTaskExecutor)
            .exceptionally(e -&amp;gt; {
                logErrorWithBatchId(batchId);
                return null;
            });
}

private void logErrorWithBatchId(String batchId) { // (6)
    log.error(&quot;Jenkins에서 조회할 수 없는 배치 Job입니다. batchId={}&quot;, batchId); // log.error로 슬랙 알람 발송
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;예를 들어 조회할 배치 2개, 스레드 풀의 스레드 수를 2개라고 가정해 보겠습니다. &lt;br /&gt;2개의 스레드가 2개의 배치를 조회 및 가공하는 (2) ~ (3) 번 과정을 수행합니다. &lt;br /&gt;문제는 fetchBatchJobItem() 이라는 (4) ~ (5)의 과정이 (3)의 과정에 포함되는 작업이기 때문에, (2)번 과정에 모든 스레드가 물려있는 상황에서 새로운 (4) ~ (5)번 작업을 위한 스레드를 요청하게 되고, 이는 곧 각자의 스레드가 서로가 끝나기만을 기다리는 데드락 현상으로 이어집니다.&lt;/p&gt;
&lt;p&gt;당시 운영에서 등록해 조회하고 있던 배치는 47개 정도여서, 해당 이슈는 &lt;b&gt;핫픽스 배포로 스레드 풀의 개수를 초기 설정 30개에서 50개로 상향 조정하여 임시 조치&lt;/b&gt;하는 방식으로 해소하였습니다.&lt;/p&gt;
&lt;p&gt;이후 정확한 원인 파악을 진행하고, 두 가지 후속조치를 진행하였는데요.&lt;br /&gt;첫 번째는 &lt;b&gt;어드민의 기존 기능들이 사용하는 스레드 풀과 대시보드 전용 스레드 풀을 분리&lt;/b&gt;하였습니다.&lt;br /&gt;후에 대시보드에서 또 다른 스레드 이슈가 발생하더라도 다른 기능들은 영향을 받지 않게끔 하기 위해서입니다.&lt;/p&gt;
&lt;p&gt;두 번째는 &lt;b&gt;(2), (4)번 과정의 네트워크 조회 로직만 대시보드 전용 스레드 풀을 사용&lt;/b&gt;하게 하고, (3), (5)번 과정의 애플리케이션 단에서의 단순 가공 로직은 기본 제공되는 Tomcat의 NIO 스레드 풀을 자연스럽게 이용할 수 있도록 &lt;b&gt;특별한 스레드 풀 지정을 하지 않았습니다.&lt;/b&gt;&lt;br /&gt;(지금 생각해보면 스레드 풀 지정을 하지 않는 것보다, 또 다른 스레드 풀을 두고 별도로 지정하는 것이 더 좋았을 것 같습니다. 톰캣의 스레드 풀은 스레드 생성 제한이 없어서 자칫하면 위험할 수 있기 때문입니다.)&lt;br /&gt;이렇게 하면 대시보드 스레드 풀의 스레드들이 정확히 네트워크 리소스에만 투입되고, 서로의 작업 간에 pending 되는 일이 없게 됩니다.&lt;br /&gt;위 후속 조치에 따라 2차 개선한 코드는 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;// AsyncConfig.java

public static final String ADMIN_DEFAULT_EXECUTOR_NAME = &quot;adminExecutor&quot;;
public static final String MONITORING_BATCH_EXECUTOR_NAME = &quot;monitoringBatchExecutor&quot;;
private static final int MONITORING_BATCH_THREAD_POOL_SIZE = 10;

@Bean
@Qualifier(ADMIN_DEFAULT_EXECUTOR_NAME)
public Executor adminExecutor() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setThreadNamePrefix(&quot;admin-async-&quot;);

    // ...
    return executor;
}

@Bean
@Qualifier(MONITORING_BATCH_EXECUTOR_NAME)
public Executor monitoringBatchExecutor() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setCorePoolSize(MONITORING_BATCH_THREAD_POOL_SIZE);
    executor.setMaxPoolSize(MONITORING_BATCH_THREAD_POOL_SIZE);
    executor.setThreadNamePrefix(&quot;monitoring-async-&quot;);

    // ...
    return executor;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;AsyncConfig.java에서는 스레드 풀을 2개로 분리하고, 대시보드 스레드 풀의 크기를 10개로 조정하였습니다. (필요 이상으로 스레드가 많을 필요도 없기 때문에 운영 상황을 보면서 조절하는 것이 좋습니다.)&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// BatchJobInfoFetcher.java

// ...

private CompletableFuture&amp;lt;BatchJobItem&amp;gt; fetchBatchJobItemFuture(String batchId) {
    return getJobInfoAsync(batchId)
            .handleAsync((jobInfo, jobInfoException) -&amp;gt; { // (3) Job 조회 후 Build 조회
                Long lastBuildNumber = jobInfo.getLastBuildNumber();
                // ... 검증

                return fetchBatchJobItem(jobInfo, lastBuildNumber);
            }); // 별도의 스레드 풀 지정하지 않음
}

private CompletableFuture&amp;lt;JobInfo&amp;gt; getJobInfoAsync(String batchId) {
    return CompletableFuture.supplyAsync(() -&amp;gt; jenkinsConnector.getJobInfo(batchId), threadPoolTaskExecutor) // 기존처럼 스레드 풀 지정
            .exceptionally(e -&amp;gt; {
                logErrorWithBatchId(batchId);
                return null;
            });
}

private BatchJobItem fetchBatchJobItem(JobInfo jobInfo, Long lastBuildNumber) {
    CompletableFuture&amp;lt;BatchJobItem&amp;gt; batchJobItemFuture = getBuildInfoAsync(jobInfo.getName(), lastBuildNumber)
            .handleAsync((buildInfo, buildInfoException) -&amp;gt; { // (5) Build 조회 후 가공
                // ... 검증

                return new BatchJobItem(jobInfo, buildInfo);
            }); // 별도의 스레드 풀 지정하지 않음

    return CompletableFuture.allOf(batchJobItemFuture) // Blocking
            .thenApply(Void -&amp;gt; batchJobItemFuture.join())
            .join();
}

private CompletableFuture&amp;lt;BuildInfo&amp;gt; getBuildInfoAsync(String batchId, long buildNumber) {
    return CompletableFuture.supplyAsync(() -&amp;gt; jenkinsConnector.getBuildInfo(batchId, buildNumber), threadPoolTaskExecutor) // 기존처럼 스레드 풀 지정
            .exceptionally(e -&amp;gt; {
                logErrorWithBatchId(batchId);
                return null;
            });
}

// ...&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;젠킨스 로직에서는 (3), (5)번 과정에서 대시보드 스레드 풀을 지정하던 부분을 제거하였습니다. &lt;br /&gt;다음 로그를 보시면, 빨간색 박스의 비동기 로직을 요청하는 메인 스레드는 톰캣의 스레드 풀(NIO)을 사용하고 있는 것을 볼 수 있고, 파란색 박스의 실제 Jenkins API 요청은 직접 지정한 스레드 풀(monitoring-async)을 사용하는 것을 확인할 수 있습니다. &lt;br /&gt;메인 스트림 스레드와 API 요청 스레드가 분리되었기 때문에 데드락이 걸리지 않게 된 것이죠.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;11_log_batch_job_info_fetcher.png&quot; data-origin-width=&quot;2059&quot; data-origin-height=&quot;783&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QFRxa/btq13y2OeIo/f7HEVPx8mvKjNsK46IKIi1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QFRxa/btq13y2OeIo/f7HEVPx8mvKjNsK46IKIi1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QFRxa/btq13y2OeIo/f7HEVPx8mvKjNsK46IKIi1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQFRxa%2Fbtq13y2OeIo%2Ff7HEVPx8mvKjNsK46IKIi1%2Fimg.png&quot; data-filename=&quot;11_log_batch_job_info_fetcher.png&quot; data-origin-width=&quot;2059&quot; data-origin-height=&quot;783&quot; data-ke-mobilestyle=&quot;widthContent&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;여러 케이스를 테스트하면서 안전성을 두번 세번 확인 후, 다시 2차 배포를 진행했습니다!  &lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2&gt;2차 배포 후&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;12_async_is_comfortable.png&quot; data-origin-width=&quot;1282&quot; data-origin-height=&quot;960&quot; width=&quot;524&quot; height=&quot;NaN&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bK90yp/btq14ZTyQvp/rk5oMcrpB9opIAdCoFnJBk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bK90yp/btq14ZTyQvp/rk5oMcrpB9opIAdCoFnJBk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bK90yp/btq14ZTyQvp/rk5oMcrpB9opIAdCoFnJBk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbK90yp%2Fbtq14ZTyQvp%2Frk5oMcrpB9opIAdCoFnJBk%2Fimg.png&quot; data-filename=&quot;12_async_is_comfortable.png&quot; data-origin-width=&quot;1282&quot; data-origin-height=&quot;960&quot; width=&quot;524&quot; height=&quot;NaN&quot; data-ke-mobilestyle=&quot;widthContent&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;추가 배포 후 다행히 10개의 스레드로도 기능은 잘 동작했고, 조회 속도도 기대한만큼 나와주었습니다.&lt;br /&gt;기존 &lt;b&gt;90초 걸리던 로직이 2초 이내로 개선&lt;/b&gt;이 되어 무려 &lt;code&gt;44배&lt;/code&gt;의 성능 개선율을 보여주었습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2&gt;추가 개선 예정 + Outro&lt;/h2&gt;
&lt;p&gt;개발 당시에는 비동기 호출 구조에만 정신이 팔려서 인지하지 못하고 있었는데, 이번에 글을 다시 작성하면서 정리하다보니 위 내용은 다음과 같이 개선 가능하다는 사실을 깨달았습니다.&lt;/p&gt;
&lt;p&gt;현재는 하나의 배치를 기준으로 보면 Job 조회 &amp;rarr; 최신 Build Id 얻음 &amp;rarr; Build 조회 로 &lt;b&gt;항상 Job을 조회한 후 Build를 조회하도록&lt;/b&gt; 순차적으로 진행하고 있는데요.&lt;br /&gt;꼭 Job 정보를 찔러서 최신 Build Id를 알아낸 뒤에 해당 Id 값을 사용해 조회하지 않아도, Jenkins API URL에 &lt;code&gt;lastBuild&lt;/code&gt;, &lt;code&gt;lastCompletedBuild&lt;/code&gt; 등과 같이 고정된 문자열을 사용해서 최신 Build에 대한 조회가 가능하다고 합니다. &lt;s&gt;이럴수가?&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;이렇게 되면 &lt;code&gt;${Jenkins_URL}/job/${job_name}/lastBuild/api/json&lt;/code&gt; 로 특정 Job에 대한 최신 Build 조회가 가능하니, 굳이 Build를 조회하기 전에 Job을 조회하지 않아도 되고, 같은 레벨에서 Job과 Build를 동시에 조회하고 조합하는 구조로 개선할 수 있겠다는 생각이 들었습니다.&lt;br /&gt;즉, 지금은 Job 50개를 먼저 다 조회한 후, Blocking을 걸어 작업 완료가 보장되면 그 다음 순서로 Build 50개를 조회하는데, 위 구조대로라면 총 100개의 요청을 &lt;b&gt;순서 상관없이 동시 요청&lt;/b&gt;하고, 조회 완료 후에는 Map 등으로 가공 로직을 거쳐 원하는 형태의 Job + Build 응답값을 구성할 수 있겠다고 판단을 했습니다.&lt;br /&gt;요 3차 개선 구조는 조만간 시간이 나면 바로 개선을 진행해볼 예정입니다. 조회 시간도 기대만큼 1초 대로 좀 더 단축이 되면 좋겠네요.&lt;/p&gt;
&lt;p&gt;만들고 개선하고 장애내고 다시 개선하는 일련의 과정을 거치면서, 비교적 친숙하지 않았던 비동기 로직의 맛을 슬쩍 볼 수 있어서 &lt;s&gt;다 끝나고 나서는&lt;/s&gt;&amp;nbsp;즐거웠습니다. &lt;br /&gt;무엇보다 미리 데드락 상황을 로컬에서 충분히 재현하고 예측할 수 있었는데 그러지 못한 점이 가장 아쉬웠고요. 이런 경험을 통해, 또 몇 달 시간이 지났지만 글로 한번 더 정리하면서, 조금 더 &lt;a href=&quot;https://woowabros.github.io/experience/2020/03/02/pilot-project-wbluke.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Real-정산지기&lt;/a&gt;를 향해 한걸음 더 나아갔다고 되돌아볼 수 있는 소중한 경험이었습니다.&lt;/p&gt;
&lt;p&gt;긴 글 읽어주셔서 감사합니다-!  &lt;/p&gt;</description>
      <category>jenkins</category>
      <category>async</category>
      <category>jenkins</category>
      <author>wbluke</author>
      <guid isPermaLink="true">https://wbluke.tistory.com/61</guid>
      <comments>https://wbluke.tistory.com/61#entry61comment</comments>
      <pubDate>Wed, 7 Apr 2021 20:33:28 +0900</pubDate>
    </item>
  </channel>
</rss>