<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>mandy</title>
    <link>https://mandykr.tistory.com/</link>
    <description>java, spring, jpa, clean code, test, aws 등 공부내용을 정리하는 용도입니다.</description>
    <language>ko</language>
    <pubDate>Tue, 14 Jul 2026 02:14:32 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>mandykr</managingEditor>
    <item>
      <title>[MySQL] 통계 정보</title>
      <link>https://mandykr.tistory.com/109</link>
      <description>&lt;p 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;1. 테이블 및 인덱스 통계 정보&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;2. 히스토그램&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; 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;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;MySQL 8.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;h2 data-ke-size=&quot;size26&quot;&gt;1. 테이블 및 인덱스 통계 정보&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비용 기반 최적화에서 가장 중요한 것은 통계 정보이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;(&lt;b&gt;규칙 기반 최적화 &lt;/b&gt;vs &lt;b&gt;비용 기반 최적화&lt;/b&gt;)&lt;/b&gt;&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;규칙 기반 최적화(RBO)는 기본적으로 대상 테이블의 레코드 건수나 선택도 등을 고려하지 않고 옵티마이저에 내장된 우선순위에 따라 실행 계획을 수립하는 방식이다. 통계 정보를 조사하지 않고 실행 계획이 수립되기 때문에 같은 쿼리에 대해서는 항상 거의 같은 실행 방법을 만들어 낸다. 오래전부터 사용하지 않는 방식이다.&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;비용 기반 최적화(CBO)는 쿼리를 처리하기 위한 여러가지 가능한 방법을 만들고, 각 단위 작업의 비용 정보와 대상 테이블의 예측된 통계 정보를 이용해 실행 계획별 비용을 산출한다. 이렇게 산출된 실행 방법별로 비용이 최소로 소요되는 처리 방식을 선택해 최종적으로 쿼리를 실행한다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;통계 정보 관리 테이블&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL 5.6 버전부터 각 테이블의 통계 정보를 mysql 데이터베이스의 테이블로 관리할 수 있게 개선됐다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;innodb_index_stats&lt;/li&gt;
&lt;li&gt;innodb_table_stats&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;● innodb_index_stats&lt;/b&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 13.2558%;&quot;&gt;&lt;b&gt;index_name&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 15.6977%;&quot;&gt;&lt;b&gt;stat_name&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 15.1163%;&quot;&gt;&lt;b&gt;stat_value&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 15.3488%;&quot;&gt;&lt;b&gt;sample_size&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 40.5814%;&quot;&gt;&lt;b&gt;stat_description&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 13.2558%;&quot;&gt;PRIMARY&lt;/td&gt;
&lt;td style=&quot;width: 15.6977%;&quot;&gt;n_diff_pfx01&lt;/td&gt;
&lt;td style=&quot;width: 15.1163%;&quot;&gt;299202&lt;/td&gt;
&lt;td style=&quot;width: 15.3488%;&quot;&gt;20&lt;/td&gt;
&lt;td style=&quot;width: 40.5814%;&quot;&gt;emp_no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 13.2558%;&quot;&gt;PRIMARY&lt;/td&gt;
&lt;td style=&quot;width: 15.6977%;&quot;&gt;n_leaf_pages&lt;/td&gt;
&lt;td style=&quot;width: 15.1163%;&quot;&gt;886&lt;/td&gt;
&lt;td style=&quot;width: 15.3488%;&quot;&gt;NULL&lt;/td&gt;
&lt;td style=&quot;width: 40.5814%;&quot;&gt;Number of leaf pages in the index&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 13.2558%;&quot;&gt;PRIMARY&lt;/td&gt;
&lt;td style=&quot;width: 15.6977%;&quot;&gt;size&lt;/td&gt;
&lt;td style=&quot;width: 15.1163%;&quot;&gt;929&lt;/td&gt;
&lt;td style=&quot;width: 15.3488%;&quot;&gt;NULL&lt;/td&gt;
&lt;td style=&quot;width: 40.5814%;&quot;&gt;Number of pages in the index&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;stat_name='n_diff_pfx%' : 인덱스가 가진 유니크한 값의 개수&lt;/li&gt;
&lt;li&gt;stat_name='n_leaf_pages' : 인덱스의 리프 노드 페이지 개수&lt;/li&gt;
&lt;li&gt;stat_name='size' : 인덱스 트리의 전체 페이지 개수&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;●&lt;b&gt; innodb_index_stats&lt;/b&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;b&gt;n_rows&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;b&gt;clustered_index_size&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;b&gt;sum_of_other_index_sizes&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;299202&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;929&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;642&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;n_rows : 테이블의 전체 레코드 건수&lt;/li&gt;
&lt;li&gt;clustered_index_size : 프라이머리 키의 크기 (innoDB 페이지 개수)&lt;/li&gt;
&lt;li&gt;sum_of_other_index_sizes : 프라이머리 키를 제외한 인덱스의 크기 (innoDB 페이지 개수)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;통계 정보 영구 보관&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테이블을 생성할 때 STATS_PERSISTENT 옵션으로 통계 정보를 보관을 할지 설정할 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;STATS_PERSISTENT=0 : 통계 정보 관리 테이블에 저장하지 않음&lt;/li&gt;
&lt;li&gt;STATS_PERSISTENT=1 : 통계 정보 관리 테이블에 저장함&lt;/li&gt;
&lt;li&gt;STATS_PERSISTENT=DEFAULT : innodb_stats_persistent 시스템 변수의 값으로 결정&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;innodb_stats_persistent 시스템 설정으로 통계 정보를 영구적 또는 단기적으로 보관하도록 설정할 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;innodb_stats_persistent=0 : 통계 정보를 단기적으로 보관&lt;/li&gt;
&lt;li&gt;innodb_stats_persistent=1&amp;nbsp;: 통계 정보를 영구적으로 보관&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;통계 정보 갱신&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음의 경우에 통계 정보가 갱신된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;테이블이 새로 오픈되는 경우&lt;/li&gt;
&lt;li&gt;테이블의 레코드가 대량으로 변경되는 경우(테이블 레코드의 1/6 정도가 INSERT / UPDATE / DELETE)&lt;/li&gt;
&lt;li&gt;ANALYZE TABLE 명령이 실행되는 경우&lt;/li&gt;
&lt;li&gt;SHOW TABLE STATUS 명령이나 SHOW INDEX FROM 명령이 실행되는 경우&lt;/li&gt;
&lt;li&gt;innoDB 모니터가 활성화되는 경우&lt;/li&gt;
&lt;li&gt;innodb_stats_on_metadata 시스템 설정이 ON인 상태에서 SHOW TABLE STATUS 명령이 실행되는 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;통계 정보 자동 갱신&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;통계 정보가 자동으로 자주 갱신되면 쿼리가 의도와 다르게 동작할 수 있기 때문에 innodb_stats_auto_recalc 변수의 값을 OFF로 설정하면 자동 갱신을 막을 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;STATS_AUTO_RECALC=1 : 테이블의 통계 정보를 자동 수집&lt;/li&gt;
&lt;li&gt;STATS_AUTO_RECALC=0 : 테이블의 통계 정보는 ANALYZE TABLE 명령을 실행할 때만 수집&lt;/li&gt;
&lt;li&gt;STATS_AUTO_RECALC=DEFAULT : 테이블의 통계 정보 수집을 innodb_stats_auto_recalc 변수의 값으로 결정&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;통계 정보 수집시 페이지 샘플링&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테이블의 통계 정보를 수집할 때 몇 개의 innoDB 테이블 블록을 샘플링 할지 설정할 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;innodb_stats_transient_sample_pages : 기본값 8, 자동으로 통계 정보 수집이 실행될 때 임의로 8개의 페이지만 샘플링해서 분석하고 통계 정보로 활용&lt;/li&gt;
&lt;li&gt;innodb_stats_persistent_sample_pages : 기본값 20, ANALYZE TABLE 명령이 실행되면 임의로 20개의 페이지만 샘플링해서 분석하고 통계 정보로 활용&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 히스토그램&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL 8.0 부터 실행 계획을 수립할 때 통계 정보와 함께 컬럼의 데이터 분포도를 참조할 수 있는 히스토그램 정보를 활용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;히스토그램 정보 수집&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;히스토그램 정보는 ANALYZE TABLE ... UPDATE HISTOGRAM 명령을 이용해 수동으로 수집하며 시스템 딕셔너리에 컬럼 단위로 저장되어 관리된다. 또한, MySQL 서버가 시작될 때 이 정보를 information_schema 데이터베이스의 column_statistics 테이블로 로드하기 때문에 이 테이블에서 참조할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1688614218979&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ANALYZE TABLE employees.employees
UPDATE HISTOGRAM ON gender, hire_date;

SELECT *
FROM COLUMN_STATISTICS
WHERE SCHEMA_NAME = 'employees'
AND TABLE_NAME = 'employees'\G;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;히스토그램 종류&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;● &lt;/b&gt;Singleton(싱글톤 히스토그램) : 컬럼값 개별로 레코드 건수를 관리. (Value-Based 히스토그램 또는 도수 분포)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;810&quot; data-origin-height=&quot;465&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CP0EK/btsmzb7XZ56/0pSgBw6PVwek3DDVDJDcF0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CP0EK/btsmzb7XZ56/0pSgBw6PVwek3DDVDJDcF0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CP0EK/btsmzb7XZ56/0pSgBw6PVwek3DDVDJDcF0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCP0EK%2Fbtsmzb7XZ56%2F0pSgBw6PVwek3DDVDJDcF0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;514&quot; height=&quot;295&quot; data-origin-width=&quot;810&quot; data-origin-height=&quot;465&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;누적된 레코드 건수의 비율을 갖기 때문에 gender 가 'F'인 레코드의 비율은 (1 - 'M'의 비율) 이다.&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;● &lt;/b&gt;Equi-Height(높이 균형 히스토그램) : 컬럼값의 범위를 균등한 개수로 구분해서 관리. (Height-Balanced 히스토그램)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;830&quot; data-origin-height=&quot;390&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qu690/btsmzGNq9xs/aUoTOQEkgSjxvO9AJKf8r0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qu690/btsmzGNq9xs/aUoTOQEkgSjxvO9AJKf8r0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qu690/btsmzGNq9xs/aUoTOQEkgSjxvO9AJKf8r0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fqu690%2FbtsmzGNq9xs%2FaUoTOQEkgSjxvO9AJKf8r0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;514&quot; height=&quot;242&quot; data-origin-width=&quot;830&quot; data-origin-height=&quot;390&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;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;히스토그램 정보 삭제&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;히스토그램의 삭제 작업은 딕셔너리의 내용만 삭제하기 때문에 다른 쿼리 처리의 성능에 영향을 주지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;히스토그램이 삭제되면 쿼리의 실행 계획이 달라질 수 있기 때문에 주의해야 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1688615035820&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ANALYZE TABLE employees.employees
DROP HISTOGRAM ON gender, hire_date;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;히스토그램의 용도&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 응용 프로그램의 데이터는 항상 균등한 분포도를 가지지 않기 때문에 테이블과 인덱스의 통계 정보만으로는 부족하다. 히스토그램은 특정 컬럼이 가지는 모든 값에 대한 분포도 정보를 가지지는 않지만 각 범위(버킷)별로 레코드의 건수와 유니크한 값의 개수 정보를 가지기 때문에 훨씬 정확한 예측을 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;히스토그램과 인덱스&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL 8.0에서 인덱스된 컬럼을 검색 조건으로 사용하는 경우 그 컬럼의 히스토그램은 사용하지 않고 실제 인덱스 다이브를 통해 직접 수집한 정보를 활용한다. 따라서 히스토그램은 주로 인덱스되지 않은 컬럼에 대한 데이터 분포도를 참조하는 용도로 사용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;(인덱스 다이브)&lt;/b&gt;&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;옵티마이저는 쿼리의 실행 계획을 수립할 때 사용 가능한 인덱스들로부터 조건절에 일치하는 건수를 예측하기 위해 실제 인덱스의 B-Tree를 샘플링해서 살펴본다. 이 작업을 인덱스 다이브라고 하며 항상 히스토그램보다 정확한 결과를 기대할 수 있다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 코스트 모델&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL 서버는 쿼리를 처리하는데 다음과 같은 다양한 작업들이 얼마나 필요한지 예측하고 전체 작업의 비용을 계산해 최적의 실행 계획을 찾는다. 이 때 단위 작업들의 비용을 코스트 모델이라고 한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;디스크로부터 데이터 페이지 읽기&lt;/li&gt;
&lt;li&gt;메모리(InnoDB 버퍼 풀)로부터 데이터 페이지 읽기&lt;/li&gt;
&lt;li&gt;인덱스 키 비교&lt;/li&gt;
&lt;li&gt;레코드 평가&lt;/li&gt;
&lt;li&gt;메모리 임시 테이블 작업&lt;/li&gt;
&lt;li&gt;디스크 임시 테이블 작업&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;코스트 모델 테이블&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL 8.0 서버의 코스트 모델은 mysql DB에 존재하는 다음 2개 테이블에 저장돼 있는 설정값을 사용한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;server_cost : 인덱스를 찾고 레코드를 비교하고 임시 테이블 처리에 대한 비용 관리&lt;/li&gt;
&lt;li&gt;engine_cost : 레코드를 가진 데이터 페이지를 가져오는 데 필요한 비용 관리&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;b&gt;●&lt;/b&gt;&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;server_cost&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;cost_name : 코스트 모델의 각 단위 작업&lt;/li&gt;
&lt;li&gt;default_value : 각 단위 작업의 비용(기본값이며 MySQL 서버 소스 코드에 설정된 값)&lt;/li&gt;
&lt;li&gt;cost_value : DBMS 관리자가 설정한 값(NULL이면 default_value 컬럼의 비용 사용)&lt;/li&gt;
&lt;li&gt;last_update : 단위 작업의 비용이 변경된 시점&lt;/li&gt;
&lt;li&gt;comment : 비용에 대한 추가 설명&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;b&gt;●&lt;/b&gt;&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;engine_cost&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;engine_cost 테이블은 위의 5개 컬럼에 추가로 2개의 컬럼을 더 가진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 테이블을 이용해 스토리지 엔진별로 단위 작업의 비용을 달리 설정할 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;engine_name : 비용이 적용된 스토리지 엔진&lt;/li&gt;
&lt;li&gt;device_type : 디스크 타입&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;코스트 모델 작업 단위&lt;/b&gt;&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 180px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 14.186%; height: 20px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 31.6279%; height: 20px;&quot;&gt;&lt;b&gt;cost_name&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 18.3721%; height: 20px;&quot;&gt;&lt;b&gt;default_value&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 35.814%; height: 20px;&quot;&gt;&lt;b&gt;설명&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 14.186%; height: 120px;&quot; rowspan=&quot;6&quot;&gt;&lt;b&gt;server_cost&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 31.6279%; height: 20px;&quot;&gt;disk_temptable_create_cost&lt;/td&gt;
&lt;td style=&quot;width: 18.3721%; height: 20px;&quot;&gt;20.00&lt;/td&gt;
&lt;td style=&quot;width: 35.814%; height: 20px;&quot;&gt;디스크 임시 테이블 생성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 31.6279%; height: 20px;&quot;&gt;disk_temptable_row_cost&lt;/td&gt;
&lt;td style=&quot;width: 18.3721%; height: 20px;&quot;&gt;0.50&lt;/td&gt;
&lt;td style=&quot;width: 35.814%; height: 20px;&quot;&gt;디스크 임시 테이블의 레코드 읽기&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 31.6279%; height: 20px;&quot;&gt;key_compare_cost&lt;/td&gt;
&lt;td style=&quot;width: 18.3721%; height: 20px;&quot;&gt;0.05&lt;/td&gt;
&lt;td style=&quot;width: 35.814%; height: 20px;&quot;&gt;인덱스 키 비교&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 31.6279%; height: 20px;&quot;&gt;memory_temptable_create_cost&lt;/td&gt;
&lt;td style=&quot;width: 18.3721%; height: 20px;&quot;&gt;1.00&lt;/td&gt;
&lt;td style=&quot;width: 35.814%; height: 20px;&quot;&gt;메모리 임시 테이블 생성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 31.6279%; height: 20px;&quot;&gt;memory_temptable_row_cost&lt;/td&gt;
&lt;td style=&quot;width: 18.3721%; height: 20px;&quot;&gt;0.10&lt;/td&gt;
&lt;td style=&quot;width: 35.814%; height: 20px;&quot;&gt;메모리 임시 테이블의 레코드 읽기&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 31.6279%; height: 20px;&quot;&gt;row_evaluate_cost&lt;/td&gt;
&lt;td style=&quot;width: 18.3721%; height: 20px;&quot;&gt;0.10&lt;/td&gt;
&lt;td style=&quot;width: 35.814%; height: 20px;&quot;&gt;레코드 비교&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 14.186%; height: 40px;&quot; rowspan=&quot;2&quot;&gt;&lt;b&gt;engine_cost&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 31.6279%; height: 20px;&quot;&gt;io_block_read_cost&lt;/td&gt;
&lt;td style=&quot;width: 18.3721%; height: 20px;&quot;&gt;1.00&lt;/td&gt;
&lt;td style=&quot;width: 35.814%; height: 20px;&quot;&gt;디스크 데이터 페이지 읽기&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 31.6279%; height: 20px;&quot;&gt;memory_block_read_cost&lt;/td&gt;
&lt;td style=&quot;width: 18.3721%; height: 20px;&quot;&gt;0.25&lt;/td&gt;
&lt;td style=&quot;width: 35.814%; height: 20px;&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;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;코스트 모델 작업 단위의 비용 조절 예&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: circle; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;b&gt;key_compare_cost&lt;/b&gt; 비용을 높이면,&lt;br /&gt;옵티마이저가 가능하면 정렬을 수행하지 않는 방향의 실행 계획을 선택할 가능성이 높아진다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;row_evaluate_cost&lt;/b&gt; 비용을 높이면,&lt;br /&gt;풀 스캔을 실행하는 쿼리들의 비용이 높아지고, 옵티마이저는 가능하면 인덱스 레인지 스캔을 사용하는 실행 계획을 선택할 가능성이 높아진다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;disk_temptable_create_cost와 disk_temptable_row_cost&lt;/b&gt; 비용을 높이면,&lt;br /&gt;옵티마이저는 디스크에 임시 테이블을 만들지 않는 방향의 실행 계획을 선택할 가능성이 높아진다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;memory_temptable_create_cost 와 memory_temptable_row_cost&lt;/b&gt; 비용을 높이면,&lt;br /&gt;옵티마이저는 메모리 임시 테이블을 만들지 않는 방향의 실행 계획을 선택할 가능성이 높아진다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;io_blok_read_cost&lt;/b&gt; 비용이 높아지면,&lt;br /&gt;옵티마이저는 가능하면 InnoDB 버퍼 풀에 데이터 페이지가 많이 적재돼 있는 인덱스를 사용하는 실행 계획을 선택할 가능성이 높아진다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;memory_block_read_cost&lt;/b&gt; 비용이 높아지면,&lt;br /&gt;MySQL 서버는 InnoDB 버퍼 풀에 적재된 데이터 페이지가 상대적으로 적다고 하더라도 그 인덱스를 사용할 가능성이 높아진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a style=&quot;color: #3db39e;&quot; href=&quot;https://product.kyobobook.co.kr/detail/S000001766482&quot;&gt;Real MySQL 8.0 (1권)&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Database/MySQL</category>
      <category>MySQL</category>
      <author>mandykr</author>
      <guid isPermaLink="true">https://mandykr.tistory.com/109</guid>
      <comments>https://mandykr.tistory.com/109#entry109comment</comments>
      <pubDate>Fri, 2 Jun 2023 14:37:19 +0900</pubDate>
    </item>
    <item>
      <title>[MySQL] 인덱스</title>
      <link>https://mandykr.tistory.com/108</link>
      <description>&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;1. 랜덤 I/O와 순차 I/O 디스크 읽기&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;2. 인덱스&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;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;1. 랜덤 I/O와 순차 I/O 디스크 읽기&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;랜덤 I/O는 하드 디스크 드라이브의 플래터를 돌려서 읽어야 할 데이터가 저장된 위치로 디스크 헤더를 이동시킨 다음 데이터를 읽는 것을 의미한다.&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;3개의 페이지를 디스크에 기록하기 위해 순차 I/O는 1번의 시스템 콜로 디스크의 헤더를 1번 움직이고,&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;랜덤 I/O는 3번의 시스템 콜로 디스크 헤드를 3번 움직인다. 디스크의 성능은 디스크 헤더의 위치 이동없이 얼마나 많은 데이터를 한 번에 기록하느냐에 의해 결정되기 때문에 랜덤 I/O 작업이 부하가 훨씬 크다.&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;테이블의 레코드 대부분을 읽는 작업에서는 인덱스 레인지 스캔(랜덤 I/O)이 아닌 풀 테이블 스캔(순차 I/O)을 사용하도록 유도할 수 있지만 그 외에는 랜덤 I/O를 순차 I/O로 바꿔서 실행할 방법은 많지 않다.&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;background-color: #f6e199;&quot;&gt;쿼리를 튜닝하는 것은 랜덤 I/O 자체를 줄이는 것으로 쿼리를 처리하는 데 꼭 필요한 데이터만 읽도록 쿼리를 개선하는 것을 의미한다.&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;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;2. 인덱스&lt;/h2&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;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;background-color: #f6e199;&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;/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;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;(1) B-Tree 인덱스&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;데이터베이스 인덱싱 알고리즘 가운데 가장 많이 사용되는 B-Tree(Balanced Tree) 인덱스는 컬럼의 원래 값을 변형시키지 않고 인덱스 구조체 내에서 항상 정렬된 상태를 유지한다.&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1168&quot; data-origin-height=&quot;853&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/8vcam/btsisM3FJiG/AzRNUkY1SCnkU6jQEykgR0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/8vcam/btsisM3FJiG/AzRNUkY1SCnkU6jQEykgR0/img.jpg&quot; data-alt=&quot;출처:&amp;amp;amp;nbsp;https://velog.io/@evelyn82ny/B-Tree-index-feat-difference-from-B-plus-Tree&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/8vcam/btsisM3FJiG/AzRNUkY1SCnkU6jQEykgR0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F8vcam%2FbtsisM3FJiG%2FAzRNUkY1SCnkU6jQEykgR0%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;1168&quot; height=&quot;853&quot; data-origin-width=&quot;1168&quot; data-origin-height=&quot;853&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처:&amp;amp;nbsp;https://velog.io/@evelyn82ny/B-Tree-index-feat-difference-from-B-plus-Tree&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;B-Tree 의 구조는 루트 노드, 브랜치 노드, 리프 노드로 이루어져 있고 가장 하위의 리프 노드는 항상 실제 데이터 레코드를 찾아가기 위한 주솟값을 가지고 있다. (MyISAM 테이블은 세컨더리 인덱스가 물리적인 주소를 가지는 반면 InnoDB 테이블은 프라이머리 키를 주소처럼 사용한다.)&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;대부분의 RDBMS의 데이터 파일에서 레코드는 특정 기준으로 정렬되지 않고 임의의 순서로 저장된다. 하지만&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;InnoDB 테이블에서 레코드는 클러스터되어 디스크에 저장되므로 기본적으로 프라이머리 키 순서로 정렬되어 저장된다.&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;B-Tree 인덱스와 클러스터링 인덱스&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;1109&quot; data-origin-height=&quot;531&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bIsuGX/btsiswzYkDl/dVtVivMo99pJMcxPOMZ7gk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bIsuGX/btsiswzYkDl/dVtVivMo99pJMcxPOMZ7gk/img.png&quot; data-alt=&quot;출처:&amp;amp;amp;nbsp;https://velog.io/@evelyn82ny/B-Tree-index-feat-difference-from-B-plus-Tree&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bIsuGX/btsiswzYkDl/dVtVivMo99pJMcxPOMZ7gk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbIsuGX%2FbtsiswzYkDl%2FdVtVivMo99pJMcxPOMZ7gk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1109&quot; height=&quot;531&quot; data-origin-width=&quot;1109&quot; data-origin-height=&quot;531&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처:&amp;amp;nbsp;https://velog.io/@evelyn82ny/B-Tree-index-feat-difference-from-B-plus-Tree&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;InnoDB 테이블에서 인덱스를 통해 레코드를 읽을 때는 인덱스에 저장돼 있는 프라이머리 키 값을 이용해 프라이머리 키 인덱스를 한번 더 검색한 후, 프라이머리 키 인덱스의 리프 페이지에 저장돼 있는 레코드를 읽는다. 즉,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;InnoDB 스토리지 엔진에서는 모든 세컨더리 인덱스 검색에서 데이터 레코드를 읽기 위해서는 반드시 클러스터링 인덱스를 한번 더 검색해야 한다.&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;레코드를 추가하면 B-Tree에 인덱스 키 추가 작업이 발생한다. 이 때 저장될 키 값을 이용해 B-Tree 상의 적절한 위치를 검색해야 한다. 저장될 위치가 결정되면 레코드의 키 값과 대상 레코드의 주소 정보를 B-Tree의 리프 노드에 저장한다. 리프 노드가 꽉 차서 더는 저장할 수 없을 때는 리프 노드가 분리돼야 하는데, 이는 상위 브랜치 노드까지 처리의 범위가 넓어진다. 이러한 작업 탓에 B-Tree는 상대적으로 쓰기 작업에 비용이 많이 든다.&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;/p&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;InnoDB는 변경해야 할 인덱스 페이지가 버퍼 풀에 있으면 바로 업데이트를 수행하지만 디스크로부터 읽어와서 업데이트 해야 한다면 이를 즉시 실행하지 않고 임시 공간에 저장해 두고 바로 사용자에게 결과를 반환하는 형태로 성능을 향상시킨다. 이때 사용하는 임시 메모리 공간을 체인지 버퍼라고 한다.&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;/div&gt;
&lt;/div&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;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;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;레코드 삭제로 B-Tree 인덱스 키 값이 삭제될때는 해당 키 값이 저장된 리프 노드를 찾아 삭제 마크하는 작업만 발생한다. 이렇게 삭제 마킹된 인덱스 키 공간은 계속 그대로 방치되거나 재활용할 수 있다. 이 작업 역시 디스크 I/O가 필요한 작업이다.&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;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;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;인덱스 키 값은 그 값에 따라 저장될 리프 노드의 위치가 결정되므로 B-Tree의 키 값이 변경되는 경우 기존 인덱스 키 값을 삭제한 후 새로운 인덱스 키 값을 추가하는 작업으로 처리되고, InnoDB 테이블은 이 작업 모두 체인지 버퍼를 활용해 지연 처리될 수 있다.&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;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;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;B-Tree 인덱스를 이용한 검색은 100% 일치, 값의 앞부분만 일치하는 경우, 부등호(&amp;lt;, &amp;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;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인덱스를 구성하는 키 값의 뒷부분만 검색하는 용도&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1685684049614&quot; class=&quot;sql&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT * FROM CUSTOMER WHERE NAME LIKE '%A';&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;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;함수나 연산을 수행한 결과로 정렬하거나 검색하는 작업&lt;br /&gt;(인덱스의 키 값에 변형이 가해진 후 비교되는 경우 변형된 값은 B-Tree 인덱스에 존재하는 값이 아니다.)&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1685684049616&quot; class=&quot;pgsql&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT * FROM EMP WHERE TO_CHAR(hire_date, 'YYYY') = '2021';&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;&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;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;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;MySQL 인덱스는 B-Tree 계열(B+Tree, B*Tree) 자료구조를 사용한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;B-Tree는 이진 탐색 트리(binary search tree, BST)의 일반화된 형태이다. 이진 탐색 트리가 자식 노드가 2개 이하인 트리라면, B-Tree는 자식 노드가 2개 이상인 트리이다. 즉, 노드의 개수를 늘리고 트리의 전체 높이를 줄여서 빠른 탐색 속도를 얻을 수 있다. 최대 M개의 자식을 가질 수 있는 B-Tree를 M차 B-Tree라고 한다.&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;680&quot; data-origin-height=&quot;380&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eBHoIf/btsol8gfIf6/gAuLQ2FbTVygkG3RlnrQSk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eBHoIf/btsol8gfIf6/gAuLQ2FbTVygkG3RlnrQSk/img.png&quot; data-alt=&quot;출처:&amp;amp;nbsp;https://hudi.blog/db-index-and-indexing-algorithms/&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eBHoIf/btsol8gfIf6/gAuLQ2FbTVygkG3RlnrQSk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeBHoIf%2Fbtsol8gfIf6%2FgAuLQ2FbTVygkG3RlnrQSk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;380&quot; data-origin-width=&quot;680&quot; data-origin-height=&quot;380&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처:&amp;nbsp;https://hudi.blog/db-index-and-indexing-algorithms/&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #191919; text-align: start;&quot;&gt;B-Tree는 정렬된 트리이므로 탐색이&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #191919; text-align: start;&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;&lt;span&gt;&lt;span&gt;O&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;l&lt;/span&gt;&lt;span&gt;o&lt;/span&gt;&lt;span&gt;g&lt;/span&gt;&lt;span&gt;N&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #191919; text-align: start;&quot;&gt;의 시간 복잡도를 갖는다. 28이라는 숫자를 찾는데 고작&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;4번&lt;/b&gt;&lt;span style=&quot;background-color: #ffffff; color: #191919; text-align: start;&quot;&gt;의 탐색을 거친다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;B+Tree&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 DBMS에서는 B+Tree를 사용하는데 B+Tree는 리프 노드가 연결리스트 형태를 가진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부등호 연산에서 B-Tree는 트리 순회를 하지만 B+Tree는 리프 노드를 순차 검색할 수 있어 B-Tree에 비해 빠르다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;(2) 클러스터링 인덱스&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;MySQL에서 InnoDB 엔진만 지원하는 기능으로 프라이머리 키 값이 비슷한 레코드끼리 묶어서 저장하는 것을 의미한다.&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;background-color: #f6e199;&quot;&gt;테이블 자체가 클러스터링 되기 때문에 &quot;클러스터링 테이블&quot;과 동의어로 사용한다.&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;&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;839&quot; data-origin-height=&quot;686&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b2omgT/btsisxr6xaN/sKDZD4kXJsHkVpyFxPeU3k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b2omgT/btsisxr6xaN/sKDZD4kXJsHkVpyFxPeU3k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b2omgT/btsisxr6xaN/sKDZD4kXJsHkVpyFxPeU3k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb2omgT%2Fbtsisxr6xaN%2FsKDZD4kXJsHkVpyFxPeU3k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;544&quot; data-origin-width=&quot;839&quot; data-origin-height=&quot;686&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;b&gt;&lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;(테이블에 프라이머리 키가 없는 경우)&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;InnoDB 엔진이 다음 우선순위대로 대체할 컬럼을 클러스터링 키로 선택한다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;프라이머리 키가 있으면 기본적으로 프라이머리 키를 선택&lt;/li&gt;
&lt;li&gt;NOT NULL 옵션의 유니크 인덱스 중에서 첫 번째 인덱스를 선택&lt;/li&gt;
&lt;li&gt;자동으로 유니크한 값을 가지도록 증가되는 컬럼을 내부적으로 추가한 후 선택&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;/div&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;InnoDB 테이블의 모든 세컨더리 인덱스는 해당 레코드의 주소가 아니라 프라이머리 키 값을 저장한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1109&quot; data-origin-height=&quot;531&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/csALpQ/btsisq0T4QS/E3kVfVx9adJm2sFpfjHZ0k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/csALpQ/btsisq0T4QS/E3kVfVx9adJm2sFpfjHZ0k/img.png&quot; data-alt=&quot;출처:&amp;amp;amp;nbsp;https://velog.io/@evelyn82ny/B-Tree-index-feat-difference-from-B-plus-Tree&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/csALpQ/btsisq0T4QS/E3kVfVx9adJm2sFpfjHZ0k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcsALpQ%2Fbtsisq0T4QS%2FE3kVfVx9adJm2sFpfjHZ0k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1109&quot; height=&quot;531&quot; data-origin-width=&quot;1109&quot; data-origin-height=&quot;531&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처:&amp;amp;nbsp;https://velog.io/@evelyn82ny/B-Tree-index-feat-difference-from-B-plus-Tree&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;따라서 클러스터링 키 값이 변경되면 레코드의 주소가 변경되는데 이때, 모든 세컨더리 인덱스가 업데이트 되는 오버헤드가 발생하지 않는다.&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;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;클러스터링 인덱스 키의 크기&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;클러스터링 테이블의 모든 세컨더리 인덱스가 PK 값을 포함하기 때문에 PK의 크기가 커지면 세컨더리 인덱스도 자동으로 크기가 커진다.&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;5개의 세컨더리 인덱스를 가지는 테이블의 경우&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;PK 크기가 커질수록 인덱스가 성능을 내기위해 필요한 메모리가 증가한다.&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 54px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 13.9147%; text-align: left; height: 20px;&quot;&gt;&lt;b&gt;PK 크기&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.217%; text-align: left; height: 20px;&quot;&gt;&lt;b&gt;레코드당 증가하는 인덱스 크기&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 52.8682%; text-align: left; height: 20px;&quot;&gt;&lt;b&gt;100만 건 레코드 저장 시 증가하는 인덱스 크기&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 13.9147%; height: 17px;&quot;&gt;10B&lt;/td&gt;
&lt;td style=&quot;width: 33.217%; height: 17px;&quot;&gt;10B * 5 = 50B&lt;/td&gt;
&lt;td style=&quot;width: 52.8682%; height: 17px;&quot;&gt;50B * 1,000,000 = 47MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 13.9147%; height: 17px;&quot;&gt;50B&lt;/td&gt;
&lt;td style=&quot;width: 33.217%; height: 17px;&quot;&gt;50B * 5 = 250B&lt;/td&gt;
&lt;td style=&quot;width: 52.8682%; height: 17px;&quot;&gt;250B * 1,000,000 = 238MB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;PK 는 메모리를 고려해 Auto-Increment 보다는 업무적인 컬럼으로 생성하는 것을 권장한다.&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;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;(3) 유니크 인덱스&lt;/h3&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;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;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;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;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;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;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;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a style=&quot;color: #3db39e;&quot; href=&quot;https://product.kyobobook.co.kr/detail/S000001766482&quot;&gt;Real MySQL 8.0 (1권)&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;&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>Database/MySQL</category>
      <category>MySQL</category>
      <author>mandykr</author>
      <guid isPermaLink="true">https://mandykr.tistory.com/108</guid>
      <comments>https://mandykr.tistory.com/108#entry108comment</comments>
      <pubDate>Fri, 2 Jun 2023 14:34:48 +0900</pubDate>
    </item>
    <item>
      <title>[MySQL] MySQL의 격리 수준</title>
      <link>https://mandykr.tistory.com/107</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;목차&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;1. READ UNCOMMITTED&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;2. READ COMMITTED&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;3. REPEATABLE READ&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;4. SERIALIZABLE&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;MySQL의 격리 수준&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;트랜잭션의 격리 수준이란 여러 트랜잭션이 동시에 처리될 때 특정 트랜잭션이 다른 트랜잭션에서 변경하거나 조회하는 데이터를 볼 수 있게 허용할지 말지를 결정하는 것이다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;1. READ UNCOMMITTED&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;각 트랜잭션에서의 변경 내용이 COMMIT이나 ROLLBACK 여부에 상관없이 다른 트랜잭션에서 조회할 수 있다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;어떤 트랜잭션에서 처리한 작업이 완료되지 않았는데도 다른 트랜잭션에서 볼 수 있는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;Dirty read 가 허용된다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #666666; 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;929&quot; data-origin-height=&quot;873&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bg0CcX/btsikTjhAdv/LoOvDYujLbPw3DVZHRD0K1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bg0CcX/btsikTjhAdv/LoOvDYujLbPw3DVZHRD0K1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bg0CcX/btsikTjhAdv/LoOvDYujLbPw3DVZHRD0K1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbg0CcX%2FbtsikTjhAdv%2FLoOvDYujLbPw3DVZHRD0K1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;517&quot; data-origin-width=&quot;929&quot; data-origin-height=&quot;873&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;2. READ COMMITTED&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;어떤 트랜잭션에서 데이터를 변경했더라도 COMMIT이 완료된 데이터만 다른 트랜잭션에서 조회할 수 있다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;●&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;Dirty read 가 발생하지 않는다.&lt;/span&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;953&quot; data-origin-height=&quot;839&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c56yiA/btsimGc82jV/xH0AZeHUNMnt2gLZeeFmFk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c56yiA/btsimGc82jV/xH0AZeHUNMnt2gLZeeFmFk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c56yiA/btsimGc82jV/xH0AZeHUNMnt2gLZeeFmFk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc56yiA%2FbtsimGc82jV%2FxH0AZeHUNMnt2gLZeeFmFk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;484&quot; data-origin-width=&quot;953&quot; data-origin-height=&quot;839&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;새로운 값인 &quot;Toto&quot; 는 테이블에 기록되고 이전 값인 &quot;Lara&quot;는 언두 영역으로 백업한다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;사용자 B의 조회 결과는 언두 영역에 백업된 레코드(&quot;Lara&quot;)가 반환된다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;●&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;NON-REPEATABLE READ 라는 부정합 문제가 발생할 수 있다.&lt;/span&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;947&quot; data-origin-height=&quot;769&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/n9q8I/btsikUCApsF/lM1msg6kGe1HI0YXrEjwX1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/n9q8I/btsikUCApsF/lM1msg6kGe1HI0YXrEjwX1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/n9q8I/btsikUCApsF/lM1msg6kGe1HI0YXrEjwX1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fn9q8I%2FbtsikUCApsF%2FlM1msg6kGe1HI0YXrEjwX1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;447&quot; data-origin-width=&quot;947&quot; data-origin-height=&quot;769&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;사용자 B가 하나의 트랜잭션 내에서 똑같은 SELECT 쿼리를 실행했을 때는 항상 같은 결과를 가져와야 한다는 REPEATABLE READ 에 어긋난다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;3. REPEATABLE READ&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;InnoDB 엔진의 기본 격리 수준이다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;MVCC 를 이용해 현재 트랜잭션의 ID보다 작은 ID로 변경된 데이터만 언두 로그에서 조회한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;790&quot; data-origin-height=&quot;831&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zZFwr/btsilIIDAtj/ame7LzkOBdsyvFmX2BqsZ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zZFwr/btsilIIDAtj/ame7LzkOBdsyvFmX2BqsZ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zZFwr/btsilIIDAtj/ame7LzkOBdsyvFmX2BqsZ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzZFwr%2FbtsilIIDAtj%2Fame7LzkOBdsyvFmX2BqsZ1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;579&quot; data-origin-width=&quot;790&quot; data-origin-height=&quot;831&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;사용자 B가 10번으로 트랜잭션을 시작했는데, 그때부터 사용자 B의 10번&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;트랜잭션 안에서 실행되는 모든 SELECT 쿼리는 트랜잭션 번호가 10(자신의 트랜잭션 번호)보다 작은 트랜잭션 번호에서 변경한 것만 조회하게 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;●&amp;nbsp;PHANTOM READ 가 발생할 수 있다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #666666; 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;806&quot; data-origin-height=&quot;770&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cnqKzL/btsilIu2gbm/pT4Q3fBX0jfyQQaWchxt11/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cnqKzL/btsilIu2gbm/pT4Q3fBX0jfyQQaWchxt11/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cnqKzL/btsilIu2gbm/pT4Q3fBX0jfyQQaWchxt11/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcnqKzL%2FbtsilIu2gbm%2FpT4Q3fBX0jfyQQaWchxt11%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;525&quot; data-origin-width=&quot;806&quot; data-origin-height=&quot;770&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&quot;SELECT ... FOR UPDATE&quot;, &quot;SELECT ... FOR SHARE&quot; 를 제외한 SELECT 에서는&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;InnoDB 스토리지 엔진에서 갭 락과 넥스트 키 락 덕분에 PHANTOM READ 가 발생하지 않는다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;SELECT ... FOR UPDATE 쿼리는 SELECT 하는 레코드에 쓰기 잠금을 걸어야 하는데, 언두 레코드에는 잠금을 걸 수 없어 변경 전이 아닌 현재의 레코드를 조회하게 된다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;4. SERIALIZABLE&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;읽기 작업도 공유잠금을 획득하기 때문에 한 트랜잭션에서 읽고 쓰는 레코드를 다른 트랜잭션에서는 접근할 수 없다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;(잠금 없는 일관된 읽기)&lt;/b&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;잠금 없는 일관된 읽기 (Non-Locking Consistent Read)&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;InnoDB 스토리지 엔진은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;격리수준이 SERIALIZABLE이 아닌 경우&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;MVCC 기술을 이용해 다른 트랜잭션이 가지고 있는 잠금을 기다리지 않고 읽기 작업이 가능하다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;격리 수준이 1 &amp;gt; 4 로 갈수록 트랜잭션 간의 격리 정도가 높아지며, 동시 처리 성능은 떨어진다.&lt;/p&gt;
&lt;table style=&quot;background-color: #ffffff; color: #666666; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;color: #666666;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;color: #666666; text-align: center;&quot;&gt;&lt;b&gt;DIRTY READ&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;color: #666666; text-align: center;&quot;&gt;&lt;b&gt;NON-REPEATABLE READ&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;color: #666666; text-align: center;&quot;&gt;&lt;b&gt;PHANTOM READ&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;color: #666666;&quot;&gt;1. READ UNCOMMITTED&lt;/td&gt;
&lt;td style=&quot;color: #666666; text-align: center;&quot;&gt;O&lt;/td&gt;
&lt;td style=&quot;color: #666666; text-align: center;&quot;&gt;O&lt;/td&gt;
&lt;td style=&quot;color: #666666; text-align: center;&quot;&gt;O&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;color: #666666;&quot;&gt;2. READ COMMITTED&lt;/td&gt;
&lt;td style=&quot;color: #666666; text-align: center;&quot;&gt;X&lt;/td&gt;
&lt;td style=&quot;color: #666666; text-align: center;&quot;&gt;O&lt;/td&gt;
&lt;td style=&quot;color: #666666; text-align: center;&quot;&gt;O&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;color: #666666;&quot;&gt;3. REPEATABLE READ&lt;/td&gt;
&lt;td style=&quot;color: #666666; text-align: center;&quot;&gt;X&lt;/td&gt;
&lt;td style=&quot;color: #666666; text-align: center;&quot;&gt;X&lt;/td&gt;
&lt;td style=&quot;color: #666666; text-align: center;&quot;&gt;O&lt;br /&gt;(InnoDB는 없음)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;color: #666666;&quot;&gt;4. SERIALIZABLE&lt;/td&gt;
&lt;td style=&quot;color: #666666; text-align: center;&quot;&gt;X&lt;/td&gt;
&lt;td style=&quot;color: #666666; text-align: center;&quot;&gt;X&lt;/td&gt;
&lt;td style=&quot;color: #666666; text-align: center;&quot;&gt;X&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;참고&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a style=&quot;color: #3db39e;&quot; href=&quot;https://product.kyobobook.co.kr/detail/S000001766482&quot;&gt;Real MySQL 8.0 (1권)&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Database/MySQL</category>
      <author>mandykr</author>
      <guid isPermaLink="true">https://mandykr.tistory.com/107</guid>
      <comments>https://mandykr.tistory.com/107#entry107comment</comments>
      <pubDate>Fri, 2 Jun 2023 14:33:07 +0900</pubDate>
    </item>
    <item>
      <title>[MySQL] 옵티마이저의 데이터 처리</title>
      <link>https://mandykr.tistory.com/105</link>
      <description>&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;2. 병렬 처리&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. ORDER BY 처리&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. GROUP BY 처리(Using&amp;nbsp;filesort)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5. DISTINCT 처리&lt;/p&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;h2 data-ke-size=&quot;size26&quot;&gt;옵티마이저&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL에서는 쿼리를 최적으로 실행하기 위해 각 테이블의 데이터가 어떤 분포로 저장돼 있는지 통계 정보를 참조해 최적의 실행 계획을 수립하는 작업이 필요한데 옵티마이저가 이 작업을 수행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;옵티마이저는 쿼리 파서의 파싱 작업을 거쳐 만들어진 SQL 파스 트리를 참조하며 다음과 같은 과정을 거쳐 실행 계획을 만든다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;불필요한 조건 제거 및 복잡한 연산의 단순화&lt;/li&gt;
&lt;li&gt;여러 테이블의 조인이 있는 경우 어떤 순서로 테이블을 읽을지 결정&lt;/li&gt;
&lt;li&gt;각 테이블에 사용된 조건과 인덱스 통계 정보를 이용해 사용할 인덱스를 결정&lt;/li&gt;
&lt;li&gt;가져온 레코드들을 임시 테이블에 넣고 다시 한번 가공해야 하는지 결정&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수립된 실행 계획대로 스토리지 엔진에 레코드를 읽어오도록 요청하고, MySQL 엔진에서는 스토리지 엔진으로부터 받은 레코드를 조인하거나 정렬하는 작업을 수행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 풀 테이블 스캔과 풀 인덱스 스캔&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;옵티마이저는 다음과 같은 조건이 일치할 때 주로 풀 테이블 스캔을 선택한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;테이블의 레코드 건수가 너무 작아서 인덱스를 통해 읽는 것보다 풀 테이블 스캔을 하는 편이 더 빠른 경우&lt;br /&gt;(일반적으로 테이블이 페이지 1개로 구성된 경우)&lt;/li&gt;
&lt;li&gt;WHERE 절이나 ON 절에 인덱스를 이용할 수 있는 적절한 조건이 없는 경우&lt;/li&gt;
&lt;li&gt;인덱스 레인지 스캔을 사용할 수 있는 쿼리라고 하더라도 옵티마이저가 판단한 조건 일치 레코드 건수가 너무 많은 경우&lt;br /&gt;(인덱스의 B-Tree를 샘플링해서 조사한 통계 정보 기준)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;풀 테이블 스캔이 실행되면 처음 몇 개의 데이터 페이지는 포그라운드 스레드가 페이지 읽기를 실행하지만 특정 시점부터는 읽기 작업을 백그라운드 스레드로 넘기고 포그라운드 스레드는 미리 버퍼 풀에 준비된 데이터를 가져다 사용하기만 하면 되므로 상당히 빨리 처리된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;(리드 어헤드)&lt;/b&gt;&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;InnoDB 스토리지 엔진은 특정 테이블의 연속된 데이터 페이지가 읽히면 백그라운드 스레드에 의해 리드 어헤드 작업이 자동으로 시작된다. 리드 어헤드란 어떤 영역의 데이터가 앞으로 필요해지리라는 것을 예측해서 요청이 오기 전에 미리 디스크에서 읽어 InnoDB 버퍼 풀에 가져다 두는 것을 의미한다.&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;innodb_read_ahead_threshold 시스템 변수에 설정된 개수만큼의 포그라운드 스레드에 의해 연속된 데이터 페이지가 읽히면 InnoDB 스토리지 엔진은 백그라운드 스레드를 이용해 대량으로 그다음 페이지들을 읽어서 버퍼 풀로 적재한다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 병렬 처리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL 8.0 에서는 아무런 WHERE 조건 없이 단순히 테이블의 전체 건수를 가져오는 쿼리만 병렬로 처리할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;innodb_parallel_read_threads 시스템 변수를 이용해 하나의 쿼리를 최대 몇 개의 스레드를 이용해서 처리할지를 변경할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1685673543006&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SET SESSION innodb_parallel_read_threads = 4;
SELECT COUNT(*) FROM salaries;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. ORDER BY 처리(Using filesort)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정렬을 처리하는 방법은 인덱스를 이용하는 방법과 &quot;Filesort&quot;라는 별도의 처리를 이용하는 방법으로 나눌 수 있다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.3566%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 50.31%;&quot;&gt;&lt;b&gt;장점&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;b&gt;단점&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.3566%;&quot;&gt;&lt;b&gt;인덱스 이용&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 50.31%;&quot;&gt;(INSERT, UPDATE, DELETE 쿼리가 실행될 때) 이미 인덱스가 정렬돼 있어서 순서대로 읽기만 하면 되므로 매우 빠르다.&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;INSERT, UPDATE, DELETE 작업 시 부가적인 인덱스 추가/삭제 작업이 필요하므로 느리다.&lt;br /&gt;인덱스 때문에 디스크 공간이 더 많이 필요하다.&lt;br /&gt;인덱스의 개수가 늘어날수로 InnoDB 버퍼 풀을 위한 메모리가 많이 필요하다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.3566%;&quot;&gt;&lt;b&gt;Filesort 이용&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 50.31%;&quot;&gt;인덱스를 생성하지 않아도 되므로 인덱스를 이용할 때의 단점이 장점으로 바뀐다.&lt;br /&gt;정렬해야 할 레코드가 많지 않으면 메모리에서 Filesort가 처리되므로 충분히 빠르다.&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;정렬 작업이 쿼리 실행시 처리되므로 레코드 대상 건수가 많아질수로 쿼리의 응답 속도가 느리다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스를 이용한 정렬이 빠르지만 다음과 같은 경우 때문에 모든 정렬을 인덱스를 이용하도록 튜닝하긴 불가능하다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;정렬 기준이 너무 많아서 요건별로 모두 인덱스를 생성하는 것이 불가능한 경우&lt;/li&gt;
&lt;li&gt;GROUP BY의 결과 또는 DISTINCT 같은 처리의 결과를 정렬해야 하는 경우&lt;/li&gt;
&lt;li&gt;UNION의 결과와 같이 임시 테이블의 결과를 다시 정렬해야 하는 경우&lt;/li&gt;
&lt;li&gt;랜덤하게 결과 레코드를 가져와야 하는 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1) 소트 버퍼&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL은 정렬이 필요한 경우에만 소트 버퍼라는 별도의 메모리 공간을 할당받아 정렬에 사용하고 쿼리의 실행이 완료되면 즉시 시스템으로 반납한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;(소트 버퍼 메모리 크기)&lt;/b&gt;&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;버퍼의 크기는 정렬해야 할 레코드의 크기에 따라 가변적으로 증가&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최대 사용 가능한 버퍼의 공간은 sort_buffer_size 시스템 변수로 설정&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;(1) 멀티 머지&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정렬해야 할 레코드의 건수가 소트 버퍼로 할당된 공간보다 큰 경우 MySQL은 레코드를 여러 조각으로 나눠서 처리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;버퍼 크기만큼 정렬된 레코드를 병합하면서 정렬을 수행한다. 이 작업들은 모두 디스크 I/O를 유발한다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;메모리의 소트 버퍼에서 정렬을 수행&lt;/li&gt;
&lt;li&gt;결과를 임시로 디스크에 기록&lt;/li&gt;
&lt;li&gt;다음 레코드를 가져와 반복 수행&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;(2) 세션 메모리 영역&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소트 버퍼는 세션 메모리 영역을 사용하는데, 커넥션이 많을수록, 정렬 작업이 많을수록 소트 버퍼로 소비되는 메모리 공간이 커지게 된다. 소트 버퍼의 크기를 10MB 이상으로 설정하면 대량의 레코드를 정렬하는 쿼리가 여러 커넥션에서 동시에 실행되면서 운영체제는 메모리 부족 현상을 겪을 수도 있다.&lt;/p&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;테스트하는 서버에서 메모리나 디스크의 특성에 따라 다를 수 있지만 일반적인 트랜잭션 처리용 서버에서 소트 버퍼 크기는 56KB ~ 1MB가 적절하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2) 정렬 알고리즘&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정렬할 때 소트 버퍼에 레코드 전체를 담을지, 정렬 기준 컬럼만 담을지에 따라 싱글 패스와 투 패스 모드로 나눌 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;(1) 싱글 패스(Single-pass)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소트 버퍼에 Select 대상이 되는 컬럼을 전부 담아서 정렬을 수행&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&amp;lt;sort_key, additional_fields&amp;gt;: 정렬 키와 레코드 전체를 가져와서 정렬하는 방식, 레코드의 컬럼들은 고정 사이즈로 메모리 저장&lt;/li&gt;
&lt;li&gt;&amp;lt;sort_key, packed_additional_fields&amp;gt;: 정렬 키와 레코드 전체를 가져와서 정렬하는 방식, 레코드의 컬럼들은 가변 사이즈로 메모리 저장&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;(2) 투 패스(Two-pass)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정렬 대상 컬럼과 PK 값만 소트 버퍼에 담아서 정렬을 수행&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;정렬된 순서대로 다시 PK로 테이블을 읽어서 SELECT 할 컬럼을 가져오는 방식&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&amp;lt;sort_key, rowid&amp;gt;: 정렬 키와 레코드의 Row ID만 가져와서 정렬하는 방식&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;최신 버전에서는 일반적으로 싱글 패스 방식을 주로 사용&lt;/span&gt;하고, 다음의 경우 투 패스 방식 사용&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;레코드의 크기가 max_length_for_sort_data 시스템 변수에 설정된 값보다 클 때&lt;/li&gt;
&lt;li&gt;BLOB이나 TEXT 타입의 컬럼이 SELECT 대상에 포함될 때&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3) 정렬 처리 방법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿼리에 ORDER BY가 사용되면 다음 3가지 방법중 하나로 정렬이 처리된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스를 사용할 수 있는지 검토하고 사용할 수 없으면 WHERE 조건에 일치하는 레코드를 검색해 정렬 버퍼에 저장하면서 Filesort 한다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 71px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 41.3566%; height: 20px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 32.5193%; height: 20px;&quot;&gt;&lt;b&gt;실행 계획의 Extra 컬럼 내용&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 13.5272%; height: 20px;&quot;&gt;&lt;b&gt;우선 순위&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 12.5969%; height: 20px;&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: 41.3566%; height: 17px;&quot;&gt;&lt;b&gt;인덱스를 사용한 정렬&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 32.5193%; height: 17px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 13.5272%; height: 17px;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;width: 12.5969%; height: 17px;&quot;&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 41.3566%; height: 17px;&quot;&gt;&lt;b&gt;조인에서 드라이빙 테이블만 정렬&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 32.5193%; height: 17px;&quot;&gt;&quot;Using filesort&quot;&lt;/td&gt;
&lt;td style=&quot;width: 13.5272%; height: 17px;&quot;&gt;2&lt;/td&gt;
&lt;td style=&quot;width: 12.5969%; height: 17px;&quot;&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 41.3566%; height: 17px;&quot;&gt;&lt;b&gt;조인에서 조인 결과를 임시 테이블로 저장 후 정렬&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 32.5193%; height: 17px;&quot;&gt;&quot;Using temporary; Using filesort&quot;&lt;/td&gt;
&lt;td style=&quot;width: 13.5272%; height: 17px;&quot;&gt;3&lt;/td&gt;
&lt;td style=&quot;width: 12.5969%; height: 17px;&quot;&gt;3&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;h4 data-ke-size=&quot;size20&quot;&gt;(1) 인덱스를 이용한 정렬&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스의 값이 정렬돼 있기 때문에 인덱스의 순서대로 읽기만 하면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1685679084222&quot; class=&quot;n1ql&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT * FROM employees e, salaries s
WHERE s.emp_no=e.emp_no
AND e.emp_no BETWEEN 1000002 AND 100020
ORDER BY e.emp_no;&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;763&quot; data-origin-height=&quot;320&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DIyzE/btsijP16J7G/ZkYzfvOr64gFPooQM4Xg41/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DIyzE/btsijP16J7G/ZkYzfvOr64gFPooQM4Xg41/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DIyzE/btsijP16J7G/ZkYzfvOr64gFPooQM4Xg41/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDIyzE%2FbtsijP16J7G%2FZkYzfvOr64gFPooQM4Xg41%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;606&quot; height=&quot;254&quot; data-origin-width=&quot;763&quot; data-origin-height=&quot;320&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스를 이용한 정렬을 위해 필요한 조건&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;ORDER BY에 명시된 컬럼이 드라이빙 테이블에 속해야 한다.&lt;/li&gt;
&lt;li&gt;ORDER BY 순서대로 생성된 인덱스가 있어야 한다.&lt;/li&gt;
&lt;li&gt;WHERE 절에 첫 번째로 읽는 테이블의 컬럼에 대한 조건이 있다면 그 조건과 ORDER BY는 같은 인덱스를 사용할 수 있어야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;(2) 조인의 드라이빙 테이블만 정렬&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;드라이빙 테이블의 레코드를 먼저 정렬한 다음 조인을 실행한다.&lt;/p&gt;
&lt;pre id=&quot;code_1685679095053&quot; class=&quot;n1ql&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT * FROM employees e, salaries s
WHERE s.emp_no=e.emp_no
AND e.emp_no BETWEEN 100002 AND 100010
ORDER BY e.last_name;&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;716&quot; data-origin-height=&quot;530&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ddl6Pe/btsij9stwtI/KhfnK8vGnDwvIGmZUbbPVK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ddl6Pe/btsij9stwtI/KhfnK8vGnDwvIGmZUbbPVK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ddl6Pe/btsij9stwtI/KhfnK8vGnDwvIGmZUbbPVK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fddl6Pe%2Fbtsij9stwtI%2FKhfnK8vGnDwvIGmZUbbPVK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;563&quot; height=&quot;417&quot; data-origin-width=&quot;716&quot; data-origin-height=&quot;530&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;인덱스를 이용해 &quot;emp_no BETWEEN 100001 AND 100010&quot; 조건을 만족하는 9건을 검색&lt;/li&gt;
&lt;li&gt;검색 결과를 last_name 컬럼으로 정렬을 수행(Filesort)&lt;/li&gt;
&lt;li&gt;정렬된 결과를 순서대로 읽으면서 salaries 테이블과 조인을 수행해 86건의 최종 결과를 가져옴&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;드라이빙 테이블만 정렬을 위해 필요한 조건&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;드라이빙 테이블의 컬럼만으로 ORDER BY 절을 작성해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(검색은 인덱스 레인지 스캔으로 처리할 수 있지만,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ORDER BY 절에 명시된 컬럼은 PK와 전혀 연관이 없으므로 인덱스를 이용한 정렬은 불가능하다.)&lt;/p&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;(3) 임시 테이블을 이용한 정렬&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;드라이빙 테이블만 정렬&quot; 방식 외의 패턴에서는 조인된 데이터를 임시 테이블에 저장하고 정렬을 수행한다.&lt;/p&gt;
&lt;pre id=&quot;code_1685679408527&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT * FROM employees e, salaries s
WHERE s.emp_no=e.emp_no 
AND e.emp_no BETWEEN 100002 AND 100010
ORDER BY s.salary;&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;699&quot; data-origin-height=&quot;649&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/djkl2D/btsij8mSJqr/GMapsRslGkRqoGF1vxwVmk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/djkl2D/btsij8mSJqr/GMapsRslGkRqoGF1vxwVmk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/djkl2D/btsij8mSJqr/GMapsRslGkRqoGF1vxwVmk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdjkl2D%2Fbtsij8mSJqr%2FGMapsRslGkRqoGF1vxwVmk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;586&quot; height=&quot;544&quot; data-origin-width=&quot;699&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(4) 정렬 처리 방법의 성능&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;스트리밍 방식 : 레코드가 검색될 때마다 바로바로 클라이언트로 전송해주는 방식&lt;/li&gt;
&lt;li&gt;버퍼링 방식 : 모든 레코드를 가져온 후, 정렬하거나 그루핑해서 차례대로 전송하는 방식&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스를 사용한 정렬 방식은 스트리밍 형태의 처리이며, 나머지 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;(JDBC 라이브러리의 버퍼링)&lt;/b&gt;&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL 서버는 스트리밍 방식으로 처리해서 레코드를 반환하더라도 JDBC 라이브러리는 MySQL 서버로부터 받는 레코드를 일단 내부 버퍼에 모두 담아두고 마지막 레코드까지 전달받으면 클라이언트의 애플리케이션에 반환하기 때문에 버퍼링 방식으로 동작한다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4) 정렬 관련 상태 변수&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL 서버는 처리하는 주요 작업에 대해서는 해당 작업의 실행 횟수를 상태 변수로 저장한다.&lt;/p&gt;
&lt;pre id=&quot;code_1685680083436&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;FULUSH STATUS;
SHOW STATUS LIKE 'Sort%';&lt;/code&gt;&lt;/pre&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25.3488%;&quot;&gt;&lt;b&gt;Sort_merge_passes&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 74.6512%;&quot;&gt;멀티 머지 처리 횟수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25.3488%;&quot;&gt;&lt;b&gt;Sort_range&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 74.6512%;&quot;&gt;인덱스 레인지 스캔을 통해 검색된 결과에 대한 정렬 작업 횟수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25.3488%;&quot;&gt;&lt;b&gt;Sort_scan&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 74.6512%;&quot;&gt;풀 테이블 스캔을 통해 검색된 결과에 대한 정렬 작업 횟수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25.3488%;&quot;&gt;&lt;b&gt;Sort_rows&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 74.6512%;&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;h2 data-ke-size=&quot;size26&quot;&gt;4. GROUP BY 처리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GROUB BY 작업은 인덱스 스캔, 루스 인덱스 스캔, 인덱스를 사용하지 못하고 임시 테이블을 사용하는 방법으로 나뉜다.&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;1) 인덱스 스캔을 이용하는 GROUP BY(타이트 인덱스 스캔)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;드라이빙 테이블에 속한 컬럼만 이용해 그루핑할 때, GROUP BY 컬럼으로 인덱스가 있다면 인덱스를 차례대로 읽으면서 그루핑 작업을 수행하고 그 결과로 조인을 처리한다. 그룹 함수등의 그룹값을 처리해야 하는 경우에는 임시 테이블이 필요할 때도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 계획의 Extra : 없음&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;2) 루스 인덱스 스캔을 이용하는 GROUP BY&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단일 테이블에 대해 수행되는 GROUP BY 처리에만 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(&lt;a href=&quot;https://mandykr.tistory.com/104#:~:text=3)-,%EB%A3%A8%EC%8A%A4%20%EC%9D%B8%EB%8D%B1%EC%8A%A4%20%EC%8A%A4%EC%BA%94,-%EC%9D%B8%EB%8D%B1%EC%8A%A4%20%EB%A0%88%EC%9D%B8%EC%A7%80%20%EC%8A%A4%EC%BA%94%EA%B3%BC&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;실행 계획의 Extra : &quot;Using index for group-by&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;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3) 임시 테이블을 사용하는 GROUP BY&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GROUP BY의 기준 컬럼이 드라이빙, 드리븐 테이블에 있는지와 관계없이 인덱스를 사용하지 못할 때 처리되는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL 8.0 에서는 GROUP BY가 필요한 경우 내부적으로 GROUP BY 절의 컬럼들로 구성된 유니크 인덱스를 가진 임시 테이블을 만들어서 중복 제거와 집합 함수 연산을 수행한다.&lt;/p&gt;
&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; text-align: start;&quot;&gt;실행 계획의 Extra :&lt;span&gt; &quot;Using temporary&quot;&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. DISTINCT 처리&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1) SELECT DISTINCT&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;집합 함수를 사용하지 않고 DISTINCT 만 사용한 경우 GROUP BY와 동일한 방식으로 처리된다.&lt;/p&gt;
&lt;pre id=&quot;code_1685681907673&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT DISTINCT emp_no FROM salaries;
SELECT emp_no FROM salaries GROUP BY emp_no;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2) 집합 함수와 함께 사용된 DISTINCT&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;COUNT(), MIN(), MAX() 같은 집합 함수 내에서 사용된 DISTINCT는 그 집합 함수의 인자로 전달된 컬럼값이 유니크한 것들을 가져오고 단순히 DISTINCT 만 사용한 경우와 다르게 동작한다.&lt;/p&gt;
&lt;pre id=&quot;code_1685682046813&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;EXPLAIN
SELECT COUNT(DISTINCT s.salary)
FROM employees e, salaries s
WHERE e.emp_no=s.emp_no
AND e.emp_no BETWEEN 100001 AND 100100;&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;&quot;COUNT(DISTINCT s.salary)&quot; 를 처리하기 위해 조인한 결과에서 salary 컬럼의 값만 저장하기 위해 임시 테이블을 사용하지만 실행 계획에서 &quot;Using temporary&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;background-color: #f6e199;&quot;&gt;인덱스된 컬럼에 대해 DISTINCT 처리를 수행할 때는 인덱스를 풀 스캔하거나 레인지 스캔하면서 임시 테이블 없이 최적화된 처리를 수행할 수 있다.&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a style=&quot;color: #3db39e;&quot; href=&quot;https://product.kyobobook.co.kr/detail/S000001766482&quot;&gt;Real MySQL 8.0 (1권)&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Database/MySQL</category>
      <category>옵티마이저</category>
      <author>mandykr</author>
      <guid isPermaLink="true">https://mandykr.tistory.com/105</guid>
      <comments>https://mandykr.tistory.com/105#entry105comment</comments>
      <pubDate>Fri, 2 Jun 2023 13:52:19 +0900</pubDate>
    </item>
    <item>
      <title>[MySQL] B-Tree 인덱스를 통한 데이터 읽기</title>
      <link>https://mandykr.tistory.com/104</link>
      <description>&lt;p 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;1. B-Tree 인덱스를 통한 데이터 읽기&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;2. B-Tree 인덱스의 정렬 및 스캔 방향&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;3. 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;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 경우에 인덱스를 사용하게 유도할지, 또는 사용하지 못하게 할지 판단하려면 MySQL이 어떻게 인덱스를 이용해서 실제 레코드를 읽어 내는지 알아야 한다. MySQL이 인덱스를 이용하는 방법들과 인덱스의 정렬, 가용성에 대해 알아본다.&lt;/p&gt;
&lt;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;h2 data-ke-size=&quot;size26&quot;&gt;1. B-Tree 인덱스를 통한 데이터 읽기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1) 인덱스 레인지 스캔&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스 레인지 스캔은 검색해야 할 인덱스의 범위가 결정됐을 때 사용하는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1684815934338&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT * FROM employees WHERE first_name BETWEEN 'Ebbe' AND 'Gad';&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;866&quot; data-origin-height=&quot;632&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9jgC1/btsg0WH7mnp/ozJHcNpdbpd2vjatGhrCKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9jgC1/btsg0WH7mnp/ozJHcNpdbpd2vjatGhrCKK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9jgC1/btsg0WH7mnp/ozJHcNpdbpd2vjatGhrCKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9jgC1%2Fbtsg0WH7mnp%2FozJHcNpdbpd2vjatGhrCKK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;650&quot; height=&quot;474&quot; data-origin-width=&quot;866&quot; data-origin-height=&quot;632&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;(1) 인덱스 레인지 스캔&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;루트 노드 &lt;span&gt;&amp;rarr; 브랜치 노드 &amp;rarr; 리프 노드(n) &amp;rarr; 리프 노드(n+1) &amp;rarr; ... &amp;rarr; 레코드 반환&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;루트 노드에서부터 비교를 시작해 브랜치 노드를 거치고 리프 노드까지 찾아가 레코드의 시작 지점을 찾는다.&lt;/li&gt;
&lt;li&gt;시작 지점부터 순서대로 레코드를 읽는다.&lt;/li&gt;
&lt;li&gt;페이지의 끝까지 읽으면 리프 노드간의 링크(Linked list)를 이용해 다음 리프 노드를 찾아서 다시 스캔한다.&lt;/li&gt;
&lt;li&gt;최종적으로 레코드를 끝까지 읽으면 사용자에게 반환한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;(2) 인덱스 레인지 스캔을 통한 레코드 읽기&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-origin-width=&quot;973&quot; data-origin-height=&quot;629&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/csZAXk/btsg8cXdYk0/MBICNyg9jECS983KYpsAoK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/csZAXk/btsg8cXdYk0/MBICNyg9jECS983KYpsAoK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/csZAXk/btsg8cXdYk0/MBICNyg9jECS983KYpsAoK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcsZAXk%2Fbtsg8cXdYk0%2FMBICNyg9jECS983KYpsAoK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;650&quot; height=&quot;420&quot; data-origin-width=&quot;973&quot; data-origin-height=&quot;629&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;인덱스 조건을 만족하는 값이 저장된 위치를 찾는다. (Index seek, 인덱스 탐색)&lt;/li&gt;
&lt;li&gt;탐색된 위치부터 필요한 만큼 인덱스를 차례대로 쭉 읽는다. (Index scan, 인덱스 스캔)&lt;/li&gt;
&lt;li&gt;읽은 인덱스 키와 레코드 주소를 이용해 레코드가 저장된 페이지를 가져오고 최종 레코드를 읽어온다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리프 노드에 저장된 레코드 주소로 데이터 파일의 레코드를 읽어오는데&lt;span&gt;&amp;nbsp;&lt;/span&gt;레코드 한 건 단위로 랜덤 I/O가 발생하기 때문에 인덱스를 통해 데이터 레코드를 읽는 작업은 비용이 많이 드는 작업으로 분류된다. 그리고&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;인덱스를 통해 읽어야 할 데이터 레코드가 20~25%를 넘으면 인덱스를 통한 읽기보다 테이블의 데이터를 직접 읽는 것이 효율적인 처리 방식이 된다.&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;/b&gt;&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿼리가 필요로 하는 데이터가 인덱스의 키로 사용되고 있는 경우 디스크의 레코드를 읽지 않을 수도 있는데 이를 커버링 인덱스라고 한다. 랜덤 읽기가 상당히 줄어들기 때문에 그만큼 성능이 빨라진다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2) 인덱스 풀 스캔&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스의 처음부터 끝까지 모두 읽는 방식을 인덱스 풀 스캔이라고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표적으로 쿼리의 조건절에 사용된 컬럼이 인덱스의 첫 번째 컬럼이 아닌 경우 사용된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;716&quot; data-origin-height=&quot;513&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sU13v/btsg1dC57aQ/cbdnwu8HIdkUjp6ZZQLHn1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sU13v/btsg1dC57aQ/cbdnwu8HIdkUjp6ZZQLHn1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sU13v/btsg1dC57aQ/cbdnwu8HIdkUjp6ZZQLHn1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsU13v%2Fbtsg1dC57aQ%2Fcbdnwu8HIdkUjp6ZZQLHn1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;650&quot; height=&quot;466&quot; data-origin-width=&quot;716&quot; data-origin-height=&quot;513&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;쿼리가 인덱스에 명시된 컬럼만으로 조건을 처리할 수 있는 경우 이 방식이 사용되고 디스크 I/O가 적기 때문에 &lt;span style=&quot;background-color: #f6e199;&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;h3 data-ke-size=&quot;size23&quot;&gt;3) 루스 인덱스 스캔&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스 레인지 스캔과 비슷하게 작동하지만 중간에 필요치 않은 인덱스 키 값은 무시(SKIP)하고 다음으로 넘어가는 형태로 처리한다. 일반적으로 GROUP BY 또는 집합 함수 가운데 MAX() 또는 MIN() 함수에 대해 최적화를 하는 경우에 사용한다.&lt;/p&gt;
&lt;pre id=&quot;code_1684817462505&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT dept_no, MIN(emp_no)
FROM dept_emp
WHERE dep_no BETWEEN 'd002' AND 'd004'
GROUP BY dept_no;&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;405&quot; data-origin-height=&quot;648&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/beR2dU/btsg0tsT59W/plaUb0seHWtHC65lTf2FZK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/beR2dU/btsg0tsT59W/plaUb0seHWtHC65lTf2FZK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/beR2dU/btsg0tsT59W/plaUb0seHWtHC65lTf2FZK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbeR2dU%2Fbtsg0tsT59W%2FplaUb0seHWtHC65lTf2FZK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;560&quot; data-origin-width=&quot;405&quot; data-origin-height=&quot;648&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;dept_no 그룹별로 첫 번째 레코드의 emp_no 값만 읽으면 되기 때문에 옵티마이저는 조건에 만족하지 않는 레코드는 무시하고 다음 레코드로 이동한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4) 인덱스 스킵 스캔&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다중 컬럼 인덱스의 선행 컬럼이 쿼리의 조건절에 없는 경우, 선행 컬럼의 유니크한 값을 비교하는 조건을 쿼리에 추가해서 다시 실행하는 형태로 동작하게 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1684818670642&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ALTER TABLE employees
ADD INDEX ix_gender_birthdate (gender, birth_date);

-- 인덱스 레인지 스캔으로 동작
SELECT * FROM employees WHERE gender = 'M' AND birth_date &amp;gt;= '1965-02-01';&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스를 효율적으로 사용하기 위해 gender 컬럼과 birth_date 컬럼의 조건을 쿼리가 모두 가져야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1684818841285&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-- 인덱스 풀 스캔으로 동작
SELECT gender, birth_date FROM employees WHERE birth_date &amp;gt;= '1965-02-01';&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스의 선행 컬럼인 gender가 쿼리의 조건에 없는 경우에는 인덱스 풀 스캔 혹은 테이블 풀 스캔으로 동작하는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL 8.0 버전부터 옵티마이저가 gender 컬럼을 건너뛰어서 인덱스 검색이 가능하도록 하는 최적화 기능이 도입됐다.&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;672&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ccLpZF/btsg1sG85HP/wqZiUktGBCtemxZjxMG5b0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ccLpZF/btsg1sG85HP/wqZiUktGBCtemxZjxMG5b0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ccLpZF/btsg1sG85HP/wqZiUktGBCtemxZjxMG5b0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FccLpZF%2Fbtsg1sG85HP%2FwqZiUktGBCtemxZjxMG5b0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;650&quot; height=&quot;683&quot; data-origin-width=&quot;640&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;우선 gender 컬럼에서 유니크한 값을 모두 조회해서 주어진 쿼리에 gender 컬럼의 조건을 추가해서 쿼리를 다시 실행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스 스킵 스캔의 제약 조건&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;WHERE 절에 조건이 없는 인덱스의 선행 column의 유니크한 값 개수가 적어야 함&lt;/li&gt;
&lt;li&gt;쿼리가 인덱스에 존재하는 column만으로 처리 가능해야 함(커버링 인덱스)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. B-Tree 인덱스의 정렬 및 스캔 방향&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스를 생성할 때 설정한 정렬 규칙에 따라서 인덱스의 키 값은 항상 오름차순이거나 내림차순으로 정렬되어 저장된다. 하지만 인덱스를 어느 방향으로 읽을지는 쿼리에 따라 옵티마이저가 실시간으로 만들어내는 실행 계획에 따라 결정된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1) 인덱스의 정렬&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스 생성 시점에 ASC, DESC 키워드로 인덱스의 정렬 방향을 설정할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1684924373495&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CREATE INDEX ix_firstname ON employees (first_name ASC);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2) 인덱스 스캔 방향&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿼리의 ORDER BY 처리나 MIN() 또는 MAX() 함수 등의 최적화가 필요한 경우에도 MySQL 옵티마이저는 인덱스의 읽기 방향을 전환해서 사용하도록 실행 계획을 만들어 낸다.&lt;/p&gt;
&lt;pre id=&quot;code_1684924707431&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-- ix_firstname 인덱스를 정순으로 10개 스캔
SELECT * FROM employees ORDER BY first_name ASC LIMIT 10;

-- ix_firstname 인덱스를 역순으로 10개 스캔
SELECT * FROM employees ORDER BY first_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;h3 data-ke-size=&quot;size23&quot;&gt;3) 내림차순 인덱스&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같은 이유로 InnoDB에서 인덱스 역순 스캔이 정순 스캔에 비해 느리다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;페이지 잠금이 인덱스 정순 스캔에 적합한 구조&lt;/li&gt;
&lt;li&gt;페이지 내에서 인덱스 레코드가 단방향으로만 연결된 구조&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 쿼리에서 자주 사용되는 정렬 순서대로 인덱스를 생성하는 것이 효율적이다.&lt;/p&gt;
&lt;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;h2 data-ke-size=&quot;size26&quot;&gt;3. B-Tree 인덱스의 가용성과 효율성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿼리의 WHERE 조건이나 GROUP BY 또는 ORDER BY 절이 어떤 경우에 인덱스를 사용할 수 있고 어떤 방식으로 사용할 수 있는지 식별할 수 있어야 쿼리의 조건을 최적화하거나, 역으로 쿼리에 맞게 인덱스를 최적으로 생성할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1) 비교 조건의 종류와 효율성&lt;/h3&gt;
&lt;pre id=&quot;code_1684926060528&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT * FROM dept_emp
WHERE dept_no = 'd002' AND emp_no &amp;gt;= 10114;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;(1) 케이스 A: INDEX (dept_no, emp_no)&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&quot; dept_no = 'd002' AND emp_no &amp;gt;= 10114 &quot; 인 레코드를 찾고, 그 이후에는 dept_no가 'd002'가 아닐 때까지 인덱스 스캔&lt;/li&gt;
&lt;li&gt;필요한 만큼의 비교작업만 수행하므로 효율적&lt;/li&gt;
&lt;li&gt;5번의 비교로 5건 반환&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;274&quot; data-origin-height=&quot;563&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bBhX3s/btshjdWd2zo/RI6jITwtYEvx3pYpAgkApK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bBhX3s/btshjdWd2zo/RI6jITwtYEvx3pYpAgkApK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bBhX3s/btshjdWd2zo/RI6jITwtYEvx3pYpAgkApK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbBhX3s%2FbtshjdWd2zo%2FRI6jITwtYEvx3pYpAgkApK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;514&quot; data-origin-width=&quot;274&quot; data-origin-height=&quot;563&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;(2) 케이스 B: INDEX (emp_no, dept_no)&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&quot; emp_no &amp;gt;= 10114 AND dept_no = 'd002' &quot; 인 레코드를 찾고, 그 이후 모든 레코드에 대해 dept_no가 'd002'인지 비교&lt;/li&gt;
&lt;li&gt;dept_no가 범위를 좁히는데 도움을 주지 못하기 때문에 비효율적&lt;/li&gt;
&lt;li&gt;7번의 비교로 5건 반환&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;402&quot; data-origin-height=&quot;563&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bHVUfz/btshjCVMce8/6kSm6a9kJjcnJaFkmwHMk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bHVUfz/btshjCVMce8/6kSm6a9kJjcnJaFkmwHMk1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bHVUfz/btshjCVMce8/6kSm6a9kJjcnJaFkmwHMk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbHVUfz%2FbtshjCVMce8%2F6kSm6a9kJjcnJaFkmwHMk1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;490&quot; data-origin-width=&quot;402&quot; data-origin-height=&quot;563&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;케이스 A에서 사용된 두 조건(dept_no = 'd002' AND emp_no &amp;gt;= 10114)과 같이 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;작업 범위를 결정하는 조건은 많으면 많을수록 쿼리의 처리 성능을 높이지만&lt;/span&gt; 케이스 B에서 사용된 조건(dept_no = 'd002')과 같은 체크 조건은 쿼리의 성능을 높이지 못하고 오히려 더 느리게 만들 때가 많다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2) 인덱스의 가용성&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;B-Tree 인덱스는 왼쪽 값에 기준해서 오른쪽 값이 정렬돼 있기 때문에 정렬 우선순위가 낮은 뒷부분의 값만으로는 인덱스의 효과를 얻을 수 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1684927139937&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;INDEX (first_name)
SELECT * FROM dept_emp WHERE first_name LIKE '%mer';&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;다중 컬럼 인덱스는 첫번째 컬럼으로 정렬한 후 두번째 컬럼으로 정렬돼 있기 때문에 인덱스의 선행 컬럼 조건없이 검색하면 인덱스를 효율적으로 사용할 수 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1684927287316&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;INDEX (dept_no, emp_no)
SELECT * FROM dept_emp WHERE emp_no &amp;gt;= 10144;&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;h3 data-ke-size=&quot;size23&quot;&gt;3) 가용성과 효율성 판단&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;(1) 인덱스 사용 불가 조건&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;NOT-EQUAL로&amp;nbsp;비교된&amp;nbsp;경우&amp;nbsp;(NOT&amp;nbsp;IN,&amp;nbsp;NOT&amp;nbsp;BETWEEN,&amp;nbsp;IS&amp;nbsp;NOT&amp;nbsp;NULL) &lt;br /&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;WHERE&amp;nbsp;column&amp;nbsp;&amp;lt;&amp;gt;&amp;nbsp;'N'&lt;/li&gt;
&lt;li&gt;WHERE&amp;nbsp;column&amp;nbsp;NOT&amp;nbsp;IN&amp;nbsp;(10,11,12)&lt;/li&gt;
&lt;li&gt;WHERE&amp;nbsp;column&amp;nbsp;IS&amp;nbsp;NOT&amp;nbsp;NULL&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;LIKE %?? 형태의 문자열 패턴 비교인 경우 &lt;br /&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;WHERE&amp;nbsp;column&amp;nbsp;LIKE&amp;nbsp;'%test&amp;lsquo;&lt;/li&gt;
&lt;li&gt;WHERE&amp;nbsp;column&amp;nbsp;LIKE&amp;nbsp;'%test%&amp;lsquo;&lt;/li&gt;
&lt;li&gt;WHERE&amp;nbsp;column&amp;nbsp;LIKE&amp;nbsp;'_test&amp;lsquo;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;스토어드&amp;nbsp;함수나&amp;nbsp;다른&amp;nbsp;연산자로&amp;nbsp;인덱스&amp;nbsp;컬럼이&amp;nbsp;변형된&amp;nbsp;후&amp;nbsp;비교된&amp;nbsp;경우 &lt;br /&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;WHERE&amp;nbsp;SUBSTRING(column,1,&amp;nbsp;1)&amp;nbsp;=&amp;nbsp;'X'&lt;/li&gt;
&lt;li&gt;WHERE&amp;nbsp;DAYOFMONTH(column)&amp;nbsp;=&amp;nbsp;1&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;NOT-DETERMINISTIC 속성의 스토어드 함수가 비교 조건에 사용된 경우
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;WHERE column = deterministic_function()&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;데이터 타입이 서로 다른 비교(인덱스 컬럼의 타입을 변환해야 비교가 가능한 경우)
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;WHERE char_column = 10&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;문자열 뎅터 타입의 콜레이션이 다른 경우
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;WHERE utf8_bin_char_column = euckr_bin_char_column&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;(2) 멀티 컬럼 인덱스의 가용성&lt;/h4&gt;
&lt;pre id=&quot;code_1684927879524&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;INDEX ix_test ( column_1, column_2, column_3, ... , column_n)&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;작업 범위 결정 조건으로 인덱스를 사용하지 못하는 경우
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;column_1 컬럼에 대한 조건이 없는 경우&lt;/li&gt;
&lt;li&gt;column_1 컬럼의 비교 조건이 위의 &quot;인덱스 사용 불가 조건&quot; 중 하나인 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;작업 범위 결정 조건으로 인덱스를 사용하는 경우
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;column_1 ~ column_(i-1) 컬럼까지 동등 비교 형태(&quot;=&quot; 또는 &quot;IN&quot;)로 비교&lt;/li&gt;
&lt;li&gt;column_i 컬럼에 대해 다음 연산자 중 하나로 비교
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;동등 비교(&quot;=&quot; 또는 &quot;IN&quot;)&lt;/li&gt;
&lt;li&gt;크다 작다 형태(&quot;&amp;gt;&quot; 또는 &quot;&amp;lt;&quot;)&lt;/li&gt;
&lt;li&gt;LIKE로 좌측 일치 패턴(LIKE 'test%')&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 일반적인 DBMS에서는 NULL 값이 인덱스에 저장되지 않지만 MySQL에서는 NULL 값도 인덱스에 저장되기 때문에 작업 범위 결정 조건으로 인덱스를 사용한다.&lt;/p&gt;
&lt;pre id=&quot;code_1684928229024&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;WHERE column IS NULL&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://product.kyobobook.co.kr/detail/S000001766482&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Real MySQL 8.0 (1권)&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;</description>
      <category>Database/MySQL</category>
      <category>index</category>
      <category>MySQL</category>
      <author>mandykr</author>
      <guid isPermaLink="true">https://mandykr.tistory.com/104</guid>
      <comments>https://mandykr.tistory.com/104#entry104comment</comments>
      <pubDate>Mon, 22 May 2023 13:05:28 +0900</pubDate>
    </item>
    <item>
      <title>[JPA] 조회 성능 최적화</title>
      <link>https://mandykr.tistory.com/102</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;목차&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1. 엔티티 단건 조회&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;2. 엔티티 목록 조회&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;3. 컬렉션 조회&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;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;N + 1&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;JPA의 N + 1 문제는 즉시로딩과 지연로딩 두 경우 모두 발생할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;즉시로딩&lt;/b&gt;은 JPQL 을 사용해 엔티티를 조회할 때 쿼리가 실행된 후 즉시로딩으로 연관 관계가 매핑된 엔티티를 조회하는 쿼리가 추가로 실행된다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;지연로딩&lt;/b&gt;은 엔티티를 조회할 때 연관 관계가 매핑된 엔티티는 프록시로 조회하지만 프록시의 필드에 접근할 때 해당 엔티티를 조회하는 쿼리가 추가로 실행된다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;다음은 지연로딩으로 매핑된 엔티티들의 연관 관계 종류에 따라 발생하는 N + 1 문제와 최적화 과정이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;지연로딩은 JPQL로 엔티티를 조회해도 연관된 엔티티를 추가로 조회하지 않기 때문에 N + 1 문제가 발생하지 않는다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;하지만 &lt;b&gt;엔티티의 목록을 조회할 때 연관된 엔티티나 컬렉션에 대해서 N + 1 문제가 발생&lt;/b&gt;한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;연관관계&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;471&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dor4wR/btsg0tZlST3/64DLkS3Yc7S0mkYq10hLiK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dor4wR/btsg0tZlST3/64DLkS3Yc7S0mkYq10hLiK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dor4wR/btsg0tZlST3/64DLkS3Yc7S0mkYq10hLiK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdor4wR%2Fbtsg0tZlST3%2F64DLkS3Yc7S0mkYq10hLiK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;471&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;471&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1682732052507&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
public class Mate {
    @Id
    private UUID id;

    @ManyToOne(optional = false, fetch = FetchType.LAZY)
    private Project project;

    @OneToOne(fetch = FetchType.LAZY)
    private Reviewer reviewer;

    @OneToMany(mappedBy = &quot;mate&quot;)
    private List&amp;lt;Review&amp;gt; reviews = new ArrayList&amp;lt;&amp;gt;();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1. 엔티티(Mate) 단건 조회&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Mate 단건을 조회하는 경우 Project, Reviewer, Review의 필드에 접근할 때 추가 쿼리가 실행된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Mate 조회&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1682655403579&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;mateRepository.findById(mateId);&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: #000000;&quot;&gt;&lt;b&gt;[Mate 조회 쿼리]&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;mateId 를 파라미터로 사용한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1682655367190&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;select *
from mate
where id = 'b061adb9-76a1-484b-beab-4f8cf4d7e798';&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: #000000;&quot;&gt;&lt;b&gt;[Project 조회 쿼리 (ManyToOne)]&lt;b&gt;&amp;nbsp;- 추가 쿼리&lt;/b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Mate 조회 쿼리의 결과로 project_id 를 파라미터로 사용한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1682731038638&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;select *
from project
where id = [mate의 project_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;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;[Reviewer 조회 쿼리 (OneToOne)]&lt;b&gt;&amp;nbsp;- 추가 쿼리&lt;/b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Mate 조회 쿼리의 결과로 reviewer_id 를 파라미터로 사용한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1682731161794&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;select *
from reviewer
where id = [mate의 reviewer_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;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;[Review 목록 조회 쿼리 (OneToMany)]&lt;b&gt;&amp;nbsp;- 추가 쿼리&lt;/b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;mateId로 review 목록을 조회한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1682731220276&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;select *
from review
where mate_id = [mate_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;span style=&quot;color: #000000;&quot;&gt;1개의 Mate를 조회하는데 추가로 3개의 쿼리가 실행되었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;문제는 &lt;b&gt;여러개의 Mate를 조회하는 경우 Mate의 갯수만큼&lt;/b&gt; 3개씩의 쿼리가 추가로 실행되게 된다.&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;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;2. 엔티티 목록 조회&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Mate 목록을 조회하는 경우 Mate의 갯수만큼 Project, Reviewer, Review의 필드에 접근할 때 추가 쿼리가 실행된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;모든 Mate를 조회한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1682733133158&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;mateRepository.findAll();&lt;/code&gt;&lt;/pre&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;쿼리&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;실행 횟수&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;결과 row&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Mate 조회&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;10&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Project 조회&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;10&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;10&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Reviewer 조회&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;10&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;10&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Review 조회&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;10&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;0+&lt;/span&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;[ 최적화 방법 ① ] 페치 조인&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;JPQL 페치 조인을 사용하면 SQL JOIN을 사용해서 페치 조인 대상까지 함께 조회한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;연관 관계 엔티티를 &lt;b&gt;지연로딩으로 설정&lt;/b&gt;하고 &lt;b&gt;필요한 경우에만&lt;/b&gt; 함께 조회하기 위해 페치 조인을 사용할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;엔티티(Mate) 목록 조회&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1682663260381&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Query(&quot;select distinct m&quot; +
        &quot; from Mate m&quot; +
        &quot; join fetch m.project p&quot; +
        &quot; join fetch m.reviewer r&quot; +
        &quot; left join fetch m.reviews s&quot;)
List&amp;lt;MateReviewResponse&amp;gt; findAllWithReviews();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;ToOne 관계에서는 Mate의 중복이 발생하지 않지만 &lt;span style=&quot;letter-spacing: 0px;&quot;&gt;OneToMany 관계(Mate - Review)에서는 DB에서 조회할 때 Review 갯수만큼 Mate가 중복된다. 이때는 &lt;span style=&quot;text-align: start;&quot;&gt;&lt;b&gt;distinct 키워드&lt;/b&gt;를 사용해 &lt;/span&gt;중복을 제거해준다. DB에서는 중복된 상태로 가져오지만 메모리에서 Mate의 중복을 제거한다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;fetch 조인 쿼리&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1682663490840&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;select distinct ...
from mate m
inner join project p
        on m.project_id = p.id 
inner join reviewer r
        on m.reviewer_id = r.id 
left outer join review w
        on m.id = w.mate_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;span style=&quot;color: #000000;&quot;&gt;하지만 필요한 경우에만 페치 조인을 사용하는 방법이 결국 프리젠테이션 계층의 영향이 데이터 접근 계층에 까지 영향을 주게된다. 화면별로 조회 쿼리를 만들어 사용하면 화면에 맞춘 리포지토리의 메소드가 증가할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;그리고 페치 조인으로 컬렉션을 조회할 때는 페이징이 불가능하다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;3. 컬렉션 조회&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;OneToMany 관계(&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;Mate - Review&lt;/span&gt;)가 있을때는 &lt;b&gt;페치 조인을 이용해 페이징 조회가 불가능&lt;/b&gt;하다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1682736127271&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Query(value = &quot;select m&quot; +
        &quot; from Mate m&quot; +
        &quot; join fetch m.project p&quot; +
        &quot; join fetch m.reviewer r&quot; +
        &quot; left join fetch m.reviews s&quot; +
        &quot; order by p.id&quot;,
        countQuery = &quot;select count(m.id) from Mate m&quot;)
Page&amp;lt;Mate&amp;gt; findAllWithReviews(PageRequest page);&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: #000000;&quot;&gt;Mate 하나에 Review가 여러개의 row 로 매핑되기 때문에 DB에서 조회하면 Review의 갯수 만큼 조회된다.&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;430&quot; data-origin-height=&quot;157&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cyOaUW/btsdbJGlClo/O23r90BKET09nKw1Lh7Ekk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cyOaUW/btsdbJGlClo/O23r90BKET09nKw1Lh7Ekk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cyOaUW/btsdbJGlClo/O23r90BKET09nKw1Lh7Ekk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcyOaUW%2FbtsdbJGlClo%2FO23r90BKET09nKw1Lh7Ekk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;157&quot; data-origin-width=&quot;430&quot; data-origin-height=&quot;157&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: #000000;&quot;&gt;Mate 를 기준으로 페이징을 해야 하지만 Review 를 기준으로 데이터가 조회되기 때문에&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;컬렉션을 페치 조인 할 때 페이징을 사용하면 하이버네이트는 &lt;b&gt;모든 데이터를 메모리에 올려서 페이징을 만들게 된다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;따라서 &lt;b&gt;컬렉션 페치 조인은 메모리 문제가 발생할 수 있다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1682736027860&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;QueryTranslatorImpl:HHH000104: firstResult/maxResults specified with collection fetch; applying in memory!&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;[최적화 방법 2] &lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;배치 사이즈로 지연로딩&lt;/span&gt; &lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;ToOne, OneToMany(컬렉션) 관계가 함께 있을 때 페이징을 하기 위해 &lt;b&gt;ToOne 은 페치 조인&lt;/b&gt;을 사용하고&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;컬렉션은 지연 로딩&lt;/b&gt;을 사용해야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1682739365065&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// repository
@Query(value = &quot;select m&quot; +
        &quot; from Mate m&quot; +
        &quot; join fetch m.project p&quot; +
        &quot; join fetch m.reviewer r&quot; +
        &quot; order by p.id&quot;,
        countQuery = &quot;select count(m.id) from Mate m&quot;)
Page&amp;lt;Mate&amp;gt; findAllMates(PageRequest page);

// service
Page&amp;lt;MateReviewResponse&amp;gt; response = mateRepository.findAllMates(PageRequest.of(0, 3))
        .map(MateReviewResponse::of);

// dto
public static MateReviewResponse of(Mate mate) {
    return new MateReviewResponse(
            mate.getId(),
            mate.getProject().getCodeStore().getName(),
            mate.getReviewer().getName(),
            mate.getReviews().stream()
                    .map(ReviewResponse::of)
                    .toList()
    );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;MateReviewResponse DTO에서 Review의 필드에 접근할 때 지연로딩이 발생하는데&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;컬렉션을 지연 로딩할 경우 Mate의 갯수만큼 Review 목록을 조회하는 쿼리가 추가로 실행되지만&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Review 프록시 객체를 &lt;b&gt;한꺼번에&lt;/b&gt; 설정한 size(&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;hibernate.default_batch_fetch_size , @BatchSize)&lt;/span&gt; 만큼 &lt;b&gt;IN 쿼리로 조회&lt;/b&gt;할 수 있다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1682739661982&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;select *
from review
where mate_id in (
    ?, ?, ?
);&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;글로벌 단위 설정&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1682739530807&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# application.properties
spring.jpa.properties.hibernate.default_batch_fetch_size=100&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;엔티티 단위 설정&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1682740210247&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@BatchSize(size = 100)
@OneToMany(mappedBy = &quot;mate&quot;)
private List&amp;lt;Review&amp;gt; reviews = new ArrayList&amp;lt;&amp;gt;();&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;[최적화 방법 3] &lt;/span&gt;QueryDSL 사용&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;QueryDSL 라이브러리를 사용하면 &lt;b&gt;페이징과&amp;nbsp; DTO 직접 조회&lt;/b&gt;를 편리하게 사용할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1682913919892&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public Page&amp;lt;MateReviewResponse&amp;gt; findAllMates(Pageable page) {
    List&amp;lt;MateReviewResponse&amp;gt; content = queryFactory
            .from(mate)
            .join(mate.project, project)
            .join(mate.reviewer, reviewer)
            .leftJoin(mate.reviews, review)
            .offset(page.getOffset())
            .limit(page.getPageSize())
            .transform(groupBy(mate.id)
                    .list(bean(MateReviewResponse.class,
                            mate.id,
                            project.codeStore.name.as(&quot;projectName&quot;),
                            reviewer.name.as(&quot;reviewerName&quot;),
                            list(bean(ReviewResponse.class,
                                    review.id,
                                    review.status)
                            ).as(&quot;reviews&quot;)))
            );

    JPAQuery&amp;lt;Long&amp;gt; countQuery = queryFactory
            .select(mate.count())
            .from(mate);

    return PageableExecutionUtils.getPage(content, page, countQuery::fetchFirst);
}&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;</description>
      <category>ORM/JPA</category>
      <category>JPA</category>
      <author>mandykr</author>
      <guid isPermaLink="true">https://mandykr.tistory.com/102</guid>
      <comments>https://mandykr.tistory.com/102#entry102comment</comments>
      <pubDate>Fri, 28 Apr 2023 13:22:48 +0900</pubDate>
    </item>
    <item>
      <title>[Kafka] 5. 스키마 레지스트리</title>
      <link>https://mandykr.tistory.com/101</link>
      <description>&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;2. 스키마 레지스트리&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. Avro&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;5. 스키마 호환성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;6. 스키마 레지스트리 실습&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;h3 data-ke-size=&quot;size23&quot;&gt;1. 스키마&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카프카의 데이터 흐름은 대부분 브로트캐스트 방식이기 때문에 프로듀서를 일방적으로 신뢰할 수밖에 없는 방식이다. 따라서 프로듀서 관리자는 카프카 토픽의 데이터를 컨슘하는 관리자에게 반드시 데이터 구조를 설명해야 한다. 데이터를 컨슘하는 부서가 많고 관리자가 자주 바뀐다면 그때마다 데이터 구조를 설명하는 일이 쉬운 일은 아닐것이다. 이와 같이 데이터를 컨슘하는 여러 부서에게 그 데이터에 대한 정확한 정의와 의미를 알려주는 역할을 스키마를 통해 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 외에도 비즈니스 변화 등에 따라 모델링이나 데이터 포맷이 변경되는 경우 새로운 필드를 추가하거나 필드 이름을 변경 또는 삭제해야 하는데 이러한 스키마의 진화도 카프카에서 지원하기 때문에 애플리케이션들이 별다른 영향없이 스키마를 변경할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 스키마 레지스트리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스키마 레지스트리는 카프카에서 스키마를 등록하고 관리하는 별도의 애플리케이션이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로듀서와 컨슈머는 스키마 레지스트리와 직접적으로 통신하는데 스키마 레지스트리가 지원하는 데이터 포맷을 사용해야 하며 대표적인 포맷이 Avro 이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;3. Avro&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Avro 는 시스템, 프로그래밍 언어, 프로세싱 프레임워크 사이에서 데이터 교환을 도와주는 오픈소스 직렬화 시스템이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스키마 레지스트리는 Avro, JSON, Protocol Buffer 포맷을 지원한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 이 중 한 가지를 선택해 사용해야 하며 컨플루언트는 다음과 같은 이유로 Avro 포맷 사용을 권장한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Avro는 JSON과 매핑된다.&lt;/li&gt;
&lt;li&gt;Avro는 매우 간결한 데이터 포맷이다.&lt;/li&gt;
&lt;li&gt;JSON은 메시지마다 필드 네임들이 포함되어 전송되므로 효율이 떨어진다.&lt;/li&gt;
&lt;li&gt;Avro는 바이너리 형태이므로 매우 빠르다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1681979180141&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
    &quot;namespace&quot;: &quot;student.avro&quot;,
    &quot;type&quot;: &quot;record&quot;,
    &quot;doc&quot;: &quot;This is an example of Avro.&quot;,
    &quot;name&quot;: &quot;Student&quot;,
    &quot;fields&quot;: [
        {&quot;name&quot;: &quot;name&quot;, &quot;type&quot;: &quot;string&quot;, &quot;doc&quot;: &quot;Name of the student&quot;},
        {&quot;name&quot;: &quot;class&quot;, &quot;type&quot;: &quot;int&quot;, &quot;doc&quot;: &quot;Class of the student&quot;},
    ]
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;namespace: 이름을 식별하는 문자열&lt;/li&gt;
&lt;li&gt;type: Avro는 record, enums, arrays, maps 등을 지원&lt;/li&gt;
&lt;li&gt;doc: 사용자들에게 이 스키마 정의에 대한 설명 제공(주석)&lt;/li&gt;
&lt;li&gt;name: 레코드의 이름을 나타내는 문자열, 필수값&lt;/li&gt;
&lt;li&gt;fields: JSON 배열로서 필드들의 리스트를 뜻함
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;name: 필드의 이름&lt;/li&gt;
&lt;li&gt;type: boolean, int, long, string 등의 데이터 타입 정의&lt;/li&gt;
&lt;li&gt;doc: 사용자들에게 해당 필드의 의미 설명(주석)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;4. 스키마 레지스트리 동작 방식&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로듀서와 컨슈머가 각자 스키마 레지스트리와 통신하면서 스키마의 정보를 주고받을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로듀서가 스키마 정보를 스키마 레지스트리에 등록함으로써 프로듀서가 전송하는 메시지의 크기와 컨슈머가 읽는 메시지의 크기도 줄일 수 있고 사전에 정의되지 않은 형식의 메시지는 전송할 수 없기 때문에 컨슈머에서 해당 처리에 대한 고민을 줄일 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1001&quot; data-origin-height=&quot;566&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ekT1Yh/btsa5V1aZLv/KISaax7oGfKcTG4p3LRyfK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ekT1Yh/btsa5V1aZLv/KISaax7oGfKcTG4p3LRyfK/img.png&quot; data-alt=&quot;출처:&amp;amp;amp;nbsp;https://docs.confluent.io/platform/current/schema-registry/fundamentals/index.html#schemas-subjects-and-topics&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ekT1Yh/btsa5V1aZLv/KISaax7oGfKcTG4p3LRyfK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FekT1Yh%2Fbtsa5V1aZLv%2FKISaax7oGfKcTG4p3LRyfK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1001&quot; height=&quot;566&quot; data-origin-width=&quot;1001&quot; data-origin-height=&quot;566&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처:&amp;amp;nbsp;https://docs.confluent.io/platform/current/schema-registry/fundamentals/index.html#schemas-subjects-and-topics&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;로컬 캐시에 없으면, 스키마 전송(등록)&lt;br /&gt;Avro 프로듀서는 컨플루언트에서 제공하는 io.confluent.kafka.serializers.KafkaAvroSerializer 라는 새로운 직렬화를 사용해 스키마 레지스트리의 스키마가 유효한지 여부를 확인한다. 만약 스키마가 확인되지 않으면 Avro 프로듀서는 스키마를 등록하고 캐시한다.&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;스키마 등록(ID 할당)&lt;br /&gt;스키마 레지스트리는 현 스키마가 저장소에 저장된 스키마와 동일한 것인지, 진화한 스키마인지 확인한다. 스키마 레지스트리 자체적으로 각 스키마에 대해 고유 ID를 할당한다. 이 ID는 순차적으로 1씩 증가하지만, 반드시 연속적이진 않다. 스키마에 문제가 없다면 스키마 레지스트리라는 프로듀서에게 고유 ID를 응답한다.&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;스키마 ID별 Avro 메시지 직렬화 전송&lt;br /&gt;프로듀서는 스키마 레지스트리로부터 받은 스키마 ID를 참고해 메시지를 카프카로 전송한다. 이때 프로듀서는 스키마의 전체 내용이 아닌 오로지 메시지와 스키마 ID만 보낸다. JSON은 key:value 형태로 전체 메시지를 전송해야 하지만, Avro를 사용하면 프로듀서가 스키마 ID와 밸류만 메시지로 보내게 되어 카프카로 전송하는 전체 메시지의 크기를 줄일 수 있다.&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;로컬 캐시에 없으면 스키마 ID별로 가져오기&lt;br /&gt;Avro 컨슈머는 스키마 ID로 컨플루언트에서 제공하는 io.confluent.kafka.serializers.KafkaAvroDeserializer 라는 새로운 역직렬화를 사용해 카프카의 토픽에 저장된 메시지를 읽는다. 이때 컨슈머가 스키마 ID를 갖고있지 않다면 스키마 레지스트리로부터 가져온다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;5. 스키마 호환성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스키마 레지스트리는 하나의 서브젝트에 대한 버전 정보별로 진화하는 각 스키마를 고유한 ID와 함께 관리해준다. 또한 스키마가 진화함에 따라 호환성 레벨을 검사해야 하는데, 스키마 레지스트리에서는 BACKWARD, FORWARD, FULL 호환성 레벨을 제공한다.&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;1) BACKWARD 호환성&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BACKWARD 호환성은 진화된 스키마를 적용한 컨슈머가 진화 전의 스키마가 적용된 프로듀서가 보낸 메시지를 읽을 수 있도록 허용하는 호환성이다.&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;902&quot; data-origin-height=&quot;442&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d0DNDY/btsbBdgTe6u/0gjcVoz8WhMwRJmGavU071/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d0DNDY/btsbBdgTe6u/0gjcVoz8WhMwRJmGavU071/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d0DNDY/btsbBdgTe6u/0gjcVoz8WhMwRJmGavU071/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd0DNDY%2FbtsbBdgTe6u%2F0gjcVoz8WhMwRJmGavU071%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;563&quot; height=&quot;276&quot; data-origin-width=&quot;902&quot; data-origin-height=&quot;442&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;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;&lt;b&gt;호환성 레벨&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;&lt;b&gt;지원 버전(컨슈머 기준)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;&lt;b&gt;변경 허용 항목&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;&lt;b&gt;스키마 업데이트 순서&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;BACKWARD&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;자신과 동일한 버전과 하나 아래의 하위 버전&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot; rowspan=&quot;2&quot;&gt;필드 삭제, &lt;br /&gt;기본 값이 지정된 필드 추가&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot; rowspan=&quot;2&quot;&gt;컨슈머 -&amp;gt; 프로듀서&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;BACKWARD_TRANSITIVE&lt;/td&gt;
&lt;td style=&quot;width: 25%;&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;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2) FORWARD 호환성&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FORWARD 호환성은 진화된 스키마가 적용된 프로듀서가 보낸 메시지를 진화 전의 스키마가 적용된 컨슈머가 읽을 수 있게 하는 호환성이다.&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;898&quot; data-origin-height=&quot;434&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/biAKWb/btsbChXp0Ww/tkQ0JU1pTkPvZbxwBJ5YrK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/biAKWb/btsbChXp0Ww/tkQ0JU1pTkPvZbxwBJ5YrK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/biAKWb/btsbChXp0Ww/tkQ0JU1pTkPvZbxwBJ5YrK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbiAKWb%2FbtsbChXp0Ww%2FtkQ0JU1pTkPvZbxwBJ5YrK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;563&quot; height=&quot;272&quot; data-origin-width=&quot;898&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;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;&lt;b&gt;호환성 레벨&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;&lt;b&gt;지원 버전(컨슈머 기준)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;&lt;b&gt;변경 허용 항목&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;&lt;b&gt;스키마 업데이트 순서&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;FORWARD&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;자신과 동일한 버전과 &lt;br /&gt;하나 위의 상위 버전&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot; rowspan=&quot;2&quot;&gt;필드 추가, &lt;br /&gt;기본 값이 지정된 필드 삭제&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot; rowspan=&quot;2&quot;&gt;프로듀서 -&amp;gt; 컨슈머&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;FORWARD_TRANSITIVE&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;자신과 동일한 버전을 포함한&lt;br /&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;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3) FULL 호환성&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FULL 호환성은 BACKWARD와 FORWARD 호환성 모두를 지원한다.&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;899&quot; data-origin-height=&quot;442&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dRFTUK/btsbCIUXTvG/0bWPjIT0iLrJnO8F1oVcV0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dRFTUK/btsbCIUXTvG/0bWPjIT0iLrJnO8F1oVcV0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dRFTUK/btsbCIUXTvG/0bWPjIT0iLrJnO8F1oVcV0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdRFTUK%2FbtsbCIUXTvG%2F0bWPjIT0iLrJnO8F1oVcV0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;563&quot; height=&quot;277&quot; data-origin-width=&quot;899&quot; data-origin-height=&quot;442&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;table style=&quot;border-collapse: collapse; width: 100%; height: 100px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 25%; height: 20px;&quot;&gt;&lt;b&gt;호환성 레벨&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 20px;&quot;&gt;&lt;b&gt;지원 버전(컨슈머 기준)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 20px;&quot;&gt;&lt;b&gt;변경 허용 항목&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 20px;&quot;&gt;&lt;b&gt;스키마 업데이트 순서&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 40px;&quot;&gt;
&lt;td style=&quot;width: 25%; height: 40px;&quot;&gt;FULL&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 40px;&quot;&gt;자신과 동일한 버전과 &lt;br /&gt;하나 위 또는 하나 아래 버전&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 80px;&quot; rowspan=&quot;2&quot;&gt;기본값이 지정된 필드 추가, 기본 값이 지정된 필드 삭제&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 80px;&quot; rowspan=&quot;2&quot;&gt;순서 상관 없음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 40px;&quot;&gt;
&lt;td style=&quot;width: 25%; height: 40px;&quot;&gt;FULL_TRANSITIVE&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 40px;&quot;&gt;자신과 동일한 버전을 포함한&lt;br /&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;h3 data-ke-size=&quot;size23&quot;&gt;6. 스키마 레지스트리 실습&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 구성&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Java 11&lt;/li&gt;
&lt;li&gt;Spring boot 2.7.9&lt;/li&gt;
&lt;li&gt;Oracle VM VirtualBox&lt;/li&gt;
&lt;li&gt;Confluent Kafka 7.2.1&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;VM에 Confluent Kafka 를 구성하고 주키퍼, 카프카, 스키마 레지스트리 순서로 기동한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IP: 192.168.56.101&lt;/p&gt;
&lt;pre id=&quot;code_1681796416862&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;zookeeper-server-start etc/kafka/zookeeper.properties
kafka-server-start etc/kafka/server.properties
schema-registry-start etc/schema-registry/schema-registry.properties&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;a style=&quot;background-color: #e6f5ff; color: #0070d1; text-align: start;&quot; href=&quot;https://developer.confluent.io/learn-kafka/spring/hands-on-cloud-schema-registry/&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;Confluent Cloud Schema Registry and Spring Boot&lt;/span&gt;&lt;/a&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;를 참고해 Spring boot 프로젝트를 설정한다.&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;build.gradle&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1681796144404&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;plugins {
    id &quot;com.github.davidmc24.gradle.plugin.avro&quot; version &quot;1.2.0&quot;
    id &quot;idea&quot;
}

repositories {
    gradlePluginPortal()
    mavenCentral()

    maven {
        url &quot;https://packages.confluent.io/maven&quot;
    }
}

dependencies {
    implementation 'org.springframework.kafka:spring-kafka'
    implementation 'org.apache.avro:avro:1.11.0'
    implementation &quot;io.confluent:kafka-avro-serializer:6.1.0&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;b&gt;application.properties&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1681796177658&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring.kafka.producer.bootstrap-servers=192.168.56.101:9092
spring.kafka.producer.properties.schema.registry.url=http://192.168.56.101:8081
spring.kafka.producer.key-serializer=org.apache.kafka.common.serialization.StringSerializer
spring.kafka.producer.value-serializer=io.confluent.kafka.serializers.KafkaAvroSerializer&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;src/main/avro/ReviewMessage.avsc&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1681796194669&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;type&quot;: &quot;record&quot;,
  &quot;name&quot;: &quot;ReviewMessage&quot;,
  &quot;namespace&quot;: &quot;com.papaco.avro.schema&quot;,
  &quot;fields&quot;: [
    {
      &quot;name&quot;: &quot;id&quot;,
      &quot;type&quot;: &quot;string&quot;
    },
    {
      &quot;name&quot;: &quot;mateId&quot;,
      &quot;type&quot;: &quot;string&quot;
    },
    {
      &quot;name&quot;: &quot;status&quot;,
      &quot;type&quot;: &quot;string&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;Gradle 탭에서 Tasks &amp;gt; source generation &amp;gt; generateAvro 를 실행하면 build/generated-main-avro-java 패키지에 ReviewMessage.avsc 파일을 사용해 ReviewMessage Java 클래스가 생성된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;kafkaTemplate 을 이용해 브로커로 메시지를 전송한다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; style=&quot;background-color: #ffffff; color: #000000;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;KafkaTemplate&amp;lt;String, ReviewMessage&amp;gt; kafkaTemplate;&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;a style=&quot;background-color: #e6f5ff; color: #0070d1; text-align: start;&quot; href=&quot;https://docs.confluent.io/platform/current/schema-registry/develop/api.html#sr-api-reference&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;Schema Registry API&lt;/span&gt;&lt;/a&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt; 를 통해 스키마 정보를 확인할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1681797194308&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ curl -s &quot;http://localhost:8081/subjects/papaco.review-value/versions/1&quot;
{&quot;subject&quot;:&quot;papaco.review-value&quot;,&quot;version&quot;:1,&quot;id&quot;:1,&quot;schema&quot;:&quot;{\&quot;type\&quot;:\&quot;record\&quot;,\&quot;name\&quot;:\&quot;ReviewMessage\&quot;,\&quot;namespace\&quot;:\&quot;com.papaco.avro.schema\&quot;,\&quot;fields\&quot;:[{\&quot;name\&quot;:\&quot;id\&quot;,\&quot;type\&quot;:{\&quot;type\&quot;:\&quot;string\&quot;,\&quot;avro.java.string\&quot;:\&quot;String\&quot;}},{\&quot;name\&quot;:\&quot;mateId\&quot;,\&quot;type\&quot;:{\&quot;type\&quot;:\&quot;string\&quot;,\&quot;avro.java.string\&quot;:\&quot;String\&quot;}},{\&quot;name\&quot;:\&quot;status\&quot;,\&quot;type\&quot;:{\&quot;type\&quot;:\&quot;string\&quot;,\&quot;avro.java.string\&quot;:\&quot;String\&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;kafka-avro-console-consumer 를 통해 전송된 데이터를 카프카 서버에서 확인할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1681797135245&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ kafka-avro-console-consumer \
 --topic papaco.review \
 --property schema.registry.url=http://localhost:8081 \
 --bootstrap-server localhost:9092 \
 --property key.deserializer=org.apache.kafka.common.serialization.StringDeserializer \
 --property print.key=true \
 --property key.separator=&quot;-&quot; \
 --from-beginning
e747975b-caa3-402a-bf10-a4aca10473f6-{&quot;id&quot;:&quot;e747975b-caa3-402a-bf10-a4aca10473f6&quot;,&quot;mateId&quot;:&quot;6fec2b25-9ad4-4dba-9f54-bc3c3305a6a5&quot;,&quot;status&quot;:&quot;DEMANDED&quot;}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://product.kyobobook.co.kr/detail/S000001932756&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;실전&amp;nbsp;카프카&amp;nbsp;개발부터&amp;nbsp;운영까지&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Kafka</category>
      <category>avro</category>
      <category>Schema Registry</category>
      <author>mandykr</author>
      <guid isPermaLink="true">https://mandykr.tistory.com/101</guid>
      <comments>https://mandykr.tistory.com/101#entry101comment</comments>
      <pubDate>Tue, 18 Apr 2023 13:28:21 +0900</pubDate>
    </item>
    <item>
      <title>[Kafka] Spring Kafka 구성(Broker, Producer, Consumer)</title>
      <link>https://mandykr.tistory.com/99</link>
      <description>&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;메시지 중복 저장을 방지하는 멱등성 프로듀서를 구성하고 카프카 클러스터는 3개의 브로커를 가지는 멀티 노드 클러스터로 만들어 가용성을 높인다. 컨슈머는 브로커 파티션의 갯수만큼 멀티 스레드로 구성해 데이터를 병렬로 처리하도록 한다.&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;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;1. 개발 환경&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;java 11&lt;/li&gt;
&lt;li&gt;spring boot 2.7.x&lt;/li&gt;
&lt;li&gt;gradle&lt;/li&gt;
&lt;li&gt;confluent kafka 3.x&lt;/li&gt;
&lt;li&gt;oracle VM&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;2. 카프카 환경 설정&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;confluent-kafka 설치&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1679371460034&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ wget https://packages.confluent.io/archive/7.2/confluent-community-7.2.1.tar.gz
$ tar xvf confluent-community-7.2.1.tar.gz&lt;/code&gt;&lt;/pre&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;힙 메모리,&amp;nbsp; path 설정&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1679372164734&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ mv confluent-7.2.1 confluent

$ vi ~/.bashrc
export KAFKA_HEAP_OPTS=&quot;-Xmx400m -Xms400m&quot;
export CONFLUENT_HOME=/home/ec2-user/confluent
export PATH=.:$PATH:$CONFLUENT_HOME/bin

$ source ~/.bashrc
$ echo $KAFKA_HEAP_OPTS
$ echo $CONFLUENT_HOME&lt;/code&gt;&lt;/pre&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_1679373618577&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[zookeeper]
$ vi etc/kafka/zookeeper.properties
dataDir=/home/mandy/data/zookeeper-logs

$ mkdir /home/mandy/data/zookeeper-logs&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;kafka broker 실행 옵션 설정&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1679373751331&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[kafka-x]
$ vi etc/kafka/server.properties
broker.id=0
listeners=PLAINTEXT://:9092
advertised.listeners=PLAINTEXT://kafka-x-IP:9092
log.dirs=/home/mandy/data/kafka-logs
zookeeper.connect=zookeeper:2181

$ mkdir -p /home/mandy/data/kafka-logs&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;schema registry 실행 옵션 설정&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1679375571194&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ vi etc/schema-registry/schema-registry.properties&lt;/code&gt;&lt;/pre&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;zookeeper, broker, schema registry 구동&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1679374923732&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[zookeeper]
$ zookeeper-server-start etc/kafka/zookeeper.properties

[kafka-x]
$ kafka-server-start etc/kafka/server_0.properties
$ kafka-server-start etc/kafka/server_1.properties
$ kafka-server-start etc/kafka/server_2.properties

[zookeeper]
$ schema-registry-start etc/schema-registry/schema-registry.properties&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;b&gt;파일 시스템 구조&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1679375261955&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;home
├── mandy
│&amp;nbsp;&amp;nbsp; ├── data
│&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp; ├── zookeeper-logs
│&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp; ├── kafka-logs
│&amp;nbsp;&amp;nbsp; ├── confluent
│&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp; ├── bin
│&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp; ├── kafka-configs
│&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp; ├── kafka-console-consumer
│&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp; ├── kafka-console-producer
│&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp; ├── kafka-server-start
│&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp; ├── kafka-topics
│&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp; ├── zookeeper-server-start
│&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp; ├── schema-registry-start
│&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp; ├── ksql-server-start
│&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp; ├── etc
│&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp; ├── kafka
│&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp; ├── server.properties
│&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp; └── zookeeper.properties
│&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp; ├── lib
│&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp; ├── LICENSE
│&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp; ├── logs
│&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp; ├── server.log&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;h2 data-ke-size=&quot;size26&quot;&gt;3. 개발&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1) 토픽&lt;/h3&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_1681781664366&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ kafka-topics --bootstrap-server localhost:9092 \
--create \
--topic papaco.review \
--partitions 3 \
--replication-factor 3
--config min.insync.replicas=2&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;● partitions 3&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://mandykr.tistory.com/95#:~:text=3)-,%EB%A9%94%EC%8B%9C%EC%A7%80%20%ED%82%A4%20%EC%82%AC%EC%9A%A9%20%EC%97%AC%EB%B6%80,-%EB%A9%94%EC%8B%9C%EC%A7%80%20%ED%82%A4%EB%A5%BC%20%EC%82%AC%EC%9A%A9%ED%95%98%EB%A9%B4&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;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;● replication-factor 3&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://mandykr.tistory.com/95#:~:text=4.-,%EB%B3%B5%EC%A0%9C%EC%99%80%20%EB%A6%AC%EB%8D%94%20/%20%ED%8C%94%EB%A1%9C%EC%9B%8C%20%ED%8C%8C%ED%8B%B0%EC%85%98,-%EB%8D%B0%EC%9D%B4%ED%84%B0%20%EB%B3%B5%EC%A0%9C%EB%8A%94%20%EC%B9%B4%ED%94%84%EC%B9%B4%EB%A5%BC&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;클러스터의 브로커 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;&lt;b&gt;최소 동기화 리플리카&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;● min.insync.replicas=2&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://mandykr.tistory.com/95#:~:text=4.-,ISR,-ISR(In%2DSync&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;ISR&lt;/a&gt;의 2개 이상의 파티션에 적재되었음을 확인한다. 리더 파티션과 1개의 팔로워 파티션에 데이터가 정상적으로 적재되었음을 보장한다. min.insync.replicas 옵션은 프로듀서의 acks를 all로 설정한 경우에만 유효하다.&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;&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_1681781689924&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ kafka-configs --bootstrap-server localhost:9092 --entity-type topics --entity-name papaco.review --describe --all
All configs for topic papaco.review are:
  compression.type=producer sensitive=false synonyms={DEFAULT_CONFIG:compression.type=producer}
  leader.replication.throttled.replicas= sensitive=false synonyms={}
  message.downconversion.enable=true sensitive=false synonyms={DEFAULT_CONFIG:log.message.downconversion.enable=true}
  min.insync.replicas=2 sensitive=false synonyms={DYNAMIC_TOPIC_CONFIG:min.insync.replicas=2, DEFAULT_CONFIG:min.insync.replicas=1}
  segment.jitter.ms=0 sensitive=false synonyms={}
  cleanup.policy=delete sensitive=false synonyms={DEFAULT_CONFIG:log.cleanup.policy=delete}
  flush.ms=9223372036854775807 sensitive=false synonyms={}
  follower.replication.throttled.replicas= sensitive=false synonyms={}
  segment.bytes=1073741824 sensitive=false synonyms={STATIC_BROKER_CONFIG:log.segment.bytes=1073741824, DEFAULT_CONFIG:log.segment.bytes=1073741824}
  retention.ms=604800000 sensitive=false synonyms={}
  flush.messages=9223372036854775807 sensitive=false synonyms={DEFAULT_CONFIG:log.flush.interval.messages=9223372036854775807}
  message.format.version=3.0-IV1 sensitive=false synonyms={DEFAULT_CONFIG:log.message.format.version=3.0-IV1}
  max.compaction.lag.ms=9223372036854775807 sensitive=false synonyms={DEFAULT_CONFIG:log.cleaner.max.compaction.lag.ms=9223372036854775807}
  file.delete.delay.ms=60000 sensitive=false synonyms={DEFAULT_CONFIG:log.segment.delete.delay.ms=60000}
  max.message.bytes=1048588 sensitive=false synonyms={DEFAULT_CONFIG:message.max.bytes=1048588}
  min.compaction.lag.ms=0 sensitive=false synonyms={DEFAULT_CONFIG:log.cleaner.min.compaction.lag.ms=0}
  message.timestamp.type=CreateTime sensitive=false synonyms={DEFAULT_CONFIG:log.message.timestamp.type=CreateTime}
  preallocate=false sensitive=false synonyms={DEFAULT_CONFIG:log.preallocate=false}
  min.cleanable.dirty.ratio=0.5 sensitive=false synonyms={DEFAULT_CONFIG:log.cleaner.min.cleanable.ratio=0.5}
  index.interval.bytes=4096 sensitive=false synonyms={DEFAULT_CONFIG:log.index.interval.bytes=4096}
  unclean.leader.election.enable=false sensitive=false synonyms={DEFAULT_CONFIG:unclean.leader.election.enable=false}
  retention.bytes=-1 sensitive=false synonyms={DEFAULT_CONFIG:log.retention.bytes=-1}
  delete.retention.ms=86400000 sensitive=false synonyms={DEFAULT_CONFIG:log.cleaner.delete.retention.ms=86400000}
  segment.ms=604800000 sensitive=false synonyms={}
  message.timestamp.difference.max.ms=9223372036854775807 sensitive=false synonyms={DEFAULT_CONFIG:log.message.timestamp.difference.max.ms=9223372036854775807}
  segment.index.bytes=10485760 sensitive=false synonyms={DEFAULT_CONFIG:log.index.size.max.bytes=10485760}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://mandykr.tistory.com/95#:~:text=1)-,%EB%A6%AC%EB%8D%94%20%ED%8C%8C%ED%8B%B0%EC%85%98%20%EC%84%A0%EC%B6%9C,-(1)%20Preferred%20Leader&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;● unclean.leader.election.enable=false&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리더 파티션의 브로커에 장애가 발생한 경우, ISR이 아닌 팔로워 파티션을 리더 파티션으로 선출하지 않고 리더 파티션이 존재하는 브로커가 다시 실행될 때까지 기다린다.&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;h3 data-ke-size=&quot;size23&quot;&gt;2) 프로듀서&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;(1) 프로듀서 설정&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;application.properties&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1681785598994&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring.kafka.producer.bootstrap-servers=192.168.56.101:9092
spring.kafka.producer.properties.schema.registry.url=http://192.168.56.101:8081
spring.kafka.producer.key-serializer=org.apache.kafka.common.serialization.StringSerializer
spring.kafka.producer.value-serializer=io.confluent.kafka.serializers.KafkaAvroSerializer
spring.kafka.producer.properties.enable.idempotence=true&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;ProducerConfig&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(org.apache.kafka.clients.producer.ProducerConfig)&lt;/p&gt;
&lt;pre id=&quot;code_1681785718234&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ProducerConfig values: 
	acks = -1
	batch.size = 16384
	bootstrap.servers = [192.168.56.101:9092]
	buffer.memory = 33554432
	client.dns.lookup = use_all_dns_ips
	client.id = producer-1
	compression.type = none
	connections.max.idle.ms = 540000
	delivery.timeout.ms = 120000
	enable.idempotence = true
	interceptor.classes = []
	key.serializer = class org.apache.kafka.common.serialization.StringSerializer
	linger.ms = 0
	max.block.ms = 60000
	max.in.flight.requests.per.connection = 5
	max.request.size = 1048576
	metadata.max.age.ms = 300000
	metadata.max.idle.ms = 300000
	metric.reporters = []
	metrics.num.samples = 2
	metrics.recording.level = INFO
	metrics.sample.window.ms = 30000
	partitioner.class = class org.apache.kafka.clients.producer.internals.DefaultPartitioner
	receive.buffer.bytes = 32768
	reconnect.backoff.max.ms = 1000
	reconnect.backoff.ms = 50
	request.timeout.ms = 30000
	retries = 2147483647
	retry.backoff.ms = 100
	sasl.client.callback.handler.class = null
	sasl.jaas.config = null
	sasl.kerberos.kinit.cmd = /usr/bin/kinit
	sasl.kerberos.min.time.before.relogin = 60000
	sasl.kerberos.service.name = null
	sasl.kerberos.ticket.renew.jitter = 0.05
	sasl.kerberos.ticket.renew.window.factor = 0.8
	sasl.login.callback.handler.class = null
	sasl.login.class = null
	sasl.login.connect.timeout.ms = null
	sasl.login.read.timeout.ms = null
	sasl.login.refresh.buffer.seconds = 300
	sasl.login.refresh.min.period.seconds = 60
	sasl.login.refresh.window.factor = 0.8
	sasl.login.refresh.window.jitter = 0.05
	sasl.login.retry.backoff.max.ms = 10000
	sasl.login.retry.backoff.ms = 100
	sasl.mechanism = GSSAPI
	sasl.oauthbearer.clock.skew.seconds = 30
	sasl.oauthbearer.expected.audience = null
	sasl.oauthbearer.expected.issuer = null
	sasl.oauthbearer.jwks.endpoint.refresh.ms = 3600000
	sasl.oauthbearer.jwks.endpoint.retry.backoff.max.ms = 10000
	sasl.oauthbearer.jwks.endpoint.retry.backoff.ms = 100
	sasl.oauthbearer.jwks.endpoint.url = null
	sasl.oauthbearer.scope.claim.name = scope
	sasl.oauthbearer.sub.claim.name = sub
	sasl.oauthbearer.token.endpoint.url = null
	security.protocol = PLAINTEXT
	security.providers = null
	send.buffer.bytes = 131072
	socket.connection.setup.timeout.max.ms = 30000
	socket.connection.setup.timeout.ms = 10000
	ssl.cipher.suites = null
	ssl.enabled.protocols = [TLSv1.2, TLSv1.3]
	ssl.endpoint.identification.algorithm = https
	ssl.engine.factory.class = null
	ssl.key.password = null
	ssl.keymanager.algorithm = SunX509
	ssl.keystore.certificate.chain = null
	ssl.keystore.key = null
	ssl.keystore.location = null
	ssl.keystore.password = null
	ssl.keystore.type = JKS
	ssl.protocol = TLSv1.3
	ssl.provider = null
	ssl.secure.random.implementation = null
	ssl.trustmanager.algorithm = PKIX
	ssl.truststore.certificates = null
	ssl.truststore.location = null
	ssl.truststore.password = null
	ssl.truststore.type = JKS
	transaction.timeout.ms = 60000
	transactional.id = null
	value.serializer = class io.confluent.kafka.serializers.KafkaAvroSerializer&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;&lt;a href=&quot;https://mandykr.tistory.com/96#:~:text=2)-,acks%20%EC%98%B5%EC%85%98,-acks%20%EC%98%B5%EC%85%98%EC%9C%BC%EB%A1%9C%20%ED%94%84%EB%A1%9C%EB%93%80%EC%84%9C%EA%B0%80&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;acks 옵션&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;● ack=all&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;ack 를 all로 설정하면 min.insync.replicas 개수 만큼의 Replicator에 복제를 수행한 뒤에 리더 브로커가 보내는 Ack 를 받은 후 다음 메시지를 전송한다. 1이나 0에 비해 프로듀서가 클러스터로 데이터를 저장하는 데에 시간이 오래 걸리지만 클러스터 또는 네트워크에 이상이 생겼을 경우 복구할 확률이 가장 높다.&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;a href=&quot;https://mandykr.tistory.com/96#:~:text=3.-,Partitioner,-%ED%8C%8C%ED%8B%B0%EC%85%94%EB%84%88%EB%8A%94%20ProducerRecord%EB%A5%BC&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;span style=&quot;color: #333333;&quot;&gt;● partitioner.class = class org.apache.kafka.clients.producer.internals.DefaultPartitioner&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;DefaultPartitioner는 키를 가지는 메시지의 경우 키 값을 Hashing하여 키 값에 따라 파티션 별로 균일하게 전송한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&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;a href=&quot;https://mandykr.tistory.com/96#:~:text=4.-,Accumulator,-Record%20Accumulator%EB%8A%94&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;span style=&quot;color: #333333;&quot;&gt;● buffer.memory = 33554432&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;● linger.ms = 0&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;● batch.size = 16384&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;전반적인 Producer와 Broker간의 전송이 느리다면 linger.ms를 0보다 크게 설정하여 Sender Thread가 하나의 Record Batch를 가져갈 때 일정 시간 대기하여 Record Batch에 메시지를 보다 많이 채울 수 있도록 적용할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;linger.ms는 보통 20ms 이하로 설정을 권장한다.&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;a href=&quot;https://mandykr.tistory.com/96#:~:text=3)-,%EB%A9%94%EC%8B%9C%EC%A7%80%C2%A0%EC%A0%84%EC%86%A1/%EC%9E%AC%C2%A0%EC%A0%84%EC%86%A1%C2%A0%EC%8B%9C%EA%B0%84%C2%A0%ED%8C%8C%EB%9D%BC%EB%AF%B8%ED%84%B0,-max.block.ms&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;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;●&amp;nbsp;&lt;/span&gt;retries&amp;nbsp;=&amp;nbsp;2147483647&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;●&amp;nbsp;&lt;/span&gt;request.timeout.ms = 30000&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;●&amp;nbsp;&lt;/span&gt;linger.ms = 0&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;●&amp;nbsp;&lt;/span&gt;retry.backoff.ms&amp;nbsp;=&amp;nbsp;100&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;●&amp;nbsp;&lt;/span&gt;delivery.timeout.ms&amp;nbsp;=&amp;nbsp;120000&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot;&gt;Sender Thread가 0ms(linger.ms)만큼 대기한 후에 Record Accumulator에서 레코드를 배치별로 가져간다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; text-align: left;&quot;&gt;Sender Thread는 배치를 브로커로 전송하고 30000ms(&lt;/span&gt;request.timeout.ms)만큼 대기한 후&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;리더 브로커로부터 에러 메시지를 받으면 100ms(retry.backoff.ms) 후에 재전송을 시도한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;이와 같은 방식으로 재전송을 2147483647회(retries) 시도하고 더 이상 retry 시도 하지 않는다.&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;a href=&quot;https://mandykr.tistory.com/96#:~:text=6.-,%EB%A9%B1%EB%93%B1%EC%84%B1(idempotence)%20%ED%94%84%EB%A1%9C%EB%93%80%EC%84%9C,-Producer%EB%8A%94%20%EB%B8%8C%EB%A1%9C%EC%BB%A4%EB%A1%9C&quot; target=&quot;_blank&quot; rel=&quot;noopener&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;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;●&amp;nbsp;&lt;/span&gt;enable.idempotence=true&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;&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;span style=&quot;color: #333333;&quot;&gt;멱등성 프로듀서는 기본 프로듀서와 달리 데이터를 브로커로 전달할 때 프로듀서 PID와 시퀀스 넘버를 함께 전달한다. 그러면 브로커는 프로듀서의 PID와 시퀀스 넘버를 확인하여 동이한 메시지의 적재 요청이 오더라도 단 한번만 데이터를 적재한다.&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;(2) 데이터 포맷&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://mandykr.tistory.com/101#:~:text=%ED%8F%AC%EB%A7%B7%EC%9D%B4%20Avro%20%EC%9D%B4%EB%8B%A4.-,3.%20Avro,-Avro%20%EB%8A%94%20%EC%8B%9C%EC%8A%A4%ED%85%9C&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Avro&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;● value.serializer&amp;nbsp;=&amp;nbsp;class&amp;nbsp;io.confluent.kafka.serializers.KafkaAvroSerializer&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;KafkaAvroSerializerConfig&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;(io.confluent.kafka.serializers.KafkaAvroSerializerConfig)&lt;/p&gt;
&lt;pre id=&quot;code_1681790904629&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;KafkaAvroSerializerConfig values: 
	auto.register.schemas = true
	basic.auth.credentials.source = URL
	basic.auth.user.info = [hidden]
	bearer.auth.credentials.source = STATIC_TOKEN
	bearer.auth.token = [hidden]
	key.subject.name.strategy = class io.confluent.kafka.serializers.subject.TopicNameStrategy
	latest.compatibility.strict = true
	max.schemas.per.subject = 1000
	proxy.host = 
	proxy.port = -1
	schema.reflection = false
	schema.registry.basic.auth.user.info = [hidden]
	schema.registry.ssl.cipher.suites = null
	schema.registry.ssl.enabled.protocols = [TLSv1.2, TLSv1.3]
	schema.registry.ssl.endpoint.identification.algorithm = https
	schema.registry.ssl.engine.factory.class = null
	schema.registry.ssl.key.password = null
	schema.registry.ssl.keymanager.algorithm = SunX509
	schema.registry.ssl.keystore.certificate.chain = null
	schema.registry.ssl.keystore.key = null
	schema.registry.ssl.keystore.location = null
	schema.registry.ssl.keystore.password = null
	schema.registry.ssl.keystore.type = JKS
	schema.registry.ssl.protocol = TLSv1.3
	schema.registry.ssl.provider = null
	schema.registry.ssl.secure.random.implementation = null
	schema.registry.ssl.trustmanager.algorithm = PKIX
	schema.registry.ssl.truststore.certificates = null
	schema.registry.ssl.truststore.location = null
	schema.registry.ssl.truststore.password = null
	schema.registry.ssl.truststore.type = JKS
	schema.registry.url = [http://192.168.56.101:8081]
	use.latest.version = false
	value.subject.name.strategy = class io.confluent.kafka.serializers.subject.TopicNameStrategy&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;a href=&quot;https://mandykr.tistory.com/101#:~:text=%EC%9D%98%EB%AF%B8%20%EC%84%A4%EB%AA%85(%EC%A3%BC%EC%84%9D)-,4.%20%EC%8A%A4%ED%82%A4%EB%A7%88%20%EB%A0%88%EC%A7%80%EC%8A%A4%ED%8A%B8%EB%A6%AC%20%EB%8F%99%EC%9E%91%20%EB%B0%A9%EC%8B%9D,-%ED%94%84%EB%A1%9C%EB%93%80%EC%84%9C%EC%99%80%20%EC%BB%A8%EC%8A%88%EB%A8%B8%EA%B0%80%20%EA%B0%81%EC%9E%90&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;● schema.registry.url&amp;nbsp;=&amp;nbsp;[http://192.168.56.101:8081]&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;key.subject.name.strategy&amp;nbsp;=&amp;nbsp;class&amp;nbsp;io.confluent.kafka.serializers.subject.TopicNameStrategy&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;value.subject.name.strategy&amp;nbsp;=&amp;nbsp;class&amp;nbsp;io.confluent.kafka.serializers.subject.TopicNameStrategy&lt;/p&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;(3) 메시지 전송&lt;/h4&gt;
&lt;pre id=&quot;code_1685766455636&quot; class=&quot;aspectj&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 final KafkaTemplate&amp;lt;String, ReviewMessage&amp;gt; kafkaTemplate;

public void sendMessage(ReviewEvent event) {
    // ...
    
    kafkaTemplate.send(topic, key, message).addCallback(new ListenableFutureCallback&amp;lt;SendResult&amp;lt;String, ReviewMessage&amp;gt;&amp;gt;() {
        @Override
        public void onFailure(Throwable ex) {
            log.error(&quot;kafka send error: {}&quot;, ex.getMessage());
        }

        @Override
        public void onSuccess(SendResult&amp;lt;String, ReviewMessage&amp;gt; result) {
            log.info(&quot;kafka send result: {}&quot;, result.toString());
        }
    });
}&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;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3) 컨슈머&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;(1) 컨슈머 설정&lt;/h4&gt;
&lt;p style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;application.properties&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1685774328614&quot; class=&quot;stylus&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring.kafka.consumer.bootstrap-servers=192.168.56.101:9092,192.168.56.101:9093,192.168.56.101:9094
spring.kafka.consumer.properties.schema.registry.url=http://192.168.56.101:8081
spring.kafka.consumer.key-deserializer=org.apache.kafka.common.serialization.StringDeserializer
spring.kafka.consumer.value-deserializer=io.confluent.kafka.serializers.KafkaAvroDeserializer
spring.kafka.consumer.properties.allow.auto.create.topics=false
spring.kafka.consumer.properties.auto.offset.reset=earliest
spring.kafka.consumer.properties.specific.avro.reader=true
spring.kafka.listener.type=BATCH
spring.kafka.listener.ack-mode=MANUAL&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;b&gt;ConsumerConfig&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;(org.apache.kafka.clients.consumer.ConsumerConfig)&lt;/p&gt;
&lt;pre id=&quot;code_1685774328615&quot; class=&quot;nix&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ConsumerConfig values: 
	allow.auto.create.topics = false
	auto.commit.interval.ms = 5000
	auto.offset.reset = earliest
	bootstrap.servers = [192.168.56.101:9092, 192.168.56.101:9093, 192.168.56.101:9094]
	check.crcs = true
	client.dns.lookup = use_all_dns_ips
	client.id = consumer-papaco-review-project-query-1
	client.rack = 
	connections.max.idle.ms = 540000
	default.api.timeout.ms = 60000
	enable.auto.commit = false
	exclude.internal.topics = true
	fetch.max.bytes = 52428800
	fetch.max.wait.ms = 500
	fetch.min.bytes = 1
	group.id = papaco-review-project-query
	group.instance.id = null
	heartbeat.interval.ms = 3000
	interceptor.classes = []
	internal.leave.group.on.close = true
	internal.throw.on.fetch.stable.offset.unsupported = false
	isolation.level = read_uncommitted
	key.deserializer = class org.apache.kafka.common.serialization.StringDeserializer
	max.partition.fetch.bytes = 1048576
	max.poll.interval.ms = 300000
	max.poll.records = 500
	metadata.max.age.ms = 300000
	metric.reporters = []
	metrics.num.samples = 2
	metrics.recording.level = INFO
	metrics.sample.window.ms = 30000
	partition.assignment.strategy = [class org.apache.kafka.clients.consumer.RangeAssignor, class org.apache.kafka.clients.consumer.CooperativeStickyAssignor]
	receive.buffer.bytes = 65536
	reconnect.backoff.max.ms = 1000
	reconnect.backoff.ms = 50
	request.timeout.ms = 30000
	retry.backoff.ms = 100
	sasl.client.callback.handler.class = null
	sasl.jaas.config = null
	sasl.kerberos.kinit.cmd = /usr/bin/kinit
	sasl.kerberos.min.time.before.relogin = 60000
	sasl.kerberos.service.name = null
	sasl.kerberos.ticket.renew.jitter = 0.05
	sasl.kerberos.ticket.renew.window.factor = 0.8
	sasl.login.callback.handler.class = null
	sasl.login.class = null
	sasl.login.connect.timeout.ms = null
	sasl.login.read.timeout.ms = null
	sasl.login.refresh.buffer.seconds = 300
	sasl.login.refresh.min.period.seconds = 60
	sasl.login.refresh.window.factor = 0.8
	sasl.login.refresh.window.jitter = 0.05
	sasl.login.retry.backoff.max.ms = 10000
	sasl.login.retry.backoff.ms = 100
	sasl.mechanism = GSSAPI
	sasl.oauthbearer.clock.skew.seconds = 30
	sasl.oauthbearer.expected.audience = null
	sasl.oauthbearer.expected.issuer = null
	sasl.oauthbearer.jwks.endpoint.refresh.ms = 3600000
	sasl.oauthbearer.jwks.endpoint.retry.backoff.max.ms = 10000
	sasl.oauthbearer.jwks.endpoint.retry.backoff.ms = 100
	sasl.oauthbearer.jwks.endpoint.url = null
	sasl.oauthbearer.scope.claim.name = scope
	sasl.oauthbearer.sub.claim.name = sub
	sasl.oauthbearer.token.endpoint.url = null
	security.protocol = PLAINTEXT
	security.providers = null
	send.buffer.bytes = 131072
	session.timeout.ms = 45000
	socket.connection.setup.timeout.max.ms = 30000
	socket.connection.setup.timeout.ms = 10000
	ssl.cipher.suites = null
	ssl.enabled.protocols = [TLSv1.2, TLSv1.3]
	ssl.endpoint.identification.algorithm = https
	ssl.engine.factory.class = null
	ssl.key.password = null
	ssl.keymanager.algorithm = SunX509
	ssl.keystore.certificate.chain = null
	ssl.keystore.key = null
	ssl.keystore.location = null
	ssl.keystore.password = null
	ssl.keystore.type = JKS
	ssl.protocol = TLSv1.3
	ssl.provider = null
	ssl.secure.random.implementation = null
	ssl.trustmanager.algorithm = PKIX
	ssl.truststore.certificates = null
	ssl.truststore.location = null
	ssl.truststore.password = null
	ssl.truststore.type = JKS
	value.deserializer = class io.confluent.kafka.serializers.KafkaAvroDeserializer&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;b&gt;bootstrap-servers&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;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;●&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;spring.kafka.consumer.bootstrap-servers=192.168.56.101:9092,192.168.56.101:9093,192.168.56.101:9094&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;bootstrap-servers 는 브로커 리스너들의 리스트이고 브로커들의 메타정보를 가져오는데 사용된다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;카프카 브로커들은 토픽 파티션의 Leader와 Follower들의 메타 정보를 서로 공유하고 있는데 카프카 컨슈머는 bootstrap-servers 에 접속해 이 메타 정보를 가져오고 메타 정보를 통해 토픽의 파티션이 있는 브로커로 다시 접속하게 된다.&lt;/p&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;(프로듀서도 동일)&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: #009a87;&quot;&gt;&lt;a style=&quot;color: #009a87;&quot; href=&quot;https://mandykr.tistory.com/97#:~:text=2)%20auto.offset.reset&quot;&gt;auto.offset.reset&lt;/a&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;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;●&lt;/span&gt;&amp;nbsp;auto.offset.reset&amp;nbsp;=&amp;nbsp;earliest&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;__consumer_offsets에 컨슈머 그룹이 해당 토픽의 파티션별로 offset 정보를 가지고 있지 않을 때 컨슈머가 접속하면 파티션의 처음 offset 부터 가져오도록 설정한다.&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: #009a87;&quot;&gt;&lt;a style=&quot;color: #009a87;&quot; href=&quot;https://mandykr.tistory.com/97#:~:text=poll%20%EC%88%98%ED%96%89()%20%EC%99%84%EB%A3%8C-,Consumer%20Fetcher%20%EC%84%A4%EC%A0%95%20%ED%8C%8C%EB%9D%BC%EB%AF%B8%ED%84%B0,-%ED%8C%8C%EB%9D%BC%EB%AF%B8%ED%84%B0&quot;&gt;Consumer Fetcher 설정 파라미터&lt;/a&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&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;●&lt;/span&gt;&amp;nbsp;fetch.min.bytes&amp;nbsp;=&amp;nbsp;1&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;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;●&lt;/span&gt;&amp;nbsp;fetch.max.wait.ms&amp;nbsp;=&amp;nbsp;500&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;●&lt;/span&gt;&amp;nbsp;fetch.max.bytes&amp;nbsp;=&amp;nbsp;52428800&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;●&lt;/span&gt;&amp;nbsp;max.partition.fetch.bytes&amp;nbsp;=&amp;nbsp;1048576&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;●&lt;/span&gt;&amp;nbsp;max.poll.records&amp;nbsp;=&amp;nbsp;500&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;●&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;BatchConsumerAwareMessageListener.DEFAULT_COMMIT_TIMEOUT = 30; (스프링 카프카 컨슈머)&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Fetcher가 Linked Queue에서 가져오는 레코드의 개수는&lt;span&gt;&amp;nbsp;&lt;/span&gt;500(max.poll.records)으로 제한한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Linked Queue에 데이터가 없으면 ConsumerNetworkClient는 30초(DEFAULT_COMMIT_TIMEOUT)까지 Broker에 메시지를 요청한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;ConsumerNetworkClient가 브로커에서 가져오는 레코드 배치의 최대 크기는&lt;span&gt;&amp;nbsp;&lt;/span&gt;max.partition.fetch.bytes와&lt;span&gt;&amp;nbsp;&lt;/span&gt;fetch.max.bytes로 설정한다.&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: #009a87;&quot;&gt;&lt;a style=&quot;color: #009a87;&quot; href=&quot;https://mandykr.tistory.com/97#:~:text=lag%EC%9D%B4%EB%9D%BC%20%EB%B6%80%EB%A5%B8%EB%8B%A4.-,4)%20%EB%A6%AC%EB%B0%B8%EB%9F%B0%EC%8A%A4%20%EA%B4%80%EB%A0%A8%20%EC%BB%A8%EC%8A%88%EB%A8%B8%20%ED%8C%8C%EB%9D%BC%EB%AF%B8%ED%84%B0,-%ED%8C%8C%EB%9D%BC%EB%AF%B8%ED%84%B0&quot;&gt;리밸런스&amp;nbsp;관련&amp;nbsp;컨슈머&amp;nbsp;파라미터&lt;/a&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;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;●&lt;/span&gt;&amp;nbsp;heartbeat.interval.ms&amp;nbsp;=&amp;nbsp;3000&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;●&lt;/span&gt;&amp;nbsp;session.timeout.ms&amp;nbsp;=&amp;nbsp;45000&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;●&lt;/span&gt;&amp;nbsp;max.poll.interval.ms&amp;nbsp;=&amp;nbsp;300000&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;컨슈머는 3초(heartbeat.interval.ms)마다 브로커로 heartbeat을 보낸다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;컨슈머가 45초(session.timeout.ms)만큼 heartbeat을 보내지 않거나,&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;300초(max.poll.interval.ms)만큼 poll() 호출을 하지 않으면 브로커(코디네이터)는 리밸런스 명령을 리더 컨슈머에게 보낸다.&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: #009a87;&quot;&gt;&lt;a style=&quot;color: #009a87;&quot; href=&quot;https://mandykr.tistory.com/97#:~:text=(4)%20Cooperative%20Sticky%20%ED%95%A0%EB%8B%B9%20%EC%A0%84%EB%9E%B5&quot;&gt;컨슈머 파티션 할당 전략(&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;Cooperative Sticky)&lt;/span&gt;&lt;/a&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;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;●&lt;/span&gt;&amp;nbsp;partition.assignment.strategy = [class org.apache.kafka.clients.consumer.CooperativeStickyAssignor]&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: #009a87;&quot;&gt;&lt;a style=&quot;color: #009a87;&quot; href=&quot;https://mandykr.tistory.com/97#:~:text=%EB%B0%9C%EC%83%9D%ED%95%A0%20%EA%B0%80%EB%8A%A5%EC%84%B1%20%EC%9E%88%EB%8B%A4.-,Cooperative%20%EB%AA%A8%EB%93%9C,-Rebalance%20%EC%88%98%ED%96%89%20%EC%8B%9C&quot;&gt;Cooperative 모드&lt;/a&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;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;(2) 메시지 컨슘&lt;/h4&gt;
&lt;pre id=&quot;code_1685774328631&quot; class=&quot;java&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@KafkaListener(
            topics = &quot;${papaco.kafka.review.avro.topic.name}&quot;, 
            groupId = &quot;papaco-review-project-query&quot;,
            concurrency = &quot;3&quot;)
public void listenReview(ConsumerRecords&amp;lt;String, ReviewMessage&amp;gt; records, Consumer&amp;lt;String, String&amp;gt; consumer) {
    consumer.commitSync();
}&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;b&gt;컨슈머 그룹 ID&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;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;●&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;@KafkaListener(groupId = &quot;papaco-review-project-query&quot;)&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;review 관련 메시지를 수신하는 컨슈머들을 컨슈머 그룹으로 묶어서 관리한다. 컨슈머 그룹을 기준으로 브로커에서 오프셋(&lt;a href=&quot;https://mandykr.tistory.com/97#:~:text=1)%20__consumer_offsets&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;__consumer_offsets&lt;/span&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;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;a style=&quot;color: #009a87;&quot; href=&quot;https://mandykr.tistory.com/97#:~:text=toString()))%3B%0A%20%20%20%20ack.acknowledge()%3B%0A%7D-,%E2%97%8F%20%EB%B0%B0%EC%B9%98%20%EC%BB%A8%EC%8A%88%EB%A8%B8%20%EB%A6%AC%EC%8A%A4%EB%84%88,-%EB%8F%99%EA%B8%B0%2C%20%EB%B9%84%EB%8F%99%EA%B8%B0%20%EC%BB%A4%EB%B0%8B%EC%9D%84&quot;&gt;&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;배치 컨슈머 리스너&lt;/span&gt;&lt;/a&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;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;●&lt;/span&gt;&amp;nbsp;spring.kafka.listener.type=BATCH&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;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;●&lt;/span&gt;&amp;nbsp;spring.kafka.listener.ack-mode=MANUAL&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;●&lt;/span&gt;&amp;nbsp;public&amp;nbsp;void&amp;nbsp;listenReview(ConsumerRecords&amp;lt;String,&amp;nbsp;ReviewMessage&amp;gt;&amp;nbsp;records,&amp;nbsp;Consumer&amp;lt;String,&amp;nbsp;String&amp;gt;&amp;nbsp;consumer)&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;●&lt;/span&gt;&amp;nbsp;consumer.commitSync();&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;스프링 카프카 컨슈머에서 제공하는 BATCH 타입의 리스너를 사용해 레코드 묶음(ConsumerRecords)을 파라미터로 받아 한 번에 처리하도록 한다. 또한 AcksMode를 MANUAL로 설정해&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;a style=&quot;color: #009a87;&quot; href=&quot;https://mandykr.tistory.com/97#:~:text=%ED%9B%84%EC%9D%98%20%EB%A9%94%EC%8B%9C%EC%A7%80%EB%A5%BC%20%EC%9D%BD%EB%8A%94%EB%8B%A4-,3)%20Auto%20Commit,-Auto%20Commit%20%EC%88%98%ED%96%89&quot;&gt;Auto commit&lt;/a&gt;&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;이 아닌 동기 혹은 비동기&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;a style=&quot;color: #009a87;&quot; href=&quot;https://mandykr.tistory.com/97#:~:text=%EC%A4%91%EB%B3%B5%EC%9C%BC%EB%A1%9C%20%EC%9D%BD%ED%9E%88%EA%B2%8C%20%EB%90%9C%EB%8B%A4.-,4)%20Manual%20Commit,-Consumer%20Property%EB%A5%BC&quot;&gt;Manual commit&lt;/a&gt;&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;을 사용하도록 설정한다. Comsumer 객체를 파라미터로 받아 commitSync() 와 같이 Manual commit 메소드를 호출할 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;동기 오프셋 커밋은 메시지를 읽고 브로커에 commit 적용이 성공적으로 될 때까지 블로킹 하도록해 비동기 방식에 비해 메시지 중복 읽기 가능성을 줄여준다.&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: #009a87;&quot;&gt;&lt;a style=&quot;color: #009a87;&quot; href=&quot;https://mandykr.tistory.com/97#:~:text=%EC%B2%98%EB%A6%AC%EB%90%A0%20%EC%88%98%20%EC%9E%88%EB%8B%A4.-,2)%20%EC%BB%A8%EC%8A%88%EB%A8%B8%20%EB%A9%80%ED%8B%B0%20%EC%8A%A4%EB%A0%88%EB%93%9C%20%EC%A0%84%EB%9E%B5,-%EC%BB%A8%EC%8A%88%EB%A8%B8%20%EB%A9%80%ED%8B%B0%20%EC%8A%A4&quot;&gt;멀티&amp;nbsp;스레드&amp;nbsp;컨슈머&lt;/a&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;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;●&lt;/span&gt;&amp;nbsp;@KafkaListener(concurrency = &quot;3&quot;)&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot;&gt;스프링 카프카 컨슈머는 concurrency 옵션값에 해당하는 만큼 컨슈머 스레드를 만들어서 병렬처리한다.&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 data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;높은 처리량 &lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카프카는 데이터 송수신을 묶어서 처리할 수 있기 때문에 동일한 양의 데이터에서 네트워크 통신 횟수를 줄일 수 있고, 데이터 배치 처리와 파티션 단위로 병렬 처리가 가능하기 때문에 높은 데이터 처리량을 보인다.&lt;/p&gt;
&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;카프카는 데이터가 적을 때 클러스터의 브로커를 최소한의 개수로 운영하다가 데이터가 많아지면 브로커의 개수를 늘려 스케일 아웃할 수 있다. 반대로 스케일 인도 가능하다.&lt;/p&gt;
&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;카프카는 디스크 기반의 파일 시스템에 데이터를 저장하기 때문에 브로커에 장애가 발생해 종료되더라도 데이터가 사라지지 않는다.&lt;/p&gt;
&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;3개 이상의 서버들로 운영되는 카프카 클러스터는 데이터를 복제해 저장하기 때문에 일부 서버에 장애가 발생하더라도 무중단으로 안전하고 지속적으로 데이터를 처리할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&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;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a style=&quot;color: #3db39e;&quot; href=&quot;http://www.yes24.com/Product/Goods/99122569&quot;&gt;아파치 카프카 애플리케이션 프로그래밍&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a style=&quot;color: #3db39e;&quot; href=&quot;https://www.inflearn.com/course/%EC%B9%B4%ED%94%84%EC%B9%B4-%EC%99%84%EB%B2%BD%EA%B0%80%EC%9D%B4%EB%93%9C-%EC%BD%94%EC%96%B4&quot;&gt;카프카&amp;nbsp;완벽&amp;nbsp;가이드&amp;nbsp;-&amp;nbsp;코어편&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Kafka</category>
      <category>kafka</category>
      <author>mandykr</author>
      <guid isPermaLink="true">https://mandykr.tistory.com/99</guid>
      <comments>https://mandykr.tistory.com/99#entry99comment</comments>
      <pubDate>Tue, 21 Mar 2023 13:55:20 +0900</pubDate>
    </item>
    <item>
      <title>[Kafka] 4. 카프카 컨슈머</title>
      <link>https://mandykr.tistory.com/97</link>
      <description>&lt;p 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;1. 컨슈머 Client&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;2. Poll&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;3. Heartbeat Thread&lt;/p&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: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;5. 오프셋 커밋&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;6. 특정 파티션만 할당&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;7. 컨슈머의 안전한 종료&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;8. 멀티 스레드 컨슈머&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;9. 컨슈머 랙&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 컨슈머 Client&lt;/h3&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;1874&quot; data-origin-height=&quot;985&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kxRjh/btr3HqWIZL3/1VJZe2D0mkCkOqTniN2fIk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kxRjh/btr3HqWIZL3/1VJZe2D0mkCkOqTniN2fIk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kxRjh/btr3HqWIZL3/1VJZe2D0mkCkOqTniN2fIk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkxRjh%2Fbtr3HqWIZL3%2F1VJZe2D0mkCkOqTniN2fIk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;788&quot; height=&quot;414&quot; data-origin-width=&quot;1874&quot; data-origin-height=&quot;985&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;기본 컨슈머 Client API 처리 로직&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1677896225552&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 1. Consumer 환경 설정(Properties 객체를 이용)
String topicName = &quot;simple-topic&quot;;
Properties props = new Properties();
props.setProperty(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, &quot;192.168.56.101:9092&quot;);
props.setProperty(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
props.setProperty(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
props.setProperty(ConsumerConfig.GROUP_ID_CONFIG, &quot;group-01&quot;);
        
// 2. 1에서 설정한 환경 설정값을 반영하여 KafkaConsumer 객체 생성.
KafkaConsumer&amp;lt;String, String&amp;gt; kafkaConsumer = new KafkaConsumer&amp;lt;String, String&amp;gt;(props);

// 3. 읽어들일 Topic을 subscribe()를 호출하여 설정
kafkaConsumer.subscribe(List.of(topicName));

// 4. 지속적으로 poll( ) 을 호출하여 Topic의 새로운 메시지를 계속 읽어 들임.
while (true) {
    ConsumerRecords&amp;lt;String, String&amp;gt; consumerRecords = kafkaConsumer.poll(Duration.ofMillis(1000));
    for (ConsumerRecord record : consumerRecords) {
        logger.info(&quot;record key:{}, record value:{}, partition:{}&quot;,
                record.key(), record.value(), record.partition());
    }
}
        
// 5. KafkaConsumer객체를 명확하게 close() 수행
kafkaConsumer.close();&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;기본 CLI 명령어&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1677037858403&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# Consumer Group id group_01을 가지는 consumer를 생성
$ bin/kafka-console-consumer.sh --bootstrap-server kafka-0:9092,kafka-1:9092,kafka-2:9092 --group group_01 --topic topic-p3r3 \
--property print.key=true --property print.value=true \
--property print.partition=true&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;kafka-consumer-groups.sh&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1677038557970&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 컨슈머 그룹 목록 확인
$ bin/kafka-consumer-groups.sh --bootstrap-server kafka-0:9092,kafka-1:9092,kafka-2:9092 --list

# 컨슈머 그룹 내의 컨슈머 정보 확인
$ bin/kafka-consumer-groups.sh --bootstrap-server kafka-0:9092,kafka-1:9092,kafka-2:9092 --describe \
--group group_01

GROUP    TOPIC    PARTITION    CURRENT-OFFSET    LOG-END-OFFSET    LAG    CONSUMER-ID    HOST    CLIENT-ID
goupe_01 topic-p3r3 ...

# 컨슈머 그룹 삭제
$ bin/kafka-consumer-groups.sh --bootstrap-server kafka-0:9092,kafka-1:9092,kafka-2:9092 --delete \
--group group_01&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. Poll&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1574&quot; data-origin-height=&quot;871&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bDjbRk/btr3LF6rXrz/KxKCrNAkHxlh1HlPKiyMik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bDjbRk/btr3LF6rXrz/KxKCrNAkHxlh1HlPKiyMik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bDjbRk/btr3LF6rXrz/KxKCrNAkHxlh1HlPKiyMik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbDjbRk%2Fbtr3LF6rXrz%2FKxKCrNAkHxlh1HlPKiyMik%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;638&quot; height=&quot;353&quot; data-origin-width=&quot;1574&quot; data-origin-height=&quot;871&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1678773364398&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ConsumerRecords&amp;lt;String, String&amp;gt; consumerRecords = kafkaConsumer.poll(Duration.ofMillis(1000));&lt;/code&gt;&lt;/pre&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;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;ConsumerNetworkClient는 비동기로 계속 브로커의 메시지를 가져와서 Linked Queue에 저장&lt;/li&gt;
&lt;li&gt;Fetcher는 Linked Queue에서 데이터를 읽음&lt;/li&gt;
&lt;li&gt;Linked Queue에 데이터가 있을 경우 Fetcher는 데이터를 가져오고 반환 하며 poll() 수행 완료&lt;/li&gt;
&lt;li&gt;Linked Queue에 데이터가 없을 경우 Fetcher가 ConsumerNetworkClient에게 데이터를 브로커로 부터 가져올 것을 요청&lt;/li&gt;
&lt;li&gt;ConsumerNetworkClient는 1000ms 까지 Broker에 메시지 요청 후 poll 수행() 완료&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Consumer Fetcher 설정 파라미터&lt;/h4&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 21.7054%; text-align: center;&quot;&gt;&lt;b&gt;파라미터&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 7.98451%; text-align: center;&quot;&gt;&lt;b&gt;기본값&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 70.31%; text-align: center;&quot;&gt;&lt;b&gt;설명&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 21.7054%;&quot;&gt;fetch.min.bytes&lt;/td&gt;
&lt;td style=&quot;width: 7.98451%;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;width: 70.31%;&quot;&gt;Fetcher가&amp;nbsp;record들을&amp;nbsp;읽어들이는&amp;nbsp;최소&amp;nbsp;bytes.&amp;nbsp;&lt;br /&gt;브로커는&amp;nbsp;지정된&amp;nbsp;fetch.min.bytes&amp;nbsp;이상의&amp;nbsp;새로운&amp;nbsp;메시지가&amp;nbsp;쌓일때&amp;nbsp;까지&amp;nbsp;전송을&amp;nbsp;하지&amp;nbsp;않음.&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 21.7054%;&quot;&gt;fetch.max.wait.ms&lt;/td&gt;
&lt;td style=&quot;width: 7.98451%;&quot;&gt;500ms&lt;/td&gt;
&lt;td style=&quot;width: 70.31%;&quot;&gt;브로커에 fetch.min.bytes 이상의 메시지가 쌓일 때까지 최대&amp;nbsp;대기&amp;nbsp;시간.&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 21.7054%;&quot;&gt;fetch.max.bytes&lt;/td&gt;
&lt;td style=&quot;width: 7.98451%;&quot;&gt;50MB&lt;/td&gt;
&lt;td style=&quot;width: 70.31%;&quot;&gt;Fetcher가&amp;nbsp;한번에&amp;nbsp;가져올&amp;nbsp;수&amp;nbsp;있는&amp;nbsp;최대&amp;nbsp;데이터&amp;nbsp;bytes.&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 21.7054%;&quot;&gt;max.partition.fetch.bytes&lt;/td&gt;
&lt;td style=&quot;width: 7.98451%;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;1MB&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 70.31%;&quot;&gt;Fetcher가 파티션별 한번에 최대로 가져올 수 있는 bytes.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 21.7054%;&quot;&gt;max.poll.records&lt;/td&gt;
&lt;td style=&quot;width: 7.98451%;&quot;&gt;500&lt;/td&gt;
&lt;td style=&quot;width: 70.31%;&quot;&gt;Fetcher가&amp;nbsp;한번에&amp;nbsp;가져올&amp;nbsp;수&amp;nbsp;있는&amp;nbsp;레코드&amp;nbsp;수.&amp;nbsp;&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;h4 data-ke-size=&quot;size20&quot;&gt;Consumer Fetcher 설정 파라미터 동작 방식&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;가져올&amp;nbsp;데이터가&amp;nbsp;1건도&amp;nbsp;없으면&amp;nbsp;poll(&amp;nbsp;)&amp;nbsp;인자&amp;nbsp;시간만큼&amp;nbsp;대기&amp;nbsp;후&amp;nbsp;Return&lt;/li&gt;
&lt;li&gt;가져와야할 과거 데이터가 많을 경우 max.partition.fetch.bytes로 배치 크기 설정.&lt;br /&gt;그렇지&amp;nbsp;않을&amp;nbsp;경우&amp;nbsp;fetch.min.bytes로&amp;nbsp;배치&amp;nbsp;크기&amp;nbsp;설정&lt;/li&gt;
&lt;li&gt;가장 최신의 offset 데이터를 가져오고 있다면 fetch.min.bytes 만큼 가져오고 return&amp;nbsp;하고&amp;nbsp;&lt;br /&gt;fetch.min.bytes만큼 쌓이지 않는다면 fetch.max.wait.ms 만큼 기다린 후&amp;nbsp;return&lt;/li&gt;
&lt;li&gt;오랜 과거 offset 데이터를 가져 온다면 최대 max.partition.fetch.bytes 만큼 파티션에서&amp;nbsp;읽은&amp;nbsp;뒤&amp;nbsp;반환&lt;/li&gt;
&lt;li&gt;max.partition.feth.bytes에 도달하지 못하여도 가장 최신의 offset에 도달하면 반환&lt;/li&gt;
&lt;li&gt;토픽에 파티션이 많아도 가져오는 데이터량은 fetch.max.bytes로 제한&lt;/li&gt;
&lt;li&gt;Fetcher가 Linked Queue에서 가져오는 레코드의 개수는 max.poll.records로 제한&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. Heartbeat Thread&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카프카 컨슈머 클라이언트는 Heart Beat Thread 를 통해서 주기적으로 브로커의 Group Coordinator에 컨슈머의 상태를 전송한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Group Coordinator 는 카프카 브로커중 한대가 담당하는데 컨슈머 Hearbeat 을 받아 컨슈머가 동작중인지 확인하고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Heartbeat 이 오지 않으면 컨슈머가 다운되었다고 판단해 컨슈머를 컨슈머 그룹에서 제외시킨다.(리밸런스를 컨슈머로 명령한다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 리밸런싱&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리밸런싱은 컨슈머 그룹 내에서 컨슈머에 변경이 생길 때 파티션과 컨슈머의 조합을 변경하는 과정이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브로커의 그룹 코디네이터가 컨슈머들에게 파티션을 재할당하는 리밸런싱을 지시한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) 리밸런싱 과정&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1650&quot; data-origin-height=&quot;940&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dvFw8C/btr3SvPGer0/8HdOS39nUoetpdpQFx71x1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dvFw8C/btr3SvPGer0/8HdOS39nUoetpdpQFx71x1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dvFw8C/btr3SvPGer0/8HdOS39nUoetpdpQFx71x1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdvFw8C%2Fbtr3SvPGer0%2F8HdOS39nUoetpdpQFx71x1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;320&quot; data-origin-width=&quot;1650&quot; data-origin-height=&quot;940&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;① 컨슈머 그룹내의 컨슈머가 브로커에 최초 접속을 요청하면 Group Coordinator 생성&lt;br /&gt;② 동일 group.id로 여러 개의 컨슈머로 브로커의 Group&amp;nbsp;Coordinator로&amp;nbsp;접속&lt;br /&gt;③ 가장 빨리 Group에 Join 요청을 한 컨슈머에게 컨슈머 그룹내의 리더 컨슈머로 지정&lt;br /&gt;④ 리더 컨슈머는 파티션 할당전략에 따라 컨슈머들에게 파티션 할당&lt;br /&gt;⑤ 리더 컨슈머는 최종 할당된 파티션 정보를 Group Coordinator에게 전달&lt;br /&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;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1650&quot; data-origin-height=&quot;940&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bFUMTv/btr3GTrj8xO/mq1TysIiooE6Ey6Q9Z7EOk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bFUMTv/btr3GTrj8xO/mq1TysIiooE6Ey6Q9Z7EOk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bFUMTv/btr3GTrj8xO/mq1TysIiooE6Ey6Q9Z7EOk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbFUMTv%2Fbtr3GTrj8xO%2Fmq1TysIiooE6Ey6Q9Z7EOk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;320&quot; data-origin-width=&quot;1650&quot; data-origin-height=&quot;940&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;⑦&amp;nbsp; 컨슈머 그룹 내에 새로운 컨슈머가 추가되거나 기존 컨슈머가 종료되면(Heartbeat 를 받지 못함) Group Coordinator는 컨슈머들에게 파티션을 재할당하는 리밸런싱을 수행하도록 지시&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;2) Consumer Group Status&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨슈머 그룹은 Group Meta Data 에 컨슈머 그룹의 상태를 저장한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;943&quot; data-origin-height=&quot;253&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/K2VB9/btr1Y9nwoKO/K5wyWHSWkNSXck4Zgq5jFk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/K2VB9/btr1Y9nwoKO/K5wyWHSWkNSXck4Zgq5jFk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/K2VB9/btr1Y9nwoKO/K5wyWHSWkNSXck4Zgq5jFk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FK2VB9%2Fbtr1Y9nwoKO%2FK5wyWHSWkNSXck4Zgq5jFk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;640&quot; height=&quot;172&quot; data-origin-width=&quot;943&quot; data-origin-height=&quot;253&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Empty : 컨슈머 그룹은 존재하지만&amp;nbsp;컨슈머는 없음&lt;/li&gt;
&lt;li&gt;Rebalance :&amp;nbsp;Rebalance 수행&lt;/li&gt;
&lt;li&gt;Stable :&amp;nbsp;Rebalance가 종료되고 안정적으로 컨슈머 운영&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3) Consumer Static Group Membership&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨슈머 그룹 내의 컨슈머들에게 고정된 IP를 부여하여,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨슈머 별로 컨슈머 그룹에 최초 조인 시 할당된 파티션을 그대로 유지하고 컨슈머가 shutdown되어도&lt;br /&gt;&lt;span style=&quot;background-color: #c1bef9;&quot;&gt;session.timeout.ms내에 재기동되면 리밸런스가 수행되지 않고, 기존 파티션이 재할당 된다.&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;컨슈머 그룹 내에서 리밸런스가 수행되면 모든 컨슈머들이 데이터를 소비하지 못하고 Consumer LAG 이 길어지게 되는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Consumer 스태틱 그룹 멤버쉽으로 Consumer Restart 에서 불필요한 리밸런스가 발생하지 않도록 한다.&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;b&gt;Consumer LAG&lt;/b&gt;&lt;br /&gt;프로듀서가 데이터를 넣는 속도가 컨슈머가 가져가는 속도보다 빠른 경우,&lt;br /&gt;토픽의 가장 최신 오프셋(LOG-END-OFFSET)과 컨슈머가 가져간 데이터의 오프셋(CURRENT-OFFSET)의 차이를 kafka consumer lag이라 부른다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4) 리밸런스 관련 컨슈머 파라미터&lt;/h4&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 134px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 18.5658%; text-align: center; height: 20px;&quot;&gt;&lt;b&gt;파라미터&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 11.3566%; text-align: center; height: 20px;&quot;&gt;&lt;b&gt;기본값(ms) &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 70.0775%; text-align: center; height: 20px;&quot;&gt;&lt;b&gt;설명&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 80px;&quot;&gt;
&lt;td style=&quot;width: 18.5658%; height: 80px;&quot;&gt;heartbeat.interval.ms&lt;/td&gt;
&lt;td style=&quot;width: 11.3566%; height: 80px;&quot;&gt;3000&lt;/td&gt;
&lt;td style=&quot;width: 70.0775%; height: 80px;&quot;&gt;Heart Beat Thread가 Heart Beat을 보내는 간격. &lt;br /&gt;session.timeout.ms 보다 낮게 설정되어야 함. &lt;br /&gt;Session.timeout.ms의 1/3 보다 낮 게 설정 권장.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 18.5658%; height: 17px;&quot;&gt;session.timeout.ms&lt;/td&gt;
&lt;td style=&quot;width: 11.3566%; height: 17px;&quot;&gt;45000&lt;/td&gt;
&lt;td style=&quot;width: 70.0775%; height: 17px;&quot;&gt;브로커가 Consumer로 Heart Beat을 기다리는 최대 시간. &lt;br /&gt;브로커는 이 시간동 안 Heart beat을 consumer로 부터 받지 못하면 해당 consumer를 Group에서 제 외하도록 rebalancing 명령을 지시&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 18.5658%; height: 17px;&quot;&gt;max.poll.interval.ms&lt;/td&gt;
&lt;td style=&quot;width: 11.3566%; height: 17px;&quot;&gt;300000&lt;/td&gt;
&lt;td style=&quot;width: 70.0775%; height: 17px;&quot;&gt;이전 poll( )호출 후 다음 호출 poll( )까지 브로커가 기다리는 시간. &lt;br /&gt;해당 시간동 안 poll( )호출이 Consumer로 부터 이뤄지지 않으면 해당 consumer는 문제가 있 는 것으로 판단하고 브로커는 rebalance 명령을 보냄.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;heartbeat.interval.ms 마다 컨슈머는 heartbeat 을 브로커로 보내고 컨슈머가 session.timeout.ms 만큼 &lt;span&gt;heartbeat&lt;span&gt; 을 보내지 않거나 max.poll.interval.ms 만큼 poll() 호출을 하지 않으면 브로커(코디네이터)는 리밸런스 명령을 리더 컨슈머에게 보낸다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #c1bef9;&quot;&gt;컨슈머에서 메시지를 읽어서 처리하는 서비스 로직이 max.poll.interval.ms 보다 커지면 의도치 않게 리밸런스가 발생할 수 있기 때문에 서비스 로직의 수행 시간을 줄일 수 없다면 max.poll.interval.ms 를 늘리거나 파티션을 추가하는 등의 방법을 고려해야 한다.&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;5) Consumer Rebalancing Protocol&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Eager&amp;nbsp;모드&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Rebalance 수행시 컨슈머들의 모든 파티션 할당을 취소하고, 새롭게 Consumer에 파티션을 다시 할당 받고 다시 메시지를 읽는다. 모든 컨슈머가 Rebalance 되는 동안 잠시 메시지를 읽지 않는 시간으로 인해 Lag가 상대적으로 크게 발생할 가능성 있다.&lt;/p&gt;
&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;Cooperative 모드 &lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Rebalance 수행 시 컨슈머들의 모든 파티션 할당을 취소하지 않고 대상이 되는 컨슈머들에 대해서 파티션에 따라 점진적으로(Incremental) 컨슈머를 할당하면서 Rebalance를 수행한다. 많은 컨슈머를 가지는 컨슈머 그룹내에서 Rebalance 시간이 오래 걸릴 시 활용도 높다.&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;6) Consumer 파티션 할당 전략&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카프카 컨슈머는 Consumer의 부하를 파티션 별로 균등하게 할당하고 데이터 처리 및 리밸런싱의 효율성 극대화하기 위해 4개의 파티션 할당 전략 유형을 제공한다.&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;(1) Round Robin 할당 전략&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1338&quot; data-origin-height=&quot;715&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kzYUI/btr1Y8a5FJL/c4KTFAO9WUyiPeXKhhhNh0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kzYUI/btr1Y8a5FJL/c4KTFAO9WUyiPeXKhhhNh0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kzYUI/btr1Y8a5FJL/c4KTFAO9WUyiPeXKhhhNh0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkzYUI%2Fbtr1Y8a5FJL%2Fc4KTFAO9WUyiPeXKhhhNh0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;363&quot; data-origin-width=&quot;1338&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;Round Robin 전략은 토픽들의 파티션별로 순차적으로 Consumer에 할당하므로 파티션 매핑이 Consumer별로 비교적 균일하게 할당한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Rebalancing 시에도 토픽들의 파티션과 Consumer들을 균등하게 매핑하므로 Rebalance 이전의 파티션과 Consumer들의 매핑이 변경되기 쉽다.&lt;/p&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;(2) Range 할당 전략 (기본 전략)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1338&quot; data-origin-height=&quot;715&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b0oOMP/btr1YJP85o9/jwH10ar77F0wpIj1ZBcuFk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b0oOMP/btr1YJP85o9/jwH10ar77F0wpIj1ZBcuFk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b0oOMP/btr1YJP85o9/jwH10ar77F0wpIj1ZBcuFk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb0oOMP%2Fbtr1YJP85o9%2FjwH10ar77F0wpIj1ZBcuFk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;363&quot; data-origin-width=&quot;1338&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;Range 전략은 서로 다른 토픽들의 동일한 파티션들을 같은 Consumer로 할당한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Rebalancing 시에도 서로 다른 토픽에서 동일한 키값을 가지는 파티션들은 같은 Consumer에서 처리 할 수 있도록 유도한다.&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;(3) Sticky 할당 전략&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1338&quot; data-origin-height=&quot;715&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xGKkT/btr2gkPcqBr/pnWygKBDwKqyamAS7voHn0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xGKkT/btr2gkPcqBr/pnWygKBDwKqyamAS7voHn0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xGKkT/btr2gkPcqBr/pnWygKBDwKqyamAS7voHn0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxGKkT%2Fbtr2gkPcqBr%2FpnWygKBDwKqyamAS7voHn0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;363&quot; data-origin-width=&quot;1338&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;Sticky 전략은 Rebalancing 시 기존 토픽들의 파티션과 Consumer 매핑은 최대한 유지하고 재할당되어야 하는 파티션들만 Consumer들에 할당한다.&lt;br /&gt;하지만 모든 Consumer들의 파티션이 일제히 취소되는 Eager Protocol 기반에서 동작한다.&lt;/p&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;(4) Cooperative Sticky 할당 전략&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1338&quot; data-origin-height=&quot;715&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/molCh/btr1329vFo7/leFZnmuvrR5bkoUKIaezCK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/molCh/btr1329vFo7/leFZnmuvrR5bkoUKIaezCK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/molCh/btr1329vFo7/leFZnmuvrR5bkoUKIaezCK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmolCh%2Fbtr1329vFo7%2FleFZnmuvrR5bkoUKIaezCK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;363&quot; data-origin-width=&quot;1338&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Rebalancing 시 기존 토픽들의 파티션과 Consumer 매핑은 최대한 유지하고 재할당되어야 하는 파티션들만 Consumer들에 할당한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 매핑을 다 취소하지 않고 기존 매핑을 그대로 유지한채 재할당되어야 할 파티션만 Consumer에 따라 순차적으로 Rebalance를 수행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 오프셋 커밋&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨슈머 그룹은 토픽이 특정 파티션으로부터 데이터를 가져가서 처리하고 이 파티션의 어느 레코드까지 가져갔는지 확인하기 위해 오프셋을 커밋한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) __consumer_offsets&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커밋한 오프셋은 브로커의 __consumer_offsets 이라는 내부 토픽에 저장하고&amp;nbsp;__consumer_offsets 토픽은 컨슈머 그룹별로 관리된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리밸런싱과 같이 컨슈머가 동일한 컨슈머 그룹으로 새롭게 접속할 시 __consumer_offsets에 있는 offset 정보를 기반으로 메시지를 가져오기 때문에 메시지 중복이나 손실이 발생하지 않는다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1516&quot; data-origin-height=&quot;874&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/k3AQN/btr2ufIADvF/8zT0DPOEvUPM73HjSQ1uck/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/k3AQN/btr2ufIADvF/8zT0DPOEvUPM73HjSQ1uck/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/k3AQN/btr2ufIADvF/8zT0DPOEvUPM73HjSQ1uck/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fk3AQN%2Fbtr2ufIADvF%2F8zT0DPOEvUPM73HjSQ1uck%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;640&quot; height=&quot;369&quot; data-origin-width=&quot;1516&quot; data-origin-height=&quot;874&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;Consumer는 poll( ) 메소드를 이용하여 주기적으로 브로커의 토픽 파티션에서 메시지를 가져오고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메시지를 성공적으로 가져 왔으면 commit을 통해서 __consumer_offse 에 다음에 읽을 offset 위치를 기재한다.&lt;/p&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;__consumer_offsets 저장&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;컨슈머 그룹의 컨슈머가 모두 종료되어도 offset 정보는 7일동안 __consumer_offsets에 저장 (offsets.retention.minutes)&lt;/li&gt;
&lt;li&gt;Topic이 삭제되고 재생성될 경우에는 해당 offset 정보는 0으로 __consumer_offsets에 기록&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2) auto.offset.reset&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;auto.offset.reset 은 __consumer_offsets에 컨슈머 그룹이 해당 토픽의 파티션별로 offset 정보를 가지고 있지 않을 때 컨슈머가 접속하면 파티션의 처음 offset 부터(earliest) 가져올 것인지, 마지막 offset 이후 부터 가져올 것인지를 설정하는 파라미터이다.&lt;/p&gt;
&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;__consumer_offsets 에 오프셋이 저장되어 있을 경우&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;earliest, latest 모두 마지막 오프셋 이 후의 메시지를 읽는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;__consumer_offsets 에 오프셋이 저장되어 있지 않을 경우&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;earliest 는 0번 오프셋 부터 메시지를 읽는다.&lt;/li&gt;
&lt;li&gt;latest 는 마지막 오프셋 이 후의 메시지를 읽는다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3) Auto Commit&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Auto Commit 수행 시점&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;poll()&lt;/li&gt;
&lt;li&gt;close()&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Consumer의&amp;nbsp;파라미터로&amp;nbsp;auto.enable.commit=true&amp;nbsp;인&amp;nbsp;경우&amp;nbsp;읽어온&amp;nbsp;메시지를&amp;nbsp;브로커에&amp;nbsp;바로&amp;nbsp;commit&amp;nbsp;적용하지&amp;nbsp;않고, &lt;br /&gt;auto.commit.interval.ms 값(기본 5초)마다 Consumer가 자동으로 Commit을 수행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;auto.commit.interval.ms 값만큼 지난 후 수행되는 poll()에서 이전 poll()에서 가져온 마지막 메시지의 offset을 commit 한다.&lt;/p&gt;
&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;auto.commit.interval.ms=5000 인 경우,&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫번째 poll() 에서 5초보다 작은 3초가 소요되면 offset을 commit하지 않는다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1515&quot; data-origin-height=&quot;874&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bYkDh2/btr2vVJJ3q0/X4YWt7VXLVGH0PspkxEB2K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bYkDh2/btr2vVJJ3q0/X4YWt7VXLVGH0PspkxEB2K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bYkDh2/btr2vVJJ3q0/X4YWt7VXLVGH0PspkxEB2K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbYkDh2%2Fbtr2vVJJ3q0%2FX4YWt7VXLVGH0PspkxEB2K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;650&quot; height=&quot;375&quot; data-origin-width=&quot;1515&quot; data-origin-height=&quot;874&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두번째 poll() 에서 5초가 지나면 이전 poll() 의 offset을 commit 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1515&quot; data-origin-height=&quot;874&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/daYRx9/btr2Cux0IAA/KHB4sGrqzm1XIxCM85EBK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/daYRx9/btr2Cux0IAA/KHB4sGrqzm1XIxCM85EBK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/daYRx9/btr2Cux0IAA/KHB4sGrqzm1XIxCM85EBK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdaYRx9%2Fbtr2Cux0IAA%2FKHB4sGrqzm1XIxCM85EBK1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;650&quot; height=&quot;375&quot; data-origin-width=&quot;1515&quot; data-origin-height=&quot;874&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #c1bef9;&quot;&gt;컨슈머가 비정상적으로 종료되면 현재까지 읽은 offset을 commit 하지 못해 메시지 중복 읽기가 발생할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;__consumer_offsets 에 커밋된 offset 부터 메시지를 읽기 때문에 &lt;span&gt;partition 0의 offset 2와&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;partition 1의 offset 1이 중복으로 읽히게 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1515&quot; data-origin-height=&quot;874&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/czwg4d/btr2Fip05Yt/0KTa8eqvmdYAyZWQysj7r0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/czwg4d/btr2Fip05Yt/0KTa8eqvmdYAyZWQysj7r0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/czwg4d/btr2Fip05Yt/0KTa8eqvmdYAyZWQysj7r0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fczwg4d%2Fbtr2Fip05Yt%2F0KTa8eqvmdYAyZWQysj7r0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;650&quot; height=&quot;375&quot; data-origin-width=&quot;1515&quot; data-origin-height=&quot;874&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;h4 data-ke-size=&quot;size20&quot;&gt;4) Manual Commit&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Consumer Property를 enable.auto.commit = false 로 설정하고 명시적으로 동기 / 비동기 방식 오프셋 커밋을 적용할 수 있다.&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;(1) 동기 오프셋 커밋&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;857&quot; data-origin-height=&quot;568&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vJuub/btr2tDu82TI/GisDzPW0HFPkhZsKtUs3M0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vJuub/btr2tDu82TI/GisDzPW0HFPkhZsKtUs3M0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vJuub/btr2tDu82TI/GisDzPW0HFPkhZsKtUs3M0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvJuub%2Fbtr2tDu82TI%2FGisDzPW0HFPkhZsKtUs3M0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;265&quot; data-origin-width=&quot;857&quot; data-origin-height=&quot;568&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Consumer객체의&amp;nbsp;commitSync(&amp;nbsp;)&amp;nbsp;메소드를&amp;nbsp;사용&lt;/li&gt;
&lt;li&gt;메시지 배치를 poll( )을 통해서 읽어오고 해당 메시지들의 마지막&amp;nbsp;offset을&amp;nbsp;브로커에&amp;nbsp;commit&amp;nbsp;적용&lt;/li&gt;
&lt;li&gt;브로커에 commit 적용이 성공적으로 될 때까지 블로킹 적용&lt;/li&gt;
&lt;li&gt;Commit 적용 완료 후에 다시 메시지를 읽어옴&lt;/li&gt;
&lt;li&gt;브로커에 Commit 적용이 실패할 경우 다시 Commit 적용 요청&lt;/li&gt;
&lt;li&gt;Commit 적용 재시도가 일정 횟수동안 실패할 경우 CommitFailedException 발생&lt;/li&gt;
&lt;li&gt;비동기 방식 대비 더 느린 수행 시간&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;(2) 비동기 오프셋 커밋&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;857&quot; data-origin-height=&quot;568&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cxQJBZ/btr2zmzlcX3/VDW86xUJGp65y0IUmW1b2K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cxQJBZ/btr2zmzlcX3/VDW86xUJGp65y0IUmW1b2K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cxQJBZ/btr2zmzlcX3/VDW86xUJGp65y0IUmW1b2K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcxQJBZ%2Fbtr2zmzlcX3%2FVDW86xUJGp65y0IUmW1b2K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;265&quot; data-origin-width=&quot;857&quot; data-origin-height=&quot;568&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Consumer객체의&amp;nbsp;commitAsync(&amp;nbsp;)&amp;nbsp;메소드를&amp;nbsp;사용&lt;/li&gt;
&lt;li&gt;메시지 배치를 poll( )을 통해서 읽어오고 해당 메시지들의 마지막 offset을 브로커에 commit 적용 요청하지만 브로커에 commit 적용이 성공적으로 되었음을 기다리지 않고(블로킹 하지 않음) 계속 메시지를 읽어옴&lt;/li&gt;
&lt;li&gt;Callback(OffsetCommitCallback) 을 통해 Exception 을 처리&amp;nbsp;&lt;/li&gt;
&lt;li&gt;브로커에 Commit 적용이 실패해도 다시 Commit 시도 안함&lt;br /&gt;(&lt;span style=&quot;background-color: #c1bef9;&quot;&gt;Consumer 장애 또는 Rebalance 시 한번 읽은 메시지를 다시 중복해서 가져 올 수 있음&lt;/span&gt;)&lt;/li&gt;
&lt;li&gt;동기 방식 대비 더 빠른 수행 시간&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5) 리밸런스 리스너&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Auto와 Manual offset commit 모두 offset 을 커밋하지 못하고 리밸런스가 발생하면 offset 커밋 이 후 읽은 메시지는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리밸런스가 완료되고 중복해서 읽게된다. 이런 문제를 해결하기 위해 리밸런스가 발생하면 현재까지 처리한 메시지를 기준으로 커밋을 시도해야 한다. 리밸런스 발생을 감지하기 위해 카프카 라이브러리는 ConsumerRebalanceListener 인터페이스를 지원한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1678171345534&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public static void main(String[] args) {
	// ...
    configs.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);

    consumer = new KafkaConsumer&amp;lt;&amp;gt;(configs);
    consumer.subscribe(Arrays.asList(TOPIC_NAME), new RebalanceListener());
    while (true) {
        ConsumerRecords&amp;lt;String, String&amp;gt; records = consumer.poll(Duration.ofSeconds(1));
        for (ConsumerRecord&amp;lt;String, String&amp;gt; record : records) {
            currentOffsets.put(
            	new TopicPartition(record.topic(), record.partition()), 
                new offsetAndMetadata(record + 1, null)
            );
        }
    }
}

public class RebalanceListener implements ConsumerRebalanceListener {
    public void onPartitionsAssigned(Collection&amp;lt;TopicPartition&amp;gt; partitions) {
    }

    public void onPartitionsRevoked(Collection&amp;lt;TopicPartition&amp;gt; partitions) {
        consumer.commitSync(currentOffsets);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;onPartitionsAssigned() : 리밸런스가 끝난 뒤에 파티션이 할당 완료되면 호출&lt;/li&gt;
&lt;li&gt;onPartitionsRevoked() : 리밸런스가 시작되기 직전에 호출&lt;br /&gt;가장 마지막 레코드를 기준으로 커밋을 실시한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. 특정 파티션만 할당&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Consumer에게 여러 개의 파티션이 있는 Topic에서 특정 파티션만 할당할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배치 처리시 특정 key레벨의 파티션을 특정 Consumer에 할당하여 처리할 경우에 적용한다.&lt;/p&gt;
&lt;pre id=&quot;code_1678155982873&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;TopicPartition topicPartition = new TopicPartition(topicName, 0);
kafkaConsumer.assign(Arrays.asList(topicPartition));&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_1678156044840&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;TopicPartition topicPartition = new TopicPartition(topicName, 1);
kafkaConsumer.assign(Arrays.asList(topicPartition));
kafkaConsumer.seek(topicPartition, 6L);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. 컨슈머의 안전한 종료&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨슈머 애플리케이션이 비정상적으로 종료되면 세션 타임아웃이 발생할때까지 컨슈머가 컨슈머 그룹에 남게되기 때문에 더는 동작을 하지 않는 컨슈머가 존재하기 때문에 파티션의 데이터는 소모되지 못하고 컨슈머 랙이 늘어나게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;KafkaConsumer.wakeup()&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1678172113300&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public static void main(String[] args) {
    Runtime.getRuntime().addShutdownHook(new ShutdownThread());

    try {
        while (true) {
            ConsumerRecords&amp;lt;String, String&amp;gt; records = consumer.poll(Duration.ofSeconds(1));
            // ...
            consumer.commitSync();
        }
    } catch (WakeupException e) {
        logger.warn(&quot;Wakeup consumer&quot;);
    } finally {
        logger.warn(&quot;Consumer close&quot;);
        consumer.close();
    }
}

static class ShutdownThread extends Thread {
    public void run() {
        logger.info(&quot;Shutdown hook&quot;);
        consumer.wakeup();
    }
}&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;컨슈머 애플리케이션이 종료되면 셧다운 훅을 사용해 wakeup() 메서드를 호출한다. &lt;span&gt;wakeup() 메서드가 실행된 이후 poll() 메서드가 호출되면 WakeupException 예외가 발생하는데&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;WakeupException 예외처리에서 사용한 자원들을 해제하고 마지막으로 close() 메서드로 카프카 클러스터에게 컨슈머의 종료를 전달해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8. 멀티 스레드 컨슈머&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파티션을 여러개로 운영하는 경우 데이터를 병렬처리하기 위해서 파티션 개수와 컨슈머 개수를 동일하게 맞추는 것이 가장 좋은 방법이다. 그러므로 컨슈머는 멀티 프로세스 방식이나 멀티 스레드 방식을 선택해 개발할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바는 멀티 스레드를 지원하므로 멀티 컨슈머 스레드를 개발할 수 있는데 멀티 스레드로 컨슈머를 운영하기 위해서는 각 컨슈머 스레드 간에 영향이 미치지 않도록 스레드 세이프 로직, 변수를 적용해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) 컨슈머 멀티 워크 스레드 전략&lt;/h4&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-origin-width=&quot;1501&quot; data-origin-height=&quot;420&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CJiB4/btr2D2nlXMV/7WT6nLJzeXGfU1C0EDtIj1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CJiB4/btr2D2nlXMV/7WT6nLJzeXGfU1C0EDtIj1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CJiB4/btr2D2nlXMV/7WT6nLJzeXGfU1C0EDtIj1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCJiB4%2Fbtr2D2nlXMV%2F7WT6nLJzeXGfU1C0EDtIj1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;610&quot; height=&quot;171&quot; data-origin-width=&quot;1501&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;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1678173510964&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ExecutorService executorService = Executors.newCachedThreadPool();
while (true) {
    ConsumerRecords&amp;lt;String, String&amp;gt; records = consumer.poll(Duration.ofSeconds(10));
    for (ConsumerRecord&amp;lt;String, String&amp;gt; record : records) {
        ConsumerWorker worker = new ConsumerWorker(record.value());
        executorService.execute(worker);
    }
}

public class ConsumerWorker implements Runnable {
    private String recordValue;

    ConsumerWorker(String recordValue) {
        this.recordValue = recordValue;
    }

    @Override
    public void run() {
        // record 처리
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ConsumerWorker 클래스는 Runnable 인터페이스를 구현해 스레드로 실행된다.&lt;/li&gt;
&lt;li&gt;ExecutorService 자바 라이브러리를 사용하면 레코드를 병렬처리하는 스레드를 효율적으로 생성하고 관리할 수 있다.&lt;/li&gt;
&lt;li&gt;Executors 를 사용해 CashedThreadPool을 사용하면 레코드 처리가 완료되면 스레드는 종료된다.&lt;/li&gt;
&lt;/ul&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;/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;각 레코드의 데이터 처리가 끝났음을 리턴받지 않고 다음 poll() 메서드를 호출하기 때문에 데이터 처리가 스레드에서 진행중임에도 다음 poll() 메서드를 호출해 커밋을 하게되면 데이터의 유실이 발생할 수 있다.&lt;/p&gt;
&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;스레드의 생성은 순서대로 진행되지만 나중에 생성된 스레드의 레코드 처리시간이 더 짧을 경우 이전 레코드가 다음 레코드보다 나중에 처리될 수 있다.&lt;/p&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;2) 컨슈머 멀티 스레드 전략&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-origin-width=&quot;998&quot; data-origin-height=&quot;446&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/KXZbT/btr2xS0cCuH/zRsS6mkvlk74iJdxbJwbBk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/KXZbT/btr2xS0cCuH/zRsS6mkvlk74iJdxbJwbBk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/KXZbT/btr2xS0cCuH/zRsS6mkvlk74iJdxbJwbBk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKXZbT%2Fbtr2xS0cCuH%2FzRsS6mkvlk74iJdxbJwbBk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;228&quot; data-origin-width=&quot;998&quot; data-origin-height=&quot;446&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1678174712134&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private final static int CONSUMER_COUNT = 3;

ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i &amp;lt; CONSUMER_COUNT; i++) {
    ConsumerWorker worker = new ConsumerWorker(configs, TOPIC_NAME, i);
    executorService.execute(worker);
}

public class ConsumerWorker implements Runnable {
    private final Properties prop;
    private final String topic;
    private final String threadName;
    private KafkaConsumer&amp;lt;String, String&amp;gt; consumer;

    ConsumerWorker(Properties prop, String topic, int number) {
        this.prop = prop;
        this.topic = topic;
        this.threadName = &quot;consumer-thread-&quot; + number;
    }

    @Override
    public void run() {
        consumer = new KafkaConsumer&amp;lt;&amp;gt;(prop);
        consumer.subscribe(Arrays.asList(topic));
        try {
            while (true) {
                ConsumerRecords&amp;lt;String, String&amp;gt; records = consumer.poll(Duration.ofSeconds(1));
                for (ConsumerRecord&amp;lt;String, String&amp;gt; record : records) {
                    logger.info(&quot;{}&quot;, record);
                }
                consumer.commitSync();
            }
        } catch (WakeupException e) {
            System.out.println(threadName + &quot; trigger WakeupException&quot;);
        } finally {
            consumer.commitSync();
            consumer.close();
        }
    }

    public void shutdown() {
        consumer.wakeup();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9. 컨슈머 랙&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨슈머 랙은 토픽의 최신 오프셋과 컨슈머 오프셋 간의 차이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨슈머 랙은 컨슈머 그룹과 토픽, 파티션별로 생성된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1516&quot; data-origin-height=&quot;874&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/u4Uez/btr2sFnybzz/vAKjdXV6LdMIQ3Af9eLdlk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/u4Uez/btr2sFnybzz/vAKjdXV6LdMIQ3Af9eLdlk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/u4Uez/btr2sFnybzz/vAKjdXV6LdMIQ3Af9eLdlk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fu4Uez%2Fbtr2sFnybzz%2FvAKjdXV6LdMIQ3Af9eLdlk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;605&quot; height=&quot;349&quot; data-origin-width=&quot;1516&quot; data-origin-height=&quot;874&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;h4 data-ke-size=&quot;size20&quot;&gt;컨슈머 랙 모니터링&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;1) 카프카 명령어&lt;/p&gt;
&lt;pre id=&quot;code_1678176754132&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;bin/kafka-consumer-group.sh --bootstrap-server localhost:9092 \
--group group_01 --describe&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;2) 컨슈머 애플리케이션&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;kafkaConsumer.metrics()&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;record-lag-max&lt;/li&gt;
&lt;li&gt;record-lag&lt;/li&gt;
&lt;li&gt;record-lag-avg&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;3) 외부 모니터링 툴&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Datadog&lt;/li&gt;
&lt;li&gt;컨플루언트 컨트롤 센터&lt;/li&gt;
&lt;li&gt;카프카 버로우&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;10. 스프링 카프카 컨슈머&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) 메시지 리스너&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 카프카에서는 리스너 컨테이너를 사용해 컨슈머를 2개의 타입으로 래핑하였다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;레코드 리스너(MessageListener) : 1개의 레코드를 처리&lt;/li&gt;
&lt;li&gt;배치 리스너(BatchMessageListener) : 한 번에 여러개의 레코드들을 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;컨슈머 타입별 리스너&lt;/b&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 10.3101%; text-align: center;&quot;&gt;&lt;b&gt;타입&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 19.1473%; text-align: center;&quot;&gt;&lt;b&gt;리스너&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 38.9922%; text-align: center;&quot;&gt;&lt;b&gt;설명&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 10.3101%;&quot; rowspan=&quot;4&quot;&gt;RECORD&lt;/td&gt;
&lt;td style=&quot;width: 19.1473%;&quot;&gt;MessageListener&lt;/td&gt;
&lt;td style=&quot;width: 38.9922%;&quot;&gt;Record 인스턴스 단위로 프로세싱.&lt;br /&gt;오토 커밋 또는 컨슈머 컨테이너의 AckMode를 사용하는 경우&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 19.1473%;&quot;&gt;Acknowledging&lt;br /&gt;MessageListener&lt;/td&gt;
&lt;td style=&quot;width: 38.9922%;&quot;&gt;Record 인스턴스 단위로 프로세싱.&lt;br /&gt;매뉴얼 커밋을 사용하는 경우&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 19.1473%;&quot;&gt;ConsumerAware&lt;br /&gt;MessageListener&lt;/td&gt;
&lt;td style=&quot;width: 38.9922%;&quot;&gt;Record 인스턴스 단위로 프로세싱.&lt;br /&gt;컨슈머 객체를 활용하고 싶은 경우&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 19.1473%;&quot;&gt;AcknowledgingConsumerAware&lt;br /&gt;MessageListener&lt;/td&gt;
&lt;td style=&quot;width: 38.9922%;&quot;&gt;Record 인스턴스 단위로 프로세싱.&lt;br /&gt;매뉴얼 커밋을 사용하고 컨슈머 객체를 활용하고 싶은 경우&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 10.3101%;&quot; rowspan=&quot;4&quot;&gt;BATCH&lt;/td&gt;
&lt;td style=&quot;width: 19.1473%;&quot;&gt;BatchMessageListener&lt;/td&gt;
&lt;td style=&quot;width: 38.9922%;&quot;&gt;Records 인스턴스 단위로 프로세싱.&lt;br /&gt;오토 커밋 또는 컨슈머 컨테이너의 AckMode를 사용하는 경우&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 19.1473%;&quot;&gt;BatchAcknowledging&lt;br /&gt;MessageListener&lt;/td&gt;
&lt;td style=&quot;width: 38.9922%;&quot;&gt;Records 인스턴스 단위로 프로세싱.&lt;br /&gt;매뉴얼 커밋을 사용하는 경우&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 19.1473%;&quot;&gt;BatchConsumerAware&lt;br /&gt;MessageListener&lt;/td&gt;
&lt;td style=&quot;width: 38.9922%;&quot;&gt;Records 인스턴스 단위로 프로세싱.&lt;br /&gt;컨슈머 객체를 활용하고 싶은 경우&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 19.1473%;&quot;&gt;BatchAcknowledgingConsumer&lt;br /&gt;MessageListener&lt;/td&gt;
&lt;td style=&quot;width: 38.9922%;&quot;&gt;Records 인스턴스 단위로 프로세싱.&lt;br /&gt;매뉴얼 커밋을 사용하고 컨슈머 객체를 활용하고 싶은 경우&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매뉴얼 커밋을 사용할 경우 Ackowledging이 붙은 리스너를 사용하고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;KafkaConsumer 인스턴스에 직접 접근하여 컨트롤하고 싶다면 ConsumerAware가 붙은 리스너를 사용하면된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-kafka/reference/html/#message-listeners&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;h4 data-ke-size=&quot;size20&quot;&gt;2) AcksMode&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카프카 클라이언트에서는 3가지로 나뉘지만 스프링 카프카에서는 커밋의 종류를 7가지로 세분화 하고 로직을 만들어 놓았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #c1bef9;&quot;&gt;AckMode의 기본값은 BATCH이고 컨슈머의 enable.auto.commit 옵션은 false로 지정된다.&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;AcksMode 종류&lt;/b&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 237px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 20.2326%; text-align: center; height: 20px;&quot;&gt;&lt;b&gt;AcksMode&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 79.7674%; text-align: center; height: 20px;&quot;&gt;&lt;b&gt;설명&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 20.2326%; height: 20px;&quot;&gt;RECORD&lt;/td&gt;
&lt;td style=&quot;width: 79.7674%; height: 20px;&quot;&gt;레코드 단위로 프로세싱 이후 커밋&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 20.2326%; height: 20px;&quot;&gt;BATCH&lt;/td&gt;
&lt;td style=&quot;width: 79.7674%; height: 20px;&quot;&gt;poll() 메서드로 호출된 레코드가 모두 처리된 이후 커밋. 기본값.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 40px;&quot;&gt;
&lt;td style=&quot;width: 20.2326%; height: 40px;&quot;&gt;TIME&lt;/td&gt;
&lt;td style=&quot;width: 79.7674%; height: 40px;&quot;&gt;특정 시간 이후에 커밋&lt;br /&gt;시간 간격을 선언하는 AckTime 옵션을 설정해야 한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 40px;&quot;&gt;
&lt;td style=&quot;width: 20.2326%; height: 40px;&quot;&gt;COUNT&lt;/td&gt;
&lt;td style=&quot;width: 79.7674%; height: 40px;&quot;&gt;특정 개수만큼 레코드가 처리된 이후에 커밋&lt;br /&gt;레코드 개수를 선언하는 AckCount 옵션을 설정해야 한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 20.2326%; height: 20px;&quot;&gt;COUNT_TIME&lt;/td&gt;
&lt;td style=&quot;width: 79.7674%; height: 20px;&quot;&gt;TIME, COUNT 옵션 중 맞는 조건이 하나라도 나올 경우 커밋&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 60px;&quot;&gt;
&lt;td style=&quot;width: 20.2326%; height: 60px;&quot;&gt;MANUAL&lt;/td&gt;
&lt;td style=&quot;width: 79.7674%; height: 60px;&quot;&gt;Acknowledgement.acknowledge() 메서드가 호출되면 다음번 poll() 때 커밋을 한다.&lt;br /&gt;매번 acknowledge() 메서드를 호출하면 BATCH 옵션과 동일하게 작동한다.&lt;br /&gt;AcknowledgingMessageListener 또는 BatchAcknowledgingMessageListener를 사용해야 한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 20.2326%; height: 17px;&quot;&gt;MANUAL_IMMEDIATE&lt;/td&gt;
&lt;td style=&quot;width: 79.7674%; height: 17px;&quot;&gt;Acknowledgement.acknowledge() 메서드를 호출한 즉시 커밋한다.&lt;br /&gt;Acknowledging&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;MessageListener 또는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;BatchAcknowledgingMessageListener를 사용해야 한다.&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;h4 data-ke-size=&quot;size20&quot;&gt;3) 기본 리스너 컨테이너&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;(1) 레코드 리스너(MessageListener)&lt;/p&gt;
&lt;pre id=&quot;code_1678763821253&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring:
  kafka:
    consumer:
      bootstrap-servers: my-kafka:9092
    listener:
      type: RECORD&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리스너 타입을 RECORD로 설정한다.&lt;/p&gt;
&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;poll()이 호출되어 가져온 레코드들은 차례대로 개별 레코드의 메시지 값을 파라미터로 받게 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1678763912114&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@KafkaListener(topics = &quot;test&quot;,
        groupId = &quot;test-group-00&quot;)
public void recordListener(ConsumerRecord&amp;lt;String,String&amp;gt; record) {
    logger.info(record.toString());
}&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;기본값인 StringDeserializer 를 사용해 String 클래스로 메시지 값을 전달받는다.&lt;/p&gt;
&lt;pre id=&quot;code_1678763923052&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@KafkaListener(topics = &quot;test&quot;,
        groupId = &quot;test-group-01&quot;)
public void singleTopicListener(String messageValue) {
    logger.info(messageValue);
}&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: #333333; text-align: start;&quot;&gt;●&lt;span&gt; 개별 리스너에 컨슈머 옵션값을 부여&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1678763934166&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@KafkaListener(topics = &quot;test&quot;,
        groupId = &quot;test-group-02&quot;, properties = {
        &quot;max.poll.interval.ms:60000&quot;,
        &quot;auto.offset.reset:earliest&quot;
})
public void singleTopicWithPropertiesListener(String messageValue) {
    logger.info(messageValue);
}&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: #333333; text-align: start;&quot;&gt;●&lt;span&gt; 멀티 스레드 리스너&lt;/span&gt;&lt;/span&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;concurrency 옵션값에 해당하는 만큼 컨슈머 스레드를 만들어서 병렬처리한다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1678763943132&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@KafkaListener(topics = &quot;test&quot;,
        groupId = &quot;test-group-03&quot;,
        concurrency = &quot;3&quot;)
public void concurrentTopicListener(String messageValue) {
    logger.info(messageValue);
}&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: #333333; text-align: start;&quot;&gt;●&lt;span&gt; 특정 토픽의 특정 파티션만 구독&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1678763956058&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@KafkaListener(topicPartitions =
        {
                @TopicPartition(topic = &quot;test01&quot;, partitions = {&quot;0&quot;, &quot;1&quot;}),
                @TopicPartition(topic = &quot;test02&quot;, partitionOffsets = @PartitionOffset(partition = &quot;0&quot;, initialOffset = &quot;3&quot;))
        },
        groupId = &quot;test-group-04&quot;)
public void listenSpecificPartition(ConsumerRecord&amp;lt;String, String&amp;gt; record) {
    logger.info(record.toString());
}&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;style6&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;(2) 배치 리스너(BatchMessageListener)&lt;/p&gt;
&lt;pre id=&quot;code_1678764553081&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring:
  kafka:
    consumer:
      bootstrap-servers: my-kafka:9092
    listener:
      type: BATCH&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: #333333; text-align: start;&quot;&gt;&lt;span&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;카프카 클라이언트 라이브러리에서 poll() 메서드로 리턴받은 ConsumerRecords를 리턷받아 사용하는 것과 동일하다.&lt;/p&gt;
&lt;pre id=&quot;code_1678764645433&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@KafkaListener(topics = &quot;test&quot;,
        groupId = &quot;test-group-01&quot;)
public void batchListener(ConsumerRecords&amp;lt;String, String&amp;gt; records) {
    records.forEach(record -&amp;gt; logger.info(record.toString()));
}&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: #333333; text-align: start;&quot;&gt;&lt;span&gt; 메시지 값의 묶음을 파라미터로 받는 리스너&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1678764657257&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@KafkaListener(topics = &quot;test&quot;,
        groupId = &quot;test-group-02&quot;)
public void batchListener(List&amp;lt;String&amp;gt; list) {
    list.forEach(recordValue -&amp;gt; logger.info(recordValue));
}&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: #333333; text-align: start;&quot;&gt;&lt;span&gt; 멀티 스레드 리스너&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;concurrency 옵션값에 해당하는 만큼 컨슈머 스레드를 만들어서 병렬처리한다.&lt;/p&gt;
&lt;pre id=&quot;code_1678764672490&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@KafkaListener(topics = &quot;test&quot;,
        groupId = &quot;test-group-03&quot;,
        concurrency = &quot;3&quot;)
public void concurrentBatchListener(ConsumerRecords&amp;lt;String, String&amp;gt; records) {
    records.forEach(record -&amp;gt; logger.info(record.toString()));
}&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;style6&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;(3) 배치 커밋 컨슈머 리스너(BatchAcknowledgingConsumerMessageListener)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배치 컨슈머 리스너는 컨슈머 인스턴스를 파라미터로 받아 동기, 비동기 커밋을 사용할 수 있고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배치 커밋 리스너는 Acknoledgment 인스턴스를 파라미터로 받아 AckMode 를 사용할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1678764983073&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring:
  kafka:
    consumer:
      bootstrap-servers: my-kafka:9092
    listener:
      type: BATCH
      ack-mode: MANUAL_IMMEDIATE&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: #333333; text-align: start;&quot;&gt;&lt;span&gt;&lt;span&gt; 배치 커밋 리스너&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&lt;span&gt;AckMode를 MANUAL 또는 MANUAL_IMMEDIATE로 사용할 경우 수동 커밋을 하기 위해 파라미터로 Acknowledgment 인스턴스를 받아야 한다. acknowledge() 메서드를 호출함으로써 커밋을 수행할 수 있다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1678765012909&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@KafkaListener(topics = &quot;test&quot;, groupId = &quot;test-group-01&quot;)
public void commitListener(ConsumerRecords&amp;lt;String, String&amp;gt; records, Acknowledgment ack) {
    records.forEach(record -&amp;gt; logger.info(record.toString()));
    ack.acknowledge();
}&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: #333333; text-align: start;&quot;&gt;&lt;span&gt;&lt;span&gt; 배치 컨슈머 리스너&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&lt;span&gt;동기, 비동기 커밋을 사용하고 싶다면 컨슈머 인스턴스를 파라미터로 받아서 사용할 수 있다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&lt;span&gt;consumer 인스턴스의 conmmitSync(), conmmitAsync() 메서드를 호출하면 사용자가 원하는 타이밍에 커밋할 수 있다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1678765022112&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@KafkaListener(topics = &quot;test&quot;, groupId = &quot;test-group-02&quot;)
public void consumerCommitListener(ConsumerRecords&amp;lt;String, String&amp;gt; records, Consumer&amp;lt;String, String&amp;gt; consumer) {
    records.forEach(record -&amp;gt; logger.info(record.toString()));
    consumer.commitSync();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4) 커스텀 리스너 컨테이너&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서로 다른 설정을 가진 2개 이상의 리스너를 구현하거나 리밸런스 리스너를 구현하기 위해서는 커스텀 리스너 컨테이너를 사용해야 한다. 카프카 리스너 컨테이너 팩토리(KafkaListenerContainerFactory) 인스턴스를 생성해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1678765728330&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
public class ListenerContainerConfiguration {

    @Bean
    public KafkaListenerContainerFactory&amp;lt;ConcurrentMessageListenerContainer&amp;lt;String, String&amp;gt;&amp;gt; customContainerFactory() {
        Map&amp;lt;String, Object&amp;gt; props = new HashMap&amp;lt;&amp;gt;();
        props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, &quot;my-kafka:9092&quot;);
        props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
        props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);

        DefaultKafkaConsumerFactory cf = new DefaultKafkaConsumerFactory&amp;lt;&amp;gt;(props);
        ConcurrentKafkaListenerContainerFactory&amp;lt;String, String&amp;gt; factory = new ConcurrentKafkaListenerContainerFactory&amp;lt;&amp;gt;();
        factory.getContainerProperties().setConsumerRebalanceListener(new ConsumerAwareRebalanceListener() {
            @Override
            public void onPartitionsRevokedBeforeCommit(Consumer&amp;lt;?, ?&amp;gt; consumer, Collection&amp;lt;TopicPartition&amp;gt; partitions) {
            }

            @Override
            public void onPartitionsRevokedAfterCommit(Consumer&amp;lt;?, ?&amp;gt; consumer, Collection&amp;lt;TopicPartition&amp;gt; partitions) {
            }

            @Override
            public void onPartitionsAssigned(Collection&amp;lt;TopicPartition&amp;gt; partitions) {
            }

            @Override
            public void onPartitionsLost(Collection&amp;lt;TopicPartition&amp;gt; partitions) {
            }
        });
        factory.setBatchListener(false);
        factory.getContainerProperties().setAckMode(ContainerProperties.AckMode.RECORD);
        factory.setConsumerFactory(cf);
        return factory;
    }
}

@KafkaListener(topics = &quot;test&quot;,
        groupId = &quot;test-group&quot;,
        containerFactory = &quot;customContainerFactory&quot;)
public void customListener(String data) {
    logger.info(data);
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DefaultKafkaConsumerFactory로 리스너 컨테이너 팩토리를 생성할 때 컨슈머 기본 옵션을 설정한다.&lt;/li&gt;
&lt;li&gt;ConcurrentKafkaListenerContainerFactory로 concurrency 옵션을 설정할 수 있는 리스너를 만들 때 사용하는 리스너 컨테이너를 만든다.&lt;/li&gt;
&lt;li&gt;setConsumerRebalanceListener() 메서드를 호출해 리밸런스 리스너를 만든다.&lt;/li&gt;
&lt;li&gt;setBatchListener() 메서드에 파라미터로 false를 넣어 리코드 리스너임을 명시한다.&lt;/li&gt;
&lt;li&gt;setAckMode() 메서드에 파라미터로 RECORD를 넣어 레코드 단위 커밋을 설정한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a style=&quot;color: #3db39e;&quot; href=&quot;http://www.yes24.com/Product/Goods/99122569&quot;&gt;아파치 카프카 애플리케이션 프로그래밍&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a style=&quot;color: #3db39e;&quot; href=&quot;https://www.inflearn.com/course/%EC%B9%B4%ED%94%84%EC%B9%B4-%EC%99%84%EB%B2%BD%EA%B0%80%EC%9D%B4%EB%93%9C-%EC%BD%94%EC%96%B4&quot;&gt;카프카&amp;nbsp;완벽&amp;nbsp;가이드&amp;nbsp;-&amp;nbsp;코어편&lt;/a&gt;&lt;/p&gt;</description>
      <category>Kafka</category>
      <category>스프링 카프카 컨슈머</category>
      <category>카프카 컨슈머</category>
      <author>mandykr</author>
      <guid isPermaLink="true">https://mandykr.tistory.com/97</guid>
      <comments>https://mandykr.tistory.com/97#entry97comment</comments>
      <pubDate>Thu, 2 Feb 2023 11:04:31 +0900</pubDate>
    </item>
    <item>
      <title>[Kafka] 3. 카프카 프로듀서</title>
      <link>https://mandykr.tistory.com/96</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;목차&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 프로듀서 Client&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. Serializer&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. Partitioner&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. Accumulator&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5. Sender&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;6. 멱등성(idempotence) 프로듀서&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;8. 스프링 카프카 프로듀서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 프로듀서 Client&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #666666;&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;1827&quot; data-origin-height=&quot;482&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tPh2p/btr0n8q9GKN/R1XsOAIF4qWtVppNt2KKr1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tPh2p/btr0n8q9GKN/R1XsOAIF4qWtVppNt2KKr1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tPh2p/btr0n8q9GKN/R1XsOAIF4qWtVppNt2KKr1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtPh2p%2Fbtr0n8q9GKN%2FR1XsOAIF4qWtVppNt2KKr1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1827&quot; height=&quot;482&quot; data-origin-width=&quot;1827&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;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;기본 프로듀서 Client API 처리 로직&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1677119628623&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 1. Producer 환경 설정(Properties 객체를 이용)
String topicName = &quot;simple-topic&quot;;
Properties props  = new Properties();
props.setProperty(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, &quot;192.168.56.101:9092&quot;);
props.setProperty(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
props.setProperty(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());

// 2. 1에서 설정한 환경 설정값을 반영하여 KafkaProducer 객체 생성.
KafkaProducer&amp;lt;String, String&amp;gt; kafkaProducer = new KafkaProducer&amp;lt;String, String&amp;gt;(props);

// 3. 토픽명과 메시지 값(Key, Value) 을 입력하여 보낼 메시지인 ProducerRecord 객체 생성
ProducerRecord&amp;lt;String, String&amp;gt; producerRecord = new ProducerRecord&amp;lt;&amp;gt;(topicName, &quot;hello world&quot;);

// 4. KafkaProducer객체의 send( )메소드를 호출하여 ProducerRecord 전송
kafkaProducer.send(producerRecord);
kafkaProducer.flush();

// 5. KafkaProducer객체의 close( )메소드를 호출하여 종료
kafkaProducer.close();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. Serializer&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1518&quot; data-origin-height=&quot;417&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0x3dd/btr0qsbkvpN/V1skyuKssyhYPPGQDEdy1k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0x3dd/btr0qsbkvpN/V1skyuKssyhYPPGQDEdy1k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0x3dd/btr0qsbkvpN/V1skyuKssyhYPPGQDEdy1k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0x3dd%2Fbtr0qsbkvpN%2FV1skyuKssyhYPPGQDEdy1k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1518&quot; height=&quot;417&quot; data-origin-width=&quot;1518&quot; data-origin-height=&quot;417&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카프카 프로듀서는 Serializer를 통해 데이터를 Byte Array 형태로 전송하고 파티션에서도 Byte Array 형태로 저장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바 객체(Object)도 Serialization을 통해서 바이트 배열(바이트 스트림) 형태로 저장하기 때문에 객체의 유형, 데이터의 포맷, 적용 시스템에 상관없이 이동/저장/복원이 자유롭다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카프카 컨슈머에서는 Byte Array를 받아서 DeSerialization 과정을 거쳐 원본 객체로 변환한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카프카 프로듀서 클라이언트에서 메시지를 Serialization 하는데 활용할 Serializer 를 명시해줘야 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1677119762986&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Properties props  = new Properties();
props.setProperty(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
props.setProperty(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()&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;카프카 클라이언트가 제공하는 기본 Serializer&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;StringSerializer&lt;/li&gt;
&lt;li&gt;ShortSerializer&lt;/li&gt;
&lt;li&gt;IntegerSerializer&lt;/li&gt;
&lt;li&gt;LongSerializer&lt;/li&gt;
&lt;li&gt;DoubleSerializer&lt;/li&gt;
&lt;li&gt;BytesSerializer&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. Partitioner&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;파티셔너는 ProducerRecord를 토픽의 어느 파티션으로 전송할 지 결정한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;기본 파티셔너는 DefaultPartitioner 이고 프로듀서 API는 RoundRobinPartitioner, UniformStickyPartitioner를 제공한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Partitioner 인터페이스를 상속받은 사용자 정의 클래스를 만들 수 있다.&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;메시지는 Partitioner를 통해 토픽의 어떤 파티션으로 전송되어야 할 지 미리 결정 되는데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Key값을 가지지 않는 경우 라운드 로빈(Round Robin), 스티키 파티션(Sticky Partition)등의 파티션 전략등이 선택되어 파티션 별로 메시지가 전송된다.&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;509&quot; data-origin-height=&quot;367&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/crQvRx/btr0mcMqQ9w/NDbkPdFBxoqV0mpr9pcGhk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/crQvRx/btr0mcMqQ9w/NDbkPdFBxoqV0mpr9pcGhk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/crQvRx/btr0mcMqQ9w/NDbkPdFBxoqV0mpr9pcGhk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcrQvRx%2Fbtr0mcMqQ9w%2FNDbkPdFBxoqV0mpr9pcGhk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;359&quot; height=&quot;259&quot; data-origin-width=&quot;509&quot; data-origin-height=&quot;367&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&gt;파티셔닝되기 때문에 메시지 순서가 보장된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;또한, 하나의 파티션에 서로 다른 키를 가지는 메시지가 저장될 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1677033763547&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 키를 가지는 메시지 전송
$ bin/kafka-console-producer.sh --bootstrap-server kafka-0:9092,kafka-1:9092,kafka-2:9092 --topic topic-p2r3 \
--property key.separator=: --property parse.key=true
&amp;gt;K1:V1
&amp;gt;K2:V1
&amp;gt;K1:V2
&amp;gt;K2:V2
&amp;gt;K3:V1&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1677034856404&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 키를 가지는 메시지 컨슘
$ bin/kafka-console-consumer.sh --bootstrap-server kafka-0:9092,kafka-1:9092,kafka-2:9092 --topic topic-p2r3 \
--property print.key=true --property print.value=true \
--property print.partition=true

Partition:0    K1    V1
Partition:1    K2    V1
Partition:0    K1    V2
Partition:1    K2    V2
Partition:0    K3    V1&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;b&gt;&lt;span&gt;RoundRobinPartitioner&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;카프카 클라이언트 2.4.0 이전 버전에서 기본 파티셔너&lt;/li&gt;
&lt;li&gt;메시지 키가 있으면 키의 해시값과 파티션을 매칭&lt;/li&gt;
&lt;li&gt;ProducerRecord가 들어오는 대로 파티션을 순회하면서 전송&lt;/li&gt;
&lt;li&gt;데이터가 배치로 묶이는 빈도가 적다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span&gt;UniformStickyPartitioner&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;카프카 클라이언트&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;2.5.0 버전에서 기본 파티셔너&lt;/li&gt;
&lt;li&gt;메시지 키가 있으면 키의 해시값과 파티션을 매칭&lt;/li&gt;
&lt;li&gt;특정&amp;nbsp;파티션으로&amp;nbsp;전송되는&amp;nbsp;하나의&amp;nbsp;배치에&amp;nbsp;메시지를&amp;nbsp;빠르게&amp;nbsp;먼저&amp;nbsp;채워서&amp;nbsp;보내는&amp;nbsp;방식&lt;/li&gt;
&lt;li&gt;어큐뮬레이터에서 데이터가 배치로 모두 묶일 때까지 기다렸다가 배치로 묶인 데이터는 모두 동일한 파티션에 전송&lt;/li&gt;
&lt;li&gt;RoundRobinPartitioner에 비해 향상된 성능을 가진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. Accumulator&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Record Accumulator는 Partitioner에 의해서 메시지 배치가 전송이 될 토픽과 Partition에 따라 저장되는 KafkaProducer 메모리 영역이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;KafkaProducer 객체의 send( ) 메소드는 호출 시마다 하나의 ProducerRecord를 입력하지만 바로 전송 되지 않고 Accumulator에 단일 메시지를 토픽 파티션에 따라 Record Batch 단위로 묶인 뒤 전송된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메시지들은 Accumulator에 여러 개의 Batch들로 buffer.memory 설정 사이즈 만큼 보관될 수 있으며 메시지 전송은 Sender Thread에 의해 여러 개의 Batch들로 한꺼번에 전송할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Sender Thread는 Accumulator에 누적된 메시지 배치를 꺼내서 브로커로 전송한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1개의 Batch를 가져갈수도, 여러 개의 Batch를 가져 갈 수도 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1908&quot; data-origin-height=&quot;686&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/obwP3/btr0DVDG8ZZ/AxD4LfibQrDwMyNKg1cex0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/obwP3/btr0DVDG8ZZ/AxD4LfibQrDwMyNKg1cex0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/obwP3/btr0DVDG8ZZ/AxD4LfibQrDwMyNKg1cex0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FobwP3%2Fbtr0DVDG8ZZ%2FAxD4LfibQrDwMyNKg1cex0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1908&quot; height=&quot;686&quot; data-origin-width=&quot;1908&quot; data-origin-height=&quot;686&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;buffer.memory : Record accumulator의 전체 메모리 사이즈&lt;/li&gt;
&lt;li&gt;linger.ms : Sender Thread로 메시지를 보내 기전 배치로 메시지를 만들어서 보내기&amp;nbsp;위한&amp;nbsp;최대&amp;nbsp;대기&amp;nbsp;시간&lt;/li&gt;
&lt;li&gt;batch.size : 단일&amp;nbsp;배치의&amp;nbsp;사이즈&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전반적인 Producer와 Broker간의 전송이 느리다면 linger.ms를 0보다 크게 설정하여 Sender Thread가 하나의 Record Batch를 가져갈 때 일정 시간 대기하여 Record Batch에 메시지를 보다 많이 채울 수 있도록 적용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;linger.ms는 보통 20ms 이하로 설정을 권장한다. 기본값은 0.&lt;/p&gt;
&lt;pre id=&quot;code_1677206302982&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;props.setProperty(ProducerConfig.BATCH_SIZE_CONFIG, &quot;32000&quot;); // 배치 사이즈 설정
props.setProperty(ProducerConfig.LINGER_MS_CONFIG, &quot;20&quot;); // linger ms 설정&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. Sender&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1737&quot; data-origin-height=&quot;340&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kY1Fw/btr0vddrp6G/3xeeg3k2llga38yk2xhr70/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kY1Fw/btr0vddrp6G/3xeeg3k2llga38yk2xhr70/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kY1Fw/btr0vddrp6G/3xeeg3k2llga38yk2xhr70/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkY1Fw%2Fbtr0vddrp6G%2F3xeeg3k2llga38yk2xhr70%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1737&quot; height=&quot;340&quot; data-origin-width=&quot;1737&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;span style=&quot;background-color: #ffffff; color: #666666;&quot;&gt;Producer Client의 Main Thread가 send( ) 메소드를 호출하면 메시지 전송을 시작하지만 바로 전송되지 않으며 내부 Buffer에 메시지를 저장 후에 별도의 Thread가 Kafka Broker에 실제 전송을 하는 방식이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #666666;&quot;&gt;1) 동기 / 비동기 메시지 전송&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #666666;&quot;&gt;(1) 동기 메시지 전송&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;1478&quot; data-origin-height=&quot;470&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/SQkaB/btr0sANt7RT/KKby5fJrrQwCGdBugEeNuK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/SQkaB/btr0sANt7RT/KKby5fJrrQwCGdBugEeNuK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/SQkaB/btr0sANt7RT/KKby5fJrrQwCGdBugEeNuK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSQkaB%2Fbtr0sANt7RT%2FKKby5fJrrQwCGdBugEeNuK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1478&quot; height=&quot;470&quot; data-origin-width=&quot;1478&quot; data-origin-height=&quot;470&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Future&amp;lt;RecordMetaData&amp;gt; = KafkaProducer.send() 기본적으로 비동기 호출인데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #666666;&quot;&gt;Future객체의 get( )을 호출하여 브로커로 부터 메시지 Ack 응답을 받을 때 까지 Main Thread를 대기 시키는 방식 으로 동기화를 구성 할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1677118665278&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;try {
    RecordMetadata recordMetadata = kafkaProducer.send(producerRecord).get();
    logger.info(&quot;\n ###### record metadata received ##### \n&quot; +
            &quot;partition:&quot; + recordMetadata.partition() +&quot;\n&quot; +
            &quot;offset:&quot; + recordMetadata.offset() + &quot;\n&quot; +
            &quot;timestamp:&quot; + recordMetadata.timestamp());
} catch (ExecutionException e) {
    e.printStackTrace();
} catch (InterruptedException e) {
    e.printStackTrace();
} finally {
    kafkaProducer.close();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동기 메시지 전송은 Record Accumulator 의 배치에 메시지를 채우고 Acks 를 기다리기 때문에 linger.ms 설정값 만큼 대기한 후 Sender Thread 가 메시지를 카프카 브로커로 전송하고 Acks 를 받은 후 RecordMetadata 를 받는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 배치에 담기는 메시지는 항상 1개이기 때문에 &lt;span style=&quot;background-color: #c1bef9;&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;size18&quot;&gt;(2) 비동기 메시지 전송&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1475&quot; data-origin-height=&quot;470&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bxmZG6/btr0m5Pjbyk/38RpZwlAMDDnDQcGe0mack/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bxmZG6/btr0m5Pjbyk/38RpZwlAMDDnDQcGe0mack/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bxmZG6/btr0m5Pjbyk/38RpZwlAMDDnDQcGe0mack/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbxmZG6%2Fbtr0m5Pjbyk%2F38RpZwlAMDDnDQcGe0mack%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1475&quot; height=&quot;470&quot; data-origin-width=&quot;1475&quot; data-origin-height=&quot;470&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Main Thread 에서 콜백 메시지를 전송할 때 콜백 객체를 함께 보내고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Sender 에서 카프카 클러스터의 응답을 받아 콜백 객체에 선언된 메소드를 실행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Main Thread 에서는 카프카 클러스터 응답에 대한 Exception 이나 RecordMetadata 를 확인한다.&lt;/p&gt;
&lt;pre id=&quot;code_1677118939539&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;kafkaProducer.send(producerRecord, (metadata, exception)-&amp;gt; {
    if (exception == null) {
        logger.info(&quot;\n ###### record metadata received ##### \n&quot; +
                &quot;partition:&quot; + metadata.partition() + &quot;\n&quot; +
                &quot;offset:&quot; + metadata.offset() + &quot;\n&quot; +
                &quot;timestamp:&quot; + metadata.timestamp());
    } else {
        logger.error(&quot;exception error from broker &quot; + exception.getMessage());
    }
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2) acks 옵션&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;acks 옵션으로 프로듀서가 메시지를 전송하고 다음 메시지를 전송하기 전에 리더 파티션으로부터 응답을 기다릴지에 대해 설정할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본값은 -1이다.&lt;/p&gt;
&lt;pre id=&quot;code_1677203446779&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;props.setProperty(ProducerConfig.ACKS_CONFIG, &quot;0&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;(1) acks = 0&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리더 브로커가 메시지를 정상적으로 받았는지에 대한 Ack 메시지를 받지 않고 다음 메시지를 바로 전송&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;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1633&quot; data-origin-height=&quot;787&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/A1D2X/btr0GaHntuC/4uU1dvfiZk8usQMU8XiMSK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/A1D2X/btr0GaHntuC/4uU1dvfiZk8usQMU8XiMSK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/A1D2X/btr0GaHntuC/4uU1dvfiZk8usQMU8XiMSK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FA1D2X%2Fbtr0GaHntuC%2F4uU1dvfiZk8usQMU8XiMSK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;711&quot; height=&quot;343&quot; data-origin-width=&quot;1633&quot; data-origin-height=&quot;787&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;(2) acks = 1&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리더 브로커가 메시지를 정상적으로 받았는지에 대한 Ack 메시지를 받은 후에 다음 메시지를 바로 전송&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;있음&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1633&quot; data-origin-height=&quot;787&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/VqjcY/btr0w1y5aOn/oWJYqV03ygwjq03CIoI9a1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/VqjcY/btr0w1y5aOn/oWJYqV03ygwjq03CIoI9a1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/VqjcY/btr0w1y5aOn/oWJYqV03ygwjq03CIoI9a1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVqjcY%2Fbtr0w1y5aOn%2FoWJYqV03ygwjq03CIoI9a1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;712&quot; height=&quot;343&quot; data-origin-width=&quot;1633&quot; data-origin-height=&quot;787&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;(3) acks = all, -1&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;min.insync.replicas 개수 만큼의 Replicator에 복제를 수행한 뒤에 리더 브로커가 보내는 Ack 를 받은 후 다음 메시지를 바로 전송. 만약 오류 메시지를 브로커로 부터 받으면 메시지를 재전송&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1633&quot; data-origin-height=&quot;843&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bfQtcB/btr0ANTFFQB/QsFzJesLmC679afSs6Kww1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bfQtcB/btr0ANTFFQB/QsFzJesLmC679afSs6Kww1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bfQtcB/btr0ANTFFQB/QsFzJesLmC679afSs6Kww1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbfQtcB%2Fbtr0ANTFFQB%2FQsFzJesLmC679afSs6Kww1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;712&quot; height=&quot;368&quot; data-origin-width=&quot;1633&quot; data-origin-height=&quot;843&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3) 메시지&amp;nbsp;전송/재&amp;nbsp;전송&amp;nbsp;시간&amp;nbsp;파라미터&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1707&quot; data-origin-height=&quot;560&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/M6vRX/btr0BCdr58p/dxGpcao1hNKxyZN66Dy0SK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/M6vRX/btr0BCdr58p/dxGpcao1hNKxyZN66Dy0SK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/M6vRX/btr0BCdr58p/dxGpcao1hNKxyZN66Dy0SK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FM6vRX%2Fbtr0BCdr58p%2FdxGpcao1hNKxyZN66Dy0SK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1707&quot; height=&quot;560&quot; data-origin-width=&quot;1707&quot; data-origin-height=&quot;560&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;max.block.ms : Send( ) 호출 시 Record Accumulator에 입력하지 못하고 block되는 최대 시간.&lt;br /&gt;초과시 Timeout Exception&lt;/li&gt;
&lt;li&gt;linger.ms : Sender Thread가 Record Accumulator에서 배치별로 가져가기 위한 최대 대기시간.&lt;/li&gt;
&lt;li&gt;request.timeout.ms : 전송에 걸리는 최대 시간. 전송 재 시도 대기시간 제외.&lt;br /&gt;초과시 retry를 하거나 Timout Exception 발생&lt;/li&gt;
&lt;li&gt;retry.backoff.ms : 전송 재 시도를 위한 대기 시간.&lt;br /&gt;retries 는 재 전송 횟수를 설정&lt;/li&gt;
&lt;li&gt;delivery.timeout.ms : Producer 메시지(배치) 전송에 허용된 최대 시간. &lt;br /&gt;( delivery.timeout.ms &amp;gt;= linger.ms + request.timeout.ms )&lt;br /&gt;초과시 Timeout Exception&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예) retries=10, request.timeout.ms=10000ms, retry.backoff.ms=30 인 경우,&lt;br /&gt;request.timeout.ms 만큼 기다린후 재 전송하기전 30ms 이후 재전송 시도&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이와 같은 방식으로 재 전송을 10회 시도하고 더 이상&amp;nbsp;retry&amp;nbsp;시도&amp;nbsp;하지&amp;nbsp;않음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;max.in.flight.requests.per.connection : 비동기 전송 시 브로커의 응답없이 한꺼번에 보낼 수 있는 Batch의 개수. &lt;br /&gt;기본값은 5. ( &lt;span style=&quot;background-color: #c1bef9;&quot;&gt;Kafka Producer의 메시지 전송 단위는 Batch 이다.&lt;/span&gt; )&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. 멱등성(idempotence) 프로듀서&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Producer는 브로커로 부터 ACK를 받은 다음에 다음 메시지를 전송하는데, Producer ID와 메시지 Sequence를 Header에 저장 하여 전송한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브로커는 Producer가 보낸 메시지의 Sequence가 브로커가 가지고 있는 메시지의 Sequence보다 1만큼 큰 경우에만 브로커에 저장하고 &lt;span style=&quot;background-color: #c1bef9;&quot;&gt;브로커에서 메시지 Sequece가 중복 될 경우 이를 메시지 로그에 기록하지 않고 Ack만 전송한다.&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;929&quot; data-origin-height=&quot;525&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b2kSzy/btr02AF4aGY/Dpt2nbBhHL9ihBOzhGEkQ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b2kSzy/btr02AF4aGY/Dpt2nbBhHL9ihBOzhGEkQ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b2kSzy/btr02AF4aGY/Dpt2nbBhHL9ihBOzhGEkQ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb2kSzy%2Fbtr02AF4aGY%2FDpt2nbBhHL9ihBOzhGEkQ1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;332&quot; data-origin-width=&quot;929&quot; data-origin-height=&quot;525&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Idempotence 를 위한 Producer 설정&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;enable.idempotence&amp;nbsp;=&amp;nbsp;true&lt;/li&gt;
&lt;li&gt;acks = all&lt;/li&gt;
&lt;li&gt;retries는 0 보다 큰 값&lt;/li&gt;
&lt;li&gt;max.in.flight.requests.per.connection은 1에서 5사이 값&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka 3.0 버전 부터는 Producer의 기본 설정이 Idempotence 이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;acks=1로 설정하는 등 다른 파라미터를 변경하면 &lt;span&gt;Idempotence&lt;span&gt; 로 동작하지 않지만 메시지는 전송된다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span style=&quot;background-color: #c1bef9;&quot;&gt;enable.idempotence = true 를 명시적으로 설정&lt;/span&gt;하고 다른 파라미터를 잘못 설정하면 Config&amp;nbsp;오류가 &lt;br /&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;시퀀스 넘버가 일정하지 않은 경우에는 OutOfOrderSequenceExceptioin 이 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(예: 시퀀스 넘버 0 다음에 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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. 트랜잭션 프로듀서&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션 프로듀서는 다수의 파티션에 데이터를 저장할 경우 모든 데이터에 대해 동일한 원자성을 만족시키기 위해 사용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1679&quot; data-origin-height=&quot;614&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EuLNt/btr02ATHCZD/MtQNdkJK1aYRrvKVvER7DK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EuLNt/btr02ATHCZD/MtQNdkJK1aYRrvKVvER7DK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EuLNt/btr02ATHCZD/MtQNdkJK1aYRrvKVvER7DK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEuLNt%2Fbtr02ATHCZD%2FMtQNdkJK1aYRrvKVvER7DK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;252&quot; data-origin-width=&quot;1679&quot; data-origin-height=&quot;614&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;트랜잭션 프로듀서를 위한 Producer 설정&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;enable.idempotence = true&lt;/li&gt;
&lt;li&gt;transactionl.id 를 임의의 String 값으로 설정&lt;/li&gt;
&lt;li&gt;컨슈머의 isolation.level = read_committed&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션 프로듀서는 사용자가 보낸 데이터를 레코드로 파티션에 저장할 뿐만 아니라 트랜잭션의 시작과 끝을 표현하기 위해 트랜잭션 레코드를 한 개 더 보낸다. (트랜잭션 레코드는 오프셋을 한 개 차지한다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션 컨슈머는 파티션에 저장된 트랜잭션 레코드를 보고 트랜잭션이 완료되었음을 확인하고 데이터를 가져간다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8. 스프링 카프카 프로듀서&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;스프링 카프카 라이브러리 디펜던시 추가&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1677468916162&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;dependencies {
    implementation 'org.springframework.boot:spring-boot-starter'
    implementation 'org.springframework.kafka:spring-kafka'
}&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;스프링 카프카 프로듀서는 Kafka Template 이라는 클래스를 사용해 데이터를 전송할 수 있다.&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;1) 기본 카프카 템플릿&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;application.properties 에 프로듀서 옵션을 설정할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;별도로 선언하지 않으면 기본값으로 설정된다.&lt;/p&gt;
&lt;pre id=&quot;code_1677469914460&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring.kafka.producer.acks
spring.kafka.producer.batch-size
spring.kafka.producer.bootstrap-servers
spring.kafka.producer.buffer-memory
spring.kafka.producer.client-id
spring.kafka.producer.compression-type
spring.kafka.producer.key-serialzier
spring.kafka.producer.properties.*
spring.kafka.producer.retries
spring.kafka.producer.transaction-id-prefix&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;메시지 전송&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1677470058060&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private static String TOPIC_NAME = &quot;test&quot;;

@Autowired
private KafkaTemplate&amp;lt;Integer, String&amp;gt; template;

template.send(TOPIC_NAME, &quot;test&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;KafkaTemplate 객체를 스프링 빈에서 주입받아 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;send() 메서드를 사용해서 토픽 이름과 케시지 값을 넣어 전송한다.&lt;/p&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;2) 커스텀 카프카 템플릿&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커스텀 카프카 템플릿은 프로듀서 팩토리를 통해 만든 카프카 템플릿 객체를 빈으로 등록하여 사용할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1677470331007&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
public class KafkaTemplateConfiguration {

    @Bean
    public KafkaTemplate&amp;lt;String, String&amp;gt; customKafkaTemplate() {

        Map&amp;lt;String, Object&amp;gt; props = new HashMap&amp;lt;&amp;gt;();
        props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, &quot;my-kafka:9092&quot;);
        props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
        props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
        props.put(ProducerConfig.ACKS_CONFIG, &quot;all&quot;);

        ProducerFactory&amp;lt;String, String&amp;gt; pf = new DefaultKafkaProducerFactory&amp;lt;&amp;gt;(props);

        return new KafkaTemplate&amp;lt;&amp;gt;(pf);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커스텀 프로듀서는 필수 옵션(bootstrap.servers, key.serializer, value.serializer)을 포함한 옵션들을 선언해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ProducerFactory 를 property 로 초기화 하고 KafkaTemplate을 빈으로 등록한다.&lt;/p&gt;
&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_1677470594923&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Autowired
private KafkaTemplate&amp;lt;String, String&amp;gt; customKafkaTemplate;

ListenableFuture&amp;lt;SendResult&amp;lt;String, String&amp;gt;&amp;gt; future = customKafkaTemplate.send(TOPIC_NAME, &quot;test&quot;);
future.addCallback(new KafkaSendCallback&amp;lt;String, String&amp;gt;() {
    @Override
    public void onSuccess(SendResult&amp;lt;String, String&amp;gt; result) {
    }

    @Override
    public void onFailure(KafkaProducerException ex) {
    }
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ListenableFuture 인스턴스에 addCallback 함수를 붙여 프로듀서가 보낸 데이터의 브로커 적재여부를 비동기로 확인할 수 있다. 브로커에 정상 적재되었다면 onSuccess 메서드가, 적재되지 않고 이슈가 발생했다면 onFailure 메서드가 호출된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;http://www.yes24.com/Product/Goods/99122569&quot;&gt;아파치 카프카 애플리케이션 프로그래밍&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.inflearn.com/course/%EC%B9%B4%ED%94%84%EC%B9%B4-%EC%99%84%EB%B2%BD%EA%B0%80%EC%9D%B4%EB%93%9C-%EC%BD%94%EC%96%B4&quot;&gt;카프카&amp;nbsp;완벽&amp;nbsp;가이드&amp;nbsp;-&amp;nbsp;코어편&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;</description>
      <category>Kafka</category>
      <category>카프카 프로듀서</category>
      <author>mandykr</author>
      <guid isPermaLink="true">https://mandykr.tistory.com/96</guid>
      <comments>https://mandykr.tistory.com/96#entry96comment</comments>
      <pubDate>Thu, 2 Feb 2023 11:03:34 +0900</pubDate>
    </item>
    <item>
      <title>[Kafka] 2. 토픽과 파티션</title>
      <link>https://mandykr.tistory.com/95</link>
      <description>&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;2. 토픽 정리 정책&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;4. 복제와 리더 / 팔로워 파티션&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5. ISR&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;6. Kafka 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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토픽과 파티션을 사용함에 있어 발생하는 여러가지 운영상 고려사항을 확인하고 멀티 노드 카프카 클러스터를 구성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 적정 파티션 개수&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토픽의 파티션 개수는 카프카의 성능과 관련이 있기 때문에 적절한 파티션 개수를 설정하고 운영하는 것이 매우 중요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;토픽 생성시 파티션 개수 고려사항&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터 처리량&lt;/li&gt;
&lt;li&gt;메시지 키 사용 여부&lt;/li&gt;
&lt;li&gt;브로커, 컨슈머 영향도&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) 데이터 처리량&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 처리량을 늘리기 위해 파티션을 추가하고 파티션 개수만큼 컨슈머를 추가해서 병렬 처리량을 늘릴 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(컨슈머가 실행되는 서버의 사양을 올리는 스케일업이나 GC 튜닝 등 컨슈머의 처리량을 늘리는 방법도 있다.)&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; &amp;lt;&amp;nbsp; 컨슈머 데이터 처리량&amp;nbsp; x&amp;nbsp; 파티션 개수&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프로듀서가 보내는 데이터가 초당 1,000 레코드이고,&lt;/li&gt;
&lt;li&gt;컨슈머가 처리할 수 있는 데이터가 초당 100 레코드이면,&lt;/li&gt;
&lt;li&gt;파티션 개수는 10&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2) 컨슈머 데이터 처리량&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;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;컨슈머 데이터 처리량 측정&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;컨슈머 데이터 처리량은 운영중인 카프카에서 더미 데이터 테스트로 구할 수 있다.&lt;/li&gt;
&lt;li&gt;그리고 프로듀서가 보내는 데이터 양을 하루, 시간, 분 단위로 쪼개서 예측한다.&lt;/li&gt;
&lt;li&gt;데이터의 최대치를 데이터 생성량으로 잡는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3) 메시지 키 사용 여부&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메시지 키를 사용하면 데이터 처리 순서가 보장받지 못하는 경우가 생길 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;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;970&quot; data-origin-height=&quot;521&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bGzuMB/btrYjySAQ19/6UM9LK1CpGggoTnbqDBBGk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bGzuMB/btrYjySAQ19/6UM9LK1CpGggoTnbqDBBGk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bGzuMB/btrYjySAQ19/6UM9LK1CpGggoTnbqDBBGk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbGzuMB%2FbtrYjySAQ19%2F6UM9LK1CpGggoTnbqDBBGk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;308&quot; data-origin-width=&quot;970&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[ 파티션 추가 후 ]&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;969&quot; data-origin-height=&quot;659&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/baSEXJ/btrYj2lBWm7/NYV12h6t5uOt9KXFq7PhAK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/baSEXJ/btrYj2lBWm7/NYV12h6t5uOt9KXFq7PhAK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/baSEXJ/btrYj2lBWm7/NYV12h6t5uOt9KXFq7PhAK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbaSEXJ%2FbtrYj2lBWm7%2FNYV12h6t5uOt9KXFq7PhAK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;392&quot; data-origin-width=&quot;969&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;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로듀서가 기본 파티셔너를 사용할 때,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메시지 키를 사용하면 프로듀서가 토픽으로 데이터를 보낼 때 메시지 키를 해시 변환하여 메시지 키를 파티션에 매칭시킨다.&lt;/li&gt;
&lt;li&gt;만약 파티션 개수가 달라지면 이미 매칭된 파티션과 메시지 키의 매칭이 깨지고 전혀 다른 파티션에 데이터가 할당된다.&lt;/li&gt;
&lt;li&gt;파티션을 변환하기 이전과 이후 메시지 키의 파티션 위치가 달라지기 때문에 컨슈머는 메시지 키의 순서를 더는 보장받지 못한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 &lt;b&gt;메시지 처리 순서가 보장되어야 한다면&lt;/b&gt;,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파티션 개수를 프로듀서가 전송하는 데이터양보다 더 넉넉하게 잡고 최대한 파티션의 변화가 발생하지 않는 방향으로 운영해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약에 파티션 개수가 변해야 하는 경우에는 기존에 사용하던 메시지 키의 매칭을 그대로 가져가기 위해 커스텀 파티션너를 개발하고 적용해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4) 브로커와 컨슈머의 영향도&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;h3 data-ke-size=&quot;size23&quot;&gt;2. 토픽 정리 정책&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토픽의 데이터를 더는 사용하지 않는 경우 cleanup.policy 옵션을 사용해 데이터를 완전 삭제(delete)하거나 압축(compact)으로 동일 메시지 키의 가장 오래된 데이터를 삭제할 수 있다.&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;1) 토픽 삭제 정책(delete policy)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;cleanup.policy&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;옵션을 delete로 설정하면 &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;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세그먼트는 토픽의 데이터를 저장하는 명시적인 파일 시스템 단위이고, &lt;span style=&quot;letter-spacing: 0px;&quot;&gt;파티션마다 별개로 생성되며 파일 이름은 오프셋 중 가장 작은 값이 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;segment.bytes&lt;/span&gt; 옵션으로 1개의 세그먼트 크기를 설정할 수 있다. segment.bytes 크기보다 커질 경우에는 기존에 적재하던 세그먼트 파일을 닫고 새로운 세그먼트를 열어서 데이터를 저장한다.&lt;/p&gt;
&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;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;retention.ms&lt;/span&gt;는 토픽의 데이터를 유지하는 기간을 밀리초 단위로 설정할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세그먼트 파일의 마지막 수정 시간이 retention.ms를 넘어가면 세그먼트는 삭제된다.&lt;/p&gt;
&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;background-color: #f6e199;&quot;&gt;retention.bytes&lt;/span&gt;는 토픽의 최대 데이터 크기를 제어한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;retention.bytes를 넘어간 세그먼트 파일들은 삭제된다.&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;2) 토픽 압축 정책(compact policy)&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;883&quot; data-origin-height=&quot;533&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pvrGY/btrYmyRGQlc/KOpvgdkDtodoa3sNAr9Wck/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pvrGY/btrYmyRGQlc/KOpvgdkDtodoa3sNAr9Wck/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pvrGY/btrYmyRGQlc/KOpvgdkDtodoa3sNAr9Wck/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpvrGY%2FbtrYmyRGQlc%2FKOpvgdkDtodoa3sNAr9Wck%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;375&quot; data-origin-width=&quot;883&quot; data-origin-height=&quot;533&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;cleanup.policy&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;옵션을 compact로 설정하면 &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;blockquote data-ke-style=&quot;style2&quot;&gt;* 액티브 세그먼트: 데이터를 저장하기 위해 사용중이 세그먼트&lt;br /&gt;압축 정책은 액티브 세그먼트를 제외한 나머지 세그먼트들에 한해서만 데이터를 처리한다.&lt;br /&gt;&lt;br /&gt;* 테일(tail) 영역: 압축 정책에 의해 압축이 완료된 레코드들&lt;br /&gt;테일 영역의 레코드들을 클린 레코드라고 한다.&lt;br /&gt;&lt;br /&gt;* 헤드(head) 영역: 압축이 진행되지 않은 레코드들&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;b&gt;더티 비율&lt;/b&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;더티비율 &lt;br /&gt;= 더티 레코드 개수 / 총 레코드 수(클린 레코드 개수 + 더티 레코드 개수)&lt;br /&gt;= 3 / (3 + 3)&lt;br /&gt;= 0.5&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;min.cleanable.dirty.ratio&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;옵션값으로 데이터의 압축 시작 시점을 결정한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;min.cleanable.dirty.ratio 옵션값은 액티브 세그먼트를 제외한 세그먼트에 남아있는 데이터의 테일 영역의 레코드 개수와 헤드 영역의 레코드 개수의 비율을 뜻한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;min.cleanable.dirty.ratio를 0.5로 설정할 경우 더티 비율이 0.5가 넘어가면 압축이 수행된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;0.9와 같이 크게 설정하면 한번 압축을 할 때 많은 데이터가 줄어들므로 압축 효과가 좋지만 압축 전까지 많은 용량을 차지하게된다.&lt;/li&gt;
&lt;li&gt;0.1과 같이 작게 설정하면 압축이 자주 일어나기 때문에 최신 데이터만 유지할 수 있지만 압축이 자주 발생하므로 브로커에 부담을 주게된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 멀티 노드 카프카 클러스터&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카프카는 데이터 복제(Replication)을 통해 분산 시스템 기반에서 카프카의 최적 가용성을 보장한다.&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;h4 data-ke-size=&quot;size20&quot;&gt;1) 설치 및 기본 설정&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;(1) EC2 인스턴스 생성 및 접속&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;zookeeper 1, broker 3대를 설치할 총 4개의 인스턴스를 생성한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;370&quot; data-origin-height=&quot;232&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dT2P5j/btrZ7GBsokJ/WFHLk2TWYiXbeWpkMzZksk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dT2P5j/btrZ7GBsokJ/WFHLk2TWYiXbeWpkMzZksk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dT2P5j/btrZ7GBsokJ/WFHLk2TWYiXbeWpkMzZksk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdT2P5j%2FbtrZ7GBsokJ%2FWFHLk2TWYiXbeWpkMzZksk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;370&quot; height=&quot;232&quot; data-origin-width=&quot;370&quot; data-origin-height=&quot;232&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;zookeeper의 보안그룹을 설정한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1635&quot; data-origin-height=&quot;251&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cBv8jX/btrZ9FBNzxp/VMRbDJUNC6QktuJHQuPY41/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cBv8jX/btrZ9FBNzxp/VMRbDJUNC6QktuJHQuPY41/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cBv8jX/btrZ9FBNzxp/VMRbDJUNC6QktuJHQuPY41/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcBv8jX%2FbtrZ9FBNzxp%2FVMRbDJUNC6QktuJHQuPY41%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1635&quot; height=&quot;251&quot; data-origin-width=&quot;1635&quot; data-origin-height=&quot;251&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;kafka 브로커의 보안그룹을 설정한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1538&quot; data-origin-height=&quot;253&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bKynYS/btr0dpyryZW/qLozc8rfPVV57pIB1PbyOK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bKynYS/btr0dpyryZW/qLozc8rfPVV57pIB1PbyOK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bKynYS/btr0dpyryZW/qLozc8rfPVV57pIB1PbyOK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbKynYS%2Fbtr0dpyryZW%2FqLozc8rfPVV57pIB1PbyOK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;253&quot; data-origin-width=&quot;1538&quot; data-origin-height=&quot;253&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;EC2에 접속한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;458&quot; data-origin-height=&quot;184&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/capdyo/btrYG76VWQW/bAkUDzt9XoShDZu5KS2KkK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/capdyo/btrYG76VWQW/bAkUDzt9XoShDZu5KS2KkK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/capdyo/btrYG76VWQW/bAkUDzt9XoShDZu5KS2KkK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcapdyo%2FbtrYG76VWQW%2FbAkUDzt9XoShDZu5KS2KkK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;458&quot; height=&quot;184&quot; data-origin-width=&quot;458&quot; data-origin-height=&quot;184&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;(2) host 설정&lt;/p&gt;
&lt;pre id=&quot;code_1676861277607&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[ec2 인스턴스]
$ sudo vi /etc/hosts
13.125.172.62 zookeeper
13.124.8.60 kafka-0
43.201.102.129 kafka-1
43.201.149.82 kafka-2&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;(3) 별칭 설정&lt;/p&gt;
&lt;pre id=&quot;code_1676863572155&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[ec2 인스턴스]
$ vi ~/.bashrc
USERNAME=zookeeper
PS1='[\e[1;31m$USERNAME\e[0m][\e[1;32m\t\e[0m][\e[1;33m\u\e[0m@\e[1;36m\h\e[0m \w] \n\$ \[\033[00m\]'

$ source ~/.bashrc&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;(4) Java 설치&lt;/p&gt;
&lt;pre id=&quot;code_1675908100828&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[ec2 인스턴스]
$ cd ~
# aws coreetto 다운로드
$ sudo curl -L https://corretto.aws/downloads/latest/amazon-corretto-11-x64-linux-jdk.rpm -o jdk11.rpm

# jdk11 설치
$ sudo yum localinstall jdk11.rpm

# jdk version 선택
$ sudo /usr/sbin/alternatives --config java

# java 버전 확인
$ java --version

# 다운받은 설치키트 제거
$ rm -rf jdk11.rpm&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;(5) 카프카 설치&lt;/p&gt;
&lt;pre id=&quot;code_1675908420158&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[ec2 인스턴스]
$ wget https://downloads.apache.org/kafka/3.4.0/kafka_2.12-3.4.0.tgz
$ tar xvf kafka_2.12-3.4.0.tgz
$ cd kafka_2.12-3.4.0.tgz&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;(6) 카프카 브로커 힙 메모리 설정&lt;/p&gt;
&lt;pre id=&quot;code_1675908580717&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[ec2 인스턴스]
$ vi ~/.bashrc
export KAFKA_HEAP_OPTS=&quot;-Xmx400m -Xms400m&quot;

$ source ~/.bashrc
$ echo $KAFKA_HEAP_OPTS&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3) 멀티 브로커 구성&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1개의 zookeeper에 연결된 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;(1) 주키퍼 실행 옵션 설정&lt;/p&gt;
&lt;pre id=&quot;code_1676869263174&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[zookeeper]
$ vi config/zookeeper.properties
dataDir=/home/ec2-user/data/zookeeper-logs

$ mkdir -p /home/ec2-user/data/zookeeper-logs&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;(2) 카프카 브로커 실행 옵션 설정&lt;/p&gt;
&lt;pre id=&quot;code_1676450336763&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[kafka-x]
$ vi config/server.properties
broker.id=0
listeners=PLAINTEXT://:9092
advertised.listeners=PLAINTEXT://kafka-x-IP:9092
log.dirs=/home/ec2-user/data/kafka-logs
zookeeper.connect=zookeeper:2181

$ mkdir -p /home/ec2-user/data/kafka-logs&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;(3) 주키퍼 실행&lt;/p&gt;
&lt;pre id=&quot;code_1675909904979&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[zookeeper]
$ bin/zookeeper-server-start.sh -daemon config/zookeeper.properties
$ jps -vm&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;(4) 브로커 실행&lt;/p&gt;
&lt;pre id=&quot;code_1675914825962&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[kafka-x]
$ bin/kafka-server-start.sh -daemon config/server.properties
$ jps -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;size18&quot;&gt;(5) 브로커 로그 확인&lt;/p&gt;
&lt;pre id=&quot;code_1675924635127&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[kafka-x]
$ tail -f logs/server.log&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_1676452045191&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;home
├── ec2-user
│&amp;nbsp;&amp;nbsp; ├── data
│&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp; ├── zookeeper-logs
│&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp; ├── kafka-logs
│&amp;nbsp;&amp;nbsp; ├── kafka_2.12-3.4.0
│&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp; ├── bin
│&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp; ├── kafka-configs.sh
│&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp; ├── kafka-console-consumer.sh
│&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp; ├── kafka-console-producer.sh
│&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp; ├── kafka-server-start.sh
│&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp; ├── kafka-topics.sh
│&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp; ├── zookeeper-server-start.sh
│&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp; ├── config
│&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp; ├── server.properties
│&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp; └── zookeeper.properties
│&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp; ├── libs
│&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp; ├── LICENSE
│&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp; ├── logs
│&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp; ├── server.log
│&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp; ├── NOTICE
│&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp; └── site-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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 복제와 리더 / 팔로워 파티션&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 복제는 카프카를 장애 허용 시스템으로 동작하도록 하는 원동력이다. 데이터 복제로 클러스터로 묶인 브로커 중 일부에 장애가 발생하더라도 데이터를 유실하지 않고 안전하게 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 복제는 파티션 단위로 이루어지고 토픽을 생성할 때 파티션의 복제 개수(replication factor)를 설정할 수 있다.&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;size18&quot;&gt;&lt;b&gt;토픽 생성&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파티션 3, replication factor 3&lt;/p&gt;
&lt;pre id=&quot;code_1676452224468&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[kafka-0]
$ bin/kafka-topics.sh --create \
--bootstrap-server kafka-0:9092,kafka-1:9092,kafka-2:9092 \
--replication-factor 3 \
--partitions 3 \
--topic topic-p3r3&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;토픽 정보 확인&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1676867137943&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[kafka-0]
$ bin/kafka-topics.sh --describe \
--bootstrap-server kafka-0:9092,kafka-1:9092,kafka-2:9092 \
--topic topic-p3r3

Topic: topic-p3r3       TopicId: pdUUUMzuSd2-xuD2kgrmFg PartitionCount: 3       ReplicationFactor: 3    Configs:
        Topic: topic-p3r3       Partition: 0    Leader: 1       Replicas: 1,0,2 Isr: 1,0,2
        Topic: topic-p3r3       Partition: 1    Leader: 0       Replicas: 0,2,1 Isr: 0,2,1
        Topic: topic-p3r3       Partition: 2    Leader: 2       Replicas: 2,1,0 Isr: 2,1,0&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파티션을 기준으로 Leader 브로커, Replica 브로커, Isr(리더,브로커)을 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1474&quot; data-origin-height=&quot;416&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/XNDxg/btrZ5kshyOe/zzlKTblCxfdKvYLw2liNx1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/XNDxg/btrZ5kshyOe/zzlKTblCxfdKvYLw2liNx1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/XNDxg/btrZ5kshyOe/zzlKTblCxfdKvYLw2liNx1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXNDxg%2FbtrZ5kshyOe%2FzzlKTblCxfdKvYLw2liNx1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1474&quot; height=&quot;416&quot; data-origin-width=&quot;1474&quot; data-origin-height=&quot;416&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;메시지 전송&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1676882376638&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[kafka-0]
$ bin/kafka-console-producer.sh --bootstrap-server kafka-0:9092,kafka-1:9092,kafka-2:9092 --topic topic-p3r3
&amp;gt;hello kafka&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;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;1319&quot; data-origin-height=&quot;670&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0oDXM/btrZ7rqC6hB/EwPcNTF5TXm86uY830bnPk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0oDXM/btrZ7rqC6hB/EwPcNTF5TXm86uY830bnPk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0oDXM/btrZ7rqC6hB/EwPcNTF5TXm86uY830bnPk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0oDXM%2FbtrZ7rqC6hB%2FEwPcNTF5TXm86uY830bnPk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1319&quot; height=&quot;670&quot; data-origin-width=&quot;1319&quot; data-origin-height=&quot;670&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;&lt;b&gt;메시지 확인&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1676882347153&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[kafka-0]
$ bin/kafka-dump-log.sh --deep-iteration --print-data-log --files /home/ec2-user/data/kafka-logs/topic-p3r3-0/00000000000000000000.log
Dumping /home/ec2-user/data/kafka-logs/topic-p3r3-0/00000000000000000000.log
Log starting offset: 0
baseOffset: 0 lastOffset: 0 count: 1 baseSequence: 0 lastSequence: 0 producerId: 0 producerEpoch: 0 partitionLeaderEpoch: 3 isTransactional: false isControl: false deleteHorizonMs: OptionalLong.empty position: 0 CreateTime: 1676881897607 size: 79 magic: 2 compresscodec: none crc: 1301629467 isvalid: true
| offset: 0 CreateTime: 1676881897607 keySize: -1 valueSize: 11 sequence: 0 headerKeys: [] payload: hello kafka&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;5) 컨트롤러 브로커&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;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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;366&quot; data-origin-height=&quot;345&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/btdH8A/btrZ2zpaHlP/tLuSaSWOKnCoYSt8CNspR1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/btdH8A/btrZ2zpaHlP/tLuSaSWOKnCoYSt8CNspR1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/btdH8A/btrZ2zpaHlP/tLuSaSWOKnCoYSt8CNspR1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbtdH8A%2FbtrZ2zpaHlP%2FtLuSaSWOKnCoYSt8CNspR1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;366&quot; height=&quot;345&quot; data-origin-width=&quot;366&quot; data-origin-height=&quot;345&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주키퍼는 디렉토리 구조 기반의 Z Node를 활용해 개별 노드의 중요 정보를 담고 있으며 Z Node를 계속 모니터링하며 Z Node에 변경 발생 시 Watch Event가 트리거 되어 변경 정보가 개별 노드들에 통보한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1676884277966&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[zookeeper]
$ bin/zookeeper-shell.sh localhost:2181

# 노드 확인
ls /
[admin, brokers, cluster, config, consumers, controller, controller_epoch, feature, isr_change_notification, latest_producer_id_block, log_dir_event_notification, zookeeper]

# 컨트롤러 확인
get /controller
{&quot;version&quot;:2,&quot;brokerid&quot;:0,&quot;timestamp&quot;:&quot;1676881398744&quot;,&quot;kraftControllerEpoch&quot;:-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;클러스터에서 &lt;span&gt;주키퍼의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;역할은 다음과 같다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Controller Broker 선출(Election)&lt;/li&gt;
&lt;li&gt;Kafka 클러스터내 Broker들의&amp;nbsp;List,&amp;nbsp;Broker&amp;nbsp;Join/Leave&amp;nbsp;관리&amp;nbsp;및&amp;nbsp;통보&lt;/li&gt;
&lt;li&gt;Topic의 파티션, replicas등의 정보를 가짐&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Heartbeat&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카프카 브로커는 주기적으로 Zookeeper에 접속하면서 Session Heartbeat을&amp;nbsp;전송하여&amp;nbsp;자신의&amp;nbsp;상태를&amp;nbsp;보고하는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Zookeeeper는 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;zookeeper.session.timeout.ms&lt;/span&gt; 이내에 Heartbeat을 받지 못하면 해당 브로커의 노드 정보를 삭제하고 Controller 노드에게 변경 사실을 통보한다.&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;Zookeeper에 가장 처음 접속을 요청한 Broker가 Controller가 되고 Controller는 파티션에 대한 Leader Election을 수행한다.&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;Leader Election&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Controller는 Zookeeper로 부터 broker 추가/Down등의 정보를 받으면 해당 broker로 인해 영향을 받는 파티션들에 대해서 새로운 Leader Election을 수행한다.&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;1400&quot; data-origin-height=&quot;778&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cIiCpG/btrZ7FWJikY/YD5k2ZyunxNxWka1LSNjJ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cIiCpG/btrZ7FWJikY/YD5k2ZyunxNxWka1LSNjJ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cIiCpG/btrZ7FWJikY/YD5k2ZyunxNxWka1LSNjJ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcIiCpG%2FbtrZ7FWJikY%2FYD5k2ZyunxNxWka1LSNjJ1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1400&quot; height=&quot;778&quot; data-origin-width=&quot;1400&quot; data-origin-height=&quot;778&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;broker-2 Shutdown&lt;/li&gt;
&lt;li&gt;Zookeeper는 session 기간동안 Heartbeat이 오지 않으므로 broker-2의 노드 정보 갱신&lt;/li&gt;
&lt;li&gt;Controller는 Zookeeper를 모니터링 하던 중 Watch Event로 broker-2에 대한 Down 정보를 받음&lt;/li&gt;
&lt;li&gt;Controller는 다운된 브로커가 관리하던 파티션들에 대해 새로운 Leader/Follower 결정&lt;/li&gt;
&lt;li&gt;결정된 새로운 Leader/Follower 정보를 Zookeeper에 저장&lt;/li&gt;
&lt;li&gt;해당 파티션을 복제하는 모든 브로커들에게 새로운 Leader/Follower 정보를 전달하고 새로운 Leader로 부터 복제 수행할 것을 요청&lt;/li&gt;
&lt;li&gt;Controller는 모든 브로커가 가지는 Metadatacache를 새로운 Leader/Follower 정보로 갱신할 것을 요청&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1579&quot; data-origin-height=&quot;191&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/U0kTF/btrZ7b8YsaM/Aj2ChivdYg9ggUbgpQlyb0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/U0kTF/btrZ7b8YsaM/Aj2ChivdYg9ggUbgpQlyb0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/U0kTF/btrZ7b8YsaM/Aj2ChivdYg9ggUbgpQlyb0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FU0kTF%2FbtrZ7b8YsaM%2FAj2ChivdYg9ggUbgpQlyb0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1579&quot; height=&quot;191&quot; data-origin-width=&quot;1579&quot; data-origin-height=&quot;191&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;903&quot; data-origin-height=&quot;80&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bOzqBO/btrZ7FIH5UA/7GVVmnsHcNQ2KWojLkONKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bOzqBO/btrZ7FIH5UA/7GVVmnsHcNQ2KWojLkONKK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bOzqBO/btrZ7FIH5UA/7GVVmnsHcNQ2KWojLkONKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbOzqBO%2FbtrZ7FIH5UA%2F7GVVmnsHcNQ2KWojLkONKK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;903&quot; height=&quot;80&quot; data-origin-width=&quot;903&quot; data-origin-height=&quot;80&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. ISR&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1211&quot; data-origin-height=&quot;358&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bH0ndA/btrZreqO8dM/Gnq3QXs99h4lS6P05gYkxk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bH0ndA/btrZreqO8dM/Gnq3QXs99h4lS6P05gYkxk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bH0ndA/btrZreqO8dM/Gnq3QXs99h4lS6P05gYkxk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbH0ndA%2FbtrZreqO8dM%2FGnq3QXs99h4lS6P05gYkxk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;216&quot; data-origin-width=&quot;1211&quot; data-origin-height=&quot;358&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ISR(In-Sync-Replicas)은 리더 파티션과 팔로워 파티션이 모두 싱크가 된 상태를 뜻한다.&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&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;replica.lag.time.max.ms&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;만큼의 주기를 가지고 팔로워 파티션이 데이터를 복제하는지 확인한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;replica.lag.time.max.ms값보다 긴 시간동안 데이터를 가져가지 않는다면 해당 팔로워 파티션에 문제가 생긴 것으로 판단하고 ISR 그룹에서 제외한다.&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;1770&quot; data-origin-height=&quot;744&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bJXKF4/btrZ9NGzHEg/2QeJzikt9aqkxicPsuBT5k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bJXKF4/btrZ9NGzHEg/2QeJzikt9aqkxicPsuBT5k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bJXKF4/btrZ9NGzHEg/2QeJzikt9aqkxicPsuBT5k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbJXKF4%2FbtrZ9NGzHEg%2F2QeJzikt9aqkxicPsuBT5k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1770&quot; height=&quot;744&quot; data-origin-width=&quot;1770&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;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;min.insync.replicas&lt;/span&gt; 파라미터는 브로커의 설정값으로 Producer가 acks=all 로 성공적으로 메시지를 보낼 수 있는 최소한의 ISR 브로커 개수를 의미한다. 프로듀서의 Acks=all 일때, min.insync.replicas 값보다 ISR 브로커의 개수가 작으면 프로듀서에게 Error를 반환한다. (NotEnoughReplicasException 또는 NotEnoughReplicasAfterAppendException)&lt;/p&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;1) 리더 파티션 선출&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;(1) Preferred Leader Election&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파티션 별로 최초 할당된 Leader/Follower Broker설정을 Preferred Broker로 그대로 유지한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;auto.leader.rebalance.enable=true&lt;/span&gt;,&amp;nbsp;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;leader.imbalance.check.interval.seconds&lt;/span&gt;를 일정시간으로 설정(기본 300초)하면 브로커가 shutdown후 재 기동될 때 Preferred Leader Broker를 일정 시간 이후에 리더로 재선출한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1319&quot; data-origin-height=&quot;357&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OpE2t/btrZ6j06eiH/KXU6qBiX3cUXxukgEPVed1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OpE2t/btrZ6j06eiH/KXU6qBiX3cUXxukgEPVed1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OpE2t/btrZ6j06eiH/KXU6qBiX3cUXxukgEPVed1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOpE2t%2FbtrZ6j06eiH%2FKXU6qBiX3cUXxukgEPVed1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1319&quot; height=&quot;357&quot; data-origin-width=&quot;1319&quot; data-origin-height=&quot;357&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1676948293606&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Topic: topic-p3r3       TopicId: pdUUUMzuSd2-xuD2kgrmFg PartitionCount: 3       ReplicationFactor: 3    Configs:
        Topic: topic-p3r3       Partition: 0    Leader: 0     Replicas: 1,0,2 Isr: 0
        Topic: topic-p3r3       Partition: 1    Leader: 0     Replicas: 0,2,1 Isr: 0
        Topic: topic-p3r3       Partition: 2    Leader: 0     Replicas: 2,1,0 Isr: 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;size18&quot;&gt;(2) Unclean Leader Election&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;unclean.leader.election.enable&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;옵션으로&amp;nbsp; ISR이 아닌 팔로워 파티션을 리더 파티션으로 선출 가능한지 여부를 선택할 수 있다.&lt;/p&gt;
&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;unclean.leader.election.enable: false&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ISR이 아닌 팔로워 파티션을 리더로 선출하지 않는다.&lt;/li&gt;
&lt;li&gt;리더 파티션이 존재하는 브로커가 다시 시작되기 까지 기다리기 때문에 서비스가 중단된다.&lt;/li&gt;
&lt;li&gt;대신 데이터의 유실은 발생하지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1319&quot; data-origin-height=&quot;357&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dsgpsh/btrZ86sRuJW/hXzniVJBOIUK2qkUavXvcK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dsgpsh/btrZ86sRuJW/hXzniVJBOIUK2qkUavXvcK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dsgpsh/btrZ86sRuJW/hXzniVJBOIUK2qkUavXvcK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdsgpsh%2FbtrZ86sRuJW%2FhXzniVJBOIUK2qkUavXvcK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1319&quot; height=&quot;357&quot; data-origin-width=&quot;1319&quot; data-origin-height=&quot;357&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1676947863504&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Topic: topic-p3r3       TopicId: pdUUUMzuSd2-xuD2kgrmFg PartitionCount: 3       ReplicationFactor: 3    Configs:
        Topic: topic-p3r3       Partition: 0    Leader: none     Replicas: 1,0,2 Isr: 0
        Topic: topic-p3r3       Partition: 1    Leader: none     Replicas: 0,2,1 Isr: 0
        Topic: topic-p3r3       Partition: 2    Leader: none     Replicas: 2,1,0 Isr: 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;b&gt;unclean.leader.election.enable: true&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ISR이 아닌 팔로워 파티션이 리더로 선출될 수 있다.&lt;/li&gt;
&lt;li&gt;서비스가 중단되지 않는다.&lt;/li&gt;
&lt;li&gt;동기화되지 않은 파티션이 리더로 선출되므로 일부 데이터가 유실될 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1676949218283&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ bin/kafka-configs.sh --bootstrap-server broker-1:9092 \
--entity-type topics \
--entity-name topic-p3r3 \
--describe --all | grep unclean

... unclean.leader.election.enable=false ...

$ bin/kafka-configs.sh --bootstrap-server broker-1:9092 \
--entity-type topics \
--entity-name topic-p3r3 \
--alter \
--add-config unclean.leader.election.enable=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;1182&quot; data-origin-height=&quot;438&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b7BvFJ/btrZmExjF4F/abWrBxQLMpeD5FBgoTe2UK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b7BvFJ/btrZmExjF4F/abWrBxQLMpeD5FBgoTe2UK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b7BvFJ/btrZmExjF4F/abWrBxQLMpeD5FBgoTe2UK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb7BvFJ%2FbtrZmExjF4F%2FabWrBxQLMpeD5FBgoTe2UK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;687&quot; height=&quot;255&quot; data-origin-width=&quot;1182&quot; data-origin-height=&quot;438&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일부 데이터가 유실되더라도 토픽과 연동 중인 서비스의 무중단이 더 중요하다면 true,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스가 중단되더라도 데이터가 유실되면 안되는 경우 false로 설정한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;6.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;Kafka Config&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 30.1163%; text-align: center;&quot;&gt;Config&amp;nbsp;구분&lt;/td&gt;
&lt;td style=&quot;width: 69.8837%; text-align: center;&quot;&gt;설명&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 30.1163%;&quot;&gt;Broker와&amp;nbsp;Topic&amp;nbsp;레벨&amp;nbsp;Config&lt;/td&gt;
&lt;td style=&quot;width: 69.8837%;&quot;&gt;&amp;bull;&amp;nbsp;Kafka&amp;nbsp;서버에서&amp;nbsp;설정되는&amp;nbsp;Config &lt;br /&gt;&amp;bull; Topic의 Config 값은 Broker 레벨에서 지정한 Config를 기본으로 설정하며 별도의&amp;nbsp;Topic&amp;nbsp;레벨&amp;nbsp;Config를&amp;nbsp;설정할&amp;nbsp;경우&amp;nbsp;이를&amp;nbsp;따름 &lt;br /&gt;&amp;bull; 보통 server.properties에 있는 Config는 변경시 Broker 재기동이 필요한 Static Config이며, Dynamic Config는 kafka-configs를 이용하여 동적으로 config 변경 가능.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 30.1163%;&quot;&gt;Producer와&amp;nbsp;Consumer&amp;nbsp;레벨&amp;nbsp;Config&lt;/td&gt;
&lt;td style=&quot;width: 69.8837%;&quot;&gt;&amp;bull;&amp;nbsp;Kafka&amp;nbsp;클라이언트에서&amp;nbsp;설정되는&amp;nbsp;Config &lt;br /&gt;&amp;bull; Client 레벨에서 설정되므로 server.properties에 존재하지 않고, kafka-configs로 수정할&amp;nbsp;수&amp;nbsp;없으며&amp;nbsp;Client&amp;nbsp;수행시마다&amp;nbsp;설정할&amp;nbsp;수&amp;nbsp;있음&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;kafka-configs.sh&lt;/b&gt; 명령어로 브로커와 토픽 레벨의 설정을 변경할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1677040612638&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# broker 0번의 config 설정 확인
$ bin/kafka-configs.sh --bootstrap-server kafka-0:9092 --entity-type brokers --entity-name 0 --all --describe

# topic의 config 설정 확인
$ bin/kafka-configs.sh --bootstrap-server kafka-0:9092 --entity-type topics --entity-name multipart-topic --all --describe
$ bin/kafka-configs.sh --bootstrap-server kafka-0:9092 --entity-type topics --entity-name multipart-topic --all --describe \
| grep max.message.bytes

# topic의 config 설정 변경
$ bin/kafka-configs.sh --bootstrap-server kafka-0:9092 --entity-type topics --entity-name multipart-topic --alter \
--add-config max.message.bytes=2088000

# 변경한 topic의 config를 다시 Default값으로 원복
$ bin/kafka-configs.sh --bootstrap-server kafka-0:9092 --entity-type topics --entity-name multipart-topic --alter \
--delete-config max.message.bytes&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;http://www.yes24.com/Product/Goods/99122569&quot;&gt;아파치 카프카 애플리케이션 프로그래밍&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.inflearn.com/course/%EC%B9%B4%ED%94%84%EC%B9%B4-%EC%99%84%EB%B2%BD%EA%B0%80%EC%9D%B4%EB%93%9C-%EC%BD%94%EC%96%B4&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;카프카&amp;nbsp;완벽&amp;nbsp;가이드&amp;nbsp;-&amp;nbsp;코어편&lt;/a&gt;&lt;/p&gt;</description>
      <category>Kafka</category>
      <category>토픽</category>
      <category>파티션</category>
      <author>mandykr</author>
      <guid isPermaLink="true">https://mandykr.tistory.com/95</guid>
      <comments>https://mandykr.tistory.com/95#entry95comment</comments>
      <pubDate>Thu, 2 Feb 2023 10:17:22 +0900</pubDate>
    </item>
    <item>
      <title>[Kafka] 1. 카프카 기본 개념</title>
      <link>https://mandykr.tistory.com/94</link>
      <description>&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;2. 주키퍼&lt;span&gt;, 클러스터,&lt;span&gt; 브로커&lt;/span&gt;&lt;/span&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;4. 컨슈머&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;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;1193&quot; data-origin-height=&quot;379&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sChQn/btrXM3EGcl7/u8h4M6PpB6J72ZRSGnvF0k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sChQn/btrXM3EGcl7/u8h4M6PpB6J72ZRSGnvF0k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sChQn/btrXM3EGcl7/u8h4M6PpB6J72ZRSGnvF0k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsChQn%2FbtrXM3EGcl7%2Fu8h4M6PpB6J72ZRSGnvF0k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1193&quot; height=&quot;379&quot; data-origin-width=&quot;1193&quot; data-origin-height=&quot;379&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 카프카&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://ko.wikipedia.org/wiki/%EC%95%84%ED%8C%8C%EC%B9%98_%EC%B9%B4%ED%94%84%EC%B9%B4&quot;&gt;카프카&lt;/a&gt;는 링크드인의 데이터팀이 파편화된 데이터 파이프라인의 복잡도를 낮추기 위해 개발한 오픈소스 프로젝트이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&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;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;&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;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;b&gt;고가용성&lt;/b&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 주키퍼, 클러스터, 브로커&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1449&quot; data-origin-height=&quot;870&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kPvZB/btrZorqZkdT/vy8tKhC6khgZ1kzhbk3ZK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kPvZB/btrZorqZkdT/vy8tKhC6khgZ1kzhbk3ZK0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kPvZB/btrZorqZkdT/vy8tKhC6khgZ1kzhbk3ZK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkPvZB%2FbtrZorqZkdT%2Fvy8tKhC6khgZ1kzhbk3ZK0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1449&quot; height=&quot;870&quot; data-origin-width=&quot;1449&quot; data-origin-height=&quot;870&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) 주키퍼&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주키퍼는 카프카의 메타데이터를 관리하는 데 사용된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;통신 보안규칙, jmx port 상태 정보, host 정보&lt;/li&gt;
&lt;li&gt;컨트롤러 역할을 하는 브로커 정보&lt;/li&gt;
&lt;li&gt;카프카에 저장된 토픽 목록 정보&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2) 클러스터, 브로커&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;size18&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;&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;(토픽의 파티션이 11개이면 11개의 디렉토리가 생성된다.)&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;(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;&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;&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-origin-width=&quot;1400&quot; data-origin-height=&quot;452&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bcZ4D4/btrZj9ELE0g/gnwnjMeFGfmNYQ0ROavitk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bcZ4D4/btrZj9ELE0g/gnwnjMeFGfmNYQ0ROavitk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bcZ4D4/btrZj9ELE0g/gnwnjMeFGfmNYQ0ROavitk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbcZ4D4%2FbtrZj9ELE0g%2FgnwnjMeFGfmNYQ0ROavitk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1400&quot; height=&quot;452&quot; data-origin-width=&quot;1400&quot; data-origin-height=&quot;452&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&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;로그 세그먼트 단위로 삭제되는 데이터의 기한을 설정할 수 있고 오래된 데이터를 압축하는 정책을 가져갈 수도 있다.&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;(5) 컨슈머 오프셋 저장&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨슈머 그룹은 토픽이 특정 파티션으로부터 데이터를 가져가서 처리하고 이 파티션의 어느 레코드까지 가져갔는지 확이하기 위해 오프셋을 커밋한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커밋한 오프셋은 __consumer_offsets 토픽에 저장된다.&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;(6) 코디네이터&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;h4 data-ke-size=&quot;size20&quot;&gt;3) 토픽, 파티션&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;953&quot; data-origin-height=&quot;367&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EhKYT/btrXQZoKJZH/t7ymV1FFOIylUycexAtGq1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EhKYT/btrXQZoKJZH/t7ymV1FFOIylUycexAtGq1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EhKYT/btrXQZoKJZH/t7ymV1FFOIylUycexAtGq1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEhKYT%2FbtrXQZoKJZH%2Ft7ymV1FFOIylUycexAtGq1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;254&quot; data-origin-width=&quot;953&quot; data-origin-height=&quot;367&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개 이상의 파티션을 소유하고 있다.&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;h3 data-ke-size=&quot;size23&quot;&gt;3. 프로듀서&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1827&quot; data-origin-height=&quot;478&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/drs21F/btrZlrdUP0c/ydU0QDKw4ibvm42JWke6s0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/drs21F/btrZlrdUP0c/ydU0QDKw4ibvm42JWke6s0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/drs21F/btrZlrdUP0c/ydU0QDKw4ibvm42JWke6s0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdrs21F%2FbtrZlrdUP0c%2FydU0QDKw4ibvm42JWke6s0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1827&quot; height=&quot;478&quot; data-origin-width=&quot;1827&quot; data-origin-height=&quot;478&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;h4 data-ke-size=&quot;size20&quot;&gt;1) ProducerRecord&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전송하고자 하는 데이터는 ProducerRecord 클래스를 통해 인스턴스를 생성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필수값인 토픽과 메시지 값을 설정하고 필요에 따라 파티션 번호, 타임스탬프, 메시지 키를 설정할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2) Partitioner&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;파티셔너는 ProducerRecord를 토픽의 어느 파티션으로 전송할 지 결정한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;기본 파티셔너는 DefaultPartitioner 이고 프로듀서 API는 RoundRobinPartitioner, UniformStickyPartitioner를 제공한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Partitioner 인터페이스를 상속받은 사용자 정의 클래스를 만들 수 있다.&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;3) Accumulator&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;h3 data-ke-size=&quot;size23&quot;&gt;4. 컨슈머&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1574&quot; data-origin-height=&quot;871&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d0pB6N/btr3QvoUw3t/3QzEFL5SFjnQ8rcufT04sK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d0pB6N/btr3QvoUw3t/3QzEFL5SFjnQ8rcufT04sK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d0pB6N/btr3QvoUw3t/3QzEFL5SFjnQ8rcufT04sK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd0pB6N%2Fbtr3QvoUw3t%2F3QzEFL5SFjnQ8rcufT04sK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;638&quot; height=&quot;353&quot; data-origin-width=&quot;1574&quot; data-origin-height=&quot;871&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카프카 클라이언트를 라이브러리로 추가하여 컨슈머 애플리케이션을 만들 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨슈머는 카프카 브로커에 적재된 데이터를 가져와서 필요한 처리를 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) Fetcher&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨슈머 애플리케이션을 실행하면 내부에서 Fetcher 인스턴스가 생성되어 컨슈머가 poll() 메서드를 호출하기 전에 미리 레코드들을 내부 큐로 가져온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후에 사용자가 명시적으로 poll() 메서드를 호출하면 컨슈머는 내부 큐에 있는 레코드들을 받아 처리를 수행한다.&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;2) 컨슈머 그룹&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1418&quot; data-origin-height=&quot;628&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1L3G7/btrZrdFsucv/pO4Jdr8SWW7IMCNNjPkoC0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1L3G7/btrZrdFsucv/pO4Jdr8SWW7IMCNNjPkoC0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1L3G7/btrZrdFsucv/pO4Jdr8SWW7IMCNNjPkoC0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1L3G7%2FbtrZrdFsucv%2FpO4Jdr8SWW7IMCNNjPkoC0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;324&quot; data-origin-width=&quot;1418&quot; data-origin-height=&quot;628&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;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;3개의 파티션을 가진 토픽에서 4개의 컨슈머로 이루어진 컨슈머 그룹은 1개의 컨슈머가 파티션을 할당받지 못하고 유휴상태가 된다.&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;3) 컨슈머&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;데이터 처리중 발생한 리밸런싱에 대응하는 코드를 작성해야 한다.&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;파티션이 컨슈머에 재할당 되는 과정을 리밸런싱이라고 한다. &lt;br /&gt;리밸런싱은 카프카 브로커중 한 대가 역할을 맡는 코디네이터에 의해 발생한다.&amp;nbsp;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 상황에서 리밸런싱이 발생한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;컨슈머가 추가되는 상황&lt;/li&gt;
&lt;li&gt;컨슈머가 제외되는 상황&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리밸런싱으로 컨슈머의 가용성을 높일 수 있지만 리밸런싱이 발생할 때 파티션의 소유권을 재할당 하는 과정에서 데이터의 중복이나 유실이 발생할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 컨슈머는 브로커로부터 데이터를 어디까지 가져갔는지 커밋을 통해 기록해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커밋은 브로커의 내부 토픽(__consumer_offsets)에 기록된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;명시적 오프셋 커밋&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;poll() 메서드 호출 후 일정 간격마다 자동으로 커밋&lt;/li&gt;
&lt;li&gt;poll() 메서드 호출 이후에 리밸런싱 또는 강제 종료 시 데이터 중복 또는 유실 발생 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;비명시적 오프셋 커밋&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;동기 오프셋 커밋
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;commitSync() 사용&lt;/li&gt;
&lt;li&gt;브로커에 커밋 요청을 하고 응답을 기다린다.&lt;/li&gt;
&lt;li&gt;컨슈머의 처리량에 영향을 미침&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;비동기 오프셋 커밋
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;commitAsync() 사용&lt;/li&gt;
&lt;li&gt;브로커에 커밋 요청을 하고 응답을 기다리지 않는다.&lt;/li&gt;
&lt;li&gt;커밋 요청이 실패한 경우 현재 처리중인 데이터의 순서를 보장하지 않으며 데이터 중복 처리가 발생할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;http://www.yes24.com/Product/Goods/99122569&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;</description>
      <category>Kafka</category>
      <category>kafka</category>
      <author>mandykr</author>
      <guid isPermaLink="true">https://mandykr.tistory.com/94</guid>
      <comments>https://mandykr.tistory.com/94#entry94comment</comments>
      <pubDate>Wed, 1 Feb 2023 13:13:29 +0900</pubDate>
    </item>
    <item>
      <title>비동기 메시징(SNS-SQS Fanout)</title>
      <link>https://mandykr.tistory.com/93</link>
      <description>&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;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;&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;서비스에 적용 가능한 IPC(&lt;span style=&quot;background-color: #ffffff; color: #202124;&quot;&gt;Inter-Process Communication&lt;/span&gt;) 기술은 HTTP 기반 REST나 gRPC 등 동기 요청/응답 기반의 통신 매커니즘도 있고, AMQP, STOMP 등 비동기 메시지 기반의 통신 메커니즘도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IPC의 선택은 시스템 가용성에 영향을 미치는데, 요청을 처리하는 과정에서 타 서비스와 동기 통신을 하면 그만큼 가용성이 떨어지므로 가능한 서비스가 비동기 메시징을 이용하여 통신하도록 설계하는 것이 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 메시징&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메시징은 서비스가 메시지를 서로 비동기적으로 주고받는 통신 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;메시지&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메시지는 헤더와 바디로 구성되고 종류는 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;문서(document) : 데이터만 포함된 제네릭한 메시지, 메시지 해석은 수신자가 결정.&lt;/li&gt;
&lt;li&gt;커맨드(command) : 호출할 작업과 전달할 매개변수가 지정되어 있다.&lt;/li&gt;
&lt;li&gt;이벤트(event) : 송신자에게 어떤 사건이 발생했음을 알리는 메시지(도메인 이벤트).&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;메시지 채널&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메시지는 메시지 채널을 통해 교환되고 종류는 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;점대점(point-to-point) 채널 : 채널을 읽는 컨슈머 중 딱 하나만 지정하여 메시지를 전달(예: 커맨드 메시지)&lt;/li&gt;
&lt;li&gt;발행-구독(publish-subscribe) 채널 : 같은 채널을 바라보는 모든 컨슈머에 메시지를 전달(예: 이벤트 메시지)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;메시지 브로커&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메시징 기반의 애플리케이션은 대부분 메시지 브로커를 사용한다.&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;느슨한 결합 : 클라이언트는 적절한 채널에 메시지를 보내는 식으로 요청하고 서비스 인스턴스를 몰라도 되므로 느슨한 결합을 갖게 한다.&lt;/li&gt;
&lt;li&gt;메시지 버퍼링 : 메시지 브로커는 처리 가능한 시점까지 메시지를 버퍼링한다. 동기 요청/응답 프로토콜과 달리 메시징을 쓰면 컨슈머가 처리할 수 있을 때까지 큐에 메시지가 쌓인다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단점&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;단일 장애점 가능성 : 메시지 브로커의 가용성이 낮으면 시스템의 신뢰도가 떨어진다.&lt;/li&gt;
&lt;li&gt;운영 복잡도 부가 : 메시징 시스템 역시 설치, 구성, 운영해야 하는 시스템 컴포넌트이다.&lt;/li&gt;
&lt;/ul&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;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 이벤트기반 아키텍처&lt;/h3&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;1845&quot; data-origin-height=&quot;915&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c0sP6b/btrSHhuSFbj/LIXH2w5e34PB0HMzgfjERk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c0sP6b/btrSHhuSFbj/LIXH2w5e34PB0HMzgfjERk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c0sP6b/btrSHhuSFbj/LIXH2w5e34PB0HMzgfjERk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc0sP6b%2FbtrSHhuSFbj%2FLIXH2w5e34PB0HMzgfjERk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1845&quot; height=&quot;915&quot; data-origin-width=&quot;1845&quot; data-origin-height=&quot;915&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: #212121;&quot;&gt;트랜잭션을 통해 데이터 일관성을 보장한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로 AWS SNS-SQS 메시징 시스템으로 비관심사를 분리하고 외부 이벤트를 발행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 AWS SNS-SQS 메시징 시스템으로 외부 이벤트의 구독 계층을 구성한다.&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;1) 첫번째&amp;nbsp;구독자&amp;nbsp;계층&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;(1) 트랜잭셔널 메시징&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;631&quot; data-origin-height=&quot;919&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bkqDam/btrVZNrT4eO/4SvmugUeB56SmKVQ3WK9UK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bkqDam/btrVZNrT4eO/4SvmugUeB56SmKVQ3WK9UK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bkqDam/btrVZNrT4eO/4SvmugUeB56SmKVQ3WK9UK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbkqDam%2FbtrVZNrT4eO%2F4SvmugUeB56SmKVQ3WK9UK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;302&quot; height=&quot;440&quot; data-origin-width=&quot;631&quot; data-origin-height=&quot;919&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;도메인 엔티티를 DB 업데이트 후 메시지가 전송되지 않은 상태에서 서비스가 중단되거나 메시징 시스템에 장애가 발생한 경우 시스템의 장애로 이어지는 문제가 발생할 수 있어 DB 업데이트와 메시지 발행 두 작업이 서비스에서 원자적으로 수행되도록 해야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이벤트를 도메인과 동일한 저장소를 사용해 저장하면 ACID 트랜잭션에 대한 처리를 DBMS에 믿고 맡길 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #212121;&quot;&gt;동일 저장소를 통해 데이터베이스를 저장하고 이벤트를 발행함에 안정적인 정합성을 보장하는 방식은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;a href=&quot;https://microservices.io/patterns/data/transactional-outbox.html&quot;&gt;Transactional outbox Pattern&lt;/a&gt;&lt;span style=&quot;color: #212121;&quot;&gt;&lt;span&gt;&amp;nbsp;&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;/p&gt;
&lt;pre id=&quot;code_1673497796276&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Transactional
public ProjectResponse createProject(ProjectCreateRequest request) {
    Project project = request.toProject(UUID.randomUUID());
    codeStoreValidationService.validateToApply(project);

    Project saveProject = projectRepository.save(project);
    eventPublisher.publish(ProjectEvent.of(saveProject, EventType.CREATED));
    return ProjectResponse.of(saveProject);
}

@Transactional
public void publish(ProjectEvent event) {
    eventPublisher.publishEvent(event);
}&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_1673497908485&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Transactional
@EventListener
public void handleEvent(ProjectEvent event) {
    projectEventRecordUseCase.record(event);
}&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: #212121;&quot;&gt;이벤트 재발행&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #212121;&quot;&gt;구독자들이 이벤트를 정상적으로 처리하더라도, 이벤트 처리를 잘못할 수 있기 때문에 언제든 이벤트를 재발행 해줄 수 있어야 하고 &lt;span style=&quot;color: #212121;&quot;&gt;이 때 구독자들이 원하는 이벤트들의 형태는 자유롭다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #212121;&quot;&gt;&lt;span style=&quot;color: #212121;&quot;&gt;따라서 다음과 같은 조건을 고려해 이벤트 형태를 설계한다.&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #212121;&quot;&gt;&lt;span style=&quot;color: #212121;&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;이벤트&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;있어야&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_1673499911927&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;create table project_event (
   id bigint not null auto_increment,
   aggregate_id varbinary(16),
   event_type varchar(255),
   created_at datetime(6),
   published bit not null,
   published_at datetime(6)
)&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;p data-ke-size=&quot;size18&quot;&gt;(2) 관심사 분리&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;833&quot; data-origin-height=&quot;915&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cdajbi/btrV5glAzJ3/hXDkbJRouJqkTKg2QnWlM0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cdajbi/btrV5glAzJ3/hXDkbJRouJqkTKg2QnWlM0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cdajbi/btrV5glAzJ3/hXDkbJRouJqkTKg2QnWlM0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcdajbi%2FbtrV5glAzJ3%2FhXDkbJRouJqkTKg2QnWlM0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;396&quot; height=&quot;435&quot; data-origin-width=&quot;833&quot; data-origin-height=&quot;915&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: #212121;&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: #212121;&quot;&gt;[ 메시지 전달 ]&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1673500269343&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Transactional
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handleEvent(ProjectEvent event) {
    messagePublisher.publish(ProjectEventPayload.from(event));
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2) 두번째 구독자 계층&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;(1) 이벤트 발행 보장&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1226&quot; data-origin-height=&quot;915&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kBOo4/btrV3V9QZXo/XqGx2KKXXOflapTAZgpwp1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kBOo4/btrV3V9QZXo/XqGx2KKXXOflapTAZgpwp1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kBOo4/btrV3V9QZXo/XqGx2KKXXOflapTAZgpwp1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkBOo4%2FbtrV3V9QZXo%2FXqGx2KKXXOflapTAZgpwp1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;373&quot; data-origin-width=&quot;1226&quot; data-origin-height=&quot;915&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: #212121;&quot;&gt;첫번째 구독자 계층에서 최초 이벤트를 기록할 때는 발행 여부를&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;false&lt;span style=&quot;color: #212121;&quot;&gt;로 저장하였다. 두번째 구독자 계층에서 이벤트 발행 여부를 기록하는 구독자를 추가하여 데이터를 업데이트 처리한다. 발행된 이벤트는 삭제하지 않고 발행 여부만 true로 변경한다.&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: #212121;&quot;&gt;[ 이벤트 발행여부 업데이트 ]&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1673500608683&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@SqsListener(value = &quot;sqs-project-event-publish-record.fifo&quot;, deletionPolicy = SqsMessageDeletionPolicy.ON_SUCCESS)
public void recordEventPublish(@NotificationMessage EventPayload payload) {
    log.info(&quot;listen from sqs-project-event-publish-record.fifo: {}&quot;, payload);
    projectEventRecordUseCase.recordPublish(Long.valueOf(payload.getEventId()));
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;(2) 비관심사 분리&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1226&quot; data-origin-height=&quot;915&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AYsIF/btrV1gtqHgJ/EZl3vHBh7O0aohsWQlaut1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AYsIF/btrV1gtqHgJ/EZl3vHBh7O0aohsWQlaut1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AYsIF/btrV1gtqHgJ/EZl3vHBh7O0aohsWQlaut1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAYsIF%2FbtrV1gtqHgJ%2FEZl3vHBh7O0aohsWQlaut1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;571&quot; height=&quot;426&quot; data-origin-width=&quot;1226&quot; data-origin-height=&quot;915&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: #212121;&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: #212121;&quot;&gt;예를 들어 로그인 프로세스에서 부가 기능은 다음과 같을때 비 관심사로 분리한다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&amp;ldquo;동일&amp;nbsp;계정&amp;nbsp;로그인&amp;nbsp;수&amp;nbsp;제한&amp;rdquo;&amp;nbsp;규칙에&amp;nbsp;따라&amp;nbsp;동일&amp;nbsp;계정이&amp;nbsp;로그인된&amp;nbsp;타&amp;nbsp;디바이스&amp;nbsp;로그아웃&amp;nbsp;처리&lt;/li&gt;
&lt;li&gt;회원이&amp;nbsp;어느&amp;nbsp;디바이스에서&amp;nbsp;로그인되었는지&amp;nbsp;기록&lt;/li&gt;
&lt;li&gt;동일&amp;nbsp;디바이스의&amp;nbsp;다른&amp;nbsp;계정&amp;nbsp;로그아웃&amp;nbsp;기록&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1673501169592&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@SqsListener(value = &quot;${sqs.login-device-login}&quot;, deletionPolicy = SqsMessageDeletionPolicy.ON_SUCCESS)
public void loginDevice(@Payload MemberLoginApplicationEvent payload) {
    devices.login(payload.getMemberNumber(), payload.getDeviceNumber());
}

@SqsListener(value = &quot;${sqs.login-member-other-device-logout}&quot;, deletionPolicy = SqsMessageDeletionPolicy.ON_SUCCESS)
public void logoutMemberOtherDevices(@Payload MemberLoginApplicationEvent payload) {
    devices.logoutMemberOtherDevices(payload.getMemberNumber(), payload.getDeviceNumber());
}

@SqsListener(value = &quot;${sqs.login-other-member-device-logout}&quot;, deletionPolicy = SqsMessageDeletionPolicy.ON_SUCCESS)
public void logoutOtherMemberDevices(@Payload MemberLoginApplicationEvent payload) {
    devices.logoutOtherMemberDevices(payload.getMemberNumber(), payload.getDeviceNumber());
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;(3) 외부 이벤트 발행&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1275&quot; data-origin-height=&quot;915&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bGjPKS/btrV47h4rJx/fVsm04OKkfqa4uXgTKiXkK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bGjPKS/btrV47h4rJx/fVsm04OKkfqa4uXgTKiXkK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bGjPKS/btrV47h4rJx/fVsm04OKkfqa4uXgTKiXkK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbGjPKS%2FbtrV47h4rJx%2FfVsm04OKkfqa4uXgTKiXkK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;540&quot; height=&quot;388&quot; data-origin-width=&quot;1275&quot; data-origin-height=&quot;915&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MSA 를 위한 외부 시스템과의 관심사 분리를 위한 외부 이벤트 발행이 필요하다. 외부 시스템에 이벤트를 전파하는 행위 또한 도메인 내에 존재하던 비관심사로 볼 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1673501347758&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@SqsListener(value = &quot;sqs-change-project-broadcast.fifo&quot;, deletionPolicy = SqsMessageDeletionPolicy.ON_SUCCESS)
public void handleBroadcast(@NotificationMessage ProjectEventPayload payload) {
    log.info(&quot;listen from sqs-change-project-broadcast.fifo: {}&quot;, payload);
    messageBroadcaster.broadcast(ProjectBroadcastMessage.from(payload));
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3) 세번째 구독자 계층&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1134&quot; data-origin-height=&quot;844&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GpNp5/btrV1Vikq0t/G6ZadsMzahMZfsGeKakKx1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GpNp5/btrV1Vikq0t/G6ZadsMzahMZfsGeKakKx1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GpNp5/btrV1Vikq0t/G6ZadsMzahMZfsGeKakKx1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGpNp5%2FbtrV1Vikq0t%2FG6ZadsMzahMZfsGeKakKx1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;389&quot; data-origin-width=&quot;1134&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;커맨드와 쿼리를 서로 분리해야 하는 필요에 따라 쿼리 서비스를 만들어 이벤트를 구독하도록 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 여러 서비스에 흩어진 데이터를 조회하는 경우, 데이터를 저장하는 서비스와 별도로 효율적으로 쿼리하는 DB가 필요한 경우에 데이터를 저장하는 서비스가 발행한 이벤트를 구독하여 항상 최신 상태로 유지되는 DB를 쿼리하는 서비스를 만들 수 있다. &lt;a href=&quot;https://learn.microsoft.com/en-us/azure/architecture/patterns/cqrs&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;CQRS&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;정리&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션 이벤트를 통해 이벤트 발행을 보장할 수 있고 내부 이벤트를 통해 비관심사를 분리할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/mandykr/papaco-project-service&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Github&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://thebook.io/007035/&quot;&gt;마이크로서비스 패턴&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://techblog.woowahan.com/7835/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;회원시스템&amp;nbsp;이벤트기반&amp;nbsp;아키텍처&amp;nbsp;구축하기&lt;/a&gt;&lt;/p&gt;</description>
      <category>MSA</category>
      <category>MSA</category>
      <category>마이크로서비스 패턴</category>
      <author>mandykr</author>
      <guid isPermaLink="true">https://mandykr.tistory.com/93</guid>
      <comments>https://mandykr.tistory.com/93#entry93comment</comments>
      <pubDate>Fri, 2 Dec 2022 14:46:01 +0900</pubDate>
    </item>
    <item>
      <title>트랜잭셔널 메시징(아웃박스, 폴링 발행기)</title>
      <link>https://mandykr.tistory.com/91</link>
      <description>&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;2. Amazon SQS&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. SQSListener&lt;/p&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;Github OAuth2 인증을 통해 테이블에 Account 계정을 생성하고 Github 계정 정보가 변경된 경우 Member 마이크로 서비스의 Account 레플리카를 업데이트 하는 기능을 개발한다. 트랜잭셔널 아웃박스 패턴과 폴링 발행기 패턴을 활용해 DB 업데이트와 메시지 발행에서 마이크로 서비스간의 데이터 일관성이 깨지는 문제를 해결한다.&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;1597&quot; data-origin-height=&quot;823&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dir20a/btrQh4xCuB8/16CoEvLrKMTNpHSkj4usi0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dir20a/btrQh4xCuB8/16CoEvLrKMTNpHSkj4usi0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dir20a/btrQh4xCuB8/16CoEvLrKMTNpHSkj4usi0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdir20a%2FbtrQh4xCuB8%2F16CoEvLrKMTNpHSkj4usi0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1597&quot; height=&quot;823&quot; data-origin-width=&quot;1597&quot; data-origin-height=&quot;823&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;DB 업데이트와 메시지 전송을 한 트랜잭션으로 묶지 않으면 마이크로 서비스간 데이터 일관성이 깨지는 문제가 발생할 수 있다. 예를 들어, DB 업데이트 후 메시지를 발행했지만 트랜잭션을 커밋하기 전에 서비스에 문제가 생겨 서버가 종료되는 경우 메시지는 전송되었지만 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를 업데이트하는 트랜잭션의 일부로 메시지를 전송하도록 하더라도 메시지 전송이 실패하는 경우 트랜잭션이 롤백 되도록 할 수는 있지만 메시지는 전송되고 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의 트랜잭션으로 &lt;span&gt;메시지를 함께 저장하면 데이터 일관성을 유지할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 트랜잭셔널 메시징&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) 트랜잭셔널 아웃박스 패턴 (&lt;a href=&quot;https://microservices.io/patterns/data/transactional-outbox.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Transactional Outbox Pattern&lt;/a&gt;)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB 업데이트와 이벤트 생성을 트랜잭션으로 묶고 이벤트 리스너에서 도메인 이벤트를 받아 Outbox 테이블에 저장하도록 작성한다.&lt;/p&gt;
&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;[ CustomOAuth2UserService ]&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1666416246241&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RequiredArgsConstructor
@Service
public class CustomOAuth2UserService implements OAuth2UserService&amp;lt;OAuth2UserRequest, OAuth2User&amp;gt; {
    private final OAuth2UserDetailsService oAuth2UserDetailsService;
    private final ObjectMapper objectMapper;

    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        // ...

        OAuth2UserDetails userDetails = oAuth2UserDetailsService
                .saveOrUpdateAccount(objectMapper.convertValue(attributes, Map.class));
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Github AOuth2 로그인이 성공하면 CustomOAuth2UserService에서 Github 계정 정보를 가져와 LoginAccountService.saveOrUpdateAccount()를 호출해 DB에 저장한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[ LoginAccountService, AccountEventPublisher ]&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1666416119439&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RequiredArgsConstructor
@Transactional
@Service
public class LoginAccountService implements OAuth2UserDetailsService {
    private final AccountRepository accountRepository;
    private final AccountEventPublisher accountEventPublisher;

    @Override
    public LoginAccount saveOrUpdateAccount(Map&amp;lt;String, Object&amp;gt; accountAttributes) {
        LoginAccount loginAccount = LoginAccount.of(accountAttributes);
        Optional&amp;lt;Account&amp;gt; optional = accountRepository.findByUserName(loginAccount.getUserName());

        Account account = null;
        if (optional.isPresent()) {
            account = optional.get();
            updateAccount(account, loginAccount);
            accountEventPublisher.publishUpdatedEvent(account);
        }
        if (optional.isEmpty()) {
            account = createAccount(loginAccount);
            accountRepository.save(account);
            accountEventPublisher.publishCreatedEvent(account);
        }

        return LoginAccount.of(account);
    }
}

@RequiredArgsConstructor
@Transactional
@Service
public class AccountEventPublisher {
    private final ApplicationEventPublisher eventPublisher;

    public void publishEvent(Account account, EventType eventType) {
        AccountEventMapper eventMapper = new AccountEventMapper(account, eventType);
        eventPublisher.publishEvent(eventMapper.toAccountEvent());
    }

    public void publishUpdatedEvent(Account account) {
        publishEvent(account, EventType.UPDATED);
    }

    public void publishCreatedEvent(Account account) {
        publishEvent(account, EventType.CREATED);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;.LoginAccountService는 DB의 Account 테이블을 변경하고 AccountCreated 혹은 AccountUpdated 이벤트를 생성한다.&lt;/li&gt;
&lt;li&gt;Account 테이블의 변경과&amp;nbsp;ApplicationEventPublisher의&amp;nbsp;publishEvent()는 한 트랜잭션으로 묶여있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[ AccountOutboxService ]&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1666416439511&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Slf4j
@RequiredArgsConstructor
@Transactional
@Service
public class AccountOutboxService {
    private final AccountOutboxRepository accountOutboxRepository;

    @EventListener
    public void saveOutbox(AccountEvent accountEvent) {
        OutboxMapper outboxMapper = new OutboxMapper();
        AccountOutbox outbox = outboxMapper.createOutbox(AccountOutbox.class, accountEvent);
        accountOutboxRepository.save(outbox);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;OutboxService에서 발행한 Account 이벤트를 받아 Outbox 테이블에 저장한다.&lt;/li&gt;
&lt;li&gt;@Transactional, @EventListener&lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;@Transactional 로 트랜잭션을 확장해 이벤트 발행과 Outbox 저장을 하나의 트랜잭션으로 묶는다.&lt;/li&gt;
&lt;li&gt;@saveOutbox에서 ObjectMapper가 동작하거나 Outbox 테이블을 변경하는 중에 예외가 발생하면 Account의 DB 변경 트랜잭션도 롤백된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2) 메시지 발행&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;b&gt;폴링 발행기 패턴 &lt;/b&gt;(&lt;a href=&quot;https://microservices.io/patterns/data/polling-publisher.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;b&gt;Polling Publisher Pattern&lt;/b&gt;&lt;/a&gt;)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Outbox 테이블을 지속적으로 Polling 해서 메시지 브로커로 전송한다.&lt;/li&gt;
&lt;li&gt;DB를 자주 폴링하면 비용이 발생하고 NoSQL DB는 쿼리 능력에 따라 사용 가능 여부가 결정된다.&lt;/li&gt;
&lt;li&gt;따라서 규모가 작을 경우 사용할 수 있고 DB 트랜잭션 로그 테일링이 좀 더 정교하고 성능이 좋은 방법이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;b&gt;로그 테일링 패턴 &lt;/b&gt;(&lt;a href=&quot;https://microservices.io/patterns/data/transaction-log-tailing.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;b&gt;Log Tailing Pattern&lt;/b&gt;&lt;/a&gt;)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;트랜잭션 로그 마이너로 DB 트랜잭션 로그를 읽어 Outbox 테이블의 변경분을 하나씩 메시지 브로커로 전송한다.&lt;/li&gt;
&lt;li&gt;트랜잭션 로그 마이너는 디비지움, DynamoDB 스트림즈 등이 있다.&lt;/li&gt;
&lt;/ul&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;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;[ MessageRelayService ]&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1666417047618&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RequiredArgsConstructor
@Transactional
@Service
public class MessageRelayService {
    private final AccountOutboxRepository accountOutboxRepository;
    private final AccountProducer accountProducer;

    @Value(&quot;${cdc.batch_size}&quot;)
    private int batchSize;

    @Scheduled(fixedDelayString = &quot;${cdc.polling_ms}&quot;)
    public void sendAccountEvent() {
        List&amp;lt;AccountOutbox&amp;gt; outboxes = accountOutboxRepository.
                findAllByPublishedIsFalseOrderByIdAsc(Pageable.ofSize(batchSize))
                .toList();
        accountProducer.send(outboxes);
        outboxes.forEach(Outbox::publish);
    }
}

@EnableScheduling
@SpringBootApplication
public class PapacoAuthServiceApplication {

    public static void main(String[] args) {
        SpringApplication.run(PapacoAuthServiceApplication.class, args);
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;간단하게&amp;nbsp;스프링의 Scheduling 기능을 사용해 Outbox 테이블을 폴링하도록 했다.&lt;/li&gt;
&lt;li&gt;@Scheduled(fixedDelayString = &quot;${cdc.polling_ms}&quot;)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;application.properties에 작성한 시간을 주기로 동작한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;findAllByPublishedIsFalseOrderByIdAsc()
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;published 값이 false인 (전송되지 않은) 데이터만 조회한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;accountProducer.send(outboxes);
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;AccountProducer는 인터페이스다.&lt;/li&gt;
&lt;li&gt;인터페이스 구현체인 AccountSQSProducer를 통해 Amazon SQS로 메시지를 발행한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;outboxes.forEach(Outbox::publish);&lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;전송한 Outbox 는 필드를 업데이트한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[ Outbox, AccountOutbox ]&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1667963262373&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class Outbox {
    @CreatedDate
    LocalDateTime createdAt;
    boolean published;
    LocalDateTime publishedAt;

    public void publish() {
        this.published = true;
        this.publishedAt = LocalDateTime.now();
    }

    public abstract Outbox create(DomainEvent event, String payload);
}

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class AccountOutbox extends Outbox {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private Long aggregateId;

    @Column(nullable = false)
    private String aggregateType;

    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private EventType eventType;

    @Lob
    @Column(nullable = false)
    private String payload;

    public AccountOutbox(Long aggregateId, String aggregateType, EventType eventType, String payload) {
        this.aggregateId = aggregateId;
        this.aggregateType = aggregateType;
        this.eventType = eventType;
        this.payload = payload;
    }

    @Override
    public Outbox create(DomainEvent event, String payload) {
        return new AccountOutbox(event.getAggregateId(), event.getClass().getSimpleName(), event.getEventType(), payload);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;createdAt, published, publishedAt
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이벤트 재발행을 위한 필드&lt;/li&gt;
&lt;li&gt;이벤트 타입, 시간, 애그리거트 속성으로 조회해 재발행할 수 있다.&lt;/li&gt;
&lt;li&gt;이벤트 재발행은 배치를 통해 이루어진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[ application.properties ]&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1666417226018&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;cdc.polling_ms=5000
cdc.batch_size=50&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;5초마다 Outbox 테이블을 폴링한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;account 테이블 업데이트와 outbox 테이블 저장이 같은 DB 트랜잭션으로 묶여있기 때문에 메시지는 전송되었지만 account 만 업데이트되는 문제가 해결되었고, 메시지 전송과 outbox 테이블 삭제가 한 트랜잭션으로 묶여있기 때문에 메시지 전송에 실패하면 outbox 테이블도 삭제되지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. Amazon SQS&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SQS 접근을 위한 IAM 사용자와 SQS를 생성해 애플리케이션에서 메시지를 전송한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MemberService에서는 SQS를 폴링해 Account 레플리카를 업데이트한다.&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;1) &lt;span style=&quot;background-color: #ffffff; color: #444444;&quot;&gt;IAM 사용자 생성&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SQS 서비스에 접근하기 위한 &lt;b&gt;IAM 사용자그룹&lt;/b&gt;을 다음과 같이 두 개의 권한을 갖도록 생성한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;AmazonEC2FullAccess&lt;/li&gt;
&lt;li&gt;AdministratorAccess&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;529&quot; data-origin-height=&quot;295&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bFquOe/btrPkqgO23O/inbKeNjIKMeFnKKBvpQjn0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bFquOe/btrPkqgO23O/inbKeNjIKMeFnKKBvpQjn0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bFquOe/btrPkqgO23O/inbKeNjIKMeFnKKBvpQjn0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbFquOe%2FbtrPkqgO23O%2FinbKeNjIKMeFnKKBvpQjn0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;295&quot; data-origin-width=&quot;529&quot; data-origin-height=&quot;295&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자 그룹에 &lt;b&gt;IAM 사용자&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-origin-width=&quot;982&quot; data-origin-height=&quot;509&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dXRkZJ/btrPg0XLLwN/20WvatbbtgxtcKiYtGGyX0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dXRkZJ/btrPg0XLLwN/20WvatbbtgxtcKiYtGGyX0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dXRkZJ/btrPg0XLLwN/20WvatbbtgxtcKiYtGGyX0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdXRkZJ%2FbtrPg0XLLwN%2F20WvatbbtgxtcKiYtGGyX0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;982&quot; height=&quot;509&quot; data-origin-width=&quot;982&quot; data-origin-height=&quot;509&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;978&quot; data-origin-height=&quot;500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dtK02X/btrPhoqwDE9/e3I2tEC3bjQkL0kdfkM1wK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dtK02X/btrPhoqwDE9/e3I2tEC3bjQkL0kdfkM1wK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dtK02X/btrPhoqwDE9/e3I2tEC3bjQkL0kdfkM1wK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdtK02X%2FbtrPhoqwDE9%2Fe3I2tEC3bjQkL0kdfkM1wK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;978&quot; height=&quot;500&quot; data-origin-width=&quot;978&quot; data-origin-height=&quot;500&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;981&quot; data-origin-height=&quot;418&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yIGBF/btrPfYl0oFV/dukonikrLXt9c9JNhQp4Nk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yIGBF/btrPfYl0oFV/dukonikrLXt9c9JNhQp4Nk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yIGBF/btrPfYl0oFV/dukonikrLXt9c9JNhQp4Nk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyIGBF%2FbtrPfYl0oFV%2FdukonikrLXt9c9JNhQp4Nk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;981&quot; height=&quot;418&quot; data-origin-width=&quot;981&quot; data-origin-height=&quot;418&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성된 사용자의 액세스 키 ID와 비밀 액세스 키는 애플리케이션에서 SQS에 메시지를 전송하는데 사용한다.&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;2) Amazon SQS 생성&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;fifo 방식의 SQS를 생성한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1903&quot; data-origin-height=&quot;2547&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kBH97/btrQIubayUj/jum026993XhdqxxlPqxiik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kBH97/btrQIubayUj/jum026993XhdqxxlPqxiik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kBH97/btrQIubayUj/jum026993XhdqxxlPqxiik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkBH97%2FbtrQIubayUj%2Fjum026993XhdqxxlPqxiik%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1903&quot; height=&quot;2547&quot; data-origin-width=&quot;1903&quot; data-origin-height=&quot;2547&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;h4 data-ke-size=&quot;size20&quot;&gt;2) 애플리케이션 적용&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[ build.gradle ]&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1666419888930&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;dependencyManagement {
    imports {
        mavenBom &quot;io.awspring.cloud:spring-cloud-aws-dependencies:2.3.3&quot;
    }
}

dependencies {
    // aws
    implementation 'io.awspring.cloud:spring-cloud-aws-context'
    implementation 'io.awspring.cloud:spring-cloud-starter-aws-messaging'
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링에서 aws를 사용하기 위해 의존성을 추가한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;io.awspring.cloud:spring-cloud-aws-context&lt;/li&gt;
&lt;li&gt;io.awspring.cloud:spring-cloud-starter-aws-messaging&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[ application.properties ]&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1666418834387&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;cloud.aws.region.static=ap-northeast-2
cloud.aws.region.auto=false
cloud.aws.stack.auto=false
cloud.aws.credentials.access-key=[access-key]
cloud.aws.credentials.secret-key=[secret-key]
cloud.aws.end-point.uri=[queue-end-point-uri]&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;access-key : IAM 사용자 access key&lt;/li&gt;
&lt;li&gt;secret-key : IAM 사용자 secret key&lt;/li&gt;
&lt;li&gt;queue-end-point-uri : SQS URL&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[ SQSConfig ]&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1666419633171&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
public class SQSConfig {
    @Value(&quot;${cloud.aws.credentials.access-key}&quot;)
    private String accessKey;

    @Value(&quot;${cloud.aws.credentials.secret-key}&quot;)
    private String secretKey;

    @Value(&quot;${cloud.aws.region.static}&quot;)
    private String region;

    @Bean
    public QueueMessagingTemplate queueMessagingTemplate(AmazonSQSAsync amazonSQSAsync){
        return new QueueMessagingTemplate(amazonSQSAsync());
    }

    private AmazonSQSAsync amazonSQSAsync() {
        BasicAWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey);
        return AmazonSQSAsyncClientBuilder
                .standard()
                .withRegion(region)
                .withCredentials(new AWSStaticCredentialsProvider(credentials))
                .build();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;QueueMessagingTemplate을 스프링 빈으로 등록한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;IAM 정보와 국가 정보를 함께 넣어준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[ AccountSQSProducer ]&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1666419680421&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Slf4j
@RequiredArgsConstructor
@Component
public class AccountSQSProducer implements AccountProducer {
    private final QueueMessagingTemplate queueMessagingTemplate;

    @Value(&quot;${cloud.aws.end-point.uri}&quot;)
    private String endpoint;

    @Transactional
    @Override
    public void send(List&amp;lt;AccountOutbox&amp;gt; outboxes) {
        outboxes.forEach(outbox -&amp;gt; {
            Message&amp;lt;String&amp;gt; message = MessageBuilder.withPayload(outbox.getPayload())
                    .setHeader(SqsMessageHeaders.SQS_GROUP_ID_HEADER, String.valueOf(outbox.getAggregateId()))
                    .setHeader(SqsMessageHeaders.SQS_DEDUPLICATION_ID_HEADER, String.valueOf(outbox.getId()))
                    .setHeader(OutboxMessageHeaders.AGGREGATE_TYPE, outbox.getAggregateType())
                    .build();

            queueMessagingTemplate.send(endpoint, message);
            log.info(&quot;Message: {}, {}, {}&quot;, outbox.getAggregateId(), outbox.getId(), message);
        });
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;FIFO 방식으로 메시지를 전달하기 위해 두 개의 헤더가 필요하다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SQS_GROUP_ID_HEADER : account의 id, 메시지 그룹을 만들어 메시지가 순서대로 처리되도록 한다.&lt;/li&gt;
&lt;li&gt;SQS_DEDUPLICATION_ID_HEADER : outbox 의 id, 중복 메시지 전송을 5분간 방지한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. SQSListener&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Member Service에서 SQS를 폴링해 &lt;span&gt;Auth Service에서 전송한 메시지를&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;가져온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Account 이벤트를 읽어 Member Service 내의 Account 레플리카를 업데이트한다.&lt;/p&gt;
&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;build.gradle, &lt;b&gt;application.properties ]&lt;/b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Auth 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;&lt;b&gt;[ AccountSQSConsumer ]&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메시지는 전송했는데 outbox 테이블을 삭제하는 과정에서 문제가 발생하면 이 후에 메시지를 중복 전송하는 문제가 생길 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Amazon SQS의 FIFO 방식으로 메시지를 전송할 때&lt;span&gt;&amp;nbsp;&lt;/span&gt;SQS_DEDUPLICATION_ID_HEADER 정보를 함께 넣어 주는데 이 값은 메시지가 중복인지를 확인하는데 사용된다. 이는 중복 메시지를 5분 동안 전송하지 않는데 사용되기 때문에 이 후에는 중복 메시지가 전송될 수 있다. 따라서 확실한 처리는 메시지를 받는 Member Service에서 해주어야 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1666420070020&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Slf4j
@RequiredArgsConstructor
@Transactional
@Service
public class AccountSQSConsumer implements AccountConsumer {
    private final ProcessedEventRepository eventRepository;
    private final AccountRepository accountRepository;
    private final ObjectMapper objectMapper;

    @Override
    @SqsListener(value = &quot;sqs-login-account-update.fifo&quot;, deletionPolicy = SqsMessageDeletionPolicy.ON_SUCCESS)
    public void receive(String payload, @Headers Map&amp;lt;String, String&amp;gt; headers) {
        Long messageId = Long.getLong(headers.get(SqsMessageHeaders.SQS_DEDUPLICATION_ID_HEADER));
        Long aggregateId = Long.getLong(headers.get(SqsMessageHeaders.SQS_GROUP_ID_HEADER));
        String aggregateType = headers.get(OutboxMessageHeaders.AGGREGATE_TYPE);

        checkDuplicate(messageId, aggregateType);
        process(payload, headers);
        saveProcessedMessage(messageId, aggregateId, aggregateType);
    }

    @Override
    public void checkDuplicate(Long duplicateId, String aggregateType) {
        if (eventRepository.findByIdAndAggregateType(duplicateId, aggregateType).isPresent()) {
            throw new MessageReceiveDuplicatedException(duplicateId);
        }
    }

    @Override
    public void process(String payload, Map&amp;lt;String, String&amp;gt; headers) {
        try {
            AccountEvent accountEvent = objectMapper.readValue(payload, AccountEvent.class);
            accountRepository.findById(accountEvent.getAggregateId())
                    .ifPresentOrElse(
                            entity -&amp;gt; entity.update(accountEvent.getName(), accountEvent.getEmail()),
                            () -&amp;gt; saveAccount(accountEvent)
                    );
        } catch (JsonProcessingException | IllegalArgumentException e) {
            log.error(&quot;payload={}, headers={}&quot;, payload, headers, e);
            throw new MessageProcessingFailedException(e);
        }
    }

    private void saveAccount(AccountEvent accountEvent) {
        Account account = accountEvent.toAccount();
        accountRepository.save(account);
    }

    @Override
    public void saveProcessedMessage(Long messageId, Long aggregateId, String aggregateType) {
        eventRepository.save(new ProcessedMessage(messageId, aggregateId, aggregateType));
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;@SqsListener
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;value: SQS 큐 이름&lt;/li&gt;
&lt;li&gt;deletionPolicy: 메시지 삭제 방식 정의
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SqsMessageDeletionPolicy.ON_SUCCESS : receive() 메소드에서 예외가 발생하지 않으면 삭제&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;checkDuplicate() : 메시지를 중복 수신했는지 확인한다.&lt;/li&gt;
&lt;li&gt;process() : 메시지를 받아 처리한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&amp;nbsp;SQSMessageProcessingFaildException
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;다음 두 개의 예외가 발생했을 때 잡아서 명시적으로 커스텀 예외로 처리한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;objectMapper.readValue(payload, AccountEvent.class)&amp;nbsp;&amp;rarr; JsonProcessingException&lt;/li&gt;
&lt;li&gt;accountRepository.save(account) &amp;rarr; IllegalArgumentException&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;예외가 발생하지 않으면 Account를 업데이트 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;saveProcessedMessage() : 메시지 처리가 완료되었으면 수신한 Account 이벤트를 저장한다
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메시지 중복 체크에 활용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;a href=&quot;https://thebook.io/007035/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;마이크로서비스 패턴&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;a href=&quot;https://techblog.woowahan.com/7835/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;회원시스템&amp;nbsp;이벤트기반&amp;nbsp;아키텍처&amp;nbsp;구축하기&lt;/a&gt;&lt;/p&gt;</description>
      <category>MSA</category>
      <category>amazon sqs</category>
      <category>트랜잭셔널 아웃박스 패턴</category>
      <category>폴링 발행기 패턴</category>
      <author>mandykr</author>
      <guid isPermaLink="true">https://mandykr.tistory.com/91</guid>
      <comments>https://mandykr.tistory.com/91#entry91comment</comments>
      <pubDate>Sat, 22 Oct 2022 15:14:34 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] 예외 처리</title>
      <link>https://mandykr.tistory.com/90</link>
      <description>&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;2. 자바 예외 활용&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;4. 예외 추상화&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5. JDBC 반복 문제 해결 - JdbcTemplate&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;6. 서블릿 예외처리&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;8. 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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;목표&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;자바의 체크 예외와 언체크(런타임) 예외의 활용법을 이해하고 스프링에서 제공하는 예외 추상화를 통해 특정 기술에 의존하지 않고 순수한 서비스 계층을 유지하는 방법에 대해 알아본다.&lt;/li&gt;
&lt;li&gt;서블릿의 예외처리 과정을 이해하고 스프링에서 예외를 공통으로 처리하는 방법에 대해 알아본다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 자바 예외&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;675&quot; data-origin-height=&quot;406&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b1IbSq/btrPfmsfo85/OqTJjtdo7UOzIEkAJFlrU0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b1IbSq/btrPfmsfo85/OqTJjtdo7UOzIEkAJFlrU0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b1IbSq/btrPfmsfo85/OqTJjtdo7UOzIEkAJFlrU0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb1IbSq%2FbtrPfmsfo85%2FOqTJjtdo7UOzIEkAJFlrU0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;675&quot; height=&quot;406&quot; data-origin-width=&quot;675&quot; data-origin-height=&quot;406&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) 예외 계층&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;(1) Throwable&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최상위 예외이다. 하위에 Exception 과 Error 가 있다.&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;(2) Error&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;상위 예외를 catch 로 잡으면 그 하위 예외까지 함께 잡기 때문에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;애플리케이션 로직에서는 Throwable 예외도 잡으면 안되는데, Error 예외도 함께 잡을 수 있기 때문에다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션&amp;nbsp;로직은&amp;nbsp;이런&amp;nbsp;이유로&amp;nbsp;Exception&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;Error&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;(3)&amp;nbsp; Exception&lt;/p&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;예외이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Exception 과 그 하위 예외(단, RuntimeException 제외)는 모두 컴파일러가 체크하는 체크 예외이다.&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;(4) RuntimeException&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;언체크&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;예외이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RuntimeException&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;h4 data-ke-size=&quot;size20&quot;&gt;2) 체크 예외&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Exception을&amp;nbsp;상속받은&amp;nbsp;예외는&amp;nbsp;체크&amp;nbsp;예외가&amp;nbsp;된다.&lt;/p&gt;
&lt;pre id=&quot;code_1666334628258&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class MyCheckedException extends Exception {
    public MyCheckedException(String message) {
        super(message);
    }
}&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;체크 예외는 try ~ catch로 잡아서 처리하거나, 또는&lt;span&gt;&amp;nbsp;&lt;/span&gt;thorws로 밖으로 던지도록 선언해야 한다.&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_1666334808437&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Service {
    public void callCatch() {
        try {
            repository.call();
        } catch (MyCheckedException e) {
            log.info(&quot;예외 처리, message={}&quot;, e.getMessage(), e);
        }
    }

    public void callThrow() throws MyCheckedException {
        repository.call();
    }
}

public class Repository {
    public void call() throws MyCheckedException {
        throw new MyCheckedException(&quot;ex&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;log.info(&quot;예외 처리, message={}&quot;, e.getMessage(), e);&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;로그를 남길 때 로그의 마지막 인수에 예외 객체를 전달해주면 로그가 해당 예외의 스택 트레이스를 추가로 출력해준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Exception 으로 MyCheckedException 을&lt;span&gt; 잡거나 던질 수 있다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;catch는 해당 타입과 그 하위 타입을 모두 잡을 수 있다.&lt;/li&gt;
&lt;li&gt;예외를 밖으로 던지는 경우에도 해당 타입과 그 하위 타입을 모두 던질 수 있다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;▶ 체크 예외 장단점&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;장점:&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;/li&gt;
&lt;li&gt;단점: 하지만 실제로는 개발자가 모든 체크 예외를 반드시 잡거나 던지도록 처리해야 하기 때문에,&amp;nbsp; 너무 번거로운 일이 된다. 크게 신경쓰고 싶지 않은 예외까지 모두 챙겨야 한다. 추가로 의존관계에 따른 단점도 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3) 언체크 예외&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RuntimeException&amp;nbsp;과&amp;nbsp;그&amp;nbsp;하위&amp;nbsp;예외는&amp;nbsp;언체크&amp;nbsp;예외로&amp;nbsp;분류된다.&lt;/p&gt;
&lt;pre id=&quot;code_1666335375425&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class MyUncheckedException extends RuntimeException {
    public MyUncheckedException(String message) {
        super(message);
    }
}&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;언체크 예외는 예외를 던지는 throws 를 선언하지 않고 생략할&amp;nbsp;수&amp;nbsp;있다.&amp;nbsp;이&amp;nbsp;경우&amp;nbsp;&lt;b&gt;자동으로&amp;nbsp;예외를&amp;nbsp;던진다.&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1666335477383&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Service {
    public void callCatch() {
        try {
            repository.call();
        } catch (MyUncheckedException e) {
            log.info(&quot;예외 처리, message={}&quot;, e.getMessage(), e);
        }
    }

    public void callThrow() {
        repository.call();
    }
}

public class Repository {
    public void call() {
        throw new MyUncheckedException(&quot;ex&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;언체크 예외는 throws를 생략해도 되지만 &lt;b&gt;중요한 예외의 경우 선언&lt;/b&gt;해두면 해당 코드를 호출하는 개발자가 이런 예외가 발생한다는 점을 IDE를 통해 좀 더 편리하게 인지할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1666335635181&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public void callThrow() throws MyCheckedException {
    repository.call();
}&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;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;장점: 신경쓰고 싶지 않은 언체크 예외를 무시할 수 있다. &lt;br /&gt;체크 예외의 경우 처리할 수 없는 예외를 밖으로던지려면 항상 throws 예외 를 선언해야 하지만, 언체크 예외는 이 부분을 생략할 수 있다.&amp;nbsp;신경쓰고&amp;nbsp;싶지&amp;nbsp;않은&amp;nbsp;예외의&amp;nbsp;의존관계를&amp;nbsp;참조하지&amp;nbsp;않아도&amp;nbsp;되는&amp;nbsp;장점이&amp;nbsp;있다.&lt;/li&gt;
&lt;li&gt;단점: 언체크 예외는 개발자가 실수로 예외를 누락할 수 있다. 반면에 체크 예외는 컴파일러를 통해 예외 누락을&amp;nbsp;잡아준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 자바 예외 활용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;672&quot; data-origin-height=&quot;209&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7ANM7/btrPcURGfGl/HxnxeqrVH2owBuiA6ilhX1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7ANM7/btrPcURGfGl/HxnxeqrVH2owBuiA6ilhX1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7ANM7/btrPcURGfGl/HxnxeqrVH2owBuiA6ilhX1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7ANM7%2FbtrPcURGfGl%2FHxnxeqrVH2owBuiA6ilhX1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;672&quot; height=&quot;209&quot; data-origin-width=&quot;672&quot; data-origin-height=&quot;209&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1666336510880&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Controller {
    Service service = new Service();
    public void request() throws SQLException, ConnectException {
        service.logic();
    }
}

public class Service {
    Repository repository = new Repository();
    NetworkClient networkClient = new NetworkClient();
    public void logic() throws SQLException, ConnectException {
        repository.call();
        networkClient.call();
    }
}

public class NetworkClient {
    public void call() throws ConnectException {
        throw new ConnectException(&quot;연결 실패&quot;);
    }
}

public class Repository {
    public void call() throws SQLException {
        throw new SQLException(&quot;ex&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;b&gt;▶ 복구 불가능한 예외&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SQLException 을 예를 들면 데이터베이스에 무언가 문제가 있어서 발생하는 예외이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이런 문제들은 대부분 복구가 불가능하다. 특히나 대부분의 서비스나 컨트롤러는 이런&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;따라서 이런 문제들은 일관성 있게 공통으로 처리해야 한다.&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;서블릿 필터, 스프링 인터셉터, 스프링의 ControllerAdvice 를 사용하면 이런 부분을 깔끔하게 공통으로 해결할 수 있다.&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;▶ 의존&amp;nbsp;관계에&amp;nbsp;대한&amp;nbsp;문제&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;throws SQLException, ConnectException&amp;nbsp;처럼 예외를 던지는 부분을 코드에 선언하면 서비스, 컨트롤러에서 java.sql.SQLException 을 의존하기 때문에 문제가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;리포지토리를 JDBC가 아닌 JPA로 변경한다면, &lt;/b&gt;&lt;br /&gt;&lt;b&gt;SQLException&amp;nbsp;에&amp;nbsp;의존하던&amp;nbsp;모든&amp;nbsp;서비스,&amp;nbsp;컨트롤러의&amp;nbsp;코드를&amp;nbsp;JPAException&amp;nbsp;에&amp;nbsp;의존하도록&amp;nbsp;고쳐야&amp;nbsp;한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OCP, DI를 통해 클라이언트 코드의 변경 없이 대상 구현체를 변경할 수 있다는 장점이 사라지게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;(2) 언체크 예외 활용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;체크 예외를 다음과 같이 언체크 예외로 변환해 체크 예외를 사용할 때 발생하는 문제를 해결한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SQLException &amp;rarr; RuntimeSQLException&lt;/li&gt;
&lt;li&gt;ConnectException &amp;rarr; RuntimeConnectException&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;668&quot; data-origin-height=&quot;228&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cr8TOy/btrPevYfpj8/RIjM9ArO1xjOx8QpkDxcz1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cr8TOy/btrPevYfpj8/RIjM9ArO1xjOx8QpkDxcz1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cr8TOy/btrPevYfpj8/RIjM9ArO1xjOx8QpkDxcz1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcr8TOy%2FbtrPevYfpj8%2FRIjM9ArO1xjOx8QpkDxcz1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;228&quot; data-origin-width=&quot;668&quot; data-origin-height=&quot;228&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1666336997636&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Controller {
    Service service = new Service();
    public void request() {
        service.logic();
    }
}

public class Service {
    Repository repository = new Repository();
    NetworkClient networkClient = new NetworkClient();
    public void logic() {
        repository.call();
        networkClient.call();
    }
}

public class NetworkClient {
    public void call() {
        throw new RuntimeConnectException(&quot;연결 실패&quot;);
    }
}

public class Repository {
    public void call() {
        try {
            runSQL();
        } catch (SQLException e) {
            throw new RuntimeSQLException(e);
        }
    }
    
    private void runSQL() throws SQLException {
        throw new SQLException(&quot;ex&quot;);
    }
}

public class RuntimeConnectException extends RuntimeException {
    public RuntimeConnectException(String message) {
        super(message);
    }
}

public class RuntimeSQLException extends RuntimeException {
    public RuntimeSQLException() {
    }
    public RuntimeSQLException(Throwable cause) {
        super(cause);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;▶ 복구 불가능한 예외&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시스템에서 발생한 예외는 대부분 복구 불가능 예외이다. &lt;b&gt;런타임 예외를 사용하면 서비스나 컨트롤러가 이런 복구 불가능한 예외를 신경쓰지 않아도 된다. &lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 이렇게 복구 불가능한 예외는 일관성 있게 공통으로 처리해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;▶&lt;span&gt;&amp;nbsp;&lt;/span&gt;의존&amp;nbsp;관계에&amp;nbsp;대한&amp;nbsp;문제&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;따라서 체크 예외 처럼 예외를 강제로 의존하지 않아도 된다.&lt;/p&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;(3) 결론&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기본적으로&amp;nbsp;언체크(런타임)&amp;nbsp;예외를&amp;nbsp;사용하자.&lt;/li&gt;
&lt;li&gt;체크&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;처리해야&amp;nbsp;하는&amp;nbsp;문제일&amp;nbsp;때만&amp;nbsp;체크&amp;nbsp;예외를&amp;nbsp;사용해야&amp;nbsp;한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;런타임 예외를 사용하면 중간에 기술이 변경되어도 해당 예외를 사용하지 않는 컨트롤러, 서비스에서는 코드를 변경하지 않아도 된다. 구현 기술이 변경되는 경우, 예외를 공통으로 처리하는 곳에서만 변경하면 되기 때문에 변경의 영향 범위는 최소화 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&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;또는 코드에 throws 런타임예외를 남겨서 중요한 예외를 인지할 수 있게 해야 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1666337488226&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; * Make an instance managed and persistent.
 * @param entity entity instance
 * @throws EntityExistsException if the entity already exists.
 * @throws IllegalArgumentException if the instance is not an
 * entity
 * @throws TransactionRequiredException if there is no transaction when
 * invoked on a container-managed entity manager of that is of type
 * &amp;lt;code&amp;gt;PersistenceContextType.TRANSACTION&amp;lt;/code&amp;gt;
 */
public void persist(Object entity);

/**
 * Issue a single SQL execute, typically a DDL statement.
 * @param sql static SQL to execute
 * @throws DataAccessException if there is any problem
 */
void execute(String sql) throws DataAccessException;&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;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 예외 포함과 스택 트레이스&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예외를 전환할 때는 꼭! 기존 예외를 포함해야 한다.&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1666337680024&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public void call() {
    try {
        runSQL();
    } catch (SQLException e) {
        throw new RuntimeSQLException(e); //기존 예외(e) 포함
    }
}

public class RuntimeSQLException extends RuntimeException {
    public RuntimeSQLException(Throwable cause) {
        super(cause);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;throw new RuntimeSQLException(e);&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예외를 포함해서 기존에 발생한 java.sql.SQLException 과 스택 트레이스를 확인할 수 있도록 해야한다.&lt;/li&gt;
&lt;/ul&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;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 예외 추상화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리포지토리에서 SQLException을 MyDbException이라는 RuntimeException으로 변환했을때,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;서비스 계층에서는 MyDbException 이라는 예외만 넘어오기 때문에 예외를 구분할 수 없는 단점이 있다.&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;h4 data-ke-size=&quot;size20&quot;&gt;1) 에러 코드&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;b&gt;키&amp;nbsp;중복&amp;nbsp;오류&amp;nbsp;코드&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;H2 DB: 23505&lt;/li&gt;
&lt;li&gt;MySQL:&amp;nbsp;1062&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;H2&amp;nbsp;데이터베이스&amp;nbsp;오류&amp;nbsp;코드&amp;nbsp;참고 &lt;br /&gt;&lt;a href=&quot;https://www.h2database.com/javadoc/org/h2/api/ErrorCode.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://www.h2database.com/javadoc/org/h2/api/ErrorCode.html&lt;/a&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2) 예외 구분&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;키 중복 오류에 대해 서비스 계층에서 별도 처리를 하기위해 기존에 사용했던 MyDbException 을 상속받아서 의미있는 계층을 형성한다. 데이터 중복의 경우에 사용하는 MyDuplicateKeyException을 만든다.&lt;/p&gt;
&lt;pre id=&quot;code_1666400396672&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class MyDuplicateKeyException extends MyDbException {
    public MyDuplicateKeyException() {
    }
    public MyDuplicateKeyException(String message) {
        super(message);
    }
    public MyDuplicateKeyException(String message, Throwable cause) {
        super(message, cause);
    }
    public MyDuplicateKeyException(Throwable cause) {
        super(cause);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MyDuplicateKeyException은 JDBC나 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;하지만 리포지토리에서 이런 방식으로 예외를 구분하면 &lt;b&gt;데이터베이스와 수많은 에러 코드에 의존성&lt;/b&gt;을 가지게 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1666400516948&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;try {
    con = dataSource.getConnection();
    pstmt = con.prepareStatement(sql);
    pstmt.setString(1, member.getMemberId());
    pstmt.setInt(2, member.getMoney());
    pstmt.executeUpdate();
    return member;
} catch (SQLException e) {
    //h2 db
    if (e.getErrorCode() == 23505) {
        throw new MyDuplicateKeyException(e);
    }
    throw new MyDbException(e);
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SQL ErrorCode는 각각의 데이터베이스 마다 다르다. 결과적으로 데이터베이스가 변경될 때 마다 ErrorCode도 모두 변경해야 한다.&lt;/li&gt;
&lt;li&gt;데이터베이스가 전달하는 오류는 키 중복 뿐만 아니라 락이 걸린 경우, SQL 문법에 오류 있는 경우 등등 수십 수백가지 오류 코드가 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3) 스프링 예외 추상화&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-origin-width=&quot;673&quot; data-origin-height=&quot;486&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zp8zL/btrPfmAFJRl/EvLCCKxuKTT8tVHaMvmU0k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zp8zL/btrPfmAFJRl/EvLCCKxuKTT8tVHaMvmU0k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zp8zL/btrPfmAFJRl/EvLCCKxuKTT8tVHaMvmU0k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fzp8zL%2FbtrPfmAFJRl%2FEvLCCKxuKTT8tVHaMvmU0k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;486&quot; data-origin-width=&quot;673&quot; data-origin-height=&quot;486&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예외의 최고 상위는 org.springframework.dao.DataAccessException 이다. &lt;br /&gt;런타임 예외를 상속 받았기 때문에 스프링이 제공하는 데이터 접근 계층의 모든 예외는 &lt;b&gt;런타임 예외&lt;/b&gt;이다.&lt;/li&gt;
&lt;li&gt;NonTransient와 Transient&lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Transient&amp;nbsp;는&amp;nbsp;일시적&lt;/b&gt;이라는&amp;nbsp;뜻이다.&amp;nbsp;&lt;br /&gt;Transient 하위 예외는 동일한 SQL을 다시 시도했을 때 성공할&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;/li&gt;
&lt;li&gt;&lt;b&gt;NonTransient&amp;nbsp;는&amp;nbsp;일시적이지&amp;nbsp;않다&lt;/b&gt;는&amp;nbsp;뜻이다.&amp;nbsp;&lt;br /&gt;같은&amp;nbsp;SQL을&amp;nbsp;그대로&amp;nbsp;반복해서&amp;nbsp;실행하면&amp;nbsp;실패한다. &lt;br /&gt;SQL&amp;nbsp;문법&amp;nbsp;오류,&amp;nbsp;데이터베이스&amp;nbsp;제약조건&amp;nbsp;위배&amp;nbsp;등이&amp;nbsp;있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;(1) 예외 변환기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링은 데이터베이스에서 발생하는 오류 코드를 스프링이 정의한 예외로 자동으로 변환해주는 변환기를 제공한다.&lt;/p&gt;
&lt;pre id=&quot;code_1666401322577&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void exceptionTranslator() {
    String sql = &quot;select bad grammar&quot;;
    try {
        Connection con = dataSource.getConnection();
        PreparedStatement stmt = con.prepareStatement(sql);
        stmt.executeQuery();
    } catch (SQLException e) {
        // e.getErrorCode() = 42122        
        SQLExceptionTranslator exTranslator = new SQLErrorCodeSQLExceptionTranslator(dataSource);
		DataAccessException resultEx = exTranslator.translate(&quot;select&quot;, sql, e);
		// resultEx.getClass(): org.springframework.jdbc.BadSqlGrammarException
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;translate()
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메서드의 첫번째 파라미터는 읽을 수 있는 설명이고, 두번째는 실행한 sql, 마지막은 발생된 SQLException 을 전달하면 된다&lt;/li&gt;
&lt;li&gt;이렇게 하면 적절한 스프링 데이터 접근 계층의 예외로 변환해서 반환해준다.&lt;/li&gt;
&lt;li&gt;SQL 문법이 잘못되었으므로 BadSqlGrammarException 을 반환&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;(2) sql-error-codes.xml&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 SQL 예외 변환기는 SQL ErrorCode를 sql-error-codes.xml 파일에 대입해서 어떤 스프링 데이터 접근 예외로 전환해야 할지 찾아낸다. 따라서 &lt;b&gt;각각의 DB마다 다른 SQL ErrorCode에 대해 같은 예외를 반환할 수 있다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, H2 데이터베이스에서는 42000, MySQL 데이터베이스에서는 1054 가 발생하면 badSqlGrammarCodes 이기 때문에 BadSqlGrammarException 을 반환한다.&lt;/p&gt;
&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;org.springframework.jdbc.support.sql-error-codes.xml &lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1666401573922&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;bean id=&quot;H2&quot; class=&quot;org.springframework.jdbc.support.SQLErrorCodes&quot;&amp;gt;
    &amp;lt;property name=&quot;badSqlGrammarCodes&quot;&amp;gt;
        &amp;lt;value&amp;gt;42000,42001,42101,42102,42111,42112,42121,42122,42132&amp;lt;/value&amp;gt;
    &amp;lt;/property&amp;gt;
    &amp;lt;property name=&quot;duplicateKeyCodes&quot;&amp;gt;
        &amp;lt;value&amp;gt;23001,23505&amp;lt;/value&amp;gt;
    &amp;lt;/property&amp;gt;
&amp;lt;/bean&amp;gt;
&amp;lt;bean id=&quot;MySQL&quot; class=&quot;org.springframework.jdbc.support.SQLErrorCodes&quot;&amp;gt;
    &amp;lt;property name=&quot;badSqlGrammarCodes&quot;&amp;gt;
        &amp;lt;value&amp;gt;1054,1064,1146&amp;lt;/value&amp;gt;
    &amp;lt;/property&amp;gt;
    &amp;lt;property name=&quot;duplicateKeyCodes&quot;&amp;gt;
        &amp;lt;value&amp;gt;1062&amp;lt;/value&amp;gt;
    &amp;lt;/property&amp;gt;
&amp;lt;/bean&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;span&gt;&amp;nbsp;&lt;/span&gt;서비스 계층에서도 스프링이 제공하는 예외를 사용하면 된다. 향후 JDBC에서 JPA로 구현 기술을 변경하더라도, 스프링은 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;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;5.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;JDBC 반복 문제 해결 - JdbcTemplate&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링이 예외를 추상화해준 덕분에, 서비스 계층은 특정 리포지토리의 구현 기술과 예외에 종속적이지 않게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 다음과 같이 리포지토리에서는 예외 변환 코드를 반복해서 작성해야 하고 다른 반복 코드들도 존재한다.&lt;/p&gt;
&lt;pre id=&quot;code_1666402480716&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;} catch (SQLException e) {
	throw exTranslator.translate(&quot;save&quot;, sql, e);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;JDBC&amp;nbsp;반복&amp;nbsp;문제&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;커넥션&amp;nbsp;조회,&amp;nbsp;커넥션&amp;nbsp;동기화&lt;/li&gt;
&lt;li&gt;PreparedStatement&amp;nbsp;생성&amp;nbsp;및&amp;nbsp;파라미터&amp;nbsp;바인딩&lt;/li&gt;
&lt;li&gt;쿼리&amp;nbsp;실행&lt;/li&gt;
&lt;li&gt;결과&amp;nbsp;바인딩&lt;/li&gt;
&lt;li&gt;예외&amp;nbsp;발생시&amp;nbsp;스프링&amp;nbsp;예외&amp;nbsp;변환기&amp;nbsp;실행&lt;/li&gt;
&lt;li&gt;리소스 종료&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링은 JDBC의 반복 문제를 해결하기 위해 JdbcTemplate 이라는 템플릿을 제공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JdbcTemplate 은 JDBC로 개발할 때 발생하는 반복을 대부분 해결해준다. 그 뿐만 아니라 &lt;b&gt;트랜잭션을 위한 커넥션 동기화&lt;/b&gt;는 물론이고, &lt;b&gt;예외 발생시 스프링 예외 변환기&lt;/b&gt;도 자동으로 실행해준다.&lt;/p&gt;
&lt;pre id=&quot;code_1666402799511&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class MemberRepository {
    private final JdbcTemplate template;

    public MemberRepository(DataSource dataSource) {
        template = new JdbcTemplate(dataSource);
    }

    @Override
    public Member save(Member member) {
        String sql = &quot;insert into member(member_id, money) values(?, ?)&quot;;
        template.update(sql, member.getMemberId(), member.getMoney());
        return member;
    }

    @Override
    public Member findById(String memberId) {
        String sql = &quot;select * from member where member_id = ?&quot;;
        return template.queryForObject(sql, memberRowMapper(), memberId);
    }

    @Override
    public void update(String memberId, int money) {
        String sql = &quot;update member set money=? where member_id=?&quot;;
        template.update(sql, money, memberId);
    }

    @Override
    public void delete(String memberId) {
        String sql = &quot;delete from member where member_id=?&quot;;
        template.update(sql, memberId);
    }

    private RowMapper&amp;lt;Member&amp;gt; memberRowMapper() {
        return (rs, rowNum) -&amp;gt; {
            Member member = new Member();
            member.setMemberId(rs.getString(&quot;member_id&quot;));
            member.setMoney(rs.getInt(&quot;money&quot;));
            return member;
        };
    }
}&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;a href=&quot;https://mandykr.tistory.com/89#:~:text=(2)-,%EB%A6%AC%EC%86%8C%EC%8A%A4%20%EB%8F%99%EA%B8%B0%ED%99%94,-PlatformTransactionManager%EB%8A%94%20%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;트랜잭션 커넥션 동기화&lt;/a&gt;를 위해 작성해야 했던 다음과 같은 코드도 &lt;span&gt;JdbcTemplate을&lt;/span&gt;&amp;nbsp;사용하면서 불필요해졌다.&lt;/p&gt;
&lt;pre id=&quot;code_1666402907828&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private void close(Connection con, Statement stmt, ResultSet rs) {
    JdbcUtils.closeResultSet(rs);
    JdbcUtils.closeStatement(stmt);
    DataSourceUtils.releaseConnection(con, dataSource);
}

private Connection getConnection() {
    Connection con = DataSourceUtils.getConnection(dataSource);
    log.info(&quot;get connection={} class={}&quot;, con, con.getClass());
    return con;
}&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;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;정리&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서비스&amp;nbsp;계층의&amp;nbsp;순수성 &lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;트랜잭션 추상화 + 트랜잭션 AOP 덕분에 서비스 계층의 순수성을 최대한 유지하면서 서비스 계층에서&amp;nbsp;트랜잭션을&amp;nbsp;사용할&amp;nbsp;수&amp;nbsp;있다.&lt;/li&gt;
&lt;li&gt;스프링이 제공하는 예외 추상화와 예외 변환기 덕분에, 데이터 접근 기술이 변경되어도 서비스 계층의 순수성을&amp;nbsp;유지하면서&amp;nbsp;예외도&amp;nbsp;사용할&amp;nbsp;수&amp;nbsp;있다.&lt;/li&gt;
&lt;li&gt;서비스 계층이 리포지토리 인터페이스에 의존한 덕분에 향후 리포지토리가 다른 구현 기술로 변경되어도&amp;nbsp;서비스&amp;nbsp;계층을&amp;nbsp;순수하게&amp;nbsp;유지할&amp;nbsp;수&amp;nbsp;있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;리포지토리에서&amp;nbsp;JDBC를&amp;nbsp;사용하는&amp;nbsp;반복&amp;nbsp;코드가&amp;nbsp;JdbcTemplate&amp;nbsp;으로&amp;nbsp;대부분&amp;nbsp;제거되었다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. 서블릿 예외처리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션은 사용자 요청별로 별도의 쓰레드가 할당되고, 서블릿 컨테이너 안에서 실행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션에서 예외가 발생했는데, 어디선가 try ~ catch로 예외를 잡아서 처리하면 아무런 문제가 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 애플리케이션에서 예외를 잡지 못하면, 다음과 같은 경로로 WAS까지 예외가 전달된다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;WAS(여기까지 전파) &amp;lt;- 필터 &amp;lt;- 서블릿 &amp;lt;- 인터셉터 &amp;lt;- 컨트롤러(예외발생)&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) 톰캣이 제공하는 오류 페이지&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;b&gt;application.properties &lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1666582510034&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;server.error.whitelabel.enabled=false&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;(1) 500&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;톰캣은 예외를 받으면 서버 내부에서 처리할 수 없는 오류가 발생한 것으로 생각해서 HTTP 상태 코드 500을 반환한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;HTTP Status 500 &amp;ndash; Internal Server Error&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;(2) 404&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;HTTP Status 404 &amp;ndash; Not Found&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2) 서블릿 컨테이너에게 오류를 전달하는 방법&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;(1) 예외 발생&lt;/p&gt;
&lt;pre id=&quot;code_1666582961921&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Controller
public class ServletExController {
    @GetMapping(&quot;/error-ex&quot;)
    public void errorEx() {
        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;size18&quot;&gt;(2) response.sendError(HTTP 상태 코드, 오류 메시지)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HttpServletResponse 가 제공하는 sendError 라는 메서드를 사용해 서블릿 컨테이너에게 오류가 발생했다는 점을 전달할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1666583080306&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@GetMapping(&quot;/error-404&quot;)
public void error404(HttpServletResponse response) throws IOException {
     response.sendError(404, &quot;404 오류!&quot;);
}

@GetMapping(&quot;/error-500&quot;)
public void error500(HttpServletResponse response) throws IOException {
     response.sendError(500);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서블릿&amp;nbsp;컨테이너는&amp;nbsp;고객에게&amp;nbsp;응답&amp;nbsp;전에&amp;nbsp;response&amp;nbsp;에&amp;nbsp;sendError()&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;/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;3) 서블릿 오류 페이지 제공 방법&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;size18&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;b&gt;에러 페이지 등록&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;/templates/error-page/404.html&lt;/li&gt;
&lt;li&gt;/templates/error-page/500.html&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;에러 페이지 컨트롤러 등록&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ErrorPageController
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;@RequestMapping(&quot;/error-page/404&quot;)&lt;/li&gt;
&lt;li&gt;@RequestMapping(&quot;/error-page/500&quot;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1666583819507&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Slf4j
@Controller
public class ErrorPageController {
    @RequestMapping(&quot;/error-page/404&quot;)
    public String errorPage404(HttpServletRequest request, HttpServletResponse
            response) {
        log.info(&quot;errorPage 404&quot;);
        return &quot;error-page/404&quot;;
    }
    
    @RequestMapping(&quot;/error-page/500&quot;)
    public String errorPage500(HttpServletRequest request, HttpServletResponse
            response) {
        log.info(&quot;errorPage 500&quot;);
        return &quot;error-page/500&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;b&gt;에러 발생시 재요청 경로 설정&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;WebServerCustomizer&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1666583499171&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
public class WebServerCustomizer implements WebServerFactoryCustomizer&amp;lt;ConfigurableWebServerFactory&amp;gt; {
    
    @Override
    public void customize(ConfigurableWebServerFactory factory) {
        ErrorPage errorPage404 = new ErrorPage(HttpStatus.NOT_FOUND, &quot;/error-page/404&quot;);
        ErrorPage errorPage500 = new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR, &quot;/error-page/500&quot;);
        ErrorPage errorPageEx = new ErrorPage(RuntimeException.class, &quot;/error-page/500&quot;);
        factory.addErrorPages(errorPage404, errorPage500, errorPageEx);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;new ErrorPage(RuntimeException.class, &quot;/error-page/500&quot;);&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;RuntimeException이 발생하면 WAS는 &quot;/error-page/500&quot; 경로로 재요청 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;(2) 오류 페이지 작동원리&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서블릿은 Exception(예외)이 발생해서 서블릿 밖으로 전달되거나 또는 response.sendError() 가 호출 되었을 때 설정된 오류 페이지를 찾는다. 오류 페이지를 출력하기 위해 오류 페이지를 재요청한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;1. WAS(여기까지 전파) &amp;lt;- 필터 &amp;lt;- 서블릿 &amp;lt;- 인터셉터 &amp;lt;- 컨트롤러(예외발생) &lt;br /&gt;2. WAS `/error-page/500` 다시 요청 -&amp;gt; 필터 -&amp;gt; 서블릿 -&amp;gt; 인터셉터 -&amp;gt; 컨트롤러(/errorpage/500) -&amp;gt; View&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;RuntimeException 예외가 WAS까지 전달되면, WAS는 오류 페이지 정보를 확인한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;new ErrorPage(RuntimeException.class, &quot;/error-page/500&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;확인해보니 RuntimeException 의 오류 페이지로 /error-page/500 이 지정되어 있다. WAS는 오류 페이지를 출력하기 위해 /error-page/500 를 다시 요청한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;(3) request.attribute&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WAS는 오류 페이지를 단순히 다시 요청만 하는 것이 아니라, 오류 정보를 request 의 attribute 에 추가해서 넘겨준다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;javax.servlet.error.exception&amp;nbsp;:&amp;nbsp;예외&lt;/li&gt;
&lt;li&gt;javax.servlet.error.exception_type&amp;nbsp;:&amp;nbsp;예외&amp;nbsp;타입&lt;/li&gt;
&lt;li&gt;javax.servlet.error.message&amp;nbsp;:&amp;nbsp;오류&amp;nbsp;메시지&lt;/li&gt;
&lt;li&gt;javax.servlet.error.request_uri&amp;nbsp;:&amp;nbsp;클라이언트&amp;nbsp;요청&amp;nbsp;URI&lt;/li&gt;
&lt;li&gt;javax.servlet.error.servlet_name&amp;nbsp;:&amp;nbsp;오류가&amp;nbsp;발생한&amp;nbsp;서블릿&amp;nbsp;이름&lt;/li&gt;
&lt;li&gt;javax.servlet.error.status_code&amp;nbsp;:&amp;nbsp;HTTP&amp;nbsp;상태&amp;nbsp;코드&lt;/li&gt;
&lt;/ul&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;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 예외가 발생해서 WAS까지 전파된다. &lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. WAS는 오류 페이지 경로를 찾아서 내부에서 오류 페이지를 호출한다. 이때 오류 페이지 경로로 필터, 서블릿, 인터셉터, 컨트롤러가 모두 다시 호출된다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. WAS는 오류 페이지와 함께 오류 정보를 request 의 attribute 에 추가해서 넘겨준다&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;4) 필터와 인터셉터 재요청 구분&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오류가 발생하면 오류 페이지를 출력하기 위해 WAS 내부에서 다시 한번 호출이 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 필터, 인터셉터도 모두 다시 호출된다. 따라서 오류 페이지를 출력하기 위한 내부 요청인지 구분해 필터와 인터셉터에서 비효율적으로 동작하지 않도록 해야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;서블릿은 이런 문제를 해결하기 위해 request.attribute에 DispatcherType 이라는 추가 정보를 제공한다.&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;DispatcherType&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;REQUEST&amp;nbsp;:&amp;nbsp;클라이언트&amp;nbsp;요청&lt;/li&gt;
&lt;li&gt;ERROR&amp;nbsp;:&amp;nbsp;오류&amp;nbsp;요청&lt;/li&gt;
&lt;li&gt;FORWARD&amp;nbsp;:&amp;nbsp;MVC에서&amp;nbsp;배웠던&amp;nbsp;서블릿에서&amp;nbsp;다른&amp;nbsp;서블릿이나&amp;nbsp;JSP를&amp;nbsp;호출할&amp;nbsp;때&lt;/li&gt;
&lt;li&gt;RequestDispatcher.forward(request,&amp;nbsp;response);&lt;/li&gt;
&lt;li&gt;INCLUDE&amp;nbsp;:&amp;nbsp;서블릿에서&amp;nbsp;다른&amp;nbsp;서블릿이나&amp;nbsp;JSP의&amp;nbsp;결과를&amp;nbsp;포함할&amp;nbsp;때&lt;/li&gt;
&lt;li&gt;RequestDispatcher.include(request,&amp;nbsp;response);&lt;/li&gt;
&lt;li&gt;ASYNC&amp;nbsp;:&amp;nbsp;서블릿&amp;nbsp;비동기&amp;nbsp;호출&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;(1) 필터에서 구분&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필터는 filterRegistrationBean.setDispatcherTypes() 으로 &lt;span&gt;DispatcherType에 따라 필터를 적용할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1666584880082&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Bean
    public FilterRegistrationBean logFilter() {
        FilterRegistrationBean&amp;lt;Filter&amp;gt; filterRegistrationBean = new
                FilterRegistrationBean&amp;lt;&amp;gt;();
        filterRegistrationBean.setFilter(new LogFilter());
        filterRegistrationBean.setOrder(1);
        filterRegistrationBean.addUrlPatterns(&quot;/*&quot;);
        filterRegistrationBean.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ERROR);
        return filterRegistrationBean;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;filterRegistrationBean.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ERROR);
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클라이언트 요청은 물론이고, 오류 페이지 요청에서도 필터가 호출된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;아무것도 넣지 않으면 기본 값이 DispatcherType.REQUEST 이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;(2) 인터셉터에서 구분&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인터셉터는 excludePathPatterns()를 이용해 오류 페이지 경로를 제외시킬 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1666585072683&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LogInterceptor())
                .order(1)
                .addPathPatterns(&quot;/**&quot;)
                .excludePathPatterns(
                        &quot;/css/**&quot;, &quot;/*.ico&quot;
                        , &quot;/error&quot;, &quot;/error-page/**&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;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. 스프링 부트 오류 페이지&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 부트는 WebServerCustomizer에 예외 종류에 따라서 ErrorPage를 추가하는 과정과&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예외 처리용 컨트롤러 ErrorPageController를 만드는 과정을 자동화해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 개발자는 특정 경로에 에러 페이지만 만들어 두면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) 기본 오류 페이지&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 부트는 ErrorPage 를 자동으로 등록한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상태코드와 예외를 설정하지 않으면 모든 오류는 /error 를 호출하게 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1666589248493&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;new ErrorPage(&quot;/error&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;b&gt;ErrorMvcAutoConfiguration&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ErrorMvcAutoConfiguration는 /error에 오류 페이지를 자동으로 등록해준다.&lt;/p&gt;
&lt;pre id=&quot;code_1666589105258&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private static class StaticView implements View {
	
    public void render(Map&amp;lt;String, ?&amp;gt; model, HttpServletRequest request, HttpServletResponse response)
            throws Exception {
 
        builder.append(&quot;&amp;lt;html&amp;gt;&amp;lt;body&amp;gt;&amp;lt;h1&amp;gt;Whitelabel Error Page&amp;lt;/h1&amp;gt;&quot;).append(
                &quot;&amp;lt;p&amp;gt;This application has no explicit mapping for /error, so you are seeing this as a fallback.&amp;lt;/p&amp;gt;&quot;)
                .append(&quot;&amp;lt;div id='created'&amp;gt;&quot;).append(timestamp).append(&quot;&amp;lt;/div&amp;gt;&quot;)
                .append(&quot;&amp;lt;div&amp;gt;There was an unexpected error (type=&quot;).append(htmlEscape(model.get(&quot;error&quot;)))
                .append(&quot;, status=&quot;).append(htmlEscape(model.get(&quot;status&quot;))).append(&quot;).&amp;lt;/div&amp;gt;&quot;);

    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2) BasicErrorController&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 부트는 BasicErrorController를 스프링 빈으로 등록한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;BasicErrorController는 &quot;/error &quot; 경로를 받아 오류 페이지를 매핑한다.&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1666589421409&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Controller
@RequestMapping(&quot;${server.error.path:${error.path:/error}}&quot;)
public class BasicErrorController extends AbstractErrorController {
	@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
	public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
		HttpStatus status = getStatus(request);
		Map&amp;lt;String, Object&amp;gt; model = Collections
				.unmodifiableMap(getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
		response.setStatus(status.value());
		ModelAndView modelAndView = resolveErrorView(request, response, status, model);
		return (modelAndView != null) ? modelAndView : new ModelAndView(&quot;error&quot;, model);
	}
}&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;BasicErrorController가&amp;nbsp;제공하는&amp;nbsp;기본&amp;nbsp;정보&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BasicErrorController 컨트롤러는 다음 정보를 model에 담아서 뷰에 전달한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;timestamp, path, status, message, error, exception, errors, trace&lt;/p&gt;
&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;application.properties에 오류 정보를 model 에 포함할지 여부 선택할 수 있다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;server.error.include-exception=false : exception 포함 여부( true , false ) &lt;br /&gt;server.error.include-message=never : message 포함 여부&lt;br /&gt;server.error.include-stacktrace=never : trace 포함 여부(never, always, on_param) &lt;br /&gt;server.error.include-binding-errors=never : errors 포함 여부(never, always, on_param)&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;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3)&amp;nbsp; 뷰 선택 우선순위&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뷰 템플릿이 정적 리소스보다 우선순위가 높고, 404, 500처럼 구체적인 것이 5xx처럼 덜 구체적인 것 보다 우선순위가 높다. 5xx, 4xx 라고 하면 500대, 400대 오류를 처리해준다.&lt;/p&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;resources/templates/error/500.html&lt;/li&gt;
&lt;li&gt;resources/templates/error/5xx.html&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2.&amp;nbsp;정적&amp;nbsp;리소스(&amp;nbsp;static&amp;nbsp;,&amp;nbsp;public&amp;nbsp;)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;resources/static/error/400.html&lt;/li&gt;
&lt;li&gt;resources/static/error/404.html&lt;/li&gt;
&lt;li&gt;resources/static/error/4xx.html&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3.&amp;nbsp;적용&amp;nbsp;대상이&amp;nbsp;없을&amp;nbsp;때&amp;nbsp;뷰&amp;nbsp;이름(&amp;nbsp;error&amp;nbsp;)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;resources/templates/error.html&lt;/li&gt;
&lt;/ul&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;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8. API 예외처리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오류 페이지는 단순히 고객에게 오류 화면을 보여주고 끝이지만, API는 각 오류 상황에 맞는 오류 응답 스펙을 정하고, JSON으로 데이터를 내려주어야 하기 때문에 생각할 내용이 더 많다.&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;1) JSON 응답&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨트롤러에서 예외가 발생하거나 response.sendError()로 응답하면 WAS는 기본 경로인 &quot;/error&quot; 를 요청해 BasicErrorController에서 받아 처리한다. HTML이 아닌 JSON으로 응답하기 위해 WebServerFactoryCustomizer를 구현하고 컨트롤러에서 WAS의 재요청을 받아 JSON으로 응답한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1666669722872&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
public class WebServerCustomizer implements WebServerFactoryCustomizer&amp;lt;ConfigurableWebServerFactory&amp;gt; {        
    @Override
    public void customize(ConfigurableWebServerFactory factory) {
        ErrorPage errorPageEx = new ErrorPage(RuntimeException.class, &quot;/error-page/500&quot;);
        factory.addErrorPages(errorPageEx);
    }
}

// controller
@RequestMapping(value = &quot;/error-page/500&quot;, produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity&amp;lt;Map&amp;lt;String, Object&amp;gt;&amp;gt; errorPage500Api(HttpServletRequest request, HttpServletResponse response) {
    Map&amp;lt;String, Object&amp;gt; result = new HashMap&amp;lt;&amp;gt;();
    Exception ex = (Exception) request.getAttribute(ERROR_EXCEPTION);
    result.put(&quot;status&quot;, request.getAttribute(ERROR_STATUS_CODE));
    result.put(&quot;message&quot;, ex.getMessage());
    Integer statusCode = (Integer) request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
    return new ResponseEntity(result, HttpStatus.valueOf(statusCode));
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;produces = MediaType.APPLICATION_JSON_VALUE
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클라이언트가 요청하는 HTTP Header의 Accept 의 값이 application/json 일 때 해당 메서드가 호출된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;ResponseEntity 를 사용해서 응답하기 때문에 메시지 컨버터가 동작하면서 클라이언트에 JSON이 반환된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2) BasicErrorController&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@RequestMapping의 &lt;span&gt;produces&lt;span&gt; 옵션으로 HTTP Header의 Accept에 따라 호출될 컨트롤러를 구분할 수 있었는데&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BasicErrorController도 &quot;/error&quot;라는 동일한 경로에 HTML을 반환할지 JSON을 반환할지 구분해 두 개의 메소드가 정의되어 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1666670138095&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {}

@RequestMapping
public ResponseEntity&amp;lt;Map&amp;lt;String, Object&amp;gt;&amp;gt; error(HttpServletRequest request) {}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;errorHtml() : produces = MediaType.TEXT_HTML_VALUE : 클라이언트 요청의 Accept 해더 값이 text/html 인 경우에는 errorHtml() 을 호출해서 view를 제공한다.&lt;/li&gt;
&lt;li&gt;error() : 그외 경우에 호출되고 ResponseEntity 로 HTTP Body에 JSON 데이터를 반환한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BasicErrorController 는 HTML 페이지를 제공하는 경우에는 매우 편리하지만,&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 오류 처리는 @ExceptionHandler 를 사용해야한다.&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;3) HandlerExceptionResolver&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨트롤러에서 예외가 발생하거나 request.sendError()를 사용했을 때 컨트롤러 밖으로 던져진 예외를 해결하고, 동작 방식을 변경하고 싶으면 HandlerExceptionResolver를 사용할 수 있다. (ExceptionResolver)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직접 HandlerExceptionResolver를 구현할 수도 있고 스프링 부트가 제공하는 3가지 HandlerExceptionResolver를 사용할 수도 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;672&quot; data-origin-height=&quot;334&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0HktQ/btrPsPpimuM/58kZ0gtA7i0kACRa42QIZ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0HktQ/btrPsPpimuM/58kZ0gtA7i0kACRa42QIZ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0HktQ/btrPsPpimuM/58kZ0gtA7i0kACRa42QIZ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0HktQ%2FbtrPsPpimuM%2F58kZ0gtA7i0kACRa42QIZ0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;672&quot; height=&quot;334&quot; data-origin-width=&quot;672&quot; data-origin-height=&quot;334&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666;&quot;&gt;참고: ExceptionResolver 로 예외를 해결해도 postHandle() 은 호출되지 않는다.&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;(1) ExceptionResolver 활용&lt;/p&gt;
&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;nbsp;상태&amp;nbsp;코드&amp;nbsp;변환&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예외를 response.sendError(xxx) 호출로 변경해서 서블릿에서 상태 코드에 따른 오류를 처리하도록&amp;nbsp;위임&lt;/li&gt;
&lt;li&gt;이후 WAS는 서블릿 오류 페이지를 찾아서 내부 호출, 예를 들어서 스프링 부트가 기본으로 설정한 /error&amp;nbsp;가&amp;nbsp;호출됨&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;WAS 재요청 생략&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;뷰 템플릿 처리나 API 응답 처리처럼 WAS에서 에러 페이지를 재요청할 필요가 없는 경우에&lt;span&gt;&amp;nbsp;&lt;/span&gt;response.sendError()를 호출하지 않으면 불필요한 과정을 생략하고 깔끔하게 응답할 수 있다.&lt;/li&gt;
&lt;li&gt;뷰 템플릿 처리&lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ModelAndView&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;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;API&amp;nbsp;응답&amp;nbsp;처리 &lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;response.getWriter().println(&quot;hello&quot;); 처럼 HTTP 응답 바디에 직접 데이터를 넣어주는 것도&amp;nbsp;가능하다.&amp;nbsp;여기에&amp;nbsp;JSON&amp;nbsp;으로&amp;nbsp;응답하면&amp;nbsp;API&amp;nbsp;응답&amp;nbsp;처리를&amp;nbsp;할&amp;nbsp;수&amp;nbsp;있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;(2) 사용자 정의 ExceptionResolver&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP 요청 해더의 ACCEPT 값이 application/json 이면 JSON으로 오류를 내려주고, 그 외 경우에는 error/500에 있는 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;&lt;b&gt;UserHandlerExceptionResolver&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1666671016981&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Slf4j
public class UserHandlerExceptionResolver implements HandlerExceptionResolver {
    private final ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        try {
            if (ex instanceof UserException) {
                String acceptHeader = request.getHeader(&quot;accept&quot;);
                response.setStatus(HttpServletResponse.SC_BAD_REQUEST);

                if (&quot;application/json&quot;.equals(acceptHeader)) {
                    Map&amp;lt;String, Object&amp;gt; errorResult = new HashMap&amp;lt;&amp;gt;();
                    errorResult.put(&quot;ex&quot;, ex.getClass());
                    errorResult.put(&quot;message&quot;, ex.getMessage());

                    String result = objectMapper.writeValueAsString(errorResult);
                    response.setContentType(&quot;application/json&quot;);
                    response.setCharacterEncoding(&quot;utf-8&quot;);
                    response.getWriter().write(result);
                    
                    return new ModelAndView();
                } else {
                    //TEXT/HTML
                    return new ModelAndView(&quot;error/500&quot;);
                }
            }
        } catch (IOException e) {
            log.error(&quot;resolver ex&quot;, e);
        }
        return null;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;response.sendError()를 호출하지 않았기 때문에 WAS는 내부 요청을 다시 하지 않는다.&lt;/li&gt;
&lt;li&gt;빈 ModelAndView: new ModelAndView() 처럼 빈 ModelAndView 를 반환하면 뷰를 렌더링 하지 않고, 정상 흐름으로 서블릿이 리턴된다. JSON 응답에 사용한다.&lt;/li&gt;
&lt;li&gt;ModelAndView 지정: ModelAndView 에 View , Model 등의 정보를 지정해서 반환하면 뷰를 렌더링 한다.&lt;/li&gt;
&lt;li&gt;null: null 을 반환하면, 다음 ExceptionResolver 를 찾아서 실행한다. 만약 처리할 수 있는 ExceptionResolver 가 없으면 예외 처리가 안되고, 기존에 발생한 예외를 서블릿 밖으로 던진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;WebConfig에 UserHandlerExceptionResolver 추가&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1666671152551&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void extendHandlerExceptionResolvers(List&amp;lt;HandlerExceptionResolver&amp;gt; resolvers) {
        resolvers.add(new UserHandlerExceptionResolver());
    }
}&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;h4 data-ke-size=&quot;size20&quot;&gt;4) 스프링이 제공하는 ExceptionResolver&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 부트가 기본으로 제공하는 ExceptionResolver 는 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. ResponseStatusExceptionResolver : HTTP 응답 코드 변경&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. DefaultHandlerExceptionResolver : 스프링 내부 예외 처리&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. ExceptionHandlerExceptionResolver : @ExceptionHandler 을 처리&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예외 발생시 동작 우선 순위&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;3 &amp;rarr; 1 &amp;rarr; 2&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;(1) ResponseStatusExceptionResolver&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ResponseStatusExceptionResolver 는 예외에 따라서 HTTP 상태 코드를 지정해주는 역할을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;response.sendError(statusCode, resolvedReason) 를 호출해 지정한 코드로 변경한다.&lt;/p&gt;
&lt;pre id=&quot;code_1666673098302&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class ResponseStatusExceptionResolver extends AbstractHandlerExceptionResolver implements MessageSourceAware {
    protected ModelAndView applyStatusAndReason(int statusCode, @Nullable String reason, HttpServletResponse response)
            throws IOException {

        if (!StringUtils.hasLength(reason)) {
            response.sendError(statusCode);
        }
        else {
            String resolvedReason = (this.messageSource != null ?
                    this.messageSource.getMessage(reason, null, reason, LocaleContextHolder.getLocale()) :
                    reason);
            response.sendError(statusCode, resolvedReason);
        }
        return new ModelAndView();
    }
}&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;b&gt;[ @ResponseStatus ]&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1666672980072&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//@ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = &quot;잘못된 요청 오류&quot;)
@ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = &quot;error.bad&quot;)
public class BadRequestException extends RuntimeException {
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BadRequestException 예외가 컨트롤러 밖으로 넘어가면 ResponseStatusExceptionResolver 가 해당 애노테이션을 확인해서 오류 코드를 HttpStatus.BAD_REQUEST (400)으로 변경하고, 메시지도 담는다.&lt;/p&gt;
&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;messages.properties&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1666673274669&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;error.bad=잘못된 요청 오류입니다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;reason 을 MessageSource 에서 찾는 기능도 제공한다.&lt;/p&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;[ ResponseStatusException ]&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@ResponseStatus 는 &lt;b&gt;개발자가 직접 변경할 수 없는 예외에는 적용할 수 없다.&lt;/b&gt; (애노테이션을 직접 넣어야 하는데, 내가 코드를 수정할 수 없는 라이브러리의 예외 코드 같은 곳에는 적용할 수 없다.) 추가로 애노테이션을 사용하기 때문에 조건에 따라 동적으로 변경하는 것도 어렵다. 이때는 ResponseStatusException 예외를 사용하면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1666673365853&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@GetMapping(&quot;/api/response-status-ex2&quot;)
public String responseStatusEx2() {
     throw new ResponseStatusException(HttpStatus.NOT_FOUND, &quot;error.bad&quot;, new IllegalArgumentException());
}&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;(2) &lt;span&gt;DefaultHandlerExceptionResolver&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DefaultHandlerExceptionResolver 는 스프링 내부에서 발생하는 스프링 예외를 해결한다.&lt;/p&gt;
&lt;pre id=&quot;code_1666673592851&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class DefaultHandlerExceptionResolver extends AbstractHandlerExceptionResolver {
    protected ModelAndView doResolveException(
            HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {

        } else if (ex instanceof TypeMismatchException) {
            return handleTypeMismatch(
                    (TypeMismatchException) ex, request, response, handler);
        }
    }

    protected ModelAndView handleTypeMismatch(TypeMismatchException ex,
            HttpServletRequest request, HttpServletResponse response, @Nullable Object handler) throws IOException {

        response.sendError(HttpServletResponse.SC_BAD_REQUEST);
        return new ModelAndView();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;파라미터 바인딩 시점에 타입이 맞지 않으면 내부에서 TypeMismatchException 이 발생하는데, 이 경우 예외가 발생했기 때문에 그냥 두면 서블릿 컨테이너까지 오류가 올라가고, 결과적으로 500 오류가 발생한다.&lt;/li&gt;
&lt;li&gt;그런데 파라미터 바인딩은 대부분 클라이언트가 HTTP 요청 정보를 잘못 호출해서 발생하는 문제이다. HTTP 에서는 이런 경우 HTTP 상태 코드 400을 사용하도록 되어 있다.&lt;/li&gt;
&lt;li&gt;DefaultHandlerExceptionResolver 는 handleTypeMismatch()에서 이것을 500 오류가 아니라 HTTP 상태 코드 400 오류로 변경한다.&lt;/li&gt;
&lt;li&gt;sendError(400) 를 호출했기 때문에 WAS에서 다시 오류 페이지( /error )를 내부 요청한다.&lt;/li&gt;
&lt;li&gt;HTTP Header의 Accept에 따라 BasicErrorController가 응답한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1666674032350&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;status&quot;: 400,
  &quot;error&quot;: &quot;Bad Request&quot;,
  &quot;exception&quot;: &quot;org.springframework.web.method.annotation.MethodArgumentTypeMismatchException&quot;,
  &quot;message&quot;: &quot;Failed to convert value of type 'java.lang.String' to required
type 'java.lang.Integer'; nested exception is java.lang.NumberFormatException:
For input string: \&quot;hello\&quot;&quot;,
  &quot;path&quot;: &quot;/api/default-handler-ex&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;(3) &lt;span&gt;ExceptionHandlerExceptionResolver&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API는 컨트롤러, 예외 등에 따라 응답에 세밀한 제어가 필요한데, @ExceptionHandler를 사용하면 편리하게 예외를 처리할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@ExceptionHandler을 사용하면 ExceptionHandlerExceptionResolver가 동작하고 ExceptionHandlerExceptionResolver는 기본으로 제공하는 ExceptionResolver 중에 우선순위도 가장 높다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[ Controller ]&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1666674611865&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RestController
public class ApiExceptionController {
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(IllegalArgumentException.class)
    public ErrorResult illegalExHandle(IllegalArgumentException e) {
        return new ErrorResult(&quot;BAD&quot;, e.getMessage());
    }

    @ExceptionHandler
    public ResponseEntity&amp;lt;ErrorResult&amp;gt; userExHandle(UserException e) {
        ErrorResult errorResult = new ErrorResult(&quot;USER-EX&quot;, e.getMessage());
        return new ResponseEntity&amp;lt;&amp;gt;(errorResult, HttpStatus.BAD_REQUEST);
    }

    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    @ExceptionHandler
    public ErrorResult exHandle(Exception e) {
        return new ErrorResult(&quot;EX&quot;, &quot;내부 오류&quot;);
    }
}&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;&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;1. 컨트롤러를 호출한 결과 IllegalArgumentException 예외가 컨트롤러 밖으로 던져진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 예외가 발생했으로 ExceptionResolver 가 작동한다. 가장 우선순위가 높은 ExceptionHandlerExceptionResolver 가 실행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. ExceptionHandlerExceptionResolver 는 해당 컨트롤러에 IllegalArgumentException 을 처리할 수 있는 @ExceptionHandler 가 있는지 확인한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. illegalExHandle() 를 실행한다. @RestController 이므로 illegalExHandle() 에도 @ResponseBody 가 적용된다. 따라서 HTTP 컨버터가 사용되고, 응답이 다음과 같은 JSON으로 반환된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5. @ResponseStatus(HttpStatus.BAD_REQUEST) 를 지정했으므로 HTTP 상태 코드 400으로 응답한다.&lt;/p&gt;
&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;다양한 예외를 한번에 처리할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1666674691581&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@ExceptionHandler({AException.class, BException.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;&lt;b&gt;예외 생략&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@ExceptionHandler 에 예외를 생략할 수 있다. 생략하면 메서드 파라미터의 예외가 지정된다.&lt;/p&gt;
&lt;pre id=&quot;code_1666674722458&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@ExceptionHandler
public ResponseEntity&amp;lt;ErrorResult&amp;gt; userExHandle(UserException e) {}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;파리미터와 응답&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@ExceptionHandler 에는 마치 스프링의 컨트롤러의 파라미터 응답처럼 다양한 파라미터와 응답을 지정할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-exceptionhandler-args&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;h4 data-ke-size=&quot;size20&quot;&gt;5) @ControllerAdvice&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@ExceptionHandler 를 사용해서 예외를 깔끔하게 처리할 수 있게 되었지만, 정상 코드와 예외 처리 코드가 하나의 컨트롤러에 섞여 있다. @ControllerAdvice 또는 @RestControllerAdvice 를 사용하면 둘을 분리할 수 있다.&lt;/p&gt;
&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;[ @ControllerAdvice ]&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Controller 코드에 있는 @ExceptionHandler 모두 제거&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1666675402892&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RestControllerAdvice
public class ExControllerAdvice {
    @ExceptionHandler
    public ResponseEntity&amp;lt;ErrorResult&amp;gt; userExHandle(UserException e) {
        ErrorResult errorResult = new ErrorResult(&quot;USER-EX&quot;, e.getMessage());
        return new ResponseEntity&amp;lt;&amp;gt;(errorResult, HttpStatus.BAD_REQUEST);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;대상 컨트롤러 지정 방법&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@ControllerAdvice 에 대상을 지정하지 않으면 모든 컨트롤러에 적용된다. &lt;a href=&quot;https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-controller-advice&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;스프링 공식 문서&lt;/a&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1666675281085&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Target all Controllers annotated with @RestController
@ControllerAdvice(annotations = RestController.class)
public class ExampleAdvice1 {}

// Target all Controllers within specific packages
@ControllerAdvice(&quot;org.example.controllers&quot;)
public class ExampleAdvice2 {}

// Target all Controllers assignable to specific classes
@ControllerAdvice(assignableTypes = {ControllerInterface.class, AbstractController.class})
public class ExampleAdvice3 {}&lt;/code&gt;&lt;/pre&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;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@ExceptionHandler 와 @ControllerAdvice 를 조합하면 예외를 깔끔하게 해결할 수 있다.&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;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;결론&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;자바 예외 활용&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;체크 예외는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #666666;&quot;&gt;예외를 반드시 잡거나 던지도록 처리해야 하는데&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #666666;&quot;&gt;복구 불가능한 예외를 잡아 처리할 수 없고 던지면 다른 계층에서의 의존관계 문제가 발생한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #666666;&quot;&gt;따라서, 체크 예외를 런타임 예외로 변환해 사용하고 런타임 예외가 발생할 수 있는 코드는 문서로 남겨서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #666666;&quot;&gt;중요한 예외를 인지할 수 있게 해야 한다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #666666;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #666666;&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;size18&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #666666;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #666666;&quot;&gt;스프링 예외 추상화&lt;/span&gt;&lt;/span&gt;&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;background-color: #ffffff; color: #666666;&quot;&gt;예외 변환기,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #666666;&quot;&gt;sql-error-codes.xml을 사용해&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&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;size18&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #666666;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #666666;&quot;&gt;서블릿 오류 처리 과정&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #666666;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #666666;&quot;&gt;먼저, 오류 페이지, 오류 페이지 경로 매핑(&lt;span style=&quot;background-color: #ffffff; color: #666666;&quot;&gt;WebServerCustomizer), 오류 페이지 컨트롤러를 정의한다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 예외가 발생해서 WAS까지 전파된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. WAS는 오류 페이지 경로를 찾아서 내부에서 오류 페이지를 호출한다. 이때 오류 페이지 경로로 필터, 서블릿, 인터셉터, 컨트롤러가 모두 다시 호출된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. WAS는 오류 페이지와 함께 오류 정보를 request 의 attribute 에 추가해서 넘겨준다.&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;background-color: #ffffff; color: #666666;&quot;&gt;스프링 부트는 오류 페이지, 오류 페이지 경로 매핑(&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #666666;&quot;&gt;WebServerCustomizer), 오류 페이지 컨트롤러를 정의하는 과정을 자동으로 해준다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;오류 페이지 생성: ErrorMvcAutoConfiguration&lt;/li&gt;
&lt;li&gt;오류 페이지 경로: &quot;/error&quot;&lt;/li&gt;
&lt;li&gt;오류 페이지 컨트롤러: BasicErrorController&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뷰 선택 우선순위를 고려해 커스텀 오류 페이지를 생성할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;API&amp;nbsp;예외처리&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HandlerExceptionResolver를 활용하면 예외 상태 코드를 변환하거나, WAS 재요청을 생략할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. ResponseStatusExceptionResolver : HTTP 응답 코드 변경&lt;br /&gt;2. DefaultHandlerExceptionResolver : 스프링 내부 예외 처리&lt;br /&gt;3. ExceptionHandlerExceptionResolver : @ExceptionHandler 을 처리&lt;br /&gt;예외 발생시 동작 우선 순위: 3&amp;nbsp;&amp;rarr;&amp;nbsp;1&amp;nbsp;&amp;rarr;&amp;nbsp;2&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #666666;&quot;&gt;@ExceptionHandler를 활용해&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #666666;&quot;&gt;예외를 깔끔하&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #666666;&quot;&gt;게 처리할 수 있고&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #666666;&quot;&gt;@ControllerAdvice를 활용해&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #666666;&quot;&gt;정상 코드와 예외 처리 코드를 분리할 수 있다.&lt;/span&gt;&lt;/span&gt;&lt;/span&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;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;a href=&quot;https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-1&quot;&gt;출처&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;a href=&quot;https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-1&quot;&gt;스프링 DB 1편 - 데이터 접근 핵심 원리(김영한)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;a href=&quot;https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-2&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;스프링&amp;nbsp;MVC&amp;nbsp;2편&amp;nbsp;-&amp;nbsp;백엔드&amp;nbsp;웹&amp;nbsp;개발&amp;nbsp;활용&amp;nbsp;기술&lt;/a&gt;&lt;/p&gt;</description>
      <category>Spring</category>
      <category>JdbcTemplate</category>
      <category>스프링 예외 처리</category>
      <category>스프링 예외 추상화</category>
      <author>mandykr</author>
      <guid isPermaLink="true">https://mandykr.tistory.com/90</guid>
      <comments>https://mandykr.tistory.com/90#entry90comment</comments>
      <pubDate>Fri, 21 Oct 2022 16:36:33 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] 트랜잭션</title>
      <link>https://mandykr.tistory.com/89</link>
      <description>&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;2. DB 락&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;4. 스프링 트랜잭션&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;목표&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터베이스의 트랜잭션에 대해 알아보고 스프링이 트랜잭션을 지원하기 위해 제공하는 트랜잭션 매니저와 트랜잭션 동기화 매니저에 대해 알아본다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 트랜잭션&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터를 단순히 파일에 저장하는 것이 아닌 데이터베이스에 저장하는 이유는 &lt;b&gt;데이터베이스가 트랜잭션이라는 개념을 지원&lt;/b&gt;하기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;트랜잭션 ACID&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션은 다음의 ACID를 보장해야 한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;원자성(Atomicity): 트랜잭션 내에서 실행한 작업들은 마치 하나의 작업인 것처럼 &lt;b&gt;모두 성공 하거나 모두 실패해야 한다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;일관성(Consistency): 모든 트랜잭션은 &lt;b&gt;일관성 있는 데이터베이스 상태를 유지해야 한다.&lt;/b&gt; 예를 들어 데이터베이스에서 정한 무결성 제약 조건을 항상 만족해야 한다.&lt;/li&gt;
&lt;li&gt;격리성(Isolation): 동시에 실행되는 트랜잭션들이 서로에게 영향을 미치지 않도록 격리한다. 예를 들어 &lt;b&gt;동시에 같은 데이터를 수정하지 못하도록 해야 한다.&lt;/b&gt; 격리성은 동시성과 관련된 성능 이슈로 인해 트랜잭션 격리 수준 (Isolation level)을 선택할 수 있다.&lt;/li&gt;
&lt;li&gt;지속성(Durability): &lt;b&gt;트랜잭션을 성공적으로 끝내면 그 결과가 항상 기록되어야 한다.&lt;/b&gt; 중간에 시스템에 문제가 발생해도 데이터베이스 로그 등을 사용해서 성공한 트랜잭션 내용을 복구해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) 데이터베이스 트랜잭션&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;(1) 커넥션과 세션&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;763&quot; data-origin-height=&quot;288&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kPSrM/btrO1vDw2Zu/GOI6F3GgtrRVXd0OySnjk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kPSrM/btrO1vDw2Zu/GOI6F3GgtrRVXd0OySnjk0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kPSrM/btrO1vDw2Zu/GOI6F3GgtrRVXd0OySnjk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkPSrM%2FbtrO1vDw2Zu%2FGOI6F3GgtrRVXd0OySnjk0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;252&quot; data-origin-width=&quot;763&quot; data-origin-height=&quot;288&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자는 클라이언트를 사용해서 데이터베이스 서버에 접근한다.&lt;/li&gt;
&lt;li&gt;클라이언트는 데이터베이스 서버에 연결을 요청하고 커넥션을 맺게된다.&lt;/li&gt;
&lt;li&gt;이 때, 데이터베이스 서버는 내부에 세션이라는 것을 만들고 해당 커넥션을 통한 모든 요청은 세션을 통해서 실행하게 된다.&lt;/li&gt;
&lt;li&gt;세션은 트랜잭션을 시작하고, 커밋 또는 롤백을 통해 트랜잭션을 종료한다.&lt;/li&gt;
&lt;li&gt;그리고 이후에 새로운 트랜잭션을 다시 시작할 수 있다.&lt;/li&gt;
&lt;li&gt;사용자가 커넥션을 닫거나, 또는 DBA(DB 관리자)가 세션을 강제로 종료하면 세션은 종료된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;(2) 수동 커밋&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;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_1666151360058&quot; class=&quot;java&quot; data-ke-language=&quot;java&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;b&gt;수동 커밋 모드로 설정하는 것을 트랜잭션을 시작한다고 표현할 수 있다.&lt;/b&gt; 수동 커밋 설정을 하면 이후에 꼭 commit , rollback 을 호출해야 한다.&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;(3) 트랜잭션&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;memberA 의 돈을 memberB 에게 2000원 계좌이체하는 트랜잭션을 실행한다.&lt;/p&gt;
&lt;pre id=&quot;code_1666152054494&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;set autocommit false;
insert into member(member_id, money) values ('memberA',10000);
insert into member(member_id, money) values ('memberB',10000);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;데이터 수정&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;716&quot; data-origin-height=&quot;292&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Xxs5P/btrOQpbLwy7/qgbM9OV5CFsahpHP03Syd0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Xxs5P/btrOQpbLwy7/qgbM9OV5CFsahpHP03Syd0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Xxs5P/btrOQpbLwy7/qgbM9OV5CFsahpHP03Syd0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXxs5P%2FbtrOQpbLwy7%2FqgbM9OV5CFsahpHP03Syd0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;292&quot; data-origin-width=&quot;716&quot; data-origin-height=&quot;292&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;세션1에서 데이터를 변경하면 세션1에서는 임시 상태로 데이터가 변경된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;세션1이 커밋하기 전에는 변경된 데이터를 세션1에서는 조회할 수 있지만 세션2에서는 기존 데이터가 조회된다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;커밋 - commit&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;716&quot; data-origin-height=&quot;283&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/diwRgk/btrOCya0u5h/tb0R3BhQNvWSA8kJ8F6OWK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/diwRgk/btrOCya0u5h/tb0R3BhQNvWSA8kJ8F6OWK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/diwRgk/btrOCya0u5h/tb0R3BhQNvWSA8kJ8F6OWK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdiwRgk%2FbtrOCya0u5h%2Ftb0R3BhQNvWSA8kJ8F6OWK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;283&quot; data-origin-width=&quot;716&quot; data-origin-height=&quot;283&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;세션1이 트랜잭션을 커밋했기 때문에 데이터베이스에 데이터 변경이 반영된다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;커밋 이후에는 모든 세션에서 변경된 데이터를 조회할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;롤백 - rollback&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;717&quot; data-origin-height=&quot;293&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7grfA/btrOZIjS4NW/UB8oLpskmZ4ddbewSUd4r1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7grfA/btrOZIjS4NW/UB8oLpskmZ4ddbewSUd4r1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7grfA/btrOZIjS4NW/UB8oLpskmZ4ddbewSUd4r1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7grfA%2FbtrOZIjS4NW%2FUB8oLpskmZ4ddbewSUd4r1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;717&quot; height=&quot;293&quot; data-origin-width=&quot;717&quot; data-origin-height=&quot;293&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;계좌이체를 실행하는 도중에 두 번째 update 쿼리에서 SQL 오류가 발생한다.&lt;/li&gt;
&lt;li&gt;그래서 memberA 의 돈을 2000원 줄이는 것에는 성공했지만, memberB 의 돈을 2000원 증가시키는 것에 실패한다.&lt;/li&gt;
&lt;li&gt;이럴 때는 &lt;b&gt;롤백을 호출해서 트랜잭션을 시작하기 전 단계로 데이터를 복구해야 한다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;716&quot; data-origin-height=&quot;321&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bxweRO/btrO2p4f6Jb/RLj8XMHzsr9oyw9XSWvyM1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bxweRO/btrO2p4f6Jb/RLj8XMHzsr9oyw9XSWvyM1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bxweRO/btrO2p4f6Jb/RLj8XMHzsr9oyw9XSWvyM1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbxweRO%2FbtrO2p4f6Jb%2FRLj8XMHzsr9oyw9XSWvyM1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;321&quot; data-origin-width=&quot;716&quot; data-origin-height=&quot;321&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;세션1이 트랜잭션을 롤백했기 때문에 임시 상태의 데이터가 DB에 반영되지 않고 트랜잭션을 시작하기 전 상태로 돌아왔다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. DB 락&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세션1이 트랜잭션을 시작하고 데이터를 수정하는 동안 아직 커밋을 수행하지 않았는데, 세션2에서 동시에 같은 데이터를 수정하게 되면 여러가지 문제가 발생한다. 바로 트랜잭션의 &lt;b&gt;원자성이 깨지는 것이다. &lt;/b&gt;&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;&lt;b&gt;데이터베이스는 락을 통해 원자성을 보장한다.&lt;/b&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) DB 락 - 변경&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;673&quot; data-origin-height=&quot;177&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oQg7D/btrO0HkhjuF/KSkEXufiZH4SodFcI2sVBk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oQg7D/btrO0HkhjuF/KSkEXufiZH4SodFcI2sVBk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oQg7D/btrO0HkhjuF/KSkEXufiZH4SodFcI2sVBk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoQg7D%2FbtrO0HkhjuF%2FKSkEXufiZH4SodFcI2sVBk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;177&quot; data-origin-width=&quot;673&quot; data-origin-height=&quot;177&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;1. 세션1은 트랜잭션을 시작한다.&lt;/li&gt;
&lt;li&gt;2. 세션1은 memberA 의 money 를 500으로 변경을 시도한다. &lt;b&gt;이때 해당 로우의 락을 먼저 획득해야 한다.&lt;/b&gt; 락이 남아 있으므로 세션1은 락을 획득한다. (세션1이 세션2보다 조금 더 빨리 요청했다.)&lt;/li&gt;
&lt;li&gt;3. 세션1은 락을 획득했으므로 해당 로우에 update sql을 수행한다.&lt;/li&gt;
&lt;li&gt;4. 세션2는 트랜잭션을 시작한다.&lt;/li&gt;
&lt;li&gt;5. 세션2도 memberA 의 money 데이터를 변경하려고 시도한다. 이때 해당 로우의 락을 먼저 획득해야 한다. 락이 없으므로 락이 돌아올 때 까지 대기한다. 참고로 세션2가 락을 무한정 대기하는 것은 아니다. &lt;b&gt;락 대기 시간을 넘어가면 락 타임아웃 오류가 발생한다.&lt;/b&gt; 락 대기 시간은 설정할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1666153142751&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SET LOCK_TIMEOUT 60000; // 락 획득 시간을 60초로 설정한다.&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1666153251223&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 락 대기 시간동안 락을 얻지 못하면 타임아웃 오류가 발생한다.
Timeout trying to lock table {0}; SQL statement:
update member set money=10000 - 2000 where member_id = 'memberA' [50200-200]
HYT00/50200&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;6. 세션1은 커밋을 수행한다. &lt;b&gt;커밋으로 트랜잭션이 종료되었으므로 락도 반납한다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;673&quot; data-origin-height=&quot;140&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bcjoBa/btrOFlVmcLK/STUJPVD7JF0T8dEZpJVHXk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bcjoBa/btrOFlVmcLK/STUJPVD7JF0T8dEZpJVHXk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bcjoBa/btrOFlVmcLK/STUJPVD7JF0T8dEZpJVHXk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbcjoBa%2FbtrOFlVmcLK%2FSTUJPVD7JF0T8dEZpJVHXk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;140&quot; data-origin-width=&quot;673&quot; data-origin-height=&quot;140&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;락을 획득하기 위해 대기하던 세션2가 락을 획득한다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;673&quot; data-origin-height=&quot;162&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sHHnP/btrOQnrxv0o/L1KvQ7y5czID3Nr9VoKpm1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sHHnP/btrOQnrxv0o/L1KvQ7y5czID3Nr9VoKpm1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sHHnP/btrOQnrxv0o/L1KvQ7y5czID3Nr9VoKpm1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsHHnP%2FbtrOQnrxv0o%2FL1KvQ7y5czID3Nr9VoKpm1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;162&quot; data-origin-width=&quot;673&quot; data-origin-height=&quot;162&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;7. 세션2는 update sql을 수행한다.&lt;/li&gt;
&lt;li&gt;8. 세션2는 커밋을 수행하고 트랜잭션이 종료되었으므로 락을 반납한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2) DB 락 - 조회&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;예를 들어서 세션1이 락을 획득하고 데이터를 변경하고 있어도, 세션2에서 데이터를 조회는 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터를 조회할 때도 락을 획득하고 싶을 때가 있다. 이럴 때는 &lt;b&gt;select for update&lt;/b&gt; 구문을 사용하면 된다.&lt;/li&gt;
&lt;li&gt;이렇게 하면 세션1이 조회 시점에 락을 가져가버리기 때문에 다른 세션에서 해당 데이터를 변경할 수 없다.&lt;/li&gt;
&lt;li&gt;물론 이 경우도 트랜잭션을 커밋하면 락을 반납한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1666153600530&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;set autocommit false;
select * from member where member_id='memberA' 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-origin-width=&quot;673&quot; data-origin-height=&quot;149&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/WlFxg/btrOQD81dfU/V10axkQqVyq2VZfCr0Vdk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/WlFxg/btrOQD81dfU/V10axkQqVyq2VZfCr0Vdk1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/WlFxg/btrOQD81dfU/V10axkQqVyq2VZfCr0Vdk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWlFxg%2FbtrOQD81dfU%2FV10axkQqVyq2VZfCr0Vdk1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;149&quot; data-origin-width=&quot;673&quot; data-origin-height=&quot;149&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;예를 들어서 애플리케이션 로직에서 memberA 의 금액을 조회한 다음에 이 금액 정보로 애플리케이션에서 어떤 계산을 수행한다. 그런데 매우 중요한 계산이어서 &lt;b&gt;계산을 완료할 때 까지&lt;/b&gt; &lt;b&gt;memberA 의 금액을 다른곳에서 변경하면 안된다. 이럴 때 조회 시점에 락을 획득하면 된다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 애플리케이션 트랜잭션 적용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 애플리케이션에서 DB 트랜잭션을 사용해서 계좌이체 같이 중요한 비즈니스 로직에서&amp;nbsp;&lt;span&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span&gt;1) 비즈니스 로직과 트랜잭션&lt;/span&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;674&quot; data-origin-height=&quot;252&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ErvHn/btrOQnZrbUh/z7cY0m1yHDq2xuebIbnnok/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ErvHn/btrOQnZrbUh/z7cY0m1yHDq2xuebIbnnok/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ErvHn/btrOQnZrbUh/z7cY0m1yHDq2xuebIbnnok/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FErvHn%2FbtrOQnZrbUh%2Fz7cY0m1yHDq2xuebIbnnok%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;252&quot; data-origin-width=&quot;674&quot; data-origin-height=&quot;252&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;그런데 트랜잭션을 시작하려면 커넥션이 필요하다. 결국 서비스 계층에서 커넥션을 만들고, 트랜잭션 커밋 이후에 커넥션을 종료해야 한다.&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;2) 커넥션과 세션&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;763&quot; data-origin-height=&quot;288&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kPSrM/btrO1vDw2Zu/GOI6F3GgtrRVXd0OySnjk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kPSrM/btrO1vDw2Zu/GOI6F3GgtrRVXd0OySnjk0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kPSrM/btrO1vDw2Zu/GOI6F3GgtrRVXd0OySnjk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkPSrM%2FbtrO1vDw2Zu%2FGOI6F3GgtrRVXd0OySnjk0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;252&quot; data-origin-width=&quot;763&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;DB 트랜잭션을 사용하려면 같은 세션을 사용해야 하기 때문에 &lt;b&gt;애플리케이션에서는 같은 커넥션을 유지해야 한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3) 적용&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링에서 제공하는 트랜잭션 기술을 사용하지 않고 트랜잭션을 구현해 발생하는 문제점을 확인한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 접근 기술은 JDBC를 활용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JDBC를 사용할 때 발생하는 SQLException은 RuntimeException으로 변환해 처리한다.&lt;/p&gt;
&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_1666154691207&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class MemberRepository {
    private final DataSource dataSource;
    
    // ... 생략

    public Member findById(Connection con, String memberId) {
        String sql = &quot;select * from member where member_id = ?&quot;;
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        try {
            pstmt = con.prepareStatement(sql);
            pstmt.setString(1, memberId);
            rs = pstmt.executeQuery();
            if (rs.next()) {
                Member member = new Member();
                member.setMemberId(rs.getString(&quot;member_id&quot;));
                member.setMoney(rs.getInt(&quot;money&quot;));
                return member;
            } else {
                throw new NoSuchElementException(&quot;member not found memberId=&quot; + memberId);
            }
        } catch (SQLException e) {
            log.error(&quot;db error&quot;, e);
            throw new RuntimeException(e);
        } finally {
            //connection은 여기서 닫지 않는다.
            JdbcUtils.closeResultSet(rs);
            JdbcUtils.closeStatement(pstmt);
        }
    }

    public void update(Connection con, String memberId, int money) {
        String sql = &quot;update member set money=? where member_id=?&quot;;
        PreparedStatement pstmt = null;
        try {
            pstmt = con.prepareStatement(sql);
            pstmt.setInt(1, money);
            pstmt.setString(2, memberId);
            pstmt.executeUpdate();
        } catch (SQLException e) {
            log.error(&quot;db error&quot;, e);
            throw new RuntimeException(e);
        } finally {
            //connection은 여기서 닫지 않는다.
            JdbcUtils.closeStatement(pstmt);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;계좌이체 트랜잭션에서 호출하는 MemberRepository 의 findyById(), update() 메소드는 커넥션을 유지하기 위해 Connection 을 파리미터로 받는다.&lt;/li&gt;
&lt;li&gt;커넥션 유지가 필요하기 때문에 리포지토리에서 커넥션을 닫지 않고 이후 서비스 로직이 끝날 때 트랜잭션을 종료하고 닫아야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[ 서비스 ]&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1666155045975&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class MemberService {
    private final DataSource dataSource;
    private final MemberRepository memberRepository;

    public void accountTransfer(String fromId, String toId, int money) {
        Connection con = dataSource.getConnection();
        try {
            con.setAutoCommit(false); //트랜잭션 시작
            //비즈니스 로직
            bizLogic(con, fromId, toId, money);
            con.commit(); //성공시 커밋
        } catch (Exception e) {
            con.rollback(); //실패시 롤백
            throw new IllegalStateException(e);
        } finally {
            release(con);
        }
    }

    private void bizLogic(Connection con, String fromId, String toId, int money) {
        Member fromMember = memberRepository.findById(con, fromId);
        Member toMember = memberRepository.findById(con, toId);
        memberRepository.update(con, fromId, fromMember.getMoney() - money);
        validation(toMember);
        memberRepository.update(con, toId, toMember.getMoney() + money);
    }
    
    private void release(Connection con) {
        if (con != null) {
            try {
                con.setAutoCommit(true); //커넥션 풀 고려
                con.close();
            } catch (Exception e) {
                log.info(&quot;error&quot;, e);
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Connection con = dataSource.getConnection();
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;트랜잭션을 시작하려면 커넥션이 필요하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;con.setAutoCommit(false); //트랜잭션 시작
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;트랜잭션을 시작하려면 자동 커밋 모드를 꺼야한다. 이렇게 하면 커넥션을 통해 세션에 set autocommit false 가 전달되고, 이후부터는 수동 커밋 모드로 동작한다. 이렇게 자동 커밋 모드를 수동 커밋 모드로 변경하는 것을 트랜잭션을 시작한다고 보통 표현한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;bizLogic(con, fromId, toId, money);
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;트랜잭션이 시작된 커넥션을 전달하면서 비즈니스 로직을 수행한다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;이렇게 분리한 이유는 트랜잭션을 관리하는 로직과 실제 비즈니스 로직을 구분하기 위함이다.&lt;/li&gt;
&lt;li&gt;memberRepository.update(con..) : 비즈니스 로직을 보면 리포지토리를 호출할 때 커넥션을 전달하는 것을 확인할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;con.commit(); //성공시 커밋
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;비즈니스 로직이 정상 수행되면 트랜잭션을 커밋한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;con.rollback(); //실패시 롤백
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;catch(Ex){..} 를 사용해서 비즈니스 로직 수행 도중에 예외가 발생하면 트랜잭션을 롤백한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;release(con);
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;finally {..} 를 사용해서 커넥션을 모두 사용하고 나면 안전하게 종료한다. 그런데 커넥션 풀을 사용하면 con.close() 를 호출 했을 때 커넥션이 종료되는 것이 아니라 풀에 반납된다. 현재 수동 커밋 모드로 동작하기 때문에 풀에 돌려주기 전에 기본 값인 자동 커밋 모드로 변경하는 것이 안전하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4) 문제점&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;(1) 서비스 계층이 특정 기술에 종속되는 문제&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;구현 기술을 변경해도 비즈니스 로직을 갖는 서비스 계층 코드는 최대한 유지할 수 있어야 한다.&lt;/li&gt;
&lt;li&gt;하지만 &lt;b&gt;트랜잭션을 적용하기 위해 JDBC 구현 기술이 서비스 계층에 누수되었다.&lt;/b&gt;&lt;br /&gt;dataSource.getConnection();&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;(2) 트랜잭션 동기화 문제&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;같은 트랜잭션을 유지하기 위해 커넥션을 파라미터로 넘겨야 한다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;또한 커넥션을 유지하는 기능과 아닌 기능을 별도로 구현해야 한다.&lt;br /&gt;memberRepository.findById(con,&amp;nbsp;fromId); &lt;br /&gt;memberRepository.findById(fromId);&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;(3) 트랜잭션 적용 반복 문제&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;트랜잭션 적용 코드를 보면 반복이 많다. try , catch , finally ...&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 스프링 트랜잭션&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링은 위의 문제들을 다음과 같은 방법으로 해결한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;트랜잭션 동기화&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;트랜잭션 추상화
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;PlatformTransactionManager 인터페이스로 트랜잭션 추상화&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;서비스 계층이 특정 기술에 종속되는 문제 해결&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;리소스 동기화
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;TransactionSynchronizationManager 클래스의 쓰레드 로컬 사용&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;트랜잭션 동기화 문제 해결&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;트랜잭션 템플릿&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;TransactionTemplate 클래스로 템플릿 콜백 패턴 적용&lt;/li&gt;
&lt;li&gt;트랜잭션 적용 반복 문제 해결&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;트랜잭션 AOP&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;@Transactional을 사용해 트랜잭션 프록시 적용&lt;/li&gt;
&lt;li&gt;&lt;b&gt;PlatformTransactionManager, TransactionSynchronizationManager 를 사용해 트랜잭션 동기화&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;트랜잭션 적용 반복 문제 해결&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;서비스 계층에 트랜잭션 처리 로직이 함께 섞여있는 문제 해결&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) 트랜잭션 동기화&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;(1) 트랜잭션 추상화&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링은 트랜잭션 기술들을 &lt;b&gt;PlatformTransactionManager(트랜잭션 매니저)로 추상화해 &lt;span style=&quot;letter-spacing: 0px;&quot;&gt;서비스 계층이 특정 기술에 종속되는 문제를 해결한다.&lt;/span&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;673&quot; data-origin-height=&quot;278&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cf0Zv4/btrO0ZsgF4n/4aUVt08G3BDdm93GULUTc1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cf0Zv4/btrO0ZsgF4n/4aUVt08G3BDdm93GULUTc1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cf0Zv4/btrO0ZsgF4n/4aUVt08G3BDdm93GULUTc1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcf0Zv4%2FbtrO0ZsgF4n%2F4aUVt08G3BDdm93GULUTc1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;278&quot; data-origin-width=&quot;673&quot; data-origin-height=&quot;278&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스가 특정 트랜잭션 기술이 아닌 PlatformTransactionManager에 의존하고 &lt;b&gt;환경에 따라 원하는 구현체를 DI를 통해 주입하면 된다.&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JDBC 사용: DataSourceTransactionManager 를 확장한 JdbcTransactionManager&lt;/li&gt;
&lt;li&gt;JPA: JpaTransactionManager&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;PlatformTransactionManager&lt;/p&gt;
&lt;pre id=&quot;code_1666160945169&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package org.springframework.transaction;

public interface PlatformTransactionManager extends TransactionManager {
    TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException;
    void commit(TransactionStatus status) throws TransactionException;
    void rollback(TransactionStatus status) throws TransactionException;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;(2) 리소스 동기화&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PlatformTransactionManager는 트랜잭션 동기화와 함께 리소스 동기화 기능을 제공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;PlatformTransactionManager는 TransactionSynchronizationManager(트랜잭션 동기화 매니저)를 사용해 데이터베이스 커넥션을 유지하도록 한다.&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;672&quot; data-origin-height=&quot;406&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/I99aG/btrOQlOshNY/HThCtPXsfNw4LAml5tsAXK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/I99aG/btrOQlOshNY/HThCtPXsfNw4LAml5tsAXK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/I99aG/btrOQlOshNY/HThCtPXsfNw4LAml5tsAXK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FI99aG%2FbtrOQlOshNY%2FHThCtPXsfNw4LAml5tsAXK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;672&quot; height=&quot;406&quot; data-origin-width=&quot;672&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;TransactionSynchronizationManager는 ThreadLocal을 사용해서 커넥션을 동기화하기 때문에 멀티쓰레드 상황에 안전하게 커넥션을 동기화 할 수 있다.&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;size18&quot;&gt;TransactionSynchronizationManager&lt;/p&gt;
&lt;pre id=&quot;code_1666161319198&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package org.springframework.transaction.support;

public abstract class TransactionSynchronizationManager {

	private static final ThreadLocal&amp;lt;Map&amp;lt;Object, Object&amp;gt;&amp;gt; resources =
			new NamedThreadLocal&amp;lt;&amp;gt;(&quot;Transactional resources&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;(3) 트랜잭션 매니저 동작 방식&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-origin-width=&quot;673&quot; data-origin-height=&quot;407&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cYAJL5/btrOQl13MIN/qPwlSdJ8dhKY5jioNA85k1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cYAJL5/btrOQl13MIN/qPwlSdJ8dhKY5jioNA85k1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cYAJL5/btrOQl13MIN/qPwlSdJ8dhKY5jioNA85k1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcYAJL5%2FbtrOQl13MIN%2FqPwlSdJ8dhKY5jioNA85k1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;407&quot; data-origin-width=&quot;673&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;1.&amp;nbsp;서비스&amp;nbsp;계층에서&amp;nbsp;&lt;b&gt;transactionManager.getTransaction()&lt;/b&gt;&amp;nbsp;을&amp;nbsp;호출해서&amp;nbsp;트랜잭션을&amp;nbsp;시작한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 트랜잭션을 시작하려면 먼저 데이터베이스 커넥션이 필요하다. 트랜잭션 매니저는 내부에서 데이터소스를 사용해서&amp;nbsp;커넥션을&amp;nbsp;생성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3.&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;4. &lt;b&gt;커넥션을 트랜잭션 동기화 매니저에 보관한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5. 트랜잭션 동기화 매니저는 &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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;673&quot; data-origin-height=&quot;415&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ci38oj/btrOD4AmBcP/ZcvQRgvbtE9n5q8dWFnfj0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ci38oj/btrOD4AmBcP/ZcvQRgvbtE9n5q8dWFnfj0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ci38oj/btrOD4AmBcP/ZcvQRgvbtE9n5q8dWFnfj0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fci38oj%2FbtrOD4AmBcP%2FZcvQRgvbtE9n5q8dWFnfj0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;415&quot; data-origin-width=&quot;673&quot; data-origin-height=&quot;415&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;6. 서비스는 비즈니스 로직을 실행하면서 리포지토리의 메서드들을 호출한다. 이때 커넥션을 파라미터로 전달하지&amp;nbsp;않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;7. 리포지토리 메서드들은 트랜잭션이 시작된 커넥션이 필요하다. 리포지토리는 &lt;b&gt;DataSourceUtils.getConnection()&lt;/b&gt; 을 사용해서 트랜잭션 동기화 매니저에 보관된 커넥션을 꺼내서 사용한다. 이 과정을 통해서 자연스럽게 같은 커넥션을 사용하고, 트랜잭션도 유지된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;8. 획득한 커넥션을 사용해서 SQL을 데이터베이스에 전달해서 실행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;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;670&quot; data-origin-height=&quot;418&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/boie6A/btrO0ZTuwty/CqIL5GfSXvLfJAkh1wmg1K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/boie6A/btrO0ZTuwty/CqIL5GfSXvLfJAkh1wmg1K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/boie6A/btrO0ZTuwty/CqIL5GfSXvLfJAkh1wmg1K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fboie6A%2FbtrO0ZTuwty%2FCqIL5GfSXvLfJAkh1wmg1K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;418&quot; data-origin-width=&quot;670&quot; data-origin-height=&quot;418&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;9.&amp;nbsp;비즈니스&amp;nbsp;로직이&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;10. 트랜잭션을 종료하려면 동기화된 커넥션이 필요하다. 트랜잭션 동기화 매니저를 통해 동기화된 커넥션을 획득한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;11.&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;12.&amp;nbsp;전체&amp;nbsp;리소스를&amp;nbsp;정리한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;트랜잭션&amp;nbsp;동기화&amp;nbsp;매니저를&amp;nbsp;정리한다.&amp;nbsp;쓰레드&amp;nbsp;로컬은&amp;nbsp;사용후&amp;nbsp;꼭&amp;nbsp;정리해야&amp;nbsp;한다.&lt;/li&gt;
&lt;li&gt;con.setAutoCommit(true)&amp;nbsp;로&amp;nbsp;되돌린다.&amp;nbsp;커넥션&amp;nbsp;풀을&amp;nbsp;고려해야&amp;nbsp;한다.&lt;/li&gt;
&lt;li&gt;con.close() 를 호출해셔 커넥션을 종료한다. 커넥션 풀을 사용하는 경우 con.close() 를 호출하면&amp;nbsp;커넥션&amp;nbsp;풀에&amp;nbsp;반환된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;(4) 트랜잭션 동기화 적용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PlatformTransactionManager, &lt;span&gt;TransactionSynchronizationManager로&lt;span&gt;&amp;nbsp;&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_1666162181097&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class MemberService {
    private final PlatformTransactionManager transactionManager;
    private final MemberRepository memberRepository;

    public void accountTransfer(String fromId, String toId, int money) {
        //트랜잭션 시작
        TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());        
        try {
            //비즈니스 로직
            bizLogic(fromId, toId, money);
            transactionManager.commit(status); //성공시 커밋
        } catch (Exception e) {
            transactionManager.rollback(status); //실패시 롤백
            throw new IllegalStateException(e);
        }
    }    
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;TransactionManager transactionManager &lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;트랜잭션 매니저를 주입 받는다. 지금은 JDBC 기술을 사용하기 때문에 DataSourceTransactionManager 구현체를 주입 받아야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;transactionManager.getTransaction()&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;트랜잭션을 시작한다. 기존에 이미 진행중인 트랜잭션이 있는 경우 해당 트랜잭션에 참여한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[ 테스트 ]&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1666230149536&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// memberService 생성
DriverManagerDataSource dataSource = new DriverManagerDataSource(URL, USERNAME, PASSWORD);
PlatformTransactionManager transactionManager = new DataSourceTransactionManager(dataSource);
MemberService memberService = new MemberService(transactionManager, memberRepository);

// ... 테스트&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;new&amp;nbsp;DataSourceTransactionManager(dataSource)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;dataSource로 트랜잭션 매니저를 생성한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[ 리포지토리 ]&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1666162371639&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class MemberRepository {
    private final DataSource dataSource;

    public Member findById(String memberId) {
        String sql = &quot;select * from member where member_id = ?&quot;;
        Connection con = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        try {
            con = getConnection();
            pstmt = con.prepareStatement(sql);
            pstmt.setString(1, memberId);
            rs = pstmt.executeQuery();
            if (rs.next()) {
                Member member = new Member();
                member.setMemberId(rs.getString(&quot;member_id&quot;));
                member.setMoney(rs.getInt(&quot;money&quot;));
                return member;
            } else {
                throw new NoSuchElementException(&quot;member not found memberId=&quot; + memberId);
            }
        } catch (SQLException e) {
            log.error(&quot;db error&quot;, e);
            throw new RuntimeException(e);
        } finally {
            close(con, pstmt, rs);
        }
    }

    private Connection getConnection() {
        //주의! 트랜잭션 동기화를 사용하려면 DataSourceUtils를 사용해야 한다.
        Connection con = DataSourceUtils.getConnection(dataSource);
        log.info(&quot;get connection={} class={}&quot;, con, con.getClass());
        return con;
    }
    
    private void close(Connection con, Statement stmt, ResultSet rs) {
        JdbcUtils.closeResultSet(rs);
        JdbcUtils.closeStatement(stmt);
        //주의! 트랜잭션 동기화를 사용하려면 DataSourceUtils를 사용해야 한다.
        DataSourceUtils.releaseConnection(con, dataSource);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;DataSourceUtils.getConnection()&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;트랜잭션&amp;nbsp;동기화&amp;nbsp;매니저가&amp;nbsp;관리하는&amp;nbsp;커넥션이&amp;nbsp;있으면&amp;nbsp;해당&amp;nbsp;커넥션을&amp;nbsp;반환한다.&lt;/li&gt;
&lt;li&gt;트랜잭션&amp;nbsp;동기화&amp;nbsp;매니저가&amp;nbsp;관리하는&amp;nbsp;커넥션이&amp;nbsp;없는&amp;nbsp;경우&amp;nbsp;새로운&amp;nbsp;커넥션을&amp;nbsp;생성해서&amp;nbsp;반환한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;DataSourceUtils.releaseConnection()&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: none;&quot;&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;트랜잭션을&amp;nbsp;사용하기&amp;nbsp;위해&amp;nbsp;동기화된&amp;nbsp;커넥션은&amp;nbsp;커넥션을&amp;nbsp;닫지&amp;nbsp;않고&amp;nbsp;그대로&amp;nbsp;유지해준다.&lt;/li&gt;
&lt;li&gt;트랜잭션&amp;nbsp;동기화&amp;nbsp;매니저가&amp;nbsp;관리하는&amp;nbsp;커넥션이&amp;nbsp;없는&amp;nbsp;경우&amp;nbsp;해당&amp;nbsp;커넥션을&amp;nbsp;닫는다.&lt;/li&gt;
&lt;li&gt;close() 에서 커넥션을 con.close() 를 사용해서 직접 닫아버리면 커넥션이 유지되지 않는 문제가 발생한다. 이 커넥션은&amp;nbsp;이후&amp;nbsp;로직은&amp;nbsp;물론이고,&amp;nbsp;트랜잭션을&amp;nbsp;종료(커밋,&amp;nbsp;롤백)할&amp;nbsp;때&amp;nbsp;까지&amp;nbsp;살아있어야&amp;nbsp;한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3) 트랜잭션 템플릿&lt;span style=&quot;color: #000000; font-size: 1.25em; letter-spacing: -1px;&quot;&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TransactionTemplate을 사용하여 &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;b&gt;커밋&lt;/b&gt;하고, 예외가 발생해서 실패하면 &lt;b&gt;롤백&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1666229285844&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//트랜잭션 시작
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
    //비즈니스 로직
    bizLogic(fromId, toId, money);
    transactionManager.commit(status); //성공시 커밋
} catch (Exception e) {
    transactionManager.rollback(status); //실패시 롤백
    throw new IllegalStateException(e);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;(1) TransactionTemplate&lt;/p&gt;
&lt;pre id=&quot;code_1666229497775&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class TransactionTemplate {
    private PlatformTransactionManager transactionManager;
    public &amp;lt;T&amp;gt; T execute(TransactionCallback&amp;lt;T&amp;gt; action){..}
    void executeWithoutResult(Consumer&amp;lt;TransactionStatus&amp;gt; action){..}
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;execute()&amp;nbsp;:&amp;nbsp;응답&amp;nbsp;값이&amp;nbsp;있을&amp;nbsp;때&amp;nbsp;사용한다.&lt;/li&gt;
&lt;li&gt;executeWithoutResult()&amp;nbsp;:&amp;nbsp;응답&amp;nbsp;값이&amp;nbsp;없을&amp;nbsp;때&amp;nbsp;사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;(2) 적용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TransactionTemplate과 템플릿 콜백 패턴을 적용해 트랜잭션 적용 반복 코드를 제거한다.&lt;/p&gt;
&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_1666229651044&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class MemberService {
    private final TransactionTemplate txTemplate;
    private final MemberRepository memberRepository;

    public MemberService(PlatformTransactionManager transactionManager, MemberRepositoryV3 memberRepository) {
        this.txTemplate = new TransactionTemplate(transactionManager);
        this.memberRepository = memberRepository;
    }

    public void accountTransfer(String fromId, String toId, int money) {
        txTemplate.executeWithoutResult(status -&amp;gt; bizLogic(fromId, toId, money));
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;TransactionTemplate&amp;nbsp;을&amp;nbsp;사용하려면&amp;nbsp;transactionManager&amp;nbsp;가&amp;nbsp;필요하다.&amp;nbsp;&lt;br /&gt;생성자에서 transactionManager&amp;nbsp;를&amp;nbsp;주입&amp;nbsp;받으면서&amp;nbsp;TransactionTemplate&amp;nbsp;을&amp;nbsp;생성했다.&lt;/li&gt;
&lt;li&gt;트랜잭션 템플릿의 기본 동작
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;비즈니스 로직이 정상 수행되면 커밋한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;언체크 예외가 발생하면 롤백한다.&lt;/b&gt; 그 외의 경우 커밋한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&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;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;b&gt;서비스 계층에 트랜잭션 처리 기능을 없애고 순수한 비즈니스 로직만 남도록 구성해야 한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 &lt;span style=&quot;background-color: #ffffff; color: #666666;&quot;&gt;JDBC의 반복 문제는 &lt;a href=&quot;https://mandykr.tistory.com/90#:~:text=%EC%A0%91%EA%B7%BC%20%EC%98%88%EC%99%B8%EB%A1%9C%20%EB%B3%80%ED%99%98%ED%95%B4%EC%A4%80%EB%8B%A4.-,5.%C2%A0JDBC%20%EB%B0%98%EB%B3%B5%20%EB%AC%B8%EC%A0%9C%20%ED%95%B4%EA%B2%B0%20%2D%20JdbcTemplate,-%EC%8A%A4%ED%94%84%EB%A7%81%EC%9D%B4%20%EC%98%88%EC%99%B8%EB%A5%BC%20%EC%B6%94%EC%83%81%ED%99%94%ED%95%B4%EC%A4%80&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;JdbcTemplat&lt;/a&gt;을 사용해&lt;span style=&quot;background-color: #ffffff; color: #666666;&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;h4 data-ke-size=&quot;size20&quot;&gt;4) 트랜잭션 AOP&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Transactional을 사용해 스프링 AOP와 프록시를 적용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;스프링 AOP와 프록시를 통해 트랜잭션 동기화 트랜잭션 처리 기능이 서비스 계층에 남아있는 문제를 해결한다.&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;(1) 트랜잭션 AOP 적용 흐름&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;673&quot; data-origin-height=&quot;353&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/UIHM1/btrO5sGNeH3/0ljh1TcAWw85OSf0X2Rk91/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/UIHM1/btrO5sGNeH3/0ljh1TcAWw85OSf0X2Rk91/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/UIHM1/btrO5sGNeH3/0ljh1TcAWw85OSf0X2Rk91/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUIHM1%2FbtrO5sGNeH3%2F0ljh1TcAWw85OSf0X2Rk91%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;353&quot; data-origin-width=&quot;673&quot; data-origin-height=&quot;353&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;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;1. &lt;b&gt;클라이언트는 서비스 객체가 아닌 프록시를 호출하고&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;2. 프록시는 스프링 컨테이너를 통해 트랜잭션 매니저를 획득한다.&lt;/li&gt;
&lt;li&gt;3. &lt;b&gt;프록시가 transactionManager.getTransaction()&amp;nbsp;을&amp;nbsp;호출해서&amp;nbsp;트랜잭션을&amp;nbsp;시작한다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;4. 트랜잭션 매니저는 내부에서 데이터소스를 사용해서 커넥션을 생성한다.&lt;/li&gt;
&lt;li&gt;5. 커넥션을 수동 커밋 모드로 변경해서 실제 데이터베이스 트랜잭션을 시작한다.&lt;/li&gt;
&lt;li&gt;6. 커넥션을 트랜잭션 동기화 매니저에 보관한다.&lt;/li&gt;
&lt;li&gt;7. 트랜잭션 동기화 매니저는 쓰레드 로컬에 커넥션을 보관한다. 따라서 멀티 쓰레드 환경에 안전하게 커넥션을 보관할 수 있다.&lt;/li&gt;
&lt;li&gt;8. &lt;b&gt;프록시가 실제 서비스를 호출하고 서비스는 비즈니스 로직을 실행하면서 리포지토리의 메서드들을 호출한다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;9. 리포지토리 메서드들은 트랜잭션이 시작된 커넥션이 필요하다. &lt;b&gt;리포지토리는 DataSourceUtils.getConnection() 을 사용해서 트랜잭션 동기화 매니저에 보관된 커넥션을 꺼내서 사용한다.&lt;/b&gt; 이 과정을 통해서 자연스럽게 같은 커넥션을 사용하고, 트랜잭션도 유지된다.&lt;br /&gt;획득한 커넥션을 사용해서 SQL을 데이터베이스에 전달해서 실행한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;(2) 트랜잭션&amp;nbsp;프록시&amp;nbsp;코드&amp;nbsp;예시&lt;/p&gt;
&lt;pre id=&quot;code_1666234100444&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class TransactionProxy {
    private MemberService target;
    
    public void logic() {
        //트랜잭션 시작
        TransactionStatus status = transactionManager.getTransaction(..);
        try {
            //실제 대상 호출
            target.logic();
            transactionManager.commit(status); //성공시 커밋
        } catch (Exception e) {
            transactionManager.rollback(status); //실패시 롤백
            throw new IllegalStateException(e);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;(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;&lt;b&gt;[ 서비스 ]&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1666234178204&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RequiredArgsConstructor
public class MemberService {
    private final MemberRepository memberRepository;

    @Transactional
    public void accountTransfer(String fromId, String toId, int money) {
        bizLogic(fromId, toId, money);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[ 테스트 ]&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1666234364334&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Slf4j
@SpringBootTest
class MemberServiceTest {
    @Autowired
    MemberRepository memberRepository;

    @Autowired
    MemberService memberService;

    @TestConfiguration
    static class TestConfig {
        @Bean
        DataSource dataSource() {
            return new DriverManagerDataSource(URL, USERNAME, PASSWORD);
        }

        @Bean
        PlatformTransactionManager transactionManager() {
            return new DataSourceTransactionManager(dataSource());
        }

        @Bean
        MemberRepository memberRepository() {
            return new MemberRepository(dataSource());
        }

        @Bean
        MemberService memberService() {
            return new MemberService(memberRepository());
        }
    }

    @Test
    void AopCheck() {
        log.info(&quot;memberService class={}&quot;, memberService.getClass());
        log.info(&quot;memberRepository class={}&quot;, memberRepository.getClass());
        Assertions.assertThat(AopUtils.isAopProxy(memberService)).isTrue();
        Assertions.assertThat(AopUtils.isAopProxy(memberRepository)).isFalse();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;@SpringBootTest : &lt;b&gt;스프링 AOP를 적용하려면 스프링 컨테이너가 필요하다.&lt;/b&gt; &lt;br /&gt;이 애노테이션이 있으면 테스트시 스프링 부트를 통해 스프링 컨테이너를 생성한다. &lt;br /&gt;그리고 테스트에서 @Autowired 등을 통해 스프링&amp;nbsp;컨테이너가&amp;nbsp;관리하는&amp;nbsp;빈들을&amp;nbsp;사용할&amp;nbsp;수&amp;nbsp;있다.&lt;/li&gt;
&lt;li&gt;@TestConfiguration&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;/li&gt;
&lt;li&gt;TestConfig &lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DataSource 스프링에서 기본으로 사용할 데이터소스를 스프링 빈으로 등록한다. &lt;br /&gt;추가로 트랜잭션 매니저에서도&amp;nbsp;사용한다.&lt;/li&gt;
&lt;li&gt;DataSourceTransactionManager&amp;nbsp;트랜잭션&amp;nbsp;매니저를&amp;nbsp;스프링&amp;nbsp;빈으로&amp;nbsp;등록한다. &lt;br /&gt;스프링이 제공하는 트랜잭션 AOP는 스프링 빈에 등록된 트랜잭션 매니저를 찾아서 사용하기 때문에&amp;nbsp;트랜잭션&amp;nbsp;매니저를&amp;nbsp;스프링&amp;nbsp;빈으로&amp;nbsp;등록해두어야&amp;nbsp;한다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;AopCheck() : memberServic는 프록시가 스프링 빈으로 등록되었다.&lt;br /&gt;실행 결과&lt;br /&gt;memberService class=class hello.jdbc.service.&lt;b&gt;MemberService$ $EnhancerBySpringCGLIB$$...&lt;/b&gt; &lt;br /&gt;memberRepository class=class hello.jdbc.repository.MemberRepositoryV3&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;(4) 스프링 부트의 자동 리소스 등록&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 부트는 다음과 같이 application.properties 에 있는 속성을 사용해서 DataSource 를 생성하고 스프링 빈으로 등록한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[ application.properties ]&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1666234782419&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.username=sa
spring.datasource.password=&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스프링&amp;nbsp;부트가&amp;nbsp;기본으로&amp;nbsp;생성하는&amp;nbsp;데이터소스는&amp;nbsp;커넥션풀을&amp;nbsp;제공하는&amp;nbsp;HikariDataSource&amp;nbsp;이다.&amp;nbsp;&lt;br /&gt;커넥션풀과 관련된 설정도 application.properties 를 통해서 지정할 수 있다&lt;/li&gt;
&lt;li&gt;spring.datasource.url&amp;nbsp;속성이&amp;nbsp;없으면&amp;nbsp;내장&amp;nbsp;데이터베이스(메모리&amp;nbsp;DB)를&amp;nbsp;생성하려고&amp;nbsp;시도한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[ 트랜잭션 매니저 ]&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스프링 부트는 적절한 트랜잭션 매니저( PlatformTransactionManager )를 자동으로 스프링 빈에 등록한다. &lt;br /&gt;자동으로&amp;nbsp;등록되는&amp;nbsp;스프링&amp;nbsp;빈&amp;nbsp;이름:&amp;nbsp;transactionManager&lt;/li&gt;
&lt;li&gt;개발자가 직접 트랜잭션 매니저를 빈으로 등록하면 스프링 부트는 트랜잭션 매니저를 자동으로 등록하지&amp;nbsp;않는다.&lt;/li&gt;
&lt;li&gt;어떤&amp;nbsp;트랜잭션&amp;nbsp;매니저를&amp;nbsp;선택할지는&amp;nbsp;현재&amp;nbsp;등록된&amp;nbsp;라이브러리를&amp;nbsp;보고&amp;nbsp;판단하는데,&amp;nbsp;&lt;b&gt;JDBC를&amp;nbsp;기술을&amp;nbsp;사용하면 &lt;/b&gt;&lt;br /&gt;&lt;b&gt;DataSourceTransactionManager 를 빈으로 등록하고, JPA를 사용하면 JpaTransactionManager 를 빈으로 등록한다.&lt;/b&gt; &lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;둘다 사용하는 경우 JpaTransactionManager 를 등록한다.&lt;/b&gt; &lt;br /&gt;JpaTransactionManager 는 DataSourceTransactionManager 가 제공하는 기능도 대부분 지원한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[ 테스트 ]&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1666235116911&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Slf4j
@SpringBootTest
class MemberServiceTest {
    @Autowired
    MemberRepository memberRepository;

    @Autowired
    MemberService memberService;

    @TestConfiguration
    static class TestConfig {
        private final DataSource dataSource;

        public TestConfig(DataSource dataSource) {
            this.dataSource = dataSource;
        }

        @Bean
        MemberRepository memberRepository() {
            return new MemberRepository(dataSource);
        }

        @Bean
        MemberService memberService() {
            return new MemberService(memberRepository());
        }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스프링 부트가 application.properties 에 지정된 속성을 참고해서 데이터소스와 트랜잭션 매니저를 자동으로 생성해준다.&lt;/li&gt;
&lt;li&gt;memberService와 memberRepository는 충돌이 없는 경우 수동으로 등록하지 않고&lt;br /&gt;@Service, @Repository를 사용해 스프링 빈으로 등록할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;정리&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;@Transactional을 사용해 트랜잭션 프록시 적용
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;PlatformTransactionManager, TransactionSynchronizationManager 를 사용해 트랜잭션 동기화&lt;/li&gt;
&lt;li&gt;트랜잭션 적용 반복 문제 해결&lt;/li&gt;
&lt;li&gt;서비스 계층에 트랜잭션 처리 로직이 함께 섞여있는 문제 해결&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;스프링 부트의 자동 리소스 등록
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;application.properties 에 있는 속성을 사용해서 DataSource 를 생성하고 스프링 빈으로 등록&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;결론&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;데이터베이스 트랜잭션&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터베이스 트랜잭션은 ACID를 보장해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터베이스는&amp;nbsp;락을&amp;nbsp;통해&amp;nbsp;원자성을&amp;nbsp;보장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션 종료 시점까지 조회한 데이터를 다른 곳에서 변경하지 못하도록 강제로 막아야 할 때는 조회 락을 사용할 수 있다.&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;애플리케이션에서 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;스프링&amp;nbsp;트랜잭션&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;트랜잭션&amp;nbsp;기술들을&amp;nbsp;PlatformTransactionManager(트랜잭션&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;b&gt;트랜잭션 동기화&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PlatformTransactionManager는&amp;nbsp;TransactionSynchronizationManager(트랜잭션&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;TransactionSynchronizationManager는&amp;nbsp;ThreadLocal을&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;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;트랜잭션&amp;nbsp;AOP&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스 로직에 @Transactional을 사용하면 스프링은 프록시를 만들어&lt;span&gt;&amp;nbsp;&lt;/span&gt;트랜잭션을 처리하는 객체와 비즈니스 로직을 처리하는 서비스 객체를 명확하게 분리할 수 있도록 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프록시는&lt;span&gt;&amp;nbsp;&lt;/span&gt;PlatformTransactionManager를 사용하기 때문에 트랜잭션 추상하와 트랜잭션 동기화를 함께 제공한다.&lt;/p&gt;
&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;background-color: #ffffff; color: #666666;&quot;&gt;스프링 부트의 자동 리소스 등록&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #666666;&quot;&gt;스프링 부트는 다음과 같이 application.properties 에 있는 속성을 사용해서 DataSource 를 생성하고 스프링 빈으로 등록한다. 등록한 빈을&lt;span&gt;&amp;nbsp;&lt;/span&gt;PlatformTransactionManager 의 구현체로 사용한다.&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;&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;size14&quot;&gt;&lt;a href=&quot;https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-1&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;출처&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;a href=&quot;https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-1&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;스프링 DB 1편 - 데이터 접근 핵심 원리(김영한)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Spring</category>
      <category>@Transactional</category>
      <category>transactionManager</category>
      <category>트랜잭션</category>
      <author>mandykr</author>
      <guid isPermaLink="true">https://mandykr.tistory.com/89</guid>
      <comments>https://mandykr.tistory.com/89#entry89comment</comments>
      <pubDate>Wed, 19 Oct 2022 12:41:06 +0900</pubDate>
    </item>
    <item>
      <title>[HTTP] 6. HTTP 헤더(캐시와 조건부 요청)</title>
      <link>https://mandykr.tistory.com/85</link>
      <description>&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;2. 검증 헤더와 조건부 요청&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;4. 캐시와 조건부 요청 헤더&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;6. 캐시 무효화&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 캐시 기본 동작&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) 캐시가 없을 때&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터가&amp;nbsp;변경되지&amp;nbsp;않아도&amp;nbsp;계속&amp;nbsp;네트워크를&amp;nbsp;통해서&amp;nbsp;데이터를&amp;nbsp;다운로드&amp;nbsp;받아야&amp;nbsp;한다. &lt;/li&gt;
&lt;li&gt;인터넷 네트워크는 매우 느리고 비싸다. &lt;/li&gt;
&lt;li&gt;브라우저 로딩 속도가 느리다. &lt;/li&gt;
&lt;li&gt;느린 사용자 경험&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2) 캐시 적용&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;캐시&amp;nbsp;덕분에&amp;nbsp;캐시&amp;nbsp;가능&amp;nbsp;시간동안&amp;nbsp;네트워크를&amp;nbsp;사용하지&amp;nbsp;않아도&amp;nbsp;된다. &lt;/li&gt;
&lt;li&gt;비싼 네트워크 사용량을 줄일 수 있다. &lt;/li&gt;
&lt;li&gt;브라우저 로딩 속도가 매우 빠르다. &lt;/li&gt;
&lt;li&gt;빠른 사용자 경험&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[ 첫 번째 요청 ]&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;chche-control&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;494&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bPloP2/btrEhqyRlWO/u2JbMU8cbUm95IuspPHBsK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bPloP2/btrEhqyRlWO/u2JbMU8cbUm95IuspPHBsK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bPloP2/btrEhqyRlWO/u2JbMU8cbUm95IuspPHBsK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbPloP2%2FbtrEhqyRlWO%2Fu2JbMU8cbUm95IuspPHBsK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;623&quot; height=&quot;342&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;494&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;867&quot; data-origin-height=&quot;490&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b5hWyJ/btrEhp7MDuB/T2rOprd8u8q5ljsk603WY1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b5hWyJ/btrEhp7MDuB/T2rOprd8u8q5ljsk603WY1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b5hWyJ/btrEhp7MDuB/T2rOprd8u8q5ljsk603WY1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb5hWyJ%2FbtrEhp7MDuB%2FT2rOprd8u8q5ljsk603WY1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;586&quot; height=&quot;331&quot; data-origin-width=&quot;867&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[ 두 번째 요청 - 캐시 사용 ]&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;776&quot; data-origin-height=&quot;321&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mUEqB/btrEfDFsqVz/HhLofZuxglaMyt4YHhY0MK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mUEqB/btrEfDFsqVz/HhLofZuxglaMyt4YHhY0MK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mUEqB/btrEfDFsqVz/HhLofZuxglaMyt4YHhY0MK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmUEqB%2FbtrEfDFsqVz%2FHhLofZuxglaMyt4YHhY0MK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;549&quot; height=&quot;227&quot; data-origin-width=&quot;776&quot; data-origin-height=&quot;321&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[ 세 번째 요청 - 캐시 시간 초과 ]&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;캐시&amp;nbsp;유효&amp;nbsp;시간이&amp;nbsp;초과하면,&amp;nbsp;서버를&amp;nbsp;통해&amp;nbsp;데이터를&amp;nbsp;다시&amp;nbsp;조회하고,&amp;nbsp;캐시를&amp;nbsp;갱신한다. &lt;/li&gt;
&lt;li&gt;이때 다시 네트워크 다운로드가 발생한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;780&quot; data-origin-height=&quot;308&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/m04nb/btrEgvN0SCR/EVk42XpnHQl9pwna9dHdF0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/m04nb/btrEgvN0SCR/EVk42XpnHQl9pwna9dHdF0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/m04nb/btrEgvN0SCR/EVk42XpnHQl9pwna9dHdF0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fm04nb%2FbtrEgvN0SCR%2FEVk42XpnHQl9pwna9dHdF0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;233&quot; data-origin-width=&quot;780&quot; data-origin-height=&quot;308&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;494&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bPloP2/btrEhqyRlWO/u2JbMU8cbUm95IuspPHBsK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bPloP2/btrEhqyRlWO/u2JbMU8cbUm95IuspPHBsK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bPloP2/btrEhqyRlWO/u2JbMU8cbUm95IuspPHBsK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbPloP2%2FbtrEhqyRlWO%2Fu2JbMU8cbUm95IuspPHBsK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;623&quot; height=&quot;342&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;494&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;867&quot; data-origin-height=&quot;490&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b5hWyJ/btrEhp7MDuB/T2rOprd8u8q5ljsk603WY1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b5hWyJ/btrEhp7MDuB/T2rOprd8u8q5ljsk603WY1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b5hWyJ/btrEhp7MDuB/T2rOprd8u8q5ljsk603WY1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb5hWyJ%2FbtrEhp7MDuB%2FT2rOprd8u8q5ljsk603WY1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;586&quot; height=&quot;331&quot; data-origin-width=&quot;867&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;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 검증 헤더와 조건부 요청&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 검증 헤더 &lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;캐시 데이터와 서버 데이터가 같은지 검증하는 데이터 &lt;/li&gt;
&lt;li&gt;Last-Modified , ETag &lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 조건부 요청 헤더 &lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;검증 헤더로 조건에 따른 분기 &lt;/li&gt;
&lt;li&gt;If-Modified-Since: Last-Modified 사용 &lt;/li&gt;
&lt;li&gt;If-None-Match: ETag 사용 &lt;/li&gt;
&lt;li&gt;조건이 만족하면 200 OK &lt;/li&gt;
&lt;li&gt;조건이 만족하지 않으면 304 Not Modified&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) Last-Modified, If-Modified-Since&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 데이터 미변경 예시 &lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;캐시: 2020년 11월 10일 10:00:00 vs 서버: 2020년 11월 10일 10:00:00 &lt;/li&gt;
&lt;li&gt;304 Not Modified, 헤더 데이터만 전송(BODY 미포함) &lt;/li&gt;
&lt;li&gt;전송 용량 0.1M (헤더 0.1M, 바디 1.0M) &lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 데이터 변경 예시 &lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;캐시: 2020년 11월 10일 10:00:00 vs 서버: 2020년 11월 10일 11:00:00 &lt;/li&gt;
&lt;li&gt;200 OK, 모든 데이터 전송(BODY 포함) &lt;/li&gt;
&lt;li&gt;전송 용량 1.1M (헤더 0.1M, 바디 1.0M)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[ 첫 번째 요청 - 검증 헤더 추가 ]&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Last-Modified&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;883&quot; data-origin-height=&quot;485&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xxN9s/btrEdrsx5C6/HS0MGVtRpB6g85Lnqheun0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xxN9s/btrEdrsx5C6/HS0MGVtRpB6g85Lnqheun0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xxN9s/btrEdrsx5C6/HS0MGVtRpB6g85Lnqheun0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxxN9s%2FbtrEdrsx5C6%2FHS0MGVtRpB6g85Lnqheun0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;647&quot; height=&quot;355&quot; data-origin-width=&quot;883&quot; data-origin-height=&quot;485&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;867&quot; data-origin-height=&quot;486&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cWWEFW/btrEelk52nA/i4IrFTvWJMluyz6HiPkc71/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cWWEFW/btrEelk52nA/i4IrFTvWJMluyz6HiPkc71/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cWWEFW/btrEelk52nA/i4IrFTvWJMluyz6HiPkc71/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcWWEFW%2FbtrEelk52nA%2Fi4IrFTvWJMluyz6HiPkc71%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;357&quot; data-origin-width=&quot;867&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[ 두 번째 요청 - 캐시 시간 초과, 조건부 요청 ]&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;if-modified-since&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;771&quot; data-origin-height=&quot;314&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/byrq1D/btrEdr7bv3d/hvKohRIZGmcKK9KEUckjz0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/byrq1D/btrEdr7bv3d/hvKohRIZGmcKK9KEUckjz0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/byrq1D/btrEdr7bv3d/hvKohRIZGmcKK9KEUckjz0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbyrq1D%2FbtrEdr7bv3d%2FhvKohRIZGmcKK9KEUckjz0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;244&quot; data-origin-width=&quot;771&quot; data-origin-height=&quot;314&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;768&quot; data-origin-height=&quot;340&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uSg0D/btrEeXqLBXi/GrFKT4BI8SmY3ErSCnKOik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uSg0D/btrEeXqLBXi/GrFKT4BI8SmY3ErSCnKOik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uSg0D/btrEeXqLBXi/GrFKT4BI8SmY3ErSCnKOik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuSg0D%2FbtrEeXqLBXi%2FGrFKT4BI8SmY3ErSCnKOik%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;266&quot; data-origin-width=&quot;768&quot; data-origin-height=&quot;340&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;304 Not Modified&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;964&quot; data-origin-height=&quot;514&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lz6NN/btrEc2T7iXt/4W7fNEnRZfNvqWaHHxgSw0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lz6NN/btrEc2T7iXt/4W7fNEnRZfNvqWaHHxgSw0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lz6NN/btrEc2T7iXt/4W7fNEnRZfNvqWaHHxgSw0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Flz6NN%2FbtrEc2T7iXt%2F4W7fNEnRZfNvqWaHHxgSw0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;366&quot; data-origin-width=&quot;964&quot; data-origin-height=&quot;514&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Last-Modified, If-Modified-Since 단점&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;1초&amp;nbsp;미만(0.x초)&amp;nbsp;단위로&amp;nbsp;캐시&amp;nbsp;조정이&amp;nbsp;불가능 &lt;/li&gt;
&lt;li&gt;날짜 기반의 로직 사용&lt;/li&gt;
&lt;li&gt;사용이 적절하지 않은 경우
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터를 수정해서 날짜가 다르지만, 같은 데이터를 수정해서 데이터 결과가 똑같은 경우 &lt;/li&gt;
&lt;li&gt;서버에서 별도의 캐시 로직을 관리하고 싶은 경우&lt;br /&gt;예) 스페이스나 주석처럼 크게 영향이 없는 변경에서 캐시를 유지하고 싶은 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2) ETag,&amp;nbsp;If-None-Match&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ETag(Entity&amp;nbsp;Tag) &lt;/li&gt;
&lt;li&gt;캐시용 데이터에 임의의 고유한 버전 이름을 달아둠 &lt;br /&gt;예) ETag: &quot;v1.0&quot;, ETag: &quot;a2jiodwjekjl3&quot; &lt;/li&gt;
&lt;li&gt;데이터가 변경되면 이 이름을 바꾸어서 변경함(Hash를 다시 생성) &lt;br /&gt;예) ETag: &quot;aaaaa&quot; -&amp;gt; ETag: &quot;bbbbb&quot; &lt;/li&gt;
&lt;li&gt;진짜 단순하게 ETag만 보내서 같으면 유지, 다르면 다시 받기!&lt;/li&gt;
&lt;li&gt;캐시&amp;nbsp;제어&amp;nbsp;로직을&amp;nbsp;서버에서&amp;nbsp;완전히&amp;nbsp;관리 &lt;/li&gt;
&lt;li&gt;클라이언트는 단순히 이 값을 서버에 제공(&lt;b&gt;클라이언트는 캐시 메커니즘을 모름&lt;/b&gt;) &lt;/li&gt;
&lt;li&gt;예) &lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버는 배타 오픈 기간인 3일 동안 파일이 변경되어도 ETag를 동일하게 유지 &lt;/li&gt;
&lt;li&gt;애플리케이션 배포 주기에 맞추어 ETag 모두 갱신&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[ 첫 번째 요청 - 검증 헤더 추가 ]&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;ETag&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;884&quot; data-origin-height=&quot;490&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bcehji/btrEdRkn1JO/P88zt0O0na6mtMIQAwNHbK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bcehji/btrEdRkn1JO/P88zt0O0na6mtMIQAwNHbK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bcehji/btrEdRkn1JO/P88zt0O0na6mtMIQAwNHbK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbcehji%2FbtrEdRkn1JO%2FP88zt0O0na6mtMIQAwNHbK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;684&quot; height=&quot;379&quot; data-origin-width=&quot;884&quot; data-origin-height=&quot;490&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;923&quot; data-origin-height=&quot;497&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cmZaRT/btrEh9p6WOc/lQkHJFb4YUJoWVMPc9LmB0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cmZaRT/btrEh9p6WOc/lQkHJFb4YUJoWVMPc9LmB0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cmZaRT/btrEh9p6WOc/lQkHJFb4YUJoWVMPc9LmB0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcmZaRT%2FbtrEh9p6WOc%2FlQkHJFb4YUJoWVMPc9LmB0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;378&quot; data-origin-width=&quot;923&quot; data-origin-height=&quot;497&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;[ 두 번째 요청 - 캐시 시간 초과, 조건부 요청 ]&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;if-None-Match&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;790&quot; data-origin-height=&quot;332&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cloyja/btrEdSXVplg/I0eHwTmLwOMKirVSxzKWZK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cloyja/btrEdSXVplg/I0eHwTmLwOMKirVSxzKWZK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cloyja/btrEdSXVplg/I0eHwTmLwOMKirVSxzKWZK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcloyja%2FbtrEdSXVplg%2FI0eHwTmLwOMKirVSxzKWZK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;278&quot; data-origin-width=&quot;790&quot; data-origin-height=&quot;332&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;304 Not Modified&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;943&quot; data-origin-height=&quot;514&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uh6v7/btrEhq61ns0/ajA0Ngx1KXmJBFmOHtKi10/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uh6v7/btrEhq61ns0/ajA0Ngx1KXmJBFmOHtKi10/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uh6v7/btrEhq61ns0/ajA0Ngx1KXmJBFmOHtKi10/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fuh6v7%2FbtrEhq61ns0%2FajA0Ngx1KXmJBFmOHtKi10%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;371&quot; data-origin-width=&quot;943&quot; data-origin-height=&quot;514&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 캐시와 조건부 요청 헤더&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) 캐시 제어 헤더&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;Cache-Control:&amp;nbsp;캐시&amp;nbsp;제어 &lt;/li&gt;
&lt;li&gt;Pragma: 캐시 제어(하위 호환) &lt;/li&gt;
&lt;li&gt;Expires: 캐시 유효 기간(하위 호환)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;[ Cache-Control: 캐시 지시어(directives) ]&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. Cache-Control: max-age &lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;캐시 유효 시간, 초 단위 &lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. Cache-Control: no-cache &lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터는 캐시해도 되지만, 항상 원(origin) 서버에 검증하고 사용 &lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. Cache-Control: no-store &lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터에 민감한 정보가 있으므로 저장하면 안됨 &lt;br /&gt;(메모리에서&amp;nbsp;사용하고&amp;nbsp;최대한&amp;nbsp;빨리&amp;nbsp;삭제)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;[ Pragma: 캐시 제어(하위 호환) ]&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Pragma:&amp;nbsp;no-cache &lt;/li&gt;
&lt;li&gt;HTTP 1.0 하위 호환&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;[ Expires: 캐시 만료일 지정(하위 호환) ]&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;expires:&amp;nbsp;Mon,&amp;nbsp;01&amp;nbsp;Jan&amp;nbsp;1990&amp;nbsp;00:00:00&amp;nbsp;GMT &lt;/li&gt;
&lt;li&gt;캐시 만료일을 정확한 날짜로 지정 &lt;/li&gt;
&lt;li&gt;HTTP 1.0 부터 사용 &lt;/li&gt;
&lt;li&gt;지금은 더 유연한 Cache-Control: max-age 권장 &lt;/li&gt;
&lt;li&gt;Cache-Control: max-age와 함께 사용하면 Expires는 무시&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2) 검증 헤더와 조건부 요청 헤더&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 검증 헤더 (Validator) &lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ETag: &quot;v1.0&quot;, ETag: &quot;asid93jkrh2l&quot; &lt;/li&gt;
&lt;li&gt;Last-Modified: Thu, 04 Jun 2020 07:19:24 GMT &lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 조건부 요청 헤더 &lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;If-Match, If-None-Match: ETag 값 사용 &lt;/li&gt;
&lt;li&gt;If-Modified-Since, If-Unmodified-Since: Last-Modified 값 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 프록시 캐시&lt;/h3&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;944&quot; data-origin-height=&quot;429&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ddG5yV/btrEdsL3CWu/oLNzAnxKcDt934dMms5YeK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ddG5yV/btrEdsL3CWu/oLNzAnxKcDt934dMms5YeK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ddG5yV/btrEdsL3CWu/oLNzAnxKcDt934dMms5YeK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FddG5yV%2FbtrEdsL3CWu%2FoLNzAnxKcDt934dMms5YeK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;308&quot; data-origin-width=&quot;944&quot; data-origin-height=&quot;429&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. Cache-Control: public &lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;응답이 public 캐시에 저장되어도 됨 &lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. Cache-Control: private &lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;응답이 해당 사용자만을 위한 것임, private 캐시에 저장해야 함(기본값) &lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. Cache-Control: s-maxage &lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프록시 캐시에만 적용되는 max-age &lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. Age: 60 (HTTP 헤더) &lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;오리진 서버에서 응답 후 프록시 캐시 내에 머문 시간(초)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. 캐시 무효화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Cache-Control - 확실한&amp;nbsp;캐시&amp;nbsp;무효화&lt;b&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. Cache-Control: no-cache &lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터는 캐시해도 되지만, 항상 원 서버에 검증하고 사용(이름에 주의!) &lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. Cache-Control: no-store &lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터에 민감한 정보가 있으므로 저장하면 안됨 &lt;br /&gt;(메모리에서&amp;nbsp;사용하고&amp;nbsp;최대한&amp;nbsp;빨리&amp;nbsp;삭제) &lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. Cache-Control: must-revalidate &lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;캐시 만료후 최초 조회시 원 서버에 검증해야함 &lt;/li&gt;
&lt;li&gt;원 서버 접근 실패시 반드시 오류가 발생해야함 - 504(Gateway Timeout) &lt;/li&gt;
&lt;li&gt;must-revalidate는 캐시 유효 시간이라면 캐시를 사용함 &lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. Pragma: no-cache &lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HTTP 1.0 하위 호환&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;[ no-cache 기본 동작 ]&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;969&quot; data-origin-height=&quot;320&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bVngXK/btrEhqsz7lg/bitB5jSznLH4kHU0K0Ijnk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bVngXK/btrEhqsz7lg/bitB5jSznLH4kHU0K0Ijnk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bVngXK/btrEhqsz7lg/bitB5jSznLH4kHU0K0Ijnk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbVngXK%2FbtrEhqsz7lg%2FbitB5jSznLH4kHU0K0Ijnk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;251&quot; data-origin-width=&quot;969&quot; data-origin-height=&quot;320&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;&lt;b&gt;[ no-cache vs must-revalidate ]&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;no-cache&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;898&quot; data-origin-height=&quot;376&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bfwfVm/btrEdzqm1V8/kjhIrfpULLD6rYPO3LmzMk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bfwfVm/btrEdzqm1V8/kjhIrfpULLD6rYPO3LmzMk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bfwfVm/btrEdzqm1V8/kjhIrfpULLD6rYPO3LmzMk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbfwfVm%2FbtrEdzqm1V8%2FkjhIrfpULLD6rYPO3LmzMk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;717&quot; height=&quot;300&quot; data-origin-width=&quot;898&quot; data-origin-height=&quot;376&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;must-revalidate&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;904&quot; data-origin-height=&quot;370&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/K9y68/btrEd5CuIKE/4Ec2S2IKz6pZ8zUBnRthK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/K9y68/btrEd5CuIKE/4Ec2S2IKz6pZ8zUBnRthK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/K9y68/btrEd5CuIKE/4Ec2S2IKz6pZ8zUBnRthK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FK9y68%2FbtrEd5CuIKE%2F4Ec2S2IKz6pZ8zUBnRthK1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;299&quot; data-origin-width=&quot;904&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;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;출처&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;a href=&quot;https://www.inflearn.com/course/http-%EC%9B%B9-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC&quot;&gt;모든 개발자를 위한 HTTP 웹 기본 지식(김영한)&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;&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>HTTP</category>
      <category>HTTP 헤더</category>
      <author>mandykr</author>
      <guid isPermaLink="true">https://mandykr.tistory.com/85</guid>
      <comments>https://mandykr.tistory.com/85#entry85comment</comments>
      <pubDate>Wed, 8 Jun 2022 12:38:22 +0900</pubDate>
    </item>
    <item>
      <title>[HTTP] 5. HTTP 헤더(일반 헤더)</title>
      <link>https://mandykr.tistory.com/84</link>
      <description>&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;2. 콘텐츠 협상&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;4. 일반 정보&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;6. 인증&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;HTTP 헤더&lt;/h3&gt;
&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;182&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/brKxSx/btrEeW5ZO4B/kXeHWjGLrML9K9ThhRhonK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/brKxSx/btrEeW5ZO4B/kXeHWjGLrML9K9ThhRhonK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/brKxSx/btrEeW5ZO4B/kXeHWjGLrML9K9ThhRhonK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbrKxSx%2FbtrEeW5ZO4B%2FkXeHWjGLrML9K9ThhRhonK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;709&quot; height=&quot;167&quot; data-origin-width=&quot;773&quot; data-origin-height=&quot;182&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;header-field&amp;nbsp;=&amp;nbsp;field-name&amp;nbsp;&quot;:&quot;&amp;nbsp;OWS&amp;nbsp;field-value&amp;nbsp;OWS&amp;nbsp;(OWS:띄어쓰기&amp;nbsp;허용)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;field-name은 대소문자 구문 없음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HTTP&amp;nbsp;전송에&amp;nbsp;필요한&amp;nbsp;모든&amp;nbsp;부가정보&lt;br /&gt;예) 메시지 바디의 내용, 메시지 바디의 크기, 압축, 인증, 요청 클라이언트(브라우저) 정보, 서버&amp;nbsp;애플리케이션&amp;nbsp;정보,&amp;nbsp;캐시&amp;nbsp;관리&amp;nbsp;정보...&lt;/li&gt;
&lt;li&gt;표준 헤더가 너무 많음&lt;br /&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/List_of_HTTP_header_fields&quot;&gt;https://en.wikipedia.org/wiki/List_of_HTTP_header_fields&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;필요시 임의의 헤더 추가 가능&lt;br /&gt;helloworld: hihi&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;RFC7230(최신 HTTP 표준)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Representation&amp;nbsp;=&amp;nbsp;representation&amp;nbsp;Metadata&amp;nbsp;+&amp;nbsp;Representation&amp;nbsp;Data&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;663&quot; data-origin-height=&quot;237&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EmyoC/btrEc4qo7ci/C0RVvtdx9uCrVUao0XrXrK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EmyoC/btrEc4qo7ci/C0RVvtdx9uCrVUao0XrXrK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EmyoC/btrEc4qo7ci/C0RVvtdx9uCrVUao0XrXrK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEmyoC%2FbtrEc4qo7ci%2FC0RVvtdx9uCrVUao0XrXrK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;663&quot; height=&quot;237&quot; data-origin-width=&quot;663&quot; data-origin-height=&quot;237&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메시지&amp;nbsp;본문(message&amp;nbsp;body)을&amp;nbsp;통해&amp;nbsp;표현&amp;nbsp;데이터&amp;nbsp;전달&lt;/li&gt;
&lt;li&gt;메시지 본문 = 페이로드(payload)&lt;/li&gt;
&lt;li&gt;표현은 요청이나 응답에서 전달할 실제 데이터&lt;/li&gt;
&lt;li&gt;표현 헤더는 표현 데이터를 해석할 수 있는 정보 제공 &lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터 유형(html, json), 데이터 길이, 압축 정보 등등&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;참고: 표현 헤더는 표현 메타데이터와, 페이로드 메시지를 구분해야 하지만, 여기서는 생략&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 표현&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;표현 헤더는 전송, 응답 둘다 사용&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;Content-Type:&amp;nbsp;표현&amp;nbsp;데이터의&amp;nbsp;형식&lt;/li&gt;
&lt;li&gt;Content-Encoding: 표현 데이터의 압축 방식&lt;/li&gt;
&lt;li&gt;Content-Language: 표현 데이터의 자연 언어&lt;/li&gt;
&lt;li&gt;Content-Length: 표현 데이터의 길이&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;[ Content-Type ] : 표현 데이터의 형식 설명&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;미디어&amp;nbsp;타입,&amp;nbsp;문자&amp;nbsp;인코딩&lt;/li&gt;
&lt;li&gt;예)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;text/html; charset=utf-8&lt;/li&gt;
&lt;li&gt;application/json&lt;/li&gt;
&lt;li&gt;image/png&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;[ Content-Encoding ] : 표현&amp;nbsp;데이터&amp;nbsp;인코딩&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;표현&amp;nbsp;데이터를&amp;nbsp;압축하기&amp;nbsp;위해&amp;nbsp;사용&lt;/li&gt;
&lt;li&gt;데이터를 전달하는 곳에서 압축 후 인코딩 헤더 추가&lt;/li&gt;
&lt;li&gt;데이터를 읽는 쪽에서 인코딩 헤더의 정보로 압축 해제&lt;/li&gt;
&lt;li&gt;예) &lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;gzip&lt;/li&gt;
&lt;li&gt;deflate&lt;/li&gt;
&lt;li&gt;identity&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;[ Content-Language ] : 표현&amp;nbsp;데이터의&amp;nbsp;자연&amp;nbsp;언어&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;표현&amp;nbsp;데이터의&amp;nbsp;자연&amp;nbsp;언어를&amp;nbsp;표현&lt;/li&gt;
&lt;li&gt;예) &lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ko&lt;/li&gt;
&lt;li&gt;en&lt;/li&gt;
&lt;li&gt;en-US&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;[ Content-Length ] : 표현 데이터의 길이&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;바이트&amp;nbsp;단위&lt;/li&gt;
&lt;li&gt;Transfer-Encoding(전송 코딩)을 사용하면 Content-Length를 사용하면 안됨&lt;/li&gt;
&lt;/ul&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;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 협상(콘텐츠 네고시에이션)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트가&amp;nbsp;선호하는&amp;nbsp;표현&amp;nbsp;요청&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;협상 헤더는 요청시에만 사용&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;Accept:&amp;nbsp;클라이언트가&amp;nbsp;선호하는&amp;nbsp;미디어&amp;nbsp;타입&amp;nbsp;전달&lt;/li&gt;
&lt;li&gt;Accept-Charset: 클라이언트가 선호하는 문자 인코딩&lt;/li&gt;
&lt;li&gt;Accept-Encoding: 클라이언트가 선호하는 압축 인코딩&lt;/li&gt;
&lt;li&gt;Accept-Language: 클라이언트가 선호하는 자연 언어&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;(1) Accept-Language 적용&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;[ Accept-Language 적용 전 ]&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;841&quot; data-origin-height=&quot;205&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zFBP9/btrEc3kI7RB/1AXovSC0uKzt7xkswwGYO1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zFBP9/btrEc3kI7RB/1AXovSC0uKzt7xkswwGYO1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zFBP9/btrEc3kI7RB/1AXovSC0uKzt7xkswwGYO1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzFBP9%2FbtrEc3kI7RB%2F1AXovSC0uKzt7xkswwGYO1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;168&quot; data-origin-width=&quot;841&quot; data-origin-height=&quot;205&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[ Accept-Language 적용 후 ]&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;830&quot; data-origin-height=&quot;206&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ZpWsm/btrEcyyNPBV/bWZEQthI4IzKbNdpRzZDqK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ZpWsm/btrEcyyNPBV/bWZEQthI4IzKbNdpRzZDqK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ZpWsm/btrEcyyNPBV/bWZEQthI4IzKbNdpRzZDqK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZpWsm%2FbtrEcyyNPBV%2FbWZEQthI4IzKbNdpRzZDqK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;167&quot; data-origin-width=&quot;830&quot; data-origin-height=&quot;206&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;&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Quality&amp;nbsp;Values(q)&amp;nbsp;값&amp;nbsp;사용&lt;/li&gt;
&lt;li&gt;0~1, 생략하면 1&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[ Accept-Language 우선순위 적용 전 ]&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;831&quot; data-origin-height=&quot;206&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dq8bMw/btrEhrc9k1M/MLgdla2xnQ5W3ZikVqP4N0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dq8bMw/btrEhrc9k1M/MLgdla2xnQ5W3ZikVqP4N0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dq8bMw/btrEhrc9k1M/MLgdla2xnQ5W3ZikVqP4N0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdq8bMw%2FbtrEhrc9k1M%2FMLgdla2xnQ5W3ZikVqP4N0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;170&quot; data-origin-width=&quot;831&quot; data-origin-height=&quot;206&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[ Accept-Language 우선순위 적용 후 ]&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-origin-width=&quot;825&quot; data-origin-height=&quot;187&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhdhFu/btrEgv1ba6v/ix3uYt6LLhzPPJ6UbALh71/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhdhFu/btrEgv1ba6v/ix3uYt6LLhzPPJ6UbALh71/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhdhFu/btrEgv1ba6v/ix3uYt6LLhzPPJ6UbALh71/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbhdhFu%2FbtrEgv1ba6v%2Fix3uYt6LLhzPPJ6UbALh71%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;676&quot; height=&quot;153&quot; data-origin-width=&quot;825&quot; data-origin-height=&quot;187&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;'q' 가 클수록 높은 우선순위&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Accept-Language: ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;1. ko-KR;q=1 (q생략)&lt;/li&gt;
&lt;li&gt;2. ko;q=0.9&lt;/li&gt;
&lt;li&gt;3. en-US;q=0.8&lt;/li&gt;
&lt;li&gt;4. en:q=0.7&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;구체적인 것이 높은 우선순위&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Accept:&amp;nbsp;text/*,&amp;nbsp;text/plain,&amp;nbsp;text/plain;format=flowed,&amp;nbsp;*/*&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;1.&amp;nbsp;text/plain;format=flowed&lt;/li&gt;
&lt;li&gt;2.&amp;nbsp;text/plain&lt;/li&gt;
&lt;li&gt;3.&amp;nbsp;text/*&lt;/li&gt;
&lt;li&gt;4.&amp;nbsp;*/*&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;구체적인&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;Accept: text/*;q=0.3, text/html;q=0.7, text/html;level=1,&amp;nbsp;text/html;level=2;q=0.4,&amp;nbsp;*/*;q=0.5&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;272&quot; data-origin-height=&quot;230&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bIAI6I/btrEcQMLweo/6M0cwAQ47BNCEaMO9XB6Ck/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bIAI6I/btrEcQMLweo/6M0cwAQ47BNCEaMO9XB6Ck/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bIAI6I/btrEcQMLweo/6M0cwAQ47BNCEaMO9XB6Ck/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbIAI6I%2FbtrEcQMLweo%2F6M0cwAQ47BNCEaMO9XB6Ck%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;214&quot; height=&quot;230&quot; data-origin-width=&quot;272&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;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 전송 방식&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;단순 전송 : [ Content-Length ]&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;911&quot; data-origin-height=&quot;353&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ZKOUH/btrEfDLVWoO/9gv2ZKOQ7FHVe4FL2ruovk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ZKOUH/btrEfDLVWoO/9gv2ZKOQ7FHVe4FL2ruovk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ZKOUH/btrEfDLVWoO/9gv2ZKOQ7FHVe4FL2ruovk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZKOUH%2FbtrEfDLVWoO%2F9gv2ZKOQ7FHVe4FL2ruovk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;682&quot; height=&quot;264&quot; data-origin-width=&quot;911&quot; data-origin-height=&quot;353&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;압축 전송 : [ Content-Encoding ]&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;900&quot; data-origin-height=&quot;313&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eBl8LX/btrEdSQA88F/yCMbkZKgC32l9URdoTTJx0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eBl8LX/btrEdSQA88F/yCMbkZKgC32l9URdoTTJx0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eBl8LX/btrEdSQA88F/yCMbkZKgC32l9URdoTTJx0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeBl8LX%2FbtrEdSQA88F%2FyCMbkZKgC32l9URdoTTJx0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;240&quot; data-origin-width=&quot;900&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;분할 전송 : [ Transfer-Encoding ]&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;919&quot; data-origin-height=&quot;405&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bsLEVn/btrEc20o2kl/XhSQKTEqWBKj13UFoguot1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bsLEVn/btrEc20o2kl/XhSQKTEqWBKj13UFoguot1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bsLEVn/btrEc20o2kl/XhSQKTEqWBKj13UFoguot1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbsLEVn%2FbtrEc20o2kl%2FXhSQKTEqWBKj13UFoguot1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;681&quot; height=&quot;300&quot; data-origin-width=&quot;919&quot; data-origin-height=&quot;405&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 크기(Byte)&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;범위 전송 : [ Range,&amp;nbsp;Content-Range ]&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;903&quot; data-origin-height=&quot;300&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c8PcuW/btrEeYCPol0/FVxdwVggosqRTJYJomV0mk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c8PcuW/btrEeYCPol0/FVxdwVggosqRTJYJomV0mk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c8PcuW/btrEeYCPol0/FVxdwVggosqRTJYJomV0mk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc8PcuW%2FbtrEeYCPol0%2FFVxdwVggosqRTJYJomV0mk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;222&quot; data-origin-width=&quot;903&quot; data-origin-height=&quot;300&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 일반 정보&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;From:&amp;nbsp;유저&amp;nbsp;에이전트의&amp;nbsp;이메일&amp;nbsp;정보&lt;/li&gt;
&lt;li&gt;Referer: 이전 웹 페이지 주소&lt;/li&gt;
&lt;li&gt;User-Agent: 유저 에이전트 애플리케이션 정보&lt;/li&gt;
&lt;li&gt;Server: 요청을 처리하는 오리진 서버의 소프트웨어 정보&lt;/li&gt;
&lt;li&gt;Date: 메시지가 생성된 날짜&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;From: 유저&amp;nbsp;에이전트의&amp;nbsp;이메일&amp;nbsp;정보&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;일반적으로&amp;nbsp;잘&amp;nbsp;사용되지&amp;nbsp;않음&lt;/li&gt;
&lt;li&gt;검색 엔진 같은 곳에서, 주로 사용&lt;/li&gt;
&lt;li&gt;요청에서 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Referer: 이전&amp;nbsp;웹&amp;nbsp;페이지&amp;nbsp;주소&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;현재&amp;nbsp;요청된&amp;nbsp;페이지의&amp;nbsp;이전&amp;nbsp;웹&amp;nbsp;페이지&amp;nbsp;주소&lt;/li&gt;
&lt;li&gt;A -&amp;gt; B로 이동하는 경우 B를 요청할 때 Referer: A 를 포함해서 요청&lt;/li&gt;
&lt;li&gt;Referer를 사용해서 유입 경로 분석 가능&lt;/li&gt;
&lt;li&gt;요청에서 사용&lt;/li&gt;
&lt;li&gt;참고: referer는 단어 referrer의 오타&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;User-Agent: 유저&amp;nbsp;에이전트&amp;nbsp;애플리케이션&amp;nbsp;정보 &lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36&amp;nbsp;(KHTML,&amp;nbsp;like&amp;nbsp;Gecko)&amp;nbsp;Chrome/86.0.4240.183&amp;nbsp;Safari/537.36&lt;/li&gt;
&lt;li&gt;클리이언트의 애플리케이션 정보(웹 브라우저 정보, 등등)&lt;/li&gt;
&lt;li&gt;통계 정보&lt;/li&gt;
&lt;li&gt;어떤 종류의 브라우저에서 장애가 발생하는지 파악 가능&lt;/li&gt;
&lt;li&gt;요청에서 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Server: 요청을&amp;nbsp;처리하는&amp;nbsp;ORIGIN&amp;nbsp;서버의&amp;nbsp;소프트웨어&amp;nbsp;정보&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Server:&amp;nbsp;Apache/2.2.22&amp;nbsp;(Debian)&lt;/li&gt;
&lt;li&gt;server: nginx&lt;/li&gt;
&lt;li&gt;응답에서 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Date: 메시지가&amp;nbsp;발생한&amp;nbsp;날짜와&amp;nbsp;시간&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Date:&amp;nbsp;Tue,&amp;nbsp;15&amp;nbsp;Nov&amp;nbsp;1994&amp;nbsp;08:12:31&amp;nbsp;GMT&lt;/li&gt;
&lt;li&gt;응답에서 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 특별한 정보&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;Host:&amp;nbsp;요청한&amp;nbsp;호스트&amp;nbsp;정보(도메인)&lt;/li&gt;
&lt;li&gt;Location: 페이지 리다이렉션&lt;/li&gt;
&lt;li&gt;Allow: 허용 가능한 HTTP 메서드&lt;/li&gt;
&lt;li&gt;Retry-After: 유저 에이전트가 다음 요청을 하기까지 기다려야 하는 시간&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Host: 요청한&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;447&quot; data-origin-height=&quot;107&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Eq9th/btrEhp7vXve/V7hoZtYOp3Zc2HVz12k651/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Eq9th/btrEhp7vXve/V7hoZtYOp3Zc2HVz12k651/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Eq9th/btrEhp7vXve/V7hoZtYOp3Zc2HVz12k651/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEq9th%2FbtrEhp7vXve%2FV7hoZtYOp3Zc2HVz12k651%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;401&quot; height=&quot;96&quot; data-origin-width=&quot;447&quot; data-origin-height=&quot;107&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;요청에서&amp;nbsp;사용&lt;/li&gt;
&lt;li&gt;필수&lt;/li&gt;
&lt;li&gt;하나의 서버가 여러 도메인을 처리해야 할 때&lt;/li&gt;
&lt;li&gt;하나의 IP 주소에 여러 도메인이 적용되어 있을 때&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가상호스트를 통해 여러 도메인을 한번에 처리할 수 있는 서버 실제&amp;nbsp;애플리케이션이&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;993&quot; data-origin-height=&quot;307&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IcBL9/btrD5OPk9ge/cEFclKc6r3KSW6tWg1xitK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IcBL9/btrD5OPk9ge/cEFclKc6r3KSW6tWg1xitK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IcBL9/btrD5OPk9ge/cEFclKc6r3KSW6tWg1xitK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIcBL9%2FbtrD5OPk9ge%2FcEFclKc6r3KSW6tWg1xitK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;196&quot; data-origin-width=&quot;993&quot; data-origin-height=&quot;307&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Location: 페이지 리다이렉션&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;웹 브라우저는 3xx 응답의 결과에 Location 헤더가 있으면, Location 위치로 자동 이동(리다이렉트)&lt;/li&gt;
&lt;li&gt;응답코드 3xx에서 설명&lt;/li&gt;
&lt;li&gt;201 (Created): Location 값은 요청에 의해 생성된 리소스 URI&lt;/li&gt;
&lt;li&gt;3xx (Redirection): Location 값은 요청을 자동으로 리디렉션하기 위한 대상 리소스를 가리킴&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Allow: 허용 가능한 HTTP 메서드&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;405&amp;nbsp;(Method&amp;nbsp;Not&amp;nbsp;Allowed)&amp;nbsp;에서&amp;nbsp;응답에&amp;nbsp;포함해야함&lt;/li&gt;
&lt;li&gt;Allow: GET, HEAD, PUT&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Retry-After: 유저 에이전트가 다음 요청을 하기까지 기다려야 하는 시간&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;503&amp;nbsp;(Service&amp;nbsp;Unavailable):&amp;nbsp;서비스가&amp;nbsp;언제까지&amp;nbsp;불능인지&amp;nbsp;알려줄&amp;nbsp;수&amp;nbsp;있음&lt;/li&gt;
&lt;li&gt;Retry-After: Fri, 31 Dec 1999 23:59:59 GMT (날짜 표기)&lt;/li&gt;
&lt;li&gt;Retry-After: 120 (초단위 표기)&lt;/li&gt;
&lt;/ul&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;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. 인증&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;Authorization:&amp;nbsp;클라이언트&amp;nbsp;인증&amp;nbsp;정보를&amp;nbsp;서버에&amp;nbsp;전달&lt;/li&gt;
&lt;li&gt;WWW-Authenticate: 리소스 접근시 필요한 인증 방법 정의&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Authorization: 클라이언트 인증 정보를 서버에 전달&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Authorization:&amp;nbsp;Basic&amp;nbsp;xxxxxxxxxxxxxxxx&lt;/p&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;WWW-Authenticate: 리소스 접근시 필요한 인증 방법 정의&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;리소스&amp;nbsp;접근시&amp;nbsp;필요한&amp;nbsp;인증&amp;nbsp;방법&amp;nbsp;정의&lt;/li&gt;
&lt;li&gt;401 Unauthorized 응답과 함께 사용&lt;/li&gt;
&lt;li&gt;WWW-Authenticate: Newauth realm=&quot;apps&quot;, type=1, title=&quot;Login to \&quot;apps\&quot;&quot;, Basic realm=&quot;simple&quot;&lt;/li&gt;
&lt;/ul&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;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. 쿠키&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;Set-Cookie:&amp;nbsp;서버에서&amp;nbsp;클라이언트로&amp;nbsp;쿠키&amp;nbsp;전달(응답)&lt;/li&gt;
&lt;li&gt;Cookie: 클라이언트가 서버에서 받은 쿠키를 저장하고, HTTP 요청시 서버로 전달&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;HTTP는 Stateless&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HTTP는 무상태(Stateless) 프로토콜이다.&lt;/li&gt;
&lt;li&gt;클라이언트와 서버가 요청과 응답을 주고 받으면 연결이 끊어진다.&lt;/li&gt;
&lt;li&gt;클라이언트가 다시 요청하면 서버는 이전 요청을 기억하지 못한다.&lt;/li&gt;
&lt;li&gt;클라이언트와 서버는 서로 상태를 유지하지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;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;923&quot; data-origin-height=&quot;403&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dcnNkx/btrEczdrN11/tzMdbbB3T7lgkKly0pfTm0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dcnNkx/btrEczdrN11/tzMdbbB3T7lgkKly0pfTm0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dcnNkx/btrEczdrN11/tzMdbbB3T7lgkKly0pfTm0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdcnNkx%2FbtrEczdrN11%2FtzMdbbB3T7lgkKly0pfTm0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;663&quot; height=&quot;289&quot; data-origin-width=&quot;923&quot; data-origin-height=&quot;403&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[ 로그인 이후 welcome 페이지 접근 ]&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;392&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b3QHV6/btrEcPUIibi/KaIK0o8iWnoRhIvl7NnEX0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b3QHV6/btrEcPUIibi/KaIK0o8iWnoRhIvl7NnEX0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b3QHV6/btrEcPUIibi/KaIK0o8iWnoRhIvl7NnEX0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb3QHV6%2FbtrEcPUIibi%2FKaIK0o8iWnoRhIvl7NnEX0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;639&quot; height=&quot;274&quot; data-origin-width=&quot;914&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;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;712&quot; data-origin-height=&quot;393&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Uiopv/btrEdRxncRe/9iKKscpSbUUVVr7zet6Xc0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Uiopv/btrEdRxncRe/9iKKscpSbUUVVr7zet6Xc0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Uiopv/btrEdRxncRe/9iKKscpSbUUVVr7zet6Xc0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUiopv%2FbtrEdRxncRe%2F9iKKscpSbUUVVr7zet6Xc0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;327&quot; data-origin-width=&quot;712&quot; data-origin-height=&quot;393&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;쿠키 특징&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예) set-cookie: sessionId=abcde1234; expires=Sat, 26-Dec-020 00:00:00 GMT; path=/; domain=.google.com; Secure&lt;/li&gt;
&lt;li&gt;사용처 &lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자 로그인 세션 관리&lt;/li&gt;
&lt;li&gt;광고 정보 트래킹&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;쿠키 정보는 항상 서버에 전송됨 &lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;네트워크 트래픽 추가 유발&lt;/li&gt;
&lt;li&gt;최소한의 정보만 사용(세션 id, 인증 토큰)&lt;/li&gt;
&lt;li&gt;서버에 전송하지 않고, 웹 브라우저 내부에 데이터를 저장하고 싶으면 웹 스토리지 (localStorage, sessionStorage) 참고&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;주의: 보안에 민감한 데이터는 저장하면 안됨(주민번호, 신용카드 번호 등등)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;쿠키 - 생명주기&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Expires,&amp;nbsp;max-age&lt;/li&gt;
&lt;li&gt;Set-Cookie: expires=Sat, 26-Dec-2020 04:39:21 GMT &lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;만료일이 되면 쿠키 삭제&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Set-Cookie: max-age=3600 (3600초) &lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;0이나 음수를 지정하면 쿠키 삭제&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;세션 쿠키: 만료 날짜를 생략하면 브라우저 종료시 까지만 유지&lt;/li&gt;
&lt;li&gt;영속 쿠키: 만료 날짜를 입력하면 해당 날짜까지 유지&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;쿠키 - 도메인&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예)&amp;nbsp;domain=example.org&lt;/li&gt;
&lt;li&gt;명시: 명시한 문서 기준 도메인 + 서브 도메인 포함 &lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;domain=example.org를 지정해서 쿠키 생성&lt;/li&gt;
&lt;li&gt;example.org는 물론이고&lt;/li&gt;
&lt;li&gt;dev.example.org도 쿠키 접근&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;생략: 현재 문서 기준 도메인만 적용 &lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;example.org 에서 쿠키를 생성하고 domain 지정을 생략&lt;/li&gt;
&lt;li&gt;example.org 에서만 쿠키 접근&lt;/li&gt;
&lt;li&gt;dev.example.org는 쿠키 미접근&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;쿠키 - 경로&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예)&amp;nbsp;path=/home&lt;/li&gt;
&lt;li&gt;이 경로를 포함한 하위 경로 페이지만 쿠키 접근&lt;/li&gt;
&lt;li&gt;일반적으로 path=/ 루트로 지정&lt;/li&gt;
&lt;li&gt;예) &lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;path=/home 지정&lt;/li&gt;
&lt;li&gt;/home -&amp;gt; 가능&lt;/li&gt;
&lt;li&gt;/home/level1 -&amp;gt; 가능&lt;/li&gt;
&lt;li&gt;/home/level1/level2 -&amp;gt; 가능&lt;/li&gt;
&lt;li&gt;/hello -&amp;gt; 불가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;쿠키&amp;nbsp;-&amp;nbsp;보안&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Secure,&amp;nbsp;HttpOnly,&amp;nbsp;SameSite&lt;/li&gt;
&lt;li&gt;Secure &lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;쿠키는 http, https를 구분하지 않고 전송&lt;/li&gt;
&lt;li&gt;Secure를 적용하면 https인 경우에만 전송&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;HttpOnly &lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;XSS 공격 방지&lt;/li&gt;
&lt;li&gt;자바스크립트에서 접근 불가(document.cookie)&lt;/li&gt;
&lt;li&gt;HTTP 전송에만 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;SameSite &lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;XSRF 공격 방지&lt;/li&gt;
&lt;li&gt;요청 도메인과 쿠키에 설정된 도메인이 같은 경우만 쿠키 전송&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;출처&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;a href=&quot;https://www.inflearn.com/course/http-%EC%9B%B9-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC&quot;&gt;모든 개발자를 위한 HTTP 웹 기본 지식(김영한)&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;&amp;nbsp;&lt;/p&gt;</description>
      <category>HTTP</category>
      <category>HTTP 헤더</category>
      <author>mandykr</author>
      <guid isPermaLink="true">https://mandykr.tistory.com/84</guid>
      <comments>https://mandykr.tistory.com/84#entry84comment</comments>
      <pubDate>Wed, 8 Jun 2022 12:37:06 +0900</pubDate>
    </item>
    <item>
      <title>[HTTP] 4. HTTP 상태코드</title>
      <link>https://mandykr.tistory.com/83</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;목차&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. HTTP 상태 코드&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 2xx - 성공&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;3. 3xx -&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;리다이렉션&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;4. 4xx -&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;클라이언트&amp;nbsp;오류&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5. 5xx - 서버&amp;nbsp;오류&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. HTTP 상태 코드&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트가 보낸 요청의 처리 상태를 응답해서 알려주는 기능&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;1xx&amp;nbsp;(Informational):&amp;nbsp;요청이&amp;nbsp;수신되어&amp;nbsp;처리중&lt;/li&gt;
&lt;li&gt;2xx (Successful): 요청 정상 처리&lt;/li&gt;
&lt;li&gt;3xx (Redirection): 요청을 완료하려면 추가 행동이 필요&lt;/li&gt;
&lt;li&gt;4xx (Client Error): 클라이언트 오류, 잘못된 문법등으로 서버가 요청을 수행할 수 없음&lt;/li&gt;
&lt;li&gt;5xx (Server Error): 서버 오류, 서버가 정상 요청을 처리하지 못함&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트가 인식할 수 없는 상태코드를 서버가 반환되는 경우&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클라이언트는&amp;nbsp;상위&amp;nbsp;상태코드로&amp;nbsp;해석해서&amp;nbsp;처리&lt;/li&gt;
&lt;li&gt;미래에 새로운 상태 코드가 추가되어도 클라이언트를 변경하지 않아도 됨&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.&amp;nbsp;2xx&amp;nbsp;-&amp;nbsp;성공&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트의&amp;nbsp;요청을&amp;nbsp;성공적으로&amp;nbsp;처리&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;200&amp;nbsp;OK&lt;/li&gt;
&lt;li&gt;201 Created&lt;/li&gt;
&lt;li&gt;202 Accepted&lt;/li&gt;
&lt;li&gt;204 No Content&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;[ 200 OK ]&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;866&quot; data-origin-height=&quot;402&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sdbSk/btrDPWfnLp0/jD491AjHNR8lq7KQY6JGI0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sdbSk/btrDPWfnLp0/jD491AjHNR8lq7KQY6JGI0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sdbSk/btrDPWfnLp0/jD491AjHNR8lq7KQY6JGI0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsdbSk%2FbtrDPWfnLp0%2FjD491AjHNR8lq7KQY6JGI0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;623&quot; height=&quot;289&quot; data-origin-width=&quot;866&quot; data-origin-height=&quot;402&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;[ 201&amp;nbsp;Created ]&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;요청&amp;nbsp;성공해서&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;1008&quot; data-origin-height=&quot;439&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kSehD/btrDTjG1GQk/AUqdDIGO98q9t1fSdpVELk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kSehD/btrDTjG1GQk/AUqdDIGO98q9t1fSdpVELk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kSehD/btrDTjG1GQk/AUqdDIGO98q9t1fSdpVELk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkSehD%2FbtrDTjG1GQk%2FAUqdDIGO98q9t1fSdpVELk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;699&quot; height=&quot;304&quot; data-origin-width=&quot;1008&quot; data-origin-height=&quot;439&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;[&lt;span&gt; 202&amp;nbsp;Accepted&lt;/span&gt;&amp;nbsp;]&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;요청이&amp;nbsp;접수되었으나&amp;nbsp;처리가&amp;nbsp;완료되지&amp;nbsp;않았음&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;배치&amp;nbsp;처리&amp;nbsp;같은&amp;nbsp;곳에서&amp;nbsp;사용&lt;/li&gt;
&lt;li&gt;예) 요청 접수 후 1시간 뒤에 배치 프로세스가 요청을 처리함&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;[&lt;span&gt;&lt;span&gt; 204&amp;nbsp;No&amp;nbsp;Content&lt;/span&gt;&lt;/span&gt;&amp;nbsp;]&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;서버가&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예)&amp;nbsp;웹&amp;nbsp;문서&amp;nbsp;편집기에서&amp;nbsp;save&amp;nbsp;버튼&lt;/li&gt;
&lt;li&gt;save 버튼의 결과로 아무 내용이 없어도 된다.&lt;/li&gt;
&lt;li&gt;save 버튼을 눌러도 같은 화면을 유지해야 한다.&lt;/li&gt;
&lt;li&gt;결과 내용이 없어도 204 메시지(2xx)만으로 성공을 인식할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 3xx - 리다이렉션&lt;/h3&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;300&amp;nbsp;Multiple&amp;nbsp;Choices&lt;/li&gt;
&lt;li&gt;301 Moved Permanently&lt;/li&gt;
&lt;li&gt;302 Found&lt;/li&gt;
&lt;li&gt;303 See Other&lt;/li&gt;
&lt;li&gt;304 Not Modified&lt;/li&gt;
&lt;li&gt;307 Temporary Redirect&lt;/li&gt;
&lt;li&gt;308 Permanent Redirect&lt;/li&gt;
&lt;/ul&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. 영구 리다이렉션 - 특정 리소스의 URI가 영구적으로 이동&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예) /members -&amp;gt; /users&lt;/li&gt;
&lt;li&gt;예) /event -&amp;gt; /new-event&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 일시 리다이렉션 - 일시적인 변경&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;주문 완료 후 주문 내역 화면으로 이동&lt;/li&gt;
&lt;li&gt;PRG: Post/Redirect/Get&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 특수 리다이렉션&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;결과 대신 캐시를 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹&amp;nbsp;브라우저는&amp;nbsp;3xx&amp;nbsp;응답의&amp;nbsp;결과에&amp;nbsp;Location&amp;nbsp;헤더가&amp;nbsp;있으면,&amp;nbsp;Location&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;844&quot; data-origin-height=&quot;452&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/C9zLy/btrDUug5F2L/NygVymh7rXG2daOd28X310/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/C9zLy/btrDUug5F2L/NygVymh7rXG2daOd28X310/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/C9zLy/btrDUug5F2L/NygVymh7rXG2daOd28X310/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FC9zLy%2FbtrDUug5F2L%2FNygVymh7rXG2daOd28X310%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;343&quot; data-origin-width=&quot;844&quot; data-origin-height=&quot;452&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;(1) 영구 리다이렉션&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;301, 308&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;리소스의&amp;nbsp;URI가&amp;nbsp;영구적으로&amp;nbsp;이동&lt;/li&gt;
&lt;li&gt;원래의 URL를 사용X, 검색 엔진 등에서도 변경 인지&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[ 영구 리다이렉션 - 301 Moved Permanently ]&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리다이렉트시 요청 메서드가 GET으로 변하고, 본문이 제거될 수 있음(MAY)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;849&quot; data-origin-height=&quot;497&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nOuW0/btrDQErWhTA/N2s9OPbQTjkOQ2RIP5zdQ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nOuW0/btrDQErWhTA/N2s9OPbQTjkOQ2RIP5zdQ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nOuW0/btrDQErWhTA/N2s9OPbQTjkOQ2RIP5zdQ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnOuW0%2FbtrDQErWhTA%2FN2s9OPbQTjkOQ2RIP5zdQ1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;661&quot; height=&quot;387&quot; data-origin-width=&quot;849&quot; data-origin-height=&quot;497&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;[ 영구 리다이렉션 - 308 Permanent Redirect ]&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;301과&amp;nbsp;기능은&amp;nbsp;같음 &lt;br /&gt;리다이렉트시&amp;nbsp;요청&amp;nbsp;메서드와&amp;nbsp;본문&amp;nbsp;유지(처음&amp;nbsp;POST를&amp;nbsp;보내면&amp;nbsp;리다이렉트도&amp;nbsp;POST&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;855&quot; data-origin-height=&quot;502&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Ofiso/btrDPUhJ9id/hMLQi1QsHbPbYggzewKySk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Ofiso/btrDPUhJ9id/hMLQi1QsHbPbYggzewKySk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Ofiso/btrDPUhJ9id/hMLQi1QsHbPbYggzewKySk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOfiso%2FbtrDPUhJ9id%2FhMLQi1QsHbPbYggzewKySk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;389&quot; data-origin-width=&quot;855&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;&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;302,&amp;nbsp;307,&amp;nbsp;303&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;리소스의&amp;nbsp;URI가&amp;nbsp;일시적으로&amp;nbsp;변경&lt;/li&gt;
&lt;li&gt;따라서 검색 엔진 등에서 URL을 변경하면 안됨&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[ 일시적인 리다이렉션 - 302&amp;nbsp;Found&amp;nbsp;]&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리다이렉트시&amp;nbsp;요청&amp;nbsp;메서드가&amp;nbsp;GET으로&amp;nbsp;변하고,&amp;nbsp;본문이&amp;nbsp;제거될&amp;nbsp;수&amp;nbsp;있음(MAY)&lt;/p&gt;
&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; 307&amp;nbsp;Temporary&amp;nbsp;Redirect&lt;/span&gt;&amp;nbsp;]&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;302와&amp;nbsp;기능은&amp;nbsp;같음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리다이렉트시 요청 메서드와 본문 유지(요청 메서드를 변경하면 안된다. MUST NOT)&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&gt; 303&amp;nbsp;See&amp;nbsp;Other&lt;/span&gt;&amp;nbsp;]&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;302와&amp;nbsp;기능은&amp;nbsp;같음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리다이렉트시 요청 메서드가 GET으로 변경&lt;/p&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) PRG: Post/Redirect/Get&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;POST로&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 data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[ PRG 사용전 ]&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;1021&quot; data-origin-height=&quot;570&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/x1S0W/btrDPV17Qeb/SuABxIJzt9f3Cj2ms9Tpq1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/x1S0W/btrDPV17Qeb/SuABxIJzt9f3Cj2ms9Tpq1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/x1S0W/btrDPV17Qeb/SuABxIJzt9f3Cj2ms9Tpq1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fx1S0W%2FbtrDPV17Qeb%2FSuABxIJzt9f3Cj2ms9Tpq1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;372&quot; data-origin-width=&quot;1021&quot; data-origin-height=&quot;570&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[ PRG 사용 ]&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;POST로&amp;nbsp;주문후에&amp;nbsp;새로&amp;nbsp;고침으로&amp;nbsp;인한&amp;nbsp;중복&amp;nbsp;주문&amp;nbsp;방지&lt;/li&gt;
&lt;li&gt;POST로 주문후에 주문 결과 화면을 GET 메서드로 리다이렉트&lt;/li&gt;
&lt;li&gt;새로고침해도 결과 화면을 GET으로 조회&lt;/li&gt;
&lt;li&gt;중복 주문 대신에 결과 화면만 GET으로 다시 요청&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1033&quot; data-origin-height=&quot;567&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FKiAN/btrDQFxK4ds/yztMLP3SbV4iQy0TslgLC0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FKiAN/btrDQFxK4ds/yztMLP3SbV4iQy0TslgLC0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FKiAN/btrDQFxK4ds/yztMLP3SbV4iQy0TslgLC0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFKiAN%2FbtrDQFxK4ds%2FyztMLP3SbV4iQy0TslgLC0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;370&quot; data-origin-width=&quot;1033&quot; data-origin-height=&quot;567&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;307,&amp;nbsp;303을&amp;nbsp;권장하지만&amp;nbsp;현실적으로&amp;nbsp;이미&amp;nbsp;많은&amp;nbsp;애플리케이션&amp;nbsp;라이브러리들이&amp;nbsp;302를&amp;nbsp;기본값으로&amp;nbsp;사용&lt;/li&gt;
&lt;li&gt;자동 리다이렉션시에 GET으로 변해도 되면 그냥 302를 사용해도 큰 문제 없음&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;(4) 기타 리다이렉션&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;300, 304&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;[ 기타 리다이렉션 - 300 Multiple Choices ]&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;안쓴다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[ 기타 리다이렉션 - 304 Not Modified ]&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;캐시를 목적으로 사용&lt;/li&gt;
&lt;li&gt;클라이언트에게 리소스가 수정되지 않았음을 알려준다. &lt;br /&gt;따라서 클라이언트는 로컬PC에 저장된&amp;nbsp;캐시를&amp;nbsp;재사용한다.&amp;nbsp;(캐시로&amp;nbsp;리다이렉트&amp;nbsp;한다.)&lt;/li&gt;
&lt;li&gt;304 응답은 응답에 메시지 바디를 포함하면 안된다. (로컬 캐시를 사용해야 하므로)&lt;/li&gt;
&lt;li&gt;조건부 GET, HEAD 요청시 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 4xx - 클라이언트&amp;nbsp;오류&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클라이언트의&amp;nbsp;요청에&amp;nbsp;잘못된&amp;nbsp;문법등으로&amp;nbsp;서버가&amp;nbsp;요청을&amp;nbsp;수행할&amp;nbsp;수&amp;nbsp;없음&lt;/li&gt;
&lt;li&gt;오류의 원인이 클라이언트에 있음&lt;/li&gt;
&lt;li&gt;클라이언트가 이미 잘못된 요청, 데이터를 보내고 있기 때문에, 똑같은 재시도가 실패함&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;[ 400 Bad Request ]&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;클라이언트가&amp;nbsp;잘못된&amp;nbsp;요청을&amp;nbsp;해서&amp;nbsp;서버가&amp;nbsp;요청을&amp;nbsp;처리할&amp;nbsp;수&amp;nbsp;없음&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;요청&amp;nbsp;구문,&amp;nbsp;메시지&amp;nbsp;등등&amp;nbsp;오류&lt;/li&gt;
&lt;li&gt;클라이언트는 요청 내용을 다시 검토하고, 보내야함&lt;/li&gt;
&lt;li&gt;예) 요청 파라미터가 잘못되거나, API 스펙이 맞지 않을 때&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 입장에서는 전달 받는 데이터에 대한 Validation을 철저히 해서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 오류가 아닌 400으로 응답해 클라이언트에서 다시 검토할 수 있도록 해야한다.&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;span&gt; 401&amp;nbsp;Unauthorized&lt;/span&gt;&amp;nbsp;]&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;클라이언트가&amp;nbsp;해당&amp;nbsp;리소스에&amp;nbsp;대한&amp;nbsp;인증이&amp;nbsp;필요함&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인증(Authentication)&amp;nbsp;되지&amp;nbsp;않음&lt;/li&gt;
&lt;li&gt;401 오류 발생시 응답에 WWW-Authenticate 헤더와 함께 인증 방법을 설명&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;참고&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인증(Authentication): 본인이 누구인지 확인, (로그인)&lt;/li&gt;
&lt;li&gt;인가(Authorization): 권한부여 (ADMIN 권한처럼 특정 리소스에 접근할 수 있는 권한, 인증이&amp;nbsp;있어야&amp;nbsp;인가가&amp;nbsp;있음)&lt;/li&gt;
&lt;li&gt;오류 메시지가 Unauthorized 이지만 인증 되지 않음을 의미&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;[ 403&amp;nbsp;Forbidden ]&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;서버가&amp;nbsp;요청을&amp;nbsp;이해했지만&amp;nbsp;승인을&amp;nbsp;거부함&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;주로&amp;nbsp;인증&amp;nbsp;자격&amp;nbsp;증명은&amp;nbsp;있지만,&amp;nbsp;접근&amp;nbsp;권한이&amp;nbsp;불충분한&amp;nbsp;경우&lt;/li&gt;
&lt;li&gt;예) 어드민 등급이 아닌 사용자가 로그인은 했지만, 어드민 등급의 리소스에 접근하는 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;[ 404 Not Found ]&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;요청&amp;nbsp;리소스를&amp;nbsp;찾을&amp;nbsp;수&amp;nbsp;없음&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;요청&amp;nbsp;리소스가&amp;nbsp;서버에&amp;nbsp;없음&lt;/li&gt;
&lt;li&gt;또는 클라이언트가 권한이 부족한 리소스에 접근할 때 해당 리소스를 숨기고 싶을 때&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;5. 5xx -&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;서버&amp;nbsp;오류&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버&amp;nbsp;문제로&amp;nbsp;오류&amp;nbsp;발생&lt;/li&gt;
&lt;li&gt;서버에 문제가 있기 때문에 재시도 하면 성공할 수도 있음(복구가 되거나 등등)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;[ 500 Internal Server Error ]&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;서버&amp;nbsp;문제로&amp;nbsp;오류&amp;nbsp;발생,&amp;nbsp;애매하면&amp;nbsp;500&amp;nbsp;오류&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버&amp;nbsp;내부&amp;nbsp;문제로&amp;nbsp;오류&amp;nbsp;발생&lt;/li&gt;
&lt;li&gt;애매하면 500 오류&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;[ 503 Service Unavailable ]&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;서비스&amp;nbsp;이용&amp;nbsp;불가&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버가&amp;nbsp;일시적인&amp;nbsp;과부하&amp;nbsp;또는&amp;nbsp;예정된&amp;nbsp;작업으로&amp;nbsp;잠시&amp;nbsp;요청을&amp;nbsp;처리할&amp;nbsp;수&amp;nbsp;없음&lt;/li&gt;
&lt;li&gt;Retry-After 헤더 필드로 얼마뒤에 복구되는지 보낼 수도 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5xx는 서버에 문제가 발생했다는 의미이기 때문에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비즈니스 예외를 발생시키는 경우 5xx을 사용하지 않아야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비즈니스 에외는 2xx, 4xx를 활용하자.&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;출처&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;a href=&quot;https://www.inflearn.com/course/http-%EC%9B%B9-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC&quot;&gt;모든 개발자를 위한 HTTP 웹 기본 지식(김영한)&lt;/a&gt;&lt;/p&gt;</description>
      <category>HTTP</category>
      <category>HTTP 상태 코드</category>
      <author>mandykr</author>
      <guid isPermaLink="true">https://mandykr.tistory.com/83</guid>
      <comments>https://mandykr.tistory.com/83#entry83comment</comments>
      <pubDate>Fri, 3 Jun 2022 13:59:07 +0900</pubDate>
    </item>
    <item>
      <title>[HTTP] 3. HTTP 메서드</title>
      <link>https://mandykr.tistory.com/82</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;목차&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. HTTP API&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. HTTP 메서드&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. HTTP 메서드 속성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. HTTP&amp;nbsp;메서드&amp;nbsp;활용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. HTTP API&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좋은 URI는 행위는 배제한 채 리소스를 식별하고 계층 구조를 활용해 &lt;span&gt;API URI를 설계하는 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;회원&amp;nbsp;목록&amp;nbsp;조회&amp;nbsp;/members&lt;/li&gt;
&lt;li&gt;회원 조회 /members/{id}&lt;/li&gt;
&lt;li&gt;회원 등록 /members/{id}&lt;/li&gt;
&lt;li&gt;회원 수정 /members/{id}&lt;/li&gt;
&lt;li&gt;회원 삭제 /members/{id}&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[ 리소스 ]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회원&lt;/p&gt;
&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;계층 구조의 URI에 행위를 의미하는 HTTP 메서드를 추가해 API를 구분할 수 있다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. HTTP 메서드&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;주요 메서드&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;GET:&amp;nbsp;리소스&amp;nbsp;조회&lt;/li&gt;
&lt;li&gt;POST: 요청 데이터 처리, 주로 등록에 사용&lt;/li&gt;
&lt;li&gt;PUT: 리소스를 대체, 해당 리소스가 없으면 생성&lt;/li&gt;
&lt;li&gt;PATCH: 리소스 부분 변경&lt;/li&gt;
&lt;li&gt;DELETE: 리소스 삭제&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;기타&amp;nbsp;메서드&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HEAD:&amp;nbsp;GET과&amp;nbsp;동일하지만&amp;nbsp;메시지&amp;nbsp;부분을&amp;nbsp;제외하고,&amp;nbsp;상태&amp;nbsp;줄과&amp;nbsp;헤더만&amp;nbsp;반환&lt;/li&gt;
&lt;li&gt;OPTIONS: 대상 리소스에 대한 통신 가능 옵션(메서드)을 설명(주로 CORS에서 사용)&lt;/li&gt;
&lt;li&gt;CONNECT: 대상 자원으로 식별되는 서버에 대한 터널을 설정&lt;/li&gt;
&lt;li&gt;TRACE: 대상 리소스에 대한 경로를 따라 메시지 루프백 테스트를 수행&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) GET&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;502&quot; data-origin-height=&quot;131&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QggpV/btrDQd0NXQ1/0VkcOpoTXinWrdOTSBUzSk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QggpV/btrDQd0NXQ1/0VkcOpoTXinWrdOTSBUzSk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QggpV/btrDQd0NXQ1/0VkcOpoTXinWrdOTSBUzSk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQggpV%2FbtrDQd0NXQ1%2F0VkcOpoTXinWrdOTSBUzSk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;119&quot; data-origin-width=&quot;502&quot; data-origin-height=&quot;131&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;리소스&amp;nbsp;조회&lt;/li&gt;
&lt;li&gt;서버에 전달하고 싶은 데이터는 query(쿼리 파라미터, 쿼리 스트링)를 통해서 전달&lt;/li&gt;
&lt;li&gt;메시지 바디를 사용해서 데이터를 전달할 수 있지만, 지원하지 않는 곳이 많아서 권장하지 않음&lt;/li&gt;
&lt;/ul&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-origin-width=&quot;863&quot; data-origin-height=&quot;358&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dqRge7/btrDF97ggcx/vLADgDWPYY4l3k9qKuq0N1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dqRge7/btrDF97ggcx/vLADgDWPYY4l3k9qKuq0N1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dqRge7/btrDF97ggcx/vLADgDWPYY4l3k9qKuq0N1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdqRge7%2FbtrDF97ggcx%2FvLADgDWPYY4l3k9qKuq0N1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;250&quot; data-origin-width=&quot;863&quot; data-origin-height=&quot;358&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;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;795&quot; data-origin-height=&quot;378&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/y7HMp/btrDPUmR6P1/nqeB7KppAKXSsVzex1V7L1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/y7HMp/btrDPUmR6P1/nqeB7KppAKXSsVzex1V7L1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/y7HMp/btrDPUmR6P1/nqeB7KppAKXSsVzex1V7L1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fy7HMp%2FbtrDPUmR6P1%2FnqeB7KppAKXSsVzex1V7L1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;285&quot; data-origin-width=&quot;795&quot; data-origin-height=&quot;378&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2) POST&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;327&quot; data-origin-height=&quot;155&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cNH38n/btrDQdT3NTd/REgaiw56elpjxSdFc1pACk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cNH38n/btrDQdT3NTd/REgaiw56elpjxSdFc1pACk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cNH38n/btrDQdT3NTd/REgaiw56elpjxSdFc1pACk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcNH38n%2FbtrDQdT3NTd%2FREgaiw56elpjxSdFc1pACk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;300&quot; height=&quot;142&quot; data-origin-width=&quot;327&quot; data-origin-height=&quot;155&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;요청&amp;nbsp;데이터&amp;nbsp;처리&lt;/li&gt;
&lt;li&gt;메시지 바디를 통해 서버로 요청 데이터 전달&lt;/li&gt;
&lt;li&gt;서버는 요청 데이터를 처리 &lt;br /&gt;메시지 바디를 통해 들어온 데이터를 처리하는 모든 기능을 수행한다.&lt;/li&gt;
&lt;li&gt;주로 전달된 데이터로 신규 리소스 등록, 프로세스 처리에 사용&lt;/li&gt;
&lt;/ul&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;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;756&quot; data-origin-height=&quot;359&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/beZ29M/btrDLWGvaBd/tgwCGyl5qMx871FJ0lo8iK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/beZ29M/btrDLWGvaBd/tgwCGyl5qMx871FJ0lo8iK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/beZ29M/btrDLWGvaBd/tgwCGyl5qMx871FJ0lo8iK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbeZ29M%2FbtrDLWGvaBd%2FtgwCGyl5qMx871FJ0lo8iK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;272&quot; data-origin-width=&quot;756&quot; data-origin-height=&quot;359&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[ 응답 ]&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;795&quot; data-origin-height=&quot;388&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/UKPGz/btrDMzYaoWI/P47vy8Tka2fSRXbcOmmUak/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/UKPGz/btrDMzYaoWI/P47vy8Tka2fSRXbcOmmUak/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/UKPGz/btrDMzYaoWI/P47vy8Tka2fSRXbcOmmUak/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUKPGz%2FbtrDMzYaoWI%2FP47vy8Tka2fSRXbcOmmUak%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;293&quot; data-origin-width=&quot;795&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;POST 사용&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리소스 URI에 POST 요청이 오면 요청 데이터를 어떻게 처리할지 리소스마다 따로 정해야 한다.&lt;/p&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.&amp;nbsp;새&amp;nbsp;리소스&amp;nbsp;생성(등록)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버가 아직 식별하지 않은 새 리소스 생성&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 요청 데이터 처리&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;단순히 데이터를 생성하거나, 변경하는 것을 넘어서 프로세스를 처리해야 하는 경우&lt;br /&gt;예) 주문에서 결제완료 -&amp;gt; 배달시작 -&amp;gt; 배달완료 처럼 단순히 값 변경을 넘어 프로세스의 상태가 변경되는 경우&lt;/li&gt;
&lt;li&gt;POST의 결과로 새로운 리소스가 생성되지 않을 수도 있음&lt;br /&gt;예) POST /orders/{orderId}/start-delivery &lt;b&gt;(컨트롤 URI)&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 다른 메서드로 처리하기 애매한 경우&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예) JSON으로 조회 데이터를 넘겨야 하는데, GET 메서드를 사용하기 어려운 경우&lt;/li&gt;
&lt;li&gt;애매하면 POST&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3) PUT&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;325&quot; data-origin-height=&quot;156&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ReC2W/btrDKKGmAY9/hRVtXMbziVIFbCwstggiCK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ReC2W/btrDKKGmAY9/hRVtXMbziVIFbCwstggiCK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ReC2W/btrDKKGmAY9/hRVtXMbziVIFbCwstggiCK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FReC2W%2FbtrDKKGmAY9%2FhRVtXMbziVIFbCwstggiCK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;325&quot; height=&quot;156&quot; data-origin-width=&quot;325&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;리소스를 대체&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;리소스가 있으면 대체&lt;/li&gt;
&lt;li&gt;리소스가 없으면 생성&lt;/li&gt;
&lt;li&gt;쉽게 이야기해서 덮어버림&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트가 리소스를 식별&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클라이언트가 리소스 위치를 알고 URI 지정&lt;/li&gt;
&lt;li&gt;POST와 차이점&lt;/li&gt;
&lt;/ul&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-origin-width=&quot;859&quot; data-origin-height=&quot;358&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bNjOKR/btrDPI1bKlz/gkaGEqj47MwZcNSW434Z90/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bNjOKR/btrDPI1bKlz/gkaGEqj47MwZcNSW434Z90/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bNjOKR/btrDPI1bKlz/gkaGEqj47MwZcNSW434Z90/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbNjOKR%2FbtrDPI1bKlz%2FgkaGEqj47MwZcNSW434Z90%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;276&quot; data-origin-width=&quot;859&quot; data-origin-height=&quot;358&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;798&quot; data-origin-height=&quot;384&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/csEoni/btrDPIz7VEQ/PPjanzj3v7ycnm4NK1zeJK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/csEoni/btrDPIz7VEQ/PPjanzj3v7ycnm4NK1zeJK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/csEoni/btrDPIz7VEQ/PPjanzj3v7ycnm4NK1zeJK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcsEoni%2FbtrDPIz7VEQ%2FPPjanzj3v7ycnm4NK1zeJK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;588&quot; height=&quot;283&quot; data-origin-width=&quot;798&quot; data-origin-height=&quot;384&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;서버에서 리소스를 &lt;span&gt;json 문서로 다루고 있는 경우 필드가 삭제되고&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Framework를 사용할떄처럼 json을 객체로 변환해 다루는 경우 객체의 필드에 기본값이 저장된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;883&quot; data-origin-height=&quot;366&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b57lWu/btrDNUBtMVf/T8TIrl6SdVqbeSgW1GuWQ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b57lWu/btrDNUBtMVf/T8TIrl6SdVqbeSgW1GuWQ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b57lWu/btrDNUBtMVf/T8TIrl6SdVqbeSgW1GuWQ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb57lWu%2FbtrDNUBtMVf%2FT8TIrl6SdVqbeSgW1GuWQ1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;638&quot; height=&quot;264&quot; data-origin-width=&quot;883&quot; data-origin-height=&quot;366&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;812&quot; data-origin-height=&quot;386&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dOwU6Y/btrDKInBPG8/K3kSt5t4iAQXbjdl0VRPxk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dOwU6Y/btrDKInBPG8/K3kSt5t4iAQXbjdl0VRPxk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dOwU6Y/btrDKInBPG8/K3kSt5t4iAQXbjdl0VRPxk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdOwU6Y%2FbtrDKInBPG8%2FK3kSt5t4iAQXbjdl0VRPxk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;279&quot; data-origin-width=&quot;812&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;&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;774&quot; data-origin-height=&quot;385&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mE2se/btrDQxx6W0c/Akt0W4VyPO3ReG46o3Spx0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mE2se/btrDQxx6W0c/Akt0W4VyPO3ReG46o3Spx0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mE2se/btrDQxx6W0c/Akt0W4VyPO3ReG46o3Spx0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmE2se%2FbtrDQxx6W0c%2FAkt0W4VyPO3ReG46o3Spx0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;295&quot; data-origin-width=&quot;774&quot; data-origin-height=&quot;385&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;813&quot; data-origin-height=&quot;395&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bGcIaO/btrDQeS1VNu/D9oDq13NP4Olxi5R2fuX70/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bGcIaO/btrDQeS1VNu/D9oDq13NP4Olxi5R2fuX70/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bGcIaO/btrDQeS1VNu/D9oDq13NP4Olxi5R2fuX70/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbGcIaO%2FbtrDQeS1VNu%2FD9oDq13NP4Olxi5R2fuX70%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;607&quot; height=&quot;295&quot; data-origin-width=&quot;813&quot; data-origin-height=&quot;395&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;h4 data-ke-size=&quot;size20&quot;&gt;4) PATCH&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;리소스&amp;nbsp;부분&amp;nbsp;변경&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;881&quot; data-origin-height=&quot;364&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2DMTx/btrDQxEUYM5/EigpsJU6NNweAXrjNkpPg1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2DMTx/btrDQxEUYM5/EigpsJU6NNweAXrjNkpPg1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2DMTx/btrDQxEUYM5/EigpsJU6NNweAXrjNkpPg1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2DMTx%2FbtrDQxEUYM5%2FEigpsJU6NNweAXrjNkpPg1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;272&quot; data-origin-width=&quot;881&quot; data-origin-height=&quot;364&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;814&quot; data-origin-height=&quot;383&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7Yw8t/btrDPVTK9Fz/eB51qHuIaViVbUldJF04k1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7Yw8t/btrDPVTK9Fz/eB51qHuIaViVbUldJF04k1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7Yw8t/btrDPVTK9Fz/eB51qHuIaViVbUldJF04k1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7Yw8t%2FbtrDPVTK9Fz%2FeB51qHuIaViVbUldJF04k1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;276&quot; data-origin-width=&quot;814&quot; data-origin-height=&quot;383&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;h4 data-ke-size=&quot;size20&quot;&gt;5) DELETE&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;리소스&amp;nbsp;제거&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;853&quot; data-origin-height=&quot;369&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/46QEb/btrDKKzEAbx/fvyMhJhqZ1Dz95Cg7TXt2K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/46QEb/btrDKKzEAbx/fvyMhJhqZ1Dz95Cg7TXt2K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/46QEb/btrDKKzEAbx/fvyMhJhqZ1Dz95Cg7TXt2K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F46QEb%2FbtrDKKzEAbx%2FfvyMhJhqZ1Dz95Cg7TXt2K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;270&quot; data-origin-width=&quot;853&quot; data-origin-height=&quot;369&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. HTTP 메서드 속성&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) 안전(Safe)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;호출해도 리소스를 변경하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GET, HEAD&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;2) 멱등(Idempotent)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;얼마든지 호출해도 결과가 똑같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;멱등은 외부 요인으로 중간에 리소스가 변경되는 것 까지는 고려하지는 않는다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;GET:&amp;nbsp;한&amp;nbsp;번&amp;nbsp;조회하든,&amp;nbsp;두&amp;nbsp;번&amp;nbsp;조회하든&amp;nbsp;같은&amp;nbsp;결과가&amp;nbsp;조회된다.&lt;/li&gt;
&lt;li&gt;PUT: 결과를 대체한다. 따라서 같은 요청을 여러번 해도 최종 결과는 같다.&lt;/li&gt;
&lt;li&gt;DELETE: 결과를 삭제한다. 같은 요청을 여러번 해도 삭제된 결과는 똑같다.&lt;/li&gt;
&lt;li&gt;POST: 멱등이 아니다! 두 번 호출하면 같은 결제가 중복해서 발생할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;활용&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;자동 복구 메커니즘&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3) 캐시 가능(Cacheable)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;응답 결과 리소스를 캐시해서 사용해도 되는가?&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;GET,&amp;nbsp;HEAD,&amp;nbsp;POST,&amp;nbsp;PATCH&amp;nbsp;캐시가능&lt;/li&gt;
&lt;li&gt;실제로는 GET, HEAD 정도만 캐시로 사용 &lt;br /&gt;POST, PATCH는 본문 내용까지 캐시 키로 고려해야 하는데, 구현이 쉽지 않음&lt;/li&gt;
&lt;/ul&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;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;4.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;HTTP&amp;nbsp;메서드&amp;nbsp;활용&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) 클라이언트에서 서버로 데이터 전송&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;쿼리 파라미터를 통한 데이터 전송&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;GET&lt;/li&gt;
&lt;li&gt;주로 정렬 필터(검색어)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;메시지 바디를 통한 데이터 전송&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;POST, PUT, PATCH&lt;/li&gt;
&lt;li&gt;회원 가입, 상품 주문, 리소스 등록, 리소스 변경&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;(1) 정적 데이터 조회&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;978&quot; data-origin-height=&quot;374&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/55PDD/btrDPjotXGr/5yiKUIRs7UDStd3qVRwqYk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/55PDD/btrDPjotXGr/5yiKUIRs7UDStd3qVRwqYk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/55PDD/btrDPjotXGr/5yiKUIRs7UDStd3qVRwqYk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F55PDD%2FbtrDPjotXGr%2F5yiKUIRs7UDStd3qVRwqYk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;588&quot; height=&quot;225&quot; data-origin-width=&quot;978&quot; data-origin-height=&quot;374&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이미지,&amp;nbsp;정적&amp;nbsp;텍스트&amp;nbsp;문서&lt;/li&gt;
&lt;li&gt;조회는&amp;nbsp;GET&amp;nbsp;사용&lt;/li&gt;
&lt;li&gt;정적 데이터는 일반적으로 쿼리 파라미터 없이 리소스 경로로 단순하게 조회가능&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;(2) 동적 데이터 조회&lt;br /&gt;&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1004&quot; data-origin-height=&quot;401&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b9furo/btrDTRcd5BS/rqBSYGdXKpdLVmGv3Lujc0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b9furo/btrDTRcd5BS/rqBSYGdXKpdLVmGv3Lujc0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b9furo/btrDTRcd5BS/rqBSYGdXKpdLVmGv3Lujc0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb9furo%2FbtrDTRcd5BS%2FrqBSYGdXKpdLVmGv3Lujc0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;231&quot; data-origin-width=&quot;1004&quot; data-origin-height=&quot;401&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;주로&amp;nbsp;검색,&amp;nbsp;게시판&amp;nbsp;목록에서&amp;nbsp;정렬&amp;nbsp;필터(검색어) &lt;/li&gt;
&lt;li&gt;조회 조건을 줄여주는 필터, 조회 결과를 정렬하는 정렬 조건에 주로 사용 &lt;/li&gt;
&lt;li&gt;조회는 GET 사용 &lt;/li&gt;
&lt;li&gt;GET은 쿼리 파라미터 사용해서 데이터를 전달&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;(3) HTML Form 데이터 전송&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&amp;nbsp;HTML&amp;nbsp;Form&amp;nbsp;전송은&amp;nbsp;GET,&amp;nbsp;POST만&amp;nbsp;지원&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[ POST 전송 - 저장 ]&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;994&quot; data-origin-height=&quot;390&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9PYKg/btrDKIu7DJv/iwI6cmoyP5mlfScRLXgml0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9PYKg/btrDKIu7DJv/iwI6cmoyP5mlfScRLXgml0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9PYKg/btrDKIu7DJv/iwI6cmoyP5mlfScRLXgml0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9PYKg%2FbtrDKIu7DJv%2FiwI6cmoyP5mlfScRLXgml0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;234&quot; data-origin-width=&quot;994&quot; data-origin-height=&quot;390&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;회원 가입, 상품 주문, 데이터 변경&lt;/li&gt;
&lt;li&gt;Content-Type:&amp;nbsp;application/x-www-form-urlencoded
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;form의&amp;nbsp;내용을&amp;nbsp;메시지&amp;nbsp;바디를&amp;nbsp;통해서&amp;nbsp;전송(key=value,&amp;nbsp;쿼리&amp;nbsp;파라미터&amp;nbsp;형식)&lt;/li&gt;
&lt;li&gt;전송&amp;nbsp;데이터를&amp;nbsp;url&amp;nbsp;encoding&amp;nbsp;처리 &lt;br /&gt;&amp;bull;&amp;nbsp;예)&amp;nbsp;abc김&amp;nbsp;-&amp;gt;&amp;nbsp;abc%EA%B9%80&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[ GET 전송 - 저장 ]&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;964&quot; data-origin-height=&quot;373&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mPxPs/btrDPYKgW1o/SlxlVCE3YEQJHAS7nGtiE1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mPxPs/btrDPYKgW1o/SlxlVCE3YEQJHAS7nGtiE1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mPxPs/btrDPYKgW1o/SlxlVCE3YEQJHAS7nGtiE1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmPxPs%2FbtrDPYKgW1o%2FSlxlVCE3YEQJHAS7nGtiE1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;625&quot; height=&quot;242&quot; data-origin-width=&quot;964&quot; data-origin-height=&quot;373&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;GET은 웹 브라우저가 form 형식의 데이터를 쿼리 파라미터로 변경한다.&lt;/li&gt;
&lt;li&gt;GET은 조회에서만 사용해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[ GET 전송 - 조회 ]&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;964&quot; data-origin-height=&quot;368&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kDlgx/btrDRNn6BoX/KClQhnEiTkQ0NyM15ylv41/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kDlgx/btrDRNn6BoX/KClQhnEiTkQ0NyM15ylv41/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kDlgx/btrDRNn6BoX/KClQhnEiTkQ0NyM15ylv41/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkDlgx%2FbtrDRNn6BoX%2FKClQhnEiTkQ0NyM15ylv41%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;650&quot; height=&quot;248&quot; data-origin-width=&quot;964&quot; data-origin-height=&quot;368&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;GET은 웹 브라우저가 form 형식의 데이터를 쿼리 파라미터로 변경한다.&lt;/li&gt;
&lt;li&gt;조회를 위한 Form 데이터 전송&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;[ multipart/form-data ]&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;981&quot; data-origin-height=&quot;443&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ck5AC6/btrDPVObgz0/N4buekHkxKewlAWxPvbtA0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ck5AC6/btrDPVObgz0/N4buekHkxKewlAWxPvbtA0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ck5AC6/btrDPVObgz0/N4buekHkxKewlAWxPvbtA0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fck5AC6%2FbtrDPVObgz0%2FN4buekHkxKewlAWxPvbtA0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;663&quot; height=&quot;299&quot; data-origin-width=&quot;981&quot; data-origin-height=&quot;443&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;파일 업로드 같은 바이너리 데이터 전송시 사용&lt;/li&gt;
&lt;li&gt;다른&amp;nbsp;종류의&amp;nbsp;여러&amp;nbsp;파일과&amp;nbsp;폼의&amp;nbsp;내용&amp;nbsp;함께&amp;nbsp;전송&amp;nbsp;가능&lt;/li&gt;
&lt;/ul&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;(4) HTTP API 데이터 전송&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;854&quot; data-origin-height=&quot;380&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/N5fYl/btrDTQRWkON/Bi9mSShQRKYSsj63NzVwLk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/N5fYl/btrDTQRWkON/Bi9mSShQRKYSsj63NzVwLk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/N5fYl/btrDTQRWkON/Bi9mSShQRKYSsj63NzVwLk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FN5fYl%2FbtrDTQRWkON%2FBi9mSShQRKYSsj63NzVwLk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;712&quot; height=&quot;317&quot; data-origin-width=&quot;854&quot; data-origin-height=&quot;380&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버&amp;nbsp;to&amp;nbsp;서버 &lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;백엔드 시스템 통신 &lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;앱 클라이언트 &lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;아이폰, 안드로이드 &lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;웹 클라이언트 &lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HTML에서 Form 전송 대신 자바 스크립트를 통한 통신에 사용(AJAX) &lt;/li&gt;
&lt;li&gt;예) React, VueJs 같은 웹 클라이언트와 API 통신 &lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;POST, PUT, PATCH: 메시지 바디를 통해 데이터 전송 &lt;/li&gt;
&lt;li&gt;GET: 조회, 쿼리 파라미터로 데이터 전달 &lt;/li&gt;
&lt;li&gt;Content-Type: application/json을 주로 사용 (사실상 표준) &lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;TEXT, XML, JSON 등등&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2) HTTP API 설계&lt;/h4&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) POST 기반 등록&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;회원&amp;nbsp;목록&amp;nbsp;/members&amp;nbsp;-&amp;gt;&amp;nbsp;GET &lt;/li&gt;
&lt;li&gt;회원 등록 /members -&amp;gt; POST &lt;/li&gt;
&lt;li&gt;회원 조회 /members/{id} -&amp;gt; GET &lt;/li&gt;
&lt;li&gt;회원 수정 /members/{id} -&amp;gt; PATCH, PUT, POST &lt;/li&gt;
&lt;li&gt;회원 삭제 /members/{id} -&amp;gt; DELETE&lt;/li&gt;
&lt;/ul&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;1. 클라이언트는 등록될 리소스의 URI를 모른다. &lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;회원 등록 /members -&amp;gt; POST &lt;/li&gt;
&lt;li&gt;POST /members&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 서버가 새로 등록된 리소스 URI를 생성해준다. &lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HTTP/1.1 201 Created &lt;br /&gt;Location:&amp;nbsp;/members/100 &lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 컬렉션(Collection) &lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버가 관리하는 리소스 디렉토리 &lt;/li&gt;
&lt;li&gt;서버가 리소스의 URI를 생성하고 관리 &lt;/li&gt;
&lt;li&gt;여기서 컬렉션은 /members&lt;/li&gt;
&lt;/ul&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;(2) PUT 기반 등록&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;파일&amp;nbsp;목록&amp;nbsp;/files&amp;nbsp;-&amp;gt;&amp;nbsp;GET &lt;/li&gt;
&lt;li&gt;파일 조회 /files/{filename} -&amp;gt; GET &lt;/li&gt;
&lt;li&gt;파일 등록 /files/{filename} -&amp;gt; PUT &lt;/li&gt;
&lt;li&gt;파일 삭제 /files/{filename} -&amp;gt; DELETE &lt;/li&gt;
&lt;li&gt;파일 대량 등록 /files -&amp;gt; POST&lt;/li&gt;
&lt;/ul&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;1. 클라이언트가 리소스 URI를 알고 있어야 한다. &lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;파일 등록 /files/{filename} -&amp;gt; PUT &lt;/li&gt;
&lt;li&gt;PUT /files/star.jpg &lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 클라이언트가 직접 리소스의 URI를 지정한다. &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 스토어(Store) &lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클라이언트가 관리하는 리소스 저장소 &lt;/li&gt;
&lt;li&gt;클라이언트가 리소스의 URI를 알고 관리 &lt;/li&gt;
&lt;li&gt;여기서 스토어는 /files&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;(3) HTML FORM 사용&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;회원 목록&amp;nbsp; &amp;nbsp; &amp;nbsp; /members -&amp;gt; GET &lt;/li&gt;
&lt;li&gt;회원 등록 폼 /members/new -&amp;gt; GET &lt;/li&gt;
&lt;li&gt;회원 등록&amp;nbsp; &amp;nbsp; &amp;nbsp; /members/new, /members -&amp;gt; POST &lt;/li&gt;
&lt;li&gt;회원 조회&amp;nbsp; &amp;nbsp; &amp;nbsp; /members/{id} -&amp;gt; GET &lt;/li&gt;
&lt;li&gt;회원 수정 폼 /members/{id}/edit -&amp;gt; GET &lt;/li&gt;
&lt;li&gt;회원 수정&amp;nbsp; &amp;nbsp; &amp;nbsp; /members/{id}/edit, /members/{id} -&amp;gt; POST &lt;/li&gt;
&lt;li&gt;회원 삭제&amp;nbsp; &amp;nbsp; &amp;nbsp; /members/{id}/delete -&amp;gt; POST&lt;/li&gt;
&lt;/ul&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;1. HTML FORM은 GET, POST만 지원 &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 컨트롤 URI &lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;GET, POST만 지원하므로 제약이 있음 &lt;/li&gt;
&lt;li&gt;이런 제약을 해결하기 위해 동사로 된 리소스 경로 사용 &lt;/li&gt;
&lt;li&gt;POST의 /new, /edit, /delete가 컨트롤 URI &lt;/li&gt;
&lt;li&gt;HTTP 메서드로 해결하기 애매한 경우 사용(HTTP API 포함)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;(4) 참고하면&amp;nbsp;좋은&amp;nbsp;URI&amp;nbsp;설계&amp;nbsp;개념&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://restfulapi.net/resource-naming/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://restfulapi.net/resource-naming/&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;1. 문서(document) &lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;단일 개념(파일 하나, 객체 인스턴스, 데이터베이스 row) &lt;/li&gt;
&lt;li&gt;예) /members/100, /files/star.jpg &lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 컬렉션(collection) &lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버가 관리하는 리소스 디렉터리 &lt;/li&gt;
&lt;li&gt;서버가 리소스의 URI를 생성하고 관리&lt;/li&gt;
&lt;li&gt;예) /members&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 스토어(store) &lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클라이언트가 관리하는 자원 저장소 &lt;/li&gt;
&lt;li&gt;클라이언트가 리소스의 URI를 알고 관리 &lt;/li&gt;
&lt;li&gt;예) /files &lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 컨트롤러(controller), 컨트롤 URI &lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;문서, 컬렉션, 스토어로 해결하기 어려운 추가 프로세스 실행 &lt;/li&gt;
&lt;li&gt;동사를 직접 사용 &lt;/li&gt;
&lt;li&gt;예) /members/{id}/delete&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컬렉션과 문서로 최대한 해결하고 복잡한 API를 설계할 때 컨트롤 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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;출처&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;a href=&quot;https://www.inflearn.com/course/http-%EC%9B%B9-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC&quot;&gt;모든 개발자를 위한 HTTP 웹 기본 지식(김영한)&lt;/a&gt;&lt;/p&gt;</description>
      <category>HTTP</category>
      <category>Delete</category>
      <category>Get</category>
      <category>HTTP 메서드</category>
      <category>Post</category>
      <category>Put</category>
      <author>mandykr</author>
      <guid isPermaLink="true">https://mandykr.tistory.com/82</guid>
      <comments>https://mandykr.tistory.com/82#entry82comment</comments>
      <pubDate>Thu, 2 Jun 2022 17:09:23 +0900</pubDate>
    </item>
    <item>
      <title>[HTTP] 2. HTTP 기본</title>
      <link>https://mandykr.tistory.com/81</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;목차&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. HTTP&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. HTTP 특징&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; 1) 클라이언트 서버 구조&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; 2) 무상태 프로토콜&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; 3) 비 연결성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; 4) HTTP 메시지&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. HTTP&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HyperText Transfer Protocol&lt;/p&gt;
&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;HTTP 메시지에 모든 것을 전송&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에는 HyperText를 통한 연결과정에서 HTTP를 사용했지만 현재는 다른 여러 곳에서도 사용이 가능하다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HTML,&amp;nbsp;TEXT&lt;/li&gt;
&lt;li&gt;MAGE, 음성, 영상, 파일&lt;/li&gt;
&lt;li&gt;JSON, XML (API)&lt;/li&gt;
&lt;li&gt;거의 모든 형태의 데이터 전송 가능&lt;/li&gt;
&lt;li&gt;서버간에 데이터를 주고 받을 때도 대부분 HTTP 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;HTTP 역사&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HTTP/0.9&amp;nbsp;1991년:&amp;nbsp;GET&amp;nbsp;메서드만&amp;nbsp;지원,&amp;nbsp;HTTP&amp;nbsp;헤더X&lt;/li&gt;
&lt;li&gt;HTTP/1.0 1996년: 메서드, 헤더 추가&lt;/li&gt;
&lt;li&gt;HTTP/1.1 1997년: 가장 많이 사용, 우리에게 가장 중요한 버전 &lt;br /&gt;RFC2068 (1997) -&amp;gt; RFC2616 (1999) -&amp;gt; RFC7230~7235 (2014)&lt;/li&gt;
&lt;li&gt;HTTP/2 2015년: 성능 개선&lt;/li&gt;
&lt;li&gt;HTTP/3 진행중: TCP 대신에 UDP 사용, 성능 개선&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;기반 프로토콜&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;TCP:&amp;nbsp;HTTP/1.1,&amp;nbsp;HTTP/2&lt;/li&gt;
&lt;li&gt;UDP: HTTP/3&lt;/li&gt;
&lt;li&gt;현재 HTTP/1.1 주로 사용 &lt;br /&gt;HTTP/2, HTTP/3 도 점점 증가&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. HTTP 특징&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클라이언트 서버 구조&lt;/li&gt;
&lt;li&gt;무상태 프로토콜(스테이스리스), 비연결성&lt;/li&gt;
&lt;li&gt;HTTP 메시지&lt;/li&gt;
&lt;li&gt;단순함, 확장 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) 클라이언트 서버 구조&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Request / Response 구조&lt;/li&gt;
&lt;li&gt;클라이언트는 서버에 요청을 보내고, 응답을 대기&lt;/li&gt;
&lt;li&gt;서버가 요청에 대한 결과를 만들어서 응답&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;637&quot; data-origin-height=&quot;143&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wlc7T/btrDPkMr6Pt/RBRXlNcSySfIrh9bjjubX0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wlc7T/btrDPkMr6Pt/RBRXlNcSySfIrh9bjjubX0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wlc7T/btrDPkMr6Pt/RBRXlNcSySfIrh9bjjubX0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fwlc7T%2FbtrDPkMr6Pt%2FRBRXlNcSySfIrh9bjjubX0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;584&quot; height=&quot;131&quot; data-origin-width=&quot;637&quot; data-origin-height=&quot;143&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트와 서버가 각자의 책임에 집중해 독립적으로 진화할 수 있도록 한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클라이언트는 UI에 집중&lt;/li&gt;
&lt;li&gt;서버는 비즈니스 로직에 집중(아키텍처, 트래픽 처리 ...)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2) 무상태 프로토콜&lt;/h4&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;장점: 서버 확장성 높음(스케일 아웃)&lt;/li&gt;
&lt;li&gt;단점: 클라이언트가 추가 데이터 전송&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;상태 유지 -Stateful&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;상태를 서버에서 갖고있기 때문에 항상 같은 서버가 유지되어야 한다.&lt;/li&gt;
&lt;li&gt;다른 서버로 변경이 불가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;786&quot; data-origin-height=&quot;435&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b26Jhd/btrDOyK2aR3/O2toXHK094RK3pRymuIpDk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b26Jhd/btrDOyK2aR3/O2toXHK094RK3pRymuIpDk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b26Jhd/btrDOyK2aR3/O2toXHK094RK3pRymuIpDk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb26Jhd%2FbtrDOyK2aR3%2FO2toXHK094RK3pRymuIpDk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;363&quot; data-origin-width=&quot;786&quot; data-origin-height=&quot;435&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;무상태 - Stateless&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버로 요청할 때 클라이언트에서 상태를 함께 전달한다.&lt;/li&gt;
&lt;li&gt;다른 서버로 변경되어도 문제가 발생하지 않는다.&lt;/li&gt;
&lt;li&gt;무상태는 응답 서버를 쉽게 바꿀 수 있다. -&amp;gt; 무한한 서버 증설 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;786&quot; data-origin-height=&quot;442&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yIMF9/btrDOyqJhtg/OndQivPa1bvN82JiqMrEyK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yIMF9/btrDOyqJhtg/OndQivPa1bvN82JiqMrEyK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yIMF9/btrDOyqJhtg/OndQivPa1bvN82JiqMrEyK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyIMF9%2FbtrDOyqJhtg%2FOndQivPa1bvN82JiqMrEyK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;647&quot; height=&quot;364&quot; data-origin-width=&quot;786&quot; data-origin-height=&quot;442&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;무상태(Stateless) - 스케일 아웃&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-origin-width=&quot;807&quot; data-origin-height=&quot;442&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cQjWkX/btrDIopDx8b/QUYNujBInumqo8cWkGeWh0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cQjWkX/btrDIopDx8b/QUYNujBInumqo8cWkGeWh0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cQjWkX/btrDIopDx8b/QUYNujBInumqo8cWkGeWh0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcQjWkX%2FbtrDIopDx8b%2FQUYNujBInumqo8cWkGeWh0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;365&quot; data-origin-width=&quot;807&quot; data-origin-height=&quot;442&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;무상태(Stateless) - 실무 한계&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예) 로그인이 필요 없는 단순한 서비스 소개 화면&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상태 유지&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예) 로그인&lt;/li&gt;
&lt;li&gt;로그인한 사용자의 경우 로그인 했다는 상태를 서버에 유지&lt;/li&gt;
&lt;li&gt;일반적으로 브라우저 쿠키와 서버 세션등을 사용해서 상태 유지&lt;/li&gt;
&lt;li&gt;상태 유지는 최소한만 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3) 비 연결성&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HTTP는&amp;nbsp;기본이&amp;nbsp;연결을&amp;nbsp;유지하지&amp;nbsp;않는&amp;nbsp;모델&lt;/li&gt;
&lt;li&gt;일반적으로 초 단위의 이하의 빠른 속도로 응답&lt;/li&gt;
&lt;li&gt;1시간 동안 수천명이 서비스를 사용해도 실제 서버에서 동시에 처리하는 요청은 수십개 이하로&amp;nbsp;매우&amp;nbsp;작음 &lt;br /&gt;예) 웹 브라우저에서 계속 연속해서 검색 버튼을 누르지는 않는다.&lt;/li&gt;
&lt;li&gt;서버 자원을 매우 효율적으로 사용할 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;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-origin-width=&quot;769&quot; data-origin-height=&quot;361&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ccQ3bA/btrDPAIp975/OayywkVQKTZlzKegKZEM8K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ccQ3bA/btrDPAIp975/OayywkVQKTZlzKegKZEM8K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ccQ3bA/btrDPAIp975/OayywkVQKTZlzKegKZEM8K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FccQ3bA%2FbtrDPAIp975%2FOayywkVQKTZlzKegKZEM8K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;585&quot; height=&quot;275&quot; data-origin-width=&quot;769&quot; data-origin-height=&quot;361&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;연결을 유지하지 않는 모델&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;[ 1. 요청과 응답 ]&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;358&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bSXZDX/btrDOyRR1St/MXrKKwO7is7HjzkLE75Ld1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bSXZDX/btrDOyRR1St/MXrKKwO7is7HjzkLE75Ld1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bSXZDX/btrDOyRR1St/MXrKKwO7is7HjzkLE75Ld1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbSXZDX%2FbtrDOyRR1St%2FMXrKKwO7is7HjzkLE75Ld1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;469&quot; height=&quot;244&quot; data-origin-width=&quot;688&quot; data-origin-height=&quot;358&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[ 2. 연결 종료 ]&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;697&quot; data-origin-height=&quot;356&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pmQNK/btrDLVUB1xL/8kLilM3TRkCY3yaSRfYIYk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pmQNK/btrDLVUB1xL/8kLilM3TRkCY3yaSRfYIYk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pmQNK/btrDLVUB1xL/8kLilM3TRkCY3yaSRfYIYk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpmQNK%2FbtrDLVUB1xL%2F8kLilM3TRkCY3yaSRfYIYk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;457&quot; height=&quot;233&quot; data-origin-width=&quot;697&quot; data-origin-height=&quot;356&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;692&quot; data-origin-height=&quot;361&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/w7U5o/btrDLV78hfK/hF8SnQTGx0LcW7fnQkQZd0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/w7U5o/btrDLV78hfK/hF8SnQTGx0LcW7fnQkQZd0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/w7U5o/btrDLV78hfK/hF8SnQTGx0LcW7fnQkQZd0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fw7U5o%2FbtrDLV78hfK%2FhF8SnQTGx0LcW7fnQkQZd0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;460&quot; height=&quot;240&quot; data-origin-width=&quot;692&quot; data-origin-height=&quot;361&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;비 연결성의 한계와 극복&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;TCP/IP&amp;nbsp;연결을&amp;nbsp;새로&amp;nbsp;맺어야&amp;nbsp;함&amp;nbsp;-&amp;nbsp;3&amp;nbsp;way&amp;nbsp;handshake&amp;nbsp;시간&amp;nbsp;추가&lt;/li&gt;
&lt;li&gt;웹 브라우저로 사이트를 요청하면 HTML 뿐만 아니라 자바스크립트, css, 추가 이미지 등 &lt;br /&gt;등&amp;nbsp;수&amp;nbsp;많은&amp;nbsp;자원이&amp;nbsp;함께&amp;nbsp;다운로드&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;극복&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HTTP 지속 연결(Persistent Connections)로 문제 해결&lt;/li&gt;
&lt;li&gt;HTTP/2, HTTP/3에서 더 많은 최적화&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[ HTTP 초기 - 연결, 종료 낭비 ]&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;435&quot; data-origin-height=&quot;337&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cmhwt6/btrDGLko1Ym/vAc4MFkd9MO6cH1RIH7Ie1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cmhwt6/btrDGLko1Ym/vAc4MFkd9MO6cH1RIH7Ie1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cmhwt6/btrDGLko1Ym/vAc4MFkd9MO6cH1RIH7Ie1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcmhwt6%2FbtrDGLko1Ym%2FvAc4MFkd9MO6cH1RIH7Ie1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;379&quot; height=&quot;294&quot; data-origin-width=&quot;435&quot; data-origin-height=&quot;337&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[ HTTP 지속 연결(Persistent Connections) ]&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;432&quot; data-origin-height=&quot;334&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cteFYr/btrDOUN6G3n/k3HuOIHblu0s0jYq8JngQ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cteFYr/btrDOUN6G3n/k3HuOIHblu0s0jYq8JngQ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cteFYr/btrDOUN6G3n/k3HuOIHblu0s0jYq8JngQ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcteFYr%2FbtrDOUN6G3n%2Fk3HuOIHblu0s0jYq8JngQ0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;303&quot; data-origin-width=&quot;432&quot; data-origin-height=&quot;334&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4) HTTP 메시지&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;HTTP 메시지 구조&lt;/b&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100.116%; height: 321px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;b&gt;HTTP 메시지 구조&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;b&gt;HTTP 요청&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;b&gt;&amp;nbsp;HTTP 응답&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;377&quot; data-origin-height=&quot;270&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kRbfj/btrDOxS362F/a6yzzZJj0G11xcHxogNhb1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kRbfj/btrDOxS362F/a6yzzZJj0G11xcHxogNhb1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kRbfj/btrDOxS362F/a6yzzZJj0G11xcHxogNhb1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkRbfj%2FbtrDOxS362F%2Fa6yzzZJj0G11xcHxogNhb1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;377&quot; height=&quot;270&quot; data-origin-width=&quot;377&quot; data-origin-height=&quot;270&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;384&quot; data-origin-height=&quot;110&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cJ1Qor/btrDOyRZglo/N5ti3SbuK5Hxo1IVBmS1r1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cJ1Qor/btrDOyRZglo/N5ti3SbuK5Hxo1IVBmS1r1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cJ1Qor/btrDOyRZglo/N5ti3SbuK5Hxo1IVBmS1r1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcJ1Qor%2FbtrDOyRZglo%2FN5ti3SbuK5Hxo1IVBmS1r1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;330&quot; height=&quot;95&quot; data-origin-width=&quot;384&quot; data-origin-height=&quot;110&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;384&quot; data-origin-height=&quot;199&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CjfeP/btrDLVAnwyJ/mPGmE0eUDRVI0jisfv0we1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CjfeP/btrDLVAnwyJ/mPGmE0eUDRVI0jisfv0we1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CjfeP/btrDLVAnwyJ/mPGmE0eUDRVI0jisfv0we1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCjfeP%2FbtrDLVAnwyJ%2FmPGmE0eUDRVI0jisfv0we1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;384&quot; height=&quot;199&quot; data-origin-width=&quot;384&quot; data-origin-height=&quot;199&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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 style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;▼ HTTP 요청 메시지&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;580&quot; data-origin-height=&quot;155&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/4Kw3A/btrDLWsE9jU/nTihYdsTICTCsQLtswHpE0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/4Kw3A/btrDLWsE9jU/nTihYdsTICTCsQLtswHpE0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/4Kw3A/btrDLWsE9jU/nTihYdsTICTCsQLtswHpE0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F4Kw3A%2FbtrDLWsE9jU%2FnTihYdsTICTCsQLtswHpE0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;468&quot; height=&quot;125&quot; data-origin-width=&quot;580&quot; data-origin-height=&quot;155&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;start-line&amp;nbsp;=&amp;nbsp;request-line&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;request-line&amp;nbsp;=&amp;nbsp;method&amp;nbsp;SP(공백)&amp;nbsp;request-target&amp;nbsp;SP&amp;nbsp;HTTP-version&amp;nbsp;CRLF(엔터)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HTTP&amp;nbsp;메서드&amp;nbsp;(GET:&amp;nbsp;조회)&lt;/li&gt;
&lt;li&gt;요청 대상 (/search?q=hello&amp;amp;hl=ko)&lt;/li&gt;
&lt;li&gt;HTTP Version&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[ HTTP 메서드 ] &lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;( GET )&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;종류:&amp;nbsp;GET,&amp;nbsp;POST,&amp;nbsp;PUT,&amp;nbsp;DELETE...&lt;/li&gt;
&lt;li&gt;서버가 수행해야 할 동작 지정&lt;/li&gt;
&lt;li&gt;GET: 리소스 조회&lt;/li&gt;
&lt;li&gt;POST: 요청 내역 처리&lt;/li&gt;
&lt;/ul&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;( /search?q=hello&amp;amp;hl=ko )&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;absolute-path[?query]&amp;nbsp;(절대경로[?쿼리])&lt;/li&gt;
&lt;li&gt;절대경로= &quot;/&quot; 로 시작하는 경로&lt;/li&gt;
&lt;li&gt;참고: *, http://...?x=y 와 같이 다른 유형의 경로지정 방법도 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[ HTTP Version ]&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;( HTTP/1.1 )&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HTTP&amp;nbsp;Version&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;▼ HTTP 응답 메시지&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;475&quot; data-origin-height=&quot;242&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bZ7SCr/btrDKKMPbxT/sgO7kSz6XkrSH5eLm4ZnjK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bZ7SCr/btrDKKMPbxT/sgO7kSz6XkrSH5eLm4ZnjK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bZ7SCr/btrDKKMPbxT/sgO7kSz6XkrSH5eLm4ZnjK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbZ7SCr%2FbtrDKKMPbxT%2FsgO7kSz6XkrSH5eLm4ZnjK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;396&quot; height=&quot;202&quot; data-origin-width=&quot;475&quot; data-origin-height=&quot;242&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;start-line = &amp;nbsp;status-line&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;status-line&amp;nbsp;=&amp;nbsp;HTTP-version&amp;nbsp;SP&amp;nbsp;status-code&amp;nbsp;SP&amp;nbsp;reason-phrase&amp;nbsp;CRLF&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HTTP 버전&lt;/li&gt;
&lt;li&gt;HTTP 상태 코드: 요청 성공, 실패를 나타냄&lt;/li&gt;
&lt;li&gt;이유 문구: 사람이 이해할 수 있는 짧은 상태 코드 설명 글&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[ HTTP Version ]&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;( HTTP/1.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;[ HTTP&amp;nbsp;상태&amp;nbsp;코드 ]&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;( 200 )&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;200: 성공&lt;/li&gt;
&lt;li&gt;400: 클라이언트 요청 오류&lt;/li&gt;
&lt;li&gt;500: 서버 내부 오류&lt;/li&gt;
&lt;/ul&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;( OK )&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;size18&quot;&gt;&lt;b&gt;(2) HTTP 헤더&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;773&quot; data-origin-height=&quot;182&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ofopV/btrDOT9FdXE/Cxz28KRkFOrTkMGRADP7mK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ofopV/btrDOT9FdXE/Cxz28KRkFOrTkMGRADP7mK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ofopV/btrDOT9FdXE/Cxz28KRkFOrTkMGRADP7mK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FofopV%2FbtrDOT9FdXE%2FCxz28KRkFOrTkMGRADP7mK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;709&quot; height=&quot;167&quot; data-origin-width=&quot;773&quot; data-origin-height=&quot;182&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;header-field&amp;nbsp;=&amp;nbsp;field-name&amp;nbsp;&quot;:&quot;&amp;nbsp;OWS&amp;nbsp;field-value&amp;nbsp;OWS&amp;nbsp;(OWS:띄어쓰기&amp;nbsp;허용)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;field-name은 대소문자 구문 없음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HTTP&amp;nbsp;전송에&amp;nbsp;필요한&amp;nbsp;모든&amp;nbsp;부가정보 &lt;br /&gt;예) 메시지 바디의 내용, 메시지 바디의 크기, 압축, 인증, 요청 클라이언트(브라우저) 정보, 서버&amp;nbsp;애플리케이션&amp;nbsp;정보,&amp;nbsp;캐시&amp;nbsp;관리&amp;nbsp;정보...&lt;/li&gt;
&lt;li&gt;표준 헤더가 너무 많음 &lt;br /&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/List_of_HTTP_header_fields&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://en.wikipedia.org/wiki/List_of_HTTP_header_fields&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;필요시 임의의 헤더 추가 가능 &lt;br /&gt;helloworld: hihi&lt;/li&gt;
&lt;/ul&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;size18&quot;&gt;&lt;b&gt;(3) HTTP 메시지 바디&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;371&quot; data-origin-height=&quot;183&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wmu1z/btrDOUHvruY/kffz3Cylf7pPq9kEGXfrxk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wmu1z/btrDOUHvruY/kffz3Cylf7pPq9kEGXfrxk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wmu1z/btrDOUHvruY/kffz3Cylf7pPq9kEGXfrxk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fwmu1z%2FbtrDOUHvruY%2Fkffz3Cylf7pPq9kEGXfrxk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;363&quot; height=&quot;179&quot; data-origin-width=&quot;371&quot; data-origin-height=&quot;183&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HTTP 요청도 메시지 바디를 가질 수 있음&lt;/li&gt;
&lt;li&gt;실제&amp;nbsp;전송할&amp;nbsp;데이터&lt;/li&gt;
&lt;li&gt;HTML 문서, 이미지, 영상, JSON 등등 byte로 표현할 수 있는 모든 데이터 전송 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;출처&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;a href=&quot;https://www.inflearn.com/course/http-%EC%9B%B9-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC&quot;&gt;모든 개발자를 위한 HTTP 웹 기본 지식(김영한)&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>HTTP</category>
      <category>http</category>
      <category>HTTP 메시지</category>
      <author>mandykr</author>
      <guid isPermaLink="true">https://mandykr.tistory.com/81</guid>
      <comments>https://mandykr.tistory.com/81#entry81comment</comments>
      <pubDate>Thu, 2 Jun 2022 14:36:53 +0900</pubDate>
    </item>
    <item>
      <title>[DDD] 5. 도메인 이벤트와 CQRS</title>
      <link>https://mandykr.tistory.com/80</link>
      <description>&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;2. 스프링&amp;nbsp;ApplicationEvent&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3.&lt;span&gt;&amp;nbsp;&lt;/span&gt;이벤트&amp;nbsp;소싱(Event&amp;nbsp;Sourcing)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. CQRS(명령&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 도메인 이벤트&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;느슨한 결합과 강한 결합&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;외부 서비스가 정상이 아닐 경우 트랜잭션 처리를 어떻게 해야 할지 애매&lt;/li&gt;
&lt;li&gt;외부 서비스 성능에 직접적인 영향을 받는 문제가 있다.&lt;/li&gt;
&lt;li&gt;도메인 객체에 서비스를 전달하면 추가로 설계상 문제가 나타날 수 있다.&lt;/li&gt;
&lt;li&gt;도메인 객체에 서비스를 전달할 떄 또 다른 문제는 기능을 추가할 때 발생한다.&lt;/li&gt;
&lt;li&gt;비동기 이벤트를 사용하면 두 시스템 간의 결합을 크게 낮출 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;이벤트&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이벤트가 발생한다는 것은 상태가 변경됐다는 것을 의미한다.&lt;/li&gt;
&lt;li&gt;도메인 모델에서도 UI 컴포넌트와 유사하게 도메인의 상태 변경을 이벤트로 표현할 수 있다.&lt;/li&gt;
&lt;li&gt;보통 '~할 때', '~가 발생하면', '만약 ~하면'과 같은 요구사항은 도메인의 상태 변경과 관련된 경우가 많고 이런 요구사항을 이벤트를 이용해서 구현할 수 있다.&lt;/li&gt;
&lt;li&gt;이벤트는 이미 과거에 일어난 사건이기 때문에 수정되거나 삭제되지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;이벤트 관련 구성요소&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1062&quot; data-origin-height=&quot;354&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QSzDc/btrDDsqWxno/3xQZgs9mfTi5juIChUP95K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QSzDc/btrDDsqWxno/3xQZgs9mfTi5juIChUP95K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QSzDc/btrDDsqWxno/3xQZgs9mfTi5juIChUP95K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQSzDc%2FbtrDDsqWxno%2F3xQZgs9mfTi5juIChUP95K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;773&quot; height=&quot;258&quot; data-origin-width=&quot;1062&quot; data-origin-height=&quot;354&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;도메인 모델에서 이벤트 주체는 엔티티, 밸류, 도메인 서비스와 같은 도메인 객체이다.&lt;/li&gt;
&lt;li&gt;도메인 객체는 도메인 로직을 실행해서 상태가 바뀌면 관련 이벤트를 발생한다.&lt;/li&gt;
&lt;li&gt;이벤트 핸들러(handler)는 이벤트 생성 주체가 발생한 이벤트에 반응한다.&lt;/li&gt;
&lt;li&gt;이벤트 핸들러는 생성 주체가 발생한 이벤트를 전달받아 이벤트에 담긴 데이터를 이용해서 원하는 기능을 실행한다.&lt;/li&gt;
&lt;li&gt;이벤트 생성 주체와 이벤트 핸들러를 연결해 주는 것이 이벤트 디스패처(dispatcher)이다.&lt;/li&gt;
&lt;li&gt;이벤트를 전달받은 디스패처는 해당 이벤트를 처리할 수 있는 핸들러에 이벤트를 전파한다.&lt;/li&gt;
&lt;/ul&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;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이벤트는 현재 기준으로 (바로 직전이라도) 과거에 벌어진 것을 표현하기 때문에 이벤트 이름에는 과거 시제를 사용한다.&lt;/li&gt;
&lt;li&gt;이벤트는 이벤트 핸들러가 작업을 수행하는 데 필요한 최소한의 데이터를 담아야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;이벤트 용도&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;도메인의 상태가 바뀔 때 다른 후처리를 해야 할 경우 후처리를 실행하기 위한 트리거로 이벤트를 사용할 수 있다.&lt;/li&gt;
&lt;li&gt;이벤트의 두 번째 용도는 서로 다른 시스템 간의 데이터 동기화이다.&lt;/li&gt;
&lt;/ul&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;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서로 다른 도메인 로직이 섞이는 것을 방지할 수 있다.&lt;/li&gt;
&lt;li&gt;이벤트 핸들러를 사용하면 기능 확장도 용이하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;비동기 이벤트 처리&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;로컬 핸들러를 비동기로 실행하기&lt;/li&gt;
&lt;li&gt;메시지 큐를 사용하기&lt;/li&gt;
&lt;li&gt;이벤트 저장소와 이벤트 포워더 사용하기&lt;/li&gt;
&lt;li&gt;이벤트 저장소와 이벤트 제공 API 사용하기&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;2.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;스프링&amp;nbsp;ApplicationEvent&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) Event Publisher 사용&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1062&quot; data-origin-height=&quot;354&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zp202/btrDzysr7eg/P3Q2TW1jLDFI3uPDYKXYt0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zp202/btrDzysr7eg/P3Q2TW1jLDFI3uPDYKXYt0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zp202/btrDzysr7eg/P3Q2TW1jLDFI3uPDYKXYt0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fzp202%2FbtrDzysr7eg%2FP3Q2TW1jLDFI3uPDYKXYt0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;231&quot; data-origin-width=&quot;1062&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;&lt;b&gt;[ Event Publisher ]&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1653979185281&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Transactional
@Service
public class ProductService {
    private final ApplicationEventPublisher publisher;

    public ProductService(ApplicationEventPublisher publisher) {
        this.publisher = publisher;
    }

    public void create(String name, int price) {
        Product product = new Product(name, BigDecimal.valueOf(price));
        publisher.publishEvent(new ProductCreatedEvent(name));
    }
}

public class ProductCreatedEvent {
    private UUID id;
    private final String name;
    private final int price;

    public ProductCreatedEvent(UUID id, String name, int price) {
        this.id = id;
        this.name = name;
        this.price = price;
    }

    public UUID getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public int getPrice() {
        return price;
    }
}&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;[ Event Listener ]&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1653979298606&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
public class EmailSender implements Sender {
    @EventListener
    public void send(ProductCreatedEvent event) {
        System.out.println(&quot;email sender = &quot; + event.getName());
    }

    @Override
    public void send(String text) {
        System.out.println(&quot;email sender = &quot; + text);
    }
}

@Component
public class SmsSender implements Sender {
    @EventListener
    public void send(ProductCreatedEvent event) {
        System.out.println(&quot;sms sender = &quot; + event.getName());
    }

    @Override
    public void send(String text) {
        System.out.println(&quot;sms sender = &quot; + text);
    }
}&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;@EventListener는 이벤트를 발행하는 트랜잭션에 묶여있기 때문에 send() 에서 실행이 실패하면 이벤트를 발행하는 로직이 롤백된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제는 &lt;b&gt;@TransactionalEventListener&lt;/b&gt;를 사용해 트랜잭션 범위 밖에서 실행되도록 할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1653979509785&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@TransactionalEventListener
public void send(ProductCreatedEvent event) {
    System.out.println(&quot;email sender = &quot; + event.getName());
}&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;pre id=&quot;code_1653980720803&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@TransactionalEventListener(condition = &quot;#event.price != 0&quot;)
public void send(ProductCreatedEvent event) {
    System.out.println(&quot;email sender = &quot; + event.getName());
}&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;[ Test ]&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@RecordApplicationEvents 를 사용해 이벤트 목록 주입받을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같은 테스트는 비용이 크기 때문에 단위 테스트 보다는 인수 테스트에서 작성해볼 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1653980132294&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RecordApplicationEvents
@SpringBootTest
class ProductServiceTest {
    @Autowired
    ProductService productService;

    @Autowired
    private ApplicationEvents events;

    @Test
    void create() {
        assertThat(events.stream(ProductCreatedEvent.class).count()).isEqualTo(0);
        productService.create(&quot;치킨&quot;, 10_000);
        assertThat(events.stream(ProductCreatedEvent.class).count()).isEqualTo(1);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2) Abstract Aggregate Root&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;명시적으로 AbstractAggregateRoot 클래스를 상속하도록 작성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;엔티티에서 이벤트를 register 하고 응용 서비스에서 명시적으로 Spring Data Jpa 의 save()를 호출해 이벤트를 발행한다.&lt;/p&gt;
&lt;pre id=&quot;code_1653984323971&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Transactional
@Service
public class ProductService {
    private final ProductRepository productRepository;
    private final ApplicationEventPublisher publisher;

    public void changePrice(UUID id, int price) {
        Product product = productRepository.findById(id).orElseThrow(EntityNotFoundException::new);
        product.changePrice(BigDecimal.valueOf(price));
        // 이벤트 발행을 위해 save()를 명시적으로 호출해줘야 한다.
        productRepository.save(product);
    }
}

@Entity
public class Product extends AbstractAggregateRoot&amp;lt;Product&amp;gt; {
    @Column(name = &quot;id&quot;, columnDefinition = &quot;varbinary(16)&quot;)
    @Id
    private UUID id;

    // ...

    public void changePrice(BigDecimal price) {
        this.price = new ProductPrice(price);
        registerEvent(new ProductPriceChangedEvent(id, price));
    }
}

public class ProductPriceChangedEvent {
    private final UUID id;
    private final BigDecimal price;

    public ProductPriceChangedEvent(UUID id, BigDecimal price) {
        this.id = id;
        this.price = price;
    }
}&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;엔티티의 ID를 @GeneratedValue를 이용해 자동 생성하는 경우 엔티티를 DB에 저장하기 전에는 ID를 알 수 없는 문제가 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 경우 @PostPersist를 사용해 엔티티가 영속화된 경우 이벤트를 생성하도록 할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1653984970865&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// service
public Product create(String name, int price) {
    Product product = productRepository.save(new Product(name, BigDecimal.valueOf(price)));
    return product;
}

// entity
@PostPersist
private void created() {
    registerEvent(new ProductCreatedEvent(id, name, price));
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 이벤트&amp;nbsp;소싱(Event&amp;nbsp;Sourcing)&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;개념&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;도메인 모델에서 발생하는 모든 이벤트를 기록하는 데이터 저장 기법&lt;/li&gt;
&lt;li&gt;반응형 시스템에 적합하고 규모 확장 용이&lt;/li&gt;
&lt;li&gt;로깅은 예외 상황이나 프로파일링까지 고려하지만 Event Sourcing은 비즈니스 이벤트에 대해서만 다룬다.&lt;/li&gt;
&lt;li&gt;Update나 Delete 연산은 수행되지 않는다.&lt;/li&gt;
&lt;/ul&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;/h4&gt;
&lt;pre id=&quot;light-73f8h1k22g&quot; class=&quot;angelscript&quot;&gt;&lt;code&gt;이벤트: 사용자 A가 통장 a에 1000원을 입금(+)한다.
현재 상태: 1000원

이벤트: 사용자 A가 통장 a에 500원을 인출(-)한다.
현재 상태: 500원

이벤트: 사용자 A가 통장 a에 1000원을 입금(+)한다.
현재 상태: 1500원&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. CQRS(명령&amp;nbsp;및&amp;nbsp;쿼리&amp;nbsp;책임&amp;nbsp;분리)&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;CQRS&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;상태를 변경하는 명령(Command)을 위한 모델과 상태를 제공하는 조회(Query)를 위한 모델을 분리하는 패턴이다.&lt;/li&gt;
&lt;li&gt;CQRS는 복잡한 도메인에 적합하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;웹과 CQRS&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메모리에 캐시하는 데이터는 DB에 보관된 데이터를 그대로 저장하기보다는 화면에 맞는 모양으로 변환한 데이터를 캐시할 때 성능에 더 유리하다.&lt;/li&gt;
&lt;li&gt;조회 속도를 높이기 위해 별도 처리를 하고 있다면 명시적으로 명령 모델과 조회 모델을 구분하자.&lt;/li&gt;
&lt;li&gt;조회 기능 때문에 명령 모델이 복잡해지는 것을 방지할 수 있고 명령 모델에 관계없이 조회 기능에 특화된 구현 기법을 보다 쉽게 적용할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;CQRS 장단점&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;명령 모델을 구현할 때 도메인 자체에 집중할 수 있다.&lt;/li&gt;
&lt;li&gt;조회 성능을 향상시키는 데 유리하다.&lt;/li&gt;
&lt;li&gt;구현해야 할 코드가 더 많다.&lt;/li&gt;
&lt;li&gt;더 많은 구현 기술이 필요하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;출처&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;a href=&quot;https://edu.nextstep.camp/&quot;&gt;NEXTSTEP&lt;/a&gt;: DDD 세레나데 3기&lt;/p&gt;</description>
      <category>DDD</category>
      <author>mandykr</author>
      <guid isPermaLink="true">https://mandykr.tistory.com/80</guid>
      <comments>https://mandykr.tistory.com/80#entry80comment</comments>
      <pubDate>Tue, 31 May 2022 14:45:25 +0900</pubDate>
    </item>
    <item>
      <title>[DDD] 4. 도메인 주도 설계 아키텍처</title>
      <link>https://mandykr.tistory.com/79</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&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;2. DTO(Data&amp;nbsp;Transfer&amp;nbsp;Object)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 의존&amp;nbsp;역전&amp;nbsp;원칙&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 전략적&amp;nbsp;설계&amp;nbsp;-&amp;nbsp;ANTICORRUPTION&amp;nbsp;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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 계층형&amp;nbsp;아키텍처&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도메인 주도 설계의 아키텍처가 별도로 정의되어 있는 것은 아니지만&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;계층형 아키텍처, 클린 아키텍처, 헥사고날 아키텍처 처럼 도메인을 보호하는 아키텍처의 개념이 중요하다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;829&quot; data-origin-height=&quot;360&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bWKWzv/btrDDsDOmpZ/SSrAnwSChyO4kJyFsAe6I1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bWKWzv/btrDDsDOmpZ/SSrAnwSChyO4kJyFsAe6I1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bWKWzv/btrDDsDOmpZ/SSrAnwSChyO4kJyFsAe6I1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbWKWzv%2FbtrDDsDOmpZ%2FSSrAnwSChyO4kJyFsAe6I1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;306&quot; data-origin-width=&quot;829&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;계층형 아키텍처의 비즈니스 로직은 도메인 모델에서 수행되도록 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Entity, Value Object 는 물론이고&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;비즈니스 로직이&lt;/span&gt; Application Service 로 넘어가지 않도록 하는 Domain Service도 도메인 모델에 속한다.&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;653&quot; data-origin-height=&quot;258&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/A9EOX/btrDttEJUGh/GOhRmoIJgGmgkvtIVSJYS0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/A9EOX/btrDttEJUGh/GOhRmoIJgGmgkvtIVSJYS0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/A9EOX/btrDttEJUGh/GOhRmoIJgGmgkvtIVSJYS0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FA9EOX%2FbtrDttEJUGh%2FGOhRmoIJgGmgkvtIVSJYS0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;605&quot; height=&quot;239&quot; data-origin-width=&quot;653&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;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) Presentation Layer&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자가&amp;nbsp;시스템을&amp;nbsp;사용할&amp;nbsp;수&amp;nbsp;있는&amp;nbsp;(화면)&amp;nbsp;흐름을&amp;nbsp;제공하고&amp;nbsp;제어&lt;/li&gt;
&lt;li&gt;사용자가의&amp;nbsp;요청을&amp;nbsp;알맞은&amp;nbsp;응용&amp;nbsp;서비스에&amp;nbsp;전달하고&amp;nbsp;결과를&amp;nbsp;사용자에게&amp;nbsp;제공한다.&lt;/li&gt;
&lt;li&gt;사용자의&amp;nbsp;세션을&amp;nbsp;관리한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2) Application Layer&lt;/h4&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Application Service&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자의 요청을 처리하기 위해 리포지터리로부터 도메인 객체를 구하고, 도메인 객체를 사용한다.&lt;/li&gt;
&lt;li&gt;로직을 직접 수행하기보다는 도메인 모델에 로직 수행을 위임한다.&lt;/li&gt;
&lt;li&gt;도메인 객체 간의 실행 흐름을 제어&lt;/li&gt;
&lt;li&gt;트랜잭션 처리&lt;/li&gt;
&lt;li&gt;도메인 영역에서 발생시킨 이벤트를 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1653960816727&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public Result doSomeFunc(SomeReq req) {
    // 1. 리포지터리에서 애그리거트를 구한다.
    SomeAgg agg = someAggRepository.findById(req.getId());
    checkNull(agg);
    // 2. 애그리거트의 도메인 기능을 실행한다.
    agg.doFunc(req.getValue());
    // 3. 결과를 리턴한다.
    return createSuccessResult(agg);
}&lt;/code&gt;&lt;/pre&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;b&gt;[ 메서드 파라미터와 값 리턴 ]&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;응용 서비스에 데이터로 전달할 파라미터가 두 개 이상 존재하면 데이터 전달을 위한 별도 클래스를 사용하는 것이 편리하다.&lt;/li&gt;
&lt;li&gt;응용 서비스는 표현 영역에서 필요한 데이터만 리턴하는 것이 기능 실행 로직의 응집도를 높이는 확실한 방법이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[ 값 검증 ]&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;값 검증은 표현 영역과 응용 서비스 두 곳에서 모두 수행할 수 있다.&lt;/li&gt;
&lt;li&gt;원칙적으로 모든 값에 대한 검증은 응용 서비스에서 처리한다.&lt;/li&gt;
&lt;li&gt;표현 영역에서 필수 값과 값의 형식을 검사하면 실질적으로 응용 서비스는 아이디 중복 여부와 같은 논리적 오류만 검사하면 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3) Domain Layer&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도메인&amp;nbsp;영역은&amp;nbsp;도메인&amp;nbsp;모델을&amp;nbsp;구현한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Domain Service&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;도메인 서비스는 애플리케이션 서비스와 달리 도메인 로직을 다룬다.&lt;/li&gt;
&lt;li&gt;엔티티와 값 객체만으로 표현하기 어려운 비즈니스 로직을 도메인 서비스를 통해 애플리케이션 계층으로 넘어가지 않도록 한다.&lt;/li&gt;
&lt;li&gt;도메인 서비스는 상태가 없을 수 있고 절차적일 수 있으며 클래스 이름이 Service로 작성되지 않을 수 있다.&lt;/li&gt;
&lt;li&gt;Spring Framework에서 애플리케이션 서비스에 @Service를 사용한다면 차별을 위해 @Component를 사용하거나 별도로 어노테이션을 만들어 관리한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예) 송금과 관련된 정책을 처리하는 도메인 서비스&lt;/p&gt;
&lt;pre id=&quot;code_1653960993243&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class TransactionValidator {
    public boolean isValid(Money amount, Account from, Account to) {
        if (!from.getCurrency().equals(amount.getCurrency())) {
            return false;
        }
        if (!to.getCurrency().equals(amount.getCurrency())) {
            return false;
        }
        if (from.getBalance().isLessThan(amount)) {
            return false;
        }
        if (amount.isGreaterThan(someThreshold)) {
            return false;
        }
        return true;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Domain Model&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;도메인 모델에서의 값 검증은 유효성 검사의 개념보다는 모델링을 구현하고 모델링의 검증에 대한 개념이 강하다.&lt;/p&gt;
&lt;pre id=&quot;code_1653961845867&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class PhoneNumber {
    private final String phoneNumber;

    public PhoneNumber(String phoneNumber) {
        Objects.requireNonNull(phoneNumber, &quot;phoneNumber must not be null&quot;); // 
        var sb = new StringBuilder();
        char ch;
        for (int i = 0; i &amp;lt; phoneNumber.length(); ++i) {
            ch = phoneNumber.charAt(i);
            if (Character.isDigit(ch)) { // 
                sb.append(ch);
            } else if (!Character.isWhitespace(ch) &amp;amp;&amp;amp; ch != '(' &amp;amp;&amp;amp; ch != ')' &amp;amp;&amp;amp; ch != '-' &amp;amp;&amp;amp; ch != '.') { // 
                throw new IllegalArgument(phoneNumber + &quot; is not valid&quot;);
            }
        }
        if (sb.length() == 0) { // 
            throw new IllegalArgumentException(&quot;phoneNumber must not be empty&quot;);
        }
        this.phoneNumber = sb.toString();
    }

    @Override
    public String toString() {
        return phoneNumber;
    }
}&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;h4 data-ke-size=&quot;size20&quot;&gt;4) Infrastructure Layer&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.&lt;span&gt;&amp;nbsp;&lt;/span&gt;DTO(Data&amp;nbsp;Transfer&amp;nbsp;Object)&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) 응용 서비스의 출력&lt;/h4&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;것&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;도메인&amp;nbsp;모델을&amp;nbsp;직접&amp;nbsp;사용하거나,&amp;nbsp;-&amp;nbsp;Domain&amp;nbsp;Model&amp;nbsp;Everywhere&lt;/li&gt;
&lt;li&gt;별도의&amp;nbsp;DTO(Data&amp;nbsp;Transfer&amp;nbsp;Object)&amp;nbsp;사용하기&amp;nbsp;-&amp;nbsp;Pure&amp;nbsp;Domain&amp;nbsp;Model&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2) DTO(Data Transfer Object)&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프로세스&amp;nbsp;간에&amp;nbsp;데이터를&amp;nbsp;전달하는&amp;nbsp;객체&lt;/li&gt;
&lt;li&gt;구조체&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;VALUE OBJECT는 DTO가 아니다. - Martin Fowler&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DTO의 용도로 VO를 사용할 수는 있지만 VO는 DTO를 의미하지 않는다.&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;3) Domain Model Everywhere&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;도메인 모델을 모든 계층에서 사용
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이미 가지고 있는 클래스를 사용할 수 있다.&lt;/li&gt;
&lt;li&gt;도메인 모델과 DTO 간에 변환이 필요 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;OSIV(Open Session In View) 사용
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://kingbbode.tistory.com/27&quot;&gt;Spring - Open Session In View - Kingbbode&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Getter 메서드, Setter 메서드 사용&lt;/li&gt;
&lt;li&gt;DTO는 나쁘다!&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4) Pure Domain Model&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클라이언트와 도메인 모델 간의 분리&lt;/li&gt;
&lt;li&gt;애플리케이션의 외부 요구 사항과 내부 요구 사항을 분리&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;항상 DTO를 만드는 것은 실용적이지 않다.&lt;/p&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;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.&lt;span&gt;&amp;nbsp;&lt;/span&gt;의존&amp;nbsp;역전&amp;nbsp;원칙&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) 고수준 모듈, 저수준 모듈&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;고수준 모듈&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;어떤 의미 있는 단일 기능을 제공하는 모듈&lt;/li&gt;
&lt;li&gt;e.g. 가격 할인 계산&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예) &lt;span style=&quot;background-color: #2d2d2d; color: #cccccc;&quot;&gt;고객 정보를 구한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;span style=&quot;background-color: #2d2d2d; color: #cccccc;&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;고수준 모듈의 기능을 구현하기 위해 필요한 하위 기능의 실제 구현&lt;/li&gt;
&lt;li&gt;e.g. 가격 할인 계산&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예) &lt;span style=&quot;background-color: #2d2d2d; color: #cccccc;&quot;&gt;RDBMS에서 JPA로 구한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;span style=&quot;background-color: #2d2d2d; color: #cccccc;&quot;&gt;Drools로 룰을 적용한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2) 고수준 모듈의 의존 문제&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;고수준 모듈이 저수준 모듈을 의존&lt;/li&gt;
&lt;li&gt;저수준 모듈의 변경에 따라 고수준 모듈이 영향을 받는다.&lt;/li&gt;
&lt;li&gt;고수준 모듈만 테스트하기 어렵다.&lt;/li&gt;
&lt;li&gt;다른 구현 기술을 사용하려면 코드의 많은 부분을 고쳐야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3) DIP&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;저수준 모듈이 고수준 모듈에 의존한다고 해서 이를 DIP(Dependency Inversion Principle, 의존 역전 원칙)라고 부른다.&lt;/li&gt;
&lt;li&gt;인프라스트럭처 영역에 의존할 때 발생했던 구현 교체가 어렵다는 문제와 테스트가 어려운 문제를 해소할 수 있다.&lt;/li&gt;
&lt;li&gt;고수준 모듈은 더 이상 저수준 모듈에 의존하지 않고 구현을 추상화한 인터페이스에 의존한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1002&quot; data-origin-height=&quot;557&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bca6sD/btrDyE66aM0/cnGOKKG7Jy6PaFyvDaQMA1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bca6sD/btrDyE66aM0/cnGOKKG7Jy6PaFyvDaQMA1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bca6sD/btrDyE66aM0/cnGOKKG7Jy6PaFyvDaQMA1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbca6sD%2FbtrDyE66aM0%2FcnGOKKG7Jy6PaFyvDaQMA1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;369&quot; data-origin-width=&quot;1002&quot; data-origin-height=&quot;557&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DIP를 적용할 때 하위 기능을 추상화한 인터페이스는 고수준 모듈 관점에서 도출한다.&lt;/li&gt;
&lt;li&gt;추상화한 인터페이스는 저수준 모듈이 아닌 고수준 모듈에 위치한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;204&quot; data-origin-height=&quot;200&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bd2ZNH/btrDy18MPtR/YZpQl0Ywy8ddnVjP1ytZgk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bd2ZNH/btrDy18MPtR/YZpQl0Ywy8ddnVjP1ytZgk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bd2ZNH/btrDy18MPtR/YZpQl0Ywy8ddnVjP1ytZgk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbd2ZNH%2FbtrDy18MPtR%2FYZpQl0Ywy8ddnVjP1ytZgk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;204&quot; height=&quot;200&quot; data-origin-width=&quot;204&quot; data-origin-height=&quot;200&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;background-color: #f6e199;&quot;&gt;계층형 아키텍처에서 DTO(Pure Domain Model)를 사용하고 DIP를 적용하는 이유는 도메인을 보호하기 위함이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;결국 계층형 아키텍처에서 도메인을 보호하는 개념의 헥사고날, 어니언, 클린 아키텍처로 가게된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4) 육각형, 양파, 클린 아키텍처&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #555555;&quot;&gt;육각형 아키텍처(hexagonal architecture), 양파 아키텍처(onion architecture) 및 클린 아키텍처(clean architecture)의 기본 규칙&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;875&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/WRFm8/btrDz69BqPx/7dyt4Iazn2veeutz2Vzr50/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/WRFm8/btrDz69BqPx/7dyt4Iazn2veeutz2Vzr50/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/WRFm8/btrDz69BqPx/7dyt4Iazn2veeutz2Vzr50/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWRFm8%2FbtrDz69BqPx%2F7dyt4Iazn2veeutz2Vzr50%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;706&quot; height=&quot;483&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.&lt;span&gt;&amp;nbsp;&lt;/span&gt;전략적&amp;nbsp;설계&amp;nbsp;-&amp;nbsp;ANTICORRUPTION&amp;nbsp;LAYER&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;손상 방지 레이어를 다른 하위 시스템 사이에 배치하여 격리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 레이어는 두 시스템 간의 통신을 변환하여, 다른 응용 프로그램이 해당 디자인 및 기술 방식을 손상시키지 않도록 하여 해당 시스템을 변경되지 않은 상태로 유지하도록 할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;616&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/95QRs/btrDAQrTVjY/YNkXbtlkeKsuEHo4jHhLk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/95QRs/btrDAQrTVjY/YNkXbtlkeKsuEHo4jHhLk1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/95QRs/btrDAQrTVjY/YNkXbtlkeKsuEHo4jHhLk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F95QRs%2FbtrDAQrTVjY%2FYNkXbtlkeKsuEHo4jHhLk1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;717&quot; height=&quot;345&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;616&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;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;출처&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;a href=&quot;https://edu.nextstep.camp/&quot;&gt;NEXTSTEP&lt;/a&gt;: DDD 세레나데 3기&lt;/p&gt;</description>
      <category>DDD</category>
      <category>계층형 아키텍처</category>
      <category>육각형 아키텍처</category>
      <category>클린 아키텍처</category>
      <author>mandykr</author>
      <guid isPermaLink="true">https://mandykr.tistory.com/79</guid>
      <comments>https://mandykr.tistory.com/79#entry79comment</comments>
      <pubDate>Tue, 31 May 2022 12:58:46 +0900</pubDate>
    </item>
    <item>
      <title>[DDD] 3. 도메인 주도 설계 기본 요소</title>
      <link>https://mandykr.tistory.com/78</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;목차&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 전술적 설계 - ENTITY와 VALUE OBJECT&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 전술적&amp;nbsp;설계&amp;nbsp;-&amp;nbsp;AGGREGATE와&amp;nbsp;REPOSITORY&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 전술적&amp;nbsp;설계&amp;nbsp;-&amp;nbsp;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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 전술적 설계 - ENTITY와 VALUE OBJECT&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도출한 도메인 모델은 Entity와 Value Object로 구현될 수 있다.&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;1) Entity&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;식별자를&amp;nbsp;갖는다. &lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;객체의&amp;nbsp;상태&amp;nbsp;중&amp;nbsp;해당&amp;nbsp;객체의&amp;nbsp;고유한&amp;nbsp;성질을&amp;nbsp;표현할&amp;nbsp;수&amp;nbsp;있는&amp;nbsp;상태들을&amp;nbsp;식별자라고&amp;nbsp;부른다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;식별자는&amp;nbsp;엔티티&amp;nbsp;객체마다&amp;nbsp;고유해서&amp;nbsp;각&amp;nbsp;엔티티는&amp;nbsp;서로&amp;nbsp;다른&amp;nbsp;식별자를&amp;nbsp;갖는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;464&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/4h3m2/btrDxo9qm7q/1sldGK3EhZe3PoGI2DSp4K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/4h3m2/btrDxo9qm7q/1sldGK3EhZe3PoGI2DSp4K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/4h3m2/btrDxo9qm7q/1sldGK3EhZe3PoGI2DSp4K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F4h3m2%2FbtrDxo9qm7q%2F1sldGK3EhZe3PoGI2DSp4K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;464&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;464&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;식별자&amp;nbsp;생성&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;특정 규칙에 따라 생성&lt;/li&gt;
&lt;li&gt;UUID 사용&lt;/li&gt;
&lt;li&gt;값을 직접 입력&lt;/li&gt;
&lt;li&gt;일련번호 사용(시퀀스나 DB의 자동 증가 칼럼 사용)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;도메인&amp;nbsp;모델에&amp;nbsp;set&amp;nbsp;메서드&amp;nbsp;넣지&amp;nbsp;않기&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1653881614657&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;changeShippingInfo() vs setShippingInfo()
completePayment() vs setOrderState()&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;도메인&amp;nbsp;모델에&amp;nbsp;getter&amp;nbsp;메서드와&amp;nbsp;setter&amp;nbsp;메서드를&amp;nbsp;무조건&amp;nbsp;추가하는&amp;nbsp;것은&amp;nbsp;좋지&amp;nbsp;않은&amp;nbsp;버릇&lt;/li&gt;
&lt;li&gt;특히&amp;nbsp;setter&amp;nbsp;메서드는&amp;nbsp;도메인의&amp;nbsp;핵심&amp;nbsp;개념이나&amp;nbsp;의도를&amp;nbsp;코드에서&amp;nbsp;사라지게&amp;nbsp;한다.&lt;/li&gt;
&lt;li&gt;setter&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;/li&gt;
&lt;li&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;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2) Value Object&lt;/h4&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예) 우편번호를 String으로 표현하는 것이 가능할까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; String으로 저장은 가능하지만 실제로 여러 특수문자가 저장될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 우편번호 값 객체를 통해 유효성 검사를 할 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;시스템이 성숙함에 따라 데이터 값을 객체로 대체&lt;/li&gt;
&lt;li&gt;밸류 타입은 불변&lt;/li&gt;
&lt;li&gt;밸류 객체의 값을 변경하는 방법은 새로운 밸류 객체를 할당하는 것뿐이다.&lt;/li&gt;
&lt;li&gt;항상 equals와 hashCode를 재정의 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;원시값으로 표현&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1653880187450&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class Car {
    private String name;
    private int position;
    
    Car(final String name, final int position) {
        if (Objects.isNull(name) || name.length() &amp;gt; 5) {
            throw new IllegalArgumentException();
        }
        this.name = name;
        this.position = position;
    }
}&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;Value Object로 표현&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1653880255939&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class CarName {
    private final value;
    
    CarName(final String value) {
        if (Objects.isNull(value) || value.length() &amp;gt; 5) {
            throw new IllegalArgumentException();
        }
        this.value = value;
    }
}

class Car {
    private CarName name;
    private int position;
    
    Car(final String name, final int position) {
        this.name = new CarName(name);
        this.position = position;
    }
    
    void changeName(final String name) {
        this.name = new CarName(name);
    }
}&lt;/code&gt;&lt;/pre&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;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모든 클래스를 상태를 변경할 수 없는 불변 클래스(immutable class)로 만들면 유지 보수성이 크게 향상된다.&lt;/li&gt;
&lt;li&gt;불변 객체에는 아래의 &amp;lsquo;식별자 변경(identity mutability)&amp;rsquo; 문제가 발생하지 않는다.&lt;/li&gt;
&lt;li&gt;객체가&amp;nbsp;완전하고&amp;nbsp;견고한&amp;nbsp;상태이거나&amp;nbsp;아니면&amp;nbsp;아예&amp;nbsp;실패하는&amp;nbsp;실패&amp;nbsp;원자성(failure&amp;nbsp;atomicity)을&amp;nbsp;가진다.&lt;/li&gt;
&lt;li&gt;시간적&amp;nbsp;결합(temporal&amp;nbsp;coupling)을&amp;nbsp;없앨&amp;nbsp;수&amp;nbsp;있다.&lt;/li&gt;
&lt;li&gt;스레드 안정성
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;객체가 여러 스레드에서 동시에(concurrently) 사용될 수 있고 예측 가능한(predictable) 결과를 보장하는 객체의 품질&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;단순성(simplicity), 객체가 더 단순해질 수록 응집도는 더 높아지고, 유지보수는 더 쉬워진다.&lt;/li&gt;
&lt;li&gt;불변 객체의 크기가 작은 이유는 불변 객체의 경우 생성자 안에서만 상태를 초기화할 수 있기 때문이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.&amp;nbsp;전술적&amp;nbsp;설계&amp;nbsp;-&amp;nbsp;AGGREGATE와&amp;nbsp;REPOSITORY&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Aggregate&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Bounded Context 내에서 Entity와 Value Object를 묶는 개념&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;관련&amp;nbsp;객체를&amp;nbsp;하나로&amp;nbsp;묶은&amp;nbsp;군집&lt;/li&gt;
&lt;li&gt;애그리거트는&amp;nbsp;군집에&amp;nbsp;속한&amp;nbsp;객체들을&amp;nbsp;관리하는&amp;nbsp;루트&amp;nbsp;엔티티를&amp;nbsp;갖는다.&lt;/li&gt;
&lt;li&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;/li&gt;
&lt;li&gt;애그리거트에&amp;nbsp;속한&amp;nbsp;객체는&amp;nbsp;유사하거나&amp;nbsp;동일한&amp;nbsp;라이프사이클을&amp;nbsp;갖는다.&lt;/li&gt;
&lt;li&gt;한&amp;nbsp;애그리거트에&amp;nbsp;속한&amp;nbsp;객체는&amp;nbsp;다른&amp;nbsp;애그리거트에&amp;nbsp;속하지&amp;nbsp;않는다.&lt;/li&gt;
&lt;li&gt;두&amp;nbsp;개&amp;nbsp;이상의&amp;nbsp;엔티티로&amp;nbsp;구성되는&amp;nbsp;애그리거트는&amp;nbsp;드물게&amp;nbsp;존재한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;711&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/clur55/btrDvMiIY4V/ZzXMR5UjDB8fYspPmVXhU0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/clur55/btrDvMiIY4V/ZzXMR5UjDB8fYspPmVXhU0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/clur55/btrDvMiIY4V/ZzXMR5UjDB8fYspPmVXhU0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fclur55%2FbtrDvMiIY4V%2FZzXMR5UjDB8fYspPmVXhU0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;412&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;711&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;AGGREGATE&amp;nbsp;ROOT&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 애그리거트와의 소통은 애그리거트&amp;nbsp;루트간에 메시지를 주고 받으며 이루어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애그리거트 루트는 글로벌 ID를 가지고 애그리거트&lt;span&gt;를 식별하는데 사용된다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;애그리거트&amp;nbsp;루트의&amp;nbsp;핵심&amp;nbsp;역할은&amp;nbsp;애그리거트의&amp;nbsp;일관성이&amp;nbsp;깨지지&amp;nbsp;않도록&amp;nbsp;하는&amp;nbsp;것이다.&lt;/li&gt;
&lt;li&gt;애그리거트&amp;nbsp;루트는&amp;nbsp;애그리거트가&amp;nbsp;제공해야&amp;nbsp;할&amp;nbsp;도메인&amp;nbsp;기능을&amp;nbsp;구현한다.&lt;/li&gt;
&lt;li&gt;이는&amp;nbsp;애그리거트의&amp;nbsp;내부&amp;nbsp;구현을&amp;nbsp;숨겨서&amp;nbsp;애그리거트&amp;nbsp;단위로&amp;nbsp;구현을&amp;nbsp;캡슐화할&amp;nbsp;수&amp;nbsp;있도록&amp;nbsp;돕는다.&lt;/li&gt;
&lt;li&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;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;AGGREGATE&amp;nbsp;참조&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;애그리거트를&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;/li&gt;
&lt;li&gt;ID&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;/li&gt;
&lt;li&gt;ID를&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;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;REPOSITORY&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;엔티티나&amp;nbsp;밸류가&amp;nbsp;요구사항에서&amp;nbsp;도출되는&amp;nbsp;도메인&amp;nbsp;모델이라면&amp;nbsp;리포지터리는&amp;nbsp;구현을&amp;nbsp;위한&amp;nbsp;도메인&amp;nbsp;모델&lt;/li&gt;
&lt;li&gt;애그리거트&amp;nbsp;단위로&amp;nbsp;도메인&amp;nbsp;객체를&amp;nbsp;저장하고&amp;nbsp;조회하는&amp;nbsp;기능을&amp;nbsp;정의한다.&lt;/li&gt;
&lt;li&gt;애그리거트를&amp;nbsp;구하는&amp;nbsp;리포지터리&amp;nbsp;메서드는&amp;nbsp;완전한&amp;nbsp;애그리거트를&amp;nbsp;제공해야&amp;nbsp;한다.&lt;/li&gt;
&lt;li&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;NullPointerException과&amp;nbsp;같은&amp;nbsp;문제가&amp;nbsp;발생하게&amp;nbsp;된다.&lt;/li&gt;
&lt;li&gt;리포지토리는&amp;nbsp;애그리거트(루트)&amp;nbsp;단위로&amp;nbsp;존재하며&amp;nbsp;테이블&amp;nbsp;단위로&amp;nbsp;존재하는&amp;nbsp;것이&amp;nbsp;아니다.&lt;/li&gt;
&lt;/ul&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;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.&lt;span&gt;&amp;nbsp;&lt;/span&gt;전술적&amp;nbsp;설계&amp;nbsp;-&amp;nbsp;SERVICE&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상품, 주문, 할인 쿠폰, 회원과 같이 여러 애그리거트를 활용해 결제 금액을 계산하는 로직은 어떤 애그리거트에 위치해야 할까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;도메인&amp;nbsp;서비스&lt;/h4&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;된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;응용 영역의 서비스가 용용 로직을 다룬다면 도메인 서비스는 도메인 로직을 다룬다.&lt;/li&gt;
&lt;li&gt;도메인 영역의 애그리거트나 밸류와 같은 다른 구성요소와 비교할 때 다른 점은 상태 없이 로직만 구현한다.&lt;/li&gt;
&lt;li&gt;서비스를 사용하는 주체는 애그리거트가 될 수도 있고 응용 서비스가 될 수도 있다.&lt;/li&gt;
&lt;li&gt;애그리거트 메서드를 실행할 때 도메인 서비스를 인자로 전달하지 않고 반대로 도메인 서비스의 기능을 실행할 때 애그리거트를 전달하기도 한다.&lt;/li&gt;
&lt;li&gt;특정 기능이 응용 서비스인지 도메인 서비스인지 감을 잡기 어려울 때는 해당 로직이 애그리거트의 상태를 변경하거나 애그리거트의 상태 값을 계산하는지 검사해 보면 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;FACTORY&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;어떤 객체를 생성하는 일이 복잡하다면 FACTORY를 이용해 이것을 캡슐화할 수 있다.&lt;/li&gt;
&lt;li&gt;생성자, 팩토리 클래스&lt;/li&gt;
&lt;li&gt;연관된 애그리거트에서 생성할 수 있다.&lt;/li&gt;
&lt;li&gt;어떤 다른 곳에서 해당 객체를 생성할 때 생산자의 정보를 필요로 하는 것을 줄일 수 있다.&lt;/li&gt;
&lt;li&gt;아울러 생산자와 생성된 객체 사이의 특별한 관계를 전해주기도 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&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;size14&quot;&gt;출처&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;a href=&quot;https://edu.nextstep.camp/&quot;&gt;NEXTSTEP&lt;/a&gt;: DDD 세레나데 3기&lt;/p&gt;</description>
      <category>DDD</category>
      <category>aggregate</category>
      <category>Entity</category>
      <category>Value Object</category>
      <author>mandykr</author>
      <guid isPermaLink="true">https://mandykr.tistory.com/78</guid>
      <comments>https://mandykr.tistory.com/78#entry78comment</comments>
      <pubDate>Mon, 30 May 2022 12:00:26 +0900</pubDate>
    </item>
    <item>
      <title>[DDD] 2. 도메인 모델링</title>
      <link>https://mandykr.tistory.com/77</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;목차&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 전략적&amp;nbsp;설계&amp;nbsp;-&amp;nbsp;UBIQUITOUS&amp;nbsp;LANGUAGE&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 전략적&amp;nbsp;설계&amp;nbsp;-&amp;nbsp;BOUNDED&amp;nbsp;CONTEXT&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 이벤트&amp;nbsp;스토밍(Event&amp;nbsp;Storming)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 전략적&amp;nbsp;설계&amp;nbsp;-&amp;nbsp;UBIQUITOUS&amp;nbsp;LANGUAGE&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유비쿼터스 언어(Ubiquitous Language)는 언제 어디서나 사용되는 언어란 뜻이다.&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;코드에 사용된 언어와 다르거나 코드에 반영하지 않으면 그 코드는 개발자에게 코드의 의미를 해석해야 하는 부담을 준다.&lt;br /&gt;&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;용어 사전&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 식당 포스기의 용어사전 예제이다. &lt;a href=&quot;https://github.com/mandykr/ddd-tactical-design&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;&lt;b&gt;매장 주문&lt;/b&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 13.4884%;&quot;&gt;방문한 손님 수&lt;/td&gt;
&lt;td style=&quot;width: 15.9302%;&quot;&gt;number of guests&lt;/td&gt;
&lt;td style=&quot;width: 70.4651%;&quot;&gt;식기가 필요한 사람 수. 필수 사항은 아니며 주문은 0명으로 등록할 수 있다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 13.4884%;&quot;&gt;빈 테이블&lt;/td&gt;
&lt;td style=&quot;width: 15.9302%;&quot;&gt;empty table&lt;/td&gt;
&lt;td style=&quot;width: 70.4651%;&quot;&gt;주문을 등록할 수 없는 주문 테이블&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 13.4884%;&quot;&gt;서빙&lt;/td&gt;
&lt;td style=&quot;width: 15.9302%;&quot;&gt;served&lt;/td&gt;
&lt;td style=&quot;width: 70.4651%;&quot;&gt;조리가 완료되어 음식이 나갈 수 있는 단계&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 13.4884%;&quot;&gt;완료&lt;/td&gt;
&lt;td style=&quot;width: 15.9302%;&quot;&gt;completed&lt;/td&gt;
&lt;td style=&quot;width: 70.4651%;&quot;&gt;고객이 모든 식사를 마치고 결제를 완료한 단계&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 13.4884%;&quot;&gt;접수&lt;/td&gt;
&lt;td style=&quot;width: 15.9302%;&quot;&gt;accepted&lt;/td&gt;
&lt;td style=&quot;width: 70.4651%;&quot;&gt;주문을 받고 음식을 조리하는 단계&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 13.4884%;&quot;&gt;접수 대기&lt;/td&gt;
&lt;td style=&quot;width: 15.9302%;&quot;&gt;waiting&lt;/td&gt;
&lt;td style=&quot;width: 70.4651%;&quot;&gt;주문이 생성되어 매장으로 전달된 단계&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 13.4884%;&quot;&gt;주문&lt;/td&gt;
&lt;td style=&quot;width: 15.9302%;&quot;&gt;order&lt;/td&gt;
&lt;td style=&quot;width: 70.4651%;&quot;&gt;매장에서 식사하는 고객 대상. 손님들이 매장에서 먹을 수 있도록 조리된 음식을 가져다준다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 13.4884%;&quot;&gt;주문 상태&lt;/td&gt;
&lt;td style=&quot;width: 15.9302%;&quot;&gt;order status&lt;/td&gt;
&lt;td style=&quot;width: 70.4651%;&quot;&gt;주문이 생성되면 매장에서 주문을 접수하고 고객이 음식을 받기까지의 단계를 표시한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 13.4884%;&quot;&gt;주문 테이블&lt;/td&gt;
&lt;td style=&quot;width: 15.9302%;&quot;&gt;order table&lt;/td&gt;
&lt;td style=&quot;width: 70.4651%;&quot;&gt;매장에서 주문이 발생하는 영역&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 13.4884%;&quot;&gt;주문 항목&lt;/td&gt;
&lt;td style=&quot;width: 15.9302%;&quot;&gt;order line item&lt;/td&gt;
&lt;td style=&quot;width: 70.4651%;&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;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;지속적으로 도메인을 탐색하며 풍부한 도메인 모델을 작성하도록 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도메인 모델은 개발자가 애플리케이션을 &lt;b&gt;개발하는데 밑거름&lt;/b&gt;이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;매장 주문&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;OrderTable은 식별자와 이름,&lt;span&gt;&amp;nbsp;&lt;/span&gt;NumberOfGuests를 가진다.&lt;/li&gt;
&lt;li&gt;OrderTable의 추가&lt;span&gt;&amp;nbsp;&lt;/span&gt;Order는&lt;span&gt;&amp;nbsp;&lt;/span&gt;OrderTable에 계속 쌓이며 모든&lt;span&gt;&amp;nbsp;&lt;/span&gt;Order가 완료되면&lt;span&gt;&amp;nbsp;&lt;/span&gt;EmptyTable이 된다.&lt;/li&gt;
&lt;li&gt;EmptyTable인 경우&lt;span&gt;&amp;nbsp;&lt;/span&gt;NumberOfGuests는 0이며 변경할 수 없다.&lt;/li&gt;
&lt;li&gt;Order는 식별자와&lt;span&gt;&amp;nbsp;&lt;/span&gt;OrderStatus, 주문 시간,&lt;span&gt;&amp;nbsp;&lt;/span&gt;OrderLineItems를 가진다.&lt;/li&gt;
&lt;li&gt;메뉴가 노출되고 있으며 판매되는 메뉴 가격과 일치하면&lt;span&gt;&amp;nbsp;&lt;/span&gt;Order가 생성된다.&lt;/li&gt;
&lt;li&gt;Order는 접수 대기 ➜ 접수 ➜ 서빙 ➜ 계산 완료 순서로 진행된다.&lt;/li&gt;
&lt;li&gt;OrderLineItem는 가격과 수량을 가진다.&lt;/li&gt;
&lt;li&gt;OrderLineItem의 수량은 기존&lt;span&gt;&amp;nbsp;&lt;/span&gt;Order를 취소하거나 변경해도 수정되지 않기 때문에 0보다 적을 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;모델링은 사업팀도 이해할 수 있도록 작성해야 한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발 용어가 포함되지 않도록 작성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클래스 다이어그램이 아닌 &lt;a href=&quot;https://mermaid-js.github.io/mermaid/#/entityRelationshipDiagram&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;mermaid&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;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.&lt;span&gt;&amp;nbsp;&lt;/span&gt;전략적&amp;nbsp;설계&amp;nbsp;-&amp;nbsp;BOUNDED&amp;nbsp;CONTEXT&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;772&quot; data-origin-height=&quot;478&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/z6S9O/btrDnk0gEz8/atjm62JQPs9ohk01AkrBFk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/z6S9O/btrDnk0gEz8/atjm62JQPs9ohk01AkrBFk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/z6S9O/btrDnk0gEz8/atjm62JQPs9ohk01AkrBFk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fz6S9O%2FbtrDnk0gEz8%2Fatjm62JQPs9ohk01AkrBFk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;425&quot; data-origin-width=&quot;772&quot; data-origin-height=&quot;478&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;같은 모델에 대해 컨텍스트별로 바라보는 관점이 다르기 때문에&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;용어 사전 정의와 도메인 모델링은 컨텍스트 별로 이루어 져야 한다.&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Sales Context 관점의 Customer: 상품을 구매하는 고객&lt;/li&gt;
&lt;li&gt;Support Context 관점의 Customer: 상담을 요청하는 고객&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Sales Context 에서는 고객의 생일 정보를 통해 생일 할인 이벤트를 적용해볼 수 있지만&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Support 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;도메인마다&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;한다.&lt;/b&gt;&lt;br /&gt;모델은&amp;nbsp;특정한&amp;nbsp;컨텍스트(문맥)하에서&amp;nbsp;완전한&amp;nbsp;의미를&amp;nbsp;갖는다.&lt;br /&gt;이렇게&amp;nbsp;구분되는&amp;nbsp;경계를&amp;nbsp;갖는&amp;nbsp;컨텍스트를&amp;nbsp;DDD에서는&amp;nbsp;BOUNDED&amp;nbsp;CONTEXT라고&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;좋은&amp;nbsp;BOUNDED&amp;nbsp;CONTEXT&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;하나의&amp;nbsp;BOUNDED&amp;nbsp;CONTEXT는&amp;nbsp;하나의&amp;nbsp;팀에만&amp;nbsp;할당되어야&amp;nbsp;한다.&lt;/li&gt;
&lt;li&gt;하나의&amp;nbsp;팀은&amp;nbsp;여러&amp;nbsp;개의&amp;nbsp;BOUNDED&amp;nbsp;CONTEXT를&amp;nbsp;다룰&amp;nbsp;수&amp;nbsp;있다.&lt;/li&gt;
&lt;li&gt;각각의&amp;nbsp;BOUNDED&amp;nbsp;CONTEXT는&amp;nbsp;각각의&amp;nbsp;개발&amp;nbsp;환경을&amp;nbsp;가질&amp;nbsp;수&amp;nbsp;있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;CONTEXT&amp;nbsp;MAP&lt;/h4&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;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;392&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bpvUxe/btrDl0gWKCk/64uDf3HcLBnk8oTsYWbetk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bpvUxe/btrDl0gWKCk/64uDf3HcLBnk8oTsYWbetk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bpvUxe/btrDl0gWKCk/64uDf3HcLBnk8oTsYWbetk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbpvUxe%2FbtrDl0gWKCk%2F64uDf3HcLBnk8oTsYWbetk%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;351&quot; height=&quot;269&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;392&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;프로젝트와&amp;nbsp;조직&amp;nbsp;관계&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;파트너십(Partnership)&amp;nbsp;:&amp;nbsp;두&amp;nbsp;CONTEXT가&amp;nbsp;하나의&amp;nbsp;트랜잭션으로&amp;nbsp;묶여&amp;nbsp;있다.&lt;/li&gt;
&lt;li&gt;공유&amp;nbsp;커널(Shared&amp;nbsp;kernel)&amp;nbsp;:&amp;nbsp;상호&amp;nbsp;의존하는&amp;nbsp;공유&amp;nbsp;모델을&amp;nbsp;관리한다.&lt;/li&gt;
&lt;li&gt;고객-공급자(Customer-Supplier&amp;nbsp;Development)&amp;nbsp;:&amp;nbsp;업스트림(서버:공급자),&amp;nbsp;다운스트림(클라이언트:고객)로&amp;nbsp;단방향으로&amp;nbsp;의존&lt;/li&gt;
&lt;li&gt;한다.&lt;/li&gt;
&lt;li&gt;순응주의자(Conformist)&amp;nbsp;:&amp;nbsp;업스트림(서버)이&amp;nbsp;모든&amp;nbsp;것을&amp;nbsp;제어한다.&lt;/li&gt;
&lt;li&gt;오픈&amp;nbsp;호스트&amp;nbsp;서비스(Open&amp;nbsp;Host&amp;nbsp;Service)&amp;nbsp;:&amp;nbsp;REST/API,&amp;nbsp;RPC,&amp;nbsp;Socket&lt;/li&gt;
&lt;li&gt;분리된&amp;nbsp;방법(Seprate&amp;nbsp;Ways)&amp;nbsp;:&amp;nbsp;의존&amp;nbsp;없음&lt;/li&gt;
&lt;li&gt;큰&amp;nbsp;진흙공(Big&amp;nbsp;ball&amp;nbsp;of&amp;nbsp;mud)&amp;nbsp;:&amp;nbsp;안티&amp;nbsp;패턴&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;DDD&amp;nbsp;vs&amp;nbsp;OOP&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DDD는 OOP에 근간을 두고 있지만 ID를 사용한 간접참조, 값 객체의 사용 등은 OOP를 위반하는 내용일 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OOP는 상속과 재사용성을 중요시하지만 DDD는 도메인의 분리를 중요시한다.&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;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.&lt;span&gt;&amp;nbsp;&lt;/span&gt;이벤트&amp;nbsp;스토밍(Event&amp;nbsp;Storming)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DDD 도입을 위해 이벤트 스토밍을 진행한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;복잡한 비즈니스 도메인을 빠르게 탐색하고 학습할 수 있는 워크숍&lt;/li&gt;
&lt;li&gt;도메인 전문가와 개발자를 학습 과정에 참여시키기 위한 빠른 설계&lt;/li&gt;
&lt;li&gt;코드를 없애고 모든 사람을 동일한 수준으로 만드는 시각적 접근 방법&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;658&quot; data-origin-height=&quot;740&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bAr1Gu/btrDobdjWMC/6gMc6gXGEYMcZn3BbqVSvK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bAr1Gu/btrDobdjWMC/6gMc6gXGEYMcZn3BbqVSvK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bAr1Gu/btrDobdjWMC/6gMc6gXGEYMcZn3BbqVSvK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbAr1Gu%2FbtrDobdjWMC%2F6gMc6gXGEYMcZn3BbqVSvK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;657&quot; height=&quot;739&quot; data-origin-width=&quot;658&quot; data-origin-height=&quot;740&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;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;각자가 작성한 이벤트는 볼 수 있지만, 토론을 시작하지 말고 자신이 옳다고 생각하는 방식으로 기록한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;841&quot; data-origin-height=&quot;394&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bgQMfb/btrDthJ7fXQ/64y8ZSAVSKJ47iTFphafP0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bgQMfb/btrDthJ7fXQ/64y8ZSAVSKJ47iTFphafP0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bgQMfb/btrDthJ7fXQ/64y8ZSAVSKJ47iTFphafP0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbgQMfb%2FbtrDthJ7fXQ%2F64y8ZSAVSKJ47iTFphafP0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;570&quot; height=&quot;267&quot; data-origin-width=&quot;841&quot; data-origin-height=&quot;394&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이벤트 전문가가 관심이 있는 어떤 사건&lt;/li&gt;
&lt;li&gt;이벤트 이름은 과거에 일어났던 일을 표현하기 때문에 과거 시제를 사용한다.&lt;/li&gt;
&lt;li&gt;이벤트가 발생한다는 것은 상태가 변경되었다는 것을 의미한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;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;/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;776&quot; data-origin-height=&quot;385&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/enwl1j/btrDvRcLNxZ/DhzX04OzaELDRw0IT8rTa1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/enwl1j/btrDvRcLNxZ/DhzX04OzaELDRw0IT8rTa1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/enwl1j/btrDvRcLNxZ/DhzX04OzaELDRw0IT8rTa1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fenwl1j%2FbtrDvRcLNxZ%2FDhzX04OzaELDRw0IT8rTa1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;653&quot; height=&quot;324&quot; data-origin-width=&quot;776&quot; data-origin-height=&quot;385&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;2단계에서 발생한 갈등을 시각화하고 캡쳐하는 데 사용한다.&lt;/li&gt;
&lt;li&gt;당장 토론하지 않는다. 나중에 문제가 해결되면 제거한다.&lt;/li&gt;
&lt;/ul&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;956&quot; data-origin-height=&quot;378&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OXNbd/btrDtuPHCZ5/KiSVGY1wJUjwGKbWW1CD3k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OXNbd/btrDtuPHCZ5/KiSVGY1wJUjwGKbWW1CD3k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OXNbd/btrDtuPHCZ5/KiSVGY1wJUjwGKbWW1CD3k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOXNbd%2FbtrDtuPHCZ5%2FKiSVGY1wJUjwGKbWW1CD3k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;706&quot; height=&quot;279&quot; data-origin-width=&quot;956&quot; data-origin-height=&quot;378&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;&lt;b&gt;커멘드 또는 액션 - 파란색&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;시스템에서 일어나는 일&lt;/li&gt;
&lt;li&gt;도메인 이벤트가 발생하는 원인&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;리드 모델 또는 정보 - 초록색&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;결정을 내리는데 필요한 정보&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;정책 - 연보라색&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;주로 `~할 때마다'라는 단어로 시작한다.&lt;/li&gt;
&lt;li&gt;도메인 이벤트와 커멘드 사이에 위치한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1234&quot; data-origin-height=&quot;648&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bS9tSr/btrDthDo0AD/kOQzACVV73hXkIj4UfXKX1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bS9tSr/btrDthDo0AD/kOQzACVV73hXkIj4UfXKX1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bS9tSr/btrDthDo0AD/kOQzACVV73hXkIj4UfXKX1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbS9tSr%2FbtrDthDo0AD%2FkOQzACVV73hXkIj4UfXKX1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;404&quot; data-origin-width=&quot;1234&quot; data-origin-height=&quot;648&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;에그리게잇 또는 비즈니스 규칙 - 연노란색&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;시스템이 기대하는 책임을 수행하는 일관성을 유지하는 단위&lt;/li&gt;
&lt;li&gt;일관성은 항상 참이어야 하는 속성을 유지함으로써 달성된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1230&quot; data-origin-height=&quot;600&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uKYFc/btrDrO2wbg8/67sGRGN1QTMmVEgMYY9Vy0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uKYFc/btrDrO2wbg8/67sGRGN1QTMmVEgMYY9Vy0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uKYFc/btrDrO2wbg8/67sGRGN1QTMmVEgMYY9Vy0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuKYFc%2FbtrDrO2wbg8%2F67sGRGN1QTMmVEgMYY9Vy0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;373&quot; data-origin-width=&quot;1230&quot; data-origin-height=&quot;600&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;자산화&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://miro.com&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Miro&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이벤트 스토밍으로 얻을 수 있는 것&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모두가 서로에게서 배운다.&lt;/li&gt;
&lt;li&gt;모두가 서로 소통한다.&lt;/li&gt;
&lt;li&gt;자신의 관점에서 문제를 발견하면 누구나 말할 수 있다.&lt;/li&gt;
&lt;li&gt;모두가 문제를 해결하는 방법에 대한 자신의 아이디어를 제공한다.&lt;/li&gt;
&lt;li&gt;워크숍 후에는 모두가 같은 사실을 알게된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;출처&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;a href=&quot;https://edu.nextstep.camp/&quot;&gt;NEXTSTEP&lt;/a&gt;: DDD 세레나데 3기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>DDD</category>
      <category>Bounded Context</category>
      <category>Ubiquitous Language</category>
      <category>도메인 모델링</category>
      <category>이벤트 스토밍</category>
      <author>mandykr</author>
      <guid isPermaLink="true">https://mandykr.tistory.com/77</guid>
      <comments>https://mandykr.tistory.com/77#entry77comment</comments>
      <pubDate>Sat, 28 May 2022 09:56:25 +0900</pubDate>
    </item>
    <item>
      <title>[DDD] 1. 도메인 주도 설계 이해</title>
      <link>https://mandykr.tistory.com/76</link>
      <description>&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;2. 도메인 주도 설계&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 도메인 주도 설계 주의&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 클린 아키텍처&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비즈니스 규칙이 Java, Javascript, SQL 등에 분산되어 있고 심지어 기획자가 알고있는 비즈니스 규칙이 코드가 아닌 문서에만 운영 정책으로 작성되어 있다면 유지보수가 어려워지고 요구사항이 추가될 때마다 시스템의 복잡도는 점점 높아질 것이다.&lt;/p&gt;
&lt;pre id=&quot;code_1653630134219&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;예) 소수점 표현 정책이 Java, Javascript, SQL 에서 모두 다르다.&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;h4 data-ke-size=&quot;size20&quot;&gt;클린 아키텍처&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #212121;&quot;&gt;클린 아키텍처는 계층을 분리하여 &lt;/span&gt;관심사를 분리하고 의존성 규칙을 통해 &lt;span&gt;아키텍처가 동작하도록 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의존성 규칙: 모든 소스코드 의존성은 반드시 외부에서 내부로, 고수준 정책을 향해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 업무의 업무 로직을 담당하는 코드들이 DB 또는 Web 같이 구체적인 세부 사항에 의존하지 않아야 한다.&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;772&quot; data-origin-height=&quot;567&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ok6mB/btrDgOBStgt/3MWjKapTk7j50Nk8Evm6C1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ok6mB/btrDgOBStgt/3MWjKapTk7j50Nk8Evm6C1/img.jpg&quot; data-alt=&quot;출처: The Clean Architecture, https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ok6mB/btrDgOBStgt/3MWjKapTk7j50Nk8Evm6C1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fok6mB%2FbtrDgOBStgt%2F3MWjKapTk7j50Nk8Evm6C1%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;703&quot; height=&quot;516&quot; data-origin-width=&quot;772&quot; data-origin-height=&quot;567&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처: The Clean Architecture, https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;클린 아키텍처는 로버트 C. 마틴이 제안한 &lt;b&gt;좋은 소프트웨어 아키텍처의 보편적인 원칙&lt;/b&gt;으로 다음과 같은 특징을 가지고 있다.&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 96px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 16px;&quot;&gt;
&lt;td style=&quot;width: 20.1163%; height: 16px;&quot;&gt;프레임워크 독립성&lt;/td&gt;
&lt;td style=&quot;width: 79.8837%; height: 16px;&quot;&gt;아키텍처는 다양한 기능의 라이브러리를 제공하는 소프트웨어, 즉 프레임워크의 존재 여부에 의존하지 않는다. 이를 통해 이러한 프레임워크를 도구로 사용할 수 있으며, 프레임워크가 지닌 제약사항 안으로 시스템을 욱여 넣도록 강제하지 않는다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 20.1163%; height: 20px;&quot;&gt;테스트 용이성&lt;/td&gt;
&lt;td style=&quot;width: 79.8837%; height: 20px;&quot;&gt;업무 규칙은 UI, 데이터베이스, 웹 서버, 또는 여타 외부 요소가 없이도 테스트할 수 있다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 20.1163%; height: 20px;&quot;&gt;UI 독립성&lt;/td&gt;
&lt;td style=&quot;width: 79.8837%; height: 20px;&quot;&gt;시스템의 나머지 부분을 변경하지 않고도 UI를 쉽게 변경할 수 있다. 예를 들어 업무 규칙을 변경하지 않은 채 웹 UI를 콘솔 UI로 대체할 수 있다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 20.1163%; height: 20px;&quot;&gt;데이터베이스 독립성&lt;/td&gt;
&lt;td style=&quot;width: 79.8837%; height: 20px;&quot;&gt;오라클이나 MS SQL 서버를 몽고DB, 빅테이블, 카우치DB 등으로 교체할 수 있다. 업무 규칙은 데이터베이스에 결합되지 않는다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 20.1163%; height: 20px;&quot;&gt;모든 외부 에이전시에 대한 독립성&lt;/td&gt;
&lt;td style=&quot;width: 79.8837%; height: 20px;&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;h4 data-ke-size=&quot;size20&quot;&gt;본질적&amp;nbsp;복잡성(essential&amp;nbsp;complexity)과&amp;nbsp;우발적&amp;nbsp;복잡성(accidental&amp;nbsp;complexity)&lt;/h4&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;/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;있다.&lt;/p&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;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;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 도메인 주도 설계&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&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;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;도메인 모델&lt;/h4&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;&lt;b&gt;의사소통&amp;nbsp;수단&lt;/b&gt;이다.&amp;nbsp;이&amp;nbsp;의사소통&amp;nbsp;수단은&amp;nbsp;&lt;b&gt;회의,&amp;nbsp;기획,&amp;nbsp;디자인,&amp;nbsp;개발에&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;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;모습으로&amp;nbsp;도메인을&amp;nbsp;이해하고&amp;nbsp;&lt;b&gt;도메인&amp;nbsp;지식을&amp;nbsp;공유하는&amp;nbsp;데&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;h4 data-ke-size=&quot;size20&quot;&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;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;background-color: #ffffff; color: #555555;&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;유비쿼터스 언어&lt;/li&gt;
&lt;li&gt;전략적 설계&lt;/li&gt;
&lt;li&gt;전술적 설계&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 도메인 주도 설계 주의&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;경우에 따라 DDD 기술 규칙과 패턴이 DDD 구현에 방해가 될 수 있다.&lt;/li&gt;
&lt;li&gt;중요한 것은 패턴 자체가 아니라 비즈니스 문제에 맞게 코드를 구성하고 동일한 비즈니스 용어(유비쿼터스 언어)를사용하는 것이다.&lt;/li&gt;
&lt;li&gt;CRUD 서비스처럼 간단한 업무는 DDD가 아닌 더 간단한 방법으로 관리할 수 있다.&lt;/li&gt;
&lt;li&gt;도메인 주도 설계는 완벽히 설계하고 그 다음 구현하는 방법을 의미하지 않는다. 설계와 구현을 병행하도록 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;출처&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;a href=&quot;https://edu.nextstep.camp/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;NEXTSTEP&lt;/a&gt;: DDD 세레나데 3기&lt;/p&gt;</description>
      <category>DDD</category>
      <category>ddd</category>
      <category>도메인 주도 설계</category>
      <author>mandykr</author>
      <guid isPermaLink="true">https://mandykr.tistory.com/76</guid>
      <comments>https://mandykr.tistory.com/76#entry76comment</comments>
      <pubDate>Fri, 27 May 2022 16:38:54 +0900</pubDate>
    </item>
    <item>
      <title>[HTTP] 1. 인터넷 네트워크</title>
      <link>https://mandykr.tistory.com/74</link>
      <description>&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;2. 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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 인터넷 네트워크&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) IP 프로토콜&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IP 프로토콜은 지정한 IP 주소(IP Address)를 이용해 패킷이라는 통신 단위로 데이터를 전달하는 역할을 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;892&quot; data-origin-height=&quot;486&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/br9SWm/btrA9iqDLTM/iHnbxjMlihLBX1Igy39vB1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/br9SWm/btrA9iqDLTM/iHnbxjMlihLBX1Igy39vB1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/br9SWm/btrA9iqDLTM/iHnbxjMlihLBX1Igy39vB1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbr9SWm%2FbtrA9iqDLTM%2FiHnbxjMlihLBX1Igy39vB1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;570&quot; height=&quot;311&quot; data-origin-width=&quot;892&quot; data-origin-height=&quot;486&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;925&quot; data-origin-height=&quot;489&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c0PHiY/btrA5bGHwuZ/SbOGWumq7QVwYEUXEtf6S0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c0PHiY/btrA5bGHwuZ/SbOGWumq7QVwYEUXEtf6S0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c0PHiY/btrA5bGHwuZ/SbOGWumq7QVwYEUXEtf6S0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc0PHiY%2FbtrA5bGHwuZ%2FSbOGWumq7QVwYEUXEtf6S0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;610&quot; height=&quot;322&quot; data-origin-width=&quot;925&quot; data-origin-height=&quot;489&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;출발지 IP 주소, 도착지 IP 주소, 전송 데이터 등을 IP 패킷에 포함해 전송&lt;/li&gt;
&lt;li&gt;인터넷 노드를 거쳐 이동&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;IP 프로토콜의 한계&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;클라이언트는 전송 대상 서버가 패킷을 받을 수 있는 상태인지 모르기 때문에&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;전송 과정에서 네트워크상의 문제로 패킷이 손실될 수 있다.&lt;/p&gt;
&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;용량이 큰 데이터는 여러개의 패킷으로 나눠서 전달하는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;패킷이 서로 다른 노드를 거쳐갈 수 있기 때문에 도착 순서가 변경될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2) TCP / UDP&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;인터넷 프로토콜 스택의 4계층&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;851&quot; data-origin-height=&quot;493&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/J6LiT/btrA7Pbwocu/Vi7rb4dLKoNVTwIpAksOIK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/J6LiT/btrA7Pbwocu/Vi7rb4dLKoNVTwIpAksOIK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/J6LiT/btrA7Pbwocu/Vi7rb4dLKoNVTwIpAksOIK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJ6LiT%2FbtrA7Pbwocu%2FVi7rb4dLKoNVTwIpAksOIK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;420&quot; data-origin-width=&quot;851&quot; data-origin-height=&quot;493&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 프로그램이 Hello, world! 메시지 생성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. SOCKET 라이브러리를 통해 전달&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. TCP 정보 생성, 메시지 데이터 포함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. IP 패킷 생성, TCP 데이터 포함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1 ~ 4 과정을 거쳐 TCP/IP 패킷이 만들어진다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;759&quot; data-origin-height=&quot;482&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dHEqxC/btrA7FMYvT5/kI52oxobXMHAbFNqeonvA0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dHEqxC/btrA7FMYvT5/kI52oxobXMHAbFNqeonvA0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dHEqxC/btrA7FMYvT5/kI52oxobXMHAbFNqeonvA0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdHEqxC%2FbtrA7FMYvT5%2FkI52oxobXMHAbFNqeonvA0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;291&quot; data-origin-width=&quot;759&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3) TCP 특징&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전송&amp;nbsp;제어&amp;nbsp;프로토콜(Transmission&amp;nbsp;Control&amp;nbsp;Protocol)&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 way handshake 방식을 사용해 클라이언트 서버간에 연결한다.&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;749&quot; data-origin-height=&quot;456&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/6l1tE/btrA8UQ8FJw/jAfydkOs343E4W74QJ6BUK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6l1tE/btrA8UQ8FJw/jAfydkOs343E4W74QJ6BUK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6l1tE/btrA8UQ8FJw/jAfydkOs343E4W74QJ6BUK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F6l1tE%2FbtrA8UQ8FJw%2FjAfydkOs343E4W74QJ6BUK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;335&quot; data-origin-width=&quot;749&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;&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-origin-width=&quot;728&quot; data-origin-height=&quot;259&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cJ2prs/btrBaymgP3E/Fbv2Q1G2iraPzgt57ip4YK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cJ2prs/btrBaymgP3E/Fbv2Q1G2iraPzgt57ip4YK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cJ2prs/btrBaymgP3E/Fbv2Q1G2iraPzgt57ip4YK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcJ2prs%2FbtrBaymgP3E%2FFbv2Q1G2iraPzgt57ip4YK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;175&quot; data-origin-width=&quot;728&quot; data-origin-height=&quot;259&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;순서 보장&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TCP 세그먼트의 전송 제어 정보를 참고해 도착 순서가 잘못된 경우&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;727&quot; data-origin-height=&quot;278&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tBBrg/btrA7GFba3m/AXG8bK9iMZk6mUb8iDmVrK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tBBrg/btrA7GFba3m/AXG8bK9iMZk6mUb8iDmVrK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tBBrg/btrA7GFba3m/AXG8bK9iMZk6mUb8iDmVrK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtBBrg%2FbtrA7GFba3m%2FAXG8bK9iMZk6mUb8iDmVrK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;497&quot; height=&quot;190&quot; data-origin-width=&quot;727&quot; data-origin-height=&quot;278&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;h4 data-ke-size=&quot;size20&quot;&gt;4) UDP 특징&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자 데이터그램 프로토콜(User Datagram Protocol)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;연결 지향 X&lt;/li&gt;
&lt;li&gt;데이터 전달 보증 X&lt;/li&gt;
&lt;li&gt;순서 보장 X&lt;/li&gt;
&lt;li&gt;데이터 전달 및 순서가 보장되지 않지만 단순하고 빠름&lt;/li&gt;
&lt;li&gt;IP와 거의 같다. (PORT, 체크섭 정도만 추가)&lt;/li&gt;
&lt;li&gt;애플리케이션에서 추가 작업 필요&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;5) PORT&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 IP 내에서 프로세스를 구분한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TCP/IP 패킷의 출발지와 도착지 PORT 정보를 사용한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;759&quot; data-origin-height=&quot;482&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dHEqxC/btrA7FMYvT5/kI52oxobXMHAbFNqeonvA0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dHEqxC/btrA7FMYvT5/kI52oxobXMHAbFNqeonvA0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dHEqxC/btrA7FMYvT5/kI52oxobXMHAbFNqeonvA0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdHEqxC%2FbtrA7FMYvT5%2FkI52oxobXMHAbFNqeonvA0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;291&quot; data-origin-width=&quot;759&quot; data-origin-height=&quot;482&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;728&quot; data-origin-height=&quot;399&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ciyWks/btrBaM5IIhI/AB8NLxhC1CTE0JCIIZMm40/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ciyWks/btrBaM5IIhI/AB8NLxhC1CTE0JCIIZMm40/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ciyWks/btrBaM5IIhI/AB8NLxhC1CTE0JCIIZMm40/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FciyWks%2FbtrBaM5IIhI%2FAB8NLxhC1CTE0JCIIZMm40%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;553&quot; height=&quot;303&quot; data-origin-width=&quot;728&quot; data-origin-height=&quot;399&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;0&amp;nbsp;~&amp;nbsp;65535&amp;nbsp;할당&amp;nbsp;가능&lt;/li&gt;
&lt;li&gt;0 ~ 1023: 잘 알려진 포트, 사용하지 않는 것이 좋음 &lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;FTP - 20, 21&lt;/li&gt;
&lt;li&gt;TELNET - 23&lt;/li&gt;
&lt;li&gt;HTTP - 80&lt;/li&gt;
&lt;li&gt;HTTPS - 443&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;6) DNS&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도메인&amp;nbsp;네임&amp;nbsp;시스템(Domain&amp;nbsp;Name&amp;nbsp;System)&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;IP 주소를 도메인 명으로 매핑한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;851&quot; data-origin-height=&quot;459&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dZ3rR4/btrA541yhCq/WwU8ZghxMYRqGEZS54WZl1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dZ3rR4/btrA541yhCq/WwU8ZghxMYRqGEZS54WZl1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dZ3rR4/btrA541yhCq/WwU8ZghxMYRqGEZS54WZl1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdZ3rR4%2FbtrA541yhCq%2FWwU8ZghxMYRqGEZS54WZl1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;595&quot; height=&quot;321&quot; data-origin-width=&quot;851&quot; data-origin-height=&quot;459&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;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. URI와 웹 브라우저 요청 흐름&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) URI&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Uniform:&amp;nbsp;리소스&amp;nbsp;식별하는&amp;nbsp;통일된&amp;nbsp;방식&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Resource: 자원, URI로 식별할 수 있는 모든 것(제한 없음)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;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;URI는 로케이터(Locator), 이름(Name) 또는 둘다 추가로 분류될 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;552&quot; data-origin-height=&quot;323&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eeaEdE/btrBaxVlkKG/unsnCJdGSCfz9tbWdgwjqK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eeaEdE/btrBaxVlkKG/unsnCJdGSCfz9tbWdgwjqK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eeaEdE/btrBaxVlkKG/unsnCJdGSCfz9tbWdgwjqK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeeaEdE%2FbtrBaxVlkKG%2FunsnCJdGSCfz9tbWdgwjqK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;256&quot; data-origin-width=&quot;552&quot; data-origin-height=&quot;323&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;h4 data-ke-size=&quot;size20&quot;&gt;2) URL, URN&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;URL - Locator: 리소스가 있는 위치를 지정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;URN - Name: 리소스에 이름을 부여&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;918&quot; data-origin-height=&quot;386&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vsKcY/btrA5sVXR8Z/YM6SxanqDMaaJjwth5bhsK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vsKcY/btrA5sVXR8Z/YM6SxanqDMaaJjwth5bhsK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vsKcY/btrA5sVXR8Z/YM6SxanqDMaaJjwth5bhsK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvsKcY%2FbtrA5sVXR8Z%2FYM6SxanqDMaaJjwth5bhsK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;293&quot; data-origin-width=&quot;918&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;&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;예) urn:isbn:8960777331 : 특정 책의 isbn URN&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;3) URL&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;scheme://[userinfo@]host[:port][/path][?query][#fragment]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;https://www.google.com:443/search?q=hello&amp;amp;hl=ko&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프로토콜(https)&lt;/li&gt;
&lt;li&gt;호스트명(www.google.com)&lt;/li&gt;
&lt;li&gt;포트 번호(443)&lt;/li&gt;
&lt;li&gt;패스(/search)&lt;/li&gt;
&lt;li&gt;쿼리 파라미터(q=hello&amp;amp;hl=ko)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;scheme (https)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주로&amp;nbsp;프로토콜&amp;nbsp;사용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로토콜: 어떤 방식으로 자원에 접근할 것인가 하는 약속 규칙&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;예) http, https, ftp 등등&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;http는 80 포트, https는 443 포트를 주로 사용, 포트는 생략 가능&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;https는 http에 보안 추가 (HTTP Secure)&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;userinfo&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;URL에&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;size18&quot;&gt;&lt;b&gt;host &lt;b&gt;(www.google.com)&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;도메인명 또는 IP 주소를 직접 사용가능&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;port (&lt;b&gt;443)&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;일반적으로 생략, 생략시 http는 80, https는 443&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;path (search)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리소스&amp;nbsp;경로(path),&amp;nbsp;계층적&amp;nbsp;구조&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;/home/file1.jpg&lt;/li&gt;
&lt;li&gt;/members&lt;/li&gt;
&lt;li&gt;/members/100, /items/iphone12&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;query (q=hello&amp;amp;hl=ko)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;key=value&amp;nbsp;형태&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;?로 시작, &amp;amp;로 추가 가능 ?keyA=valueA&amp;amp;keyB=valueB&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;query parameter, query string 등으로 불림, 웹서버에 제공하는 파라미터, 문자 형태&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;fragment&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예) https://docs.spring.io/spring-boot/docs/current/reference/html/getting-started.html&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;#getting-started.introducing-spring-boot&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;html&amp;nbsp;내부&amp;nbsp;북마크&amp;nbsp;등에&amp;nbsp;사용 &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;4) 웹 브라우저 요청 흐름&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;HTTP 메시지 생성&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. DNS 서버에서 조회해 IP 주소를 얻어오고&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. HTTP PORT를 확인해&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. HTTP 요청 메시지를 생성한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;695&quot; data-origin-height=&quot;278&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pYXEC/btrA539zQxy/RqvwGWx8WMzspGCp3SdhPk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pYXEC/btrA539zQxy/RqvwGWx8WMzspGCp3SdhPk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pYXEC/btrA539zQxy/RqvwGWx8WMzspGCp3SdhPk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpYXEC%2FbtrA539zQxy%2FRqvwGWx8WMzspGCp3SdhPk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;258&quot; data-origin-width=&quot;695&quot; data-origin-height=&quot;278&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;441&quot; data-origin-height=&quot;189&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/owtU3/btrA97JvJC7/srNy4ttWIaj0dWQIIUVvKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/owtU3/btrA97JvJC7/srNy4ttWIaj0dWQIIUVvKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/owtU3/btrA97JvJC7/srNy4ttWIaj0dWQIIUVvKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FowtU3%2FbtrA97JvJC7%2FsrNy4ttWIaj0dWQIIUVvKk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;359&quot; height=&quot;154&quot; data-origin-width=&quot;441&quot; data-origin-height=&quot;189&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;HTTP 메시지 전송&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 웹 브라우저가 HTTP 메시지 생성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. SOCKET 라이브러리를 통해 전달&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; - A: TCP/IP 연결(IP, PORT, 3 way handshaking)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; - B: 데이터 전달&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. TCP/IP 패킷 생성, HTTP 메시지 포함&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;635&quot; data-origin-height=&quot;375&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yv3xD/btrBaymvITv/YSWr6mOIuBEFg5khKJe8Q1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yv3xD/btrBaymvITv/YSWr6mOIuBEFg5khKJe8Q1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yv3xD/btrBaymvITv/YSWr6mOIuBEFg5khKJe8Q1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fyv3xD%2FbtrBaymvITv%2FYSWr6mOIuBEFg5khKJe8Q1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;635&quot; height=&quot;375&quot; data-origin-width=&quot;635&quot; data-origin-height=&quot;375&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;757&quot; data-origin-height=&quot;375&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Pc0XF/btrA68WWI2P/FkvJGmMz8iUJSSC8vM4T81/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Pc0XF/btrA68WWI2P/FkvJGmMz8iUJSSC8vM4T81/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Pc0XF/btrA68WWI2P/FkvJGmMz8iUJSSC8vM4T81/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPc0XF%2FbtrA68WWI2P%2FFkvJGmMz8iUJSSC8vM4T81%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;584&quot; height=&quot;289&quot; data-origin-width=&quot;757&quot; data-origin-height=&quot;375&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;size18&quot;&gt;&lt;b&gt;HTTP 응답&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;웹 브라우저는 응답 패킷을 받아 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-origin-width=&quot;636&quot; data-origin-height=&quot;151&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cIy8L0/btrBaxOGD4E/Fe60Lc3QWCYia9t0H0QbK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cIy8L0/btrBaxOGD4E/Fe60Lc3QWCYia9t0H0QbK0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cIy8L0/btrBaxOGD4E/Fe60Lc3QWCYia9t0H0QbK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcIy8L0%2FbtrBaxOGD4E%2FFe60Lc3QWCYia9t0H0QbK0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;151&quot; data-origin-width=&quot;636&quot; data-origin-height=&quot;151&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;618&quot; data-origin-height=&quot;142&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/y6Jg2/btrA4KWBiYw/4a1Bk2KHxQewesC6z8vLjK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/y6Jg2/btrA4KWBiYw/4a1Bk2KHxQewesC6z8vLjK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/y6Jg2/btrA4KWBiYw/4a1Bk2KHxQewesC6z8vLjK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fy6Jg2%2FbtrA4KWBiYw%2F4a1Bk2KHxQewesC6z8vLjK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;138&quot; data-origin-width=&quot;618&quot; data-origin-height=&quot;142&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;655&quot; data-origin-height=&quot;179&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xnyK9/btrBa8t9EOj/gSG8hbleBo2eydTcJjW7Nk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xnyK9/btrBa8t9EOj/gSG8hbleBo2eydTcJjW7Nk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xnyK9/btrBa8t9EOj/gSG8hbleBo2eydTcJjW7Nk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxnyK9%2FbtrBa8t9EOj%2FgSG8hbleBo2eydTcJjW7Nk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;615&quot; height=&quot;168&quot; data-origin-width=&quot;655&quot; data-origin-height=&quot;179&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;440&quot; data-origin-height=&quot;213&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cIElVg/btrA7OcQFaQ/uVTgll1bMAyACx2FjKFKP1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cIElVg/btrA7OcQFaQ/uVTgll1bMAyACx2FjKFKP1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cIElVg/btrA7OcQFaQ/uVTgll1bMAyACx2FjKFKP1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcIElVg%2FbtrA7OcQFaQ%2FuVTgll1bMAyACx2FjKFKP1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;353&quot; height=&quot;171&quot; data-origin-width=&quot;440&quot; data-origin-height=&quot;213&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;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;출처&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;a href=&quot;https://www.inflearn.com/course/http-%EC%9B%B9-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;모든 개발자를 위한 HTTP 웹 기본 지식(김영한)&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;&amp;nbsp;&lt;/p&gt;</description>
      <category>HTTP</category>
      <category>http</category>
      <category>TCP</category>
      <category>URL</category>
      <author>mandykr</author>
      <guid isPermaLink="true">https://mandykr.tistory.com/74</guid>
      <comments>https://mandykr.tistory.com/74#entry74comment</comments>
      <pubDate>Sat, 30 Apr 2022 14:22:31 +0900</pubDate>
    </item>
    <item>
      <title>[JPA] 8. JPQL(2)</title>
      <link>https://mandykr.tistory.com/69</link>
      <description>&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;2. 페치 조인&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;span&gt;4.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;엔티티 직접 사용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5. Named 쿼리&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;6. 벌크 연산&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 경로 표현식&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;경로 표현식은 .(점)을 찍어 객체 그래프를 탐색하는 것이다.&lt;/p&gt;
&lt;pre id=&quot;code_1649918364943&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;select m.username -&amp;gt; 상태 필드
 from Member m
 join m.team t -&amp;gt; 단일 값 연관 필드
 join m.orders o -&amp;gt; 컬렉션 값 연관 필드
where t.name = '팀A'&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;상태 필드(state field)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히 값을 저장하기 위한 필드(ex: m.username)&lt;/p&gt;
&lt;pre id=&quot;code_1649918520859&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;select m.name, m.age from Member 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;size18&quot;&gt;&lt;b&gt;연관 필드(association field)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연관관계를 위한 필드&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;단일 값 연관 필드: @ManyToOne,&amp;nbsp;@OneToOne,&amp;nbsp;대상이&amp;nbsp;엔티티(ex:&amp;nbsp;m.team)&lt;/li&gt;
&lt;li&gt;컬렉션 값 연관 필드: @OneToMany,&amp;nbsp;@ManyToMany,&amp;nbsp;대상이&amp;nbsp;컬렉션(ex:&amp;nbsp;m.orders)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;단일 값 연관 경로&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;묵시적 내부 조인 발생, team.name 탐색 O&lt;/p&gt;
&lt;pre id=&quot;code_1649918611633&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;select m.team.name from Member 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;&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&gt;묵시적 내부 조인 발생,&lt;span&gt; t.members.name과 같이 &lt;/span&gt;&lt;/span&gt;탐색 X&lt;/p&gt;
&lt;pre id=&quot;code_1649918724225&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;select t.members from Team t&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FROM 절에서 명시적 조인을 통해 별칭을 얻으면 별칭을 통 해 탐색 가능하다.&lt;/p&gt;
&lt;pre id=&quot;code_1649919179561&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;select m.name from Team t join t.members 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;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;조인은 SQL 튜닝에 중요 포인트&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;묵시적 조인은 조인이 일어나는 상황을 한눈에 파악하기 어려움&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPQL과 SQL의 형태를 최대한 맞춰 명시적 조인을 사용하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 페치 조인(fetch join)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SQL 조인의 종류가 아니며 JPQL에서 성능 최적화를 위해 제공하는 기능&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연관된 엔티티나 컬렉션을 SQL 한번에 함께 조회&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;join fetch 명령어 사용&lt;/p&gt;
&lt;pre id=&quot;code_1649908623033&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;String jpql = &quot;select m from Member m join fetch m.team&quot;;
List&amp;lt;Member&amp;gt; resultList = em.createQuery(jpql, Member.class)
        .getResultList();

for (Member member : resultList) {
    System.out.println(&quot;username = &quot; + member.getName() + &quot;,&quot; +
            &quot; teamName = &quot; + member.getTeam().getName());
}&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;일반 조인&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;( &lt;b&gt;select 절에서 m.team을 조회하지 않는 경우&lt;/b&gt; )&lt;/p&gt;
&lt;pre id=&quot;code_1649908645735&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;select m from Member m join m.team&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조회하는 시점에 Team의 프록시가 영속성 컨텍스트에 담기고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉시 로딩인 경우 Member를 조회한 후 Team을 조회하는 쿼리가 실행되고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지연 로딩인 경우 실제 Team의 데이터를 사용하는 시점에 조회 쿼리가 실행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[즉시 로딩]&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;392&quot; data-origin-height=&quot;417&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lLPeY/btrzjnO5uxW/1pQ62ZKcajak2zHqCMzl2K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lLPeY/btrzjnO5uxW/1pQ62ZKcajak2zHqCMzl2K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lLPeY/btrzjnO5uxW/1pQ62ZKcajak2zHqCMzl2K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlLPeY%2FbtrzjnO5uxW%2F1pQ62ZKcajak2zHqCMzl2K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;417&quot; data-origin-width=&quot;392&quot; data-origin-height=&quot;417&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&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;302&quot; data-origin-height=&quot;150&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/z2l6g/btrziEqexNd/3YbSg3o0DZgoLT9Fgw4ozk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/z2l6g/btrziEqexNd/3YbSg3o0DZgoLT9Fgw4ozk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/z2l6g/btrziEqexNd/3YbSg3o0DZgoLT9Fgw4ozk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fz2l6g%2FbtrziEqexNd%2F3YbSg3o0DZgoLT9Fgw4ozk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;302&quot; height=&quot;150&quot; data-origin-width=&quot;302&quot; data-origin-height=&quot;150&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;301&quot; data-origin-height=&quot;154&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vWwVv/btrzn9Pj447/X4yiW7GWzTIbAct7LO7p11/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vWwVv/btrzn9Pj447/X4yiW7GWzTIbAct7LO7p11/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vWwVv/btrzn9Pj447/X4yiW7GWzTIbAct7LO7p11/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvWwVv%2Fbtrzn9Pj447%2FX4yiW7GWzTIbAct7LO7p11%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;301&quot; height=&quot;154&quot; data-origin-width=&quot;301&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;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;260&quot; data-origin-height=&quot;63&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/scxz5/btrzmkw05GF/pQBlnntNk7HTR4L7PJJRE1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/scxz5/btrzmkw05GF/pQBlnntNk7HTR4L7PJJRE1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/scxz5/btrzmkw05GF/pQBlnntNk7HTR4L7PJJRE1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fscxz5%2Fbtrzmkw05GF%2FpQBlnntNk7HTR4L7PJJRE1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;260&quot; height=&quot;63&quot; data-origin-width=&quot;260&quot; data-origin-height=&quot;63&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[지연 로딩]&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;392&quot; data-origin-height=&quot;417&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lLPeY/btrzjnO5uxW/1pQ62ZKcajak2zHqCMzl2K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lLPeY/btrzjnO5uxW/1pQ62ZKcajak2zHqCMzl2K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lLPeY/btrzjnO5uxW/1pQ62ZKcajak2zHqCMzl2K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlLPeY%2FbtrzjnO5uxW%2F1pQ62ZKcajak2zHqCMzl2K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;417&quot; data-origin-width=&quot;392&quot; data-origin-height=&quot;417&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&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;337&quot; data-origin-height=&quot;199&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cfM2ug/btrziDSqk9I/F0XUbuC8bW2r9ekrdAAWfK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cfM2ug/btrziDSqk9I/F0XUbuC8bW2r9ekrdAAWfK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cfM2ug/btrziDSqk9I/F0XUbuC8bW2r9ekrdAAWfK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcfM2ug%2FbtrziDSqk9I%2FF0XUbuC8bW2r9ekrdAAWfK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;337&quot; height=&quot;199&quot; data-origin-width=&quot;337&quot; data-origin-height=&quot;199&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&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;336&quot; data-origin-height=&quot;174&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bGZxXR/btrziEX6hrN/gvpa6Fln00MwLuKB1DKsu1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bGZxXR/btrziEX6hrN/gvpa6Fln00MwLuKB1DKsu1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bGZxXR/btrziEX6hrN/gvpa6Fln00MwLuKB1DKsu1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbGZxXR%2FbtrziEX6hrN%2Fgvpa6Fln00MwLuKB1DKsu1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;336&quot; height=&quot;174&quot; data-origin-width=&quot;336&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;&lt;span&gt;회원의 소속 팀이 모두 다른 경우 조회되는 Member 수 만큼 Team을 조회하는 쿼리가 실행되기 때문에&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;지연로딩, 즉시로딩 모두 N + 1 문제가 발생한다.&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;span&gt;페치 조인&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;페치 조인은 엔티티에 직접 적용하는 &lt;b&gt;글로벌 로딩 전략&lt;/b&gt;보다 우선하다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;( @OneToMany(fetch = FetchType.LAZY) // 글로벌 로딩 전략 )&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1649908684323&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;select m from Member m join fetch m.team&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조회하는 시점에 Team의 프록시가 아닌 실제 엔티티가 영속성 컨텍스트에 담긴다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;443&quot; data-origin-height=&quot;527&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bLizoa/btrzkx4KNvS/0jCr7fDVxwHkZPgHS8Asd1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bLizoa/btrzkx4KNvS/0jCr7fDVxwHkZPgHS8Asd1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bLizoa/btrzkx4KNvS/0jCr7fDVxwHkZPgHS8Asd1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbLizoa%2Fbtrzkx4KNvS%2F0jCr7fDVxwHkZPgHS8Asd1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;527&quot; data-origin-width=&quot;443&quot; data-origin-height=&quot;527&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;컬렉션 페치 조인&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Team과 Member를 DB에서 일대다 조인하면 &lt;b&gt;Member의 수 만큼 Team이 중복되어 조회&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;445&quot; data-origin-height=&quot;253&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bitxHg/btrzkypOf2K/a5Hhr29UZuFEjrGVHG81c0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bitxHg/btrzkypOf2K/a5Hhr29UZuFEjrGVHG81c0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bitxHg/btrzkypOf2K/a5Hhr29UZuFEjrGVHG81c0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbitxHg%2FbtrzkypOf2K%2Fa5Hhr29UZuFEjrGVHG81c0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;445&quot; height=&quot;253&quot; data-origin-width=&quot;445&quot; data-origin-height=&quot;253&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팀A에 회원1과 회원2가 소속되어 있을 때 팀A에 해당하는 데이터가 2개의 row로 조회된다.&lt;/p&gt;
&lt;pre id=&quot;code_1649919827930&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[SQL]
SELECT M.*, T.* FROM MEMBER M
INNER JOIN TEAM T ON M.TEAM_ID=T.ID&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;440&quot; data-origin-height=&quot;214&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oJNV4/btrziwsn3Rz/27dewTBFynOjqdXfz2mULk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oJNV4/btrziwsn3Rz/27dewTBFynOjqdXfz2mULk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oJNV4/btrziwsn3Rz/27dewTBFynOjqdXfz2mULk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoJNV4%2Fbtrziwsn3Rz%2F27dewTBFynOjqdXfz2mULk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;214&quot; data-origin-width=&quot;440&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPQL도 마찬가지로 같은 객체이지만 &lt;span&gt;팀A의 참조값이 &lt;/span&gt;페치 조인 결과 리스트에 중복되어 저장된다.&lt;/p&gt;
&lt;pre id=&quot;code_1649919918282&quot; class=&quot;reasonml&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;String jpql = &quot;select t from Team t join fetch t.members where t.name = '팀A'&quot;;
List&amp;lt;Team&amp;gt; resultList = em.createQuery(jpql, Team.class)
        .getResultList();

for(Team team : resultList) {
    System.out.println(&quot;teamname = &quot; + team.getName() + &quot;, team = &quot; + team);
    for (Member member : team.getMembers()) {
        System.out.println(&quot;-&amp;gt; username = &quot; + member.getName()+ &quot;, member = &quot; + member);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;540&quot; data-origin-height=&quot;131&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yw44x/btrzoOxutk7/8O3Mq8wB2X3juBdveBm8u0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yw44x/btrzoOxutk7/8O3Mq8wB2X3juBdveBm8u0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yw44x/btrzoOxutk7/8O3Mq8wB2X3juBdveBm8u0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fyw44x%2FbtrzoOxutk7%2F8O3Mq8wB2X3juBdveBm8u0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;540&quot; height=&quot;131&quot; data-origin-width=&quot;540&quot; data-origin-height=&quot;131&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;470&quot; data-origin-height=&quot;135&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bncyYb/btrzpVKbPrn/K2UEw4FY3xQ3e7jY7MNvnk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bncyYb/btrzpVKbPrn/K2UEw4FY3xQ3e7jY7MNvnk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bncyYb/btrzpVKbPrn/K2UEw4FY3xQ3e7jY7MNvnk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbncyYb%2FbtrzpVKbPrn%2FK2UEw4FY3xQ3e7jY7MNvnk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;135&quot; data-origin-width=&quot;470&quot; data-origin-height=&quot;135&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;&lt;b&gt;페치 조인과 DISTINCT&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SQL 상에서 DISTINCT는 서로 다른 row이기 때문에 중복이 제거되지 않고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;JPQL 상에서 DISTINCT는 JPA가 중복되는(&lt;b&gt;같은 식별자를 가진)&lt;/b&gt; 엔티티를 제거해준다.&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1649909982344&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;select distinct t from Team t join fetch t.members where t.name = '팀A'&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;532&quot; data-origin-height=&quot;67&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HKsZw/btrzgHUMcpk/900Z7u02YaJ0yvIxjWc0a1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HKsZw/btrzgHUMcpk/900Z7u02YaJ0yvIxjWc0a1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HKsZw/btrzgHUMcpk/900Z7u02YaJ0yvIxjWc0a1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHKsZw%2FbtrzgHUMcpk%2F900Z7u02YaJ0yvIxjWc0a1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;532&quot; height=&quot;67&quot; data-origin-width=&quot;532&quot; data-origin-height=&quot;67&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;513&quot; data-origin-height=&quot;160&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bIWWQI/btrzkxExjNG/WIFBWvWXfWn4C1gG15TFKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bIWWQI/btrzkxExjNG/WIFBWvWXfWn4C1gG15TFKK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bIWWQI/btrzkxExjNG/WIFBWvWXfWn4C1gG15TFKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbIWWQI%2FbtrzkxExjNG%2FWIFBWvWXfWn4C1gG15TFKK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;513&quot; height=&quot;160&quot; data-origin-width=&quot;513&quot; data-origin-height=&quot;160&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;&lt;b&gt;페치 조인과 일반 조인의 차이&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPQL은 결과를 반환할 때 연관관계를 고려하지 않고&amp;nbsp;SELECT 절에 지정한 엔티티만 조회할 뿐이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;페치 조인은 연관된 엔티티도 함께 조회한다. 객체 그래프를 SQL 한번에 조회하는 개념.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;( N + 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;size18&quot;&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;/p&gt;
&lt;pre id=&quot;code_1649921691803&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;select t from Team t join fetch t.members m where m.age &amp;gt; 10&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;&lt;b&gt;컬렉션을 페치 조인하면 페이징 API(setFirstResult, setMaxResults)를 사용할 수 없다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일대다는 데이터 중복으로 row 수가 증가하기 때문에 페이징할 수 없다.&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;페치 조인의 특징&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;연관된 엔티티들을 SQL 한 번으로 조회&lt;/li&gt;
&lt;li&gt;N + 1 문제를 해결해 성능 최적화&lt;/li&gt;
&lt;li&gt;실무에서 글로벌 로딩 전략은 모두 지연 로딩&lt;/li&gt;
&lt;li&gt;최적화가 필요한 곳은 페치 조인 적용&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;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_1649913360955&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;select m from Member m join fetch m.team&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;페치 조인 보다는 일반 조인을 사용하고 필요한 데이터들만 조회해서 DTO로 반환하는 것이 효과적이다.&lt;/p&gt;
&lt;pre id=&quot;code_1649913303546&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;select m, t from Member m join m.team t&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 다형성 쿼리&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;624&quot; data-origin-height=&quot;335&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lDjfq/btrznbG6y5x/Fgb7AesKJoXtcUQIOVSSo1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lDjfq/btrznbG6y5x/Fgb7AesKJoXtcUQIOVSSo1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lDjfq/btrznbG6y5x/Fgb7AesKJoXtcUQIOVSSo1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlDjfq%2FbtrznbG6y5x%2FFgb7AesKJoXtcUQIOVSSo1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;335&quot; data-origin-width=&quot;624&quot; data-origin-height=&quot;335&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;&lt;b&gt;Type&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;Item&amp;nbsp;중에&amp;nbsp;Book,&amp;nbsp;Movie를&amp;nbsp;조회해라&lt;/p&gt;
&lt;pre id=&quot;code_1649913725371&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[JPQL]
select i from Item i
where type(i) IN (Book, Movie)&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1649913748448&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[SQL]
select i from i
where i.DTYPE in (&amp;lsquo;B&amp;rsquo;, &amp;lsquo;M&amp;rsquo;)&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;TREAT(JPA&amp;nbsp;2.1)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예)&amp;nbsp;부모인&amp;nbsp;Item과&amp;nbsp;자식&amp;nbsp;Book이&amp;nbsp;있다.&lt;/p&gt;
&lt;pre id=&quot;code_1649913859090&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[JPQL]
select i from Item i
where treat(i as Book).auther = &amp;lsquo;kim&amp;rsquo;&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1649913868236&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[SQL]
select i.* from Item i
where i.DTYPE = &amp;lsquo;B&amp;rsquo; and i.auther = &amp;lsquo;kim&amp;rsquo;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 엔티티 직접 사용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;기본 키 값&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPQL에서 엔티티를 직접 사용하면 SQL에서 해당 엔티티의 기본 키 값을 사용한다.&lt;/p&gt;
&lt;pre id=&quot;code_1649914014739&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[JPQL]
select count(m.id) from Member m //엔티티의 아이디를 사용
select count(m) from Member m //엔티티를 직접 사용&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1649914007843&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[SQL]
select count(m.id) as cnt from Member 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;파라미터로 전달하는 경우도 마찬가지로 기본 키 값을 사용한다.&lt;/p&gt;
&lt;pre id=&quot;code_1649914082892&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[JPQL]
select m from Member m where m.id = :memberId
select m from Member m where m = :member&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1649914103210&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[SQL]
select m.* from Member m where m.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;size18&quot;&gt;&lt;b&gt;외래 키 값&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연관된 엔티티를 직접 사용하거나 연관된 엔티티의 기본 키 값을 사용해도 SQL에서는 외래 키 값을 사용한다.&lt;/p&gt;
&lt;pre id=&quot;code_1649914189748&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[JPQL]
select m from Member m where m.team.id = :teamId
select m from Member m where m.team = :team&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1649914208949&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[SQL]
select m.* from Member m where m.team_id=?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. Named 쿼리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;미리 정의해서 이름을 부여해 두고 사용하는 JPQL&lt;/p&gt;
&lt;pre id=&quot;code_1649915336184&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
@NamedQuery(
        name = &quot;Member.findByUsername&quot;,
        query=&quot;select m from Member m where m.name = :username&quot;)
public class Member {&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1649915304966&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;List&amp;lt;Member&amp;gt; resultList = em.createNamedQuery(&quot;Member.findByUsername&quot;, Member.class)
        .setParameter(&quot;username&quot;, &quot;회원1&quot;)
        .getResultList();

for (Member member : resultList) {
    System.out.println(&quot;username = &quot; + member);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션 로딩 시점에 Named 쿼리를 SQL로 파싱하기 때문에 사전에 &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;Spring Data JPA의 @Query 어노테이션을 사용할 때 Named 쿼리로 등록된다.&lt;/p&gt;
&lt;pre id=&quot;code_1649915246919&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface UserRepository extends JpaRepository&amp;lt;User, Long&amp;gt; {

  @Query(&quot;select u from User u where u.emailAddress = ?1&quot;)
  User findByEmailAddress(String emailAddress);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. 벌크 연산&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA 변경 감지 기능으로 실행하려면 너무 많은 SQL이 실행되는 경우&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;예) 재고가 10개 미만인 모든 상품의 가격을 10% 상승하려면?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1.&amp;nbsp;재고가&amp;nbsp;10개&amp;nbsp;미만인&amp;nbsp;상품을&amp;nbsp;리스트로&amp;nbsp;조회한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 상품 엔티티의 가격을 10% 증가한다.&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;변경 감지를 이용하면 변경된 데이터가 100건이라면 100번의 UPDATE SQL 실행&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_1649915861115&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;int resultCount = em.createQuery(&quot;update Member m set m.age = 20&quot;)
                .executeUpdate();&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;executeUpdate()의&amp;nbsp;결과는&amp;nbsp;영향받은&amp;nbsp;엔티티&amp;nbsp;수 반환&lt;/li&gt;
&lt;li&gt;UPDATE,&amp;nbsp;DELETE&amp;nbsp;지원&lt;/li&gt;
&lt;li&gt;INSERT(insert into .. select, 하이버네이트 지원)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;766&quot; data-origin-height=&quot;456&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Ntik6/btrzjbVDiTH/JTpvafl0JtCTuIP36jYx81/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Ntik6/btrzjbVDiTH/JTpvafl0JtCTuIP36jYx81/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Ntik6/btrzjbVDiTH/JTpvafl0JtCTuIP36jYx81/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNtik6%2FbtrzjbVDiTH%2FJTpvafl0JtCTuIP36jYx81%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;685&quot; height=&quot;408&quot; data-origin-width=&quot;766&quot; data-origin-height=&quot;456&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1649917636643&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;em.persist(memberA);

int resultCount = em.createQuery(&quot;update Member m set m.age = 20&quot;)
        .executeUpdate();

// em.clear();

Member findMemberA = em.find(Member.class, memberA.getId());
System.out.println(&quot;memberA.age = &quot; + findMemberA.getAge());&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;135&quot; data-origin-height=&quot;23&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d1cKI3/btrzoOkzexE/JR1kNDK3K0Dp1NFszkHyR0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d1cKI3/btrzoOkzexE/JR1kNDK3K0Dp1NFszkHyR0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d1cKI3/btrzoOkzexE/JR1kNDK3K0Dp1NFszkHyR0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd1cKI3%2FbtrzoOkzexE%2FJR1kNDK3K0Dp1NFszkHyR0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;135&quot; height=&quot;23&quot; data-origin-width=&quot;135&quot; data-origin-height=&quot;23&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPQL이 실행될 때 영속성 컨텍스트에 저장된 SQL은 함께 flush 되어 나가기 때문에 문제가 없지만&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영속성 컨텍스트의 memberA 나이와 벌크 연산으로 수정된 memberA 나이가 달라지는 경우&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;벌크 연산 이 후 memberA를 조회하면 1차 캐시에서 조회하기 때문에 변경되기 전의 나이가 조회된다.&lt;/b&gt;&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;벌크 연산 수행 후 영속성 컨텍스트를 초기화&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;Spring Data JPA에서는 @Modifying의 clearAutomatically 옵션을 통해 영속성 컨텍스트를 초기화 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;clearAutomatically의 기본값은 false이고 true로 설정해 주어야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1649918254947&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package org.springframework.data.jpa.repository;

@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })
@Documented
public @interface Modifying {
	boolean flushAutomatically() default false;
	boolean clearAutomatically() default false;
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1649917834767&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Modifying(clearAutomatically = true)
@Query(&quot;update User u set u.firstname = ?1 where u.lastname = ?2&quot;)
int setFixedFirstnameFor(String firstname, String lastname);&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;a href=&quot;https://www.inflearn.com/course/ORM-JPA-Basic&quot;&gt;https://www.inflearn.com/course/ORM-JPA-Basic&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;자바 ORM 표준 JPA 프로그래밍 - 기본편(김영한)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>ORM/JPA</category>
      <category>fetch join</category>
      <category>named query</category>
      <category>벌크 연산</category>
      <author>mandykr</author>
      <guid isPermaLink="true">https://mandykr.tistory.com/69</guid>
      <comments>https://mandykr.tistory.com/69#entry69comment</comments>
      <pubDate>Thu, 14 Apr 2022 16:38:38 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] 3. 스프링 빈</title>
      <link>https://mandykr.tistory.com/68</link>
      <description>&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;2. 빈 생명주기 콜백 사용 방법&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;4. 웹 스코프&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 빈 생명주기 콜백&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터베이스 커넥션 풀이나, 네트워크 소켓처럼 &lt;b&gt;애플리케이션 시작 시점에 필요한 연결을 미리 해두고, &lt;/b&gt;&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;p data-ke-size=&quot;size16&quot;&gt;예제로 이 NetworkClient 는 애플리케이션 시작 시점에 connect() 를 호출해서 연결을 맺어두어야 하고,&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션이 종료되면 disConnect() 를 호출해서 연결을&amp;nbsp;끊어야&amp;nbsp;한다.&lt;/p&gt;
&lt;pre id=&quot;code_1650075868189&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class NetworkClient {

    private String url;
    
    public NetworkClient() {
        System.out.println(&quot;생성자 호출, url = &quot; + url);
        connect();
        call(&quot;초기화 연결 메시지&quot;);
    }
    
    public void setUrl(String url) {
        this.url = url;
    }
    
    //서비스 시작시 호출
    public void connect() {
        System.out.println(&quot;connect: &quot; + url);
    }
    
    public void call(String message) {
        System.out.println(&quot;call: &quot; + url + &quot; message = &quot; + message);
    }
    
    //서비스 종료시 호출
    public void disconnect() {
        System.out.println(&quot;close: &quot; + url);
    }
}&lt;/code&gt;&lt;/pre&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;생성자는&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;수행한다.&lt;/p&gt;
&lt;pre id=&quot;code_1650075328611&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
class LifeCycleConfig {

    @Bean
    public NetworkClient networkClient() {
        NetworkClient networkClient = new NetworkClient(&quot;http://hello-spring.dev&quot;);
        return networkClient;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NetworkClient를 스프링 빈으로 등록할 때&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;nbsp;명확하게&amp;nbsp;나누는&amp;nbsp;것이&amp;nbsp;유지보수&amp;nbsp;관점에서&amp;nbsp;좋다.&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1650075679608&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;NetworkClient networkClient = new NetworkClient();
networkClient.setUrl(&quot;http://hello-spring.dev&quot;);&lt;/code&gt;&lt;/pre&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;있다.&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;b&gt;객체 생성 -&amp;gt; 의존관계 주입&lt;/b&gt; 순서이기 때문에 객체의 초기화 작업이 모두 완료되기 전,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체 생성 단계(NetworkClient 생성자 호출)에서는 url 정보를 알 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 &lt;b&gt;객체의 초기화가 완료된 시점을 알아야&lt;/b&gt; url 정보가 초기화된 NetworkClient의 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;b&gt;초기화 시점을 알려주는 다양한 기능을&amp;nbsp;제공&lt;/b&gt;한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한&amp;nbsp;스프링은&amp;nbsp;스&lt;b&gt;프링&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;있다.&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;gt; 스프링 빈 생성 -&amp;gt; 의존관계 주입 -&amp;gt; &lt;b&gt;초기화 콜백&lt;/b&gt; -&amp;gt; 사용 -&amp;gt; &lt;b&gt;소멸전 콜백&lt;/b&gt; -&amp;gt; 스프링 종료&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;초기화&amp;nbsp;콜백:&amp;nbsp;빈이&amp;nbsp;생성되고,&amp;nbsp;빈의&amp;nbsp;의존관계&amp;nbsp;주입이&amp;nbsp;완료된&amp;nbsp;후&amp;nbsp;호출&lt;/li&gt;
&lt;li&gt;소멸전&amp;nbsp;콜백:&amp;nbsp;빈이&amp;nbsp;소멸되기&amp;nbsp;직전에&amp;nbsp;호출&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 빈 생명주기 콜백 사용 방법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링은&amp;nbsp;크게&amp;nbsp;3가지&amp;nbsp;방법으로&amp;nbsp;빈&amp;nbsp;생명주기&amp;nbsp;콜백을&amp;nbsp;지원한다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인터페이스(InitializingBean,&amp;nbsp;DisposableBean)&lt;/li&gt;
&lt;li&gt;설정&amp;nbsp;정보에&amp;nbsp;초기화&amp;nbsp;메서드,&amp;nbsp;종료&amp;nbsp;메서드&amp;nbsp;지정&lt;/li&gt;
&lt;li&gt;@PostConstruct,&amp;nbsp;@PreDestroy&amp;nbsp;애노테이션&amp;nbsp;지원&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) 인터페이스(InitializingBean, DisposableBean)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;InitializingBean : &lt;/b&gt;afterPropertiesSet() 메서드로 초기화를 지원한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;DisposableBean : &lt;/b&gt;destroy() 메서드로 소멸을 지원한다&lt;/p&gt;
&lt;pre id=&quot;code_1650076703172&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class NetworkClient implements InitializingBean, DisposableBean {
    //...   
    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println(&quot;NetworkClient.afterPropertiesSet&quot;);
        connect();
        call(&quot;초기화 연결 메시지&quot;);
    }
    
    @Override
    public void destroy() throws Exception {
        System.out.println(&quot;NetworkClient.destroy&quot;);
        disConnect();
    }  
}&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 alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;869&quot; data-origin-height=&quot;329&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/db5kGm/btrzyYf0PUR/SkSfO9PKKLjTsSuKAF7WN1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/db5kGm/btrzyYf0PUR/SkSfO9PKKLjTsSuKAF7WN1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/db5kGm/btrzyYf0PUR/SkSfO9PKKLjTsSuKAF7WN1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdb5kGm%2FbtrzyYf0PUR%2FSkSfO9PKKLjTsSuKAF7WN1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;638&quot; height=&quot;242&quot; data-origin-width=&quot;869&quot; data-origin-height=&quot;329&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체 초기화가 완료된 시점에 afterPropertiesSet()가 호출되었고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 컨테이너의 종료가 호출되자 destroy() 소멸 메서드가 호출되었다.&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이&amp;nbsp;인터페이스는&amp;nbsp;스프링&amp;nbsp;전용&amp;nbsp;인터페이스다.&amp;nbsp;해당&amp;nbsp;코드가&amp;nbsp;스프링&amp;nbsp;전용&amp;nbsp;인터페이스에&amp;nbsp;의존한다.&lt;/li&gt;
&lt;li&gt;초기화,&amp;nbsp;소멸&amp;nbsp;메서드의&amp;nbsp;이름을&amp;nbsp;변경할&amp;nbsp;수&amp;nbsp;없다.&lt;/li&gt;
&lt;li&gt;내가&amp;nbsp;코드를&amp;nbsp;고칠&amp;nbsp;수&amp;nbsp;없는&amp;nbsp;외부&amp;nbsp;라이브러리에&amp;nbsp;적용할&amp;nbsp;수&amp;nbsp;없다.&lt;/li&gt;
&lt;/ul&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금은 다음의 더 &lt;span style=&quot;letter-spacing: 0px;&quot;&gt;나은&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;h4 data-ke-size=&quot;size20&quot;&gt;2) 설정 정보에 초기화 메서드, 종료 메서드 지정&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정 정보에 @Bean(initMethod = &quot;init&quot;, destroyMethod = &quot;close&quot;) 처럼 초기화, 소멸 메서드를 지정할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1650077895911&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
static class LifeCycleConfig {

    @Bean(initMethod = &quot;init&quot;, destroyMethod = &quot;close&quot;)
    public NetworkClient networkClient() {
        NetworkClient networkClient = new NetworkClient();
        networkClient.setUrl(&quot;http://hello-spring.dev&quot;);
        return networkClient;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1650077858699&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class NetworkClient {
	//...
    public void init() {
        // 의존관계 주입후
        System.out.println(&quot;NetworkClient.init&quot;);
        connect();
        call(&quot;초기화 연결 메세지&quot;);

    }

    public void close() {
        // 빈이 종료될 때
        System.out.println(&quot;NetworkClient.close&quot;);
        disconnect();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;특징&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메서드&amp;nbsp;이름을&amp;nbsp;자유롭게&amp;nbsp;줄&amp;nbsp;수&amp;nbsp;있다.&lt;/li&gt;
&lt;li&gt;스프링&amp;nbsp;빈이&amp;nbsp;스프링&amp;nbsp;코드에&amp;nbsp;의존하지&amp;nbsp;않는다.&lt;/li&gt;
&lt;li&gt;코드가 아니라 설정 정보를 사용하기 때문에 코드를 고칠 수 없는 외부 라이브러리에도 초기화, 종료메서드를 적용할 수 있다.&lt;/li&gt;
&lt;/ul&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;@Bean의&amp;nbsp;destroyMethod&amp;nbsp;는&amp;nbsp;기본값이&amp;nbsp;(inferred)&amp;nbsp;(추론)으로&amp;nbsp;등록되어&amp;nbsp;있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이&amp;nbsp;추론&amp;nbsp;기능은&amp;nbsp;close&amp;nbsp;,&amp;nbsp;shutdown&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;호출해준다.&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;3) @PostConstruct, @PreDestroy 애노테이션 지원&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@PostConstruct , @PreDestroy 이 두 애노테이션을 사용하면 가장 편리하게 초기화와 종료를 실행할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1650078477255&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class NetworkClient {
	//...
    @PostConstruct
    public void init() {
        // 의존관계 주입후
        System.out.println(&quot;NetworkClient.init&quot;);
        connect();
        call(&quot;초기화 연결 메세지&quot;);

    }

    @PreDestroy
    public void close() {
        // 빈이 종료될 때
        System.out.println(&quot;NetworkClient.close&quot;);
        disconnect();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;특징&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;최신 스프링에서 가장 권장하는 방법이다.&lt;/li&gt;
&lt;li&gt;패키지를 보면 javax.annotation.PostConstruct 이다. 스프링에 종속적인 기술이 아니라 JSR-250 라는&amp;nbsp;자바&amp;nbsp;표준이다.&amp;nbsp;따라서&amp;nbsp;스프링이&amp;nbsp;아닌&amp;nbsp;다른&amp;nbsp;컨테이너에서도&amp;nbsp;동작한다.&lt;/li&gt;
&lt;li&gt;컴포넌트 스캔과 잘 어울린다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;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;적용하지&amp;nbsp;못한다는&amp;nbsp;것이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부 라이브러리를 초기화, 종료 해야 하면 @Bean의&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;b&gt;정리&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@PostConstruct,&amp;nbsp;@PreDestroy&amp;nbsp;애노테이션을&amp;nbsp;사용하자 &lt;br /&gt;코드를 고칠 수 없는 외부 라이브러리를 초기화, 종료해야 하면 @Bean 의 initMethod , destroyMethod 를&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;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 빈 스코프&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빈 스코프는 스프링 빈이 존재할 수 있는 범위를 뜻한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 빈은 기본적으로 싱글톤 스코프로 생성된다.&lt;/p&gt;
&lt;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;싱글톤&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;/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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;request: 웹 요청이 들어오고 나갈때 까지 유지되는 스코프이다.&lt;/li&gt;
&lt;li&gt;session: 웹 세션이 생성되고 종료될 때 까지 유지되는 스코프이다.&lt;/li&gt;
&lt;li&gt;application: 웹의 서블릿 컨텍스트와 같은 범위로 유지되는 스코프이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) 싱글톤&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-origin-width=&quot;764&quot; data-origin-height=&quot;407&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/blnjdt/btrzLwYJyWJ/MP3ApKwQCDt8BEP3s71KCK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/blnjdt/btrzLwYJyWJ/MP3ApKwQCDt8BEP3s71KCK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/blnjdt/btrzLwYJyWJ/MP3ApKwQCDt8BEP3s71KCK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fblnjdt%2FbtrzLwYJyWJ%2FMP3ApKwQCDt8BEP3s71KCK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;352&quot; data-origin-width=&quot;764&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;1.&amp;nbsp;싱글톤&amp;nbsp;스코프의&amp;nbsp;빈을&amp;nbsp;스프링&amp;nbsp;컨테이너에&amp;nbsp;요청한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2.&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;3.&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;p data-ke-size=&quot;size16&quot;&gt;[ 예제 ]&lt;/p&gt;
&lt;pre id=&quot;code_1650335155709&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class SingletonTest {

  @Test
  void singletonBeanFind() {
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SingletonBean.class);

    SingletonBean singletonBean1 = ac.getBean(SingletonBean.class);
    SingletonBean singletonBean2 = ac.getBean(SingletonBean.class);
    System.out.println(&quot;singletonBean1 = &quot; + singletonBean1);
    System.out.println(&quot;singletonBean2 = &quot; + singletonBean2);

    assertThat(singletonBean1).isSameAs(singletonBean2);

    ac.close();

  }

  static class SingletonBean {

    @PostConstruct
    public void init() {
      System.out.println(&quot;SingletonBean.init&quot;);
    }

    @PreDestroy
    public void destroy() {
      System.out.println(&quot;SingletonBean.destroy&quot;);
    }
  }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;855&quot; data-origin-height=&quot;111&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bkspqd/btrzI1yIsnf/TH8Ay5jimwKqfrUV3ndDA0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bkspqd/btrzI1yIsnf/TH8Ay5jimwKqfrUV3ndDA0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bkspqd/btrzI1yIsnf/TH8Ay5jimwKqfrUV3ndDA0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbkspqd%2FbtrzI1yIsnf%2FTH8Ay5jimwKqfrUV3ndDA0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;855&quot; height=&quot;111&quot; data-origin-width=&quot;855&quot; data-origin-height=&quot;111&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;빈 초기화 메서드를 실행&lt;/li&gt;
&lt;li&gt;같은 인스턴스의 빈을 조회&lt;/li&gt;
&lt;li&gt;종료 메서드까지 정상 호출&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고&lt;/p&gt;
&lt;pre id=&quot;code_1650335381608&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;new AnnotationConfigApplicationContext(SingletonBean.class);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ApplicationContext 를 생성할 때 파라미터로 빈으로 등록할 클래스를 전달하면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Component 필요 없이 컨포넌트 스캔 대상이 된다.&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;2) 프로토타입 스코프&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-origin-width=&quot;763&quot; data-origin-height=&quot;413&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bdQMAT/btrzOGzjFAR/URnNvvwo9kKrD6mAZA1CCK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bdQMAT/btrzOGzjFAR/URnNvvwo9kKrD6mAZA1CCK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bdQMAT/btrzOGzjFAR/URnNvvwo9kKrD6mAZA1CCK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbdQMAT%2FbtrzOGzjFAR%2FURnNvvwo9kKrD6mAZA1CCK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;367&quot; data-origin-width=&quot;763&quot; data-origin-height=&quot;413&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1.&amp;nbsp;프로토타입&amp;nbsp;스코프의&amp;nbsp;빈을&amp;nbsp;스프링&amp;nbsp;컨테이너에&amp;nbsp;요청한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2.&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;761&quot; data-origin-height=&quot;409&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/F4FJf/btrzKQXsXzr/QdtY3p5HB6mwOLrA3KotV1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/F4FJf/btrzKQXsXzr/QdtY3p5HB6mwOLrA3KotV1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/F4FJf/btrzKQXsXzr/QdtY3p5HB6mwOLrA3KotV1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FF4FJf%2FbtrzKQXsXzr%2FQdtY3p5HB6mwOLrA3KotV1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;679&quot; height=&quot;365&quot; data-origin-width=&quot;761&quot; data-origin-height=&quot;409&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;4. 이후에 스프링 컨테이너에 같은 요청이 오면 항상 새로운 프로토타입 빈을 생성해서 반환한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[ 예제 ]&lt;/p&gt;
&lt;pre id=&quot;code_1650335562675&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class PrototypeTest {

  @Test
  void prototypeBeanFind() {
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class);
    PrototypeBean prototypeBean1 = ac.getBean(PrototypeBean.class);
    PrototypeBean prototypeBean2 = ac.getBean(PrototypeBean.class);
    System.out.println(&quot;prototypeBean1 = &quot; + prototypeBean1);
    System.out.println(&quot;prototypeBean2 = &quot; + prototypeBean2);
    
    assertThat(prototypeBean1).isNotSameAs(prototypeBean2);

    ac.close();

  }

  @Scope(&quot;prototype&quot;)
  static class PrototypeBean {

    @PostConstruct
    void init() {
      System.out.println(&quot;PrototypeBean.init&quot;);
    }

    @PreDestroy
    void destroy() {
      System.out.println(&quot;SingletonBean.destroy&quot;);
    }

  }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;852&quot; data-origin-height=&quot;113&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DkjEk/btrzKKCma5P/wvkFg5UWFSkJBszFouEerK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DkjEk/btrzKKCma5P/wvkFg5UWFSkJBszFouEerK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DkjEk/btrzKKCma5P/wvkFg5UWFSkJBszFouEerK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDkjEk%2FbtrzKKCma5P%2FwvkFg5UWFSkJBszFouEerK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;852&quot; height=&quot;113&quot; data-origin-width=&quot;852&quot; data-origin-height=&quot;113&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;스프링 컨테이너에 요청할 때 마다 새로 생성된다.&lt;/li&gt;
&lt;li&gt;스프링 컨테이너는 프로토타입 빈의 생성과 의존관계 주입 그리고 초기화까지만 관여한다.&lt;/li&gt;
&lt;li&gt;종료 메서드가 호출되지 않는다.&lt;/li&gt;
&lt;li&gt;그래서 프로토타입 빈은 프로토타입 빈을 조회한 클라이언트가 관리해야 한다.&lt;/li&gt;
&lt;li&gt;종료 메서드에 대한 호출도 클라이언트가 직접 해야한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;br /&gt;3) 프로토타입 스코프 - 싱글톤 빈과 함께 사용시 문제점&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;싱글톤 빈(ClientBean)에서 프로토타입 스코프 빈(PrototypeBean)을 참조해 사용하는 예제이다.&lt;/p&gt;
&lt;pre id=&quot;code_1650337216085&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class SingletonWithPrototypeTest1 {

  @Test
  void singletonClientUsePrototype() {
    ApplicationContext ac = new AnnotationConfigApplicationContext(ClientBean.class, PrototypeBean.class);
    ClientBean clientBean1 = ac.getBean(ClientBean.class);
    int count1 = clientBean1.logic();
    assertThat(count1).isEqualTo(1);

    ClientBean clientBean2 = ac.getBean(ClientBean.class);
    int count2 = clientBean2.logic();
    assertThat(count2).isEqualTo(2);
  }

  static class ClientBean {
    private final PrototypeBean prototypeBean; // 생성시점에 주입

    ClientBean(PrototypeBean prototypeBean) {
      this.prototypeBean = prototypeBean;
    }

    public int logic() {
      prototypeBean.addCount();
      return prototypeBean.getCount();
    }
  }

  @Scope(&quot;prototype&quot;)
  static class PrototypeBean {
    private int count = 0;

    public void addCount() {
      count++;
    }

    public int getCount() {
      return count;
    }

    @PostConstruct
    public void init() {
      System.out.println(&quot;PrototypeBean.init &quot; + this);
    }

    @PreDestroy
    public void destroy() {
      System.out.println(&quot;PrototypeBean.destroy &quot; + this);
    }

  }
}&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;760&quot; data-origin-height=&quot;392&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cbrzJI/btrzMaHXGst/2D3Fcwlc8a5IB9mrQkwfX1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cbrzJI/btrzMaHXGst/2D3Fcwlc8a5IB9mrQkwfX1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cbrzJI/btrzMaHXGst/2D3Fcwlc8a5IB9mrQkwfX1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcbrzJI%2FbtrzMaHXGst%2F2D3Fcwlc8a5IB9mrQkwfX1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;331&quot; data-origin-width=&quot;760&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;clientBean 은 싱글톤이므로, 보통 스프링 컨테이너 생성 시점에 함께 생성되고, 의존관계 주입도 발생한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;1. clientBean 은 의존관계 자동 주입을 사용한다. 주입 시점에 스프링 컨테이너에 프로토타입 빈을 요청한다.&lt;/li&gt;
&lt;li&gt;2. 스프링 컨테이너는 프로토타입 빈을 생성해서 clientBean 에 반환한다. 프로토타입 빈의 count 필드 값은 0이다.이제&amp;nbsp;clientBean&amp;nbsp;은&amp;nbsp;프로토타입&amp;nbsp;빈을&amp;nbsp;내부&amp;nbsp;필드에&amp;nbsp;보관한다.&amp;nbsp;(정확히는&amp;nbsp;참조값을&amp;nbsp;보관한다.)&lt;/li&gt;
&lt;/ul&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;766&quot; data-origin-height=&quot;396&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bsEjjZ/btrzKSnuN2e/hPbnAN2uGyxHCaXSzkYe5k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bsEjjZ/btrzKSnuN2e/hPbnAN2uGyxHCaXSzkYe5k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bsEjjZ/btrzKSnuN2e/hPbnAN2uGyxHCaXSzkYe5k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbsEjjZ%2FbtrzKSnuN2e%2FhPbnAN2uGyxHCaXSzkYe5k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;334&quot; data-origin-width=&quot;766&quot; data-origin-height=&quot;396&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트 A는 clientBean 을 스프링 컨테이너에 요청해서 받는다.싱글톤이므로 항상 같은 clientBean&amp;nbsp;이&amp;nbsp;반환된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;3.&amp;nbsp;클라이언트&amp;nbsp;A는&amp;nbsp;clientBean.logic()&amp;nbsp;을&amp;nbsp;호출한다.&lt;/li&gt;
&lt;li&gt;4.&amp;nbsp;clientBean&amp;nbsp;은&amp;nbsp;prototypeBean의&amp;nbsp;addCount()&amp;nbsp;를&amp;nbsp;호출해서&amp;nbsp;프로토타입&amp;nbsp;빈의&amp;nbsp;count를&amp;nbsp;증가한다. &lt;br /&gt;count값이&amp;nbsp;1이&amp;nbsp;된다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;765&quot; data-origin-height=&quot;394&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cjMNdO/btrzMJ4o4jx/GALoZeVNQHsHgIMYNm7xK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cjMNdO/btrzMJ4o4jx/GALoZeVNQHsHgIMYNm7xK0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cjMNdO/btrzMJ4o4jx/GALoZeVNQHsHgIMYNm7xK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcjMNdO%2FbtrzMJ4o4jx%2FGALoZeVNQHsHgIMYNm7xK0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;627&quot; height=&quot;323&quot; data-origin-width=&quot;765&quot; data-origin-height=&quot;394&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트 B는 clientBean 을 스프링 컨테이너에 요청해서 받는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;싱글톤이므로 항상 같은 clientBean 이 반환된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;clientBean이 내부에 가지고 있는 프로토타입 빈은 이미 과거에 주입이 끝난 빈이다. &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;&lt;b&gt;사용 할 때마다 새로 생성되는 것이 아니다!&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;5. 클라이언트&amp;nbsp;B는&amp;nbsp;clientBean.logic()&amp;nbsp;을&amp;nbsp;호출한다.&lt;/li&gt;
&lt;li&gt;6.&amp;nbsp;clientBean&amp;nbsp;은&amp;nbsp;prototypeBean의&amp;nbsp;addCount()&amp;nbsp;를&amp;nbsp;호출해서&amp;nbsp;프로토타입&amp;nbsp;빈의&amp;nbsp;count를&amp;nbsp;증가한다. &lt;br /&gt;원래&amp;nbsp;count&amp;nbsp;값이&amp;nbsp;1이었으므로&amp;nbsp;2가&amp;nbsp;된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[ 문제점 ]&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;싱글톤 빈이 프로토타입 빈을 &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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;싱글톤 빈과 함께 계속 유지되는 문제가 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4) 프로토타입 스코프 - 싱글톤 빈과 함께 사용시 Provider로 문제 해결&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Dependency Lookup(DL)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의존관계를 외부에서 주입(DI) 받는게 아니라 직접 필요한 의존관계를 찾는 것을 의존관계 조회(탐색)(DL) 이라한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;싱글톤 빈이 프로토타입 빈을 사용할 때마다 프로토타입 빈을 새로 생성하려면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DL 방식을 사용해 스프링 컨테이너에서 프로토타입 빈을 꺼내오면 된다.&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) ObjectFactory, ObjectProvider&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1650861491256&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;static class ClientBean {
    @Autowired
    private ObjectProvider&amp;lt;PrototypeBean&amp;gt; prototypeBeanProvider;

    public int logic() {
        PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
        prototypeBean.addCount();
        int count = prototypeBean.getCount();
        return count;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링의 애클리케이션 컨텍스트를 주입받아 PrototypeBean을 받아올 수 있지만&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ObjectFactory, ObjectProvider를 사용하면 지정한 프로토타입 빈만을 컨테이너에서 찾아주는 DL 정도의 기능만 사용할 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ObjectFactory:&amp;nbsp;기능이&amp;nbsp;단순,&amp;nbsp;별도의&amp;nbsp;라이브러리&amp;nbsp;필요&amp;nbsp;없음,&amp;nbsp;스프링에&amp;nbsp;의존 &lt;/li&gt;
&lt;li&gt;ObjectProvider: ObjectFactory 상속, 옵션, 스트림 처리등 편의 기능이 많고, 별도의 라이브러리 필요 없음,&amp;nbsp;스프링에&amp;nbsp;의존&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;(2) JSR-330&amp;nbsp;Provider&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1650861904430&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;static class ClientBean {
    @Autowired
    private Provider&amp;lt;PrototypeBean&amp;gt; provider;
    public int logic() {
        PrototypeBean prototypeBean = provider.get();
        prototypeBean.addCount();
        int count = prototypeBean.getCount();
        return count;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링에 의존하지 않고 DL을 사용하려면 javax.inject.Provider 라는 JSR-330 자바 표준을 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이&amp;nbsp;방법을&amp;nbsp;사용하려면&amp;nbsp;javax.inject:javax.inject:1&amp;nbsp;라이브러리를&amp;nbsp;gradle에&amp;nbsp;추가해야&amp;nbsp;한다.&lt;/p&gt;
&lt;pre id=&quot;code_1650861972550&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;implementation 'javax.inject:javax.inject: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;&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;프로토타입&amp;nbsp;빈을&amp;nbsp;직접적으로&amp;nbsp;사용하는&amp;nbsp;일은&amp;nbsp;매우&amp;nbsp;드물다. &lt;br /&gt;ObjectProvider , JSR330 Provider 등은 프로토타입 뿐만 아니라 DL이 필요한 경우는 언제든지 사용할&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;ObjectProvider vs JSR-330 Provider&lt;/b&gt;&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 웹 스코프&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹&amp;nbsp;스코프는&amp;nbsp;웹&amp;nbsp;환경에서만&amp;nbsp;동작한다. &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 스코프는 프로토타입과 다르게 스프링이 해당 스코프의 종료시점까지 관리한다.&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;웹 스코프 종류&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;request:&amp;nbsp;HTTP&amp;nbsp;요청&amp;nbsp;하나가&amp;nbsp;들어오고&amp;nbsp;나갈&amp;nbsp;때&amp;nbsp;까지&amp;nbsp;유지되는&amp;nbsp;스코프,&amp;nbsp;&lt;b&gt;각각의&amp;nbsp;HTTP&amp;nbsp;요청마다&amp;nbsp;별도의&amp;nbsp;빈 &lt;/b&gt;&lt;br /&gt;&lt;b&gt;인스턴스가&amp;nbsp;생성되고,&amp;nbsp;관리된다.&lt;/b&gt; &lt;/li&gt;
&lt;li&gt;session:&amp;nbsp;HTTP&amp;nbsp;Session과&amp;nbsp;동일한&amp;nbsp;생명주기를&amp;nbsp;가지는&amp;nbsp;스코프 &lt;/li&gt;
&lt;li&gt;application:&amp;nbsp;서블릿&amp;nbsp;컨텍스트(&amp;nbsp;ServletContext&amp;nbsp;)와&amp;nbsp;동일한&amp;nbsp;생명주기를&amp;nbsp;가지는&amp;nbsp;스코프 &lt;/li&gt;
&lt;li&gt;websocket:&amp;nbsp;웹&amp;nbsp;소켓과&amp;nbsp;동일한&amp;nbsp;생명주기를&amp;nbsp;가지는&amp;nbsp;스코프&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;request 스코프&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;607&quot; data-origin-height=&quot;310&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/u9JDX/btrAqJ3biOh/wuGHw1JTFEffAPsKAvx0z1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/u9JDX/btrAqJ3biOh/wuGHw1JTFEffAPsKAvx0z1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/u9JDX/btrAqJ3biOh/wuGHw1JTFEffAPsKAvx0z1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fu9JDX%2FbtrAqJ3biOh%2FwuGHw1JTFEffAPsKAvx0z1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;607&quot; height=&quot;310&quot; data-origin-width=&quot;607&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;웹 스코프는 웹 환경에서만 동작하므로 web 환경이 동작하도록 라이브러리를 추가해야 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1650863266268&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;implementation 'org.springframework.boot:spring-boot-starter-web'&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1650864430942&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
@Scope(value = &quot;request&quot;)
public class MyLogger {
    
    private String uuid;
    private String requestURL;

    public void setRequestURL(String requestURL) {
        this.requestURL = requestURL;
    }

    public void log(String message) {
        System.out.println(&quot;[&quot; + uuid + &quot;]&quot; + &quot;[&quot; + requestURL + &quot;] &quot; +
                message);
    }

    @PostConstruct
    public void init() {
        uuid = UUID.randomUUID().toString();
        System.out.println(&quot;[&quot; + uuid + &quot;] request scope bean create:&quot; + this);
    }

    @PreDestroy
    public void close() {
        System.out.println(&quot;[&quot; + uuid + &quot;] request scope bean close:&quot; + this);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1650864542318&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Controller
@RequiredArgsConstructor
public class LogDemoController {
    private final LogDemoService logDemoService;
    private final ObjectProvider&amp;lt;MyLogger&amp;gt; myLoggerProvider;

    @RequestMapping(&quot;log-demo&quot;)
    @ResponseBody
    public String logDemo(HttpServletRequest request) {
        String requestURL = request.getRequestURL().toString();
        MyLogger myLogger = myLoggerProvider.getObject();
        myLogger.setRequestURL(requestURL);
        myLogger.log(&quot;controller test&quot;);
        logDemoService.logic(&quot;testId&quot;);
        return &quot;OK&quot;;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1650864552029&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
public class LogDemoService {    
    private final ObjectProvider&amp;lt;MyLogger&amp;gt; myLoggerProvider;
    
    public void logic(String id) {
        MyLogger myLogger = myLoggerProvider.getObject();
        myLogger.log(&quot;service id = &quot; + id);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MyLogger 클래스를 request 스코프로 지정했다.&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;size16&quot;&gt;LogDemoController와 LogDemoService는 싱글톤 빈이고 애플리케이션이 실행되는 시점에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;싱글톤 빈은 생성해서 주입이 가능하지만, request 스코프 빈은 고객의 요청이 와야 생성된다.&lt;/p&gt;
&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;ObjectProvider를 이용해서 ObjectProvider.getObject() 를 호출하는 시점까지 request scope 빈을 애플리케이션 컨텍스트에서 조회하는 처리를 지연할&amp;nbsp;수&amp;nbsp;있다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ObjectProvider.getObject() 를 호출하시는 시점에는 HTTP 요청이 진행중이므로 request scope 빈의&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;request 스코프 빈은 HTTP 요청마다 하나의 인스턴스를 생성되고 관리된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 &lt;b&gt;같은 HTTP요청(같은 UUID)이면 같은 스프링 빈이 반환된다.&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;b&gt;스코프와 프록시&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MyLogger의 가짜 프록시 클래스를 만들어두고 HTTP request와 상관 없이 가짜 프록시 클래스를&amp;nbsp;다른&amp;nbsp;빈에&amp;nbsp;미리&amp;nbsp;주입해&amp;nbsp;둘&amp;nbsp;수&amp;nbsp;있다.&lt;/p&gt;
&lt;pre id=&quot;code_1650865223520&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
@Scope(value = &quot;request&quot;, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger {
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1650865268560&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;myLogger = class hello.core.common.MyLogger$$EnhancerBySpringCGLIB$$b68b726d&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Scope 의 proxyMode = ScopedProxyMode.TARGET_CLASS) 를 설정하면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 컨테이너는 CGLIB 라는 바이트코 를 조작하는 라이브러리를 사용해서,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MyLogger를 상속받은 가짜 프록시 객체 생성한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;604&quot; data-origin-height=&quot;289&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cQme7m/btrAqr2Ryov/CK2yH5fyekoTKV7Jkcp161/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cQme7m/btrAqr2Ryov/CK2yH5fyekoTKV7Jkcp161/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cQme7m/btrAqr2Ryov/CK2yH5fyekoTKV7Jkcp161/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcQme7m%2FbtrAqr2Ryov%2FCK2yH5fyekoTKV7Jkcp161%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;289&quot; data-origin-width=&quot;604&quot; data-origin-height=&quot;289&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CGLIB라는&amp;nbsp;라이브러리로&amp;nbsp;내&amp;nbsp;클래스를&amp;nbsp;상속&amp;nbsp;받은&amp;nbsp;가짜&amp;nbsp;프록시&amp;nbsp;객체를&amp;nbsp;만들어서&amp;nbsp;주입한다. &lt;/li&gt;
&lt;li&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;들어있다. &lt;/li&gt;
&lt;li&gt;가짜 프록시 객체는 실제 request scope와는 관계가 없다. 그냥 가짜이고, 내부에 단순한 위임 로직만 &lt;br /&gt;있고,&amp;nbsp;싱글톤&amp;nbsp;처럼&amp;nbsp;동작한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;정리&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Provider를 사용하든, 프록시를 사용하든 핵심 아이디어는 진짜 객체 조회를 꼭 필요한 시점까지 지연처리&amp;nbsp;한다는&amp;nbsp;점이다. &lt;/li&gt;
&lt;li&gt;단지&amp;nbsp;애노테이션&amp;nbsp;설정&amp;nbsp;변경만으로&amp;nbsp;원본&amp;nbsp;객체를&amp;nbsp;프록시&amp;nbsp;객체로&amp;nbsp;대체할&amp;nbsp;수&amp;nbsp;있다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;이것이 바로 다형성과 DI 컨테이너가&amp;nbsp;가진&amp;nbsp;큰&amp;nbsp;강점이다. &lt;/li&gt;
&lt;li&gt;꼭&amp;nbsp;웹&amp;nbsp;스코프가&amp;nbsp;아니어도&amp;nbsp;프록시는&amp;nbsp;사용할&amp;nbsp;수&amp;nbsp;있다.&lt;/li&gt;
&lt;/ul&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;다르게&amp;nbsp;동작하기&amp;nbsp;때문에&amp;nbsp;결국&amp;nbsp;주의해서&amp;nbsp;사용해야&amp;nbsp;한다. &lt;br /&gt;이런 특별한 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;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&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;size14&quot;&gt;&lt;a href=&quot;https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8&quot;&gt;스프링 핵심 원리&lt;/a&gt;&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Spring</category>
      <category>spring bean</category>
      <category>빈 생명주기 콜백</category>
      <category>빈 스코프</category>
      <author>mandykr</author>
      <guid isPermaLink="true">https://mandykr.tistory.com/68</guid>
      <comments>https://mandykr.tistory.com/68#entry68comment</comments>
      <pubDate>Wed, 13 Apr 2022 16:53:12 +0900</pubDate>
    </item>
    <item>
      <title>[JPA] 8. JPQL(1)</title>
      <link>https://mandykr.tistory.com/67</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;목차&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. JPQL&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 기본 문법과 쿼리 API&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;4. 페이징&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;6. 서브 쿼리&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;7. JPQL 타입 표현&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;8. 조건식 - CASE 식&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;9. JPQL 함수&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;673&quot; data-origin-height=&quot;666&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b4pL4V/btrxEElftjX/yWkG5nSVPn385fsNG8vdy0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b4pL4V/btrxEElftjX/yWkG5nSVPn385fsNG8vdy0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b4pL4V/btrxEElftjX/yWkG5nSVPn385fsNG8vdy0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb4pL4V%2FbtrxEElftjX%2FyWkG5nSVPn385fsNG8vdy0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;666&quot; data-origin-width=&quot;673&quot; data-origin-height=&quot;666&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. JPQL&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA를 사용하면 엔티티 객체를 중심으로 개발을 진행하는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EntityManager.find()나 객체 그래프 탐색(a.get())으로 조회가 어려운&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;검색 쿼리를 작성할 때 JPQL을 사용한다.&lt;/p&gt;
&lt;pre id=&quot;code_1648524068643&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;String jpql = &quot;select m From Member m where m.name like &amp;lsquo;%hello%'&quot;;
List&amp;lt;Member&amp;gt; result = em.createQuery(jpql, Member.class)
        .getResultList();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JPA는&amp;nbsp;SQL을&amp;nbsp;추상화한&amp;nbsp;JPQL이라는&amp;nbsp;객체&amp;nbsp;지향&amp;nbsp;쿼리&amp;nbsp;언어&amp;nbsp;제공&lt;/li&gt;
&lt;li&gt;SQL과 문법 유사, SELECT, FROM, WHERE, GROUP BY, HAVING,&amp;nbsp;JOIN&amp;nbsp;지원&lt;/li&gt;
&lt;li&gt;JPQL은 엔티티 객체를 대상으로 쿼리&lt;/li&gt;
&lt;li&gt;JPQL은 결국 SQL로 변환됨&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SQL을 추상화해서 제공하기 때문에 &lt;b&gt;특정 데이터베이스 SQL에 의존하지 않는다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPQL은 SQL을 문자열 타입으로 작성하기 때문에 컴파일 과정에서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문법에 대한 체크가 이루어 지지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 QueryDSL 라이브러리를 함께 사용하도록 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 기본 문법과 쿼리 API&lt;/h3&gt;
&lt;pre id=&quot;code_1648524473976&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;select m from Member as m where m.age &amp;gt; 18&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPQL은 엔티티를 대상으로 쿼리하기 때문에 엔티티와 속성에 대한 대소문자를 구별한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(Member, age)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPQL 키워드는 대소문자를 구분하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(select, from, where)&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;TypeQuery,&amp;nbsp;Query&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TypeQuery:&amp;nbsp;반환&amp;nbsp;타입이&amp;nbsp;명확할&amp;nbsp;때&amp;nbsp;사용&lt;/p&gt;
&lt;pre id=&quot;code_1648524543892&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;TypedQuery&amp;lt;Member&amp;gt; query =
        em.createQuery(&quot;SELECT m FROM Member m&quot;, Member.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;Query:&amp;nbsp;반환&amp;nbsp;타입이&amp;nbsp;명확하지&amp;nbsp;않을&amp;nbsp;때&amp;nbsp;사용&lt;/p&gt;
&lt;pre id=&quot;code_1648524570070&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Query query =
        em.createQuery(&quot;SELECT m.username, m.age from Member m&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;size18&quot;&gt;&lt;b&gt;결과 조회 API&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;query.getResultList():&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;pre id=&quot;code_1648524697208&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;List&amp;lt;Member&amp;gt; result = em.createQuery(&quot;select m from Member m&quot;, Member.class)
        .getResultList();&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;query.getSingleResult():&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;javax.persistence.NoResultException&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘&amp;nbsp;이상이면:&amp;nbsp;javax.persistence.NonUniqueResultException&lt;/p&gt;
&lt;pre id=&quot;code_1648524734579&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Member result = em.createQuery(&quot;select m from Member m&quot;, Member.class)
        .getSingleResult();&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;파라미터 바인딩&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이름 기준&lt;/p&gt;
&lt;pre id=&quot;code_1648524792552&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT m FROM Member m where m.username=:username
query.setParameter(&quot;username&quot;, usernameParam);&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_1648524798751&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT m FROM Member m where m.username=?1
query.setParameter(1, usernameParam);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유지보수를 위해 위치 기준보다는 &lt;b&gt;이름 기준을 사용하자.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 프로젝션&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;createQuery()의 파라미터로 select 절에 조회할 대상을 지정한다.&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;엔티티 프로젝션&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1648524925619&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;List&amp;lt;Member&amp;gt; result = em.createQuery(&quot;select m from Member m&quot;, Member.class)
        .getResultList();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Member 객체로 반환값을 매핑한다.&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;묵시적 조인&lt;/p&gt;
&lt;pre id=&quot;code_1648525012009&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;List&amp;lt;Team&amp;gt; result = em.createQuery(&quot;select m.team from Member m&quot;, Team.class)
        .getResultList();&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_1648525260795&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;List&amp;lt;Team&amp;gt; result = em.createQuery(&quot;select t from Member m join m.team t&quot;, Team.class)
        .getResultList();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 결과를 예측 가능하도록 &lt;b&gt;명시적 조인을 사용&lt;/b&gt;해서 조인이 발생함을 명확히 명시하자.&lt;/p&gt;
&lt;p 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-origin-width=&quot;154&quot; data-origin-height=&quot;317&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bMMPLy/btrxNZOESiQ/0rEt0JTKxO3nC9K6oxwlek/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bMMPLy/btrxNZOESiQ/0rEt0JTKxO3nC9K6oxwlek/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bMMPLy/btrxNZOESiQ/0rEt0JTKxO3nC9K6oxwlek/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbMMPLy%2FbtrxNZOESiQ%2F0rEt0JTKxO3nC9K6oxwlek%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;154&quot; height=&quot;317&quot; data-origin-width=&quot;154&quot; data-origin-height=&quot;317&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1648525421494&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;List&amp;lt;Address&amp;gt; result = em.createQuery(&quot;select o.address from Orders o&quot;, Address.class)
        .getResultList();&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;266&quot; data-origin-height=&quot;135&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dzlouF/btrxHx6V7gR/rtXiULVT2Wtp6YW42Pwhm1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dzlouF/btrxHx6V7gR/rtXiULVT2Wtp6YW42Pwhm1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dzlouF/btrxHx6V7gR/rtXiULVT2Wtp6YW42Pwhm1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdzlouF%2FbtrxHx6V7gR%2FrtXiULVT2Wtp6YW42Pwhm1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;266&quot; height=&quot;135&quot; data-origin-width=&quot;266&quot; data-origin-height=&quot;135&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;size18&quot;&gt;&lt;b&gt;프로젝션 - 여러 값 조회&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Query 타입으로 조회&lt;/p&gt;
&lt;pre id=&quot;code_1648525889012&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;List results = em.createQuery(&quot;select m.name, m.age from Member m&quot;)
        .getResultList();&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;Object[] 타입으로 조회&lt;/p&gt;
&lt;pre id=&quot;code_1648525925632&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;List&amp;lt;Object[]&amp;gt; results = em.createQuery(&quot;select m.name, m.age from Member m&quot;)
        .getResultList();&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;new 명령어로 조회&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1648525790821&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;List&amp;lt;MemberDto&amp;gt; results = 
        em.createQuery(&quot;select jpabook.jpql.MemberDTO(m.username, m.age) from Member m&quot;, MemberDto.class)
        .getResultList();&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 페이징&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA는&amp;nbsp;페이징을&amp;nbsp;다음&amp;nbsp;두&amp;nbsp;API로&amp;nbsp;추상화&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;setFirstResult(int startPosition) : 조회 시작 위치 (0부터&amp;nbsp;시작)&lt;/li&gt;
&lt;li&gt;setMaxResults(int maxResult) : 조회할 데이터 수&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1648527562361&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;String jpql = &quot;select m from Member m order by m.name desc&quot;;
List&amp;lt;Member&amp;gt; resultList = em.createQuery(jpql, Member.class)
        .setFirstResult(10)
        .setMaxResults(20)
        .getResultList();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;페이징&amp;nbsp;API&amp;nbsp;-&amp;nbsp;MySQL&amp;nbsp;방언&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1648527619240&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT
	M.ID AS ID,
	M.AGE AS AGE,
	M.TEAM_ID AS TEAM_ID,
	M.NAME AS NAME
FROM
	MEMBER M
ORDER BY
	M.NAME DESC LIMIT ?, ?&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;페이징&amp;nbsp;API&amp;nbsp;-&amp;nbsp;Oracle&amp;nbsp;방언&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1648527709377&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT * FROM
	( SELECT ROW_.*, ROWNUM ROWNUM_
	FROM
		( SELECT
			M.ID AS ID,
			M.AGE AS AGE,
            M.TEAM_ID AS TEAM_ID,
            M.NAME AS NAME
		FROM MEMBER M
		ORDER BY M.NAME
        ) ROW_
    WHERE ROWNUM &amp;lt;= ?
	)
WHERE ROWNUM_ &amp;gt; ?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 조인&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) 조인 기본 문법&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;엔티티간의 연관관계를 통해 조인 SQL을 만든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;( Member m join m.team t )&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_1649816249192&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT m FROM Member m [INNER] JOIN m.team t&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;396&quot; data-origin-height=&quot;416&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ddY36e/btrzfvZuKaW/PJHoU96I68Ytd8ZIMhqnM0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ddY36e/btrzfvZuKaW/PJHoU96I68Ytd8ZIMhqnM0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ddY36e/btrzfvZuKaW/PJHoU96I68Ytd8ZIMhqnM0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FddY36e%2FbtrzfvZuKaW%2FPJHoU96I68Ytd8ZIMhqnM0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;396&quot; height=&quot;416&quot; data-origin-width=&quot;396&quot; data-origin-height=&quot;416&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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_1649816298074&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT m FROM Member m LEFT [OUTER] JOIN m.team t&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;394&quot; data-origin-height=&quot;414&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/4hH4f/btrzdNmL9iX/sPUxP3IiTnLrY9YIQXGTgK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/4hH4f/btrzdNmL9iX/sPUxP3IiTnLrY9YIQXGTgK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/4hH4f/btrzdNmL9iX/sPUxP3IiTnLrY9YIQXGTgK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F4hH4f%2FbtrzdNmL9iX%2FsPUxP3IiTnLrY9YIQXGTgK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;414&quot; data-origin-width=&quot;394&quot; data-origin-height=&quot;414&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;&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_1649816466477&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;select count(m) from Member m, Team t where m.username = t.name&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;393&quot; data-origin-height=&quot;441&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/txvf4/btrzfv6gCr3/D2s9bYXIH19nJnebKq6xXk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/txvf4/btrzfv6gCr3/D2s9bYXIH19nJnebKq6xXk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/txvf4/btrzfv6gCr3/D2s9bYXIH19nJnebKq6xXk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Ftxvf4%2Fbtrzfv6gCr3%2FD2s9bYXIH19nJnebKq6xXk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;441&quot; data-origin-width=&quot;393&quot; data-origin-height=&quot;441&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;h4 data-ke-size=&quot;size20&quot;&gt;2) ON 절을 활용한 조인&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;ON 절을 추가해 조인 대상을 필터링 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예)&amp;nbsp;회원과&amp;nbsp;팀을&amp;nbsp;조인하면서,&amp;nbsp;팀&amp;nbsp;이름이&amp;nbsp;A인&amp;nbsp;팀만&amp;nbsp;조인&lt;/p&gt;
&lt;pre id=&quot;code_1649816721158&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT m, t FROM Member m LEFT JOIN m.team t on t.name = 'A'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SQL의 ON 절의 조인 조건으로 team.name = 'A' 가 추가된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;393&quot; data-origin-height=&quot;483&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/B1aT7/btrzdMBpcBL/5CXN0WELmFsUHBfT954IGK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/B1aT7/btrzdMBpcBL/5CXN0WELmFsUHBfT954IGK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/B1aT7/btrzdMBpcBL/5CXN0WELmFsUHBfT954IGK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FB1aT7%2FbtrzdMBpcBL%2F5CXN0WELmFsUHBfT954IGK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;483&quot; data-origin-width=&quot;393&quot; data-origin-height=&quot;483&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;size18&quot;&gt;&lt;b&gt;연관관계&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;회원의&amp;nbsp;이름과&amp;nbsp;팀의&amp;nbsp;이름이&amp;nbsp;같은&amp;nbsp;대상&amp;nbsp;외부&amp;nbsp;조인&lt;/p&gt;
&lt;pre id=&quot;code_1649816909012&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT m, t FROM Member m LEFT JOIN Team t on m.username = t.name&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SQL ON 절에 id 조건이 빠지고 member.username = team.name 조건이 추가된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;395&quot; data-origin-height=&quot;465&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhUVi6/btrzg4ARlH2/2Zz1ccW1iBitdBLblef431/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhUVi6/btrzg4ARlH2/2Zz1ccW1iBitdBLblef431/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhUVi6/btrzg4ARlH2/2Zz1ccW1iBitdBLblef431/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbhUVi6%2Fbtrzg4ARlH2%2F2Zz1ccW1iBitdBLblef431%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;395&quot; height=&quot;465&quot; data-origin-width=&quot;395&quot; data-origin-height=&quot;465&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. 서브 쿼리&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) 서브 쿼리 기본 문법&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예) 나이가 평균보다 많은 회원&lt;/p&gt;
&lt;pre id=&quot;code_1649817680686&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;select m from Member m
where m.age &amp;gt; (select avg(m2.age) from Member m2)&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_1649817670054&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;select m from Member m
where (select count(o) from Order o where m = o.member) &amp;gt; 0&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2) 서브 쿼리 지원 함수&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[NOT]&amp;nbsp;&lt;b&gt;EXISTS&lt;/b&gt;&amp;nbsp;(subquery):&amp;nbsp;서브쿼리에&amp;nbsp;결과가&amp;nbsp;존재하면&amp;nbsp;참&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;{ALL | ANY | SOME} (subquery)&lt;/li&gt;
&lt;li&gt;ALL 모두 만족하면 참&lt;/li&gt;
&lt;li&gt;ANY, SOME: 같은 의미, 조건을 하나라도 만족하면 참&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[NOT] &lt;b&gt;IN&lt;/b&gt; (subquery): 서브쿼리의 결과 중 하나라도 같은 것이 있으면 참&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예) 팀A&amp;nbsp;소속인&amp;nbsp;회원&lt;/p&gt;
&lt;pre id=&quot;code_1649817775848&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;select m from Member m
where exists (select t from m.team t where t.name = &amp;lsquo;팀A')&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예) 전체&amp;nbsp;상품&amp;nbsp;각각의&amp;nbsp;재고보다&amp;nbsp;주문량이&amp;nbsp;많은&amp;nbsp;주문들&lt;/p&gt;
&lt;pre id=&quot;code_1649817787323&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;select o from Order o
where o.orderAmount &amp;gt; ALL (select p.stockAmount from Product p)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예) 어떤&amp;nbsp;팀이든&amp;nbsp;팀에&amp;nbsp;소속된&amp;nbsp;회원&lt;/p&gt;
&lt;pre id=&quot;code_1649817793825&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;select m from Member m
where m.team = ANY (select t from Team t)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3) JPA 서브 쿼리 한계&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JPA는&amp;nbsp;WHERE,&amp;nbsp;HAVING&amp;nbsp;절에서만&amp;nbsp;서브&amp;nbsp;쿼리&amp;nbsp;사용&amp;nbsp;가능&lt;/li&gt;
&lt;li&gt;SELECT 절도 가능(하이버네이트에서 지원)&lt;/li&gt;
&lt;li&gt;FROM 절의 서브 쿼리는 현재 JPQL에서 불가능 &lt;br /&gt;(조인 &amp;gt; 쿼리 분할 &amp;gt; 네이티브 쿼리 순서로 풀 수 있으면 풀어서 해결)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. JPQL 타입 표현&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;문자:&amp;nbsp;&amp;lsquo;HELLO&amp;rsquo;,&amp;nbsp;&amp;lsquo;She&amp;rsquo;&amp;rsquo;s&amp;rsquo;&lt;/li&gt;
&lt;li&gt;숫자: 10L(Long), 10D(Double), 10F(Float)&lt;/li&gt;
&lt;li&gt;Boolean: TRUE, FALSE&lt;/li&gt;
&lt;li&gt;ENUM: jpabook.MemberType.Admin (패키지명 포함)&lt;/li&gt;
&lt;li&gt;엔티티 타입: TYPE(m) = Member (상속 관계에서 사용) &lt;a href=&quot;https://mandykr.tistory.com/18?category=896296&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;상속관계 매핑 조인전략&lt;/a&gt; 참고&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;JPQL 기타 문법&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;EXISTS,&amp;nbsp;IN&lt;/li&gt;
&lt;li&gt;AND, OR, NOT&lt;/li&gt;
&lt;li&gt;=, &amp;gt;, &amp;gt;=, &amp;lt;, &amp;lt;=, &amp;lt;&amp;gt;&lt;/li&gt;
&lt;li&gt;BETWEEN, LIKE, IS NULL&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8. 조건식&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) CASE 식&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;기본 CASE 식&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1649819257585&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;select
 case when m.age &amp;lt;= 10 then '학생요금'
 when m.age &amp;gt;= 60 then '경로요금'
 else '일반요금'
 end
from Member 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;size18&quot;&gt;&lt;b&gt;단순 CASE 식&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1649819273313&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;select
 case t.name
 when '팀A' then '인센티브110%'
 when '팀B' then '인센티브120%'
 else '인센티브105%'
 end
from Team t&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2) COALESCE, NULLIF&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;COALESCE&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나씩&amp;nbsp;조회해서&amp;nbsp;null이&amp;nbsp;아니면&amp;nbsp;반환&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예) 사용자 이름이 없으면 이름 없는 회원을 반환&lt;/p&gt;
&lt;pre id=&quot;code_1649819493364&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;select coalesce(m.username,'이름 없는 회원') from Member 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;size18&quot;&gt;&lt;b&gt;NULLIF&lt;/b&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;예) 사용자 이름이 &amp;lsquo;관리자&amp;rsquo;면 null을 반환하고 나머지는 본인의 이름을 반환&lt;/p&gt;
&lt;pre id=&quot;code_1649819500181&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;select NULLIF(m.username, '관리자') from Member m&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9. JPQL 함수&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) JPQL 기본 함수&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CONCAT&lt;/li&gt;
&lt;li&gt;SUBSTRING&lt;/li&gt;
&lt;li&gt;TRIM&lt;/li&gt;
&lt;li&gt;LOWER, UPPER&lt;/li&gt;
&lt;li&gt;LENGTH&lt;/li&gt;
&lt;li&gt;LOCATE&lt;/li&gt;
&lt;li&gt;ABS, SQRT, MOD&lt;/li&gt;
&lt;li&gt;SIZE, INDEX(JPA 용도)&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1649820017662&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;select size(t.members) from Team t&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2) 사용자 정의 함수&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;H2&amp;nbsp;데이터베이스&amp;nbsp;경우&amp;nbsp;H2Dialect&amp;nbsp;클래스에&amp;nbsp;JPQL&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;[MyH2Dialect]&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1649820706910&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class MyH2Dialect extends H2Dialect {

public MyH2Dialect() {
    registerFunction(&quot;group_concat&quot;, 
               new StandardSQLFunction(&quot;group_concat&quot;, StandardBasicTypes.STRING));
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[persistence.xml]&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_1649820737725&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;!--&amp;lt;property name=&quot;hibernate.dialect&quot; value=&quot;org.hibernate.dialect.H2Dialect&quot;/&amp;gt;--&amp;gt;
&amp;lt;property name=&quot;hibernate.dialect&quot; value=&quot;jpql.MyH2Dialect&quot;/&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1649820757693&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;select function('group_concat', i.name) from Item i&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;a href=&quot;https://www.inflearn.com/course/ORM-JPA-Basic&quot;&gt;https://www.inflearn.com/course/ORM-JPA-Basic&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;자바 ORM 표준 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;</description>
      <category>ORM/JPA</category>
      <category>join</category>
      <category>JPQL</category>
      <category>페이징</category>
      <category>프로젝션</category>
      <author>mandykr</author>
      <guid isPermaLink="true">https://mandykr.tistory.com/67</guid>
      <comments>https://mandykr.tistory.com/67#entry67comment</comments>
      <pubDate>Tue, 29 Mar 2022 13:22:20 +0900</pubDate>
    </item>
    <item>
      <title>[클린코드] 10. 클래스</title>
      <link>https://mandykr.tistory.com/66</link>
      <description>&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;2. 단일 책임 원칙&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;4. 클래스는 변경하기 쉬워야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 클래스 체계&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클래스를 정의하는 표준 자바 관례에 따르면 다음과 같은 순서이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;static public 상수 &amp;gt; private 변수&lt;/li&gt;
&lt;li&gt;공개 함수 &amp;gt; private 함수&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비공개 함수는 자신을 호출하는 공개 함수 직수에 넣는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추상화 단계가 순차적으로 내려가 가독성이 높아진다.&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-origin-width=&quot;582&quot; data-origin-height=&quot;345&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/G3ro3/btrxyksAah1/rWt1Byluc6G1DvQEkcAxNk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/G3ro3/btrxyksAah1/rWt1Byluc6G1DvQEkcAxNk/img.png&quot; data-alt=&quot;https://www.programminghunk.com/2020/01/encapsulation.html&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/G3ro3/btrxyksAah1/rWt1Byluc6G1DvQEkcAxNk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FG3ro3%2FbtrxyksAah1%2FrWt1Byluc6G1DvQEkcAxNk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;525&quot; height=&quot;311&quot; data-origin-width=&quot;582&quot; data-origin-height=&quot;345&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://www.programminghunk.com/2020/01/encapsulation.html&lt;/figcaption&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;필드를 private으로 제한, 외부의 잘못된 사용을 방지한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 단일 책임 원칙 (SRP)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클래스는 함수와 마찬가지로 작아야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작은 클래스는 각자 맡은 책임이 하나며, 변경할 이유가 하나며,&lt;/p&gt;
&lt;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;853&quot; data-origin-height=&quot;207&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Doxc6/btrxHwL0vd0/iN5dFJV2H5lgXeEn6Gglak/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Doxc6/btrxHwL0vd0/iN5dFJV2H5lgXeEn6Gglak/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Doxc6/btrxHwL0vd0/iN5dFJV2H5lgXeEn6Gglak/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDoxc6%2FbtrxHwL0vd0%2FiN5dFJV2H5lgXeEn6Gglak%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;853&quot; height=&quot;207&quot; data-origin-width=&quot;853&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;/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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 낮은 결합도와 높은 응집도&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;348&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Q3VuE/btrxAbWrRVg/E7xoL8KML21vRWUD62bPg0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Q3VuE/btrxAbWrRVg/E7xoL8KML21vRWUD62bPg0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Q3VuE/btrxAbWrRVg/E7xoL8KML21vRWUD62bPg0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQ3VuE%2FbtrxAbWrRVg%2FE7xoL8KML21vRWUD62bPg0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;524&quot; height=&quot;348&quot; data-origin-width=&quot;524&quot; data-origin-height=&quot;348&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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-origin-width=&quot;440&quot; data-origin-height=&quot;248&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/emXVx5/btrxlDe4NXr/gz9ifUW2lGkpShkARnYAvk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/emXVx5/btrxlDe4NXr/gz9ifUW2lGkpShkARnYAvk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/emXVx5/btrxlDe4NXr/gz9ifUW2lGkpShkARnYAvk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FemXVx5%2FbtrxlDe4NXr%2Fgz9ifUW2lGkpShkARnYAvk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;248&quot; data-origin-width=&quot;440&quot; data-origin-height=&quot;248&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;시스템의 결합도를 낮추면 유연성과 재사용성도 더욱 높아진다.&lt;/li&gt;
&lt;li&gt;DIP - 클래스가 상세한 구현이 아니라 추상화에 의존해야 한다.&lt;/li&gt;
&lt;li&gt;추상화를 이용하면 테스트 코드 짜기에 용이하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;결합도가 높은 클래스의 문제점&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;연관된 클래스가 변경되면 수정이 필요하다.&lt;/li&gt;
&lt;li&gt;결합도가 높으면 연관된 클래스들을 모두 이해해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/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-origin-width=&quot;464&quot; data-origin-height=&quot;389&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Duz1X/btrxyiuN7t3/QLzL05Ik7Uei0hZPD87YUK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Duz1X/btrxyiuN7t3/QLzL05Ik7Uei0hZPD87YUK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Duz1X/btrxyiuN7t3/QLzL05Ik7Uei0hZPD87YUK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDuz1X%2FbtrxyiuN7t3%2FQLzL05Ik7Uei0hZPD87YUK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;464&quot; height=&quot;389&quot; data-origin-width=&quot;464&quot; data-origin-height=&quot;389&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클래스는 인스턴수 변수 수가 적어야 한다.&lt;/li&gt;
&lt;li&gt;메서드는 인스턴스 변수를 하나 이상 사용해야 한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;메서드가 인스턴스 변수를 많이 사용할 수록 응집도가 높아진다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;응집도가 높다 = 클래스에 속한 메서드와 변수가 서로 의존하며 논리적인 단위로 묶인다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;클래스가 응집도를 잃어간다면 함수를 쪼개야 한다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;응집도가 낮은 클래스의 문제점&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;여러 기능이 있으므로 이해하기 어렵다.&lt;/li&gt;
&lt;li&gt;재사용하기 어렵다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 클래스는 변경하기 쉬워야 한다.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로운 기능을 추가할 때 기존 코드에도 수정이 발생하면 OCP에 위반된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1667&quot; data-origin-height=&quot;704&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cgvJqS/btrxt1njvEy/zoC8IKnKgvvhFLS5uZ2ARK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cgvJqS/btrxt1njvEy/zoC8IKnKgvvhFLS5uZ2ARK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cgvJqS/btrxt1njvEy/zoC8IKnKgvvhFLS5uZ2ARK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcgvJqS%2Fbtrxt1njvEy%2FzoC8IKnKgvvhFLS5uZ2ARK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1667&quot; height=&quot;704&quot; data-origin-width=&quot;1667&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;새로운 SQL을 추가할 때도 수정이 발생하고, 기존 SQL 문을 수정할 때도 수정이 발생하므로 OCP에 위반된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 공개 인터페이스를 전부 SQL 클래스에서 파생하는 클래스로 만들고,&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;Clean Code(클린 코드) 애자일 소프트웨어 장인 정신 - 로버트 C. 마틴 지음&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;a href=&quot;https://zero-base.co.kr/category_dev_camp/cleancode_1book&quot;&gt;https://zero-base.co.kr/category_dev_camp/cleancode_1book&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;&amp;nbsp;&lt;/p&gt;</description>
      <category>책 내용 정리/클린코드(clean code)</category>
      <category>clean code</category>
      <category>OCP</category>
      <category>SRP</category>
      <author>mandykr</author>
      <guid isPermaLink="true">https://mandykr.tistory.com/66</guid>
      <comments>https://mandykr.tistory.com/66#entry66comment</comments>
      <pubDate>Mon, 28 Mar 2022 13:25:23 +0900</pubDate>
    </item>
    <item>
      <title>[클린코드] 8. 경계</title>
      <link>https://mandykr.tistory.com/65</link>
      <description>&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;2. 외부 코드 사용하기&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 경계란&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트에서 오픈소스, 라이브러리 같은 외부 코드를 자주 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리 코드는 외부 코드의 인터페이스를 제공받아 사용하는데&lt;/p&gt;
&lt;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;h3 data-ke-size=&quot;size23&quot;&gt;2. 외부 코드 사용하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;캡슐화&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부 코드를 사용하는 부분을 캡슐화해&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터를 보호하고 외부 인터페이스가 변할 경우 우리 코드에 미치는 영향을 최소화 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;java.util.Map 인터페이스를 예로 든다.&lt;/p&gt;
&lt;pre id=&quot;code_1648436430594&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Map&amp;lt;Sensor&amp;gt; sensors = new HashMap&amp;lt;Sensor&amp;gt;();
...
Sensor s = sensors.get(sensorId );&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Map 인터페이스가 제공하는 clear 등 불필요한 기능이 노출된다.&lt;/li&gt;
&lt;li&gt;외부 코드가 함부로 호출하면 sensor 데이터가 손상될 수 있다.&lt;/li&gt;
&lt;li&gt;Map 인터페이스가 변경되면 사용하는 우리 코드의 수정할 코드가 많아진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1648436460947&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Sensors {
    private Map sensors = new HashMap();
    
    public Sensor getById(String id) {
        return (Sensor) sensors.get(id);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;캡슐화를 통해 Map을 감춘다.&lt;/li&gt;
&lt;li&gt;원하는 기능(getById())만 공개할 수 있다.&lt;/li&gt;
&lt;li&gt;Map 인터페이스가 변하더라도 나머지 프로그램에는 영향을 미치지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Map과 같은 경계 인터페이스를 이용할 때는&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;Adapter 패턴&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Adapter 패턴은 외부 코드를 호출할 때,&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;657&quot; data-origin-height=&quot;248&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2feqB/btrxwA3Hnap/pgGliZpJsCJB7nXfgkdKG1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2feqB/btrxwA3Hnap/pgGliZpJsCJB7nXfgkdKG1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2feqB/btrxwA3Hnap/pgGliZpJsCJB7nXfgkdKG1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2feqB%2FbtrxwA3Hnap%2FpgGliZpJsCJB7nXfgkdKG1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;657&quot; height=&quot;248&quot; data-origin-width=&quot;657&quot; data-origin-height=&quot;248&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TransmitterAdapter에 API 사용을 캡슐화해 API가 바뀔 때 수정할 코드를 한 곳으로 모은다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CommunicationController 코드는 Transmitter 인터페이스를 정의해 외부 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;또한, 이런 설계는 테스트를 만들때는 적절한 Fake 객체를 만들어 주입해 사용할 수 있기 때문에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부 API와 분리해 우리의 코드가 제대로 동작하는지 테스트할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Adapter 패턴 in Elastic Search&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;332&quot; data-origin-height=&quot;303&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/usug9/btrxwYwqJdW/lA405COH4l3ZL1E28MqlrK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/usug9/btrxwYwqJdW/lA405COH4l3ZL1E28MqlrK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/usug9/btrxwYwqJdW/lA405COH4l3ZL1E28MqlrK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fusug9%2FbtrxwYwqJdW%2FlA405COH4l3ZL1E28MqlrK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;303&quot; data-origin-width=&quot;332&quot; data-origin-height=&quot;303&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;652&quot; data-origin-height=&quot;422&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2x0EX/btrxyiVJdvB/bSRG74VtECnpMPJgsIrga0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2x0EX/btrxyiVJdvB/bSRG74VtECnpMPJgsIrga0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2x0EX/btrxyiVJdvB/bSRG74VtECnpMPJgsIrga0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2x0EX%2FbtrxyiVJdvB%2FbSRG74VtECnpMPJgsIrga0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;422&quot; data-origin-width=&quot;652&quot; data-origin-height=&quot;422&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 우리 코드(내 패키지)가 아닌 외부 코드를 import 하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 외부 코드인 NettyAdaptor는 nettyChannel을 감싸고 있다.&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;494&quot; data-origin-height=&quot;278&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9QwUP/btrxkJ0Mky9/PI6t6grkcr229fIUVPsfSK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9QwUP/btrxkJ0Mky9/PI6t6grkcr229fIUVPsfSK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9QwUP/btrxkJ0Mky9/PI6t6grkcr229fIUVPsfSK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9QwUP%2FbtrxkJ0Mky9%2FPI6t6grkcr229fIUVPsfSK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;494&quot; height=&quot;278&quot; data-origin-width=&quot;494&quot; data-origin-height=&quot;278&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Adapter에 우리가 원하는 타입의 파라미터를 전달해 경계 인터페이스를 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ByteBuffer, Page타입의 파라미터를 ByteBuf로 변환해 외부 코드인 nettyChannel에 전달한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;635&quot; data-origin-height=&quot;293&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dxIjqJ/btrxu87mxg0/TsYx3AAOhOqN4kcg6i0Vnk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dxIjqJ/btrxu87mxg0/TsYx3AAOhOqN4kcg6i0Vnk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dxIjqJ/btrxu87mxg0/TsYx3AAOhOqN4kcg6i0Vnk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdxIjqJ%2Fbtrxu87mxg0%2FTsYx3AAOhOqN4kcg6i0Vnk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;635&quot; height=&quot;293&quot; data-origin-width=&quot;635&quot; data-origin-height=&quot;293&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 Adapter를 통해 변환을 거치지 않았다면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;nettyChannel에 데이터를 전달할 때마다 타입을 변환하는 과정이 필요해진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 학습 테스트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;학습 테스트를 작성해 외부 코드에 대한 이해도를 높이고 안정성도 미리 검증할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한, 외부 코드의 버전이 변경됐을 때, 우리 코드와 호환되는지 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예) 경로&amp;nbsp;탐색을&amp;nbsp;위한&amp;nbsp;라이브러리&amp;nbsp;JGraph의&amp;nbsp;학습&amp;nbsp;테스트&lt;/p&gt;
&lt;pre id=&quot;code_1648438266416&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
public void getDijkstraShortestPath() {
    WeightedMultigraph&amp;lt;String, DefaultWeightedEdge&amp;gt; graph
            = new WeightedMultigraph(DefaultWeightedEdge.class);
    graph.addVertex(&quot;v1&quot;);
    graph.addVertex(&quot;v2&quot;);
    graph.addVertex(&quot;v3&quot;);
    graph.setEdgeWeight(graph.addEdge(&quot;v1&quot;, &quot;v2&quot;), 2);
    graph.setEdgeWeight(graph.addEdge(&quot;v2&quot;, &quot;v3&quot;), 2);
    graph.setEdgeWeight(graph.addEdge(&quot;v1&quot;, &quot;v3&quot;), 100);

    DijkstraShortestPath dijkstraShortestPath
            = new DijkstraShortestPath(graph);
    List&amp;lt;String&amp;gt; shortestPath 
            = dijkstraShortestPath.getPath(&quot;v3&quot;, &quot;v1&quot;).getVertexList();

    assertThat(shortestPath.size()).isEqualTo(3);
}​&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;Clean Code(클린 코드) 애자일 소프트웨어 장인 정신 - 로버트 C. 마틴 지음&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;a href=&quot;https://zero-base.co.kr/category_dev_camp/cleancode_1book&quot;&gt;https://zero-base.co.kr/category_dev_camp/cleancode_1book&lt;/a&gt;&lt;/p&gt;</description>
      <category>책 내용 정리/클린코드(clean code)</category>
      <category>clean code</category>
      <category>경계</category>
      <category>학습 테스트</category>
      <author>mandykr</author>
      <guid isPermaLink="true">https://mandykr.tistory.com/65</guid>
      <comments>https://mandykr.tistory.com/65#entry65comment</comments>
      <pubDate>Mon, 28 Mar 2022 12:32:21 +0900</pubDate>
    </item>
    <item>
      <title>[클린코드] 7. 오류 처리</title>
      <link>https://mandykr.tistory.com/64</link>
      <description>&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;2.&lt;span&gt;&amp;nbsp;&lt;/span&gt;Business&amp;nbsp;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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 예외 처리 방식&lt;/h3&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-origin-width=&quot;514&quot; data-origin-height=&quot;399&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bzKVuL/btrxfv9ghkR/Exk2NjUlt5qsNQVcMMRD40/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bzKVuL/btrxfv9ghkR/Exk2NjUlt5qsNQVcMMRD40/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bzKVuL/btrxfv9ghkR/Exk2NjUlt5qsNQVcMMRD40/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbzKVuL%2Fbtrxfv9ghkR%2FExk2NjUlt5qsNQVcMMRD40%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;514&quot; height=&quot;399&quot; data-origin-width=&quot;514&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;628&quot; data-origin-height=&quot;432&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bsUQlU/btrxieFlz3A/vkZzW4i3EGGypCuRlfpoV1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bsUQlU/btrxieFlz3A/vkZzW4i3EGGypCuRlfpoV1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bsUQlU/btrxieFlz3A/vkZzW4i3EGGypCuRlfpoV1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbsUQlU%2FbtrxieFlz3A%2FvkZzW4i3EGGypCuRlfpoV1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;628&quot; height=&quot;432&quot; data-origin-width=&quot;628&quot; data-origin-height=&quot;432&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오류가 발생한 부분에서 Checked Exception을 발생시키고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상위 레벨에서는 예외를 catch 하여 처리하거나 메소드 선언부에 throws를 명시한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Checked Exception은 예외에 대한 처리가 반드시 필요하기 떄문에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하위 레벨 메소드에서 Checked Exception을 발생시키면 catch 블록이 나오기 까지 모든 상위 레벨 메소드에서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;throws 처리를 해주어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 상위 레벨 메소드에서 하위 레벨 메소드의 디테일에 대해 알아야 하기 때문에 &lt;b&gt;OCP 원칙에 위배&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;b&gt;Unchecked Exception을 사용하라.&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;785&quot; data-origin-height=&quot;488&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qdDYf/btrxh95gi7G/9xTOK1m1ChdkLXDwG4HScK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qdDYf/btrxh95gi7G/9xTOK1m1ChdkLXDwG4HScK/img.png&quot; data-alt=&quot;출처&amp;amp;amp;nbsp;https://www.nextree.co.kr/p3239/&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qdDYf/btrxh95gi7G/9xTOK1m1ChdkLXDwG4HScK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqdDYf%2Fbtrxh95gi7G%2F9xTOK1m1ChdkLXDwG4HScK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;488&quot; data-origin-width=&quot;785&quot; data-origin-height=&quot;488&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처&amp;amp;nbsp;https://www.nextree.co.kr/p3239/&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;Exception을 상속하면 Checked Exception 명시적인 예외 처리가 필요하다. &lt;br /&gt;(예)IOException,SQLException&lt;/li&gt;
&lt;li&gt;RuntimeException을 상속하면 Unchekced Exception 명시적인 예외 처리가 필요하지 않다. &lt;br /&gt;(예)NullPointerException, IllegalArgumentException, IndexOutOfBoundException&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;C#, C++, 파이썬, 루비 등의 언어는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Checked Exception을 지원하지 않지만 안정적인 소프트웨어를 구현하기에는 무리가 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;명시적인 예외 처리가 필요하지 않은 Unchekced Exception을 사용하자.&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_1648189780963&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private DeviceHandle getHandle(DeviceID id) {
    ...
    throw new DeviceShutDownError(&quot;Invalid handle for: &quot; + id.toString());
    ...
}&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;size18&quot;&gt;&lt;b&gt;Exception Wrapper를 사용하라.&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1648190028986&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ACMEPort port = new ACMEPort(12);
try {
    port.open();
} catch (DeviceResponseException e) {
    reportPortError(e);
    logger.log(&quot;Device response exception&quot;, e);
} catch (ATM1212UnlockedException e) {
    reportPortError(e);
    logger.log(&quot;Unlock exception&quot;, e);
} catch (GMXError e) {
    reportPortError(e);
    logger.log(&quot;Device response exception&quot;);
} finally {
    &amp;hellip;
}&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;호출하는 라이브러리 API를 감싸면서 porte.open()이 던지는 Checked Exception을 하나의 예외 유형으로 감싸서 반환하면 코드를 간결하게 고칠 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1648190392997&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;LocalPort port = new LocalPort(12);
try {
    port.open();
} catch (PortDeviceFailure e) {
    reportError(e);
    logger.log(e.getMessage(), e);
} finally {
    &amp;hellip;
}

public class LocalPort {
    private ACMEPort innerPort;
    
    public LocalPort(int portNumber) {
        innerPort = new ACMEPort(portNumber);
    }
    
    public void open() {
        try {
            innerPort.open();
        } catch (DeviceResponseException e) {
            throw new PortDeviceFailure(e);
        } catch (ATM1212UnlockedException e) {
            throw new PortDeviceFailure(e);
        } catch (GMXError e) {
            throw new PortDeviceFailure(e);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부 API를 감싸면 외부 라이브러리와 프로그램 사이의 의존성도 크게 줄어든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나중에 다른 라이브러리로 변경할 떄의 비용도 적다.&lt;/p&gt;
&lt;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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;null을 반환하지 마라.&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;505&quot; data-origin-height=&quot;166&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bA2PZH/btrxfwHhId9/ZENxHl4QWWvMlAwU5m6EoK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bA2PZH/btrxfwHhId9/ZENxHl4QWWvMlAwU5m6EoK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bA2PZH/btrxfwHhId9/ZENxHl4QWWvMlAwU5m6EoK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbA2PZH%2FbtrxfwHhId9%2FZENxHl4QWWvMlAwU5m6EoK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;166&quot; data-origin-width=&quot;505&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;반환 데이터가 없는 경우 null을 리턴하면 이 후 모든 코드에서 null 체크가 있어야 하기 때문에&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;&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;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;441&quot; data-origin-height=&quot;228&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cLw7mJ/btrxfvBySOV/CaZIucKsUYmlCdNk8xpv51/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cLw7mJ/btrxfvBySOV/CaZIucKsUYmlCdNk8xpv51/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cLw7mJ/btrxfvBySOV/CaZIucKsUYmlCdNk8xpv51/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcLw7mJ%2FbtrxfvBySOV%2FCaZIucKsUYmlCdNk8xpv51%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;441&quot; height=&quot;228&quot; data-origin-width=&quot;441&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;복수형의 데이터를 가져올 때는 데이터의 없음을 의미하는 컬렉션을 리턴하면 된다. &lt;br /&gt;null 보다 size가 0인 컬렉션이 훨씬 안전하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;(2) 도메인에 맞는 기본값을 리턴한다. (getOrElse)&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;583&quot; data-origin-height=&quot;218&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ccSnA8/btrxids0dc5/K7yfl0V4c2khiK9xaDpZEk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ccSnA8/btrxids0dc5/K7yfl0V4c2khiK9xaDpZEk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ccSnA8/btrxids0dc5/K7yfl0V4c2khiK9xaDpZEk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FccSnA8%2Fbtrxids0dc5%2FK7yfl0V4c2khiK9xaDpZEk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;583&quot; height=&quot;218&quot; data-origin-width=&quot;583&quot; data-origin-height=&quot;218&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1648191574857&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;UserLevel userLevel = userService.getUserLevelOrDefault(userId);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시스템이 중단되지 않고 정상적인 흐름을 가져갈 수 있도록 도메인에 맞는 기본값인 basic level을 리턴한다.&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;(3) 예외를 던진다.&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;659&quot; data-origin-height=&quot;185&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/buFVBZ/btrxgEEN8jZ/nr7WneftTdbncirgfYm6y1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/buFVBZ/btrxgEEN8jZ/nr7WneftTdbncirgfYm6y1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/buFVBZ/btrxgEEN8jZ/nr7WneftTdbncirgfYm6y1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbuFVBZ%2FbtrxgEEN8jZ%2Fnr7WneftTdbncirgfYm6y1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;185&quot; data-origin-width=&quot;659&quot; data-origin-height=&quot;185&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본값을 반환할 수 없는 경우에는 Unchecked Exception을 던질수 있다.&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;null을 전달하지 마라.&lt;/b&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;애초에 null을 넘기지 못하도록 금지하는 정책이 합리적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. Business&amp;nbsp;Exception&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바에서 제공하는 Exception이 아닌 커스텀 Exception을 만들어 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;RuntimeException을 상속하는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;Business Exception을 만들고 enum으로 ErrorCode를 정의한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;610&quot; data-origin-height=&quot;435&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OAwu1/btrxb1gONez/1pCHJNmk7Gd6ANCBOfZrT1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OAwu1/btrxb1gONez/1pCHJNmk7Gd6ANCBOfZrT1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OAwu1/btrxb1gONez/1pCHJNmk7Gd6ANCBOfZrT1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOAwu1%2Fbtrxb1gONez%2F1pCHJNmk7Gd6ANCBOfZrT1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;610&quot; height=&quot;435&quot; data-origin-width=&quot;610&quot; data-origin-height=&quot;435&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커스텀 Exception을 사용하면 IllegalArgumentException 처럼 외부 라이브러리에서 발생할 수 있는 예외와 섞이지 않고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에러 로그에서 우리 시스템에서 발생한 예외를 파악하기에 용이하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자세한 내용 참고 : &lt;a href=&quot;https://cheese10yun.github.io/spring-guide-exception/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Exception 전략&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;Clean Code(클린 코드) 애자일 소프트웨어 장인 정신 - 로버트 C. 마틴 지음&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;a href=&quot;https://zero-base.co.kr/category_dev_camp/cleancode_1book&quot;&gt;https://zero-base.co.kr/category_dev_camp/cleancode_1book&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>책 내용 정리/클린코드(clean code)</category>
      <category>exception</category>
      <category>예외처리</category>
      <author>mandykr</author>
      <guid isPermaLink="true">https://mandykr.tistory.com/64</guid>
      <comments>https://mandykr.tistory.com/64#entry64comment</comments>
      <pubDate>Fri, 25 Mar 2022 16:28:10 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] 2. 컴포넌트 스캔과 의존관계 자동 주입</title>
      <link>https://mandykr.tistory.com/63</link>
      <description>&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;2. 컴포넌트 스캔&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 컴포넌트 스캔과 의존관계 주입&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://mandykr.tistory.com/61&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[Spring] 1. 스프링 컨테이너&lt;/a&gt; 예제에서는 AppConfig와 같이 설정 정보 클래스에서 빈과 의존관계를 설정해 주었다.&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;pre id=&quot;code_1647829687902&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
@ComponentScan
public class AutoAppConfig {

}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1647829760682&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
public class OrderServiceImpl implements OrderService {
    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

    @Autowired
    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy
            discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1647830028932&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 로그
ClassPathBeanDefinitionScanner - Identified candidate component class:
.. RateDiscountPolicy.class
.. MemberServiceImpl.class
.. MemoryMemberRepository.class
.. OrderServiceImpl.class&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Bean으로 등록할 필요 없이 설정 정보 클래스에 &lt;b&gt;@ComponentScan&lt;/b&gt;을 붙여주면&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;@Component&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;@Bean으로 설정 정보를 직접 만들어 사용할 때와 달리 의존관계 주입을 할 수 없기 때문에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;@Autowired&lt;/b&gt;를 붙여서 의존관계를 &lt;span&gt;자동으로&lt;span&gt;&amp;nbsp;&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;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-origin-width=&quot;837&quot; data-origin-height=&quot;382&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OnNEF/btrwreT3SwO/B46Nbi1OdmhRLhyMU8dh6K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OnNEF/btrwreT3SwO/B46Nbi1OdmhRLhyMU8dh6K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OnNEF/btrwreT3SwO/B46Nbi1OdmhRLhyMU8dh6K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOnNEF%2FbtrwreT3SwO%2FB46Nbi1OdmhRLhyMU8dh6K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;837&quot; height=&quot;382&quot; data-origin-width=&quot;837&quot; data-origin-height=&quot;382&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@ComponentScan은 @Component가 붙은 모든 클래스를 스프링 빈으로 등록한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 스프링 빈의 기본 이름은 클래스명을 사용하되 맨 앞글자만 소문자를 사용한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이름 기본 전략: MemberServiceImpl --&amp;gt; memberServiceImpl&lt;/li&gt;
&lt;li&gt;빈 이름 직접 지정:&amp;nbsp;@Component(&quot;memberService2&quot;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;787&quot; data-origin-height=&quot;349&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cPcOks/btrwsIaGKHG/U4vJN0tgsrLZr1bgXbHPf0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cPcOks/btrwsIaGKHG/U4vJN0tgsrLZr1bgXbHPf0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cPcOks/btrwsIaGKHG/U4vJN0tgsrLZr1bgXbHPf0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcPcOks%2FbtrwsIaGKHG%2FU4vJN0tgsrLZr1bgXbHPf0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;349&quot; data-origin-width=&quot;787&quot; data-origin-height=&quot;349&quot;/&gt;&lt;/span&gt;&lt;/figure&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;이때 &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;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 컴포넌트 스캔&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) 탐색 위치&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_1647830678963&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@ComponentScan(
        basePackages = &quot;hello.core&quot;,
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기본 설정은 @ComponentScan이 붙은 설정 정보 클래스의 패키지가 시작 위치&lt;/li&gt;
&lt;li&gt;basePackage: 탐색할 패키지의 시작 위치를 지정&lt;/li&gt;
&lt;li&gt;basePackageClasses: 지정한 클래스의 패키지를 탐색 시작 위치로 지정&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;되도록 설정 정보 클래스의 위치를 최상단에 두고 별도의 패키지 위치를 지정하지 않도록 권장.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;@SpringBootApplication&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1647831004184&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
		@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot에서는 @SpringBootApplication에 @ComponentScan이 들어있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 &lt;b&gt;@SpringBootApplication이 붙은 클래스의 패키지가 컴포넌트 스캔의 시작 위치&lt;/b&gt;가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2) 컴포넌트 스캔 기본 대상&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴포넌트&amp;nbsp;스캔은&amp;nbsp;@Component&amp;nbsp;뿐만&amp;nbsp;아니라&amp;nbsp;다음과&amp;nbsp;내용도&amp;nbsp;추가로&amp;nbsp;대상에&amp;nbsp;포함한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;@Component&amp;nbsp;:&amp;nbsp;컴포넌트&amp;nbsp;스캔에서&amp;nbsp;사용&lt;/li&gt;
&lt;li&gt;@Controlller&amp;nbsp;:&amp;nbsp;스프링&amp;nbsp;MVC&amp;nbsp;컨트롤러에서&amp;nbsp;사용&lt;/li&gt;
&lt;li&gt;@Service&amp;nbsp;:&amp;nbsp;스프링&amp;nbsp;비즈니스&amp;nbsp;로직에서&amp;nbsp;사용&lt;/li&gt;
&lt;li&gt;@Repository&amp;nbsp;:&amp;nbsp;스프링&amp;nbsp;데이터&amp;nbsp;접근&amp;nbsp;계층에서&amp;nbsp;사용&lt;/li&gt;
&lt;li&gt;@Configuration&amp;nbsp;:&amp;nbsp;스프링&amp;nbsp;설정&amp;nbsp;정보에서&amp;nbsp;사용&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1647831161175&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
public @interface Controller {
}&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;@Controller, @Configuration과 같은 어노테이션은 &lt;b&gt;@Component를 포함&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;&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;아니라&amp;nbsp;다음&amp;nbsp;애노테이션이&amp;nbsp;있으면&amp;nbsp;스프링은&amp;nbsp;부가&amp;nbsp;기능을&amp;nbsp;수행한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;@Controller&amp;nbsp;:&amp;nbsp;스프링&amp;nbsp;MVC&amp;nbsp;컨트롤러로&amp;nbsp;인식&lt;/li&gt;
&lt;li&gt;@Repository&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;/li&gt;
&lt;li&gt;@Configuration&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;/li&gt;
&lt;li&gt;@Service&amp;nbsp;:&amp;nbsp;사실&amp;nbsp;@Service&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;된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트의 DB를 바꾸면 예외의 내용이 바뀌는데 &lt;span&gt;@Repository가 &lt;/span&gt;Spring 예외로 변환해 추상화 해주기 때문에 별도의 문제가 발생하지 않는다.&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;3) 필터&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴포넌트 스캔 대상을 추가하거나 제외하도록 설정할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1647831475077&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
@ComponentScan(
        includeFilters = @Filter(type = FilterType.ANNOTATION, classes =
                MyIncludeComponent.class),
        excludeFilters = @Filter(type = FilterType.ANNOTATION, classes =
                MyExcludeComponent.class)
)&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;includeFilters&amp;nbsp;:&amp;nbsp;컴포넌트&amp;nbsp;스캔&amp;nbsp;대상을&amp;nbsp;추가로&amp;nbsp;지정한다.&lt;/li&gt;
&lt;li&gt;excludeFilters&amp;nbsp;:&amp;nbsp;컴포넌트&amp;nbsp;스캔에서&amp;nbsp;제외할&amp;nbsp;대상을&amp;nbsp;지정한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;FilterType 옵션&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ANNOTATION:&amp;nbsp;기본값,&amp;nbsp;애노테이션을&amp;nbsp;인식해서&amp;nbsp;동작한다. &lt;br /&gt;ex)&amp;nbsp;org.example.SomeAnnotation&lt;/li&gt;
&lt;li&gt;ASSIGNABLE_TYPE:&amp;nbsp;지정한&amp;nbsp;타입과&amp;nbsp;자식&amp;nbsp;타입을&amp;nbsp;인식해서&amp;nbsp;동작한다. &lt;br /&gt;ex)&amp;nbsp;org.example.SomeClass&lt;/li&gt;
&lt;li&gt;ASPECTJ:&amp;nbsp;AspectJ&amp;nbsp;패턴&amp;nbsp;사용 &lt;br /&gt;ex)&amp;nbsp;org.example..*Service+&lt;/li&gt;
&lt;li&gt;REGEX:&amp;nbsp;정규&amp;nbsp;표현식 &lt;br /&gt;ex)&amp;nbsp;org\.example\.Default.*&lt;/li&gt;
&lt;li&gt;CUSTOM:&amp;nbsp;TypeFilter&amp;nbsp;이라는&amp;nbsp;인터페이스를&amp;nbsp;구현해서&amp;nbsp;처리 &lt;br /&gt;ex)&amp;nbsp;org.example.MyTypeFilter&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 부트는 컴포넌트 스캔을 기본으로 제공하는데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;옵션을 변경하면서 사용하기 보다는 스프링의 기본 설정에 최대한 맞추어 사용하는 것을 권장한다.&lt;/p&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;3) 중복 등록과 충돌&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Component을 이용한 자동 빈 등록과 @Bean을 이용한 수동 빈 등록 과정에서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;같은 빈 이름을 등록&lt;/b&gt;하도록 요청하면 &lt;span&gt;컴포넌트 스캔에서&lt;span&gt; 다음과 같은 처리가 이루어 진다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;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;ConflictingBeanDefinitionException 예외 발생&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;수동 vs 자동&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1647832211927&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
public class MemoryMemberRepository implements MemberRepository {}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1647832231393&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
@ComponentScan(
        excludeFilters = @Filter(type = FilterType.ANNOTATION, classes =
                Configuration.class)
)
public class AutoAppConfig {
    @Bean(name = &quot;memoryMemberRepository&quot;)
    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1647832281550&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 로그
Overriding bean definition for bean 'memoryMemberRepository' with a different
definition: replacing&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이&amp;nbsp;경우&amp;nbsp;&lt;b&gt;수동&amp;nbsp;빈&amp;nbsp;등록이&amp;nbsp;우선권&lt;/b&gt;을&amp;nbsp;가진다. &lt;br /&gt;(수동&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;최근 &lt;b&gt;스프링 부트에서는 수동 빈 등록과 자동 빈 등록이 충돌하면 오류가 발생&lt;/b&gt;하도록 기본 값을 바꾸었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 부트인 CoreApplication을 실행하면 다음과 같은 에러가 발생한다.&lt;/p&gt;
&lt;pre id=&quot;code_1647832429686&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Consider renaming one of the beans or enabling overriding by setting
spring.main.allow-bean-definition-overriding=true&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 의존관계 자동 주입&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) 다양한 의존관계 주입 방법&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;&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;size18&quot;&gt;&lt;b&gt;생성자 주입&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1648091797867&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
public class OrderServiceImpl implements OrderService {
    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;
    
    @Autowired
    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }
}&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;&lt;b&gt;불변, 필수&lt;/b&gt; 의존관계에서 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성자가 1개만 있으면 @Autowired를 생략할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;생성자 주입을 선택하자.&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;생성자 주입 이외의 경우에는 외부에서 의존관계를 변경할 수 있다.&lt;/p&gt;
&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;된 경우 생성자 주입 방식에서는 런타임 시점에서 NPE가 발생하지만&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;final 키워드&lt;/b&gt;를 함께 사용하면 더 안전하게 의존관계 주입을 설정할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1648093447586&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private final MemberRepository memberRepository;&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;수정자 주입&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1648091922272&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
public class OrderServiceImpl implements OrderService {
    private MemberRepository memberRepository;
    private DiscountPolicy discountPolicy;
    
    @Autowired
    public void setMemberRepository(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
    
    @Autowired
    public void setDiscountPolicy(DiscountPolicy discountPolicy) {
        this.discountPolicy = discountPolicy;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필드의 값을 변경하는 setter 메서드를 통해서 의존관계를 주입하는 방법이다.&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;필드 주입&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1648092185619&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
public class OrderServiceImpl implements OrderService {
    @Autowired
    private MemberRepository memberRepository;
    
    @Autowired
    private DiscountPolicy discountPolicy;
}&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;DI 프레임워크가 없으면 외부에서 협력 객체 변경이 불가능해서 테스트하기 힘들다.&lt;/b&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;상황에 따라 필요한 객체 혹은 가짜 객체(Mock, Stub, Fake 등)를 만들어서 주입하는 방식으로 &lt;span&gt;테스트 코드를 만들지 못한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션의 실제 코드와 관계없는 테스트 코드, 스프링 설정을 목적으로 하는 @Configuration 같은 곳에서만 특별한 용도로 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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_1648092326580&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
public class OrderServiceImpl implements OrderService {
    private MemberRepository memberRepository;
    private DiscountPolicy discountPolicy;
    
    @Autowired
    public void init(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }
}&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;h4 data-ke-size=&quot;size20&quot;&gt;2) 옵션 처리&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주입할 스프링 빈이 없어도 동작해야 할 때가 있는데 이런 경우&amp;nbsp;&lt;b&gt;자동 주입 대상을 옵션으로 처리&lt;/b&gt;할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1648092682044&quot; class=&quot;reasonml&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//호출 안됨
@Autowired(required = false)
public void setNoBean1(Member member) {
    System.out.println(&quot;setNoBean1 = &quot; + member);
}

//null 호출
@Autowired
public void setNoBean2(@Nullable Member member) {
    System.out.println(&quot;setNoBean2 = &quot; + member);
}

//Optional.empty 호출
@Autowired(required = false)
public void setNoBean3(Optional&amp;lt;Member&amp;gt; member) {
    System.out.println(&quot;setNoBean3 = &quot; + member);
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;@Autowired(required=false)&amp;nbsp;:&amp;nbsp;자동&amp;nbsp;주입할&amp;nbsp;대상이&amp;nbsp;없으면&amp;nbsp;수정자&amp;nbsp;메서드&amp;nbsp;자체가&amp;nbsp;호출&amp;nbsp;안됨&lt;/li&gt;
&lt;li&gt;org.springframework.lang.@Nullable&amp;nbsp;:&amp;nbsp;자동&amp;nbsp;주입할&amp;nbsp;대상이&amp;nbsp;없으면&amp;nbsp;null이&amp;nbsp;입력된다.&lt;/li&gt;
&lt;li&gt;Optional&amp;lt;&amp;gt;&amp;nbsp;:&amp;nbsp;자동&amp;nbsp;주입할&amp;nbsp;대상이&amp;nbsp;없으면&amp;nbsp;Optional.empty&amp;nbsp;가&amp;nbsp;입력된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Nullable, Optional은 스프링 전반에 걸쳐서 지원된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성자 주입, 수정자 주입에서 특정 필드에만 적용해도 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3) 롬복&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;롬복 라이브러리 설치, IntelliJ Lombok 플러그인 설치, &lt;span style=&quot;color: #555555;&quot;&gt;Enable annotation processing 설정.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #555555;&quot;&gt;생성자 주입 방식에서 코드를 간결하게 수정할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1648094505747&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 OrderServiceImpl implements OrderService {
    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@RequiredArgsConstructor 는 final 필드에 대해 생성자를 만들어 준다.&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;4) 조회 대상 빈이 2개인 경우&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조회 대상 빈이 2개인 경우 NoUniqueBeanDefinitionException이 발생한다.&lt;/p&gt;
&lt;pre id=&quot;code_1648095576834&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
public class FixDiscountPolicy implements DiscountPolicy {}

@Component
public class RateDiscountPolicy implements DiscountPolicy {}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1648095602670&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
public class OrderServiceImpl implements OrderService {
    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;
    
    @Autowired
    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1648095659623&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;NoUniqueBeanDefinitionException: No qualifying bean of type
'hello.core.discount.DiscountPolicy' available: expected single matching bean
but found 2: fixDiscountPolicy,rateDiscountPolicy&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;@Autowired 필드 명&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;/p&gt;
&lt;pre id=&quot;code_1648095868233&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy rateDiscountPolicy) {
    this.memberRepository = memberRepository;
    this.discountPolicy = rateDiscountPolicy;
}&lt;/code&gt;&lt;/pre&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;타입 매칭&lt;/li&gt;
&lt;li&gt;타입 매칭의 결과가 2개 이상일 때 필드 명, 파라미터 명으로 빈 이름 매칭&lt;/li&gt;
&lt;/ol&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;@Qualifier&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빈 등록과 주입시에 @Qualifier를 붙여주고 이름으로 매칭되도록 설정한다.&lt;/p&gt;
&lt;pre id=&quot;code_1648095954882&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
@Qualifier(&quot;mainDiscountPolicy&quot;)
public class RateDiscountPolicy implements DiscountPolicy {}

@Component
@Qualifier(&quot;fixDiscountPolicy&quot;)
public class FixDiscountPolicy implements DiscountPolicy {}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1648095996137&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Autowired
public OrderServiceImpl(MemberRepository memberRepository, 
                        @Qualifier(&quot;mainDiscountPolicy&quot;) DiscountPolicy discountPolicy) {
    this.memberRepository = memberRepository;
    this.discountPolicy = discountPolicy;
}&lt;/code&gt;&lt;/pre&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;@Qualifier끼리 매칭&lt;/li&gt;
&lt;li&gt;빈 이름 매칭 (mainDiscountPolicy)&lt;/li&gt;
&lt;li&gt;NoSuchBeanDefinitionException 예외 발생&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Qualifier(&quot;mainDiscountPolicy&quot;) 에서 mainDiscountPolicy는 문자이기 때문에 컴파일 과정에서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의존관계 주입이 정상적으로 동작할 지 확인할 수 있도록 어노테이션을 별도로 만들어서 사용할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1648096697889&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER,
ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Qualifier(&quot;mainDiscountPolicy&quot;)
public @interface MainDiscountPolicy {
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1648096728670&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Autowired
public OrderServiceImpl(MemberRepository memberRepository,
                        @MainDiscountPolicy DiscountPolicy discountPolicy) {
    this.memberRepository = memberRepository;
    this.discountPolicy = discountPolicy;
}&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;@Primary&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Autowired 시에 여러 빈이 매칭되면 @Primary 가 우선권을 가진다.&lt;/p&gt;
&lt;pre id=&quot;code_1648096249177&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy {}

@Component
public class FixDiscountPolicy implements DiscountPolicy {}&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;@Primary,&amp;nbsp;@Qualifier&amp;nbsp;활용&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 순위 : @Primary &amp;lt; @Qualifier&lt;/p&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;있고,&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;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;메인 데이터베이스의&amp;nbsp;커넥션을&amp;nbsp;획득하는&amp;nbsp;스프링&amp;nbsp;빈은&amp;nbsp;@Primary&amp;nbsp;를&amp;nbsp;적용&lt;/b&gt;해서&amp;nbsp;조회하는&amp;nbsp;곳에서&amp;nbsp;@Qualifier &lt;br /&gt;지정&amp;nbsp;없이&amp;nbsp;편리하게&amp;nbsp;조회하고,&amp;nbsp;&lt;b&gt;서브&amp;nbsp;데이터베이스&amp;nbsp;커넥션&amp;nbsp;빈을&amp;nbsp;획득할&amp;nbsp;때는&amp;nbsp;@Qualifier&amp;nbsp;를&amp;nbsp;지정&lt;/b&gt;해서 &lt;br /&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;p data-ke-size=&quot;size16&quot;&gt;물론 이때 메인 데이터베이스의 스프링&amp;nbsp;빈을&amp;nbsp;등록할&amp;nbsp;때&amp;nbsp;@Qualifier&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;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;5) 조회한 빈이 모두 필요할 때&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의도적으로 해당 타입의 스프링 빈이 모두 필요한 경우,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Map과 List로 주입받아 간단하게 전략 패턴을 구현할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1648097750597&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class DiscountService {
    private final Map&amp;lt;String, DiscountPolicy&amp;gt; policyMap;
    private final List&amp;lt;DiscountPolicy&amp;gt; policies;
    
    public DiscountService(Map&amp;lt;String, DiscountPolicy&amp;gt; policyMap,
                           List&amp;lt;DiscountPolicy&amp;gt; policies) {
        this.policyMap = policyMap;
        this.policies = policies;
    }
    
    public int discount(Member member, int price, String discountCode) {
        DiscountPolicy discountPolicy = policyMap.get(discountCode);
        return discountPolicy.discount(member, price);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;Map&amp;lt;String,&amp;nbsp;DiscountPolicy&amp;gt;&amp;nbsp;:&amp;nbsp;map의&amp;nbsp;키에&amp;nbsp;스프링&amp;nbsp;빈의&amp;nbsp;이름을&amp;nbsp;넣어주고,&amp;nbsp;그&amp;nbsp;값으로&lt;br /&gt;DiscountPolicy&amp;nbsp;타입으로&amp;nbsp;조회한&amp;nbsp;모든&amp;nbsp;스프링&amp;nbsp;빈을&amp;nbsp;담아준다.&lt;/li&gt;
&lt;li&gt;List&amp;lt;DiscountPolicy&amp;gt;&amp;nbsp;:&amp;nbsp;DiscountPolicy&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;빈&amp;nbsp;컬렉션이나&amp;nbsp;Map을&amp;nbsp;주입한다&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1648097804801&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;int discountPrice = discountService.discount(member, 10000, &quot;fixDiscountPolicy&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;b&gt;클라이언트가 할인 정책을 선택해서 할인 받도록 하는 경우&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;discountCode 파라미터로 빈 이름을 넘겨주면서 사용될 스프링 빈을 결정할 수 있다.&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;6) 자동, 수동 등록 기준&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;편리한&amp;nbsp;자동&amp;nbsp;기능을&amp;nbsp;기본으로&amp;nbsp;사용하자&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링은@Component&amp;nbsp;뿐만&amp;nbsp;아니라&amp;nbsp;@Controller&amp;nbsp;,&amp;nbsp;@Service&amp;nbsp;,&amp;nbsp;@Repository&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;부트는&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;br /&gt;설계했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;&lt;b&gt;직접&amp;nbsp;등록하는&amp;nbsp;기술&amp;nbsp;지원&amp;nbsp;객체는&amp;nbsp;수동&amp;nbsp;등록&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&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;처리하는 &lt;br /&gt;리포지토리등이&amp;nbsp;모두&amp;nbsp;업무&amp;nbsp;로직이다.&amp;nbsp;보통&amp;nbsp;비즈니스&amp;nbsp;요구사항을&amp;nbsp;개발할&amp;nbsp;때&amp;nbsp;추가되거나&amp;nbsp;변경된다.&lt;/li&gt;
&lt;li&gt;기술&amp;nbsp;지원&amp;nbsp;빈:&amp;nbsp;기술적인&amp;nbsp;문제나&amp;nbsp;공통&amp;nbsp;관심사(AOP)를&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;지원하기&amp;nbsp;위한&amp;nbsp;하부&amp;nbsp;기술이나&amp;nbsp;공통&amp;nbsp;기술들이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기술 지원 로직은 업무 로직과 비교해서 그 수가 매우 적고, 보통 애플리케이션 전반에 걸쳐서 광범위하게 영향을 미친다. 그리고 업무 로직은 문제가 발생했을 때 어디가 문제인지 명확하게 잘 들어나지만, 기술 지원 로직은 적용이 잘 되고 있는지 아닌지 조차 파악하기 어려운 경우가 많다. 그래서 이런 &lt;b&gt;기술 지원 로직들은 가급적&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;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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 부분들은 스프링 자체를 잘 이해하고 스프링의 의도대로 잘 사용하는게 중요하다. 스프링 부트의 경우 DataSource 같은 데이터베이스 연결에 사용하는 기술 지원 로직까지 내부에서 자동으로 등록하는데, 이런 부분은 메뉴얼을 잘 참고해서 스프링 부트가 의도한 대로 편리하게 사용하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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_1648099115100&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private final Map&amp;lt;String, DiscountPolicy&amp;gt; policyMap;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DiscountPolicy 로 어떤 빈들이 들어오게 될지 한눈에 확인이 되지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 경우 수동 빈으로 등록하거나 또는 자동으로하면 특정 패키지에 같이 묶어두는게 좋다.&lt;/p&gt;
&lt;pre id=&quot;code_1648099196483&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
public class DiscountPolicyConfig {

    @Bean
    public DiscountPolicy rateDiscountPolicy() {
        return new RateDiscountPolicy();
    }
    @Bean
    public DiscountPolicy fixDiscountPolicy() {
        return new FixDiscountPolicy();
    }
}&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;a href=&quot;https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8&quot;&gt;스프링 핵심 원리&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;- 기본편(김영한)&lt;/p&gt;</description>
      <category>Spring</category>
      <category>@Autowired</category>
      <category>@ComponentScan</category>
      <category>@Primary</category>
      <category>@Qualifier</category>
      <category>의존관계 주입</category>
      <category>컴포넌트스캔</category>
      <author>mandykr</author>
      <guid isPermaLink="true">https://mandykr.tistory.com/63</guid>
      <comments>https://mandykr.tistory.com/63#entry63comment</comments>
      <pubDate>Mon, 21 Mar 2022 12:18:11 +0900</pubDate>
    </item>
    <item>
      <title>[클린코드] 6. 객체와 자료 구조</title>
      <link>https://mandykr.tistory.com/62</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;목차&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 자료구조 vs 객체&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;3. DTO&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. Active Record vs Data Mapper&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 자료 구조 vs 객체&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;자료 구조(Data Structure)&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;객체(Object)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;데이터 그 자체&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;비즈니스 로직과 관련&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;자료를 공개한다.&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;자료를 숨기고 추상화한다.&lt;br /&gt;자료를 다루는 함수만 공개한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;변수 사이에 조회 함수와 설정 함수로 변수를 다룬다고 객체가 되지 않는다. (getter, setter)&lt;/td&gt;
&lt;td style=&quot;width: 50%;&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;b&gt;예시 (1)&lt;/b&gt; Vehicle - 자료 구조&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조회 함수와 설정 함수로 자료를 공개한다.&lt;/p&gt;
&lt;pre id=&quot;code_1646617948824&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface Vehicle {
    double getFuelTankCapacityInGallons();
    double getGallonsOfGasoline();
}

public class Car implements Vehicle {
    double fuelTankCapacityInGallons;
    double gallonsOfGasoline;

    @Override
    public double getFuelTankCapacityInGallons() {
        return fuelTankCapacityInGallons;
    }

    @Override
    public double getGallonsOfGasoline() {
        return gallonsOfGasoline;
    }
}&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;예시 (1)&lt;/b&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;Vehicle - 객체&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자료를 숨기고 추상화한다. 자료를 다루는 함수만 공개한다.&lt;/p&gt;
&lt;pre id=&quot;code_1646618102614&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface Vehicle {
    double getPercentFuelRemaining();
}

public class Car implements Vehicle {
    double fuelTankCapacityInGallons;
    double gallonsOfGasoline;


    @Override
    public double getPercentFuelRemaining() {
        return this.gallonsOfGasoline / this.fuelTankCapacityInGallons * 100;
    }
}&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)&lt;/b&gt;&lt;span&gt;&lt;span&gt; Shape &lt;/span&gt;- 자료 구조&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;절차적인 코드는 새로운 자료 구조(도형 클래스)를 추가하기 어렵다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;하지만 Geometry 클래스의 함수를 고쳐야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1646618276293&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Square {
    public Point topLeft;
    public double side;
}

public class Rectangle {
    public Point topLeft;
    public double height;
    public double width;
}

public class Circle {
    public Point center;
    public double radius;
}

public class Geometry {
    public final double PI = 3.141592653589793;

    public double area(Object shape) throws NoSuchShapeException {
        if (shape instanceof Square) {
            Square s = (Square) shape;
            return s.side * s.side;
        } else if (shape instanceof Rectangle) {
            Rectangle r = (Rectangle) shape;
            return r.height * r.width;
        } else if (shape instanceof Circle) {
            Circle c = (Circle) shape;
            return PI * c.radius * c.radius;
        }
        throw new NoSuchShapeException();
    }
}&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)&lt;/b&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;Shape&lt;span&gt;&amp;nbsp;&lt;/span&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;하지만 함수를 추가하려면 도형 클래스 전체에 추가 해야 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1646618598217&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Square implements Shape {
    private Point topLeft;
    private double side;

    public double area() {
        return side * side;
    }
}

public class Rectangle implements Shape {
    private Point topLeft;
    private double height;
    private double width;

    public double area() {
        return height * width;
    }
}

public class Circle implements Shape {
    private Point center;
    private double radius;
    public final double PI = 3.141592653589793;

    public double area() {
        return PI * radius * radius;
    }
}&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;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;자료 구조를 사용하는 절차적인 코드는 기본 자료 구조를 변경하지 않으면서 새 함수를 추가하기 쉽다.&lt;/li&gt;
&lt;li&gt;절차적인 코드는 새로운 자료 구조를 추가하기 어렵다. 그러려면 모든 함수를 고쳐야 한다.&lt;/li&gt;
&lt;li&gt;객체지향 코드는 기존 함수를 변경하지 않으면서 새 클래스를 추가하기 쉽다.&lt;/li&gt;
&lt;li&gt;객체지향 코드는 새로운 함수를 추가하기 어렵다. 그러려면 모든 클래스를 고쳐야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 객체 - 디미터 법칙&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;디미터 법칙&lt;/b&gt;은 잘 알려진 휴리스틱으로 모듈은 자신이 조작하는 객체의 속사정을 몰라야 한다는 법칙이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클래스 C의 메서드 f는 다음과 같은 객체의 메서드만 호출해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(휴리스틱 : 경험에 기반하여 문제를 해결 하기 위해 발견한 방법. 의사 결정을 단순화 하기 위한 법칙들.)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;클래스 C&lt;/li&gt;
&lt;li&gt;자신이 생성한 객체&lt;/li&gt;
&lt;li&gt;자신의 인수로 넘어온 객체&lt;/li&gt;
&lt;li&gt;C 인스턴스 변수에 저장된 객체&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 객체에서 허용된 메서드가 반환하는 객체의 메서드는 호출하면 안된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같은 코드를 &lt;b&gt;기차 충돌&lt;/b&gt;이라 부른다.&lt;/p&gt;
&lt;pre id=&quot;code_1646619221648&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath();&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;디미터 법칙을 위반하는지 여부는 ctxt, options, scratchDir이 자료 구조인지 객체인지에 달렸다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자료 구조라면 당연히 내부 구조를 노출하므로 디미터 법칙이 적용되지 않고 다음과 같이 구현할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1646619643116&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;final String outputDir = ctxt.options.scratchDir.absolutePath;&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;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_1646620069728&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;BufferedOutputStream bos = ctxt.createScratchFileStream(classFileName);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. DTO&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;DTO&lt;/b&gt;(Data Transfer Object) : 자료 구조&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 계층 가 데이터를 교환할 때 사용&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;로직 없이 필드만 갖는다.&lt;/li&gt;
&lt;li&gt;일반적으로 클래스 명이 Dto or DTO로 끝난다.&lt;/li&gt;
&lt;li&gt;getter/setter를 갖기도 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Beans&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java Beans : 데이터 표현이 목적인 자바 객체&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;멤버 변수는 private 속성이다.&lt;/li&gt;
&lt;li&gt;getter와 setter를 가진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. Active Record vs Data Mapper&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;Active Record&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Database row를 객체에 매핑하는 패턴&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&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;247&quot; data-origin-height=&quot;166&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cnV8hW/btru7Rtuxtt/JKCD94MmJ2kIrn3BPaMX70/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cnV8hW/btru7Rtuxtt/JKCD94MmJ2kIrn3BPaMX70/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cnV8hW/btru7Rtuxtt/JKCD94MmJ2kIrn3BPaMX70/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcnV8hW%2Fbtru7Rtuxtt%2FJKCD94MmJ2kIrn3BPaMX70%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;247&quot; height=&quot;166&quot; data-origin-width=&quot;247&quot; data-origin-height=&quot;166&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;객체가 row를 담을 뿐 아니라 database에 대한 접근을 포함한다.&lt;/li&gt;
&lt;li&gt;Person의 속성을 담을 뿐 아니라, 생성/수정도 객체 안에서 수행할 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;사례 - Ruby on rails&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Data Mapper&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;310&quot; data-origin-height=&quot;88&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dVzRtL/btrvkFZxqJr/wvG9Suyz5vhCDKkqZpWOSk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dVzRtL/btrvkFZxqJr/wvG9Suyz5vhCDKkqZpWOSk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dVzRtL/btrvkFZxqJr/wvG9Suyz5vhCDKkqZpWOSk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdVzRtL%2FbtrvkFZxqJr%2FwvG9Suyz5vhCDKkqZpWOSk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;384&quot; height=&quot;109&quot; data-origin-width=&quot;310&quot; data-origin-height=&quot;88&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;row를 담는 객체와 database에 접근할 수 있는 객체가 분리 되어 있다.&lt;/li&gt;
&lt;li&gt;Person은 값만 담고 있고, 생성/수정 등 액션은 PersonMapper에서 담당 한다.&lt;/li&gt;
&lt;li&gt;사례-Hibernate&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;Clean Code(클린 코드) 애자일 소프트웨어 장인 정신 - 로버트 C. 마틴 지음&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;a href=&quot;https://zero-base.co.kr/category_dev_camp/cleancode_1book&quot;&gt;https://zero-base.co.kr/category_dev_camp/cleancode_1book&lt;/a&gt;&lt;/p&gt;</description>
      <category>책 내용 정리/클린코드(clean code)</category>
      <category>clean code</category>
      <category>객체와 자료 구조</category>
      <category>디미터 법칙</category>
      <author>mandykr</author>
      <guid isPermaLink="true">https://mandykr.tistory.com/62</guid>
      <comments>https://mandykr.tistory.com/62#entry62comment</comments>
      <pubDate>Mon, 7 Mar 2022 11:42:42 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] 1. 스프링 컨테이너</title>
      <link>https://mandykr.tistory.com/61</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;목차&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. DI 컨테이너&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;3. 싱글톤 컨테이너&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. DI 컨테이너&lt;/h3&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;613&quot; data-origin-height=&quot;213&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cMBGNm/btru8wnBVIv/LVuXmwSVl49jBBtHI991R0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cMBGNm/btru8wnBVIv/LVuXmwSVl49jBBtHI991R0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cMBGNm/btru8wnBVIv/LVuXmwSVl49jBBtHI991R0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcMBGNm%2Fbtru8wnBVIv%2FLVuXmwSVl49jBBtHI991R0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;613&quot; height=&quot;213&quot; data-origin-width=&quot;613&quot; data-origin-height=&quot;213&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;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;다음과 같이 주문 서비스가 회원 저장소와 할인 정책의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&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-origin-width=&quot;614&quot; data-origin-height=&quot;192&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/4VDHn/btru2vpE3HD/aozM69UOWeGeHL8JAP42KK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/4VDHn/btru2vpE3HD/aozM69UOWeGeHL8JAP42KK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/4VDHn/btru2vpE3HD/aozM69UOWeGeHL8JAP42KK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F4VDHn%2Fbtru2vpE3HD%2FaozM69UOWeGeHL8JAP42KK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;192&quot; data-origin-width=&quot;614&quot; data-origin-height=&quot;192&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 보면 주문 서비스가 회원 저장소의 구현체인 MemoryMemberRepository와 할인 정책의 구현체인 FixDiscountPolicy에 의존하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터베이스가 변경되거나 할인 정책이 변경되면 비즈니스 코드인 OrderServiceImpl 클래스를 수정해 주어야 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1646371445695&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class OrderServiceImpl implements OrderService {
    private final MemberRepository memberRepository = new MemoryMemberRepository();
    private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
    
    @Override
    public Order createOrder(Long memberId, String itemName, int itemPrice) {
        Member member = memberRepository.findById(memberId);
        int discountPrice = discountPolicy.discount(member, itemPrice);
        return new Order(memberId, itemName, itemPrice, discountPrice);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 &lt;b&gt;&lt;b&gt;OCP와&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/b&gt;&lt;b&gt;DIP원칙&lt;/b&gt;에 따라 다음과 같이 역할과 구현을 분리해 주어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;역할을 추상화 하면 구현 객체를&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;자유롭게&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;조립하면서 유연하게 회원 저장소와 할인 정책을 변경할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1932&quot; data-origin-height=&quot;674&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cFRDkG/btruZPW3TYf/gLoWkRgzO9tqUkcgH956Bk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cFRDkG/btruZPW3TYf/gLoWkRgzO9tqUkcgH956Bk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cFRDkG/btruZPW3TYf/gLoWkRgzO9tqUkcgH956Bk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcFRDkG%2FbtruZPW3TYf%2FgLoWkRgzO9tqUkcgH956Bk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1932&quot; height=&quot;674&quot; data-origin-width=&quot;1932&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;회원 저장소와 할인 정책의 역할을 정의하는 인터페이스와 역할을 구현하는 클래스를 만들고 주문 서비스가 추상에 의존하도록 설정하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직은 구현 객체가 주문 서비스 내에 없기 때문에 NPE(null pointer 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;b&gt;SRP원칙&lt;/b&gt;에 따라 &lt;b&gt;구현 객체를 생성하고 연결하는 책임을 별도의 클래스&lt;/b&gt;가 갖도록 위임하고 &lt;span&gt;주문 서비스는&lt;span&gt; 해당 객체를 전달받아 사용하며 주문에 대한 기능에만 집중하도록 할 수 있다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;612&quot; data-origin-height=&quot;416&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bMgSan/btru5qu1WOj/cn4I5kH5MBnuKQS9Vuty50/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bMgSan/btru5qu1WOj/cn4I5kH5MBnuKQS9Vuty50/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bMgSan/btru5qu1WOj/cn4I5kH5MBnuKQS9Vuty50/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbMgSan%2Fbtru5qu1WOj%2Fcn4I5kH5MBnuKQS9Vuty50%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;612&quot; height=&quot;416&quot; data-origin-width=&quot;612&quot; data-origin-height=&quot;416&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구성 역할을 하는 AppConfig 클래스를 만들어 사용한다.​&lt;/p&gt;
&lt;pre id=&quot;code_1646372868068&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class AppConfig {
    public MemberService memberService() {
        return new MemberServiceImpl(memberRepository());
    }
    
    public OrderService orderService() {
        return new OrderServiceImpl(memberRepository(), discountPolicy());
    }
    
    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }
    
    public DiscountPolicy discountPolicy() {
        return new FixDiscountPolicy();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션을 실행하는 시점에 AppConfig에서 필요한 객체를 꺼내 전달하고&lt;/p&gt;
&lt;pre id=&quot;code_1646372836649&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class OrderApp {
    public static void main(String[] args) {
        AppConfig appConfig = new AppConfig();
        
        MemberService memberService = appConfig.memberService();
        OrderService orderService = appConfig.orderService();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주문 서비스에서는 생성자를 통해 객체를 전달받아 사용한다.&lt;/p&gt;
&lt;pre id=&quot;code_1646372769156&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class OrderServiceImpl implements OrderService {
    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;
    
    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }
    
    @Override
    public Order createOrder(Long memberId, String itemName, int itemPrice) {
        Member member = memberRepository.findById(memberId);
        int discountPrice = discountPolicy.discount(member, itemPrice);
        return new Order(memberId, itemName, itemPrice, discountPrice);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AppConfig의 등장으로 객체를 생성하고 구성하는 영역과 사용 영역으로 &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;위와 같이 필요한 구현 객체를 생성하고 사용하며 프로그램의 제어를 스스로 하지 않고 AppConfig 처럼 &lt;span&gt;&lt;span&gt;외부에서 프로그램의 흐름을 제어하는 것을 &lt;b&gt;제어의 역전(IoC)&lt;/b&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;span&gt;&lt;span&gt;애플리케이션 실행 시점(런타임)에 외부에서 실제 객체를 생성하고 사용 영역에 전달해서 실제 의존관계가 연결되는 것을 &lt;b&gt;의존관계 주입(DI)&lt;/b&gt;이라고 하며, &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;AppConfig 처럼 DI를 담당하는 것을&lt;b&gt; DI 컨테이너&lt;/b&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 스프링 컨테이너&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Configuration, @Bean 어노테이션으로 스프링이 AppConfig를 사용해서 컨테이너를 생성하고 DI를 하도록 설정할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1646715276557&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
public class AppConfig {
    @Bean
    public MemberService memberService() {
        return new MemberServiceImpl(memberRepository());
    }
    @Bean
    public OrderService orderService() {
        return new OrderServiceImpl(
                memberRepository(),
                discountPolicy());
    }
    @Bean
    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }
    @Bean
    public DiscountPolicy discountPolicy() {
        return new RateDiscountPolicy();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링은 @Configuration이 붙은 AppConfig를 설정정보로 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Bean이 붙은 메서드를 모두 호출해서 반환된 객체를 스프링 컨테이너에 등록한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 &lt;b&gt;스프링 빈&lt;/b&gt;은 @Bean이 붙은 &lt;b&gt;메서드의 이름&lt;/b&gt;을 빈의 이름으로 사용한다.&lt;/p&gt;
&lt;pre id=&quot;code_1646715486417&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
MemberService memberService = ac.getBean(&quot;memberService&quot;, MemberService.class);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정 정보(AppConfig)를 전달해 스프링 컨테이너를 생성하고 ApplicationContext 에서 스프링 빈을 꺼내 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;스프링 컨테이너&lt;/b&gt;는 BeanFactory와 ApplicationContext로 구분되어 있는데&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;위의 코드에서 자바 설정 클래스를 기반으로 &lt;b&gt;AnnotationConfigApplicationContext&lt;/b&gt;를 생성했는데 XML 기반으로 &lt;b&gt;GenericXmlApplicationContext&lt;/b&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;h4 data-ke-size=&quot;size20&quot;&gt;1) 스프링 컨테이너 생성 과정&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링은 &lt;b&gt;스프링 빈을 생성&lt;/b&gt;하고, &lt;b&gt;의존관계를 설정&lt;/b&gt;하는 단계가 나누어져 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/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;스프링 빈 이름은 메서드 이름을 사용하지만 @Bean(name=&quot;memberService2&quot;)와 같이 직접 부여할 수도 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;762&quot; data-origin-height=&quot;347&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Zbn2a/btrvnWavlmm/RypqXjRP2WBdTPOkhytGKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Zbn2a/btrvnWavlmm/RypqXjRP2WBdTPOkhytGKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Zbn2a/btrvnWavlmm/RypqXjRP2WBdTPOkhytGKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZbn2a%2FbtrvnWavlmm%2FRypqXjRP2WBdTPOkhytGKk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;762&quot; height=&quot;347&quot; data-origin-width=&quot;762&quot; data-origin-height=&quot;347&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;의존관계 설정&lt;/b&gt;&lt;/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-origin-width=&quot;760&quot; data-origin-height=&quot;349&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bsuOMo/btrvhoxMGWm/hgXEK21glyuSGdOsk4ywuk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bsuOMo/btrvhoxMGWm/hgXEK21glyuSGdOsk4ywuk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bsuOMo/btrvhoxMGWm/hgXEK21glyuSGdOsk4ywuk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbsuOMo%2FbtrvhoxMGWm%2FhgXEK21glyuSGdOsk4ywuk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;349&quot; data-origin-width=&quot;760&quot; data-origin-height=&quot;349&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;h4 data-ke-size=&quot;size20&quot;&gt;2) 스프링 빈 조회&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;(1) 모든 빈 조회 : ac.getBeanDefinitionNames()&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;스프링 컨테이너에 등록된 모든 빈&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1646716921082&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;String[] beanDefinitionNames = ac.getBeanDefinitionNames();
for (String beanDefinitionName : beanDefinitionNames) {
    Object bean = ac.getBean(beanDefinitionName);
    System.out.println(&quot;name=&quot; + beanDefinitionName + &quot; object=&quot; + bean);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;애플리케이션 빈&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1646717010778&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;String[] beanDefinitionNames = ac.getBeanDefinitionNames();
for (String beanDefinitionName : beanDefinitionNames) {
    BeanDefinition beanDefinition = ac.getBeanDefinition(beanDefinitionName);
    
    //Role ROLE_APPLICATION: 직접 등록한 애플리케이션 빈
    //Role ROLE_INFRASTRUCTURE: 스프링이 내부에서 사용하는 빈
    if (beanDefinition.getRole() == BeanDefinition.ROLE_APPLICATION) {
        Object bean = ac.getBean(beanDefinitionName);
        System.out.println(&quot;name=&quot; + beanDefinitionName + &quot; object=&quot; + bean);
    }
}&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;(2) 스프링 빈 조회 : ac.getBean(빈이름, 타입)&lt;/p&gt;
&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_1646717250858&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;MemberService memberService = ac.getBean(&quot;memberService&quot;, MemberService.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;&lt;b&gt;타입만으로 조회&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1646717277294&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;MemberService memberService = ac.getBean(MemberService.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;&lt;b&gt;이름과 타입으로 조회&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1646717416471&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;MemberServiceImpl memberService = ac.getBean(&quot;memberService&quot;, MemberServiceImpl.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;&lt;b&gt;타입으로 모든 빈 조회&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1646717507876&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Map&amp;lt;String, MemberRepository&amp;gt; beansOfType = ac.getBeansOfType(MemberRepository.class);
for (String key : beansOfType.keySet()) {
    System.out.println(&quot;key = &quot; + key + &quot; value = &quot; + beansOfType.get(key));
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;상속 관계 스프링 빈 조회&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부모 타입으로 조회하면 모든 자식 타입도 함꼐 조회된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Object 타입으로 조회하면 모든 빈이 조회된다.&lt;/p&gt;
&lt;pre id=&quot;code_1646717677237&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Map&amp;lt;String, DiscountPolicy&amp;gt; beansOfType = ac.getBeansOfType(DiscountPolicy.class);
for (String key : beansOfType.keySet()) {
    System.out.println(&quot;key = &quot; + key + &quot; value=&quot; + beansOfType.get(key));
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예외&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;조회 대상 스프링 빈이 없는 경우 :&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;NoSuchBeanDefinitionException&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조회 대상 스프링 빈이 둘 이상인 경우 : NoUniqueBeanDefinitionException&lt;/p&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;3) BeanFactory와 ApplicationContext&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;274&quot; data-origin-height=&quot;433&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cvpnBb/btrvrhESCJe/KpzOoT9pfUAbEqwUH9nhgK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cvpnBb/btrvrhESCJe/KpzOoT9pfUAbEqwUH9nhgK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cvpnBb/btrvrhESCJe/KpzOoT9pfUAbEqwUH9nhgK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcvpnBb%2FbtrvrhESCJe%2FKpzOoT9pfUAbEqwUH9nhgK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;274&quot; height=&quot;433&quot; data-origin-width=&quot;274&quot; data-origin-height=&quot;433&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;BeanFactory : 빈 관리기능&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;(getBean() 등 제공)&lt;/p&gt;
&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;ApplicationContext : 빈 관리기능 + 편리한 부가 기능&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빈을 관리하고 검색하는 기능은 BeanFactory의 기능을 상속받아서 사용하고.&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;761&quot; data-origin-height=&quot;268&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ccfPs6/btrvoz0zoH0/8KZoO6zqDqIGKEX5Y2fF80/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ccfPs6/btrvoz0zoH0/8KZoO6zqDqIGKEX5Y2fF80/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ccfPs6/btrvoz0zoH0/8KZoO6zqDqIGKEX5Y2fF80/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FccfPs6%2Fbtrvoz0zoH0%2F8KZoO6zqDqIGKEX5Y2fF80%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;761&quot; height=&quot;268&quot; data-origin-width=&quot;761&quot; data-origin-height=&quot;268&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MessageSource&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메시지소스를 활용한 국제화 기능&lt;/li&gt;
&lt;li&gt;한국에서 들어오면 한국어로, 영어권에서 들어오면 영어로 출력&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EnvironmentCapable&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;환경변수&lt;/li&gt;
&lt;li&gt;로컬,&amp;nbsp;개발,&amp;nbsp;운영등을&amp;nbsp;구분해서&amp;nbsp;처리&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;ApplicationEventPublisher&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;애플리케이션 이벤트&lt;/li&gt;
&lt;li&gt;이벤트를&amp;nbsp;발행하고&amp;nbsp;구독하는&amp;nbsp;모델을&amp;nbsp;편리하게&amp;nbsp;지원&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ResourceLoader&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;편리한 리소스 조회&lt;/li&gt;
&lt;li&gt;파일,&amp;nbsp;클래스패스,&amp;nbsp;외부&amp;nbsp;등에서&amp;nbsp;리소스를&amp;nbsp;편리하게&amp;nbsp;조회&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ApplicationContext 구현체&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 컨테이너는 다양한 ApplicationContext의 구현체를 제공하기 때문에 애플리케이션의 설정정보를 유연하게 변경할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;764&quot; data-origin-height=&quot;452&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bJwFkj/btrvvvIuJFZ/M2YSbvVjAiIhRDT52mDpI1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bJwFkj/btrvvvIuJFZ/M2YSbvVjAiIhRDT52mDpI1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bJwFkj/btrvvvIuJFZ/M2YSbvVjAiIhRDT52mDpI1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbJwFkj%2FbtrvvvIuJFZ%2FM2YSbvVjAiIhRDT52mDpI1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;452&quot; data-origin-width=&quot;764&quot; data-origin-height=&quot;452&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;h4 data-ke-size=&quot;size20&quot;&gt;4) BeanDefinition&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다양한 설정 파일을 통해 스프링 컨테이너가 스프링 빈을 생성할 수 있는 이유는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각각 빈에 대한 메타정보를 BeanDefinition이라는 추상화를 만들기 때문이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;763&quot; data-origin-height=&quot;418&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/YW9lj/btrvpazCeJB/zECTgIUnkRE8A72dtCegdk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/YW9lj/btrvpazCeJB/zECTgIUnkRE8A72dtCegdk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YW9lj/btrvpazCeJB/zECTgIUnkRE8A72dtCegdk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYW9lj%2FbtrvpazCeJB%2FzECTgIUnkRE8A72dtCegdk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;763&quot; height=&quot;418&quot; data-origin-width=&quot;763&quot; data-origin-height=&quot;418&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정 형식 각각 BeanDefinitionReader를 통해 BeanDefinition을 만들고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 컨테이너는 BeanDefinition에 저장된 빈에 대한 메타정보를 기반을 스프링 빈을 생성한다.&lt;/p&gt;
&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;BeanDefinition&amp;nbsp;정보&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;BeanClassName:&amp;nbsp;생성할&amp;nbsp;빈의&amp;nbsp;클래스&amp;nbsp;명(자바&amp;nbsp;설정&amp;nbsp;처럼&amp;nbsp;팩토리&amp;nbsp;역할의&amp;nbsp;빈을&amp;nbsp;사용하면&amp;nbsp;없음)&lt;/li&gt;
&lt;li&gt;factoryBeanName:&amp;nbsp;팩토리&amp;nbsp;역할의&amp;nbsp;빈을&amp;nbsp;사용할&amp;nbsp;경우&amp;nbsp;이름,&amp;nbsp;예)&amp;nbsp;appConfig&lt;/li&gt;
&lt;li&gt;factoryMethodName:&amp;nbsp;빈을&amp;nbsp;생성할&amp;nbsp;팩토리&amp;nbsp;메서드&amp;nbsp;지정,&amp;nbsp;예)&amp;nbsp;memberService&lt;/li&gt;
&lt;li&gt;Scope:&amp;nbsp;싱글톤(기본값)&lt;/li&gt;
&lt;li&gt;lazyInit: 스프링 컨테이너를 생성할 때 빈을 생성하는 것이 아니라, 실제 빈을 사용할 때 까지 최대한 생성을&amp;nbsp;지연처리&amp;nbsp;하는지&amp;nbsp;여부&lt;/li&gt;
&lt;li&gt;InitMethodName:&amp;nbsp;빈을&amp;nbsp;생성하고,&amp;nbsp;의존관계를&amp;nbsp;적용한&amp;nbsp;뒤에&amp;nbsp;호출되는&amp;nbsp;초기화&amp;nbsp;메서드&amp;nbsp;명&lt;/li&gt;
&lt;li&gt;DestroyMethodName:&amp;nbsp;빈의&amp;nbsp;생명주기가&amp;nbsp;끝나서&amp;nbsp;제거하기&amp;nbsp;직전에&amp;nbsp;호출되는&amp;nbsp;메서드&amp;nbsp;명&lt;/li&gt;
&lt;li&gt;Constructor&amp;nbsp;arguments,&amp;nbsp;Properties:&amp;nbsp;의존관계&amp;nbsp;주입에서&amp;nbsp;사용한다.&amp;nbsp;(자바&amp;nbsp;설정&amp;nbsp;처럼&amp;nbsp;팩토리&lt;/li&gt;
&lt;li&gt;역할의&amp;nbsp;빈을&amp;nbsp;사용하면&amp;nbsp;없음)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바 설정 파일을 통해 BeanDefinition의 정보를 만들때는 외부에서 AppConfig의 메소드를 호출하는 팩토리 메소드 방식이기 때문에 BeanClassName이 비어있고 factoryBeanName, factoryMethodName이 등록되어 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 싱글톤 컨테이너&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) 싱글톤 패턴&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;순수한 DI 컨테이너인 AppConfig는 요청을 할 때마다 객체를 새로 생성하기 때문에 &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;895&quot; data-origin-height=&quot;444&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bnvsjX/btrwlRe0QRb/nMhrzwhdsghpT9IMNKiz6K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bnvsjX/btrwlRe0QRb/nMhrzwhdsghpT9IMNKiz6K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bnvsjX/btrwlRe0QRb/nMhrzwhdsghpT9IMNKiz6K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbnvsjX%2FbtrwlRe0QRb%2FnMhrzwhdsghpT9IMNKiz6K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;727&quot; height=&quot;361&quot; data-origin-width=&quot;895&quot; data-origin-height=&quot;444&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 클래스의 인스턴스를 1개만 만들어 공유하도록 DI 객체를 구성하기 위한 클래스에 &lt;b&gt;싱글톤 패턴&lt;/b&gt;을 적용해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;static 영역에 인스턴스를 미리 1개 만들어 올려두고 getInstance() 메서드를 통해서만 인스턴스를 제공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성자는 private으로 선언해 외부에서의 객체생성 요청을 막는다.&lt;/p&gt;
&lt;pre id=&quot;code_1647655622500&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class SingletonService {
    //1. static 영역에 객체를 딱 1개만 생성해둔다.
    private static final SingletonService instance = new SingletonService();
    
    //2. public으로 열어서 객체 인스터스가 필요하면 이 static 메서드를 통해서만 조회하도록 허용한다.
    public static SingletonService getInstance() {
        return instance;
    }
    
    //3. 생성자를 private으로 선언해서 외부에서 new 키워드를 사용한 객체 생성을 못하게 막는다.
    private SingletonService() {
    }
    
    public void logic() {
        System.out.println(&quot;싱글톤 객체 로직 호출&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1647655904779&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Test
SingletonService singletonService1 = SingletonService.getInstance();
SingletonService singletonService2 = SingletonService.getInstance();
assertThat(singletonService1).isSameAs(singletonService2);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;싱글톤 패턴을 적용해서 고객의 요청에 대해 하나의 &lt;span&gt;인스턴스를&lt;span&gt; 공유하도록 만들었지만&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;다음과 같은 문제점이 있다.&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;싱글톤&amp;nbsp;패턴을&amp;nbsp;구현하는&amp;nbsp;코드&amp;nbsp;자체가&amp;nbsp;많이&amp;nbsp;들어간다.&lt;/li&gt;
&lt;li&gt;의존관계상&amp;nbsp;클라이언트가&amp;nbsp;구체&amp;nbsp;클래스에&amp;nbsp;의존한다.&amp;nbsp;DIP를&amp;nbsp;위반한다.&lt;/li&gt;
&lt;li&gt;클라이언트가&amp;nbsp;구체&amp;nbsp;클래스에&amp;nbsp;의존해서&amp;nbsp;OCP&amp;nbsp;원칙을&amp;nbsp;위반할&amp;nbsp;가능성이&amp;nbsp;높다.&lt;/li&gt;
&lt;li&gt;테스트하기&amp;nbsp;어렵다.&lt;/li&gt;
&lt;li&gt;내부&amp;nbsp;속성을&amp;nbsp;변경하거나&amp;nbsp;초기화&amp;nbsp;하기&amp;nbsp;어렵다.&lt;/li&gt;
&lt;li&gt;private&amp;nbsp;생성자로&amp;nbsp;자식&amp;nbsp;클래스를&amp;nbsp;만들기&amp;nbsp;어렵다.&lt;/li&gt;
&lt;li&gt;결론적으로&amp;nbsp;유연성이&amp;nbsp;떨어진다.&lt;/li&gt;
&lt;li&gt;안티패턴으로&amp;nbsp;불리기도&amp;nbsp;한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2) 싱글톤 컨테이너&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;DIP, OCP, 테스트, private 생성자로 부터 자유롭게 싱글톤을 사용할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;671&quot; data-origin-height=&quot;336&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cc5hMm/btrwnOOSwX2/Emn1BzwdBCIkTvBOgD7wl1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cc5hMm/btrwnOOSwX2/Emn1BzwdBCIkTvBOgD7wl1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cc5hMm/btrwnOOSwX2/Emn1BzwdBCIkTvBOgD7wl1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcc5hMm%2FbtrwnOOSwX2%2FEmn1BzwdBCIkTvBOgD7wl1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;354&quot; data-origin-width=&quot;671&quot; data-origin-height=&quot;336&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;h4 data-ke-size=&quot;size20&quot;&gt;3) @Configuration과 싱글톤&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Configuration은 AppConfig와 같은 설정정보 클래스의 빈을 스프링 컨테이너에 등록할 때&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;CGLIB 바이트 코드 조작 라이브러리를 사용&lt;/b&gt;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 AppConfig는 컨테이너에 등록하지 않고 &lt;b&gt;AppConfig를 상속받는 클래스의 빈을 등록&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;672&quot; data-origin-height=&quot;357&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bBEp26/btrwmsTlSrW/SaOaL5kFOSncb77OMC3oCk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bBEp26/btrwmsTlSrW/SaOaL5kFOSncb77OMC3oCk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bBEp26/btrwmsTlSrW/SaOaL5kFOSncb77OMC3oCk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbBEp26%2FbtrwmsTlSrW%2FSaOaL5kFOSncb77OMC3oCk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;672&quot; height=&quot;357&quot; data-origin-width=&quot;672&quot; data-origin-height=&quot;357&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 컨테이너에서 AppConfig 타입으로 빈을 조회해 보면 클래스의 타입이 다른 것을 확인할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1647657110123&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
AppConfig bean = ac.getBean(AppConfig.class);
System.out.println(&quot;bean = &quot; + bean.getClass());&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;@Configuration을 사용하지 않은 경우&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1647657155784&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;bean = class hello.core.AppConfig&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;b&gt;@Configuration을 사용한 경우&lt;/b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1647740644773&quot; class=&quot;gams&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;bean = class hello.core.AppConfig$$EnhancerBySpringCGLIB$$bd479d70&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;AppConfig@CGLIB&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AppConfig@CGLIB 는 AppConfig를 통해 등록되는 빈들의 &lt;b&gt;싱글톤을 보장&lt;/b&gt;해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Bean을 통해 컨텍스트에 스프링 빈을 등록할 수 있지만 &lt;b&gt;@Configuration이 없으면 스프링 빈 간의 의존 관계에서 싱글톤을 보장하지 않는다.&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;AppConfig를 통해 @Configuration과 싱글톤을 테스트 해본다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AppConfig에서 MemoryMemberRepository가 생성되는 메소드는 3곳이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;memberService(), orderService(), memberRepository()&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Configuration이 싱글톤을 보장해 주더라도 AppConfig를 통해 스프링 빈을 만들면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;memberRepository()는 3번 호출되어야 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1647657475447&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class AppConfig {
    @Bean
    public MemberService memberService() {
        System.out.println(&quot;call AppConfig.memberService&quot;);
        return new MemberServiceImpl(memberRepository());
    }
    
    @Bean
    public OrderService orderService() {
        System.out.println(&quot;call AppConfig.orderService&quot;);
        return new OrderServiceImpl(
                memberRepository(),
                discountPolicy());
    }
    
    @Bean
    public MemberRepository memberRepository() {
        System.out.println(&quot;call AppConfig.memberRepository&quot;);
        return new MemoryMemberRepository();
    }
    
    @Bean
    public DiscountPolicy discountPolicy() {
        return new RateDiscountPolicy();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1647657759007&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Test
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
MemberServiceImpl memberService = ac.getBean(&quot;memberService&quot;, MemberServiceImpl.class);
OrderServiceImpl orderService = ac.getBean(&quot;orderService&quot;, OrderServiceImpl.class);
MemberRepository memberRepository = ac.getBean(&quot;memberRepository&quot;, MemberRepository.class);

System.out.println(&quot;memberService -&amp;gt; memberRepository = &quot; + memberService.getMemberRepository());
System.out.println(&quot;orderService -&amp;gt; memberRepository = &quot; + orderService.getMemberRepository());
System.out.println(&quot;memberRepository = &quot; + memberRepository);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Configuration 없이 테스트해 보면 총 3번의 memberRepository()가 호출된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 new 키워드로 memberRepository를 생성하기 때문에 모두 다른 객체이며&amp;nbsp;&lt;b&gt;싱글톤을 보장하지 않는다.&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1647657813828&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;call AppConfig.memberService
call AppConfig.memberRepository
call AppConfig.orderService
call AppConfig.memberRepository
call AppConfig.memberRepository&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Configuration 을 사용하면 memberRepository()가 단 한번만 호출되기 때문에 &lt;b&gt;싱글톤을 보장한다.&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1647657877921&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;call AppConfig.memberService
call AppConfig.memberRepository
call AppConfig.orderService&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 테스트에서 볼 수 있듯이 @Configuration은 이미 스프링 컨테이너에 등록된 빈은&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새롭게 생성하지 않는다는 것을 확인할 수 있고 이 것이 가능한 건 AppConfig가 아닌&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;AppConfig@CGLIB를 사용하기 때문&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;b&gt;AppConfig@CGLIB&amp;nbsp;예상&amp;nbsp;코드&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1647658322745&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
public MemberRepository memberRepository() {

    if (memoryMemberRepository가 이미 스프링 컨테이너에 등록되어 있으면?) {
        return 스프링 컨테이너에서 찾아서 반환;
    } else { //스프링 컨테이너에 없으면
        기존 로직을 호출해서 MemoryMemberRepository를 생성하고 스프링 컨테이너에 등록
        return 반환;
    }
}&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;@Bean만 사용해도 스프링 빈으로 등록되지만 memberRepository()처럼 &lt;b&gt;의존관계 주입이 필요해서 메서드를 직접 호출하는 경우에는 싱글톤을 보장하지 않지만 @Configuration을 통해 가능&lt;/b&gt;하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4) 싱글톤 방식의 주의점&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&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;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;대신에&amp;nbsp;자바에서&amp;nbsp;공유되지&amp;nbsp;않는,&amp;nbsp;지역변수,&amp;nbsp;파라미터,&amp;nbsp;ThreadLocal&amp;nbsp;등을&amp;nbsp;사용해야&amp;nbsp;한다.&lt;/p&gt;
&lt;pre id=&quot;code_1647740952858&quot; class=&quot;arduino&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class StatefulService {
    private int price; //상태를 유지하는 필드
    
    public void order(String name, int price) {
        System.out.println(&quot;name = &quot; + name + &quot; price = &quot; + price);
        this.price = price; //여기가 문제!
    }
    
    public int getPrice() {
        return price;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1647740952861&quot; class=&quot;axapta&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;statefulService1.order(&quot;userA&quot;, 10000);
statefulService2.order(&quot;userB&quot;, 20000);
int price = statefulService1.getPrice(); //20000원 출력&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;StatefulService를 price라는 상태를 갖도록 설계하고 스프링 빈으로 등록하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;userA, userB가 싱글톤 객체를 공유하기 때문에 price 필드를 공유한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;userA의 주문 금액이 userB에 의해 변경되는 문제가 발생하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;출처&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;a href=&quot;https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;스프링 핵심 원리&lt;/a&gt; - 기본편(김영한)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Spring</category>
      <category>@Configuration</category>
      <category>ApplicationContext</category>
      <category>BeanDefinition</category>
      <category>BeanFactory</category>
      <category>cglib</category>
      <category>Spring</category>
      <category>스프링 컨테이너</category>
      <author>mandykr</author>
      <guid isPermaLink="true">https://mandykr.tistory.com/61</guid>
      <comments>https://mandykr.tistory.com/61#entry61comment</comments>
      <pubDate>Fri, 4 Mar 2022 14:33:45 +0900</pubDate>
    </item>
    <item>
      <title>[클린코드] 5. 형식 맞추기</title>
      <link>https://mandykr.tistory.com/60</link>
      <description>&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;2. 적절한 코드 형식을 작성하는 방법&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;4.&lt;span&gt;&amp;nbsp;&lt;/span&gt;TeamCodingConvention&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 형식을 맞추는 목적&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좋은 소프트웨어 시스템은 읽기 쉬운 문서로 이루어 져야 하기 때문에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소스 코드의 스타일이 일관적이고 매끄러워야 한다.&lt;/p&gt;
&lt;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;h3 data-ke-size=&quot;size23&quot;&gt;2. 적절한 코드 형식을 작성하는 방법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;신문 기사와 같이 위에서 아래로 자연스럽게 읽히며 고차원에서 시작해 마지막에 저차원의 개념이 나오도록 작성한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) 개념은 빈 행으로 분리&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;h4 data-ke-size=&quot;size20&quot;&gt;2) 클래스의 적절한 행 길이&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드길이를 200 줄정도로 제한 하는것은 반드시 지킬 엄격한 규칙은 아니지만,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 큰 파일보다 작은 파일이 이해하기 쉬우며&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클래스가 200 라인을 넘어가는 큰 파일인 경우 여러가지 일을 하고 있는지 의심해 봐야 한다.&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;3) 수직 거리&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;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;&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;&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;함수 이름과 이어지는 괄호 사이에는 공백을 넣지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 클래스 및 인터페이스 선언&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오라클 문서에서 권장하는 Class 내부 코드 순서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://www.oracle.com/java/technologies/javase/codeconventions-fileorganization.html#1852&quot;&gt;Oracle Code Convention&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;1) static변수 : public -&amp;gt; protected -&amp;gt; package -&amp;gt; private순서&lt;br /&gt;2) instance변수 : public -&amp;gt; protected -&amp;gt; package -&amp;gt; private&lt;br /&gt;3) 생성자&lt;br /&gt;4) 메서드 : public 메서드에서 호출되는 private 메서드는 그 아래에 둔다. 가독성 위주로 그룹핑.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. Team Coding Convention&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발 언어의 컨벤션이 우선이지만, 애매한 부분은 팀 컨벤션을 따른다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예) [MySQL Convention] - 컬럼명은 snake_case로 네이밍한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예) [Team Convention] - enum 타입으로 사용하는 varchar 타입의 경우 컬럼명은 _type으로 끝나도록 네이밍한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 참고할 만한 컨벤션&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1)&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://google.github.io/styleguide/javaguide.html&quot;&gt;Google Java Style Guide&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2)&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://naver.github.io/hackday-conventions-java/&quot;&gt;Naver Hackday Java Convention&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;Clean Code(클린 코드) 애자일 소프트웨어 장인 정신 - 로버트 C. 마틴 지음&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;a href=&quot;https://zero-base.co.kr/category_dev_camp/cleancode_1book&quot;&gt;https://zero-base.co.kr/category_dev_camp/cleancode_1book&lt;/a&gt;&lt;/p&gt;</description>
      <category>책 내용 정리/클린코드(clean code)</category>
      <category>클린 코드</category>
      <category>형식 맞추기</category>
      <author>mandykr</author>
      <guid isPermaLink="true">https://mandykr.tistory.com/60</guid>
      <comments>https://mandykr.tistory.com/60#entry60comment</comments>
      <pubDate>Thu, 3 Mar 2022 17:14:14 +0900</pubDate>
    </item>
    <item>
      <title>[JPA] 7. 값 타입</title>
      <link>https://mandykr.tistory.com/58</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;목차&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. JPA의 데이터 타입 분류&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;3. 값 타입과 불변 객체&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;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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. JPA의 데이터 타입 분류&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) 엔티티 타입&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;@Entity로 정의하는 객체&lt;/li&gt;
&lt;li&gt;데이터가 변해도 식별자로 지속해서 추적 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2) 값 타입&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;자바 기본 타입이나 객체&lt;/li&gt;
&lt;li&gt;식별자가 없고 값만 있으므로 변경시 추적 불가&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(1) 기본값 타입&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;자바 기본 타입(int, double)&lt;/li&gt;
&lt;li&gt;래퍼 클래스(Integer, Long)&lt;/li&gt;
&lt;li&gt;String&lt;/li&gt;
&lt;/ul&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;Integer같은 래퍼 클래스나 String 같은 특수한 클래스는 공유 가능한 객체이지만&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;(2) 임베디드 타입(embedded type, 복합 값 타입)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(3) 컬렉션 값 타입(collection value type)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 임베디드 타입(복합 값 타입)&lt;/h3&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;538&quot; data-origin-height=&quot;313&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/clFDxE/btruPazEJu2/YbsjrX0hkUDf3IkBxetuP0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/clFDxE/btruPazEJu2/YbsjrX0hkUDf3IkBxetuP0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/clFDxE/btruPazEJu2/YbsjrX0hkUDf3IkBxetuP0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FclFDxE%2FbtruPazEJu2%2FYbsjrX0hkUDf3IkBxetuP0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;538&quot; height=&quot;313&quot; data-origin-width=&quot;538&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;Member 엔티티의 임베디드 타입으로 Period, Address 클래스를 만들어 매핑하면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각각 클래스의 필드가 하나의 DB 테이블에 컬럼으로 매핑된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;480&quot; data-origin-height=&quot;383&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cMHMES/btruNXm5c6K/bho7VfkbKveGaA0YbN8Qkk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cMHMES/btruNXm5c6K/bho7VfkbKveGaA0YbN8Qkk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cMHMES/btruNXm5c6K/bho7VfkbKveGaA0YbN8Qkk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcMHMES%2FbtruNXm5c6K%2Fbho7VfkbKveGaA0YbN8Qkk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;480&quot; height=&quot;383&quot; data-origin-width=&quot;480&quot; data-origin-height=&quot;383&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;임베디드&amp;nbsp;타입&amp;nbsp;사용법&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;@Embeddable: 값 타입을 정의하는 곳에 표시&lt;/li&gt;
&lt;li&gt;@Embedded: 값 타입을 사용하는 곳에 표시&lt;/li&gt;
&lt;li&gt;기본 생성자 필수&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;임베디드 타입을 포함한 모든 값 타입은, 값 타입을 소유한 엔티 티에 생명주기를 의존함&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1646203417803&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
public class Member extends BaseEntity {
    @Id @GeneratedValue
    @Column(name = &quot;MEMBER_ID&quot;)
    private Long id;

    private String name;

    @Embedded
    private Period period;

    @Embedded
    private Address address;
}

@Embeddable
public class Period {
    private LocalDateTime startDate;
    private LocalDateTime endDate;

    public boolean isWork() {
    	...
        return true;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;279&quot; data-origin-height=&quot;320&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GIVTr/btruUn5TGBT/FFAwvO7eA4k3B9dJd4GnE0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GIVTr/btruUn5TGBT/FFAwvO7eA4k3B9dJd4GnE0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GIVTr/btruUn5TGBT/FFAwvO7eA4k3B9dJd4GnE0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGIVTr%2FbtruUn5TGBT%2FFFAwvO7eA4k3B9dJd4GnE0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;279&quot; height=&quot;320&quot; data-origin-width=&quot;279&quot; data-origin-height=&quot;320&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;임베디드 타입은 엔티티의 값일 뿐이기 때문에 다음과 같이 속성을 정의하는 것이 가능하다.&lt;/p&gt;
&lt;pre id=&quot;code_1646204217644&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Column(name = &quot;ZIPCODE&quot;)
private String zipcode;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;임베디드 타입의 장점&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;재사용&lt;/li&gt;
&lt;li&gt;높은 응집도&lt;/li&gt;
&lt;li&gt;Period.isWork()처럼 해당 값 타입만 사용하는 의미 있는 메소드를&amp;nbsp;만들&amp;nbsp;수&amp;nbsp;있음&lt;/li&gt;
&lt;li&gt;객체와 테이블을 아주 세밀하게(find-grained) 매핑하는 것이 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;임베디드 타입과 연관관계&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;임베디드 타입은 다시 임베디드 타입의 값과 엔티티를 필드로 갖을 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;599&quot; data-origin-height=&quot;191&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dZPcQR/btruTjiosd5/hdXv85cqLAg8jl4oCKKBe1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dZPcQR/btruTjiosd5/hdXv85cqLAg8jl4oCKKBe1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dZPcQR/btruTjiosd5/hdXv85cqLAg8jl4oCKKBe1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdZPcQR%2FbtruTjiosd5%2FhdXv85cqLAg8jl4oCKKBe1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;191&quot; data-origin-width=&quot;599&quot; data-origin-height=&quot;191&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;@AttributeOverride:&amp;nbsp;속성&amp;nbsp;재정의&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;pre id=&quot;code_1646204743062&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Embedded
private Address homeAddress;

@Embedded
@AttributeOverrides({
        @AttributeOverride(name=&quot;city&quot;,
                column=@Column(name = &quot;WORK_CITY&quot;)),
        @AttributeOverride(name=&quot;street&quot;,
                column=@Column(name = &quot;WORK_STREET&quot;)),
        @AttributeOverride(name=&quot;zipcode&quot;,
                column=@Column(name = &quot;WORK_ZIPCODE&quot;))
private Address workAddress;
})&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;283&quot; data-origin-height=&quot;380&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Upr3H/btruThdNv9Z/NzfrYCIYxzuK5pVbvOcrM1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Upr3H/btruThdNv9Z/NzfrYCIYxzuK5pVbvOcrM1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Upr3H/btruThdNv9Z/NzfrYCIYxzuK5pVbvOcrM1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUpr3H%2FbtruThdNv9Z%2FNzfrYCIYxzuK5pVbvOcrM1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;283&quot; height=&quot;380&quot; data-origin-width=&quot;283&quot; data-origin-height=&quot;380&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 값 타입과 불변 객체&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) 값 타입 공유 참조&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;임베디드 타입 같은 값 타입을 여러 엔티티에서 공유하면 부작용(side effect) 발생한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;470&quot; data-origin-height=&quot;182&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/20Jmr/btruNkaLXGP/OVqoS5KonI8KLCYyeP1pWk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/20Jmr/btruNkaLXGP/OVqoS5KonI8KLCYyeP1pWk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/20Jmr/btruNkaLXGP/OVqoS5KonI8KLCYyeP1pWk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F20Jmr%2FbtruNkaLXGP%2FOVqoS5KonI8KLCYyeP1pWk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;182&quot; data-origin-width=&quot;470&quot; data-origin-height=&quot;182&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1646205843114&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Address address = new Address(&quot;city&quot;, &quot;street&quot;, &quot;zipcode&quot;);

Member member1 = new Member(&quot;member1&quot;);
member1.setHomeAddress(address);

Member member2 = new Member(&quot;member2&quot;);
member2.setHomeAddress(address);

em.persist(member1);
em.persist(member2);

member2.getHomeAddress().setCity(&quot;newCity&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회원1과 회원2의 city값이 모두 변경된다.&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;2) 값 타입 복사&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-origin-width=&quot;478&quot; data-origin-height=&quot;182&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BGo7q/btruWj9PYAx/tLG5w8skHeVWHk3kIkkTi0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BGo7q/btruWj9PYAx/tLG5w8skHeVWHk3kIkkTi0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BGo7q/btruWj9PYAx/tLG5w8skHeVWHk3kIkkTi0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBGo7q%2FbtruWj9PYAx%2FtLG5w8skHeVWHk3kIkkTi0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;182&quot; data-origin-width=&quot;478&quot; data-origin-height=&quot;182&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1646206024354&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Address address = new Address(&quot;city&quot;, &quot;street&quot;, &quot;zipcode&quot;);

Member member1 = new Member(&quot;member1&quot;);
member1.setHomeAddress(address);

Address newAddress = new Address(address.getCity(), address.getStreet(), address.getZipcode());
Member member2 = new Member(&quot;member2&quot;);
member2.setHomeAddress(newAddress);

em.persist(member1);
em.persist(member2);

member2.getHomeAddress().setCity(&quot;newCity&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3) 불변 객체&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;객체&amp;nbsp;타입의&amp;nbsp;한계&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;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;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;b&gt;생성자로만 값을 설정하고 수정자(setter)를 만들지 않아야 한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(ex, Integer, String)&lt;/p&gt;
&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_1646206832430&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Address newAddress = new Address(&quot;newCity&quot;, address.getStreet(), address.getZipcode());
member2.setHomeAddress(newAddress);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 값 타입의 비교&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;값 타입은 a.equals(b)를 사용해서 동등성 비교를 해야 한다.&lt;br /&gt;값 타입의 equals() 메소드를 적절하게 재정의(주로 모든 필드 사용)&lt;/p&gt;
&lt;pre id=&quot;code_1646207294652&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    Address address = (Address) o;
    return city.equals(address.city) &amp;amp;&amp;amp; street.equals(address.street) &amp;amp;&amp;amp; zipcode.equals(address.zipcode);
}

@Override
public int hashCode() {
    return Objects.hash(city, street, zipcode);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 값 타입 컬렉션&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;309&quot; data-origin-height=&quot;148&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Qwc0X/btruUokvch1/Lp910H89IY5TedgHlEUWY0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Qwc0X/btruUokvch1/Lp910H89IY5TedgHlEUWY0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Qwc0X/btruUokvch1/Lp910H89IY5TedgHlEUWY0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQwc0X%2FbtruUokvch1%2FLp910H89IY5TedgHlEUWY0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;309&quot; height=&quot;148&quot; data-origin-width=&quot;309&quot; data-origin-height=&quot;148&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;favoriteFoods, addressHistory와 같이 값 타입을 하나 이상 사용하는 경우&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컬렉션을 별도의 테이블 FAVORITE_FOOD, ADDRESS에 저장한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;477&quot; data-origin-height=&quot;237&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dPJYnk/btruUox20Oy/xZFJy1cXkvKRtjkHZKBsQ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dPJYnk/btruUox20Oy/xZFJy1cXkvKRtjkHZKBsQ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dPJYnk/btruUox20Oy/xZFJy1cXkvKRtjkHZKBsQ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdPJYnk%2FbtruUox20Oy%2FxZFJy1cXkvKRtjkHZKBsQ1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;237&quot; data-origin-width=&quot;477&quot; data-origin-height=&quot;237&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;값 타입 컬렉션 사용법&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1646274964572&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Member extends BaseEntity {
    @Id @GeneratedValue
    @Column(name = &quot;MEMBER_ID&quot;)
    private Long id;
    private String name;
    
	// ...
    
    @Embedded
    private Address homeAddress;

    @ElementCollection
    @CollectionTable(name = &quot;FAVORITE_FOOD&quot;, joinColumns =
        @JoinColumn(name = &quot;MEMBER_ID&quot;)
    )
    @Column(name = &quot;FOOD_NAME&quot;)
    private Set&amp;lt;String&amp;gt; favoriteFoods = new HashSet&amp;lt;&amp;gt;();

    @ElementCollection
    @CollectionTable(name = &quot;ADDRESS&quot;, joinColumns =
        @JoinColumn(name = &quot;MEMBER_ID&quot;)
    )
    private List&amp;lt;Address&amp;gt; addressHistory = new ArrayList&amp;lt;&amp;gt;();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;240&quot; data-origin-height=&quot;125&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Eu6b9/btruVLNjfdW/TL8C1FaRchh8uGXV0d3Ip0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Eu6b9/btruVLNjfdW/TL8C1FaRchh8uGXV0d3Ip0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Eu6b9/btruVLNjfdW/TL8C1FaRchh8uGXV0d3Ip0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEu6b9%2FbtruVLNjfdW%2FTL8C1FaRchh8uGXV0d3Ip0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;125&quot; data-origin-width=&quot;240&quot; data-origin-height=&quot;125&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;236&quot; data-origin-height=&quot;81&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/PAi82/btruZPHHBNQ/kPRAQA2FVwuLrFEAnS22l0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/PAi82/btruZPHHBNQ/kPRAQA2FVwuLrFEAnS22l0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PAi82/btruZPHHBNQ/kPRAQA2FVwuLrFEAnS22l0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPAi82%2FbtruZPHHBNQ%2FkPRAQA2FVwuLrFEAnS22l0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;236&quot; height=&quot;81&quot; data-origin-width=&quot;236&quot; data-origin-height=&quot;81&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;277&quot; data-origin-height=&quot;313&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3FBHQ/btruWiYrx44/tgRoY1Xg5hH1LSXmKcnWgk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3FBHQ/btruWiYrx44/tgRoY1Xg5hH1LSXmKcnWgk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3FBHQ/btruWiYrx44/tgRoY1Xg5hH1LSXmKcnWgk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3FBHQ%2FbtruWiYrx44%2FtgRoY1Xg5hH1LSXmKcnWgk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;313&quot; data-origin-width=&quot;277&quot; data-origin-height=&quot;313&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;&lt;b&gt;값 타입 컬렉션 저장&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1646275051998&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Member member = new Member(&quot;member&quot;);
member.setHomeAddress(new Address(&quot;homeCity&quot;, &quot;street&quot;, &quot;zipcode&quot;));

member.getFavoriteFoods().add(&quot;치킨&quot;);
member.getFavoriteFoods().add(&quot;족발&quot;);
member.getFavoriteFoods().add(&quot;피자&quot;);

member.getAddressHistory().add(new Address(&quot;oldCity1&quot;, &quot;street&quot;, &quot;zipcode&quot;));
member.getAddressHistory().add(new Address(&quot;oldCity2&quot;, &quot;street&quot;, &quot;zipcode&quot;));

em.persist(member);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;favoriteFoods, addressHistory가 별도의 테이블로 관리되지만&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히 엔티티의 값 타입이기 때문에 Member와 &lt;b&gt;생명주기가 동일&lt;/b&gt;하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;값 타입 컬렉션은 영속성 전이(Cascade) + 고아 객체 제거 기능을 필수로 가진다고 볼 수 있다.&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;지연 로딩&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;favoriteFoods, addressHistory는 사용하는 시점에 조회한다.&lt;/p&gt;
&lt;pre id=&quot;code_1646275869685&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Member findMember = em.find(Member.class, member.getId());

Set&amp;lt;String&amp;gt; favoriteFoods = findMember.getFavoriteFoods();
for (String favoriteFood : favoriteFoods) {
    System.out.println(&quot;favoriteFood = &quot; + favoriteFood);
}

List&amp;lt;Address&amp;gt; addressHistory = findMember.getAddressHistory();
for (Address address : addressHistory) {
    System.out.println(&quot;address = &quot; + address);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;410&quot; data-origin-height=&quot;461&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rwwVS/btruNjDtghU/ewl6nSrLqO1H8Se2uUeLx0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rwwVS/btruNjDtghU/ewl6nSrLqO1H8Se2uUeLx0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rwwVS/btruNjDtghU/ewl6nSrLqO1H8Se2uUeLx0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrwwVS%2FbtruNjDtghU%2Fewl6nSrLqO1H8Se2uUeLx0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;410&quot; height=&quot;461&quot; data-origin-width=&quot;410&quot; data-origin-height=&quot;461&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&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;370&quot; data-origin-height=&quot;149&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bnxFPH/btruWiYwftD/PWztRPzNNzGDBnkRz9vdm1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bnxFPH/btruWiYwftD/PWztRPzNNzGDBnkRz9vdm1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bnxFPH/btruWiYwftD/PWztRPzNNzGDBnkRz9vdm1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbnxFPH%2FbtruWiYwftD%2FPWztRPzNNzGDBnkRz9vdm1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;370&quot; height=&quot;149&quot; data-origin-width=&quot;370&quot; data-origin-height=&quot;149&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&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;367&quot; data-origin-height=&quot;189&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ID3O2/btruNikikdN/iLjcKkPdrMm7O9H4hADkZK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ID3O2/btruNikikdN/iLjcKkPdrMm7O9H4hADkZK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ID3O2/btruNikikdN/iLjcKkPdrMm7O9H4hADkZK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FID3O2%2FbtruNikikdN%2FiLjcKkPdrMm7O9H4hADkZK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;367&quot; height=&quot;189&quot; data-origin-width=&quot;367&quot; data-origin-height=&quot;189&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;값 타입 컬렉션 수정&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;String 타입&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1646276637341&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Member findMember = em.find(Member.class, member.getId());
findMember.getFavoriteFoods().remove(&quot;치킨&quot;);
findMember.getFavoriteFoods().add(&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;342&quot; data-origin-height=&quot;281&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zzFuy/btruRLN4Q8h/wW6sxzDWETloSSggDl8F0K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zzFuy/btruRLN4Q8h/wW6sxzDWETloSSggDl8F0K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zzFuy/btruRLN4Q8h/wW6sxzDWETloSSggDl8F0K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzzFuy%2FbtruRLN4Q8h%2FwW6sxzDWETloSSggDl8F0K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;281&quot; data-origin-width=&quot;342&quot; data-origin-height=&quot;281&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB 테이블에서 delete와 insert 가 실행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Object 타입&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컬렉션에서 remove는 equals()로 동작하기 때문에 &lt;span style=&quot;letter-spacing: 0px;&quot;&gt;Address 클래스에 equals(), hashCode()를 재정의 해야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1646276737669&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 저장
member.getAddressHistory().add(new Address(&quot;oldCity1&quot;, &quot;street&quot;, &quot;zipcode&quot;));
member.getAddressHistory().add(new Address(&quot;oldCity2&quot;, &quot;street&quot;, &quot;zipcode&quot;));

// 수정
Member findMember = em.find(Member.class, member.getId());
findMember.getAddressHistory().remove(new Address(&quot;oldCity1&quot;, &quot;street&quot;, &quot;zipcode&quot;));
findMember.getAddressHistory().add(new Address(&quot;newCity1&quot;, &quot;street&quot;, &quot;zipcode&quot;));&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;350&quot; data-origin-height=&quot;407&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cg8ZQI/btruW9UsfHx/ytZtZxuS27ook3L7ZA6HAk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cg8ZQI/btruW9UsfHx/ytZtZxuS27ook3L7ZA6HAk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cg8ZQI/btruW9UsfHx/ytZtZxuS27ook3L7ZA6HAk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcg8ZQI%2FbtruW9UsfHx%2FytZtZxuS27ook3L7ZA6HAk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;407&quot; data-origin-width=&quot;350&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주인 엔티티와 연관된 &lt;b&gt;모든 데이터를 삭제하고, 값 타입 컬렉션에 있는 현재 값을 모두 다시&amp;nbsp;저장&lt;/b&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;p data-ke-size=&quot;size16&quot;&gt;값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 기본키를 구성해야 한다. (null 입력X, 중복 저장X)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;값&amp;nbsp;타입&amp;nbsp;컬렉션&amp;nbsp;대안&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에서는 상황에 따라 값 타입 컬렉션 대신에 &lt;b&gt;일대다 관계를 고려&lt;/b&gt;해야 한다.&lt;br /&gt;일대다 관계를 위한 엔티티를 만들고, 여기에서 값 타입을 사용.&lt;br /&gt;영속성 전이(Cascade) + 고아 객체 제거를 사용해서 값 타입 컬렉션 처럼 사용.&lt;/p&gt;
&lt;pre id=&quot;code_1646278903471&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Member Class
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = &quot;MEMBER_ID&quot;)
private List&amp;lt;AddressEntity&amp;gt; addressHistory = new ArrayList&amp;lt;&amp;gt;();&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1646278945183&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
@Table(name = &quot;ADDRESS&quot;)
public class AddressEntity {
    @Id @GeneratedValue
    private Long id;
    private Address address;
    
    public boolean isSame(AddressEntity addressEntity) {
        return this.address.equals(addressEntity.getAddress());
    }
    
    /...
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1646278762593&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;AddressEntity findAddress = addressHistory.stream()
        .filter(a -&amp;gt; a.isSame(oldAddress))
        .findFirst()
        .orElseThrow(IllegalArgumentException::new);
findAddress.setAddress(newAddress.getAddress()); // 편의상 setter 사용&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;a href=&quot;https://www.inflearn.com/course/ORM-JPA-Basic&quot;&gt;https://www.inflearn.com/course/ORM-JPA-Basic&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;자바 ORM 표준 JPA 프로그래밍 - 기본편(김영한)&lt;/p&gt;</description>
      <category>ORM/JPA</category>
      <category>attributeoverride</category>
      <category>Embedded</category>
      <category>값타입</category>
      <author>mandykr</author>
      <guid isPermaLink="true">https://mandykr.tistory.com/58</guid>
      <comments>https://mandykr.tistory.com/58#entry58comment</comments>
      <pubDate>Wed, 2 Mar 2022 16:50:19 +0900</pubDate>
    </item>
    <item>
      <title>[ATDD] 4. 테스트 기반 문서화</title>
      <link>https://mandykr.tistory.com/57</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;목차&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. ATDD + 문서화&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;3. Spring Rest Docs 프로세스&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. ATDD + 문서 자동화&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/mandykr?tab=repositories&quot;&gt;전체 코드 확인&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. ATDD + 문서화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #737373;&quot;&gt;내부 개발자든 외부 파트너든 &lt;span style=&quot;background-color: #ffffff; color: #737373;&quot;&gt;새로운 사용자를 온보딩하는 데 소요되는 시간을 줄여줄 뿐만 아니라 &lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #737373;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #737373;&quot;&gt;API에 대한 이해와 활용도를 높이기 위해선 API의 정보를 문서화 하는 작업이 필요하다.&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;ATDD + TDD cycle 로 개발하면서 어느 시점에 문서화 작업을 진행할 수 있을까?&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;626&quot; data-origin-height=&quot;433&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bOcKVf/btrtwHZHgm3/JQVQgSVHJ4Y7oBqkTYyDiK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bOcKVf/btrtwHZHgm3/JQVQgSVHJ4Y7oBqkTYyDiK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bOcKVf/btrtwHZHgm3/JQVQgSVHJ4Y7oBqkTYyDiK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbOcKVf%2FbtrtwHZHgm3%2FJQVQgSVHJ4Y7oBqkTYyDiK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;433&quot; data-origin-width=&quot;626&quot; data-origin-height=&quot;433&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;개발 전 문서화 장점&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;백엔드 개발자간 병렬 작업&lt;/li&gt;
&lt;li&gt;백엔드 &amp;amp; 프론트엔드 개발자간 병렬 작업&lt;/li&gt;
&lt;li&gt;커뮤니케이션 비용을 줄일 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 문서 자동화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문서를 코드레벨에서 관리할 수 있도록 하여&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문서와 기능에 대한 동기화 작업이 없어지기 때문에 두 개의 관리 포인트를 하나로 줄여준다.&lt;/p&gt;
&lt;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;Swagger&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;Swagger는 API call하여 테스트하는 기능에 특화&lt;/li&gt;
&lt;li&gt;Swagger는 기능이 많은 반면에 Spring Rest Docs는 단순히 문서임&lt;/li&gt;
&lt;li&gt;Swagger&amp;nbsp;사용&amp;nbsp;시&amp;nbsp;불필요한&amp;nbsp;프로덕션&amp;nbsp;코드&amp;nbsp;오염이&amp;nbsp;발염&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Spring Rest Docs&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;테스트 코드에 설정(및 작성)하여 프로덕션 코드에 영향이 적음&lt;/li&gt;
&lt;li&gt;Swagger의 api call 기능은 intellij(local)에서 Http Request하는 기능을 사용하여&amp;nbsp;대체함&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. Spring Rest Docs 프로세스&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;520&quot; data-origin-height=&quot;244&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vbHAx/btrtBN5LUbz/IbZLo1aYrtl5Tehflsh7P1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vbHAx/btrtBN5LUbz/IbZLo1aYrtl5Tehflsh7P1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vbHAx/btrtBN5LUbz/IbZLo1aYrtl5Tehflsh7P1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvbHAx%2FbtrtBN5LUbz%2FIbZLo1aYrtl5Tehflsh7P1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;244&quot; data-origin-width=&quot;520&quot; data-origin-height=&quot;244&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) Test&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;테스트 작성 과정에서 문서화에 필요한 요청, 응답 Snippet 정보를 작성해 세팅한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;517&quot; data-origin-height=&quot;123&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2y3Qu/btrtAB5C4DC/nVgK0KcNiKkQ0MzMrJuWw1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2y3Qu/btrtAB5C4DC/nVgK0KcNiKkQ0MzMrJuWw1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2y3Qu/btrtAB5C4DC/nVgK0KcNiKkQ0MzMrJuWw1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2y3Qu%2FbtrtAB5C4DC%2FnVgK0KcNiKkQ0MzMrJuWw1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;517&quot; height=&quot;123&quot; data-origin-width=&quot;517&quot; data-origin-height=&quot;123&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;489&quot; data-origin-height=&quot;234&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rLPSt/btrtvqR2slS/LYItS67EIjzId2n6wnL2bk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rLPSt/btrtvqR2slS/LYItS67EIjzId2n6wnL2bk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rLPSt/btrtvqR2slS/LYItS67EIjzId2n6wnL2bk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrLPSt%2FbtrtvqR2slS%2FLYItS67EIjzId2n6wnL2bk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;489&quot; height=&quot;234&quot; data-origin-width=&quot;489&quot; data-origin-height=&quot;234&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;h4 data-ke-size=&quot;size20&quot;&gt;2) Spring Rest Docs&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트를 활용해(테스트 실행) 문서화에 사용될 코드 조각(Snippet)을 만든다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;160&quot; data-origin-height=&quot;185&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/p5670/btrtzb7c5R4/zkl7ow9W3BjGUj2DUXfxRK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/p5670/btrtzb7c5R4/zkl7ow9W3BjGUj2DUXfxRK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/p5670/btrtzb7c5R4/zkl7ow9W3BjGUj2DUXfxRK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fp5670%2Fbtrtzb7c5R4%2Fzkl7ow9W3BjGUj2DUXfxRK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;160&quot; height=&quot;185&quot; data-origin-width=&quot;160&quot; data-origin-height=&quot;185&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;1031&quot; data-origin-height=&quot;275&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cQIVrE/btrtwLanijm/t1lWY8Yo40PlO5jKwMMcfK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cQIVrE/btrtwLanijm/t1lWY8Yo40PlO5jKwMMcfK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cQIVrE/btrtwLanijm/t1lWY8Yo40PlO5jKwMMcfK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcQIVrE%2FbtrtwLanijm%2Ft1lWY8Yo40PlO5jKwMMcfK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1031&quot; height=&quot;275&quot; data-origin-width=&quot;1031&quot; data-origin-height=&quot;275&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;h4 data-ke-size=&quot;size20&quot;&gt;3) 문서 템플릿&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Adoc 문서 템플릿 파일에 Snipet 들을 가져와 필요한 위치에 include 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;172&quot; data-origin-height=&quot;83&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ltzIW/btrAR7iyn3U/wZoItKCV0rqAq6p012E8yk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ltzIW/btrAR7iyn3U/wZoItKCV0rqAq6p012E8yk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ltzIW/btrAR7iyn3U/wZoItKCV0rqAq6p012E8yk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FltzIW%2FbtrAR7iyn3U%2FwZoItKCV0rqAq6p012E8yk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;172&quot; height=&quot;83&quot; data-origin-width=&quot;172&quot; data-origin-height=&quot;83&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;1013&quot; data-origin-height=&quot;482&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bZ8ISo/btrtvqYQdm5/llyfNqLnstsD0ttQxVLfmK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bZ8ISo/btrtvqYQdm5/llyfNqLnstsD0ttQxVLfmK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bZ8ISo/btrtvqYQdm5/llyfNqLnstsD0ttQxVLfmK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbZ8ISo%2FbtrtvqYQdm5%2FllyfNqLnstsD0ttQxVLfmK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1013&quot; height=&quot;482&quot; data-origin-width=&quot;1013&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;h4 data-ke-size=&quot;size20&quot;&gt;4) Asciidoctor&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Asciidoctor를 실행해 Adoc 파일을 활용하여 html 문서를 만든다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;406&quot; data-origin-height=&quot;132&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b6sJJz/btrATcxno0K/Ad0duHhOKK8TxR35sHXelK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b6sJJz/btrATcxno0K/Ad0duHhOKK8TxR35sHXelK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b6sJJz/btrATcxno0K/Ad0duHhOKK8TxR35sHXelK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb6sJJz%2FbtrATcxno0K%2FAd0duHhOKK8TxR35sHXelK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;406&quot; height=&quot;132&quot; data-origin-width=&quot;406&quot; data-origin-height=&quot;132&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;161&quot; data-origin-height=&quot;104&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cQHn5d/btrtBO4IHZB/qQAZrzN74Uj2rKBPQZiSR1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cQHn5d/btrtBO4IHZB/qQAZrzN74Uj2rKBPQZiSR1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cQHn5d/btrtBO4IHZB/qQAZrzN74Uj2rKBPQZiSR1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcQHn5d%2FbtrtBO4IHZB%2FqQAZrzN74Uj2rKBPQZiSR1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;161&quot; height=&quot;104&quot; data-origin-width=&quot;161&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;gradle 설정 등 전체 코드 확인 &lt;a href=&quot;https://github.com/mandykr/atdd-subway-fare&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;github&lt;/a&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. ATDD + 문서 자동화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;623&quot; data-origin-height=&quot;518&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BAKVc/btrtADPQIR6/EenxIEelJcT0Wkf4qeOJw0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BAKVc/btrtADPQIR6/EenxIEelJcT0Wkf4qeOJw0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BAKVc/btrtADPQIR6/EenxIEelJcT0Wkf4qeOJw0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBAKVc%2FbtrtADPQIR6%2FEenxIEelJcT0Wkf4qeOJw0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;623&quot; height=&quot;518&quot; data-origin-width=&quot;623&quot; data-origin-height=&quot;518&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;개발 순서&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) 인수 테스트 만들기&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;3) 응답값이 있는 경우 응답값 생성 로직은 서비스로 위임&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4) 위임한 부분(서비스)에 대한 stubbing 하고 문서화 테스트 만들기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5) TDD 로 기능 구현&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&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;size14&quot;&gt;nextstep -&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://edu.nextstep.camp/c/R89PYi5H&quot;&gt;ATDD와&amp;nbsp;함께&amp;nbsp;클린&amp;nbsp;API로&amp;nbsp;가는&amp;nbsp;길&lt;/a&gt;&lt;/p&gt;</description>
      <category>Test/ATDD</category>
      <category>ATDD</category>
      <category>spring rest docs</category>
      <author>mandykr</author>
      <guid isPermaLink="true">https://mandykr.tistory.com/57</guid>
      <comments>https://mandykr.tistory.com/57#entry57comment</comments>
      <pubDate>Thu, 17 Feb 2022 16:07:43 +0900</pubDate>
    </item>
    <item>
      <title>[ATDD] 3. ATDD 기반 리팩터링</title>
      <link>https://mandykr.tistory.com/56</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #555555;&quot;&gt;목차&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #555555;&quot;&gt;1. 인수&amp;nbsp;테스트&amp;nbsp;통합&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #555555;&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;&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;a href=&quot;https://github.com/mandykr?tab=repositories&quot;&gt;전체 코드 확인&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #555555;&quot;&gt;1. 인수 테스트 통합&lt;/span&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #555555;&quot;&gt;1) 인수 테스트 리팩터링&lt;/span&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;442&quot; data-origin-height=&quot;475&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dStm1M/btrtb0yS3NQ/iJjGSzkfYDo7Kxxup3LXc1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dStm1M/btrtb0yS3NQ/iJjGSzkfYDo7Kxxup3LXc1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dStm1M/btrtb0yS3NQ/iJjGSzkfYDo7Kxxup3LXc1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdStm1M%2Fbtrtb0yS3NQ%2FiJjGSzkfYDo7Kxxup3LXc1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;442&quot; height=&quot;475&quot; data-origin-width=&quot;442&quot; data-origin-height=&quot;475&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: #555555;&quot;&gt;인수 테스트를 다음 세가지 방법으로 리팩터링 하면서 활용해 볼 수 있다.&lt;/span&gt;&lt;span style=&quot;color: #555555;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;인수 테스트 리팩터링 : 메소드 분리, 스텝 분리 &lt;a href=&quot;https://mandykr.tistory.com/53&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;참고&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;단위 테스트로 분리 : 비지니스 규칙에 대한 부분을 단위 테스트로 분리&lt;/li&gt;
&lt;li&gt;인수 테스트 통합 : CRUD 각각의 인수 테스트를 하나로 통합&lt;/li&gt;
&lt;/ul&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;span style=&quot;color: #555555;&quot;&gt;2) 인수 테스트의 검증 범위&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #555555;&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;336&quot; data-origin-height=&quot;177&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cApVT7/btrtmUYrrHr/7BQ3DWpkDC32QY9k9GKM11/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cApVT7/btrtmUYrrHr/7BQ3DWpkDC32QY9k9GKM11/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cApVT7/btrtmUYrrHr/7BQ3DWpkDC32QY9k9GKM11/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcApVT7%2FbtrtmUYrrHr%2F7BQ3DWpkDC32QY9k9GKM11%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;336&quot; height=&quot;177&quot; data-origin-width=&quot;336&quot; data-origin-height=&quot;177&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;365&quot; data-origin-height=&quot;293&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ldHGg/btrteifErat/1iIwcpIX9wFNdCavGE3Fx0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ldHGg/btrteifErat/1iIwcpIX9wFNdCavGE3Fx0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ldHGg/btrteifErat/1iIwcpIX9wFNdCavGE3Fx0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FldHGg%2FbtrteifErat%2F1iIwcpIX9wFNdCavGE3Fx0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;365&quot; height=&quot;293&quot; data-origin-width=&quot;365&quot; data-origin-height=&quot;293&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;given/when/then&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 data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #555555;&quot;&gt;(생성에 대한 인수 테스트에서 조회에 대한 검증이 포함되기 때문에 조회에 대한 인수 테스트와 함께 조회 &lt;span style=&quot;background-color: #f6e199;&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #555555;&quot;&gt;3) 인수 테스트 통합&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #555555;&quot;&gt;생성, 조회 등 CRUD에 대한 각각의 인수 테스트를 하나로 통합하는 방식으로 리팩터링을 진행한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #555555;&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 style=&quot;color: #555555;&quot;&gt;인수 테스트 통합 예시&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;313&quot; data-origin-height=&quot;359&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bQE1RE/btruryuYAK9/jy1ovmuKmh3whvRsZOvpsK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bQE1RE/btruryuYAK9/jy1ovmuKmh3whvRsZOvpsK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bQE1RE/btruryuYAK9/jy1ovmuKmh3whvRsZOvpsK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbQE1RE%2FbtruryuYAK9%2Fjy1ovmuKmh3whvRsZOvpsK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;313&quot; height=&quot;359&quot; data-origin-width=&quot;313&quot; data-origin-height=&quot;359&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;&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;size18&quot;&gt;&lt;b&gt;장점&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;api 단위가 아닌 &lt;b&gt;사용자 관점&lt;/b&gt;의 기능과 플로우를 검증 대상으로 설정할 수 있다. (Journy&amp;nbsp;Test&amp;nbsp;or&amp;nbsp;Story&amp;nbsp;Test)&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: #555555;&quot;&gt;스텝의 중복을 효과적으로 제거할 수 있다.&lt;/span&gt;&lt;span style=&quot;color: #555555;&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;span style=&quot;color: #555555;&quot;&gt;방법&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #555555;&quot;&gt;(1) 기존 처럼 따로 만든 다음 하나로 통합하기&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #555555;&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #555555;&quot;&gt;2. 레거시 리팩터링(인수 테스트를 이용한)&lt;/span&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #555555;&quot;&gt;1) 인수 테스트가 있는 경우&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #555555;&quot;&gt;인수 테스트가 있으면 레거시를 리팩터링 할 때 인수테스트가 성공하는 시점으로 돌아갈 수 있기 때문에&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #555555;&quot;&gt;기능 변경 혹은 리팩터링을 하더라도 마음껏 할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #555555;&quot;&gt;2) 인수 테스트가 없는 경우&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #555555;&quot;&gt;&lt;a href=&quot;https://www.2e.co.kr/news/articleView.html?idxno=203903&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;스트랭글러패턴&lt;/a&gt; 참고&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #555555;&quot;&gt;인수 테스트를 먼저 만들고 시작&lt;/span&gt;&lt;span style=&quot;color: #555555;&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;span style=&quot;color: #555555;&quot;&gt;인수 테스트 다음 스텝&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;인수 테스트를 성공시키기 위한 단위 테스트 작성&lt;/li&gt;
&lt;li&gt;단위 테스트를 성공시기기 위한 기능 구현 (or 리팩터링)&lt;/li&gt;
&lt;li&gt;단위 테스트 성공 후 리팩터링&lt;/li&gt;
&lt;li&gt;그리고 반복&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;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;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;새로운 테스트 만들기(ATDD + TDD)&lt;/li&gt;
&lt;li&gt;기능 구현하기&lt;/li&gt;
&lt;li&gt;기존 테스트, 프로덕션 코드 제거&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;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;기존 코드와 신규 코드가 혼재되는 기간을 짧게 가져가도록 해야함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&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;size14&quot;&gt;nextstep -&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://edu.nextstep.camp/c/R89PYi5H&quot;&gt;ATDD와&amp;nbsp;함께&amp;nbsp;클린&amp;nbsp;API로&amp;nbsp;가는&amp;nbsp;길&lt;/a&gt;&lt;/p&gt;</description>
      <category>Test/ATDD</category>
      <category>ATDD</category>
      <category>인수테스트 통합</category>
      <author>mandykr</author>
      <guid isPermaLink="true">https://mandykr.tistory.com/56</guid>
      <comments>https://mandykr.tistory.com/56#entry56comment</comments>
      <pubDate>Tue, 15 Feb 2022 12:11:40 +0900</pubDate>
    </item>
    <item>
      <title>[ATDD] 2. ATDD + TDD</title>
      <link>https://mandykr.tistory.com/55</link>
      <description>&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;2. 협력 객체&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 통합과 고립(Sociable and Solitary)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. Test Double&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5. Classist TDD vs Mockist TDD&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;6.&lt;span&gt;&amp;nbsp;&lt;/span&gt;ATDD&amp;nbsp;+&amp;nbsp;TDD&amp;nbsp;Cycle&lt;/p&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;size14&quot;&gt;TDD, ATDD 란?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://mandykr.tistory.com/53&quot;&gt;ATDD(Acceptance Test Driven Development)&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;a href=&quot;https://github.com/mandykr?tab=repositories&quot;&gt;전체 코드 확인&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 단위 테스트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;단위 테스트란?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단위는 클래스, 메소드 등 정의하기 나름이지만 단위 테스트의 목적은&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&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;h3 data-ke-size=&quot;size23&quot;&gt;2. 협력 객체&lt;/h3&gt;
&lt;pre id=&quot;code_1644201647598&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
@Transactional
public class LineService {
    private final LineRepository lineRepository;

    public LineService(LineRepository lineRepository) {
        this.lineRepository = lineRepository;
    }
    
    public LineResponse saveLine(LineRequest request) {
        Line line = lineRepository.save(new Line(request.getName(), request.getColor()));
        return LineResponse.of(line);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Service 계층의 메소드에 대한 단위 테스트를 작성할 때 Repository 객체에 의존하고 있으면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Repository 객체가 단위 테스트의 협력 객체가 된다.&lt;/p&gt;
&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;협력 객체가 없는 경우는 비교적 테스트가 쉽지만,&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;h3 data-ke-size=&quot;size23&quot;&gt;3. 통합과 고립(Sociable and Solitary)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;협력 객체의 사용 방식에 따라 다음 두 가지 단위 테스트로 나눌 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) 통합 : 협력 객체를 실제 객체를 사용해 단위 테스트를 작성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp;- 장점 : 협력 객체의 상세 구현을 알 필요가 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &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) 고립 : 협력 객체를 Mock(가짜)로 사용해 단위 테스트를 작성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp;- 장점 : 외부 요인(협력 객체)로 부터 격리되어 테스트 대상의 검증에 집중할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp;- 단점 :&lt;span&gt;&amp;nbsp;&lt;/span&gt;테스트가 협력 객체의 상세 구현을 알아야 함.&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. Test Double&lt;/h3&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;541&quot; data-origin-height=&quot;197&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/p7Jgj/btrsC9vHQJp/HFm5AVT0qZ1IX0aYCADeI0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/p7Jgj/btrsC9vHQJp/HFm5AVT0qZ1IX0aYCADeI0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/p7Jgj/btrsC9vHQJp/HFm5AVT0qZ1IX0aYCADeI0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fp7Jgj%2FbtrsC9vHQJp%2FHFm5AVT0qZ1IX0aYCADeI0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;197&quot; data-origin-width=&quot;541&quot; data-origin-height=&quot;197&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Stub&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 alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;474&quot; data-origin-height=&quot;440&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ddLvA6/btrsJLgCKO1/X0ZazrbMKmHO4ESShMfq30/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ddLvA6/btrsJLgCKO1/X0ZazrbMKmHO4ESShMfq30/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ddLvA6/btrsJLgCKO1/X0ZazrbMKmHO4ESShMfq30/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FddLvA6%2FbtrsJLgCKO1%2FX0ZazrbMKmHO4ESShMfq30%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;474&quot; height=&quot;440&quot; data-origin-width=&quot;474&quot; data-origin-height=&quot;440&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Fake&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 alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;540&quot; data-origin-height=&quot;430&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rmlGt/btrsC9JkvAh/Q2v6JLZzkJEE7icfmQU8YK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rmlGt/btrsC9JkvAh/Q2v6JLZzkJEE7icfmQU8YK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rmlGt/btrsC9JkvAh/Q2v6JLZzkJEE7icfmQU8YK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrmlGt%2FbtrsC9JkvAh%2FQ2v6JLZzkJEE7icfmQU8YK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;540&quot; height=&quot;430&quot; data-origin-width=&quot;540&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Mock&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;Mock 객체가 만들어 지면 특정 메소드가 몇 번 호출 되었는지를 카운트해 검증 하는데 사용한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;459&quot; data-origin-height=&quot;322&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bHPjoI/btrsHIdnWRy/NKwinKqKfXzS2Ew6p39MRk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bHPjoI/btrsHIdnWRy/NKwinKqKfXzS2Ew6p39MRk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bHPjoI/btrsHIdnWRy/NKwinKqKfXzS2Ew6p39MRk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbHPjoI%2FbtrsHIdnWRy%2FNKwinKqKfXzS2Ew6p39MRk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;322&quot; data-origin-width=&quot;459&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Stubbing &lt;/b&gt;: Stub 객체를 정의하는 행위&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) Mockito 활용&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1128&quot; data-origin-height=&quot;645&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b7EwC5/btrsxEDAmb5/21wgKKfSlniZWuJepXtE51/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b7EwC5/btrsxEDAmb5/21wgKKfSlniZWuJepXtE51/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b7EwC5/btrsxEDAmb5/21wgKKfSlniZWuJepXtE51/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb7EwC5%2FbtrsxEDAmb5%2F21wgKKfSlniZWuJepXtE51%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;373&quot; data-origin-width=&quot;1128&quot; data-origin-height=&quot;645&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2) MockitoExtension 활용&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1055&quot; data-origin-height=&quot;708&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GZrx8/btrszmoV7BF/iCbQra2mGDIStHr7uRy5VK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GZrx8/btrszmoV7BF/iCbQra2mGDIStHr7uRy5VK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GZrx8/btrszmoV7BF/iCbQra2mGDIStHr7uRy5VK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGZrx8%2FbtrszmoV7BF%2FiCbQra2mGDIStHr7uRy5VK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;663&quot; height=&quot;445&quot; data-origin-width=&quot;1055&quot; data-origin-height=&quot;708&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3) Spring을&amp;nbsp;활용한&amp;nbsp;Stubbing&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring 컨테이너를 띄우고 실제 객체를 Mock Bean으로 변경해 주입한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1036&quot; data-origin-height=&quot;697&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HcffW/btrsxEwLo3G/ULCcMyZ7Ii3IPe7YocxXVK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HcffW/btrsxEwLo3G/ULCcMyZ7Ii3IPe7YocxXVK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HcffW/btrsxEwLo3G/ULCcMyZ7Ii3IPe7YocxXVK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHcffW%2FbtrsxEwLo3G%2FULCcMyZ7Ii3IPe7YocxXVK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;683&quot; height=&quot;460&quot; data-origin-width=&quot;1036&quot; data-origin-height=&quot;697&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. Classist TDD vs Mockist TDD&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단위 테스트를 바라보는 두 관점 Classist와 Mockist.&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;1) Classist TDD&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;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;도메인 설계가 충분히 이루어진 다음 진행 가능&lt;/li&gt;
&lt;li&gt;TDD 사이클을 이어나가기가 상대적으로 어려움&lt;/li&gt;
&lt;li&gt;프로덕션 코드에 덜 의존적인 테스트가 작성됨&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;TDD 진행 방식 : Inside-Out&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;h4 data-ke-size=&quot;size20&quot;&gt;2) Mockist TDD&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 대상과 협력 객체의 격리&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;도메인에 대한 이해도가 높지 않은 상태에서 진행이 가능&lt;/li&gt;
&lt;li&gt;상대적으로 프로덕션 코드에 의존적인 테스트가 작성됨 (깨지기 쉬운 테스트)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;TDD 진행 방식 : Outside-In&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(1) 의존성이 큰 상위 레벨(Controller)부터 시작&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(2) 테스트 더블을 활용하여 테스트 대상이 의존하는 협력 객체(Service)의 예상 결과를 정의&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(3) 다음 사이클로 테스트 더블로 미리 정의한 협력 객체를 테스트 대상으로 함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. ATDD&amp;nbsp;+&amp;nbsp;TDD&amp;nbsp;Cycle&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;626&quot; data-origin-height=&quot;433&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/r2YtH/btrszlwXPyp/LejVYLWRBKAq5W7UbX6XZ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/r2YtH/btrszlwXPyp/LejVYLWRBKAq5W7UbX6XZ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/r2YtH/btrszlwXPyp/LejVYLWRBKAq5W7UbX6XZ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fr2YtH%2FbtrszlwXPyp%2FLejVYLWRBKAq5W7UbX6XZ1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;506&quot; height=&quot;350&quot; data-origin-width=&quot;626&quot; data-origin-height=&quot;433&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;1) 인수 테스트 작성을 통해 요구사항과 기능 전반에 대한 이해를 선행&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;3) 도메인에 대한 이해가 생길 때 까지 Outside-In TDD 를 진행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4) 도메인에 대한 이해가 생기면 도메인부터 차근차근 Inside-Out TDD로 기능 구현&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;출처&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;nextstep -&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://edu.nextstep.camp/c/R89PYi5H&quot;&gt;ATDD와&amp;nbsp;함께&amp;nbsp;클린&amp;nbsp;API로&amp;nbsp;가는&amp;nbsp;길&lt;/a&gt;&lt;/p&gt;</description>
      <category>Test/ATDD</category>
      <category>ATDD</category>
      <category>Classist</category>
      <category>Mockist</category>
      <category>TDD</category>
      <author>mandykr</author>
      <guid isPermaLink="true">https://mandykr.tistory.com/55</guid>
      <comments>https://mandykr.tistory.com/55#entry55comment</comments>
      <pubDate>Mon, 7 Feb 2022 13:53:06 +0900</pubDate>
    </item>
    <item>
      <title>[ATDD] 1. ATDD(Acceptance Test Driven Development)</title>
      <link>https://mandykr.tistory.com/53</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;목차&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. TDD(Test Driven Development) : 테스트 주도 개발&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. ATDD(Acceptance Test Driven Development) : 인수 테스트 주도 개발&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 단위 테스트 vs 통합 테스트 vs 인수 테스트&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. ATDD 학습 방법&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/mandykr?tab=repositories&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;h3 data-ke-size=&quot;size23&quot;&gt;1. TDD(Test Driven Development) : 테스트 주도 개발&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;751&quot; data-origin-height=&quot;529&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/q4J6E/btrrd6IgDXn/S0y1N1dSECoWGpFkrFd1v0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/q4J6E/btrrd6IgDXn/S0y1N1dSECoWGpFkrFd1v0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/q4J6E/btrrd6IgDXn/S0y1N1dSECoWGpFkrFd1v0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fq4J6E%2Fbtrrd6IgDXn%2FS0y1N1dSECoWGpFkrFd1v0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;538&quot; height=&quot;379&quot; data-origin-width=&quot;751&quot; data-origin-height=&quot;529&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;2) 테스트를 성공시키기 위한 최소한의 프로덕션 코드 작성&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;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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. ATDD(Acceptance Test Driven Development) : 인수 테스트 주도 개발&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;789&quot; data-origin-height=&quot;302&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cnnQ28/btrrjB07nIR/BP33eybxQaDqFSSrwdPMsK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cnnQ28/btrrjB07nIR/BP33eybxQaDqFSSrwdPMsK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cnnQ28/btrrjB07nIR/BP33eybxQaDqFSSrwdPMsK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcnnQ28%2FbtrrjB07nIR%2FBP33eybxQaDqFSSrwdPMsK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;789&quot; height=&quot;302&quot; data-origin-width=&quot;789&quot; data-origin-height=&quot;302&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TDD와 ATDD는 요구사항을 명세하고 기능을 구현하는 개발 방식은 동일하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만, 요구사항의 형태가 다른데 ATDD에서는 시나리오 형태의 요구사항을 작성한다.&lt;/p&gt;
&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;인수 테스트(Acceptance Test)&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;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;인수&amp;nbsp;테스트&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;765&quot; data-origin-height=&quot;200&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cUgUe6/btrrhSvIMA3/Agpu4yLqKF6HzLT5kygkU1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cUgUe6/btrrhSvIMA3/Agpu4yLqKF6HzLT5kygkU1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cUgUe6/btrrhSvIMA3/Agpu4yLqKF6HzLT5kygkU1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcUgUe6%2FbtrrhSvIMA3%2FAgpu4yLqKF6HzLT5kygkU1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;765&quot; height=&quot;200&quot; data-origin-width=&quot;765&quot; data-origin-height=&quot;200&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;1) 인수 조건(요구사항)을 시나리오 형식으로 만든다.&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;3) 프로덕션 코드를 작성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인수 조건 예시&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;390&quot; data-origin-height=&quot;577&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tFUy4/btruLtT0aDD/10g2GXhMarK95kGc010LfK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tFUy4/btruLtT0aDD/10g2GXhMarK95kGc010LfK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tFUy4/btruLtT0aDD/10g2GXhMarK95kGc010LfK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtFUy4%2FbtruLtT0aDD%2F10g2GXhMarK95kGc010LfK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;390&quot; height=&quot;577&quot; data-origin-width=&quot;390&quot; data-origin-height=&quot;577&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: #555555;&quot;&gt;CRUD에 대한 각각의 인수 테스트를 하나로 통합하는 방식으로 리팩터링을 진행할 수도 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://mandykr.tistory.com/56&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;b&gt;ATDD의 목적&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 인수 테스트의 가독성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인수 테스트는 사용자의 관점에서 인수 조건을 시나리오 형태로 만들어 검증하는 방식이기 때문에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구체적인 행위 보다는 목적이 뚜렷하게 드러나도록 하는 것이 좋다.&lt;/p&gt;
&lt;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;메소드 분리, CRUD 추상화, BDD도구 사용 등으로 리팩토링&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;2) 스텝 메소드(Given, When, Then)들을 static 선언하여 별도의 클래스로 분리(결국 Cucumber같은 BDD 도구가 필요해짐)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3) BeforEach를 활용해 중복 코드 제거&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;한글 메소드 예시&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;673&quot; data-origin-height=&quot;431&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/buKD7b/btruAi5UuWo/6v3yZORYbeS4FasQW1FdaK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/buKD7b/btruAi5UuWo/6v3yZORYbeS4FasQW1FdaK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/buKD7b/btruAi5UuWo/6v3yZORYbeS4FasQW1FdaK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbuKD7b%2FbtruAi5UuWo%2F6v3yZORYbeS4FasQW1FdaK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;431&quot; data-origin-width=&quot;673&quot; data-origin-height=&quot;431&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스텝 메소드 예시&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;569&quot; data-origin-height=&quot;130&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/PXoBG/btruQslLm3I/4eL8h9KQXebq0VNJdk7VPK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/PXoBG/btruQslLm3I/4eL8h9KQXebq0VNJdk7VPK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PXoBG/btruQslLm3I/4eL8h9KQXebq0VNJdk7VPK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPXoBG%2FbtruQslLm3I%2F4eL8h9KQXebq0VNJdk7VPK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;569&quot; height=&quot;130&quot; data-origin-width=&quot;569&quot; data-origin-height=&quot;130&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 단위 테스트 vs 통합 테스트 vs 인수 테스트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인수 테스트의 개념은 &lt;b&gt;테스트 의도에 따라 정해지는 것&lt;/b&gt;이지 &lt;span style=&quot;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 data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;단위 테스트 : 구현한 부분, 단위를 검증&lt;/li&gt;
&lt;li&gt;통합 테스트 : 각 단위들이 유기적으로 잘 동작하는지 검증&lt;/li&gt;
&lt;li&gt;인수 테스트 : 요구사항을 만족하는지를 검증&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 테스트 비용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인수 테스트는 반드시 확인해야할 인수 조건에 한해 검증하는 것에 집중한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나머지 데이터 검증, 예외 검증 등은 단위 테스트 등으로 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. ATDD 학습 방법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백엔드 개발자의 입장에서 ATDD를 단독으로 실천하기 위해서 인수 테스트의 의도를&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;고객을 프론트엔드 개발자 혹은 API를 활용하는 사람을 대상으로 한 E2E 테스트(API 접점에서 검증)로 정한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API의 Request와 Response 정보 이외 내부 정보는 최대한 가리는 블랙박스 형식의 테스트를 통해 ATDD를 경험해 볼 수 있다.&lt;/p&gt;
&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;내부 구현이나 기술에 의존적이지 않은 테스트로 내부 코드가 변경되더라도 테스트가 쉽게 깨지지 않도록 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;출처&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;nextstep - &lt;a href=&quot;https://edu.nextstep.camp/c/R89PYi5H&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;ATDD와&amp;nbsp;함께&amp;nbsp;클린&amp;nbsp;API로&amp;nbsp;가는&amp;nbsp;길&lt;/a&gt;&lt;/p&gt;</description>
      <category>Test/ATDD</category>
      <category>ATDD</category>
      <category>TDD</category>
      <author>mandykr</author>
      <guid isPermaLink="true">https://mandykr.tistory.com/53</guid>
      <comments>https://mandykr.tistory.com/53#entry53comment</comments>
      <pubDate>Fri, 21 Jan 2022 13:04:55 +0900</pubDate>
    </item>
    <item>
      <title>[클린코드] 4. 주석</title>
      <link>https://mandykr.tistory.com/49</link>
      <description>&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;2. 좋은 주석&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;4. 주석보다 annotation&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5. JavaDoc&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 주석을 최대한 쓰지 말자.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로그래밍 언어를 치밀하게 사용해 의도를 표현할 능력이 있다면, 주석은 거의 필요하지 않다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대부분 주석 없이는 자신을 표현할 방법을 찾지 못해 할 수 없이 주석을 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 &lt;span style=&quot;background-color: #f6e199;&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;span style=&quot;background-color: #f6e199;&quot;&gt;주석을 가능한 줄이도록 꾸준히 노력해야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) 주석은 나쁜 코드를 보완하지 못한다.&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;h4 data-ke-size=&quot;size20&quot;&gt;2) 코드로 의도를 표현하라.&lt;/h4&gt;
&lt;pre id=&quot;code_1642401291826&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 직원에게 복지 혜택을 받을 자격이 있는지 검사한다.
if ((employee.flags &amp;amp; HOURLY_FLAG) &amp;amp;&amp;amp; (employee.age &amp;gt; 65))&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1642401316576&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;if (employee.isEligibleForFullBenefits())&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;코드만으로 의도를 분명히 표현할 수 있는 경우라면 주석 대신에 함수를 사용해도 충분하다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 좋은 주석&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) 정보를 제공하는 주석&lt;/h4&gt;
&lt;pre id=&quot;code_1642402208665&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// kk:mm:ss EEE, MMM dd, yyyy 형식이다.
Pattern timeMatcher = Pattern.compile(
        &quot;\\d*:\\d*:\\d* \\w*, \\w* \\d*, \\d*&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;/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;2) 의도를 설명하는 주석&lt;/h4&gt;
&lt;pre id=&quot;code_1642402516004&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 스레드를 대량 생성하는 방법으로 어떻게든 경쟁 조건을 만들려 시도한다.
for (int i = 0; i &amp;lt; 25000; i++) {
    WidgetBuilderThread widgetBuilderThread =
            new WidgetBuilderThread(widgetBuilder, text, parent, failFlag);
    Thread thread = new Thread(widgetBuilderThread);
    thread.start();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3) 중요성을 설명하는 주석&lt;/h4&gt;
&lt;pre id=&quot;code_1642402730513&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 문자열에 시작 공백이 있으면 다른 문자열로 인식되기 때문에
// trim 함수로 공백을 제거해야 한다.
String listItemContent = match.group(3).trim();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4) TODO, FIXME 주석&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;535&quot; data-origin-height=&quot;275&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Ny85A/btrqQes134L/GQui38EKl59Ygm7wT6QOhk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Ny85A/btrqQes134L/GQui38EKl59Ygm7wT6QOhk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Ny85A/btrqQes134L/GQui38EKl59Ygm7wT6QOhk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNy85A%2FbtrqQes134L%2FGQui38EKl59Ygm7wT6QOhk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;535&quot; height=&quot;275&quot; data-origin-width=&quot;535&quot; data-origin-height=&quot;275&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;TODO : 지금은 해결하지 않지만 나중에 해야할 일을 기술&lt;/li&gt;
&lt;li&gt;FIXME : 문제가 있지만 당장 수정할 필요는 없을 때 기술&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IDE에서 하이라이팅 되고 별도의 윈도우에서 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 나쁜 주석&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잘못된 정보를 제공하는, 중복 되는 주석은 나쁜 주석이기 떄문에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반듯이 필요한 주석만을 공들여 작성할 필요가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) 의무적으로 다는 주석&lt;/h4&gt;
&lt;pre id=&quot;code_1642405917932&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/**
 *
 * @param title CD 제목
 * @param author CD 저자
 * @param tracks CD 트랙 숫자
 * @param durationInMinutes CD 길이(단위: 분)
 */
public void addCD(String title, String author,
                  int tracks, int durationInMinutes) {
    CD cd = new CD();
    cd.title = title;
    cd.author = author;
    cd.tracks = tracks;
    cd.duration = duration;
    cdList.add(cd);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 함수에 Javadocs를 달거나 모든 변수에 주석을 달면&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;h3 data-ke-size=&quot;size23&quot;&gt;4. 주석보다 annotation&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어노테이션을 사용해서 Class, Method, Field에 대한 정보를 보여줄 수 있고&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;@Deprecated : 컴파일러가 warning을 발생시킨다. 필드나 메소드가 곧 없어짐을 의미하기도 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #555555;&quot;&gt;@NotThreadSafe : Thread Safe하지 않읂을 나타낸다.&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: #555555;&quot;&gt;&lt;span style=&quot;color: #555555;&quot;&gt;@Immutable,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;@Nullable 등 java.lang.annotation 패키지의 어노테이션들을 사용하면&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #555555;&quot;&gt;간단하면서 명료하게 주석보다 큰 효과를 얻을 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. JavaDoc&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java 코드에서 API 문서를 HTML 형식으로 생성해 주는 도구&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IDE에서 Reader Mode로 주석 기호를 삭제하고 텍스트만 확인할 수 있다.&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;1) Class level&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;738&quot; data-origin-height=&quot;283&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/6EntZ/btrvl8G6uxr/6wncUXqz8q2cUXPMJ0W4X1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6EntZ/btrvl8G6uxr/6wncUXqz8q2cUXPMJ0W4X1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6EntZ/btrvl8G6uxr/6wncUXqz8q2cUXPMJ0W4X1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F6EntZ%2Fbtrvl8G6uxr%2F6wncUXqz8q2cUXPMJ0W4X1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;283&quot; data-origin-width=&quot;738&quot; data-origin-height=&quot;283&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;h4 data-ke-size=&quot;size20&quot;&gt;2) Method level&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;790&quot; data-origin-height=&quot;369&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CjP91/btru8IprykJ/03y77KEGyJsbwCBaikH4A0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CjP91/btru8IprykJ/03y77KEGyJsbwCBaikH4A0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CjP91/btru8IprykJ/03y77KEGyJsbwCBaikH4A0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCjP91%2Fbtru8IprykJ%2F03y77KEGyJsbwCBaikH4A0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;369&quot; data-origin-width=&quot;790&quot; data-origin-height=&quot;369&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;h4 data-ke-size=&quot;size20&quot;&gt;3) Field level&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;807&quot; data-origin-height=&quot;176&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GHGHc/btrvkGjMoTS/p3audjhmsqZDCrMLXsBNAK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GHGHc/btrvkGjMoTS/p3audjhmsqZDCrMLXsBNAK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GHGHc/btrvkGjMoTS/p3audjhmsqZDCrMLXsBNAK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGHGHc%2FbtrvkGjMoTS%2Fp3audjhmsqZDCrMLXsBNAK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;807&quot; height=&quot;176&quot; data-origin-width=&quot;807&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;출처&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;Clean Code(클린 코드) 애자일 소프트웨어 장인 정신 - 로버트 C. 마틴 지음&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;a href=&quot;https://zero-base.co.kr/category_dev_camp/cleancode_1book&quot;&gt;https://zero-base.co.kr/category_dev_camp/cleancode_1book&lt;/a&gt;&lt;/p&gt;</description>
      <category>책 내용 정리/클린코드(clean code)</category>
      <category>clean code</category>
      <category>주석</category>
      <author>mandykr</author>
      <guid isPermaLink="true">https://mandykr.tistory.com/49</guid>
      <comments>https://mandykr.tistory.com/49#entry49comment</comments>
      <pubDate>Mon, 17 Jan 2022 17:15:34 +0900</pubDate>
    </item>
    <item>
      <title>[java 8] 3-2. Stream (리덕션, 병렬 스트림)</title>
      <link>https://mandykr.tistory.com/47</link>
      <description>&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;2. 병렬 스트림&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 리덕션&lt;/h3&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;658&quot; data-origin-height=&quot;299&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sxcfS/btrpcPHknC4/rYYXUvE8Ka1APieAAh9u1k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sxcfS/btrpcPHknC4/rYYXUvE8Ka1APieAAh9u1k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sxcfS/btrpcPHknC4/rYYXUvE8Ka1APieAAh9u1k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsxcfS%2FbtrpcPHknC4%2FrYYXUvE8Ka1APieAAh9u1k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;299&quot; data-origin-width=&quot;658&quot; data-origin-height=&quot;299&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1640746718522&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;T reduce(T identity, BinaryOperator&amp;lt;T&amp;gt; accumulator);&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1640746838206&quot; class=&quot;java&quot; style=&quot;display: block; overflow: auto; padding: 20px; color: #383a42; background: #f8f8f8; font-size: 14px; font-family: 'SF Mono', Menlo, Consolas, Monaco, monospace; border: 1px solid #ebebeb; line-height: 1.71; margin: 20px auto 0px; cursor: default; z-index: 1; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface BinaryOperator&amp;lt;T&amp;gt; extends BiFunction&amp;lt;T,T,T&amp;gt; {
// T apply(T t1, T t2)
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1640746884285&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;List&amp;lt;String&amp;gt; ls = Arrays.asList(&quot;Box&quot;, &quot;Simple&quot;, &quot;Complex&quot;, &quot;Robot&quot;);

BinaryOperator&amp;lt;String&amp;gt; lc = 
    (s1, s2) -&amp;gt; { 
       if(s1.length() &amp;gt; s2.length())
           return s1;
       else 
           return s2;                   
    };

String str = ls.stream()
              .reduce(&quot;&quot;, lc);

System.out.println(str); // Complex&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 개의 String 형 인자를 받아 문자열의 길이가 긴 데이터를 리턴하는 BinaryOperator 를 만들어&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;reduce() 메소드의 두 번째 인자로 전달한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;reduce() 연산의 결과 값으로 문자열의 길이가 가장 긴 Complex 가 반환된다.&lt;/p&gt;
&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;reduce() 메소드의 첫 번째 인자 T&amp;nbsp;identity&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;identity 로 전달되는 값은 reduce()의 첫번째 연산에 사용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 코드에서 reduce(&quot;1234567&quot;, lc) 와 같이 인자를 전달하면 스트림을 통해 전달되는 값은&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;1234567&quot;, &quot;Box&quot;, &quot;Simple&quot;, &quot;Complex&quot;, &quot;Robot&quot; 이고 연산의 최종 결과 값으로 문자열의 길이가 가장 긴 &quot;1234567&quot; 가 반환된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1646628963857&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Optional&amp;lt;T&amp;gt; reduce(BinaryOperator&amp;lt;T&amp;gt; accumulator);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;identity&lt;span&gt; 없이&amp;nbsp;&lt;/span&gt;&lt;/span&gt;BinaryOperator만&lt;span&gt; 전달받아 reduce() 연산을 할 수도 있다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1646629073219&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;positives.stream().reduce(Positive::add).get().getNumber();&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1646629103403&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public Positive add(Positive positive) {
    return new Positive(this.number + positive.number);
}&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;h3 data-ke-size=&quot;size23&quot;&gt;2. 병렬 스트림&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;병렬 처리: 한 개의 작업을 여러개의 core 가 진행하도록 한다.&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;484&quot; data-origin-height=&quot;218&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bvoJmO/btro7hxWNWS/dS7EnY0UMpJI3AncLkFriK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bvoJmO/btro7hxWNWS/dS7EnY0UMpJI3AncLkFriK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bvoJmO/btro7hxWNWS/dS7EnY0UMpJI3AncLkFriK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbvoJmO%2Fbtro7hxWNWS%2FdS7EnY0UMpJI3AncLkFriK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;484&quot; height=&quot;218&quot; data-origin-width=&quot;484&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;parallelStream() 메소드를 사용해서 간단하게 병렬 처리 연산 작업을 할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1640748014835&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;List&amp;lt;String&amp;gt; ls = Arrays.asList(&quot;Box&quot;, &quot;Simple&quot;, &quot;Complex&quot;, &quot;Robot&quot;);

BinaryOperator&amp;lt;String&amp;gt; lc = 
    (s1, s2) -&amp;gt; { 
       if(s1.length() &amp;gt; s2.length())
           return s1;
       else 
           return s2;                   
    };

String str = ls.parallelStream()
              .reduce(&quot;&quot;, lc);

System.out.println(str);&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;일반 스트림의 parallel() 메소드를 호출해서 일반 스트림을 병렬 스트림으로 변경할 수도 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1640749477320&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;String str = ss.parallel()
              .reduce(&quot;&quot;, lc);&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;/p&gt;
&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;size14&quot;&gt;&lt;a href=&quot;https://cafe.naver.com/cstudyjava&quot;&gt;https://cafe.naver.com/cstudyjava&lt;/a&gt;&amp;nbsp;윤성우의 열혈 java&lt;/p&gt;</description>
      <category>Java/Java 8</category>
      <category>parallelStream</category>
      <category>reduce</category>
      <author>mandykr</author>
      <guid isPermaLink="true">https://mandykr.tistory.com/47</guid>
      <comments>https://mandykr.tistory.com/47#entry47comment</comments>
      <pubDate>Wed, 29 Dec 2021 12:21:30 +0900</pubDate>
    </item>
    <item>
      <title>[java 8] 3-1. Stream (생성, 중간 연산, 최종 연산)</title>
      <link>https://mandykr.tistory.com/46</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;목차&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. Strean&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;3. 중간 연산(중개 오퍼래이션)&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. Strean&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스트림을 통해 배열, 컬렉션 인스턴스의 데이터들을 가공할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스트림 생성, 중간 연산, 최종 연산 순으로 진행되고 최종 연산이 없으면 아무 결과를 얻지 못한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 스트림 생성&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) 배열&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Arrays 클래스의 static 메소드 사용&lt;/p&gt;
&lt;pre id=&quot;code_1640743239014&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public static IntStream stream(int[] array, int startInclusive, int endExclusive)&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1640743355817&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;int[] ints = {1, 2, 3, 4, 5};
// 인덱스 1 ~ 3 데이터를 대상으로 스트림 생성
Arrays.stream(ints, 1, 4)
        .forEach(System.out::println); // 2, 3, 4&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2) 컬렉션 인스턴스&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Collection&amp;lt;E&amp;gt; 인터페이스의 디폴트 메소드 사용&lt;/p&gt;
&lt;pre id=&quot;code_1640743548236&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;default Stream&amp;lt;E&amp;gt; stream()&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1640743599133&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;List&amp;lt;String&amp;gt; list = Arrays.asList(&quot;Toy&quot;, &quot;Robot&quot;, &quot;Box&quot;);
list.stream()
        .forEach(System.out::println);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3) of() 메소드&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인자로 데이터를 전달하면서 스트림을 생성한다.&lt;/p&gt;
&lt;pre id=&quot;code_1640748452792&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public static&amp;lt;T&amp;gt; Stream&amp;lt;T&amp;gt; of(T t)
public static&amp;lt;T&amp;gt; Stream&amp;lt;T&amp;gt; of(T... values)
public static&amp;lt;T&amp;gt; Stream&amp;lt;T&amp;gt; ofNullable(T t)&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1640748397322&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ex 1
Stream.of(11, 22, 33, 44)
    .forEach(n -&amp;gt; System.out.print(n + &quot;\t&quot;));
System.out.println();

// ex 2
Stream.of(&quot;So Simple&quot;)
    .forEach(s -&amp;gt; System.out.print(s + &quot;\t&quot;));
System.out.println();

// ex 3
List&amp;lt;String&amp;gt; sl = Arrays.asList(&quot;Toy&quot;, &quot;Robot&quot;, &quot;Box&quot;);
Stream.of(sl)
    .forEach(w -&amp;gt; System.out.print(w + &quot;\t&quot;));
System.out.println();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ex3 은 컬렉션 인스턴스의 원소를 통해 스트림을 생성한 것이 아닌&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인스턴스의 참조변수인 sl 을 원소로 갖는 스트림을 생성한 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 다수의 컬렉션 인스턴스의 참조값을 원소로 갖는 스트림도 생성할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4) IntStream, DoubleStream, LongStream&lt;/h4&gt;
&lt;pre id=&quot;code_1640749218884&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public static IntStream of(int... values)
public static DoubleStream of(double... values)
public static LongStream of(long... values)

public static IntStream range(int startInclusive, int endExclusive)
public static IntStream rangeClosed(int startInclusive, int endInclusive)
public static LongStream range(long startInclusive, final long endExclusive)
public static LongStream rangeClosed(long startInclusive, final long endInclusive)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;5) 스트림 연결&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Stream,&amp;nbsp;IntStream, DoubleStream, LongStream 의 concat 메소드를 활용해 스트림을 연결할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1640749840870&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public static &amp;lt;T&amp;gt; Stream&amp;lt;T&amp;gt; concat(Stream&amp;lt;? extends T&amp;gt; a, Stream&amp;lt;? extends T&amp;gt; b)
Stream.concat(s1, s2);&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;h3 data-ke-size=&quot;size23&quot;&gt;3. 중간 연산(중개 오퍼래이션) : 스트림을 리턴한다.&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) map&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;(1) map (1:1 매핑)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원소 한 개의 매핑 리턴 값으로 한 개의 데이터가 반환된다.&lt;/p&gt;
&lt;pre id=&quot;code_1640744440115&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;List&amp;lt;String&amp;gt; list = Arrays.asList(&quot;Toy&quot;, &quot;Robot&quot;, &quot;Box&quot;);
list.stream()
        .map(s -&amp;gt; s.length())
        .forEach(System.out::println);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 코드에서 데이터를 매핑할 때 map 메소드는 다음과 같이 Function 타입의 인자를 받는다.&lt;/p&gt;
&lt;pre id=&quot;code_1640744425384&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;R&amp;gt; Stream&amp;lt;R&amp;gt; map(Function&amp;lt;? super T, ? extends R&amp;gt; mapper);&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1640744570561&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface Function&amp;lt;T, R&amp;gt; {
    R apply(T t);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Function 인터페이스는 리턴 타입을 R 타입으로 제네릭을 사용하는데 제네릭으로 기본 자료형 타입을 사용할 수 없기 때문에 R을 통해 전달되는 과정에서 int는 Integer로 auto boxing과 다시 auto unboxing 이 진행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 경우에는 리턴 타입이 int로 선언되어 있는 mapToInt 메소드를 사용해 불필요한 boxing, unboxing을 줄일 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1640744917723&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;IntStream mapToInt(ToIntFunction&amp;lt;? super T&amp;gt; mapper);&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1640744933500&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface ToIntFunction&amp;lt;T&amp;gt; {
    int applyAsInt(T value);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;mapToInt, mapToLong, mapToDouble&lt;/p&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) flatMap (1:N 매핑)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원소 한 개의 매핑 리턴 값으로 N개의 데이터가 반환된다.&lt;/p&gt;
&lt;pre id=&quot;code_1640750331399&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Stream&amp;lt;String&amp;gt; ss1 = Stream.of(&quot;MY_AGE&quot;, &quot;YOUR_LIFE&quot;);

Stream&amp;lt;String&amp;gt; ss2 = ss1.flatMap(s -&amp;gt; Arrays.stream(s.split(&quot;_&quot;)));
ss2.forEach(s -&amp;gt; System.out.print(s + &quot;\t&quot;)); // MY	AGE	YOUR	LIFE&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;flatMapToInt, flatMapToLong, flatMapToDouble&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;2) sorted : 정렬&lt;/h4&gt;
&lt;pre id=&quot;code_1640752865319&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Stream&amp;lt;T&amp;gt; sorted(Comparator&amp;lt;? super T&amp;gt; comparator);&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1640752989537&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Stream.of(&quot;Box&quot;, &quot;Apple&quot;, &quot;Rabbit&quot;)
    .sorted((s1, s2) -&amp;gt; s1.length() - s2.length())
    .forEach(s -&amp;gt; System.out.print(s + '\t'));&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3) looping&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반복 연산 forEach 는 최종 연산이지만 peek 은 중간 연산이기 때문에 최종 연산이 없으면 연산을 진행하지 않는다.&lt;/p&gt;
&lt;pre id=&quot;code_1640753297356&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;IntStream.of(1, 3, 5)
    .peek(d -&amp;gt; System.out.print(d + &quot;\t&quot;)); // 출력하지 않음

IntStream.of(5, 3, 1)
    .peek(d -&amp;gt; System.out.print(d + &quot;\t&quot;)) // 5	3	1
    .sum();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 최종 연산(종료 오퍼레이션) : 스트림을 리턴하지 않는다.&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) sum, count, average, min, max&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;average 는 OptionalDouble 타입을&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;min, max 는 OptionalInt 타입을 리턴한다.&lt;/p&gt;
&lt;pre id=&quot;code_1640754558221&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;int sum = IntStream.of(1, 3, 5, 7, 9)
                  .sum();
                  
long cnt = IntStream.of(1, 3, 5, 7, 9)
                  .count();
                  
IntStream.of(1, 3, 5, 7, 9)
        .average()
        .ifPresent(av -&amp;gt; System.out.println(&quot;avg = &quot; + av));
        
IntStream.of(1, 3, 5, 7, 9)
        .min()
        .ifPresent(mn -&amp;gt; System.out.println(&quot;min = &quot; + mn));
        
IntStream.of(1, 3, 5, 7, 9)
        .max()
        .ifPresent(mx -&amp;gt; System.out.println(&quot;max = &quot; + mx));&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2) forEach&lt;/h4&gt;
&lt;pre id=&quot;code_1640754677762&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;default void forEach(Consumer&amp;lt;? super T&amp;gt; action)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3) __match()&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스트림의 데이터가 조건을 어느정도 만족하는지 확인.&lt;/p&gt;
&lt;pre id=&quot;code_1640754490176&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;boolean b = IntStream.of(1, 2, 3, 4, 5)
                  .allMatch(n -&amp;gt; n%2 == 0);

b = IntStream.of(1, 2, 3, 4, 5)
            .anyMatch(n -&amp;gt; n%2 == 0);

b = IntStream.of(1, 2, 3, 4, 5)
            .noneMatch(n -&amp;gt; n%2 == 0);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4) collect&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;collet() 메소드로 스트림에 있는 데이터를 저장할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1640755346061&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// interface Stream&amp;lt;T&amp;gt;
&amp;lt;R&amp;gt; R collect(Supplier&amp;lt;R&amp;gt; supplier,
        BiConsumer&amp;lt;R, ? super T&amp;gt; accumulator,
        BiConsumer&amp;lt;R, R&amp;gt; combiner);
        
&amp;lt;R, A&amp;gt; R collect(Collector&amp;lt;? super T, A, R&amp;gt; collector);

// interface Collector&amp;lt;T, A, R
public interface Collector&amp;lt;T, A, R&amp;gt; {    
    Collector&amp;lt;T, ?, List&amp;lt;T&amp;gt;&amp;gt; toList() {
        return new Collectors.CollectorImpl&amp;lt;&amp;gt;((Supplier&amp;lt;List&amp;lt;T&amp;gt;&amp;gt;) ArrayList::new, List::add,
                (left, right) -&amp;gt; { left.addAll(right); return left; },
                CH_ID);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1640755656551&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;List&amp;lt;String&amp;gt; ls = ss.filter(s -&amp;gt; s.length() &amp;lt; 5)
                  .collect(
                      () -&amp;gt; new ArrayList&amp;lt;&amp;gt;(),
                      (c, s) -&amp;gt; c.add(s),
                      (lst1, lst2) -&amp;gt; lst1.addAll(lst2));

ss.filter(s -&amp;gt; s.length() &amp;lt; 5)
        .collect(Collectors.toList());&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;collect() 의 첫 번째 인자의 리턴값이 두 번째 인자의 c 가 되고 스트림의 데이터가 s 가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동일한 작업을 Collectors.toList() 메소드를 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;collect() 메소드의 세 번째 인자는 병렬 스트림을 사용할 때 최종적으로 어떻게 취합할지를 결정한다.&lt;/p&gt;
&lt;pre id=&quot;code_1640756271795&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;List&amp;lt;String&amp;gt; ls = ss.parallel()
                  .filter(s -&amp;gt; s.length() &amp;lt; 5)                          
                  .collect(
                      () -&amp;gt; new ArrayList&amp;lt;&amp;gt;(),
                      (c, s) -&amp;gt; c.add(s),
                      (lst1, lst2) -&amp;gt; lst1.addAll(lst2));&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ArrayList 를 만들고 저장하는 작업을 병렬로 처리한 다음 리스트의 addAll() 메소드를 통해 데이터를 취합한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;a href=&quot;https://cafe.naver.com/cstudyjava&quot;&gt;https://cafe.naver.com/cstudyjava&lt;/a&gt;&amp;nbsp;윤성우의 열혈 java&lt;/p&gt;</description>
      <category>Java/Java 8</category>
      <category>maptoint</category>
      <category>Stream</category>
      <author>mandykr</author>
      <guid isPermaLink="true">https://mandykr.tistory.com/46</guid>
      <comments>https://mandykr.tistory.com/46#entry46comment</comments>
      <pubDate>Wed, 29 Dec 2021 11:44:05 +0900</pubDate>
    </item>
    <item>
      <title>[Spring Boot] Spring Boot Slice Test (JUnit 5)</title>
      <link>https://mandykr.tistory.com/45</link>
      <description>&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;2. JUnit 5&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. IntelliJ 에서 JUnit 5를 이용한 테스트&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. @ExtendWith(SpringExtension.class)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5. Spring Boot Slice Test&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;6. 테스트 전략&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 단위 테스트와 통합 테스트&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) 단위테스트&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;br /&gt;따라서 동작하는데 필요한 협력 클래스(외부 라이브러리)의 객체는 mock(가짜) 객체로 만들어 복잡도를 낮추고&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;background-color: #ffffff; color: #4d5156;&quot;&gt;Java mocking framework 중 하나로 Mockito 가 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #4d5156;&quot;&gt;&lt;a href=&quot;https://mandykr.tistory.com/category/Test/Mockito&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Mockito 사용법&lt;/a&gt; 참고&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2) 통합테스트&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단위 테스트에서 모듈 단위로 테스트를 완료하면&amp;nbsp;모든 클래스가 협력 클래스의 실제 객체와 의존관계를 맺고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션이 정상적으로 작동하는지 확인하는 작업이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트의 복잡도가 높아 에러를 찾는 과정이 어려울 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. JUnit 5&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바 애플리케이션을 단위 테스트 할 때 사용하는 프레임워크 중 JUnit 이 있고 최신 버전이 JUnit 5이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JUnit 5는 이전 버전과 다르게 다음과 같은 구조로 이루어져 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JUnit Platform + JUnit Jupiter + JUnit Vintage&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;612&quot; data-origin-height=&quot;290&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/q0XVE/btroC8B0fW4/5tus4yMUaKmGOVLcttwjY1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/q0XVE/btroC8B0fW4/5tus4yMUaKmGOVLcttwjY1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/q0XVE/btroC8B0fW4/5tus4yMUaKmGOVLcttwjY1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fq0XVE%2FbtroC8B0fW4%2F5tus4yMUaKmGOVLcttwjY1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;612&quot; height=&quot;290&quot; data-origin-width=&quot;612&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;b&gt;JUnit Platform&lt;/b&gt;&lt;br /&gt;JUnit&amp;nbsp;Platform은&amp;nbsp;JVM에서&amp;nbsp;테스트&amp;nbsp;프레임워크를&amp;nbsp;실행하는데&amp;nbsp;기초를&amp;nbsp;제공한다.&amp;nbsp;또한&amp;nbsp;TestEngine&amp;nbsp;API를&amp;nbsp;제공해&amp;nbsp;테스트&amp;nbsp;프레임워크를&amp;nbsp;개발할&amp;nbsp;수&amp;nbsp;있다. &lt;br /&gt;&lt;br /&gt;&lt;b&gt;JUnit Jupiter&lt;/b&gt;&lt;br /&gt;JUnit&amp;nbsp;Jupiter는&amp;nbsp;JUnit&amp;nbsp;5에서&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;&lt;b&gt;JUnit Vintage&lt;/b&gt;&lt;br /&gt;JUnit&amp;nbsp;Vintage는&amp;nbsp;하위&amp;nbsp;호환성을&amp;nbsp;위해&amp;nbsp;JUnit3과&amp;nbsp;JUnt4를&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;a href=&quot;https://mandykr.tistory.com/category/Test/JUnit%205&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;JUnit 5 사용법&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;h3 data-ke-size=&quot;size23&quot;&gt;3. IntelliJ 에서 JUnit 5를 이용한 테스트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://start.spring.io/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;spring initializr&lt;/a&gt; 에서 Spring Boot 프로젝트를 만들면 다음과 같이 테스트용 Spring Boot Starter 라이브러리를 dependency로 추가한다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;testImplementation 'org.springframework.boot:spring-boot-starter-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;여기에 JUnit5, Mockito, Hamcrest 등의 패키지가 포함되어 있어 별도로 dependency에 추가하지 않아도 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만든 프로젝트를 JUnit 5 를 지원하는 버전의 IntelliJ 에서 열고 테스트 클래스를 만든다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;823&quot; data-origin-height=&quot;124&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NwL7G/btroEzFeAVg/tq7AOqkGtSMYyS9cWyVlq1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NwL7G/btroEzFeAVg/tq7AOqkGtSMYyS9cWyVlq1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NwL7G/btroEzFeAVg/tq7AOqkGtSMYyS9cWyVlq1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNwL7G%2FbtroEzFeAVg%2Ftq7AOqkGtSMYyS9cWyVlq1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;823&quot; height=&quot;124&quot; data-origin-width=&quot;823&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;테스트 대상 클래스명에서 단축키 Ctrl + Shift + T 를 이용해 만들 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;581&quot; data-origin-height=&quot;352&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DNO5d/btroIUn6YIz/1BPJyQJY55toGwEaIy95ck/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DNO5d/btroIUn6YIz/1BPJyQJY55toGwEaIy95ck/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DNO5d/btroIUn6YIz/1BPJyQJY55toGwEaIy95ck/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDNO5d%2FbtroIUn6YIz%2F1BPJyQJY55toGwEaIy95ck%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;352&quot; data-origin-width=&quot;581&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;b&gt;Testing library&lt;/b&gt; 가 자동으로 JUnit 5로 설정되어 있는 걸 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JUnit 5 는 @Test 어노테이션을 이용해 테스트 메소드인지 확인하기 때문에 다음과 같이 메소드 위에 붙여주고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JUnit Jupiter Assertions 의 api를 이용해 테스트 한다.&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;516&quot; data-origin-height=&quot;143&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Hi4LX/btroITQkBJQ/lbA3JTeliaThKeUHNWpVuK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Hi4LX/btroITQkBJQ/lbA3JTeliaThKeUHNWpVuK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Hi4LX/btroITQkBJQ/lbA3JTeliaThKeUHNWpVuK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHi4LX%2FbtroITQkBJQ%2FlbA3JTeliaThKeUHNWpVuK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;516&quot; height=&quot;143&quot; data-origin-width=&quot;516&quot; data-origin-height=&quot;143&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Assertions static import 단축키는 Alt(cmd) + Enter&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;437&quot; data-origin-height=&quot;209&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bD3fhp/btroFKGnxtm/kvNpocsWsMt6rmOKdkYXw1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bD3fhp/btroFKGnxtm/kvNpocsWsMt6rmOKdkYXw1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bD3fhp/btroFKGnxtm/kvNpocsWsMt6rmOKdkYXw1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbD3fhp%2FbtroFKGnxtm%2FkvNpocsWsMt6rmOKdkYXw1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;209&quot; data-origin-width=&quot;437&quot; data-origin-height=&quot;209&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;test() 메소드를 실행하면 JUnit 이 테스트를 진행한다.&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;621&quot; data-origin-height=&quot;107&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b1znxb/btroEyNfKQh/qE2TPWJkipkkQsdsJaTzB1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b1znxb/btroEyNfKQh/qE2TPWJkipkkQsdsJaTzB1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b1znxb/btroEyNfKQh/qE2TPWJkipkkQsdsJaTzB1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb1znxb%2FbtroEyNfKQh%2FqE2TPWJkipkkQsdsJaTzB1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;107&quot; data-origin-width=&quot;621&quot; data-origin-height=&quot;107&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;h3 data-ke-size=&quot;size23&quot;&gt;4. @ExtendWith(SpringExtension.class)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@ExtendWith는 junit 테스트 컨텍스트 프레임워크의 확장 클래스를 지정할 때 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JUnit 이 테스트를 진행할 때 사용할 확장 기능을 클래스, 메소드 등에 적용할 수 있는 어노테이션이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확장 클래스를 SpringExtension.class로 설정하면 junit이 테스트를 진행하는 중에 Spring이 &lt;b&gt;Applicationcontext&lt;/b&gt; 를 만들고 관리해준다.&lt;/p&gt;
&lt;pre id=&quot;code_1640233784360&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class AppTest {
    @Autowired ApplicationContext context;

    @Test
    void test() {
        assert context != null;

        String[] beans = context.getBeanDefinitionNames();
        System.out.println(&quot;count = &quot; + context.getBeanDefinitionCount());
        Arrays.asList(beans).forEach(b -&amp;gt; System.out.println(&quot;bean = &quot; + b));
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@ExtendWith(SpringExtension.class) 를 붙이지 않으면 ApplicationContext 는 만들어지지 않고 다음과 같이 null 로 확인된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;691&quot; data-origin-height=&quot;112&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HDiRE/btroCxu2bYQ/agbiqdgKtcpcek62UCwS60/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HDiRE/btroCxu2bYQ/agbiqdgKtcpcek62UCwS60/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HDiRE/btroCxu2bYQ/agbiqdgKtcpcek62UCwS60/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHDiRE%2FbtroCxu2bYQ%2FagbiqdgKtcpcek62UCwS60%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;691&quot; height=&quot;112&quot; data-origin-width=&quot;691&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;@ExtendWith(SpringExtension.class) 를 붙이고 확인해보면 ApplicationContext 가 만들어 졌고 8개의 bean을 등록해 관리하는 것을 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;702&quot; data-origin-height=&quot;187&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cbMo7g/btroE8t1eY0/Bj9qtdn8nZU0SOnoRI3Kck/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cbMo7g/btroE8t1eY0/Bj9qtdn8nZU0SOnoRI3Kck/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cbMo7g/btroE8t1eY0/Bj9qtdn8nZU0SOnoRI3Kck/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcbMo7g%2FbtroE8t1eY0%2FBj9qtdn8nZU0SOnoRI3Kck%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;187&quot; data-origin-width=&quot;702&quot; data-origin-height=&quot;187&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;Spring 은 &lt;b&gt;하나의 ApplicationContext를 만들어 테스트 클래스 내 모든 메소드에서 공유&lt;/b&gt;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 ApplicationContext 를 @Autowired로 주입받을 수 있는 이유는 스프링 애플리케이션 컨텍스트를 초기화할 때 자기 자신도 빈으로 등록하기 떄문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. Spring Boot Slice Test&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot는 단위 테스트와 통합 테스트의 중간 쯤에 해당하는 Slice Test를 할 때 어노테이션으로 간편하게 설정할 수 있도록 지원한다. &lt;a href=&quot;https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#test-auto-configuration&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Test Auto-configuration Annotations&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;1) &lt;b&gt;@DataJpaTest&lt;/b&gt; : jpa를 사용하는 repository를 빈으로 만들어 주입해주고 이 외에 @Component 가 붙은 다른 클래스(@Controller, @Service, JPA를 사용하지 않는 @Repository)는 무시한다. 데이터베이스 연결과 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;2) &lt;b&gt;@WebMvcTest&lt;/b&gt; : @Controller, @ControllerAdvice, @JsonComponent, Converter, GenericConverter, Filter 등을 스캔해 웹과 관련해 요청/응답 테스트를 할 때 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@WebMvcTest&lt;span&gt;&amp;nbsp;&lt;/span&gt;어노테이션을 사용해 가짜 요청(MockMvc 사용)을 dispatcherServlet 에게 보내고 응답을 확인하는 테스트를 할 때 @Service, @Repository 어노테이션이 붙은 클래스는 bean생성 대상에서 제외하지만&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;dispatcherServlet, webConfig, requestMappingHandlerMapping, mvcValidator 등 웹과 관련된 bean들을 컨텍스트에 등록하고 서로 조합이 된 상태로 동작하는 테스트이기 때문에 단위테스트 보다는 큰 단위라고 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이렇게 특정 계층을 테스트 하기 위해 필요한 일부 bean 들만 등록하고 사용하기 때문에 통합 테스트에 비해 &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;@SpringBootTest&lt;/b&gt; 는 실제 애플리케이션을 구동하기 때문에 필요한 모든 bean 을 로드한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션의 규모가 클 수록 Slice Test 에서 사용하는 어노테이션과의 속도 차이 또한 클 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;Slice Test 각 어노테이션들은 다음과 같이 @ExtendWith(SpringExtension.class) 를 사용하기 때문에 테스트를 진행할 때 Spring이 ApplicationContext를 만들고 관리한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;413&quot; data-origin-height=&quot;279&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cJQgl2/btroFMdolsU/KlQ1SEp0KVUvKu9eoMoZok/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cJQgl2/btroFMdolsU/KlQ1SEp0KVUvKu9eoMoZok/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cJQgl2/btroFMdolsU/KlQ1SEp0KVUvKu9eoMoZok/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcJQgl2%2FbtroFMdolsU%2FKlQ1SEp0KVUvKu9eoMoZok%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;279&quot; data-origin-width=&quot;413&quot; data-origin-height=&quot;279&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1640238603530&quot; class=&quot;java&quot; style=&quot;display: block; overflow: auto; padding: 20px; color: #383a42; background: #f8f8f8; font-size: 14px; font-family: 'SF Mono', Menlo, Consolas, Monaco, monospace; border: 1px solid #ebebeb; line-height: 1.71; margin: 20px auto 0px; cursor: default; z-index: 1; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@WebMvcTest(SupplementCategoryController.class)
public class AppTest {
    @Autowired ApplicationContext context;

    @Autowired MockMvc mockMvc;
    @MockBean SupplementCategoryService categoryService;
    @Autowired SupplementCategoryController controller = new SupplementCategoryController(categoryService);

    @Test
    void test() {
        assert context != null;

        String[] beans = context.getBeanDefinitionNames();
        System.out.println(&quot;count = &quot; + context.getBeanDefinitionCount());
        Arrays.asList(beans).forEach(b -&amp;gt; System.out.println(&quot;bean = &quot; + b));
    }
}&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;1185&quot; data-origin-height=&quot;321&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cPIh0T/btroHbRxoSH/1Kk9F1fo0znKXx5WFybfB1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cPIh0T/btroHbRxoSH/1Kk9F1fo0znKXx5WFybfB1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cPIh0T/btroHbRxoSH/1Kk9F1fo0znKXx5WFybfB1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcPIh0T%2FbtroHbRxoSH%2F1Kk9F1fo0znKXx5WFybfB1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1185&quot; height=&quot;321&quot; data-origin-width=&quot;1185&quot; data-origin-height=&quot;321&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;Spring Boot Slice Test는 웹과 관련된 테스트를 할 때 필요한 설정을 autoconfigure 패키지들을 import 하는 방식으로 지원하고 있으며 @WebMvcTest 는 다음과 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1271&quot; data-origin-height=&quot;936&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/XIR6e/btroEppu0im/kBUqPiPAzNMJVhdV223FOk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/XIR6e/btroEppu0im/kBUqPiPAzNMJVhdV223FOk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/XIR6e/btroEppu0im/kBUqPiPAzNMJVhdV223FOk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXIR6e%2FbtroEppu0im%2FkBUqPiPAzNMJVhdV223FOk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1271&quot; height=&quot;936&quot; data-origin-width=&quot;1271&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;목록중에 org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration 클래스를 통해&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;package org.springframework.boot.autoconfigure.web.servlet 클래스를 확인해 보면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DispatcherServlet 빈을 스프링 컨테이너에 하는 걸 볼 수 있다.&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;799&quot; data-origin-height=&quot;347&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/k97db/btroJBoCF90/Mm7SFzPAcfnZUlPiXJ3ji1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/k97db/btroJBoCF90/Mm7SFzPAcfnZUlPiXJ3ji1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/k97db/btroJBoCF90/Mm7SFzPAcfnZUlPiXJ3ji1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fk97db%2FbtroJBoCF90%2FMm7SFzPAcfnZUlPiXJ3ji1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;799&quot; height=&quot;347&quot; data-origin-width=&quot;799&quot; data-origin-height=&quot;347&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;888&quot; data-origin-height=&quot;604&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qTQLw/btroDrVBbhI/RHP9NgDpzhp8KZwANGkDVK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qTQLw/btroDrVBbhI/RHP9NgDpzhp8KZwANGkDVK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qTQLw/btroDrVBbhI/RHP9NgDpzhp8KZwANGkDVK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqTQLw%2FbtroDrVBbhI%2FRHP9NgDpzhp8KZwANGkDVK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;888&quot; height=&quot;604&quot; data-origin-width=&quot;888&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;u&gt;Spring boot가 Configuration 관련 어노테이션을 사용해 동작하는 위의 내용은 정확하지 않기 때문에, Spring Boot autoconfiguration을 공부하고 수정할 예정.&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. 테스트 전략&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) null 값 처리를 포함한 도메인 객체 테스트&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2) 가짜 협력자를 활용해서 모든 비즈니스 로직을 검사하는 서비스 계층 테스트&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3) Slice Test&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;&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>Spring Boot</category>
      <category>junit 5</category>
      <category>slice test</category>
      <category>Spring Boot</category>
      <author>mandykr</author>
      <guid isPermaLink="true">https://mandykr.tistory.com/45</guid>
      <comments>https://mandykr.tistory.com/45#entry45comment</comments>
      <pubDate>Thu, 23 Dec 2021 15:25:00 +0900</pubDate>
    </item>
    <item>
      <title>[java 8] 4. Optional</title>
      <link>https://mandykr.tistory.com/44</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;목차&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. Optional&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. Optional 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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. Optional&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) 자바 프로그래밍에서 NullPointerException을 종종 보게 되는 이유&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;null을&amp;nbsp;리턴하니까!&amp;nbsp;&amp;amp;&amp;amp;&amp;nbsp;null&amp;nbsp;체크를&amp;nbsp;깜빡했으니까!&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;br /&gt;2) 메소드에서 작업 중 특별한 상황에서 값을 제대로 리턴할 수 없는 경우 선택할 수 있는 방법&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; - 예외를 던진다. (비싸다, 스택트레이스를 찍어두니까.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; - null을 리턴한다. (비용 문제가 없지만 그 코드를 사용하는 클리어인트 코드가 주의해야 한다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; - (자바 8부터) Optional을 리턴한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;(클라이언트에 코드에게 명시적으로 빈 값일 수도 있다는 걸 알려주고, 빈 값인 경우에 대한 처리를 강제한다.)&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;br /&gt;3) Optional&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오직 값 한 개가 들어있을 수도 없을 수도 있는 컨네이너.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4) 주의할 것&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;파라미터로 넘어오는 Optional이 null일 수 있기 때문에 null 체크를 하지 않으면 NullPointerException&lt;/p&gt;
&lt;pre id=&quot;code_1639789074298&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// setter
public void setProgress(Optional&amp;lt;Progress&amp;gt; progress) {
	progress.ifPresent(p -&amp;gt; this.progress = progress.get());
}

// main
OnlineClass spring_boot = new OnlineClass(1, &quot;spring boot&quot;, true);
spring_boot.setProgress(null); // NullPointerException&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; - Optional을 리턴하는 메소드에서 null을 리턴하지 말자. -&amp;gt; 결국 메소드를 호출하는 코드에서 null 체크를 해야함...(Optional.empty() 사용)&lt;br /&gt;&amp;nbsp; - 프리미티브 타입용 Optional은 따로 있다. OptionalInt, OptionalLong,... &lt;br /&gt;&amp;nbsp; - Collection, Map, Stream, Array, Optional은 Opiontal로 감싸지 말 것.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.&amp;nbsp;Optional API&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) Optional 만들기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Optional.of()&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Optional.ofNullable() : 값이 null 일 수도 있는 경우 사용, null인 경우 Optional.empty()&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Optional.empty()&lt;/p&gt;
&lt;pre id=&quot;code_1639792413763&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public Optional&amp;lt;Progress&amp;gt; getProgress() {
	return Optional.ofNullable(progress);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2) Optional에 값이 있는지 없는지 확인하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;isPresent()&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;isEmpty()&amp;nbsp;(Java&amp;nbsp;11부터&amp;nbsp;제공)&lt;/p&gt;
&lt;pre id=&quot;code_1639792489177&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;boolean present = classOptional.isPresent();
boolean empty = classOptional.isEmpty();&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;br /&gt;3) Optional에 있는 값 가져오기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;get() :&amp;nbsp;비어있는&amp;nbsp;경우&amp;nbsp;NoSuchElementException&amp;nbsp;발생&lt;/p&gt;
&lt;pre id=&quot;code_1639792515588&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;OnlineClass onlineClass = classOptional.get();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4) Optional에 값이 있는 경우 처리&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ifPresent(Consumer) :&amp;nbsp;Optional에 값이 있는 경우에 그 값을 가지고 ~~를 하라.&lt;/p&gt;
&lt;pre id=&quot;code_1639792563804&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;classOptional.ifPresent(oc -&amp;gt; System.out.println(oc.getTitle()));&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;5) Optional에 값이 있으면 가져오고 없는 경우 처리&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(1) orElse(T) : Optional에 값이 있으면 가져오고 없는 경우에 ~~를 리턴하라.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;orElse 의 파라미터가 람다식이나 메소드 참조가 아니기 때문에 값이 없는 경우에도 createNewClass()가 실행이 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1639792597553&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;OnlineClass onlineClass1 = classOptional.orElse(createNewClass());&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) orElseGet(Supplier) : Optional에 값이 있으면 가져오고 없는 경우에 ~~를 하라.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;orElseGet 의 파라미터로 Supplier 타입을 넘겨준다. &lt;br /&gt;값이 없느 경우에만 createNewClass() 가 실행된다.&lt;/p&gt;
&lt;pre id=&quot;code_1639792664689&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;OnlineClass onlineClass2 = classOptional.orElseGet(() -&amp;gt; createNewClass());&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;(3) orElseThrow() :&amp;nbsp;Optional에 값이 있으면 가졍고 없는 경우 에러를 던져라.&lt;/p&gt;
&lt;pre class=&quot;livescript&quot;&gt;&lt;code&gt;OnlineClass onlineClass3 = classOptional.orElseThrow(() -&amp;gt; new IllegalStateException());&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;6) Optional에 들어있는 값 걸러내기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Optional filter(Predicate)&lt;/p&gt;
&lt;pre class=&quot;erlang-repl&quot;&gt;&lt;code&gt;Optional&amp;lt;OnlineClass&amp;gt; filterOptional = classOptional.filter(oc -&amp;gt; !oc.isClosed());&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;7) Optional에 들어있는 값 변환하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Optional map(Function)&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;Optional&amp;lt;Integer&amp;gt; integer = classOptional.map(OnlineClass::getId);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Optional flatMap(Function): Optional 안에 들어있는 인스턴스가 Optional인 경우에 사용하면&amp;nbsp;편리하다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;Optional&amp;lt;Optional&amp;lt;Progress&amp;gt;&amp;gt; mapProgress = classOptional.map(OnlineClass::getProgress);
Optional&amp;lt;Progress&amp;gt; progress = mapProgress.orElse(Optional.empty());

Optional&amp;lt;Progress&amp;gt; flatMapProgress = classOptional.flatMap(OnlineClass::getProgress);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;전체 소스코드&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1639792846530&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public static void main(String[] args) {
    List&amp;lt;OnlineClass&amp;gt; springClasses = new ArrayList&amp;lt;&amp;gt;();
    springClasses.add(new OnlineClass(1, &quot;spring boot&quot;, true));
    springClasses.add(new OnlineClass(2, &quot;spring data jpa&quot;, true));
    springClasses.add(new OnlineClass(3, &quot;spring mvc&quot;, false));
    springClasses.add(new OnlineClass(4, &quot;spring core&quot;, false));
    springClasses.add(new OnlineClass(5, &quot;rest api development&quot;, false));

    // Stream 중계형 오퍼레이션에서 Optional 사용
    Optional&amp;lt;OnlineClass&amp;gt; classOptional = springClasses.stream()
            .filter(oc -&amp;gt; oc.getTitle().startsWith(&quot;spring&quot;))
            .findFirst();

    // 값이 있는지 없는지 확인
    boolean present = classOptional.isPresent();
    System.out.println(present);

    boolean empty = classOptional.isEmpty();
    System.out.println(empty);

    // 꺼내기
    // 비어있는 경우 NoSuchElementException 발생
    OnlineClass onlineClass = classOptional.get();
    System.out.println(onlineClass.getTitle());

    // 값이 있는 경우 처리
    classOptional.ifPresent(oc -&amp;gt; System.out.println(oc.getTitle()));

    // 값이 있는 경우 꺼내고 없는 경우 처리 1
    // orElse 의 파라미터가 람다식이나 메소드 참조가 아니기 때문에
    // 값이 없는 경우에도 createNewClass()가 실행이 된다.
    OnlineClass onlineClass1 = classOptional.orElse(createNewClass());
    System.out.println(onlineClass1.getTitle());

    // 값이 있는 경우 꺼내고 없는 경우 처리 2
    // orElseGet 의 파라미터로 Supplier 타입을 넘겨준다.
    // 값이 없느 경우에만 createNewClass() 가 실행된다.
    OnlineClass onlineClass2 = classOptional.orElseGet(() -&amp;gt; createNewClass());

    // 값이 있는 경우 꺼내고 없는 경우 처리 3
    // 값이 없는 경우 exception 이 발생되도록
    OnlineClass onlineClass3 = classOptional.orElseThrow(() -&amp;gt; new IllegalStateException());

    // 걸러내기
    // 값이 없는 경우 Optional.empty 리턴
    Optional&amp;lt;OnlineClass&amp;gt; filterOptional = classOptional.filter(oc -&amp;gt; !oc.isClosed());
    System.out.println(filterOptional);

    // 변환하기
    // map
    Optional&amp;lt;Integer&amp;gt; integer = classOptional.map(OnlineClass::getId);

    // 변환하기 2
    // Ontinal&amp;lt;Optinal&amp;gt;
    Optional&amp;lt;Optional&amp;lt;Progress&amp;gt;&amp;gt; mapProgress = classOptional.map(OnlineClass::getProgress);
    Optional&amp;lt;Progress&amp;gt; progress = mapProgress.orElse(Optional.empty());

    Optional&amp;lt;Progress&amp;gt; flatMapProgress = classOptional.flatMap(OnlineClass::getProgress);


}

private static OnlineClass createNewClass() {
    return new OnlineClass(10, &quot;New class&quot;, false);
}&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;a href=&quot;https://www.inflearn.com/course/the-java-java8&quot;&gt;https://www.inflearn.com/course/the-java-java8&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;더 자바, Java 8(백기선)&lt;/p&gt;</description>
      <category>Java/Java 8</category>
      <category>java8</category>
      <category>optinal</category>
      <author>mandykr</author>
      <guid isPermaLink="true">https://mandykr.tistory.com/44</guid>
      <comments>https://mandykr.tistory.com/44#entry44comment</comments>
      <pubDate>Sat, 18 Dec 2021 11:02:27 +0900</pubDate>
    </item>
    <item>
      <title>[java 8] 3. Stream</title>
      <link>https://mandykr.tistory.com/43</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;목차&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. Stream&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. Stream 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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. Stream&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) Stream&amp;nbsp;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;sequence of elements supporting sequential and parallel aggregate operations&lt;/li&gt;
&lt;li&gt;데이터를 담고 있는 저장소(컬렉션)가 아니다.&lt;/li&gt;
&lt;li&gt;Funtional in nature, &lt;span style=&quot;background-color: #f6e199;&quot;&gt;스트림이 처리하는 데이터 소스를 변경하지 않는다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;스트림으로 처리하는 데이터는 오직 한번만 처리한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;무제한일 수도 있다. (Short Circuit 메소드를 사용해서 제한할 수 있다.)&lt;/li&gt;
&lt;li&gt;중개 오퍼레이션은 근본적으로 lazy 하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1639729644141&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;List&amp;lt;String&amp;gt; names = new ArrayList&amp;lt;&amp;gt;();
names.add(&quot;mandy&quot;);
names.add(&quot;hongseok&quot;);
names.add(&quot;keesun&quot;);
names.add(&quot;toby&quot;);

// 중계형 오퍼레이션
// 데이터가 모든 오퍼레이션을 한번만 사용하고
// 종료 오퍼레이션이 있어야 실행된다.
names.stream().map(s -&amp;gt; {
    System.out.println(s); // 출력되지 않음
    return s.toUpperCase();
});

System.out.println(&quot;=====================&quot;);

// 종료형 오퍼레이션
List&amp;lt;String&amp;gt; collect = names.stream().map(s -&amp;gt; {
    System.out.println(s);
    return s.toUpperCase();
}).collect(Collectors.toList());
collect.forEach(System.out::println);

System.out.println(&quot;=====================&quot;);

// 원본 데이터를 바꾸지 않는다.
names.forEach((name) -&amp;gt; System.out.println(name));
names.forEach(System.out::println);&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;손쉽게 병렬 처리할 수 있다. (parallelStream)&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1639729762672&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;List&amp;lt;String&amp;gt; names = new ArrayList&amp;lt;&amp;gt;();
names.add(&quot;mandy&quot;);
names.add(&quot;hongseok&quot;);
names.add(&quot;keesun&quot;);
names.add(&quot;toby&quot;);

List&amp;lt;String&amp;gt; collect = names.parallelStream().map(s -&amp;gt; {
    System.out.println(s + &quot; &quot; + Thread.currentThread().getName());
    return s.toUpperCase();
}).collect(Collectors.toList());
collect.forEach(System.out::println);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2) Stream 파이프라인&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;0&amp;nbsp;또는&amp;nbsp;다수의&amp;nbsp;중개&amp;nbsp;오퍼레이션&amp;nbsp;(intermediate&amp;nbsp;operation)과&amp;nbsp;한개의&amp;nbsp;종료&amp;nbsp;오퍼레이션 &lt;br /&gt;(terminal operation)으로 구성한다.&lt;/li&gt;
&lt;li&gt;스트림의 데이터 소스는 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;오직 터미널 오퍼네이션을 실행할 때에만 처리한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1639729621278&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;names.stream().map(s -&amp;gt; {
    System.out.println(s); // 출력되지 않음
    return s.toUpperCase();
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3) 중개 오퍼레이션&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;Stream을 리턴한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;Stateless / Stateful 오퍼레이션으로 더 상세하게 구분할 수도 있다. (대부분은&lt;br /&gt;Stateless지만&amp;nbsp;distinct나&amp;nbsp;sorted&amp;nbsp;처럼&amp;nbsp;이전&amp;nbsp;이전&amp;nbsp;소스&amp;nbsp;데이터를&amp;nbsp;참조해야&amp;nbsp;하는&lt;br /&gt;오퍼레이션은 Stateful 오퍼레이션이다.)&lt;/li&gt;
&lt;li&gt;filter, map, limit, skip, sorted, ...&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4) 종료 오퍼레이션&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;Stream을 리턴하지 않는다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;collect, allMatch, count, forEach, min, max, ...&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. Stream API&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) 걸러내기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;filter(Predicate) &lt;br /&gt;예) 이름이 3글자 이상인 데이터만 새로운 스트림으로&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;br /&gt;2) 변경하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;map(Function) 또는 flatMap(Function) &lt;br /&gt;예) 각각의 Post 인스턴스에서 String title만 새로운 스트림으로 &lt;br /&gt;예) List&amp;lt;Stream&amp;lt;String&amp;gt;&amp;gt;을 String의 스트림으로&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;3) 생성하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;generate(Supplier) 또는 Iterate(T seed, UnaryOperator) &lt;br /&gt;예) 10부터 1씩 증가하는 무제한 숫자 스트림 &lt;br /&gt;예) 랜덤 int 무제한 스트림&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;4) 제한하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;limit(long) 또는 skip(long) &lt;br /&gt;예) 최대 5개의 요소가 담긴 스트림을 리턴한다. &lt;br /&gt;예) 앞에서 3개를 뺀 나머지 스트림을 리턴한다.&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;5) 스트림에 있는 데이터가 특정 조건을 만족하는지 확인&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;anyMatch(), allMatch(), nonMatch() &lt;br /&gt;예) k로 시작하는 문자열이 있는지 확인한다. (true 또는 false를 리턴한다.) &lt;br /&gt;예) 스트림에 있는 모든 값이 10보다 작은지 확인한다.&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;6) 개수 세기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;count() &lt;br /&gt;예) 10보다 큰 수의 개수를 센다.&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;7) 스트림을 데이터 하나로 뭉치기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;reduce(identity, BiFunction), collect(), sum(), max() &lt;br /&gt;예) 모든 숫자 합 구하기 &lt;br /&gt;예) 모든 데이터를 하나의 List 또는 Set에 옮겨 담기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1639729874028&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;List&amp;lt;OnlineClass&amp;gt; springClasses = new ArrayList&amp;lt;&amp;gt;();
springClasses.add(new OnlineClass(1, &quot;spring boot&quot;, true));
springClasses.add(new OnlineClass(2, &quot;spring data jpa&quot;, true));
springClasses.add(new OnlineClass(3, &quot;spring mvc&quot;, false));
springClasses.add(new OnlineClass(4, &quot;spring core&quot;, false));
springClasses.add(new OnlineClass(5, &quot;rest api development&quot;, false));

System.out.println(&quot;spring 으로 시작하는 수업&quot;);
springClasses.stream()
        .filter(oc -&amp;gt; oc.getTitle().startsWith(&quot;spring&quot;))
        .forEach(oc -&amp;gt; System.out.println(oc.getTitle()));

System.out.println(&quot;close 되지 않은 수업&quot;);
springClasses.stream()
        .filter(Predicate.not(OnlineClass::isClosed))
        .forEach(oc -&amp;gt; System.out.println(oc.getTitle()));

System.out.println(&quot;수업 이름만 모아서 스트림 만들기&quot;);
springClasses.stream()
        .map(OnlineClass::getTitle)
        .forEach(System.out::println);

List&amp;lt;OnlineClass&amp;gt; javaClasses = new ArrayList&amp;lt;&amp;gt;();
javaClasses.add(new OnlineClass(6, &quot;The Java, Test&quot;, true));
javaClasses.add(new OnlineClass(7, &quot;The Java, Code manipulation&quot;, true));
javaClasses.add(new OnlineClass(8, &quot;The Java, 8 to 11&quot;, false));

List&amp;lt;List&amp;lt;OnlineClass&amp;gt;&amp;gt; keesunEvents = new ArrayList&amp;lt;&amp;gt;();
keesunEvents.add(springClasses);
keesunEvents.add(javaClasses);

System.out.println(&quot;두 수업 목록에 들어있는 모든 수업 아이디 출력&quot;);
keesunEvents.stream()
        .flatMap(Collection::stream)
        .forEach(oc -&amp;gt; System.out.println(oc.getId()));

System.out.println(&quot;10부터 1씩 증가하는 무제한 스트림 중에서 앞에 10개 빼고 최대 10개 까지만&quot;);
Stream.iterate(10, i -&amp;gt; i +1)
        .skip(10)
        .limit(10)
        .forEach(System.out::println);

System.out.println(&quot;자바 수업 중에 Test가 들어있는 수업이 있는지 확인&quot;);
boolean test = javaClasses.stream()
        .anyMatch(oc -&amp;gt; oc.getTitle().contains(&quot;Test&quot;));
System.out.println(&quot;test = &quot; + test);

System.out.println(&quot;스프링 수업 중에 제목에 spring이 들어간 것만 모아서 List로 만들기&quot;);
List&amp;lt;String&amp;gt; spring = springClasses.stream()
        .map(OnlineClass::getTitle)
        .filter(t -&amp;gt; t.contains(&quot;spring&quot;))
        .collect(Collectors.toList());
spring.forEach(System.out::println);&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;/p&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;size14&quot;&gt;출처&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;a href=&quot;https://www.inflearn.com/course/the-java-java8&quot;&gt;https://www.inflearn.com/course/the-java-java8&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;더 자바, Java 8(백기선)&lt;/p&gt;</description>
      <category>Java/Java 8</category>
      <category>java8</category>
      <category>Stream</category>
      <author>mandykr</author>
      <guid isPermaLink="true">https://mandykr.tistory.com/43</guid>
      <comments>https://mandykr.tistory.com/43#entry43comment</comments>
      <pubDate>Fri, 17 Dec 2021 17:32:51 +0900</pubDate>
    </item>
    <item>
      <title>[java 8] 1-1. 함수형 인터페이스와 람다 (메소드 참조)</title>
      <link>https://mandykr.tistory.com/42</link>
      <description>&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;2. 인스턴스 메소드 참조 1&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;4. 생성자 참조&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5. static 메소드 참조&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 메소드 참조&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메소드 참조를 통해 기본적으로 람다식보다 조금 더 코드를 단순하게 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일부 람다식을 메소드 참조로 대신하게 할 수 있다.&lt;/p&gt;
&lt;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;background-color: #f6e199;&quot;&gt;인자를 그대로 메소드 참조에 사용&lt;/span&gt;하겠다는 약속을 기반으로&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;람다식 대신 메소드 참조를 사용할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1639719717353&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public static void main(String[] args) {
    List&amp;lt;Integer&amp;gt; list = Arrays.asList(1, 3, 5, 7, 9);
    list = new ArrayList&amp;lt;&amp;gt;(list);

    // 익명 클래스
    Consumer&amp;lt;List&amp;lt;Integer&amp;gt;&amp;gt; consumer1 = new Consumer&amp;lt;List&amp;lt;Integer&amp;gt;&amp;gt;() {
        @Override
        public void accept(List&amp;lt;Integer&amp;gt; n) {
            Collections.reverse(n);
        }
    };

    // 람다
    Consumer&amp;lt;List&amp;lt;Integer&amp;gt;&amp;gt; consumer2 = n -&amp;gt; Collections.reverse(n);

    // 메소드 참조
    // accept() 의 인자를 Collections.reverse() 의 인자로 사용하도록 약속
    Consumer&amp;lt;List&amp;lt;Integer&amp;gt;&amp;gt; consumer3 = Collections::reverse;

//    consumer1.accept(list);
//    consumer2.accept(list);
    consumer3.accept(list);

    list.forEach(System.out::println);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 인스턴스 메소드 참조 1&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) effectively final&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;따라서, 해당 지역변수를 사실상 상수(effectively final)일 때만 람다식, 메소드 참조가 가능하도록 제한을 둔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 값으로 변경되면 컴파일 에러 발생.&lt;/p&gt;
&lt;pre id=&quot;code_1639721353559&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class JustSort {
    public void sort(List&amp;lt;?&amp;gt; lst) {
        Collections.reverse(lst);
    }
}

class ArrangeList3 {
    public static void main(String[] args) {
        List&amp;lt;Integer&amp;gt; ls = Arrays.asList(1, 3, 5, 7, 9);
        ls = new ArrayList&amp;lt;&amp;gt;(ls);
        JustSort js = new JustSort(); // js는 effectively final

        Consumer&amp;lt;List&amp;lt;Integer&amp;gt;&amp;gt; c = e -&amp;gt; js.sort(e);
//        Consumer&amp;lt;List&amp;lt;Integer&amp;gt;&amp;gt; c = js::sort;

        c.accept(ls);
        System.out.println(ls);
        
//        js = new JustSort(); // 람다, 메소드 참조 작성 불가능
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2) forEach 메소드&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Iterable 인터페이스의 &lt;a href=&quot;https://mandykr.tistory.com/21&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;default 메소드&lt;/a&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1639721773098&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface Iterable&amp;lt;T&amp;gt; {
    default void forEach(Consumer&amp;lt;? super T&amp;gt; action) {
        Objects.requireNonNull(action);
        for (T t : this) {
            action.accept(t);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;forEach 메소드의 this는 default 메소드이기 때문에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 Iterable 인터페이스 하위 인터페이스, 클래스의 인스턴스 (아래 코드에서 List&amp;lt;String&amp;gt; ls)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;197&quot; data-origin-height=&quot;396&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/F9NFo/btrob8gfVB8/JPutQpxrTycQGEvOmpkma1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/F9NFo/btrob8gfVB8/JPutQpxrTycQGEvOmpkma1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/F9NFo/btrob8gfVB8/JPutQpxrTycQGEvOmpkma1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FF9NFo%2Fbtrob8gfVB8%2FJPutQpxrTycQGEvOmpkma1%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;197&quot; height=&quot;396&quot; data-origin-width=&quot;197&quot; data-origin-height=&quot;396&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Collection 인터페이스를 구현하는 클래스에서 사용이 가능하다.&lt;/p&gt;
&lt;pre id=&quot;code_1639722370814&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public static void main(String[] args) {
    List&amp;lt;String&amp;gt; ls = Arrays.asList(&quot;Box&quot;, &quot;Robot&quot;);
    ls.forEach(s -&amp;gt; System.out.println(s));
    ls.forEach(System.out::println);
}&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;forEach() 메소드는 Consumer 타입의 인자를 받아 accept() 메소드를 호출한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;accept() 메소드가 받을 인자를 람다식으로 &quot;s -&amp;gt;&quot; 와 같이 작성하지만 메소드 참조로 표현하면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;람다식의 인자를 Consumer의&amp;nbsp; void accept(T t) 메소드의 인자로 그대로 사용하는 형태가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 인스턴스 메소드 참조 2&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) 인스턴스 없이 인스턴스 메소드 참조&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(b1, b2) -&amp;gt; b1.larger(b2) 처럼&amp;nbsp; 두 개의 인자를 받아 b1의 메소드를 호출하면서 b2 를 인자로 넘기는 형태&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(IBox 의 lager 와 같은 형태)를 다음과 같이 메소드 참조 형태로 표현할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ToIntBiFunction&amp;lt;IBox, IBox&amp;gt; bf = (b1, b2) -&amp;gt; b1.larger(b2); &lt;br /&gt;ToIntBiFunction&amp;lt;IBox, IBox&amp;gt; bf = IBox::larger;&lt;/p&gt;
&lt;pre id=&quot;code_1639724172017&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface ToIntBiFunction&amp;lt;T, U&amp;gt; {
    int applyAsInt(T t, U u);
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1639724190143&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class IBox {
    private int n; 

    public IBox(int i) { n = i; }

    public int larger(IBox b) {
        if(n &amp;gt; b.n)
            return n;
        else
            return b.n;
    }
}

class NoObjectMethodRef2 {
    public static void main(String[] args) {
        IBox ib1 = new IBox(5);
        IBox ib2 = new IBox(7);

//        ToIntBiFunction&amp;lt;IBox, IBox&amp;gt; bf = (b1, b2) -&amp;gt; b1.larger(b2);
        ToIntBiFunction&amp;lt;IBox, IBox&amp;gt; bf = IBox::larger;
        int bigNum = bf.applyAsInt(ib1, ib2);
        System.out.println(bigNum);}
}&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;h3 data-ke-size=&quot;size23&quot;&gt;4. 생성자 참조&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) 생성자 참조(default)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Greeting 타입을 리턴하는 default 생성자&lt;/p&gt;
&lt;pre id=&quot;code_1639725542482&quot; class=&quot;java&quot; style=&quot;margin: 20px auto 0px; display: block; overflow: auto; padding: 20px; color: #383a42; background: #f8f8f8; font-size: 14px; font-family: 'SF Mono', Menlo, Consolas, Monaco, monospace; border: 1px solid #ebebeb; line-height: 1.71; cursor: default; z-index: 1;&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Supplier&amp;lt;Greeting&amp;gt; greetingSupplier = Greeting::new;
System.out.println(greetingSupplier.get().hello(&quot;mandy&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;2) 생성자 참조(parameter)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;String 타입을 인자로 받아 Greeting 타입을 리턴하는 생성자&lt;/p&gt;
&lt;pre id=&quot;code_1639725542483&quot; class=&quot;java&quot; style=&quot;margin: 20px auto 0px; display: block; overflow: auto; padding: 20px; color: #383a42; background: #f8f8f8; font-size: 14px; font-family: 'SF Mono', Menlo, Consolas, Monaco, monospace; border: 1px solid #ebebeb; line-height: 1.71; cursor: default; z-index: 1;&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Function&amp;lt;String, Greeting&amp;gt; greetingFunction = Greeting::new;
Greeting mandy = greetingFunction.apply(&quot;mandy&quot;);
System.out.println(mandy.getName());&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. static 메소드 참조&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UnaryOperator&amp;lt;T&amp;gt; extends Function&amp;lt;T, T&amp;gt; : 입력값과 리턴값이 같은 Function인 경우 사용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Greeting 의 hi() String 타입을 인자로 받아 String 타입을 리턴하는 static 메소드&lt;/p&gt;
&lt;pre id=&quot;code_1639725621534&quot; class=&quot;java&quot; style=&quot;margin: 20px auto 0px; display: block; overflow: auto; padding: 20px; color: #383a42; background: #f8f8f8; font-size: 14px; font-family: 'SF Mono', Menlo, Consolas, Monaco, monospace; border: 1px solid #ebebeb; line-height: 1.71; cursor: default; z-index: 1;&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;UnaryOperator&amp;lt;String&amp;gt; hi = Greeting::hi;
System.out.println(hi.apply(&quot;mandy&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;출처&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;a href=&quot;https://cafe.naver.com/cstudyjava&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://cafe.naver.com/cstudyjava&lt;/a&gt;&amp;nbsp;윤성우의 열혈 java&lt;/p&gt;</description>
      <category>Java/Java 8</category>
      <category>java8</category>
      <category>메소드참조</category>
      <author>mandykr</author>
      <guid isPermaLink="true">https://mandykr.tistory.com/42</guid>
      <comments>https://mandykr.tistory.com/42#entry42comment</comments>
      <pubDate>Fri, 17 Dec 2021 16:28:09 +0900</pubDate>
    </item>
    <item>
      <title>[클린코드] 9. 단위 테스트</title>
      <link>https://mandykr.tistory.com/41</link>
      <description>&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;2. 깨끗한 테스트 코드&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. FIRST 규칙&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. TDD 법칙&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;6. Test Double&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;8. 예제&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 테스트는 유연성, 유지보수성, 재사용성을 제공한다.&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;h4 data-ke-size=&quot;size20&quot;&gt;2. 깨끗한 테스트 코드&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;b&gt;리팩토링 전)&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1639470450411&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public void testGetPageHieratchyAsXml() throws Exception
{
    crawler.addPage(root, PathParser.parse(&quot;PageOne&quot;));
    crawler.addPage(root, PathParser.parse(&quot;PageOne.ChildOne&quot;));
    crawler.addPage(root, PathParser.parse(&quot;PageTwo&quot;));
    request.setResource(&quot;root&quot;);
    request.addInput(&quot;type&quot;, &quot;pages&quot;);
    Responder responder = new SerializedPageResponder();
    SimpleResponse response =
            (SimpleResponse) responder.makeResponse(
                    new FitNesseContext(root), request);
    String xml = response.getContent();
    assertEquals(&quot;text/xml&quot;, response.getContentType());
    assertSubString(&quot;&amp;lt;name&amp;gt;PageOne&amp;lt;/name&amp;gt;&quot;, xml);
    assertSubString(&quot;&amp;lt;name&amp;gt;PageTwo&amp;lt;/name&amp;gt;&quot;, xml);
    assertSubString(&quot;&amp;lt;name&amp;gt;ChildOne&amp;lt;/name&amp;gt;&quot;, xml);
}&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) given-when-then 패턴을 사용하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;given : 테스트 자료를 만든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;when : 테스트 자료를 조작한다.&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2) 테스트 API를 만들어 사용하자.&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;테스트 코드를 짜기도 읽기도 쉬워진다.&lt;/p&gt;
&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_1639470483330&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public void testGetPageHierarchyAsXml() throws Exception {
	//given
    makePages(&quot;PageOne&quot;, &quot;PageOne.ChildOne&quot;, &quot;PageTwo&quot;);
    
    //when
    submitRequest(&quot;root&quot;, &quot;type:pages&quot;);
    
    //then
    assertResponseIsXML();
    assertResponseContains(
            &quot;&amp;lt;name&amp;gt;PageOne&amp;lt;/name&amp;gt;&quot;, &quot;&amp;lt;name&amp;gt;PageTwo&amp;lt;/name&amp;gt;&quot;, &quot;&amp;lt;name&amp;gt;ChildOne&amp;lt;/name&amp;gt;&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;3) 테스트당 assert 를 최대한 줄이자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;assert 문이 하나인 함수는 코드를 이해하기 쉬워진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러개의 assert를 수행하는 테스트는 함수를 쪼개면 가능하지만&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;복합적으로 고려해서 테스트당 assert 를 최대한 줄이자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. FIRST 규칙&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Fast 빠르게 : 테스트는 빨라야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Inenpendent 독립적으로 : 각 테스트는 서로 의존하면 안된다. (의존적이면 실패 원인을 찾기 어렵다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Repeatable 반복가능하게 : 환경과 무관하게 반복 실행 되어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Self-Validating 자가검증하는 : 테스트 스스로 성공과 실패를 가늠하도록 만든다.&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; (테스트 결과 확인을 위해 로그를 확인하도록)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Timely 적시에 : 프로덕트 코드를 구현하기 직전에 구현한다. (TDD 법칙)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. TDD 법칙&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫째 : 실패하는 단위 테스트를 작성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘째 : 컴파일은 실패하지 않으면서 실행이 실패하는 정도로만 프로덕트 코드를 작성한다.&lt;/p&gt;
&lt;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;h3 data-ke-size=&quot;size23&quot;&gt;5. 테스트 라이브러리 사용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://mandykr.tistory.com/category/Test/JUnit%205&quot;&gt;JUnit 5&lt;/a&gt;, AssertJ,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://mandykr.tistory.com/category/Test/Mockito&quot;&gt;Mockito&lt;/a&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;h3 data-ke-size=&quot;size23&quot;&gt;6. Test Double&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트에서 원본 객체를 대신하는 객체&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Stub&lt;/b&gt;&lt;br /&gt;원래의구현을최대한단순한것으로대체한다&lt;br /&gt;테스트를위해프로그래밍된항목에만응답한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;b&gt;Spy&lt;/b&gt;&lt;br /&gt;Stub의역할을하면서호출에대한정보를기록한다&lt;br /&gt;이메일서비스에서메시지가몇번전송되었는지확인할때&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;b&gt;Mock&lt;/b&gt;&lt;br /&gt;행위를검증하기위해가짜객체를만들어테스트하는방법&lt;br /&gt;호출에대한동작을프로그래밍할수있다.&lt;br /&gt;Stub은상태를검증하고Mock은행위를검증한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. 테스트 종류&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;UnitTest:프로그램내부의개별컴포넌트의동작을테스트한&lt;br /&gt;다.배포하기전에자동으로실행되도록많이사용한다.&lt;/li&gt;
&lt;li&gt;IntegrationTest:프로그램내부의개별컴포넌트들을합쳐서&lt;br /&gt;동작을테스트한다.UnitTest는각컴포넌트를고립시켜테스트&lt;br /&gt;하기때문에컴포넌트의interaction을확인하는Integration&lt;br /&gt;Test가필요하다.&lt;/li&gt;
&lt;li&gt;E2ETest:EndtoEndTest.실제유저의시나리오대로네트워&lt;br /&gt;크를통해서버의Endpoint를호출해테스트한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8. 예제&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) Unit Test&lt;/h4&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;305&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CBubI/btrvkHJLoo8/fIZeNvMUmpFDnl0CqdBLe1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CBubI/btrvkHJLoo8/fIZeNvMUmpFDnl0CqdBLe1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CBubI/btrvkHJLoo8/fIZeNvMUmpFDnl0CqdBLe1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCBubI%2FbtrvkHJLoo8%2FfIZeNvMUmpFDnl0CqdBLe1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;530&quot; height=&quot;305&quot; data-origin-width=&quot;530&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;&lt;b&gt;Controller&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;personRepository 객체를 통해 Person 객체를 조회한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;findByLastName() 으로 객체를 조회했을 때&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;값이 있으면 이름으로 문자열을 만들어 반환하고, 없으면 안내 메세지를 리턴한다.&lt;/p&gt;
&lt;pre id=&quot;code_1646616398940&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RestController
public class ExampleController {

    private final PersonRepository personRepo;

    @Autowired
    public ExampleController(final PersonRepository personRepo) {
        this.personRepo = personRepo;
    }

    @GetMapping(&quot;/hello/{lastName}&quot;)
    public String hello(@PathVariable final String lastName) {
        Optional&amp;lt;Person&amp;gt; foundPerson = personRepo.findByLastName(lastName);

        return foundPerson
                .map(person -&amp;gt; String.format(&quot;Hello %s %s!&quot;,
                        person.getFirstName(),
                        person.getLastName()))
                .orElse(String.format(&quot;Who is this '%s' you're talking about?&quot;,
                        lastName));
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Unit Test&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PersonRepository mock 을 만들어 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;given-when-then 구조이고, 컨트롤러의 반환값 두 가지 경우를 테스트 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1646616398941&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class ExampleControllerTest {

    private ExampleController subject;

    @Mock
    private PersonRepository personRepo;

    @Before
    public void setUp() throws Exception {
        initMocks(this);
        subject = new ExampleController(personRepo);
    }

    @Test
    public void shouldReturnFullNameOfAPerson() throws Exception {
        Person peter = new Person(&quot;Peter&quot;, &quot;Pan&quot;);
        given(personRepo.findByLastName(&quot;Pan&quot;))
            .willReturn(Optional.of(peter));

        String greeting = subject.hello(&quot;Pan&quot;);

        assertThat(greeting, is(&quot;Hello Peter Pan!&quot;));
    }

    @Test
    public void shouldTellIfPersonIsUnknown() throws Exception {
        given(personRepo.findByLastName(anyString()))
            .willReturn(Optional.empty());

        String greeting = subject.hello(&quot;Pan&quot;);

        assertThat(greeting, is(&quot;Who is this 'Pan' you're talking about?&quot;));
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2) Integration Test&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;(1)&amp;nbsp;Integration Test (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;488&quot; data-origin-height=&quot;269&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cgIiYu/btrviIIVaqH/y0A8os4KiJiOBRHdQDtLq1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cgIiYu/btrviIIVaqH/y0A8os4KiJiOBRHdQDtLq1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cgIiYu/btrviIIVaqH/y0A8os4KiJiOBRHdQDtLq1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcgIiYu%2FbtrviIIVaqH%2Fy0A8os4KiJiOBRHdQDtLq1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;488&quot; height=&quot;269&quot; data-origin-width=&quot;488&quot; data-origin-height=&quot;269&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Integration Test (Database)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PersonRepository 가 데이터베이스와 정상적으로 연결 동작하는 지 확인&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 간의 의존성이 생기지 않도록 테스트 실행 전에 tearDown() 에서 테이블의 데이터를 삭제한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;findByLastName() 이 정상 동작하는지 확인&lt;/p&gt;
&lt;pre id=&quot;code_1646616398945&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RunWith(SpringRunner.class)
@DataJpaTest
public class PersonRepositoryIntegrationTest {
    @Autowired
    private PersonRepository subject;

    @After
    public void tearDown() throws Exception {
        subject.deleteAll();
    }

    @Test
    public void shouldSaveAndFetchPerson() throws Exception {
        Person peter = new Person(&quot;Peter&quot;, &quot;Pan&quot;);
        subject.save(peter);

        Optional&amp;lt;Person&amp;gt; maybePeter = subject.findByLastName(&quot;Pan&quot;);

        assertThat(maybePeter, is(Optional.of(peter)));
    }
}&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) Integration Test (Service)&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;415&quot; data-origin-height=&quot;295&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rllSF/btrveK1OxEB/M2RcmcQScijCmmhqCnaUl0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rllSF/btrveK1OxEB/M2RcmcQScijCmmhqCnaUl0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rllSF/btrveK1OxEB/M2RcmcQScijCmmhqCnaUl0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrllSF%2FbtrveK1OxEB%2FM2RcmcQScijCmmhqCnaUl0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;295&quot; data-origin-width=&quot;415&quot; data-origin-height=&quot;295&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Integration Test (Service)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WireMock을 이용해 mock 서버를 띄운다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;client가 실제 서버가 아닌 mock 서버로 요청하게 해서 client 동작을 테스트한다.&lt;/p&gt;
&lt;pre id=&quot;code_1646616398947&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RunWith(SpringRunner.class)
@SpringBootTest
public class WeatherClientIntegrationTest {

    @Autowired
    private WeatherClient subject;

    @Rule
    public WireMockRule wireMockRule = new WireMockRule(8089); // mock 서버를 띄운다.

    @Test
    public void shouldCallWeatherService() throws Exception {
    	// given: mock 서버에 응답을 설정한다.
        wireMockRule.stubFor(get(urlPathEqualTo(&quot;/some-test-api-key/53.5511,9.9937&quot;))
                .willReturn(aResponse()
                        .withBody(FileLoader.read(&quot;classpath:weatherApiResponse.json&quot;))
                        .withHeader(CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
                        .withStatus(200)));
		
        // when: weatherClient가 mock서버로부터 응답을 받는다.
        Optional&amp;lt;WeatherResponse&amp;gt; weatherResponse = subject.fetchWeather();
		
        // then: 받아온 응답이 기대값과 일치하는 지 확인한다.
        Optional&amp;lt;WeatherResponse&amp;gt; expectedResponse = Optional.of(new WeatherResponse(&quot;Rain&quot;));
        assertThat(weatherResponse, is(expectedResponse));
    }
}&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;출처&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;Clean Code(클린 코드) 애자일 소프트웨어 장인 정신 - 로버트 C. 마틴 지음&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;a href=&quot;https://zero-base.co.kr/category_dev_camp/cleancode_1book&quot;&gt;https://zero-base.co.kr/category_dev_camp/cleancode_1book&lt;/a&gt;&lt;/p&gt;</description>
      <category>책 내용 정리/클린코드(clean code)</category>
      <category>clean code</category>
      <category>단위테스트</category>
      <author>mandykr</author>
      <guid isPermaLink="true">https://mandykr.tistory.com/41</guid>
      <comments>https://mandykr.tistory.com/41#entry41comment</comments>
      <pubDate>Tue, 14 Dec 2021 17:57:24 +0900</pubDate>
    </item>
  </channel>
</rss>