<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>디그다의 개발</title>
    <link>https://digda.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Thu, 27 Aug 2026 10:45:24 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>땅속 디그다</managingEditor>
    <image>
      <title>디그다의 개발</title>
      <url>https://tistory1.daumcdn.net/tistory/5215577/attach/84000895b731475a96facca21be9fbad</url>
      <link>https://digda.tistory.com</link>
    </image>
    <item>
      <title>웹소켓 사용하기</title>
      <link>https://digda.tistory.com/44</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;관리자와 직원 사이에 양방향 통신을 할 일이 존재했고 polling 방식과 websocket 방식 사이에 선택을 해야하는 것이 필요했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 다음과 같은 이유로 웹소켓을 통해 양방향 통신을 제공했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 관리자 - 직원 간의 통신하는데 있어 실시간 통신이 필요했다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. polling 방식을 통해서 구현시 데이터 조회시에 select 절의 사용이 꽤나 큰 비용이였다. (웹소켓은 패킷 데이터에 대한 내용을 db에 insert 하기만 하면 됐고, 이전 문의 내역이나 공지사항 같은 내용은 초기 연결 시에 db에서 select 하게 되므로 그 후의 내용들은 select 가 발생하지 않는다.)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. polling 은 매번 커넥션을 맺는 비용이 드는 반면 웹소켓의 경우 매번 커넥션을 맺지 않고 유지를 하면 된다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4. 웹소켓을 도입해도 추가로 port 내용 작성이나, cors 문제를 신경쓸 필요가 없다. 톰캣에서도 웹소켓을 지원하기 때문이다. 즉, 따로 어플리케이션 level의 코드 부분을 추가 할 뿐이지 따로 인프라 구축에 대한 추가 cost 가 발생하지 않는다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;5. 웹소켓이라는 기술에 대해 도전을 하고 싶었다.&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;다음과 같은 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;다만 결과로써 아쉬웠던 점은 채팅방의 개념이 아닌 관리자 - 직원간의 소통에 대한 실시간 통신이였기 때문에 서브 프로토콜을 사용한 sub - pub 구조가 필요하지 않아 따로 서브 프로토콜을 작성하지 않고 데이터 패킷 형식을 front-end 와 맞추어 진행하였습니다.&lt;/p&gt;
&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;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;기존의 단방향 HTTP 프로토콜과 호환되어 양방향 통신을 제공하기 위해 개발된 프로토콜 입니다. http 프로토콜과 포트 공유가 가능하며 접속까지는 HTTP 프로토콜을 이용하고 이후 통신은 자체적인 WebSocket 프로토콜로 통신하게 됩니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;과정은 어떻게 되는가?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&lt;b&gt;1. tcp/ip 연결 (3 way handshake)&lt;/b&gt;&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&lt;b&gt;2. 웹소켓 열기 handshaking 과정&lt;/b&gt;&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&lt;b&gt;3. 웹소켓으로 데이터 송 수신&lt;/b&gt;&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2번을 자세하게 살펴보도록 하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹소켓을 여는 과정에서 클라이언트가 http 요청을 하게 됩니다. 하지만 반드시 Upgrade, Connection, websocket-key 및 version 에 대한 헤더, 즉 웹소켓과 관련된 헤더들이 포함 되어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Upgrade 의 경우 클라이언트 쪽에서 서버에게 어떠한 프로토콜로 전환하고 싶은지를 명시하는 헤더입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Connection 의 경우 http 연결에서 프로토콜을 변경하기 위해서는 반드시 &quot;upgrade&quot;로 명시를 해야합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Upgrade&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Upgrade&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1667840499461&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Upgrade - HTTP | MDN&quot; data-og-description=&quot;The HTTP 1.1 (only) Upgrade header can be used to upgrade an already established client/server connection to a different protocol (over the same transport protocol). For example, it can be used by a client to upgrade a connection from HTTP 1.1 to HTTP 2.0,&quot; data-og-host=&quot;developer.mozilla.org&quot; data-og-source-url=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Upgrade&quot; data-og-url=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Upgrade&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/epLdmd/hyQttkhfW7/uEA1fwuHD3qmxgoVSbIeFk/img.png?width=1920&amp;amp;height=1080&amp;amp;face=0_0_1920_1080&quot;&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Upgrade&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Upgrade&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/epLdmd/hyQttkhfW7/uEA1fwuHD3qmxgoVSbIeFk/img.png?width=1920&amp;amp;height=1080&amp;amp;face=0_0_1920_1080');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Upgrade - HTTP | MDN&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;The HTTP 1.1 (only) Upgrade header can be used to upgrade an already established client/server connection to a different protocol (over the same transport protocol). For example, it can be used by a client to upgrade a connection from HTTP 1.1 to HTTP 2.0,&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.mozilla.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체적으로 간략화 해서 살펴보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;b&gt;HandShake Request&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;GET&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;/chat HTTP/1.1&lt;br /&gt;&lt;b&gt;Host&lt;/b&gt;: server.gorany.org&lt;br /&gt;&lt;b&gt;Upgrade&lt;/b&gt;: websocket&lt;br /&gt;&lt;b&gt;Connection&lt;/b&gt;: Upgrade&lt;br /&gt;&lt;b&gt;Sec&lt;/b&gt;-&lt;b&gt;WebSocket&lt;/b&gt;-&lt;b&gt;Key&lt;/b&gt;: 임의의 키값&lt;br /&gt;&lt;b&gt;Origin&lt;/b&gt;: http://localhost:8080&lt;br /&gt;Sec-WebSocket-Protocol: 서브 프로토콜 대표적으로 stomp 존재&lt;br /&gt;&lt;b&gt;Sec-WebSocket-Version&lt;/b&gt;: 13&lt;/blockquote&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&gt;&lt;span style=&quot;background-color: #000000; color: #ffffff;&quot;&gt;&lt;b&gt;&amp;nbsp;Header Name&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;background-color: #000000; color: #ffffff;&quot;&gt;&lt;b&gt;&amp;nbsp;div&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;background-color: #000000; color: #ffffff;&quot;&gt;&lt;b&gt;&amp;nbsp;Description&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;GET&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;Require&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;요청 명령어는 GET을 사용해야 하며, HTTP 버전은 1.1 이상이어야 한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Host&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;Require&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;웹소켓 서버의 주소&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Upgrade&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;Require&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;WebSocket이라는 단어를 사용해야 한다. 대소문자는 구분 X&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Connection&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;Require&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Upgrade라는 단어를 사용해야 한다. 대소문자는 구분 X&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Sec-WebSocket-Key&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;Require&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;길이가 16Byte인 임의로 선택된 숫자를 base64 인코딩한 값 이다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Origin&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;Require&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;클라이언트로 웹 브라우저를 사용하는 경우 필수항목으로, 클라이언트의 주소&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Sec-WebSocket-Version&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;Require&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;13을 사용한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sec-WebSocket-Protocol&lt;/td&gt;
&lt;td&gt;Option&lt;/td&gt;
&lt;td&gt;클라이언트가 사용하고 싶은 하위 프로토콜 이름을 명시한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sec-WebSocket-Extensions&lt;/td&gt;
&lt;td&gt;Option&lt;/td&gt;
&lt;td&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;span&gt;&lt;b&gt;HandShake Response&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;HTTP/1.1 101&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;Switching Protocols&lt;br /&gt;&lt;b&gt;Upgrade&lt;/b&gt;: websocket&lt;br /&gt;&lt;b&gt;Connection&lt;/b&gt;: Upgrade&lt;br /&gt;&lt;b&gt;Sec-WebSocket-Accept&lt;/b&gt;: 클라이언트에서 넘겨준 키값을 계산한 값&lt;/blockquote&gt;
&lt;table style=&quot;border-collapse: collapse; width: 99.8837%; height: 358px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;background-color: #000000; color: #ffffff;&quot;&gt;&lt;b&gt;&amp;nbsp;Header Name&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;background-color: #000000; color: #ffffff;&quot;&gt;&lt;b&gt;&amp;nbsp;div&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;background-color: #000000; color: #ffffff;&quot;&gt;&lt;b&gt;&amp;nbsp;Description&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;HTTP&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;Require&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;HTTP 버전은 1.1이며, 클라이언트로부터의 요청이 이상 없는 경우 101을 상태코드로 사용한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Upgrade&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;Require&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;WebSocket이라는 단어를 사용해야 한다. 대소문자는 구분 X&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Connection&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;Require&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Upgrade라는 단어를 사용해야 한다. 대소문자는 구분 X&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Sec-WebSocket-Accept&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;Require&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;클라이언트로부터 받은 Sec-WebSocket-Key를 사용하여 계산된 값이다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sec-WebSocket-Protocol&lt;/td&gt;
&lt;td&gt;Option&lt;/td&gt;
&lt;td&gt;서버에서 서비스하는 하위 프로토콜을 명시한다. 클라이언트가 요청하지 않는 하위 프로토콜을 명시하면 HandShake는 실패한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sec-WebSocket-Extensions&lt;/td&gt;
&lt;td&gt;Option&lt;/td&gt;
&lt;td&gt;서버가 사용하는 추가 옵션을 기술한다. 클라이언트가 요청하지 않는 추가 옵션을 명시하면 HandShake는 실패한다.&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.mozilla.org/ko/docs/Web/API/WebSockets_API/Writing_WebSocket_servers#step_2_%EB%8D%B0%EC%9D%B4%ED%84%B0_%ED%94%84%EB%A0%88%EC%9E%84_%EA%B5%90%ED%99%98&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://developer.mozilla.org/ko/docs/Web/API/WebSockets_API/Writing_WebSocket_servers#step_2_%EB%8D%B0%EC%9D%B4%ED%84%B0_%ED%94%84%EB%A0%88%EC%9E%84_%EA%B5%90%ED%99%98&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1667843251547&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;웹소켓 서버 작성하기 - Web API | MDN&quot; data-og-description=&quot;웹 소켓 서버는 특정한 프로토콜을 따르는 서버의 임의 포트를 리스닝하고 있는 TCP 어플리케이션입니다. 사용자 서버를 만드는 작업은 두려운 일일수도 있습니다. 하지만, 당신이 선택한 플랫&quot; data-og-host=&quot;developer.mozilla.org&quot; data-og-source-url=&quot;https://developer.mozilla.org/ko/docs/Web/API/WebSockets_API/Writing_WebSocket_servers#step_2_%EB%8D%B0%EC%9D%B4%ED%84%B0_%ED%94%84%EB%A0%88%EC%9E%84_%EA%B5%90%ED%99%98&quot; data-og-url=&quot;https://developer.mozilla.org/ko/docs/Web/API/WebSockets_API/Writing_WebSocket_servers&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/Ebx5m/hyQtucsqbc/8vrbABuvewbddHKMfHmoh0/img.png?width=1920&amp;amp;height=1080&amp;amp;face=0_0_1920_1080&quot;&gt;&lt;a href=&quot;https://developer.mozilla.org/ko/docs/Web/API/WebSockets_API/Writing_WebSocket_servers#step_2_%EB%8D%B0%EC%9D%B4%ED%84%B0_%ED%94%84%EB%A0%88%EC%9E%84_%EA%B5%90%ED%99%98&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.mozilla.org/ko/docs/Web/API/WebSockets_API/Writing_WebSocket_servers#step_2_%EB%8D%B0%EC%9D%B4%ED%84%B0_%ED%94%84%EB%A0%88%EC%9E%84_%EA%B5%90%ED%99%98&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/Ebx5m/hyQtucsqbc/8vrbABuvewbddHKMfHmoh0/img.png?width=1920&amp;amp;height=1080&amp;amp;face=0_0_1920_1080');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;웹소켓 서버 작성하기 - Web API | MDN&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;웹 소켓 서버는 특정한 프로토콜을 따르는 서버의 임의 포트를 리스닝하고 있는 TCP 어플리케이션입니다. 사용자 서버를 만드는 작업은 두려운 일일수도 있습니다. 하지만, 당신이 선택한 플랫&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.mozilla.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-framework/docs/4.2.3.RELEASE/spring-framework-reference/html/websocket.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://docs.spring.io/spring-framework/docs/4.2.3.RELEASE/spring-framework-reference/html/websocket.html&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1667910545171&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;25.&amp;nbsp;WebSocket Support&quot; data-og-description=&quot;This part of the reference documentation covers Spring Framework&amp;rsquo;s support for WebSocket-style messaging in web applications including use of STOMP as an application level WebSocket sub-protocol. Section&amp;nbsp;25.1, &amp;ldquo;Introduction&amp;rdquo; establishes a frame of m&quot; data-og-host=&quot;docs.spring.io&quot; data-og-source-url=&quot;https://docs.spring.io/spring-framework/docs/4.2.3.RELEASE/spring-framework-reference/html/websocket.html&quot; data-og-url=&quot;https://docs.spring.io/spring-framework/docs/4.2.3.RELEASE/spring-framework-reference/html/websocket.html&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-framework/docs/4.2.3.RELEASE/spring-framework-reference/html/websocket.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.spring.io/spring-framework/docs/4.2.3.RELEASE/spring-framework-reference/html/websocket.html&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;25.&amp;nbsp;WebSocket Support&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;This part of the reference documentation covers Spring Framework&amp;rsquo;s support for WebSocket-style messaging in web applications including use of STOMP as an application level WebSocket sub-protocol. Section&amp;nbsp;25.1, &amp;ldquo;Introduction&amp;rdquo; establishes a frame of m&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.spring.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#websocket&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#websocket&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1667910550102&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Web on Servlet Stack&quot; data-og-description=&quot;Spring Web MVC is the original web framework built on the Servlet API and has been included in the Spring Framework from the very beginning. The formal name, &amp;ldquo;Spring Web MVC,&amp;rdquo; comes from the name of its source module (spring-webmvc), but it is more com&quot; data-og-host=&quot;docs.spring.io&quot; data-og-source-url=&quot;https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#websocket&quot; data-og-url=&quot;https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#websocket&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/pp88z/hyQvgEwPXl/Zmqtpau8ZLIkdAHkB9EaYk/img.png?width=1022&amp;amp;height=415&amp;amp;face=0_0_1022_415,https://scrap.kakaocdn.net/dn/qZHKw/hyQvnKnfCO/WboLZfzEzzzj4LbZcg7v21/img.png?width=1000&amp;amp;height=392&amp;amp;face=0_0_1000_392,https://scrap.kakaocdn.net/dn/CphF3/hyQvpOXILr/1mKaHEZgsz8g5LxbLtPMU1/img.png?width=582&amp;amp;height=536&amp;amp;face=0_0_582_536&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#websocket&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#websocket&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/pp88z/hyQvgEwPXl/Zmqtpau8ZLIkdAHkB9EaYk/img.png?width=1022&amp;amp;height=415&amp;amp;face=0_0_1022_415,https://scrap.kakaocdn.net/dn/qZHKw/hyQvnKnfCO/WboLZfzEzzzj4LbZcg7v21/img.png?width=1000&amp;amp;height=392&amp;amp;face=0_0_1000_392,https://scrap.kakaocdn.net/dn/CphF3/hyQvpOXILr/1mKaHEZgsz8g5LxbLtPMU1/img.png?width=582&amp;amp;height=536&amp;amp;face=0_0_582_536');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Web on Servlet Stack&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Spring Web MVC is the original web framework built on the Servlet API and has been included in the Spring Framework from the very beginning. The formal name, &amp;ldquo;Spring Web MVC,&amp;rdquo; comes from the name of its source module (spring-webmvc), but it is more com&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.spring.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식 사이트를 통해 추가로 알아보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식 사이트에도 나와 있듯이 proxy server를 사용하여 WebSocket server 가 webServer 뒤에서 동작한다면 redirect 시에 upgrade request 를 포함해주어 야합니다. 실제로도 spring boot 프로젝트 적용시에 문제가 되었던 부분입니다.&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;Note that, if a WebSocket server is running behind a web server (e.g. nginx), you likely need to configure it to pass WebSocket upgrade requests on to the WebSocket server. Likewise, if the application runs in a cloud environment, check the instructions of the cloud provider related to WebSocket support.&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;WebSocket 을 사용할 때 http 를 사용하기 때문에 Spring MVC 에 의존적인 코드를 사용해야 할 것 같지만 Spring 은 WebSocketHttpRequestHandler 를 통해 Spring MVC 에 의존적인 코드를 사용하지 않게 해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의구심이 드는 것은 톰켓이 어떻게 웹소켓을 지원하는지에 대한 부분이 남았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;톰켓이 단순하게 서블릿 표준만 지원하는 서블릿 컨테이너인 줄 알았는데 따로 웹소켓 컨테이너 기능이 포함되어 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자세한 부분은 알 수 없었으나 다음과 같은 클래스로 대략적인 맥락이 이론과 동일했음을 알 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1082&quot; data-origin-height=&quot;681&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CNgGp/btrQItFah3o/JLbr7z3MEImKUEv9dsUgYk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CNgGp/btrQItFah3o/JLbr7z3MEImKUEv9dsUgYk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CNgGp/btrQItFah3o/JLbr7z3MEImKUEv9dsUgYk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCNgGp%2FbtrQItFah3o%2FJLbr7z3MEImKUEv9dsUgYk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1082&quot; height=&quot;681&quot; data-origin-width=&quot;1082&quot; data-origin-height=&quot;681&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식 사이트에서도 잘 찾을 수 없었으나 웹소켓을 직접 만드는 예제를 작성한 블로그가 있었고, 그냥 단순하게 소켓을 유지하는 것으로 보입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://nowonbun.tistory.com/285&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://nowonbun.tistory.com/285&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1667922585026&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Java]  Servlet에서 사용하는 웹 소켓 (WebSocket)&quot; data-og-description=&quot;안녕하세요. 명월입니다. 이 글은 Java의 Servlet에서 사용하는 웹 소켓 (WebSocket) 에 대한 글입니다. 보통의 웹 환경은 브라우저(클라이언트)에서 웹 서버에 Html 문서를 요청하면, 웹 서버는 Html를 작&quot; data-og-host=&quot;nowonbun.tistory.com&quot; data-og-source-url=&quot;https://nowonbun.tistory.com/285&quot; data-og-url=&quot;https://nowonbun.tistory.com/285&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/tknCF/hyQwJd98Sc/LkKcjYnEmtvhRlXchpZAgk/img.png?width=783&amp;amp;height=657&amp;amp;face=411_112_464_170,https://scrap.kakaocdn.net/dn/bfbe6i/hyQwBHcJPA/AatC9K1pjM4NwdqG9tuQH0/img.png?width=783&amp;amp;height=657&amp;amp;face=411_112_464_170,https://scrap.kakaocdn.net/dn/cqKNli/hyQwAask00/Bybeeq5HD5vGgSFwOZOhDK/img.png?width=783&amp;amp;height=657&amp;amp;face=411_112_464_170&quot;&gt;&lt;a href=&quot;https://nowonbun.tistory.com/285&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://nowonbun.tistory.com/285&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/tknCF/hyQwJd98Sc/LkKcjYnEmtvhRlXchpZAgk/img.png?width=783&amp;amp;height=657&amp;amp;face=411_112_464_170,https://scrap.kakaocdn.net/dn/bfbe6i/hyQwBHcJPA/AatC9K1pjM4NwdqG9tuQH0/img.png?width=783&amp;amp;height=657&amp;amp;face=411_112_464_170,https://scrap.kakaocdn.net/dn/cqKNli/hyQwAask00/Bybeeq5HD5vGgSFwOZOhDK/img.png?width=783&amp;amp;height=657&amp;amp;face=411_112_464_170');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[Java] Servlet에서 사용하는 웹 소켓 (WebSocket)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;안녕하세요. 명월입니다. 이 글은 Java의 Servlet에서 사용하는 웹 소켓 (WebSocket) 에 대한 글입니다. 보통의 웹 환경은 브라우저(클라이언트)에서 웹 서버에 Html 문서를 요청하면, 웹 서버는 Html를 작&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;nowonbun.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론적으로는 웹소켓에 대한 upgrade, 해석에 대한 내용들은 모두 톰켓에서 도맡는 것을 확인 할 수 있었고 일반 웹서버와 포트를 공유하는 것이 가능합니다. url 의 protocol prefix 로 어느 컨테이너에서 처리할 것인지 선택하는 것으로 보입니다. (이부분은 확실치 않습니다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 톰켓의 웹소켓 컨테이너의 경우 WsSession 을 통해 연결을 관리하게 되는 것으로 보입니다.&lt;/p&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;/p&gt;
&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;spring 에서 웹소켓을 활용할 때에는 단순하게&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 웹소켓 핸드쉐이킹(upgrade 과정) 및 핸드쉐이킹 전 interceptor&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 클라이언트에서 데이터프레임이 들어왔을 때의 handling 과정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. cors&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. WebSocketSeesion 의 관리&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;WebSocketSession 을 통해서 클라이언트를 식별하고 메시지를 전송하기 때문에 Set으로써 관리하는 편이 좋아보입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹소켓을 공부하면서 통신에 대한 내용들이 너무 추상화가 잘되어 있어 low level 쪽에서의 부분들에 대해 많은 지식의 부족을 느꼈던 것 같습니다. 소켓 프로그래밍에 대한 공부의 필요성을 느끼며 포스팅을 마치겠습니다.&lt;/p&gt;</description>
      <category>개발일지</category>
      <author>땅속 디그다</author>
      <guid isPermaLink="true">https://digda.tistory.com/44</guid>
      <comments>https://digda.tistory.com/44#entry44comment</comments>
      <pubDate>Tue, 22 Nov 2022 14:53:27 +0900</pubDate>
    </item>
    <item>
      <title>엑셀 파일 유연하게 읽어보기</title>
      <link>https://digda.tistory.com/43</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 진행 도중 excel 파일에 대해 요청을 받고 그 엑셀 파일을 객체에 매핑시켜주는 기능을 작성해야하는 일이 생겼습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아파치 에서 제공하는 excel xml 파서기인 아파치 poi 를 통해 해당 기능을 작성해보겠습니다.&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;394&quot; data-origin-height=&quot;68&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/YwvL8/btrQyW0mBPy/MaCLJ0PgEnDWAfQdUk01C1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/YwvL8/btrQyW0mBPy/MaCLJ0PgEnDWAfQdUk01C1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YwvL8/btrQyW0mBPy/MaCLJ0PgEnDWAfQdUk01C1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYwvL8%2FbtrQyW0mBPy%2FMaCLJ0PgEnDWAfQdUk01C1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;68&quot; data-origin-width=&quot;394&quot; data-origin-height=&quot;68&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;엑셀 파일을 처리하는 결과 값으로는 ExcelDAO 의 List 형태로 반납하게 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;693&quot; data-origin-height=&quot;285&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b8pQw2/btrQvOu5IYL/CvKDPdlgnV1194hKxVOKLk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b8pQw2/btrQvOu5IYL/CvKDPdlgnV1194hKxVOKLk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b8pQw2/btrQvOu5IYL/CvKDPdlgnV1194hKxVOKLk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb8pQw2%2FbtrQvOu5IYL%2FCvKDPdlgnV1194hKxVOKLk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;693&quot; height=&quot;285&quot; data-origin-width=&quot;693&quot; data-origin-height=&quot;285&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 Reflection 과 제네릭을 활용하여 단순하게 하나의 객체에만 매핑하는 것이 아닌 다른 객체에도 매핑이 되게끔 만들어 보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사전지식을 알아가보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;리플렉션 이란?&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;리플렉션은 힙 영역에 로드된 Class 타입의 객체를 통해, 원하는 클래스의 인스턴스를 생성할 수 있도록 지원하고, 인스턴스의 필드와 메소드를 접근 제어자와 상관 없이 사용할 수 있도록 지원하는 API 입니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 로드된 클래스는 JVM의 클래스 로더에서 클래스 파일에 대한 로딩을 완료한 후 해당 클래스의 정보를 담은 Class 타입의 객체를 생성하여 메모리의 힙 영역에 저장해 둔것을 의미 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;제네릭이란?&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;데이터의 타입을 일반화 하여 클래스나 메소드에서 사용할 내부 데이터 타입을 컴파일 시에 미리 지정하는 방법입니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반화를 시켰기 때문에 특정 클래스에서만 사용할 수 있는 메서드가 아닙니다. 따라서 공통화된 처리를 시킬 수 있게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예제에서는 제네릭 메서드를 사용할 것이기 때문에 해당 선언에 대한 내용이 필요합니다. 간단하게 다음과 같이 선언 할 수 있습니다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;접근제어자 [static] &amp;lt;T&amp;gt; 반환타입 메서드명(...)&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 목표는 다음과 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 엑셀파일을 읽어온다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 각 row (행)을 ExcelDAO 의 필드에 매칭 시킨다. (이 과정에서 제네릭, 리플렉션이 사용되며 부가적으로 Annotation 을 활용하여 더욱 유연하게 매핑을 시도해 본다.)&lt;/b&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아파치 poi 에 대해 의존성을 추가해보도록 하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;// poi lab 의 의존성 추가&lt;br /&gt;implementation group: 'org.apache.commons', name: 'commons-lang3', version: '3.12.0'&lt;br /&gt;implementation group: 'org.apache.poi', name: 'poi', version: '5.2.0'&lt;br /&gt;implementation group: 'org.apache.poi', name: 'poi-ooxml', version: '5.2.0'&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;이제 본격적으로 poi 를 통해 엑셀을 객체로 매핑 해보겠습니다. 먼저 흐름은 다음과 같습니다.&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. 엑셀파일의 최상단 구분행을 읽어들이고 DAO 객체와 정보 매핑 (셀의 row index 번호를 알아야 한다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 정보를 가지고 있는 나머지 행들을 읽어 DAO 객체를 생성하고 set 한다.&lt;/p&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, kind, inOut, superCategoryType, categoryType 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아파치 poi 의 경우 다음과 같은 자료구조를 가지고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;엑셀 파일 (Workbook) -&amp;gt; 여러개의 시트 (Sheet) -&amp;gt; 여러개의 row(엑셀 행 하나하나) -&amp;gt; 여러개의 cell (칸 하나하나)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 위에서 말씀드린 각 row 의 cell 들을 DAO 필드에 하나하나 매핑시킵니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 클래스들에 대한 세부 메서드는 직접 살피시는 편이 좋아보입니다.&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;엑셀 파일이 맞는지 여부부터 확인 해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;엑셀 파일의 경우 xls, xlsx 의 확장자를 가지고 있습니다. 따라서 String 비교를 통해 시도해보겠습니다.&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;977&quot; data-origin-height=&quot;409&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qHZ6C/btrQvZwwQjL/J4YLlgKT62bKwIB4UHyjkk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qHZ6C/btrQvZwwQjL/J4YLlgKT62bKwIB4UHyjkk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qHZ6C/btrQvZwwQjL/J4YLlgKT62bKwIB4UHyjkk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqHZ6C%2FbtrQvZwwQjL%2FJ4YLlgKT62bKwIB4UHyjkk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;977&quot; height=&quot;409&quot; data-origin-width=&quot;977&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;아파치 poi 는 xls 형식의 경우 HSSFWorkbook 을 , xlsx 형식의 경우 XSSFWorkbook 생성자를 사용해야 합니다. 이 두개의 상위 타입인 Workbook을 반환하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자 이제 시트에서 구분 행을 불러들이겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1103&quot; data-origin-height=&quot;554&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/JFgRS/btrQAwf5rqU/05hf03Uo3TOf1vWM8XfuPk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/JFgRS/btrQAwf5rqU/05hf03Uo3TOf1vWM8XfuPk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/JFgRS/btrQAwf5rqU/05hf03Uo3TOf1vWM8XfuPk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJFgRS%2FbtrQAwf5rqU%2F05hf03Uo3TOf1vWM8XfuPk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1103&quot; height=&quot;554&quot; data-origin-width=&quot;1103&quot; data-origin-height=&quot;554&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;convertToDAO 가 결국 최종 완성할 함수입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 살펴번 getWorkbook 을 통해 workbook 을 가져왔고 workbook.sheetIterator를 통해 각 시트별로 구분행을 가져오게 됩니다. 규모를 크게 가져가게 된다면 각시트별로 맞는 DAO 객체를 만들고 매핑을 시켜줘야 하지만 예제에서는 하나의 시트만을 사용하는 엑셀파일이므로 넘어가도록 하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;sheet.getFirstRowNum() 을 통해 값이 채워져 있는 행의 index 번호를 가져옵니다. (행의 index 는 0부터 시작)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;createFinder 라는 메서드를 통해 ExcelDAO 필드가 엑셀 행의 몇번째 index 에 있는지 알 수 있는 Map 자료구조를 생성하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가적으로 ExcelColumn 이라는 어노테이션을 만들어 유연하게 처리해보겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;768&quot; data-origin-height=&quot;176&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/l1hbu/btrQrGkMemE/hfCpiJOvMdg2HYmxqocL70/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/l1hbu/btrQrGkMemE/hfCpiJOvMdg2HYmxqocL70/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/l1hbu/btrQrGkMemE/hfCpiJOvMdg2HYmxqocL70/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fl1hbu%2FbtrQrGkMemE%2FhfCpiJOvMdg2HYmxqocL70%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;768&quot; height=&quot;176&quot; data-origin-width=&quot;768&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;이 어노테이션을 DAO에 매핑해보고 예제를 위해서 엑셀파일에 없는 DAO 필드인 person 또한 작성해 보겠습니다.&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;811&quot; data-origin-height=&quot;262&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhAscI/btrQyXE0tou/xzUyoGnA0XhIAP4bIliDM1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhAscI/btrQyXE0tou/xzUyoGnA0XhIAP4bIliDM1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhAscI/btrQyXE0tou/xzUyoGnA0XhIAP4bIliDM1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbhAscI%2FbtrQyXE0tou%2FxzUyoGnA0XhIAP4bIliDM1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;811&quot; height=&quot;262&quot; data-origin-width=&quot;811&quot; data-origin-height=&quot;262&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;결론부터 말하면 createFinder는 다음과 같은 형태의 map 을 반환하게 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;734&quot; data-origin-height=&quot;153&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pZnG0/btrQuMxscCa/kzElLNm7peuoKAKEqaxszK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pZnG0/btrQuMxscCa/kzElLNm7peuoKAKEqaxszK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pZnG0/btrQuMxscCa/kzElLNm7peuoKAKEqaxszK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpZnG0%2FbtrQuMxscCa%2FkzElLNm7peuoKAKEqaxszK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;734&quot; height=&quot;153&quot; data-origin-width=&quot;734&quot; data-origin-height=&quot;153&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 excel 구분자에 없는 필드 였다면 -1 을 만약 있는 행이라면 필드가 매핑해야 할 row의 cell index를 매핑해주는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 @ExcelColumn 에 있는 inOut 부분으로 매핑을 시켜주게 됩니다.&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;1377&quot; data-origin-height=&quot;646&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/z5Fa9/btrQrG58jk1/nciwbZsKKqTrVmayZEsk60/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/z5Fa9/btrQrG58jk1/nciwbZsKKqTrVmayZEsk60/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/z5Fa9/btrQrG58jk1/nciwbZsKKqTrVmayZEsk60/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fz5Fa9%2FbtrQrG58jk1%2FnciwbZsKKqTrVmayZEsk60%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1377&quot; height=&quot;646&quot; data-origin-width=&quot;1377&quot; data-origin-height=&quot;646&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;create Finder 를 보게 되면 다음과 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Class&amp;lt;T&amp;gt; daoClass 를 통해서 리플렉션 api 를 이용해 Field 객체 배열을 가져오고 각 Field 의 어노테이션 존재 유무에 따라서 어노테이션의 name 으로 엑셀을 매핑할 것인지, 필드 변수 이름으로 엑셀을 매핑할 것인지 정하게 됩니다.&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;자 이제 마지막 3번 부분 작성을 통해 실제 DAO 객체에 매핑을 해보도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1103&quot; data-origin-height=&quot;554&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/JFgRS/btrQAwf5rqU/05hf03Uo3TOf1vWM8XfuPk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/JFgRS/btrQAwf5rqU/05hf03Uo3TOf1vWM8XfuPk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/JFgRS/btrQAwf5rqU/05hf03Uo3TOf1vWM8XfuPk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJFgRS%2FbtrQAwf5rqU%2F05hf03Uo3TOf1vWM8XfuPk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1103&quot; height=&quot;554&quot; data-origin-width=&quot;1103&quot; data-origin-height=&quot;554&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;rowToDAO 메서드를 사용하여 해당 row 에 맞는 ExcelDAO 객체를 반환하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;rowToDAO 함수를 살펴보면 다음과 같습니다.&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;1279&quot; data-origin-height=&quot;791&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Jx0p0/btrQyvV8U3l/g6XtOs2FnhtitPMgLGR1uk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Jx0p0/btrQyvV8U3l/g6XtOs2FnhtitPMgLGR1uk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Jx0p0/btrQyvV8U3l/g6XtOs2FnhtitPMgLGR1uk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJx0p0%2FbtrQyvV8U3l%2Fg6XtOs2FnhtitPMgLGR1uk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1279&quot; height=&quot;791&quot; data-origin-width=&quot;1279&quot; data-origin-height=&quot;791&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매개변수가 없는 기본생성자를 통해 T 타입 객체를 생성하고 finder 를 통해 현재 필드 변수가 row의 몇번째 cell을 읽어야하는 지를 알고 set 을 하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주목해야 할 점은 private, protected 접근제어자의 경우 field.setAccessible 을 통해 접근 true 를 하고 set 을 해야합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 내용은 Spring 에서 제공하는 @RequestBody, @ResponseBody 와 같은 원리라고 볼 수 있습니다.&lt;/p&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 에서 제공하는 메시지 컨버터를 사용하여 (내부적으로 reflection 동작) 객체를 생성, set 을 하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;엑셀 파일의 경우에 이 mapping 과정이 가장 까다로운데, cell 별 타입에 따라 사용해야하는 함수가 다르기 때문입니다. 따라서 matchFields 함수를 통해서 cell 의 값을 가져오고 가공하여 반환해보겠습니다.&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;1296&quot; data-origin-height=&quot;700&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lJzE7/btrQBKebvnM/nkCUIjs2zwVLttRpxaMGb0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lJzE7/btrQBKebvnM/nkCUIjs2zwVLttRpxaMGb0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lJzE7/btrQBKebvnM/nkCUIjs2zwVLttRpxaMGb0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlJzE7%2FbtrQBKebvnM%2FnkCUIjs2zwVLttRpxaMGb0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1296&quot; height=&quot;700&quot; data-origin-width=&quot;1296&quot; data-origin-height=&quot;700&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;switch - case 문을 통해 각 Cell의 type 에 따라 result 를 반환 하겠습니다. result는 문자열, 숫자, 소수(숫자) 등 다양한 형태로 반환이 되어야 하기 때문에 result 의 타입은 Object 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가적으로 아파치 poi 의 경우 NUMERIC 의 경우 무조건 double 로 반환하기 때문에 필드에 Long, 이나 Integer 같이 포함 되어 있을 경우 별도로 정수 타입으로 형변환이 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 마지막으로 테스팅을 하여 올바르게 매핑이 되었는지 확인해 보겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1183&quot; data-origin-height=&quot;464&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/KhVyl/btrQsD8ZTWT/04y2wfMyKG3kZk5yPh8s81/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/KhVyl/btrQsD8ZTWT/04y2wfMyKG3kZk5yPh8s81/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/KhVyl/btrQsD8ZTWT/04y2wfMyKG3kZk5yPh8s81/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKhVyl%2FbtrQsD8ZTWT%2F04y2wfMyKG3kZk5yPh8s81%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1183&quot; height=&quot;464&quot; data-origin-width=&quot;1183&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DAO 객체가 변경이 되어도 올바르게 작동하는지 살펴보겠습니다.&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;1011&quot; data-origin-height=&quot;329&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bz9aSD/btrQw18NhGj/hHlQ4gSz56tvbVTvz7NCH1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bz9aSD/btrQw18NhGj/hHlQ4gSz56tvbVTvz7NCH1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bz9aSD/btrQw18NhGj/hHlQ4gSz56tvbVTvz7NCH1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbz9aSD%2FbtrQw18NhGj%2FhHlQ4gSz56tvbVTvz7NCH1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1011&quot; height=&quot;329&quot; data-origin-width=&quot;1011&quot; data-origin-height=&quot;329&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;1251&quot; data-origin-height=&quot;566&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FAjWi/btrQz53WJCw/dGlIcMVjaKkgdNXTOaJVE0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FAjWi/btrQz53WJCw/dGlIcMVjaKkgdNXTOaJVE0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FAjWi/btrQz53WJCw/dGlIcMVjaKkgdNXTOaJVE0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFAjWi%2FbtrQz53WJCw%2FdGlIcMVjaKkgdNXTOaJVE0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1251&quot; height=&quot;566&quot; data-origin-width=&quot;1251&quot; data-origin-height=&quot;566&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;</description>
      <category>개발일지</category>
      <author>땅속 디그다</author>
      <guid isPermaLink="true">https://digda.tistory.com/43</guid>
      <comments>https://digda.tistory.com/43#entry43comment</comments>
      <pubDate>Mon, 7 Nov 2022 11:24:17 +0900</pubDate>
    </item>
    <item>
      <title>batch insert 와 JPA 의 한계</title>
      <link>https://digda.tistory.com/42</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;파이썬을 통해 검사결과 양식을 받아서 spring 서버에서 처리하는 도중 검사 결과 1건당 15~20 여개의 insert query 가 발생했습니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당연하게도 검사 파일은 수십만건이므로 insert query 또한 수백만 건이 발생하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 insert 에 대해 단건 insert 가 수백만 개가 예상되어 상당한 성능 저하가 예상이 되었고 이를 batch insert 로 해결해 보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;batch insert 로 처리를 계획한 이후 당연하게도 jpa 의 영속성 컨텍스트와 관련하여 키생성 전략과 맞물려 이슈가 발생하였고 이를 해결하는 과정을 포스팅 해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;알아볼 내용은 다음과 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 영속성 컨텍스트의 Entity 저장 전략&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 각 키생성 전략의 Id 초기화 방식&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. spring Data Jpa 의 문제점&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;먼저 JPA의 영속성 컨텍스트 부터 살펴 보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영속성 컨텍스트는 Entity의 @Id 를 통해서 엔티티 객체를 식별하고 동등성을 비교하게 됩니다. 즉, 엔티티 객체가 영속성 컨텍스트에 올라가기 위해서는 id 값을 반드시 필수로 가지고 있어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당연하게도 @Id + @GenerateValue 를 사용하게 되면 이러한 문제는 거의 직면하지 않습니다. 만약 @GenerateValue가 없으면 어떨까요? JpaSystemException 이 발생하게 됩니다.&lt;/p&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;320&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AK3Y1/btrQj5Sqwrp/ABU5eMsM5nnbJiJm1y5vDK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AK3Y1/btrQj5Sqwrp/ABU5eMsM5nnbJiJm1y5vDK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AK3Y1/btrQj5Sqwrp/ABU5eMsM5nnbJiJm1y5vDK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAK3Y1%2FbtrQj5Sqwrp%2FABU5eMsM5nnbJiJm1y5vDK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;320&quot; data-origin-width=&quot;787&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;Member 는 간단하게 id, name 만을 column 으로 가지는 엔티티입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;940&quot; data-origin-height=&quot;467&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbid2M/btrQj4zeY8T/AwJBcW7odQRzWFozHXgxxK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbid2M/btrQj4zeY8T/AwJBcW7odQRzWFozHXgxxK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbid2M/btrQj4zeY8T/AwJBcW7odQRzWFozHXgxxK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbbid2M%2FbtrQj4zeY8T%2FAwJBcW7odQRzWFozHXgxxK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;940&quot; height=&quot;467&quot; data-origin-width=&quot;940&quot; data-origin-height=&quot;467&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;id 값을 넣어주게 되면 잘 저장이 되게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;하지만 여기서 Spring data jpa 의 한계가 들어나게 되는데, Spring data jpa 의 경우 Entity의 id 값이 채워져 있다면 merge 를 하게 됩니다.&lt;/b&gt; 즉 다음과 같은 형식입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;if ( 엔티티의 id 값이 채워져 있다면 ){&lt;br /&gt;&amp;nbsp; entityManager.merge()&lt;br /&gt;} else {&lt;br /&gt;&amp;nbsp; &amp;nbsp;entityManager.persist()&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;spring Data Jpa 는 id 값이 채워져 있다면 엔티티의 상태가 영속성 컨텍스트에 한번 들어갔다 나온 detach 상태로 파악을 하고 merge 를 하게 되는 것이고 이 merge 과정 속에서 단순하게 insert 쿼리 한번을 기대했지만 db에 select 후 없는 것을 확인한 후 쓰기 지연 저장소를 통해 insert query 가 나가는 것을 확인 할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Hibernate: select member0_.id as id1_1_0_, member0_.name as name2_1_0_ from member member0_ where member0_.id=? &lt;br /&gt;&lt;br /&gt;Hibernate: insert into member (name, id) values (?, ?)&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;그렇다면 EntityManager를 직접 사용하는 경우는 어떨까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 insert 만이 발생하는 것을 확인 할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 결론적으로는 @GenerateValue 를 사용하지 않고 DB 에 insert 를 할 경우 jpa 를 고집할 이유는 없어 보입니다.&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;이를 @GenerateValue 들로 전환해 각각 키생성이 어떤식으로 이루어 지는지 살펴보겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;781&quot; data-origin-height=&quot;319&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dbCdp6/btrQmRSpBzk/Kz4gbFVMkNoTaU5quJIOo1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dbCdp6/btrQmRSpBzk/Kz4gbFVMkNoTaU5quJIOo1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dbCdp6/btrQmRSpBzk/Kz4gbFVMkNoTaU5quJIOo1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdbCdp6%2FbtrQmRSpBzk%2FKz4gbFVMkNoTaU5quJIOo1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;781&quot; height=&quot;319&quot; data-origin-width=&quot;781&quot; data-origin-height=&quot;319&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;처음으로 시퀀스 전략을 사용해보겠습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시퀀스 전략의 경우에 데이터베이스 자체적으로 시퀀스 전략을 지원해야지 사용할 수 있습니다. h2의 경우 아무설정을 하지 않는 다면&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;409&quot; data-origin-height=&quot;84&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bppwa4/btrQliQM9LE/WAXwCkZspKs4F4CbCfWzhk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bppwa4/btrQliQM9LE/WAXwCkZspKs4F4CbCfWzhk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bppwa4/btrQliQM9LE/WAXwCkZspKs4F4CbCfWzhk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbppwa4%2FbtrQliQM9LE%2FWAXwCkZspKs4F4CbCfWzhk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;409&quot; height=&quot;84&quot; data-origin-width=&quot;409&quot; data-origin-height=&quot;84&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음을 볼 수 있게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시퀀스 전략을 사용했을 때의 쿼리를 살펴보도록 하겠습니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시퀀스 전략은 기본적으로 JPA 에서 Hibernate Sequence next value 를 가져와 id 를 부여하게 됩니다 hibernate sequence 가져 오게 될 때 기본값이 1인 next value 를 수정해서 사용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿼리를 살펴 보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. call&amp;nbsp;next&amp;nbsp;value&amp;nbsp;for&amp;nbsp;hibernate_sequence&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666;&quot;&gt;2. Hibernate: insert into member (name, id) values (?, ?)&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666;&quot;&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;&lt;span style=&quot;background-color: #fcfcfc; color: #666666;&quot;&gt;하지만 시퀀스 전략의 경우 시퀀스 값을 가져오면 자동으로 update 되는 시스템과는 달리 테이블 전략의 경우 select 를 통해 id 값을 가져온후 update 를 통해 테이블의 값을 업데이트를 하여 시퀀스 전략보다 성능이 떨어집니다.&lt;/span&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;select tbl.next_val from hibernate_sequences tbl where tbl.sequence_name=? for update&lt;br /&gt;update hibernate_sequences set next_val=? where next_val=? and sequence_name=?&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666;&quot;&gt;추가적으로 동시성을 보장을 위해 시퀀스 테이블에 대한 value 에 락을 걸게 됩니다.&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;background-color: #fcfcfc; color: #666666;&quot;&gt;따라서 아무리 봐도 시퀀스 전략을 흉내내는 전략인 테이블 전략을 사용하는 것을 지양해야 할 듯 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666;&quot;&gt;마지막 전략은 Identity 전략으로 데이터베이스에 들어가는 시점에 data row 의 id 값을 알 수 있는 방식입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666;&quot;&gt;그런데 여기서 문제가 발생합니다. 영속성 컨텍스트의 경우 엔티티를 persist 상태로 두기위해서는 id 값이 반드시 필수 입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666;&quot;&gt;Identity 전략의 경우에 당연하게도 insert query 시에 id 값을 할당 받기 때문에 identity 전략으로 insert 에 대한 쓰기 지연을 사용하지 못합니다.&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;background-color: #fcfcfc; color: #666666;&quot;&gt;쓰기 지연을 사용하지 못하기 때문에 다시말해 batch insert 를 사용하지 못하게 됩니다.&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;background-color: #fcfcfc; color: #666666;&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;878&quot; data-origin-height=&quot;375&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bzNB2z/btrQkRF03HT/IdaNj7VuorkAQghBo9GjpK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bzNB2z/btrQkRF03HT/IdaNj7VuorkAQghBo9GjpK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bzNB2z/btrQkRF03HT/IdaNj7VuorkAQghBo9GjpK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbzNB2z%2FbtrQkRF03HT%2FIdaNj7VuorkAQghBo9GjpK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;878&quot; height=&quot;375&quot; data-origin-width=&quot;878&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;&lt;span style=&quot;background-color: #fcfcfc; color: #666666;&quot;&gt;2개의 member Entity 를 저장해 보겠습니다&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;background-color: #fcfcfc; color: #666666;&quot;&gt;처음으로 identity 전략입니다. 앞서 말씀드렸다싶이 쓰기 지연이 적용되지 않게 되므로 insert query가 바로 나가는 것을 확인 할 수 있습니다.&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;717&quot; data-origin-height=&quot;88&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bYRZF7/btrQljWvYNE/wch7Y6ES7LjoQUtJZDbpQ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bYRZF7/btrQljWvYNE/wch7Y6ES7LjoQUtJZDbpQ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bYRZF7/btrQljWvYNE/wch7Y6ES7LjoQUtJZDbpQ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbYRZF7%2FbtrQljWvYNE%2Fwch7Y6ES7LjoQUtJZDbpQ0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;88&quot; data-origin-width=&quot;717&quot; data-origin-height=&quot;88&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 시퀀스 전략을 보겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;660&quot; data-origin-height=&quot;93&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RlQNW/btrQomEIAXK/cLQjIYnO0NgWjpN32D39s1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RlQNW/btrQomEIAXK/cLQjIYnO0NgWjpN32D39s1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RlQNW/btrQomEIAXK/cLQjIYnO0NgWjpN32D39s1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRlQNW%2FbtrQomEIAXK%2FcLQjIYnO0NgWjpN32D39s1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;93&quot; data-origin-width=&quot;660&quot; data-origin-height=&quot;93&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;insert 쿼리 없이 단순하게 hibernate_sequence value 만 업데이트 하는 것을 볼 수 있습니다.&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;각 키생성 전략에 대해 설명을 드렸고 다음으로 현재 프로젝트에서 나오는 문제점을 파악하고 해결하는 과정을 서술해 보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 batch insert 관련입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;batch insert 의 경우 물론 JPA 에서 쓰기 지연 저장소를 통해 지원을 하게 됩니다. 추가 사항은 설정파일에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;spring.jpa.properties.hibernate.jdbc.batch_size 옵션에 숫자를 부여하면 되겠습니다.&lt;/p&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시퀀스 전략, 테이블 전략 모두 batch insert 를 지원하고 identity 전략은 batch insert 를 사용하지 못합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 프로젝트의 DataBase 환경은 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;따라서 문제점에 대한 방법은 다음 3개로 압축됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 테이블 전략을 사용해 batch insert 를 작성하기&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. identity 전략 그냥 사용하기&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. jdbc 직접 사용하여 batch insert 작성 (물론 완전 직접 사용 아니고 spring jdbc 를 사용합니다.)&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;각각의 환경에서의 속도를 h2 에서 테스팅을 해보고 마무리를 해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 batch insert 를 적용하지 않았을 때의 시퀀스 전략 소요시간과 적용했을 때의 소요시간의 차이를 살펴 보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Member Entity 객체를 10만건을 넣어보는 비교를 진행하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;879&quot; data-origin-height=&quot;416&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bsuPXv/btrQn7PxhN1/9jdoQ5Sn20OmykDUIvrK51/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bsuPXv/btrQn7PxhN1/9jdoQ5Sn20OmykDUIvrK51/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bsuPXv/btrQn7PxhN1/9jdoQ5Sn20OmykDUIvrK51/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbsuPXv%2FbtrQn7PxhN1%2F9jdoQ5Sn20OmykDUIvrK51%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;879&quot; height=&quot;416&quot; data-origin-width=&quot;879&quot; data-origin-height=&quot;416&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;802&quot; data-origin-height=&quot;304&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lTXmR/btrQp6V11o9/UOovKKxaKMqDXKrkQgP1g1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lTXmR/btrQp6V11o9/UOovKKxaKMqDXKrkQgP1g1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lTXmR/btrQp6V11o9/UOovKKxaKMqDXKrkQgP1g1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlTXmR%2FbtrQp6V11o9%2FUOovKKxaKMqDXKrkQgP1g1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;802&quot; height=&quot;304&quot; data-origin-width=&quot;802&quot; data-origin-height=&quot;304&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Member Entity 객체를 사용하여 insert 쿼리를 진행 시켜 보겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;449&quot; data-origin-height=&quot;80&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjPzUe/btrQo9MZWRM/vuwpV6gZtq0q20WWNPf0e1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjPzUe/btrQo9MZWRM/vuwpV6gZtq0q20WWNPf0e1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjPzUe/btrQo9MZWRM/vuwpV6gZtq0q20WWNPf0e1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbjPzUe%2FbtrQo9MZWRM%2FvuwpV6gZtq0q20WWNPf0e1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;449&quot; height=&quot;80&quot; data-origin-width=&quot;449&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;여러번 실험결과 대략 13초 ~ 16초정도의 시간이 소요됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로는 batch insert 를 통해 쿼리를 진행 시켜 보겠습니다. batch size 는 1000으로 진행을 했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;489&quot; data-origin-height=&quot;57&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bwraXa/btrQpUIgdH2/D3atKhkWgx6N2lZiupLLYk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bwraXa/btrQpUIgdH2/D3atKhkWgx6N2lZiupLLYk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bwraXa/btrQpUIgdH2/D3atKhkWgx6N2lZiupLLYk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbwraXa%2FbtrQpUIgdH2%2FD3atKhkWgx6N2lZiupLLYk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;57&quot; data-origin-width=&quot;489&quot; data-origin-height=&quot;57&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대략 8~10초 정도의 시간이 소요됩니다. 추가적으로 기본값인 allocate size를 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;대략 40퍼센트의 성능 차이가 발생하는 것을 확인해 볼 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 Table 전략을 사용하면 어떻게 반응할까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테이블 전략의 경우 별도의 시퀀스를 흉내내는 테이블을 기반으로 id 값을 select , update 하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마찬가지로 현재 allocate size 를 1로 두고 진행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;412&quot; data-origin-height=&quot;91&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/WrimO/btrQon5Q4d0/2O2RbGgpjwn2b8xxlrhEpk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/WrimO/btrQon5Q4d0/2O2RbGgpjwn2b8xxlrhEpk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/WrimO/btrQon5Q4d0/2O2RbGgpjwn2b8xxlrhEpk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWrimO%2FbtrQon5Q4d0%2F2O2RbGgpjwn2b8xxlrhEpk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;412&quot; height=&quot;91&quot; data-origin-width=&quot;412&quot; data-origin-height=&quot;91&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;별도의 시퀀스 테이블을 관리하여 select update 쿼리로 인해 오히려 성능이 떨어지는 현상을 발견 할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 마찬가지로 allocate size 를 크게 가져갈 경우 더 성능을 좋게 만들 수 있습니다.&lt;/p&gt;
&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;마지막으로 Identity 전략(DB 에서 id 를 지정) + spring jdbc 를 사용하여 가장 빠른 결과를 얻어내 보겠습니다.&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;830&quot; data-origin-height=&quot;294&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qTRjF/btrQp5JDtdq/zqiyjEBRCrRawB3uIzu8hk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qTRjF/btrQp5JDtdq/zqiyjEBRCrRawB3uIzu8hk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qTRjF/btrQp5JDtdq/zqiyjEBRCrRawB3uIzu8hk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqTRjF%2FbtrQp5JDtdq%2FzqiyjEBRCrRawB3uIzu8hk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;830&quot; height=&quot;294&quot; data-origin-width=&quot;830&quot; data-origin-height=&quot;294&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서와 마찬가지로 batch size 는 1000으로 진행하였습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;504&quot; data-origin-height=&quot;78&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/KemVO/btrQo8ObhDz/PqqmTrpG7SKtMLesjYpjoK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/KemVO/btrQo8ObhDz/PqqmTrpG7SKtMLesjYpjoK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/KemVO/btrQo8ObhDz/PqqmTrpG7SKtMLesjYpjoK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKemVO%2FbtrQo8ObhDz%2FPqqmTrpG7SKtMLesjYpjoK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;504&quot; height=&quot;78&quot; data-origin-width=&quot;504&quot; data-origin-height=&quot;78&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 내용의 경우 시퀀스값을 가져오거나 시퀀스 테이블에 대한 select + update 가 발생하지 않고 오직 batch insert 쿼리만 작성 되기 때문에 상당히 빠른 결과를 도출해 낼 수 있게 됩니다.&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;결론적으로 프로젝트에서 여러개의 insert 가 나오는 부분을 해결하는 과정은 다음과 같아집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 검사 결과 하나당 10~20개의 insert 쿼리 발생 -&amp;gt; batch insert 를 통해 쿼리 최적화&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. mysql 에서는 시퀀스 전략을 채택할 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; 대안 1. 테이블 전략&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;--&amp;gt; 개발자가 직접 테이블 전략에 대한 설정 관리 유지보수를 해야하고 allocate size 설정을 잘못하면 select - update 가 자주 나가게 되어 오히려 개별 insert 보다 성능이 떨어질 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt;&lt;b&gt; 대안 2. identity 전략(database 의 auto increasement) + jdbcTemplate 사용&amp;nbsp; &lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;--&amp;gt;&amp;nbsp; 데이터 베이스의 auto increasement 전략 사용시에는 JPA를 사용하여 batch insert 를 사용하지 못한다. 따라서 jdbcTemplate를 사용해야 한다.&lt;/p&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를 선택하여 insert 쿼리를 최적화를 진행했고 성능에 대해 70퍼센트 정도의 시간소요를 줄일 수 있게 되었습니다.&lt;/p&gt;</description>
      <category>개발일지</category>
      <author>땅속 디그다</author>
      <guid isPermaLink="true">https://digda.tistory.com/42</guid>
      <comments>https://digda.tistory.com/42#entry42comment</comments>
      <pubDate>Fri, 4 Nov 2022 15:01:05 +0900</pubDate>
    </item>
    <item>
      <title>선착순 동시성 문제 해결하기 낙관적 락, 비관적 락</title>
      <link>https://digda.tistory.com/41</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;각각 낙관적 락과 비관적 락으로 해결해보는 과정을 보이려고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 프로젝트의 도메인을 간략하게 살펴 보도록 하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;학부모는 설명회를 신청하고, 신청인원이 마감되어 있다면 대기 신청을 진행하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 각각 설명회(Presentation), 신청(Participation), 대기(Waiting) 의 도메인에 대한 설명을 해보겠습니다.&lt;/p&gt;
&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_1667457781633&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
@NoArgsConstructor
@Getter
public class Presentation {

    @Id
    @GeneratedValue
    private Long id;
    private String name;
    private Integer maxPerson;
    private Integer participantCnt;
    private Integer waitingCnt;
    
}&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_1667457897524&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
@NoArgsConstructor
@Getter
public class Participation {

    @Id @GeneratedValue
    private Long id;
    private LocalTime time;
    
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn
    private Presentation presentation;
    
}&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_1667457915187&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
@NoArgsConstructor
@Getter
public class Waiting {

    @Id @GeneratedValue
    private Long id;
    private Integer orders;
    private LocalTime localTime;
    
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn
    private Presentation presentation;

}&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;또한 해당하는 내용에 맞게 PresentationService.register() 를 만들어 보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트 쪽에서 PresentationId 를 기반으로 요청이 들어오게 되면 Persistence Level 에서 설명회를 조회를 하고 신청 정보를 Participation 테이블이 insert 를 하고 설명회의 신청인원 현황을 + 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;따라서 PresentationId 를 기반으로 설명회를 조회하는 과정과 Presentation 도메인의 ParticipantCnt 를 +1 하는 과정에서 갱신 분실 문제가 발생하고 이로 인해서 예정 인원보다 많은 인원이 설명회를 신청할 수 있게 된다는 이슈가 발생합니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;즉, Presentation 정보를 가져오는 과정에서 동시성 문제가 발생하게 됩니다.&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Lock 없이 Presentation 정보를 가져오고 update 하는 과정을 살펴보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PresentationService&lt;/p&gt;
&lt;pre id=&quot;code_1667458299887&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
@Slf4j
public class PresentationService {

     private final ParticipationRepository participationRepository;
     private final PresentationRepository presentationRepository;
     private final WaitingRepository waitingRepository;

     @Transactional
     public void registerWithNoLock(Long presentationId) {

         Presentation presentation = presentationRepository.findById(presentationId).get();
         log.info(&quot;presentation 상태 : {}/{} (신청/최대) | {} (대기)&quot;,
                 presentation.getParticipantCnt(), presentation.getMaxPerson(), presentation.getWaitingCnt());

         if (canParticipate(presentation)){
             participate(presentation);
         } else {
             wait(presentation);
         }
     }
     
    private boolean canParticipate(Presentation presentation){
         // 설명회에서 수용가능한 인원
         Integer maxPerson = presentation.getMaxPerson();
         // 신청 인원
         Integer participantCnt = presentation.getParticipantCnt();
         return maxPerson &amp;gt; participantCnt;
     }

     private void participate(Presentation presentation){
         // 설명회 인원수 증가
         presentation.plusParticipantCnt();

         Participation participation = Participation.createParticipation(presentation);
         participationRepository.save(participation);
     }

     private void wait(Presentation presentation){
         // 대기 인원 수 증가
         presentation.plusWaitingCnt();
         // 대기 번호 부여
         Integer waitingCnt = presentation.getWaitingCnt();

         Waiting waiting = Waiting.createWaiting(waitingCnt, presentation);
         waitingRepository.save(waiting);
     }
}&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;registerWithNoLock 의 메서드를 살펴 보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가져온 설명회 정보에서 신청가능 최대 인원과 신청 인원 현황을 바탕으로 학부모를 신청상태로 만들 것인지 대기 상태로 만들 것인지를 판단하고 그에 맞는 Participation / Waiting 정보를 데이터베이스에 insert 하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Test를 통해 해당 올바르게 신청이 되는지 확인해 보겠습니다.&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;1045&quot; data-origin-height=&quot;454&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cfxE2a/btrQksZPojm/Aq8HsuT0v3YshppLMffTqk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cfxE2a/btrQksZPojm/Aq8HsuT0v3YshppLMffTqk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cfxE2a/btrQksZPojm/Aq8HsuT0v3YshppLMffTqk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcfxE2a%2FbtrQksZPojm%2FAq8HsuT0v3YshppLMffTqk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1045&quot; height=&quot;454&quot; data-origin-width=&quot;1045&quot; data-origin-height=&quot;454&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;결과를 확인해보도록 하겠습니다. 설명회를 신청할 수 있는 최대 인원은 10명 으로 설정하고 Thread 갯수를 200개로 실험을 진행했습니다.&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;1472&quot; data-origin-height=&quot;435&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c6hvee/btrQf3Nz9ds/hvFkGhHL4PejbiXNLJYLa1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c6hvee/btrQf3Nz9ds/hvFkGhHL4PejbiXNLJYLa1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c6hvee/btrQf3Nz9ds/hvFkGhHL4PejbiXNLJYLa1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc6hvee%2FbtrQf3Nz9ds%2FhvFkGhHL4PejbiXNLJYLa1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1472&quot; height=&quot;435&quot; data-origin-width=&quot;1472&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;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;오류가 없이 작업이 이루어 지지만 Presentation 의 ParticipantCnt 값이 dirtyUpdate로 갱신 분실이 발생하고 여러개의 커넥션에서 같은 버전(상태)의 Presentation 정보를 Read 하기 때문에 설명회 신청을 10명 이상 할 수 있게 되었습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DataBase 의 저장상태를 확인해 보겠습니다.&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;458&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LmIOr/btrQkvhQapF/leV1Vh2mYHCJuqEIHqOF71/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LmIOr/btrQkvhQapF/leV1Vh2mYHCJuqEIHqOF71/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LmIOr/btrQkvhQapF/leV1Vh2mYHCJuqEIHqOF71/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLmIOr%2FbtrQkvhQapF%2FleV1Vh2mYHCJuqEIHqOF71%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;883&quot; height=&quot;458&quot; data-origin-width=&quot;883&quot; data-origin-height=&quot;458&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설명회에 신청가능 인원이 10 명임에도 불구하고 88명이 신청을 한 것을 확인 할 수 있습니다.&lt;/p&gt;
&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;기존 Presentation Service 에서 메서드를 추가로 작성합니다. (추가적으로 repo 계층에서의 비관적락을 추가합니다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;h2 의 경우에 데이터베이스의 락을 선택할 시에 for update 를 통해 데이터베이스 락을 획득하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가적으로 QueryHint 를 통해 락을 받으려고 하는 대기시간에 대한 timeout 을 설정 할 수 있습니다.&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;@QueryHints({@QueryHint(name = &quot;javax.persistence.lock.timeout&quot;, value = &quot;1000&quot;)})&lt;/blockquote&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;974&quot; data-origin-height=&quot;154&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/WuZfM/btrQkGpXn6y/UyYtispj2Nq8HPW8u2vPW1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/WuZfM/btrQkGpXn6y/UyYtispj2Nq8HPW8u2vPW1/img.png&quot; data-alt=&quot;비관적락 사용하기&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/WuZfM/btrQkGpXn6y/UyYtispj2Nq8HPW8u2vPW1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWuZfM%2FbtrQkGpXn6y%2FUyYtispj2Nq8HPW8u2vPW1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;974&quot; height=&quot;154&quot; data-origin-width=&quot;974&quot; data-origin-height=&quot;154&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;비관적락 사용하기&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1101&quot; data-origin-height=&quot;361&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Tm07N/btrQgdv82kd/cJFEepkhFXDfR2loYK2Vkk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Tm07N/btrQgdv82kd/cJFEepkhFXDfR2loYK2Vkk/img.png&quot; data-alt=&quot;비관적 Lock을 통해 Presentation 획득&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Tm07N/btrQgdv82kd/cJFEepkhFXDfR2loYK2Vkk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FTm07N%2FbtrQgdv82kd%2FcJFEepkhFXDfR2loYK2Vkk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1101&quot; height=&quot;361&quot; data-origin-width=&quot;1101&quot; data-origin-height=&quot;361&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;비관적 Lock을 통해 Presentation 획득&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마찬 가지로 Test 를 진행 해보겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1069&quot; data-origin-height=&quot;441&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wJcmz/btrQgLGeaCv/jEPPLL9rMafznpeHPdud50/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wJcmz/btrQgLGeaCv/jEPPLL9rMafznpeHPdud50/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wJcmz/btrQgLGeaCv/jEPPLL9rMafznpeHPdud50/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwJcmz%2FbtrQgLGeaCv%2FjEPPLL9rMafznpeHPdud50%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1069&quot; height=&quot;441&quot; data-origin-width=&quot;1069&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;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1503&quot; data-origin-height=&quot;287&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vqPP0/btrQgdCVJzm/IlJ1sW5nWETDxUxHdq55Wk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vqPP0/btrQgdCVJzm/IlJ1sW5nWETDxUxHdq55Wk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vqPP0/btrQgdCVJzm/IlJ1sW5nWETDxUxHdq55Wk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvqPP0%2FbtrQgdCVJzm%2FIlJ1sW5nWETDxUxHdq55Wk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1503&quot; height=&quot;287&quot; data-origin-width=&quot;1503&quot; data-origin-height=&quot;287&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 순차적으로 설명회를 잘 신청하는 것을 확인해 볼 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1139&quot; data-origin-height=&quot;509&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b4X1id/btrQkRES1n3/A13aL4boBFptvMNFYFexI0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b4X1id/btrQkRES1n3/A13aL4boBFptvMNFYFexI0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b4X1id/btrQkRES1n3/A13aL4boBFptvMNFYFexI0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb4X1id%2FbtrQkRES1n3%2FA13aL4boBFptvMNFYFexI0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1139&quot; height=&quot;509&quot; data-origin-width=&quot;1139&quot; data-origin-height=&quot;509&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터베이스에서 또한 10개의 Particiaption 이 등록되어 있는 것을 확인 할 수 있었습니다.&lt;/p&gt;
&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;낙관적 락의 경우 @Version 을 사용하여 Presentation 을 업데이트 하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 update 쿼리문이 나갈 때 version 에 대한 update 와 where 절을 통한 presentation 을 검색하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 update 이전에 다른 Thread 에서 해당 row 에 대한 변경이 일어났다면 update 가 진행 되지 않을 것이고 OptimisticLock Exception 을 터트리게 됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비관적 락의 경우 데이터베이스 락을 획득하고 관리했지만 낙관적 락의 경우 Version 을 통해 application level 에서 동시성을 관리하게 됩니다.&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;894&quot; data-origin-height=&quot;101&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EEM5E/btrQlqG6Jd6/s6uWnW3Nm5EhJnzqjpCJzk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EEM5E/btrQlqG6Jd6/s6uWnW3Nm5EhJnzqjpCJzk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EEM5E/btrQlqG6Jd6/s6uWnW3Nm5EhJnzqjpCJzk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEEM5E%2FbtrQlqG6Jd6%2Fs6uWnW3Nm5EhJnzqjpCJzk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;894&quot; height=&quot;101&quot; data-origin-width=&quot;894&quot; data-origin-height=&quot;101&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;1096&quot; data-origin-height=&quot;351&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cewVDG/btrQjmTtUsE/konzK4ivzZ5Mg6lNeGzv7k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cewVDG/btrQjmTtUsE/konzK4ivzZ5Mg6lNeGzv7k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cewVDG/btrQjmTtUsE/konzK4ivzZ5Mg6lNeGzv7k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcewVDG%2FbtrQjmTtUsE%2FkonzK4ivzZ5Mg6lNeGzv7k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1096&quot; height=&quot;351&quot; data-origin-width=&quot;1096&quot; data-origin-height=&quot;351&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마찬가지로 Test 진행시에 수많은 오류 메시지를 볼 수 있었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1530&quot; data-origin-height=&quot;297&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kzP2L/btrQig68gsT/vLH3wr3PlTImEA2k80Fu9k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kzP2L/btrQig68gsT/vLH3wr3PlTImEA2k80Fu9k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kzP2L/btrQig68gsT/vLH3wr3PlTImEA2k80Fu9k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkzP2L%2FbtrQig68gsT%2FvLH3wr3PlTImEA2k80Fu9k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1530&quot; height=&quot;297&quot; data-origin-width=&quot;1530&quot; data-origin-height=&quot;297&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내용은 version 충돌에 의한 OptimisticLock 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;오류에 대한 처리를 따로 가져가지 않은 결과 데이터베이스에는 23 개의 신청내역 (대기 + 신청)만 저장되게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 동시에 여러 요청을 처리하게 된다면 낙관적락은 오류를 피하기 힘들어 보입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 어플리케이션 레벨에서 이 오류에 대한 처리를 정의할 수 있는 점은 장점이자 단점입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;788&quot; data-origin-height=&quot;427&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cQdGlG/btrQjm64VLy/9vgug2U14FUnWJZvkHUlnK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cQdGlG/btrQjm64VLy/9vgug2U14FUnWJZvkHUlnK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cQdGlG/btrQjm64VLy/9vgug2U14FUnWJZvkHUlnK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcQdGlG%2FbtrQjm64VLy%2F9vgug2U14FUnWJZvkHUlnK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;427&quot; data-origin-width=&quot;788&quot; data-origin-height=&quot;427&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 비관적 락과 낙관적 락중에 어떠한 것을 선택해야 할까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트래픽이 몰리지 않는 상황이거나 계좌나 선착순 같은 중요한 로직들 같은 경우에는 비관적락을 사용하는 것이 좋다고 생각합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 베이스는 서버와는 달리 고비용의 자원이고 비관적락의 경우에는 데이터베이스의 자원을 사용하게 됩니다. 동시성이슈가 별로 없는 상황에서 또한 락을 잡게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 어플리케이션의 상황에 맞게 락의 종류를 고르는 것을 권장합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저의 프로젝트의 경우에는 선착순에 대한 로직이지만, 트래픽이 몰리는 상황이 전혀 아니였고, 다른 서비스 어플리케이션 들이 하나의 데이터베이스를 사용하는 상황이였기 때문에 데이터베이스에 대한 자원의 가치가 더 높았습니다.&lt;/p&gt;
&lt;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;물론 이러한 상황속에서 좋은 방법으로 분산락의 방법이 있기는 하나 redis 에 대한 인프라 비용과 기술 내용 미숙지로 차차 도입을 할 예정입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;++ 추가)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근에 좋은 기회로 이 부분에 대해서 의구심이 생기는 주제가 주어졌습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 포스팅 기준으로는 h2가 commited read 기준으로 포스팅을 작성했는데요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 mysql 같은 repeatable read 의 격리 수준의 DB에 대해서 update 상황이 발생하면 어떤 자료에 대해 update 를 하게 될까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가로 설명을 더 하자면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;commited read 의 경우 다른 트랜잭션에서 update 된 내용을 읽게 되고 비관적락에 대해서 where version 을 통해 update 여부를 확인하여 구분을 할 수 있습니다. 하지만 repeatable read 라면 ? 다른 트랜잭션에서 update 된 내용을 알지 못하고 where version = ? 이 항상 true 이게 되므로 항상 update 를 진행 할 수 있겠네요? 뭔가 이상합니다&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;다시한번 실험을 통해 알아 보겠습니다. datasource 를 h2 에서 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;/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;948&quot; data-origin-height=&quot;274&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dsum9I/btrQ3SwNqhe/N4NEkBCfJUryGa7wKfIh2K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dsum9I/btrQ3SwNqhe/N4NEkBCfJUryGa7wKfIh2K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dsum9I/btrQ3SwNqhe/N4NEkBCfJUryGa7wKfIh2K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdsum9I%2FbtrQ3SwNqhe%2FN4NEkBCfJUryGa7wKfIh2K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;948&quot; height=&quot;274&quot; data-origin-width=&quot;948&quot; data-origin-height=&quot;274&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;1418&quot; data-origin-height=&quot;620&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nVllP/btrQ0RezgBk/EhlIqA233Xo1KOJedob1mk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nVllP/btrQ0RezgBk/EhlIqA233Xo1KOJedob1mk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nVllP/btrQ0RezgBk/EhlIqA233Xo1KOJedob1mk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnVllP%2FbtrQ0RezgBk%2FEhlIqA233Xo1KOJedob1mk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1418&quot; height=&quot;620&quot; data-origin-width=&quot;1418&quot; data-origin-height=&quot;620&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오류가 낙관적락에 대한 exception 이 아닌 deadLock 과 관련된 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;왜 다른 결과가 나오게 된 것일까? MySql 과 h2 데이터 베이스 사이에 락 관련하여 차이로 인해 발생되는 결과 였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 h2 에서의 낙관적 락에서의 사이클은 흐름은 다음과 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 설명회 내용 조회 (lock X)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 해당 설명회에 대한 대기/신청 을 위한 row 데이터 삽입&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. 설명회를 업데이트 하기 위해서 update 를 위한 설명회 row에 대한 lock 점유 (sql : update)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. update 하면서 version 을 체킹 하게 되는데 read - commited 이므로 이전에 커밋된 내용이 있으면 update 반영이 0이므로 해당 내용을 통해 optimistic lock 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;그렇다면 mysql 에서는 왜 deadLock 이 터질까?&lt;/p&gt;
&lt;p data-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. 설명회 내용 조회 (lock X)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 해당 설명회에 대한 대기/신청 을 위한 row 데이터 삽입 =&amp;gt; mysql 에서는 참조되는 row 즉 설명회에 대한 shared lock 이 걸림&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. 설명회를 업데이트 하기 위해서 update 를 위한 설명회 row에 대한 x - lock 점유 (sql : update)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. update 하면서 version 을 체킹 하게 되는데 repeated commit 입장에서 ...&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;=&amp;gt; 이부분은 공식적인 문서로는 확인 할 수 없으나 추측하기로는 mysql 의 경우 inno DB(스토리지 엔진) 에서 undo 레코드 읽기를 통한 repeatable read 를 보장합니다. 즉 select 에 대해서는 undo 레코드를 update 에 대해서는 undo 레코드에 대해서가 아닌 실제 데이터에 대한 update 를 진행하려고해서 해당 설명회를 update 못하고 맞는 version 을 찾지못해 update 를 못하는 특성이 나오는 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 inno DB 에서 sql update 문을 처리 할 때는 undo 영역이 아닌 스토리지 영역을 update 하기 때문에 version controll 이 올바르게 유지 되는 것 같습니다 (추측입니다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가적으로 deadLock이 왜 발생하는 지 보겠습니다. 여기서 주의 깊게 봐야할 것은 같은 transaction 안에서는 s-lock 과 x-lock 의 충돌이 발생하지 않습니다. 추가적으로 insert 문을 진행 할 때 참조 되는 row 에 locking 이 걸린다는 것을 주의 깊게 봐야 합니다. 고로 여러 트랜잭션간에 s-lock을 얻는 과정과 update 에 대한 x - lock 얻기가 맞물려 dead lock 이 발생하고 예외가 발생하는 것입니다.&lt;/p&gt;</description>
      <category>개발일지</category>
      <author>땅속 디그다</author>
      <guid isPermaLink="true">https://digda.tistory.com/41</guid>
      <comments>https://digda.tistory.com/41#entry41comment</comments>
      <pubDate>Thu, 3 Nov 2022 17:40:55 +0900</pubDate>
    </item>
    <item>
      <title>다형성과 캡슐화, Spring DI 를 사용해 유연한 정책 변화 만들기</title>
      <link>https://digda.tistory.com/40</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;기존 프로젝트의 요구 사항이 바뀌어 짐에 따라서 이미지를 저장하는 정책이 변경되는 이슈가 발생했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트에는 이미지를 저장, 변경하는 도메인들이 존재했고 프로필 이미지를 업데이트 하거나, 정보에 관련된 이미지를 업데이트 하는 서비스를 제공 중 이였습니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 추가로 발생할지 모르는 정책변경과 기존 정책의 활용 가능성으로 인해 imageService를 인터페이스로 공통화 하고 이미지를 사용하는 domain들을 공통 처리를 하는 과정을 작성해 보겠습니다.&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;&lt;b&gt;1. 이미지 사용 도메인들을 다형성을 사용하여 공통화&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Q) 제네릭을 사용하면 안되는 가?&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A) imageService를 bean에 올려놓고 사용할 것이기 때문에 이는 불가 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;b&gt;추가적으로 이미지 서비스를 사용하지 않는 도메인들이 이미지 서비스를 사용하려고 한다면 컴파일 단계에서 에러를 잡아낼 수 있습니다.&lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p 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;612&quot; data-origin-height=&quot;277&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vRBq0/btrQeudbXDr/m08epB0ijyyaYY9OMJHohk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vRBq0/btrQeudbXDr/m08epB0ijyyaYY9OMJHohk/img.png&quot; data-alt=&quot;BaseImageEntity 로 공통화&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vRBq0/btrQeudbXDr/m08epB0ijyyaYY9OMJHohk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvRBq0%2FbtrQeudbXDr%2Fm08epB0ijyyaYY9OMJHohk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;277&quot; data-origin-width=&quot;612&quot; data-origin-height=&quot;277&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;BaseImageEntity 로 공통화&lt;/figcaption&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;2. Spring 컨테이너를 통한 DI를 활용하여 imageService에 의존하는 클래스들이 코드 변경없이 유연하게 정책을 변경 할 수 있도록 만들자.&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가지 입니다. Local Server PC 에 저장, Amazon S3 에 저장.&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;600&quot; data-origin-height=&quot;296&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dvvCQc/btrQd7WNGp0/fxeUIciABXdjDnMSpguHF0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dvvCQc/btrQd7WNGp0/fxeUIciABXdjDnMSpguHF0/img.png&quot; data-alt=&quot;이미지 서비스 정책&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dvvCQc/btrQd7WNGp0/fxeUIciABXdjDnMSpguHF0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdvvCQc%2FbtrQd7WNGp0%2FfxeUIciABXdjDnMSpguHF0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;600&quot; height=&quot;296&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;296&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;이미지 서비스 정책&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 ImageService Interface 를 통해 2가지를 공통적으로 묶어 보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ImageService 에서 제공해야하는 내용은 다음과 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. Profile 사진에 대한 변경, 저장 (바이너리 파일을 클라이언트에서 받은 후 서버 or S3에 저장)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. Info 사진에 대한 변경, 저장 (바이너리 파일을 클라이언트에서 받은 후 서버 or S3에 저장)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. Profile 사진 가져오기 (URL 반환)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4. Info 사진 가져오기 (URL 반환)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 1, 2 번에 대해 클라이언트에서 바로 S3에 저장을 하면 되지 않느냐? 라고 의구심이 생길 수 있는데, domain 의 이미지 URL 을 데이터베이스에서 관리를 해야하고, S3의 버킷과 키를 정하는 과정이 Server 의 정책에 따라서 정해지는게 맞다고 생각을 하여 서버에서 바이너리 파일을 직접 받아 수행했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 Interface 를 살펴보면 다음과 같아 집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1667390482225&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface ImageService {
    String saveInfoImages(List&amp;lt;MultipartFile&amp;gt; images, BaseImageEntity entity);
    String saveProfileImage(MultipartFile image, BaseImageEntity entity);
    String getProfileImage(BaseImageEntity entity);
    List&amp;lt;String&amp;gt; getInfoImages(BaseImageEntity entity);
}&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;여기서 BaseImageEntity 는 이미지 서비스를 사용하는 domain 들에 대한 부모클래스로 이미지 서비스를 지원하지 않는 도메인들이 이미지 서비스를 사용하지 못하도록 컴파일 단계에서 잡아주게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA를 사용하기 때문에 @MappedBySuperClass 를 사용해 줍시다.&lt;/p&gt;
&lt;pre id=&quot;code_1667390699854&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Getter
@MappedSuperclass
public class BaseImageEntity {

    @Lob
    protected String infoImagePath;
    protected String profileImagePath;
    protected Integer imgCnt;

    public void updateInfoImagePath(Integer imgCnt, String infoImagePath) {
        this.imgCnt = imgCnt;
        this.infoImagePath = infoImagePath;
    }

    public void updateProfileImagePath(String profileImagePath) {
        this.profileImagePath = profileImagePath;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미지 서비스를 사용하려고 하는 도메인들은 모두 이 BaseImageEntity 클래스를 사용하게 됩니다. 현재 BaseImageEntity 는 일반 클래스로 작성되어 있으나 해당 클래스는 독자적으로 사용할 일이 없으므로 추상클래스를 사용하는 것을 권장 드립니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 Local Image Service와 S3 Image Service에 대한 ImageService 구현체를 만들어 보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 S3 를 사용하기 위해서는 Amazon S3 에서 제공하는 AmazonS3Client 를 통해서 이미지 저장을 실행하게 되므로 해당 의존성을 추가합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;// AWS S3 사용을 위한 의존성 추가 Spring-Cloud-AWS 추가&lt;br /&gt;implementation 'io.awspring.cloud:spring-cloud-starter-aws:2.3.1'&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;추가적으로 S3에 사용되는 Bucket 과 key 값들을 세팅하기 위해 application-s3.yml 을 생성하고, application.yml 에 import 를 해줍니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;708&quot; data-origin-height=&quot;248&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bxZwB3/btrQeObboSX/DVtPlm4lKjKhk5d9KL4pgK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bxZwB3/btrQeObboSX/DVtPlm4lKjKhk5d9KL4pgK/img.png&quot; data-alt=&quot;application.yml&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bxZwB3/btrQeObboSX/DVtPlm4lKjKhk5d9KL4pgK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbxZwB3%2FbtrQeObboSX%2FDVtPlm4lKjKhk5d9KL4pgK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;105&quot; data-origin-width=&quot;708&quot; data-origin-height=&quot;248&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;application.yml&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1667395810091&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;cloud:
  aws:
    credentials:
      accessKey: ???
      secretKey: ???
    s3:
      bucket: #버킷 이름
    region:
      static: #지역 이름
    stack:
      auto: false
    prefix: # url 정보&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;추가적으로 AmazonS3Client 를 Bean 등록을 해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1667395940994&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
public class S3Config {
    @Bean
    public AmazonS3Client amazonS3Client( @Value(&quot;${cloud.aws.credentials.accessKey}&quot;) String accessKey,
                                          @Value(&quot;${cloud.aws.credentials.secretKey}&quot;) String secretKey,
                                          @Value(&quot;${cloud.aws.region.static}&quot;) String region) {
        BasicAWSCredentials awsCreds = new BasicAWSCredentials(accessKey, secretKey);
        return (AmazonS3Client) AmazonS3ClientBuilder.standard()
                .withRegion(region)
                .withCredentials(new AWSStaticCredentialsProvider(awsCreds))
                .build();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 S3ImageService를 개발 해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;bucket 설정이외에도, 프로젝트에서는 이미지 저장에 대한 유지보수를 위해 domain 별로 key(도메인 이름) 를 나누고, id를 통해 추가적으로 구분을 하게 됩니다. 다음과 같은 구성입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;S3 url ------ UserProfile --- id(1)&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;# UserId 가 1 인 유저의 ProfileImage&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;|--- id(2)&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;|--- ...&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;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;|- UserInfo----&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; |- id(1) --- image1&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;# UserId가 1인 유저의 정보 사진들 모음&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;|--- image2&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;| --- ...&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; | - id(2) --- image1&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 domain 클래스의 이름, id 값이 필요하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;S3 구현체의 image 저장 부분을 간단하게 살펴봅시다.&lt;/p&gt;
&lt;pre id=&quot;code_1667396726779&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RequiredArgsConstructor
@Service
@Primary
public class S3ImageService implements ImageService {

    @Value(&quot;${cloud.aws.s3.bucket}&quot;)
    private String bucket;

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

    private final AmazonS3Client amazonS3Client;

    public String saveProfileImage(MultipartFile image, BaseImageEntity entity) {
        // null 이거나 비어있다면 return
        if (image == null || image.isEmpty()) {
            entity.updateProfileImagePath(null);
            return null;
        }
        // image fullpath 완성
        String destPath = getProfileDestPath(image, entity);
        // 이미지 저장 로직 + entity 업데이트 추가 작업 필요
        saveImage(image, destPath);
        updateProfileImagePath(entity, destPath);
        return null;
    }

    private void saveImage(MultipartFile image, String destPath) {
        ObjectMetadata objectMetadata = new ObjectMetadata();
        objectMetadata.setContentLength(image.getSize());
        objectMetadata.setContentType(image.getContentType());
        try (InputStream inputStream = image.getInputStream()) {
            amazonS3Client.putObject(new PutObjectRequest(bucket, destPath, inputStream, objectMetadata)
//                    .withCannedAcl(CannedAccessControlList.PublicRead)
            );
        } catch (IOException e) {
            throw new ImageException(ImageErrorResult.IMAGE_SAVE_FAILED);
        }
    }
    
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;getProfileDestPath 를 통해 ProfileImage에 대한 full Url Path 를 완성 시키는 것을 확인해 볼 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가령 UserId 1 의 ProfileImage를 저장하게 될 경우&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;https://xxx.s3.ap-northeast-2.amazonaws.com/UserProfile/1의 Full path 가 완성되는 것을 확인 할 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;저의 경우에 BaseImageEntity 의 다형성과 java Reflection 을 활용하여 Domain 에 대한 이름 추출과 Id 값에 대한 추출을 진행 하였습니다.&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;java Reflection 을 사용할 경우 컴파일 시점에 오류를 잡을 수 없고 런타임시점에 클래스에 대한 메타 데이터를 가져오므로 신중하게 결정이 필요 할 듯 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필요 정보를 추출하는 abstractEntityId 와 abstractEntityName 매서드를 살펴봅시다.&lt;/p&gt;
&lt;pre id=&quot;code_1667397287670&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class S3ImageService implements ImageService {

    protected Long abstractEntityId(BaseImageEntity entity) {
        try {
            Class&amp;lt;?&amp;gt; clazz = entity.getClass();
            Field[] declaredFields = clazz.getDeclaredFields();
            while(true) {
                for (Field field : declaredFields) {
                    Annotation annotation = field.getAnnotation(Id.class);
                    if (annotation != null) {
                        field.setAccessible(true);
                        Long entityId = (Long) field.get(entity);
                        return entityId;
                    }
                }
                clazz = clazz.getSuperclass();
                declaredFields = clazz.getDeclaredFields();
            }
        } catch (IllegalAccessException exception) {
            throw new ImageException(ImageErrorResult.IMAGE_ANALYZE_FAILED);
        }
    }

    /**
     * 엔티티 class 이름 추출
     */
    protected String abstractEntityName(Class&amp;lt;? extends BaseImageEntity&amp;gt; clazz) {
        String absoluteName = clazz.getName();
        String[] split = absoluteName.split(&quot;[.]&quot;);
        String entityName = split[split.length - 1];
        return entityName.toLowerCase();
    }

    protected String getProfileFolder(BaseImageEntity entity) {
        String className = abstractEntityName(entity.getClass());
        String folder = className + &quot;Profile&quot; + '/';
        return folder;
    }

    public String getProfileDestPath(MultipartFile multipartFile, BaseImageEntity entity) {
        String profileFolderPath = getProfileFolder(entity);
        Long entityId = abstractEntityId(entity);
        String ext = extractExt(multipartFile.getOriginalFilename());
        return profileFolderPath + entityId + '.' + ext;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LocalImageService 또한 해당 정책에 맞게 구현을 하였으며, 이제 Spring DI 를 이용하여 ImageService를 사용하는 클래스들의 변경 없이 imageService를 제공할 수 있게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;S3ImageService 에 @Primary 를 명시하게 되었으므로 모든 ImageService 의존 객체들은 S3ImageService를 이용하게 됩니다.&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;1738&quot; data-origin-height=&quot;404&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eOX2wy/btrQfDG2RE8/GETJTCzSoSkf45TdHZQgoK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eOX2wy/btrQfDG2RE8/GETJTCzSoSkf45TdHZQgoK/img.png&quot; data-alt=&quot;post 의 ImageService 사용&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eOX2wy/btrQfDG2RE8/GETJTCzSoSkf45TdHZQgoK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeOX2wy%2FbtrQfDG2RE8%2FGETJTCzSoSkf45TdHZQgoK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1738&quot; height=&quot;404&quot; data-origin-width=&quot;1738&quot; data-origin-height=&quot;404&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;post 의 ImageService 사용&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제대로 S3ImageService를 의존하고 있는지 살펴 보겠습니다.&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;2038&quot; data-origin-height=&quot;178&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/R9Jwc/btrQeVamaAe/ubXkT3emk7XZssn2ZOjjOk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/R9Jwc/btrQeVamaAe/ubXkT3emk7XZssn2ZOjjOk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/R9Jwc/btrQeVamaAe/ubXkT3emk7XZssn2ZOjjOk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FR9Jwc%2FbtrQeVamaAe%2FubXkT3emk7XZssn2ZOjjOk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2038&quot; height=&quot;178&quot; data-origin-width=&quot;2038&quot; data-origin-height=&quot;178&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;</description>
      <category>개발일지</category>
      <author>땅속 디그다</author>
      <guid isPermaLink="true">https://digda.tistory.com/40</guid>
      <comments>https://digda.tistory.com/40#entry40comment</comments>
      <pubDate>Wed, 2 Nov 2022 23:10:01 +0900</pubDate>
    </item>
    <item>
      <title>[Spring Security + AOP] 로그를 찍어보자 - 로그인 인증 (2)</title>
      <link>https://digda.tistory.com/39</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://digda.tistory.com/38&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://digda.tistory.com/38&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1667291548159&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Spring Security + AOP] 로그를 찍어보자 - 로그인 인증 (1)&quot; data-og-description=&quot;https://digda.tistory.com/36 Spring Security 흐름 Spring Security Stream 스프링 시큐리티의 제일 중요한 포인트는 인증(Authentication) 과 허가 (Authorization)이다. 인증 - '누구인지 증명하는 과정' 인가(허가) - '권한&quot; data-og-host=&quot;digda.tistory.com&quot; data-og-source-url=&quot;https://digda.tistory.com/38&quot; data-og-url=&quot;https://digda.tistory.com/38&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/G6tV2/hyQpD1b8vO/XsTfCmKYx3inzuWFgggEYK/img.png?width=721&amp;amp;height=510&amp;amp;face=0_0_721_510,https://scrap.kakaocdn.net/dn/dtueEJ/hyQqWEO10S/DWO6DZfKcxOh2K0ucKi4q0/img.png?width=721&amp;amp;height=510&amp;amp;face=0_0_721_510,https://scrap.kakaocdn.net/dn/dd9LFh/hyQpsk4eFv/BGXklnjKDrEHwd7lJbPXXk/img.png?width=721&amp;amp;height=510&amp;amp;face=0_0_721_510&quot;&gt;&lt;a href=&quot;https://digda.tistory.com/38&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://digda.tistory.com/38&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/G6tV2/hyQpD1b8vO/XsTfCmKYx3inzuWFgggEYK/img.png?width=721&amp;amp;height=510&amp;amp;face=0_0_721_510,https://scrap.kakaocdn.net/dn/dtueEJ/hyQqWEO10S/DWO6DZfKcxOh2K0ucKi4q0/img.png?width=721&amp;amp;height=510&amp;amp;face=0_0_721_510,https://scrap.kakaocdn.net/dn/dd9LFh/hyQpsk4eFv/BGXklnjKDrEHwd7lJbPXXk/img.png?width=721&amp;amp;height=510&amp;amp;face=0_0_721_510');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[Spring Security + AOP] 로그를 찍어보자 - 로그인 인증 (1)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;https://digda.tistory.com/36 Spring Security 흐름 Spring Security Stream 스프링 시큐리티의 제일 중요한 포인트는 인증(Authentication) 과 허가 (Authorization)이다. 인증 - '누구인지 증명하는 과정' 인가(허가) - '권한&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;digda.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당&amp;nbsp;예제의&amp;nbsp;github&amp;nbsp;:&amp;nbsp;&lt;a href=&quot;https://github.com/digda5624/spring_security_study&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/digda5624/spring_security_study&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1667291566659&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - digda5624/spring_security_study: 서버 로깅 작업을 위한 스프링 시큐리티 예제 + 로깅 시스템 개발&quot; data-og-description=&quot;서버 로깅 작업을 위한 스프링 시큐리티 예제 + 로깅 시스템 개발. Contribute to digda5624/spring_security_study development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/digda5624/spring_security_study&quot; data-og-url=&quot;https://github.com/digda5624/spring_security_study&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/2NeVT/hyQqSbk5o1/1X87QuaMApGETd45VFblI1/img.png?width=1200&amp;amp;height=600&amp;amp;face=931_183_1000_258&quot;&gt;&lt;a href=&quot;https://github.com/digda5624/spring_security_study&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/digda5624/spring_security_study&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/2NeVT/hyQqSbk5o1/1X87QuaMApGETd45VFblI1/img.png?width=1200&amp;amp;height=600&amp;amp;face=931_183_1000_258');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - digda5624/spring_security_study: 서버 로깅 작업을 위한 스프링 시큐리티 예제 + 로깅 시스템 개발&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;서버 로깅 작업을 위한 스프링 시큐리티 예제 + 로깅 시스템 개발. Contribute to digda5624/spring_security_study development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;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;인증에 대한 성공을 마쳤으므로 JWT 를 발급해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저 같은 경우에는 Spring Security Context 객체를 통해서 JWT 를 발급했는데요, Security Context 의 경우 Session Login 의 핵심 역할을 하게 되므로 잘 알아 두어야 한다고 생각합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잠깐 Security Context Holder 의 내용을 살펴 봅시다.&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;498&quot; data-origin-height=&quot;192&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/K4fzM/btrP5D9yNqi/kwk3ZWu4PaBnP8prsC79G0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/K4fzM/btrP5D9yNqi/kwk3ZWu4PaBnP8prsC79G0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/K4fzM/btrP5D9yNqi/kwk3ZWu4PaBnP8prsC79G0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FK4fzM%2FbtrP5D9yNqi%2Fkwk3ZWu4PaBnP8prsC79G0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;498&quot; height=&quot;192&quot; data-origin-width=&quot;498&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;SecurityContextHolder 는 말 그대로 Context 를 저장하는 저장소 개념의 역할을 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부에는 Authentication 정보를 포함하고 있다고 생각해보 무방해 보입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Security Context Holder 는 이 인증 객체에 대한 정보를 저장할 수 있는 정책이 여러가지가 존재하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 정책은 Thread Local 입니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 74px;&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: 50%; height: 20px;&quot;&gt;정책&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 20px;&quot;&gt;내용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 18px;&quot;&gt;MODE_THREADLOCAL&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 18px;&quot;&gt;스레드당 Context 객체를 할당. default&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 18px;&quot;&gt;MODE_INHERITABLETHREADLOCAL&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 18px;&quot;&gt;메인 스레드와 자식 스레드에 관하여 동일한 Context 유지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 18px;&quot;&gt;MODE_GLOBAL&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 18px;&quot;&gt;Memory 에 단 하나의 Context 만을 가지고 있음&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예제에서는 기본적으로 스레드 로컬 전략을 사용하고 있고, JWT의 경우에 StateLess 한 성질을 목표로 만들었기 때문에 별도로 SecurityContext 에 인증 객체를 넣을 필요는 없습니다. (Login 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;현재 핵심은 인증 완료된 객체를 가지고 JWT 를 만들어 클라이언트에게 발급하는 것 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 JWT 란 무엇일까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JWT 란 Json 포맷을 이용하여 사용자에 대한 속성을 저장하는 Claim 기반의 Web Token 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JWT 는 Header, PayLoad, Signature의 구조로 각 부분은 Base64 로 인코딩 되어 표현 됩니다.&lt;/p&gt;
&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://jwt.io/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://jwt.io/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1667368999830&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;JWT.IO&quot; data-og-description=&quot;JSON Web Tokens are an open, industry standard RFC 7519 method for representing claims securely between two parties.&quot; data-og-host=&quot;jwt.io&quot; data-og-source-url=&quot;https://jwt.io/&quot; data-og-url=&quot;http://jwt.io/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/CaG5n/hyQqSiHs6w/D1qjgQCjSj53OlYllCITU1/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/b0fiKO/hyQqX5oFc8/spq6ltGhZLnVelDujzM3v1/img.png?width=1024&amp;amp;height=512&amp;amp;face=0_0_1024_512&quot;&gt;&lt;a href=&quot;https://jwt.io/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://jwt.io/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/CaG5n/hyQqSiHs6w/D1qjgQCjSj53OlYllCITU1/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/b0fiKO/hyQqX5oFc8/spq6ltGhZLnVelDujzM3v1/img.png?width=1024&amp;amp;height=512&amp;amp;face=0_0_1024_512');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;JWT.IO&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;JSON Web Tokens are an open, industry standard RFC 7519 method for representing claims securely between two parties.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;jwt.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토큰의 헤더는 typ 와 alg 두 가지 정보로 구성이 되며 alg 의 경우 Signature 를 해싱하기 위한 알고리즘을 지정하는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토큰의 페이로드에는 토큰에서 사용할 정보의 조각들인 클레임(Claim) 이 담겨 있습니다. 클레임은 총 3가지로 나누어지며, Json(Key/Value) 형태로 다수의 정보를 넣을 수 있습니다.&lt;/p&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. Registered Claim - 토큰 정보를 표현하기 위해 이미 정해진 종류의 데이터&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. Public Claim - 사용자 정의 클레임, 공개용 정보를 위해 사용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. Private Claim - 사용자 정의 클레임, 서버와 클라이언트 사이에 임의로 지정한 정보를 저장&lt;/p&gt;
&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: #000000;&quot;&gt;서명은 위에서 만든 헤더와 페이로드의 값을 각각 BASE64로 인코딩하고, 인코딩한 값을 비밀 키를 이용해 헤더에서 정의한 알고리즘으로 해싱을 하고, 이 값을 다시 BASE64로 인코딩하여 생성합니다.&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;자 이제 LoginSuccess Handler 에서 JWT 를 발급하는 로직을 작성해 봅시다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;손쉬운 JWT 작성을 위해 gradle 에 라이브러리를 추가합니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;// JWT 의존성 추가&lt;br /&gt;implementation group: 'com.auth0', name: 'java-jwt', version: '3.18.3'&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JWT 와 관련된 설정 변수인 secretKey, 만료시간들의 경우에 application.yml 을 통해 따로 뺄 수 있지만 예제의 경우 static 변수를 통해 지정해 주었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1667369806857&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Slf4j
@Component
@RequiredArgsConstructor
public final class JwtUtils {

    private static final String secretKey = &quot;digda's secretKey&quot;;
    private static final int jwtExpirationInMs = 1800;

    public static String createAccessToken(Authentication authentication) {
        User user = (User) authentication.getPrincipal();
        return accessToken(user);
    }

    private static String accessToken(User user){
        Map&amp;lt;String, Object&amp;gt; payload = createClaims(user);

        return JWT.create()
                .withSubject(&quot;Digda_Test_token&quot;)
                .withExpiresAt(new Date(System.currentTimeMillis() + jwtExpirationInMs * 1000L))
                .withPayload(Map.of(&quot;userInfo&quot;, payload))
                .sign(Algorithm.HMAC512(secretKey));
    }

    private static Map&amp;lt;String, Object&amp;gt; createClaims(User user){
        Map&amp;lt;String, Object&amp;gt; claims = new HashMap&amp;lt;&amp;gt;();

        claims.put(&quot;id&quot;, user.getId());
        claims.put(&quot;name&quot;, user.getName());
        claims.put(&quot;role&quot;, user.getRole().toString());

        return claims;
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JWT 발급 유틸 클래스를 작성했으므로 LoginSuccessHandler 를 작성해 보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1667369879222&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class JwtLoginSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {

    /**
     * 들어온 Authentication 기반으로 토큰 설정하기
     */
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {

        // Authentication 기반으로 토큰생성
        System.out.println(&quot;request = &quot; + request);
        String accessToken = JwtUtils.createAccessToken(authentication);
        response.setHeader(&quot;Access-Token&quot;, accessToken);
        response.getWriter().println(&quot;login success&quot;);
    }
    
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 spring security 에 대한 설정 파일 작성을 통해서 인증에 대한 포스팅을 마쳐보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1667370048254&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
@EnableWebSecurity
@Import(UserDetailsServiceImpl.class)
public class JwtSecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http, AuthenticationManager authenticationManager) throws Exception {
        http.csrf().disable()
                // jwt 토큰을 사용하므로 세션 저장소에 Context를 저장할 필요없다.
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                // 스프링 시큐리티에서 제공하는 로그인 폼을 사용하지 않을 것이므로 disable
                .formLogin().disable()
                .addFilterBefore(jwtAuthenticationFilter(authenticationManager), LogoutFilter.class)
                .authenticationManager(authenticationManager)
                // 일단 현재에서는 Test를 위해서 모든 요청들에 대해 인가 검사를 하지 않게 적용했다.
                .authorizeRequests()
                .anyRequest()
                .permitAll();

        return http.build();
    }

    /**
     * jwtAuthenticationFilter Bean 등록
     */
    @Bean
    public JwtAuthenticationFilter jwtAuthenticationFilter(AuthenticationManager authenticationManager){
        JwtAuthenticationFilter filter = new JwtAuthenticationFilter(authenticationManager);
        filter.setAuthenticationSuccessHandler(new JwtLoginSuccessHandler());
        return filter;
    }

    /**
     * AuthenticationManager 등록
     */
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationProvider authenticationProvider){
        return new ProviderManager(authenticationProvider);
    }

    @Bean
    public AuthenticationProvider authenticationProvider(UserDetailsService userDetailsService){
        return new JwtAuthenticationProvider(userDetailsService);
    }
    
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HttpSecurity.build() 를 통해서 스프링 시큐리티의 필터 체인을 빌드하고 이를 빈으로 등록합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가적으로 스프링 시큐리티 체인의 경우 우선순위가 사용자 정의 필터보다 선순위를 가지게 되므로 주의가 필요합니다.&lt;/p&gt;</description>
      <category>개발일지</category>
      <author>땅속 디그다</author>
      <guid isPermaLink="true">https://digda.tistory.com/39</guid>
      <comments>https://digda.tistory.com/39#entry39comment</comments>
      <pubDate>Wed, 2 Nov 2022 15:23:00 +0900</pubDate>
    </item>
    <item>
      <title>[Spring Security + AOP] 로그를 찍어보자 - 로그인 인증 (1)</title>
      <link>https://digda.tistory.com/38</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a title=&quot;spring security 관련 글&quot; href=&quot;https://digda.tistory.com/36&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://digda.tistory.com/36&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1667179159133&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Spring Security 흐름&quot; data-og-description=&quot;Spring Security Stream 스프링 시큐리티의 제일 중요한 포인트는 인증(Authentication) 과 허가 (Authorization)이다. 인증 - '누구인지 증명하는 과정' 인가(허가) - '권한이 있는지 확인하는 과정' 모든 HTTP..&quot; data-og-host=&quot;digda.tistory.com&quot; data-og-source-url=&quot;https://digda.tistory.com/36&quot; data-og-url=&quot;https://digda.tistory.com/36&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/Lqnzc/hyQppHfGnz/gg6D4QgPDUdrKEQnm6kVfk/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/bI6qrv/hyQnV8PG3C/GGdvSuqES4lz4jZqEPRrOK/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/kgwGF/hyQnSYBGSp/G6OlCkIrymnxxuUNdjvpA0/img.png?width=1002&amp;amp;height=644&amp;amp;face=0_0_1002_644&quot;&gt;&lt;a href=&quot;https://digda.tistory.com/36&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://digda.tistory.com/36&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/Lqnzc/hyQppHfGnz/gg6D4QgPDUdrKEQnm6kVfk/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/bI6qrv/hyQnV8PG3C/GGdvSuqES4lz4jZqEPRrOK/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/kgwGF/hyQnSYBGSp/G6OlCkIrymnxxuUNdjvpA0/img.png?width=1002&amp;amp;height=644&amp;amp;face=0_0_1002_644');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Spring Security 흐름&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Spring Security Stream 스프링 시큐리티의 제일 중요한 포인트는 인증(Authentication) 과 허가 (Authorization)이다. 인증 - '누구인지 증명하는 과정' 인가(허가) - '권한이 있는지 확인하는 과정' 모든 HTTP..&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;digda.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 예제의 github : &lt;a href=&quot;https://github.com/digda5624/spring_security_study&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/digda5624/spring_security_study&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1667290682156&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - digda5624/spring_security_study: 서버 로깅 작업을 위한 스프링 시큐리티 예제 + 로깅 시스템 개발&quot; data-og-description=&quot;서버 로깅 작업을 위한 스프링 시큐리티 예제 + 로깅 시스템 개발. Contribute to digda5624/spring_security_study development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/digda5624/spring_security_study&quot; data-og-url=&quot;https://github.com/digda5624/spring_security_study&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/2NeVT/hyQqSbk5o1/1X87QuaMApGETd45VFblI1/img.png?width=1200&amp;amp;height=600&amp;amp;face=931_183_1000_258&quot;&gt;&lt;a href=&quot;https://github.com/digda5624/spring_security_study&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/digda5624/spring_security_study&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/2NeVT/hyQqSbk5o1/1X87QuaMApGETd45VFblI1/img.png?width=1200&amp;amp;height=600&amp;amp;face=931_183_1000_258');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - digda5624/spring_security_study: 서버 로깅 작업을 위한 스프링 시큐리티 예제 + 로깅 시스템 개발&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;서버 로깅 작업을 위한 스프링 시큐리티 예제 + 로깅 시스템 개발. Contribute to digda5624/spring_security_study development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의 login package 를 참고하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;포스팅기준으로는 JWT Login 을 작성했으나 예제에는 Session 로그인 또한 존재한다.&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;기존에 spring security 의 흐름을 살펴보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;spring security filter 의 요소들을 살펴보면 다음과 같은 구조이다.&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;721&quot; data-origin-height=&quot;510&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/H4gFm/btrPTkV9VGr/QAfRWjFx2KZf0UgtyNvK4K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/H4gFm/btrPTkV9VGr/QAfRWjFx2KZf0UgtyNvK4K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/H4gFm/btrPTkV9VGr/QAfRWjFx2KZf0UgtyNvK4K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FH4gFm%2FbtrPTkV9VGr%2FQAfRWjFx2KZf0UgtyNvK4K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;721&quot; height=&quot;510&quot; data-origin-width=&quot;721&quot; data-origin-height=&quot;510&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그림에서도 알 수 있듯이 인증과 관련된 내용은 USernamePasswordAuthenticationFilter에서 진행한다고 생각하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 우리는 AuthenticationManager, SuccessHandler 와 FailerHandler 를 재정의를 하면 되겠다.&lt;/p&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 security 의 경우 특별한 것이 있는 것이 아니고 요청에 따라서 요청 내용을 검사 가공해준다. 서블릿 필터와 기능이 동일하다는 것이다.&lt;/p&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 Security 를 사용하게 되면 default 로 Login Form 을 사용해서 인증을 하게 되는데 이를 해지하고 나만의 AuthenticationFilter 를 만들자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 인증 단계에서 핵심적으로 필요한 내용은 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 자신이 정한 로그인 정책에 맞는 Authentication Filter 작성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. Authentication Filter 가 인증을 위임할 Authentication Manager&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 실질적인 인증을 맡아줄 Authentication Provider (Manager 가 다시 한번 위임한다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 136px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 20px;&quot;&gt;클래스 (Class)&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 20px;&quot;&gt;역할&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 40px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 40px;&quot;&gt;AuthenticationFilter (인증 필터 관련)&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 40px;&quot;&gt;인증이 필요한 객체 생성 및 인증을 Authentication Manager 에게 위임&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 20px;&quot;&gt;AuthenticationManager&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 20px;&quot;&gt;실질적으로 인증을 담당하는 AuthenticationProvider 를 관리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 20px;&quot;&gt;AuthenticationProvider&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 20px;&quot;&gt;실질적인 인증을 담당&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;Authentication&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;Principal (의미론적)&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;쉽게 말해 User (인증을 받게 될 주체)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;credentials (의미론적)&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;쉽게 말해 비밀번호&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 18px;&quot;&gt;UserDetailsService&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 18px;&quot;&gt;UserInformation 가공을 위한 Supporting class&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 18px;&quot;&gt;UserDetails&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 18px;&quot;&gt;UserInformation 을 정의&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;기존에 프로젝트에서는 JWT 를 사용하기에 JWT방식의 로그인을 demo project에 적용했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JWT Authentication Filter 를 작성할 때 UsernamePasswordAuthenticationFilter 를 상속 받아 사용했고, spring security 에서 제공하는 다른 AuthenticationFilter 를 사용해도 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 AutheticationFilter 들의 핵심은 attemptAuthentication() 이고 이를 @override 하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드는 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1667183053545&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Slf4j
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

    public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
        setAuthenticationManager(authenticationManager);
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        // 인증을 위한 객체를 만든다.
        // 전송이 오면 AuthenticationFilter로 먼저 요청이 오게 되고, 아이디와 비밀번호를 기반으로 Token 을 발급해야한다.
        // 현재 principal 을 loginId 로 놓고 유효성을 검사하게 된다.
        log.info(&quot;JwtAuthenticationFilter {} {}&quot;, request.getMethod(), request.getRequestURL());
        ObjectMapper objectMapper = new ObjectMapper();
        try {
            final User user = objectMapper.readValue(request.getInputStream(), User.class);
            UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(user.getName(), user.getPassword());
            return getAuthenticationManager().authenticate(authRequest);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JWT 필터에서 하는 역할은 요청 request 의 Body를 분석하여 인증을 해야할 객체를 만들고 이 객체를 앞서 말한 Manager 에게 인증을 위임하게 된다. 해당 코드에서 살펴보면 UsernamePasswordAuthenticationToken 이 인증 객체이고 이를 Manager 에게 넘기는 것을 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심 부분은 getAuthenticationManager().authenticate() 부분이다. 또한 생성자에서 직접 set Manager 를 하게 되는데, 직접 설정하지 않으면 default 로 다른 매니저가 들어가게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 AuthenticationManager 의 경우에도 Provider 쪽에 인증을 위임하게 되지만 살펴볼 필요가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AuthentcationManager 는 interface 로 다양한 구현체가 있지만 사실상 ProviderManager 를 사용하게 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;778&quot; data-origin-height=&quot;159&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cH9jxq/btrP4XtkvjC/p66uBjUx4RqVhExSjpeoDK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cH9jxq/btrP4XtkvjC/p66uBjUx4RqVhExSjpeoDK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cH9jxq/btrP4XtkvjC/p66uBjUx4RqVhExSjpeoDK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcH9jxq%2FbtrP4XtkvjC%2Fp66uBjUx4RqVhExSjpeoDK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;778&quot; height=&quot;159&quot; data-origin-width=&quot;778&quot; data-origin-height=&quot;159&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 핵심은 ProviderManager 가 어떤식으로 AuthenticationProvider 에게 인증을 위임 받는지만 살피면 되겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ProviderManager의 authenticate 을 살펴보면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1667289339493&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {

	//...
	private List&amp;lt;AuthenticationProvider&amp;gt; providers = Collections.emptyList();

	public ProviderManager(AuthenticationProvider... providers) {
		this(Arrays.asList(providers), null);
	}
	//...
	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
		Class&amp;lt;? extends Authentication&amp;gt; toTest = authentication.getClass();
		// ...
		for (AuthenticationProvider provider : getProviders()) {
			// AuthenticationProvider 의 정책과 맞는지 확인 하는 부분
			if (!provider.supports(toTest)) {
				continue;
			}
			if (logger.isTraceEnabled()) {
				logger.trace(LogMessage.format(&quot;Authenticating request with %s (%d/%d)&quot;,
				provider.getClass().getSimpleName(), ++currentPosition, size));
			}
			try {
				// 위임하는 부분
				result = provider.authenticate(authentication);
				if (result != null) {
					copyDetails(authentication, result);
					break;
				}
			}
			// ...
		}
	}
}&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;즉 ProviderManager 는 그대로 사용하되 AuthenticationProvider 를 사용자 정의 하여 ProviderManager 에게 제공을 하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ProviderManager 의 경우 실질적인 인증을 담당하게 되므로 이곳에서 dataBase 접근을 통한 id, password 확인이 이루어 지게 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1667289819323&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RequiredArgsConstructor
public class JwtAuthenticationProvider implements AuthenticationProvider {

	// repository 접근 및 인증완료 객체 생성을 위한 userDetailsService
    private final UserDetailsService userDetailsService;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {

        String userName = (String) authentication.getPrincipal();
        String password = (String) authentication.getCredentials();
        MyUserDetails userDetails = (MyUserDetails) userDetailsService.loadUserByUsername(userName);

        if(!userDetails.getPassword().equals(password)){
            throw new RuntimeException(&quot;password 틀립니다&quot;);
        }

        return new UsernamePasswordAuthenticationToken(userDetails.getUser(), null, userDetails.getAuthorities());
    }

    @Override
    public boolean supports(Class&amp;lt;?&amp;gt; authentication) {
    	// 넘어올 인증 객체에 대한 Class 정보 true 이어야지 해당 클래스의 인증을 작업한다.
        return authentication.equals(UsernamePasswordAuthenticationToken.class);
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인자로 넘어온 Authentication 객체 즉, 인증을 받아야하는 객체에서 id(principal), password(credentials) 을 넘겨 받은 후&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비밀번호 아이디 확인 작업이 끝난 후 인증이 완료된 객체인 UserPasswordAuthenticationToken 을 발급하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UserDetailsService 의 경우 정말 말그대로 User에 대한 정보를 로딩을 Support 해주는 클래스이다. 살펴보면&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;352&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b5UTSW/btrP7NcH1hE/xWANJQUr3gDCacXyn6qOF1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b5UTSW/btrP7NcH1hE/xWANJQUr3gDCacXyn6qOF1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b5UTSW/btrP7NcH1hE/xWANJQUr3gDCacXyn6qOF1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb5UTSW%2FbtrP7NcH1hE%2FxWANJQUr3gDCacXyn6qOF1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;898&quot; height=&quot;352&quot; data-origin-width=&quot;898&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;다음과 같음을 알 수 있다. 우리는 UserRepository 를 접근해서 User 에 대한 내용을 UserDetails 객체에 담은 후 return 하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1667290852395&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RequiredArgsConstructor
@Component
public class UserDetailsServiceImpl implements UserDetailsService {

    private final UserRepository userRepository;

    @Override
    public MyUserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // Exception 을 Handling 할 수도 있지만 예제에서 생략
        User user = userRepository.findByName(username)
                .orElseThrow(() -&amp;gt; new RuntimeException(&quot;존재하지 않는 유저&quot;));
        return new MyUserDetails(user);
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로도 UserRepository 에서 User 조회후 해당 user 정보를 통해 UserDetails 를 만드는 것을 살필 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1667290796031&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/**
 * userDetails 를 바탕으로 인증을 하게 되므로 MyUserDetails를 구현하여야 한다.
 */
@Getter
@RequiredArgsConstructor
public class MyUserDetails implements UserDetails {

    private final User user;

    @Override
    public Collection&amp;lt;? extends GrantedAuthority&amp;gt; getAuthorities() {
        Collection&amp;lt;GrantedAuthority&amp;gt; authorities = new ArrayList&amp;lt;&amp;gt;();
        GrantedAuthority grantedAuthority = () -&amp;gt; user.getRole().toString();
        authorities.add(grantedAuthority);
        return authorities;
    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getName();
    }

    @Override
    public boolean isAccountNonExpired() {
        return false;
    }

    @Override
    public boolean isAccountNonLocked() {
        return false;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return false;
    }

    @Override
    public boolean isEnabled() {
        return false;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자 결국 이를 통해서 UserDetails 의 정보를 통해 인증을 완료하게 되면, 앞서 본 AuthenticationProvider 에서 인증이 완료된 객체인 UsernamePasswordAuthenticationToken을 발급하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 인증 정보를 통해서 Filter 에서는 Login Success Handler 를 호출 하게 되는데 이는 다음 포스팅에서 알아보자.&lt;/p&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 Security 의 인증 흐름은 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. Spring Security Filter Chain 에서 인증 필터(로그인 필터) 로의 접근&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 인증 필터는 인증을 AuthenticationManager 를 통해 AuthenticationProvider 에게 위임하고 인증이 필요한 객체를 생성해 넘김&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 넘어온 인증필요 객체를 UserDetailsService 를 통해 UserDetails 를 작성하고 Principal, Credentials 와 비교하여 올바른 유저인지 확인 한다. 올바른 유저가 맞다면 인증 완료 객체를 발급하고 다시 인증 필터에게 넘겨준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. Exception 이 없었다면 필터는 LoginSuccessHandler를 호출하고 실패하게 될 경우 별도의 Exception Handler 를 호출하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주의 할 점은 이 부분들은 서블릿 Filter 단에서 일어나는 작업이므로 Exception Handler 를 통한 Listening 은 불가하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 이런 저런 제약사항으로 인해서 팀원 이모씨께서는 Login 은 Filter를 사용하지 않았다...&lt;/p&gt;</description>
      <category>개발일지</category>
      <author>땅속 디그다</author>
      <guid isPermaLink="true">https://digda.tistory.com/38</guid>
      <comments>https://digda.tistory.com/38#entry38comment</comments>
      <pubDate>Tue, 1 Nov 2022 17:30:28 +0900</pubDate>
    </item>
    <item>
      <title>spring 복습을 하면서 정리 (아는 만큼 보인다)</title>
      <link>https://digda.tistory.com/37</link>
      <description>&lt;h1&gt;SOLID&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;SRP : 단일 책임 원칙&lt;/li&gt;
&lt;li&gt;OCP : 개방 - 폐쇄 원칙&lt;/li&gt;
&lt;li&gt;LSP : 리스코프 치환 원칙&lt;/li&gt;
&lt;li&gt;ISP : 인터페이스 분리 원칙&lt;/li&gt;
&lt;li&gt;DIP : 의존관계 역전 원칙&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;SRP&lt;/h2&gt;
&lt;p&gt;하나의 클래스는 하나의 책임만을 가져야 한다.&lt;/p&gt;
&lt;p&gt;하나의 책임이라는 것은 모호하다&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;클 수 있고, 작을 수 있다.&lt;/li&gt;
&lt;li&gt;문맥과 상황에 따라 다르다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;그렇다면 책임이라는 기준이 뭘까?&lt;/p&gt;
&lt;p&gt;중요한 기준은 &amp;quot;변경&amp;quot; =&amp;gt; 변경이 있을 때 파급 효과가 많다면 잘 따르지 못한 것이다.&lt;/p&gt;
&lt;h2&gt;OCP&lt;/h2&gt;
&lt;p&gt;소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있다.&lt;/p&gt;
&lt;p&gt;=&amp;gt; 인터페이스는 변경을 하지 않고 구현 클래스를 만드는 것은 OCP 원칙을 잘 지킨 것이라고 볼 수 있다.&lt;/p&gt;
&lt;p&gt;하지만 개발의 특성상 OCP 원칙을 지키는 것은 정말 어려운 일이다. 예를 들어 보자&lt;/p&gt;
&lt;p&gt;만약 MemberService에서 MemberRepository를 참조하고 사용하고 있다고 해보자 MemberRepository를 작성할 때,&lt;br&gt;우리는 다형성을 잘 지키고자 MemberRepository 라는 인터페이스를 만들었고 그 구현체 들로 JdbcRepository, JpaRepository를 작성했다.&lt;/p&gt;
&lt;p&gt;하지만 여기서 문제상황이 발생하게 된다. MemberService는 다음과 같이 구성된다.&lt;/p&gt;
&lt;p&gt;  MemberService&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class MemberService{

    // private final MemberRepositroy memberRepositroy = new JdbcRepository();
    private final MemberRepositroy memberRepositroy = new JpaRepository();

    // 정책을 변경하게 되면 클라이언트 코드 즉 MemberService 에 대한 코드가 수정이 되어야 하고 이것은 OCP 원칙을 어기는 것이다.
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;따라서 이문제를 해결하기 위해서&lt;/p&gt;
&lt;p&gt;객체를 생성하고, 연관관계를 맺어주는 별도의 조립, 설정자가 필요하다. =&amp;gt; 이로 인해 DI, IOC 가 필요하게 되는 것이다.&lt;/p&gt;
&lt;h2&gt;LSP&lt;/h2&gt;
&lt;p&gt;리스코프 치환 원칙&lt;/p&gt;
&lt;p&gt;프로그램 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀수 있어야 한다.&lt;/p&gt;
&lt;p&gt;다형성에서 하위 클래스는 인터페이스 규약을 다 지켜야 한다는 것, 다형성을 지원하기 위한 원칙이다.&lt;br&gt;인터페이스를 구현한 구현체를 믿고 사용하려면 이 원칙이 필요하다.&lt;/p&gt;
&lt;p&gt;자동차의 엑셀에 대한 interface 규약은 앞으로 가는 것인데 구현체에서 뒤로가는 구현을 해놓은 경우 LSP 원칙을 지키지 않은 것이다.&lt;/p&gt;
&lt;h2&gt;ISP&lt;/h2&gt;
&lt;p&gt;인터페이스 분리 원칙&lt;/p&gt;
&lt;p&gt;인터페이스 여러개가 범용 인터페이스 하나보다 낫다.&lt;/p&gt;
&lt;p&gt;예를 들면 회원이 주문을 하는 상황이 있다고 했을 때 주문과 회원에 대한 Service 인터페이스를 하나로 작성하고 모든 코드를&lt;br&gt;작성 MemberOrderService 로 작성할 수 있다. 하지만 Order과 Member 로 분리를 하게 되면 유지 보수가 좋아진다.&lt;/p&gt;
&lt;p&gt;인터페이스가 명확해지고 대체 가능성이 높아진다.&lt;/p&gt;
&lt;h2&gt;DIP&lt;/h2&gt;
&lt;p&gt;의존관계 역전 원칙&lt;/p&gt;
&lt;p&gt;추상화에 의존해야지, 구체화에 의존하면 안된다.&lt;/p&gt;
&lt;p&gt;예를 들어, MemberService 에서는 그저 인터페이스에 의존하고 Interface 의 내용만 알면 된다.&lt;/p&gt;
&lt;p&gt;  MemberService&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class MemberService{

    // private final MemberRepositroy memberRepositroy = new JdbcRepository();
    private final MemberRepositroy memberRepositroy = new JpaRepository();

    // 정책을 변경하게 되면 클라이언트 코드 즉 MemberService 에 대한 코드가 수정이 되어야 하고 이것은 OCP 원칙을 어기는 것이다.
    // 또한 MemberService는 MemberRepository 라는 인터페이스에도 의존하면서 특정 Repo 를 알아야 하므로 특정 구현체
    // 에도 의존하고 있다 따라서 DIP 원칙을 위배한다.
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;해당 SOLID 를 지키려면...&lt;/h2&gt;
&lt;p&gt;구성 코드와 실행 코드의 분리 즉 실행을 담당하는 코드와 역할을 배분시키는 코드를 분리 해야 한다.&lt;/p&gt;
&lt;p&gt;예를 들자면 배우들과 감독을 분리해야한다. 배우들이 상대 배우들의 역할은 배분시키는 것은 옳지 않다.&lt;/p&gt;
&lt;p&gt;위의 내용에서는 config 패키지를 통해서 분리작업이 일어났으며 실 사용 부분들은 나머지 클래스들에 속한다.&lt;/p&gt;
&lt;p&gt;실사용 부분들에는 interface를 사용한 코드가 좋고 interface를 사용하지 않아도 실행 코드 쪽에서 new 하여 객체를 새로이 생성하는&lt;br&gt;행위는 옳지 않다. new를 하고 객체의 참조를 넣어주는 것은 config 에서 담당한다.&lt;/p&gt;
&lt;h2&gt;spring은...&lt;/h2&gt;
&lt;p&gt;spring이 없었다면 개발자들은 config 정보들을 통해서 어플리케이션 실행을 config.메서드() 를 통해 실행 코드의 객체간 참조를&lt;br&gt;넣어주게 될 것이다. 하지만 이것은 상당히 번거로운 작업임은 틀림이 없다.&lt;/p&gt;
&lt;p&gt;따라서 spring은 개발자가 실행의 흐름을 제어하지 않도록 자신들이 실행 코드의 객체간 참조를 넣어주게 된다.&lt;/p&gt;
&lt;p&gt;이를 제어의 역전(IOC)라고 부른다. 따라서 spring은 IOC 컨테이너 이다.&lt;/p&gt;
&lt;p&gt;또한, 이런 객체간의 참조를 주입시키는 것을 특히 DI 라고 하고 요즘에는 spring을 DI 컨테이너 라고 많이 부르게 된다.&lt;/p&gt;
&lt;h2&gt;spring의 싱글톤&lt;/h2&gt;
&lt;p&gt;싱글톤은 자기 자신에 대한 인스턴스를 생성하고, getInstance()를 통해서 공유 객체를 반환하는 패턴이다.&lt;/p&gt;
&lt;p&gt;따라서 이 공유객체를 관리하는 데에서 문제가 생길 수 있다. 공유 객체 내에 자원에 대해서 주의를 해야 한다.&lt;br&gt;공유 객체에 대해서 stateless 한 상태를 유지해야 하고, thread 간의 동시성을 고려하여 stack 영역의 데이터를 건드리도록&lt;br&gt;프로그래머가 신경을 써야 한다.&lt;/p&gt;
&lt;p&gt;또한 @Bean 을 통해서 주입을 하는 경우 생성자 level 에서 여러개의 싱글톤 객체가 생성된다고 생각 할 수 있는데,&lt;br&gt;spring은 @Configuration 이 붙어 있는 클래스를 CGLIB 객체로 새로 만들어(프록시 활용) 다른 동작을 하게 한다.&lt;/p&gt;
&lt;p&gt;수도 코드로 보면 다음과 같아진다.&lt;/p&gt;
&lt;p&gt;  재정의된 Configuration class 의 내부 로직&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Bean
public MemberRepository memberRepository() {
        if (memoryMemberRepository가 이미 스프링 컨테이너에 등록되어 있으면?) { 
            return 스프링 컨테이너에서 찾아서 반환;
        } else { 
            //스프링 컨테이너에 없으면
        기존 로직을 호출해서 MemoryMemberRepository를 생성하고 스프링 컨테이너에 등록 return 반환
        } 
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;따라서 @Configuration 을 적용하지 않고 작성을 하게 되면 싱글톤을 보장하지 않게 된다는 점을 유의하자.&lt;/p&gt;
&lt;p&gt;실제로도 각 @Bean에 넣었던 내용들이 전부 출력되는 것을 알 수 있다.&lt;/p&gt;
&lt;h2&gt;@ComponentScan, @Component&lt;/h2&gt;
&lt;p&gt;간단하게 말해 기존 @Configuration + @Bean 조합을 =&amp;gt; @Component + @ComponentScan 조합으로 변경한다.&lt;/p&gt;
&lt;p&gt;클래스 단에 @Component 어노테이션만 달아주게 되면 된다. @ComponentScan 대상을 정할 수 있다.&lt;br&gt;지정을 하지 않게 되면 현재 패키지가 시작위치가 되고 하위 패키지를 뒤지게 된다.&lt;/p&gt;
&lt;p&gt;@Component, @Controller, @Service, @Repository, @Configuration 은 내부적으로 @Component 가 붙어있다.&lt;br&gt;&lt;br&gt;참고로 이 어노테이션이 특정 어노테이션을 들고 있는 것을 인식할 수 있는 것은 자바 언어가 지원하는 기능은 아니고, Spring 이 지원하는 기능이다.&lt;/p&gt;
&lt;p&gt;또한 @Repository 는 스프링 데이터 접근 계층으로 인식하고, 데이터 계층의 예외를 스프링 예외로 변환시켜준다.&lt;/p&gt;
&lt;p&gt;필드의 의존관계에 대해서 주입을 하게 되며 주입관련 내용으로는 생성자, 필드, setter 주입이 있다.&lt;/p&gt;
&lt;h2&gt;같은 빈이름으로 등록 -&amp;gt; 충돌&lt;/h2&gt;
&lt;p&gt;Conflict 가 난다.&lt;/p&gt;
&lt;p&gt;자동 빈등록(ComponentScan) + 수동 빈등록(@Bean) == overriding이 되어 버린다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;Overriding bean definition for bean &amp;#39;memoryMemberRepository&amp;#39; with a different definition&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;spring Boot 기준으로는 override 하지 않고 팅겨버린다.&lt;/p&gt;
&lt;h2&gt;spring 의 의존관계 주입 life-cycle&lt;/h2&gt;
&lt;p&gt;spring은 용빼는 재주가 있는 것이 아니다. 결국 spring-Container에 등록을 할 때 객체를 올리고 의존 관계를 주입하게 된다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;객체 생성 후 (스프링 컨테이너에 등록 후)&lt;/li&gt;
&lt;li&gt;의존 관계 주입&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;생성자 주입의 경우 1, 2번이 동시에 일어나게 된다. 필드 주입,  수정자 주입의 경우 1번후 2번이 일어나게 된다&lt;/p&gt;
&lt;p&gt;생성자 주입을 선택해라!&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;의존관계를 변경할 일이 없다. 불변해야한다. (대부분)&lt;/li&gt;
&lt;li&gt;Test 코드와의 관련성 Test 코드를 작성할 때 순수한 java 코드로 Test를 작성하고 싶다면... 후에 Mock 객체를 사용할 때도 생성자를 통한 test&lt;br&gt;target 설정이 편리하다. 물론 injectMock을 쓸때도 마찬가지&lt;/li&gt;
&lt;li&gt;final 키워드를 통한 컴파일 오류 잡아내기가 가능해진다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;@Qualifier tips&lt;/h2&gt;
&lt;p&gt;기존에는 Qualifier(value = &amp;quot;&amp;quot;) 형식으로 사용했지만 컴파일 시점에 매칭을 잡아내기 힘들다는 점이 있다.&lt;br&gt;만약 &amp;quot;mainDataBase&amp;quot; string 에 오타가 있다면? 따라서 spring에서 제공하는 어노테이션 커스텀을 사용하여 쉽게 해보자&lt;/p&gt;
&lt;p&gt;  @Qualifier custom&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Qualifier(&amp;quot;mainDiscountPolicy&amp;quot;)
public @interface MainDiscountPolicy {
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Collection 을 이용한 spring의 전략패턴 제공&lt;/h2&gt;
&lt;p&gt;동일 타입의 bean 객체를 가져오려고 하면 오류가 난다. NoUniqueBeanDefinitionException 기본적으로 어플리케이션에서&lt;br&gt;전략을 동적으로 교체하거나, 여러개를 가질 일이 거의 없기 때문에 대부분의 로직은 @Primary 혹은 @Qualifier 로 해결이 가능하다.&lt;br&gt;하지만 만약 여러개의 전략이 필요한 경우라면??&lt;/p&gt;
&lt;p&gt;  여러가지 전략 패턴을 지원하는 spring&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static class DiscountService{
        private final Map&amp;lt;String, DiscountPolicy&amp;gt; policyMap;
        private final List&amp;lt;DiscountPolicy&amp;gt; policies;

        @Autowired
        public DiscountService(Map&amp;lt;String, DiscountPolicy&amp;gt; policyMap, List&amp;lt;DiscountPolicy&amp;gt; policies) {
            this.policyMap = policyMap;
            this.policies = policies;
            System.out.println(&amp;quot;policyMap = &amp;quot; + policyMap);
            System.out.println(&amp;quot;policies = &amp;quot; + policies);
        }

        public int discount(Member member, int price, Strategy strategy){
            DiscountPolicy discountPolicy = policyMap.get(strategy.getPolicy());
            return discountPolicy.discount(member, price);
        }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;필드로 다양한 DiscountPolicy 전략을 가질 수 있는 것을 확인해 볼 수 있다.&lt;/p&gt;
&lt;p&gt;추가적으로 Strategy라는 Enum 타입 선언으로 깔끔하게 코드를 뽑아 낼 수 있다.&lt;/p&gt;
&lt;p&gt;  Strategy Enum&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Getter
@AllArgsConstructor
public enum Strategy {
    FIX_POLICY(&amp;quot;fixDiscountPolicy&amp;quot;),
    RATE_POLICY(&amp;quot;rateDiscountPolicy&amp;quot;);

    String policy;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;spring 앱을 개발할 때 나오는 개괄적인 bean 종류&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;업무 로직 빈 - 웹을 지원하는 컨트롤러, 핵심 비지지스 로직이 있는 서비스, 리포지토리 등이 모두 업무 로직&lt;/li&gt;
&lt;li&gt;기술 지원 빈 - 기술적인 문제나 공통 관심사(AOP)를 처리할 때 주로 사용&lt;/li&gt;
&lt;li&gt;비즈니스 로직이 다양한 전략을 가져야 할 경우&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;1번은 대부분 자동 빈등록, 2번은 기술을 명확하게 들어내야할 수록 편하기 때문에 수동빈 등록 권장&lt;br&gt;3번의 경우도 마찬가지로 전략에 대한 Config를 따로 빼내어서 전략들을 개괄적으로 보여주는 것이 편리하다.&lt;/p&gt;
&lt;p&gt;  전략에 대한 Config 명확하게&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&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;h2&gt;빈 생명주기 콜백&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;데이터베이스 커넥션은 어떻게 생성이 되는 걸까? =&amp;gt; 데이터베이스 커넥션 풀의 경우에 미리 커넥션을 만들고 할당해 놓는다&lt;/li&gt;
&lt;li&gt;네트워크 소캣을 미리 열어둘 수 있다면?&lt;/li&gt;
&lt;li&gt;어플리케이션 종료 시점에 사용하던 resources 들을 어떻게 release 시킬까? &lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;먼저 spring bean은 객체를 생성 -&amp;gt; 의존관계 주입의 순서를 따른다.&lt;/p&gt;
&lt;p&gt;  빈 생성과 의존관계 주입&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Configuration
static class BeanLifeCycle {

    @Bean
    public NetworkClient networkClient(){
        // spring 은 다음과 같은 의존관계 주입 순서를 가진다.

        // 1. 객체 생성
        NetworkClient networkClient = new NetworkClient();

        // 2. 의존관계 주입 완료 여기서는 setter 로 퉁치자 물론 예시가 부정확하지만 대략 이런 느낌이라고 생각하자.
        networkClient.setUrl(&amp;quot;http://digda5624/test&amp;quot;);
        return networkClient;
        }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;스프링 빈은 객체를 생성하고, 의존관계 주입이 다 끝난 다음에야 필요한 데이터를 사용할 수 있는 준비가 완료&lt;br&gt;따라서 초기화 작업은 의존관계 주입이 모두 완료되고 난 다음에 호출해야 한다.&lt;/p&gt;
&lt;p&gt;=&amp;gt; 이것을 어떻게 알 것인가?&lt;/p&gt;
&lt;p&gt;스프링은 의존관계 주입이 완료되면 스프링 빈에게 콜백 메서드를 통해서 초기화 시점을 알려주는 다양한 기능 제공(후에 빈 후처리기 라고 함)&lt;br&gt;또한 스프링 컨테이너가 종료되기 직전에 소멸 콜백을 준다. &lt;/p&gt;
&lt;h2&gt;용어 정리 해보기&lt;/h2&gt;
&lt;p&gt;여기서 참 재밌는 메서드 명이 나온다. 예전에 봤을 때는 몰랐던 afterPropertiesSet 이 그것이다.&lt;/p&gt;
&lt;p&gt;AfterPropertiesSet은 말 그대로 의존관계 주입이후를 의미하게 된다.&lt;/p&gt;
&lt;p&gt;코드 level 에서 살펴보자&lt;/p&gt;
&lt;p&gt;  AfterPropertiesSet, Destroy&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class NetworkClient implements InitializingBean, DisposableBean {

    private String url;

    public NetworkClient() {
        System.out.println(&amp;quot;생성자 호출, url = &amp;quot; + url);
        connect();
        call(&amp;quot;초기화 연결 메세지&amp;quot;);
    }

    public void setUrl(String url) {
        this.url = url;
    }

    // 서비스 시작시 호출
    public void connect() {
        System.out.println(&amp;quot;connect: &amp;quot; + url);
    }

    public void call(String message) {
        System.out.println(&amp;quot;call: &amp;quot; + url + &amp;quot; message = &amp;quot; + message);
    }

    // 서비스 종료시 호출
    public void disconnect(){
        System.out.println(&amp;quot;close = &amp;quot; + url);
    }

    @Override
    public void destroy() throws Exception {
        System.out.println(&amp;quot;destroy&amp;quot;);
        disconnect();
    }

    // spring bean 이 생성된 이후
    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println(&amp;quot;afterPropertiesSet&amp;quot;);
        connect();
        call(&amp;quot;초기화 연결 메시지&amp;quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;사실 @PostConstruct, @PreDestroy 쓰면 된다.&lt;/p&gt;
&lt;p&gt;추가 학습할 것 AutoClosable&lt;/p&gt;
&lt;h2&gt;빈 스코프&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;싱글톤&lt;/li&gt;
&lt;li&gt;프로토타입 =&amp;gt; 초기화 메서드는 호출 하지만 종료 콜백은 호출되지 않음(@Destroy 호출 X)&lt;/li&gt;
&lt;li&gt;request (Spring Web 관련)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;2, 3 의 경우 주입 시점에 객체가 생성되고 2번의 경우 스프링 빈에서 관리 되지 않는다. 3번의 경우 request, session, application&lt;br&gt;스코프가 존재한다.&lt;/p&gt;</description>
      <category>Spring</category>
      <author>땅속 디그다</author>
      <guid isPermaLink="true">https://digda.tistory.com/37</guid>
      <comments>https://digda.tistory.com/37#entry37comment</comments>
      <pubDate>Tue, 11 Oct 2022 02:16:49 +0900</pubDate>
    </item>
    <item>
      <title>Spring Security 흐름</title>
      <link>https://digda.tistory.com/36</link>
      <description>&lt;h1&gt;Spring Security Stream&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 시큐리티의 제일 중요한 포인트는 인증(Authentication) 과 허가 (Authorization)이다.&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;모든 HTTP 통신이 거쳐야하는 FilterChain&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;인증 필터 =&amp;gt; AuthenticationFilter&lt;/li&gt;
&lt;li&gt;인가 필터 =&amp;gt; FilterSecurityInterceptor&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AuthenticationFilter 의 AuthenticationManager 에서 인증을 위임&lt;br /&gt;FilterSecurityInterceptor 의 AccessDecisionManager 에서 인가를 위임&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Authentication 과정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 AuthenticationFilter 에서 인증이 실패 할 경우 (로그인 실패 할 경우)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AuthenticationFailureHandler 를 호출하여 후처리&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 성공 할 경우 AuthenticationSuccessHandler 를 통해서 후처리를 담당&lt;br /&gt;후처리 과정에서 Session 로그인을 진행 할 경우 세션 저장소에 SecurityContext를 저장하는 등의 과정을 수행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 만약 스프링 시큐리티의 Authentication 을 사용한다면 Filter 에서 인증을 처리 후에 결과 return 된다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인증을 하는 과정에는 주로 Username/Password를 사용하고 persistence level 에서 DB조회를 통한&lt;br /&gt;확인 절차를 거치게 되는데, 주로 USerDetails, UserDetailsService 를 implement 해서 사용하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://user-images.githubusercontent.com/69373314/192448016-f262310d-f5e5-4003-9e1e-2e9665d624be.png&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Authorization 과정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 FilterSecurityInterceptor 를 활용한 Authorization 을 사용하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인가에 실패하게 될 경우 FilterSecurityInterceptor 는 AccessDeniedException 을 throw 하게 되고, ExceptionTranslationFilter 에서 catch 하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인가에 대한 오류 후처리는 ExceptionTranslationFilter 에서 담당하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 오류필터 내부에는 후처리기인 AccessDeniedHandler 가 존재하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://user-images.githubusercontent.com/69373314/192447772-3ece95a7-8ca1-40ac-a0e1-554867fb3f6e.png&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;</description>
      <category>Spring</category>
      <author>땅속 디그다</author>
      <guid isPermaLink="true">https://digda.tistory.com/36</guid>
      <comments>https://digda.tistory.com/36#entry36comment</comments>
      <pubDate>Tue, 27 Sep 2022 15:22:50 +0900</pubDate>
    </item>
    <item>
      <title>트랜잭션 이해하기</title>
      <link>https://digda.tistory.com/35</link>
      <description>&lt;h1&gt;  트랜잭션&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  트랜잭션 알기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터베이스를 사용하는 이유?&lt;/p&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;transaction (commit 과 rollback)&lt;/li&gt;
&lt;li&gt;다른 서비스와의 데이터 공유&lt;/li&gt;
&lt;li&gt;동시성&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;트랜잭션의 ACID&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;원자성&lt;br /&gt;트랜잭션 내에서 실행한 작업들은 마치 하나의 작업인 것처럼 모두 성공하거나 모두 실패해야 한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; start=&quot;2&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;일관성&lt;br /&gt;모든 트랜잭션은 일관성 있는 데이터베이스 상태를 유지해야 한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; start=&quot;3&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;격리성&lt;br /&gt;동시에 실행되는 트랜잭션들이 서로에게 영향을 미치지 않도록 격리한다.&lt;br /&gt;transaction의 isolation level을 생각하면 된다.&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;트랜잭션 격리 수준 (Isolation level)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;READ UNCOMMITED(커밋되지 않은 읽기)&lt;/li&gt;
&lt;li&gt;READ COMMITTED(커밋된 읽기)&lt;/li&gt;
&lt;li&gt;REPEATABLE READ(반복 가능한 읽기)&lt;/li&gt;
&lt;li&gt;SERIALIZABLE(직렬화 가능)&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 JPA에서는 데이터베이스에 관계없이 1차캐시(영속성 컨텍스트)를 이용, Repeatable Read를 지원한다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; start=&quot;4&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;지속성&lt;br /&gt;트랜잭션을 성공적으로 끝내면 그 결과가 항상 기록되어야 한다. 중각에 시스템에 문제가 발생해도 데이터베이스 로그 등을&lt;br /&gt;사용 해서 성공한 트랜잭션 내용을 복구해야한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;들어가기전...&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DataBase의 세션 개념을 알아갈 필요가 있다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;커넥션을 맺고 난뒤 해당 커넥션에 맞게 데이터베이스 서버는 세션을 생성한다.&lt;/li&gt;
&lt;li&gt;커넥션을 통한 모든 요청은 각 세션을 통해서 실행된다.&lt;/li&gt;
&lt;li&gt;세션은 트랜잭션을 시작하고 커밋 또는 롤백을 통해 트랜잭션을 종료한다.&lt;/li&gt;
&lt;li&gt;여기서 세션별로 다시 테이블을 가진다고 생각하면 편하다. (트랜잭션 격리를 위함)&lt;/li&gt;
&lt;li&gt;쉽게 말하자면 2개의 세션이 있다고 가정했을 때 한쪽 세션에서 트랜잭션 시작후 insert, update, delete를 한다고 해서&lt;br /&gt;다른 세션에서 그 결과를 볼 수 있는 것이 아니다. commit을 해야 보인다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  락의 필요성 등장&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 대부분의 데이터베이스들은 select 시점에 commit 된 결과를 가져오는 READ COMMITTED 이상의 트랜잭션 레벨과&lt;br /&gt;REPEATABLE READ 이하의 트랜잭션 레벨을 제공한다.&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;직렬화 레벨의 경우 단순 조회 쿼리에도 다른 트랜잭션에서 해당 row를 조회하고 있다면 대기가 걸리기 때문에 성능 저하의 이슈가 있기 때문이다.&lt;br /&gt;곧, 그 말은 대부분의 db들은 select 레벨에서 조회시에 대기(락)를 하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;고로 다음과 같은 문제가 발생 할 수 있다.(for update로 락을 걸지 않고 가져오는 경우, JPA dirty check로 설명)&lt;/p&gt;
&lt;ol 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;1번 트랜잭션에서 먼저 계좌를 가져온다.&lt;/li&gt;
&lt;li&gt;2번 트랜잭션에서 또 계좌 정보를 가져온다.&lt;/li&gt;
&lt;li&gt;1번 트랜잭션에서 계좌에서-2000 원을 한다&lt;/li&gt;
&lt;li&gt;1번 트랜잭션에서 commit&lt;/li&gt;
&lt;li&gt;2번 트랜잭션에서 계좌에서 +2000 원을 한다.&lt;/li&gt;
&lt;li&gt;2번 트랜잭션 commit&lt;/li&gt;
&lt;li&gt;결과적으로 계좌는 +2000원이 되었다.(덮어씌워짐)&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;락을 걸지 않을 경우 다음과 같은 문제가 발생하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;고로 어플리케이션 레벨에서 sql에 변경을 위한 조회일 경우 for update를 통해(조회 락) 데이터를 조회하는 것이 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;=&amp;gt; 주의 할점은 for update를 통해 가져와도 다른 트랜잭션에서 select로 가져오면 말짱 도로묵이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;후에 락에 대해서 좀더 포스팅 해봐야겠다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;application + transaction&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션 도중 어플리케이션에서 오류가 발생할 경우 try-catch를 이용해 데이터베이스를 rollback 시킬 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 어플리케이션에서 트랜잭션을 어디에서 시작하고 끝내야 할까&lt;/p&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;어플리케이션에서 오류가 나는 곳은 비지니스 로직(서비스 계층) 부분이다.&lt;/li&gt;
&lt;li&gt;그말은 곧, 하나의 request는 같은 트랜잭션을 사용 즉 같은 커넥션을 유지해야 한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션을 사용하여 Jdbc를 사용해보자&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;jdbc + transaction&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  Repository&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@Slf4j
@RequiredArgsConstructor
public class MemberRepository {

    private final DataSource dataSource;

    public Member save(Member member) throws SQLException {
        String sql = &quot;insert into member(member_id, money) values (?, ?)&quot;;

        Connection con = null;
        PreparedStatement pstmt = null;

        try {
            con = getConnection();
            pstmt = con.prepareStatement(sql);
            pstmt.setString(1, member.getMemberId());
            pstmt.setInt(2, member.getMoney());
            pstmt.executeUpdate();
            return member;
        } catch (SQLException e) {
            log.error(&quot;db error&quot;, e);
            throw e;
        } finally {
            close(con, pstmt, null);
        }

    }

    public Member findById(String memberId) throws SQLException {
        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 e;
        } finally {
            close(con, pstmt, rs);
        }

    }

    public Member findById(Connection con, String memberId) throws SQLException {
        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 e;
        } finally {
            //connection은 여기서 닫지 않는다.
            JdbcUtils.closeResultSet(rs);
            JdbcUtils.closeStatement(pstmt);
        }

    }

    public void update(String memberId, int money) throws SQLException {
        String sql = &quot;update member set money=? where member_id=?&quot;;

        Connection con = null;
        PreparedStatement pstmt = null;

        try {
            con = getConnection();
            pstmt = con.prepareStatement(sql);
            pstmt.setInt(1, money);
            pstmt.setString(2, memberId);
            int resultSize = pstmt.executeUpdate();
            log.info(&quot;resultSize={}&quot;, resultSize);
        } catch (SQLException e) {
            log.error(&quot;db error&quot;, e);
            throw e;
        } finally {
            close(con, pstmt, null);
        }

    }

    public void update(Connection con, String memberId, int money) throws SQLException {
        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);
            int resultSize = pstmt.executeUpdate();
            log.info(&quot;resultSize={}&quot;, resultSize);
        } catch (SQLException e) {
            log.error(&quot;db error&quot;, e);
            throw e;
        } finally {
            //connection은 여기서 닫지 않는다.
            JdbcUtils.closeStatement(pstmt);
        }

    }

    public void delete(String memberId) throws SQLException {
        String sql = &quot;delete from member where member_id=?&quot;;

        Connection con = null;
        PreparedStatement pstmt = null;

        try {
            con = getConnection();
            pstmt = con.prepareStatement(sql);
            pstmt.setString(1, memberId);
            pstmt.executeUpdate();
        } catch (SQLException e) {
            log.error(&quot;db error&quot;, e);
            throw e;
        } finally {
            close(con, pstmt, null);
        }

    }

    private void close(Connection con, Statement stmt, ResultSet rs) {
        JdbcUtils.closeResultSet(rs);
        JdbcUtils.closeStatement(stmt);
        JdbcUtils.closeConnection(con);
    }

    private Connection getConnection() throws SQLException {
        Connection con = dataSource.getConnection();
        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;  Service&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@Slf4j
@RequiredArgsConstructor
public class MemberService {

    private final DataSource dataSource;
    private final MemberRepository memberRepository;

    public void accountTransfer(String fromId, String toId, int money) throws SQLException {
        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) throws SQLException {
        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 validation(Member toMember) {
        if (toMember.getMemberId().equals(&quot;ex&quot;)) {
            throw new IllegalStateException(&quot;이체중 예외 발생&quot;);
        }
    }

    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;p data-ke-size=&quot;size16&quot;&gt;핵심들은 같은 커넥션을 유지하기 위해서 service에서 repository 쪽으로 connection을 전달해야하며&lt;br /&gt;service 계층에서 커넥션을 얻고 commit 혹은 rollback 하는 과정이 매우 겹친다고 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션에서 DB 트랜잭션을 적용하려면 서비스 계층이 매우 지저분해지고 커넥션을 repo 레벨로 계속 던져 주어&lt;br /&gt;야 하기 때문에 유지 보수에도 쉽지 않아 진다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제점&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서비스 계층은 특정 기술에 의존하지 않고 순수 java 코드로 작성하는 편이 좋다
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SQLException&lt;/li&gt;
&lt;li&gt;DataSource&lt;/li&gt;
&lt;li&gt;Connection&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;현재 서비스 계층은 jdbc를 의존하고 있다.&lt;/li&gt;
&lt;li&gt;트랜잭션을 사용하기 위해서 jdbc 기술에 의존 하게 된다. jdbc는 데이터 접근 기술 중 하나임을&lt;br /&gt;잊지 말자(물론 모든 db 접근 기술들은 jdbc를 내부적으로 사용하고 있다.) jdbc 에서는 connection을 통해&lt;br /&gt;setAutoCommit으로 트랜잭션을 열고 commit, rollback 으로 트랜잭션을 닫는다.&lt;/li&gt;
&lt;li&gt;2번의 이유로 서비스에서 특정 계층을 의존하고 있으므로 JDBC에서 JPA 같은 기술로 바꾸어&lt;br /&gt;사용하게 되면 서비스 코드 또한 고쳐져야한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;문제점 정리&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;jdbc 구현 기술이 서비스 계층에 누수 되었다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;트랜잭션을 적용하기 위해 jdbc 구현 기술이 서비스 계층에 누수&lt;/li&gt;
&lt;li&gt;서비스 계층은 순수 java 코드로 만들고 특정 기술에 의존하는 행위들은 모두 spring에서는&lt;br /&gt;component 로 관리하거나 따로 Class로 관리하는 것이 좋다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;물론 따로 빼놔도 Transaction을 적용하게 될 경우 같은 Connection 즉, 같은 세션을&lt;br /&gt;유지하기 위해서 jdbc connection을 사용하면서 service가 특정 계층을 의존하게 되었다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;트랜잭션 동기화 문제
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;단순 조회의 경우 트랜잭션을 유지할 필요가 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;트랜잭션을 위한 반복 코드
&lt;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;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  데이터 접근 기술 추상화&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정기술에 의존하지 않기 위해서 결국 interface를 통한 추상화가 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전에 배웠던, 커넥션 풀과 DriverManger를 추상하기 위한 DataSource interface나&lt;br /&gt;다양한 db Driver를 관리하는 DriverManger 들이 그러하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 접근 기술에 따라서 트랜잭션을 시작하는 방식이 모두 다르다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;jdbc는 connection.setAutoCommit 을 통하여&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;jpa 는 transaction.begin 을 통하여&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;mybatis는 sqlMapClient.startTransaction 을 통하여 진행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;고로 이런 데이터 접근 계층을 Class 하나씩으로 구현하게 될경우 특정 기술에 의존적이다&lt;br /&gt;따라서 이전에도 해봤듯이 각각의 Class들을 하나로 묶어줄 interface가 필요하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;service 계층에서 사용할 repository 레벨을 interface로 정의 한뒤 각 구현체들을 상황에 맞게&lt;br /&gt;끼워 맞추면 되는 것이다. 구현체가 변경이 된다고 해서 service 의 계층이 변화되지 않게 바뀐다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;spring + 추상화&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 위처럼 추상화를 한다고 해서 반복적인 코드가 사라지는 것은 아니다. service계층에서&lt;br /&gt;repo 계층의 의존성을 줄인 것이지 interface 를 구현하고 있는 부분은 반복적인 코드가 겹쳐지게 된다.&lt;br /&gt;따라서 Transaction 자체를 추상화할 필요가 있으며 jpa, jdbc, mybatis 같은 다양한&lt;br /&gt;데이터 접근 기술들에 대한 transaction 관리를 하나의 인터페이스로 적용 시켜야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;spring은 각각 데이터 접근 기술에 대해 TransactionManager를 구상했고 스프링에서&lt;br /&gt;PlatformTransactionManager interface를 통해 각각의 데이터 접근 기술들의 transaction&lt;br /&gt;을 관리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  PlatformTransactionManager&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;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;size16&quot;&gt;또한 예외도 TransactionException으로 한번 더 감싸 Exception을 추상화 한것을 볼수 있다.&lt;br /&gt;getTransaction은 이미 시작한 Transaction이 존재할 경우 같은 트랜잭션을 유지 시켜준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링이 제공하는 interface은 크게 2가지 역할을 한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;트랜잭션 추상화&lt;/li&gt;
&lt;li&gt;리소스 동기화 (같은 트랜잭션 유지)&lt;/li&gt;
&lt;li&gt;같은 트랜잭션을 유지하기 위해 (즉, 같은 커넥션을 사용하기 위해) 파라미터로 커넥션을 전달 했는데 이제 로컬 스레드를&lt;br /&gt;사용하여 파라미터로 넘기지 않아도 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  로컬 스레드 - 스레드 별로 임시저장소를 가진다고 생각하면 편하다. 단, thread pool 을 사용하는 상황일경우&lt;br /&gt;주의해서 다뤄야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링은 트랜잭션 동기화를 위한 별도의 클래스를 제공한다. org.springframework.transaction.support.TransactionSynchronizationManager&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동작 순서는 다음과 같다&lt;/p&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;트랜잭션 매니저는 DataSource를 통해 커넥션을 만들고 트랜잭션을 시작한다.&lt;/li&gt;
&lt;li&gt;트랜잭션 매니저는 트랜잭션이 시작된 커넥션을 트랜잭션 동기화 매니저에 보관&lt;/li&gt;
&lt;li&gt;repo 계층은 트랜잭션 동기화 매니저에 보관된 커넥션을 꺼내서 사용한다.&lt;/li&gt;
&lt;li&gt;트랜잭션이 종료되면 트랜잭션 매니저는 트랜잭션 동기화 매니저에 보관된 커넥션을 통해 트랜잭션을 종료, 리소르를 해제한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;클래스 정리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DataSource - jdbc DriverManager, Thead Pool Connection 추상화&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PlatformTransactionManger - 각기 다른 데이터 접근 기술의 Transaction 관리, 동기화 추상화&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;현재까지의 문제점 정리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;목표 : Transaction 과 비지니스 로직의 분리&lt;/p&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;현재 jdbc 기술에 의존하고 있다.&lt;/li&gt;
&lt;li&gt;=&amp;gt; 다른 db 접근 기술로 바뀌게 될경우 service 계층을 바꾸어야 한다.&lt;/li&gt;
&lt;li&gt;db접근 기술들을 통합하려고 interface를 사용해도 service 단에서는 계속해서 Transaction 시작과 종료 코드가 드러난다.(같은 Transaction 유지를 위함)&lt;/li&gt;
&lt;li&gt;추가적으로 여전히 dataSource를 repository 계층으로 파라미터로 넘겨서 같은 Connection 을 유지해야 한다.&lt;/li&gt;
&lt;li&gt;2번을 해결하기 위해서 repository에서 같은 Connection 사용을 위해 동기화 TransactionManager 적용이 필요하다.&lt;/li&gt;
&lt;li&gt;서비스 계층에서는 PlatformTransactionManager를 사용하여 디비 접근 기술에 대한 의존성을 줄인다.&lt;/li&gt;
&lt;li&gt;service 계층에서 반복적인 try - catch 및 반복성 있는 코드&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;해결을 위한 코드&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  TestCode 에서 의존성 주입 단계&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;/**
 * 트랜잭션 - 트랜잭션 매니저
 */
class MemberServiceTest {

    private MemberRepository memberRepository;
    private MemberService memberService;

    // 의존성 주입 단계
    @BeforeEach
    void before() {
        // DataSource 를 사용하여 어떤 DB 사용할것인지 + 커넥션 풀 사용 or jdbc Connection 사용할 것인지 정함
        // 현재는 jdbc Connection 지원하는 spring DriverManagerDataSource 사용
        DataSource dataSource = new DriverManagerDataSource(URL, USERNAME, PASSWORD);
        memberRepository = new MemberRepositoryV3(dataSource);
        // 어떤 디비접근 기술을 사용할 것인지를 명시 jdbc를 사용할 것이므로 DataSourceTrnasactionManager 주입
        // JPA 사용 할 경우 JpaTransactionManager 로 주입
        // 결국 바뀌는 것은 Config 파일 즉, 의존성을 주입시켜주는 코드만 바뀔 뿐이다.
        PlatformTransactionManager transactionManager = new DataSourceTransactionManager(dataSource);
        memberService = new MemberServiceV3_1(transactionManager, memberRepository);
    }

    // ... test 코드들 작성
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  service 계층&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@Slf4j
@RequiredArgsConstructor
public class MemberService {

    private final PlatformTransactionManager transactionManager;
    private final MemberRepository memberRepository;

    public void accountTransfer(String fromId, String toId, int money) throws SQLException {
        //트랜잭션 시작
        //Transaction 에 대한 설정을 작성하기 위해 DefaultTransactionDefinition 을 주입한다,
        TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());

        try {
            //비즈니스 로직
            bizLogic(fromId, toId, money);
            transactionManager.commit(status); //성공시 커밋
        } catch (Exception e) {
            transactionManager.rollback(status); //실패시 롤백
            throw new IllegalStateException(e);
        }

    }

    private void bizLogic(String fromId, String toId, int money) throws SQLException {
        Member fromMember = memberRepository.findById(fromId);
        Member toMember = memberRepository.findById(toId);

        memberRepository.update(fromId, fromMember.getMoney() - money);
        validation(toMember);
        memberRepository.update(toId, toMember.getMoney() + money);
    }

    private void validation(Member toMember) {
        if (toMember.getMemberId().equals(&quot;ex&quot;)) {
            throw new IllegalStateException(&quot;이체중 예외 발생&quot;);
        }
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  repository 계층&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;/**
 * 트랜잭션 - 트랜잭션 매니저
 * DataSourceUtils.getConnection()
 * DataSourceUtils.releaseConnection()
 */
@Slf4j
public class MemberRepository {

    private final DataSource dataSource;

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

    public Member save(Member member) throws SQLException {
        String sql = &quot;insert into member(member_id, money) values (?, ?)&quot;;

        Connection con = null;
        PreparedStatement pstmt = null;

        try {
            // 같은 connection 사용을 위해 DataSourceUtils 를 사용한다. 
            // 내부적으로 Transaction 동기화 매니저를 사용하여 같은 Connection을 보장한다( Local Thread 사용 )
            con = getConnection();
            pstmt = con.prepareStatement(sql);
            pstmt.setString(1, member.getMemberId());
            pstmt.setInt(2, member.getMoney());
            pstmt.executeUpdate();
            return member;
        } catch (SQLException e) {
            log.error(&quot;db error&quot;, e);
            throw e;
        } finally {
            close(con, pstmt, null);
        }
    }

    private void close(Connection con, Statement stmt, ResultSet rs) {
        JdbcUtils.closeResultSet(rs);
        JdbcUtils.closeStatement(stmt);
        //주의! 트랜잭션 동기화를 사용하려면 DataSourceUtils를 사용해야 한다.
        DataSourceUtils.releaseConnection(con, dataSource);
    }


    private Connection getConnection() throws SQLException {
        //주의! 트랜잭션 동기화를 사용하려면 DataSourceUtils를 사용해야 한다.
        Connection con = DataSourceUtils.getConnection(dataSource);
        log.info(&quot;get connection={}, class={}&quot;, con, con.getClass());
        return con;
    }&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;법칙&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DB 에 대한 정보는 DataSource 를 통해 얻어야한다. (항상 jdbc 를 사용하게 된다. Connection Pool 또한)&lt;/li&gt;
&lt;li&gt;service 를 시작할 때 Transaction 을 열고 닫는 부분이 있어야한다.&lt;/li&gt;
&lt;li&gt;repository 는 같은 Service 메서드에 묶여있을 때 항상 같은 Connection 을 공유해야 한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;결국에 큰 틀은 변하지 않는다. 어떤 데이터 접근 기술을 써도 Connection 을 생성하기 위해 DataSource 가 필요하다.&lt;/li&gt;
&lt;li&gt;DataSource는 DB 설정파일을 통해 관리한다. (물론 어떤 Connection 을 의존하느냐에 따라 추가적인 세팅은 필요할 것이다)&lt;/li&gt;
&lt;li&gt;Service 계층에서 트랜잭션을 시작하기 위해 Connection을 얻고 트랜잭션 매니저를 통해서 트랜잭션을 시작한다.&lt;/li&gt;
&lt;li&gt;(Connection을 얻을때 DataSource를 통해 얻는다)&lt;/li&gt;
&lt;li&gt;Repository 계층에서 같은 Connection을 사용하기 위해서 트랜잭션 동기화 매니저를 사용하며 마찬가지로 Connection을&lt;br /&gt;얻는 과정이기 때문에 dataSource를 필요로 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;흐름 정리&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서비스 계층에서 TransactionManager.getTransaction() 으로 트랜잭션 시작&lt;/li&gt;
&lt;li&gt;트랜잭션을 시작하기위해 TransactionManager 내부에 초기화된 DataSource로 커넥션을 얻는다.&lt;/li&gt;
&lt;li&gt;얻은 커넥션을 setAutocommit(false) 를 통해 실제 데이터 베이스 트랜잭션을 시작한다. (jdbc 기반)&lt;/li&gt;
&lt;li&gt;커넥션을 Transaction 동기화 Manager에 보관(Local Thread 사용)&lt;/li&gt;
&lt;li&gt;서비스는 비즈니스 로직을 실행하면서 리포지토리의 메서드들을 호출한다. 동기화 매니저를 통해 같은 connection을 사용&lt;/li&gt;
&lt;li&gt;위에 코드에서는 DataSourceUtils.getConnection()을 사용하여 트랜잭션 동기화 매니저에 보관된 커넥션을 꺼낸다.&lt;/li&gt;
&lt;li&gt;각 repository 에 SQL 전달하여 소통&lt;/li&gt;
&lt;li&gt;트랜잭션을 종료한다. commit or rollback&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;Thead Local 정리&lt;/li&gt;
&lt;li&gt;setAutoCommit 정리&lt;/li&gt;
&lt;li&gt;커넥션 종료 or 반납&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;✅ 현재까지의 해결&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;트랜잭션 추상화(interface + Local Thread) 를 사용하여 JDBC 기술에 의존하지 않는다.&lt;/li&gt;
&lt;li&gt;더이상 커넥션을 파라미터로 넘기지 않아도 된다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;=&amp;gt; but, 반복적인 코드는 아직 유효하다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;반복적인 코드 고치기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재는 try - catch - finally 와 트랜잭션을 시작 - 종료상황이 반복적으로 쓰여지고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 템플릿 콜백 패턴을 활용해서 반복문제를 해결 해야 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  template - callback 패턴?&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;콜백 이란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로그래밍에서 콜백은 다른 코드의 인수(파라미터)로서 넘겨주는 실행 가능한 코드를 말한다. 콜백을 넘겨받는 코드는&lt;br /&gt;필요에 따라 즉시 실행할 수도 있고, 나중에 실행 할 수도있다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;java 에서는 코드를 인수로 넘기려면 객체화를 시켜야 할텐데 java 8 부터는 람다식이 있다.&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;  template - callback&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;@Slf4j
public class CallbackTest {

    public interface Callback&amp;lt;T&amp;gt;{
        T call();
    }

    public static class Template {
        public &amp;lt;T&amp;gt; T execute(Callback&amp;lt;T&amp;gt; callback){
            log.info(&quot;do something&quot;);
            T call = callback.call();
            log.info(&quot;end&quot;);
            return call;
        }
    }

    @Test
    void testCallback(){
        Template template = new Template();
        String result = template.execute(() -&amp;gt; {
            log.info(&quot;callback execute&quot;);
            return &quot;test&quot;;
        });
        Assertions.assertThat(result).isEqualTo(&quot;test&quot;);
    }

    @Test
    void testCallback2(){
        Template template = new Template();
        String result = template.&amp;lt;String&amp;gt;execute(() -&amp;gt; {
            log.info(&quot;callback execute&quot;);
            return &quot;test&quot;;
        });
        Assertions.assertThat(result).isEqualTo(&quot;test&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;spring 은 이를 위해 TransactionTemplate 을 제공하며 callback은 비지니스 로직으로 치환된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;service 를 수정하면 다음과 같아진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  TransactionTemplate + bizlogic(callback)&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@Slf4j
public class MemberServiceV3_2 {

    private final TransactionTemplate txTemplate;
    private final MemberRepositoryV3 memberRepository;

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

    public void accountTransfer(String fromId, String toId, int money) throws SQLException {
        txTemplate.executeWithoutResult((status) -&amp;gt; {
            //비즈니스 로직
            try {
                bizLogic(fromId, toId, money);
            } catch (SQLException e) {
                // 아쉽게도 람다식에서는 checked Exception을 무조건 처리해야 해서 try-catch가 감싸진다.
                throw new IllegalStateException(e);
            }
        });
    }

    private void bizLogic(String fromId, String toId, int money) throws SQLException {
        Member fromMember = memberRepository.findById(fromId);
        Member toMember = memberRepository.findById(toId);

        memberRepository.update(fromId, fromMember.getMoney() - money);
        validation(toMember);
        memberRepository.update(toId, toMember.getMoney() + money);
    }

    private void validation(Member toMember) {
        if (toMember.getMemberId().equals(&quot;ex&quot;)) {
            throw new IllegalStateException(&quot;이체중 예외 발생&quot;);
        }
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;⚡ 여전한 문제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반복적인 코드를 줄였음에도 람다식을 사용했기 때문에 Exception 처리를 해줘야하고 서비스 로직에서 Transaction을 호출하는&lt;br /&gt;코드는 계속 남는다. 결국 DI의 힘을 빌려한다. (proxy 기술 사용)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동적프록시를 사용하여 코드를 단순화 해야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프록시 + AOP를 적용하여 문제를 해결 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;=&amp;gt; 해당 내용을 @Transacation 으로 해결하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프록시를 통한 구현은 더 공부가 필요하다...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;spring 에서는 동적 프록시를 사용하나 일반적인 프록시 개념으로 적용을 한다면 다음과 같은 모습이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  proxy 적용&lt;/p&gt;
&lt;pre class=&quot;cpp&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;size16&quot;&gt;대략 프록시는 대상(target) 의 기능외에 추가 할때 사용을 많이 하는데 마침 전후로 Transaction을&lt;br /&gt;처리해야 하므로 프록시를 사용하기가 적절하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 @Transaction 어노테이션 하나만으로 관심사가 최종적으로 분리되었으며 service 계층은 db 접근 기술을&lt;br /&gt;의존하지 않게 변했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✅ 리소스 관리하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 어플리케이션 레벨에서 관리해야 할 클래스는 크게 2가지 이다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;dataSource&lt;/li&gt;
&lt;li&gt;TransactionManager&lt;/li&gt;
&lt;li&gt;TransactionSynchronizationManager =&amp;gt; 하지만 개발자가 관리할 일은 없다 TransactionManager 가 동기화를 위해&lt;br /&gt;알아서 사용한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 부트는 autoConfiguration 으로 데이터소스와 라이브러리에 등록된 데이터 접근 기술을 사용하여 TransactionManager를&lt;br /&gt;자동으로 등록한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링을 사용한다면 직접 dataSource와 내가 사용할 기술에 대한 TransactionManager를 등록해야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  직접 빈 등록&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;public class Configure {
   @Bean
   DataSource dataSource() {
       // 해당 dataSource 정보는 설정파일로 관리하여 env 로 뽑아내던, static 변수로 관리를 하던 한다.
       return new DriverManagerDataSource(URL, USERNAME, PASSWORD);
   }

   @Bean
   PlatformTransactionManager transactionManager() {
       return new DataSourceTransactionManager(dataSource());
   }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자동 등록은 그저 application.yml 혹은 application.properties 로 관리를 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링부트가 기본으로 생성하는 데이터소스는 커넥션풀을 제공하는 HikariDataSource 이다.&lt;br /&gt;spring.datasource.url 속성이 없다면 내장 데이터베이스(메모리 DB)를 생성하려고 시도한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;application.properties&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;spring.datasource.url=jdbc:h2:tcp://localhost/~/test&lt;br /&gt;spring.datasource.username=username&lt;br /&gt;spring.datasource.password=&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Spring/JPA</category>
      <author>땅속 디그다</author>
      <guid isPermaLink="true">https://digda.tistory.com/35</guid>
      <comments>https://digda.tistory.com/35#entry35comment</comments>
      <pubDate>Tue, 23 Aug 2022 11:27:30 +0900</pubDate>
    </item>
  </channel>
</rss>