<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>DevLog  </title>
    <link>https://cl8d.tistory.com/</link>
    <description>기록하면 내 머리에도 남지 않을까</description>
    <language>ko</language>
    <pubDate>Mon, 29 Jun 2026 19:20:21 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>dolmeng2</managingEditor>
    <image>
      <title>DevLog  </title>
      <url>https://tistory1.daumcdn.net/tistory/5489871/attach/a126b80f45004349bca818ab038505cb</url>
      <link>https://cl8d.tistory.com</link>
    </image>
    <item>
      <title>[Spring] 프로퍼티 주입 시 동적으로 핸들링하기</title>
      <link>https://cl8d.tistory.com/132</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&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;블로그를 자주 써야겠다고 생각했는데 또 마지막 글을 쓴 지 3개월이 지나버렸다. 그래서, 사내에서 개발하던 중에 발생했던 이슈가 생각나서 간단하게 적어보고자 한다. 사실 해결 방법은 간단했던 문제였는데, 어떤 식으로 문제를 해결해나갔는지 적어두면 좋을 것 같아서 작성해보고자 한다.&lt;br&gt;&lt;br&gt;현재 사내 시스템에서는 &lt;b&gt;&lt;span style=&quot;color: #EF5369;&quot;&gt;카프카를 통해 비동기로 핵심 비즈니스 로직을 처리하는 부분&lt;/span&gt;&lt;/b&gt;이 많은데, 우리 서비스의 핵심 기능이라고 볼 수 있을 만큼 중요한 기능이다 보니까 작은 피처를 개발하더라도 꼭 테스트를 해주고 넘어가는 부분들이 있다.&lt;br&gt;이때, 보통은 본인의 로컬 환경에서 띄워서 테스트를 하는 경우도 많지만, 나 같은 경우 로컬에서 처리되는 느린 속도 &amp;amp; 실제 앱 환경에서 어떤 식으로 돌아가는지 (모든 플로우가 정상적으로 동작하는지) 확인하고 싶은 경우가 많아서 개발 환경에 작업물을 띄워서 테스트하는 경우가 많았다.&lt;br&gt;&amp;nbsp;&lt;br&gt;이때, 현재 사내 시스템 상으로는 내가 작업한 branch를 대상으로 내 요청이 자동으로 포워딩 되는 기능을 제공하고 있어 테스트하는데 어렵지는 않지만, 다음과 같은 문제가 발생하고 있었다.&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style3&quot;&gt;☁︎&amp;nbsp;카프카를 사용하는 부분에 대해서는 별도로 토픽, 혹은 컨슈머 그룹을 분리해주지 않으면 내 개인 작업 환경에서 프로듀싱한 데이터가 개발환경에서 컨슘이 되어 원하는대로 테스트가 동작하지 않음&lt;br&gt;☁︎&amp;nbsp;개발 환경에 바로 merge를 하게 되면 타인의 작업물과 겹치기도 하고, 실수로 운영 환경으로 배포본이 나갈 수 있음&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;첫 번째 문제의 경우, 개발 환경과 내가 작업한 브랜치 환경이 둘 다 컨슘을 진행하게 되어서 &lt;b&gt;일부 트래픽은 개발 환경으로, 일부 트래픽은 나의 개인 브랜치 환경으로&lt;/b&gt; 컨슘이 되어, 모든 로직들이 정상적으로 동작하는지 확인이 어려운 문제였다.&lt;br&gt;&amp;nbsp;&lt;br&gt;그래서 보통 우리 팀 인원들은 개발 환경에 바로 merge를 진행하여 테스트를 진행하는 경우도 있었는데, 사실 올바른 테스트 방법은 아니라는 생각이 계속 들었었다. (믿음 주도 개발....) 혹은, 개인 작업물 대상으로 매번 토픽이나 컨슈머 그룹을 변경하여 push 하고, merge 시에 제거하는 방법을 활용하는 방법도 있었다. 당연하게도 이는 매우 귀찮다 보니까 자주 사용하지 않는 방법이었다... ㅠㅠ&lt;br&gt;&amp;nbsp;&lt;br&gt;물론! &lt;span style=&quot;color: #EF5369;&quot;&gt;통합 테스트를 통해서 어느 정도 확인이 가능&lt;/span&gt;하지만 (embedded kafka 활용) 앱 내에서 확인하는 것이 조금 더 편하게 느껴져서 다른 방법으로 해결을 하고 싶었다. (사실 통합 테스트를 사용하는 것이 맞다...! 다만 우리의 모든 비즈니스 로직에 대해 완벽하게 적용이 되어 있는 것은 아니라서 테스트에 조금 더 고도화가 필요한 상황이었던 것도 맞다.)&lt;br&gt;&amp;nbsp;&lt;br&gt;그래서, ‘&lt;span style=&quot;color: #EF5369;&quot;&gt;개인 branch에서 테스트를 할 때 사용자에 따라 자동으로 토픽이 변경되도록 만들기&lt;/span&gt;’를 목표로 삼고 이번에 작업을 진행해보았었다.&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot;&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&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;현재 사내 프로젝트에서는 토픽에 대한 정보가 application.yml 파일에 정의되어 있어, 토픽에 대한 정보를 받아올 때 다음과 같이 @Value 어노테이션을 사용하여 프로퍼티 정보를 읽어왔었다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Value(&quot;\${very.important.topic}&quot;)
val topic: String&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;br&gt;그래서 처음 작업을 들어갈 때는 스프링의 @Value 어노테이션이 어떤 식으로 돌아가는지 살펴보고 싶어, @Value 어노테이션에 적혀 있는 주석을 먼저 읽어보았었다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Note&amp;nbsp;that&amp;nbsp;actual&amp;nbsp;processing&amp;nbsp;of&amp;nbsp;the&amp;nbsp;@Value&amp;nbsp;annotation&amp;nbsp;is&amp;nbsp;performed&amp;nbsp;by&amp;nbsp;a&amp;nbsp;BeanPostProcessor&amp;nbsp;which&amp;nbsp;in&amp;nbsp;turn&amp;nbsp;means&amp;nbsp;that&amp;nbsp;you&amp;nbsp;cannot&amp;nbsp;use&amp;nbsp;@Value&amp;nbsp;within&amp;nbsp;BeanPostProcessor&amp;nbsp;or&amp;nbsp;BeanFactoryPostProcessor&amp;nbsp;types.&amp;nbsp;Please&amp;nbsp;consult&amp;nbsp;the&amp;nbsp;javadoc&amp;nbsp;for&amp;nbsp;the&amp;nbsp;AutowiredAnnotationBeanPostProcessor&amp;nbsp;class&amp;nbsp;(which,&amp;nbsp;by&amp;nbsp;default,&amp;nbsp;checks&amp;nbsp;for&amp;nbsp;the&amp;nbsp;presence&amp;nbsp;of&amp;nbsp;this&amp;nbsp;annotation)&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br&gt;위 내용을 보니, 실질적으로 @Value 어노테이션에 대한 처리는 BeanPostProcessor 에 의해서 수행이 된다고 써있는 것을 볼 수 있었으며, &lt;span style=&quot;color: #EF5369;&quot;&gt;AutowiredAnnotationBeanPostProcessor&lt;/span&gt; 을 참고해보라는 내용이었다. 오호, 그러면 이 친구를 통해서 진행하겠다 싶어서 대충 내부 내용을 보았다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public AutowiredAnnotationBeanPostProcessor() {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;this.autowiredAnnotationTypes.add(Autowired.class);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;this.autowiredAnnotationTypes.add(Value.class);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;...
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br&gt; 생성자에서 ‘autowiredAnnotationTypes’ 라는 필드에 @Value 어노테이션을 넣어두는 것을 볼 수 있었다.&lt;br&gt;그리고, 조금 더 파고 들다 보니 대충 다음과 같이 동작한다는 것을 파악하였다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Override
public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// 빈에 대한 메타데이터 조회하기
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;InjectionMetadata metadata = findAutowiringMetadata(beanName, bean.getClass(), pvs);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// 의존성 주입하기
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;metadata.inject(bean, beanName, pvs);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return pvs;
}

private InjectionMetadata findAutowiringMetadata(String beanName, Class&amp;lt;?&amp;gt; clazz, @Nullable PropertyValues pvs) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;...
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;InjectionMetadata metadata = this.injectionMetadataCache.get(cacheKey);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;...
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// 메타데이터 생성
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;metadata = buildAutowiringMetadata(clazz);
}

private InjectionMetadata buildAutowiringMetadata(Class&amp;lt;?&amp;gt; clazz) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// @Value나 @Autowired 가 붙은 게 아니라면 패스
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (!AnnotationUtils.isCandidateClass(clazz, this.autowiredAnnotationTypes)) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return InjectionMetadata.EMPTY;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;...

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;List&amp;lt;InjectionMetadata.InjectedElement&amp;gt; elements = new ArrayList&amp;lt;&amp;gt;();

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;final List&amp;lt;InjectionMetadata.InjectedElement&amp;gt; currElements = new ArrayList&amp;lt;&amp;gt;();

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// 어노테이션 찾아서 메타데이터 생성
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ReflectionUtils.doWithLocalFields(targetClass, field -&amp;gt; {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;MergedAnnotation&amp;lt;?&amp;gt; ann = findAutowiredAnnotation(field);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (ann != null) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (Modifier.isStatic(field.getModifiers())) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (logger.isInfoEnabled()) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;logger.info(&quot;Autowired annotation is not supported on static fields: &quot; + field);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;boolean required = determineRequiredStatus(ann);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;currElements.add(new AutowiredFieldElement(field, required));
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
});

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;...

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return InjectionMetadata.forElements(elements, clazz);
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br&gt;- 특정 클래스에서 @Autowired가 붙은 필드와 메서드를 찾아내고, 해당 필드나 메서드가 의존성 주입을 받을 수 있도록 메타데이터를 구축한다.&lt;br&gt;- 구축된 메타데이터를 바탕으로 실제로 주입을 진행한다.&lt;br&gt;-&amp;nbsp;이것을&amp;nbsp;보고&amp;nbsp;대충&amp;nbsp;빈&lt;span style=&quot;color: #EF5369;&quot;&gt;&amp;nbsp;후처리기를&amp;nbsp;활용하여&amp;nbsp;리플렉션을&amp;nbsp;통해&amp;nbsp;동적으로&amp;nbsp;필드&amp;nbsp;내용을&amp;nbsp;변경해줄&amp;nbsp;수&amp;nbsp;있을&amp;nbsp;것&amp;nbsp;같다&lt;/span&gt;는&amp;nbsp;생각이&amp;nbsp;들었다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot;&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; &amp;nbsp;&amp;nbsp;사용자 구분하기&lt;/b&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면, 이번에는 사용자를 어떻게 식별하면 좋을지가 고민이 되었다.&lt;br&gt;실제로 개인 branch를 대상으로 애플리케이션이 띄워질 때 서버의 프로퍼티를 동적으로 주입해줄 수 있도록 만들어도 되지만, &lt;b&gt;인프라 레벨까지 무언가 수정을 하고 싶지 않아서&lt;/b&gt; 어떤 정보를 활용할 수 있을지 고민을 했었다.&lt;br&gt;&amp;nbsp;&lt;br&gt;그러다가 발견한 것이, 처음 스프링이 시작될 때 뜨는 문구였다.&lt;br&gt;처음 스프링이 시작되면 ‘&lt;b&gt;Starting TestApplication using Java 17.0.10 on jwjw-1 with PID …&lt;/b&gt;‘ 와 같은 문구가 뜬다는 것을 보고, 여기에서 ‘jwjw-1’라는 이름은 어디에서 가져오는지 궁금해졌었다.&lt;br&gt;&amp;nbsp;&lt;br&gt;실제로 다른 개발자분들이 서버를 뜰 때 다 다른 이름이 노출되는 것을 보고,&lt;span style=&quot;color: #EF5369;&quot;&gt; 이 이름을 사용하여 토픽의 postfix로 붙여준다면 딱 사용자 수만큼만 신규 토픽이 생성되니 괜찮을 것&lt;/span&gt;이라고 판단하였다.&lt;br&gt;&amp;nbsp;&lt;br&gt;그래서, 이 문구를 어디에서 찍어줄지 확인해보기 위해서 또 다시 하나씩 파고 들기 시작했다.&lt;br&gt;기본적으로 코틀린 + 스프링 부트를 사용한다면 main 함수에 다음과 같은 실행 함수가 있는 것을 볼 수 있다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;fun main(args: Array&amp;lt;String&amp;gt;) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;runApplication&amp;lt;TestApplication&amp;gt;(*args)
}

inline fun &amp;lt;reified T : Any&amp;gt; runApplication(vararg args: String): ConfigurableApplicationContext =
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;SpringApplication.run(T::class.java, *args)&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;br&gt;여기에서 내부를 계속 파고 들어가다 보면, 다음과 같이 start messsage를 만드는 것을 볼 수 있는데, 여기에서 appendOn() 함수를 보면, 내부적으로 &lt;b&gt;InetAddress.getLocalHost().getHostName()&lt;/b&gt; 를 통해서 받아오는 것을 알 수 있었다!&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;private CharSequence getStartingMessage() {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;StringBuilder message = new StringBuilder();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;message.append(&quot;Starting &quot;);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;appendApplicationName(message);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;appendVersion(message, this.sourceClass);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;appendJavaVersion(message);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// 여기!
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;appendOn(message);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;appendPid(message);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;appendContext(message);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return message;
}

private void appendOn(StringBuilder message) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;long startTime = System.currentTimeMillis();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;append(message, &quot;on &quot;, () -&amp;gt; InetAddress.getLocalHost().getHostName());
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;...
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br&gt; 호스트 이름을 활용하게 되면 개인 branch에서 작성한 내용이 나간 서버의 경우 해당 branch를 트리거한 사용자의 정보가 남기 때문에 이를 바탕으로 식별을 하면 좋을 것 같다고 판단하였다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot;&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  &amp;nbsp;빈 후처리기를 활용한 초기 구성&lt;/b&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;어느 정도 윤곽이 잡혔으니, 실제로 코드를 작성해보자.&lt;br&gt;초기에는 `@ConvertTopic` 이라는 어노테이션을 만들어서, 해당 어노테이션이 붙어 있는 필드들에 대해 리플렉션으로 접근하여 객체를 다시 만들어주는 방법을 생각하였다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Target(AnnotationTarget.FIELD)
@Retention(AnnotationRetention.RUNTIME)
annotation class ConvertTopic(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;val topic: String
)&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;여기에서 필드로 가지고 있는 ‘topic’에 실제 토픽 정보를 담은 프로퍼티 이름을 담을 수 있도록 설정하였다.&lt;br&gt;이후, 빈 후처리기 함수의 인터페이스 중에서 스프링 빈이 생성된 이후, &lt;span style=&quot;color: #EF5369;&quot;&gt;초기화 콜백이 호출되기 이전에 호출될 수 있도록 postProcessBeforeInitialization()&lt;/span&gt; 함수를 활용하였다.&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Component
class ConvertTopicAnnotationBeanPostProcessor(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private val environment: Environment,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@Value(&quot;\${spring.profiles.active}&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private val profile: String,
) : BeanPostProcessor {

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;override fun postProcessBeforeInitialization(bean: Any, beanName: String): Any? {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ReflectionUtils.doWithFields(bean::class.java, ConvertTopicCallback(bean, environment, profile))
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return bean
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br&gt;다행히도 스프링에서는 &lt;b&gt;ReflectionUtils&lt;/b&gt; 라는 고급 도구를 제공해주고 있기 때문에, 조금 더 간결하게 코드를 작성할 수 있었다.&lt;br&gt;두 번째 인자로 작성한 FieldCallback 을 통하여 특정 필드에 대해 해당 콜백 내부에서 조작해 첫 번째 인자로 받은 클래스를 원하는대로 바꿀 수 있게 된다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;class ConvertTopicCallback(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private val bean: Any,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private val environment: Environment,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private val profile: String,
) : ReflectionUtils.FieldCallback {

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;override fun doWith(field: Field) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// 원하는 profile이 아니면 조작 X
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (profile != &quot;...&quot;) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;val targetAnnotation = field.getAnnotation(ConvertTopic::class.java) ?: return
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;field.isAccessible = true

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// topic 필드에 담긴 프로퍼티 정보를 바탕으로 실제 값 가져오기
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;val original = environment.getProperty(targetAnnotation.topic)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// 바뀐 값
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;val hostName = InetAddress.getLocalHost().hostName
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;val new = original.plus(&quot;.$hostName&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;field.set(bean, new)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;그리고, 해당 어노테이션이 달려 있는 필드에 대해서만 동작할 수 있도록 간단한 검증 조건을 추가해주고, 흔한 리플렉션 코드에서 많이 볼 수 있는 isAccessible 처리를 통해 필드 값을 조작할 수 있게 해주었다. 실제로는 profile 별로 조금 더 컨버팅 하는 로직을 분리하였지만, 여기에서는 간략하게 &lt;b&gt;호스트 이름을 불러온 프로퍼티에 추가하는 정도&lt;/b&gt;로만 작성해두었다.&lt;br&gt;굉장히 복잡해 보여도, 실제 코드를 보면 단순히 필드 값을 원하는대로 바꾸어서 셋팅해주는 정도이다.&lt;br&gt;&amp;nbsp;&lt;br&gt;정상적으로 기능이 동작하는지 &lt;b&gt;테스트 코드를 통해 확인&lt;/b&gt;해보자.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Component
class TestClass(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@Value(&quot;\${test-topic1}&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private val testTopic1: String
) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@ConvertTopic(&quot;test-topic2&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private lateinit var testTopic2: String

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;fun getTestTopic1(): String {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return testTopic1
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;fun getTestTopic2(): String {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return testTopic2
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br&gt;먼저,&amp;nbsp;테스트&amp;nbsp;클래스를&amp;nbsp;하나&amp;nbsp;정의하여&amp;nbsp;만들어준&amp;nbsp;@ConvertTopic&amp;nbsp;어노테이션을&amp;nbsp;붙인&amp;nbsp;필드와&amp;nbsp;기본&amp;nbsp;@Value&amp;nbsp;를&amp;nbsp;통하여&amp;nbsp;정의한&amp;nbsp;필드를&amp;nbsp;각각&amp;nbsp;선언해주었다.&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Import(TestClass::class)
@TestPropertySource(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;properties = [
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;test-topic1=topic1&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;test-topic2=topic2&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;]
)
class ConvertTopicAnnotationBeanPostProcessorTest(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private val testClass: TestClass,
) : BehaviorSpec({

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Given(&quot;토픽 치환 테스트&quot;) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;When(&quot;@ConvertTopic 가 붙어 있지 않은 변수에 대해서&quot;) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Then(&quot;컨버팅을 진행하지 않는다.&quot;) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;testClass.getTestTopic1() shouldBe &quot;topic1&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;When(&quot;@ConvertTopic 가 붙은 변수에 대해서&quot;) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Then(&quot;컨버팅을 진행한다.&quot;) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;testClass.getTestTopic2() shouldBe &quot;topic2.test&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
})&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br&gt;그리고, &lt;span style=&quot;color: #EF5369;&quot;&gt;@TestPropertySource 를 통해서 실제 테스트 클래스에서 사용할 테스트 프로퍼티를 정의&lt;/span&gt;해주었다. application-test.yml 등을 활용해도 좋지만, 해당 테스트 클래스에서만 유효하기를 바랬고, 무엇보다 명시적으로 테스트에 드러나기를 바랬다. (별도의 파일로 분리하게 되면 아무래도 해당 파일을 다시 들어가서 확인을 해봐야 하니까 불편하게 느껴질 수도 있겠다는 생각이 들었다,)&lt;br&gt;&amp;nbsp;&lt;br&gt;실제로 테스트를 돌려보면 의도대로 잘 동작을 하였으며, test-topic1의 경우 그대로 topic1 이라는 값으로, test-topic2의 경우 topic2가 아닌 topic2.test로 컨버팅이 잘 된 것을 볼 수 있었다!&lt;br&gt;&amp;nbsp;&lt;br&gt;그러나, 여기에서 한 가지 찜찜한 점이 있었다. 바로, TestClass에서도 볼 수 있는 것처럼 &lt;span style=&quot;color: #EF5369;&quot;&gt;생성자 주입을 할 수 없었던 것&lt;/span&gt;이었다.&lt;br&gt;아무래도 클래스의 생성자 파라미터가 아닌 필드 파라미터를 접근하도록 하려고 하다 보니 그랬던 것 같았었다.  이를 해결하기 위해 다음과 같이 @Value + @ConvertTopic 을 함께 혼용하여 사용하고, 필드에 대한 콜백 로직을 살짝 수정해주었다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;override fun doWith(field: Field) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// 필드에 어노테이션이 존재하는지 확인
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (field.isAnnotationPresent(ConvertTopic::class.java).not()) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// ...

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// bean 객체에 대해 field로 지정된 필드 값을 조회할 수 있도록 수정
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// 원본 값
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;val original = field.get(bean).toString()

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// 바뀐 값
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;val hostName = InetAddress.getLocalHost().hostName
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;val new = original.plus(&quot;.$hostName&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;field.set(bean, new)
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br&gt;어노테이션의&amp;nbsp;필드로&amp;nbsp;주어졌던&amp;nbsp;값을&amp;nbsp;활용했던&amp;nbsp;것과&amp;nbsp;다르게,&amp;nbsp;이번에는&amp;nbsp;&lt;b&gt;bean&amp;nbsp;객체의&amp;nbsp;field&amp;nbsp;값을&amp;nbsp;직접&amp;nbsp;접근&lt;/b&gt;하도록&amp;nbsp;만들었다.&amp;nbsp;이&amp;nbsp;코드가&amp;nbsp;잘&amp;nbsp;이해가&amp;nbsp;안&amp;nbsp;될&amp;nbsp;수&amp;nbsp;있는데,&amp;nbsp;아래&amp;nbsp;테스트&amp;nbsp;클래스의&amp;nbsp;예제를&amp;nbsp;바탕으로&amp;nbsp;알아보자.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Component
class TestClass(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@Value(&quot;\${test-topic1}&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private val testTopic1: String,

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@ConvertTopic
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@Value(&quot;\${test-topic2}&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private val testTopic2: String,
)&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;빈 후처리기의 경우 빈이 생성된 이후, 즉 스프링에서 &lt;span style=&quot;color: #EF5369;&quot;&gt;프로퍼티에 대한 placeHolder 작업이 완료된 이후에 처리&lt;/span&gt;되기 때문에 testTopic2에는 @Value 로 부터 주입받은 ‘topic2’ 라는 값이 들어가 있게 된다.&lt;br&gt;이후, &lt;b&gt;field.get(bean)&lt;/b&gt; 를 하게 되면 주입된 값인 ‘topic2’ 라는 값이 반환되기 때문에, environment에 대해서도 접근할 필요 없이 바로 해당 값을 original로 판단하여 호스트 이름을 postfix로 붙여주면 된다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br&gt;&lt;br&gt;&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; &amp;nbsp;하지만 문제가 발생하였다&lt;br&gt;&lt;/b&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;그러나, 항상 그렇듯이 이렇게 끝나면 참 좋았겠지만… 위의 로직에는 2가지 정도의 문제가 있었다.&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style3&quot;&gt;☁︎&amp;nbsp;처음&amp;nbsp;보면&amp;nbsp;이해하기&amp;nbsp;어려운&amp;nbsp;코드&amp;nbsp;+&amp;nbsp;리플렉션&amp;nbsp;사용으로&amp;nbsp;인한&amp;nbsp;거부감&lt;br&gt;☁︎&amp;nbsp;lazy&amp;nbsp;대리자에&amp;nbsp;대해서&amp;nbsp;제대로&amp;nbsp;작동되지&amp;nbsp;않음&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;먼저, 리플렉션의 경우 성능 저하가 발생할 수는 있으나, 어차피 처음에 빈이 띄워질 때 수정이 되는 것이라서 크게 우려되지 않았다. 그러나, 이것보다는 &lt;b&gt;너무 스프링에 의존적인 코드이기 때문에&lt;/b&gt; 처음 보는 사람이 읽기 어려운 코드가 될 수 있겠다는 생각이 들었다.&lt;br&gt;또한, 사실상 실제 비즈니스 로직은 호스트 이름을 가져와서 프로퍼티에 붙여주기만 하면 되는 것인데, &lt;span style=&quot;color: #EF5369;&quot;&gt;bean이나 ReflectionUtils 같은 것들이 들어가면서 왜인지 모를 거부감이 들 수 있겠다는 생각&lt;/span&gt;이 들었다.&lt;br&gt;&lt;br&gt;사실, 위의 문제보다 그 아래 문제가 더 크게 걸리는 부분이었다. 이전 문제의 경우 팀원들에게 충분한 공유를 하고 사용할 수 있도록 리딩할 예정이었기에 괜찮았지만, 이는 아예 정상적으로 동작하지 않는 문제가 나타났던 것이었다.&lt;br&gt;&amp;nbsp;&lt;br&gt;사내 프로젝트에서는 다음과 같이 코틀린에서 제공하는 lazy 대리자를 활용하여 기본적인 기능들에 대해 추상화를 진행해두어 사용하고 있었다. 실제 코드를 발췌할 수 없기 때문에, 대략 이런 느낌으로 사용한다고만 생각해두자.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Component
class ImportantHandler(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@Value(&quot;\${very.important.topic}&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;topic: String,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;factory: Factory,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;...
): Handler by factory.create(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;topic = topic
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;...
)

inline fun &amp;lt;reified T: ...&amp;gt; Factory.create(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;topic: String
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;...
): ConcurrentHandler&amp;lt;String, T&amp;gt; {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;...
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br&gt;다만, 위의 상황에서 @ConvertTopic을 사용하면 어떻게 될까?&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Component
class ImportantHandler(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@ConvertTopic
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@Value(&quot;\${very.important.topic}&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;topic: String,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;factory: Factory,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;...
): Handler by factory.create(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;topic = topic
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;...
)&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br&gt;먼저, 위와 같이 사용하기 위해서는 @ConvertTopic의 타겟 타입에 &lt;b&gt;value parameter&lt;/b&gt;도 추가해줘야 한다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Target(AnnotationTarget.FIELD, AnnotationTarget.VALUE_PARAMETER)
@Retention(AnnotationRetention.RUNTIME)
annotation class ConvertTopic&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br&gt;이러면, factory.create() 로 넘어가는 topic 값이 변화가 될 것이라 생각할 수 있지만 실제로는 그렇게 동작하지 않는다. 이는, 이 함수가 실제로 자바에서 어떻게 동작하는지 확인하면 알 수 있기에 디컴파일을 진행해보자.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class ImportantHandler implements Handler {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private final ConcurrentHandler $$delegate_0;

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;public ImportantHandler(@ConvertTopic @Value(&quot;\${very.important.topic}&quot;) @NotNull String topic, ...) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Intrinsics.checkNotNullParameter(topic, &quot;topic&quot;);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;...
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;String[] var12 = new String[]{topic};
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;this.$$delegate_0 = DefaultImpls.create$default(var12, ...)
}

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// 실제 비즈니스 로직을 담은 함수. 대리자에 의해 다음과 같이 수행된다.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;public void run() {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;this.$$delegate_0.run();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br&gt;위임을 위해 ImportantHandler의 필드로 ConcurrentHandler 를 가지고, 생성자 내부에서 delegate 필드가 채워지는 것을 볼 수 있다. 앞서 말했던 것처럼, &lt;span style=&quot;color: #EF5369;&quot;&gt;빈 후처리기의 초기화 과정은 빈이 생성된 이후 = 즉, 객체가 생성된 이후&lt;/span&gt;에 진행된다. delegate 필드가 초기화되는 시점에서 topic 필드는 아직 컨버팅이 되기 이전의 값이기 때문에, &lt;b&gt;컨버팅이 되기 이전의 값으로 데이터가 초기화가 되면서 정상적으로 동작하지 않게 되는 것&lt;/b&gt;이다.&lt;br&gt;&lt;br&gt;이후 빈 후처리기 과정에서 topic이 초기화가 되더라도,&lt;b&gt; 이미 delegate 필드가 가지고 있는 속성은 초기화가 완료가 된 상태이기 때문에 변화되지 않는다&lt;/b&gt;. 그래서 비즈니스 로직으로 인해 run() 메서드가 수행이 되더라도 topic 값이 변하지 않은 속성을 가진 delegate.run()이 수행이 되기 때문에 의도대로 동작하지 않는 것이다.&lt;br&gt;&amp;nbsp;&lt;br&gt;그래서, 위의 문제를 해결하기 위해 처음에는 lazy 대리자를 사용하지 않고 다음과 같이 직접 위임을 하는 것은 어떨지 생각해보았다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Component
class ImportantHandler(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@ConvertTopic
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@Value(&quot;\${very.important.topic}&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;topic: String,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;factory: Factory,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;...
): Handler {

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private lateinit var delegate: ConcurrentHandler

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@PostConsturct
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;fun init() {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;delegate = factory.create(topic, ..)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;override fun run() {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;delegate.run()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// ...
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br&gt;이때, &lt;span style=&quot;color: #EF5369;&quot;&gt;@PostConsturct 를 통해서 빈 후처리기가 동작한 이후에 delegate 필드가 초기화가 될 수 있도&lt;/span&gt;록 만들었다. 이러면 컨버팅이 된 topic 값을 통해 delegate 객체가 생성되기 때문에 의도대로 잘 동작은 하였다.&lt;br&gt;&amp;nbsp;&lt;br&gt;그러나, override를 통해 모든 메서드들을 다시 재정의하는 것도 너무 번거롭고, &lt;b&gt;코틀린 언어의 장점을 하나도 사용하지 못하는 코드&lt;/b&gt;인 것 같았다. 코드의 길이 역시 이전보다 훨씬 길어지기 때문에 위와 같은 방식을 사용하는 것은 팀원들의 동의를 얻는 것도 어려울 것 같다는 생각이 들었다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot;&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; &amp;nbsp;SpEL 활용하기&lt;/b&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;그러던 중, 공식 문서에서 &lt;span style=&quot;color: #EF5369;&quot;&gt;SpEL&lt;/span&gt; 이라는 친구를 발견하게 되었다. &lt;span style=&quot;color: #EF5369;&quot;&gt;SpEL은 ‘런타임’에 객체 그래프를 쿼리하고 조작할 수 있는 강력한 표현식 언어&lt;/span&gt;로, @Value 어노테이션을 활용하여 런타임에 조작이 가능하다는 것이었다!&lt;br&gt;실제로 공식 문서를 보면, 다음과 같이 함께 사용할 수 있는 예제를 보여주고 있다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure data-ke-type=&quot;opengraph&quot; data-og-title=&quot;Using @Value :: Spring Framework&quot; data-ke-align=&quot;alignCenter&quot; data-og-description=&quot;A default lenient embedded value resolver is provided by Spring. It will try to resolve the property value and if it cannot be resolved, the property name (for example ${catalog.name}) will be injected as the value. If you want to maintain strict control o&quot; data-og-host=&quot;docs.spring.io&quot; data-og-source-url=&quot;https://docs.spring.io/spring-framework/reference/core/beans/annotation-config/value-annotations.html&quot; data-og-image=&quot;&quot; data-og-url=&quot;https://docs.spring.io/spring-framework/reference/core/beans/annotation-config/value-annotations.html&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-framework/reference/core/beans/annotation-config/value-annotations.html&quot; target=&quot;_blank&quot; data-source-url=&quot;https://docs.spring.io/spring-framework/reference/core/beans/annotation-config/value-annotations.html&quot;&gt;&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('')&quot;&gt; &lt;/div&gt;&lt;div class=&quot;og-text&quot;&gt;&lt;p class=&quot;og-title&quot;&gt;Using @Value :: Spring Framework&lt;/p&gt;&lt;p class=&quot;og-desc&quot;&gt;A default lenient embedded value resolver is provided by Spring. It will try to resolve the property value and if it cannot be resolved, the property name (for example ${catalog.name}) will be injected as the value. If you want to maintain strict control o&lt;/p&gt;&lt;p class=&quot;og-host&quot;&gt;docs.spring.io&lt;/p&gt;&lt;/div&gt;&lt;/a&gt;&lt;/figure&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Component
class MovieRecommender(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@Value(&quot;#{systemProperties['user.catalog'] + 'Catalog' }&quot;) private val catalog: String
)&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br&gt;위의 예제처럼 &lt;b&gt;작성하고 싶은 표현식을 #{} 내부에 작성&lt;/b&gt;하게 되면, 내부적으로 컨버팅을 진행해준다.&lt;br&gt;&amp;nbsp;&lt;br&gt;적용 방법은 완전 쉬우니, 한 번 이전 코드에 적용을 해보자.&lt;br&gt;기존에 작성하였던 콜백 로직을 조금 더 적합한 이름의 클래스로 추출을 진행해주었다. 또한, &lt;b&gt;이제는 이 클래스 자체를 빈으로 등록&lt;/b&gt;하면 되기 때문에 다른 빈에 대해 주입도 받을 수 있어서 더 편리하게 사용이 가능해졌다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Component
class TopicConverter(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private val environment: Environment,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private val profile: String,
) {

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;fun execute(property: String): String {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// ... 기존의 로직 작성
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return original.plus(&quot;.$hostName&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&lt;br&gt;함수의 인자로는 변경을 원하는 프로퍼티의 키를, 함수의 반환 값으로는 컨버팅된 값을 반환하도록 만들어주었다.&lt;br&gt;그리고, 실제로 사용할 때는 다음과 같이 사용해주면 된다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Value(&quot;#{topicConverter.execute('very.important.topic')}&quot;)
topic: String,&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br&gt;더 이상 별도의 어노테이션도 필요하지 않고, 기존 @Value 내부에서 함수를 호출하는 것처럼 만들면 되기 때문에 사용성 역시 훨씬 편리해졌다! 또한, intellij 기능 덕분인지, 하이라이팅도 해주기 때문에 코드를 따라가기도 쉬워져서 더 마음에 들었다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;904&quot; data-origin-height=&quot;84&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bDWxDg/btsJQnX3lis/YQTn7IIgBmZCSzuKcOpAI0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bDWxDg/btsJQnX3lis/YQTn7IIgBmZCSzuKcOpAI0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bDWxDg/btsJQnX3lis/YQTn7IIgBmZCSzuKcOpAI0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbDWxDg%2FbtsJQnX3lis%2FYQTn7IIgBmZCSzuKcOpAI0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;904&quot; height=&quot;84&quot; data-origin-width=&quot;904&quot; data-origin-height=&quot;84&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&lt;br&gt;그래서 최종적으로는 위와 같이 기존 코드에 모두 적용을 완료하였으며, 덕분에 개인 branch 작업물에 대해 개발자마다 트래픽을 제어할 수 있도록 쉽게 핸들링이 가능해졌다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br&gt;&lt;br&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;꽤나 여러 가지로 삽질을 했었지만, 덕분에 공식 문서를 꼭 봐야겠다는 생각이 들었던 시간이었다. (사실, 처음에 @Value 어노테이션의 설명에서도 다음과 같이 나와있었기 때문에 제대로 읽지 않은 내 문제도 컸었다... ㅎㅎ&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;A common use case is to inject values using #{systemProperties. myProp} style SpEL (Spring Expression Language) expressions. Alternatively, values may be injected using ${my. app. myProp} style property placeholders.&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;처음에는 내가 직면한 문제를 spEL이랑 엮어봐야겠다는 생각조차 못했었다가 나중에 공식 문서 스터디를 진행하면서 spEL에 대한 정보를 알게 되고 그때 눈에 띄었던 것 같다. 사람들이 이래서 공식 문서를 읽으라고 하는 건가... 하면서 많이 배웠던 시간이었다.&lt;br&gt;&amp;nbsp;&lt;br&gt;사실 정말 이 방법이 올바른 방법이 되는지는 잘 모르겠다. 보통의 사람들이라면 테스트 코드로 잘 검증할 수 있는게 맞지 않나요? 라고 충분히 반박할 수도 있다고 생각이 든다.&lt;br&gt;&amp;nbsp;&lt;br&gt;아직까지는 E2E 테스트를 완전히 믿고 신뢰할 수 있을 만큼 내가 정교하게 작성하지 못한 탓도 큰 것 같다. 실제로 메인 비즈니스 로직에 대한 통합 테스트 코드는 작성이 되어 있으나, 해당 테스트 코드를 돌리고 항상 개발 앱 환경에서 다시 한 번 테스트를 돌려보는 내 모습을 바라보면서 아직은 많이 부족하다는 생각이 들었다. 개발에는 정답이 없는 것이니까... 우선은 현 자리에서 최대한 열심히 노력해봐야겠다. ㅎㅎ&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;아무튼, 오랜만에 글을 쓰다 보니 길어진 것 같다.&lt;br&gt;쓰고 싶은 글은 많은데 게으름으로 인해 글을 많이 못 쓰고 있는 것 같다… 종종 생각날 때 조금씩 써야겠다… 끝!&lt;/p&gt;</description>
      <category>개발일지</category>
      <category>@Value</category>
      <category>spEL</category>
      <category>spring</category>
      <author>dolmeng2</author>
      <guid isPermaLink="true">https://cl8d.tistory.com/132</guid>
      <comments>https://cl8d.tistory.com/132#entry132comment</comments>
      <pubDate>Sat, 28 Sep 2024 09:21:10 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] 통합 테스트 환경에서 로직의 일부를 stubbing 하기</title>
      <link>https://cl8d.tistory.com/131</link>
      <description>&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;span style=&quot;color: #ef5369;&quot;&gt;실제 빈 중에 일부만 stubbing을 하고 싶은 상황&lt;/span&gt;이 생긴다.&lt;br /&gt;예를 들어, 외부 API를 통신하거나 외부 시스템을 활용하게 되면 테스트에서 그대로 활용하기 어려운데, 해당 부분에 대해서만 우리가 원하는 시나리오를 제공하여 우리 애플리케이션이 잘 동작하는지 확인하고 싶을 수 있다. 하지만, 보통 Spring 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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;☁︎ 테스트 시나리오&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&amp;nbsp;- 사용자가 상품을 구매하게 되면, 외부 API를 호출하여 재고가 존재하는지 확인한다.&lt;br /&gt;&amp;nbsp; - 만약 재고가 존재하면 외부 API는 &amp;lsquo;SUCCESS&amp;rsquo;라는 응답을, 재고가 존재하지 않으면 &amp;lsquo;FAIL&amp;rsquo;이라는 응답을 반환한다.&lt;br /&gt;- 우리는 여기에서 &amp;lsquo;외부 API가 SUCCESS 혹은 FAIL&amp;rsquo;이라는 응답을 반환하는 부분에 대해서 stubbing을 진행할 것이다.&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;br /&gt;본 글에서는 아키텍처나 세부 코드에 대한 구현은 중요하지 않기 때문에 가장 기본적인 코드로 작성하였다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;✔️ 기본 코드&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1719740853108&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RestController
class OrderController(
    private val orderService: OrderService,
) {

    @PostMapping(&quot;/order&quot;)
    fun order(
        @RequestBody request: OrderRequest
    ) {
        orderService.order(request.productId)
    }
}

data class OrderRequest(
    val productId: Long,
)&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1719740867724&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
class OrderService(
    private val externalStockCheckApi: ExternalStockCheckApi
) {

    fun order(productId: Long) {
        val result = externalStockCheckApi.check(productId)
        if (result == StockStatus.FAIL) {
            throw OutOfStockException(&quot;재고가 없습니다.&quot;)
        }
        otherLogic()
    }

    private fun otherLogic() {
        // do something...
    }
}

class OutOfStockException(
    override val message: String
) : RuntimeException(message) {
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;여기에서 otherLogic의 경우 매우 복잡한 비즈니스 로직을 담는다고 가정한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실패의 케이스에 대해서도 복잡한 경우가 포함되지만, 여기에서는 간단하게 예외만 발생시키도록 제어를 해두었다.&lt;/p&gt;
&lt;pre id=&quot;code_1719740884789&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
class ExternalStockCheckApi {
    fun check(productId: Long): StockStatus {
        return callExternalApi(productId)
    }

    private fun callExternalApi(productId: Long): StockStatus {
        // call external api...
        if (Random().nextInt() % 2 == 0) {
            return StockStatus.SUCCESS
        }
        return StockStatus.FAIL
    }
}

enum class StockStatus {
    SUCCESS,
    FAIL
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;ExternalStockCheckApi 의 경우, &lt;b&gt;실제로는 외부 API와 통신하는 구간&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 우리의 코드에서는 해당 부분은 중요하지 않기 때문에 제외하고, 그냥 랜덤한 값에 따라서 성공과 실패에 대해서 반환하도록 수정하였다.&lt;br /&gt;&lt;br /&gt;우리는 위와 같은 코드가 있을 때, `ExternalStockCheckApi`의 결과를 제어하여 우리의 애플리케이션이 의도하는대로 잘 동작하는지를 확인해볼 예정이다. 또한, &lt;span style=&quot;color: #ef5369;&quot;&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;각 컴포넌트들간의 정상적인 협력 관계가 잘 동작하고 있는지 판단하고 싶을 때는 통합 테스트가 좋은 방법이 될 수 있다&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;lsquo;테스트 환경에서도 실제 외부 API를 호출하면 되지 않는가?&amp;rsquo; 라고 생각할 수 있다. 그러나, 만약 외부 API가 일시적으로 장애가 발생했어서 늘 통과하던 테스트가 갑자기 통과하지 않는다면 어떻게 될까? 테스트의 경우 늘 일관적인 시나리오로 동작해야 하기 때문에 이에 대해 위배하게 된다. (FIRST 원칙, 물론 이는 단위 테스트에 대한 원칙이지만 그래도 살짝 인용하자면 그렇다.)&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;또한, 사업적인 관점으로 봤을 때 외부 API에 대한 호출 비용을 받는 곳이라면? 테스트 할 때마다 비용 지출이 발생할 수 있기 때문에 어느 정도의 stubbing은 필요한 상황이다. (실제로 사내에서 동일한 케이스가 있어서, 테스트 환경에서는 아예 dummy 값을 내려주도록 처리하는 부분도 존재한다.)&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를 활용하여 작성해주었다.&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;pre id=&quot;code_1719741577835&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@TestConstructor(autowireMode = TestConstructor.AutowireMode.ALL)
class OrderControllerTest() {

    @LocalServerPort
    private var port: Int = 0

    @BeforeAll
    fun setUp() {
        RestAssured.port = port
    }

    @Test
    fun `재고가 존재하는 경우에는 정상 응답을 반환한다`() {
        val result = callOrderApi(1L)
        assertThat(result.statusCode())
            .isEqualTo(HttpStatus.OK.value())
    }

    @Test
    fun `재고가 존재하지 않는 경우 오류 응답을 반환한다`() {
        val result = callOrderApi(2L)
        assertThat(result.statusCode())
            .isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR.value())
    }

    private fun callOrderApi(
        productId: Long
    ): ExtractableResponse&amp;lt;Response&amp;gt; {
        return RestAssured.given()
            .given().log().all()
            .contentType(&quot;application/json&quot;)
            .body(
                mapOf(&quot;productId&quot; to productId)
            )
            .`when`()
            .post(&quot;/order&quot;)
            .then().log().all().extract()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;우리가 원하는 시나리오는 위와 같은 2가지 상황을 테스트 할 수 있다.&lt;br /&gt;- 재고가 존재하는 경우 아무 오류도 발생하지 않음&lt;br /&gt;- 재고가 존재하지 않는 경우 예외가 발생함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당연히 현재의 테스트 코드는 깨지는 것이 맞다.&lt;br /&gt;&lt;br /&gt;&lt;/p&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;br /&gt;&lt;br /&gt;&lt;b&gt;  @MockBean을 활용하여 재정의하기&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;☁︎ @MockBean이 무엇일까?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저, 첫 번째 방법으로는 @MockBean을 활용하여 `ExternalStockCheckApi` 에 대해 재정의를 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@MockBean의 경우 Spring 1.4에서 추가된 테스트 서포팅용 어노테이션으로, &lt;span style=&quot;color: #ef5369;&quot;&gt;스프링의 ApplicationContext에 mocking 된 객체를 추가하는데 사용하는 어노테이션&lt;/span&gt;이다. 빈의 타입이나 이름으로 등록이 가능하며, 기존의 컨텍스트에서 일치하는 빈에 대해서 mock 빈으로 대체하게 만들어준다. 만약 기존에 빈이 존재하지 않는다면 새로운 빈으로 추가하여 등록해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;Junit4를 사용한다면 `@RunWith(SpringRunner.class)`와 함께, Junit5를 사용한다면 `@ExtendWith(SpringExtension.class)` 와 함께 사용할 수 있다. 나는 &lt;b&gt;@SpringBootTest 어노테이션을 사용할 예정이기 때문에 따로 붙여주지는 않았다&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;참고로, 이러한 @MockBean의 경우 `MockitoTestExecutionListener` 에 의해서 처리되고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`MockitoTestExecutionListener` 의 경우 `&lt;span style=&quot;color: #ef5369;&quot;&gt;AbstractTestExecutionListener&lt;/span&gt;` 을 상속받았으며, 테스트 컨텍스트를 활용하여 테스트 코드 수행 시 어떤 전처리 / 후처리를 진행할지 정해줄 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1719744479986&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class MockitoTestExecutionListener extends AbstractTestExecutionListener {

    @Override
    public void prepareTestInstance(TestContext testContext) throws Exception {
        closeMocks(testContext);
        initMocks(testContext);
        injectFields(testContext);
    }
}

private void initMocks(TestContext testContext) {
    if (hasMockitoAnnotations(testContext)) {
        testContext.setAttribute(MOCKS_ATTRIBUTE_NAME, MockitoAnnotations.openMocks(testContext.getTestInstance()));
    }
}

private void injectFields(TestContext testContext) {
    postProcessFields(testContext, (mockitoField, postProcessor) -&amp;gt; postProcessor.inject(mockitoField.field,
    mockitoField.target, mockitoField.definition));
}

private void postProcessFields(TestContext testContext, BiConsumer&amp;lt;MockitoField, MockitoPostProcessor&amp;gt; consumer) {
    DefinitionsParser parser = new DefinitionsParser();
    parser.parse(testContext.getTestClass());

    if (!parser.getDefinitions().isEmpty()) {
        MockitoPostProcessor postProcessor = testContext.getApplicationContext()
            .getBean(MockitoPostProcessor.class);

        for (Definition definition : parser.getDefinitions()) {
            Field field = parser.getField(definition);
            if (field != null) {
                consumer.accept(new MockitoField(field, testContext.getTestInstance(), definition), postProcessor);
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;testContext를 활용하면 테스트 코드에 선언된 어노테이션 정보를 읽어올 수 있기 때문에, mockito 기반 어노테이션들을 찾아서 mock field로 주입해주는 것을 확인할 수 있다.&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;br /&gt;&lt;b&gt;☁︎ 실제로 적용해보기&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1719744523970&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@TestConstructor(autowireMode = TestConstructor.AutowireMode.ALL)
class OrderControllerTest(
    @MockBean
    private val externalStockCheckApi: ExternalStockCheckApi
) {

    @Test
    fun `재고가 존재하는 경우에는 정상 응답을 반환한다`() {
        Mockito.`when`(
            externalStockCheckApi.check(anyLong())
        ).thenReturn(StockStatus.SUCCESS)

        val result = callOrderApi(1L)
        assertThat(result.statusCode())
            .isEqualTo(HttpStatus.OK.value())
    }

    @Test
    fun `재고가 존재하지 않는 경우 오류 응답을 반환한다`() {
        Mockito.`when`(
            externalStockCheckApi.check(anyLong())
        ).thenReturn(StockStatus.FAIL)

        val result = callOrderApi(2L)
        assertThat(result.statusCode())
            .isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR.value())
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;Mockito.when().thenReturn()을 사용&lt;/span&gt;하면 mockBean으로 등록된 mock 객체에 대해서 쉽게 stubbing을 진행할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;when절에는 우리가 스터빙을 진행하고 싶은 메서드를 입력하고, then 절에는 해당 상황에서 어떤 값을 반환하면 좋을지 작성해주면 된다. thenReturn 외에도 thenThrow, thenAnswer 등으로 더 유연한 테스트 코드를 작성할 수 있다.&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;size18&quot;&gt;&lt;b&gt;☁︎ 단점은 없을까?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;아쉽게도, @MockBean을 활용하게 되면 스프링 컨텍스트를 재사용할 수 없는 문제가 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선, @MockBean을 사용하게 되면 &lt;span style=&quot;color: #ef5369;&quot;&gt;테스트 클래스를 수행할 때마다 Spring Context에 대해 refresh를 진행&lt;/span&gt;하는데, 이는 위에 첨부해둔 `postProcessFields()` 함수의 다음과 같은 부분을 살펴보면 알 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1719744629833&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// postProcessFields 
if (!parser.getDefinitions().isEmpty()) {
    MockitoPostProcessor postProcessor = testContext.getApplicationContext()
        .getBean(MockitoPostProcessor.class);
}

// DefaultTestContext.getApplicationContext
@Override
public ApplicationContext getApplicationContext() {
    ApplicationContext context = this.cacheAwareContextLoaderDelegate.loadContext(this.mergedConfig);
    ...
}

// DefaultCacheAwareContextLoaderDelegate.loadContext
@Override
public ApplicationContext loadContext(MergedContextConfiguration mergedConfig) {
    ...
    synchronized (this.contextCache) {
        ApplicationContext context = this.contextCache.get(mergedConfig);
        ...
        context = loadContextInternal(mergedConfig);
        ...
    }


    protected ApplicationContext loadContextInternal(MergedContextConfiguration mergedConfig) throws Exception {
        ContextLoader contextLoader = getContextLoader(mergedConfig);
        if (contextLoader instanceof SmartContextLoader smartContextLoader) {
            return smartContextLoader.loadContext(mergedConfig);
        }
        ...
    }

    // SpringBootContextLoader.loadContext
    private ApplicationContext loadContext(MergedContextConfiguration mergedConfig, Mode mode,
        ApplicationContextInitializer&amp;lt;ConfigurableApplicationContext&amp;gt; initializer) throws Exception {
        ...
        return hook.run(() -&amp;gt; application.run(args));
    }

    // SpringApplication.run
    public ConfigurableApplicationContext run(String... args) {
    refreshContext(context);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;여기에서 테스트 컨텍스트 내에 있는 applicationContext를 꺼내오는 것을 알 수 있는데, 내부로 타고 들어가게 되면 &lt;span style=&quot;color: #ef5369;&quot;&gt;context에 대해 refresh를 하는 것&lt;/span&gt;을 볼 수 있다.&lt;br /&gt;코드의 중간 부분을 보면 스프링에서는 최대한 캐시된 테스트 컨텍스트를 사용하려고 하지만 (contextCache 같은 필드들이 존재하는 걸 볼 수 있다), &lt;span style=&quot;color: #ef5369;&quot;&gt;@MockBean을 사용하게 되면 해당 빈 뿐만 아니라 해당 빈을 사용하는 다른 친구들도 모두 교체&lt;/span&gt;해줘야 하기 때문에 캐시를 하지 않고 아예 refresh를 해주는 것을 볼 수 있다.&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;class A(val b: B) 와 같은 코드가 있을 때, B가 mockBean이라면 자연스럽게 B를 의존하고 있는 A 역시 mocking된 B를 사용할 수 있도록 교체 작업이 필요해진다. 만약 빈끼리 더 복잡한 의존 관계를 가지고 있다면 자연스럽게 더 많은 빈들에 대해서 교체하는 작업이 필요해질 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;참고로, 기본적으로 스프링의 context caching의 경우 다음과 같은 상황에 대해서 발생한다.&lt;/b&gt;&lt;br /&gt;&amp;nbsp; - 동일한 bean의 조합을 사용했을 때&lt;br /&gt;&amp;nbsp; - 이전의 테스트에서 applicationContext가 오염되지 않은 경우&lt;br /&gt;&amp;nbsp; - @DirtyContext 를 활용하지 않는 경우&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&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;br /&gt;&lt;br /&gt;&lt;b&gt;  @Profile 설정을 통해서 테스트용 빈을 분리하기&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약, 위와 같은 spring context refresh가 싫다면, 테스트에서 사용할 프로파일을 지정해주고 해당 프로파일에서는 다른 동작을 하도록 만들어 줄 수 있다. 하지만, &lt;b&gt;스프링에서는 기본적으로 동일한 이름을 가진 빈을 2개 띄울 수 없다&lt;/b&gt;. 이를 위해 `ExternalStockCheckApi`를 인터페이스로 분리해주는 작업을 진행해주자. 인터페이스의 경우 구체적인 행위가 들어나지 않도록, 재고를 확인하는 용도의 기능 정도만 나타내 줄 수 있도록 만들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1719793558673&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;interface CheckStockPort {
    fun check(productId: Long): StockStatus
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&amp;nbsp;그리고,&amp;nbsp;@Profile&amp;nbsp;어노테이션을&amp;nbsp;활용하여&amp;nbsp;profile&amp;nbsp;별로&amp;nbsp;다른&amp;nbsp;코드가&amp;nbsp;동작할&amp;nbsp;수&amp;nbsp;있도록&amp;nbsp;만들어주자.&lt;/p&gt;
&lt;pre id=&quot;code_1719793574852&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
@Profile(&quot;!test&quot;)
class ExternalStockCheckApi: CheckStockPort {
    override fun check(productId: Long): StockStatus {
        return callExternalApi(productId)
    }
}

@Component
@Profile(&quot;test&quot;)
class TestStockCheckApi: CheckStockPort {
    override fun check(productId: Long): StockStatus {
        if (productId == 1L) {
            return StockStatus.SUCCESS
        }
        return StockStatus.FAIL
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;기존의 ExternalStockCheckApi 에 대해서 CheckStockPort 을 오버라이드 하도록 만들고, &lt;b&gt;특정 Test 프로파일에 대해서는 TestStockCheckApi 가 동작&lt;/b&gt;하도록 만들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고, 우리의 테스트 시나리오에 맞춰 productId 별로 다른 결과가 나타날 수 있도록 만들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에 ExternalStockApi를 의존하던 서비스 코드 역시 인터페이스를 의존하도록 변경해주자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1719793603340&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
class OrderService(
    private val checkStockPort: CheckStockPort,
) {

    fun order(productId: Long) {
        val result = checkStockPort.check(productId)
        ...
    }
}&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;그리고, 다시 테스트 코드로 돌아가서 &amp;lsquo;test&amp;rsquo;라는 profile을 가질 수 있도록 &lt;span style=&quot;color: #ef5369;&quot;&gt;@ActiveProfiles&lt;/span&gt; 어노테이션을 추가해주도록 하자.&lt;/p&gt;
&lt;pre id=&quot;code_1719793626633&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@TestConstructor(autowireMode = TestConstructor.AutowireMode.ALL)
@ActiveProfiles(&quot;test&quot;)
class OrderControllerTestV2 {

    @Test
    fun `재고가 존재하는 경우에는 정상 응답을 반환한다`() {
        val result = callOrderApi(1L)
        assertThat(result.statusCode())
            .isEqualTo(HttpStatus.OK.value())
    }

    @Test
    fun `재고가 존재하지 않는 경우 오류 응답을 반환한다`() {
        val result = callOrderApi(2L)
        assertThat(result.statusCode())
            .isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR.value())
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이러면 @MockBean을 사용하지 않더라도 깔끔하게 테스트 코드를 작성할 수 있으며, 테스트 시나리오에 따라서 테스트용 빈이 어떤 행위를 수행할지 설정해준다면 동일한 컨텍스트를 가지도록 재사용도 할 수 있기 때문에 테스트 성능 역시 올릴 수 있다.&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;style3&quot; /&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;  @TestConfiguration 활용해주기&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 Profile 어노테이션을 활용하는 방법도 있지만, &lt;span style=&quot;color: #ef5369;&quot;&gt;@TestConfiguration 을 활용해서도 다른 빈을 사용하도록 제어&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;기존의 코드의 경우 운영 코드에서 컴포넌트 스캔으로 인해서 `TestStockCheckApi` 가 빈으로 등록이 되었었는데, 테스트 컨텍스트 내에서만 빈으로 등록하도록 만들어보자. 먼저, @TestConfiguration을 활용하여 테스트용 빈을 선언해주자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1719793722605&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@TestConfiguration
class TestConfig {

    @Bean
    fun checkStockPort() : CheckStockPort {
        return TestStockCheckApi()
    }
}

class TestStockCheckApi: CheckStockPort {
    override fun check(productId: Long): StockStatus {
        if (productId == 1L) {
            return StockStatus.SUCCESS
        }
        return StockStatus.FAIL
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;만약 동일한 타입의 빈을 등록해줄 일이 있다면 @Primary를 활용하여 적절하게 조절하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고, 테스트 코드에서 &lt;span style=&quot;color: #ef5369;&quot;&gt;@Import를 통해서 (혹은 @ContextConfiguration을 활용해도 된다) 해당 Configuration 에 대해 로드&lt;/span&gt;를 해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1719793757995&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@TestConstructor(autowireMode = TestConstructor.AutowireMode.ALL)
//@ContextConfiguration(classes = [TestConfig::class])
@Import(TestConfig::class)
class OrderControllerTestV3 {

    @Test
    fun `재고가 존재하는 경우에는 정상 응답을 반환한다`() {
        val result = callOrderApi(1L)
        assertThat(result.statusCode())
            .isEqualTo(HttpStatus.OK.value())
    }

    @Test
    fun `재고가 존재하지 않는 경우 오류 응답을 반환한다`() {
        val result = callOrderApi(2L)
        assertThat(result.statusCode())
            .isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR.value())
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이렇게 하면 @Profile 어노테이션과 동일하게 테스트용 checkStockPort 을 사용할 수 있게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;&lt;b&gt;☁︎ @ConditionalOnProperty 활용하기&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한, &lt;span style=&quot;color: #ef5369;&quot;&gt;@TestConfiguration과 @ConditionalOnProperty&lt;/span&gt; 2가지의 어노테이션을 조합해서 다음과 같이 코드를 구성해볼 수도 있다. 대신&lt;b&gt; 조금 더 복잡도가 높아지기 때문에 그냥 이런 방법도 있다는 정도&lt;/b&gt;만 넘어가도 될 것 같다.&lt;br /&gt;먼저, 운영 코드에서 다음과 같이 `@ConditionalOnProperty` 를 활용하여 특정 프로퍼티가 존재할 때 해당 빈을 사용할 수 있도록 수정해주자.&lt;/p&gt;
&lt;pre id=&quot;code_1719880002353&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
@ConditionalOnProperty(name = [&quot;check-stock.test&quot;], havingValue = &quot;false&quot;)
class ExternalStockCheckApi: CheckStockPort {
    override fun check(productId: Long): StockStatus {
        return callExternalApi(productId)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;여기에서는 `check-stock.test` 라는 프로퍼티가 false인 경우에 운영 코드가 동작할 수 있도록 설정해주었다.&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;그리고, 테스트 코드를 조금 수정해주자. 우선, 프로퍼티 값을 사용하는 김에 해당 프로퍼티 값에 따라서 success를 응답하는 외부 API와 fail을 응답하는 외부 API 빈으로 분리하고자 한다. 이를 위해 &lt;b&gt;Conditional 인터페이스&lt;/b&gt;를 활용해줄 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1719880047871&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class CheckStockSuccessCondition : Condition {
    override fun matches(context: ConditionContext, metadata: AnnotatedTypeMetadata): Boolean {
        val environment = context.environment
        val checkStockTest = environment.getProperty(&quot;check-stock.test&quot;) == &quot;true&quot;
        val checkStockResultSuccess = environment.getProperty(&quot;check-stock.result-success&quot;) == &quot;true&quot;
        return checkStockTest &amp;amp;&amp;amp; checkStockResultSuccess
    }
}

class CheckStockFailCondition : Condition {
    override fun matches(context: ConditionContext, metadata: AnnotatedTypeMetadata): Boolean {
        val environment = context.environment
        val checkStockTest = environment.getProperty(&quot;check-stock.test&quot;) == &quot;true&quot;
        val checkStockResultFailure = environment.getProperty(&quot;check-stock.result-success&quot;) == &quot;false&quot;
        return checkStockTest &amp;amp;&amp;amp; checkStockResultFailure
    }
}&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;`check-stock.test` 프로퍼티가 true이면서, `check-stock.result-success` &lt;b&gt;프로퍼티가 true인지 false인지에 따라서 Condition을 검증&lt;/b&gt;하는 로직이다. 그리고, 다음과 같이 빈을 등록해주자.&lt;/p&gt;
&lt;pre id=&quot;code_1719880078289&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@TestConfiguration
class TestConfigV2 {

    @Bean(name = [&quot;checkStockPort&quot;])
    @Conditional(CheckStockSuccessCondition::class)
    fun checkStockPortReturnSuccess(
        environment: Environment
    ): CheckStockPort {
        return object : CheckStockPort {
            override fun check(productId: Long): StockStatus {
                return StockStatus.SUCCESS
            }
        }
    }

    @Bean(name = [&quot;checkStockPort&quot;])
    @Conditional(CheckStockFailCondition::class)
    fun checkStockPortReturnFail(
        environment: Environment
    ): CheckStockPort {
        return object : CheckStockPort {
            override fun check(productId: Long): StockStatus {
                return StockStatus.FAIL
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&amp;nbsp;둘 다 &amp;lsquo;checkStockPort&amp;rsquo; 라는 이름으로 빈 등록이 될 수 있게 네임 지정을 해주고, &lt;span style=&quot;color: #ef5369;&quot;&gt;각각의 조건이 true일 때 해당 빈을 활용&lt;/span&gt;할 수 있도록 @Conditional 어노테이션을 활용해주었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로, 테스트에서 해당 빈 정보를 활용할 수 있도록 import 하고, 프로퍼티를 주입해주자. 프로퍼티 주입을 위해서 여러 방법이 있겠지만, 나는 @SpringBootTest가 제공하는 프로퍼티 속성을 활용하여 주입해주었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;다만, 이러다 보니까 실패와 성공 케이스에 대해 각각 테스트 클래스를 분리하였다. 이런 방법도 있을 뿐이지, 실제로 활용하기에는 효용이 좋을지는 잘 모르겠다. (정말 이런 방법이 있기만 하다는 것 정도만 인지하자!)&lt;/p&gt;
&lt;pre id=&quot;code_1719880124086&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@SpringBootTest(
    webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
    properties = [&quot;check-stock.test=true&quot;, &quot;check-stock.result-success=true&quot;]
)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@TestConstructor(autowireMode = TestConstructor.AutowireMode.ALL)
@Import(TestConfigV2::class)
class OrderControllerTestV4_success {

    @Test
    fun `재고가 존재하는 경우에는 정상 응답을 반환한다`() {
        val result = callOrderApi(1L)
        assertThat(result.statusCode())
            .isEqualTo(HttpStatus.OK.value())
    }
}

@SpringBootTest(
    webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
    properties = [&quot;check-stock.test=true&quot;, &quot;check-stock.result-success=false&quot;]
)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@TestConstructor(autowireMode = TestConstructor.AutowireMode.ALL)
@Import(TestConfigV2::class)
class OrderControllerTestV4_fail {
    ...
}&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;style3&quot; /&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;  mock 객체를 활용하기&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;이때는 테스트용 빈을 등록할 때&lt;span style=&quot;color: #ef5369;&quot;&gt; 테스트 클래스가 아닌, mock 객체를 반환&lt;/span&gt;해주면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1719880200426&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@TestConfiguration
class TestConfigV3 {

    @Bean
    fun checkStockPort() : CheckStockPort {
        return Mockito.mock(CheckStockPort::class.java)
    }
}&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;만약 kotest를 활용한다면 mockk&amp;lt;CheckStockPort&amp;gt; 를 활용하여 반환해줄 수 있다.&lt;br /&gt;그리고, 테스트 코드에서 checkStockPort를 주입해주자. 이러면 위에서 선언한 mock 객체가 반환된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1719880220922&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@SpringBootTest(
    webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@TestConstructor(autowireMode = TestConstructor.AutowireMode.ALL)
@Import(TestConfigV3::class)
class OrderControllerTestV5(
    private val checkStockPort: CheckStockPort,
) {

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이 상황에서 그냥 테스트를 돌리면 어떻게 될까?&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2378&quot; data-origin-height=&quot;792&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b8N9io/btsIkhkomtP/JAdaPn3H8CdAJOlmOWRYa1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b8N9io/btsIkhkomtP/JAdaPn3H8CdAJOlmOWRYa1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b8N9io/btsIkhkomtP/JAdaPn3H8CdAJOlmOWRYa1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb8N9io%2FbtsIkhkomtP%2FJAdaPn3H8CdAJOlmOWRYa1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2378&quot; height=&quot;792&quot; data-origin-width=&quot;2378&quot; data-origin-height=&quot;792&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;mocking된 객체에 대해서 하는 일을 지정해주지 않았기 때문에 결과로 null을 반환&lt;/span&gt;하게 되며,&amp;nbsp;당연하게도 테스트도 실패한다.&lt;br /&gt;이를 위해, 각 테스트 시나리오에 맞춰 어떤 행위를 할지 지정해주도록 하자.&lt;/p&gt;
&lt;pre id=&quot;code_1719880277903&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
fun `재고가 존재하는 경우에는 정상 응답을 반환한다`() {
    Mockito.`when`(
        checkStockPort.check(Mockito.anyLong())
    ).thenReturn(StockStatus.SUCCESS)

    val result = callOrderApi(1L)
    assertThat(result.statusCode())
        .isEqualTo(HttpStatus.OK.value())
}

@Test
fun `재고가 존재하지 않는 경우 오류 응답을 반환한다`() {
    Mockito.`when`(
        checkStockPort.check(Mockito.anyLong())
    ).thenReturn(StockStatus.FAIL)

    val result = callOrderApi(2L)
    assertThat(result.statusCode())
        .isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR.value())
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;b&gt;Mockito의 When, ThenReturn 절&lt;/b&gt;을 활용하면 어떤 행위를 할지 지정해줄 수 있으며, 만약&amp;nbsp;kotest를 사용 중이라면 every - returns를 사용하여 해주면 된다.&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;style3&quot; /&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;&lt;b&gt;  마무리&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요즘 회사에서 통합 테스트를 자주 짜려고 노력 중인데, 한 번 구축하고 나니까 다음 번에 테스트를 짤 때 더 금방 짤 수 있는 것 같다.&lt;br /&gt;개인적으로는 위 4가지 방법 중에서 2번과 4번을 많이 쓰고 있다.&lt;br /&gt;2번은 슬랙 메시지나 금원 지급, 외부 API를 호출해야 하는 상황 등등, 테스트에서 반드시 호출이 되지 않을 필요가 있거나 고정된 특정한 응답이 필요한 경우 사용하고, 4번은 외부 API이지만 해당 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;요즘 테스트 속도에 대해서도 고민이 많아서, 아예 테스트용 빈을 관리하는 @TestConfiguration 클래스를 생성한 다음에, 모든 클래스에서 재사용할 수 있도록 만들어서 속도를 개선해볼까도 고민 중이다. 또한, 개인적으로는 kotest를 선호하는 편이라서 mockk와 함께 조합해서 많이 사용하고 있다.&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>개발일지</category>
      <category>jUnit</category>
      <category>MockBean</category>
      <category>spring</category>
      <category>통합테스트</category>
      <author>dolmeng2</author>
      <guid isPermaLink="true">https://cl8d.tistory.com/131</guid>
      <comments>https://cl8d.tistory.com/131#entry131comment</comments>
      <pubDate>Sun, 16 Jun 2024 19:27:50 +0900</pubDate>
    </item>
    <item>
      <title>[MySQL] Replication을 활용하여 Master-Slave DB 분리하기 (1)</title>
      <link>https://cl8d.tistory.com/130</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&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;사내에서 Replication을 활용하고 있다는 건 알고 있는데, 구체적으로 어떤 식으로 동작하는지는 제대로 이해하지 못하고 있었던 것 같아서 간단 하게 MySQL을 활용하여 Replication이 어떤 식으로 동작하는지 구축해보았다.&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&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;기본적으로&amp;nbsp;&lt;span style=&quot;color: #ef5369;&quot;&gt;master&amp;nbsp;노드에서&amp;nbsp;데이터가&amp;nbsp;변경되었을&amp;nbsp;때&amp;nbsp;slave&amp;nbsp;노드에도&amp;nbsp;변경된&amp;nbsp;내용을&amp;nbsp;적용&lt;/span&gt;시키는&amp;nbsp;것을&amp;nbsp;의미하며,&amp;nbsp;MySQL에서는&amp;nbsp;기본적으로&amp;nbsp;비동기&amp;nbsp;복제&amp;nbsp;방식을&amp;nbsp;활용하여&amp;nbsp;둘&amp;nbsp;사이의&amp;nbsp;데이터&amp;nbsp;정합성을&amp;nbsp;맞추고&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-05-15 오후 5.50.34.png&quot; data-origin-width=&quot;2762&quot; data-origin-height=&quot;1130&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/s2LXS/btsHqdcEQrW/JVzfKMRfXhCWX0fECrKTH0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/s2LXS/btsHqdcEQrW/JVzfKMRfXhCWX0fECrKTH0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/s2LXS/btsHqdcEQrW/JVzfKMRfXhCWX0fECrKTH0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fs2LXS%2FbtsHqdcEQrW%2FJVzfKMRfXhCWX0fECrKTH0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2762&quot; height=&quot;1130&quot; data-filename=&quot;스크린샷 2024-05-15 오후 5.50.34.png&quot; data-origin-width=&quot;2762&quot; data-origin-height=&quot;1130&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;0. 클라이언트가 커밋을 진행한다. (데이터베이스에 데이터 저장 요청)&lt;br /&gt;1. Connection Thread는 스토리지 엔진에게 해당 트랜잭션에 대한 &lt;b&gt;Prepare&lt;/b&gt;를 진행한다.&lt;br /&gt;- 해당 단계는 Commit 을 실제로 진행하는 단계가 아닌, &amp;lsquo;준비&amp;rsquo;를 하는 단계이다. (2PC, 2 Phase Commit)&lt;br /&gt;-&amp;nbsp;이&amp;nbsp;시점에&amp;nbsp;(InnoDB의&amp;nbsp;경우)&amp;nbsp;Redo&amp;nbsp;Log를&amp;nbsp;기록하게&amp;nbsp;된다.&lt;br /&gt;- 만약 이 단계에서 서버가 다운되면, 복구할 때 해당 트랜잭션은 롤백된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. Commit 을 진행하기 전, &lt;span style=&quot;color: #ef5369;&quot;&gt;바이너리 로그에 변경사항을 기록&lt;/span&gt;한다.&lt;br /&gt;3. 스토리지 엔진은 변경 사항에 대해 커밋을 완료한다.&lt;br /&gt;- 이 시점까지 정상적으로 완료되면, 트랜잭션은 커밋이 완료되어 바이너리 로그와 (InnoDB라면) 리두 로그까지 존재하게 된다.&lt;br /&gt;4-5.&amp;nbsp;Master&amp;nbsp;Thread는&amp;nbsp;&lt;b&gt;비동기적으로&amp;nbsp;바이너리&amp;nbsp;로그를&amp;nbsp;읽어서&lt;/b&gt;,&amp;nbsp;Slave로&amp;nbsp;전송한다.&lt;br /&gt;6. Slave의 I/O 스레드는 수신한 변경 데이터를 릴레이 로그에 기록한다.&lt;br /&gt;7-8.&amp;nbsp;Slave의&amp;nbsp;SQL&amp;nbsp;스레드는&amp;nbsp;릴레이&amp;nbsp;로그에&amp;nbsp;기록된&amp;nbsp;데이터를&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&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;  실습 준비&amp;nbsp; - 마스터 서버 구축하기&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단하게 실습을 진행하기 위해, docker를 활용하여 mySQL 서버를 띄워주었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 먼저 master 서버를 띄워주게 될 건데, mySQL의 설정 파일을 수정할 필요가 있기 때문에 my.cnf 라는 파일을 생성하여 다음과 같이 작성해두자.&lt;/p&gt;
&lt;pre id=&quot;code_1715763268929&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[mysqld]
log-bin=mysql-bin    
server-id=1          
binlog-do-db=practice  
authentication_policy=mysql_native_password&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;mysqld&lt;/b&gt;]: MySQL 서버의 프로퍼티를 지정하기 위한 시작 prefix 값&lt;br /&gt;-&amp;nbsp;&lt;b&gt;log-bin&lt;/b&gt;&amp;nbsp;:&amp;nbsp;바이너리&amp;nbsp;로깅을&amp;nbsp;활성화하고,&amp;nbsp;로그&amp;nbsp;파일의&amp;nbsp;이름을&amp;nbsp;지정하는&amp;nbsp;것.&amp;nbsp;테이블에&amp;nbsp;대한&amp;nbsp;모든&amp;nbsp;변경사항을&amp;nbsp;로그&amp;nbsp;파일로&amp;nbsp;기록하게&amp;nbsp;된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로&amp;nbsp;mySQL&amp;nbsp;에서&amp;nbsp;replication을&amp;nbsp;진행할&amp;nbsp;때&amp;nbsp;&lt;span style=&quot;color: #ef5369;&quot;&gt;바이너리&amp;nbsp;로그&amp;nbsp;파일에&amp;nbsp;대한&amp;nbsp;내용&lt;/span&gt;을&amp;nbsp;기준으로&amp;nbsp;복제를&amp;nbsp;진행하기&amp;nbsp;때문에&amp;nbsp;로깅을&amp;nbsp;활성화하게&amp;nbsp;된다.&lt;br /&gt;-&amp;nbsp;&lt;b&gt;server-id&lt;/b&gt;:&amp;nbsp;MySQL&amp;nbsp;서버에&amp;nbsp;대한&amp;nbsp;고유&amp;nbsp;아이디&amp;nbsp;값으로,&amp;nbsp;추후&amp;nbsp;구축할&amp;nbsp;slave&amp;nbsp;용&amp;nbsp;서버와&amp;nbsp;&lt;span style=&quot;color: #ef5369;&quot;&gt;꼭&amp;nbsp;다르게&amp;nbsp;설정을&amp;nbsp;해줘야&amp;nbsp;하는&amp;nbsp;값&lt;/span&gt;이다.&amp;nbsp;복제된&amp;nbsp;프로세스들&amp;nbsp;사이에서&amp;nbsp;각&amp;nbsp;mySQL&amp;nbsp;서버&amp;nbsp;프로세스를&amp;nbsp;구분하는데&amp;nbsp;사용하는&amp;nbsp;값이다.&lt;br /&gt;-&amp;nbsp;&lt;b&gt;binlog-do-db&lt;/b&gt;:&amp;nbsp;바이너리&amp;nbsp;로깅을&amp;nbsp;특정한&amp;nbsp;데이터베이스에서만&amp;nbsp;사용할&amp;nbsp;수&amp;nbsp;있도록&amp;nbsp;지정하는&amp;nbsp;것이다.&amp;nbsp;여기에서는&amp;nbsp;추후&amp;nbsp;만들&amp;nbsp;&amp;lsquo;practice&amp;rsquo;&amp;nbsp;라는&amp;nbsp;데이터베이스에&amp;nbsp;대해서만&amp;nbsp;로깅을&amp;nbsp;허용하도록&amp;nbsp;지정해주었다.&lt;br /&gt;-&amp;nbsp;&lt;b&gt;authentication_policy&lt;/b&gt;&amp;nbsp;:&amp;nbsp;MySQL&amp;nbsp;8.0부터는&amp;nbsp;SHA-256&amp;nbsp;Hashing을&amp;nbsp;구현하기&amp;nbsp;위하여&amp;nbsp;2가지의&amp;nbsp;플러그인을&amp;nbsp;제공하고&amp;nbsp;있는데,&amp;nbsp;기본적으로&amp;nbsp;caching_sha2_password&amp;nbsp;라는&amp;nbsp;캐싱을&amp;nbsp;활용한&amp;nbsp;플러그인을&amp;nbsp;활용하고&amp;nbsp;있다.&amp;nbsp;이때&amp;nbsp;이를&amp;nbsp;사용하기&amp;nbsp;위해서&amp;nbsp;SSL&amp;nbsp;보안&amp;nbsp;연결이나&amp;nbsp;RSA&amp;nbsp;보안을&amp;nbsp;적용해야&amp;nbsp;하는데,&amp;nbsp;꽤나&amp;nbsp;번거롭기&amp;nbsp;때문에&amp;nbsp;구버전&amp;nbsp;플러그인인&amp;nbsp;mysql_native_password을&amp;nbsp;사용하도록&amp;nbsp;설정하는&amp;nbsp;것이다.&amp;nbsp;(이&amp;nbsp;부분을&amp;nbsp;빼먹으면&amp;nbsp;나중에&amp;nbsp;통신이&amp;nbsp;안&amp;nbsp;된다&amp;hellip;)&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;이러한 파일을 생성해주었다면, 설정 파일을 적용한 Dockerfile을 만들어 주자.&lt;/p&gt;
&lt;pre id=&quot;code_1715763403442&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;FROM mysql:8.0

COPY ./my.cnf /etc/my.cnf&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;mySQL 8.0 버전을 사용하였으며, 위에서 만들었던 my.cnf 파일을 설정 파일로 활용하도록 만들기 위해 COPY를 통해 내부 설정 파일을 덮어씌우도록 만들어주었다.&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;그리고, 만든 Dockerfile을 활용하여 도커 인스턴스를 띄울 수 있도록 docker-compose를 작성해주도록 하자.&lt;/p&gt;
&lt;pre id=&quot;code_1715763465462&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;version: '3'
services:
  mysql:
    build: .
    container_name: mysql
    environment:
      MYSQL_DATABASE: practice
      MYSQL_ROOT_PASSWORD: test
    ports:
      - 3306:3306 # HOST:CONTAINER
    volumes:
      - ./mysql/data:/var/lib/mysql
    networks:
      - practice-net

networks:
  practice-net:
    driver: bridge&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;-&amp;nbsp;image&amp;nbsp;대신에&amp;nbsp;build를&amp;nbsp;활용하여&amp;nbsp;현재&amp;nbsp;디렉터리&amp;nbsp;(.)에&amp;nbsp;있는&amp;nbsp;Dockerfile을&amp;nbsp;활용하여&amp;nbsp;빌드할&amp;nbsp;수&amp;nbsp;있돌고&amp;nbsp;설정해주었다.&lt;br /&gt;- 또한, &lt;span style=&quot;color: #ef5369;&quot;&gt;slave와 동일한 네트워크를 사용하도록 만들어줘야 하기 때문에&lt;/span&gt; 커스텀하게 네트워크를 정의해주었다. (요것도 설정 안 해주면 나중에 통신이 안 된다&amp;hellip; 꼭 설정해두도록 하자.)&lt;br /&gt;-&amp;nbsp;참고로,&amp;nbsp;네트워크&amp;nbsp;이름은&amp;nbsp;현재&amp;nbsp;위치한&amp;nbsp;{패키지&amp;nbsp;이름_설정한&amp;nbsp;이름}&amp;nbsp;과&amp;nbsp;같이&amp;nbsp;만들어지기&amp;nbsp;때문에,&amp;nbsp;나의&amp;nbsp;경우&amp;nbsp;네트워크&amp;nbsp;이름이&amp;nbsp;master-practice-net으로&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;다 만들었다면, 컨테이너를 띄워주자. -d를 통해 백그라운드로 띄울 수 있도록 만들었다.&lt;/p&gt;
&lt;pre id=&quot;code_1715763529473&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;docker-compose -f docker-compose.yml up -d&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정상적으로 되었는지 확인하기 위해, 컨테이너 내부로 접속하여 설정 파일이 잘 적용되었는지 확인해보자.&lt;/p&gt;
&lt;pre id=&quot;code_1715763567329&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;docker exec -it {containerID} bash&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;682&quot; data-origin-height=&quot;246&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vn9KM/btsHrxnrqqA/F77yweXzH4Hhj21w4bKIh1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vn9KM/btsHrxnrqqA/F77yweXzH4Hhj21w4bKIh1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vn9KM/btsHrxnrqqA/F77yweXzH4Hhj21w4bKIh1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fvn9KM%2FbtsHrxnrqqA%2FF77yweXzH4Hhj21w4bKIh1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;621&quot; height=&quot;224&quot; data-origin-width=&quot;682&quot; data-origin-height=&quot;246&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;mySQL 버전에 따라 다를 테지만, 나의 경우 /etc 하위에 설정 파일이 기본적으로 존재하였으며, 파일 내용이 위와 같이 잘 변경되어 있는 것을 확인할 수 있었다.&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;참고로, 나의 경우 이것저것 삽질하면서 시간을 많이 보냈었는데 혹시나 설정을 잘못하여 my.cnf 파일을 수정해야 하는 일이 발생했다면, 꼭 빌드 시 no-cache 옵션을 설정해두도록 하자. 아니면 캐시로 인해서 적용이 늦게 되었는데 왜 안 되지? 라며 시간을 보내는 일이 생길 수도 있다...&lt;/p&gt;
&lt;pre id=&quot;code_1715763682739&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;docker-compose build --no-cache&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;style2&quot; /&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;  실습 준비&amp;nbsp; - 슬레이브 서버 구축하기&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 비슷한 방법으로 슬레이브 서버도 구축하고자 한다. 설정 파일은 다음과 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1715763739771&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[mysqld]
log-bin=mysql-bin    
server-id=2  
relay_log=/var/lib/mysql/mysql-relay-bin
log_replica_updates='ON'
read_only='ON'
authentication_policy=mysql_native_password&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;-&amp;nbsp;앞서&amp;nbsp;말했던&amp;nbsp;것처럼,&amp;nbsp;server-id의&amp;nbsp;값은&amp;nbsp;&lt;span style=&quot;color: #ef5369;&quot;&gt;꼭&amp;nbsp;마스터&amp;nbsp;서버와&amp;nbsp;다르게&amp;nbsp;설정&lt;/span&gt;해줘야&amp;nbsp;한다.&lt;br /&gt;-&amp;nbsp;&lt;b&gt;relay_log&lt;/b&gt;:&amp;nbsp;&lt;span style=&quot;color: #ef5369;&quot;&gt;릴레이&amp;nbsp;로그&lt;/span&gt;의&amp;nbsp;위치를&amp;nbsp;지정한다.&lt;br /&gt;-&amp;nbsp;&lt;b&gt;log_replica_updates&lt;/b&gt;:&amp;nbsp;릴레이&amp;nbsp;로그를&amp;nbsp;활용하여&amp;nbsp;자신의&amp;nbsp;데이터베이스에&amp;nbsp;적용하는&amp;nbsp;모든&amp;nbsp;변경&amp;nbsp;사항을&amp;nbsp;바이너리&amp;nbsp;로그에도&amp;nbsp;기록한다.&lt;br /&gt;-&amp;nbsp;&lt;b&gt;read_only&lt;/b&gt;:&amp;nbsp;슬레이브&amp;nbsp;서버는&amp;nbsp;읽기&amp;nbsp;전용으로&amp;nbsp;설정한다.&amp;nbsp;(마스터&amp;nbsp;서버에서만&amp;nbsp;쓰기&amp;nbsp;연산이&amp;nbsp;발생하도록&amp;nbsp;만들기&amp;nbsp;위해&amp;nbsp;설정해준다.)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;-&amp;nbsp;참고로,&amp;nbsp;super&amp;nbsp;권한을&amp;nbsp;가진&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;다음으로 docker-compose 도 만들어주자. 포트와 컨테이너 이름은 다르게, 그리고 네트워크 정보는 꼭 동일하게 설정해주자.&lt;/p&gt;
&lt;pre id=&quot;code_1715763845777&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;version: '3'
services:
  mysql:
    build: .
    container_name: mysql-slave
    environment:
      MYSQL_DATABASE: practice
      MYSQL_ROOT_PASSWORD: test
    ports:
      - 3307:3306 # HOST:CONTAINER
    volumes:
      - ./mysql/data:/var/lib/mysql
    networks:
      - practice-net

networks:
  practice-net:
    external: true
    name: master_practice-net&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;그리고, 컨테이너에 접속한 다음 실제로 read_only 같은 옵션이 잘 설정되었는지 확인해보자.&lt;/p&gt;
&lt;pre id=&quot;code_1715763864270&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;show global variables like '%read_only%';&lt;/code&gt;&lt;/pre&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;2000&quot; data-origin-height=&quot;1146&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bO6dHZ/btsHpLU03JD/GzzNkRjuVUNseXTzFZmJq0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bO6dHZ/btsHpLU03JD/GzzNkRjuVUNseXTzFZmJq0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bO6dHZ/btsHpLU03JD/GzzNkRjuVUNseXTzFZmJq0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbO6dHZ%2FbtsHpLU03JD%2FGzzNkRjuVUNseXTzFZmJq0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;303&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;1146&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;  연동하기&amp;nbsp;&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;/b&gt;&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Replication 에서는 Slave Thread가 Master Thread 쪽으로 접속을 요청하기 때문에, &lt;span style=&quot;color: #ef5369;&quot;&gt;Slave가 일종의 클라이언트이고, Master가 서버라고 볼 수 있다&lt;/span&gt;. 이때 &lt;b&gt;Master에는 Slave가 로그인을 하기 위한 계정&lt;/b&gt;과 권한 (Replication Slave)가 필요하다. 마스터 서버에 접속하여 아래와 같이 계정을 생성하고 권한을 부여해주자.&lt;/p&gt;
&lt;pre id=&quot;code_1715763928911&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;mysql -u root -p
create user 'test-replication'@'%' identified by 'test';
grant replication slave on *.* to 'test-replication'@'%';
flush privileges;&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;권한이&amp;nbsp;정상적으로&amp;nbsp;잘&amp;nbsp;부여되었는지&amp;nbsp;한&amp;nbsp;번&amp;nbsp;확인하고&amp;nbsp;넘어가주자.&lt;/p&gt;
&lt;pre id=&quot;code_1715763945256&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;use mysql;
select user, host from user;
show grants for 'test-replication'@'%';&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;1766&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bMHQz2/btsHqbTtrhi/gXZ9BlKnnPI3PGjPimIwW1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bMHQz2/btsHqbTtrhi/gXZ9BlKnnPI3PGjPimIwW1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bMHQz2/btsHqbTtrhi/gXZ9BlKnnPI3PGjPimIwW1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbMHQz2%2FbtsHqbTtrhi%2FgXZ9BlKnnPI3PGjPimIwW1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;599&quot; height=&quot;529&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;1766&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;위와 같이 slave 권한이 잘 부여된 것을 확인할 수 있다.&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;연동을&amp;nbsp;위하여&amp;nbsp;마스터&amp;nbsp;서버의&amp;nbsp;바이너리&amp;nbsp;로깅&amp;nbsp;상태를&amp;nbsp;확인해보자.&lt;/p&gt;
&lt;pre id=&quot;code_1715764014907&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;show master status;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;457&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bRcVN3/btsHq0cqa59/cTPM8BD5SgzZEJLZQq3Qxk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bRcVN3/btsHq0cqa59/cTPM8BD5SgzZEJLZQq3Qxk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bRcVN3/btsHq0cqa59/cTPM8BD5SgzZEJLZQq3Qxk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbRcVN3%2FbtsHq0cqa59%2FcTPM8BD5SgzZEJLZQq3Qxk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2000&quot; height=&quot;457&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;457&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;b&gt;File&lt;/b&gt;:&amp;nbsp;현재&amp;nbsp;사용&amp;nbsp;중인&amp;nbsp;바이너리&amp;nbsp;로그&amp;nbsp;파일의&amp;nbsp;이름이다.&lt;br /&gt;-&amp;nbsp;&lt;b&gt;Position&lt;/b&gt;:&amp;nbsp;바이너리&amp;nbsp;로그&amp;nbsp;파일&amp;nbsp;내의&amp;nbsp;오프셋이며,&amp;nbsp;&lt;span style=&quot;color: #ef5369;&quot;&gt;마지막으로&amp;nbsp;복제를&amp;nbsp;완료한&amp;nbsp;지점&lt;/span&gt;에&amp;nbsp;대해&amp;nbsp;나타낸다.&lt;br /&gt;-&amp;nbsp;&lt;b&gt;Binlog_Do_DB&lt;/b&gt;:&amp;nbsp;바이너리&amp;nbsp;로깅이&amp;nbsp;활성화된&amp;nbsp;데이터베이스이다.&amp;nbsp;우리가&amp;nbsp;앞서&amp;nbsp;마스터&amp;nbsp;서버를&amp;nbsp;구축해주었을&amp;nbsp;때&amp;nbsp;practice&amp;nbsp;데이터베이스에&amp;nbsp;대해&amp;nbsp;지정했던&amp;nbsp;것이&amp;nbsp;여기에&amp;nbsp;반영된&amp;nbsp;것이다.&lt;br /&gt;-&amp;nbsp;&lt;b&gt;Binlog_Ignore_DB&lt;/b&gt;:&amp;nbsp;바이너리&amp;nbsp;로깅에서&amp;nbsp;제외된&amp;nbsp;데이터베이스이다.&lt;br /&gt;-&amp;nbsp;&lt;b&gt;Executed_Gtid_set&lt;/b&gt;: GTID (Global Transaction Identifier), 해당 서버에서 실행된 모든 트렌잭션의 GTID를 의미한다. 커밋된 트랜잭션들이 가지게 되는 고유한 식별자이다. (마스터-슬레이브 모두 동일하게 사용)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; 기본적으로 트랜잭션이 커밋되면 GTID를 할당받아 마스터의 바이너리 로그에 기록이  된다. 이때, 슬레이브가 바이너리 로그에 있던 내용을 릴레이 로그에 저장하면, &lt;b&gt;GTID 정보를 토대로&lt;/b&gt; 해당 트랜잭션이 아직 미반영된 상태라면 반영하게 된다. 그리고 슬레이브의 바이너리 로그에도 기록하게 된다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;✔️ 연동하기&amp;nbsp; -  마스터 서버의 IP 주소 알아내기&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;여기까지 진행했다면 이제 두 서버에 대해 연결을 해줄 차례이다. 먼저, 마스터 서버의 컨테이너 IP 주소를 확인해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는, &lt;span style=&quot;color: #ef5369;&quot;&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;/p&gt;
&lt;pre id=&quot;code_1715764197834&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;docker inspect {container_name} | grep Network&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;315&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b1TVas/btsHqo53Dao/7UAcO5cesaGlcixkGmdWNK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b1TVas/btsHqo53Dao/7UAcO5cesaGlcixkGmdWNK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b1TVas/btsHqo53Dao/7UAcO5cesaGlcixkGmdWNK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb1TVas%2FbtsHqo53Dao%2F7UAcO5cesaGlcixkGmdWNK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2000&quot; height=&quot;315&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;315&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;327&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pm0lE/btsHqOQKKSO/6bfv5LYGMnFpOdtBnodlYk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pm0lE/btsHqOQKKSO/6bfv5LYGMnFpOdtBnodlYk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pm0lE/btsHqOQKKSO/6bfv5LYGMnFpOdtBnodlYk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fpm0lE%2FbtsHqOQKKSO%2F6bfv5LYGMnFpOdtBnodlYk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2000&quot; height=&quot;327&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;327&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;만약 위의 캡쳐 화면처럼 마스터 서버와 슬레이브 서버의 네트워크 정보가 다르다면, 아래와 같은 방법으로 수정해줄 수 있다. (물론 재생성을 해도 괜찮다)&lt;/p&gt;
&lt;pre id=&quot;code_1715764274402&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// mysql-slave 컨테이너에 대해 slave_default 네트워크 끊어주기
docker network disconnect slave_default mysql-slave

// 대신 master_default 네트워크를 연결해주기
docker network connect master_default mysql-slave&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1316&quot; data-origin-height=&quot;236&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/YfSVH/btsHpMfi355/YGO9JfyRqT9FqWCrGCjTwK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/YfSVH/btsHpMfi355/YGO9JfyRqT9FqWCrGCjTwK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YfSVH/btsHpMfi355/YGO9JfyRqT9FqWCrGCjTwK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYfSVH%2FbtsHpMfi355%2FYGO9JfyRqT9FqWCrGCjTwK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1316&quot; height=&quot;236&quot; data-origin-width=&quot;1316&quot; data-origin-height=&quot;236&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;그럼 위와 같이 master 컨테이너와 동일한 네트워크 아이디를 할당받은 것을 볼 수 있다.&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;위에서 확인한 네트워크 아이디를 바탕으로, 전체 docker 네트워크 목록을 확인하여 어떤 네트워크에 연결되어 있는지 체크하자.&amp;nbsp; &amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1715764335136&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;docker network ls&lt;/code&gt;&lt;/pre&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-05-15 오후 6.13.59.png&quot; data-origin-width=&quot;2586&quot; data-origin-height=&quot;850&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/opb2M/btsHqJBWYjy/lDPHpe4TnpHawC1q9Mg1h0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/opb2M/btsHqJBWYjy/lDPHpe4TnpHawC1q9Mg1h0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/opb2M/btsHqJBWYjy/lDPHpe4TnpHawC1q9Mg1h0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fopb2M%2FbtsHqJBWYjy%2FlDPHpe4TnpHawC1q9Mg1h0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;760&quot; height=&quot;250&quot; data-filename=&quot;스크린샷 2024-05-15 오후 6.13.59.png&quot; data-origin-width=&quot;2586&quot; data-origin-height=&quot;850&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;앞서 봤던 네트워크 아이디가 95dc~ 로 시작한 것을 볼 수 있으니, 현재 연결된 네트워크 이름이 &amp;lsquo;master_default&amp;rsquo; 인 것을 확인할 수 있었다. 해당 네트워크의 IP 주소를 알아보기 위해 아래와 같이 다시 입력해주자.&lt;/p&gt;
&lt;pre id=&quot;code_1715764553816&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;docker inspect {network_name} | grep IPv4Address&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1318&quot; data-origin-height=&quot;82&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pcyot/btsHquLLDxa/f15G2PfB2RXw8k4DLAv2dK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pcyot/btsHquLLDxa/f15G2PfB2RXw8k4DLAv2dK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pcyot/btsHquLLDxa/f15G2PfB2RXw8k4DLAv2dK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fpcyot%2FbtsHquLLDxa%2Ff15G2PfB2RXw8k4DLAv2dK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1318&quot; height=&quot;82&quot; data-origin-width=&quot;1318&quot; data-origin-height=&quot;82&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;style8&quot; /&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;✔️ 연동하기&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;이제 IP 주소도 알게 되었으니, &lt;span style=&quot;color: #ef5369;&quot;&gt;슬레이브 서버에 접속&lt;/span&gt;하여 아래와 같은 명령어를 입력해주자.&lt;/p&gt;
&lt;pre id=&quot;code_1715764577171&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CHANGE MASTER TO MASTER_HOST='172.29.0.2', MASTER_PORT=3306, MASTER_USER='test-replication', MASTER_PASSWORD='test', MASTER_LOG_FILE='mysql-bin.000003', MASTER_LOG_POS=880, GET_MASTER_PUBLIC_KEY=1;&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;- &lt;b&gt;MASTER_HOST / MASTER_POR&lt;/b&gt;T: 마스터 서버의 IP 주소 / Port로, &lt;b&gt;슬레이브 서버가 마스터 서버에 접속하기 위해 알아야 하는&lt;/b&gt; IP 정보와 포트 정보이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;b&gt;MASTER_USER / MASTER_PASSWORD&lt;/b&gt;: 이전에 &lt;span style=&quot;color: #ef5369;&quot;&gt;마스터 서버에서 생성했던 슬레이브 서버용 계정 정보&lt;/span&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;b&gt;MASTER_LOG_FILE / MASTER_LOG_POST&lt;/b&gt;: 앞서 마스터 서버의 바이너리 로그 파일 정보에서 봤던 &lt;b&gt;파일 이름과 오프셋 정보&lt;/b&gt;이다. (어디서부터 읽어올 것인지 정하는 것)&lt;br /&gt;- &lt;b&gt;GET_MASTER_PUBLIC_KEY=1&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;
&lt;p data-ke-size=&quot;size16&quot;&gt;연동을 시작해주기 위해 start slave를 입력해주자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;재구성하고 싶을 때는 &lt;b&gt;stop replica &amp;gt; reset slave&lt;/b&gt;;를 활용해주면 된다. &amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1715764867943&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;start slave;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;마지막으로 연동을 확인하기 위해 아래의 명령어를 입력해보자.&lt;/p&gt;
&lt;pre id=&quot;code_1715764886511&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; show slave status\G&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;여기에서 Slave_IO_Running, Slave_SQL_Running의 값이 모두 Yes로 표시가 되어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 Slave_IO_Running가 Connecting으로 뜬다면, 아래로 내려 Last_IO_Error 항목을 보자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;870&quot; data-origin-height=&quot;510&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dDxXqM/btsHqzlZpK3/9qQE6C3iXUxu2kk8Kg41g0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dDxXqM/btsHqzlZpK3/9qQE6C3iXUxu2kk8Kg41g0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dDxXqM/btsHqzlZpK3/9qQE6C3iXUxu2kk8Kg41g0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdDxXqM%2FbtsHqzlZpK3%2F9qQE6C3iXUxu2kk8Kg41g0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;641&quot; height=&quot;376&quot; data-origin-width=&quot;870&quot; data-origin-height=&quot;510&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1510&quot; data-origin-height=&quot;188&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cJP4Ng/btsHqoruXky/f2xnScXr8R5EjhKJO3HER0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cJP4Ng/btsHqoruXky/f2xnScXr8R5EjhKJO3HER0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cJP4Ng/btsHqoruXky/f2xnScXr8R5EjhKJO3HER0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcJP4Ng%2FbtsHqoruXky%2Ff2xnScXr8R5EjhKJO3HER0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1510&quot; height=&quot;188&quot; data-origin-width=&quot;1510&quot; data-origin-height=&quot;188&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;나 같은 경우는 초기에 network 설정을 제대로 안 해줘서 이렇게 통신이 실패했었다.&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;2000&quot; data-origin-height=&quot;207&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yr3ux/btsHpPb2NSq/hhHc3pMY4MTd4yX2llV1x0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yr3ux/btsHpPb2NSq/hhHc3pMY4MTd4yX2llV1x0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yr3ux/btsHpPb2NSq/hhHc3pMY4MTd4yX2llV1x0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fyr3ux%2FbtsHpPb2NSq%2FhhHc3pMY4MTd4yX2llV1x0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2000&quot; height=&quot;207&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;207&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;혹은, 이런 식으로 Authentication 오류가 발생했다면, 초기에 mysql 설정 파일에서 default_authentication_plugin 값을 지정해주지 않았을 확률이 높다&amp;hellip; 왜 안 되는지 나도 알고 싶지 않았지만 이 2가지 오류 때문에 시간을 많이 썼다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;슬레이브 서버에서 마스터 서버 연동 시 GET_MASTER_PUBLIC_KEY 이 옵션을 빼먹었을 확률도 있다&amp;hellip;! 잘 확인해보자.&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;br /&gt;최종적으로 제대로 되었다면 아래와 같이 둘 다 Yes로 뜰 것이다.&lt;br /&gt;호스트 주소가 이전과 다른 이유는 오류가 꽤 여러 번 떴었어서 다시 만드느라 바뀌었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;1147&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/czxkp3/btsHp9H6FVJ/19LD3NzeN3AWzaLiGVNLvk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/czxkp3/btsHp9H6FVJ/19LD3NzeN3AWzaLiGVNLvk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/czxkp3/btsHp9H6FVJ/19LD3NzeN3AWzaLiGVNLvk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fczxkp3%2FbtsHp9H6FVJ%2F19LD3NzeN3AWzaLiGVNLvk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;589&quot; height=&quot;338&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;1147&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;br /&gt;마스터에서도 슬레이브의 접속 정보를 확인할 수 있는데, 아래와 같이 입력해보자.&lt;/p&gt;
&lt;pre id=&quot;code_1715765013059&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;show processlist\G&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;스크린샷 2024-05-15 오후 6.24.33.png&quot; data-origin-width=&quot;1572&quot; data-origin-height=&quot;1672&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bMrYHn/btsHqa77wez/CsD891UL4nQfNBxwRRsoXK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bMrYHn/btsHqa77wez/CsD891UL4nQfNBxwRRsoXK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bMrYHn/btsHqa77wez/CsD891UL4nQfNBxwRRsoXK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbMrYHn%2FbtsHqa77wez%2FCsD891UL4nQfNBxwRRsoXK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;587&quot; height=&quot;624&quot; data-filename=&quot;스크린샷 2024-05-15 오후 6.24.33.png&quot; data-origin-width=&quot;1572&quot; data-origin-height=&quot;1672&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;가장 아래에 우리가 연동했던 test-replication 이라는 친구에 대해 State로 &amp;lsquo;source가 모든 binlog를 replication을 보냈다&amp;rsquo; 라는 문구를 확인할 수 있다!&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;style3&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&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;pre id=&quot;code_1715765119838&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;use practice;

create table test(
    id bigint not null primary key,
    name varchar(100)
) engine = 'INNODB';

insert into test (id, name) values (1, 'test');&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;insert 후 마스터 서버의 모습이다.&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;2000&quot; data-origin-height=&quot;554&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b7ZaOo/btsHpmH8jYy/zVxpGp51cRPRIjxtiIp9u1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b7ZaOo/btsHpmH8jYy/zVxpGp51cRPRIjxtiIp9u1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b7ZaOo/btsHpmH8jYy/zVxpGp51cRPRIjxtiIp9u1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb7ZaOo%2FbtsHpmH8jYy%2FzVxpGp51cRPRIjxtiIp9u1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;170&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;554&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;그리고, 슬레이브 서버에 접속하여 해당 데이터가 잘 복제되었는지 확인해보자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;657&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/boTTFx/btsHq46Th9L/XXTqfnhx8xkJfzKZ6oAxh1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/boTTFx/btsHq46Th9L/XXTqfnhx8xkJfzKZ6oAxh1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/boTTFx/btsHq46Th9L/XXTqfnhx8xkJfzKZ6oAxh1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FboTTFx%2FbtsHq46Th9L%2FXXTqfnhx8xkJfzKZ6oAxh1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;529&quot; height=&quot;174&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;657&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;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&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;이번 포스팅에서는 mySQL을 활용하여 간단하게 master &amp;lt;-&amp;gt; slave 간 replication 을 만들어보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에서는 고려해야 하는 점들이 훨씬 더 많겠지만, master의 내용이 알아서 slave로 복제되는 모습을 직접 보니까 굉장히 신기했다.&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 포스팅이 언제가 될지 모르겠지만... 시간 내서 2편도 작성해봐야겠다. 끝!&lt;/p&gt;</description>
      <category>개발일지</category>
      <category>master</category>
      <category>mysql</category>
      <category>Replication</category>
      <category>Slave</category>
      <category>복제</category>
      <author>dolmeng2</author>
      <guid isPermaLink="true">https://cl8d.tistory.com/130</guid>
      <comments>https://cl8d.tistory.com/130#entry130comment</comments>
      <pubDate>Wed, 15 May 2024 18:29:54 +0900</pubDate>
    </item>
    <item>
      <title>[Intellij] 안전한 리팩터링 진행하기 - Intellij를 활용한 점진적 리팩터링</title>
      <link>https://cl8d.tistory.com/129</link>
      <description>&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;나는 평소에 마우스는 사용하지 않지만, 맥북 터치패드를 정말 많이 사용하는 편이었어서 개발할 때 단축키를 많이 사용하지 않는 편이었다. 그러다 보니 자동으로 개발할 때 미묘하게 속도 차이가 났었는데, 이번에 단축키도 공부할겸, 리팩터링 시 어떻게 하면 Intellij 를 최대한 활용할 수 있는지 스터디 하는 시간을 가졌다. (갓나니개발자님 덕분에 많이 배울 수 있는 시간이었다 ㅎ_ㅎ)&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;이번 포스팅에서 사용된 샘플 코드는 백명석 님의 깃허브를 가면 확인할 수 있는데, 나는 사내에서 코틀린을 사용하고 있다 보니까 해당 코드를 코틀린 + Kotest로 변환하여 연습을 해보았다.&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;figure id=&quot;og_1708834977278&quot; style=&quot;color: #333333; text-align: start;&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cubGLS/hyVqsTwrU4/t2Rt5y1U7wzQ1takmtkoAk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot; data-og-url=&quot;https://github.com/Cl8D/kotlin-expense&quot; data-og-source-url=&quot;https://github.com/Cl8D/kotlin-expense&quot; data-og-host=&quot;github.com&quot; data-og-description=&quot;rafactoring practice. Contribute to Cl8D/kotlin-expense development by creating an account on GitHub.&quot; data-og-title=&quot;GitHub - Cl8D/kotlin-expense: rafactoring practice&quot; data-og-type=&quot;object&quot; data-ke-align=&quot;alignCenter&quot; data-ke-type=&quot;opengraph&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://github.com/Cl8D/kotlin-expense&quot; data-source-url=&quot;https://github.com/Cl8D/kotlin-expense&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cubGLS/hyVqsTwrU4/t2Rt5y1U7wzQ1takmtkoAk/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; style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - Cl8D/kotlin-expense: rafactoring practice&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; style=&quot;color: #909090;&quot; data-ke-size=&quot;size16&quot;&gt;rafactoring practice. Contribute to Cl8D/kotlin-expense development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; style=&quot;color: #909090;&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;main 브랜치의 경우 리팩터링 이전의 코드이고, refactoring 브랜치를 가면 점진적 리팩터링 과정을 참고할 수 있으니 혹시 필요한 분이 계시다면 참고하면 좋을 것 같다.&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&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;  원본 코드의 문제점&amp;nbsp;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선, 리팩터링이 되기 이전의 샘플 코드이다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;class Expense(
    var type: Type,
    var amount: Int
) {
    enum class Type {
        DINNER,
        BREAKFAST,
        CAR_RENTAL
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;interface ReportPrinter {
    fun print(text: String?)
}

class ExpenseReport {
    private val expenses: MutableList&amp;lt;Expense&amp;gt; = ArrayList()
    private val date: String
        get() = &quot;9/12/2002&quot;
    
    fun printReport(printer: ReportPrinter) {
        var total = 0
        var mealExpenses = 0
        printer.print(&quot;Expenses &quot; + date + &quot;\\n&quot;)

        for (expense in expenses) {
            if (expense.type == Expense.Type.BREAKFAST || expense.type == Expense.Type.DINNER) {
                mealExpenses += expense.amount
            }
            var name = &quot;TILT&quot;

            when (expense.type) {
                Expense.Type.DINNER -&amp;gt; name = &quot;Dinner&quot;
                Expense.Type.BREAKFAST -&amp;gt; name = &quot;Breakfast&quot;
                Expense.Type.CAR_RENTAL -&amp;gt; name = &quot;Car Rental&quot;
            }

            printer.print(
                String.format(
                    &quot;%s\\t%s\\t$%.02f\\n&quot;,
                    if (expense.type == Expense.Type.DINNER
                        &amp;amp;&amp;amp; expense.amount &amp;gt; 5000
                        || expense.type === Expense.Type.BREAKFAST
                        &amp;amp;&amp;amp; expense.amount &amp;gt; 1000) &quot;X&quot; else &quot; &quot;,
                    name, expense.amount / 100.0
                )
            )
            total += expense.amount
        }
        printer.print(String.format(&quot;\\nMeal expenses $%.02f&quot;, mealExpenses / 100.0))
        printer.print(String.format(&quot;\\nTotal $%.02f&quot;, total / 100.0))
    }

    fun addExpense(expense: Expense) {
        expenses.add(expense)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞으로 위의 코드가 우리가 원하는 시나리오대로 잘 동작하는지 검증할 수 있도록, 아래의 테스트 코드를 사용할 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 리팩터링의 시작은, &amp;lsquo;우리가 코드를 수정하더라도 정상적으로 작동하는 것을 보장받을 수 있는가?&amp;rsquo;로부터 시작하는 것 같다. 실무에서는 테스트 코드가 작성되어 있지 않다면 어렵겠지만, 웬만하면 리팩터링을 진행할 때 테스트 코드를 함께 동반하는 것이 좋은 것 같다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;class MockReportPrinter : ReportPrinter {
    private var printedText = &quot;&quot;

    override fun print(text: String?) {
        printedText += text

    }

    fun getText(): String {
        return printedText
    }
}

internal class ExpenseReportTest : StringSpec({
    lateinit var report: ExpenseReport
    lateinit var printer: MockReportPrinter

    beforeTest {
        report = ExpenseReport()
        printer = MockReportPrinter()
    }

    &quot;printEmpty&quot; {
        report.printReport(printer)

        printer.getText() shouldBe &quot;&quot;&quot;
            Expenses 9/12/2002

            Meal expenses $0.00
            Total $0.00
        &quot;&quot;&quot;.trimIndent()
    }

    &quot;printOneDinner&quot; {
        report.addExpense(Expense(Expense.Type.DINNER, 1678))
        report.printReport(printer)

        printer.getText() shouldBe &quot;&quot;&quot;
            Expenses 9/12/2002
             	Dinner	$16.78

            Meal expenses $16.78
            Total $16.78
        &quot;&quot;&quot;.trimIndent()
    }

    &quot;twoMeals&quot; {
        report.addExpense(Expense(Expense.Type.DINNER, 1000))
        report.addExpense(Expense(Expense.Type.BREAKFAST, 500))
        report.printReport(printer)

        printer.getText() shouldBe &quot;&quot;&quot;
            Expenses 9/12/2002
             	Dinner	$10.00
             	Breakfast	$5.00

            Meal expenses $15.00
            Total $15.00
        &quot;&quot;&quot;.trimIndent()
    }

    &quot;twoMealsAndCarRental&quot; {
        report.addExpense(Expense(Expense.Type.DINNER, 1000))
        report.addExpense(Expense(Expense.Type.BREAKFAST, 500))
        report.addExpense(Expense(Expense.Type.CAR_RENTAL, 50000))
        report.printReport(printer)

        printer.getText() shouldBe &quot;&quot;&quot;
            Expenses 9/12/2002
             	Dinner	$10.00
             	Breakfast	$5.00
             	Car Rental	$500.00

            Meal expenses $15.00
            Total $515.00
        &quot;&quot;&quot;.trimIndent()
    }

    &quot;overages&quot; {
        report.addExpense(Expense(Expense.Type.BREAKFAST, 1000))
        report.addExpense(Expense(Expense.Type.BREAKFAST, 1001))
        report.addExpense(Expense(Expense.Type.DINNER, 5000))
        report.addExpense(Expense(Expense.Type.DINNER, 5001))
        report.printReport(printer)

        printer.getText() shouldBe &quot;&quot;&quot;
            Expenses 9/12/2002
             	Breakfast	$10.00
            X	Breakfast	$10.01
             	Dinner	$50.00
            X	Dinner	$50.01

            Meal expenses $120.02
            Total $120.02
        &quot;&quot;&quot;.trimIndent()
    }
})&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존의 코드의 경우 다음과 같은 문제점이 존재한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;1. ExpenseReport 가 하고 있는 일이 너무 많음&lt;br /&gt;2. 비용에 대한 계산을 하는 함수와 출력에 대한 함수가 합쳐져 있어서 기능 자체의 결합도가 높음&lt;br /&gt;3. 비용에 대한 변수의 스코프가 printReport() 함수의 지역 변수로 선언되어 있어, 개발자가 인지하기가 어려움.&lt;br /&gt;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;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&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;  Step1 - 가장 간단한 부분에 대해서 함수로 분리하기&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;fun printReport(printer: ReportPrinter) {
    var total = 0
    var mealExpenses = 0
    printer.print(&quot;Expenses &quot; + date + &quot;\\n&quot;)

    for (expense in expenses) {
        if (expense.type == Expense.Type.BREAKFAST || expense.type == Expense.Type.DINNER) {
            mealExpenses += expense.amount
        }
        var name = &quot;TILT&quot;

        when (expense.type) {
            Expense.Type.DINNER -&amp;gt; name = &quot;Dinner&quot;
            Expense.Type.BREAKFAST -&amp;gt; name = &quot;Breakfast&quot;
            Expense.Type.CAR_RENTAL -&amp;gt; name = &quot;Car Rental&quot;
        }

        printer.print(
            String.format(
                &quot;%s\\t%s\\t$%.02f\\n&quot;,
                if (expense.type == Expense.Type.DINNER
                    &amp;amp;&amp;amp; expense.amount &amp;gt; 5000
                    || expense.type === Expense.Type.BREAKFAST
                    &amp;amp;&amp;amp; expense.amount &amp;gt; 1000) &quot;X&quot; else &quot; &quot;,
                name, expense.amount / 100.0
            )
        )
        total += expense.amount
    }
    printer.print(String.format(&quot;\\nMeal expenses $%.02f&quot;, mealExpenses / 100.0))
    printer.print(String.format(&quot;\\nTotal $%.02f&quot;, total / 100.0))
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저, 위 함수에서 분리할 수 있는 부분을 최대한 추출하여 함수로 만들어보자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1822&quot; data-origin-height=&quot;1350&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bh8Vhq/btsFfTA2eD4/E9jYsXzN5m20FRLKSoT6S0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bh8Vhq/btsFfTA2eD4/E9jYsXzN5m20FRLKSoT6S0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bh8Vhq/btsFfTA2eD4/E9jYsXzN5m20FRLKSoT6S0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbh8Vhq%2FbtsFfTA2eD4%2FE9jYsXzN5m20FRLKSoT6S0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1822&quot; height=&quot;1350&quot; data-origin-width=&quot;1822&quot; data-origin-height=&quot;1350&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;우리는 위의 함수를 크게 3가지 부분으로 분리할 수 있다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;1. 헤더 정보 출력하기&lt;br /&gt;2. 비용을 계산하고 중간 계산 결과를 출력하기&lt;br /&gt;3. 최종 비용을 출력하기&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;비교적 복잡한 2번 내용을 우선 배제하고, 1번과 3번 내용을 추출하기 위해 다음과 같이 함수로 추출하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때, 1번 내용에서 total, mealExpenses 는 헤더 정보를 출력하기 위해 사용되는 정보는 아니기 때문에 선언된 변수를 2번 부분에게 내릴 수 있으며, 3번 부분의 경우 파라미터를 통해 total, mealExpenses를 넘겨주면 되기 때문에 크게 무리가 없다.&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: #ef5369;&quot;&gt;command + option + m (extract method)&lt;/span&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;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;fun printReport(printer: ReportPrinter) {
    printHeader(printer)
    var total = 0
    var mealExpenses = 0

    for (expense in expenses) {
        if (expense.type == Expense.Type.BREAKFAST || expense.type == Expense.Type.DINNER) {
            mealExpenses += expense.amount
        }
        var name = &quot;TILT&quot;

        when (expense.type) {
            Expense.Type.DINNER -&amp;gt; name = &quot;Dinner&quot;
            Expense.Type.BREAKFAST -&amp;gt; name = &quot;Breakfast&quot;
            Expense.Type.CAR_RENTAL -&amp;gt; name = &quot;Car Rental&quot;
        }

        printer.print(
            String.format(
                &quot;%s\\t%s\\t$%.02f\\n&quot;,
                if (expense.type == Expense.Type.DINNER
                    &amp;amp;&amp;amp; expense.amount &amp;gt; 5000
                    || expense.type === Expense.Type.BREAKFAST
                    &amp;amp;&amp;amp; expense.amount &amp;gt; 1000) &quot;X&quot; else &quot; &quot;,
                name, expense.amount / 100.0
            )
        )
        total += expense.amount
    }
    printTotal(printer, mealExpenses, total)
}

private fun printTotal(printer: ReportPrinter, mealExpenses: Int, total: Int) {
    printer.print(String.format(&quot;\\nMeal expenses $%.02f&quot;, mealExpenses / 100.0))
    printer.print(String.format(&quot;\\nTotal $%.02f&quot;, total / 100.0))
}

private fun printHeader(printer: ReportPrinter) {
    printer.print(&quot;Expenses &quot; + date + &quot;\\n&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트가 정상적으로 통과한다면, 안심하고 다음 스텝으로 넘어가보자.&lt;/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;style3&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  Step 2 - 비즈니스 로직과 출력을 분리하자&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 다음에는 2번 부분을 뿌실 차례이다. 해당 부분은 출력과 계산이 함께 혼합되어 있기 때문에 분리하기가 현재로서는 어려우니, &lt;b&gt;반복문을 2개로 분리&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;1914&quot; data-origin-height=&quot;1338&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rD2U1/btsFjKo0rX7/5Lg0k2OL0NKr4BqDjhnC6K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rD2U1/btsFjKo0rX7/5Lg0k2OL0NKr4BqDjhnC6K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rD2U1/btsFjKo0rX7/5Lg0k2OL0NKr4BqDjhnC6K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrD2U1%2FbtsFjKo0rX7%2F5Lg0k2OL0NKr4BqDjhnC6K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1914&quot; height=&quot;1338&quot; data-origin-width=&quot;1914&quot; data-origin-height=&quot;1338&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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: #ef5369;&quot;&gt;option + 방향키 윗키&lt;/span&gt;를 누르게 되면 블록별로 한 번에 select를 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;터치패드나 마우스를 쓰지 않고도 블록을 선택할 수 있는 좋은 키이다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;fun printReport(printer: ReportPrinter) {
    printHeader(printer)
    var total = 0
    var mealExpenses = 0

    for (expense in expenses) {
        if (expense.type == Expense.Type.BREAKFAST || expense.type == Expense.Type.DINNER) {
            mealExpenses += expense.amount
        }
        total += expense.amount
    }

    for (expense in expenses) {
        var name = &quot;TILT&quot;

        when (expense.type) {
            Expense.Type.DINNER -&amp;gt; name = &quot;Dinner&quot;
            Expense.Type.BREAKFAST -&amp;gt; name = &quot;Breakfast&quot;
            Expense.Type.CAR_RENTAL -&amp;gt; name = &quot;Car Rental&quot;
        }

        printer.print(
            String.format(
                &quot;%s\\t%s\\t$%.02f\\n&quot;,
                if (expense.type == Expense.Type.DINNER
                    &amp;amp;&amp;amp; expense.amount &amp;gt; 5000
                    || expense.type === Expense.Type.BREAKFAST
                    &amp;amp;&amp;amp; expense.amount &amp;gt; 1000) &quot;X&quot; else &quot; &quot;,
                name, expense.amount / 100.0
            )
        )
    }

    printTotal(printer, mealExpenses, total)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면, 이제 비용을 계산하는 부분에 대해서 함수로 분리해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때, Intellij의 도움을 받는다면 꽤나 이상한 형태로 분리를 해주는 모습을 볼 수 있다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;private fun calculateExpense(): Pair&amp;lt;Int, Int&amp;gt; {
    var total = 0
    var mealExpenses = 0

    for (expense in expenses) {
        if (expense.type == Expense.Type.BREAKFAST || expense.type == Expense.Type.DINNER) {
            mealExpenses += expense.amount
        }
        total += expense.amount
    }
    return Pair(total, mealExpenses)
}&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;위와 같이 Pair를 통해서 분리를 해주려고 한다. Pair의 경우 first, second 를 통해서 각각의 변수에 접근할 수 있지만, &lt;b&gt;읽는 사람의 입장에서 각 변수가 무엇을 뜻하는지 인지하기가 어려워&lt;/b&gt;진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서, 이 방법 대신에 total, mealExpenses의 변수를 &lt;b&gt;클래스의 변수로 옮겨서&lt;/b&gt;, 실질적으로 해당 함수에는 클래스의 변수를 접근하여 값을 계산할 수 있도록 만들어보자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;430&quot; data-origin-height=&quot;392&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HFpYy/btsFhVLjDde/OPINgYBeJbeeaGX3QiKJ6K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HFpYy/btsFhVLjDde/OPINgYBeJbeeaGX3QiKJ6K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HFpYy/btsFhVLjDde/OPINgYBeJbeeaGX3QiKJ6K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHFpYy%2FbtsFhVLjDde%2FOPINgYBeJbeeaGX3QiKJ6K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;315&quot; height=&quot;287&quot; data-origin-width=&quot;430&quot; data-origin-height=&quot;392&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: #ef5369;&quot;&gt;command + option + F (extract field)&lt;/span&gt;를 누르게 되면, 아래와 같이 해당 변수를 어디로 옮길지 선택할 수 있는 창이 뜨게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, 코틀린에서는 var 변수의 경우 잘 동작하지 않아서 수동으로 옮겨주었다. (val 변수면 괜찮을 것 같은데, var 변수여서 잘 적용이 안 된 것 같아서 아쉽다.)&lt;/p&gt;
&lt;pre id=&quot;code_1708836437040&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class ExpenseReport {  
    private var total = 0
    private var mealExpenses = 0
		....
}&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;이제, 비즈니스 로직에 대해서 함수로 분리를 해보자. 비용에 대한 계산을 진행하고 있으니, calculateExpense 라는 이름을 붙였다.&lt;/p&gt;
&lt;pre id=&quot;code_1708836478679&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private fun calculateExpenses() {
    for (expense in expenses) {
        if (expense.type == Expense.Type.BREAKFAST || expense.type == Expense.Type.DINNER) {
            mealExpenses += expense.amount
        }
        total += expense.amount
    }
}&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;이 함수는 현재 if문이 하는 일을 정확하게 알기 어려우니, 해당 부분도 함수로 분리해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비용의 타입이 아침이나 저녁인 경우 mealExpenses를 더하고 있으니, 간단하게 isMeal 이라는 네이밍을 붙여주었다.&lt;/p&gt;
&lt;pre class=&quot;crystal&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;private fun calculateExpenses() {
    for (expense in expenses) {
        if (isMeal(expense)) {
            mealExpenses += expense.amount
        }
        total += expense.amount
    }
}

private fun isMeal(expense: Expense) = expense.type == Expense.Type.BREAKFAST || expense.type == Expense.Type.DINNER
&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;여전히 함수가 2 depth 여서 알아보기가 어렵다. 한 번 더 분리를 해주자.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;private fun calculateExpenses() {
    for (expense in expenses) {
        addTotal(expense)
    }
}

private fun addTotal(expense: Expense) {
    if (isMeal(expense)) {
        mealExpenses += expense.amount
    }
    total += expense.amount
}
&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마찬가지로, 출력에 대한 부분도 함수로 분리해보자.&lt;/p&gt;
&lt;pre id=&quot;code_1708836521112&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private fun printExpenses(printer: ReportPrinter) {
    for (expense in expenses) {
        var name = &quot;TILT&quot;

        when (expense.type) {
            Expense.Type.DINNER -&amp;gt; name = &quot;Dinner&quot;
            Expense.Type.BREAKFAST -&amp;gt; name = &quot;Breakfast&quot;
            Expense.Type.CAR_RENTAL -&amp;gt; name = &quot;Car Rental&quot;
        }

        printer.print(
            String.format(
                &quot;%s\t%s\t$%.02f\n&quot;,
                if (expense.type == Expense.Type.DINNER
                    &amp;amp;&amp;amp; expense.amount &amp;gt; 5000
                    || expense.type === Expense.Type.BREAKFAST
                    &amp;amp;&amp;amp; expense.amount &amp;gt; 1000
                ) &quot;X&quot; else &quot; &quot;,
                name, expense.amount / 100.0
            )
        )
    }
}&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;위 함수에서도 크게 2가지로 분리할 수 있게 된다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;1. 비용에 대한 이름을 계산하기&lt;br /&gt;2. 실질적으로 비용에 대한 이름을 출력하기&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;이에 따라서 함수를 또 2개로 분리하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때, name의 경우 함수로부터 얻어오게 되기 때문에 더 이상 var 로 있을 필요가 없기 때문에 val로 변경해주었다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;private fun printExpenses(printer: ReportPrinter) {
    for (expense in expenses) {
        val name = getName(expense)

        printer.print(
            String.format(
                &quot;%s\\t%s\\t$%.02f\\n&quot;,
                if (expense.type == Expense.Type.DINNER
                    &amp;amp;&amp;amp; expense.amount &amp;gt; 5000
                    || expense.type === Expense.Type.BREAKFAST
                    &amp;amp;&amp;amp; expense.amount &amp;gt; 1000
                ) &quot;X&quot; else &quot; &quot;,
                name, expense.amount / 100.0
            )
        )
    }
}

private fun getName(expense: Expense): String {
    var name = &quot;TILT&quot;

    when (expense.type) {
        Expense.Type.DINNER -&amp;gt; name = &quot;Dinner&quot;
        Expense.Type.BREAKFAST -&amp;gt; name = &quot;Breakfast&quot;
        Expense.Type.CAR_RENTAL -&amp;gt; name = &quot;Car Rental&quot;
    }
    return name
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지 진행하고, 마찬가지로 테스트 코드를 통해 중간중간 체크를 진행해주자.&lt;/p&gt;
&lt;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;style3&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  Step3 - 불필요한 메서드 파라미터 제거하기&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Step 1, 2를 진행하면서 가장 메인이 되면 public 함수인 printReport() 가 간결해진 것을 확인할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;class ExpenseReport {
    private val expenses: MutableList&amp;lt;Expense&amp;gt; = ArrayList()
    private val date: String
        get() = &quot;9/12/2002&quot;

    private var total = 0
    private var mealExpenses = 0

    fun printReport(printer: ReportPrinter) {
        printHeader(printer)
        calculateExpenses()
        printExpenses(printer)
        printTotal(printer, mealExpenses, total)
    }

    private fun printExpenses(printer: ReportPrinter) {
        for (expense in expenses) {
            val name = getName(expense)

            printer.print(
                String.format(
                    &quot;%s\t%s\t$%.02f\n&quot;,
                    if (expense.type == Expense.Type.DINNER
                        &amp;amp;&amp;amp; expense.amount &amp;gt; 5000
                        || expense.type === Expense.Type.BREAKFAST
                        &amp;amp;&amp;amp; expense.amount &amp;gt; 1000
                    ) &quot;X&quot; else &quot; &quot;,
                    name, expense.amount / 100.0
                )
            )
        }
    }

    private fun getName(expense: Expense): String {
        var name = &quot;TILT&quot;

        when (expense.type) {
            Expense.Type.DINNER -&amp;gt; name = &quot;Dinner&quot;
            Expense.Type.BREAKFAST -&amp;gt; name = &quot;Breakfast&quot;
            Expense.Type.CAR_RENTAL -&amp;gt; name = &quot;Car Rental&quot;
        }
        return name
    }

    private fun calculateExpenses() {
        for (expense in expenses) {
            addTotal(expense)
        }
    }

    private fun addTotal(expense: Expense) {
        if (isMeal(expense)) {
            mealExpenses += expense.amount
        }
        total += expense.amount
    }

    private fun isMeal(expense: Expense) = expense.type == Expense.Type.BREAKFAST || expense.type == Expense.Type.DINNER

    private fun printTotal(printer: ReportPrinter, mealExpenses: Int, total: Int) {
        printer.print(String.format(&quot;\nMeal expenses $%.02f&quot;, mealExpenses / 100.0))
        printer.print(String.format(&quot;\nTotal $%.02f&quot;, total / 100.0))
    }

    private fun printHeader(printer: ReportPrinter) {
        printer.print(&quot;Expenses &quot; + date + &quot;\n&quot;)
    }

    fun addExpense(expense: Expense) {
        expenses.add(expense)
    }
}&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;크게 헤더를 출력하기 / 비용을 계산하기 / 비용을 출력하기 / 총 합계를 출력하기, 이렇게 4가지로 분리되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나, 함수로 추출하면서 parameter drilling 으로 인해 필요가 없음에도 인자로 계속 넘겨주는 부분들이 눈에 들어올 것이다.&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;ReportPrinter 가 계속해서 인자로 넘어가고 있다는 점&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 부분을 제거하기 위해, 마찬가지로 ReportPrinter에 대해 클래스의 변수로 승격시키는 작업을 진행해보자.&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: #ef5369;&quot;&gt;command + option + F&lt;/span&gt; 를 통해서 잘 추출이 되지만, 코틀린이기 때문에 우선 수기로 추출을 진행해주었다. 이때, printer의 경우 NPE 방지를 위해서 lateinit 을 통해 지연 초기화가 가능하도록 설정해주었다.&lt;/p&gt;
&lt;pre id=&quot;code_1708836846738&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private lateinit var printer: ReportPrinter

fun printReport(printer: ReportPrinter) {
    this.printer = printer
    printHeader(printer)
    calculateExpenses()
    printExpenses(printer)
    printTotal(printer, mealExpenses, total)
}&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러면,  클래스의 필드 레벨에서 printer가 선언이 되어 있기 때문에 더 이상 인자로 계속 넘겨줄 필요가 없어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바라면 대상이 되는 함수에서 &lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;command + option + n (Inline Parameter)&lt;/b&gt;&lt;/span&gt; 을 누르면 위와 같이 클래스의 변수로 선언된 값을 사용하도록 쉽게 리팩터링이 가능하다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1106&quot; data-origin-height=&quot;582&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/WRe4t/btsFir4jJP5/68ZOUrua0C6IqpCXSsGiUK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/WRe4t/btsFir4jJP5/68ZOUrua0C6IqpCXSsGiUK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/WRe4t/btsFir4jJP5/68ZOUrua0C6IqpCXSsGiUK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWRe4t%2FbtsFir4jJP5%2F68ZOUrua0C6IqpCXSsGiUK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;636&quot; height=&quot;335&quot; data-origin-width=&quot;1106&quot; data-origin-height=&quot;582&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: #ef5369;&quot;&gt;command + F6 (Change Signature)&lt;/span&gt; 를 통해서 인자로 넘어온 printer 를 제거해주었다. printer를 인자로 받는 함수들에 대해서 모두 적용해보도록 하자.&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: #ef5369;&quot;&gt; command + backspace&lt;/span&gt;를 활용하고 &lt;span style=&quot;color: #ef5369;&quot;&gt;command + enter&lt;/span&gt;를 누르면 변경된 내용이 적용된다. (이런 부분에 대해서도 터치패드를 사용하지 않는 노력을 하는 것이 좋을 것 같다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정에서 conflict view가 나오면 continue로 타겟을 옮긴 다음 space를 눌려주면 적용된다!&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1118&quot; data-origin-height=&quot;842&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b4eA4X/btsFiM1Djj6/rw5VszqTMgWH0RjYqhVeaK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b4eA4X/btsFiM1Djj6/rw5VszqTMgWH0RjYqhVeaK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b4eA4X/btsFiM1Djj6/rw5VszqTMgWH0RjYqhVeaK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb4eA4X%2FbtsFiM1Djj6%2Frw5VszqTMgWH0RjYqhVeaK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;566&quot; height=&quot;426&quot; data-origin-width=&quot;1118&quot; data-origin-height=&quot;842&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;p data-ke-size=&quot;size16&quot;&gt;cf) 테스트 코드를 실행할 때도 &lt;b&gt;control + r (바로 이전의 실행 내역 재실행)&lt;/b&gt; 을 하거나, &lt;b&gt;control + option + r&lt;/b&gt;을 통해서 실행 내역 중에서 원하는 것을 실행할 수 있도록 해보자.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;class ExpenseReport {
    private val expenses: MutableList&amp;lt;Expense&amp;gt; = ArrayList()
    private val date: String
        get() = &quot;9/12/2002&quot;

    private var total = 0
    private var mealExpenses = 0
    private lateinit var printer: ReportPrinter

    fun printReport(printer: ReportPrinter) {
        this.printer = printer
        printHeader()
        calculateExpenses()
        printExpenses()
        printTotal()
    }

    private fun printExpenses() {
        for (expense in expenses) {
            val name = getName(expense)

            printer.print(
                String.format(
                    &quot;%s\t%s\t$%.02f\n&quot;,
                    if (expense.type == Expense.Type.DINNER
                        &amp;amp;&amp;amp; expense.amount &amp;gt; 5000
                        || expense.type === Expense.Type.BREAKFAST
                        &amp;amp;&amp;amp; expense.amount &amp;gt; 1000
                    ) &quot;X&quot; else &quot; &quot;,
                    name, expense.amount / 100.0
                )
            )
        }
    }

    private fun getName(expense: Expense): String {
        var name = &quot;TILT&quot;

        when (expense.type) {
            Expense.Type.DINNER -&amp;gt; name = &quot;Dinner&quot;
            Expense.Type.BREAKFAST -&amp;gt; name = &quot;Breakfast&quot;
            Expense.Type.CAR_RENTAL -&amp;gt; name = &quot;Car Rental&quot;
        }
        return name
    }

    private fun calculateExpenses() {
        for (expense in expenses) {
            addTotal(expense)
        }
    }

    private fun addTotal(expense: Expense) {
        if (isMeal(expense)) {
            mealExpenses += expense.amount
        }
        total += expense.amount
    }

    private fun isMeal(expense: Expense) = expense.type == Expense.Type.BREAKFAST || expense.type == Expense.Type.DINNER

    private fun printTotal() {
        printer.print(String.format(&quot;\nMeal expenses $%.02f&quot;, mealExpenses / 100.0))
        printer.print(String.format(&quot;\nTotal $%.02f&quot;, total / 100.0))
    }

    private fun printHeader() {
        printer.print(&quot;Expenses &quot; + date + &quot;\n&quot;)
    }

    fun addExpense(expense: Expense) {
        expenses.add(expense)
    }
}&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;여기에서, printReport()의 함수를 한 번 더 분리하여 완전하게 계산과 출력에 대한 부분으로 나누자.&lt;/p&gt;
&lt;pre id=&quot;code_1708837084002&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun printReport(printer: ReportPrinter) {
    this.printer = printer
    calculateExpenses()
    printExpensesAndTotal()
}

private fun printExpensesAndTotal() {
    printHeader()
    printExpenses()
    printTotal()
}&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;printExpenses() 역시 함수가 2 depth 를 넘어가니 알아보기 어려운 것 같다. 한 번 더 분리해주자.&lt;/p&gt;
&lt;pre id=&quot;code_1708837097911&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private fun printExpenses() {
    for (expense in expenses) {
        printExpense(expense)
    }
}

private fun printExpense(expense: Expense) {
    val name = getName(expense)

    printer.print(
        String.format(
            &quot;%s\t%s\t$%.02f\n&quot;,
            if (expense.type == Expense.Type.DINNER
                &amp;amp;&amp;amp; expense.amount &amp;gt; 5000
                || expense.type === Expense.Type.BREAKFAST
                &amp;amp;&amp;amp; expense.amount &amp;gt; 1000
            ) &quot;X&quot; else &quot; &quot;,
            name, expense.amount / 100.0
        )
    )
}&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;printExpense() 내부에서 name 이 한 곳에서만 쓰이고 있으니 inline 을 통해서 조금 더 줄여줄 수 있다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;private fun printExpense(expense: Expense) {
    printer.print(
        String.format(
            &quot;%s\t%s\t$%.02f\n&quot;,
            if (expense.type == Expense.Type.DINNER
                &amp;amp;&amp;amp; expense.amount &amp;gt; 5000
                || expense.type === Expense.Type.BREAKFAST
                &amp;amp;&amp;amp; expense.amount &amp;gt; 1000
            ) &quot;X&quot; else &quot; &quot;,
            getName(expense), expense.amount / 100.0
        )
    )
}&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;추가적으로 값을 포맷팅 하는 부분의 가독성을 위해 if문을 한 번 더 함수로 분리해주자.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;private fun printExpense(expense: Expense) {
    printer.print(
        String.format(
            &quot;%s\t%s\t$%.02f\n&quot;,
            if (isOverage(expense)) &quot;X&quot; else &quot; &quot;,
            getName(expense), expense.amount / 100.0
        )
    )
}

private fun isOverage(expense: Expense) = (expense.type == Expense.Type.DINNER
        &amp;amp;&amp;amp; expense.amount &amp;gt; 5000
        || expense.type === Expense.Type.BREAKFAST
        &amp;amp;&amp;amp; expense.amount &amp;gt; 1000)&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;마지막으로, 기존 코드에서는 100으로 나누는 부분들이 상당히 겹치는 것을 볼 수 있었다. 이 부분에 대해서 함수로 추출해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때, 현재 상태에서 단순하게 함수로 추출하게 되면 겹치는 부분들에 대해 intellij 가 추천을 해주지 않기 때문에, 1차적으로 지역변수로 분리를 해보자. (어차피 사라질 변수들이기 때문에 이름을 대충 지었다.)&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;private fun printExpense(expense: Expense) {
    val amount = expense.amount
    printer.print(
        String.format(
            &quot;%s\\t%s\\t$%.02f\\n&quot;,
            if (isOverage(expense)) &quot;X&quot; else &quot; &quot;,
            getName(expense), amount / 100.0
        )
    )
}

private fun printTotal() {
    val tempMealExpense = mealExpenses
    val tempTotal = total
    printer.print(String.format(&quot;\\nMeal expenses $%.02f&quot;, tempMealExpense / 100.0))
    printer.print(String.format(&quot;\\nTotal $%.02f&quot;, tempTotal / 100.0))
}&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;이 상태에서 extract method 를 하게 되면, 함수의 시그니처가 Int &amp;rarr; Double 로 고정되기 때문에 사용되는 모든 부분에 대해서 전부 교체가 가능해진다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;private fun printExpense(expense: Expense) {
    val amount = expense.amount
    printer.print(
        String.format(
            &quot;%s\t%s\t$%.02f\n&quot;,
            if (isOverage(expense)) &quot;X&quot; else &quot; &quot;,
            getName(expense), getRate(amount)
        )
    )
}

private fun printTotal() {
    val tempMealExpense = mealExpenses
    val tempTotal = total
    printer.print(String.format(&quot;\nMeal expenses $%.02f&quot;, getRate(tempMealExpense)))
    printer.print(String.format(&quot;\nTotal $%.02f&quot;, getRate(tempTotal)))
}

private fun getRate(amount: Int) = amount / 100.0&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;마지막으로 inline variable 을 통해서 임시 변수를 제거해주자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 최종적으로 우리가 원하는 형태로 잘 수정된 것을 볼 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1708837345920&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private fun printExpense(expense: Expense) {
    printer.print(
        String.format(
            &quot;%s\t%s\t$%.02f\n&quot;,
            if (isOverage(expense)) &quot;X&quot; else &quot; &quot;,
            getName(expense), getRate(expense.amount)
        )
    )
}

private fun printTotal() {
    printer.print(String.format(&quot;\nMeal expenses $%.02f&quot;, getRate(mealExpenses)))
    printer.print(String.format(&quot;\nTotal $%.02f&quot;, getRate(total)))
}

private fun getRate(amount: Int) = amount / 100.0&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;style3&quot; /&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;  Step4 - 도메인의 응집도를 높여보자&lt;/b&gt;&lt;/h4&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;하지만, 핵심 도메인인 Expense 가 하는 일이 굉장히 빈약하게 느껴진다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;출력과 클래스의 전역 변수와 관련없이 수행되고 있는 &amp;lsquo;비용의 이름에 대해서 가져오는 기능&amp;rsquo;과 &amp;lsquo;meal인지 판단하는 기능&amp;rsquo;, &amp;lsquo;평균인지 계산하는 기능&amp;rsquo;은 도메인에게 넘겨줄 수 있지 않을까?&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;span style=&quot;color: #ef5369;&quot;&gt;f6을 눌러 (Move Method)&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&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;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;아래의 글에서 설정하는 방법을 바탕으로 미리 해두면 좋다. (갓망규)&lt;/p&gt;
&lt;figure id=&quot;og_1708844350641&quot; style=&quot;color: #333333; text-align: start;&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/NhCJT/hyVqmsc2EA/el5hHkPLErRMQVUg4Jk4VK/img.png?width=800&amp;amp;height=468&amp;amp;face=0_0_800_468,https://scrap.kakaocdn.net/dn/S3Hcq/hyVmXnk1w5/bavxgXB65jwkIrLl05LKg1/img.png?width=800&amp;amp;height=468&amp;amp;face=0_0_800_468,https://scrap.kakaocdn.net/dn/cUoqIt/hyVqucIrTu/6KJNrJK6LONpUI45TbBf6K/img.png?width=1280&amp;amp;height=905&amp;amp;face=0_0_1280_905&quot; data-og-url=&quot;https://mangkyu.tistory.com/337&quot; data-og-source-url=&quot;https://mangkyu.tistory.com/337&quot; data-og-host=&quot;mangkyu.tistory.com&quot; data-og-description=&quot;1. 코틀린에서 move instance method 리팩토링 기능 활성화하기 [ move instance method 리팩토링 기능 소개 ] 예를 들어 신용카드를 발급하는 유스케이스가 있고, 신용카드 발급을 위해서는 기본적으로 사용&quot; data-og-title=&quot;[Kotlin] 인텔리제이(IntelliJ)에서 코틀린 move instance method(다른 클래스로 메소드 옮기기) 리팩토링&quot; data-og-type=&quot;article&quot; data-ke-align=&quot;alignCenter&quot; data-ke-type=&quot;opengraph&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://mangkyu.tistory.com/337&quot; data-source-url=&quot;https://mangkyu.tistory.com/337&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/NhCJT/hyVqmsc2EA/el5hHkPLErRMQVUg4Jk4VK/img.png?width=800&amp;amp;height=468&amp;amp;face=0_0_800_468,https://scrap.kakaocdn.net/dn/S3Hcq/hyVmXnk1w5/bavxgXB65jwkIrLl05LKg1/img.png?width=800&amp;amp;height=468&amp;amp;face=0_0_800_468,https://scrap.kakaocdn.net/dn/cUoqIt/hyVqucIrTu/6KJNrJK6LONpUI45TbBf6K/img.png?width=1280&amp;amp;height=905&amp;amp;face=0_0_1280_905');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;[Kotlin] 인텔리제이(IntelliJ)에서 코틀린 move instance method(다른 클래스로 메소드 옮기기) 리팩토링&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; style=&quot;color: #909090;&quot; data-ke-size=&quot;size16&quot;&gt;1. 코틀린에서 move instance method 리팩토링 기능 활성화하기 [ move instance method 리팩토링 기능 소개 ] 예를 들어 신용카드를 발급하는 유스케이스가 있고, 신용카드 발급을 위해서는 기본적으로 사용&lt;/p&gt;
&lt;p class=&quot;og-host&quot; style=&quot;color: #909090;&quot; data-ke-size=&quot;size16&quot;&gt;mangkyu.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&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 class=&quot;kotlin&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;class Expense(
    var type: Type,
    var amount: Int
) {
    enum class Type {
        DINNER,
        BREAKFAST,
        CAR_RENTAL
    }

    fun getName(): String {
        var name = &quot;TILT&quot;

        when (this.type) {
            Type.DINNER -&amp;gt; name = &quot;Dinner&quot;
            Type.BREAKFAST -&amp;gt; name = &quot;Breakfast&quot;
            Type.CAR_RENTAL -&amp;gt; name = &quot;Car Rental&quot;
        }
        return name
    }

    fun isMeal() = this.type == Type.BREAKFAST || this.type == Type.DINNER

    fun isOverage() = (this.type == Type.DINNER
        &amp;amp;&amp;amp; this.amount &amp;gt; 5000
        || this.type === Type.BREAKFAST
        &amp;amp;&amp;amp; this.amount &amp;gt; 1000)
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;hr data-ke-style=&quot;style3&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;  Step5 - 출력하는 부분에 대해서 분리해보자&lt;/b&gt;&lt;/h3&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;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;그러나&amp;hellip; 자바에는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;control + t &amp;rarr; extract delegate&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;를 선택하면 쉽게 분리가 가능하지만, 코틀린에는 존재하지 않는다. (가장 아쉽다)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1210&quot; data-origin-height=&quot;856&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nneFY/btsFinARoqW/7VBniKTeRj4zjdaz8wWuV0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nneFY/btsFinARoqW/7VBniKTeRj4zjdaz8wWuV0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nneFY/btsFinARoqW/7VBniKTeRj4zjdaz8wWuV0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnneFY%2FbtsFinARoqW%2F7VBniKTeRj4zjdaz8wWuV0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;604&quot; height=&quot;427&quot; data-origin-width=&quot;1210&quot; data-origin-height=&quot;856&quot;/&gt;&lt;/span&gt;&lt;/figure&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;그래서, extract delegate 대신&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;extract superclass&lt;/b&gt;&lt;/span&gt;를 활용하여 약간의 꼼수로 클래스를 분리해보고자 한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;먼저, 기존의 클래스의 이름을 ExpenseReporter 로 변경하여 출력에 대한 기능들만 남길 수 있도록 의미를 부여해주었다. (&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;Shift + F6 - Rename&lt;/b&gt;&lt;/span&gt;)&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그리고, extract superclass를 통해서 비용을 계산하는 부분을 추출해주자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1136&quot; data-origin-height=&quot;1256&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/coYdm0/btsFh6TC1En/3wd6CcZ7YZ5tLFKmv1d0C1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/coYdm0/btsFh6TC1En/3wd6CcZ7YZ5tLFKmv1d0C1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/coYdm0/btsFh6TC1En/3wd6CcZ7YZ5tLFKmv1d0C1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcoYdm0%2FbtsFh6TC1En%2F3wd6CcZ7YZ5tLFKmv1d0C1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;573&quot; height=&quot;634&quot; data-origin-width=&quot;1136&quot; data-origin-height=&quot;1256&quot;/&gt;&lt;/span&gt;&lt;/figure&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;함수를 선택하다 보면 위와 같이 !가 떠있는 것을 볼 수 있는데, 이는 같이 옮겨야 깨지지 않는 친구들을 Intellj 가 감지하여 알려준다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;!가 뜬 친구들까지 함께 체크하여 선택&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;그럼 아래와 같이 ExpenseReport 라는 친구가 생기게 된다. 우리는 SuperClass로 어쩔 수 없이 생성했기 때문에 open class가 되었는데, 해당 클래스를 일반 클래스로 바꾸어주고, 외부에서 호출할 수 있도록 함수 역시 public 으로 열어두도록 하자.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// AS-IS
open class ExpenseReport {
    protected val expenses: MutableList&amp;lt;Expense&amp;gt; = ArrayList()
    protected var total = 0
    protected var mealExpenses = 0
    protected fun calculateExpenses() {
        for (expense in expenses) {
            addTotal(expense)
        }
    }

    private fun addTotal(expense: Expense) {
        if (expense.isMeal()) {
            mealExpenses += expense.amount
        }
        total += expense.amount
    }

    fun addExpense(expense: Expense) {
        expenses.add(expense)
    }
}

// TO-BE
class ExpenseReport {
     val expenses: MutableList&amp;lt;Expense&amp;gt; = ArrayList()
     var total = 0
     var mealExpenses = 0

     fun calculateExpenses() {
        for (expense in expenses) {
            addTotal(expense)
        }
    }

    private fun addTotal(expense: Expense) {
        if (expense.isMeal()) {
            mealExpenses += expense.amount
        }
        total += expense.amount
    }

    fun addExpense(expense: Expense) {
        expenses.add(expense)
    }
}&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;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;또한, 기존 클래스도 깨져있을 테니까 수정이 필요하다. Report에 대해 전역 변수를 선언해준 다음,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;F2를 눌러 오류 부분을 찾아가며&lt;/span&gt; 수정해주자. (addExpense의 경우 테스트 코드를 최대한 덜 깨지게 하기 위해서 다시 만들어 두었다.)&lt;/p&gt;
&lt;pre id=&quot;code_1708844350646&quot; class=&quot;kotlin&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;class ExpenseReporter {
    private val date: String
        get() = &quot;9/12/2002&quot;

    private lateinit var printer: ReportPrinter
    private val expenseReport = ExpenseReport()

    fun printReport(printer: ReportPrinter) {
        this.printer = printer
        expenseReport.calculateExpenses()
        printExpensesAndTotal()
    }

    private fun printExpensesAndTotal() {
        printHeader()
        printExpenses()
        printTotal()
    }

    private fun printExpenses() {
        for (expense in expenseReport.expenses) {
            printExpense(expense)
        }
    }

    private fun printExpense(expense: Expense) {
        printer.print(
            String.format(
                &quot;%s\t%s\t$%.02f\n&quot;,
                if (isOverage(expense)) &quot;X&quot; else &quot; &quot;,
                expense.getName(), expense.amount / 100.0
            )
        )
    }

    private fun isOverage(expense: Expense) = (expense.type == Expense.Type.DINNER
            &amp;amp;&amp;amp; expense.amount &amp;gt; 5000
            || expense.type === Expense.Type.BREAKFAST
            &amp;amp;&amp;amp; expense.amount &amp;gt; 1000)

    private fun printTotal() {
        printer.print(String.format(&quot;\nMeal expenses $%.02f&quot;, expenseReport.mealExpenses / 100.0))
        printer.print(String.format(&quot;\nTotal $%.02f&quot;, expenseReport.total / 100.0))
    }

    private fun printHeader() {
        printer.print(&quot;Expenses &quot; + date + &quot;\n&quot;)
    }

    fun addExpense(expense: Expense) {
        expenseReport.expenses.add(expense)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;hr data-ke-style=&quot;style3&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;  Step6 - OCP를 개선해보자&lt;/b&gt;&lt;/h3&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_1708844350648&quot; class=&quot;kotlin&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// 출력을 담당하는 클래스
class ExpenseReporter {
    private val date: String
        get() = &quot;9/12/2002&quot;
    private lateinit var printer: ReportPrinter
    private val expenseReport: ExpenseReport = ExpenseReport()

    fun printReport(printer: ReportPrinter) {
        this.printer = printer
        expenseReport.calculateExpenses()
        printExpensesAndTotal()
    }

    private fun printExpensesAndTotal() {
        printHeader()
        printExpenses()
        printTotal()
    }

    private fun printExpenses() {
        for (expense in expenseReport.expenses) {
            printExpense(expense)
        }
    }

    private fun printExpense(expense: Expense) {
        printer.print(
            String.format(
                &quot;%s\t%s\t$%.02f\n&quot;,
                if (expense.isOverage()) &quot;X&quot; else &quot; &quot;,
                expense.getName(), getRate(expense.amount)
            )
        )
    }

    private fun printTotal() {
        printer.print(String.format(&quot;\nMeal expenses $%.02f&quot;, getRate(expenseReport.mealExpenses)))
        printer.print(String.format(&quot;\nTotal $%.02f&quot;, getRate(expenseReport.total)))
    }

    private fun getRate(amount: Int) = amount / 100.0

    private fun printHeader() {
        printer.print(&quot;Expenses &quot; + date + &quot;\n&quot;)
    }

    fun addExpense(expense: Expense) {
        expenseReport.addExpense(expense)
    }
}

// 계산을 담당하는 클래스
class ExpenseReport {
    val expenses: MutableList&amp;lt;Expense&amp;gt; = ArrayList()
    var total = 0
    var mealExpenses = 0

    fun calculateExpenses() {
        for (expense in expenses) {
            addTotal(expense)
        }
    }

    private fun addTotal(expense: Expense) {
        if (expense.isMeal()) {
            mealExpenses += expense.amount
        }
        total += expense.amount
    }

    fun addExpense(expense: Expense) {
        expenses.add(expense)
    }
}

// 비용에 대한 도메인
class Expense(
    var type: Type,
    var amount: Int
) {
    enum class Type {
        DINNER,
        BREAKFAST,
        CAR_RENTAL
    }

    fun getName(): String {
        var name = &quot;TILT&quot;

        when (this.type) {
            Type.DINNER -&amp;gt; name = &quot;Dinner&quot;
            Type.BREAKFAST -&amp;gt; name = &quot;Breakfast&quot;
            Type.CAR_RENTAL -&amp;gt; name = &quot;Car Rental&quot;
        }
        return name
    }

    fun isMeal() = this.type == Type.BREAKFAST || this.type == Type.DINNER

    fun isOverage() = (this.type == Type.DINNER
            &amp;amp;&amp;amp; this.amount &amp;gt; 5000
            || this.type === Type.BREAKFAST
            &amp;amp;&amp;amp; this.amount &amp;gt; 1000)
}&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;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;span style=&quot;color: #ef5369;&quot;&gt;새로운 Expense의 타입이 추가되었을 때 유연하게 확장할 수 있도록&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;각각에 대해 클래스로 분리하는 작업을 진행할 것이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;먼저, 원활한 작업을 위해서 Expense의 클래스를 미리 abstract class로 바꾸어두자. (이렇게 하지 않으면, 아래에서 세부 클래스들을 만들 때 단축키를 사용할 수 없다.)&lt;/p&gt;
&lt;pre id=&quot;code_1708844350652&quot; class=&quot;angelscript&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;abstract class Expense(
    var type: Type,
    var amount: Int
)&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;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;span style=&quot;color: #ef5369;&quot;&gt;테스트 코드를 수정&lt;/span&gt;하는 작업을 진행해보자.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;기존의 테스트 코드에서 비용 객체를 생성하기 위해 Dinner 타입을 넘겨준 부분을, 저녁에 대한 비용을 나타내는 새로운 클래스를 만들어주기 위해 새롭게 생성해준다.&lt;/p&gt;
&lt;pre class=&quot;awk&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;// AS-IS
&quot;printOneDinner&quot; {
    report.addExpense(Expense(Expense.Type.DINNER, 1678))
    report.printReport(printer)

    printer.getText() shouldBe &quot;&quot;&quot;
        Expenses 9/12/2002
         	Dinner	$16.78

        Meal expenses $16.78
        Total $16.78
    &quot;&quot;&quot;.trimIndent()
}

// TO-BE
&quot;printOneDinner&quot; {
    report.addExpense(DinnerExpense(1678))
    report.printReport(printer)

    printer.getText() shouldBe &quot;&quot;&quot;
        Expenses 9/12/2002
         	Dinner	$16.78

        Meal expenses $16.78
        Total $16.78
    &quot;&quot;&quot;.trimIndent()
}
&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;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그럼, 우리는&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;이 테스트를 깨지지 않도록 만드는 것&lt;/span&gt;이 가장 중요하다. DinnerExpense라는 클래스를 새롭게 만들어주자.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;(마치 TDD를 하는 것처럼, 테스트를 중점으로 하여 리팩터링을 진행하는 것이다.)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1346&quot; data-origin-height=&quot;546&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b5mBxP/btsFf4P0L8y/fOHjbn0mX7wejqEpQFSDQ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b5mBxP/btsFf4P0L8y/fOHjbn0mX7wejqEpQFSDQ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b5mBxP/btsFf4P0L8y/fOHjbn0mX7wejqEpQFSDQ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb5mBxP%2FbtsFf4P0L8y%2FfOHjbn0mX7wejqEpQFSDQ1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1346&quot; height=&quot;546&quot; data-origin-width=&quot;1346&quot; data-origin-height=&quot;546&quot;/&gt;&lt;/span&gt;&lt;/figure&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;span style=&quot;color: #ef5369;&quot;&gt;option + enter&lt;/span&gt;를 누르면 위와 같이 create class를 통해 만들어줄 수 있다.&lt;/p&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;이미 클래스에서 어떤 타입인지 드러나고 있어, 인자로 type 조건을 넘겨주는 것이 어색하기 때문에 클래스 생성 후 change Signature를 통해 일괄적으로 제거해주었다.&lt;/p&gt;
&lt;pre id=&quot;code_1708844350653&quot; class=&quot;haskell&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;class DinnerExpense(amount: Int) : Expense(Type.DINNER, amount) 

class BreakfastExpense(amount: Int) : Expense(Type.BREAKFAST, amount)

class CarRentalExpense(amount: Int) : Expense(Type.CAR_RENTAL, amount)&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;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그럼, 테스트 코드 역시 아래와 같이 바뀌었을 것이다.&lt;/p&gt;
&lt;pre id=&quot;code_1708844350653&quot; class=&quot;stata&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;internal class ExpenseReportTest : StringSpec({
    lateinit var report: ExpenseReporter
    lateinit var printer: MockReportPrinter

    beforeTest {
        report = ExpenseReporter()
        printer = MockReportPrinter()
    }

    &quot;printEmpty&quot; {
        report.printReport(printer)

        printer.getText() shouldBe &quot;&quot;&quot;
            Expenses 9/12/2002

            Meal expenses $0.00
            Total $0.00
        &quot;&quot;&quot;.trimIndent()
    }

    &quot;printOneDinner&quot; {
        report.addExpense(DinnerExpense(1678))
        report.printReport(printer)

        printer.getText() shouldBe &quot;&quot;&quot;
            Expenses 9/12/2002
             	Dinner	$16.78

            Meal expenses $16.78
            Total $16.78
        &quot;&quot;&quot;.trimIndent()
    }

    &quot;twoMeals&quot; {
        report.addExpense(DinnerExpense(1000))
        report.addExpense(BreakfastExpense(500))
        report.printReport(printer)

        printer.getText() shouldBe &quot;&quot;&quot;
            Expenses 9/12/2002
             	Dinner	$10.00
             	Breakfast	$5.00

            Meal expenses $15.00
            Total $15.00
        &quot;&quot;&quot;.trimIndent()
    }

    &quot;twoMealsAndCarRental&quot; {
        report.addExpense(DinnerExpense(1000))
        report.addExpense(BreakfastExpense(500))
        report.addExpense(CarRentalExpense(50000))
        report.printReport(printer)

        printer.getText() shouldBe &quot;&quot;&quot;
            Expenses 9/12/2002
             	Dinner	$10.00
             	Breakfast	$5.00
             	Car Rental	$500.00

            Meal expenses $15.00
            Total $515.00
        &quot;&quot;&quot;.trimIndent()
    }

    &quot;overages&quot; {
        report.addExpense(BreakfastExpense(1000))
        report.addExpense(BreakfastExpense(1001))
        report.addExpense(DinnerExpense(5000))
        report.addExpense(DinnerExpense(5001))
        report.printReport(printer)

        printer.getText() shouldBe &quot;&quot;&quot;
            Expenses 9/12/2002
             	Breakfast	$10.00
            X	Breakfast	$10.01
             	Dinner	$50.00
            X	Dinner	$50.01

            Meal expenses $120.02
            Total $120.02
        &quot;&quot;&quot;.trimIndent()
    }
})&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;hr data-ke-style=&quot;style3&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  Step7 - 자식 클래스들에게 책임을 분리하자&lt;/b&gt;&lt;/h4&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;우리의 Expense 클래스를 되돌아보자. 여기에서 각 함수들은 자식 클래스들로 내려가기에 좋아 보인다.&lt;/p&gt;
&lt;pre id=&quot;code_1708844350654&quot; class=&quot;kotlin&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;abstract class Expense(
    var type: Type,
    var amount: Int
) {
    enum class Type {
        DINNER,
        BREAKFAST,
        CAR_RENTAL
    }

    fun getName(): String {
        var name = &quot;TILT&quot;

        when (this.type) {
            Type.DINNER -&amp;gt; name = &quot;Dinner&quot;
            Type.BREAKFAST -&amp;gt; name = &quot;Breakfast&quot;
            Type.CAR_RENTAL -&amp;gt; name = &quot;Car Rental&quot;
        }
        return name
    }

    fun isMeal() = this.type == Type.BREAKFAST || this.type == Type.DINNER
    fun isOverage() = (this.type == Type.DINNER
            &amp;amp;&amp;amp; this.amount &amp;gt; 5000
            || this.type === Type.BREAKFAST
            &amp;amp;&amp;amp; this.amount &amp;gt; 1000)
}&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;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;span style=&quot;color: #ef5369;&quot;&gt;control + t &amp;rarr; push member down&lt;/span&gt;을 눌러주자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;928&quot; data-origin-height=&quot;592&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/m6ZlG/btsFh6zi3bU/b2BGlfwiQYwinksgd7Gut1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/m6ZlG/btsFh6zi3bU/b2BGlfwiQYwinksgd7Gut1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/m6ZlG/btsFh6zi3bU/b2BGlfwiQYwinksgd7Gut1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fm6ZlG%2FbtsFh6zi3bU%2Fb2BGlfwiQYwinksgd7Gut1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;544&quot; height=&quot;347&quot; data-origin-width=&quot;928&quot; data-origin-height=&quot;592&quot;/&gt;&lt;/span&gt;&lt;/figure&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;그러면, 아래와 같이 Expense 클래스의 각 메서드들이 abstract 클래스로 변하게 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1708844350656&quot; class=&quot;crystal&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;abstract class Expense(
    var type: Type,
    var amount: Int
) {
    enum class Type {
        DINNER,
        BREAKFAST,
        CAR_RENTAL
    }

    abstract fun getName(): String
    abstract fun isMeal(): Boolean
    abstract fun isOverage(): Boolean
}&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;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이제, 각각의 클래스들에게 가서 어울리는 행동을 할 수 있도록 수정해주자.&lt;/p&gt;
&lt;pre id=&quot;code_1708844350656&quot; class=&quot;kotlin&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// AS-IS
class BreakfastExpense(amount: Int) : Expense(Type.BREAKFAST, amount) {
    override fun getName(): String {
        var name = &quot;TILT&quot;

        when (this.type) {
            Type.DINNER -&amp;gt; name = &quot;Dinner&quot;
            Type.BREAKFAST -&amp;gt; name = &quot;Breakfast&quot;
            Type.CAR_RENTAL -&amp;gt; name = &quot;Car Rental&quot;
        }
        return name
    }

    override fun isMeal() = this.type == Type.BREAKFAST || this.type == Type.DINNER
    override fun isOverage() = (this.type == Type.DINNER
            &amp;amp;&amp;amp; this.amount &amp;gt; 5000
            || this.type === Type.BREAKFAST
            &amp;amp;&amp;amp; this.amount &amp;gt; 1000)

}

// TO-BE
class BreakfastExpense(amount: Int) : Expense(Type.BREAKFAST, amount) {
    override fun getName(): String = &quot;Breakfast&quot;
    override fun isMeal() = true
    override fun isOverage() = this.amount &amp;gt; 1000
}

// 나머지도 동일하게 변경
class CarRentalExpense(amount: Int) : Expense(Type.CAR_RENTAL, amount) {
    override fun getName(): String = &quot;Car Rental&quot;
    override fun isMeal() = false
    override fun isOverage() = false
}

class DinnerExpense(amount: Int) : Expense(Type.DINNER, amount) {
    override fun getName(): String = &quot;Dinner&quot;
    override fun isMeal() = true
    override fun isOverage() = this.amount &amp;gt; 5000
}&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;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;또한, 여기에서 기존에 정의해주었던 Expense 클래스 내부의 Type이라는 enum class는 getName을 위해서 사용되었던 클래스이기 때문에 제거해도 무리가 없다. 지워주도록 하자. 마찬가지로 이전에 사용했던&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;command + F6 (Change Signature)&lt;/span&gt;를 사용하자. 이렇게 하면 자식 클래스에 가서 직접 제거해줄 필요가 없이 편하게 지울 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;952&quot; data-origin-height=&quot;1100&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cJddvR/btsFiOrDty0/g315LbIZRMU96DqiSoC9T0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cJddvR/btsFiOrDty0/g315LbIZRMU96DqiSoC9T0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cJddvR/btsFiOrDty0/g315LbIZRMU96DqiSoC9T0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcJddvR%2FbtsFiOrDty0%2Fg315LbIZRMU96DqiSoC9T0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;568&quot; height=&quot;656&quot; data-origin-width=&quot;952&quot; data-origin-height=&quot;1100&quot;/&gt;&lt;/span&gt;&lt;/figure&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;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;916&quot; data-origin-height=&quot;396&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dOct3a/btsFeFXB1BA/mnRdUKDoFwYC2AXfzCMCgK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dOct3a/btsFeFXB1BA/mnRdUKDoFwYC2AXfzCMCgK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dOct3a/btsFeFXB1BA/mnRdUKDoFwYC2AXfzCMCgK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdOct3a%2FbtsFeFXB1BA%2FmnRdUKDoFwYC2AXfzCMCgK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;576&quot; height=&quot;249&quot; data-origin-width=&quot;916&quot; data-origin-height=&quot;396&quot;/&gt;&lt;/span&gt;&lt;/figure&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;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style3&quot; data-ke-type=&quot;horizontalRule&quot; /&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: #000000; text-align: start;&quot; 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-02-25 오후 3.53.24.png&quot; data-origin-width=&quot;2164&quot; data-origin-height=&quot;932&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bC30Kx/btsFfVeydJn/FXDwsJcm9ESKJWyFNUVFp0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bC30Kx/btsFfVeydJn/FXDwsJcm9ESKJWyFNUVFp0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bC30Kx/btsFfVeydJn/FXDwsJcm9ESKJWyFNUVFp0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbC30Kx%2FbtsFfVeydJn%2FFXDwsJcm9ESKJWyFNUVFp0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;632&quot; height=&quot;272&quot; data-filename=&quot;스크린샷 2024-02-25 오후 3.53.24.png&quot; data-origin-width=&quot;2164&quot; data-origin-height=&quot;932&quot;/&gt;&lt;/span&gt;&lt;/figure&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;p style=&quot;color: #333333; text-align: start;&quot; 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;236&quot; data-origin-height=&quot;213&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kRvcp/btsFiPcZY9X/lydMk4XBLqQnGdsLsavEBK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kRvcp/btsFiPcZY9X/lydMk4XBLqQnGdsLsavEBK/img.jpg&quot; data-alt=&quot;뿌듯!&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kRvcp/btsFiPcZY9X/lydMk4XBLqQnGdsLsavEBK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkRvcp%2FbtsFiPcZY9X%2FlydMk4XBLqQnGdsLsavEBK%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;297&quot; height=&quot;268&quot; data-origin-width=&quot;236&quot; data-origin-height=&quot;213&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;뿌듯!&lt;/figcaption&gt;
&lt;/figure&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;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;처음에 원본 코드를 보았을 때는 코드의 설계 관점만 생각해서 수기로 클래스를 만들고 분리하는 일들을 많이 했었는데, 이번 리팩터링을 각 단계마다 천천히 진행하면서 느꼈던 건 Intellij 에서 생각보다 많은 기능을 지원해주고 있었다는 점이었다.&lt;/p&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;&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;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;일을 진행하면서 코드 대신에 직접 수기로 QA를 하던 날들이 많았는데, 앞으로는 리팩터링 할 때 최대한 테스트 코드에 의존하여 수정하도록 습관을 들여야겠다. 단축키 연습도 열심히 해서 앞으로는 키보드로만 개발할 수 있도록... 노력해야겠다&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #777777; text-align: center;&quot;&gt; &lt;/span&gt;&lt;/p&gt;</description>
      <category>개발일지</category>
      <category>IntelliJ</category>
      <category>단축키</category>
      <category>리팩터링</category>
      <author>dolmeng2</author>
      <guid isPermaLink="true">https://cl8d.tistory.com/129</guid>
      <comments>https://cl8d.tistory.com/129#entry129comment</comments>
      <pubDate>Sun, 25 Feb 2024 16:04:58 +0900</pubDate>
    </item>
    <item>
      <title>[객체지향의사실과오해] 06 - 객체 지도</title>
      <link>https://cl8d.tistory.com/128</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;974&quot; data-origin-height=&quot;1388&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/enmvil/btsDHr7afxz/UimKsX7WeOWxyiEVyDcq5k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/enmvil/btsDHr7afxz/UimKsX7WeOWxyiEVyDcq5k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/enmvil/btsDHr7afxz/UimKsX7WeOWxyiEVyDcq5k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fenmvil%2FbtsDHr7afxz%2FUimKsX7WeOWxyiEVyDcq5k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;437&quot; height=&quot;623&quot; data-origin-width=&quot;974&quot; data-origin-height=&quot;1388&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;✔️ 객체 지도&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;프로그래머의 가장 큰 숙제는 '&lt;span style=&quot;color: #ef5369;&quot;&gt;변화하는 요구사항에 얼마나 더 잘 대응할 수 있는가&lt;/span&gt;?' 이다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어보자. 고양이 스낵바에 새로운 신입 셰프냥이 들어왔다. 신입 셰프냥은 아직 메뉴 숙지에 미숙해서 레시피를 다 외우지 못했다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;이때 손님냥이 알바냥에게 딸기라떼를 주문했고, 알바냥은 신입 셰프냥에게 딸기라떼를 만들라고 지시했다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 신입 셰프냥은 딸기라떼 레시피를 몰라서 어떻게 해야 할지 고민 중인 상황이라면, 어떤 행위를 할 수 있을까?&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; 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;2870&quot; data-origin-height=&quot;1064&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uMiTk/btsDGnqpobV/c6gCS3vCEhcRReAYAewa61/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uMiTk/btsDGnqpobV/c6gCS3vCEhcRReAYAewa61/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uMiTk/btsDGnqpobV/c6gCS3vCEhcRReAYAewa61/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuMiTk%2FbtsDGnqpobV%2Fc6gCS3vCEhcRReAYAewa61%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2870&quot; height=&quot;1064&quot; data-origin-width=&quot;2870&quot; data-origin-height=&quot;1064&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;1.&amp;nbsp;수석&amp;nbsp;셰프냥에게&amp;nbsp;딸기라떼&amp;nbsp;만드는&amp;nbsp;방법&amp;nbsp;물어보기&lt;br /&gt;2.&amp;nbsp;레시피&amp;nbsp;북을&amp;nbsp;참고하여&amp;nbsp;딸기라떼&amp;nbsp;만들기&lt;/blockquote&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;먼저, 첫 번째 방법의 경우, 만약 딸기라떼의 레시피를 얻을 수는 있지만,&lt;b&gt; 변화하는 요구사항에 대응할 수 없다&lt;/b&gt;.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;만약 손님이 추가적으로 &amp;lsquo;엇, 근데 딸기라떼에 들어가는 우유를 두유로 바꿔주실 수 있을까요?&amp;rsquo; 라고 말을 했을 때&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;신입 셰프냥은 또 다시 멘붕에 빠져 수석 셰프냥을 귀찮게 할지도 모른다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;반면에, 레시피 북을 보게 된다면 여러 가지 상황에 대비할 수 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;우유가 아닌 두유를 넣을 때의 비율, 딸기 시럽의 비율 등 더 정확한 정보를 담고 있기 때문이다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;이와 같이, 사용자의 요구사항은 변할 수밖에 없기 때문에 &amp;lsquo;딸기라떼를 만들어달라&amp;rsquo;는 &lt;span style=&quot;color: #ef5369;&quot;&gt;기능 자체에 집중하는 것이 아닌,&lt;/span&gt; &amp;lsquo;다양한 재료들로 여러 메뉴를 만들 수 있는&amp;rsquo;&lt;span style=&quot;color: #ef5369;&quot;&gt; 구조 중심의 개발을 진행하는 것&lt;/span&gt;이 좋다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;✔️ 기능 설계 vs 구조 설계&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;사용자의 요구사항은 기능을 기반으로 하기 때문에 훌륭한 소프트웨어를 만들기 위한 충분 조건이 될 수 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;그러나, 훌륭한 구조는 훌륭한 소프트웨어를 만들기 위한 필요 조건이다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;탄탄한 구조를 가지고 있으면, &lt;b&gt;자연스럽게 새로운 기능을 안정적으로 빠르게 개발&lt;/b&gt;할 수 있기 때문이다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;(진흙탕에서 개발하는 게 쉬울까, 아니면 싱그러운 풀밭에서 개발하는 게 쉬울까?)&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;기능 기반으로 설계하게 되면, 각 기능들이 서로 밀접하게 관련이 있기 때문에&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;요구사항에 따라서 &lt;b&gt;하나의 기능이 바뀌면 다른 기능들까지 영향을 받게 된다&lt;/b&gt;.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;개발자는 늘 변화하는 요구사항에 대해 우리는 최대한 적은 비용으로 대응할 수 있도록 만드는 것이 숙명이다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;그리고 이를 위해서는 안정적인 구조가 꼭 필요하다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;✔️ 객체지향 세계 구축하기&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;먼저, 사용자에게 제공할 기능과, 기능을 담기 위한 구조를 준비해야 한다.&lt;br /&gt;여기서 &lt;b&gt;기능&lt;/b&gt;이란, &lt;span style=&quot;color: #ef5369;&quot;&gt;사용자의 목표를 만족시키기 위해 책임을 수행하는 시스템의 행위&lt;/span&gt;를 의미하며 대표적으로 '유스케이스 모델링' 이라는 기법을 사용할 수 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;반대로, &lt;b&gt;구조&lt;/b&gt;는 사용자나 이해 관계자들이 &lt;span style=&quot;color: #ef5369;&quot;&gt;도메인에 대해 생각하는 개념과, 개념들과의 관계로 표현&lt;/span&gt;하는 것을 의미하며, 대표적으로 '도메인 모델링' 이라는 기법을 사용할 수 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;  안정적인 재료 파밍하기 - 구조 설계&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;앞서 '도메인 모델링'이라는 말을 했는데, 도메인 모델링이라는 것이 뭘 의미하는 것일까?&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;이는 사용자가 프로그램을 &lt;b&gt;사용하는 대상 영역에 관한 지식을 선택적으로 단순화하고 의식적으로 구조화한 형태&lt;/b&gt;를 말한다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;한마디로 정의하자면, '멘탈 모델'이라고도 할 수 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;정말 쉽게 예를 들어보자. 우리는 다음과 같은 문장들을 접했다고 생각해보자.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;- 물건을 살 수 있다.&lt;br /&gt;- 물건을 팔 수 있다.&lt;br /&gt;- 물건을 팔면 돈을 벌 수 있다.&lt;br /&gt;- 물건을 사기 위해서는 돈이 필요하다.&lt;br /&gt;...&lt;/blockquote&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;우리는 앞서 제시된 4가지의 문장을 보면, 자연스럽게 '상점' 과 같은 두루뭉실한 것들을 머릿속에 떠올릴 수 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;대상에 대해서 우리의 뇌는 구조화를 하여 생각을 한 것이며, 하나의 멘탈 모델을 그렸다고 볼 수 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;멘탈 모델은 크게&amp;nbsp;&lt;span style=&quot;color: #ef5369;&quot;&gt;사용자 모델 / 디자인 모델 / 시스템 이미지&lt;/span&gt; 3가지를 포괄하도록 추상화한 소프트웨어 모델이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;1400&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d0EDHz/btsDKzWUW13/cBhNckxYuB8u8fNra05qdk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d0EDHz/btsDKzWUW13/cBhNckxYuB8u8fNra05qdk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d0EDHz/btsDKzWUW13/cBhNckxYuB8u8fNra05qdk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd0EDHz%2FbtsDKzWUW13%2FcBhNckxYuB8u8fNra05qdk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;597&quot; height=&quot;418&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;1400&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;디자이너와 사용자는 직접 교류하지 않고, 오직 시스템을 통해서 서로 소통하게 된다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;우리가 객체지향을 사용하면 &lt;b&gt;사용자들이 이해하고 있는 도메인의 구조와 최대한 유사하게 코드를 구조화&lt;/b&gt;할 수 있다.&lt;br /&gt;물론, 이 말이 현실의 구조를 그대로 따라가야 한다는 것은 아니다.&lt;span style=&quot;color: #ef5369;&quot;&gt; &amp;lsquo;모방&amp;rsquo;하는 것이 아닌, &amp;lsquo;은유&amp;rsquo;를 통해서 재창조를 진행&lt;/span&gt;하는 것이며,&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;현실 객체가 할 수 없는 행동이나 가지지 않는 특성들을 가질 수도 있다. (= 이를 &amp;lsquo;표현적 차이&amp;rsquo;, &amp;lsquo;의미적 차이&amp;rsquo;라고도 한다)&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어, 우리가 코드를 설계할 때 '커피'라는 객체에게 무료인지 판단하는 기능을 부여해줄 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1705681511381&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class Coffee(
    val price: Int
) {

    fun isFree(): Boolean {
        return price == 0 
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 실세계에서 커피가 무료인지 행동할 수 있는가? 없다. 우리는 객체지향 세계에서만 이를 재창조한 것이다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;즉, 우리는 &lt;span style=&quot;color: #ef5369;&quot;&gt;사용자가 이해하고 있는 도메인의 모델을 은유하여 코드에 나타내는 것&lt;/span&gt;이 목표이며,&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;이러한 사용자 모델의 경우 비교적 변경될 가능성이 적기 때문에 도메인 모델로 나타냈을 때 안정적인 구조를 설계할 수 있게 된다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;(우리가 이해하고 있는 '커피'라는 개념이 갑자기 '치킨'으로 바뀌지 않는 것처럼.)&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;  불완전한 재료 파밍하기 - 기능 설계&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;사용자들은 시스템을 통해서 &lt;b&gt;달성하고 싶은 목표&lt;/b&gt;가 존재하며, 시스템은 이에 대한 기능들을 제공해야 한다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;이는 곧 &lt;span style=&quot;color: #ef5369;&quot;&gt;사용자와 시스템 간의 상호작용을 만들며&lt;/span&gt;, 이러한 상호작용을 텍스트로 정리한 것이 바로 &amp;lsquo;&lt;b&gt;유스케이스&lt;/b&gt;&amp;rsquo;이다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;사용자의 목표가 곧 유스케이스의 핵심이며, 유스케이스는 공통의 사용자 목표를 통해 강하게 연관된 시나리오의 집합이다.&amp;nbsp;&lt;br /&gt;- 마틴 파울로&lt;/blockquote&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;위 문장을 보면, 왜 유스케이스가 '사용자 액터 다이어그램'이라고 불리는지 이해할 수 있을 것이다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;유스케이스의 가장 큰 핵심은 '사용자가 달성하고 싶은 목표'를 나타내는 것이기 때문이다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;유스케이스는 크게 다음과 같은 특징들을 가지고 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;1. 유스케이스는 사용자와 시스템 간의 상호작용을 보여주는 &amp;lsquo;텍스트&amp;rsquo;일 뿐이다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;2. 여러 시나리오들의 집합으로 구성될 수 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;3. 단순한 기능의 집합이 아닌, &lt;b&gt;문맥을 통해 어떠한 이야기를 제공&lt;/b&gt;해야 한다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;4. 사용자 인터페이스와 관련된 세부 정보를 포함하지 않는다. (자주 변경되는 요소를 제외하고, 행위에만 집중)&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;비슷하게, 내부 설계에 대한 정보 역시 드러내지 않는다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;유스케이스를 기반으로 객체를 변환하는 것은 창조일 뿐이며, 유스케이스를 통해 객체의 변화가 더 쉬워지는 것은 아니다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;유스케이스는 객체에 대해 어떠한 정보도 제공하지 않는다&lt;/span&gt;. 그저 도메인 모델에 대한 힌트 정도일 뿐이다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;✔️ 재료를 모아모아 - 기능과 구조를 통합하기&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;우리는 안정적인 구조를 도메인 모델로, 불안정한 기능을 유스케이스로 나타내는 방법을 알았다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;이제는 코드로서 객체들의 책임으로 분리를 해야 한다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;시스템은 &lt;span style=&quot;color: #ef5369;&quot;&gt;사용자의 목표를 만족시키기 위해 협력하는 커다란 객체&lt;/span&gt;로 볼 수 있으며, 그 내부에는 더 작은 객체들로 쪼개질 수 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;시스템에 할당된 큰 책임이 작은 단위의 책임들로 분리되고, 필요한 메시지에 대해 식별해나가면서 객체들에게 책임을 할당할 수 있게 된다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;그리고, 이러한 객체들을 구현하기 위해 클래스를 추가하고, 속성과 함께 메서드를 추가하게 되면&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;시스템의 기능이 완성되어 나가는 모습을 볼 수 있을 것이다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;객체&amp;nbsp;설계는&amp;nbsp;요구사항을&amp;nbsp;식별하고&amp;nbsp;도메인&amp;nbsp;모델을&amp;nbsp;생성한&amp;nbsp;후,&lt;br /&gt;소프트웨어&amp;nbsp;클래스에&amp;nbsp;메서드들을&amp;nbsp;추가하고,&lt;br /&gt;요구사항을&amp;nbsp;충족시키기&amp;nbsp;위해&amp;nbsp;객체들&amp;nbsp;간의&amp;nbsp;메시지&amp;nbsp;전송을&amp;nbsp;정의하는&amp;nbsp;것이다.&amp;nbsp;&lt;br /&gt;- Larman (2001)&lt;/blockquote&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이러한 책임 주도 설계를 통해 &lt;b&gt;유스케이스로부터&lt;/b&gt; &lt;span style=&quot;color: #ef5369;&quot;&gt;메시지와 사용자가 달성하려는 목표&lt;/span&gt;를,&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;도메인 모델&lt;/b&gt;로부터 &lt;span style=&quot;color: #ef5369;&quot;&gt;안정적인 구조&lt;/span&gt;를 제공받아서 협력하는 객체들의 공동체를 만들어내게 된다.&lt;br /&gt;&lt;br /&gt;앞서 얘기했던 내용에 대해 큰 그림을 그려보자면 다음과 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;1444&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Ch8i5/btsDJTIkFbT/7ht1nFCcDbsaTnNts1HFNk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Ch8i5/btsDJTIkFbT/7ht1nFCcDbsaTnNts1HFNk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Ch8i5/btsDJTIkFbT/7ht1nFCcDbsaTnNts1HFNk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCh8i5%2FbtsDJTIkFbT%2F7ht1nFCcDbsaTnNts1HFNk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;730&quot; height=&quot;527&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;1444&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;도메인 모델을 중심으로 객체 구조를 설계하고, 유스케이스의 기능을 객체의 책임으로 분배&lt;/b&gt;하는 것이다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;그렇다면, 왜 도메인 모델을 기반으로 객체의 구조를 설계할까?&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;앞서 커피 예시를 들었던 것처럼, 도메인 모델을 구성하는 개념은 (정말 완전히 뒤엎는 게 아닌 이상) &lt;span style=&quot;color: #ef5369;&quot;&gt;안정적으로 유지&lt;/span&gt;되기 때문이다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;안정적으로 유지되는 이유는, 도메인 모델을 구성하는 개념들 사이의 관계는 비즈니스 규칙을 기반으로 하고 있기 때문이다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;시스템의 비즈니스 정책이 정말 크게 변하지 않는 한, 비교적 안정적으로 유지되기 때문에 전체적인 구조가 한 번에 흔들리지 않는다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;그리고, 우리는 이러한 것들을 객체지향을 사용하여 코드로서 잘 녹여내게 된다면,&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;코드의 변경으로서 역으로 도메인의 변경도 유추&lt;/b&gt;할 수 있기 때문에 (&lt;span style=&quot;color: #ef5369;&quot;&gt;가역성&lt;/span&gt;, reversibility)&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;협업의 관점으로서, 그리고 유지 보수의 관점으로서도 큰 장점을 얻어갈 수 있게 된다.&lt;/p&gt;</description>
      <category> /객체지향의 사실과 오해</category>
      <category>객체지향의사실과오해</category>
      <author>dolmeng2</author>
      <guid isPermaLink="true">https://cl8d.tistory.com/128</guid>
      <comments>https://cl8d.tistory.com/128#entry128comment</comments>
      <pubDate>Sat, 20 Jan 2024 10:00:25 +0900</pubDate>
    </item>
    <item>
      <title>[객체지향의사실과오해] 05 - 책임과 메시지</title>
      <link>https://cl8d.tistory.com/127</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;974&quot; data-origin-height=&quot;1388&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bfZxt2/btsDlzbILZ6/m3flxOBC320rcpm3fklhg1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bfZxt2/btsDlzbILZ6/m3flxOBC320rcpm3fklhg1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bfZxt2/btsDlzbILZ6/m3flxOBC320rcpm3fklhg1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbfZxt2%2FbtsDlzbILZ6%2Fm3flxOBC320rcpm3fklhg1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;371&quot; height=&quot;529&quot; data-origin-width=&quot;974&quot; data-origin-height=&quot;1388&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;옛날에 책 읽으면서 작성했던 건데 오랜만에 글 올릴 겸... 시리즈 마무리는 하면 좋을 것 같아서 올리려고 한다 :D&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; 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;style3&quot; /&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;p data-ke-size=&quot;size16&quot;&gt;이때, 객체가 행동하는 유일한 이유는, &lt;span style=&quot;color: #ef5369;&quot;&gt;다른 객체로부터 요청을 수신했기 때문&lt;/span&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 요청을 처리하기 위해 객체가 수행하는 행동을 &amp;lsquo;&lt;b&gt;책임&lt;/b&gt;&amp;rsquo;이라고 하며, 자율적인 객체란 &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;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;839&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zbo1u/btsDh87EkKP/7JzkQI6r4NHFuquIBWINA1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zbo1u/btsDh87EkKP/7JzkQI6r4NHFuquIBWINA1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zbo1u/btsDh87EkKP/7JzkQI6r4NHFuquIBWINA1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fzbo1u%2FbtsDh87EkKP%2F7JzkQI6r4NHFuquIBWINA1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2000&quot; height=&quot;839&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;839&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;lsquo;카페라떼 10잔, 아메리카노 10잔&amp;rsquo;을 제조를 지시한 상황을 생각해보자.&lt;br /&gt;셰프냥이 카페라떼를 먼저 만들고 아메리카노를 만들든, 아메리카노를 만들고 카페라떼를 만들든 상관이 없어진다. 그저 어떻게든 &lt;b&gt;단순히 주문한 목록에 대해서 수행을 완료하기만&lt;/b&gt; 하면 되는 것이다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;832&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pAEul/btsDkP62B1b/kAw0AibjSMMFLEOkKCzYQ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pAEul/btsDkP62B1b/kAw0AibjSMMFLEOkKCzYQ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pAEul/btsDkP62B1b/kAw0AibjSMMFLEOkKCzYQ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpAEul%2FbtsDkP62B1b%2FkAw0AibjSMMFLEOkKCzYQ0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2000&quot; height=&quot;832&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;832&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;하지만, 너무나도 바쁜 아침에 주문이 많이 들어온 알바냥이 잔뜩 심술이 난 나머지, 셰프냥에게 아메리카노 3잔을 만들고 카페라떼 5잔을 만든 다음에, 다시 아메리카노 7잔을 만들고 카페라떼 5잔을 만들어 달라고 했다 생각해보자.&lt;br /&gt;이러한 요청은 셰프냥에 선택할 수 있는 자유를 지나치게 제한한 것이며, &lt;b&gt;자율적으로 책임을 수행할 수 없도록 만든 것&lt;/b&gt;이다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;876&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zLnoC/btsDkS3Hxkx/JdSK4YlXD8rhg0D4KtIgN1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zLnoC/btsDkS3Hxkx/JdSK4YlXD8rhg0D4KtIgN1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zLnoC/btsDkS3Hxkx/JdSK4YlXD8rhg0D4KtIgN1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzLnoC%2FbtsDkS3Hxkx%2FJdSK4YlXD8rhg0D4KtIgN1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2000&quot; height=&quot;876&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;876&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;물론, 역으로 너무 추상적이어도 좋지 않다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;손님냥이 주문한 아메리카노, 카페라떼 10잔씩을 단순히 커피 20잔을 만들어달라고 했다 생각해보자.&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;span style=&quot;color: #ef5369;&quot;&gt;추상적인 책임을 주면서, 동시에 의도를 알 수 있도록 충분히 구체적&lt;/span&gt;이어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이와 같은 자율적인 책임은 객체가 수행할 행위를 &amp;lsquo;어떻게&amp;rsquo; 할지 지정하는 것이 아닌, &lt;span style=&quot;color: #ef5369;&quot;&gt;&amp;lsquo;무엇&amp;rsquo;을 할지 지정&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;객체는 다른 객체로부터 전송된 요청을 &amp;lsquo;수신&amp;rsquo;해야만 행동을 시작하며, 결국 외부로부터 어떠한 자극이 전달되면 본인에게 할당된 책임을 수행하기 시작하는 것이다. 우리는 이러한 자극을 &amp;lsquo;&lt;span style=&quot;color: #ef5369;&quot;&gt;메시지&lt;/span&gt;&amp;rsquo;라고 부르며, &lt;b&gt;메시지는 객체가 행동할 수 있도록 만드는 유일한 방법&lt;/b&gt;이다.&lt;br /&gt;&lt;br /&gt;&lt;/p&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;br /&gt;&lt;br /&gt;&lt;b&gt;✔️ 메시지와 메서드&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 객체는 메시지를 전송하여 다른 객체에 접근하며, 이때 메시지의 argument를 통해 처리를 위한 추가 정보를 제공할 수 있다.&lt;br /&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 메시지에 대해서 어떻게 행동할지는 송신자도 알 수 없으며, 외부에 공개된 것은 단순히 메시지 자체일 뿐이다.&lt;br /&gt;&lt;br /&gt;우리가 앞에서 이야기 했던 내용을 정리해보자면 다음과 같다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;1.&amp;nbsp;송신자는&amp;nbsp;메시지를&amp;nbsp;전송한다.&lt;br /&gt;2.&amp;nbsp;수신자는&amp;nbsp;메시지를&amp;nbsp;받아&amp;nbsp;처리할&amp;nbsp;방법을&amp;nbsp;자율적으로&amp;nbsp;선택한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;여기서, 수신자가 &lt;b&gt;내부적으로 메시지를 처리하기 위해 선택하는 방법&lt;/b&gt;이 &amp;lsquo;&lt;span style=&quot;color: #ef5369;&quot;&gt;메서드&lt;/span&gt;&amp;rsquo;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 객체는 본인이 처리할 수 있는지에 대한 여부를 확인하고, 처리가 가능하다면 어떤 식으로 처리할지 메서드를 선택하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;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;br /&gt;&lt;br /&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;/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;아메리카노 10잔, 카페라떼 10잔&amp;rsquo;을 만들라는 주문을 전달했을 때 주방에서는 무슨 일이 일어날까?&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;1136&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/M20E5/btsDdETYW9O/niAV35NmwJBDQ6UAI3J831/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/M20E5/btsDdETYW9O/niAV35NmwJBDQ6UAI3J831/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/M20E5/btsDdETYW9O/niAV35NmwJBDQ6UAI3J831/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FM20E5%2FbtsDdETYW9O%2FniAV35NmwJBDQ6UAI3J831%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2000&quot; height=&quot;1136&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;1136&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;수석 셰프냥은 뛰어난 실력으로 인해 커피를 만들 때 늘 아메리카노를 먼저 만들고, 라떼아트를 가득 담은 카페라떼를 만든다.&amp;nbsp;&lt;br /&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;그저 아메리카노 10잔과 카페라떼 10잔&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;다형성은, &lt;span style=&quot;color: #ef5369;&quot;&gt;동일한 역할을 수행할 수 있는 객체 사이의 &amp;lsquo;대체 가능성&amp;rsquo;&lt;/span&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;p data-ke-size=&quot;size16&quot;&gt;어떤 셰프냥이든, 그저 커피만 만들 줄 알면 되는 것이니까!&lt;br /&gt;&lt;br /&gt;이러한 다형성의 이점은, &lt;span style=&quot;color: #ef5369;&quot;&gt;수신자의 종류를 캡슐화&lt;/span&gt;를 할 수 있다는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;알바냥의 입장에서는 내부적으로 어떤 셰프냥이 요리를 하든 그저 &amp;lsquo;커피를 만들 줄 아는&amp;rsquo; 고양이라면 충분히 협력할 수 있으며, 셰프냥이 아니더라도 점장냥이 나와서 커피를 만들어 낼 수도 있는 것이다. (물론, 고양이가 아니어도 된다. 그저 커피를 만들 줄만 알면 되는 것이다!)&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다형성을 잘 활용하게 된다면 메시지에 대한 결합도를 낮출 수 있기 때문에,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유연하고 확장성과 재사용성이 높은 객체 지향의 특징을 만족할 수 있게 될 것이다.&lt;br /&gt;&lt;br /&gt;&lt;/p&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;br /&gt;&lt;br /&gt;&lt;b&gt;✔️ 메시지의 힘&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 다형성이 모든 걸 책임지는 것처럼 말했지만, 실질적으로는 &lt;span style=&quot;color: #ef5369;&quot;&gt;메시지가 송-수신자 사이의 결합도를 낮추기 때문에&lt;/span&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;br /&gt;&lt;br /&gt;기본적으로&amp;nbsp;코드를&amp;nbsp;작성할&amp;nbsp;때&amp;nbsp;많이&amp;nbsp;하는&amp;nbsp;실수가,&amp;nbsp;데이터를&amp;nbsp;위주로&amp;nbsp;생각하면서&amp;nbsp;협력이&amp;nbsp;아닌&amp;nbsp;객체&amp;nbsp;내부의&amp;nbsp;데이터&amp;nbsp;구조를&amp;nbsp;먼저&amp;nbsp;생각하고,&amp;nbsp;&lt;b&gt;데이터&amp;nbsp;조작에&amp;nbsp;필요한&amp;nbsp;연산들을&amp;nbsp;나중에&amp;nbsp;고려하는&amp;nbsp;것&lt;/b&gt;이다. (필자는 이 대목에서 굉장히 찔렸다. 필요한 기능을 먼저 나열하고, 해당 기능에 짜맞추면서 코드를 작성하는 경우가 굉장히 많았으니까.)&lt;br /&gt;&lt;br /&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;우리는 협력 관계에서 다른 객체에게 어떤 것을 제공하고, 어떤 것을 얻을 수 있는지 판단할 때 좋은 객체 지향적 설계를 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 결과적으로 나무를 보는 게 아니라 &amp;lsquo;&lt;span style=&quot;color: #ef5369;&quot;&gt;협력 관계&lt;/span&gt;&amp;rsquo; 라는 숲을 바라봐야 하는 것이다.&lt;br /&gt;&lt;br /&gt;&lt;/p&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;br /&gt;&lt;br /&gt;&lt;b&gt;✔️ 책임 주도 설계&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체가 본인에게 할당된 책임을 수행해야 하는데, 본인에게 없는 정보가 필요하다면 외부 객체에게 메시지를 전송하여 물어봐야 한다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;771&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bqcQmV/btsDkTuKIVo/uspk4EiDJEghO1uDF2eLkk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bqcQmV/btsDkTuKIVo/uspk4EiDJEghO1uDF2eLkk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bqcQmV/btsDkTuKIVo/uspk4EiDJEghO1uDF2eLkk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbqcQmV%2FbtsDkTuKIVo%2Fuspk4EiDJEghO1uDF2eLkk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2000&quot; height=&quot;771&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;771&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;예를 들어, 손님냥이 알바냥에게 샌드위치 10개를 사고 싶다고 말했는데, 재고가 2개밖에 없는 상황이라면?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나머지 8개의 샌드위치를 손님냥에게 주기 위해서는 얼마나 기다려야 하는지 알바냥이 가지고 있는 정보로는 알 수 없다.&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-origin-width=&quot;2000&quot; data-origin-height=&quot;2332&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Hm7eq/btsDjZbcXMM/Rc7ZjfwmjJMIcGu94x7KO1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Hm7eq/btsDjZbcXMM/Rc7ZjfwmjJMIcGu94x7KO1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Hm7eq/btsDjZbcXMM/Rc7ZjfwmjJMIcGu94x7KO1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHm7eq%2FbtsDjZbcXMM%2FRc7ZjfwmjJMIcGu94x7KO1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;355&quot; height=&quot;414&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;2332&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;&lt;br /&gt;게임 이미지를 참고하자면... 셰프냥은 아마 샌드위치 1개 만드는데 4초가 걸릴 테니 총 32초 정도가 걸릴 것이다 (?)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;각 객체들이 적절한 책임을 가지고 있다면, 이러한 상황에서 어떤 객체에게 질의를 해야 할지 쉽게 판단할 수 있게 된다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;  결과적으로 책임 주도 설계는 객체들 간의 주고받는 메시지를 기반으로 적절한 역할과 책임, 협력을 발견하는 것이다.&amp;nbsp;&lt;br /&gt;이때, 도움이 필요한 객체는 메시지를 결정하고, 해당 메시지를 처리하기에 적합한 객체를 선택할&amp;nbsp;수&amp;nbsp;있다.&amp;nbsp;&lt;br /&gt;메시지는&amp;nbsp;수신자가&amp;nbsp;해야&amp;nbsp;하는&amp;nbsp;책임을&amp;nbsp;결정한다.&lt;/blockquote&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;br /&gt;&lt;b&gt;What / Who 사이클&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 언급한 책임 주도 설계를 잘 활용하기 위하여, What-Who 사이클을 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 객체 사이의 협력 관계를 설계하기 위해, 어떤 행위 (what)를 수행해야 하는지 결정한 다음, 누가 (who) 그 행위를 수행할 것인지 결정하는 것이다. 여기서 &lt;b&gt;what은 메시지, who는 이를 처리할 객체&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요한 점은, what을 먼저 설정함으로서 &lt;span style=&quot;color: #ef5369;&quot;&gt;어떤 객체가 어떤 기능을 가지고 있는지 먼저 판단하지 않는다는 것&lt;/span&gt;이다. 내가 필요한 행위를 생각하고, 이에 대해 처리할 수 있는 객체를 선택한다는 점이다. 위와 같은 사이클을 반복하게 되면, 객체의 인터페이스를 구성할 수 있게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;892&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bxkZ0u/btsDkBOFbJJ/YDJUd45bRJrBbUgipYKQX0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bxkZ0u/btsDkBOFbJJ/YDJUd45bRJrBbUgipYKQX0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bxkZ0u/btsDkBOFbJJ/YDJUd45bRJrBbUgipYKQX0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbxkZ0u%2FbtsDkBOFbJJ%2FYDJUd45bRJrBbUgipYKQX0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2000&quot; height=&quot;892&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;892&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약, 알바냥이 손님냥에게 질문을 들었을 때 아무 생각 없이 &amp;lsquo;흠&amp;hellip; &lt;b&gt;나보다 오래 알바한 선배 알바냥이 알겠지?&lt;/b&gt;&amp;rsquo; 라는 생각으로 선배냥에게 여쭤봤다고 생각해보자. 이는 메시지를 먼저 생각하지 않고, 선배 알바냥 (Who)에게 먼저 물어봤기 때문에 What/Who 사이클을 위반하게 된 것이다. 항상 목적을 먼저 생각하고, 이에 대한 객체를 할당하자.&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;br /&gt;&lt;b&gt;묻지 말고 시켜라! (= 디미터의 법칙)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;What / Who 사이클에서 What을 결정했다면, &lt;span style=&quot;color: #ef5369;&quot;&gt;어떤 객체가 이를 수신할지 모르기 때문에&lt;/span&gt; &lt;b&gt;해당 객체에 대해서 물어볼 수가 없다&lt;/b&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;  메시지가 어떻게 해야 하는지 지시하지 말고, 무엇을 해야 하는지 요청하라. &lt;br /&gt;메시지를 믿게 되면 자율적인 책임도 함께 따라오게 된다.&lt;/blockquote&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;style3&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;✔️ 객체 인터페이스&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 인터페이스는 다음과 같은 특징을 가진다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;- 내부 구조나 동작은 알 필요 없이 겉으로 드러나는 사용법으로 쉽게 사용이 가능하다.&lt;br /&gt;- 내부 구조나 작동 방식을 변경하더라도 사용자에게 영향을 주지 않는다.&lt;br /&gt;- 구체화된 대상이 변경되더라도 동일한 인터페이스를 제공하면 아무런 문제 없이 상호작용이 가능하다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;객체의 인터페이스는 객체가 수신할 수 있는 메시지의 목록으로 구성되기 때문에, &lt;b&gt;객체가 어떤 메시지를 수신하는지에 따라서 달라진다&lt;/b&gt;.&lt;br /&gt;여기서 말하는 인터페이스는 외부에서 접근 가능한 것과, 내부에서만 접근 가능한 것으로 나누어지며,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 둘의 차이는 메시지를 보낸 주체가 다른 객체인지, 객체 자기 자신인지에 따라서 달라진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;  객체의 책임은 자율적이다. 스스로가 결졍해야 한다.&lt;br /&gt;객체는 메시지를 통해 서로 소통하며, 메서드를 통해 메시지를 어떻게 수행할지 결정한다.&lt;br /&gt;객체는&amp;nbsp;인터페이스를&amp;nbsp;통해&amp;nbsp;이러한&amp;nbsp;메시지를&amp;nbsp;받을&amp;nbsp;수&amp;nbsp;있다.&lt;/blockquote&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;style3&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;br /&gt;&lt;b&gt;✔️&amp;nbsp;인터페이스와&amp;nbsp;구현의&amp;nbsp;분리&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인터페이스의 3가지 원칙은 다음과 같다.&lt;br /&gt;- 인터페이스는 추상적이어야 한다.&lt;br /&gt;- 외부에서 사용할 필요가 없다면 최대한 노출하지 말아야 한다.&lt;br /&gt;- 객체의 내부와 외부를 구현해야 한다.&lt;br /&gt;&lt;br /&gt;여기서 말하는 객체의 내부가 무엇일까?&lt;br /&gt;이는, 객체를 구성하지만, 외부에 공개된 인터페이스 (=공용 인터페이스)에 포함되지 않는 모든 것을 의미한다. 이를 곧 &amp;lsquo;구현&amp;rsquo;이라고 한다.&lt;br /&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;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;br /&gt;&lt;b&gt;✔️&amp;nbsp;객체의&amp;nbsp;자율성은&amp;nbsp;왜&amp;nbsp;이렇게&amp;nbsp;중요할까?&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체의 책임이 자율적일수록 협력 관계는 이해하기 쉬워지고, 유연하게 변경할 수 있게 되며, &lt;span style=&quot;color: #ef5369;&quot;&gt;이는 협력에 대한 전체 품질을 결정&lt;/span&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;불필요한 책임까지 안게 되는 것&lt;/b&gt;이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한, 셰프냥이 커피 만드는 순서를 변경하더라도, 알바냥에게 아메리카노 10잔을 만드는 사실은 변함이 없기 때문에 외부에 불필요하게 영향을 끼치지 않게 된다. &lt;b&gt;내부에서 무엇을 하든 협력 관계는 그대로 유지&lt;/b&gt;된다는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;만약 셰프냥이 휴가를 갔다면?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아메리카노 10잔을 만드는 주체가 셰프냥이 아니라 다른 주체여도 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자율적인 책임으로 인해서, 해당 행위를 수행할 수 있기만 하면 되기 때문이다.&lt;br /&gt;&lt;br /&gt;또한, 객체의 책임이 자율적이면 &lt;b&gt;객체의 존재 이유도 명확하게 표현&lt;/b&gt;될 수 있다.&amp;nbsp;&lt;br /&gt;자율적으로 수행하게 되면, 이는 곧 객체의 목적을 달성하는데 집중을 하게 되고, 곧 객체 자체가 목적을 달성하기 위해, &lt;span style=&quot;color: #ef5369;&quot;&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;/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;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;/p&gt;</description>
      <category> /객체지향의 사실과 오해</category>
      <category>객체지향의사실과오해</category>
      <author>dolmeng2</author>
      <guid isPermaLink="true">https://cl8d.tistory.com/127</guid>
      <comments>https://cl8d.tistory.com/127#entry127comment</comments>
      <pubDate>Wed, 10 Jan 2024 20:08:52 +0900</pubDate>
    </item>
    <item>
      <title>[JPA] @Embedded 사용 시 주의할 점, 레거시 코드 리팩터링하기</title>
      <link>https://cl8d.tistory.com/126</link>
      <description>&lt;h4 style=&quot;text-align: left;&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; style=&quot;text-align: left;&quot;&gt;너무 오랜만에 작성하는 블로그 글...! 최근에 사내에서 매우 많이 쓰이는 도메인에 대해서 리팩터링을 진행했었는데, 거기서 만났던 이슈들과 간단한 생각 기록을 블로그에 남기면 좋을 것 같아서 작성하고자 한다. 이미 팀 내에서 공유도 했었고, 동기들에게도 공유한 내용이기도 하고, 사실 별거 아닌 거라 그냥 간단하게만 정리할 예정이다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot;&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h4 style=&quot;text-align: left;&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; style=&quot;text-align: left;&quot;&gt;리팩터링을 진행하고자 한 도메인은 우리 팀에서 매우 중요한 비즈니스 도메인 객체 중 하나였고, 이미 안정적으로 잘 운영되고 있지만 굉장히 옛날부터 레거시로 내려오고 있던 객체였기 때문에 신규 입사자가 보기에 좋은 코드가 아니라는 생각이 들었다.&lt;br&gt;사내 도메인인 만큼 그대로 가져올 수는 없지만, 비슷한 느낌으로 아래와 같이 세팅을 진행해보았다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;CREATE TABLE `student_group` (
&amp;nbsp;&amp;nbsp;`id` bigint NOT NULL AUTO_INCREMENT,
&amp;nbsp;&amp;nbsp;`name` varchar(255) DEFAULT NULL,
&amp;nbsp;&amp;nbsp;PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb3


CREATE TABLE `student` (
&amp;nbsp;&amp;nbsp; `id` bigint NOT NULL AUTO_INCREMENT,
&amp;nbsp;&amp;nbsp; `city` varchar(255) DEFAULT NULL,
&amp;nbsp;&amp;nbsp; `county` varchar(255) DEFAULT NULL,
&amp;nbsp;&amp;nbsp; `district` varchar(255) DEFAULT NULL,
&amp;nbsp;&amp;nbsp; `age` int NOT NULL,
&amp;nbsp;&amp;nbsp; `name` varchar(255) DEFAULT NULL,
&amp;nbsp;&amp;nbsp; `group_id` bigint NOT NULL,
&amp;nbsp;&amp;nbsp; PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;간단하게 생각하면 학생의 그룹과 학생에 대한 테이블이고, 이에 대한 엔티티 매핑은 아래와 같았다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Entity
class Student(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;val name: String,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;val age: Int,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;group: StudentGroup
) {

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@Id
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@GeneratedValue(strategy = GenerationType.IDENTITY)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;val id: Long = 0L

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@ManyToOne
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@JoinColumn(name = &quot;group_id&quot;, insertable = false, updatable = false)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;val group: StudentGroup = group

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@Column(name = &quot;group_id&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;val groupId: Long = group.id

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@Column
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;val city: String? = null

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@Column
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;val county: String? = null

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@Column
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;val district: String? = null
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;실제 도메인에서는 30개가 넘는 필드들이 있었으며, 굉장히 많은 일들을 하고 있는 객체였기 때문에 파악하기 쉽지 않은 객체였다.&lt;br&gt;나는 위의 엔티티를 보고, 다음과 같은 2가지 포인트에 집중하였다.&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style3&quot;&gt;1. group이라는 엔티티를 이미 의존하고 있는데, 꼭 groupId라는 필드를 따로 두어야 할까?&lt;br&gt;2. 맥락상으로 연관이 있는 필드들은 &lt;span style=&quot;color: #EF5369;&quot;&gt;@Embedded&lt;/span&gt;로&amp;nbsp;묶어두면&amp;nbsp;가독성이&amp;nbsp;올라가지&amp;nbsp;않을까?&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&lt;br&gt;먼저, 1번의 경우 &lt;b&gt;JPA에 대한 의존성을 최대한 낮추고 싶었다&lt;/b&gt;. 이미 @JoinColumn으로 엔티티 직접 참조를 통해 의존성을 가지고 있음에도, @Column을 통해 간접 참조까지 가지고 있는 게 처음 본 사람의 입장에서는 인지 오류를 줄 수 있는 포인트라고 생각했었다. (또한, 현재는 group_id만 있지만 실질적으로 엔티티 참조를 가지고 있던 객체들이 모두 위와 같이 별도로 Id를 위한 필드들이 존재하고 있었다.) 언뜻 보면 group_id라는 컬럼이 2번 있는 것인가? 라는 오류를 줄 수 있을 것 같았다.&lt;br&gt;&amp;nbsp;&lt;br&gt;2번의 경우, 현재 위의 엔티티를 실질적으로 도메인 객체로 사용하고 있다 보니 &lt;b&gt;굉장히 많은 비즈니스 로직에서 사용되는 객체&lt;/b&gt;였다. 그러나 수많은 필드 및 메서드들이 주석 없이 단순 필드명으로, 혹은 테이블 스키마에 작성된 커멘트 내용을 보고 파악을 진행하는 게 까다로웠다. (사실 Persistence Layer에 있는 엔티티가 여러 곳에서 도메인 객체로 쓰이는 것부터가 굉장히 어색하지만, 레거시라는 게 생각처럼 쉽게 리팩터링이 되는 부분이 아니었기에 우선 해당 부분에 대해서는 다음 기회에 고쳐나가고자 했다.)&lt;br&gt;&lt;br&gt;아무튼, 2가지 포인트를 바탕으로 아래와 같이 리팩터링을 진행하고자 했다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Entity
class Student(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;val name: String,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;val age: Int,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;group: StudentGroup
) {

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@Id
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@GeneratedValue(strategy = GenerationType.IDENTITY)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;val id: Long = 0L

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@ManyToOne
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@JoinColumn(name = &quot;group_id&quot;, insertable = false, updatable = false)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;val group: StudentGroup = group
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@Embedded
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;val address: Address = Address.init()
}

@Embeddable
class Address protected constructor() {

&amp;nbsp;&amp;nbsp;@Column
&amp;nbsp;&amp;nbsp;val city: String? = null

&amp;nbsp;&amp;nbsp;@Column
&amp;nbsp;&amp;nbsp;val county: String? = null

&amp;nbsp;&amp;nbsp;@Column
&amp;nbsp;&amp;nbsp;val district: String? = null

&amp;nbsp;&amp;nbsp;companion object {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;fun init(): Address {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return Address()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;단순 코드상으로는 정말 간단하다. 엔티티를 참조하고 있는 경우 간접 참조를 위한 논리적 FK 값을 제거하고, '주소'라는 도메인에 맞춰 Address라는 새로운 클래스를 정의하여&lt;span style=&quot;color: #EF5369;&quot;&gt; @Embedded, @Embeddable&lt;/span&gt;를 통해 리팩터링을 진행하였다.&lt;br&gt;&amp;nbsp;&lt;br&gt;처음에는 비즈니스 로직의 수정 없이, 단순히 클래스를 추가한 부분이기 때문에 아무 문제가 없을 것이라고 생각했었다.&lt;br&gt;그러나, 실제로는 매우 큰 2가지의 문제점이 존재했었는데, 이는 테스트 케이스로 한 번 살펴보자.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot;&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  Case 1 - JPA 사용 시 옵션을 제대로 보자&lt;/b&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;먼저,&amp;nbsp;정말&amp;nbsp;간단하게&amp;nbsp;저장&amp;nbsp;및&amp;nbsp;조회하는&amp;nbsp;서비스&amp;nbsp;코드가&amp;nbsp;아래와&amp;nbsp;같이&amp;nbsp;존재한다고&amp;nbsp;생각해보자.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Service
class StudentService(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private val studentRepository: StudentRepository,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private val groupRepository: StudentGroupRepository
) {

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;fun saveGroup(name: String) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;val group = StudentGroup(name)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;groupRepository.save(group)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;fun saveStudent(group: StudentGroup, name: String, age: Int): Student {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;val student = Student(name, age, group)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return studentRepository.save(student)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;그리고, 그룹을 저장하고 해당 그룹에 대해 학생을 저장하는 코드를 테스트 코드로 작성해보자.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@SpringBootTest
@TestConstructor(autowireMode = TestConstructor.AutowireMode.ALL)
@ActiveProfiles(&quot;test&quot;)
class StudentServiceTest(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private val studentService: StudentService
) {

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@Test
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;fun test() {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;val group = studentService.saveGroup(&quot;그룹&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;studentService.saveStudent(group, &quot;학생&quot;, 20)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;위의&amp;nbsp;테스트&amp;nbsp;결과는&amp;nbsp;어떻게&amp;nbsp;나올까?&amp;nbsp;언뜻&amp;nbsp;보면&amp;nbsp;성공할&amp;nbsp;것&amp;nbsp;같다.&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2010&quot; data-origin-height=&quot;358&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bXKY5F/btsB3ssQVHM/OkKXPjUfN5y00UEGqOqM21/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bXKY5F/btsB3ssQVHM/OkKXPjUfN5y00UEGqOqM21/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bXKY5F/btsB3ssQVHM/OkKXPjUfN5y00UEGqOqM21/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbXKY5F%2FbtsB3ssQVHM%2FOkKXPjUfN5y00UEGqOqM21%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2010&quot; height=&quot;358&quot; data-origin-width=&quot;2010&quot; data-origin-height=&quot;358&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;하지만, 실제로는 위와&amp;nbsp;같이&amp;nbsp;&lt;b&gt;Field&amp;nbsp;'group_id'&amp;nbsp;doesn't&amp;nbsp;have&amp;nbsp;a&amp;nbsp;default&amp;nbsp;value&lt;/b&gt;&amp;nbsp;라는&amp;nbsp;오류가&amp;nbsp;발생한다.&lt;br&gt;현재 재현할 때는 위 사진처럼 어떤 쿼리가 발생했는지 오류 메시지로 함께 나왔지만, 당시에는 default value가 없다는 오류만 발생했었기 때문에 바로 인지하지 못했다. (또한, 위 테스트 코드의 경우 비즈니스 로직이 없기 때문에 당연히 이런 흐름이 잘 보이지만, 내가 부딪혔던 상황에서는 그 사이에 비즈니스 로직이 너무 많아서 파악하기가 어려웠다.)&lt;br&gt;&amp;nbsp;&lt;br&gt;그래서 나는 처음에는 student를 저장할 때 &lt;b&gt;student group이 영속화 되지 않은 객체가 넘어와서&lt;/b&gt; (혹은 객체가 아예 null로 넘어와서) group id가 null로 나오는 것인지 고민했었다. 그래서 디버깅을 통해 student가 저장되는 시점의 group을 체크하였다.&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1294&quot; data-origin-height=&quot;408&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wpWmn/btsB7hRlI5H/CK5eFvJxpynxIGWNlwGAqk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wpWmn/btsB7hRlI5H/CK5eFvJxpynxIGWNlwGAqk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wpWmn/btsB7hRlI5H/CK5eFvJxpynxIGWNlwGAqk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwpWmn%2FbtsB7hRlI5H%2FCK5eFvJxpynxIGWNlwGAqk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1294&quot; height=&quot;408&quot; data-origin-width=&quot;1294&quot; data-origin-height=&quot;408&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;하지만, 사진을 보면 당연하게도 group 정보와 id 값이 잘 있는 것을 확인할 수 있다.&amp;nbsp;&lt;br&gt;그렇다면 뭐가 문제였을까 한참을 고민하다가, 선언되어 있는 Entity를 주의 깊게 살펴보니 바로 알아챌 수 있었다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@JoinColumn(name = &quot;group_id&quot;, insertable = false, updatable = false)&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;바로, &lt;span style=&quot;color: #EF5369;&quot;&gt;insertable = false 옵션&lt;/span&gt;으로 인해 오류가 발생했던 것이었다.&lt;br&gt;정확하게 말하면, 스키마 상으로는 &lt;b&gt;group_id가 NOT NULL로 지정되어 있던 상태&lt;/b&gt;에서, insertable = false로 인해 &lt;b&gt;insert 시에 해당 컬럼을 제외하고 쿼리가 발생&lt;/b&gt;했기 때문에 오류가 발생했던 것이다.&lt;br&gt;&amp;nbsp;&lt;br&gt;여기서 내가 초래했던 실수는 다음과 같다고 생각한다.&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style3&quot;&gt;1. 개발자가 작성한 Entity가 테이블 형상과 그대로 일치할 것이라 생각하고 테이블 스키마를 주의깊게 살펴보지 않은 점&lt;br&gt;2. 기존 레거시 코드를 그대로 옮겨야 잘 동작한다고 생각하고, 옵션에 대해 주의 깊게 살펴보지 않은 점.&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;1번의 경우, 다르게 생각하면 Persistence Layer인 Entity가 DB의 형상과 다르게 유지되면서 (사내에서는 flyway 같은 마이그레이션 툴을 사용하지 않는다.) 발생된 인지 오류였다.&amp;nbsp;필드가 당연히 nullable 하게 선언되어 있을 것이라고 생각했다.&lt;br&gt;2번의 경우, 사실 개인적으로 리팩터링을 진행하면서 조금만 수정해도 예상과 다르게 동작했던 점이 여러 번 있다 보니 '최대한 기존 코드를 유지해야겠다!'라는 마음 가짐에서 했던 행동이었다. 주의깊게 봤더라면 처음부터 이상한 점을 알아챌 수 있었을 텐데 부끄러울 정도로 명확한 내 실수 그 자체였다.&lt;br&gt;&lt;br&gt;그렇다면, 지금까지는&amp;nbsp;어떻게&amp;nbsp;insert가&amp;nbsp;되었던&amp;nbsp;것일까?&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@ManyToOne
@JoinColumn(name = &quot;group_id&quot;, insertable = false, updatable = false)
val group: StudentGroup = group

@Column(name = &quot;group_id&quot;)
val groupId: Long = group.id&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;br&gt;내가 리팩터링을 하려고 했던 그&lt;b&gt; groupId라는 필드를 통해서 insert가 정상적으로 진행&lt;/b&gt;이 되고 있던 상태였다.&lt;br&gt;아마 코드를 처음 작성하신 분의 의도로는 (확실하지는 않지만) Spring data JPA를 사용할 때 메서드명을 통해 select를 많이 하는데, group이라는 도메인 대신에 id를 통해서 바로 값을 가져올 수 있도록 하기 위해 따로 분리하신 게 아닐까 싶다.&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2042&quot; data-origin-height=&quot;212&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CZvEm/btsB2uSfNR0/JPgcuCXws4D8TC22QKJo8K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CZvEm/btsB2uSfNR0/JPgcuCXws4D8TC22QKJo8K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CZvEm/btsB2uSfNR0/JPgcuCXws4D8TC22QKJo8K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCZvEm%2FbtsB2uSfNR0%2FJPgcuCXws4D8TC22QKJo8K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2042&quot; height=&quot;212&quot; data-origin-width=&quot;2042&quot; data-origin-height=&quot;212&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Unable to build Hibernate SessionFactory; nested exception is org.hibernate.MappingException: Column 'group_id' is duplicated in mapping for entity&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;또한, 둘 다 insert가 가능한 상태라면 위와 같이 오류가 발생하기 때문에 엔티티에 대해서는 insertable=false로 막아두신 걸로 추측된다.&lt;br&gt;&amp;nbsp;&lt;br&gt;이전에 작성했던 엔티티를 다시 롤백한 다음 테스트 코드를 돌려보자.&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2024&quot; data-origin-height=&quot;928&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bXpb8j/btsB8V77z5S/XgqlqOjmKmiGhnxKLl39D0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bXpb8j/btsB8V77z5S/XgqlqOjmKmiGhnxKLl39D0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bXpb8j/btsB8V77z5S/XgqlqOjmKmiGhnxKLl39D0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbXpb8j%2FbtsB8V77z5S%2FXgqlqOjmKmiGhnxKLl39D0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2024&quot; height=&quot;928&quot; data-origin-width=&quot;2024&quot; data-origin-height=&quot;928&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;바인딩 파라미터 옵션을 켜고 확인해보면,&amp;nbsp;&lt;b&gt;group_id에&amp;nbsp;아이디&amp;nbsp;값이&amp;nbsp;제대로&amp;nbsp;들어간&amp;nbsp;것을&amp;nbsp;확인&lt;/b&gt;할&amp;nbsp;수&amp;nbsp;있었다.&lt;br&gt;위 문제를 해결하기 위해서는 엔티티 직접 참조 유지 + insertable = false를 사용하거나, 기존 형상을 유지하면 된다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot;&gt;&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;br&gt;&lt;b&gt;  Case 2 - 알 수 없는 하이버네이트&lt;/b&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;사실 위 문제의 경우 비즈니스 로직 수행 후 금방 잡힐 수 있는 케이스였지만, 이번 케이스는 조금 신기한 경우이다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;fun findStudent(id: Long): Student {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return studentRepository.findById(id).orElseThrow()
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;위와 같이 조회를 위한 비즈니스가 추가되었고, 실질적으로 아래와 같은 로직이었다고 생각해보자.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Test
fun test2() {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;val group = studentService.saveGroup(&quot;그룹&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;val student = studentService.saveStudent(group, &quot;학생&quot;, 20)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;val findStudent = studentService.findStudent(student.id)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// 학생의 address 정보를 다른 api로 전달한다고 가정
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;callExternalApi(findStudent.address)
}

fun callExternalApi(address: Address) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// 대충 address의 각 요소에 대해서 접근하여 호출한다고 가정
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;println(&quot;address.city = ${address.city}&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;println(&quot;address.county = ${address.county}&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;println(&quot;address.district = ${address.district}&quot;)
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;이때,&amp;nbsp;위&amp;nbsp;코드는&amp;nbsp;어떤&amp;nbsp;문제가&amp;nbsp;발생할까?&lt;br&gt;당시 내가 이해했던 흐름으로는, address의 경우 기본값으로&lt;b&gt; Address.init()을 통해 빈 객체를 할당&lt;/b&gt;해주었기 때문에 실질적으로 city, county, district 값이 null인 address가 할당이 되었을 것이라고 생각했었다.&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1998&quot; data-origin-height=&quot;446&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2Qkq9/btsB5CBY07t/CYNjlAwlhNWGNbzkZKKARK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2Qkq9/btsB5CBY07t/CYNjlAwlhNWGNbzkZKKARK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2Qkq9/btsB5CBY07t/CYNjlAwlhNWGNbzkZKKARK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2Qkq9%2FbtsB5CBY07t%2FCYNjlAwlhNWGNbzkZKKARK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1998&quot; height=&quot;446&quot; data-origin-width=&quot;1998&quot; data-origin-height=&quot;446&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;하지만, &lt;span style=&quot;color: #EF5369;&quot;&gt;실제로&amp;nbsp;위&amp;nbsp;코드의&amp;nbsp;경우&amp;nbsp;NPE가&amp;nbsp;발생&lt;/span&gt;하게&amp;nbsp;된다.&lt;br&gt;코틀린 같은 언어에서 NPE가 발생한다는 건 굉장히 주의깊게 살펴봐야 하는 부분이라서, 처음에는 매우 당황했었다.&lt;br&gt;또한, 비즈니스 로직에서 무조건 발생하는 게 아니라 분기에 따라서 발생한 로직이었어서 잘못하면 인지하지 못하고 배포를 나갔을 수도 있던 부분이라서 한 편으로는 지금 잡혀서 다행이라고 생각했었다... ㅎㅎ&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1342&quot; data-origin-height=&quot;368&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbez9H/btsB6nR4x3n/FItfoGDVy8AjHnkMysldP0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbez9H/btsB6nR4x3n/FItfoGDVy8AjHnkMysldP0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbez9H/btsB6nR4x3n/FItfoGDVy8AjHnkMysldP0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbbez9H%2FbtsB6nR4x3n%2FFItfoGDVy8AjHnkMysldP0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1342&quot; height=&quot;368&quot; data-origin-width=&quot;1342&quot; data-origin-height=&quot;368&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;아무튼, 실제로 디버깅을 통해 확인해보아도 위와 같이 address에 null이 들어간 것을 확인할 수 있었다.&lt;br&gt;&lt;span style=&quot;color: #EF5369;&quot;&gt;Address&amp;nbsp;객체&amp;nbsp;자체를&amp;nbsp;non-nullable한&amp;nbsp;타입으로&amp;nbsp;설정&lt;/span&gt;하였는데&amp;nbsp;어떻게&amp;nbsp;NPE가&amp;nbsp;발생한&amp;nbsp;것일까?&lt;br&gt;처음에는 디컴파일을 통해, 뭔가 JPA의 내가 모르는 기능으로 인해 nullable한 타입으로 선언된다고 생각했었다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1388&quot; data-origin-height=&quot;1382&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/VF3Io/btsB7lGejpx/65JDh6VUgYcMmjeYOhrLo0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/VF3Io/btsB7lGejpx/65JDh6VUgYcMmjeYOhrLo0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/VF3Io/btsB7lGejpx/65JDh6VUgYcMmjeYOhrLo0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVF3Io%2FbtsB7lGejpx%2F65JDh6VUgYcMmjeYOhrLo0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;634&quot; height=&quot;631&quot; data-origin-width=&quot;1388&quot; data-origin-height=&quot;1382&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;하지만, 필드와 getter에 대해 모두&amp;nbsp;&lt;b&gt;@NotNull&amp;nbsp;어노테이션&lt;/b&gt;이&amp;nbsp;붙어있는&amp;nbsp;것을&amp;nbsp;확인할&amp;nbsp;수&amp;nbsp;있었다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;773&quot; data-origin-height=&quot;773&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IAOyJ/btsB8UOUGVu/KGrkZgUhk9dMhYMfA6bya0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IAOyJ/btsB8UOUGVu/KGrkZgUhk9dMhYMfA6bya0/img.jpg&quot; data-alt=&quot;처음 오류를 봤을 때 나의 심정...&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IAOyJ/btsB8UOUGVu/KGrkZgUhk9dMhYMfA6bya0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIAOyJ%2FbtsB8UOUGVu%2FKGrkZgUhk9dMhYMfA6bya0%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;394&quot; height=&quot;394&quot; data-origin-width=&quot;773&quot; data-origin-height=&quot;773&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;처음 오류를 봤을 때 나의 심정...&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;br&gt;처음에는 내가 코틀린을 잘못 알고 있는 건가 싶어서 막 구글링을 하다가 아래와 같은 이슈를 발견하였다.&lt;/p&gt;&lt;figure data-ke-type=&quot;opengraph&quot; data-og-title=&quot;[HHH-7610] - Hibernate JIRA&quot; data-ke-align=&quot;alignCenter&quot; data-og-description=&quot;&quot; data-og-host=&quot;hibernate.atlassian.net&quot; data-og-source-url=&quot;https://hibernate.atlassian.net/browse/HHH-7610&quot; data-og-image=&quot;&quot; data-og-url=&quot;https://hibernate.atlassian.net/browse/HHH-7610&quot;&gt;&lt;a href=&quot;https://hibernate.atlassian.net/browse/HHH-7610&quot; target=&quot;_blank&quot; data-source-url=&quot;https://hibernate.atlassian.net/browse/HHH-7610&quot;&gt;&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('')&quot;&gt; &lt;/div&gt;&lt;div class=&quot;og-text&quot;&gt;&lt;p class=&quot;og-title&quot;&gt;[HHH-7610] - Hibernate JIRA&lt;/p&gt;&lt;p class=&quot;og-desc&quot;&gt;&lt;/p&gt;&lt;p class=&quot;og-host&quot;&gt;hibernate.atlassian.net&lt;/p&gt;&lt;/div&gt;&lt;/a&gt;&lt;/figure&gt;&lt;blockquote data-ke-style=&quot;style3&quot;&gt;When all of the values in an @Embedded object are NULL, Hibernate sets the field in the parent object to null.&amp;nbsp;&lt;br&gt;This&amp;nbsp;can&amp;nbsp;lead&amp;nbsp;to&amp;nbsp;NullPointerExceptions&amp;nbsp;if&amp;nbsp;not&amp;nbsp;handled&amp;nbsp;correctly.&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;요약: &lt;span style=&quot;color: #EF5369;&quot;&gt;Embedded 객체의 모든 필드가 null이면 해당 객체도 null로 하이버네이트가 세팅&lt;/span&gt;을 한다.&lt;br&gt;&lt;br&gt;가만히 생각해보면 JPA의 경우 리플랙션을 통해 객체를 세팅하니까, NotNull로 세팅하더라도 충분히 null로 세팅이 가능할 것 같다는 생각이 들었다. (사실 이부분을 직접 재현해보려고 했는데 생각보다 공수가 더 들어가서 우선 패스...)&lt;br&gt;&amp;nbsp;&lt;br&gt;아무튼, 이를 해결하기 위해 hibernate의 릴리즈 노트를 봤는데 해결책이 있었다! (5.1 version)&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1816&quot; data-origin-height=&quot;352&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bse4kz/btsB17bFNMw/XUuOKRFKIF6EjaBXhLWm9k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bse4kz/btsB17bFNMw/XUuOKRFKIF6EjaBXhLWm9k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bse4kz/btsB17bFNMw/XUuOKRFKIF6EjaBXhLWm9k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbse4kz%2FbtsB17bFNMw%2FXUuOKRFKIF6EjaBXhLWm9k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1816&quot; height=&quot;352&quot; data-origin-width=&quot;1816&quot; data-origin-height=&quot;352&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;spring.jpa.properties.hibernate.create_empty_composites.enabled=true&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;바로, 위의 옵션을 true로 지정해주면 되는 것이었다.&lt;br&gt;궁금해서 하이버네이트 쪽을 살짝 찾아봤는데 다음과 같이 구현되어 있다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public ComponentMetamodel(Component component, MetadataBuildingOptions metadataBuildingOptions) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;...
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;final ConfigurationService cs = component.getMetadata().getMetadataBuildingOptions().getServiceRegistry()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.getService(ConfigurationService.class);

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;this.createEmptyCompositesEnabled = ConfigurationHelper.getBoolean(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Environment.CREATE_EMPTY_COMPOSITES_ENABLED,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;cs.getSettings(),
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;false
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;);
}

public ComponentType(TypeFactory.TypeScope typeScope, ComponentMetamodel metamodel) {
	...
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;this.createEmptyCompositesEnabled = metamodel.isCreateEmptyCompositesEnabled();
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;이렇게 create_empty_composites.enabled에 대한 옵션 정보를 통해서 플래그를 지정해주고...!&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// 객체의 값 비교 로직 중 일부
// null value and empty component are considered equivalent
Object[] xvalues = getPropertyValues( x, entityMode );
Object[] yvalues = getPropertyValues( y, entityMode );&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;br&gt;잘은 모르겠지만 null value and empty component are considered equivalent 라는 주석도 있었다.&lt;br&gt;(내부적으로 null 과 빈 객체가 동일하다고 판단하기 위함인 것 같다)&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// 프로퍼티 가져올 때
public Object getPropertyValue(Object component, int i)throws HibernateException {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (component == null) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;component = new Object[propertySpan];
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;...
}

// resolve 할 때
@Override
public Object resolve(Object value, SessionImplementor session, Object owner) throws HibernateException {

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if ( value != null ) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Object result = instantiate( owner, session );
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Object[] values = (Object[]) value;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Object[] resolvedValues = new Object[values.length]; //only really need new array during semiresolve!
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;for ( int i = 0; i &amp;lt; values.length; i++ ) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;resolvedValues[i] = propertyTypes[i].resolve( values[i], session, owner );
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;setPropertyValues( result, resolvedValues, entityMode );
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return result;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// 여기서 아까 옵션이 활성화 되어 있는 경우 정상적으로 초기화하도록!
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;else if ( isCreateEmptyCompositesEnabled() ) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return instantiate( owner, session );
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;else {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return null;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;그리고 빈 객체에 대해서 초기화가 가능하도록 되어 있다. (else if 분기)&lt;br&gt;원래였으면 else 문으로 인해서 null이 반환되었을 텐데, 저 분기 덕분에 빈 객체로 초기화가 가능하게 된 것이다.&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1784&quot; data-origin-height=&quot;662&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dInm75/btsB8T3xyUx/0h8aG7Mz7AuEiKuqLd0b4K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dInm75/btsB8T3xyUx/0h8aG7Mz7AuEiKuqLd0b4K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dInm75/btsB8T3xyUx/0h8aG7Mz7AuEiKuqLd0b4K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdInm75%2FbtsB8T3xyUx%2F0h8aG7Mz7AuEiKuqLd0b4K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1784&quot; height=&quot;662&quot; data-origin-width=&quot;1784&quot; data-origin-height=&quot;662&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;아무튼, 위의 옵션을 적용해보고 돌려보면 다음과 같이 빈 객체가 잘 할당되는 것을 확인할 수 있다!&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot;&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;h4 style=&quot;text-align: left;&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; style=&quot;text-align: left;&quot;&gt;사실 주요 도메인인 만큼 신중하게 리팩터링을 진행했어야 했는데, 간단한 작업이라 큰 영향이 없을 것이라고 생각한 것이 문제였던 것 같다.&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style3&quot;&gt;1. 무조건&amp;nbsp;엔티티에&amp;nbsp;작성된&amp;nbsp;것만&amp;nbsp;보고&amp;nbsp;의존하지&amp;nbsp;말자.&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;지금까지 개인 프로젝트를 했을 때는 테이블 세팅부터 내가 진행했다 보니까 자연스럽게 JPA 엔티티가 테이블의 형상을 그대로 따라간다고 생각했던 것 같다. 하지만, 현업에서는 이미 테이블이 세팅되어 있고, 테이블의 스키마가 변함에도 불구하고 엔티티가 업데이트 되지 않는 경우가 많다. 앞으로는 테이블 스키마부터 먼저 체크를 해봐야겠다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style3&quot;&gt;2. 레거시라고&amp;nbsp;무조건&amp;nbsp;따라가지&amp;nbsp;말기&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;앞서 신중하자고 했지만, 무엇보다 레거시 코드를 그대로 믿고 따라가는 건 다른 레거시를 또 낳는 행동인 것 같다.&lt;br&gt;신중하게 영향 범위를 잘 파악해서 조금씩 리팩터링을 진행하는 게 가장 중요한 것 같다.&lt;br&gt;사실 이번 리팩터링은 규모가 커서 다시 롤백했지만...   1월에 다시 한 번 시도해볼 예정이다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style3&quot;&gt;3. JPA의&amp;nbsp;러닝커브&amp;nbsp;고민해보기&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;사실 우리 팀의 경우 JPA의 기능을 제대로 활용하지 않는다. 정말 CRUD를 편하게 해주는 도구 그 이상, 그 이하도 아니게 사용하는 느낌?&lt;br&gt;예전에는 JPA가 무조건 좋다고 생각했는데, 회사를 입사하고 든 가장 큰 생각은 모두가 생각하는 지향점과 러닝 커브가 새로운 기술을 도입하고 어떠한 것을 사용할 때 가장 중요하다는 생각이 들었다. 사용할&amp;nbsp;거면&amp;nbsp;제대로&amp;nbsp;알아보고,&amp;nbsp;팀원&amp;nbsp;모두가&amp;nbsp;해당&amp;nbsp;지식에&amp;nbsp;대해&amp;nbsp;완벽하게&amp;nbsp;싱크가&amp;nbsp;되어&amp;nbsp;있을&amp;nbsp;때&amp;nbsp;사용하는&amp;nbsp;것이&amp;nbsp;좋을&amp;nbsp;것&amp;nbsp;같다.&lt;br&gt;&amp;nbsp;&lt;br&gt;JPA의 러닝커브를 생각해보았을 때, @ManyToOne 같은 JPA 종속적인 어노테이션을 많이 사용해야 하는가? 라는 의문점도 들고... 아마 다음 리팩터링 때는 @ManyToOne 같은 어노테이션을 제거하는 방향도 한 번 고려해볼 것 같다.&lt;br&gt;이번 @Embedded 케이스도 사실 팀 내에서 가장 익숙한 사람이 나였어서, 다시 사용하지 않는 방향으로 나아가는 걸 고민 중이다. (모두가 편하게 사용하는 게 가장 중요하니까)&lt;br&gt;&lt;br&gt;오랜만에 블로그 글을 작성하다 보니 생각보다 시간도 많이 쓰였지만, 발표까지 했던 주제(...) 였던 것 만큼 개인적으로 정리해보고 싶었다. 내년부터는 다시 조금씩 블로그 글도 열심히 작성해야겠다... 파이팅!&lt;/p&gt;</description>
      <category>개발일지</category>
      <category>Embedded</category>
      <category>JPA</category>
      <author>dolmeng2</author>
      <guid isPermaLink="true">https://cl8d.tistory.com/126</guid>
      <comments>https://cl8d.tistory.com/126#entry126comment</comments>
      <pubDate>Sat, 16 Dec 2023 10:00:31 +0900</pubDate>
    </item>
    <item>
      <title>[Redis] Redis는 언제 활용할 수 있을까? 1편 - 분산락 구현하기 (2)</title>
      <link>https://cl8d.tistory.com/125</link>
      <description>&lt;h4 style=&quot;text-align: left;&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;a title=&quot;지난 포스팅&quot; href=&quot;https://cl8d.tistory.com/124&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;이번 포스팅에서는 조금 더 색다른 방법으로 레디스를 활용하여 분산락을 구현해보고자 한다.&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;style3&quot; /&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;✔ Case 4 - Lua Script 활용하기&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;Redis 2.6 버전에서 루아 스크립트 엔진이 추가되면서, 레디스 서버에서 루아 스크립트를 실행할 수 있게 되었다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;레디스 내부에서는 EVAL (혹은 EVALSHA)이라는 명령어를 통해 사용할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1697469689231&quot; class=&quot;routeros&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;EVAL script numkeys [key [key ...]] [arg [arg ...]]&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;만약 스크립트가 길어지게 된다면 스크립트 전체를 EVAL로 전송하기에는 네트워크 대역의 비용이 발생할 수 있기 때문에, 레디스에서는 SCRIPT LOAD 명령어를 통해 스크립트를 서버 측에 캐싱한 다음에, 캐싱 후 반환된 키를 활용해 EVALSHA로 스크립트를 관리할 수 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;(물론 이것도 마냥 좋은 건 아니고, 레디스 노드가 많아진다면 캐싱도 전체 노드에서 해야 하기 때문에 문제가 있을 수 있다.)&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;루아(Lua)는 일종의 프로그래밍 언어로, 절차지향적 언어이다. 사실 C, C++와 사용하는 경우가 많지만, 레디스에서 사용하게 되면 사용자가 레디스 명령어를 직접 작성할 수 있으며,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;해당 스크립트가 실행되는 동안에는 원자성을 보장&lt;/span&gt;하게 된다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이는 마냥 좋은 점은 아닌 게,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;스크립트에 대한 실행 요청이 들어왔을 때 다른 레디스 명령어들은 실행이 불가능&lt;/b&gt;하기 때문에 스크립트의 실행 시간이 반드시 고려되어야 한다. 하나의 스크립트에 여러 레디스 명령어를 사용하면서 원자성을 보장하고 싶을 때 주로 사용한다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1697469689231&quot; class=&quot;kotlin&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;override fun executeScript(key: String, value: String, duration: String, script: RedisScript&amp;lt;Boolean&amp;gt;): Boolean {
    return redisTemplate.execute(script, listOf(key), value, duration)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;redisTemplate는 스크립트를 실행할 수 있는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;execute() 메서드를 제공&lt;/span&gt;하고 있으며, 실제로 내부 구현체를 살펴보면 eval()을 사용하는 것을 볼 수 있었다. 아래의 코드를 이해할 필요는 없고, 그냥 eval을 사용하고 있구나... 정도로만 넘어가자!&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-10-14 오전 12.58.33.png&quot; data-origin-width=&quot;1716&quot; data-origin-height=&quot;686&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/beBQiI/btsytaWs3Xc/LKI05hzFgVGQ6L1GPgeCi1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/beBQiI/btsytaWs3Xc/LKI05hzFgVGQ6L1GPgeCi1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/beBQiI/btsytaWs3Xc/LKI05hzFgVGQ6L1GPgeCi1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbeBQiI%2FbtsytaWs3Xc%2FLKI05hzFgVGQ6L1GPgeCi1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1716&quot; height=&quot;686&quot; data-filename=&quot;스크린샷 2023-10-14 오전 12.58.33.png&quot; data-origin-width=&quot;1716&quot; data-origin-height=&quot;686&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;또한,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;key의 리스트&lt;/b&gt;와 내부 스크립트에서 사용할&lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;변수들을 가변 인자로 (args)&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;받을 수 있는 것을 확인할 수 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;아무튼, 이에 맞춰서 한 번 비즈니스 로직을 생성해보자.&lt;/p&gt;
&lt;pre id=&quot;code_1697469689232&quot; class=&quot;kotlin&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@PostMapping(&quot;/withdraw-lua&quot;)
fun withdrawUsingLuaScript(@RequestBody request: WithdrawRequest): ResponseEntity&amp;lt;BalanceResponse&amp;gt; {
    val result = accountService.withdrawUsingLuaScript(request)
    return ResponseEntity.ok(result)
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1697469689232&quot; class=&quot;kotlin&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Transactional
fun withdrawUsingLuaScript(request: WithdrawRequest): BalanceResponse {
    val accountId = request.accountId
    val accountKey = keyGenerator.generateAccountKey(accountId)
    // 파일로 로드해도 되지만 직관적으로 여기에 넣었다.
    val script = &quot;&quot;&quot;
        local key = KEYS[1]
        local value = ARGV[1]
        local expiration = tonumber(ARGV[2])

        local existingValue = redis.call('GET', key)
        if not existingValue then
            redis.call('SET', key, value, 'EX', expiration)
            return true
        else
            return false
        end
    &quot;&quot;&quot;.trimIndent()
    val redisScript = RedisScript&amp;lt;Boolean&amp;gt;(script)

    while (!cacheService.executeScript(accountKey, &quot;account-withdraw&quot;, &quot;5&quot;, redisScript)) {
        Thread.sleep(1000)
    }

    val account = accountRepository.getAccountById(accountId)
    account.subtractBalance(request.amount)

    // 캐시 제거
    cacheService.delete(accountKey)
    return BalanceResponse(account.id, account.balance)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;스크립트의 경우 파일로 관리해도 되지만, 그렇게 길지 않기 때문에 문자열을 사용하여 바로 로드하였다.&lt;/p&gt;
&lt;pre id=&quot;code_1697469689233&quot; class=&quot;pgsql&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;local key = KEYS[1]
local value = ARGV[1]
local expiration = tonumber(ARGV[2])

local existingValue = redis.call('GET', key)
if not existingValue then
    redis.call('SET', key, value, 'EX', expiration)
    return true
else
    return false
end&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부 스크립트를 한 번 살펴보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;KEYS를 통해 listOf(key)로 넘겨준 키의 첫 번째 값을 받아오고, ARGV를 통해 각각 인자로 넘겨준 변수를 사용&lt;/span&gt;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때, 숫자값을 사용하기 위해서 tonumber()를 활용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부 로직의 경우 GET을 통해 받아오고, 만약 값이 존재하지 않으면 SET을 통해 값을 지정하고, 아니면 false를 반환하는 간단한 로직이다. 사실상 앞서 Case 2번의 애플리케이션 레벨에서 진행했던 행위를 루아 스크립트를 통해서 원자성을 보장하도록 만들어준 것이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1697469689233&quot; class=&quot;reasonml&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Test
fun 루아스크립트를_통한_동시_출금요청() {
    // given
    val accountId = 계좌_생성()
    초기_잔액_설정(accountId, 100)
    val requestCount = 2

    // when
    // 동시에 20씩 출금을 요청함
    val executor = Executors.newFixedThreadPool(requestCount)
    val latch = CountDownLatch(requestCount)
    repeat(requestCount) {
        executor.submit {
            루아스크립트를_통한_출금_요청(accountId, 20)
            latch.countDown()
        }
    }

    latch.await()

    // then
    val balanceResponse = 잔액_조회(accountId)

    // 락으로 인해서 순차적으로 처리되기 때문에 정상적으로 60 반환
    assertThat(balanceResponse.balance)
        .isEqualTo(60)
}

 private fun 루아스크립트를_통한_출금_요청(accountId: Long, amount: Int): BalanceResponse {
    val withdrawRequest = WithdrawRequest(accountId, amount)

    return RestAssured.given()
        .log().all()
        .contentType(MediaType.APPLICATION_JSON_VALUE)
        .`when`()
        .body(withdrawRequest)
        .post(&quot;/accounts/withdraw-lua&quot;)
        .then()
        .log().all()
        .extract()
        .`as`(object : TypeRef&amp;lt;BalanceResponse&amp;gt;() {})
}&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-10-14 오전 1.12.38.png&quot; data-origin-width=&quot;1370&quot; data-origin-height=&quot;476&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Bne5Z/btsyssiNoMs/S8lCqkpTVAOPPOhbzqu6pK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Bne5Z/btsyssiNoMs/S8lCqkpTVAOPPOhbzqu6pK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Bne5Z/btsyssiNoMs/S8lCqkpTVAOPPOhbzqu6pK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBne5Z%2FbtsyssiNoMs%2FS8lCqkpTVAOPPOhbzqu6pK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1370&quot; height=&quot;476&quot; data-filename=&quot;스크린샷 2023-10-14 오전 1.12.38.png&quot; data-origin-width=&quot;1370&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;✔ Case 5 - 스핀락 대신 pub-sub 구조 활용하기 (Redisson)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, 앞선 방식들은 모두 스핀락을 사용하다 보니 락을 얻기 위해서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;레디스 서버로 계속 요청을 보내야 한다는 부담&lt;/span&gt;이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한, 앞선 예제들에서는 키에 대해 TTL도 걸어주고, 명시적으로 delete도 지정해주었기 때문에 크게 문제가 되지 않았지만, 개발자가 직접 스핀락을 구현하다 보니 만약 TTL을 빼먹거나, 키에 대한 삭제 처리를 진행하지 않으면&lt;span&gt;&amp;nbsp;&lt;/span&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;그래서, Lettuce 대신에&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;Redission에서 제공하는 분산락을 사용&lt;/span&gt;하여 이를 해결해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redisson의 경우 기본적으로 Pub-Sub 기반의 구조이기 때문에 계속 연결을 시도하지 않는다.&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;Publish-Subscribe (Pub-Sub) 구조는 Publisher와 Subscriber가&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;서로 알지 못해도 통신이 가능&lt;/span&gt;하도록 만들어진 패턴이다.&lt;br /&gt;Publisher는 Subscriber에게 직접적으로 메시지를 보내지 않고, Channel에 publish를 진행하게 된다.&lt;br /&gt;Subscriber는 관심이 있는 채널을 필요에 따라 Subscribe를 통해 메시지를 수신할 수 있다.&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-10-14 오전 1.42.22.png&quot; data-origin-width=&quot;1540&quot; data-origin-height=&quot;388&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mlupn/btsytYHZsij/oCjBl6E47h8E3x6YI4o4BK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mlupn/btsytYHZsij/oCjBl6E47h8E3x6YI4o4BK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mlupn/btsytYHZsij/oCjBl6E47h8E3x6YI4o4BK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fmlupn%2FbtsytYHZsij%2FoCjBl6E47h8E3x6YI4o4BK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1540&quot; height=&quot;388&quot; data-filename=&quot;스크린샷 2023-10-14 오전 1.42.22.png&quot; data-origin-width=&quot;1540&quot; data-origin-height=&quot;388&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 채널을 Subscribe를 하고 있는 스레드들이 Publish된 메시지를 받아야 락 점유를 시도하기 때문에 비교적 부하가 덜하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레디스에서는 다음과 같이 명령어를 제공하고 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1697469689235&quot; class=&quot;smali&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// order라는 채널에 대해서 new-order라는 메시지 발행 예제
PUBLISH order new-order

// order, delivery라는 채널에 대해서 메시지 구독 예제
SUBSCRIBE order, delivery&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;publish 시&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;채널과 발행할 메시지를 입력&lt;/b&gt;하고, subscribe 시&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;구독하고 싶은 채널 이름&lt;/b&gt;을 나열하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;publish 후 성공적으로 메시지를 발행하면 1을 반환하고, 만약 아무도 구독하고 있지 않는 채널에 발행을 하면 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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 코드로 구현하기 위해 다음과 같은 의존성을 추가해주도록 하자.&lt;/p&gt;
&lt;pre id=&quot;code_1697469689235&quot; class=&quot;stylus&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;implementation(&quot;org.redisson:redisson-spring-boot-starter:3.23.5&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고, 비즈니스 로직을 작성해보자.&lt;/p&gt;
&lt;pre id=&quot;code_1697469689235&quot; class=&quot;kotlin&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@PostMapping(&quot;withdraw-redisson&quot;)
fun withdrawUsingRedisson(@RequestBody request: WithdrawRequest): ResponseEntity&amp;lt;BalanceResponse&amp;gt; {
    val result = accountService.withdrawUsingRedisson(request)
    return ResponseEntity.ok(result)
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1697469689235&quot; class=&quot;kotlin&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Service
class AccountService(
    // RedissonClient에 대해 의존성 주입 필요
    private val redissonClient: RedissonClient,
    private val accountRepository: AccountRepository,
    private val keyGenerator: KeyGenerator
) {

    @Transactional
    fun withdrawUsingRedisson(request: WithdrawRequest): BalanceResponse {
        val accountId = request.accountId
        val accountKey = keyGenerator.generateAccountKey(accountId)

        // 락 획득
        val lock = redissonClient.getLock(accountKey)
        try {
            val canLock = lock.tryLock(3, 5, TimeUnit.SECONDS)
            if (canLock.not()) {
                throw RuntimeException(&quot;락 획득에 실패하였습니다.&quot;)
            }
            val account = accountRepository.getAccountById(accountId)
            account.subtractBalance(request.amount)
            return BalanceResponse(account.id, account.balance)
            
        } finally {
            // 락 해제
            lock.unlock()
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;redissionClient에서는 getLock()이라는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;메서드를 통해 인자로 넘겨준 이름을 가진 Lock instance를 생성&lt;/span&gt;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때, 스레드가 락을 획득하는 순서를 보장하지 않는다. (non-fair locking이라고 하는데, 조금 더 정확하게는 특정 스레드가 먼저 들어와서 오랫동안 락을 대기하고 있을 때, 새로운 스레드가 와서 요청을 보내면 옛날 스레드가 아니라 새로운 스레드가 락을 점유할 수 있다는 의미인 것 같다. 참고&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://stackoverflow.com/questions/63431161/difference-in-internal-storing-between-fair-and-unfair-lock&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;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;락을 획득할 수 있는 상황이라면 (tryLock) 점유하고, 아니라면 예외를 발생&lt;/b&gt;&lt;/span&gt;하도록 만들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;항상 락에 대한 해제 로직은 돌아야 하기 때문에 try-finally 구문을 통해 unlock을 꼭 해주도록 하자.&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;pre id=&quot;code_1697469977021&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
fun 레디슨을_통한_동시_출금요청() {
    // given
    val accountId = 계좌_생성()
    초기_잔액_설정(accountId, 100)
    val requestCount = 2

    // when
    // 동시에 20씩 출금을 요청함
    val executor = Executors.newFixedThreadPool(requestCount)
    val latch = CountDownLatch(requestCount)
    repeat(requestCount) {
        executor.submit {
            레디슨을_통한_출금_요청(accountId, 20)
            latch.countDown()
        }
    }

    latch.await()

    // then
    val balanceResponse = 잔액_조회(accountId)

    // 락으로 인해서 순차적으로 처리되기 때문에 정상적으로 60 반환
    assertThat(balanceResponse.balance)
        .isEqualTo(60)
}

private fun 레디슨을_통한_출금_요청(accountId: Long, amount: Int): BalanceResponse {
    val withdrawRequest = WithdrawRequest(accountId, amount)

    return RestAssured.given()
        .log().all()
        .contentType(MediaType.APPLICATION_JSON_VALUE)
        .`when`()
        .body(withdrawRequest)
        .post(&quot;/accounts/withdraw-redisson&quot;)
        .then()
        .log().all()
        .extract()
        .`as`(object : TypeRef&amp;lt;BalanceResponse&amp;gt;() {})
}&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-10-14 오전 2.11.31.png&quot; data-origin-width=&quot;1546&quot; data-origin-height=&quot;714&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/UKfzh/btsyumoncSC/P4sofnY3PKh03vEXbRpeaK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/UKfzh/btsyumoncSC/P4sofnY3PKh03vEXbRpeaK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/UKfzh/btsyumoncSC/P4sofnY3PKh03vEXbRpeaK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUKfzh%2FbtsyumoncSC%2FP4sofnY3PKh03vEXbRpeaK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;713&quot; height=&quot;329&quot; data-filename=&quot;스크린샷 2023-10-14 오전 2.11.31.png&quot; data-origin-width=&quot;1546&quot; data-origin-height=&quot;714&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;이대로 끝내면 아쉽기 때문에, 내부적으로 어떻게 pub-sub 구조를 통해 락을 획득하는지 확인해보자.&lt;/p&gt;
&lt;pre id=&quot;code_1697469689236&quot; class=&quot;reasonml&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저, tryLock의 경우 내부적으로 3개의 인자를 받는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;waitTime은 락을 사용할 수 있을 때까지 대기하는 시간, leaseTime은 락을 점유하는 시간, unit은 시간에 대한 단위이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 즉,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;leaseTime이 지나게 되면 자동으로 락이 해제되며, 이미 락을 점유하고 있는 스레드가 존재할 때 다른 스레드가 요청을 보내면 waitTime까지 대기&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;tryLock의 내부 로직을 살짝 살펴보면 한 가지 흥미로운 점을 발견할 수 있는데,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;바로 루아 스크립트를 활용&lt;/b&gt;한다는 점이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-10-14 오전 2.31.39.png&quot; data-origin-width=&quot;2096&quot; data-origin-height=&quot;1384&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/csL6gp/btsytNma8n0/fwZ7bXGZSctnkCtEkgW7f1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/csL6gp/btsytNma8n0/fwZ7bXGZSctnkCtEkgW7f1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/csL6gp/btsytNma8n0/fwZ7bXGZSctnkCtEkgW7f1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcsL6gp%2FbtsytNma8n0%2FfwZ7bXGZSctnkCtEkgW7f1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;779&quot; height=&quot;514&quot; data-filename=&quot;스크린샷 2023-10-14 오전 2.31.39.png&quot; data-origin-width=&quot;2096&quot; data-origin-height=&quot;1384&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-10-14 오전 2.42.10.png&quot; data-origin-width=&quot;2548&quot; data-origin-height=&quot;634&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ecBu18/btsypyqA7eq/my4CwKDEahwsO3NYhe7iSK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ecBu18/btsypyqA7eq/my4CwKDEahwsO3NYhe7iSK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ecBu18/btsypyqA7eq/my4CwKDEahwsO3NYhe7iSK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FecBu18%2FbtsypyqA7eq%2Fmy4CwKDEahwsO3NYhe7iSK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;783&quot; height=&quot;195&quot; data-filename=&quot;스크린샷 2023-10-14 오전 2.42.10.png&quot; data-origin-width=&quot;2548&quot; data-origin-height=&quot;634&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;락이 존재하지 않는다면 락에 대한 키와 스레드 아이디를 기반으로 값을 1 증가시키고, 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;그리고, tryLock 메서드의 하단으로 내려가보면 아래와 같이 락에 대해 대기하는 로직이 나온다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-10-16 오전 9.17.45.png&quot; data-origin-width=&quot;1838&quot; data-origin-height=&quot;928&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bVomLP/btsys6tD3pZ/FeCfybnHITmZ3XD66mERb1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bVomLP/btsys6tD3pZ/FeCfybnHITmZ3XD66mERb1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bVomLP/btsys6tD3pZ/FeCfybnHITmZ3XD66mERb1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbVomLP%2Fbtsys6tD3pZ%2FFeCfybnHITmZ3XD66mERb1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1838&quot; height=&quot;928&quot; data-filename=&quot;스크린샷 2023-10-16 오전 9.17.45.png&quot; data-origin-width=&quot;1838&quot; data-origin-height=&quot;928&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때, subscribe 내부 로직을 보면&lt;span style=&quot;color: #ef5369;&quot;&gt; 세마포어를 활용&lt;/span&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;subscribe를 실패하면 (예외가 발생하면) false를 반환하며 로직을 종료한다.&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;스크린샷 2023-10-17 오전 12.11.31.png&quot; data-origin-width=&quot;964&quot; data-origin-height=&quot;612&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/m9M7p/btsytJMk83G/7KlOkhau5HSqNLwJO7acD1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/m9M7p/btsytJMk83G/7KlOkhau5HSqNLwJO7acD1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/m9M7p/btsytJMk83G/7KlOkhau5HSqNLwJO7acD1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fm9M7p%2FbtsytJMk83G%2F7KlOkhau5HSqNLwJO7acD1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;478&quot; height=&quot;303&quot; data-filename=&quot;스크린샷 2023-10-17 오전 12.11.31.png&quot; data-origin-width=&quot;964&quot; data-origin-height=&quot;612&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후, while문을 돌면서 남아 있는 시간 동안 락을 획득하기 위해 재시도를 진행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 tryAcquire()에 의해 반환되는 ttl 값은 현재 스레드가 점유를 시도하고 있는 락이 다른 스레드에 의해 언제 해제될지 나타내는 값을 (= 락이 얼마나 더 유지될지) 의미하며, &lt;b&gt;null이라면 락을 획득할 수 있는 상태이기 때문에&lt;/b&gt; true를 반환하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 락 획득에 실패했다면 time (waitTime을 나타내며, 대기 시간을 의미한다)의 값을 갱신해주고, 갱신된 값이 만료되었다면 락 획득에 실패했다고 간주하여 false를 반환하게 된다.&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;스크린샷 2023-10-17 오전 12.33.47.png&quot; data-origin-width=&quot;1746&quot; data-origin-height=&quot;760&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cEHAHM/btsytRDEcUu/omkaCFDyRPAFHq8G0KF7vk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cEHAHM/btsytRDEcUu/omkaCFDyRPAFHq8G0KF7vk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cEHAHM/btsytRDEcUu/omkaCFDyRPAFHq8G0KF7vk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcEHAHM%2FbtsytRDEcUu%2FomkaCFDyRPAFHq8G0KF7vk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1746&quot; height=&quot;760&quot; data-filename=&quot;스크린샷 2023-10-17 오전 12.33.47.png&quot; data-origin-width=&quot;1746&quot; data-origin-height=&quot;760&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;만약 ttl (락에 대해 남은 유효 시간)이 0보다 크거나 같으면서 time (남은 대기 시간)보다 작으면 ttl 시간 동안 다시 락 획득을 대기하며, 그게 아니라면 남은 대기 시간 동안 기다리게 된다. (= 그냥 획득을 ttl, time만큼 대기하며 시도한다고 생각하기)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종적으로 대기 시간인 time을 갱신하고, 만료되었다면 락 획득에 실패했다고 간주하고 false를 반환하게 된다.&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: #ef5369;&quot;&gt;unsubscribe를 통해 구독을 해제&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-10-17 오전 12.41.06.png&quot; data-origin-width=&quot;1154&quot; data-origin-height=&quot;666&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cC5H1j/btsyDsoVCkV/hpzti7JoJjX5hK63Xs1wgk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cC5H1j/btsyDsoVCkV/hpzti7JoJjX5hK63Xs1wgk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cC5H1j/btsyDsoVCkV/hpzti7JoJjX5hK63Xs1wgk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcC5H1j%2FbtsyDsoVCkV%2Fhpzti7JoJjX5hK63Xs1wgk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;629&quot; height=&quot;363&quot; data-filename=&quot;스크린샷 2023-10-17 오전 12.41.06.png&quot; data-origin-width=&quot;1154&quot; data-origin-height=&quot;666&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;hr data-ke-style=&quot;style3&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  마무리&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;레디스 관련해서 계속 써야지, 써야지 했는데 어쩌다 보니 계속 미루다가 이제서야 작성하게 되었다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;10월에는 글 2개 정도는 올리도록 노력해봐야겠다...   나태해진 나 반성해...&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 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;모든 기술의 도입에는 이유가 있어야 한다고 생각하기 때문에... 레디스를 활용하지 않더라도 얼마든지 동시성 제어는 가능하다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;앞으로 도입을 하게 된다면 무작정 도입하지 않고, 이러한 점들에 대해 trade-off를 잘 고민해봐야겠다  &lt;/p&gt;</description>
      <category>개발일지</category>
      <category>LUA</category>
      <category>REDIS</category>
      <category>Redisson</category>
      <category>루아스크립트</category>
      <category>분산락</category>
      <author>dolmeng2</author>
      <guid isPermaLink="true">https://cl8d.tistory.com/125</guid>
      <comments>https://cl8d.tistory.com/125#entry125comment</comments>
      <pubDate>Tue, 17 Oct 2023 09:24:08 +0900</pubDate>
    </item>
    <item>
      <title>[Redis] Redis는 언제 활용할 수 있을까? 1편 - 분산락 구현하기 (1)</title>
      <link>https://cl8d.tistory.com/124</link>
      <description>&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  들어가기 전&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;조금 오래되긴 했지만 이전 &lt;a title=&quot;포스팅&quot; href=&quot;https://cl8d.tistory.com/112&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;포스팅&lt;/span&gt;&lt;/a&gt;에서 네임드락을 통한 분산락 구현을 진행하였다. 마지막 포스팅에서 레디스를 활용한다고 말했었는데, 계속 미루다가 이번에 레디스 공부를 하면서 간단하게 구현을 진행해보았다. 분산락에 대한 개념은 이전 포스팅에서 다루었기 때문에, 레디스를 이용해서 바로 분산락을 구현해보자. 또한, 지난 포스팅에서는 Jmeter를 활용하였지만, 이번 포스팅에서는 E2E 테스트를 통해 직접 구현을 해보고자 한다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: left;&quot; 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-origin-width=&quot;1556&quot; data-origin-height=&quot;260&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yDDCZ/btsykrjBOSN/e4qijkTIGrgJ9udm1LUmc1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yDDCZ/btsykrjBOSN/e4qijkTIGrgJ9udm1LUmc1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yDDCZ/btsykrjBOSN/e4qijkTIGrgJ9udm1LUmc1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyDDCZ%2FbtsykrjBOSN%2Fe4qijkTIGrgJ9udm1LUmc1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1556&quot; height=&quot;260&quot; data-origin-width=&quot;1556&quot; data-origin-height=&quot;260&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;정말 간단하게 잔액을 의미하는 'Balance'라는 값에, &lt;span style=&quot;color: #ef5369;&quot;&gt;2개의 요청이 동시에 들어왔을 때 레디스를 통한 분산락을 구현하여 제어&lt;/span&gt;해보자.&lt;br /&gt;위의 결과에서 최악의 상황이라면 Request 1, 2번 모두 80이라는 값을 반환하여 정상적으로 잔액이 갱신되지 않을 수 있다.&lt;br /&gt;우리는 아래와 같은 플로우를 구현할 예정이다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;1. 공유 자원을 표현할 수 있는 이름을 생성하여 락의 키를 결정하기 &lt;br /&gt;2. NX 옵션을 통해서 해당 &lt;span style=&quot;color: #ef5369;&quot;&gt;키가 존재하지 않는 경우에만 락을 획득&lt;/span&gt;하기&lt;br /&gt;3. &lt;span style=&quot;color: #ef5369;&quot;&gt;이미 락을 획득 한 경우 nil을 반환&lt;/span&gt;하여 프로세스 대기하기 &lt;br /&gt;4. 자원의 작업이 끝나면 DEL을 통해 락 제거하기&lt;/blockquote&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;여기서 NX와 DEL은 레디스에서 사용하는 명령어다.&lt;br /&gt;NX는 'IF NOT EXISTS SET'과 같이, &lt;span style=&quot;color: #ef5369;&quot;&gt;키가 존재하지 않을 경우에만 SET을 해준다.&lt;/span&gt; 싱글 스레드로 동작하는 레디스에서 키가 존재하는지 판단하기 위해서는 GET으로 확인 후 SET을 해야 한다. 하지만 멀티 스레드 환경인 스프링 같은 곳에서 사용할 때 정합성 문제가 발생할 수 있기 때문에, &lt;b&gt;한 번에 중복을 체크하고 삽입까지 진행할 수 있도록 만들어진 명령어라고&lt;/b&gt; 생각하면 이해하기 쉽다.&lt;br /&gt;DEL의 경우 쉽게 예상할 수 있듯, 'DELETE'를 의미하는 키 삭제 기능을 의미한다. &lt;br /&gt;&lt;br /&gt;우리는 &lt;b&gt;스핀 락 (Spin Lock) 방식을 통해&lt;/b&gt; 락을 점유하고 있는지 계속 확인하면서, 락을 잡을 수 있다면 락을 잡고 아니면 대기하는 방식으로 구현하고자 한다. 락에 대한 키를 계속 점유하지 않도록 키는 임계 영역을 빠져나온 후 제거를 해줘야 하며, 키 설정 시 TTL도 함께 걸어주는 것이 좋다.&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;  &lt;b&gt;구현하기 - 환경 구축&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;하단에 나온 설정들은 앞으로 redis 관련 포스팅을 할 때 계속 사용할 예정이기 때문에, 이번 포스팅에서만 환경 구축에 대해 작성하고자 한다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;implementation(&quot;org.springframework.boot:spring-boot-starter-data-redis&quot;)
implementation(&quot;org.springframework.boot:spring-boot-starter-data-jpa&quot;)
runtimeOnly(&quot;com.h2database:h2&quot;)
testImplementation(&quot;io.rest-assured:rest-assured&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;먼저, 기본 구현을 위해 필요한 dependency들이다. (언어는 Kotlin으로 작성할 예정이다.)&lt;br /&gt;스프링에서 제공하는 redis 및 jpa, 테스트 환경 구축을 위한 h2와 RestAssured를 추가해주자.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Configuration
class RedisConfig {

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@Bean
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;fun redisConnectionFactory(): RedisConnectionFactory =
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;LettuceConnectionFactory()

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@Bean
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;fun redisTemplate(): RedisTemplate&amp;lt;String, String&amp;gt; {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return RedisTemplate&amp;lt;String, String&amp;gt;().apply {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;connectionFactory = redisConnectionFactory()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;keySerializer = StringRedisSerializer()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;valueSerializer = StringRedisSerializer()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;이번엔 레디스 관련 설정 클래스를 만들어주자.&lt;br /&gt;레디스 클라이언트 중 자바 기반 환경에서 대표적으로 뽑히는 Lettuce를 사용하였으며, 레디스에 저장될 key, value는 현재 스펙에서 모두 String을 사용하면 되기 때문에 &lt;b&gt;StringRedisSerializer()&lt;/b&gt;를 사용해주었다. 취향에 맞게 커스텀을 해주면 될 것 같다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1697191798417&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class RedisService(
    private val redisTemplate: RedisTemplate&amp;lt;String, String&amp;gt;
) : CacheService {
    override fun delete(key: String) {
        redisTemplate.delete(key)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;또한, 캐시의 키를 제거해주는 함수를 미리 작성해두었다. (자주 사용될 예정이라 제일 상단으로 빼두었다.)&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;style3&quot; /&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt; &lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;구현하기 - 서비스 로직&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저, 계좌 정보를 저장하기 위해 간단한 도메인 객체를 만들어보자.&lt;/p&gt;
&lt;pre id=&quot;code_1697191895116&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
class Account(

    @Id
    val id: Long = 0L
) {
    var balance: Int = 0

    fun addBalance(amount: Int) {
        balance += amount
    }

    fun subtractBalance(amount: Int) {
        balance -= amount
    }
}&lt;/code&gt;&lt;/pre&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;pre id=&quot;code_1697192168451&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import org.springframework.data.jpa.repository.JpaRepository

fun AccountRepository.getAccountById(id: Long): Account {
    return findAccountById(id) ?: throw IllegalArgumentException(&quot;존재하지 않는 계좌입니다&quot;)
}

interface AccountRepository : JpaRepository&amp;lt;Account, Long&amp;gt; {
    fun findAccountById(id: Long): Account?
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 간단하게 아이디를 통해 계좌를 조회해주는 확장 함수를 만들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개인적으로 확장 함수를 활용해서 위와 같이 예외를 터트리는 방식을 선호하는데, 어떤 아키텍처를 사용하는지에 따라서 예외를 발생시키는 부분도 달라질 것 같다. (요즘은 생각이 바뀌어서, 아마 다르게 구현하지 않을까 싶다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1697192422121&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;interface KeyGenerator {
    fun generateAccountKey(accountId: Long): String
}

@Component
class KeyGeneratorImpl : KeyGenerator {
    override fun generateAccountKey(accountId: Long): String {
        return &quot;$accountId:account&quot;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로,&lt;span style=&quot;color: #ef5369;&quot;&gt; 레디스의 키를 생성해주는 메서드를 추가&lt;/span&gt;해주었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;키는 겹치지 않는 것이 제일 중요한데, 여기서는 계좌에 대한 고유 아이디 (PK) 값과 'account'를 알려주는 String을 추가해서 만들어주었다. 실제로는 어떤 기능을 개발할지에 따라서 키를 만드는 방법도 다양하기 때문에 유의해야 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1697192888566&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RestController
@RequestMapping(&quot;/accounts&quot;)
class AccountController(
    private val accountService: AccountService
) {

    @PostMapping(&quot;/create&quot;)
    fun createAccount(): ResponseEntity&amp;lt;AccountResponse&amp;gt; {
        val result = accountService.create()
        return ResponseEntity.ok(result)
    }

    @PostMapping(&quot;/deposit&quot;)
    fun deposit(@RequestBody request: DepositRequest): ResponseEntity&amp;lt;BalanceResponse&amp;gt; {
        val result = accountService.deposit(request)
        return ResponseEntity.ok(result)
    }

    @GetMapping(&quot;/balance/{accountId}&quot;)
    fun getBalance(@PathVariable accountId: Long): ResponseEntity&amp;lt;BalanceResponse&amp;gt; {
        val result = accountService.getBalance(accountId)
        return ResponseEntity.ok(result)
    }

    @PostMapping(&quot;/withdraw&quot;)
    fun withdraw(@RequestBody request: WithdrawRequest): ResponseEntity&amp;lt;BalanceResponse&amp;gt; {
        val result = accountService.withdraw(request)
        return ResponseEntity.ok(result)
    }
}

data class DepositRequest(
    val accountId: Long,
    val amount: Int
)

data class WithdrawRequest(
    val accountId: Long,
    val amount: Int
)

data class AccountResponse(
    val accountId: Long
)

data class BalanceResponse(
    val accountId: Long,
    val balance: Int
)&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1697193038346&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class AccountService(
    private val accountRepository: AccountRepository,
    private val keyGenerator: KeyGenerator,
    private val cacheService: CacheService
) {

    @Transactional
    fun create(): AccountResponse {
        val account = Account()
        val savedAccount = accountRepository.save(account)
        return AccountResponse(savedAccount.id)
    }

    @Transactional
    fun deposit(request: DepositRequest): BalanceResponse {
        val accountId = request.accountId
        val account = accountRepository.getAccountById(accountId)
        account.addBalance(request.amount)
        return BalanceResponse(account.id, account.balance)
    }

    @Transactional(readOnly = true)
    fun getBalance(accountId: Long): BalanceResponse {
        val account = accountRepository.getAccountById(accountId)
        return BalanceResponse(account.id, account.balance)
    }

    @Transactional
    fun withdraw(request: WithdrawRequest): BalanceResponse {
        val accountId = request.accountId
        val account = accountRepository.getAccountById(accountId)
        account.subtractBalance(request.amount)
        return BalanceResponse(account.id, account.balance)
    }
}&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;계좌를 생성하고, 입금하고, 출금하고, 조회하는 기능이다.&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&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;✔ Case 1 - 아무 락도 걸지 않았을 때&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1697193310326&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class AccountControllerTest : IntegrationTestHelper() {

    @Test
    fun 락_없이_동시_출금요청() {
        // given
        val accountId = 계좌_생성()
        초기_잔액_설정(accountId, 100)
        val requestCount = 2

        // when
        // 동시에 20씩 출금을 요청함
        val executor = Executors.newFixedThreadPool(requestCount)
        val latch = CountDownLatch(requestCount)
        repeat(requestCount) {
            executor.submit {
                출금_요청(accountId, 20)
                latch.countDown()
            }
        }

        latch.await()

        // then
        val balanceResponse = 잔액_조회(accountId)

        // 원하는 결과는 60이겠지만, 다른 값이 나옴
        assertThat(balanceResponse.balance)
            .isNotEqualTo(60)
    }

    private fun 계좌_생성(): Long {
        return RestAssured.given()
            .log().all()
            .contentType(MediaType.APPLICATION_JSON_VALUE)
            .`when`()
            .post(&quot;/accounts/create&quot;)
            .then()
            .log().all()
            .extract()
            .`as`(object : TypeRef&amp;lt;AccountResponse&amp;gt;() {})
            .accountId
    }

    private fun 초기_잔액_설정(accountId: Long, amount: Int): BalanceResponse {
        val depositRequest = DepositRequest(accountId, amount)
        return RestAssured.given()
            .log().all()
            .contentType(MediaType.APPLICATION_JSON_VALUE)
            .`when`()
            .body(depositRequest)
            .post(&quot;/accounts/deposit&quot;)
            .then()
            .log().all()
            .extract()
            .`as`(object : TypeRef&amp;lt;BalanceResponse&amp;gt;() {})
    }

    private fun 출금_요청(accountId: Long, amount: Int): BalanceResponse {
        val withdrawRequest = WithdrawRequest(accountId, amount)

        return RestAssured.given()
            .log().all()
            .contentType(MediaType.APPLICATION_JSON_VALUE)
            .`when`()
            .body(withdrawRequest)
            .post(&quot;/accounts/withdraw&quot;)
            .then()
            .log().all()
            .extract()
            .`as`(object : TypeRef&amp;lt;BalanceResponse&amp;gt;() {})
    }

    private fun 잔액_조회(accountId: Long): BalanceResponse {
        return RestAssured.given()
            .log().all()
            .`when`()
            .get(&quot;/accounts/balance/$accountId&quot;)
            .then()
            .log().all()
            .extract()
            .`as`(object : TypeRef&amp;lt;BalanceResponse&amp;gt;() {})
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;우선, 아무 락도 걸지 않은 상태로 요청을 한다면 어떻게 될까? 위 테스트 시나리오는 다음과 같다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;1. 초기 잔액을 100으로 세팅한다.&lt;br /&gt;2. 2개의 요청이 동시에 20씩 출금 요청을 진행한다.&lt;br /&gt;3. 잔액을 조회했을 때 100 - 20 - 20 = 60이 나오기를 기대한다.&lt;/blockquote&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;2개의 요청이 동시에 오고 있으며, 테스트 코드가 먼저 끝나는 것을 방지하기 위해 모든 요청이 종료될 때까지 await()으로 대기할 수 있도록 만들었다. 하지만, 테스트 코드를 보면 &lt;b&gt;isNotEqualTo(60)&lt;/b&gt;으로, 60이라는 값이 나오지 않은 것을 볼 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-10-13 오후 11.58.16.png&quot; data-origin-width=&quot;1430&quot; data-origin-height=&quot;440&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c85LLl/btsytifBS1F/f5fDX1yJfo3nk00iaF5dy0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c85LLl/btsytifBS1F/f5fDX1yJfo3nk00iaF5dy0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c85LLl/btsytifBS1F/f5fDX1yJfo3nk00iaF5dy0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc85LLl%2FbtsytifBS1F%2Ff5fDX1yJfo3nk00iaF5dy0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1430&quot; height=&quot;440&quot; data-filename=&quot;스크린샷 2023-10-13 오후 11.58.16.png&quot; data-origin-width=&quot;1430&quot; data-origin-height=&quot;440&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이는, 동시성 문제로 인해서 '20 출금하라'라는 요청이 동시에 오면서 첫 번째 요청, 두 번째 요청 모두 100 -&amp;gt; 80으로 출금을 진행했기 때문에 발생한 문제였다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;style1&quot; /&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;✔ Case 2 - 레디스의 get - set 활용하기&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;그렇다면, 레디스를 활용해 보면 어떨까?&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;우리는 초기에 구현 시나리오에서 &lt;span style=&quot;color: #ef5369;&quot;&gt;'키가 존재하지 않을 경우 락을 획득하고, 존재하면 락을 획득할 때까지 대기&lt;/span&gt;'하도록 플로우를 구성하고자 했다. 간단하게 레디스를 활용해서 키가 존재하는지 확인하기 위해 get으로 가져오고, 존재하지 않는 경우에 set을 통해 락을 획득하는 로직을 작성해보자.&lt;/p&gt;
&lt;pre id=&quot;code_1697207297540&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;interface KeyGenerator {
    fun generateAccountKey(accountId: Long): String
}

@Component
class KeyGeneratorImpl : KeyGenerator {
    override fun generateAccountKey(accountId: Long): String {
        return &quot;$accountId:account&quot;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;먼저, 레디스의 키를 생성해주는 메서드를 추가해주었다.&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;br /&gt;키는 겹치지 않는 것이 제일 중요한데, 여기서는 계좌에 대한 고유 아이디 (PK) 값과 'account'를 알려주는 String을 추가해서 만들어주었다. 실제로는&amp;nbsp;어떤&amp;nbsp;기능을&amp;nbsp;개발할지에&amp;nbsp;따라서&amp;nbsp;키를&amp;nbsp;만드는&amp;nbsp;방법도&amp;nbsp;다양하기&amp;nbsp;때문에&amp;nbsp;유의해야&amp;nbsp;된다.&lt;/p&gt;
&lt;pre id=&quot;code_1697207367779&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
class RedisService(
    private val redisTemplate: RedisTemplate&amp;lt;String, String&amp;gt;
) : CacheService {
    override fun get(key: String): String? {
        return redisTemplate.opsForValue().get(key)
    }

    override fun set(key: String, value: String, duration: Duration) {
        redisTemplate.opsForValue().set(key, value, duration)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;다음으로,&lt;span style=&quot;color: #ef5369;&quot;&gt; redisTemplate을 활용&lt;/span&gt;하여 String 값을 조회하고 설정하는 함수를 추가하였다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;둘 모두 &lt;b&gt;opsForValues().set()과 opsForValues().get()을 활용&lt;/b&gt;하면 간단하게 구현할 수 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;set의 경우 해당 키가 언제까지 유효하도록 만들지 TTL 값을 설정해줄 수 있는데, Duration을 사용하여 원하는 만큼 지정해줄 수 있다. 보통 오래 걸리는 로직이 아니라면 30초 ~ 1분 정도로 설정하는 경우를 많이 본 것 같다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1697216851413&quot; class=&quot;kotlin&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@PostMapping(&quot;/withdraw-non-atomic-lock&quot;)
fun withdrawUsingNonAtomicLock(@RequestBody request: WithdrawRequest): ResponseEntity&amp;lt;BalanceResponse&amp;gt; {
    val result = accountService.withdrawUsingNonAtomicLock(request)
    return ResponseEntity.ok(result)
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1697207506814&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Transactional
fun withdrawUsingNonAtomicLock(request: WithdrawRequest): BalanceResponse? {
    val accountId = request.accountId
    val accountKey = keyGenerator.generateAccountKey(accountId)

    while (true) {
        if (cacheService.get(accountKey) == null) {
            cacheService.set(accountKey, &quot;account-withdraw&quot;, Duration.ofSeconds(5))
            break;
        }
        Thread.sleep(1000)
    }

    val account = accountRepository.getAccountById(accountId)
    account.subtractBalance(request.amount)

    // 캐시 제거 - TTL을 짧게 잡기는 했지만 명시적으로 제거함
    cacheService.delete(accountKey)
    return BalanceResponse(account.id, account.balance)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;그리고, 실질적인 비즈니스 코드를 작성해주었다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;먼저, get()을 통해서 &lt;b&gt;키가 존재하는지 확인한 다음, 존재하지 않는다면 set을 해주고, 존재하는 경우 1초씩 대기&lt;/b&gt;하며 재시도를 진행한다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;참고로, set을 통해 세팅해주는 value 값은 자유롭게 설정해주어도 된다. (의미없는 String 값도 상관없다.)&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;중요한 건, 다른 비즈니스 로직과 키만 겹치지 않도록 만들어 주는 것이 중요하다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;추가적으로, Facade 패턴 등을 도입해서 락을 얻고 반환하는 로직은 추상화할 수 있다.&lt;br /&gt;이번에는 레디스를 중심으로 적용하다 보니 이에 대해 따로 고려하지 않았지만, 지난 포스팅에서 퍼사드 패턴을 활용했기 때문에 해당 내용을 참고하여 리팩터링 해도 좋을 것 같다는 생각이 든다.&lt;/blockquote&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;우리가 원하는 로직대로 작성했기 때문에, 이제 결과값으로 60이 나오지 않을까?&lt;/p&gt;
&lt;pre id=&quot;code_1697208078941&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
fun 단순_락을_이용한_동시_출금요청_원자성을보장하지않음() {
    // given
    val accountId = 계좌_생성()
    초기_잔액_설정(accountId, 100)
    val requestCount = 2

    // when
    // 동시에 20씩 출금을 요청함
    // 캐시를 사용하긴 했지만 완전히 원자성을 보장하지 않는 로직
    val executor = Executors.newFixedThreadPool(requestCount)
    val latch = CountDownLatch(requestCount)
    repeat(requestCount) {
        executor.submit {
            락을_통한_출금_요청(accountId, 20)
            latch.countDown()
        }
    }

    latch.await()

    // then
    val balanceResponse = 잔액_조회(accountId)

    // 마찬가지로 원하는 결과는 60이겠지만, 다른 값이 나옴
    assertThat(balanceResponse.balance)
        .isNotEqualTo(60)
}

private fun 락을_통한_출금_요청(accountId: Long, amount: Int): BalanceResponse {
    val withdrawRequest = WithdrawRequest(accountId, amount)

    return RestAssured.given()
        .log().all()
        .contentType(MediaType.APPLICATION_JSON_VALUE)
        .`when`()
        .body(withdrawRequest)
        .post(&quot;/accounts/withdraw-non-atomic-lock&quot;)
        .then()
        .log().all()
        .extract()
        .`as`(object : TypeRef&amp;lt;BalanceResponse&amp;gt;() {})
}&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-10-14 오전 12.00.38.png&quot; data-origin-width=&quot;1422&quot; data-origin-height=&quot;446&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cAvc61/btsyt0sfFxn/RtS1Vr0aPWTpffkULkyDlK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cAvc61/btsyt0sfFxn/RtS1Vr0aPWTpffkULkyDlK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cAvc61/btsyt0sfFxn/RtS1Vr0aPWTpffkULkyDlK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcAvc61%2Fbtsyt0sfFxn%2FRtS1Vr0aPWTpffkULkyDlK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1422&quot; height=&quot;446&quot; data-filename=&quot;스크린샷 2023-10-14 오전 12.00.38.png&quot; data-origin-width=&quot;1422&quot; data-origin-height=&quot;446&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;하지만, 예상과 다르게 여전히 isNotEqualTo()를 통해 60이 나오지 않은 것을 볼 수 있다. 왜 이럴까?&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;사실, 앞에서 이미 답을 냈다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;우리의 스프링 애플리케이션은 멀티 스레드 기반이기 때문에, 레디스가 싱글 스레드로 동작한다고 해도 &lt;span style=&quot;color: #ef5369;&quot;&gt;하나의 스레드가 get - set을 하는 중간에 다른 스레드가 get을 충분히 할 수 있는 상황&lt;/span&gt;이 만들어졌기 때문이다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;스크린샷 2023-10-13 오후 11.48.39.png&quot; data-origin-width=&quot;1156&quot; data-origin-height=&quot;526&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bFsK13/btsyujLRWyn/PNFkFxE6XoQegFdIquVjm1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bFsK13/btsyujLRWyn/PNFkFxE6XoQegFdIquVjm1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bFsK13/btsyujLRWyn/PNFkFxE6XoQegFdIquVjm1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbFsK13%2FbtsyujLRWyn%2FPNFkFxE6XoQegFdIquVjm1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;618&quot; height=&quot;281&quot; data-filename=&quot;스크린샷 2023-10-13 오후 11.48.39.png&quot; data-origin-width=&quot;1156&quot; data-origin-height=&quot;526&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;그림으로 보면 다음과 같은 상황이다. 레디스 내부에서 싱글 스레드로 열심히 명령어를 처리하더라도, 애플리케이션 레벨에서 들어오는 요청들은 여러 스레드가 동시에 들어오기 때문에 20, 20씩 출금하라는 요청이 적용되지 않았던 것이다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;그렇다면, GET - SET을 동시에 하도록 = 원자성을 보장하도록 만들 수 없을까?&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;바로, &lt;span style=&quot;color: #ef5369;&quot;&gt;앞서 말했던 것처럼 NX을 사용&lt;/span&gt;하면 된다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;style1&quot; /&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;✔ Case 3 - 레디스의 NX 활용하기&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1697469705731&quot; class=&quot;kotlin&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;override fun setNX(key: String, value: String, duration: Duration): Boolean {
    return redisTemplate.opsForValue().setIfAbsent(key, value, duration) ?: false
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;NX의 경우 redisTemplate에서 제공하는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;setIfAbsent()&lt;/span&gt;를 사용하면 된다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;우리가 애플리케이션 레벨에서 GET을 통해 키가 존재하는지 확인하고, SET을 통해서 값을 세팅해주는 과정을 레디스 내에서 만든 명령어라고 생각하면 된다. 만약&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;키가 이미 존재해서 값을 세팅하지 못했다면 false&lt;/b&gt;를,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;키가 존재하지 않아서 값을 세팅했다면 true를 반환&lt;/b&gt;한다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1697469705732&quot; class=&quot;kotlin&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@PostMapping(&quot;/withdraw-atomic-lock&quot;)
fun withdrawUsingAtomicLock(@RequestBody request: WithdrawRequest): ResponseEntity&amp;lt;BalanceResponse&amp;gt; {
    val result = accountService.withdrawUsingAtomicLock(request)
    return ResponseEntity.ok(result)
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1697469705733&quot; class=&quot;reasonml&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Transactional
fun withdrawUsingAtomicLock(request: WithdrawRequest): BalanceResponse {
    val accountId = request.accountId
    val accountKey = keyGenerator.generateAccountKey(accountId)

    while (!cacheService.setNX(accountKey, &quot;account-withdraw&quot;, Duration.ofSeconds(5))) {
        Thread.sleep(1000)
    }

    val account = accountRepository.getAccountById(accountId)
    account.subtractBalance(request.amount)

    // 캐시 제거
    cacheService.delete(accountKey)
    return BalanceResponse(account.id, account.balance)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;실제 비즈니스 로직에 적용해보자. setNX를 통해 true, false 값을 반환하기 때문에 값을 세팅하지 못했을 경우 1초씩 대기하도록 while문을 통해 쉽게 제어할 수 있게 되었다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;그렇다면, 테스트 코드를 통해 결과를 확인해보자.&lt;/p&gt;
&lt;pre id=&quot;code_1697469705733&quot; class=&quot;reasonml&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
fun 분산락을_통한_동시_출금요청() {
    // given
    val accountId = 계좌_생성()
    초기_잔액_설정(accountId, 100)
    val requestCount = 2

    // when
    // 동시에 20씩 출금을 요청함
    val executor = Executors.newFixedThreadPool(requestCount)
    val latch = CountDownLatch(requestCount)
    repeat(requestCount) {
        executor.submit {
            분산락을_통한_출금_요청(accountId, 20)
            latch.countDown()
        }
    }

    latch.await()

    // then
    val balanceResponse = 잔액_조회(accountId)

    // 락으로 인해서 순차적으로 처리되기 때문에 정상적으로 60 반환
    assertThat(balanceResponse.balance)
        .isEqualTo(60)
}

private fun 분산락을_통한_출금_요청(accountId: Long, amount: Int): BalanceResponse {
    val withdrawRequest = WithdrawRequest(accountId, amount)

    return RestAssured.given()
        .log().all()
        .contentType(MediaType.APPLICATION_JSON_VALUE)
        .`when`()
        .body(withdrawRequest)
        .post(&quot;/accounts/withdraw-atomic-lock&quot;)
        .then()
        .log().all()
        .extract()
        .`as`(object : TypeRef&amp;lt;BalanceResponse&amp;gt;() {})
}&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-10-14 오전 12.22.15.png&quot; data-origin-width=&quot;1438&quot; data-origin-height=&quot;594&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oPdw2/btsysIe4yjg/ZUjaCGOzXAq5QRMDBxaIf1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oPdw2/btsysIe4yjg/ZUjaCGOzXAq5QRMDBxaIf1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oPdw2/btsysIe4yjg/ZUjaCGOzXAq5QRMDBxaIf1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoPdw2%2FbtsysIe4yjg%2FZUjaCGOzXAq5QRMDBxaIf1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1438&quot; height=&quot;594&quot; data-filename=&quot;스크린샷 2023-10-14 오전 12.22.15.png&quot; data-origin-width=&quot;1438&quot; data-origin-height=&quot;594&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;드디어 우리가 원하는대로 60을 반환하는 것을 확인할 수 있다!&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 이대로 끝내기에는 뭔가 아쉽기 때문에, 조금 더 귀찮은 방법으로 원자성을 보장하는 방법을 소개해보고자 한다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;style3&quot; /&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;글이 생각보다 너무 길어져서 1, 2부로 나누어서 작성하였습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;다음 포스팅: &lt;a href=&quot;https://cl8d.tistory.com/125&quot;&gt;https://cl8d.tistory.com/125&lt;/a&gt;&lt;/p&gt;</description>
      <category>개발일지</category>
      <category>REDIS</category>
      <category>Spin Lock</category>
      <category>동시성문제</category>
      <category>분산락</category>
      <author>dolmeng2</author>
      <guid isPermaLink="true">https://cl8d.tistory.com/124</guid>
      <comments>https://cl8d.tistory.com/124#entry124comment</comments>
      <pubDate>Tue, 17 Oct 2023 09:23:18 +0900</pubDate>
    </item>
    <item>
      <title>[JPA] JPA Auditing과 em.merge()의 동작 원리 살펴보기 - 테스트에서 id 값을 주의하자</title>
      <link>https://cl8d.tistory.com/123</link>
      <description>&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&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;pre id=&quot;code_1694266628166&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
@EntityListeners(AuditingEntityListener.class)
public class User {

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

    @Column(nullable = false)
    @CreatedDate
    private LocalDateTime createdAt;

    @LastModifiedDate
    private LocalDateTime updatedAt;&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;User 클래스는 사용자에 대한 엔티티이며, id&amp;nbsp;전략은&amp;nbsp;IDENTITY이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때, createdAt, updatedAt에 대해서 JPA auditing 기능을 사용한 것을 볼 수 있다.&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;pre id=&quot;code_1694266676457&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class UserTest {
    public static final User JAVAJIGI = new User(1L, &quot;javajigi&quot;, &quot;password&quot;, &quot;name&quot;, &quot;javajigi@slipp.net&quot;);
    public static final User SANJIGI = new User(&quot;sanjigi&quot;, &quot;password&quot;, &quot;name&quot;, &quot;sanjigi@slipp.net&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요 클래스는 테스트에서 사용할 간단한 픽스처이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 JAVAJIGI에 대해서는 &lt;span style=&quot;color: #ef5369;&quot;&gt;id 값이 초기화&lt;/span&gt;가 되어 있고, SANJIGI 변수에 대해서는 id 값이 초기화 되어 있지 않은 상태임을 유의하자.&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;style2&quot; /&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;pre id=&quot;code_1694266762283&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
@DisplayName(&quot;유저를 저장한다.&quot;)
void save1() {
	final User actualUser = userRepository.save(UserTest.JAVAJIGI);

	assertThat(actualUser).usingRecursiveComparison()
		.ignoringFields(&quot;id&quot;)
		.isEqualTo(UserTest.JAVAJIGI);
}

@Test
@DisplayName(&quot;유저를 저장한다.&quot;)
void save2() {
	final User actualUser = userRepository.save(UserTest.SANJIGI);

	assertThat(actualUser).usingRecursiveComparison()
		.ignoringFields(&quot;id&quot;)
		.isEqualTo(UserTest.SANJIGI);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자를 저장하는 2개의 테스트를 작성하였다.&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-origin-width=&quot;1280&quot; data-origin-height=&quot;512&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cyPpy7/btstvRuBvKN/244Q07Vqgkh90eFmSwwri0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cyPpy7/btstvRuBvKN/244Q07Vqgkh90eFmSwwri0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cyPpy7/btstvRuBvKN/244Q07Vqgkh90eFmSwwri0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcyPpy7%2FbtstvRuBvKN%2F244Q07Vqgkh90eFmSwwri0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;512&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;512&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오류 상황을 보면, User 엔티티의 &lt;b&gt;createdAt, updatedAt의 값이 채워져 있지 않아 발생&lt;/b&gt;한 것을 볼 수 있다.&lt;br /&gt;save()를&amp;nbsp;진행하게&amp;nbsp;되면&amp;nbsp;영속성&amp;nbsp;컨텍스트의&amp;nbsp;관리에&amp;nbsp;의해서&amp;nbsp;UserTest의&amp;nbsp;JAVAJIGI&amp;nbsp;변수와&amp;nbsp;actualUser가&amp;nbsp;같은&amp;nbsp;객체를&amp;nbsp;바라보고&amp;nbsp;있어서&amp;nbsp;auditing에&amp;nbsp;의해&amp;nbsp;&lt;b&gt;User&amp;nbsp;엔티티의&amp;nbsp;값이&amp;nbsp;업데이트가&amp;nbsp;되면&amp;nbsp;JAVAJIGI&amp;nbsp;변수의&amp;nbsp;값도&amp;nbsp;업데이트가&amp;nbsp;될&amp;nbsp;줄&amp;nbsp;알았는데&lt;/b&gt;&amp;nbsp;그렇지&amp;nbsp;않았던&amp;nbsp;것이다.&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;style2&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  JPA Auditing의 동작 원리 살펴보기&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 Auditing에 대해 문제라고 생각이 들어서, 디버깅을 통해 언제 값이 채워지는지 살펴보기로 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저, Auditing 시 동작하는 AuditingEntityListener 클래스의 &lt;b&gt;touchForCreate()&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;1280&quot; data-origin-height=&quot;723&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/KXj87/btsts230OLL/HmiFwrBtwCFB2tb9KQZJS0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/KXj87/btsts230OLL/HmiFwrBtwCFB2tb9KQZJS0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/KXj87/btsts230OLL/HmiFwrBtwCFB2tb9KQZJS0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKXj87%2Fbtsts230OLL%2FHmiFwrBtwCFB2tb9KQZJS0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;723&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;723&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 메서드에서 인자로 target 값에 JAVAJIGI가 들어온 것을 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 auditing이 동작하지 않았기 때문에 createdAt, updatedAt 모두 null 값으로 채워진 것을 볼 수 있다.&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.markCreated() 메서드로 타고 들어가보자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;558&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c6j7AT/btstwG0DW5F/hm1oAqZ7LbvbwduolCFxmK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c6j7AT/btstwG0DW5F/hm1oAqZ7LbvbwduolCFxmK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c6j7AT/btstwG0DW5F/hm1oAqZ7LbvbwduolCFxmK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc6j7AT%2FbtstwG0DW5F%2Fhm1oAqZ7LbvbwduolCFxmK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;558&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;558&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;183&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/85vai/btstzwP1xqO/OELtpfsjHKGUoGS8PAmdkK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/85vai/btstzwP1xqO/OELtpfsjHKGUoGS8PAmdkK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/85vai/btstzwP1xqO/OELtpfsjHKGUoGS8PAmdkK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F85vai%2FbtstzwP1xqO%2FOELtpfsjHKGUoGS8PAmdkK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;183&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;183&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째 라인에서 target의 값으로 JAVAJIGI가 들어와서 어떠한 Wrapper 객체를 생성한 것을 볼 수 있다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;286&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FXTzE/btstyym0UAW/AzZT9Jcs2oeuNvq8jh1Rd1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FXTzE/btstyym0UAW/AzZT9Jcs2oeuNvq8jh1Rd1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FXTzE/btstyym0UAW/AzZT9Jcs2oeuNvq8jh1Rd1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFXTzE%2Fbtstyym0UAW%2FAzZT9Jcs2oeuNvq8jh1Rd1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;286&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;286&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고, 날짜에 대한 auditing을 위해 touchDate() 메서드로 제어를 이동해서 살펴보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 보면 touchAuditor를 통해 auditor에 대한 정보가 필요하면 채워넣게 되며 (@CreatedBy 같은 어노테이션을 사용할 때 동작하는 듯하다.) 우리 코드에서는 필요가 없기 때문에 바로 &lt;b&gt;touchDate()&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-origin-width=&quot;1280&quot; data-origin-height=&quot;407&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zu92r/btstr4HMoGq/KVFLsHldoncGx3UQI3gsfK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zu92r/btstr4HMoGq/KVFLsHldoncGx3UQI3gsfK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zu92r/btstr4HMoGq/KVFLsHldoncGx3UQI3gsfK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fzu92r%2Fbtstr4HMoGq%2FKVFLsHldoncGx3UQI3gsfK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;407&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;407&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째 라인에서 현재 시간을 가져온 다음, &lt;span style=&quot;color: #ef5369;&quot;&gt;wrapper::setCreatedDate, wrapper::setLastModifiedDate&lt;/span&gt;를 통해서 JAVAJIGI의 값을 채워넣는 것을 볼 수 있다!&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;676&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bdGXtL/btstrN0xp1P/4Myg4aBHP7pdBqbBuKkVFk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bdGXtL/btstrN0xp1P/4Myg4aBHP7pdBqbBuKkVFk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bdGXtL/btstrN0xp1P/4Myg4aBHP7pdBqbBuKkVFk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbdGXtL%2FbtstrN0xp1P%2F4Myg4aBHP7pdBqbBuKkVFk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;676&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;676&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로, target인 JAVAJIGI에 시간 관련 필드가 채워져 있는 것을 확인할 수 있었다.&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;style3&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;br /&gt;&lt;b&gt;  JPA의 save는 어떻게 동작할까?&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 정말 이상하다. auditing까지 정상적으로 동작했음에도 불구하고 왜 createdAt, updatedAt이 null이었던 것일까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 알아보기 위해서 JPA의 save() 메서드를 한 번 눈여겨 볼 필요가 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1266&quot; data-origin-height=&quot;704&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0DA15/btstxoSJKJO/1RlcL4DKWIuFRgNEB7QS11/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0DA15/btstxoSJKJO/1RlcL4DKWIuFRgNEB7QS11/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0DA15/btstxoSJKJO/1RlcL4DKWIuFRgNEB7QS11/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0DA15%2FbtstxoSJKJO%2F1RlcL4DKWIuFRgNEB7QS11%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;660&quot; height=&quot;367&quot; data-origin-width=&quot;1266&quot; data-origin-height=&quot;704&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 userRepository.save() 메서드가 호출되면, isNew()라는 어떠한 메서드를 통해 새로운 객체인지 판단한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 새로운 객체라면 persist 후 &lt;span style=&quot;color: #ef5369;&quot;&gt;기존의 엔티티를 반환&lt;/span&gt;하고, &lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;그렇지 않으면 merge 후&lt;/span&gt;&amp;nbsp;&quot;새로운 객체를 반환&quot;&lt;/span&gt; 한다는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 힌트를 얻었다. 혹시 새로운 객체를 반환하면서 null로 된 것이 아닐까?&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;521&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dfzDOm/btstw4fXeQ8/TQGTsbrc3cujcWiekHwCYK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dfzDOm/btstw4fXeQ8/TQGTsbrc3cujcWiekHwCYK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dfzDOm/btstw4fXeQ8/TQGTsbrc3cujcWiekHwCYK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdfzDOm%2Fbtstw4fXeQ8%2FTQGTsbrc3cujcWiekHwCYK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;774&quot; height=&quot;315&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;521&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;기본적으로 id (@Id 어노테이션이 붙어있는 필드)가&lt;b&gt; primitive 타입이 아니라면 null 여부를 통해&lt;/b&gt; 새로운 객체임을 판단하고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;primitive 타입 중 &lt;b&gt;Number 타입이라면 값이 0L인지 체크&lt;/b&gt;하여 새로운 객체인지를 판단하게 된다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1694267829937&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
@EntityListeners(AuditingEntityListener.class)
public class User {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
}&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;User 엔티티의 아이디는 Long이고, ReferenceType이기 때문에&lt;span style=&quot;color: #ef5369;&quot;&gt; null이 아닌 1L을 가지게 되면 isNew() 메서드에서 false를 반환&lt;/span&gt;하게 된다. 그렇기 때문에, DB에 데이터가 존재하든 말든 persist가 아닌 &lt;span style=&quot;color: #ef5369;&quot;&gt;merge() 를 진행&lt;/span&gt;하게 된 것이었다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;618&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dtEMcI/btstvPjdT4y/3CsEFqbEY3iV3N5dC2ZWk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dtEMcI/btstvPjdT4y/3CsEFqbEY3iV3N5dC2ZWk0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dtEMcI/btstvPjdT4y/3CsEFqbEY3iV3N5dC2ZWk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdtEMcI%2FbtstvPjdT4y%2F3CsEFqbEY3iV3N5dC2ZWk0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;748&quot; height=&quot;361&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;618&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로, em.merge() 메서드를 타고 들어가다 보면 아래와 같이 MergeEventListener에 대한 이벤트가 발생하는 것을 볼 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;920&quot; data-origin-height=&quot;210&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/M6sGC/btstsPcLTgz/nyZ7CqxlTQu1RZVr9ta6rk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/M6sGC/btstsPcLTgz/nyZ7CqxlTQu1RZVr9ta6rk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/M6sGC/btstsPcLTgz/nyZ7CqxlTQu1RZVr9ta6rk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FM6sGC%2FbtstsPcLTgz%2FnyZ7CqxlTQu1RZVr9ta6rk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;733&quot; height=&quot;167&quot; data-origin-width=&quot;920&quot; data-origin-height=&quot;210&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 MergeEventListener::onMerge 메서드를 타고 들어가다 보면, entityState 라는 변수에 &amp;lsquo;DETACHED&amp;rsquo;라는 값이 들어간다.&lt;br /&gt;우리는&amp;nbsp;이전에&amp;nbsp;JAVAJIGI&amp;nbsp;엔티티를&amp;nbsp;저장하거나&amp;nbsp;하지&amp;nbsp;않았음에도&amp;nbsp;&lt;span style=&quot;color: #ef5369;&quot;&gt;id를&amp;nbsp;지정했기&amp;nbsp;때문에&amp;nbsp;분리된&amp;nbsp;상태라고&amp;nbsp;판단&lt;/span&gt;하게&amp;nbsp;된&amp;nbsp;것이다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;305&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cvhkGA/btstxoSJQ6w/1R5G5iCEDt5F7KbSaK0Yh0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cvhkGA/btstxoSJQ6w/1R5G5iCEDt5F7KbSaK0Yh0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cvhkGA/btstxoSJQ6w/1R5G5iCEDt5F7KbSaK0Yh0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcvhkGA%2FbtstxoSJQ6w%2F1R5G5iCEDt5F7KbSaK0Yh0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;747&quot; height=&quot;178&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;305&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고, entityIsDetached() 메서드를 또 타고 들어가면 위와 같은 부분을 만날 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Transient한 상태의 엔티티를 저장하는 것처럼 보이는데, 내부적으로 파고 들어가보자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;284&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjj00t/btstq7Y0rUB/baQvkFguZS8lGArfXwc231/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjj00t/btstq7Y0rUB/baQvkFguZS8lGArfXwc231/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjj00t/btstq7Y0rUB/baQvkFguZS8lGArfXwc231/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbjj00t%2Fbtstq7Y0rUB%2FbaQvkFguZS8lGArfXwc231%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;716&quot; height=&quot;159&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;284&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 위와 같이 callbackRegistry.preCreate() 라는 메서드를 호출하는 것을 볼 수 있다.&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;1280&quot; data-origin-height=&quot;715&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RxZvc/btstxpRFmQi/K1D2xiKvkTeT2XD1inTAYK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RxZvc/btstxpRFmQi/K1D2xiKvkTeT2XD1inTAYK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RxZvc/btstxpRFmQi/K1D2xiKvkTeT2XD1inTAYK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRxZvc%2FbtstxpRFmQi%2FK1D2xiKvkTeT2XD1inTAYK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;715&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;715&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;이때, 인자로 넘겨주는 bean의 경우 createdAt, updatedAt이 채워지지 않은 상태의 JAVAJIGI 객체 정보이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;474&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cMKAWb/btstsN0iHMT/v7MkpKKLEprl92zmt2VDuK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cMKAWb/btstsN0iHMT/v7MkpKKLEprl92zmt2VDuK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cMKAWb/btstsN0iHMT/v7MkpKKLEprl92zmt2VDuK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcMKAWb%2FbtstsN0iHMT%2Fv7MkpKKLEprl92zmt2VDuK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;474&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;474&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 callback을 확인해보면 User 엔티티에서 설정했던 &lt;b&gt;AuditingEntityListener&lt;/b&gt;가 실행된 것을 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한, 콜백타입 역시 PRE_PERSIST로 설정이 되어 있는데, 이건 무엇을 뜻하는 것일까?&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;718&quot; data-origin-height=&quot;604&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bRSJY7/btstyxPaD5F/FkT7YxqedicdnANK59dJT0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bRSJY7/btstyxPaD5F/FkT7YxqedicdnANK59dJT0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bRSJY7/btstyxPaD5F/FkT7YxqedicdnANK59dJT0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbRSJY7%2FbtstyxPaD5F%2FFkT7YxqedicdnANK59dJT0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;510&quot; height=&quot;429&quot; data-origin-width=&quot;718&quot; data-origin-height=&quot;604&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;바로, 위에서 봤던 JPA Auditing 기능에서 존재했던 &lt;span style=&quot;color: #ef5369;&quot;&gt;touchForUpdate() 메서드 위에 달려있던 어노테이션&lt;/span&gt;이었던 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 정리하자면&lt;b&gt; save() &amp;rarr; em.merge() 동작 &amp;rarr; merge 이벤트 시 JPA Auditing 동작 &amp;rarr; 필드 채워짐&lt;/b&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;style3&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;br /&gt;&lt;b&gt;  아니, 그래서 테스트는 왜 깨진 건데요?&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 돌아가서, em.merge()가 수행될 때 호출되는 fireMerge() 메서드를 보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 언급했지만, JPA에서 save 시&lt;span style=&quot;color: #ef5369;&quot;&gt; isNew()가 false라면 em.merge()가 수행된 결과값을 save에서 반환&lt;/span&gt;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리의 텍스트 픽스처 JAVAJIGI의 경우 id의 타입이 레퍼런스 타입인 Long이었으며, 1L로 채워져있기 때문에 isNew()에서 false가 반환되었던 상태이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;755&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sAOUd/btstztMwW2x/2BizeEUER7uMqOMSlPNwj0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sAOUd/btstztMwW2x/2BizeEUER7uMqOMSlPNwj0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sAOUd/btstztMwW2x/2BizeEUER7uMqOMSlPNwj0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsAOUd%2FbtstztMwW2x%2F2BizeEUER7uMqOMSlPNwj0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;755&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;755&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 메서드에서는 event.getResult() 라는 값이 반환되는데, result에서는 createdAt, updatedAt 값이 채워진 객체가 반환된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, 여기서 중요한 건 &lt;span style=&quot;color: #ef5369;&quot;&gt;원본 객체 (텍스트 픽스처인 JAVAJIGI)와 반환되는 객체가 아예 다른 객체&lt;/span&gt;라는 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;719&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bZtLW0/btstxirqw4h/P4CWI7f9YHHpkKzLjOPnO1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bZtLW0/btstxirqw4h/P4CWI7f9YHHpkKzLjOPnO1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bZtLW0/btstxirqw4h/P4CWI7f9YHHpkKzLjOPnO1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbZtLW0%2Fbtstxirqw4h%2FP4CWI7f9YHHpkKzLjOPnO1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;719&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;719&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;original의 경우 User@8627이었으나, result의 경우 User@8631으로, 아예 다른 객체를 반환하고 있는 것을 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1694268917333&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
@DisplayName(&quot;유저를 저장한다.&quot;)
void save1() {
	final User actualUser = userRepository.save(UserTest.JAVAJIGI);

	assertThat(actualUser).usingRecursiveComparison()
		.ignoringFields(&quot;id&quot;)
		.isEqualTo(UserTest.JAVAJIGI);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리의 테스트 코드에서는 save로 반환된 객체와 JAVAJIGI가 아예 다른 객체가 된 것이고, save로 인해 기존의 JAVAJIGI 엔티티의 값을 변경할 것이라는 예상과 다르게&lt;span style=&quot;color: #ef5369;&quot;&gt; '값이 채워진 상태의 새로운 객체를 만들어 반환'&lt;/span&gt;했기 때문에 오류가 발생한 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, JAVAJIGI의 createdAt, updatedAt은 그대로 null이고, auditing은 새로운 객체에서 진행하게 되어 값이 채워져 반환된 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1694269086521&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
@DisplayName(&quot;유저를 저장한다.&quot;)
void save1() {
	final User actualUser = userRepository.save(UserTest.JAVAJIGI);

	assertThat(entityManager.contains(UserTest.JAVAJIGI))
		.isFalse();

	assertThat(entityManager.contains(actualUser))
		.isTrue();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한, 기존의 JAVAJIGI 객체는 영속성 컨텍스트에서 관리되지 않고 &lt;span style=&quot;color: #ef5369;&quot;&gt;merge 이후 새롭게 반환받은 객체가 영속성 컨텍스트에서 관리&lt;/span&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;style3&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  그렇다면, save2()에서는 어떻게 동작했을까?&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1104&quot; data-origin-height=&quot;580&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cL060W/btstxn0A9HM/RVIt1E3mmERSsBRvvcAik0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cL060W/btstxn0A9HM/RVIt1E3mmERSsBRvvcAik0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cL060W/btstxn0A9HM/RVIt1E3mmERSsBRvvcAik0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcL060W%2Fbtstxn0A9HM%2FRVIt1E3mmERSsBRvvcAik0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;641&quot; height=&quot;337&quot; data-origin-width=&quot;1104&quot; data-origin-height=&quot;580&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;save2()에서 사용된 SANJIGI의 경우 id가 지정되지 않아 처음 save() 시 merge가 아닌 persist() 메서드가 호출된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;652&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/4rOvO/btstxPoYVWE/3deMEq98DuSIAmUaG2ljVK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/4rOvO/btstxPoYVWE/3deMEq98DuSIAmUaG2ljVK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/4rOvO/btstxPoYVWE/3deMEq98DuSIAmUaG2ljVK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F4rOvO%2FbtstxPoYVWE%2F3deMEq98DuSIAmUaG2ljVK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;652&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;652&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;em.persist() 메서드를 타고 들어가다 보면 아래와 같이 persist에 대한 이벤트가 발생하는 것을 확인할 수 있다.&lt;br /&gt;여기서 메서드의 차이도 있는데, fireMerge()의 경우 event의 result 값을 반환했었지만 여기서는 메서드의 반환값이 void인 것을 볼 수 있다. 즉, &lt;span style=&quot;color: #ef5369;&quot;&gt;새로운 객체를 생성하는 것이 아닌 기존의 객체를 관리한다&lt;/span&gt;는 것을 추론할 수 있다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;940&quot; data-origin-height=&quot;672&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yDvQf/btstwZFDAwr/f6VDEWmSPwaxRzr7bh6BlK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yDvQf/btstwZFDAwr/f6VDEWmSPwaxRzr7bh6BlK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yDvQf/btstwZFDAwr/f6VDEWmSPwaxRzr7bh6BlK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyDvQf%2FbtstwZFDAwr%2Ff6VDEWmSPwaxRzr7bh6BlK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;505&quot; height=&quot;361&quot; data-origin-width=&quot;940&quot; data-origin-height=&quot;672&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;PersistEventListener::onPersist를&amp;nbsp;타고&amp;nbsp;들어가다&amp;nbsp;보면&amp;nbsp;아래와&amp;nbsp;같이&amp;nbsp;entityState가&amp;nbsp;&amp;lsquo;TRANSIENT&amp;rsquo;&amp;nbsp;상태인&amp;nbsp;것을&amp;nbsp;볼&amp;nbsp;수&amp;nbsp;있다.&lt;br /&gt;이는&lt;b&gt; entityManager에 의해 관리되지 않는 순수한 상태&lt;/b&gt;를 의미한다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;328&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yo3ZW/btstxn7k8Cq/NwdsRRFQlzJTk0MsHruwIk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yo3ZW/btstxn7k8Cq/NwdsRRFQlzJTk0MsHruwIk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yo3ZW/btstxn7k8Cq/NwdsRRFQlzJTk0MsHruwIk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fyo3ZW%2Fbtstxn7k8Cq%2FNwdsRRFQlzJTk0MsHruwIk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;328&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;328&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 메서드에서 entityIsTransient() 메서드를 타고 들어가다 보면, 위와 같은 상황을 만날 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 인자로 넘기는 entity에 대한 정보는 &lt;span style=&quot;color: #ef5369;&quot;&gt;우리가 넘긴 SANJIGI 객체&lt;/span&gt;인 것이다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;saveWithGeneratedId()의&amp;nbsp;이후의&amp;nbsp;과정은&amp;nbsp;save1()의&amp;nbsp;예제에서&amp;nbsp;봤던&amp;nbsp;동작&amp;nbsp;원리랑&amp;nbsp;똑같다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만, createdAt, updatedAt에 대한 값이 &lt;b&gt;실제 SANJIGI 객체에서 채워진다&lt;/b&gt;는 것이 큰 특징이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;cf. 위에서 다루지 않은 것 같아 말하지만 Id 값 역시 이 메서드에서 채워진다!&lt;br /&gt;어떤 제너레이터를 사용하는지에 따라 달라진다. (IdentifierGenerator 활용)&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;1280&quot; data-origin-height=&quot;690&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HLJ9e/btstsOkB3yy/1LrU3KPuMszT4ZG56MHaNK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HLJ9e/btstsOkB3yy/1LrU3KPuMszT4ZG56MHaNK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HLJ9e/btstsOkB3yy/1LrU3KPuMszT4ZG56MHaNK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHLJ9e%2FbtstsOkB3yy%2F1LrU3KPuMszT4ZG56MHaNK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;690&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;690&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 save 전후의 값을 살펴보면 동일한 엔티티에 대해서 (User@8547) 값이 채워져서 들어가는 것을 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1694269434948&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
@DisplayName(&quot;유저를 저장한다.&quot;)
void save2() {
	final User actualUser = userRepository.save(UserTest.SANJIGI);

	assertThat(entityManager.contains(UserTest.SANJIGI))
		.isTrue();
	assertThat(entityManager.contains(actualUser))
		.isTrue();
}&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;또한,&amp;nbsp; save2()에서는 save 이후 SANJIGI와 actualUser 모두 영속성 컨텍스트에서 관리하는 것이 특징이다.&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;style3&quot; /&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;  em.merge() 시에 반환받은 엔티티만 사용하자&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1694269484006&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
@DisplayName(&quot;유저를 저장한다.&quot;)
void save1_duplicated_save() {
	final User actualUser1 = userRepository.save(UserTest.JAVAJIGI);
	final User actualUser2 = userRepository.save(UserTest.JAVAJIGI);

	assertThat(actualUser1).usingRecursiveComparison()
		.ignoringFields(&quot;id&quot;, &quot;createdAt&quot;, &quot;updatedAt&quot;)
		.isEqualTo(UserTest.JAVAJIGI);

	assertThat(actualUser2).usingRecursiveComparison()
		.ignoringFields(&quot;id&quot;)
		.isEqualTo(UserTest.JAVAJIGI);
}&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;save1()&amp;nbsp;메서드에서&amp;nbsp;한&amp;nbsp;번&amp;nbsp;더&amp;nbsp;객체를&amp;nbsp;저장했을&amp;nbsp;때의&amp;nbsp;실험이다.&lt;br /&gt;actualUser1의 경우 JAVAJIGI와 actualUser1이 다르게 관리되었기 때문에 두 값이 달랐었다. (createdAt, updatedAt이 채워짐)&lt;br /&gt;하지만,&amp;nbsp;한&amp;nbsp;번&amp;nbsp;더&amp;nbsp;저장한&amp;nbsp;actualUser2의&amp;nbsp;경우&lt;b&gt;&amp;nbsp;JAVAJIGI와&amp;nbsp;동일하게&amp;nbsp;createdAt,&amp;nbsp;updatedAt이&amp;nbsp;null&lt;/b&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1004&quot; data-origin-height=&quot;346&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bdjNYR/btstCFMGPTD/pvbUIkJX0f188c2KN6Smj0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bdjNYR/btstCFMGPTD/pvbUIkJX0f188c2KN6Smj0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bdjNYR/btstCFMGPTD/pvbUIkJX0f188c2KN6Smj0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbdjNYR%2FbtstCFMGPTD%2FpvbUIkJX0f188c2KN6Smj0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;779&quot; height=&quot;268&quot; data-origin-width=&quot;1004&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;먼저, actualUser1의 형태는 위와 같이 createdAt, updatedAt이 채워져 있는 형태이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;562&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5C0st/btstr3PD3Od/IIxDJtx5RZd8k6oX8qLYD1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5C0st/btstr3PD3Od/IIxDJtx5RZd8k6oX8qLYD1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5C0st/btstr3PD3Od/IIxDJtx5RZd8k6oX8qLYD1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5C0st%2Fbtstr3PD3Od%2FIIxDJtx5RZd8k6oX8qLYD1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;562&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;562&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;actualUser1이 저장되는 시점의 merge() 로직을 다시 한 번 보자.&lt;br /&gt;첫 번째 save() 로직에서는 MergeEventListener::onMerge 메서드에서 (merge 이벤트 발생할 때) 내부를 따라가다보면 영속성 컨텍스트에서 &lt;b&gt;동일한 키값으로 관리되는 엔티티가 있는지 확인&lt;/b&gt;한다. &lt;span style=&quot;color: #ef5369;&quot;&gt;처음 save()를 할 때는 저장된 엔티티가 없기 때문에 없다&lt;/span&gt;고&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;1280&quot; data-origin-height=&quot;398&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tFvYV/btstyuLGd9W/EtRDIkG9wQUA0yeAdTEog1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tFvYV/btstyuLGd9W/EtRDIkG9wQUA0yeAdTEog1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tFvYV/btstyuLGd9W/EtRDIkG9wQUA0yeAdTEog1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtFvYV%2FbtstyuLGd9W%2FEtRDIkG9wQUA0yeAdTEog1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;398&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;398&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한, entityState가 null이었지만, getEntityState를 통해 DETACHED인 상태라고 판단하게 된다.&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;1280&quot; data-origin-height=&quot;503&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0VDY8/btsts66mQsA/ritCKiR9usKYHphTpIcQA0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0VDY8/btsts66mQsA/ritCKiR9usKYHphTpIcQA0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0VDY8/btsts66mQsA/ritCKiR9usKYHphTpIcQA0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0VDY8%2Fbtsts66mQsA%2FritCKiR9usKYHphTpIcQA0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;503&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;503&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고,&amp;nbsp;처음&amp;nbsp;봤을&amp;nbsp;때처럼&amp;nbsp;entityState가&amp;nbsp;DETACHED여서&amp;nbsp;entityIsDetached()&amp;nbsp;메서드를&amp;nbsp;타게&amp;nbsp;된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, 영속성 컨텍스트 내부에는 id = 1이면서 entityName이 User인 객체가 존재하지 않아 result 값이 null이 되어,&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;DETACHED 상태임에도 실제로는 영속화가 필요한 객체라고 판단하여 영속화를 진행&lt;/span&gt;하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;689&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bpXyfi/btstwFgk2dx/jT0BwMkrk50ojh69T3DQi0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bpXyfi/btstwFgk2dx/jT0BwMkrk50ojh69T3DQi0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bpXyfi/btstwFgk2dx/jT0BwMkrk50ojh69T3DQi0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbpXyfi%2FbtstwFgk2dx%2FjT0BwMkrk50ojh69T3DQi0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;689&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;689&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, 두 번째 save() 시에는 로직이 달라진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;em.merge()를 진행하게 되면 인자로 넘겨준 객체가 아닌, &lt;b&gt;반환받은 객체를 영속성 컨텍스트에서 관리&lt;/b&gt;한다고 하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇기 때문에 첫 번째 save() 시에 반환된 actualUser1만 영속성 컨텍스트에서 관리하고, &lt;span style=&quot;color: #ef5369;&quot;&gt;JAVAJIGI는 영속성 컨텍스트에서 관리하지 않게 된다&lt;/span&gt;. 하지만, 두 번째 save 시 id 값이 이미 1L로 채워진 객체가 인자로 들어왔고, 영속성 컨텍스트에는 id = 1L인 actualUser가 관리되고 있기 때문에 entry 값이 null이 아니게 된다.&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;1124&quot; data-origin-height=&quot;420&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/btTfrl/btstwHytcl9/1PXKvHSOcF3gwzZXR3WKw1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/btTfrl/btstwHytcl9/1PXKvHSOcF3gwzZXR3WKw1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/btTfrl/btstwHytcl9/1PXKvHSOcF3gwzZXR3WKw1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbtTfrl%2FbtstwHytcl9%2F1PXKvHSOcF3gwzZXR3WKw1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;256&quot; data-origin-width=&quot;1124&quot; data-origin-height=&quot;420&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 entry 값이 위와 같이 createdAt, updatedAt이 채워진 actualUser1 객체임을 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후,&amp;nbsp;entityState가 DETACHED여서 entityIsDetached() 메서드를 타게 된다.&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;1280&quot; data-origin-height=&quot;553&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BmZZq/btstsNsvchO/BDzbqIrjktn0OeUK9WU2ek/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BmZZq/btstsNsvchO/BDzbqIrjktn0OeUK9WU2ek/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BmZZq/btstsNsvchO/BDzbqIrjktn0OeUK9WU2ek/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBmZZq%2FbtstsNsvchO%2FBDzbqIrjktn0OeUK9WU2ek%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;553&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;553&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부를 보면 첫 번째 save와 다르게 id=1, entityName이 User인 actualUser1가 관리되고 있어 result가 존재하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 else 구문에 있는 새로운 로직이 실행되는 것을 볼 수 있다.&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;1280&quot; data-origin-height=&quot;487&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bJwTVU/btstwfBWUhc/sROnsFc3arM7pwkZn3vN0k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bJwTVU/btstwfBWUhc/sROnsFc3arM7pwkZn3vN0k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bJwTVU/btstwfBWUhc/sROnsFc3arM7pwkZn3vN0k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbJwTVU%2FbtstwfBWUhc%2FsROnsFc3arM7pwkZn3vN0k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;487&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;487&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MergeContext.put()의 경우, &lt;b&gt;첫 번째 인자로 병합할 엔티티를, 두 번째 인자로 이미 관리되고 있는 엔티티를 받아서 병합&lt;/b&gt;한다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;872&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zUlS6/btsts6k2Sck/DkeXkHZLo9UIqTCQFPfk30/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zUlS6/btsts6k2Sck/DkeXkHZLo9UIqTCQFPfk30/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zUlS6/btsts6k2Sck/DkeXkHZLo9UIqTCQFPfk30/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzUlS6%2Fbtsts6k2Sck%2FDkeXkHZLo9UIqTCQFPfk30%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;728&quot; height=&quot;496&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;872&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 호출부를 보면 병합할 엔티티로 기존의 JAVAJIGI 객체가, 이미 관리되고 있는 엔티티로 영속성 컨텍스트에 의해 관리되던 actualUser1 값이 들어가는 것을 볼 수 있다.&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;1280&quot; data-origin-height=&quot;649&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Icdfr/btstxmm5jJT/8u5gjYZ9IkErPdH44QB360/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Icdfr/btstxmm5jJT/8u5gjYZ9IkErPdH44QB360/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Icdfr/btstxmm5jJT/8u5gjYZ9IkErPdH44QB360/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIcdfr%2Fbtstxmm5jJT%2F8u5gjYZ9IkErPdH44QB360%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;649&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;649&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 copyValues를 통해 JAVAJIGI 객체에 대한 정보를 바탕으로 actualUser1의 객체의 정보에 복사가 진행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이로 인해 &lt;span style=&quot;color: #ef5369;&quot;&gt;JAVAJIGI가 가진 createdAt, updatedAt의 null 값이 전파&lt;/span&gt;된 것이다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;797&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/JMCLG/btstyuyaHF2/i3gbkRB8HBiVQkdWMhpZO0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/JMCLG/btstyuyaHF2/i3gbkRB8HBiVQkdWMhpZO0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/JMCLG/btstyuyaHF2/i3gbkRB8HBiVQkdWMhpZO0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJMCLG%2FbtstyuyaHF2%2Fi3gbkRB8HBiVQkdWMhpZO0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;714&quot; height=&quot;445&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;797&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종적으로 결과를 세팅해주는 부분을 보면 null로 업데이트 되어서 result를 세팅해주는 것을 코드로 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서, actualUser2의 createdAt, updatedAt 값이 null이 되어서 JAVAJIGI랑 같은 값을 가지게 되는 것이다.&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;style3&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;br /&gt;&lt;b&gt;  결론&lt;/b&gt;&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;- em.merge()는 파라미터로 넘어온 엔티티의 '식별자 값'으로 영속성 컨텍스트를 조회하고, 찾는 엔티티가 없으면 데이터베이스에서 조회한다.&lt;br /&gt;- 만약 데이터베이스에서도 발견하지 못하면 새로운 엔티티를 생성하여 병합한다.&lt;br /&gt;- 병합은 준영속 상태와 비영속 상태를 신경쓰지 않기 때문에, 식별자 값으로 엔티티를 조회할 수 있으면 조회하여 병합하고, 조회할 수 없으면 새로 생성하여 병합한다.&lt;br /&gt;- 따라서 병합은 save or update 기능을 수행한다.&lt;br /&gt;- em.merge를 사용할 때는 반환받은 객체만 영속성 컨텍스트에서 관리한다는 점을 유의하자!&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;JPA는 알다가도 정말 모르겠다 ㅎㅎ  &lt;/p&gt;</description>
      <category>Back-end/JPA</category>
      <category>em.merge()</category>
      <category>JPA</category>
      <category>JPA Auditing</category>
      <author>dolmeng2</author>
      <guid isPermaLink="true">https://cl8d.tistory.com/123</guid>
      <comments>https://cl8d.tistory.com/123#entry123comment</comments>
      <pubDate>Sun, 10 Sep 2023 13:44:28 +0900</pubDate>
    </item>
    <item>
      <title>[QueryDSL] QueryDSL 사용 시 NPE가 발생했을 때 해결하기</title>
      <link>https://cl8d.tistory.com/122</link>
      <description>&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;프로젝트를 진행하면서 동적 쿼리를 작성할 일이 많아 QueryDSL을 도입하게 되었는데, 이번에 색다른 오류를 발견하게 되었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-08-13 오후 11.17.06.png&quot; data-origin-width=&quot;2224&quot; data-origin-height=&quot;162&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Nydnl/btsrdiM2CZU/ppdQtUcuKORvXm5RKUZPGK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Nydnl/btsrdiM2CZU/ppdQtUcuKORvXm5RKUZPGK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Nydnl/btsrdiM2CZU/ppdQtUcuKORvXm5RKUZPGK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNydnl%2FbtsrdiM2CZU%2FppdQtUcuKORvXm5RKUZPGK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2224&quot; height=&quot;162&quot; data-filename=&quot;스크린샷 2023-08-13 오후 11.17.06.png&quot; data-origin-width=&quot;2224&quot; data-origin-height=&quot;162&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;java.lang.NullPointerException: Cannot read field &quot;id&quot; because &quot;co.kirikiri.domain.goalroom.QGoalRoomMember.goalRoomMember.goalRoom.roadmapContent.roadmap&quot; is null&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NPE를 본 건 오랜만이어서 조금 삽질을 진행했다.&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;style3&quot; /&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&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;a title=&quot;지난 포스팅&quot; href=&quot;https://cl8d.tistory.com/120&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;지난 포스팅&lt;/a&gt;과 같은 프로젝트이기 때문에 도메인 구조는 거의 동일한데, 위 문제 상황을 이해하기 위한 추가적인 도메인 정보가 필요하다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-08-13 오후 10.09.19.png&quot; data-origin-width=&quot;1562&quot; data-origin-height=&quot;1074&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mwxqi/btsrawLxhXA/HyjxikktS2Hi089vlfYYy1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mwxqi/btsrawLxhXA/HyjxikktS2Hi089vlfYYy1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mwxqi/btsrawLxhXA/HyjxikktS2Hi089vlfYYy1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fmwxqi%2FbtsrawLxhXA%2FHyjxikktS2Hi089vlfYYy1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;745&quot; height=&quot;512&quot; data-filename=&quot;스크린샷 2023-08-13 오후 10.09.19.png&quot; data-origin-width=&quot;1562&quot; data-origin-height=&quot;1074&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 로드맵 본문(RoadmapContent) 정보에 대해서 여러 개의 골룸(GoalRoom)이 생성될 수 있고, &lt;span style=&quot;color: #ef5369;&quot;&gt;하나의 골룸에 대해서 여러 개의 골룸 대기 멤버(GoalRoomPendingMembers)&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;/p&gt;
&lt;pre id=&quot;code_1691931838494&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
void 카테고리_조건_없이_주어진_로드맵_이전의_데이터를_참가중인_인원이_많은순으로_조회한다() {
    ...

    final Roadmap gameRoadmap1 = 노드_정보를_포함한_로드맵을_생성한다(&quot;게임 로드맵&quot;, creator, gameCategory);
    final Roadmap gameRoadmap2 = 노드_정보를_포함한_로드맵을_생성한다(&quot;게임 로드맵2&quot;, creator, gameCategory);
    final Roadmap travelRoadmap = 노드_정보를_포함한_로드맵을_생성한다(&quot;여행 로드맵&quot;, creator, travelCategory);
  
    final GoalRoom gameRoadmap1GoalRoom = 골룸을_생성한다(gameRoadmap1.getContents().getValues().get(0), creator);
    final GoalRoom gameRoadmap2GoalRoom = 골룸을_생성한다(gameRoadmap2.getContents().getValues().get(0), creator);
    final GoalRoom travelRoadmapGoalRoom = 골룸을_생성한다(travelRoadmap.getContents().getValues().get(0), creator);

    // gameRoadmap1 : 참가인원 0명
    // gameRoadmap2 : 참가인원 1명
    final List&amp;lt;GoalRoomMember&amp;gt; gameRoadmap2GoalRoomMembers = List.of(
            new GoalRoomMember(GoalRoomRole.LEADER, LocalDateTime.now(), gameRoadmap2GoalRoom, creator));
    goalRoomMemberRepository.saveAll(gameRoadmap2GoalRoomMembers);

    // travelRoadmap : 참가인원 2명
    final List&amp;lt;GoalRoomMember&amp;gt; travelRoadmapGoalRoomMembers = List.of(
            new GoalRoomMember(GoalRoomRole.LEADER, LocalDateTime.now(), travelRoadmapGoalRoom, creator),
            new GoalRoomMember(GoalRoomRole.FOLLOWER, LocalDateTime.now(), travelRoadmapGoalRoom, follower));
    goalRoomMemberRepository.saveAll(travelRoadmapGoalRoomMembers);

    final RoadmapCategory category = null;
    final RoadmapFilterType orderType = RoadmapFilterType.PARTICIPANT_COUNT;

    // when
    final List&amp;lt;Roadmap&amp;gt; firstRoadmapRequest = roadmapRepository.findRoadmapsByCategory(category, orderType,
            null, 2);
    final List&amp;lt;Roadmap&amp;gt; secondRoadmapRequest = roadmapRepository.findRoadmapsByCategory(category, orderType,
            gameRoadmap2.getId(), 10);

    ...
}&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;/p&gt;
&lt;pre id=&quot;code_1691932495399&quot; class=&quot;kotlin&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;@Override
public List&amp;lt;Roadmap&amp;gt; findRoadmapsByCategory(final RoadmapCategory category, final RoadmapFilterType orderType,
                                            final Long lastId, final int pageSize) {

    return selectFrom(roadmap)
            ...
            .where(
                    lessThanLastId(lastId, orderType)
           	)
            .fetch();
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1691932502229&quot; class=&quot;reasonml&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 BooleanExpression lessThanLastId(final Long lastId, final RoadmapFilterType orderType) {
    ...
    final NumberPath&amp;lt;Long&amp;gt; goalRoomMemberRoadmapId = goalRoomMember.goalRoom.roadmapContent.roadmap.id;
    return participantCountCond(goalRoomMemberRoadmapId.eq(roadmap.id))
            .lt(participantCountCond(goalRoomMemberRoadmapId.eq(lastId)));
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인자로 받은 lastId (현재는 로드맵의 PK 값이 들어오고 있다.)에 해당하는 &lt;b&gt;로드맵의 참여자 수보다 더 적은 참여자 수를 구하는 로드맵을 구하는 쿼리&lt;/b&gt;이다.&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&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;  NPE가 발생할 지점을 찾아보기&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;co.kirikiri.domain.goalroom.QGoalRoomMember.goalRoomMember.goalRoom.&lt;span style=&quot;color: #ef5369;&quot;&gt;roadmapContent&lt;span style=&quot;text-align: left;&quot;&gt;.roadmap&lt;/span&gt; is null&lt;/span&gt;.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;675&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b2T0cT/btsq0vAvh9n/Lyq1iYbqZCMVrcXxDPom5K/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b2T0cT/btsq0vAvh9n/Lyq1iYbqZCMVrcXxDPom5K/img.jpg&quot; data-alt=&quot;음... roadmap이 null이구나&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b2T0cT/btsq0vAvh9n/Lyq1iYbqZCMVrcXxDPom5K/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb2T0cT%2Fbtsq0vAvh9n%2FLyq1iYbqZCMVrcXxDPom5K%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;455&quot; height=&quot;341&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;675&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;음... roadmap이 null이구나&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 goalRoomMember를 저장한 후에 로드맵에 대한 정보가 제대로 저장이 안 되어 있나 싶어서 findAll로 찾아보았다.&lt;/p&gt;
&lt;pre id=&quot;code_1691932916701&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;final List&amp;lt;GoalRoomMember&amp;gt; goalRoomMembers = goalRoomMemberRepository.findAll();
assertThat(goalRoomMembers.get(0).getGoalRoom().getRoadmapContent().getRoadmap().getId()).isNotNull();
assertThat(goalRoomMembers.get(1).getGoalRoom().getRoadmapContent().getRoadmap().getId()).isNotNull();
assertThat(goalRoomMembers.get(2).getGoalRoom().getRoadmapContent().getRoadmap().getId()).isNotNull();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NPE니까 당연히 roadmap의 id 값이 Null이 되어서 테스트가 실패하겠지?라고 생각하였다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-08-13 오후 10.23.41.png&quot; data-origin-width=&quot;1514&quot; data-origin-height=&quot;290&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cqiGRO/btsq2kd6ZLO/D3S4eD16NySmgQt8DvNAdk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cqiGRO/btsq2kd6ZLO/D3S4eD16NySmgQt8DvNAdk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cqiGRO/btsq2kd6ZLO/D3S4eD16NySmgQt8DvNAdk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcqiGRO%2Fbtsq2kd6ZLO%2FD3S4eD16NySmgQt8DvNAdk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1514&quot; height=&quot;290&quot; data-filename=&quot;스크린샷 2023-08-13 오후 10.23.41.png&quot; data-origin-width=&quot;1514&quot; data-origin-height=&quot;290&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  QueryDSL의 객체 그래프&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1691933101531&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;final NumberPath&amp;lt;Long&amp;gt; goalRoomMemberRoadmapId = goalRoomMember.goalRoom.roadmapContent.roadmap.id;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제가 발생했던 라인은 정확하게 위 라인이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떠한 값이 들어가고 말고의 문제를 떠나서, 객체 그래프를 탐색할 때 뭔가 문제가 있을 것 같다는 생각이 들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 열심히 구글링을 해본 결과, 아래와 같은 글을 발견하게 되었다.&lt;/p&gt;
&lt;figure id=&quot;og_1691933237355&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;NullPointerException on QueryDSL where clause&quot; data-og-description=&quot;Using Query DSL with hibernate (Spring Data JPA) to build a query like so if( bankId != null ){ query.where( coopMember.personId.bankAccountId.isNotNull().and(&quot; data-og-host=&quot;stackoverflow.com&quot; data-og-source-url=&quot;https://stackoverflow.com/questions/48380798/nullpointerexception-on-querydsl-where-clause&quot; data-og-url=&quot;https://stackoverflow.com/questions/48380798/nullpointerexception-on-querydsl-where-clause&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bbcHl6/hyTCEWvw1F/P15iD4AjTfI5QfLfJpm6X1/img.png?width=316&amp;amp;height=316&amp;amp;face=0_0_316_316&quot;&gt;&lt;a href=&quot;https://stackoverflow.com/questions/48380798/nullpointerexception-on-querydsl-where-clause&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://stackoverflow.com/questions/48380798/nullpointerexception-on-querydsl-where-clause&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bbcHl6/hyTCEWvw1F/P15iD4AjTfI5QfLfJpm6X1/img.png?width=316&amp;amp;height=316&amp;amp;face=0_0_316_316');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;NullPointerException on QueryDSL where clause&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Using Query DSL with hibernate (Spring Data JPA) to build a query like so if( bankId != null ){ query.where( coopMember.personId.bankAccountId.isNotNull().and(&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;stackoverflow.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 답변 내용에서 말해 주신 &lt;a title=&quot;공식 문서&quot; href=&quot;http://querydsl.com/static/querydsl/3.6.0/reference/html/ch03s03.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;공식 문서&lt;/a&gt;를 한 번 읽어보았다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;By default Querydsl initializes only reference properties of the first two levels. &lt;br /&gt;In cases where longer initialization paths are required, these have to be annotated in the domain types via&amp;nbsp;&lt;br /&gt;com.mysema.query.annotations.QueryInit&amp;nbsp;annotations. &lt;br /&gt;QueryInit is used on properties where deep initializations are needed.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 QueryDSL은 &lt;span style=&quot;color: #ef5369;&quot;&gt;초기 2단계의 레벨에 있는 프로퍼티만 초기화&lt;/span&gt;하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 그 이상의 path를 초기화하고 싶다면 &lt;b&gt;@QueryInit 어노테이션&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  @QueryInit&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@QueryInit의 경우 기본적으로 &lt;b&gt;@Entity 어노테이션이 붙어 있는 클래스의 필드에 대해서 적용이 가능&lt;/b&gt;하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부 인자로 초기화하고 싶은 path를 지정해줄 수 있으며, *을 통해 와일드카드를 통해서도 지정이 가능하다.&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;먼저, @QueryInit을 지정하지 않았을 때의 큐파일을 디버깅해보았다. (QGoalRoom)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-08-14 오후 1.20.03.png&quot; data-origin-width=&quot;1578&quot; data-origin-height=&quot;802&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bXrhmG/btsq1FiUpq5/jVCnTHR69wEYq7vl00BCy1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bXrhmG/btsq1FiUpq5/jVCnTHR69wEYq7vl00BCy1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bXrhmG/btsq1FiUpq5/jVCnTHR69wEYq7vl00BCy1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbXrhmG%2Fbtsq1FiUpq5%2FjVCnTHR69wEYq7vl00BCy1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1578&quot; height=&quot;802&quot; data-filename=&quot;스크린샷 2023-08-14 오후 1.20.03.png&quot; data-origin-width=&quot;1578&quot; data-origin-height=&quot;802&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확인해보면 GoalRoom에서 QRoadmapContent까지의 값은 잘 가져오지만, QRoadmap에 대해서는 정의가 되어 있지 않은 것을 볼 수 있다. 여기서 QRoadmap 값이 null로 정의되었기 때문에 탐색이 불가능했던 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1691986844834&quot; class=&quot;kotlin&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;public class GoalRoomMember {

   ...

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = &quot;goal_room_id&quot;, nullable = false)
    @QueryInit(value = {&quot;roadmapContent.roadmap&quot;})
    protected GoalRoom goalRoom;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서, 위와 같이 GoalRoomMember 엔티티가 GoalRoom 엔티티를 정의한 필드에 @QueryInit을 정의해주었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 되면 QGoalRoom이 초기화될 때 RoadmapContent와 Roadmap 정보까지 함께 초기화가 가능할 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로, 내부 value의 값은 goalRoom 엔티티가 참조하고 있는 실제 필드명으로 작성해야 한다.&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-08-14 오후 1.26.13.png&quot; data-origin-width=&quot;2266&quot; data-origin-height=&quot;304&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bqgecw/btsq8Kp4Ltt/hDIXS1nA9uqj21sJZmjfF1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bqgecw/btsq8Kp4Ltt/hDIXS1nA9uqj21sJZmjfF1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bqgecw/btsq8Kp4Ltt/hDIXS1nA9uqj21sJZmjfF1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbqgecw%2Fbtsq8Kp4Ltt%2FhDIXS1nA9uqj21sJZmjfF1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2266&quot; height=&quot;304&quot; data-filename=&quot;스크린샷 2023-08-14 오후 1.26.13.png&quot; data-origin-width=&quot;2266&quot; data-origin-height=&quot;304&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;goalRoom이 실제로 의존하고 있는 필드명으로 작성해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1691987245148&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = &quot;goal_room_id&quot;, nullable = false)
@QueryInit(value = {&quot;roadmapContent.*&quot;})
protected GoalRoom goalRoom;&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-08-14 오후 1.22.44.png&quot; data-origin-width=&quot;1706&quot; data-origin-height=&quot;886&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/daUKo2/btsrcrDJVwU/ZcirUnq5mnahyiQNqmQUO1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/daUKo2/btsrcrDJVwU/ZcirUnq5mnahyiQNqmQUO1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/daUKo2/btsrcrDJVwU/ZcirUnq5mnahyiQNqmQUO1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdaUKo2%2FbtsrcrDJVwU%2FZcirUnq5mnahyiQNqmQUO1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1706&quot; height=&quot;886&quot; data-filename=&quot;스크린샷 2023-08-14 오후 1.22.44.png&quot; data-origin-width=&quot;1706&quot; data-origin-height=&quot;886&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수정 후에 다시 디버깅을 진행해보면 이번에는 roadmap에 대한 정보도 잘 채워진 것을 확인할 수 있다.&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;스크린샷 2023-08-14 오후 1.28.57.png&quot; data-origin-width=&quot;1174&quot; data-origin-height=&quot;330&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bMSTse/btsrgozIzne/uJFvGXIpZEQfF2hLc4bJu1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bMSTse/btsrgozIzne/uJFvGXIpZEQfF2hLc4bJu1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bMSTse/btsrgozIzne/uJFvGXIpZEQfF2hLc4bJu1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbMSTse%2FbtsrgozIzne%2FuJFvGXIpZEQfF2hLc4bJu1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1174&quot; height=&quot;330&quot; data-filename=&quot;스크린샷 2023-08-14 오후 1.28.57.png&quot; data-origin-width=&quot;1174&quot; data-origin-height=&quot;330&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;+) 추가적으로, 이렇게 @QueryInit을 특정 필드에 적용하고 나면 기존에 잘 돌아가던 쿼리들이 안 돌아갈 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나 같은 경우에는 비슷한 다른 쿼리에서 오류가 발생했었는데, 2단계 레벨에 있는 필드에 대해서 NPE가 발생했었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-08-14 오후 1.58.30.png&quot; data-origin-width=&quot;1982&quot; data-origin-height=&quot;120&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CS2ZG/btsrf2RfOpj/Bt2zV42Z77mmdu1HbD6T81/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CS2ZG/btsrf2RfOpj/Bt2zV42Z77mmdu1HbD6T81/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CS2ZG/btsrf2RfOpj/Bt2zV42Z77mmdu1HbD6T81/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCS2ZG%2Fbtsrf2RfOpj%2FBt2zV42Z77mmdu1HbD6T81%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1982&quot; height=&quot;120&quot; data-filename=&quot;스크린샷 2023-08-14 오후 1.58.30.png&quot; data-origin-width=&quot;1982&quot; data-origin-height=&quot;120&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비슷한 오류지만 goalRoomMember -&amp;gt; member -&amp;gt; identifier (VO여서 단계에 반영하지 않는 것 같다. 처음에는 3단계라고 생각했는데 공식문서에서 2단계라고 정확하게 명시했으니까...)에 대해서 오류가 발생한 것이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@QueryInit을 달지 않았을 때는 3단계까지 잘 갔었는데, 명시적으로 어노테이션을 지정해주니까 이렇게 된 것 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1691989227735&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = &quot;goal_room_id&quot;, nullable = false)
@QueryInit(value = {&quot;roadmapContent.*&quot;})
protected GoalRoom goalRoom;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = &quot;member_id&quot;, nullable = false)
@QueryInit(value = {&quot;identifier&quot;})
protected Member member;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 member 필드에 대해서는 이렇게 identifier에 대해서도 정의를 해주었다.&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>개발일지</category>
      <category>@QueryInit</category>
      <category>querydsl</category>
      <author>dolmeng2</author>
      <guid isPermaLink="true">https://cl8d.tistory.com/122</guid>
      <comments>https://cl8d.tistory.com/122#entry122comment</comments>
      <pubDate>Mon, 14 Aug 2023 14:01:06 +0900</pubDate>
    </item>
    <item>
      <title>토스 2023 NEXT 개발자 챌린지 Server Developer 최종 합격 후기</title>
      <link>https://cl8d.tistory.com/121</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;사실 발표가 난 지는 꽤 됐지만 블로그에 기록으로 남겨두면 좋을 것 같아서 쓰는 글  &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추후 2024 NEXT가 진행되었을 때 도움이 된 분들이 있었으면 좋겠다!&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;style3&quot; /&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;올해 6월 말 정도, 토스에서 2023 NEXT 개발자 챌린지를 통해서 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;Core Banking이랑 Server 중에 어떤 걸 할지 고민했는데, 금융 관련 도메인에 대한 이해도가 현저히 낮은지라 Server 쪽 직무로 지원하게 되었다. 분야별 어디 파트를 하고 싶은지도 처음에 넣을 수 있는데, 그때는 토스랑 토스뱅크를 넣었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;코딩 테스트 &amp;gt; 직무 인터뷰 &amp;gt; 문화 적합성 인터뷰 &amp;gt; 레퍼런스 체크 &amp;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;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;&amp;nbsp;&lt;/h4&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시간 30분 동안 알고리즘 7문제랑 서술형 5문제를 풀어야 한다. 코테 문제는 4문제, 서술형은 2~3문제 정도 답을 써서 내고 나왔다.&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;또한, 문제 풀 때 테스트 케이스가 되게 많았어서 본인이 틀리게 풀었는지 점검이 가능한 점이 좋았던 것 같다.&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~3문제 중에서 제대로 쓴 건 2문제 정도여서 처참하다고 말할 수 있지만...ㅎ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1문제를 진짜 자세하게 썼다. 그냥 &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;span style=&quot;color: #ef5369;&quot;&gt;테크 유튜브들을 자주 챙겨보면 좋을 것 같다&lt;/span&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;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;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;&amp;nbsp;&lt;/h4&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;edited_edited_스크린샷 2023-08-10 오후 5.28.59.png&quot; data-origin-width=&quot;1708&quot; data-origin-height=&quot;649&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cXGhim/btsqQ9cLgFM/xb0tyVt6EqxSZbuVacSONk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cXGhim/btsqQ9cLgFM/xb0tyVt6EqxSZbuVacSONk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cXGhim/btsqQ9cLgFM/xb0tyVt6EqxSZbuVacSONk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcXGhim%2FbtsqQ9cLgFM%2Fxb0tyVt6EqxSZbuVacSONk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;794&quot; height=&quot;302&quot; data-filename=&quot;edited_edited_스크린샷 2023-08-10 오후 5.28.59.png&quot; data-origin-width=&quot;1708&quot; data-origin-height=&quot;649&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;그래도 기회가 왔으니 잡아야겠다는 생각으로 조금씩 준비했다.&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;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;노션을 통해 작성한 다음 PDF로 변형해서 제출&lt;/b&gt;했고&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;,&lt;/span&gt; 약 2페이지 정도&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이력서의 분량보다는, &lt;b&gt;이력서 내의 내용을 본인이 얼마나 숙지&lt;/b&gt;하고 있는지가 가장 중요한 것 같아서, &lt;span style=&quot;color: #ef5369;&quot;&gt;7페이지 - 8페이지 정도의 질문 목록을 작성&lt;/span&gt;하고 외워서 갔다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;1. A가 뭔가요?&lt;br /&gt;2. B도 있는데 왜 A를 쓰셨나요?&lt;br /&gt;3. A를 쓰면서 뭐가 어려웠나요?&lt;br /&gt;4. A를 쓸 때 ~~라는 상황이 발생할 수 있을 텐데, 이건 어떻게 해결할 수 있을 것 같나요?&lt;/blockquote&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;그리고 면접은... 약 1시간 30분 정도 진행이 되었었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;640&quot; data-origin-height=&quot;640&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cLlK7R/btsqLmc1GA3/h79hoYBeYMj33q3MLJeLC0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cLlK7R/btsqLmc1GA3/h79hoYBeYMj33q3MLJeLC0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cLlK7R/btsqLmc1GA3/h79hoYBeYMj33q3MLJeLC0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcLlK7R%2FbtsqLmc1GA3%2Fh79hoYBeYMj33q3MLJeLC0%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;350&quot; height=&quot;350&quot; data-origin-width=&quot;640&quot; data-origin-height=&quot;640&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;나름 이력서의 내용을 숙지하고 갔다고 생각했는데 아니었다... ㅎㅎ   대답을 잘 못한 부분도 많았다.&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;이력서 내용은 &lt;span style=&quot;color: #ef5369;&quot;&gt;100%가 아닌 200% 정도는 커버해서 준비&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;그리고 마지막 20분 정도는 역질문을 할 수 있는데, &lt;b&gt;이 시간 동안 정말 꽉꽉 채워서 여쭤봤었다&lt;/b&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;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;style3&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-08-10 오후 6.25.02.png&quot; data-origin-width=&quot;1658&quot; data-origin-height=&quot;688&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lTZ6e/btsqSG1Y8al/KiWKULtgXyplywQE8x18pK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lTZ6e/btsqSG1Y8al/KiWKULtgXyplywQE8x18pK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lTZ6e/btsqSG1Y8al/KiWKULtgXyplywQE8x18pK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlTZ6e%2FbtsqSG1Y8al%2FKiWKULtgXyplywQE8x18pK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1658&quot; height=&quot;688&quot; data-filename=&quot;스크린샷 2023-08-10 오후 6.25.02.png&quot; data-origin-width=&quot;1658&quot; data-origin-height=&quot;688&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;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;span style=&quot;color: #ef5369;&quot;&gt;토스피드와 토스 유튜브를 &lt;span style=&quot;color: #333333;&quot;&gt;봤다&lt;/span&gt;&lt;/span&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;면접은 테크 리더님과 1:1로 1시간 정도 진행하였다.&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; (만약 토스 문화를 보면서 본인과 잘 안 맞을 것 같다는 생각이 든다면 본인이 정말 토스에 가고 싶은지 고민하기!) 고민을 많이 해보면 좋을 것 같다.&amp;nbsp;&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;style3&quot; /&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&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;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;&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;8월 한 달 동안 푹 쉬고 우테코도 잘 마무리해야겠다. (우테코를 끝까지 못한 게 정말 너무 아쉽다  ... 내 테코톡...)&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;360&quot; data-origin-height=&quot;360&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cM3HYp/btsqKDe6Sh9/1jzrPkjYkR53dSPtnt4M2k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cM3HYp/btsqKDe6Sh9/1jzrPkjYkR53dSPtnt4M2k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cM3HYp/btsqKDe6Sh9/1jzrPkjYkR53dSPtnt4M2k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcM3HYp%2FbtsqKDe6Sh9%2F1jzrPkjYkR53dSPtnt4M2k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;296&quot; height=&quot;296&quot; data-origin-width=&quot;360&quot; data-origin-height=&quot;360&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;축하해준 모든 분들에게 감사 인사 돌립니다  &amp;zwj;♀️&lt;/p&gt;</description>
      <category>2023 NEXT</category>
      <category>토스</category>
      <author>dolmeng2</author>
      <guid isPermaLink="true">https://cl8d.tistory.com/121</guid>
      <comments>https://cl8d.tistory.com/121#entry121comment</comments>
      <pubDate>Thu, 10 Aug 2023 18:26:16 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] @SpringBootTest에서 지연 로딩 사용하기 - no Session 방지하기</title>
      <link>https://cl8d.tistory.com/120</link>
      <description>&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;우리 팀의 경우 E2E 테스트 환경을 구축하기 위해서 @SpringBootTest를 통해 테스트 코드를 작성하고 있는데, 우리 팀의 팀원분이 아래와 같은 오류를 만나게 되었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-07-29 오후 9.13.53.png&quot; data-origin-width=&quot;1956&quot; data-origin-height=&quot;258&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cA6ks4/btspkJOdkiU/I3rgvDEO4CGt2K4bar0Ih0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cA6ks4/btspkJOdkiU/I3rgvDEO4CGt2K4bar0Ih0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cA6ks4/btspkJOdkiU/I3rgvDEO4CGt2K4bar0Ih0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcA6ks4%2FbtspkJOdkiU%2FI3rgvDEO4CGt2K4bar0Ih0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1956&quot; height=&quot;258&quot; data-filename=&quot;스크린샷 2023-07-29 오후 9.13.53.png&quot; data-origin-width=&quot;1956&quot; data-origin-height=&quot;258&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;failed to lazily initialize a collection of role: co.kirikiri.domain.roadmap.RoadmapContent.nodes.values: could not initialize proxy - no Session&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상황은 아래와 같다. (추후 코드로 더 잘 살펴볼 예정이다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A라는 생성 API와 B라는 조회 API가 있을 때, 팀 내에서 기능을 세분화한 다음 각자 개발을 진행하다 보니 B를 개발하는 시점에 A라는 API가 없어, 통합 테스트 때 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;하지만, save 후 반환된 엔티티의 객체를 조회할 때 &lt;span style=&quot;color: #ef5369;&quot;&gt;지연 로딩을 사용하다 보니 트랜잭션이 필요&lt;/span&gt;하게 되었는데, 테스트 메서드에서는 &lt;span style=&quot;color: #ef5369;&quot;&gt;repository를 호출하는 시점에서만 트랜잭션이 걸리고&lt;/span&gt;&lt;b&gt;, 해당 객체를 사용하는 시점에는 트랜잭션이 없어 영속성 컨텍스트가 없는 상태가 된 것이다.&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;당시 이 오류를 처음 접했을 때는 테스트 메서드의 트랜잭션이 없어서 발생한 문제니까, &lt;b&gt;A api가 merge 된 이후에 작업하셔도 충분&lt;/b&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;
&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;style3&quot; /&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-07-29 오후 9.45.01.png&quot; data-origin-width=&quot;2066&quot; data-origin-height=&quot;844&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mkoTi/btspnjtYaRg/nz8H0bpMpCwGBUCcwTuLT1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mkoTi/btspnjtYaRg/nz8H0bpMpCwGBUCcwTuLT1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mkoTi/btspnjtYaRg/nz8H0bpMpCwGBUCcwTuLT1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmkoTi%2FbtspnjtYaRg%2Fnz8H0bpMpCwGBUCcwTuLT1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2066&quot; height=&quot;844&quot; data-filename=&quot;스크린샷 2023-07-29 오후 9.45.01.png&quot; data-origin-width=&quot;2066&quot; data-origin-height=&quot;844&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;로드맵 (Roadmap)은 로드맵 내용들(RoadmapContents)에 대해서 1:N 관계를 맺고 있으며, 각 로드맵 내용(RoadmapContent)은 로드맵 노드들(RoadmapNodes)에 대해서 또 다시 1:N 관계를 가지고 있다.&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;pre id=&quot;code_1690634235536&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class Roadmap extends BaseEntity {

    ...
    
    @Embedded
    private RoadmapContents contents = new RoadmapContents();
    
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1690633719939&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class RoadmapContent extends BaseUpdatedTimeEntity {

    ...

    @Embedded
    private final RoadmapNodes nodes = new RoadmapNodes();

   ...

    public RoadmapNodes getNodes() {
        return nodes;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1690633750790&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Embeddable
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class RoadmapNodes {

    @OneToMany(fetch = FetchType.LAZY, cascade = {CascadeType.PERSIST, CascadeType.MERGE}, mappedBy = &quot;roadmapContent&quot;)
    private final List&amp;lt;RoadmapNode&amp;gt; values = new ArrayList&amp;lt;&amp;gt;();

    ...
    
    public List&amp;lt;RoadmapNode&amp;gt; getValues() {
        return new ArrayList&amp;lt;&amp;gt;(values);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 봐야하는 점은, 로드맵(Roadmap) - 로드맵 본문(RoadmapContent)과 로드맵 본문(RoadmapContent)과 로드맵 노드(RoadmapNode) 모두가 &lt;span style=&quot;color: #ef5369;&quot;&gt;LAZY 전략을 사용&lt;/span&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;style3&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&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;@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)&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;pre id=&quot;code_1690635114588&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
void 테스트() throws JsonProcessingException {
    ...

    final Long 로드맵_아이디 = 로드맵을_생성한다(액세스_토큰, 카테고리.getId(), &quot;로드맵 제목&quot;, &quot;로드맵 소개글&quot;, &quot;로드맵 본문&quot;,
            RoadmapDifficultyType.DIFFICULT, 30, List.of(노드1, 노드2));

    final RoadmapContent 로드맵_본문 = 로드맵으로부터_본문을_가져온다(로드맵_아이디);
    final List&amp;lt;RoadmapNode&amp;gt; 로드맵_노드들 = 로드맵_본문.getNodes().getValues(); // Here!
    ...
}

private RoadmapContent 로드맵으로부터_본문을_가져온다(final Long 로드맵_아이디) {
    final Roadmap 로드맵 = roadmapRepository.findById(로드맵_아이디).get();
    return roadmapContentRepository.findFirstByRoadmapOrderByCreatedAtDesc(로드맵).get();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드의 첫 라인을 보면 &lt;b&gt;로드맵을_생성한다()&lt;/b&gt;; 메서드를 통해 로드맵 아이디를 반환받고 있다. 여기서는 실제 API call을 진행하고 있기 때문에 실제로 생성된 로드맵의 아이디만을 반환받는다. 반환받은 아이디를 바탕으로 로드맵 엔티티를 얻어오기 위해 &lt;span style=&quot;color: #ef5369;&quot;&gt;findById()를 통해 로드맵 엔티티를 조회&lt;/span&gt;해오며, 여기서 가장 최근에 생성된 로드맵 본문 엔티티를 가져오기 위해 &lt;span style=&quot;color: #ef5369;&quot;&gt;한 번 더 조회를 진행&lt;/span&gt;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;  물론, 로드맵 엔티티로부터 가장 최근의 로드맵 본문을 받아오는 방법도 있겠지만, &lt;b&gt;현 프로덕션 코드에서는 해당 부분이 필요하지 않았고&lt;/b&gt;, 만약 그렇게 코드를 작성했다면 &lt;b&gt;본문을 가져오는 과정에서부터&lt;/b&gt; 오류가 발생했을 것이다. &lt;br /&gt;&lt;br /&gt;또한, 현재 작성한 부분은&lt;span style=&quot;color: #ef5369;&quot;&gt; 생성 API가 아직 만들어지지 않은 상태로 조회 API를 만들다 보니&lt;/span&gt; 생성을 위한 엔티티를 만드려고 '로드맵 노드들' 엔티티를 조회해온 코드여서 추후 생성 API가 만들어진다면 제거될 부분이긴 하다!&lt;/blockquote&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;style1&quot; /&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;  cf. findFirst~() 메서드에서는 트랜잭션이 어딨을까?&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1690636754412&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface RoadmapContentRepository extends JpaRepository&amp;lt;RoadmapContent, Long&amp;gt; {

    Optional&amp;lt;RoadmapContent&amp;gt; findFirstByRoadmapOrderByCreatedAtDesc(final Roadmap roadmap);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;roadmapContentRepository의 경우 위와 같이 JpaRepository를 상속받고 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-07-29 오후 10.18.35.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;158&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d6HRFH/btsplMcpouD/iHPrYvkqparq57Kiin6Lek/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d6HRFH/btsplMcpouD/iHPrYvkqparq57Kiin6Lek/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d6HRFH/btsplMcpouD/iHPrYvkqparq57Kiin6Lek/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd6HRFH%2FbtsplMcpouD%2FiHPrYvkqparq57Kiin6Lek%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1440&quot; height=&quot;158&quot; data-filename=&quot;스크린샷 2023-07-29 오후 10.18.35.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;158&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때, JpaRepository 인터페이스의 구현체인 SimpleJpaRepository 클래스를 가면 위와 같이 @Transactional이 붙어 있는 것을 볼 수 있다. 그렇기 때문에 해당 메서드가 호출하는 시점에 대해서만 트랜잭션이 걸려있게 되는 것이다.&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: #ef5369;&quot;&gt;영속성 컨텍스트는 트랜잭션과 생명주기가 동일&lt;/span&gt;하기 때문에, roadmapContentRepository로부터 조회해온 RoadmapContent는 트랜잭션이 종료되면서 영속성 컨텍스트의 관리 범위에서도 함께 벗어나게 된다. 현재 로드맵 본문 엔티티는 준영속 상태가 되었음을 기억하자.&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;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1690636829187&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;final List&amp;lt;RoadmapNode&amp;gt; 로드맵_노드들 = 로드맵_본문.getNodes().getValues(); // Here!&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 돌아와서, here! 이라고 특정된 부분을 보자. 해당 부분이 오류가 발생하는 지점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로드맵 본문으로부터 노드에 대한 정보를 가져오려고 할 때 오류가 발생한 것이다.&lt;/p&gt;
&lt;pre id=&quot;code_1690636342810&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;RoadmapNode&amp;gt; getValues() {
    return new ArrayList&amp;lt;&amp;gt;(values);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;노드에서 getValues() 메서드를 호출하면 values()에 대한 접근이 일어나게 되는데, 이때 아래와 같은 일들이 발생한다.&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;스크린샷 2023-07-29 오후 10.14.30.png&quot; data-origin-width=&quot;1682&quot; data-origin-height=&quot;326&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/A2WzT/btspk96Mgt7/C8Q43Bxfwo27k4TX0eJCPK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/A2WzT/btspk96Mgt7/C8Q43Bxfwo27k4TX0eJCPK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/A2WzT/btspk96Mgt7/C8Q43Bxfwo27k4TX0eJCPK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FA2WzT%2Fbtspk96Mgt7%2FC8Q43Bxfwo27k4TX0eJCPK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1682&quot; height=&quot;326&quot; data-filename=&quot;스크린샷 2023-07-29 오후 10.14.30.png&quot; data-origin-width=&quot;1682&quot; data-origin-height=&quot;326&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;read() 라는 메서드를 따라 들어가다 보면, 어떠한 초기화 작업이 처음에 발생하는 것을 볼 수 있다.&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;스크린샷 2023-07-29 오후 10.15.03.png&quot; data-origin-width=&quot;1654&quot; data-origin-height=&quot;694&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dnklwV/btspgoqkIbg/uAP89kWkQdi7Xjb4a36Q2K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dnklwV/btspgoqkIbg/uAP89kWkQdi7Xjb4a36Q2K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dnklwV/btspgoqkIbg/uAP89kWkQdi7Xjb4a36Q2K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdnklwV%2FbtspgoqkIbg%2FuAP89kWkQdi7Xjb4a36Q2K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1654&quot; height=&quot;694&quot; data-filename=&quot;스크린샷 2023-07-29 오후 10.15.03.png&quot; data-origin-width=&quot;1654&quot; data-origin-height=&quot;694&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;상위의 주석을 잘 읽어보면, &lt;b&gt;초기화가 불가능할 때 LazyInitializationException이 발생함&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;스크린샷 2023-07-29 오후 10.09.00.png&quot; data-origin-width=&quot;1704&quot; data-origin-height=&quot;496&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tNNNj/btspgoRqDWA/BkgttkKqmKOsqr3MC7aRG1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tNNNj/btspgoRqDWA/BkgttkKqmKOsqr3MC7aRG1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tNNNj/btspgoRqDWA/BkgttkKqmKOsqr3MC7aRG1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtNNNj%2FbtspgoRqDWA%2FBkgttkKqmKOsqr3MC7aRG1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1704&quot; height=&quot;496&quot; data-filename=&quot;스크린샷 2023-07-29 오후 10.09.00.png&quot; data-origin-width=&quot;1704&quot; data-origin-height=&quot;496&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고, 해당 메서드를 타고 들어가면 session 정보가 없을 때 &lt;span style=&quot;color: #ef5369;&quot;&gt;외부의 트랜잭션을 사용할 수 있는지 판단하는데, 사용 불가능하기 때문에&lt;/span&gt; (allowLoadOutsideTransaction=false) 하단의 else 구문으로 제어가 내려가게 되어 오류가 발생하게 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-07-29 오후 10.08.47.png&quot; data-origin-width=&quot;2218&quot; data-origin-height=&quot;434&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cmRNIA/btspgo4XNTC/IDvDlHtT1uQOzCJrPs40E0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cmRNIA/btspgo4XNTC/IDvDlHtT1uQOzCJrPs40E0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cmRNIA/btspgo4XNTC/IDvDlHtT1uQOzCJrPs40E0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcmRNIA%2Fbtspgo4XNTC%2FIDvDlHtT1uQOzCJrPs40E0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2218&quot; height=&quot;434&quot; data-filename=&quot;스크린샷 2023-07-29 오후 10.08.47.png&quot; data-origin-width=&quot;2218&quot; data-origin-height=&quot;434&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로, allowLoadOutsideTransaction의 경우 제일 처음 세션 정보를 세팅해줄 때 위의 메서드에서 적용된다.&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;한 번 천천히 해결해나가보자.&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;style3&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  그냥 @Transactional을 붙이면 되지 않나?&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1690637003016&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
@Transactional // 트랜잭션!
void 테스트() throws JsonProcessingException {
    ...
	
    final RoadmapCategory 카테고리 = 로드맵_카테고리를_저장한다(&quot;여행&quot;);
    final Long 로드맵_아이디 = 로드맵을_생성한다(액세스_토큰, 카테고리.getId(), &quot;로드맵 제목&quot;, &quot;로드맵 소개글&quot;, &quot;로드맵 본문&quot;,
            RoadmapDifficultyType.DIFFICULT, 30, List.of(노드1, 노드2));

    final RoadmapContent 로드맵_본문 = 로드맵으로부터_본문을_가져온다(로드맵_아이디);
    final List&amp;lt;RoadmapNode&amp;gt; 로드맵_노드들 = 로드맵_본문.getNodes().getValues(); 
    ...
}

 private RoadmapCategory 로드맵_카테고리를_저장한다(final String 카테고리_이름) {
    final RoadmapCategory 로드맵_카테고리 = new RoadmapCategory(카테고리_이름);
    return roadmapCategoryRepository.save(로드맵_카테고리);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션이 없어서 생긴 문제라면,&lt;b&gt; @Transactional을 붙이면 된다&lt;/b&gt;고 생각할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 아까 예제와 다르게 로드맵에 대한 카테고리 생성 메서드가 추가된 것을 볼 수 있는데, 오류가 발생한 포인트이기 때문에 넣어두었다. 카테고리의 경우 생성하는 API가 없기 때문에 (나중에 admin 기능을 추가하면 넣을 예정이었다.) 위와 같이 직접 save를 해준다.&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;스크린샷 2023-07-29 오후 10.24.34.png&quot; data-origin-width=&quot;1940&quot; data-origin-height=&quot;286&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bACKop/btspmgEiKOF/PF5erSYk8qAD3RB6oG6VB1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bACKop/btspmgEiKOF/PF5erSYk8qAD3RB6oG6VB1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bACKop/btspmgEiKOF/PF5erSYk8qAD3RB6oG6VB1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbACKop%2FbtspmgEiKOF%2FPF5erSYk8qAD3RB6oG6VB1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1940&quot; height=&quot;286&quot; data-filename=&quot;스크린샷 2023-07-29 오후 10.24.34.png&quot; data-origin-width=&quot;1940&quot; data-origin-height=&quot;286&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, 정말 뜬금없이 NPE가 발생하는 것을 볼 수 있다. 이는, 응답값이 제대로 반환되지 않으면서 response header 값이 제대로 내려오지 않았기 때문이다.&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;스크린샷 2023-07-29 오후 10.31.30.png&quot; data-origin-width=&quot;1964&quot; data-origin-height=&quot;322&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zNL2d/btspkK7sggR/eLmliY8b8qq6sdOecQa7Fk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zNL2d/btspkK7sggR/eLmliY8b8qq6sdOecQa7Fk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zNL2d/btspkK7sggR/eLmliY8b8qq6sdOecQa7Fk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzNL2d%2FbtspkK7sggR%2FeLmliY8b8qq6sdOecQa7Fk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1964&quot; height=&quot;322&quot; data-filename=&quot;스크린샷 2023-07-29 오후 10.31.30.png&quot; data-origin-width=&quot;1964&quot; data-origin-height=&quot;322&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 해당 로그의 상단으로 올라가보면, 위와 같이 카테고리에 대한 Exception이 발생한 것을 볼 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-07-29 오후 10.37.32.png&quot; data-origin-width=&quot;2650&quot; data-origin-height=&quot;712&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Or5Q1/btsppHVGK2E/tes7dAqjTsQhd4N3huo9A1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Or5Q1/btsppHVGK2E/tes7dAqjTsQhd4N3huo9A1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Or5Q1/btsppHVGK2E/tes7dAqjTsQhd4N3huo9A1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOr5Q1%2FbtsppHVGK2E%2Ftes7dAqjTsQhd4N3huo9A1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2650&quot; height=&quot;712&quot; data-filename=&quot;스크린샷 2023-07-29 오후 10.37.32.png&quot; data-origin-width=&quot;2650&quot; data-origin-height=&quot;712&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는, 로드맵 생성 API가 호출되면서 create 하는 과정에 request body 값으로 받은 &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;이는, @SpringBootTest(webEnvironment = &lt;span style=&quot;color: #ef5369;&quot;&gt;SpringBootTest.WebEnvironment.RANDOM_PORT&lt;/span&gt;) 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전에 테스트 컨텍스트에 대한 &lt;a title=&quot;게시글&quot; href=&quot;https://cl8d.tistory.com/82&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;게시글&lt;/a&gt;을 작성한 적이 있는데, 그때 random_port 옵션에 대해 이와 같이 커멘트를 남겼다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-07-29 오후 10.40.43.png&quot; data-origin-width=&quot;1686&quot; data-origin-height=&quot;266&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5c0tV/btsplbjc1A9/tAVW9DUES6DvwvJBcmTnN1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5c0tV/btsplbjc1A9/tAVW9DUES6DvwvJBcmTnN1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5c0tV/btsplbjc1A9/tAVW9DUES6DvwvJBcmTnN1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5c0tV%2Fbtsplbjc1A9%2FtAVW9DUES6DvwvJBcmTnN1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1686&quot; height=&quot;266&quot; data-filename=&quot;스크린샷 2023-07-29 오후 10.40.43.png&quot; data-origin-width=&quot;1686&quot; data-origin-height=&quot;266&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;random_port 옵션을 지정하게 되면&lt;span style=&quot;color: #ef5369;&quot;&gt;&amp;nbsp;별개의 스레드에서 컨테이너가 실행&lt;/span&gt;된다는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;= 즉, 프로덕션 코드에서 작성한 메서드의 스레드와 @SpringBootTest의 메서드의 스레드가 다르다는 의미이다.&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;기본적으로 @Transactional이 붙게 되면 작업 스레드는 커넥션 풀에서 Connection 객체를 가져와서 사용하게 되는데, &lt;b&gt;스레드가 달라지게 되면 사용하게 되는 Connection이 달라지게 된다&lt;/b&gt;. 즉, &lt;b&gt;트랜잭션이 아예 달라지게 되는 것&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇기 때문에 프로덕션 코드가 실행되는 &lt;b&gt;로드맵 생성 API&lt;/b&gt;의 스레드 입장으로서는 테스트 메서드인 &lt;b&gt;로드맵_카테고리를_생성한다()&amp;nbsp;&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;스크린샷 2023-07-29 오후 11.54.53.png&quot; data-origin-width=&quot;1472&quot; data-origin-height=&quot;430&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GcEoa/btspjV16lin/FNqrHgOyFX5j8P6M9ADboK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GcEoa/btspjV16lin/FNqrHgOyFX5j8P6M9ADboK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GcEoa/btspjV16lin/FNqrHgOyFX5j8P6M9ADboK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGcEoa%2FbtspjV16lin%2FFNqrHgOyFX5j8P6M9ADboK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;678&quot; height=&quot;198&quot; data-filename=&quot;스크린샷 2023-07-29 오후 11.54.53.png&quot; data-origin-width=&quot;1472&quot; data-origin-height=&quot;430&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 한 가지 삽질을 했는데, 콘솔상으로는 쿼리가 발생하길래 계속 insert가 된다고 생각했었다. (IDENTITY 전략으로 인해)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-07-29 오후 11.55.12.png&quot; data-origin-width=&quot;472&quot; data-origin-height=&quot;198&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tTKB2/btspkJ8yvWe/uMGgYwcYXkeWihY6SKCDT0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tTKB2/btspkJ8yvWe/uMGgYwcYXkeWihY6SKCDT0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tTKB2/btspkJ8yvWe/uMGgYwcYXkeWihY6SKCDT0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtTKB2%2FbtspkJ8yvWe%2FuMGgYwcYXkeWihY6SKCDT0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;393&quot; height=&quot;165&quot; data-filename=&quot;스크린샷 2023-07-29 오후 11.55.12.png&quot; data-origin-width=&quot;472&quot; data-origin-height=&quot;198&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, 실제로 DB에 가서 확인해보니 결과가 저장되지 않았었고, 아마 쿼리만 발생한 것 같다고 추측된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(트랜잭션 시작 후 커밋이 되지 않은 상태라고 보는 게 더 정확할 것 같다.)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-07-30 오전 12.10.59.png&quot; data-origin-width=&quot;454&quot; data-origin-height=&quot;116&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bJI7ep/btspfPnKHyV/QIVbV4HHWzWGuv0m021eI1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bJI7ep/btspfPnKHyV/QIVbV4HHWzWGuv0m021eI1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bJI7ep/btspfPnKHyV/QIVbV4HHWzWGuv0m021eI1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbJI7ep%2FbtspfPnKHyV%2FQIVbV4HHWzWGuv0m021eI1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;454&quot; height=&quot;116&quot; data-filename=&quot;스크린샷 2023-07-30 오전 12.10.59.png&quot; data-origin-width=&quot;454&quot; data-origin-height=&quot;116&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Transactional을 제거하면 위와 같이 레코드가 저장된다. (트랜잭션이 없으니 바로 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;p data-ke-size=&quot;size16&quot;&gt;그러면 이 문제를 어떻게 해결해야 할까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RANDOM_PORT를 사용하지 않게 되면 E2E 테스트의 의미가 없고, API call로 대체할 수 없는 상황이다.&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;style3&quot; /&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;p data-ke-size=&quot;size16&quot;&gt;그렇다면 조회하는 시점에 새로운 트랜잭션을 생성해주는 건은 어떨까?&lt;/p&gt;
&lt;pre id=&quot;code_1690645140625&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
void 테스트() throws JsonProcessingException {
    ...
	
    final RoadmapCategory 카테고리 = 로드맵_카테고리를_저장한다(&quot;여행&quot;);
    final List&amp;lt;RoadmapNode&amp;gt; 로드맵_노드들 = 로드맵_노드들을_반환한다(액세스_토큰, 카테고리, 노드1, 노드2);
    
    ...
}


@Transactional // 트랜잭션!
public List&amp;lt;RoadmapNode&amp;gt; 로드맵_노드들을_반환한다(final String 액세스_토큰, final RoadmapCategory 카테고리, final RoadmapNodeSaveRequest 노드1,
                                        final RoadmapNodeSaveRequest 노드2) {
    final Long 로드맵_아이디 = 로드맵을_생성한다(액세스_토큰, 카테고리.getId(), &quot;로드맵 제목&quot;, &quot;로드맵 소개글&quot;, &quot;로드맵 본문&quot;,
            RoadmapDifficultyType.DIFFICULT, 30, List.of(노드1, 노드2));

    final RoadmapContent 로드맵_본문 = 로드맵으로부터_본문을_가져온다(로드맵_아이디);
    return 로드맵_본문.getNodes().getValues();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, 위 코드는 동작하지 않는다. 이는 기본적으로 @Transactional은 Spring 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;이때 크게 2가지의 특징이 존재한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 타겟 클래스를 상속하여 프록시 객체를 생성하기 때문에 상속 자체가 불가능한 &lt;span style=&quot;color: #ef5369;&quot;&gt;private 메서드에 대해서는 적용 불가&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 동일한 클래스의 &lt;span style=&quot;color: #ef5369;&quot;&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;위 코드에서는 2번의 경우, 내부 메서드를 사용했기 때문에 트랜잭션을 적용할 수 없는 것이다.&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&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;pre id=&quot;code_1690692845992&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
@TestConstructor(autowireMode = AutowireMode.ALL)
public class RoadmapTestHelper {

    private final RoadmapRepository roadmapRepository;
    private final RoadmapContentRepository roadmapContentRepository;

    public RoadmapTestHelper(final RoadmapRepository roadmapRepository,
                             final RoadmapContentRepository roadmapContentRepository) {
        this.roadmapRepository = roadmapRepository;
        this.roadmapContentRepository = roadmapContentRepository;
    }

    @Transactional(readOnly = true)
    public List&amp;lt;RoadmapNode&amp;gt; 로드맵_노드들을_조회한다(final Long 로드맵_아이디) {
        final RoadmapContent 로드맵_본문 = 로드맵으로부터_본문을_가져온다(로드맵_아이디);
        return 로드맵_본문.getNodes().getValues();
    }

    private RoadmapContent 로드맵으로부터_본문을_가져온다(final Long 로드맵_아이디) {
        final Roadmap 로드맵 = roadmapRepository.findById(로드맵_아이디).get();
        return roadmapContentRepository.findFirstByRoadmapOrderByCreatedAtDesc(로드맵).get();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;로드맵 노드를 조회하기 위한 별도의 Helper 클래스를 생성&lt;/b&gt;해준다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로드맵_본문 엔티티가 detached 상태가 되지 않도록 조회를 해오는 시점의 메서드부터 helper 클래스에 두었으며, 본문으로부터 노드 정보를 지연로딩을 통해 가져와서 반환하도록 만들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1690692959099&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
void 테스트() throws JsonProcessingException {
    ...
	
    final RoadmapCategory 카테고리 = 로드맵_카테고리를_저장한다(&quot;여행&quot;);
    final Long 로드맵_아이디 = 로드맵을_생성한다(액세스_토큰, 카테고리.getId(), &quot;로드맵 제목&quot;, &quot;로드맵 소개글&quot;, &quot;로드맵 본문&quot;,
            RoadmapDifficultyType.DIFFICULT, 30, List.of(노드1, 노드2));
    
    // Here!
    final List&amp;lt;RoadmapNode&amp;gt; 로드맵_노드들 = roadmapTestHelper.로드맵_노드들을_조회한다(로드맵_아이디);
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 기존의 테스트 코드에서 helper 클래스를 통해서 노드를 조회해왔다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-07-30 오후 1.57.03.png&quot; data-origin-width=&quot;1968&quot; data-origin-height=&quot;692&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/x2SBL/btspmVGRfYE/Gk12XGtGGDvp4mYvXHxe7k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/x2SBL/btspmVGRfYE/Gk12XGtGGDvp4mYvXHxe7k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/x2SBL/btspmVGRfYE/Gk12XGtGGDvp4mYvXHxe7k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fx2SBL%2FbtspmVGRfYE%2FGk12XGtGGDvp4mYvXHxe7k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1968&quot; height=&quot;692&quot; data-filename=&quot;스크린샷 2023-07-30 오후 1.57.03.png&quot; data-origin-width=&quot;1968&quot; data-origin-height=&quot;692&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;226&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zf3z6/btspfPg6Xr0/2yXJoapZxRr17g0Y9nC3Ik/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zf3z6/btspfPg6Xr0/2yXJoapZxRr17g0Y9nC3Ik/img.gif&quot; data-alt=&quot;와 성공~!&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zf3z6/btspfPg6Xr0/2yXJoapZxRr17g0Y9nC3Ik/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/zf3z6/btspfPg6Xr0/2yXJoapZxRr17g0Y9nC3Ik/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;226&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;226&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;와 성공~!&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, 위 방법은 매우 찝찝하다. &lt;b&gt;지연로딩이 필요한 코드마다 이렇게 테스트 클래스로 분리&lt;/b&gt;해야 하기 때문이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 테스트하는 케이스가 정말 많아진다면 그럴 때마다 helper 클래스에 메스드가 엄청나게 늘어나게 될 것이다.&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&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;필요할 때마다 helper 클래스의 메서드로 분리하는 게 아니라, 트랜잭션이 필요한 로직에 대해서만 외부 클래스에서 실행되도록 만들 수는 없을까? 즉, &lt;span style=&quot;color: #ef5369;&quot;&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;람다식을 활용&lt;/b&gt;할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 인자로 넘긴 값으로 List&amp;lt;RoadmapNode&amp;gt;를 받아야 하기 때문에 T를 반환하는 시그니처를 가진 함수형 인터페이스를 하나 설정할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1690693418784&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@FunctionalInterface
public interface TransactionalTask&amp;lt;T&amp;gt; {
    T execute();
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;  함수형 인터페이스란?&lt;/b&gt;&lt;br /&gt;오직 1개의 추상 메서드를 가지는 인터페이스.&amp;nbsp;&lt;br /&gt;여기서 추상 메서드란, 자식 클래스에서 반드시 오버라이딩 해야 사용할 수 있는 메서드이다.&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;pre id=&quot;code_1690693543844&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
public class TransactionHelper {

    @Transactional(readOnly = true)
    public &amp;lt;T&amp;gt; T getResult(final TransactionalTask&amp;lt;T&amp;gt; task) {
        return task.execute();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;헬퍼 클래스에서는 TransactionTask 타입의 어떠한 람다식을 받아서, &lt;span style=&quot;color: #ef5369;&quot;&gt;해당 람다식을 @Transactional이 걸린 상태로 실행해주는 역할&lt;/span&gt;을 진행한다. 이제 이 헬퍼 클래스를 활용하게 되면 아래와 같이 테스트 코드를 작성할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1690693820099&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
void 테스트() throws JsonProcessingException {
    ...
	
    final RoadmapCategory 카테고리 = 로드맵_카테고리를_저장한다(&quot;여행&quot;);
    final Long 로드맵_아이디 = 로드맵을_생성한다(액세스_토큰, 카테고리.getId(), &quot;로드맵 제목&quot;, &quot;로드맵 소개글&quot;, &quot;로드맵 본문&quot;,
            RoadmapDifficultyType.DIFFICULT, 30, List.of(노드1, 노드2));
            
    final List&amp;lt;RoadmapNode&amp;gt; 로드맵_노드들 = transactionHelper.getResult(new TransactionalTask&amp;lt;List&amp;lt;RoadmapNode&amp;gt;&amp;gt;() {
            @Override
            public List&amp;lt;RoadmapNode&amp;gt; execute() {
                final RoadmapContent 로드맵_본문 = 로드맵으로부터_본문을_가져온다(로드맵_아이디);
                return 로드맵_본문.getNodes().getValues();
            }
        });
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;transactionHelper 클래스의 getResult() 메서드의 인자로 &lt;b&gt;함수형 인터페이스의 추상 메서드인 execute()의 익명 클래스가&lt;/b&gt;&amp;nbsp;들어간다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고, 해당 구현체에 로드맵 본문을 조회하여 로드맵 노드를 반환하는 로직을 넣었다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;  익명 클래스&lt;/b&gt;&lt;br /&gt;클래스의 선언과 인스턴스화를 동시에 진행할 수 있는 클래스로, 이름이 없는 클래스라 익명 클래스라고 부른다.&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;pre id=&quot;code_1690694272791&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
void 테스트() throws JsonProcessingException {
    ...
	
    final RoadmapCategory 카테고리 = 로드맵_카테고리를_저장한다(&quot;여행&quot;);
    final Long 로드맵_아이디 = 로드맵을_생성한다(액세스_토큰, 카테고리.getId(), &quot;로드맵 제목&quot;, &quot;로드맵 소개글&quot;, &quot;로드맵 본문&quot;,
            RoadmapDifficultyType.DIFFICULT, 30, List.of(노드1, 노드2));
            
    final List&amp;lt;RoadmapNode&amp;gt; 로드맵_노드들 = transactionHelper.getResult(() -&amp;gt; {
    	final RoadmapContent 로드맵_본문 = 로드맵으로부터_본문을_가져온다(로드맵_아이디);
    	return 로드맵_본문.getNodes().getValues();
    });
    
    ...
}&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-07-30 오후 2.19.15.png&quot; data-origin-width=&quot;1942&quot; data-origin-height=&quot;684&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cudi8i/btsppIAszU8/3GGKmIomna8wnaCa7wi9i1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cudi8i/btsppIAszU8/3GGKmIomna8wnaCa7wi9i1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cudi8i/btsppIAszU8/3GGKmIomna8wnaCa7wi9i1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcudi8i%2FbtsppIAszU8%2F3GGKmIomna8wnaCa7wi9i1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1942&quot; height=&quot;684&quot; data-filename=&quot;스크린샷 2023-07-30 오후 2.19.15.png&quot; data-origin-width=&quot;1942&quot; data-origin-height=&quot;684&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;이제 테스트 코드에서 지연로딩이 필요한 부분에 대해 (then절 같이 응답값을 꺼내어 검증할 때) 이를 잘 사용하면 된다.&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;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;style3&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;+ 추가&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;갓말랑 선생님께서 더 좋은 방법을 제시해주셔서 공유!&lt;/p&gt;
&lt;pre id=&quot;code_1693538480510&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Autowird
private TransactionTemplate transactionTemplate;

@Test
void 테스트() throws JsonProcessingException {
    ...
	
    final RoadmapCategory 카테고리 = 로드맵_카테고리를_저장한다(&quot;여행&quot;);
    final Long 로드맵_아이디 = 로드맵을_생성한다(액세스_토큰, 카테고리.getId(), &quot;로드맵 제목&quot;, &quot;로드맵 소개글&quot;, &quot;로드맵 본문&quot;,
            RoadmapDifficultyType.DIFFICULT, 30, List.of(노드1, 노드2));
    
    final List&amp;lt;RoadmapNode&amp;gt; 로드맵_노드들 = transactionTemplate.execute(new TransactionCallback&amp;lt;List&amp;lt;RoadmapNode&amp;gt;&amp;gt;() {
            @Override
            public List&amp;lt;RoadmapNode&amp;gt; doInTransaction(final TransactionStatus status) {
                final RoadmapContent 로드맵_본문 = 로드맵으로부터_본문을_가져온다(로드맵_아이디);
                return 로드맵_본문.getNodes().getValues();
            }
        });
    
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링에서는 이미 &lt;span style=&quot;color: #ef5369;&quot;&gt;TransactionTemplate&lt;/span&gt; 이라는 인터페이스를 통해서 제공을 해주고 있었다... ㅎㅎ (그냥 혼자 구현한 사람 됨)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 반환값이 필요한 경우라면 TransactionCallback을 인자로 받으면 된다.&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_1693538620024&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Autowird
private TransactionTemplate transactionTemplate;

@Test
void 테스트() throws JsonProcessingException {
    ...
	
    final RoadmapCategory 카테고리 = 로드맵_카테고리를_저장한다(&quot;여행&quot;);
    final Long 로드맵_아이디 = 로드맵을_생성한다(액세스_토큰, 카테고리.getId(), &quot;로드맵 제목&quot;, &quot;로드맵 소개글&quot;, &quot;로드맵 본문&quot;,
            RoadmapDifficultyType.DIFFICULT, 30, List.of(노드1, 노드2));
    
    final List&amp;lt;RoadmapNode&amp;gt; 로드맵_노드들 = transactionTemplate.execute(status -&amp;gt; {
            final RoadmapContent 로드맵_본문 = 로드맵으로부터_본문을_가져온다(로드맵_아이디);
            return 로드맵_본문.getNodes().getValues();
    });
    
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금 보면 직접 만든 transactionHelper와 완전 동일하다고 봐도 무방하다. 그냥 요렇게 사용하는 게 더 좋을 것 같다.&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: #ef5369;&quot;&gt;TransactionCallbackWithoutResult&lt;/span&gt;를 인자로 받으면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1693538755425&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Autowird
private TransactionTemplate transactionTemplate;

@Test
void 테스트() throws JsonProcessingException {
    ...
	
    final RoadmapCategory 카테고리 = 로드맵_카테고리를_저장한다(&quot;여행&quot;);
    final Long 로드맵_아이디 = 로드맵을_생성한다(액세스_토큰, 카테고리.getId(), &quot;로드맵 제목&quot;, &quot;로드맵 소개글&quot;, &quot;로드맵 본문&quot;,
            RoadmapDifficultyType.DIFFICULT, 30, List.of(노드1, 노드2));
    
    transactionTemplate.execute(new TransactionCallbackWithoutResult() {
            @Override
            protected void doInTransactionWithoutResult(final TransactionStatus status) {
                final RoadmapContent 로드맵_본문 = 로드맵으로부터_본문을_가져온다(로드맵_아이디);
                로드맵_본문.getNodes().getValues();
            }
        });
        
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좋은 방법 공유해준 말랑 선생님 고마워요  &amp;zwj;♀️&lt;/p&gt;</description>
      <category>개발일지</category>
      <category>@SpringBootTest</category>
      <category>JPA</category>
      <category>no Session</category>
      <category>RANDOM_PORT</category>
      <category>지연로딩</category>
      <category>통합테스트</category>
      <author>dolmeng2</author>
      <guid isPermaLink="true">https://cl8d.tistory.com/120</guid>
      <comments>https://cl8d.tistory.com/120#entry120comment</comments>
      <pubDate>Sun, 30 Jul 2023 14:23:01 +0900</pubDate>
    </item>
    <item>
      <title>[Gradle] Jacoco를 활용하여 테스트 커버리지 설정하기</title>
      <link>https://cl8d.tistory.com/119</link>
      <description>&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;우리 팀은 CI 플로우 과정 중에서 Jacoco를 활용하여 테스트 리포트를 발행하고, 커버리지를 체크하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(테스트를 정말 철저하게 하자는 취지인데, 팀원 모두가 테스트를 정말 꼼꼼하게 작성해 주셔서 좋다  )&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CI에 대한 글은 다음에 완전하게 구축된 이후에 작성하는 게 좋을 것 같아서, 오늘은 Jacoco에 대해서만 가볍게 짚고 넘어가고자 한다.&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;style3&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  Jacoco 설정하기&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Jacoco는, &lt;span style=&quot;color: #ef5369;&quot;&gt;테스트&amp;nbsp;코드&amp;nbsp;커버리지를&amp;nbsp;분석해주는&lt;/span&gt;&amp;nbsp;자바의&amp;nbsp;무료&amp;nbsp;라이브러리이다.&lt;br /&gt;Jacoco 플러그인에는 JacocoTestReport와 JacocoTestCoverageVerification Task 등이 존재한다. (여기서는 이 2가지를 위주로 알아보도록 하자.)&lt;br /&gt;- &lt;b&gt;JacocoTestReport&lt;/b&gt;: 커버리지 결과를 리포트로 저장하는 역할&lt;br /&gt;- &lt;b&gt;JacocoTestCoverageVerification&lt;/b&gt;: 원하는 커버리지 기준을 만족하는 확인하는 Task&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;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;  build.gradle 설정해주기 - &lt;b&gt;JacocoTestReport&lt;/b&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1690094829564&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;plugins {
    id 'jacoco'
}

jacoco {
    toolVersion = &quot;0.8.10&quot; // https://www.jacoco.org/jacoco/trunk/doc/changes.html
}

jacocoTestReport {
    dependsOn test
    reports {
        html.required = true
        xml.required = true
    }

    // QueryDSL QDomain 제외시키기
    def QDomains = []

    for (qPattern in '**/QA'..'**/QZ') {
        QDomains.add(qPattern + '*')
    }
    afterEvaluate {
        classDirectories.setFrom(
                // 그 외의 매칭되는 클래스도 제외 대상
                files(classDirectories.files.collect {
                    fileTree(dir: it, excludes: [
                            &quot;co.kirikiri.domain.**.**&quot;,
                            &quot;**/*Application*&quot;,
                            &quot;**/*Config*&quot;,
                            &quot;**/*Dto*&quot;,
                            &quot;**/*Request*&quot;,
                            &quot;**/*Response*&quot;,
                            &quot;**/*Interceptor*&quot;,
                            &quot;**/*Exception*&quot;
                    ] + QDomains)
                })
        )
    }
    // 리포트 생성 후 커버리지 체크
    finalizedBy jacocoTestCoverageVerification
}

jacocoTestCoverageVerification {
    // QueryDSL QDomain 제외시키기
    def QDomains = []
    // qPattern = &quot;*.QA&quot;,&quot;*.QB&quot;,&quot;*.QC&quot;, ... &quot;*.QZ&quot;
    for (qPattern in '*.QA'..'*.QZ') {
        QDomains.add(qPattern + '*')
    }

    violationRules {
        rule {
            // rule 활성화
            enabled = true

            // 클래스 단위로 룰 체크
            element = 'CLASS'

            // 라인 커버리지를 최소 80% 만족
            limit {
                counter = 'LINE'
                value = 'COVEREDRATIO'
                minimum = 0.80
            }
						
            // 마찬가지로 제거 대상 지정
            excludes = [
                    &quot;co.kirikiri.domain.**.**&quot;,
                    &quot;**.*Application*&quot;,
                    &quot;**.*Config*&quot;,
                    &quot;**.*Dto*&quot;,
                    &quot;**.*Request*&quot;,
                    &quot;**.*Response*&quot;,
                    &quot;**.*Interceptor*&quot;,
                    &quot;**.*Exception*&quot;
            ] + QDomains
        }
    }
}

tasks.named('test') {
    outputs.dir snippetsDir
    useJUnitPlatform()
    // test 수행 이후 리포트 생성
    finalizedBy jacocoTestReport
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;build.gradle 코드가 상당히 길기는 한데, 그렇게 어려운 내용은 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1690095008440&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;plugins {
    id 'jacoco'
}

jacoco {
    toolVersion = &quot;0.8.10&quot; // https://www.jacoco.org/jacoco/trunk/doc/changes.html
}&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;먼저, jacoco에 대한 플러그인을 설정해주는 부분이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 스냅샷 버전 제외 가장 최신 버전이 0.8.10이길래 이 버전으로 설치를 해주었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1690095055203&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;jacocoTestReport {
    reports {
        html.required = true
        xml.required = true
    }

    // QueryDSL QDomain 제외시키기
    def QDomains = []

    for (qPattern in '**/QA'..'**/QZ') {
        QDomains.add(qPattern + '*')
    }
    
    ...
}

tasks.named('test') {
    ...
    // test 수행 이후 리포트 생성
    finalizedBy jacocoTestReport
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;jacoco에서 제공하는 테스트 리포트 발행을 위해 사용하는 부분이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;test가 실행된 다음에 리포트를 발행해야 하기 때문에  finalizedBy을 통해서 &lt;b&gt;test 실행 이후 동작하도록 만들었다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;finalizedBy는&lt;span style=&quot;color: #ef5369;&quot;&gt; A task의 성공과 실패에 상관없이&lt;/span&gt; A가 끝나야 B가 실행되기 때문에 주의해서 사용하도록 하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(성공해야 실행되도록 하려면 dependsOn을 사용한다. 테스트가 실패하더라도 리포트를 발행해야 하기 때문에 여기서는 finalizedBy를 활용하였다.)&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;이후, xml과 html에 모두에 대해서 리포트를 발행하도록 하였다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 실제로는 html만 보는 경우가 많아서 꼭 xml까지 하지 않아도 된다고 생각한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1690096169701&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    ...
    
    // QueryDSL QDomain 제외시키기
    def QDomains = []

    for (qPattern in '**/QA'..'**/QZ') {
        QDomains.add(qPattern + '*')
    }
    afterEvaluate {
        classDirectories.setFrom(
								// 그 외의 매칭되는 클래스도 제외 대상
                files(classDirectories.files.collect {
                    fileTree(dir: it, excludes: [
                            &quot;co.kirikiri.domain.**.**&quot;,
                            &quot;**/*Application*&quot;,
                            &quot;**/*Config*&quot;,
                            &quot;**/*Dto*&quot;,
                            &quot;**/*Request*&quot;,
                            &quot;**/*Response*&quot;,
                            &quot;**/*Interceptor*&quot;,
                            &quot;**/*Exception*&quot;
                    ] + QDomains)
                })
        )
    }
    finalizedBy jacocoTestCoverageVerification
    
 }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리팀은 현재 QueryDSL을 사용하고 있기 때문에 QueryDSL로 인해 생성된 &lt;b&gt;QDomain&lt;/b&gt;이라는 것이 존재한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 친구들이 테스트 커버리지에 들어가게 되면 50% 이하로 커버리지가 안 나오는 기이한 현상이 발생하기 때문에&lt;span style=&quot;color: #ef5369;&quot;&gt; qDomain은 패턴 매칭을 통해서 제외&lt;/span&gt;시켜주었다. 또한, Dto나 Config, Exception 같은 클래스는 테스트할 필요가 없다고 생각해서 제외해두었다.&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;b&gt;도메인은 페어 프로그래밍을 통해 작성한 다음,&lt;/b&gt; 사전에 develop branch에 push를 해두고 분기 처리를 진행하고 있기 때문에 각자의 feature에서는 본인이 맡은 기능이 아니면 도메인에 대한 테스트를 작성하지 않게 된다. 그래서 도메인까지는 커버리지 범위에 두지 않았는데, 나중에 도메인 객체를 전부 작성하게 되면 추가할까 생각 중이다.&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;span style=&quot;color: #ef5369;&quot;&gt; build/reports/jacoco/test/html&lt;/span&gt; 하위에 생성된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;492&quot; data-origin-height=&quot;428&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b1UjnN/btsoxqvuSgI/gXnvAEVTF0as4mX3CWD3jk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b1UjnN/btsoxqvuSgI/gXnvAEVTF0as4mX3CWD3jk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b1UjnN/btsoxqvuSgI/gXnvAEVTF0as4mX3CWD3jk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb1UjnN%2FbtsoxqvuSgI%2FgXnvAEVTF0as4mX3CWD3jk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;428&quot; data-origin-width=&quot;492&quot; data-origin-height=&quot;428&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서&amp;nbsp;jacoco-resources&amp;nbsp;밑쪽에&amp;nbsp;있는&amp;nbsp;index.html이랑&amp;nbsp;tests&amp;nbsp;쪽의&amp;nbsp;index.html&amp;nbsp;둘로&amp;nbsp;나뉘어져&amp;nbsp;있는데,&amp;nbsp;나오는&amp;nbsp;정보가&amp;nbsp;살짝&amp;nbsp;다르다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상위에 있는 index.html이 jacoco에 의해서 생성된 리포트이고, 아래의 index.html은 gradle에서 자동으로 만들어주는 것으로 알고 있다.&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;스크린샷 2023-07-23 오후 4.15.27.png&quot; data-origin-width=&quot;2152&quot; data-origin-height=&quot;428&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/drjgcU/btsoDJ09HZP/XyA1YrBoBJuZqas8uVo37k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/drjgcU/btsoDJ09HZP/XyA1YrBoBJuZqas8uVo37k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/drjgcU/btsoDJ09HZP/XyA1YrBoBJuZqas8uVo37k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdrjgcU%2FbtsoDJ09HZP%2FXyA1YrBoBJuZqas8uVo37k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2152&quot; height=&quot;428&quot; data-filename=&quot;스크린샷 2023-07-23 오후 4.15.27.png&quot; data-origin-width=&quot;2152&quot; data-origin-height=&quot;428&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-07-23 오후 4.15.50.png&quot; data-origin-width=&quot;1812&quot; data-origin-height=&quot;258&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rW7Ci/btsoyayWz7w/K95Lut3KgCsq8ibF3hBam0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rW7Ci/btsoyayWz7w/K95Lut3KgCsq8ibF3hBam0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rW7Ci/btsoyayWz7w/K95Lut3KgCsq8ibF3hBam0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrW7Ci%2FbtsoyayWz7w%2FK95Lut3KgCsq8ibF3hBam0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1812&quot; height=&quot;258&quot; data-filename=&quot;스크린샷 2023-07-23 오후 4.15.50.png&quot; data-origin-width=&quot;1812&quot; data-origin-height=&quot;258&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상위의 index.html의 경우 위와 같이 어느 클래스에서 어떤 파일이 커버리지에 불충족하는지 나오게 된다.&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;스크린샷 2023-07-23 오후 4.16.25.png&quot; data-origin-width=&quot;2192&quot; data-origin-height=&quot;1142&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b3VkpQ/btsoyZcuhi4/piJJQQ8OyxlDs2r0gYJjQ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b3VkpQ/btsoyZcuhi4/piJJQQ8OyxlDs2r0gYJjQ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b3VkpQ/btsoyZcuhi4/piJJQQ8OyxlDs2r0gYJjQ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb3VkpQ%2FbtsoyZcuhi4%2FpiJJQQ8OyxlDs2r0gYJjQ0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;753&quot; height=&quot;392&quot; data-filename=&quot;스크린샷 2023-07-23 오후 4.16.25.png&quot; data-origin-width=&quot;2192&quot; data-origin-height=&quot;1142&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;  build.gradle 설정해주기 -&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;JacocoTestCoverageVerification&lt;/b&gt;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1690096737721&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;jacocoTestCoverageVerification {
    // QueryDSL QDomain 제외시키기
    def QDomains = []
    // qPattern = &quot;*.QA&quot;,&quot;*.QB&quot;,&quot;*.QC&quot;, ... &quot;*.QZ&quot;
    for (qPattern in '*.QA'..'*.QZ') {
        QDomains.add(qPattern + '*')
    }

   ...
}&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;마찬가지로 QDomain에 대해서는 측정할 필요가 없기 때문에 제거하도록 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1690096745849&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; ...
 
 violationRules {
        rule {
            // rule 활성화
            enabled = true

            // 클래스 단위로 룰 체크
            element = 'CLASS'

            // 라인 커버리지를 최소 80% 만족
            limit {
                counter = 'LINE'
                value = 'COVEREDRATIO'
                minimum = 0.80
            }
						
            // 마찬가지로 제거 대상 지정
            excludes = [
                    &quot;co.kirikiri.domain.**.**&quot;,
                    &quot;**.*Application*&quot;,
                    &quot;**.*Config*&quot;,
                    &quot;**.*Dto*&quot;,
                    &quot;**.*Request*&quot;,
                    &quot;**.*Response*&quot;,
                    &quot;**.*Interceptor*&quot;,
                    &quot;**.*Exception*&quot;
            ] + QDomains
        }
    }&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;b&gt;1. element: &lt;/b&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;커버리지를 체크할 기준&lt;/span&gt; 정하기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- BUNDLE: 프로젝트의 모든 파일을 합친 것 (디폴트)&lt;br /&gt;- CLASS: 클래스&lt;br /&gt;- GROUP: 논리적 번들 그룹&lt;br /&gt;- METHOD: 메서드&lt;br /&gt;- PACKAGE: 패키지&lt;br /&gt;- SOURCEFILE: 소스 파일&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;나는 클래스 단위로 설정하기 위해 CLASS라고 지정해주었으며, 대부분 클래스 단위로 많이 보시는 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좀 더 빡세게 하고 싶다면 METHOD로 해도 될 것 같은데, 그럼 너무 확인하기 힘들지 않을까 싶다.&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;2.&amp;nbsp;counter&lt;/b&gt;: limit을 통해 지정할 수 있으며, &lt;span style=&quot;color: #ef5369;&quot;&gt;커버리지 측정을 위한 최소 단위&lt;/span&gt;. 자바 바이트 코드의 실행을 기준으로 측정된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- BRANCH: 조건문 등의 분기 수&lt;br /&gt;- CLASS: 클래스 수, 내부 메서드가 한 번이라도 실행되었다면 실행된 것으로 간주한다&lt;br /&gt;- COMPLEXITY: 복잡도&lt;br /&gt;- INSTRUCTION: Java 바이트코드 명령의 수 (디폴트)&lt;br /&gt;- METHOD: 메서드 수, 메서드가 한 번이라도 실행되었다면 실행된 것으로 간주&lt;br /&gt;- LINE: 빈 줄을 제외한 실제 코드의 라인 수, 라인이 한 번이라도 실행되었다면 실행된 것으로 간주&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: #ef5369;&quot;&gt; LINE 기준으로 80%의 기준을 잡았다&lt;/span&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조금 더 널널하게 하고 싶다면 METHOD로 해도 될 것 같은데, 어차피 모든 분기점에 대해 테스트를 작성하려면 라인으로 하는 게 가장 나을 것 같다고 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;b&gt;3. value&lt;/b&gt;: limit을 통해 지정할 수 있으며, &lt;span style=&quot;color: #ef5369;&quot;&gt;측정한 커버리지를 어떠한 방식으로 보여줄 것인지&lt;/span&gt; 정한다.&lt;br /&gt;- COVEREDCOUNT: 커버된 개수&lt;br /&gt;- COVEREDRATIO: 커버된 비율, 0~1 사이의 수로 1이 100% (기본값)&lt;br /&gt;- MISSEDCOUNT: 커버되지 않은 개수&lt;br /&gt;- MISSEDRATIO: 커버되지 않은 비율, 0~1 사이의 수로 1이 100%&lt;br /&gt;- TOTALCOUNT: 전체 개수&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;(20%나 못했어요!보다는 80% 했으니까 100%까지 채워봐요~의 느낌이 더 좋은 것처럼?)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. &lt;b&gt;minimum&lt;/b&gt;: counter 값을 value에 맞게 표현하였을 때의 최소값, 이를 통해서&lt;span style=&quot;color: #ef5369;&quot;&gt; 커버리지 판단의 성공 여부가 결정&lt;/span&gt;된다고 볼 수 있다.&lt;br /&gt;- 기본적으로 표기한 자리수만큼 value가 출력되기 때문에 90%의 커버리지를 원한다면 0.9가 아닌 0.90으로 입력해줘야 한다. 아니면 0.9로 입력하면 0.9x 값을 모두 0.9로 인식한다!&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;나는 우선 80% 정도로 설정하였는데, 90%까지 올릴 생각은 있다.&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;5. &lt;b&gt;excludes&lt;/b&gt;: &lt;span style=&quot;color: #ef5369;&quot;&gt;커버리지 측정 시 제외할 클래스를 지정&lt;/span&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&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;Test 실행의 경우 ./gradlew test, ./gradlew clean build로도 실행이 가능하고, intellij를 사용하면 gradle 도구를 활용할 수도 있다!&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;652&quot; data-origin-height=&quot;488&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b363QB/btsoCjaDtxd/RsBesCsePa1kKrMtRLwoA0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b363QB/btsoCjaDtxd/RsBesCsePa1kKrMtRLwoA0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b363QB/btsoCjaDtxd/RsBesCsePa1kKrMtRLwoA0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb363QB%2FbtsoCjaDtxd%2FRsBesCsePa1kKrMtRLwoA0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;652&quot; height=&quot;488&quot; data-origin-width=&quot;652&quot; data-origin-height=&quot;488&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;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;222&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/coObrn/btsoyJ18Obv/t6QOSbKW2VeYHZbeq6dKI0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/coObrn/btsoyJ18Obv/t6QOSbKW2VeYHZbeq6dKI0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/coObrn/btsoyJ18Obv/t6QOSbKW2VeYHZbeq6dKI0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcoObrn%2FbtsoyJ18Obv%2Ft6QOSbKW2VeYHZbeq6dKI0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;222&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;222&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;참고로, Lombok의 어노테이션에 대한 코드 커버리지가 0%로 나오는 문제를 방지하기 위해서 lombok.config를 추가로 설정해주자.&lt;/p&gt;
&lt;pre id=&quot;code_1690097279134&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;lombok.addLombokGeneratedAnnotation = true&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;332&quot; data-origin-height=&quot;490&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/blPDr8/btsoCkN53Yz/QigTcwIcog2e35HWf8hnt1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/blPDr8/btsoCkN53Yz/QigTcwIcog2e35HWf8hnt1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/blPDr8/btsoCkN53Yz/QigTcwIcog2e35HWf8hnt1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FblPDr8%2FbtsoCkN53Yz%2FQigTcwIcog2e35HWf8hnt1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;332&quot; height=&quot;490&quot; data-origin-width=&quot;332&quot; data-origin-height=&quot;490&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 프로젝트의 최상단 경로에 적용해두어야 적용이 된다.&lt;br /&gt;참고로 아직 메서드 단위로 측정을 패스하는 게 없어서, 커버리지 측정을 원하지 않는 클래스라면 패키지 단위로 묶어두는 게 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모두 테스트 파이팅  &lt;/p&gt;</description>
      <category>개발일지</category>
      <category>Gradle</category>
      <category>jacoco</category>
      <category>테스트리포트</category>
      <category>테스트커버리지</category>
      <author>dolmeng2</author>
      <guid isPermaLink="true">https://cl8d.tistory.com/119</guid>
      <comments>https://cl8d.tistory.com/119#entry119comment</comments>
      <pubDate>Sun, 23 Jul 2023 16:29:29 +0900</pubDate>
    </item>
    <item>
      <title>[JPA] @OneToMany 단방향 매핑 시 고려해야 하는 것들</title>
      <link>https://cl8d.tistory.com/118</link>
      <description>&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;JPA에서 @OneToMany &lt;b&gt;단방향 매핑보다는 다대일 양방향 매핑을 권장&lt;/b&gt;한다는 말을 보았을 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, 객체지향으로 코드를 설계하다 보면 다대일의 상황보다는 &lt;b&gt;1인 엔티티를 기준으로 N인 엔티티를 조회하는 경우가 더 많고&lt;/b&gt;, (적어도 내 경험에서는 그랬다) N인 엔티티가 굳이 1인 엔티티에 대한 정보를 몰라도 됐던 적이 많았다. 그러다 보니 자연스럽게 일대다 단방향 매핑을 많이 사용했었는데, 왜 그랬던 것인지 궁금해서 나름대로 실험을 해보고자 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(ex. 게시글과 게시글 이미지의 관계에서, 게시글 이미지를 기준으로 게시글 정보를 찾아오는 것보다는 게시글을 기준으로 게시글 이미지 정보를 가져오는 게 더 많았음)&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;style3&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  기본 엔티티 설계하기&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1689500207186&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
class Board(
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long = 0L,

    @Embedded
    val title: BoardTitle,

    @Embedded
    val content: BoardContent,

    @Embedded
    var boardTags: BoardTags = BoardTags(Collections.emptyList())
) {

    fun addTag(boardTag: BoardTag) {
        boardTags.addTag(boardTag)
        boardTags = BoardTags(boardTags.tags)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 게시글에 제목과 내용 정보가 존재하며, &lt;span style=&quot;color: #ef5369;&quot;&gt;게시글 태그에 대한 정보가 1:N으로 구성된 엔티티&lt;/span&gt;다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BoardTitle과 BoardContent는 VO이기 때문에 별도로 첨부하지는 않겠다.&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;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;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  조인 테이블을 생성하여 일대다 관계 구성하기&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1689503682574&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Embeddable
class BoardTags(
    tags: MutableList&amp;lt;BoardTag&amp;gt; = Collections.emptyList()
) {

    @OneToMany(fetch = FetchType.LAZY)
    val tags: MutableList&amp;lt;BoardTag&amp;gt; = tags.toMutableList()

    fun addTag(boardTag: BoardTag) {
        tags.add(boardTag)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;904&quot; data-origin-height=&quot;798&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Wj5IK/btsnEoZK6nl/ZhkTDYp4cGk6VIhs05VdrK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Wj5IK/btsnEoZK6nl/ZhkTDYp4cGk6VIhs05VdrK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Wj5IK/btsnEoZK6nl/ZhkTDYp4cGk6VIhs05VdrK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWj5IK%2FbtsnEoZK6nl%2FZhkTDYp4cGk6VIhs05VdrK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;466&quot; data-origin-width=&quot;904&quot; data-origin-height=&quot;798&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히 @OneToMany 어노테이션만 달았을 때, 생성된 테이블 구조를 보면&lt;span style=&quot;color: #ef5369;&quot;&gt; board_tags라는 별도의 조인 테이블&lt;/span&gt;이 생성된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 board_tags에는&amp;nbsp;board_id와&amp;nbsp;tags_id라는&amp;nbsp;필드가&amp;nbsp;추가되었다.&amp;nbsp;여기서&amp;nbsp;board_id의&amp;nbsp;네이밍은&amp;nbsp;BoardTags를&amp;nbsp;가지고&amp;nbsp;있는&amp;nbsp;엔티티&amp;nbsp;(&lt;b&gt;board)의&amp;nbsp;이름&amp;nbsp;+&amp;nbsp;_id&lt;/b&gt;를&amp;nbsp;합친&amp;nbsp;것이고,&amp;nbsp;tags_id의&amp;nbsp;네이밍은&lt;b&gt;&amp;nbsp;@OneToMany로&amp;nbsp;선언한&amp;nbsp;필드명&amp;nbsp;(tags)&amp;nbsp;+&amp;nbsp;_id&lt;/b&gt;를&amp;nbsp;붙인&amp;nbsp;형태이다.&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;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;✔️ 저장 테스트&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1689503815066&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
@Rollback(false) // 테스트 환경에서 롤백되는 것을 방지 
fun save() { 
    val 태그1 = BoardTag(name = &quot;태그1&quot;)
    val 태그2 = BoardTag(name = &quot;태그2&quot;)
    boardTagRepository.save(태그1)
    boardTagRepository.save(태그2)

    val board = Board(
        title = BoardTitle(&quot;제목&quot;), content = BoardContent(&quot;내용&quot;)
    )
    board.addTag(태그1)
    board.addTag(태그2)

    boardRepository.save(board)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;875&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/YlqPn/btsnILlDau9/bIMDPV7VMwJRGreEI81PKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/YlqPn/btsnILlDau9/bIMDPV7VMwJRGreEI81PKK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YlqPn/btsnILlDau9/bIMDPV7VMwJRGreEI81PKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYlqPn%2FbtsnILlDau9%2FbIMDPV7VMwJRGreEI81PKK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;739&quot; height=&quot;505&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;875&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 먼저, 게시글 태그와 게시글에 대해서 insert 하는 쿼리가 발생한다.&lt;br /&gt;이때,&amp;nbsp;게시글&amp;nbsp;태그&amp;nbsp;저장&amp;nbsp;시&amp;nbsp;&lt;span style=&quot;color: #ef5369;&quot;&gt;board_id에&amp;nbsp;대한&amp;nbsp;컬럼은&amp;nbsp;채워지지&amp;nbsp;않고&amp;nbsp;name,&amp;nbsp;id에&amp;nbsp;대해서만&amp;nbsp;삽입&lt;/span&gt;된다.&lt;br /&gt;cf) 키 전략이 IDENTITY이기 때문에 save 시 쿼리가 발생한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;659&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3bKzO/btsnE9A2D1j/TDdSo4KRV5zKK43DBYXhh1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3bKzO/btsnE9A2D1j/TDdSo4KRV5zKK43DBYXhh1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3bKzO/btsnE9A2D1j/TDdSo4KRV5zKK43DBYXhh1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3bKzO%2FbtsnE9A2D1j%2FTDdSo4KRV5zKK43DBYXhh1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;755&quot; height=&quot;389&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;659&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후,&amp;nbsp;flush&amp;nbsp;시점에서&amp;nbsp;조인&amp;nbsp;테이블에&amp;nbsp;위와&amp;nbsp;같이&amp;nbsp;insert&amp;nbsp;쿼리가&amp;nbsp;발생한다.&lt;br /&gt;즉,&amp;nbsp;쿼리가&amp;nbsp;총&amp;nbsp;5번이나&amp;nbsp;나가게&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&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;대한&amp;nbsp;태그를&amp;nbsp;삭제하는&amp;nbsp;테스트이다.&lt;br /&gt;이때,&amp;nbsp;게시글에&amp;nbsp;존재하는&amp;nbsp;태그&amp;nbsp;정보를&amp;nbsp;제거하고,&amp;nbsp;그&amp;nbsp;다음&amp;nbsp;게시글의&amp;nbsp;태그&amp;nbsp;정보를&amp;nbsp;제거한다.&lt;/p&gt;
&lt;pre id=&quot;code_1689503890241&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
@Rollback(false)
fun delete() {
    // 저장 로직
    val 태그1 = BoardTag(name = &quot;태그1&quot;)
    val 태그2 = BoardTag(name = &quot;태그2&quot;)
    boardTagRepository.save(태그1)
    boardTagRepository.save(태그2)

    val board = Board(title = BoardTitle(&quot;제목&quot;), content = BoardContent(&quot;내용&quot;))
    board.addTag(태그1)
    board.addTag(태그2)

    boardRepository.save(board)
    entityManager.flush()
		
		/** 제거 로직 */
    val savedBoard = boardRepository.findById(1L).get()
    savedBoard.boardTags.tags.removeAt(0) // 필수!
    boardTagRepository.deleteById(1L)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;786&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bfiror/btsnLvbJ3aP/yCEE41pck6XdaRzrraTJ5k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bfiror/btsnLvbJ3aP/yCEE41pck6XdaRzrraTJ5k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bfiror/btsnLvbJ3aP/yCEE41pck6XdaRzrraTJ5k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbfiror%2FbtsnLvbJ3aP%2FyCEE41pck6XdaRzrraTJ5k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;718&quot; height=&quot;441&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;786&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 태그 정보를 제거하려면 deleteById를 사용해야 한다고 생각할 수 있다. 하지만,&amp;nbsp;바로&amp;nbsp;deleteById를&amp;nbsp;사용하게&amp;nbsp;되면&amp;nbsp;&lt;span style=&quot;color: #ef5369;&quot;&gt;조인&amp;nbsp;테이블에&amp;nbsp;걸려&amp;nbsp;있는&amp;nbsp;외래키에&amp;nbsp;의해서&lt;/span&gt;&amp;nbsp;board_tag&amp;nbsp;테이블에&amp;nbsp;있는&amp;nbsp;필드를&amp;nbsp;바로&amp;nbsp;제거할&amp;nbsp;수&amp;nbsp;없다.&lt;br /&gt;그래서 board에 존재하는 tag 리스트에서 removeAt을 통해 1차적으로 조인 테이블에 대해서 제거를 해야 한다. (savedBoard.boardTags.tags.removeAt(0))&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;쿼리를 보면, 우선 조인 테이블인 board_tags에 대해서 delete 쿼리가 발생한다. 이때,&lt;b&gt; board_id를 기준으로 제거하기 때문에 board_tags에 저장된 2개의 레코드 모두가 제거&lt;/b&gt;된다. 이후, 제거되지 않은 남은 레코드에 대해 (tags_id = 2인 레코드) &lt;span style=&quot;color: #ef5369;&quot;&gt;다시 삽입하는 과정이 발생&lt;/span&gt;한다. 만약,&amp;nbsp;board_id&amp;nbsp;=&amp;nbsp;1인&amp;nbsp;레코드가&amp;nbsp;5개가&amp;nbsp;있었다면&amp;nbsp;5개&amp;nbsp;모두에&amp;nbsp;대해&amp;nbsp;제거하고,&amp;nbsp;4개에&amp;nbsp;대해서&amp;nbsp;추가적인&amp;nbsp;insert&amp;nbsp;쿼리가&amp;nbsp;발생하여&amp;nbsp;더&amp;nbsp;불필요한&amp;nbsp;쿼리가&amp;nbsp;발생하였을&amp;nbsp;것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;최종적으로 위의 과정이 끝나고 board_tag에 대해서 최종적인 태그 제거 쿼리가 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로 insert 쿼리로 인해서 원하지 않는 쿼리가 엄청 나갈 수도 있다는 특징을 가진다.&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;style3&quot; /&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;조인 테이블에 의해서 insert 쿼리가 발생한 것이라면, 조인 테이블 자체를 없애면 되지 않을까?&lt;/p&gt;
&lt;pre id=&quot;code_1689504061679&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Embeddable
class BoardTags(
    tags: MutableList&amp;lt;BoardTag&amp;gt; = Collections.emptyList()
) {

    @OneToMany(fetch = FetchType.LAZY)
		@JoinColumn(name = &quot;board_id&quot;)
    val tags: MutableList&amp;lt;BoardTag&amp;gt; = tags.toMutableList()

    fun addTag(boardTag: BoardTag) {
        tags.add(boardTag)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;@JoinColumn을&amp;nbsp;통해서&lt;/span&gt;&amp;nbsp;board_tag에&amp;nbsp;명시적으로&amp;nbsp;board에&amp;nbsp;대한&amp;nbsp;아이디를&amp;nbsp;저장할&amp;nbsp;수&amp;nbsp;있는&amp;nbsp;필드를&amp;nbsp;지정해주면&amp;nbsp;된다.&lt;br /&gt;1:N&amp;nbsp;관계에서&amp;nbsp;외래키는&amp;nbsp;항상&amp;nbsp;N&amp;nbsp;테이블에&amp;nbsp;걸리기&amp;nbsp;때문에&amp;nbsp;&lt;b&gt;n쪽에서&amp;nbsp;column&amp;nbsp;정보를&amp;nbsp;지정&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;914&quot; data-origin-height=&quot;618&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cHybyL/btsnSZQQrVR/7WnQaUjFixv7ZPLKySGsRk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cHybyL/btsnSZQQrVR/7WnQaUjFixv7ZPLKySGsRk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cHybyL/btsnSZQQrVR/7WnQaUjFixv7ZPLKySGsRk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcHybyL%2FbtsnSZQQrVR%2F7WnQaUjFixv7ZPLKySGsRk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;567&quot; height=&quot;383&quot; data-origin-width=&quot;914&quot; data-origin-height=&quot;618&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성된 테이블을 보면, 이번에는 조인 테이블 대신 board_tag 테이블에 board 정보를 지정하기 위한 board_id 정보가 추가된 것을 볼 수 있다.&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;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;✔️&amp;nbsp; 저장 테스트&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1689504152822&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
@Rollback(false) // 테스트 환경에서 롤백되는 것을 방지 
fun save() { 
    val 태그1 = BoardTag(name = &quot;태그1&quot;)
    val 태그2 = BoardTag(name = &quot;태그2&quot;)
    boardTagRepository.save(태그1)
    boardTagRepository.save(태그2)

    val board = Board(
        title = BoardTitle(&quot;제목&quot;), content = BoardContent(&quot;내용&quot;)
    )
    board.addTag(태그1)
    board.addTag(태그2)

    boardRepository.save(board)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;675&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c8ZiB8/btsnSXyIYvo/DolUAv6dapvkU9bp55ZqQK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c8ZiB8/btsnSXyIYvo/DolUAv6dapvkU9bp55ZqQK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c8ZiB8/btsnSXyIYvo/DolUAv6dapvkU9bp55ZqQK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc8ZiB8%2FbtsnSXyIYvo%2FDolUAv6dapvkU9bp55ZqQK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;708&quot; height=&quot;373&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;675&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저,&amp;nbsp;태그&amp;nbsp;2개가&amp;nbsp;저장된&amp;nbsp;결과이다.&lt;br /&gt;마찬가지로&amp;nbsp;name,&amp;nbsp;id만&amp;nbsp;값이&amp;nbsp;채워져서&amp;nbsp;board_id에&amp;nbsp;대한&amp;nbsp;컬럼은&amp;nbsp;null&amp;nbsp;값을&amp;nbsp;가지게&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;1280&quot; data-origin-height=&quot;398&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tUX6M/btsnTDtq8fm/xABMkFuJhvLnyyvMbIhUM1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tUX6M/btsnTDtq8fm/xABMkFuJhvLnyyvMbIhUM1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tUX6M/btsnTDtq8fm/xABMkFuJhvLnyyvMbIhUM1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtUX6M%2FbtsnTDtq8fm%2FxABMkFuJhvLnyyvMbIhUM1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;738&quot; height=&quot;229&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;398&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고, 게시글에 대한 insert 쿼리가 발생한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;804&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/epGek6/btsnSW0SN06/INNYJErYiQnWN2Vxs9R5LK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/epGek6/btsnSW0SN06/INNYJErYiQnWN2Vxs9R5LK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/epGek6/btsnSW0SN06/INNYJErYiQnWN2Vxs9R5LK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FepGek6%2FbtsnSW0SN06%2FINNYJErYiQnWN2Vxs9R5LK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;664&quot; height=&quot;417&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;804&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, 여기서 문제가 발생한다.&lt;span style=&quot;color: #ef5369;&quot;&gt; board_tag에 대해 추가적으로 업데이트 쿼리&lt;/span&gt;가 발생한 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 처음에 board_tag를 저장할 때 board_id에 대한 정보가 없는 상태였기 때문에 flush 시점에 board.addTag()로 인해 추가된 정보를 바탕으로 업데이트를 하게 된 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 @OneToMany 단방향에서는 &lt;span style=&quot;color: #ef5369;&quot;&gt;데이터 저장 시 업데이트 쿼리가 발생&lt;/span&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;style1&quot; /&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;✔️&amp;nbsp; 삭제 테스트&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1689504296210&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
@Rollback(false)
fun delete() {
    // 저장 로직
    ...

    // 강제 flush -&amp;gt; update 쿼리 발생
    entityManager.flush()

    // 조회 -&amp;gt; 영속성 컨텍스트에서 발생
    val savedBoard = boardRepository.findById(1L).get()
	
   // 보드에 존재하는 보드 태그 제거
    savedBoard.boardTags.tags.removeAt(0)
    
   // 보드 태그 제거
    boardTagRepository.deleteById(1L)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;745&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/UvHtR/btsnEM0mYSr/8xmqYWkuL43lAfa4K1U15K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/UvHtR/btsnEM0mYSr/8xmqYWkuL43lAfa4K1U15K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/UvHtR/btsnEM0mYSr/8xmqYWkuL43lAfa4K1U15K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUvHtR%2FbtsnEM0mYSr%2F8xmqYWkuL43lAfa4K1U15K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;748&quot; height=&quot;435&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;745&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;entityManager.flush&amp;nbsp;이후의&amp;nbsp;로직에서&amp;nbsp;발생하는&amp;nbsp;쿼리이다.&lt;br /&gt;저장 후 영속성 컨텍스트에서 게시글을 조회한 뒤, 해당 게시글에 연관된 태그를 제거하였다. removeAt(0)을&amp;nbsp;진행하게&amp;nbsp;되면&amp;nbsp;게시글&amp;nbsp;기준으로&amp;nbsp;&amp;lsquo;게시글의&amp;nbsp;첫&amp;nbsp;번째&amp;nbsp;태그&amp;nbsp;정보가&amp;nbsp;제거&amp;rsquo;&amp;nbsp;된&amp;nbsp;것이기&amp;nbsp;때문에&lt;span style=&quot;color: #ef5369;&quot;&gt;&amp;nbsp;id&amp;nbsp;=&amp;nbsp;1인&amp;nbsp;board_tag에&amp;nbsp;존재하는&amp;nbsp;board_id를&amp;nbsp;null로&amp;nbsp;세팅하는&amp;nbsp;쿼리&lt;/span&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;이는 사실 Hibernate의 flush 순서와 좀 더 가까운 문제이다. Hibernae에서 자식 엔티티를 FK 없이 저장하고 컬렉션에 대해 처리를 하게 되면&lt;span style=&quot;color: #ef5369;&quot;&gt; FK에 대한 update 쿼리가 발생&lt;/span&gt;하기 때문이다. 현재&amp;nbsp;테이블&amp;nbsp;구조&amp;nbsp;상&amp;nbsp;FK가&amp;nbsp;걸리지&amp;nbsp;않아서&amp;nbsp;&lt;b&gt;FK가&amp;nbsp;없는&amp;nbsp;상태니까&amp;nbsp;update&amp;nbsp;연산이&amp;nbsp;먼저&amp;nbsp;발생하고&amp;nbsp;엔티티에&amp;nbsp;대한&amp;nbsp;delete가&amp;nbsp;발생&lt;/b&gt;한&amp;nbsp;것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;cf) 참고로, update - delete 모두 flush 시에 발생하며 실제로 board_tag를 제거하지 않는다면 (deleteById) 두 쿼리 모두 발생하지 않는다. delete 시에 추가로 update 쿼리가 발생하는 느낌이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;  Hibernate의 flush 순서는 다음과 같다.&lt;br /&gt;&lt;/b&gt;&lt;br /&gt;1. OrphanRemovalAction &lt;br /&gt;2. AbstractEntityInsertAction &lt;br /&gt;3. EntityUpdateAction &lt;br /&gt;4. QueuedOperationCollectionAction &lt;br /&gt;5. CollectionRemoveAction &lt;br /&gt;6. CollectionUpdateAction &lt;br /&gt;7. CollectionRecreateAction &lt;br /&gt;8. EntityDeleteAction&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;+ 무조건 '제거 행위'를 한다고 해서 update 쿼리가 발생하는 것은 아니다.&lt;/p&gt;
&lt;pre id=&quot;code_1689504420861&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
@Rollback(false)
fun delete() {
    // 저장 로직
    ...

    // 강제 flush -&amp;gt; update 쿼리 발생
    entityManager.flush()

    // 조회 -&amp;gt; 영속성 컨텍스트에서 발생
    val savedBoard = boardRepository.findById(1L).get()

    // 보드 태그 제거
    boardTagRepository.deleteById(1L)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;239&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MpQdu/btsnSYEpERc/mE5wwyZL9HC0WUNsd9S4yk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MpQdu/btsnSYEpERc/mE5wwyZL9HC0WUNsd9S4yk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MpQdu/btsnSYEpERc/mE5wwyZL9HC0WUNsd9S4yk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMpQdu%2FbtsnSYEpERc%2FmE5wwyZL9HC0WUNsd9S4yk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;239&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;239&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만,&amp;nbsp;removeAt()을&amp;nbsp;통해서&amp;nbsp;제거하지&amp;nbsp;않는다면&amp;nbsp;단순한&amp;nbsp;delete&amp;nbsp;쿼리만&amp;nbsp;발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히 boardTag의 id 값만을 참고하여 delete를 하기 때문에, board_id에 대한 정보가 딱히 없기 때문이다. (이전에는 게시글을 기준으로 태그 정보를 제거하였기 때문)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서&lt;b&gt; 삭제 시에 update 쿼리가 무조건적으로 발생한다는 말은 틀린 말이다.&lt;/b&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;style3&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  그렇다면, 일대다 단방향은 아예 쓰면 안 되는 걸까? - updatable = false 지정하기&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;update 쿼리를 없애고 싶을 것이다. 그렇다면, 추가적으로 board_id에 대한 업데이트가 진행되지 않도록 쿼리 자체에 updatable에 대한 조건을 false로 지정해보자.&lt;/p&gt;
&lt;pre id=&quot;code_1689504598983&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Embeddable
class BoardTags(
    tags: MutableList&amp;lt;BoardTag&amp;gt; = Collections.emptyList()
) {

    @OneToMany(fetch = FetchType.LAZY)
    @JoinColumn(name = &quot;board_id&quot;, updatable = false)
    val tags: MutableList&amp;lt;BoardTag&amp;gt; = tags.toMutableList()

    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 updatable=false를 지정하게 되면, board_tag 테이블에 존재하는 board_id 필드는 수정 불가능 상태가 된다.&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &lt;span style=&quot;color: #ef5369;&quot;&gt;한 번 지정된 연관관계를 다시 세팅하는 것이 불가능&lt;/span&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;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;✔️&amp;nbsp; 저장 테스트&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1689504663741&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
@Rollback(false)
fun save() {
    val 태그1 = BoardTag(name = &quot;태그1&quot;)
    val 태그2 = BoardTag(name = &quot;태그2&quot;)
    boardTagRepository.save(태그1)
    boardTagRepository.save(태그2)

    val board = Board(
        title = BoardTitle(&quot;제목&quot;), content = BoardContent(&quot;내용&quot;)
    )
    board.addTag(태그1)
    board.addTag(태그2)

    boardRepository.save(board)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;678&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/8smMJ/btsnEnzNuLY/nn0JGU5p0C6Of1mJFKHf91/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/8smMJ/btsnEnzNuLY/nn0JGU5p0C6Of1mJFKHf91/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/8smMJ/btsnEnzNuLY/nn0JGU5p0C6Of1mJFKHf91/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F8smMJ%2FbtsnEnzNuLY%2Fnn0JGU5p0C6Of1mJFKHf91%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;705&quot; height=&quot;373&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;678&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;400&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0XamB/btsnGZj7F55/sg0CB59yAX7J3npiR83LdK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0XamB/btsnGZj7F55/sg0CB59yAX7J3npiR83LdK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0XamB/btsnGZj7F55/sg0CB59yAX7J3npiR83LdK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0XamB%2FbtsnGZj7F55%2Fsg0CB59yAX7J3npiR83LdK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;741&quot; height=&quot;232&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;400&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;update 쿼리는 발생하지 않지만, 처음 board_tag를 삽입할 때&lt;b&gt; board_id에 대한 정보가 지정되지 않는 상태로 insert&lt;/b&gt;가 된 것을 볼 수 있다. 추후&lt;span style=&quot;color: #ef5369;&quot;&gt;&amp;nbsp;board가&amp;nbsp;삽입되더라도&amp;nbsp;계속&amp;nbsp;board_tag의&amp;nbsp;board_id는&amp;nbsp;계속&amp;nbsp;null로&amp;nbsp;남게&amp;nbsp;되는&amp;nbsp;것&lt;/span&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;style1&quot; /&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;✔️&amp;nbsp; 삭제 테스트&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1689504750021&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
@Rollback(false)
fun delete() {
    // 저장 로직
    ...

    // 강제 flush -&amp;gt; update 쿼리 발생
    entityManager.flush()

    // 조회 -&amp;gt; 영속성 컨텍스트에서 발생
    val savedBoard = boardRepository.findById(1L).get()
		
    // 보드에 존재하는 보드 태그 제거
    savedBoard.boardTags.tags.removeAt(0)

    // 보드 태그 제거
    boardTagRepository.deleteById(1L)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;313&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cMdkGx/btsnLarQOy6/IXuRziW250qicINIkqoevK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cMdkGx/btsnLarQOy6/IXuRziW250qicINIkqoevK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cMdkGx/btsnLarQOy6/IXuRziW250qicINIkqoevK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcMdkGx%2FbtsnLarQOy6%2FIXuRziW250qicINIkqoevK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;313&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;313&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, 삭제 테스트에서는 보드에 존재하는 태그 정보를 removeAt으로 제거하더라도 &lt;span style=&quot;color: #ef5369;&quot;&gt;update 쿼리가 발생하지 않는다&lt;/span&gt;!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;insert 시 이미 board_id가 null이기 때문에 별도의 update 쿼리가 발생할 필요가 없어서 그냥 delete 쿼리만 나간 것이다.&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;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;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  board_id도 값이 채워졌으면 좋겠어! - nullable = false 지정해주기&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;board_id가 null인 것이 마음에 들지 않는 것이니까, 애초에 null 값이 들어오지 않도록 만들어보자.&lt;/p&gt;
&lt;pre id=&quot;code_1689504899875&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Embeddable
class BoardTags(
    tags: MutableList&amp;lt;BoardTag&amp;gt; = Collections.emptyList()
) {

    @OneToMany(fetch = FetchType.LAZY)
    @JoinColumn(name = &quot;board_id&quot;, updatable = false, nullable = false)
    val tags: MutableList&amp;lt;BoardTag&amp;gt; = tags.toMutableList()
		...
}&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;size18&quot;&gt;&lt;b&gt;✔️&amp;nbsp; 저장 테스트&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1689504908280&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
@Rollback(false)
fun save() {
    val 태그1 = BoardTag(name = &quot;태그1&quot;)
    val 태그2 = BoardTag(name = &quot;태그2&quot;)
    boardTagRepository.save(태그1)
    boardTagRepository.save(태그2)

    val board = Board(
        title = BoardTitle(&quot;제목&quot;), content = BoardContent(&quot;내용&quot;)
    )
    board.addTag(태그1)
    board.addTag(태그2)

    boardRepository.save(board)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;162&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bIeRfY/btsnLwaEiRP/cXjU9BKb1JMW1CNnAkP41k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bIeRfY/btsnLwaEiRP/cXjU9BKb1JMW1CNnAkP41k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bIeRfY/btsnLwaEiRP/cXjU9BKb1JMW1CNnAkP41k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbIeRfY%2FbtsnLwaEiRP%2FcXjU9BKb1JMW1CNnAkP41k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;162&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;162&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;null 값이 저장되지 않아 원하는 결과가 나올 것 같다고 예상했지만, 위와 같이 오류가 발생한 것을 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는, board_tag에 태그 정보가 저장하는 시점에 board_id에 대한 정보가 들어가야 하는데 현재는 &lt;span style=&quot;color: #ef5369;&quot;&gt;board에 대한 정보가 하나도 없으니 null 값이 들어가려다가 제약 조건 위반으로 인해&lt;/span&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;style3&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  태그 정보가 저장될 때 게시글 정보도 함께 저장할래! - CascadeType 지정하기&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면,&amp;nbsp;board_tag가&amp;nbsp;저장하는&amp;nbsp;시점에&amp;nbsp;board에&amp;nbsp;대한&amp;nbsp;정보를&amp;nbsp;알면&amp;nbsp;되는&amp;nbsp;것이&amp;nbsp;아닐까?&lt;br /&gt;하지만, 우리는 board &amp;rarr; board_tag로의 단방향 관계만 가지도록 만들고 싶기 때문에&lt;b&gt; board_tag가 board에 대한 정보를 알지 않기를 바란다&lt;/b&gt;. 이를&amp;nbsp;위해서는,&amp;nbsp;&lt;span style=&quot;color: #ef5369;&quot;&gt;board_tag를&amp;nbsp;알고&amp;nbsp;있는&amp;nbsp;board가&amp;nbsp;저장되는&amp;nbsp;시점에&amp;nbsp;태그에&amp;nbsp;대한&amp;nbsp;정보도&amp;nbsp;함께&amp;nbsp;저장&lt;/span&gt;되도록&amp;nbsp;만들&amp;nbsp;수&amp;nbsp;있다.&lt;/p&gt;
&lt;pre id=&quot;code_1689505472629&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Embeddable
class BoardTags(
    tags: MutableList&amp;lt;BoardTag&amp;gt; = Collections.emptyList()
) {

    @OneToMany(fetch = FetchType.LAZY, cascade = [CascadeType.PERSIST])
    @JoinColumn(name = &quot;board_id&quot;, updatable = false, nullable = false)
    val tags: MutableList&amp;lt;BoardTag&amp;gt; = tags.toMutableList()

    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와&amp;nbsp;같이&amp;nbsp;cascade&amp;nbsp;옵션을&amp;nbsp;활용하여&amp;nbsp;PERSIST로&amp;nbsp;지정해주자.&lt;br /&gt;이러면&amp;nbsp;board가&amp;nbsp;저장되는&amp;nbsp;시점에&amp;nbsp;board_tag&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;✔️&amp;nbsp; 저장 테스트&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존의&amp;nbsp;저장&amp;nbsp;로직을&amp;nbsp;변경하여,&amp;nbsp;board가&amp;nbsp;저장될&amp;nbsp;때&amp;nbsp;board_tag도&amp;nbsp;함께&amp;nbsp;지정하여&amp;nbsp;저장해보자.&lt;/p&gt;
&lt;pre id=&quot;code_1689505497862&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
@Rollback(false)
fun save_casacde() {
    val boardTags = mutableListOf(BoardTag(name = &quot;태그1&quot;), BoardTag(name = &quot;태그2&quot;))
    val board = Board(
        title = BoardTitle(&quot;제목&quot;), content = BoardContent(&quot;내용&quot;),
        boardTags = BoardTags(boardTags)
    )
    boardRepository.save(board)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;419&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nRldo/btsnG0i2s8e/Mo201cep5P5bNrP5xl8tIK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nRldo/btsnG0i2s8e/Mo201cep5P5bNrP5xl8tIK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nRldo/btsnG0i2s8e/Mo201cep5P5bNrP5xl8tIK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnRldo%2FbtsnG0i2s8e%2FMo201cep5P5bNrP5xl8tIK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;735&quot; height=&quot;241&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;419&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저,&amp;nbsp;board에&amp;nbsp;대한&amp;nbsp;정보가&amp;nbsp;저장되는&amp;nbsp;insert&amp;nbsp;쿼리가&amp;nbsp;발생하는&amp;nbsp;것을&amp;nbsp;볼&amp;nbsp;수&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;1280&quot; data-origin-height=&quot;829&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7Mdac/btsnOgefIhf/VZJ4Ru3iuEqC4GfPOb64w0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7Mdac/btsnOgefIhf/VZJ4Ru3iuEqC4GfPOb64w0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7Mdac/btsnOgefIhf/VZJ4Ru3iuEqC4GfPOb64w0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7Mdac%2FbtsnOgefIhf%2FVZJ4Ru3iuEqC4GfPOb64w0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;693&quot; height=&quot;449&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;829&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고,&lt;span style=&quot;color: #ef5369;&quot;&gt;&amp;nbsp;board_id가&amp;nbsp;1로&amp;nbsp;채워진&amp;nbsp;board_tag에&amp;nbsp;대한&amp;nbsp;insert&amp;nbsp;쿼리가&amp;nbsp;발&lt;/span&gt;생하는&amp;nbsp;것을&amp;nbsp;볼&amp;nbsp;수&amp;nbsp;있다.&lt;br /&gt;즉,&amp;nbsp;update&amp;nbsp;쿼리가&amp;nbsp;발생하지&amp;nbsp;않으면서&amp;nbsp;우리가&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;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;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  @OneToMany 양방향&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;그러나, 양방향 연관관계를 설정하기 위한 연관관계 편의 메서드가 존재해야 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1689505625126&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
class Board(
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long = 0L,

    @Embedded
    val title: BoardTitle,

    @Embedded
    val content: BoardContent,

    @Embedded
    var boardTags: BoardTags = BoardTags(Collections.emptyList())
) {

    fun addTag(boardTag: BoardTag) {
        boardTag.board = this // here!
        boardTags.addTag(boardTag)
        boardTags = BoardTags(boardTags.tags)
    }
}

@Embeddable
class BoardTags(
    tags: MutableList&amp;lt;BoardTag&amp;gt; = Collections.emptyList()
) {

    @OneToMany(fetch = FetchType.LAZY, mappedBy = &quot;board&quot;)
    val tags: MutableList&amp;lt;BoardTag&amp;gt; = tags.toMutableList()
    
    ...
}

@Entity
class BoardTag(

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long = 0L,

    val name: String,

    @ManyToOne(fetch = FetchType.LAZY)
    var board: Board? = null
) {
	...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;addTag() 메서드에서 연관관계를 세팅해주기 위해 boardTag.board = this라는 메서드를 추가해주었다.&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;style1&quot; /&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;✔️ 저장 테스트&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1689505768079&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
@Rollback(false)
fun save() {
    val 태그1 = BoardTag(name = &quot;태그1&quot;)
    val 태그2 = BoardTag(name = &quot;태그2&quot;)

    val board = Board(
        title = BoardTitle(&quot;제목&quot;), content = BoardContent(&quot;내용&quot;)
    )
    board.addTag(태그1)
    board.addTag(태그2)

    boardRepository.save(board)
    boardTagRepository.save(태그1)
    boardTagRepository.save(태그2)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;양방향 연관관계가 설정되었기 때문에 태그를 먼저 저장하는 대신에, 게시글에 대해서 먼저 저장해준다.&lt;br /&gt;이때&amp;nbsp;&lt;b&gt;board.addTag()&amp;nbsp;시&amp;nbsp;board에&amp;nbsp;대한&amp;nbsp;연관관계도&amp;nbsp;설정되기&amp;nbsp;때문에&lt;/b&gt;&amp;nbsp;자연스럽게&amp;nbsp;&lt;span style=&quot;color: #ef5369;&quot;&gt;boardTag가&amp;nbsp;저장되는&amp;nbsp;시점에&amp;nbsp;board에&amp;nbsp;대한&amp;nbsp;정보를&amp;nbsp;알게&amp;nbsp;되어&amp;nbsp;insert&amp;nbsp;시&amp;nbsp;업데이트&amp;nbsp;쿼리가&amp;nbsp;발생하지&amp;nbsp;않는다&lt;/span&gt;.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;436&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cLt9OG/btsnFeWDeHk/JNk67t4VKCVNFkM02bYxwK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cLt9OG/btsnFeWDeHk/JNk67t4VKCVNFkM02bYxwK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cLt9OG/btsnFeWDeHk/JNk67t4VKCVNFkM02bYxwK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcLt9OG%2FbtsnFeWDeHk%2FJNk67t4VKCVNFkM02bYxwK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;436&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;436&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 쿼리 정보를 보면, 먼저 게시글에 대한 insert 쿼리가 발생한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;842&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bm99MZ/btsnFL0VTYm/MZACxPXp97iDA6luInym11/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bm99MZ/btsnFL0VTYm/MZACxPXp97iDA6luInym11/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bm99MZ/btsnFL0VTYm/MZACxPXp97iDA6luInym11/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbm99MZ%2FbtsnFL0VTYm%2FMZACxPXp97iDA6luInym11%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;696&quot; height=&quot;458&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;842&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고, board_tag에 대해서 insert 쿼리가 발생할 때도&lt;b&gt; board_id에 대한 정보가 잘 채워져서&lt;/b&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;style1&quot; /&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;✔️ 삭제 테스트&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1689505869443&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
@Rollback(false)
fun delete_bidirection() {
    // 저장 로직
    ...

    // 조회 -&amp;gt; 영속성 컨텍스트에서 발생
    val savedBoard = boardRepository.findById(1L).get()

    // 보드에 존재하는 보드 태그 제거
    savedBoard.boardTags.tags.removeAt(0)

    // 보드 태그 제거
    boardTagRepository.deleteById(1L)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;260&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cBpBa8/btsnEW9Nokm/lT9lJLWY3HkMegrr1HJwP0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cBpBa8/btsnEW9Nokm/lT9lJLWY3HkMegrr1HJwP0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cBpBa8/btsnEW9Nokm/lT9lJLWY3HkMegrr1HJwP0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcBpBa8%2FbtsnEW9Nokm%2FlT9lJLWY3HkMegrr1HJwP0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;260&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;260&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때도&amp;nbsp;단순히&amp;nbsp;board_tag에&amp;nbsp;대한&amp;nbsp;delete&amp;nbsp;쿼리&amp;nbsp;한&amp;nbsp;번만&amp;nbsp;발생하게&amp;nbsp;된다.&lt;br /&gt;tags.removeAt(0)을 진행하더라도 외래키 제약조건으로 인해서 update 쿼리가 발생하지 않고 바로 엔티티에 대한 delete가 발생하게 된다.&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;style3&quot; /&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;blockquote data-ke-style=&quot;style3&quot;&gt;- @OneToMany 단방향 관계에서는 불필요한 update 쿼리가 발생할 수 있다.&lt;br /&gt;- 이를 해결하기 위해서는 nullable = false, updatable = false를 지정해주고 부모 엔티티가 영속화될 때 자식 엔티티도 함께 영속화될 수 있도록 PERSIST 옵션을 지정하자.&lt;br /&gt;- 그런 하위 관계가 아니라면 양방향 관계를 고려하자.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일대다 단방향은 부모 엔티티와 자식 엔티티의 생명주기가 같을 경우만 사용하도록 하자! (완전한 하위 관계일 때)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그게 아니라면 양방향으로 설정하거나, 아니면 그냥 엔티티끼리 연관관계를 가지는 게 아니라 단순히 id 값을 필드로 가지도록 만들 것 같다.&lt;/p&gt;</description>
      <category>Back-end/JPA</category>
      <category>@OneToMany</category>
      <category>JPA</category>
      <category>일대다 단방향</category>
      <author>dolmeng2</author>
      <guid isPermaLink="true">https://cl8d.tistory.com/118</guid>
      <comments>https://cl8d.tistory.com/118#entry118comment</comments>
      <pubDate>Sun, 16 Jul 2023 20:13:50 +0900</pubDate>
    </item>
    <item>
      <title>[Real MySQL 8.0] 인덱스와 B-Tree 알아보기</title>
      <link>https://cl8d.tistory.com/117</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  디스크 읽기 방식&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;  HDD (하드 디스크 드라이브) / SSD (솔리드 스레이트 드라이브)&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;602&quot; data-origin-height=&quot;318&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xDApv/btsmtOpYksr/yhi6ZurDkcZOmNhfUy8cT0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xDApv/btsmtOpYksr/yhi6ZurDkcZOmNhfUy8cT0/img.png&quot; data-alt=&quot;https://texit.tistory.com/15&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xDApv/btsmtOpYksr/yhi6ZurDkcZOmNhfUy8cT0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxDApv%2FbtsmtOpYksr%2Fyhi6ZurDkcZOmNhfUy8cT0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;602&quot; height=&quot;318&quot; data-origin-width=&quot;602&quot; data-origin-height=&quot;318&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://texit.tistory.com/15&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모리에 비해 실제 하드웨어 장치의 발전은 비교적 느리게 발전한다는 소리를 들은 적이 있을 것이다. 특히 HHD는 플래터(원판)을 돌려서 읽고나 쓰다 보니 성능이 매우 느릴 수밖에 없다. 이를 보완하기 위해 등장한 SSD의 경우&amp;nbsp;기존 하스크 드라이브에서 데이터 저장용 플래터(원판)을 제거하고 그 대신 &lt;span style=&quot;color: #ef5369;&quot;&gt;플래시 메모리를 장착해서 빠르게 데이터를 읽고 쓸 수 있다&lt;/span&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;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;  랜덤 I/O와 순차 I/O&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디스크 읽기 연산에서 '랜덤 I/O'와 '순차 I/O'라는 말을 들은 적이 있을 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;랜덤 I/O나 순차 I/O 모두 하드 디스크 드라이브 플래퍼(원판)을 돌려서, &lt;b&gt;읽어야&amp;nbsp;할&amp;nbsp;데이터가&amp;nbsp;저장된&amp;nbsp;위치로&amp;nbsp;디스크&amp;nbsp;헤더를&amp;nbsp;이동&lt;/b&gt;시킨 다음 다음 데이터를 읽는 것이다. 하지만, 순차 I/O의 경우 3개의 페이지를 기록할 때 1번의 시스템 콜을 사용하지만, 랜덤 I/O는 3번의 시스템 콜을 요청한다. 이는, 랜덤 I/O의 경우 &lt;b&gt;읽어야 하는 데이터가 물리적으로 불연속한 위치에 존재&lt;/b&gt;하기 때문에 헤드를 이동시키고 시스템 콜을 호출하지만, 순차 I/O는 &lt;b&gt;그대로 읽기만 하면 되기 때문에&lt;/b&gt; 1번이면 충분하기 때문이다. 즉, 순차 I/O가 랜덤 I/O보다 약 3배 빠르다는 것이다. 그래서&lt;span style=&quot;color: #ef5369;&quot;&gt;&amp;nbsp;여러&amp;nbsp;번&amp;nbsp;쓰기나&amp;nbsp;읽기를&amp;nbsp;하는&amp;nbsp;랜덤&amp;nbsp;I/O&amp;nbsp;작업이&amp;nbsp;부하가&amp;nbsp;더&amp;nbsp;크다&lt;/span&gt;.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;  SSD의 경우 플래터가 없어서 별 차이가 없다고 생각할 수 있다.&amp;nbsp;&lt;br /&gt;하지만, SSD에서도 순차 I/O에 비해 랜덤 I/O의 throughout은 떨어진다고 한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 디스크의 성능은 얼마나 많은 데이터를 한 번에 기록하느냐에 의해서 결정된다.&lt;br /&gt;랜덤 I/O와 순차 I/O 모두 파일 쓰기 실행 시 동기화 작업 (flush or fsync) 작업이 필요한데, 순차 I/O의 경우 &lt;b&gt;동기화 작업이 너무 빈번하면 랜덤 I/O처럼 비효율적으로 처리할 수 있어서&lt;/b&gt; 보통 RAID 컨트롤러의 캐시 메모리의 경우 효율적으로 순차 I/O를 처리할 수 있도록 도와주기도 한다.&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: #ef5369;&quot;&gt;쿼리를 튜닝한다는 것은 랜덤 I/O를 어떻게 줄이는 것인지가 포인트&lt;/span&gt;이다.&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;밑에서 다루겠지만, 인덱스 레인지 스캔은 데이터를 읽을 때 랜덤 I/O를 사용하고 풀 테이블 스캔은 순차 I/O를 사용하는데, 오히려 &lt;b&gt;테이블에 데이터가 굉장히 많고 해당 데이터의 대부분을 조회하는 경우&lt;/b&gt;에는 일부러 풀 테이블 스캔을 사용하여 역으로 순차 I/O를 통해 처리하는 경우도 존재한다. &lt;/span&gt;순차 I/O의 경우 더 빠르고 많은 데이터를 읽을 수 있어서 &lt;b&gt;OLTP (On-Line Transaction Processing)&lt;/b&gt; 성격의 서비스보다 통계 작업에서 자주 사용된고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;-&lt;b&gt; 클러스터링 인덱스&lt;/b&gt;를 활용해서 레인지 스캔을 하게 되면, 실제 물리적인 쿼리 위치가 정렬된 상태를 유지하기 때문에 &quot;&lt;b&gt;데이터를 순서대로 조회한다면&lt;/b&gt;&quot; 랜덤 I/O가 아닌 &lt;b&gt;순차 I/O를 진행&lt;/b&gt;하게 된다. 물론, 순서대로가 아닌 중간중간 몇 개씩만 데이터를 조회한다면 랜덤 I/O를 진행하게 될 것이다.&lt;br /&gt;&lt;br /&gt;- 반대로,&lt;b&gt; 세컨더리 인덱스&lt;/b&gt;를 사용하게 되면 클러스터링 인덱스를 가리키고 있고, 물리적인 위치가 정렬되지 않기 때문에 분산이 되어 있을 것이다. 그래서 순차 I/O에 비해서 &lt;b&gt;랜덤 I/O를 사용하여&lt;/b&gt; 동작하게 될 것이다.&amp;nbsp;&lt;/blockquote&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;style3&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;br /&gt;&lt;b&gt;  인덱스&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스란 &lt;span style=&quot;color: #ef5369;&quot;&gt;컬럼의 값과 해당 레코드가 저장된 주소를 key-value로 삼아  만드는 것&lt;/span&gt;을 의미한다. 이때, 컬럼의 값은 주어진 순서대로 미리 정렬해서 보관하며, 인덱스가 많은 테이블은 CUD 작업은 느리지만&lt;b&gt;, R에서 독보적인 성능&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;
&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;span style=&quot;color: #ef5369;&quot;&gt;레코드를 대표하는 컬럼의 값으로 만들어진 인덱스&lt;/span&gt;로, 테이블에서 레코드를 식별할 수 있는 기준값을 사용한다. 기본적으로 NULL과 중복을 허용하지 않는다. 반대로 세컨더리 인덱스는 &lt;span style=&quot;color: #ef5369;&quot;&gt;PK를 제외한 나머지 필드로&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;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  B-Tree 인덱스 (Balanced Tree)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스 구조 중에 가장 많이 사용하는 타입은 &lt;b&gt;B-Tree&lt;/b&gt;이다. (AVL 등 다른 구조도 있지만, B-Tree가 블록 단위로 데이터에 대한 I/O 작업을 진행할 때 가장 효율적으로 사용할 수 있기 때문이다.) 인덱스는&lt;b&gt; 페이지 단위&lt;/b&gt;로 저장되며, &lt;span style=&quot;color: #ef5369;&quot;&gt;인덱스 키를 기준으로&amp;nbsp;항상 정렬된 상태를 유지&lt;/span&gt;한다는 것이 특징이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-07-05 오전 1.28.15.png&quot; data-origin-width=&quot;2978&quot; data-origin-height=&quot;1592&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cMPGnW/btsmup4xsPs/NcFtRGbvsOW2iTRtLUpBhk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cMPGnW/btsmup4xsPs/NcFtRGbvsOW2iTRtLUpBhk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cMPGnW/btsmup4xsPs/NcFtRGbvsOW2iTRtLUpBhk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcMPGnW%2Fbtsmup4xsPs%2FNcFtRGbvsOW2iTRtLUpBhk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2978&quot; height=&quot;1592&quot; data-filename=&quot;스크린샷 2023-07-05 오전 1.28.15.png&quot; data-origin-width=&quot;2978&quot; data-origin-height=&quot;1592&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왼쪽에 있는 부분이 B-Tree 인덱스라고 생각하면 되고 (루트 노드 생략), 오른쪽이 &lt;b&gt;프라이머리 키 인덱스&lt;/b&gt;이다. (= 클러스터 인덱스)&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최상위는 루트 노드, 중간은 브랜치 노드, 가장 하위는 리프 노드라고 말하는데, 루트와 브랜치는 본인의 하위 자식 노드의 주소를 가지고 있으며, &lt;span style=&quot;color: #ef5369;&quot;&gt;리프 노드의 경우 데이터 파일에 저장된 레코드 주소를 가지고 있다&lt;/span&gt;. 또한, &lt;b&gt;인덱스의 키 값은 항상 정렬되어 있지만&lt;/b&gt; 데이터 파일의 레코드는 정렬이 되어 있지 않아서 일반적인 DBMS에서는 INSERT 순서로 저장하는 것이 아닌 중간에 DELETE로 인해 비어있는 공간이 생기면 해당 위치에 삽입이 진행된다. (즉, 임의의 순서) 하지만, 특별하게 &lt;span style=&quot;color: #ef5369;&quot;&gt;innoDB의 경우 레코드가 클러스터되기 때문에 PK 순서대로 정렬되어 저장&lt;/span&gt;되기 때문에 PK 자체가 ROWID (실제 레코드의 물리적인 주소) 역할을 하며, 이로 인해 PK가 '논리적인 주소'를 가진다고 말한다. (myISAM에서는 세컨더리 인덱스가 물리적인 주소를 가짐)&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;그래서 innoDB 테이블은 인덱스를 통해 데이터를 읽어도 바로 데이터 파일을 찾을 수 없으며, 인덱스 영역에 저장된 PK를 통해 &lt;b&gt;PK 인덱스를 한 번 더 검색한 다음, PK 인덱스의 리프 페이지에 저장된 레코드를 읽는다.&lt;/b&gt; 즉, 모든 세컨더리 인덱스 검색에서 데이터 레코드를 읽으려면 PK를 저장하고 있는 B-Tree를 다시 검색해야 한다.&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;기본적으로 PK로 레코드를 조회할 때 데이터 파일은 정렬되어 있지 않아서 PK 자체가 어느 페이지에 저장되어 있는지 알 수 없기 때문에 &lt;b&gt;랜덤 I/O 발생 후&lt;/b&gt;, 해당 PK 값을 따라가서 리프 노드에 저장된 실제 레코드를 받아오게 되는 것이다.&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;style3&quot; /&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;  B-Tree 인덱스 키 추가&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;B-tree에 새로운 키를 추가하기 위해서는 저장될 위치를 결정한 다음, &lt;b&gt;레코드의 키 값과 대상 레코드의 주소 정보를&lt;/b&gt; B-Tree의 인덱스에 저장한다. 만약,&lt;span style=&quot;color: #ef5369;&quot;&gt; 리프 노드가 꽉차면 해당 노드를 분리&lt;/span&gt;하는데, 이때 상위 브랜치 노드까지 처리 범위가 넓어져서 새로운 키를 추가하기 때문에 (부모로 승격하는 것) 해당 연산은 비용이 많이 드는 작업이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;보통 레코드 추가 작업이 1이라면 테이블의 인덱스에 키를 추가하는 비용은 1.5 정도로 측정하며, 예를 들어 인덱스가 3개라면 1.5 * 3 + 1 = 5.5 정도를 비용으로 생각한다. innoDB에서는 체인지 버퍼를 활용하여 인덱스 키를 추가하는 작업을 지연시킬 수 있지만, PK, UK 같이 &lt;b&gt;유니크 제약 조건이 걸려있을 경우 중복 체크를 해야 하기 때문에 즉시 B-Tree에 추가하거나 삭제&lt;/b&gt;하는 작업을 진행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;  체인지 버퍼&lt;/b&gt;&lt;br /&gt;체인지 버퍼는 특정 데이터 페이지가 버퍼 풀에 없을 때 세컨더리 인덱스 페이지에 대한 변경 사항을 캐시하는 데이터 구조이다,&lt;br /&gt;인덱스 삽입 / 업데이트 시 디스크에 대한 랜덤 I/O 연산이 비용이 크기 때문에 변경이 필요한 인덱스 페이지가 &lt;b&gt;버퍼 풀에 있으면 바로 업데이트를 하고&lt;/b&gt;, 디스크로부터 읽어와야 한다면&lt;b&gt; 인서트 버퍼에 저장했다가&lt;/b&gt; 사용자에게 결과를 바로 반환하도록 최적화한다.&lt;br /&gt;- 지연된 작업은 추후 다른 읽기 작업을 통해 버퍼 풀에서 페이지 로드 시 merge&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 사용자 쿼리 실행 -&amp;gt; 버퍼 풀에 변경 사항이 발생한 페이지 (B-Tree의 리프 노드) 있다면 즉시 변경 사항 반영 -&amp;gt; 버퍼 풀에 페이지 없으면 인서트 버퍼에 임시로 기록해두고 쿼리 실행 완료 -&amp;gt; 추후 인덱스 페이지를 읽을 때마다 인서트 버퍼에서 머지해야 하는 키값을 확인한 다음 병합 (B-Tree 인덱스 반영)&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;style3&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;br /&gt;&lt;b&gt;  B-Tree 인덱스 키 삭제&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스 키 삭제 시 삭제할 키 값이 저장된 &lt;span style=&quot;color: #ef5369;&quot;&gt;B-Tree의 리프 노드를 찾아서 삭제 마킹 처리를 진행&lt;/span&gt;한다. 마킹된 공간은 방치할 수도 있고, 재활용할 수도 있는데 마킹 작업 역시 디스크 I/O 작업이 필요해서 비용이 크다. 마찬가지로 MySQL&amp;nbsp;5.5&amp;nbsp;이상부터는&amp;nbsp;인덱스&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;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;br /&gt;&lt;b&gt;  B-Tree 인덱스 키 변경&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스 키 값은 해당 값에 따라 저장될 리프 노드의 위치가 결정되기 때문에, B-Tree의 키 값이 변경되면 인덱스의 키 값만 변경하는 것은 불가능하다. 그래서 보통 &lt;span style=&quot;color: #ef5369;&quot;&gt;먼저 키 값을 삭제한 뒤 새로운 키 값을 추가&lt;/span&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;style3&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;br /&gt;&lt;b&gt;  B-Tree 인덱스 키 검색&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 키 탐색 시 &amp;lsquo;&lt;b&gt;트리 탐색&lt;/b&gt;&amp;rsquo; 기법을 사용하여 조회한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;100% 값이 일치하는지 찾는 &lt;span style=&quot;color: #ef5369;&quot;&gt;equality 비교나 부등호 비교 조건, 혹은 like에서 앞부분만 일치&lt;/span&gt;하는 경우 (like%)에 대해서 인덱스가 걸린 필드를 사용하면 가능하지만, 인덱스 값을 변형한 다음에 사용한다면 빠른 검색을 활용할 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한, innoDB 테이블의 경우 검색 시 넥스트 키 락이나 레코드 락을 통해서 인덱스를 잠그고 테이블의 레코드를 잠그기 때문에, UPDATE / DELETE 시 테이블에 사용할만한 인덱스가 없으면 &lt;b&gt;불필요하게 많은 레코드를 잠글 수 있기 때문에&lt;/b&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;style3&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;br /&gt;&lt;b&gt;  B-Tree 인덱스 사용 시 고려할 요소&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;B-Tree에서 인덱스를 사용할 때 &lt;span style=&quot;color: #ef5369;&quot;&gt;인덱스를 구성하는 컬럼의 크기&lt;/span&gt;와&lt;span style=&quot;color: #ef5369;&quot;&gt; 레코드 건수&lt;/span&gt;, &lt;span style=&quot;color: #ef5369;&quot;&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;size18&quot;&gt;&lt;b&gt;  인덱스 키 값의 크기&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;innoDB의 데이터 저장 시 가장 기본 단위를 페이지 or 블록이라고 한다. (버퍼 풀에서 데이터를 버퍼링하기 위한 기본 단위이기도 하다) 이때,&lt;b&gt; 인덱스&amp;nbsp;역시&amp;nbsp;페이지&amp;nbsp;단위로&amp;nbsp;관리&lt;/b&gt;되며, 루트, 브랜치, 리프 노드를 구분하는 기준이 &amp;lsquo;페이지&amp;rsquo;이다. (기본은 16KB)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 B-Tree 구조에서 자식 노드의 개수는 가변적이기 때문에 &lt;span style=&quot;color: #ef5369;&quot;&gt;인덱스의 페이지 크기와 키 값의 크기에 따라서 결정&lt;/span&gt;된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;768&quot; data-origin-height=&quot;516&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BD1Oz/btsm7xOd6PV/A9wtxuIkzKKz7CfE927d6k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BD1Oz/btsm7xOd6PV/A9wtxuIkzKKz7CfE927d6k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BD1Oz/btsm7xOd6PV/A9wtxuIkzKKz7CfE927d6k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBD1Oz%2Fbtsm7xOd6PV%2FA9wtxuIkzKKz7CfE927d6k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;522&quot; height=&quot;351&quot; data-origin-width=&quot;768&quot; data-origin-height=&quot;516&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;위 그림을 기준으로 하면 하나의 인덱스 페이지에 총 16*1024/(&lt;span style=&quot;color: #ef5369;&quot;&gt;16&lt;/span&gt;+12) = 585개의 키를 저장할 수 있으며, 자식 노드를 585개 가질 수 있는 B-Tree가 된다. (현재 키 값은 16바이트) 하지만,&amp;nbsp;인덱스&amp;nbsp;키&amp;nbsp;값이&amp;nbsp;32바이트로&amp;nbsp;늘어난다면&amp;nbsp;16*1024/(&lt;span style=&quot;color: #ef5369;&quot;&gt;32&lt;/span&gt;+12)&amp;nbsp;=&amp;nbsp;372개가&amp;nbsp;저장되며,&amp;nbsp;만약&amp;nbsp;한&amp;nbsp;번&amp;nbsp;SELECT&amp;nbsp;시&amp;nbsp;500개&amp;nbsp;이상의&amp;nbsp;데이터를&amp;nbsp;읽어야&amp;nbsp;한다면&amp;nbsp;&lt;b&gt;두&amp;nbsp;번째&amp;nbsp;경우는&amp;nbsp;디스크를&amp;nbsp;2번&amp;nbsp;읽어야&amp;nbsp;해서&amp;nbsp;&lt;/b&gt;더&amp;nbsp;느려지게&amp;nbsp;된다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;  즉, 인덱스 키 값이 길어지면 &lt;span style=&quot;color: #ef5369;&quot;&gt;전체적인 인덱스의 크기도 커지기 때문에&lt;/span&gt;, 제한된 크기의 버퍼 풀에서 하나의 레코드를 위한 인덱스 크기가 커지면 &lt;span style=&quot;color: #ef5369;&quot;&gt;메모리에 캐시할 수 있는 레코드의 수가 줄어들어&lt;/span&gt; 결국 효율성 역시 떨어지게 된다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;&lt;b&gt;  B-Tree 깊이&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;B-Tree의 깊이는 직접 제어가 불가능하다. 만약&amp;nbsp;B-Tree의&amp;nbsp;깊이가&amp;nbsp;3이라고&amp;nbsp;가정한다면&amp;nbsp;위의&amp;nbsp;예제에서&amp;nbsp;최대&amp;nbsp;2억&amp;nbsp;(585^3)을&amp;nbsp;가지지만,&amp;nbsp;인덱스&amp;nbsp;키&amp;nbsp;값이&amp;nbsp;32바이트라면&amp;nbsp;최대&amp;nbsp;5천만&amp;nbsp;(372^3)이라서&amp;nbsp;줄어들고,&lt;b&gt;&amp;nbsp;결국&amp;nbsp;디스크&amp;nbsp;접근&amp;nbsp;횟수가&amp;nbsp;또&amp;nbsp;늘어나는&amp;nbsp;결과를&amp;nbsp;초래&lt;/b&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;size18&quot;&gt;&lt;br /&gt;&lt;b&gt;  선택도(Selectivity) = 기수성(Cardinality)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;선택도란&lt;span style=&quot;color: #ef5369;&quot;&gt; 모든 인덱스 키 값중 유니크한 값의 수를 의미&lt;/span&gt;한다. 만약 테이블의 레코드에서 중복값이 많아졌다면 기수성이 낮다고 볼 수 있고, 기수성이 낮으면 검색해야 하는 대상이 많아져서 속도가 느려지게 된다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;ex) 전체 레코드 수는 10000개일 때 쿼리 조건에 일치하는 레코드가 1개라면? &lt;br /&gt;이때, 특정 컬럼 A에 대해 유니크한 값의 개수가 10개라면 9개를 더 읽게 되는 것이고, 유니크한 값의 개수가 1000개라면 999개를 더 읽은 것이 된다. 즉, 후자의 경우가 비효율적인 방식이 된다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, A 컬럼에 인덱스를 걸었다면 &lt;span style=&quot;color: #ef5369;&quot;&gt;인덱스된 컬럼에 대해서 전체 레코드의 개수나 유니크한 값의 개수 등의 통계 정보&lt;/span&gt;를 가지고 있다. (인덱스별로 평균 몇 개의 레코드가 있는지 미리 계산하는 것) 이때 전자의 경우 10000 / 10 = 1000개를 미리 읽게 되어, 1개만 일치한다면 999개가 낭비된다. 반면, 후자의 경우 10000 / 1000 = 10개를 미리 읽게 되어, 9개가 낭비되기 때문에, 인덱스를 걸면 오히려 결과가 반대되는 것을 볼 수 있다. 그래서 인덱스의 경우 &lt;span style=&quot;color: #ef5369;&quot;&gt;유니크한 값의 개수가 많을수록 더 효율적&lt;/span&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;style3&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&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;일반적인 DBMS의 옵티마이저는 인덱스를 통해 레코드 1건을 읽는 게 직접 레코드에서 데이터를 읽는 것보다 4~5배 정도 비용이 많이 든다고 예측한다. 그래서&amp;nbsp;인덱스를&amp;nbsp;통해&amp;nbsp;읽을&amp;nbsp;레코드의&amp;nbsp;개수가&lt;b&gt;&amp;nbsp;전체&amp;nbsp;테이블&amp;nbsp;레코드의&amp;nbsp;20~25%를&amp;nbsp;넘는다면&amp;nbsp;인덱스를&amp;nbsp;사용하지&amp;nbsp;않고&lt;/b&gt;&amp;nbsp;그냥&amp;nbsp;풀테이블&amp;nbsp;스캔&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;&lt;b&gt;  인덱스 레인지 스캔&lt;/b&gt;&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;span style=&quot;color: #ef5369;&quot;&gt; 오름차순 / 내림차순으로 정렬된 상태로 데이터&lt;/span&gt;를 가져오게 된다. 그리고, 조건을 다 찾으면 반환 후 쿼리를 종료한다.&lt;/p&gt;
&lt;pre id=&quot;code_1688966115347&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT * FROM crew WHERE name BETWEEN Ice AND Journey&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;712&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IBrvY/btsm83NgiSm/pQfAgN536k3fiZcKkNrrU1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IBrvY/btsm83NgiSm/pQfAgN536k3fiZcKkNrrU1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IBrvY/btsm83NgiSm/pQfAgN536k3fiZcKkNrrU1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIBrvY%2Fbtsm83NgiSm%2FpQfAgN536k3fiZcKkNrrU1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;651&quot; height=&quot;362&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;712&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리프 노드 5번 페이지에 도달하기 위해서 루트 노드, 브랜치 노드에서 찾아들어간다. 이후 찾은 다음에는 쭉 레인지 스캔을 진행하며 &amp;lsquo;Journey&amp;rsquo; 레코드를 찾을 때까지 쭉 스캔하다가 찾으면 반환하게 된다.&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;span style=&quot;color: #ef5369;&quot;&gt;데이터 파일로부터 읽어와야 하기 때문에&lt;/span&gt; (일치하는 개수만큼 디스크에서 처리해야 함) &lt;span style=&quot;color: #ef5369;&quot;&gt;랜덤 I/O 작업이 발&lt;/span&gt;생하여 비용이 상당히 크다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;  &lt;b&gt;인덱스 레인지 스캔 과정&lt;/b&gt;&lt;br /&gt;1. 인덱스에서 조건을 만족하는 값이 &lt;span style=&quot;color: #ef5369;&quot;&gt;저장된 위치를 찾는다&lt;/span&gt;. (index seek)&amp;nbsp;&lt;br /&gt;2. 탐색된 위치로부터&lt;span style=&quot;color: #ef5369;&quot;&gt; 필요한 만큼 인덱스를 읽어나간다&lt;/span&gt;. (index scan)&lt;br /&gt;3. &lt;b&gt;읽은 인덱스 키와 레코드 주소를 통해 &lt;/b&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;데이터 페이지&lt;/span&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;를 가져오고, 저장된 레코드를 읽어온다&lt;/span&gt;. (랜덤 I/O)&lt;br /&gt;- 만약&amp;nbsp;커버링&amp;nbsp;인덱스를&amp;nbsp;사용한다면&amp;nbsp;디스크의&amp;nbsp;레코드를&amp;nbsp;읽어오지&amp;nbsp;않기&amp;nbsp;때문에&amp;nbsp;랜덤&amp;nbsp;읽기가&amp;nbsp;줄어들고&amp;nbsp;성능이&amp;nbsp;빨라진다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커버링 인덱스란 쿼리를 충족시키기 위한 모든 데이터를 가지고 있는 인덱스로, where, order by, group by 등 조건절에 대해서 사용되는 모든 컬럼이 인덱스 컬럼에 포함되는 경우를 의미한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;- index seek, index scan 과정은 mySQL에서 다음과 같은 쿼리로 알 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1688991079814&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SHOW STATUS LIKE 'handler_%';&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;676&quot; data-origin-height=&quot;226&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/6pvOe/btsm9fudqqs/8KFezgsIbdKwJTBB2u7Szk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6pvOe/btsm9fudqqs/8KFezgsIbdKwJTBB2u7Szk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6pvOe/btsm9fudqqs/8KFezgsIbdKwJTBB2u7Szk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F6pvOe%2Fbtsm9fudqqs%2F8KFezgsIbdKwJTBB2u7Szk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;562&quot; height=&quot;188&quot; data-origin-width=&quot;676&quot; data-origin-height=&quot;226&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;b&gt;handler_read_key&lt;/b&gt;: index seek가 실행된 횟수&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; handler_read_next, handler_read_prev&lt;/b&gt;: index scan에서 읽은 레코드 개수&lt;br /&gt;handler_read_next: 인덱스 정순으로 읽은 레코드 개수 / handler_read_prev:&amp;nbsp;인덱스&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;b&gt; handler_read_first, handler_read_last&lt;/b&gt;: 인덱스의 첫 번째, 마지막 레코드를 읽은 횟수&lt;br /&gt;보통&amp;nbsp;MIN(),&amp;nbsp;MAX()&amp;nbsp;같이&amp;nbsp;값이&amp;nbsp;가장&amp;nbsp;작거나&amp;nbsp;큰&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&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;  인덱스 풀 스캔&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스의 처음부터 끝까지 모두 읽는 방식으로, &lt;span style=&quot;color: #ef5369;&quot;&gt;조건절에&amp;nbsp;사용된&amp;nbsp;컬럼이&amp;nbsp;인덱스의&amp;nbsp;첫&amp;nbsp;번째&amp;nbsp;컬럼이&amp;nbsp;아니라면&amp;nbsp;&lt;/span&gt;보통&amp;nbsp;인덱스&amp;nbsp;풀&amp;nbsp;스캔이&amp;nbsp;발생한다.&lt;br /&gt;ex)&amp;nbsp;인덱스가&amp;nbsp;A,&amp;nbsp;B,&amp;nbsp;C&amp;nbsp;순서지만&amp;nbsp;쿼리에서는&amp;nbsp;B,&amp;nbsp;C&amp;nbsp;컬럼으로&amp;nbsp;검색하는&amp;nbsp;경우&lt;br /&gt;인덱스&amp;nbsp;리프&amp;nbsp;노드의&amp;nbsp;제일&amp;nbsp;앞,&amp;nbsp;뒤로&amp;nbsp;이동한&amp;nbsp;다음,&amp;nbsp;처음부터&amp;nbsp;끝까지&amp;nbsp;스캔한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론,&amp;nbsp;인덱스가&amp;nbsp;테이블&amp;nbsp;크기보다&amp;nbsp;작기&amp;nbsp;때문에&amp;nbsp;테이블&amp;nbsp;풀&amp;nbsp;스캔에&amp;nbsp;비해서는&amp;nbsp;좀&amp;nbsp;더&amp;nbsp;효율적이다.&lt;br /&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;&lt;b&gt;  루스 인덱스 스캔&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레인지 스캔이랑 비슷하지만, &lt;span style=&quot;color: #ef5369;&quot;&gt;중간에 필요하지 않은 인덱스 키는 무시하고 다음으로 넘어가는 방식&lt;/span&gt;이다. GROUP&amp;nbsp;BY,&amp;nbsp;MIN(),&amp;nbsp;MAX()에&amp;nbsp;대해&amp;nbsp;최적화를&amp;nbsp;하는&amp;nbsp;경우&amp;nbsp;사용한다.&lt;/p&gt;
&lt;pre id=&quot;code_1688991372909&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT name, MIN(age)
FROM crew
WHERE id BETWEEN 1 AND 100
GROUP BY id;&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;crew 테이블은 (id, age)로 인덱스가 생성되어 있고, 정렬까지 되어 있는 상태라고 가정해보자.&lt;br /&gt;그렇다면 위의 쿼리를 처리할 때&lt;b&gt; id 그룹별로 첫  번째 레코드의 age 값만 읽으면 되기 때문에&lt;/b&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;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;&lt;b&gt;  인덱스 스킵 스캔&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1688991496198&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ALTER TABLE crew ADD INDEX idx_gender_birth (gender, birth);&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;crew&amp;nbsp;테이블에&amp;nbsp;위와&amp;nbsp;같이&amp;nbsp;성별과&amp;nbsp;생일&amp;nbsp;정보를&amp;nbsp;가진&amp;nbsp;복합&amp;nbsp;인덱스를&amp;nbsp;만들어보자.&lt;br /&gt;이때,&amp;nbsp;위의&amp;nbsp;인덱스를&amp;nbsp;사용하기&amp;nbsp;위해서는&amp;nbsp;gender에&amp;nbsp;대한&amp;nbsp;조건이&amp;nbsp;꼭&amp;nbsp;들어가야&amp;nbsp;했었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;하지만,&amp;nbsp;MySQL&amp;nbsp;8.0부터는&lt;b&gt;&amp;nbsp;birth&amp;nbsp;컬럼으로도&amp;nbsp;인덱스&amp;nbsp;검색이&amp;nbsp;가능하게&amp;nbsp;해주는&amp;nbsp;&amp;lsquo;인덱스&amp;nbsp;스킵&amp;nbsp;스캔&amp;rsquo;&lt;/b&gt;&amp;nbsp;최적화&amp;nbsp;기능이&amp;nbsp;도입되었다!&lt;br /&gt;- explain으로 실행 계획을 보면 type에 range라고 떠있을 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- gender 컬럼에서 유니크한 값을 다 조회한 다음, 쿼리에 gender 컬럼을 내부적으로 알아서 추가해서 다시 실행하는 형태로 처리된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1688991581056&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT gender, birth FROM crew WHERE birth &amp;gt;= '2001-03-12'&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1688991597160&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 내부적으로 아래와 같이 쿼리 생성 후 최적화
SELECT gender, birth FROM crew WHERE birth &amp;gt;= '2001-03-12' AND gender = 'W'
SELECT gender, birth FROM crew WHERE birth &amp;gt;= '2001-03-12' AND gender = 'M'&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;enum 값이 아니더라도 인덱스에 존재하는 모든 값을 먼저 추출하고, 이를 바탕으로 인덱스 스킵 스캔을 진행한다.&lt;br /&gt;하지만, 위 기능을 사용하려면 &amp;lsquo;&lt;span style=&quot;color: #ef5369;&quot;&gt;조건이 없는 인덱스의 컬럼의 유니크한 값의 개수가 적어야 하고&lt;/span&gt;&amp;rsquo;, &amp;lsquo;쿼리가 인덱스에 존재하는 컬럼만으로 처리가 가능해야 한다. (&lt;span style=&quot;color: #ef5369;&quot;&gt;커버링 인덱스&lt;/span&gt;)&amp;rsquo; 만약, 유니크한 값이 많아지면 스캔 시작 지점을 검색하는 작업도 많이 필요해질 것이다. 예를 들어 (gender, birth)라면 gender의 개수만큼 스캔 시작 지점이 생기게 되는 것이니까 유니크한 값이 적어야 한다는 것.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;&lt;b&gt;  다중 컬럼 인덱스 (복합 인덱스)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 개의 컬럼을 포함하는 인덱스이다.&lt;span style=&quot;color: #ef5369;&quot;&gt; 인덱스의&amp;nbsp;두&amp;nbsp;번째&amp;nbsp;컬럼은&amp;nbsp;첫&amp;nbsp;번째&amp;nbsp;컬럼에&amp;nbsp;의존해서&amp;nbsp;정렬&lt;/span&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;style3&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;br /&gt;&lt;b&gt;  B-Tree 인덱스의 정렬과 스캔 방향&lt;/b&gt;&lt;/h4&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;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;&lt;b&gt;  인덱스의 정렬&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;mySQL 8.0부터는 정렬 순서를 혼합한 인덱스를 사용할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1688991764364&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CREATE INDEX idx_name_age ON crew (name ASC, age DESC);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;  인덱스 스캔 방향&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스는 항상 오름차순으로 정렬되어 있지만, 최솟값부터 읽으면 오름차순으로 가져오고 최댓값부터 읽으면 내림차순으로 값을 가져올 수 있다. 즉,&amp;nbsp;&lt;b&gt;쿼리가&amp;nbsp;인덱스를&amp;nbsp;사용하는&amp;nbsp;시점에&amp;nbsp;효율적인&amp;nbsp;방법을&amp;nbsp;선택&lt;/b&gt;하는&amp;nbsp;것이다.&lt;/p&gt;
&lt;pre id=&quot;code_1688991821104&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT * FROM crew 
WHERE name &amp;gt;= 'journey' 
ORDER BY name ASC
LIMIT 4;&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;name의 인덱스를 통해 &amp;lsquo;journey&amp;rsquo; 레코드를 찾은 다음, 정방향으로 인덱스를 읽으면서 4개를 가져오면 된다. (ASC)&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1688991827324&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT * FROM crew 
ORDER BY name DESC
LIMIT 4;&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 인덱스를 역순으로 읽어서 4개만 가져오면 된다. (DESC)&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;br /&gt;&lt;b&gt;  내림차순 인덱스&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;복합 인덱스에서 내림차순, 오름차순이 혼합되어 있다면 &amp;lsquo;내림차순 인덱스&amp;rsquo;의 사용도 고려해야 한다.&lt;br /&gt;소량의&amp;nbsp;레코드를&amp;nbsp;대상으로&amp;nbsp;한다면&amp;nbsp;그냥&amp;nbsp;그대로&amp;nbsp;사용해도&amp;nbsp;되지만,&amp;nbsp;대량의&amp;nbsp;레코드를&amp;nbsp;대상으로&amp;nbsp;한다면&amp;nbsp;내림차순&amp;nbsp;인덱스가&amp;nbsp;더&amp;nbsp;효율적일&amp;nbsp;수&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;1100&quot; data-origin-height=&quot;532&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cvkmaK/btsm1S8dDUX/XZpM7VpivfBJUhCLqGbYb1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cvkmaK/btsm1S8dDUX/XZpM7VpivfBJUhCLqGbYb1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cvkmaK/btsm1S8dDUX/XZpM7VpivfBJUhCLqGbYb1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcvkmaK%2Fbtsm1S8dDUX%2FXZpM7VpivfBJUhCLqGbYb1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;604&quot; height=&quot;292&quot; data-origin-width=&quot;1100&quot; data-origin-height=&quot;532&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;p data-ke-size=&quot;size16&quot;&gt;- &lt;b&gt;오름차순 인덱스&lt;/b&gt;: 작은 값의 인덱스 키가 B-Tree의 왼쪽으로 정렬된 인덱스&lt;br /&gt;- &lt;b&gt;내림차순 인덱스&lt;/b&gt;: 큰 값의 인덱스 키가 B-Tree의 왼쪽으로 정렬된 인덱스&lt;br /&gt;- &lt;b&gt;인덱스 정방향 스캔&lt;/b&gt;: 인덱스 리프 노드의 왼쪽 &amp;rarr; 오른쪽으로 스캔 (순서대로 읽는 것)&lt;br /&gt;-&lt;b&gt; 인덱스 역방향 스캔&lt;/b&gt;: 인덱스 리프 노드의 오른쪽 &amp;rarr; 왼쪽으로 스캔 (반대 방향으로 읽는 것)&lt;br /&gt;&lt;br /&gt;보통 인덱스 정방향 스캔이 좀 더 빠르게 이루어진다. 이는 페이지 잠금이 인덱스 정방향 스캔에 좀 더 적합하게 설계되었으며, 페이지 내의 인덱스 레코드가 단방향으로만 연결되어 있기 때문이다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;- 리프 노트의 페이지는 이중 연결 리스트로 되어 있는데, 페이지 락 과정에서 데드락 방지를 위해 락을 획득하는 게 정방향에서만 가능하고, 역방향에서는 좀 복잡하기 때문이다. 그래서 구조적으로 인덱스 스캔에는 정방향이 좀 더 적합한 느낌?&lt;br /&gt;&lt;br /&gt;- 또한, 인덱스는 페이지 내부에서 4~8개 정도 묶어서 그룹을 만들어, 해당 그룹의 대표키를 뽑아 리스트로 관리한다. (= 페이지 디렉터리) 근데 이 디렉터리가 단방향으로 연결되어 있다 보니 역방향으로 접근이 안 되어서 더 느린 것.&lt;br /&gt;ex) 오름차순 인덱스라면 키값이 오름차순으로 정렬되어 있어 정방향 스캔이 빠른데, 내림차순 인덱스라면 키값이 내림차순으로 정렬되어 있어 역방향 스캔이 좀 더 빠른 느낌... 인 것 같다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;그래서 역방향 스캔을 사용한다면 내림차순 인덱스를 고려해보자.&lt;/p&gt;
&lt;pre id=&quot;code_1688992410390&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT * FROM crew
WHERE id = ?
ORDER BY name DESC
LIMIT 10;&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;위의&amp;nbsp;쿼리에&amp;nbsp;대해서&amp;nbsp;소량이라면&amp;nbsp;오름차순,&amp;nbsp;대량이면&amp;nbsp;내림차순을&amp;nbsp;사용하는&amp;nbsp;게&amp;nbsp;좋다.&lt;br /&gt;오름차순&amp;nbsp;인덱스:&amp;nbsp;INDEX&amp;nbsp;(id&amp;nbsp;ASC,&amp;nbsp;name&amp;nbsp;ASC);&lt;br /&gt;내림차순&amp;nbsp;인덱스:&amp;nbsp;INDEX&amp;nbsp;(id&amp;nbsp;DESC,&amp;nbsp;name&amp;nbsp;DESC);&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;style3&quot; /&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;  B-Tree 인덱스의 가용성과 효율성&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;  비교 조건의 종류와 효율성&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1688992463207&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT * FROM crew
WHERE course = 'Backend' and age &amp;gt;= 23;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 쿼리에 아래와 같은 인덱스가 추가되었다고 생각해보자.&lt;/p&gt;
&lt;pre id=&quot;code_1688992473777&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;INDEX (course, age)
INDEX (age, 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;첫 번째 인덱스의 경우 course = 'Backend' and age &amp;gt;= 23인 레코드를 찾은 다음, course=&amp;rsquo;Backend&amp;rsquo;가 아닐 때까지 인덱스를 그냥 쭉 읽는다. 이때는&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;하지만 두 번째 인덱스의 경우 age &amp;gt;= 23 and course = 'Backend'인 레코드를 찾고, &lt;b&gt;모든 레코드에 대해서 course=&amp;rsquo;Backend&amp;rsquo;인지 경우를 찾아야 한다&lt;/b&gt;. (필터링) 왜냐면&amp;nbsp;course의&amp;nbsp;경우&amp;nbsp;age를&amp;nbsp;기준으로&amp;nbsp;정렬이&amp;nbsp;되기&amp;nbsp;때문에&amp;nbsp;&amp;lsquo;course&amp;rsquo;라는&amp;nbsp;컬럼은&amp;nbsp;비교&amp;nbsp;작업을&amp;nbsp;좁히는데&amp;nbsp;도움을&amp;nbsp;주지&amp;nbsp;않기&amp;nbsp;때문이다.&amp;nbsp;단순히&amp;nbsp;해당&amp;nbsp;값이&amp;nbsp;일치하는지에만&amp;nbsp;사용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;여기서 첫&amp;nbsp;번째&amp;nbsp;인덱스의&amp;nbsp;course&amp;nbsp;=&amp;nbsp;'Backend'&amp;nbsp;and&amp;nbsp;age&amp;nbsp;&amp;gt;=&amp;nbsp;23을&amp;nbsp;&lt;b&gt;작업&amp;nbsp;범위&amp;nbsp;결정&amp;nbsp;조건&lt;/b&gt;,&amp;nbsp;두&amp;nbsp;번째&amp;nbsp;인덱스의&amp;nbsp;course&amp;nbsp;=&amp;nbsp;'Backend'를&amp;nbsp;&lt;b&gt;체크&amp;nbsp;조건&lt;/b&gt;이라고&amp;nbsp;한다.&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;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;&lt;b&gt;  인덱스의 가용성&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;B-Tree 인덱스는 왼쪽 값을 기준으로 오른쪽 값이 정렬되어 있다. 그래서 &lt;span style=&quot;color: #ef5369;&quot;&gt;왼쪽 부분이 없으면 인덱스 레인지 스캔이 안 된다&lt;/span&gt;.&lt;br /&gt;단일&amp;nbsp;인덱스의&amp;nbsp;like&amp;nbsp;검색에서&amp;nbsp;위&amp;nbsp;상황을&amp;nbsp;볼&amp;nbsp;수&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;394&quot; data-origin-height=&quot;480&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bWuaYU/btsnaMrlmM8/fCDiTINHAe6aFwKDyvUGi0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bWuaYU/btsnaMrlmM8/fCDiTINHAe6aFwKDyvUGi0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bWuaYU/btsnaMrlmM8/fCDiTINHAe6aFwKDyvUGi0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbWuaYU%2FbtsnaMrlmM8%2FfCDiTINHAe6aFwKDyvUGi0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;243&quot; height=&quot;296&quot; data-origin-width=&quot;394&quot; data-origin-height=&quot;480&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1688992566585&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT * FROM crew WHERE name like '%nez';&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 컬럼에 저장된 값을 한 글자씩 비교해가며 일치하는 레코드를 찾아야 하지만, 위와 같이&lt;span style=&quot;color: #ef5369;&quot;&gt; 왼쪽 부분이 고정되지 않았다면&lt;/span&gt; 인덱스의 효과를 보기 어렵다.&lt;br /&gt;&lt;br /&gt;또한, 복합 인덱스 인덱스의 경우 &lt;b&gt;선행 컬럼이 없다면 문제가 발생&lt;/b&gt;한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;524&quot; data-origin-height=&quot;512&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/JBhGQ/btsnaMrlp1I/XJcsW7YqsfAtkrLFhlt8gk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/JBhGQ/btsnaMrlp1I/XJcsW7YqsfAtkrLFhlt8gk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/JBhGQ/btsnaMrlp1I/XJcsW7YqsfAtkrLFhlt8gk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJBhGQ%2FbtsnaMrlp1I%2FXJcsW7YqsfAtkrLFhlt8gk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;315&quot; height=&quot;308&quot; data-origin-width=&quot;524&quot; data-origin-height=&quot;512&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1688992603241&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT * FROM crew WHERE age &amp;gt;= 20;&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;여기서 course 정보에 대한 조건절이 아닌, age에 대한 조건절이라면 효율적으로 인덱스를 사용할 수 없다.&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;style1&quot; /&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;다음과 같은 조건에서 B-Tree 인덱스를 사용할 수 없다. (작업 범위 결정 조건으로 사용이 불가능)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;b&gt;- NOT EQUAL 비교&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;WHERE&amp;nbsp;name&amp;nbsp;&amp;lt;&amp;gt;&amp;nbsp;&amp;lsquo;journey&amp;rsquo;&lt;br /&gt;WHERE&amp;nbsp;name&amp;nbsp;NOT&amp;nbsp;IN&amp;nbsp;(&amp;rsquo;journey&amp;rsquo;,&amp;nbsp;&amp;lsquo;hello&amp;rsquo;)&lt;br /&gt;WHERE&amp;nbsp;age&amp;nbsp;NOT&amp;nbsp;BETWEEN&amp;nbsp;20&amp;nbsp;AND&amp;nbsp;23&lt;br /&gt;WHERE&amp;nbsp;name&amp;nbsp;IS&amp;nbsp;NOT&amp;nbsp;NULL&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;b&gt;- LIKE &amp;lsquo;%??&amp;rsquo;&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;WHERE&amp;nbsp;name&amp;nbsp;LIKE&amp;nbsp;&amp;lsquo;%ney&amp;rsquo;&lt;br /&gt;WHERE&amp;nbsp;name&amp;nbsp;LIKE&amp;nbsp;&amp;lsquo;_ney&amp;rsquo;&lt;br /&gt;WHERE&amp;nbsp;name&amp;nbsp;LIKE&amp;nbsp;&amp;lsquo;%our%&amp;rsquo;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;b&gt;- 스토어드 함수나 다른 연산자로 인덱스 컬럼이 변형된 후 비교된 경우&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;WHERE&amp;nbsp;SUBSTRING&amp;nbsp;(name,&amp;nbsp;1,&amp;nbsp;1)&amp;nbsp;=&amp;nbsp;&amp;lsquo;J&amp;rsquo;&lt;br /&gt;WHERE&amp;nbsp;DAYOFMONTH(date)&amp;nbsp;=&amp;nbsp;1&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;b&gt;- NON-DETERMINISTIC 속성의 스토어드 함수가 비교 조건에 있을 경우&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;WHERE&amp;nbsp;name&amp;nbsp;=&amp;nbsp;function()&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;b&gt;- 데이터 타입이 서로 다른 비교&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;WHERE&amp;nbsp;char_column&amp;nbsp;=&amp;nbsp;20&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;b&gt;- 문자열 데이터 타입의 콜레이션이 다른 경우&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;WHERE&amp;nbsp;utf8_bin_char_column&amp;nbsp;=&amp;nbsp;euckr_bin_char_column&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로, MySQL에서는 NULL도 인덱스에 저장된다.&lt;br /&gt;&lt;br /&gt;다중&amp;nbsp;컬럼&amp;nbsp;인덱스는&amp;nbsp;아래와&amp;nbsp;같은&amp;nbsp;경우에&amp;nbsp;적용이&amp;nbsp;안&amp;nbsp;된다.&lt;/p&gt;
&lt;pre id=&quot;code_1688992771150&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CREATE INDEX idx_example (col_1, col_2, col_3, ... col_n)&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;col_1에 대한 비교 조건이 없는 경우&lt;/b&gt;&lt;br /&gt;- col_1에 대한 비교 조건이 위에서 봤던 사용할 수 없는 경우에 속하는 경우&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;가능한&amp;nbsp;경우는&amp;nbsp;다음과&amp;nbsp;같다.&amp;nbsp;(i는&amp;nbsp;2&amp;nbsp;이상&amp;nbsp;n&amp;nbsp;미만)&lt;br /&gt;- col_1 ~ col_(i-1)까지 동등 비교 형태일 때 &lt;b&gt;(=이나 in)&lt;/b&gt;&lt;br /&gt;- col_i에 대해 동등 비교 (&lt;b&gt;=이나 in&lt;/b&gt;), 크고 작음 비교 (&lt;b&gt;&amp;lt;, &amp;gt;&lt;/b&gt;), like로 좌측 일치 비교 (&lt;b&gt;like &amp;lsquo;jour%&amp;rsquo;&lt;/b&gt;)&lt;br /&gt;이때&amp;nbsp;col_1&amp;nbsp;~&amp;nbsp;col_i는&amp;nbsp;작업&amp;nbsp;범위&amp;nbsp;결정&amp;nbsp;조건으로,&amp;nbsp;col_i+1&amp;nbsp;~&amp;nbsp;col_n은&amp;nbsp;체크&amp;nbsp;조건으로&amp;nbsp;사용된다.&lt;/p&gt;</description>
      <category> /Real MySQL 8.0</category>
      <category>B-Tree</category>
      <category>인덱스</category>
      <category>인덱스 레인지 스캔</category>
      <author>dolmeng2</author>
      <guid isPermaLink="true">https://cl8d.tistory.com/117</guid>
      <comments>https://cl8d.tistory.com/117#entry117comment</comments>
      <pubDate>Mon, 10 Jul 2023 21:40:49 +0900</pubDate>
    </item>
    <item>
      <title>[우테코 5기] 코끼리끼리팀 - 기획부터 1차 데모데이까지 회고</title>
      <link>https://cl8d.tistory.com/116</link>
      <description>&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-07-07 오후 11.19.17.png&quot; data-origin-width=&quot;1860&quot; data-origin-height=&quot;1268&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ctuov7/btsmQEnbswD/B0G8KcAnO9Y0ds2nQ3dHlK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ctuov7/btsmQEnbswD/B0G8KcAnO9Y0ds2nQ3dHlK/img.png&quot; data-alt=&quot;나름 열심히 만든 임시 로고 (?) 그림판으로 만들었다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ctuov7/btsmQEnbswD/B0G8KcAnO9Y0ds2nQ3dHlK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fctuov7%2FbtsmQEnbswD%2FB0G8KcAnO9Y0ds2nQ3dHlK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;554&quot; height=&quot;378&quot; data-filename=&quot;스크린샷 2023-07-07 오후 11.19.17.png&quot; data-origin-width=&quot;1860&quot; data-origin-height=&quot;1268&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;나름 열심히 만든 임시 로고 (?) 그림판으로 만들었다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;드디어 레벨 3이 시작하면서 본격적인 팀 프로젝트가 진행되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 레벨 2 회고 때 언급했던 것처럼, 운이 좋게도 내가 아이디어를 냈던 주제로 프로젝트를 진행할 수 있게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 달 동안 함께 할 크루는 &lt;span style=&quot;color: #ef5369;&quot;&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 다들 대화할 때 눈을 마주치고 경청을 잘해주시기도 하고, 그렇다고 말이 없는 것도 아니고 회의도 꽤 재밌게 진행되는 부분이 있어서 좋다. 적당히 내향적이면서 외향적인 사람들이 잘 만난 긍정적인 케이스를 보는 느낌....? 특히 토론할만한 주제에 대해서 20분, 30분도 심도있게 토론하는 시간들이 굉장히 좋았다. 앞으로도 계속 이런 분위기를 유지하면 좋겠다!&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;style3&quot; /&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;  6월 27일 - 팀 이름 정하기&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;그리고 내가 나름 기획자이기 때문에 어떤 의도로 기획을 했는지 간단하게 팀원들에게 소개하는 시간을 가졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 이 과정에서 팀 이름을 정하기 위해 엄청 고민을 많이 했었는데... 거의 2시간은 &lt;span style=&quot;color: #ef5369;&quot;&gt;팀 이름 정하는데 쓴 것 같다&lt;/span&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GPT의 도움도 받고, 열심히 짱돌을 굴렸다. 덕분에 아래와 같은 멋진 이름들이 나왔다 ^_^&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-07-07 오후 11.25.29.png&quot; data-origin-width=&quot;676&quot; data-origin-height=&quot;1040&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bYHEET/btsmRoqR4oq/zcq6Kplp3lz2veHMHLKDD1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bYHEET/btsmRoqR4oq/zcq6Kplp3lz2veHMHLKDD1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bYHEET/btsmRoqR4oq/zcq6Kplp3lz2veHMHLKDD1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbYHEET%2FbtsmRoqR4oq%2Fzcq6Kplp3lz2veHMHLKDD1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;444&quot; height=&quot;683&quot; data-filename=&quot;스크린샷 2023-07-07 오후 11.25.29.png&quot; data-origin-width=&quot;676&quot; data-origin-height=&quot;1040&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개인적으로 1번도 좋았는데, 이미 존재하는 서비스의 이름을 빼다보니 저런 식으로 남게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;8번인 골골도 너무 마음에 들었는데, 재밌게도 투표 결과로 20번이 5표가 받게 되었다.&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; '함께'라는 의미를 가진 두 단어인 'Co'와 '끼리끼리'가 만나 꽤 위트&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;서비스의 방향성이 모호하다 보니 서로가 그리는 이미지가 상당히 달라서 토론이 꽤 길고 오래 걸렸었다  &lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-07-08 오전 12.09.33.png&quot; data-origin-width=&quot;2262&quot; data-origin-height=&quot;1022&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dEKvas/btsmOKIKvUs/nLTLxNA24EMkbh64ubVIdk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dEKvas/btsmOKIKvUs/nLTLxNA24EMkbh64ubVIdk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dEKvas/btsmOKIKvUs/nLTLxNA24EMkbh64ubVIdk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdEKvas%2FbtsmOKIKvUs%2FnLTLxNA24EMkbh64ubVIdk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2262&quot; height=&quot;1022&quot; data-filename=&quot;스크린샷 2023-07-08 오전 12.09.33.png&quot; data-origin-width=&quot;2262&quot; data-origin-height=&quot;1022&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  6월 28일 - 크루들에게 인터뷰 진행하기&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;&lt;span style=&quot;color: #ef5369;&quot;&gt;어떤 고객을 대상으로 타겟팅 할지 알아보면서 우리가 어떤 서비스를 제공하면 좋을지&lt;/span&gt; 고민하는 시간을 가졌다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-07-07 오후 11.31.12.png&quot; data-origin-width=&quot;1074&quot; data-origin-height=&quot;606&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ccx8dk/btsmRsGMlca/sPNuRSmGI72c64nNuEyXx1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ccx8dk/btsmRsGMlca/sPNuRSmGI72c64nNuEyXx1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ccx8dk/btsmRsGMlca/sPNuRSmGI72c64nNuEyXx1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fccx8dk%2FbtsmRsGMlca%2FsPNuRSmGI72c64nNuEyXx1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;518&quot; height=&quot;292&quot; data-filename=&quot;스크린샷 2023-07-07 오후 11.31.12.png&quot; data-origin-width=&quot;1074&quot; data-origin-height=&quot;606&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 기획 시에는 스터디를 위주로 생각하다 보니 '&lt;b&gt;계획 + 일정 관리 + 스터디&lt;/b&gt;'를 위주로 방향성을 가졌었다.&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-07-07 오후 11.32.40.png&quot; data-origin-width=&quot;448&quot; data-origin-height=&quot;688&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dbgrB9/btsmQfgTp6B/wo6reEShrMdbor4QfKbWk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dbgrB9/btsmQfgTp6B/wo6reEShrMdbor4QfKbWk1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dbgrB9/btsmQfgTp6B/wo6reEShrMdbor4QfKbWk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdbgrB9%2FbtsmQfgTp6B%2Fwo6reEShrMdbor4QfKbWk1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;312&quot; height=&quot;479&quot; data-filename=&quot;스크린샷 2023-07-07 오후 11.32.40.png&quot; data-origin-width=&quot;448&quot; data-origin-height=&quot;688&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후, &lt;b&gt;약 40분 간의&lt;/b&gt; 1:1 전담 인터뷰를 진행하였다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;20문항 정도의 질문을 준비해서 그런지, 한 명 한 명의 인터뷰가 꽤나 길었다.&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;span style=&quot;color: #ef5369;&quot;&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;/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;style3&quot; /&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  6월 29일 - 킥오프 미팅과 인터뷰 분석, 페르소나 작성하기&lt;/b&gt;&lt;/h4&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;킥오프 미팅 때는 간단한 팀 문화를 정하였다. 우리는 매일 아침 10시 - 10시 30분에 데일리 회고를, 월요일은 1시 30분부터 3시까지 한 주를 여는 회의를, 금요일은 6시 - 6시 30분까지 한주를 마무리하기 위한 회고를 나누기로 하였다.&lt;/span&gt;&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;&amp;nbsp;&lt;/span&gt;&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;이후, 기존의 서비스를 사용하면서 불편했던 점들과 어떤 기능을 제공하면 좋을지,&lt;/span&gt;&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-07-07 오후 11.44.33.png&quot; data-origin-width=&quot;1062&quot; data-origin-height=&quot;886&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vi70S/btsmR9fTuqT/Fn11t2EGFH2ic4hk2GFmKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vi70S/btsmR9fTuqT/Fn11t2EGFH2ic4hk2GFmKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vi70S/btsmR9fTuqT/Fn11t2EGFH2ic4hk2GFmKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fvi70S%2FbtsmR9fTuqT%2FFn11t2EGFH2ic4hk2GFmKk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;509&quot; height=&quot;425&quot; data-filename=&quot;스크린샷 2023-07-07 오후 11.44.33.png&quot; data-origin-width=&quot;1062&quot; data-origin-height=&quot;886&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;투표를 하면서 생각보다 많은 사용자들이 '&lt;span style=&quot;color: #ef5369;&quot;&gt;기존에 짜여있는, 잘 잡혀있는 커리큘럼'&lt;/span&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-07-07 오후 11.46.57.png&quot; data-origin-width=&quot;1600&quot; data-origin-height=&quot;802&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EEVbu/btsmQqv8zNJ/Sg6LGYApMK4WftMO95KhB1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EEVbu/btsmQqv8zNJ/Sg6LGYApMK4WftMO95KhB1/img.png&quot; data-alt=&quot;극히 일부분만 가져옴&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EEVbu/btsmQqv8zNJ/Sg6LGYApMK4WftMO95KhB1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEEVbu%2FbtsmQqv8zNJ%2FSg6LGYApMK4WftMO95KhB1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;694&quot; height=&quot;348&quot; data-filename=&quot;스크린샷 2023-07-07 오후 11.46.57.png&quot; data-origin-width=&quot;1600&quot; data-origin-height=&quot;802&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;극히 일부분만 가져옴&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후, 인터뷰의 모든 문항에 대해서 전부 취합하면서 직접 읽어보고, 핵심이 될만한 문장에 &lt;span style=&quot;color: #ef5369;&quot;&gt;다 같이 형광펜을 그으면서 토론&lt;/span&gt;하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2-3시간 정도 찐하게 토론을 진행한 다음에, 목적을 도출하는 단계를 가졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;'크리에이터 성향 특징 / 팔로워 성향 특징 / 추가되었으면 좋겠는 기능'&lt;/b&gt; 3가지로 나누어서 형광펜을 그은 부분을 전부 분류하였다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-07-07 오후 11.54.21.png&quot; data-origin-width=&quot;2974&quot; data-origin-height=&quot;1662&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bpfmyb/btsmQgmx1RL/rIV66V4OSDdbHGgQK7q0P1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bpfmyb/btsmQgmx1RL/rIV66V4OSDdbHGgQK7q0P1/img.png&quot; data-alt=&quot;발표 자료였지만 잘 정리된 것 같아서 올리는 ㅎㅎ&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bpfmyb/btsmQgmx1RL/rIV66V4OSDdbHGgQK7q0P1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbpfmyb%2FbtsmQgmx1RL%2FrIV66V4OSDdbHGgQK7q0P1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2974&quot; height=&quot;1662&quot; data-filename=&quot;스크린샷 2023-07-07 오후 11.54.21.png&quot; data-origin-width=&quot;2974&quot; data-origin-height=&quot;1662&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;발표 자료였지만 잘 정리된 것 같아서 올리는 ㅎㅎ&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-07-07 오후 11.54.33.png&quot; data-origin-width=&quot;2924&quot; data-origin-height=&quot;1662&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/JkviJ/btsmRqvr3se/VHV2OdB1eKDdSA8QTFDvIK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/JkviJ/btsmRqvr3se/VHV2OdB1eKDdSA8QTFDvIK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/JkviJ/btsmRqvr3se/VHV2OdB1eKDdSA8QTFDvIK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJkviJ%2FbtsmRqvr3se%2FVHV2OdB1eKDdSA8QTFDvIK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2924&quot; height=&quot;1662&quot; data-filename=&quot;스크린샷 2023-07-07 오후 11.54.33.png&quot; data-origin-width=&quot;2924&quot; data-origin-height=&quot;1662&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 도출한 3가지를 바탕으로 5명의 &lt;span style=&quot;color: #ef5369;&quot;&gt;페르소나를 추출&lt;/span&gt;하고, &lt;span style=&quot;color: #ef5369;&quot;&gt;각 사용자들에 맞는 Needs를 분석&lt;/span&gt;하는 시간을 가졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3명의 팔로워와 2명의 크리에이터로 나누었으며, 팔로워의 경우 '취업 / 입시 / 취미' 정도로 나누고, 크리에이터의 경우 '지식 공유 / 공동체 생활 주도' 성향을 가지도록 설정하였다. 연령대가 너무 넓다고 느낄 수도 있겠지만, '무언가를 함께 하고 싶은 사람들이 계획을 공유하는 주체인지, 혹은 이에 대해 참여하는 성향인지'에 따라서 분리하였기 때문에 구체적으로 잘 잡았다는 생각이 들었다.&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;스크린샷 2023-07-07 오후 11.54.55.png&quot; data-origin-width=&quot;2960&quot; data-origin-height=&quot;1634&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/s5ugF/btsmRmmqfFb/mpQxSihmx94D8fEBpja6T1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/s5ugF/btsmRmmqfFb/mpQxSihmx94D8fEBpja6T1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/s5ugF/btsmRmmqfFb/mpQxSihmx94D8fEBpja6T1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fs5ugF%2FbtsmRmmqfFb%2FmpQxSihmx94D8fEBpja6T1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;742&quot; height=&quot;410&quot; data-filename=&quot;스크린샷 2023-07-07 오후 11.54.55.png&quot; data-origin-width=&quot;2960&quot; data-origin-height=&quot;1634&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후, &lt;b&gt;각 페르소나 별로 어떤 상황인지 묘사하고(GIVEN), 해당 상황에서 어떤 점이 필요한지(WHEN), 그리고 본 서비스를 통해 어떤 점들을 이룰 수 있는지(THEN)&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;span style=&quot;color: #ef5369;&quot;&gt;팔로워 중에서도 무리를 이끼는 것을 좋아하는 사용자인 '골룸 생성자'&lt;/span&gt; (우리는 페르소나에서 사용된 '황시진씨'의 이름인 '시진이'라고 내부적으로 많이 말했다.)라는 새로운 사용자 층을 이끌어낼 수 있었다!&lt;span style=&quot;color: #ef5369;&quot;&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;/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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&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;  6월 30일 - 그라운드 룰과 기획 고도화&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2488&quot; data-origin-height=&quot;1666&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjEmdD/btsmP6q68bj/eM79Rp1BmQ66euUls7vdk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjEmdD/btsmP6q68bj/eM79Rp1BmQ66euUls7vdk0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjEmdD/btsmP6q68bj/eM79Rp1BmQ66euUls7vdk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbjEmdD%2FbtsmP6q68bj%2FeM79Rp1BmQ66euUls7vdk0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;621&quot; height=&quot;416&quot; data-origin-width=&quot;2488&quot; data-origin-height=&quot;1666&quot;/&gt;&lt;/span&gt;&lt;/figure&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;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;'송파구에서 일을 더 잘하는 11가지 방법'을 바탕으로 만든 팀 그라운드 룰이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;일주일이 지난 시점에서 6번과 9번은 정말 착실하게 지키고 있기 때문에 매우 마음에 드는 그라운드 룰이다.&lt;/p&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;a style=&quot;color: #0070d1;&quot; href=&quot;https://github.com/woowacourse-teams/2023-co-kirikiri&quot;&gt;위키&lt;/a&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;사실 무엇보다 8번이 가장 중요하다고 느낀다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;설득에 실패했다면 그 결과에 대한 책임도 내가 져야 한다는 게 얼마나 중요한 것&lt;/span&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-07-07 오후 11.40.29.png&quot; data-origin-width=&quot;1338&quot; data-origin-height=&quot;640&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5FSMv/btsmPGFL7gD/JPNeRIYvXjxw4pgrvOmKt1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5FSMv/btsmPGFL7gD/JPNeRIYvXjxw4pgrvOmKt1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5FSMv/btsmPGFL7gD/JPNeRIYvXjxw4pgrvOmKt1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5FSMv%2FbtsmPGFL7gD%2FJPNeRIYvXjxw4pgrvOmKt1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;328&quot; data-filename=&quot;스크린샷 2023-07-07 오후 11.40.29.png&quot; data-origin-width=&quot;1338&quot; data-origin-height=&quot;640&quot;/&gt;&lt;/span&gt;&lt;/figure&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;p style=&quot;color: #333333; text-align: start;&quot; 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-07-07 오후 11.42.18.png&quot; data-origin-width=&quot;1018&quot; data-origin-height=&quot;722&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cGFHO9/btsmOIREXtZ/XXkk6qhEllJ83LlfuhbvcK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cGFHO9/btsmOIREXtZ/XXkk6qhEllJ83LlfuhbvcK/img.png&quot; data-alt=&quot;늘 3인칭으로 출퇴근을 공유하는 귀여운 팀원들 ^_^&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cGFHO9/btsmOIREXtZ/XXkk6qhEllJ83LlfuhbvcK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcGFHO9%2FbtsmOIREXtZ%2FXXkk6qhEllJ83LlfuhbvcK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;440&quot; height=&quot;312&quot; data-filename=&quot;스크린샷 2023-07-07 오후 11.42.18.png&quot; data-origin-width=&quot;1018&quot; data-origin-height=&quot;722&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;늘 3인칭으로 출퇴근을 공유하는 귀여운 팀원들 ^_^&lt;/figcaption&gt;
&lt;/figure&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 data-ke-size=&quot;size16&quot;&gt;이후 남은 시간 동안에는 또 다시 피그잼을 통해서 기획에 대한 회의를 진행하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;큰 MVP를 생성한 다음, 각 MVP 별 세부 기능을 간단하게 토론해보았다. &lt;b&gt;개발 리소스도 생각하면서 사용자 경험과 주어진 기간&lt;/b&gt;... 3가지를 모두 도출해내려고 회의를 진행하다 보니 기능 1개만 회의하는데도 거의 1시간이 넘게 걸렸었다.&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;부끄럽지만 나는 지금까지 개발을 진행하면서 항상 사용자 경험보다 어떤 기능을 제공할 건지, '하나의 기능 구현'에 초점을 많이 맞췄었는데 그럴 때마다 팀원들이 '&lt;b&gt;이러한 상황에서는 사용자 입장에서 불편하지 않을까요?&lt;/b&gt;' 같은 의문점들을 많이 던져주셔서 조금 더 사용자 경험으로서의 서비스 고도화를 진행할 수 있었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;늘 짧게 게시판 기능이라면 제목 - 내용 - 사진 첨부 - 댓글 같은 정형화된 기능만 생각했던 나 자신에 대해 반성하면서, 하나의 기능을 만들더라도 &lt;span style=&quot;color: #ef5369;&quot;&gt;사용자가 어느 정도의 상호작용을 진행할지, 어떤 상황에서 사용할지, 어떤 불편함을 겪을지 진지하게 고려&lt;/span&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;style3&quot; /&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  7월 3일 - 본격적인 기획 진행&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;이번 주는 데모데이와 기획을 마무리하는 방향으로 정하였다 ✌ &lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-07-08 오전 12.18.30.png&quot; data-origin-width=&quot;910&quot; data-origin-height=&quot;558&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bF3xPn/btsmRsGM8MJ/pcukex9Ni1Go22Lb2DDOUK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bF3xPn/btsmRsGM8MJ/pcukex9Ni1Go22Lb2DDOUK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bF3xPn/btsmRsGM8MJ/pcukex9Ni1Go22Lb2DDOUK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbF3xPn%2FbtsmRsGM8MJ%2Fpcukex9Ni1Go22Lb2DDOUK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;910&quot; height=&quot;558&quot; data-filename=&quot;스크린샷 2023-07-08 오전 12.18.30.png&quot; data-origin-width=&quot;910&quot; data-origin-height=&quot;558&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대략적으로 우리 서비스를 사용하기 위해서 어떤 기능이 필요한지 핵심 MVP를 정하고, 내부적으로 필요한 세부 기능을 정하였다.&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;그러면서 &lt;span style=&quot;color: #ef5369;&quot;&gt;크리에이터는 '로드맵'을 생성하고, 팔로워는 '로드맵'을 따르기 위한 '골룸'이라는 것을 통해 다같이 목표를 이루어나가는 서비스로 방향&lt;/span&gt;을 잡을 수 있었다. 팔로워 중에서 조금 더 리드 성향을 가진 '시진이'가 골룸을 생성하는 사람을 맡으면서 시진이에게 어느 정도의 책임을 줘야 할지 많이 고민했다. 또한, 골룸에서 어떤 기능이 필요한지, 너무 CRUD를 위한 프로젝트가 되는 건 아닌지 주의하면서 기능을 설계하였다. 추후 스프린트 고도화를 진행하면서 조금씩 변화하지 않을까 싶다!&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;style3&quot; /&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  7월 4, 5일 -  커밋 컨벤션과 브랜치 전략, 기능 명세서 작성하기&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-07-08 오전 12.25.49.png&quot; data-origin-width=&quot;752&quot; data-origin-height=&quot;526&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bHa98T/btsmN6yQTYQ/Xd3OLE1LECcFExT2TFtlT1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bHa98T/btsmN6yQTYQ/Xd3OLE1LECcFExT2TFtlT1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bHa98T/btsmN6yQTYQ/Xd3OLE1LECcFExT2TFtlT1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbHa98T%2FbtsmN6yQTYQ%2FXd3OLE1LECcFExT2TFtlT1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;431&quot; height=&quot;301&quot; data-filename=&quot;스크린샷 2023-07-08 오전 12.25.49.png&quot; data-origin-width=&quot;752&quot; data-origin-height=&quot;526&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;&lt;span style=&quot;color: #ef5369;&quot;&gt;커밋 메시지는 angularJS&lt;/span&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;스크린샷 2023-07-08 오전 12.29.06.png&quot; data-origin-width=&quot;2356&quot; data-origin-height=&quot;330&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cg6eS2/btsmOloWf1m/WK0ek8e8PGVzrngQfEXVZk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cg6eS2/btsmOloWf1m/WK0ek8e8PGVzrngQfEXVZk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cg6eS2/btsmOloWf1m/WK0ek8e8PGVzrngQfEXVZk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcg6eS2%2FbtsmOloWf1m%2FWK0ek8e8PGVzrngQfEXVZk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2356&quot; height=&quot;330&quot; data-filename=&quot;스크린샷 2023-07-08 오전 12.29.06.png&quot; data-origin-width=&quot;2356&quot; data-origin-height=&quot;330&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 남은 날들은 기능 명세서를 작성하면서 &lt;span style=&quot;color: #ef5369;&quot;&gt;대략적인 제약 조건을 설정&lt;/span&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;또 다시 이 과정에서 엄청 토론도 하고, 특히 골룸(Goal Room) 파트 정할 때 거의 1시간 넘게 얘기한 것 같다...!&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;/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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  7월 6일 - 데모데이 준비하기&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;대학 4년 내내 PPT 엄청 만들었던 경험을 바탕으로 엄청 열심히 만들었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-07-08 오전 12.35.55.png&quot; data-origin-width=&quot;2570&quot; data-origin-height=&quot;1440&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yAFdt/btsmOntxm4V/gROvSkpigcNnZkJ3uVZw7K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yAFdt/btsmOntxm4V/gROvSkpigcNnZkJ3uVZw7K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yAFdt/btsmOntxm4V/gROvSkpigcNnZkJ3uVZw7K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyAFdt%2FbtsmOntxm4V%2FgROvSkpigcNnZkJ3uVZw7K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2570&quot; height=&quot;1440&quot; data-filename=&quot;스크린샷 2023-07-08 오전 12.35.55.png&quot; data-origin-width=&quot;2570&quot; data-origin-height=&quot;1440&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1차 데모데이에서는 위와 같은 내용이 들어가도록 구성하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 작년 기수분들의 데모데이는 모두 뷰까지 나온 상태여서 걱정을 많이 했는데, 다행히 아직 뷰까지 구성한 팀들이 많이 없었다. 우리는&lt;span style=&quot;color: #ef5369;&quot;&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;개인적인 이야기지만, 옛날에는 백엔드에 대해 완전히 몰랐을 때 어떻게든 팀에 민폐 안 끼치려고 PPT라도 엄청 열심히 만들기 시작한 건데 지금은 재밌어서 만든 게 더 컸다. 내 마음대로 꾸미고, 어떻게 하면 좋은 전달력을 가질지 고민하면서 만들다 보니까 자연스럽게 발표자의 입장에서 생각하게 되고, 우리 서비스에 대한 이해도도 한 번 더 높일 수 있던 시간이었다.&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;무엇보다 하루만에 만든 PPT 치고 퀄이 상당히 마음에 들게 나왔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;실제로 발표할 때도 PPT 칭찬해주셔서 뿌듯&lt;/b&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;style3&quot; /&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  7월 7일 - 1차 데모데이, 디자인 레퍼런스 공유하기&lt;/b&gt;&lt;/h4&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;스크린샷 2023-07-08 오전 12.51.28.png&quot; data-origin-width=&quot;2548&quot; data-origin-height=&quot;1450&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mcVdL/btsmOkDBD5p/FUf0ZZtmAmk03I6OkX2KJ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mcVdL/btsmOkDBD5p/FUf0ZZtmAmk03I6OkX2KJ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mcVdL/btsmOkDBD5p/FUf0ZZtmAmk03I6OkX2KJ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmcVdL%2FbtsmOkDBD5p%2FFUf0ZZtmAmk03I6OkX2KJ1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;688&quot; height=&quot;392&quot; data-filename=&quot;스크린샷 2023-07-08 오전 12.51.28.png&quot; data-origin-width=&quot;2548&quot; data-origin-height=&quot;1450&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;드디어 1차 데모데이 시간이 왔다! 내가 발표자도 아닌데 너무 떨렸었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 앞에서 &lt;b&gt;ppt 넘겨주는 역할 + QnA 도우미&lt;/b&gt;로 갔다. ㅋㅋㅋ 다행히 유튜브에는 안 올라올 것 같다... ^^ ㅎㅎ&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;하지만 무엇보다 밀리가 발표도 너무 잘해주셨고, QnA도 나름 무리없이 잘 말한 것 같아서 만족스러웠다.  &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;스크린샷 2023-07-08 오전 12.52.43.png&quot; data-origin-width=&quot;1500&quot; data-origin-height=&quot;104&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/v81q1/btsmP5FNXRl/TcZiiLzcHhotyKMvvaHLTk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/v81q1/btsmP5FNXRl/TcZiiLzcHhotyKMvvaHLTk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/v81q1/btsmP5FNXRl/TcZiiLzcHhotyKMvvaHLTk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fv81q1%2FbtsmP5FNXRl%2FTcZiiLzcHhotyKMvvaHLTk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1500&quot; height=&quot;104&quot; data-filename=&quot;스크린샷 2023-07-08 오전 12.52.43.png&quot; data-origin-width=&quot;1500&quot; data-origin-height=&quot;104&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 질문은 받았던 QnA 중 하나이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 2차 데모데이 때 구현하려고 했던 내용 중에 사용자 인증 / 인가가 있었는데, 이때 완전한 회원가입 플로우를 다 구축할 생각이었다. (소셜 로그인, 회원가입, 로그인, 비밀번호 암호화, 전화번호 인증... 등등) 하지만, 에자일을 이용하는 다른 팀의 발표를 들으면서 이런 식으로 구축하면 너무 waterfall에 가깝기도 하고, &lt;span style=&quot;color: #ef5369;&quot;&gt;2차 데모데이까지 우리가 보여줘야 하는 핵심 기능을 만들 수 없겠다는 생각이 들었다&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;MVP를 만족할 수 있는 최소한의 기능을 만들고, 그 다음 고도화&lt;/b&gt;를 시켜나간다는 다른 팀의 발표를 듣고 나니까 우리도 조금 방향성을 바꾸는 게 좋겠다는 생각이 들었다. 데모데이가 끝나고 나서 잠깐 이야기 나누어 보았었는데, '&lt;span style=&quot;color: #ef5369;&quot;&gt;한정된 기간 + 사용자의 피드백을 원활하게 받을 수 없는 환경에서도 에자일이 과연 좋은 개발 방법론&lt;/span&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;우선 백엔드 크루들끼리는 회원가입 처리에서 인증 관련이나 소셜 관련은 제외하고, 로드맵 관련 부분, 골룸 관련 부분까지 병렬적으로 기능들을 할당받아서 스프린트를 진행하는 쪽으로 진행하고자 했다. 그리고 2차 대신, 3차 데모데이까지 핵심 MVP가 나올 수 있도록 목표를 세웠다. (아직 스프린트 시작도 안 했고, 얘기를 깊게 나누어 보지는 못했지만 ㅎㅎ 좀 더 고민해보지 않을까?)&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;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;style3&quot; /&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;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;지금까지는 많아야 4명이서 프로젝트를 했었는데, 7명이라는 많은 인원들과 진행하면서 생각의 차이도 있고, 서로 의견도 공유하다 보니 이게 진짜 진한 협업이구나... 라는 생각이 절로 들었다. 프로젝트에서 중요한 거는 단순 기능이 아닌&lt;span style=&quot;color: #ef5369;&quot;&gt; 소프트 스킬이 정말 중요하다&lt;/span&gt;는 것을 또 한 번 느꼈다. 나중에 PM, 디자이너, 기획자까지 포함된다면 얼마나 커질까? 벌써 재밌을 것 같다...&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;아무튼 레벨 3 잘 마무리해서 멋진 완성본을 만들어냈으면 좋겠다  &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>우아한테크코스/레벨 3</category>
      <category>데모데이</category>
      <category>우아한테크코스</category>
      <category>우테코</category>
      <category>코끼리끼리</category>
      <author>dolmeng2</author>
      <guid isPermaLink="true">https://cl8d.tistory.com/116</guid>
      <comments>https://cl8d.tistory.com/116#entry116comment</comments>
      <pubDate>Sat, 8 Jul 2023 01:06:23 +0900</pubDate>
    </item>
    <item>
      <title>[JPA] @GeneratedType - 키 생성 전략 알아보기</title>
      <link>https://cl8d.tistory.com/115</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  JPA 기본 키 생성 전략&amp;nbsp;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA에서 제공하는 기본 키 생성 전략은 크게 5가지로 나누어진다.&lt;br /&gt;- &lt;span style=&quot;color: #ef5369;&quot;&gt;TABLE, SEQUENCE, IDENTITY, UUID, AUTO&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 전략에 대해서 하나씩 알아보도록 하자.&lt;/p&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;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  TABLE&lt;/b&gt;&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;  Indicates that the persistence provider must assign primary keys for the entity using an underlying database table to ensure uniqueness.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 데이터베이스 테이블을 사용하여 엔티티에 기본 키를 할당해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;&lt;b&gt;  엔티티 설정&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1688697211513&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
class User(
    @Id @GeneratedValue(strategy = GenerationType.TABLE)
    val id: Long = 0L
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 가장 간단하게 설정하기 위해서 User 엔티티에 아이디에 대한 필드만 설정해두었다.&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;size18&quot;&gt;&lt;b&gt;  h2 database&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;1280&quot; data-origin-height=&quot;514&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/umDHG/btsmHck0ZHH/YzcwgR4lLeovyo5NYiKjK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/umDHG/btsmHck0ZHH/YzcwgR4lLeovyo5NYiKjK0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/umDHG/btsmHck0ZHH/YzcwgR4lLeovyo5NYiKjK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FumDHG%2FbtsmHck0ZHH%2FYzcwgR4lLeovyo5NYiKjK0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;790&quot; height=&quot;317&quot; data-origin-width=&quot;1280&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;h2에서는 먼저 user 테이블과 &lt;span style=&quot;color: #ef5369;&quot;&gt;시퀀스를 관리하기 위한 hibernate_sequences 테이블&lt;/span&gt;을 만든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 내부적으로 시퀀스 값 자체를 관리하기 위한 next_val과 시퀀스 이름 필드인 sequence_name을 만들며, 생성하는 테이블 각각에 대해 시퀀스를 관리하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후, 가장 처음 시퀀스 값을 초기화하기 위해서 insert를 진행하며, 0으로 초기화를 해두었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;  mysql&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;1280&quot; data-origin-height=&quot;515&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bcF9IS/btsmMrgFoxc/QXvQdFL5KbYFBguMwKyAMK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bcF9IS/btsmMrgFoxc/QXvQdFL5KbYFBguMwKyAMK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bcF9IS/btsmMrgFoxc/QXvQdFL5KbYFBguMwKyAMK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbcF9IS%2FbtsmMrgFoxc%2FQXvQdFL5KbYFBguMwKyAMK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;795&quot; height=&quot;320&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;515&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;mysql 역시 마찬가지로 user와 시퀀스를 관리하기 위한 테이블을 추가한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;innoDB 스토리지 엔진을 사용한다는 점 빼고는 동일하다.&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;size18&quot;&gt;&lt;b&gt;  postgresql&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;1280&quot; data-origin-height=&quot;324&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nkRSK/btsmGKo0jHu/Okg3KTIa8Jq583P6KqWClk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nkRSK/btsmGKo0jHu/Okg3KTIa8Jq583P6KqWClk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nkRSK/btsmGKo0jHu/Okg3KTIa8Jq583P6KqWClk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnkRSK%2FbtsmGKo0jHu%2FOkg3KTIa8Jq583P6KqWClk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;324&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;324&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;  insert&amp;nbsp;시&amp;nbsp;동작&amp;nbsp;과정&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;1280&quot; data-origin-height=&quot;545&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c9NhKA/btsmG9oNO1B/6PG38kZmODAWp3Ioz2b8KK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c9NhKA/btsmG9oNO1B/6PG38kZmODAWp3Ioz2b8KK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c9NhKA/btsmG9oNO1B/6PG38kZmODAWp3Ioz2b8KK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc9NhKA%2FbtsmG9oNO1B%2F6PG38kZmODAWp3Ioz2b8KK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;545&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;545&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 select를 통해서 현재 테이블에 &lt;span style=&quot;color: #ef5369;&quot;&gt;저장되어 있는 시퀀스 값&lt;/span&gt;을 가져온다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때,&lt;b&gt; select for update 구문을 활용하여 락을 걸고 있기 때문에&lt;/b&gt; 외부에서 해당 테이블에 값을 삽입할 수가 없어 데이터의 일관성은 만족된다. 이후, 가져온 값을 바탕으로 시퀀스 값을 update 해주고, 최종적으로 user 엔티티에 대해 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;위의 상황에서는 디폴트로 0이 저장되어 있고, update 시 1을 추가하여 0 + 1 = 1의 값을 가지고, insert 시 업데이트된 값을 통해서 id를 할당한 상황이라고 볼 수 있다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;  @TableGenerator&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1688705313231&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
class User(
    @Id @GeneratedValue(strategy = GenerationType.TABLE, generator = &quot;user_seq_generator&quot;)
    @TableGenerator(name = &quot;user_seq_generator&quot;, table=&quot;user_sequence&quot;,
        pkColumnName = &quot;name&quot;, pkColumnValue = &quot;user_col&quot;, allocationSize = 50)
    val id: Long = 0L
)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1198&quot; data-origin-height=&quot;352&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cvFDOY/btsmHmuQDHS/ky0XtJ92bkyNybox7t8XUk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cvFDOY/btsmHmuQDHS/ky0XtJ92bkyNybox7t8XUk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cvFDOY/btsmHmuQDHS/ky0XtJ92bkyNybox7t8XUk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcvFDOY%2FbtsmHmuQDHS%2Fky0XtJ92bkyNybox7t8XUk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;710&quot; height=&quot;209&quot; data-origin-width=&quot;1198&quot; data-origin-height=&quot;352&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테이블 전략의 경우 &lt;span style=&quot;color: #ef5369;&quot;&gt;@TableGenerator 어노테이션과 함께 사용하면 시퀀스 테이블에 대한 정보를 커스텀&lt;/span&gt;할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;b&gt;name&lt;/b&gt;: 생성하는 제너레이터의 이름 (@GeneratedValue의 인자로 제너레이터를 참조할 때 참조하는 이름이다.)&lt;br /&gt;- &lt;b&gt;table&lt;/b&gt;: 생성하는 제너레이터 테이블의 이름&lt;br /&gt;- &lt;b&gt;pkColumnName&lt;/b&gt;: 제너레이터 테이블에서 지정할 시퀀스 이름 필드명&lt;br /&gt;&lt;b&gt;- pkColumnValue&lt;/b&gt;:&amp;nbsp;시퀀스&amp;nbsp;이름&amp;nbsp;필드에&amp;nbsp;대해서&amp;nbsp;해당&amp;nbsp;테이블을&amp;nbsp;어떤&amp;nbsp;이름으로&amp;nbsp;관리할지&amp;nbsp;지정&lt;br /&gt;- &lt;b&gt;allocationSize&lt;/b&gt;: 사전에 정의한 시퀀스 사이즈. &lt;span style=&quot;color: #ef5369;&quot;&gt;DB에서는&amp;nbsp;할당한&amp;nbsp;사이즈만큼&amp;nbsp;먼저&amp;nbsp;수를&amp;nbsp;증가&lt;/span&gt;시켜두고,&amp;nbsp;트랜잭션이&amp;nbsp;끝날&amp;nbsp;때마다&amp;nbsp;사이즈만큼&amp;nbsp;추가한다.&amp;nbsp;(기본값이&amp;nbsp;50이기&amp;nbsp;때문에&amp;nbsp;필요하다면&amp;nbsp;1로&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;pre id=&quot;code_1688705381132&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
@Transactional
fun test() {
    userRepository.save(User())
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;514&quot; data-origin-height=&quot;656&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b4VUvv/btsmJDo5IFn/pE4gNefsStPQA5I09LJzLk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b4VUvv/btsmJDo5IFn/pE4gNefsStPQA5I09LJzLk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b4VUvv/btsmJDo5IFn/pE4gNefsStPQA5I09LJzLk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb4VUvv%2FbtsmJDo5IFn%2FpE4gNefsStPQA5I09LJzLk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;326&quot; height=&quot;416&quot; data-origin-width=&quot;514&quot; data-origin-height=&quot;656&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로, allocationSize에 대해 테스트를 하기 위해 위와 같이 사용자 정보를 저장해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선, 앞서 봤던 것처럼 시퀀스 정보를 select 후 해당 값을 update 한 것을 볼 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;558&quot; data-origin-height=&quot;116&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5SMwG/btsmOJ2ISAA/UKOd9hiNSjBtskDNKpgtAk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5SMwG/btsmOJ2ISAA/UKOd9hiNSjBtskDNKpgtAk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5SMwG/btsmOJ2ISAA/UKOd9hiNSjBtskDNKpgtAk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5SMwG%2FbtsmOJ2ISAA%2FUKOd9hiNSjBtskDNKpgtAk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;116&quot; data-origin-width=&quot;558&quot; data-origin-height=&quot;116&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 sequence 정보를 확인해보면, user 하나만 저장했음에도 &lt;span style=&quot;color: #ef5369;&quot;&gt;할당한 크기(50)만큼 값이 추가된 것&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;만약,&amp;nbsp;다시&amp;nbsp;한&amp;nbsp;번&amp;nbsp;테스트&amp;nbsp;코드를&amp;nbsp;실행하게&amp;nbsp;된다면&amp;nbsp;다음&amp;nbsp;sequence는&amp;nbsp;어떻게&amp;nbsp;될까?&amp;nbsp;(ddl는&amp;nbsp;update로&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;550&quot; data-origin-height=&quot;110&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bHHPyz/btsmKU5QvSt/OeaSfCpueEgQ7NS8IDDTQ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bHHPyz/btsmKU5QvSt/OeaSfCpueEgQ7NS8IDDTQ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bHHPyz/btsmKU5QvSt/OeaSfCpueEgQ7NS8IDDTQ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbHHPyz%2FbtsmKU5QvSt%2FOeaSfCpueEgQ7NS8IDDTQ1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;550&quot; height=&quot;110&quot; data-origin-width=&quot;550&quot; data-origin-height=&quot;110&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때는 db에 next_val이 100으로 저장이 되어 있다. (기존 next_val 값 50 + allocationSize 50 = 100)&lt;/p&gt;
&lt;pre id=&quot;code_1688708765810&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
@Transactional
fun test() {
    // 한 번 더 실행했을 때의 결과
    val user = userRepository.save(User())
    assertThat(user.id).isEqualTo(2)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, user의 pk 값은 2로 나오는 것을 확인할 수 있다. 이는 내부적으로 받아온 next_val의 값과 allocationSize 값을 바탕으로 pk를 계산한 값이다. 레코드 1개를 삽입할 때, &lt;span style=&quot;color: #ef5369;&quot;&gt;처음 select 시 얻어온 (next_val - allocation_size) + 1&lt;/span&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;style3&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;br /&gt;&lt;b&gt;  SEQUENCE&lt;/b&gt;&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;  Indicates that the persistence provider must assign primary keys for the entity using a database sequence.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터베이스에서 제공하는 &lt;span style=&quot;color: #ef5369;&quot;&gt;시퀀스를 사용하여&lt;/span&gt; 엔티티의 기본 키를 할당해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때, 시퀀스를&amp;nbsp;지원하지&amp;nbsp;않는&amp;nbsp;DB라면&amp;nbsp;&lt;span style=&quot;color: #ef5369;&quot;&gt;테이블&amp;nbsp;전략을&amp;nbsp;사용&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;  엔티티 설정&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1688713690453&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
class User(
    @Id @GeneratedValue(strategy = GenerationType.SEQUENCE)
    val id: Long = 0L
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;  h2 database&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;1004&quot; data-origin-height=&quot;310&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Q730P/btsmOn644YH/XQ6Ucy8yfvMkai4Q8u35Uk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Q730P/btsmOn644YH/XQ6Ucy8yfvMkai4Q8u35Uk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Q730P/btsmOn644YH/XQ6Ucy8yfvMkai4Q8u35Uk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQ730P%2FbtsmOn644YH%2FXQ6Ucy8yfvMkai4Q8u35Uk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;660&quot; height=&quot;204&quot; data-origin-width=&quot;1004&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;h2의 경우 sequence를 지원하기 때문에 &amp;lsquo;create sequence&amp;rsquo;라는 것을 통해서 sequence 관리한다.&lt;br /&gt;이때, 디폴트로 &lt;b&gt;시퀀스 값은 1부터 시작하여 50씩 증가&lt;/b&gt;한다. 즉, 하나의 트랜잭션이 끝나면 50개 이하를 삽입하더라도 50씩 시퀀스 값이 증가하기 때문에 중간중간 비어있는 PK 값을 가질 수 있다는 것이 특징이다.&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;br /&gt;&lt;b&gt;  mysql&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;664&quot; data-origin-height=&quot;486&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bBSqoC/btsmOJhG6jQ/Khk0TEpbthyJKJjYqBQin1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bBSqoC/btsmOJhG6jQ/Khk0TEpbthyJKJjYqBQin1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bBSqoC/btsmOJhG6jQ/Khk0TEpbthyJKJjYqBQin1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbBSqoC%2FbtsmOJhG6jQ%2FKhk0TEpbthyJKJjYqBQin1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;477&quot; height=&quot;349&quot; data-origin-width=&quot;664&quot; data-origin-height=&quot;486&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;⭐️ 그러나, mysql에서는 sequence를 지원하지 않기 때문에&lt;span style=&quot;color: #ef5369;&quot;&gt; 테이블 전략처럼 user_seq라는 테이블이 생성&lt;/span&gt;된다.&lt;br /&gt;이때, 시퀀스 테이블에 대한 네이밍은 엔티티명 + _seq와 같은 형태로 동작한다.&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;size18&quot;&gt;&lt;b&gt;  postgresql&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;980&quot; data-origin-height=&quot;312&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/m6abl/btsmOkbLXNp/X9R2MhJRZK3MEydYCdgni1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/m6abl/btsmOkbLXNp/X9R2MhJRZK3MEydYCdgni1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/m6abl/btsmOkbLXNp/X9R2MhJRZK3MEydYCdgni1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fm6abl%2FbtsmOkbLXNp%2FX9R2MhJRZK3MEydYCdgni1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;602&quot; height=&quot;192&quot; data-origin-width=&quot;980&quot; data-origin-height=&quot;312&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;h2와 마찬가지로 postgresql 역시 sequence를 지원하기 때문에 create sequence를 통해서 시퀀스를 생성한다.&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;br /&gt;&lt;b&gt;  insert 시 동작 과정&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;1280&quot; data-origin-height=&quot;263&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mRn3S/btsmOBqN8Q3/2KkVTz8H2iGMk9WgYNwjuk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mRn3S/btsmOBqN8Q3/2KkVTz8H2iGMk9WgYNwjuk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mRn3S/btsmOBqN8Q3/2KkVTz8H2iGMk9WgYNwjuk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmRn3S%2FbtsmOBqN8Q3%2F2KkVTz8H2iGMk9WgYNwjuk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;263&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;263&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저, 시퀀스로부터 다음 값을 받아오는 select 쿼리가 발생한다. (&quot;next value for&quot; = 1이라는 값을 받아온다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전의 TABLE 전략과 다르게 &lt;span style=&quot;color: #ef5369;&quot;&gt;'그 다음 insert 시 사용할 시퀀스 값'을 저장&lt;/span&gt;하기 때문에 시퀀스에 대한 별도의 update를 쿼리로 관리하는 것이 아닌 내부적으로 관리된다고 추측된다. (물론 MySQL이라면 테이블 전략이기 때문에 update 쿼리 발생)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한, 추후 나올 IDENTITY 전략과 다르게 &lt;span style=&quot;color: #ef5369;&quot;&gt;persist 시 insert를 하지 않아도 돼서 트랜잭션 커밋 시 insert&lt;/span&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;size18&quot;&gt;&lt;b&gt;  @SequenceGenerator&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1688714824067&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
class User(
    @Id @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = &quot;user_seq_generator&quot;)
    @SequenceGenerator(name = &quot;user_seq_generator&quot;, sequenceName = &quot;user_sequence&quot;,
        initialValue = 10, allocationSize = 50)
    val id: Long = 0L
)&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;@SequenceGenerator를&amp;nbsp;활용하면&amp;nbsp;시퀀스에&amp;nbsp;대한&amp;nbsp;정보를&amp;nbsp;설정해줄&amp;nbsp;수&amp;nbsp;있다.&lt;br /&gt;- &lt;b&gt;name&lt;/b&gt;: 생성하는 제너레이터의 이름 (@GeneratedValue의 인자로 제너레이터를 참조할 때 참조하는 이름이다.)&lt;br /&gt;- &lt;b&gt;sequenceName&lt;/b&gt;: 생성하는 시퀀스의 이름&lt;br /&gt;- &lt;b&gt;initialValue&lt;/b&gt;: 시퀀스의 초기값&lt;br /&gt;- &lt;b&gt;allocationSize&lt;/b&gt;: 사전에 정의한 시퀀스 사이즈. DB에서는 할당한 사이즈만큼 먼저 수를 증가시켜두고, 트랜잭션이 끝날 때마다 사이즈만큼 추가한다.&lt;br /&gt;마찬가지로 위의 정보 외에도 다양한 설정값들을 세팅할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;240&quot; data-origin-height=&quot;212&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bNAsj4/btsmN3VtcnQ/ForkuU7zqp3VeOWkKMZqFk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bNAsj4/btsmN3VtcnQ/ForkuU7zqp3VeOWkKMZqFk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bNAsj4/btsmN3VtcnQ/ForkuU7zqp3VeOWkKMZqFk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbNAsj4%2FbtsmN3VtcnQ%2FForkuU7zqp3VeOWkKMZqFk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;240&quot; height=&quot;212&quot; data-origin-width=&quot;240&quot; data-origin-height=&quot;212&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;키 할당 전략은 table 때와 똑같이 (next_val - allocation_size) + 1와 같이 설정된다.&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;style3&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;br /&gt;&lt;b&gt;  IDENTITY&lt;/b&gt;&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;  Indicates that the persistence provider must assign primary keys for&amp;nbsp;the&amp;nbsp;entity&amp;nbsp;using&amp;nbsp;a&amp;nbsp;database&amp;nbsp;identity&amp;nbsp;column.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 데이터베이스의 auto-increment 열을 사용하여 엔티티에 기본 키를 할당해야 한다.&lt;br /&gt;레코드가&amp;nbsp;새롭게&amp;nbsp;추가될&amp;nbsp;때마다&amp;nbsp;&lt;b&gt;자동으로&amp;nbsp;고유값을&amp;nbsp;생성하고&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;size18&quot;&gt;&lt;br /&gt;&lt;b&gt;  엔티티 설정&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1688715043531&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
class User(
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long = 0L
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;  h2 database&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;882&quot; data-origin-height=&quot;218&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ccghAV/btsmOUqbz3j/XiKjxm9SJNK9IyUDWd0h3K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ccghAV/btsmOUqbz3j/XiKjxm9SJNK9IyUDWd0h3K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ccghAV/btsmOUqbz3j/XiKjxm9SJNK9IyUDWd0h3K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FccghAV%2FbtsmOUqbz3j%2FXiKjxm9SJNK9IyUDWd0h3K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;674&quot; height=&quot;167&quot; data-origin-width=&quot;882&quot; data-origin-height=&quot;218&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;별도의 시퀀스 테이블을 생성하지 않고, &lt;span style=&quot;color: #ef5369;&quot;&gt;pk로 사용된 컬럼에 as identity라는 이름이 들어간다&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;size18&quot;&gt;&lt;br /&gt;&lt;b&gt;  mysql&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;720&quot; data-origin-height=&quot;214&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zeV1E/btsmOTrfYue/IE8WnvcGwckyA4I2eQnzm0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zeV1E/btsmOTrfYue/IE8WnvcGwckyA4I2eQnzm0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zeV1E/btsmOTrfYue/IE8WnvcGwckyA4I2eQnzm0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzeV1E%2FbtsmOTrfYue%2FIE8WnvcGwckyA4I2eQnzm0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;633&quot; height=&quot;188&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;214&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;h2와 거의 비슷하지만 &lt;span style=&quot;color: #ef5369;&quot;&gt;auto_increment&lt;/span&gt;가 pk 컬럼에 붙게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;cf. 참고로 innoDB에서는 auto_increment를 위한 자동 증가 락이라는 것을 통해서 해당 값을 관리한다.&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;br /&gt;&lt;b&gt;  postgresql&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;562&quot; data-origin-height=&quot;228&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bqGykH/btsmOUwYd2l/XTJSHVp6tKkE3dYVHTGAe0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bqGykH/btsmOUwYd2l/XTJSHVp6tKkE3dYVHTGAe0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bqGykH/btsmOUwYd2l/XTJSHVp6tKkE3dYVHTGAe0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbqGykH%2FbtsmOUwYd2l%2FXTJSHVp6tKkE3dYVHTGAe0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;439&quot; height=&quot;178&quot; data-origin-width=&quot;562&quot; data-origin-height=&quot;228&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;postgresql의 경우 &lt;span style=&quot;color: #ef5369;&quot;&gt;bigserial&lt;/span&gt;이라는 값을 통해서 관리한다.&lt;br /&gt;이는 postgresql에서&amp;nbsp;사용하는&amp;nbsp;자동&amp;nbsp;증가&amp;nbsp;타입으로,&amp;nbsp;컬럼이&amp;nbsp;삽입될&amp;nbsp;때마다&amp;nbsp;1씩&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;  insert 시 동작 과정&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;220&quot; data-origin-height=&quot;312&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bqLSh2/btsmOAMh2Dw/JZvIzgQRkmKrRw6JjW2iY0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bqLSh2/btsmOAMh2Dw/JZvIzgQRkmKrRw6JjW2iY0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bqLSh2/btsmOAMh2Dw/JZvIzgQRkmKrRw6JjW2iY0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbqLSh2%2FbtsmOAMh2Dw%2FJZvIzgQRkmKrRw6JjW2iY0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;220&quot; height=&quot;312&quot; data-origin-width=&quot;220&quot; data-origin-height=&quot;312&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 본 테이블, &lt;span style=&quot;color: #ef5369;&quot;&gt;시퀀스 전략과 다르게 select 쿼리가 없이 바로 insert 쿼리가 발생&lt;/span&gt;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때, ()와 같이 빈 값이 들어가는 것을 볼 수 있는데,&lt;b&gt; 이는 삽입하는 시점에는 id 값을 알 수 없기 때문에&lt;/b&gt; 빈값 (java라면 null)이 들어가는 것이다. 즉, 삽입 시점의 id 값을 애초에 모르니까 삽입 전에 select 쿼리를 날릴 수 없는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;IDENTITY&amp;nbsp;전략&amp;nbsp;사용&amp;nbsp;시&lt;span style=&quot;color: #ef5369;&quot;&gt; 트랜잭션 커밋 시점이 아닌, persist 시점에 쿼리가 발생하여,&amp;nbsp;쓰기 지연이 불가능&lt;/span&gt;하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로&amp;nbsp;JPA의&amp;nbsp;영속성&amp;nbsp;컨텍스트에서&amp;nbsp;객체를&amp;nbsp;관리하기&amp;nbsp;위해서는&amp;nbsp;@Id&amp;nbsp;속성이&amp;nbsp;꼭&amp;nbsp;필요한데&amp;nbsp;auto_increment의&amp;nbsp;경우&lt;b&gt;&amp;nbsp;DB에&amp;nbsp;직접&amp;nbsp;레코드를&amp;nbsp;insert를&amp;nbsp;한&amp;nbsp;뒤에&amp;nbsp;ID&amp;nbsp;값을&amp;nbsp;알&amp;nbsp;수&amp;nbsp;있기&amp;nbsp;때문에&lt;/b&gt; persist() 시점에 insert를 진행하여 식별자를 미리 알아둔다. 이후, 알아본 값을 영속성 컨텍스트의 1차 캐시 내부에 저장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;  IDENTITY with generator&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 속성을 같이 쓰면 어떻게 동작할지 궁금해서 실험해보았다.&lt;/p&gt;
&lt;pre id=&quot;code_1688715405442&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
class User(
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY, generator = &quot;user_seq_generator&quot;)
    @SequenceGenerator(name = &quot;user_seq_generator&quot;, sequenceName = &quot;user_sequence&quot;,
        initialValue = 10, allocationSize = 50)
    val id: Long = 0L
)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;768&quot; data-origin-height=&quot;486&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ecoaNP/btsmOmtJ0UW/Tikj8CTIRn1TpNKZepgPB1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ecoaNP/btsmOmtJ0UW/Tikj8CTIRn1TpNKZepgPB1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ecoaNP/btsmOmtJ0UW/Tikj8CTIRn1TpNKZepgPB1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FecoaNP%2FbtsmOmtJ0UW%2FTikj8CTIRn1TpNKZepgPB1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;520&quot; height=&quot;329&quot; data-origin-width=&quot;768&quot; data-origin-height=&quot;486&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확인 결과, &lt;span style=&quot;color: #ef5369;&quot;&gt;시퀀스 옵션이 더 우선으로&lt;/span&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;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;br /&gt;&lt;b&gt;  AUTO&lt;/b&gt;&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;  Indicates that the persistence provider should pick an appropriate strategy for the particular database. &lt;br /&gt;The AUTO generation strategy may expect a database resource to exist, or it may attempt to create one. &lt;br /&gt;A vendor may provide documentation on how to create such resources in the event that it does not support schema generation or cannot create the schema resource at runtime.&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;size18&quot;&gt;&lt;br /&gt;&lt;b&gt;  엔티티 설정&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1688715521172&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
class User(
    @Id @GeneratedValue(strategy = GenerationType.AUTO)
    val id: Long = 0L
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;  h2 database&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;990&quot; data-origin-height=&quot;312&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ci8Bgv/btsmOiLU5Sl/2zqjPocGzhXLgm9BoUghA0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ci8Bgv/btsmOiLU5Sl/2zqjPocGzhXLgm9BoUghA0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ci8Bgv/btsmOiLU5Sl/2zqjPocGzhXLgm9BoUghA0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fci8Bgv%2FbtsmOiLU5Sl%2F2zqjPocGzhXLgm9BoUghA0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;730&quot; height=&quot;230&quot; data-origin-width=&quot;990&quot; data-origin-height=&quot;312&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;h2의 경우 시퀀스를 지원하기 때문에 시퀀스 전략을 자동으로 채택한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;  mysql&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;662&quot; data-origin-height=&quot;486&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bcwbrB/btsmOAr1rSy/dORykWEJrTrutTTfhCSIB0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bcwbrB/btsmOAr1rSy/dORykWEJrTrutTTfhCSIB0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bcwbrB/btsmOAr1rSy/dORykWEJrTrutTTfhCSIB0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbcwbrB%2FbtsmOAr1rSy%2FdORykWEJrTrutTTfhCSIB0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;429&quot; height=&quot;315&quot; data-origin-width=&quot;662&quot; data-origin-height=&quot;486&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시퀀스 전략을 지원하지 않는 mysql의 경우 테이블 전략을 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;&lt;b&gt;  postgresql&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;970&quot; data-origin-height=&quot;314&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HaXAC/btsmMJDAnZ6/B5KThp4MA87nirT0XgXtH1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HaXAC/btsmMJDAnZ6/B5KThp4MA87nirT0XgXtH1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HaXAC/btsmMJDAnZ6/B5KThp4MA87nirT0XgXtH1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHaXAC%2FbtsmMJDAnZ6%2FB5KThp4MA87nirT0XgXtH1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;677&quot; height=&quot;219&quot; data-origin-width=&quot;970&quot; data-origin-height=&quot;314&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;h2와 마찬가지로 시퀀스 전략을 사용한다.&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;br /&gt;&lt;b&gt;  SEQUENCE vs AUTO&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지만 봤다면 시퀀스 전략이나 auto나 별다른 게 없다고 생각이 들 것이다.&lt;br /&gt;하지만,&lt;span style=&quot;color: #ef5369;&quot;&gt;&amp;nbsp;pk가&amp;nbsp;uuid인&amp;nbsp;&amp;lsquo;엔티티를&amp;nbsp;저장할&amp;nbsp;때&amp;rsquo;&amp;nbsp;시퀀스&amp;nbsp;전략의&amp;nbsp;경우&amp;nbsp;오류가&amp;nbsp;발생&lt;/span&gt;하고,&amp;nbsp;auto의&amp;nbsp;경우&amp;nbsp;정상적으로&amp;nbsp;동작한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때, 단순히 테이블 생성 자체는 정상적으로 동작한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;sequence&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1688715883551&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
class User(
    @Id @GeneratedValue(strategy = GenerationType.SEQUENCE)
    val id: UUID = UUID.randomUUID()
)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1040&quot; data-origin-height=&quot;318&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mNhgQ/btsmPyUz3s2/52ajQjt9n5BckyvJN8yn61/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mNhgQ/btsmPyUz3s2/52ajQjt9n5BckyvJN8yn61/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mNhgQ/btsmPyUz3s2/52ajQjt9n5BckyvJN8yn61/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmNhgQ%2FbtsmPyUz3s2%2F52ajQjt9n5BckyvJN8yn61%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;693&quot; height=&quot;212&quot; data-origin-width=&quot;1040&quot; data-origin-height=&quot;318&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;h2로 테스트를 진행했기 때문에 시퀀스 테이블을 생성하는 것을 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때, user 테이블의 id가 uuid로 설정된 것을 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1688716275415&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;userRepository.save(User())&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;368&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dom6Gs/btsmOJ3fu4V/KfC7fNEgekyffdBoanhWP1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dom6Gs/btsmOJ3fu4V/KfC7fNEgekyffdBoanhWP1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dom6Gs/btsmOJ3fu4V/KfC7fNEgekyffdBoanhWP1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdom6Gs%2FbtsmOJ3fu4V%2FKfC7fNEgekyffdBoanhWP1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;368&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;368&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 User 객체의 id에 기본값을 정의하였기 때문에 &lt;b&gt;select 쿼리를 통해서&lt;/b&gt; 이미 객체가 존재하는지 확인한 다음, &lt;span style=&quot;color: #ef5369;&quot;&gt;존재하지 않으면 user_seq의 시퀀스 값으로부터 다음 값을 가져온다&lt;/span&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Unknown integral data type for ids: java.util.UUID&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, 이후 &lt;b&gt;UUID 타입으로는 save가 되지 않는다&lt;/b&gt;는 오류가 발생한다.&lt;br /&gt;이는,&amp;nbsp;초기&amp;nbsp;sequence&amp;nbsp;정의를&amp;nbsp;보면&amp;nbsp;1부터&amp;nbsp;시작하여&amp;nbsp;50씩&amp;nbsp;증가하는&amp;nbsp;디폴트&amp;nbsp;설정을&amp;nbsp;가지고&amp;nbsp;있지만,&amp;nbsp;pk&amp;nbsp;값이&amp;nbsp;uuid로&amp;nbsp;되어&amp;nbsp;있다&amp;nbsp;보니&amp;nbsp;u&lt;b&gt;uid에&amp;nbsp;대해&amp;nbsp;증가&amp;nbsp;연산을&amp;nbsp;할&amp;nbsp;수&amp;nbsp;없어&amp;nbsp;데이터&amp;nbsp;타입에&amp;nbsp;대한&amp;nbsp;오류가&amp;nbsp;발생&lt;/b&gt;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;&lt;b&gt;auto&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1688716369646&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
class User(
    @Id @GeneratedValue(strategy = GenerationType.AUTO)
    val id: UUID = UUID.randomUUID()
)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;530&quot; data-origin-height=&quot;230&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbMKSg/btsmOjqEmhC/2c2vjbsCydjZskwxHo7PxK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbMKSg/btsmOjqEmhC/2c2vjbsCydjZskwxHo7PxK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbMKSg/btsmOjqEmhC/2c2vjbsCydjZskwxHo7PxK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbbMKSg%2FbtsmOjqEmhC%2F2c2vjbsCydjZskwxHo7PxK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;436&quot; height=&quot;189&quot; data-origin-width=&quot;530&quot; data-origin-height=&quot;230&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시퀀스를 지원하는 h2임에도, 시퀀스를 생성하지 않고 &lt;b&gt;단순히 pk가 UUID인 user 테이블을 생성하는 것&lt;/b&gt;을 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1688716442993&quot; class=&quot;isbl&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;userRepository.save(User())&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;443&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/V49Ho/btsmOjYt1b6/qzkKVRzozBqNu2jsbB2JR1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/V49Ho/btsmOjYt1b6/qzkKVRzozBqNu2jsbB2JR1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/V49Ho/btsmOjYt1b6/qzkKVRzozBqNu2jsbB2JR1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FV49Ho%2FbtsmOjYt1b6%2FqzkKVRzozBqNu2jsbB2JR1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;443&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;443&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 아이디 값에 대해서 UUID.randomUUID()를 활용하여 기본값을 설정하게 되면, 이미 존재하는 레코드인지 확인하기 위해 저장 전에 select 쿼리가 발생하고 이후 insert 쿼리가 날라간다. 이때, 레코드 삽입 시&amp;nbsp;&lt;span style=&quot;color: #ef5369;&quot;&gt;해당&amp;nbsp;PK&amp;nbsp;값을&amp;nbsp;증가시키면서&amp;nbsp;관리할&amp;nbsp;필요가&amp;nbsp;없기&amp;nbsp;때문에&lt;/span&gt;&amp;nbsp;오류가&amp;nbsp;발생하지&amp;nbsp;않고&amp;nbsp;잘&amp;nbsp;지정되는&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;size18&quot;&gt;&lt;br /&gt;&lt;b&gt;identity&amp;nbsp;(번외)&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1688716504405&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
class User(
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long = 0L
)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1248&quot; data-origin-height=&quot;496&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Au7Do/btsmNNZMRv0/vFJqAL60G4L54NOdDCKvq1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Au7Do/btsmNNZMRv0/vFJqAL60G4L54NOdDCKvq1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Au7Do/btsmNNZMRv0/vFJqAL60G4L54NOdDCKvq1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAu7Do%2FbtsmNNZMRv0%2FvFJqAL60G4L54NOdDCKvq1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;764&quot; height=&quot;304&quot; data-origin-width=&quot;1248&quot; data-origin-height=&quot;496&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로, identity로 설정했을 때는&lt;span style=&quot;color: #ef5369;&quot;&gt; save가 아닌 테이블을 생성하는 시점부터&lt;/span&gt; 오류가 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;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;⭐️ 결과적으로 식별자 타입이 UUID면 UUID를 식별자로 사 용하고, 숫자면서 시퀀스를 지원하면 시퀀스로, 아니면 테이블로 사용한다고 생각하면 된다.&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;style3&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;br /&gt;&lt;b&gt;  UUID&lt;/b&gt;&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;  Indicates that the persistence provider must assign primary keys for the entity by generating an RFC 4122 Univerally Unique IDentifier.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- RFC 4122 Universally Unique Identifier를 통해서 엔티티의 기본 키를 할당할 수 있다.&lt;br /&gt;PK 필드 자체를 UUID로 설정하고, AUTO 전략을 사용하는 것과 동일하게 동작한다.&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;br /&gt;&lt;b&gt;  엔티티 설정&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1688790508042&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
class User(
    @Id @GeneratedValue(strategy = GenerationType.UUID)
    val id: Long = 0L
)&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;  h2 database&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;506&quot; data-origin-height=&quot;218&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/72Tjx/btsmOmgiCPd/ZYgfwd8JUslDqN7w4HqC30/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/72Tjx/btsmOmgiCPd/ZYgfwd8JUslDqN7w4HqC30/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/72Tjx/btsmOmgiCPd/ZYgfwd8JUslDqN7w4HqC30/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F72Tjx%2FbtsmOmgiCPd%2FZYgfwd8JUslDqN7w4HqC30%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;415&quot; height=&quot;179&quot; data-origin-width=&quot;506&quot; data-origin-height=&quot;218&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;id 값의 타입으로 uuid가 설정되는 것을 볼 수 있다. 이때 별도의 시퀀스가 있다든지,&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;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;  mysql&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;556&quot; data-origin-height=&quot;222&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/czxNhe/btsmOdRPCyw/C01HMkqbsf2qsvGuiUMsqK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/czxNhe/btsmOdRPCyw/C01HMkqbsf2qsvGuiUMsqK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/czxNhe/btsmOdRPCyw/C01HMkqbsf2qsvGuiUMsqK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FczxNhe%2FbtsmOdRPCyw%2FC01HMkqbsf2qsvGuiUMsqK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;493&quot; height=&quot;197&quot; data-origin-width=&quot;556&quot; data-origin-height=&quot;222&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;mysql의 경우 binary라는 타입을 활용하여 필드가 생성이 된다.&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;size18&quot;&gt;&lt;br /&gt;&lt;b&gt;  postgresql&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;488&quot; data-origin-height=&quot;220&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhWJsx/btsmOUcRoiS/jhnBHNR0SF8kurUfMnQS2K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhWJsx/btsmOUcRoiS/jhnBHNR0SF8kurUfMnQS2K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhWJsx/btsmOUcRoiS/jhnBHNR0SF8kurUfMnQS2K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbhWJsx%2FbtsmOUcRoiS%2FjhnBHNR0SF8kurUfMnQS2K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;435&quot; height=&quot;196&quot; data-origin-width=&quot;488&quot; data-origin-height=&quot;220&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;h2와 동일하게 uuid라는 필드값으로 생성된다.&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;style3&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;br /&gt;&lt;b&gt;  @IdGeneratorType&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@TableGenerator, @SequenceGenerator 외에도 범용적으로 사용할 수 있는 &lt;b&gt;@GenericGenerator&lt;/b&gt;라는 어노테이션이 존재한다.&lt;br /&gt;하지만,&amp;nbsp;공식&amp;nbsp;문서에서&lt;span style=&quot;color: #ef5369;&quot;&gt;&amp;nbsp;@IdGeneratorType을&amp;nbsp;대신&amp;nbsp;사용하는&amp;nbsp;것을&amp;nbsp;권고&lt;/span&gt;하였기&amp;nbsp;때문에&amp;nbsp;사실상&amp;nbsp;deprecated&amp;nbsp;되었다고&amp;nbsp;생각한다.&lt;br /&gt;@IdGeneratorType을&amp;nbsp;사용하면&amp;nbsp;어노테이션이&amp;nbsp;아닌&amp;nbsp;인터페이스를&amp;nbsp;활용하여&amp;nbsp;조금&amp;nbsp;더&amp;nbsp;세세하게&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;pre id=&quot;code_1688717044261&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class CustomSequenceGenerator implements IdentifierGenerator {

	public CustomSequenceGenerator(
			Sequence config,
			Member annotatedMember,
			CustomIdGeneratorCreationContext context) {
		//...
	}

	@Override
	public Object generate(
			SharedSessionContractImplementor session,
			Object object) {
		//...
}

@IdGeneratorType( CustomSequenceGenerator.class )
@Target({METHOD, FIELD})
@Retention(RUNTIME)
public @interface Sequence {
	String name();
	int startWith() default 1;
	int incrementBy() default 50;
	Class&amp;lt;? extends Optimizer&amp;gt; optimizer() default Optimizer.class;
}&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;사실 직접 사용할 일은 없을 것 같아서 그냥 Member 위에 @Sequence라는 커스텀 어노테이션이 적용되었을 경우를 보여주는 예제 코드를 가져왔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1688717072266&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class TableGenerator implements PersistentIdentifierGenerator {
	...
  @Override
  public Object generate(final SharedSessionContractImplementor session, final Object obj) {
    final SqlStatementLogger statementLogger = session.getFactory().getServiceRegistry()
            .getService( JdbcServices.class )
            .getSqlStatementLogger();
    final SessionEventListenerManager statsCollector = session.getEventListenerManager();

    return optimizer.generate(
            new AccessCallback() {
              @Override
              public IntegralDataTypeHolder getNextValue() {
                return session.getTransactionCoordinator().createIsolationDelegate().delegateWork(
                        new AbstractReturningWork&amp;lt;&amp;gt;() {
                          @Override
                          public IntegralDataTypeHolder execute(Connection connection) throws SQLException {
                            return nextValue( connection, statementLogger, statsCollector );
                          }
                        },
                        true
                );
              }

              @Override
              public String getTenantIdentifier() {
                return session.getTenantIdentifier();
              }
            }
    );
  }
}&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;실제로&amp;nbsp;&lt;b&gt;@TableGenerator&amp;nbsp;역시&amp;nbsp;IdentifierGenerator&amp;nbsp;인터페이스를&amp;nbsp;활용하여&lt;/b&gt;&amp;nbsp;구현되어&amp;nbsp;있는&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;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;br /&gt;&lt;b&gt;  아예 아무것도 붙이지 않는다면?&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1688717123144&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
class User(
    @Id
    val id: Long = 0L
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@GenerateType을&amp;nbsp;아예&amp;nbsp;붙이지&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;size18&quot;&gt;&lt;br /&gt;&lt;b&gt;  h2 database&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;518&quot; data-origin-height=&quot;218&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wyNwa/btsmOA6PsZ6/BWRXBcD1q7kVAf5wdQLKNk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wyNwa/btsmOA6PsZ6/BWRXBcD1q7kVAf5wdQLKNk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wyNwa/btsmOA6PsZ6/BWRXBcD1q7kVAf5wdQLKNk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwyNwa%2FbtsmOA6PsZ6%2FBWRXBcD1q7kVAf5wdQLKNk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;466&quot; height=&quot;196&quot; data-origin-width=&quot;518&quot; data-origin-height=&quot;218&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 user table만 생성하고, id에도 auto_increment 같이 관리하는 조건이 들어가지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 상태로 삽입을 하게 되면 다음과 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1688717176716&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;userRepository.save(User())&lt;/code&gt;&lt;/pre&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;1280&quot; data-origin-height=&quot;196&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b1jAK8/btsmOIQUoVp/5fCuog05Mqbea9NdGqDqb0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b1jAK8/btsmOIQUoVp/5fCuog05Mqbea9NdGqDqb0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b1jAK8/btsmOIQUoVp/5fCuog05Mqbea9NdGqDqb0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb1jAK8%2FbtsmOIQUoVp%2F5fCuog05Mqbea9NdGqDqb0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;196&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;196&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;id에 대한 바인딩이 0으로 들어간 것을 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1688717203058&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;userRepository.save(User())
userRepository.save(User())&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;77&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dembBC/btsmNUZkPUQ/XVrCTUiONabMSG0IRts1L0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dembBC/btsmNUZkPUQ/XVrCTUiONabMSG0IRts1L0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dembBC/btsmNUZkPUQ/XVrCTUiONabMSG0IRts1L0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdembBC%2FbtsmNUZkPUQ%2FXVrCTUiONabMSG0IRts1L0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;77&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;77&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한, save를 2번 호출하게 되면은 위와 같이&lt;b&gt; 중복된 식별자를 사용한다는 오류&lt;/b&gt;가 발생한다. (PK는 unique 해야 하니까)&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;이는 PK 값에 대해 별도로 관리하는 방법을 지정하지 않았기 때문에 &lt;span style=&quot;color: #ef5369;&quot;&gt;사용자가 직접 id를 넣어줘야 되면서&lt;/span&gt; 생긴 문제이다. 우선 엔티티 생성 시 아이디에 대한 기본값인 0L을 지정하면서 또 다시 '0L'이라는 동일한 키를 가진 엔티티가 삽입되면서 오류가 발생한 것이다.그래서 복합키 같은 특수 상황을 제외하면 웬만하면&amp;nbsp;생성&amp;nbsp;전략을&amp;nbsp;통해서&amp;nbsp;관리하는&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;size18&quot;&gt;&lt;br /&gt;&lt;b&gt;  mysql&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;504&quot; data-origin-height=&quot;218&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cCdaNz/btsmO822oj1/amkdPThdgP11dKxir72U31/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cCdaNz/btsmO822oj1/amkdPThdgP11dKxir72U31/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cCdaNz/btsmO822oj1/amkdPThdgP11dKxir72U31/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcCdaNz%2FbtsmO822oj1%2FamkdPThdgP11dKxir72U31%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;405&quot; height=&quot;175&quot; data-origin-width=&quot;504&quot; data-origin-height=&quot;218&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;마찬가지로 단순히 테이블의 필드값으로 bigint만 존재하는 것을 볼 수 있다.&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;br /&gt;&lt;b&gt;  postgresql&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;496&quot; data-origin-height=&quot;224&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bYJnbm/btsmN65U1Nq/E78QUKNXEkciutM29OLiI0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bYJnbm/btsmN65U1Nq/E78QUKNXEkciutM29OLiI0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bYJnbm/btsmN65U1Nq/E78QUKNXEkciutM29OLiI0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbYJnbm%2FbtsmN65U1Nq%2FE78QUKNXEkciutM29OLiI0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;405&quot; height=&quot;183&quot; data-origin-width=&quot;496&quot; data-origin-height=&quot;224&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;h2와 동일하게 생성된 것을 볼 수 있다.&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;style3&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;br /&gt;&lt;b&gt;  기본값을 할당해주지 않았다면?&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서도 기본값을 어떻게 할당하는지에 따라서 결과가 달라진다. (generatedType을 지정하지 않았을 때의 결과)&lt;/p&gt;
&lt;pre id=&quot;code_1688717423854&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
class User(
    @Id
    val id: Long = 0L
)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;232&quot; data-origin-height=&quot;314&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/L6r8Y/btsmOcrStQU/d95lJF6shPaqqse06oNjZk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/L6r8Y/btsmOcrStQU/d95lJF6shPaqqse06oNjZk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/L6r8Y/btsmOcrStQU/d95lJF6shPaqqse06oNjZk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FL6r8Y%2FbtsmOcrStQU%2Fd95lJF6shPaqqse06oNjZk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;232&quot; height=&quot;314&quot; data-origin-width=&quot;232&quot; data-origin-height=&quot;314&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흔히 사용하는 Long 타입 id에 &lt;b&gt;0L 같은 기본값으로 초기화하게 된다면&lt;/b&gt;, &lt;span style=&quot;color: #ef5369;&quot;&gt;엔티티 저장 시 단순히 하나의 insert 쿼리&lt;/span&gt;만 발생하게 된다. (Int 타입도 마찬가지)&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;428&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bz8ctG/btsmOck6xRi/Q1PPWXnEsvIy9GvqSiwUrk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bz8ctG/btsmOck6xRi/Q1PPWXnEsvIy9GvqSiwUrk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bz8ctG/btsmOck6xRi/Q1PPWXnEsvIy9GvqSiwUrk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbz8ctG%2FbtsmOck6xRi%2FQ1PPWXnEsvIy9GvqSiwUrk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;428&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;428&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, 1L 같이 특정한 값으로 설정하게 되면 &lt;span style=&quot;color: #ef5369;&quot;&gt;select 쿼리가 발생하고 그 다음 insert 쿼리가 발생&lt;/span&gt;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 삽입 전에 이미 해당 레코드가 존재하는지 확인하고 (관리 전략을 설정해주지 않았으니까) insert를 날려서&lt;b&gt; 존재하지 않는 것에 대해서만 insert&lt;/b&gt;를 하기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1688717554207&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
class User(
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long = 0L
)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;242&quot; data-origin-height=&quot;304&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/O2iUS/btsmOb7Dbau/ukIoDmDajJOJf6RK3vWfBk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/O2iUS/btsmOb7Dbau/ukIoDmDajJOJf6RK3vWfBk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/O2iUS/btsmOb7Dbau/ukIoDmDajJOJf6RK3vWfBk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FO2iUS%2FbtsmOb7Dbau%2FukIoDmDajJOJf6RK3vWfBk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;242&quot; height=&quot;304&quot; data-origin-width=&quot;242&quot; data-origin-height=&quot;304&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 원리로 IDENTITY를 설정하게 되면 애초에 insert 시 아이디가 없으니까 애초에 검증할 값이 없으니 단순히 insert만 발생한다. (persist 시에 insert 쿼리 발생)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1688717606623&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
class User(
    @Id 
    val id: String = &quot;&quot;
)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;430&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qUDvP/btsmOk3Xx9P/81HRslzLMH9bAyeHkt4b8k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qUDvP/btsmOk3Xx9P/81HRslzLMH9bAyeHkt4b8k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qUDvP/btsmOk3Xx9P/81HRslzLMH9bAyeHkt4b8k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqUDvP%2FbtsmOk3Xx9P%2F81HRslzLMH9bAyeHkt4b8k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;430&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;430&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;String의 경우 무조건 select가 발생한다. (애초에 String? 타입으로 설정 후 기본 값을 null로 지정하면 오류가 발생한다.)&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는, save() 시의 메서드를 보면 파악할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-07-08 오후 4.51.46.png&quot; data-origin-width=&quot;1100&quot; data-origin-height=&quot;612&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bWcMm2/btsmUuRZ3Gg/mPBD865MR0b7UiKJItg70k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bWcMm2/btsmUuRZ3Gg/mPBD865MR0b7UiKJItg70k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bWcMm2/btsmUuRZ3Gg/mPBD865MR0b7UiKJItg70k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbWcMm2%2FbtsmUuRZ3Gg%2FmPBD865MR0b7UiKJItg70k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;338&quot; data-filename=&quot;스크린샷 2023-07-08 오후 4.51.46.png&quot; data-origin-width=&quot;1100&quot; data-origin-height=&quot;612&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;save() 시 메서드를 보면 엔티티가 새로우면 persist, 아니면 merge를 하게 되는 걸 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 isNew() 메서드를 타고 들어가면 다음과 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-07-08 오후 4.52.30.png&quot; data-origin-width=&quot;1706&quot; data-origin-height=&quot;696&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LwTnq/btsmOKbeWC4/mXdoPGrkCCyr5ZiusHwMR0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LwTnq/btsmOKbeWC4/mXdoPGrkCCyr5ZiusHwMR0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LwTnq/btsmOKbeWC4/mXdoPGrkCCyr5ZiusHwMR0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLwTnq%2FbtsmOKbeWC4%2FmXdoPGrkCCyr5ZiusHwMR0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;779&quot; height=&quot;318&quot; data-filename=&quot;스크린샷 2023-07-08 오후 4.52.30.png&quot; data-origin-width=&quot;1706&quot; data-origin-height=&quot;696&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;primitive 타입이 아니라면 null로 체크, Number 형이라면 0L으로 체크해서 이게 새로운 값인지 테스트하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Back-end/JPA</category>
      <category>@GeneratedType</category>
      <category>GenerationType.AUTO</category>
      <category>GenerationType.IDENTITY</category>
      <category>GenerationType.SEQUENCE</category>
      <category>GenerationType.TABLE</category>
      <category>JPA</category>
      <author>dolmeng2</author>
      <guid isPermaLink="true">https://cl8d.tistory.com/115</guid>
      <comments>https://cl8d.tistory.com/115#entry115comment</comments>
      <pubDate>Fri, 7 Jul 2023 17:16:31 +0900</pubDate>
    </item>
    <item>
      <title>[JPA] CascadeType.REMOVE vs orphanRemoval=true 차이점 알아보기 - 2편</title>
      <link>https://cl8d.tistory.com/114</link>
      <description>&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 title=&quot;지난 포스팅&quot; href=&quot;https://cl8d.tistory.com/113&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;지난 포스팅&lt;/a&gt;에서는 CascadeType.REMOVE에 대해서 중점적으로 알아봤었는데, 이번에는 orphanRemoval=true 옵션에 대해서 한 번 알아보자. 엔티티 세팅은 지난 번과 거의 동일하기 때문에 변화가 생긴 부분에 대해서만 따로 짚도록 하겠다.&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;style3&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&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;이번에는 CascadeType.REMOVE 대신에 orphanRemoval=true를 적용하자.&lt;/p&gt;
&lt;pre id=&quot;code_1688051235775&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
class Concert(
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long = 0L,

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

    @Column(nullable = false)
    val ticketLimit: Int,

    concertTickets: MutableList&amp;lt;ConcertTicket&amp;gt; = Collections.emptyList()
) {

    @OneToMany(
        fetch = FetchType.LAZY,
        mappedBy = &quot;concert&quot;,
        cascade = [CascadeType.PERSIST],
        orphanRemoval = true // here!
    )
    val concertTickets: MutableList&amp;lt;ConcertTicket&amp;gt; = concertTickets.toMutableList()
}&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;style1&quot; /&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;  orphanRemoval = true&amp;nbsp;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt; &amp;nbsp;부모&amp;nbsp;엔티티&amp;nbsp;제거하기&amp;nbsp;-&amp;nbsp;콘서트&amp;nbsp;엔티티&amp;nbsp;제거&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1688051299006&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
@DisplayName(&quot;orphanRemoval=true 테스트 - 부모 엔티티 제거&quot;)
fun orphanRemoval_true_test_부모_엔티티_제거() {
    // given
    val 콘서트 = Concert(name = &quot;인기 많은 콘서트&quot;, ticketLimit = 10)

    val 콘서트_티켓1 = ConcertTicket(userId = 1L, concert = 콘서트)
    val 콘서트_티켓2 = ConcertTicket(userId = 2L, concert = 콘서트)
    콘서트.addTicket(콘서트_티켓1)
    콘서트.addTicket(콘서트_티켓2)
    concertRepository.save(콘서트)

    val 저장된_콘서트들 = concertRepository.findAll()
    val 저장된_콘서트_티켓들 = concertTicketRepository.findAll()
    assertThat(저장된_콘서트들).hasSize(1)
    assertThat(저장된_콘서트_티켓들).hasSize(2)

    // when
    concertRepository.delete(콘서트)

    // then
    val 삭제_이후_저장된_콘서트들 = concertRepository.findAll()
    val 삭제_이후_저장된_콘서트_티켓들 = concertTicketRepository.findAll()

    assertThat(삭제_이후_저장된_콘서트들).hasSize(0)
    assertThat(삭제_이후_저장된_콘서트_티켓들).hasSize(0)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;하나의 콘서트 엔티티에 대해 2개의 콘서트 티켓 엔티티가 존재하는 형태이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이때, 콘서트 엔티티를 제거하게 되면 CascadeType.REMOVE와 마찬가지로&lt;span style=&quot;color: #ef5369;&quot;&gt; 부모와 연관된 자식 엔티티도 함께 제거되어&lt;/span&gt; 조회된 콘서트 티켓 엔티티의 개수가 0개인 것을 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-06-30 오전 12.09.29.png&quot; data-origin-width=&quot;888&quot; data-origin-height=&quot;1450&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bQ710b/btslVlO7N85/J47rt81Zxtg67KyHSNS7Ek/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bQ710b/btslVlO7N85/J47rt81Zxtg67KyHSNS7Ek/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bQ710b/btslVlO7N85/J47rt81Zxtg67KyHSNS7Ek/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbQ710b%2FbtslVlO7N85%2FJ47rt81Zxtg67KyHSNS7Ek%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;321&quot; height=&quot;524&quot; data-filename=&quot;스크린샷 2023-06-30 오전 12.09.29.png&quot; data-origin-width=&quot;888&quot; data-origin-height=&quot;1450&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;실제로 발생한 쿼리를 보더라도 콘서트 티켓 엔티티 2개에 대한 delete 쿼리와 콘서트 엔티티에 대한 1개의 delete 쿼리가 발생하여, 총 3개의 쿼리가 나간 것을 확인할 수 있다.&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&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;size18&quot;&gt;&lt;b&gt; &amp;nbsp;부모&amp;nbsp;엔티티와&amp;nbsp;자식&amp;nbsp;엔티티&amp;nbsp;연관관계&amp;nbsp;끊기&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1688051423966&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
@DisplayName(&quot;orphanRemoval=true 테스트 - 부모 엔티티에서 자식 엔티티 제거&quot;)
fun orphanRemoval_true_test_부모_엔티티에서_자식_엔티티_제거() {
    // given
    val 콘서트 = Concert(name = &quot;인기 많은 콘서트&quot;, ticketLimit = 10)

    val 콘서트_티켓1 = ConcertTicket(userId = 1L, concert = 콘서트)
    val 콘서트_티켓2 = ConcertTicket(userId = 2L, concert = 콘서트)
    콘서트.addTicket(콘서트_티켓1)
    콘서트.addTicket(콘서트_티켓2)
    concertRepository.save(콘서트)

    val 저장된_콘서트들 = concertRepository.findAll()
    val 저장된_콘서트_티켓들 = concertTicketRepository.findAll()
    assertThat(저장된_콘서트들).hasSize(1)
    assertThat(저장된_콘서트_티켓들).hasSize(2)

    // when
    콘서트.concertTickets.remove(콘서트_티켓1)
    콘서트.concertTickets.remove(콘서트_티켓2)

    // then
    val 삭제_이후_저장된_콘서트들 = concertRepository.findAll()
    val 삭제_이후_저장된_콘서트_티켓들 = concertTicketRepository.findAll()

    assertThat(삭제_이후_저장된_콘서트들).hasSize(1) // here!
    assertThat(삭제_이후_저장된_콘서트_티켓들).hasSize(0)
}&lt;/code&gt;&lt;/pre&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;하지만, CascadeType.REMOVE와 다르게 단순히 콘서트 엔티티와 콘서트 티켓 엔티티 사이의 관계를 끊어주니 콘서트 티켓 엔티티가 실제 DB에서 제거된 것을 확인할 수 있다. 즉, &lt;span style=&quot;color: #ef5369;&quot;&gt;부모 엔티티와 자식 엔티티의 관계가 끊어지게 되면 고아(orphan)가 되고, 고아 객체에 대해 delete 쿼리(remove)가 발생&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-06-30 오전 12.12.20.png&quot; data-origin-width=&quot;908&quot; data-origin-height=&quot;1046&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b85Kbg/btslMn2Fqyu/5hFbkwljgKRJfYWNotx1TK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b85Kbg/btslMn2Fqyu/5hFbkwljgKRJfYWNotx1TK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b85Kbg/btslMn2Fqyu/5hFbkwljgKRJfYWNotx1TK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb85Kbg%2FbtslMn2Fqyu%2F5hFbkwljgKRJfYWNotx1TK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;276&quot; height=&quot;318&quot; data-filename=&quot;스크린샷 2023-06-30 오전 12.12.20.png&quot; data-origin-width=&quot;908&quot; data-origin-height=&quot;1046&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;실제로 발생한 쿼리를 확인하면 콘서트 티켓 엔티티 2개에 대한 delete 쿼리가 발생한 것을 볼 수 있다.&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  CascadeType.REMOVE + orphanRemoval = true 함께 사용하기&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1688088087685&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
class Concert(
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long = 0L,

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

    @Column(nullable = false)
    val ticketLimit: Int,

    concertTickets: MutableList&amp;lt;ConcertTicket&amp;gt; = Collections.emptyList()
) {

    @OneToMany(
        fetch = FetchType.LAZY,
        mappedBy = &quot;concert&quot;,
        cascade = [CascadeType.PERSIST, CascadeType.REMOVE],
        orphanRemoval = true
    )
    val concertTickets: MutableList&amp;lt;ConcertTicket&amp;gt; = concertTickets.toMutableList()
}&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;style1&quot; /&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; &amp;nbsp;부모&amp;nbsp;엔티티&amp;nbsp;제거하기&amp;nbsp;-&amp;nbsp;콘서트&amp;nbsp;엔티티&amp;nbsp;제거&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1688088298464&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
@DisplayName(&quot;CascadeType.REMOVE + orphanRemoval=true 테스트 - 부모 엔티티 제거&quot;)
fun cascadeType_remove_orphanRemoval_true_test_부모_엔티티_제거() {
    // given
    val 콘서트 = Concert(name = &quot;인기 많은 콘서트&quot;, ticketLimit = 10)

    val 콘서트_티켓1 = ConcertTicket(userId = 1L, concert = 콘서트)
    val 콘서트_티켓2 = ConcertTicket(userId = 2L, concert = 콘서트)
    콘서트.addTicket(콘서트_티켓1)
    콘서트.addTicket(콘서트_티켓2)
    concertRepository.save(콘서트)

    val 저장된_콘서트들 = concertRepository.findAll()
    val 저장된_콘서트_티켓들 = concertTicketRepository.findAll()
    assertThat(저장된_콘서트들).hasSize(1)
    assertThat(저장된_콘서트_티켓들).hasSize(2)

    // when
    concertRepository.delete(콘서트)

    // then
    val 삭제_이후_저장된_콘서트들 = concertRepository.findAll()
    val 삭제_이후_저장된_콘서트_티켓들 = concertTicketRepository.findAll()

    assertThat(삭제_이후_저장된_콘서트들).hasSize(0)
    assertThat(삭제_이후_저장된_콘서트_티켓들).hasSize(0)
}&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-06-30 오전 10.25.11.png&quot; data-origin-width=&quot;856&quot; data-origin-height=&quot;1466&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/byt7xk/btslSlWW64i/Oe3RLHZCTZYQuHIFuzDSK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/byt7xk/btslSlWW64i/Oe3RLHZCTZYQuHIFuzDSK0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/byt7xk/btslSlWW64i/Oe3RLHZCTZYQuHIFuzDSK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbyt7xk%2FbtslSlWW64i%2FOe3RLHZCTZYQuHIFuzDSK0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;250&quot; height=&quot;428&quot; data-filename=&quot;스크린샷 2023-06-30 오전 10.25.11.png&quot; data-origin-width=&quot;856&quot; data-origin-height=&quot;1466&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CascadeType.REMOVE나 orphanRemoval = true 옵션 모두 부모 엔티티를 제거하면 연관된 자식 엔티티도 함께 제거되기 때문에 당연하게 두 옵션을 함께 사용하면 똑같이 제거되는 것을 볼 수 있다.&amp;nbsp;&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;style1&quot; /&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; &amp;nbsp;부모&amp;nbsp;엔티티와&amp;nbsp;자식&amp;nbsp;엔티티&amp;nbsp;연관관계&amp;nbsp;끊기&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1688088477154&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
@DisplayName(&quot;CascadeType.REMOVE + orphanRemoval=true - 부모 엔티티에서 자식 엔티티 제거&quot;)
fun cascadeType_remove_orphanRemoval_true_test_부모_엔티티에서_자식_엔티티_제거() {
    // given
    val 콘서트 = Concert(name = &quot;인기 많은 콘서트&quot;, ticketLimit = 10)

    val 콘서트_티켓1 = ConcertTicket(userId = 1L, concert = 콘서트)
    val 콘서트_티켓2 = ConcertTicket(userId = 2L, concert = 콘서트)
    콘서트.addTicket(콘서트_티켓1)
    콘서트.addTicket(콘서트_티켓2)
    concertRepository.save(콘서트)

    val 저장된_콘서트들 = concertRepository.findAll()
    val 저장된_콘서트_티켓들 = concertTicketRepository.findAll()
    assertThat(저장된_콘서트들).hasSize(1)
    assertThat(저장된_콘서트_티켓들).hasSize(2)

    // when
    콘서트.concertTickets.remove(콘서트_티켓1)
    콘서트.concertTickets.remove(콘서트_티켓2)

    // then
    val 삭제_이후_저장된_콘서트들 = concertRepository.findAll()
    val 삭제_이후_저장된_콘서트_티켓들 = concertTicketRepository.findAll()

    assertThat(삭제_이후_저장된_콘서트들).hasSize(1)
    assertThat(삭제_이후_저장된_콘서트_티켓들).hasSize(0)
}&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-06-30 오전 10.28.09.png&quot; data-origin-width=&quot;888&quot; data-origin-height=&quot;1058&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cy7OjM/btslRHMXNvG/fKAhs5hFGjoIXzqxNLxWUK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cy7OjM/btslRHMXNvG/fKAhs5hFGjoIXzqxNLxWUK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cy7OjM/btslRHMXNvG/fKAhs5hFGjoIXzqxNLxWUK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcy7OjM%2FbtslRHMXNvG%2FfKAhs5hFGjoIXzqxNLxWUK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;277&quot; height=&quot;330&quot; data-filename=&quot;스크린샷 2023-06-30 오전 10.28.09.png&quot; data-origin-width=&quot;888&quot; data-origin-height=&quot;1058&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 이유로 부모 엔티티와 자식 엔티티의 연관관계를 끊더라도 orphanRemoval = true 옵션에 의해서 제거되는 것을 확인할 수 있다.&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;style3&quot; /&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  orphanRemoval = true는 언제 사용을 지양하는 게 좋을까?&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1688088658635&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
class Concert(
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long = 0L,

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

    @Column(nullable = false)
    val ticketLimit: Int,

    concertTickets: MutableList&amp;lt;ConcertTicket&amp;gt; = Collections.emptyList()
) {

    @OneToMany(
        fetch = FetchType.LAZY,
        mappedBy = &quot;concert&quot;
    )
    val concertTickets: MutableList&amp;lt;ConcertTicket&amp;gt; = concertTickets.toMutableList()
}

@Entity
class ConcertTicket(
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long = 0L,

    @Column(nullable = false)
    val userId: Long,

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(
        name = &quot;concert_id&quot;,
        foreignKey = ForeignKey(name = &quot;fk_ticket_concert_id&quot;)
    )
    var concert: Concert
)

@Entity
class User(
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long = 0L,

    val name: String,

    concertTickets: MutableList&amp;lt;ConcertTicket&amp;gt; = Collections.emptyList()
) {

    @OneToMany(
        fetch = FetchType.LAZY,
        orphanRemoval = true
    )
    val concertTickets: MutableList&amp;lt;ConcertTicket&amp;gt; = concertTickets.toMutableList()
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트를 위해서 사용자를 나타내는 'User' 엔티티를 만들고, 사용자 엔티티가 콘서트 티켓 엔티티를 자식으로 가지고 있는 상태로 만들었다. 즉, &lt;span style=&quot;color: #ef5369;&quot;&gt;'콘서트 티켓 엔티티'에 대해서는 '콘서트'와 '사용자'라는 두 개의 부모 엔티티가 존재하는 형태&lt;/span&gt;인 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로, 콘서트 티켓 엔티티에 대해서는 사용자 엔티티에 대한 연관관계를 설정하지 않았다. (단방향) 또한, 기존 &lt;b&gt;콘서트 엔티티의&lt;/b&gt; &lt;b&gt;CascadeType.PERSIST 옵션을 제거&lt;/b&gt;하고 사용자 엔티티가 가진 &lt;b&gt;콘서트 티켓 엔티티에 대해서&lt;/b&gt; orphanRemoval=true를 추가하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1688088879070&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
@DisplayName(&quot;orphanRemoval=true - 다중 부모를 가지고 있는 경우, 하나의 부모 삭제&quot;)
fun orphanRemoval_true_multiple_parents_부모_엔티티_제거() {
    // given
    val 콘서트 = Concert(name = &quot;인기 많은 콘서트&quot;, ticketLimit = 10)

    val 콘서트_티켓1 = ConcertTicket(userId = 1L, concert = 콘서트)
    val 콘서트_티켓2 = ConcertTicket(userId = 2L, concert = 콘서트)
    concertRepository.save(콘서트)
    concertTicketRepository.saveAll(listOf(콘서트_티켓1, 콘서트_티켓2))

    val 사용자 = User(name = &quot;져니&quot;)
    userRepository.save(사용자)

    // when
    userRepository.delete(사용자)

    // then
    val 삭제_이후_저장된_콘서트들 = concertRepository.findAll()
    val 삭제_이후_저장된_콘서트_티켓들 = concertTicketRepository.findAll()
    val 모든_사용자들 = userRepository.findAll()

    assertThat(삭제_이후_저장된_콘서트들).hasSize(1)
    assertThat(삭제_이후_저장된_콘서트_티켓들).hasSize(2)
    assertThat(모든_사용자들).hasSize(0)
}&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-06-30 오전 10.34.46.png&quot; data-origin-width=&quot;508&quot; data-origin-height=&quot;532&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bB5QVs/btslS2QuJzK/RllhakGG80h7n7auJbpKak/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bB5QVs/btslS2QuJzK/RllhakGG80h7n7auJbpKak/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bB5QVs/btslS2QuJzK/RllhakGG80h7n7auJbpKak/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbB5QVs%2FbtslS2QuJzK%2FRllhakGG80h7n7auJbpKak%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;225&quot; height=&quot;236&quot; data-filename=&quot;스크린샷 2023-06-30 오전 10.34.46.png&quot; data-origin-width=&quot;508&quot; data-origin-height=&quot;532&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선, PERSIST 없이 여러 부모가 있을 때 하나의 부모에 대해서 제거를 하게 되면 &lt;span style=&quot;color: #ef5369;&quot;&gt;단순히 사용자에 대한 delete 쿼리&lt;/span&gt;만 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1688089497450&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
class User(
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long = 0L,

    val name: String,

    concertTickets: List&amp;lt;ConcertTicket&amp;gt; = Collections.emptyList()
) {

    @OneToMany(
        fetch = FetchType.LAZY,
        cascade = [CascadeType.PERSIST],
        orphanRemoval = true
    )
    val concertTickets: MutableList&amp;lt;ConcertTicket&amp;gt; = concertTickets.toMutableList()
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, 사용자와 콘서트 엔티티에 대해서 cascade 옵션을 통해 PERSIST를 준다면 결과가 달라지게 된다.&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;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;  부모 엔티티 제거하기 - 사용자 엔티티 제거 (다중 부모)&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1688089529934&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
@DisplayName(&quot;orphanRemoval=true - 다중 부모를 가지고 있는 경우, 하나의 부모 삭제&quot;)
fun orphanRemoval_true_multiple_parents_부모_엔티티_제거() {
    // given
    val 콘서트 = Concert(name = &quot;인기 많은 콘서트&quot;, ticketLimit = 10)

    val 콘서트_티켓1 = ConcertTicket(userId = 1L, concert = 콘서트)
    val 콘서트_티켓2 = ConcertTicket(userId = 2L, concert = 콘서트)
    concertRepository.save(콘서트)

    val 사용자 = User(name = &quot;져니&quot;, concertTickets = mutableListOf(콘서트_티켓1, 콘서트_티켓2))
    userRepository.save(사용자)

    // when
    userRepository.delete(사용자)

    // then
    val 삭제_이후_저장된_콘서트들 = concertRepository.findAll()
    val 삭제_이후_저장된_콘서트_티켓들 = concertTicketRepository.findAll()
    val 모든_사용자들 = userRepository.findAll()

    assertThat(삭제_이후_저장된_콘서트들).hasSize(1)
    assertThat(삭제_이후_저장된_콘서트_티켓들).hasSize(0)
    assertThat(모든_사용자들).hasSize(0)
}&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-06-30 오전 10.45.50.png&quot; data-origin-width=&quot;854&quot; data-origin-height=&quot;1472&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cjpH8K/btslUNrFq0W/2uQwuzkuyUx2t6vHX4cnsk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cjpH8K/btslUNrFq0W/2uQwuzkuyUx2t6vHX4cnsk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cjpH8K/btslUNrFq0W/2uQwuzkuyUx2t6vHX4cnsk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcjpH8K%2FbtslUNrFq0W%2F2uQwuzkuyUx2t6vHX4cnsk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;278&quot; height=&quot;479&quot; data-filename=&quot;스크린샷 2023-06-30 오전 10.45.50.png&quot; data-origin-width=&quot;854&quot; data-origin-height=&quot;1472&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만,&lt;span style=&quot;color: #ef5369;&quot;&gt; 사용자를 저장할 때 콘서트 티켓을 함께 저장&lt;/span&gt;하고 (PERSIST에 의해서 영속화 -&amp;gt; insert) 사용자 엔티티를 제거하게 되면 영속화된 &lt;span style=&quot;color: #ef5369;&quot;&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;&lt;span&gt;&amp;nbsp;&lt;/span&gt;콘서트 티켓에 대해서는 제거하고 싶지 않은 상황이 발생한다면&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;(물론, 이런 상황은 거의 없을 것이다. 대부분 자식의 생명주기는 부모에게 종속되는 것이 일반적이니까) orphanRemoval = true의 사용을 잘 고민해봐야 한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&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; &amp;nbsp;부모&amp;nbsp;엔티티와&amp;nbsp;자식&amp;nbsp;엔티티&amp;nbsp;연관관계&amp;nbsp;끊기&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1688090703515&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
@DisplayName(&quot;orphanRemoval=true - 다중 부모를 가지고 있는 경우, 하나의 부모에서 자식 엔티티 제거&quot;)
fun orphanRemoval_true_multiple_parents_부모에서_자식_엔티티_제거() {
    // given
    val 콘서트 = Concert(name = &quot;인기 많은 콘서트&quot;, ticketLimit = 10)
    concertRepository.save(콘서트)

    val 콘서트_티켓1 = ConcertTicket(userId = 1L, concert = 콘서트)
    val 콘서트_티켓2 = ConcertTicket(userId = 2L, concert = 콘서트)
    val 사용자 = User(name = &quot;져니&quot;, concertTickets = mutableListOf(콘서트_티켓1, 콘서트_티켓2))
    userRepository.save(사용자)

    // when
    사용자.concertTickets.remove(콘서트_티켓1)
    사용자.concertTickets.remove(콘서트_티켓2)

    // then
    val 삭제_이후_저장된_콘서트들 = concertRepository.findAll()
    val 삭제_이후_저장된_콘서트_티켓들 = concertTicketRepository.findAll()
    val 모든_사용자들 = userRepository.findAll()

    assertThat(삭제_이후_저장된_콘서트들).hasSize(1)
    assertThat(삭제_이후_저장된_콘서트_티켓들).hasSize(0)
    assertThat(모든_사용자들[0].concertTickets).hasSize(0)
}&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-06-30 오전 11.05.16.png&quot; data-origin-width=&quot;886&quot; data-origin-height=&quot;1048&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/UFwqc/btslYynekdS/THMiCPF7ziATLrKy5pZeS0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/UFwqc/btslYynekdS/THMiCPF7ziATLrKy5pZeS0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/UFwqc/btslYynekdS/THMiCPF7ziATLrKy5pZeS0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUFwqc%2FbtslYynekdS%2FTHMiCPF7ziATLrKy5pZeS0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;318&quot; height=&quot;376&quot; data-filename=&quot;스크린샷 2023-06-30 오전 11.05.16.png&quot; data-origin-width=&quot;886&quot; data-origin-height=&quot;1048&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 영속성 전이 옵션을 설정한 부모 엔티티 (사용자)에서 자식 엔티티 (콘서트 티켓)에 대한 연관관계를 끊어보자. 콘서트 티켓 엔티티는 &lt;b&gt;DB 상으로 콘서트와 연관관계가 있음에도&lt;/b&gt; &lt;span style=&quot;color: #ef5369;&quot;&gt;영속성 전이 옵션이 설정되어 있는&lt;/span&gt; 사용자와 콘서트 티켓 사이의 &lt;span style=&quot;color: #ef5369;&quot;&gt;연관관계가 끊어지니 제거&lt;/span&gt;되는 것을 볼 수 있다.&amp;nbsp;&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;style3&quot; /&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;- 영속성 전이 옵션을(PERSIST) 통해서  부모와 자식의 생명주기가 맞춰져 있어야 한다.&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;CascadeType.REMOVE&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;&lt;b&gt;@OneToMany&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 부모 엔티티 제거 시 부모와 자식 모두 실제 DB에서 제거된다.&lt;/p&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;&lt;b&gt;@ManyToOne&lt;/b&gt;&lt;br /&gt;- 부모 / 자식 엔티티 제거 시 연관된 부모, 자식 모두 실제 DB에서 제거된다.&lt;br /&gt;- 이때, 자식 엔티티가 여러 개일 경우 외래키 제약 조건 위반으로 인해 예외가 발생할 수 있다.&lt;br /&gt;- 부모 엔티티와 자식 엔티티의 연관관계가 끊어지더라도 실제 DB에서 제거되지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;&lt;b&gt;orphanRemoval = true&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 부모 엔티티 제거 시 부모와 자식 모두 실제 DB에서 제거된다.&lt;br /&gt;- 부모 엔티티와 자식 엔티티의 연관관계가 끊어지면 자식은 실제 DB에서 제거된다.&lt;/p&gt;</description>
      <category>Back-end/JPA</category>
      <category>CascadeType.REMOVE</category>
      <category>JPA</category>
      <category>OrphanRemoval</category>
      <category>영속성전이</category>
      <author>dolmeng2</author>
      <guid isPermaLink="true">https://cl8d.tistory.com/114</guid>
      <comments>https://cl8d.tistory.com/114#entry114comment</comments>
      <pubDate>Fri, 30 Jun 2023 11:14:22 +0900</pubDate>
    </item>
    <item>
      <title>[JPA] CascadeType.REMOVE vs orphanRemoval=true 차이점 알아보기 - 1편</title>
      <link>https://cl8d.tistory.com/113</link>
      <description>&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;JPA를 공부하면서&lt;b&gt; CascadeType.REMOVE&lt;/b&gt;와 &lt;b&gt;orphanRemoval = true&lt;/b&gt; 옵션에 대해서 어떤 차이가 있는지 제대로 인지한 적이 없는 것 같아서, 이번에 공부할 겸 여러 가지 테스트를 진행해보며 두 옵션의 차이를 공부해보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로 말하자면 Cascade 옵션은 부모 엔티티와 자식 엔티티의 영속 상태를 관리하는 것이고, orphanRemoval은 조금 더 세부적으로 고아 객체에 대한 관리라는 생각이 들었다. 지금부터 여러 케스트를 테스트하며 하나씩 알아보자.&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;style3&quot; /&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&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;1:N 관계를 담당해줄 '&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;span style=&quot;color: #ef5369;&quot;&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;&amp;lt;Concert&amp;gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1687944984057&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
class Concert(
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long = 0L,

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

    @Column(nullable = false)
    val ticketLimit: Int,

    concertTickets: MutableList&amp;lt;ConcertTicket&amp;gt; = Collections.emptyList()
) {

    @OneToMany(fetch = FetchType.LAZY, mappedBy = &quot;concert&quot;)
    val concertTickets: MutableList&amp;lt;ConcertTicket&amp;gt; = concertTickets.toMutableList()

    fun addTicket(concertTicket: ConcertTicket) {
        concertTickets.add(concertTicket)
    }
}&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;&amp;lt;&lt;b&gt;ConcertTicket&lt;/b&gt;&amp;gt;&lt;/p&gt;
&lt;pre id=&quot;code_1687945005334&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
class ConcertTicket(
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long = 0L,

    @Column(nullable = false)
    val userId: Long,

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(
        name = &quot;concert_id&quot;,
        foreignKey = ForeignKey(name = &quot;fk_ticket_concert_id&quot;)
    )
    var concert: Concert
)&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&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;&amp;lt;&lt;b&gt;application-test.yml&lt;/b&gt;&amp;gt;&lt;/p&gt;
&lt;pre id=&quot;code_1687945080972&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring:
  datasource:
    driver-class-name: org.h2.Driver
    url: jdbc:h2:mem:testdb;MODE=MySQL

  jpa:
    hibernate:
      ddl-auto: create-drop
    properties:
      hibernate:
        globally_quoted_identifiers: true
        format_sql: true
    show-sql: true&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 환경 세팅을 위해 application-test.yml 파일을 생성하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 show-sql 옵션을 통해서 JPA에서 어떤 쿼리가 발생하는지 확인하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1687945144172&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@DataJpaTest
@ActiveProfiles(&quot;test&quot;)
@TestConstructor(autowireMode = TestConstructor.AutowireMode.ALL)
class ConcertTest(
    private val concertRepository: ConcertRepository,
    private val concertTicketRepository: ConcertTicketRepository
) {
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트에서 사용한 뼈대 코드이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;@DataJpaTest&lt;/b&gt;를&amp;nbsp;통해서&amp;nbsp;JPA&amp;nbsp;환경을&amp;nbsp;테스트할&amp;nbsp;수&amp;nbsp;있도록&amp;nbsp;만들었으며,&amp;nbsp;활성&amp;nbsp;프로파일은&amp;nbsp;test로&amp;nbsp;주어서&amp;nbsp;application-test.yml의&amp;nbsp;정보를&amp;nbsp;읽을&amp;nbsp;수&amp;nbsp;있도록&amp;nbsp;만들었다.&amp;nbsp;또한,&amp;nbsp;&lt;b&gt;생성자에서&amp;nbsp;바로&amp;nbsp;주입받을&amp;nbsp;수&amp;nbsp;있도록&lt;/b&gt;&amp;nbsp;autowireMode를&amp;nbsp;TestConstructor.AutowireMode.ALL로&amp;nbsp;설정하였다.&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;style3&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  @OneToMany에서 CascadeType.REMOVE&amp;nbsp;&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1687945396682&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
class Concert(
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long = 0L,

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

    @Column(nullable = false)
    val ticketLimit: Int,

    concertTickets: MutableList&amp;lt;ConcertTicket&amp;gt; = Collections.emptyList()
) {

    @OneToMany(
        fetch = FetchType.LAZY,
        mappedBy = &quot;concert&quot;,
        cascade = [CascadeType.REMOVE, CascadeType.PERSIST] // Here!
    )
    val concertTickets: MutableList&amp;lt;ConcertTicket&amp;gt; = concertTickets.toMutableList()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트를 위해서 콘서트 티켓 엔티티에 대해서 CascadeType.REMOVE를 설정해주었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때,&lt;b&gt; 부모 엔티티 저장 시 자식 엔티티도 함께 저장&lt;/b&gt;되도록 만들기 위해서 CascadeType.PERSIST로 설정해주었다.&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;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;  부모 엔티티 제거하기 - 콘서트 엔티티 제거&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1687945460965&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
@DisplayName(&quot;@OneToMany: CascadeType.REMOVE 테스트 - 부모 엔티티 제거&quot;)
fun oneToMany_cascadeType_REMOVE_test_부모_엔티티_제거() {
    // given
    val 콘서트 = Concert(name = &quot;인기 많은 콘서트&quot;, ticketLimit = 10)

    val 콘서트_티켓1 = ConcertTicket(userId = 1L, concert = 콘서트)
    val 콘서트_티켓2 = ConcertTicket(userId = 2L, concert = 콘서트)
    콘서트.addTicket(콘서트_티켓1)
    콘서트.addTicket(콘서트_티켓2)
    concertRepository.save(콘서트)

    val 저장된_콘서트들 = concertRepository.findAll()
    val 저장된_콘서트_티켓들 = concertTicketRepository.findAll()
    assertThat(저장된_콘서트들).hasSize(1)
    assertThat(저장된_콘서트_티켓들).hasSize(2)

    // when
    concertRepository.delete(콘서트)

    // then
    val 삭제_이후_저장된_콘서트들 = concertRepository.findAll()
    val 삭제_이후_저장된_콘서트_티켓들 = concertTicketRepository.findAll()

    assertThat(삭제_이후_저장된_콘서트들).hasSize(0)
    assertThat(삭제_이후_저장된_콘서트_티켓들).hasSize(0)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 콘서트에 대해서 2개의 콘서트 티켓을 발급받고, 저장 직후 findAll을 통해 잘 저장되었는지 확인했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후, 부모 엔티티인 '콘서트 엔티티'를 제거한 다음 조회를 해보면 &lt;span style=&quot;color: #ef5369;&quot;&gt;콘서트 엔티티와 콘서트 티켓 엔티티 모두가 제거&lt;/span&gt;된 것을 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-06-28 오후 6.45.38.png&quot; data-origin-width=&quot;896&quot; data-origin-height=&quot;1470&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bvM9Uw/btslKddw75C/zuIy0S8ab5fiiIJiZHyKF1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bvM9Uw/btslKddw75C/zuIy0S8ab5fiiIJiZHyKF1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bvM9Uw/btslKddw75C/zuIy0S8ab5fiiIJiZHyKF1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbvM9Uw%2FbtslKddw75C%2FzuIy0S8ab5fiiIJiZHyKF1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;280&quot; height=&quot;459&quot; data-filename=&quot;스크린샷 2023-06-28 오후 6.45.38.png&quot; data-origin-width=&quot;896&quot; data-origin-height=&quot;1470&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로&amp;nbsp;발생한&amp;nbsp;쿼리를&amp;nbsp;확인해보면&amp;nbsp;&lt;b&gt;콘서트&amp;nbsp;티켓&amp;nbsp;엔티티를&amp;nbsp;제거&lt;/b&gt;하기&amp;nbsp;위한&amp;nbsp;delete&amp;nbsp;쿼리가&amp;nbsp;2번,&lt;b&gt;&amp;nbsp;콘서트&amp;nbsp;엔티티를&amp;nbsp;제거&lt;/b&gt;하기&amp;nbsp;위한&amp;nbsp;delete&amp;nbsp;쿼리가&amp;nbsp;1번&amp;nbsp;발생하여&amp;nbsp;&lt;span style=&quot;color: #ef5369;&quot;&gt;총&amp;nbsp;3번의&amp;nbsp;쿼리가&amp;nbsp;나간&amp;nbsp;것&lt;/span&gt;을&amp;nbsp;볼&amp;nbsp;수&amp;nbsp;있다.&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;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;  부모 엔티티와 자식 엔티티 연관관계 끊기&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1688047139037&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
@DisplayName(&quot;@OneToMany: CascadeType.REMOVE 테스트 - 부모 엔티티에서 자식 엔티티 제거&quot;)
fun oneToMany_cascadeType_REMOVE_test_부모_엔티티에서_자식_엔티티_제거() {
    // given
    val 콘서트 = Concert(name = &quot;인기 많은 콘서트&quot;, ticketLimit = 10)

    val 콘서트_티켓1 = ConcertTicket(userId = 1L, concert = 콘서트)
    val 콘서트_티켓2 = ConcertTicket(userId = 2L, concert = 콘서트)
    콘서트.addTicket(콘서트_티켓1)
    콘서트.addTicket(콘서트_티켓2)
    concertRepository.save(콘서트)

    val 저장된_콘서트들 = concertRepository.findAll()
    val 저장된_콘서트_티켓들 = concertTicketRepository.findAll()
    assertThat(저장된_콘서트들).hasSize(1)
    assertThat(저장된_콘서트_티켓들).hasSize(2)

    // when
    콘서트.concertTickets.remove(콘서트_티켓1)
    콘서트.concertTickets.remove(콘서트_티켓2)

    // then
    val 삭제_이후_저장된_콘서트들 = concertRepository.findAll()
    val 삭제_이후_저장된_콘서트_티켓들 = concertTicketRepository.findAll()

    assertThat(삭제_이후_저장된_콘서트들).hasSize(1)
    assertThat(삭제_이후_저장된_콘서트_티켓들).hasSize(2)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부모 엔티티인 콘서트 엔티티를 조회한 후, 해당 엔티티와 관련된 콘서트 티켓 엔티티에 대해 remove를 진행하여 &lt;b&gt;부모와 자식간의 연관관계를 끊어주었다&lt;/b&gt;. 이때 콘서트 티켓 엔티티 2개는 '고아 객체'가 되었다고 판단하는데, &lt;span style=&quot;color: #ef5369;&quot;&gt;CascadeType.REMOVE에서는 고아 객체가 발생하더라도 실제 DB에서 제거되지는 않는다&lt;/span&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;style3&quot; /&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;  @ManyToOne에서 CascadeType.REMOVE&amp;nbsp;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;cascadeType의&amp;nbsp;경우&amp;nbsp;@OneToMany뿐만&amp;nbsp;아니라&amp;nbsp;@ManyToOne에서도&amp;nbsp;지정할&amp;nbsp;수&amp;nbsp;있다.&lt;/p&gt;
&lt;pre id=&quot;code_1688047283436&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
class Concert(
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long = 0L,

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

    @Column(nullable = false)
    val ticketLimit: Int,

    concertTickets: MutableList&amp;lt;ConcertTicket&amp;gt; = Collections.emptyList()
) {

    @OneToMany(
        fetch = FetchType.LAZY,
        mappedBy = &quot;concert&quot;,
        cascade = [CascadeType.PERSIST] // here!
    )
    val concertTickets: MutableList&amp;lt;ConcertTicket&amp;gt; = concertTickets.toMutableList()
}

@Entity
class ConcertTicket(
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long = 0L,

    @Column(nullable = false)
    val userId: Long,

    @ManyToOne(
        fetch = FetchType.LAZY,
        cascade = [CascadeType.REMOVE]
    )
    @JoinColumn(
        name = &quot;concert_id&quot;,
        foreignKey = ForeignKey(name = &quot;fk_ticket_concert_id&quot;)
    )
    var concert: Concert
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 콘서트 엔티티에 적용하였던 CascadeType.REMOVE를 제거하고, 콘서트 티켓 엔티티에 대해서 적용하였다.&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;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;  자식 엔티티 제거하기 - 콘서트 티켓 엔티티 제거&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1688047686771&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
@DisplayName(&quot;@ManyToOne: CascadeType.REMOVE 테스트 - 자식 엔티티 제거&quot;)
fun manyToOne_cascadeType_REMOVE_test_자식_엔티티_제거() {
    // given
    val 콘서트 = Concert(name = &quot;인기 많은 콘서트&quot;, ticketLimit = 10)
    val 콘서트_티켓1 = ConcertTicket(userId = 1L, concert = 콘서트)
    콘서트.addTicket(콘서트_티켓1)
    concertRepository.save(콘서트)

    val 저장된_콘서트들 = concertRepository.findAll()
    val 저장된_콘서트_티켓들 = concertTicketRepository.findAll()
    assertThat(저장된_콘서트들).hasSize(1)
    assertThat(저장된_콘서트_티켓들).hasSize(1)

    // when
    concertTicketRepository.delete(콘서트_티켓1);

    // then
    val 삭제_이후_저장된_콘서트들 = concertRepository.findAll()
    val 삭제_이후_저장된_콘서트_티켓들 = concertTicketRepository.findAll()

    assertThat(삭제_이후_저장된_콘서트들).hasSize(0)
    assertThat(삭제_이후_저장된_콘서트_티켓들).hasSize(0)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 테스트의 경우, 콘서트 엔티티에&lt;b&gt; 한 개의 콘서트 티켓 엔티티&lt;/b&gt;가 있는 경우이다. (자식이 1개만 있는 경우)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 자식 엔티티를 제거하게 된다면 해당 &lt;span style=&quot;color: #ef5369;&quot;&gt;자식 엔티티와 연관된 부모 엔티티와 자식 엔티티 모두가 제거&lt;/span&gt;된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 참고로, 여기서 연관이 되어 있다는 것은 단순히 'DB에서 FK로 연결이 되어 있다'라는 의미보다, &lt;b&gt;영속화가 되어 있는지&lt;/b&gt;를 확인한다. 영속화되지 않고 단순히 FK로만 연결이 되어 있다면 콘서트 티켓 엔티티를 제거하더라도 콘서트 엔티티까지 제거되지는 않는다.&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;pre id=&quot;code_1688047948945&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
@DisplayName(&quot;@ManyToOne: CascadeType.REMOVE 테스트 - 자식 엔티티 제거 : 자식이 여러 개일 경우&quot;)
fun manyToOne_cascadeType_REMOVE_test_자식_엔티티_제거_자식이_여러개일_경우() {
    // given
    val 콘서트 = Concert(name = &quot;인기 많은 콘서트&quot;, ticketLimit = 10)
    val 콘서트_티켓1 = ConcertTicket(userId = 1L, concert = 콘서트)
    val 콘서트_티켓2 = ConcertTicket(userId = 2L, concert = 콘서트)
    콘서트.addTicket(콘서트_티켓1)
    콘서트.addTicket(콘서트_티켓2)
    concertRepository.save(콘서트)

    val 저장된_콘서트들 = concertRepository.findAll()
    val 저장된_콘서트_티켓들 = concertTicketRepository.findAll()
    assertThat(저장된_콘서트들).hasSize(1)
    assertThat(저장된_콘서트_티켓들).hasSize(2)

    // when
    concertTicketRepository.delete(콘서트_티켓1)

    // then
    assertThatThrownBy {
        concertRepository.findAll()
		// concertTicketRepository.findAll()
    }.isInstanceOf(DataIntegrityViolationException::class.java)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;콘서트 엔티티에 2개의 콘서트 티켓 엔티티가 있고, 첫 번째 콘서트 티켓 엔티티에 대해서 제거한 상황이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때, 콘서트에 대해 &lt;span style=&quot;color: #ef5369;&quot;&gt;조회하는 시점&lt;/span&gt;에서 DataIntegrityViolationException이 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 참고로, 주석으로 concertTicketRepository.findAll()을 해두었는데, 콘서트든 콘서트 티켓 엔티티든 조회하는 쿼리를 발생시킬 때 오류가 발생하기 때문이다.&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;/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;[concert]&lt;/b&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 33px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 16px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; height: 16px; text-align: center;&quot;&gt;&lt;b&gt;id&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 16px; text-align: center;&quot;&gt;&lt;b&gt;name&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 16px; text-align: center;&quot;&gt;&lt;b&gt;ticket_limit&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px; text-align: center;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px; text-align: center;&quot;&gt;인기 많은 콘서트&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px; text-align: center;&quot;&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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;[concert_ticket]&lt;/b&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 50px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 16px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; height: 16px; text-align: center;&quot;&gt;&lt;b&gt;id&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 16px; text-align: center;&quot;&gt;&lt;b&gt;user_id&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 16px; text-align: center;&quot;&gt;&lt;b&gt;concert_id&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px; text-align: center;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px; text-align: center;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px; text-align: center;&quot;&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center; height: 17px;&quot;&gt;2&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center; height: 17px;&quot;&gt;2&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center; height: 17px;&quot;&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PK = 1인 콘서트 레코드가 저장되어 있고, 해당 레코드를 FK로 가지고 있는 PK = 1, 2인 콘서트 티켓 레코드가 저장되어 있는 형태이다.&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;스크린샷 2023-06-29 오후 11.14.19.png&quot; data-origin-width=&quot;584&quot; data-origin-height=&quot;774&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cb2pDV/btslOB0KWJj/y3bJJtkrfaEqFiXUAxSqu1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cb2pDV/btslOB0KWJj/y3bJJtkrfaEqFiXUAxSqu1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cb2pDV/btslOB0KWJj/y3bJJtkrfaEqFiXUAxSqu1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcb2pDV%2FbtslOB0KWJj%2Fy3bJJtkrfaEqFiXUAxSqu1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;276&quot; height=&quot;366&quot; data-filename=&quot;스크린샷 2023-06-29 오후 11.14.19.png&quot; data-origin-width=&quot;584&quot; data-origin-height=&quot;774&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, 실제 생성된 쿼리를 보면 1개의 콘서트 티켓에 대해 제거하고, 바로 콘서트를 제거하는 쿼리가 발생한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;concert_ticket 테이블의 PK=1인 첫 번째 레코드가 제거되고, concert 테이블의 PK=1인 첫 번째 레코드가 제거되는 상황이다. 하지만, concert 테이블의 레코드가 제거되려고 할 때 &lt;span style=&quot;color: #ef5369;&quot;&gt;엔티티에 걸어두었던 외래키 제약 조건에 의해서 오류가 발생&lt;/span&gt;하게 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1688048342035&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
class ConcertTicket(
    ....
    @JoinColumn(
        name = &quot;concert_id&quot;,
        foreignKey = ForeignKey(name = &quot;fk_ticket_concert_id&quot;) // here
    )
    var concert: Concert
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;때문에 concert 테이블의 PK=1인 레코드를 제거하면, concert_ticket 테이블의 &lt;b&gt;두 번째 레코드가 참조하고 있기 때문에&lt;/b&gt; fk 값이 null이 될 수 없어 오류가 발생하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;⭐️ 왜 delete하는 시점이 아닌, findAll()을 통해 조회하는 시점에 오류가 발생하는 걸까?&lt;/blockquote&gt;
&lt;pre id=&quot;code_1688048452082&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
@DisplayName(&quot;@ManyToOne: CascadeType.REMOVE 테스트 - 자식 엔티티 제거 : 자식이 여러 개일 경우&quot;)
fun manyToOne_cascadeType_REMOVE_test_자식_엔티티_제거_자식이_여러개일_경우() {
    // given
    val 콘서트 = Concert(name = &quot;인기 많은 콘서트&quot;, ticketLimit = 10)
    val 콘서트_티켓1 = ConcertTicket(userId = 1L, concert = 콘서트)
    val 콘서트_티켓2 = ConcertTicket(userId = 2L, concert = 콘서트)
    콘서트.addTicket(콘서트_티켓1)
    콘서트.addTicket(콘서트_티켓2)
    concertRepository.save(콘서트)

    val 저장된_콘서트들 = concertRepository.findAll()
    val 저장된_콘서트_티켓들 = concertTicketRepository.findAll()
    assertThat(저장된_콘서트들).hasSize(1)
    assertThat(저장된_콘서트_티켓들).hasSize(2)

    // when
    assertDoesNotThrow {
        concertTicketRepository.delete(콘서트_티켓1)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로, 단순히 콘서트 티켓 레파지토리에서 delete를 한다고 해서 오류가 발생하는 것은 아니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-06-29 오후 11.21.29.png&quot; data-origin-width=&quot;960&quot; data-origin-height=&quot;1226&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/efyTVY/btslVmtHtAW/2pXkq9nLbrRj8K2HrBRFM1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/efyTVY/btslVmtHtAW/2pXkq9nLbrRj8K2HrBRFM1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/efyTVY/btslVmtHtAW/2pXkq9nLbrRj8K2HrBRFM1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FefyTVY%2FbtslVmtHtAW%2F2pXkq9nLbrRj8K2HrBRFM1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;291&quot; height=&quot;372&quot; data-filename=&quot;스크린샷 2023-06-29 오후 11.21.29.png&quot; data-origin-width=&quot;960&quot; data-origin-height=&quot;1226&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;애초에 조회하기 전까지 delete 쿼리가 발생하지도 않는다. (위의 select는 콘서트, 콘서트 티켓에 대한 findAll())&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;JPA의 영속성 컨텍스트를 생각하면 간단하게 답이 나올 것이다.&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;delete의 경우 실제 DB로 &lt;b&gt;제거를 위한 쿼리를 발생시키는 것이 아닌&lt;/b&gt;, &lt;span style=&quot;color: #ef5369;&quot;&gt;단순히 '영속성 컨텍스트'에서 해당 엔티티를 '제거 처리'&lt;/span&gt;를 하는 것이기 때문이다.&lt;/p&gt;
&lt;pre id=&quot;code_1688048585148&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@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;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;실제로, 구현체를 슬쩍 보면 em.remove()를 통해서 entityManager에 존재하는 엔티티에 대해서 제거하는 것을 볼 수 있다.&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;그래서, 실제 DB에 제거 쿼리를 발생시키고 싶다면 아래와 같이 테스트를 수정해야 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1688048636312&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun manyToOne_cascadeType_REMOVE_test_자식_엔티티_제거_자식이_여러개일_경우() {
    // given
    val 콘서트 = Concert(name = &quot;인기 많은 콘서트&quot;, ticketLimit = 10)
    val 콘서트_티켓1 = ConcertTicket(userId = 1L, concert = 콘서트)
    val 콘서트_티켓2 = ConcertTicket(userId = 2L, concert = 콘서트)
    콘서트.addTicket(콘서트_티켓1)
    콘서트.addTicket(콘서트_티켓2)
    concertRepository.save(콘서트)

    val 저장된_콘서트들 = concertRepository.findAll()
    val 저장된_콘서트_티켓들 = concertTicketRepository.findAll()
    assertThat(저장된_콘서트들).hasSize(1)
    assertThat(저장된_콘서트_티켓들).hasSize(2)

    // when
    assertDoesNotThrow {
        concertTicketRepository.delete(콘서트_티켓1)
    }

    assertThatThrownBy {
        entityManager.flush()
    }.isInstanceOf(ConstraintViolationException::class.java)
}&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-06-29 오후 11.27.37.png&quot; data-origin-width=&quot;1510&quot; data-origin-height=&quot;622&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Ig9Pj/btslQoNuo1r/kHUlVG3MrqRMdLPNVnCqZ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Ig9Pj/btslQoNuo1r/kHUlVG3MrqRMdLPNVnCqZ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Ig9Pj/btslQoNuo1r/kHUlVG3MrqRMdLPNVnCqZ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIg9Pj%2FbtslQoNuo1r%2FkHUlVG3MrqRMdLPNVnCqZ0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1510&quot; height=&quot;622&quot; data-filename=&quot;스크린샷 2023-06-29 오후 11.27.37.png&quot; data-origin-width=&quot;1510&quot; data-origin-height=&quot;622&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;enti&lt;/span&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;tyManager에 대해 강제로 플러시&lt;/span&gt;를 해서, 영속성 컨텍스트의 내용을 DB에 반영시키는 것이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이렇게 되면 아까와 다르게 별도의 조회 쿼리를 발생시키지 않더라도 delete 쿼리가 발생한다. 다만, 외래키 제약 조건이 발생하기 때문에 똑같이 ConstraintViolationException은 생길 수밖에 없다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;  DataIntegrityViolationException와 ConstraintViolationException은 모두 외래키 제약 조건 위반에 의해서 발생한다. 다만, ConstraintViolationException의 경우 스프링 프레임워크에서 제공하는 예외 클래스이고, ConstraintViolationException는 하이버네이트에서 제공하는 예외 클래스이다.&lt;/blockquote&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; 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;style1&quot; /&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;size18&quot;&gt;&lt;b&gt; &amp;nbsp;부모&amp;nbsp;엔티티와&amp;nbsp;자식&amp;nbsp;엔티티&amp;nbsp;연관관계&amp;nbsp;끊기&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1688048961605&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
@DisplayName(&quot;@ManyToOne: CascadeType.REMOVE 테스트 - 부모 엔티티에서 자식 엔티티 제거&quot;)
fun manyToOne_cascadeType_REMOVE_test_부모_엔티티에서_자식_엔티티_제거() {
    // given
    val 콘서트 = Concert(name = &quot;인기 많은 콘서트&quot;, ticketLimit = 10)

    val 콘서트_티켓1 = ConcertTicket(userId = 1L, concert = 콘서트)
    val 콘서트_티켓2 = ConcertTicket(userId = 2L, concert = 콘서트)
    콘서트.addTicket(콘서트_티켓1)
    콘서트.addTicket(콘서트_티켓2)
    concertRepository.save(콘서트)

    val 저장된_콘서트들 = concertRepository.findAll()
    val 저장된_콘서트_티켓들 = concertTicketRepository.findAll()
    assertThat(저장된_콘서트들).hasSize(1)
    assertThat(저장된_콘서트_티켓들).hasSize(2)

    // when
    콘서트.concertTickets.remove(콘서트_티켓1)
    콘서트.concertTickets.remove(콘서트_티켓2)

    // then
    val 삭제_이후_저장된_콘서트들 = concertRepository.findAll()
    val 삭제_이후_저장된_콘서트_티켓들 = concertTicketRepository.findAll()

    assertThat(삭제_이후_저장된_콘서트들).hasSize(1)
    assertThat(삭제_이후_저장된_콘서트_티켓들).hasSize(2)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전의 @OneToMany의 경우, 부모 엔티티에서 자식 엔티티를 제거하더라도 아무 일도 발생하지 않았었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마찬가지로 @ManyToOne의 역시 &lt;b&gt;고아 객체가 발생하더라도 실제 DB에 delete 쿼리가 발생하지 않아서&lt;/b&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;style3&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생각보다 글이 길어져서 orphanRemoval에 대한 내용은 2편에서 알아보도록 하자!&lt;/p&gt;</description>
      <category>Back-end/JPA</category>
      <category>CascadeType.REMOVE</category>
      <category>JPA</category>
      <category>OrphanRemoval</category>
      <category>영속성전이</category>
      <author>dolmeng2</author>
      <guid isPermaLink="true">https://cl8d.tistory.com/113</guid>
      <comments>https://cl8d.tistory.com/113#entry113comment</comments>
      <pubDate>Thu, 29 Jun 2023 23:58:40 +0900</pubDate>
    </item>
    <item>
      <title>동시성 문제를 해결해보자! 분산락 구현하기 (네임드락 - Named Lock 활용하기)</title>
      <link>https://cl8d.tistory.com/112</link>
      <description>&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 공부하다가 '&lt;a title=&quot;네임드락&quot; href=&quot;https://cl8d.tistory.com/108&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;네임드락&lt;/a&gt;'에 대해서 알게 되었는데, 네임드락을 사용하면 분산락을 구현할 수 있다는 글을 보고 한 번 테스트해보고 싶어서 글을 작성해보고자 한다. 전체 소스코드는 &lt;a title=&quot;여기&quot; href=&quot;https://github.com/Cl8D/Kotlin-Study/tree/master/ticketService&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;&lt;/p&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;&amp;nbsp;&lt;/h4&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-06-22 오전 2.24.40.png&quot; data-origin-width=&quot;1044&quot; data-origin-height=&quot;818&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5oLgK/btskRNfHBhS/LvkXZVLurnOuiIi42ctqA1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5oLgK/btskRNfHBhS/LvkXZVLurnOuiIi42ctqA1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5oLgK/btskRNfHBhS/LvkXZVLurnOuiIi42ctqA1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5oLgK%2FbtskRNfHBhS%2FLvkXZVLurnOuiIi42ctqA1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;435&quot; height=&quot;341&quot; data-filename=&quot;스크린샷 2023-06-22 오전 2.24.40.png&quot; data-origin-width=&quot;1044&quot; data-origin-height=&quot;818&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분산락이란 &lt;span style=&quot;color: #ef5369;&quot;&gt;멀티&amp;nbsp;스레드 환경에서 공유 자원에 접근&lt;/span&gt;할 때, &lt;b&gt;데이터의 정합성을 지키기 위해&lt;/b&gt; 사용하는 기술이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 여러 스레드가 공유 자원을 접근하며 경쟁하는 상황을 '&lt;b&gt;Race Condition (경쟁 상태)&lt;/b&gt;'라고도 부르며, 자바에서는 '&lt;b&gt;synchronized&lt;/b&gt;'라는 키워드를 통해서 &lt;span style=&quot;color: #ef5369;&quot;&gt;하나의 스레드만 접근할 수 있도록&lt;/span&gt; 동기화 기능을 제공한다. 하지만, 스프링 웹 애플리케이션 환경에서 규모가 커진다면 서버 역시 여러 대로 띄울 확률이 높다. 이러한&lt;b&gt; 다중 서버에서는 synchronized 만으로는 동시성 이슈를 해결할 수 없기 때문에&lt;/b&gt; 다른 방법을 사용해야 하는데, 분산락을 구현하는 다양한 기법들을 통해서 이를 해결할 수 있다.&amp;nbsp;&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;style3&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&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;그래서 간단하게&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;스크린샷 2023-06-22 오후 5.52.33.png&quot; data-origin-width=&quot;650&quot; data-origin-height=&quot;456&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QCjaq/btskXONddAS/oh7JQ8rOy6GWiDqotDL0EK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QCjaq/btskXONddAS/oh7JQ8rOy6GWiDqotDL0EK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QCjaq/btskXONddAS/oh7JQ8rOy6GWiDqotDL0EK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQCjaq%2FbtskXONddAS%2Foh7JQ8rOy6GWiDqotDL0EK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;376&quot; height=&quot;264&quot; data-filename=&quot;스크린샷 2023-06-22 오후 5.52.33.png&quot; data-origin-width=&quot;650&quot; data-origin-height=&quot;456&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;콘서트와 티켓은 1:N 관계를 가지고, &lt;b&gt;콘서트는 몇 좌석까지 가질 수 있는지에 대한 개수&lt;/b&gt;를 가지고 있는다. (ticketLimit)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1687423690953&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
class Concert(
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long = 0L,

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

    @Column(nullable = false)
    val ticketLimit: Int,

    concertTickets: MutableList&amp;lt;ConcertTicket&amp;gt; = Collections.emptyList()
) {

    @OneToMany(fetch = FetchType.LAZY, mappedBy = &quot;concert&quot;)
    val concertTickets: MutableList&amp;lt;ConcertTicket&amp;gt; = concertTickets.toMutableList()

    fun isFull(): Boolean {
        return concertTickets.size &amp;gt;= ticketLimit
    }
}&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;pre id=&quot;code_1687423733413&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
class ConcertTicket(
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long = 0L,

    @Column(nullable = false)
    val userId: Long,

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(
        name = &quot;concert_id&quot;,
        foreignKey = ForeignKey(name = &quot;fk_ticket_concert_id&quot;)
    )
    val concert: Concert
)&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;pre id=&quot;code_1687423624979&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CREATE TABLE `concert_ticket` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `user_id` bigint NOT NULL,
  `concert_id` bigint NOT NULL,
  PRIMARY KEY (`id`),
  KEY `fk_ticket_concert_id` (`concert_id`),
  CONSTRAINT `fk_ticket_concert_id` FOREIGN KEY (`concert_id`) REFERENCES `concert` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1687423642487&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CREATE TABLE `concert` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `name` varchar(255) NOT NULL,
  `ticket_limit` int NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테이블은 위와 같이 JPA가 만들어 주었다.&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;style3&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&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;  Repository&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1687501507463&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun ConcertRepository.getConcertById(id: Long): Concert {
    return findConcertById(id) ?: throw NotFoundException(&quot;콘서트 정보가 존재하지 않습니다.&quot;)
}

interface ConcertRepository : JpaRepository&amp;lt;Concert, Long&amp;gt; {
    fun findConcertById(id: Long): Concert?
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1687501515265&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun ConcertTicketRepository.getConcertTicketById(id: Long): ConcertTicket {
    return findConcertTicketById(id) ?: throw NotFoundException(&quot;콘서트 티켓 정보가 존재하지 않습니다.&quot;)
}

interface ConcertTicketRepository : JpaRepository&amp;lt;ConcertTicket, Long&amp;gt; {
    fun findConcertTicketById(id: Long): ConcertTicket?
}&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;size18&quot;&gt;&lt;b&gt;  Service&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1687449733151&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
@Transactional(readOnly = true)
class ConcertService(
    private val concertRepository: ConcertRepository,
    private val concertTicketRepository: ConcertTicketRepository
) {
    ...
    
    @Transactional
    fun createConcertTicket(
        concertId: Long,
        concertTicketCreateRequest: ConcertTicketCreateRequest
    ): Long {
        val concert = concertRepository.getConcertById(concertId)
        if (concert.isFull()) {
            throw ConcertFullException(&quot;정원이 가득 찼습니다.&quot;)
        }
        val concertTicket =
            ConcertTicket(userId = concertTicketCreateRequest.userId, concert = concert)
        val savedConcertTicket = concertTicketRepository.save(concertTicket)
        return savedConcertTicket.id
    }

    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 중요한 메서드는 티켓에 대한 생성 메서드이기 때문에, 이 부분만 주목하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;콘서트 티켓 생성 시 &lt;b&gt;정원이 가득 차면 예외가 발생하도록&lt;/b&gt; 만들었다. 만약 요청이 여러 개가 오다가 정원이 가득차면 더 이상 insert는 진행되지 않을 것이다.&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;  Controller&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1687459940878&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RestController
@RequestMapping(&quot;/concerts&quot;)
class ConcertController(
    private val concertService: ConcertService
) {
    ...

    @PostMapping(&quot;/tickets/{concertId}&quot;)
    fun createConcertTicket(
        @PathVariable(&quot;concertId&quot;) concertId: Long,
        @RequestBody concertTicketCreateRequest: ConcertTicketCreateRequest
    ) {
        concertService.createConcertTicket(concertId, concertTicketCreateRequest)
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p 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;티켓을 생성하는 API이다. 콘서트나 티켓 조회 같은 간단한 API는 전체 소스코드에 남겨두었다.&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;   JMeter 테스트&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부하 테스트를 어떻게 진행할까 고민했는데, &lt;span style=&quot;color: #ef5369;&quot;&gt;JMeter를 활용&lt;/span&gt;하면 좋을 것 같아서 진행해보았다.&lt;/p&gt;
&lt;pre id=&quot;code_1687501628212&quot; class=&quot;mipsasm&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;java -jar -Dserver.port=8080 ticketService.jar
java -jar -Dserver.port=8081 ticketService.jar&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저, 분산된 환경을 만들기 위해 8080, 8081 2개의 포트를 띄워서 진행하였다.&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;스크린샷 2023-06-23 오후 12.54.32.png&quot; data-origin-width=&quot;898&quot; data-origin-height=&quot;114&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rnVm5/btsk7qcDxcc/cxGKm9H00h87hAacoA5vtK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rnVm5/btsk7qcDxcc/cxGKm9H00h87hAacoA5vtK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rnVm5/btsk7qcDxcc/cxGKm9H00h87hAacoA5vtK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrnVm5%2Fbtsk7qcDxcc%2FcxGKm9H00h87hAacoA5vtK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;781&quot; height=&quot;99&quot; data-filename=&quot;스크린샷 2023-06-23 오후 12.54.32.png&quot; data-origin-width=&quot;898&quot; data-origin-height=&quot;114&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;샘플 데이터로 콘서트 1번에 대한 데이터를 미리 삽입해두었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 콘서트의 제한 인원은 100명으로, 예상 시나리오라면 &lt;span style=&quot;color: #ef5369;&quot;&gt;100명 이상이 자리잡았을 때 오류가 발생할 것&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;200개의 스레드를 활용하여&lt;/b&gt; &lt;b&gt;2개의 프로세스&lt;/b&gt;에서 동시에 콘서트 티켓 발행 요청을 날렸다.&lt;/p&gt;
&lt;pre id=&quot;code_1687584975944&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;select count(*) from concert_ticket;&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-06-23 오후 3.14.08.png&quot; data-origin-width=&quot;388&quot; data-origin-height=&quot;112&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MuCf2/btsk9dxAlo9/EqUXfk02PVvysjUrsPFUiK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MuCf2/btsk9dxAlo9/EqUXfk02PVvysjUrsPFUiK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MuCf2/btsk9dxAlo9/EqUXfk02PVvysjUrsPFUiK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMuCf2%2Fbtsk9dxAlo9%2FEqUXfk02PVvysjUrsPFUiK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;388&quot; height=&quot;112&quot; data-filename=&quot;스크린샷 2023-06-23 오후 3.14.08.png&quot; data-origin-width=&quot;388&quot; data-origin-height=&quot;112&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 결과, 100명까지만 받아야 하는데도 불구하고 111개의 값이 들어간 것을 확인할 수 있다.&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;스크린샷 2023-06-23 오후 3.20.45.png&quot; data-origin-width=&quot;1098&quot; data-origin-height=&quot;1282&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/t0r9n/btsk26NVaUB/xd8UDKzAIWznxH0iHHHdn0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/t0r9n/btsk26NVaUB/xd8UDKzAIWznxH0iHHHdn0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/t0r9n/btsk26NVaUB/xd8UDKzAIWznxH0iHHHdn0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Ft0r9n%2Fbtsk26NVaUB%2Fxd8UDKzAIWznxH0iHHHdn0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;574&quot; height=&quot;670&quot; data-filename=&quot;스크린샷 2023-06-23 오후 3.20.45.png&quot; data-origin-width=&quot;1098&quot; data-origin-height=&quot;1282&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 개의 스레드가 조회하는 시점에서는 분명 가득차있지 않았지만, &lt;span style=&quot;color: #ef5369;&quot;&gt;검증을 통과해버린 스레드가 동시에 여러 개가 발생하면서&lt;/span&gt; insert 연산 역시 한 번에 진행되어 100개가 넘는 데이터가 삽입된 것이다.&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;style3&quot; /&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  Named Lock으로 분산락 구현하기&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;간단하다.&lt;span style=&quot;color: #ef5369;&quot;&gt; 티켓을 저장하는 로직에 대해서 순차적으로 처리할 수 있도록&lt;/span&gt; 만들면 되는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1687501483621&quot; class=&quot;kotlin&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun LockRepository.executeWithLock(
    lockName: String, timeout: String, action: () -&amp;gt; Unit) {
    try {
        getLock(lockName, timeout)
        action()
    } finally {
        releaseLock(lockName)
    }
}

interface LockRepository: JpaRepository&amp;lt;ConcertTicket, Long&amp;gt; {

    @Query(&quot;select get_lock(:name, :time)&quot;, nativeQuery = true)
    fun getLock(@Param(value = &quot;name&quot;) name: String,
                @Param(value = &quot;time&quot;) time: String)
                
    @Query(&quot;select release_lock(:name)&quot;, nativeQuery = true)
    fun releaseLock(@Param(value = &quot;name&quot;) name: String)
}&lt;/code&gt;&lt;/pre&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;span style=&quot;color: #ef5369;&quot;&gt;락을 획득하는 getLock()과 락을 해제하는 releaseLock()&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;메서드를 선언하였다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;두 가지 모두 jpql이 아닌 native query를 사용해야 하기 때문에, nativeQuery 옵션을 true로 선언해주었다.&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;getLock의 경우 파라미터로&lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;입력받은 name에 대해서, 입력받은 time 만큼 락을 획득하기를 시도&lt;/b&gt;한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;만약 time 값으로 음수를 넣으면 락을 획득할 때까지 무한 대기하기 때문에 주의해야 한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;나는 MySQL 8.0을 사용하고 있어서 name으로 60자 이내를 넣어야 한다.&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;releaseLock의 경우 입력받은 name에 대해서 획득한 락을 해제하는 역할을 한다.&lt;/p&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;span style=&quot;color: #ef5369;&quot;&gt;명시적으로 락을 꼭 해제해줘야 하기 때문에&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&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;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;메서드 전후로 락을 걸고 해제하도록 만들기 위해&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;executeWithLock() 메서드를 만들어 주었다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;락 해제의 경우&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;finally를 통해 예외가 발생하더라도 꼭 실행되도록 만들었다&lt;/span&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;pre id=&quot;code_1687501456500&quot; class=&quot;kotlin&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
class ConcertServiceFacade(
    private val lockRepository: LockRepository,
    private val concertService: ConcertService
){
    @Transactional
    fun createConcertTicket(
        concertId: Long,
        concertTicketCreateRequest: ConcertTicketCreateRequest
    ) {
        val lockName = &quot;concert_$concertId&quot;
        val timeout = &quot;3000&quot;
        lockRepository.executeWithLock(lockName, timeout) {
            concertService.createConcertTicket(concertId, concertTicketCreateRequest)
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고,&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;facade 패턴을 사용하여 lockRepository에 대한 부분을 분리&lt;/span&gt;하였다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;비즈니스 로직과 DB 관련 로직을 분리하는 차원에서 별도의 서비스를 생성했다고 생각하면 된다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;lockName으로는 중복되지 않게 하게 위해서&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;concert_라는 prefix와 concertId를 생성해주었고, 타임아웃은 3초 정도 주었다.&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;pre id=&quot;code_1687502123559&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Transactional(propagation = Propagation.REQUIRES_NEW)
fun createConcertTicket(
    concertId: Long,
    concertTicketCreateRequest: ConcertTicketCreateRequest
) {
	...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그리고, ⭐️ 기존의 콘서트 티켓 발행 로직에서 &lt;span style=&quot;color: #ef5369;&quot;&gt;트랜잭션 전파 속성을 변경&lt;/span&gt;해줘야 한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이는,&lt;b&gt; 락을 제어하는 커넥션과 비즈니스 로직을 위한 커넥션을 분리하기 위해서&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;b&gt; 락이 함께 반환되지 않을 수 있다&lt;/b&gt;. 또한, 비즈니스 로직이 수행하는 역할이 많다면 (위 상황은 아니지만, 외부 호출을 진행하거나, 로직 자체가 길거나) 트랜잭션이 길어지기 때문에 &lt;b&gt;락에 대한 제어과 비즈니스 로직의 트랜잭션을 분리하는 것이 좋다&lt;/b&gt;.&amp;nbsp;&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;pre id=&quot;code_1687501689102&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@PostMapping(&quot;/tickets-lock/{concertId}&quot;)
fun createConcertTicketWithLock(
    @PathVariable(&quot;concertId&quot;) concertId: Long,
    @RequestBody concertTicketCreateRequest: ConcertTicketCreateRequest
) {
    concertServiceFacade.createConcertTicket(concertId, concertTicketCreateRequest)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제, 만든 facade 클래스를 호출하는 간단한 end-point를 추가하여 호출해보자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-06-23 오후 4.57.01.png&quot; data-origin-width=&quot;340&quot; data-origin-height=&quot;458&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DxKJ3/btsk8hH9Zef/X5EnBUZdAi0cM3RX3wKjh0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DxKJ3/btsk8hH9Zef/X5EnBUZdAi0cM3RX3wKjh0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DxKJ3/btsk8hH9Zef/X5EnBUZdAi0cM3RX3wKjh0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDxKJ3%2Fbtsk8hH9Zef%2FX5EnBUZdAi0cM3RX3wKjh0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;340&quot; height=&quot;458&quot; data-filename=&quot;스크린샷 2023-06-23 오후 4.57.01.png&quot; data-origin-width=&quot;340&quot; data-origin-height=&quot;458&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-06-23 오후 4.59.13.png&quot; data-origin-width=&quot;2044&quot; data-origin-height=&quot;200&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/B4vna/btsk7o8P8Cc/UWWos6kDPB6kKen8ZAUUg1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/B4vna/btsk7o8P8Cc/UWWos6kDPB6kKen8ZAUUg1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/B4vna/btsk7o8P8Cc/UWWos6kDPB6kKen8ZAUUg1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FB4vna%2Fbtsk7o8P8Cc%2FUWWos6kDPB6kKen8ZAUUg1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2044&quot; height=&quot;200&quot; data-filename=&quot;스크린샷 2023-06-23 오후 4.59.13.png&quot; data-origin-width=&quot;2044&quot; data-origin-height=&quot;200&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;2023-06-23T16:57:19.359+09:00 ERROR 81614 --- [o-8080-exec-289] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: org.springframework.transaction.CannotCreateTransactionException: Could not open JPA EntityManager for transaction] with root cause java.sql.SQLTransientConnectionException: HikariPool-1 - &lt;span style=&quot;color: #ef5369;&quot;&gt;Connection is not available, request timed out after 30005ms. at&lt;/span&gt; com.zaxxer.hikari.pool.HikariPool.createTimeoutException(HikariPool.java:696) ~[HikariCP-5.0.1.jar!/:na] at com.zaxxer.hikari.pool.HikariPool.getConnection(HikariPool.java:181) ~[HikariCP-5.0.1.jar!/:na] at com.zaxxer.hikari.pool.HikariPool.getConnection(HikariPool.java:146) ~[HikariCP-5.0.1.jar!/:na] at com.zaxxer.hikari.HikariDataSource.getConnection(HikariDataSource.java:128) ~[HikariCP-5.0.1.jar!/:na]&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-06-23 오후 5.00.24.png&quot; data-origin-width=&quot;402&quot; data-origin-height=&quot;310&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EMlNu/btslafoGmxi/mNuKKJmVV6t1wX7MD2iNJK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EMlNu/btslafoGmxi/mNuKKJmVV6t1wX7MD2iNJK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EMlNu/btslafoGmxi/mNuKKJmVV6t1wX7MD2iNJK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEMlNu%2FbtslafoGmxi%2FmNuKKJmVV6t1wX7MD2iNJK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;402&quot; height=&quot;310&quot; data-filename=&quot;스크린샷 2023-06-23 오후 5.00.24.png&quot; data-origin-width=&quot;402&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;하지만, 성공적으로 락을 얻었다면 insert 쿼리 이후 락을 해제하게 된다.&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-06-23 오후 5.02.31.png&quot; data-origin-width=&quot;404&quot; data-origin-height=&quot;124&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/8QwlZ/btsk8nhil0T/tPUhZNYM9IqcjwdK3wNQk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/8QwlZ/btsk8nhil0T/tPUhZNYM9IqcjwdK3wNQk1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/8QwlZ/btsk8nhil0T/tPUhZNYM9IqcjwdK3wNQk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F8QwlZ%2Fbtsk8nhil0T%2FtPUhZNYM9IqcjwdK3wNQk1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;404&quot; height=&quot;124&quot; data-filename=&quot;스크린샷 2023-06-23 오후 5.02.31.png&quot; data-origin-width=&quot;404&quot; data-origin-height=&quot;124&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종적으로 실행 결과를 보면 위와 같이 딱 100개의 정원만 차게 된다.&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;style3&quot; /&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  Connection Pool Size 조절하기&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1687582790667&quot; class=&quot;yaml&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring:
  datasource:
    hikari:
      maximum-pool-size: 100&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스레드를 200개로 했기 때문에 connection pool size를 조금 더 늘려서 테스트를 해봐도 괜찮지 않을까 생각이 들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서&lt;span style=&quot;color: #ef5369;&quot;&gt; connection pool size를 100으로 설정&lt;/span&gt;하고 테스트를 진행해보았다. (default는 10이다)&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;스크린샷 2023-06-24 오후 1.59.26.png&quot; data-origin-width=&quot;2026&quot; data-origin-height=&quot;178&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CGtGT/btslcCxvdkD/oRrIroxIFtCW6DUxB37af1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CGtGT/btslcCxvdkD/oRrIroxIFtCW6DUxB37af1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CGtGT/btslcCxvdkD/oRrIroxIFtCW6DUxB37af1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCGtGT%2FbtslcCxvdkD%2FoRrIroxIFtCW6DUxB37af1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2026&quot; height=&quot;178&quot; data-filename=&quot;스크린샷 2023-06-24 오후 1.59.26.png&quot; data-origin-width=&quot;2026&quot; data-origin-height=&quot;178&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: org.springframework.transaction.CannotCreateTransactionException: Could not open JPA EntityManager for transaction] with root cause java.sql.SQLNonTransientConnectionException: Data source rejected establishment of connection, message from server: &quot;Too many connections&quot; at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:111) ~[mysql-connector-j-8.0.33.jar!/:8.0.33]&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전에 비해서 티켓 삽입에 대한 속도는 빨라졌지만, 위와 같이 '&lt;b&gt;Too many connections&lt;/b&gt;' 오류가 발생하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1687582592113&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;show variables like 'max_connections';&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-06-24 오후 2.02.43.png&quot; data-origin-width=&quot;674&quot; data-origin-height=&quot;110&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/z20yc/btsk9EwPHq4/vUCby2ORkSEVLiVcSDLir0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/z20yc/btsk9EwPHq4/vUCby2ORkSEVLiVcSDLir0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/z20yc/btsk9EwPHq4/vUCby2ORkSEVLiVcSDLir0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fz20yc%2Fbtsk9EwPHq4%2FvUCby2ORkSEVLiVcSDLir0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;674&quot; height=&quot;110&quot; data-filename=&quot;스크린샷 2023-06-24 오후 2.02.43.png&quot; data-origin-width=&quot;674&quot; data-origin-height=&quot;110&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확인해보니, 기본적으로&amp;nbsp;mysql에서 제공하는 &lt;span style=&quot;color: #ef5369;&quot;&gt;최대 커넥션 수는 151&lt;/span&gt;개이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2대의 서버에서 100개씩 점유하고, &lt;b&gt;총 200개의 요청이 오다 보니&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;이런 상황에서는 mysql 서버의 max_connections 수를 충분히 늘리거나, 혹은 hikari pool size를 좀 더 줄이는 방향이 있다. 나는 hikari pool size를 줄이는 방향으로 진행했다.&lt;span style=&quot;color: #ef5369;&quot;&gt; 75로 진행한다면 딱 151개니까 오류가 발생하지 않을 것이라고 &lt;/span&gt;예측했다. (1개는 아무 클라이언트가 연결되어 있지 않아도 기본적으로 점유하고 있다. 아마 MySQL 내부에서 사용하는 스레드가 아닐까 추측한다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1687584750473&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;show status where `variable_name` = 'Threads_connected';&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-06-24 오후 2.33.17.png&quot; data-origin-width=&quot;662&quot; data-origin-height=&quot;114&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/4svCk/btslbw5vidO/0lk1On3d4R4KzKZYOBntJK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/4svCk/btslbw5vidO/0lk1On3d4R4KzKZYOBntJK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/4svCk/btslbw5vidO/0lk1On3d4R4KzKZYOBntJK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F4svCk%2Fbtslbw5vidO%2F0lk1On3d4R4KzKZYOBntJK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;662&quot; height=&quot;114&quot; data-filename=&quot;스크린샷 2023-06-24 오후 2.33.17.png&quot; data-origin-width=&quot;662&quot; data-origin-height=&quot;114&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 2대의 서버를 띄운 다음, 연결된 스레드의 수를 보면 정확하게 151개를 점유한 것을 볼 수 있다.&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;스크린샷 2023-06-24 오후 2.30.01.png&quot; data-origin-width=&quot;440&quot; data-origin-height=&quot;142&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Mxst5/btsk9d0zPlr/PUxRHTVESyNshPyTrjDe7k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Mxst5/btsk9d0zPlr/PUxRHTVESyNshPyTrjDe7k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Mxst5/btsk9d0zPlr/PUxRHTVESyNshPyTrjDe7k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMxst5%2Fbtsk9d0zPlr%2FPUxRHTVESyNshPyTrjDe7k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;381&quot; height=&quot;123&quot; data-filename=&quot;스크린샷 2023-06-24 오후 2.30.01.png&quot; data-origin-width=&quot;440&quot; data-origin-height=&quot;142&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;티켓 생성 결과를 보니까 정확하게 100개만큼 차있는 것을 확인할 수 있다.&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;스크린샷 2023-06-24 오후 2.30.18.png&quot; data-origin-width=&quot;974&quot; data-origin-height=&quot;74&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eDwObB/btslgiFnuUX/69gwMP22WE0Zj3MkJDYKQ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eDwObB/btslgiFnuUX/69gwMP22WE0Zj3MkJDYKQ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eDwObB/btslgiFnuUX/69gwMP22WE0Zj3MkJDYKQ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeDwObB%2FbtslgiFnuUX%2F69gwMP22WE0Zj3MkJDYKQ1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;974&quot; height=&quot;74&quot; data-filename=&quot;스크린샷 2023-06-24 오후 2.30.18.png&quot; data-origin-width=&quot;974&quot; data-origin-height=&quot;74&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한, grep을 통해 too many connections 오류가 발생했는지 확인했는데, 100개가 차는 동안 발생하지 않았다.&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;style3&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 포스팅에서는 간단하게 named lock을 통해서 분산락을 구현해보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단일 서버가 아닌 다중 서버에서 동시성이 중요한 기능을 구축해야 할 때 redis 같은 외부 인프라 서비스 구축 비용이 부담된다면 이렇게 네임드락을 활용해서도 충분히 진행할 수 있다는 생각이 들었다. 다만, 네임드락에 대한 이해도 있어야 하기 때문에 락에 대한 모니터링을 철저하게 해야겠다는 생각이 들었다. 특히, 타임아웃을 적절하게 설정해서 꼭 점유한 락에 대해서 해제할 수 있도록 하는 게 중요하다는 걸 느꼈다. 또한, 지금은 엄청 작은 규모의 테스트였지만, 실제 운영에서 thread 수와 connection 수를 잘 설정해서 사용자의 요청을 처리해야겠다고 생각했다. 다음 포스팅에서는 redis를 활용해서 분산락을 구현해봐야겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;  REFERENCE&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&lt;a href=&quot;https://hudi.blog/distributed-lock-with-redis/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://hudi.blog/distributed-lock-with-redis/&lt;/a&gt;&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&lt;a href=&quot;https://sudal.site/namedLock/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://sudal.site/namedLock/&lt;/a&gt;&lt;/i&gt;&lt;/p&gt;</description>
      <category>개발일지</category>
      <category>Named Lock</category>
      <category>REDIS</category>
      <category>네임드락</category>
      <category>동시성 이슈</category>
      <category>분산락</category>
      <author>dolmeng2</author>
      <guid isPermaLink="true">https://cl8d.tistory.com/112</guid>
      <comments>https://cl8d.tistory.com/112#entry112comment</comments>
      <pubDate>Fri, 23 Jun 2023 17:39:08 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] Kotlin에서 data class의 필드가 1개일 때 역직렬화가 안 되는 문제 (no delegate- or property-based Creator)</title>
      <link>https://cl8d.tistory.com/111</link>
      <description>&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;간단하게 테스트하고 싶은 내용이 생겨서 DTO용 data class를 생성하였는데, 아래와 같이 Jackson의 역직렬화 관련 오류가 발생하였다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Resolved [org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Cannot construct instance of `study.ticketService.domain.application.dto.ConcertTicketCreateRequest` (although at least one Creator exists): cannot deserialize from Object value (no delegate- or property-based Creator)]&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;pre id=&quot;code_1687497150026&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;data class ConcertTicketCreateRequest(
    @NotNull(message = &quot;사용자 아이디는 비어있을 수 없습니다.&quot;)
    val userId: Long
)&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;문제가 된 data class는 위와 같다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1687497751743&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;Jackson fails to deserialize 1 field POJO &amp;middot; Issue #3085 &amp;middot; FasterXML/jackson-databind&quot; data-og-description=&quot;Describe the bug When trying to deserialize JSON to POJO it fails when POJO has only one field. Version information 2.11.4 To Reproduce POJO: @AllArgsConstructor //lombok, if i add constructor manu...&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/FasterXML/jackson-databind/issues/3085&quot; data-og-url=&quot;https://github.com/FasterXML/jackson-databind/issues/3085&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cRq5R9/hyS5ybdPNh/ytZBOGEzFPvBfWzNhd9O2k/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/FasterXML/jackson-databind/issues/3085&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/FasterXML/jackson-databind/issues/3085&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cRq5R9/hyS5ybdPNh/ytZBOGEzFPvBfWzNhd9O2k/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;Jackson fails to deserialize 1 field POJO &amp;middot; Issue #3085 &amp;middot; FasterXML/jackson-databind&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Describe the bug When trying to deserialize JSON to POJO it fails when POJO has only one field. Version information 2.11.4 To Reproduce POJO: @AllArgsConstructor //lombok, if i add constructor manu...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 깃허브 이슈에 가면 친절하게 그 이유를 설명해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필드가 1개인 경우 기본 생성자를 만드는 전략이&lt;b&gt; Delegating / Properties-based로 나뉘는데&lt;/b&gt;, &lt;span style=&quot;color: #ef5369;&quot;&gt;두 모드 중에서 어떤 것을 선택해야 할지 알 수 없어서 발생한 오류&lt;/span&gt;인 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 이슈에서는 @JsonCreator 어노테이션을 통해서 명시적으로 모드를 지정해주는 방법을 소개하는데, data class에서 어떻게 기본 생성자를 만들 수 있을까?&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;style3&quot; /&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&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;span style=&quot;color: #ef5369;&quot;&gt;기본값을 명시적으로 지정&lt;span style=&quot;color: #333333;&quot;&gt;하거나&lt;/span&gt;, 부생성자를 활용&lt;span style=&quot;color: #333333;&quot;&gt;하거나&lt;/span&gt;, @JsonProperty&lt;span style=&quot;color: #333333;&quot;&gt;를 사용하면 된다&lt;/span&gt;&lt;/span&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;  기본 값 지정하기&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1687499894720&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;data class ConcertTicketCreateRequest(
    @NotNull(message = &quot;사용자 아이디는 비어있을 수 없습니다.&quot;)
    val userId: Long = 0L
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주생성자 자체에 기본 값을 지정해준다. 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;&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_1687498061259&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;data class ConcertTicketCreateRequest(
    @NotNull(message = &quot;사용자 아이디는 비어있을 수 없습니다.&quot;)
    val userId: Long
) {
    constructor() : this(0)
}&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;  @JsonProperty 활용하기&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1687500138388&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;data class ConcertTicketCreateRequest(
    @JsonProperty(&quot;userId&quot;)
    @NotNull(message = &quot;사용자 아이디는 비어있을 수 없습니다.&quot;)
    val userId: Long
)&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;이때는 @JsonProperty를 통해서 명시적으로 json 값의 key와 해당 객체의 필드를 매칭시켜줄 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 어노테이션에 대해서 좀 더 알고 싶다면 &lt;a title=&quot;이 글&quot; href=&quot;https://cl8d.tistory.com/66&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;&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;개인적으로는 1번 방법이 가장 깔끔해서 그냥 기본값을 지정해주는 걸로 만들었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-06-23 오후 3.02.07.png&quot; data-origin-width=&quot;480&quot; data-origin-height=&quot;312&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/njy8Q/btsk7ZG5J8q/ND8keCBVMCnH95qkUKvNg0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/njy8Q/btsk7ZG5J8q/ND8keCBVMCnH95qkUKvNg0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/njy8Q/btsk7ZG5J8q/ND8keCBVMCnH95qkUKvNg0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fnjy8Q%2Fbtsk7ZG5J8q%2FND8keCBVMCnH95qkUKvNg0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;391&quot; height=&quot;254&quot; data-filename=&quot;스크린샷 2023-06-23 오후 3.02.07.png&quot; data-origin-width=&quot;480&quot; data-origin-height=&quot;312&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오류 없이 잘 들어가는 것을 볼 수 있다.&lt;/p&gt;</description>
      <category>개발일지</category>
      <category>data class</category>
      <category>Jackson</category>
      <category>kotlin</category>
      <category>역직렬화</category>
      <category>직렬화</category>
      <author>dolmeng2</author>
      <guid isPermaLink="true">https://cl8d.tistory.com/111</guid>
      <comments>https://cl8d.tistory.com/111#entry111comment</comments>
      <pubDate>Fri, 23 Jun 2023 15:03:27 +0900</pubDate>
    </item>
    <item>
      <title>[MySQL] 트랜잭션 격리 수준을 쿼리를 통해 직접 테스트해보기</title>
      <link>https://cl8d.tistory.com/110</link>
      <description>&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;a title=&quot;리마큐 포스팅&quot; href=&quot;https://cl8d.tistory.com/109&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;리마큐 포스팅&lt;/a&gt;에서 그림으로 설명했던 'CREW' 테이블에 대해서 직접 &lt;b&gt;코드 레벨&lt;/b&gt;로 작성해보고자 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1687331533775&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CREATE TABLE `CREW` (
  `id` bigint NOT NULL,
  `name` varchar(255) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB;&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;사전 설정을 위해서 스토리지 엔진을 innoDB로 설정한 'CREW' 테이블을 생성해준다.&lt;/p&gt;
&lt;pre id=&quot;code_1687333306685&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SET AUTOCOMMIT = false;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;⭐️ 그리고, &lt;span style=&quot;color: #ef5369;&quot;&gt;autocommit 모드는 꼭 OFF로 설정&lt;/span&gt;해두고 진행하자.&lt;/p&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;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;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  READ UNCOMMITTED&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;1283&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Coaho/btskLsKejgW/SE0b8WXRok8uQ5NUjUBPWk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Coaho/btskLsKejgW/SE0b8WXRok8uQ5NUjUBPWk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Coaho/btskLsKejgW/SE0b8WXRok8uQ5NUjUBPWk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCoaho%2FbtskLsKejgW%2FSE0b8WXRok8uQ5NUjUBPWk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;470&quot; height=&quot;471&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;1283&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1687331946759&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;글로벌 설정으로 격리 수준을 변경하면 여파가 크기 때문에 각 세션에 대해서 위와 같이 READ UNCOMMITTED를 지정해주었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1687332002308&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT @@SESSION.transaction_isolation;&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-06-21 오후 4.20.07.png&quot; data-origin-width=&quot;780&quot; data-origin-height=&quot;114&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9bJUt/btskSprpd4d/gpYQwOmTitxHN9nIgsBcnK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9bJUt/btskSprpd4d/gpYQwOmTitxHN9nIgsBcnK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9bJUt/btskSprpd4d/gpYQwOmTitxHN9nIgsBcnK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9bJUt%2FbtskSprpd4d%2FgpYQwOmTitxHN9nIgsBcnK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;671&quot; height=&quot;98&quot; data-filename=&quot;스크린샷 2023-06-21 오후 4.20.07.png&quot; data-origin-width=&quot;780&quot; data-origin-height=&quot;114&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 세션 정보를 확인해보면 위와 같이 &lt;span style=&quot;color: #ef5369;&quot;&gt;READ-UNCOMMITTED&lt;/span&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;style1&quot; /&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;  Dirty Read 확인해보기 - INSERT&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1687331603543&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;START TRANSACTION;
INSERT INTO CREW (id, name) VALUES (1, 'hello');
commit;&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-06-21 오후 4.14.15.png&quot; data-origin-width=&quot;452&quot; data-origin-height=&quot;112&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dLcfHk/btskSnf3tfh/CIfFjTdyjCaIZKKtajRuQ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dLcfHk/btskSnf3tfh/CIfFjTdyjCaIZKKtajRuQ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dLcfHk/btskSnf3tfh/CIfFjTdyjCaIZKKtajRuQ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdLcfHk%2FbtskSnf3tfh%2FCIfFjTdyjCaIZKKtajRuQ1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;452&quot; height=&quot;112&quot; data-filename=&quot;스크린샷 2023-06-21 오후 4.14.15.png&quot; data-origin-width=&quot;452&quot; data-origin-height=&quot;112&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고, 그림이랑 똑같이 만들기 위해서 우선 1번 레코드인 'hello'를 삽입해두자. (어떤 세션이든 상관 없다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1687332121866&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 세션 1
START TRANSACTION;
INSERT INTO CREW (id, name) VALUES (2, 'journey');&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후, 세션 1에서 Commit 하지 않고 단순히 2번 레코드 'journey'를 삽입하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1687332261781&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 세션 2
select * from CREW;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;420&quot; data-origin-height=&quot;156&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c3cdtO/btskK2d2NYS/C5aK1OKRKejFQdF4fbFRC1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c3cdtO/btskK2d2NYS/C5aK1OKRKejFQdF4fbFRC1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c3cdtO/btskK2d2NYS/C5aK1OKRKejFQdF4fbFRC1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc3cdtO%2FbtskK2d2NYS%2FC5aK1OKRKejFQdF4fbFRC1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;420&quot; height=&quot;156&quot; data-origin-width=&quot;420&quot; data-origin-height=&quot;156&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세션 2에서 결과를 확인해보면, 세션 1에서 &lt;span style=&quot;color: #ef5369;&quot;&gt;아직 커밋을 하지 않았음에도 삽입이 되어 있는 것을 확인&lt;/span&gt;할 수 있다. (&lt;b&gt;Dirty 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;그럼, 이 상태에서 세션 1을 롤백시키면 어떻게 될까?&lt;/p&gt;
&lt;pre id=&quot;code_1687332374253&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 세션 1
ROLLBACK;&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-06-21 오후 4.53.17.png&quot; data-origin-width=&quot;444&quot; data-origin-height=&quot;118&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c7H2E2/btskKDk8Dtt/ytOQUlNMfjKbF78dzk1RKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c7H2E2/btskKDk8Dtt/ytOQUlNMfjKbF78dzk1RKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c7H2E2/btskKDk8Dtt/ytOQUlNMfjKbF78dzk1RKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc7H2E2%2FbtskKDk8Dtt%2FytOQUlNMfjKbF78dzk1RKk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;444&quot; height=&quot;118&quot; data-filename=&quot;스크린샷 2023-06-21 오후 4.53.17.png&quot; data-origin-width=&quot;444&quot; data-origin-height=&quot;118&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;롤백 사항이 반영되어 데이터가 나오지 않는 것을 볼 수 있다.&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;  Dirty Read 확인해보기 - UPDATE&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;⭐️ 위의 경우보다 update 시에 더 명확하게 드러나는 것 같아서 한 가지 더 테스트를 하고자 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1687334607723&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 기본 데이터 삽입 후 커밋
START TRANSACTION;
INSERT INTO CREW (id, name) VALUES (1, 'hello');
COMMIT;

# 해당 데이터에 대해서 업데이트
START TRANSACTION;
UPDATE CREW SET name = 'hi' where id = 1;&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;아까처럼 hello라는 레코드가 삽입된 다음, 업데이트에 대한 트랜잭션을 시작하고 커밋을 하지 않은 상태이다.&lt;/p&gt;
&lt;pre id=&quot;code_1687334648704&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 세션 2
SELECT * FROM CREW;&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-06-21 오후 5.04.18.png&quot; data-origin-width=&quot;454&quot; data-origin-height=&quot;94&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dbjA1d/btskRNGcNkn/tLAl5IPEyxvm4gOBTrsKuK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dbjA1d/btskRNGcNkn/tLAl5IPEyxvm4gOBTrsKuK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dbjA1d/btskRNGcNkn/tLAl5IPEyxvm4gOBTrsKuK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdbjA1d%2FbtskRNGcNkn%2FtLAl5IPEyxvm4gOBTrsKuK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;454&quot; height=&quot;94&quot; data-filename=&quot;스크린샷 2023-06-21 오후 5.04.18.png&quot; data-origin-width=&quot;454&quot; data-origin-height=&quot;94&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, READ UNCOMMITTED로 인해서 커밋되지 않은 사항에 대해서도&lt;span style=&quot;color: #ef5369;&quot;&gt; 변경된 레코드 값이 조회되는 것을 볼 수 있다&lt;/span&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1687334735887&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 세션 1
ROLLBACK;&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-06-21 오후 5.06.23.png&quot; data-origin-width=&quot;418&quot; data-origin-height=&quot;108&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bi1nTE/btskLOz0gbq/ijW4d18MJkLLk7UQDSNG70/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bi1nTE/btskLOz0gbq/ijW4d18MJkLLk7UQDSNG70/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bi1nTE/btskLOz0gbq/ijW4d18MJkLLk7UQDSNG70/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbi1nTE%2FbtskLOz0gbq%2FijW4d18MJkLLk7UQDSNG70%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;418&quot; height=&quot;108&quot; data-filename=&quot;스크린샷 2023-06-21 오후 5.06.23.png&quot; data-origin-width=&quot;418&quot; data-origin-height=&quot;108&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 상태에서 세션 1에 대해 롤백을 한다면 hello라는 값을 받게 된다...!&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;style3&quot; /&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;  READ COMMITTED&lt;/b&gt;&lt;/h4&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;1280&quot; data-origin-height=&quot;1253&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dxLlhl/btskKDeeMS5/Xv6aRh939zEMmPY5K4MRI0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dxLlhl/btskKDeeMS5/Xv6aRh939zEMmPY5K4MRI0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dxLlhl/btskKDeeMS5/Xv6aRh939zEMmPY5K4MRI0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdxLlhl%2FbtskKDeeMS5%2FXv6aRh939zEMmPY5K4MRI0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;504&quot; height=&quot;493&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;1253&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;pre id=&quot;code_1687332631862&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 트랜잭션 격리 레벨을 Read committed로 변경하고 살펴보자.&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;style1&quot; /&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;&lt;b&gt;  해결된 Dirty Read 현상 확인하기&lt;/b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1687332863057&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;START TRANSACTION;
INSERT INTO CREW (id, name) VALUES (1, 'hello');
INSERT INTO CREW (id, name) VALUES (2, 'journey');
COMMIT;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;436&quot; data-origin-height=&quot;174&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b3POAR/btskKGhMtS1/596rDrRTFlqEJSqVcd7ij1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b3POAR/btskKGhMtS1/596rDrRTFlqEJSqVcd7ij1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b3POAR/btskKGhMtS1/596rDrRTFlqEJSqVcd7ij1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb3POAR%2FbtskKGhMtS1%2F596rDrRTFlqEJSqVcd7ij1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;393&quot; height=&quot;157&quot; data-origin-width=&quot;436&quot; data-origin-height=&quot;174&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저, 그림과 같은 상황을 만들기 위해 2개의 레코드를 삽입해두자. (이전의 실험 결과는 TRUNCATE로 제거하였다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1687332944537&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 세션 1
START TRANSACTION;
UPDATE CREW SET name = 'hi' WHERE id = 1;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후, 세션 1에서 1번 레코드 'hello'에 대해 hi로 업데이트하는 쿼리를 작성한다. 단, &lt;span style=&quot;color: #ef5369;&quot;&gt;커밋하지 않는다&lt;/span&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1687334172269&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 세션 2
SELECT * FROM CREW;&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-06-21 오후 4.56.17.png&quot; data-origin-width=&quot;432&quot; data-origin-height=&quot;140&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/brF9cz/btskKGIVWqJ/NI7ZFRZ8VHgPgy24D2qGv0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/brF9cz/btskKGIVWqJ/NI7ZFRZ8VHgPgy24D2qGv0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/brF9cz/btskKGIVWqJ/NI7ZFRZ8VHgPgy24D2qGv0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbrF9cz%2FbtskKGIVWqJ%2FNI7ZFRZ8VHgPgy24D2qGv0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;432&quot; height=&quot;140&quot; data-filename=&quot;스크린샷 2023-06-21 오후 4.56.17.png&quot; data-origin-width=&quot;432&quot; data-origin-height=&quot;140&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 세션 2에서 조회해보면 언두 영역에 있는 데이터를 읽어오기 때문에 &lt;span style=&quot;color: #ef5369;&quot;&gt;hi가 아닌 hello로 보이는 것을 볼 수 있다&lt;/span&gt;.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;⭐️ 이전의 read uncommitted 예제와 달라진 것을 확인할 수 있다. (dirty read 해결)&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;이후, 세션 1에서 커밋을 하고 나면 아래와 같이 변경 사항이 완전히 반영된다.&lt;/p&gt;
&lt;pre id=&quot;code_1687334267563&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 세션 1
COMMIT;

# 세션 2
SELECT * FROM CREW;&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-06-21 오후 4.57.52.png&quot; data-origin-width=&quot;430&quot; data-origin-height=&quot;156&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AcsvV/btskRcsUVCf/ttxAivQk6ymU3znE5VxwV1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AcsvV/btskRcsUVCf/ttxAivQk6ymU3znE5VxwV1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AcsvV/btskRcsUVCf/ttxAivQk6ymU3znE5VxwV1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAcsvV%2FbtskRcsUVCf%2FttxAivQk6ymU3znE5VxwV1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;430&quot; height=&quot;156&quot; data-filename=&quot;스크린샷 2023-06-21 오후 4.57.52.png&quot; data-origin-width=&quot;430&quot; data-origin-height=&quot;156&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;style1&quot; /&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;&lt;b&gt;   NON-REPEATABLE READ 확인하기&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;위와 같은 상황에서 세션 2의 조회 쿼리 2번이 &lt;span style=&quot;color: #ef5369;&quot;&gt;하나의 트랜잭션에서 수행&lt;/span&gt;된다면 어떨까?&lt;/p&gt;
&lt;pre id=&quot;code_1687337792160&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 세션 1
START TRANSACTION;
UPDATE CREW SET name = 'hi' WHERE id = 1;

# 세션 2
START TRANSACTION;
SELECT * FROM CREW; # 1번 결과

# 세션 1
COMMIT;

# 세션 2
SELECT * FROM CREW; # 2번 결과&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-06-21 오후 5.09.44.png&quot; data-origin-width=&quot;1606&quot; data-origin-height=&quot;540&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/biYtLS/btskQi8sLV2/O5CKFSnVQKkxjj5zBckLqK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/biYtLS/btskQi8sLV2/O5CKFSnVQKkxjj5zBckLqK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/biYtLS/btskQi8sLV2/O5CKFSnVQKkxjj5zBckLqK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbiYtLS%2FbtskQi8sLV2%2FO5CKFSnVQKkxjj5zBckLqK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1606&quot; height=&quot;540&quot; data-filename=&quot;스크린샷 2023-06-21 오후 5.09.44.png&quot; data-origin-width=&quot;1606&quot; data-origin-height=&quot;540&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재는 쿼리를 직접 날리고 있기 때문에 알아보기 어렵지만, 만약 스프링에서 개발을 진행하는 상황이라고 해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 트랜잭션으로 묶여 있고, &lt;span style=&quot;color: #ef5369;&quot;&gt;동일한 쿼리를 날렸음에도 위와 같이 다른 결과를 받아올 수 있는 문제&lt;/span&gt;가 발생할 수 있어 주의해야 한다. (&lt;b&gt;non-repeatable read 발생&lt;/b&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;style3&quot; /&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;  REPEATABLE READ&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;653&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9AZ9Y/btskOS2WwNd/k8jan7Dofn735n5cxWX6DK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9AZ9Y/btskOS2WwNd/k8jan7Dofn735n5cxWX6DK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9AZ9Y/btskOS2WwNd/k8jan7Dofn735n5cxWX6DK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9AZ9Y%2FbtskOS2WwNd%2Fk8jan7Dofn735n5cxWX6DK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;653&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;653&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;pre id=&quot;code_1687336300476&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마찬가지로 이번에는 격리 수준을 repeatable read로 설정해준다.&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;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;  해결된 NON-REPEATABLE READ 확인하기&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1687336334043&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;START TRANSACTION;
INSERT INTO CREW (id, name) VALUES (1, 'hello');
INSERT INTO CREW (id, name) VALUES (2, 'journey');
COMMIT;&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;pre id=&quot;code_1687336398426&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 세션 2
SELECT * FROM CREW;&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-06-21 오후 5.33.04.png&quot; data-origin-width=&quot;454&quot; data-origin-height=&quot;160&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cuTJKA/btskNe6mlve/FkykTxzzpXFHIushA3W5ik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cuTJKA/btskNe6mlve/FkykTxzzpXFHIushA3W5ik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cuTJKA/btskNe6mlve/FkykTxzzpXFHIushA3W5ik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcuTJKA%2FbtskNe6mlve%2FFkykTxzzpXFHIushA3W5ik%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;454&quot; height=&quot;160&quot; data-filename=&quot;스크린샷 2023-06-21 오후 5.33.04.png&quot; data-origin-width=&quot;454&quot; data-origin-height=&quot;160&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;pre id=&quot;code_1687336457427&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 세션 1
START TRANSACTION;
UPDATE CREW SET name = 'hi' where id = 1;

# 세션 2
SELECT * FROM CREW;&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-06-21 오후 5.35.33.png&quot; data-origin-width=&quot;456&quot; data-origin-height=&quot;154&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bSBdGf/btskRcs1YI0/jhSnVjQiZ3RcLNcpYJi6Qk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bSBdGf/btskRcs1YI0/jhSnVjQiZ3RcLNcpYJi6Qk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bSBdGf/btskRcs1YI0/jhSnVjQiZ3RcLNcpYJi6Qk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbSBdGf%2FbtskRcs1YI0%2FjhSnVjQiZ3RcLNcpYJi6Qk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;456&quot; height=&quot;154&quot; data-filename=&quot;스크린샷 2023-06-21 오후 5.35.33.png&quot; data-origin-width=&quot;456&quot; data-origin-height=&quot;154&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 상태에서 세션 1이 1번 레코드에 대해 업데이트 쿼리를 날리고, 세션 2에서 조회하면 그대로 'hello'라는 레코드가 뜬다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1687336595490&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 세션 1
COMMIT;

# 세션 2
SELECT * FROM CREW;&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-06-21 오후 5.36.43.png&quot; data-origin-width=&quot;448&quot; data-origin-height=&quot;168&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RDxoV/btskSSUIFNg/E6eiDTKdQiDliWoYjW0Vm0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RDxoV/btskSSUIFNg/E6eiDTKdQiDliWoYjW0Vm0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RDxoV/btskSSUIFNg/E6eiDTKdQiDliWoYjW0Vm0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRDxoV%2FbtskSSUIFNg%2FE6eiDTKdQiDliWoYjW0Vm0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;448&quot; height=&quot;168&quot; data-filename=&quot;스크린샷 2023-06-21 오후 5.36.43.png&quot; data-origin-width=&quot;448&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;하지만, read committed와 다르게 세션 1에서 커밋을 했더라도 세션 2의 결과는 &lt;span style=&quot;color: #ef5369;&quot;&gt;hi가 아닌 hello가 그대로 나오는 것&lt;/span&gt;을 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;⭐️ 즉, 이전에 발생하였던 non-repeatable read를 해결한 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1687336687278&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 세션 2
COMMIT;

# 다시 조회
SELECT * FROM CREW;&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-06-21 오후 5.37.48.png&quot; data-origin-width=&quot;434&quot; data-origin-height=&quot;166&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QxLyl/btskS4AMOHt/NJmCf2gW7BKXg4K9NKixf0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QxLyl/btskS4AMOHt/NJmCf2gW7BKXg4K9NKixf0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QxLyl/btskS4AMOHt/NJmCf2gW7BKXg4K9NKixf0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQxLyl%2FbtskS4AMOHt%2FNJmCf2gW7BKXg4K9NKixf0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;434&quot; height=&quot;166&quot; data-filename=&quot;스크린샷 2023-06-21 오후 5.37.48.png&quot; data-origin-width=&quot;434&quot; data-origin-height=&quot;166&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후, 세션 2가 시작하였던 &lt;span style=&quot;color: #ef5369;&quot;&gt;트랜잭션을 커밋하고 다시 조회 쿼리를 날려보면 위와 같이 변경된 사항을 읽어오는 것&lt;/span&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;style1&quot; /&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;  PHANTOM READ 테스트하기&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;1280&quot; data-origin-height=&quot;1121&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dSAbBx/btskSo7nytg/o0RSRAVkaOqWCZ2SEXArs0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dSAbBx/btskSo7nytg/o0RSRAVkaOqWCZ2SEXArs0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dSAbBx/btskSo7nytg/o0RSRAVkaOqWCZ2SEXArs0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdSAbBx%2FbtskSo7nytg%2Fo0RSRAVkaOqWCZ2SEXArs0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;507&quot; height=&quot;444&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;1121&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1687337143521&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CREATE TABLE `ISAM_CREW` (
  `id` bigint NOT NULL,
  `name` varchar(255) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=MyISAM;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선, innoDB에서는 Phantom Read가 발생하지 않도록 하기 때문에 새로운 테이블을 만들자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1687337318936&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;START TRANSACTION;
INSERT INTO ISAM_CREW (id, name) VALUES (1, 'hello');
INSERT INTO ISAM_CREW (id, name) VALUES (2, 'journey');
COMMIT;&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;pre id=&quot;code_1687337297919&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 세션 2
START TRANSACTION;
SELECT * FROM ISAM_CREW WHERE id &amp;gt;= 2 FOR UPDATE;&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-06-21 오후 5.49.08.png&quot; data-origin-width=&quot;446&quot; data-origin-height=&quot;110&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DFNjw/btskSqYqWoS/YbKyV1kOnkgxyL6jSUIQCK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DFNjw/btskSqYqWoS/YbKyV1kOnkgxyL6jSUIQCK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DFNjw/btskSqYqWoS/YbKyV1kOnkgxyL6jSUIQCK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDFNjw%2FbtskSqYqWoS%2FYbKyV1kOnkgxyL6jSUIQCK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;446&quot; height=&quot;110&quot; data-filename=&quot;스크린샷 2023-06-21 오후 5.49.08.png&quot; data-origin-width=&quot;446&quot; data-origin-height=&quot;110&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세션 2에서 SELECT FOR UPDATE 구문을 활용하여 레코드를 조회해보자. 현재 'journey' 레코드만 나오는 것을 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1687337512290&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 세션 1
START TRANSACTION;
INSERT INTO ISAM_CREW(id, name) VALUES (3, 'cat');
COMMIT;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후, 세션 1에서 새로운 레코드를 삽입하고 커밋을 해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1687337537631&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 세션 2
SELECT * FROM ISAM_CREW WHERE id &amp;gt;= 2 FOR UPDATE;&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-06-21 오후 5.52.45.png&quot; data-origin-width=&quot;444&quot; data-origin-height=&quot;152&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lvt81/btskSntWaG5/oQkzfK1ej1drXq8WjR1PMk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lvt81/btskSntWaG5/oQkzfK1ej1drXq8WjR1PMk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lvt81/btskSntWaG5/oQkzfK1ej1drXq8WjR1PMk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Flvt81%2FbtskSntWaG5%2FoQkzfK1ej1drXq8WjR1PMk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;444&quot; height=&quot;152&quot; data-filename=&quot;스크린샷 2023-06-21 오후 5.52.45.png&quot; data-origin-width=&quot;444&quot; data-origin-height=&quot;152&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 journey에 이어 cat이라는 3번 레코드가 추가된 것을 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 트랜잭션 내인데도 다른 결과가 나오는 '&lt;span style=&quot;color: #ef5369;&quot;&gt;phantom read&lt;/span&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;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;  PHANTOM READ - innoDB에서는 정말 발생하지 않을까?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 사용했던 innoDB 스토리지 엔진으로 생성된 'CREW' 테이블을 다시 한 번 가져오자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 진행했던 실험과 동일하게 샘플 레코드 삽입 후 SELECT FOR UPDATE를 진행해주자.&lt;/p&gt;
&lt;pre id=&quot;code_1687338813150&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 세션 2
START TRANSACTION;
SELECT * FROM CREW WHERE id &amp;gt;= 2 FOR UPDATE;

# 세션 1
START TRANSACTION;
INSERT INTO CREW(id, name) VALUES (3, 'cat'); # 삽입 대기&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-06-21 오후 6.33.59.png&quot; data-origin-width=&quot;1428&quot; data-origin-height=&quot;784&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cPXAFy/btskNfqSYZq/yywqHRlmVZwI76xOhjCJPk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cPXAFy/btskNfqSYZq/yywqHRlmVZwI76xOhjCJPk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cPXAFy/btskNfqSYZq/yywqHRlmVZwI76xOhjCJPk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcPXAFy%2FbtskNfqSYZq%2FyywqHRlmVZwI76xOhjCJPk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;645&quot; height=&quot;354&quot; data-filename=&quot;스크린샷 2023-06-21 오후 6.33.59.png&quot; data-origin-width=&quot;1428&quot; data-origin-height=&quot;784&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;innoDB에서는 세션 2의 트랜잭션이 커밋되기 전까지 ⭐️ &lt;span style=&quot;color: #ef5369;&quot;&gt;넥스트 키&lt;/span&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;락이 걸리기 때문에 삽입이 불가능&lt;/span&gt;하며, 타임아웃이 발생하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1687339509385&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 세션 2
COMMIT;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;타임아웃이 발생하기 전에&lt;/b&gt; 세션 2에서 &lt;b&gt;커밋을 진행하게 되면 &lt;/b&gt;insert가 성공적으로 진행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1687340323846&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 세션 1 - 이전에 작성하였던 insert query
START TRANSACTION;
INSERT INTO CREW(id, name) VALUES (3, 'cat');

# 이 트랜잭션에서 조회 진행
SELECT * FROM CREW;

# 세션 3 - 세션 1의 커밋 전에 조회 진행
SELECT * FROM CREW;

# 세션 1 커밋
COMMIT;

# 세션 3 재조회
SELECT * FROM CREW;&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-06-21 오후 6.45.22.png&quot; data-origin-width=&quot;1798&quot; data-origin-height=&quot;1174&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7No7E/btskRo7Y03d/VDMNykGBHsDb1PqKfUFsM1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7No7E/btskRo7Y03d/VDMNykGBHsDb1PqKfUFsM1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7No7E/btskRo7Y03d/VDMNykGBHsDb1PqKfUFsM1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7No7E%2FbtskRo7Y03d%2FVDMNykGBHsDb1PqKfUFsM1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;624&quot; height=&quot;407&quot; data-filename=&quot;스크린샷 2023-06-21 오후 6.45.22.png&quot; data-origin-width=&quot;1798&quot; data-origin-height=&quot;1174&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서&lt;b&gt; insert을 진행했던 트랜잭션에서&lt;/b&gt; select를 작성하면 삽입이 된 모습을 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 commit 이전에는 다른 트랜잭션에서 값을 확인할 수 없기 때문에 commit 후에 확인이 가능하다.&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;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;  PHANTOM READ -  SELECT FOR UPDATE&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;1280&quot; data-origin-height=&quot;863&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bSZ310/btskQhWsdQg/tHr3PrBc9qUv4fwd7NJKl1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bSZ310/btskQhWsdQg/tHr3PrBc9qUv4fwd7NJKl1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bSZ310/btskQhWsdQg/tHr3PrBc9qUv4fwd7NJKl1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbSZ310%2FbtskQhWsdQg%2FtHr3PrBc9qUv4fwd7NJKl1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;666&quot; height=&quot;449&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;863&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, innoDB를 사용하더라도 select for update를 사용하게 되면 데이터의 부정합이 발생할 수 있다.&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;pre id=&quot;code_1687355828639&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 세션 2
START TRANSACTION;
SELECT * FROM CREW WHERE id &amp;gt;= 1;

# 세션 1
START TRANSACTION;
UPDATE CREW SET name = 'hi' where id = 1;
COMMIT

# 세션 2 (기존의 트랜잭션에서 진행)
SELECT * FROM CREW WHERE id &amp;gt;= 1 FOR UPDATE;&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-06-21 오후 11.03.38.png&quot; data-origin-width=&quot;2052&quot; data-origin-height=&quot;458&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wmi4d/btskSp6EoPU/lNoVrS1m526f0WGFNlhAK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wmi4d/btskSp6EoPU/lNoVrS1m526f0WGFNlhAK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wmi4d/btskSp6EoPU/lNoVrS1m526f0WGFNlhAK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fwmi4d%2FbtskSp6EoPU%2FlNoVrS1m526f0WGFNlhAK1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2052&quot; height=&quot;458&quot; data-filename=&quot;스크린샷 2023-06-21 오후 11.03.38.png&quot; data-origin-width=&quot;2052&quot; data-origin-height=&quot;458&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 위와 같이 처음에는 hello, journey가 조회되다가 insert가 진행되자 &lt;span style=&quot;color: #ef5369;&quot;&gt;undo 영역의 데이터가 아닌 원본 테이블의 내용을 읽어 hi, journey가 조회되는 것을 확인할 수 있다&lt;/span&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;style3&quot; /&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  SERIALIZABLE&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1687341856781&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 마지막으로 serializable에 대해서 알아보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1687341923577&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 세션 2
START TRANSACTION;
select * from CREW;&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-06-21 오후 7.05.29.png&quot; data-origin-width=&quot;434&quot; data-origin-height=&quot;94&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ovQ92/btskRcteawa/k3K3hmVPNy5GNy8Kks9W11/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ovQ92/btskRcteawa/k3K3hmVPNy5GNy8Kks9W11/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ovQ92/btskRcteawa/k3K3hmVPNy5GNy8Kks9W11/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FovQ92%2FbtskRcteawa%2Fk3K3hmVPNy5GNy8Kks9W11%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;434&quot; height=&quot;94&quot; data-filename=&quot;스크린샷 2023-06-21 오후 7.05.29.png&quot; data-origin-width=&quot;434&quot; data-origin-height=&quot;94&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;pre id=&quot;code_1687341972105&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 세션 1
START TRANSACTION;
INSERT INTO CREW (id, name) VALUES (1, 'hello');&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-06-21 오후 7.06.18.png&quot; data-origin-width=&quot;870&quot; data-origin-height=&quot;52&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cInIKV/btskOSB7cFZ/jW2ojt0v4ytFmTSk8epzAK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cInIKV/btskOSB7cFZ/jW2ojt0v4ytFmTSk8epzAK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cInIKV/btskOSB7cFZ/jW2ojt0v4ytFmTSk8epzAK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcInIKV%2FbtskOSB7cFZ%2FjW2ojt0v4ytFmTSk8epzAK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;870&quot; height=&quot;52&quot; data-filename=&quot;스크린샷 2023-06-21 오후 7.06.18.png&quot; data-origin-width=&quot;870&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;이후, 세션 1이 삽입 쿼리를 날리려고 하면 오른쪽과 같이 계속 대기 상태에 들어가는 것을 볼 수 있다. (쿼리 실행 16s)&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;스크린샷 2023-06-21 오후 7.06.56.png&quot; data-origin-width=&quot;898&quot; data-origin-height=&quot;72&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kQMd5/btskNeyLB0c/aumph35IiCB6da3OCcSLNk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kQMd5/btskNeyLB0c/aumph35IiCB6da3OCcSLNk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kQMd5/btskNeyLB0c/aumph35IiCB6da3OCcSLNk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkQMd5%2FbtskNeyLB0c%2Faumph35IiCB6da3OCcSLNk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;898&quot; height=&quot;72&quot; data-filename=&quot;스크린샷 2023-06-21 오후 7.06.56.png&quot; data-origin-width=&quot;898&quot; data-origin-height=&quot;72&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;p data-ke-size=&quot;size16&quot;&gt;즉, 세션 2는 단순히 조회 쿼리만 날렸는데도&lt;span style=&quot;color: #ef5369;&quot;&gt; 세션 1이 삽입하지 못하기 때문에 읽기에 대해서도 락이 걸린다는 것을 확인&lt;/span&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;style3&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 트랜잭션 격리 수준에 대해서 전부 테스트를 완료하였다.&lt;/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;</description>
      <category>✏️/CS</category>
      <category>dirty read</category>
      <category>Isolation Level</category>
      <category>Non Repeatable Read</category>
      <category>phantom read</category>
      <category>read committed</category>
      <category>READ UNCOMMITTED</category>
      <category>repeatable read</category>
      <category>transaction</category>
      <category>격리 수준</category>
      <category>트랜잭션</category>
      <author>dolmeng2</author>
      <guid isPermaLink="true">https://cl8d.tistory.com/110</guid>
      <comments>https://cl8d.tistory.com/110#entry110comment</comments>
      <pubDate>Wed, 21 Jun 2023 17:53:35 +0900</pubDate>
    </item>
    <item>
      <title>[Real MySQL 8.0] InnoDB의 인덱스와 락, 트랜잭션 격리 수준</title>
      <link>https://cl8d.tistory.com/109</link>
      <description>&lt;h4 style=&quot;text-align: left;&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; style=&quot;text-align: left;&quot;&gt;이번 포스팅에서는 InnoDB의 인덱스와 락의 상관관계와 트랜잭션 격리 수준에 대해서 알아보자.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot;&gt;&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;&lt;h4 style=&quot;text-align: left;&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; style=&quot;text-align: left;&quot;&gt;지난 포스팅에서 레코드 락에 대해 정리할 때 다음과 같은 멘트를 남겼다.&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;innoDB의 경우 레코드 자체를 잠그는 것보다는, 인덱스의 레코드를 잠근다.&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;인덱스를 잠그기 때문에 레코드 검색 시 &lt;span style=&quot;color: #EF5369;&quot;&gt;발견된 모든 레코드의 락을 걸게 된다&lt;/span&gt;.&lt;br&gt;&amp;nbsp;&lt;br&gt;예를 들어, crew 테이블에 age = 23인 사람이 100명이라고 생각해보자.&lt;br&gt;이때, &lt;span style=&quot;color: #EF5369;&quot;&gt;age에는 인덱스가, name에는 인덱스가 걸려있지 않은&lt;/span&gt; 상태다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;SELECT COUNT(*) FROM crew WHERE age = 23;
# result: 100&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;br&gt;하지만, age = 23이면서 name = 'journey'인 결과는 1개만 나온다고 가정해보자.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;SELECT COUNT(*) FROM crew WHERE age = 23 and name = 'journey';
# result: 1&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;br&gt;이때, age = 23이고 name = 'journey'인 레코드에 대해서 last_modified_at 레코드를 NOW()로 업데이트한다고 생각해보자.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;UPDATE crew SET last_modified_at = NOW() WHERE age = 23 and name = 'journey';&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;br&gt;위의 쿼리문을 실행하기 위해서 몇 개의 레코드에 락을 걸게 될까?&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1938&quot; data-origin-height=&quot;840&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cD0qRM/btskiAabxw7/eJtWsPaOUgnkeCKxonnh50/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cD0qRM/btskiAabxw7/eJtWsPaOUgnkeCKxonnh50/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cD0qRM/btskiAabxw7/eJtWsPaOUgnkeCKxonnh50/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcD0qRM%2FbtskiAabxw7%2FeJtWsPaOUgnkeCKxonnh50%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;341&quot; data-origin-width=&quot;1938&quot; data-origin-height=&quot;840&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;바로, age = 23의 결과로 나온 &lt;b&gt;100개의 레코드에 대해서 모두 락이 걸리게 된다&lt;/b&gt;.&lt;br&gt;&lt;span style=&quot;color: #EF5369;&quot;&gt;인덱스가 만약 하나도 없다면 테이블에 대해 전체 스캔 (full-scan)을 하면서 update를 진행&lt;/span&gt;하기 때문에 모든 레코드에 대해서 락을 걸게 될 것이고, 성능에 치명적인 문제가 발생할 것이다. 그렇기 때문에 인덱스를 잘 거는 것이 매우 중요하다!&lt;br&gt;&amp;nbsp;&lt;br&gt;MySQL 8.0부터는 락에 대한 정보를 performance_schema DB를 통해서 확인할 수 있다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;SELECT
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;t1.trx_id waiting_trx_id,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;t1.trx_mysql_thread_id waiting_thread,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;t1.trx_query waiting_query,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;t2.trx_id blocking_trx_id,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;t2.trx_mysql_thread_id blocking_thread,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;t2.trx_query blocking_query
FROM performance_schema.data_lock_waits w
INNER JOIN information_schema.INNODB_TRX t1 ON t1.trx_id = w.REQUESTING_ENGINE_TRANSACTION_ID
INNER JOIN information_schema.INNODB_TRX t2 ON t2.trx_id = w.BLOCKING_ENGINE_TRANSACTION_ID&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;waiting_trx_id의 경우 대기하고 있는 트랜잭션의 아이디를, waiting_thread는 대기하고 있는 스레드의 번호를, waiting_query는 대기하고 있는 쿼리 정보를, blocking_trx_id는 블락킹 상태에 있는 트랜잭션의 아이디를, blocking_thread는 블락킹 상태에 있는 스레드를, blocking_query는 블락킹 상태에 있는 쿼리 정보를 의미한다.&lt;br&gt;&amp;nbsp;&lt;br&gt;여기서 대기 상태는 트랜잭션이 &lt;span style=&quot;color: #EF5369;&quot;&gt;다른 트랜잭션의 작업이 완료되기를 기다리는&lt;/span&gt; 상태를, 블락킹 상태는 &lt;span style=&quot;color: #EF5369;&quot;&gt;다른 트랜잭션이 이미 사용 중인 데이터나 락을 요청한 상태&lt;/span&gt;를 의미한다.&lt;br&gt;= 즉, waiting_trx_id가 blocking_trx_id를 기다리고 있는 상태를 한눈에 확인할 수 있게 된다.&lt;br&gt;락을 풀기 위해서는 blocking_trx_id가 가지고 있는 락을 해제하고, 그 뒤로 waiting_trx_id의 작업이 이루어져야 한다.&lt;br&gt;&amp;nbsp;&lt;br&gt;만약 더 상세한 정보를 확인하고 싶다면 data_locks의 테이블을 모두 확인해보자.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;SELECT * FROM performance_schema.data_locks;&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;여기서 engine_transaction_id와 thread_id, 그리고 &lt;b&gt;lock_type과 lock_mode&lt;/b&gt;을 확인해보면 어떤 트랜잭션이 어떤 스레드가 사용하고 있고, 어떤 락을 가지고 있는지까지 확인이 가능하다. 만약 결과에서 REC_NOT_GAP 이라는 lock_mode가 있다면 이는 레코드 락에서 갭이 포함되지 않은 순수한 락을 가지고 있음을 의미한다. (lock_mode의 결과로는 S[,GAP], X[,GAP], IS[,GAP], IX[,GAP], AUTO_INC 및 UNKNOWN가 나올 수 있다.)&lt;br&gt;&amp;nbsp;&lt;br&gt;만약, 특정 스레드가 락을 너무 오래 보유하고 있다면 아래와 같이 KILL을 해주자.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;KILL thread_id&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot;&gt;&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;&lt;h4 style=&quot;text-align: left;&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; style=&quot;text-align: left;&quot;&gt;트랜잭션 격리 수준은 여러 트랜잭션이 동시에 처리될 때 특정 트랜잭션이 다른 트랜잭션에서 변경하거나 조회하는 데이터를 볼 수 있도록 할지 말지 결정하는 것이다. 크게 4가지의 격리 수준이 존재하며, &lt;span style=&quot;color: #EF5369;&quot;&gt;READ UNCOMMITTED / READ COMMITTED / REPEATABLE READ / SERIALIZABLE&lt;/span&gt;로 나뉜다. Read Uncommitted와 Serializable의 경우 보통 잘 사용되지 않는다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;&lt;tbody&gt;&lt;tr style=&quot;height: 17px;&quot;&gt;&lt;td style=&quot;width: 20.0%; height: 17px; text-align: center;&quot;&gt;&amp;nbsp;&lt;/td&gt;&lt;td style=&quot;width: 20.0%; height: 17px; text-align: center;&quot;&gt;&lt;b&gt;DIRTY-READ&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;width: 20.0%; height: 17px; text-align: center;&quot;&gt;&lt;b&gt;NON-REPEATABLE READ&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;width: 20.0%; height: 17px; text-align: center;&quot;&gt;&lt;b&gt;PHANTOM READ&lt;/b&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 17px;&quot;&gt;&lt;td style=&quot;width: 20.0%; height: 17px; text-align: center;&quot;&gt;&lt;b&gt;READ UNCOMMITTED&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;width: 20.0%; height: 17px; text-align: center;&quot;&gt;O&lt;/td&gt;&lt;td style=&quot;width: 20.0%; height: 17px; text-align: center;&quot;&gt;O&lt;/td&gt;&lt;td style=&quot;width: 20.0%; height: 17px; text-align: center;&quot;&gt;O&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 17px;&quot;&gt;&lt;td style=&quot;width: 20.0%; height: 17px; text-align: center;&quot;&gt;&lt;b&gt;READ COMMITTED&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;width: 20.0%; height: 17px; text-align: center;&quot;&gt;X&lt;/td&gt;&lt;td style=&quot;width: 20.0%; height: 17px; text-align: center;&quot;&gt;O&lt;/td&gt;&lt;td style=&quot;width: 20.0%; height: 17px; text-align: center;&quot;&gt;O&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 17px;&quot;&gt;&lt;td style=&quot;width: 20.0%; height: 17px; text-align: center;&quot;&gt;&lt;b&gt;REPEATABLE READ&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;width: 20.0%; height: 17px; text-align: center;&quot;&gt;X&lt;/td&gt;&lt;td style=&quot;width: 20.0%; height: 17px; text-align: center;&quot;&gt;X&lt;/td&gt;&lt;td style=&quot;width: 20.0%; height: 17px; text-align: center;&quot;&gt;O&lt;br&gt;(innoDB에서는 발생 X)&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 17px;&quot;&gt;&lt;td style=&quot;width: 20.0%; height: 17px; text-align: center;&quot;&gt;&lt;b&gt;SERIALIZABLE&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;width: 20.0%; height: 17px; text-align: center;&quot;&gt;X&lt;/td&gt;&lt;td style=&quot;width: 20.0%; height: 17px; text-align: center;&quot;&gt;X&lt;/td&gt;&lt;td style=&quot;width: 20.0%; height: 17px; text-align: center;&quot;&gt;X&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;오라클에서는 Read Committed를 사용하며, MySQL에서는 Repeatable Read를 주로 사용한다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size18&quot; style=&quot;text-align: left;&quot;&gt;&lt;b&gt;  READ UNCOMMITTED&lt;/b&gt;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;기본적으로&lt;span style=&quot;color: #EF5369;&quot;&gt; 커밋이나 롤백 여부에 상관없이 다른 트랜잭션에서 확인&lt;/span&gt;할 수 있는 격리 수준이다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1406&quot; data-origin-height=&quot;1410&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yjrpR/btskuNfeilD/iCLxtDKDYfKT4SXwM52t6k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yjrpR/btskuNfeilD/iCLxtDKDYfKT4SXwM52t6k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yjrpR/btskuNfeilD/iCLxtDKDYfKT4SXwM52t6k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyjrpR%2FbtskuNfeilD%2FiCLxtDKDYfKT4SXwM52t6k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;581&quot; height=&quot;583&quot; data-origin-width=&quot;1406&quot; data-origin-height=&quot;1410&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;위 그림을 보면, 아직 'journey'라는 레코드가 커밋되지 않았는데도 사용자 B가 해당 내용을 read 할 수 있는 것을 볼 수 있다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;작업이 완료되지 않았는데도 다른 트랜잭션에서 볼 수 있는 현상&lt;/b&gt;을 '&lt;span style=&quot;color: #EF5369;&quot;&gt;Dirty Read&lt;/span&gt;'라고 하며, 이런 경우엔 데이터가 나타났다가 사라졌다가 하는 현상을 초래하기 때문에 데이터의 정합성에 문제가 발생한다. 웬만하면 read uncommitted의 경우 사용하지 않는 것이 좋다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size18&quot; style=&quot;text-align: left;&quot;&gt;&lt;b&gt;  READ COMMITTED&lt;/b&gt;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;오라클에서 기본적으로 사용하는 격리 수준이며, Dirty Read가 해결된 격리 수준이다.&lt;br&gt;다른 트랜잭션에 의해 데이터가 변경되더라도 &lt;span style=&quot;color: #EF5369;&quot;&gt;Commit이 완료된 데이터만 조회가 가능&lt;/span&gt;하다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1430&quot; data-origin-height=&quot;1400&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/o0pfn/btskiAnJ5S8/9pEKx0ohOKB8Sp9kYHXG3K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/o0pfn/btskiAnJ5S8/9pEKx0ohOKB8Sp9kYHXG3K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/o0pfn/btskiAnJ5S8/9pEKx0ohOKB8Sp9kYHXG3K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fo0pfn%2FbtskiAnJ5S8%2F9pEKx0ohOKB8Sp9kYHXG3K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;602&quot; height=&quot;589&quot; data-origin-width=&quot;1430&quot; data-origin-height=&quot;1400&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;위 그림을 보면, 기존의 1번 레코드는 name = hello였고, 이에 대해서 'hi'라고 업데이트를 한 것을 볼 수 있다.&lt;br&gt;이때 사용자 B가 1번 레코드에 대해서 조회할 때, &lt;span style=&quot;color: #EF5369;&quot;&gt;테이블이 아닌 업데이트 시 언두 로그에 저장했던 값을 읽어서 반환&lt;/span&gt;하기 때문에 업데이트 되기 전 데이터가 반환되며, 추후 커밋이 되어야 완전하게 반영이 완료된다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1754&quot; data-origin-height=&quot;1284&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/beSQIK/btskhDyv2iE/qAXUb00qqVlI9OLkxW0AI0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/beSQIK/btskhDyv2iE/qAXUb00qqVlI9OLkxW0AI0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/beSQIK/btskhDyv2iE/qAXUb00qqVlI9OLkxW0AI0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbeSQIK%2FbtskhDyv2iE%2FqAXUb00qqVlI9OLkxW0AI0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;666&quot; height=&quot;488&quot; data-origin-width=&quot;1754&quot; data-origin-height=&quot;1284&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;커밋된 데이터만 읽기 때문에 '작업이 완료되지 않은 데이터를 읽는다'는 Dirty Read는 해결되었지만,&lt;span style=&quot;color: #EF5369;&quot;&gt; 하나의 트랜잭션에서&lt;/span&gt; &lt;span style=&quot;color: #EF5369;&quot;&gt;SELECT 시 항상 같은 결과를 반환하는 것이 아닌&lt;/span&gt; UPDATE 이전의 데이터와 이후의 데이터가 다른 데이터가 반환되기 때문에 '&lt;span style=&quot;color: #EF5369;&quot;&gt;Non-Repeatable Read&lt;/span&gt;'가 발생한다.&lt;br&gt;&amp;nbsp;&lt;br&gt;단순한 웹 서비스에서 이는 크게 문제가 되지 않지만, 하나의 트랜잭션에서 &lt;b&gt;동일한 데이터를 여러 번 읽고 쓰는 작업이 금전 행위와 관련이 되면&lt;/b&gt; 문제가 발생할 수 있다. 특히, 입-출금 로직을 생각해보자. 처음에 10000원이 조회되어서 1000원을 추가하고 11000원이 나오길 기대했는데, 다시 조회하니 6000원으로 나온다면 매우 당혹스러울 것이다.&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;참고로, 트랜잭션 내부에서 실행되는 SELECT 문과 외부에서 실행되는 SELECT문의 차이는 없다.&lt;br&gt;하지만, Repeatable-Read에서는 SELECT까지 트랜잭션 범위 내에서만 작동하여 계속 동일한 결과만 보이도록 만들게 된다. 이에 대해서는 아래에서 확인해보자.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size18&quot; style=&quot;text-align: left;&quot;&gt;&lt;b&gt;   REPEATABLE READ&lt;/b&gt;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&lt;b&gt;InnoDB에서 기본으로 사용하는 격리 수준&lt;/b&gt;으로, 바이너리 로그를 가진 MySQL 서버에서는 최소 Repeatable read 이상의 격리 수준을 가져야 한다. innoDB에서는 MVCC를 활용하여 변경 전 레코드를 언두 공간에 백업해두고, 실제 레코드 값을 변경하는데 이 &lt;span style=&quot;color: #EF5369;&quot;&gt;MVCC를 활용하여 동일한 트랜잭션에서는 동일한 결과를 보여주도록 보장&lt;/span&gt;한다.&lt;br&gt;근데 앞서 봤던 Read Committed 역시 언두 로그에 있는 값을 보여줬었는데, repeatable read와 뭐가 다른 것일까?&lt;br&gt;이는, &lt;b&gt;언두 영역에 백업된 값 중 몇 번째 이전까지 찾아 들어가는지에 따라 달라진다&lt;/b&gt;.&lt;br&gt;&amp;nbsp;&lt;br&gt;언두 영역에는 기본적으로&lt;b&gt; 변경을 발생시킨 트랜잭션의 번호도 함께 저장&lt;/b&gt;해둔다. innoDB는 불필요하다고 판단하는 시점에 과거의 언두 로그를 제거하는데, 이때 &lt;span style=&quot;color: #EF5369;&quot;&gt;현재 실행 중인 트랜잭션 중 가장 오래된 트랜잭션 번호보다 앞선 언두 영역의 데이터는 제거가 불가능&lt;/span&gt;하다. 정확하게는, 특정 트랜잭션 구간 내에서 백업된 언두 데이터가 보존되어야 한다.&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2938&quot; data-origin-height=&quot;1500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b3MaPE/btsknPdEEAm/U5ise0qDDvoSmTxb61WHC0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b3MaPE/btsknPdEEAm/U5ise0qDDvoSmTxb61WHC0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b3MaPE/btsknPdEEAm/U5ise0qDDvoSmTxb61WHC0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb3MaPE%2FbtsknPdEEAm%2FU5ise0qDDvoSmTxb61WHC0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2938&quot; height=&quot;1500&quot; data-origin-width=&quot;2938&quot; data-origin-height=&quot;1500&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;그림이 너무 아래로 길어져서 양옆으로 쪼갰다.&lt;br&gt;먼저, 사용자 B가 트랜잭션을 시작하기 전에 id = 1, name = hello인 레코드와 id = 2, name = journey인 레코드가 존재했다.&lt;br&gt;이때 select를 통해 조회했을 때 name = hello를 조회하였다.&lt;br&gt;&amp;nbsp;&lt;br&gt;이후, 트랜잭션 아이디 12번이 update를 통해서 id = 1의 레코드의 name을 hi로 변경하였고, 변경 전 사항에 대해 언두 로그에 기록하였다. 이때, &lt;b&gt;언두 로그에는 변경 전 트랜잭션 아이디인 6번도 함께 기록되어 있다&lt;/b&gt;.&lt;br&gt;&amp;nbsp;&lt;br&gt;사용자 B가 다시 id = 1에 대해 조회를 했을 때 언두 로그에 대한 정보를 확인하는데, 이때 &lt;span style=&quot;color: #EF5369;&quot;&gt;사용자 B의 트랜잭션 아이디인 10보다 작은 언두 로그의 값을 확인하여 조회&lt;/span&gt;하게 된다. 덕분에 업데이트가 되든 안 되든 항상 같은 name = hello의 값을 읽어올 수 있게 된다.&lt;br&gt;위 경우는 예제를 단순화하기 위해서 언두 영역의 데이터가 하나지만, 당연히 여러 개의 데이터가 존재할 수 있다. 트랜잭션이 길어지면 길어질수록 언두 영역의 데이터 역시 계속 늘어날 수 있기 때문에 MySQL 서버 성능에도 영향을 끼친다는 것을 주의하자.&lt;br&gt;&amp;nbsp;&lt;br&gt;하지만, 이 역시 부정합이 발생할 수 있다.&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1764&quot; data-origin-height=&quot;1546&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bCxAw8/btskifqCOcG/JlGoIX5biCCfKIDzzUmkk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bCxAw8/btskifqCOcG/JlGoIX5biCCfKIDzzUmkk1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bCxAw8/btskifqCOcG/JlGoIX5biCCfKIDzzUmkk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbCxAw8%2FbtskifqCOcG%2FJlGoIX5biCCfKIDzzUmkk1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;693&quot; height=&quot;607&quot; data-origin-width=&quot;1764&quot; data-origin-height=&quot;1546&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;처음 SELECT에서는 journey만 받아오지만, 두 번째 SELECT에서 journey와 cat을 받아왔다.&lt;br&gt;이렇게 &lt;b&gt;다른 트랜잭션에 수행한 변경 작업에 의해 레코드가 보였다 안 보였다 하는 현상&lt;/b&gt;을 '&lt;span style=&quot;color: #EF5369;&quot;&gt;Phantom Read&lt;/span&gt;' 라고 한다.&lt;br&gt;&amp;nbsp;&lt;br&gt;기본적으로 select ... for update는 쓰기 락을 걸어야 하는데,&lt;b&gt; 언두 로그에는 락을 걸 수 없기 때문에&lt;/b&gt;&lt;span style=&quot;color: #EF5369;&quot;&gt; select ... for update나 select ... lock in share mode로 조회되는 레코드는&lt;/span&gt; 변경 전 데이터가 아닌 &lt;span style=&quot;color: #EF5369;&quot;&gt;현재 레코드의 값을 가져오게 된다&lt;/span&gt;.&lt;br&gt;- select for update는 특정 레코드에 대해서 X-Lock을 거는 구문이고 (업데이트를 위해서 베타 락을 거는 느낌), select lock in share mode는 S-Lock을 거는 구문이다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;  Non-Repeatable Read랑 똑같은 거 아닐까?&lt;/b&gt;&lt;br&gt;&amp;nbsp;여기서 '결과의 집합이 다르게 나왔다'는 점에 포커스를 맞춰야 한다. non-repeatable read의 경우 1개의 결과가 나오지만, 업데이트로 인해서 hello, hi라는 2개의 결과가 나왔고, 여기에서는 journey와 journey, cat이라는 2개의 결과가 나왔다.&amp;nbsp;&lt;br&gt;동일한 트랜잭션 내에서 동일한 쿼리를 실행하더라도 다른 결과를 얻는다는 게 non-repeatable read이고, 다른 결과 집합을 얻을 수 있다는 게 phantom-read이다. &lt;span style=&quot;color: #EF5369;&quot;&gt;non-repeatable read의 경우 수정된 데이터&lt;/span&gt;&lt;span style=&quot;color: #666666;&quot;&gt;에 대한 일관성 문제&lt;/span&gt;를,&lt;span style=&quot;color: #EF5369;&quot;&gt; phantom read의 경우 삽입이나 삭제된 데이터&lt;/span&gt;에 대한 일관성 문제를 가진다.&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size18&quot; style=&quot;text-align: left;&quot;&gt;&lt;b&gt;  InnoDB에서는 왜 Phantom Read가 발생하지 않을까?&lt;/b&gt;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&lt;a href=&quot;https://cl8d.tistory.com/108&quot; target=&quot;_blank&quot; title=&quot;지난 포스팅&quot;&gt;&lt;span&gt;지난 포스팅&lt;/span&gt;&lt;/a&gt;에서 봤듯이, innoDB에서 제공하는 락 중에서 '레코드 락'과 '갭 락'을 합친 '&lt;b&gt;넥스트 키 락&lt;/b&gt;'을 제공한다는 것을 알 수 있다.&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2112&quot; data-origin-height=&quot;1474&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/F6VrH/btskRQpFw54/z2Yc1MuGNbcr2rgxK9LCk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/F6VrH/btskRQpFw54/z2Yc1MuGNbcr2rgxK9LCk0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/F6VrH/btskRQpFw54/z2Yc1MuGNbcr2rgxK9LCk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FF6VrH%2FbtskRQpFw54%2Fz2Yc1MuGNbcr2rgxK9LCk0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;769&quot; height=&quot;537&quot; data-origin-width=&quot;2112&quot; data-origin-height=&quot;1474&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;위와 같이 사용자 B의 트랜잭션이 커밋되기 전에 select 시 락을 잡고 있어서 계속 같은 결과를 반환하는 것을 볼 수 있다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2042&quot; data-origin-height=&quot;1378&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zXwZP/btskQQqFH7q/xiMLeJ5KHkAoU7YVk8Gvx0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zXwZP/btskQQqFH7q/xiMLeJ5KHkAoU7YVk8Gvx0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zXwZP/btskQQqFH7q/xiMLeJ5KHkAoU7YVk8Gvx0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzXwZP%2FbtskQQqFH7q%2FxiMLeJ5KHkAoU7YVk8Gvx0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;790&quot; height=&quot;533&quot; data-origin-width=&quot;2042&quot; data-origin-height=&quot;1378&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;⭐️ 하지만, select 이후 select for update로 조회하게 된다면 언두 영역이 아닌 원본 테이블의 값을 받아오기 때문에 데이터의 부정합이 발생한다. 즉, innoDB에서&amp;nbsp;&lt;span style=&quot;color: #EF5369;&quot;&gt;phantom read가 무조건 발생하지 않는 것은 아니다&lt;/span&gt;! &lt;span style=&quot;color: #EF5369;&quot;&gt;select for update의 경우 발생할 수 있다&lt;/span&gt;.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size18&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;   SERIALIZABLE&lt;/b&gt;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;가장 단순하면서도 엄격한 격리 수준이다. 그렇기 때문에 다른 격리 수준보다 동시성 작업이 떨어진다.&lt;br&gt;기본적으로 innoDB 테이블에서 순수한 select 작업은 락 없이도 수행되지만, serializable에서는 &lt;span style=&quot;color: #EF5369;&quot;&gt;읽기에 대해서도 S-Lock을 걸어야 하며, 다른 트랜잭션에서는 해당 레코드를 변경할 수 없게 된다&lt;/span&gt;. 이 덕분에 Phantom read 현상이 발생하지는 않지만, innoDB에서는 Gap Lock과 Next Key Lock 덕분에 repeatable read에서도 phantom read가 발생하지 않아서 굳이 사용할 필요가 없다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot;&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;br&gt;코드 레벨로 위 격리 수준을 확인하고 싶다면 아래 포스팅을 확인해 주세요  &lt;br&gt;&lt;i&gt;&lt;a href=&quot;https://cl8d.tistory.com/110&quot; target=&quot;_blank&quot; title=&quot;[MySQL] 트랜잭션 격리 수준을 쿼리를 통해 직접 테스트해보기&quot;&gt;&lt;span&gt;[MySQL]&amp;nbsp;트랜잭션&amp;nbsp;격리&amp;nbsp;수준을&amp;nbsp;쿼리를&amp;nbsp;통해&amp;nbsp;직접&amp;nbsp;테스트해보기&lt;/span&gt;&lt;/a&gt;&lt;/i&gt;&lt;br&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category> /Real MySQL 8.0</category>
      <category>dirty read</category>
      <category>Isolation Level</category>
      <category>Non Repeatable Read</category>
      <category>phantom read</category>
      <category>read committed</category>
      <category>READ UNCOMMITTED</category>
      <category>real mysql 8.0</category>
      <category>repeatable read</category>
      <category>Serializable</category>
      <category>인덱스</category>
      <author>dolmeng2</author>
      <guid isPermaLink="true">https://cl8d.tistory.com/109</guid>
      <comments>https://cl8d.tistory.com/109#entry109comment</comments>
      <pubDate>Mon, 19 Jun 2023 00:51:00 +0900</pubDate>
    </item>
    <item>
      <title>[Real MySQL 8.0] 트랜잭션과 MySQL / InnoDB의 락 (Lock)</title>
      <link>https://cl8d.tistory.com/108</link>
      <description>&lt;h4 style=&quot;text-align: left;&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; style=&quot;text-align: left;&quot;&gt;이번 포스팅에서는 트랜잭션에 대한 간단한 개념과, MySQL과 InnoDB 스토리지엔진에서 제공하는 락에 대해서 알아보자.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot;&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;h4 style=&quot;text-align: left;&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; style=&quot;text-align: left;&quot;&gt;트랜잭션은 하나의 논리적인 작업 셋에 &lt;span style=&quot;color: #EF5369;&quot;&gt;완전히 적용(commit)되거나, 아무것도 적용되지 않음 (rollback)&lt;/span&gt;을 보장해야 한다.&lt;br&gt;InnoDB와 MyISAM 테이블은 트랜잭션 관점에서도 차이를 보이는데, 이를 확인해보자.&lt;br&gt;우선, 아래와 같이 myISAM과 innoDB를 사용한 2개의 테이블을 만들자.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;CREATE TABLE tb_myisam (
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;id INT NOT NULL,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;PRIMARY KEY (id)
) ENGINE=MYISAM;

CREATE TABLE tb_innodb (
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;id INT NOT NULL,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;PRIMARY KEY (id)
) ENGINE=innodb;&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;br&gt;그리고, 각각에 대해서 레코드를 1개씩 저장해보자.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;insert into tb_myisam (id) values (3);
insert into tb_innodb (id) values (3);&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;br&gt;이후, auto commit 모드를 활성화한 다음 다음과 같이 레코드를 추가해보자.&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style3&quot;&gt;  AUTO COMMIT이란?&lt;br&gt;사용자가 commit 명령어를 사용하지 않아도 SQL문이 성공적으로 실행되면 자동으로 커밋을, 문제가 발생하면 알아서 롤백을 해준다. 기본적으로는 활성화가 되어 있다.&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;set autocommit = on;

insert into tb_myisam (id) values (1), (2), (3);
insert into tb_innodb (id) values (1), (2), (3);&lt;/code&gt;&lt;/pre&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;896&quot; data-origin-height=&quot;74&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cpuRlJ/btskgrejr5m/T2mxoeTbxXwnhPhuZIqy6k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cpuRlJ/btskgrejr5m/T2mxoeTbxXwnhPhuZIqy6k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cpuRlJ/btskgrejr5m/T2mxoeTbxXwnhPhuZIqy6k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcpuRlJ%2Fbtskgrejr5m%2FT2mxoeTbxXwnhPhuZIqy6k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;896&quot; height=&quot;74&quot; data-origin-width=&quot;896&quot; data-origin-height=&quot;74&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;80&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QIHdP/btskhcgqlbV/RKGCP4PYWbPNEUNAQNWC7K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QIHdP/btskhcgqlbV/RKGCP4PYWbPNEUNAQNWC7K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QIHdP/btskhcgqlbV/RKGCP4PYWbPNEUNAQNWC7K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQIHdP%2FbtskhcgqlbV%2FRKGCP4PYWbPNEUNAQNWC7K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;900&quot; height=&quot;80&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;80&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;당연히 primary key로 설정된 컬럼에 동일한 값을 넣으려고 했기 때문에, 두 테이블 모두 오류가 발생한다.&lt;br&gt;&amp;nbsp;&lt;br&gt;하지만, 실제로 select 했을 때의 결과는 다음과 같다.&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;252&quot; data-origin-height=&quot;194&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bpKd03/btskiyXz1fh/9SWtZHAt8P6Xx8K8SCHmDk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bpKd03/btskiyXz1fh/9SWtZHAt8P6Xx8K8SCHmDk/img.png&quot; data-alt=&quot;tb_myisam&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bpKd03/btskiyXz1fh/9SWtZHAt8P6Xx8K8SCHmDk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbpKd03%2FbtskiyXz1fh%2F9SWtZHAt8P6Xx8K8SCHmDk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;252&quot; height=&quot;194&quot; data-origin-width=&quot;252&quot; data-origin-height=&quot;194&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;tb_myisam&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;238&quot; data-origin-height=&quot;114&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ntkOc/btskhaiBoL8/ADVYeZiTikuXQgynzwnQb0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ntkOc/btskhaiBoL8/ADVYeZiTikuXQgynzwnQb0/img.png&quot; data-alt=&quot;tb_innodb&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ntkOc/btskhaiBoL8/ADVYeZiTikuXQgynzwnQb0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FntkOc%2FbtskhaiBoL8%2FADVYeZiTikuXQgynzwnQb0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;238&quot; height=&quot;114&quot; data-origin-width=&quot;238&quot; data-origin-height=&quot;114&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;tb_innodb&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;br&gt;myISAM으로 생성한 테이블의 경우 &lt;span style=&quot;color: #EF5369;&quot;&gt;1, 2, 3 모두가 그대로 삽입&lt;/span&gt;되어 있는 것이다.&lt;br&gt;이는, 롤백이 발생하지 않았다기보다는, 쿼리문에서 &lt;span style=&quot;color: #EF5369;&quot;&gt;1, 2는 삽입 성공했지만 3을 삽입한 순간에 오류가 발생&lt;/span&gt;하여 3에 대해서만 삽입하지 않고 종료한 것이다. 반면에, innoDB의 경우 우리가 흔히 생각한 것처럼 1, 2, 3 모두를 삽입하는 쿼리에 대해서 롤백을 한다.&lt;br&gt;이런 현상을 '&lt;span style=&quot;color: #EF5369;&quot;&gt;Partial Update&lt;/span&gt;'라고 하며, 데이터의 경합성을 맞추는 데 어려움을 주는 요소 중 하나이다.&lt;br&gt;왜 innoDB를 많이들 사용하는지 한 가지 이유를 더 알아간다  &lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot;&gt;&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; &lt;/b&gt;&lt;b&gt; 락 - MySQL&lt;/b&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;MySQL에서 락은 크게 &lt;b&gt;스토리지 엔진 레벨과 MySQL 엔진 레벨&lt;/b&gt;로 나눌 수 있다.&lt;br&gt;MySQL 엔진 레벨의 락은&lt;span style=&quot;color: #EF5369;&quot;&gt; 글로벌 락, 테이블 락, 메타데이터 락, 네임드 락&lt;/span&gt;을 제공한다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size18&quot; style=&quot;text-align: left;&quot;&gt;&lt;b&gt;  글로벌 락&lt;/b&gt;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;MySQL에서 제공하는 락 중에서 가장 범위가 큰 락이다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;FLUSH TABLES WITH READ LOCK&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;하나의 세션에서 글로벌 락을 획득하면 다른 세션에서&lt;span style=&quot;color: #EF5369;&quot;&gt; select를 제외한 대부분의 DDL / DML 문장 실행 시, 글로벌 락이 해제될 때까지 계속 대기 상태&lt;/span&gt;로 남게 된다. MySQL 서버 전체에 영향을 주기 때문에 작업 중인 테이블이나 데이터베이스를 넘어서 동일하게 영향을 준다.&lt;br&gt;&amp;nbsp;&lt;br&gt;만약, 글로벌 락이 실행되기 전에 쓰기 락이 걸린 SQL이 실행됐다면, 기존의 쓰기 락이 완전히 해제된 이후에 글로벌 락이 걸리게 된다. 즉, 현재 실행 중인 모든 쿼리가 완료되어야만 글로벌 락을 실행할 수 있는 것이다. 하지만, MySQL 8.0부터 Xtrabackup, Enterprise Backup 같이 '&lt;span style=&quot;color: #EF5369;&quot;&gt;백업 락&lt;/span&gt;'이라는 조금 더 가벼운 락이 추가되었다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;# 백업 락
LOCK INSTANCE FOR BACKUP;

# 락 해제
UNLOCK INSTANCE;&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;br&gt;백업 락의 경우 특정 세션에서 획득하면 모든 세션에서 아래와 같은 정보를 변경할 수 없다.&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style3&quot;&gt;- 데이터베이스나 테이블 등, 모든 객체의 생성 및 변경, 삭제&lt;br&gt;- REPAIR TABLE / OPTIMIZE TABLE&lt;br&gt;- 사용자 관리 및 비밀번호 변경&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;하지만, 일반적인 테이블의 데이터 변경은 허용하기 때문에&lt;b&gt; 이전의 글로벌 락보다 확실히 완화된 락을 사용&lt;/b&gt;할 수 있다.&lt;br&gt;기존의 글로벌 락을 사용했다면 복제용 서버 (레플리카 서버)에서 데이터의 복제가 일어나는 도중에 기존 서버에서 오류가 생겼다면 백업 역시 계속 지연 / 혹은 실패했을 것이다. 백업이라는 것은 상당히 오래 걸리는 작업이기 때문에 실패 시에 또 다시 엄청난 시간을 들여야 하고, 그러면 서비스 자체에도 영향이 크기 때문에 이러한 문제를 해결하기 위해서 '백업 락'이라는 것이 도입되었다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size18&quot; style=&quot;text-align: left;&quot;&gt;&lt;b&gt;  테이블 락&lt;/b&gt;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;테이블 락의 경우 &lt;span style=&quot;color: #EF5369;&quot;&gt;테이블 단위로 설정하는 잠금&lt;/span&gt;이며, 명시적 (explicit) / 암시적 (implicit) 락을 획득할 수 있다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;# 락 획득
LOCK TABLES table_name [READ | WRITE]

# 락 해제
UNLOCK TABLES&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;br&gt;락을 획득할 때 2개의 옵션을 줄 수 있는데, 하나는 READ, 하나는 WRITE이다.&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;READ&lt;/b&gt;&lt;br&gt;- 테이블에 대한 읽기 락 획득&lt;br&gt;- 다른 세션이 동시에 같은 테이블에 대한 읽기 락 &lt;span style=&quot;color: #EF5369;&quot;&gt;획득 가능&lt;/span&gt;&lt;br&gt;- &lt;span style=&quot;color: #EF5369;&quot;&gt;테이블에 대한 내용은 읽을 수 있지만, 수정은 불가능&lt;/span&gt;&amp;nbsp;&lt;br&gt;- 동시에 다른 세션이 해당 테이블에 대해서 쓰기 락 획득 불가능&lt;br&gt;&lt;br&gt;&lt;b&gt;WRITE&lt;br&gt;&lt;/b&gt;- 테이블에 대한 쓰기 락 획득&amp;nbsp;&lt;br&gt;- 다른 세션이 동시에 같은 테이블에 대한 읽기 락, 쓰기 락 &lt;span style=&quot;color: #EF5369;&quot;&gt;획득 불가능&lt;/span&gt;&lt;br&gt;- 쓰기 락을 획득한 세션은 테이블의 내용을 읽고 수정할 수 있음&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;br&gt;명시적 락의 경우 위와 같이 &lt;b&gt;쿼리를 통해서 직접적으로 선언하는 방법&lt;/b&gt;이며, 묵시적 락의 경우 테이블을 변경하는 쿼리를 실행했을 때 &lt;b&gt;MySQL 서버가 해당 테이블에 락을 설정하고 데이터를 변경한 다음, 바로 락을 해제하는 형태&lt;/b&gt;로 사용된다. 자동으로 할당 및 해제가 일어나기 때문에 편리하지만, innoDB의 경우 레코드 기반 락을 제공하기 때문에 테이블 락이 실행되더라도 DML에서는 무시되고, DDL에서만 영향을 준다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size18&quot; style=&quot;text-align: left;&quot;&gt;&lt;b&gt;  네임드 락&lt;/b&gt;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;테이블, 레코드가 아닌 &lt;span style=&quot;color: #EF5369;&quot;&gt;임의의 문자열에 대해 잠금을 획득&lt;/span&gt;할 수 있다. (일종의 변수 느낌?)&lt;br&gt;1대의 DB 서버에 여러 대의 웹 서버가 접속하였을 때 특정 정보를 동기화해야 하는 상황에서 네임드락을 사용할 수 있다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;# hello라는 문자열에 대해서 잠금 획득
# 이미 사용 중이라면 2초 대기 후 해제
SELECT GET LOCK('hello', 2);

# 잠금이 설정되어 있는지 확인
SELECT IS_FREE_LOCK('hello');

# hello라는 문자열에 대해 획득했던 잠금 해제
SELECT RELEASE_LOCK('hello');&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;3가지 쿼리 모두 select문의 결과로 락을 정상적으로 획득하거나 해제하면 1을, 아니라면 null이나 0을 반환한다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;# 동시에 락 걸기
SELECT GET_LOCK('hello1', 10);
SELECT GET_LOCK('hello2', 10);

# 모두 해제
SELECT RELEASE_ALL_LOCKS();&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;네임드 락의 경우 &lt;span style=&quot;color: #EF5369;&quot;&gt;복잡한 조건으로 레코드를 한꺼번에 많이 변경할 때 사용&lt;/span&gt;하며, MySQL 8.0부터는 네임드 락을 중첩하여 사용하는 것을 가능하도록 만들었다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size18&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;  메타데이터 락&lt;/b&gt;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&lt;span style=&quot;color: #EF5369;&quot;&gt;테이블이나 뷰의 이름이나 구조를 변경하는 경우&lt;/span&gt;, 메타데이터 락을 획득한다.&amp;nbsp;&lt;br&gt;명시적으로 획득하거나 해제가 안 되며 RENAME 등의 쿼리를 실행할 때 자동으로 획득하는 잠금이다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;# 이러면 tb_1, tb_2 모두에 대해서 잠금을 설정한다
RENAME TABLE tb_1 TO tb_2;&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;br&gt;테이블의 이름을 변경할 때 혹시나 실서비스에서 '해당하는 테이블이 없습니다' 같은 경고 메시지가 뜨지 않도록 하기 위해서는, 하나의 rename table 쿼리에 여러 개의 조건을 넣어두면 된다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;RENAME TABLE tb_1 to tb_2, tb_3 to tb_1&lt;/code&gt;&lt;/pre&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;642&quot; data-origin-height=&quot;208&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AKpkw/btsklotDaO6/NKYIT5uJ73SY4Dr6a2QekK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AKpkw/btsklotDaO6/NKYIT5uJ73SY4Dr6a2QekK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AKpkw/btsklotDaO6/NKYIT5uJ73SY4Dr6a2QekK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAKpkw%2FbtsklotDaO6%2FNKYIT5uJ73SY4Dr6a2QekK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;472&quot; height=&quot;153&quot; data-origin-width=&quot;642&quot; data-origin-height=&quot;208&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;이렇게 되면 tb_1에 대해서는 tb_2로 바꾸면서, 기존의 tb_3을 tb_1이라는 이름으로 바로 바꾸기 때문에 경고 메시지가 뜨지 않는다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size18&quot; style=&quot;text-align: left;&quot;&gt;&lt;b&gt;  테이블 락 + 메타데이터 락&amp;nbsp;&lt;/b&gt;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;로그 정보를 저장하는 로그 테이블이 있다고 생각해보자. 단순히 사용자의 웹 요청에 대한 정보를 저장하기 때문에 INSERT만 진행되는 테이블이다. 하지만, 테이블의 구조를 변경해야 한다면 어떻게 해야 할까?&lt;br&gt;기본적으로 MySQL의 DDL을 사용한다면 단일 스레드로 동작하기 때문에 시간이 오래 걸릴 것이다.&amp;nbsp;&lt;br&gt;보통 이런 경우에는 새로운 테이블을 생성하고, &lt;span style=&quot;color: #EF5369;&quot;&gt;비교적 최근 데이터를 제외하고&lt;/span&gt; PK로 범위를 나눈 레코드들에 대해 여러 개의 스레드로 복사하는 게 일반적이다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;INSERT INTO tb_new SELECT * FROM tb_origin WHERE id &amp;gt;= 0 AND id &amp;lt;= 10000;&lt;/code&gt;&lt;/pre&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1014&quot; data-origin-height=&quot;440&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rrenC/btskgcnKuTH/Imy2g32D3uZtOzqd6rkvd1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rrenC/btskgcnKuTH/Imy2g32D3uZtOzqd6rkvd1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rrenC/btskgcnKuTH/Imy2g32D3uZtOzqd6rkvd1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrrenC%2FbtskgcnKuTH%2FImy2g32D3uZtOzqd6rkvd1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;579&quot; height=&quot;251&quot; data-origin-width=&quot;1014&quot; data-origin-height=&quot;440&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;br&gt;그리고, 남은 데이터에 대해서는 양이 적기 때문에 테이블 구조 변경이 용이해지며, 비교적 서비스에 덜 영향을 미치게 된다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;# auto commit 모드 끄기
SET autocommit = 0;

# 대상 테이블에 대해서 쓰기 락 획득 (테이블 락)
LOCK TABLES tb_origin WRITE, tb_new WRITE;

# 남아있는 데이터를 복사해주기
SELECT MAX(id) AS @max_id FROM tb_new;
INSERT INTO tb_new SELECT * FROM tb_origin WHERE id &amp;gt; @max_id;
COMMIT;

# 복사 완료 시 rename 진행 (메타데이터 락)
RENAME TABLE tb_origin TO tb_origin_old, tb_new to tb_origin;

# 임시 테이블 삭제
DROP TABLE tb_origin_old&lt;/code&gt;&lt;/pre&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1228&quot; data-origin-height=&quot;604&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bcyehd/btskk85rwJv/fsEmyAHYL7SvxHLiH0QnSk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bcyehd/btskk85rwJv/fsEmyAHYL7SvxHLiH0QnSk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bcyehd/btskk85rwJv/fsEmyAHYL7SvxHLiH0QnSk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbcyehd%2Fbtskk85rwJv%2FfsEmyAHYL7SvxHLiH0QnSk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;689&quot; height=&quot;339&quot; data-origin-width=&quot;1228&quot; data-origin-height=&quot;604&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;이렇게 되면 테이블 락의 경우 20000~20100번째 레코드를 복사할 때만 걸리기 때문에, &lt;span style=&quot;color: #EF5369;&quot;&gt;기존 데이터를 tb_new로 복사할 때 최대한 많은 데이터를 복사해둬야 락을 점유하는 시간이 짧아진다&lt;/span&gt;.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot;&gt;&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; &lt;/b&gt;&lt;b&gt;&amp;nbsp;락 - innoDB 스토리지 엔진&lt;/b&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;innoDB에서는 &lt;b&gt;MySQL에서 제공하는 락과는 별도로 락을 제공&lt;/b&gt;하고 있다.&lt;br&gt;기본적으로 information_schema 테이블의 INNODB_TRX, INNODB_LOCKS, INNODB_LOCK_WAITS 테이블에 있는 정보들을 바탕으로 어떤 트랜잭션이 락을 점유하고 대기 중인지 알 수 있으며, 여러 모니터링 방법들을 통해서 스토리지 엔진에서 제공하는 락까지 효율적으로 관리할 수 있도록 돕고 있다.&lt;br&gt;&amp;nbsp;&lt;br&gt;innoDB에서는 총 8가지의 락을 제공하고 있으며, 이는 다음과 같다.&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style3&quot;&gt;- Shared / Exclusive Lock&lt;br&gt;- Intention Lock&lt;br&gt;- Record Lock&lt;br&gt;- Gap Lock&lt;br&gt;- Next-Key Lock&lt;br&gt;- Insert Intention Lock&lt;br&gt;- Auto-increment Lock&lt;br&gt;- Predicate Lock for Spatial Indexes&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size18&quot; style=&quot;text-align: left;&quot;&gt;&lt;b&gt;  Shared / Exclusive Lock (공유 락, 베타 락)&lt;/b&gt;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;공유 락(S-Lock)과 배타 락(X-Lock)이라는 레코드 레벨의 락이며,&lt;b&gt; 공유 락의 경&lt;/b&gt;우 해당 락을 보유한 트랜잭션이 &lt;span style=&quot;color: #EF5369;&quot;&gt;해당 레코드를 읽을 수 있도록 하고&lt;/span&gt;, &lt;b&gt;배타 락의 경우&lt;/b&gt; 해당 잠금을 보유한 트랜잭션이 &lt;span style=&quot;color: #EF5369;&quot;&gt;행을 업데이트하거나 삭제할 수 있도록&lt;/span&gt; 만드는 것이다.&lt;br&gt;&amp;nbsp;&lt;br&gt;앞서 말했던 테이블 락의 READ / WRITE 모드와 거의 비슷하다고 생각하면 된다.&lt;br&gt;트랜잭션 A가 S-Lock을 가지고 있을 때 트랜잭션 B는 S-Lock을 보유할 수 있지만, 트랜잭션 A가 X-Lock을 가지고 있다면 어떤 락 요청이든 허용되지 않고 계속 대기해야 한다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size18&quot; style=&quot;text-align: left;&quot;&gt;&lt;b&gt;  Intention Lock (의도 락)&lt;/b&gt;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;의도 락(Intention Lock)의 경우 &lt;span style=&quot;color: #EF5369;&quot;&gt;테이블 락과 레코드 락이 공존할 수 있도록&lt;/span&gt; 만든다.&lt;br&gt;테이블의 특정 레코드에 대해서 추후 락을 걸겠다고 표현하는 것과 같다.&lt;br&gt;만약 WRITE 모드의 테이블 락을 걸었다면, innoDB에서는 2가지의 타입으로 의도 락을 수행한다.&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;Intention Shared Lock (IS)&lt;/b&gt;&lt;br&gt;- 트랜잭션이 테이블의 개별 레코드에 공유 락 설정&lt;br&gt;&lt;br&gt;&lt;b&gt;Intention Exclusive Lock (IX)&lt;/b&gt;&lt;br&gt;- 트랜잭션이 테이블의 개별 레코드에 베타 락 설정&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;br&gt;만약 SELECT ... LOCK IN SHARE MODE을 실행한다면 &lt;span style=&quot;color: #EF5369;&quot;&gt;해당 테이블에 대해서 먼저 IS 이상의 락을 걸고&lt;/span&gt;, 그 다음 레코드에 대해서 S-Lock이 걸리며, SELECT ... FOR UPDATE의 경우 &lt;span style=&quot;color: #EF5369;&quot;&gt;해당 테이블에 대해서 먼저 IX 이상의 락을 걸고&lt;/span&gt;, 그 다음에 레코드에 X-Lock이 걸린다. 왜 이렇게 2번 거는 것일까?&lt;br&gt;- 이는, 트랜잭션 A에서 이미 테이블에 대해 락을 걸어두었기 때문에 &lt;b&gt;다른 트랜잭션에서 해당 테이블의 레코드에 접근하여 락을 걸지 못하도록&lt;/b&gt; 하기 위해서이다. 쓰기 작업 시 이미 IX 락이 걸려 있기 때문에 다른 테이블에서 멋대로 데이터를 변경할 수가 없게 되는 것이다.&lt;br&gt;&amp;nbsp;&lt;br&gt;아래는 락 종류에 따른 다른 트랜잭션 접근 시 호환성에 대한 표이다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;&lt;tbody&gt;&lt;tr style=&quot;height: 17px;&quot;&gt;&lt;td style=&quot;width: 20.0%; height: 17px; text-align: center;&quot;&gt;&amp;nbsp;&lt;/td&gt;&lt;td style=&quot;width: 20.0%; height: 17px; text-align: center;&quot;&gt;&lt;b&gt;S&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;width: 20.0%; height: 17px; text-align: center;&quot;&gt;&lt;b&gt;IS&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;width: 20.0%; height: 17px; text-align: center;&quot;&gt;&lt;b&gt;X&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;width: 20.0%; height: 17px; text-align: center;&quot;&gt;&lt;b&gt;IX&lt;/b&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 17px;&quot;&gt;&lt;td style=&quot;width: 20.0%; height: 17px; text-align: center;&quot;&gt;&lt;b&gt;S&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;width: 20.0%; height: 17px; text-align: center;&quot;&gt;호환 가능&lt;/td&gt;&lt;td style=&quot;width: 20.0%; height: 17px; text-align: center;&quot;&gt;호환 가능&lt;/td&gt;&lt;td style=&quot;width: 20.0%; height: 17px; text-align: center;&quot;&gt;충돌&lt;/td&gt;&lt;td style=&quot;width: 20.0%; height: 17px; text-align: center;&quot;&gt;충돌&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 17px;&quot;&gt;&lt;td style=&quot;width: 20.0%; height: 17px; text-align: center;&quot;&gt;&lt;b&gt;IS&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;width: 20.0%; height: 17px; text-align: center;&quot;&gt;호환 가능&lt;/td&gt;&lt;td style=&quot;width: 20.0%; height: 17px; text-align: center;&quot;&gt;호환 가능&lt;/td&gt;&lt;td style=&quot;width: 20.0%; height: 17px; text-align: center;&quot;&gt;충돌&lt;/td&gt;&lt;td style=&quot;width: 20.0%; height: 17px; text-align: center;&quot;&gt;호환 가능&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 17px;&quot;&gt;&lt;td style=&quot;width: 20.0%; height: 17px; text-align: center;&quot;&gt;&lt;b&gt;X&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;width: 20.0%; height: 17px; text-align: center;&quot;&gt;충돌&lt;/td&gt;&lt;td style=&quot;width: 20.0%; height: 17px; text-align: center;&quot;&gt;충돌&lt;/td&gt;&lt;td style=&quot;width: 20.0%; height: 17px; text-align: center;&quot;&gt;충돌&lt;/td&gt;&lt;td style=&quot;width: 20.0%; height: 17px; text-align: center;&quot;&gt;충돌&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 17px;&quot;&gt;&lt;td style=&quot;width: 20.0%; height: 17px; text-align: center;&quot;&gt;&lt;b&gt;IX&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;width: 20.0%; height: 17px; text-align: center;&quot;&gt;충돌&lt;/td&gt;&lt;td style=&quot;width: 20.0%; height: 17px; text-align: center;&quot;&gt;호환 가능&lt;/td&gt;&lt;td style=&quot;width: 20.0%; height: 17px; text-align: center;&quot;&gt;충돌&lt;/td&gt;&lt;td style=&quot;width: 20.0%; height: 17px; text-align: center;&quot;&gt;호환 가능&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;기본적으로 표를 보면 IS의 경우 X, IX의 경우 S, X을 제외하고 여러 트랜잭션에서 동시에 사용이 가능하다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size18&quot; style=&quot;text-align: left;&quot;&gt;&lt;b&gt;  Record Lock (레코드 락)&lt;/b&gt;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;레코드 락의 경우&lt;span style=&quot;color: #EF5369;&quot;&gt; 레코드에 대해서 잠금을 거는 것&lt;/span&gt;이다.&lt;br&gt;innoDB의 경우 레코드 자체를 잠그는 것보다는, &lt;span style=&quot;color: #EF5369;&quot;&gt;인덱스의 레코드&lt;/span&gt;를 잠근다. 하지만, 인덱스가 없더라도 클러스터링 키를 내부적으로 생성해줬던 것처럼, 여기서도 &lt;b&gt;클러스터 인덱스를 알아서 생성해주기 때문에&lt;/b&gt; 해당 인덱스를 레코드 잠금에 사용한다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;# id = 1인 레코드에 대해 S-Lock
SELECT id FROM tb_1 WHERE id = 1 LOCK IN SHARE MODE;

# id = 1인 레코드에 대해 X-Lock
SELECT id FROM tb_1 WHERE id = 1 FOR UPDATE;&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;br&gt;레코드가 아닌 인덱스의 레코드에 대해 락을 거는 이유는 아래와 같다.&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style3&quot;&gt;- innoDB의 B+ Tree 인덱스를 통해 데이터를 저장하거나 검색하는데, 이때 인덱스의 레코드를 탐색하여 특정 레코드를 찾아내기 때문에 인덱스의 레코드에 대한 접근이 필요하다.&lt;br&gt;&lt;br&gt;- 또한, 인덱스에 대해서만 락을 걸기 때문에 다른 트랜잭션들이 접근하는 경우에도 충돌을 최소화하여 성능 자체에도 도움을 줄 수 있다.&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size18&quot; style=&quot;text-align: left;&quot;&gt;&lt;b&gt;  Gap Lock (갭 락)&lt;/b&gt;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;갭 락의 경우 레코드가 아닌 &lt;span style=&quot;color: #EF5369;&quot;&gt;레코드와 인접한 다른 레코드 사이의 간격을 잠그는 것&lt;/span&gt;을 의미한다. 이때, &lt;span style=&quot;color: #EF5369;&quot;&gt;첫 번째와 마지막 레코드 앞&lt;/span&gt;&lt;span style=&quot;color: #EF5369;&quot;&gt;(Negative Infinity)&lt;/span&gt;&lt;span style=&quot;color: #EF5369;&quot;&gt; 뒤(Positive Infinity)에 가상의 레코드가 있다고 가정&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;하고 생성하는 것도 가능&lt;/span&gt;하다. 이러한 가상 레코드를 'Supremum' 가상 레코드라고 한다.&lt;br&gt;&amp;nbsp;&lt;br&gt;갭 락을 사용하게 되면, 조회 쿼리를 두 번 실행했을 때 &lt;b&gt;다른 트랜잭션에서 수정이 발생하더라도 같은 결과가 리턴되도록 보장할 수 있다&lt;/b&gt;. 즉, Phantom Read를 방지하는 효과를 가진다. 이는 갭 락이 레코드 사이에 새로운 레코드가 삽입되는 것을 제어하는 것이기 때문이다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;select * from tb_1 where id between 1 and 10;&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;위와 같은 쿼리가 있을 때 1~10 사이에 X락이 걸리는 것과 동일하기 때문에, 새로운 데이터가 삽입되려면 대기해야 된다.&lt;br&gt;&amp;nbsp;&lt;br&gt;또한,&lt;span style=&quot;color: #EF5369;&quot;&gt; 동일한 갭에 대해서 서로 다른 트랜잭션에서 충돌하는 락을 가질 수도 있다&lt;/span&gt;. 즉, 트랜잭션 A가 Gap S-Lock을 가지고 있고, 트랜잭션 B가 동일한 갭에 대해서 Gap X-Lock을 가질 수 있다. 이는 특정 레코드가 제거되었을 때 서로 다른 트랜잭션에서 해당 레코드에 가지고 있던 갭 락을 병합해야 하기 때문에 충돌이 허용하는 것이다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;  Where 절을 통해 데이터를 조회한 결과가 하나일 때, Record vs Gap 중 어떤 락이 사용될까?&lt;/b&gt;&lt;br&gt;- 기본적으로 컬럼에 unique index가 걸려 있다면 record lock이 사용된다.&lt;br&gt;- 인덱스가 없거나, unique 하지 않은 index라면 gap lock을 사용해야 한다.&lt;br&gt;: 인덱스가 있다면 쿼리 결과를 조회하기 위해 스캔한 인덱스 범위에 대해 gap lock이 적용되고, 인덱스가 없다면 테이블 전체를 스캔해야 하기 때문에 모든 레코드에 대해서 락이 걸린다.&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size18&quot; style=&quot;text-align: left;&quot;&gt;&lt;b&gt;  Next-Key Lock (넥스트 키 락)&lt;/b&gt;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&lt;span style=&quot;color: #EF5369;&quot;&gt;레코드 락과 갭 락을 합친 형태&lt;/span&gt;를 넥스트 키 락이라고 한다.&lt;br&gt;innoDB에서는 테이블 인덱스를 검색하거나 스캔할 때 실제 인덱스 레코드에 대해 S / X-Lock을 설정하여 레코드 단위 락을 걸게 된다. 하지만, &lt;b&gt;넥스트 키 락의 경우 해당 인덱스 레코드 이전의 갭에도 영향을 미치기 때문에&lt;/b&gt; 하나의 세션이 특정 레코드 1번에 S / X-Lock을 걸면 다른 세션은 1번 바로 앞의 갭에 새로운 레코드를 생성할 수 없게 된다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style3&quot;&gt;예를 들어, 10, 11, 13, 20에 인덱스가 걸려 있다면 아래와 같은 구간에서 락이 생성된다.&lt;br&gt;- (Negative infinity, 10]&lt;br&gt;- (10, 11]&lt;br&gt;- (11, 13]&lt;br&gt;- (13, 20]&lt;br&gt;- (20, Positive infinity)&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;br&gt;잘 이해가 되지 않을 수도 있으니, 예시를 들어보자. 아래는 crew 테이블에 대해 id가 100 이상인 레코드를 조회하는 쿼리이다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;SELECT * FROM crew WHERE id &amp;gt; 100 FOR UPDATE;&lt;/code&gt;&lt;/pre&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1372&quot; data-origin-height=&quot;874&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dKhCVV/btskg95c3zj/UihHAEoa5WcpRzl5gmq9J1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dKhCVV/btskg95c3zj/UihHAEoa5WcpRzl5gmq9J1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dKhCVV/btskg95c3zj/UihHAEoa5WcpRzl5gmq9J1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdKhCVV%2Fbtskg95c3zj%2FUihHAEoa5WcpRzl5gmq9J1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;526&quot; height=&quot;335&quot; data-origin-width=&quot;1372&quot; data-origin-height=&quot;874&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;이때, id &amp;gt; 100을 만족하는 id = 101인 리코드를 발견하면, 해당 레코드 직전의 레코드와 그 사이 락이 걸린다. (50 ~ 101 사이 갭 락)&lt;br&gt;이후, &lt;span style=&quot;color: #EF5369;&quot;&gt;id &amp;gt; 100을 만족하는 레코드에 대해서도 갭 락과 레코드 락이 걸리게 되며&lt;/span&gt;, 이때 id = 150이 최대이기 때문에 Positive Infinity Gap Lock이 걸리게 된다.&lt;br&gt;&amp;nbsp;&lt;br&gt;넥스트 키 락의 경우 바이너리 로그가 기록되는 쿼리가 레플리카 서버에서 실행될 때, &lt;b&gt;기존 서버에서 만들어낸 결과와 동일한 결과를 만들어내도록 보장하는 것이 주 목적&lt;/b&gt;이다. 로그 포맷의 경우 statement, row, mixed로 나누어지는데, 아래와 같은 특징을 가진다.&lt;br&gt;&amp;nbsp;&lt;br&gt;- statement: 가장 오래된 포맷, 데이터 변경으로부터 사용되는 모든 쿼리를 저장&lt;br&gt;- row : 변경된 모든 레코드에 대한 정보 기록&lt;br&gt;- mixed: statement + row를 혼합하여 사용하는 방법&lt;br&gt;&amp;nbsp;&lt;br&gt;기존에는 statement 포맷의 바이너리 로그를 사용하는 것이 보편적이었으나, 이 경우 repeatable-read 이상의 격리 수준을 사용해야 한다는 점과 (read-committed 사용 시 실행 시점마다 결과가 달라질 수 있어서) 쿼리의 실행마다 결과가 달라지는 경우 (사용자 정의 함수, 프로시저 사용 등) 해당 쿼리는 사용할 수 없었다는 단점이 있었다. 또한,&amp;nbsp;넥스트 키 락과 갭 락으로 인해 데드락이 발생할 수 있으니 &lt;b&gt;바이너리 로그 포맷을 ROW 형태로 바꿔서 락 자체를 줄이는 것&lt;/b&gt;이 좋다. (MySQL 8.0부터는 ROW 포맷의 바이너리 로그가 기본이다.)&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size18&quot; style=&quot;text-align: left;&quot;&gt;&lt;b&gt;  Insert Intention Lock (삽입 의도 락)&lt;/b&gt;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;삽입 의도 락은&lt;span style=&quot;color: #EF5369;&quot;&gt; INSERT 구문이 실행될 때 묵시적으로 설정되는 일종의 갭 락&lt;/span&gt;이다.&lt;br&gt;&lt;b&gt;여러 개의 트랜잭션이 갭 내부의 서로 다른 위치에 삽입을 진행하려고 할 때 대기 없이 실행&lt;/b&gt;되도록 하는 것이 목적이다. 기본적으로 삽입 의도 락들끼리는 충돌이 발생하지 않기 때문이다.&lt;br&gt;&amp;nbsp;&lt;br&gt;만약 id = 3, 7인 레코드가 있을 때 트랜잭션 A는 id = 5를, 트랜잭션 B는 id = 4을 삽입하는 상황을 가정해보자.&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1858&quot; data-origin-height=&quot;524&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eiFmY1/btskhLbVlhx/6AVfCis79GwQ5KgW7YPwuK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eiFmY1/btskhLbVlhx/6AVfCis79GwQ5KgW7YPwuK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eiFmY1/btskhLbVlhx/6AVfCis79GwQ5KgW7YPwuK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeiFmY1%2FbtskhLbVlhx%2F6AVfCis79GwQ5KgW7YPwuK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1858&quot; height=&quot;524&quot; data-origin-width=&quot;1858&quot; data-origin-height=&quot;524&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;기본적인 갭 락을 걸었다면 트랜잭션 A가 5를 삽입하는 과정에서 &lt;b&gt;이전 레코드인 3 ~ 5 사이에 갭 락&lt;/b&gt;이 걸릴 것이다. 이때, 트랜잭션 B가 id = 4을 삽입하려고 했을 때 &lt;b&gt;이미 3~5 사이에 갭락이 걸려 있기 때문에 종료될 때까지 기다려야 해서 대기 시간이 존재&lt;/b&gt;한다.&lt;br&gt;&amp;nbsp;&lt;br&gt;반면에, 삽입 의도 락을 걸었다면 트랜잭션 A가 5를 삽입하는 과정에서&lt;b&gt; 3 ~ 5 사이에 삽입 의도 락&lt;/b&gt;이 걸린다. 이때, 트랜잭션 B가 id = 4를 삽입하려고 할 때 &lt;span style=&quot;color: #EF5369;&quot;&gt;삽입 의도 락은 충돌이 가능하기 때문에 대기 시간 없이 바로 삽입이 가능&lt;/span&gt;해진다.&lt;br&gt;&amp;nbsp;&lt;br&gt;삽입을 위한 X-Lock을 얻기 위해 삽입 의도 락을 통해서 묵시적으로 락을 걸어두게 된다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size18&quot; style=&quot;text-align: left;&quot;&gt;&lt;b&gt;  Auto-increment Lock (자동 증가 락)&lt;/b&gt;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;MySQL에서는 auto_increment라는 컬럼을 통해서 자동으로 증가하는 값을 관리할 수 있다.&lt;br&gt;auto_increment가 사용된 테이블에&lt;span style=&quot;color: #EF5369;&quot;&gt; 여러 개의 레코드가 삽입되는 경우, auto_increment 역시 일관성있게 증가&lt;/span&gt;해야 한다. 이를 위해서 innoDB에서는 자동 증가 락을 통해 (테이블 레벨 락) 관리한다.&lt;br&gt;&amp;nbsp;&lt;br&gt;자동 증가 락의 경우 기본적으로 &lt;b&gt;insert, replace 같이 새로운 레코드를 삽입하는 쿼리에서만 필요&lt;/b&gt;하며, auto_increment 값을 가져오는 순간에만 락이 걸렸다가 즉시 해제된다. 테이블당 단 1개만 존재하기 때문에, 2개의 insert문이 동시에 실행되는 경우&lt;b&gt; 나머지 하나는 락이 해제될 때까지 기다려야 한다&lt;/b&gt;.&lt;br&gt;&amp;nbsp;&lt;br&gt;명시적으로 자동 증가 락은 획득하고 해제하는 방법은 없지만, 어차피 엄청 짧은 시간 동안 걸렸다가 해제하기 때문에 크게 영향은 없다. MySQL 5.1부터는 어떤 식으로 자동 증가 락을 설정할지 시스템 변수로 관리도 가능하다.&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;- innodb_autoinc_lock_mode = 0&amp;nbsp;&lt;/b&gt;&lt;br&gt;모든 INSERT 문장은 자동 증가 락을 사용한다. 이때, 테이블 수준의 락을 적용한다.&lt;br&gt;&lt;br&gt;&lt;b&gt;- innodb_autoinc_lock_mode = 1 (MySQL 5.x 기본 값)&lt;/b&gt;&lt;br&gt;Bulk Inert 사용 시 테이블 레벨의 자동 증가 락을 사용하고, 명령문이 끝날 때까지 이를 유지하게 된다.&lt;br&gt;INSERT ... SELECT, REPLACE ... SELECT, LOAD DATA 같은 구문에서 사용한다.&lt;br&gt;하지만, 만약&lt;b&gt; 삽입될 레코드의 수를 미리 알고 있다면&lt;/b&gt;, &lt;b&gt;래치 (뮤텍스) 제어 하에 필요한 수만큼의 자동 증가 값을 얻어서 수행되며, 테이블 레벨의 잠금을 방지&lt;/b&gt;한다.&amp;nbsp;&lt;br&gt;&lt;br&gt;레코드 건수를 정확하게 알 수 없는 경우에는 모든 자동 증가 값이 연속적인 값이 되기 때문에 안전하게 사용이 가능하다.&lt;br&gt;그러나, Mixed-mode insert의 경우 삽입할 행의 수보다 더 큰 자동 증가 값을 할당될 수 있음을 유의해야 한다.&lt;br&gt;ex) insert into tb_1 values (1, 'A'), (NULL, 'B')&amp;nbsp;&lt;br&gt;- 이런 식으로 null 값이 혼합하여 들어가는 경우.&lt;br&gt;&lt;br&gt;&lt;b&gt;- innodb_autoinc_lock_mode = 2 (MySQL 8.0 기본값)&lt;/b&gt;&lt;br&gt;자동 증가 락을 절대 사용하지 않고 무조건 래치(뮤텍스)를 사용한다.  auto_increment 값의 유니크성 / 단조 증가성을 보장한다.&lt;br&gt;대량의 삽입 쿼리가 실행될 때 다른 트랜잭션에서도 INSERT를 수행할 수 있기 때문에 동시 처리 성능은 높아진다. (여러 명령문이 동시에 숫자를 생성할 수 있음) 하지만, 단건 삽입의 경우 auto_increment 값 사이의 갭이 존재하지 않지만 &lt;b&gt;여러 개가 삽입될 경우 갭이 존재할 수 있다.&amp;nbsp;&lt;/b&gt;&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;이러한 자동 증가 락으로 인해 auto_increment의 값은 기본적으로 한 번 증가하면 절대 줄어들지 않는 것을 기본으로 한다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot;&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;br&gt;Predicate Locks for Spatial Indexes의 경우 공간 인덱싱을 지원한다고 하는데, 이해하기 어려워서 우선 패스...!&lt;br&gt;내용이 어려워서 알듯말듯한 것 같다.&lt;br&gt;&amp;nbsp;&lt;br&gt;다음 포스팅에서는 인덱스와 락의 관계 + 격리 수준에 대해서 알아보도록 하자!&amp;nbsp;&lt;/p&gt;</description>
      <category> /Real MySQL 8.0</category>
      <category>Auto increment Lock</category>
      <category>Gap Lock</category>
      <category>Insert Intention Lock</category>
      <category>Intention Lock</category>
      <category>Next Key Lock</category>
      <category>Real My SQL</category>
      <category>real mysql 8.0</category>
      <category>Record Lock</category>
      <category>S Lock X Lock</category>
      <category>Table Lock</category>
      <author>dolmeng2</author>
      <guid isPermaLink="true">https://cl8d.tistory.com/108</guid>
      <comments>https://cl8d.tistory.com/108#entry108comment</comments>
      <pubDate>Sun, 18 Jun 2023 17:50:32 +0900</pubDate>
    </item>
    <item>
      <title>[Network] 서버의 패킷 수신 동작 알아보기</title>
      <link>https://cl8d.tistory.com/107</link>
      <description>&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-06-16 오후 8.34.15.png&quot; data-origin-width=&quot;2676&quot; data-origin-height=&quot;710&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/6Hlaz/btskgcHcnkL/WgUajJP3L7lQ6az40FW950/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6Hlaz/btskgcHcnkL/WgUajJP3L7lQ6az40FW950/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6Hlaz/btskgcHcnkL/WgUajJP3L7lQ6az40FW950/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F6Hlaz%2FbtskgcHcnkL%2FWgUajJP3L7lQ6az40FW950%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2676&quot; height=&quot;710&quot; data-filename=&quot;스크린샷 2023-06-16 오후 8.34.15.png&quot; data-origin-width=&quot;2676&quot; data-origin-height=&quot;710&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;이번 포스팅에서는 웹 서버측에서 어떤 식으로 패킷을 받아오는지 알아볼 것이다!&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;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;&amp;nbsp;&lt;/h4&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 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;다시 돌아와서, 클라이언트에서 보낸 패킷이 서버로 도착한 직후 어떤 일이 발생하는지 간단하게만 알아보자.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;(이 부분은 크게 중요한 건 아니라서 간단하게 짚고 넘어가고자 한다.)&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;우선, 서버 측으로 들어온 패킷의 경우 사실 '전기 신호'의 형태로 들어온다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;전기 신호는 LAN 어댑터에서 수신되며, 여기서 1과 0의 디지털 데이터로 바뀌게 된다&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-06-16 오후 11.29.02.png&quot; data-origin-width=&quot;1758&quot; data-origin-height=&quot;748&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/de0Ab7/btskfqe9aEv/qW6tb9TOF96amYPTP00zgK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/de0Ab7/btskfqe9aEv/qW6tb9TOF96amYPTP00zgK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/de0Ab7/btskfqe9aEv/qW6tb9TOF96amYPTP00zgK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fde0Ab7%2Fbtskfqe9aEv%2FqW6tb9TOF96amYPTP00zgK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;705&quot; height=&quot;300&quot; data-filename=&quot;스크린샷 2023-06-16 오후 11.29.02.png&quot; data-origin-width=&quot;1758&quot; data-origin-height=&quot;748&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;위 그림은 전기 신호에서 디지털 데이터로 변환되었을 때 패킷의 형태이다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;변경된 디지털 데이터에서 패킷의 가장 마지막단에 있는 FCS (Frame Check Sequence, 오류 검사용 데이터)를 통해 오류의 유무를 검사하게 되고, 데이터가 무결하다면 맨 앞에 있는 MAC 헤더에 있는 &lt;b&gt;수신처의 MAC 주소를 확인해서 본인에게 온 데이터인지 체크&lt;/b&gt;한다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이는, 전기 신호가 LAN 전체로 흘러가고 해당하는 것만 신호를 수신하기 때문에 다른 기기로 가야 하는 패킷이 들어올 수도 있기 때문이다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이후 LAN 어댑터 내부의 버퍼에 저장해두고, &lt;span style=&quot;color: #ef5369;&quot;&gt;인터럽트&lt;/span&gt;를 통해서 CPU에게 패킷이 도착했음을 알린다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그럼 CPU는 하던 작업을 중단하고 LAN 드라이버를 통해 LAN 어댑터의 버퍼 메모리에서 수신한 패킷을 추출하고, MAC 헤더의 타입 필드를 통해 프로토콜을 판별하고, 해당 프로토콜을 처리할 수 있는 소프트웨어를 호출한다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;보통 해당 타입 필드 값은 IP 프로토콜을 나타내는 값이기 때문에&lt;span style=&quot;color: #ef5369;&quot;&gt; TCP / IP의 프로토콜 스택을 호출 후 패킷을 건네준다&lt;/span&gt;.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; 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;style3&quot; /&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  프로토콜 스택과 IP 담당 부분&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로토콜 스택까지 패킷이 오게 되면 IP 담당 부분이 IP 헤더를 점검한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MAC 헤더와 마찬가지로 IP 헤더에서 &lt;b&gt;수신처 IP 주소를 통해 본인에게 보내는 패킷인지 체크&lt;/b&gt;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 OS 내부에서 패킷 중계 시 예상치 못하게 다른 패킷이 들어올 수도 있기 때문이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-06-16 오후 11.53.19.png&quot; data-origin-width=&quot;1378&quot; data-origin-height=&quot;164&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bCFbvx/btskhLvngL1/saQV50TtMu1PCElAHJPEAK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bCFbvx/btskhLvngL1/saQV50TtMu1PCElAHJPEAK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bCFbvx/btskhLvngL1/saQV50TtMu1PCElAHJPEAK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbCFbvx%2FbtskhLvngL1%2FsaQV50TtMu1PCElAHJPEAK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1378&quot; height=&quot;164&quot; data-filename=&quot;스크린샷 2023-06-16 오후 11.53.19.png&quot; data-origin-width=&quot;1378&quot; data-origin-height=&quot;164&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후, 패킷이 잘 분할되었는지 체크하고 (IP 헤더의 플래그 값을 통해 확인) 분할된 패킷이 모두 도착할 때까지 메모리에 저장한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-06-16 오후 11.54.22.png&quot; data-origin-width=&quot;1352&quot; data-origin-height=&quot;120&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Bq43x/btskiTGDM7B/34cVVrZz2xaAiIyElkW7BK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Bq43x/btskiTGDM7B/34cVVrZz2xaAiIyElkW7BK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Bq43x/btskiTGDM7B/34cVVrZz2xaAiIyElkW7BK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBq43x%2FbtskiTGDM7B%2F34cVVrZz2xaAiIyElkW7BK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1352&quot; height=&quot;120&quot; data-filename=&quot;스크린샷 2023-06-16 오후 11.54.22.png&quot; data-origin-width=&quot;1352&quot; data-origin-height=&quot;120&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;전부 도착하게 되면 재조립하여 원래의 패킷으로 복원&lt;/span&gt;을 하고, 프로토콜 번호 항목에 따라서 TCP or UDP or ICMP 담당 부분에게 패킷을 건네준다.&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;style3&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000;&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;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;1:1로 접속하여 대화&lt;/b&gt;하는 것이 일반적이다. 보통&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ef5369;&quot;&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;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://cl8d.tistory.com/68&quot;&gt;프로토콜 스택과 메시지 송신 과정&lt;/a&gt;에 관한 포스팅에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;클라이언트의 데이터 송-수신 과정&lt;/b&gt;을 다음과 같이 정의하였다.&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;1. 소켓 작성&lt;br /&gt;2.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;서버의 소켓과 파이프로 연결 (접속)&lt;/span&gt;&lt;br /&gt;3. 데이터 송-수신&lt;br /&gt;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;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;1. 소켓 작성&lt;br /&gt;2-1.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;소켓을 접속 대기 상태&lt;/span&gt;로 만들기&lt;br /&gt;2-2.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;소켓 접속에 대해 접수&lt;/span&gt;&amp;nbsp;&lt;br /&gt;3. 데이터 송-수신&lt;br /&gt;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;클라이언트의 소켓 연결 프로세스와 비슷하지만, 추가적으로 bind, listen, accept 같은 코드가 생긴 것을 볼 수 있다.&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;스크린샷 2023-06-16 오후 10.07.54.png&quot; data-origin-width=&quot;1760&quot; data-origin-height=&quot;1322&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ya47w/btskexljHAN/mcjoJW1DWVu7R2gSPMA7h1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ya47w/btskexljHAN/mcjoJW1DWVu7R2gSPMA7h1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ya47w/btskexljHAN/mcjoJW1DWVu7R2gSPMA7h1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fya47w%2FbtskexljHAN%2FmcjoJW1DWVu7R2gSPMA7h1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;670&quot; height=&quot;503&quot; data-filename=&quot;스크린샷 2023-06-16 오후 10.07.54.png&quot; data-origin-width=&quot;1760&quot; data-origin-height=&quot;1322&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;b&gt;1. bind : 소켓에 주소를 할당하기&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1686929301271&quot; class=&quot;cpp&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;int bind(int sockfd, struct sockaddr *addr, socklen_t addrlen);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;bind 메서드의 경우 총 3개의 인자를 받는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째 인자의 경우 socket()을 통해 받아오는 디스크립터이며, 두 번째 인자의 경우 IP 주소와 포트 번호를 담고 있는 구조체이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세 번째 인자의 경우 주소 정보를 담은 변수의 길이이며, 바인딩이 성공하면 0을, 실패하면 -1을 반환한다.&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;2. listen: 연결 요청을 대기하기&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1686929301272&quot; class=&quot;cpp&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;int listen(int sock, int backlog);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;bind 메서드를 통해서 하나의 소켓에 ip 주소와 포트 번호를 할당했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제, 클라이언트가 해당 소켓에 연결할 수 있도록 해당 요청을 대기하는 상태로 만들어줘야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;listen이 호출된 이후부터 클라이언트는 connect를 통해서 서버로 연결을 요청할 수 있게 된다.&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;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;3. accept: 연결 요청 수락하기&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1686929301272&quot; class=&quot;cpp&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;int accept(int sock, struct sockaddr*addr, socklen_t *addrlen);&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;첫 번째 인자는 기존의 디스크립터 번호, 두 번째 인자는 대기 큐에서 얻어온 클라이언트의 주소 정보, 세 번째 인자는 addr 변수의 크기를 의미한다. 만약, 대기 큐가 비어있다면 accept은 반환되지 않고 새로운 요청이 올 때까지 blocking 상태가 된다.&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;여기서 accept은 기존의 디스크립터가 아닌, 새로운 디스크립터를 (디스크립터 2) 반환한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;앞에서 사용했던 디스크립터는 연결 요청에 대한 대기까지만 진행하고, accept을 통해 새롭게 할당받은 디스크립터를 통해 실제 클라이언트와 데이터 송-수신 과정을 진행&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&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;접속 대기 소켓이 사라지게 되어서 다음에 다른 클라이언트가 접속하게 된다면 이미 소켓이 연결된 상태니까 해당 요청을 처리할 수 없게&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;되어 곤란해진다. 그래서,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ef5369;&quot;&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;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ef5369;&quot;&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;&lt;span&gt;&amp;nbsp;&lt;/span&gt;하나의 포트 번호에 여러 개의 소켓이 존재할 수도 있는 것&lt;/b&gt;이다. (접속 대기 용도 / 데이터 송-수신 용도) 클라이언트에서 패킷이 도착했을 때 단순히 포트 번호로 어떤 소켓이랑 대화하고 있는지 알 수 없기 때문에, 추가적으로 아래와 같은 정보들을 통해 소켓을 식별한다.&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;- 클라이언트의 IP 주소&lt;br /&gt;- 클라이언트의 포트 번호&lt;br /&gt;- 서버측의 IP 주소&lt;br /&gt;- 서버측의 포트 번호&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;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;클라이언트 측은 모두 다른 포트 번호를 할당하기 때문에&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;각 소켓들이 충분히 식별된다. 하지만, 클라이언트와 서버의 포트 번호만 가지고 식별은 불가능하다. 왜냐하면,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;여러 클라이언트가 접속하게 된다면 클라이언트의 포트 번호 역시 중복될 수 있기 때문&lt;/span&gt;이다. 그래서 추가적으로&lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;클라이언트의 IP 주소까지 합하여&lt;/b&gt;&lt;span&gt;&amp;nbsp;&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;소켓을 만든 직후 접속 동작을 하기 전에는 클라이언트의 IP, 포트 번호를 알 수 없기 때문이다&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 4가지 정보보다는 디스크립터 하나로 식별하는 게 더 간단하고 (메모리 공간도 적게 차지하니까 성능 개선 가능), 표준 인터페이스인 만큼 프로그램과 OS 사이의 원활한 통신을 지원하게 된다.&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;style3&quot; /&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  서버에서 소켓을 받는 과정 - TCP 헤더의 관점에서 분석하기&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;바로 앞서 말한 내용과 비슷하지만, TCP 헤더가 하는 일을 위주로 되새겨보자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-06-17 오전 12.41.15.png&quot; data-origin-width=&quot;2072&quot; data-origin-height=&quot;1144&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bYeYFi/btskgcmYHyn/ggdtdxAfR8yNKIkeEP6gE0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bYeYFi/btskgcmYHyn/ggdtdxAfR8yNKIkeEP6gE0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bYeYFi/btskgcmYHyn/ggdtdxAfR8yNKIkeEP6gE0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbYeYFi%2FbtskgcmYHyn%2FggdtdxAfR8yNKIkeEP6gE0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;842&quot; height=&quot;465&quot; data-filename=&quot;스크린샷 2023-06-17 오전 12.41.15.png&quot; data-origin-width=&quot;2072&quot; data-origin-height=&quot;1144&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;  소켓 접속 동작&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 TCP 헤더의 &lt;span style=&quot;color: #ef5369;&quot;&gt;SYN 컨트롤 비트가 1&lt;/span&gt;로 패킷이 들어왔다면, 해당 패킷은 접속 동작의 패킷이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 패킷이 들어오면 &lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;패킷의 수신처 포트 번호를 조사하여 해당 번호와 같은 포트 번호를 할당한 접속 대기 상태의 소켓이 있는지 확인&lt;/b&gt;&lt;/span&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; (위에서 봤던 디스크립터2에 대한 부분) 송신처의 IP 주소, 포트 번호, 시퀀스 번호의 초기값, 윈도우 값 등의 정보를 기록한다. 또한, 송-수신 버퍼로 사용하는 메모리 영역을 확보한 다음, ACK 번호, 서버에서 클라이언트에 보내는 데이터에 대한 시퀀스 번호의 초기값, 클라이언트에서 서버로 보낸 데이터를 받기 위해 수신 버퍼의 빈 용량을 나타내는 윈도우 값 등을 기록한 TCP 헤더를 만들어서 IP 담당 부분에게 의뢰하여 클라이언트에게 반송한다.&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;ACK를 보낼 것이구, 해당 패킷을 받게 되면 소켓의 접속 동작이 완료&lt;/b&gt;된다. accept을 통해 쉬고 있던 서버는 새로 만든 소켓의 디스크립터를 전달하여 다시 동작을 재개하게 된다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;1. TCP 헤더의 SYN 컨트롤 비트 확인&lt;br /&gt;2. 수신처 포트 번호 조사&lt;br /&gt;3. 해당 포트 번호와 동일한 번호를 가진 접속 대기 소켓 복사 후 새 소켓 생성&lt;br /&gt;4. 송신처의 IP / 포트  번호 등 기록&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;&amp;nbsp;&lt;/p&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;접속이 완료되면 &lt;span style=&quot;color: #ef5369;&quot;&gt;데이터 송-수신 단계&lt;/span&gt;를 들어갈 텐데, 먼저 도착한 패킷이 어느 소켓에 해당하는지 조사한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로운 소켓이 복사되면서 동일한 포트 번호를 가진 소켓이 여러 개가 될 수 있기 때문에 송-수신처의 IP, 포트 번호 모두를 조합하여 소켓을 찾는다. 이후, 소켓에 기록된 시퀀스 번호나 이전에 온 데이터 조각의 길이로부터 다음 시퀀스 번호 값을 계산하여, TCP 헤더에 기록된 시퀀스 번호와 일치하는지 확인하여&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;span style=&quot;color: #ef5369;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;분할된 패킷을 원래로 복원하기 위해서&lt;/span&gt; 새로운 패킷이 도착할 때마다 데이터 조각들을 연결&lt;/span&gt;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수신한 데이터를 수신 버퍼에 저장하면 응답용 TCP 헤더에 수신 패킷의 시퀀스 번호와 데이터 조각으로부터 계산한 ACK 번호를 기록한 뒤, IP 담당 부분에게 의뢰하여 클라이언트에게 다시 반송한다. 이후 애플리케이션의 필요에 따라서 버퍼에 저장된 데이터를 건네주게 된다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;1. 도착한 패킷의 송-수신처 IP / 포트 번호를 통해 소켓 식별&lt;br /&gt;2. 데이터 조각들을 연결하여 수신 버퍼에 보관&lt;br /&gt;3. 클라이언트에게 ACK 되돌려줌&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;&amp;nbsp;&lt;/p&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;클라이언트든 서버든 close 호출하고, FIN=1을 IP 담당 부분에게 의뢰한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 받은 수신측은 ACK 번호를 반송하고, 또 다시 FIN=1을 보내고, 다시 ACK를 보내고 연결이 끊긴다.&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;흔히 생각하는 4-way handshake 과정이기 때문에 자세한 내용은 생략하겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 &lt;a title=&quot;포스팅&quot; href=&quot;https://cl8d.tistory.com/71&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;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 대략적인 클라이언트 &amp;lt;-&amp;gt; 서버 간의 통신 과정을 알아보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 중간중간 생략한 내용도 있고, 특히 리퀘스트 메시지 해석하는 부분은 책 한 권으로 쓰일 수 있을 만큼 내용이 많아서 따로 정리하지는 않았다. 정말 간단하게 줄인다면, 다음과 같다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;1. 리퀘스트 메시지를 파싱하여 액세스 제어에 따른 검증 절차 진행&lt;br /&gt;2. 리퀘스트 메시지 처리&lt;br /&gt;3. 응답 메시지 생성 후 클라이언트로 반송 (클라이언트가 받을 때는 다수의 패킷으로 받음)&lt;br /&gt;4. 클라이언트를 이를 받아 전기 신호 -&amp;gt; 디지털 데이터 -&amp;gt; 분할된 패킷 모으기 -&amp;gt; 메시지로 변환 -&amp;gt; 브라우저에게 전송&lt;br /&gt;5. 브라우저는 Content-Type 같은 헤더 값에 따라서 어떤 식으로 렌더링할지 정함&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;/p&gt;</description>
      <category>✏️/Network</category>
      <category>네트워크</category>
      <category>소켓</category>
      <category>프로토콜스택</category>
      <author>dolmeng2</author>
      <guid isPermaLink="true">https://cl8d.tistory.com/107</guid>
      <comments>https://cl8d.tistory.com/107#entry107comment</comments>
      <pubDate>Sat, 17 Jun 2023 01:03:09 +0900</pubDate>
    </item>
    <item>
      <title>[우테코 5기] 레벨 2 레벨 인터뷰 정리 및 감정 회고</title>
      <link>https://cl8d.tistory.com/105</link>
      <description>&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지난 레벨 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;저번에는 2일차에 인터뷰였어서 준비할 시간이 넉넉했는데, 이번에는 1일차에 걸린 바람에 준비할 시간도 별로 없었다 ㅎㅎㅎ&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1686893559883&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;레벨 로그 / 레벨 인터뷰&quot; data-og-description=&quot;직렬화와 역직렬화&quot; data-og-host=&quot;www.notion.so&quot; data-og-source-url=&quot;https://www.notion.so/cl8d/443f9e8df568404b802747b38f193bde?pvs=4&quot; data-og-url=&quot;https://www.notion.so/443f9e8df568404b802747b38f193bde&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/nXrVv/hyS0c1fBwO/rJjp7LwoWXbyItl503g6y0/img.jpg?width=2000&amp;amp;height=1335&amp;amp;face=0_0_2000_1335,https://scrap.kakaocdn.net/dn/brddiU/hyS0hVM2rn/8TjhsicgWOgoW4ZOsfgrlk/img.jpg?width=2000&amp;amp;height=1335&amp;amp;face=0_0_2000_1335&quot;&gt;&lt;a href=&quot;https://www.notion.so/cl8d/443f9e8df568404b802747b38f193bde?pvs=4&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.notion.so/cl8d/443f9e8df568404b802747b38f193bde?pvs=4&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/nXrVv/hyS0c1fBwO/rJjp7LwoWXbyItl503g6y0/img.jpg?width=2000&amp;amp;height=1335&amp;amp;face=0_0_2000_1335,https://scrap.kakaocdn.net/dn/brddiU/hyS0hVM2rn/8TjhsicgWOgoW4ZOsfgrlk/img.jpg?width=2000&amp;amp;height=1335&amp;amp;face=0_0_2000_1335');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;레벨 로그 / 레벨 인터뷰&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;직렬화와 역직렬화&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.notion.so&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;노션에 레벨 인터뷰를 준비하며 열심히 정리했었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레벨 2에서 배운 게 워낙 중구난방해서 그런가... 최대한 스프링에 대한 것만 공부하고 갔다.&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-06-16 오후 2.37.11.png&quot; data-origin-width=&quot;1300&quot; data-origin-height=&quot;938&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/6vynn/btsj8A3wRzU/JMm5b9onfMBpk4zDWd8k2K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6vynn/btsj8A3wRzU/JMm5b9onfMBpk4zDWd8k2K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6vynn/btsj8A3wRzU/JMm5b9onfMBpk4zDWd8k2K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F6vynn%2Fbtsj8A3wRzU%2FJMm5b9onfMBpk4zDWd8k2K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;504&quot; height=&quot;364&quot; data-filename=&quot;스크린샷 2023-06-16 오후 2.37.11.png&quot; data-origin-width=&quot;1300&quot; data-origin-height=&quot;938&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;8명이 나를 바라보고 있는 상황 + 인터뷰어의 눈을 마주치려고 하다 보니까 딱 앉았을 때 굉장히 떨렸다.&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-06-16 오후 2.54.08.png&quot; data-origin-width=&quot;490&quot; data-origin-height=&quot;254&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ellczv/btske6sRmk1/QoCW9iWYjVa7cqihv7ukC1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ellczv/btske6sRmk1/QoCW9iWYjVa7cqihv7ukC1/img.png&quot; data-alt=&quot;체크인 점수 ;ㅅ;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ellczv/btske6sRmk1/QoCW9iWYjVa7cqihv7ukC1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fellczv%2Fbtske6sRmk1%2FQoCW9iWYjVa7cqihv7ukC1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;382&quot; height=&quot;198&quot; data-filename=&quot;스크린샷 2023-06-16 오후 2.54.08.png&quot; data-origin-width=&quot;490&quot; data-origin-height=&quot;254&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;체크인 점수 ;ㅅ;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&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;레벨 1의 인터뷰와 다르게 이번에는 상대의 레벨 로그를 보지 못한 상태로 질문을 하다 보니, 최대한 인터뷰이가 대답한 내용에 대해서 꼬리질문을 하는 식으로 들어갔다. 또한, &lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;질문할 때 최대한 인터뷰이의 눈을 마주치려고 노력&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;/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: #333333;&quot;&gt;&lt;b&gt;인터뷰이들이 대답했던 내용을 바탕으로 나의 개념을 다시 한 번 정리할 수 있던 시간이 되었다&lt;/b&gt;&lt;/span&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;&amp;nbsp;&lt;/p&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;오전의 3타임 연속으로 옵저버를 하다 보니 집중력에도 살짝 한계가 왔었지만... 중간중간 대답하는 센스가 좋은 인터뷰들의 매너를 많이 배웠다. 특히 개발자에게는 단순 지식보다는 소프트 스킬이 중요하다는 걸 다시 한 번 깨닫게 되는 순간이었다.&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;그리고, 잘 모르는 주제가 나오더라도 최대한 자신이 대답할 수 있는 수준까지 인터뷰어에게 유도하는 게 중요하다는 것을 느꼈다...&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;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;후반부로 갈수록 덜 떨릴 줄 알았는데, 마지막이라고 생각하니까 너무 떨렸다... ㅎㅎㅎ&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;스크린샷 2023-06-16 오후 2.47.30.png&quot; data-origin-width=&quot;864&quot; data-origin-height=&quot;158&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bmvW9o/btskc9KF9ai/9eFYee08KlNh7H6HkVkEkk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bmvW9o/btskc9KF9ai/9eFYee08KlNh7H6HkVkEkk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bmvW9o/btskc9KF9ai/9eFYee08KlNh7H6HkVkEkk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbmvW9o%2Fbtskc9KF9ai%2F9eFYee08KlNh7H6HkVkEkk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;674&quot; height=&quot;123&quot; data-filename=&quot;스크린샷 2023-06-16 오후 2.47.30.png&quot; data-origin-width=&quot;864&quot; data-origin-height=&quot;158&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;덕분에 엄청 똘망똘망하게 쳐다본다는 피드백을 받았다. ㅋㅋㅋㅋ 좋다.&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;b&gt;프로젝트 경험이 많은 게 보인다고 말씀해주셔서 뿌듯했다&lt;/b&gt;. (생각해보면 1개밖에 없는데...  ) 앞으로도 이렇게만 공부하라고 말씀해 주셔서 너무... 좋았다   내가 잘못된 건 아니었구나...&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;이번 레벨 2 학습에서 목표가 뭐였냐&lt;/b&gt;'라는 게 있었는데, 나는 이전에 스프링을 공부한 적이 있어서 스프링 사용법보다는 객체지향적으로 코드를 작성하는 방법이나 CI / CD, 인증 / 인가 쪽을 더 공부했다고 말했다.&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;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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;  이번 레벨 학습 측면에서 좋은 점과 부족한 점은?&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;- 처음에 자신있게 말한 것처럼, 학습에 대한 깊이가 깊은 것이 느껴졌다. 이미 많이 공부를 한 상황이라, 이번 레벨에 대한 학습에서는 이미 충분해서 별로 부족한 부분이 느껴지지 않았다. 인증 방식에 대해서도 자신만의 생각을 명확히 가지고 있었다. 에러 상황에 대한 대처도 당황하지 않고 잘 설명하는 것이 멋졌다.&lt;br /&gt;&lt;br /&gt;- 처음에 자신있게 말한 것처럼, 학습에 대한 깊이가 깊은 것이 느껴졌다. 이미 많이 공부를 한 상황이라, 이번 레벨에 대한 학습에서는 이미 충분해서 별로 부족한 부분이 느껴지지 않았다. 인증 방식에 대해서도 자신만의 생각을 명확히 가지고 있었다. 에러 상황에 대한 대처도 당황하지 않고 잘 설명하는 것이 멋졌다.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;- 본인이 어떤 부분을 고민해야 할 지 확실히 생각해보고 공부한 것으로 느껴졌다.&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;blockquote data-ke-style=&quot;style2&quot;&gt;  인터뷰, 말하기 측면에서 좋은 점과 개선할 부분은?&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;[좋은 점]&lt;br /&gt;- 처음에 자신이 '스프링은 이미 알고 있어서, 객체지향에 대한 부분을 고민했다'라고 확실한 컨셉을 밝혀서, 자신이 잘 아는 쪽으로 질문을 이끈 것 같다. 질문자를 똘망똘망 잘 쳐다보는 모습이 보기 좋다.&lt;br /&gt;- 아이 컨택도 좋고, 어조도 좋은 것 같다.&amp;nbsp;&lt;br /&gt;- 어조가 자연스럽고 목소리에 힘이 있다. &lt;br /&gt;- 겪어보지 못한 문제에 대해서도 어떻게 할 것인지 고민해보고 대답하는 모습이 좋았음. &lt;br /&gt;- 이해하지 못한 질문은 다시 한번 확실히 물어본 후에 답하는 모습이 신뢰감있게 느껴진다.&lt;br /&gt;&lt;br /&gt;[개선할 부분]&lt;br /&gt;- 너무 완벽하게 설명하려고 배경을 설명하다가 답변을 잊어버린다. 식별자로 id를 사용한다고 먼저 말하고 그렇게 생각한 근거를 뒤에 이야기하는 것이 더 좋을 것 같다.&lt;br /&gt;- 개인적으로 손동작이 추가되어도 더 좋다고 생각한다.&lt;br /&gt;- 몸이 계속 흔들린다. 제스처를 책상 밑에서 해서인것 같은데, 확실하게 책상 위로 보이게 하는 편이 더 좋을 것 같다.&amp;nbsp;&lt;/blockquote&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;그리고 두괄식 표현도 꼭 지켜야겠다. 이건 말하기 스킬이 부족해서 그랬던 것 같은데, 앞으로 잘 늘려나갸야겠다...  &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;476&quot; data-origin-height=&quot;386&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/baEw6v/btskeEXHEcI/K1qJcDkAENCGRZNRPxO5C1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/baEw6v/btskeEXHEcI/K1qJcDkAENCGRZNRPxO5C1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/baEw6v/btskeEXHEcI/K1qJcDkAENCGRZNRPxO5C1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbaEw6v%2FbtskeEXHEcI%2FK1qJcDkAENCGRZNRPxO5C1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;311&quot; height=&quot;252&quot; data-origin-width=&quot;476&quot; data-origin-height=&quot;386&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정말 뿌듯하게 마무리한 인터뷰... 끝나자마자 크루들이랑 얘기 좀 하구, 집 가서 기획서 만들면서 쉬었다 ✌ &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;BE_져니.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/bup2xW/btskfeKZu6g/T1GHkNrVfNlgZuxjQ2zGs1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bup2xW/btskfeKZu6g/T1GHkNrVfNlgZuxjQ2zGs1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bup2xW/btskfeKZu6g/T1GHkNrVfNlgZuxjQ2zGs1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbup2xW%2FbtskfeKZu6g%2FT1GHkNrVfNlgZuxjQ2zGs1%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;BE_져니.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;/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;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;style3&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;음... 돌아봤을 때 레벨 2 기간 동안 꽤 많이 지쳤던 것 같다. 오히려 레벨 1 보다 덜 열심히 했던 것 같은데 왜 그렇게 지쳤던 걸까... 잘 모르겠다. 근데 이런 감정을 나만 느낀 게 아니라 다른 크루분들도 요즘 하기 싫다는 말을 정말 많이 하셨다.&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;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-06-16 오후 3.05.18.png&quot; data-origin-width=&quot;1134&quot; data-origin-height=&quot;280&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bs0x3P/btskespLgZf/AxyddtMTdy6QEWsBYK9gUK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bs0x3P/btskespLgZf/AxyddtMTdy6QEWsBYK9gUK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bs0x3P/btskespLgZf/AxyddtMTdy6QEWsBYK9gUK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbs0x3P%2FbtskespLgZf%2FAxyddtMTdy6QEWsBYK9gUK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;678&quot; height=&quot;167&quot; data-filename=&quot;스크린샷 2023-06-16 오후 3.05.18.png&quot; data-origin-width=&quot;1134&quot; data-origin-height=&quot;280&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;p data-ke-size=&quot;size16&quot;&gt;객체지향적인 설계에 대해서 계속 고민했던 시간들이 떠오르는 거 보니, 헛되게 보낸 것 같지는 않다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-06-16 오후 3.06.15.png&quot; data-origin-width=&quot;904&quot; data-origin-height=&quot;112&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mZvgw/btskeMOKXCU/DmXmHjOnSUiubU01qs6ON1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mZvgw/btskeMOKXCU/DmXmHjOnSUiubU01qs6ON1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mZvgw/btskeMOKXCU/DmXmHjOnSUiubU01qs6ON1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmZvgw%2FbtskeMOKXCU%2FDmXmHjOnSUiubU01qs6ON1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;747&quot; height=&quot;93&quot; data-filename=&quot;스크린샷 2023-06-16 오후 3.06.15.png&quot; data-origin-width=&quot;904&quot; data-origin-height=&quot;112&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;마지막 미션 회고 글 빼고는 거의 밀리지 않고 작성했다. 앞으로도 이렇게 해야겠다.&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;또... 글도 많이 썼다. 딱 20개 정도 쓴 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 내가 공부한 걸 public한 공간에 글을 작성하는 게 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 사람이 본다고 생각하니까 더 열심히 정리하기도 하고, 크루들이 글 잘 보고 있다고, 도움된다고 말해줄 때마다 엄청 뿌듯하기 때문이다. 레벨 1 때는 노션을 많이 썼었는데, 레벨 2 와서는 거의 안 썼다. 나만 보는 공간이라고 생각하니 열심히 안 쓰게 되고, 미루게 되는 게 크기 때문이다. 우테코 수료 쯤에는 글 200개 채우는 게 목표인데, 가능할까? ㅎㅎㅎ (이제 100개 씀) 모르겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;글 쓸 때 최대한 가득가득 내용을 담아서 쓰려고 하는데, 오히려 가독성이 떨어진다는 말도 들었어서 (너무 길어서 읽기 싫다는....?) 요건 좀 고민이다. 앞으로 좀 더 고민해봐야겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-06-16 오후 3.13.29.png&quot; data-origin-width=&quot;1290&quot; data-origin-height=&quot;510&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bRrCyl/btsj9B2opMT/n99Xi2k1B1BDT1UWT4M4a0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bRrCyl/btsj9B2opMT/n99Xi2k1B1BDT1UWT4M4a0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bRrCyl/btsj9B2opMT/n99Xi2k1B1BDT1UWT4M4a0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbRrCyl%2Fbtsj9B2opMT%2Fn99Xi2k1B1BDT1UWT4M4a0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;731&quot; height=&quot;289&quot; data-filename=&quot;스크린샷 2023-06-16 오후 3.13.29.png&quot; data-origin-width=&quot;1290&quot; data-origin-height=&quot;510&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-06-16 오후 3.13.46.png&quot; data-origin-width=&quot;1796&quot; data-origin-height=&quot;326&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tzm2o/btske6fqCIw/v8BDFzEwUy4nfFJ6WwPQD0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tzm2o/btske6fqCIw/v8BDFzEwUy4nfFJ6WwPQD0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tzm2o/btske6fqCIw/v8BDFzEwUy4nfFJ6WwPQD0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Ftzm2o%2Fbtske6fqCIw%2Fv8BDFzEwUy4nfFJ6WwPQD0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1796&quot; height=&quot;326&quot; data-filename=&quot;스크린샷 2023-06-16 오후 3.13.46.png&quot; data-origin-width=&quot;1796&quot; data-origin-height=&quot;326&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-06-16 오후 3.13.59.png&quot; data-origin-width=&quot;1456&quot; data-origin-height=&quot;306&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1Azf7/btskfsWyful/i0ztXkuec4Ie8iHzWSPtj1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1Azf7/btskfsWyful/i0ztXkuec4Ie8iHzWSPtj1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1Azf7/btskfsWyful/i0ztXkuec4Ie8iHzWSPtj1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1Azf7%2FbtskfsWyful%2Fi0ztXkuec4Ie8iHzWSPtj1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1456&quot; height=&quot;306&quot; data-filename=&quot;스크린샷 2023-06-16 오후 3.13.59.png&quot; data-origin-width=&quot;1456&quot; data-origin-height=&quot;306&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-06-16 오후 3.15.02.png&quot; data-origin-width=&quot;2004&quot; data-origin-height=&quot;488&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/biwNIt/btskdWSxToT/VFWYVR1ktSkE0OwHsAvvmk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/biwNIt/btskdWSxToT/VFWYVR1ktSkE0OwHsAvvmk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/biwNIt/btskdWSxToT/VFWYVR1ktSkE0OwHsAvvmk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbiwNIt%2FbtskdWSxToT%2FVFWYVR1ktSkE0OwHsAvvmk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2004&quot; height=&quot;488&quot; data-filename=&quot;스크린샷 2023-06-16 오후 3.15.02.png&quot; data-origin-width=&quot;2004&quot; data-origin-height=&quot;488&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;데일리조에서 나는 게임을 잘하는 이미지가 되어버렸다. + 소식...? 내가 왜 소식좌가 되었을까?...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우테코 하면서 나도 모르는 내 이미지가 만들어지는 것 같아서 신기하다. ㅋㅋㅋㅋ 게임 잘한다는 말 들어본 적 없었던 것 같은데 신기해  &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(tmi: 나는 게임하는 걸 그렇게 좋아하지 않는다. 그냥 심심하니까 하는 것일뿐...?)&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;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;다만 코드를 안 쓰니 할 것도 없고 심심해서 고민 중이다... mysql 공부 좀 하고...  &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;p data-ke-size=&quot;size16&quot;&gt;JPA나 좀 공부할까 고민 중이다. 어차피 레벨 3 시작하면 쉬고 싶어도 못 쉴 테니까 ^_^...&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;수료까지 파이팅&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;p data-ke-size=&quot;size16&quot;&gt; &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다들 파이팅!&lt;/p&gt;</description>
      <category>우아한테크코스/레벨 2</category>
      <category>레벨로그</category>
      <category>레벨인터뷰</category>
      <category>우테코</category>
      <category>우테코 5기</category>
      <author>dolmeng2</author>
      <guid isPermaLink="true">https://cl8d.tistory.com/105</guid>
      <comments>https://cl8d.tistory.com/105#entry105comment</comments>
      <pubDate>Fri, 16 Jun 2023 15:18:51 +0900</pubDate>
    </item>
    <item>
      <title>[우테코 5기] 장바구니 협업 미션 회고</title>
      <link>https://cl8d.tistory.com/104</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;라온, 쥬니, 세인, 쵸파랑 진행했던 레벨 2 마지막 미션, 장바구니 협업 미션이다!&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;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-06-15 오후 4.58.44.png&quot; data-origin-width=&quot;1334&quot; data-origin-height=&quot;474&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3XACL/btsj6P5HWX7/7cVOBoTeuyZ4u2TytI4ne0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3XACL/btsj6P5HWX7/7cVOBoTeuyZ4u2TytI4ne0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3XACL/btsj6P5HWX7/7cVOBoTeuyZ4u2TytI4ne0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3XACL%2Fbtsj6P5HWX7%2F7cVOBoTeuyZ4u2TytI4ne0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1334&quot; height=&quot;474&quot; data-filename=&quot;스크린샷 2023-06-15 오후 4.58.44.png&quot; data-origin-width=&quot;1334&quot; data-origin-height=&quot;474&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;style3&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;background-color: #ffffff; color: #5c5c5c; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;✔️ 작성한 코드&lt;/b&gt;&lt;/h4&gt;
&lt;figure id=&quot;og_1686816055665&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 - Cl8D/jwp-shopping-order: 우아한테크코스 레벨 2 장바구니 협업 미션 레파지토리&quot; data-og-description=&quot;우아한테크코스 레벨 2 장바구니 협업 미션 레파지토리. Contribute to Cl8D/jwp-shopping-order development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/cl8d/jwp-shopping-order&quot; data-og-url=&quot;https://github.com/Cl8D/jwp-shopping-order&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/hPNez/hyS0lwucMX/vMDZQLICeNrbMBCnvDDm3k/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/cl8d/jwp-shopping-order&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/cl8d/jwp-shopping-order&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/hPNez/hyS0lwucMX/vMDZQLICeNrbMBCnvDDm3k/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 - Cl8D/jwp-shopping-order: 우아한테크코스 레벨 2 장바구니 협업 미션 레파지토리&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;우아한테크코스 레벨 2 장바구니 협업 미션 레파지토리. Contribute to Cl8D/jwp-shopping-order 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;h4 style=&quot;background-color: #ffffff; color: #5c5c5c; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;background-color: #ffffff; color: #5c5c5c; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;✔️ PR&lt;/b&gt;&lt;/h4&gt;
&lt;figure id=&quot;og_1686816078200&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;[2단계 - 주문 기능 구현] 져니(이지원) 미션 제출합니다. by Cl8D &amp;middot; Pull Request #19 &amp;middot; woowacourse/jwp-shoppi&quot; data-og-description=&quot;안녕하세요, 찰리! 져니입니다   만나서 반가워요! ☃️ 벌써 레벨 2 마지막 미션이네요... 시간이 너무 빠른 것 같습니다 ㅎㅎ ㅠ 다음 주 월요일이 마지막 PR 요청인 거 보고 리뷰에 좀 더 집중&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/woowacourse/jwp-shopping-order/pull/19&quot; data-og-url=&quot;https://github.com/woowacourse/jwp-shopping-order/pull/19&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bvlqHu/hyS0bgkVsW/K14Shnhj6oVbX5CWMtknFk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/woowacourse/jwp-shopping-order/pull/19&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/woowacourse/jwp-shopping-order/pull/19&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bvlqHu/hyS0bgkVsW/K14Shnhj6oVbX5CWMtknFk/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;[2단계 - 주문 기능 구현] 져니(이지원) 미션 제출합니다. by Cl8D &amp;middot; Pull Request #19 &amp;middot; woowacourse/jwp-shoppi&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;안녕하세요, 찰리! 져니입니다   만나서 반가워요! ☃️ 벌써 레벨 2 마지막 미션이네요... 시간이 너무 빠른 것 같습니다 ㅎㅎ ㅠ 다음 주 월요일이 마지막 PR 요청인 거 보고 리뷰에 좀 더 집중&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 style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;마지막 코드 리뷰라는 게 너무 슬프다...  &lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot; 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;style3&quot; /&gt;
&lt;h4 style=&quot;background-color: #ffffff; color: #5c5c5c; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;background-color: #ffffff; color: #5c5c5c; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;✔️ 기능 요구사항&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- AWS로 배포하기 (서버 배포용 인스턴스 / DB용 인스턴스)&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;: 이때, DB용 인스턴스는 외부 IP로 공개되지 않는다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- 장바구니에 담은 상품을 주문할 수 있다.&lt;br /&gt;- 상품 주문 시 현실 세계의 쇼핑 서비스가 제공하는 재화 관련 요소를 최소 1가지 이상 추가한다.&lt;br /&gt;: 재화 관련 요소: 쿠폰, 포인트, 할인 등&lt;br /&gt;ex) 5만원 이상 주문 시 전체 금액에서 10% 할인이 된다.&lt;br /&gt;- 사용자 별로 주문 목록을 확인할 수 있다.&lt;br /&gt;- 특정 주문의 상세 정보를 확인할 수 있다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot; 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;style3&quot; /&gt;
&lt;h4 style=&quot;background-color: #ffffff; color: #5c5c5c; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;background-color: #ffffff; color: #5c5c5c; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;✔️ AWS 배포 및 협업 환경 구축하기&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;거의 다 블로그로 올린 내용이어서 따로 설명하지는 않고 넘어가고자 한다.&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;size18&quot;&gt;&lt;b&gt;1. HTTP -&amp;gt; HTTPS 적용하기 (SSL / Nginx로 리버스 프록시 적용하기)&lt;/b&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1686816554891&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Infra] AWS 배포 후 도메인 연결 및 HTTPS 적용, nginx로 리버스 프록시 적용하기&quot; data-og-description=&quot;  들어가기 전 무과금으로 HTTPS 적용 프로젝트를 진행해보았다. 정석대로라면 가비아 + Route53 + ACM or 가비아 + nginx로만 진행하면 좋았겠지만... 여러 제약사항으로 인해서 색다른 방법으로 도메&quot; data-og-host=&quot;cl8d.tistory.com&quot; data-og-source-url=&quot;https://cl8d.tistory.com/97&quot; data-og-url=&quot;https://cl8d.tistory.com/97&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/let8k/hyS0ec5QxO/S94QhuHq9eLSk7T0mOoIHK/img.png?width=800&amp;amp;height=320&amp;amp;face=0_0_800_320,https://scrap.kakaocdn.net/dn/biHEvn/hyS0gPvwX7/125EDWcw2tq1ymg29e92Lk/img.png?width=800&amp;amp;height=320&amp;amp;face=0_0_800_320,https://scrap.kakaocdn.net/dn/c8N4sd/hyS0m92xvd/XW2MXpT8dv77MPI9dmbGqk/img.png?width=3022&amp;amp;height=646&amp;amp;face=0_0_3022_646&quot;&gt;&lt;a href=&quot;https://cl8d.tistory.com/97&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://cl8d.tistory.com/97&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/let8k/hyS0ec5QxO/S94QhuHq9eLSk7T0mOoIHK/img.png?width=800&amp;amp;height=320&amp;amp;face=0_0_800_320,https://scrap.kakaocdn.net/dn/biHEvn/hyS0gPvwX7/125EDWcw2tq1ymg29e92Lk/img.png?width=800&amp;amp;height=320&amp;amp;face=0_0_800_320,https://scrap.kakaocdn.net/dn/c8N4sd/hyS0m92xvd/XW2MXpT8dv77MPI9dmbGqk/img.png?width=3022&amp;amp;height=646&amp;amp;face=0_0_3022_646');&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;[Infra] AWS 배포 후 도메인 연결 및 HTTPS 적용, nginx로 리버스 프록시 적용하기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;  들어가기 전 무과금으로 HTTPS 적용 프로젝트를 진행해보았다. 정석대로라면 가비아 + Route53 + ACM or 가비아 + nginx로만 진행하면 좋았겠지만... 여러 제약사항으로 인해서 색다른 방법으로 도메&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;cl8d.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;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;2. 젠킨스로 CI / CD 구축하기&lt;/b&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1686816578523&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Jenkins] AWS 인스턴스를 젠킨스로 배포해보기 - 1편&quot; data-og-description=&quot;  들어가기 전 이번 미션에서 젠킨스를 통해 CI / CD를 구축해보고 싶어서 ⭐️베베 선생님⭐️의 힘을 빌려서 한 번 진행해보았다. 나는 t4g.micro를 사용하다 보니 램이 1기가밖에 안 되어서 swap&quot; data-og-host=&quot;cl8d.tistory.com&quot; data-og-source-url=&quot;https://cl8d.tistory.com/95&quot; data-og-url=&quot;https://cl8d.tistory.com/95&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/zmtIR/hyS0jeotjR/HdZQqa0xnZVuYcKaZTwVP0/img.png?width=526&amp;amp;height=84&amp;amp;face=0_0_526_84,https://scrap.kakaocdn.net/dn/cHe63j/hyS0lDgtUr/i9sPXlHuPduh0ht4ZmHnkK/img.png?width=526&amp;amp;height=84&amp;amp;face=0_0_526_84,https://scrap.kakaocdn.net/dn/VWV2s/hySZ8YeDfm/HzRJTta8rqgQ0tk2ClZGGk/img.jpg?width=764&amp;amp;height=511&amp;amp;face=0_0_764_511&quot;&gt;&lt;a href=&quot;https://cl8d.tistory.com/95&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://cl8d.tistory.com/95&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/zmtIR/hyS0jeotjR/HdZQqa0xnZVuYcKaZTwVP0/img.png?width=526&amp;amp;height=84&amp;amp;face=0_0_526_84,https://scrap.kakaocdn.net/dn/cHe63j/hyS0lDgtUr/i9sPXlHuPduh0ht4ZmHnkK/img.png?width=526&amp;amp;height=84&amp;amp;face=0_0_526_84,https://scrap.kakaocdn.net/dn/VWV2s/hySZ8YeDfm/HzRJTta8rqgQ0tk2ClZGGk/img.jpg?width=764&amp;amp;height=511&amp;amp;face=0_0_764_511');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[Jenkins] AWS 인스턴스를 젠킨스로 배포해보기 - 1편&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;  들어가기 전 이번 미션에서 젠킨스를 통해 CI / CD를 구축해보고 싶어서 ⭐️베베 선생님⭐️의 힘을 빌려서 한 번 진행해보았다. 나는 t4g.micro를 사용하다 보니 램이 1기가밖에 안 되어서 swap&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;cl8d.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1686816588128&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Jenkins] AWS 인스턴스를 젠킨스로 배포해보기 - 2편&quot; data-og-description=&quot;  들어가기 전 지난 포스팅은 젠킨스를 설치하고 필요한 플러그인을 설치하는 과정까지 진행하였다.   레파지토리 WebHook 등록하기 우리는 깃허브 레파지토리의 특정 브랜치에 push 이벤트가 &quot; data-og-host=&quot;cl8d.tistory.com&quot; data-og-source-url=&quot;https://cl8d.tistory.com/93&quot; data-og-url=&quot;https://cl8d.tistory.com/93&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/ciaPdQ/hyS0lcaN0Q/LEdIG2mxgrIyjc0XtNcKb1/img.png?width=800&amp;amp;height=335&amp;amp;face=0_0_800_335,https://scrap.kakaocdn.net/dn/cdLsmB/hyS0jFsTcu/aeQkZqDnX1kQdcNuB6VId1/img.png?width=800&amp;amp;height=335&amp;amp;face=0_0_800_335,https://scrap.kakaocdn.net/dn/cnGAQi/hyS0fC2PCg/IDqeT7IKD2rvBDumuTQwik/img.png?width=2366&amp;amp;height=992&amp;amp;face=0_0_2366_992&quot;&gt;&lt;a href=&quot;https://cl8d.tistory.com/93&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://cl8d.tistory.com/93&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/ciaPdQ/hyS0lcaN0Q/LEdIG2mxgrIyjc0XtNcKb1/img.png?width=800&amp;amp;height=335&amp;amp;face=0_0_800_335,https://scrap.kakaocdn.net/dn/cdLsmB/hyS0jFsTcu/aeQkZqDnX1kQdcNuB6VId1/img.png?width=800&amp;amp;height=335&amp;amp;face=0_0_800_335,https://scrap.kakaocdn.net/dn/cnGAQi/hyS0fC2PCg/IDqeT7IKD2rvBDumuTQwik/img.png?width=2366&amp;amp;height=992&amp;amp;face=0_0_2366_992');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[Jenkins] AWS 인스턴스를 젠킨스로 배포해보기 - 2편&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;  들어가기 전 지난 포스팅은 젠킨스를 설치하고 필요한 플러그인을 설치하는 과정까지 진행하였다.   레파지토리 WebHook 등록하기 우리는 깃허브 레파지토리의 특정 브랜치에 push 이벤트가&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;cl8d.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;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;3. Logback으로 에러를 파일로 로깅하기&lt;/b&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1686816645908&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Spring] 에러 로깅하기 - Logback을 사용해서 ERROR 레벨만 파일로 로그를 남겨보자!&quot; data-og-description=&quot;  들어가기 전 이번 장바구니 미션에서는 프론트 크루들과 협업을 해야 했기 때문에 앞으로 에러 로그를 볼 일이 많아질 것 같다고 생각했다. 배포 스크립트를 작성하면서 스프링이 띄워질 때&quot; data-og-host=&quot;cl8d.tistory.com&quot; data-og-source-url=&quot;https://cl8d.tistory.com/96&quot; data-og-url=&quot;https://cl8d.tistory.com/96&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/eLFCg/hySZ84YWui/4ISdfbHUeqKqJI0qziEyoK/img.png?width=800&amp;amp;height=263&amp;amp;face=0_0_800_263,https://scrap.kakaocdn.net/dn/TWEkz/hyS0fpvHsz/q3KFTLzxymzkKpzjDkDhA1/img.png?width=800&amp;amp;height=263&amp;amp;face=0_0_800_263,https://scrap.kakaocdn.net/dn/dlMyw8/hyS0kqPTgI/77qdOqrjKwKhwNVwS99yn0/img.png?width=2570&amp;amp;height=964&amp;amp;face=0_0_2570_964&quot;&gt;&lt;a href=&quot;https://cl8d.tistory.com/96&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://cl8d.tistory.com/96&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/eLFCg/hySZ84YWui/4ISdfbHUeqKqJI0qziEyoK/img.png?width=800&amp;amp;height=263&amp;amp;face=0_0_800_263,https://scrap.kakaocdn.net/dn/TWEkz/hyS0fpvHsz/q3KFTLzxymzkKpzjDkDhA1/img.png?width=800&amp;amp;height=263&amp;amp;face=0_0_800_263,https://scrap.kakaocdn.net/dn/dlMyw8/hyS0kqPTgI/77qdOqrjKwKhwNVwS99yn0/img.png?width=2570&amp;amp;height=964&amp;amp;face=0_0_2570_964');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[Spring] 에러 로깅하기 - Logback을 사용해서 ERROR 레벨만 파일로 로그를 남겨보자!&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;  들어가기 전 이번 장바구니 미션에서는 프론트 크루들과 협업을 해야 했기 때문에 앞으로 에러 로그를 볼 일이 많아질 것 같다고 생각했다. 배포 스크립트를 작성하면서 스프링이 띄워질 때&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;cl8d.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;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;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&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;p data-ke-size=&quot;size16&quot;&gt;우리 팀은 '쿠폰'이라는 도메인을 선택하였고,&lt;span style=&quot;color: #ef5369;&quot;&gt; 처음으로 가입했을 때와 처음으로 상품을 주문했을 때&lt;/span&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-06-15 오후 6.26.52.png&quot; data-origin-width=&quot;2060&quot; data-origin-height=&quot;1320&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cwdsx9/btsj5Rjc19A/CSzGw1UKATywEDDuhCrRc0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cwdsx9/btsj5Rjc19A/CSzGw1UKATywEDDuhCrRc0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cwdsx9/btsj5Rjc19A/CSzGw1UKATywEDDuhCrRc0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcwdsx9%2Fbtsj5Rjc19A%2FCSzGw1UKATywEDDuhCrRc0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;828&quot; height=&quot;531&quot; data-filename=&quot;스크린샷 2023-06-15 오후 6.26.52.png&quot; data-origin-width=&quot;2060&quot; data-origin-height=&quot;1320&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;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;환불 정책의 경우&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;지하철 미션에서 진행했던 정책 관리 클래스의 방법&lt;/b&gt;을 그대로 따랐다. 마음에 든다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;- 한 가지 아쉬운 건, 주문 도메인이 장바구니 아이템 도메인 대신에 별도의 주문 상품 도메인을 가지고 있던 게 더 어울리지 않았을까 싶다. 주문이 완료된 이후에 주문에 대해 조회를 해올 때 장바구니 아이템 도메인을 가지고 있다 보니 논리적으로 어색하다는 느낌이 들었기 때문이다...   물론 리뷰어님은 별다른 피드백을 주시지 않았지만, 개인적으로 다음에 구현한다면 분리할 포인트라고 느껴졌다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&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;  도메인과 ID&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한,&lt;span style=&quot;color: #ef5369;&quot;&gt; 도메인이 id를 가지고 있도록 만들었는데&lt;/span&gt;, 사실 처음에는 이 부분을 분리했다가 나중에 피드백을 받고 수정했다.&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-06-15 오후 10.38.58.png&quot; data-origin-width=&quot;984&quot; data-origin-height=&quot;292&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/UYfJY/btsj8hnIHHO/kLSwMFkMHJADqHWLbRCa10/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/UYfJY/btsj8hnIHHO/kLSwMFkMHJADqHWLbRCa10/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/UYfJY/btsj8hnIHHO/kLSwMFkMHJADqHWLbRCa10/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUYfJY%2Fbtsj8hnIHHO%2FkLSwMFkMHJADqHWLbRCa10%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;611&quot; height=&quot;181&quot; data-filename=&quot;스크린샷 2023-06-15 오후 10.38.58.png&quot; data-origin-width=&quot;984&quot; data-origin-height=&quot;292&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;JPA를 쓰다 보면 어차피 일어날 수밖에 없는 것이고, 오히려 분리함으로서 복잡도가 증가한 것이 사실이니까...&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;size18&quot;&gt;&amp;nbsp;&lt;/p&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;또 한 가지 특이한 점은 인터페이스로 풀어나간 점인데, 우선 사용자의 비밀번호부터 보자.&lt;/p&gt;
&lt;pre id=&quot;code_1686821805952&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface MemberPassword {

    String getPassword();
}&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 data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는, 사용자가 회원가입 시 입력하는 비밀번호에 대한 제약 조건에 의해 생성자에서 객체를 만들 때 검증하게 되는데, DB에서 조회해온 값 (암호화 적용)으로 도메인 객체를 만들게 되면 &lt;span style=&quot;color: #ef5369;&quot;&gt;제약 조건이 필요없으니, 별도의 메서드를 만드는 게 꽤나 번거롭다&lt;/span&gt;고 느껴졌기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한, 암호화가 되어 있는 비밀번호와 암호화가 되지 않은 비밀번호가 &lt;b&gt;하나의 객체로서 표현되는 게&lt;/b&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt; 2개의 책임&lt;/span&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;/p&gt;
&lt;pre id=&quot;code_1686821978895&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class NaturalPassword implements MemberPassword {

    private final String password;

    ...

    public static NaturalPassword create(final String password) {
        validatePassword(password);
        return new NaturalPassword(password);
    }

    private static void validatePassword(final String password) {
        if (password.length() &amp;lt; PASSWORD_MIN_LENGTH || password.length() &amp;gt; PASSWORD_MAX_LENGTH) {
            throw new BadRequestException(MEMBER_PASSWORD_LENGTH);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이거는 사용자의 입력으로 들어온 비밀번호 객체이다. validatePassword 메서드를 통해 길이를 검증하였다.&lt;/p&gt;
&lt;pre id=&quot;code_1686822014405&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class EncryptedPassword implements MemberPassword {

    private final String password;

    ...
    
    public static EncryptedPassword create(final String password) {
        return new EncryptedPassword(password);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면에, 암호화된 비밀번호를 담는 객체이다. 별도의 검증 없이 바로 객체를 생성하도록 만들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로, &lt;span style=&quot;color: #ef5369;&quot;&gt;비밀번호 암호화의 경우 SHA256을 통해서 진행&lt;/span&gt;하였다. (현재 우리 구조상 원본 비밀번호를 알 이유가 없기 때문에 복호화가 안 되는 알고리즘을 채택하였다.)&lt;/p&gt;
&lt;pre id=&quot;code_1686822113850&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public static String encrypt(final String target) {
    try {
        final MessageDigest md = MessageDigest.getInstance(&quot;SHA-256&quot;);
        md.update(target.getBytes());
        return bytesToHex(md.digest());
    } catch (NoSuchAlgorithmException ignored) {
        throw new RuntimeException(&quot;암호화 중 오류가 발생하였습니다.&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가적으로 주문 역시 인터페이스로 구현하였는데, 사용자의 비밀번호와 같은 이유로 쿠폰을 사용한 주문과, 쿠폰을 사용하지 않은 주문으로 분리하였다.&lt;/p&gt;
&lt;pre id=&quot;code_1686822279190&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class CouponOrder implements Order {

    ...
    private final Coupon coupon;

    // 총 주문 금액을 계산하는 메서드
    private BigDecimal calculateTotalOrderPrice() {
        return OrderPriceCalculator.calculateTotalOrderPrice(cartItems);
    }
	
    // 쿠폰에 의해서 할인된 금액을 계산하는 메서드
    private BigDecimal calculateDiscountPrice() {
        final int discountRate = coupon.discountRate();
        final BigDecimal convertedDiscountRate = BigDecimal.valueOf(
            (PERCENTAGE - discountRate) * DECIMAL_CONVERSION);
        return totalPrice.multiply(convertedDiscountRate).setScale(0, RoundingMode.DOWN);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿠폰을 사용한 주문은 쿠폰 도메인에 대한 의존 관계를 추가적으로 가지고 있으며, &lt;span style=&quot;color: #ef5369;&quot;&gt;쿠폰에 의해 할인된 금액도 함께 관리&lt;/span&gt;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만, 주문에 대한 필드가 늘어나면서 인터페이스의 getter가 엄청 많아졌다는 단점은 있다.&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;공통되는 필드를 관리하는 OrderDetail 같은 객체를 만들어서 관리했으면 더 좋았을 것 같다&lt;/b&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;style3&quot; /&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&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;b&gt;예외에 관한 클래스에 어떤 정보까지 가지고 있어야 하는가?&lt;/b&gt;'&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-06-15 오후 10.50.22.png&quot; data-origin-width=&quot;1454&quot; data-origin-height=&quot;368&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eswPf6/btsj5UG9dkk/ukF27ckPMvHwbogvJ5n8R0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eswPf6/btsj5UG9dkk/ukF27ckPMvHwbogvJ5n8R0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eswPf6/btsj5UG9dkk/ukF27ckPMvHwbogvJ5n8R0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeswPf6%2Fbtsj5UG9dkk%2FukF27ckPMvHwbogvJ5n8R0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1454&quot; height=&quot;368&quot; data-filename=&quot;스크린샷 2023-06-15 오후 10.50.22.png&quot; data-origin-width=&quot;1454&quot; data-origin-height=&quot;368&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;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;스크린샷 2023-06-15 오후 10.56.05.png&quot; data-origin-width=&quot;726&quot; data-origin-height=&quot;160&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/VLWJ1/btsj5wz3FCN/pVkTzOtHlc9jklNEwX5NWk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/VLWJ1/btsj5wz3FCN/pVkTzOtHlc9jklNEwX5NWk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/VLWJ1/btsj5wz3FCN/pVkTzOtHlc9jklNEwX5NWk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVLWJ1%2Fbtsj5wz3FCN%2FpVkTzOtHlc9jklNEwX5NWk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;490&quot; height=&quot;108&quot; data-filename=&quot;스크린샷 2023-06-15 오후 10.56.05.png&quot; data-origin-width=&quot;726&quot; data-origin-height=&quot;160&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 상황이 주어졌을 때 &lt;b&gt;사용자에게 '상품 ID 1번이 존재하지 않습니다' 같은 메시지는 적합하지 않다&lt;/b&gt;고 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예전에 개발했을 때는 클라이언트에게 에러 코드 모음을 내려주는 전용 api가 있어서, 해당 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;span style=&quot;color: #ef5369;&quot;&gt;예외 클래스에서는 상태 코드를 함께 관리&lt;/span&gt;하도록 만들었다.&lt;/p&gt;
&lt;pre id=&quot;code_1686839275895&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class ForbiddenException extends RuntimeException {

    private static final HttpStatus httpStatus = HttpStatus.FORBIDDEN;
    private final ErrorCode errorCode;
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;덕분에 ExceptionHandler에서의 코드가 줄어서 마음에 든다. 지금까지는 왜 이런 생각을 못 했을까...!  &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞으로도 이런 식으로 작성하지 않을까 싶다. (errorCode는 협의하는 거에 따라 달라질 것 같다!)&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;style3&quot; /&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&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;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;style3&quot;&gt;&amp;nbsp;1. 쿠폰 발행의 경우 다양한 서비스에서 활용할 수 있는 비즈니스 로직이다.&amp;nbsp;&lt;br /&gt;다른 서비스에서 사용할 때마다 쿠폰에 대한 의존 관계가 뻗어나가는 것보다, &lt;span style=&quot;color: #ef5369;&quot;&gt;이벤트 방식을 통해 의존성을 역전&lt;/span&gt;시키는 게 더 깔끔할 것 같다고 생각했다.&lt;br /&gt;&lt;br /&gt;2. 회원가입과 쿠폰 발행이 하나의 트랜잭션으로 묶여있을 때, 쿠폰 발행 중에 예외가 발생했다면 회원가입 로직까지 롤백이 될 것이다. 사용자의 입장에서 회원가입은 굉장히 번거로운 작업이고, 사용자가 가입하는 시점에서는 쿠폰이 발행된다는 것을 알지 못하기 때문에 &lt;span style=&quot;color: #ef5369;&quot;&gt;쿠폰 때문에 회원가입 자체가 실패해버리면 다시 가입하지 않을 것&lt;/span&gt;이라는 생각이 들었다. 이를 위해 가입 로직과 쿠폰 발행 로직에 대한 트랜잭션을 분리할 필요성을 느꼈다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서, 아래와 같이 회원 가입 시 쿠폰 발행을 진행하도록 이벤트를 발행하였다.&lt;/p&gt;
&lt;pre id=&quot;code_1686840487671&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Transactional
public long save(final MemberJoinRequest memberJoinRequest) {
    if (memberRepository.existByName(memberJoinRequest.getName())) {
        throw new BadRequestException(ErrorCode.MEMBER_DUPLICATE_NAME);
    }
    final Member member = convertMember(memberJoinRequest);
    final long savedMemberId = memberRepository.insert(member);
    applicationEventPublisher.publishEvent(new JoinMemberCouponEvent(savedMemberId));
    return savedMemberId;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고, 쿠폰 저장 로직은 아래와 같이 진행하였다.&lt;/p&gt;
&lt;pre id=&quot;code_1686841666512&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@TransactionalEventListener
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveJoinMemberCoupon(final JoinMemberCouponEvent joinMemberCouponEvent) {
    final Long memberId = joinMemberCouponEvent.getMemberId();
    final Coupon coupon = couponRepository.findByNameAndDiscountRate(JOIN_MEMBER_COUPON.getName(),
        JOIN_MEMBER_COUPON.getDiscountRate());

    final LocalDateTime issuedAt = LocalDateTime.now();
    validateAlreadyIssued(memberId, coupon.couponId());
    final MemberCoupon memberCoupon = convertMemberCoupon(coupon, issuedAt);
    memberCouponRepository.save(memberId, memberCoupon);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로직 자체는 쿠폰을 조회해서 이미 발생했는지 확인하고 저장한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;  @TransactionalEventListener란?&lt;/b&gt;&lt;br /&gt;- phase 옵션을 통해서 &lt;span style=&quot;color: #ef5369;&quot;&gt;트랜잭션에 따른&lt;/span&gt; 이벤트 처리 방법을 지원한다.&amp;nbsp;&lt;br /&gt;기본적으로는 'AFTER_COMMIT'으로 설정되어 있기 때문에&amp;nbsp;&lt;span style=&quot;color: #ef5369;&quot;&gt;트랜잭션 커밋 시 이벤트를 실행&lt;/span&gt;한다.&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;span style=&quot;color: #ef5369;&quot;&gt;@Transactional(propagation = Propagation.REQUIRES_NEW)&lt;/span&gt;라는 옵션이 눈에 띌 것이다.&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-06-16 오전 12.10.24.png&quot; data-origin-width=&quot;1428&quot; data-origin-height=&quot;160&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bAsyWN/btsj5RYcWFJ/5enWWOi054akjythReYG10/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bAsyWN/btsj5RYcWFJ/5enWWOi054akjythReYG10/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bAsyWN/btsj5RYcWFJ/5enWWOi054akjythReYG10/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbAsyWN%2Fbtsj5RYcWFJ%2F5enWWOi054akjythReYG10%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1428&quot; height=&quot;160&quot; data-filename=&quot;스크린샷 2023-06-16 오전 12.10.24.png&quot; data-origin-width=&quot;1428&quot; data-origin-height=&quot;160&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 그럴까 고민을 했었는데, 나는 아래와 같이 이해했다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;1. JdbcTemplate의 경우 @transactional 어노테이션이 없어도 기본적으로 jdbcTemplate.update나 query() 메서드를 사용하는 시점에서 DB에 쿼리가 날라간다. (JPA와 다른 점)&lt;br /&gt;&lt;br /&gt;2. &lt;span style=&quot;color: #ef5369;&quot;&gt;예외가 발생했을 때 롤백이 일어나기 위해서는 @transactional을 통해서 묶어줘야&lt;/span&gt; 한다. 아니면 JdbcTemplate에서 이미 save를 진행했기 때문에 이에 대해 롤백할 수 없다. &lt;br /&gt;&lt;br /&gt;3. 기본적으로 @TransactionalEventListener는 시작된 트랜잭션 commit 후에 동작한다. &lt;br /&gt;&lt;br /&gt;4. 하지만, 단순히 @transactional을 붙이면 기존 회원 가입에서 걸려있던 트랜잭션에 참여하게 된다. &lt;span style=&quot;color: #ef5369;&quot;&gt;그러나, 커밋된 트랜잭션에 대해서는 다시 커밋이 불가능하기 때문에 롤백이든, 다른 연산을 수행하더라도 무의미&lt;/span&gt;하다. &lt;br /&gt;-&amp;gt; 그래서 만약 JPA 같이 삽입, 수정, 삭제 연산에서 @Transcational이 필요한 경우였다면 애초에 쿼리 자체가 날라가지 않았을 것이다. (물론 나의 가정이다)&lt;br /&gt;&lt;br /&gt;5. ⭐️ 하지만, 지금은 j&lt;span style=&quot;color: #ef5369;&quot;&gt;dbcTemplate을 사용했기 때문에 쿼리는 날라간다. 하지만, 예외 발생 시 롤백이 불가능&lt;/span&gt;하다. &lt;br /&gt;&lt;br /&gt;6. 이를 해결하기 위해 &lt;span style=&quot;color: #ef5369;&quot;&gt;기존의 트랜잭션과 다른 트랜잭션으로 분기시켜서 예외 발생 시 롤백이 되도록&lt;/span&gt; 만든다!&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-06-16 오전 12.39.19.png&quot; data-origin-width=&quot;1648&quot; data-origin-height=&quot;642&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dtptuF/btsj7KX06vH/FOnG8IB8IxTJcKfZV41EbK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dtptuF/btsj7KX06vH/FOnG8IB8IxTJcKfZV41EbK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dtptuF/btsj7KX06vH/FOnG8IB8IxTJcKfZV41EbK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdtptuF%2Fbtsj7KX06vH%2FFOnG8IB8IxTJcKfZV41EbK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;747&quot; height=&quot;291&quot; data-filename=&quot;스크린샷 2023-06-16 오전 12.39.19.png&quot; data-origin-width=&quot;1648&quot; data-origin-height=&quot;642&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원래는 이미 커밋된 트랜잭션에 참여하기 때문에 수정이든 삭제든 불가능하지만 (JPA 같이 영속성 컨텍스트를 통해서 flush가 발생하는 경우), jdbcTemplate의 경우&lt;b&gt; 트랜잭션 어노테이션에 상관없이 쿼리를 execute 하는 순간 DB에 쿼리가 날라가기 때문에&lt;/b&gt; @Transactional 어노테이션이 없어도 저장 자체는 잘 되었다.&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;span style=&quot;color: #ef5369;&quot;&gt;트랜잭션 전파 속성&lt;/span&gt;을 활용하여 별도의 트랜잭션으로 분기해주었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션은 기본적으로 전파 속성이 'REQUIRED'로 설정되어 있기 때문에, 기존 트랜잭션이 있으면 해당 트랜잭션에 참여한다. (없으면 새로 생성) 반면에, REQUIRES_NEW를 사용하게 되면&lt;span style=&quot;color: #ef5369;&quot;&gt; 항상 새로운 트랜잭션에서 실행&lt;/span&gt;된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-06-16 오전 12.40.31.png&quot; data-origin-width=&quot;1660&quot; data-origin-height=&quot;660&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vykyB/btsj6NU3VZT/tFm5P3LvxLyIo0mnwEIxPk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vykyB/btsj6NU3VZT/tFm5P3LvxLyIo0mnwEIxPk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vykyB/btsj6NU3VZT/tFm5P3LvxLyIo0mnwEIxPk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvykyB%2Fbtsj6NU3VZT%2FtFm5P3LvxLyIo0mnwEIxPk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;792&quot; height=&quot;315&quot; data-filename=&quot;스크린샷 2023-06-16 오전 12.40.31.png&quot; data-origin-width=&quot;1660&quot; data-origin-height=&quot;660&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 트랜잭션이 분리되었기 때문에 B의 롤백이 A에는 영향을 끼치지 않게 되었으며, 덕분에 쿠폰 발행 도중 예외가 발생하더라도 회원가입 로직과 관계 없이 잘 롤백되는 것을 확인할 수 있었다.&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;pre id=&quot;code_1686844254183&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
@DisplayName(&quot;회원 가입 축하 쿠폰 발행 시 이미 발행이 완료되었다면 예외가 발생한다.&quot;)
void saveJoinMemberCoupon_already_issued() {
    // given
    final String 져니_이름 = &quot;journey2&quot;;
    final Long 저장된_져니_아이디 = 사용자를_저장한다(져니_이름);
    final JoinMemberCouponEvent 회원_가입_쿠폰_발행_이벤트 = new JoinMemberCouponEvent(저장된_져니_아이디);
    memberCouponService.saveJoinMemberCoupon(회원_가입_쿠폰_발행_이벤트);

    // then
    assertThatThrownBy(() -&amp;gt; memberCouponService.saveJoinMemberCoupon(회원_가입_쿠폰_발행_이벤트))
        .isInstanceOf(BadRequestException.class)
        .extracting(&quot;errorCode&quot;)
        .isEqualTo(ErrorCode.COUPON_ALREADY_EXIST);

    // then
    final List&amp;lt;MemberCouponDto&amp;gt; 져니_쿠폰들 = memberCouponDao.findMyCouponsByName(져니_이름);
    assertThat(져니_쿠폰들)
        .hasSize(1);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예외 발생 후 쿠폰을 조회했을 때 이미 발행된 1장의 쿠폰만 존재하는 것을 확인할 수 있다.&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;style3&quot; /&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;사실 이거는 페어 프로그래밍으로 진행한 건 아니고, 내가 따로 만들고 싶어서 만든 api이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;주문한 지 3일 이내라면 전액, 7일 이내라면 반액을 반환하도록 만들었다. &lt;span style=&quot;color: #333333;&quot;&gt;물론 그 이상은 환불 불가능하다&lt;/span&gt;.&lt;/span&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;pre id=&quot;code_1686845494751&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Before
public RefundPolicyComposite refundPolicyComposite(
    final RefundPolicy fullRefundPolicy, final RefundPolicy halfRefundPolicy) {
    return new RefundPolicyComposite(
        List.of(fullRefundPolicy, halfRefundPolicy)
    );
}

// After
public RefundPolicyComposite refundPolicyComposite(final List&amp;lt;RefundPolicy&amp;gt; refundPolicies) {
    return new RefundPolicyComposite(refundPolicies);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정책 관련 클래스를 빈으로 주입할 때 기존에는 RefundPolicy에 대해서 하나씩 집어넣었는데 &lt;span style=&quot;color: #ef5369;&quot;&gt;매번 Config를 수정해야 한다는 단점이 존재해서 아래와 같이 List를 통해서 주입받도록 변경&lt;/span&gt;하였다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;688&quot; data-origin-height=&quot;354&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/baWdWp/btsj8X3FARD/8D6Z60ABjnSO3GcJGdKUg0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/baWdWp/btsj8X3FARD/8D6Z60ABjnSO3GcJGdKUg0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/baWdWp/btsj8X3FARD/8D6Z60ABjnSO3GcJGdKUg0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbaWdWp%2Fbtsj8X3FARD%2F8D6Z60ABjnSO3GcJGdKUg0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;476&quot; height=&quot;245&quot; data-origin-width=&quot;688&quot; data-origin-height=&quot;354&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;이렇게 하면 컴포넌트 스캔 순서대로 등록이 되어서 (사전순으로 보인다) Full, Half가 빈으로 주입되었다. 스캔 순서에 따른 빈에 대한 순서 보장이 확실하지 않기 때문에 &lt;b&gt;정책이 적용 가능한지 판단하는 메서드에 대해 경계값을 철저하게 검증하는 방향으로 변경&lt;/b&gt;하였다.&lt;/p&gt;
&lt;pre id=&quot;code_1686845796060&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Before - 50% 할인 시 3일 이상인지만 검증
@Override
public boolean isAvailable(final Order order, final LocalDateTime currentTime) {
    return currentTime.isBefore(order.getOrderedAt().plusDays(HALF_REFUND.getDay()));
}

// After - 3일 ~ 7일인지 검증
@Override
public boolean isAvailable(final Order order, final LocalDateTime currentTime) {
    final LocalDateTime orderedAt = order.getOrderedAt();
    return currentTime.isAfter(orderedAt.plusDays(FULL_REFUND.getDay())) &amp;amp;&amp;amp;
        currentTime.isBefore(orderedAt.plusDays(HALF_REFUND.getDay()));
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한, &lt;span style=&quot;color: #ef5369;&quot;&gt;금액의 경우 정밀한 계산을 해야 하기 때문에 BigDecimal을 사용하는 게 좋다&lt;/span&gt;고 들어서 요렇게 적용하였다.&lt;/p&gt;
&lt;pre id=&quot;code_1686845831361&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Override
public BigDecimal calculatePrice(final BigDecimal price) {
    final BigDecimal refundRate = BigDecimal.valueOf((PERCENTAGE - HALF_REFUND_RATE) * DECIMAL_CONVERSION);
    return price.multiply(refundRate).setScale(0, RoundingMode.DOWN);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;setScale(0, RoundingMode.DOWN)을 통해서 소수점 이하의 값을 버리도록 만들었다. (아니면 값이 더러워진다!)&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;style3&quot; /&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&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;이 부분도 합의된 API는 아니고, 개인 학습용으로 작성했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;항상 JPA로 만들었다가 쌩 쿼리로 작성해본 건 처음이었는데, 생각보다 어렵지 않았다.&lt;/p&gt;
&lt;pre id=&quot;code_1686844572813&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@GetMapping(&quot;/pages&quot;)
public ResponseEntity&amp;lt;ProductPageResponse&amp;gt; getProductsByPage(@RequestParam(&quot;page&quot;) final int page,
                                                             @RequestParam(&quot;size&quot;) final int size) {
    return ResponseEntity.ok(productService.getProductsByPage(page, size));
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상품 목록에 대한 페이징 API이다. 쿼리 파라미터로 &lt;span style=&quot;color: #ef5369;&quot;&gt;조회할 페이지 번호와 한 페이지에 몇 개씩 받을 것&lt;/span&gt;인지 입력받았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1686844995308&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class ProductPageResponse {

    private final long totalPage;
    private final List&amp;lt;ProductResponse&amp;gt; productResponse;
    ...
}&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;pre id=&quot;code_1686844622035&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;ProductEntity&amp;gt; getProductsByPage(final int page, final int size) {
    final int offset = calculateOffset(page, size);
    final String sql = &quot;SELECT id, name, image_url, price, is_deleted FROM product &quot;
        + &quot;ORDER BY id LIMIT ? OFFSET ?&quot;;
    return jdbcTemplate.query(sql, productEntityRowMapper, size, offset);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나름 핵심이 되는 페이지 처리 쿼리다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;⭐️ LIMIT의 경우 결과 중에서 몇 개만 조회할 것인지 설정하고, OFFSET은 어디서부터 가져올지&lt;/span&gt;를 나타낸다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;SELECT * FROM table_name LIMIT 10 OFFSET 20;&lt;br /&gt;= 20번째 (계산된 오프셋) 행부터 10개 (size) 조회 &lt;br /&gt;= 21번째 ~ 30번째 행까지 출력&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;여기서 offset을 계산하는 메서드를 호출했는데, 이는 다음과 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1686844664085&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private int calculateOffset(final int page, final int size) {
    if (page == 1) {
        return 0;
    }
    return (page - 1) * size;
}&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;클라이언트 개발자에게 1부터 값을 받는다고 가정하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1이라면 첫 번째 페이지니까 offset을 0을, 그 이상이라면 (page - 1) * size만큼을 설정해주었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 page = 3, size = 10이라는 값이 들어왔다면 2 * 10 = 20번째 행부터 10개를 가져오게 되는 것이다.&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;pre id=&quot;code_1686845088649&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private long calculateTotalPage(final int size, final long totalProductCount) {
    long totalPage = totalProductCount / size;

    if (totalProductCount % size &amp;gt; 0) {
        totalPage++;
    }
    return totalPage;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상품의 총 개수에서 한 페이지에 출력할 상품의 개수를 나눈 값을 총 페이지로 설정하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때, &lt;span style=&quot;color: #ef5369;&quot;&gt;총 개수에서 한 페이지에 출력할 상품의 개수의 나머지 값이 0보다 크면 1을 증가&lt;/span&gt;시켜주었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는, 총 25개의 상품이 있을 때 10개씩 한 페이지에 출력한다면 총 페이지는 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;사실 page랑 size에 대해서도 제약 조건을 걸어둬야 하고 (0 이상이라든지, 정해진 값으로 size를 받는다든지...)&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;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;style3&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 미션은 확실히 기존 코드 리팩터링 + 인프라 구축에 신경을 많이 써서 그런가 크게 회고할 내용이 없었다.&lt;/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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레벨 2 회고도 얼른 쓰고 방학 신나게 놀아야지&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;/p&gt;</description>
      <category>우아한테크코스/레벨 2</category>
      <category>5기</category>
      <category>@Transactional(propagation = Propagation.REQUIRES_NEW)</category>
      <category>@TransactionalEventListener</category>
      <category>도메인 이벤트</category>
      <category>우테코</category>
      <category>트랜잭션 전파</category>
      <category>페이징쿼리</category>
      <author>dolmeng2</author>
      <guid isPermaLink="true">https://cl8d.tistory.com/104</guid>
      <comments>https://cl8d.tistory.com/104#entry104comment</comments>
      <pubDate>Fri, 16 Jun 2023 01:23:35 +0900</pubDate>
    </item>
    <item>
      <title>[Real MySQL 8.0] InnoDB 스토리지 엔진 알아보기 - 3편 (Buffer Pool - on disk structures)</title>
      <link>https://cl8d.tistory.com/103</link>
      <description>&lt;h4 style=&quot;text-align: left;&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; style=&quot;text-align: left;&quot;&gt;이번 포스팅에서는 InnoDB의 전반적인 아키텍처 중에서 디스크 영역에 대해서 알아보자.&lt;br&gt;마찬가지로 간단하게만 짚고 넘어가고자 한다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot;&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;856&quot; data-origin-height=&quot;667&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/PQgc9/btsjYer6Dyb/8iUuPzFC5K6jievLKG9AOk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/PQgc9/btsjYer6Dyb/8iUuPzFC5K6jievLKG9AOk/img.png&quot; data-alt=&quot;저번 포스팅 그림 재탕...~&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PQgc9/btsjYer6Dyb/8iUuPzFC5K6jievLKG9AOk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPQgc9%2FbtsjYer6Dyb%2F8iUuPzFC5K6jievLKG9AOk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;601&quot; height=&quot;468&quot; data-origin-width=&quot;856&quot; data-origin-height=&quot;667&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;저번 포스팅 그림 재탕...~&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  Tablespace&lt;/b&gt;&lt;/h4&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2164&quot; data-origin-height=&quot;678&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zc2PU/btsjXI1jGGk/zItlNadNiw6hySrvueBQK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zc2PU/btsjXI1jGGk/zItlNadNiw6hySrvueBQK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zc2PU/btsjXI1jGGk/zItlNadNiw6hySrvueBQK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fzc2PU%2FbtsjXI1jGGk%2FzItlNadNiw6hySrvueBQK1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2164&quot; height=&quot;678&quot; data-origin-width=&quot;2164&quot; data-origin-height=&quot;678&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;테이블 스페이스는 데이터를 저장하기 위해 사용되는 큰 논리적인 단위이며, &lt;span style=&quot;color: #EF5369;&quot;&gt;Segment -&amp;gt; Extent -&amp;gt; Page -&amp;gt; Row&lt;/span&gt;와 같은 형태로 구성된다. MySQL에서 테이블 스페이스는 저장하는 데이터의 종류에 따라서 5가지로 분류가 가능한데, 이는 다음과 같다.&lt;br&gt;&amp;nbsp;&lt;br&gt;- System tablespace&lt;br&gt;- File-Per-Table tablespace&lt;br&gt;- General Tablespace&lt;br&gt;- Temporary Tablespace&lt;br&gt;- Undo Tablespace&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot;&gt;&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  System tablespace&lt;/b&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;System tablespace의 경우 &lt;b&gt;MySQL에서 기본적으로 사용되는&lt;/b&gt; Shared Tablespace이다.&lt;br&gt;체인지 버퍼가 있는 영역이며, 여기서 테이블을 생성하게 되면 테이블과 인덱스 데이터를 저장할 수 있다.&lt;br&gt;내부의 데이터 파일의 크기와 수는 innodb_data_file_path라는 시스템 환경변수로 설정이 가능하다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot;&gt;&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; &amp;nbsp; File-Per-Table tablespace&lt;/b&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&lt;span style=&quot;color: #EF5369;&quot;&gt;하나의 테이블에 대한 데이터와 인덱스를 개별적인 파일로 관리&lt;/span&gt;하는 테이블 스페이스이다.&lt;br&gt;즉, 우리가 생성하는 테이블은 이 공간에 저장된다고 생각하면 된다.&amp;nbsp;&lt;br&gt;inno_file_per_table 옵션으로 조절이 가능하며, 해당 옵션을 비활성화 하면 시스템 테이블스페이스에 새엇ㅇ된다.&lt;br&gt;&amp;nbsp;&lt;br&gt;- truncate / drop 연산 시 디스크 공간은 OS에게 반납된다. (테이블 삭제 시 버퍼 풀을 잠그고 스캔하기 때문에 버퍼 풀이 크다면 시간이 더 소요될 수 있다는 점 주의)&lt;br&gt;- 다른 MySQL 인스턴스에서 file-per-table 공간에 있는 테이블을 가져올 수 있다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot;&gt;&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  General&amp;nbsp;tablespace&lt;/b&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;create tablespace로 만든 구문을 사용해서 만들 수 있으며, shared tablespace여서 여러 테이블의 데이터를 저장할 수 있다.&lt;br&gt;다른 테이블 스페이스를 general로 변경할 수 없으며, 임시 테이블을 지원하지 않는다.&lt;br&gt;file-per-table 테이블 스페이스와 다르게 truncate / drop 시에 데이터 파일에 비어있는 공간이 생기며, OS로 반환되지 않는다.&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot;&gt;&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  Temporary&amp;nbsp;tablespace&lt;/b&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;내부적으로 2가지의 테이블 스페이스로 분리되는데, &lt;span style=&quot;color: #EF5369;&quot;&gt;Session / Global Temporary tablespace&lt;/span&gt;로 나뉜다.&lt;br&gt;&amp;nbsp;&lt;br&gt;Session Temporary tablespace의 경우 &lt;b&gt;사용자가 생성한 임시 테이블과 옵티마이저가 임시로 생성하는 임시 테이블의 내용을 저장&lt;/b&gt;한다.&lt;br&gt;이 공간은 커넥션이 종료되면 truncate 되어서 temp tablespace pool로 반환된다. 풀의 경우 서버가 시작되면 10개의 temporary tablespace를 생성하며, 필요에 따라서 풀에 더 추가된다.&lt;br&gt;&amp;nbsp;&lt;br&gt;Global Temporary tablespace의 경우&lt;b&gt; 임시 테이블에 대한 롤백 세그먼트들을 저장&lt;/b&gt;한다. 데이터 파일의 상대 경로나 이름, 크기, 속성을 정의하며 (innodb_temp_data_file_path 변수로 지정) 값을 지정하지 않았다면 하나의 데이터 파일로 생성하게 된다. (innodb_data_home_dir 디렉터리에 생성)&lt;br&gt;마찬가지로 종료되거나 중단되면 제거되고, 서버가 재시작되면 다시 생성된다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot;&gt;&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; &amp;nbsp;Undo Tablespace&lt;/b&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;Undo tablespace에는 클러스터링 인덱스에 대한 Undo Log 정보가 포함되어 있다. Undo 로그의 경우 롤백 세그먼트 내부에 포함된 언두 로그 세그먼트에 존재하며, 롤백 세그먼트의 경우 undo tablespace or global tempory tablespace 안에 있다.&lt;br&gt;기본적으로 2개의 undo tablespace가 생성되며, innodb_undo_directory 시스템 변수에 정의된 위치에 파일이 생성된다. (미지정 시 mysql data 디렉터리에 생성)&lt;br&gt;⭐️ 여기서 &lt;b&gt;일반 테이블에 대한 트랜잭션은 Undo tablespace&lt;/b&gt;, &lt;b&gt;임시 테이블에 대한 트랜잭션은 Global temporary tablespace&lt;/b&gt;에 undo log가 할당된다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size18&quot; style=&quot;text-align: left;&quot;&gt;&lt;b&gt;  Undo Log&lt;/b&gt;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;InnoDB는 트랜잭션 (롤백 대비)과 격리 수준 보장을 위해 &lt;span style=&quot;color: #EF5369;&quot;&gt;CUD 쿼리로 데이터가 변경되기 이전의 데이터를 별도로 백업&lt;/span&gt;하며, 이를 &lt;span style=&quot;color: #EF5369;&quot;&gt;Undo Log&lt;/span&gt;라고 한다. 매우 중요한 역할을 하는 만큼 관리 비용도 상당하다.&lt;br&gt;&amp;nbsp;&lt;br&gt;만약, A라는 트랜잭션이 매우 길어서 그 사이에 여러 CUD 작업이 일어났다면, &lt;b&gt;사이에 발생한 작업의 트랜잭션이 끝나더라도 언두 로그는 계속해서 보존될 것이다.&lt;/b&gt; MySQL 5.5 이전에서는 한 번 증가한 언두 로그 공간이 다시 줄어들지 않았다. 이러다 보니 대용량의 데이터를 처리할 때 언두 로그의 양이 급격하게 증가되고, 조회 시마다 커진 언두 로그를 스캔하면서 성능이 나빠지게 되었다. 하지만 8.0으로 오면서 &lt;span style=&quot;color: #EF5369;&quot;&gt;필요한 시점에 크기를 줄여주거나, 언두 로그를 돌아가면서 사용해서 디스크 공간이 크게 늘어나지 않도록 만들어두었&lt;/span&gt;다.&lt;br&gt;&amp;nbsp;&lt;br&gt;또한, 언두 로그가 얼마나 증가했는지 모니터링하는 기능도 추가되었다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;SHOW ENGINE INNODB STATUS \G&lt;/code&gt;&lt;/pre&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1778&quot; data-origin-height=&quot;452&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/T0GeI/btsjYsCVwx8/EI4KcAGmWfDRkazuc3Rick/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/T0GeI/btsjYsCVwx8/EI4KcAGmWfDRkazuc3Rick/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/T0GeI/btsjYsCVwx8/EI4KcAGmWfDRkazuc3Rick/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FT0GeI%2FbtsjYsCVwx8%2FEI4KcAGmWfDRkazuc3Rick%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1778&quot; height=&quot;452&quot; data-origin-width=&quot;1778&quot; data-origin-height=&quot;452&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;이런 식으로 나온다. 저기서 History List에 있는 숫자가 언두 로그의 건수이다. (딱히 작업한 게 없어서 0이다)&lt;br&gt;&amp;nbsp;&lt;br&gt;MySQL 8.0이라면 아래와 같이 좀 더 명시적으로 확인할 수 있다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;select count
from information_schema.INNODB_METRICS
where SUBSYSTEM='transaction'
and name = 'trx_rseg_history_len';&lt;/code&gt;&lt;/pre&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;308&quot; data-origin-height=&quot;124&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IVKI4/btsjRXj1Fh4/0AX0lNRHI5dt3Cgswk2330/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IVKI4/btsjRXj1Fh4/0AX0lNRHI5dt3Cgswk2330/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IVKI4/btsjRXj1Fh4/0AX0lNRHI5dt3Cgswk2330/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIVKI4%2FbtsjRXj1Fh4%2F0AX0lNRHI5dt3Cgswk2330%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;261&quot; height=&quot;105&quot; data-origin-width=&quot;308&quot; data-origin-height=&quot;124&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style3&quot;&gt;  참고로, 언두 로그의 경우 최대 4개로 분리된다.&lt;br&gt;&lt;br&gt;- 사용자 정의 테이블에 대한 INSERT&lt;br&gt;- 사용자 정의 테이블에 대한 UPDATE / DELETE&lt;br&gt;- 사용자 정의 임시 테이블에 대한 INSERT&lt;br&gt;- 사용자 정의 임시 테이블에 대한 UPDATE / DELETE&lt;br&gt;&lt;br&gt;이는 UPDATE / DELETE 시 발생한 언두 로그는 MVCC, 데이터 복구에 모두 사용되지만 INSERT의 경우 데이터 복구에 대해서만 사용되기 때문에 &lt;span style=&quot;color: #EF5369;&quot;&gt;위에서 사용했던 확인 방법으로는 UPDATE / DELETE로 인해 발생한 언두 로그 개수만 표시&lt;/span&gt;된다.&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;br&gt;이러한 언두 로그는&lt;span style=&quot;color: #EF5369;&quot;&gt; MySQL 8.0부터 로그 파일을 통해서 &lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;기&lt;/span&gt;록된다.&lt;br&gt;이전에는 시스템 테이블스페이스에 저장됐었는데, 1~128개의 롤백 세크먼스를 가진다. (페이지 크기 / 16bytes개)&lt;br&gt;하나의 롤백 세그먼트 내에는 다시 1개 이상의 Undo 슬롯을 가지고 있으며, 하나의 트랜잭션은 최대 4개까지의 언두 슬롯을 사용한다. (일반적으로는 2개)&lt;br&gt;그래서, 동시에 처리할 수 있는 트랜잭션의 개수는 보통 &lt;b&gt;(innoDB 페이지 크기) / 16 * (롤백 세그먼트 개수) * (언두 테이블스페이스 개수)&lt;/b&gt;로 구할 수 있다. 언두 로그 공간이 부족하면 트랜잭션을 시작할 수 없기 때문에, &lt;span style=&quot;color: #EF5369;&quot;&gt;언두 로그 관련 변수를 조절할 때는 동시에 처리할 수 있는 트랜잭션 개수에 따라서 테이블스페이스와 롤백 세그먼트 개수를 설정&lt;/span&gt;해야 한다.&lt;br&gt;&amp;nbsp;&lt;br&gt;MySQL 8.0부터는 create undo tablespace나 drop tablespace 등을 통해서 동적으로 테이블 스페이스를 추가하거나 삭제할 수 있다.&amp;nbsp;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;# undo log 관련 테이블스페이스 확인
select TABLESPACE_NAME, FILE_NAME
from information_schema.FILES
where FILE_TYPE like 'undo log'&lt;/code&gt;&lt;/pre&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;890&quot; data-origin-height=&quot;158&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/brhcBj/btsjYr47Ozb/46PsdHLKDdT7KyCiKE30eK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/brhcBj/btsjYr47Ozb/46PsdHLKDdT7KyCiKE30eK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/brhcBj/btsjYr47Ozb/46PsdHLKDdT7KyCiKE30eK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbrhcBj%2FbtsjYr47Ozb%2F46PsdHLKDdT7KyCiKE30eK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;753&quot; height=&quot;134&quot; data-origin-width=&quot;890&quot; data-origin-height=&quot;158&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size18&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;  Undo tablespace 공간 줄이기&lt;/b&gt;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;만약 언두 테이블스페이스의 공간을 줄이고 싶다면, 2가지의 방법을 사용할 수 있다.&lt;br&gt;CUD 쿼리 작업 시 언두 로그로 기록되는데, 트랜잭션이 커밋되면 언두 로그에 복사된 값은 불필요하기 때문에 퍼지 스레드가 주기적으로 해당 값을 제거한다. 이때, innodb_undo_log_truncate 시스템 변수가 on이라면 &lt;span style=&quot;color: #EF5369;&quot;&gt;퍼지 스레드가 주기적으로 사용하지 않는 공간을 OS에게 반납&lt;/span&gt;한다. (반납 주기는 innodb_purge_rseg_truncate_frequency 변수 조정)&lt;br&gt;&amp;nbsp;&lt;br&gt;혹은, &lt;span style=&quot;color: #EF5369;&quot;&gt;직접 비활성화 쿼리를 이용&lt;/span&gt;해서 퍼지 스레드가 비활성 상태인 테이블스페이스를 반납하도록 만들 수 있다. (이때는 테이블스페이스가 최소 3개 이상은 되어야 작동이 가능하다.)&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;# 비활성화
alter undo tablespace tablespace_name set inactive;

# 퍼지 스레드에 의해 언두 테이블스페이스 공간이 반납되면 재활성화
alter undo tablespace tablespace_name set active;&lt;/code&gt;&lt;/pre&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot;&gt;&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; &amp;nbsp;&lt;/b&gt;&lt;b&gt;Double Write Buffer&lt;/b&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;리두 로그는 페이지의 변경된 부분만 기록하는데, 만약 더티 페이지 플러시 시 일부만 기록되는 문제가 발생한다면 해당 페이지 내용은 복구 불가능할 수도 있다. 이런 식으로 &lt;b&gt;일부만 기록되는 것을 Partial-Page, 또는 Torn-Page&lt;/b&gt;라고한다.&lt;br&gt;이러한 문제를 막기 위해 InnoDB에서는 &lt;span style=&quot;color: #EF5369;&quot;&gt;' Double-Write'라는 공간을 통해, 버퍼 풀로부터 플러시된 페이지를 데이터 파일에 쓰기 전에 저장&lt;/span&gt;한다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style3&quot;&gt;1. A~E까지의 더티 페이지 플러시&lt;br&gt;2. 디스크로 기록하기 전에 A~E까지의 데이터를 묶어서 Double Write Bufer에 기록&lt;br&gt;3. 더티 페이지를 적당한 위치에 랜덤으로 쓰기 작업 진행&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;br&gt;만약 &lt;b&gt;정상 기록 시 Double Write Buffer에 기록된 것은 제거되고, 비정상적으로 종료되었다면 해당 내용을 참조하여 복구&lt;/b&gt;한다.&lt;br&gt;데이터의 무결성이 중요한 서비스라면 사용하는 것이 좋다. (innodb_doublewrite 변수 사용)&lt;br&gt;- 참고로 innodb_flush_log_at_trx_commit 옵션을 (커밋 시 아무 작업도 X, 해당 옵션이 켜져 있다면 커밋 시 로그 버퍼에 데이터 쓰고 =&amp;gt; 디스크에 데이터 쓰는 작업 진행) 성능상으로 껐다면 doublewrite도 끄자.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot;&gt;&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; &amp;nbsp;&lt;/b&gt;&lt;b&gt;Redo Log&amp;nbsp;&lt;/b&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&lt;span style=&quot;color: #EF5369;&quot;&gt;Redo Log는 비정상적으로 종료된 트랜잭션에 의해 변경된 데이터를 적용하기 위해서 사용&lt;/span&gt;한다. (복구 작업) 기본적으로 MySQL은 innoDB의 변경 내용을 바로 디스크에 저장하지 않고 버퍼 풀에 저장하기 때문에, 변경된 데이터가 디스크에 적용되지 않았다면 재시작 동작 중에 Redo 작업을 먼저 수행하게 된다.&amp;nbsp;&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style3&quot;&gt;- 참고로, 복구 작업은 다음과 같은 단계로 이루어진다.&lt;br&gt;테이블 스페이스 탐색하기 -&amp;gt; 리두 로그 적용하기 -&amp;gt; 완료되지 않은 트랜잭션 롤백하기 -&amp;gt; 체인지 버퍼 병합하기 -&amp;gt; 퍼지하기&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;br&gt;기본적으로 버퍼 풀은 디스크에서 데이터를 읽은 상태로 전혀 변경되지 않은 Clean 페이지와, 변경되었지만 디스크에 아직 기록되지 않은 Dirty 페이지를 가지고 있는데,&lt;b&gt;&amp;nbsp;Dirty 페이지의 경우 디스크와 버퍼 풀 메모리에 적재된 데이터의 상태가 다르기 때문에 둘을 동기화&lt;/b&gt;해야 한다.&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;더티 페이지의 경우 사실상 버퍼풀에 임시로 유지되는 데이터 상태이기 때문에, innoDB에서는 이를 제거하기 위해 여러 가지 방법을 사용한다. 그리고, 이 방법 중 하나가 '&lt;span style=&quot;color: #EF5369;&quot;&gt;활성 리두 로그 - Active Redo Log&lt;/span&gt;'를 사용하는 것이다.&lt;br&gt;&amp;nbsp;&lt;br&gt;활성 리두 로그의 경우 여러 파일로 구성되어 있으며, &lt;b&gt;데이터 변경이 발생할 때마다 새로운 로그 엔트리로 덮어씌우며 기록된 파일을 순차적으로 재사용하는 것이다.&lt;/b&gt; (쓰여진 리두 로그 자체) 재사용을 하기 때문에 리두 로그에서는 재사용이 가능한 공간과 재사용이 불가능한 공간으로 나누어지며, 이 중에서 &lt;b&gt;활성 리두 로그의 경우 '재사용 불가능 공간'을 의미&lt;/b&gt;한다.&lt;br&gt;&amp;nbsp;&lt;br&gt;리두 로그는 순환하기 때문에 기존의 데이터는 계속 덮어 씌워지면서 없어진다. 하지만, 기록될 때마다 계속 증가하는 '&lt;span style=&quot;color: #EF5369;&quot;&gt;LSN (Log Sequence Number)&lt;/span&gt;'이라는 값을 표시한다. LSN은 Redo Log에 기록된 작업의 시점을 가리킨다고 볼 수 있다.&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style3&quot;&gt;  InnoDB는 주기적으로 체크포인트를 통해 Redo 로그와 버퍼 풀의 Dirty 페이지를 디스크로 동기화한다. &lt;br&gt;이렇게 발생한 체크포인트 중에서 가장 최근 체크포인트 지점의 LSN이 활성 리두 로그 공간의 시작점이 된다.&lt;/blockquote&gt;&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;  CheckPoint&lt;/b&gt;&lt;b&gt;&lt;br&gt;&lt;/b&gt;&lt;span style=&quot;color: #666666;&quot;&gt;- 체크포인트가 발생하면 버퍼 풀 내의 변경된 페이지를 디스크로 동기화 작업을 진행한다.&lt;/span&gt;&lt;b&gt;&lt;br&gt;&lt;br&gt;&lt;/b&gt;&lt;b&gt;Sharp CheckPoint&lt;/b&gt;&lt;b&gt;&lt;br&gt;&lt;/b&gt;&lt;span style=&quot;color: #666666;&quot;&gt;- 커밋된 트랜잭션의 모든 더티 페이지를 디스크에 플러시하고, 가장 최근에 커밋된 트랜잭션의 LSN을 기록함&lt;br&gt;&lt;/span&gt;- 정상 종료나 리두 로그 파일을 순환하여 재사용할 때 Sharp CheckPoint 활용&lt;br&gt;&lt;br&gt;&lt;b&gt;Fuzzy CheckPoint&lt;/b&gt;&lt;br&gt;- 더티 페이지를 조금씩 디스크로 플러시하고, 그 위치를 시작 - 종료 지점의 LSN을 기록하여 관리&lt;br&gt;- 마스터 스레드에 대해 1~10초마다 주기적으로 플러시 리스트 확인&amp;nbsp;&lt;br&gt;- 프리 페이지의 여유 공간이 부족할 때 LRU 리스트의 더티 페이지 체크포인트&lt;br&gt;- 리두 로그 파일이 다 찼을 때 일부 페이지 강제 플러시 시 사용&lt;br&gt;- 더티 페이지가 너무 많을 때 체크포인트&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;br&gt;또한, &lt;span style=&quot;color: #EF5369;&quot;&gt;가장 최근 지점의 LSN과 마지막 리두 로그 엔트리의 LSN 차이를 Checkpoint Age&lt;/span&gt;라고 하며, 해당 값이 클수록 아직 디스크에 반영되지 않은 변경사항이 많다는 것을 의미하기 때문에 체크포인트를 적절한 시점에 수행해주는 것이 중요하다.&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style3&quot;&gt;결과적으로 버퍼 풀의 dirty 페이지는 특정 리두 로그 엔트리와 관계를 가지고, &lt;b&gt;체크포인트가 발생하면 체크포인트 LSN보다 작은 리두 로그 엔트리와 관련된 dirty 페이지, 리두 로그 엔트리는 모두 디스크로 동기화돼야 한다&lt;/b&gt;.&amp;nbsp;&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;br&gt;ex) 버퍼 풀이 100기가, 리두 로그 파일의 전체 크기는 100메가&lt;br&gt;- Checkpoint Age 역시 최대 100메가까지 허용된다. 리두 로그 엔트리가 평균적으로 4KB라면 100MB / 4KB = 25600개 정도의 dirty 페이지만 버퍼 풀에 보관할 수 있게 된다. 만약 데이터 페이지가 16KB라면 허용 가능한 전체 더티 페이지는 400MB 정도이다.&lt;br&gt;= 버퍼 풀은 매우 크지만, 실제 쓰기 버퍼링을 위한 효과는 거의 못 보는 상태이다.&lt;br&gt;&amp;nbsp;&lt;br&gt;ex) 버퍼 풀은 100메가, 리두 로그 파일은 100기가&lt;br&gt;- 위의 경우와 다르게 전체 더티 페이지는 최대 400기가 정도 가질 수 있게 된다. 하지만, 버퍼 풀의 크기가 100메가니까 허용 가능한 더티 페이지는 100메가 정도이다. 이 경우에는 버퍼 풀이 필요할 때 너무 많은 dirty 페이지를 한 번에 기록해야 될 수도 있어서 좋지는 않다.&amp;nbsp;&lt;br&gt;= 리두 로그 파일은 매우 크지만, 버퍼 풀이 필요할 때 더티 페이지를 갑자기 엄청 써야 하는 일이 생길 수 있어서 부하가 생길 수도 있다.&lt;br&gt;&amp;nbsp;&lt;br&gt;두 예제 모두 상당히 극단적이다.&lt;br&gt;만약 버퍼 풀의 크기가 100기가 이하라면 대략 5~10기가 정도로만 선택하자.&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size18&quot; style=&quot;text-align: left;&quot;&gt;&lt;b&gt;  Redo Log 동작 순서&lt;/b&gt;&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style3&quot;&gt;1. 버퍼 풀에서 데이터가 변경되면 해당되는 페이지를 수정한 다음 Dirty 마킹 진행&lt;br&gt;2. 관련된 리두 로그 레코드를 Double Write Buffer에 저장&lt;br&gt;3. 리두 로그 레코드를 Log Buffer로 이동&amp;nbsp;&lt;br&gt;4. 리두 로그 레코드를 Redo Log File로 플러시&lt;br&gt;5. 변경된 Dirty 페이지에 대해 체크포인트를 수행하여 System tablespace에 저장&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;- 중요한 점은 바로 디스크 영역인 System tablespace에 저장하는 게 아니라 Log Buffer에 들어가고, 한 번에 모아서 Redo Log File로 플러시한다는 게 중요하다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size18&quot; style=&quot;text-align: left;&quot;&gt;&lt;b&gt;  Redo Log 파일의 전체 크기&lt;/b&gt;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;리두 로그 파일의 전체 크기는 버퍼 풀의 효율성 을 결정할만큼 중요하다.&lt;br&gt;&lt;span style=&quot;color: #EF5369;&quot;&gt;innodb_log_file_size&lt;/span&gt;&amp;nbsp;변수를 통해 리두 로그 파일의 크기를,&amp;nbsp;&lt;span style=&quot;color: #EF5369;&quot;&gt;innodb_log_files_in_group&lt;/span&gt;을 통해 리두 로그 파일의 개수를 설정할 수 있어, 결과적으로 두 값을 곱한 값이 로그 파일의 전체 크기가 된다. 로그 버퍼의 크기는 기본값이 16MB이며, 만약 큰 데이터를 자주 변경한다면 더 크게 설정하는 것이 좋다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size18&quot; style=&quot;text-align: left;&quot;&gt;&lt;b&gt;  Redo Log 아카이빙&lt;/b&gt;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;리두 로그의 경우 중요한 만큼, 로그를 계속 추적하면서 새로 추가된 리두 로그 엔트리를 복사하는 기능이 추가되었다. (MySQL 8.0 엔터프라이즈 버전) 이때, 데이터 변경이 많아서&amp;nbsp;&lt;span style=&quot;color: #EF5369;&quot;&gt;리두 로그 내용이 복사되기도 전에 덮어씌워지더라도 리두 로그 아카이빙을 활용하면 백업이 실패하지 않게 된다&lt;/span&gt;. (innodb_redo_log_archive_start, nnodb_redo_log_archive_stop)&lt;br&gt;&amp;nbsp;&lt;br&gt;기본적으로 리두 로그는 항상 활성화되어 있는데, MySQL 8.0부터는 비활성화하도록 만들 수도 있다.&lt;br&gt;대용량 데이터를 복구하거나 한 번에 적재하는 경우에는 비활성화하여 적재 시간을 낮출 수 있다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;# 비활성화
ALTER INSTANCE DISABLE INNODB REDO_LOG

# 활성화
ALTER INSTANCE ENABLE INNODB REDO_LOG&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;물론, 비활성 후 데이터 적재 작업을 진행했다면 꼭 다시 활성화를 해줘야 한다. (비정상 종료에 늘 대비하기)&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot;&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;br&gt;내용이 많아서 어지럽다...&lt;br&gt;나중에 다시 정리해봐야겠다 ㅎㅎ;&lt;/p&gt;</description>
      <category> /Real MySQL 8.0</category>
      <category>Double Write Buffer</category>
      <category>InnoDB</category>
      <category>real mysql 8.0</category>
      <category>REDO LOG</category>
      <category>tablespace</category>
      <author>dolmeng2</author>
      <guid isPermaLink="true">https://cl8d.tistory.com/103</guid>
      <comments>https://cl8d.tistory.com/103#entry103comment</comments>
      <pubDate>Thu, 15 Jun 2023 13:52:59 +0900</pubDate>
    </item>
    <item>
      <title>[Real MySQL 8.0] InnoDB 스토리지 엔진 알아보기 - 2편 (Buffer Pool - in memory structures)</title>
      <link>https://cl8d.tistory.com/102</link>
      <description>&lt;h4 style=&quot;text-align: left;&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; style=&quot;text-align: left;&quot;&gt;이번 포스팅에서는 InnoDB의 전반적인 아키텍처 중에서 메모리 영역에 대해서 알아보자.&lt;br&gt;상당히 복잡하기 때문에 대략적인 내용만 이해하고 넘어가도 충분하다고 생각한다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot;&gt;&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&amp;nbsp;&lt;/b&gt;&lt;/h4&gt;&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  InnoDB 버퍼 풀 (Buffer Pool)&lt;/b&gt;&lt;/h4&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;856&quot; data-origin-height=&quot;667&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/s342a/btsjPI8NMkH/7jrNxGEzGWiBZYkVuV4zQ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/s342a/btsjPI8NMkH/7jrNxGEzGWiBZYkVuV4zQ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/s342a/btsjPI8NMkH/7jrNxGEzGWiBZYkVuV4zQ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fs342a%2FbtsjPI8NMkH%2F7jrNxGEzGWiBZYkVuV4zQ0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;503&quot; height=&quot;392&quot; data-origin-width=&quot;856&quot; data-origin-height=&quot;667&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;위 그림은 MySQL 8.0 기준 InnoDB 구조를 나타낸 그림이다.&lt;br&gt;이중에서 버퍼 풀의 경우 InnoDB 스토리지 엔진의 가장 핵심적인 부분이다.&lt;br&gt;&lt;b&gt;디스크의 데이터 파일, 인덱스 정보를 메모리에 캐시도 해둘 수 있고, 쓰기 작업을 지연시켜서 일괄 처리도 가능&lt;/b&gt;하다.&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;  그렇다면 크기는 어느 정도로 설정해야 할까?&lt;/b&gt;&lt;br&gt;보통 OS 전체 메모리 공간이 8기가 이하라면 50% 정도만 버퍼 풀로 설정하고, 나머지는 다른 프로그램이 사용할 공간으로 남겨두는 것이 좋다. 만약 메모리 공간이 더 크다면 조금씩 늘려가면서 최적점을 찾아나가는 것이 좋다.&lt;br&gt;50기가 이상이라면 대략 15~30기가 정도만 제외하고 나머지는 버퍼풀에게 양보해주자.&amp;nbsp;&lt;br&gt;공식 문서에서는 최대 80%가 할당된다고는 말하고 있다.&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;또한, 이미 늘어나있던 풀의 크기를 줄이는 건 영향이 매우 크기 때문에 주의해야 한다.&lt;br&gt;&amp;nbsp;&lt;br&gt;innoDB 버퍼 풀은 여러 개로 쪼개어 관리할 수 있게 되었는데, 쪼개진 각각을 '&lt;span style=&quot;color: #EF5369;&quot;&gt;버퍼 풀 인스턴스&lt;/span&gt;'라고 부른다.&lt;br&gt;기본적으로 8개로 초기화되어 있지만, 만약 &lt;b&gt;버퍼 풀의 크기가 1기가 미만이라면 1개만 생성&lt;/b&gt;된다.&lt;br&gt;보통 40기가 이하라면 8개, 그 이상이라면 인스턴스당 5기가 정도로 잡고 개수를 할당하는 것이 좋다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;show variables like '%innodb_buffer%'&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;궁금해서 나도 한 번 조회해봤는데 다음과 같았다.&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1276&quot; data-origin-height=&quot;82&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bXuxVd/btsjpEZOdPp/dHQKwFf5KtFHcyMU5DIKG1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bXuxVd/btsjpEZOdPp/dHQKwFf5KtFHcyMU5DIKG1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bXuxVd/btsjpEZOdPp/dHQKwFf5KtFHcyMU5DIKG1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbXuxVd%2FbtsjpEZOdPp%2FdHQKwFf5KtFHcyMU5DIKG1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1276&quot; height=&quot;82&quot; data-origin-width=&quot;1276&quot; data-origin-height=&quot;82&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;위의 값을 1024로 2번 나눈 값이 메가바이트인데, 딱 128MB이다. (기본값)&lt;br&gt;참고로 이 값은 항상 innodb_buffer_pool_chunk_size * innodb_buffer_pool_instances과 같아야 한다.&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1322&quot; data-origin-height=&quot;314&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ck5Hrc/btsjseGz3te/sAtJDyyQ7VBMkeQIBUKtmK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ck5Hrc/btsjseGz3te/sAtJDyyQ7VBMkeQIBUKtmK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ck5Hrc/btsjseGz3te/sAtJDyyQ7VBMkeQIBUKtmK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fck5Hrc%2FbtsjseGz3te%2FsAtJDyyQ7VBMkeQIBUKtmK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1322&quot; height=&quot;314&quot; data-origin-width=&quot;1322&quot; data-origin-height=&quot;314&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;확인해보면 현재 인스턴스가 1개여서 chunk_size 값이 그대로 size로 배정받은 것을 볼 수 있다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot;&gt;&lt;p data-ke-size=&quot;size18&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  &lt;/b&gt;&lt;b&gt;&amp;nbsp;버퍼 풀의 구조 - Free / Flush / LRU List&lt;/b&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;기본적으로 innoDB는 '버퍼 풀'이라는 거대한 메모리 공간을 페이지 크기 조각으로 쪼개서, 필요할 때 해당 데이터 페이지를 읽어 각 조각에 저장한다. 이때, 페이지 크기 조각을 관리하기 위해 &lt;span style=&quot;color: #EF5369;&quot;&gt;LRU / Flush / Free List 3개의 자료 구조&lt;/span&gt;를 관리한다.&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2108&quot; data-origin-height=&quot;718&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ufgg9/btsjqwHC73i/zJvG8Yw1NG9QtVERK2Yg00/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ufgg9/btsjqwHC73i/zJvG8Yw1NG9QtVERK2Yg00/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ufgg9/btsjqwHC73i/zJvG8Yw1NG9QtVERK2Yg00/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fufgg9%2FbtsjqwHC73i%2FzJvG8Yw1NG9QtVERK2Yg00%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;707&quot; height=&quot;241&quot; data-origin-width=&quot;2108&quot; data-origin-height=&quot;718&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;LRU 리스트는 LRU 정책에 의해서 교체된 페이지들의 목록을 관리하는데, 밑에서 자세하게 알아보자.&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size18&quot; style=&quot;text-align: left;&quot;&gt;&lt;b&gt;  Flush 리스트&lt;/b&gt;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;Flush 리스트는 &lt;span style=&quot;color: #EF5369;&quot;&gt;디스크로 동기화되지 않은 데이터를 가진 데이터 페이지&lt;/span&gt; (dirty page)의 변경 시점 기준의 페이지 목록을 관리한다. 만약, 디스크에서 읽은 상태에서 변화가 없다면 Flush 리스트에서 관리되지 않지만 특정 시점이 지나게 되면 디스크로 기록이 되어야 한다.&lt;br&gt;기본적으로 데이터가 변경되면 InnoDB는 Redo 로그에 기록하면서, 버퍼 풀의 데이터 페이지에도 해당 내용을 반영한다. 하지만, 항상 &lt;b&gt;Redo 로그가 기록되었다고 데이터 페이지에도 반영되었음을 보장할 수 없기 때문에 '체크 포인트'를 발생시켜 두 상태를 동기화&lt;/b&gt;한다. 이때 동기화를 위해서 사용하는 게 Flush 리스트라고 생각하면 된다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size18&quot; style=&quot;text-align: left;&quot;&gt;&lt;b&gt;  Free 리스트&lt;/b&gt;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;Free 리스트는 버퍼 풀에서 &lt;b&gt;실제 사용자 데이터로 채워지지 않은 비어 있는 페이지들의 목록&lt;/b&gt;으로, 사용자 쿼리가 새롭게 디스크의 데이터 페이지를 읽어와야 하는 경우에 사용된다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size18&quot; style=&quot;text-align: left;&quot;&gt;&lt;b&gt;  LRU 리스트&lt;/b&gt;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;가장 중요한 LRU 리스트에 대해서 자세히 알아보자.&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1590&quot; data-origin-height=&quot;804&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/btCsqc/btsjJVdWBUo/mFzxe8dyv4mK4KJeMJXxBK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/btCsqc/btsjJVdWBUo/mFzxe8dyv4mK4KJeMJXxBK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/btCsqc/btsjJVdWBUo/mFzxe8dyv4mK4KJeMJXxBK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbtCsqc%2FbtsjJVdWBUo%2FmFzxe8dyv4mK4KJeMJXxBK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;690&quot; height=&quot;349&quot; data-origin-width=&quot;1590&quot; data-origin-height=&quot;804&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;LRU 리스트의 경우 LRU + MRU 리스트가 결합된 형태로, 위 그림에서 &lt;span style=&quot;color: #EF5369;&quot;&gt;Old 서브 리스트 영역은 LRU, New 서브 리스트 영역은 MRU&lt;/span&gt;라고 생각하면 된다. &lt;b&gt;자주 접근되는 데이터 페이지를 New 서브리스트&lt;/b&gt;에 적재하고, &lt;b&gt;자주 접근되지 않는 데이터 페이지를 Old 서브리스트&lt;/b&gt;에 저장하는 것이다. 이는 한 번 디스크로부터 읽어온 페이지는 최대한 오랫동안 버퍼 풀이 관리하도록 해서 디스크로부터 다시 읽어오는 작업을 최소화하기 위해서이다.&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;  LRU (Least Recently Used)&lt;/b&gt;&lt;br&gt;가장 오랜 시간 동안 참조되지 않은 페이지를 교체하는 정책&lt;br&gt;&lt;br&gt;&lt;b&gt;  MRU (Most Recently Used)&lt;/b&gt;&lt;br&gt;가장 최근에 사용된 페이지를 교체하는 정책&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;여기서 New 서브 리스트의 Tail과 Old 서브 리스트의 head가 만나는 지점이 버퍼 풀 전체의 'MidPoint'라고 할 수 있으며, &lt;span style=&quot;color: #EF5369;&quot;&gt;InnoDB가 새로운 데이터 페이지를 버퍼 풀로 읽어올 때 midPoint에 삽입&lt;/span&gt;하게 된다. (Old 서브리스트의 Head에 삽입)&lt;br&gt;&amp;nbsp;&lt;br&gt;만약&lt;b&gt; Old 서브리스트에 존재하는 페이지를 접근하면 해당 데이터 페이지는 'Young'이라고 판단&lt;/b&gt;되며, New 서브리스트의 head로 이동한다. 이때, 사용자가 작성한 쿼리나 실제로 필요해서 접근한 페이지는 바로 young이 되지만, read-ahead에 의해서 자동으로 읽어오는 데이터의 경우 young으로 판단되지 않는다.&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;  Read-Ahead&lt;/b&gt;&lt;br&gt;&lt;b&gt;사용될 것 같은 데이터 페이지에 대해 버퍼 풀로 동기화&lt;/b&gt;하는 I/O request를 의미한다. 이때, 비동기로 동작한다.&lt;br&gt;하나의 extent (64개의 페이지 그룹) 전체를 버퍼 풀에 prefetch 하게 되는데, 2개의 알고리즘을 사용한다.&lt;br&gt;- linear read-ahead / random read-ahead.&lt;br&gt;- 버퍼 풀 내에서 순차적으로 읽혀진 페이지의 개수로 판단하거나, 버퍼풀 내에 존재하는 페이지의 개수로 판단하거나.&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;이러한 과정을 반복하면서, 버퍼풀에 있는 데이터 중에서 &lt;b&gt;자주 접근되지 않으면 young이 되는 페이지가 앞으로 이동하면서 자연스럽게 tail로 이동&lt;/b&gt;하게 된다. 또한, Old 서브리스트에 있는 페이지들은 새로운 페이지들이 계속 midPoint에 삽입되다 보니까 결국 tail 쪽으로 가까워지게 되고, 계속 접근되지 않으면 결국 삭제되는 것이다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size18&quot; style=&quot;text-align: left;&quot;&gt;&lt;b&gt;  InnoDB에서 데이터 찾는 과정&lt;/b&gt;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&lt;b&gt;1. 필요한 레코드가 저장된 페이지가 버퍼 풀에 존재하는지 검사한다.&lt;/b&gt;&lt;br&gt;- 어댑티브 해시 인덱스 / 테이블의 인덱스를 활용하여&amp;nbsp;버퍼 풀에서 페이지를 검색한다.&lt;br&gt;- 버퍼 풀에 이미 데이터 페이지가 &lt;b&gt;존재한다면 해당 페이지의 포인터를 New 서브리스트 쪽으로 승급&lt;/b&gt;시킨다.&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;  어댑티브 해시 인덱스 (Adaptive Hash Index)&lt;br&gt;&lt;/b&gt;자주 사용되는 컬럼을 해시로 정의하여 B-Tree를 타지 않고 바로 데이터로 접근할 수 있는 기능이다.&lt;br&gt;어댑티브 해시 인덱스의 경우 Innodb_buffer_pool_size의 1/64 정도의 크기로 초기화된다.&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;br&gt;2. 디스크에서 필요한 데이터 페이지를 버퍼 풀에 적재하고, &lt;b&gt;적재된 페이지에 대한 포인터를 Old 서브리스트의 헤더 부분에 추가&lt;/b&gt;한다. (midPoint 삽입)&lt;br&gt;&amp;nbsp;&lt;br&gt;3. Old 서브리스트 헤더 부분에 적재된 데이터 페이지가&lt;b&gt; 실제로 읽히면 New 서브리스트 헤더 부분으로 이동&lt;/b&gt;한다.&amp;nbsp;&lt;br&gt;- 이때, 대량의 읽기 작업의 경우 데이터 페이지가 버퍼 풀에 적재는 되지만, 실제 쿼리상으로는 사용되지 않을 수 있기 때문에 New 서브 리스트의 헤더 부분으로 이동하지는 않는다.&lt;br&gt;&amp;nbsp;&lt;br&gt;4. 버퍼 풀에 상주하는 데이터 페이지는 &lt;b&gt;사용자가 최근에 얼마나 접근했는지에 따라서 Age를 부여&lt;/b&gt;받으며, 쿼리에서 오랫동안 사용되지 않는다면&lt;span style=&quot;color: #EF5369;&quot;&gt; Age가 오래되기 때문에 (Aging) 버퍼 풀에서 제거&lt;/span&gt;된다. (Eviction)&lt;br&gt;- 만약, 버퍼 풀의 데이터 페이지가 쿼리에 의해서 &lt;b&gt;사용되면 나이가 다시 초기화되기 때문에&lt;/b&gt; New 서브리스트의 헤더 부분으로 옮겨진다.&lt;br&gt;- 결과적으로 최근에 얼마나 접근했는지에 따라서 Old / New 서브리스트로 이동하는 것이며, Old 서브리스트 끝으로 밀려난 데이터는 버퍼 풀에서 제거해서 새로운 데이터 페이지를 적재할 수 있는 빈 공간을 준비하게 된다.&lt;br&gt;&amp;nbsp;&lt;br&gt;5. 필요한 데이터가 &lt;b&gt;자주 접근되는 데이터라면, 해당 페이지의 인덱스 키를 어댑티브 해시 인덱스에 추가&lt;/b&gt;한다.&lt;br&gt;&amp;nbsp;&lt;br&gt;결과적으로 처음에 한 번 읽힌 데이터 페이지가 자주 사용되면 New 서브리스트에서 계속 살아남게 되고, 거의 사용되지 않는다면 계속 밀려서 Old 서브리스트 끝으로 밀려나다가 버퍼 풀에서 제거된다.&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot;&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  Adaptive Hash Index (어댑티브 해시 인덱스)&lt;/b&gt;&lt;/h4&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;509&quot; data-origin-height=&quot;345&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FPVCU/btsjJekrfNk/SHpGZvZzWJDJPusfKK0xV1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FPVCU/btsjJekrfNk/SHpGZvZzWJDJPusfKK0xV1/img.png&quot; data-alt=&quot;https://tech.kakao.com/2016/04/07/innodb-adaptive-hash-index/&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FPVCU/btsjJekrfNk/SHpGZvZzWJDJPusfKK0xV1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFPVCU%2FbtsjJekrfNk%2FSHpGZvZzWJDJPusfKK0xV1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;444&quot; height=&quot;301&quot; data-origin-width=&quot;509&quot; data-origin-height=&quot;345&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://tech.kakao.com/2016/04/07/innodb-adaptive-hash-index/&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;br&gt;어댑티브 해시 인덱스는 &lt;span style=&quot;color: #EF5369;&quot;&gt;사용자가 자주 요청하는 데이터에 대해서 자동으로 생성하는 인덱스&lt;/span&gt;이다.&lt;br&gt;자주 읽히는 데이터 페이지의 키 값을 이용해 해시 인덱스를 만들어서, 해당 인덱스를 기준으로 데이터 페이지를 바로 찾아가는 것이다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1098&quot; data-origin-height=&quot;296&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/p1CGT/btsjWsRcZpR/mkrbb9fGzsSu3hubq8xij1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/p1CGT/btsjWsRcZpR/mkrbb9fGzsSu3hubq8xij1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/p1CGT/btsjWsRcZpR/mkrbb9fGzsSu3hubq8xij1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fp1CGT%2FbtsjWsRcZpR%2Fmkrbb9fGzsSu3hubq8xij1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;600&quot; height=&quot;162&quot; data-origin-width=&quot;1098&quot; data-origin-height=&quot;296&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;해시 인덱스의 경우 인덱스 키 값과 인덱스 키 값이 저장된 데이터 페이지 주소의 쌍으로 관리되며, 인덱스 키 값은 B-Tree 인덱스의 고유 번호와 실제 값으로 이루어져 있다. &lt;span style=&quot;color: #EF5369;&quot;&gt;모든 B-Tree 인덱스에 대한&amp;nbsp;어댑티브 해시 인덱스는 하나의 해시 인덱스에 저장되어야 하기 때문&lt;/span&gt;에 B-Tree의 고유 번호를 통해 겹치지 않도록 만든 것이다.&lt;br&gt;&amp;nbsp;&lt;br&gt;버퍼 풀에 로딩된 페이지 주소를 사용하기 때문에 버퍼 풀에 올라간 데이터에 대해서만 관리된다.&lt;br&gt;그래서, &lt;span style=&quot;color: #EF5369;&quot;&gt;버퍼 풀에서 삭제되면 인덱스 정보 역시 함께 제거된다&lt;/span&gt;.&lt;br&gt;= 즉, 어댑티브 해시 인덱스는 버퍼풀에 올라간 데이터에 대해 더 빠르게 접근할 수 있도록 만든 것이다.&lt;br&gt;&amp;nbsp;&lt;br&gt;다음과 같은 상황에서 어댑티브 해시 인덱스는 도움이 된다.&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style3&quot;&gt;- 디스크의 데이터가 innoDB 버퍼 풀의 크기와 비슷한 경우 (읽기 작업이 많지 않음)&lt;br&gt;- 동등 비교와 IN 연산을 통한 비교 작업이 많은 경우&lt;br&gt;- 쿼리가 일부 데이터만 집중되는 경우&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;br&gt;하지만, 다음과 같은 경우에는 크게 의미있지 않기 때문에 잘 고려해서 사용하자.&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style3&quot;&gt;- 디스크 읽기 작업이 많은 경우&lt;br&gt;- 특정 패턴의 쿼리가 많은 경우 (JOIN, LIKE 검색)&lt;br&gt;- 매우 큰 데이터를 가진 테이블의 레코드를 폭넓게 읽는 경우&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;⭐️ 어댑티브 해시 인덱스는&amp;nbsp;&lt;/span&gt;&lt;b&gt;테이블을 삭제하거나 변경할 때 해당 테이블의 모든 데이터 페이지 내용을 어댑티브 해시 인덱스에서 제거&lt;/b&gt;해야 하다 보니 이럴 때는 CPU cost가 크다. 잘 고려해서 사용하자.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;show engine innodb status;&lt;/code&gt;&lt;/pre&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;820&quot; data-origin-height=&quot;394&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vvCpf/btsjYd62suX/IYzqpSfybK6hGhZ6X4yMBk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vvCpf/btsjYd62suX/IYzqpSfybK6hGhZ6X4yMBk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vvCpf/btsjYd62suX/IYzqpSfybK6hGhZ6X4yMBk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvvCpf%2FbtsjYd62suX%2FIYzqpSfybK6hGhZ6X4yMBk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;537&quot; height=&quot;258&quot; data-origin-width=&quot;820&quot; data-origin-height=&quot;394&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;위 사진은 innodb status를 확인했을 때의 결과이다. 현재는 별도의 작업을 하는 게 없어서 0으로 나오지만, 여기서 &lt;b&gt;hash searches는 어댑티브 해시 인덱스를 사용한 결과&lt;/b&gt;를, &lt;b&gt;non-hash searches는 사용하지 않은 결과&lt;/b&gt;이다.&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;(hash searches) / (hash searches + non-hash searches) * 100을 했을 때 100%에 가깝다면 어댑티브 해시 인덱스를 잘 사용하는 것이고, 낮다면 비활성화하는 것도 고려해보는 것이 좋다. 비활성화를 하게 되면 버퍼 풀은 더 많은 메모리를 사용할 수 있을 것이다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;# 어댑티브 해시 인덱스의 메모리 사용량
SELECT EVENT_NAME, CURRENT_NUMBER_OF_BYTES_USED
FROM performance_schema.memory_summary_global_by_event_name
WHERE EVENT_NAME='memory/innodb/adaptive hash index'&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot;&gt;&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; &amp;nbsp;Change Buffer (체인지 버퍼)&lt;/b&gt;&lt;/h4&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;581&quot; data-origin-height=&quot;408&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tRyeI/btsjRhQuk8r/x0x1x8QVmD4pc9zu1SgUV1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tRyeI/btsjRhQuk8r/x0x1x8QVmD4pc9zu1SgUV1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tRyeI/btsjRhQuk8r/x0x1x8QVmD4pc9zu1SgUV1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtRyeI%2FbtsjRhQuk8r%2Fx0x1x8QVmD4pc9zu1SgUV1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;413&quot; height=&quot;290&quot; data-origin-width=&quot;581&quot; data-origin-height=&quot;408&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;체인지 버퍼는&lt;span style=&quot;color: #EF5369;&quot;&gt; 특정 데이터 페이지가 버퍼 풀에 없을 때 secondary-index 페이지에 대한 변경 사항을 캐시하는 특수한 데이터 구조&lt;/span&gt;이다.&lt;br&gt;&amp;nbsp;&lt;br&gt;Secondary-index의 경우 유니크하지 않고, 임의의 순서로 삽입된다. insert, update 작업 시 데이터 파일뿐만 아니라 인덱스도 함께 업데이트하면서 랜덤하게 디스크를 읽어야 하기 때문에 비용이 많이 들게 된다. 이를 위해 변경해야 하는 인덱스 페이지가 버퍼 풀에 있으면 바로 업데이트를 수행하지만, 디스크로부터 읽어와서 업데이트해야 하다면 즉시 실행하지 않고 &lt;span style=&quot;color: #EF5369;&quot;&gt;체인지 버퍼에 저장했다가 사용자에게 결과를 바로 반환하도록 최적화&lt;/span&gt;가 되어 있다.&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;⭐️ 단, &lt;b&gt;유니크 인덱스의 경우 중복 체크를 해야 하므로 체인지 버퍼를 사용할 수 없다&lt;/b&gt;.&lt;br&gt;체인지 버퍼에 저장된 인덱스 레코드는 백그라운드 스레드 중 '&lt;b&gt;체인지 버퍼 머지 스레드 (Merge Thread)&lt;/b&gt;'에 의해서 병합된다.&lt;br&gt;innodb_change_buffering이라는 변수를 통해서 어떤 작업인지에 따라 체인지 버퍼를 활성화할 수 있다.&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style3&quot;&gt;- all: 모든 인덱스 관련 작업 (inserts, deletes, purges) 버퍼링&lt;br&gt;- none: 버퍼링 X&lt;br&gt;- inserts: 새로운 아이템을 추가하는 작업만 버퍼링&lt;br&gt;- deletes: 기존 아이템을 삭제하는 작업만 버퍼링 (영구 삭제가 아닌, 삭제됐다고 마킹만 함)&lt;br&gt;- changes: inserts + deletes만 버퍼링&lt;br&gt;- purges: purges - 영구 삭제 작업만 버퍼링&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;br&gt;체인지 버퍼의 경우 버퍼 풀의 25%~50%까지 사용할 수 있으며, 어느 정도 사용 중인지는 아래와 같이 확인할 수 있다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;select EVENT_NAME, CURRENT_NUMBER_OF_BYTES_USED
from performance_schema.memory_summary_global_by_event_name
where EVENT_NAME='memory/innodb/ibuf0ibuf';&lt;/code&gt;&lt;/pre&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1236&quot; data-origin-height=&quot;116&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dABXzL/btsjO3ERNns/Ovj3rdgDxVO7KYFK3wMLL1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dABXzL/btsjO3ERNns/Ovj3rdgDxVO7KYFK3wMLL1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dABXzL/btsjO3ERNns/Ovj3rdgDxVO7KYFK3wMLL1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdABXzL%2FbtsjO3ERNns%2FOvj3rdgDxVO7KYFK3wMLL1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1236&quot; height=&quot;116&quot; data-origin-width=&quot;1236&quot; data-origin-height=&quot;116&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot;&gt;&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  Log Buffer (로그 버퍼)&lt;/b&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;로그 버퍼는 &lt;span style=&quot;color: #EF5369;&quot;&gt;디스크의 로그 파일에 기록할 데이터를 보관하는 데이터 영역&lt;/span&gt;이다.&lt;br&gt;대부분의 데이터베이스 서버는 데이터의 변경 내용을 먼저 로그로 기록하는데, &lt;b&gt;이는 디스크에 바로 기록하는 것이 상당한 비용이 들기 때문이다&lt;/b&gt;. 또한, 리두 로그에 작성하면 비정상 종료 시 복구도 가능하기 때문에 상당히 중요하다.&lt;br&gt;&amp;nbsp;&lt;br&gt;흔히 비정상 종료된 이후, 일관되지 않은 데이터는 2종류로 나뉜다.&lt;br&gt;- 커밋은 됐지만 데이터 파일에 기록되지 않음&lt;br&gt;- 롤백은 됐지만 데이터 파일에 이미 기록이 되어버림&lt;br&gt;&amp;nbsp;&lt;br&gt;1번의 경우 리두 로그에 저장된 걸 기록하면 되고, 2번의 경우 변겨오디지 않은 데이터를 가져와서 기록해야 한다.&lt;br&gt;당연히 &lt;b&gt;데이터의 일관성을 위해서라면 트랜잭션이 커밋될 때마다 리두 로그를 즉시 디스크에 기록하는 게 좋지만, 비용이 큰 디스크 작업 상 부하가 일어날 포인트&lt;/b&gt;이다. 이를 위해 innodb_flush_log_at_trx_commit 시스템 변수를 통해서 어느 정도 주기로 동기화할지 설정할 수 있다.&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style3&quot;&gt;0: 1초에 한 번씩 리두 로그를 디스크로 기록 후 동기화&amp;nbsp;&lt;br&gt;1: 트랜잭션이 커밋될 때마다 디스크로 기록 후 동기화 (기본값)&lt;br&gt;2: 트랜잭션이 커밋될 때마다 디스트 기록은 되지만, 동기화는 1초에 한 번씩&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;br&gt;Log Buffer의 경우 디스크 영역에 있는 Redo Log와 밀접한 관계를 가지기 때문에 자세한 건 다음 포스팅에서 알아보자.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot;&gt;&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; &amp;nbsp;&lt;/b&gt;&lt;b&gt;버퍼 풀 플러시&amp;nbsp;&lt;/b&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&lt;span style=&quot;color: #EF5369;&quot;&gt; dirty page를 디스크에 동기화하는 것을 더티 페이지 플러시&lt;/span&gt;라고 한다.&lt;br&gt;InnoDB에서는 플러시 시 성능 문제를 고려하여 2가지의 방법을 제시하고 있는데, '&lt;b&gt;플러시 리스트 플러시(Flush-List Flush)&lt;/b&gt;'와 '&lt;b&gt;LRU 리스트 플러시 (LRU-List Flush)&lt;/b&gt;가 있다.&lt;/p&gt;&lt;p data-ke-size=&quot;size18&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;  플러시 리스트 플러시&lt;/b&gt;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;InnoDB는 리두 공간의 재활용을 위해서, 주기적으로 오래된 리두 로그 엔트리가 사용하는 공간을 비운다. 이때, &lt;span style=&quot;color: #EF5369;&quot;&gt;디스크로 동기화되는 작업이 꼭 선행되어야 하는데&lt;/span&gt; Flush_List 함수를 호출해서 오래전에 변경된 데이터 페이지 순서대로 디스크에 동기화하는 작업을 수행한다. 해당 작업은 '&lt;b&gt;클리너 스레드 (Cleaner Thread)&lt;/b&gt;'를 통해서 이루어진다.&lt;br&gt;&amp;nbsp;&lt;br&gt;이를 위해 다양한 시스템 변수들을 제공하는데, 이는 다음과 같다.&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;- innodb_page_cleaners&lt;/b&gt;&lt;br&gt;: 클리너 스레드의 개수를 조정한다. 버퍼 풀의 개수보다 많으면 innodb_buffer_pool_instances 값으로 변경된다.&lt;br&gt;더 적다면 하나의 클리너 스레드가 여러 개의 버퍼 풀을 처리하게 된다. (웬만하면 동일하게 하자.) 기본값은 4이다.&lt;br&gt;&lt;br&gt;&lt;b&gt;- innodb_max_dirty_pages_pct&lt;/b&gt;&lt;br&gt;: 버퍼 풀이 더티 페이지를 몇 퍼센트나 가지고 있을지 비율 설정 (클수록 여러 번의 디스크 쓰기를 한 번으로 최적화 가능)&lt;br&gt;최대 90%까지 가지고 있을 수 있다.&lt;br&gt;&lt;br&gt;&lt;b&gt;- innodb_max_dirty_pages_pct_lwm&lt;/b&gt;&lt;br&gt;: 디스크로 작성되는 페이지보다 버퍼 풀에 더 많은 더티 페이지가 존재하게 되면, 더티 페이지를 모두 디스크 페이지에 작성하기 위해서 디스크 쓰기 작업이 폭발할 수 있다. (즉, pages_pct로 정의한 임계값까지 도달하지 않도록 만드는 변수) 이를 막기 위해 일정 수 이상의 더티 페이지가 생기면 조금씩 디스크로 기록하는 '사전 플러시' 비율을 조정한다. 기본 값은 10%이다.&amp;nbsp;&lt;br&gt;&lt;br&gt;&lt;b&gt;- innodb_io_capacity&lt;/b&gt;&lt;br&gt;: 일반적인 상황에서 어느 정도의 디스크 읽고 쓰기가 가능한지 설정. 여기서 말하는 디스크 작업은 '백그라운드 스레드'가 수행하는 작업이며, 대부분 더티 페이지의 쓰기 작업이 해당한다.&lt;br&gt;&lt;br&gt;&lt;b&gt;- innodb_io_capacity_max&lt;/b&gt;&lt;br&gt;: 최대의 성능을 발휘할 때 어느 정도의 디스크 읽고 쓰기가 가능한지 설정.&lt;br&gt;&lt;br&gt;&lt;b&gt;- innodb_flush_neighbors&lt;/b&gt;&lt;br&gt;: 디스크 기록 시 디스크에서 근접한 페이지에 더티 페이지가 있다면 함께 묶어서 기록하는 기능을 활성화할지 설정.&lt;br&gt;-이면 비활성화 (SSD 권장), 1이면 활성화 (HDD 권장), 2면 연속된 더티 페이지를 플러시하는 게 아닌, 그냥 하나만 플러시.&lt;br&gt;&lt;br&gt;&lt;b&gt;- innodb_lru_scan_depth&lt;/b&gt;&lt;br&gt;버퍼 풀 인스턴스마다 클리너 스레드가 더티 페이지 플러시를 위해 버퍼 풀 LRU 리스트를 얼마나 깊게 스캔할지 설정.&lt;br&gt;innodb_lru_scan_depth * innodb_buffer_pool_instances가 클리너 스레드가 1초에 수행하는 작업량을 정의한다.&lt;br&gt;&lt;br&gt;&lt;b&gt;- innodb_adaptive_flushing&lt;/b&gt;&lt;br&gt;: 어댑티브 플러시 활성화 여부 설정. 활성화 시 capacity, capacity_max 같은 시스템 변수 값을 의존하지 않고, 별도의 새로운 알고리즘을 사용하여 적절한 수의 더티 페이지가 버퍼 풀에 유지되도록 디스크 쓰기 작업을 수행한다.&lt;br&gt;버퍼 풀의 더티 페이지 수와 리두 로그 레코드의 생성 속도를 바탕으로 어느 정도의 더티 페이지를 플러시할지 알아서 결정한다.&lt;br&gt;&lt;br&gt;&lt;b&gt;- innodb_adaptive_flushing_lwm&lt;/b&gt;&lt;br&gt;: 활성 리두 공간이 n% 미만일 때 어댑티브 플러시를 활성화 할지 적용하는 값. 이 임계값을 넘게 되면 innodb_adaptive_flushing이 비활성화되어 있더라도 알아서 어댑티브 플러시를 적용하여 활성화한다.&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size18&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;  LRU 리스트 플러시&lt;/b&gt;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;LRU 리스트에서 &lt;span style=&quot;color: #EF5369;&quot;&gt;사용 빈도가 낮으면 Old 서브리스트에서 밀려나다가 플러시&lt;/span&gt;되는 게 기억날 것이다. 이때 사용하는 게 LRU 리스트 플러시이며, innodb_lru_scan_depth 변수에 설정된 개수만큼 페이지를 스캔하며 더티 페이지를 디스크에 동기화한다.&lt;br&gt;&amp;nbsp;&lt;br&gt;플러시된 더티 페이지들의 프레임을 비운 다음, free 페이지로 만들어서 Free List에 삽입한다. 이때, 버퍼 풀 인스턴스별로 최대 설정한 개수만큼 스캔하기 때문에 innodb_buffer_pool_instances * innodb_lru_scan_depth 수만큼 수행된다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot;&gt;&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; &amp;nbsp;&lt;/b&gt;&lt;b&gt;버퍼 풀 상태 백업 및 복구&amp;nbsp;&lt;/b&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&lt;span style=&quot;color: #EF5369;&quot;&gt;디스크의 데이터 버퍼 풀에 적재되어 있는 상태를 '워밍업'&lt;/span&gt;이라고 하는데, 잘 워밍업되어 있다면 당연히 쿼리 속도가 매우 증가한다.&lt;br&gt;하지만, 서버를 재시작하는 경우 적재되어 있는 게 없으니  처음에는 요청 속도가 매우 느리게 된다. 하지만, MySQL 5.6부터는 재시작 시 &lt;b&gt;innodb_buffer_dump_now라는 변수를 통해서 &lt;/b&gt;&lt;span style=&quot;color: #EF5369;&quot;&gt;현재의 버퍼 풀 상태를 백업&lt;/span&gt;할 수 있다. 백업 후 innodb_buffer_dump_load_now를 통해서 다시 복구가 가능하다!&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;# 백업하기
SET GLOBAL innodb_buffer_pool_dump_now = ON;
# 로드하기
SET GLOBAL innodb_buffer_pool_load_now = ON;&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;백업 시에는 적재된 데이터 페이지의 메타 정보만 가져와서 저장하기 때문에 실제 용량은 그렇게 크지 않으며 (ib_buffer_pool 파일 확인), 백업 속도도 빠르다. 하지만, 다시 로드할 때는 적재 작업을 재진행해야 하다 보니까 시간이 좀 걸릴 수 있다. 진행 상황을 확인하고 싶다면 아래와 같이 입력하자.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;# 얼마나 복구되었는지 확인
SHOW STATUS LIKE 'Innodb_buffer_pool_dump_status'\G&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;만약 너무 오래 걸려서 참을 수 없다면 멈추자.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;SET GLOBAL innodb_buffer_pool_load_abort = ON;&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;br&gt;근데 이런 걸 수동으로 하는 건 쉽지 않다.&lt;br&gt;그래서&lt;b&gt; MySQL 서버가 셧다운 되기 전에 백업하고, 시작 시 자동으로 복구하도록&lt;/b&gt; innodb_buffer_pool_dump_at_shutdown과 innodb_buffer_pool_load_at_startup을 설정 파일에 넣어두자. (기본적으로 활성화가 되어 있긴 하다 ㅎㅎ)&lt;br&gt;&amp;nbsp;&lt;br&gt;그렇다면, 버퍼 풀에는 어떤 내용이 적재되어 있을까?&lt;br&gt;아래는 테이블의 인덱스별로 데이터 페이지가 얼마나 innoDB 버퍼 풀에 적재되어 있는지를 나타낸다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;select
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;it.NAME AS table_name,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ii.NAME AS index_name,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ici.N_CACHED_PAGES AS n_cached_pages
from information_schema.INNODB_TABLES it
inner join information_schema.INNODB_INDEXES ii on ii.TABLE_ID = it.TABLE_ID
inner join information_schema.INNODB_CACHED_INDEXES ici on ici.INDEX_ID = ii.INDEX_ID;&lt;/code&gt;&lt;/pre&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1152&quot; data-origin-height=&quot;330&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/blXL6p/btsjWsP6TJC/c4S92ipK396hhMaqkQRQDK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/blXL6p/btsjWsP6TJC/c4S92ipK396hhMaqkQRQDK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/blXL6p/btsjWsP6TJC/c4S92ipK396hhMaqkQRQDK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FblXL6p%2FbtsjWsP6TJC%2Fc4S92ipK396hhMaqkQRQDK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;692&quot; height=&quot;198&quot; data-origin-width=&quot;1152&quot; data-origin-height=&quot;330&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;</description>
      <category> /Real MySQL 8.0</category>
      <category>adaptive hash index</category>
      <category>buffer pool</category>
      <category>change buffer</category>
      <category>InnoDB</category>
      <category>log buffer</category>
      <category>LRU</category>
      <category>real mysql 8.0</category>
      <category>버퍼풀</category>
      <author>dolmeng2</author>
      <guid isPermaLink="true">https://cl8d.tistory.com/102</guid>
      <comments>https://cl8d.tistory.com/102#entry102comment</comments>
      <pubDate>Wed, 14 Jun 2023 21:48:29 +0900</pubDate>
    </item>
    <item>
      <title>[Real MySQL 8.0] InnoDB 스토리지 엔진 알아보기 - 1편 (클러스터링 / 외래키 / MVCC)</title>
      <link>https://cl8d.tistory.com/101</link>
      <description>&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  InnoDB 스토리지 엔진 아키텍처&lt;/b&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;MySQL에서 가장 많이 사용하는 스토리지 엔진인 'InnoDB'에 대한 아키텍처를 알아보자.&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2098&quot; data-origin-height=&quot;1254&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7U5Z1/btsjpE5ezxt/QnRLoKb6JBQR8oq2AfRY8K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7U5Z1/btsjpE5ezxt/QnRLoKb6JBQR8oq2AfRY8K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7U5Z1/btsjpE5ezxt/QnRLoKb6JBQR8oq2AfRY8K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7U5Z1%2FbtsjpE5ezxt%2FQnRLoKb6JBQR8oq2AfRY8K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2098&quot; height=&quot;1254&quot; data-origin-width=&quot;2098&quot; data-origin-height=&quot;1254&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;그림이 복잡하기는 하지만, 왼쪽은 이전에 봤던 MySQL 엔진이고, 오른쪽이 InnoDB 스토리지 엔진의 구성도이다.&lt;br&gt;이번 포스팅에서는 innoDB 스토리지 엔진의 특징에 대해서 알아보도록 하자.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot;&gt;&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1. PK에 의한 클러스터링&lt;/b&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;기본적으로 &lt;span style=&quot;color: #EF5369;&quot;&gt;InnoDB의 모든 테이블은 PK를 기준으로 클러스터링&lt;/span&gt;되어 저장된다.&lt;br&gt;여기서 클러스터링된다는 것은, &lt;b&gt;PK 값이 비슷한 레코드끼리 묶어서 저장&lt;/b&gt;하는 것을 의미한다. PK 값이 레코드의 저장 위치를 결정하기 때문에 신중하게 결정해야 한다.&amp;nbsp;&lt;br&gt;일반적인 DBMS에서 세컨더리 인덱스의 리프 노드에는 데이터의 실질적인 물리 주소가 들어가서, 해당 인덱스의 데이터를 바로 가지고 오지만 &lt;span style=&quot;color: #EF5369;&quot;&gt;innoDB에서는 PK 값에 대한 주소를 가지고 있&lt;/span&gt;다. 그래서 사용 시 PK 값을 찾아나가게 된다. (세컨더리 인덱스 -&amp;gt; PK 값 -&amp;gt; 데이터)&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;  세컨더리 인덱스란? (Secondary-Index)&lt;/b&gt;&lt;br&gt;PK 외의 정렬 기준이 필요할 때 사용되며, 하나의 테이블에 여러 개를 가질 수 있다.&lt;br&gt;순서를 가지지 않기 때문에 정렬되지 않아도 되며, unique 하지 않아도 된다는 점이 특징이다. (물론, 세컨더리 인덱스라고 했을 때 배표적인 예시로 유니크 키를 많이 뽑기는 한다.)&lt;br&gt;일종의 보조 역할을 하는 인덱스라고 생각하면 된다.&amp;nbsp;&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;모든 세컨더리 인덱스가 PK 값을 가지고 있다고 생각하면 된다.&lt;br&gt;⭐️ 만약 innoDB 테이블을 생성할 때 PK를 지정하지 않으면 내부적으로 대체할 컬럼을 찾게 되는데, NOT NULL이면서 unique한 인덱스 중에서 첫 번째 것을 클러스터 키로 삼게 된다. (단, 내부적으로 관리되기 때문에 조회 불가)&lt;br&gt;&amp;nbsp;&lt;br&gt;반면에, MyISAM 스토리지 엔진에서는 클러스터링 키를 지원하지 않기 때문에 &lt;b&gt;PK와 세컨더리 인덱스가 구조적으로 아무런 차이가 없다&lt;/b&gt;. 단지, PK는 unique하다는 조건이 추가된 세컨더리 인덱스와 동일해진다. 또한, MyISAM 테이블의 모든 인덱스는 물리적인 레코드 주소값 (ROW-ID)을 가지기 때문에, PK 값이 변한다고 해서 실제 데이터 레코드의 위치가 변하지 않는다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot;&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. 외래키 지원&lt;/b&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;MyISAM과 다르게&lt;span style=&quot;color: #EF5369;&quot;&gt; InnoDB의 경우 FK를 지원&lt;/span&gt;한다. 사실 실무에서는 외래키를 잘 걸지 않는다는 말을 많이 들어서 이게 큰 이점이 될지는 모르겠지만, 로컬 환경에서 개발할 때는 좋게 쓰일 것으로 생각된다.&amp;nbsp;&lt;br&gt;만약 임시로 외래키 체크를 끄고 싶다면 다음 옵션을 사용하면 된다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;# 글로벌 설정
SET foreign_key_checks=OFF;

# 현재 작업을 진행하는 세션에 대해서만 끄기
SET SESSION foreign_key_checks=OFF;&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;해당 옵션을 끄게 되면 ON DELETE CASCADE, ON UPDATE CASCADE 같은 옵션은 사용할 수 없음을 유의하자.&lt;br&gt;참고로 외래키 옵션을 끈 뒤로 테이블에 대한 조작 작업을 했다면, 레코드 사이의 일관성이 깨지지 않았는지 잘 확인하고 다시 옵션을 켜야 한다.&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot;&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3. MVCC (Multi Version Concurrency Control)&lt;/b&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;MVCC는 동시 접근을 허용하는 데이터베이스에서 동시성을 제어하기 위해 사용하는 방법 중 하나이다. 여기서 말하는 'Multi Version'의 경우&lt;span style=&quot;color: #EF5369;&quot;&gt; 하나의 레코드에 대해서 여러 개의 버전을 동시에 관리한다는 의미&lt;/span&gt;이다. (레코드 접근 시점에 대한 스냅샷을 관리한다.)&lt;br&gt;일반적으로 레코드 레벨의 트랜잭션을 지원하는 DBMS가 지원하는 기능이며, &lt;b&gt;락을 사용하지 않으면서 일관된 값을 읽도록 만드는 것&lt;/b&gt;이 목적이다. InnoDB의 경우 Undo log를 활용하여 이 기능을 구현한다.&lt;br&gt;&amp;nbsp;&lt;br&gt;격리 수준이 READ_COMMITTED일 때 어떤 식으로 처리하는지 그림으로 알아보자.&lt;br&gt;⭐️ 참고로, InnoDB에서는 기본적으로 REPEATABLE_READ을 기본 격리 레벨로 사용한다.&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;  READ_COMMITTED&lt;/b&gt;&lt;br&gt;특정 트랜잭션이 진행되는 동안 &lt;b&gt;다른 트랜잭션은 해당 데이터에 접근할 수 없는 격리 레벨&lt;/b&gt;.&lt;br&gt;read 연산 시 실제 테이블 값을 가져오지 않고, Undo 영역의 백업된 레코드에서 값을 가져온다.&lt;br&gt;= 즉, 트랜잭션이 시작되기 전 데이터를 읽어오기 때문에 완전히 커밋된 데이터만 읽게 된다.&lt;br&gt;단, 커밋된 데이터만 읽기 때문에 동일한 select 쿼리를 날리더라도 같은 결과를 반환받지 못할 수도 있다.&lt;/blockquote&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;CREATE TABLE `member` (
&amp;nbsp;&amp;nbsp;`id` bigint NOT NULL AUTO_INCREMENT,
&amp;nbsp;&amp;nbsp;`name` varchar(10) NOT NULL,
&amp;nbsp;&amp;nbsp;`password` varchar(64) NOT NULL,
&amp;nbsp;&amp;nbsp;PRIMARY KEY (`id`),
&amp;nbsp;&amp;nbsp;INDEX KEY `name` (`name`)
) ENGINE=InnoDB;&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;위와 같이 간단하게 member에 대해 관리하는 테이블이 있다.&lt;br&gt;위 테이블에 다음과 같이 한 명의 멤버를 넣어보자.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;insert into member(name, password) values ('journey', 'testpassword');&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;해당 레코드는 아래와 같이 관리된다.&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1388&quot; data-origin-height=&quot;938&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/D3lse/btsjmCs9FHS/3KftEBUE8uiwKgtpmlNI6k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/D3lse/btsjmCs9FHS/3KftEBUE8uiwKgtpmlNI6k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/D3lse/btsjmCs9FHS/3KftEBUE8uiwKgtpmlNI6k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FD3lse%2FbtsjmCs9FHS%2F3KftEBUE8uiwKgtpmlNI6k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;642&quot; height=&quot;434&quot; data-origin-width=&quot;1388&quot; data-origin-height=&quot;938&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;PK와 name, password가 InnoDB 버퍼 풀과 디스크에 각각 저장된 것을 볼 수 있다.&lt;br&gt;&amp;nbsp;&lt;br&gt;만약, 해당 레코드에 업데이트가 일어났다면 어떻게 될까?&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;update member set password='realpassword' where id = 1;&lt;/code&gt;&lt;/pre&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1728&quot; data-origin-height=&quot;888&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mLFMy/btsjk5JJaFM/2HwHehXQsABwtqC2AyXY0K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mLFMy/btsjk5JJaFM/2HwHehXQsABwtqC2AyXY0K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mLFMy/btsjk5JJaFM/2HwHehXQsABwtqC2AyXY0K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmLFMy%2Fbtsjk5JJaFM%2F2HwHehXQsABwtqC2AyXY0K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;665&quot; height=&quot;342&quot; data-origin-width=&quot;1728&quot; data-origin-height=&quot;888&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;update가 실행되면 &lt;span style=&quot;color: #EF5369;&quot;&gt;변경 이전의 값을 undo 로그에 저장하고, 버퍼 풀에는 업데이트된 새로운 값으로 수정&lt;/span&gt;된다.&lt;br&gt;디스크의 경우 InnoDB의 백그라운드 스레드 중 &lt;b&gt;쓰기 스레드 (Write)에 의해서 새로운 값으로 업데이트&lt;/b&gt;되며, 이때 ACID를 보장하기 때문에 일반적으로는 버퍼 풀과 데이터 파일이 동일하게 된다. (시점에 따라서 업데이트되기 전일수도 있지만)&lt;br&gt;&amp;nbsp;&lt;br&gt;그리고, 이 상태에서 select 쿼리를 날리게 되면 격리 레벨에 따라서 어디에 있는 데이터를 조회할지 달라진다. 만약&lt;b&gt; read_uncommitted라면 버퍼 풀에 있는 데이터&lt;/b&gt;를&lt;b&gt;, read commmitted 이상의 레벨이라면 undo 로그에 있는 데이터&lt;/b&gt;를 읽게 된다.&lt;br&gt;이렇게 &lt;span style=&quot;color: #EF5369;&quot;&gt;하나의 레코드에 대해서 여러 개의 버전 (버퍼 풀, 언두 로그)를 관리하기 때문에 'MVCC'라고 표현&lt;/span&gt;하며, 필요에 따라서 어떤 데이터가 보여질지 달라지게 됨을 의미한다.&lt;br&gt;&amp;nbsp;&lt;br&gt;이후 커밋이 일어나면, 해당 트랜잭션의 undo 로그가 더 이상 필요하지 않을 때 undo 로그를 제거하게 된다.&amp;nbsp;&lt;br&gt;보통 undo 영역은 일정 크기로 유지되기 때문에 가장 오래된 undo 세그먼트는 제거되거나, 혹은 정책에 의해서 관리된다.&lt;br&gt;&amp;nbsp;&lt;br&gt;만약, 위 상황에서 롤백이 일어난다면 어떻게 될까?&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1808&quot; data-origin-height=&quot;904&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bczJSl/btsjsgbSpwH/etB3Qw2wVziYwFzr9dMdRk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bczJSl/btsjsgbSpwH/etB3Qw2wVziYwFzr9dMdRk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bczJSl/btsjsgbSpwH/etB3Qw2wVziYwFzr9dMdRk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbczJSl%2FbtsjsgbSpwH%2FetB3Qw2wVziYwFzr9dMdRk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;774&quot; height=&quot;387&quot; data-origin-width=&quot;1808&quot; data-origin-height=&quot;904&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;위와 같이 undo 영역에 있는 값을 복구하고, undo 영역에 있는 값을 제거하게 된다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot;&gt;&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;4. 잠금 없는 일관된 읽기 (Non-Locking Consistent Read)&lt;/b&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;앞서 말한 MVCC 덕분에 InnoDB의 경우 락 없이도 일관된 읽기를 제공할 수 있다.&lt;br&gt;여기서 말하는 '일관된 읽기'란, 격리 레벨이 READ_UNCOMMITTED / READ_COMMITTED / REPEATABLE_READ일 때 &lt;span style=&quot;color: #EF5369;&quot;&gt;순수한 읽기 작업은 다른 트랜잭션의 변경 작업과 관련 없이 undo 영역의 값을 읽으면서 바로 실행&lt;/span&gt;됨을 의미한다.&lt;br&gt;&amp;nbsp;&lt;br&gt;하지만, 이를 위해 undo 영역의 값을 바로 삭제하지 못하고 계속 유지해야 하기 때문에 트랜잭션의 범위가 커질수록 MySQL의 서버가 느려지거나 문제가 발생할 수 있어서 주의해야 한다. (트랜잭션은 가능한 짧게 가져가는 게 좋다.)&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;  READ UNCOMMITTED&lt;/b&gt;&lt;br&gt;커밋, 롤백 여부와 상관없이 다른 트랜잭션의 값을 읽을 수 있는 격리 레벨.&lt;br&gt;이미 롤백이 되었는데도 해당 값을 읽을 수 있기 때문에 주의해야 한다. (Dirty read)&lt;br&gt;&lt;br&gt;&lt;b&gt;  REPEATABLE_READ&lt;/b&gt;&lt;br&gt;트랜잭션마다 ID를 부여하여, 실행 중인 트랜잭션 ID보다 작은 ID를 가진 트랜잭션의 변경 사항만 읽는 격리 레벨.&lt;br&gt;Undo 영역에 백업된 모든 레코드는 변경을 발생시킨 트랜잭션의 번호를 추가로 관리하고, 특정 트랜잭션보다 앞 번호의 레코드에서만 변경된 내용을 읽도록 만든다. &lt;br&gt;그래서 실행 중인 트랜잭션 중에서 가장 오래된 트랜잭션 번호보다 앞에 있는 Undo 영역의 데이터는 삭제할 수 없다.&lt;br&gt;하지만, 다른 트랜잭션에 의해서 값이 삽입되거나 변경되었을 때 해당 쿼리 전후로 select한 조회 결과가 달라질 수 있다. (Phantom read) -&amp;gt; InnoDB에서는 Next Key Lock을 활용하여 발생하지 않도록 만들었다.&lt;br&gt;&lt;br&gt;&lt;b&gt;  SERIALIZABLE&lt;/b&gt;&lt;br&gt;작업이 시작된 트랜잭션이 가지고 있는 레코드는 다른 트랜잭션에서 접근할 수 없는 격리 레벨.&lt;br&gt;읽기 작업에도 락이 필요하기 때문에 데이터의 정합성은 보장하지만, 처리 성능이 굉장히 떨어진다.&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;- 락에 대해서는 다음에 자세히 다룰 예정이니 이번에는 넘어가자  &lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot;&gt;&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;5. 자동 데드락 감지&lt;/b&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;InnoDB에서는 교착 상태 감지를 위해서&lt;span style=&quot;color: #EF5369;&quot;&gt; 락에 대한 대기 목록을 그래프 (Wait-for List) 형태로 관리&lt;/span&gt;한다.&lt;br&gt;- 여기서 교착 상태(데드락)란, 서로 다른 트랜잭션이 서로 필요한 락을 가지고 있어서 서로 무한하게 대기하고 있는 상태.&lt;br&gt;&amp;nbsp;&lt;br&gt;교착 상태의 경우 트랜잭션이 여러 테이블의 행에 대해 락을 거는 경우 (update나 select for update 구문 등을 통해서) 발생할 수 있는데, 이때 테이블 수준의 락을 위해서 테이블에 대한 잠금 목록을 관리한다. 이때, 두 번째 트랜잭션이 해당 레코드를 업데이트하거나, 이미 락이 걸린 테이블에 대해 다시 락을 획득하려고 하면, &lt;b&gt;InnoDB는 해당 레코드에 대한 락 요청을 대기 목록에 추가&lt;/b&gt;한다. 최종적으로 락을 얻기 위해서는 레코드에 걸려있는 모든 락이 제거되어야만 얻을 수 있게 된다.&lt;br&gt;&amp;nbsp;&lt;br&gt;아래와 같이 information_schema / performance_schema DB를 확인하게 되면 트랜잭션에 대한 상태를 확인할 수 있다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;# 트랜잭션 상태 확인
select * from information_schema.INNODB_TRX;

# 현재 실행 중인 트랜잭션에서 잠금을 획득한 데이터나 유형, 트랜잭션 정보 확인 (MySQL 8.0부터)
# 어떤 트랜잭션의 어떤 데이터가 락이 걸린 상태인지 확인 가능
select * from performance_schema.data_locks;

# 락 대기 요청 목록 확인 (MySQL 8.0부터)
# 어떤 트랜잭션이 다른 트랜잭션의 락을 대기 중인지 확인 가능
select * from performance_schema.data_lock_waits;&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;br&gt;데드락을 줄이기 위해서는 테이블 자체에 락을 거는 것보다는 트랜잭션 자체를 작게 유지하는 것이 좋다. 여러 트랜잭션이나 여러 테이블이 동시에 레코드를 업데이트한다면, 최대한 해당 트랜잭션들이 변경하는 레코드의 순서들을 일관되게 (동일한 순서로 업데이트되도록) 만드는 것이 좋다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size18&quot; style=&quot;text-align: left;&quot;&gt;&lt;b&gt;  데드락 감지 스레드&lt;/b&gt;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;InnoDB의 경우 내부적으로 데드락 감지 스레드를 가지고 있으며, 해당  스레드는 그래프를 정기적으로 검사하여 &lt;b&gt;교착 상태에 빠진 트랜잭션을 찾아 강제로 종료&lt;/b&gt;한다. 이때,&lt;span style=&quot;color: #EF5369;&quot;&gt; Undo 로그의 양이 더 적은 트랜잭션을 롤백 대상으로 설정&lt;/span&gt;하게 된다.&lt;br&gt;&amp;nbsp;&lt;br&gt;참고로, InnoDB 스토리지 엔진이 아닌 MySQL 엔진 위에서 잠금된 테이블 (LOCK TABLES)은 볼 수가 없기 때문에 데드락에 대한 감지가 불확실할 수도 있지만,&lt;b&gt; innodb_table_locks 변수를 활성화하면 레코드 락뿐만 아니라 테이블 락까지 감지가 가능&lt;/b&gt;해진다. (웬만하면 활성화하도록 하자.)&lt;br&gt;&amp;nbsp;&lt;br&gt;하지만, 만약 동시에 처리하는 스레드가 매우 많거나 하나의 트랜잭션에서 가지고 있는 락이 매우 많아진다면 데드락 감지 스레드 역시 느려진다. 이는 데드락 감지 스레드가 잠금 상태가 변경되지 않도록 잠금 목록이 저장되어 있는 테이블에 새로운 락을 걸고 스레드를 찾기 때문에, 데드락 감지 스레드가 느려지면 실제 비즈니스 로직을 처리하는 스레드 역시 락에 걸려서 계속 대기 상태에 빠질 수도 있다.&lt;br&gt;&amp;nbsp;&lt;br&gt;이를 위해서 MySQL 서버는 innodb_deadlock_detect라는 변수를 제공하고 있으며, &lt;span style=&quot;color: #EF5369;&quot;&gt;해당 변수를 OFF로 하면 데드락 감지 스레드가 더 이상 동작하지 않게 된다&lt;/span&gt;. 하지만, InnoDB 스토리지 엔진 내부에서 데드락이 발생하더라도 중재하는 스레드가 없으니 무한정 대기하기 때문에 innodb_lock_wait_timeout 변수를 활성화하여 &lt;b&gt;일정 시간이 지나면 자동으로 요청이 실패하고 에러 메시지를 반환하도록&lt;/b&gt; 만들어야 한다. 즉, 데드락 감지 스레드 자체가 성능에 영향을 끼친다면 해당 스레드를 사용하지 않는 대신, 일종의 ttl을 통해 대기하도록 만드는 전략인 것이다. 기본값은 50초인데, 훨씬 더 낮은 시간을 사용하는 게 좋다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot;&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;6. 자동화된 장애 복구&lt;/b&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;InnoDB에는 손실이나 장애로부터 데이터를 보호하기 위한 매커니즘이 탑재되어 있다.&amp;nbsp;&lt;br&gt;기본적으로 MySQL 서버가 시작될 때 항상 자동 복구를 수행하기 때문에, &lt;span style=&quot;color: #EF5369;&quot;&gt;복구 불가능한 손상이 있다면 자동 복구를 멈추고 MySQL 서버 자체를 종료해&lt;/span&gt;버린다. (이때는 innodb_force_recovery 변수를 통해서 서버를 재시작해야 한다.) 재시작 후 mysqldump를 통해 가능한 데이터를 백업하고 서버와 DB를 재생성하자.&lt;br&gt;&amp;nbsp;&lt;br&gt;아래는 innodb_force_recovery 변수값에 따른 복구 모드 전략이다.&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;- 1 (SRV_FORCE_IGNORE_CORRUPT)&lt;/b&gt;&lt;br&gt;테이블스페이스의 데이터나 인덱스 페이지에서 손상된 부분이 발견되어도 무시하고 재시작. &lt;br&gt;테이블의 데이터 파일이 손상됐다면 사용하자.&lt;br&gt;&lt;br&gt;&lt;b&gt;- 2 (SRV_FORCE_NO_BACKGROUND)&lt;/b&gt;&lt;br&gt;백그라운드 스레드 중 메인 스레드를 시작하지 않고 재시작. &lt;br&gt;이는 메인 스레드가 undo 영역을 주기적으로 비울 때 장애가 발생했다면 사용하자.&lt;br&gt;&lt;br&gt;&lt;b&gt;- 3 (SRV_FORCE_NO_TRX_UNDO)&lt;/b&gt;&lt;br&gt;커밋되지 않고 종료된 트랜잭션은 그대로 남아있도록 재시작. &lt;br&gt;innoDB에서는 트랜잭션 실행 시 롤백에 대비하여 변경 전의 데이터를 undo 영역에 기록하고, 서버 재시작 시 undo 영역의 데이터를 우선적으로 데이터 파일에 적용하는데 보통 커밋되지 않은 데이터는 롤백을 한다. &lt;br&gt;이때 롤백을 하지 않고 재시작을 하는 전략이다.&lt;br&gt;&lt;br&gt;&lt;b&gt;- 4 (SRV_FORCE_NO_IBUF_MERGE)&lt;/b&gt;&lt;br&gt;인서트 버퍼의 내용을 무시하고 강제로 재시작. &lt;br&gt;innoDB는 CUD 작업으로 인해 인덱스가 변경 작업을 바로 처리하거나 인서트 버퍼에 저장해두고 나중에 데이터 파일에 merge하거나, 둘 중 하나를 채택하는데 만약 인서트 버퍼가 손상되었다면 사용하는 전략이다.&amp;nbsp;&lt;br&gt;&lt;br&gt;&lt;b&gt;- 5 (SRV_FORCE_NO_UNDO_LOG_SCAN)&lt;/b&gt;&lt;br&gt;undo 로그를 모두 무시하고 재시작.&amp;nbsp;&lt;br&gt;innoDB에서는 MySQL 재시작 시 undo 영역에 있는 데이터를 이용하여 복구하고, redo 로그를 활용하여 종료 시점이나 장애 시점의 상태를 재현하며, 커밋되지 않은 트랜잭션에서 변경한 작업은 모두 롤백 처리되는데, 이러한 undo 로그가 손상되었을 때 사용하는 전략이다. 단, 이 전략을 사용하면 커밋되지 않은 작업도 모두 커밋된 것처럼 처리되기 때문에 주의해야 한다.&lt;br&gt;&lt;br&gt;&lt;b&gt;- 6 (SRV_FORCE_NO_LOG_REDO)&lt;/b&gt;&lt;br&gt;redo 로그를 모두 무시하고 재시작.&lt;br&gt;커밋되었거나 redo 로그에만 기록되고 데이터 파일에 기록되지 않은 데이터는 모두 무시되며, 마지막 체크포인트 시점에서의 데이터만 남게 된다.&amp;nbsp;&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot;&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;br&gt;다음 포스팅에서는 innoDB의 핵심 부분인 버퍼 풀에 대해서 알아보도록 하자.&lt;/p&gt;</description>
      <category> /Real MySQL 8.0</category>
      <category>InnoDB</category>
      <category>MVCC</category>
      <category>real mysql 8.0</category>
      <category>외래키</category>
      <category>클러스터링</category>
      <author>dolmeng2</author>
      <guid isPermaLink="true">https://cl8d.tistory.com/101</guid>
      <comments>https://cl8d.tistory.com/101#entry101comment</comments>
      <pubDate>Sun, 11 Jun 2023 16:32:44 +0900</pubDate>
    </item>
    <item>
      <title>[Real MySQL 8.0] MySQL 엔진과 스토리지 엔진, 쿼리 실행 처리 순서</title>
      <link>https://cl8d.tistory.com/100</link>
      <description>&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  MySQL 전체 구조&lt;/b&gt;&lt;/h4&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2008&quot; data-origin-height=&quot;1130&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/orQ0j/btsjjC8H1ij/2eO2Yh0bnUuSKOdkqX6k80/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/orQ0j/btsjjC8H1ij/2eO2Yh0bnUuSKOdkqX6k80/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/orQ0j/btsjjC8H1ij/2eO2Yh0bnUuSKOdkqX6k80/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2ForQ0j%2FbtsjjC8H1ij%2F2eO2Yh0bnUuSKOdkqX6k80%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;725&quot; height=&quot;408&quot; data-origin-width=&quot;2008&quot; data-origin-height=&quot;1130&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;MySQL 서버는 크게&lt;span style=&quot;color: #EF5369;&quot;&gt; MySQL 엔진과 스토리지 엔진&lt;/span&gt;으로 나눌 수 있다.&lt;br&gt;&amp;nbsp;&lt;br&gt;MySQL 엔진의 경우 &lt;b&gt;클라이언트로부터 접속하고, 쿼리 요청을 처리&lt;/b&gt;하는 커넥션 핸들러와 SQL 파서, 옵지마이저로 이루어져 있다.&lt;br&gt;쿼리 관련 처리는 이곳에서 하기 때문에, 거의 '두뇌'와 같은 역할을 하면 된다고 생각하자.&lt;br&gt;&amp;nbsp;&lt;br&gt;스토리지 엔진의 경우 데이터를&lt;b&gt; 실제 디스크에 저장하거나, 혹은 디스크에 저장된 데이터를 읽어&lt;/b&gt;오는 작업을 하며 하나의 MySQL 서버는 여러 개의 스토리지 엔진을 사용할 수 있다.&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;MySQL 엔진과 스토리지 엔진은 서로 '&lt;span style=&quot;color: #EF5369;&quot;&gt;핸들러 API&lt;/span&gt;'라는 것을 사용하여 데이터를 주고받으며, 어느 정도의 데이터 작업이 일어났는지 확인하고 싶다면 아래와 같이 명령어를 입력하면 된다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;show global status like 'handler%';&lt;/code&gt;&lt;/pre&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;792&quot; data-origin-height=&quot;888&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cLPvhg/btsjkLKRrAG/EvaEzGvWADapbZQGPSPy81/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cLPvhg/btsjkLKRrAG/EvaEzGvWADapbZQGPSPy81/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cLPvhg/btsjkLKRrAG/EvaEzGvWADapbZQGPSPy81/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcLPvhg%2FbtsjkLKRrAG%2FEvaEzGvWADapbZQGPSPy81%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;392&quot; height=&quot;440&quot; data-origin-width=&quot;792&quot; data-origin-height=&quot;888&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;요렇게 다양한 핸들러 API들을 확인할 수 있다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot;&gt;&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  MySQL Thread&lt;/b&gt;&lt;/h4&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1450&quot; data-origin-height=&quot;940&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sdpNS/btsjpDLDSAO/IHQQsDQJN775zikQZ1pUPK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sdpNS/btsjpDLDSAO/IHQQsDQJN775zikQZ1pUPK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sdpNS/btsjpDLDSAO/IHQQsDQJN775zikQZ1pUPK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsdpNS%2FbtsjpDLDSAO%2FIHQQsDQJN775zikQZ1pUPK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;641&quot; height=&quot;416&quot; data-origin-width=&quot;1450&quot; data-origin-height=&quot;940&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;MySQL 서버는 기본적으로 스레드 기반으로 동작하며,&lt;span style=&quot;color: #EF5369;&quot;&gt; Foreground / background 스레드&lt;/span&gt;로 나누어져 있다.&lt;br&gt;아래와 같이 입력하면 현재 실행 중인 스레드 목록을 확인할 수 있다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;select thread_id, name, type, processlist_user, processlist_host
from performance_schema.threads order by type, thread_id;&lt;/code&gt;&lt;/pre&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2028&quot; data-origin-height=&quot;328&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kYY4G/btsjk69n5xG/sVaDkEWcIWGSikK022HkU0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kYY4G/btsjk69n5xG/sVaDkEWcIWGSikK022HkU0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kYY4G/btsjk69n5xG/sVaDkEWcIWGSikK022HkU0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkYY4G%2Fbtsjk69n5xG%2FsVaDkEWcIWGSikK022HkU0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2028&quot; height=&quot;328&quot; data-origin-width=&quot;2028&quot; data-origin-height=&quot;328&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;엄청 많기는 한데, 현재 나는 38개의 스레드가 실행 중인 것을 볼 수 있었다.&lt;br&gt;여기서 4, 5행을 보면 동일한 스레드 이름을 가진 것을 볼 수 있는데, 이는 여러 스레드가 동일한 작업을 병렬로 처리할 때 나온다.&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1986&quot; data-origin-height=&quot;146&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bOEmA7/btsjmvACsf5/EArRPE9SaIDAUu9knhzIS1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bOEmA7/btsjmvACsf5/EArRPE9SaIDAUu9knhzIS1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bOEmA7/btsjmvACsf5/EArRPE9SaIDAUu9knhzIS1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbOEmA7%2FbtsjmvACsf5%2FEArRPE9SaIDAUu9knhzIS1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1986&quot; height=&quot;146&quot; data-origin-width=&quot;1986&quot; data-origin-height=&quot;146&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;특히 마지막 3개는 foreground로 동작하고 있으며, 이 중에서 thread/sql/one_connection이 실제로 사용자의 요청을 처리하는 foreground 스레드이다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size18&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;  Foreground Thread&lt;/b&gt;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;foreground thread의 경우 &lt;span style=&quot;color: #EF5369;&quot;&gt;최소 MySQL에 접속된 클라이언트 수만큼 존재하며, 클라이언트가 요청하는 쿼리문을 수행&lt;/span&gt;한다.&lt;br&gt;데이터를 MySQL의 데이터 버퍼나 캐시로부터 가져오고, 없으면 직접 디스크에 있는 데이터를 가져오거나 인덱스 파일로부터 파일을 읽어온다. 이때, MyISAM 같은 스토리지 엔진은 디스크 쓰기 작업까지 foreground가 하지만, &lt;b&gt;InnoDB의 경우 데이터 버퍼 / 캐시까지만 관리하고 디스크 쓰기는 background가 처&lt;/b&gt;리한다.&lt;br&gt;&amp;nbsp;&lt;br&gt;실행 후 커넥션 종료 시 해당 스레드는 스레드 캐시로 돌아가는데, 만약 스레드 캐시에 이미 일정 개수 이상의 대기 중인 스레드가 있으면 스레드 캐시에 넣지 않고 스레드 자체를 종료시켜서 스레드 캐시에 존재하는 스레드 수를 관리하는 역할도 한다.&lt;br&gt;보통, 해당 스레드 수는 thread_cache_size라는 시스템 변수로 컨트롤이 가능하다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size18&quot; style=&quot;text-align: left;&quot;&gt;&lt;b&gt;  Background Thread&lt;/b&gt;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;InnoDB 스토리지 엔진의 경우 백그라운드 스레드가 정말 여러 가지를 처리한다.&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style3&quot;&gt;- 인서트 버퍼 (insert buffer) 병합&lt;br&gt;- ⭐️ 디스크에 로그 기록 (Log Thread)&lt;br&gt;- ⭐️ 버퍼 풀 (buffer pool)의 데이터를 디스크에 기록 (Write Thread)&lt;br&gt;- 데이터를 버퍼로 읽어오기&lt;br&gt;- 잠금, 데드락 모니터링&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;여기서 중요한 log / write thread의 경우 innodb_write_io_threads, innodb_read_io_threads 시스템 변수로 스레드 개수를 제어할 수 있다. 쓰기 스레드의 경우 2~4개, 별도의 스토리지 사용 시 디스크를 최적으로 사용할 만큼 설정하는 것이 좋다.&lt;br&gt;&amp;nbsp;&lt;br&gt;추가로, 데이터 쓰기 작업은 지연이 가능하지만 읽기 작업은 지연 처리가 안 되기 때문에 보통 DBMS에서 쓰기 작업은 버퍼링을 통해 일괄 처리하는 기능이 존재한다. 덕분에 CUD 쿼리 작성 시 디스크에 반영될 때까지 완전히 기다릴 필요가 없다. (InnoDB) 하지만, MyISAM에서는 포그라운드 스레드가 쓰기 작업까지 하다 보니까 쓰기 버퍼링 기능을 사용할 수 없다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot;&gt;&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  MySQL Memory Allocation&lt;/b&gt;&lt;/h4&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1426&quot; data-origin-height=&quot;836&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Qb0IQ/btsjlELBKO4/k0aZ6Gj6IxiZazxqPvJWVk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Qb0IQ/btsjlELBKO4/k0aZ6Gj6IxiZazxqPvJWVk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Qb0IQ/btsjlELBKO4/k0aZ6Gj6IxiZazxqPvJWVk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQb0IQ%2FbtsjlELBKO4%2Fk0aZ6Gj6IxiZazxqPvJWVk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;722&quot; height=&quot;423&quot; data-origin-width=&quot;1426&quot; data-origin-height=&quot;836&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;MySQL에서 사용하는 메모리 공간은 크게 &lt;span style=&quot;color: #EF5369;&quot;&gt;글로벌 메모리 영역과 세션 메모리 영역&lt;/span&gt; (=로컬 메모리 영역)으로 구분된다.&lt;br&gt;&amp;nbsp;&lt;br&gt;글로벌 메모리 영역의 경우 MySQL 서버가 시작되면서 OS로부터 할당되며, OS마다 조금씩 정책이 다르다. 글로벌 메모리 영역은 단 하나의 메모리 공간만 할당받으며, 모든 스레드에게 공유된다.&lt;br&gt;&amp;nbsp;&lt;br&gt;반면, 세션 메모리 영역의 경우 스레드당 하나씩 생성되며, 공유되지 않고 사용된다.&lt;br&gt;쿼리의 용도별로 공간이 할당되고, 필요하지 않으면 아예 메모리 공간 자체가 생성되지 않을 수도 있기 때문에 (정렬이나 조인 버퍼 같은 경우) 주의해야 한다.&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size18&quot; style=&quot;text-align: left;&quot;&gt;&lt;b&gt;  MySQL에서의 쿼리 실행 과정&lt;/b&gt;&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;⭐️ SqL 파서 -&amp;gt; SQL 옵티마이저 -&amp;gt; SQL 실행기 -&amp;gt; 데이터의 읽기/쓰기 작업 -&amp;gt; 디스크&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;MySQL에서는 MySQL 엔진에 의해 SQL 실행기까지 실행되고, 마지막으로 디스크에 데이터를 읽거나 쓰는 작업은 스토리지 엔진이 처리한다. 어떤 스토리지 엔진을 사용하든 MySQL 엔진이 실행하는 부분은 대부분 비슷하며, &lt;b&gt;어떤 식으로 디스크에 데이터를 읽고 쓰는지가 스토리지 엔진별로 갈린다&lt;/b&gt;고 생각하면 된다. 이때, 데이터를 읽고 쓰는 작업은 대부분 1건의 레코드 단위로 처리된다.&lt;br&gt;이때, MySQL 엔진은 스토리지 엔진을 조정하기 위해 '핸들러'라는 것을 사용한다. (추후 다시 다룰 예정이다.)&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;SHOW ENGINES;&lt;/code&gt;&lt;/pre&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2022&quot; data-origin-height=&quot;468&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sERTw/btsjkihXSAW/1y3d4Ygp2UFCwrfbIiZ3e0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sERTw/btsjkihXSAW/1y3d4Ygp2UFCwrfbIiZ3e0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sERTw/btsjkihXSAW/1y3d4Ygp2UFCwrfbIiZ3e0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsERTw%2FbtsjkihXSAW%2F1y3d4Ygp2UFCwrfbIiZ3e0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2022&quot; height=&quot;468&quot; data-origin-width=&quot;2022&quot; data-origin-height=&quot;468&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;이거는 MySQL 서버에서 지원하는 스토리지 엔진의 목록이다. 여기서 support 컬럼에는 4가지의 옵션이 있는데, 다음과 같다.&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style3&quot;&gt;- &lt;b&gt;YES&lt;/b&gt;: MySQL 서버에 포함되어 있고, 사용 가능으로 활성화&lt;br&gt;- &lt;b&gt;DEFAULT&lt;/b&gt;: YES와 동일한데 필수 스토리지 엔진임&lt;br&gt;- &lt;b&gt;NO&lt;/b&gt;: MySQL 서버에 포함 X, 사용 시 재빌드 진행해야 함&lt;br&gt;- &lt;b&gt;DISABLED&lt;/b&gt;: MySQL 서버에 포함은 되어 있는데 비활성화&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;br&gt;또한, MySQL 서버에는 여러 플러그인도 있기 때문에 확인하고 싶다면 show plugins으로 확인하면 된다!&lt;br&gt;참고로, MySQL 8.0부터는 컴포넌트 아키텍처라는 것을 지원하는데, 이는 플러그인의 단점을 보완하기 위해 구현되었다.&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style3&quot;&gt;- 플러그인은 오직 MySQL 서버와 인터페이스가 가능하고, 플러그인끼리는 통신이 불가능함&lt;br&gt;- MySQL 서버의 변수나 함수를 직접 호출하기 때문에 안전하지 않음&lt;br&gt;- 상호 의존 관계를 설정할 수 없어서 초기화가 어려움&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;설치된 컴포넌트는 아래와 같은 명령어로 확인이 가능하다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;select * from mysql.component;&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot;&gt;&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  MySQL Query Execution&lt;/b&gt;&lt;/h4&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1856&quot; data-origin-height=&quot;922&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bTkvde/btsjjz5jefH/wPFx7W4dcQHBBp5xR0BdAk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bTkvde/btsjjz5jefH/wPFx7W4dcQHBBp5xR0BdAk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bTkvde/btsjjz5jefH/wPFx7W4dcQHBBp5xR0BdAk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbTkvde%2Fbtsjjz5jefH%2FwPFx7W4dcQHBBp5xR0BdAk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1856&quot; height=&quot;922&quot; data-origin-width=&quot;1856&quot; data-origin-height=&quot;922&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;MySQL에서 쿼리를 실행하는 구조이다. 차례로 따라가보자.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size18&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;  쿼리 파서 (SQL Parser)&lt;/b&gt;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;사용자 요청으로 들어온 &lt;span style=&quot;color: #EF5369;&quot;&gt;쿼리 문장을 Token으로 분리하여 트리 형태의 구조로 만들어내&lt;/span&gt;는 작업을 의미한다.&lt;br&gt;쿼리 문장의 &lt;b&gt;기본적인 문법 오류&lt;/b&gt;는 이 단계에서 발생하며, 이 경우 오류 메시지가 사용자에게 나가게 된다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size18&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;  전처리기&lt;/b&gt;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;파서를 통해 나온 &lt;span style=&quot;color: #EF5369;&quot;&gt;트리를 바탕으로 쿼리 문장에 구조적인 문제&lt;/span&gt;가 있는지 확인한다.&lt;br&gt;테이블 이름이나 컬럼 이름, 내장 함수 존재 여부, 객체의 접근 권한 등을 여기에서 확인한다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size18&quot; style=&quot;text-align: left;&quot;&gt;&lt;b&gt;  옵티마이저&lt;/b&gt;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;쿼리 문장을 &lt;span style=&quot;color: #EF5369;&quot;&gt;가장 저렴한 비용으로 처리하기 위한 최적화&lt;/span&gt;를 진행한다. 매우 중요한 과정이다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size18&quot; style=&quot;text-align: left;&quot;&gt;&lt;b&gt;  실행 엔진&lt;/b&gt;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;핸들러에게 요청해서 &lt;span style=&quot;color: #EF5369;&quot;&gt;받은 결과를 사용자나 또 다른 핸들러의 요청에 대한 입력으로 연결&lt;/span&gt;하는 역할을 수행한다.&lt;br&gt;핸들러는 실제로 일을 처리하는 역할을 수행하고, 실행 엔진이 옵티마이저와 실행기 사이에서 연결하는 역할을 한다.&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;ex) group by 처리 과정&lt;/b&gt;&lt;br&gt;실행 엔진이 핸들러에게 임시 테이블 생성 요청&lt;br&gt;-&amp;gt; 실행 엔진은 where절에 대한 레코드를 읽으라고 핸들러에게 요청&lt;br&gt;-&amp;gt; 읽어온 테이블을 임시 테이블에게 저장하라고 핸들러에게 요청&lt;br&gt;-&amp;gt; 생성된 임시테이블에 대해 필요한 방식으로 데이터를 읽어오라고 핸들러에게 요청&lt;br&gt;-&amp;gt; 최종 결과를 실행 엔진이 사용자나 다른 핸들러에게 넘긴다.&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size18&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;  핸들러 (=스토리지 엔진)&lt;/b&gt;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;실행 엔진의 요청에 따라서 &lt;span style=&quot;color: #EF5369;&quot;&gt;데이터를 디스크로 저장하거나 디스크로부터 읽어오는 역할&lt;/span&gt;을 진행한다.&lt;br&gt;어떤 스토리지 엔진을 가진 테이블을 처리하는지에 따라서 InnoDB 엔진을 사용할지, MyISAM 엔진을 사용할지 (혹은 그외) 채택하게 된다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size18&quot; style=&quot;text-align: left;&quot;&gt;&lt;b&gt;  쿼리 캐시&lt;/b&gt;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;그림에는 없지만, MySQL의 쿼리 캐시는 &lt;span style=&quot;color: #EF5369;&quot;&gt;SQL 실행 결과를 메모리에 캐시하고&lt;/span&gt; 동일한 쿼리가 실행되면 테이블을 읽는 대신에 메모리에 저장된 결과를 바로 반환하는 역할을 한다.&lt;br&gt;하지만, 테이블의 데이터가 변경되면 캐시도 함께 갱신되어야 하기 때문에 변경된 테이블에 관련된 내용은 전부 삭제했어야 했다. 전부 삭제하다 보니 성능상에도 치명적인 문제가 생겨, &lt;b&gt;8.0부터는 쿼리 캐시 기능 자체가 제거&lt;/b&gt;되었다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size18&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;  스레드 풀&lt;/b&gt;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;MySQL 엔터프라이즈 버전에서는 스레드 풀 기능을 지원한다. 여기서는 &lt;a href=&quot;https://www.percona.com/software/mysql-database/percona-server&quot; target=&quot;_blank&quot; title=&quot;Percona Server&quot;&gt;&lt;span&gt;Percona Server&lt;/span&gt;&lt;/a&gt;에서 제공하는 스레드 풀 기능을 살펴보자.&lt;br&gt;스레드 풀의 경우 &lt;span style=&quot;color: #EF5369;&quot;&gt;동시에 실행 중인 스레드를 CPU가 최대한 잘 처리할 수 있는 수준까지 줄여서 빠르게 처리하도록 만드는 기능&lt;/span&gt;이기 때문에, CPU 시간을 잘 확보하지 못한다면 오히려 쿼리 처리가 느려질 수도 있다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;  thread_pool_size&lt;/b&gt;&lt;br&gt;Percona Server에서는 기본적으로 CPU 코어 수만큼 스레드 그룹을 생성하며, thread_pool_size 시스템 변수를 수정하여 조정이 가능하다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;  thread_pool_oversubscribe&lt;/b&gt;&lt;br&gt;만약, 이미 스레드 풀이 처리하는 작업이 있으면 thread_pool_oversubscribe 변수를 통해 얼마나 더 추가로 처리할지 받아들일 수 있다. (물론, 무작정 늘려도 좋지 않다. 그만큼 스케줄링해야 하는 스레드 수가 많아진다.) 기본값은 3이다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;  thread_pool_stall_limit&lt;/b&gt;&lt;br&gt;스레드 그룹 내의 모든 스레드가 일을 처리하고 있다면 해당 스레드 그룹에 새로운 Worker Thread를 추가할지, 아니면 기존 작업이 끝날 때까지 기다릴지 판단해야 한다. 스레드 풀 내의 타이머 스레드는 스레드 그룹의 상태를 주기적으로 체크하여 thread_pool_stall_limit 변수에 정의된 초만큼 작업 스레드가 처리 중인 작업이 끝나지 않았을 때 새로운 스레드를 생성해서 추가하도록 만든다. 당연하게도 이 수는 할당 가능한 전체 스레드 수 (thread_pool_max_threads)를 넘을 수 없다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;- 그렇다면, thread_pool_stall_limit을 0으로 생성해서 바로 스레드를 생성하도록 만드는 게 좋지 않을까?&lt;/b&gt;&lt;br&gt;이것은 아니다. 스레드 풀을 사용하는 의미 자체가 없어지는 것과 동일하다. (계속 새롭게 만드니 풀이 의미가 없어짐)&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;  선순위 / 후순위 큐&lt;/b&gt;&lt;br&gt;Percona Server에서는 선순위 / 후순위 큐를 통해서 &lt;span style=&quot;color: #EF5369;&quot;&gt;특정 트랜잭션이나 쿼리를 우선으로 처리&lt;/span&gt;하도록 만드는 기능도 있다. (thread_pool_high_prio_tickets)&lt;br&gt;&amp;nbsp;&lt;br&gt;이는 &lt;b&gt;동시에 실행되는 쿼리의 수를 제한하며, 열려 있는 트랜잭션의 수를 낮추기 위해 사용&lt;/b&gt;한다.&lt;br&gt;새로운 커넥션이 생성될 때마다 '우선순위 티켓' 같은 것을 할당하여 선순위 큐에 들어갈 수 있도록 만들며, 기본적으로 &lt;b&gt;이미 시작된 커넥션의 경우 선순위 큐에 들어가게 된다&lt;/b&gt;. 만약 모든 커넥션에 대해 이런 스케줄링을 사용하고 싶지 않다면, 변수 값을 0으로 할당하고, 값이 클수록 각각의 트랜잭션은 선순위 큐에 들어갈 기회를 더 많이 얻게 된다.&lt;br&gt;&amp;nbsp;&lt;br&gt;반대로, 모든 워크 스레드 수가 oversubscribe까지 도달하게 된다면 우선순위가 낮은 커넥션을 제한하여서 방지할 수 있다. 이미 시작된 트랜잭션을 우선으로 처리하고, 해당 트랜잭션이 처리될 때까지 새로운 트랜잭션을 시작하지 않아서 스레드 그룹이 과다하게 사용되는 것을 제한하는 것이다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size18&quot; style=&quot;text-align: left;&quot;&gt;&lt;b&gt;  트랜잭션 지원 메타데이터&lt;/b&gt;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;DB 서버에서 테이블의 구조 정보와 스토어드 프로그램 등의 정보를 데이터 딕셔너리 or 메타데이터라고 한다.&lt;br&gt;MySQL 5.7까지는 FRM 파일에 테이블의 구조 정보를 관리했으나, 이는 MySQL 서버가 비정상적으로 종료되거나 생성, 변경 시 트랜잭션을 지원하지 않아서 일관성이 자주 깨지는 현상이 발생하였다.&lt;br&gt;그래서 8.0부터는 &lt;span style=&quot;color: #EF5369;&quot;&gt;모두 InnoDB 테이블에 저장되며, '시스템 테이블' 같은 형태로 관리&lt;/span&gt;된다.&amp;nbsp;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;show databases;&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;456&quot; data-origin-height=&quot;202&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LV1kI/btsjkFjZYsr/JFh3bygn8N9YAyiOv5Pnc1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LV1kI/btsjkFjZYsr/JFh3bygn8N9YAyiOv5Pnc1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LV1kI/btsjkFjZYsr/JFh3bygn8N9YAyiOv5Pnc1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLV1kI%2FbtsjkFjZYsr%2FJFh3bygn8N9YAyiOv5Pnc1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;375&quot; height=&quot;166&quot; data-origin-width=&quot;456&quot; data-origin-height=&quot;202&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;실제로, show databases를 입력했을 때 나오는 'mysql'이라는 DB에 저장되는 것을 확인할 수 있다.&lt;br&gt;(mysql.ibd라는 테이블스페이스에 저장되기 때문에 해당 파일에 대해 관리가 필요하다!)&lt;br&gt;&amp;nbsp;&lt;br&gt;여기서 의문인 점은, use tables를 통해 확인해도 실제 테이블의 구조가 저장된 테이블을 볼 수 없다는 것이다.&lt;br&gt;이는 사용자가 직접 임의로 수정할까봐 조회 불가능하게 만든 것이며, 이 대신에 information_schema DB를 통해서 확인하도록 만들었다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;select * from information_schema.tables;&lt;/code&gt;&lt;/pre&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2786&quot; data-origin-height=&quot;614&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/m5yGo/btsjlZaZcoY/KqK2s1zokvAtybsKN2nl41/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/m5yGo/btsjlZaZcoY/KqK2s1zokvAtybsKN2nl41/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/m5yGo/btsjlZaZcoY/KqK2s1zokvAtybsKN2nl41/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fm5yGo%2FbtsjlZaZcoY%2FKqK2s1zokvAtybsKN2nl41%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2786&quot; height=&quot;614&quot; data-origin-width=&quot;2786&quot; data-origin-height=&quot;614&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;실제로 이렇게 내가 생성했던 테이블의 정보를 조회할 수 있는 것을 확인할 수 있다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;select * from mysql.tables;&lt;/code&gt;&lt;/pre&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1030&quot; data-origin-height=&quot;152&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bUQVo4/btsjjzqJF9g/Ad1IBbZqTCGDb1Ic1vke11/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bUQVo4/btsjjzqJF9g/Ad1IBbZqTCGDb1Ic1vke11/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bUQVo4/btsjjzqJF9g/Ad1IBbZqTCGDb1Ic1vke11/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbUQVo4%2FbtsjjzqJF9g%2FAd1IBbZqTCGDb1Ic1vke11%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;731&quot; height=&quot;108&quot; data-origin-width=&quot;1030&quot; data-origin-height=&quot;152&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;반면에, mysql DB를 통해서 테이블 목록을 확인하려고 보면 위와 같이 권한이 없다는 문구가 뜬다!&lt;br&gt;&amp;nbsp;&lt;br&gt;그렇다면, 이렇게 innoDB 테이블로 관리되면 어떤 것이 좋을까?&lt;br&gt;바로, &lt;b&gt;'원자성'을 만족할 수 있다는 점&lt;/b&gt;이다.&lt;br&gt;테이블로 관리된다는 것은 설정정보가 테이블에 커밋되거나, 혹은 롤백 (Atomicity)되는 것을 보장할 수밖에 없기 때문에 기존처럼 파일로 관리했을 때와 다르게 데이터의 일관성이 깨지는 일이 발생하지 않는 것이다.&lt;br&gt;하지만, innoDB가 아닌 다른 스토리지 엔진을 사용하는 테이블의 경우, 여전히 메타 정보를 SDI 파일으로 관리하기 때문에 유의해야 한다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot;&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;br&gt;다음 포스팅부터 InnoDB 스토리지 엔진에 대해서 알아보도록 하자.&lt;/p&gt;</description>
      <category> /Real MySQL 8.0</category>
      <category>InnoDB</category>
      <category>mysql</category>
      <category>real mysql 8.0</category>
      <category>스토리지엔진</category>
      <author>dolmeng2</author>
      <guid isPermaLink="true">https://cl8d.tistory.com/100</guid>
      <comments>https://cl8d.tistory.com/100#entry100comment</comments>
      <pubDate>Sun, 11 Jun 2023 00:30:27 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] 에러 로깅하기 - Logback을 사용해서 ERROR 레벨만 파일로 로그를 남겨보자!</title>
      <link>https://cl8d.tistory.com/96</link>
      <description>&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;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;style3&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&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;pre id=&quot;code_1685026890800&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;nohup sudo java -jar $JAR_NAME &amp;gt;&amp;gt; $REPOSITORY/deploy.log 2&amp;gt; $REPOSITORY/deploy-err.log &amp;amp;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;nohup &lt;/span&gt;: 터미널을 종료해도 계속 실행하기.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;java -jar $JAR_NAME &lt;/span&gt;: jar 파일 실행하기.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&amp;gt;&amp;gt;&lt;/span&gt; : 출력 리다이렉션. jar의 실행 결과를 파일로 저장하고 싶어! (deploy.log 파일로 저장)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 참고로, &amp;gt;&amp;gt;의 경우 기존의 파일에 내용을 추가하는 것이고 새롭게 덮어씌우고 싶다면 &amp;gt;을 사용하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;⭐️ 2&amp;gt;&lt;/span&gt; : 표준 오류 출력 리다이렉션. jar의 실행 결과 중 '표준 오류'에 대해서 deploy-err.log 저장하기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&amp;amp;&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;나는 여기서 2&amp;gt; $REPOSITORY/deploy-err.log 이 친구 때문에 스프링에서 에러 로그가 발생하면 deploy-err.log로 저장될 것이라고 생각했다.&amp;nbsp;하지만... 아무리 해도 아무것도 저장이 되지 않아서 절망하고 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는, 2&amp;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;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;  표준 오류 출력이 뭘까?&lt;/b&gt;&lt;br /&gt;보통 쉘에서 명령어를 실행하게 되면, 해당 명령어에 대한 출력은 표준 출력 (STDOUT)을 통해 나오게 된다.&lt;br /&gt;하지만, 해당 명령어를 실행하는 도중에 발생하는 오류나 경고, 예외 메시지의 경우 표준 출력 (STDERR)을 통해 나오게 되는데, 예를 들어 파일을 찾을 수 없거나, 잘못된 명령어를 입력하는 것 같은, &lt;span style=&quot;color: #ef5369;&quot;&gt;'명령어를 실행하는 도중'에 발생한 오류&lt;/span&gt;에 대해서 관리하게 된다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 스프링에서 아래와 같이 에러에 대한 로그 핸들링을 하고 있었다.&lt;/p&gt;
&lt;pre id=&quot;code_1685027843168&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@ControllerAdvice
public class ControllerExceptionHandler {

    private final Logger log = LoggerFactory.getLogger(getClass());

    @ExceptionHandler(Exception.class)
    public ResponseEntity&amp;lt;Void&amp;gt; exception(Exception e) {
        log.error(e.getMessage(), e);
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
    }
}&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;여기서 log.error()를 사용하여 출력한 것은 단순히 로깅 라이브러리에서 제공하는 기능을 통해 오류 레벨의 로그를 기록하게 되며, &lt;span style=&quot;color: #ef5369;&quot;&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;/p&gt;
&lt;pre id=&quot;code_1685028075393&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@ControllerAdvice
public class ControllerExceptionHandler {

    private final Logger log = LoggerFactory.getLogger(getClass());

    @ExceptionHandler(Exception.class)
    public ResponseEntity&amp;lt;Void&amp;gt; exception(Exception e) {
        System.err.println(e);
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;The &quot;standard&quot; error output stream. &lt;br /&gt;This stream is already open and ready to accept output data.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;err에 대해서 위와 같이 '표준 에러' 출력 스트림으로 지정한다고 되어 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만... System.out.println을 통해서 로깅 처리를 안 하듯이, 에러 역시 저런 식으로 처리하는 것을 두고 볼 수 없다.&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;size18&quot;&gt;&lt;b&gt;  근데 왜 System.out.println을 쓰면 안 될까?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로깅 라이브러리는 로그 출력 레벨을 통해 개발 환경에 따른 로그 레벨을 적용할 수 있지만, System.out.println은 그런 게 안 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모두 동일한 로그가 출력되다 보니 로컬 환경에서는 debug 레벨부터, 프로덕션 환경에서 warn 레벨부터 볼 수 있도록 조절할 수 없다는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한, 내부적으로 synchorized를 통해 멀티 스레드 환경에서는 락이 걸리기 때문에 성능적으로도 문제가 생긴다고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자세한 건 이 &lt;a title=&quot;레퍼런스&quot; href=&quot;https://hudi.blog/do-not-use-system-out-println-for-logging/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;레퍼런스&lt;/a&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;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  Logback 활용하기&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바로, Logback이라는 친구를 활용하는 것이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Logback은 대표적인 로깅 프레임워크 중 하나로, Log4j를 대체하기 위해서 나왔다고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링부트의 경우 &lt;b&gt;기본적으로 지원하고 있기 때문에 별도의 dependency를 설정해줄 필요가 없어서 간편&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;기본적으로 Logback의 경우 &lt;span style=&quot;color: #ef5369;&quot;&gt;Logger, Appender, Layout&lt;/span&gt;이라는 3가지의 클래스로 구성되어 있다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;Logger&lt;/span&gt;: 로그 메시지에 대한 일종의 컨텍스트로, 애플리케이션이 로그 메시지를 생성하기 위해 상호작용하는 클래스이다.&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;Appender&lt;/span&gt;: 로그 메시지를 배치하는 역할로, Logger는 둘 이상의 Appender를 가질 수 있다.&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;Layout&lt;/span&gt;: 출력될 메시지를 준비하며, 메시지 포매팅을 위한 클래스 생성을 지원한다.&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;또한, '로그 레벨'이라는 개념을 이해해야 하는데, Logback의 로그 레벨은 총 5단계로 구성되어 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Trace - Debug - info - Warn - Error 순이며, 보통 다음과 같은 의미를 가진다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;TRACE&lt;/span&gt;: 애플리케이션 내부의 작은 단위 동작이나 메서드 호출, 변수 값 등에 대한 정보를 모두 포함한다. (엄청 방대한 양의 로그가 쌓이게 된다.) 로컬 개발 환경에서 많이 사용한다.&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;DEBUG&lt;/span&gt;: Trace보다 조금 더 상위 수준의 레벨이며, SQL에 대한 로깅을 진행할 수 있다. (경험상 로컬, 개발 서버까지 debug 레벨을 많이 사용하는 것 같다.)&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;INFO&lt;/span&gt;: 애플리케이션의 주요 동작이나 이벤트, 요청 처리 정보를 포함하여 스프링 부트에서는 디폴트로 INFO 레벨부터 보인다.&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;WARN&lt;/span&gt;: 경고 메시지를 기록하며, 서비스 운영 자체에는 영향이 없지만, 주의가 필요한 부분에 들어간다.&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;ERROR&lt;/span&gt;: 가장 심각한 단계이다. 즉시 조치를 취해야 하는 수준의 레벨이라고 한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 개인적으로 ExceptionHandler에서 처리하는 로깅은 어느 레벨에서 처리해야 할지 고민인데...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개인적으로 클라이언트와 계속 개발하는 단계에서는 ERROR로 핸들링해야 어떤 상황에서 오류가 발생했는지 클라이언트 개발자에게 전해주기 좋을 것 같아서 ExceptionHandler의 로깅 레벨을 ERROR로 설정하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1685186657564&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@ControllerAdvice
public class ControllerExceptionHandler {

    private final Logger log = LoggerFactory.getLogger(getClass());

    @ExceptionHandler(AuthenticationException.class)
    public ResponseEntity&amp;lt;Void&amp;gt; handlerAuthenticationException(AuthenticationException e) {
        log.error(e.getMessage(), e);
        return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
    }

    @ExceptionHandler(CartItemException.IllegalMember.class)
    public ResponseEntity&amp;lt;Void&amp;gt; handleException(CartItemException.IllegalMember e) {
        log.error(e.getMessage(), e);
        return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity&amp;lt;Void&amp;gt; exception(Exception e) {
        log.error(e.getMessage(), e);
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 우선, 발생할 수 있는 모든 예외 상황에 대해서 log.error로 로깅 처리를 진행해주었다.&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;1. 기본적으로&lt;b&gt; info 하위 레벨의 로그는 console에 출력&lt;/b&gt;하기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. ⭐️ 이때, error 레벨의 로그는 &lt;span style=&quot;color: #ef5369;&quot;&gt;콘솔 및 별도의 파일에 함께 출력&lt;/span&gt;하기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. &lt;b&gt;error 레벨의 로그는 날짜별로 관리&lt;/b&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;style3&quot; /&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  Logback  분석하기&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;src/resources 하위에 &lt;span style=&quot;color: #ef5369;&quot;&gt;logback-spring.xml&lt;/span&gt;이라는 파일을 생성한다.&lt;/p&gt;
&lt;pre id=&quot;code_1685186701401&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;configuration&amp;gt;
  &amp;lt;property resource=&quot;logback-variables.properties&quot;/&amp;gt;

  &amp;lt;timestamp key=&quot;ToDay&quot; datePattern=&quot;yyyy-MM-dd&quot;/&amp;gt;

  &amp;lt;appender name=&quot;CONSOLE&quot; class=&quot;ch.qos.logback.core.ConsoleAppender&quot;&amp;gt;
    &amp;lt;layout class=&quot;ch.qos.logback.classic.PatternLayout&quot;&amp;gt;
      &amp;lt;Pattern&amp;gt;
        ${LOG_PATTERN}
      &amp;lt;/Pattern&amp;gt;
    &amp;lt;/layout&amp;gt;
  &amp;lt;/appender&amp;gt;

  &amp;lt;appender name=&quot;FILE&quot; class=&quot;ch.qos.logback.core.rolling.RollingFileAppender&quot;&amp;gt;
    &amp;lt;filter class=&quot;ch.qos.logback.classic.filter.LevelFilter&quot;&amp;gt;
      &amp;lt;level&amp;gt;error&amp;lt;/level&amp;gt;
      &amp;lt;onMatch&amp;gt;ACCEPT&amp;lt;/onMatch&amp;gt;
      &amp;lt;onMismatch&amp;gt;DENY&amp;lt;/onMismatch&amp;gt;
    &amp;lt;/filter&amp;gt;

    &amp;lt;file&amp;gt;${LOG_PATH}/${ToDay}/${LOG_FILE_NAME}.log&amp;lt;/file&amp;gt;
    &amp;lt;rollingPolicy class=&quot;ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy&quot;&amp;gt;
      &amp;lt;fileNamePattern&amp;gt;
        ${LOG_PATH}/%d{yyyy-MM-dd}/${LOG_FILE_NAME}_%i.log
      &amp;lt;/fileNamePattern&amp;gt;
      &amp;lt;maxFileSize&amp;gt;10MB&amp;lt;/maxFileSize&amp;gt;
      &amp;lt;maxHistory&amp;gt;30&amp;lt;/maxHistory&amp;gt;
    &amp;lt;/rollingPolicy&amp;gt;
    &amp;lt;encoder&amp;gt;
      &amp;lt;pattern&amp;gt;${LOG_PATTERN}&amp;lt;/pattern&amp;gt;
    &amp;lt;/encoder&amp;gt;
  &amp;lt;/appender&amp;gt;

  &amp;lt;root level=&quot;INFO&quot;&amp;gt;
    &amp;lt;appender-ref ref=&quot;FILE&quot;/&amp;gt;
    &amp;lt;appender-ref ref=&quot;CONSOLE&quot;/&amp;gt;
  &amp;lt;/root&amp;gt;

&amp;lt;/configuration&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고, logback-variables.properties라는 파일을 생성한다.&lt;/p&gt;
&lt;pre id=&quot;code_1685186774909&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;LOG_PATH=./logs
LOG_FILE_NAME=jwp-shopping-order
LOG_PATTERN=%d{yyyy-MM-dd HH:mm:ss.SSS} [%level] [%thread] [%logger{36}] - %msg%n&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&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;  프로퍼티와 타임스탬프 선언해주기&lt;/b&gt;&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-05-27 오후 8.34.24.png&quot; data-origin-width=&quot;1726&quot; data-origin-height=&quot;568&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/byumul/btshAuX9MFW/WBUiHh9dXS5oQTufILfLKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/byumul/btshAuX9MFW/WBUiHh9dXS5oQTufILfLKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/byumul/btshAuX9MFW/WBUiHh9dXS5oQTufILfLKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbyumul%2FbtshAuX9MFW%2FWBUiHh9dXS5oQTufILfLKk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1726&quot; height=&quot;568&quot; data-filename=&quot;스크린샷 2023-05-27 오후 8.34.24.png&quot; data-origin-width=&quot;1726&quot; data-origin-height=&quot;568&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&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;  콘솔 출력을 위한 로깅 설정 진행하기&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-05-27 오후 8.43.10.png&quot; data-origin-width=&quot;2334&quot; data-origin-height=&quot;638&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/8jmEl/btshAZXVPHD/hAR1HMp8gmGEItKgJBcP0K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/8jmEl/btshAZXVPHD/hAR1HMp8gmGEItKgJBcP0K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/8jmEl/btshAZXVPHD/hAR1HMp8gmGEItKgJBcP0K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F8jmEl%2FbtshAZXVPHD%2FhAR1HMp8gmGEItKgJBcP0K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2334&quot; height=&quot;638&quot; data-filename=&quot;스크린샷 2023-05-27 오후 8.43.10.png&quot; data-origin-width=&quot;2334&quot; data-origin-height=&quot;638&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;다음으로 콘솔 출력에 대한 appender를 정의하였다.&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-05-27 오후 9.18.19.png&quot; data-origin-width=&quot;2568&quot; data-origin-height=&quot;346&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bzvkPK/btshCdnQS6Y/HTti4Ly5qQGUUW9dFqKpkk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bzvkPK/btshCdnQS6Y/HTti4Ly5qQGUUW9dFqKpkk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bzvkPK/btshCdnQS6Y/HTti4Ly5qQGUUW9dFqKpkk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbzvkPK%2FbtshCdnQS6Y%2FHTti4Ly5qQGUUW9dFqKpkk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2568&quot; height=&quot;346&quot; data-filename=&quot;스크린샷 2023-05-27 오후 9.18.19.png&quot; data-origin-width=&quot;2568&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;&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&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;  파일 출력을 위한 로깅 설정 진행하기&lt;/b&gt;&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-05-27 오후 9.23.20.png&quot; data-origin-width=&quot;1904&quot; data-origin-height=&quot;502&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bfxl2m/btshE9S0Bm0/fkd6dEVUdgqTQ8PatMWer1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bfxl2m/btshE9S0Bm0/fkd6dEVUdgqTQ8PatMWer1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bfxl2m/btshE9S0Bm0/fkd6dEVUdgqTQ8PatMWer1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbfxl2m%2FbtshE9S0Bm0%2Ffkd6dEVUdgqTQ8PatMWer1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1904&quot; height=&quot;502&quot; data-filename=&quot;스크린샷 2023-05-27 오후 9.23.20.png&quot; data-origin-width=&quot;1904&quot; data-origin-height=&quot;502&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;콘솔 때와 마찬가지로 appender를 정의해준다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때, 우리는 2번 조건 (error 레벨의 로그는 파일로 저장)을 만족하기 위해 별도의 filter를 정의해주었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 ERROR 레벨이라면 허용, 아니라면 거절하도록 만들어준 부분이다.&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;스크린샷 2023-05-27 오후 9.41.05.png&quot; data-origin-width=&quot;2570&quot; data-origin-height=&quot;964&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NIy60/btshzZR26gW/c7Q7JCQ4wLoill9wPtk0c1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NIy60/btshzZR26gW/c7Q7JCQ4wLoill9wPtk0c1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NIy60/btshzZR26gW/c7Q7JCQ4wLoill9wPtk0c1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNIy60%2FbtshzZR26gW%2Fc7Q7JCQ4wLoill9wPtk0c1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2570&quot; height=&quot;964&quot; data-filename=&quot;스크린샷 2023-05-27 오후 9.41.05.png&quot; data-origin-width=&quot;2570&quot; data-origin-height=&quot;964&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단하게 생각하면 정의한 fileSize와 maxHistory에 따라서 새로운 로그 파일이 생성되도록 정의하는 부분이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고, 새로운 로그파일은 _0, _1이라는 postfix와 함께 계속 생성되게 된다.&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;여기서 f&lt;b&gt;ile과 fileNamePattern을 왜 나누었나&lt;/b&gt; 싶을 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선, rollingPolicy를 사용하기 위해서는 fileNamePattern 옵션을 꼭 넣어줘야 한다. (아니면 오류남)&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;하지만, fileNamePattern만 사용하게 되면 롤링 정책에 의해 새로운 로그 파일이 생성되기 전, &lt;span style=&quot;color: #ef5369;&quot;&gt;첫 번째 파일을 생성할 때 위와 같이 _0라는 postfix가 붙어서&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-05-27 오후 9.43.25.png&quot; data-origin-width=&quot;440&quot; data-origin-height=&quot;88&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cQ03ZR/btshAr1KZgc/hercnRBmcqYABwFKYGkkt0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cQ03ZR/btshAr1KZgc/hercnRBmcqYABwFKYGkkt0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cQ03ZR/btshAr1KZgc/hercnRBmcqYABwFKYGkkt0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcQ03ZR%2FbtshAr1KZgc%2FhercnRBmcqYABwFKYGkkt0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;440&quot; height=&quot;88&quot; data-filename=&quot;스크린샷 2023-05-27 오후 9.43.25.png&quot; data-origin-width=&quot;440&quot; data-origin-height=&quot;88&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예쁨 주도 개발을 해야 하기 때문에 참을 수 없다. (현재 개발 수준에서는 10MB가 채워지려면 엄청난 요청이 들어와야 하기 때문에 롤링 정책이 적용되는 건 거의 보지 못했다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 전역적으로 첫 파일이 생성될 때는 _0을 제거시키기 위해 별도의 file 태그를 통해 정의해주었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-05-27 오후 9.46.08.png&quot; data-origin-width=&quot;422&quot; data-origin-height=&quot;122&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bMx9s0/btshCdnRDi6/TxY3V54E1C937XHhdMT4L1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bMx9s0/btshCdnRDi6/TxY3V54E1C937XHhdMT4L1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bMx9s0/btshCdnRDi6/TxY3V54E1C937XHhdMT4L1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbMx9s0%2FbtshCdnRDi6%2FTxY3V54E1C937XHhdMT4L1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;422&quot; height=&quot;122&quot; data-filename=&quot;스크린샷 2023-05-27 오후 9.46.08.png&quot; data-origin-width=&quot;422&quot; data-origin-height=&quot;122&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아무튼, file로 설정해주면 새로운 로그 파일이 생성될 때 _0을 통해 로그 파일이 생성된다.&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;style3&quot; /&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;  루트 로거에 설정해주기&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;All&amp;nbsp;loggers&amp;nbsp;are descendants of the predefined root logger&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;모든 로그의 경우 사전에 정의된 root logger의 자손&lt;/span&gt;이다.&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-05-27 오후 9.58.45.png&quot; data-origin-width=&quot;1884&quot; data-origin-height=&quot;624&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/byaHc7/btshyVifqR1/AGInFPIQW6KtuO51BYIjW0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/byaHc7/btshyVifqR1/AGInFPIQW6KtuO51BYIjW0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/byaHc7/btshyVifqR1/AGInFPIQW6KtuO51BYIjW0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbyaHc7%2FbtshyVifqR1%2FAGInFPIQW6KtuO51BYIjW0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1884&quot; height=&quot;624&quot; data-filename=&quot;스크린샷 2023-05-27 오후 9.58.45.png&quot; data-origin-width=&quot;1884&quot; data-origin-height=&quot;624&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;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-05-27 오후 9.53.52.png&quot; data-origin-width=&quot;770&quot; data-origin-height=&quot;406&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FcrX0/btshCgZg6nK/21URZrlCAzcx2VCoKmAQu0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FcrX0/btshCgZg6nK/21URZrlCAzcx2VCoKmAQu0/img.png&quot; data-alt=&quot;덩그러니... 나중에 다른 사람들한테 이걸로 몰래 카메라 해주고 싶다  &quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FcrX0/btshCgZg6nK/21URZrlCAzcx2VCoKmAQu0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFcrX0%2FbtshCgZg6nK%2F21URZrlCAzcx2VCoKmAQu0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;541&quot; height=&quot;285&quot; data-filename=&quot;스크린샷 2023-05-27 오후 9.53.52.png&quot; data-origin-width=&quot;770&quot; data-origin-height=&quot;406&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;덩그러니... 나중에 다른 사람들한테 이걸로 몰래 카메라 해주고 싶다  &lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아무튼! 최종적으로 이렇게 진행하고 나면, 다음과 같이 에러 레벨의 로깅에 대해서만 편하게 관리할 수 있게 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-05-28 오후 3.49.49.png&quot; data-origin-width=&quot;2262&quot; data-origin-height=&quot;294&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/brIaRS/btshCcvWPPe/ItEkxeM4Tky6z84dVN7hZK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/brIaRS/btshCcvWPPe/ItEkxeM4Tky6z84dVN7hZK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/brIaRS/btshCcvWPPe/ItEkxeM4Tky6z84dVN7hZK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbrIaRS%2FbtshCcvWPPe%2FItEkxeM4Tky6z84dVN7hZK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2262&quot; height=&quot;294&quot; data-filename=&quot;스크린샷 2023-05-28 오후 3.49.49.png&quot; data-origin-width=&quot;2262&quot; data-origin-height=&quot;294&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이건 콘솔에서 INFO 레벨 이상의 로그가 출력되는 것이고!&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-05-28 오후 3.50.27.png&quot; data-origin-width=&quot;2194&quot; data-origin-height=&quot;408&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/x2JU8/btshAsmoejr/ce0Y5RcNRXL3f40ymgCiN0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/x2JU8/btshAsmoejr/ce0Y5RcNRXL3f40ymgCiN0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/x2JU8/btshAsmoejr/ce0Y5RcNRXL3f40ymgCiN0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fx2JU8%2FbtshAsmoejr%2Fce0Y5RcNRXL3f40ymgCiN0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2194&quot; height=&quot;408&quot; data-filename=&quot;스크린샷 2023-05-28 오후 3.50.27.png&quot; data-origin-width=&quot;2194&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;요렇게 파일로도 확인이 가능하게 된다  &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>개발일지</category>
      <category>Logback</category>
      <category>로깅</category>
      <author>dolmeng2</author>
      <guid isPermaLink="true">https://cl8d.tistory.com/96</guid>
      <comments>https://cl8d.tistory.com/96#entry96comment</comments>
      <pubDate>Mon, 5 Jun 2023 19:36:16 +0900</pubDate>
    </item>
    <item>
      <title>[Jenkins] AWS 인스턴스를 젠킨스로 배포해보기 - 2편</title>
      <link>https://cl8d.tistory.com/93</link>
      <description>&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt; &lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  레파지토리 WebHook 등록하기&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;span style=&quot;color: #ef5369;&quot;&gt;깃허브 레파지토리의 특정 브랜치에 push 이벤트가 발생하면 자동으로 배포&lt;/span&gt;가 일어나게 만들 것이기 때문에, 해당 push 이벤트에 대해서 감지할 수 있도록 'webhook'이라는 친구를 등록해야 한다.&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-05-27 오전 10.44.43.png&quot; data-origin-width=&quot;2366&quot; data-origin-height=&quot;992&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bUAext/btshy64zYlF/kwwttC0xc3atbFbOgo30g0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bUAext/btshy64zYlF/kwwttC0xc3atbFbOgo30g0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bUAext/btshy64zYlF/kwwttC0xc3atbFbOgo30g0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbUAext%2Fbtshy64zYlF%2FkwwttC0xc3atbFbOgo30g0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2366&quot; height=&quot;992&quot; data-filename=&quot;스크린샷 2023-05-27 오전 10.44.43.png&quot; data-origin-width=&quot;2366&quot; data-origin-height=&quot;992&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;등록하고 싶은 레파지토리의 Settings &amp;gt; Webhooks &amp;gt; Add webhook을 클릭한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-05-30 오후 8.14.15.png&quot; data-origin-width=&quot;1628&quot; data-origin-height=&quot;1040&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bCx0vb/btsh030yEkg/vmNYQkKYiqiWmX3i5GtLa0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bCx0vb/btsh030yEkg/vmNYQkKYiqiWmX3i5GtLa0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bCx0vb/btsh030yEkg/vmNYQkKYiqiWmX3i5GtLa0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbCx0vb%2Fbtsh030yEkg%2FvmNYQkKYiqiWmX3i5GtLa0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1628&quot; height=&quot;1040&quot; data-filename=&quot;스크린샷 2023-05-30 오후 8.14.15.png&quot; data-origin-width=&quot;1628&quot; data-origin-height=&quot;1040&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이런 식으로 payload URL과 content-type을 지정해준다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;aws 인스턴스로 띄웠다면 publicIP:8081과 같은 형태가 위 URL에 들어갈 것이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;⭐️ 여기서 제일 중요한 거, &lt;span style=&quot;color: #ef5369;&quot;&gt;끝에 꼭 /github-webhook/ 붙여줘야 한다&lt;/span&gt;... 이거 때문에 1시간 넘게 삽질했다.&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-05-30 오후 8.15.16.png&quot; data-origin-width=&quot;1664&quot; data-origin-height=&quot;392&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CxqCW/btsh34jADda/Zf0DDKV4gCTGcfP8kp7tW0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CxqCW/btsh34jADda/Zf0DDKV4gCTGcfP8kp7tW0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CxqCW/btsh34jADda/Zf0DDKV4gCTGcfP8kp7tW0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCxqCW%2Fbtsh34jADda%2FZf0DDKV4gCTGcfP8kp7tW0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1664&quot; height=&quot;392&quot; data-filename=&quot;스크린샷 2023-05-30 오후 8.15.16.png&quot; data-origin-width=&quot;1664&quot; data-origin-height=&quot;392&quot;/&gt;&lt;/span&gt;&lt;/figure&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;p style=&quot;color: #333333; text-align: start;&quot; 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;style3&quot; /&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 style=&quot;color: #333333; text-align: start;&quot; 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-05-27 오전 11.29.40.png&quot; data-origin-width=&quot;338&quot; data-origin-height=&quot;668&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pHpRh/btshBj9q21V/XEh1yg3Xs2yR0Il2jZh3c0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pHpRh/btshBj9q21V/XEh1yg3Xs2yR0Il2jZh3c0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pHpRh/btshBj9q21V/XEh1yg3Xs2yR0Il2jZh3c0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpHpRh%2FbtshBj9q21V%2FXEh1yg3Xs2yR0Il2jZh3c0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;280&quot; height=&quot;553&quot; data-filename=&quot;스크린샷 2023-05-27 오전 11.29.40.png&quot; data-origin-width=&quot;338&quot; data-origin-height=&quot;668&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;깃허브 계정에 대한 settings에 들어가준다. 그리고 왼쪽에 있는 'Developer Settings'에 들어간다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-05-27 오전 11.31.48.png&quot; data-origin-width=&quot;2202&quot; data-origin-height=&quot;492&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ws5rs/btshAvbsrHa/9JyOxVP9OUPsbcgJPVQ930/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ws5rs/btshAvbsrHa/9JyOxVP9OUPsbcgJPVQ930/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ws5rs/btshAvbsrHa/9JyOxVP9OUPsbcgJPVQ930/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fws5rs%2FbtshAvbsrHa%2F9JyOxVP9OUPsbcgJPVQ930%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2202&quot; height=&quot;492&quot; data-filename=&quot;스크린샷 2023-05-27 오전 11.31.48.png&quot; data-origin-width=&quot;2202&quot; data-origin-height=&quot;492&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Personal access tokens &amp;gt; Tokens (classic) &amp;gt; Generate new token &amp;gt; Generate new token (classic)을 클릭한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-05-27 오전 11.32.58.png&quot; data-origin-width=&quot;1598&quot; data-origin-height=&quot;1380&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbNS4h/btshyPWocrb/SV8BHUwTXtbxdSy319PYW1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbNS4h/btshyPWocrb/SV8BHUwTXtbxdSy319PYW1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbNS4h/btshyPWocrb/SV8BHUwTXtbxdSy319PYW1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbbNS4h%2FbtshyPWocrb%2FSV8BHUwTXtbxdSy319PYW1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;694&quot; height=&quot;599&quot; data-filename=&quot;스크린샷 2023-05-27 오전 11.32.58.png&quot; data-origin-width=&quot;1598&quot; data-origin-height=&quot;1380&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그리고 Select Scopes에서 &lt;span style=&quot;color: #ef5369;&quot;&gt;repo, admin:org, admin:repo_hook&lt;/span&gt; 3가지에 대한 권한을 준다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;간단하게 설명하면 레파지토리에 대한 권한, orgnaization에 대한 권한, 레파지토리 훅(웹훅 같은)에 대한 권한을 주는 것이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;Generate token을 클릭하면 토큰 정보가 나올 텐데, 잊어버리지 않게 다른 곳에 복사&lt;/span&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&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: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  젠킨스 와 깃허브 연동해주기 - 토큰 연동&amp;nbsp;&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; 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-06-01 오후 2.54.49.png&quot; data-origin-width=&quot;1922&quot; data-origin-height=&quot;654&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cgFiCT/btsicO3m8Od/9nOvuQY1srHPlRXebgkdNk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cgFiCT/btsicO3m8Od/9nOvuQY1srHPlRXebgkdNk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cgFiCT/btsicO3m8Od/9nOvuQY1srHPlRXebgkdNk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcgFiCT%2FbtsicO3m8Od%2F9nOvuQY1srHPlRXebgkdNk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1922&quot; height=&quot;654&quot; data-filename=&quot;스크린샷 2023-06-01 오후 2.54.49.png&quot; data-origin-width=&quot;1922&quot; data-origin-height=&quot;654&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;깃허브 대시보드에서 Manage Jenkins &amp;gt; 아래쪽의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;Credentials&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-06-01 오후 2.56.10.png&quot; data-origin-width=&quot;2340&quot; data-origin-height=&quot;468&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dno6Zu/btsielfi1XK/971ia9NR3pKuzT7fP7YQMk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dno6Zu/btsielfi1XK/971ia9NR3pKuzT7fP7YQMk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dno6Zu/btsielfi1XK/971ia9NR3pKuzT7fP7YQMk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdno6Zu%2Fbtsielfi1XK%2F971ia9NR3pKuzT7fP7YQMk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2340&quot; height=&quot;468&quot; data-filename=&quot;스크린샷 2023-06-01 오후 2.56.10.png&quot; data-origin-width=&quot;2340&quot; data-origin-height=&quot;468&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Stores Scope to Jenkin&lt;/b&gt;에서 Domains의 (globals) 클릭. (전역적으로 설정을 등록해줘야 함)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-05-27 오전 11.43.34.png&quot; data-origin-width=&quot;2112&quot; data-origin-height=&quot;492&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/o5ZZ6/btshBkmZkdK/UWgGmjQa8mCfe6nrhkiIAK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/o5ZZ6/btshBkmZkdK/UWgGmjQa8mCfe6nrhkiIAK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/o5ZZ6/btshBkmZkdK/UWgGmjQa8mCfe6nrhkiIAK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fo5ZZ6%2FbtshBkmZkdK%2FUWgGmjQa8mCfe6nrhkiIAK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2112&quot; height=&quot;492&quot; data-filename=&quot;스크린샷 2023-05-27 오전 11.43.34.png&quot; data-origin-width=&quot;2112&quot; data-origin-height=&quot;492&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Add Credentials 클릭. (1편부터 따라했다면 원래 여기에 ssh 키도 있어야 하는데, 이전에 캡쳐한 자료를 가져오느라 이렇게 됐다... ㅎㅎ)&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-05-27 오전 11.46.22.png&quot; data-origin-width=&quot;1532&quot; data-origin-height=&quot;1038&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bGR3Li/btshBl0xgxy/F9BJ9MKM65vF2AOb6OU9Ek/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bGR3Li/btshBl0xgxy/F9BJ9MKM65vF2AOb6OU9Ek/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bGR3Li/btshBl0xgxy/F9BJ9MKM65vF2AOb6OU9Ek/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbGR3Li%2FbtshBl0xgxy%2FF9BJ9MKM65vF2AOb6OU9Ek%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;736&quot; height=&quot;499&quot; data-filename=&quot;스크린샷 2023-05-27 오전 11.46.22.png&quot; data-origin-width=&quot;1532&quot; data-origin-height=&quot;1038&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&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;span style=&quot;color: #ef5369;&quot;&gt;Username with password&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;지정 후,&amp;nbsp;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;깃허브 아이디와 위에서 발급받은 token 정보, 그리고 id를 지정&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-06-01 오후 3.03.41.png&quot; data-origin-width=&quot;2506&quot; data-origin-height=&quot;602&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b1kswX/btsiekHzFrZ/uUzQSbaNgDkWg88F87WBFK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b1kswX/btsiekHzFrZ/uUzQSbaNgDkWg88F87WBFK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b1kswX/btsiekHzFrZ/uUzQSbaNgDkWg88F87WBFK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb1kswX%2FbtsiekHzFrZ%2FuUzQSbaNgDkWg88F87WBFK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2506&quot; height=&quot;602&quot; data-filename=&quot;스크린샷 2023-06-01 오후 3.03.41.png&quot; data-origin-width=&quot;2506&quot; data-origin-height=&quot;602&quot;/&gt;&lt;/span&gt;&lt;/figure&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;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;  여기서 secret-id는 지금 신경쓰지 않아도 된다. 바로 아래에서 진행할 예정이기 때문이다 ㅎㅎ&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-05-27 오전 11.26.50.png&quot; data-origin-width=&quot;2130&quot; data-origin-height=&quot;782&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/blvfhE/btshzkICVKa/e7MCqtiP2HekK6dtTznI5k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/blvfhE/btshzkICVKa/e7MCqtiP2HekK6dtTznI5k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/blvfhE/btshzkICVKa/e7MCqtiP2HekK6dtTznI5k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FblvfhE%2FbtshzkICVKa%2Fe7MCqtiP2HekK6dtTznI5k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2130&quot; height=&quot;782&quot; data-filename=&quot;스크린샷 2023-05-27 오전 11.26.50.png&quot; data-origin-width=&quot;2130&quot; data-origin-height=&quot;782&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;다시 젠킨스 대시보드에서 Manage Jenkins &amp;gt; System을 클릭한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-05-27 오전 11.36.24.png&quot; data-origin-width=&quot;1768&quot; data-origin-height=&quot;912&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cGDxes/btshz1hoZwG/Ago7fwE9ETGNWsIpfiDUa0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cGDxes/btshz1hoZwG/Ago7fwE9ETGNWsIpfiDUa0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cGDxes/btshz1hoZwG/Ago7fwE9ETGNWsIpfiDUa0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcGDxes%2Fbtshz1hoZwG%2FAgo7fwE9ETGNWsIpfiDUa0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;838&quot; height=&quot;432&quot; data-filename=&quot;스크린샷 2023-05-27 오전 11.36.24.png&quot; data-origin-width=&quot;1768&quot; data-origin-height=&quot;912&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;내리다 보면 GitHub라는 게 있을 텐데, Name 지정 후 Credentials 밑에 있는 Add &amp;gt; Jenkins를 클릭해준다.&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-05-27 오후 12.22.58.png&quot; data-origin-width=&quot;1622&quot; data-origin-height=&quot;792&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QQBqf/btshzZc9rq7/c3rZHD0gqhiR1phyS7NEn1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QQBqf/btshzZc9rq7/c3rZHD0gqhiR1phyS7NEn1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QQBqf/btshzZc9rq7/c3rZHD0gqhiR1phyS7NEn1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQQBqf%2FbtshzZc9rq7%2Fc3rZHD0gqhiR1phyS7NEn1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1622&quot; height=&quot;792&quot; data-filename=&quot;스크린샷 2023-05-27 오후 12.22.58.png&quot; data-origin-width=&quot;1622&quot; data-origin-height=&quot;792&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그럼 이런 식으로 또 다시 Credential을 등록하는 과정이 있을 텐데, 여기서 위에서 발급받은 깃허브 token을 또 다시 넣어준다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;앞서 웹훅 등록시, 깃허브 측에서 POST 요청으로 우리의 젠킨스 서버에 요청을 보낸다고 했었는데, 여기서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;인증 정보를 등록하는 과정&lt;/b&gt;이라고 보면 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-05-27 오후 12.28.46.png&quot; data-origin-width=&quot;2214&quot; data-origin-height=&quot;722&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/U5Pg7/btshzC91yIF/CSxmJLxEZEDPWVLICJ3K51/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/U5Pg7/btshzC91yIF/CSxmJLxEZEDPWVLICJ3K51/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/U5Pg7/btshzC91yIF/CSxmJLxEZEDPWVLICJ3K51/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FU5Pg7%2FbtshzC91yIF%2FCSxmJLxEZEDPWVLICJ3K51%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2214&quot; height=&quot;722&quot; data-filename=&quot;스크린샷 2023-05-27 오후 12.28.46.png&quot; data-origin-width=&quot;2214&quot; data-origin-height=&quot;722&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;Credentials에 위에서 지정한 id 값을 선택해주고, Test Connection이 성공하는지 확인한다.&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&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: #000000; text-align: start;&quot; 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-05-27 오후 12.00.00.png&quot; data-origin-width=&quot;1820&quot; data-origin-height=&quot;786&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nirga/btshzkBTceN/4AA8Pa7WBJK2kwPT90A7K0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nirga/btshzkBTceN/4AA8Pa7WBJK2kwPT90A7K0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nirga/btshzkBTceN/4AA8Pa7WBJK2kwPT90A7K0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fnirga%2FbtshzkBTceN%2F4AA8Pa7WBJK2kwPT90A7K0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1820&quot; height=&quot;786&quot; data-filename=&quot;스크린샷 2023-05-27 오후 12.00.00.png&quot; data-origin-width=&quot;1820&quot; data-origin-height=&quot;786&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;다시 대시보드로 돌아와서, 이번에는 New Item을 클릭한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-05-27 오후 12.01.56.png&quot; data-origin-width=&quot;1324&quot; data-origin-height=&quot;868&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7EcEh/btshCc3lavL/Huz2ktvi8fKOKkwx3QkhKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7EcEh/btshCc3lavL/Huz2ktvi8fKOKkwx3QkhKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7EcEh/btshCc3lavL/Huz2ktvi8fKOKkwx3QkhKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7EcEh%2FbtshCc3lavL%2FHuz2ktvi8fKOKkwx3QkhKk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;446&quot; data-filename=&quot;스크린샷 2023-05-27 오후 12.01.56.png&quot; data-origin-width=&quot;1324&quot; data-origin-height=&quot;868&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;아이템 이름은 자유롭게 설정해주고, Pipeline을 선택한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-05-27 오후 12.04.23.png&quot; data-origin-width=&quot;1570&quot; data-origin-height=&quot;872&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/M91G3/btshAq9k899/W58k233DF5BkK3IE2milMK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/M91G3/btshAq9k899/W58k233DF5BkK3IE2milMK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/M91G3/btshAq9k899/W58k233DF5BkK3IE2milMK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FM91G3%2FbtshAq9k899%2FW58k233DF5BkK3IE2milMK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1570&quot; height=&quot;872&quot; data-filename=&quot;스크린샷 2023-05-27 오후 12.04.23.png&quot; data-origin-width=&quot;1570&quot; data-origin-height=&quot;872&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Github project, Build Triggers &amp;gt; Github hook trigger for GITscm polling을 체크한다.&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&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;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;스크린샷 2023-05-30 오후 8.21.38.png&quot; data-origin-width=&quot;1838&quot; data-origin-height=&quot;250&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cAkR52/btsh23SZHdG/RgWJPhqioXvEoSgNzWASPk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cAkR52/btsh23SZHdG/RgWJPhqioXvEoSgNzWASPk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cAkR52/btsh23SZHdG/RgWJPhqioXvEoSgNzWASPk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcAkR52%2Fbtsh23SZHdG%2FRgWJPhqioXvEoSgNzWASPk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1838&quot; height=&quot;250&quot; data-filename=&quot;스크린샷 2023-05-30 오후 8.21.38.png&quot; data-origin-width=&quot;1838&quot; data-origin-height=&quot;250&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선, 현재 나의 EC2 저장소의 구조이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1685445513960&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;pipeline {
    agent any
    tools {
        gradle 'gradle'
    }
    stages {
        stage('Git Clone') {
            steps {
                git branch: '{branch_name}', credentialsId: 'secret-id-github', url: '{Git Repository URL}'
            }
        }
        stage('Build') {
            steps {
                sh &quot;./gradlew clean build&quot;
            }
        }
        stage('Deploy') {
            steps {
                sshagent(credentials: ['journey-shop-ec2']) {
                    sh '''
                        ssh -o StrictHostKeyChecking=no ${DEPLOY_HOST} uptime
                        scp build/libs/jwp-shopping-order.jar ${DEPLOY_HOST}:/home/ubuntu/jwp-shopping-order
                        ssh -t ${DEPLOY_HOST} /home/ubuntu/jwp-shopping-order/deploy.sh
                    '''
                }
            }
        }
    }
    environment {
        DEPLOY_HOST = '{EC2 접속 사용자 이름}@{EC2 private IP}'
    }
}&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;원하는 깃 레파지토리의 브랜치에서 clone을 한 다음, gradlew를 통해 빌드를 진행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 배포를 시작하는데, ssh agent를 사용한 덕분에 위와 같이 ssh 명령어를 사용할 수 있게 된다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;1. 원격 호스트에 대해서 SSH 연결 후 uptime을 통해 실행 시간 정보를 표시한다. 이때, 호스트의 공개 키를 검사하지 않고 SSH 연결을 진행한다.&lt;br /&gt;2. 위에서 빌드된 build/libs/jwp-shopping-order.jar 파일을 지정한 경로로 복사한다.&lt;br /&gt;3. ssh 연결 후 deploy.sh 스크립트를 실행한다. (배포)&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;⭐️ 이때, 원격 호스트 정보는 이미&lt;span style=&quot;color: #ef5369;&quot;&gt; 젠킨스 자체가 EC2 위에 띄워져 있기 때문에 private IP로 접속&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;/p&gt;
&lt;pre id=&quot;code_1685445943771&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#!/bin/bash
REPOSITORY=/home/ubuntu/jwp-shopping-order
PROJECT_NAME=jwp-shopping-order
cd $REPOSITORY/$PROJECT_NAME/
echo &quot;&amp;gt; 현재 구동중인 애플리케이션 pid 확인&quot;
CURRENT_PID=$(sudo lsof -i tcp:8080 | awk 'NR!=1 {print$2}')
echo &quot;현재 구동중인 애플리케이션pid: $CURRENT_PID&quot;
if [ -z &quot;$CURRENT_PID&quot; ]; then
    echo &quot;&amp;gt; 현재 구동중인 애플리케이션이 없으므로 종료하지 않습니다.&quot;
else
    echo &quot;&amp;gt; kill -9 $CURRENT_PID&quot;
    sudo kill -9 $CURRENT_PID
    sleep 5
fi
echo &quot;&amp;gt; 새 애플리케이션 배포&quot;
JAR_NAME=$(ls -tr $REPOSITORY/*.jar | tail -n 1)
echo &quot;&amp;gt; JAR Name: $JAR_NAME&quot;
rm -rf $REPOSITORY/deploy.log $REPOSITORY/deploy-err.log
nohup sudo java -jar $JAR_NAME --spring.profiles.active=prod --spring.config.location=$REPOSITORY/application.yml &amp;gt;&amp;gt; $REPOSITORY/deploy.log 2&amp;gt; $REPOSITORY/deploy-err.log &amp;amp;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그냥 이동하고, 8080으로 띄워진 애플리케이션 확인 후 기존에 돌아가는 것은 제거한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새롭게 빌드하면 기존의 로그 파일은 제거한 다음, cp로 인해 복사된 파일을 java-jar를 통해서 실행해준다 ㅎㅎ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;config 정보의 경우 외부로 노출시키지 않기 위해서 인스턴스 내부에 저장해두고 사용하고 있다  &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;pre id=&quot;code_1685446123030&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#!/bin/bash
REPOSITORY=/home/ubuntu/jwp-shopping-order
PROJECT_NAME=jwp-shopping-order
cd $REPOSITORY/$PROJECT_NAME/
echo &quot;&amp;gt; Git Pull&quot;
git pull origin step1
echo &quot;&amp;gt; 현재 구동중인 애플리케이션 pid 확인&quot;
CURRENT_PID=$(sudo lsof -i tcp:8080 | awk 'NR!=1 {print$2}')
echo &quot;현재 구동중인 애플리케이션pid: $CURRENT_PID&quot;
if [ -z &quot;$CURRENT_PID&quot; ]; then
    echo &quot;&amp;gt; 현재 구동중인 애플리케이션이 없으므로 종료하지 않습니다.&quot;
else
    echo &quot;&amp;gt; kill -9 $CURRENT_PID&quot;
    sudo kill -9 $CURRENT_PID
    sleep 5
fi
echo &quot;&amp;gt; 프로젝트 Build 시작&quot;
./gradlew build -x test
echo &quot;&amp;gt; Build 파일 복사&quot;
cp $REPOSITORY/$PROJECT_NAME/build/libs/*.jar $REPOSITORY/
echo &quot;&amp;gt; 새 애플리케이션 배포&quot;
JAR_NAME=$(ls -tr $REPOSITORY/*.jar | tail -n 1)
echo &quot;&amp;gt; JAR Name: $JAR_NAME&quot;
rm -rf $REPOSITORY/deploy.log $REPOSITORY/deploy-err.log
nohup sudo java -jar --Dspring.profiles.active=prod $JAR_NAME --spring.config.location=$REPOSITORY/application.yml &amp;gt;&amp;gt; $REPOSITORY/deploy.log 2&amp;gt; $REPOSITORY/deploy-err.log &amp;amp;&lt;/code&gt;&lt;/pre&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;스크린샷 2023-05-30 오후 8.23.29.png&quot; data-origin-width=&quot;1630&quot; data-origin-height=&quot;538&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bONjOi/btshVAkiZQK/qkkHLpZ1jXiVy3xrwc7AGk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bONjOi/btshVAkiZQK/qkkHLpZ1jXiVy3xrwc7AGk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bONjOi/btshVAkiZQK/qkkHLpZ1jXiVy3xrwc7AGk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbONjOi%2FbtshVAkiZQK%2FqkkHLpZ1jXiVy3xrwc7AGk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1630&quot; height=&quot;538&quot; data-filename=&quot;스크린샷 2023-05-30 오후 8.23.29.png&quot; data-origin-width=&quot;1630&quot; data-origin-height=&quot;538&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-05-30 오후 8.27.02.png&quot; data-origin-width=&quot;2482&quot; data-origin-height=&quot;804&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bdEFyP/btsh2sMhJWj/goTvDW4whkIKwFrhhV1p40/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bdEFyP/btsh2sMhJWj/goTvDW4whkIKwFrhhV1p40/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bdEFyP/btsh2sMhJWj/goTvDW4whkIKwFrhhV1p40/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbdEFyP%2Fbtsh2sMhJWj%2FgoTvDW4whkIKwFrhhV1p40%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2482&quot; height=&quot;804&quot; data-filename=&quot;스크린샷 2023-05-30 오후 8.27.02.png&quot; data-origin-width=&quot;2482&quot; data-origin-height=&quot;804&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종적으로 브랜치에 push 이벤트가 발생하게 되면, 젠킨스 파이프라인이 돌면서 위의 스크립트를 실행하여 배포에 성공하게 된다!!&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;style3&quot; /&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; &amp;zwj;♀️&lt;/p&gt;</description>
      <category>개발일지</category>
      <author>dolmeng2</author>
      <guid isPermaLink="true">https://cl8d.tistory.com/93</guid>
      <comments>https://cl8d.tistory.com/93#entry93comment</comments>
      <pubDate>Tue, 30 May 2023 20:32:33 +0900</pubDate>
    </item>
    <item>
      <title>[Jenkins] AWS 인스턴스를 젠킨스로 배포해보기 - 1편</title>
      <link>https://cl8d.tistory.com/95</link>
      <description>&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt; &lt;b&gt;&amp;nbsp;들어가기 전&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;이번 미션에서 젠킨스를 통해 CI / CD를 구축해보고 싶어서 ⭐️&lt;b&gt;베베 선생님&lt;span style=&quot;color: #555555;&quot;&gt;⭐️&lt;/span&gt;&lt;/b&gt;의 힘을 빌려서 한 번 진행해보았다.&lt;br /&gt;나는 t4g.micro를 사용하다 보니 램이 1기가밖에 안 되어서 swap을 해주었어야 했는데,&lt;br /&gt;⭐️ &lt;span style=&quot;color: #ef5369;&quot;&gt;중요한 건 swap 시 1~1.5기가 정도만 해야 한다는 것&lt;/span&gt;이었다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;나는 2기가로 해서 지금 Use가 95%로 간당간당하다.&lt;br /&gt;원래 안 돼서 로컬로 실행하고 난리치다가 다시 시도했는데 되기는 했다...&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;664&quot; data-origin-height=&quot;228&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bnObiU/btsh32MOqEA/Xep8QUwC5e0xr80at0V4nk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bnObiU/btsh32MOqEA/Xep8QUwC5e0xr80at0V4nk/img.png&quot; data-alt=&quot;죽어가는 친구&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bnObiU/btsh32MOqEA/Xep8QUwC5e0xr80at0V4nk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbnObiU%2Fbtsh32MOqEA%2FXep8QUwC5e0xr80at0V4nk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;664&quot; height=&quot;228&quot; data-origin-width=&quot;664&quot; data-origin-height=&quot;228&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;죽어가는 친구&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;1.5기가로 만들고 싶다면 아래와 같이 실행해주면 된다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# 기존에 존재하는 /swapfile을 비활성화하기
sudo swapoff /swapfile

# 크기가 1.5기가인 /swapfile 생성하기
sudo fallocate -l 1.5G /swapfile

# /swapfile에 대한 파일의 권한 변경하기
sudo chmod 600 /swapfile

# swapfile을 추가하기 위한 swap 공간을 생성한다.
sudo mkswap /swapfile

# swapfile을 swap 메모리에 추가한다.
sudo swapon /swapfile

# /esc/fstab 파일에 파일 시스템 마운트 시 필요한 구성 파일을 수정한다.
sudo vim /etc/fstab

# 파일의 맨 끝에 /swapfile을 스왑 영역으로 마운트하기 위한 옵션을 추가한다.
# 마운트할 파일의 경우 + 파일 시스템 유형 지정 (none) + 마운트할 파일의 유형 지정 (swap) + 마운트 옵션 (sw, 스왑 영역을 의미함)
# 0 (파일 시스템이 백업되어야 하는지 여부, 여기서는 백업 X) + 0 (파일 시스템의 검사 순서 지정) - 검사하지 않음
/swapfile none swap sw 0 0

# 시스템 재부팅
sudo reboot&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;이후 free를 통해 swap이 잘 되었는지 확인하도록 하자.&lt;/p&gt;
&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt; &lt;b&gt; 도커로 젠킨스 실행하기&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;  참고로, 여기 아래에서 나오는 이미지들은 로컬 젠킨스로 시도했을 때 캡쳐했던 것들이어서 맥 환경입니다! (추후 EC2 환경에서 함)&lt;/b&gt;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;먼저, 준비물로 도커가 필요하다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;sudo snap install docker
sudo apt install docker.io&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;docker.io를 실행할 때 경고 메시지가 떠도 괜찮다. 정상적으로 설치되었음을 의미한다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;잘 설치되었는지 체크하기 위해 도커 버전을 확인해주자.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;docker --version&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;526&quot; data-origin-height=&quot;84&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/TMWzv/btshy6jrorJ/yR0p9TL1z8WTH38hkDctrk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/TMWzv/btshy6jrorJ/yR0p9TL1z8WTH38hkDctrk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/TMWzv/btshy6jrorJ/yR0p9TL1z8WTH38hkDctrk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FTMWzv%2Fbtshy6jrorJ%2FyR0p9TL1z8WTH38hkDctrk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;526&quot; height=&quot;84&quot; data-origin-width=&quot;526&quot; data-origin-height=&quot;84&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;그리고, 젠킨스를 실행해주기 위한 docker-compose 파일을 작성해주자.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;version: &quot;3.9&quot;
services:
&amp;nbsp;&amp;nbsp;jenkins:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;image: jenkins/jenkins
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;restart: always
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ports:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- &quot;8081:8080&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;간단하게 진행하기 위해서 이미지와 포트 옵션 외에 별다른 것은 진행하지 않았다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;이때, 포트 옵션을 8081로 두었는데, &lt;span style=&quot;color: #ef5369;&quot;&gt;EC2 인스턴스의 inbound rules에서 허용된 포트 주소&lt;/span&gt;여야 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1552&quot; data-origin-height=&quot;900&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dZSznB/btshBkU0f3P/OsT4IyPQPQwx1Iz4mA6QE1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dZSznB/btshBkU0f3P/OsT4IyPQPQwx1Iz4mA6QE1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dZSznB/btshBkU0f3P/OsT4IyPQPQwx1Iz4mA6QE1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdZSznB%2FbtshBkU0f3P%2FOsT4IyPQPQwx1Iz4mA6QE1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;738&quot; height=&quot;428&quot; data-origin-width=&quot;1552&quot; data-origin-height=&quot;900&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;나는 여기서 사용할 수 있는 게 8081밖에 없는 상황이었어서 8081 포트로 진행하였다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;이후, docker-compose 파일을 실행하여 도커 컨테이너를 실행하자.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;docker-compose up -d&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1050&quot; data-origin-height=&quot;124&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bIMAZm/btshChjuzM2/VscA68yKEQZMxmplmwo2oK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bIMAZm/btshChjuzM2/VscA68yKEQZMxmplmwo2oK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bIMAZm/btshChjuzM2/VscA68yKEQZMxmplmwo2oK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbIMAZm%2FbtshChjuzM2%2FVscA68yKEQZMxmplmwo2oK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1050&quot; height=&quot;124&quot; data-origin-width=&quot;1050&quot; data-origin-height=&quot;124&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;참고로 나는 이미 이미지가 다운받아진 상태여서 이렇게만 뜨지만, 처음이라면 젠킨스 도커 이미지를 다운받느라 조금 걸릴 것이다.&lt;br /&gt;(위에 뜨는 경고 메시지는 현재 실행하지 않는 도커 컨테이너가 있어서 뜬 오류인데, 신경쓰지 않아도 된다!)&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;docker ps -a&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2332&quot; data-origin-height=&quot;96&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ci7LV6/btshzmzQRkr/inex7ZYMa58AxihKrh8fW1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ci7LV6/btshzmzQRkr/inex7ZYMa58AxihKrh8fW1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ci7LV6/btshzmzQRkr/inex7ZYMa58AxihKrh8fW1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fci7LV6%2FbtshzmzQRkr%2Finex7ZYMa58AxihKrh8fW1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2332&quot; height=&quot;96&quot; data-origin-width=&quot;2332&quot; data-origin-height=&quot;96&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;이후, 잘 실행되었는지 확인하기 위해 STATUS에&lt;span style=&quot;color: #ef5369;&quot;&gt; UP xx minutes&lt;/span&gt;가 뜨는지 확인하자.&lt;br /&gt;&amp;nbsp;&lt;br /&gt; EC2에서 진행하기 위해서는 매번 sudo를 입력하고 있다면, 번거로우니까 아래와 같이 docker 실행에 대한 권한을 주자.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;sudo chmod 666 /var/run/docker.sock&lt;/blockquote&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;이후 재시작을 하게 되면 sudo 없이 편하게 실행할 수 있을 것이다  &lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt; &lt;b&gt;&amp;nbsp;도커로 젠킨스 접속하기&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2026&quot; data-origin-height=&quot;860&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mXn27/btshCflEq5F/zUap8loYOmWxklH9bPpWvk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mXn27/btshCflEq5F/zUap8loYOmWxklH9bPpWvk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mXn27/btshCflEq5F/zUap8loYOmWxklH9bPpWvk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmXn27%2FbtshCflEq5F%2FzUap8loYOmWxklH9bPpWvk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;735&quot; height=&quot;312&quot; data-origin-width=&quot;2026&quot; data-origin-height=&quot;860&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;publicIP:8081로 접속했다면 위와 같은 창이 뜰 것이다.&lt;br /&gt;가장 첫 접속이기 때문에 비밀번호를 입력해야 한다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;docker logs {container_id}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;터미널에 docker logs + 젠킨스가 띄워진 도커 컨테이너의 아이디를 입력해보자. 그럼 아래와 같은 창이 뜰 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1410&quot; data-origin-height=&quot;444&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b5dD2i/btshy8IjWzA/FSHYy8wKwIInvCM2Kt8Kuk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b5dD2i/btshy8IjWzA/FSHYy8wKwIInvCM2Kt8Kuk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b5dD2i/btshy8IjWzA/FSHYy8wKwIInvCM2Kt8Kuk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb5dD2i%2Fbtshy8IjWzA%2FFSHYy8wKwIInvCM2Kt8Kuk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1410&quot; height=&quot;444&quot; data-origin-width=&quot;1410&quot; data-origin-height=&quot;444&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;딱 봐도 비밀번호처럼 생긴 친구가 있다.&lt;br /&gt;이 친구를 복사해서 위의 Administrator password에 입력해주자.&lt;br /&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;1714&quot; data-origin-height=&quot;806&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ljQyj/btshzBDqWXL/GSkF6BHPnQEGdEncWQrZb0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ljQyj/btshzBDqWXL/GSkF6BHPnQEGdEncWQrZb0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ljQyj/btshzBDqWXL/GSkF6BHPnQEGdEncWQrZb0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FljQyj%2FbtshzBDqWXL%2FGSkF6BHPnQEGdEncWQrZb0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;662&quot; height=&quot;311&quot; data-origin-width=&quot;1714&quot; data-origin-height=&quot;806&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;그럼 여기서 install suggested plugins를 눌러준다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1936&quot; data-origin-height=&quot;1026&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/PReg0/btshE1UZhUW/i2Wdr3A4tkGlh0HiZTVwFk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/PReg0/btshE1UZhUW/i2Wdr3A4tkGlh0HiZTVwFk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PReg0/btshE1UZhUW/i2Wdr3A4tkGlh0HiZTVwFk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPReg0%2FbtshE1UZhUW%2Fi2Wdr3A4tkGlh0HiZTVwFk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;668&quot; height=&quot;354&quot; data-origin-width=&quot;1936&quot; data-origin-height=&quot;1026&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;위와 같이 열심히 실행되고 있을 텐데, 설치에는 5~10분 정도 걸리기 때문에 여유롭게 기다리자!&lt;br /&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;1966&quot; data-origin-height=&quot;1374&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjse8E/btshBiQqIem/t9aD8M2YNJbnT59mYZXRz1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjse8E/btshBiQqIem/t9aD8M2YNJbnT59mYZXRz1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjse8E/btshBiQqIem/t9aD8M2YNJbnT59mYZXRz1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbjse8E%2FbtshBiQqIem%2Ft9aD8M2YNJbnT59mYZXRz1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;645&quot; height=&quot;451&quot; data-origin-width=&quot;1966&quot; data-origin-height=&quot;1374&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;이후, 어드민 계정으로 로그인하기 위한 정보를 입력해준다.&lt;br /&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;992&quot; data-origin-height=&quot;796&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RR7Ip/btshCbKhSQD/v6kSVHSQT9BowF75EuBViK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RR7Ip/btshCbKhSQD/v6kSVHSQT9BowF75EuBViK/img.png&quot; data-alt=&quot;원래는 localhost가 아니라 EC2 Public IP:8081 같은 형태여야 한다. (옛날에 캡쳐함...)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RR7Ip/btshCbKhSQD/v6kSVHSQT9BowF75EuBViK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRR7Ip%2FbtshCbKhSQD%2Fv6kSVHSQT9BowF75EuBViK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;487&quot; height=&quot;391&quot; data-origin-width=&quot;992&quot; data-origin-height=&quot;796&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;원래는 localhost가 아니라 EC2 Public IP:8081 같은 형태여야 한다. (옛날에 캡쳐함...)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;Jenkins에 접속하기 위한 URL을 입력한다. (기본적으로 입력되어 있는 상태긴 하다 ㅎㅎ)&lt;br /&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;2552&quot; data-origin-height=&quot;1360&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kE15v/btshE1UZWLf/ykU8hkthk2nuiyNjrM0kO1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kE15v/btshE1UZWLf/ykU8hkthk2nuiyNjrM0kO1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kE15v/btshE1UZWLf/ykU8hkthk2nuiyNjrM0kO1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkE15v%2FbtshE1UZWLf%2FykU8hkthk2nuiyNjrM0kO1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;830&quot; height=&quot;442&quot; data-origin-width=&quot;2552&quot; data-origin-height=&quot;1360&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;그럼 이렇게 Jenkins 화면이 멋지게 나타난다!&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt; &lt;b&gt;&amp;nbsp;젠킨스 플러그인 설치하기 - gradle&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;우리는 gradle로 빌드를 진행할 예정이기 때문에 gradle 관련 플러그인이 필요하다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2152&quot; data-origin-height=&quot;734&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/s1tq2/btshyPoJTik/UNJUnCY4FZMWmdeUKKRVk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/s1tq2/btshyPoJTik/UNJUnCY4FZMWmdeUKKRVk1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/s1tq2/btshyPoJTik/UNJUnCY4FZMWmdeUKKRVk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fs1tq2%2FbtshyPoJTik%2FUNJUnCY4FZMWmdeUKKRVk1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2152&quot; height=&quot;734&quot; data-origin-width=&quot;2152&quot; data-origin-height=&quot;734&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;Dashboard &amp;gt; Manage Jenkins &amp;gt; Tools 클릭!&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2054&quot; data-origin-height=&quot;1174&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cTmV2o/btshAAxlnTr/xRWS4K3pge8PxainRKKY91/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cTmV2o/btshAAxlnTr/xRWS4K3pge8PxainRKKY91/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cTmV2o/btshAAxlnTr/xRWS4K3pge8PxainRKKY91/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcTmV2o%2FbtshAAxlnTr%2FxRWS4K3pge8PxainRKKY91%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2054&quot; height=&quot;1174&quot; data-origin-width=&quot;2054&quot; data-origin-height=&quot;1174&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;Gradle Installations &amp;gt; Version 선택 후 Save 진행하기!&lt;br /&gt;나 같은 경우는 최신이면서 가장 안정화된 gradle 8.1을 설치하였다.&lt;/p&gt;
&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt; &lt;b&gt;&amp;nbsp;젠킨스 플러그인 설치하기 - ssh agent&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;추후 나올 젠킨스 배포 스크립트에서 ssh를 통해 우리의 EC2 인스턴스에 접근을 할 것이다.&lt;br /&gt;이를 위한 ssh agent라는 친구를 설치해보자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2128&quot; data-origin-height=&quot;692&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/l1p6x/btshyPIZGcg/hxVMVpC93fko09oK3lORP1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/l1p6x/btshyPIZGcg/hxVMVpC93fko09oK3lORP1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/l1p6x/btshyPIZGcg/hxVMVpC93fko09oK3lORP1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fl1p6x%2FbtshyPIZGcg%2FhxVMVpC93fko09oK3lORP1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2128&quot; height=&quot;692&quot; data-origin-width=&quot;2128&quot; data-origin-height=&quot;692&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;Dashboard &amp;gt; Manage Jenkins &amp;gt; Plugins 클릭!&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2042&quot; data-origin-height=&quot;572&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dZeyv4/btshAZcukYU/9AL1ncNktSY6UIHx5OqAfK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dZeyv4/btshAZcukYU/9AL1ncNktSY6UIHx5OqAfK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dZeyv4/btshAZcukYU/9AL1ncNktSY6UIHx5OqAfK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdZeyv4%2FbtshAZcukYU%2F9AL1ncNktSY6UIHx5OqAfK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2042&quot; height=&quot;572&quot; data-origin-width=&quot;2042&quot; data-origin-height=&quot;572&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;Available plugins &amp;gt; ssh agent 검색 &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-06-01 오후 2.54.49.png&quot; data-origin-width=&quot;1922&quot; data-origin-height=&quot;654&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bcAOlD/btsicLFCAcy/OHdX3EbUJDZTudAhNioqZk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bcAOlD/btsicLFCAcy/OHdX3EbUJDZTudAhNioqZk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bcAOlD/btsicLFCAcy/OHdX3EbUJDZTudAhNioqZk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbcAOlD%2FbtsicLFCAcy%2FOHdX3EbUJDZTudAhNioqZk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1922&quot; height=&quot;654&quot; data-filename=&quot;스크린샷 2023-06-01 오후 2.54.49.png&quot; data-origin-width=&quot;1922&quot; data-origin-height=&quot;654&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;다시 깃허브 대시보드로 돌아간 다음&lt;/b&gt;, 왼쪽의 Manage Jenkins &amp;gt; 아래쪽의 &lt;span style=&quot;color: #ef5369;&quot;&gt;Credentials&lt;/span&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;스크린샷 2023-06-01 오후 2.56.10.png&quot; data-origin-width=&quot;2340&quot; data-origin-height=&quot;468&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cSrEWq/btsijgEsN2K/b34iBnqWsqVLG9u00rMXk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cSrEWq/btsijgEsN2K/b34iBnqWsqVLG9u00rMXk0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cSrEWq/btsijgEsN2K/b34iBnqWsqVLG9u00rMXk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcSrEWq%2FbtsijgEsN2K%2Fb34iBnqWsqVLG9u00rMXk0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2340&quot; height=&quot;468&quot; data-filename=&quot;스크린샷 2023-06-01 오후 2.56.10.png&quot; data-origin-width=&quot;2340&quot; data-origin-height=&quot;468&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;Stores scoped To Jenkins &amp;gt; Domains &amp;gt; global 클릭!&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-05-27 오전 11.43.34.png&quot; data-origin-width=&quot;2112&quot; data-origin-height=&quot;492&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Ru4oW/btsijLEhz8y/XM5iwYaeIogKFFTuJTPQTK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Ru4oW/btsijLEhz8y/XM5iwYaeIogKFFTuJTPQTK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Ru4oW/btsijLEhz8y/XM5iwYaeIogKFFTuJTPQTK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRu4oW%2FbtsijLEhz8y%2FXM5iwYaeIogKFFTuJTPQTK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2112&quot; height=&quot;492&quot; data-filename=&quot;스크린샷 2023-05-27 오전 11.43.34.png&quot; data-origin-width=&quot;2112&quot; data-origin-height=&quot;492&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Add Credentials 클릭!&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1866&quot; data-origin-height=&quot;938&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cepAD7/btshzDuxmZR/T0wWin0hvwbnSBNk1kX4Z0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cepAD7/btshzDuxmZR/T0wWin0hvwbnSBNk1kX4Z0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cepAD7/btshzDuxmZR/T0wWin0hvwbnSBNk1kX4Z0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcepAD7%2FbtshzDuxmZR%2FT0wWin0hvwbnSBNk1kX4Z0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1866&quot; height=&quot;938&quot; data-origin-width=&quot;1866&quot; data-origin-height=&quot;938&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;SSH-Username with private key 선택 후, ID는 아무거나, Username은 EC2 호스트 네임을, Private key에는 EC2 인스턴스 접속을 위한 pem 키를 입력한다. (cat xx.pem을 통해 나온 내용을 복사 붙여넣기 해준다.)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2562&quot; data-origin-height=&quot;550&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kCJV1/btshA4Suy1Y/nZpaZabQUuOLf2KWTWaoWK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kCJV1/btshA4Suy1Y/nZpaZabQUuOLf2KWTWaoWK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kCJV1/btshA4Suy1Y/nZpaZabQUuOLf2KWTWaoWK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkCJV1%2FbtshA4Suy1Y%2FnZpaZabQUuOLf2KWTWaoWK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2562&quot; height=&quot;550&quot; data-origin-width=&quot;2562&quot; data-origin-height=&quot;550&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;추가가 완료되면 이런 식으로 목록에 뜬다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;생각보다 글이 길어져서 2개로 나누고자 한다.&lt;br /&gt;다음 포스팅으로 깃허브 레파지토리 연동 + 젠킨스 배포 스크립트를 설정해보자!&lt;/p&gt;</description>
      <category>개발일지</category>
      <author>dolmeng2</author>
      <guid isPermaLink="true">https://cl8d.tistory.com/95</guid>
      <comments>https://cl8d.tistory.com/95#entry95comment</comments>
      <pubDate>Tue, 30 May 2023 20:31:43 +0900</pubDate>
    </item>
    <item>
      <title>[MySQL] Error 1093: You can't specify target table for update in FROM clause</title>
      <link>https://cl8d.tistory.com/98</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  문제 상황&amp;nbsp;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자의 요청으로부터 받은 장바구니 아이디와 사용자의 아이디가 일치하는 경우에만 제거하기 위해 아래와 같은 쿼리를 작성했다.&lt;/p&gt;
&lt;pre id=&quot;code_1685335297419&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;DELETE FROM cart_item AS c
WHERE c.id
IN (
    SELECT ci.id FROM cart_item AS ci
    JOIN member AS m ON ci.member_id = m.id
    AND ci.id IN (6, 8) AND m.id = 1
);&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-05-29 오후 1.41.49.png&quot; data-origin-width=&quot;968&quot; data-origin-height=&quot;76&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BJQTg/btshE0Cv29c/BN4SOa6DXpDGXqGxkoVlKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BJQTg/btshE0Cv29c/BN4SOa6DXpDGXqGxkoVlKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BJQTg/btshE0Cv29c/BN4SOa6DXpDGXqGxkoVlKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBJQTg%2FbtshE0Cv29c%2FBN4SOa6DXpDGXqGxkoVlKk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;968&quot; height=&quot;76&quot; data-filename=&quot;스크린샷 2023-05-29 오후 1.41.49.png&quot; data-origin-width=&quot;968&quot; data-origin-height=&quot;76&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, 다음과 같이 target table (cart_item)의 c에 대해서 업데이트를 할 수 없다는 문구가 나왔다.&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;style3&quot; /&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&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;MySQL에서는 &lt;span style=&quot;color: #ef5369;&quot;&gt;update, delete 시에 자기 자신의 테이블 데이터를 바로 사용할 수 없었기 때문에&lt;/span&gt; 발생한 오류였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(참고로 H2 환경에서는 잘 돌아간다. 옛날에도 이런 오류가 났었는데... 기록하지 않은 자의 최후  )&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;style3&quot; /&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;IN 절 내부에 존재하는&lt;span style=&quot;color: #ef5369;&quot;&gt; 서브 쿼리에 대해서 임시 테이블로 만든 다음&lt;/span&gt;에, 해당 테이블을 참조하도록 만들면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1685335616642&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;DELETE FROM cart_item AS c
WHERE c.id
IN (
    SELECT cart_item_temp.id
    FROM
    (
        SELECT ci.id AS id FROM cart_item AS ci
        JOIN member AS m ON ci.member_id = m.id
        AND ci.id IN (6, 8) AND m.id = 1
    ) cart_item_temp
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드가 복잡하기는 하지만, h2와 mysql 환경 모두에서 잘 동작하도록 만드려면 어쩔 수 없는 선택이었다.&lt;/p&gt;
&lt;pre id=&quot;code_1685335752107&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;DELETE c
FROM cart_item AS c
JOIN member AS m ON c.member_id = m.id
WHERE c.id IN (6, 8) AND m.id = 1;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순하게 이렇게 조인을 해도 되지만, &lt;span style=&quot;color: #ef5369;&quot;&gt;h2에서는 delete 절에 대해서 별칭을 지원하지 않기 때문에&lt;/span&gt; 사용할 수가 없었다...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;h2 mode를 MySQL로 하더라도 이런 쿼리에 대해서는 완전 동일하게 동작하지는 않는 것 같다.&lt;/p&gt;</description>
      <category>개발일지</category>
      <category>DELETE</category>
      <category>SubQuery</category>
      <category>서브쿼리</category>
      <author>dolmeng2</author>
      <guid isPermaLink="true">https://cl8d.tistory.com/98</guid>
      <comments>https://cl8d.tistory.com/98#entry98comment</comments>
      <pubDate>Mon, 29 May 2023 13:53:14 +0900</pubDate>
    </item>
    <item>
      <title>[Infra] AWS 배포 후 도메인 연결 및 HTTPS 적용, nginx로 리버스 프록시 적용하기</title>
      <link>https://cl8d.tistory.com/97</link>
      <description>&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;무과금으로 HTTPS 적용 프로젝트를 진행해보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정석대로라면 가비아 + Route53 + ACM or 가비아 + nginx로만 진행하면 좋았겠지만...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 제약사항으로 인해서 색다른 방법으로 도메인 연결 및 HTTPS 적용을 진행해보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;  제약사항&lt;/b&gt;&lt;br /&gt;- 무료 도메인 사용하기&lt;br /&gt;- 서버 1대로 구축하기 (끊임없이 고통받는 t4g.micro)&lt;br /&gt;- http 접속 시 https로 리다이렉트시키기&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;나중에 Route53 + ACM + ELB를 통해 도입했던 것도 포스팅으로 작성해보고자 한다. (이번 미션에서는 못 했지만 ㅠ)&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;style3&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&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;figure id=&quot;og_1685256949352&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;내도메인.한국 - 한글 무료 도메인 등록센터&quot; data-og-description=&quot;한글 무료 도메인 내도메인.한국, 웹포워딩, DNS 등 무료 도메인 기능 제공&quot; data-og-host=&quot;xn--220b31d95hq8o.xn--3e0b707e&quot; data-og-source-url=&quot;https://xn--220b31d95hq8o.xn--3e0b707e/&quot; data-og-url=&quot;https://xn--220b31d95hq8o.xn--3e0b707e/&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://xn--220b31d95hq8o.xn--3e0b707e/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://xn--220b31d95hq8o.xn--3e0b707e/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;내도메인.한국 - 한글 무료 도메인 등록센터&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;한글 무료 도메인 내도메인.한국, 웹포워딩, DNS 등 무료 도메인 기능 제공&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;xn--220b31d95hq8o.xn--3e0b707e&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;굉장히 수상하게 생겼지만 속도도 빠르고, 적용하는 데에는 크게 무리가 없어서 여기서 구입하기로 결정하였다.&lt;/p&gt;
&lt;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;스크린샷 2023-05-28 오후 3.56.43.png&quot; data-origin-width=&quot;1730&quot; data-origin-height=&quot;694&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Vq2rB/btshG78OnY1/NnVlFdK6WrUtqXHy15Vjmk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Vq2rB/btshG78OnY1/NnVlFdK6WrUtqXHy15Vjmk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Vq2rB/btshG78OnY1/NnVlFdK6WrUtqXHy15Vjmk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVq2rB%2FbtshG78OnY1%2FNnVlFdK6WrUtqXHy15Vjmk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1730&quot; height=&quot;694&quot; data-filename=&quot;스크린샷 2023-05-28 오후 3.56.43.png&quot; data-origin-width=&quot;1730&quot; data-origin-height=&quot;694&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;5번은 현재 내가 사용하고 있기 때문에 등록 불가로 뜬다 ㅎㅎ&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-05-28 오후 3.58.39.png&quot; data-origin-width=&quot;1676&quot; data-origin-height=&quot;1142&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dK6qAt/btshA4k0qUr/V0axBM3McbjcOQyBPz4EG0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dK6qAt/btshA4k0qUr/V0axBM3McbjcOQyBPz4EG0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dK6qAt/btshA4k0qUr/V0axBM3McbjcOQyBPz4EG0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdK6qAt%2FbtshA4k0qUr%2FV0axBM3McbjcOQyBPz4EG0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;832&quot; height=&quot;567&quot; data-filename=&quot;스크린샷 2023-05-28 오후 3.58.39.png&quot; data-origin-width=&quot;1676&quot; data-origin-height=&quot;1142&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에 구입하면 위와 같이 DNS 연결을 할 수 있는데, 우선 A 레코드에 AWS 인스턴스의 public IP를 작성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 A 레코드를 등록한다는 의미는,&lt;span style=&quot;color: #ef5369;&quot;&gt; DNS 서버에 우리가 구매한 도메인과 IP를 연결한다는 의미&lt;/span&gt;인데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;journey-shop.kro.kr로 브라우저에서 접속하면 12.34.56.78 (IP)로 접속할 수 있도록 만든다는 뜻이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CNAME은 추후 설정할 예정이지만, 해당 도메인에 대한 별칭을 지정한다는 의미 정도로 생각하면 된다.&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;스크린샷 2023-05-28 오후 4.04.10.png&quot; data-origin-width=&quot;3022&quot; data-origin-height=&quot;646&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bvuMc8/btshzYsjszI/mJcd5BnRDRs2D8xkdCqEkK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bvuMc8/btshzYsjszI/mJcd5BnRDRs2D8xkdCqEkK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bvuMc8/btshzYsjszI/mJcd5BnRDRs2D8xkdCqEkK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbvuMc8%2FbtshzYsjszI%2FmJcd5BnRDRs2D8xkdCqEkK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3022&quot; height=&quot;646&quot; data-filename=&quot;스크린샷 2023-05-28 오후 4.04.10.png&quot; data-origin-width=&quot;3022&quot; data-origin-height=&quot;646&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;등록하고 journey-shop.kro.kr:8080/admin으로 접속하게 되면 위와 같이 멋진 창이 뜨게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더 이상 IP 주소로 접근하지 않고, 사람에게 익숙한 도메인명으로 사이트에 접속이 가능한 것이다.&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;style3&quot; /&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;  HTTPS 연동하기&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, 위에서 진행한 과정은 IP 대신에 단순히 도메인명을 사용했을 뿐이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리가 네이버에 접속할 때 naver.com:8080과 같이 접속하지 않는 것처럼, 포트 번호 없이, https 환경으로 접속하고 싶을 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(또한, 프론트엔드 서버가 배포 서버라면 https -&amp;gt; http는 CORS에 의해서 통신이 불가능하다  )&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 이번에는 간단하게 https를 적용해보고자 한다.&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;https는 암호화된 프로토콜이다. 그렇기 때문에&lt;span style=&quot;color: #ef5369;&quot;&gt; 신뢰된 기관이 제공해주는 인증서가 필요&lt;/span&gt;하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 certbot을 사용하려고 했으나... 무료 도메인을 사용하다 보니 kro.kr로 요청이 너무 많이 들어와서 당장 처리할 수 없다는 오류가 발생하고 말았다.&lt;/p&gt;
&lt;pre id=&quot;code_1685257851673&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;too many certificates already issued for &quot;kro.kr&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 1시간 정도 처음에는 기다리려고 했지만, 바로 진행하고 싶어서 결국 포기...&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;pre id=&quot;code_1685257994881&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo apt update

# certbot 설치
sudo apt install certbot

# www의 경우 선택사항
sudo certbot certonly --standalone -d journey-shop.kro.kr -d www.journey-shop.kro.kr

# 인증서 파일 확인
sudo ls /etc/letsencrypt/live/journey-shop.kro.kr/
cert.pem  chain.pem  fullchain.pem  privkey.pem&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;아무튼, 어떤 방법을 사용할지 고민하다가 SSL for Free이라는 사이트를 발견하였다.&lt;/p&gt;
&lt;figure id=&quot;og_1685258043158&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;SSL For Free - Free SSL Certificates in Minutes&quot; data-og-description=&quot;Wildcard SSL Certificates Wildcard certificates allow you to secure any sub-domains under a domain. If you want to secure any sub-domains of example.org that you have now or in the future you can make a wildcard certificate. To generate wildcard certificat&quot; data-og-host=&quot;www.sslforfree.com&quot; data-og-source-url=&quot;https://www.sslforfree.com/&quot; data-og-url=&quot;https://www.sslforfree.com/&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://www.sslforfree.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.sslforfree.com/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;SSL For Free - Free SSL Certificates in Minutes&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Wildcard SSL Certificates Wildcard certificates allow you to secure any sub-domains under a domain. If you want to secure any sub-domains of example.org that you have now or in the future you can make a wildcard certificate. To generate wildcard certificat&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.sslforfree.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-05-28 오후 4.15.04.png&quot; data-origin-width=&quot;1810&quot; data-origin-height=&quot;936&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cMh9AH/btshy89IIxp/qG72bNOxrA8Akw0FKKMkj1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cMh9AH/btshy89IIxp/qG72bNOxrA8Akw0FKKMkj1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cMh9AH/btshy89IIxp/qG72bNOxrA8Akw0FKKMkj1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcMh9AH%2Fbtshy89IIxp%2FqG72bNOxrA8Akw0FKKMkj1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1810&quot; height=&quot;936&quot; data-filename=&quot;스크린샷 2023-05-28 오후 4.15.04.png&quot; data-origin-width=&quot;1810&quot; data-origin-height=&quot;936&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가입하고 나서 Domain 입력해주고, Validity에 90일, Auto-Generate CSR을 켜주면 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-05-28 오후 4.19.24.png&quot; data-origin-width=&quot;1596&quot; data-origin-height=&quot;1196&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bdF1Z6/btshzCik1dP/tRhGwvYcSkbhsfqwKSGOJk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bdF1Z6/btshzCik1dP/tRhGwvYcSkbhsfqwKSGOJk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bdF1Z6/btshzCik1dP/tRhGwvYcSkbhsfqwKSGOJk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbdF1Z6%2FbtshzCik1dP%2FtRhGwvYcSkbhsfqwKSGOJk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;637&quot; height=&quot;477&quot; data-filename=&quot;스크린샷 2023-05-28 오후 4.19.24.png&quot; data-origin-width=&quot;1596&quot; data-origin-height=&quot;1196&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고, 악의적인 사람이 내 도메인에 대해서 멋대로 접근할 수 있기 때문에 &lt;span style=&quot;color: #ef5369;&quot;&gt;journey.kro.kr이 정말 SSL 발급자의 소유 도메인인지 확인하기 위해&lt;/span&gt; 3가지의 인증 방법이 주어진다. (메일 인증, DNS, HTTP File upload)&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;메일 인증의 경우 내도메인.한국을 사용했다 보니 kro.kr의 소유주의 이메일로 전송되기 때문에 사용할 수 없었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;File upload의 경우 우리의 스프링 부트 서버에 ssl for free에서 지정해준 경로에 파일을 업로드 후 배포하여, 해당 url로 이 사이트가 GET 요청을 보내 health checking을 진행하는 방식인데, 프로덕션 코드가 더럽혀지는 게 싫어서 패스하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 마지막 방법으로, 이 사이트에서 &lt;span style=&quot;color: #ef5369;&quot;&gt;지정한 CNAME을 우리의 도메인에 연결하여 소유권을 검증하는 방법을 채택&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-05-28 오후 4.23.09.png&quot; data-origin-width=&quot;1644&quot; data-origin-height=&quot;372&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kVP6U/btshzBqcwTG/bF2FQFLGaIkVAakkAyDuBk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kVP6U/btshzBqcwTG/bF2FQFLGaIkVAakkAyDuBk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kVP6U/btshzBqcwTG/bF2FQFLGaIkVAakkAyDuBk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkVP6U%2FbtshzBqcwTG%2FbF2FQFLGaIkVAakkAyDuBk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1644&quot; height=&quot;372&quot; data-filename=&quot;스크린샷 2023-05-28 오후 4.23.09.png&quot; data-origin-width=&quot;1644&quot; data-origin-height=&quot;372&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 주어진 NAME, Point to를 각각 입력해주면 된다.&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;verify Domain&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-05-28 오후 4.24.03.png&quot; data-origin-width=&quot;1064&quot; data-origin-height=&quot;156&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rK96b/btshzAZbiLb/2FqHulEUNd7CVFTORJ4s3K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rK96b/btshzAZbiLb/2FqHulEUNd7CVFTORJ4s3K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rK96b/btshzAZbiLb/2FqHulEUNd7CVFTORJ4s3K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrK96b%2FbtshzAZbiLb%2F2FqHulEUNd7CVFTORJ4s3K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1064&quot; height=&quot;156&quot; data-filename=&quot;스크린샷 2023-05-28 오후 4.24.03.png&quot; data-origin-width=&quot;1064&quot; data-origin-height=&quot;156&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고, Certificate 파일을 다운로드 받아야 하는데 'Ubuntu'으로 설정한 다음 다운로드 받았다.&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;b&gt;ca_bundle.crt, certificate.crt, private.key&lt;/b&gt;를 얻게 된다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;ca_bundle.crt (= chain.pem)&lt;br /&gt;&lt;/b&gt;&amp;nbsp;: 인증 체인이나 중간 인증 기관의 인증서들을 포함한다.&lt;br /&gt;인증 체인은 인증서 발급 기관에서 최상위 인증 기관까지 계층적 구조로 구성되며, 서버 인증서와 함께 제종되는 CA의 인증서 체인을 의미한다. 보통 클라이언트가 인증서를 검증할 때 사용한다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;certificate.crt (= fullchain.pem)&lt;/b&gt;&lt;br /&gt;: 서버의 SSL / TLS 인증서를 포함하며, 웹 서버에 설치되는 인증서이다.&lt;br /&gt;서버의 공개키와 서버 정보가 포함되어 있다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;private.key (= privkey.pem)&lt;/b&gt;&lt;br /&gt;: 서버의 개인 키를 포함하며, 서버 측에서 인증서와 매칭되는 일종의 비밀 키이다.&lt;br /&gt;SSL / TLS의 암복호화에서 사용되기 때문에 외부에  노출돼서는 안 된다.&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;하지만, 추후 nginx에서 인증서를 등록하기 위해서는 crt 파일 형식이 아닌 pem 키 형식이 필요하기 때문에 별도로 변환하는 작업이 필요하다.&lt;/p&gt;
&lt;pre id=&quot;code_1685259336245&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;openssl x509 -inform PEM -in certificate.crt &amp;gt; certificate.pem&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;그래서, 서버의 인증서를 openssl을 통해서 pem 형식으로 변환해주었다.&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;마지막으로 인증서와 키를 내 ec2 쪽으로 옮겨보자.&lt;/p&gt;
&lt;pre id=&quot;code_1685260428827&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;scp -i {EC2 접속을 위한 키} certificate.pem ubuntu@{EC2 public IP}:{이동할 path}
scp -i {EC2 접속을 위한 키} private.key ubuntu@{EC2 public IP}:{이동할 path}&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  Nginx로 리버스 프록시 진행하기&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;399&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/PASrZ/btshy82Uzu1/UfMK0nogbHuQyqQMjWJT80/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/PASrZ/btshy82Uzu1/UfMK0nogbHuQyqQMjWJT80/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PASrZ/btshy82Uzu1/UfMK0nogbHuQyqQMjWJT80/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPASrZ%2Fbtshy82Uzu1%2FUfMK0nogbHuQyqQMjWJT80%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;399&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;399&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;우리는 여기서 'nginx'라는 친구를 이용해서 http로 온 요청을 https로 바꾸는 작업을 진행해보고자 한다  &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1685260646769&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo apt install nginx&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저, nginx를 설치해준다.&lt;/p&gt;
&lt;pre id=&quot;code_1685260721915&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo ufw enable
sudo ufw status&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고, 방화벽에 대한 설정을 해줘야 하는데, &lt;span style=&quot;color: #ef5369;&quot;&gt;우분투는 ufw 방화벽을 사용&lt;/span&gt;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 비활성화되어 있기 때문에 enable을 통해서 활성화 시켜준다. 이러면 모든 포트가 자동적으로 막히게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;status를 통해 현재 어떤 포트가 열려있는지 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-05-28 오후 5.00.06.png&quot; data-origin-width=&quot;1050&quot; data-origin-height=&quot;402&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dj70FC/btshE0B9pAK/hR1tsnxQa5k55UTbxfGVzk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dj70FC/btshE0B9pAK/hR1tsnxQa5k55UTbxfGVzk/img.png&quot; data-alt=&quot;중간에 캡쳐한 거여서 몇몇이 열려있다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dj70FC/btshE0B9pAK/hR1tsnxQa5k55UTbxfGVzk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdj70FC%2FbtshE0B9pAK%2FhR1tsnxQa5k55UTbxfGVzk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;656&quot; height=&quot;251&quot; data-filename=&quot;스크린샷 2023-05-28 오후 5.00.06.png&quot; data-origin-width=&quot;1050&quot; data-origin-height=&quot;402&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;중간에 캡쳐한 거여서 몇몇이 열려있다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1685260915264&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo ufw allow ssh
sudo ufw allow 'Nginx Full'
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 8080/tcp&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 다음과 같이 ssh, nginx Full (사실 이게 80이랑 443 열어주긴 함), 80, 443, 8080을 열어주었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1685260994999&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo systemctl start nginx
sudo systemctl status nginx&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고, 잘 가동되는지 확인하기 위해 restart 후 status를 확인한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-05-28 오후 5.03.45.png&quot; data-origin-width=&quot;400&quot; data-origin-height=&quot;94&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bGfHbO/btshzBRlX5h/XDKyQ0kkSskqqQfokMRKnK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bGfHbO/btshzBRlX5h/XDKyQ0kkSskqqQfokMRKnK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bGfHbO/btshzBRlX5h/XDKyQ0kkSskqqQfokMRKnK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbGfHbO%2FbtshzBRlX5h%2FXDKyQ0kkSskqqQfokMRKnK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;400&quot; height=&quot;94&quot; data-filename=&quot;스크린샷 2023-05-28 오후 5.03.45.png&quot; data-origin-width=&quot;400&quot; data-origin-height=&quot;94&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요런 식으로 Active가 떴다면 성공이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나중에는 restart 옵션을 많이 사용할 텐데, status로 항상 상태를 함께 확인하는 것을 추천한다.&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: #ef5369;&quot;&gt;/etc/nginx/site-enabled&lt;/span&gt;로 이동한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;거기로 가면 'default'라는 파일이 있을 텐데, 해당 파일을 vi로 수정한다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1685261274653&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;cd /etc/nginx/site-enabled
sudo vi default&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1685261181744&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;server {
	listen 80 default_server;
	listen [::]:80 default_server;

	listen 443 ssl default_server;
	listen [::]:443 ssl default_server;

	root /var/www/html;

	index index.html index.htm index.nginx-debian.html;

	server_name journey-shop.kro.kr;
	ssl_certificate /home/ubuntu/certificate.pem;
	ssl_certificate_key /home/ubuntu/private.key;
	ssl_prefer_server_ciphers on;

	location / {
          proxy_pass http://localhost:8080;
          proxy_set_header X-Real_IP $remote_addr;
          proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
          proxy_set_header Host $http_host;
	}
}


server {
  listen 80;
  server_name journey-shop.kro.kr;
  return 301 https://$host$request_uri;
}&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&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;  Nginx 설정 파일 분석하기&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-05-28 오후 5.13.37.png&quot; data-origin-width=&quot;1410&quot; data-origin-height=&quot;360&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c1sADM/btshBiQKAuI/Bkss7Ej1qoA2ZFtNkONUE1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c1sADM/btshBiQKAuI/Bkss7Ej1qoA2ZFtNkONUE1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c1sADM/btshBiQKAuI/Bkss7Ej1qoA2ZFtNkONUE1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc1sADM%2FbtshBiQKAuI%2FBkss7Ej1qoA2ZFtNkONUE1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1410&quot; height=&quot;360&quot; data-filename=&quot;스크린샷 2023-05-28 오후 5.13.37.png&quot; data-origin-width=&quot;1410&quot; data-origin-height=&quot;360&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 default_server라는 친구를 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;nginx에서 &lt;b&gt;어떤 포트로 요청이 들어왔을 때 지정한 server 블록을 기본으로 설정&lt;/b&gt;하겠다는 의미이며, 만약 지정하지 않은 경우 요청에 대해 가장 구체적인 서버 블록을 찾으려고 매칭을 시도한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;⭐️ 여기서, 기본 서버 블록이라는 건 &lt;span style=&quot;color: #ef5369;&quot;&gt;아무것도 매칭되지 않을 때 사용한다는 의미&lt;/span&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;1. 요청한&amp;nbsp;호스트 이름과 일치하는 server_name 지시문이 있는 서버 블록&amp;nbsp;지정&lt;br /&gt;2. 요청된 호스트 이름과 일치하는 와일드카드 server_name 지시문이 있는 서버 블록 지정&lt;br /&gt;3. 기본 서버 블록 (default_server)&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;예를 들어서, journey-shop.kro.kr으로 요청이 들어온다면 다음과 같은 순서로 매칭된다.&lt;/p&gt;
&lt;pre id=&quot;code_1685262019351&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;server {
    listen 80;
    server_name *.journey-shop.kro.kr;
}

server {
    listen 80;
    server_name *.journey-shop.kro.kr;
}

server {
    listen 80 default_server;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 3개의 서버 블록이 있다면 가장 구체적인 첫 번째 블록이 선택될 것이며, 만약 첫 번째 블록이 잘못되었다면 두 번째 블록으로, 두 번째 블록도 잘못되었다면 default_server로 설정되어 있는 세 번째 블록이 선택된다.&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-05-28 오후 5.23.11.png&quot; data-origin-width=&quot;1388&quot; data-origin-height=&quot;190&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bvvEUb/btshzZrhxwV/5gsfiNUpbKxDC9pd34SXQ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bvvEUb/btshzZrhxwV/5gsfiNUpbKxDC9pd34SXQ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bvvEUb/btshzZrhxwV/5gsfiNUpbKxDC9pd34SXQ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbvvEUb%2FbtshzZrhxwV%2F5gsfiNUpbKxDC9pd34SXQ1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1388&quot; height=&quot;190&quot; data-filename=&quot;스크린샷 2023-05-28 오후 5.23.11.png&quot; data-origin-width=&quot;1388&quot; data-origin-height=&quot;190&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;root의 경우 &lt;span style=&quot;color: #ef5369;&quot;&gt;정적 파일에 대한 기본 경로&lt;/span&gt;이기 때문에, journey-shop.kro.kr/index.html 요청이 들어온다면&amp;nbsp; /var/www/html/index.html에서 파일을 찾게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;index의 경우, &lt;b&gt;request에 아무 파일도 지정하지 않은 상태로 요청이 들어온다면&lt;/b&gt; /var/www/html/index, /var/www/html/index.html, /var/www/html/index.htm, /var/www/html/index.nginx-debian.html &lt;span style=&quot;color: #ef5369;&quot;&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;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-05-28 오후 5.34.06.png&quot; data-origin-width=&quot;1636&quot; data-origin-height=&quot;118&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sWJDP/btshCegnE4F/AgBwkWkhFx6xHCadmiwg20/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sWJDP/btshCegnE4F/AgBwkWkhFx6xHCadmiwg20/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sWJDP/btshCegnE4F/AgBwkWkhFx6xHCadmiwg20/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsWJDP%2FbtshCegnE4F%2FAgBwkWkhFx6xHCadmiwg20%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1636&quot; height=&quot;118&quot; data-filename=&quot;스크린샷 2023-05-28 오후 5.34.06.png&quot; data-origin-width=&quot;1636&quot; data-origin-height=&quot;118&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;server_name에는 여러 도메인을 지정할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1685262931766&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 2개의 도메인에 대해서 처리
server_name journey-shop.kro.kr www.journey-shop.kro.kr

# .journey-shop.kro.kr과 동일하다. 서브도메인에 대한 요청을 처리할 수 있다.
server_name *.journey-shop.kro.kr

# 특정 이름으로 시작하는 도메인에 대한 요청을 처리할 수 있다.
server_name journey-shop.kro.kr.*&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로, nginx의 경우 단순히 HTTP 헤더에 있는 이름을 통해 요청에 응답하기 때문에&lt;span style=&quot;color: #ef5369;&quot;&gt; 도메인 이름이 유효한지는 알 수 없다&lt;/span&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 유효하지 않은 도메인 이름을 server_name에 지정할 수도 있다. (물론 에러 페이지로 가겠지만?)&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-05-28 오후 5.37.39.png&quot; data-origin-width=&quot;1510&quot; data-origin-height=&quot;184&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ctjPKz/btshMWeKmag/E4PydlSIxkkyAKAdRjWf11/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ctjPKz/btshMWeKmag/E4PydlSIxkkyAKAdRjWf11/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ctjPKz/btshMWeKmag/E4PydlSIxkkyAKAdRjWf11/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FctjPKz%2FbtshMWeKmag%2FE4PydlSIxkkyAKAdRjWf11%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1510&quot; height=&quot;184&quot; data-filename=&quot;스크린샷 2023-05-28 오후 5.37.39.png&quot; data-origin-width=&quot;1510&quot; data-origin-height=&quot;184&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아까 crt를 통해 보냈던&lt;span style=&quot;color: #ef5369;&quot;&gt; 인증서와 키를 여기에서 지정&lt;/span&gt;해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 마지막 옵션의 경우 서버에서 지정한 암호화 알고리즘을 우선한다는 의미이며, off로 설정하면 외부에서 약화된 알고리즘을 사용하여 공격할 수 있기 때문에 웬만하면 키는 게 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로, ssl_protocols을 통해 프로토콜을 지정하거나 ssl_ciphers를 통해 알고리즘 지정도 가능하다.&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-05-28 오후 5.46.19.png&quot; data-origin-width=&quot;2368&quot; data-origin-height=&quot;502&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/W1EtM/btshKQMwkgW/Jsc1KHDFbIsKkXqTMv1pYK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/W1EtM/btshKQMwkgW/Jsc1KHDFbIsKkXqTMv1pYK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/W1EtM/btshKQMwkgW/Jsc1KHDFbIsKkXqTMv1pYK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FW1EtM%2FbtshKQMwkgW%2FJsc1KHDFbIsKkXqTMv1pYK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2368&quot; height=&quot;502&quot; data-filename=&quot;스크린샷 2023-05-28 오후 5.46.19.png&quot; data-origin-width=&quot;2368&quot; data-origin-height=&quot;502&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 proxy_pass를 통해 http, https로 들어온 요청에 대해서 우리가 스프링부트를 띄운 8080으로 포워딩되도록 설정하였다.&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;여기서 호스트 이름이 조금 헷갈렸었는데, 사용자가 journey-shop.kro.kr에 접속했을 때 남는 http 헤더의 host 값 = journey-shop.kro.kr을 프록시 헤더로 지정해주는 것이었다. 즉,&lt;span style=&quot;color: #ef5369;&quot;&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;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-05-28 오후 5.49.51.png&quot; data-origin-width=&quot;1544&quot; data-origin-height=&quot;296&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/TSP5f/btshCcvZAgd/2KVnVg06u1YlAFKZ0STU0K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/TSP5f/btshCcvZAgd/2KVnVg06u1YlAFKZ0STU0K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/TSP5f/btshCcvZAgd/2KVnVg06u1YlAFKZ0STU0K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FTSP5f%2FbtshCcvZAgd%2F2KVnVg06u1YlAFKZ0STU0K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1544&quot; height=&quot;296&quot; data-filename=&quot;스크린샷 2023-05-28 오후 5.49.51.png&quot; data-origin-width=&quot;1544&quot; data-origin-height=&quot;296&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;위에서 설명했던 것처럼 journey-shop.kro.kr로 요청이 들어오면 &lt;span style=&quot;color: #ef5369;&quot;&gt;가장 구체적인 server_name이 지정된 이 블록이 매칭&lt;/span&gt;된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 http 요청을 모두 https로 리다이렉트 시키기 위해서 위와 같이 만들어주었다.&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;host의 경우 클라이언트의 요청에서 Host 헤더 값을 의미하며, request_uri는 클라이언트가 요청한 URI이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;nginx에서 자동으로 설정해주는 값이기 때문에 별도로 재정의할 필요는 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;  참고로, URI의 경우 쿼리 파라미터도 포함한다.&lt;/b&gt;&lt;br /&gt;ex) https://journey-shop.kro.kr/products?name='치킨'&lt;br /&gt;&lt;br /&gt;host: https://journey-shop.kro.kr&lt;br /&gt;request_uri: journey-shop.kro.kr/products?name='치킨'&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;다 끝났다! 이제 브라우저에서 journey-shop.kro.kr/admin로 들어가면 바로 https로 디라이렉트된다!&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-05-28 오후 5.56.09.png&quot; data-origin-width=&quot;670&quot; data-origin-height=&quot;70&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/buLR9V/btshy7XhS9h/MukJGIHbhSXiptjsDskeK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/buLR9V/btshy7XhS9h/MukJGIHbhSXiptjsDskeK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/buLR9V/btshy7XhS9h/MukJGIHbhSXiptjsDskeK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbuLR9V%2Fbtshy7XhS9h%2FMukJGIHbhSXiptjsDskeK1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;670&quot; height=&quot;70&quot; data-filename=&quot;스크린샷 2023-05-28 오후 5.56.09.png&quot; data-origin-width=&quot;670&quot; data-origin-height=&quot;70&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;style3&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 캠퍼스에서 썼어야 했는데...&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;캠퍼스 등교하면 www도 추가해야겠다 ^^...&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>개발일지</category>
      <category>https</category>
      <category>nginx</category>
      <category>Proxy</category>
      <category>SSL</category>
      <author>dolmeng2</author>
      <guid isPermaLink="true">https://cl8d.tistory.com/97</guid>
      <comments>https://cl8d.tistory.com/97#entry97comment</comments>
      <pubDate>Sun, 28 May 2023 17:56:46 +0900</pubDate>
    </item>
    <item>
      <title>[Network] CDN - 캐시 서버로 빠르게 로드하기</title>
      <link>https://cl8d.tistory.com/92</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  CDS (Content Delivery Service)&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;수많은 파일과 데이터에 대해 전세계에 응답해야 하는 상황에서, 멀리 있는 국가의 사용자와 대해서 전달해야 한다면?&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;span style=&quot;color: #ef5369;&quot;&gt;다수의 캐시 서버를 여러 개의 지역에 배치하여 트래픽을 분산시키는 방법이 필요&lt;/span&gt;하다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 캐시 서버는 어디에 둬야 할까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&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;스크린샷 2023-05-25 오후 9.58.39.png&quot; data-origin-width=&quot;1236&quot; data-origin-height=&quot;244&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wHqll/btshqjJvpeI/yGzu04hJiglmS8c82nbWh0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wHqll/btshqjJvpeI/yGzu04hJiglmS8c82nbWh0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wHqll/btshqjJvpeI/yGzu04hJiglmS8c82nbWh0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwHqll%2FbtshqjJvpeI%2FyGzu04hJiglmS8c82nbWh0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1236&quot; height=&quot;244&quot; data-filename=&quot;스크린샷 2023-05-25 오후 9.58.39.png&quot; data-origin-width=&quot;1236&quot; data-origin-height=&quot;244&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;size18&quot;&gt;&lt;b&gt;2. 클라이언트 측에 두기&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-05-25 오후 10.05.37.png&quot; data-origin-width=&quot;1196&quot; data-origin-height=&quot;270&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wOsKW/btshpX7Nk1Q/ItK93tIEfleAUL6g8YVLG0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wOsKW/btshpX7Nk1Q/ItK93tIEfleAUL6g8YVLG0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wOsKW/btshpX7Nk1Q/ItK93tIEfleAUL6g8YVLG0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwOsKW%2FbtshpX7Nk1Q%2FItK93tIEfleAUL6g8YVLG0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1196&quot; height=&quot;270&quot; data-filename=&quot;스크린샷 2023-05-25 오후 10.05.37.png&quot; data-origin-width=&quot;1196&quot; data-origin-height=&quot;270&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1번의 경우와 달리, 인터넷에 흐르는 트레픽을 억제할 수 있다.&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;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;size18&quot;&gt;&lt;b&gt;3. 인터넷 주위에 두기&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-05-25 오후 10.05.47.png&quot; data-origin-width=&quot;1234&quot; data-origin-height=&quot;340&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dl3KhY/btshtEk9enX/5t21JCJm2jIMeAqd4mZEo1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dl3KhY/btshtEk9enX/5t21JCJm2jIMeAqd4mZEo1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dl3KhY/btshtEk9enX/5t21JCJm2jIMeAqd4mZEo1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdl3KhY%2FbtshtEk9enX%2F5t21JCJm2jIMeAqd4mZEo1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1234&quot; height=&quot;340&quot; data-filename=&quot;스크린샷 2023-05-25 오후 10.05.47.png&quot; data-origin-width=&quot;1234&quot; data-origin-height=&quot;340&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;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;이렇게 되면 인터넷에서 흐르는 트래픽도 제어할 수 있으며, 서버 운영자가 캐시 서버를 제어&lt;/b&gt;&lt;/span&gt;할 수 있게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, 서버 측에서는 인터넷의 어느 측에서 액세스하는지 알 수 없기 때문에 POP (point of presence, 인터넷 액세스 포인트) 전부에 캐시 서버를 설치해야 돼서 현실적으로 불가능하다. 이를 해결하기 위해서 보통은 중요한 프로바이더 위주로 캐시 서버를 두어 캐시 서버를 줄이는 방법을 사용한다.&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;span style=&quot;color: #ef5369;&quot;&gt;CDS (Content Delivery Service) - 콘텐츠 배포 서비스&lt;/span&gt;라고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CDSP (CDS를 제공하는 사업자)는 중요한 프로바이더와 계약하고, 그곳에 여러 개의 캐시 서버를 설치하면서, 동시에 웹 서버 운영자와도 계약하여 웹 서버와 캐시 서버 사이를 연결해주는 역할을 한다.&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;ex) AWS의 CloudFront가 대표적인 제공 서비스 중 하나이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;+ 보통은 'CDN'이라는 명칭을 많이 사용하기 때문에 아래에서도 그렇게 부르겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;  CDN을 사용하면 무엇이 좋을까?&lt;/b&gt;&lt;br /&gt;- 페이지 로드 시간 단축&lt;br /&gt;- 대역폭 비용 절감 (원본 서버의 제공 데이터 양을 줄이게 되니까)&lt;br /&gt;- 콘텐트 가용성 (더 많은 트래픽 처리 가능)&lt;br /&gt;- 웹 사이트의 보안 강화 (DDoS 공격도 분산시키니 조금 더 강함)&lt;br /&gt;&lt;br /&gt;&lt;b&gt;  보통 언제 사용할까?&lt;/b&gt;&lt;br /&gt;- 빠르게 컨텐츠를 전송해야 할 때 (뉴스 같은...?)&lt;br /&gt;- 실시간 스트리밍&lt;br /&gt;- 동시 사용자가 많은 경우&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;기본적으로 CDN의 경우 사용자가 웹 브라우저에 요청을 보내면 (클라이언트의 요청이 서버로 오게 되면) 원본 서버로부터 컨텐츠를 가져와 유저에게 전송한 동시에 캐시 서버에 저장한다. 그리고, 이후에 발생한 요청의 경우 &lt;b&gt;'가장 가까운' 캐시 서버에게 액세스&lt;/b&gt;하도록 만든다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;  '가장 가까운 캐시 서버를 찾아내고, 그곳에 액세스한다'&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 말은 어디에서 들어본 것 같지 않은가? 바로 DNS 서버에서 IP 주소를 찾아내는 과정이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-05-25 오후 10.36.54.png&quot; data-origin-width=&quot;1588&quot; data-origin-height=&quot;844&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bcCXJD/btshmXmWwXX/Hr1PgX1D6jYjlapPTAnsOk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bcCXJD/btshmXmWwXX/Hr1PgX1D6jYjlapPTAnsOk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bcCXJD/btshmXmWwXX/Hr1PgX1D6jYjlapPTAnsOk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbcCXJD%2FbtshmXmWwXX%2FHr1PgX1D6jYjlapPTAnsOk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;659&quot; height=&quot;350&quot; data-filename=&quot;스크린샷 2023-05-25 오후 10.36.54.png&quot; data-origin-width=&quot;1588&quot; data-origin-height=&quot;844&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 그림을 보자. 위에 써있는 MAC, IP 같은 정보는 클라이언트의 조회 메시지이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트 측의 DNS 서버는 클라이언트와 같은 장소에, 라우팅 테이블을 입수한 라우터는 캐시 서버의 설치 장소에 있다고 가정해보자.&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;서버 측의 DNS 서버는 캐시 서버에 있는 라우터로부터 라우팅 테이블을 받게 된다&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라우팅 테이블의 정보를 조합하다 보면 어떠한 라우터 A -&amp;gt; 프로바이더 X -&amp;gt; 프로바이더 Y -&amp;gt; 클라이언트 측 DNS 서버... 이런 식으로 경로에 대한 정보를 알 수 있기 때문에 대략적인 거리 정를 파악할 수 있게 된다.&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;서버 측 DNS 서버는라우터 개수만큼 라우터 테이블을 가지고 있게 되며, 이러한&lt;span style=&quot;color: #ef5369;&quot;&gt; 경로 정보와 받은 송신처 IP 주소를 통해&lt;/span&gt; 캐시 서버와 클라이언트 측의 DNS 서버 사이의 거리를 조사하여 가장 가까운 캐시 서버의 IP 주소를 찾는다. (모든 라우터 정보에 대해 비교하여 가장 가까운 라우터 찾기)&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;style3&quot; /&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&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;b&gt;Location&lt;/b&gt;'이라는 HTTP 헤더를 사용하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Location 헤더에 담긴 리소스 위치로 이동하여 가까운 서버로 이동해나가는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고, 이렇게 이동하는 과정을 '&lt;span style=&quot;color: #ef5369;&quot;&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;span style=&quot;color: #ef5369;&quot;&gt;리다이렉트용 서버를 웹 서버측의 DNS 서버에 등록&lt;/span&gt;하게 되며, 클라이언트는 DNS 서버에 등록된 정보를 바탕으로 HTTP 리퀘스트를 보내게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 리다이렉트 서버 역시 &lt;b&gt;DNS 서버처럼 경로 정보를 모아 가장 가까운 캐시 서버를 찾게 된다&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 캐시 서버를 나타내는 경로를 Location 헤더에 넣어 응답을 전송해주면 클라이언트는 해당 정보를 바탕으로 캐시 서버에 액세스하게 된다. 또한, 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;사실 거의 비슷하지만, 첫 번째 방법은 가장 가까운 캐시 서버의 IP 주소를 반환해서 해당 캐시 서버에 접속하는 느낌이고...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째 방법은 HTTP 메시지를 별도의 서버로 보내서 해당 서버에서 경로 정보를 계산하여 헤더 값으로 받는 느낌...?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째 방법은 http 이전 단계인 것 같고, 두 번째 방법은 http 메시지를 통해 얻는 것 같은데 헷갈린다.&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;style3&quot; /&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;p data-ke-size=&quot;size16&quot;&gt;기존에는 한 번 액세스한 데이터를 두 번째 액세스 시 반환하도록 하는 방법이 있었는데 (ttl 고려) 이 역시 기존 데이터와의 정합성을 보장해줄 수 없기 때문에, &lt;span style=&quot;color: #ef5369;&quot;&gt;웹 서버에서 기존의 데이터를 갱신할 경우 캐시 서버와 동기화를 진행&lt;/span&gt;해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러면 항상 캐시 서버의 데이터가 최신이라는 것을 보장할 수 있으며, CDN의 캐시 서버는 이에 대한 방법이 내장되어 있다.&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;예를 들어, 앞서 말했던 AWS CloudFront의 경우 기본적으로 파일의 캐시가 24시간 정보 유지된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP 헤더의 Cache-Control 헤더를 통해서 캐시의 유지 시간을 설정할 수 있지만, 개발자가 직접 캐시가 만료되기 전 파일 내용을 갱신할 수 있는 'Invalidation'이라는 기능을 제공하고 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;667&quot; data-origin-height=&quot;180&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cDDG7k/btshp6p1CuH/XMXJnqKnmdZoG1Eld4n890/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cDDG7k/btshp6p1CuH/XMXJnqKnmdZoG1Eld4n890/img.png&quot; data-alt=&quot;http://blog.a-cloud.co.kr/2020/01/23/invalidation%EC%9C%BC%EB%A1%9C-cloudfront-%EC%BD%98%ED%85%90%EC%B8%A0-%EA%B0%B1%EC%8B%A0%ED%95%98%EA%B8%B0cache-control/&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cDDG7k/btshp6p1CuH/XMXJnqKnmdZoG1Eld4n890/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcDDG7k%2Fbtshp6p1CuH%2FXMXJnqKnmdZoG1Eld4n890%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;667&quot; height=&quot;180&quot; data-origin-width=&quot;667&quot; data-origin-height=&quot;180&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;http://blog.a-cloud.co.kr/2020/01/23/invalidation%EC%9C%BC%EB%A1%9C-cloudfront-%EC%BD%98%ED%85%90%EC%B8%A0-%EA%B0%B1%EC%8B%A0%ED%95%98%EA%B8%B0cache-control/&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한, 캐시 규칙을 찾아보았는데 기본적으로 CDN의 캐시 정책은 HTTP에 따르는 것 같다.&lt;/p&gt;
&lt;figure id=&quot;og_1685023678045&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Cache Rules_Content Delivery Network_User Guide_Domain Name Settings_Cache Settings_Huawei Cloud&quot; data-og-description=&quot;You can configure the maximum age for one or more cached resources on CDN nodes. If the maximum age of a file cached on CDN nodes has reached, CDN requests the most recent content of the file from the origin server when a user requests the file. CDN return&quot; data-og-host=&quot;support.huaweicloud.com&quot; data-og-source-url=&quot;https://support.huaweicloud.com/intl/en-us/usermanual-cdn/cdn_01_0116.html&quot; data-og-url=&quot;https://support.huaweicloud.com/intl/en-us/usermanual-cdn/cdn_01_0116.html&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/n5oUE/hySLzmWSB2/qhRCM0B3awnkzuQMtK5G11/img.png?width=859&amp;amp;height=401&amp;amp;face=0_0_859_401,https://scrap.kakaocdn.net/dn/bB4pDv/hySKG2hugf/1NUZMmqEoCwrwTCC6QMFYK/img.png?width=991&amp;amp;height=293&amp;amp;face=0_0_991_293,https://scrap.kakaocdn.net/dn/qpaep/hySKIy234e/QqekgByG1ktPi6Qx7FQKsk/img.png?width=757&amp;amp;height=357&amp;amp;face=0_0_757_357&quot;&gt;&lt;a href=&quot;https://support.huaweicloud.com/intl/en-us/usermanual-cdn/cdn_01_0116.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://support.huaweicloud.com/intl/en-us/usermanual-cdn/cdn_01_0116.html&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/n5oUE/hySLzmWSB2/qhRCM0B3awnkzuQMtK5G11/img.png?width=859&amp;amp;height=401&amp;amp;face=0_0_859_401,https://scrap.kakaocdn.net/dn/bB4pDv/hySKG2hugf/1NUZMmqEoCwrwTCC6QMFYK/img.png?width=991&amp;amp;height=293&amp;amp;face=0_0_991_293,https://scrap.kakaocdn.net/dn/qpaep/hySKIy234e/QqekgByG1ktPi6Qx7FQKsk/img.png?width=757&amp;amp;height=357&amp;amp;face=0_0_757_357');&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;Cache Rules_Content Delivery Network_User Guide_Domain Name Settings_Cache Settings_Huawei Cloud&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;You can configure the maximum age for one or more cached resources on CDN nodes. If the maximum age of a file cached on CDN nodes has reached, CDN requests the most recent content of the file from the origin server when a user requests the file. CDN return&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;support.huaweicloud.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;You can control cache aging by configuring the Cache-Control: max-age&amp;nbsp;field in an HTTP response header. By leveraging cache rules, you can optimize cache periods for different services.&amp;nbsp;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애초에 개발자가 캐싱에 대한 규칙을 정할 수 있는 것처럼 보인다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;  퀴즈&lt;/b&gt;&lt;br /&gt;&lt;b&gt;Q. 현재 주류가 되어 있는 방화벽의 유형이 무엇일까?&lt;/b&gt;&lt;br /&gt;A. 패킷 필터링형&lt;br /&gt;&lt;br /&gt;&lt;b&gt;Q. 방화벽에서 애플리케이션 종류를 지정할 때 점검하는 정보는?&lt;/b&gt;&lt;br /&gt;A. 포트 번호&lt;br /&gt;&lt;br /&gt;&lt;b&gt;Q. 웹 서버의 부하를 막기 위해 액세스를 여러 대의 서버에 분배하는 장치는?&lt;/b&gt;&lt;br /&gt;A. 로드 밸런서&lt;br /&gt;&lt;br /&gt;&lt;b&gt;Q. 서버 측에 설치하는 프록시는 무엇일까?&lt;/b&gt;&lt;br /&gt;A. 리버스 프록시&lt;br /&gt;&lt;br /&gt;&lt;b&gt;Q. 인터넷에 다수의 캐시 서버를 설치하고, 웹 서버 운영자에게 대출하는 서비스는?&lt;/b&gt;&lt;br /&gt;A. CDS&amp;nbsp;&lt;/blockquote&gt;</description>
      <category>✏️/Network</category>
      <category>CDN</category>
      <category>캐시서버</category>
      <author>dolmeng2</author>
      <guid isPermaLink="true">https://cl8d.tistory.com/92</guid>
      <comments>https://cl8d.tistory.com/92#entry92comment</comments>
      <pubDate>Thu, 25 May 2023 23:14:49 +0900</pubDate>
    </item>
    <item>
      <title>[우테코 5기] 지하철 미션 회고</title>
      <link>https://cl8d.tistory.com/91</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;가비와 진행한 레벨 2 세 번째 페어 프로그래밍 미션인 지하철 미션이다...!&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;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;스크린샷 2023-05-22 오전 10.19.01.png&quot; data-origin-width=&quot;1138&quot; data-origin-height=&quot;482&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qxStg/btsgL97TYPY/GCj7JrI4Mo2AReLdpSJrlK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qxStg/btsgL97TYPY/GCj7JrI4Mo2AReLdpSJrlK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qxStg/btsgL97TYPY/GCj7JrI4Mo2AReLdpSJrlK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqxStg%2FbtsgL97TYPY%2FGCj7JrI4Mo2AReLdpSJrlK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;673&quot; height=&quot;285&quot; data-filename=&quot;스크린샷 2023-05-22 오전 10.19.01.png&quot; data-origin-width=&quot;1138&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;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h4 style=&quot;background-color: #ffffff; color: #5c5c5c; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;background-color: #ffffff; color: #5c5c5c; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;✔️ 작성한 코드&lt;/b&gt;&lt;/h4&gt;
&lt;figure id=&quot;og_1684718396511&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 - Cl8D/jwp-subway-path: 레벨 2 지하철 미션 레파지토리&quot; data-og-description=&quot;레벨 2 지하철 미션 레파지토리. Contribute to Cl8D/jwp-subway-path development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/Cl8D/jwp-subway-path&quot; data-og-url=&quot;https://github.com/Cl8D/jwp-subway-path&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cyjwEA/hySHfRefil/jVD4eXrjjvpzKuf8Pq18X1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/Cl8D/jwp-subway-path&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/Cl8D/jwp-subway-path&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cyjwEA/hySHfRefil/jVD4eXrjjvpzKuf8Pq18X1/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 - Cl8D/jwp-subway-path: 레벨 2 지하철 미션 레파지토리&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;레벨 2 지하철 미션 레파지토리. Contribute to Cl8D/jwp-subway-path 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;
&lt;h4 style=&quot;background-color: #ffffff; color: #5c5c5c; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;✔️ 1차 PR&lt;/b&gt;&lt;/h4&gt;
&lt;figure id=&quot;og_1684718426184&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;[1단계 - 지하철 정보 관리 기능] 져니(이지원) 미션 제출합니다. by Cl8D &amp;middot; Pull Request #45 &amp;middot; woowacourse/&quot; data-og-description=&quot;안녕하세요, 또링! 져니입니다  &amp;zwj;♀️ 레벨 1의 첫 미션 리뷰어가 또링이었는데 벌써 레벨 2가 되었네요...! 다시 만나서 반가워요   이번 미션은 시간이 많이 부족해서 제가 봐도 코드가 너&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/woowacourse/jwp-subway-path/pull/45&quot; data-og-url=&quot;https://github.com/woowacourse/jwp-subway-path/pull/45&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/c1F41A/hySJmuoMtW/SYUUKKwlEmsLj9mPKa0plK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/woowacourse/jwp-subway-path/pull/45&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/woowacourse/jwp-subway-path/pull/45&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/c1F41A/hySJmuoMtW/SYUUKKwlEmsLj9mPKa0plK/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;[1단계 - 지하철 정보 관리 기능] 져니(이지원) 미션 제출합니다. by Cl8D &amp;middot; Pull Request #45 &amp;middot; woowacourse/&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;안녕하세요, 또링! 져니입니다  &amp;zwj;♀️ 레벨 1의 첫 미션 리뷰어가 또링이었는데 벌써 레벨 2가 되었네요...! 다시 만나서 반가워요   이번 미션은 시간이 많이 부족해서 제가 봐도 코드가 너&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;background-color: #ffffff; color: #5c5c5c; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;✔️ 2차 PR&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;figure id=&quot;og_1684718438631&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;[2단계 - 경로 조회 기능] 져니(이지원) 미션 제출합니다. by Cl8D &amp;middot; Pull Request #111 &amp;middot; woowacourse/jwp-subwa&quot; data-og-description=&quot;안녕하세요, 또링! 져니입니다! 지난 1단계 때 피드백을 빠르게 주신 덕분에 2단계도 금방 구현할 수 있었습니다~! 2단계 - 경로 조회 기능 (DB 설정, 경로 조회 API, 요금 조회 기능)에 해당하는 부&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/woowacourse/jwp-subway-path/pull/111&quot; data-og-url=&quot;https://github.com/woowacourse/jwp-subway-path/pull/111&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/b59ufX/hySHbgWow1/L6z7rW5Kia6Kca5kZxQKJ0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/woowacourse/jwp-subway-path/pull/111&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/woowacourse/jwp-subway-path/pull/111&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/b59ufX/hySHbgWow1/L6z7rW5Kia6Kca5kZxQKJ0/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;[2단계 - 경로 조회 기능] 져니(이지원) 미션 제출합니다. by Cl8D &amp;middot; Pull Request #111 &amp;middot; woowacourse/jwp-subwa&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;안녕하세요, 또링! 져니입니다! 지난 1단계 때 피드백을 빠르게 주신 덕분에 2단계도 금방 구현할 수 있었습니다~! 2단계 - 경로 조회 기능 (DB 설정, 경로 조회 API, 요금 조회 기능)에 해당하는 부&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h4 style=&quot;background-color: #ffffff; color: #5c5c5c; 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;- 지하철 노선에 역을 등록 / 제거하는 API 구현&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;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;- 노선에 2개만 역이 남았을 경우 모두 제거해야 한다.&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;- 출발역에서 도착역까지 갈 수 있는 최단 경로 출력 및 요금 정보 조회 API 추가&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;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;&amp;nbsp;&lt;/h4&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 위주가 아닌, 도메인 위주의 코드를 작성하고 싶어서 처음부터 도메인을 위주로 작성했었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;노선과 지하철 역, 그리고 노선에 등록된 역 정보를 나타내기 위해 구간 정보를 나타내는 3개의 도메인을 메인&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-05-22 오후 4.50.37.png&quot; data-origin-width=&quot;994&quot; data-origin-height=&quot;564&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GW72c/btsg1txRV38/3YSE1Kr864tZ2s5uv3Ln7k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GW72c/btsg1txRV38/3YSE1Kr864tZ2s5uv3Ln7k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GW72c/btsg1txRV38/3YSE1Kr864tZ2s5uv3Ln7k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGW72c%2Fbtsg1txRV38%2F3YSE1Kr864tZ2s5uv3Ln7k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;561&quot; height=&quot;318&quot; data-filename=&quot;스크린샷 2023-05-22 오후 4.50.37.png&quot; data-origin-width=&quot;994&quot; data-origin-height=&quot;564&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;   Station&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;지하철 이름 말고 특별한 정보가 없기 때문에 딱히 설명할 것이 없다 ㅎㅎ&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;b&gt; &lt;/b&gt;&amp;nbsp;Section&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;처음에는 역의 하행, 상행 정보를 알기 위해서 이에 대한 boolean 필드를 추가할까 고민했었는데, map을 사용해서 이는 해결할 수 있는 문제여서 추후 수정하게 되었다. (허브 갓  )&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;&lt;b&gt; &lt;/b&gt;&amp;nbsp;Line&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;노선 도메인의 경우 이름과 색상, 추가 요금, 그리고 노선에 대한 역 정보인 SubwayLine을 가지고 있도록 하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Section에서 Line을 참조하는 식으로 설계를 하신 크루들도 몇몇 봤었는데, 테이블상에서는 Section 테이블이 Line 테이블을 참조하는 것이 자연스럽지만 실세계에서&lt;span style=&quot;color: #ef5369;&quot;&gt; '노선이 역을 가진다'라는 조건에 부합하지 않는다고 생각&lt;/span&gt;했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 노선 도메인이 SubwayLine이라는 도메인을 가지도록 만들었고, &lt;b&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;size18&quot;&gt;&lt;b&gt;&lt;b&gt; &lt;/b&gt;&amp;nbsp;SubwayLine&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SubwayLine이라는 도메인은 &lt;span style=&quot;color: #ef5369;&quot;&gt;하나의 노선에 대한 구간 정보 리스트와 순서대로 정렬되어 있는 역 리스트&lt;/span&gt;를 가지고 있으며, Line&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 DB와 가장 간극이 큰 도메인이 아닐까 싶다. (이 과정에서 객체지향적 설계를 정말 많이 고민했다.)&lt;/p&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;객체에게 책임을 주기 위해 DB에서 조회해온 도메인과 실제 연산을 통해 변경된 부분에 대해서 변경 감지 (프록시 객체를 이용하거나, 혹은 차집합을 이용하거나...)를 활용하여 업데이트를 한 크루들도 보였었는데, 오히려 프로그램의 복잡도가 너무 증가하는 것 같아서 실제 sections 정보가 업데이트 되더라도 도메인의 필드는 변경되지 않도록 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대신, &lt;span style=&quot;color: #ef5369;&quot;&gt;새로운 구간 정보가 들어올 수 있는지에 대한 '검증 작업'을 위주로 도메인에게 책임을 전가하도록 설계&lt;/span&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;스크린샷 2023-05-22 오후 5.58.12.png&quot; data-origin-width=&quot;2586&quot; data-origin-height=&quot;1062&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ylXEw/btsgUtTvUKU/rfVTONIZyDGki6EFp8IoKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ylXEw/btsgUtTvUKU/rfVTONIZyDGki6EFp8IoKK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ylXEw/btsgUtTvUKU/rfVTONIZyDGki6EFp8IoKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FylXEw%2FbtsgUtTvUKU%2FrfVTONIZyDGki6EFp8IoKK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2586&quot; height=&quot;1062&quot; data-filename=&quot;스크린샷 2023-05-22 오후 5.58.12.png&quot; data-origin-width=&quot;2586&quot; data-origin-height=&quot;1062&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드 부분을 대충 가져왔다. 이런 식으로 Line 도메인을 생성한 다음에, subwayLine을 뽑아냈다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;  여기서 Line의 subwayLine을 뒤늦게 업데이트 하는 이유는 단순히 라인에 대한 조회 후, 라인과 연관된 섹션 정보를 따로 조회하기 때문이다.&amp;nbsp;&lt;br /&gt;&lt;b&gt;findById를 통해서 요청받은 노선의 아이디가 유효한지 검증&lt;/b&gt;하고, 유효하다면 섹션 정보를 조회하는 식으로 구현하려고 하다 보니 저런 구조가 됐다. 또한, findById에서 굳이 여러 정보를 가져오려고 조합하는 것보다 단순히 line에 대한 정보만 조회되도록 만들고 싶었기 때문이다. findById는 비교적 가볍게 쓰이는 메서드라고 생각했기 때문이다... ㅎㅎ&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;그리고 validateSection을 통해서 요청받은 도메인에 대한 검증 작업을 진행한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-05-22 오후 6.03.35.png&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;590&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9oDoc/btsgMatuubu/w52Y81ZvfC5QRCxkBSm4h1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9oDoc/btsgMatuubu/w52Y81ZvfC5QRCxkBSm4h1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9oDoc/btsgMatuubu/w52Y81ZvfC5QRCxkBSm4h1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9oDoc%2FbtsgMatuubu%2Fw52Y81ZvfC5QRCxkBSm4h1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;275&quot; data-filename=&quot;스크린샷 2023-05-22 오후 6.03.35.png&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;590&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;이런 식으로 &lt;b&gt;도메인이 내부적으로 검증할 수 있는 내용은 검증하도록 만들면서 책임을 넘기고, 실제 DB에 삽입, 삭제, 수정해야 하는 부분은 서비스에게 책임을 넘겨&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;또한, SubwayLine이 정렬된 역 정보에 대해서 필드로 관리하고 있는 게 약간 찝찝하기도 했었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;sections 정보를 통해서 역 정보는 충분히 유추 가능한 정보이기 때문에, 사실 필드로 관리하지 않고 필요할 때마다 정렬해서 사용하는 방법도 있기 때문이다.&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;pre id=&quot;code_1684746684061&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private List&amp;lt;Station&amp;gt; sort(final List&amp;lt;Section&amp;gt; sections) {
    final Map&amp;lt;Station, Station&amp;gt; stationRelationShip = getStationRelationShip(sections);
    final Optional&amp;lt;Station&amp;gt; startStation = getStartStation(stationRelationShip);
    return startStation.map(station -&amp;gt; getSortedStations(stationRelationShip, station))
        .orElse(Collections.emptyList());
}

private Map&amp;lt;Station, Station&amp;gt; getStationRelationShip(final List&amp;lt;Section&amp;gt; sections) {
    return sections.stream()
        .collect(Collectors.toMap(Section::source, Section::target));
}

private Optional&amp;lt;Station&amp;gt; getStartStation(final Map&amp;lt;Station, Station&amp;gt; stationRelationShip) {
    return stationRelationShip.keySet().stream()
        .filter(station -&amp;gt; !stationRelationShip.containsValue(station))
        .findFirst();
}

private List&amp;lt;Station&amp;gt; getSortedStations(final Map&amp;lt;Station, Station&amp;gt; stationRelationShip,
                                        final Station startStation) {
    final List&amp;lt;Station&amp;gt; sortedStations = new ArrayList&amp;lt;&amp;gt;();
    Optional&amp;lt;Station&amp;gt; nextStation = Optional.ofNullable(startStation);
    while (nextStation.isPresent()) {
        sortedStations.add(nextStation.get());
        nextStation = Optional.ofNullable(stationRelationShip.get(nextStation.get()));
    }
    return sortedStations;
}&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&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;pre id=&quot;code_1684746940751&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CREATE TABLE IF NOT EXISTS station
(
    id bigint auto_increment NOT NULL,
    name VARCHAR(255) NOT NULL UNIQUE,
    PRIMARY KEY(id)
);

CREATE TABLE IF NOT EXISTS line
(
    id bigint auto_increment NOT NULL,
    name VARCHAR(255) NOT NULL UNIQUE,
    color VARCHAR(20) NOT NULL,
    extra_fare int NOT NULL,
    PRIMARY KEY(id)
);

CREATE TABLE IF NOT EXISTS section
(
    id bigint auto_increment NOT NULL,
    line_id bigint NOT NULL,
    source_station_id bigint NOT NULL,
    target_station_id bigint NOT NULL,
    distance int NOT NULL,
    PRIMARY KEY(id),
    FOREIGN KEY(line_id) REFERENCES line(id) ON DELETE CASCADE,
    FOREIGN KEY(source_station_id) REFERENCES station(id),
    FOREIGN KEY(target_station_id) REFERENCES station(id)
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 외래키에 대해서도 정말 의견이 분분하게 나뉘는데, 개인적으로 이렇게 작은 규모라면 외래키를 걸어두는 게 더 좋을 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 그만큼 테스트가 빡세지만... 테스트는 픽스처를 활용하여 어느 정도 해소할 수 있을 것 같다.&lt;/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;그리고, section 테이블의 필드에는&lt;span style=&quot;color: #ef5369;&quot;&gt; on delete cascade&lt;/span&gt; 옵션을 주었는데, 이는 부모 테이블의 row가 제거되었을 때 함께 제거되어 section의 고아 필드(?)가 남지 않도록 만들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서도 말했던 것처럼 section의 경우 line의 정보와 연관되어 있는 정보이기 때문에, &lt;span style=&quot;color: #ef5369;&quot;&gt;노선이 제거된 이후에도 해당 노선의 구간 정보가 존재할 필요가 없다고 생각&lt;/span&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;그리고, 원래 구현할 때는 station 쪽에도 on delete cascade를 걸어두었었는데, 생각해보니까 역 정보가 제거된다고 해서 구간 정보를 바로 제거해버리면 기존 구간에 대한 거리 업데이트가 안 될 것 같아서 회고를 작성하며 간단하게 리팩터링을 해보았다.&lt;/p&gt;
&lt;pre id=&quot;code_1684747968563&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public int deleteById(final Long id) {
    try {
        final String sql = &quot;DELETE FROM station WHERE id = ?&quot;;
        return jdbcTemplate.update(sql, id);
    } catch (final DataIntegrityViolationException e) {
        throw new BadRequestException(ErrorCode.STATION_DELETE_IF_EXISTS_SECTION);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그냥 이렇게 FK에 대한 오류가 발생하면 이미 구간에 대한 정보가 있기 때문에 역을 제거할 수 없다는 예외를 반환하도록 만들었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-05-22 오후 6.33.23.png&quot; data-origin-width=&quot;832&quot; data-origin-height=&quot;222&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/VqiKJ/btsg2kATtvb/CmJ2TEGT8VE4cSKdvDVUZ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/VqiKJ/btsg2kATtvb/CmJ2TEGT8VE4cSKdvDVUZ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/VqiKJ/btsg2kATtvb/CmJ2TEGT8VE4cSKdvDVUZ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVqiKJ%2Fbtsg2kATtvb%2FCmJ2TEGT8VE4cSKdvDVUZ1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;593&quot; height=&quot;158&quot; data-filename=&quot;스크린샷 2023-05-22 오후 6.33.23.png&quot; data-origin-width=&quot;832&quot; data-origin-height=&quot;222&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&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;  인터페이스로 의존성 역전시키기 (DIP)&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;저번 장바구니 미션 회고 글을 작성하면서 마지막에 다음과 같은 커멘트를 남겼었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-05-22 오후 6.52.16.png&quot; data-origin-width=&quot;1406&quot; data-origin-height=&quot;176&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dP7Njr/btsgNX8tvtR/7WNpKuzm8hW4P67BwjMVVk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dP7Njr/btsgNX8tvtR/7WNpKuzm8hW4P67BwjMVVk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dP7Njr/btsgNX8tvtR/7WNpKuzm8hW4P67BwjMVVk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdP7Njr%2FbtsgNX8tvtR%2F7WNpKuzm8hW4P67BwjMVVk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1406&quot; height=&quot;176&quot; data-filename=&quot;스크린샷 2023-05-22 오후 6.52.16.png&quot; data-origin-width=&quot;1406&quot; data-origin-height=&quot;176&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-05-22 오후 9.01.07.png&quot; data-origin-width=&quot;696&quot; data-origin-height=&quot;326&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/UD4vG/btsg2Y5Ea1s/o8HCL4wZzjRtP89b4fCoSK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/UD4vG/btsg2Y5Ea1s/o8HCL4wZzjRtP89b4fCoSK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/UD4vG/btsg2Y5Ea1s/o8HCL4wZzjRtP89b4fCoSK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUD4vG%2Fbtsg2Y5Ea1s%2Fo8HCL4wZzjRtP89b4fCoSK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;417&quot; height=&quot;195&quot; data-filename=&quot;스크린샷 2023-05-22 오후 9.01.07.png&quot; data-origin-width=&quot;696&quot; data-origin-height=&quot;326&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스에서 바로 Dao를 참조하다 보니, Dao가 반환하는 값인&lt;b&gt; Entity와 의존 관계&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;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-05-22 오후 9.05.46.png&quot; data-origin-width=&quot;1626&quot; data-origin-height=&quot;174&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/X7qGs/btsgKorUERV/fOgivtXvNMIzvQBXMsuMeK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/X7qGs/btsgKorUERV/fOgivtXvNMIzvQBXMsuMeK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/X7qGs/btsgKorUERV/fOgivtXvNMIzvQBXMsuMeK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FX7qGs%2FbtsgKorUERV%2FfOgivtXvNMIzvQBXMsuMeK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1626&quot; height=&quot;174&quot; data-filename=&quot;스크린샷 2023-05-22 오후 9.05.46.png&quot; data-origin-width=&quot;1626&quot; data-origin-height=&quot;174&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Repository 계층을 추가해서 서비스 레이어는 도메인만 바라보고, &lt;span style=&quot;color: #ef5369;&quot;&gt;도메인과 엔티티를 변환하는 작업을 Repository에게 위임한 것&lt;/span&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;하지만, DAO와 Repository는 Persistence layer에 존재하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;application layer가 Repository를 import 하면서 여전히 persistence layer를 의존하며 domain 패키지에 존재하는 도메인 객체와 모두 상호 참조하게된다는 단점이 존재했다.&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: #ef5369;&quot;&gt;Repository의 인터페이스는 도메인 레이어에 두고&lt;/span&gt;, &lt;span style=&quot;color: #0593d3;&quot;&gt;이에 대한 구현체를 persistence layer에 두어 의존 관계를 역전&lt;/span&gt;시키는 전략을 채택하였다! 덕분에 Application Layer에서는 도메인 계층의 패키지만 의존하게 되어서 더 이상 순환 참조가 발생하지 않았다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-05-22 오후 9.36.30.png&quot; data-origin-width=&quot;806&quot; data-origin-height=&quot;578&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cUFvbb/btsgTh6MoOw/d5o7chCvkQcsQYy5khn3P1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cUFvbb/btsgTh6MoOw/d5o7chCvkQcsQYy5khn3P1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cUFvbb/btsgTh6MoOw/d5o7chCvkQcsQYy5khn3P1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcUFvbb%2FbtsgTh6MoOw%2Fd5o7chCvkQcsQYy5khn3P1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;394&quot; height=&quot;283&quot; data-filename=&quot;스크린샷 2023-05-22 오후 9.36.30.png&quot; data-origin-width=&quot;806&quot; data-origin-height=&quot;578&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 서비스 레이어의 import를 보면 application, domain으로만 참조 관계가 형성된 것을 확인할 수 있다.&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;스크린샷 2023-05-22 오후 9.51.05.png&quot; data-origin-width=&quot;1640&quot; data-origin-height=&quot;472&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/t4QT9/btsgNXU6uYy/XiCIg1L9vFrbTdWSeWIU7k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/t4QT9/btsgNXU6uYy/XiCIg1L9vFrbTdWSeWIU7k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/t4QT9/btsgNXU6uYy/XiCIg1L9vFrbTdWSeWIU7k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Ft4QT9%2FbtsgNXU6uYy%2FXiCIg1L9vFrbTdWSeWIU7k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1640&quot; height=&quot;472&quot; data-filename=&quot;스크린샷 2023-05-22 오후 9.51.05.png&quot; data-origin-width=&quot;1640&quot; data-origin-height=&quot;472&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;p data-ke-size=&quot;size18&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;최단 경로를 구하기 위하여 jgrapht 라이브러리를 사용했어야 했는데, 부끄럽지만 내 처음 제출 코드는 다음과 같았다.&lt;/p&gt;
&lt;pre id=&quot;code_1684760093508&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// RouteService.java
private WeightedMultigraph&amp;lt;Station, DefaultWeightedEdge&amp;gt; getPossibleRouteGraph(
    final Map&amp;lt;Long, List&amp;lt;LineWithSectionRes&amp;gt;&amp;gt; sectionsByLineId) {
    final WeightedMultigraph&amp;lt;Station, DefaultWeightedEdge&amp;gt; routeGraph = new WeightedMultigraph&amp;lt;&amp;gt;(
        DefaultWeightedEdge.class);
    sectionsByLineId.values()
        .forEach(sectionRes -&amp;gt; addRouteBySectionRes(routeGraph, sectionRes));
    return routeGraph;
}&lt;/code&gt;&lt;/pre&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-05-22 오후 6.39.39.png&quot; data-origin-width=&quot;1404&quot; data-origin-height=&quot;196&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/db9itQ/btsgMcSqvik/EQKy0ahONmFvbI4XuslY1K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/db9itQ/btsgMcSqvik/EQKy0ahONmFvbI4XuslY1K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/db9itQ/btsgMcSqvik/EQKy0ahONmFvbI4XuslY1K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdb9itQ%2FbtsgMcSqvik%2FEQKy0ahONmFvbI4XuslY1K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1404&quot; height=&quot;196&quot; data-filename=&quot;스크린샷 2023-05-22 오후 6.39.39.png&quot; data-origin-width=&quot;1404&quot; data-origin-height=&quot;196&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;어떤 방법을 사용하면 좋을지 고민하다, 인터페이스를 활용하기로 결정하였다.&lt;/p&gt;
&lt;pre id=&quot;code_1684760377852&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface GraphProvider {

    List&amp;lt;Station&amp;gt; getShortestPath(final List&amp;lt;SubwayLine&amp;gt; sections, final Station sourceStation,
                                  final Station targetStation);
}&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;그리고, 외부 서비스임을 나타내는 'infra' 패키지에 JgraphService를 추가하였다.&lt;/p&gt;
&lt;pre id=&quot;code_1684760503388&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
public class JgraphtService implements GraphProvider {

    @Override
    public List&amp;lt;Station&amp;gt; getShortestPath(final List&amp;lt;SubwayLine&amp;gt; sections,
                                         final Station sourceStation,
                                         final Station targetStation) {
        final WeightedMultigraph&amp;lt;Station, DefaultWeightedEdge&amp;gt; routeGraph = createRouteGraph(sections);
        validateRequestRoute(sourceStation, targetStation, routeGraph);
        return getShortestPath(sourceStation, targetStation, routeGraph);
    }
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 내부는 그냥 최단 경로 구하는 코드여서 의미는 없을 것 같아 첨부하지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인자로 넘겨주는 값이 많기는 하지만,&lt;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;이렇게 설계한 덕분에 application layer에서는 도메인 계층의 GraphProvider 인터페이스만 바라보고 있어 추후 다른 라이브러리로 교체되더라도 application layer에 대한 변경사항은 없어 영향 범위를 최소화할 수 있게 되었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  개인적으로 infra 패키지는 왜 항상 인터페이스를 사용하는 것일까 궁금했는데... 이렇게 직접 구성하면서 완전히 이해할 수 있었다.&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;style3&quot; /&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&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;pre id=&quot;code_1684760918439&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public Fare calculateFare(final TotalDistance distance) {
    if (distance.lessThan(EXTRA_FARE_SECTION_TEN)) {
        return BASIC_FARE;
    }
    if (distance.lessAndEqualsThan(EXTRA_FARE_SECTION_FIFTY)) {
        final TotalDistance extraDistance = distance.subtract(EXTRA_FARE_SECTION_TEN);
        return BASIC_FARE.add(calculateByDistance(extraDistance, FARE_SECTION_UNIT_FIVE));
    }
    final TotalDistance fareSectionDistance = EXTRA_FARE_SECTION_FIFTY.subtract(EXTRA_FARE_SECTION_TEN);
    final TotalDistance extraDistance = distance.subtract(EXTRA_FARE_SECTION_FIFTY);
    final Fare extraFare = calculateByDistance(fareSectionDistance, FARE_SECTION_UNIT_FIVE)
        .add(calculateByDistance(extraDistance, FARE_SECTION_UNIT_EIGHT));
    return BASIC_FARE.add(extraFare);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;10km, 10~50km, 50km 이상에 대해 각각 분기문을 통해 처리하고 있다 보니 확장에 매우 취약한 구조가 된 것이다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;OCP를 위반하고 있어요. 어떻게 개선해볼수 있을까요? (Hint. 해당 구간에 판단하는지 여부와 요금을 계산하는 메서드가 공통적이네요)&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요기서 리뷰어님이 굉장한 힌트를 주셨다. &lt;span style=&quot;color: #ef5369;&quot;&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;그래서 아예 정책별로 클래스를 나누기로 결정하였다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선, 각 정책에 대해 공통적인 행위를 정의하는 인터페이스를 생성해두었다.&lt;/p&gt;
&lt;pre id=&quot;code_1684762206032&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface FarePolicy {

    boolean isAvailable(final Distance distance);

    Fare calculateFare(final Distance distance);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;isAvailable가 구간에 대해 정책 적용이 가능한지 판단하는 메서드이고, calculateFare가 실제로 요금을 계산하는 메서드이다.&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-05-22 오후 10.28.37.png&quot; data-origin-width=&quot;2074&quot; data-origin-height=&quot;1274&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/I8QCu/btsg3W7CQYI/dkxXRKBC04ZN5NiSk6Skp1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/I8QCu/btsg3W7CQYI/dkxXRKBC04ZN5NiSk6Skp1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/I8QCu/btsg3W7CQYI/dkxXRKBC04ZN5NiSk6Skp1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FI8QCu%2Fbtsg3W7CQYI%2FdkxXRKBC04ZN5NiSk6Skp1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;536&quot; height=&quot;329&quot; data-filename=&quot;스크린샷 2023-05-22 오후 10.28.37.png&quot; data-origin-width=&quot;2074&quot; data-origin-height=&quot;1274&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고,&lt;b&gt; 해당 인터페이스를 구현한 3개의 구체 정책 클래스를 생성&lt;/b&gt;하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;10km 이하에 대한 기본 정책은 BasicFarePolicy, 10~50km는 UnitFiveFarePolicy, 50km는 UnitEightFarePolicy로 설정했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이름이 좀 아쉽긴 한데... 5km당, 8km당 100원씩 추가된다는 게 더 중요한 것 같아서 저렇게 만들어두었다 ㅎㅎ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;공통적으로 사용하는 필드 값은 enum을 통해서 정의&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;span style=&quot;color: #ef5369;&quot;&gt;'FarePolicyComposite'라는 클래스를 통해 한 번에 관리&lt;/span&gt;하고 있는 것이다.&lt;/p&gt;
&lt;pre id=&quot;code_1684762356894&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public Fare getTotalFare(final Distance shortestDistance) {
    return farePolicies.stream()
        .filter(policy -&amp;gt; policy.isAvailable(shortestDistance))
        .map(policy -&amp;gt; policy.calculateFare(shortestDistance))
        .findFirst()
        .orElseThrow();
}&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 data-ke-size=&quot;size16&quot;&gt;현재 설계상 구간 정보는 각 정책 중 하나의 범위에 무조건 들어가기 때문에 orElseThrow()로 던져질 위험이 없을 것이라 판단하였고, 이렇게 되면 60km 이상의 구간에서는 다른 정책을 추가해야 할 때&lt;b&gt; farePolices 리스트에 정책을 추가해주기만 하면 되기 때문에 확장에 열린 구조를 유지할 수 있었다&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;  단, 이 구조를 채택하기 위해서는 isAvailable의 조건 정보가 촘촘해야 된다는 점이다. 현재 구조는 촘촘하게 설계되어 괜찮지만!&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;각 정책들은 스프링 빈으로 관리되고 있기 때문에 @Configuration을 활용하면 쉽게 주입이 가능해진다.&lt;/p&gt;
&lt;pre id=&quot;code_1684762533142&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
public class FarePolicyConfig {

    @Bean
    public FarePolicyComposite farePolicies(
        final FarePolicy basicFarePolicy,
        final FarePolicy unitFiveFarePolicy,
        final FarePolicy unitEightFarePolicy
    ) {
        return new FarePolicyComposite(
            List.of(basicFarePolicy, unitFiveFarePolicy, unitEightFarePolicy)
        );
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 파라미터로 주입되는 클래스의 이름은 꼭 빈에 등록된 클래스의 이름과 맞춰줘야 한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;  스프링에서&amp;nbsp; 기본적으로 파라미터의 타입을 기준으로 빈을 조회한다.&lt;br /&gt;만약, 같은 타입의 빈이 여러 개 조회되면 &lt;span style=&quot;color: #ef5369;&quot;&gt;추가적으로 필드나 파라미터 이름으로 빈을 조회&lt;/span&gt;한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 원리를 바탕으로 'FarePolicyComposite' 타입의 'farePolicies'라는 이름을 가진 빈을 등록했기 때문에, application layer에서도 다음과 같이 주입받아서 사용하면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1684762779962&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
public class RouteService {
    ...
    private final FarePolicyComposite farePolicies;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 바탕으로 추가 요금 정책도 동일하게 구현할 수 있었다 ㅎㅎ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나 같은 경우는 나이 정보를 따로 받지 않고, 그냥 할인된 가격도 함께 반환하도록 만들다 보니 가능했던 것 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1684762852173&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public DiscountFare getDiscountFares(final Fare totalFare) {
    final List&amp;lt;Fare&amp;gt; fares = discountFarePolicies.stream()
        .map(policy -&amp;gt; policy.calculateFare(totalFare))
        .collect(Collectors.toUnmodifiableList());
    return new DiscountFare(fares.get(0), fares.get(1));
}&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;만약 나이를 입력받는다면 이렇게 map -&amp;gt; collect 대신에 요금 정책을 적용했던 것처럼 filter를 통해 받을 수 있을 것 같다.&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;style3&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쓸 내용이 별로 없을 것이라고 생각했는데 쓰다 보니까 오늘 하루가 다 가버렸다  &lt;/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;</description>
      <category>우아한테크코스/레벨 2</category>
      <category>우아한테크코스</category>
      <category>우테코</category>
      <category>우테코5기</category>
      <category>지하철미션</category>
      <author>dolmeng2</author>
      <guid isPermaLink="true">https://cl8d.tistory.com/91</guid>
      <comments>https://cl8d.tistory.com/91#entry91comment</comments>
      <pubDate>Mon, 22 May 2023 22:47:06 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] Rest-docs 연동하기 (version 3.3.2)</title>
      <link>https://cl8d.tistory.com/73</link>
      <description>&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;사실 rest-docs는 미션 요구사항은 아니었지만, 개인적인 욕심으로 한 번 직접 구축해보고 싶다는 생각이 들었어서 미션하는 김에 함께 진행하게 되었다. (어차피 추후 미션 진행하면서 프론트 크루와 협업하게 되면 세팅해야 하니까...)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RestDocs를 연동할 때 인수 테스트 레벨에서 진행할지 (RestAssured) 아니면 컨트롤러의 단위 테스트에서 진행할지 고민했었는데, RestAssured를 통해 사용하게 되면 BDD 스타일이라 더 직관적일 것 같긴 하지만 아무래도 속도가 느리다 보니까 그냥 컨트롤러 레이어에서 진행했다. (실제로도 컨트롤러 레벨에서 많이 진행하는 것 같다.)&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;800&quot; data-origin-height=&quot;585&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wzmMs/btsdpy354kQ/akPy4iDXsheZph7edJNsR0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wzmMs/btsdpy354kQ/akPy4iDXsheZph7edJNsR0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wzmMs/btsdpy354kQ/akPy4iDXsheZph7edJNsR0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwzmMs%2Fbtsdpy354kQ%2FakPy4iDXsheZph7edJNsR0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;356&quot; height=&quot;260&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;585&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;style3&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;✔️ Rest-docs가 어떤 거지?&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Rest Docs는 &lt;span style=&quot;color: #ef5369;&quot;&gt;Restful 서비스에 대해 정확하고 읽기 쉬운 문서를 생성할 때 도움을 주는 문서 자동화 도구&lt;/span&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흔히 백엔드, 프론트엔드가 협업할 때 프론트엔드는 API endPoint를 알고 있어야 해당 API로 요청을 보내서 response를 받아 처리한다. 만약 백엔드 개발자가 API 문서를 수정했다면 프론트엔드 개발자한테 어떻게 알려야 할까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직접 메신저로  &amp;zwj;♀️ 'oo님, xxx endpoint의 쿼리 파라미터 요구사항이 변경되었어요' 라고 말할 수도 있겠지만, 이는 매우 번거롭다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 프론트엔드 개발자가 당장 수정해야 하는 hotfix 요구사항임에도 메신저를 읽지 못했다면? 혹은, 원하는 시간에 서로 소통할 수 없다면? 하나의 정형화된 문서를 통해서 관리하는 게 훨씬 쉬울 것이다.&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;Swagger를 사용하는 경우도 있고, Rest docs를 사용하는 경우도 있을 텐데 각각의 장단점이 뚜렷하다.&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;1033&quot; data-origin-height=&quot;487&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/u2AfS/btsdeG93S38/hR2scwPaJkVukgtDWNhxlk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/u2AfS/btsdeG93S38/hR2scwPaJkVukgtDWNhxlk/img.png&quot; data-alt=&quot;https://jojoldu.tistory.com/31&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/u2AfS/btsdeG93S38/hR2scwPaJkVukgtDWNhxlk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fu2AfS%2FbtsdeG93S38%2FhR2scwPaJkVukgtDWNhxlk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1033&quot; height=&quot;487&quot; data-origin-width=&quot;1033&quot; data-origin-height=&quot;487&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://jojoldu.tistory.com/31&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Swagger의 경우 테스트 코드 작성에 부담이 없지만, 프로덕션 코드 자체에 문서에 대한 설정 내용이 어노테이션으로 들어가기 때문에 이 자체가 부담이 될 수 있다. Swagger에서 Rest docs로 전환해야 한다면 &lt;span style=&quot;color: #ef5369;&quot;&gt;프로덕션 코드 자체를 전부 바꿔야 하기&lt;/span&gt; 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, Rest-docs보다는 비교적 적용하기가 쉬우며, 문서 자체는 우리가 직접 구축할 필요없이 알아서 만들어주고, &lt;span style=&quot;color: #ef5369;&quot;&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;반면에 Rest-docs의 경우 테스트 코드를 기반으로 하기 때문에 &lt;span style=&quot;color: #ef5369;&quot;&gt;프로덕션 코드에는 영향을 끼치지 않는다&lt;/span&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 말은 곧, 테스트 코드가 정말 꼼꼼하게 짜여 있어야 한다는 것이다. 성공했을 때, 예외가 발생했을 때 등등... response 값이 변화하여 내려가는 경우에는 웬만하면 테스트 코드를 작성해야 한다. (그렇기 때문에 RestAssured보다 덜 무거운 MockMvc를 활용하는 게 더 낫다고 판단한 점도 있다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한, 스웨거에 비해 구축이 어려운 편이다. 문서 구축 자체는 빌드된 adoc 파일을 직접 조립해야 하고, 프론트엔드 개발자가 직접 테스트해볼 수도 없다. 하지만, 직접 구축하기 때문에 &lt;span style=&quot;color: #ef5369;&quot;&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;span style=&quot;color: #333333;&quot;&gt;개인적으로 규모가 작다면 swagger를 사용해볼만 하다고 생각하지만, 프로덕션 코드 자체를 수정하는 게 찝찝하기 때문에 rest-docs를 조금 더 선호하는 편이다. (물론 그때부터 테스트 지옥이지만... ㅎ_ㅎ) 한 번 적용해두면 그뒤부터 응용은 쉽기 때문에 처음 러닝커브만 조금 견디면 된다!&lt;/span&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;style3&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;✔️ SetUp - build.gradle&lt;/b&gt;&lt;/h4&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;  그전에, Rest Docs의 경우 기본적으로 '&lt;/span&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;Asciidoctor&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;'라는 친구를 사용하는데, 이 친구는 일반 텍스트를 처리하면서 HTM L 형태로 만들어주는 친구다. 이 친구의 개념 자체가 중요한 건 아니지만, HTML로 문서를 생성하기 위한 일종의 전처리 도구라고 생각하자.&lt;/span&gt;&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;  또한, 각각의 테스트 코드가 성공하면 '&lt;span style=&quot;color: #ef5369;&quot;&gt;snippet&lt;/span&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;먼저, build.gradle에 다음과 같은 내용을 추가해야 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1682917309970&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;plugins {
    id &quot;org.asciidoctor.jvm.convert&quot; version &quot;3.3.2&quot; 
}

configurations {
    asciidoctorExt 
}

ext {
    snippetsDir = file('build/generated-snippets') 
}

test {
    outputs.dir snippetsDir 
}

asciidoctor {
    inputs.dir snippetsDir 
    configurations 'asciidoctorExt' 
    dependsOn test 
    
    sources {
        include(&quot;**/index.adoc&quot;) 
    }

    baseDirFollowsSourceFile() 
}

asciidoctor.doFirst {
    delete file('src/main/resources/templates/docs') 
}

task copyDocument(type: Copy) { 
    dependsOn asciidoctor
    from file(&quot;build/docs/asciidoc&quot;)
    into file(&quot;src/main/resources/templates/docs&quot;)
}

build {
    dependsOn copyDocument 
}

dependencies {
    asciidoctorExt 'org.springframework.restdocs:spring-restdocs-asciidoctor'
    testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc' 
}

bootJar {
    dependsOn asciidoctor 
    from (&quot;${asciidoctor.outputDir}/html5&quot;) { 
        into 'static/docs'
    }
}&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;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;plugins {
    id &quot;org.asciidoctor.jvm.convert&quot; version &quot;3.3.2&quot; (1)
}

configurations {
    asciidoctorExt (2) 
}

ext {
    snippetsDir = file('build/generated-snippets') (3)
}

tasks.named('test') {
    outputs.dir snippetsDir (4)
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(1): Asciidoctor 플러그인을 적용한다.&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;(2): asciidoctorExt를 프로젝트의 configuration에 추가한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;asciidoctorExt는 Asciidoctor를 사용하여 문서를 생성하고 포맷하는 데 사용하는 플러그인인데, 이를 이용하여 HTML 형식으로 출력할 수 있도록 만든다고 생각하면 된다.&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): 생성된 스니펫들을 어느 위치에 저장할 것인지 지정하는 것이다. (디폴트로 build 폴더 밑에 생성하는 편)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(4): 저장한 스니펫 경로가 테스트 결과로 생성되는 디렉터리에 추가되도록 설정하는 것이다.&lt;/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;pre id=&quot;code_1682945879594&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;asciidoctor { 
    inputs.dir snippetsDir (5)
    configurations 'asciidoctorExt' (6)
    dependsOn test (7)
    
    sources { 
        include(&quot;**/index.adoc&quot;) (8)
    }

    baseDirFollowsSourceFile() (9)
}

asciidoctor.doFirst {
    delete file('src/main/resources/templates/docs') (10)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(5) asciidoctor가 할 일을 정의하는 부분이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전에 설정한 스니펫 저장 경로를 입력 디렉터리로 만들어서, asciidoctor가 문서를 생성할 때 어디를 참조할지 지정하는 것이다.&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;(6) ext에서 구성한 asciidoctorExt를 구성 파일로 사용하도록 만드는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(asciidoctor의 configuration을 설정할 건데, 어떤 설정 정보를 참조할 것인지 확인하는 느낌?)&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;(7) 문서가 생성되기 전에 테스트가 실행될 수 있도록, test에 종속적이도록 만든다. (test 실행 -&amp;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;(8) rest-docs 문서의 경우, .adoc 파일을 바탕으로 생성하게 되는데 이때 source를 지정하게 되면 특정 adoc만 HTML로 만든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 모든 adoc에 대해 각각 html을 만들고 싶다면 해당 옵션은 제거해도 된다.&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;(9) 특정 .adoc에 다른 adoc을 include 하고 싶을 때 경로를 baseDir로 맞춰주는 역할이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나 같은 경우 index.adoc으로 세부 adoc을 로드하도록 만들었기 때문에 이 옵션을 활성화해주었다.&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;(10) static.docs 파일을 초기화하고 다시 만 들도록 설정 하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로는 static 하위에 두는 경우가 많은데, 미션에서 타임리프를 사용하다 보니 templates 폴더 하위로 설정하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1682946005509&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;task copyDocument(type: Copy) { (11)
    dependsOn asciidoctor
    from file(&quot;build/docs/asciidoc&quot;)
    into file(&quot;src/main/resources/templates/docs&quot;)
}

build {
    dependsOn copyDocument (12)
}

dependencies { 
    asciidoctorExt 'org.springframework.restdocs:spring-restdocs-asciidoctor' (13)
    testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc' (14)
}

bootJar { 
    dependsOn asciidoctor (15)
    from (&quot;${asciidoctor.outputDir}/html5&quot;) { 
        into 'templates/docs'
    }
}&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;(11) asciidoctor 작업한 이후 (dependsOn), build/docs/asciidoc을 보면 상단에서 설정해준 source 옵션에 따라 html이 생기게 된다. 나 같은 경우 프로덕션 코드에서 html에 접근할 수 있도록 하기 위해서 resources/templates/docs에 복사를 진행하였다.&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;(12) 빌드 작업이 copyDocument 이후에 진행되도록 설정한다.&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;(13) asciidoctorExt configuration을 사용하여 restdocs-asciidoctor 라이브러리에 대한 의존성을 설정한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 .adoc 파일에서 빌드, 생성된 스니펫을 가리키도록 스니펫 속성이 자동으로 구성된다.&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;(14) MockMVC를 활용하여 rest-docs를 구성할 때 사용한다. (Rest-Assured를 사용하면 spring-restdocs-restassured 사용)&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;(15) 생성된 문서를 jar 파일에 패키징해서 정적 컨텐츠로 제공할 때 사용하는 것이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;asciidoctor에 종속적이도록 만들어서, jar 파일을 빌드하기 전에 문서가 생성되었는지 체크한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 되면 jar 파일이 빌드되기 전에 문서가 생성되고, 생성된 문서는 jar 파일에 포함된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;asciidoctor 진행 이후 문서를 templates/docs로 이동 시킨다.&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;  그래서 결과적으로 ./gradlew clean build를 진행하게 되면 test -&amp;gt; asciidoctor -&amp;gt; copyDocument -&amp;gt; build 순으로 세팅이 진행된다.&lt;/p&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;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;✔️ 코드에 적용하기 (Junit5 기준)&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;controller가 여러 개라면 공통적으로 사용할 부분은 빼는 것이 좋기 때문에, 나는 별도의 helper 클래스를 생성해두었다.&lt;/p&gt;
&lt;pre id=&quot;code_1682919829967&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@ExtendWith(RestDocumentationExtension.class) (1)
public class RestDocsHelper {

    private MockMvc mockMvc;

    private RestDocumentationResultHandler documentationResultHandler;

    @BeforeEach
    void setUp(final WebApplicationContext webApplicationContext,
               final RestDocumentationContextProvider restDocumentationContextProvider) {
  
        this.documentationResultHandler = MockMvcRestDocumentation.document( 
                &quot;{class-name}/{method-name}&quot;, (2)
                Preprocessors.preprocessRequest(Preprocessors.prettyPrint()), (3)
                Preprocessors.preprocessResponse(Preprocessors.prettyPrint()));

        this.mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext) (4)
                .addFilter(new CharacterEncodingFilter(&quot;UTF-8&quot;, true)) (5)
                .alwaysDo(MockMvcResultHandlers.print()) (6)
                .alwaysDo(documentationResultHandler) (7)
                .apply(documentationConfiguration(restDocumentationContextProvider)) (8)
                .build();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(1): Junit5에서 사용할 수 있으며, Spring Rest Docs의 RestDocumentationExtension 클래스를 사용하여 테스트를 확장시키는 것이다. 어렵게 생각할 필요없이, 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;(2): 문서의 세부 경로를 설정해주는 부분이다. build/generated-snippets 패키지 밑에, class-name 패키지, 그리고&amp;nbsp; method-name 패키지 하단에 각각의 스니펫이 생성되도록 만드는 설정이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 사실 이 부분에 나중에 배포될 때를 대비하여 프로토콜 정보나 host 정보도 지정해줄 수 있다. (Preprocessors.modifyUris 활용)&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) request, response에 대해서 전처리 작업을 해주는 것인데 요청, 응답에 대해 읽기 쉽도록 만들어준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(4): 각각의 테스트 메서드가 실행되기 전에 공통적으로 실행할 작업을 정의한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;mockMvc에 대해서 설정하는 것을 볼 수 있는데, 이를 통해 Spring REST docs에서 생성된 컨텍스트를 사용하여 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;(5) 문서 생성 시 한글이 깨질 수 있기 때문에 이를 위해 인코딩 형식을 지정해주는 부분이다.&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;(6) 처리한 내용에 대해 항상 로깅 작업을 수행하도록 print를 해주는 부분이다.&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;(7) 위에서 설정한 정보들을 항상 적용하도록 만드는 것이다.&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;style3&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생각보다 글이 길어질 것 같아서, 실제로 코드에 어떤 식으로 적용되는지는 다음 포스팅에서 작성할 예정이다  &lt;/p&gt;</description>
      <category>Back-end/Spring</category>
      <category>rest-docs</category>
      <category>RestDocs</category>
      <category>spring</category>
      <author>dolmeng2</author>
      <guid isPermaLink="true">https://cl8d.tistory.com/73</guid>
      <comments>https://cl8d.tistory.com/73#entry73comment</comments>
      <pubDate>Mon, 22 May 2023 13:56:57 +0900</pubDate>
    </item>
    <item>
      <title>[Network] 서버의 액세스가 증가할 때 어떻게 처리할까? - 캐시 서버와 포워드 프록시, 리버스 프록시</title>
      <link>https://cl8d.tistory.com/90</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  DNS 서버를 통해 다중 서버로 분산 처리하기&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;단일 서버의 한계를 극복하기 위해서는&lt;b&gt;, 여러 대의 서버를 이용하여 처리를 분산시킬 수 있는데&lt;/b&gt; 이를 '&lt;span style=&quot;color: #ef5369;&quot;&gt;분산 처리&lt;/span&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; DNS 서버를 통해 분배&lt;/b&gt;하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DNS 서버에 동일한 이름으로 여러 대의 웹 서버를 등록해둔다면, 조회가 있을 때마다 조회된 IP 서버를 차례로 반환한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때, 라운드 로빈 방식을 통해 균등하게 액세스를 분산시킬 수 있도록 돕는다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;  aaa.com이라는 도메인에 대해 192.0.2.60, 192.0.2.70, 192.0.2.80이 있다면 아래와 같은 순서로 반환한다.&lt;/b&gt;&lt;br /&gt;- 192.0.2.60, 192.0.2.70, 192.0.2.80&lt;br /&gt;- 192.0.2.70, 192.0.2.80, 192.0.2.60&lt;br /&gt;- 192.0.2.80, 192.0.2.60, 192.0.2.70&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;DNS 서버는 웹 서버가 고장났는지 확인할 수 없기 때문에 고장난 서버의 IP 주소를 반환해버릴수도 있다.&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;style3&quot; /&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&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;DNS 서버를 이용한 라운드 로빈 웹 서버 분배 방식의 단점을 극복하기 위해 별도의 부하 분산 장치를 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DNS 서버에 &lt;span style=&quot;color: #ef5369;&quot;&gt;웹 서버 자체가 아닌, 부하 분산 장치를 대신 등록&lt;/span&gt;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 클라이언트가 DNS 서버에서 IP 주소를 받아갈 때 부하 분산 장치의 IP 주소를 받아가기 때문에 부하 분산 장치 쪽으로 요청을 보내게 될 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-05-21 오후 10.15.25.png&quot; data-origin-width=&quot;2506&quot; data-origin-height=&quot;1372&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9FVpe/btsgC3OQ6WD/YusEBc9ZJuuOrKblkVi0y0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9FVpe/btsgC3OQ6WD/YusEBc9ZJuuOrKblkVi0y0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9FVpe/btsgC3OQ6WD/YusEBc9ZJuuOrKblkVi0y0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9FVpe%2FbtsgC3OQ6WD%2FYusEBc9ZJuuOrKblkVi0y0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;631&quot; height=&quot;345&quot; data-filename=&quot;스크린샷 2023-05-21 오후 10.15.25.png&quot; data-origin-width=&quot;2506&quot; data-origin-height=&quot;1372&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;하게 되는데, 기본적으로 웹 서버와 정기적으로 정보를 교환하여 CPU나 메모리 사용량 등을 수집하여 부하 상태를 확인한다. (혹은 패킷을 전송하여 응답 시간을 바탕으로 판단할 수도 있다.)&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;ex) A라는 사용자가 상품을 주문 (주문 요청) -&amp;gt; 결제하고 (결제 요청) -&amp;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;기본적으로 HTTP는 리퀘스트 메시지를 보내기 전에 TCP 접속 동작을 진행하고, 응답 메시지를 반환하면 연결을 끊는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 상태에 대한 정보가 없기 때문에 (stateless) 서로 관계있는 요청인지 알 수 없는 것이다.&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;&lt;a title=&quot;쿠키 / 세션 방식 정리&quot; href=&quot;https://cl8d.tistory.com/77&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;&lt;a title=&quot;⭐️ JWT 인증 방식 정리&quot; href=&quot;https://cl8d.tistory.com/83&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;⭐️ JWT 인증 방식 정리&lt;/a&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;style3&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&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;그래서 같은 기능의 서버를 설치하는 대신, &lt;span style=&quot;color: #ef5369;&quot;&gt;캐시 서버&lt;/span&gt;를 두어서 다른 역할을 주는 방법도 있다.&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;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;/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;p data-ke-size=&quot;size16&quot;&gt;캐시 서버도 마찬가지로 부하 분산 장치에 실제 웹 서버 대신에 DNS 서버에 등록된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 사용자는 캐시 서버에 HTTP 리퀘스트 메시지를 보내게 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-05-21 오후 10.45.40.png&quot; data-origin-width=&quot;2782&quot; data-origin-height=&quot;1196&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cDFLyc/btsgEeiuS3O/SrHRQYOzJerCwNTC4EoD4K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cDFLyc/btsgEeiuS3O/SrHRQYOzJerCwNTC4EoD4K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cDFLyc/btsgEeiuS3O/SrHRQYOzJerCwNTC4EoD4K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcDFLyc%2FbtsgEeiuS3O%2FSrHRQYOzJerCwNTC4EoD4K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;779&quot; height=&quot;335&quot; data-filename=&quot;스크린샷 2023-05-21 오후 10.45.40.png&quot; data-origin-width=&quot;2782&quot; data-origin-height=&quot;1196&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약, 캐시에 저장되어 있지 않다면 &lt;b&gt;Via 헤더에 프록시 서버 정보를 추가&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;gt; 웹 서버에서만 Via가 추가되었는데 캐시 서버 -&amp;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;- 보통 리퀘스트의 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;반대로, 캐시에 데이터가 저장되어 있었다면 웹 서버 측에게&amp;nbsp; 데이터가 변경되었는지 조사하기 위해&lt;span style=&quot;color: #ef5369;&quot;&gt; If-Modified-Since&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-05-21 오후 10.57.13.png&quot; data-origin-width=&quot;2172&quot; data-origin-height=&quot;652&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pLMsu/btsgL9Nr7l6/SdxQcXaXgqZ3pjGQRGA5Rk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pLMsu/btsgL9Nr7l6/SdxQcXaXgqZ3pjGQRGA5Rk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pLMsu/btsgL9Nr7l6/SdxQcXaXgqZ3pjGQRGA5Rk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpLMsu%2FbtsgL9Nr7l6%2FSdxQcXaXgqZ3pjGQRGA5Rk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2172&quot; height=&quot;652&quot; data-filename=&quot;스크린샷 2023-05-21 오후 10.57.13.png&quot; data-origin-width=&quot;2172&quot; data-origin-height=&quot;652&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 서버는 If-modified-since 필드 값과 데이터의 최종 갱신 일시를 비교하여 &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;캐시 서버는 변경사항이 없음을 파악했기 때문에 캐시 서버에 저장된 데이터를 반환하게 된다.&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;스크린샷 2023-05-21 오후 10.57.26.png&quot; data-origin-width=&quot;2222&quot; data-origin-height=&quot;696&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/D1J22/btsgL90XAIg/ptGuh11XVwD7ZIYL1UKlkK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/D1J22/btsgL90XAIg/ptGuh11XVwD7ZIYL1UKlkK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/D1J22/btsgL90XAIg/ptGuh11XVwD7ZIYL1UKlkK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FD1J22%2FbtsgL90XAIg%2FptGuh11XVwD7ZIYL1UKlkK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2222&quot; height=&quot;696&quot; data-filename=&quot;스크린샷 2023-05-21 오후 10.57.26.png&quot; data-origin-width=&quot;2222&quot; data-origin-height=&quot;696&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;.&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;참고로, If-Modified-Since 헤더의 경우 GET이나 HEAD 요청에서만 사용할 수 있다!&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;style3&quot; /&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;size18&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;보통 클라이언트 측에 둔 프록시의 경우 방화벽처럼 원하는 패킷만 전송하도록 하는 것이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;패킷 필터링형 방화벽의 경우 IP 주소나 포트 번호 정보만 보지만, &lt;span style=&quot;color: #ef5369;&quot;&gt;프록시는 HTTP 리퀘스트 내용까지 함께 볼 수 있기 때문에 조금 더 세미하게 액세스 제한을 걸 수 있다는 것&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-05-21 오후 11.25.16.png&quot; data-origin-width=&quot;2824&quot; data-origin-height=&quot;842&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Dcml1/btsgDKnZKCB/Pkb7TjKIJbogRgajretm5k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Dcml1/btsgDKnZKCB/Pkb7TjKIJbogRgajretm5k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Dcml1/btsgDKnZKCB/Pkb7TjKIJbogRgajretm5k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDcml1%2FbtsgDKnZKCB%2FPkb7TjKIJbogRgajretm5k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2824&quot; height=&quot;842&quot; data-filename=&quot;스크린샷 2023-05-21 오후 11.25.16.png&quot; data-origin-width=&quot;2824&quot; data-origin-height=&quot;842&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;포워드 프록시를 하게 되면 URL의 내용에 관계 없이 클라이언트의 요청은 전부 프록시 쪽으로 전송된다.&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;URI 정보만 리퀘스트 URI에 기록하지만, 포워드 프록시를 활용하면 URL 전부를 기록하게 된다&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇기 때문에 캐시 서버처럼 URI에 따라 전송할 웹 서버를 사전에 지정해줄 필요도 없이, 모든 웹 서버에서 전송할 수 있게 된다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;  포워드 프록시는 보통 다음과 같은 상황에서 많이 사용한다.&lt;br /&gt;- &lt;b&gt;접속 제한 회피&lt;/b&gt; : 방화벽을 통해 제한된 액세스를 제공하는 사이트에 대해서 회피하여 이용 가능&lt;br /&gt;- &lt;b&gt;특정 컨텐츠 제한:&lt;/b&gt; 특정 클라이언트가 특정 리소스에 접근하는 것을 막을 수 있음&lt;br /&gt;- &lt;b&gt;캐싱&lt;/b&gt;: 요청의 응답값을 웹 서버까지 가지 않더라도 프록시에서 반환 가능&lt;br /&gt;- &lt;b&gt;IP 우회&lt;/b&gt;: 웹 서버 측은 클라이언트의 정보를 알지 못하고 프록시에 대한 정보만 받을 수 있게 된다.&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&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;스크린샷 2023-05-21 오후 11.29.32.png&quot; data-origin-width=&quot;2782&quot; data-origin-height=&quot;868&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bkHObG/btsgFdv88Y3/YqkC81lly10ZsDqg3wOhkk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bkHObG/btsgFdv88Y3/YqkC81lly10ZsDqg3wOhkk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bkHObG/btsgFdv88Y3/YqkC81lly10ZsDqg3wOhkk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbkHObG%2FbtsgFdv88Y3%2FYqkC81lly10ZsDqg3wOhkk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2782&quot; height=&quot;868&quot; data-filename=&quot;스크린샷 2023-05-21 오후 11.29.32.png&quot; data-origin-width=&quot;2782&quot; data-origin-height=&quot;868&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;리버스 프록시의 경우 &lt;span style=&quot;color: #ef5369;&quot;&gt;웹 서버 / WAS 앞단에 위치&lt;/span&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;리버스 프록시의 경우 클라이언트가 실제 서버를 알 수 없다.&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;  리버스 프록시는 보통 다음과 같은 상황에서 많이 사용한다.&lt;br /&gt;- &lt;b&gt;로드 밸런싱&lt;/b&gt;: 리버스 프록시 서버를 단일 서버 앞에 두어, 특정 서버의 과부하를 막는 방법이다.&lt;br /&gt;- &lt;b&gt;서버 보안&lt;/b&gt;: 서버의 IP 주소를 노출시키지 않기 때문에 DDos 공격 차단에 용이하다.&lt;br /&gt;- &lt;b&gt;캐싱&lt;/b&gt;: 포워드 프록시의 캐싱과 유사하다. 마찬가지로 요청을 웹 서버까지 보내지 않고 바로 응답할 수 있다.&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;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 95px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 25%; height: 19px; text-align: center;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 33.0233%; height: 19px; text-align: center;&quot;&gt;&lt;b&gt;포워드 프록시&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 41.9767%; height: 19px; text-align: center;&quot;&gt;&lt;b&gt;리버스 프록시&amp;nbsp;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 25%; height: 19px; text-align: center;&quot;&gt;&lt;b&gt;어디에서?&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.0233%; height: 19px; text-align: center;&quot;&gt;클라이언트 앞단&lt;/td&gt;
&lt;td style=&quot;width: 41.9767%; height: 19px; text-align: center;&quot;&gt;웹&amp;nbsp;서버&amp;nbsp;/&amp;nbsp;WAS&amp;nbsp;앞단&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 25%; height: 19px; text-align: center;&quot;&gt;&lt;b&gt;통신은?&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.0233%; height: 19px; text-align: center;&quot;&gt;클라이언트 &amp;lt;-&amp;gt; 프록시&lt;/td&gt;
&lt;td style=&quot;width: 41.9767%; height: 19px; text-align: center;&quot;&gt;프록시&amp;nbsp;&amp;lt;-&amp;gt;&amp;nbsp;웹&amp;nbsp;서버&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 25%; height: 19px; text-align: center;&quot;&gt;&lt;b&gt;실제 웹 요청은?&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.0233%; height: 19px; text-align: center;&quot;&gt;웹 서버&lt;/td&gt;
&lt;td style=&quot;width: 41.9767%; height: 19px; text-align: center;&quot;&gt;프록시&amp;nbsp;서버&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 25%; height: 19px; text-align: center;&quot;&gt;&lt;b&gt;감춰지는 정보는?&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.0233%; height: 19px; text-align: center;&quot;&gt;클라이언트 정보&lt;/td&gt;
&lt;td style=&quot;width: 41.9767%; height: 19px; text-align: center;&quot;&gt;서버 정보&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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: #ef5369;&quot;&gt;네트워크 내부에서 요청을 받아서 다른 곳&lt;/span&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;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: #ef5369;&quot;&gt;네트워크 바깥에서 네트워크 내부로 들어오는 요청을 받아서 서버로 전달&lt;/span&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;&amp;nbsp;&lt;/p&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;리퀘스트 메시지의 패킷 맨 앞에 기록된 &lt;b&gt;수신처 IP 주소를 조사하여 액세스 대상 웹 서버를 판단&lt;/b&gt;하는 방법이 트랜스 페어런트 프록시이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;브라우저에서 웹 서버로 흘러가는 메시지를 가로채서&lt;/span&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;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;인터넷을 사용하기 위해 반드시 지나가야 하는 게이트웨이&lt;/b&gt;'에서 웹 요청을 가로채며 동작하는 방식이라고 한다. (강제로 위임하는 느낌?)&lt;/p&gt;</description>
      <category>✏️/Network</category>
      <category>리버스프록시</category>
      <category>포워드프록시</category>
      <author>dolmeng2</author>
      <guid isPermaLink="true">https://cl8d.tistory.com/90</guid>
      <comments>https://cl8d.tistory.com/90#entry90comment</comments>
      <pubDate>Mon, 22 May 2023 00:00:41 +0900</pubDate>
    </item>
    <item>
      <title>[JPA] OSIV 찍먹하기 2편 - 준영속 상태 엔티티를 지연 로딩하기 (FetchType.EAGER와 N+1 문제, fetch join)</title>
      <link>https://cl8d.tistory.com/89</link>
      <description>&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 title=&quot;지난 포스팅&quot; href=&quot;https://cl8d.tistory.com/88&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;이는, &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;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;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  글로벌 페치 전략을 LAZY에서 EAGER로 수정하기&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단하게 말하면 지연 로딩을 사용하지 말고, 즉시 로딩을 사용하자는 것이다  &lt;/p&gt;
&lt;pre id=&quot;code_1684655127416&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Crew {

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

    @Column(nullable = false)
    private String name;

    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = &quot;wootecho_id&quot;, nullable = false, foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT))
    private Wootecho wootecho;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;크루 클래스의 fetch 전략을 EAGER로 설정하였다. (어차피 @ManyToOne의 디폴트 전략이 fetch이기 때문에 명시적으로 설정하지 않아도 똑같다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉시 로딩을 사용하게 되면, &lt;b&gt;Crew를 조회할 때 연관된 엔티티인 Wootecho 역시 함께 가져오게 된다&lt;/b&gt;.&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-05-21 오후 4.50.27.png&quot; data-origin-width=&quot;660&quot; data-origin-height=&quot;584&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kCzIQ/btsgG6jcDpq/MfjneCutjN8C3bgbNkOK60/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kCzIQ/btsgG6jcDpq/MfjneCutjN8C3bgbNkOK60/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kCzIQ/btsgG6jcDpq/MfjneCutjN8C3bgbNkOK60/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkCzIQ%2FbtsgG6jcDpq%2FMfjneCutjN8C3bgbNkOK60%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;416&quot; height=&quot;368&quot; data-filename=&quot;스크린샷 2023-05-21 오후 4.50.27.png&quot; data-origin-width=&quot;660&quot; data-origin-height=&quot;584&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같이 Join을 통해 한 번에 가져온 것을 볼 수 있다.&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;  join을 통해 가져오는 거면 더 좋은 거 아니야?&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약, crew 엔티티에 연관된 엔티티가 지금처럼 하나가 아니라 여러 개라면?&lt;/p&gt;
&lt;pre id=&quot;code_1684655950230&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Crew {

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

    @Column(nullable = false)
    private String name;

    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = &quot;wootecho_id&quot;, nullable = false, foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT))
    private Wootecho wootecho;

    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = &quot;wootecho2_id&quot;, nullable = false, foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT))
    private Wootecho wootecho2;

    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = &quot;wootecho3_id&quot;, nullable = false, foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT))
    private Wootecho wootecho3;
}&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-05-21 오후 4.59.37.png&quot; data-origin-width=&quot;668&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OAHy8/btsgJZ5bfI3/15h9KKh8PgFnlzBdFPnvyk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OAHy8/btsgJZ5bfI3/15h9KKh8PgFnlzBdFPnvyk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OAHy8/btsgJZ5bfI3/15h9KKh8PgFnlzBdFPnvyk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOAHy8%2FbtsgJZ5bfI3%2F15h9KKh8PgFnlzBdFPnvyk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;361&quot; height=&quot;553&quot; data-filename=&quot;스크린샷 2023-05-21 오후 4.59.37.png&quot; data-origin-width=&quot;668&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상당히 어지러운 쿼리가 나가는 것을 볼 수 있다. 심지어, 위 예제는 정말 간단한 예제인데도 여러 join이 걸리게 되는 것이다.&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;size18&quot;&gt;&lt;b&gt;  N+1 문제&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 JPQL을 사용했을 때의 문제이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPQL을 통해 JPA가 SQL을 사용하게 되면,&lt;span style=&quot;color: #ef5369;&quot;&gt; 글로벌 페치 전략은 신경쓰지 않고 JPQL 자체 정보만 사용하기 때문에 연관된 엔티티에 대해서 join 대신 where 절로 묶인 다수 개의 쿼리가 나가게 된다&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;이 문제를 재현하기 위해서는 OneToMany 관계를 살펴봐야 하기 때문에, 우테코를 기준으로 조회 API를 생성할 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우테코 엔티티를 다음과 같이 수정하자.&lt;/p&gt;
&lt;pre id=&quot;code_1684657131876&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Wootecho {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Enumerated(EnumType.STRING)
    private Course course;

    @OneToMany(fetch = FetchType.EAGER, mappedBy = &quot;wootecho&quot;)
    private List&amp;lt;Crew&amp;gt; crews;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;crew에 대한 정보를 추가해주고, 연관관계의 주인을 Wootecho로 설정해주었다.&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;그리고, JPQL을 사용하기 위해서 위와 같이 커스텀 메서드를 생성해주었다.&lt;/p&gt;
&lt;pre id=&quot;code_1684657160928&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface WootechoRepository extends JpaRepository&amp;lt;Wootecho, Long&amp;gt; {

    @Query(&quot;select w from Wootecho w&quot;)
    List&amp;lt;Wootecho&amp;gt; findWootechos();
}&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;서비스와 컨트롤러, DTO이다. 간단한 예제이기 때문에 추가 설명은 작성하지 않겠다.&lt;/p&gt;
&lt;pre id=&quot;code_1684657337012&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// WootechoService.java
@Service
@RequiredArgsConstructor
public class WootechoService {
    private final WootechoRepository wootechoRepository;

    public List&amp;lt;Wootecho&amp;gt; getAll() {
        return wootechoRepository.findWootechos();
    }
}


// WootechoController.java
@RestController
@RequestMapping(&quot;/wootecho&quot;)
@RequiredArgsConstructor
public class WootechoController {
    private final WootechoService wootechoService;

    @GetMapping
    public ResponseEntity&amp;lt;List&amp;lt;WootechoResponse&amp;gt;&amp;gt; getAll() {
        final List&amp;lt;Wootecho&amp;gt; wootechos = wootechoService.getAll();
        final List&amp;lt;WootechoResponse&amp;gt; result = wootechos.stream()
            .map(WootechoResponse::of)
            .toList();
        return ResponseEntity.ok(result);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로운 WootechoService와 WootechoController를 생성하였다.&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;스크린샷 2023-05-21 오후 5.45.03.png&quot; data-origin-width=&quot;488&quot; data-origin-height=&quot;316&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nuI86/btsgEDIT0i0/oMkx42wb5QQeMek9WrxOBK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nuI86/btsgEDIT0i0/oMkx42wb5QQeMek9WrxOBK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nuI86/btsgEDIT0i0/oMkx42wb5QQeMek9WrxOBK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnuI86%2FbtsgEDIT0i0%2FoMkx42wb5QQeMek9WrxOBK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;199&quot; data-filename=&quot;스크린샷 2023-05-21 오후 5.45.03.png&quot; data-origin-width=&quot;488&quot; data-origin-height=&quot;316&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1684658807775&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Hibernate: 
    select
        w1_0.id,
        w1_0.course 
    from
        wootecho w1_0
Hibernate: 
    select
        c1_0.wootecho_id,
        c1_0.id,
        c1_0.name 
    from
        crew c1_0 
    where
        c1_0.wootecho_id=?
Hibernate: 
    select
        c1_0.wootecho_id,
        c1_0.id,
        c1_0.name 
    from
        crew c1_0 
    where
        c1_0.wootecho_id=?
Hibernate: 
    select
        c1_0.wootecho_id,
        c1_0.id,
        c1_0.name 
    from
        crew c1_0 
    where
        c1_0.wootecho_id=?&lt;/code&gt;&lt;/pre&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-05-21 오후 5.53.48.png&quot; data-origin-width=&quot;944&quot; data-origin-height=&quot;934&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bvcRS1/btsgEfO3EPV/2uKOPl3CthpAErc6xjCvp1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bvcRS1/btsgEfO3EPV/2uKOPl3CthpAErc6xjCvp1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bvcRS1/btsgEfO3EPV/2uKOPl3CthpAErc6xjCvp1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbvcRS1%2FbtsgEfO3EPV%2F2uKOPl3CthpAErc6xjCvp1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;587&quot; height=&quot;581&quot; data-filename=&quot;스크린샷 2023-05-21 오후 5.53.48.png&quot; data-origin-width=&quot;944&quot; data-origin-height=&quot;934&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 Wootecho만 조회하려고 했으나,  &lt;b&gt;즉시 로딩으로 인해 Crew에 대한 정보도 가져오면서 1+N개의 쿼리가 발생&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;JPQL이 코드 분석 시 다음과 같은 절차를 밟으며 발생한 문제이다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;1. select w from Wootecho w를 보고 select * from wootecho 쿼리 생성&lt;br /&gt;2. 생성된 쿼리를 바탕으로 List&amp;lt;Wootecho&amp;gt; 엔티티 생성&lt;br /&gt;3. &lt;b&gt;쿼리가 생성된 이후&lt;/b&gt;, 즉시 로딩 전략으로 인해서 Wootecho와 연관된 Crew 엔티티 역시 함께 즉시 로딩&lt;br /&gt;4. Wootecho 엔티티의 개수만큼 select * from crew where wootecho_id = ? 쿼리 발생&lt;/blockquote&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;style3&quot; /&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  LAZY로 유지하면서 fetch join 사용하기&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지연 로딩을 사용하면서, &lt;span style=&quot;color: #ef5369;&quot;&gt;jpql을 호출하는 시점에 한 번에 가져오도록 최적화를 하는 전략&lt;/span&gt;이다.&lt;/p&gt;
&lt;pre id=&quot;code_1684660001694&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface WootechoRepository extends JpaRepository&amp;lt;Wootecho, Long&amp;gt; {

    @Query(&quot;select w from Wootecho w join fetch w.crews&quot;)
    List&amp;lt;Wootecho&amp;gt; findWootechos();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 jpql에 &lt;b&gt;join fetch를 추가&lt;/b&gt;해주면 된다.&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-05-21 오후 6.07.30.png&quot; data-origin-width=&quot;694&quot; data-origin-height=&quot;536&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/PgMHK/btsgEebzAPq/wyaua8tu3CRbVZ8K0rzysK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/PgMHK/btsgEebzAPq/wyaua8tu3CRbVZ8K0rzysK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PgMHK/btsgEebzAPq/wyaua8tu3CRbVZ8K0rzysK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPgMHK%2FbtsgEebzAPq%2Fwyaua8tu3CRbVZ8K0rzysK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;320&quot; data-filename=&quot;스크린샷 2023-05-21 오후 6.07.30.png&quot; data-origin-width=&quot;694&quot; data-origin-height=&quot;536&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;깔끔하게 join을 이용하여 한 번에 가져온 것을 확인할 수 있다  &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;style3&quot; /&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;  강제로 초기화하기 - Hibernate.initialize()&lt;/b&gt;&lt;/h4&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;JPA는 기본적으로 관련 기능을 제공하지 않기 때문에, 하이버네이트에서 제공하는&lt;span style=&quot;color: #ef5369;&quot;&gt; Hibernate.initialize()&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;크루를 조회할 때 우테코 엔티티를 함께 조회하는 경우&lt;/b&gt;'에 대해 적용해보자.&lt;/p&gt;
&lt;pre id=&quot;code_1684660883616&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Crew {
	...
    
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = &quot;wootecho_id&quot;, nullable = false, foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT))
    private Wootecho wootecho;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 Crew 엔티티에서 Wootecho 엔티티의 페치 전략을 LAZY로 설정해준다. (마찬가지로 Wootecho 엔티티의 Crew 역시 LAZY로 설정해주었다.)&lt;/p&gt;
&lt;pre id=&quot;code_1684660761091&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// CrewService.java
public Crew getById(final Long wootechoId) {
    final Crew findCrew = crewRepository.findById(wootechoId)
        .orElseThrow(() -&amp;gt; new IllegalArgumentException(&quot;존재하지 않는 아이디입니다.&quot;));
    Hibernate.initialize(findCrew.getWootecho());
    return findCrew;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고, 다음과 같이 getById() 메서드에 Hibernate.initialize()를 추가해주었다.&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;기존에는 no Session Exception이 터졌던 것과 다르게, 잘 조회되는 것을 볼 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-05-21 오후 6.24.07.png&quot; data-origin-width=&quot;766&quot; data-origin-height=&quot;674&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wWYvb/btsgDKH8O3U/5Uwqx9U4qowiAPpjzORw11/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wWYvb/btsgDKH8O3U/5Uwqx9U4qowiAPpjzORw11/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wWYvb/btsgDKH8O3U/5Uwqx9U4qowiAPpjzORw11/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwWYvb%2FbtsgDKH8O3U%2F5Uwqx9U4qowiAPpjzORw11%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;443&quot; height=&quot;390&quot; data-filename=&quot;스크린샷 2023-05-21 오후 6.24.07.png&quot; data-origin-width=&quot;766&quot; data-origin-height=&quot;674&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;Hibernate 기술을 직접적으로 사용하는 것이 부담스럽다면, &lt;b&gt;영속성 컨텍스트가 살아있는 범위에서 프레젠테이션 계층이 사용할 데이터를 실제로 사용해버리는 방법&lt;/b&gt;도 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1684661147959&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// CrewService.java
public Crew getById(final Long wootechoId) {
    final Crew findCrew = crewRepository.findById(wootechoId)
        .orElseThrow(() -&amp;gt; new IllegalArgumentException(&quot;존재하지 않는 아이디입니다.&quot;));
    findCrew.getWootecho().getCourse(); // 실제 사용해버리기
    return findCrew;
}&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;span style=&quot;color: #ef5369;&quot;&gt;뷰가 필요한 엔티티에 따라서 서비스 계층의 로직을 변경해야 한다&lt;/span&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 CrewResponse가 아닌 다른 DTO 객체가 생기고, 거기서는 course만 필요한 것이 아닌 name이나 다른 필드들의 값들이 필요하다면? 뷰의 변경사항이 서비스까지 함께 영향을 받기 때문에 유지보수 포인트가 증가하는 것이다.&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;해야 하며, FACADE 패턴을 활용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스 앞단에서 Facade가 1차적으로&amp;nbsp;프레젠테이션 계층과 도메인 모델 사이의 논리적 의존성을 분리해줌으로서, &lt;span style=&quot;color: #ef5369;&quot;&gt;프레젠테이션 계층에서 필요한 프록시 객체를 초기화하는 역할을 담당&lt;/span&gt;하는 것이다.&lt;/p&gt;
&lt;pre id=&quot;code_1684661660974&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
@RequiredArgsConstructor
public class CrewFacade {

    private final CrewService crewService;
    
    public Crew getById(final Long crewId) {
        final Crew crew = crewService.getById(crewId);
        // 프록시 객체가 필요한 데이터 초기화
        crew.getWootecho().getCrews();
        return crew;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1684661696099&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// CrewController.java
@GetMapping(&quot;/{crewId}&quot;)
public ResponseEntity&amp;lt;CrewResponse&amp;gt; getById(@PathVariable Long crewId) {
    final Crew crew = crewFacade.getById(crewId);
    return ResponseEntity.ok(CrewResponse.of(crew));
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만... 이런 식으로 중간 계층이 들어가면 결과적으로 개발자가 작성해야 하는 코드가 많아진다는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한, JPA에 대한 이해도가 없는 개발자와 협업을 하게 된다면 해당 코드에 대해 제대로 이해하지 못한 상태로 제거해버리는 상황이 발생할 수도 있다  &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: #ef5369;&quot;&gt;엔티티가 프레젠테이션 계층에서 준영속 상태이기 때문에&lt;/span&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;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;  OSIV (Open session In View)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;OSIV는 영속성 컨텍스트를 뷰까지 열어둔다&lt;/span&gt;는 의미이다. 뷰까지 컨텍스트가 열려 있기 때문에 자동으로 지연 로딩이 가능해지는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;cf. 참고로, OSIV는 하이버네이트에서 사용하는 용어여서 JPA에서는 OEIV (Open EntityManager In View)를 더 많이 사용하지만, OSIV가 더 익숙하기 때문에 OSIV라고 부르겠다.&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;OSIV는 여러 방식이 있는데, 클라이언트의 요청과 생명주기가 동일한 &lt;span style=&quot;color: #ef5369;&quot;&gt;Transaction per request OSIV&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-05-21 오후 6.47.31.png&quot; data-origin-width=&quot;2938&quot; data-origin-height=&quot;1158&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eiwHqk/btsgFuELSTq/tRRqf7LTUXE8Nz8ZRJ2hc0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eiwHqk/btsgFuELSTq/tRRqf7LTUXE8Nz8ZRJ2hc0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eiwHqk/btsgFuELSTq/tRRqf7LTUXE8Nz8ZRJ2hc0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeiwHqk%2FbtsgFuELSTq%2FtRRqf7LTUXE8Nz8ZRJ2hc0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;769&quot; height=&quot;303&quot; data-filename=&quot;스크린샷 2023-05-21 오후 6.47.31.png&quot; data-origin-width=&quot;2938&quot; data-origin-height=&quot;1158&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지연 로딩 역시 자유롭게 가능하지만, 엔티티와 트랜잭션 생명주기가 너무 길기 때문에 &lt;b&gt;프레젠테이션 영역에서 엔티티를 자유롭게 참조&lt;/b&gt;할 수 있다는 것이다. 엔티티는 코어한 영역이기 때문에 외부에서 쉽게 참조하는 건 좋지 않다. 도메인이 DTO를 참조하지 않는 것과 동일한 영역이라고 봐도 된다. ㅎㅎ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(개인적인 생각으로는 entity가 controller 영역까지 참조되는 걸 선호하지 않기 때문에 그러한 관점에서도 별로다)&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;그러면, 스프링에서는 어떤 OSIV 전략을 채택하고 있을까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 다양한 OSIV 클래스가 있기 때문에 이를 필터나 인터셉터가 등록하여 사용할 수 있다. (중요한 내용은 아니니 생략하겠다)&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;size18&quot;&gt;&lt;b&gt;  스프링 프레임워크가 제공하는 OSIV&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OSIV를 사용하지만, 비즈니스 계층까지만 트랜잭션을 사용하는 전략이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-05-21 오후 6.55.55.png&quot; data-origin-width=&quot;2970&quot; data-origin-height=&quot;1430&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xORIM/btsgDNyzR4A/m5Z2l9qWMfGmN2obLczlOk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xORIM/btsgDNyzR4A/m5Z2l9qWMfGmN2obLczlOk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xORIM/btsgDNyzR4A/m5Z2l9qWMfGmN2obLczlOk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxORIM%2FbtsgDNyzR4A%2Fm5Z2l9qWMfGmN2obLczlOk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2970&quot; height=&quot;1430&quot; data-filename=&quot;스크린샷 2023-05-21 오후 6.55.55.png&quot; data-origin-width=&quot;2970&quot; data-origin-height=&quot;1430&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트 요청이 들어오면 &lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;서블릿 필터나 스프링 필터에서 영속성 컨텍스트를 생성하지만&lt;/b&gt;&lt;/span&gt;, 트랜잭션은 시작하지 않는다&lt;/span&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후, 서비스 계층에서 @Transactional을 시작할 때 초기에 생성한 영속성 컨텍스트를 바탕으로 트랜잭션을 시작한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스 계층이 종료되면 트랜잭션 커밋 후 영속성 컨텍스트를 플러시하고,&lt;span style=&quot;color: #ef5369;&quot;&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;/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;span style=&quot;color: #ef5369;&quot;&gt;엔티티에 대한 변경이 불가능&lt;/span&gt;하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; 정확하게는, 이미 서비스 계층에서 플러시까지 진행되었고 필터-인터셉터에서도 플러시를 호출하지 않기 때문에, 플러시를 호출하지 않아 엔티티의 변경사항에 대해 DB로 반영하지 않는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; 강제로 플러시하더라도 트랜잭션 범위 밖이라 수정할 수 없다는 예외가 발생한다. (TransactionRequiredException)&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;blockquote data-ke-style=&quot;style3&quot;&gt;  여기서 중요한 건, &lt;b&gt;영속성 컨텍스트가 살아있기 때문에 내부에 변경사항이 저장되어 있는 것&lt;/b&gt;이다.&lt;br /&gt;프레젠테이션 레이어에서 @Transactional 포함한 비즈니스 로직을 호출하게 되면 해당 비즈니스 로직이 종료될 때 이미 이전에 저장되었던 변경사항을 함께 flush 해버리기 때문에 예기치 못한 버그가 발생할 수도 있다.&lt;br /&gt;- 보통은 컨트롤러에서 서비스를 먼저 호출하기 때문에 이런 일은 거의 없겠지만...&amp;nbsp;&lt;br /&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;플러시를 호출하지 않아서 변경사항을 반영하지 않는다는 점&lt;/span&gt;을 기억하자!&lt;/blockquote&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;style3&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  다시 돌고 돌아서...&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링부트는 기본적으로 OSIV를 true로 설정해두었기 때문에 뷰까지 데이터를 참조한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 개인적으로 OSIV 옵션은 끄고 사용하는 것이 좋다고 생각한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 &lt;b&gt;DB 커넥션을 계속 잡고 있기 때문에 리소스의 낭비가 발생할 수 있기 때문&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;또한, 스프링의 OSIV 전략은 프레젠테이션의 데이터 변경은 막지만, 애초에 프&lt;b&gt;레젠테이션까지 엔티티가 참조 관계를 가지는 건 레이어의 단방향 흐름을 깬다고 생각&lt;/b&gt;하기 때문이다. (같은 의미로 DTO 역시 서비스 레이어에서 생성한 다음에 뷰로 반환하는 것을 선호한다.)&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;각 레이어간의 단방향 관계를 유지하기 위해서는 엔티티에 대한 조작은 서비스 레이어까지만 진행하고, 이후 뷰에 처리될 데이터는 DTO를 통해 연관관계를 끊은 상태로 조작하는 게 더 좋다는 것이 나의 의견이다  &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(그리고 성능 최적화 방법으로 Command-Query separation 이라는 것도 있던데, 핵심 로직과 뷰 관련 서비스 로직을 분리하는 것이다. 나중에 미션할 때 한 번 이걸로 적용해봐야겠다...)&lt;/p&gt;</description>
      <category>Back-end/JPA</category>
      <author>dolmeng2</author>
      <guid isPermaLink="true">https://cl8d.tistory.com/89</guid>
      <comments>https://cl8d.tistory.com/89#entry89comment</comments>
      <pubDate>Sun, 21 May 2023 19:22:32 +0900</pubDate>
    </item>
    <item>
      <title>[JPA] OSIV 찍먹하기 1편 - 프록시 객체와 준영속 상태 엔티티</title>
      <link>https://cl8d.tistory.com/88</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;금요일에 근로하면서 구구와 근로 크루들의 도움 덕분에 JPA를 다시금 공부하게 됐다  &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;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;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  영속성 컨텍스트와 트랜잭션&lt;/b&gt;&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;  영속성 컨텍스트란?&lt;/b&gt;&lt;br /&gt;엔티티를 영속화시키는 환경. EntityManager를 통해 엔티티를 저장하거나 조회하면 영속성 컨텍스트에 보관된 엔티티 정보를 바탕으로 요청을 처리하게 된다. EntityManager 생성 시 1개가 만들어진다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;  특징&lt;/b&gt;&lt;br /&gt;- 식별자 값으로 엔티티를 구분한다.&lt;br /&gt;- 트랜잭션 커밋 시점에 flush. (쓰기 지연 - 내부 쿼리 저장소에 SQL 저장 후 한 번에 플러시)&lt;br /&gt;- 1차 캐시를 사용한다. (처음에 1차 캐시에서 엔티티 조회 &amp;gt; 없으면 DB 조회 &amp;gt; 1차 캐시에 저장하는 형태 = '영속화')&lt;br /&gt;- 1차 캐시에 저장된 엔티티는 == 비교가 가능하다. (동일성 보장)&lt;br /&gt;- 변경 감지 ('영속 상태의' 변경된 엔티티에 대해서 업데이트 쿼리 저장 &amp;gt; 플러시 &amp;gt; 트랜잭션 커밋)&lt;/blockquote&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;동일한 트랜잭션 안에서는 항상 같은 영속성 컨텍스트에 접근&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;span style=&quot;color: #ef5369;&quot;&gt;@Transactional&lt;/span&gt; 어노테이션의 범위를 의미한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Transactional이 붙어있으면 해당 메서드를 호출하기 전에 트랜잭션 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;JPA에서는 영속성 컨텍스트를 플러시하여 변경 사항을 DB에 반영한 뒤, DB 트랜잭션을 커밋&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-05-21 오후 6.43.25.png&quot; data-origin-width=&quot;2884&quot; data-origin-height=&quot;978&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sONCP/btsgEhe498s/Nwx3Jlc4MfCH6sa0a149o1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sONCP/btsgEhe498s/Nwx3Jlc4MfCH6sa0a149o1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sONCP/btsgEhe498s/Nwx3Jlc4MfCH6sa0a149o1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsONCP%2FbtsgEhe498s%2FNwx3Jlc4MfCH6sa0a149o1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2884&quot; height=&quot;978&quot; data-filename=&quot;스크린샷 2023-05-21 오후 6.43.25.png&quot; data-origin-width=&quot;2884&quot; data-origin-height=&quot;978&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흔히 우리는 관습적으로 개발할 때 @Transactional 어노테이션을 서비스 레이어에 붙이곤 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇기 때문에 서비스 계층이 끝나는 시점에 트랜잭션이 종료되며 영속성 컨텍스트도 함께 종료되어, 서비스와 레파지토리 레이어에서는 영속 상태인 엔티티가 &lt;b&gt;컨트롤러나 뷰 같은 presentation layer에서는 준영속 상태가 된다&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;그래서 컨트롤러나 뷰 (혹은 DTO)에서 준영속 상태인 엔티티를 통해 &lt;b&gt;지연 로딩을 시도하면 예외가 발생&lt;/b&gt;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;  지연 로딩 (Lazy Loading)이란?&lt;/b&gt;&lt;br /&gt;- 엔티티를 조회할 때 연관된 엔티티에 대해 실제 DB에서 바로 접근하지 않고, 연관된 엔티티를 실제로 사용하는 시점에 로딩하는 방법. 지연 로딩을 사용하기 위해서는 실제 엔티티 객체 대신에 데이터베이스 조회를 지연할 수 있는 가짜 객체가 필요하며, 이를 위해 프록시 객체를 사용한다.&lt;br /&gt;- OneToMany, ManyToMany에서 디폴트로 사용하는 전략이다.&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;span style=&quot;color: #ef5369;&quot;&gt;스프링 부트를 활용하여 개발할 때는 다르다&lt;/span&gt;.&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-05-21 오후 2.34.51.png&quot; data-origin-width=&quot;2794&quot; data-origin-height=&quot;86&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dT9QjG/btsgQpPJcve/VnDcb8kkITp1GN2knblfRk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dT9QjG/btsgQpPJcve/VnDcb8kkITp1GN2knblfRk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dT9QjG/btsgQpPJcve/VnDcb8kkITp1GN2knblfRk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdT9QjG%2FbtsgQpPJcve%2FVnDcb8kkITp1GN2knblfRk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2794&quot; height=&quot;86&quot; data-filename=&quot;스크린샷 2023-05-21 오후 2.34.51.png&quot; data-origin-width=&quot;2794&quot; data-origin-height=&quot;86&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;기본적으로 스프링부트는 OSIV를 활성화&lt;/span&gt;하기 때문에 view rendering 시점까지 DB 쿼리의 영향을 받을 수 있다는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이론만 보면 당연히 이해하기 힘들다. 간단한 클래스를 구성하여 직접 살펴보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(OSIV에 대해서는 다음 포스팅에서 더 자세히 다룰 예정이다.)&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;style3&quot; /&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&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;/b&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA를 생각하면 뺄 수 없는 개념 중 하나가 프록시 객체이다.&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;JPA에서 엔티티를 하나 조회할 때는 EntityManager.find()라는 메서드를 사용하는데, &lt;b&gt;이는 영속성 컨텍스트에 엔티티가 없으면 실제 DB를 조회하는 메서드&lt;/b&gt;이다. 하지만, 이런 식으로 직접 조회하게 되면&lt;span style=&quot;color: #ef5369;&quot;&gt; 해당 엔티티의 사용 여부와 관계없이 무조건 DB를 조회&lt;/span&gt;하기 때문에 cost가 매우 높다.&amp;nbsp;만약 DB 조회를 실제 사용 시점까지 미루고 싶다면 EntityManager.getReference()를 사용하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 DB 조회 대신에 이를 대체할&lt;span style=&quot;color: #ef5369;&quot;&gt; 프록시 객체를 생성&lt;/span&gt;하게 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-05-21 오후 4.38.31.png&quot; data-origin-width=&quot;1150&quot; data-origin-height=&quot;406&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/poj3j/btsgQpbbRsF/2CY6QSVwV9y5oP7MWFk1zK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/poj3j/btsgQpbbRsF/2CY6QSVwV9y5oP7MWFk1zK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/poj3j/btsgQpbbRsF/2CY6QSVwV9y5oP7MWFk1zK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fpoj3j%2FbtsgQpbbRsF%2F2CY6QSVwV9y5oP7MWFk1zK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;747&quot; height=&quot;264&quot; data-filename=&quot;스크린샷 2023-05-21 오후 4.38.31.png&quot; data-origin-width=&quot;1150&quot; data-origin-height=&quot;406&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 프록시 객체를 생성하기 위해서는&lt;span style=&quot;color: #ef5369;&quot;&gt; '상속'을 사용하기 때문에 기본 생성자가 필요&lt;/span&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;위의 상황에서 프록시 객체는 crew.getName()을 통해 직접 접근하게 되면 실제 DB를 조회하여 실제 엔티티를 생성하게 되는데, 이를 '&lt;span style=&quot;color: #ef5369;&quot;&gt;프록시 객체의 초기화&lt;/span&gt;'라고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;1. 프록시 객체의 crew.getName()을 호출하여 실제 데이터 조회하기&lt;br /&gt;2. &lt;b&gt;실제 엔티티가 생성되지 않았다면&lt;/b&gt;, 영속성 컨텍스트에게 실제 엔티티의 생성 요청&lt;br /&gt;3. 영속성 컨텍스트는 DB를 조회하여 실제 엔티티 객체 생성&lt;br /&gt;4. 프록시 객체는 생성된 엔티티 객체의 참조를 보관&lt;br /&gt;5. 보관한 참조값(crew)의 getName()을 호출하여 결과 반환&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 프록시 객체는 처음 사용될 때 딱 한 번만 초기화된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한, 영속성 컨텍스트에 이미 엔티티가 있으면 DB를 조회할 필요가 없기 때문에 em.getReference()를 호출해도 프록시 객체 대신 실제 엔티티를 반환하게 된다!&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;자세한 내용은 차차 JPA 공부하면서 올릴 예정!  &lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&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 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;✔️ Entity&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1684637298888&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Wootecho {

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

    @Enumerated(EnumType.STRING)
    private Course course;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우테코 클래스이다. Course라는 enum에는 백엔드, 프론트, 안드로이드라는 과정 정보가 들어가있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1684637329740&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Crew {

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

    @Column(nullable = false)
    private String name;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = &quot;wootecho_id&quot;, nullable = false, foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT))
    private Wootecho wootecho;
}&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;크루 클래스와 우테코 클래스는 N:1의 관계를 가지고 있으며, 이때 &lt;b&gt;지연 로딩 전략&lt;/b&gt;을 채택하였다. (FetchType.LAZY)&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;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-05-21 오후 2.10.55.png&quot; data-origin-width=&quot;378&quot; data-origin-height=&quot;476&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bFfzNX/btsgEKne0W3/bRJgaO82U1i2MRCiOiz6Ok/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bFfzNX/btsgEKne0W3/bRJgaO82U1i2MRCiOiz6Ok/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bFfzNX/btsgEKne0W3/bRJgaO82U1i2MRCiOiz6Ok/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbFfzNX%2FbtsgEKne0W3%2FbRJgaO82U1i2MRCiOiz6Ok%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;249&quot; height=&quot;314&quot; data-filename=&quot;스크린샷 2023-05-21 오후 2.10.55.png&quot; data-origin-width=&quot;378&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, 이는 논리적인 연관관계일 뿐이며 실제로 테이블 스크립트를 보면 아래와 같이 외래키 제약조건이 설정되지 않은 것을 볼 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1684637667314&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CREATE TABLE `wootecho` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `course` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1684637654658&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CREATE TABLE `crew` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `name` varchar(255) NOT NULL,
  `wootecho_id` bigint NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;crew 테이블은 단순히 wootecho_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;이는 다음과 같은 설정 때문이다.&lt;/p&gt;
&lt;pre id=&quot;code_1684637714499&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@JoinColumn(... foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT))
private Wootecho wootecho;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서&lt;span style=&quot;color: #ef5369;&quot;&gt; foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT)&lt;/span&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자식 테이블의 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;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-05-21 오후 4.47.17.png&quot; data-origin-width=&quot;1370&quot; data-origin-height=&quot;530&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/A8TYM/btsgEK8EyWR/FkbfvDJyfjhJwspVed00Vk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/A8TYM/btsgEK8EyWR/FkbfvDJyfjhJwspVed00Vk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/A8TYM/btsgEK8EyWR/FkbfvDJyfjhJwspVed00Vk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FA8TYM%2FbtsgEK8EyWR%2FFkbfvDJyfjhJwspVed00Vk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;705&quot; height=&quot;273&quot; data-filename=&quot;스크린샷 2023-05-21 오후 4.47.17.png&quot; data-origin-width=&quot;1370&quot; data-origin-height=&quot;530&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;아무튼, 이게 중요한 것은 아니다. 도메인을 위와 같이 설계했고, 레파지토리와 서비스, 컨트롤러는 다음과 같다.&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;size18&quot;&gt;&lt;b&gt;✔️ Repository&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1684640157742&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface CrewRepository extends JpaRepository&amp;lt;Crew, Long&amp;gt; {

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흔히 사용하는 Jpa를 사용한 레파지토리 구성이다.&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;size18&quot;&gt;&lt;b&gt;✔️ Service&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1684640503004&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
@Transactional(readOnly = true)
@AllArgsConstructor
public class CrewService {

    private final CrewRepository crewRepository;

    public Crew getById(final Long wootechoId) {
        final Crew findCrew = crewRepository.findById(wootechoId)
            .orElseThrow(() -&amp;gt; new IllegalArgumentException(&quot;존재하지 않는 아이디입니다.&quot;));
        return findCrew;
    }
}&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;✔️ Controller&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1684640863788&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RestController
@RequestMapping(&quot;/crew&quot;)
@AllArgsConstructor
public class CrewController {

    private final CrewService crewService;

    @GetMapping(&quot;/{crewId}&quot;)
    public ResponseEntity&amp;lt;CrewResponse&amp;gt; getById(@PathVariable Long crewId) {
        final Crew crew = crewService.getById(crewId);
        return ResponseEntity.ok(CrewResponse.of(crew));
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨트롤러이다. 여기서 주목할 점은 서비스에서 엔티티를 받아와서 dto로 변환하는 로직을 이곳에서 한다는 것이다.&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;size18&quot;&gt;&lt;b&gt;✔️ Dto&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1684640948701&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Getter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class WootechoResponse {
    private final Long id;
    private final String course;

    public static WootechoResponse of(final Wootecho wootecho) {
        return new WootechoResponse(wootecho.getId(), wootecho.getCourse().name());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1684641195443&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Getter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class CrewResponse {

    private final Long id;
    private final String name;
    private final WootechoResponse wootechoResponse;

    public static CrewResponse of(final Crew crew) {
        return new CrewResponse(crew.getId(), crew.getName(), WootechoResponse.of(crew.getWootecho()));
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;크루에 대한 응답 정보를 나타내기 위해서 crew 엔티티에 있는 값들과 연관된 엔티티인 wootecho 엔티티를 가져오고 있다.&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-05-21 오후 3.40.35.png&quot; data-origin-width=&quot;1014&quot; data-origin-height=&quot;584&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dbVKTC/btsgG4Td7yo/tmQLkMysN28IVBffqYKRA1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dbVKTC/btsgG4Td7yo/tmQLkMysN28IVBffqYKRA1/img.png&quot; data-alt=&quot;현재 도메인 구조는 이렇게 구성되어 있다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dbVKTC/btsgG4Td7yo/tmQLkMysN28IVBffqYKRA1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdbVKTC%2FbtsgG4Td7yo%2FtmQLkMysN28IVBffqYKRA1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;504&quot; height=&quot;290&quot; data-filename=&quot;스크린샷 2023-05-21 오후 3.40.35.png&quot; data-origin-width=&quot;1014&quot; data-origin-height=&quot;584&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;현재 도메인 구조는 이렇게 구성되어 있다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&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;  준영속 상태와 지연 로딩&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;준영속 상태에서는 지연 로딩이 동작하지 않는다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;  준영속 상태 엔티티란?&lt;/b&gt;&lt;br /&gt;영속성 컨텍스트가 관리하던 영속 상태인 엔티티가 더 이상 영속성 컨텍스트에 의해 관리되지 않는 상태&lt;/blockquote&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: #ef5369;&quot;&gt;준영속 상태인 엔티티는 영속성 컨텍스트가 없기 때문에 지연 로딩 자체가 불가능&lt;/span&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;우선, 앞서 말했던 것처럼 스프링부트는 기본적으로 뷰까지 DB 쿼리가 날라가는 것을 허용하기 때문에 이 옵션을 꺼야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1684650851823&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring:
  jpa:
    open-in-view: false&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;application.yml에서 open-in-view 옵션을 false로 설정해준다.&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;그리고, 크루 1명을 조회하는 API 요청을 날려보자. 나는 '져니'라는 크루를 조회할 예정이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-05-21 오후 3.35.26.png&quot; data-origin-width=&quot;2108&quot; data-origin-height=&quot;354&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/58Cvp/btsgC4UlX0D/k2XPHpipA9T6plZDgmrpQ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/58Cvp/btsgC4UlX0D/k2XPHpipA9T6plZDgmrpQ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/58Cvp/btsgC4UlX0D/k2XPHpipA9T6plZDgmrpQ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F58Cvp%2FbtsgC4UlX0D%2Fk2XPHpipA9T6plZDgmrpQ0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2108&quot; height=&quot;354&quot; data-filename=&quot;스크린샷 2023-05-21 오후 3.35.26.png&quot; data-origin-width=&quot;2108&quot; data-origin-height=&quot;354&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;could not initialize proxy [com.example.study.osiv.Wootecho#1] - no Session&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세션이 없기 때문에 proxy를 초기화할 수 없다고 나온다.&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_1684652695689&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// CrewResponse.java
public static CrewResponse of(final Crew crew) {
    return new CrewResponse(crew.getId(), crew.getName(), WootechoResponse.of(crew.getWootecho()));
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CrewResponse의 코드이다. 여기서 crew.getWootecho()를 통해 조회를 할 때에는 담겨있던 프록시 객체를 호출하게 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-05-21 오후 4.08.53.png&quot; data-origin-width=&quot;1308&quot; data-origin-height=&quot;56&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LOKwt/btsgECJUS7k/XP7eucraxKbHKi2rZsTSQ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LOKwt/btsgECJUS7k/XP7eucraxKbHKi2rZsTSQ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LOKwt/btsgECJUS7k/XP7eucraxKbHKi2rZsTSQ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLOKwt%2FbtsgECJUS7k%2FXP7eucraxKbHKi2rZsTSQ1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1308&quot; height=&quot;56&quot; data-filename=&quot;스크린샷 2023-05-21 오후 4.08.53.png&quot; data-origin-width=&quot;1308&quot; data-origin-height=&quot;56&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 보면, HibernateProxy라는 친구를 통해서 프록시 객체를 가져온 것을 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1684652754518&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// WootechoResponse.java
public static WootechoResponse of(final Wootecho wootecho) {
    return new WootechoResponse(wootecho.getId(), wootecho.getCourse().name());
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고, WootechoResponse에서 실제로 값을 채워넣으려고 할 때 프록시 객체를 초기화하면서 실제 값을 채워넣으려고 하자 오류가 발생한 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-05-21 오후 3.47.08.png&quot; data-origin-width=&quot;1434&quot; data-origin-height=&quot;912&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/btLKvv/btsgQqnBH4K/XEFRKOvXOral3QhPoikZqk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/btLKvv/btsgQqnBH4K/XEFRKOvXOral3QhPoikZqk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/btLKvv/btsgQqnBH4K/XEFRKOvXOral3QhPoikZqk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbtLKvv%2FbtsgQqnBH4K%2FXEFRKOvXOral3QhPoikZqk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;590&quot; height=&quot;375&quot; data-filename=&quot;스크린샷 2023-05-21 오후 3.47.08.png&quot; data-origin-width=&quot;1434&quot; data-origin-height=&quot;912&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;position: absolute;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;dto 변환을 controller = presentation layer에서 하면서, 그림과 같이 서비스 레이어를 지나 &lt;span style=&quot;color: #ef5369;&quot;&gt;트랜잭션 범위가 끝나면서 영속성 컨텍스트도 함께 제거되었기 때문&lt;/span&gt;이다. no session이라는 것이 곧 EntityManager가 없어서 값을 가져올 수가 없던 것이다.&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 레이어에서 조회해온 Crew 엔티티는 '&lt;b&gt;영속 상태&lt;/b&gt;'였으나, controller 레이어로 넘어오면서 영속성 컨텍스트가 제거되어 Crew 엔티티는 &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;사실 가장 좋은 방법은 컨트롤러에서 반환하지 않고, 그냥 서비스에서 DTO를 반환하도록 코드를 개선하는 것이다.&lt;/p&gt;
&lt;pre id=&quot;code_1684653788459&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// CrewService.java
public CrewResponse getById(final Long wootechoId) {
    final Crew findCrew = crewRepository.findById(wootechoId)
        .orElseThrow(() -&amp;gt; new IllegalArgumentException(&quot;존재하지 않는 아이디입니다.&quot;));
    return CrewResponse.of(findCrew);
}&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;하지만, 우리는 컨트롤러에서 엔티티를 dto로 변환하는 작업은 할 수 없는 것일까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 포스팅에서 어떻게 하면 이를 개선할 수 있을지 살펴보자.&lt;/p&gt;</description>
      <category>Back-end/JPA</category>
      <category>OSIV</category>
      <category>준영속</category>
      <category>프록시객체</category>
      <author>dolmeng2</author>
      <guid isPermaLink="true">https://cl8d.tistory.com/88</guid>
      <comments>https://cl8d.tistory.com/88#entry88comment</comments>
      <pubDate>Sun, 21 May 2023 16:44:21 +0900</pubDate>
    </item>
    <item>
      <title>[Network] 방화벽의 패킷 필터링 과정 알아보기</title>
      <link>https://cl8d.tistory.com/87</link>
      <description>&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-05-19 오전 12.40.05.png&quot; data-origin-width=&quot;3018&quot; data-origin-height=&quot;312&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cspkd9/btsguhr1Ugl/xIptEJg5HivWOR4CuqJDzk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cspkd9/btsguhr1Ugl/xIptEJg5HivWOR4CuqJDzk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cspkd9/btsguhr1Ugl/xIptEJg5HivWOR4CuqJDzk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcspkd9%2Fbtsguhr1Ugl%2FxIptEJg5HivWOR4CuqJDzk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3018&quot; height=&quot;312&quot; data-filename=&quot;스크린샷 2023-05-19 오전 12.40.05.png&quot; data-origin-width=&quot;3018&quot; data-origin-height=&quot;312&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지난 포스팅까지는 클라이언트 측 LAN에서 라우터까지 어떻게 패킷이 흘렀는지 알아보았었다.&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;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;style3&quot; /&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&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;b&gt;사내의 LAN에 서버를 설치한 다음, 인터넷에서 직접적으로 액세스했다.&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;하지만, 이 방법은 서버와 클라이언트 모두에 글로벌 주소를 할당해야 돼서 IP 주소가 부족해진다.&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: #ef5369;&quot;&gt;방화벽&lt;/span&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;에 서버를 가지고 들어가서 설치하거나, 프로바이더가 소유하고 있는 서버를 빌려쓰는 형태로 운영하는 경우도 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아무튼 이 경우에는 인터넷 중심 부분에서 데이터 센터로 패킷이 흘러가고, 서버 머신에 도착하기 때문에 데이터 센터 쪽에 방화벽이 설치되어 있다면 1차적으로 걸러진 패킷에 대해서 패킷이 받게 된다.&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&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;size18&quot;&gt;&lt;b&gt;  패킷 필터링&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;방화벽은 기본적으로 &lt;span style=&quot;color: #ef5369;&quot;&gt;특정 서버와 해당 서버 내부의 특정 애플리케이션에 액세스하는 패킷만 통과시키고, 나머지는 차단&lt;/span&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;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;궁금해서 간단하게 애플리케이션 게이트웨이와 서킷 게이트웨이에 대해서 찾아보았다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;- 애플리케이션 게이트웨이&lt;/b&gt;&lt;br /&gt;OSI 7계층의 응용 레이어에서 동작하는 게이트웨이이다.&lt;br /&gt;방화벽의 프록시를 이용하여 사용할 수 있으며, 서비스마다 다른 프록시 서버를 통해서 연결하는 방법이다.&lt;br /&gt;패킷 필털이 방식보다 비교적 높은 보안성을 가지고 있지만, 하드웨어에 의존적이고 응용 계층에서 동작하다 보니 네트워크에 부하가 많이 든다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;- 서킷 게이트웨이&lt;br /&gt;&lt;/b&gt;OSI 7계층에서 세션 레이어, 응용 레이어에서 사이에서 동작하는 게이트웨이이다.&lt;br /&gt;서비스마다 프록시 서버가 있는 것이 아닌, 하나의 프록시를 통해 모든 서비스를 처리할 수 있도록 만들었다.&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;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 328px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 13.6821%; text-align: center; height: 19px;&quot;&gt;&lt;b&gt;헤더의 종류&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 26.2403%; text-align: center; height: 19px;&quot;&gt;&lt;b&gt;조건 설정에 사용하는 항목&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 60.0775%; text-align: center; height: 19px;&quot;&gt;&lt;b&gt;설명&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 13.6821%; height: 17px; text-align: center;&quot;&gt;&lt;b&gt;MAC 헤더&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 26.2403%; height: 17px; text-align: center;&quot;&gt;송신처 MAC 주소&lt;/td&gt;
&lt;td style=&quot;width: 60.0775%; height: 17px;&quot;&gt;라우터는 패킷 중계 시 MAC 주소를 바꿔쓴다.&lt;br /&gt;- 중계 대상 라우터의 MAC 주소는 수신처 MAC 주소 항목에, 자신의 MAC 주소는 송신처 MAC 주소 항목에 기록한다.&lt;br /&gt;- 이때, 송신처 MAC 주소를 통해서 직전에 중계한 라우터의 MAC 주소를 알 수 있게 된다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 13.6821%; height: 51px; text-align: center;&quot; rowspan=&quot;3&quot;&gt;&lt;b&gt;IP 헤더&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 26.2403%; height: 17px; text-align: center;&quot;&gt;송신처 IP 주소&lt;/td&gt;
&lt;td style=&quot;width: 60.0775%; height: 17px;&quot;&gt;패킷을 최초로 송신한 기기의 IP 주소.&lt;br /&gt;패킷을 송신한 기기를 조건으로 설정하는 경우에 사용한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 26.2403%; height: 17px; text-align: center;&quot;&gt;수신처 IP 주소&lt;/td&gt;
&lt;td style=&quot;width: 60.0775%; height: 17px;&quot;&gt;패킷을 건네줄 대상의 IP 주소.&lt;br /&gt;패킷이 갈 목적지를 조건으로 설정하는 경우에 사용한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 26.2403%; height: 17px; text-align: center;&quot;&gt;프로토콜 번호&lt;/td&gt;
&lt;td style=&quot;width: 60.0775%; height: 17px;&quot;&gt;주요 프로토콜 번호는 다음과 같다.&lt;br /&gt;- IP:0, ICMP:1, TCP:6, UDP:17, OSPF:89&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 13.6821%; height: 224px; text-align: center;&quot; rowspan=&quot;4&quot;&gt;&lt;b&gt;TCP 헤더&lt;/b&gt;&lt;br /&gt;&lt;b&gt;or&lt;/b&gt;&lt;br /&gt;&lt;b&gt;UDP 헤더&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 26.2403%; height: 17px; text-align: center;&quot;&gt;송신처 포트 번호&lt;/td&gt;
&lt;td style=&quot;width: 60.0775%; height: 17px;&quot;&gt;패킷을 송신한 프로그램에 할당된 포트번호.&lt;br /&gt;- 서버 프로그램에 할당된 포트 번호는 고정되기 때문에 서버 측에서 반송된 패킷에 적힌 포트 번호를 통해서 서버에 대한 판별이 가능하다.&lt;br /&gt;- 클라이언트 프로그램의 포트 번호는 랜덤으로 할당되기 때문에 판단하기 어렵다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 57px;&quot;&gt;
&lt;td style=&quot;width: 26.2403%; text-align: center; height: 57px;&quot;&gt;수신처 포트 번호&lt;/td&gt;
&lt;td style=&quot;width: 60.0775%; height: 57px;&quot;&gt;패킷을 건네줄 대상의 프로그램에 할당된 포트 번호.&lt;br /&gt;- 마찬가지로 서버의 포트 번호는 조건 설정으로 많이 하는데, 클라이언트 포트 번호는 거의 하지 않는다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 133px;&quot;&gt;
&lt;td style=&quot;width: 26.2403%; text-align: center; height: 133px;&quot;&gt;TCP 컨트롤 비트&lt;/td&gt;
&lt;td style=&quot;width: 60.0775%; height: 133px;&quot;&gt;TCP 프로토콜 제어 시 사용하는 정보이다.&lt;br /&gt;ACK: 유효함 (정확하게 도착)&lt;br /&gt;PSH: 송신 버퍼테 저장 없이 바로 송신&lt;br /&gt;RST: 접속을 강제로 종료&lt;br /&gt;SYN: 통신 개시 시 접속 동작에서 최초로 보낸 패킷의 SYN=1, ACK = 0&lt;br /&gt;이때 이 패킷을 필터링 하면 그 뒤의 동작을 차단할 수 있다.&lt;br /&gt;FIN: 연결 끊음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 26.2403%; height: 17px; text-align: center;&quot;&gt;프레그먼트&lt;/td&gt;
&lt;td style=&quot;width: 60.0775%; height: 17px;&quot;&gt;IP 프로토콜의 조각 나누기 기능으로 패킷을 분할하고, 이 패킷이 두 번째 이후임을 나타낸다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 13.6821%; text-align: center; height: 17px;&quot;&gt;&lt;b&gt;ICMP 메시지&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 26.2403%; text-align: center; height: 17px;&quot;&gt;ICMP 메시지 유형&lt;/td&gt;
&lt;td style=&quot;width: 60.0775%; height: 17px;&quot;&gt;ICMP 메시지는 패킷 배송 도중에 이상이 있거나, 통신 상대의 동작을 확인할 때 사용한다.&lt;br /&gt;0: ping 명령으로 보내는 ICMP 에코 메시지에 응답하는 것.&lt;br /&gt;- 0과 8을 차단하면 ping 명령의 응답은 돌아오지 않는다. (어떤 기기가 네트워크에 존재하는지 ping 명령어로 찾을 때가 많은데 그런 걸 차단해버리는 거임)&lt;br /&gt;8: ping 명령을 실행하면 ICMP 에코 메시지가 송싱되는데, 이를 ICMP 에코라고 한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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;스크린샷 2023-05-18 오후 11.59.20.png&quot; data-origin-width=&quot;2520&quot; data-origin-height=&quot;1558&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xOU1Z/btsgtt7xdLc/kM7AoSIFpnSBgT4lu6juCk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xOU1Z/btsgtt7xdLc/kM7AoSIFpnSBgT4lu6juCk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xOU1Z/btsgtt7xdLc/kM7AoSIFpnSBgT4lu6juCk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxOU1Z%2Fbtsgtt7xdLc%2FkM7AoSIFpnSBgT4lu6juCk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;643&quot; height=&quot;398&quot; data-filename=&quot;스크린샷 2023-05-18 오후 11.59.20.png&quot; data-origin-width=&quot;2520&quot; data-origin-height=&quot;1558&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공개용 서버를 설치한 LAN과 사내 LAN이 분리되어 있고, 웹 서버는 공개 서버용 LAN에 접속되어 있다고 생각해보자.&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;패킷 필터링 조건 설정 시 먼저, 송/수신처 IP 주소에 따라서 시작점과 종점을 판단한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인터넷에서 웹 서버로 패킷이 들어오다면 종점인 &lt;b&gt;수신처 IP 주소가 실제 웹 서버의 IP 주소와 일치하는지 판단하여 패킷을 통과&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;첫 번째 행을 보자.&lt;span style=&quot;color: #ef5369;&quot;&gt; 송신처 IP, 포트 번호는 어떤 것이든 상관없지만 수신처 IP / 포트번호가 일치해야&lt;/span&gt; 해당 요청이 들어오게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 여기서 포트 번호까지 보는 것은 애플리케이션까지 한정시키려고 하는 것이다. 여기서는 웹 서버에서 흔히 사용하는 80번으로 고정시킨 사례를 보는 것이다.&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;b&gt;반대로 수신처의 IP, 포트 번호는 상관 없이 송신처의 IP / 포트번호가 패킷을 보냈던 곳인지 판단&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;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;  IP / 포트 번호만으로는 부족해!&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 IP와 포트 번호를 통해서 어떤 웹 서버의 애플리케이션으로 패킷이 들어오게 할지는 정했다.&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;단순히 웹 서버에서 인터넷으로 흐르는 패킷을 정지시키면 &lt;b&gt;양방향 통신인 TCP 프로토콜에 의해 인터넷에서 웹 서버 측으로 흐르게 하는 것도 정지하게 된다&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;span style=&quot;color: #ef5369;&quot;&gt;TCP 헤더의 컨트롤 비트&lt;/span&gt;를 사용한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 말한 것처럼 최초의 패킷만 SYN=1, ACK=0으로 설정되는데, 인터넷 -&amp;gt; 웹 서버로 보내는 이 값을 차단했다고 생각해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 최초의 패킷에 대해서 응답하는 경우가 없기 때문에 웹 서버 -&amp;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;반대로 인터넷&amp;nbsp;-&amp;gt;&amp;nbsp;웹&amp;nbsp;서버로&amp;nbsp;패킷이&amp;nbsp;흐를&amp;nbsp;때&amp;nbsp;최초의&amp;nbsp;패킷의&amp;nbsp;수신처는&amp;nbsp;'웹&amp;nbsp;서버'가&amp;nbsp;될&amp;nbsp;것이다.&lt;br /&gt;그러면 표의 첫 번째 행이 일치해서 통과할 것이다.&lt;br /&gt;이후,&amp;nbsp;두&amp;nbsp;번째&amp;nbsp;패킷은&amp;nbsp;송신처가&amp;nbsp;웹&amp;nbsp;서버가&amp;nbsp;될&amp;nbsp;것이지만,&amp;nbsp;최초의&amp;nbsp;패킷이&amp;nbsp;아니기&amp;nbsp;때문에&amp;nbsp;컨트롤&amp;nbsp;비트가&amp;nbsp;SYN=1,&amp;nbsp;ACK=0이&amp;nbsp;아닐&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;이런 식으로 이후에 인터넷 -&amp;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;
&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;TCP가 아닌 UDP에서도 완전하게 위의 구조가 동작할까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 이전에 DNS 서버를 통해 IP 주소를 얻어오는 행위는 UDP 프로토콜을 사용한다고 배웠었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, UDP의 경우 별도의 접속 단계가 없기 때문에 &lt;b&gt;TCP처럼 컨트롤 비트를 통해 액세스 방향을 판별할 수 없다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 &lt;span style=&quot;color: #ef5369;&quot;&gt;웹 서버 -&amp;gt; 인터넷은 허용하지만 인터넷 -&amp;gt; 웹 서버는 차단하도록 만들 수는 없다&lt;/span&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;  사내 LAN과 공개 서버 LAN&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;스크린샷 2023-05-19 오전 12.20.17.png&quot; data-origin-width=&quot;1590&quot; data-origin-height=&quot;694&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/beiQc0/btsgtvK5VLB/3zsiHfkvgEhWoA5Abthuw1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/beiQc0/btsgtvK5VLB/3zsiHfkvgEhWoA5Abthuw1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/beiQc0/btsgtvK5VLB/3zsiHfkvgEhWoA5Abthuw1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbeiQc0%2FbtsgtvK5VLB%2F3zsiHfkvgEhWoA5Abthuw1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;580&quot; height=&quot;253&quot; data-filename=&quot;스크린샷 2023-05-19 오전 12.20.17.png&quot; data-origin-width=&quot;1590&quot; data-origin-height=&quot;694&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;lt;-&amp;gt; 공개 서버용 LAN 패킷을 고려하였지만, 사실 사내 LAN &amp;lt;-&amp;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;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #ef5369;&quot;&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;&lt;b&gt;인터넷 &amp;lt;-&amp;gt; 사내 LAN을 왕래하는 패킷 사이의 주소 변환 기능도 필요&lt;/b&gt;하다.&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt; 보통 &lt;b&gt;인터넷 라우터의 라우팅 테이블에는 프라이빗 주소를 등록하지 않아서&lt;/b&gt; 프라이빗 주소로 갈 패킷을 중계하지 않고 버리기 때문에 &lt;span style=&quot;color: #ef5369;&quot;&gt;주소 변환이 필요&lt;/span&gt;하다.&lt;br /&gt;&lt;br /&gt;하지만, 방화벽에 내장되어 있는 라우터는 &lt;b&gt;사용자가 스스로 설정할 수 있기 때문에&lt;/b&gt; 프라이빗 주소를 등록하면 프라이빗 주소 그대로 공개 서버용 LAN과 사내 LAN 사이에서 패킷을 중계할 수 있다. 그래서 별도로 &lt;span style=&quot;color: #ef5369;&quot;&gt;공개 서버용 LAN과 사내 서버용 LAN을 왕래하는 패킷은 주소 변환을 할 필요가 없다&lt;/span&gt;!&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;b&gt; 출발지 IP 주소와 도착지 IP 주소를 확인하고 필요한 경우 주소 변환을 작업을 진행&lt;/b&gt;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때, 패킷 필터링형 방화벽의 경우 프라이빗 IP 주소와 글로벌 IP 주소 간의 대응을 자동으로 처리할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 내부 네트워크에서 출발한 패킷의 프라이빗 IP 주소를 방화벽이 감지하게 되면, 해당 주소를 인터넷에 접속하기 위해 할당된 글로벌 IP 주소로 변환하고 (주소 변환 테이블 참고), 변환된 패킷은 인터넷에 전송된다. 포트 번호 역시 마찬가지로 안 쓰는 포트로 변환하고, 나중에 원래의 포트로 변환하는 그런 작업을 진행한다. (이전 포스팅에서 설명했던 과정과 비슷하다.)&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;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;span style=&quot;color: #ef5369;&quot;&gt;방화벽은 단순히 시점과 종점만을 확인하기 때문에&lt;/span&gt; 패킷 사이에 특수한 데이터는 알 수가 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 패킷 내용을 조사해서 위험한 데이터가 들어있는지 판단해서 차단하는 장치나 그런 걸 준비하는 것이 중요하다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;  패킷 필터링형 방화벽은 수신처 IP 주소, 송신처 IP 주소, 수신처 포트 번호, 송신처 포트 번호, 컨트롤 비트 등으로 패킷을 통과시킬지 판단한다.&lt;/blockquote&gt;</description>
      <category>✏️/Network</category>
      <author>dolmeng2</author>
      <guid isPermaLink="true">https://cl8d.tistory.com/87</guid>
      <comments>https://cl8d.tistory.com/87#entry87comment</comments>
      <pubDate>Fri, 19 May 2023 00:42:25 +0900</pubDate>
    </item>
    <item>
      <title>[우테코 5기] 장바구니 미션 회고</title>
      <link>https://cl8d.tistory.com/86</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;헙크와 진행한 레벨 2 두 번째 페어 프로그래밍 미션인 장바구니 미션이다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어쩌다 보니 레벨 1 데일리 조 팀원들과 한 번씩 페어 프로그래밍을 하는 느낌이다 ㅎ_ㅎ 재밌다!&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;스크린샷 2023-05-08 오후 10.22.00.png&quot; data-origin-width=&quot;1140&quot; data-origin-height=&quot;484&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c7gkSa/btsepNf4BA2/gkyGWJbilaHaGzKVcgrno0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c7gkSa/btsepNf4BA2/gkyGWJbilaHaGzKVcgrno0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c7gkSa/btsepNf4BA2/gkyGWJbilaHaGzKVcgrno0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc7gkSa%2FbtsepNf4BA2%2FgkyGWJbilaHaGzKVcgrno0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;697&quot; height=&quot;296&quot; data-filename=&quot;스크린샷 2023-05-08 오후 10.22.00.png&quot; data-origin-width=&quot;1140&quot; data-origin-height=&quot;484&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;하지만... 지금 와서 코드를 보니 아직 많이 부족하다는 생각이 들었다  ... 어렵다!&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;style3&quot; /&gt;
&lt;h4 style=&quot;background-color: #ffffff; color: #5c5c5c; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;✔️ 작성한 코드&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;figure id=&quot;og_1683552255152&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 - Cl8D/jwp-shopping-cart: 레벨 2 장바구니 미션 레파지토리&quot; data-og-description=&quot;레벨 2 장바구니 미션 레파지토리. Contribute to Cl8D/jwp-shopping-cart development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/cl8d/jwp-shopping-cart&quot; data-og-url=&quot;https://github.com/Cl8D/jwp-shopping-cart&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/gv7Ic/hySyizGJOG/1eKihF6WVE42ZT0reLV0kk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/cl8d/jwp-shopping-cart&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/cl8d/jwp-shopping-cart&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/gv7Ic/hySyizGJOG/1eKihF6WVE42ZT0reLV0kk/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 - Cl8D/jwp-shopping-cart: 레벨 2 장바구니 미션 레파지토리&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;레벨 2 장바구니 미션 레파지토리. Contribute to Cl8D/jwp-shopping-cart 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;
&lt;h4 style=&quot;background-color: #ffffff; color: #5c5c5c; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;✔️ 1차 PR&lt;/b&gt;&lt;/h4&gt;
&lt;figure id=&quot;og_1683552297215&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;[1단계 - 상품 관리 기능] 져니(이지원) 미션 제출합니다. by Cl8D &amp;middot; Pull Request #202 &amp;middot; woowacourse/jwp-shopp&quot; data-og-description=&quot;안녕하세요, 코지! 백엔드 5기 크루 져니입니다  &amp;zwj;♀️ 이번에 헙크와 레벨 2 두 번째 미션인 장바구니 기능을 구현해보았어요. 조금 더 이것저것 해보고 싶어서 상품에 대한 카테고리 필드 &quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/woowacourse/jwp-shopping-cart/pull/202&quot; data-og-url=&quot;https://github.com/woowacourse/jwp-shopping-cart/pull/202&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bAxMIq/hySyiNe5k2/RcSWKMfSomq12pKZ1kkGy0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/woowacourse/jwp-shopping-cart/pull/202&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/woowacourse/jwp-shopping-cart/pull/202&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bAxMIq/hySyiNe5k2/RcSWKMfSomq12pKZ1kkGy0/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;[1단계 - 상품 관리 기능] 져니(이지원) 미션 제출합니다. by Cl8D &amp;middot; Pull Request #202 &amp;middot; woowacourse/jwp-shopp&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;안녕하세요, 코지! 백엔드 5기 크루 져니입니다  &amp;zwj;♀️ 이번에 헙크와 레벨 2 두 번째 미션인 장바구니 기능을 구현해보았어요. 조금 더 이것저것 해보고 싶어서 상품에 대한 카테고리 필드&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;h4 style=&quot;background-color: #ffffff; color: #5c5c5c; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;background-color: #ffffff; color: #5c5c5c; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;✔️ 2차 PR&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;figure id=&quot;og_1683552338203&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;[2단계 - 장바구니 기능] 져니(이지원) 미션 제출합니다. by Cl8D &amp;middot; Pull Request #270 &amp;middot; woowacourse/jwp-shoppi&quot; data-og-description=&quot;안녕하세요, 코다!  &amp;zwj;♀️ 지난 1단계 미션 때 빠르게 피드백 해주셔서 2단계도 금방 진행할 수 있었어요. 감사드려요! 개인적인 욕심으로 여러 가지 기능을 더 구현하고 싶기는 했는데, 먼저 &quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/woowacourse/jwp-shopping-cart/pull/270&quot; data-og-url=&quot;https://github.com/woowacourse/jwp-shopping-cart/pull/270&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bISg5A/hySynOxE0Q/pLR1WwH3L838rteQRgGusk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/woowacourse/jwp-shopping-cart/pull/270&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/woowacourse/jwp-shopping-cart/pull/270&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bISg5A/hySynOxE0Q/pLR1WwH3L838rteQRgGusk/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;[2단계 - 장바구니 기능] 져니(이지원) 미션 제출합니다. by Cl8D &amp;middot; Pull Request #270 &amp;middot; woowacourse/jwp-shoppi&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;안녕하세요, 코다!  &amp;zwj;♀️ 지난 1단계 미션 때 빠르게 피드백 해주셔서 2단계도 금방 진행할 수 있었어요. 감사드려요! 개인적인 욕심으로 여러 가지 기능을 더 구현하고 싶기는 했는데, 먼저&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h4 style=&quot;background-color: #ffffff; color: #5c5c5c; 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;- 상품 목록 페이지 연동하기 (Thymeleaf)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 상품 관리 CRUD API 작성하기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 관리자 도구 페이지 연동하기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 상품 도메인 설계 (자유롭게, ID, 이름, 이미지 URL, 가격은 고정)&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;p data-ke-size=&quot;size16&quot;&gt;- 장바구니 기능 구현 (상품 추가, 제거, 목록 조회, Basic 인증 방식)&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;나는 여기다가 사용자 추가, 상품 카테고리 기능, 관리자 기능, api 문서까지 적용해두었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;rest-docs 관련해서는 추가적으로 글을 작성할 예정이다 ㅎ_ㅎ&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  Redirect가 무엇일까?&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 이거는... 내가 리다이렉트에 대한 개념을 제대로 잡지 못해서 발생한 문제였다  &lt;/p&gt;
&lt;pre id=&quot;code_1683552955228&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@GetMapping
public String getProducts(final Model model) {
    final List&amp;lt;ProductDto&amp;gt; products = productService.getProducts();
    model.addAttribute(&quot;products&quot;, products);
    return &quot;admin&quot;;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 코드에서 뷰 이름을 반환하는 것은 리다이렉트로 봐야 하냐고 질문을 드렸었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리다이렉트는 요청한 리소스가 다른 위치에 있으니,&lt;b&gt; 해당 리소스 위치를 반환할 때 사용&lt;/b&gt;하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;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;이거는 추가적으로 JS 쪽 코드인데... 관련은 없지만 새롭게 안 거여서 정리해둔다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 51px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px; text-align: center;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px; text-align: center;&quot;&gt;&lt;b&gt;location.href&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px; text-align: center;&quot;&gt;&lt;b&gt;location.replace&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px; text-align: center;&quot;&gt;기능&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px; text-align: center;&quot;&gt;새로운 페이지로 이동&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px; text-align: center;&quot;&gt;기존 페이지를 새로운 페이지로 변경&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px; text-align: center;&quot;&gt;형태&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px; text-align: center;&quot;&gt;속성&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px; text-align: center;&quot;&gt;메서드&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center;&quot;&gt;주소 히스토리&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center;&quot;&gt;기록된다 (뒤로 가기 가능)&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center;&quot;&gt;기록되지 않는다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center;&quot;&gt;예시&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center;&quot;&gt;location.href=&quot;&quot;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center;&quot;&gt;location.replace(&quot;&quot;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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;style3&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  유효성 검사는 어디에서 해야 하는가? - 도메인 vs DTO&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1683554498763&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class ProductDto {

    @Length(min = 1, max = 25, message = &quot;상품 이름의 길이는 {min} ~ {max}글자여야 합니다.&quot;)
    private final String name;

    private final String imageUrl;

    @NotNull(message = &quot;상품 가격은 비어있을 수 없습니다.&quot;)
    @Range(min = 0, max = 10_000_000, message = &quot;상품 가격은 {min} ~ {max}원까지 가능합니다.&quot;)
    private final Integer price;

    @NotNull(message = &quot;상품 카테고리는 비어있을 수 없습니다.&quot;)
    private final ProductCategory category;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 코드를 작성했을 때는 위와 같이 DTO에서 이름, 가격에 대한 세부 조건들도 제어할 수 있도록 validation 어노테이션을 활용하였었다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;  &lt;br /&gt;저는 개인적으로 유효성 검사도 여러가지 종류가 있다고 생각하는데요.&lt;br /&gt;1. 비즈니스와 관련된 유효성 검사&lt;br /&gt;2. 단순 유효성 검사&lt;br /&gt;요렇게 나누어서 처리할 수 있는 레이어가 있다고 생각해요!&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 리뷰어님은 다음과 같이 유효성 검사를 하는 부분을 나눠보는 게 어떠냐고 이야기를 해주셨다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당시에는 validation annotation에 굉장히 집착(?)을 하고 있었어서, 도메인에서 @Range, @Length 같은 어노테이션을 활용하여 검증하면 도메인에서 뷰에 출력할 문구를 의존하는 형태가 될 것 같아서 선뜻 그렇게 하기가 어려웠다. (지난번 미션 때 이 부분에 대해서 피드백을 들었어서 더 조심스러웠던 것도 있다 ㅎㅎ)&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;하지만...! 생각해보면 굳이 validation annotation을 사용할 필요는 없었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레벨 1 미션에서는 그냥 자바 코드로도 충분히 검증을 했었고, 만약 새로운 개발자가 와서 '&lt;span style=&quot;color: #ef5369;&quot;&gt;상품 도메인에 필요한 필드들의 제약 사항은 어디에서 관리되지&lt;/span&gt;?'라고 생각했을 때 DTO보다는 도메인 클래스를 먼저 열어볼 것 같았다.&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;또한, 지금은 SSR이기 때문에 메시지 자체가 바로 뷰로 노출되지만, CSR 상황이라면 여기서 전파되는 메시지는 실사용자가 아닌 상대 개발자, 즉 클라이언트가 확인하는 메시지이기 때문에 '도메인이 뷰에 출력될 메시지를 관리한다'라는 관점과는 조금 떨어져 있다고 봐도 된다.&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;무엇보다 도메인 객체는 POJO로 관리되는 게 더 좋을 것 같아서, 아래와 같이 그냥 자바 코드를 통해 검증을 진행해주었다.&lt;/p&gt;
&lt;pre id=&quot;code_1683555089341&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class ProductName {

    private static final int NAME_MIN_LENGTH = 1, NAME_MAX_LENGTH = 25;

    private final String name;
	
    ...

    private static void validateNameLength(final String name) {
        if (name.length() &amp;lt; NAME_MIN_LENGTH || name.length() &amp;gt; NAME_MAX_LENGTH) {
            throw new GlobalException(PRODUCT_NAME_LENGTH);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 DTO 쪽에는 단순히 유효성 검사를 진행해주었다.&lt;/p&gt;
&lt;pre id=&quot;code_1683555196469&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class ProductRequest {

    @NotBlank(message = &quot;상품 이름은 비어있을 수 없습니다.&quot;)
    private final String name;

    @NotNull(message = &quot;상품 가격은 비어있을 수 없습니다.&quot;)
    private final Integer price;

    @NotNull(message = &quot;상품 카테고리는 비어있을 수 없습니다.&quot;)
    private final String category;
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞으로는 다음과 같은 기준을 통해서 유효성 검증을 해보고자 한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;- DTO : &lt;span style=&quot;color: #ef5369;&quot;&gt;데이터 타입에 대한 검증&lt;/span&gt;&lt;br /&gt;ex. &lt;br /&gt;숫자값이 올바르게 들어오는가? &lt;br /&gt;빈 값이 들어오는가? &lt;br /&gt;전화번호 형태 (-로 구분된 13글자)인가? &lt;br /&gt;이메일 형태 (@로 구분된 형태)인가?&lt;br /&gt;&lt;br /&gt;- 도메인: &lt;span style=&quot;color: #ef5369;&quot;&gt;비즈니스 요구사항에 대한 검증&lt;/span&gt;&lt;br /&gt;ex.&lt;br /&gt;가격은 0원 ~ 10000원 이하의 값을 가지는가?&lt;br /&gt;이름의 길이는 최소 1글자 이상인가?&lt;br /&gt;이메일의 도메인이 특정 도메인만 사용하는가? (gmail, daum, naver... etc)&lt;/blockquote&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;style3&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  Dto &amp;lt;-&amp;gt; Entity 변환 로직은 어디에 있으면 좋을까?&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1683556860213&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class ProductDto {
    public Product toEntity() {
        return new Product(name, imageUrl, price, category);
    }

    public static ProductDto fromEntity(final Product product) {
        return new ProductDto(product.getId(), product.getName(), product.getImageUrl(),
                product.getPrice(), product.getCategory());
    }
}&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;내 리뷰어님 같은 경우는 의존관계가 Controller -&amp;gt; Dto -&amp;gt; Entity와 같은 형태로 흐르게 된다면 &lt;span style=&quot;color: #ef5369;&quot;&gt;컨트롤러에서 해당 Dto 클래스를 통해서 도메인에 대해 접근할 수 있는 여지가 생기는 게 아니냐&lt;/span&gt;고 말씀해 주셨다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론, 개발자의 이해도가 높다면 Dto에서 Entity로 변환하여 컨트롤러에서 코어한 도메인 계층의 객체를 침범하는 경우는 거의 없겠지만, 클린 아키텍처의 시작은 '~한 위험이 있지 않을까?'로 부터 시작된다고 생각하기 때문에 이런 위험성은 끊어주는 게 옳다고 생각했다.&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 layer에서 private method를 통해 위와 같은 변환 로직을 처리하도록 만들었고, 추후 매핑하는 클래스들이 많아진다면 별도의 mapper class를 두어서 처리하는 것으로 생각하였다. (매퍼 클래스 역시 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;+) 의존 관계를 단방향으로 가져가려면 dto가 존재해야 하는 패키지는 service 쪽이어야 한다  &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아무 생각 없이 controller 쪽에 두었다가 controller -&amp;gt; service -&amp;gt; dto 같은 요상한 흐름 관계가 생겨버렸었다... ㅎ_ㅎ 유의하자!&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;&amp;nbsp;&lt;/h4&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;사실 처음 페어 프로그래밍을 할 때는 엔티티와 도메인을 구분하지 않고, &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;미션 요구사항이 CRUD이다 보니까 DB와 굉장히 밀접해지고, 테이블에 내용을 삽입하거나, 조회하거나 하는 로직만 있다 보니 별도의 비즈니스 로직이 없다고 생각되어 별도로 둘 필요성을 느끼지 못했기 때문이다. 또한, 엔티티의 경우 DB와 1:1로 값을 매핑하고, 별도의 로직 없이 단순히 값을 반환하는 역할만 해야 한다고 생각했었다.&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;엔티티에서 검증을 진행하기에는, 엔티티가 비즈니스 로직을 가지는 것 같아서 하고 싶지 않았고, 그렇다고 도메인을 만들기에는 단순히 검증밖에 안 하기 때문에 굳이 만들어야 하나...? 라는 생각도 들었기 때문이다...   (여기서 머리 터짐)&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;br /&gt;새로운 개발자가 도메인에 대한 요구사항을 확인하기 위해서는 어떤 클래스를 확인할까요?&lt;/blockquote&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-05-08 오후 11.31.31.png&quot; data-origin-width=&quot;1538&quot; data-origin-height=&quot;918&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wyc8s/btseseKnkla/YOQ3ibVwYTJX49SkpUqGVk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wyc8s/btseseKnkla/YOQ3ibVwYTJX49SkpUqGVk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wyc8s/btseseKnkla/YOQ3ibVwYTJX49SkpUqGVk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fwyc8s%2FbtseseKnkla%2FYOQ3ibVwYTJX49SkpUqGVk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1538&quot; height=&quot;918&quot; data-filename=&quot;스크린샷 2023-05-08 오후 11.31.31.png&quot; data-origin-width=&quot;1538&quot; data-origin-height=&quot;918&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;말이 길지만 요약하자면, &lt;span style=&quot;color: #ef5369;&quot;&gt;비즈니스 로직을 표현하기 위한 객체는 도메인 객체&lt;/span&gt;며, &lt;span style=&quot;color: #ef5369;&quot;&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바로... Join을 통해서 조회해올 때는 어떻게 엔티티로 처리해야 하는지 결정할 수 없는 것이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에는 조인을 진행하는 주 테이블에 대한 dao (CartDao)에서 아래와 같이 코드를 작성하였고, MemberProductEntity라는 객체를 생성하여 조회해온 결과를 담아오도록 만들었다.&lt;/p&gt;
&lt;pre id=&quot;code_1683559298059&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class MemberProductEntity {
    private final Long cartId;
    private final Long memberId;
    private final Long productId;
    private final String productName;
    private final String productImageUrl;
    private final int productPrice;
    private final String productCategory;
	
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1683559172961&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;MemberProductEntity&amp;gt; getProductByMemberId(final Long memberId) {
    final String query = &quot;SELECT c.id, c.member_id, c.product_id, p.name, p.image_url, p.price, p.category &quot; +
            &quot;FROM cart c JOIN product p ON c.product_id = p.id WHERE c.member_id = ?&quot;;
    return jdbcTemplate.query(query, (result, count) -&amp;gt;
            new MemberProductEntity(result.getLong(&quot;id&quot;), result.getLong(&quot;member_id&quot;),
                    result.getLong(&quot;product_id&quot;), result.getString(&quot;name&quot;),
                    result.getString(&quot;image_url&quot;), result.getInt(&quot;price&quot;),
                    result.getString(&quot;category&quot;)), memberId);
}&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-05-09 오전 12.23.57.png&quot; data-origin-width=&quot;1628&quot; data-origin-height=&quot;370&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/N86fU/btserEvGh2x/WMiDCHDT4ebWue6LAO9l41/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/N86fU/btserEvGh2x/WMiDCHDT4ebWue6LAO9l41/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/N86fU/btserEvGh2x/WMiDCHDT4ebWue6LAO9l41/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FN86fU%2FbtserEvGh2x%2FWMiDCHDT4ebWue6LAO9l41%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1628&quot; height=&quot;370&quot; data-filename=&quot;스크린샷 2023-05-09 오전 12.23.57.png&quot; data-origin-width=&quot;1628&quot; data-origin-height=&quot;370&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; 1줄 요약: SRP 위반 아닐까요? + 엔티티를 이런 식으로 써도 되나요? + 결국 도메인과 엔티티의 차이점이 뭐죠? (다시 원점)&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;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;  도메인: 우리가 해결하고자 하는 문제 영역이자, 문제 영역을 클래스화 하여 코드로 옮긴 부분&lt;br /&gt;  엔티티: 식별자를 가지고 있는 객체&lt;br /&gt;위 정의대로라면 도메인이 더 큰 범위이면서, 엔티티가 그 일부분으로 보일 텐데, 맞습니다!&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도메인에는 VO, Entity, Domain Service 등 많은 것들이 포함되어 있으며, &lt;span style=&quot;color: #ef5369;&quot;&gt;문제 영역을 클래스화 한 것이라면 모두 도메인으로 바라본다&lt;/span&gt;고 하셨다. (이에 대해서 오늘 허브랑도 이야기를 나눴었는데, 허브의 리뷰어님도 비즈니스 로직에 대한 Service Layer를 도메인 쪽으로 바라본다고 말씀해 주셨다.) 그리고, 그 중에서 식별자가 있다면 엔티티가 된다.&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: #ef5369;&quot;&gt;두 개의 객체를 분리할 수 있는 고유한 기준&lt;/span&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB의 pk 값이라고 생각할 수 있겠지만, PK가 될 수도 있고 다른 필드 값이 될 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떠한 객체를 구분하는 기준이기 때문에, 만약 비즈니스 로직 상 '이름'이라는 필드가 고유하게 존재한다면 해당 객체는 '이름'이라는 식별자를 가진 엔티티가 될 수도 있는 것이다. &lt;span style=&quot;color: #ef5369;&quot;&gt;별도의 식별자가 있는 도메인은 곧 엔티티&lt;/span&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;마틴 파울러는 다음과 같이 각 개념들을 정의하였다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;- Entity&lt;/b&gt;:&amp;nbsp;Objects that have a distinct identity that runs through time and different representations. You also hear these called &quot;reference objects&quot;.&lt;br /&gt;&lt;br /&gt;- &lt;b&gt;Value Object&lt;/b&gt;: Objects that matter only as the combination of their attributes. Two value objects with the same values for all their attributes are considered equal. I also describe value objects in P of EAA.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;- Service&lt;/b&gt;: A standalone operation within the context of your domain. A Service Object collects one or more services into an object. Typically you will have only one instance of each service object type within your execution context.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 미션을 하면서 Reference Object와 Value Object에 대해서 공부한 적이 있었는데 여기서 엔티티를 Reference Object로서 생각한다는 점을 보고 확실하게 이해하게 되었다. 식별자로 상태가 변하는 객체가 되는 것. 우선 나는 이 정도로 이해를 했다. 그래서 보통 엔티티는 식별자로 동일성을 정의할 수 있기 때문에 equals, hashcode를 일반적으로 정의하지 않는다.&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;아무튼, 다시 돌아와서... 엔티티를 데이터베이스 테이블에 해당하는 객체로 자주 사용하는 이유는 보통 PK를 이용해 식별이 되는 Reference Object이기 때문이다. 그래서 이번 레벨에서는&lt;b&gt; DB의 PK를 식별자로 사용하는 엔티티를 별도로 두었다&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;그리고, 장바구니에 대한 CUD 로직은 다음과 같이 풀어나갔다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;- '장바구니 (Cart)' 라는 도메인은 Member와 List&amp;lt;Product&amp;gt;의 조합으로 만들기.&lt;br /&gt;: 한 명의 사용자가 담은 물건 리스트를 관리하는 도메인 객체.&lt;br /&gt;&lt;br /&gt;- 장바구니 상품 저장, 제거 시 식별자를 가진 엔티티인 CartEntity를 활용하면 바로 insert가 가능하기 때문에 (본 비즈니스 로직은 DB에 대한 연산이 위주임) 별도의 도메인 객체로 표현할 필요가 없을 것 같다고 판단. (어차피 엔티티도 문제를 해결하는 범위로 본다면 도메인 객체에 들어갈 수 있다.)&lt;br /&gt;&lt;br /&gt;- 장바구니 상품 정보 반환 시 조인을 사용하다 보니 Entity보다는 DTO로 사용 (여러 값들을 조합해서 사용하고 있음)&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;pre id=&quot;code_1684135372588&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public CartResponse getCartResponseByMemberEmail(final String memberEmail) {
    final MemberEntity memberEntity = getMemberEntity(memberEmail);
    final List&amp;lt;CartDto&amp;gt; cartDtos = cartDao.getProductsByMemberId(memberEntity.getId());
    final Cart cart = convertToCart(memberEntity, cartDtos);
    final List&amp;lt;ProductResponse&amp;gt; productResponses = convertToProductResponse(cart);
    final int productCount = cart.getProductCount();
    return new CartResponse(productCount, productResponses);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만, 이 과정에서 entity -&amp;gt; domain으로 만드는 컨버팅 로직이 많아져서 코드가 약간 보기 힘들어졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한, 서비스 레이어에서 도메인, 엔티티를 둘 다 가지고 있으면서 service -&amp;gt; entity(persistence layer), service -&amp;gt; domain와 같은 순환참조 형태가 발생해서 단방향으로 데이터가 흐르지 않게 되었다.&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>우아한테크코스/레벨 2</category>
      <category>우아한테크코스</category>
      <category>우테코</category>
      <category>우테코5기</category>
      <category>장바구니미션</category>
      <author>dolmeng2</author>
      <guid isPermaLink="true">https://cl8d.tistory.com/86</guid>
      <comments>https://cl8d.tistory.com/86#entry86comment</comments>
      <pubDate>Mon, 15 May 2023 16:24:29 +0900</pubDate>
    </item>
    <item>
      <title>[Network] 라우터의 주소 변환과 패킷 필터링 기능</title>
      <link>https://cl8d.tistory.com/85</link>
      <description>&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;이번 포스팅에서는 라우터의 주소 변환 기능과 패킷 필터링 기능에 대해서 알아보자!&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;style3&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; &amp;nbsp;주소&amp;nbsp;변환이&amp;nbsp;나온&amp;nbsp;이유&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라우터는 다양한 일을 하지만, 그중 핵심은 &lt;span style=&quot;color: #ef5369;&quot;&gt;주소 변환 및 패킷 필터링&lt;/span&gt; 기능이다.&lt;br /&gt;주소&amp;nbsp;변환은&amp;nbsp;왜&amp;nbsp;나왔을까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이전에는 IP주소를 관리 기관에 신청서를 내서 할당받았지만, 인터넷에 접속하는 기기가 많아지면서 할당해줄 주소가 부족해지는 현상이 발생했었다. 이를&amp;nbsp;해결하기&amp;nbsp;위해서&lt;b&gt;&amp;nbsp;동일한&amp;nbsp;회사에서&amp;nbsp;사용하는&amp;nbsp;주소는&amp;nbsp;다른&amp;nbsp;회사의&amp;nbsp;주소와&amp;nbsp;동일할&amp;nbsp;수&amp;nbsp;있도록&lt;/b&gt;&amp;nbsp;만들었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는&amp;nbsp;어차피&amp;nbsp;다른&amp;nbsp;회사의&amp;nbsp;패킷은&amp;nbsp;서로&amp;nbsp;왕래할&amp;nbsp;일이&amp;nbsp;없으며,&amp;nbsp;같은&amp;nbsp;회사에서끼리&amp;nbsp;별도의&amp;nbsp;사내망으로&amp;nbsp;통신하기&amp;nbsp;때문에&amp;nbsp;다른&amp;nbsp;회사와&amp;nbsp;겹쳐도&amp;nbsp;상관없는&amp;nbsp;것이다.&amp;nbsp;덕분에&amp;nbsp;같은&amp;nbsp;회사에&amp;nbsp;있는&amp;nbsp;모든&amp;nbsp;기기에게&amp;nbsp;다른&amp;nbsp;주소를&amp;nbsp;할당해줄&amp;nbsp;필요가&amp;nbsp;없어졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;다만, 무작정 할당할 수 없기 때문에 특정 주소만 사용하며, 이를 &lt;span style=&quot;color: #ef5369;&quot;&gt;프라이빗 주소&lt;/span&gt;로 두고 이전에 사용했던 모든 기기의 고유한 주소를 &lt;span style=&quot;color: #ef5369;&quot;&gt;글로벌 주소&lt;/span&gt;라고 부르기로 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;프라이빗&amp;nbsp;주소는&amp;nbsp;다음&amp;nbsp;범위로&amp;nbsp;한정한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;10.0.0.0&amp;nbsp;~&amp;nbsp;10.255.255.255&lt;br /&gt;127.16.0.0&amp;nbsp;~&amp;nbsp;172.31.255.255&lt;br /&gt;192.168.0.0&amp;nbsp;~&amp;nbsp;192.168.255.255&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;b&gt;관리 기관에 신청서를 낼 필요 없이 자유롭게 이용하면 된다&lt;/b&gt;. (동일한 회사 내에서만 중복을 피하면 됨)&lt;br /&gt;&lt;br /&gt;하지만,&amp;nbsp;사내&amp;nbsp;네트워크가&amp;nbsp;외부랑&amp;nbsp;통신하는&amp;nbsp;일이&amp;nbsp;발생할&amp;nbsp;수도&amp;nbsp;있다.&lt;br /&gt;그래서&amp;nbsp;사내&amp;nbsp;네트워크는&amp;nbsp;보통&amp;nbsp;&lt;span style=&quot;color: #ef5369;&quot;&gt;인터넷에&amp;nbsp;공개된&amp;nbsp;서버를&amp;nbsp;접속하는&amp;nbsp;부분&lt;/span&gt;과&amp;nbsp;&lt;span style=&quot;color: #0593d3;&quot;&gt;사내용&amp;nbsp;네트워크&lt;/span&gt; 2가지로 나눈다. 그리고 사내 네트워크에 할당된 프라이빗 주소는 외부랑 통신하지 않도록 만들기 때문에&lt;b&gt; 외부랑 통신하기 위해서&lt;/b&gt;&amp;nbsp;&lt;b&gt;특별한&amp;nbsp;구조를&amp;nbsp;사용하는데&amp;nbsp;이때&amp;nbsp;주소&amp;nbsp;변환을&amp;nbsp;사용&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;스크린샷 2023-05-15 오전 10.53.06.png&quot; data-origin-width=&quot;1214&quot; data-origin-height=&quot;1060&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/WgM8p/btsfgg9cj71/ZSARwf8k33kAkatqvC4Pw0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/WgM8p/btsfgg9cj71/ZSARwf8k33kAkatqvC4Pw0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/WgM8p/btsfgg9cj71/ZSARwf8k33kAkatqvC4Pw0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWgM8p%2Fbtsfgg9cj71%2FZSARwf8k33kAkatqvC4Pw0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;437&quot; data-filename=&quot;스크린샷 2023-05-15 오전 10.53.06.png&quot; data-origin-width=&quot;1214&quot; data-origin-height=&quot;1060&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;사내 네트워크에서 사용하는 프라이빗 주소를 외부에서 사용하는 글로벌 주소로 바꾸거나, 글로벌 주소에서 들어온 패킷을 프라이빗 주소로 바꿔주는 동작을 진행한다.&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;style3&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;br /&gt;&lt;b&gt; &amp;nbsp;주소&amp;nbsp;변환의&amp;nbsp;동작&amp;nbsp;알아보기&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주소 변환은 패킷 중계 시 &lt;span style=&quot;color: #ef5369;&quot;&gt;IP 헤더에 적힌 IP 주소와 포트 번호를 다르게 바꾸는 것&lt;/span&gt;이다.&lt;br /&gt;&lt;br /&gt;먼저,&amp;nbsp;TCP&amp;nbsp;접속&amp;nbsp;시&amp;nbsp;송신처의&amp;nbsp;IP&amp;nbsp;주소를&amp;nbsp;프라이빗&amp;nbsp;주소에서&amp;nbsp;글로벌&amp;nbsp;주소로&amp;nbsp;바꿔쓴다.&lt;br /&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;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;  이때, 포트 번호는 왜 바꿔쓰는 것일까?&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;만약 IP 주소만 바꿔쓰게 된다면,&amp;nbsp;&lt;b&gt;프라이빗 주소와 글로벌 주소가 1:1로 매칭&lt;/b&gt;되어야 하기 때문에&amp;nbsp;글로벌 주소가 매우 많이 필요하다.&lt;br /&gt;하지만, 사내 네트워크에 수백 대의 단말기가 존재한다면 수백 개의 글로벌 주소가 필요해진다.&lt;br /&gt;이를 해결하기 위해서 포트 번호도 바꿔쓰며 각각의 단말기를 식별하고자 했고, 어차피 클라이언트 측의 포트 번호는 원래 비어있기 때문에 무작위로 선택해서 사용하므로 바꿔쓰더라도 문제가 발생하지 않는다.&lt;br /&gt;결과적으로 &lt;span style=&quot;color: #ef5369;&quot;&gt;한 개의 글로벌 주소를 수만 개의 프라이빗 주소에 매칭시켜&lt;/span&gt; 주소 고갈 문제를 해결했다.&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;span style=&quot;color: #ef5369;&quot;&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;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-05-15 오후 2.31.52.png&quot; data-origin-width=&quot;1522&quot; data-origin-height=&quot;1008&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/phwTi/btsfjKppHwc/QRYRijH10dKLaJiGfTck9K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/phwTi/btsfjKppHwc/QRYRijH10dKLaJiGfTck9K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/phwTi/btsfjKppHwc/QRYRijH10dKLaJiGfTck9K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FphwTi%2FbtsfjKppHwc%2FQRYRijH10dKLaJiGfTck9K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;590&quot; height=&quot;391&quot; data-filename=&quot;스크린샷 2023-05-15 오후 2.31.52.png&quot; data-origin-width=&quot;1522&quot; data-origin-height=&quot;1008&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;위에서는&amp;nbsp;주소&amp;nbsp;변환이&amp;nbsp;글로벌&amp;nbsp;&amp;lt;-&amp;gt;&amp;nbsp;프라이빗&amp;nbsp;주소&amp;nbsp;간&amp;nbsp;양방향으로&amp;nbsp;일어난다고&amp;nbsp;했다.&lt;br /&gt;이때,&amp;nbsp;&lt;span style=&quot;color: #ef5369;&quot;&gt;프라이빗&amp;nbsp;주소에서&amp;nbsp;글로벌&amp;nbsp;주소로&amp;nbsp;변환할&amp;nbsp;때는&amp;nbsp;라우팅&amp;nbsp;테이블에&amp;nbsp;정보가&amp;nbsp;등록되지&amp;nbsp;않았더라도&amp;nbsp;중계가&amp;nbsp;가능&lt;/span&gt;하다.&lt;br /&gt;기본적으로&amp;nbsp;글로벌&amp;nbsp;주소는&amp;nbsp;라우터에&amp;nbsp;할당이&amp;nbsp;되어&amp;nbsp;있고,&amp;nbsp;포트&amp;nbsp;번호는&amp;nbsp;사실&amp;nbsp;비어있는&amp;nbsp;정보를&amp;nbsp;사용하면&amp;nbsp;되기&amp;nbsp;때문이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, &lt;span style=&quot;color: #0593d3;&quot;&gt;프라이빗 주소로 변환할 때에는 라우팅 테이블의 정보가 없으면 내부에서 어떤 주소가 사용되는지 알 수 없기&lt;/span&gt; 때문에 꼭 필요하다.&lt;br /&gt;&lt;br /&gt;그렇기&amp;nbsp;때문에&amp;nbsp;외부에서&amp;nbsp;액세스하지&amp;nbsp;않는&amp;nbsp;단말&amp;nbsp;기기에는&amp;nbsp;외부에서&amp;nbsp;패킷을&amp;nbsp;송신할&amp;nbsp;수&amp;nbsp;없으며,&amp;nbsp;액세스하더라도&amp;nbsp;실제로&amp;nbsp;통신에&amp;nbsp;사용하는&amp;nbsp;포트&amp;nbsp;번호가&amp;nbsp;아니라면&amp;nbsp;그쪽으로&amp;nbsp;패킷을&amp;nbsp;보낼&amp;nbsp;수&amp;nbsp;없다.&amp;nbsp;덕분에&amp;nbsp;부정&amp;nbsp;침입을&amp;nbsp;방지하는&amp;nbsp;효과를&amp;nbsp;가지게&amp;nbsp;된다.&lt;br /&gt;&lt;br /&gt;만약,&amp;nbsp;외부에서&amp;nbsp;액세스하지&amp;nbsp;않더라도&amp;nbsp;그쪽으로&amp;nbsp;패킷을&amp;nbsp;송신하고&amp;nbsp;싶다면&amp;nbsp;직접&amp;nbsp;라우팅&amp;nbsp;테이블에&amp;nbsp;등록해두면&amp;nbsp;된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공개용 서버는 라우팅 밖의 영역에서 글로벌 주소를 할당하지만, &lt;b&gt;서버의 프라이빗 주소를 라우터에 등록해두면 외부로 해당 주소를 공개할 수 있게 된다&lt;/b&gt;. (DNS 서버에 등록할 때는 라우터에 등록한 글로벌 주소를 등록하면 된다)&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;style3&quot; /&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;패킷 필터링 시 MAC, IP, TCP 헤더에 기록된 내용을 조사하여 &lt;span style=&quot;color: #ef5369;&quot;&gt;사전에 설정된 조건에 부합되면 패킷을 중계하거나, 혹은 폐기&lt;/span&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;  퀴즈&lt;/b&gt;&lt;br /&gt;&lt;b&gt;&lt;br /&gt;Q. 입력 신호를 모든 포트에 출력하는 것은 스위칭 허브와 리피터 허브 중 어떤 것일까?&lt;/b&gt;&lt;br /&gt;A. 리피터 허브&lt;br /&gt;&lt;br /&gt;&lt;b&gt;Q. 네트워크 번호와 호스트 번호의 비트 수를 결정하는 것은 무엇일까?&lt;/b&gt;&lt;br /&gt;A. 넷마스크&lt;br /&gt;&lt;br /&gt;&lt;b&gt;Q. 큰 패킷을 분할하는 기능을 뭐라고 할까?&lt;/b&gt;&lt;br /&gt;A. 조각 나누기&lt;br /&gt;&lt;br /&gt;&lt;b&gt;Q. 라우터의 경로표의 '넷마스크'에 0.0.0.0인 경로 정보는 무엇을 의미할까?&lt;/b&gt;&lt;br /&gt;A. 기본 경로&lt;/blockquote&gt;</description>
      <category>✏️/Network</category>
      <category>라우터</category>
      <category>주소변환</category>
      <author>dolmeng2</author>
      <guid isPermaLink="true">https://cl8d.tistory.com/85</guid>
      <comments>https://cl8d.tistory.com/85#entry85comment</comments>
      <pubDate>Mon, 15 May 2023 14:38:01 +0900</pubDate>
    </item>
    <item>
      <title>  깃허브 / 티스토리 닉네임의 의미</title>
      <link>https://cl8d.tistory.com/notice/84</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;640&quot; data-origin-height=&quot;640&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Iqzu0/btsfdcfikp4/rJYTk4484xd9xnNkY0TOm1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Iqzu0/btsfdcfikp4/rJYTk4484xd9xnNkY0TOm1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Iqzu0/btsfdcfikp4/rJYTk4484xd9xnNkY0TOm1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIqzu0%2Fbtsfdcfikp4%2FrJYTk4484xd9xnNkY0TOm1%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;485&quot; height=&quot;485&quot; data-origin-width=&quot;640&quot; data-origin-height=&quot;640&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;자꾸 깃허브 닉네임 볼 때마다 욕 아니냐는 의견을 들어서 작성하는 글...  &lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; 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;스크린샷 2023-05-14 오후 6.59.51.png&quot; data-origin-width=&quot;1554&quot; data-origin-height=&quot;906&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/byH85b/btsfdaPoqJM/JekZX0blqq7u6KjiFwCF01/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/byH85b/btsfdaPoqJM/JekZX0blqq7u6KjiFwCF01/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/byH85b/btsfdaPoqJM/JekZX0blqq7u6KjiFwCF01/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbyH85b%2FbtsfdaPoqJM%2FJekZX0blqq7u6KjiFwCF01%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;630&quot; height=&quot;367&quot; data-filename=&quot;스크린샷 2023-05-14 오후 6.59.51.png&quot; data-origin-width=&quot;1554&quot; data-origin-height=&quot;906&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;나는 깃허브를 꽤 옛날에 가입했다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;2019년... 고3 졸업식을 앞두고, 대학교도 입학하기 전이다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;만들게 된 계기는 &lt;s&gt;컴공&lt;/s&gt; 소프트웨어학부 입학을 앞두고 코딩의 코자도 모르는 사람의 불안감(?)으로 시작됐다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-05-14 오후 7.39.25.png&quot; data-origin-width=&quot;564&quot; data-origin-height=&quot;182&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RuC3s/btsfPCvSbS7/gJkD07Kxqlj488b16quo31/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RuC3s/btsfPCvSbS7/gJkD07Kxqlj488b16quo31/img.png&quot; data-alt=&quot;네 바꿨어요&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RuC3s/btsfPCvSbS7/gJkD07Kxqlj488b16quo31/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRuC3s%2FbtsfPCvSbS7%2FgJkD07Kxqlj488b16quo31%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;182&quot; data-filename=&quot;스크린샷 2023-05-14 오후 7.39.25.png&quot; data-origin-width=&quot;564&quot; data-origin-height=&quot;182&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;네 바꿨어요&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;당시에 우리 과 19학번 카페가 만들어졌었는데, 의무적으로 자기소개를 하나씩 올렸어야 했다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;그렇게 다른 사람의 자기소개를 보다 보니 예상 외로 C언어에 대해 예습한다는 학우들이 많았던 것이다!&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; 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;1080&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dsa4CY/btsfPCWU2x9/MVggDVEAuqYrLkZ2C4E4a0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dsa4CY/btsfPCWU2x9/MVggDVEAuqYrLkZ2C4E4a0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dsa4CY/btsfPCWU2x9/MVggDVEAuqYrLkZ2C4E4a0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdsa4CY%2FbtsfPCWU2x9%2FMVggDVEAuqYrLkZ2C4E4a0%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;512&quot; height=&quot;512&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;1080&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;옛날부터 뒤처지는 건 정말 참을 수 없었기 때문에 뭔가라도 해보고 싶어서 야심차게 깃허브를 가입하려고 했다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;나는 다음, 네이버, 구글 웬만한 사이트들에 대해 아이디를 통일하고 비밀번호만 조금씩 다르게 사용하는 편인데,&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;이상하게 이날은 뭔가 다른 닉네임을 사용하고 싶었다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: center;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;XD, &lt;/b&gt;&lt;b&gt;:P, &lt;/b&gt;&lt;b&gt;:D, &lt;/b&gt;&lt;b&gt;:)&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: center;&quot; 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-05-14 오후 7.11.42.png&quot; data-origin-width=&quot;1712&quot; data-origin-height=&quot;318&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QV88b/btsfpeiyuvB/ounE3cHYb1zawxLxph4MKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QV88b/btsfpeiyuvB/ounE3cHYb1zawxLxph4MKK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QV88b/btsfpeiyuvB/ounE3cHYb1zawxLxph4MKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQV88b%2FbtsfpeiyuvB%2FounE3cHYb1zawxLxph4MKK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1712&quot; height=&quot;318&quot; data-filename=&quot;스크린샷 2023-05-14 오후 7.11.42.png&quot; data-origin-width=&quot;1712&quot; data-origin-height=&quot;318&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;나무위키를 보면 다음과 같이 설명되어 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&quot;반시계 방향 혹은 시계 방향으로 90도 돌려봐야 이해할 수 있는 모양&quot;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;와! 이거 귀엽다&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; 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-05-14 오후 7.13.03.png&quot; data-origin-width=&quot;674&quot; data-origin-height=&quot;760&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/btrpZF/btsfdcl74FJ/5ZwWmLLWtoyvFpTqreFIiK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/btrpZF/btsfdcl74FJ/5ZwWmLLWtoyvFpTqreFIiK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/btrpZF/btsfdcl74FJ/5ZwWmLLWtoyvFpTqreFIiK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbtrpZF%2Fbtsfdcl74FJ%2F5ZwWmLLWtoyvFpTqreFIiK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;271&quot; height=&quot;306&quot; data-filename=&quot;스크린샷 2023-05-14 오후 7.13.03.png&quot; data-origin-width=&quot;674&quot; data-origin-height=&quot;760&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;누구신지 모르겠지만 안녕하세요?&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;그외에 나머지는 특수문자가 들어가있어서 할 수가 없었다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;그래서 뭐하지... 하고 고민하다가 나만의 이모티콘을 만들고 싶었다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; 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;스크린샷 2023-05-14 오후 7.17.46.png&quot; data-origin-width=&quot;1222&quot; data-origin-height=&quot;426&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bfq4GI/btsfvoZr2LW/jBFs6wVvzxeJRkck0CFGo1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bfq4GI/btsfvoZr2LW/jBFs6wVvzxeJRkck0CFGo1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bfq4GI/btsfvoZr2LW/jBFs6wVvzxeJRkck0CFGo1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbfq4GI%2FbtsfvoZr2LW%2FjBFs6wVvzxeJRkck0CFGo1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;674&quot; height=&quot;235&quot; data-filename=&quot;스크린샷 2023-05-14 오후 7.17.46.png&quot; data-origin-width=&quot;1222&quot; data-origin-height=&quot;426&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;모자와 썬글라스를 쓰고 웃고 있는 사람&lt;/span&gt;을 만들자!&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;그래서 Cl8D라는 귀여운 닉네임이 만들어진 것이다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;(씨x 아님, C8 아님...)&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; 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;스크린샷 2023-05-14 오후 7.19.05.png&quot; data-origin-width=&quot;276&quot; data-origin-height=&quot;64&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/plok4/btsfdpMiIXl/VoAgf4E3B32KmwyDlrccL1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/plok4/btsfdpMiIXl/VoAgf4E3B32KmwyDlrccL1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/plok4/btsfdpMiIXl/VoAgf4E3B32KmwyDlrccL1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fplok4%2FbtsfdpMiIXl%2FVoAgf4E3B32KmwyDlrccL1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;276&quot; height=&quot;64&quot; data-filename=&quot;스크린샷 2023-05-14 오후 7.19.05.png&quot; data-origin-width=&quot;276&quot; data-origin-height=&quot;64&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; 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-05-14 오후 7.21.25.png&quot; data-origin-width=&quot;1184&quot; data-origin-height=&quot;354&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bIpnaQ/btsfdrDloQ9/fRaiosvkpggBJ41CIrlPF1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bIpnaQ/btsfdrDloQ9/fRaiosvkpggBJ41CIrlPF1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bIpnaQ/btsfdrDloQ9/fRaiosvkpggBJ41CIrlPF1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbIpnaQ%2FbtsfdrDloQ9%2FfRaiosvkpggBJ41CIrlPF1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;646&quot; height=&quot;193&quot; data-filename=&quot;스크린샷 2023-05-14 오후 7.21.25.png&quot; data-origin-width=&quot;1184&quot; data-origin-height=&quot;354&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;요건 이제 모자와 선글라스를 쓰고 메롱하는 사람이 된 것이다!&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;아무튼... 더 이상 아무 오해도 없었으면  &lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;+) 그럼 티스토리는?&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;추가적으로 티스토리는 왜 돌멩이냐면...&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;그냥 돌멩이라는 단어를 좋아한다. 어감이 동글동글 귀엽다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;근데 한글로 돌멩이 하려고 하니까 안 돼서 영어로 dolmeng2라고 했다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; 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;스크린샷 2023-05-14 오후 7.23.47.png&quot; data-origin-width=&quot;402&quot; data-origin-height=&quot;498&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2DwcC/btsfpebN9IY/6iQvTydRmGryuziRw5N8GK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2DwcC/btsfpebN9IY/6iQvTydRmGryuziRw5N8GK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2DwcC/btsfpebN9IY/6iQvTydRmGryuziRw5N8GK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2DwcC%2FbtsfpebN9IY%2F6iQvTydRmGryuziRw5N8GK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;332&quot; height=&quot;411&quot; data-filename=&quot;스크린샷 2023-05-14 오후 7.23.47.png&quot; data-origin-width=&quot;402&quot; data-origin-height=&quot;498&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;실제로 여러 곳에서 내 닉네임은 돌멩이다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; 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-05-14 오후 7.26.01.png&quot; data-origin-width=&quot;342&quot; data-origin-height=&quot;274&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bnplVc/btsfeBTf5Yy/R6hKlArIbMwxkwhBeoSR9K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bnplVc/btsfeBTf5Yy/R6hKlArIbMwxkwhBeoSR9K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bnplVc/btsfeBTf5Yy/R6hKlArIbMwxkwhBeoSR9K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbnplVc%2FbtsfeBTf5Yy%2FR6hKlArIbMwxkwhBeoSR9K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;342&quot; height=&quot;274&quot; data-filename=&quot;스크린샷 2023-05-14 오후 7.26.01.png&quot; data-origin-width=&quot;342&quot; data-origin-height=&quot;274&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; 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-05-14 오후 7.27.28.png&quot; data-origin-width=&quot;706&quot; data-origin-height=&quot;266&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bI9PC3/btsffvrGNav/bWobmisltHjUkuvhpokCbK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bI9PC3/btsffvrGNav/bWobmisltHjUkuvhpokCbK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bI9PC3/btsffvrGNav/bWobmisltHjUkuvhpokCbK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbI9PC3%2FbtsffvrGNav%2FbWobmisltHjUkuvhpokCbK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;626&quot; height=&quot;236&quot; data-filename=&quot;스크린샷 2023-05-14 오후 7.27.28.png&quot; data-origin-width=&quot;706&quot; data-origin-height=&quot;266&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;요건 아이디어스...&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;등등 정말 여러 곳에서 사용하고 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;귀여우니까 ^_^&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;우테코 시작하고 져니로 바꾸고 싶었는데, 이미 있는지 적용이 안 된다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;그래서 그냥 돌멩이로 살고 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;가끔 돌멩이 블로그 잘 보고 있어요~라고 하면 기분이 좋다  &lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&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;스크린샷 2023-05-14 오후 7.06.19.png&quot; data-origin-width=&quot;1568&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tpjM6/btsfjI47Oee/6WvkUWkTl3xno2TqC8kyBK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tpjM6/btsfjI47Oee/6WvkUWkTl3xno2TqC8kyBK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tpjM6/btsfjI47Oee/6WvkUWkTl3xno2TqC8kyBK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtpjM6%2FbtsfjI47Oee%2F6WvkUWkTl3xno2TqC8kyBK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;658&quot; height=&quot;453&quot; data-filename=&quot;스크린샷 2023-05-14 오후 7.06.19.png&quot; data-origin-width=&quot;1568&quot; data-origin-height=&quot;1080&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;그렇게 만든 첫 레파지토리(?)&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;당연히 비공개다&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;C언어에 기죽지 않겠다고 만든 건데 언어는 왜 HTML인지 모르겠다&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;(HTML이 프로그래밍 언어 아닌 거 앎 반박 시 당신 말이 다 맞아요)&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;이때까지만 해도 내가 백엔드 할지 정말 꿈에도 상상 못했지....&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; 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;스크린샷 2023-05-14 오후 7.22.32.png&quot; data-origin-width=&quot;1346&quot; data-origin-height=&quot;344&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IrtaQ/btsfA3goDEg/YfMWbnpHV5PwY7FUG95qcK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IrtaQ/btsfA3goDEg/YfMWbnpHV5PwY7FUG95qcK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IrtaQ/btsfA3goDEg/YfMWbnpHV5PwY7FUG95qcK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIrtaQ%2FbtsfA3goDEg%2FYfMWbnpHV5PwY7FUG95qcK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1346&quot; height=&quot;344&quot; data-filename=&quot;스크린샷 2023-05-14 오후 7.22.32.png&quot; data-origin-width=&quot;1346&quot; data-origin-height=&quot;344&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;근데 저 레파지토리 두 번 커밋하고 버려짐&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;아무튼 다들 닉네임 보고 오해하지 않아주셨으면 좋겠다...&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;욕 아니에요&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <author>dolmeng2</author>
      <guid isPermaLink="true">https://cl8d.tistory.com/notice/84</guid>
      <pubDate>Sun, 14 May 2023 19:28:12 +0900</pubDate>
    </item>
    <item>
      <title>[Web] JWT를 통한 인증 과정 알아보기</title>
      <link>https://cl8d.tistory.com/83</link>
      <description>&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;인증과 인가, 쿠키 / 세션 방식에 대해서는 이전 포스팅에서 작성하였습니다.&lt;/p&gt;
&lt;figure id=&quot;og_1683522743081&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Web] 인증과 인가란? - 쿠키와 세션에 대해서 알아보자!&quot; data-og-description=&quot;  들어가기 전 테코톡에서 진행한 인증과 인가 관련 자료들을 바탕으로 글을 작성하였습니다 :D   인증 (Authentication) 보호된 리소스에 접근하는 것을 허용하기 이전에, 등록된 유저의 신원을 &quot; data-og-host=&quot;cl8d.tistory.com&quot; data-og-source-url=&quot;https://cl8d.tistory.com/77&quot; data-og-url=&quot;https://cl8d.tistory.com/77&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cWCETX/hySwJFjrGp/5Jjmf7HFTOtDpU8pJ1gy51/img.png?width=800&amp;amp;height=333&amp;amp;face=0_0_800_333,https://scrap.kakaocdn.net/dn/bSS79v/hySwO0Uu3x/ah0idzfTxElV4HDB2vvDOk/img.png?width=800&amp;amp;height=333&amp;amp;face=0_0_800_333,https://scrap.kakaocdn.net/dn/cfaYY3/hySyi0o5qi/FiXWRmkuDAhhHjYqgIuAJK/img.jpg?width=764&amp;amp;height=511&amp;amp;face=0_0_764_511&quot;&gt;&lt;a href=&quot;https://cl8d.tistory.com/77&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://cl8d.tistory.com/77&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cWCETX/hySwJFjrGp/5Jjmf7HFTOtDpU8pJ1gy51/img.png?width=800&amp;amp;height=333&amp;amp;face=0_0_800_333,https://scrap.kakaocdn.net/dn/bSS79v/hySwO0Uu3x/ah0idzfTxElV4HDB2vvDOk/img.png?width=800&amp;amp;height=333&amp;amp;face=0_0_800_333,https://scrap.kakaocdn.net/dn/cfaYY3/hySyi0o5qi/FiXWRmkuDAhhHjYqgIuAJK/img.jpg?width=764&amp;amp;height=511&amp;amp;face=0_0_764_511');&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;[Web] 인증과 인가란? - 쿠키와 세션에 대해서 알아보자!&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;  들어가기 전 테코톡에서 진행한 인증과 인가 관련 자료들을 바탕으로 글을 작성하였습니다 :D   인증 (Authentication) 보호된 리소스에 접근하는 것을 허용하기 이전에, 등록된 유저의 신원을&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;cl8d.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&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;4. 효율적으로 인증하기&lt;/b&gt;&lt;/h4&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;b&gt;사용자의 정보를 관리하는 주체가 너무 다양하다는 것&lt;/b&gt;이다. 이러한 문제점을 해결하기 위해서, 상태를 관리하는 것을 따로 두지 않고 &lt;b&gt;요청과 응답 내부에서 처리하는 것&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;+) 세션 기반 인증 방식에서 토큰 인증 방식이 나온 이유 중에 하나가 DB에 인증 정보를 저장하지 않아서 서버에 대한 부하를 줄이는 것도 있다고 많이들 말한다. 암호화된 인증 정보를 클라이언트가 관리함으로서 부하를 줄인다고는 하지만... 사실 보안 측면으로 보면 JWT 역시 DB를 사용하게 되다 보니까 이런 관점에서는 크게 좋은지 잘 모르겠다. (개인적인 의견)&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;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;'&lt;span style=&quot;color: #ef5369;&quot;&gt;JWT (Json Web Token)&lt;/span&gt;'가 존재한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;JWT는 정보를 Json 객체를 통해 안전하게 전송하기 위한 간결하고, 독립적인 방법을 정의하는 개방형 표준이다. (RFC 7519)&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;기본적으로 &lt;b&gt;HMAC 알고리즘을 통해 암호화&lt;/b&gt;가 되어 있으며, RSA나 ECDSA를 사용하는 &lt;b&gt;공개-개인키 쌍을 통해 서명&lt;/b&gt;할 수 있다.&lt;/span&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-05-08 오후 2.48.10.png&quot; data-origin-width=&quot;1018&quot; data-origin-height=&quot;270&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bwYCCf/btseh9b84MX/kXCQztukmi3lzgQPFpHLTk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bwYCCf/btseh9b84MX/kXCQztukmi3lzgQPFpHLTk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bwYCCf/btseh9b84MX/kXCQztukmi3lzgQPFpHLTk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbwYCCf%2Fbtseh9b84MX%2FkXCQztukmi3lzgQPFpHLTk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;622&quot; height=&quot;165&quot; data-filename=&quot;스크린샷 2023-05-08 오후 2.48.10.png&quot; data-origin-width=&quot;1018&quot; data-origin-height=&quot;270&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;JWT는 크게 헤더, 페이로드, 그리고 시그니처로 이루어져 있다.&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;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;  Header&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;헤더는 일반적으로 &lt;span style=&quot;color: #ef5369;&quot;&gt;토큰 유형과 서명 알고리즘&lt;/span&gt; 2가지가 포함되어 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1683525319504&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;alg&quot;: &quot;HS256&quot;,
  &quot;typ&quot;: &quot;JWT&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;만약 이런 식으로 구성되어 있다면, 이는 현재 타입은 JWT, 서명 알고리즘으로 HS256을 사용했음을 나타낸다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 정보를 Base64로 인코딩하게 되면 JWT의 헤더 부분이 완성된다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;  Payload&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;payload은 claim 정보가 포함되며,&lt;span style=&quot;color: #ef5369;&quot;&gt; claim은 entity에 대한 내용이 들어간다&lt;/span&gt;. (사용자에 대한 정보)&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;인증에 필요한 정보가 보통 이곳에 들어가지만, &lt;b&gt;민감한 정보는 넣지 않는 것이 좋다&lt;/b&gt;.&lt;/p&gt;
&lt;pre id=&quot;code_1683528423851&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;sub&quot;: &quot;1234567890&quot;,
  &quot;name&quot;: &quot;John Doe&quot;,
  &quot;admin&quot;: true
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;마찬가지로 위에 대한 정보를 base64로 인코딩하면 payload에 대한 정보가 들어간다.&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;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;  Signature&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;인코딩된 헤더 및 인코딩된 페이로드,&lt;span style=&quot;color: #ef5369;&quot;&gt; secret (비밀 키 정보)&lt;/span&gt;, 헤더에 지정된 알고리즘을 사용하여 서명할 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;만약 HMAC SHA256 알고리즘을 사용한다면 아래와 같이 서명이 가능하다.&lt;/p&gt;
&lt;pre id=&quot;code_1683528583766&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;HMACSHA256(
  base64UrlEncode(header) + &quot;.&quot; +
  base64UrlEncode(payload),
  secret
)&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;시그니처 정보는 도중에 변경되지 않았는지 확인하는데 사용되며, 개인키로 서명된 토큰이라면 JWT의 발신자가 본인이 맞는지 확인하는데도 사용된다.&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;그래서 결과적으로 JWT는 .으로 &lt;span style=&quot;color: #ef5369;&quot;&gt;구분된 3개의 Base64로 인코딩된 문자열의 합&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-05-08 오후 3.53.08.png&quot; data-origin-width=&quot;858&quot; data-origin-height=&quot;210&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bTaFeF/btsepKbSJvo/97kriCFNnCtCMrTzhnhzy0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bTaFeF/btsepKbSJvo/97kriCFNnCtCMrTzhnhzy0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bTaFeF/btsepKbSJvo/97kriCFNnCtCMrTzhnhzy0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbTaFeF%2FbtsepKbSJvo%2F97kriCFNnCtCMrTzhnhzy0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;654&quot; height=&quot;160&quot; data-filename=&quot;스크린샷 2023-05-08 오후 3.53.08.png&quot; data-origin-width=&quot;858&quot; data-origin-height=&quot;210&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;보통 우리가 코드에서 활용할 때에는 'Bearer' 라는 prefix를 사용하여 Authorization 헤더에 넣는 것이 일반적이다.&lt;/p&gt;
&lt;pre id=&quot;code_1683529017660&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Authorization: Bearer &amp;lt;token&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;참고로, 나중에 실제 클라이언트와 통신할 때 CORS 문제를 겪을 수 있기 때문에 서버측에서 exposedHeader 값으로 Authroization 헤더를 허용해줘야 한다. (이거는 다음에 포스팅을 따로 해보겠다 ㅎㅎ)&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_1683548394178&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public String createToken(String payload) {
    Claims claims = Jwts.claims().setSubject(payload);
    Date now = new Date();
    Date validity = new Date(now.getTime() + validityInMilliseconds);

    return Jwts.builder()
            .setClaims(claims)
            .setIssuedAt(now)
            .setExpiration(validity)
            .signWith(SignatureAlgorithm.HS256, secretKey)
            .compact();
}&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-05-08 오후 9.26.01.png&quot; data-origin-width=&quot;1830&quot; data-origin-height=&quot;636&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MxxKY/btses5Gij5I/nNUTjrLanVHEXse3HkbjMk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MxxKY/btses5Gij5I/nNUTjrLanVHEXse3HkbjMk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MxxKY/btses5Gij5I/nNUTjrLanVHEXse3HkbjMk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMxxKY%2Fbtses5Gij5I%2FnNUTjrLanVHEXse3HkbjMk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1830&quot; height=&quot;636&quot; data-filename=&quot;스크린샷 2023-05-08 오후 9.26.01.png&quot; data-origin-width=&quot;1830&quot; data-origin-height=&quot;636&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;코드를 각각 분석해보면 위와 같이 claim을 통한 사용자 정보 지정 및 만료 시간, 그리고 시크릿키를 통한 암호화를 하는 것을 볼 수 있다.&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_1683548519788&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public String getPayload(String token) {
    return Jwts.parser()
        .setSigningKey(secretKey)
        .parseClaimsJws(token)
        .getBody()
        .getSubject();
}&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-05-08 오후 9.48.30.png&quot; data-origin-width=&quot;1730&quot; data-origin-height=&quot;556&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0udFx/btsesfoT8xz/RqGy4xpPijhfN7ax2s4nAK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0udFx/btsesfoT8xz/RqGy4xpPijhfN7ax2s4nAK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0udFx/btsesfoT8xz/RqGy4xpPijhfN7ax2s4nAK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0udFx%2FbtsesfoT8xz%2FRqGy4xpPijhfN7ax2s4nAK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;785&quot; height=&quot;252&quot; data-filename=&quot;스크린샷 2023-05-08 오후 9.48.30.png&quot; data-origin-width=&quot;1730&quot; data-origin-height=&quot;556&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;  여기서 한 가지 주목할 점이 있다.&lt;br /&gt;parseClaimsJws()라는 메서드를 사용하고 있는 걸 볼 수 있는데, 이거 대신에 parseClaimsJwt()를 사용하면 오류가 발생한다.&lt;br /&gt;이는, 우리가 처음에 토큰을 생성할 때 signWith을 통해서 서명을 진행했기 때문에 복호화 시에도 서명에 대한 검증을 진행해야 하기 때문이다. (Jwt()의 경우 서명 검증 없이 단순히 헤더와 클레임만 추출함)&lt;br /&gt;parseClaimsJwt()을 사용하고 싶다면 토큰 생성 시에 signWith을 통해서 서명에 대한 정보를 넘겨주지 않으면 된다.&lt;/blockquote&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;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;  동작 방식 알아보기&lt;/b&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;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-05-14 오후 6.25.33.png&quot; data-origin-width=&quot;1698&quot; data-origin-height=&quot;786&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/YK5xu/btsfdq5rYMk/b1UKq2xFI1ixItk5qfYq61/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/YK5xu/btsfdq5rYMk/b1UKq2xFI1ixItk5qfYq61/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YK5xu/btsfdq5rYMk/b1UKq2xFI1ixItk5qfYq61/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYK5xu%2Fbtsfdq5rYMk%2Fb1UKq2xFI1ixItk5qfYq61%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1698&quot; height=&quot;786&quot; data-filename=&quot;스크린샷 2023-05-14 오후 6.25.33.png&quot; data-origin-width=&quot;1698&quot; data-origin-height=&quot;786&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;클라이언트에게 생성된 토큰을 내려주는 방식은 다양한데, 나는 위와 같이 Authorization 헤더에 담아서 내려줬다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;클라이언트는 해당 값을 받아서 다음 요청 시 Authorization 헤더에 JWT 정보를 함께 넣어서 보내는데, &lt;b&gt;서버는 해당 키를 생성할 때 사용하였던 시크릿키 정보를 활용하여 다시 디코딩 후에&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;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;  장점 / 단점&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;토큰 기반 인증 방식의 경우 서버 측에서 따로 저장할 필요가 없으며, &lt;span style=&quot;color: #ef5369;&quot;&gt;클라이언트가 요청에 대해 헤더 값에 함께 포함시켜 보내주면 된다&lt;/span&gt;.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;또한, MSA 같은 구조나 서버가 여러 대인 상황이라면 &lt;b&gt;같은 토큰을 통해 여러 서버에서 인증이 가능하기 때문에 더 효율적&lt;/b&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;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;하지만, JWT의 경우 탈취 당한다면 해당 사용자의 권한을 그대로 가지게 된다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;탈취당한 JWT의 경우 유효 기간이 지나기 전까지는 &lt;span style=&quot;color: #ef5369;&quot;&gt;강제로 만료시킬 수 없기 때문에&lt;/span&gt;, 정보를 바로 제거할 수 있는 세션에 비해서는 보안적 측면에서는 떨어진다고 볼 수 있다. (물론, 해당 토큰의 claim 정보가 악성 사용자임을 확실하게 나타낼 수 있다면, 해당 사용자의 요청은 차단하는 형태로는 만들 수 있을 것 같다.) 그래서 은행 같은 보안이 중요한 시스템에서는 세션을 많이 사용하는 것으로 알고 있다. (뇌피셜)&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;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;  Refresh Token&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;보통 탈취 문제로 인해서 JWT (이제는 'access token'이라고 부르겠다.)의&lt;b&gt;&amp;nbsp;유효 기간을 짧게 가져가는 편이며&lt;/b&gt; (30분 정도), 기한이 짧으면 요청에 대해서 새롭게 발급받아야 하는 횟수가 늘어나기 때문에 이를 방지하기 위해 '&lt;span style=&quot;color: #ef5369;&quot;&gt;refresh token&lt;/span&gt;'이라는 것을 따로 두기도 한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;access token에 대한 재발급 기능을 책임지는 token이며,&amp;nbsp; 기본적으로 서버에 저장할 수 있고 (&lt;b&gt;세션에 저장하거나 쿠키에 저장하는 방법도 있지만, in-memory db에 저장하는 것이 일반적&lt;/b&gt;), 탈취를 당하더라도 해당 사용자의 refresh token을 강제로 제거하여 보안에 대해서도 안정성이 높은 토큰이다.&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;보통 refresh token의 경우 유효 기간을 일주일~이주일 정도로 만들어둔다.&lt;/p&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;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-05-14 오후 6.28.48.png&quot; data-origin-width=&quot;1732&quot; data-origin-height=&quot;1008&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbQ0O7/btsfvoSFgw7/WZ5CMHi2vUzFW1PKNa2co1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbQ0O7/btsfvoSFgw7/WZ5CMHi2vUzFW1PKNa2co1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbQ0O7/btsfvoSFgw7/WZ5CMHi2vUzFW1PKNa2co1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbbQ0O7%2FbtsfvoSFgw7%2FWZ5CMHi2vUzFW1PKNa2co1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1732&quot; height=&quot;1008&quot; data-filename=&quot;스크린샷 2023-05-14 오후 6.28.48.png&quot; data-origin-width=&quot;1732&quot; data-origin-height=&quot;1008&quot;/&gt;&lt;/span&gt;&lt;/figure&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;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;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-05-14 오후 6.42.41.png&quot; data-origin-width=&quot;1902&quot; data-origin-height=&quot;1078&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/H3v8M/btsfe7RSeH0/EtfEACjLmRS4KA30P23aq1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/H3v8M/btsfe7RSeH0/EtfEACjLmRS4KA30P23aq1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/H3v8M/btsfe7RSeH0/EtfEACjLmRS4KA30P23aq1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FH3v8M%2Fbtsfe7RSeH0%2FEtfEACjLmRS4KA30P23aq1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1902&quot; height=&quot;1078&quot; data-filename=&quot;스크린샷 2023-05-14 오후 6.42.41.png&quot; data-origin-width=&quot;1902&quot; data-origin-height=&quot;1078&quot;/&gt;&lt;/span&gt;&lt;/figure&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;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;로직이 상당히 복잡해졌지만, access token의 단점을 그나마 보완할 수 있기 때문에 많이 사용하는 방법이다.&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;짧은 access token의 유효시간에 대처하기 위한 다른 방법으로&lt;span style=&quot;color: #ef5369;&quot;&gt; sliding session&lt;/span&gt;이라는 친구도 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;사용자가 계속해서 서비스를 이용하고 있다면 (JS의 이벤트 핸들러로 감지) access token이 만료되기 전에 새로운 access token을 발급받아 자동으로 갱신한 토큰을 받아오는 것이다. 이건 구현해본 적이 없어서 잘 모르겠지만... 아무래도 클라이언트 측에서 계속 확인해야 할 것 같다.&lt;/p&gt;</description>
      <category>✏️/CS</category>
      <category>accesstoken</category>
      <category>jsonwebtoken</category>
      <category>JWT</category>
      <category>refreshtoken</category>
      <author>dolmeng2</author>
      <guid isPermaLink="true">https://cl8d.tistory.com/83</guid>
      <comments>https://cl8d.tistory.com/83#entry83comment</comments>
      <pubDate>Sun, 14 May 2023 18:48:01 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] 스프링 테스트 - 테스트 컨텍스트 캐싱, @SpringBootTest, @WebMvcTest</title>
      <link>https://cl8d.tistory.com/82</link>
      <description>&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;테스트 쪽은 공식 문서도 생각보다 가독성이 너무 안 좋아서 최대한 간략하게만 훑어보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 모르는 게 정말 많은 것 같다 ㅎㅎ&lt;/p&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;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  통합 테스트 vs 인수 테스트&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;통합 테스트는 특정 작업을 수행하기 위해, 외부 작업들과 연관되어 있다면&lt;span style=&quot;color: #ef5369;&quot;&gt; 해당 외부 작업들을 포함하여 구성 요소가 잘 돌아가는지&lt;/span&gt; 테스트하는 방법으로, @SpringBootTest 어노테이션을 활용하여 진행하는 방법이 많다. (관련된 모든 빈을 가져와서 테스트 하는 느낌)&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: #ef5369;&quot;&gt;사용자의 시나리오에 맞춰&lt;/span&gt; 수행하는 테스트이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인수 테스트의 방법론으로 E2E 테스트를 통해 주어진 시나리오에 따라 &lt;b&gt;애플리케이션의 모든 계층이 서로 잘 동작하는지 확인&lt;/b&gt;할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RestAssured를 활용해서 테스트 코드를 작성하는 방식을 많이 보았다.&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;사실 인수 테스트와 E2E 테스트가 동일한 것이라고 생각했었는데, E2E 테스트가 조금 더 큰 범위라고 생각할 수 있을 것 같다.&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&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;스프링은 테스트에 사용되는 애플리케이션 컨텍스트를 생성하고 관리하고, 테스트에 적용해주는 기능을 가진 &amp;lsquo;&lt;b&gt;테스트 프레임워크&lt;/b&gt;&amp;rsquo;를 제공한다. 그리고 이를 &amp;lsquo;&lt;span style=&quot;color: #ef5369;&quot;&gt;테스트 컨텍스트 프레임워크&lt;/span&gt;&amp;rsquo;라고 부른다.&lt;br /&gt;&lt;br /&gt;테스트 대상 메서드에 @Test를 붙이면 해당 메서드가 속한 클래스는 &amp;lsquo;테스트 클래스&amp;rsquo;로서 동작하며, &lt;b&gt;각 메서드는 독립적인 테스트&lt;/b&gt;가 된다.&lt;br /&gt;하지만, 테스트마다 테스트 컨텍스트를 매번 새로 생성하게 된다면 오버헤드가 크기 때문에 전체 테스트의 실행 속도가 느려져셔 개발자의 생산성이 떨어진다. 이때문에 테스트 컨텍스트는 &lt;span style=&quot;color: #ef5369;&quot;&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;@ContextConfiguration&lt;/b&gt; 어노테이션을 선언함으로서 구성 정보를 주입해줄 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;  페어가 @Import 어노테이션이랑 다른 거냐고 물어봤는데, 내부적으로 조금 까보니까 등록되는 빈이 조금 다르다.&lt;br /&gt;@Import의 경우 importsCleanupPostProcessor라는 빈이 추가로 등록되는 거를 확인했다.&lt;br /&gt;또한, @Import의 경우 기존 ApplicationContext에 해당 Component를 추가한다는 것이고, @ContextConfiguration의 경우는 어떤 애플리케이션 컨텍스트를 사용할지, 그리고 XML 같은 걸로 설정도 할 수 있다는 점이 있을 것 같다.&lt;br /&gt;사실 명확하게 둘의 차이를 이해하기는 어렵지만... 유스케이스가 다른 것 같다.&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;  컨텍스트 설정 정보 주입하기&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1683463740556&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@ExtendWith(SpringExtension.class) 
@ContextConfiguration(classes = {AppConfig.class, TestConfig.class}) 
class MyTest {

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  @ExtendWith 어노테이션의 경우 스프링 프레임워크와 Junit5를 연동해주는 역할으로서, @Test 같은 어노테이션을 사용하기 위해서는 붙여줘야 한다. (Junit4에서는 @RunWith(SpringRunner.class)을 사용했었음)&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;@ContextConfiguration 어노테이션을 활용하여&lt;b&gt; 어떤 설정 정보 파일을 구성할지 설정할 수 있다&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 classes 인자로 준 AppConfig.class와 TestConfig.class에 등록된 빈 정보를 바탕으로 ApplicationContext가 구성되며, 이때 각각의 Configuration 클래스의 경우 @Configuration 어노테이션 (스프링 부트라면 @TestConfiguration도 가능)이 붙어 있거나, @Component, @Service, @Repository 같은 스테레오 타입 어노테이션이 붙어있는 클래스면 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1683464858147&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
public class Hello {
    @Bean
    public Nested nested() {
        return new Nested();
    }

    static class Nested {

    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 static nested class로 선언되어 있는 빈을 등록한다고 생각해보자.&lt;/p&gt;
&lt;pre id=&quot;code_1683464899447&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = {Hello.class})
public class Sample {

    @Autowired
    private Nested nested;

    @Test
    void test() {

    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 위와 같이 Hello라는 설정 정보 클래스를 @ContextConfiguration으로 불러오게 되면, Hello라는 Configuration 클래스에 대한 정보를 읽어서 등록되어 있는 Nested라는 빈에 대해 의존관계를 주입받을 수 있게 된다.&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;만약, @ContextConfiguration 어노테이션을 제거하게 되면 아래와 같이 컴파일 오류가 발생한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-05-07 오후 10.09.10.png&quot; data-origin-width=&quot;1494&quot; data-origin-height=&quot;774&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bevYsQ/btsd3edNK7t/g8ARhxwZK1fJrmZZ2BCqjK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bevYsQ/btsd3edNK7t/g8ARhxwZK1fJrmZZ2BCqjK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bevYsQ/btsd3edNK7t/g8ARhxwZK1fJrmZZ2BCqjK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbevYsQ%2Fbtsd3edNK7t%2Fg8ARhxwZK1fJrmZZ2BCqjK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;702&quot; height=&quot;364&quot; data-filename=&quot;스크린샷 2023-05-07 오후 10.09.10.png&quot; data-origin-width=&quot;1494&quot; data-origin-height=&quot;774&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&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;하지만, 나는 지금까지 @ContextConfiguration이라는 친구를 한 번도 테스트 코드에서 사용한 적이 없었다.&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-05-07 오후 10.10.26.png&quot; data-origin-width=&quot;1452&quot; data-origin-height=&quot;72&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IYOT9/btsecuGEzyA/piBzda1Wi0wCbBiDNUeCt0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IYOT9/btsecuGEzyA/piBzda1Wi0wCbBiDNUeCt0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IYOT9/btsecuGEzyA/piBzda1Wi0wCbBiDNUeCt0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIYOT9%2FbtsecuGEzyA%2FpiBzda1Wi0wCbBiDNUeCt0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1452&quot; height=&quot;72&quot; data-filename=&quot;스크린샷 2023-05-07 오후 10.10.26.png&quot; data-origin-width=&quot;1452&quot; data-origin-height=&quot;72&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Unable to find a @SpringBootConfiguration, you need to use @ContextConfiguration or @SpringBootTest(classes=...) with your test java.lang.IllegalStateException: Unable to find a @SpringBootConfiguration, you need to use @ContextConfiguration or @SpringBootTest(classes=...) with your test&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로는 @ContextConfiguration이나 @SpringBootTest 어노테이션을 지정해달라는 오류가 발생한다.&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;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;b&gt;⭐️&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;를 사용하고, &lt;span style=&quot;color: #ef5369;&quot;&gt;@SpringBootApplication 어노테이션이 붙은 클래스가 존재하는 패키지의 하위 패키지에 테스트 클래스를 두었다면&lt;/span&gt;, 알아서 적절한 정보를 찾게 된다. (보통 최상단 패키지에 @SpringBootApplication을 두는 이유가 여기서 나온다)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-05-07 오후 10.12.06.png&quot; data-origin-width=&quot;1508&quot; data-origin-height=&quot;98&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bYLCgc/btseh7YtSII/0WQf6XL9jNAStrlTsZpFq1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bYLCgc/btseh7YtSII/0WQf6XL9jNAStrlTsZpFq1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bYLCgc/btseh7YtSII/0WQf6XL9jNAStrlTsZpFq1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbYLCgc%2Fbtseh7YtSII%2F0WQf6XL9jNAStrlTsZpFq1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1508&quot; height=&quot;98&quot; data-filename=&quot;스크린샷 2023-05-07 오후 10.12.06.png&quot; data-origin-width=&quot;1508&quot; data-origin-height=&quot;98&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Found @SpringBootConfiguration com.example.study.StudyApplication for test class com.example.study.hi.Sample&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 코드를 돌려보면 위와 같이 @SpringBootConfiguration이라는 어노테이션을 찾았다는 것을 볼 수 있는데, 이는 @SpringBootApplication 어노테이션이 존재하는 클래스를 찾으면서&lt;b&gt; 내부적으로 중첩되어 있는 어노테이션인 @SpringBootConfiguration&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-05-07 오후 10.15.21.png&quot; data-origin-width=&quot;1702&quot; data-origin-height=&quot;414&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3tsVE/btsd9ecRIy7/oAc88IjLT76INsEoxpTxZK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3tsVE/btsd9ecRIy7/oAc88IjLT76INsEoxpTxZK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3tsVE/btsd9ecRIy7/oAc88IjLT76INsEoxpTxZK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3tsVE%2Fbtsd9ecRIy7%2FoAc88IjLT76INsEoxpTxZK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1702&quot; height=&quot;414&quot; data-filename=&quot;스크린샷 2023-05-07 오후 10.15.21.png&quot; data-origin-width=&quot;1702&quot; data-origin-height=&quot;414&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이&lt;span style=&quot;color: #ef5369;&quot;&gt; @SpringBootApplication 어노테이션의 경우 중첩으로 @SpringBootConfiguration이라는 어노테이션을 가지고 있다&lt;/span&gt;. (@Configuration 어노테이션이랑 거의 동일하게 동작한다고 생각하면 된다.) 덕분에 별도의 컨텍스트 정보를 지정하지 않더라도 알아서 해당 어노테이션이 붙은 클래스를 중심으로 하위 클래스들의 설정 정보를 자동으로 찾아서 등록하게 된다.&amp;nbsp;&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;style3&quot; /&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  Context Caching&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;테스트 컨텍스트 프레임워크는 테스트에 대한 ApplicationContext를 로드하면, &lt;span style=&quot;color: #ef5369;&quot;&gt;해당 컨텍스트가 캐싱되어 동일한 테스트 컨텍스트 구성을 사용하게 되면 해당 컨텍스트를 재사용&lt;/span&gt;하게 된다. 보통 ApplicationContext의 경우 이를 로드하기 위한 configuration parameter의 조합을 통해 고유하게 식별이 가능하다.&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: #333333;&quot;&gt;configuration parameter로는 다음과 같은 정보들을 사용한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 136px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 17px; text-align: center;&quot;&gt;&lt;b&gt;locations&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 17px; text-align: center;&quot;&gt;@ContextConfiguration 어노테이션의 인자&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 17px; text-align: center;&quot;&gt;&lt;b&gt;classes&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 17px; text-align: center;&quot;&gt;@ContextConfiguration 어노테이션의 인자&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 17px; text-align: center;&quot;&gt;&lt;b&gt;contextInitializerClasses&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 17px; text-align: center;&quot;&gt;@ContextConfiguration 어노테이션의 인자&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 17px; text-align: center;&quot;&gt;&lt;b&gt;contextCustomizers&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 17px; text-align: center;&quot;&gt;ContextCustomizerFactory로부터 로드&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 17px; text-align: center;&quot;&gt;&lt;b&gt;contextLoader&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 17px; text-align: center;&quot;&gt;@ContextConfiguration 어노테이션의 인자&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 17px; text-align: center;&quot;&gt;&lt;b&gt;parent&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 17px; text-align: center;&quot;&gt;@ContextHierarchy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 17px; text-align: center;&quot;&gt;&lt;b&gt;activeProfiles&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 17px; text-align: center;&quot;&gt;@ActiveProfile&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 17px; text-align: center;&quot;&gt;&lt;b&gt;propertySourceLocations&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 17px; text-align: center;&quot;&gt;@TestPropertySource&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%; text-align: center;&quot;&gt;&lt;b&gt;propertySourceProperties&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 50%; text-align: center;&quot;&gt;@TestPropertySource&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%; text-align: center;&quot;&gt;&lt;b&gt;resourceBasePath&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 50%; text-align: center;&quot;&gt;@WebAppConfiguration&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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;컨텍스트&amp;nbsp;캐시의&amp;nbsp;크기는&amp;nbsp;32로&amp;nbsp;제한되어&amp;nbsp;있으며,&amp;nbsp;최대&amp;nbsp;크기에&amp;nbsp;도달하게&amp;nbsp;되면&amp;nbsp;LRU&amp;nbsp;알고리즘에&amp;nbsp;따라&amp;nbsp;가장&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;실제로 코드를 통해 확인해보자. 위에서 선언했던 Sample 클래스에 동일한 설정 정보를 가진 Sample2 클래스를 생성해주었다.&lt;/p&gt;
&lt;pre id=&quot;code_1683466029094&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = {Hello.class})
public class Sample {

    @Autowired
    private Nested nested;

    @Autowired
    ApplicationContext ac;

    @Test
    void test() {
        System.out.println(&quot;test1 = &quot; + ac);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1683466034575&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = {Hello.class})
public class Sample2 {

    @Autowired
    private Nested nested;

    @Autowired
    ApplicationContext ac;

    @Test
    void test() {
        System.out.println(&quot;test2 = &quot; + ac);
    }
}&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-05-07 오후 10.27.29.png&quot; data-origin-width=&quot;1504&quot; data-origin-height=&quot;66&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wBLlu/btsd5ZUzaYV/sblau0WWicSINxJ5wxkgVK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wBLlu/btsd5ZUzaYV/sblau0WWicSINxJ5wxkgVK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wBLlu/btsd5ZUzaYV/sblau0WWicSINxJ5wxkgVK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwBLlu%2Fbtsd5ZUzaYV%2Fsblau0WWicSINxJ5wxkgVK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;66&quot; data-filename=&quot;스크린샷 2023-05-07 오후 10.27.29.png&quot; data-origin-width=&quot;1504&quot; data-origin-height=&quot;66&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-05-07 오후 10.27.40.png&quot; data-origin-width=&quot;1510&quot; data-origin-height=&quot;98&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d9E5nk/btsd6sWTNFM/88OCffWltfUsvPigZxJPd1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d9E5nk/btsd6sWTNFM/88OCffWltfUsvPigZxJPd1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d9E5nk/btsd6sWTNFM/88OCffWltfUsvPigZxJPd1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd9E5nk%2Fbtsd6sWTNFM%2F88OCffWltfUsvPigZxJPd1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1510&quot; height=&quot;98&quot; data-filename=&quot;스크린샷 2023-05-07 오후 10.27.40.png&quot; data-origin-width=&quot;1510&quot; data-origin-height=&quot;98&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘 다 GenericApplicationContext@75437611이라는 동일한 applicationContext 객체를 사용하는 것을 볼 수 있다.&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;@ContextConfiguration 어노테이션 대신에 @SpringBootTest를 사용하는 테스트 클래스를 하나 더 추가해보자.&lt;/p&gt;
&lt;pre id=&quot;code_1683466448785&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@SpringBootTest
@ExtendWith(SpringExtension.class)
public class Sample3 {

    @Autowired
    private Nested nested;

    @Autowired
    ApplicationContext ac;

    @Test
    void test() {
        System.out.println(&quot;test3 = &quot; + ac);
    }
}&lt;/code&gt;&lt;/pre&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;스크린샷 2023-05-07 오후 10.37.00.png&quot; data-origin-width=&quot;1936&quot; data-origin-height=&quot;66&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bywjKJ/btsd3erk3EQ/S0YV1st7PnqNSdKal930ek/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bywjKJ/btsd3erk3EQ/S0YV1st7PnqNSdKal930ek/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bywjKJ/btsd3erk3EQ/S0YV1st7PnqNSdKal930ek/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbywjKJ%2Fbtsd3erk3EQ%2FS0YV1st7PnqNSdKal930ek%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1936&quot; height=&quot;66&quot; data-filename=&quot;스크린샷 2023-05-07 오후 10.37.00.png&quot; data-origin-width=&quot;1936&quot; data-origin-height=&quot;66&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-05-07 오후 10.37.13.png&quot; data-origin-width=&quot;1986&quot; data-origin-height=&quot;82&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/byJ3Iz/btseh9aVX6H/U9AKIQss3qeWtlGVTMJPE1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/byJ3Iz/btseh9aVX6H/U9AKIQss3qeWtlGVTMJPE1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/byJ3Iz/btseh9aVX6H/U9AKIQss3qeWtlGVTMJPE1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbyJ3Iz%2Fbtseh9aVX6H%2FU9AKIQss3qeWtlGVTMJPE1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1986&quot; height=&quot;82&quot; data-filename=&quot;스크린샷 2023-05-07 오후 10.37.13.png&quot; data-origin-width=&quot;1986&quot; data-origin-height=&quot;82&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-05-07 오후 10.37.21.png&quot; data-origin-width=&quot;2070&quot; data-origin-height=&quot;78&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/KWCoK/btsegf3tW92/k0Pqd3zJIiD6dBfIZBwJB1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/KWCoK/btsegf3tW92/k0Pqd3zJIiD6dBfIZBwJB1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/KWCoK/btsegf3tW92/k0Pqd3zJIiD6dBfIZBwJB1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKWCoK%2Fbtsegf3tW92%2Fk0Pqd3zJIiD6dBfIZBwJB1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2070&quot; height=&quot;78&quot; data-filename=&quot;스크린샷 2023-05-07 오후 10.37.21.png&quot; data-origin-width=&quot;2070&quot; data-origin-height=&quot;78&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확인해보면 @SpringBootTest를 사용한 test3만 다른 ApplicationContext 객체를 사용하는 것을 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;  만약 @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)을 사용하게 된다면 실제 웹 환경이 구성되기 때문에 다른 컨텍스트 정보를 지정하게 되면 다른 웹 서버가 띄워지게 된다.&amp;nbsp;&lt;/blockquote&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;style3&quot; /&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;  컨텍스트 강제 초기화하기 - @DirtiesContext&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 강제로 컨텍스트를 제거하고 싶다면 클래스, 혹은 메서드 레벨에 @DirtiesContext 어노테이션을 추가하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러면 동일한 애플리케이션 컨텍스트를 사용하더라도&lt;span style=&quot;color: #ef5369;&quot;&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;  새로운 스프링 컨테이너에서 테스트를 실행하고 싶은 경우&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1683466760343&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 현재 테스트 클래스 이전에 컨텍스트를 더티 처리
@DirtiesContext(classMode = BEFORE_CLASS) 
class FreshContextTests {
}

// 각각의 테스트 메서드 전에 컨텍스트를 더티 처리
@DirtiesContext(classMode = BEFORE_EACH_TEST_METHOD)
class FreshContextTests {
}

// 현재 테스트 메서드 이전의 컨텍스트를 더티 처리
@DirtiesContext(methodMode = BEFORE_METHOD) 
@Test
void testProcessWhichRequiresFreshAppCtx() {
}&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;span style=&quot;color: #333333;&quot;&gt;  스프링 컨텍스트를 더럽히는 동작을 내부적으로 수행할 것 같을 때&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1683466837437&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 현재 클래스 이후에 컨텍스트를 더티 처리
@DirtiesContext 
class ContextDirtyingTests {
}

// 각각의 테스트 메서드 후에 컨텍스트를 더티 처리
@DirtiesContext(classMode = AFTER_EACH_TEST_METHOD) 
class ContextDirtyingTests {
}

// 현재 테스트 메서드 이후의 컨텍스트를 더티 처리
@DirtiesContext 
@Test
void testProcessWhichDirtiesAppCtx() {
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;  페어랑 얘기하면서 언제 @DirtiesContext를 사용할 수 있을지 이야기를 나누어보았다.&lt;br /&gt;&lt;br /&gt;다른 분들은 데이터베이스 초기화를 위해서 사용하신다고 말씀하셨는데, 사실 @DirtiesContext의 경우 새로운 서버를 띄우면서 (기존의 서버를 닫고) 인메모리 데이터베이스를 새롭게 띄우게 되다 보니까 데이터베이스가 초기화된다고 생각했다. 그렇다면 만약 서버를 띄워도 초기화되지 않는 데이터베이스를 사용한다면 (프로파일별로 다른 DB를 사용할 경우...) @DirtiesContext을 사용하더라도 초기화가 안 될 것 같다고 생각했다.&lt;br /&gt;&lt;br /&gt;벨덩 같은 글을 보면 싱글톤 빈이 상태를 가지고 있는 경우 내부적으로 해당 빈을 조작했을 때 applicationContext를 강제적으로 초기화하려면 사용하라는 말이 있었는데, 애초에 싱글톤 빈이 상태를 가지고 있는 것 자체가 잘못된 설계라는 생각도 들고... 어렵다   앞으로 사용할 일이 있을까?... 모르겠다.&lt;/blockquote&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;style3&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, 스프링 부트에서는 애플리케이션 테스트 시 도움이 되는 다양한 유틸리티와 어노테이션을 지원한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히, spring-boot-starter-test dependency를 사용하게 되면 Junit, AssertJ 같은 유용한 라이브러리들도 함께 사용할 수 있다!&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;size18&quot;&gt;&lt;b&gt;  @SpringBootTest&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 언급했던 스프링 프레임워크 표준 어노테이션인 @ContextConfiguration 대신 @SpringBootTest 어노테이션을 활용할 수 있다. @SpringBootTest&amp;nbsp;애노테이션의&amp;nbsp;경우&amp;nbsp;&lt;b&gt;기본적으로는&amp;nbsp;서버를&amp;nbsp;시작하지&amp;nbsp;않지만&lt;/b&gt;,&amp;nbsp;webEnvironment&amp;nbsp;옵션을&amp;nbsp;통해서&amp;nbsp;어떤&amp;nbsp;식으로&amp;nbsp;테스트&amp;nbsp;환경을&amp;nbsp;조성할지&amp;nbsp;설정할&amp;nbsp;수&amp;nbsp;있다.&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;MOCK (기본값)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;: ApplicationContext을 로드라고 가짜 웹 환경을 구성한다. 내장 서버가 시작되지 않으며, mock-based의 웹 애플리케이션의 테스트를 하기 위해서 @AutoConfigureMockMvc 또는 @AutoConfigureWebTestClient와 함게 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;RANDOM_PORT&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WebServerApplicationContext를 로드하고, 실제 웹 환경을 제공한다. 내장 서버가 실행되고, 임의의 포트에서 실행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 만약, 사용한 포트 정보가 궁금하다면 @LocalServerPort 어노테이션을 통해서 해당 포트 정보를 얻어올 수 있다. (@Value(&quot;${local.server.port}&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;span style=&quot;color: #ef5369;&quot;&gt;DEFINED_PORT&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WebServerApplicationContext를 로드하고 실제 웹 환경을 제공하며, 내장 서버가 실행되지만 application.yml에 지정한 포트나 기본 포트인 8080을 사용한다.&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: #ef5369;&quot;&gt;NONE&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ApplicationContext를 로드하지만 웹 환경을 구축하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;⭐️ 만약, @Transactional을 테스트 메서드에 적용할 경우, 각 테스트 메서드가 끝날 때 롤백된다. 하지만 RANDOM_PORT, DEFINED_PORT를 통해 실제 웹 환경을 사용하게 되면 &lt;span style=&quot;color: #ef5369;&quot;&gt;트랜잭션이 잡힌 테스트 메서드의 스레드와 웹 서버의 스레드가 다르기 때문에&lt;/span&gt; 별도의 트랜잭션에 실행되어 테스트 메서드에서 실행된 메서드의 경우 트랜잭션 롤백이 되지 않는다.&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;  @WebMvcTest&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring MVC 인프라를 자동을 구성하고,&lt;b&gt; 웹 계층에서 사용하는 컴포넌트들을 스캔&lt;/b&gt;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 스캔되는 컴포넌트는 @Controller, @ControllerAdvice, @JsonComponent, Converter, GenericConverter, Filter, HandlerInterceptor, WebMvcConfigurer, WebMvcRegistrations 및 HandlerMethodArgumentResolver 등이 사용되며, 우리가 흔히 사용하는 &lt;span style=&quot;color: #ef5369;&quot;&gt;@Service, @Component, @Repository가 붙은 클래스들은 스캔 대상이 아니다&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;/p&gt;
&lt;pre id=&quot;code_1683471662273&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Controller
public class SampleController {
    void test() {

    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트용 컨트롤러이다. 그리고, 해당 컨트롤러를 테스트하기 위한 테스트용 메서드를 생성해보자.&lt;/p&gt;
&lt;pre id=&quot;code_1683472082262&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@WebMvcTest
class SampleControllerTest {

    @Autowired
    private SampleController sampleController;

    @Test
    void test() {

    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히 @WebMvcTest만 생성하고, @Autowired를 통해 주입받으면 아무 오류 없이 잘 동작한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-05-08 오전 12.08.44.png&quot; data-origin-width=&quot;870&quot; data-origin-height=&quot;288&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/csadSP/btsd02dxl3q/opimaSOuBVW0DEkdfxcxwK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/csadSP/btsd02dxl3q/opimaSOuBVW0DEkdfxcxwK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/csadSP/btsd02dxl3q/opimaSOuBVW0DEkdfxcxwK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcsadSP%2Fbtsd02dxl3q%2FopimaSOuBVW0DEkdfxcxwK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;594&quot; height=&quot;197&quot; data-filename=&quot;스크린샷 2023-05-08 오전 12.08.44.png&quot; data-origin-width=&quot;870&quot; data-origin-height=&quot;288&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, SampleController 클래스의 어노테이션을 @Service로 만들어보자.&lt;/p&gt;
&lt;pre id=&quot;code_1683473404231&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
public class SampleController {

    void test() {

    }
}&lt;/code&gt;&lt;/pre&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;스크린샷 2023-05-08 오전 12.26.06.png&quot; data-origin-width=&quot;2282&quot; data-origin-height=&quot;218&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bsvdK6/btsedanJcBN/UeKrPQO44wIVq5jQcWNHE1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bsvdK6/btsedanJcBN/UeKrPQO44wIVq5jQcWNHE1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bsvdK6/btsedanJcBN/UeKrPQO44wIVq5jQcWNHE1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbsvdK6%2FbtsedanJcBN%2FUeKrPQO44wIVq5jQcWNHE1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2282&quot; height=&quot;218&quot; data-filename=&quot;스크린샷 2023-05-08 오전 12.26.06.png&quot; data-origin-width=&quot;2282&quot; data-origin-height=&quot;218&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Error creating bean with name 'com.example.study.SampleControllerTest': Unsatisfied dependency expressed through field 'sampleController': No qualifying bean of type 'com.example.study.SampleController' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}&lt;/blockquote&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;즉, @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;그래서 나는 위와 같은 경우에 보통 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;1. 인자로 대상 클래스를 지정해주기&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1683473256147&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@WebMvcTest(SampleController.class)
class SampleControllerTest {

    @Autowired
    private SampleController sampleController;

    @Test
    void test() {
    }
}&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;2. Mock 객체로 선언해주기&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1683473360026&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@ExtendWith(MockitoExtension.class)
class SampleControllerTest {

    @InjectMocks
    private SampleController sampleController;

    @Test
    void test() {
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개인적으로 미션을 진행하면서 페어에게 @WebMvcTest의 경우 컨트롤러 계층을 테스트할 때 사용하는 게 좋을 것 같다는 이야기를 들었어서, 두 번째 방법으로 서비스 레이어를 테스트하곤 했다.&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;size18&quot;&gt;&lt;b&gt;  @JdbcTest&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@JdbcTest 어노테이션의 경우 기본적으로 내당 데이터베이스 및 JdbcTemplate 빈을 구성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB 레이어를 테스트할 때 많이 사용하며, 내부적으로 @Transactional 어노테이션이 선언되어 있기 때문에 클래스 레벨에 붙이면 각 테스트 메서드가 종료될 때 함께 롤백된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-05-08 오전 12.32.07.png&quot; data-origin-width=&quot;816&quot; data-origin-height=&quot;624&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b6VvYr/btsd4c0Z2cj/eV22RGf5Ws94NuQcRFGb3K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b6VvYr/btsd4c0Z2cj/eV22RGf5Ws94NuQcRFGb3K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b6VvYr/btsd4c0Z2cj/eV22RGf5Ws94NuQcRFGb3K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb6VvYr%2Fbtsd4c0Z2cj%2FeV22RGf5Ws94NuQcRFGb3K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;459&quot; height=&quot;351&quot; data-filename=&quot;스크린샷 2023-05-08 오전 12.32.07.png&quot; data-origin-width=&quot;816&quot; data-origin-height=&quot;624&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 @AutoConfigureJdbc 어노테이션을 통해서 Jdbc 와 관련된 빈들만 컨텍스트로 등록된다.&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;또한, @AutoConfigureTestDatabase 어노테이션 덕분에 인메모리 데이터베이스를 사용할 수 있게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로, 이 어노테이션의 경우 replace 옵션에 따라서 인메모리 데이터베이스를 사용할지 말지 결정할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1683474051070&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@JdbcTest
@AutoConfigureTestDatabase
class SampleControllerTest {

    @Test
    void test() {
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단하게 살펴보자. 해당 어노테이션을 사용한 결과이다. (사용하지 않은 경우에도 @JdbcTest에 의해서 지정된 것과 같은 효과)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 참고로, Replace 옵션은 기본적으로 Any이다. Any의 경우 자동으로 구성된 정보를 사용하거나, 혹은 수동으로 구성된 정보를 사용한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-05-08 오전 12.41.23.png&quot; data-origin-width=&quot;1988&quot; data-origin-height=&quot;54&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bfQYNJ/btsd6sJr3uB/Y0XqYeXVsKUXk7CyBnZAn0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bfQYNJ/btsd6sJr3uB/Y0XqYeXVsKUXk7CyBnZAn0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bfQYNJ/btsd6sJr3uB/Y0XqYeXVsKUXk7CyBnZAn0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbfQYNJ%2Fbtsd6sJr3uB%2FY0XqYeXVsKUXk7CyBnZAn0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1988&quot; height=&quot;54&quot; data-filename=&quot;스크린샷 2023-05-08 오전 12.41.23.png&quot; data-origin-width=&quot;1988&quot; data-origin-height=&quot;54&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;pre id=&quot;code_1683474210391&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@JdbcTest
@AutoConfigureTestDatabase(replace = Replace.NONE)
class SampleControllerTest {

    @Test
    void test() {
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면에, Replace를 NONE으로 해보자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-05-08 오전 12.44.08.png&quot; data-origin-width=&quot;940&quot; data-origin-height=&quot;74&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IJTVQ/btsd02dxWnE/Er1mSunhgKVNK2m9M2kax0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IJTVQ/btsd02dxWnE/Er1mSunhgKVNK2m9M2kax0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IJTVQ/btsd02dxWnE/Er1mSunhgKVNK2m9M2kax0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIJTVQ%2Fbtsd02dxWnE%2FEr1mSunhgKVNK2m9M2kax0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;940&quot; height=&quot;74&quot; data-filename=&quot;스크린샷 2023-05-08 오전 12.44.08.png&quot; data-origin-width=&quot;940&quot; data-origin-height=&quot;74&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때는 application.yml에 정의한 database를 사용하게 된다.&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;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;  @DataJdbcTest&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@DataJdbcTest의 경우 @JdbcTest와 거의 비슷하긴 하다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-05-08 오전 12.34.24.png&quot; data-origin-width=&quot;1052&quot; data-origin-height=&quot;744&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bpJYBK/btsd0tWJl9z/RqtwNcpahOX5n2qPfEDZ10/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bpJYBK/btsd0tWJl9z/RqtwNcpahOX5n2qPfEDZ10/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bpJYBK/btsd0tWJl9z/RqtwNcpahOX5n2qPfEDZ10/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbpJYBK%2Fbtsd0tWJl9z%2FRqtwNcpahOX5n2qPfEDZ10%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;523&quot; height=&quot;370&quot; data-filename=&quot;스크린샷 2023-05-08 오전 12.34.24.png&quot; data-origin-width=&quot;1052&quot; data-origin-height=&quot;744&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 딱 한 가지 다른 점이 있는데, @AutoConfigureDataJdbc라는 어노테이션이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 Spring Data JDBC repositories를 사용하는 테스트에서 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용해본 적이 없어서 좀 찾아봤는데, JPA랑 문법이 거의 비슷한 것 같다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;알아보고 싶다면 &lt;a title=&quot;공식 문서&quot; href=&quot;https://docs.spring.io/spring-data/jdbc/docs/current/reference/html/&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;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 쪽은 파고 팔수록 내용이 너무 많아서 자르기가 힘들다 ㅠ_ㅠ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래도 기술 부채 중 하나였는데 가볍게 잘 보고 넘어가는 것 같다!&lt;/p&gt;</description>
      <category>Back-end/Spring</category>
      <category>@SpringBootTest</category>
      <category>@WebMvcTest</category>
      <category>컨텍스트캐싱</category>
      <category>통합테스트</category>
      <author>dolmeng2</author>
      <guid isPermaLink="true">https://cl8d.tistory.com/82</guid>
      <comments>https://cl8d.tistory.com/82#entry82comment</comments>
      <pubDate>Mon, 8 May 2023 00:50:12 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] IoC Container의 생명주기와 DI, 빈 스코프, 빈과 스태틱 메서드</title>
      <link>https://cl8d.tistory.com/81</link>
      <description>&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;프롤로그 로드맵의 키워드 위주로 정리하였습니다!&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;style3&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  DI (의존관계 주입)&lt;/b&gt;&lt;/h4&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;A가 B를 사용하고, B를 변경하면 A에 영향을 끼치는 관계.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의존관계 주입은 다음 세 가지 조건을 충족하는 작업을 의미한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;- 클래스 모델이나 코드에는 런타임 시점의 의존관계가 드러나지 않도록, 인터페이스만 의존해야 한다.&lt;br /&gt;- 런타임 시점의 의존관계는 컨테이너나 팩터리와 같은 제 3의 존재 (ex. IoC container)가 결정한다.&lt;br /&gt;- 의존관계는 사용할 오브젝트에 대한 레퍼런스를 외부에서 제공해줌으로서 만들어진다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;✔️ DI를 사용하면 무엇이 좋은가?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 객체간의 결합도 감소 &amp;rarr; 재사용성, 유지보수성 증가&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;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;style3&quot; /&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&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 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;의존 관계 주입은 크게 4가지 방법으로 나누어진다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;⭐️ &lt;span style=&quot;color: #ef5369;&quot;&gt;스프링 컨테이너가 관리하는 스프링 빈이어야만 의존관계 주입이 동작&lt;/span&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;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;  생성자 주입&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;생성자를 통해서 의존 관계를 주입받는 방법. 생성자 호출 시점에 딱 1번만 호출된다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;만약 &lt;b&gt;필드를 private final로 선언했다면 (일반적) 불변 및 생성에 대한 보장을 해줄 수 있다&lt;/b&gt;. (Spring 팀에서 권장하는 주입 방식)&lt;/p&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;만약 생성자가 1개라면 자동으로 @Autowired가 붙은 것과 동일한 효과를 주기 때문에 생략이 가능하다.&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;blockquote data-ke-style=&quot;style3&quot;&gt;⭐️ 기본적으로 스프링은 빈을 등록하는 단계와 의존관계를 주입하는 단계가 나누어져 있지만, 생성자 주입의 경우 &lt;span style=&quot;color: #ef5369;&quot;&gt;빈을 등록하는 단계와 의존관계 주입이 동시에 일어난다&lt;/span&gt;.&lt;br /&gt;&lt;br /&gt;이는 빈을 등록할 때 생성자를 호출해야 하기 때문에 의존 관계도 함께 주입되기 때문이며, 생성자 주입과 수정자 주입을 혼용하더라도 동시에 일어난다. (생성자 주입 이후 setter 주입 진행)&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;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;  수정자 주입&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;setter를 통해 의존 관계를 세팅해주는 방법. 생성자 주입을 통해 의존 관계가 주입되었더라도, 변경하고 싶을 때 사용 가능&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;수정자에 @Autowired를 붙여서 주입할 수 있다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;⭐️ 몇몇 글에서는 수정자 주입, 필드 주입을 진행하면 순환참조 문제를 잡을 수 없다고 말하지만, 스프링 2.6부터는 순환 참조를 기본으로 안 되도록 변경했기 때문에 런타임 시점에 서버가 실행되지 않는다.&lt;/blockquote&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;  필드 주입&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;필드에 @Autowired를 선언하여 바로 주입하는 방법.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;코드는 간결하지만, 외부에서 변경이 불가능하기 때문에 테스트하기 어려워진다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;DI 프레임워크에 의존적인 형태이다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt; &lt;span&gt;&amp;nbsp;&lt;/span&gt;일반 메서드 주입&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;일반 메서드를 통해서 주입하는 방법.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;수정자 주입처럼 setXXX의 형태여야 하지만, 수정자 주입과 다르게 여러 개의 파라미터를 넘길 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; 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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; &lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;b&gt;IoC (제어의 역전)&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;오브젝트의 생성과 관계 설정, 사용, 제거 등의 작업을 외부로 위임하는 것.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;✔️ IoC container (Spring Container)&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-05-07 오후 6.02.16.png&quot; data-origin-width=&quot;1358&quot; data-origin-height=&quot;842&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QSb50/btsd00fuyac/dRSdtknS4aXP7Q3QaP8sV1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QSb50/btsd00fuyac/dRSdtknS4aXP7Q3QaP8sV1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QSb50/btsd00fuyac/dRSdtknS4aXP7Q3QaP8sV1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQSb50%2Fbtsd00fuyac%2FdRSdtknS4aXP7Q3QaP8sV1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;503&quot; height=&quot;312&quot; data-filename=&quot;스크린샷 2023-05-07 오후 6.02.16.png&quot; data-origin-width=&quot;1358&quot; data-origin-height=&quot;842&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 스프링 프레임워크에서 IoC를 통해 Object를 관리하기 위해 사용하는 개념&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;BeanFactory / ApplicationContext 인터페이스.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ApplicationContext가 BeanFactory를 상속받고 있기 때문에 일반적으로 더 많이 사용한다.&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;- IoC container에 등록된 Bean에 대해서 DI를 내부적으로 관리&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때, 컨테이너에 등록하기 위해 개발자가 제공하는 정보를 &amp;lsquo;metadata&amp;rsquo;라고 표현&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;XML 기반 / 어노테이션 기반 구성&lt;/b&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;style3&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  스프링 컨테이너의 라이프사이클은 어떻게 되는가?&lt;/b&gt;&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;⭐️ 생성과 초기화를 분리하는 것이 핵심 역할이다.&lt;/blockquote&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;&lt;b&gt;✔️&lt;/b&gt;&amp;nbsp;빈이란?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring IoC 컨테이너에 의해서 인스턴스화 되고, 관리되는 객체이다.&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;size18&quot;&gt;&lt;b&gt;1. 스프링 컨테이너 생성&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. 스프링 빈 생성&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;  @Configuration +&lt;/b&gt; &lt;b&gt;@Bean&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;개발자가 컨트롤할 수 없는 외부 라이브러리들을 Bean으로 등록&lt;/span&gt;할 때 사용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ex) ObjectMapper 클래스를 커스텀하고 싶을 때&lt;/p&gt;
&lt;pre id=&quot;code_1683450261753&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 이런 식으로 'RedisTemplate'이라는 클래스에 직접 접근할 수 없는 경우
@Bean
public RedisTemplate&amp;lt;String, Object&amp;gt; redisTemplate(RedisConnectionFactory redisConnectionFactory) {
    RedisTemplate&amp;lt;String, Object&amp;gt; redisTemplate = new RedisTemplate&amp;lt;&amp;gt;();
    redisTemplate.setConnectionFactory(redisConnectionFactory);
    redisTemplate.setKeySerializer(new StringRedisSerializer());
    redisTemplate.setValueSerializer(new StringRedisSerializer());
    return redisTemplate;
}&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;  @Component&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;개발자가 컨트롤할 수 있는 클래스에 대해서&lt;/span&gt; 사용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Component의 경우 클래스에 대해서만 지정이 가능하다.&lt;/p&gt;
&lt;pre id=&quot;code_1683450286672&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 이런 식으로 직접 작성한 클래스에 대해서 선언하고 싶을 때 지정
@Component
public class RaceNumberGenerator implements NumberGenerator {
	...
}&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;- @ComponentScan 어노테이션에 의해서 @Component 및 스테레오 타입 (@Controller, @Service, @Repository) 어노테이션이 부여된 클래스들을 자동으로 스캔하여 빈으로  등록해준다.&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;&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;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;3. 의존 관계 주입&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성자에 지정된 @Autowired를 보고, 스프링 컨테이너가 자동으로 빈을 찾아 조회한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의존 관계 주입 시 &amp;lsquo;&lt;span style=&quot;color: #ef5369;&quot;&gt;타입이 같은 빈&lt;/span&gt;&amp;rsquo;을 찾아서 조회한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 동일한 타입이 있다면 @Primary나 @Qualifier 어노테이션을 사용하여 어떤 빈을 우선으로 주입할지 지정해야 한다.&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;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;4. 초기화 콜백&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;스프링은 의존관계 주입이 완료되면, 스프링 빈에게 콜백 메서드를 통해서 어떠한 초기화 기능을 할 수 있도록 제공한다.&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;  InitializingBean&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;InitializingBean 인터페이스의 afterProperiesSet() 메서드를 통해 초기화를 지원할 수 있다.&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;p data-ke-size=&quot;size16&quot;&gt;@Bean(initMethod=&amp;rdquo;&amp;rdquo;)을 통해서 초기화 메서드를 지정할 수 있다.&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;⭐️ @PostConstruct 사용&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;자바 표준 기술이기 때문에 스프링에 종속적이지 않다.&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;blockquote data-ke-style=&quot;style3&quot;&gt;위의 방법들을 혼용하여 사용한다면, 다음과 같은 순서로 호출된다.&lt;br /&gt;@PostConstruct &amp;rarr; afterPropertiesSet() &amp;rarr; @Bean(initMethod=&amp;rdquo;&amp;rdquo;)&lt;/blockquote&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;5. 실제로 사용하기&lt;/b&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;&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;size18&quot;&gt;&lt;b&gt;6. 소멸 전 콜백&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;&lt;b&gt;  &lt;/b&gt;DisposableBean 사용&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DisposableBean 인터페이스의 destroy() 메서드를 통해 소멸을 지원할 수 있다.&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;/b&gt;빈 설정 정보에 소멸 메서드 지정&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Bean(destroyMethod=&amp;rdquo;&amp;rdquo;)을 통해서 초기화 메서드를 지정할 수 있다.&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; 외부 라이브러리를 대부분 close, shutdown이라는 이름으로 종료 메서드를 사용&lt;/b&gt;하고 있다. destroyMethod는 내부적으로 추론 기능이 있기 때문에 스프링 빈으로 등록했다면 종료 메서드를 따로 작성하지 않아도 알아서 동작한다.&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;⭐️ @PreDestroy 사용&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;자바 표준 기술이기 때문에 스프링에 종속적이지 않다.&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;blockquote data-ke-style=&quot;style3&quot;&gt;위의 방법들을 혼용하여 사용한다면, 다음과 같은 순서로 호출된다.&lt;br /&gt;@PreDestroy &amp;rarr; destroy() &amp;rarr; @Bean(destroyMethod=&amp;rdquo;&amp;rdquo;)&lt;/blockquote&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;7. 스프링 컨테이너 종료&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;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;&amp;nbsp;&lt;/h4&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;lsquo;빈이 존재할 수 있는 범위&amp;rsquo;를 의미한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링에서는 총 6가지의 스코프를 제공하며, @Scope 어노테이션을 이용하여 지정할 수 있다.&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;size18&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;&lt;span style=&quot;color: #ef5369;&quot;&gt;스프링 컨테이너가 시작될 때 생성되어서, 종료될 때까지 유지&lt;/span&gt;한다. (가장 넓은 범위)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IoC 컨테이너에서 조회하게 되면 항상 같은 인스턴스의 빈을 반환한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&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;IoC 컨테이너는 빈의 생성과 의존 관계, 초기화까지만 관여하고, 그 이상 관리하지 않는 스코프이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;종료 콜백 메서드는 호출되지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IoC 컨테이너에 조회 시 때 빈을 생성하고, 필요한 의존관계를 주입하기 때문에 매번 새로운 인스턴스를 반환한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;⭐️&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;싱글톤 스코프 with 프로토타입 스코프&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;싱글톤 스코프를 가진 빈은 컨테이너 생성 시점에 생성되고, 의존관계 주입이 발생한다. &lt;br /&gt;만약 주입 시 필요한 빈이 프로토타입 빈이라면, 컨테이너는 이를 받아 새로운 인스턴스를 생성하여 반환한다.&lt;br /&gt;&lt;br /&gt;그러면 싱글톤 빈은 여전히 프로토타입에 대한 빈의 의존 관계를 가지고 있기 때문에, 참조 관계를 보관하고 있다.&lt;br /&gt;단, 과거에 주입이 끝난 상태로 반환받은 것이기 때문에 사용한다고 해서 새롭게 생성되는 것은 아니다.&lt;br /&gt;만약, 싱글톤 빈을 사용할 때 내부적으로 프로토타입 빈을 사용하는 로직을 호출했다면 &lt;span style=&quot;color: #ef5369;&quot;&gt;이미 주입이 끝난 시점이기 때문에 새로운 빈이 아닌 기존의 빈을 계속해서 사용하는 형태로 사용&lt;/span&gt;하게 된다. (=즉, 싱글톤 빈과 생명주기가 동일해진다)&lt;br /&gt;&lt;br /&gt;이를 해결하기 위해서는 &lt;b&gt;ObjectProvder를 사용&lt;/b&gt;할 수 있다. (컨테이너에서 조회하기 때문에 새로운 인스턴스를 생성하기 때문에 프로토타입의 기능을 사용할 수 있게 됨)&lt;/blockquote&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;  request 스코프&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP 요청과 동일한 생명주기는 스코프이다. HTTP 요청마다 인스턴스가 생성되고 관리되는 형태이다.&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;b&gt;  &lt;/b&gt;session 스코프&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Http 세션과 동일한 생명주기를 가지는 스코프이다.&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;b&gt;  &lt;/b&gt;application 스코프&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;과거에는 스프링 컨테이너를 여러 개 띄웠기 때문에 각 컨테이너마다 관리되는 빈을 다르게 하기 위해서 사용했었다. (지금은 하나의 스프링 컨테이너를 사용해서 처리하는 것이 대부분이다.)&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;b&gt;  &lt;/b&gt;websocket 스코프&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 소켓과 동일한 생명주기를 가지는 스코프이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;⭐️ 싱글톤 스코프 with 웹 스코프&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;위의 4가지 경우를 웹 스코프라고 한다.&lt;br /&gt;싱글톤 빈의 경우 스프링 컨테이너와 라이프 사이클이 동일하기 때문에 컨테이너 생성 시 같이 생성되지만, 웹 스코프의 경우 각각의 요청이 들어왔을 때 빈이 생성되기 때문에, &lt;span style=&quot;color: #ef5369;&quot;&gt;만약 싱글톤 빈이 웹 스코프 빈을 의존 관계로 가지고 있을 때 의존 관계 주입이 불가능해진다&lt;/span&gt;.&lt;br /&gt;&lt;br /&gt;이를 위해서 2가지 방법을 사용할 수 있다.&lt;br /&gt;&lt;b&gt;1. ObjectProvider 사용하기&lt;/b&gt;&lt;br /&gt;- getObject()를 호출하는 시점까지 웹 스코프 빈의 생성을 지연시키기&lt;br /&gt;&lt;br /&gt;&lt;b&gt;2. 프록시 사용하기&lt;/b&gt;&lt;br /&gt;- @Scope 어노테이션의 인자로 proxyMode = ScopedProxyMode.TARGET_CLASS 지정하기 (클래스일 경우, 인터페이스라면 INTERFACES 사용)&lt;br /&gt;- 이러면 가짜 프록시 객체를 주입해주기 때문에 상관이 없어진다. 가짜 프록시 객체의 경우, 요청이 왔을 때 진짜 빈으로 요청을 위임한다.&lt;/blockquote&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;style3&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&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;lsquo;싱글톤&amp;rsquo;으로 관리하고 싶은 인스턴스에 대해서만 스프링 빈으로 등록하는 것이 올바른 방법이라고 생각한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, 싱글톤 빈의 경우 항상 같은 인스턴스를 반환받는다. 만약 &lt;span style=&quot;color: #ef5369;&quot;&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;물론, 불가능한 건 아니다&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;blockquote data-ke-style=&quot;style3&quot;&gt;These bean definitions correspond to the actual objects that make up your application. &lt;br /&gt;Typically, you define service layer objects, persistence layer objects such as repositories or data access objects (DAOs), presentation objects such as Web controllers, infrastructure objects such as a JPA EntityManagerFactory, JMS queues, and so forth. &lt;br /&gt;&lt;br /&gt;Typically, one does not configure fine-grained domain objects in the container, because it is usually the responsibility of repositories and business logic to create and load domain objects.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빈에 대한 정의는 애플리케이션을 구성하는 객체들로 이루어져 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 service layer, 레포지토리나 DAO 같은 persistence layer, 컨트롤러 같은 presentation layer, 혹은 infrastructure layer 등에서 사용되는 객체를 정의한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, &lt;span style=&quot;color: #ef5369;&quot;&gt;도메인 객체를 생성하고 로드하는 것은 &amp;lsquo;레파지토리 및 비즈니스 로직의 책임&amp;rsquo;이기 때문에 IoC 컨테이너에서 이를 구성하는 것은 일반적이지 않다&lt;/span&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;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;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  빈으로 등록하는 것 vs 스태틱 메서드를 사용하는 것&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전의 웹 스코프를 설명할 때 말했던 것처럼, 스프링 컨테이너를 여러 개를 띄우는 상황에서 전역적으로 관리하고 싶다면 static method를 사용해서 관리할 수 있을 것 같다. (다만, application scope가 있기 때문에 사실 이마저도 의미있는지 잘 모르겠다.)&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;static 메서드의 경우 인자가 동일하다면 항상 동일한 결과를 반환해야 하는데, 이를 지킬 수 없다면 POJO bean으로 만들라는 &lt;a title=&quot;글&quot; href=&quot;http://kwon37xi.egloos.com/4844149&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;글&lt;/a&gt;이 있었다. &lt;span style=&quot;color: #ef5369;&quot;&gt;외부 자원을 의존하지 않으면서, 항상 동일한 결과만 반환함을 보증&lt;/span&gt;할 수 있다면 (util성 클래스) 굳이 빈으로 등록할 필요는 없을 것 같다.&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;추가적으로, 지금은 크게 상관없지만 static 메서드의 경우 공유 범위가 클래스 로더를 기준으로 하고, 싱글톤 빈의 경우 애플리케이션 컨텍스트를 기준으로 하기 때문에 앞서 말한 것처럼 컨테이너를 여러 개 띄우는 상황이라면 생명주기 차원에서도 static method를 사용할 수 있을 것 같다.&amp;nbsp;&lt;/p&gt;</description>
      <category>Back-end/Spring</category>
      <category>DI</category>
      <category>IocContainer</category>
      <category>spring</category>
      <category>빈스코프</category>
      <category>생성자주입</category>
      <author>dolmeng2</author>
      <guid isPermaLink="true">https://cl8d.tistory.com/81</guid>
      <comments>https://cl8d.tistory.com/81#entry81comment</comments>
      <pubDate>Sun, 7 May 2023 18:19:40 +0900</pubDate>
    </item>
  </channel>
</rss>