<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>영호</title>
    <link>https://00h0.tistory.com/</link>
    <description>.</description>
    <language>ko</language>
    <pubDate>Fri, 3 Jul 2026 16:15:26 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>0h0</managingEditor>
    <image>
      <title>영호</title>
      <url>https://tistory1.daumcdn.net/tistory/5372316/attach/da2aa2925b6643849ad3e9a608846293</url>
      <link>https://00h0.tistory.com</link>
    </image>
    <item>
      <title>분산 환경에서 로컬 캐시의 정합성을 어떻게 보장할까</title>
      <link>https://00h0.tistory.com/112</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;size16&quot;&gt;글로벌&amp;nbsp;캐시를&amp;nbsp;사용하다보니&amp;nbsp;&amp;nbsp;분산환경에서 로컬캐시를 사용했을 때 어떻게 캐시 데이터 정합성을 맞출 수 있을지 고민해본 내용을 정리하려고 합니다.&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;b&gt;글에서 다룰 내용&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;로컬 캐시를 언제 활용할 수 있을지&lt;/li&gt;
&lt;li&gt;분산 환경에서 로컬 캐시의 정합성을 어떻게 맞출 수 있을지&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;로컬 캐시를 언제 활용할 수 있을까&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 로컬캐시를 &lt;b&gt;캐시 서버의 트래픽을 분산&lt;/b&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;size16&quot;&gt;데이터를 캐싱한다고 하면 redis 같은 별도의 캐시 서버를 많이 활용합니다. 하지만 글로벌 캐시만을 활용해 캐싱 작업을 진행한다면 데이터가 많아질수록 부하가 발생할 것입니다.&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;예를 들어, 조회요청이 많은 주식 종목이란 공통적인 데이터를 글로벌 캐시에 추가한다고 가정해보겠습니다. 이때, 기존 캐시 서버에 다른 데이터들도 캐싱되어 있을 경우 새롭게 추가된 주식 종목 캐싱으로 인해 캐시 서버에 부하가 발생할 수 있습니다. 이때, 해당 데이터를 각 WAS의 로컬 캐시에 관리할 경우 캐시 서버로의 트래픽을 줄일 수 있을 것 같습니다.&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;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캐시 종류&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캐시는 크게 글로벌 캐시, 로컬 캐시 2종류가 있습니다.&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;b&gt;글로벌 캐시&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;별도의 캐시서버를 두어 데이터를 조회하는 방식입니다. 대표적으로 redis가 있습니다.&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;b&gt;로컬 캐시&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WAS별로 데이터를 캐싱하여 사용하는 방식입니다. 이로 인해, 분산환경에서 사용 시 각 WAS별로 데이터 정합성을 맞춰줘야 되는 어려움이 있습니다.&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;b&gt;분산환경에서 로컬 캐시 주의점&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로컬 캐시는 각 WAS에서 데이터를 캐싱하고 있는 것이기 때문에 1번 WAS를 통해 원본 데이터에 수정이 발생할 경우 다른 WAS들의 캐시 데이터에도 이를 적용 해줘야됩니다. 즉, 데이터의 정합성을 맞춰줘야 됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;어떻게 정합성을 맞출 수 있을까&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2가지 정도 방법이 있을 것 같습니다. TTL 설정, invalidation message propagation&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;TTL 설정&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캐싱되어 있는 데이터가 &lt;b&gt;유효한 시간을 정해주는 방법&lt;/b&gt;입니다. TTL이 지난 데이터는 캐시에서 사라지고 추후 조회 요청이 발생하면 최신 데이터를 조회하여 캐시를 채우는 방식입니다. 해당 방법은 데이터가 주기적으로 바뀔 때 유용할 것 같습니다.&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;예를 들어, 어제의 게임 점수 랭킹을 보여주는 기능이 있다고 가정해보겠습니다. 그렇다면 해당 캐시 데이터의 TTL을 하루로 잡으면 날이 바뀔 때 마다 기존 캐시 데이터는 제거되고, 최신 데이터가 캐싱되면서 빠른 조회 성능을 챙길 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Invalidation Message Propagation&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 방법은 캐시 데이터에 수정이 발생할 경우 기존 캐시 데이터는 유효하지 않다는 것을 다른 WAS에 전파시켜 기존 캐시 데이터를 무효화시키는 방법입니다.&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;자바 진영의 cache 라이브러리 중 하나인 &lt;a href=&quot;https://github.com/ben-manes/caffeine/issues/256&quot;&gt;caffeine 측의 issue&lt;/a&gt;에 보면 분산 환경에서 어떻게 데이터를 동기화 하는지에 대한 질문이 있습니다. 이에 대한 대답으로 redis pub/sub 활용하면 좋다는 답변이 있습니다.&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;이처럼 데이터 수정이 발생한 WAS가 변경된 캐시 데이터의 key가 이제 유효하지 않다는 메시지를 다른 WAS에게 전파하는 방식입니다.&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;write-back, write-through&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가적으로 write-back, write-through 등의 방법도 있는데 이는 분산 환경의 로컬 캐시에서는 적합하지 않다고 생각합니다. 글로벌 캐시의 경우 여러 WAS가 동일한 캐시 서버를 바라보기 때문에 이 경우에는 가능할 것 같습니다.&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에 추가적인 변경이 발생합니다. 이때, 데이터 변경 요청을 받지 않은 서버들은 원본 DB의 변경사실을 모른채 계속해서 캐싱된 데이터를 내려줄 것입니다. 그렇기 때문에 분산 환경의 로컬 캐시에서는 다른 WAS들의 캐시 데이터를 무효화시킬 필요가 있어보입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Invalidation Message Propagation 예제 코드&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;employeeService&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Slf4j
@Service
@RequiredArgsConstructor
public class EmployeeService {

    private final EmployeeRepository employeeRepository;
    private final CacheManager cacheManager;
    private final RedisPublisher redisPublisher;

    @Transactional
    public Long save(EmployeeCreateDto employeeDto) {
        Employee employee = new Employee(employeeDto.getName(), employeeDto.getPhone(), employeeDto.getAge());
        return employeeRepository.save(employee).getId();
    }

    @Transactional
//    @CacheEvict(value = &quot;employees&quot;, allEntries = true) 단일 환경에서 활용 가능
    public Long modify(EmployeeModifyDto employeeDto) {
        Employee employee = employeeRepository.findById(employeeDto.getEmployeeId())
                .orElseThrow(() -&amp;gt; new IllegalArgumentException(&quot;Employee not found&quot;));

        employee.modify(employeeDto.getName(), employeeDto.getPhone());

				// TransactionalEventListener를 활용해 commit 이후 발행하도록 분리 가능
        redisPublisher.publish(ChannelTopic.of(&quot;employee-invalidation&quot;), employeeDto.getEmployeeId());
        return employee.getId();
    }

    @Transactional(readOnly = true)
    @Cacheable(&quot;employees&quot;)
    public List&amp;lt;Employee&amp;gt; find(Integer id) {
        return findEmployeeById(id);
    }
}

&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;write-back, write-through&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>DB</category>
      <author>0h0</author>
      <guid isPermaLink="true">https://00h0.tistory.com/112</guid>
      <comments>https://00h0.tistory.com/112#entry112comment</comments>
      <pubDate>Sun, 25 Aug 2024 01:45:27 +0900</pubDate>
    </item>
    <item>
      <title>@ConfigurationProperties 는 무엇을 기준으로 값을 주입할까</title>
      <link>https://00h0.tistory.com/111</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;size16&quot;&gt;최근에 같이 우테코를 수료한 지인과 각자 실무를 하면서 기본기를 다시 다지기 위해 Spring 스터디를 시작했습니다.&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;스터디원이 ConfigurationProperties 관련해 흥미로운 점을 알려줬고, 관련해서 조금 더 파본 내용을 간단하게 정리한 글입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실험을 진행한 환경은 Spring Boot 3.x 버전입니다.&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;b&gt;궁금한 점&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;꼭 설정 파일의 key 값과 java 코드의 필드명이 같아야 될까?&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HikariConfig 를 보면 maxPoolSize 란 필드가 있습니다. 그러나 설정파일 에서는 maximum-pool-size 키 값을 사용해 우리가 원하는 값을 주입 해줍니다. 즉, 설정 파일의 key 값과 자바 코드의 필드명이 달라도 매핑이 됩니다.&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;주목할 점은 2가지가 있습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;yml 파일의 key 값이 name 이 아닌 na 입니다.&lt;/li&gt;
&lt;li&gt;자바 코드의 필드명은 name 이지만, &lt;b&gt;파라미터 이름은 설정 파일의 키 값과 동일한 na&lt;/b&gt; 입니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;yaml&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;my:
  name: ee
  age: 5
  school:
    name: wooteco
    course: BE
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@ConfigurationProperties(&quot;my&quot;)
public class ConfigProperty {

    private final String name;
    private final Integer age;
    private final School school;

    public ConfigProperty(String na, Integer age, School school) {
        this.name = na;
        this.age = age;
        this.school = school;
    }

    @PostConstruct
    public void init() {
        System.out.println(&quot;school name: &quot; + age);
        System.out.println(&quot;school name: &quot; + name);
        System.out.println(&quot;school course: &quot; + school.course);

    }

    static class School {
        private String name;
        private String course;

        public School(String name, String course) {
            this.name = name;
            this.course = course;
        }
    }
}
&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;이렇게 실행해보면 name 필드에 ee 라는 값으로 정상적으로 매핑됩니다.&lt;b&gt;&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;size16&quot;&gt;내부 코드를 따라가다 보니 Binder 라는 클래스를 찾을 수 있었습니다.&lt;/p&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;설정 파일의 키 값을 기준으로 value 를 파싱한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;11.png&quot; data-origin-width=&quot;977&quot; data-origin-height=&quot;101&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/y62ja/btsIBbYrz0E/TRIywxkNIOVxpbhhhN8DX1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/y62ja/btsIBbYrz0E/TRIywxkNIOVxpbhhhN8DX1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/y62ja/btsIBbYrz0E/TRIywxkNIOVxpbhhhN8DX1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fy62ja%2FbtsIBbYrz0E%2FTRIywxkNIOVxpbhhhN8DX1%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;977&quot; height=&quot;101&quot; data-filename=&quot;11.png&quot; data-origin-width=&quot;977&quot; data-origin-height=&quot;101&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;2. 이를 주입해야 하는 target 클래스에 값을 바인딩한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;생성자 주입과, getter 와 setter 방식으로 주입하는 경우는 BindMethod 란 enum 에서 이를 구분하여 다르게 동작하고 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;yml 의 키 값을 토대로 생성자 parameter 이름과 매핑되어 값이 주입된다.&lt;/p&gt;</description>
      <category>Spring</category>
      <category>configurationproperties</category>
      <category>Java</category>
      <category>spring boot</category>
      <category>설정값</category>
      <author>0h0</author>
      <guid isPermaLink="true">https://00h0.tistory.com/111</guid>
      <comments>https://00h0.tistory.com/111#entry111comment</comments>
      <pubDate>Mon, 15 Jul 2024 23:39:24 +0900</pubDate>
    </item>
    <item>
      <title>[spring] Transactional outbox pattern 을 활용해 이벤트 유실 개선하기</title>
      <link>https://00h0.tistory.com/110</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;PR 링크: &lt;a title=&quot;링크&quot; href=&quot;https://github.com/youngh0/2023-stamp-crush/pull/3/files/f19dc9042caa2f180ac910f567f390bc6ed8d036..1c1199a244e384cb883ff52234ece60c952c23da&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;링크&lt;/a&gt;&lt;/b&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;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;부가 로직: 스탬프 적립 시 방문 기록 저장&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;기존 구조&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-04-29 오전 2.34.17.png&quot; data-origin-width=&quot;1668&quot; data-origin-height=&quot;482&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bx86aJ/btsGZMNiXt9/XKFkSPoKjdAJyXcLAZkO51/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bx86aJ/btsGZMNiXt9/XKFkSPoKjdAJyXcLAZkO51/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bx86aJ/btsGZMNiXt9/XKFkSPoKjdAJyXcLAZkO51/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbx86aJ%2FbtsGZMNiXt9%2FXKFkSPoKjdAJyXcLAZkO51%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;1668&quot; height=&quot;482&quot; data-filename=&quot;스크린샷 2024-04-29 오전 2.34.17.png&quot; data-origin-width=&quot;1668&quot; data-origin-height=&quot;482&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2번 과정(방문 기록 저장)이 실패하면 스탬프는 적립 되었지만  이에 해당하는 방문 기록이 없어져 데이터 정합성이 틀어집니다. 즉, 스탬프 적립 이벤트가 유실됩니다.&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;size16&quot;&gt;&lt;b&gt;[도메인 로직 완료 이벤트 유실]&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 설명했듯이 스탬프 적립 이벤트에 따른 부가기능을 수행하는 로직 실패 시 데이터 정합이 틀어지는 문제가 존재합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-04-29 오전 2.36.00.png&quot; data-origin-width=&quot;1656&quot; data-origin-height=&quot;362&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjt9WX/btsG1gzZeM5/59ISHQvIDTKpe7ir6AKlz0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjt9WX/btsG1gzZeM5/59ISHQvIDTKpe7ir6AKlz0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjt9WX/btsG1gzZeM5/59ISHQvIDTKpe7ir6AKlz0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbjt9WX%2FbtsG1gzZeM5%2F59ISHQvIDTKpe7ir6AKlz0%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;1656&quot; height=&quot;362&quot; data-filename=&quot;스크린샷 2024-04-29 오전 2.36.00.png&quot; data-origin-width=&quot;1656&quot; data-origin-height=&quot;362&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;p data-ke-size=&quot;size16&quot;&gt;이벤트 기반으로 프로젝트를 구성할 때 이벤트 유실을 방지하여 결과적 일관성을 맞추기 위해 Transactional outbox pattern 을 활용할 수 있습니다.&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;b&gt;결과적 일관성&lt;/b&gt;을 맞추기 위해 Transactional outbox pattern을 사용했습니다.&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;스탬프 적립 완료 이벤트를 저장하는 event outbox 테이블을 활용해 해당 이벤트의 부가로직 성공여부를 관리하고, 실패 이벤트가 있다면 이를 다시 처리하여 결과적 일관성을 맞추도록 했습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;1. 스탬프 적립 로직 수행&lt;/li&gt;
&lt;li&gt;1번 로직 커밋 이전에 event outbox 저장&lt;/li&gt;
&lt;li&gt;스탬프 적립 이벤트 발행&lt;/li&gt;
&lt;li&gt;방문 기록 저장
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;저장 성공 시 event outbox 는 true 로 업데이트&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;주기적으로 fail 상태의 event outbox 를 조회하여 해당 이벤트 재발행&lt;/li&gt;
&lt;/ul&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;size16&quot;&gt;아래 코드는 스탬프 생성 로직이 저장되기 전 스탬프 생성 이벤트를 저장합니다. BEFORE_COMMIT 을 활용하여 비즈니스 로직과, event outbox 저장을 원자적으로 관리합니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
@Transactional
public void saveStampAccumulateOutbox(StampCreateEvent stampCreateEvent) {
    UUID eventId = stampCreateEvent.getEventId();
    Long cafeId = stampCreateEvent.getCafeId();
    Long customerId = stampCreateEvent.getCustomerId();
    int stampCount = stampCreateEvent.getStampCount();

    StampAccumulateEventOutbox stampOutbox = new StampAccumulateEventOutbox(eventId, cafeId, customerId, stampCount);
    stampAccumulateEventOutboxRepository.save(stampOutbox);
}&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;아래 코드는 스탬프 생성 로직 커밋 이후 이에 따른 부가기능인 방문기록을 저장하고, outbox 의 상태를 성공으로 바꿉니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@TransactionalEventListener
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void createVisitHistory(StampCreateEvent stampCreateEvent) {
    Cafe cafe = findCafe(stampCreateEvent.getCafeId());
    Customer customer = findCustomer(stampCreateEvent.getCustomerId());
    VisitHistory visitHistory = new VisitHistory(cafe, customer, stampCreateEvent.getStampCount());
    visitHistoryRepository.save(visitHistory);

    StampAccumulateEventOutbox stampAccumulateEventOutbox = findStampCreateEvent(stampCreateEvent);
    stampAccumulateEventOutbox.success();
}&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;아래 코드는 스케줄링을 통해 fail 상태의 outbox 를 조회하고 재발행합니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@Transactional(readOnly = true)
@Scheduled(cron = &quot;*/10 * * * * *&quot;)
public void scheduledStampCreateEvent() {
    List&amp;lt;StampAccumulateEventOutbox&amp;gt; falseStampCreateEvents = stampAccumulateEventOutboxRepository.findByStateIsFalse();
    falseStampCreateEvents.forEach(event -&amp;gt; applicationEventPublisher.publishEvent(event));
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;구현 중 겪은 문제점&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&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;size16&quot;&gt;현재 이벤트를 저장하는 로직도 @TransactionalEventListener(BEFORE_COMMIT) 을 통해 비즈니스 로직과 분리한 상황입니다. 즉, 도메인 완료 이벤트를 발행하는 service 의 비즈니스 로직에선 이벤트의 식별자를 모르는 상황입니다.&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;도메인 이벤트 Listener 가 자신의 로직을 수행한 후 event 의 상태를 success 로 변경하기 위해선 해당 이벤트의 식별자가 있어야 됩니다. 예를 들어, 쿠폰 생성에 관한 이벤트라면 해당 이벤트를 식별하기 위해 생성된 쿠폰의 ID 를 쓸 수 있을것입니다.&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;그러나 현재 스탬프 적립의 경우 CASCADE.PERSIST 옵션을 통해 저장하고 있습니다. 즉 별도의 repository.save 를 호출하여 저장하지 않기 때문에 생성된 스탬프의 ID 를 알 수 없습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;쿠폰과 스탬프는 1 : N 관계입니다.&amp;nbsp;&lt;/blockquote&gt;
&lt;pre id=&quot;code_1714742659889&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
public class Coupon extends BaseDate {

	@Id
    @GeneratedValue(strategy = IDENTITY)
    private Long id;
    
    @OneToMany(mappedBy = &quot;coupon&quot;, fetch = LAZY, cascade = CascadeType.ALL)
    private List&amp;lt;Stamp&amp;gt; stamps = new ArrayList&amp;lt;&amp;gt;();
    
    // 나머지 코드
}&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;이를 해결하기 위해, Event 엔티티의 식별자의 경우 @GeneratedValue 를 사용하지 않고 UUID 를 통해 직접 식별자를 주입해서 해결했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1714742506391&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class StampAccumulateEventOutbox {

    @Id
    private UUID id;
    
    // 나머지 코드
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1714742706317&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class StampCreateEventCommand {

    private StampCreateEventCommand() {
    }

    public static StampCreateEvent createEvent(Coupon coupon, int stampCount) {
        return new StampCreateEvent(UUID.randomUUID(), coupon.getCafe().getId(), coupon.getCustomer().getId(), stampCount);
    }
}&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;이렇게 UUID 를 통해 이벤트 식별자를 만들고 이를 활용하여 문제를 해결했습니다.&lt;/p&gt;</description>
      <category>Spring</category>
      <category>Spring</category>
      <category>transactional outbox pattern</category>
      <author>0h0</author>
      <guid isPermaLink="true">https://00h0.tistory.com/110</guid>
      <comments>https://00h0.tistory.com/110#entry110comment</comments>
      <pubDate>Sat, 27 Apr 2024 23:54:54 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] 역할에 따른 멀티모듈 구성으로 프로젝트 개선하기</title>
      <link>https://00h0.tistory.com/109</link>
      <description>&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-filename=&quot;스크린샷 2024-04-11 오전 12.41.55.png&quot; data-origin-width=&quot;1776&quot; data-origin-height=&quot;864&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhuzyQ/btsGyeCkNWw/AxBTkGBTqEIKk1ir0GMikk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhuzyQ/btsGyeCkNWw/AxBTkGBTqEIKk1ir0GMikk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhuzyQ/btsGyeCkNWw/AxBTkGBTqEIKk1ir0GMikk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbhuzyQ%2FbtsGyeCkNWw%2FAxBTkGBTqEIKk1ir0GMikk%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;1776&quot; height=&quot;864&quot; data-filename=&quot;스크린샷 2024-04-11 오전 12.41.55.png&quot; data-origin-width=&quot;1776&quot; data-origin-height=&quot;864&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 프로젝트는 단일모듈로 구성되어 있어 코드 변경 시 전체 코드가 컴파일, 배포되는 구조입니다. 현재 프로젝트에서는 크게 2가지 영역이 있습니다.&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;spring mvc 를 활용한 어플리케이션 코드&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스케줄러의 코드 변경은 어플리케이션 코드에 전혀 영향이 없습니다. 그러나 단일 모듈 구조에서는 스케줄러 코드가 변경되어도 전체 코드가 컴파일, 배포 됩니다.&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;스케줄러 코드의 변경으로 전혀 영향이 없는 어플리케이션 코드도 재배포 됩니다.
&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;CI 속도가 불필요하게 오래 걸린다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스케줄러 코드 변경으로 인해 전체 코드 테스트 수행 &amp;rarr; 전체 코드 컴파일 &amp;rarr; 배포 되는 구조이기 때문에 스케줄러 모듈만 배포하는 것보다 CI 에 오랜 시간이 걸립니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&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;스케줄러와 어플리케이션 코드(spring mvc) 의 변경 주기는 다르다.&lt;/li&gt;
&lt;li&gt;스케줄러와 어플리케이션 코드(spring mvc) 의 역할은 명확하게 차이가 난다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 2가지 이유로 스케줄러 모듈과 앱 모듈을 분리하기로 했습니다. 스케줄러 모듈에서 사용하는 엔티티는 앱 모듈의 엔티티를 그대로 사용하기로 결정했습니다. 그 이유는 &lt;b&gt;스케줄러에서 주차장 정보를 갱신&lt;/b&gt;하고, &lt;b&gt;앱 모듈에서는 이 정보들을 각 요청에 맞게 조회&lt;/b&gt;하여 보여주기 때문에 분리할 필요가 없다고 판단했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 &lt;b&gt;'스케줄러 모듈 &amp;rarr; 앱 모듈'&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;size16&quot;&gt;그 구조는 아래와 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-04-11 오전 12.42.20.png&quot; data-origin-width=&quot;2012&quot; data-origin-height=&quot;794&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ClsGj/btsGwDiLEWH/ghXipru0Ev6gXj1Cd8bvK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ClsGj/btsGwDiLEWH/ghXipru0Ev6gXj1Cd8bvK0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ClsGj/btsGwDiLEWH/ghXipru0Ev6gXj1Cd8bvK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FClsGj%2FbtsGwDiLEWH%2FghXipru0Ev6gXj1Cd8bvK0%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;2012&quot; height=&quot;794&quot; data-filename=&quot;스크린샷 2024-04-11 오전 12.42.20.png&quot; data-origin-width=&quot;2012&quot; data-origin-height=&quot;794&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CI 를 위한 테스트 시간
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스케줄러 변경이 발생하면 해당 모듈만 다시 배포하면 되기 때문에 CI 시간이 단축됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;독립적으로 확장 가능한 구조
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이제 스케줄러 모듈과 앱 모듈은 독자적으로 확장될 수 있습니다.&lt;/li&gt;
&lt;li&gt;스케줄러 모듈의 경우 경기도의 주차장 정보를 추가하더라도 독립적으로 배포가 가능해 확장성을 개선할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;재배포 주기 개선
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;앱 모듈의 불필요한 재배포를 줄일 수 있습니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;처음 서버를 띄우면 JVM 에 클래스가 로드 되어야 하기 때문에 서버 성능이 떨어지는 시점이 존재합니다. 그래서 실제로 배포 후 웜업을 진행한다는 &lt;a href=&quot;https://www.youtube.com/watch?v=CQi3SS2YspY&quot;&gt;세미나&lt;/a&gt;도 존재합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Spring</category>
      <category>Spring</category>
      <category>멀티모듈</category>
      <author>0h0</author>
      <guid isPermaLink="true">https://00h0.tistory.com/109</guid>
      <comments>https://00h0.tistory.com/109#entry109comment</comments>
      <pubDate>Thu, 11 Apr 2024 00:43:49 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] TaskScheduler 를 활용해 만료된 인증코드 제거하기</title>
      <link>https://00h0.tistory.com/108</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;size16&quot;&gt;프로젝트를 진행하면서 인증코드를 redis 로 관리했습니다. 인증코드는 3분이란 유효시간이 있고 유효시간이 지난 인증코드로 인증 요청을 하면 실패해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;redis 는 메모리 기반이기 때문에 유효하지 않은 데이터는 제거하여 유효한 데이터만 저장하고 싶었기 때문에 만료된 인증코드를 어떻게 바로바로 제거할지 고민한 과정을 포스팅 할 예정입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;redis 의 Set ex 파라미터 활용&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;redis 에는 다양한 command 가 있고, 그 중 key 를 저장하면서 만료시간을 설정할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;redis 의 ttl 만료된 키 삭제 메커니즘&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;redis 는 만료된 키를 삭제하는 2가지의 메커니즘이 있습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;만료된 key 접근 시 삭제&lt;/li&gt;
&lt;li&gt;일정 주기로 ttl 이 설정된 키를 선정해 만료된 key 를 제거하고, 선정된 key 중 25% 이상이 만료되었으면 다시 반복&lt;/li&gt;
&lt;/ol&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://redis.io/commands/expire/#how-redis-expires-keys&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;공식문서&lt;/a&gt;에 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;개선하고 싶은 부분&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;redis ttl 만료 키 삭제 메커니즘으로 인해 만료된 key 가 redis 에 만료된 인증코드 데이터가 남아있을 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 이 부분이 마음에 들지 않았고 인증코드의 유효시간이 지나면 바로 삭제시키고 싶었습니다. 그래서 Spring 의 스케줄링 관련 내용을 찾아봤고, &lt;b&gt;TaskScheduler 를 활용&lt;/b&gt;해 인증코드의 유효시간이 지나면 바로 삭제하도록 구현했습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;만료된 key 순회하면서 제거하기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;keys&lt;/b&gt; 명령어를 통해 현재 존재하는 모든 key 를 조회하고, 이를 순회하며 접근해 만료되었다면 삭제시키는 방법을 고려했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나, keys 명령어는 O(N) 의 시간 복잡도를 가지기 때문에 싱글 스레드 기반인 redis 에서는 주의해야 되는 명령어입니다. 물론 현재 인증코드 데이터가 많지 않기 때문에 상관 없겠지만, 이왕이면 더 나은 방법으로 해결하고 싶어서 선택하지 않았습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;TaskScheduler 활용&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;인증코드가 생성되었을 때, 만료시간 이후 삭제 코드를 실행&lt;/b&gt;시키면 keys 명령어 없이도 제가 원하는 개선을 이룰 수 있을 것 같았습니다.&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;a href=&quot;https://docs.spring.io/spring-framework/reference/integration/scheduling.html#scheduling-task-scheduler&quot;&gt;Spring 의 TaskScheduler 문서&lt;/a&gt;를 보면 schedule() 메서드의 다양한 파라미터를 통해 스케줄링 태스크를 추가할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 중 &lt;b&gt;schedule(Runnable task, Instant startTime);&lt;/b&gt; 메서드를 활용하면 스케줄링 태스크가 추가되고 startTime 에 해당 태스크가 실행되도록 할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해, 인증코드 생성 이후, 만료시간이 되면 해당 인증코드를 삭제하는 명령어를 동적으로 실행시킬 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void scheduledAuthCodeRemove(AuthCodeCreateEvent authCodeCreateEvent) {
    String authCode = authCodeCreateEvent.getAuthCode();
    String destination = authCodeCreateEvent.getDestination();
    String authCodePlatform = authCodeCreateEvent.getAuthCodePlatform();
    String authCodeCategory = authCodeCreateEvent.getAuthCodeCategory();

    String authCodeKey = AuthCodeKeyConverter.convert(authCode, destination, authCodePlatform, authCodeCategory);
    // 스케줄 task 등록
    taskScheduler.schedule(() -&amp;gt; redisTemplate.delete(authCodeKey), Instant.now().plusSeconds(authCodeExpired));
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼 인증코드 생성 후 이를 저장하는 트랜잭션이 종료되면 해당 이벤트를 구독하는 쪽에서 인증코드 &lt;b&gt;key 를 삭제하는 스케줄링 태스크를 현재 시간 + 유효기간 에 실행되도록 추가&lt;/b&gt;하여 문제를 해결했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;최종 구조&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-03-12 오후 9.03.45.png&quot; data-origin-width=&quot;871&quot; data-origin-height=&quot;569&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/btQhSI/btsFJlp16ZD/IpgkkwduwKaW5ZE7oJa840/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/btQhSI/btsFJlp16ZD/IpgkkwduwKaW5ZE7oJa840/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/btQhSI/btsFJlp16ZD/IpgkkwduwKaW5ZE7oJa840/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbtQhSI%2FbtsFJlp16ZD%2FIpgkkwduwKaW5ZE7oJa840%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;871&quot; height=&quot;569&quot; data-filename=&quot;스크린샷 2024-03-12 오후 9.03.45.png&quot; data-origin-width=&quot;871&quot; data-origin-height=&quot;569&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>Spring</category>
      <category>redis</category>
      <category>scheduling</category>
      <category>Spring</category>
      <author>0h0</author>
      <guid isPermaLink="true">https://00h0.tistory.com/108</guid>
      <comments>https://00h0.tistory.com/108#entry108comment</comments>
      <pubDate>Tue, 12 Mar 2024 16:44:29 +0900</pubDate>
    </item>
    <item>
      <title>스탬프 중복 적립 개선기</title>
      <link>https://00h0.tistory.com/107</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;size16&quot;&gt;전에 참여했던 카페 쿠폰 서비스의 쿠폰에 스탬프를 적립하는 로직에서 동시성 이슈를 발견하여 이를 해결하기 위해 고민한 과정을 정리하려고 합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;현재 문제점&lt;/b&gt;&lt;/h2&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;size16&quot;&gt;스탬프 적립 요청 시 &lt;b&gt;{어느 쿠폰}&lt;/b&gt;에 &lt;b&gt;{몇 개의 스탬프}&lt;/b&gt;를 적립 할지, 인증 토큰에 대한 인자만 받고 있습니다. 이로 인해, 네트워크 지연 등에 대한 재시도로 인해 동일한 쿠폰에 대한 스탬프 적립 요청이 2번 들어오면 2배의 스탬프가 적립됩니다. 즉, &lt;b&gt;중복 적립 문제&lt;/b&gt;가 발생합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;기존 API&amp;nbsp; 대략적인 구조&lt;/b&gt;&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;POST coupon/{couponId} &lt;br /&gt;&lt;br /&gt;BODY &lt;br /&gt;int: stampCount&lt;b&gt;&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;원하는 결과&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흔히 말하는 동시성, 따닥 이슈 발생 시 하나의 요청만 유효하게 처리하길 원했습니다. 이를 만족하기 위해 어떤 고민을 했는지 작성해보겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;결론&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론부터 말하자면, &lt;b&gt;api 스펙 변화 + 분산락&lt;/b&gt;을 통해 스탬프 중복 적립 문제를 해결했습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;변경 후 API 구조&lt;/b&gt;&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;POST coupon/{couponID}&lt;br /&gt;&lt;br /&gt;BODY&lt;br /&gt;int: stampCount&lt;br /&gt;int: currentAccumulatedStamp&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;스탬프 적립 관련 엔티티 (Coupon, Stamp)&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Coupon 과 Stamp 엔티티는 1 : N 관계로 이루어져 있습니다. 이렇게 설계한 이유는 스탬프 적립 내역을 보여주기 위함입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Coupon&lt;/b&gt;&lt;/h3&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Entity
public class Coupon extends BaseDate {

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

    @ManyToOne(fetch = LAZY)
    @JoinColumn(name = &quot;customer_id&quot;)
    private Customer customer;

    @ManyToOne(fetch = LAZY)
    @JoinColumn(name = &quot;cafe_id&quot;)
    private Cafe cafe;

    @OneToMany(mappedBy = &quot;coupon&quot;, fetch = LAZY, cascade = CascadeType.PERSIST)
    private List&amp;lt;Stamp&amp;gt; stamps = new ArrayList&amp;lt;&amp;gt;();

		// 스탬프 적립 메서드
    public void accumulate(int earningStampCount) {
        for (int i = 0; i &amp;lt; earningStampCount; i++) {
            Stamp stamp = new Stamp();
            stamp.registerCoupon(this);
        }
        if (cafePolicy.isSameMaxStampCount(stamps.size())) {
            status = CouponStatus.REWARDED;
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Stamp&lt;/b&gt;&lt;/h3&gt;
&lt;pre class=&quot;scala&quot;&gt;&lt;code&gt;@Entity
public class Stamp extends BaseDate {

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

    @ManyToOne(fetch = LAZY)
    @JoinColumn(name = &quot;coupon_id&quot;)
    private Coupon coupon;

    public void registerCoupon(Coupon coupon) {
        this.coupon = coupon;
        coupon.getStamps().add(this);
    }
}
&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;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Service 로직&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 스탬프 적립 시 호출되는 service 로직입니다. 스탬프 적립에 필요한 부분만 간단하게 작성했습니다. 적립하려는 coupon 이 존재하는지 확인하고 존재한다면 coupon 에 스탬프를 적립합니다.&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;b&gt;쿠폰의 존재유무만 보고&lt;/b&gt; 존재한다면 바로 스탬프 적립로직을 수행합니다. 이로 인해 네트워크 지연 등의 이유로 동일한 쿠폰에 대한 &lt;b&gt;스탬프 적립 요청이 동시에 온다면 그대로 전부 다 적립&lt;/b&gt;됩니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@RequiredArgsConstructor
@Transactional
@Service
public class ManagerCouponCommandService {

	private final CouponRepository couponRepository;	

	public void createStamp(StampCreateDto stampCreateDto) {
				Coupon coupon = couponRepository.findById(stampCreateDto.getCouponId())
                .orElseThrow(IllegalArgumentException::new);
        // 쿠폰 존재 시 바로 스탬프 적립
        int earningStampCount = stampCreateDto.getEarningStampCount();

        coupon.accumulate(earningStampCount);       
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Getter
@RequiredArgsConstructor
public class StampCreateDto {

    private final Long ownerId;
    private final Long customerId;
    private final Long couponId;
    private final Integer earningStampCount;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span data-token-index=&quot;0&quot;&gt;중복 적립 &lt;/span&gt;해결 방법&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;api 의 request 스펙에 현재 쿠폰에 적립되어 있는 스탬프 개수를 받고, service 로직에서 이를 검증하는 로직을 추가하면 해결 될듯 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이 코드도 아직 &lt;b&gt;동시성 문제&lt;/b&gt;가 있어보입니다. 테스트를 통해 확인해보겠습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@RequiredArgsConstructor
@Transactional
@Service
public class ManagerCouponCommandService {

    private final CouponRepository couponRepository;	

    public void createStamp(StampCreateDto stampCreateDto) {
        Coupon coupon = couponRepository.findById(stampCreateDto.getCouponId())
        .orElseThrow(() -&amp;gt; new IllegalArgumentException(&quot;coupon not found&quot;));

        // 쿠폰에 적립되어 있는 스탬프 개수 비교
        if (coupon.getStampCount() != stampCreateDto.getCurrentStampCount()) {
            throw new IllegalArgumentException(&quot;incorrect stamp count&quot;);
        }
        int earningStampCount = stampCreateDto.getEarningStampCount();
        coupon.accumulate(earningStampCount);       
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Getter
@RequiredArgsConstructor
public class StampCreateDto {

    private final Long ownerId;
    private final Long customerId;
    private final Long couponId;
    private final Integer earningStampCount;
}
&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;size16&quot;&gt;스탬프가 0개인 쿠폰에 2개의 스탬프를 적립하려는 요청이 순차적으로 2번 발생하는 상황입니다. 제가 원하는 결과는 하나의 요청만 처리되어 2개의 스탬프가 적립되는 것입니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Test
void 스탬프를_적립한다() throws InterruptedException {
    // given
    Long couponId = 쿠폰_생성_요청하고_아이디_반환();
    
    // when
    int accumulatingStampCount = 2;
    int currentStampCount = 0;
		
    // 쿠폰에 쌓을 스탬프 개수, 현재 쿠폰에 적립된 스탬프 개수
    StampCreateRequest stampCreateRequest = new StampCreateRequest(accumulatingStampCount, currentStampCount);

    for (int i = 0; i &amp;lt; 2; i++) {
         쿠폰에_스탬프를_적립_요청(couponId, stampCreateRequest);
    }

    // then
    CustomerAccumulatingCouponFindResponse coupon = 고객의_쿠폰_조회하고_결과_반환(customerId, couponId);

    assertThat(coupon.getStampCount()).isEqualTo(2);
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;결과&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-02-22 오후 4.30.21.png&quot; data-origin-width=&quot;1055&quot; data-origin-height=&quot;346&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bfEBQI/btsFeEileaQ/x5b0DOOFf8ac8tw03L1CN0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bfEBQI/btsFeEileaQ/x5b0DOOFf8ac8tw03L1CN0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bfEBQI/btsFeEileaQ/x5b0DOOFf8ac8tw03L1CN0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbfEBQI%2FbtsFeEileaQ%2Fx5b0DOOFf8ac8tw03L1CN0%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;592&quot; height=&quot;194&quot; data-filename=&quot;스크린샷 2024-02-22 오후 4.30.21.png&quot; data-origin-width=&quot;1055&quot; data-origin-height=&quot;346&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기대한대로 2개가 적립되었고, 스탬프 개수가 일치하지 않아 예외가 발생한것을 예외 메시지를 통해 확인할 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;동시 요청 테스트&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번엔 순차 테스트와 원하는 결과는 같지만 요청이 동시에 온다는 차이점이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;0개가 적립되어 있는 쿠폰에 2개의 스탬프를 적립하려는 요청 2개가 동시에 들어온 상황입니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Test
void 스탬프를_적립한다() throws InterruptedException {
    // given
    Long couponId = 쿠폰_생성_요청하고_아이디_반환();
    // when
    int accumulatingStampCount = 2;
    int currentStampCount = 0;
		
    // 쿠폰에 쌓을 스탬프 개수, 현재 쿠폰에 적립된 스탬프 개수
    StampCreateRequest stampCreateRequest = new StampCreateRequest(accumulatingStampCount, currentStampCount);

    ExecutorService executor = Executors.newFixedThreadPool(2);
    CountDownLatch countDownLatch = new CountDownLatch(2);

    for (int i = 0; i &amp;lt; 2; i++) {
        executor.submit(() -&amp;gt; {
            try {
                쿠폰에_스탬프를_적립_요청(couponId, stampCreateRequest);
            }finally {
                countDownLatch.countDown();
            }
        });
    }
    countDownLatch.await();

    // then
    List&amp;lt;CustomerAccumulatingCouponFindResponse&amp;gt; coupons = 고객의_쿠폰_조회하고_결과_반환(ownerToken, savedCafeId, customerId);
    CustomerAccumulatingCouponFindResponse coupon = coupons.get(0);

    assertThat(coupon.getStampCount()).isEqualTo(10000);
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;결과&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-02-22 오후 4.35.19.png&quot; data-origin-width=&quot;936&quot; data-origin-height=&quot;310&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/TpnuO/btsFefpynTI/A2keakSc0krfiejDJltsdk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/TpnuO/btsFefpynTI/A2keakSc0krfiejDJltsdk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/TpnuO/btsFefpynTI/A2keakSc0krfiejDJltsdk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FTpnuO%2FbtsFefpynTI%2FA2keakSc0krfiejDJltsdk%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;592&quot; height=&quot;196&quot; data-filename=&quot;스크린샷 2024-02-22 오후 4.35.19.png&quot; data-origin-width=&quot;936&quot; data-origin-height=&quot;310&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기대와 다르게 4개가 적립되었습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;케이스 2&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-02-22 오후 5.59.42.png&quot; data-origin-width=&quot;940&quot; data-origin-height=&quot;288&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FXdD4/btsFbi2jMea/622fGiQ1rxmhqqJob0vhfK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FXdD4/btsFbi2jMea/622fGiQ1rxmhqqJob0vhfK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FXdD4/btsFbi2jMea/622fGiQ1rxmhqqJob0vhfK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFXdD4%2FbtsFbi2jMea%2F622fGiQ1rxmhqqJob0vhfK%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;592&quot; height=&quot;181&quot; data-filename=&quot;스크린샷 2024-02-22 오후 5.59.42.png&quot; data-origin-width=&quot;940&quot; data-origin-height=&quot;288&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-02-22 오후 6.01.52.png&quot; data-origin-width=&quot;938&quot; data-origin-height=&quot;274&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dcS0R4/btsFcQRmrgD/Hr7q3Tj0dIs7Jr5mwWAq10/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dcS0R4/btsFcQRmrgD/Hr7q3Tj0dIs7Jr5mwWAq10/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dcS0R4/btsFcQRmrgD/Hr7q3Tj0dIs7Jr5mwWAq10/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdcS0R4%2FbtsFcQRmrgD%2FHr7q3Tj0dIs7Jr5mwWAq10%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;592&quot; height=&quot;173&quot; data-filename=&quot;스크린샷 2024-02-22 오후 6.01.52.png&quot; data-origin-width=&quot;938&quot; data-origin-height=&quot;274&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번엔 좀 더 확실하게 10개의 동시 요청을 보내보겠습니다. 동일한 테스트 코드지만 어쩔때는 18개가 적립되었고, 어쩔때는 20개가 적립됩니다.&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-filename=&quot;스크린샷 2024-02-23 오전 12.13.04.png&quot; data-origin-width=&quot;983&quot; data-origin-height=&quot;602&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbnPiC/btsFaOmGwUW/E1r6TW8NoTc8rLiPafRXvk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbnPiC/btsFaOmGwUW/E1r6TW8NoTc8rLiPafRXvk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbnPiC/btsFaOmGwUW/E1r6TW8NoTc8rLiPafRXvk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbbnPiC%2FbtsFaOmGwUW%2FE1r6TW8NoTc8rLiPafRXvk%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;558&quot; height=&quot;342&quot; data-filename=&quot;스크린샷 2024-02-23 오전 12.13.04.png&quot; data-origin-width=&quot;983&quot; data-origin-height=&quot;602&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그림에서의 빨간 박스 부분을 잘 살펴봐야합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Thread1 의 트랜잭션이 COMMIT 되기 전에 Thread2 의 트랜잭션이 Coupon의 Stamp 개수를 읽기 때문에 2개의 스레드 모두 쿠폰의 스탬프 개수를 0으로 읽게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이로 인해, 2개의 스레드 모두유효성 검사를 통과하게 됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;동시성 문제 해결 방법&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. 트랜잭션 격리수준을 READ_COMMITED 로 낮춘다. (X)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션 격리수준을 낮추더라도 Thead1 이 insert 하기 전에 Thread2 에서 쿠폰의 스탬프 개수를 조회한다면 소용이 없습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. 낙관적 락 (X)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;낙관적 락의 경우 version 을 통해 동시성 문제를 관리합니다. 그러나 현재 저희의 구조는 Coupon 에서 컬럼으로 stamp 개수를 관리하는 것이 아닌 별도의 Stamp 엔티티와 1 : N 연관관계로 stamp 를 관리하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스탬프 적립 시 Stamp 테이블에 insert 가 발생하기 때문에 버전을 통해 관리하는 낙관적 락을 적용하기 여러운 구조입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3. 비관적 락 (X)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비관적 락의 경우 엔티티를 조회할 때 for update 구문을 추가하여 레코드의 인덱스에 X-lock 을 획득하여 동시성 문제를 관리합니다.&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;b&gt;MySQL 의 REPEATABLE READ 격리 수준에서는 팬텀 리드가 발생하지 않기 때문&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;size16&quot;&gt;아래 그림의 빨간 박스에서 Thread 2의 트랜잭션이 시작됩니다. MySQL 의 REPEATABLE_READ 에서는 MVCC 에 의해 트랜잭션 시작 시점의 데이터에서 값을 조회합니다. 이로 인해, Thread 1 에서 stamp 를 insert 하고 commit 하더라도 Thread 2 의 트랜잭션 안에서는 stamp 의 개수가 0개로 조회됩니다. 그래서 스탬프 적립 로직의 유효성 검증을 통과하면서 스탬프가 적립됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-02-23 오전 12.16.46.png&quot; data-origin-width=&quot;1261&quot; data-origin-height=&quot;734&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wiLnk/btsFeFVSsLY/zAxS57RVo0pNUyEEmgsBw0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wiLnk/btsFeFVSsLY/zAxS57RVo0pNUyEEmgsBw0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wiLnk/btsFeFVSsLY/zAxS57RVo0pNUyEEmgsBw0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwiLnk%2FbtsFeFVSsLY%2FzAxS57RVo0pNUyEEmgsBw0%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;663&quot; height=&quot;386&quot; data-filename=&quot;스크린샷 2024-02-23 오전 12.16.46.png&quot; data-origin-width=&quot;1261&quot; data-origin-height=&quot;734&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;이러한 문제로 인해 현재 stamp 를 insert 하여 적립하는 구조에서는 비관적 락으로도 동시성 문제를 해결하지 못합니다. 물론 트랜잭션 격리수준을 READ_COMMITED 로 낮추면 가능하지만, 팬텀 리드가 발생하기 때문에 선택하지 않았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;CouponRepository&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public interface CouponRepository extends Repository&amp;lt;Coupon, Long&amp;gt; {

    @Lock(LockModeType.PESSIMISTIC_WRITE)
    Optional&amp;lt;Coupon&amp;gt; findById(Long id);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;테스트 결과&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-02-23 오전 12.21.53.png&quot; data-origin-width=&quot;944&quot; data-origin-height=&quot;269&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/boXZPy/btsFbXQYikM/xyyck4KxeFpK0gviSDQSN0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/boXZPy/btsFbXQYikM/xyyck4KxeFpK0gviSDQSN0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/boXZPy/btsFbXQYikM/xyyck4KxeFpK0gviSDQSN0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FboXZPy%2FbtsFbXQYikM%2Fxyyck4KxeFpK0gviSDQSN0%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;592&quot; height=&quot;169&quot; data-filename=&quot;스크린샷 2024-02-23 오전 12.21.53.png&quot; data-origin-width=&quot;944&quot; data-origin-height=&quot;269&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4. 분산 락 (O)&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: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Coupon 조회&lt;/li&gt;
&lt;li&gt;Coupon 에 적립되어 있는 stamp 개수 조회 (stamp 테이블 접근)&lt;/li&gt;
&lt;li&gt;stamp 적립&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &lt;b&gt;여러 테이블에 접근하는 작업&lt;/b&gt;에 대한 동시성을 보장하기 위해 MySQL 의 NamedLock 을 활용했습니다.&lt;/p&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;NamedLock 획득&lt;/li&gt;
&lt;li&gt;Coupon 조회&lt;/li&gt;
&lt;li&gt;Coupon 에 적립되어 있는 stamp 개수 조회 (stamp 테이블 접근)&lt;/li&gt;
&lt;li&gt;stamp 적립&lt;/li&gt;
&lt;li&gt;NamedLock 반환&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분산락 관련 코드는 &lt;a href=&quot;https://techblog.woowahan.com/2631/&quot;&gt;우아한형제들 기술블로그&lt;/a&gt;를 참고 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 최종 적인 service 코드는 아래와 같습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@RequiredArgsConstructor
public class CouponCommandFacadeService {

    private final NamedLockService namedLockService;
    private final ManagerCouponCommandService managerCouponCommandService;

    public void createStamp(StampCreateDto stampCreateDto) {
        namedLockService.executeWithLock(
                &quot;create_stamp&quot; + stampCreateDto.getCouponId(), 
                3000, 
                () -&amp;gt;  managerCouponCommandService.createStamp(stampCreateDto)
        );
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;테스트 결과&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-02-23 오전 12.23.07.png&quot; data-origin-width=&quot;857&quot; data-origin-height=&quot;198&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bFSCax/btsFcDxTelE/AWBbuXBQgu40AGGEtjXTq1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bFSCax/btsFcDxTelE/AWBbuXBQgu40AGGEtjXTq1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bFSCax/btsFcDxTelE/AWBbuXBQgu40AGGEtjXTq1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbFSCax%2FbtsFcDxTelE%2FAWBbuXBQgu40AGGEtjXTq1%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;582&quot; height=&quot;134&quot; data-filename=&quot;스크린샷 2024-02-23 오전 12.23.07.png&quot; data-origin-width=&quot;857&quot; data-origin-height=&quot;198&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;size16&quot;&gt;현재 코드 구조를 유지하는 선에서 동시성 이슈를 해결하기 위해 다양한 고민을 해보고 해결 해봤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Spring</category>
      <category>JPA</category>
      <category>MySQL</category>
      <category>낙관적락</category>
      <category>동시성</category>
      <category>분산락</category>
      <category>비관적락</category>
      <author>0h0</author>
      <guid isPermaLink="true">https://00h0.tistory.com/107</guid>
      <comments>https://00h0.tistory.com/107#entry107comment</comments>
      <pubDate>Fri, 23 Feb 2024 00:24:52 +0900</pubDate>
    </item>
    <item>
      <title>AOP와 Multi-Datasource를 활용해 동시성 문제 해결해보기 2탄(feat. NamedLock)</title>
      <link>https://00h0.tistory.com/106</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;size16&quot;&gt;&lt;a href=&quot;https://00h0.tistory.com/105&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전 포스팅&lt;/a&gt;에서 구현한 namedLock 에 이어서 multi-datasource 와 AOP 를 적용하는 과정에 대해 적어보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/youngh0/spring-experiment/tree/master/src/main/java/com/example/experiment/mysqlnamedlock&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;전체 코드 github&lt;/a&gt;입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Multi-datasource 가 필요한 이유&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;namedLock 을 얻는 datasource 와 다른 비즈니스 로직을 수행하기 위한 datasource가 동일한 경우 &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;size16&quot;&gt;만약, namedLock 을 얻으려는 요청이 갑자기 몰린다고 생각해봅시다. 그러면 해당 요청에 할당된 커넥션은namedLock 을 얻기 위해 기다릴 것입니다. 물론 timeout 을 설정해서 대기 시간을 조절할 수 있지만, 해당 시간 동안 비즈니스 로직 수행에 필요한 커넥션을 사용한다는 사실은 변하지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 이유로 namedLock 을 얻고 해제하는 datasource 를 별도로 설정하면 커넥션 부족할 수 있다는 문제를 예방할 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Multi-datasource 분리하기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;application.yml&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;spring:
  datasource:
    main: // main datasource
      jdbc-url: jdbc:mysql://localhost/experiment
      username: root
      password: root  
	  pool-name: Spring-HikariPool-lock
    lock: // lock datasource
      jdbc-url: jdbc:mysql://localhost/experiment
      username: root
      password: root
			max-lifetime: 60000
      connection-timeout: 5000
      pool-name: Spring-HikariPool-lock-aop&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;이런 식으로 분리할 수 있습니다. 그리고 이를 통해 namedLock 을 얻기 위한 datasource 에는 별도의 connection-timeout 값과 커넥션 풀 사이즈 등의 설정을 별도로 지정할 수 있습니다.&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;이제 namedLock 을 얻는&lt;b&gt; LockRepository의 datasource&lt;/b&gt;에만 application.yml 에 주석으로 작성한 &lt;b&gt;lock datasource 를 주입&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;size16&quot;&gt;&lt;b&gt;DatasourceConfig.java&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Configuration
public class DatasourceConfig {

    @Primary
    @ConfigurationProperties(&quot;spring.datasource.main&quot;)
    @Bean
    public DataSource mainDatasource() {
        return DataSourceBuilder.create().type(HikariDataSource.class).build();
    }

    @ConfigurationProperties(&quot;spring.datasource.lock&quot;)
    @Bean
    public DataSource lockDatasource() {
        return DataSourceBuilder.create().type(HikariDataSource.class).build();
    }

    @Bean
    public LockRepository lockRepository() {
        return new LockRepository(lockDatasource());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@ConfigurationProperties 을 통해 application.yml 에 작성한 값을 인식해 &lt;b&gt;별도의 DataSource 빈을 등록&lt;/b&gt;합니다. 이후 namedLock 을 관리하는 &lt;b&gt;LockRepository빈 등록 시 lockDatasource 를 주입&lt;/b&gt;함으로써 별도의 Datasource 를 사용할 수 있습니다.&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;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;LockRepository.java&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가독성을 위해 로그를 제외한 코드는 지웠습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Slf4j
@RequiredArgsConstructor
public class LockRepository {

    private final DataSource dataSource;

    public void executeWithLock(String userLockName,
                                int timeoutSeconds,
                                Runnable runnable) {

        try (Connection connection = dataSource.getConnection()) {
            log.info(&quot;lock datasource: {}&quot;, dataSource);
            // namedLock 획득
			// 비즈니스 로직 수행
			// namedLock 반환
        } catch (SQLException | RuntimeException e) {
            throw new RuntimeException(e.getMessage(), e);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;LectureService.java&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@RequiredArgsConstructor
@Slf4j
@Service
public class LectureService {

    private final LectureRepository lectureRepository;
    private final LectureStudentRepository lectureStudentRepository;
    private final DataSource dataSource;

    @Transactional
    public void enrolmentWithNamedLock(Long lectureId, Long studentId) {
        log.info(&quot;main datasource: {}&quot;, dataSource);
        Lecture lecture = lectureRepository.findById(lectureId)
                .orElseThrow();

        if (lecture.isPossibleEnrolment()) {
            lectureStudentRepository.save(new LectureStudent(lectureId, studentId));
            lecture.decrease();
            return;
        }

        log.info(&quot;{} 수강신청 실패&quot;, Thread.currentThread().getName());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-11-15 오전 12.19.26.png&quot; data-origin-width=&quot;964&quot; data-origin-height=&quot;51&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bKppV1/btsAm2BLlX8/Z9UXMgzzM3Ny2ptZ0bPxPK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bKppV1/btsAm2BLlX8/Z9UXMgzzM3Ny2ptZ0bPxPK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bKppV1/btsAm2BLlX8/Z9UXMgzzM3Ny2ptZ0bPxPK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbKppV1%2FbtsAm2BLlX8%2FZ9UXMgzzM3Ny2ptZ0bPxPK%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;51&quot; data-filename=&quot;스크린샷 2023-11-15 오전 12.19.26.png&quot; data-origin-width=&quot;964&quot; data-origin-height=&quot;51&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;namedLock을 관리하는 LockRepository 와 비즈니스 로직을 수행하는 LectureService 가 별도의 히카리풀 을 사용하는 것을 &lt;b&gt;pool-name 이 다른 것을 통해 확인&lt;/b&gt;할 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;AOP 를 통해 손쉽게 namedLock 적용하기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;namedLock 을 통한 동시성 제어의 흐름을 생각해보면 &lt;b&gt;namedLock 획득 &amp;rarr; 비즈니스 로직 수행 및 커밋 &amp;rarr; namedLock 반환&lt;/b&gt; 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &lt;b&gt;동시성 제어가 필요한 메서드의 실행 전후로 namedLock 획득, 해제&lt;/b&gt;가 이루어지면 됩니다. 그렇다면 이를 AOP와 별도의 어노테이션 생성을 통해 동시성 제어가 필요한 메서드에 손쉽게 적용할 수 있을 것 같다는 생각이 들었습니다.&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;b&gt;NamedLock.annotation&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface NamedLock {

    String lockKey();

    int timeout() default 3000;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;namedLock 키이름은 무조건 입력받도록 하고, timeout은 기본 3000으로 설정 했습니다.&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;b&gt;NamedLockAop.java&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Slf4j
@RequiredArgsConstructor
@Aspect
@Order(Ordered.LOWEST_PRECEDENCE - 1)
public class NamedLockAop {

    private static final String GET_LOCK = &quot;SELECT GET_LOCK(?, ?)&quot;;
    private static final String RELEASE_LOCK = &quot;SELECT RELEASE_LOCK(?)&quot;;

    private final DataSource dataSource;

    @Around(&quot;@annotation(com.example.experiment.mysqlnamedlock.NamedLock)&quot;)
    public void lock(ProceedingJoinPoint joinPoint) {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        NamedLock namedLock = method.getAnnotation(NamedLock.class);

        String lockKey = namedLock.lockKey();
        int timeout = namedLock.timeout();
        log.info(&quot;lockKey: {}, timeout: {}&quot;, lockKey, timeout);
        try (Connection connection = dataSource.getConnection()) {
            log.info(&quot;lock with aop datasource: {}, thread: {}&quot;, dataSource, Thread.currentThread().getName());
            try {
                getLock(connection, lockKey, timeout);
                log.info(&quot;getLock= {}, timeoutSeconds = {}, connection = {}, thread: {}&quot;, lockKey, timeout, connection, Thread.currentThread().getName());
                joinPoint.proceed(); // 비즈니스 로직 수행
            } catch (Throwable e) {
                throw new RuntimeException(e);
            } finally {
                releaseLock(connection, lockKey);
                log.info(&quot;releaseLock = {}, connection = {}, thread = {}&quot;, lockKey, connection, Thread.currentThread().getName());
            }
        } catch (SQLException | RuntimeException e) {
            throw new RuntimeException(e.getMessage(), e);
        }
    }

    private void getLock(Connection connection,
                         String userLockName,
                         int timeoutseconds) throws SQLException {

        try (PreparedStatement preparedStatement = connection.prepareStatement(GET_LOCK)) {
            preparedStatement.setString(1, userLockName);
            preparedStatement.setInt(2, timeoutseconds);
            preparedStatement.executeQuery();
        }
    }

    private void releaseLock(Connection connection,
                             String userLockName) throws SQLException {
        try (PreparedStatement preparedStatement = connection.prepareStatement(RELEASE_LOCK)) {
            preparedStatement.setString(1, userLockName);
            preparedStatement.executeQuery();
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;namedLock 획득, 해제하는 과정은 &lt;a href=&quot;https://00h0.tistory.com/105&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전 포스팅&lt;/a&gt;과 동일합니다. 다만 AOP 적용을 통해 &lt;b&gt;@NamedLock&lt;/b&gt; 이 붙어있는 메서드를 탐지해 해당 메서드 실행 시 실행 전후로 namedLock 획득, 해제 하도록 구현했습니다.&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;b&gt;LectureService.java&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@RequiredArgsConstructor
@Slf4j
@Service
public class LectureService {

    private final LectureRepository lectureRepository;
    private final LectureStudentRepository lectureStudentRepository;
    private final DataSource dataSource;

    @NamedLock(lockKey = &quot;lecture_enrolment&quot;)
    @Transactional
    public void enrolmentWithNamedLockAndAop(Long lectureId, Long studentId) {
        Lecture lecture = lectureRepository.findById(lectureId)
                .orElseThrow();

        if (lecture.isPossibleEnrolment()) {
            lectureStudentRepository.save(new LectureStudent(lectureId, studentId));
            lecture.decrease();
            return;
        }

        log.info(&quot;{} 수강신청 실패&quot;, Thread.currentThread().getName());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼 동시성 제어가 필요한 메서드에 @NamedLock 을 통해 손쉽게 namedLock 적용이 가능해졌습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;테스트 해보기&lt;/b&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1699975317799&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
void 다중_요청_수강_신청() throws InterruptedException {
    int maxLectureStudentNum = 10;
    Long lectureId = 강의등록(maxLectureStudentNum);

    int studentsNum = 50;
    ExecutorService executorService = Executors.newFixedThreadPool(15);
    CountDownLatch countDownLatch = new CountDownLatch(studentsNum);

    for (int i = 0; i &amp;lt; studentsNum; i++) {
        executorService.submit(() -&amp;gt; {
            try {
                락과_AOP를_이용한_수강신청(lectureId);
            } finally {
                countDownLatch.countDown();
            }
        });
    }
    countDownLatch.await();

    int lectureStudentNum = 강의_수강생_조회(lectureId);
    assertThat(lectureStudentNum).isEqualTo(maxLectureStudentNum);
}

private void 락과_AOP를_이용한_수강신청(Long lectureId) {
    RestAssured.given()
            .contentType(JSON)
            .body(new LectureRequest(3L))
            .when()
            .post(&quot;/lecture/namedLock/aop/{lectureId}&quot;, lectureId)
            .then()
            .extract();
}

private int 강의_수강생_조회(Long lectureId) {
    ExtractableResponse&amp;lt;Response&amp;gt; extract = RestAssured.given()
            .contentType(JSON)
            .when()
            .get(&quot;/lecture/{lectureId}&quot;, lectureId)
            .then()
            .extract();

    return extract.response().body().as(Integer.class);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-11-15 오전 12.22.08.png&quot; data-origin-width=&quot;528&quot; data-origin-height=&quot;58&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/t3Z0Q/btsAjnmymiE/oLfNYh6sE341fCb1d77Y1k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/t3Z0Q/btsAjnmymiE/oLfNYh6sE341fCb1d77Y1k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/t3Z0Q/btsAjnmymiE/oLfNYh6sE341fCb1d77Y1k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Ft3Z0Q%2FbtsAjnmymiE%2FoLfNYh6sE341fCb1d77Y1k%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;528&quot; height=&quot;58&quot; data-filename=&quot;스크린샷 2023-11-15 오전 12.22.08.png&quot; data-origin-width=&quot;528&quot; data-origin-height=&quot;58&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;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;주의점&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@NamedLock, @Transactional 모두 AOP 를 이용하고 있기 때문에 어느 AOP가 먼저 수행되느냐는 매우 중요합니다. 현재 저희는&lt;b&gt; 반드시 @NamedLock 어노테이션이 먼저 수행&lt;/b&gt;되어야 합니다.&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;만약, @Transactional 이 먼저 수행되면 다음과 같은 문제점이 있습니다.&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션 시작 &amp;rarr; namedLock 획득 &amp;rarr; 비즈니스 로직 수행(커밋 X) &amp;rarr; namedLock 해제 &amp;rarr; 비즈니스 로직 커밋&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;768&quot; data-origin-height=&quot;514&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/njVXe/btsAjiMncFb/ofyjjx119Fzagkp99ubRP0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/njVXe/btsAjiMncFb/ofyjjx119Fzagkp99ubRP0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/njVXe/btsAjiMncFb/ofyjjx119Fzagkp99ubRP0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnjVXe%2FbtsAjiMncFb%2Fofyjjx119Fzagkp99ubRP0%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;768&quot; height=&quot;514&quot; data-origin-width=&quot;768&quot; data-origin-height=&quot;514&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;이런 흐름으로 동작하면 전혀 &lt;b&gt;동시성 제어가 안됩니다.&lt;/b&gt; 왜냐하면 &lt;b&gt;namedLock이 해제되고 커밋 전에 컨텍스트 스위칭&lt;/b&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;size16&quot;&gt;그래서 &lt;b&gt;NamedLockAop.java&lt;/b&gt; 를 보면 @Order 를 통해 @Transactional 보다 먼저 수행되도록 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-framework/reference/data-access/transaction/declarative/annotations.html&quot;&gt;공식문서&lt;/a&gt;를 보면 @Transactional 의 order값은 Ordered.LOWEST_PRECEDENCE 로 되어 있다고 나와있습니다. 그래서 &lt;b&gt;NamedLockAop의 @Order 는 Ordered.LOWEST_PRECEDENCE - 1 로 설정&lt;/b&gt;했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1137&quot; data-origin-height=&quot;260&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bZQnxq/btsAmCXxMA6/7myetMxNQLwPKyOVuTIdn1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bZQnxq/btsAmCXxMA6/7myetMxNQLwPKyOVuTIdn1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bZQnxq/btsAmCXxMA6/7myetMxNQLwPKyOVuTIdn1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbZQnxq%2FbtsAmCXxMA6%2F7myetMxNQLwPKyOVuTIdn1%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;1137&quot; height=&quot;260&quot; data-origin-width=&quot;1137&quot; data-origin-height=&quot;260&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;size16&quot;&gt;Multi-datasource 를 통해 커넥션이 부족할 수 있는 상황을 예방하고 AOP 를 통해 손쉽게 namedLock 을 적용해봤습니다.&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;namedLock 을 구현하면서 느낀 점은 커넥션 풀의 개수, timeout 시간 등 설정 값을 팀원과 협의하에 적절한 값으로 설정하는 것이 중요해보입니다. timeout 을 너무 길게 설정하면 커넥션 고갈 문제가 발생할 수 있기 때문입니다.&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;그리고 특정 자원에 대한 동시성이 요구된다면 비관적 락이나 낙관적 락도 충분히 고려할만 한 것 같습니다. 다만, namedLock 은 존재하지 않는 자원 즉 insert 되는 작업들에 대한 동시성 처리가 가능하기 때문에 이를 고려하여 동시성 제어 방법을 선택하면 좋아보입니다.&lt;/p&gt;</description>
      <category>개발지식</category>
      <category>AOP</category>
      <category>multi-datasource</category>
      <category>namedLock</category>
      <category>동시성제어</category>
      <category>분산락</category>
      <author>0h0</author>
      <guid isPermaLink="true">https://00h0.tistory.com/106</guid>
      <comments>https://00h0.tistory.com/106#entry106comment</comments>
      <pubDate>Wed, 15 Nov 2023 00:25:28 +0900</pubDate>
    </item>
    <item>
      <title>AOP와 Multi-Datasource를 활용해 동시성 문제 해결해보기 1탄(feat. NamedLock)</title>
      <link>https://00h0.tistory.com/105</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;size16&quot;&gt;분산서버에서 발생하는 동시성 문제를 어떻게 해결할 수 있을까 고민하다가 namedLock 이란 개념에 대해 알게되어 이를 간단하게 구현해본 과정을 정리하려고 합니다.&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;&lt;a href=&quot;https://github.com/youngh0/spring-experiment/tree/master/src/main/java/com/example/experiment/mysqlnamedlock&quot;&gt;전체코드&lt;/a&gt; 주소입니다. &lt;a href=&quot;https://00h0.tistory.com/106&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;다음 포스팅 (AOP, multi-datasource 적용)&lt;/a&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;size16&quot;&gt;수강신청 상황을 예제로 사용할 예정입니다. 정원이 있는 강의에 여러 명의 사용자가 수강신청 요청을 보낼 때, 강의 정원 만큼만 수강신청이 가능한 상황입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 수강신청은 순서도 중요하지만 이번 포스팅에선 동시성 제어에만 초점을 맞춰주시면 감사하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Lecture&lt;/b&gt; 는 강의 엔티티이고 강의 정원 (restCount) 필드를 가지고 있습니다. &lt;b&gt;LectureStudent&lt;/b&gt; 엔티티는 강의에 수강신청한 학생들의 목록을 관리하고 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-11-14 오후 11.15.50.png&quot; data-origin-width=&quot;492&quot; data-origin-height=&quot;168&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c4bUqy/btsAjm8YaaW/sP4PzkZd7y9oHKdoXSAtJK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c4bUqy/btsAjm8YaaW/sP4PzkZd7y9oHKdoXSAtJK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c4bUqy/btsAjm8YaaW/sP4PzkZd7y9oHKdoXSAtJK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc4bUqy%2FbtsAjm8YaaW%2FsP4PzkZd7y9oHKdoXSAtJK%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;492&quot; height=&quot;168&quot; data-filename=&quot;스크린샷 2023-11-14 오후 11.15.50.png&quot; data-origin-width=&quot;492&quot; data-origin-height=&quot;168&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;수강신청의 흐름은 아래와 같습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;인자로 받은 lectureId 를 통해 lecture 를 조회한다.&lt;/li&gt;
&lt;li&gt;lecture 의 정원이 남았다면 LectureStudent 에 수강신청 목록 데이터를 추가한다.&lt;/li&gt;
&lt;li&gt;lecture 의 남은 정원에 -1 을 한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;namedLock 사용 이유&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RealMySQL 책을 보다가 namedLock을 활용하면 분산서버에서 발생하는 동시성 문제를 쉽게 해결할 수 있다는 내용을 보고 직접 해보고 싶어져서 namedLock을 사용했습니다.&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;b&gt;예제 코드&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Lecture.java&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Entity
public class Lecture {

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

    private Integer restCount; // 남은 정원

    public Lecture() {
    }

    public Lecture(Integer restCount) {
        this.restCount = restCount;
    }

    public Lecture(Long id, Integer restCount) {
        this.id = id;
        this.restCount = restCount;
    }

    public boolean isPossibleEnrolment() {
        return restCount &amp;gt; 0;
    }

    public void decrease() {
        restCount--;
    }

    public Long getId() {
        return id;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;LectureStudent.java&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Entity
public class LectureStudent {

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

    private Long lectureId;
    private Long studentId;

    public LectureStudent() {
    }

    public LectureStudent(Long lectureId, Long studentId) {
        this.lectureId = lectureId;
        this.studentId = studentId;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;LectureRepository.java&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;@Repository
public interface LectureRepository extends JpaRepository&amp;lt;Lecture, Long&amp;gt; {
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;LectureStudentRepository.java&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;@Repository
public interface LectureStudentRepository extends JpaRepository&amp;lt;LectureStudent, Long&amp;gt; {

    int countByLectureId(Long lectureId);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;LectureService.java&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@RequiredArgsConstructor
@Slf4j
@Service
public class LectureService {

    private final LectureRepository lectureRepository;
    private final LectureStudentRepository lectureStudentRepository;
    private final DataSource dataSource;

    @Transactional
    public Long create(int number) { // 강의 생성
        Lecture lecture = new Lecture(number);
        return lectureRepository.save(lecture).getId();
    }

    @Transactional
    public void enrolment(Long lectureId, Long studentId) { // 수강신청
        Lecture lecture = lectureRepository.findById(lectureId)
                .orElseThrow();

        if (lecture.isPossibleEnrolment()) {
            lectureStudentRepository.save(new LectureStudent(lectureId, studentId));
            lecture.decrease();
            return;
        }
        log.info(&quot;{} 수강신청 실패&quot;, Thread.currentThread().getName());
    }

    @Transactional(readOnly = true)
    public int countLectureStudents(Long lectureId) { // 강의 수강 신청 인원 조회
        lectureRepository.findById(lectureId).orElseThrow();
        return lectureStudentRepository.countByLectureId(lectureId);
    }
}
&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;size16&quot;&gt;RestAssured와 CountDownLatch 를 활용해 테스트 해보겠습니다. api 코드는 &lt;a href=&quot;https://github.com/youngh0/spring-experiment/tree/master/src/main/java/com/example/experiment/mysqlnamedlock&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;github&lt;/a&gt;에 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수강신청이 완료되면 강의 정원인 10명만 수강신청 목록에 있기를 기대하고 있습니다.&lt;/p&gt;
&lt;pre class=&quot;livescript&quot;&gt;&lt;code&gt;@Test
void 다중_요청_수강_신청() throws InterruptedException {
    int maxLectureStudentNum = 10;
    Long lectureId = 강의등록(maxLectureStudentNum);

    int studentsNum = 50;
    ExecutorService executorService = Executors.newFixedThreadPool(15);
    CountDownLatch countDownLatch = new CountDownLatch(studentsNum);

    for (int i = 0; i &amp;lt; studentsNum; i++) {
        executorService.submit(() -&amp;gt; {
            try {
                수강신청(lectureId);
            } finally {
                countDownLatch.countDown();
            }
        });
    }
    countDownLatch.await();

    int lectureStudentNum = 강의_수강생_조회(lectureId);
    assertThat(lectureStudentNum).isEqualTo(maxLectureStudentNum);
}

private void 수강신청(Long lectureId) {
    RestAssured.given()
            .contentType(JSON)
            .body(new LectureRequest(3L))
            .when()
            .post(&quot;/lecture/{lectureId}&quot;, lectureId)
            .then()
            .extract();
}

private int 강의_수강생_조회(Long lectureId) {
    ExtractableResponse&amp;lt;Response&amp;gt; extract = RestAssured.given()
            .contentType(JSON)
            .when()
            .get(&quot;/lecture/{lectureId}&quot;, lectureId)
            .then()
            .extract();

    return extract.response().body().as(Integer.class);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-11-14 오후 9.35.13.png&quot; data-origin-width=&quot;608&quot; data-origin-height=&quot;150&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dBLvqz/btsAmGlksmT/E0DuJ3oWE70TEZ5DUnRDOk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dBLvqz/btsAmGlksmT/E0DuJ3oWE70TEZ5DUnRDOk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dBLvqz/btsAmGlksmT/E0DuJ3oWE70TEZ5DUnRDOk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdBLvqz%2FbtsAmGlksmT%2FE0DuJ3oWE70TEZ5DUnRDOk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;608&quot; height=&quot;150&quot; data-filename=&quot;스크린샷 2023-11-14 오후 9.35.13.png&quot; data-origin-width=&quot;608&quot; data-origin-height=&quot;150&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;39명이나 수강신청에 성공해 테스트가 실패합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;namedLock 구현&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 namedLock을 이용해 여러 요청이 동시에 와도 정합성을 유지해보겠습니다.&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의 namedLock은 &lt;b&gt;SELECT GET_LOCK(keyName, timeout)&lt;/b&gt; 와 &lt;b&gt;SELECT RELEASE_LOCK(keyname)&lt;/b&gt; 를 통해 namedLock을 획득하고 해제할 수 있습니다. 명시적으로 락을 획득하는 만큼 &lt;b&gt;반드시 명시적으로 락을 해제&lt;/b&gt;해줘야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;timeout 은 namedLock 을 얻기 위한 최대 대기 시간입니다. 이를 통해 무한 대기를 방지할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;namedLock  과 비즈니스 로직 수행 흐름&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;namedLock을 구현할 때 중요한 점은 반드시 &lt;b&gt;namedLock 획득 &amp;rarr; 비즈니스 로직 commit &amp;rarr; namedLock 해제&lt;/b&gt; 순으로 이루어져야 합니다. 아래 코드의 경우를 살펴보겠습니다.&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;@Transactional
public void create(){
	// getLock
	// 비즈니스 로직
	// releaseLock
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비즈니스 로직이 커밋되기 전에 namedLock 이 해제됩니다. 이로 인해, namedLock이 해제되고 비즈니스 로직이 commit 되기 전에 다른 커넥션이 namedLock 을 점유해 자신의 작업을 수행할 수 있습니다.&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;그러면 namedLock 을 사용하기 전과 마찬가지로 동시성 문제가 발생하게 됩니다. 그래서 반드시 &lt;b&gt;namedLock 획득 &amp;rarr; 비즈니스 로직 commit &amp;rarr; namedLock 해제&lt;/b&gt; 순으로 동작해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 구현 코드를 살펴보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;LockRepository.java&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;@Slf4j
@RequiredArgsConstructor
@Repository
public class LockRepository {

  private static final String GET_LOCK = &quot;SELECT GET_LOCK(?, ?)&quot;;
  private static final String RELEASE_LOCK = &quot;SELECT RELEASE_LOCK(?)&quot;;

  private final DataSource dataSource;

  public void executeWithLock(String userLockName,
                              int timeoutSeconds,
                              Runnable runnable) {

      try (Connection connection = dataSource.getConnection()) {
          log.info(&quot;lock datasource: {}&quot;, dataSource.toString());
          try {
              getLock(connection, userLockName, timeoutSeconds);
              log.info(&quot;getLock= {}, timeoutSeconds = {}, connection = {}, thread: {}&quot;, userLockName, timeoutSeconds, connection, Thread.currentThread().getName());
              runnable.run();

          } finally {
              releaseLock(connection, userLockName);
              log.info(&quot;releaseLock = {}, connection = {}, thread = {}&quot;, userLockName, connection, Thread.currentThread().getName());
          }
      } catch (SQLException | RuntimeException e) {
          throw new RuntimeException(e.getMessage(), e);
      }
  }

  private void getLock(Connection connection,
                       String userLockName,
                       int timeoutseconds) throws SQLException {

      try (PreparedStatement preparedStatement = connection.prepareStatement(GET_LOCK)) {
          preparedStatement.setString(1, userLockName);
          preparedStatement.setInt(2, timeoutseconds);
      }
  }

  private void releaseLock(Connection connection,
                           String userLockName) throws SQLException {
      try (PreparedStatement preparedStatement = connection.prepareStatement(RELEASE_LOCK)) {
          preparedStatement.setString(1, userLockName);
      }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;락을 얻고 해제하는 코드는 단순히 datasource 를 활용해 쿼리만 실행하는 거라 &lt;a href=&quot;https://techblog.woowahan.com/2631/&quot;&gt;링크 코드&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;b&gt;getLock()&lt;/b&gt; 메서드는 &lt;b&gt;SELECT GET_LOCK(?, ?)&lt;/b&gt; 쿼리를 통해 namedLock 을 획득합니다. &lt;b&gt;releaseLock()&lt;/b&gt; 메서드는 &lt;b&gt;SELECT RELEASE_LOCK(?)&lt;/b&gt; 쿼리를 통해 비즈니스 로직 수행 후 namedLock 을 해제합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;비즈니스 로직 수행은 Runnable&lt;/b&gt; 을 활용했습니다.(이 부분은 링크의 예제 코드와 다릅니다 ㅎ)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;getLock() &amp;rarr; 비즈니스 로직 수행 및 커밋 &amp;rarr; releaseLock()&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;size16&quot;&gt;JpaRepository 를 사용하지 않은 이유는 namedLock 을 얻을 때는 어떤 엔티티 객체도 필요하지 않기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JdbcTemplate 의 경우 namedLock 을 얻기위해 쿼리를 날리면 해당 커넥션이 반환됩니다. 그러나, namedLock 은 커넥션 단위에 걸리는 것이기 때문에 반환 전에 반드시 namedLock 을 해제 해줘야 하기 때문에 Datasource 를 활용했습니다.&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;b&gt;FacadeLectureService.java&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@RequiredArgsConstructor
@Component
public class FacadeLectureService {

    private final LectureService lectureService;
    private final LockRepository lockRepository;

    public void enrolment(Long lectureId, Long studentId) {
        lockRepository.executeWithLock(&quot;lecture_enrolment&quot;, 3000,
                () -&amp;gt; lectureService.enrolmentWithNamedLock(lectureId, studentId));
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비즈니스 로직과 namedLock을 얻는 작업을 분리하기 위해 Facade 패턴을 사용했습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;다시 테스트 해보기&lt;/b&gt;&lt;/h2&gt;
&lt;pre class=&quot;livescript&quot;&gt;&lt;code&gt;@Test
void 다중_요청_수강_신청() throws InterruptedException {
    int maxLectureStudentNum = 10;
    Long lectureId = 강의등록(maxLectureStudentNum);

    int studentsNum = 50;
    ExecutorService executorService = Executors.newFixedThreadPool(15);
    CountDownLatch countDownLatch = new CountDownLatch(studentsNum);

    for (int i = 0; i &amp;lt; studentsNum; i++) {
        executorService.submit(() -&amp;gt; {
            try {
                락을_이용한_수강신청(lectureId);
            } finally {
                countDownLatch.countDown();
            }
        });
    }
    countDownLatch.await();

    int lectureStudentNum = 강의_수강생_조회(lectureId);
    assertThat(lectureStudentNum).isEqualTo(maxLectureStudentNum);
}

private void 락을_이용한_수강신청(Long lectureId) {
    RestAssured.given()
            .contentType(JSON)
            .body(new LectureRequest(3L))
            .when()
            .post(&quot;/lecture/namedLock/{lectureId}&quot;, lectureId)
            .then()
            .extract();
}

private int 강의_수강생_조회(Long lectureId) {
    ExtractableResponse&amp;lt;Response&amp;gt; extract = RestAssured.given()
            .contentType(JSON)
            .when()
            .get(&quot;/lecture/{lectureId}&quot;, lectureId)
            .then()
            .extract();

    return extract.response().body().as(Integer.class);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-11-14 오후 11.27.30.png&quot; data-origin-width=&quot;524&quot; data-origin-height=&quot;56&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nxdKK/btsAmBqNjku/7pmfYCy8SxwEFNzcnA6chK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nxdKK/btsAmBqNjku/7pmfYCy8SxwEFNzcnA6chK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nxdKK/btsAmBqNjku/7pmfYCy8SxwEFNzcnA6chK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnxdKK%2FbtsAmBqNjku%2F7pmfYCy8SxwEFNzcnA6chK%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;524&quot; height=&quot;56&quot; data-filename=&quot;스크린샷 2023-11-14 오후 11.27.30.png&quot; data-origin-width=&quot;524&quot; data-origin-height=&quot;56&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;size16&quot;&gt;&lt;b&gt;커넥션 부족&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 구조는 lock 을 얻기 위한 Datasource와 비즈니스 로직을 수행하는 Datasource가 동일합니다. 이로 인해, lock을 얻기 위한 커넥션들로 인해 비즈니스 로직을 수행할 커넥션이 부족해질 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 해결하기 위해 다음 포스팅에서는 multi-datasource 를 통해 각각의 Datasource를 분리해보겠습니다.&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;b&gt;손쉽게 namedLock 적용해보기&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드를 구현하다 보니 메서드에 어노테이션을 붙이는 것으로 namedLock을 적용할 수 있을 것 같습니다. 비즈니스 로직 수행 전후로 namedLock 획득, 해제만 해주면 되기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 포스팅에서 multi-datasource 와 AOP 적용을 다뤄보겠습니다.&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;b&gt;namedLock 을 획득한 커넥션이 있는 프로세스의 ec2 가 다운됐을 때는 어떻게 namedLock 을 반환할까?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비즈니스 로직을 수행하다 발생하는 예외는 try&amp;hellip;catch&amp;hellip;finally 를 통해 namedLock을 해제할 수 있습니다. 하지만 namedLock을 점유하고 ec2 가 namedLock을 해제하기 전에 죽는 경우는 try&amp;hellip;catch&amp;hellip;finally로 가능하지 않습니다.&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;이 경우 커넥션 타임아웃으로 인해 커넥션이 종료될 경우 자동적으로 namedLock 을 해제해주고 있습니다. 실제로 mysql 에서 namedLock 을 점유하고 있는 스레드를 kill 명령어를 이용해 죽이면 해당 스레드의 커넥션이 점유하고 있는 namedLock 이 해제됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;마무리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직접 구현하면서 느낀 점은 namedLock 은 특정 자원에 대한 잠금이 아니라 어떠한 행위를 하기 위해 팀 내부적으로 약속한 잠금이라는 느낌을 받았습니다.&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;만약 A테이블과 B테이블에 각각 잠금을 거는 트랜잭션 TX1, TX2 가 있다고 가정해봅시다. TX1 은 B테이블에 잠금을 걸고 컨텍스트 스위칭이 발생해 TX2 가 A테이블에 잠금을 겁니다. 이후 B테이블에 잠금을 걸기 위해 TX1 이 잠금을 해제하길 기다립니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-11-14 오후 11.21.02.png&quot; data-origin-width=&quot;743&quot; data-origin-height=&quot;499&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bM4nCJ/btsAjSNmz9L/PPH5cwuZ7wyHHnrBURnq10/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bM4nCJ/btsAjSNmz9L/PPH5cwuZ7wyHHnrBURnq10/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bM4nCJ/btsAjSNmz9L/PPH5cwuZ7wyHHnrBURnq10/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbM4nCJ%2FbtsAjSNmz9L%2FPPH5cwuZ7wyHHnrBURnq10%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;743&quot; height=&quot;499&quot; data-filename=&quot;스크린샷 2023-11-14 오후 11.21.02.png&quot; data-origin-width=&quot;743&quot; data-origin-height=&quot;499&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;이런 작업들은 &amp;lsquo;namedLock&amp;rsquo; 이라는 문자열에 대한 잠금을 얻고 자신의 작업을 수행하도록 설정하면 데드락을 피할 수 있습니다. TX1은 자신의 작업을 하기 위해 &amp;lsquo;namedLock&amp;rsquo; 문자열에 대해 잠금을 얻고 B테이블에 잠금을 겁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 컨텍스트 스위칭이 일어나 TX2 가 자신의 작업을 위해 &amp;lsquo;namedLock&amp;rsquo; 문자열에 대한 잠금을 얻으려고 하지만 실패하고 다시 TX1으로 컨텍스트 스위칭이 발생합니다. TX1이 자신의 작업을 마무리 하고 namedLock 을 반환하면 TX2 가 namedLock 을 얻고 자신의 작업을 마무리하는 방식으로 데드락 회피 및 동시성 제어가 가능합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;참고&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://techblog.woowahan.com/2631/&quot;&gt;https://techblog.woowahan.com/2631/&lt;/a&gt;&lt;/p&gt;</description>
      <category>개발지식</category>
      <category>AOP</category>
      <category>multi-datasource</category>
      <category>namedLock</category>
      <category>동시성 제어</category>
      <category>분산락</category>
      <author>0h0</author>
      <guid isPermaLink="true">https://00h0.tistory.com/105</guid>
      <comments>https://00h0.tistory.com/105#entry105comment</comments>
      <pubDate>Tue, 14 Nov 2023 23:29:35 +0900</pubDate>
    </item>
    <item>
      <title>[Junit] 병렬 실행을 통해 빌드 시간 단축해보기</title>
      <link>https://00h0.tistory.com/104</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;size16&quot;&gt;평소 길어진 빌드 시간을 개선하기 위해 방법을 찾아보다가 Junit 병렬 실행에 대해 알게됐고 이를 적용해봤습니다. 이 글에서는 Junit 병렬 실행 설정을 통해 테스트 실행 시간을 줄인 방법에 대해서 작성해보려고 합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;설정 방법&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;junit-platform.properties 파일을 생성해서 병렬 실행 설정을 해주면 됩니다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;junit.jupiter.execution.parallel.enabled=true
junit.jupiter.execution.parallel.mode.classes.default = concurrent
junit.jupiter.execution.parallel.config.strategy=fixed
junit.jupiter.execution.parallel.config.fixed.parallelism=6&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;우선 &lt;code&gt;junit.jupiter.execution.parallel.enabled=true&lt;/code&gt; 이 설정을 통해 병렬 실행을 허용해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;junit.jupiter.execution.parallel.mode.classes.default = concurrent&lt;/code&gt; 이 설정을 통해 각 클래스 파일들을 기본적으로 병렬 실행으로 설정해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;junit.jupiter.execution.parallel.config.strategy=fixed junit.jupiter.execution.parallel.config.fixed.parallelism=6&lt;/code&gt; 이 2가지 옵션은 병렬로 수행할 스레드 개수를 설정한 것입니다. 저는 6개의 스레드로 병렬 수행하도록 설정했습니다.&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;그 이유는 싱글 스레드로 돌아가야 하는 부류의 클래스들이 3개 있었습니다. (인수 테스트, 컨트롤러 테스트, docs 테스트) 이 클래스들은 각각 싱글 스레드로 돌리고, 나머지 클래스들은 남은 3개의 스레드를 통해 병렬 수행하기 위해서였습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;주의점&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 Junit 을 병렬로 실행할 때는 thread-safe 한 코드들만 병렬로 수행해야 합니다. 예를 들면 RestAssured를 이용하는 인수테스트의 경우 병렬로 실행하면 동시성 문제로 인해 테스트에 실패하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 싱글 스레드로 돌려야 하는 클래스에 @Execution(ExecutionMode.*SAME_THREAD*) 어노테이션을 붙여 싱글 스레드로 동작하도록 설정할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Execution(ExecutionMode.SAME_THREAD)
@SpringBootTest(webEnvironment = RANDOM_PORT)
public abstract class AcceptanceTest { ... }&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;size16&quot;&gt;제 로컬(인텔 맥) 에서 빌드 시간이 1분 9초 에서 45초 줄어들었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;병렬 실행 X&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-11-10 오전 1.37.05.png&quot; data-origin-width=&quot;214&quot; data-origin-height=&quot;29&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ctjgq0/btsAboZf0Nc/K45ZtDeTudpf7bNawrOMK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ctjgq0/btsAboZf0Nc/K45ZtDeTudpf7bNawrOMK0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ctjgq0/btsAboZf0Nc/K45ZtDeTudpf7bNawrOMK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fctjgq0%2FbtsAboZf0Nc%2FK45ZtDeTudpf7bNawrOMK0%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;214&quot; height=&quot;29&quot; data-filename=&quot;스크린샷 2023-11-10 오전 1.37.05.png&quot; data-origin-width=&quot;214&quot; data-origin-height=&quot;29&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;병령 실행 O&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-11-10 오전 1.30.34.png&quot; data-origin-width=&quot;197&quot; data-origin-height=&quot;27&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/VI9En/btsz60y3zXP/XzhymYL7PVpKr6ttuLIGhK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/VI9En/btsz60y3zXP/XzhymYL7PVpKr6ttuLIGhK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/VI9En/btsz60y3zXP/XzhymYL7PVpKr6ttuLIGhK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVI9En%2Fbtsz60y3zXP%2FXzhymYL7PVpKr6ttuLIGhK%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;197&quot; height=&quot;27&quot; data-filename=&quot;스크린샷 2023-11-10 오전 1.30.34.png&quot; data-origin-width=&quot;197&quot; data-origin-height=&quot;27&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;더 다양한 옵션은 &lt;a href=&quot;https://junit.org/junit5/docs/snapshot/user-guide/#writing-tests-parallel-execution&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;공식문서&lt;/a&gt;에 있습니다~&lt;/p&gt;</description>
      <category>Test/JUnit</category>
      <category>JUnit</category>
      <category>Parallel</category>
      <category>병렬</category>
      <category>성능최적화</category>
      <author>0h0</author>
      <guid isPermaLink="true">https://00h0.tistory.com/104</guid>
      <comments>https://00h0.tistory.com/104#entry104comment</comments>
      <pubDate>Sun, 12 Nov 2023 15:13:46 +0900</pubDate>
    </item>
    <item>
      <title>[JPA] 엔티티 구조 변경을 통한 로직 복잡성 낮추기, 쿼리 개선기</title>
      <link>https://00h0.tistory.com/103</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;size16&quot;&gt;이번 글에서는 프로젝트를 진행하며 엔티티 구조를 변경해 로직 복잡성을 낮추고, 쿼리를 개선한 경험을 작성하려고 합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;상황 및 엔티티 구조 설명&lt;/b&gt;&lt;/h2&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;size16&quot;&gt;저희 프로젝트는 개인 카페의 쿠폰을 한 곳에서 관리해주는 서비스 입니다. 여기에 더불어 카페 사장님은 쿠폰의 디자인, 쿠폰에 찍히는 스탬프의 위치를 모두 커스텀 할 수 있습니다. 그리고 발급된 쿠폰은 발급 당시의 이미지 정보를 토대로 보여지게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A쿠폰 발급 후 사장님이 카페의 쿠폰 이미지를 변경해도 A쿠폰은 발급 당시의 이미지로 보여집니다. 즉, 쿠폰은 &lt;b&gt;발급 당시 이미지, 스탬프 위치 정보를 적용&lt;/b&gt;합니다.&lt;/p&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;size16&quot;&gt;위 요구사항을 만족하기 위해 카페 별 쿠폰 정보를 저장하는 복사 테이블을 만들었습니다. 그 이유는 당시 저희는 사장님이 쿠폰 이미지 정보를 변경하면 update 를 통해 최신화를 했습니다. 그래서 발급 당시의 정보를 관리하기 위해 복사 테이블을 만들었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구조를 간단하게 도식화 하면 아래와 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-11-11 오후 3.44.32.png&quot; data-origin-width=&quot;1103&quot; data-origin-height=&quot;794&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mYobT/btsz60ey7v8/OQ3TK5kYbfzP22daUvueDK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mYobT/btsz60ey7v8/OQ3TK5kYbfzP22daUvueDK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mYobT/btsz60ey7v8/OQ3TK5kYbfzP22daUvueDK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmYobT%2Fbtsz60ey7v8%2FOQ3TK5kYbfzP22daUvueDK%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;1103&quot; height=&quot;794&quot; data-filename=&quot;스크린샷 2023-11-11 오후 3.44.32.png&quot; data-origin-width=&quot;1103&quot; data-origin-height=&quot;794&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;복사본은 CouponDesign, StampCoordinate 에서 cafe 에 대한 참조만 없는 형태입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;기존 문제점&lt;/b&gt;&lt;/h2&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;size16&quot;&gt;이 상황에서 복사본을 만드는 책임을 각 CouponDesign, StampCoordinate 에 부여했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 코드는 쿠폰 디자인 복사 로직입니다. 이와 같은 코드가 StampCoordinate 에도 존재합니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;public CouponDesign copy() {
    CouponDesign couponDesign = new CouponDesign(frontImageUrl, backImageUrl, stampImageUrl);
    for (CafeStampCoordinate cafeStampCoordinate : cafeStampCoordinates) {
        CouponStampCoordinate couponStampCoordinate = cafeStampCoordinate.copy(couponDesign);
        couponDesign.addCouponStampCoordinate(couponStampCoordinate);
    }
    return couponDesign;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 코드는 쿠폰 발급 service 로직 일부분입니다. 쿠폰 발급을 하는데 &lt;b&gt;복사본 생성 &amp;rarr; 복사본 저장&lt;/b&gt; 등의 로직이 더 들어가있습니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;private Coupon issueCoupon(Customer customer, Cafe cafe, CafePolicy cafePolicy, CafeCouponDesign cafeCouponDesign) {
    CouponDesign couponDesign = cafeCouponDesign.copy();
    couponDesignRepository.save(couponDesign);
    CouponPolicy couponPolicy = cafePolicy.copy();
    couponPolicyRepository.save(couponPolicy);

    LocalDate expiredDate = LocalDate.now().plusMonths(couponPolicy.getExpiredPeriod());

    return new Coupon(expiredDate, customer, cafe, couponDesign, couponPolicy);
}&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;service 단에서 CouponDesign, StampCoordinate의 복사 메서드를 호출해서 복사본 객체를 만든다.&lt;/li&gt;
&lt;li&gt;각 복사본 객체를 DB에 저장한다.&lt;/li&gt;
&lt;li&gt;이를 참조하는 Coupon 객체를 저장한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;다수의 insert 쿼리 발생&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿠폰 하나를 저장하기 위해 해당 쿠폰의 이미지 정보, 각 스탬프 위치 정보를 추가로 저장해야했습니다. 이로 인해 실제로 발생하는 insert 쿼리는 아래와 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;StampCoordinate 정보 저장 (N개 - 최대 10개)&lt;/li&gt;
&lt;li&gt;CouponDesign 저장 (1개)&lt;/li&gt;
&lt;li&gt;Coupon 저장 (1개)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;개선점&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 상황에서 쿠폰 디자인 변경 시 update 가 아닌 insert 를 통해 관리하면 쿠폰 발급 시 3번 작업만으로 쿠폰 발급 로직을 간단하게 풀어낼 수 있다고 판단했습니다. 그리고 &lt;b&gt;쿠폰은 현재 카페의 사용중인 CouponDesign, StampCoordinate 에 대한 참조&lt;/b&gt;를 가지면 쿠폰 발급 당시 이미지 정보를 적용할 수 있다고 판단했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;update 가 아닌 insert 를 통해 쿠폰 디자인 변경을 관리하는 방법은 아래 근거를 통해 결정했습니다.&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;사장님에게 쿠폰 디자인 변경 이력을 제공해 줄 수 있다.&lt;/li&gt;
&lt;li&gt;불필요한 복사본 저장 쿼리가 줄어든다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;개선 후 엔티티 구조&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-11-11 오후 3.44.13.png&quot; data-origin-width=&quot;1109&quot; data-origin-height=&quot;531&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bgDniU/btsz6N64U2m/Kfd0vC1po3xj8TgKAzouf1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bgDniU/btsz6N64U2m/Kfd0vC1po3xj8TgKAzouf1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bgDniU/btsz6N64U2m/Kfd0vC1po3xj8TgKAzouf1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbgDniU%2Fbtsz6N64U2m%2FKfd0vC1po3xj8TgKAzouf1%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;1109&quot; height=&quot;531&quot; data-filename=&quot;스크린샷 2023-11-11 오후 3.44.13.png&quot; data-origin-width=&quot;1109&quot; data-origin-height=&quot;531&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;쿼리 개선&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기존 쿠폰 발급 시 발생하던 insert 쿼리 13번 &amp;rarr; 1번으로 감소&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;로직 복잡성 개선&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;private Coupon issueCoupon(Customer customer, Cafe cafe, CafePolicy cafePolicy, CafeCouponDesign cafeCouponDesign) {
    LocalDate expiredDate = LocalDate.now().plusMonths(cafePolicy.getExpirePeriod());
    return new Coupon(expiredDate, customer, cafe, cafeCouponDesign, cafePolicy);
}&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;기존의 issueCoupon 메서드보다 로직이 매우 간결해졌습니다.&lt;/li&gt;
&lt;li&gt;CouponDesign, StampCoordinate 도 copy() 메서드가 사라지면서 보다 가벼워졌습니다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>JPA</category>
      <category>성능최적화</category>
      <category>쿼리개선</category>
      <author>0h0</author>
      <guid isPermaLink="true">https://00h0.tistory.com/103</guid>
      <comments>https://00h0.tistory.com/103#entry103comment</comments>
      <pubDate>Sat, 11 Nov 2023 15:47:31 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] @TransactionalEventListener 에서 CUD 가 안되는 이유</title>
      <link>https://00h0.tistory.com/102</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;size16&quot;&gt;우테코 프로젝트에서 @TransactionalEventListener 를 사용할 때 READ 는 되는데 CUD 쿼리가 발생하지 않는 문제를 겪었다. 그래서 이에 대한 원인에 대해 공부한 과정을 정리 해볼 예정이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;트랜잭션 생성 과정과 커밋 과정&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://00h0.tistory.com/100&quot;&gt;REQUIRED, REQUIES_NEW 옵션의 트랜잭션 생성 과정&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://00h0.tistory.com/99&quot;&gt;물리 트랜잭션, 논리 트랜잭션 커밋 과정&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각각에 대한 자세한 내용은 링크에 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단하게 요약하자면, 아래와 같은 흐름으로 진행됩니다.&lt;/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;REQUIRED 옵션은 새로운 트랜잭션을 논리 트랜잭션으로 생성&lt;/li&gt;
&lt;li&gt;REQUIRES_NEW 는 스레드 로컬에 자원이 있어도 새로운 물리 트랜잭션으로 생성&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;이후 커밋 시 물리 트랜잭션이라면 flush() 가 호출
&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;커밋 시 논리 트랜잭션이면 flush() 호출 X&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;@TransactionalEventListener + @Transaction&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모두 기본 옵션인 AFTER_COMMIT, REQUIRED 를 사용한다고 가정하겠습니다.&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;&lt;span style=&quot;color: #f89009;&quot;&gt;&lt;b&gt;Publisher (이벤트 발행자)&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Transactional
@RequiredArgsConstructor
@Service
public class Publisher {

    private final ApplicationEventPublisher publisher;
    private final EventEntityRepo eventEntityRepo;

    public void publish() {
        eventEntityRepo.save(new EventEntity());
        publisher.publishEvent(new MyEvent());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Listener (이벤트 구독자)&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@RequiredArgsConstructor
@Component
public class Listner {

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

    private final EventEntityRepo eventEntityRepo;
    private final EntityManager em;

    @TransactionalEventListener
    public void listen(MyEvent myEvent) {
        EventEntity save = eventEntityRepo.save(new EventEntity());       
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span style=&quot;color: #f89009;&quot;&gt;&lt;b&gt;Publisher&lt;/b&gt;&lt;/span&gt; 로직이 커밋되어 이제&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;Listener&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;로직이 수행 되는 상황입니다. 이 때,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;Listener&lt;/span&gt;의&lt;/b&gt; 이벤트 구독 메서드에서 repository.save() 를 호출합니다.&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;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #f89009;&quot;&gt;&lt;b&gt;Publisher&lt;/b&gt;&lt;/span&gt;&amp;nbsp;측의 메서드가 실행되면서 트랜잭션을 획득한다.&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #f89009;&quot;&gt;&lt;b&gt;Publisher&lt;/b&gt;&lt;/span&gt;&amp;nbsp;측 repository.save() 가 호출된다.&lt;/li&gt;
&lt;li&gt;이벤트를 발행한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;color: #f89009;&quot;&gt;&lt;b&gt;Publisher&lt;/b&gt;&lt;/span&gt;&amp;nbsp;측 트랜잭션이 커밋&lt;/b&gt;된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;Listener&lt;/b&gt;&lt;/span&gt;의 메서드가 실행&lt;/b&gt;된다.&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;Listener&lt;/b&gt;&lt;/span&gt;&amp;nbsp;측의 repository.save() 가 호출된다.
&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;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;Listener&lt;/b&gt;&lt;/span&gt;&amp;nbsp;측 메서드가 종료된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;color: #f89009;&quot;&gt;&lt;b&gt;Publisher&lt;/b&gt;&lt;/span&gt;&amp;nbsp;측 트랜잭션에 관한 스레드로컬 자원이 정리&lt;/b&gt;된다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 주목할 점은 4, 5 번 사이입니다. 아마 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;Listener&lt;/b&gt;&lt;/span&gt;가 없었으면 4번 이후 8번 작업이 바로 실행됐을겁니다. 그러나 @TransactionalEventListener 가 존재하므로, 5번 작업이 수행됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5번 작업이 수행되는 상황을 도식화 하면 아래와 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;무제.jpg&quot; data-origin-width=&quot;2688&quot; data-origin-height=&quot;2036&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/onZJU/btsytb9oM6Y/0ACtHWIMG8N3nHFxzeLB50/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/onZJU/btsytb9oM6Y/0ACtHWIMG8N3nHFxzeLB50/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/onZJU/btsytb9oM6Y/0ACtHWIMG8N3nHFxzeLB50/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FonZJU%2Fbtsytb9oM6Y%2F0ACtHWIMG8N3nHFxzeLB50%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2688&quot; height=&quot;2036&quot; data-filename=&quot;무제.jpg&quot; data-origin-width=&quot;2688&quot; data-origin-height=&quot;2036&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;&lt;span style=&quot;color: #f89009;&quot;&gt;&lt;b&gt;Publisher&lt;/b&gt;&lt;/span&gt;&amp;nbsp;트랜잭션이 커밋 되었더라도 아직 스레드 로컬에 자원이 남아 있기 때문에 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;Listener&lt;/b&gt;&lt;/span&gt; 측에서 얻는 트랜잭션은 논리 트랜잭션 입니다. 물리 트랜잭션이 커밋되어야 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;Listener&lt;/b&gt;&lt;/span&gt; 측의 쿼리가 flush() 가 되는데, 이미 물리 트랜잭션은 커밋된 상태라 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;Listener&lt;/b&gt;&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;size16&quot;&gt;그렇다면 해결 방법은 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;Listener&lt;/b&gt;&lt;/span&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;b&gt;@Async&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션을 얻을 때 기존 트랜잭션이 있다고 판단하는 첫 번째 근거는 스레드 로컬의 자원 유무입니다. 그러면 새로운 스레드에서 이벤트 구독 메서드가 실행되도록 하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;REQUIRED_NEW&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Transactional 의 옵션을 REQUIRED_NEW 로 주는 방법도 있습니다. REQUIRED_NEW 는 기존 스레드 로컬에 다른 트랜잭션 자원이 있어도 새로운 물리 트랜잭션을 반환해줍니다.&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;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;</description>
      <category>Spring</category>
      <category>Spring</category>
      <category>Transaction</category>
      <category>TransactionalEventListener</category>
      <author>0h0</author>
      <guid isPermaLink="true">https://00h0.tistory.com/102</guid>
      <comments>https://00h0.tistory.com/102#entry102comment</comments>
      <pubDate>Sun, 15 Oct 2023 19:38:57 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] 외부 API 와 비즈니스 로직 분리하기</title>
      <link>https://00h0.tistory.com/101</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;size16&quot;&gt;프로젝트 도중, &lt;b&gt;아래의 요구사항을 구현&lt;/b&gt;하면서 &lt;b&gt;@TransactionalEventListner&lt;/b&gt; 를 사용해 기존 코드의 문제점이었던 &lt;b&gt;코드의 유지보수성&lt;/b&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;스탬프 적립이 정상적으로 완료되면, 슬랙으로 알림을 보낸다.&lt;br /&gt;&lt;span style=&quot;color: #666666; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;알림 발송에 실패해도 스탬프 적립이 롤백되어선 안된다.&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;기존 코드&lt;/b&gt;&lt;/h2&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Transactional
public void createStamp(StampCreateDto stampCreateDto) {
    // 스탬프 적립 전 여러 검증 코드
    coupon.accumulate(earningStampCount);
    // 슬랙 API 호출
}&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;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;&lt;b&gt;외부 API 와 비즈니스 로직이 매우 강하게 결합&lt;/b&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;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;외부(슬랙) API 호출 과정에서 예외&lt;/b&gt;가 발생한다면 &lt;b&gt;스탬프 적립까지 롤백&lt;/b&gt;된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;외부(슬랙) API 호출이 동기적&lt;/b&gt;으로 이루어지면서 API 응답 속도가 불필요하게 느려진다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;[1번 문제 해결] 비즈니스 로직과 외부 API의 강한 결합 해결&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분은 EventListner 를 사용해 해결했다. &lt;b&gt;스탬프 적립 알림 발송&lt;/b&gt;은 &lt;b&gt;스탬프 적립 비즈니스 로직의 관심사가 아니라고 생각&lt;/b&gt;했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 슬랙 API 호출 부분을 분리하기 위한 방법을 고민하다가 &lt;b&gt;@EventListener&lt;/b&gt; 를 알게되어 이를 사용하기로 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드는 아래와 같다.&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;@Transactional
public void createStamp(StampCreateDto stampCreateDto) {
    // 스탬프 적립 전 여러 검증 코드
		coupon.accumulate(earningStampCount);

		eventPublisher.publishEvent(new StampCreateEvent());
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해 비즈니스 로직과 외부 API 가 강하게 결합되어 있던 코드를 개선할 수 있었다. 이제 해당 메서드를 보고 핵심 관심사가 무엇인지 파악하기 용이해졌다.&lt;/p&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;외부 API 호출 과정에서 예외 발생 시 스탬프 적립도 롤백된다.&lt;/li&gt;
&lt;li&gt;@EventListner 는 동기적으로 실행되기 때문에 스탬프 적립 관련 비즈니스 로직(슬랙 알림 제외)이 완료되어도 외부 API 호출 작업이 완료될 때 까지 기다리게 된다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;[2번 문제 상황] 외부 API로 인한 롤백 문제 상황&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제는 외부 API 로직이 이벤트를 발행하는 서비스 메서드의 트랜잭션을 같이 공유하면서 발생하는 문제다. 이를 해결하기 위해 &lt;b&gt;@TransactionalEventListner&lt;/b&gt; 를 활용했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;롤백 문제 상황을 간단하게 도식화 하면 아래와 같다. @EventListner 를 사용했을 때의 상황이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;무제.jpg&quot; data-origin-width=&quot;2516&quot; data-origin-height=&quot;2036&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dI7qSP/btsytthIey5/jzX2O9QKrkOlK7YKCTvmr1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dI7qSP/btsytthIey5/jzX2O9QKrkOlK7YKCTvmr1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dI7qSP/btsytthIey5/jzX2O9QKrkOlK7YKCTvmr1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdI7qSP%2FbtsytthIey5%2FjzX2O9QKrkOlK7YKCTvmr1%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2516&quot; height=&quot;2036&quot; data-filename=&quot;무제.jpg&quot; data-origin-width=&quot;2516&quot; data-origin-height=&quot;2036&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;b&gt;Listner 가 동일한 스레드에서 동기적&lt;/b&gt;으로 실행된다.&lt;/li&gt;
&lt;li&gt;1번의 상황으로 인해 &lt;b&gt;기존 트랜잭션을 그대로 사용&lt;/b&gt;하게 된다.&lt;/li&gt;
&lt;li&gt;구독자 측에서 발생한 &lt;b&gt;runtime 예외로 인해 트랜잭션에 rollback 마킹&lt;/b&gt;이 된다.&lt;/li&gt;
&lt;li&gt;트랜잭션 롤백 작업 수행&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 해결하기 위해 &lt;b&gt;@TransactionalEventListner, @Async&lt;/b&gt; 를 활용할 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;[2, 3번 문제 해결]@TransactionalEventListner, @Async 를 활용한 해결&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;@TransactionalEventListner&lt;/b&gt; 는 다양한 옵션이 있는데, 그 중 &lt;b&gt;AFTER_COMMIT&lt;/b&gt; 옵션을 활용했다. 그 이유는 간단하다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이벤트를 발행하는 서비스 트랜잭션이 commit 되었단 것은 정상적으로 로직 수행이 완료됐다는 의미이다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이벤트 발행 로직이 정상적으로 commit 된 후 외부 API 를 호출하면 외부 API의 성공 여부에 관계 없이 비즈니스 로직에서 저장한 데이터는 남아있게 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;@Async&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해 동기 문제도 동시에 해결할 수 있다. 이 어노테이션으로 슬랙 API 는 비즈니스 로직과 별도의 스레드에서 수행된다.&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;</description>
      <category>Spring</category>
      <category>@Async</category>
      <category>@TransactionalEventListener</category>
      <category>event</category>
      <category>Spring</category>
      <author>0h0</author>
      <guid isPermaLink="true">https://00h0.tistory.com/101</guid>
      <comments>https://00h0.tistory.com/101#entry101comment</comments>
      <pubDate>Sun, 15 Oct 2023 18:27:26 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] @Transactional 의 REQUIRED, REQUIRES_NEW 트랜잭션 생성 과정</title>
      <link>https://00h0.tistory.com/100</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;size16&quot;&gt;트랜잭션 전파 옵션에는 REQUIRED, REQUIRES_NEW 등등이 있다. 그 중에서 이번에는 REQUIRED, REQUIRES_NEW 옵션일 때 트랜잭션 생성 과정에 대해 알아볼 예정이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;REQUIRED 는 어떻게 기존 트랜잭션이 존재하면서 해당 트랜잭션에 참여하고, 없으면 새로운 트랜잭션을 만들까?&lt;/li&gt;
&lt;li&gt;REQUIRES_NEW 는 어떻게 매번 새로운 트랜잭션을 만들까?&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분에 대해 알아볼 때 핵심적인 기능을 하는 클래스는 &lt;code&gt;AbstractPlatformTransactionManager, JpaTransactionManager, TransactionSynchronizationManager&lt;/code&gt; 가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 클래스들이 기존 트랜잭션이 있는지 확인한다. 없으면 새로운 트랜잭션 설정을 해주고, 기존 트랜잭션이 있으면 REQUIRED, REQUIRES_NEW 옵션에 맞게 트랜잭션 설정을 해준다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;REQUIRED&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;[REQUIRED] 첫 트랜잭션 생성 과정&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 트랜잭션이 생성되는 경우를 우선 살펴보자. 우선 트랜잭션 관련 인스턴스를 생성하고 여기에 트랜잭션 관련 리소스를 할당해주는 과정을 거친다. 아래는 트랜잭션 생성 시 초반에 호출되는 과정이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;1.jpg&quot; data-origin-width=&quot;3302&quot; data-origin-height=&quot;1732&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c8wOnb/btsytJEiYMK/rU4jJfiykmdLFtTlIf7hXK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c8wOnb/btsytJEiYMK/rU4jJfiykmdLFtTlIf7hXK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c8wOnb/btsytJEiYMK/rU4jJfiykmdLFtTlIf7hXK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc8wOnb%2FbtsytJEiYMK%2FrU4jJfiykmdLFtTlIf7hXK%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3302&quot; height=&quot;1732&quot; data-filename=&quot;1.jpg&quot; data-origin-width=&quot;3302&quot; data-origin-height=&quot;1732&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;2.jpg&quot; data-origin-width=&quot;3048&quot; data-origin-height=&quot;1852&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/capwTX/btsytRoNldz/2KhySQbnULEDS3ODbJL7lk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/capwTX/btsytRoNldz/2KhySQbnULEDS3ODbJL7lk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/capwTX/btsytRoNldz/2KhySQbnULEDS3ODbJL7lk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcapwTX%2FbtsytRoNldz%2F2KhySQbnULEDS3ODbJL7lk%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3048&quot; height=&quot;1852&quot; data-filename=&quot;2.jpg&quot; data-origin-width=&quot;3048&quot; data-origin-height=&quot;1852&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;눈 여겨 봐야할 부분은 &lt;b&gt;JpaTransactionManager&lt;/b&gt; 에서 &lt;b&gt;emHolder 가 null&lt;/b&gt; 이 반환되고, &lt;b&gt;txObject 의 entityManagerHolder 도 null 인 상태&lt;/b&gt;로 반환된다는 것이다. newEntityManagerHolder 의 경우 다른 코드가 호출되면서 true 로 바뀌게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 만들어진 &lt;b&gt;인스턴스를 토대로 기존에 트랜잭션이 존재하는지 확인&lt;/b&gt; 하는데 그 과정은 아래 코드를 통해 이루어진다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;3.jpg&quot; data-origin-width=&quot;3039&quot; data-origin-height=&quot;1839&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/YVPbn/btsynXKCouF/JPcBkEmh6SbvVQrLy3eIXK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/YVPbn/btsynXKCouF/JPcBkEmh6SbvVQrLy3eIXK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YVPbn/btsynXKCouF/JPcBkEmh6SbvVQrLy3eIXK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYVPbn%2FbtsynXKCouF%2FJPcBkEmh6SbvVQrLy3eIXK%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3039&quot; height=&quot;1839&quot; data-filename=&quot;3.jpg&quot; data-origin-width=&quot;3039&quot; data-origin-height=&quot;1839&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;entityManagerHolder 가 null 이기 때문에 기존에 존재하던 트랜잭션이 없다고 판단한다. 이후 &lt;code&gt;AbstractPlatformTransactionManager.startTransaction()&lt;/code&gt; 이 실행되면서 트랜잭션 생성이 마무리 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;그리고, 이 과정에서 TransactionSynchronizationManager.bindResource()&lt;/code&gt; 를 호출한다. 그래서 이후 생성되는 트랜잭션은 TransactionSynchronizationManager 에 관련 자원이 있기 때문에 기존 트랜잭션이 존재한다고 판단된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;4.jpg&quot; data-origin-width=&quot;3952&quot; data-origin-height=&quot;1496&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bh3pkh/btsytYatsqw/N9t5VO6OpSjQs3JOzEVHs1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bh3pkh/btsytYatsqw/N9t5VO6OpSjQs3JOzEVHs1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bh3pkh/btsytYatsqw/N9t5VO6OpSjQs3JOzEVHs1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbh3pkh%2FbtsytYatsqw%2FN9t5VO6OpSjQs3JOzEVHs1%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3952&quot; height=&quot;1496&quot; data-filename=&quot;4.jpg&quot; data-origin-width=&quot;3952&quot; data-origin-height=&quot;1496&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;[REQUIRED] 기존에 트랜잭션이 존재하는 경우&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 트랜잭션이 생성되는 과정과는 다르게 이번엔 TransactionSynchronizationManager 에 값이 있기 때문에 트랜잭션 관련 인스턴스의 &lt;b&gt;entityManagerHolder가 null 아니&lt;/b&gt;라는 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;5.jpg&quot; data-origin-width=&quot;3764&quot; data-origin-height=&quot;1808&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cIVC2q/btsytRvzKES/UbnCRVoWlkNGJT36syQOs0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cIVC2q/btsytRvzKES/UbnCRVoWlkNGJT36syQOs0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cIVC2q/btsytRvzKES/UbnCRVoWlkNGJT36syQOs0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcIVC2q%2FbtsytRvzKES%2FUbnCRVoWlkNGJT36syQOs0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3764&quot; height=&quot;1808&quot; data-filename=&quot;5.jpg&quot; data-origin-width=&quot;3764&quot; data-origin-height=&quot;1808&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;이로 인해 &lt;code&gt;AbstractPlatformTransactionManager.isExistringTransaction()&lt;/code&gt; 이 true 가 되어 &lt;code&gt;this.handleExistingTransaction()&lt;/code&gt; 이 호출된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;6.jpg&quot; data-origin-width=&quot;3468&quot; data-origin-height=&quot;1806&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/k8pVQ/btsytiNItQu/ZcuOAHXzi2oBkcQWY0iqN1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/k8pVQ/btsytiNItQu/ZcuOAHXzi2oBkcQWY0iqN1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/k8pVQ/btsytiNItQu/ZcuOAHXzi2oBkcQWY0iqN1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fk8pVQ%2FbtsytiNItQu%2FZcuOAHXzi2oBkcQWY0iqN1%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3468&quot; height=&quot;1806&quot; data-filename=&quot;6.jpg&quot; data-origin-width=&quot;3468&quot; data-origin-height=&quot;1806&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;아래 캡쳐본에서 JpaTransactionManager 의 박스친 부분을 보면 &lt;b&gt;newTransaction = false&lt;/b&gt; 로 설정되는 것을 볼 수 있다. 즉, 현재 만들어지는 트랜잭션은 새로운 트랜잭션이 아니라는 것이다. &lt;b&gt;이는 나중에 COMMIT 에 영향&lt;/b&gt;을 미친다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;7.jpg&quot; data-origin-width=&quot;3342&quot; data-origin-height=&quot;2066&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bvIUX8/btsyuGmPgfL/rT1pKKJJ9hzUNEktYHPwIk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bvIUX8/btsyuGmPgfL/rT1pKKJJ9hzUNEktYHPwIk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bvIUX8/btsyuGmPgfL/rT1pKKJJ9hzUNEktYHPwIk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbvIUX8%2FbtsyuGmPgfL%2FrT1pKKJJ9hzUNEktYHPwIk%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3342&quot; height=&quot;2066&quot; data-filename=&quot;7.jpg&quot; data-origin-width=&quot;3342&quot; data-origin-height=&quot;2066&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;이 과정에서 &lt;code&gt;TransactionSynchronizationManager.bindResource()&lt;/code&gt; 가 호출되지 않는다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;REQUIRES_NEW&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;REQUIRES_NEW 는 기존에 트랜잭션이 존재하더라도 새롭게 트랜잭션을 생성한다. REQUIRES_NEW 옵션으로 처음 트랜잭션을 생성하는 것과 REQUIRED 옵션으로 생성하는 것과 과정은 동일하기 때문에 바로 기존 트랜잭션이 존재하는 경우에 대해 알아보자.&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;b&gt;REQUIRES_NEW&lt;/b&gt; 옵션이라도 트랜잭션이 존재하는 상황에서는 &lt;code&gt;isExistingTransaction() == true&lt;/code&gt; 이다. 그래서 &lt;code&gt;handleExistingTransaction()&lt;/code&gt; 이 호출되는데, 해당 메서드 실행 과정이 REQUIRED 와 다르다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #0593d3;&quot;&gt;&lt;b&gt;REQUIRED&lt;/b&gt;&lt;/span&gt;&amp;nbsp;는 &lt;code&gt;handleExistingTransaction()&lt;/code&gt;의 &lt;span style=&quot;color: #0593d3;&quot;&gt;else 블록이 실행&lt;/span&gt;됐지만 &lt;span style=&quot;color: #f3c000;&quot;&gt;&lt;b&gt;REQUIRES_NEW&lt;/b&gt;&lt;/span&gt; 는 &lt;span style=&quot;color: #f3c000;&quot;&gt;다른 블록이 실행&lt;/span&gt;된다. 다른 블록이 실행되는 이유는 &lt;b&gt;definition 필드&lt;/b&gt;가 다르기 때문이다. definition 에는 propagation, isolation 정보가 있다.&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;code&gt;startTransaction()&lt;/code&gt; 이 실행된다는 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;8.jpg&quot; data-origin-width=&quot;3792&quot; data-origin-height=&quot;1928&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b4AYpK/btsyuiGvlhu/ktZ4XsdUH33DS7CZGDnG8k/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b4AYpK/btsyuiGvlhu/ktZ4XsdUH33DS7CZGDnG8k/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b4AYpK/btsyuiGvlhu/ktZ4XsdUH33DS7CZGDnG8k/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb4AYpK%2FbtsyuiGvlhu%2FktZ4XsdUH33DS7CZGDnG8k%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3792&quot; height=&quot;1928&quot; data-filename=&quot;8.jpg&quot; data-origin-width=&quot;3792&quot; data-origin-height=&quot;1928&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;이를 통해, REQUIRES_NEW 로 만들어진 트랜잭션은 &lt;b&gt;newTransaction 속성이 true&lt;/b&gt; 로 만들어지고, &lt;code&gt;doBegin()&lt;/code&gt; 을 통해 해당 트랜잭션 관련 리소스도 TransactionSynchronizationManager에 저장된다. 물론 기존 값에 대한 처리도 같이 해주고 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;결론&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디버깅을 통해 REQUIRED 는 어떻게 기존 트랜잭션에 참여하고, REQUIRES_NEW 는 어떻게 매번 새로운 트랜잭션을 만드는지 살펴봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단하게 결론만 요약하자면 &lt;code&gt;AbstractPlatformTransactionManager.handleExistingTransaction()&lt;/code&gt; 메서드의 분기문에 의해 REQUIRED, REQUIRES_NEW 동작이 달라지는 것을 확인할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;혹시 잘못된 부분 있으면 언제든 댓글 달아주세요~&lt;/p&gt;</description>
      <category>Spring</category>
      <category>propagation</category>
      <category>Required</category>
      <category>REQUIRES_NEW</category>
      <category>Spring</category>
      <category>Transaction</category>
      <author>0h0</author>
      <guid isPermaLink="true">https://00h0.tistory.com/100</guid>
      <comments>https://00h0.tistory.com/100#entry100comment</comments>
      <pubDate>Sat, 14 Oct 2023 23:25:18 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] @Transactional 물리 트랜잭션과 논리 트랜잭션 커밋 동작 차이</title>
      <link>https://00h0.tistory.com/99</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;size16&quot;&gt;이번 글에서는 @Transactional 이 커밋되는 과정을 물리 트랜잭션, 논리 트랜잭션 2 경우에 대해 디버깅을 통해 공부한 과정을 정리할 예정이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 트랜잭션에는 물리 트랜잭션과 논리 트랜잭션이란 개념이 있다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;물리 트랜잭션은 데이터베이스 커넥션을 통해 우리의 쿼리가 실제 커넥션을 통해 커밋/롤백 하는 역할을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;논리 트랜잭션은 A라는 트랜잭션에 B 라는 트랜잭션이 참가하는 경우. 즉, 하나의 트랜잭션 내부에 다른 트랜잭션이 추가로 사용하는 경우 이 트랜잭션들을 논리 트랜잭션 이라고 한다.&lt;br /&gt;트랜잭션이 하나라면 물리, 논리 트랜잭션을 구분하지 않는다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;트랜잭션의 COMMIT 동작 흐름&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@TransactIonal 을 활용한 트랜잭션은 AbstractPlatformTransactionManager 클래스의 commit() 을 통해 이루어진다. 그런데 해당 메서드의 내부를 보면 아래와 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;블로그.001.jpeg&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ondRq/btsyuEoVx52/hvltUEfqWGRsjrIsRnoNwk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ondRq/btsyuEoVx52/hvltUEfqWGRsjrIsRnoNwk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ondRq/btsyuEoVx52/hvltUEfqWGRsjrIsRnoNwk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FondRq%2FbtsyuEoVx52%2FhvltUEfqWGRsjrIsRnoNwk%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1920&quot; height=&quot;1080&quot; data-filename=&quot;블로그.001.jpeg&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 status 는 트랜잭션의 상태다. 내부에 다양한 값이 들어있지만, 그 중 boolean newTransaction 필드가 존재한다. 이를 통해, 커밋하려는 트랜잭션이 가장 외부에서 생성된 물리 트랜잭션인지 논리 트랜잭션인지 판단하는 것으로 추측한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 현재 트랜잭션이 기존 트랜잭션에 참가한 논리 트랜잭션이라면 newTransaction = false 로 설정되어 있어 COMMIT 되지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 실제 예제 코드를 통해 살펴보자.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
@Transactional
public class Publisher {

    private final ApplicationEventPublisher publisher;
    private final EventEntityRepo eventEntityRepo;

    public void publish() {
        eventEntityRepo.save(new EventEntity());
        publisher.publishEvent(new MyEvent());
    }
}&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;Publisher.publish() 메서드 호출로 인해 &lt;b&gt;트랜잭션을 얻고 실행&lt;/b&gt;한다.&lt;/li&gt;
&lt;li&gt;eventEntityRepo.save() 를 호출하면서 &lt;b&gt;동일하게 트랜잭션을 얻고 실행&lt;/b&gt;한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;eventEntityRepo.save() 작업은 중간에 참여하는 다른 트랜잭션이 없기 때문에&amp;nbsp;&lt;b&gt;바로 COMMIT 까지 진행한다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;publish() 메서드가 끝나면서&lt;/b&gt; 해당 메서드를 시작하면서 얻은 &lt;b&gt;1번 트랜잭션을 COMMIT&lt;/b&gt; 한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 과정을 살펴보기 전에 한 가지 명심하고 가면 좋은 점은 &lt;b&gt;물리 트랜잭션&lt;/b&gt;은 트랜잭션의 status 중 newTransaction = true 이고, &lt;b&gt;논리 트랜잭션&lt;/b&gt;은 newTransaction = false 라는 것이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1번. Publisher.publish() 호출을 통한 트랜잭션 생성&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 코드는 &lt;b&gt;AbstractPlatformTransactionManager&lt;/b&gt; 의 getTransaction() 코드다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;블로그 - 물리 트랜잭션과 논리 트랜잭션 커밋 동작 차이.002.jpeg&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bDFsxa/btsysrK8nxu/6EWPhikWBE0IF1oquv9SO0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bDFsxa/btsysrK8nxu/6EWPhikWBE0IF1oquv9SO0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bDFsxa/btsysrK8nxu/6EWPhikWBE0IF1oquv9SO0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbDFsxa%2FbtsysrK8nxu%2F6EWPhikWBE0IF1oquv9SO0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1920&quot; height=&quot;1080&quot; data-filename=&quot;블로그 - 물리 트랜잭션과 논리 트랜잭션 커밋 동작 차이.002.jpeg&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브레이크 포인트(110 줄)로 설정한 부분은 추상 클래스인 &lt;b&gt;AbstractPlatformTransactionManager.doGetTransaction() 을 오버라이딩한 자식 클래스의 메서드가 호출된다. JPA 를 사용하면 JpaTransactionManager.doGetTransaction() 이 호출된다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;110번 라인(브레이크 포인트) 까지 실행하고 만들어진 transation 인스턴스를 이용해 132번 라인에서 &lt;b&gt;startTransaction()&lt;/b&gt; 을 호출해 &lt;b&gt;TransactionStatus&lt;/b&gt; 인스턴스를 생성한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;블로그 - 물리 트랜잭션과 논리 트랜잭션 커밋 동작 차이.003.jpeg&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vaeRb/btsynZuRLCz/td2sxNVWGRNca5NWdjhlb1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vaeRb/btsynZuRLCz/td2sxNVWGRNca5NWdjhlb1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vaeRb/btsynZuRLCz/td2sxNVWGRNca5NWdjhlb1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvaeRb%2FbtsynZuRLCz%2Ftd2sxNVWGRNca5NWdjhlb1%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1920&quot; height=&quot;1080&quot; data-filename=&quot;블로그 - 물리 트랜잭션과 논리 트랜잭션 커밋 동작 차이.003.jpeg&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 눈여겨 볼 점은 &lt;b&gt;newTransaction = true&lt;/b&gt; 로 설정해주는 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;블로그.003.jpeg&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bRfCc3/btsys7S9oSl/beVW9R9kRlM3dkkBry3kHk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bRfCc3/btsys7S9oSl/beVW9R9kRlM3dkkBry3kHk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bRfCc3/btsys7S9oSl/beVW9R9kRlM3dkkBry3kHk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbRfCc3%2Fbtsys7S9oSl%2FbeVW9R9kRlM3dkkBry3kHk%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1920&quot; height=&quot;1080&quot; data-filename=&quot;블로그.003.jpeg&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 TransactionSynchronizationManager 의 스레드 로컬에 디비 작업 관련 리소스를 바인딩 해주고 autocommit = false 로 설정하는 작업 등을 수행함으로써 트랜잭션 커밋 직전까지의 작업을 수행하면서 1번 작업이 종료된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2번 eventEntityRepo.save() 를 호출을 통한 트랜잭션 생성 및 커밋&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 Publisher.publish() 메서드가 끝나지 않았기 때문에 1번 트랜잭션의 COMMIT 이 발생하지 않고 2번 작업으로 넘어간다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;eventEntityRepo.save()&lt;/b&gt; &lt;b&gt;트랜잭션 생성 과정&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SimpleJpaRepository 의 메서드에도 @Transactional 이 붙어 있기 때문에 2번 작업을 수행하기 위해서 트랜잭션을 시작하는 작업이 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 1번 과는 살짝 다른 과정이 발생하는데 이를 디버깅을 통해 살펴보자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;블로그.004.jpeg&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bTH9bY/btsytiGUf7p/UfL9jkKMK2rouUhXjbgra0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bTH9bY/btsytiGUf7p/UfL9jkKMK2rouUhXjbgra0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bTH9bY/btsytiGUf7p/UfL9jkKMK2rouUhXjbgra0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbTH9bY%2FbtsytiGUf7p%2FUfL9jkKMK2rouUhXjbgra0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1920&quot; height=&quot;1080&quot; data-filename=&quot;블로그.004.jpeg&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 상황을 살펴보면 1번과 동일하게 &lt;b&gt;AbstractPlatformTransactionManager.getTransaction()&lt;/b&gt; 이 호출됐지만 &lt;b&gt;publish() 에서 얻은 트랜잭션이 존재&lt;/b&gt;하기 때문에 if 문에 걸리게 된다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;this.isExistingTransaction() 의 동작 방식은 JpaObject transaction의 entityManagerHolder != null &amp;amp;&amp;amp; entityManagerHolder.entityManagerHolder.isTransactionActive() 을 통해 결정된다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;110 번 줄이 실행되면서 JpaTransactionManager 내부적으로 TransactionSynchronizationManager 의 스레드 로컬을 통해 값이 있으면 transaction 에 entityManager 값을 할당해준다. 이미 publish() 의 트랜잭션 할당 과정에서 스레드 로컬에 디비 관련 리소스가 할당됐기 때문에 2번 트랜잭션을 만들면서 entityManager 가 null 이 아닌 스레드로컬에 할당된 값이 바인딩 되면서 해당 if 문에 걸리게 되는 것이다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 이번에는 113 번 줄(&lt;b&gt;this.handleExistingTransaction()&lt;/b&gt;)을 통해 &lt;b&gt;TransactionStatus&lt;/b&gt; 인스턴스가 생성된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;this.handleExistingTransaction()&lt;/b&gt; 코드는 매우 길기 때문에 실제 수행되는 코드만 캡쳐했다. else 로직이 수행된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;블로그.006.jpeg&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/TourB/btsysoOmouG/fCBn0l61DKpv2lOE1XVxd0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/TourB/btsysoOmouG/fCBn0l61DKpv2lOE1XVxd0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/TourB/btsysoOmouG/fCBn0l61DKpv2lOE1XVxd0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FTourB%2FbtsysoOmouG%2FfCBn0l61DKpv2lOE1XVxd0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1920&quot; height=&quot;1080&quot; data-filename=&quot;블로그.006.jpeg&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해 newTransaction = false 인 트랜잭션이 생성됐다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;블로그.007.jpeg&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cr3gYr/btsytcAeEKC/CpKsFsK1Ky6VUfjSKicI8K/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cr3gYr/btsytcAeEKC/CpKsFsK1Ky6VUfjSKicI8K/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cr3gYr/btsytcAeEKC/CpKsFsK1Ky6VUfjSKicI8K/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcr3gYr%2FbtsytcAeEKC%2FCpKsFsK1Ky6VUfjSKicI8K%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1920&quot; height=&quot;1080&quot; data-filename=&quot;블로그.007.jpeg&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;트랜잭션 커밋 과정&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 eventEntityRepo.save() 메서드가 종료되면서 해당 트랜잭션은 커밋 되어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정에서 &lt;b&gt;AbstractPlatformTransactionManager.processCommit() 이 호출된다. 이 코드도 매우 길기 때문에 호출되는 부분만 캡쳐했다.&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;블로그.008.jpeg&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ezrtlx/btsywjY8pjU/VkOE5kkY4y87urciV215R0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ezrtlx/btsywjY8pjU/VkOE5kkY4y87urciV215R0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ezrtlx/btsywjY8pjU/VkOE5kkY4y87urciV215R0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fezrtlx%2FbtsywjY8pjU%2FVkOE5kkY4y87urciV215R0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1920&quot; height=&quot;1080&quot; data-filename=&quot;블로그.008.jpeg&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;isNewTransaction() == false&lt;/b&gt; 로 인해 &lt;b&gt;doCommit()&lt;/b&gt; 메서드가 실행되지 않기 때문에 &lt;b&gt;해당 트랜잭션은 commit 되지 않는다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 sql 로그를 확인해보면 이 시점에는 insert 쿼리가 나가지 않는 것을 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;블로그.009.jpeg&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vEDC4/btsyueRzTEq/Pot8MFYQMuWSK0SCQdjXK1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vEDC4/btsyueRzTEq/Pot8MFYQMuWSK0SCQdjXK1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vEDC4/btsyueRzTEq/Pot8MFYQMuWSK0SCQdjXK1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvEDC4%2FbtsyueRzTEq%2FPot8MFYQMuWSK0SCQdjXK1%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1920&quot; height=&quot;1080&quot; data-filename=&quot;블로그.009.jpeg&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3번 Publisher.publish() 트랜잭션 커밋&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 외부에서 트랜잭션을 얻은 &lt;b&gt;Publisher.publish() 메서드가 종료되는 시점&lt;/b&gt;에 아래의 코드가 실행된다. 2번과 다르게 &lt;b&gt;newTransaction == true&lt;/b&gt; 이기 때문에 &lt;b&gt;this.doCommit() 이 호출&lt;/b&gt;된다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;블로그.010.jpeg&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ukC2W/btsyrAaHhpQ/zk6sIjpmBBgwxB2Xi9KXxk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ukC2W/btsyrAaHhpQ/zk6sIjpmBBgwxB2Xi9KXxk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ukC2W/btsyrAaHhpQ/zk6sIjpmBBgwxB2Xi9KXxk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FukC2W%2FbtsyrAaHhpQ%2Fzk6sIjpmBBgwxB2Xi9KXxk%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1920&quot; height=&quot;1080&quot; data-filename=&quot;블로그.010.jpeg&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;doCommit()은 JpaTransactionManager 가 오버라이딩한 doCommit()이 실행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 보면 해당 트랜잭션이 가지고 있는 entityManager 를 가지고 와서 commit() 을 호출한다. 이런 식으로 계속 타고 들어가다 보면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;flush() 를 호출해서 entityManager 가 저장하고 있는 쿼리가 실질적으로 외부 트랜잭션의 커넥션을 이용해 데이터베이스에 커밋된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;블로그ㅁ.001.jpeg&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b9E2X5/btsytMgC0Ij/mKq16Xk96rxLrGkxOwVI2k/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b9E2X5/btsytMgC0Ij/mKq16Xk96rxLrGkxOwVI2k/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b9E2X5/btsytMgC0Ij/mKq16Xk96rxLrGkxOwVI2k/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb9E2X5%2FbtsytMgC0Ij%2FmKq16Xk96rxLrGkxOwVI2k%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1920&quot; height=&quot;1080&quot; data-filename=&quot;블로그ㅁ.001.jpeg&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;블로그.012.jpeg&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/loxGg/btsywg2pI9C/mADHgomT5hFK7M5Qut3vnK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/loxGg/btsywg2pI9C/mADHgomT5hFK7M5Qut3vnK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/loxGg/btsywg2pI9C/mADHgomT5hFK7M5Qut3vnK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FloxGg%2Fbtsywg2pI9C%2FmADHgomT5hFK7M5Qut3vnK%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1920&quot; height=&quot;1080&quot; data-filename=&quot;블로그.012.jpeg&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;그럼 물리 트랜잭션과 논리 트랜잭션은 entityManager 를 공유할까?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커밋 과정을 다시 한 번 살펴보면 물리 트랜잭션 한 번의 커밋으로 논리 트랜잭션에서 발생한 쿼리도 같이 flush() 가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분에서 아래와 같은 의문점이 생겼다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;중간에 참여한 트랜잭션에서 발생한 쿼리는 어디서 관리되길래 외부 트랜잭션 커밋 한 번으로 그동안 발생한 쿼리가 데이터베이스로 나가는걸까?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 물리 트랜잭션과 논리 트랜잭션의 entityManager 를 비교해보기 위해 다시 한 번 디버깅을 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 결과 &lt;b&gt;물리 트랜잭션이 가지고 있는 entityManager 를 해당 물리 트랜잭션에 참여한 논리 트랜잭션이 그대로 사용&lt;/b&gt;하고 있는 것을 확인 할 수 있었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;블로그.013.jpeg&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mTvkX/btsyuauOQJ0/LXPzTaluE4gfzsx4HfKNL1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mTvkX/btsyuauOQJ0/LXPzTaluE4gfzsx4HfKNL1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mTvkX/btsyuauOQJ0/LXPzTaluE4gfzsx4HfKNL1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmTvkX%2FbtsyuauOQJ0%2FLXPzTaluE4gfzsx4HfKNL1%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1920&quot; height=&quot;1080&quot; data-filename=&quot;블로그.013.jpeg&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>Spring</category>
      <category>commit</category>
      <category>JPA</category>
      <category>Spring</category>
      <category>Transactional</category>
      <category>논리 트랜잭션</category>
      <category>물리 트랜잭션</category>
      <author>0h0</author>
      <guid isPermaLink="true">https://00h0.tistory.com/99</guid>
      <comments>https://00h0.tistory.com/99#entry99comment</comments>
      <pubDate>Sat, 14 Oct 2023 17:28:03 +0900</pubDate>
    </item>
    <item>
      <title>[JPA] 상속관계 사용 시 주의점</title>
      <link>https://00h0.tistory.com/98</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;size16&quot;&gt;우테코에서 프로젝트를 진행하면서 엔티티 상속 구조를 JOINED 전략을 사용한 적이 있었는데, 이 과정에서 경험한 상속 구조 사용 시 주의점에 대해 간략하게 정리할 예정이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;상속 구조를 적용한 상황&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리 서비스는 2종류의 회원이 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;전화번호를 통해 가입한 임시회원&lt;/li&gt;
&lt;li&gt;실제 우리 서비스에 접속해 회원가입을 한 정식 회원&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&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;size16&quot;&gt;결론부터 빠르게 정리하자면 &lt;b&gt;abstract 클래스의 dtype 을 변경할 일이 있을 때는 사용하기 부적절&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;size16&quot;&gt;위에 작성한 dtype 변경 상황을 예로 들자면, &lt;b&gt;임시회원 상태에서 서비스에 접속해 회원가입&lt;/b&gt; 하면 해당 고객은 정식 회원 타입으로 변경해야 한다. 이 과정에서 &lt;b&gt;customer 테이블의 dtype 을 수정&lt;/b&gt;해야 하는데, 이를 위해서는 자바 코드가 아닌 sql 을 직접 작성해야 한다.&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;b&gt;처음 엔티티 구조&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 이러한 구조를 택한 이유는 하나의 단일 테이블에 Temporary, Register 에 필요한 컬럼을 몰아넣으면 &lt;b&gt;null 값이 존재하기 때문에 이를 피하기 위함&lt;/b&gt;이 가장 컸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 JPA의 상속에 대해 잘 모르는 상태에서 &lt;b&gt;dtype 값을 자바 코드를 통해 변경할 수 있다고 생각&lt;/b&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-filename=&quot;상속구조.001.jpeg&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dd1YNI/btsw7yXC1qd/yxJlFPkoaHMp3UwqMF8WzK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dd1YNI/btsw7yXC1qd/yxJlFPkoaHMp3UwqMF8WzK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dd1YNI/btsw7yXC1qd/yxJlFPkoaHMp3UwqMF8WzK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdd1YNI%2Fbtsw7yXC1qd%2FyxJlFPkoaHMp3UwqMF8WzK%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1920&quot; height=&quot;1080&quot; data-filename=&quot;상속구조.001.jpeg&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&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;size16&quot;&gt;임시회원인 상태에서 정식회원으로 가입 이후 임시회원의 데이터를 &lt;b&gt;정식으로 가입한 Customer 에 맞게 연동하는 과정에서 문제가 발생&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;size16&quot;&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;size16&quot;&gt;이 과정에서 우리 팀은 다양한 시도를 했지만, 결국 &lt;b&gt;상속구조를 제거하는 방향으로 엔티티 구조를 수정&lt;/b&gt;하기로 결정했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;시도1 (fk 수정) 채택 X&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;임시회원 상태일 때 발급받은 Coupon 의 fk 를 새롭게 가입한 회원의 id 로 업데이트 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;상속구조.005.jpeg&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/beuGoz/btswaqnTzR6/Ght0SAYpyeaLZ5LRQrTfTK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/beuGoz/btswaqnTzR6/Ght0SAYpyeaLZ5LRQrTfTK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/beuGoz/btswaqnTzR6/Ght0SAYpyeaLZ5LRQrTfTK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbeuGoz%2FbtswaqnTzR6%2FGht0SAYpyeaLZ5LRQrTfTK%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1920&quot; height=&quot;1080&quot; data-filename=&quot;상속구조.005.jpeg&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&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;size16&quot;&gt;즉, Coupon 에 있는 fk 값들을 우리가 조절하는 것이다. 이 방법을 선택하지 않은 이유는 아래와 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;개발자의 실수로 fk 를 업데이트 하는 로직을 잘못 짜면 &lt;b&gt;데이터 무결성에 문제&lt;/b&gt;가 생길 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;fk 수정 시 관련된 index 데이터를 저장하는 구조가 바뀌&lt;/b&gt;기 때문에 &lt;b&gt;mysql 내부적으로 추가적인 작업이 벌어질 것으로 예상&lt;/b&gt;했다. (mysql 은 인덱스 데이터를 정렬해서 저장하기 때문에 내부 값이 변경되면 전체적인 재정렬 작업이 발생한다.)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;시도2 (단일 테이블 전략 + dtype 수정 sql 작성) 채택 X&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JOINED 전략을 버리고 &lt;b&gt;단일 테이블 전략을 고민&lt;/b&gt;해봤다. 이 방법은 &lt;b&gt;테이블의 fk 값을 변경하지 않아도 된다는 장점&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;size16&quot;&gt;그러나 &lt;b&gt;dtype 을 수정하기 위한 별도의 sql 을 작성해야 한다는 단점&lt;/b&gt;이 있다. 더불어, JPA 영속성 컨텍스트의 쓰기 지연 저장소에 있는 쿼리가 발생하는 시점 중 하나가 jpql 을 실행할 때도 있기 때문에 dtype 을 수정하는 쿼리의 실행 시점도 신경써야 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;데이터 연동 흐름&lt;/b&gt;&lt;/h4&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;정식 회원 가입한 새로운 customer 레코드가 생긴다.&lt;/li&gt;
&lt;li&gt;이와 맞는 임시회원의 컬럼에 새롭게 가입한 Customer 의 정보를 업데이트한다.&lt;/li&gt;
&lt;li&gt;수정된 임시회원의 dtype 을 register 로 변경하는 쿼리를 실행한다.&lt;/li&gt;
&lt;li&gt;정식 회원 가입하면서 생긴 새로운 레코드를 제거한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;3번과 4번의 실행 순서를 바꾸는게 영속성 컨텍스트의 쓰기 지연 저장소를 더 잘 활용하는 방법인 것 같다.&lt;/blockquote&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;처음 &lt;b&gt;1번(정식 회원 가입 시 새로운 레코드 생성) 까지의 상황&lt;/b&gt;은 아래 그림과 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;상속구조.002.jpeg&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b7IBcJ/btsw6Z8089L/UXRtGS9LReMa5kZQlkEBJ1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b7IBcJ/btsw6Z8089L/UXRtGS9LReMa5kZQlkEBJ1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b7IBcJ/btsw6Z8089L/UXRtGS9LReMa5kZQlkEBJ1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb7IBcJ%2Fbtsw6Z8089L%2FUXRtGS9LReMa5kZQlkEBJ1%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1920&quot; height=&quot;1080&quot; data-filename=&quot;상속구조.002.jpeg&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 2, 3 번 작업을 수행하면 아래와 같이 변경된다. &lt;b&gt;(정식 회원의 값을 임시 회원 레코드로 덮어쓰기)&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;상속구조.003.jpeg&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cb7sfN/btswatkBbFh/UgF1DfEN3m5fZG72dkyM21/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cb7sfN/btswatkBbFh/UgF1DfEN3m5fZG72dkyM21/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cb7sfN/btswatkBbFh/UgF1DfEN3m5fZG72dkyM21/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcb7sfN%2FbtswatkBbFh%2FUgF1DfEN3m5fZG72dkyM21%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1920&quot; height=&quot;1080&quot; data-filename=&quot;상속구조.003.jpeg&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 4번 작업을 실행하면 아래와 같다. &lt;b&gt;(정식 회원 레코드 제거)&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;상속구조.004.jpeg&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/PBnpj/btsw2LwuSpN/JHHAViK2xgaKTRusJbBtL1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/PBnpj/btsw2LwuSpN/JHHAViK2xgaKTRusJbBtL1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PBnpj/btsw2LwuSpN/JHHAViK2xgaKTRusJbBtL1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPBnpj%2Fbtsw2LwuSpN%2FJHHAViK2xgaKTRusJbBtL1%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1920&quot; height=&quot;1080&quot; data-filename=&quot;상속구조.004.jpeg&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&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;size16&quot;&gt;위에서 잠깐 언급했듯이 &lt;b&gt;dtype 을 수정하기 위한 sql 쿼리를 별도로 작성&lt;/b&gt;해야 한다. 이것을 단점이라고 생각한 이유는 아래와 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터 연동 과정에서 갑자기 sql 을 활용해 dtype 을 바꾸는 코드가 있다면 &lt;b&gt;추후 유지보수성이 떨어질 수 있다.&lt;/b&gt; 왜냐하면 이 모든 상황을 아는 우리는 이해할 수 있겠지만 &lt;b&gt;이 코드를 처음 본 사람은 이해하는데 조금의 시간이 걸릴 수도 있다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;그리고, dtype 은 상속구조를 사용할 때 JPA 가 해당 엔티티의 타입을 판별하는데 쓰이는 컬럼이다. 그런데 이 컬럼을 우리가 &lt;b&gt;임의로 수정했을 때 발생할 영향을 예측하기 어려웠다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 이유로 인해 이 방법도 선택하지 않았다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;시도3 (상속 구조 제거) 채택 O&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 결국 상속 구조를 제거하고 CustomerType 이라는 enum 을 통해 Customer 를 구분하기로 했다. 이후 데이터 연동 작업은 시도2와 동일하게 가져가기로 했다.&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;이 방법을 통해 시도2 에서 단점으로 생각했던 dtype 을 변경하기 위해 sql 을 작성한다는 점을 보완할 수 있었다. 단순히 Customer 의 customerType 만 REGISTER 로 변경하면 JPA의 변경감지를 통해 Customer 의 타입을 손쉽게 바꿀 수 있기 때문이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;마무리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트에서 상속 구조를 사용하면서 겪은 어려움과 이를 해결하기 위해 시도한 방법들을 정리해봤다.&lt;/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;</description>
      <category>JPA</category>
      <category>JPA</category>
      <category>상속</category>
      <author>0h0</author>
      <guid isPermaLink="true">https://00h0.tistory.com/98</guid>
      <comments>https://00h0.tistory.com/98#entry98comment</comments>
      <pubDate>Thu, 5 Oct 2023 16:12:56 +0900</pubDate>
    </item>
    <item>
      <title>[JPA] 엔티티 delete 시 발생하는 N+1 개선</title>
      <link>https://00h0.tistory.com/97</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;size16&quot;&gt;프로젝트 중 spring data jpa 의 deleteAll() 을 사용할 때 매우 많은 쿼리가 발생했는데, 그 이유와 해결방안에 대해서 작성할 예정입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 사진과 같이 Coupon, CouponDesign 이 연관관계가 설정되어 있고, cascade.REMOVE 옵션도 설정되어 있는 상태에서 여러개의 Coupon 을 지우는 상황이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;delete_blog.png&quot; data-origin-width=&quot;1126&quot; data-origin-height=&quot;494&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lRQjQ/btsw2HAq4Iz/1oIYucLZ50I1OnQOTTz1U0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lRQjQ/btsw2HAq4Iz/1oIYucLZ50I1OnQOTTz1U0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lRQjQ/btsw2HAq4Iz/1oIYucLZ50I1OnQOTTz1U0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlRQjQ%2Fbtsw2HAq4Iz%2F1oIYucLZ50I1OnQOTTz1U0%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;1126&quot; height=&quot;494&quot; data-filename=&quot;delete_blog.png&quot; data-origin-width=&quot;1126&quot; data-origin-height=&quot;494&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Coupon&lt;/b&gt;&lt;/h3&gt;
&lt;pre class=&quot;scala&quot;&gt;&lt;code&gt;@Entity
public class Coupon extends BaseDate {

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

    private Boolean deleted = Boolean.FALSE;

    @ManyToOne(fetch = LAZY)
    @JoinColumn(name = &quot;customer_id&quot;)
    private Customer customer;

    @OneToOne(fetch = LAZY, cascade = CascadeType.ALL)
    @JoinColumn(name = &quot;coupon_design_id&quot;)
    private CouponDesign couponDesign;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;CouponDesign&lt;/b&gt;&lt;/h3&gt;
&lt;pre class=&quot;scala&quot;&gt;&lt;code&gt;@Entity
public class CouponDesign extends BaseDate {

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

    private String frontImage;
    private String backImage;
}
&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;size16&quot;&gt;회원 탈퇴 발생 시 회원이 발급받았던 Coupon을 데이터베이스에서 삭제 하는 요구사항이 있다. 그리고 이 과정에서 Coupon 과 연관된 CouponDesign 도 같이 삭제해야한다. CouponDesign 은 Coupon 에서만 사용하고 있기 때문에 cascade 옵션을 통해 요구사항을 만족하려고 했다.&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;b&gt;Coupon 삭제 흐름&lt;/b&gt;은 아래와 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. customer 에 맞는 List&amp;lt;Coupon&amp;gt; 을 조회&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. repository.deleteAll(coupons) 를 통해 회원 탈퇴 시 해당 회원이 보유한 쿠폰을 삭제하고 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;repository.deleteAll() 호출 시 발생하는 쿼리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;deleteAll(List.of(Coupon))&lt;/b&gt; 을 통해 한 개의 쿠폰을 지운다고 가정해보자. 이를 실행하면 아래와 같이 &lt;b&gt;4개의 쿼리&lt;/b&gt;가 나간다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Coupon 조회&lt;/li&gt;
&lt;li&gt;CouponDesign 조회&lt;/li&gt;
&lt;li&gt;Coupon 삭제&lt;/li&gt;
&lt;li&gt;CouponDesign 삭제&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 지우려는 쿠폰이 2개라면 8개, 3개면 12개의 쿼리가 발생한다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;Hibernate: 
    select
        c1_0.id,
        c1_0.coupon_design_id,
        c1_0.customer_id,
    from
        coupon c1_0 
    where
        c1_0.id=? 
Hibernate: 
    select
        c1_0.id,
        c1_0.back_image_url,
        c1_0.deleted,
        c1_0.front_image_url,
    from
        coupon_design c1_0 
    where
        c1_0.id=? 
Hibernate: 
    UPDATE
        coupon 
    SET
        deleted = true 
    WHERE
        id = ?
Hibernate: 
    UPDATE
        coupon_design 
    SET
        deleted = true 
    WHERE
        id = ?
&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;size16&quot;&gt;분명히 Coupon 만 지우려고 했는데 select, delete 쿼리가 예상보다 많이 나가고 있다. 그 이유는 뭘까? &lt;span style=&quot;color: #f89009;&quot;&gt;&lt;b&gt;SimpleJpaRepository&lt;/b&gt;&lt;/span&gt; 코드를 살펴보면 그 이유를 알 수 있다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Override
@Transactional
public void deleteAll(Iterable&amp;lt;? extends T&amp;gt; entities) {

	Assert.notNull(entities, &quot;Entities must not be null&quot;);

	for (T entity : entities) {
		delete(entity);
	}
}

@Override
@Transactional
@SuppressWarnings(&quot;unchecked&quot;)
public void delete(T entity) {

	Assert.notNull(entity, &quot;Entity must not be null&quot;);

	if (entityInformation.isNew(entity)) {
		return;
	}

	Class&amp;lt;?&amp;gt; type = ProxyUtils.getUserClass(entity);

	T existing = (T) em.find(type, entityInformation.getId(entity));

	// if the entity to be deleted doesn't exist, delete is a NOOP
	if (existing == null) {
		return;
	}

	em.remove(em.contains(entity) ? entity : em.merge(entity));
}&lt;/code&gt;&lt;/pre&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;deleteAll(entities)&lt;/b&gt; 호출 시 &lt;b&gt;entities 를 순회하면서 delete(entity)&lt;/b&gt; 를 호출한다.&lt;/li&gt;
&lt;li&gt;delete(entity)는 내부적으로 &lt;b&gt;em.find()&lt;/b&gt; 를 통해 지우려는 entity 를 영속성 컨텍스트에 등록한다.&lt;/li&gt;
&lt;li&gt;이후 &lt;b&gt;em.remove()&lt;/b&gt; 를 호출한다. 여기서 cascade 가 걸려있는 엔티티도 삭제하기 위해 영속성 컨텍스트에 등록하는 과정에서 select 쿼리가 추가적으로 나가는 걸로 추측된다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 발생했던 4개의 쿼리가 SimpleJpaRepository 의 어느 부분에서 발생했는지 분석해보자.&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;Coupon 조회&lt;/b&gt;는 &lt;b&gt;2번의 em.find()&lt;/b&gt; 에서 호출된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;CouponDesign 조회&lt;/b&gt;는 &lt;b&gt;3번의 em.remove()&lt;/b&gt; 에서 cascade 옵션으로 인해 select 쿼리가 발생한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Coupon 삭제&lt;/b&gt; 역시 &lt;b&gt;3번의 em.remove()&lt;/b&gt; 에서 발생한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;CouponDesign 삭제&lt;/b&gt;도 &lt;b&gt;3번의 em.remove()&lt;/b&gt; 를 실행할 때 cascade 옵션으로 인해 발생한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 Coupon의 Customer 는 cascade 옵션이 없기 때문에 Coupon 삭제 시 Customer 조회, 삭제 쿼리가 발생하지 않고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 이유로 미루어봤을 때 cascade.REMOVE 옵션이 설정되어 있는 엔티티를 지울 때 JpaRepository 가 제공하는 delete 를 활용하게 되면 예상치 못한 select 쿼리가 발생한다는 것을 알 수 있다.&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;b&gt;지우려는 엔티티(1) + cascade.REMOVE 가 걸려있는 엔티티 조회(N) + 지우려는 엔티티 별 delete 쿼리 (N)&lt;/b&gt;번 만큼의 쿼리가 발생하게 된다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;이 과정에서 한 가지 궁금한 점이 생겼다. 지우려는 엔티티(Coupon) 를 조회할 때 fetch join을 통해 영속성 컨텍스트에 프록시가 아닌 실제 객체를 등록하면 select 쿼리는 발생하지 않을 수도 있겠다는 생각을 했다. 그래서 실제로 List&amp;lt;Coupon&amp;gt; 을 조회할 때 @Query 로 연관 객체를 fetch join 으로 가져오고 deleteAll 을 하니 fetch join 을 한 객체에 한해서 delete 시 select 쿼리가 나가지 않는 것을 확인했다.&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;해결 방법&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론부터 말하자면 별도의 삭제 jpql을 작성했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #f89009;&quot;&gt;&lt;b&gt;SimpleJpaRepository&lt;/b&gt;&lt;/span&gt; 에서 지원하는 delete 는 크게 분류하자면 &lt;b&gt;delete(), deleteAll(), deleteAllInBatch()&lt;/b&gt; 정도가 있다. 이 중 &lt;b&gt;delete()&lt;/b&gt;, &lt;b&gt;deleteAll()&lt;/b&gt; 은 위에서 살펴봤듯이 지우려는 &lt;b&gt;엔티티를 조회(em.find())&lt;/b&gt; 하고 삭제**(em.remove()) 하는 과정에서 예상치 못한 쿼리**가 발생할 수 있다.&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;b&gt;deleteAllInBatch()&lt;/b&gt; 는 쿼리 한 번으로 삭제가 가능하지만 where 절에 삭제하려는 엔티티 개수만큼 &lt;b&gt;or 연산자가 생성&lt;/b&gt;된다. 예를 들어 지우려는 엔티티가 3개면 &lt;b&gt;where id = ? or id =? or id = ?&lt;/b&gt; 이런식으로 쿼리가 발생한다. 그러나 or 을 사용하면 &lt;b&gt;index 를 타지 않을 확률이 매우 높기&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;size16&quot;&gt;이 상황에서 delete로 인해 발생하는 쿼리를 줄이기 위해 &lt;span style=&quot;color: #f89009;&quot;&gt;&lt;b&gt;직접 delete 쿼리를 작성&lt;/b&gt;&lt;/span&gt;하는 방법밖에 떠오르지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;jpql 을 사용해 &lt;b&gt;where 절에 in 을 활용&lt;/b&gt;해 한 번의 쿼리로 List&amp;lt;Coupon&amp;gt; 데이터를 지우고, 불필요한 select 쿼리 발생을 막을 수 있었다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Modifying
@Query(&quot;update Coupon c set c.deleted = true where c in :coupons&quot;)
void deleteAllCoupons(List&amp;lt;Coupon&amp;gt; coupons);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;실제 발생 쿼리&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;Hibernate: 
/* update
    Coupon c 
set
    c.deleted = true 
where
    c in :coupons */ update coupon 
set
    deleted=true 
where
    id in (?,?,?) 
    and (
        coupon.deleted = false
    )
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존과 다르게 한 번의 쿼리로 원하는 List&amp;lt;Coupon&amp;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;size16&quot;&gt;jpql 을 활용한 방법은 cascade 옵션을 활용하지 못한다. 그래서 지우려는 엔티티를 참조하는 객체가 많고, 엔티티 삭제 시 참조한고 있는 객체도 지워야 하는 상황이라면 개발자가 직접 참조하고 있는 다른 엔티티를 삭제하는 쿼리를 직접 작성해야 하는 단점이 있다.&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;이러한 점을 고려해 delete 쿼리가 N+1 만큼 발생하더라도 개발자가 모든 엔티티를 지우는 쿼리를 짜는게 부담스러운 상황이라면 JpaRepository 가 제공하는 delete 기능과 cascade 를 활용할 것 같고, 그렇지 않다면 직접 jpql 을 통해 발생하는 쿼리를 줄여 성능을 챙길 것 같다.&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;</description>
      <category>JPA</category>
      <category>Delete</category>
      <category>JPA</category>
      <category>N+1</category>
      <category>Spring data JPA</category>
      <author>0h0</author>
      <guid isPermaLink="true">https://00h0.tistory.com/97</guid>
      <comments>https://00h0.tistory.com/97#entry97comment</comments>
      <pubDate>Wed, 4 Oct 2023 12:56:03 +0900</pubDate>
    </item>
    <item>
      <title>하나의 객체에서 여러 Builder 사용해보기 (feat. Builder 컴파일 과정, lombok)</title>
      <link>https://00h0.tistory.com/96</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;들어가면서,&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우테코 5기 프로젝트를 진행하면서 처음 빌더 패턴을 써봤다. 그 과정에서 하나의 객체에 대해 &lt;b&gt;2가지 버전의 Builder 를 활용&lt;/b&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;회원은 2가지 종류가 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;임시 회원&lt;/li&gt;
&lt;li&gt;정식 회원&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;임시회원은 전화번호만 필요하고, 정식 회원은 현재 OAuth를 활용하기 때문에 OAuthProvider, OAuthId 가 필요하다. 추가적으로 임시회원은 임시 닉네임으로 전화번호 마지막 4자리를 사용하기 때문에 임시회원만 전화번호를 이용해 닉네임을 생성하는 작업이 필요하다.&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Customer 객체에 회원 종류 별 Builder 메서드를 이용해 각 회원 객체를 생성할 때 개발자의 실수를 줄이고, 회원에 맞는 닉네임을 설정하고 싶었다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에 builderMethodName 이란 옵션을 찾았고, 이를 적용해 아래와 같이 코드를 작성했다. 그러나 내 예상과 다르게 동작해서 공식문서와 컴파일된 코드를 직접 확인 하면서 문제점을 찾아봤다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Getter
public class Customer {

    private String name;
    private String phone;
    private String oAuthProvider;
    private String oAuthID;
    private Type customerType;

    @Builder(builderMethodName = &quot;registeredCustomer&quot;)
    public Customer(String name, String oAuthProvider, String oAuthID) {
        this.name = name;
        this.oAuthProvider = oAuthProvider;
        this.oAuthID = oAuthID;
        this.type = Type.REGISTER;
    }

    @Builder(builderMethodName = &quot;temporaryCustomerBuilder&quot;)
    public Customer(String phone) {
        this.name = &quot;xxxx&quot;;
        this.phone = phone;
        this.type = Type.TEMPORARY;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각각의 @Builder 를 통해 회원 인스턴스를 생성해도&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Customer.temporaryCustomerBuilder().phone(&amp;rdquo;xxx&amp;rdquo;).build()&lt;/code&gt; 를 통해 Customer를 만들고 test 를 돌려보니 실패했는데 그 원인은 lombok 의 @Builder 컴파일 과정에 있었다.&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;b&gt;build() 메서드에서 사용할 파라미터를 가진 생성자를 생성한다.&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예를 들어 phone, name 을 가지는 builder 가 있다면 phone, name 을 파라미터로 가지는 생성자가 생성된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Builder 는 내부적으로 클래스이름+Builder 라는 static class 를 객체 클래스 내부에 생성한다.&lt;/li&gt;
&lt;li&gt;static Builder class 내부에 Builder 메서드의 target 이 되는 &lt;b&gt;파라미터들을 non-static, non-final&lt;/b&gt; 로 생성한다.&lt;/li&gt;
&lt;li&gt;static builder 기본 생성자를 생성한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;build()&lt;/b&gt;, setter 역할을 하는 fieldName() 등의 메서드를 생성한다.&lt;/li&gt;
&lt;li&gt;더 자세한 내용은 &lt;a href=&quot;https://projectlombok.org/features/Builder&quot;&gt;공식문서&lt;/a&gt;에 나와있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 각 builder 를 구분하기 위해 사용한 &lt;b&gt;builderMethodName&lt;/b&gt; 옵션은 내부적으로 builderMethodName 에 선언한 이름으로 Builder 객체를 반환해주는 생성자가 생성된다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;public static CustomerBuilder temporaryCustomerBuilder() {
    return new CustomerBuilder();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나의 경우는 &lt;b&gt;builderMethodName&lt;/b&gt; 을 이용해 2개의 Builder 를 생성했기 때문에 컴파일된 코드를 보면 아래와 같다. 동일한 Builder 인스턴스를 생성해 반환해주는 것을 볼 수 있다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;public static CustomerBuilder temporaryCustomerBuilder() {
    return new CustomerBuilder();
}

public static CustomerBuilder registeredCustomer() {
    return new CustomerBuilder();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 잠깐 언급했지만 내부적으로 각 Builder 의 target 에 맞는 public 생성자가 생성된다. 현재 &lt;b&gt;target parameter 가 다른 2개의 builder&lt;/b&gt; 가 존재하기 때문에 &lt;b&gt;2개의 Customer 생성자가 생성&lt;/b&gt;된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 Builder.build() 메서드는 어떤 생성자를 통해 Customer 인스턴스를 생성해줄까?&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public Customer(String name, String phone, String oAuthProvider, String oAuthID) {
    this.name = name;
    this.phone = phone;
    this.oAuthProvider = oAuthProvider;
    this.oAuthID = oAuthID;
    this.Type = Type.REGISTER;
}

public Customer(String phone) {
    this.name = &quot;xxxx&quot;;
    this.phone = phone;
    this.Type = Type.TEMPORARY;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정답은 &lt;b&gt;먼저 선언한 Builder 에 맞는 생성자&lt;/b&gt;를 호출해 인스턴스를 반환한다. 이 경우 먼저 선언한 정식회원 용 builder 에 맞는 생성자를 호출해준다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;public Customer build() {
    return new Customer(this.name, this.phone, this.oAuthProvider, this.oAuthID);
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;출력문으로 확인해보기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각각의 Builder 에서 호출을 통해 추가적으로 확인해보자.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Getter
public class Customer {

  private String name;
  private String phone;
  private String oAuthProvider;
  private String oAuthID;
  private Type type;

  @Builder(builderMethodName = &quot;registeredCustomerBuilder&quot;)
  public Customer(String name, String phone, String oAuthProvider, String oAuthID) {
      log.info(&quot;call by registeredCustomerBuilder&quot;);
      this.name = name;
      this.phone = phone;
      this.oAuthProvider = oAuthProvider;
      this.oAuthID = oAuthID;
      this.Type = Type.REGISTER;
  }

  @Builder(builderMethodName = &quot;temporaryCustomerBuilder&quot;)
  public Customer(String phone) {
      log.info(&quot;call by temporaryCustomerBuilder&quot;);
      this.name = &quot;xxxx&quot;;
      this.phone = phone;
      this.Type = Type.TEMPORARY;
  }
}

// main method
Customer.registeredCustomerBuilder().name(&quot;one&quot;).build();
Customer.temporaryCustomerBuilder().phone(&quot;two phone&quot;).build();&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;// call by registeredCustomerBuilder
// call by registeredCustomerBuilder&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;source code&lt;/h2&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Getter
public class Customer {

    private String name;
    private String phone;
    private String oAuthProvider;
    private String oAuthID;
    private Type type;

    @Builder(builderMethodName = &quot;temporaryCustomerBuilder&quot;)
    public Customer(String phone) {
        this.name = &quot;xxxx&quot;;
        this.phone = phone;
        this.Type = Type.TEMPORARY;
    }

    @Builder(builderMethodName = &quot;registeredCustomerBuilder&quot;)
    public Customer(String name, String phone, String oAuthProvider, String oAuthID) {
        this.name = name;
        this.phone = phone;
        this.oAuthProvider = oAuthProvider;
        this.oAuthID = oAuthID;
        this.Type = Type.REGISTER;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;compile code&lt;/h2&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class Customer {
	private String name;
	private String phone;
	private String oAuthProvider;
	private String oAuthID;
	private Type type;
    
public Customer(String phone) {
    this.name = &quot;xxxx&quot;;
    this.phone = phone;
    this.Type = Type.TEMPORARY;
}

public Customer(String name, String phone, String oAuthProvider, String oAuthID) {
    this.name = name;
    this.phone = phone;
    this.oAuthProvider = oAuthProvider;
    this.oAuthID = oAuthID;
    this.Type = Type.REGISTER
}

public static CustomerBuilder temporaryCustomerBuilder() {
    return new CustomerBuilder();
}

public static CustomerBuilder registeredCustomerBuilder() {
    return new CustomerBuilder();
}

public static class CustomerBuilder {

    private String phone;
    private String name;
    private String oAuthProvider;
    private String oAuthID;

    CustomerBuilder() {
    }

    public CustomerBuilder phone(final String phone) {
        this.phone = phone;
        return this;
    }

    public Customer build() {
        return new Customer(this.phone);
    }

    public String toString() {
        return &quot;Customer.CustomerBuilder(phone=&quot; + this.phone + &quot;)&quot;;
    }

    public CustomerBuilder name(final String name) {
        this.name = name;
        return this;
    }

    public CustomerBuilder oAuthProvider(final String oAuthProvider) {
        this.oAuthProvider = oAuthProvider;
        return this;
    }

    public CustomerBuilder oAuthID(final String oAuthID) {
        this.oAuthID = oAuthID;
        return this;
    }
}
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결 방법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 생각한 해결 방법은 2가지가 있다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;모든 필드를 targetParameter로 받는 &lt;b&gt;Builder 하나를 통해&lt;/b&gt; 임시회원, 정식회원 인스턴스를 생성하도록 한다.&lt;/li&gt;
&lt;li&gt;각 &lt;b&gt;Builder 별로 별도의 static builder 클래스를 생성&lt;/b&gt;하도록 설정을 변경한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2번 방법을 선택했다. 1번 방법을 선택하지 않은 이유는 임시 회원 인스턴스를 생성할 때 정식회원과 별개로 전화번호 뒷자리를 닉네임으로 설정해주는 로직이 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 2 종류의 회원이 필요한 정보가 아예 다르기 때문에 이를 분리하여 개발자의 실수를 막고 싶었기 때문에 2번을 선택했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Builder 분리하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;builderClassName&lt;/b&gt; 이란 옵션이 있다. 이 옵션을 사용하면 내가 작성한 이름으로 static class 가 생성된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 코드를 보면 2개의 static builder 클래스가 생성된 것을 볼 수 있다. 그리고 &lt;b&gt;각각의 build() 메서드&lt;/b&gt;를 보면 각 &lt;b&gt;builder 에 맞는 생성자를 호출해 Customer 인스턴스를 반환&lt;/b&gt;해주는 것을 볼 수 있다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;public class Customer {

  private String name;
  private String phone;
  private String oAuthProvider;
  private String oAuthID;

  public Customer(String name, String phone, String oAuthProvider, String oAuthID) {
      this.name = name;
      this.phone = phone;
      this.oAuthProvider = oAuthProvider;
      this.oAuthID = oAuthID;
  }

  public Customer(String phone) {
      this.name = &quot;xxxx&quot;;
      this.phone = phone;
  }

  public static RegisteredCustomerBuilder registeredCustomer() {
      return new RegisteredCustomerBuilder();
  }

  public static CustomerBuilder temporaryCustomerBuilder() {
      return new CustomerBuilder();
  }

  public String getName() {
      return this.name;
  }

  public String getPhone() {
      return this.phone;
  }

  public String getOAuthProvider() {
      return this.oAuthProvider;
  }

  public String getOAuthID() {
      return this.oAuthID;
  }

  public static class RegisteredCustomerBuilder {

      private String name;
      private String phone;
      private String oAuthProvider;
      private String oAuthID;

      RegisteredCustomerBuilder() {
      }

      public RegisteredCustomerBuilder name(final String name) {
          this.name = name;
          return this;
      }

      public RegisteredCustomerBuilder phone(final String phone) {
          this.phone = phone;
          return this;
      }

      public RegisteredCustomerBuilder oAuthProvider(final String oAuthProvider) {
          this.oAuthProvider = oAuthProvider;
          return this;
      }

      public RegisteredCustomerBuilder oAuthID(final String oAuthID) {
          this.oAuthID = oAuthID;
          return this;
      }

      public Customer build() {
          return new Customer(this.name, this.phone, this.oAuthProvider, this.oAuthID);
      }
  }

  public static class CustomerBuilder {

      private String phone;

      CustomerBuilder() {
      }

      public CustomerBuilder phone(final String phone) {
          this.phone = phone;
          return this;
      }

      public Customer build() {
          return new Customer(this.phone);

  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빌더를 처음 학습하면서 정리한 내용이라 부족한 부분 댓글로 알려주시면 감사하겠습니다!&lt;/p&gt;</description>
      <category>Spring</category>
      <category>builder</category>
      <category>Lombok</category>
      <category>Spring</category>
      <author>0h0</author>
      <guid isPermaLink="true">https://00h0.tistory.com/96</guid>
      <comments>https://00h0.tistory.com/96#entry96comment</comments>
      <pubDate>Thu, 7 Sep 2023 18:16:48 +0900</pubDate>
    </item>
    <item>
      <title>[JPA] service 테스트 코드 개선하기</title>
      <link>https://00h0.tistory.com/95</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;들어가면서,&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우아한테크코스 레벨3를 진행하면서 모든 layer에 대해 테스트코드를 작성했다. 그 중에서도 &lt;b&gt;&lt;span style=&quot;color: #f89009;&quot;&gt;service레이어 테스트는 @SpringBootTest, @Transactional을 이용한 테스트&lt;/span&gt;&lt;/b&gt;를 진행했다. 그 이유는 우리의 비즈니스 로직 수행 후 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;하지만, RestAssured를 통한 인수테스트를 작성하는 상황에서 service레이어의 통합테스트가 필요한지에 대해 고민하다가 service레이어의 테스트 코드를 수정한 과정에 대해 기록하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;개선 이유1&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #f89009;&quot;&gt;&lt;b&gt;service레이어의 관심사&lt;/b&gt;&lt;/span&gt;가 무엇인지 생각해봤습니다. service레이어는 entity의 메서드, repository의 호출을 통해 &lt;b&gt;요구사항에 맞는 비즈니스 로직을 수행하는 것이 관심사라고 생각&lt;/b&gt;했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, service레이어 단위 테스트는 우리가 원하는 데이터가 실제로 영속화되는지 보다는 우리의 의도대로 비즈니스 로직이 수행되는지에 초점이 맞춰져야 한다고 생각합니다. 하지만 현재 @SpringBootTest를 통한 테스트는 service를 통해 실제 데이터의 영속화까지 검증하기 때문에 service레이어의 단위테스트 초점에 벗어난다고 생각했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;개선 이유2&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #f89009;&quot;&gt;&lt;b&gt;실행 속도가 오래 걸린다&lt;/b&gt;.&lt;/span&gt; 기본적으로 @SpringBootTest는 테스트를 위한 context를 새롭게 띄웁니다. 이 과정은 테스트의 실행속도를 오래걸리게 합니다. 물론 Spring의 &lt;a href=&quot;https://00h0.tistory.com/90&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Test context caching&lt;/a&gt;의 이점을 받을 순 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;제가 사용하고 있는 노트북이 오래된 문제도 있지만,&lt;span&gt; service레이어를 @SpringBootTest로 진행하다보니 테스트 실행 시간이 오래 걸려 테스트를 돌리고 물을 떠오곤 했습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;개선 이유3&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&lt;span style=&quot;color: #f89009;&quot;&gt;&lt;b&gt;테스트를 위한 더미데이터 생성 작업이 너무 복잡&lt;/b&gt;&lt;/span&gt;해진다. 다른 엔티티와 연관관계가 복잡한 엔티티에 대한 service테스트를 할 때면 더미 데이터를 생성하기 위해 연관된 엔티티에 대한 데이터를 모두 생성함으로써 테스트코드가 상당히 복잡해졌습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;Mock테스트로 개선하기&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;이러한 이유로 service레이어의 테스트는 팀 회의를 통해 mocking을 사용하기로 결정했습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;예시 service 로직&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿠폰에 스탬프를 적립하는 요구사항이 있다고 가정하면 스탬프 적립 시 발생할 수 있는 상황은 크게 3가지가 있습니다. 쿠폰에서 보상을 위한 스탬프 개수를 maxStamp라고 부르겠습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;1. 새롭게 스탬프를 찍어도 쿠폰의 스탬프 개수가 &lt;b&gt;maxStamp보다 적은 경우&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;2. 새롭게 스탬프를 찍으면 쿠폰의 스탬프 개수가 &lt;b&gt;maxStamp개수와 같은 경우&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;3. 새롭게 스탬프를 찍으면 쿠폰의 스탬프 개수가 &lt;b&gt;maxStamp를 초과하는 경우&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;1번의 경우 단순히 Stamp데이터만 추가하면 끝납니다. 2번은 스탬프를 찍고 이에 따른 쿠폰에 대한 보상 데이터가 추가되어야 합니다. 3번은 쿠폰에 대한 보상, 초과된 스탬프를 찍을 새로운 쿠폰 데이터도 추가되어야 합니다.&lt;/p&gt;
&lt;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;기존 service테스트 코드&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1691894398625&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
void 기존에_적립된_스탬프가_있을_때_추가_적립한_스탬프로_리워드를_받기_위한_스탬프_개수를_초과하면_리워드를_생성하고_새로운_쿠폰에_나머지_스탬프가_찍힌다() {
    // given, when
    coupon6.accumulate(4);

    managerCouponCommandService.createStamp(new StampCreateDto(owner1.getId(), registerCustomer2.getId(), coupon6.getId(), 9));
    List&amp;lt;Coupon&amp;gt; usingCoupons = couponRepository.findByCafeAndCustomerAndStatus(cafe1, registerCustomer2, CouponStatus.ACCUMULATING);
    Coupon usingCoupon = usingCoupons.stream().findAny().get();

    SoftAssertions softAssertions = new SoftAssertions();
    softAssertions.assertThat(coupon6.getStatus()).isEqualTo(CouponStatus.REWARDED);
    softAssertions.assertThat(coupon6.getStampCount()).isEqualTo(10);
    softAssertions.assertThat(rewardRepository.findAllByCustomerIdAndCafeIdAndUsed(registerCustomer2.getId(), cafe1.getId(), false).size()).isEqualTo(1);
    softAssertions.assertThat(usingCoupons.size()).isEqualTo(1);
    softAssertions.assertThat(usingCoupon.getStampCount()).isEqualTo(3);
    softAssertions.assertAll();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 코드만 보면 괜찮을진 몰라도 위에서 언급한 service단위 테스트에 어울리지 않는 코드가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째로, &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;persistence레이어의 관심사까지 테스트&lt;/b&gt;&lt;/span&gt;하고 있습니다. 저희는 이미 repository에 대한 단위테스트를 작성했고, service는 이를 호출하는지만 확인하면 service레이어는 요구사항에 맞게 동작하고 있음을 보장할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, service로직을 단위 테스트하면서 service비즈니스 로직으로 인해 생성된 실제 데이터를 테스트함으로써 불필요하게 persistence레이어 관심사 까지 테스트하고 있습니다.&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;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;더미데이터 생성 작업이 매우 복잡&lt;/b&gt;&lt;/span&gt;합니다(약 130줄). 그 이유는 스탬프를 적립하기 위한 사전 데이터가 많이 필요했기 때문입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;카페의 사장 회원 데이터&lt;/li&gt;
&lt;li&gt;카페 데이터&lt;/li&gt;
&lt;li&gt;카페의 쿠폰 보상 정책 데이터&lt;/li&gt;
&lt;li&gt;고객 회원 데이터&lt;/li&gt;
&lt;li&gt;고객의 쿠폰 데이터&lt;/li&gt;
&lt;li&gt;등등&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그래서 이러한 더미 데이터를 복잡하게 하나하나 생성해야 했기 때문에 더미데이터의 상황을 파악하는 것도 힘들었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;아래 코드는 매우 긴 더미데이터 생성 코드이기 때문에 쭉 넘어가도 상관없습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1691895266637&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@BeforeEach
void setUp() {
    temporaryCustomer1 = temporaryCustomerRepository.save(TEMPORARY_CUSTOMER_1);
    temporaryCustomer2 = temporaryCustomerRepository.save(TEMPORARY_CUSTOMER_2);
    temporaryCustomer3 = temporaryCustomerRepository.save(TEMPORARY_CUSTOMER_3);
    registerCustomer1 = registerCustomerRepository.save(REGISTER_CUSTOMER_1);
    registerCustomer2 = registerCustomerRepository.save(REGISTER_CUSTOMER_2);

    owner1 = ownerRepository.save(OWNER1);
    owner2 = ownerRepository.save(OWNER2);

    cafe1 = cafeRepository.save(
            new Cafe(
                    &quot;하디까페&quot;,
                    LocalTime.of(12, 30),
                    LocalTime.of(18, 30),
                    &quot;0211111111&quot;,
                    &quot;http://www.cafeImage.com&quot;,
                    &quot;안녕하세요&quot;,
                    &quot;잠실동12길&quot;,
                    &quot;14층&quot;,
                    &quot;11111111&quot;,
                    owner1
            )
    );
    cafe2 = cafeRepository.save(
            new Cafe(
                    &quot;하디까페&quot;,
                    LocalTime.of(12, 30),
                    LocalTime.of(18, 30),
                    &quot;0211111111&quot;,
                    &quot;http://www.cafeImage.com&quot;,
                    &quot;안녕하세요&quot;,
                    &quot;잠실동12길&quot;,
                    &quot;14층&quot;,
                    &quot;11111111&quot;,
                    owner2
            )
    );

    cafeCouponDesign1 = cafeCouponDesignRepository.save(
            new CafeCouponDesign(
                    &quot;past_#&quot;,
                    &quot;past_#&quot;,
                    &quot;past_#&quot;,
                    true,
                    cafe1
            )
    );

    cafeCouponDesign2 = cafeCouponDesignRepository.save(
            new CafeCouponDesign(
                    &quot;cur_#&quot;,
                    &quot;cur_#&quot;,
                    &quot;cur_#&quot;,
                    false,
                    cafe1
            )
    );

    cafePolicy1 = cafePolicyRepository.save(
            new CafePolicy(
                    2,
                    &quot;아메리카노&quot;,
                    12,
                    true,
                    cafe1
            )
    );

    cafePolicy2 = cafePolicyRepository.save(
            new CafePolicy(
                    10,
                    &quot;아메리카노&quot;,
                    12,
                    false,
                    cafe1
            )
    );

    couponDesign1 = couponDesignRepository.save(COUPON_DESIGN_1);
    couponDesign2 = couponDesignRepository.save(COUPON_DESIGN_2);
    couponDesign3 = couponDesignRepository.save(COUPON_DESIGN_3);
    couponDesign4 = couponDesignRepository.save(COUPON_DESIGN_4);
    couponDesign5 = couponDesignRepository.save(COUPON_DESIGN_5);
    couponDesign6 = couponDesignRepository.save(COUPON_DESIGN_6);

    couponPolicy1 = couponPolicyRepository.save(COUPON_POLICY_1);
    couponPolicy2 = couponPolicyRepository.save(COUPON_POLICY_2);
    couponPolicy3 = couponPolicyRepository.save(COUPON_POLICY_3);
    couponPolicy4 = couponPolicyRepository.save(COUPON_POLICY_4);
    couponPolicy5 = couponPolicyRepository.save(COUPON_POLICY_5);
    couponPolicy6 = couponPolicyRepository.save(COUPON_POLICY_6);

    coupon1 = new Coupon(LocalDate.EPOCH, temporaryCustomer1, cafe1, couponDesign1, couponPolicy1);
    Stamp stamp1 = new Stamp();
    stamp1.registerCoupon(coupon1);

    Stamp stamp2 = new Stamp();
    stamp2.registerCoupon(coupon1);
    Coupon save = couponRepository.save(coupon1);
    save.reward();

    coupon2 = new Coupon(LocalDate.EPOCH, registerCustomer1, cafe1, couponDesign2, couponPolicy2);
    Stamp stamp3 = new Stamp();
    stamp3.registerCoupon(coupon2);
    couponRepository.save(coupon2);

    coupon3 = new Coupon(LocalDate.EPOCH, temporaryCustomer2, cafe2, couponDesign3, couponPolicy3);
    Stamp stamp4 = new Stamp();
    stamp4.registerCoupon(coupon3);
    couponRepository.save(coupon3);

    coupon4 = new Coupon(LocalDate.EPOCH, temporaryCustomer3, cafe2, couponDesign4, couponPolicy4);
    couponRepository.save(coupon4);

    coupon5 = new Coupon(LocalDate.EPOCH, registerCustomer2, cafe2, couponDesign5, couponPolicy5);
    couponRepository.save(coupon5);

    coupon6 = new Coupon(LocalDate.EPOCH, registerCustomer2, cafe1, couponDesign6, couponPolicy6);
    couponRepository.save(coupon6);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;mocking으로 수정하기&lt;/h3&gt;
&lt;pre id=&quot;code_1691894662333&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@BeforeAll
static void setUp() {
    cafe = new Cafe(1L, &quot;name&quot;, &quot;road&quot;, &quot;detailAddress&quot;, &quot;phone&quot;, null);
    customer = new TemporaryCustomer(1L, &quot;name&quot;, &quot;phone&quot;);
    couponPolicy = new CouponPolicy(10, &quot;reward&quot;, 6);
}

@Test
void 기존에_스탬프가_있는_쿠폰에_추가_적립한_스탬프로_리워드를_받기_위한_스탬프_개수를_초과하면_리워드를_생성하고_새로운_쿠폰에_나머지_스탬프가_찍힌다() {
    // given
    Coupon currentCoupon = new Coupon(LocalDate.EPOCH, customer, cafe, null, couponPolicy);
    currentCoupon.accumulate(4);
    int maxStampCount = 10;
    스탬프_적립을_위해_필요한_엔티티를_조회한다(maxStampCount, currentCoupon);

    // when
    int earningStamp = 17;
    StampCreateDto stampCreateDto = new StampCreateDto(1L, 1L, 1L, earningStamp);
    managerCouponCommandService.createStamp(stampCreateDto);

    // then
    then(rewardRepository).should(times(2)).save(any());
    then(couponRepository).should(times(2)).save(any());
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;mocking으로 대체하면서 &lt;b&gt;&lt;span style=&quot;color: #f89009;&quot;&gt;더미데이터 setUp은 단 3줄의 코드로 대체&lt;/span&gt;&lt;/b&gt; 가능했습니다. 그리고, persistence레이어의 관심사를 테스트하지 않고 &lt;b&gt;&lt;span style=&quot;color: #f89009;&quot;&gt;service레이어의 코드가 의도한 대로 동작&lt;/span&gt;&lt;/b&gt;하는지 &lt;b&gt;&lt;span style=&quot;color: #f89009;&quot;&gt;BDDMockito의 then(), should(times())를 통해 쉽게 확인&lt;/span&gt;&lt;/b&gt;할 수 있었습니다.&amp;nbsp;&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;이를 통해 위에서 언급한 개선이유 3가지를 모두 개선시킬 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;생각해볼 점&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;mocking을 활용한 테스트는 text context caching의 이점을 챙길 순 없습&lt;/span&gt;&lt;/b&gt;니다. 그래서 단순히 실행속도 관점에서는 &lt;b&gt;@SpringBootTest가 많을 경우&lt;/b&gt; @SpringBootTest가 더 빠를 수도 있다는 생각이 들어, 이 부분은 프로젝트를 더 진행하면서 비교를 해봐야 할 것 같습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고, service레이어가 별다른 비즈니스 로직없이 단순히 repository를 조회해서 이를 그대로 반환하거나 혹은, dto로 변경해서 반환하는 경우 정상적인 상황에 대한 테스트가 필요한지에 대해서는 의문점이 듭니다.&lt;/p&gt;
&lt;pre id=&quot;code_1691895855456&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public List&amp;lt;MemerDto&amp;gt; findMembers(Long teamId) {
    Team team = teamRepository.findById(teamId)
            .orElseThrow(IllegalArgumentException::new);

    List&amp;lt;Member&amp;gt; members = MemberRepository.findByTeam(team);
    return members.stream()
            .map(MemberDto::new)
            .toList();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드에서 service메서드가 수행하는 것은 크게 3가지 입니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;존재하는 team인지 확인&lt;/li&gt;
&lt;li&gt;team에 존재하는 member조회&lt;/li&gt;
&lt;li&gt;member -&amp;gt; dto 변환&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서, 조회된 Member의 데이터를 검증하는 것은 우리가 mocking한 repository의 반환값을 우리가 또 검증하기 때문에 의미가 있는 테스트인지 의문이 들었고. 실제 반환값을 검증하는 것은 repository의 영역이란 생각을 했다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 현재는 이와 같은 경우 예외테스트를 진행하고 정상테스트 역시 repository의 findByTeam메서드가 호출되는지 정도만 호출하면 될 것 같습니다.&lt;/p&gt;</description>
      <category>JPA</category>
      <category>JPA</category>
      <category>service test</category>
      <category>Test</category>
      <author>0h0</author>
      <guid isPermaLink="true">https://00h0.tistory.com/95</guid>
      <comments>https://00h0.tistory.com/95#entry95comment</comments>
      <pubDate>Sun, 13 Aug 2023 12:25:00 +0900</pubDate>
    </item>
    <item>
      <title>[JPA] 테스트에서 repository.save()해도 createdAt이 null일 때</title>
      <link>https://00h0.tistory.com/94</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;들어가면서,&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;repository단위 테스트를 작성하면서 save한 엔티티의 createdAt이 필요했는데, 해당 값이 계속 null인 문제를 해결한 과정입니다.&lt;br /&gt;현재 createdAt은 &lt;b&gt;@EntityListeners(AuditingEntityListener.class)&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;pre class=&quot;haxe&quot;&gt;&lt;code&gt;@DataJpaTest
class CafePolicyRepositoryTest {

    @Autowired
    private CafePolicyRepository cafePolicyRepository;

    @Test
    void 특정_시간보다_이후에_생성된_카페_리워드_정책이_없으면_빈_리스트_반환() {
        Cafe savedCafe = cafeRepository.save(new Cafe());
        CafePolicy currentPolicy = cafePolicyRepository.save(new CafePolicy());

        System.out.println(currentPolicy.getCreatedAt()); // null
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제가 기대한건 cafePolicyRepository.save()를 하면서 엔티티의 createdAt이 생성되고, 이를 활용하길 원했습니다. 그러나 테스트가 계속 실패했고 원인을 살펴보니 createdAt에 null이 들어있었습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;해결 방법&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해결방법은 간단합니다. 바로 Test클래스에 엔티티와 마찬가지로 &lt;b&gt;@EntityListeners(AuditingEntityListener.class)&lt;/b&gt; 를 붙여주면 됩니다.&lt;/p&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;@EntityListeners(AuditingEntityListener.class)를 붙여주면 됩니다.
@DataJpaTest
class CafePolicyRepositoryTest {

    @Autowired
    private CafePolicyRepository cafePolicyRepository;

    @Test
    void 특정_시간보다_이후에_생성된_카페_리워드_정책이_없으면_빈_리스트_반환() {
        Cafe savedCafe = cafeRepository.save(new Cafe());
        CafePolicy currentPolicy = cafePolicyRepository.save(new CafePolicy());

        System.out.println(currentPolicy.getCreatedAt()); // 정상적으로 값이 들어감
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a title=&quot;스택오버플로우&quot; href=&quot;https://stackoverflow.com/questions/51709727/spring-boot-jpacreateddate-lastmodifieddate-not-being-populated-when-saving-th/51710770&quot;&gt;스택오버플로우&lt;/a&gt;를 보면 &lt;b&gt;AuditingEntityListener&lt;/b&gt;는 @PrePersist, @PostPersist절에서 실행된다고 합니다. 실제 &lt;b&gt;AuditingEntityListener&lt;/b&gt;코드를 살펴보겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;AuditingEntityListener&lt;/b&gt;&lt;/h2&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;@Configurable
public class AuditingEntityListener {

    private @Nullable ObjectFactory&amp;lt;AuditingHandler&amp;gt; handler;

    @PrePersist
    public void touchForCreate(Object target) {
        Assert.notNull(target, &quot;Entity must not be null&quot;);

        if (handler != null) {

            AuditingHandler object = handler.getObject();
            if (object != null) {
                object.markCreated(target);
            }
        }
    }

    @PreUpdate
    public void touchForUpdate(Object target) {
        Assert.notNull(target, &quot;Entity must not be null&quot;);

        if (handler != null) {

            AuditingHandler object = handler.getObject();
            if (object != null) {
                object.markModified(target);
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;touchForCreate을 보면 &lt;b&gt;@PrePersist&lt;/b&gt;안에서 target과 handler에 대해 null검사를 하면서 실제 날짜 데이터를 넣어주는 것으로 추정되는 &lt;b&gt;markCraeted()&lt;/b&gt; 를 호출합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;target과 handler에 어떤 값이 들어오는지 궁금해서 디버깅 해봤습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;AuditingEntityListener있는 상황&lt;/b&gt;&lt;/h2&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@EnableJpaAuditing
@DataJpaTest
class CafePolicyRepositoryTest {

    @Autowired
    private CafePolicyRepository cafePolicyRepository;

    @Autowired
    private CafeRepository cafeRepository;

    @Autowired
    private OwnerRepository ownerRepository;

    @Test
    void auditingTest() {
        ownerRepository.save(new Owner(&quot;name&quot;, &quot;id&quot;, &quot;pw&quot;, &quot;phone&quot;));
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-08-06 오후 8.15.58.png&quot; data-origin-width=&quot;564&quot; data-origin-height=&quot;145&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/otnD2/btsp8z4uHlg/xzHfCEKrJbalB6xs8KSdjK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/otnD2/btsp8z4uHlg/xzHfCEKrJbalB6xs8KSdjK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/otnD2/btsp8z4uHlg/xzHfCEKrJbalB6xs8KSdjK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FotnD2%2Fbtsp8z4uHlg%2FxzHfCEKrJbalB6xs8KSdjK%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;564&quot; height=&quot;145&quot; data-filename=&quot;스크린샷 2023-08-06 오후 8.15.58.png&quot; data-origin-width=&quot;564&quot; data-origin-height=&quot;145&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;target에는 저장하려는 엔티티, handler에는 targetBeanName이 jpaAuditingHandler&lt;/b&gt; 로 들어오는 것을 확인할 수 있었습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;AuditingEntityListener없는 상황&lt;/b&gt;&lt;/h2&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;// @EnableJpaAuditing
@DataJpaTest
class CafePolicyRepositoryTest {

    @Autowired
    private CafePolicyRepository cafePolicyRepository;

    @Autowired
    private CafeRepository cafeRepository;

    @Autowired
    private OwnerRepository ownerRepository;
        @Test
    void auditingTest() {
        ownerRepository.save(new Owner(&quot;name&quot;, &quot;id&quot;, &quot;pw&quot;, &quot;phone&quot;));
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-08-06 오후 8.14.47.png&quot; data-origin-width=&quot;307&quot; data-origin-height=&quot;90&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cC8ea6/btsp7J0D5Mc/cbmqd62srzKQ8es8eM5pxK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cC8ea6/btsp7J0D5Mc/cbmqd62srzKQ8es8eM5pxK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cC8ea6/btsp7J0D5Mc/cbmqd62srzKQ8es8eM5pxK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcC8ea6%2Fbtsp7J0D5Mc%2Fcbmqd62srzKQ8es8eM5pxK%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;307&quot; height=&quot;90&quot; data-filename=&quot;스크린샷 2023-08-06 오후 8.14.47.png&quot; data-origin-width=&quot;307&quot; data-origin-height=&quot;90&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;target에는 마찬가지로 저장하려는 엔티티, handler에는 null&lt;/b&gt; 이 들어오면서 날짜가 생성되지 않고 넘어가는 것을 확인할 수 있었습니다.&lt;/p&gt;</description>
      <category>JPA</category>
      <category>Auditing</category>
      <category>AuditingEventListener</category>
      <category>JPA</category>
      <author>0h0</author>
      <guid isPermaLink="true">https://00h0.tistory.com/94</guid>
      <comments>https://00h0.tistory.com/94#entry94comment</comments>
      <pubDate>Sun, 6 Aug 2023 20:19:45 +0900</pubDate>
    </item>
    <item>
      <title>[JAVA] Jackson사용 시 primitive boolean주의점</title>
      <link>https://00h0.tistory.com/93</link>
      <description>&lt;h3&gt;&lt;strong&gt;들어가면서,&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;이번 프로젝트 중 만든 DTO에 &lt;code&gt;isRegistered&lt;/code&gt;라는 boolean타입 필드가 존재했다. 나는 해당 필드에 null이 들어갈 일이 없다고 생각해 primitive type인 boolean을 사용하면서 발생한 문제와 해결 방법에 대해 쓸 예정이다.&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;문제점&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;jackson라이브러리에서 primitive type인 boolean을 쓰고 필드명에 &lt;code&gt;isXXX&lt;/code&gt;를 사용하면 is가 자동으로 삭제된다. 그래서 만약 &lt;code&gt;isSuccess&lt;/code&gt;라는 필드가 boolean으로 선언되어 있다면 실제 반환되는 json에는 &lt;code&gt;success&lt;/code&gt;가 들어가게된다.&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;해결방법&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;현재 내가 알고 있는 해결방법은 3가지가 있다.&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;@JsonProperty(value = &amp;quot;isXXX&amp;quot;)&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;@JsonProperty를 사용하면 반환되는 json의 key이름을 내가 지정할 수 있다. 그러나 이 방법은 &lt;code&gt;isXXX&lt;/code&gt;와 &lt;code&gt;XXX&lt;/code&gt;가 동시에 반환된다는 문제가 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class jacksonBooleanTest {

    @Test
    void test() throws JsonProcessingException {
        String s = new ObjectMapper().writeValueAsString(new TestDto(true));
        System.out.println(s);
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;pre&gt;&lt;code&gt;@Getter
@NoArgsConstructor
@AllArgsConstructor
public class TestDto {

    @JsonProperty(value = &amp;quot;isSuccess&amp;quot;)
    private boolean isSuccess;
}&lt;/code&gt;&lt;/pre&gt;&lt;pre&gt;&lt;code&gt;{&amp;quot;success&amp;quot;:false,&amp;quot;isSuccess&amp;quot;:false}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이처럼 success, isSuccess가 모두 반환되는 것을 볼 수 있다.&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;Wrapper클래스 사용하기&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;boolean을 primitive가 아닌 Wrapper클래스인 Boolean을 사용하면 된다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Getter
@NoArgsConstructor
@AllArgsConstructor
public class TestDto {

    private Boolean isSuccess;
}&lt;/code&gt;&lt;/pre&gt;&lt;pre&gt;&lt;code&gt;{&amp;quot;isSuccess&amp;quot;:true}&lt;/code&gt;&lt;/pre&gt;&lt;h4&gt;&lt;strong&gt;getter메서드 네이밍 변경&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;보통 boolean의 getter는 getXXX가 아닌 isXXX로 생성된다. 이때, 해당 getter의 네이밍을 &lt;code&gt;getIsXXX&lt;/code&gt;로 설정하면 우리가 원하는 대로&lt;code&gt;isXXX&lt;/code&gt;로 반환된다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Getter
@NoArgsConstructor
@AllArgsConstructor
public class TestDto {


    private boolean isSuccess;

    public boolean getIsSuccess() {
        return isSuccess;
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;&lt;strong&gt;결론&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;primitive type인 boolean을 쓰고 싶으면 getter의 네이밍을 &lt;code&gt;getIsXXX&lt;/code&gt;로 하고, 그게 아니라면 Wrapper클래스인 Boolean을 사용해서 API명세에 맞는 DTO를 만드는게 좋아보인다.&lt;/p&gt;</description>
      <category>Language/JAVA</category>
      <category>Jackson library</category>
      <author>0h0</author>
      <guid isPermaLink="true">https://00h0.tistory.com/93</guid>
      <comments>https://00h0.tistory.com/93#entry93comment</comments>
      <pubDate>Sun, 23 Jul 2023 13:46:39 +0900</pubDate>
    </item>
    <item>
      <title>[JPA] 다양한 기본 키 자동 생성 전략</title>
      <link>https://00h0.tistory.com/92</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;들어가면서,&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관계형 데이터베이스를 사용하면서 각각의 데이터를 식별하는 기본키는 중요한 개념이다. 데이터베이스마다 기본 키를 생성해주는 방식은 다양하다. 그래서 JPA에서도 다양한 방식에 대응하기 위해 4가지 정도의 기본 키 생성전략이 존재하는데 각각의 특징에 대해서 정리하려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;AUTO&lt;/b&gt;&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 전략의 경우 id로 사용하려는 필드의 타입, 사용하는 데이터베이스에 따라 전략을 &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;size16&quot;&gt;우선, id로 사용하려는 필드 타입의 경우 2가지가 있다. &lt;b&gt;UUID, Numeric(Integer, Long)&lt;/b&gt;이 있다. &lt;b&gt;UUID를 사용&lt;/b&gt;하는 경우 JPA는 &lt;b&gt;UUIDGenerator&lt;/b&gt;를 이용해 기본 키를 생성해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고, &lt;b&gt;Numeric&lt;/b&gt;의 경우 &lt;b&gt;SequenceStyleGenerator&lt;/b&gt;에 의해 기본 키가 생성된다.&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;SequenceStyleGenerator&lt;/b&gt;는 구현되어 있는 클래스다. 만약 사용하고 있는 데이터 베이스가 &lt;b&gt;시퀀스를 지원&lt;/b&gt;하면 &lt;b&gt;시퀀스 방식으로 기본 키를 생성&lt;/b&gt;해주고, &lt;b&gt;시퀀스를 지원하지 않으면 테이블 방식으로 기본 키를 생성&lt;/b&gt;해준다. (시퀀스, 테이블 방식은 아래에서 알아볼 예정이다.)&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; UUID, 시퀀스 지원 유무에 따라 어떤 쿼리가 나가는지 알아보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AUTO는 기본 값이기 때문에 @GeneratedValue만 사용하면 AUTO전략을 이용한다.&lt;/p&gt;
&lt;pre id=&quot;code_1688289001368&quot; class=&quot;less&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
public class Person {

    @Id
    @GeneratedValue
    private UUID id;

    public UUID getId() {
        return id;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;UUID를 사용하는 경우&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1688288835884&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Hibernate: 
    
    create table person (
       id binary(255) not null,
        primary key (id)
    ) engine=InnoDB&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;id의 타입이 binary인 것을 확인할 수 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Numeric + 시퀀스 지원하지 않는 DB(MySQL)&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1688288958030&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Hibernate: 
    
    create table hibernate_sequence (
       next_val bigint
    ) engine=InnoDB
Hibernate: 
    
    insert into hibernate_sequence values ( 1 )
Hibernate: 
    
    create table person (
       id bigint not null,
        primary key (id)
    ) engine=InnoDB&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시퀀스를 지원하지 않기 때문에 테이블 방식이 선택되면서 &lt;b&gt;hibernate_sequence테이블이 생성&lt;/b&gt;되는 것을 확인할 수 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Numeric + 시퀀스 지원 DB(h2)&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1688289216371&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Hibernate: 
    
    create table person (
       id bigint not null,
        primary key (id)
    )&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;별도의 시퀀스 테이블이 생성되지 않는 것을 확인할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;IDENTITY&lt;/b&gt;&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 키 생성을 데이터베이스에 맡기는 전략이다. 예를 들어, &lt;b&gt;MySQL의 auto_increment&lt;/b&gt;가 있다. 주로, MySQL, PostgreSQL등에서 사용한다.&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;이 전략은 기본 키 생성을 데이터베이스에 맡기기 때문에, persist()시점에 실제 insert쿼리가 발생하게 된다. 그래서 쓰기지연이 동작하지 않는다는 점이 있다.&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;사용방법&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1688289472152&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
public class Person {

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

    public Long getId() {
        return id;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1688289530659&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Hibernate: 
    
    create table person (
       id bigint not null auto_increment,
        primary key (id)
    ) engine=InnoDB&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;@GeneratedValue의 strategy옵션을 IDENTITY로 명시하면 된다. 위 코드를 실행시키면 아래와 같은 쿼리가 발생한다.&lt;/li&gt;
&lt;li&gt;MySQL을 사용한 예제인데, id컬럼을 보면 auto_increment가 붙은 것을 볼 수 있다.&lt;/li&gt;
&lt;/ul&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;size16&quot;&gt;persist()시점에 기본 키가 생성되는지 코드로 확인해보고 싶어서 확인해봤다.&lt;/p&gt;
&lt;pre id=&quot;code_1688291624088&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
public void test() {

    Person person = new Person();
    System.out.println(&quot;persist -----&quot;);
    em.persist(person);
    System.out.println(&quot;----&quot;);

    System.out.println(&quot;flush-----&quot;);
    em.flush();
    System.out.println(&quot;------&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1688291608606&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;persist -----
Hibernate: 
    insert 
    into
        person
        
    values
        ( )
----
flush-----&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;실제로 flush()가 아닌 persist시점에 insert쿼리가 나가는 것을 확인할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;SEQUENCE&lt;/b&gt;&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터베이스 시퀀스는 식별자 값들을 순서대로 생성하는 데이터베이스 오브젝트인데, SEQUENCE전략은 해당 시퀀스를 사용해 기본키를 생성하는 전략이다. 해당 전략은 시퀀스를 지원하는 DB인 h2, 오라클 등에서 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;AUTO와의 공통점&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AUTO전략의 Numeric타입의 id를 사용할 때와 마찬가지로 SequenceStyleGenerator를 이용해 기본 키를 생성해준다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;AUTO와의 차이점&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AUTO와의 차이점은 UUID를 지원해주지 않는다는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SEQUENCE전략을 사용하면서 기본 키 타입을 UUID로 하면 엔티티 저장 시 IdentifierGenerationException예외가 발생한다.&amp;nbsp;&lt;/p&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;size16&quot;&gt;IDENTITY와 마찬가지로 @GeneratedValue의 옵션을 SEQUENCE로 설정하면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1688289953905&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
public class Person {

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private Long id;
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1688289977343&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Hibernate: 
    
    create table person (
       id bigint not null,
        primary key (id)
    )&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;b&gt;주의점&lt;/b&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;SEQUENCE전략을 이대로 사용한다면, &lt;b&gt;하나의 시퀀스를 다른 테이블이 공유&lt;/b&gt;하게 된다. 만약 Person을 저장하고, Item이라는 다른 엔티티를 저장할 경우 각각의 id값이 1,1이 아닌 1,2가 된다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;테이블마다 시퀀스를 다르게 사용하고 싶다면 @SequenceGenerator를 이용하면 된다. 구체적인 사용법은 다루지 않겠다.&lt;/p&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;size16&quot;&gt;그렇다면, 시퀀스를 지원하지 않는 DB에서 SEQUENCE전략을 사용하면 예외가 발생하는지 궁금해서 직접 해본 결과 예외 대신 hibernate_sequence라는 테이블이 생성된다. 이후 select쿼리를 통해 기본 키 값을 조회한 뒤, insert쿼리가 나가는 것을 확인했다.&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;a title=&quot;공식문서&quot; href=&quot;https://docs.jboss.org/hibernate/orm/6.2/userguide/html_single/Hibernate_User_Guide.html#identifiers-generators-sequence&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;공식문서&lt;/a&gt;에서 SEQUENCE전략은 위에서 말했던 SequenceStyleGenerator를 사용한다. 만약, 시퀀스를 이용하지 않는 DB를 사용하는데 SEQUENCE전략을 사용하면 내부적으로 시퀀스 역할을 하는 테이블을 만들어서 사용하기 때문에 예외가 발생하지 않는다. 이로 인해, hibernate는 DB간 이식성을 높였다고 설명한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;2.7.9. Using sequences&lt;br /&gt;For implementing database sequence-based identifier value generation Hibernate makes use of its&amp;nbsp;org.hibernate.id.enhanced.SequenceStyleGenerator&amp;nbsp;id generator. It is important to note that&amp;nbsp;SequenceStyleGenerator&amp;nbsp;is capable of working against databases that do not support sequences by transparently switching to a table as the underlying backing, which gives Hibernate a huge degree of portability across databases while still maintaining consistent id generation behavior (versus say choosing between SEQUENCE and IDENTITY).&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 시퀀스를 지원하지 않는 MySQL에서 SEQUENCE를 사용했을 때의 쿼리다.&lt;/p&gt;
&lt;pre id=&quot;code_1688290475346&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Hibernate: 
    
    create table hibernate_sequence (
       next_val bigint
    ) engine=InnoDB
Hibernate: 
    
    insert into hibernate_sequence values ( 1 )
Hibernate: 
    
    create table person (
       id bigint not null,
        primary key (id)
    ) engine=InnoDB
    
Hibernate: // insert전에 select로 기본 키 생성
select
    next_val as id_val 
from
    hibernate_sequence for update
            
Hibernate: 
    update
        hibernate_sequence 
    set
        next_val= ? 
    where
        next_val=?

Hibernate: 
    insert 
    into
        person
        (id) 
    values
        (?)&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;쿼리를 보면 hibernate_sequence라는 시퀀스 역할을 테이블이 생성된다.&lt;/li&gt;
&lt;li&gt;이후, insert하기 전에 해당 테이블에 select쿼리를 통해 기본 키를 생성한 뒤 insert하는 것을 확인할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;TABLE&lt;/b&gt;&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 전략은 키 생성 테이블을 만들고 이를 통해 데이터베이스 시퀀스를 흉내내는 전략이다. 이 방법도 마찬가지로 @GeneratedValue의 strategy옵션을 TABLE로 설정하면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1688290932784&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Hibernate: 
    
    create table hibernate_sequences (
       sequence_name varchar(255) not null,
        next_val bigint,
        primary key (sequence_name)
    )
    
Hibernate: 
    
    insert into hibernate_sequences(sequence_name, next_val) values ('default',0)
    
Hibernate: 
    
    create table person (
       id bigint not null,
        primary key (id)
    )&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 전략도 마찬가지로 @TableGenerator를 이용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;결론(내 생각)&lt;/b&gt;&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 AUTO로 하면 장땡이라고 생각했다. 그러나, 공부를 하다 보니 무조건 AUTO를 사용한다면 시퀀스를 지원하지 않는 DB인 MySQL의 경우 auto-increment를 이용할 수 없다. 그래서 사용하려는 DB의 시퀀스 지원 유무에 따라 SEQUENCE, IDENTITY를 선택해서 사용하는 것이 좋아보인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜냐하면, MySQL을 이용하는데 AUTO를 사용할 경우 하나의 시퀀스 테이블을 공유하면서 2개의 다른 엔티티를 저장했을 때 각각 1, 1을 id로 갖는 것이 아닌 1, 2를 가지기 때문이다.&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;만약, 기본 키 타입을 UUID로 하고 싶으면 AUTO를 사용하면 될 것 같다. AUTO를 제외한 전략에서 UUID를 사용할 수 있는지는 더 찾아봐야겠다.&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;그리고, IDENTITY의 경우 기본 키를 생성하기 위해 데이터베이스에 저장해야 되는 점으로 인해 쓰기지연이 동작하지 않는다. 이걸 해결하기 위한 방법은 조금 더 공부를 해봐야겠다.&lt;/p&gt;</description>
      <category>JPA</category>
      <category>hibernate</category>
      <category>PK</category>
      <author>0h0</author>
      <guid isPermaLink="true">https://00h0.tistory.com/92</guid>
      <comments>https://00h0.tistory.com/92#entry92comment</comments>
      <pubDate>Sun, 2 Jul 2023 19:01:42 +0900</pubDate>
    </item>
    <item>
      <title>도메인 객체의 ID부여</title>
      <link>https://00h0.tistory.com/91</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;들어가기 앞서,&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우테코 레벨2를 진행하면서 기존과는 다르게 도메인 객체를 고유하게 식별해야 되는 상황이 생겼고, 이를 해결하기 위해 구분되어야 하는 객체에 id필드가 생기게 됐습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이로 인해, 도메인 객체가 데이터베이스에 종속적인 존재가 된다는 느낌을 받고, 주변 크루들과 이야기 하면서 현재 저의 주관적인 생각을 정리한 글입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 의견이나 잘못된 내용이 있으면 언제든 피드백 해주시면 감사하겠습니다 ^.^&lt;/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;size16&quot;&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;size16&quot;&gt;그리고, 정말 DB에 종속적이지 않은게 꼭 좋은거라고 생각하지 않습니다. 요즘 느낀 것은 다양한 선택지 중 &lt;b&gt;트레이드 오프를 생각해 근거 있는 선택&lt;/b&gt;이라면 괜찮다고 생각합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB에 종속적이지 않겠다고, 식별자를 두지 않고 객체를 식별하기 위해 복잡한 로직이 존재한다면 식별자를 둠으로써 복잡성을 줄이는 선택을 할 수 있다고 생각합니다.&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;size16&quot;&gt;필드 간 동등성 비교를 통해 유일한 인스턴스를 식별할 수도 있지 않냐는 의견도 있었다. 그러나 이 경우 unique한 필드가 있는 경우만 유효하다고 생각된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약, 비즈니스 규칙 중 &quot;Member의 email은 중복되어선 안된다.&quot;같은 규칙이 존재한다면 email필드의 동등성 비교로 다양한 Member인스턴스 중 email을 통해 Member를 구분할 수 있다.&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;&lt;b&gt;인조키(id)를 통한 객체 구분&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 예제와 달리 자연키가 아닌 인조키를 통해 구분한다면 email대신 아이디가 중복되어선 안된다는 비즈니스 규칙이 수정되더라도 Member를 구분하는 식별자에는 영향이 없다. 여기서 인조키란 말 그대로 인의적으로 생성한 키(식별자)다. 인조키란 흔히, DB의 Autoincrement로 설정하는 값 처럼 객체에 필요한 필드가 아닌 객체의 식별을 위해 생성된 키다.&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;이제, id를 이용해 Member를 식별하고, email 대신 닉네임이 중복되어선 안된다고 요구사항이 바뀌었다고 가정해보면, Member를 식별하는데 아무런 문제가 되지 않는다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;어떤 객체에 id를 부여하나&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 인스턴스 중 유일하게 하나의 인스턴스를 식별할 필요가 있는 객체에 id부여가 필요하다고 생각한다. 일반적으로, Money라는 객체가 있다고 가정해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 해당 Money객체의 가치(가격)가 중요하지 Money의 일렬번호가 궁금하진 않고, 실제 비즈니스 요구사항 역시 Money의 가격만 중요하다면 해당 Money에는 굳이 식별자를 부여하지 않아도 된다고 생각한다.&lt;/p&gt;</description>
      <category>우아한테크코스5기</category>
      <category>객체지향</category>
      <author>0h0</author>
      <guid isPermaLink="true">https://00h0.tistory.com/91</guid>
      <comments>https://00h0.tistory.com/91#entry91comment</comments>
      <pubDate>Sun, 4 Jun 2023 23:09:53 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] test context caching</title>
      <link>https://00h0.tistory.com/90</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;잘못된 내용이 있으면 언제든지 피드백 해주시면 감사하겠습니다 ^.^&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Context Caching?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring에서 테스트를 작성할 때 bean으로 등록한 것들을 테스트하기 위해선 Application context가 필요하다. 하지만 테스트의 독립성을 유지하기 위해 테스트마다 모든 bean으로 구성된 Application context를 만든다면 속도가 매우 느릴것입니다. 그래서 spring의 testContext는 context caching을 해줍니다. 그렇다면 어떤 기준으로 context caching하는지 알아보겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;언제 새로운 context를 만들지?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 textContext는 재사용할 수 있다면 이미 만들어진 context를 caching해 재사용합니다. 그렇다면 언제 재사용을 할까요? 2가지 기준이 있습니다.&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;context가 오염되지 않은 경우&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 2가지 경우를 하나씩 살펴보겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;동일한 구성환경&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식문서에 따르면 Application context를 만드는데 사용된 매개변수들을 조합해 고유하게 식별합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;locations&amp;nbsp;(from&amp;nbsp;&lt;b&gt;@ContextConfiguration&lt;/b&gt;)&lt;/li&gt;
&lt;li&gt;classes&amp;nbsp;(from&amp;nbsp;&lt;b&gt;@ContextConfiguration&lt;/b&gt;)&lt;/li&gt;
&lt;li&gt;contextInitializerClasses&amp;nbsp;(from&amp;nbsp;&lt;b&gt;@ContextConfiguration&lt;/b&gt;)&lt;/li&gt;
&lt;li&gt;contextCustomizers&amp;nbsp;(from&amp;nbsp;ContextCustomizerFactory) &amp;ndash; this includes&amp;nbsp;&lt;b&gt;@DynamicPropertySource&lt;/b&gt;&amp;nbsp;methods as well as various features from Spring Boot&amp;rsquo;s testing support such as&amp;nbsp;&lt;b&gt;@MockBean&lt;/b&gt;&amp;nbsp;and&amp;nbsp;&lt;b&gt;@SpyBean&lt;/b&gt;.&lt;/li&gt;
&lt;li&gt;contextLoader&amp;nbsp;(from&amp;nbsp;&lt;b&gt;@ContextConfiguration&lt;/b&gt;)&lt;/li&gt;
&lt;li&gt;parent&amp;nbsp;(from&amp;nbsp;&lt;b&gt;@ContextHierarchy&lt;/b&gt;)&lt;/li&gt;
&lt;li&gt;activeProfiles&amp;nbsp;(from&amp;nbsp;&lt;b&gt;@ActiveProfiles&lt;/b&gt;)&lt;/li&gt;
&lt;li&gt;propertySourceLocations&amp;nbsp;(from&amp;nbsp;&lt;b&gt;@TestPropertySource&lt;/b&gt;)&lt;/li&gt;
&lt;li&gt;propertySourceProperties&amp;nbsp;(from&amp;nbsp;&lt;b&gt;@TestPropertySource&lt;/b&gt;)&lt;/li&gt;
&lt;li&gt;resourceBasePath&amp;nbsp;(from&amp;nbsp;&lt;b&gt;@WebAppConfiguration&lt;/b&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 직관적인 예시로 &lt;b&gt;@ContextConfiguration&lt;/b&gt;어노테이션을 들 수 있습니다. 현재 2개의 Configuration파일이 있습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Configuration
public class AConfig {

    @Bean
    public A a() {
        return new A();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Configuration
public class BConfig {

    @Bean
    public B b() {
        return new B();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 상황에서 @ContextConfiguration을 이용해 2개의 테스트가 이용하는 context설정을 다르게 주고 출력문을 통해 context의 고유 키 값을 확인해보겠습니다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@SpringBootTest
@ContextConfiguration(classes = AConfig.class)
public class ATest {
    
    @Autowired
    ApplicationContext applicationContext;

    @Test
    void a() {
        System.out.println(applicationContext);
    }

    @Test
    void b() {
        System.out.println(applicationContext);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@SpringBootTest
@ContextConfiguration(classes = BConfig.class)
public class BTest {

    @Autowired
    ApplicationContext applicationContext;

    @Test
    void a() {
        System.out.println(applicationContext);
    }

    @Test
    void b() {
        System.out.println(applicationContext);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2개의 코드는 한 가지만 빼고 모두 똑같습니다. 바로, @ContextConfiguration의 class종류입니다. 위 테스트를 돌렸을 때 나오는 출력문을 확인해보겠습니다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;// ATest
org.springframework.web.context.support.GenericWebApplicationContext@4102b1b1

// BTest
org.springframework.web.context.support.GenericWebApplicationContext@6d67f5eb
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;고유 식별번호가 다른 것을 확인할 수 있습니다. 그렇다면, &lt;b&gt;@ContextConfiguration을 제거&lt;/b&gt;해 동일환 환경에서 context가 구성되도록 하여 context의 고유 번호를 확인해보겠습니다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;@SpringBootTest
public class ATest {
    
    @Autowired
    ApplicationContext applicationContext;

    @Test
    void a() {
        System.out.println(applicationContext);
    }

    @Test
    void b() {
        System.out.println(applicationContext);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;@SpringBootTest
public class BTest {

    @Autowired
    ApplicationContext applicationContext;

    @Test
    void a() {
        System.out.println(applicationContext);
    }

    @Test
    void b() {
        System.out.println(applicationContext);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;// ATest
org.springframework.web.context.support.GenericWebApplicationContext@50b1f030

// BTest
org.springframework.web.context.support.GenericWebApplicationContext@50b1f030
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;context구성환경이 동일하기 때문에 하나의 context를 재사용 하는 것을 볼 수 있습니다. &lt;b&gt;@JdbcTest&lt;/b&gt;와 &lt;b&gt;@SpringBootTest&lt;/b&gt;등 등록되는 bean의 종류가 다를 때도 context를 caching하지 않고 새로운 context를 만들게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;@JdbcTest, @SpringBootTest&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가적으로 @SpringBootTest의 webEnvironment옵션에 따른 context caching 여부가 궁금해서 확인해본 결과 &lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;@SpringBootTest의 webEnvironment옵션에 따라서 모두 다른 context가 생성됩니다. 물론 webEnvironment가 같아도 구성환경 등이 다르면 다른 context가 생성될 것 입니다.&lt;/span&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;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;@SpringBootTest와 @JdbcTest구현 클래스를 보면 다양한 어노테이션이 있는데 그 중 @BootStrapWith어노테이션을 보면 parameter가 각각 SpringBootTestContextBootsrapper, JdbcTestContextBootstrapper로 다른 것을 볼 수 있습니다. &lt;b&gt;@BootStrapper&lt;/b&gt;란 &lt;a href=&quot;https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/test/context/BootstrapWith.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;공식문서&lt;/a&gt;에 따르면 아래와 같이 나옵니다.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;@BootstrapWith&lt;br /&gt;&amp;nbsp;defines class-level metadata that is used to determine how to bootstrap the&amp;nbsp;&lt;br /&gt;Spring TestContext Framework.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &lt;b&gt;testContext를 구성하는 메타데이터 정보&lt;/b&gt;로 @JdbcTest와 @SpringBootTest는 context구성 메타데이터가 다르기 때문에 context caching을 통한 재사용이 발생하지 않습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;context오염&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 테스트를 할 때는 테스트별로 독립적이어야 합니다. 하지만 기존의 context가 오염된 상황에서 각 테스트가 독립적일 수 있을까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 오염이란 &lt;b&gt;context내부 빈의 정보가 수정, 추가, 삭제&lt;/b&gt; 등 context정보에 변경이 있는 것을 말합니다. 대표적으로 mocking을 예로 들 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 A라는 bean을 mocking하게 되면 context를 구성하던 A라는 bean이 mocking객체로 변경됩니다. 이 과정에서 기존 context의 A빈이 오염됐다고 표현할 수 있습니다. 이로 인해, testContext는 context를 재사용하지 않고 새로운 context를 만듭니다.&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;우선 A라는 객체를 빈으로 등록하겠습니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;@Component
public class A {
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;@SpringBootTest
public class ATest {

    @Autowired
    ApplicationContext applicationContext;

    @MockBean
    A a;

    @Test
    void a() {
        System.out.println(applicationContext);
    }

    @Test
    void b() {
        System.out.println(applicationContext);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;@SpringBootTest
public class BTest {

    @Autowired
    ApplicationContext applicationContext;

    @Test
    void a() {
        System.out.println(applicationContext);
    }

    @Test
    void b() {
        System.out.println(applicationContext);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드를 수행한 결과를 확인해보겠습니다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;// ATest
org.springframework.web.context.support.GenericWebApplicationContext@7bdb4f17

//BTest
org.springframework.web.context.support.GenericWebApplicationContext@2c0f7678
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ATest에서 context를 만들었지만, BTest에서 mocking으로 인해 contextrk&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 A도 똑같이 mocking하면 어떻게 될까요?&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;public class ATest {

    @Autowired
    ApplicationContext applicationContext;

    @MockBean
    A a;

    @Test
    void a() {
        System.out.println(applicationContext);
    }

    @Test
    void b() {
        System.out.println(applicationContext);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;@SpringBootTest
public class BTest {

    @Autowired
    ApplicationContext applicationContext;

	  @MockBean
    A a;

    @Test
    void a() {
        System.out.println(applicationContext);
    }

    @Test
    void b() {
        System.out.println(applicationContext);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;// ATest
org.springframework.web.context.support.GenericWebApplicationContext@5d10455d

// BTest
org.springframework.web.context.support.GenericWebApplicationContext@5d10455d
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BTest는 ATest의 context를 오염시키지 않았고, 구성환경도 동일하기 때문에 context를 재사용한 것을 볼 수 있습니다.&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;또한, @Import역시 context에 빈이 추가된 것이기 때문에 context caching으로 재사용하지 않습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;DirtiestContext&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@DirtiesContext의 구현 클래스를 들어가보면 주석에 아래와 같은 문구가 있습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Test annotation which indicates that the ApplicationContext associated with a test is dirty and should therefore be closed and removed from the context cache.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 Test에 해당 어노테이션이 붙어있으면 구성환경이 동일한 context가 있더라고 context를 재사용하지 않고 새로 만들어서 사용하게 됩니다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@SpringBootTest
@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_CLASS)
public class ATest {

    @Autowired
    ApplicationContext applicationContext;

    @Test
    void a() {
        System.out.println(applicationContext);
    }

    @Test
    void b() {
        System.out.println(applicationContext);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;@SpringBootTest
public class BTest {

    @Autowired
    ApplicationContext applicationContext;
    
    @Test
    void a() {
        System.out.println(applicationContext);
    }

    @Test
    void b() {
        System.out.println(applicationContext);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드를 보면, @DirtiesContext말고 차이가 없습니다. 그렇다면 젤 처음에 살펴본 예시처럼 모든 구성환경이 같기 때문에 동일한 context를 caching해 재사용할까요?&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;// ATest
org.springframework.web.context.support.GenericWebApplicationContext@2d83c5a5

// BTest
org.springframework.web.context.support.GenericWebApplicationContext@aee05f4
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로운 context를 생성해 사용하는 것을 확인할 수 있습니다. 이처럼 @DirtiestContext가 붙으면 무조건 새로운 context를 만들게 되는데요. 이것도 메서드 단위로 만들지, 클래스 단위로 만들지 다양하게 선택할 수 있습니다.&lt;/p&gt;</description>
      <category>Spring</category>
      <category>context</category>
      <category>Context Caching</category>
      <category>Spring</category>
      <category>Test</category>
      <author>0h0</author>
      <guid isPermaLink="true">https://00h0.tistory.com/90</guid>
      <comments>https://00h0.tistory.com/90#entry90comment</comments>
      <pubDate>Sun, 21 May 2023 03:06:16 +0900</pubDate>
    </item>
    <item>
      <title>DAO(Data Access Object)</title>
      <link>https://00h0.tistory.com/89</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;size16&quot;&gt;우테코 미션을 진행하면 dao에 대해 공부하면서  dao를 적용했을 때의 장점을 생각해봤습니다. 잘못된 내용에 대해 피드백 해주시면 감사하겠습니다 ㅎㅎ&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;DAO&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;baeldung의 dao관련 글을 보면 아래와 같은 문구로 시작한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;The Data Access Object (DAO) pattern is a structural pattern that allows us to&amp;nbsp;isolate the application/business layer from the persistence layer (usually a relational database but could be any other persistence mechanism) using an abstract API.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단하게 요약해보면, &amp;ldquo;추상 API를 사용하여 business계층과 persistence계층을 분리할 수 있는 구조적패턴&amp;rdquo;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 business계층과 persistence&lt;b&gt;계층의 분리&lt;/b&gt;, &lt;b&gt;추상 API&lt;/b&gt;라고 생각한다. 이 2가지에 대해서 간단하게 내 생각을 정리해보려고 한다.&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;size16&quot;&gt;dao를 통해 영속성 관련 변경은 persistence계층에만 영향을 끼치게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 &lt;b&gt;business계층의 관심사는 business로직 수행 + 데이터 영속화 요청&lt;/b&gt;이라고 생각한다. 그러나 데이터를 어떤 방식으로 영속화하는지 까지 알 필요는 없다고 생각한다. 단지 저장해줘, 조회해줘, 수정해줘, 삭제해줘의 요청만 해야지 구체적으로 sql을 이용해 저장하는 것은 너무 많은 책임을 가진다고 생각한다.&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;만약, business계층이 JdbcTemplate과 sql문을 정의해 직접 데이터를 저장하고 있다고 가정해보자. 테이블의 구조가 변경되면 이 영향은 persistence계층이 아닌 business계층에 영향을 끼친다. 그리고 business요구사항의 변경도 business계층에 영향을 끼친다.&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;그러나, dao를 이용해 계층분리가 이루어진 경우 테이블 구조의 변경으로 영향을 받는 것은 dao가 존재하는 persistence계층 뿐이다. 왜냐하면 구체적인 영속화 작업은 dao에서 이루어지기 때문에, 테이블 구조 변경으로 인한 query수정은 dao에서만 수행하면 된다. &lt;b&gt;dao를 통해 business계층은 dao에게 &amp;lsquo;영속화해줘&amp;rsquo;라고 요청할 뿐 구체적인 방식은 모르게 된다.&lt;/b&gt; 이처럼 dao를 통해 고수준인 business계층과 저수준인 persistence계층을 분리해 영속성 관련 변경의 영향은 persistence계층에만 영향을 끼치게 된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;추상 API&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 baeldung 글을 보면서 추상 API라는 말이 마음에 걸렸다. 왜 추상화한 DAO를 통해 business계층과 persistence계층을 분리할까? 그냥 dao로만 분리해도 충분히 관심사의 분리가 이루어진 것 같기 때문이다.&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;만약, businiess계층이 MySQL을 이용한 구체 dao클래스에 의존하고 있는 상황에서 테스트를 위한 InMemoryDao가 필요하면 어떻게 될까? InMemoryDao를 추가 구현하고 business계층의 dao의존 코드에 수정이 필요하다. 또한, &lt;b&gt;business계층은 persistence계층에게 &amp;ldquo;MySQL을 이용해 영속화 해줘&amp;rdquo;로 요청&lt;/b&gt;하게 된다. business계층은 persistence가 어떤 방식으로 데이터를 영속화하는지 알 필요가 있을까?&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;b&gt;business계층은 데이터가 영속화되는 것만 궁금&lt;/b&gt;하지 MySQL벤더 사를 이용하든, PostgreSQL벤더 사를 이용하든, InMemory방식으로 영속화하든 관심이 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나, 추상화된 DAO를 사용하지 않으면 business계층은 MySQL API를 사용하는 구체 클래스인 MySQLDao같은 것에 의존하게 될 것이다. 이것은 business계층이 persistence계층에 대해 과하게 알게 된다. 즉, 강하게 결합하게 된다. 이와 같은 구조는 확장성이 떨어진다는 단점이 있다.&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-filename=&quot;무제a.png&quot; data-origin-width=&quot;421&quot; data-origin-height=&quot;91&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bxvOmg/btsdpxD7dY6/KL9kcOwaglvErfwAnV0oV1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bxvOmg/btsdpxD7dY6/KL9kcOwaglvErfwAnV0oV1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bxvOmg/btsdpxD7dY6/KL9kcOwaglvErfwAnV0oV1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbxvOmg%2FbtsdpxD7dY6%2FKL9kcOwaglvErfwAnV0oV1%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;421&quot; height=&quot;91&quot; data-filename=&quot;무제a.png&quot; data-origin-width=&quot;421&quot; data-origin-height=&quot;91&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;위 구조를 추상화를 통해 service가 Dao인터페이스에 의존하게 한 뒤, 이를 구현한 클래스가 존재하면 확장성 측면에서 이점을 가져갈 수 있고, service는 구체적인 영속화 방법에 대해 모르게 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;무제.png&quot; data-origin-width=&quot;506&quot; data-origin-height=&quot;271&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bcF44h/btsdeeTsmQ7/uitcHqMOMmkkmQWtpzyTFK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bcF44h/btsdeeTsmQ7/uitcHqMOMmkkmQWtpzyTFK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bcF44h/btsdeeTsmQ7/uitcHqMOMmkkmQWtpzyTFK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbcF44h%2FbtsdeeTsmQ7%2FuitcHqMOMmkkmQWtpzyTFK%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;506&quot; height=&quot;271&quot; data-filename=&quot;무제.png&quot; data-origin-width=&quot;506&quot; data-origin-height=&quot;271&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;size16&quot;&gt;&lt;b&gt;GameDao.java&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;public interface GameDao {
    public Long createGame(final GameSaveDto gameSaveDto);

    public List&amp;lt;GameWinnerDto&amp;gt; findAllGames();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;JdbcGameDao.java&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;public class JdbcGameDao implements GameDao{
		@Override
    public Long createGame(final GameSaveDto gameSaveDto) {
        // 저장 로직
    }
		@Overrid
    public List&amp;lt;GameWinnerDto&amp;gt; findAllGames() {
				// 조회 로직
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;GameService.java&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;public class GameFindService {
    private final GameDao gameDao;

    public GameFindService(GameDao gameDao) {
        this.gameDao = gameDao;
    }
}
&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;size16&quot;&gt;dao에 대해 고민해보면서 추상화된 API를 이용해 dao를 분리하면 위와 같은 장점이 있다는 생각이 들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 추상 API의 경우 아직 막 와닿지는 않는다. 과연 &amp;lsquo;데이터베이스 벤더 사를 변경하는 경우가 자주 있을까?&amp;rsquo;라는 생각이 들었고, 테스트를 위한 InMemoryDao역시 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;그러나 첫 번째 장점이라고 생각한 계층분리는 확실한 장점이라고 생각한다. dao를 통한 계층 분리로 business계층은 business로직에만 집중하고, persistence계층은 CRUD에만 집중해 개발할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;참고자료&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.baeldung.com/java-dao-pattern&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://www.baeldung.com/java-dao-pattern&lt;/a&gt;&lt;/p&gt;</description>
      <category>OOP</category>
      <category>dao</category>
      <author>0h0</author>
      <guid isPermaLink="true">https://00h0.tistory.com/89</guid>
      <comments>https://00h0.tistory.com/89#entry89comment</comments>
      <pubDate>Mon, 1 May 2023 19:39:25 +0900</pubDate>
    </item>
    <item>
      <title>ResponseEntity의 created(URI)는 뭘까? (feat.httpCode 201)</title>
      <link>https://00h0.tistory.com/88</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;개요&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레벨2 자동차 경주 미션을 진행하면서 201응답을 반환하는 상황이 있었다. 이 과정에서 ResponseEntity의 created는 왜 URI를 인자로 받는지 페어와 이야기 했었다. 당시에 추측만 하고 명확하게 답을 내지 못해서 따로 공부한 내용을 정리해보려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;ResponseEntity.created(URI)구현코드&lt;/b&gt;&lt;/h3&gt;
&lt;div style=&quot;background-color: #ffffff; color: #080808;&quot;&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;public static BodyBuilder created(URI location) {
   return status(HttpStatus.CREATED).location(location);
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ResponseEntity클래스의 created메서드는 위와 같이 구현되어 있다. &lt;b&gt;parameter에 URI&lt;/b&gt;가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도대체 저 URI가 뭐지? 당시 디투와 얘기 했을 때는 &lt;b&gt;redirect를 위한 것 같다&lt;/b&gt;고 결론 지었다. 그러나 확실한 건 아니어서 &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/201&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;mozila문서&lt;/a&gt;의 201코드에 대해 확인해봤다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;201 status code&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;http응답으로 상태코드 201이 왔다는 것은 &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;size16&quot;&gt;그렇다면 해당 &lt;b&gt;자원에 접근&lt;/b&gt;하기 위해선 무엇이 필요할까? &lt;b&gt;식별자&lt;/b&gt;다. 즉, 생성된 자원에 접근할 수 있도록 식별자가 포함된 URI를 반환하기 위해 created(URI)를 받는것이다.&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;생성된 자원에 접근할 수 있는 URI반환&lt;/li&gt;
&lt;li&gt;생성된 자원 반환&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;ResponseEntity클래스 내부 뜯어보기&lt;/b&gt;&lt;/h3&gt;
&lt;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;HeaderBuilder&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;build()는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;ResponseEntity내부에 정의&lt;/b&gt;되어 있다. ResponseEntity내부에&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;HeaderBuilder인터페이스&lt;/b&gt;가 정의되어 있고 이 인터페이스 안에&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;build()가 정의&lt;/b&gt;되어 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;전체코드를 가져오기엔 너무 많아서 build만 가져왔다.&lt;/p&gt;
&lt;pre id=&quot;code_1682152488790&quot; class=&quot;php&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/**
 * Defines a builder that adds headers to the response entity.
 * @since 4.1
 * @param &amp;lt;B&amp;gt; the builder subclass
 */
public interface HeadersBuilder&amp;lt;B extends HeadersBuilder&amp;lt;B&amp;gt;&amp;gt; {
    /**
     * Build the response entity with no body.
     * @return the response entity
     * @see BodyBuilder#body(Object)
     */
    &amp;lt;T&amp;gt; ResponseEntity&amp;lt;T&amp;gt; build();
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;build()의 주석을 보면 response body없이 ResponseEntity를 생성한다고 한다.&lt;/blockquote&gt;
&lt;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;BodyBuilder&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;위에 있는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;HeaderBuilder를 상속한 인터페이스&lt;/b&gt;다. 네이밍 그대로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;response의 body부분을 설정&lt;/b&gt;하는 기능을 담당한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;코드의 일부분만 가져왔다.&lt;/p&gt;
&lt;pre id=&quot;code_1682152488792&quot; class=&quot;dart&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/**
 * Defines a builder that adds a body to the response entity.
 * @since 4.1
 */
public interface BodyBuilder extends HeadersBuilder&amp;lt;BodyBuilder&amp;gt; {
    /**
     * Set the body of the response entity and returns it.
     * @param &amp;lt;T&amp;gt; the type of the body
     * @param body the body of the response entity
     * @return the built response entity
     */
    &amp;lt;T&amp;gt; ResponseEntity&amp;lt;T&amp;gt; body(@Nullable T body);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;DefaultBuilder&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; BodyBuilder를 구현한 static class&lt;/b&gt;로 ResponseEntity안에 위치한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;build()&lt;/b&gt;와&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;body(@Nullable T body)를 구현&lt;/b&gt;하고 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1682152488796&quot; class=&quot;java&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private static class DefaultBuilder implements BodyBuilder {

    private final Object statusCode;

    private final HttpHeaders headers = new HttpHeaders();

    public DefaultBuilder(Object statusCode) {
        this.statusCode = statusCode;
    }

    @Override
    public &amp;lt;T&amp;gt; ResponseEntity&amp;lt;T&amp;gt; build() {
        return body(null);
    }

    @Override
    public &amp;lt;T&amp;gt; ResponseEntity&amp;lt;T&amp;gt; body(@Nullable T body) {
        return new ResponseEntity&amp;lt;&amp;gt;(body, this.headers, this.statusCode);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이제 식별자를 반환하는 방법에 대해 알아보자.&lt;/b&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. 생성된 자원의 URI 반환하기&lt;/b&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;이 방법은 build()를 통해 식별자가 포함된 URI를 header의 location에 담아 반환한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 User를 새로 생성했다고 가정하고 생성된 User에 접근할 수 있는 URI를 반환한다고 해보자. 아래와 같은 코드를 작성할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1682140691594&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;return ResponseEntity.created(URI.create(&quot;/users/&quot; + id)).build();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드의 실행 결과를 포스트맨으로 확인해보면 &lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;reponse header의 Location에 생성한 URI가 삽입된 것을 확인할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-04-22 오후 2.26.00.png&quot; data-origin-width=&quot;1458&quot; data-origin-height=&quot;534&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dOqWpf/btsb1Qq4Qfz/paT3GntgXJO4xf2eMl9bgk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dOqWpf/btsb1Qq4Qfz/paT3GntgXJO4xf2eMl9bgk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dOqWpf/btsb1Qq4Qfz/paT3GntgXJO4xf2eMl9bgk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdOqWpf%2Fbtsb1Qq4Qfz%2FpaT3GntgXJO4xf2eMl9bgk%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;1458&quot; height=&quot;534&quot; data-filename=&quot;스크린샷 2023-04-22 오후 2.26.00.png&quot; data-origin-width=&quot;1458&quot; data-origin-height=&quot;534&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;ResponseEntity를 반환하는 코드를 다시 보면, URI.create()를 통해 URI를 생성 후 &lt;b&gt;build()&lt;/b&gt;메서드를 호출했다. 해당 메서드의 기능을 간략하게 설명하면 아래와 같다.&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;build메서드 기능 : 응답의 body가 없을 때 ResponseEntity를 생성해 반환해준다(빌더패턴)&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;build()의 ResponseEntity생성 과정&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;build()를 사용하는 아래 코드는 어떤 식으로 ResponseEntity를 만드는 과정을 따라가보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1682147035574&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;return ResponseEntity.created(URI.create(&quot;/users/&quot; + id)).build();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. DefaultBuilder의 build()호출&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선, DefaultBuilder의 build메서드를 호출한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;현재 build()를 호출한 것이기 때문에 status, URI는 만들어진 상태로 header의 Location도 설정되어 있다.&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-04-22 오후 3.55.57.png&quot; data-origin-width=&quot;1408&quot; data-origin-height=&quot;398&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sQ9fm/btsb1QEFjkE/rnnu0BK4QdYXVgWyYlkW31/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sQ9fm/btsb1QEFjkE/rnnu0BK4QdYXVgWyYlkW31/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sQ9fm/btsb1QEFjkE/rnnu0BK4QdYXVgWyYlkW31/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsQ9fm%2Fbtsb1QEFjkE%2Frnnu0BK4QdYXVgWyYlkW31%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;683&quot; height=&quot;193&quot; data-filename=&quot;스크린샷 2023-04-22 오후 3.55.57.png&quot; data-origin-width=&quot;1408&quot; data-origin-height=&quot;398&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-04-22 오후 4.00.24.png&quot; data-origin-width=&quot;794&quot; data-origin-height=&quot;268&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/4KNn7/btsbVbCyu9E/MYHwTP9ssP0UBd3NKxM8lk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/4KNn7/btsbVbCyu9E/MYHwTP9ssP0UBd3NKxM8lk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/4KNn7/btsbVbCyu9E/MYHwTP9ssP0UBd3NKxM8lk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F4KNn7%2FbtsbVbCyu9E%2FMYHwTP9ssP0UBd3NKxM8lk%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;598&quot; height=&quot;202&quot; data-filename=&quot;스크린샷 2023-04-22 오후 4.00.24.png&quot; data-origin-width=&quot;794&quot; data-origin-height=&quot;268&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. body값 설정 후 ResponseEntity생성 후 반환&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 build()에 구현되어 있는 body(null)을 호출한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;body값을 설정하고 ResponseEntity를 생성해 반환한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-04-22 오후 4.01.05.png&quot; data-origin-width=&quot;1600&quot; data-origin-height=&quot;398&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cb3QrM/btsbRzZae2s/DjWRBJPHRHq7MsCXlqP2OK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cb3QrM/btsbRzZae2s/DjWRBJPHRHq7MsCXlqP2OK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cb3QrM/btsbRzZae2s/DjWRBJPHRHq7MsCXlqP2OK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcb3QrM%2FbtsbRzZae2s%2FDjWRBJPHRHq7MsCXlqP2OK%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;715&quot; height=&quot;178&quot; data-filename=&quot;스크린샷 2023-04-22 오후 4.01.05.png&quot; data-origin-width=&quot;1600&quot; data-origin-height=&quot;398&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-04-22 오후 4.12.05.png&quot; data-origin-width=&quot;842&quot; data-origin-height=&quot;454&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/caB7UE/btsb1RDzHuF/s1zBMIO0T0z3NaqnWkVqc1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/caB7UE/btsb1RDzHuF/s1zBMIO0T0z3NaqnWkVqc1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/caB7UE/btsb1RDzHuF/s1zBMIO0T0z3NaqnWkVqc1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcaB7UE%2Fbtsb1RDzHuF%2Fs1zBMIO0T0z3NaqnWkVqc1%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;495&quot; height=&quot;267&quot; data-filename=&quot;스크린샷 2023-04-22 오후 4.12.05.png&quot; data-origin-width=&quot;842&quot; data-origin-height=&quot;454&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;ResponseEntity는 HttpEntity를 상속했기 때문에 뒤에 추가적인 작업이 있지만, 너무 많아지기 때문에 생략하겠습니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 반환된 Location값을 통해 새롭게 생성된 자원에 접근할 수 있다.&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;b&gt;주문 후 주문 내역 페이지로 이동&lt;/b&gt;하는데 활용할 수 있지 않을까 싶다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. 생성된 자원 반환&lt;/b&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;이 방법은 식별자를 response body에 포함하는 방식으로 식별자를 반환한다.&lt;/blockquote&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;div style=&quot;background-color: #ffffff; color: #080808;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;User createUser = userService.saveUser(user);
return ResponseEntity.status(HttpStatus.CREATED).body(createUser);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;reposnse body가 존재하기 때문에 &lt;b&gt;build()를 호출하지 않 는다.&lt;/b&gt; 대신 &lt;b&gt;body(@Nullable T body)&lt;/b&gt;를 통해 &lt;b&gt;response body에 새롭게 저장된 자원을 반환&lt;/b&gt;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고, &lt;b&gt;URI를 반환하지 않는다.&lt;/b&gt; 그래서,  &lt;b&gt;status code&lt;/b&gt;를 created(URI)가 아닌 &lt;b&gt;status(HttpStatus)를 통해 설정&lt;/b&gt;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;body()의 ResponseEntity생성 과정&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;body값을 설정한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;build()와 마찬가지로 &lt;b&gt;DefaultBuilder&lt;/b&gt;의 메서드를 호출한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;차이점은 build()가 아닌 &lt;b&gt;body()를 통해 body에 null이 아닌 실제 데이터를 집어 넣는다.&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;size16&quot;&gt;이후 과정은 build()와 동일하게 진행된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-04-22 오후 4.56.29.png&quot; data-origin-width=&quot;1680&quot; data-origin-height=&quot;222&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sfo8g/btsbRzrkIs6/epsHhhXPOQ18aiadTAoOCk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sfo8g/btsbRzrkIs6/epsHhhXPOQ18aiadTAoOCk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sfo8g/btsbRzrkIs6/epsHhhXPOQ18aiadTAoOCk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fsfo8g%2FbtsbRzrkIs6%2FepsHhhXPOQ18aiadTAoOCk%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;686&quot; height=&quot;91&quot; data-filename=&quot;스크린샷 2023-04-22 오후 4.56.29.png&quot; data-origin-width=&quot;1680&quot; data-origin-height=&quot;222&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-04-22 오후 4.56.57.png&quot; data-origin-width=&quot;796&quot; data-origin-height=&quot;408&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QDuKc/btsbV6gLYNK/kE2ZKKN0sKPqcqq1lrAAgk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QDuKc/btsbV6gLYNK/kE2ZKKN0sKPqcqq1lrAAgk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QDuKc/btsbV6gLYNK/kE2ZKKN0sKPqcqq1lrAAgk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQDuKc%2FbtsbV6gLYNK%2FkE2ZKKN0sKPqcqq1lrAAgk%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;485&quot; height=&quot;249&quot; data-filename=&quot;스크린샷 2023-04-22 오후 4.56.57.png&quot; data-origin-width=&quot;796&quot; data-origin-height=&quot;408&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;size18&quot;&gt;&lt;b&gt;response확인&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;header&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-04-22 오후 5.24.25.png&quot; data-origin-width=&quot;1504&quot; data-origin-height=&quot;476&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/btnsAZ/btsb5kZPbhk/a90ClwvaSqsJ2mRoSBrP9K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/btnsAZ/btsb5kZPbhk/a90ClwvaSqsJ2mRoSBrP9K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/btnsAZ/btsb5kZPbhk/a90ClwvaSqsJ2mRoSBrP9K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbtnsAZ%2Fbtsb5kZPbhk%2Fa90ClwvaSqsJ2mRoSBrP9K%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;1504&quot; height=&quot;476&quot; data-filename=&quot;스크린샷 2023-04-22 오후 5.24.25.png&quot; data-origin-width=&quot;1504&quot; data-origin-height=&quot;476&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;body&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-04-22 오후 5.24.41.png&quot; data-origin-width=&quot;1418&quot; data-origin-height=&quot;384&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dbYB5V/btsbRy60PTF/321hZIkTu8SoF2TLPFW6H1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dbYB5V/btsbRy60PTF/321hZIkTu8SoF2TLPFW6H1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dbYB5V/btsbRy60PTF/321hZIkTu8SoF2TLPFW6H1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdbYB5V%2FbtsbRy60PTF%2F321hZIkTu8SoF2TLPFW6H1%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;1418&quot; height=&quot;384&quot; data-filename=&quot;스크린샷 2023-04-22 오후 5.24.41.png&quot; data-origin-width=&quot;1418&quot; data-origin-height=&quot;384&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 이 방법은 나중에 어떻게 활용할지 생각이 안난다.&lt;/p&gt;</description>
      <category>Spring</category>
      <category>ResponseEntity</category>
      <category>status code</category>
      <author>0h0</author>
      <guid isPermaLink="true">https://00h0.tistory.com/88</guid>
      <comments>https://00h0.tistory.com/88#entry88comment</comments>
      <pubDate>Mon, 24 Apr 2023 16:39:40 +0900</pubDate>
    </item>
    <item>
      <title>공식문서로 알아보는 IoC container</title>
      <link>https://00h0.tistory.com/87</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;size16&quot;&gt;Spring의 IoC container를 알아보기 위해 아래와 같은 개념들에 대해 정리한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;IoC란?&lt;/li&gt;
&lt;li&gt;Spring에서 IoC를 제공하기 위한 인터페이스&lt;/li&gt;
&lt;li&gt;IoC컨테이너 안에서 bean을 관리하는 생명주기&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;IoC(Inversion of control)&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 단어를 해석하면 &lt;b&gt;&amp;lsquo;제어의 역전&amp;rsquo;&lt;/b&gt;이다. 나는 여기서 제어라는 것은 의존성 관리를 의미한다고 생각한다. 객체지향 프로그래밍은 다양한 객체를 생성하고 객체끼리의 의존성을 관리하면서 객체를 사용하는 코드를 작성해 서비스를 구성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 A객체에서 B객체의 기능을 사용할 필요가 있어서 A클래스 내부에서 B객체를 생성하는 코드를 작성하게 되면 A와 B는 강하게 결합된다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;class ServiceA {
    public void doSomething() {
    }
}

class ServiceB {
    private ServiceA serviceA;

    public ServiceB() {
        serviceA = new ServiceA(); // ServiceA의 인스턴스를 직접 생성
    }

    public void doSomethingWithA() {
        serviceA.doSomething(); // ServiceA의 메서드를 호출
    }
}
&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;어떤 구체 클래스에 의존할지 내부적으로 관리하고 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;IoC적용&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드를 IoC를 적용해 객체의 생성, 의존성 관리 책임을 없애보겠습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;interface IService {
    void doSomething();
}

class ServiceA implements IService {
    @Override
    public void doSomething() {
        
    }
}

class ServiceB {
    private IService serviceA;

    public ServiceB(IService serviceA) {
        this.serviceA = serviceA; // IService 인터페이스를 통해 주입받음
    }

    public void doSomethingWithA() {
        serviceA.doSomething(); // IService 인터페이스를 통해 메서드를 호출
    }
}
&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;ServiceB는 객체를 생성하지 않는다.&lt;/li&gt;
&lt;li&gt;serviceA를 필드로 선언하고 있는데 의존성 관리가 없어졌다고 할 수 있나..?
&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;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;IoC == DI??&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring공식문서를 보면 아래와 같은 문구가 있다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;IoC is also known as dependency injection (DI).&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 개인적으로 DI는 IoC의 수단으로 활용된다고 생각한다. IoC는 객체 생성과 의존성 주입을 통해 이루어지는데, DI가 객체 생성까지 관여하진 않기 때문이다.&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;IoC container&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring에서 관리하는 객체를 bean이라고 한다. Spring에서는 BeanFactory, ApplicationContext인터페이스를 통해 bean생성과 의존성을 관리해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BeanFactory는 bean을 관리하기 위한 IoC의 기본적인 기능이 정의된 인터페이스다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ApplicationContext는 BeanFactory의 기능에 더불어 아래 기능을 제공한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Easier integration with Spring&amp;rsquo;s AOP features&lt;/li&gt;
&lt;li&gt;Message resource handling (for use in internationalization)&lt;/li&gt;
&lt;li&gt;Event publication&lt;/li&gt;
&lt;li&gt;Application-layer specific contexts such as the&amp;nbsp;WebApplicationContext&amp;nbsp;for use in web applications.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 내용에 대해선 추가적인 학습이 필요해서 ApplicationContext는 더 많은 기능을 제공하는구나 정도로만 알고 넘어간다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;빈 생명주기 관리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IoC container는 메타데이터를 기반으로 bean인스턴스화, 의존성 주입, 소멸을 진행한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-04-19 오후 9.56.24.png&quot; data-origin-width=&quot;685&quot; data-origin-height=&quot;493&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QtcMg/btsbqAWDKQI/nY8nqIz8eoRQHWCdvD11rK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QtcMg/btsbqAWDKQI/nY8nqIz8eoRQHWCdvD11rK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QtcMg/btsbqAWDKQI/nY8nqIz8eoRQHWCdvD11rK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQtcMg%2FbtsbqAWDKQI%2FnY8nqIz8eoRQHWCdvD11rK%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;685&quot; height=&quot;493&quot; data-filename=&quot;스크린샷 2023-04-19 오후 9.56.24.png&quot; data-origin-width=&quot;685&quot; data-origin-height=&quot;493&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 메타데이터란 bean으로 관리할 객체, 객체 간 의존정보를 의미한다. 메타데이터는 3가지 방식으로 작성할 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;XML&lt;/li&gt;
&lt;li&gt;Annotation&lt;/li&gt;
&lt;li&gt;JAVA&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;빈 생명주기&lt;/b&gt;&lt;/h3&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;빈 생성 및 등록
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;생성자 주입의 경우 생성자 파라미터로 객체를 받기 때문에 이 부분에서 의존성 주입이 같이 발생한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;의존성 주입
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;필드 주입, 수정자 주입의 경우 모든 빈을 생성한 뒤 의존성 주입이 발생한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;초기화&lt;/li&gt;
&lt;li&gt;소멸&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;생명주기 중 초기화 부분에서 bean에 추가적으로 초기화할 정보를 세팅한다고 하는데, 생성자에서 세팅하지 않고 별도의 메서드를 통해 초기화 해야하는 이유가 뭘까?
&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;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;&lt;a href=&quot;https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans&quot;&gt;https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Spring</category>
      <category>IOC</category>
      <category>Spring</category>
      <category>Spring Container</category>
      <author>0h0</author>
      <guid isPermaLink="true">https://00h0.tistory.com/87</guid>
      <comments>https://00h0.tistory.com/87#entry87comment</comments>
      <pubDate>Thu, 20 Apr 2023 19:04:55 +0900</pubDate>
    </item>
    <item>
      <title>Jdbc와 Jdbc template비교</title>
      <link>https://00h0.tistory.com/86</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;기존 JDBC의 문제점&lt;/b&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;DB연결을 위한 자원 명시하기&lt;/li&gt;
&lt;li&gt;파라미터 바인딩&lt;/li&gt;
&lt;li&gt;마지막에 자원 반납하기
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 부분은 try with resource로 어느정도 해결이 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;결과 바인딩&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JDBC는 아래 코드 처럼 디비 연결, 파라미터 바인딩, 결과 변환 등등을 다른 쿼리를 날리는 메서드에서도 반복해야 한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;public List&amp;lt;Long&amp;gt; findAllGameRooms() {
    String selectQuery = &quot;SELECT game_room_id, status FROM game_room&quot;;
    try (Connection connection = dbConnection.getConnection()) {
        PreparedStatement preparedStatement = connection.prepareStatement(selectQuery);

        ResultSet resultSet = preparedStatement.executeQuery();
        List&amp;lt;Long&amp;gt; roomIds = new ArrayList&amp;lt;&amp;gt;();
        while (resultSet.next()) {
            roomIds.add(resultSet.getLong(&quot;game_room_id&quot;));
        }
        return roomIds;
    } catch (SQLException e) {
        throw new RuntimeException(e);
    }
}

public boolean saveChessBoard(Map&amp;lt;Position, Piece&amp;gt; board, Side currentTurn, Long roomId) {
    final String saveQuery = &quot;INSERT INTO pieces(piece_type, side, piece_rank, piece_file, game_room_id_fk) VALUES(?,?,?,?,?)&quot;;
    try (Connection connection = dbConnection.getConnection();
         PreparedStatement preparedStatement = connection.prepareStatement(saveQuery);) {
        try {
            for (Map.Entry&amp;lt;Position, Piece&amp;gt; pieces : board.entrySet()) {
                File file = pieces.getKey().getFile();
                Rank rank = pieces.getKey().getRank();
                PieceType pieceType = pieces.getValue().getPieceType();
                Side pieceSide = pieces.getValue().getSide();

                preparedStatement.setString(1, pieceType.name());
                preparedStatement.setString(2, pieceSide.name());
                preparedStatement.setString(3, rank.getText());
                preparedStatement.setString(4, file.getText());
                preparedStatement.setLong(5, roomId);

                preparedStatement.executeUpdate();
            }
            connection.commit();
            return true;
        } catch (SQLException sqlException) {
            connection.rollback();
            throw new RuntimeException(sqlException);
        }
    } catch (SQLException e) {
        throw new RuntimeException(e);
    }
}

public Connection getConnection() {
    try {
        Connection connection = DriverManager.getConnection(&quot;jdbc:mysql://&quot; + SERVER + &quot;/&quot; + DATABASE + OPTION, USERNAME, PASSWORD);
        connection.setAutoCommit(false);
        return connection;
    } catch (final SQLException e) {
        System.err.println(&quot;DB 연결 오류:&quot; + e.getMessage());
        e.printStackTrace();
        return null;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;JDBC Template&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바로 코드를 보면서 비교해보겠다. 코드는 공식문서에서 가져왔다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;DB연결&lt;/b&gt;&lt;/h3&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Repository 
public class JdbcCorporateEventDao implements CorporateEventDao {

    private JdbcTemplate jdbcTemplate;

    @Autowired 
    public JdbcCorporateEventDao(DataSource dataSource) {
        this.jdbcTemplate = new JdbcTemplate(dataSource); 
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB연결이 끝났다.이제 jdbcTemplate을 이용해 쿼리를 날릴 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;DataSouce는 디비 연결을 위한 정보(username, password등등)를 관리하는 객체다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&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;stata&quot;&gt;&lt;code&gt;String query = &quot;insert into t_actor (first_name, last_name) values (?, ?)&quot;;
this.jdbcTemplate.update(query, &quot;Leonor&quot;, &quot;Watling&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 JDBC코드보다 파라미터 바인딩이 훨씬 깔끔해졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나, 개인적으로 위 방식은 개발자가 파라미터를 잘못 바인딩할 위험이 있다고 생각한다. 그래서 Spring에서도 NamedParameterJdbcTemplate을 제공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드만 간단하게 보자면 아래와 같다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;String sql = &quot;select count(*) from T_ACTOR where first_name = :first_name&quot;;
SqlParameterSource namedParameters = new MapSqlParameterSource(&quot;first_name&quot;, firstName);

return this.namedParameterJdbcTemplate.queryForObject(sql, namedParameters, Integer.class);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 식으로 명시적으로 파라미터를 바인딩 해 실수를 줄일 수 있다. 자세한 내용은 &lt;a href=&quot;https://docs.spring.io/spring-framework/docs/current/reference/html/data-access.html#jdbc-NamedParameterJdbcTemplate%20&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;공식문서&lt;/a&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;reasonml&quot;&gt;&lt;code&gt;private final RowMapper&amp;lt;Actor&amp;gt; actorRowMapper = (resultSet, rowNum) -&amp;gt; {
    Actor actor = new Actor();
    actor.setFirstName(resultSet.getString(&quot;first_name&quot;));
    actor.setLastName(resultSet.getString(&quot;last_name&quot;));
    return actor;
};

public List&amp;lt;Actor&amp;gt; findAllActors() {
    return this.jdbcTemplate.query(&quot;select first_name, last_name from t_actor&quot;, actorRowMapper);
}
&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;RowMapper를 이용해 조회 결과를 기반으로 객체를 만들 수 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여기서 resultSet은 Jdbc와 마찬가지로 조회된 결과를 의미하고, rowNum은 조회된 데이터의 row개수를 의미한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;query는 여러 건 조회, queryForObject를 사용하면 단건 조회가 가능해서 return이 List가 아닌 Actor가 된다.&lt;/li&gt;
&lt;li&gt;공식문서를 보면 &lt;a href=&quot;https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/FunctionalInterface.html&quot;&gt;@FunctionalInterface&lt;/a&gt;가 붙어있는데 실제 코드를 보면 해당 어노테이션은 없다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;RowMapper는 메서드가 하나만 존재하는 인터페이스라 람다 전달이 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-04-20 오전 12.36.39.png&quot; data-origin-width=&quot;1178&quot; data-origin-height=&quot;584&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cqe2XB/btsblzkxZ5m/GYU3bud7ihf7uLtPrk35bK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cqe2XB/btsblzkxZ5m/GYU3bud7ihf7uLtPrk35bK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cqe2XB/btsblzkxZ5m/GYU3bud7ihf7uLtPrk35bK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcqe2XB%2FbtsblzkxZ5m%2FGYU3bud7ihf7uLtPrk35bK%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;680&quot; height=&quot;337&quot; data-filename=&quot;스크린샷 2023-04-20 오전 12.36.39.png&quot; data-origin-width=&quot;1178&quot; data-origin-height=&quot;584&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-04-20 오전 12.35.53.png&quot; data-origin-width=&quot;1034&quot; data-origin-height=&quot;744&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uLeg4/btsbtfLzgG0/qsYbzY8anegQkXshfwEJm1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uLeg4/btsbtfLzgG0/qsYbzY8anegQkXshfwEJm1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uLeg4/btsbtfLzgG0/qsYbzY8anegQkXshfwEJm1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuLeg4%2FbtsbtfLzgG0%2FqsYbzY8anegQkXshfwEJm1%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;675&quot; height=&quot;486&quot; data-filename=&quot;스크린샷 2023-04-20 오전 12.35.53.png&quot; data-origin-width=&quot;1034&quot; data-origin-height=&quot;744&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JDBC Template은 JDBC의 반복코드를 상당부분 줄여줘 개발 생산성을 높여주고, 가독성도 높아진다.&lt;/li&gt;
&lt;li&gt;추가적으로 insert를 손쉽게 지원해주는 SimpleJdbcInsert도 존재한다.
&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://docs.spring.io/spring-framework/docs/current/reference/html/data-access.html#jdbc-simple-jdbc-insert-1&quot;&gt;https://docs.spring.io/spring-framework/docs/current/reference/html/data-access.html#jdbc-simple-jdbc-insert-1&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;사용해본적은 없지만 SimpleJdbcCall도 존재한다.&lt;/li&gt;
&lt;/ul&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;공식문서에는 JdbcTemplate을 초기화 할 때 DataSource를 주입받는다. 그러나 Spring boot를 사용하면 명시적으로 주입 받을 필요가 없다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이유를 찾아보니 application.yml에 정의한 db관련 리소스를 바탕으로 Spring boot가 자동으로 DataSource빈을 생성한다.&lt;/li&gt;
&lt;li&gt;이후, JdbcTemplate에 주입해주기 때문에 별도로 DataSource객체를 생성하려고 신경쓰지 않아도 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1681918788886&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private final JdbcTemplate jdbcTemplate;

public GameDao(JdbcTemplate jdbcTemplate) {
    this.jdbcTemplate = jdbcTemplate;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Spring</category>
      <category>jdbc Template</category>
      <category>Spring</category>
      <author>0h0</author>
      <guid isPermaLink="true">https://00h0.tistory.com/86</guid>
      <comments>https://00h0.tistory.com/86#entry86comment</comments>
      <pubDate>Thu, 20 Apr 2023 19:04:05 +0900</pubDate>
    </item>
    <item>
      <title>[Spring JDBC] SimpleJdbcInsert로 쉽게 INSERT하기</title>
      <link>https://00h0.tistory.com/85</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;SimpleJdbcInsert란&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 JdbcTemplate를 이용한 Insert보다 &lt;b&gt;손쉽게 데이터를 저장하기 위해 제공하는 구현체&lt;/b&gt;다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 저장 후 primaryKey를 알고 싶은 경우 SimpleJdbcInsert를 이용해 keyHolder없이 구현할 수 있다.&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;SimpleJdbcInsert와 JdbcTemplate제공 기능 차이&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;SimpleJdbcInsert&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;SimpleJdbcInsert는 &lt;b&gt;데이터를 저장하기 위한 기능만 제공&lt;/b&gt;하기 위해 &lt;b&gt;SimpleJdbcInsertOperations를 구현&lt;/b&gt;하고 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-04-11 오후 11.54.23.png&quot; data-origin-width=&quot;787&quot; data-origin-height=&quot;62&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vNS1q/btr9CJg8o3o/MiXGKPTkMGTYWQNJVevMAk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vNS1q/btr9CJg8o3o/MiXGKPTkMGTYWQNJVevMAk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vNS1q/btr9CJg8o3o/MiXGKPTkMGTYWQNJVevMAk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvNS1q%2Fbtr9CJg8o3o%2FMiXGKPTkMGTYWQNJVevMAk%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;787&quot; height=&quot;62&quot; data-filename=&quot;스크린샷 2023-04-11 오후 11.54.23.png&quot; data-origin-width=&quot;787&quot; data-origin-height=&quot;62&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;execute()&lt;/li&gt;
&lt;li&gt;executeAndReturnKey()&lt;/li&gt;
&lt;li&gt;withTableName()&lt;/li&gt;
&lt;li&gt;등등...&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;JdbcTemplate&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JdbcTemplate은 데이터 &lt;b&gt;저장뿐만 아니라 조회, 삭제, 업데이트 등의 기능을 제공&lt;/b&gt;하고 있기 때문에 &lt;b&gt;JdbcOperations를 구현&lt;/b&gt;하고 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-04-11 오후 11.56.47.png&quot; data-origin-width=&quot;614&quot; data-origin-height=&quot;63&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/y5HOK/btr9rgupxwk/zqrT4t8OcMoJCDVlPTNLJ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/y5HOK/btr9rgupxwk/zqrT4t8OcMoJCDVlPTNLJ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/y5HOK/btr9rgupxwk/zqrT4t8OcMoJCDVlPTNLJ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fy5HOK%2Fbtr9rgupxwk%2FzqrT4t8OcMoJCDVlPTNLJ0%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;614&quot; height=&quot;63&quot; data-filename=&quot;스크린샷 2023-04-11 오후 11.56.47.png&quot; data-origin-width=&quot;614&quot; data-origin-height=&quot;63&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;JdbcTemplate으로 INSERT후 primaryKey가져오기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;keyHolder를 이용해 코드를 작성해야 한다.&lt;/p&gt;
&lt;div style=&quot;background-color: #ffffff; color: #080808;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;final String sql = &quot;insert into customers (first_name, last_name) values (?, ?)&quot;;
GeneratedKeyHolder keyHolder = new GeneratedKeyHolder();
jdbcTemplate.update(connection -&amp;gt; {
    PreparedStatement ps = connection.prepareStatement(sql, new String[]{&quot;id&quot;});
    ps.setString(1, customer.getFirstName());
    ps.setString(2, customer.getLastName());
    return ps;
}, keyHolder);

long key = keyHolder.getKey().longValue();
return key;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 코드를 처음보고 이해가 안됐다. PreparedStatement객체 생성하고, 파라미터 바인딩하고, 반환하고 예제 코드를 봤을 때 읽기 싫었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 레벨1 체스미션에서 데이터 생성 후 primaryKey가 필요한 상황이 있었기 때문에 코드를 이해하고 넘어가고 싶었지만 쉽지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다행히 SimpleJdbcInsert가 위와 같은 과정을 간단하게 제공해주는 기능이 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;SimpleJdbcInsert로 대체하기&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;SimpleJdbcInsert객체 생성&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1681225991437&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private SimpleJdbcInsert insertActor;

public void setDataSource(DataSource dataSource) {
    this.insertActor = new SimpleJdbcInsert(dataSource)
            .withTableName(&quot;t_actor&quot;)
            .usingGeneratedKeyColumns(&quot;id&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;usingGeneratedKeyColumns메서드를 통해 자동생성되는 key가 있는 column을 지정해준다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;저장할 Actor코드&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1681226617346&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Actor {
    private long id;
    private String firstName
    private String lastName;

    public Actor(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    public Actor(long id, String firstName, String lastName) {
        this.id = id;
        this.firstName = firstName;
        this.lastName = lastName;
    }

    public long getId() {
        return id;
    }

    public String getFirstName() {
        return firstName;
    }

    public String getLastName() {
        return lastName;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Map을 통한 저장&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1681226532046&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public Actor add(Actor actor) {
    Map&amp;lt;String, Object&amp;gt; parameters = new HashMap&amp;lt;String, Object&amp;gt;(2);
    parameters.put(&quot;first_name&quot;, actor.getFirstName());
    parameters.put(&quot;last_name&quot;, actor.getLastName());
    Long newId = insertActor.executeAndReturnKey(parameters).longValue();
    
    return new Actor(newId, actor.getFirstName(), actor.getLastName());
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JdbcTemplate보단 코드 가독성이 좋다. 그러나 Map보다 사용하기 편한 방법을 더 제공해준다.&lt;/p&gt;
&lt;h3 id=&quot;jdbc-simple-jdbc-parameters&quot; style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;SqlParameterSource를 이용한 저장&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #191e1e;&quot;&gt;SqlParameterSource인터페이스를 구현한 &lt;span style=&quot;color: #191e1e; text-align: start;&quot;&gt;&lt;b&gt;MapSqlParameterSource&lt;/b&gt;, &lt;span style=&quot;color: #24292e; text-align: start;&quot;&gt;&lt;b&gt;BeanPropertySqlParameterSource&lt;/b&gt;클래스를 제공한다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;MapSqlParameterSource&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1681226993723&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public Actor add(Actor actor) {
    SqlParameterSource parameters = new MapSqlParameterSource()
            .addValue(&quot;first_name&quot;, actor.getFirstName())
            .addValue(&quot;last_name&quot;, actor.getLastName());
    Long newId = insertActor.executeAndReturnKey(parameters).longValue();

	return new Actor(newId, actor.getFirstName(), actor.getLastName());
}&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;Map방식의 put대신&amp;nbsp;메서드 체이닝 방식을 이용해 데이터를 만든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(사실 Map방식과 큰 차이를 모르겠다..)&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;BeanPropertySqlParameterSource&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1681227206420&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public Actor add(Actor actor) {
    SqlParameterSource parameters = new BeanPropertySqlParameterSource(actor);
    Long newId = insertActor.executeAndReturnKey(parameters).longValue();
	
    return new Actor(newId, actor.getFirstName(), actor.getLastName());
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB컬럼과 인스턴스 변수가 일치하는 객체(빈)이 존재한다면 이 방법을 사용할 수 있다. &lt;b&gt;NOT NULL컬럼의 경우 반드시 해당 객체의 인스턴스 변수로 존재해야 한다.&lt;/b&gt; 만약, DB테이블에 age컬럼이 NOT NULL로 존재하는데 Actor클래스에 age변수가 없다면 에러가 발생한다.&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;b&gt;getter를 이용해 데이터를 추출해 저장&lt;/b&gt;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;마무리&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우아한테크코스 레벨2에서 Spring을 학습하기 전 맛보기 느낌으로 알게된 개념들이라 아직 실제로 사용해보진 못했다. 그러나, 레벨1 체스미션에서 직접 Jdbc API를 사용헤보고, JdbcTemplate도 사용해보고 나서 SimpleJdbcInsert를 사용해보니 발전된 과정을 본듯한 기분이 들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SimpleJdbcCall도 있는데 이건 아직 학습을 못했다. 미션 하면서 사용해봐야겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;출처&lt;/b&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://docs.spring.io/spring-framework/docs/current/reference/html/data-access.html#jdbc-simple-jdbc&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://docs.spring.io/spring-framework/docs/current/reference/html/data-access.html#jdbc-simple-jdbc&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Spring</category>
      <category>Java</category>
      <category>JDBC</category>
      <category>SimpleJdbcInsert</category>
      <author>0h0</author>
      <guid isPermaLink="true">https://00h0.tistory.com/85</guid>
      <comments>https://00h0.tistory.com/85#entry85comment</comments>
      <pubDate>Wed, 12 Apr 2023 21:06:31 +0900</pubDate>
    </item>
    <item>
      <title>[JAVA] generic에는 왜 primitive type을 쓸 수 없나?</title>
      <link>https://00h0.tistory.com/84</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;개요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;generic에서 왜 기본타입을 사용할 수 없는지 궁금해서 찾아본 내용입니다:)&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;generic등장 이전&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;generic이 존재하지 않을 땐 아래 코드처럼 List를 사용할 수 있었습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1680414816782&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;List integerList = new ArrayList();
integerList.add(new Integer(1));
Integer integer = (Integer) integerList.get(0);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 List는 Object타입을 원소를 받을 수 있습니다. 즉, 기본 타입을 제외한 모든 참조타입을 원소로 넣을 수 있습니다.&lt;/p&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;get으로 원소에 접근할 때 명시적인 형변환 작업이 필요하다.&lt;/li&gt;
&lt;li&gt;만약 String타입 원소를 넣었는데 Interger로 변환하는 실수를 컴파일 시점에 알 수 없다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;generic등장&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 문제점을 해결하는 generic이 JAVA5에 추가됐습니다. generic이 타입 안정성을 보장하고, 형변환의 번거로움을 해소시켜 줬습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;왜 primitive type은 사용할 수 없나?&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;타입 소거 방식을 사용한다.&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제네릭은 컴파일러에 의해 타입소거 과정을 거치게 됩니다. 이로 인해 기존 generic type은 모두 Object로 대치됩니다. 이로 인해 generic타입 파라미터에는 Object로 형변환 될 수 있는 참조 타입만 올 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1680441640049&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Example&amp;lt;T&amp;gt; {
    private T data;

    public T getData() {
        return data;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드가 컴파일러의 타입 소거 과정을 거치면 아래와 같이 변환됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1680441765309&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Example {
    private Object value;

    public Object getValue() {
        return value;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;generic type에 extends를 사용한 경우는 Object가 아닌 extends에 명시된 타입으로 대치됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1680442249630&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Example&amp;lt;T extends Number&amp;gt; {
    private T data;

    public T getData() {
        return data;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1680442265619&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Example {
    private Number value;

    public Number getValue() {
        return value;
    }
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Language/JAVA</category>
      <author>0h0</author>
      <guid isPermaLink="true">https://00h0.tistory.com/84</guid>
      <comments>https://00h0.tistory.com/84#entry84comment</comments>
      <pubDate>Sun, 2 Apr 2023 22:31:32 +0900</pubDate>
    </item>
    <item>
      <title>[OOP] EnumMap과 함수형 인터페이스로 커맨드 패턴 적용해보기</title>
      <link>https://00h0.tistory.com/83</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;우테코 5기 레벨1 체스미션을 진행하면서 커맨드 패턴을 적용한 과정을 정리해보겠습니다:)&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;size16&quot;&gt;체스미션에는 다양한 명령어가 있다. 움직이는 MOVE, 게임을 시작하는 START, 게임 점수를 보여주는 STATUS등등이 있다. 1단계 컨트롤러를 구현하면서 시간이 없어서 &lt;b&gt;명령어마다 분기문을 통해 수행되는 로직을 구현&lt;/b&gt;한 결과 &lt;b&gt;매우 더러운 코드&lt;/b&gt;가 나왔다.&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;private void play(ChessGame chessGame) {
    List&amp;lt;String&amp;gt; userCommandInput = repeatBySupplier(inputView::requestUserCommandInGame);
    try {
        GameCommand command = GameCommand.from(userCommandInput);
    if (isExitCommand(command)) {
        chessGameService.saveChessGame(ChessGameSaveRequestDto.from(chessGame));
    }
    if (!chessGame.isPlayable()) {
        return;
    }
    if (userCommandInput.equals(&quot;status&quot;)) {
        Map&amp;lt;Side, Double&amp;gt; scores = chessGame.calculateScores();
        outputView.printGameScores(scores);
        outputView.printWinner(chessGame.calculateWinner());
    }
    //move
    try {
        Position sourcePosition = Position.of(inputs.get(SOURCE_FILE_INDEX), inputs.get(SOURCE_RANK_INDEX));
        Position targetPosition = Position.of(inputs.get(TARGET_FILE_INDEX), inputs.get(TARGET_RANK_INDEX));

        chessGame.move(sourcePosition, targetPosition);
    } catch (IllegalArgumentException e) {
        outputView.printErrorMessage(e.getMessage());
        play(chessGame);
    }

    } catch (IllegalArgumentException e) {
        outputView.printErrorMessage(e.getMessage());
        play(chessGame);
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상당히 더러워서 읽기도 싫다. 그래서 &lt;b&gt;명령어.execute()&lt;/b&gt;를 하면 명령어에 맞는 기능이 수행되도록 고치고 싶었다. 주변 크루들에게 이러한 고민을 말하니까 커맨드 패턴을 얘기하길래 적용해봤다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;EnumMap과 BiConsumer를 이용한 패턴 적용&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;EnumMap을 사용한 이유&lt;/b&gt;는, 현재 명령어는 GameCommand라는 Enum으로 관리하고 있기 때문에, &lt;b&gt;명령어 별로 동작을 수행하기 위해&lt;/b&gt;서 GameCommand를 Key로 가지는 EnumMap을 사용했다.&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;b&gt;BiConsumer를 사용한 이유&lt;/b&gt;는, 게임 시작 이후 받을 수 있는 명령어는 현재 MOVE, END, STATUS가 있다. 3가지 명령어 모두 &lt;b&gt;반환타입은 없고&lt;/b&gt; parameter는 필요하다. 그 중 &lt;b&gt;MOVE명령어는 2개의 parameter(ChessGame, 이동 경로 List)가 필요&lt;/b&gt;했기 때문에 BiConsumer를 사용했다.&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;private final Map&amp;lt;GameCommand, BiConsumer&amp;lt;ChessGame, List&amp;lt;String&amp;gt;&amp;gt;&amp;gt; commands;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;livescript&quot;&gt;&lt;code&gt;public ChessController(InputView inputView, OutputView outputView, ChessGameService chessGameService) {
    this.inputView = inputView;
    this.outputView = outputView;
    this.chessGameService = chessGameService;
    this.commands = new EnumMap&amp;lt;&amp;gt;(GameCommand.class);

    commands.put(GameCommand.END, (chessGame, positions) -&amp;gt; endCommandExecute(chessGame));
    commands.put(GameCommand.STATUS, (chessGame, positions) -&amp;gt; statusCommandExecute(chessGame));
    commands.put(GameCommand.MOVE, (chessGame, positions) -&amp;gt; moveCommandExecute(chessGame, positions));
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드와 같이 EnumMap을 GameCommand, BiConsumer를 이용해 생성했다. 그리고 수행할 로직을 메서드로 만들고 람다를 이용해 명령어 별 수행 메서드를 저장했다.&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;div&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;private void endCommandExecute(ChessGame chessGame) {
    chessGameService.saveChessGame(ChessGameSaveRequestDto.from(chessGame));
    chessGame.end();
}

private void statusCommandExecute(ChessGame chessGame) {
    Map&amp;lt;Side, Double&amp;gt; scores = chessGame.calculateScores();
    outputView.printGameScores(scores);
    outputView.printWinner(chessGame.calculateWinner());
}

private void moveCommandExecute(ChessGame chessGame, List&amp;lt;String&amp;gt; commands) {
    String sourceText = commands.get(SOURCE_TEXT_INDEX);
    String targetText = commands.get(TARGET_TEXT_INDEX);

    chessGame.move(convertPosition(sourceText), convertPosition(targetText));
}

private Position convertPosition(String positionText) {
    List&amp;lt;String&amp;gt; positionTexts = Arrays.asList(positionText.split(POSITION_DELIMITER));
    return Position.of(positionTexts.get(FILE_INDEX), positionTexts.get(RANK_INDEX));
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;코드 비교&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1679760433476&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private void play(ChessGame chessGame) {
    List&amp;lt;String&amp;gt; userCommandInput = repeatBySupplier(inputView::requestUserCommandInGame);
    try {
        GameCommand command = GameCommand.from(userCommandInput);
    if (isExitCommand(command)) {
        chessGameService.saveChessGame(ChessGameSaveRequestDto.from(chessGame));
    }
    if (!chessGame.isPlayable()) {
        return;
    }
    if (userCommandInput.equals(&quot;status&quot;)) {
        Map&amp;lt;Side, Double&amp;gt; scores = chessGame.calculateScores();
        outputView.printGameScores(scores);
        outputView.printWinner(chessGame.calculateWinner());
    }
    //move
    try {
        Position sourcePosition = Position.of(inputs.get(SOURCE_FILE_INDEX), inputs.get(SOURCE_RANK_INDEX));
        Position targetPosition = Position.of(inputs.get(TARGET_FILE_INDEX), inputs.get(TARGET_RANK_INDEX));

        chessGame.move(sourcePosition, targetPosition);
    } catch (IllegalArgumentException e) {
        outputView.printErrorMessage(e.getMessage());
        play(chessGame);
    }

    } catch (IllegalArgumentException e) {
        outputView.printErrorMessage(e.getMessage());
        play(chessGame);
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;private void play(ChessGame chessGame) {
    List&amp;lt;String&amp;gt; userCommandInput = repeatBySupplier(inputView::requestUserCommandInGame);
    try {
        GameCommand command = GameCommand.from(userCommandInput);
        commands.get(command).accept(chessGame, userCommandInput);
    } catch (IllegalArgumentException e) {
        outputView.printErrorMessage(e.getMessage());
        play(chessGame);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;패턴 적용 이후 별도의 분기문 없이 명령어 별 로직을 수행할 수 있게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 새로운 명령어가 필요하면 EnumMap에 추가하고, 해당 명령어가 수행할 메서드만 생성하면 손쉽게 명령어를 추가 할 수 있습니다!&lt;/p&gt;</description>
      <category>OOP</category>
      <category>커맨드 패턴</category>
      <category>함수형 인터페이스</category>
      <author>0h0</author>
      <guid isPermaLink="true">https://00h0.tistory.com/83</guid>
      <comments>https://00h0.tistory.com/83#entry83comment</comments>
      <pubDate>Sun, 26 Mar 2023 01:10:31 +0900</pubDate>
    </item>
    <item>
      <title>[JAVA] Collections.emptyList() vs List.of()</title>
      <link>https://00h0.tistory.com/82</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;개요&lt;/b&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Collections.emptyList()&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 단순히 Collections에서 정적 팩토리 메서드를 이용해 빈 리스트를 생성해 반환해주나? 라는 생각으로 접근했습니다. Collections.emptyList()내부 구현을 봤더니 예상하지 못했던 코드가 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;EmptyList라는 클래스가 존재한다.&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EmptyList라는 클래스가 별도로 존재했고 이를 List&amp;lt;T&amp;gt;타입으로 캐스팅해 반환해주고 있었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-03-18 오후 3.22.28.png&quot; data-origin-width=&quot;757&quot; data-origin-height=&quot;444&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/u09Zv/btr4AUHHwtD/dmB737up3SMukzV5RmhsCk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/u09Zv/btr4AUHHwtD/dmB737up3SMukzV5RmhsCk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/u09Zv/btr4AUHHwtD/dmB737up3SMukzV5RmhsCk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fu09Zv%2Fbtr4AUHHwtD%2FdmB737up3SMukzV5RmhsCk%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;757&quot; height=&quot;444&quot; data-filename=&quot;스크린샷 2023-03-18 오후 3.22.28.png&quot; data-origin-width=&quot;757&quot; data-origin-height=&quot;444&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EmptyList의 Collections의 메서드들에 대해 빈 리스트에 맞게 정의되어 있습니다. get을 하려하면 IndexOutOfBoundsException예외 발생, size()는 return 0; 등이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 캡쳐본은 static EmptyList클래스의 구현 코드 중 일부입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-03-18 오후 3.25.22.png&quot; data-origin-width=&quot;909&quot; data-origin-height=&quot;447&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/PUfZb/btr4wLY8dCT/KD6d6q9k228znVTlvoNmO0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/PUfZb/btr4wLY8dCT/KD6d6q9k228znVTlvoNmO0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PUfZb/btr4wLY8dCT/KD6d6q9k228znVTlvoNmO0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPUfZb%2Fbtr4wLY8dCT%2FKD6d6q9k228znVTlvoNmO0%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;909&quot; height=&quot;447&quot; data-filename=&quot;스크린샷 2023-03-18 오후 3.25.22.png&quot; data-origin-width=&quot;909&quot; data-origin-height=&quot;447&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Immutable하다.&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식문서를 보면 아래와 같이 나와있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-03-18 오후 3.44.12.png&quot; data-origin-width=&quot;767&quot; data-origin-height=&quot;52&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b8kCY6/btr4yi9Weqo/INNwvvDtYjmo1k9ZlwobW1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b8kCY6/btr4yi9Weqo/INNwvvDtYjmo1k9ZlwobW1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b8kCY6/btr4yi9Weqo/INNwvvDtYjmo1k9ZlwobW1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb8kCY6%2Fbtr4yi9Weqo%2FINNwvvDtYjmo1k9ZlwobW1%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;52&quot; data-filename=&quot;스크린샷 2023-03-18 오후 3.44.12.png&quot; data-origin-width=&quot;767&quot; data-origin-height=&quot;52&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;b&gt;List&amp;lt;Object&amp;gt;가 반환된다.&lt;/b&gt;&lt;/h4&gt;
&lt;div style=&quot;background-color: #ffffff; color: #080808;&quot;&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;List&amp;lt;Object&amp;gt; emptyList = Collections.emptyList();&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드와 같이 Collections.emptyList()는 List&amp;lt;Object&amp;gt;가 반환됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;background-color: #ffffff; color: #080808;&quot;&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;List&amp;lt;Object&amp;gt; emptyList = Collections.emptyList();
System.out.println(emptyList.getClass());
emptyList.add(&quot;1&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;emptyList는 Immutable하기 때문에 add메서드를 호출하면 UnsupportedOperationException이 발생합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;EmptyList외 다양한 Empty자료구조&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;List외에도 Map, Set등 다양한 자료 구조에 대해 EmptyList와 동일하게 구현되어 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-03-18 오후 3.27.04.png&quot; data-origin-width=&quot;771&quot; data-origin-height=&quot;552&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kEycl/btr4uJ17eJz/U764IFN0C4fFjprSvFoaik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kEycl/btr4uJ17eJz/U764IFN0C4fFjprSvFoaik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kEycl/btr4uJ17eJz/U764IFN0C4fFjprSvFoaik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkEycl%2Fbtr4uJ17eJz%2FU764IFN0C4fFjprSvFoaik%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;771&quot; height=&quot;552&quot; data-filename=&quot;스크린샷 2023-03-18 오후 3.27.04.png&quot; data-origin-width=&quot;771&quot; data-origin-height=&quot;552&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-03-18 오후 3.27.18.png&quot; data-origin-width=&quot;949&quot; data-origin-height=&quot;709&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vTkZE/btr4xKFxSC8/jwBN5YOIjRh5UU1TGgggN0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vTkZE/btr4xKFxSC8/jwBN5YOIjRh5UU1TGgggN0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vTkZE/btr4xKFxSC8/jwBN5YOIjRh5UU1TGgggN0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvTkZE%2Fbtr4xKFxSC8%2FjwBN5YOIjRh5UU1TGgggN0%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;949&quot; height=&quot;709&quot; data-filename=&quot;스크린샷 2023-03-18 오후 3.27.18.png&quot; data-origin-width=&quot;949&quot; data-origin-height=&quot;709&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;List.of()&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;List.of()를 통해서도 리스트를 생성할 수 있습니다. 다만 List.of()는  ImmutableCollections의 리스트가 반환되기 때문에 이 역시 &lt;b&gt;불변&lt;/b&gt;합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;List인터페이스의 of메서드&lt;/b&gt;&lt;/h4&gt;
&lt;div style=&quot;background-color: #ffffff; color: #080808;&quot;&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;static &amp;lt;E&amp;gt; List&amp;lt;E&amp;gt; of() {
    return ImmutableCollections.emptyList();
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;ImmutableCollections의 emptyList메서드&lt;/b&gt;&lt;/h4&gt;
&lt;div style=&quot;background-color: #ffffff; color: #080808;&quot;&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;static &amp;lt;E&amp;gt; List&amp;lt;E&amp;gt; emptyList() {
    return (List&amp;lt;E&amp;gt;) ListN.EMPTY_LIST;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;세부 구현 코드&lt;/b&gt;&lt;/h4&gt;
&lt;div style=&quot;background-color: #ffffff; color: #080808;&quot;&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;static final class ListN&amp;lt;E&amp;gt; extends AbstractImmutableList&amp;lt;E&amp;gt;
        implements Serializable {

    // EMPTY_LIST may be initialized from the CDS archive.
    static @Stable List&amp;lt;?&amp;gt; EMPTY_LIST;

    static {
        VM.initializeFromArchive(ListN.class);
        if (EMPTY_LIST == null) {
            EMPTY_LIST = new ListN&amp;lt;&amp;gt;();
        }
    }

    @Stable
    private final E[] elements;

    @SafeVarargs
    ListN(E... input) {
        // copy and check manually to avoid TOCTOU
        @SuppressWarnings(&quot;unchecked&quot;)
        E[] tmp = (E[])new Object[input.length]; // implicit nullcheck of input
        for (int i = 0; i &amp;lt; input.length; i++) {
            tmp[i] = Objects.requireNonNull(input[i]);
        }
        elements = tmp;
    }

    @Override
    public boolean isEmpty() {
        return size() == 0;
    }

    @Override
    public int size() {
        return elements.length;
    }

    @Override
    public E get(int index) {
        return elements[index];
    }

    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        throw new InvalidObjectException(&quot;not serial proxy&quot;);
    }

    private Object writeReplace() {
        return new CollSer(CollSer.IMM_LIST, elements);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;List.of(), Collections.emptyList() 공통점&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;둘 다 불변&lt;/b&gt;이기 때문에 add, remove등의 작업을 수행하려고 하면 예외가 발생합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;List.of(), Collections.emptyList() 차이점&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;get()&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;List.of()&lt;/b&gt;의 경우 생성 시에 리스트에 원소를 설정할 수 있기 때문에 get메서드 호출 시, &lt;b&gt;예외가 발생하지 않습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Collections.emptyList()&lt;/b&gt;의 경우 내부에 원소가 있을 수 없기 때문에 get()호출 시 &lt;b&gt;UnsupportedOperationException 발생&lt;/b&gt;합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;size()&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;List.of()는 위와 마찬가지로 생성 시 원소 설정이 가능하기 때문에 아래 코드와 같이 동작합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #ffffff; color: #080808;&quot;&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;public int size() {
    return elements.length;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Collections.emptyList()는 비어있기 때문에 무조건 0을 반환합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #ffffff; color: #080808;&quot;&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;public int size() {return 0;}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 외에도 isEmpty(), contains() 등등의 메서드에서 차이가 있습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&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;size16&quot;&gt;List.of()와 Collections.emptyList()를 아래와 같이 사용할 것 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;만약 정말 &lt;b&gt;아무것도 없는 불변 리스트를 반환&lt;/b&gt;하고 싶으면 &lt;b&gt;Collections.emptyList()&lt;/b&gt;사용&lt;/li&gt;
&lt;li&gt;불변이지만 &lt;b&gt;내부 원소를 선언하고 싶으면 List.of()&lt;/b&gt;사용&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Language/JAVA</category>
      <category>Collections.emptyList()</category>
      <category>List.of()</category>
      <category>자바</category>
      <author>0h0</author>
      <guid isPermaLink="true">https://00h0.tistory.com/82</guid>
      <comments>https://00h0.tistory.com/82#entry82comment</comments>
      <pubDate>Sat, 18 Mar 2023 16:06:17 +0900</pubDate>
    </item>
    <item>
      <title>다형성이란?</title>
      <link>https://00h0.tistory.com/81</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;size16&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;size16&quot;&gt;단어만 보면 &lt;b&gt;&quot;다양한 형태를 가질 수 있는 성질인가?&quot;&lt;/b&gt;라는 생각이 먼저 들었습니다. 위키백과를 보면 다형성을 아래와 같이 설명하고 있습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;프로그램 언어의 각 요소들(&lt;a href=&quot;https://ko.wikipedia.org/wiki/%EC%83%81%EC%88%98&quot;&gt;상수&lt;/a&gt;,&amp;nbsp;&lt;a href=&quot;https://ko.wikipedia.org/wiki/%EB%B3%80%EC%88%98_(%EC%BB%B4%ED%93%A8%ED%84%B0_%EA%B3%BC%ED%95%99)&quot;&gt;변수&lt;/a&gt;,&amp;nbsp;&lt;a href=&quot;https://ko.wikipedia.org/wiki/%EC%8B%9D&quot;&gt;식&lt;/a&gt;,&amp;nbsp;&lt;a href=&quot;https://ko.wikipedia.org/wiki/%EC%98%A4%EB%B8%8C%EC%A0%9D%ED%8A%B8&quot;&gt;오브젝트&lt;/a&gt;,&amp;nbsp;&lt;a href=&quot;https://ko.wikipedia.org/wiki/%ED%95%A8%EC%88%98&quot;&gt;함수&lt;/a&gt;,&amp;nbsp;&lt;a href=&quot;https://ko.wikipedia.org/wiki/%EB%A9%94%EC%86%8C%EB%93%9C&quot;&gt;메소드&lt;/a&gt;&amp;nbsp;등)이 다양한 자료형(type)에 속하는 것이 허가되는 성질을 가리킨다.&amp;nbsp;&lt;/blockquote&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;&lt;b&gt;인터페이스를 통한 다형성&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;고스톱을 할 때, 패를 나눠주는 사람은 한 명이지만, 다양한 기술을 가진 딜러들이 있습니다. 타짜가 나눠줄 수도 있고, 공정한 플레이어가 나눠줄 수도 있습니다. 이처럼 패를 나눠주는 사람(Dealer)은 다양한 타입의 객체가 올 수 있습니다.  코드를 통해 이 예제를 살펴보겠습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Dealer인터페이스&lt;/b&gt;&lt;/h4&gt;
&lt;div style=&quot;background-color: #ffffff; color: #080808;&quot;&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;public interface Dealer {
    public void deal();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;패를 나눠주는 Dealer인터페이스를 정의합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;손기술이 나쁜 Tajja구현체&lt;/b&gt;&lt;/h4&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #080808;&quot;&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;public class Tajja implements Dealer{
    @Override
    public void deal() {
        System.out.println(&quot;밑장 빼기&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;얍삽한 플레이를 하는 Tajja딜러를 구현합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;정직한 PairDealer구현체&lt;/b&gt;&lt;/h4&gt;
&lt;div style=&quot;background-color: #ffffff; color: #080808;&quot;&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;public class PairDealer implements Dealer{
    @Override
    public void deal() {
        System.out.println(&quot;섞어서 나눠주기&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공정한 플레이를 하는 pairDealer딜러를 구현합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;고스톱 게임&lt;/b&gt;&lt;/h4&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;background-color: #ffffff; color: #080808;&quot;&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;public class GoStop {
    private final Dealer dealer;

    GoStop(final Dealer dealer) {
        this.dealer = dealer;
    }

    public void run() {
        dealer.deal();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GoStop게임을 진행할 GoStop클래스를 만들고 Dealer를 주입받습니다. 이후, 게임을 진행하기 위해 run메서드를 정의합니다. 해당 메서드는 딜러가 패를 나눠주는 Dealer.deal() 메서드를 호출합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;실행을 위한 메인메서드&lt;/b&gt;&lt;/h4&gt;
&lt;div style=&quot;background-color: #ffffff; color: #080808;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class Application {
    public static void main(String[] args) {
        final GoStop goStop = new GoStop(new Tajja());
        goStop.run(); //밑장 빼기
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 메인메서드를 생성하고 타짜 딜러가 카드게임을 진행해보겠습니다. 얍삽한 타짜가 카드를 나눠줬기 때문에 &quot;밑장 빼기&quot;가 출력됩니다.&lt;/p&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;size16&quot;&gt;이제 공정한 딜러인 PairDealer가 카드를 나눠줄 차례입니다. 코드 수정 부분은 메인메서드에서 딜러를 주입해주는 부분의 수정만 일어납니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #ffffff; color: #080808;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class Application {
    public static void main(String[] args) {
        final GoStop goStop = new GoStop(new PairDealer());
        goStop.run(); //섞어서 나눠주기
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 딜러는 아주 공정하게 카드를 나눠줬습니다.&lt;/p&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;size16&quot;&gt;그런데 갑자기 새로운 타짜 기술인 가운데서 빼기 기술을 사용하는 CenterTajjaDealer가 나타났습니다. 저희는 다형성을 적용해 코드를 구성했기 때문에 기존 코드 수정없이 손쉽게 새로운 Dealer타입을 추가할 수 있습니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #ffffff; color: #080808;&quot;&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;public class CenterTajjaDealer implements Dealer{
    @Override
    public void deal() {
        System.out.println(&quot;가운데서 빼기&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드처럼 새로운 Dealer코드를 기존 코드 수정 없이 추가를 통해 구현할 수 있습니다. 이후 메인메서드에서 GoStop에 Dealer를 주입해주는 부분만 CenterTajjaDealer로 수정하게 되면, 매우 간단하게 새로운 타입의 Dealer가 추가됩니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #ffffff; color: #080808;&quot;&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;public class Application {
    public static void main(String[] args) {
        final GoStop goStop = new GoStop(new CenterTajjaDealer());
        goStop.run();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;내가 생각하는 다형성의 이점&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼 다형성을 적용한다면, Dealer dealer에 Dealer인터페이스를 구현한 new Tajja(), new CenterTajjaDealer(), new PairDealer()같은 &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;size16&quot;&gt;다형성을 통해 &lt;b&gt;SOLID의 OCP, DIP&lt;/b&gt;원칙을 지킬 수 있습니다. 우선, &lt;b&gt;OCP의 경우&lt;/b&gt;, 새로운 CenterTajjaDealer를 추가할 때 &lt;b&gt;기존 코드의 변경 없이 새로운 딜러를 추가&lt;/b&gt;할 수 있었습니다. 수정엔 닫혀 있고, 확장엔 열려 있는 OCP원칙을 만족했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 &lt;b&gt;DIP의 경우&lt;/b&gt;, GoStop의 Dealer dealer는 Dealer구현체인 Tajja, CenterTajjaDealer, PairDealer에 의존하지 않고 &lt;b&gt;Dealer라는 추상타입에 의존&lt;/b&gt;하고 있기 때문에 DIP를 지킬 수 있었습니다.&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;b&gt;다형성은 추후 다양한 패턴의 근간&lt;/b&gt;이 된다고 생각하기 때문에 확실하게 잡고 가면 좋은 개념이라는 생각이 듭니다:)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>OOP</category>
      <category>Java</category>
      <category>다형성</category>
      <author>0h0</author>
      <guid isPermaLink="true">https://00h0.tistory.com/81</guid>
      <comments>https://00h0.tistory.com/81#entry81comment</comments>
      <pubDate>Sat, 11 Mar 2023 00:00:29 +0900</pubDate>
    </item>
    <item>
      <title>eqauls, hashCode(feat. 동일성과 동등성)</title>
      <link>https://00h0.tistory.com/80</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;동일성&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 객체가 &lt;b&gt;물리적으로 같은 주소에 저장되어 있는지&lt;/b&gt;에 대한 성질로 &lt;b&gt;==연산자&lt;/b&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;size16&quot;&gt;&lt;b&gt;두 객체의 필드값이 같은지에 대한 성질&lt;/b&gt;입니다. 두 객체의 필드 값이 같다면 두 객체는 논리적으로 동등하다고 할 수 있고, &lt;b&gt;equals()&lt;/b&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;size16&quot;&gt;우리는 객체가 같은지 비교할 때 equals()를 사용한다. 그 이유는 &lt;b&gt;==&lt;/b&gt;은 객체의 주소를 비교해 두 객체가 동일한지 &lt;b&gt;물리적 동일성을 비교&lt;/b&gt;한다. &lt;b&gt;equals()&lt;/b&gt;는 객체의 주소가 아닌 내부 필드값들이 같은지 &lt;b&gt;논리적 동등성을 비교&lt;/b&gt;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제부터 equals를 쓰기 위해 equals, hashCode에 대해 알아보겠습니다. equals와 hashCode를 재정의 하면 contains에서도 이점을 볼 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;equals()&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;eqauls()는 위에서 적었듯이 두 객체의 논리적 동등성을 비교합니다. 즉, new를 통해 2개의 인스턴스를 생성할 때, 필드를 동일한 값으로 설정하고 equals()를 사용하면 true가 반환됩니다. 그러나 저희의 의도대로 equals()가 동작하려면 오버라이딩이 필요합니다. 오버라이딩은 인텔리제이에서 제공해 주기 때문에 이것을 사용하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 Human클래스를 만들고 인스턴스 변수로 &lt;b&gt;String name&lt;/b&gt;, &lt;b&gt;String Address&lt;/b&gt;를 선언해 줍니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;public class Human {
    private final String name;
    private final String address;

    Human(final String name, final String address) {
        this.name = name;
        this.address = address;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 2개의 Human인스턴스를 생성하고 equals()로 비교를 하면 false가 반환됩니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;final Human human1 = new Human(&quot;잠실&quot;, &quot;레오&quot;);
final Human human2 = new Human(&quot;잠실&quot;, &quot;레오&quot;);

System.out.println(human1.equals(human2)); //false&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Object의 eqauls&lt;/b&gt;메서드를 보면 아래와 같습니다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;public boolean equals(Object obj) {
    return (this == obj);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼 &lt;b&gt;물리적 동일성을 비교&lt;/b&gt;하고 있기 때문에 equals() 메서드를 저희의 의도에 맞게 논리적 동등성을 비교하도록 오버라이딩할 필요가 있습니다. 인텔리제이가 생성해주는 equals를 사용하면 아래와 같은 코드가 나옵니다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Override
public boolean equals(final Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    final Human human = (Human) o;
    return Objects.equals(name, human.name) &amp;amp;&amp;amp; Objects.equals(address, human.address);
}&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;만약 두 인스턴스의 물리적으로 동일하면 true를 반환합니다.&lt;/li&gt;
&lt;li&gt;parameter로 들어온 인스턴스가 null이거나, 두 인스턴스의 타입이 다르면 false를 반환합니다.&lt;/li&gt;
&lt;li&gt;타입도 같고, null도 아니라면 Human타입으로 캐스팅합니다.&lt;/li&gt;
&lt;li&gt;이후 논리적 동등성을 비교하길 원하는 인스턴스 변수 각각에 대해 equals로 논리적 동등성을 비교하고 이 결과를 반환합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;만약 인스턴스 변수에 별도로 만든 객체가 있다면 해당 객체에도 equals, hashCode오버라이딩이 필요합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 오버라이딩을 마치고 다시 equals비교를 해보면 true가 반환됩니다.&lt;/p&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;System.out.println(human1.equals(human2)); //true&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;hashCode()&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;hashCode()는 Object클래스에 정의되어 있는 메서드로, 객체가 저장된 주소값을 int로 변환하여 반환합니다. 즉, 이 메서드를 이용해 물리적 동일성을 비교할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 생성했던 human1, human2의 hashCode를 출력해 보면 아래와 같습니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;final Human human1 = new Human(&quot;잠실&quot;, &quot;레오&quot;);
        final Human human2 = new Human(&quot;잠실&quot;, &quot;레오&quot;);

        System.out.println(human1.equals(human2));
        System.out.println(human1.hashCode()); //1579572132
        System.out.println(human2.hashCode()); //359023572&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;new를 통해 새로운 인스턴스를 생성했기 때문에 서로 다른 주소에 저장되어 있는 걸 볼 수 있습니다.&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;Object명세 중 &lt;b&gt;'equals(Object)가 두 객체를 같다고 판단했다면, 두 객체의 hashCode는 똑같은 값을 반환해야 한다.'라는&lt;/b&gt; 내용이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나, equals를 통해 human1과 human2를 비교하면 같다고 나오지만 hashCode는 다르게 나옵니다. hashCode가 다르게 나오는 이유는 equals와 마찬가지로 Object클래스의 hashCode()는 기본적으로 주소를 가지고 hashCode를 만들어내기 때문에 hashCode()도 오버라이딩을 통해 인스턴스 변수의 논리적 동등성을 비교하도록 해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 역시 인텔리제이에서 생성해 주는 코드를 가져오겠습니다.&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;@Override
    public int hashCode() {
        return Objects.hash(name, address);
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 다시 human1, human2의 hashCode값을 확인하면 동일한 것을 볼 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;System.out.println(human1.hashCode()); //52169753
System.out.println(human2.hashCode()); //52169753&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;hashCode()를 오버라이딩 안 했을 때 문제점&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 Human객체들을 관리할 때 이름, 주소 모두 중복되지 않는 객체들만 저장하고 싶어서 HashSet을 사용할 때 문제가 발생합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;hashSet의 add과정을 간략하게 아래와 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt; 저장하려는 객체의 hash값을 hashCode로 알아낸다.&lt;/li&gt;
&lt;li&gt;이미 저장된 객체들의 hash값 중 같은 것이 있는지 살펴본다.&lt;/li&gt;
&lt;li&gt;hash값이 같다면 equals를 통해 비교한다.&lt;/li&gt;
&lt;li&gt;equals도 true라면 같은 객체로 판별한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼 hash값을 사용하는 컬렉션들은 모두 hashCode를 이용해 hash값을 얻은 뒤, 이를 토대로 기능을 수행합니다. 그래서 만약 hashCode를 오버라이딩 하지 않고 아래와 같은 코드를 실행하면 원하는 결과가 나오지 않습니다.&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;Set&amp;lt;Human&amp;gt; humanHashSet = new HashSet&amp;lt;&amp;gt;();
humanHashSet.add(human2);
humanHashSet.add(human1);
System.out.println(humanHashSet.size()); // 2
humanHashSet.forEach(a -&amp;gt; System.out.println(a.hashCode()));
// 359023572
// 1579572132&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;hashCode만 오버라이딩 한 경우&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;hashCode만 오버라이딩&lt;/b&gt;하고 위 코드를 실행하면 아래와 같은 결과가 나옵니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;Set&amp;lt;Human&amp;gt; humanHashSet = new HashSet&amp;lt;&amp;gt;();
humanHashSet.add(human2);
humanHashSet.add(human1);
System.out.println(humanHashSet.size()); // 2
humanHashSet.forEach(a -&amp;gt; System.out.println(a.hashCode()));
// 52169753
// 52169753&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;equals를 재정의 하지 않아 add과정 중 마지막에 다른 객체로 판별해 hashSet에 추가되어 hashSet의 size가 2가 됩니다.&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;size16&quot;&gt;만약 객체의 인스턴스 간 필드 값이 같은지에 따라 같은 객체로 봐야 하는 상황이 있다면 equals(), hashCode()를 모두 재정의 해야 합니다. 그리고, 이를 통해 contains이점도 볼 수 있기 때문에 equals()와 hashCode() 모두 재정의 하는 것이 좋다!&lt;/p&gt;</description>
      <category>Language/JAVA</category>
      <category>equals</category>
      <category>hashCode</category>
      <category>Java</category>
      <category>동등성</category>
      <category>동일성</category>
      <author>0h0</author>
      <guid isPermaLink="true">https://00h0.tistory.com/80</guid>
      <comments>https://00h0.tistory.com/80#entry80comment</comments>
      <pubDate>Sat, 4 Mar 2023 19:31:53 +0900</pubDate>
    </item>
    <item>
      <title>원시값 포장을 왜 할까?</title>
      <link>https://00h0.tistory.com/79</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;들어가며&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 프리코스부터 레벨 1 미션동안 빠지지 않던 요구사항인 &lt;b&gt;원시값 포장&lt;/b&gt;에 대해 미션을 진행하면서 이것이 왜 필요한지, 반드시 적용해야 하는지에 대한 &lt;b&gt;주관적인 입장&lt;/b&gt;을&amp;nbsp;정리해보겠습니다.&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;b&gt;오용을 방지&lt;/b&gt;할 수 있다고 생각합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;파라미터에 동일한 타입이 존재하는 예시&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1677228339020&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Person {
    private final String name;
    private final String address;

    Person(final String name, final String address) {
        this.name = name;
        this.address = address;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드처럼 작성한 경우 클라이언트에서 아래와 같은 실수를 할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1677228473018&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Application {
    public static void main(String[] args) {
        final Person person = new Person(&quot;잠실&quot;, &quot;레오&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;이름, 주소&quot;&lt;/b&gt;순서로 인자로 넘어가야 하지만&lt;b&gt; &quot;주소, 이름&quot;&lt;/b&gt; 순서로 인자가 넘어갔기 때문에 해당 인스턴스는 &quot;잠실에 사는 레오&quot;가 아닌 &quot;레오에 사는 잠실&quot;이 됐습니다.&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;b&gt;원시값 포장&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 &lt;b&gt;이름을 포장해 보겠습니다.&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1677228667114&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Name {
    private final String name;

    public Name(final String name) {
        // 유효성 검사~~~
        this.name = name;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;name을 인스턴스 변수로 갖는 Name객체를 생성했습니다. 만약 시스템 요구사항에 &quot;이름은 1~5글자 사이만 가능하다!&quot;가 있을 경우 해당 유효성 검사를 생성자에서 진행함으로써 요구사항에 맞는 도메인 인스턴스를 생성할 수 있습니다.&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;b&gt;주소에 대해 포장해 보겠습니다.&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1677228791887&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class address {
    private final String nation;
    private final String city;

    address(final String nation, final String city) {
    	// 유효성 검사~~~
        this.nation = nation;
        this.city = city;
    }
}&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;이제 String name, String address를 받던 &lt;b&gt;Person의 생성자를 수정해보겠습니다.&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1677228984689&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Person {
    private final Name name;
    private final Address address;

    Person(final Name name, final Address address) {
        this.name = name;
        this.address = address;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 Person인스턴스를 생성하는 코드를 보면 컴파일 시점에 잘못된 타입을 건네주기 때문에 컴파일 오류가 발생하는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-02-24 오후 5.56.51.png&quot; data-origin-width=&quot;992&quot; data-origin-height=&quot;270&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/VZnxI/btr0GaBEFMi/wo7HDrgHWv2rOLdmtwXIR0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/VZnxI/btr0GaBEFMi/wo7HDrgHWv2rOLdmtwXIR0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/VZnxI/btr0GaBEFMi/wo7HDrgHWv2rOLdmtwXIR0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVZnxI%2Fbtr0GaBEFMi%2Fwo7HDrgHWv2rOLdmtwXIR0%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;992&quot; height=&quot;270&quot; data-filename=&quot;스크린샷 2023-02-24 오후 5.56.51.png&quot; data-origin-width=&quot;992&quot; data-origin-height=&quot;270&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;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;동일한 primitive타입의 파라미터가 있을 경우 이를 클라이언트 측에서 오용할 수 있지만, 이를 방지한다.&lt;/li&gt;
&lt;li&gt;캡슐화에 도움이 된다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인스턴스 변수로 원시값을 가지고 있기 때문에, 적절한 책임을 가지고 있는 객체가 탄생할 수 있다고 생각합니다.&lt;/li&gt;
&lt;li&gt;예를 들어, 로또 당첨 번호를 원시값으로 포장해 LottoNumber가 있다고 가정한 뒤, 사용자가 뽑은 로또 번호에 대해 LottoNumber와 비교해 당첨여부를 판단해 boolean값을 반환하는 책임을 가질 수 있다고 생각합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;그러면 모든 원시값을 무조건 포장해야 할까?&lt;/b&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;b&gt;요구사항이 있는 원시타입의 경우에 한해 포장을 해줘도 괜찮지 않을까?라는&lt;/b&gt; 입장입니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예를 들어, 자동차 경주 게임의 시도 횟수는 최대 10번까지 가능하다.라는 요구사항이 있을 경우 이 시도 횟수를 TryCount로 만들어 요구사항에 맞는 객체를 생성할 수 있습니다.&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>우아한테크코스5기/생각정리</category>
      <category>레벨1</category>
      <category>우아한테크코스</category>
      <category>원시값 포장</category>
      <author>0h0</author>
      <guid isPermaLink="true">https://00h0.tistory.com/79</guid>
      <comments>https://00h0.tistory.com/79#entry79comment</comments>
      <pubDate>Fri, 24 Feb 2023 19:12:42 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] 컴포넌트 스캔</title>
      <link>https://00h0.tistory.com/78</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;우선 Spring의 특징&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring은 객체지향의 장점을 살릴 수 있도록 도와줍니다. 객체지향 프레임워크는 객체들의 집합으로 객체들 간의 협력을 통해 기능을 완성시키기 때문에, 객체 간 의존관계를 잘 관리해야 합니다.&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;그렇다면, Spring에서는 어떤 식으로 객체들을 관리하는지 알아보겠습니다. 우선, Spring에서는 이러한 &lt;b&gt;객체들을 빈(bean)&lt;/b&gt;이라는 개념으로 표현합니다. 스프링 컨테이너에 빈들을 등록하고 관리하는 방식입니다.&amp;nbsp;&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;b&gt;@Bean&lt;/b&gt;, &lt;b&gt;컴포넌트 스캔&lt;/b&gt;을 활용하는 방법 &lt;b&gt;2가지&lt;/b&gt;가 있습니다. 이 2가지 방법을 비교해 보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;@Bean&lt;/b&gt;&lt;/h2&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Configuration
public class ApplicationConfig {

    @Bean
    public ARepo aRepo() {
        return new ARepo();
    }

    @Bean
    public AService aService() {
        return new AService(new ARepo());
    }
}&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;객체 관리를 담당하는 클래스를 생성한 뒤 @Configuration어노테이션을 작성합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 스프링 컨터이너에서 관리하고 싶은 빈들의 생성, 의존관계 주입을 수행하는 메서드를 작성하면서 위에 &lt;b&gt;@Bean어노테이션을 작성해 줍니다.&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;size16&quot;&gt;&lt;b&gt;aRepo() 메서드의&lt;/b&gt; 경우 스프링 컨테이너에 &lt;b&gt;&quot;aRepo : 객체&quot;&lt;/b&gt; 형태로 &lt;b&gt;key에는 return type의 앞 글자만 소문자로 바꾼 String&lt;/b&gt;이 설정되고, &lt;b&gt;value는 해당 객체가 할당&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;size16&quot;&gt;aService()처럼 의존관계를 설정할 수도 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;컴포넌트 스캔&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 방법처럼 수동으로 모든 객체를 생성하고 의존관계를 주입한다면 양도 많아지고, 이 과정에서 분명 실수도 나올 것입니다. 그래서 이러한 작업들을 Spring에서는 컴포넌트 스캔을 통해 개발자가 어노테이션만 적절하게 작성하면 빈 생성, 의존관계 주입을 자동으로 해줍니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Configuration
@ComponentScan
public class ApplicationConfig {
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Configuration, @ComponentScan을 같이 사용하게 되면 해당 클래스를 통해 모든 빈 생성, 의존관계 주입이 완료됩니다.&lt;/p&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;size16&quot;&gt;Spring은 &lt;b&gt;@ComponentScan이 작성된 클래스가 존재하는 패키지 하위에 존재하는 모든 클래스&lt;/b&gt; 파일 중 &lt;b&gt;@Component어노테이션&lt;/b&gt;이 붙은 객체들을 모두 &lt;b&gt;스프링 컨테이너에 빈으로 등록&lt;/b&gt;합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 빈 등록이 끝나면 Spring은 @Autowired를 통해 의존관계 주입을 시작합니다.&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;b&gt;@Controller, @Service, @Repository, @Configuration&lt;/b&gt;을 타고 들어가면 @Component가 붙어있는 것을 볼 수 있습니다. 그래서 해당 어노테이션도 &lt;b&gt;컴포넌트 스캔의 대상&lt;/b&gt;이 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-12-26 오후 11.18.17.png&quot; data-origin-width=&quot;1458&quot; data-origin-height=&quot;596&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bEP3R3/btrUInzxw2b/lKW8fRwT8KeTVRqrzBs53K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bEP3R3/btrUInzxw2b/lKW8fRwT8KeTVRqrzBs53K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bEP3R3/btrUInzxw2b/lKW8fRwT8KeTVRqrzBs53K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbEP3R3%2FbtrUInzxw2b%2FlKW8fRwT8KeTVRqrzBs53K%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;1458&quot; height=&quot;596&quot; data-filename=&quot;스크린샷 2022-12-26 오후 11.18.17.png&quot; data-origin-width=&quot;1458&quot; data-origin-height=&quot;596&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&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;size16&quot;&gt;이 과정에서&amp;nbsp; 아래 코드처럼 스프링 컨테이너에 등록되는 key값을 설정할 수 있습니다. 만약 key값이 중복되는 빈들이 있다면 예외가 발생하니 조심해야 합니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class A {
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;@Component
public class B {
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Configuration
@ComponentScan
public class TestConfig {
    @Bean(name = &quot;b&quot;)
    public A a() {
        return new A();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;현재 &lt;b&gt;A를 @Bean(name = &quot;b&quot;)를 통해 b를 key값으로 스프링 컨테이너에 등록&lt;/b&gt;했고, &lt;b&gt;@Component를 통해 B를 등록하는 과정에서 b로 변환되어 스프링 컨테이너에 빈이 등록&lt;/b&gt;됩니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 상황에서 Spring을 동작하면 에러가 발생하고 아래와 같은 설명이 나옵니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;The bean 'b', defined in class path resource [클래스 파일 존재 경로], could not be registered. A bean with that name has already been defined in file [클래스 파일 존재 경로]&amp;nbsp;and&amp;nbsp;overriding&amp;nbsp;is&amp;nbsp;disabled.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇기 때문에 &lt;b&gt;b라는 key값이 중복&lt;/b&gt;이 되어 에러가 발생한 것입니다.&lt;/p&gt;</description>
      <category>Spring</category>
      <category>Spring</category>
      <category>컴포넌트 스캔</category>
      <author>0h0</author>
      <guid isPermaLink="true">https://00h0.tistory.com/78</guid>
      <comments>https://00h0.tistory.com/78#entry78comment</comments>
      <pubDate>Mon, 26 Dec 2022 23:19:49 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] BeanFactory, ApplicationContext</title>
      <link>https://00h0.tistory.com/77</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;size16&quot;&gt;Spring은 객체지향의 장점을 살리면서 개발할 수 있는 프레임워크입니다. 객체지향 프로그래밍은 여러 객체들이 서로 의존하면서 주어진 기능을 수행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 위해 Spring에선 BeanFactory, ApplicationContext를 제공해 객체들을 관리해 줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;BeanFactory&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #474747;&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/beans/factory/BeanFactory.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;스프링 공식문서&lt;/a&gt;를 보면 &lt;/span&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #474747;&quot;&gt;&lt;/span&gt;&lt;/b&gt;&lt;u&gt;&lt;i&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #474747;&quot;&gt;&quot;The root interface for accessing a Spring bean container.&quot;&lt;/span&gt;&lt;/b&gt;&lt;/i&gt;&lt;/u&gt;&lt;span style=&quot;background-color: #ffffff; color: #474747;&quot;&gt; 즉,  스프링 빈에 접근하기 위한 기능들을 제공해 줍니다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스프링 컨테이너의 최상위 인터페이스입니다.&lt;/li&gt;
&lt;li&gt;스프링 빈을 조회, 관리하는 역할을 담당합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 빈 조회 시 사용되는&amp;nbsp;&lt;b&gt;getBean() 메서드를&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;size16&quot;&gt;그렇다면 ApplicationContext는 스프링 빈에 대해 어떤 기능을 제공하는지 알아보겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;ApplicationContext&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ApplicationContext는 BeanFactory를 상속한 인터페이스입니다. BeanFactory에 더해 추가적인 기능을 제공해 줍니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;MessageSource인터페이스&lt;/b&gt;를 상속받아 국제화 기능을 제공합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffffff; color: #373c46;&quot;&gt;이를 활용해 특정 message가 어떤 언어로 표현할지 쉽게 설정할 수 있습니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ListableBeanFactory&lt;/b&gt;를 상속받아 빈 관리에 대한 추가적인 기능을 제공합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;getBeanDefinitionCount&lt;span style=&quot;background-color: #ffffff; color: #353833;&quot;&gt;()&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffffff; color: #353833;&quot;&gt;getBeanDefinitionNames()&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffffff; color: #353833;&quot;&gt;등등...&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffffff; color: #353833;&quot;&gt;&lt;b&gt;ResourceLoader인터페이스&lt;/b&gt;를 상속받아 쉽게 파일을 읽어올 수 있습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #353833;&quot;&gt;ApplicationEventPublisher인터페이스&lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;background-color: #ffffff; color: #353833;&quot;&gt;를 상속받아 이벤트 프로그래밍 기능을 제공합니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #353833;&quot;&gt;정리&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #353833;&quot;&gt;이처럼 ApplicationContext는 BeanFactory를 상속받아 추가적인 기능을 제공하기 때문에, 보통 스프링 컨테이너는 ApplicationContext를 의미한다고 합니다.&lt;/span&gt;&lt;/p&gt;</description>
      <category>Spring</category>
      <category>ApplicationContext</category>
      <category>BeanFactory</category>
      <category>Spring</category>
      <category>스프링컨테이너</category>
      <author>0h0</author>
      <guid isPermaLink="true">https://00h0.tistory.com/77</guid>
      <comments>https://00h0.tistory.com/77#entry77comment</comments>
      <pubDate>Fri, 23 Dec 2022 16:07:56 +0900</pubDate>
    </item>
    <item>
      <title>[OOP] SOLID - DIP(의존성 역전 원칙)</title>
      <link>https://00h0.tistory.com/76</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;의존성 역전 원칙(DIP)란&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단하게 말해 &lt;b&gt;&quot;구체 클래스가 아닌 추상화에 의존해라&quot;&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;size16&quot;&gt;DIP에 찾아보면 &lt;b&gt;고수준 모듈(클래스)&lt;/b&gt;, &lt;b&gt;저수준 모듈(클래스)&lt;/b&gt;이란 단어가 많이 보입니다. &lt;b&gt;저수준 모듈은 고수준 모듈의 작업을 돕는 역할&lt;/b&gt;입니다. 이는 고수준 모듈이 저수준 모듈에 의존한다는 것을 의미합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 DIP는 고수준 모듈은 저수준 모듈에 의존하는 것이 아닌 추상화에 의존해야 한다고 합니다.&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;365&quot; data-origin-height=&quot;61&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/clp8Ax/btrUoLHkQ4Z/PNusWwatlpP0VTqpZjPkB0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/clp8Ax/btrUoLHkQ4Z/PNusWwatlpP0VTqpZjPkB0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/clp8Ax/btrUoLHkQ4Z/PNusWwatlpP0VTqpZjPkB0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fclp8Ax%2FbtrUoLHkQ4Z%2FPNusWwatlpP0VTqpZjPkB0%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;365&quot; height=&quot;61&quot; data-origin-width=&quot;365&quot; data-origin-height=&quot;61&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 그림은 고수준이 저수준을 직접 의존하는 관계입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;b&gt;DIP준수&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 자동차라는 고수준 모듈이 직접 스노우타이어라는 저수준 모듈에 의존하고 있습니다. 그러나 저는 이를 DIP를 지키기 위해 자동차가 추상화에 의존하도록 하고 싶습니다.&amp;nbsp;그래서 인터페이스를 이용해 스노우타이어를 타이어로 추상화한 그림은 아래와 같습니다.&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;414&quot; data-origin-height=&quot;146&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cj2x1b/btrUmCkaBsP/qvt5tc2bzsEsjUpzjM0SOK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cj2x1b/btrUmCkaBsP/qvt5tc2bzsEsjUpzjM0SOK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cj2x1b/btrUmCkaBsP/qvt5tc2bzsEsjUpzjM0SOK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcj2x1b%2FbtrUmCkaBsP%2Fqvt5tc2bzsEsjUpzjM0SOK%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;414&quot; height=&quot;146&quot; data-origin-width=&quot;414&quot; data-origin-height=&quot;146&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;size16&quot;&gt;이처럼 &lt;b&gt;변하기 쉬운 저수준 모듈에 직접 의존하기보단 변경 가능성이 적은 추상화에 의존함으로써 변경의 영향을 덜 받게 하는 것&lt;/b&gt;이 의존성 역전 원칙입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 인터페이스를 통한 추상화를 사용해서 그렇지만, 만약 구현 클래스가 잘 바뀌지 않는 경우도 있을 수 있다고 생각합니다. 이 경우 어떤 방법을 통해 DIP를 만족할 수 있을지에 대해 추가적으로 찾아볼 예정입니다.&lt;/p&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;size16&quot;&gt;&lt;a href=&quot;https://wikidocs.net/167372&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://wikidocs.net/167372&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;SOLID&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt; &lt;a href=&quot;https://00h0.tistory.com/32&quot;&gt;SRP&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://00h0.tistory.com/34&quot;&gt;OCP&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://00h0.tistory.com/41&quot;&gt;LSP&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://00h0.tistory.com/45&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;ISP&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;DIP&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>OOP</category>
      <category>DIP</category>
      <category>OOP</category>
      <category>solid</category>
      <category>의존성 역전 원칙</category>
      <author>0h0</author>
      <guid isPermaLink="true">https://00h0.tistory.com/76</guid>
      <comments>https://00h0.tistory.com/76#entry76comment</comments>
      <pubDate>Fri, 23 Dec 2022 01:49:01 +0900</pubDate>
    </item>
    <item>
      <title>우아한테크코스 5기 최종 코딩 테스트 회고</title>
      <link>https://00h0.tistory.com/75</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;size16&quot;&gt;우아한테크코스 백엔드 5기 최종 코딩 테스트 후기를 작성해보려고 합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;문제를 보고 느낀 점&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종 테스트를 보기 전 같이 피어리뷰 스터디를 하던 분들과 중꺾마 얘기를 하고 갔기 때문에 아무리 어려워도 끝까지 최선을 다한다는 마음으로 임했습니다. 사실 처음엔 문제가 잘 이해되진 않았지만 하나씩 풀어나가다 보니 서서히 풀린다는 느낌을 받았습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종 코딩 테스트 관련 메일에서&amp;nbsp;&lt;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;size16&quot;&gt;그리고 기능목록도 계속해서 업데이트하려고 했습니다. 그동안 해왔던 방식인 각 기능목록을 체크박스로 만들고 구현 완료 시 체크하는 방식으로 구현해왔기 때문에, 이를 최종 코딩 테스트에서도 적용했습니다. 이에 더불어 중간중간 생각나는 기능, 예외사항 역시 계속 추가하면서 최종 코딩 테스트를 치렀습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;아쉬웠던 점&lt;/b&gt;&lt;/h2&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;size16&quot;&gt;우선 하나의 카테고리는 2번까지만 추천될 수 있다는 요구사항이 있었습니다. 이에 저는 이를 만족하는 메서드를 구현하고 테스트코드 작성까지 완료했습니다. 하지만 &lt;b&gt;시험이 끝나고&lt;/b&gt; 추천하는 과정에서 추천된 카테고리의 횟수를 증가시키는 &lt;b&gt;메서드 호출을 빼먹은 것&lt;/b&gt;을 알게 됐습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 추천된 카테고리가 2번 추천됐는지 확인은 하지만 실제 추천된 횟수를 증가시키지 않기 때문에, 카테고리는 2번까지만 추천될 수 있다는 요구사항을 충족시키지 못했습니다. 호출 코드 하나를 놓친 점이 아쉽게 남았습니다. 전체적인 프로그램 흐름을 따라가면서 확인하는 작업을 가졌다면 쉽게 잡을 수 있는 실수라 더욱 아쉽게 남았습니다.&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;b&gt;원시값 포장, 일급 컬렉션&lt;/b&gt;&lt;/h4&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;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;만족한 점&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Enum사용&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 프리코스를 진행하면서 처음으로 자바를 사용해 봤습니다. 그래서 프리코스에서 Enum을 처음 사용해 봤고, 최종 코딩 테스트에서도 Enum을 적절하게 사용했다고 생각합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 메뉴 입력 시 유효한 메뉴인지 확인할 때 Enum을 활용해 구현 존재하는 메뉴가 아니라면 예외를 발생시키도록 했습니다.&lt;/p&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;size16&quot;&gt;그동안 객체지향적인 설계 경험 없이 프리코스를 시작해 관련 책을 읽고, 다양한 세미나를 찾아보는 등 객체지향에 익숙해지려고 노력했습니다. 그리고 다양한 문제를 풀어보면서 계속해서 고민하는 과정을 통해 확실히 객체지향에 익숙해졌다는 느낌을 최종 코딩 테스트에서 받았습니다.&lt;/p&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;size16&quot;&gt;프리코스, 최종 코딩 테스트를 준비하면서 우테코 커뮤니티, 피어 리뷰 스터디를 통해 다른 분들과 설계에 대한 의견도 주고받고, Java기능에 대해서도 보다 효율적인 방법을 조언받았습니다. 이 과정을 통해 StringJoiner, EnumMap, @MethodSource 등 다양한 Java기능들에 대해 새롭게 알 수 있었습니다.&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;b&gt;메시지를 보내라&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프리코스 중 피드백 내용에 있었던 내용 중 객체에 메시지를 보내라는 내용이 있었고, 이 부분 역시 계속 연습해오던 부분이었습니다. 그리고 이를 최종코딩테스트에서도 적용했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 추천된 메뉴가 코치가 못 먹는 음식인지 판단할 때 코치 별 못 먹는 음식을 가지고 있는 객체에게 isForbiddenMenu()라는 메서드를 구현해 데이터를 가져와 판단하는 것이 아닌 메시지를 보내는 방식으로 구현했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;구현 코드&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/youngh0/java-menu/tree/youngh0&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/youngh0/java-menu/tree/youngh0&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1671605478461&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - youngh0/java-menu&quot; data-og-description=&quot;Contribute to youngh0/java-menu development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/youngh0/java-menu/tree/youngh0&quot; data-og-url=&quot;https://github.com/youngh0/java-menu&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bWFqoa/hyQYOgcuNo/Twbnp9L0Rmt7iKQtABD8Hk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/youngh0/java-menu/tree/youngh0&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/youngh0/java-menu/tree/youngh0&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bWFqoa/hyQYOgcuNo/Twbnp9L0Rmt7iKQtABD8Hk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - youngh0/java-menu&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Contribute to youngh0/java-menu development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>우아한테크코스5기</category>
      <category>우아한테크코스</category>
      <category>최종코딩테스트</category>
      <author>0h0</author>
      <guid isPermaLink="true">https://00h0.tistory.com/75</guid>
      <comments>https://00h0.tistory.com/75#entry75comment</comments>
      <pubDate>Wed, 21 Dec 2022 15:52:05 +0900</pubDate>
    </item>
    <item>
      <title>[JAVA] HashMap을 이용해 Enum인스턴스 조회하기</title>
      <link>https://00h0.tistory.com/74</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;size16&quot;&gt;Enum을 활용하다 보면 filed값을 이용해 Enum 인스턴스를 조회해야 하는 경우가 있습니다. 이때, Stream, HashMap 등을 사용할 수 있습니다. 이 글에서는 &lt;b&gt;HashMap을 활용한 Enum인스턴스 조회 &lt;/b&gt;방법을 알아보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;HashMap활용 코드&lt;/b&gt;&lt;/h2&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;package enumPractice;

import java.util.Arrays;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;

public enum Number {
    ONE(1),
    TWO(2),
    THREE(3);

    private final int number;

    Number(int number) {
        this.number = number;
    }

    public int getNumber() {
        return number;
    }

    private static final Map&amp;lt;Integer, Number&amp;gt; numbers =
            Arrays.stream(Number.values())
                    .collect(Collectors.toMap(Number::getNumber, Function.identity()));

    public static Number findNumber(int number) {
        if (numbers.containsKey(number)) {
            return numbers.get(number);
        }
        throw new IllegalArgumentException(&quot;존재하지 않는 field 입니다.&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Stream을 활용해 &lt;b&gt;&quot;field : EnumInstance&quot;형태의 HashMap&lt;/b&gt;을 생성합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Function.identity()&lt;/b&gt;의 경우 HashMap의 &lt;b&gt;value에는 실제 instance&lt;/b&gt;가 들어가야 하기 때문에 사용합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;Arrays.asList(&quot;a&quot;, &quot;b&quot;, &quot;c&quot;)
          .stream()
          .map(Function.identity()) // &amp;lt;- 이것과
          .map(str -&amp;gt; str)          // &amp;lt;- 같습니다.&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;이후 findNumber함수의 parameter를 통해 HashMap에 접근해 값에 맞는 Enum instance를 반환합니다.&lt;/li&gt;
&lt;/ul&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;instance조회가 많이 발생하는 Enum의 경우 처음 HashMap을 만든 뒤에 이를 조회해서 반환해주기 때문에 &lt;b&gt;성능 측면에서 이점&lt;/b&gt;을 얻을 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Language/JAVA</category>
      <category>enum</category>
      <category>Java</category>
      <author>0h0</author>
      <guid isPermaLink="true">https://00h0.tistory.com/74</guid>
      <comments>https://00h0.tistory.com/74#entry74comment</comments>
      <pubDate>Tue, 13 Dec 2022 01:18:48 +0900</pubDate>
    </item>
    <item>
      <title>우아한테크코스 5기 프리코스 4주차 후기</title>
      <link>https://00h0.tistory.com/73</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;size16&quot;&gt;4주 간의 프리코스가 끝나면서 소감을 정리해보려고 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4주 간의 소감&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프리코스를 진행하기 전 저는 객체지향에 대한 감이 없었습니다. 4주 동안 &lt;span&gt;의식적인 연습을&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;통해 미션을 수행하면서 메서드를 분리하고, 클래스를 분리하고, 코드 컨벤션을 지키려고 노력했습니다. 그러다 보니 1주 차에 어색했던 부분이 2주 차에서 보다 능숙해지고 2주 차에 어색한 부분이 3주 차에는 보다 능숙해지는 경험을 할 수 있었습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;프리코스를 마치고 1, 2주 차의 코드를 보면 정말 리팩터링 할 요소가 너무 많이 보입니다. 그만큼 프리코스 기간 동안 성장한 거 같아 기쁘고, 앞으로도 다른 기수의 프리코스 내용을 연습하면서 계속해서 역량을 키워나갈 예정입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;4주 동안 힘들기도 했지만, 많은 성장을 할 수 있어서 보람 있는 4주로 기억될 거 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;배운 점&lt;/b&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;2가지 경로를 통해 역량을 키울 수 있었는데, 하나는 미션 내용으로 주어지는 코드 컨벤션과 다양한 클린 코드 원칙이 있습니다. 다른 하나는 프리코스 커뮤니티에서 진행되는 피어 리뷰입니다.&lt;/p&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;size16&quot;&gt;우선 프리코스를 진행하면서 지켜야 하는 다양한 클린 코드 원칙, 자바 컨벤션 등을 통해 가독성 있는 코드 작성에 대한 능력을 키울 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;테스트 코드&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 기본적으로 단위 테스트를 작성하면서 어떻게 해야 테스트하기 좋은 코드를 작성하는지에 대한 감을 잡을 수 있었습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;테스트하기 어려운 코드&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;테스트하기 어려운 random, 사용자 입력&lt;/b&gt; 값 등 개발자가 관리할 수 없이 항상 다른 값에 대해서 해당 부분을 &lt;b&gt;메서드의 parameter로 받게 함으로써&lt;/b&gt; 테스트하기 유용한 코드를 작성할 수 있었습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;테스트 코드도 코드다&lt;/span&gt;&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;3주 차 공통 피드백에 제시된 내용입니다. 테스트 코드도 코드기 때문에 지속적인 리팩터링을 통해 중복 제거가 필요합니다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;그동안 저는 3주 차 미션에서 제 테스트 코드는 정말 많은 중복이 있었습니다.&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;아래 내용을 보면 동일한 코드에 parameter만 &quot;1000 &quot;, &quot;1100&quot;, &quot;900&quot;으로 달라집니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1669351151531&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Nested
@DisplayName(&quot;로또 구입 금액 유효성 예외테스트&quot;)
class PaymentLottoMoneyExceptionTest {
    @Test
    @DisplayName(&quot;로또 구입 금액에 숫자가 아닌 것이 있으면 예외 발생&quot;)
    void hasNotNumberExceptionTest() {
        IllegalArgumentException exception = Assertions.assertThrows(IllegalArgumentException.class,
                () -&amp;gt; new PaymentLottoMoney(&quot;1000 &quot;));
        assertThat(exception.getMessage()).isEqualTo(ExceptionMessages.PAYMENT_ONLY_NUMBER);
    }

    @Test
    @DisplayName(&quot;로또구입금액이 천원 단위가 아니면 예외발생&quot;)
    void notThousandUnitExceptionTest() {
        IllegalArgumentException exception = Assertions.assertThrows(IllegalArgumentException.class,
                () -&amp;gt; new PaymentLottoMoney(&quot;1100&quot;));
        assertThat(exception.getMessage()).isEqualTo(ExceptionMessages.PAYMENT_ONLY_ONE_THOUSAND_UNIT);
    }

    @Test
    @DisplayName(&quot;로또구입금액이 천원 미만이면 예외발생&quot;)
    void lessThanThousandExceptionTest() {
        IllegalArgumentException exception = Assertions.assertThrows(IllegalArgumentException.class,
                () -&amp;gt; new PaymentLottoMoney(&quot;900&quot;));
        assertThat(exception.getMessage()).isEqualTo(ExceptionMessages.PAYMENT_ONLY_ONE_THOUSAND_UNIT);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 같은 로직은 중복 제거가 필요합니다 그래서 저는 4주 차에 아래와 같이 중복을 제거한 테스트 코드를 작성할 수 있었습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1669351378893&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@ParameterizedTest
@CsvSource(value = {&quot;0:U&quot;, &quot;1:D&quot;, &quot;2:D&quot;, &quot;3:U&quot;}, delimiter = ':')
@DisplayName(&quot;입력과 다리의 해당 칸 값이 일치한다면 true&quot;)
void isPassStepTest(int index, String moving) {
    assertThat(bridge.isPassStep(index, moving)).isEqualTo(true);
}&lt;/code&gt;&lt;/pre&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;size16&quot;&gt;4주차 미션을 진행하면서 InputView, OutputView의 경우 멀티 쓰레드 환경이 아니기 때문에 하나의 인스턴스만 있어도 되지 않을까?라는 생각을 했습니다. 그래서 저는 그동안 이론으로만 알고 있던 싱글톤 패턴을 적용해보려고 시도했습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class InputView {
    private static InputView inputview = new InputView();

    private InputView() {
    }

    public static InputView getInputView() {
        if (inputview == null) {
            inputview = new InputView();
        }
        return inputview;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class InputService {
    private static InputService inputService = new InputService();

    private InputService() {
    }

    public static InputService getInputService() {
        if (inputService == null) {
            inputService = new InputService();
        }
        return inputService;
    }

    public int inputBridgeSize() {
        while (true) {
            try {
                return InputView.getInputView().readBridgeSize();
            } catch (IllegalArgumentException exception) {
                OutputView.getOutputView().printErrorMessage(ExceptionMessage.BRIDGE_RANGE.getMessage());
            }
        }
    }

    public String inputPlayerMoving() {
        while (true) {
            try {
                return InputView.getInputView().readMoving();
            } catch (IllegalArgumentException exception) {
                OutputView.getOutputView().printErrorMessage(ExceptionMessage.MOVE.getMessage());
            }
        }
    }

    public String inputRetryCommand() {
        while (true) {
            try {
                return InputView.getInputView().readGameCommand();
            } catch (IllegalArgumentException exception) {
                OutputView.getOutputView().printErrorMessage(ExceptionMessage.RETRY_INPUT.getMessage());
            }
        }
    }
}&lt;/code&gt;&lt;/pre&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;size16&quot;&gt;view분리 중 미션을 진행하며 input, output객체들은 하나만 있으면 된다고 판단했습니다. 그래서 평소에 대략적인 이론만 알고있던 싱글톤 패턴을 더 찾아보면서 적용해봤습니다. 사실 디자인 패턴들에 대해 배울 때 &amp;ldquo;저런 이유 때문에 필요할 수 있겠구나&amp;rdquo;정도로 생각 했습니다. 그러나 미션을 진행하면서 &amp;ldquo;이런 부분을 해결하기 위해서 해당 패턴이 생겨났구나&amp;ldquo;라는 공감 했습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;b&gt;객체의 생성과 분리&lt;/b&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4주차 미션을 진행하면서, controller는 객체를 생성하지 않고 사용만 하고 싶었습니다. &amp;ldquo;오브젝트&amp;rdquo;라는 책을 읽으면서 객체의 생성과 분리를 읽고 보니, 저는 하나의 controller에서 객체를 생성하고, 메시지를 보내고 있었습니다. 그래서 저는 inputView, OutputView를 싱글톤으로 변경 후 인스턴스를 가져오는 형식으로 변경했습니다. 비록 게임 결과 인스턴스를 생성하는 부분도 분리하고 싶었지만, 성공하지 못한 점이 아쉽게 남았습니다.&lt;b&gt;&lt;/b&gt;&lt;/p&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;size16&quot;&gt;클래스를 작게 유지하려고 노력했습니다. controller에서 사용자의 입력을 받고 유효한 입력이 들어올 때 까지 반복하는 로직이 들어가있었습니다. 이렇게 하다 보니 controller의 크기가 너무 커졌다는 느낌을 받았습니다. 그래서 저는 InputService를 통해 유효한 입력이 들어올 때 까지 입력받는 책임을 분담 시키는 과정을 통해 작은 클래스를 유지하려고 노력했습니다.&lt;/p&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;size16&quot;&gt;&amp;nbsp;계속해서 제 코드를 살펴보면서 고칠 부분을 찾고, 이를 고치기 위해 구글링을 하다 보니 새로운 내용을 알게되고, 또 이를 바탕으로 고칠 점을 찾는 과정이 너무 재밌었습니다. 코드에 정답은 없기에 아직도 제 코드에는 리팩토링 할 요소가 있을 것이라 생각됩니다.&amp;nbsp;이번 프리코스를 통해 리팩토링이 재밌게 다가왔습니다.&lt;/p&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;size16&quot;&gt;기본적으로 프리코스를 진행하면서 기능 요구사항 목록을 정리해야 된다는 요구사항이 있습니다. 처음에는 이 부분이 어색하게 다가왔습니다. 그러나 미션을 반복하면서 요구사항을 정리하고 이를 기반으로 구현하고 기능별로 커밋하다 보니&lt;b&gt; 구현 중 길을 잃더라도&lt;/b&gt; 다시 지금 제가 &lt;b&gt;무엇을 해야 하는지 리마인드&lt;/b&gt; 하면서 길을 찾을 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;또한 &lt;b&gt;살아있는 문서를 위해&lt;/b&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;캡슐화&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;프리코스를 진행하면서 객체 지향과 관련된 책을 사서 읽고 많은 내용을 찾아봤습니다. 그 결과, 제 생각에는 &lt;b&gt;캡슐화를 지키는 것이 좋은 객체지향의 근간이 된다는 생각&lt;/b&gt;이 들었습니다. 저는 그동안 요구사항을 만족하기 위한 데이터들은 어떤 것이 있을지에 대해 먼저 생각을 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;하지만, 이번 기간 동안 공부를 하다 보니 데이터 중심이 아닌 &lt;b&gt;책임을 중심&lt;/b&gt;으로 생각해야 된다는 사실을 알게 됐습니다. 그래서 요구사항을 충족하기 위한 기능 목록을 정리하고 해당 기능을 수행하는 객체가 무엇인지 생각하고, 해당 기능을 수행하기 위한 데이터를 생각하며 구현했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;그리고, 메서드는 최대한 &lt;b&gt;추상화하여 public으로 공개&lt;/b&gt;하고 &lt;b&gt;세부적인 행동은 private&lt;/b&gt;으로 감싸려고 고민했습니다. 이렇게 코드를 작성하니 내부의 세부 구현 방식이 변경되더라고 변경으로 인한 영향이 해당 객체 밖으로 번지지 않아서 리팩터링이 보다 수월해짐을 느낄 수 있었습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1669352121332&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public void startGame() {
    BridgeGameResult bridgeGameResult = new BridgeGameResult(bridgeSize);
    while (gameProgress &amp;amp;&amp;amp; !isAllAnswer) {
        bridgeGameResult.clearResult();
        gameTryCount++;
        progressOneLife(bridgeGameResult);
    }
    showFinalResult(bridgeGameResult.getFinalResult(isAllAnswer, gameTryCount));
}


private void progressOneLife(BridgeGameResult bridgeGameResult) {
    int moveIndex = 0;
    boolean isPlay = true;
    while (isPlay &amp;amp;&amp;amp; !isAllAnswer) {
        isPlay = progressPlayerMove(moveIndex++, bridgeGameResult);
        isAllAnswer = bridgeGameResult.isGameSuccess();
    }
}

private boolean progressPlayerMove(int moveIndex, BridgeGameResult bridgeGameResult) {
    boolean isPossibleMove = bridgeGame.move(moveIndex, InputService.getInputService().inputPlayerMoving(), bridgeGameResult);
    OutputView.getOutputView().printMap(bridgeGameResult.getCurrentResult());
    if (!isPossibleMove) {
        askReplay();
    }
    return isPossibleMove;
}

private void askReplay() {
    String gameCommand = InputService.getInputService().inputRetryCommand();
    if (bridgeGame.retry(gameCommand)) {
        return;
    }
    gameProgress = false;
}

private void showFinalResult(StringBuffer result) {
    OutputView.getOutputView().printResult(result);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4주 차 구현 내용 중 일부입니다. 게임을 시작하고 싶으면 외부에서는 해당 객체 인스턴스의 startGame만 호출하면 &quot;나머지 세부 동작은 해당 인스턴스에서 알아서 처리해줄 거야&quot;라고 생각합니다. 그래서 startGame의 parameter가 추가되는 등의 변경이 아닌 내부 세부 동작 방식이 바뀌더라도 외부에 영향이 갈 일이 없습니다.&lt;/p&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;size16&quot;&gt;클래스를 식별할 때 책임을 수행할 클래스를 식별하기 위해 노력했습니다. 그리고 이렇게 식별된 객체들의 협력을 통해 요구사항을 만족시킬 수 있도록 노력했습니다. 이런 방식을 통해 객체를 식별하다 보니 제 생각엔 자율적인 객체들이 완성됐습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;아직 부족한 실력이지만 리팩터링을 통해 계속해서 클래스를 분리하는 과정을 통해 앞으로도 역량을 키울 수 있는 원동력을 얻어갈 수 있었습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;일급 컬렉션&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분도 미션을 진행하면서 새롭게 알게 된 지식입니다. 일급 컬렉션이란 &lt;b&gt;하나의 컬렉션을 인스턴스 변수로 가지는 객체&lt;/b&gt;를 말합니다. 자세한 설명은 &lt;a title=&quot;일급컬렉션&quot; href=&quot;https://jojoldu.tistory.com/412&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;다른 블로그의 글&lt;/a&gt;을 첨부하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;저는 4주 차 미션에서 게임 결과를 UP, DOWN각각을 별도의 List로 관리하는 방식을 택했습니다. 생각해보면 UP, DOWN의 List는 다리 건너기 게임 도메인에 종속적인 자료구조라는 생각을 했습니다. 그래서 저는 각각을 별도의 BridgeStair라는 객체로 분리해서 사용했습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;사실 BridgeStair가 좋은 네이밍은 아니라고 생각하지만, 단순히 List &amp;lt;String&amp;gt;보다는 BridgeStair타입으로 생성되는 것이 더 명확하다고 생각합니다.&lt;/p&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;size16&quot;&gt;이 부분은 프리코스 기간 동안 지키려고 노력한 부분입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;해당 내용은 공통 피드백에도 있었고, 객체지향 관련 서적과 다양한 글에서도 나오는 내용입니다. 예를 들어, 게임 성공 여부를 판단할 때 게임 결과를 알고 있는 객체에게 getter를 사용하는 것이 아닌 isGameSuccess와 같은 public메서드를 통해 성공 했는지 여부를 알려달라는 메시지를 보내는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;제가 생각한 메시지를 보내서 얻는 이점은 &lt;b&gt;캡슐화에 가까워 진다는 것&lt;/b&gt;입니다. 데이터와 관련 책임을 한 곳에 모으기 위해서는 getter가 아닌 해당 객체에서 데이터를 가지고 처리하고, 다른 객체에게는 단순히 &lt;b&gt;추상화된 메서드를 통해 내부 동작을 숨길 수 있기 때문&lt;/b&gt;입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1669358114141&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class BridgeStair{
	public boolean isGameSuccess() {
        return currentResult.size() == bridgeSize
                &amp;amp;&amp;amp; currentResult.get(currentResult.size() - 1).equals(BridgeGameResultStatus.CORRECT);
    }
}&lt;/code&gt;&lt;/pre&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;size16&quot;&gt;기능 구현을 위해 코드를 작성하다 보면 분명히 다른 부분에서 리팩터링 할 부분이 보이곤 합니다. 저는 이 경우 바로 리팩터링을 하곤 했습니다. 그러나 이렇게 하다 보면 기능 별 커밋이 어려워질 뿐만 아니라, 코드가 꼬이는 경험을 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;그래서 4주차 미션을 진행할 때는 리팩터링 할 요소가 보이면 문서에 기록을 한 뒤, 작성하던 기능 구현을 마치고 커밋한 뒤 구현을 하려고 노력했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이렇게 하니 마지막 미션을 그동안의 미션과 다르게 짧은 시간을 투자할 수 밖에 없었지만, 중간에 길을 잃어 허비하는 시간을 줄일 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;피어 리뷰&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 우 테코 5기부터 프리코스 커뮤니티가 생기면서 다양한 활동이 있었지만, 저는 그중에서 피어 리뷰에 참여하면서 얻어가는 것이 많았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;다른 분들이 제 코드에 대해 더 나은 방법을 제시해주시고, 또 제 코드에 대해 물어보시면서 다양한 생각을 할 수 있었습니다. 그리고 저 역시 다른 분들의 코드를 리뷰하면서 제가 생각한 방법보다 더 좋은 해결방법을 알 수 있었습니다. 또한,&amp;nbsp;&lt;span&gt;EnumMap, getOrDefault, 문자열 처리, StringJoiner 등 &lt;/span&gt;다양한 지식도 얻을 수 있는 경험이었습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;enum에서의 문자열 처리&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예외상황에 대한 문자열 처리를 할 때 저는 문자열 전체를 상수로 관리했습니다. 그러나 다른 분들의 코드를 보니 문자열 포매팅을 이용해 처리하는 방식이 더 좋아 보여 4주 차 미션에서 아래와 같이 적용해봤습니다.&lt;/p&gt;
&lt;pre class=&quot;processing&quot;&gt;&lt;code&gt;public enum ExceptionMessage {
    BRIDGE_RANGE(&quot; %d ~ %d 사이의 숫자를 입력해야 합니다.&quot;,
            BridgeSizeRange.MIN_BRIDGE_SIZE.getBridgeSize(),
            BridgeSizeRange.MAX_BRIDGE_SIZE.getBridgeSize()),
    MOVE(&quot; 이동 입력은 대문자 %s 혹은 대문자 %s 만 가능합니다.&quot;,
            BridgeStep.UP.getStep(),
            BridgeStep.DOWN.getStep()
    ),
    RETRY_INPUT(&quot; 재시작 입력은 대문자 %s 혹은 대문자 %s 만 가능합니다.&quot;,
            BridgeGameCommand.RETRY.getCommand(),
            BridgeGameCommand.QUIT.getCommand());

    private final String message;

    ExceptionMessage(String message, Object... replacers) {
        this.message = String.format(message, replacers);
    }

    public String getMessage() {
        String baseErrorMessage = &quot;[ERROR]&quot;;
        return baseErrorMessage + message;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;StringJoiner를 통한 반복 제거&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출력 형식을 맞추기 위해 저는 기존에 StringBuffer.append를 계속해서 사용했습니다. 그러나 다른 분이 제 코드를 리뷰해주시면서 StringJoiner에 대해 언급해주셨습니다. 4주 차 미션을 진행하면서 결과를 출력할 때, 해당 내용을 적용할 수 있는 부분이 있어서 아래와 같이 적용해봤습니다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;public StringJoiner getCurrentResult() {
    StringJoiner bridgeResultMessage = new StringJoiner(GameConstants.resultDelimiter, GameConstants.resultPrefix, GameConstants.resultPostfix);
    for (BridgeGameResultStatus result : currentResult) {
        bridgeResultMessage.add(result.getResultStatus());
    }
    return bridgeResultMessage;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;processing&quot;&gt;&lt;code&gt;public class GameConstants {
    public static final String startGameMessage = &quot;다리 건너기 게임을 시작합니다.\n&quot;;
    public static final String finalResultMessage = &quot;최종 게임 결과\n&quot;;
    public static final String resultDelimiter = &quot; | &quot;;
    public static final String resultPrefix = &quot;[ &quot;;
    public static final String resultPostfix = &quot; ]&quot;;

    private GameConstants() {

    }
}
&lt;/code&gt;&lt;/pre&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;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;nbsp;이 과정을 통해 같이 시너지를 내며 성장하는 경험을 할 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>우아한테크코스5기</category>
      <category>Java</category>
      <category>백엔드</category>
      <category>우테코</category>
      <category>프리코스</category>
      <author>0h0</author>
      <guid isPermaLink="true">https://00h0.tistory.com/73</guid>
      <comments>https://00h0.tistory.com/73#entry73comment</comments>
      <pubDate>Fri, 25 Nov 2022 15:22:16 +0900</pubDate>
    </item>
    <item>
      <title>우아한테크코스 5기 프리코스 3주차 후기</title>
      <link>https://00h0.tistory.com/72</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;size16&quot;&gt;3주 차 미션에서는 로또 게임이 나왔습니다. 미션을 진행하면서 있었던 일들에 대해 작성해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;신경 썼던 점&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;자율적인 객체 만들기&lt;/li&gt;
&lt;li&gt;테스트하기 좋은 코드 작성&lt;/li&gt;
&lt;li&gt;연관관계와 의존관계 분리&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;자율적인 객체&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캡슐화를 위해 객체가 자신이 가지고 있는 정보에 대한 책임을 가지게 함으로써 응집도를 높이고 결합도를 낮추고 싶었습니다. 왜냐하면 프리코스를 진행하면서 읽은 책, 구글링, 세미나 영상 등 모두 좋은 객체지향 설계를 위해서는 응집도를 높이고 결합도를 낮춰야 한다는 내용을 설명하며 캡슐화가 빠지지 않았습니다. 물론 2주 차 미션에서도 해당 내용을 지키기 위해서 노력했지만, 지속적인 노력을 통해 실력을 키우고 싶었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이 과정에서 로또 당첨 순위를 판별하는 책임을 전체 로또 정보를 가지고 있는 EntireLotto에 부여해야 하는지, 당첨 번호 정보를 알고 있는 WinningNumbers에 부여해야 하는지 고민했습니다. 이 과정에서 현재 객체들이 어떤 식으로 협력 하는지 관계도를 그리고 이렇게 해보고 저렇게 해보고 계속 고민을 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;고민한 결과 당첨 순위를 판별하기 위해서는 각각의 Lotto에 접근해야 하는데, EntireLotto는 이미 Lotto에 접근하고 있기 때문에 EntireLotto에서 당첨 순위를 판별하는 책임을 수행하도록 접근했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;그 결과 아래와 같은 코드를 작성했습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public void judgementEntireLottoWinning(WinningNumbers winningNumbers, RankingCount rankingCount) {
    for (Lotto lotto : entireLotto) {
        int correctLottoNumberCount = calculateContainsWinningNumbers(lotto, winningNumbers);
        boolean isBonus = calculateHasBonusNumber(lotto, winningNumbers);
        applyLottoRank(correctLottoNumberCount, isBonus, rankingCount);
    }
}

private int calculateContainsWinningNumbers(Lotto lotto, WinningNumbers winningNumbers) {
    int count = 0;
    for (int index = 0; index &amp;lt; Constant.CORRECT_LOTTO_SIZE; index++) {
        int lottoNumber = lotto.getIndexLottoNumber(index);
        if (winningNumbers.contains(lottoNumber)) {
            count += 1;
        }
    }
    return count;
}

private boolean calculateHasBonusNumber(Lotto lotto, WinningNumbers winningNumbers) {
    for (int index = 0; index &amp;lt; Constant.CORRECT_LOTTO_SIZE; index++) {
        int lottoNumber = lotto.getIndexLottoNumber(index);
        if (winningNumbers.hasBonusNumber(lottoNumber)) {
            return true;
        }
    }
    return false;
}

private void applyLottoRank(int correctLottoNumberCount, boolean isBonus, RankingCount rankingCount) {
    for (Ranking value : Ranking.values()) {
        if (value.getCorrectNumberCount() == correctLottoNumberCount &amp;amp;&amp;amp; value.isBonus() == isBonus) {
            rankingCount.plusRankingCount(value.name());
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;각각의 로또를 돌면서 winningNumber와 비교하며 당첨번호와 몇 개가 겹치는지, 보너스 번호가 맞는지 확인합니다.&lt;/li&gt;
&lt;li&gt;이후 당첨됐다면 &amp;lt;순위, count&amp;gt;로 구성된 rankingCount객체에게 해당 순위의 count를 증가시키는 메시지를 보냅니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이렇게 데이터와 책임을 묶다 보니 자연스럽게 domain이라는 패키지에 분리할 수 있는 객체들이 생겼고, 전체적인 요청을 받고 적절한 domain객체에 요청을 보내는 controller도 만들게 됐습니다. 또한 요구사항에 있는 view기능도 분리하다 보니 자연스럽게 MVC패턴의 형태를 띄게 된 거 같습니다. 사실 MVC패턴에 대해 알고 있긴 했지만, 이를 의도적으로 적용하려 하진 않았습니다. 단지 객체들의 분리에 초점을 맞췄고, view분리라는 요구사항을 만족하기 위해 계속해서 고민하다 보니 MVC형태를 띄고 있는 것이 신기했습니다.&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;b&gt;테스트하기 좋은 코드&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지난 2주차 미션을 진행하면서 다음 미션에는 테스트하기 좋은 코드를 작성하고 싶었습니다. 그래서 어떻게 해야 테스트하기 좋은 코드를 짤 수 있고, 테스트하기 어려운 코드는 무엇인지에 대해 찾아봤습니다.&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;size16&quot;&gt;외부 상태에 의존하는 코드입니다. 예를 들어 A메서드는 현재 시간을 기준으로 어떤 값을 도출합니다. 하지만 시간을 계속 흐르기 때문에 A메서드는 실행되는 시간에 따라 항상 다른 결과가 도출될 것입니다. 그리고 사용자의 입력에 따라 처리하는 메서드도 마찬가지입니다.&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;size16&quot;&gt;A메서드가 테스트할 수 없는 구조로 구현됐다고 가정합시다. B메서드에서 A를 사용하고, C메서드에서 B메서드를 사용한다고 생각해보면, 테스트할 수 없는 C메서드로 인해 A,B메서드 모두 테스트 할 수 없게 됩니다.&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;size16&quot;&gt;로또 게임에서 사용자의 입력을 받아 값을 생성하는 구매금액, 당첨번호의 생성자에서 값을 입력받는 것이 아닌 이를 parameter로 받게 변경했습니다. 이런 방법을 통해, 인스턴스 생성 시 해당 값들을 제가 임의로 설정해 테스트가 유용한 코드를 작성할 수 있었습니다.&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;b&gt;연관관계 vs 의존관계&lt;/b&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;&amp;nbsp;차이점을 알고 제 코드를 보니 모두 연관관계로 이루어져 있었고, 저는 해당 객체와의 협력이 하나의 public메서드에서만 쓰인다면 의존관계로 분리하자.&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;&amp;nbsp;그 결과 대부분 클래스의 인스턴스 변수를 1~3개 사이로 유지할 수 있었고, 이로 인해 클린 코드에 한 발짝 다가갔다고 생각해 신기했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;개선할 점&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;EnumMap&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에 Enum을 사용하면서 당첨 순위 별 상금, 맞춰야 하는 번호 개수, 보너스 번호 여부를 작성했습니다. 그리고 처음에는 여기에 count라는 값을 해당 순위에 당첨된 인원을 관리하고자 하는 의미로 추가했지만, 이는 상수의 개념이 아니기 때문에 별도의 map으로 관리하고자 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;하지만, 저는 &lt;b&gt;EnumMap&lt;/b&gt;개념을 모르고 아래 &lt;b&gt;Ranking enum&lt;/b&gt;의 상수값을 map자료구조를 가지고 있는 RankingCount에 key값으로 설정했습니다.&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class RankingCount {
    private Map&amp;lt;String, Integer&amp;gt; rankingCount = new LinkedHashMap&amp;lt;&amp;gt;();

    public RankingCount() {
        for (Ranking ranking : Ranking.values()) {
            rankingCount.put(ranking.name(), 0);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public enum Ranking {
    _5TH(3, false, 5_000, &quot;3개 일치 (5,000원)&quot;),
    _4TH(4, false, 50_000, &quot;4개 일치 (50,000원)&quot;),
    _3RD(5, false, 1_500_000, &quot;5개 일치 (1,500,000원)&quot;),
    _2ND(5, true, 30_000_000, &quot;5개 일치, 보너스 볼 일치 (30,000,000원)&quot;),
    _1ST(6, false, 2_000_000_000, &quot;6개 일치 (2,000,000,000원)&quot;);

    private int correctNumberCount;
    private boolean isBonus;
    private int price;
    private String printFormat;

    Ranking(int correctNumberCount, boolean isBonus, int price, String printFormat) {
        this.correctNumberCount = correctNumberCount;
        this.isBonus = isBonus;
        this.price = price;
        this.printFormat = printFormat;
    }

    public int getCorrectNumberCount() {
        return correctNumberCount;
    }

    public boolean isBonus() {
        return isBonus;
    }

    public String getPrintFormat() {
        return printFormat;
    }

    public int getPrice() {
        return price;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 피어 리뷰를 진행하면서 받은 공통적인 피드백이 해당 부분을 EnumMap을 활용하는 것이 더 좋아 보인다는 것이었습니다. 그래서 EnumMap에 대해 찾아보니 제가 고민하면서 별도의 map으로 분리했던 문제에 대한 해결책이 바로 EnumMap이었습니다.&amp;nbsp;&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;b&gt;Java기본기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분 역시 피어 리뷰를 받으면서 아직 Java기본기가 부족하다고 느꼈습니다. 저는 map에서 value를 뽑을 때 key가 없어서 발생하는 에러를 방지하기 위해 생성자에서 모든 순위에 대해 value를 0으로 초기화하는 과정을 거쳤습니다. 하지만 이는 map.getOrDefault로 방지할 수 있는 부분이라는 피드백을 받았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;그리고 StringBuffer를 이용하면서 개행에 있어 append(&quot;\n&quot;)을 사용했습니다. 그러나 이 부분 역시 StringJoiner로 해결할 수 있는 부분임을 알게 됐습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;또한, stream의 allMatch, anyMatch 등의 기능을 이용해도 for... if를 줄일 수 있다는 점을 알게 됐습니다.&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;b&gt;한 가지만 하기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기능 구현을 할 때는 리팩토링 요소가 보여도 참아야겠다는 생각을 했습니다. 자꾸 기능 구현하다가 리팩토링을 하니 한 번에 여러 개의 수정요소가 생겨서 기능별 커밋도 어려워졌습니다. 그리고 중간에 자꾸 코드가 변경되니 &lt;b&gt;내가 지금 뭘 하고 있었지&lt;/b&gt; 라는 생각이 들면서 길을 잃곤 했습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;그래서 다음 미션에서는 아무리 리팩토링 요소가 보여도 기능 구현 시에는 잠시 접어두고 기능 구현 완료 후에 리팩토링을 진행을 해봐야겠습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;다양한 입력값으로 테스트해보기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;다양한 예외 입력이 생각이 났는데 안 했다기보다는, 생각을 못한 부분이긴 합니다. 그래도 계속해서 예외 사항에 대해 생각해보고 터무니없는 값을 넣어보면서 최대한 요구사항에 맞게 구현해야겠다는 생각을 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;예를 들어, 수익률의 경우 저는 모든 총상금을 다 더한 뒤, 수익률을 계산했습니다. 아주 희박한 확률이지만 1등과 2등이 당첨되면 int범위를 벗어나기 때문에 overflow에러가 발생합니다. 그래서 저는 자료형을 int보다 큰 것으로 바꾸기보단 순위 별로 당첨이 될 때마다 해당 수익률을 누적시켜 가는 방식으로 다양한 상황에 대해 생각해봤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;만약 1등이 2번 당첨됐다고 가정을 하면 20억에 대한 수익률을 구하고, 다시 한번 20억에 대한 수익률을 구하는 방식으로 구현했습니다. 그러나 구매 금액이 int범위를 넘을 수도 있다는 생각은 제출기간이 지나고 나서야 생각났습니다.&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;b&gt;4주차 구현 코드&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/youngh0/java-bridge/tree/youngh0&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/youngh0/java-bridge/tree/youngh0&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>우아한테크코스5기</category>
      <category>Java</category>
      <category>백엔드</category>
      <category>우테코</category>
      <category>프리코스</category>
      <author>0h0</author>
      <guid isPermaLink="true">https://00h0.tistory.com/72</guid>
      <comments>https://00h0.tistory.com/72#entry72comment</comments>
      <pubDate>Wed, 16 Nov 2022 15:59:02 +0900</pubDate>
    </item>
    <item>
      <title>우아한테크코스 5기 프리코스 2주차 후기</title>
      <link>https://00h0.tistory.com/71</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;size16&quot;&gt;1주 차에는 코딩 테스트 형식의 문제들이 나왔고, 2주 차에는 프리코스를 검색해보면 항상 나오는 숫자 야구 게임이 나왔습니다. 1주 차 미션을 진행하면서 무엇을 신경 쓰면서 진행했는지 정리해보겠습니다.&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;데이터 중심이 아닌 책임에 중심을 둔 설계&lt;/li&gt;
&lt;li&gt;캡슐화&lt;/li&gt;
&lt;li&gt;객체에서 외부에 공개할 메서드와 내부에 감출 메서드&lt;/li&gt;
&lt;/ul&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;size16&quot;&gt;그동안 저는 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;&amp;nbsp;그래서 저는 구현해야 하는 기능 목록을 정리하고 해당 기능을 수행하기 위한 객체들이 무엇이 있는지 찾고 이를 더욱더 세분화하면서 설계 및 구현했습니다. 이 과정에서 계속해서 설계가 바뀌는 과정을 겪고 설계는 정말 정확한 정답이 있는 게 아니라는 것을 체감할 수 있었습니다.&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;size16&quot;&gt;캡슐화에 대한 설명은 너무 많이 나와있기 때문에 생략하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 숫자 야구 게임의 결과인 strike, ball의 개수를 저장하는 BaseballResult라는 객체를 생성했습니다. 그리고 해당 데이터를 가지고 할 수 있는 결과 출력, 게임 종료 조건 판단 기능을 해당 객체에 같이 정의했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;캡슐화를 지키니 다음과 같은 이점을 얻었습니다. 처음에 strike, ball을 각각 int타입으로 저장을 하고 있었지만, 이후, Map 자료구조로 변경을 했습니다. 하지만, 이 변경이 BaseballResult객체 외부에 영향을 미치지 않았습니다. 데이터와 해당 데이터를 처리하는 메서드를 객체가 자율적으로 진행하고 있기 때문입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;그래서 다음 미션을 진행할 때도 캡슐화에 더 신경 쓸 예정입니다. 다음 미션이라고 해봤자 오늘 나오긴 하지만,,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;객체 외부 공개 메서드&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캡슐화와 비슷한 맥락입니다. 객체가 외부에 구체적인 구현을 공개하지 않도록 하기 위해 노력한 부분입니다. 사용자가 정답을 입력하는 기능을 예로 들겠습니다. 입력에 대해서는 아래와 같은 조건이 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;1~9로 이루어져야 한다.&lt;/li&gt;
&lt;li&gt;중복되는 숫자는 올 수 없다.&lt;/li&gt;
&lt;li&gt;3자리로 이루어진다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 해당 유효성을 검사하는 PlayerInputValidator객체를 만들고 1, 2번에 해당하는 유효성 검사 메서드를 각각 만들었다고 가정합시다. 그렇다면 외부에서 입력이 유효한지 알기 위해 1, 2, 3번 메서드를 각각 호출해야 할까요? 아니면 객체 내부에서 validateInput() 메서드를 생성하고 1, 2, 3번 메서드를 호출해 최종적으로 유효한 입력값인지 알려주는 메서드를 만들어 이를 외부에 노출시키는 게 좋을까요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;저는 후자가 좋다고 생각합니다. 왜냐하면 나중에 유효성 조건이 추가될 경우 PlayerInputValidator내부만 수정한다면 외부는 변경으로 인한 영향을 받지 않습니다. 그러나 전자처럼 외부에서 각각의 메서드를 호출하는 방식으로 구현한다면 외부 코드도 수정하는 영향이 발생합니다. 아래 코드는 후자의 방식으로 구현해본 코드입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이러한 부분을 다음 미션에선 더욱 잘 지키면 보다 좋은 코드를 작성할 수 있지 않을까 기대하고 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1667924680787&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class PlayerInputValidator {
    private final static int PLAYER_ANSWER_CORRECT_SIZE = 3;
    private final static String PLAYER_ANSWER_REGEX = &quot;^[1-9]*$&quot;;

    public List&amp;lt;Integer&amp;gt; validatePlayerAnswerInput(String playerInput) {
        validateThreeLength(playerInput);
        validateSatisfiedRange(playerInput);
        List&amp;lt;Integer&amp;gt; playerAnswerNumbers = convertStringToIntegerList(playerInput);
        validateIsNonDuplicateNums(playerAnswerNumbers);
        return playerAnswerNumbers;
    }

    private List&amp;lt;Integer&amp;gt; convertStringToIntegerList(String nums) {
        List&amp;lt;Integer&amp;gt; integerList = new ArrayList&amp;lt;&amp;gt;();
        for (char num : nums.toCharArray()) {
            integerList.add(Integer.parseInt(String.valueOf(num)));
        }
        return integerList;
    }

    private static void validateThreeLength(String inputNumbers) {
        if (inputNumbers.length() != PLAYER_ANSWER_CORRECT_SIZE) {
            throw new IllegalArgumentException();
        }
    }

    private static void validateSatisfiedRange(String inputNumbers) {
        if (!inputNumbers.matches(PLAYER_ANSWER_REGEX)) {
            throw new IllegalArgumentException();
        }
    }

    private static void validateIsNonDuplicateNums(List&amp;lt;Integer&amp;gt; input) {
        long individualNumberCount = input.stream()
                .distinct()
                .count();
        if (individualNumberCount != PLAYER_ANSWER_CORRECT_SIZE) {
            throw new IllegalArgumentException();
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;private 메서드는 어떻게 테스트하지?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 코드를 작성하다 보니 private메서드에 대해 테스트를 하려는 순간 멈칫했습니다. 외부에서 해당 메서드를 접근할 수 없기 때문입니다. 그래서 구글링 한 결과 제가 선택한 방법은 해당 private메서드를 사용하는 public메서드를 이용해 테스트하는 것입니다. 그래서 저는 아래와 같이 public메서드를 이용해 테스트 코드를 작성했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1667925530182&quot; class=&quot;livescript&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class PlayerInputValidatorTest {
    PlayerInputValidator playerInputValidator = new PlayerInputValidator();

    @Test
    void 사용자_입력이_유효성_검사를_통과하면_리스트반환() {
        assertThat(playerInputValidator.validatePlayerAnswerInput(&quot;451&quot;))
                .isEqualTo(List.of(4, 5, 1));
    }

    @Test
    void 사용자가_입력한_정답이_세글자가_아니면_예외발생() {
        Assertions.assertThrows(IllegalArgumentException.class,
                () -&amp;gt; playerInputValidator.validatePlayerAnswerInput(&quot;1234&quot;));
    }

    @Test
    void 사용자가_입력한_정답에_문자가_있다면_예외발생() {
        Assertions.assertThrows(IllegalArgumentException.class,
                () -&amp;gt; playerInputValidator.validatePlayerAnswerInput(&quot;12&quot;));
    }

    @Test
    void 사용자가_입력한_정답에_중복숫자가_있으면_예외발생() {
        Assertions.assertThrows(IllegalArgumentException.class,
                () -&amp;gt; playerInputValidator.validatePlayerAnswerInput(&quot;313&quot;));
    }
}&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;size16&quot;&gt;사실 처음 제출을 하니 '예기치 못한 에러 발생'문구가 나와서 코드에서 잘못된 부분을 찾으려고 했지만 찾지 못하고 결국 객체들에 나눠놨던 메서드들을 하나의 파일에 모았습니다. 이후, 제출 성공을 확인한 뒤 다시 설계하고, 구현을 하면서 어찌어찌 제출은 성공했습니다. 그러나 이 과정에서 기존에 작성했던 테스트 코드들을 삭제하고 다시 꼼꼼하게 테스트 코드 작성을 못한 것이 아쉬운 점으로 남았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;위에서 언급은 안 했지만 함수 별로 테스트 코드를 작성하는 것도 이번 미션을 진행하면서 많은 신경을 쓴 부분 중 하나이기 때문입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;그리고 리팩토링을리팩터링을 할 때도 변경해야 하는 사항들을 정리한 뒤 진행해볼 생각입니다. 머릿속으로만 생각하고 바로 리팩토링을 하다 보니 중간에 꼬여서 다시 처음으로 돌리는 경험을 한 뒤, 종이에 변경할 구조를 그리고 진행하니 빠르게 리팩토링을 할 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;또한 테스트를 작성하다 보면 테스트 코드 작성이 어려웠던 메서드들이 있었습니다. 그래서 테스트하기 좋게 코드를 짜는 방법에 대해서도 알아본 뒤, 다음 미션에 적용하고 싶습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;다음 미션을 진행할 때는 이번 미션을 진행하면서 지키려고 한 요소뿐만 아니라 아쉬웠던 점을 보완하기 위해 노력해볼 예정입니다.&lt;/p&gt;</description>
      <category>우아한테크코스5기</category>
      <category>Java</category>
      <category>숫자 야구 게임</category>
      <category>우테코</category>
      <category>프리코스</category>
      <author>0h0</author>
      <guid isPermaLink="true">https://00h0.tistory.com/71</guid>
      <comments>https://00h0.tistory.com/71#entry71comment</comments>
      <pubDate>Wed, 9 Nov 2022 01:26:54 +0900</pubDate>
    </item>
    <item>
      <title>[JAVA] Array를 stream으로 (Arrays.stream() vs Stream.of())</title>
      <link>https://00h0.tistory.com/70</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;size16&quot;&gt;stream을 공부하면서 Array를 stream으로 만드는 방법이 2가지가 있는 것을 알았고, 2가지 방법의 차이점에 대해 궁금증이 생겨 알아본 내용입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Collection이 아닌 Array를 stream으로 만들 때 2가지 방법이 있습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Arrays.stream()&lt;/li&gt;
&lt;li&gt;Stream.of()&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 2가지 방법은 parameter로 primitive타입의 배열을 넘기느냐, non-promitive타입의 배열을 넘기느냐에 따라 반환 값이 달라집니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Stream.of()&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;primitive타입 배열로 Stream.of() 호출했을 때 실행되는 코드&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1666955552824&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public static&amp;lt;T&amp;gt; Stream&amp;lt;T&amp;gt; of(T t) {
    return StreamSupport.stream(new Streams.StreamBuilderImpl&amp;lt;&amp;gt;(t), false);
}

// int배열의 반환타입 : Stream&amp;lt;int[]&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;non-primitive타입 배열로 Stream.of() 호출했을 때 실행되는 코드&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1666955607644&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@SafeVarargs
@SuppressWarnings(&quot;varargs&quot;) // Creating a stream from an array is safe
public static&amp;lt;T&amp;gt; Stream&amp;lt;T&amp;gt; of(T... values) {
    return Arrays.stream(values);
}

// Integer배열의 반환타입 : Stream&amp;lt;Integer&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt; Stream.of()의 경우 primitive, non-primitive에 상관없이 모두 Stream객체가 반환됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Arrays.stream()&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;primitive타입 배열로 Arrays.stream() 호출했을 때 실행되는 코드&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1666955933176&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public static IntStream stream(int[] array) {
    return stream(array, 0, array.length);
}

// int배열의 반환타입 : IntStream&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;non-primitive타입 배열로 Arrays.stream() 호출했을 때 실행되는 코드&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1666956047762&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public static &amp;lt;T&amp;gt; Stream&amp;lt;T&amp;gt; stream(T[] array) {
    return stream(array, 0, array.length);
}

// Integer의 반환타입 : Stream&amp;lt;Integer&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Arrays.stream()의 경우 &lt;b&gt;primitive타입&lt;/b&gt;을 넘기면 해당 타입에 맞는 &lt;b&gt;기본형 stream이 반환&lt;/b&gt;됩니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;non-primitive&lt;/b&gt;의 경우 &lt;b&gt;Stream객체&lt;/b&gt;가 반환됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;결론&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt; stream.of()의 경우 primitive, non-primitive 상관없이 Stream객체를 반환하지만,&lt;/li&gt;
&lt;li&gt;Arrays.stream()의 경우 primitive는 그에 맞는 기본형 stream, non-primitive는 Stream객체가 반환됩니다.&lt;/li&gt;
&lt;li&gt;세부적인 이유까지 찾아보려 했지만, 본 포스팅 정도로만 찾아볼 수 있었습니다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;&lt;a title=&quot;Github Link&quot; href=&quot;https://github.com/eugenp/tutorials/tree/master/core-java-modules/core-java-arrays-convert&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Github Link&lt;/a&gt;에 자세한 구현 코드가 있으니 더 알아보고 싶으신 분들은 참고하시면 좋을 거 같습니다:)&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Language/JAVA</category>
      <category>Array to stream</category>
      <category>Java</category>
      <category>STREAM</category>
      <author>0h0</author>
      <guid isPermaLink="true">https://00h0.tistory.com/70</guid>
      <comments>https://00h0.tistory.com/70#entry70comment</comments>
      <pubDate>Fri, 28 Oct 2022 20:28:12 +0900</pubDate>
    </item>
    <item>
      <title>[백준] 2170 선 긋기(Python)</title>
      <link>https://00h0.tistory.com/69</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;size16&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/2170&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://www.acmicpc.net/problem/2170&lt;/a&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;size16&quot;&gt;가장 핵심 로직은 겹치는 줄 들을 어떻게 합치느냐입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예제 입력 중 (1,3), (2,5), (3,5)는 (1,5)로 합칠 수 있습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;합쳐지는 조건&lt;/b&gt;&lt;/h4&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;첫 번째 입력 좌표를 start, end변수로 설정합니다.&lt;/li&gt;
&lt;li&gt;두 번째 좌표부터 끝까지 탐색하며 합칠 수 있는지 살펴봅니다.
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;만약 i번 좌표의 시작점이 기존 좌표의 &lt;b&gt;start &amp;lt;= lines[i][0] &amp;lt;= end&lt;/b&gt;이면 &lt;b&gt;기존 줄과 합칠 수 있기 때문에&lt;/b&gt; end변수를 갱신하고, 합쳐진 줄들의 좌표를 저장하는 combine_lines의 가장 마지막 인덱스의 1번 값을 end변수와 동일하게 갱신합니다.
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이때, &lt;b&gt;무조건적으로 end좌표를 갱신하는 것이 아니라 기존 end와 i번 째 end 중 더 큰 값으로 갱신&lt;/b&gt;해야 합니다. ex) (1,6), (2,4)&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;i번 째 좌표의 시작점이 기존 start보다 크다면 i번 째 줄은 새로운 줄로 판단하고 기존 줄의 start, end좌표를 저장한 뒤 start, end를 갱신합니다.
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;저는 combine_lines라는 리스트에 합쳐진 줄들의 좌표를 저장했습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Code&lt;/b&gt;&lt;/h2&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;# 선 긋기
# https://www.acmicpc.net/problem/2170

import sys
input = sys.stdin.readline

n = int(input())

lines = []
for _ in range(n):
    lines.append(tuple(map(int,input().split())))

lines.sort()
ans = 0

combine_lines = []

# 2. 첫 번째 입력좌표를 start, end변수로 설정
start = lines[0][0]
end = lines[0][1]
combine_lines.append([start,end])

is_add = False

for i in range(1,n):
    n_start = lines[i][0]
    n_end = lines[i][1]
    
    # 기존 줄과 합칠 수 있는 경우
    if start &amp;lt;= n_start &amp;lt;= end:
        combine_lines[-1][1] = max(combine_lines[-1][1], n_end)
        end = max(combine_lines[-1][1], n_end)
    
    # 기존 줄과 합칠 수 없는 경우
    elif n_start &amp;gt; end:
        combine_lines.append([n_start, n_end])
        start = lines[i][0]
        end = lines[i][1]

for start,end in combine_lines:
    ans += abs(end - start)

print(ans)&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Git&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;a href=&quot;https://github.com/youngh0/Algorithm/blob/master/greedy/boj_2170.py&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/youngh0/Algorithm/blob/master/greedy/boj_2170.py&lt;/a&gt;&lt;/b&gt;&lt;/p&gt;</description>
      <category>Algorithm/Greedy</category>
      <category>greedy</category>
      <category>Python</category>
      <category>백준</category>
      <author>0h0</author>
      <guid isPermaLink="true">https://00h0.tistory.com/69</guid>
      <comments>https://00h0.tistory.com/69#entry69comment</comments>
      <pubDate>Tue, 25 Oct 2022 20:38:30 +0900</pubDate>
    </item>
    <item>
      <title>[백준] 퇴사 14501 (Java)</title>
      <link>https://00h0.tistory.com/67</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;size16&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/14501&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://www.acmicpc.net/problem/14501&lt;/a&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;나의 풀이&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;주의점&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;n + 1일에 퇴사를 하기 때문에 상담이 n+1 일에 끝나는 경우까지 고려해야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;처음 접근&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;1일부터 n일까지 해당 날짜의 상담을 진행하는 경우 안하는 경우를 고려해서 점화식을 세우려고 시도해봤습니다.&lt;/li&gt;
&lt;li&gt;만약, 1일차 상담에 이틀이 필요할 때 3일차 상담을 진행하는 경우의 점화식을 어떻게 세울지 고민하다 결국 해결하지 못하고 구글링을 했습니다.&lt;/li&gt;
&lt;/ul&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;size16&quot;&gt;1일 부터 시작하는 것이 마지막 n일부터 시작해서 dp테이블을 갱신합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;n=7&lt;/li&gt;
&lt;li&gt;7일차 상담은 이틀이 필요하기 때문에 7 + 2 &amp;gt; n+1 이므로, 상담을 진행할 수 없습니다.&lt;/li&gt;
&lt;li&gt;2일차 상담의 경우 5일이 필요하기 때문에 max(20 + dp[5], dp[i+1])이 됩니다.&amp;nbsp;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;첫 번째 인자가 2일차 상담을 진행하는 경우&lt;/li&gt;
&lt;li&gt;두 번째 인자가 2일차 상담을 진행하지 않는 경우입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 36px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 14.2857%;&quot;&gt;1일차&lt;/td&gt;
&lt;td style=&quot;width: 14.2857%;&quot;&gt;2일차&lt;/td&gt;
&lt;td style=&quot;width: 14.2857%;&quot;&gt;3일차&lt;/td&gt;
&lt;td style=&quot;width: 14.2857%;&quot;&gt;4일차&lt;/td&gt;
&lt;td style=&quot;width: 14.2857%;&quot;&gt;5일차&lt;/td&gt;
&lt;td style=&quot;width: 14.2857%;&quot;&gt;6일차&lt;/td&gt;
&lt;td style=&quot;width: 14.2857%;&quot;&gt;7일차&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 14.2857%; height: 18px;&quot;&gt;3&lt;/td&gt;
&lt;td style=&quot;width: 14.2857%; height: 18px;&quot;&gt;5&lt;/td&gt;
&lt;td style=&quot;width: 14.2857%; height: 18px;&quot;&gt;1&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 14.2857%; height: 18px;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;width: 14.2857%; height: 18px;&quot;&gt;2&lt;/td&gt;
&lt;td style=&quot;width: 14.2857%; height: 18px;&quot;&gt;4&lt;/td&gt;
&lt;td style=&quot;width: 14.2857%; height: 18px;&quot;&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 14.2857%; height: 18px;&quot;&gt;10&lt;/td&gt;
&lt;td style=&quot;width: 14.2857%; height: 18px;&quot;&gt;20&lt;/td&gt;
&lt;td style=&quot;width: 14.2857%; height: 18px;&quot;&gt;10&lt;/td&gt;
&lt;td style=&quot;width: 14.2857%; height: 18px;&quot;&gt;20&lt;/td&gt;
&lt;td style=&quot;width: 14.2857%; height: 18px;&quot;&gt;15&lt;/td&gt;
&lt;td style=&quot;width: 14.2857%; height: 18px;&quot;&gt;40&lt;/td&gt;
&lt;td style=&quot;width: 14.2857%; height: 18px;&quot;&gt;200&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Code&lt;/b&gt;&lt;/h2&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;package dp.boj_14501;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.StringTokenizer;


public class Main {
    static int[] dp;
    static int[] costs;
    static int[] pay;
    static StringTokenizer st;

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

        int n = Integer.parseInt(br.readLine());

        dp = new int[n + 2];
        costs = new int[n + 1];
        pay = new int[n + 1];


        for (int i = 1; i &amp;lt; n+1; i++) {
            st = new StringTokenizer(br.readLine());
            int cost = Integer.parseInt(st.nextToken());
            int p = Integer.parseInt(st.nextToken());

            costs[i] = cost;
            pay[i] = p;
        }


        for (int i = n; i &amp;gt;= 0; i--) {
            if (costs[i] + i &amp;gt; n+1) {
                //상담 진행 불가
                dp[i] = dp[i + 1];
            }
            else{
                dp[i] = Math.max(pay[i] + dp[i + costs[i]], dp[i + 1]);
            }
        }
        System.out.println(dp[0]);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Git&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;a href=&quot;https://github.com/youngh0/java-algorithm/blob/master/algorithm/src/main/java/dp/boj_14501/Main.java&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/youngh0/java-algorithm/blob/master/algorithm/src/main/java/dp/boj_14501/Main.java&lt;/a&gt;&lt;/b&gt;&lt;/p&gt;</description>
      <category>Algorithm/DP</category>
      <category>DP</category>
      <category>Java</category>
      <category>백준</category>
      <author>0h0</author>
      <guid isPermaLink="true">https://00h0.tistory.com/67</guid>
      <comments>https://00h0.tistory.com/67#entry67comment</comments>
      <pubDate>Thu, 13 Oct 2022 13:30:56 +0900</pubDate>
    </item>
    <item>
      <title>[백준] 불 5427 (Java)</title>
      <link>https://00h0.tistory.com/66</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;size16&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/5427&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://www.acmicpc.net/problem/5427&lt;/a&gt;&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;기본적으로 최소시간을 구하는 문제라 BFS를 사용했습니다.&lt;/li&gt;
&lt;li&gt;&lt;span&gt;그리고 문제를&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;'&lt;span style=&quot;background-color: #ffffff; color: #555555;&quot;&gt;불이 옮겨진 칸 또는 이제 불이 붙으려는 칸으로 이동할 수 없다&lt;/span&gt;'&lt;/b&gt;&lt;span&gt;&amp;nbsp;라는 문장이 있습니다. 즉, 불이 먼저 동서남북으로 한 칸씩 번지게 한 다음에 상근이가 동서남북으로 움직일 수 있는지 구별해야 합니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span&gt;구현 기능&lt;/span&gt;&lt;/b&gt;&lt;/h4&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;/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;역시 벽으로는 이동할 수 없고, 불이 붙으려는 칸으로 이동할 수 없다. -&amp;gt; 불을 먼저 번지게 함으로써 구별&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;상근이가 x,y 좌표 중 하나라도 map의 가장자리에 도착하게 되면 탈출가능한 경우이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;막혔던 점&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;sBfs()함수의 while문 바로 다음에 나오는 for문의 조건에 pSize가 아닌 person.size()를 바로 넣게 되면 해당 person.size가 계속 동적으로 변하는 걸 간과하고 코딩을 해서 25%에서 계속 틀렸습니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;person은 상근이의 위치정보를 저장하는 Queue입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Code&lt;/b&gt;&lt;/h2&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;package bfs.boj_5427;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.*;

public class Main {
    static char[][] maps;

    static StringTokenizer st;
    static BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
    static int[] dx = new int[]{-1, 1, 0, 0};
    static int[] dy = new int[]{0, 0, 1, -1};
    static Queue&amp;lt;int[]&amp;gt; fireLocation;
    static Queue&amp;lt;Point&amp;gt; person;

    static int row;
    static int col;

    public static void main(String[] args) throws IOException {

        int testcases = Integer.parseInt(br.readLine());

        for (int i = 0; i &amp;lt; testcases; i++) {
            inputData();
        }
    }

    static void inputData() throws IOException {
        st = new StringTokenizer(br.readLine());

        col = Integer.parseInt(st.nextToken());
        row = Integer.parseInt(st.nextToken());

        maps = new char[row][col];


        fireLocation = new LinkedList&amp;lt;&amp;gt;();
        person = new LinkedList&amp;lt;&amp;gt;();

        for (int i = 0; i &amp;lt; row; i++) {
            maps[i] = br.readLine().toCharArray();
            for (int j = 0; j &amp;lt; col; j++) {
                if (maps[i][j] == '*') {

                    fireLocation.offer(new int[]{i,j});


                }
                if (maps[i][j] == '@') {
                    person.offer(new Point(i, j, 0));
                }
            }
        }

        int res = sBfs();
        if(res &amp;gt; 0){
            System.out.println(res);
        }
        else{
            System.out.println(&quot;IMPOSSIBLE&quot;);
        }
    }

    static void fireBfs() {
        int fires = fireLocation.size();

        for (int j = 0; j &amp;lt; fires; j++) {
            int[] poll = fireLocation.poll();
            int x = poll[0];
            int y = poll[1];


            for (int i = 0; i &amp;lt; 4; i++) {
                int nx = x + dx[i];
                int ny = y + dy[i];

                if (0 &amp;lt;= nx &amp;amp;&amp;amp; nx &amp;lt; row &amp;amp;&amp;amp; 0 &amp;lt;= ny &amp;amp;&amp;amp; ny &amp;lt; col) {
                    if (maps[nx][ny] == '.' || maps[nx][ny] == '@') {
                        fireLocation.offer(new int[]{nx,ny});
                        maps[nx][ny] = '*';
                    }
                }
            }
        }
    }

    static int sBfs() {
        while (!person.isEmpty()) {
            fireBfs();

//            person.stream().forEach(x -&amp;gt; System.out.print(x.toString()));
            int pSize = person.size();
            for (int k = 0; k &amp;lt; pSize; k++) {
                Point poll = person.poll();
                int x = poll.x;
                int y = poll.y;

                int dis = poll.dis;
                if (x == 0 || x == row - 1 || y == 0 || y == col - 1) {
                    return dis+1;
                }

                for (int i = 0; i &amp;lt; 4; i++) {
                    int nx = x + dx[i];
                    int ny = y + dy[i];

                    if (0 &amp;lt;= nx &amp;amp;&amp;amp; nx &amp;lt; row &amp;amp;&amp;amp; 0 &amp;lt;= ny &amp;amp;&amp;amp; ny &amp;lt; col) {
                        if (maps[nx][ny] == '.') {
                            maps[nx][ny] = '@';
                            person.offer(new Point(nx,ny,dis+1));

                        }
                    }
                }
            }
        }
        return 0;

    }

    static class Point{
        int x;
        int y;
        int dis;

        public Point(int x, int y, int dis) {
            this.x = x;
            this.y = y;
            this.dis = dis;
        }

        @Override
        public String toString() {
            return &quot;Point{&quot; +
                    &quot;x=&quot; + x +
                    &quot;, y=&quot; + y +
                    &quot;, dis=&quot; + dis +
                    '}';
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Git&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;a href=&quot;https://github.com/youngh0/java-algorithm/blob/master/algorithm/src/main/java/bfs/boj_5427/Main.java&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/youngh0/java-algorithm/blob/master/algorithm/src/main/java/bfs/boj_5427/Main.java&lt;/a&gt;&lt;/b&gt;&lt;/p&gt;</description>
      <category>Algorithm/BFS</category>
      <category>BFS</category>
      <category>Java</category>
      <category>백준</category>
      <author>0h0</author>
      <guid isPermaLink="true">https://00h0.tistory.com/66</guid>
      <comments>https://00h0.tistory.com/66#entry66comment</comments>
      <pubDate>Wed, 12 Oct 2022 17:17:50 +0900</pubDate>
    </item>
    <item>
      <title>[백준] 제곱수의 합 1699(Java)</title>
      <link>https://00h0.tistory.com/65</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;size16&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/1941&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://www.acmicpc.net/problem/1941&lt;/a&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;나의 풀이&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;잘못된 접근&lt;/b&gt;&lt;/h3&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;size16&quot;&gt;&lt;b&gt;$$ dp[i] = dp[i - (가장 큰 제곱근 ^ 2)] + 1$$&amp;nbsp;&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;size16&quot;&gt;단순하게 가장 큰 제곱근의 제곱을 빼서 dp 테이블을 갱신하는 것이 최솟값이라고 생각을 했습니다.&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; 예를 들어 i = 26의 경우&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 큰 제곱근인 5의 제곱 25를 26에서 뺀 뒤 나온 1. 즉, dp[26-25] + 1 이 무조건 최솟값이라고 생각했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$$ 26 = 1^2 + 5^2$$&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;size16&quot;&gt;점화식의 경우 위에서 세운 점화식이 아예 틀린 것은 아니지만 가장 큰 제곱근만 생각하는 것이 아닌 1 ~ 가장 큰 제곱근의 경우를 모두 생각해야 합니다. 12를 시도해보면 좋을 거 같습니다.&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;$$ dp[i] = min(dp[i - 1^2 부터&amp;nbsp; (가장 큰 제곱근^2)] + 1, dp[i])$$&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 점화식을 바탕으로 i=1부터 n까지 갱신을 하면 됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Code&lt;/b&gt;&lt;/h2&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Arrays;

public class Main {
    static int[] dp;

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

        int n = Integer.parseInt(br.readLine());
        dp = new int[n + 1];

        for (int i = 1; i &amp;lt; n + 1; i++) {
            dp[i] = i;
        }

        for (int i = 1; i &amp;lt; n + 1; i++) {
            for (int j = 1; j * j &amp;lt;= i; j++) {
                dp[i] = Math.min(dp[i], dp[i - (j * j)] + 1);
            }
        }
        System.out.println(dp[n]);

    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Git&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;a href=&quot;https://github.com/youngh0/java-algorithm/blob/master/algorithm/src/main/java/dp/boj_1699/Main.java&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/youngh0/java-algorithm/blob/master/algorithm/src/main/java/dp/boj_1699/Main.java&lt;/a&gt;&lt;/b&gt;&lt;/p&gt;</description>
      <category>Algorithm/DP</category>
      <category>DP</category>
      <category>Java</category>
      <category>백준</category>
      <author>0h0</author>
      <guid isPermaLink="true">https://00h0.tistory.com/65</guid>
      <comments>https://00h0.tistory.com/65#entry65comment</comments>
      <pubDate>Sat, 8 Oct 2022 16:59:58 +0900</pubDate>
    </item>
    <item>
      <title>[JAVA] instance method vs static method</title>
      <link>https://00h0.tistory.com/64</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;instance method란&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; class의 instance를 생성하고, 해당 instance를 통해 호출할 수 있는 method입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;static method란&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;컴파일 시점에 메모리에 올라갑니다.&lt;/li&gt;
&lt;li&gt;instance 생성없이 클래스를 통해 호출할 수 있는 method입니다.&lt;/li&gt;
&lt;li&gt;instance변수를 사용할 수 없습니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;instance변수란 말 그대로 동적으로 생성된 instance의 변수이기 때문에 컴파일 시점에 존재하지 않습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;static method주의점&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;GC(Garbage Collector)가 메모리를 해제하지 않기 때문에, 너무 많이 사용하면 메모리 측면에서 문제가 생길 수 있습니다.&lt;/li&gt;
&lt;li style=&quot;list-style-type: none;&quot;&gt;&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Language/JAVA</category>
      <category>Java</category>
      <category>static method</category>
      <author>0h0</author>
      <guid isPermaLink="true">https://00h0.tistory.com/64</guid>
      <comments>https://00h0.tistory.com/64#entry64comment</comments>
      <pubDate>Tue, 4 Oct 2022 16:56:28 +0900</pubDate>
    </item>
    <item>
      <title>[백준] 스티커 9465 (python)</title>
      <link>https://00h0.tistory.com/63</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;size16&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/9465&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://www.acmicpc.net/problem/9465&lt;/a&gt;&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;우선, 3xn 크기의 dp테이블과 2xn 크기의 stickers배열을 생성합니다.&lt;/li&gt;
&lt;li&gt;dp[0][n] stickers[0][n]의 스티커를 뜯었을 때의 최댓값을 저장하고, 두 번째 행도 이와 마찬가지입니다.&lt;/li&gt;
&lt;li&gt;dp[2][n]은 stickers[0][n], stickers[1][n] 모두 뜯지 않았을 때의 최댓값을 저장합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;스티커를 뜯는 방식으로 3가지를 고려할 수 있습니다.&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt; 0행의 n열 스티커를 뜯는 경우 -&amp;gt; stickers[0][n]을 뜯는 경우&amp;nbsp;&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;stickers[0][n]을 뜯은 경우 이와 인접한 stickers[0][n-1] 스티커는 뜯을 수 없기 때문에, stickers[0][n-1] 스티커를 뜯지 않은 경우인 dp[1][n-1], dp[2][n-1] 값 중 최댓값과 stickers[0][n]의 값을 더한 결과가 최댓값이 됩니다.&lt;/li&gt;
&lt;li&gt;이를 점화식으로 표현하면 아래와 같이 나오게 됩니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;dp[0][n] = max(dp[1][n-1], dp[2][n-1]) + stickers[0][n]&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;1행의 &lt;span&gt;n열&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;스티커를 뜯는 경우&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위 1번의 경우와 마찬가지로 stickers[1][n]을 뜯게 되면 stickers[1][n-1]은 뜯을 수 없기 때문에 아래와 같이 점화식이 나오게 됩니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;dp[1][n] = max(dp[0][n-1], dp[2][n-1]) + stickers[0][n]&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;n열의&lt;/span&gt;&amp;nbsp;스티커를 모두 뜯지 않는 경우&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 경우는 stickers[0][n-1], stickers[1][n-1]모두 뜯을 수 있기 때문에, dp[2][n]은 dp[0][n-1], dp[1][n-1]중 최댓값이 됩니다. &lt;b&gt;이때 n열의 스티커는 뜯지 않는 것에 주의해야 합니다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;dp[2][n] = max(dp[0][n-1], dp[1][n-1])&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Code&lt;/b&gt;&lt;/h2&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;# 스티커
# https://www.acmicpc.net/problem/9465

import sys
input = sys.stdin.readline

test_cases = int(input())

for _ in range(test_cases):
    stickers_num = int(input())
    stickers = [list(map(int, input().split())) for _ in range(2)]

    dp = [[0] * stickers_num for _ in range(3)]
    dp[0][0] = stickers[0][0]
    dp[1][0] = stickers[1][0]

    for n in range(1,stickers_num):
        # 0행의 i번 째 스티커 뜯은 경우
        dp[0][n] = max(dp[1][n-1], dp[2][n-1]) + stickers[0][n]

        # 1행의 i번 째 스티커 뜯은 경우
        dp[1][n] = max(dp[0][n - 1], dp[2][n - 1]) + stickers[1][n]

        # i번 째 열의 스티커를 뜯지 않은 경우
        dp[2][n] = max(dp[0][n-1], dp[1][n-1])

    answer = max(dp[0][-1], dp[1][-1])
    answer = max(answer, dp[2][-1])
    print(answer)&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Git&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;a href=&quot;https://github.com/youngh0/Algorithm/blob/master/dp/boj_9465.py&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/youngh0/Algorithm/blob/master/dp/boj_9465.py&lt;/a&gt;&lt;/b&gt;&lt;/p&gt;</description>
      <category>Algorithm/DP</category>
      <category>DP</category>
      <category>Python</category>
      <category>백준</category>
      <category>알고리즘</category>
      <author>0h0</author>
      <guid isPermaLink="true">https://00h0.tistory.com/63</guid>
      <comments>https://00h0.tistory.com/63#entry63comment</comments>
      <pubDate>Sun, 18 Sep 2022 17:30:15 +0900</pubDate>
    </item>
    <item>
      <title>[Network] Transport layer(2) - TCP reliable data transfer</title>
      <link>https://00h0.tistory.com/62</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;TCP란&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;Transport layer의 프로토콜 중 하나로 연결 지향적인 프로토콜입니다.&lt;/li&gt;
&lt;li&gt;패킷 손실 복구, 패킷 순서 보장, 흐름 제어, 혼잡 제어 등의 기능을 제공합니다.&lt;/li&gt;
&lt;li&gt;두 개의 host 간 connection이 설정되면 &lt;b&gt;양방향 소통이 가능&lt;/b&gt;합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;handshaking&lt;/b&gt; 과정을 통해 연결 초기 설정을 합니다.&lt;/li&gt;
&lt;li&gt;receiver의 상태에 따라 패킷 전송속도를 조절합니다.&lt;/li&gt;
&lt;li&gt;TCP헤더는 기본 20byte이고, 옵션 추가에 따라 60byte까지 늘어날 수 있습니다.&lt;/li&gt;
&lt;li&gt;헤더의 &lt;b&gt;Sequence number, Acknowledgement number&lt;/b&gt;필드를 통해 &lt;b&gt;reliable data transfer&lt;/b&gt;가 가능합니다.&lt;/li&gt;
&lt;li&gt;헤더의 &lt;b&gt;Window size&lt;/b&gt;필드를 통해 receiver의 상태를 파악해 &lt;b&gt;데이터 전송 속도를 조절&lt;/b&gt;합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;825&quot; data-origin-height=&quot;491&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b270yU/btrHUMLwUOw/ZzNEnvsnNJEk1qxK4lHOHk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b270yU/btrHUMLwUOw/ZzNEnvsnNJEk1qxK4lHOHk/img.png&quot; data-alt=&quot;TCP header&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b270yU/btrHUMLwUOw/ZzNEnvsnNJEk1qxK4lHOHk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb270yU%2FbtrHUMLwUOw%2FZzNEnvsnNJEk1qxK4lHOHk%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; alt=&quot;tcp-header&quot; loading=&quot;lazy&quot; width=&quot;539&quot; height=&quot;321&quot; data-origin-width=&quot;825&quot; data-origin-height=&quot;491&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;TCP header&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;TCP헤더(Sequence number)&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;전송할 segment의 데이터 필드의 시작 byte에 부여된 &lt;b&gt;바이트 단위의 순서 번호를&lt;/b&gt; 의미합니다.&lt;/li&gt;
&lt;li&gt;sequence number는 무조건 0부터 시작하는 것이 아닌 난수 발생기를 통해 결정합니다.&lt;/li&gt;
&lt;li&gt;Receiver 측에서 sequence number를 보고 데이터를 조립합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;TCP헤더(Acknowledgement number)&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;전송받기를 원하는 다음 byte의 sequence number&lt;/b&gt;를 의미합니다.&lt;/li&gt;
&lt;li&gt;sequence number 10까지 받았다면 Acknowledge number는 11이 되고, 이것은 sequence number 10 까지는 받았다는 것을 의미 함으로, cumulative Ack 기능을 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;TCP 동작 과정&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Chapter_3_V7.01-61.jpg&quot; data-origin-width=&quot;2640&quot; data-origin-height=&quot;1980&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bOC3ZJ/btrHROjT8c3/RaphrqtLmj19Dro8OSQdo0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bOC3ZJ/btrHROjT8c3/RaphrqtLmj19Dro8OSQdo0/img.jpg&quot; data-alt=&quot;TCP action&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bOC3ZJ/btrHROjT8c3/RaphrqtLmj19Dro8OSQdo0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbOC3ZJ%2FbtrHROjT8c3%2FRaphrqtLmj19Dro8OSQdo0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;TCP-action&quot; loading=&quot;lazy&quot; width=&quot;551&quot; height=&quot;413&quot; data-filename=&quot;Chapter_3_V7.01-61.jpg&quot; data-origin-width=&quot;2640&quot; data-origin-height=&quot;1980&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;TCP action&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위 사진은 telenet을 사용하는 시나리오입니다. telnet은 한 글자를 타이핑할 때마다 전송합니다..&lt;/li&gt;
&lt;li&gt;host A가 host B에게 seq=42, data = &quot;C&quot;로 헤더를 채워서 전송을 합니다.&lt;/li&gt;
&lt;li&gt;ACK=79는 이전에 host B가 seq=78을 보냈다고 유추할 수 있습니다.&lt;/li&gt;
&lt;li&gt;data=&quot;C&quot;에 해당하는 sequence number는 42를 의미합니다.&lt;/li&gt;
&lt;li&gt;host B는 Ack를 seq=42까지 받았다는 의미로 ACK=43으로 채우고, 자신이 보낼 데이터와 해당 데이터의 sequence number를 채워서 host A에게 전송합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;TCP Reliable data transfer&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TCP는 기본적으로&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a title=&quot;이전 포스팅&quot; href=&quot;https://00h0.tistory.com/61&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전 포스팅&lt;/a&gt;에서 살펴본 pipelined segments&lt;/li&gt;
&lt;li&gt;cumulative ack&lt;/li&gt;
&lt;li&gt;하나의 timer&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 3가지를 사용해 reliable 한 데이터 전송을 지원합니다. cumulative ack, pipiline을 사용하기 때문에 Go-Back-N을 사용한다고 생각할 수 있지만, TCP에서는 timeout이 발생하면 &lt;b&gt;해당 패킷 하나만 재전송&lt;/b&gt;하는 점에서 Go-Back-N과 차이점이 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;재전송 trigger&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;timeout&lt;/li&gt;
&lt;li&gt;duplicate acks&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;TCP Sender&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Chapter_3_V7.01-68 2.jpg&quot; data-origin-width=&quot;2640&quot; data-origin-height=&quot;1980&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oKREC/btrHWFmvLUE/txXtr0vC75nKKkAc1Cowck/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oKREC/btrHWFmvLUE/txXtr0vC75nKKkAc1Cowck/img.jpg&quot; data-alt=&quot;TCP sender&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oKREC/btrHWFmvLUE/txXtr0vC75nKKkAc1Cowck/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoKREC%2FbtrHWFmvLUE%2FtxXtr0vC75nKKkAc1Cowck%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;TCP-sender&quot; loading=&quot;lazy&quot; width=&quot;505&quot; height=&quot;379&quot; data-filename=&quot;Chapter_3_V7.01-68 2.jpg&quot; data-origin-width=&quot;2640&quot; data-origin-height=&quot;1980&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;TCP sender&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 사진은 TCP의 sender입장에서 일어나는 일들을 간단하게 FSM으로 나타낸 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 가장 &lt;b&gt;초기 작업인 1번&lt;/b&gt;을 보면 NextSeqNum, SendBase를 InitialSeqNum으로 설정해줍니다. 이때 InitialSeqNum은 무조건 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;b&gt;Application layer로부터 데이터를 받은 상황인 2번&lt;/b&gt;을 보게 되면 Application layer로부터 받은 데이터로 세그먼트를 만들고 Network layer로 전송해줍니다. 이후 NextSeqNum을 보낸 데이터의 길이만큼 늘려줍니다. 이때 동작하고 있는 timer가 없다면 timer를 동작시킵니다.&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;b&gt;3번은 timeout이 발생한 상황&lt;/b&gt;입니다. 이 때는 아직 Ack를 받지 않은 세그먼트들 중 &lt;b&gt;seq num이 가장 작은 세그먼트에 대해서만 재전송&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;size16&quot;&gt;&lt;b&gt;4번은 Ack가 온 상황&lt;/b&gt;입니다. TCP는 &lt;b&gt;cumulative Ack를 사용&lt;/b&gt;하기 때문에 받은 Ack 번호 이전 세그먼트들 중 아직 Ack를 못 받은 세그먼트들도 모두 Ack를 받았다고 표시해주고 sendbase를 옮깁니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;TCP 재전송 시나리오(timeout)&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Chapter_3_V7.01-69.jpg&quot; data-origin-width=&quot;2640&quot; data-origin-height=&quot;1980&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bIT620/btrHY5EOlD7/xzk5VEKJAI8iKd7DHpqbK0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bIT620/btrHY5EOlD7/xzk5VEKJAI8iKd7DHpqbK0/img.jpg&quot; data-alt=&quot;TCP timeout 시나리오&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bIT620/btrHY5EOlD7/xzk5VEKJAI8iKd7DHpqbK0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbIT620%2FbtrHY5EOlD7%2Fxzk5VEKJAI8iKd7DHpqbK0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;tcp-timeout&quot; loading=&quot;lazy&quot; width=&quot;531&quot; height=&quot;398&quot; data-filename=&quot;Chapter_3_V7.01-69.jpg&quot; data-origin-width=&quot;2640&quot; data-origin-height=&quot;1980&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;TCP timeout 시나리오&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 사진은 timeout이 발생할 경우 TCP가 어떤 식으로 재전송해서 데이터의 손실 방지, 순서를 보장해주는지 보여주는 사진입니다.&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;b&gt;1번&lt;/b&gt;의 경우&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Host A는 Seq=92, 8byte의 데이터(92~99)를 보냅니다.&lt;/li&gt;
&lt;li&gt;Host B에 정상적으로 도착을 했기 때문에 이제 Host B는 seq 100의 세그먼트를 받기를 원하기 때문에 Ack=100으로 설정한 뒤 Host A에 보냅니다.&lt;/li&gt;
&lt;li&gt;이때 Host B가 Host A에게 보낼 세그먼트가 있다면 추가해서 보내게 됩니다.&lt;/li&gt;
&lt;li&gt;이 과정에서 &lt;b&gt;Ack가 손실&lt;/b&gt;이 됐기 때문에 &lt;b&gt;Host A에선 timeout이 발생&lt;/b&gt;합니다.&lt;/li&gt;
&lt;li&gt;그래서 아직 Ack를 받지 않은 세그먼트 중 seq num이 가장 작은 &lt;b&gt;seq=92를 재전송&lt;/b&gt;합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2번&lt;/b&gt;의 경우 1번과 비슷합니다. 다만 Ack가 손실이 나진 않았지만 네트워크 상황에 의해 &lt;b&gt;delay가 발생&lt;/b&gt;해 Host A에 늦게 도착했기 때문에 도착 이전에 timeout이 발생합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Host A입장에선 {seq=92, 8byte 데이터(92~99)}, {seq = 100, 20byte 데이터(100~119)} 보냈고,&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Host B&lt;/b&gt;는 이를 모두 정상적으로 받았기 때문에 이제 &lt;b&gt;seq=120 세그먼트를 받길 희망&lt;/b&gt;합니다.&lt;/li&gt;
&lt;li&gt;그러나 이 과정에서 Host B의 Ack가 delay에 의해 Host A에 늦게 도착했고, &lt;b&gt;timeout이 발생&lt;/b&gt;했기 때문에 &lt;b&gt;Host A는 seq=92, 8byte의 데이터를 재전송&lt;/b&gt;합니다.&lt;/li&gt;
&lt;li&gt;이에 Host B는 본인이 원하는 seq=120이 아니기 때문에, &lt;b&gt;Host A에게 다시 Ack=120을 보냅니다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;이를 받은 Sender는 cumulative Ack에 의해 119 세그먼트 까지는 정상적으로 도착했다는 것을 알게 됐고, sendbase를 120으로 수정하고 이후, 이를 토대로 세그먼트를 전송하게 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;TCP 재전송 시나리오(duplicate Ack)&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 사진에는 없지만 재전송 trigger부분에서 언급한 &lt;b&gt;duplicate Ack&lt;/b&gt;는 만약 sender가 동일한 Ack를 3개 받게 되면 timeout과 동일하게 아직 Ack를 못 받은 세그먼트 중 seq num이 가장 작은 세그먼트를 재전송하게 됩니다. 이를 &lt;b&gt;'triple duplicate Ack'&lt;/b&gt;라고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 사진이 'triple duplicate Ack'에 관한 내용입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Chapter_3_V7.01-73.jpg&quot; data-origin-width=&quot;2640&quot; data-origin-height=&quot;1980&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bVVokh/btrHVg8Tfnj/8vJ9ffzzUACjOxz6kyKa8K/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bVVokh/btrHVg8Tfnj/8vJ9ffzzUACjOxz6kyKa8K/img.jpg&quot; data-alt=&quot;duplicate ack&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bVVokh/btrHVg8Tfnj/8vJ9ffzzUACjOxz6kyKa8K/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbVVokh%2FbtrHVg8Tfnj%2F8vJ9ffzzUACjOxz6kyKa8K%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;duplicate-ack&quot; loading=&quot;lazy&quot; width=&quot;463&quot; height=&quot;347&quot; data-filename=&quot;Chapter_3_V7.01-73.jpg&quot; data-origin-width=&quot;2640&quot; data-origin-height=&quot;1980&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;duplicate ack&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 사진을 보면 Host A는 아직 timeout이 발생하지 않았지만 동일한 Ack를 3개 받자 재전송하는 것을 볼 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Ack=100을 4개 받고 보냈다고 생각할 수 있지만 &lt;b&gt;처음 온 Ack=100은&lt;/b&gt; &lt;b&gt;Seq=92, 8byte 데이터에 대한 Ack입니다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;이후로 오는 Ack=100들은 Seq=100의 손실로 인해 오는 duplicate Ack&lt;/b&gt;이기 때문에, &lt;b&gt;3개의 동일한 Ack를 받고 재전송&lt;/b&gt;하는 것입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;다음으로&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 방식을 이용해 TCP는 reliable data transfer기능을 제공해줍니다. 다음에는 TCP는 연결 지향형 프로토콜이기 때문에 Handshaking에 대해 알아보겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;출처&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.gatevidyalay.com/transmission-control-protocol-tcp-header/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://www.gatevidyalay.com/transmission-control-protocol-tcp-header/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;http://www.ktword.co.kr/test/view/view.php?m_temp1=1889&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;http://www.ktword.co.kr/test/view/view.php?m_temp1=1889&lt;/a&gt;&lt;/p&gt;</description>
      <category>Network</category>
      <category>reliable data transfer</category>
      <category>tcp</category>
      <category>transport layer</category>
      <author>0h0</author>
      <guid isPermaLink="true">https://00h0.tistory.com/62</guid>
      <comments>https://00h0.tistory.com/62#entry62comment</comments>
      <pubDate>Fri, 22 Jul 2022 14:52:52 +0900</pubDate>
    </item>
  </channel>
</rss>