<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>DOES-LOG</title>
    <link>https://does-log.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Sun, 12 Jul 2026 20:40:48 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>더즈</managingEditor>
    <image>
      <title>DOES-LOG</title>
      <url>https://tistory1.daumcdn.net/tistory/5153881/attach/574e31e657eb4288a3517d202f3979c8</url>
      <link>https://does-log.tistory.com</link>
    </image>
    <item>
      <title>소프트웨어와 복잡성 관리</title>
      <link>https://does-log.tistory.com/64</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;도입&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;8월 13일 당근마켓의 박용권님의 세미나를 참여했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세미나의 제목은 &amp;lsquo;모놀리스 시스템 분할과 정복&amp;rsquo;이었고 복잡하고 거대한 시스템을 MSA가 아닌 modular monolith 아키텍처로 푸는 일련의 과정을 들을 수 있었다.&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;modular monolith에 대해서는 이번 포스팅의 핵심 내용은 아니다. 관련해서는 용권님의 또 다른 발표 영상이 많이 있으니 보면 이해가 잘될 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=SrQeIz3gXZg&quot;&gt;https://www.youtube.com/watch?v=SrQeIz3gXZg&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://youtu.be/uTSuVFyv81w?si=nFItCijVrAL4mvW0&quot;&gt;https://youtu.be/uTSuVFyv81w?si=nFItCijVrAL4mvW0&lt;/a&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;한 문장으로 설명한다면 모듈형 모놀리스 아키텍처란 전통적인 모놀리스 아키텍처의 단순성과 모듈화의 이점을 결합한 아키텍처 스타일이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;modular monolith 세미나 후기인데 modular monolith가 핵심이 아니라면 무엇이냐고 생각할 수도 있지만 이번 포스팅에서는 소프트웨어와 복잡성에 대해 얘기해보고자 한다. &lt;s&gt;(의식의 흐름 주의)&lt;/s&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;모놀리스는 지옥인가?&lt;/h2&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;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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;애플리케이션이 커지면 IDE 속도도 느리다.&lt;/li&gt;
&lt;li&gt;빌드도 느리다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;배포가 험난하다.&lt;/li&gt;
&lt;li&gt;확장이 어렵다.&lt;/li&gt;
&lt;li&gt;&amp;hellip; 등등&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 많은 대규모 시스템들이 모놀리스로 시스템을 운영하고 있는 것도 사실이다. (MAU 몇 백만의 어떤 주황색 서비스도 모놀리스로 운영되고 있다고 한다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시스템이 복잡한 건 결국 응집과 결합의 문제이지 MSA이냐 아니냐가 아니라는 것이다. MSA도 얼마든지 복잡하게 얽힌 지옥으로 만들 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;msa와 modular monolith의 공통점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;msa와 모놀리스는 대척점에 있는 것처럼 보이지만 용권님이 설명해주신 modular monolith와 msa는 어떤 공통점이 보였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바로 &amp;lsquo;개별 컴포넌트는 단순하지만 컴포넌트 간 통합이 복잡하다&amp;rsquo;는 것이다.&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;MSA의 경우 하나하나의 서비스들은 독립적으로 운영, 배포되고 분리된 하나의 관심사에만 집중해 개발을을 하게 된다. 즉 하나의 서비스만 따로 봤을 땐 복잡도가 낮고 단순하다.&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;하지만 각 서비스들의 통합은 복잡하다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;외부 API 통신&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;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 서비스를 통합하여 하나의 마이크로서비스 아키텍처를 구축하는 것은 복잡하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;modular monolith도 마찬가지다. 각 모듈 하나하나는 단순하고 자신의 관심사에만 집중하는 단순한 구조다. 하지만 각 모듈을 통합하거나, 경계를 긋는 부분은 복잡하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 용권님이 소개하신 모듈간 통합을 위한 중재자 패턴에서 중재자에 해당하는 부패 방지 계층은 익숙하지 않은 구조라 그런가 매우 복잡하게 느껴졌다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;ACL(Anti-corruption layer, 손상 방지 계층)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 컴포넌트가 다른 컴포넌트를 의존해야할 때 다른 컴포넌트를 직접 사용하거나 의존하면 컴포넌트 간 결합이 발생한다. 어떤 경우엔 의존조차 할 수 없는 경우도 있다. 컴포넌트가 마이크로서비스인 경우 다른 서비스와 사용하는 프로토콜이 다른 경우다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;616&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oDZyZ/btsJmHbV3xB/8bo4LsJNeVkYiPKNxJmZx1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oDZyZ/btsJmHbV3xB/8bo4LsJNeVkYiPKNxJmZx1/img.png&quot; data-alt=&quot;Anti-corruption layer&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oDZyZ/btsJmHbV3xB/8bo4LsJNeVkYiPKNxJmZx1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoDZyZ%2FbtsJmHbV3xB%2F8bo4LsJNeVkYiPKNxJmZx1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;804&quot; height=&quot;387&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;616&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Anti-corruption layer&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 때 두 컴포넌트 사이에 하나의 계층을 두고, 이 계층을 통해 통신하도록 만들 수 있는데 이 계층을 &amp;lsquo;손상 방지 계층&amp;rsquo;이라 부른다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;손상 방지 계층을 중간에 둠으로써 각 컴포넌트가 느슨하게 결합을 유지하며 통합될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서로 프로토콜이 호환되지 않는 마이크로서비스 간이 아니여도 모놀리스 시스템 안에서도 각 모듈 간 결합을 느슨하게 유지하고 싶을 때 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;손상 방지 계층을 이해하고 바로 든 생각은 &amp;lsquo;복잡하다&amp;rsquo;는 것이었다. 손상 방지 계층은 다른 컴포넌트들에 대해 강하게 결합 되어 있고 각 컴포넌트 간 느슨한 결합을 위해 본인은 복잡한 로직과 구성을 가진다.&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;결국 각 컴포넌트에 복잡도를 낮추기 위해 손상 방지 계층에 그 복잡도를 치워버린 것이 아닌가 하는 생각도 들었다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;소프트웨어 설계의 핵심은 복잡성을 어떻게 관리하는가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지 생각이 미치자 한 가지를 깨달았다. 소프트웨어 설계를 잘 한다는 것은 결국 복잡도를 없애는 것이 아니다. 복잡도를 잘 관리하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지는 막연하게 코드를 짜거나 설계할 때 어떻게 하면 복잡하지 않게, 변경하기 쉬운 구조를 만들까에 집중했었다. 설계를 잘 한다면 하나도 복잡하지 않은 아키텍처가 나올 수도 있다고 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kGCVE/btsJmLenjBy/IkhkoFpZ2F32k2y2dwqLnK/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kGCVE/btsJmLenjBy/IkhkoFpZ2F32k2y2dwqLnK/img.webp&quot; data-alt=&quot;ChatGPT가 그려준 이상적인 아키텍처&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kGCVE/btsJmLenjBy/IkhkoFpZ2F32k2y2dwqLnK/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkGCVE%2FbtsJmLenjBy%2FIkhkoFpZ2F32k2y2dwqLnK%2Fimg.webp&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;502&quot; height=&quot;502&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;ChatGPT가 그려준 이상적인 아키텍처&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 소프트웨어는 원래 복잡한 것이고, 그 복잡도를 0에 수렴시키는 것은 애초에 불가능하다. 중요한 것은 어디에 복잡도를 위치시키고 관리할 것인가인 것이다.&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;그리고 우리들은 지금까지 복잡도 관리를 계속 해오고 있었다. 바로 트레이드 오프다. 우리 개발자들은 항상 트레이드 오프를 거치며 개발을 해왔다. 하나를 얻으면 하나를 잃고, 우리는 더 감당할 수 있는 걸 잃기로 결정한다.&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;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;fun someMethod() {
  // ...
  // 20줄 이상의 복잡하고 긴 로직
  // ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주석으로 간단하게 표현했지만 이해하기 어렵고 유지보수 하기 힘든 코드일 것이다. someMehtod() 안에 모여 있는 복잡성을 다른 메서드로 이동시면 someMethod() 메서드는 단순해질 것이다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;fun someMethod() {
  val res = someSimpleMethod()
  
  processSomething(res)  
  
  finishSomething(res)
}

private fun someSimpleMethod(): String { ... }

private fun processSomething(arg: String) { ... }

private fun finishSomething(arg: String) { ... }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메서드 추출을 통해 한 메서드에 모여 있던 복잡성을 여러 메서드로 분산시킨 것이다. 복잡성은 분리시키면 단순해진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더 나아가 한 클래스가 여전히 너무 복잡하다면 private 메서드를 다른 클래스에게 맡겨 그 클래스를 의존하는 방법도 존재한다. 이는 다른 클래스에 복잡성을 이동시키는 방법이다.&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;각 클래스의 로직은 단순해졌을지 몰라도 클래스들이 협력하기 위해 의존하고 관계를 맺는 코드는 복잡해져 버린다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;fun main() {
  val service = SomeService(SomeClass1(SomeClass2(...)))
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 의존 코드의 복잡성을 어디로 치워버릴 수 있을까? 객체의 의존 관계를 설정하고 얻을 수 있는 Factory 클래스를 만들어 복잡성을 치워버릴 순 있을 것이다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;fun main() {
  val service: SomeService = factory.someService()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 실제로 저 factory 클래스 내부는 매우 복잡할 것이다. 비즈니스 로직을 담당하는 클래스들은 단순해졌지만 factory 클래스를 유지보수 하는 일은 만만치 않을 것 같다.&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;스프링 프레임워크를 사용하면 이러한 복잡성을 감출 수 있다. 스프링의 DI 관련 코드를 보면 매우 복잡하지만 우리들은 신경쓰지 않아도 된다. (물론 내부 로직을 공부하면 많은 도움이 되지만 말이다)&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;메서드 추출 방법이 복잡성을 잘게 분리해서 전체적으로 복잡도의 농도를 낮추는 작업이었다면 스프링을 사용하는 방법은 복잡도를 프레임워크에 위임하는 작업이다.&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;흔히 손상 방지 계층하면 큰 시스템 레벨에서의 중재 계층을 의미하는 듯한데 코드 레벨로 낮춰 본다면 프레임워크는 곧 손상 방지 계층의 역할을 수행하고 있다고 생각이 든다.&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;이렇듯 복잡성을 관리해주는 훌륭한 도구를 쓰는 것도 방법이다. 우리는 복잡성을 다른 곳에 치워두고 집중해야 하는 로직을 단순하게 유지할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;복잡성 관리 - msa &amp;amp; modular monolith&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커다란 시스템을 어떻게 관리할 것인가. 매우 어려운 문제이지만 근본적으로 생각할 부분은 메서드 추출과 같은 간단한 리팩터링을 할 때와 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lsquo;복잡한 것을 어디에 둘 것인가&amp;rsquo; 그리고 단순하게 유지하고 싶은 곳은 어디인가.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;MSA
&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;Modular Monolith
&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;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직은 두 아키텍처가 필요한 시스템을 경험해 보지 못해서 와닿지 않는다. 하지만 그 때가 왔을 때, 우리가 무엇 때문에 복잡하다고 느끼는지, 이 복잡성을 어디로 치워버려 필요한 부분은 단순하게 유지할 수 있을지 팀 내에서 고민해야할 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;치워버린다고 하니 내팽겨친다는 것처럼 들리지만 그 복잡한 곳이 손상 방지 계층임을, 의도된 복잡함이 있는 곳이라는 점을 인지하고 잘 관리해야할 것이다. 손상 방지 계층은 트레이드 오프의 결과에서 우리가 &amp;lsquo;잃은 것&amp;rsquo;이라고도 볼 수 있고, 이는 우리가 감당하기로 결정한 부분이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;복잡성의 패턴화&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;복잡성 관리의 핵심은 복잡성을 어딘가에 배치시키는 것 뿐 아니라 팀이 관리하고 이해할 수 있도록 만드는 것이 중요하다고 생각한다. 복잡성을 패턴화 시키면 어떨까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;복잡한 코드나 로직이더라도 구조화된 방법과 패턴을 사용한다면 이를 이해하는 시점에서 복잡성 조차 단순해질 수 있다고 생각한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 핵사고날 아키텍처에서의 포트/어댑터 패턴은 복잡하지만 아키텍처를 이해하고 나서 바라보는 여러 port들과 adapter, 그리고 그 구현체들은 익숙해지고 단순해질 수 있다.&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;잘 알려진 아키텍처나 컨벤션이 아니더라도, 팀 내에서 합의된 어떠한 손상 방지 계층에 대해 패턴화, 컨벤션화 시켜놓는다면 그건 더이상 복잡한 로직이 아니게 될 지도 모른다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세미나에서부터 꽤나 먼 길을 돌아온 것 같은 느낌이 든다. 세미나를 되돌아보며 소프트웨어에서 복잡성을 다루는 것은 그것을 제거하는 것이 아닌 효과적으로 관리하는 점이라는 것을 깨닫게 되었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Reference&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;8/13 박용권님의 세미나 &amp;lsquo;모놀리스 시스템 분할과 정복&amp;rsquo;&lt;/li&gt;
&lt;li&gt;마이크로서비스 패턴 1장 모놀리식 지옥에서 벗어나라&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=SrQeIz3gXZg&quot;&gt;https://www.youtube.com/watch?v=SrQeIz3gXZg&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://youtu.be/uTSuVFyv81w?si=nFItCijVrAL4mvW0&quot;&gt;https://youtu.be/uTSuVFyv81w?si=nFItCijVrAL4mvW0&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://medium.com/solutions-architecture-patterns/anti-corruption-layer-pattern-bd75e1f2be7f&quot;&gt;https://medium.com/solutions-architecture-patterns/anti-corruption-layer-pattern-bd75e1f2be7f&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>방법론</category>
      <author>더즈</author>
      <guid isPermaLink="true">https://does-log.tistory.com/64</guid>
      <comments>https://does-log.tistory.com/64#entry64comment</comments>
      <pubDate>Sun, 1 Sep 2024 22:54:51 +0900</pubDate>
    </item>
    <item>
      <title>TestContainer와 H2의 장점만 골라먹기</title>
      <link>https://does-log.tistory.com/63</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;H2 DB로 테스트 수행 시 문제점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링으로 웹개발을 하고 테스트를 작성하고 있다면 H2 데이터베이스를 많이 사용한다. H2는 인메모리 데이터베이스로 스프링부트에 내장되어 있어 별도의 DB 설정 없이 간편하게 이용할 수 있다.&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;깃허브에서 프로젝트를 클론한 사람 모두가 테스트를 아무런 환경 세팅 없이 쉽게 돌릴 수 있고 테스트 속도도 빠르기에 개발 초기 단계나 토이 프로젝트에서 많이 사용하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 운영 환경에서는 MySQL 같은 데이터베이스를 사용하기 때문에 운영 환경과 다른 환경에서 테스트를 한다는 불안감이 있다. 실제로 환경적인 측면에나 문법적인 측면에서 다른 부분이 있어 테스트는 돌아가더라도 운영 환경에서 장애가 날 수도 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Transactional(readOnly = true) 문제&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링에서 서비스 계층에 트랜잭션을 적용하기 위해 @Transactional을 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예전에 프로젝트를 진행하던 중 읽기 전용 메서드가 아니었는데 실수로 readOnly 속성을 true로 해서 문제가 생겼던 적이 있다. H2 DB를 사용하면 readOnly = true여도 데이터 변경을 막지 않기 때문에 테스트가 통과해서 문제가 없는 줄 알았다.&lt;/p&gt;
&lt;pre id=&quot;code_1705924545195&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class XXXService {&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 배포 후 운영 환경에서 장애가 발생했다. 에러 로그를 살펴보니 아래와 같았다.&lt;/p&gt;
&lt;pre id=&quot;code_1705924573216&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Caused by: java.sql.SQLException: Connection is read-only. Queries leading to data modification are not allowed&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL을 사용할 때는 read-only 속성이 걸리면 데이터를 변경하지 못하게 막고 있던 것이었다. 이 경험 이후로 H2에 대한 불신이 생겼고 이를 해결할 수 없을지 생각해보게 되었다.&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;이 문제 이외에도 문법적인 부분에서 차이가 있는 것도 있어 얼마 전 DB 스키마를 flyway로 마이그레이션 하던 도중 문법이 MySQL에서도 통하는 것이 맞나 싶은 마음에 일일이 확인하고 수정했던 적도 있었다. 운영 DB 환경에서의 테스트가 절실한 순간이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로컬에서 MySQL을 직접 설치하거나 도커를 이용해 MySQL을 구동하여 테스트를 진행할 수도 있지만 여간 번거로운 작업이 아니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;TestContainer란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 컨테이너는 Docker 컨테이너로 래핑된 실제 서비스와의 통합 테스트를 위한 쉽고 가벼운 API를 제공하는 테스트 라이브러리이다. 테스트 컨테이너를 사용하면 mock 서비스나 인메모리 서비스 없이도 프로덕션에서 사용하는 것과 동일한 유형의 서비스와 대화하는 테스트를 작성할 수 있다.&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;테스트 컨테이너를 사용하면 자동으로 도커를 시작하고 원하는 컨테이너 서비스를 구동시킬 수 있다. 운영 환경과 같은 서비스를 사용할 수 있고 테스트가 끝나면 컨테이너를 자동으로 제거해준다.&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;도커를 설치하고 켜두어야 한다는 부분은 있지만 나머지 작업은 테스트 컨테이너가 모두 맡아서 처리한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;적용 방법&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;build.gradle에 다음 의존성을 추가한다.&lt;/p&gt;
&lt;pre id=&quot;code_1705924651984&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;testImplementation &quot;org.testcontainers:testcontainers:1.19.3&quot;
testImplementation &quot;org.testcontainers:junit-jupiter:1.19.3&quot;
testImplementation &quot;org.testcontainers:mysql:1.19.3&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 application.yml에 아래와 같이 DB 설정을 작성하면 테스트 컨테이너로 MySQL을 연결하여 테스트를 진행할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1705924676613&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring:
  datasource:
    driver-class-name: org.testcontainers.jdbc.ContainerDatabaseDriver
    url: jdbc:tc:mysql:8.0:///test_container_test
    username: root
    password: password&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 설정을 적용하여 테스트를 돌리자 도커 컨테이너가 자동으로 실행되고 테스트를 MySQL DB와 연결하여 진행할 수 있었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;258&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjdkxO/btsDQ8LQHIa/KcINK5fKPszAqKkDCfkVN1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjdkxO/btsDQ8LQHIa/KcINK5fKPszAqKkDCfkVN1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjdkxO/btsDQ8LQHIa/KcINK5fKPszAqKkDCfkVN1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbjdkxO%2FbtsDQ8LQHIa%2FKcINK5fKPszAqKkDCfkVN1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;705&quot; height=&quot;91&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;258&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Transactional(readOnly = true)인 경우 데이터 변경을 했을 때 테스트가 실패하는 것도 확인할 수 있었다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;TestContainer 테스트의 문제점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 컨테이너는 정말 좋은 테스트 도구이지만 문제 아닌 문제가 크게 2가지가 있었다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;도커가 없다면 설치해야 하고 테스트 전 도커를 켜야 한다.&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 도커를 한 번만 설치하면 되고 또 개발을 시작하기 전에 켜두면 되긴 하지만 이 또한 번거로울 수 있다. 게다가 깃허브에서 우리 프로젝트를 클론 받은 사람들이 아무런 사전 작업 없이 우리 서비스를 시작해볼 수가 없고 추가적인 환경 세팅을 해야하는 점도 마음에 걸렸다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;테스트 속도가 느리다.&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제가 좀 치명적이었다. 현재 개발하고 있는 서비스는 아직 초기 단계라 테스트가 약 70개 정도 존재한다. 아래 그림을 보면 알 수 있듯이 테스트 컨테이너를 사용하면 테스트가 총 13초가 걸리게 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;325&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/djbUf0/btsDJiivvMA/74XaaXhDJpqwZESpZWp4lK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/djbUf0/btsDJiivvMA/74XaaXhDJpqwZESpZWp4lK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/djbUf0/btsDJiivvMA/74XaaXhDJpqwZESpZWp4lK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdjbUf0%2FbtsDJiivvMA%2F74XaaXhDJpqwZESpZWp4lK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2000&quot; height=&quot;325&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;325&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 H2를 사용하게 되면 아래처럼 3초가 걸려 테스트 속도가 4,5배나 빠르고 이는 테스트 개수가 많아질수록 더 치명적일거라 생각된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;356&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bzv7iX/btsDKyLGlXv/zzTEKrg9k39r3ZAu3BkHl0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bzv7iX/btsDKyLGlXv/zzTEKrg9k39r3ZAu3BkHl0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bzv7iX/btsDKyLGlXv/zzTEKrg9k39r3ZAu3BkHl0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbzv7iX%2FbtsDKyLGlXv%2FzzTEKrg9k39r3ZAu3BkHl0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2000&quot; height=&quot;356&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;356&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;H2, TestContainer 둘 다 사용하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론적으로 H2 DB와 테스트 컨테이너를 둘 다 사용하기로 결정했다. 이유는 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;평소 로컬에서 개발할 때는 H2 DB를 사용해서 다른 환경 세팅이 필요없도록 하고 빠른 테스트 속도를 누림&lt;/li&gt;
&lt;li&gt;운영 환경에 배포하기 전엔 MySQL 테스트가 필요하기 때문에 최소 1번 이상 테스트 컨테이너로 테스트할 수 있도록 실행 환경을 분리&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;application.yml은 원래 active profile이 개발할 때의 local 설정과 운영 환경에서 실제 DB에 연결하기 위한 prod 설정이 존재한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;application-local.yml&lt;/li&gt;
&lt;li&gt;application-prod.yml&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기에 테스트 컨테이너를 사용하는 환경인 설정 파일을 하나 더 추가한다. 그리고 평소에는 H2 테스트를 진행하다가 배포하기 전에 MySQL 테스트를 위해 active profile을 테스트 컨테이너로 변경하면 원할 때에만 테스트 컨테이너 환경의 테스트를 진행할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1705924869051&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# application.yml
spring:
  profiles:
    active: test-container # 기본으로 local로 설정되어 있지만 원할 때 변경&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Github Action에서 테스트 컨테이너 환경 CI&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 수동으로 환경 변수를 변경해서 테스트 컨테이너 환경으로 테스트하는 것도 번거롭다. 게다가 휴먼 에러로 MySQL 테스트를 하지 않고 배포해 버릴 수도 있다.&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;이를 방지하기 위해 pull request 시 Github Action에서 CI를 진행하면서 테스트 컨테이너를 사용하도록 구성하기로 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래처럼 테스트 컨테이너 환경으로 동작하는 application-ci.yml을 추가한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;1775&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/r7oqB/btsDRJkKKtL/jK5Rz6RNk81o5uL8MTenC1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/r7oqB/btsDRJkKKtL/jK5Rz6RNk81o5uL8MTenC1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/r7oqB/btsDRJkKKtL/jK5Rz6RNk81o5uL8MTenC1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fr7oqB%2FbtsDRJkKKtL%2FjK5Rz6RNk81o5uL8MTenC1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;292&quot; height=&quot;1775&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;1775&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 pull request 시 동작하는 Github Action 스크립트를 아래처럼 작성한다. 그럼 기능 개발이 끝나고 PR을 날리면 자동으로 테스트 컨테이너로 MySQL 테스트를 수행하며 빌드를 진행하게 된다. 빌드 실패 시 PR이 머지되는 것을 막는다면 MySQL 테스트 없이 운영 환경에 배포하는 실수를 완벽히 차단할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1705924935983&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;name: CI Build
on:
  pull_request:
    branches:
      - '**'

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: 저장소 Checkout
        uses: actions/checkout@v3

      - name: 자바 17 설정
        uses: actions/setup-java@v3
        with:
          java-version: '17'
          distribution: 'adopt'
      - name: 빌드
        run: SPRING_PROFILES_ACTIVE=ci ./gradlew build&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;H2와 테스트 컨테이너를 모두 사용하고, CI를 통해 테스트 컨테이너 환경의 테스트를 강제하면서 H2 DB와 테스트 컨테이너의 장점을 모두 가져갈 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 걱정되는 것은 지금까진 MySQL에서의 문법과 환경이 H2와 함께 맞물리는 것이 가능했기에 H2로도 테스트하고 MySQL로도 테스트 하는 것이 가능했다. 만약 MySQL에서만 가능하고 H2에서는 가능하지 않는 문법이나 환경을 사용해야할 때가 온다면 테스트 컨테이너만 사용해야할 수도 있다.&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;아래 링크는 작업한 PR 링크이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/hufscheer/spectator-server/pull/96&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/hufscheer/spectator-server/pull/96&lt;/a&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;참고&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://testcontainers.com/guides/introducing-testcontainers/&quot;&gt;https://testcontainers.com/guides/introducing-testcontainers/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://medium.com/@jojiapp/junit-spring-boot에서-testcontainer-사용-c352a3ee746e&quot;&gt;https://medium.com/@jojiapp/junit-spring-boot에서-testcontainer-사용-c352a3ee746e&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>스프링/테스트</category>
      <category>DB</category>
      <category>Spring</category>
      <category>Test</category>
      <category>TestContainer</category>
      <author>더즈</author>
      <guid isPermaLink="true">https://does-log.tistory.com/63</guid>
      <comments>https://does-log.tistory.com/63#entry63comment</comments>
      <pubDate>Mon, 22 Jan 2024 21:03:34 +0900</pubDate>
    </item>
    <item>
      <title>JPA와 외래키로 인한 데드락 해결기</title>
      <link>https://does-log.tistory.com/62</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트를 진행하던 중 배포를 한 뒤 에러가 발생해서 로그를 봤더니 데이터베이스에서 데드락이 발생하고 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따로 명시적인 락을 잡지 않았는데도 불구하고 데드락이 발생하니 처음엔 무슨 상황인지 이해할 수 없었다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;엔티티 구조&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/ldk980130/TIL/assets/78652144/c297c4d9-5553-4c22-9e19-ad61fceb8bb9&quot; width=&quot;315&quot; height=&quot;371&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트의 핵심 도메인은 &amp;lsquo;지원서&amp;rsquo;이다. 동아리나 기업에 지원할 때 자소서나 지원서를 작성하게 되는데 지원서 작성과 보관을 도와주는 서비스기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 &amp;lsquo;지원서&amp;rsquo;에는 여러 &amp;lsquo;문항&amp;rsquo;이 존재하기에 &amp;lsquo;지원 문항&amp;rsquo;과 그 &amp;lsquo;답변&amp;rsquo;을 하나로 묶은 &amp;lsquo;지원 항목&amp;rsquo;을 &amp;lsquo;지원서&amp;rsquo;와 일대다로 맺어주었다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class ApplicationForm {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = &quot;application_form_id&quot;)
    private Long id;

    // ...

    @Embedded
    private ApplicationItems applicationItems = new ApplicationItems();&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Embeddable
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class ApplicationItems {

    @OneToMany(mappedBy = &quot;applicationForm&quot;, cascade = {CascadeType.PERSIST, CascadeType.REMOVE})
    private List&amp;lt;ApplicationItem&amp;gt; values = new ArrayList&amp;lt;&amp;gt;();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA를 사용하면서 일대다 양방향 매핑을 맺어주었다. 그 이유는 영속성 전이를 사용해서 &lt;code&gt;ApplicationForm&lt;/code&gt; 내부의 일급 컬렉션인 &lt;code&gt;applicationItems&lt;/code&gt;에 &lt;code&gt;ApplicationItem&lt;/code&gt;을 추가하기만 하더라도 &lt;code&gt;insert&lt;/code&gt; 쿼리를 내보내기 위해서고 &lt;code&gt;ApplicationForm&lt;/code&gt;이 삭제되었을 때 &lt;code&gt;ApplicationItem&lt;/code&gt;도 모두 삭제해 주기 위해서다. &amp;lsquo;지원서&amp;rsquo;와 &amp;lsquo;지원 항목&amp;rsquo;이 비즈니스 로직 상 밀접한 관련이 있고 같은 생명 주기를 공유하고 있기 때문에 이와 같이 설계했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;데드락 발생 코드&lt;/h2&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public Long addItem(Long applicationFormId,
                    LoginMember loginMember,
                    ApplicationItemCreateRequest request) {
        Writer writer = new Writer(loginMember.id());
        ApplicationForm form = writerCheckedFormService.checkWriterAndGet(applicationFormId, writer);
        ApplicationItem item = form.addItem(
                new ApplicationQuestion(request.applicationQuestion()),
                new ApplicationAnswer(request.applicationAnswer())
        );
        applicationFormRepository.flush();
        return item.getId();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드는 &amp;lsquo;지원서&amp;rsquo;에 &amp;lsquo;지원 항목&amp;rsquo;을 추가하는 로직을 담당하는 코드다. 이 기능을 실행하면 &amp;lsquo;지원서&amp;rsquo; 하나에 &amp;lsquo;지원 항목&amp;rsquo;이 3개였다면 4개가 되도록 추가할 수 있는 것이다. 영속성 전이를 사용해서 구현했기에 &lt;code&gt;ApplicationItem&lt;/code&gt;에 대한 별도의 &lt;code&gt;save()&lt;/code&gt; 호출 없이 자바 컬렉션에 저장하는 것 만으로 데이터베이스에 영속시킬 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;flush()&lt;/code&gt;를 호출하는 이유는 영속성 전이로 저장되는 엔티티의 경우 그냥 &lt;code&gt;save()&lt;/code&gt;를 호출했을 때와 달리 PK를 바로 가져와주지 않기 때문이다. 트랜잭션 커밋 시점에 flush를 통해 영속성 전이에 의한 insert 쿼리가 발생하기에 &lt;code&gt;flush()&lt;/code&gt;를 명시적으로 호출해서 &amp;lsquo;지원 항목&amp;rsquo;의 PK를 반환하고 있다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;Hibernate: 
    insert 
    into
        application_item
        (application_item_id, application_answer, application_form_id, application_question, category_id) 
    values
        (default, ?, ?, ?, ?)
Hibernate: 
    update
        application_form 
    set
        last_modified_time=?,
        recruiter=?,
        semester=?,
        member_id=?,
        writing_state=?,
        years=? 
    where
        application_form_id=?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿼리는 위와 같이 발생한다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;영속성 전이에 의한 insert 쿼리 발생&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ApplicatinoForm&lt;/code&gt; 내부의 &lt;code&gt;ApplicatinoItem&lt;/code&gt; 컬렉션에 변경이 발생했으니 변경 감지가 발생해서 update 쿼리 발생&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필요없는 update 쿼리가 발생하는 것이 조금 마음에 걸렸지만 크게 신경쓰지는 않았다. 영속성 전이 기능을 통해 데이터베이스 종속적이지 않은 코드를 작성하는 것을 더 중요시 했었기 때문이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;데드락 로그 확인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lsquo;지원 항목 추가&amp;rsquo; 기능에서 데드락이 발생한 것인데 동시에 2개 이상의 &amp;lsquo;지원 항목&amp;rsquo;을 추가하게 되면 데드락이 발생하는 것을 확인했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 만약 한 번에 2개 이상의 지원 항목을 추가한다면 프론트에서 2개의 API 요청을 한 번에 날리게 된다. 이 동시 요청에서 데드락이 발생하는 것인데 데이터베이스에 접속해서 로그를 확인해보니 아래와 같았다.&lt;/p&gt;
&lt;pre class=&quot;vbscript&quot;&gt;&lt;code&gt;LATEST DETECTED DEADLOCK
------------------------
2023-05-19 14:10:34 22487951918848
*** (1) TRANSACTION:
TRANSACTION 11701, ACTIVE 0 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 5 lock struct(s), heap size 1128, 2 row lock(s), undo log entries 1
MySQL thread id 888, OS thread handle 22487844890368, query id 75644 10.0.0.157 root updating
update application_form set last_modified_time='2023-05-19 23:10:34.142917', recruiter='우아한테크코스 4기', semester='SECOND_HALF', member_id=1, writing_state='COMPLETE', years=2021 where application_form_id=3

*** (1) HOLDS THE LOCK(S):
RECORD LOCKS space id 19 page no 4 n bits 80 index PRIMARY of table `wannafly`.`application_form` trx id 11701 lock mode S locks rec but not gap
Record lock, heap no 6 PHYSICAL RECORD: n_fields 9; compact format; info bits 0
 0: len 8; hex 8000000000000003; asc         ;;
 1: len 6; hex 000000002da5; asc     - ;;
 2: len 7; hex 01000001960795; asc        ;;
 3: len 8; hex 8000000000000001; asc         ;;
 4: len 8; hex 434f4d504c455445; asc COMPLETE;;
 5: len 26; hex ec9ab0ec9584ed959ced858ced81acecbd94ec8aa42034eab8b0; asc                       4   ;;
 6: len 5; hex 99b0277217; asc   'r ;;
 7: len 4; hex 800007e5; asc     ;;
 8: len 11; hex 5345434f4e445f48414c46; asc SECOND_HALF;;

*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 19 page no 4 n bits 80 index PRIMARY of table `wannafly`.`application_form` trx id 11701 lock_mode X locks rec but not gap waiting
Record lock, heap no 6 PHYSICAL RECORD: n_fields 9; compact format; info bits 0
 0: len 8; hex 8000000000000003; asc         ;;
 1: len 6; hex 000000002da5; asc     - ;;
 2: len 7; hex 01000001960795; asc        ;;
 3: len 8; hex 8000000000000001; asc         ;;
 4: len 8; hex 434f4d504c455445; asc COMPLETE;;
 5: len 26; hex ec9ab0ec9584ed959ced858ced81acecbd94ec8aa42034eab8b0; asc                       4   ;;
 6: len 5; hex 99b0277217; asc   'r ;;
 7: len 4; hex 800007e5; asc     ;;
 8: len 11; hex 5345434f4e445f48414c46; asc SECOND_HALF;;

*** (2) TRANSACTION:
TRANSACTION 11699, ACTIVE 0 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 5 lock struct(s), heap size 1128, 2 row lock(s), undo log entries 1
MySQL thread id 886, OS thread handle 22488434579200, query id 75646 10.0.0.157 root updating
update application_form set last_modified_time='2023-05-19 23:10:34.135738', recruiter='우아한테크코스 4기', semester='SECOND_HALF', member_id=1, writing_state='COMPLETE', years=2021 where application_form_id=3

*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 19 page no 4 n bits 80 index PRIMARY of table `wannafly`.`application_form` trx id 11699 lock mode S locks rec but not gap
Record lock, heap no 6 PHYSICAL RECORD: n_fields 9; compact format; info bits 0
 0: len 8; hex 8000000000000003; asc         ;;
 1: len 6; hex 000000002da5; asc     - ;;
 2: len 7; hex 01000001960795; asc        ;;
 3: len 8; hex 8000000000000001; asc         ;;
 4: len 8; hex 434f4d504c455445; asc COMPLETE;;
 5: len 26; hex ec9ab0ec9584ed959ced858ced81acecbd94ec8aa42034eab8b0; asc                       4   ;;
 6: len 5; hex 99b0277217; asc   'r ;;
 7: len 4; hex 800007e5; asc     ;;
 8: len 11; hex 5345434f4e445f48414c46; asc SECOND_HALF;;

*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 19 page no 4 n bits 80 index PRIMARY of table `wannafly`.`application_form` trx id 11699 lock_mode X locks rec but not gap waiting
Record lock, heap no 6 PHYSICAL RECORD: n_fields 9; compact format; info bits 0
 0: len 8; hex 8000000000000003; asc         ;;
 1: len 6; hex 000000002da5; asc     - ;;
 2: len 7; hex 01000001960795; asc        ;;
 3: len 8; hex 8000000000000001; asc         ;;
 4: len 8; hex 434f4d504c455445; asc COMPLETE;;
 5: len 26; hex ec9ab0ec9584ed959ced858ced81acecbd94ec8aa42034eab8b0; asc                       4   ;;
 6: len 5; hex 99b0277217; asc   'r ;;
 7: len 4; hex 800007e5; asc     ;;
 8: len 11; hex 5345434f4e445f48414c46; asc SECOND_HALF;;

*** WE ROLL BACK TRANSACTION (2)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그가 복잡하지만 간단하게 정리하면 다음과 같다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;트랜잭션 11699번과 11701번이 동시에 &amp;lsquo;지원 항목 추가&amp;rsquo; 쿼리 실행&lt;/li&gt;
&lt;li&gt;두 트랜잭션 모두 &lt;code&gt;application_form&lt;/code&gt; 테이블에 S Lock을 획득&lt;/li&gt;
&lt;li&gt;두 트랜잭션 모두 &lt;code&gt;application_form&lt;/code&gt;에 대해 X Lock 획득 시도&lt;/li&gt;
&lt;li&gt;다른 트랜잭션에서 S Lock을 걸고 있기 때문에 X Lock을 획득하기 위해 대기&lt;/li&gt;
&lt;li&gt;서로 기다리게 되면서 데드락 발생&lt;/li&gt;
&lt;li&gt;트랜잭션 11699번을 롤백&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;데드락 원인 파악&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 S Lock과 X Lock이 무엇인지 이해하는 것이 필요하다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;shared lock&lt;/b&gt;은 트랜잭션이 row를 읽기 위해 거는 락
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여러 트랜잭션이 동시에 데이터를 읽어도 문제 없고 shared lock끼리는 동시에 접근 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;exclusive lock&lt;/b&gt;은 트랜잭션이 row를 update하거나 delete하기 위해 거는 락
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;exclusive lock을 동시에 걸 순 없고 다른 트랜잭션이 읽는 것조차 막는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더 쉽게 풀자면 S Lock은 한 트랜잭션이 어떤 테이블 row를 읽을 때 다른 트랜잭션에서 이 row을 변경하지 못하도록 막는 잠금이다. 변경만 막고 단순 select나 S Lock을 함께 거는 것도 막지 않는다. 반면 X Lock은 다른 트랜잭션의 단순 select까지는 허용하지만 그 어떤 잠금(S Lock도 포함)도 거는 것을 허용하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 데드락 로그에서 살펴 봤듯이 두 트랜잭션에서 같은 테이블 row에 대해 S Lock &amp;rarr; X Lock 순으로 잠금을 획득하려고 할 때 동시 접근이 발생하면 데드락이 발생할 수밖에 없는 것이다. 두 트랜잭션이 서로가 S Lock을 해제하기를 기다리며 X Lock을 걸려고 기다리고 있기 때문이다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;트랜잭션 1&lt;/th&gt;
&lt;th&gt;트랜잭션 2&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;S Lock 획득&lt;/td&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;S Lock 획득&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;X Lock 획득 시도, 대기&lt;/td&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;X Lock 획득 시도, 대기&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;데드락 탐지, 롤백&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;X Lock 획득&lt;/td&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;커밋&lt;/td&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;외래키 제약과 잠금 전파&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lsquo;지원 항목 추가&amp;rsquo; 기능의 쿼리가 S Lock &amp;rarr; X Lock 순으로 잠금을 획득하는 것은 외래키 제약과 큰 관련이 있다. &lt;code&gt;application_form&lt;/code&gt;과 &lt;code&gt;application_item&lt;/code&gt;은 외래키 제약을 맺고 있다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;Hibernate: 
    insert 
    into
        application_item
        (application_item_id, application_answer, application_form_id, application_question, category_id) 
    values
        (default, ?, ?, ?, ?)
Hibernate: 
    update
        application_form 
    set
        last_modified_time=?,
        recruiter=?,
        semester=?,
        member_id=?,
        writing_state=?,
        years=? 
    where
        application_form_id=?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 SQL을 다시 살펴보면 &lt;code&gt;applciation_item&lt;/code&gt;에 insert 후 &lt;code&gt;application_form&lt;/code&gt;에 update를 하고 있다. &lt;code&gt;application_form&lt;/code&gt;에 대한 update 쿼리 수행 시 X Lock을 획득하는 것은 당연한 흐름이다. 그런데 S Lock은 대체 어디서 획득하는 것일일까? 답은 외래키 제약과 참조 무결성에 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;참조 무결성&lt;/b&gt;(referential integrity)은 관계 데이터베이스 관계 모델에서 외래키 제약이 걸린 테이블 간의 일관성(데이터 무결성)을 말한다. 쉽게 말하면 &lt;code&gt;appcliation_item&lt;/code&gt;을 insert하거나 update하는 과정에서 다른 트랜잭션에서 &lt;code&gt;application_form&lt;/code&gt;을 변경하거나 삭제하게 되면 무결성을 침해할 수 있다. 때문에 부모 테이블을 변경하지 못하도록 부모 테이블에 S Lock을 걸게 되는 것이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;데드락 해결&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원인은 파악했으니 이제 해결을 해야 한다. 해결 방법은 몇 가지가 있다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;프론트에서 &amp;lsquo;지원 항목 추가&amp;rsquo; API를 동시에 날리지 못하게 한다 &amp;rarr; 사용자 경험 상 좋지 않고 애초에 동시에 날리면 안 되는 API가 말이 되지 않는다.&lt;/li&gt;
&lt;li&gt;외래키 제약을 해제한다. &amp;rarr; 실제로 참조 관계를 갖고 있지만 외래키 제약을 맺지 않는 다른 테이블이 존재하기는 한다. 하지만 &amp;lsquo;지원서&amp;rsquo;와 &amp;lsquo;지원 항목&amp;rsquo;은 밀접한 관련을 갖고 있고 참조 무결성을 지켜야 하는 중요한 관계라고 판단해서 해제하지 않는 것으로 했다.&lt;/li&gt;
&lt;li&gt;필요 없는 update 쿼리를 삭제한다. &amp;rarr; 위에서도 언급했듯이 update 쿼리는 영속성 전이, 변경 감지를 사용하면서 딸려 나가는 쿼리고 비즈니스 로직상 필요가 없다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필요 없는 update 쿼리, 필요 없는 X Lock을 제거하는 것이 가장 좋다고 판단해서 3번 해결 방법을 채택하고 코드를 변경했다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public Long addItem(Long applicationFormId,
                        LoginMember loginMember,
                        ApplicationItemCreateRequest request) {
        Writer writer = new Writer(loginMember.id());
        ApplicationForm form = writerCheckedFormService.checkWriterAndGet(applicationFormId, writer);
        ApplicationItem item = new ApplicationItem(
                form,
                new ApplicationQuestion(request.applicationQuestion()),
                new ApplicationAnswer(request.applicationAnswer())
        );
        applicationItemRepository.save(item);
        return item.getId();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;ApplicationItem&lt;/code&gt;을 영속성 전이에 의한 저장이 아닌 명시적인 &lt;code&gt;save()&lt;/code&gt; 호출로 저장함으로써 insert 쿼리만 발생하게 하였다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;Hibernate: 
    insert 
    into
        application_item
        (application_item_id, application_answer, application_form_id, application_question, category_id) 
    values
        (default, ?, ?, ?, ?)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lsquo;지원 항목 추가&amp;rsquo;에는 영속성 전이가 필요 없어졌지만 &amp;lsquo;지원서&amp;rsquo;를 저장할 때 &amp;lsquo;지원 항목&amp;rsquo;도 함께 저장하도록 되어 있기 때문에 &lt;code&gt;CascadeType.PERSIST&lt;/code&gt;는 유지하고 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;발생하는 쿼리를 변경함으로써 발생하는 데드락을 해결할 수 있었다.&lt;/li&gt;
&lt;li&gt;외래키에 의한 데드락 문제는 은근 많이 발생하는 문제인 것 같다. 특히 이번 문제는 JPA 동작 방식과 외래키에 의한 잠금 전파 등에 대한 지식이 없으면 해결하기 어려웠을 것이다.&lt;/li&gt;
&lt;li&gt;JPA의 기능을 적극 활용하는 것은 좋지만 이번에 데드락 같은 문제나 필요 없는 쿼리, 잠금을 획득하는 부분이 있었다. 좀 더 조심해서 사용해야 겠다는 생각이 들었다.&lt;/li&gt;
&lt;li&gt;도메인 주도 설계나 JPA를 사용하면 &amp;lsquo;데이터베이스 종속적이지 않은 코드(또는 설계)&amp;rsquo;를 한다고 하지만 결국 데이터베이스 종속적일 수밖에 없다는 생각이 들었다. DB의 동작 방식 때문에 코드를 변경하게 됐으니 말이다.&lt;/li&gt;
&lt;li&gt;그래도 최대한 도메인 중심으로 설계 후 성능이나 버그에 맞춰 데이터베이스를 신경쓰는 절차 자체는 필요하다는 생각이 든다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>DB</category>
      <category>DB</category>
      <category>jpa</category>
      <category>데드락</category>
      <category>프로젝트</category>
      <author>더즈</author>
      <guid isPermaLink="true">https://does-log.tistory.com/62</guid>
      <comments>https://does-log.tistory.com/62#entry62comment</comments>
      <pubDate>Fri, 2 Jun 2023 10:47:48 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] @Transactional과 @Async</title>
      <link>https://does-log.tistory.com/61</link>
      <description>&lt;p data-ke-size=&quot;size18&quot;&gt;최근에 처음으로 기술 면접을 경험했다. 프로젝트에 관한 질문이 많았는데 당시에 비즈니스 로직 중 비동기 처리를 해야 했었다. DB에 접근하는 로직이었기에 @Transactional과 @Async를 함께 적용시켜 사용했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;493&quot; data-origin-height=&quot;159&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sjoeg/btrRsFcpWUr/Ks23MHRzKuROJJwWvYlRmK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sjoeg/btrRsFcpWUr/Ks23MHRzKuROJJwWvYlRmK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sjoeg/btrRsFcpWUr/Ks23MHRzKuROJJwWvYlRmK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fsjoeg%2FbtrRsFcpWUr%2FKs23MHRzKuROJJwWvYlRmK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;493&quot; height=&quot;159&quot; data-origin-width=&quot;493&quot; data-origin-height=&quot;159&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이에 대해 면접관이 물었다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Async와 @Transactional이 내부에서 어떻게 동작하는지 아시나요?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;덧붙여서 상상력을 발휘해도 좋다고 했다. 정확하게 AOP 내부 구현을 몰랐던 나는 다음과 같이 대답했다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아마 트랜잭션 관련 AOP 프록시가 부가 기능을 수행한 뒤 비동기 관련 AOP 프록시가 비동기 부가 기능을 넣고 마지막으로 target을 호출할 것 같습니다. 내부적으로 순서가 있을 것 같은데 반대의 순서도 상관없을 것 같습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;정확히 저렇게 말하진 않았겠지만 저런 흐름이었다. &lt;s&gt;자신이 없어서 목소리가 기어들어갔던 것으로 기억한다&amp;hellip;&lt;/s&gt; 이후 면접이 끝나고 공부를 해보니 저 대답이 &lt;b&gt;완전히 틀린 대답&lt;/b&gt;이라는 것을 알게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;a href=&quot;https://does-log.tistory.com/60&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전 글&lt;/a&gt;을 읽고 오면 아래 글의 이해가 더 쉬울 것이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;하나의 프록시, n개의 어드바이저&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;스프링에서는 &lt;b&gt;적용해야 하는 어드바이저가 여러 개더라도 하나의 프록시만 생성하고 여러 어드바이저를 적용하도록 되어 있다.&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아마 트랜잭션 관련 AOP 프록시가 부가 기능을 수행한 뒤 비동기 관련 AOP 프록시가 비동기 부가 기능을 넣고 마지막으로 target을 호출할 것 같습니다&amp;hellip;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;따라서 위의 대답은 틀린 것이었다. @Transactional과 @Async를 같이 붙여도 프록시는 하나만 생성되는 것이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;트랜잭션 처리와 비동기 Advice 테스트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;직접 만든 커스텀 어노테이션인 @MyTransactional과 @MyAsync로 AOP 구현을 확인해보고자 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실제 호출 메서드&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;766&quot; data-origin-height=&quot;195&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cZB9IN/btrRssjYSxz/UGgkmVRPNoDrOsbvKlTLW0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cZB9IN/btrRssjYSxz/UGgkmVRPNoDrOsbvKlTLW0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cZB9IN/btrRssjYSxz/UGgkmVRPNoDrOsbvKlTLW0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcZB9IN%2FbtrRssjYSxz%2FUGgkmVRPNoDrOsbvKlTLW0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;766&quot; height=&quot;195&quot; data-origin-width=&quot;766&quot; data-origin-height=&quot;195&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;MyTransactionalAspect&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;914&quot; data-origin-height=&quot;686&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cpzMjB/btrRozdt28d/uBUNgMnHTk4iD9LJDXBdoK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cpzMjB/btrRozdt28d/uBUNgMnHTk4iD9LJDXBdoK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cpzMjB/btrRozdt28d/uBUNgMnHTk4iD9LJDXBdoK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcpzMjB%2FbtrRozdt28d%2FuBUNgMnHTk4iD9LJDXBdoK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;914&quot; height=&quot;686&quot; data-origin-width=&quot;914&quot; data-origin-height=&quot;686&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;MyAsyncAspect&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;487&quot; data-origin-height=&quot;490&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cyurxr/btrRrQFf72R/szazqloEkEDWlfQyqERcyk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cyurxr/btrRrQFf72R/szazqloEkEDWlfQyqERcyk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cyurxr/btrRrQFf72R/szazqloEkEDWlfQyqERcyk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcyurxr%2FbtrRrQFf72R%2FszazqloEkEDWlfQyqERcyk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;487&quot; height=&quot;490&quot; data-origin-width=&quot;487&quot; data-origin-height=&quot;490&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;@Order 어노테이션을 통해 일단 비동기 부가 기능이 먼저 적용되도록 설정하였다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;테스트 코드&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;롤백이 일어났을 때 한 트랜잭션으로 묶여 잘 롤백되는지 확인하는 테스트이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;797&quot; data-origin-height=&quot;606&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c0YYjJ/btrRozxMxKn/pB3k8TZO2uLsOnsZmRQugk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c0YYjJ/btrRozxMxKn/pB3k8TZO2uLsOnsZmRQugk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c0YYjJ/btrRozxMxKn/pB3k8TZO2uLsOnsZmRQugk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc0YYjJ%2FbtrRozxMxKn%2FpB3k8TZO2uLsOnsZmRQugk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;797&quot; height=&quot;606&quot; data-origin-width=&quot;797&quot; data-origin-height=&quot;606&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CglibAopProxy&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;프록시로 대체된 UserService가 메서드를 호출하고 디버그를 찍어보니&amp;nbsp;CglibAopProxy의 intercept 메서드 안으로 들어와 지는 것을 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;587&quot; data-origin-height=&quot;67&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IaWHH/btrRo4xxpKz/7LAgENFbuPtOYo9iToSWf1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IaWHH/btrRo4xxpKz/7LAgENFbuPtOYo9iToSWf1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IaWHH/btrRo4xxpKz/7LAgENFbuPtOYo9iToSWf1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIaWHH%2FbtrRo4xxpKz%2F7LAgENFbuPtOYo9iToSWf1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;587&quot; height=&quot;67&quot; data-origin-width=&quot;587&quot; data-origin-height=&quot;67&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;내부에 chain이라는 지역변수를 발견했는데 안에 @Aspect로 적용한 비동기와 트랜잭션 Advise가 들어 있는 것을 확인했다. 하나의 프록시가 여러 Advice를 가지고 있으면서 재귀적으로 부가 기능을 적용해 가면서 로직을 호출해 간다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;875&quot; data-origin-height=&quot;226&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bSmxZh/btrRsH2hYEk/VZR16Ng6Ac1Wh3g1zyzxz1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bSmxZh/btrRsH2hYEk/VZR16Ng6Ac1Wh3g1zyzxz1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bSmxZh/btrRsH2hYEk/VZR16Ng6Ac1Wh3g1zyzxz1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbSmxZh%2FbtrRsH2hYEk%2FVZR16Ng6Ac1Wh3g1zyzxz1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;875&quot; height=&quot;226&quot; data-origin-width=&quot;875&quot; data-origin-height=&quot;226&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;순서에 따라 상이한 테스트 결과&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;비동기를 먼저 적용했을 때 테스트를 통과하는 것을 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;415&quot; data-origin-height=&quot;101&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nnfm5/btrRo4xxINS/oKqfKy0mbtk5lsU50o6Cc0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nnfm5/btrRo4xxINS/oKqfKy0mbtk5lsU50o6Cc0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nnfm5/btrRo4xxINS/oKqfKy0mbtk5lsU50o6Cc0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fnnfm5%2FbtrRo4xxINS%2FoKqfKy0mbtk5lsU50o6Cc0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;415&quot; height=&quot;101&quot; data-origin-width=&quot;415&quot; data-origin-height=&quot;101&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;그런데 트랜잭션을 먼저 적용하자 테스트가 실패하였다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;735&quot; data-origin-height=&quot;304&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dKfhjB/btrRrmRRQK9/nj5tYqkPEclB8D3NFFlq0k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dKfhjB/btrRrmRRQK9/nj5tYqkPEclB8D3NFFlq0k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dKfhjB/btrRrmRRQK9/nj5tYqkPEclB8D3NFFlq0k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdKfhjB%2FbtrRrmRRQK9%2Fnj5tYqkPEclB8D3NFFlq0k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;735&quot; height=&quot;304&quot; data-origin-width=&quot;735&quot; data-origin-height=&quot;304&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;@Transactional과 스레드&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;스프링 공식 문서에는 다음과 같이 나와 있다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Transactional은 일반적으로 PlatformTransactionManager가 관리하는 thread-bound 트랜잭션에서 동작하며, 현재 실행 스레드 내의 데이터 접근 트랜잭션을 노출한다. NOTE: 새로운 스레드에게는 전파되지 않는다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;스프링은 Transaction 관련 정보를 ThreadLocal에 저장하는데 다른 스레드로 분기해 버리면, 즉 트랜잭션 내부에 비동기 처리가 일어나면 그 로직은 트랜잭션에 합류시킬 수 없어서 트랜잭션이 적용되지 않는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;결론적으로 비동기와 트랜잭션 처리를 함께 해주고 싶다면 먼저 비동기 Advice를 적용하고 후에 트랜잭션 Advice를 적용하여서 새로운 스레드 안에서 트랜잭션을 시작해야 한다는 것이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;스프링이 보장하는 @Transactional, @Async 순서&lt;/h2&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;size18&quot;&gt;즉 위처럼 말했던 것은 틀린 답변이었다. @Async 부가 기능이 먼저 적용되어야 두 부가 기능이 정상적으로 동작한다. 그리고 스프링은 내부적으로 이 둘의 순서를 보장하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;아래 사진은 CglibAopProxy 클래스에 디버그를 찍고 확인한 사진이다. Adivce를 List로 가지고 있는 것을 확인하였는데 두 어노테이션을 둘 다 붙인 메서드를 호출했을 때 내부적으로 아래와 같은 순서로 되어 있었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;349&quot; data-origin-height=&quot;75&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lln8W/btrRppatk0s/o3gZH7Ja77x4HezJvPIXQ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lln8W/btrRppatk0s/o3gZH7Ja77x4HezJvPIXQ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lln8W/btrRppatk0s/o3gZH7Ja77x4HezJvPIXQ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Flln8W%2FbtrRppatk0s%2Fo3gZH7Ja77x4HezJvPIXQ0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;349&quot; height=&quot;75&quot; data-origin-width=&quot;349&quot; data-origin-height=&quot;75&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;AsyncExecutionInterceptor&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;@Async가 적용되는 Advice에는 Order가 Integer.MIN_VALUE가 적용되어 가장 먼저 동작하도록 되어 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;516&quot; data-origin-height=&quot;74&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cmT4RX/btrRrPzEjRX/GS0QflwrgrMKKIIgRxEf2K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cmT4RX/btrRrPzEjRX/GS0QflwrgrMKKIIgRxEf2K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cmT4RX/btrRrPzEjRX/GS0QflwrgrMKKIIgRxEf2K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcmT4RX%2FbtrRrPzEjRX%2FGS0QflwrgrMKKIIgRxEf2K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;516&quot; height=&quot;74&quot; data-origin-width=&quot;516&quot; data-origin-height=&quot;74&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;EnableTransactionManagemet&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;@Transactional이 적용되는 Advice에는 Order가 Integer.MAX_VALUE가 적용되어 가장 나중에 동작하도록 되어 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;393&quot; data-origin-height=&quot;51&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bONkj4/btrRpXksMkU/kTjvAkeOVOAFUDLMQQXpM1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bONkj4/btrRpXksMkU/kTjvAkeOVOAFUDLMQQXpM1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bONkj4/btrRpXksMkU/kTjvAkeOVOAFUDLMQQXpM1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbONkj4%2FbtrRpXksMkU%2FkTjvAkeOVOAFUDLMQQXpM1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;393&quot; height=&quot;51&quot; data-origin-width=&quot;393&quot; data-origin-height=&quot;51&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;AOP 동작 과정에 대해 공부해보니 지금까지 @Transactional 같은 어노테이션을 잘 모르고 사용하고 있었다는 느낌을 받았다. 아마 커스텀 AOP를 만들었을 때 순서에 대한 개념이 없었다면 예상치 못한 버그에 대해 갈피를 못 잡았을 수도 있었을 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;결론적으로 다시 질문을 받는다면 다음과 같이 답변을 수정할 수 있을 것 같다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부적으로 프록시가 하나 생성됩니다. 프록시는 부가 기능의 모음인 Advise 리스트를 가지고 있습니다. Advise들은 각자의 Order 값에 의해 순서가 정해지는데 스프링에서 내부적으로 AsyncExecutionInterceptor는 Integer.MIN_VALUE이고 TransactionInterceptor는 Integer.MAX_VALUE라서 무조건 비동기 부가 기능을 씌우고 트랜잭션을 씌우도록 되어 있습니다. 반대의 순서라면 트랜잭션이 동작하지 않기 때문입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;사용하는 기술에 대해 어느 정도 알아보고 사용해야 한다는 것을 더 뼈저리게 느끼게 된 면접이었다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;참고&lt;/h3&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;a href=&quot;https://newwisdom.tistory.com/m/127&quot;&gt;https://newwisdom.tistory.com/m/127&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-framework/docs/current/reference/html/data-access.html#tx-decl-explained&quot;&gt;https://docs.spring.io/spring-framework/docs/current/reference/html/data-access.html#tx-decl-explained&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#aop-introduction-defn&quot;&gt;https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#aop-introduction-defn&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>스프링</category>
      <category>AOP</category>
      <category>Spring</category>
      <category>비동기</category>
      <category>트랜잭션</category>
      <author>더즈</author>
      <guid isPermaLink="true">https://does-log.tistory.com/61</guid>
      <comments>https://does-log.tistory.com/61#entry61comment</comments>
      <pubDate>Thu, 17 Nov 2022 14:10:11 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] AOP와 동적 프록시</title>
      <link>https://does-log.tistory.com/60</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;스프링 AOP&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;애플리케이션 기능의 종류에는 크게 &lt;b&gt;핵심 기능&lt;/b&gt;과 &lt;b&gt;부가 기능&lt;/b&gt;으로 나눌 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;OrderService라는 객체에서 핵심 기능이라 함은 &amp;lsquo;주문을 한다&amp;rsquo;이고 부가 기능이라 함은 &amp;lsquo;트랜잭션 처리&amp;rsquo;라고 예를 들 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;696&quot; data-origin-height=&quot;111&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/78dBa/btrRoyEZU0o/JQEin8frobhTPkVSp9FbCK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/78dBa/btrRoyEZU0o/JQEin8frobhTPkVSp9FbCK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/78dBa/btrRoyEZU0o/JQEin8frobhTPkVSp9FbCK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F78dBa%2FbtrRoyEZU0o%2FJQEin8frobhTPkVSp9FbCK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;696&quot; height=&quot;111&quot; data-origin-width=&quot;696&quot; data-origin-height=&quot;111&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이러한 부가 기능은 보통 여러 클래스에서 동시에 적용되는 데 이를 횡단 관심사(cross-cutting concerens)이라고 한다. 이러한 부가 기능을 적용하기 위해선 보통 많은 반복과 중복 코드, 그리고 중복으로 인한 많은 수정을 요하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;AOP는 &lt;b&gt;관점 지향 프로그래밍(Aspect-Oriented Programming)&lt;/b&gt;의 약자로 이러한 중복되는 횡단 관심사를 모듈화 하여 분리하기 위해 나온 패러다임이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;동적 프록시&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;스프링에서는 AOP를 구현하기 위해 동적 프록시 기술을 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;클라이언트가 서버를 호출한다고 했을 때 직접 호출하는 것이 아니라 어떤 대리자를 통해 간접적으로 요청할 수도 있는데 이 대리자를 프록시라고 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;604&quot; data-origin-height=&quot;70&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bmXJ6y/btrRoQZP332/FnxnxsBKyjR2iBkGZb64G1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bmXJ6y/btrRoQZP332/FnxnxsBKyjR2iBkGZb64G1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bmXJ6y/btrRoQZP332/FnxnxsBKyjR2iBkGZb64G1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbmXJ6y%2FbtrRoQZP332%2FFnxnxsBKyjR2iBkGZb64G1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;604&quot; height=&quot;70&quot; data-origin-width=&quot;604&quot; data-origin-height=&quot;70&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;클라이언트 클래스의 코드와 실제 객체 클래스의 코드를 거의 건드리지 않고 부가 기능을 추가하기 위해 중간에 프록시를 호출하고, 프록시가 부가 기능(트랜잭션 처리)과 실제 객체의 로직을 함께 호출하도록 하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이렇게 프록시를 사용하여 새로운 기능 추가를 목적으로 하는 것을 데코레이터 패턴이라고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;하지만 그냥 프록시를 사용하게 되면 부가 기능이 추가될 때마다 새로운 프록시 클래스를 만들어야 한다. (트랜잭션 프록시, 비동기 프록시, 로깅 프록시&amp;hellip;) 이를 해결하기 위해 동적 프록시 기술을 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;동적 프록시 기능을 사용하면 개발자가 직접 프록시 클래스를 만들지 않아도 개발자가 부가 기능과 적용 대상만 잘 지정하면 런타임에 동적으로 만들어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;스프링에서는 인터페이스 기반의 JDK 동적 프록시와 상속을 통한 CGLIB 프록시 기술을 제공한다. 최근 스프링부트에서는 인터페이스가 존재하든 안하든 CGLIB 프록시로 프록시를 생성하도록 하고 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;569&quot; data-origin-height=&quot;174&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dr3ubP/btrRpYJREtt/BQs9OKTd3sJcGUur8vfieK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dr3ubP/btrRpYJREtt/BQs9OKTd3sJcGUur8vfieK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dr3ubP/btrRpYJREtt/BQs9OKTd3sJcGUur8vfieK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdr3ubP%2FbtrRpYJREtt%2FBQs9OKTd3sJcGUur8vfieK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;569&quot; height=&quot;174&quot; data-origin-width=&quot;569&quot; data-origin-height=&quot;174&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;프록시 적용해보기&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;776&quot; data-origin-height=&quot;178&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/4CGo6/btrRrPZ5dQk/EYk7eKpyJYaGATu8f8k4U1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/4CGo6/btrRrPZ5dQk/EYk7eKpyJYaGATu8f8k4U1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/4CGo6/btrRrPZ5dQk/EYk7eKpyJYaGATu8f8k4U1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F4CGo6%2FbtrRrPZ5dQk%2FEYk7eKpyJYaGATu8f8k4U1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;776&quot; height=&quot;178&quot; data-origin-width=&quot;776&quot; data-origin-height=&quot;178&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;위처럼 직접 만든 어노테이션을 붙인 UserService의 changePassword 메서가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;아래의 테스트에서 stubUserHistoryDao는 현재 log 메서드를 호출하면 예외가 발생하도록 되어 있다. 때문에 channgePassword를 하면 user의 비밀번호는 update하지만 log를 호출하면서 예외가 발생한다. 트랜잭션이 적용된다면 원래 비밀번호가 새로운 비밀번호와 같지 않다는 아래 테스트는 통과해야 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;690&quot; data-origin-height=&quot;321&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EmB25/btrRppgHEFF/qCIewlPTfUKAhquZukSZB1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EmB25/btrRppgHEFF/qCIewlPTfUKAhquZukSZB1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EmB25/btrRppgHEFF/qCIewlPTfUKAhquZukSZB1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEmB25%2FbtrRppgHEFF%2FqCIewlPTfUKAhquZukSZB1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;690&quot; height=&quot;321&quot; data-origin-width=&quot;690&quot; data-origin-height=&quot;321&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;당연히 현재는 프록시가 적용되지 않았기 때문에 실패한다. 이를 통과하도록 만들어 보자.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Advise, Pointcut, Advisor&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;지금부터 스프링이 지원하는 방법으로 프록시를 만들어볼 텐데 우선 다음의 용어를 익혀야 한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;포인트컷(Pointcut)&lt;/b&gt;: 어디에 부가 기능을 적용할지 판단하는 필터링 로직&lt;/li&gt;
&lt;li&gt;&lt;b&gt;어드바이스(Advice)&lt;/b&gt;: 프록시가 호출하는 부가 기능&lt;/li&gt;
&lt;li&gt;&lt;b&gt;어드바이저(Advisor)&lt;/b&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;TransactionPointcut&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;639&quot; data-origin-height=&quot;197&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qZklR/btrRnLkHgd3/k7PBoATqMn44Dbqk1bmdq1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qZklR/btrRnLkHgd3/k7PBoATqMn44Dbqk1bmdq1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qZklR/btrRnLkHgd3/k7PBoATqMn44Dbqk1bmdq1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqZklR%2FbtrRnLkHgd3%2Fk7PBoATqMn44Dbqk1bmdq1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;639&quot; height=&quot;197&quot; data-origin-width=&quot;639&quot; data-origin-height=&quot;197&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;우선 포인트컷부터 만들어보자. 포인트컷은 Pointcut 인터페이스를 구현하여 만들 수 있다. 아래는 구현을 더 편하게 하기 위해 StaticMethodMatcherPoitncut을 상속하여 조건을 명시했다. (Target 클래스의 클래스 이름, 메소드 이름을 기반으로 Pointcut을 만들고자 할 때 사용하는 클래스이다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;메서드나 클래스에 MyTransactional 어노테이션이 존재하면 true를 반환하도록 하여 부가 기능을 적용할 대상을 판단하도록 구현했다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;TransactionAdvice&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;915&quot; data-origin-height=&quot;556&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/brCPTW/btrRoP0XLwW/4kkHkh2OEb17nmJgtP8xJ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/brCPTW/btrRoP0XLwW/4kkHkh2OEb17nmJgtP8xJ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/brCPTW/btrRoP0XLwW/4kkHkh2OEb17nmJgtP8xJ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbrCPTW%2FbtrRoP0XLwW%2F4kkHkh2OEb17nmJgtP8xJ0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;915&quot; height=&quot;556&quot; data-origin-width=&quot;915&quot; data-origin-height=&quot;556&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;MethodInterceptor는 Interceptor를, Interceptor는 Advice 인터페이스를 상속한다. invoke 메서드를 오버라이드 하여 트랜잭션 처리 부가 기능을 추가했다. MethodInvocation 객체 안에 메서드를 호출하는 방법, 현재 프록시, 메서드 매개 변수, 메서드 정보 등이 포함되어 있어서 proceed() 메서드를 호출하는 것으로 실제 기능을 호출할 수 있게 된다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;TransactionAdvicor&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;539&quot; data-origin-height=&quot;452&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1fPtm/btrRn5JSrra/sm8TuSwODGy2U9a0HLdHO0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1fPtm/btrRn5JSrra/sm8TuSwODGy2U9a0HLdHO0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1fPtm/btrRn5JSrra/sm8TuSwODGy2U9a0HLdHO0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1fPtm%2FbtrRn5JSrra%2Fsm8TuSwODGy2U9a0HLdHO0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;539&quot; height=&quot;452&quot; data-origin-width=&quot;539&quot; data-origin-height=&quot;452&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;어드바이저는 포인트컷 + 어드바이스라고 했다. 위처럼 코드를 구성하면 준비는 끝난다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;ProxyFactory&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;스프링은 동적 프록시를 통합해서 편리하게 만들어주는 프록시 팩토리(ProxyFactory)라는 기능을 제공한다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;693&quot; data-origin-height=&quot;259&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qolOX/btrRppnp3hJ/GAcKIMHOA7dG0K8RymiLC1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qolOX/btrRppnp3hJ/GAcKIMHOA7dG0K8RymiLC1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qolOX/btrRppnp3hJ/GAcKIMHOA7dG0K8RymiLC1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqolOX%2FbtrRppnp3hJ%2FGAcKIMHOA7dG0K8RymiLC1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;693&quot; height=&quot;259&quot; data-origin-width=&quot;693&quot; data-origin-height=&quot;259&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;프록시 팩토리를 통해 프록시를 생성할 때 어드바이저를 제공하면 어디에 어떤 기능을 제공할지 알 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;658&quot; data-origin-height=&quot;218&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bnAd1P/btrRpYwmkLm/7x6IvaBscrnkj5w7GutOvk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bnAd1P/btrRpYwmkLm/7x6IvaBscrnkj5w7GutOvk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bnAd1P/btrRpYwmkLm/7x6IvaBscrnkj5w7GutOvk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbnAd1P%2FbtrRpYwmkLm%2F7x6IvaBscrnkj5w7GutOvk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;658&quot; height=&quot;218&quot; data-origin-width=&quot;658&quot; data-origin-height=&quot;218&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;실제 객체(target)를 받아 프록시를 생성하는 메서드를 만들었다. 프록시 팩토리에 target과 어드바이스와 포인트컷으로 만든 어드바이저를 넣어주면 getProxy() 메서드를 통해 동적으로 프록시를 생성할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;838&quot; data-origin-height=&quot;319&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bZBa1f/btrRoySyXFr/yNrnED4ytW6icwpPXjpBI1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bZBa1f/btrRoySyXFr/yNrnED4ytW6icwpPXjpBI1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bZBa1f/btrRoySyXFr/yNrnED4ytW6icwpPXjpBI1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbZBa1f%2FbtrRoySyXFr%2FyNrnED4ytW6icwpPXjpBI1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;838&quot; height=&quot;319&quot; data-origin-width=&quot;838&quot; data-origin-height=&quot;319&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;테스트 실행 결과 통과하는 것을 확인했고 트랜잭션이 잘 적용되었다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;빈 후처리기를 통한 프록시 주입&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;스프링 IoC 컨테이너로 UserService를 주입받는다고 했을 때 어떻게 프록시로 대체해줄 수 있을까? 아무것도 해주지 않으면 아래처럼 주입받으면 실제 객체가 들어올 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;291&quot; data-origin-height=&quot;61&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/V38bb/btrRoydYJRK/JVwUwTT6Aw3FeG4JhqSDV1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/V38bb/btrRoydYJRK/JVwUwTT6Aw3FeG4JhqSDV1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/V38bb/btrRoydYJRK/JVwUwTT6Aw3FeG4JhqSDV1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FV38bb%2FbtrRoydYJRK%2FJVwUwTT6Aw3FeG4JhqSDV1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;291&quot; height=&quot;61&quot; data-origin-width=&quot;291&quot; data-origin-height=&quot;61&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;스프링에서는 이를 빈 후처리기를 통해 해결한다. 빈 후처리기란 빈 저장소에 등록할 목적으로 생성한 객체를 빈 저장소에 등록하기 전에 조작하거나 대체하고 싶을 때 사용할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;690&quot; data-origin-height=&quot;222&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lmW6a/btrRnwacTtM/ql51vohHjZPsl9JgOyQbo0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lmW6a/btrRnwacTtM/ql51vohHjZPsl9JgOyQbo0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lmW6a/btrRnwacTtM/ql51vohHjZPsl9JgOyQbo0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlmW6a%2FbtrRnwacTtM%2Fql51vohHjZPsl9JgOyQbo0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;690&quot; height=&quot;222&quot; data-origin-width=&quot;690&quot; data-origin-height=&quot;222&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;BeanPostProcessor 인터페이스를 구현하여 만들 수 있다. 아래 코드는 어떤 빈이 TransactionPointcut을 만족하면 프록시로 바꿔 칠 수 있도록 구현했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;빈 후처리기를 빈으로 등록하면 DI를 통해 빈을 주입받더라도 프록시로 대체되게 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;827&quot; data-origin-height=&quot;509&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ch94Ys/btrRrO7XOS4/GofY3ZzKH29MIE2ZOoKzX1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ch94Ys/btrRrO7XOS4/GofY3ZzKH29MIE2ZOoKzX1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ch94Ys/btrRrO7XOS4/GofY3ZzKH29MIE2ZOoKzX1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fch94Ys%2FbtrRrO7XOS4%2FGofY3ZzKH29MIE2ZOoKzX1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;827&quot; height=&quot;509&quot; data-origin-width=&quot;827&quot; data-origin-height=&quot;509&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;스프링이 제공하는 자동 프록시 생성기&lt;/b&gt;&lt;/h3&gt;
&lt;pre class=&quot;clean&quot;&gt;&lt;code&gt;implementation 'org.springframework.boot:spring-boot-starter-aop'
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;일일이 BeanPostProcessor를 구현하는 것도 귀찮기 때문에 스프링에서는 AnnotationAwareAspectJAutoProxyCreator라는 자동 프록시 생성기를 제공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;위 의존성을 추가하면 단순히 아래처럼 Advisor를 빈으로 등록하는 것만으로 포인트컷에 해당하는 빈을 프록시로 대체할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;767&quot; data-origin-height=&quot;265&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Erf97/btrRrPlv2yl/YajUa9bsa8eDMA2cfIxNE1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Erf97/btrRrPlv2yl/YajUa9bsa8eDMA2cfIxNE1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Erf97/btrRrPlv2yl/YajUa9bsa8eDMA2cfIxNE1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FErf97%2FbtrRrPlv2yl%2FYajUa9bsa8eDMA2cfIxNE1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;767&quot; height=&quot;265&quot; data-origin-width=&quot;767&quot; data-origin-height=&quot;265&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;@Aspect AOP&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Advisor, Advise, Pointcut 인터페이스를 각각 구현해서 Advisor를 빈으로 등록하는 것도 여간 귀찮은 것이 아니기 때문에 @Aspect 어노테이션으로 편하게 Advisor를 등록할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;915&quot; data-origin-height=&quot;576&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cGzj2g/btrRop2uovO/KL2VZdKBXbfcqokFrUtxDk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cGzj2g/btrRop2uovO/KL2VZdKBXbfcqokFrUtxDk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cGzj2g/btrRop2uovO/KL2VZdKBXbfcqokFrUtxDk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcGzj2g%2FbtrRop2uovO%2FKL2VZdKBXbfcqokFrUtxDk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;915&quot; height=&quot;576&quot; data-origin-width=&quot;915&quot; data-origin-height=&quot;576&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;@Aspect 어노테이션으로 Advisor를 등록할 수 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;@Aspect만으로는 빈 등록이 안 되기 때문에 @Component나 @Bean을 통해 따로 등록해주어야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;@Around가 붙은 메서드는 Advise가 된다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;@Around는 실제 기능 앞 뒤로 부가 기능을 넣겠다는 뜻&lt;/li&gt;
&lt;li&gt;내부에 포인트컷 표현식을 넣을 수 있는데 위에선 MyTransactional 어노테이션을 인식하도록 했다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;ProceedingJointPoint를 통해 실제 호출 대상을 호출할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이런 식으로 커스텀 Advisor를 만들어놓으면 자동 프록시 생성기는 다음과 같이 동작한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;622&quot; data-origin-height=&quot;267&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bN5PSx/btrRoPUiGJH/tZ1ilJ0aPS7zfdPrG0Jj50/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bN5PSx/btrRoPUiGJH/tZ1ilJ0aPS7zfdPrG0Jj50/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bN5PSx/btrRoPUiGJH/tZ1ilJ0aPS7zfdPrG0Jj50/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbN5PSx%2FbtrRoPUiGJH%2FtZ1ilJ0aPS7zfdPrG0Jj50%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;622&quot; height=&quot;267&quot; data-origin-width=&quot;622&quot; data-origin-height=&quot;267&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;빈들이 자동 프록시 생성기(빈 후처리기)에 전달됨&lt;/li&gt;
&lt;li&gt;Advisor 빈을 조회&lt;/li&gt;
&lt;li&gt;@Aspect 어노테이션이 붙은 클래스를 찾아 어드바이저 빌더가 Advisor로 변환&lt;/li&gt;
&lt;li&gt;Advisor들 안의 포인트컷으로 프록시 대상 빈을 바꿔치기해서 스프링 빈으로 등록&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;참고&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B3%A0%EA%B8%89%ED%8E%B8/dashboard&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;스프링 핵심 원리 - 고급편&lt;/a&gt;&lt;/p&gt;</description>
      <category>스프링</category>
      <category>AOP</category>
      <category>Spring</category>
      <category>프록시</category>
      <author>더즈</author>
      <guid isPermaLink="true">https://does-log.tistory.com/60</guid>
      <comments>https://does-log.tistory.com/60#entry60comment</comments>
      <pubDate>Thu, 17 Nov 2022 12:50:36 +0900</pubDate>
    </item>
    <item>
      <title>[JPA] 일대다 단방향 매핑은 무조건 피해야 할까</title>
      <link>https://does-log.tistory.com/59</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;일대다 단방향 매핑은 쓰지 말자?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;영한님의 JPA 기본편 강의를 보면 일대다 단방향 매핑을 지양해야 한다는 내용이 나온다. 자바 ORM 표준 JPA 프로그래밍 책에도 다음의 내용이 적혀 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;일대다 단방향 매핑을 사용하면 엔티티를 매핑한 테이블이 아닌 다른 테이블의 외래 키를 관리해야 한다. 이것은 성능 문제도 있지만 관리도 부담스럽다. &amp;hellip; 다대일 양방향 매핑은 관리해야 하는 외래 키가 본인 테이블에 있다. &amp;hellip; 상황에 따라 다르겠지만 일대다 단방향 매핑보다는 다대일 양방향 매핑을 권장한다.&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;양방향 참조는 객체지향적으로 손해&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;하지만 흔히 알고 있듯이 순환 참조, 양방향 참조는 객체지향 세계에서는 지양하는 편이다. 의존 관계나 연관 관계를 단방향으로 흐르게 하여 A가 변할 때 B는 영향을 받더라도 B가 변할 때조차 A가 영향을 받게 하는 건 유지보수적인 관점으로 손해라는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;서로를 사용한다면 또 모를까 오로지 일대다 단방향의 단점을 해소하기 위해 양방향을 맺는 경우도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;아래의 Menu 객체와 MenuProduct 객체 예시를 보자.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Entity
public class MenuProduct {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long seq;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = &quot;menu_id&quot;, nullable = false)
    private Menu menu;
}

@Entity
public class Menu {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @OneToMany(mappedBy = &quot;menu&quot;, {CascadeType.PERSIST, CascadeType.REMOVE})
    private List&amp;lt;MenuProduct&amp;gt; menuProducts = new ArrayList&amp;lt;&amp;gt;();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;두 엔티티는 양방향으로 참조 관계를 가지고 있다. 실제 두 엔티티가 가지고 있는 메서드는 생략하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;비즈니스 규칙 상 Menu가 MenuProduct를 사용해 검증 등의 로직을 가지고 있다. 그리고 Menu 없는 MenuProduct는 있을 수 없고, MenuProduct는 Menu와 같이 생성되어 저장된다는 특성을 가지고 있어서 CascadeType.PERSIST와 CascadeType.REMOVE를 걸어준 상태이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;하지만 MenuProduct는 Menu로 뭔가를 하는 일이 없다. 양방향 참조를 맺어주기 위한 메서드만을 가지고 있을 뿐이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;순환 참조라는 문제도 있지만 MenuProduct만을 테스트하고 싶을 때 Menu도 항상 필요하다는 문제도 생긴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;그리고 Menu를 저장하고 MenuProduct를 저장할 때 한 번에 insert 쿼리를 보내는 것이 아니라 update 쿼리를 추가로 보내는 문제도 있다. 여기서 오는 패러다임 불일치 때문에 복잡한 실무에서는 문제가 될 수 있다고도 한다. &lt;s&gt;&lt;span style=&quot;font-size: 16px; letter-spacing: 0px;&quot;&gt;왜 Menu 엔티티를 건드렸는데 menu_product 테이블에서 update 쿼리가 나가지?&lt;/span&gt;&lt;/s&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;다대일 양방향에서의 저장 쿼리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;일단 update 쿼리가 나가는 것에 대해 얘기해보고자 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;도메인에 대해 간단히 설명하면 MenuGroup 안에 여러 Menu들이 존재한다. Menu에는 어떤 Product가 몇 개씩 필요한지에 대한 정보가 필요하므로 Product가 몇 개인지 나타내는 MenuProduct가 n개 필요하다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;630&quot; data-origin-height=&quot;486&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/olbLX/btrPPAFuwx9/RmTe9AyqSSkybBQMkG1Gok/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/olbLX/btrPPAFuwx9/RmTe9AyqSSkybBQMkG1Gok/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/olbLX/btrPPAFuwx9/RmTe9AyqSSkybBQMkG1Gok/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FolbLX%2FbtrPPAFuwx9%2FRmTe9AyqSSkybBQMkG1Gok%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;630&quot; height=&quot;486&quot; data-origin-width=&quot;630&quot; data-origin-height=&quot;486&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;위에 봤던 코드처럼 양방향일 경우 아래 사진처럼 쿼리가 나간다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;396&quot; data-origin-height=&quot;644&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uc6aU/btrPTkHos72/OQ8BOh3lFA16bZ9lWkxZsk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uc6aU/btrPTkHos72/OQ8BOh3lFA16bZ9lWkxZsk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uc6aU/btrPTkHos72/OQ8BOh3lFA16bZ9lWkxZsk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fuc6aU%2FbtrPTkHos72%2FOQ8BOh3lFA16bZ9lWkxZsk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;396&quot; height=&quot;644&quot; data-origin-width=&quot;396&quot; data-origin-height=&quot;644&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;menu를 insert한 뒤 cascade = CascadeType.PERSIST에 의해 menu_product 2개가 바로 각각 insert 된다. (fk도 바로 넣어준다.)&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;일대다 단방향일 때&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;그럼 문제의 일대다 단방향 매핑을 해놓고 쿼리를 살펴보자.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Entity
public class MenuProduct {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long seq;
}

@Entity
public class Menu {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @OneToMany(cascade = {CascadeType.PERSIST, CascadeType.REMOVE})
    @JoinColumn(name = &quot;menu_id&quot;)
    private List&amp;lt;MenuProduct&amp;gt; menuProducts = new ArrayList&amp;lt;&amp;gt;();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;흔히 알고 있듯이 처음에는 menu_product를 insert 하는데 fk(menu_id)는 넣어주지 않는다. 그리고 후에 fk를 update 쿼리를 통해 넣어주고 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;360&quot; data-origin-height=&quot;789&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MlSlo/btrPUnYaX12/KhdRYXMBGb7yvvKevewWxK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MlSlo/btrPUnYaX12/KhdRYXMBGb7yvvKevewWxK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MlSlo/btrPUnYaX12/KhdRYXMBGb7yvvKevewWxK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMlSlo%2FbtrPUnYaX12%2FKhdRYXMBGb7yvvKevewWxK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;360&quot; height=&quot;789&quot; data-origin-width=&quot;360&quot; data-origin-height=&quot;789&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;여기서 만약 menu_product 테이블의 menu_id가 not null이면 어떻게 될까? 실제로도 메뉴 없이 메뉴 상품이 존재할 순 없을 테니 menu_product는 not null일 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;not null일 경우에는 아래와 같이 fk를 null로 menu_product를 저장하려다 DataIntegrityViolationException 예외를 맞고 만다. 제약 조건을 위반했으니 당연한 결과다. fk가 not null인 경우에는 일대다 단방향을 쓸 수조차 없다는 것일까?&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;org.springframework.dao.DataIntegrityViolationException: could not execute statement; SQL [n/a]; constraint [null]; nested exception is org.hibernate.exception.ConstraintViolationException: could not execute statement&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;nullable = false&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;@JoinColumn의 속성으로 nullable=false로 주면 어떻게 될까? 원래 코드에서는 JPA가 menu_id가 not null이라는 것을 모르는 상태였다. 테이블의 ddl에 맞춰 코드를 추가해 보았다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@OneToMany(cascade = {CascadeType.PERSIST, CascadeType.REMOVE})
@JoinColumn(name = &quot;menu_id&quot;, nullable = false)
private List&amp;lt;MenuProduct&amp;gt; menuProducts = new ArrayList&amp;lt;&amp;gt;();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;원래 쿼리와 똑같은 거 아니냐고 생각할 수 있지만 잘 살펴보면 다르다. menu_product의 menu_id가 not null임에도 예외가 발생하지 않았다. 자세히 보면 처음에 insert 할 때 menu_id도 같이 넣어주고 있는 것을 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;393&quot; data-origin-height=&quot;843&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/calypm/btrPQMq7imI/tOEVASUNAVW70cC1Lt8rWK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/calypm/btrPQMq7imI/tOEVASUNAVW70cC1Lt8rWK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/calypm/btrPQMq7imI/tOEVASUNAVW70cC1Lt8rWK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcalypm%2FbtrPQMq7imI%2FtOEVASUNAVW70cC1Lt8rWK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;393&quot; height=&quot;843&quot; data-origin-width=&quot;393&quot; data-origin-height=&quot;843&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;그리고 후에 update 쿼리도 보내는데 이미 fk가 저장된 상태라서 쓸데없는 쿼리가 되어버린 듯하다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;nullable=false + updatable=false&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;@JoinColumn의 속성으로는 updatable=false라는 속성도 있다. 해당 컬럼의 update를 막는 속성인데 추가해 보면 어떻게 될지 궁금했다. 굳이 안 보내도 되는 update 쿼리를 막을 수 있을 것 같았다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@OneToMany(cascade = {CascadeType.PERSIST, CascadeType.REMOVE})
@JoinColumn(name = &quot;menu_id&quot;, nullable = false, updatable = false)
private List&amp;lt;MenuProduct&amp;gt; menuProducts = new ArrayList&amp;lt;&amp;gt;();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;결과는 성공이었다. 깔끔하게 insert 쿼리만 나간 것을 확인할 수 있었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;397&quot; data-origin-height=&quot;653&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bCvXyL/btrPTlfeXtD/hYh4pBkwNgoxk2rztCoUJ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bCvXyL/btrPTlfeXtD/hYh4pBkwNgoxk2rztCoUJ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bCvXyL/btrPTlfeXtD/hYh4pBkwNgoxk2rztCoUJ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbCvXyL%2FbtrPTlfeXtD%2FhYh4pBkwNgoxk2rztCoUJ0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;397&quot; height=&quot;653&quot; data-origin-width=&quot;397&quot; data-origin-height=&quot;653&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;부모 자식 관계를 바꿀 수 없는 문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;그런데 nullable=false로 하는 순간 자바 컬렉션에서의 제거가 DB 쿼리로 이어지지 않는다. 즉 자바 객체와 DB 테이블 관계 간의 불일치가 발생하게 된다.&lt;/p&gt;
&lt;pre class=&quot;maxima&quot;&gt;&lt;code&gt;MenuProduct 메뉴_후라이드 = 후라이드와_양념.getMenuProducts().get(0);
후라이드와_양념.getMenuProducts().remove(메뉴_후라이드);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;nullalbe=false와 updatable=false 중 하나라도 있어도 쿼리는 나가지 않았다. 컬렉션에서의 제거는 곧 menu_id(fk)에 null로 update 쿼리를 보내겠다는 말이다. update가 막혀 있고 설령 update를 실행시키더라도 null을 넣는 쿼리이기 때문에 nullable=false에 의해 막히는 듯하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;둘 다 제거하면 아래처럼 객체 참조 제거가 update 쿼리로 이어진다. 단 not null에 의해 예외가 발생하지만 말이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;464&quot; data-origin-height=&quot;380&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b17lnm/btrPQxgG8ee/Fd7jJHKAmQVZcCwRoExAsK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b17lnm/btrPQxgG8ee/Fd7jJHKAmQVZcCwRoExAsK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b17lnm/btrPQxgG8ee/Fd7jJHKAmQVZcCwRoExAsK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb17lnm%2FbtrPQxgG8ee%2FFd7jJHKAmQVZcCwRoExAsK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;464&quot; height=&quot;380&quot; data-origin-width=&quot;464&quot; data-origin-height=&quot;380&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;@OneToMany 단방향의 단점 중 하나는 부모, 자식을 한 번에 insert 할 때, fk=null인 채로 자식을 insert 하고 후에 update로 fk를 별도로 변경하는 데에 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;일대다 단방향으로 하면서 nullable=false와 updatable=false 속성을 주면 update 없이 한 번에 insert 쿼리가 나가게 되어 위 단점은 해결할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;하지만 부모에게서 자식을 떼어 놓을 수 없고 자식을 다른 부모에게 옮기는 것도 동작하지 않게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;비즈니스 로직 상 부모 자식 간의 관계가 끊어지지 않고 라이프 사이클이 완전히 동일한 경우에 nullable=false, + updatable=false + 일대다 단방향을 활용해서 양방향 참조를 끊을 수도 있을 듯하다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;위의 상황에서 update 쿼리가 추가로 나가는 단점은 해결할 수 있을 듯 하나 강의에서 영한님은 다음의 문제도 있다고 말씀해 주신다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;엔티티가 관리하는 외래 키가 다른 테이블에 있다는 것 자체가 어마어마한 단점입니다 제가 생각할 때&amp;hellip;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;복잡한 실무 상황에서 위 말씀처럼 얼마나 혼란을 줄 수 있을지는 아직 겪어보지 않아서 모르겠다. 그래도 사이드 프로젝트를 할 때 실제 Menu와 MenuProduct와 같은 관계를 만나게 된다면 지금까지처럼 일대다 단방향을 무조건 피하자는 생각보단 한 번 더 생각해 볼 듯하다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;추가) 영한님과의 소통&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;906&quot; data-origin-height=&quot;467&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cAPwGf/btrPQH5d78R/zVfwzZCkcEmpK6dbaqOikk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cAPwGf/btrPQH5d78R/zVfwzZCkcEmpK6dbaqOikk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cAPwGf/btrPQH5d78R/zVfwzZCkcEmpK6dbaqOikk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcAPwGf%2FbtrPQH5d78R%2FzVfwzZCkcEmpK6dbaqOikk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;906&quot; height=&quot;467&quot; data-origin-width=&quot;906&quot; data-origin-height=&quot;467&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;888&quot; data-origin-height=&quot;327&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pJ0qV/btrPQqvXoOB/ODh37dgadHjOsmYYYsOi50/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pJ0qV/btrPQqvXoOB/ODh37dgadHjOsmYYYsOi50/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pJ0qV/btrPQqvXoOB/ODh37dgadHjOsmYYYsOi50/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpJ0qV%2FbtrPQqvXoOB%2FODh37dgadHjOsmYYYsOi50%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;888&quot; height=&quot;327&quot; data-origin-width=&quot;888&quot; data-origin-height=&quot;327&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;참고&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;자바 ORM 표준 JPA 프로그래밍 기본편&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;자바 ORM 표준 JPA 프로그래밍&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>JPA</category>
      <category>jpa</category>
      <author>더즈</author>
      <guid isPermaLink="true">https://does-log.tistory.com/59</guid>
      <comments>https://does-log.tistory.com/59#entry59comment</comments>
      <pubDate>Sat, 29 Oct 2022 16:20:00 +0900</pubDate>
    </item>
    <item>
      <title>운영 서버에서 예외 발생 시 자동 깃헙 이슈 생성</title>
      <link>https://does-log.tistory.com/58</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제 상황&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;프로젝트를 하면서 우리도 모르게 예외가 터지는 경우가 많아서 실제로 기능에 버그가 있는데 빠르게 캐치하지 못한 적이 많았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;예외가 발생했을 때 개발자인 우리에게 알려줄 수 있으면 좋겠다고 생각이 들었다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Github 이슈 자동 등록&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;스모디 팀 슬랙 채널이 존재한다. 프로젝트 깃허브에 PR, Issue 등록, Merge 등의 이벤트가 발생하면 슬랙 채널에 메시지가 오도록 Guthub 앱을 등록한 상태다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;648&quot; data-origin-height=&quot;341&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ZvTIV/btrOsasVwwJ/Bq1NKd3WMWkf4XiNPppYo1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ZvTIV/btrOsasVwwJ/Bq1NKd3WMWkf4XiNPppYo1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ZvTIV/btrOsasVwwJ/Bq1NKd3WMWkf4XiNPppYo1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZvTIV%2FbtrOsasVwwJ%2FBq1NKd3WMWkf4XiNPppYo1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;648&quot; height=&quot;341&quot; data-origin-width=&quot;648&quot; data-origin-height=&quot;341&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;a href=&quot;https://docs.github.com/en/rest/issues/issues#create-an-issue&quot;&gt;Create an Issue&lt;/a&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;POST https://api.github.com/repos/woowacourse-teams/2022-smody/issues&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Autuorization: token {accessToken}&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
    &quot;title&quot;:&quot;이슈 제목&quot;,
    &quot;body&quot;:&quot;이슈 바디&quot;,
    &quot;assignees&quot;:[&quot;ldk980130&quot;],
    &quot;labels&quot;:[&quot;에러 해결&quot;, &quot;백엔드&quot;]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;위 형태로 HTTP 요청을 하면 명시한 repo에 이슈가 자동으로 등록이 된다. 다만 api를 사용하려면 Authorization 헤더에 accessToken이 필요한데 이는 다음의 과정을 거쳐 쉽게 얻을 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Access Token 얻기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;1. Seetings에 들어간다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;212&quot; data-origin-height=&quot;561&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/864rc/btrOs29dj8e/GHqLaxCkwrIey4V0udyzOK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/864rc/btrOs29dj8e/GHqLaxCkwrIey4V0udyzOK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/864rc/btrOs29dj8e/GHqLaxCkwrIey4V0udyzOK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F864rc%2FbtrOs29dj8e%2FGHqLaxCkwrIey4V0udyzOK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;212&quot; height=&quot;561&quot; data-origin-width=&quot;212&quot; data-origin-height=&quot;561&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;2. 왼쪽 바 최하단에 Developers settings에 들어간다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;283&quot; data-origin-height=&quot;168&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Sf5fk/btrOsbFlOMS/Cy3bqxyrcfv4CKzzRlrnD1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Sf5fk/btrOsbFlOMS/Cy3bqxyrcfv4CKzzRlrnD1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Sf5fk/btrOsbFlOMS/Cy3bqxyrcfv4CKzzRlrnD1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSf5fk%2FbtrOsbFlOMS%2FCy3bqxyrcfv4CKzzRlrnD1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;283&quot; height=&quot;168&quot; data-origin-width=&quot;283&quot; data-origin-height=&quot;168&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;3. Personal access tokens 탭에서 Generate new token 버튼을 누른다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;933&quot; data-origin-height=&quot;373&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/6Sb0t/btrOqAzCHtZ/RrKG9YzKmjbqQPjRT2wLd1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6Sb0t/btrOqAzCHtZ/RrKG9YzKmjbqQPjRT2wLd1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6Sb0t/btrOqAzCHtZ/RrKG9YzKmjbqQPjRT2wLd1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F6Sb0t%2FbtrOqAzCHtZ%2FRrKG9YzKmjbqQPjRT2wLd1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;933&quot; height=&quot;373&quot; data-origin-width=&quot;933&quot; data-origin-height=&quot;373&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;4. NOTE에 간단한 설명을 입력하고 repo에만 체크를 해준 뒤 생성한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;653&quot; data-origin-height=&quot;564&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bD0KkI/btrOqFnnAN0/aQDoNIy9gn703kPKbV8RIK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bD0KkI/btrOqFnnAN0/aQDoNIy9gn703kPKbV8RIK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bD0KkI/btrOqFnnAN0/aQDoNIy9gn703kPKbV8RIK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbD0KkI%2FbtrOqFnnAN0%2FaQDoNIy9gn703kPKbV8RIK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;653&quot; height=&quot;564&quot; data-origin-width=&quot;653&quot; data-origin-height=&quot;564&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;5. 생성한 뒤 나오는 페이지에 token이 명시되는데 잘 복사해 둔다&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;자바 코드에서 api 요청하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;우선 application.properties에 방금 받아 온 accessToken을 넣어둔다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;374&quot; data-origin-height=&quot;37&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yyhLB/btrOsbyARHj/ciHXeQkETT70Fcy2CJKNY1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yyhLB/btrOsbyARHj/ciHXeQkETT70Fcy2CJKNY1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yyhLB/btrOsbyARHj/ciHXeQkETT70Fcy2CJKNY1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyyhLB%2FbtrOsbyARHj%2FciHXeQkETT70Fcy2CJKNY1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;374&quot; height=&quot;37&quot; data-origin-width=&quot;374&quot; data-origin-height=&quot;37&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;그리고 Exception을 매개변수로 받아 이슈 내용을 채워서 RestTemplate을 통해 이슈를 생성하는 로직을 짠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 메서드에 Exception을 넣어주면 해당 Exception의 정보가 Issue 제목과 본문에 들어가게 된다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Component
@RequiredArgsConstructor
public class GithubIssueGenerator {

	private static final String REPO_URL = &quot;https://api.github.com/repos/woowacourse-teams/2022-smody/issues&quot;;
	private static final List ASSIGNEES = List.of(&quot;ldk980130&quot;, &quot;bcc0830&quot;, &quot;jojogreen91&quot;, &quot;tonic523&quot;);
	private static final List LABELS = List.of(&quot;장애&quot;, &quot;에러 해결&quot;, &quot;백엔드&quot;);
	private static final MediaType JSON_MEDIA_TYPE = new MediaType(&quot;application&quot;, &quot;json&quot;, StandardCharsets.UTF_8);

	@Value(&quot;${github.access.token}&quot;)
	private String accessToken;
	private final ObjectMapper objectMapper;
	private final RestTemplate restTemplate;

	public void create(Exception exception) {
		// api 요청
		restTemplate.exchange(
			REPO_URL,
			HttpMethod.POST,
			new HttpEntity&amp;lt;&amp;gt;(createRequestJson(exception), setAuthorization()),
			String.class
		);
	}

	// DTO 객체를 JSON 문자열로 변환
	private String createRequestJson(Exception exception) {
		return parseJsonString(new IssueCreateRequest(
			&quot;[ERROR] 서버 장애 발생 &quot; + exception.getMessage(),
			createIssueBody(exception),
			ASSIGNEES,
			LABELS
		));
	}

	private String parseJsonString(IssueCreateRequest request) {
		try {
			return objectMapper.writeValueAsString(request);
		} catch (JsonProcessingException e) {
			throw new BusinessException(ExceptionData.DATA_INTEGRITY_ERROR);
		}
	}

	// 발생한 예외의 stackTrace를 마크다운 코드 블럭으로 감싸서 작성
	private String createIssueBody(Exception exception) {
		StringWriter stringWriter = new StringWriter();
		exception.printStackTrace(new PrintWriter(stringWriter));
		return &quot;```\\n&quot; + stringWriter + &quot;\\n```&quot;;
	}

	// Authorization 헤더에 accessToken 입력
	private HttpHeaders setAuthorization() {
		HttpHeaders httpHeaders = new HttpHeaders();
		httpHeaders.set(&quot;Authorization&quot;, &quot;token &quot; + accessToken);
		httpHeaders.setContentType(JSON_MEDIA_TYPE);
		return httpHeaders;
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;예상치 못한 예외 상황에 자동으로 api 호출&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;현재 ControllerAdvice 클래스에서 비즈니스 로직상 자연스럽게 발생하는 예외에 대해서는 BusinessException이라고 명시하고 로그를 찍어주고 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;939&quot; data-origin-height=&quot;205&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/YodHz/btrOqLgBO4l/A46J2nzO7SFOk2hA2g511K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/YodHz/btrOqLgBO4l/A46J2nzO7SFOk2hA2g511K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YodHz/btrOqLgBO4l/A46J2nzO7SFOk2hA2g511K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYodHz%2FbtrOqLgBO4l%2FA46J2nzO7SFOk2hA2g511K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;939&quot; height=&quot;205&quot; data-origin-width=&quot;939&quot; data-origin-height=&quot;205&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;BusinessException이 아닌 Exception은 우리가 예상치 못한 예외라는 뜻이고 이는 곧 버그다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;그래서 BusinessException 이외의 모든 예외를 잡은 @ExceptionHandler를 만들고 거기서 GithubIssueGenerator.create()를 호출하도록 했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;503&quot; data-origin-height=&quot;128&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cvhLPc/btrOr9ALZUe/UYJoXXXcSimkOKsSNvCG60/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cvhLPc/btrOr9ALZUe/UYJoXXXcSimkOKsSNvCG60/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cvhLPc/btrOr9ALZUe/UYJoXXXcSimkOKsSNvCG60/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcvhLPc%2FbtrOr9ALZUe%2FUYJoXXXcSimkOKsSNvCG60%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;503&quot; height=&quot;128&quot; data-origin-width=&quot;503&quot; data-origin-height=&quot;128&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;하지만 이렇게만 하면 로컬 환경이나 QA 환경 서버에서 버그가 났을 때도 자동으로 이슈가 등록된다. 운영 서버에서만 동작하도록 코드를 수정할 필요가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;우선 클래스에서 운영 환경 Profile 이름인 &amp;ldquo;prod&amp;rdquo;를 상수 선언하고 org.springframework.core.env.Environment를 빈 주입받도록 했다. Enviroment 빈을 통해 현재 실행되고 있는 환경의 ActiveProfile을 알 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;548&quot; data-origin-height=&quot;233&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/S4Nak/btrOqjkyyS0/kDDy2KqCaOLjsG3klZHR41/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/S4Nak/btrOqjkyyS0/kDDy2KqCaOLjsG3klZHR41/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/S4Nak/btrOqjkyyS0/kDDy2KqCaOLjsG3klZHR41/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FS4Nak%2FbtrOqjkyyS0%2FkDDy2KqCaOLjsG3klZHR41%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;548&quot; height=&quot;233&quot; data-origin-width=&quot;548&quot; data-origin-height=&quot;233&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;아래와 같이 ActiveProfile이 prod일 때만 동작하도록 if문을 간단히 추가하는 것 만으로 운영 환경에서만 동작하게 되었다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;541&quot; data-origin-height=&quot;297&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b4Qhm7/btrOsdJUU3Q/BAzOLOUCsQcCupIZihDW50/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b4Qhm7/btrOsdJUU3Q/BAzOLOUCsQcCupIZihDW50/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b4Qhm7/btrOsdJUU3Q/BAzOLOUCsQcCupIZihDW50/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb4Qhm7%2FbtrOsdJUU3Q%2FBAzOLOUCsQcCupIZihDW50%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;541&quot; height=&quot;297&quot; data-origin-width=&quot;541&quot; data-origin-height=&quot;297&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결과&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;테스트를 해보니 이슈도 잘 등록되고 이슈 발생에 따라 슬랙에도 알림이 와 있는 것을 확인했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;925&quot; data-origin-height=&quot;763&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cxRIMO/btrOs2OVLKq/GsnqAPbAcqt4TYgDhX6erk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cxRIMO/btrOs2OVLKq/GsnqAPbAcqt4TYgDhX6erk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cxRIMO/btrOs2OVLKq/GsnqAPbAcqt4TYgDhX6erk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcxRIMO%2FbtrOs2OVLKq%2FGsnqAPbAcqt4TYgDhX6erk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;925&quot; height=&quot;763&quot; data-origin-width=&quot;925&quot; data-origin-height=&quot;763&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;657&quot; data-origin-height=&quot;416&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bYL2UM/btrOq2PViGX/7AfSwe6zIGCLcTNGW4GYXK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bYL2UM/btrOq2PViGX/7AfSwe6zIGCLcTNGW4GYXK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bYL2UM/btrOq2PViGX/7AfSwe6zIGCLcTNGW4GYXK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbYL2UM%2FbtrOq2PViGX%2F7AfSwe6zIGCLcTNGW4GYXK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;657&quot; height=&quot;416&quot; data-origin-width=&quot;657&quot; data-origin-height=&quot;416&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>우아한테크코스/프로젝트-SMODY</category>
      <category>smody</category>
      <category>스모디</category>
      <category>우테코</category>
      <author>더즈</author>
      <guid isPermaLink="true">https://does-log.tistory.com/58</guid>
      <comments>https://does-log.tistory.com/58#entry58comment</comments>
      <pubDate>Wed, 12 Oct 2022 20:48:18 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] 동시성과 예외, 그리고 트랜잭션</title>
      <link>https://does-log.tistory.com/57</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;새로운 기능 추가에 대한 이야기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;스모디 서비스는 사람들에게 동기부여를 해주는 서비스로, 챌린지에 도전하여 인증을 하는 것이 주 콘텐츠이다. 단순한 인증을 넘어, 조금 더 사용자들에게 동기부여를 해주기 위해 랭킹에 관한 기능을 추가하기로 했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;랭킹 로직을 이벤트로 처리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;회원이 챌린지를 인증하면 주간 랭킹 점수에 반영 된다.라는 기능을 추가하려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;챌린지 인증 &amp;rarr; CycleService.increaseProgress()&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;랭킹 관련 로직 &amp;rarr; RankingService&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;랭킹 로직과 챌린지 로직이 강한 결합을 갖는 것을 막고 랭킹에 장애가 발생해도 챌린지 인증은 정상 동작시키기 위하여 이벤트(@TransactionalEventListener) + 비동기 처리(@Async)를 하기로 선택했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;( CycleService.increaseProgress() 메서드는 이미 이벤트를 발행하고 있기 때문에 CycleService의 코드 수정 없이 랭킹 기능을 추가할 수 있는 구조이다.)&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Transactional
public ProgressResponse increaseProgress(TokenPayload tokenPayload, ProgressRequest progressRequest) {
    // ...
    // 챌린지 인증 관련 로직
    // ...
    applicationEventPublisher.publishEvent(new CycleProgressEvent(cycle));
    return new ProgressResponse(cycle.getProgress());
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;왜 비동기 처리를 하였는가&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이벤트를 사용하기로 했더라도 동기적으로 실행할 수 있다. 하지만 동기적으로 실행하게 되면 위에서 언급한 &lt;i&gt;랭킹에 장애가 발생해도 챌린지 인증은 정상 동작 시키기&lt;/i&gt;가 할 수 없게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;랭킹에 장애가 발생하면 우리 서비스의 핵심 콘텐츠인 챌린지 인증을 할 수 없는 것인데 이렇게 할 수는 없다고 판단했다. 랭킹을 당장에 못 보더라도 핵심 기능은 정상 동작시키고 싶었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;결과적으로 챌린지 인증 이벤트가 발생했을 때 랭킹 로직을 아래와 같이 처리하도록 했다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@TransactionalEventListener // 이벤트를 발행하는 로직이 커밋된 뒤에 실행하도록
@Async(&quot;asyncExecutor&quot;) // 비동기 처리 (&quot;asyncExecutor&quot; 스레드 풀에서 스레드를 받아옴)
@Transactional // DB를 다루기에 트랜잭션 처리
public void handle(CycleProgressEvent event) {
    // event에서 가져온 정보를 토대로 특정 회원의 랭킹 점수에 관한 로직을 실행
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;CycleService.increaseProgress()는 CycleProgressEvent를 발행하고 위 메서드는 이 이벤트를 구독했다. CycleProgressEvent가 발행되면 위 로직이 실행되면서 랭킹 점수에 관한 로직을 실행할 것이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;랭킹 엔티티 구조&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;랭킹 기간: RankingPeriod
&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;랭킹 활동: RankingActivity
&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;size18&quot;&gt;랭킹 점수를 올리기 위해서는 RankingActivity(랭킹 활동)을 생성해야 하는데 RankingActivity는 인증 기간(RankingPeriod)과 회원(Member) 엔티티가 있어야 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;662&quot; data-origin-height=&quot;173&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kAIXW/btrNvqrdYms/R352KC4cngPnyGs1Z63RsK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kAIXW/btrNvqrdYms/R352KC4cngPnyGs1Z63RsK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kAIXW/btrNvqrdYms/R352KC4cngPnyGs1Z63RsK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkAIXW%2FbtrNvqrdYms%2FR352KC4cngPnyGs1Z63RsK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;662&quot; height=&quot;173&quot; data-origin-width=&quot;662&quot; data-origin-height=&quot;173&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;인증 기간이 항상 존재해야 하는 로직의 구현 방법&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;챌린지 인증에 따라 랭킹 점수를 집계하기 위해서는 항상 RankingPeriod는 존재해야 하며 현 정책상 RankingPeriod는 7일 간격으로 매주 월~일의 기간이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;RankingPeriod를 생성하는 api는 존재하지 않기 때문에 항상 존재해야한다는 정합성을 보장하기 위해 아래의 두 가지 방법을 생각했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;@Scheduled를 사용해 매주 일요일 밤 11시 55분에 다음 주 RankingPeriod를 생성한다.&lt;/li&gt;
&lt;li&gt;첫 사용자의 챌린지 인증이 발생했을 때 함께 생성하고, 그 뒤부터 인증하는 사용자들은 있는 RankingPeriod를 조회해서 사용한다. (사용자가 인증하지 않았으면 보여줄 랭킹이 없다고 판단)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;스케줄러를 사용하는 방법은 일요일 밤 등의 특정 시간에 장애가 발생하면 정합성이 안 맞는 문제가 발생하고 일요일 후에 배포를 하면 RakingPeriod를 직접 넣어줘야 하는 등 관리하기가 까다롭다고 판단했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;그래서 첫 인증 발생 요청을 판단하여 RankingPeriod를 생성하기로 결정했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;로직 흐름&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자가 인증을 한다.&lt;/li&gt;
&lt;li&gt;현재 시간에 해당하는RankingPeriod를 조회한다.&lt;/li&gt;
&lt;li&gt;있으면 사용해서 RankingActivity에 점수를 기록한다.&lt;/li&gt;
&lt;li&gt;없으면 RankingPeriod부터 만들고 만든 RankingPeriod로 RankingActivity를 만들어 점수를 기록한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;동시에 첫 인증이 발생했을 때 동시성 문제&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;RankingPeriod는 &amp;lsquo;시작 날짜&amp;rsquo;와 &amp;lsquo;기간&amp;rsquo;의 두 속성으로 판별된다. 같은 날에 시작해 같은 기간 동안 지속되는 2개의 RankingPeriod는 필요 없고 있어서도 안 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;243&quot; data-origin-height=&quot;234&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cfnIt7/btrNyFmYBzN/hvKGn09P2Nod1vcZLJBnm0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cfnIt7/btrNyFmYBzN/hvKGn09P2Nod1vcZLJBnm0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cfnIt7/btrNyFmYBzN/hvKGn09P2Nod1vcZLJBnm0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcfnIt7%2FbtrNyFmYBzN%2FhvKGn09P2Nod1vcZLJBnm0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;243&quot; height=&quot;234&quot; data-origin-width=&quot;243&quot; data-origin-height=&quot;234&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;때문에 동시에 두 사용자가 그 주의 첫 인증을 할 경우 같은 기간 동안 지속되는 RankingPeriod가 생기게 되는 동시성 문제가 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;실제로 랭킹 기간이 없으면 만들어서 랭킹 점수를 기록하는 비동기 메서드 2개를 한 번에 실행해서 테스트를 해보니 1개만 생성되어야 할 RankingPeriod가 2개 생성되는 것을 확인했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;698&quot; data-origin-height=&quot;257&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ohiAa/btrNvjeVWiv/51na1EQfS65xUFlvVARvXK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ohiAa/btrNvjeVWiv/51na1EQfS65xUFlvVARvXK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ohiAa/btrNvjeVWiv/51na1EQfS65xUFlvVARvXK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FohiAa%2FbtrNvjeVWiv%2F51na1EQfS65xUFlvVARvXK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;698&quot; height=&quot;257&quot; data-origin-width=&quot;698&quot; data-origin-height=&quot;257&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;해결 첫 번째 단계 - Unique 제약&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;시작 날짜와 기간의 복합 컬럼으로 유니크 제약을 설정했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;458&quot; data-origin-height=&quot;144&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bsMyCd/btrNzEOUWrN/YkHRbxgVkxRrsY503Qagu1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bsMyCd/btrNzEOUWrN/YkHRbxgVkxRrsY503Qagu1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bsMyCd/btrNzEOUWrN/YkHRbxgVkxRrsY503Qagu1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbsMyCd%2FbtrNzEOUWrN%2FYkHRbxgVkxRrsY503Qagu1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;458&quot; height=&quot;144&quot; data-origin-width=&quot;458&quot; data-origin-height=&quot;144&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이렇게 되면 한 트랜잭션에서 이 조합으로 이미 insert 쿼리를 날리면 (설령 커밋 전이라고 해도) 다른 트랜잭션에서는 이 조합으로 insert를 날리지 못하고 대기하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;먼저 insert를 한 트랜잭션이 커밋되면 두 번째 트랜잭션은 유니크 제약으로 인해 예외를 발생시키고, 롤백되면 정상 커밋된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;로그를 확인해 보자.&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;1. 두 스레드에서 동시에 RankingPeriod에 insert 쿼리 실행. 두 쿼리의 duration과 start_date는 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;405&quot; data-origin-height=&quot;428&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/WA9gr/btrNvxxetmU/6X7Lll6FSjbR40q29a7fM1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/WA9gr/btrNvxxetmU/6X7Lll6FSjbR40q29a7fM1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/WA9gr/btrNvxxetmU/6X7Lll6FSjbR40q29a7fM1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWA9gr%2FbtrNvxxetmU%2F6X7Lll6FSjbR40q29a7fM1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;405&quot; height=&quot;428&quot; data-origin-width=&quot;405&quot; data-origin-height=&quot;428&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;2. [async-pool1] 스레드가 빨랐는지 먼저 &lt;span style=&quot;color: #000000;&quot; data-token-index=&quot;2&quot; data-reactroot=&quot;&quot;&gt;RankingActivity&lt;/span&gt;를 만들어서 점수를 update 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;574&quot; data-origin-height=&quot;470&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qlBdr/btrNAN5Sr6N/H2oYFg82KwPYAN2mV9FodK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qlBdr/btrNAN5Sr6N/H2oYFg82KwPYAN2mV9FodK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qlBdr/btrNAN5Sr6N/H2oYFg82KwPYAN2mV9FodK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqlBdr%2FbtrNAN5Sr6N%2FH2oYFg82KwPYAN2mV9FodK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;574&quot; height=&quot;470&quot; data-origin-width=&quot;574&quot; data-origin-height=&quot;470&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;3. [async-pool1]가 커밋될 때까지 기다리던 [async-pool2]에서 이제야 유니크 제약으로 인한 예외가 발생한 것을 확인할 수 있다. 만약 [async-pool1]에서 롤백이 발생했다면 [async-pool2]은 insert에 성공했을 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;395&quot; data-origin-height=&quot;165&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oCbb0/btrNyFgdihb/bdJiJM8mjMdj88oDUBvjh0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oCbb0/btrNyFgdihb/bdJiJM8mjMdj88oDUBvjh0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oCbb0/btrNyFgdihb/bdJiJM8mjMdj88oDUBvjh0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoCbb0%2FbtrNyFgdihb%2FbdJiJM8mjMdj88oDUBvjh0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;395&quot; height=&quot;165&quot; data-origin-width=&quot;395&quot; data-origin-height=&quot;165&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;결과적으로 Unique 제약으로 lock을 걸지 않고도 한 주에 한 &lt;span style=&quot;color: #000000;&quot; data-token-index=&quot;1&quot; data-reactroot=&quot;&quot;&gt;RankingPeriod&lt;/span&gt;를 생성할 수 있게 되었다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;챌린지 인증과 랭킹 점수 간의 정합성 문제 발생&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;하지만 테스트는 여전히 실패하는데 내용은 아래와 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;571&quot; data-origin-height=&quot;104&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/baWAIF/btrNvMt4KQn/rEnQQrYXBAsIn1I2TH9xK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/baWAIF/btrNvMt4KQn/rEnQQrYXBAsIn1I2TH9xK0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/baWAIF/btrNvMt4KQn/rEnQQrYXBAsIn1I2TH9xK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbaWAIF%2FbtrNvMt4KQn%2FrEnQQrYXBAsIn1I2TH9xK0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;571&quot; height=&quot;104&quot; data-origin-width=&quot;571&quot; data-origin-height=&quot;104&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;첫 번째 트랜잭션에서 먼저 RankingPeriod를 insert 했기 때문에 두 번째 트랜잭션이 예외를 발생하는 상황은 RankingPeriod를 하나만 생성하지만 두 번째 트랜잭션은 예외를 맞고 롤백하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;그럼 두 번째 트랜잭션은 챌린지 인증은 했지만 점수는 사라지게 된다. 정책적으로 이는 말이 안 된다.&lt;/b&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;해결 두 번째 단계 - try/catch로 잃어버린 점수 찾기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;유니크 제약으로 인해 예외를 맞더라도 catch문 안에서 다른 스레드가 저장해준 RankingPeriod를 사용해 점수를 올릴 수 있도록 다음과 같이 코드를 수정했다.&lt;/p&gt;
&lt;pre id=&quot;code_1696671463324&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    @Async(&quot;asyncExecutor&quot;)
    @TransactionalEventListener
    public void handle(CycleProgressEvent event) {
        Cycle cycle = event.getCycle();
        Long memberId = cycle.getMember().getId();
        try {
            applyRankingPoint(cycle);
        } catch (DataIntegrityViolationException e) {
            /**
             * 여러 사람이 동시에 랭킹 점수에 관한 이벤트를 발생했을 경우
             * unique 제약조건으로 예외가 발생한 이벤트를 다시 한번 처리한다.
             */
            applyRankingPoint(cycle);
        }
    }
    
    private void applyRankingPoint(Cycle cycle) {
        List&amp;lt;RankingActivity&amp;gt; activities = findTargetActivities(cycle);
        updateActivities(cycle.getLatestCycleDetail(), activities);
    }
    
    // ...&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;해결 세 번째 단계 - JPA session 초기화&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;유니크 예외가 터지고 catch에서 랭킹 점수 로직을 다시 잘 실행해주길 원했으나 다른 예외가 또 발생했다.&lt;/p&gt;
&lt;pre id=&quot;code_1664618821130&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;org.hibernate.AssertionFailure: null id in com.woowacourse.smody.ranking.domain.RankingPeriod entry (don't flush the Session after an exception occurs)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;찾아보니 이는 세션에 남아 있는 엔티티 때문이다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;try 안에서 RankingPeriod를 insert 하려다가 DataIntegrityViolationException 발생&lt;/li&gt;
&lt;li&gt;hibernate는 예외가 발생하더라도 세션을 초기화(clear) 해주지 않음&lt;/li&gt;
&lt;li&gt;때문에 catch 안에서 jpa flush를 호출할 때 try 안에서 지워지지 않은 entity가 남아 있어 오류를 일으킴&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고: &lt;a href=&quot;https://juneyr.dev/hibernate-exception-does-not-flush&quot;&gt;https://juneyr.dev/hibernate-exception-does-not-flush&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;408&quot; data-origin-height=&quot;118&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ba7btI/btrNwGs9cZR/kRF7djdrgpSWfJuSIyiHH0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ba7btI/btrNwGs9cZR/kRF7djdrgpSWfJuSIyiHH0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ba7btI/btrNwGs9cZR/kRF7djdrgpSWfJuSIyiHH0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fba7btI%2FbtrNwGs9cZR%2FkRF7djdrgpSWfJuSIyiHH0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;408&quot; height=&quot;118&quot; data-origin-width=&quot;408&quot; data-origin-height=&quot;118&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세션을 강제로 초기화시켜주니 AssertionFailure 예외는 사라졌다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;UnexpectedRollbackException 발생&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세션까지 강제 초기화시켜주자 예상한 대로 흘러가는 듯했다. 로그를 보니&lt;/p&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;[async-pool1] 스레드는 RankingPeriod를 insert 하고 자신의 점수를 기록하고 커밋&lt;/li&gt;
&lt;li&gt;[async-pool2]에서 unique 예외가 발생&lt;/li&gt;
&lt;li&gt;catch 문으로 들어가 점수 로직 실행&lt;/li&gt;
&lt;li&gt;&lt;b&gt;UnexpectedRollbackException 발생&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;653&quot; data-origin-height=&quot;326&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ciDzCI/btrNAxveGos/KAPTrjRx4xs7niKh8XwBak/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ciDzCI/btrNAxveGos/KAPTrjRx4xs7niKh8XwBak/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ciDzCI/btrNAxveGos/KAPTrjRx4xs7niKh8XwBak/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FciDzCI%2FbtrNAxveGos%2FKAPTrjRx4xs7niKh8XwBak%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;653&quot; height=&quot;326&quot; data-origin-width=&quot;653&quot; data-origin-height=&quot;326&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;보면 [async-pool1]가 넣어준 RankingPeriod를 사용해 RankingActivity를 insert 하는 쿼리까지는 실행했다. 그런데 커밋을 하려고 하다가 UnexpectedRollbackException이 발생한 듯하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;(원래 update 쿼리까지 나가야 하는데 커밋 직전에 flush 하려다가 rollback이 발생한 듯)&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;@Transactional 어노테이션은 메서드 내에서 예외가 발생하면 롤백 처리를 하지만 try/catch로 예외를 잡아서 롤백하지 않을 줄 알았는데 아니었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;자세한 건 &lt;a href=&quot;https://techblog.woowahan.com/2606/&quot;&gt;응? 이게 왜 롤백되는 거지?를&lt;/a&gt; 읽어 보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;요약하면 &lt;b&gt;참여 중인 트랜잭션이 실패하면 그 트랜잭션은 전역적으로 rollback-only로 마킹되어 해당 트랜잭션을 재사용할 수 없다는 것이다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;설령 예외를 잡았다고 해도 로직을 실행하다 커밋하는 시점에 어? 롤백 마크 처리가 되어있네 하고 롤백해버린다는 것이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;해결 마지막 단계 - TransactionTemplate을 이용한 예외 처리&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;예외가 발생한 트랜잭션을 재사용할 수 없다면 트랜잭션을 분리하면 된다. 하지만 @Transactional 어노테이션을 사용하면서 트랜잭션을 분리하기 위해선 전파 속성을 조절할 필요가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;applyRankingPoint() 메서드의 전파 속성을 REQUIRES_NEW로 바꾸면 해결될 수도 있지만 AOP로 구현되어 있는 @Transactional의 특성상 클래스가 다른 클래스의 메서드를 호출하는 상황에서만 트랜잭션이 적용되기 때문에 사용할 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;그래서 @Transactional이 아닌 TransactionTemplate을 사용하기로 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;아래와 같이 어노테이션을 지우고 TransactionTemplate을 빈으로 주입받아 try와 catch 안에서 각각 사용해 catch 안의 트랜잭션을 try 안의 트랜잭션이 끝나고 실행되도록 하였다.&lt;/p&gt;
&lt;pre id=&quot;code_1696671575438&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    @Async(&quot;asyncExecutor&quot;)
    @TransactionalEventListener
    public void handle(CycleProgressEvent event) {
        Cycle cycle = event.getCycle();
        Long memberId = cycle.getMember().getId();
        try {
            applyRankingPointInTransaction(cycle);
        } catch (DataIntegrityViolationException e) {
            /**
             * 여러 사람이 동시에 랭킹 점수에 관한 이벤트를 발생했을 경우
             * unique 제약조건으로 예외가 발생한 이벤트를 새로운 트랜잭션으로 다시 한번 처리한다.
             */
            applyRankingPointInTransaction(cycle);
        }
    }

    private void applyRankingPointInTransaction(Cycle cycle) {
        transactionTemplate.executeWithoutResult(
                status -&amp;gt; {
                    List&amp;lt;RankingActivity&amp;gt; activities = findTargetActivities(cycle);
                    updateActivities(cycle.getLatestCycleDetail(), activities);
                }
        );
    }&lt;/code&gt;&lt;/pre&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;try 문 안에서 RankingPeriod가 없으면 생성하는 로직과 점수를 올리는 로직을 실행하게 된다.&lt;/li&gt;
&lt;li&gt;만약 동시성 문제 때문에 예외가 발생하여 catch 문으로 오게 되면 첫 번째 트랜잭션이 이미 RankingPeriod를 생성해 주었을 것이다.&lt;/li&gt;
&lt;li&gt;여기서 아무 로직도 실행하지 않으면 점수가 사라지겠지만 한 번 더 점수를 올리는 로직을 실행하여 다른 트랜잭션이 먼저 만들어준 RankingPeriod를 조회해 점수를 올린다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;catch 문에서는 이미 커밋된 RankingPeriod를 읽을 수 있게 되면서 정상적으로 다시 점수를 올릴 수 있게 된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;테스트 통과!!&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;535&quot; data-origin-height=&quot;95&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/TmDjO/btrNvPEpAoQ/1mpwYQxnMkO5JLVGO7QGzk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/TmDjO/btrNvPEpAoQ/1mpwYQxnMkO5JLVGO7QGzk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/TmDjO/btrNvPEpAoQ/1mpwYQxnMkO5JLVGO7QGzk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FTmDjO%2FbtrNvPEpAoQ%2F1mpwYQxnMkO5JLVGO7QGzk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;535&quot; height=&quot;95&quot; data-origin-width=&quot;535&quot; data-origin-height=&quot;95&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;특정 비즈니스 정책을 만족시키기 위해 비동기, 동시성, 트랜잭션 등등의 복합적인 상황들을 맞이하고 해결해 본 뜻깊은 경험이었으나 정말 머리를 많이 써서 힘들었다.. 사실 이 해결 방법이 최선의 해결 방법이 아닐 수도 있고 더 간단한 방법이 있을 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;하지만 일단 해결했다는 것에 의의를 둔다&amp;hellip;&lt;/p&gt;</description>
      <category>우아한테크코스/프로젝트-SMODY</category>
      <category>jpa</category>
      <category>동시성</category>
      <category>스프링</category>
      <category>우테코</category>
      <category>트랜잭션</category>
      <author>더즈</author>
      <guid isPermaLink="true">https://does-log.tistory.com/57</guid>
      <comments>https://does-log.tistory.com/57#entry57comment</comments>
      <pubDate>Sat, 1 Oct 2022 19:11:45 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] 알림 기능 비동기 처리하기</title>
      <link>https://does-log.tistory.com/56</link>
      <description>&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;a href=&quot;https://does-log.tistory.com/53&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;알림 적용기 1편&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;a href=&quot;https://does-log.tistory.com/54&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;알림 적용기 2편&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이것저것 알아봤으니 이제 프로젝트 코드에 알림 트랜잭션 분리를 적용해 보고자 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;챌린지 인증 임박 알림 트랜잭션 분리 테스트 작성&lt;/h2&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@DisplayName(&quot;알림 이벤트에 예외가 발생해도 새로운 사이클은 저장된다.&quot;)
@Test
void cycleCreate_pushEventException() throws InterruptedException {
		// given
		LocalDateTime now = LocalDateTime.now();

		willThrow(new RuntimeException(&quot;알림 로직에 예상치 못한 예외 발생!&quot;))
			.given(pushEventListener).handle(any(PushEvent.class));

		// when
		Long cycleId = cycleService.create(
			new TokenPayload(조조그린_ID),
			new CycleRequest(now, 스모디_방문하기_ID)
		);

		// then
		Optional&amp;lt;Cycle&amp;gt; cycle = cycleService.findById(cycleId);
		List&amp;lt;PushNotification&amp;gt; notifications = pushNotificationRepository.findAll();
		assertAll(
			() -&amp;gt; assertThat(cycle).isPresent(),
			() -&amp;gt; assertThat(notifications).isEmpty()
		);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;620&quot; data-origin-height=&quot;77&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DFKOR/btrMjWkp6op/2rVzmz65dUyokURbJeQLa1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DFKOR/btrMjWkp6op/2rVzmz65dUyokURbJeQLa1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DFKOR/btrMjWkp6op/2rVzmz65dUyokURbJeQLa1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDFKOR%2FbtrMjWkp6op%2F2rVzmz65dUyokURbJeQLa1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;620&quot; height=&quot;77&quot; data-origin-width=&quot;620&quot; data-origin-height=&quot;77&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;알림 이벤트 처리에 @TransactinalEventListenr와 전파 속성 REQUIRES_NEW로 처리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이전 포스팅에서 검증한 대로 테스트는 다 통과하였다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@TransactionalEventListener
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void handle(PushEvent event) {
    pushStrategies.get(event.getPushCase())
            .push(event.getEntity());
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;557&quot; data-origin-height=&quot;195&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nNMze/btrMldZO00O/bHbiqlcV9ErPJZfzcBJso0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nNMze/btrMldZO00O/bHbiqlcV9ErPJZfzcBJso0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nNMze/btrMldZO00O/bHbiqlcV9ErPJZfzcBJso0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnNMze%2FbtrMldZO00O%2FbHbiqlcV9ErPJZfzcBJso0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;557&quot; height=&quot;195&quot; data-origin-width=&quot;557&quot; data-origin-height=&quot;195&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;알림 로직에서 병목이 발생하면 어떻게 될까?&lt;/h2&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@TransactionalEventListener
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void handle(PushEvent event) {
        try {
            Thread.sleep(100000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        pushStrategies.get(event.getPushCase())
                .push(event.getEntity());
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;다른 트랜잭션이라도 한 스레드가 동기적으로 실행하는 일이기 때문에 사이클도 저장되는 데 한참 걸린다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;629&quot; data-origin-height=&quot;292&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ekD4tA/btrMnKW5wdq/TjceKmmJHY0M8uujz1tSx1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ekD4tA/btrMnKW5wdq/TjceKmmJHY0M8uujz1tSx1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ekD4tA/btrMnKW5wdq/TjceKmmJHY0M8uujz1tSx1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FekD4tA%2FbtrMnKW5wdq%2FTjceKmmJHY0M8uujz1tSx1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;629&quot; height=&quot;292&quot; data-origin-width=&quot;629&quot; data-origin-height=&quot;292&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;비동기 처리를 해야할 필요를 느꼈다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;비동기 처리&lt;/h2&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@SpringBootApplication
@EnableScheduling
@EnableJpaAuditing
@EnableAsync
public class SmodyApplication {&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Async
@TransactionalEventListener
public void handle(PushEvent event) {
        try {
            Thread.sleep(100000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        pushStrategies.get(event.getPushCase())
                .push(event.getEntity());
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이제 아예 다른 스레드에서 실행되기 때문에 트랜잭션 분리도 자연스럽게 될 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;테스트도 순식간에 도는 것을 확인했지만 알림 처리 스레드가 Thread.sleep() 때문에 늦게 동작하면서 알림이 저장되지 않아 알림 저장 테스트들이 실패했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;비동기 처리가 되는 것을 확인했으니Thread.sleep() 코드를 제거하고 다시 돌려봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;그럼에도 잘 되던 테스트들이 실패했다.&lt;/b&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;비동기 테스트는 어떻게?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;디버그를 찍어 보니 알림 스레드가 알림을 저장하기도 전에 main 스레드가 알림이 저장되었는지 검증을 해버려서 테스트가 실패하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;참고:&lt;a href=&quot;https://stackoverflow.com/questions/42438862/junit-testing-a-spring-async-void-service-method&quot;&gt;https://stackoverflow.com/questions/42438862/junit-testing-a-spring-async-void-service-method&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;위 링크의 방법대로 모든 활성 스레드를 종료시킨 후 검증을 다시 해보았다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;498&quot; data-origin-height=&quot;86&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/chRoQE/btrMjDrT6cy/4qSGKEUHRBL8u2gtkuRXsK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/chRoQE/btrMjDrT6cy/4qSGKEUHRBL8u2gtkuRXsK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/chRoQE/btrMjDrT6cy/4qSGKEUHRBL8u2gtkuRXsK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FchRoQE%2FbtrMjDrT6cy%2F4qSGKEUHRBL8u2gtkuRXsK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;498&quot; height=&quot;86&quot; data-origin-width=&quot;498&quot; data-origin-height=&quot;86&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;964&quot; data-origin-height=&quot;291&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cIJsW7/btrMoR2KjbX/06Tk4ilkUYKhSk03H8I46K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cIJsW7/btrMoR2KjbX/06Tk4ilkUYKhSk03H8I46K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cIJsW7/btrMoR2KjbX/06Tk4ilkUYKhSk03H8I46K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcIJsW7%2FbtrMoR2KjbX%2F06Tk4ilkUYKhSk03H8I46K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;964&quot; height=&quot;291&quot; data-origin-width=&quot;964&quot; data-origin-height=&quot;291&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;607&quot; data-origin-height=&quot;106&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bYtJF4/btrMkgpqwu8/NTsYejmUJugEj9cw0kgKA1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bYtJF4/btrMkgpqwu8/NTsYejmUJugEj9cw0kgKA1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bYtJF4/btrMkgpqwu8/NTsYejmUJugEj9cw0kgKA1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbYtJF4%2FbtrMkgpqwu8%2FNTsYejmUJugEj9cw0kgKA1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;607&quot; height=&quot;106&quot; data-origin-width=&quot;607&quot; data-origin-height=&quot;106&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;사이클과 알림 모두 저장하는 테스트가 비동기 처리에서도 통과하는 것을 확인할 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;비동기로 인해 JPA 지연 로딩을 하지 못하는 경우 발생&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;위 테스트는 통과했지만 통과하지 않는 테스트가 생겼다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;LazyInitializationException은 JPA 관련 예외로 프록시 객체를 지연 로딩하지 못할 때 발생한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;147&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/o0i85/btrMlVR9x1o/MDbwyIqpjKKTZnPb4WseaK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/o0i85/btrMlVR9x1o/MDbwyIqpjKKTZnPb4WseaK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/o0i85/btrMlVR9x1o/MDbwyIqpjKKTZnPb4WseaK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fo0i85%2FbtrMlVR9x1o%2FMDbwyIqpjKKTZnPb4WseaK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;147&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;147&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Override
@Transactional
public void push(Object entity) {
        Cycle cycle = (Cycle) entity;
        deleteInCompleteNotificationIfSamePathIdPresent(cycle);
        if (cycle.isSuccess()) {
            return;
        }
        pushNotificationService.register(buildNotification(cycle));
}

//... 나중에 cycle.getChallenge().getName()을 호출할 때 예외 발생!&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;EventListenr는 이벤트를 수신하고 알맞은 알림 전략 클래스의 push() 메서드를 호출한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이 때 관련 엔티티를 전달받는데 이 메서드가 호출되는 건 저 엔티티를 생성한 스레드와 다른 스레드다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;즉 저 엔티티는 영속성 컨텍스트의 관리를 받지 못하는 준영속 상태인 것이다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;Cycle cycle = cycleService.search(((Cycle) entity).getId());
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;그래서 CycleService를 의존하여 다시 조회하도록 하니 테스트가 통과하였다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;860&quot; data-origin-height=&quot;602&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/U3rDz/btrMkM9gCiL/xkBaPH4JqbtfMvBOpY6eTk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/U3rDz/btrMkM9gCiL/xkBaPH4JqbtfMvBOpY6eTk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/U3rDz/btrMkM9gCiL/xkBaPH4JqbtfMvBOpY6eTk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FU3rDz%2FbtrMkM9gCiL%2FxkBaPH4JqbtfMvBOpY6eTk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;860&quot; height=&quot;602&quot; data-origin-width=&quot;860&quot; data-origin-height=&quot;602&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;어노테이션 단 2개로 비동기 처리를 할 수 있었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;알림 이벤트를 발생시키는 곳과 이벤트를 처리하는 곳의 스레드가 달라지면서 테스트하기가 조금 힘들어지거나 jpa 관련 이슈가 있었지만 다행히 금방 해결할 수 있었다.&lt;/p&gt;</description>
      <category>우아한테크코스/프로젝트-SMODY</category>
      <category>Event</category>
      <category>jpa</category>
      <category>smody</category>
      <category>Spring</category>
      <category>비동기</category>
      <category>스레드</category>
      <category>우테코</category>
      <author>더즈</author>
      <guid isPermaLink="true">https://does-log.tistory.com/56</guid>
      <comments>https://does-log.tistory.com/56#entry56comment</comments>
      <pubDate>Sat, 17 Sep 2022 18:38:04 +0900</pubDate>
    </item>
    <item>
      <title>레벨 4 톰캣 만들기 미션 서버 소켓 코드 분석</title>
      <link>https://does-log.tistory.com/55</link>
      <description>&lt;p data-ke-size=&quot;size18&quot;&gt;레벨 4로 접어들면서 톰캣 만들기 미션을 하게 되었는데 제공받은 &lt;a href=&quot;https://github.com/ldk980130/jwp-dashboard-http&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;초기 코드&lt;/a&gt;부터가 흥미로웠다.&lt;/p&gt;
&lt;pre id=&quot;code_1662186337155&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Application {

    private static final Logger log = LoggerFactory.getLogger(Application.class);

    public static void main(String[] args) {
        log.info(&quot;web server start.&quot;);
        final var tomcat = new Tomcat();
        tomcat.start();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;위 코드만 실행 시켜도 자바 애플리케이션이 꺼지지도 않으면서 localhost:8080으로 오는 요청을 받고 있는 것이 신기해서 소스 코드를 한 번 분석해 보았다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Application {&lt;/h2&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;final var tomcat = new Tomcat();
tomcat.start();&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Tomcat 클래스 구동 (start())&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Tomcat {&lt;/h2&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;public void start() {
        var connector = new Connector();
        connector.start();

        try {
            // make the application wait until we press any key.
            System.in.read();
        } catch (IOException e) {
            log.error(e.getMessage(), e);
        } finally {
            log.info(&quot;web server stop.&quot;);
            connector.stop();
        }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Connector 객체 생성 및 구동&lt;/li&gt;
&lt;li&gt;콘솔에 입력이 들어올 때까지 대기 (애플리케이션을 중단시키지 않기 위해)&lt;/li&gt;
&lt;li&gt;예외가 발생하면 Connector 작동 중지&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Connector {&lt;/h2&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class Connector implements Runnable {

    private static final int DEFAULT_PORT = 8080;
    private static final int DEFAULT_ACCEPT_COUNT = 100;

    private final ServerSocket serverSocket;
    private boolean stopped;

    public Connector() {
        this(DEFAULT_PORT, DEFAULT_ACCEPT_COUNT);
    }

    public Connector(final int port, final int acceptCount) {
        this.serverSocket = createServerSocket(port, acceptCount);
        this.stopped = false;
    }

    private ServerSocket createServerSocket(final int port, final int acceptCount) {
        try {
            final int checkedPort = checkPort(port);
            final int checkedAcceptCount = checkAcceptCount(acceptCount);
            return new ServerSocket(checkedPort, checkedAcceptCount);
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Connector는 Runnable 인터페이스를 구현하고 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Runnable 인터페이스를 구현하는 클래스는 run() 메서드를 재정의 해야 한다.&lt;/li&gt;
&lt;li&gt;Runnable 구현 클래스가 스레드를 생성하는 데 사용되는 경우 스레드를 시작하면 별도의 스레드에서 run() 메서드가 호출된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;ServerSocket과 현재 중단 상태인지를 나타내는 stopped 필드를 가지고 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ServerSocket은 서버 소켓의 구현 클래스이다. 서버 소켓은 네트워크로부터 요청이 오기를 기다린다.&lt;/li&gt;
&lt;li&gt;요청이 오면 작동되고, 결과를 반환해 준다.&lt;/li&gt;
&lt;li&gt;위 코드에서는 기본 port와 acceptCount로 서버 소켓을 생성하고 있다. (기본 생성자를 사용했기 때문)&lt;/li&gt;
&lt;li&gt;위 코드로 생성된 소켓은 지정된 포트에 바인딩되며 acceptCount만큼의 최대 큐 길이를 가질 수 있다. 큐가 가득 찼을 때는 연결이 거부된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;start() {&lt;/h3&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;var thread = new Thread(this);
thread.setDaemon(true);
thread.start();
stopped = false;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Tomcat 클래스에서 connector.start() 메서드를 호출했으니 위 코드가 실행된다.&lt;/li&gt;
&lt;li&gt;Connector 자신으로 스레드를 생성했기에 thread.start() 메서드에 의해 connector.run() 메서드가 호출될 것이다.&lt;/li&gt;
&lt;li&gt;thread.setDemon(true)로 의해 위 스레드는 데몬 스레드가 된다.
&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;애플리케이션이 종료될 때 일반 스레드 전부가 종료되어야 JVM 프로세스가 종료된다.&lt;/li&gt;
&lt;li&gt;하지만 JVM은 데몬 스레드의 종료를 기다리지 않기 때문에 데몬 스레드를 그냥 셧다운 시킨다. 때문에 종료 시 특별한 처리가 필요한 작업이라면 데몬 스레드에서 실행하면 안 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;run() {&lt;/h3&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;@Override
public void run() {
    while (!stopped) {
        connect();
    }
}

private void connect() {
    try {
        process(serverSocket.accept());
    } catch (IOException e) {
        log.error(e.getMessage(), e);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스레드가 시작되었으니 run() 메서드가 호출되고 stopped 상태가 false이니 while 문이 돌면서 연결을 유지하게 된다.&lt;/li&gt;
&lt;li&gt;ServerSocket의 accept() 메서드를 호출하면 프로그램은 클라이언트와 포트로 연결될 때까지 대기한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;연결이 이루어지면 Socket 객체를 반환한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;process(final Socket connection) {&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;if (connection == null) {
    return;
}
log.info(&quot;connect host: {}, port: {}&quot;, connection.getInetAddress(), connection.getPort());
var processor = new Http11Processor(connection);
new Thread(processor).start();
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ServerSocket이 연결을 통해 반환한 Socket connection은 연결을 요청한 클라이언트로부터의 정보가 들어 있다.&lt;/li&gt;
&lt;li&gt;이 요청을 토대로 서버는 적절한 결과를 리턴해주면 된다.&lt;/li&gt;
&lt;li&gt;HTTP request 형식으로 왔을 정보를 알맞은 결과로 반한하기 위해 Runnable을 구현한 Http11Processor에게 connection을 넘겨 주어 새로운 스레드에서 요청을 처리하도록 넘긴다.&lt;/li&gt;
&lt;li&gt;그럼 Connector는 다시 while 문으로 돌아가 stopped가 true가 될 때까지 다른 요청을 기다리며 계속 들어오는 새로운 요청을 여러 스레드에게 처리하도록 맡길 것이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;while (!stopped) {
    connect();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Http11Processor {&lt;/h2&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;	@Override
	public void run() {
		process(connection);
	}

	@Override
	public void process(final Socket connection) {
		try (final var inputStream = connection.getInputStream();
			 final var outputStream = connection.getOutputStream();
			 final var requestReader = new BufferedReader(new InputStreamReader(inputStream))
		) {
			HttpRequest request = new HttpRequest(requestReader);
			log.info(&quot;REQUEST \r\n{}&quot;, request);

			final var response = handleHttpRequest(request);
			log.info(&quot;RESPONSE \r\n{}&quot;, response);

			outputStream.write(response.getBytes());
			outputStream.flush();
		} catch (IOException | UncheckedServletException e) {
			log.error(e.getMessage(), e);
		}
	}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이제 들어온 요청을 처리하는 로직을 살펴보자&lt;/li&gt;
&lt;li&gt;스레드로 생성하여 시작하였으니 똑같이 run(), process() 메서드가 차례로 실행될 것이다.&lt;/li&gt;
&lt;li&gt;Socket에서 InputStream과 OutputStream을 꺼낼 수 있는데 InputStream으로부터 요청 메시지를 읽어올 수 있고, OutputStream으로 알맞은 결과를 응답 메시지를 쓸 수 있다.&lt;/li&gt;
&lt;li&gt;BufferedReader를 통해 InputStream을 읽으면서 로직을 처리하여 OutStream.write()를 통해 결과를 반환하면 한 요청에 대한 프로세스가 종료가 된다.&lt;/li&gt;
&lt;li&gt;위 코드에 있는 HttpRequest와 HttpResponse 그리고 handleHttpRequest() 메서드는 필자가 설계해 본 구조인데 이런 흐름으로 요청을 처리하는 것이 톰캣 만들기 미션을 하면서 이해한 간단한 웹서버 작동 원리다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>우아한테크코스</category>
      <category>소켓</category>
      <category>우아한테크코스</category>
      <category>우테코</category>
      <category>톰캣</category>
      <author>더즈</author>
      <guid isPermaLink="true">https://does-log.tistory.com/55</guid>
      <comments>https://does-log.tistory.com/55#entry55comment</comments>
      <pubDate>Sat, 3 Sep 2022 15:30:41 +0900</pubDate>
    </item>
  </channel>
</rss>