<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>semtax의 개발 일지</title>
    <link>https://semtax.tistory.com/</link>
    <description>개인적으로 공부하고 있는 개발 관련된 내용을 올리는 블로그 입니다.
웹 백엔드를 기반으로 머신러닝, OS커널과 같은 다양한 개발분야를 좋아합니다</description>
    <language>ko</language>
    <pubDate>Wed, 27 May 2026 11:37:31 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>semtax</managingEditor>
    <item>
      <title>Make Simple Query Engine With ANTLR4</title>
      <link>https://semtax.tistory.com/107</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;공부할겸 개인적으로, ANTLR4 를 이용해서 DML, DDL 등을 파싱 및 해석해서 처리하는 초 간단 쿼리엔진을 만들어 본 내용을 발표한 자료 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignCenter&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/bBssAg/btrXooQyPb8/If2LpQ0C9uWvD3bJWPbtCk/How%20to%20make%20query%20engine%20with%20ANTLR4.pdf?attach=1&amp;amp;knm=tfile.pdf&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;How to make query engine with ANTLR4.pdf&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;2.92MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>개발/데이터베이스</category>
      <category>ANTLR4</category>
      <category>DBMS</category>
      <category>Query Engine</category>
      <category>데이터베이스</category>
      <category>파서 제너레이터</category>
      <author>semtax</author>
      <guid isPermaLink="true">https://semtax.tistory.com/107</guid>
      <comments>https://semtax.tistory.com/107#entry107comment</comments>
      <pubDate>Sat, 28 Jan 2023 23:00:08 +0900</pubDate>
    </item>
    <item>
      <title>그래프 데이터베이스 모델링 &amp;amp; JanusGraph</title>
      <link>https://semtax.tistory.com/106</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;그래프 데이터 를 RDB &amp;amp; NoSQL에 어떻게 모델링 해서 저장할지와 JanusGraph 라는 DB에 대한 소개입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignCenter&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/dZyFxH/btq8GPYKE3Q/9YLQCvesIUUrjkuXGocbJK/%E1%84%80%E1%85%B3%E1%84%85%E1%85%A2%E1%84%91%E1%85%B3%20%E1%84%83%E1%85%A6%E1%84%8B%E1%85%B5%E1%84%90%E1%85%A5%20%E1%84%86%E1%85%A9%E1%84%83%E1%85%A6%E1%86%AF%E1%84%85%E1%85%B5%E1%86%BC%20in%20RDB%20%26amp%3B%20NoSQL.pdf?attach=1&amp;amp;knm=tfile.pdf&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;그래프 데이터 모델링 in RDB &amp;amp;amp; NoSQL.pdf&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;1.33MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>개발/데이터베이스</category>
      <category>db</category>
      <category>Janusgraph</category>
      <category>LSMTree</category>
      <category>NoSQL</category>
      <category>RDB</category>
      <category>그래프</category>
      <category>그래프 모델링</category>
      <category>데이터베이스</category>
      <category>야누스그래프</category>
      <author>semtax</author>
      <guid isPermaLink="true">https://semtax.tistory.com/106</guid>
      <comments>https://semtax.tistory.com/106#entry106comment</comments>
      <pubDate>Sun, 4 Jul 2021 15:53:28 +0900</pubDate>
    </item>
    <item>
      <title>Point in polygon</title>
      <link>https://semtax.tistory.com/105</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;도형 내부에 포함된 점을 찾는 알고리즘에 대한 내용입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignCenter&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/ZVRD4/btq8OsgaeHG/MFWkK0KTYZSlI7l62uB4t0/Point%20In%20Polygon.key?attach=1&amp;amp;knm=tfile.key&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;Point In Polygon.key&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;3.92MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>개발/알고리즘</category>
      <category>pip</category>
      <category>Point in polygon</category>
      <category>알고리즘</category>
      <category>자바</category>
      <author>semtax</author>
      <guid isPermaLink="true">https://semtax.tistory.com/105</guid>
      <comments>https://semtax.tistory.com/105#entry105comment</comments>
      <pubDate>Sun, 4 Jul 2021 15:51:03 +0900</pubDate>
    </item>
    <item>
      <title>AOP Reflection 으로 유저 인증 기능 리팩토링 하기</title>
      <link>https://semtax.tistory.com/104</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 AOP를 이용해서 반복되는 유저 인증 코드를 리팩토링 한 내용입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignCenter&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/lCAwj/btq8KRagvHN/QrYzAjxhWi3tfPURHi5XQk/Code%20Refactoring%20with%20AOP%20%26amp%3B%20reflection.pdf?attach=1&amp;amp;knm=tfile.pdf&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;Code Refactoring with AOP &amp;amp;amp; reflection.pdf&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;1.06MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>개발/Java</category>
      <category>AOP</category>
      <category>HTTP</category>
      <category>Spring</category>
      <category>springboot</category>
      <category>리팩토링</category>
      <category>백엔드</category>
      <category>스프링</category>
      <category>스프링 부트</category>
      <category>웹 개발</category>
      <category>유저 인증</category>
      <author>semtax</author>
      <guid isPermaLink="true">https://semtax.tistory.com/104</guid>
      <comments>https://semtax.tistory.com/104#entry104comment</comments>
      <pubDate>Sun, 4 Jul 2021 15:47:20 +0900</pubDate>
    </item>
    <item>
      <title>Websocket Protocol</title>
      <link>https://semtax.tistory.com/103</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;웹소켓 프로토콜 에 대해 공부한 내용 + 웹소켓 Handshake 과정을 직접 구현 해본 내용을 공유해봅니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignCenter&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/POFbh/btq8IghN1m2/PL4EmKNreKXWrarK7wlew1/Websocket%20Protocol.zip?attach=1&amp;amp;knm=tfile.zip&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;Websocket Protocol.zip&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;0.95MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>개발/네트워크</category>
      <category>HTTP</category>
      <category>java</category>
      <category>web socket</category>
      <category>WebSocket</category>
      <category>네트워크</category>
      <category>웹소켓</category>
      <category>자바</category>
      <author>semtax</author>
      <guid isPermaLink="true">https://semtax.tistory.com/103</guid>
      <comments>https://semtax.tistory.com/103#entry103comment</comments>
      <pubDate>Sun, 4 Jul 2021 15:41:49 +0900</pubDate>
    </item>
    <item>
      <title>전자 결제 시스템 테스트 대역 스프링으로 만들어보기</title>
      <link>https://semtax.tistory.com/102</link>
      <description>&lt;p&gt;&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignCenter&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/AhDjs/btq8OsAthKD/SrbnENJGYJmt5KTSGR4gGK/%E1%84%8C%E1%85%A5%E1%86%AB%E1%84%8C%E1%85%A1%20%E1%84%80%E1%85%A7%E1%86%AF%E1%84%8C%E1%85%A6%20%E1%84%89%E1%85%A5%E1%84%87%E1%85%B5%E1%84%89%E1%85%B3.key?attach=1&amp;amp;knm=tfile.key&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;전자 결제 서비스.key&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;2.23MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링으로 테스트 전용의 전자 결제 Mocking 시스템을 만든 내용을 올려봅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(popit 에서 go로 구현한 내용을 스프링+kotlin으로 포팅해보았습니다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처 : &lt;a href=&quot;https://www.popit.kr/%EC%BB%A4%EB%A8%B8%EC%8A%A4-%EC%BD%94%EB%93%9C-%EC%9E%90%EC%82%B0%ED%99%94-%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80-6-%EA%B2%B0%EC%A0%9C-%EB%8C%80%ED%96%89-%EC%84%9C%EB%B9%84%EC%8A%A4%EB%A5%BC-%ED%85%8C/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://www.popit.kr/%EC%BB%A4%EB%A8%B8%EC%8A%A4-%EC%BD%94%EB%93%9C-%EC%9E%90%EC%82%B0%ED%99%94-%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80-6-%EA%B2%B0%EC%A0%9C-%EB%8C%80%ED%96%89-%EC%84%9C%EB%B9%84%EC%8A%A4%EB%A5%BC-%ED%85%8C/&lt;/a&gt;&lt;/p&gt;</description>
      <category>개발/Java</category>
      <category>kotlin</category>
      <category>Spring</category>
      <category>스프링</category>
      <category>스프링부트</category>
      <category>전자결제</category>
      <category>전자결제 시스템</category>
      <category>테스트대역</category>
      <author>semtax</author>
      <guid isPermaLink="true">https://semtax.tistory.com/102</guid>
      <comments>https://semtax.tistory.com/102#entry102comment</comments>
      <pubDate>Sun, 4 Jul 2021 15:39:24 +0900</pubDate>
    </item>
    <item>
      <title>스프링으로 대용량 파일 업로드 기능 구현 하기</title>
      <link>https://semtax.tistory.com/101</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;HTTP 프로토콜을 이용해서 스프링으로 대용량 파일을 업로드 하는 기능을 구현한 내용입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignCenter&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/ciFoYP/btq8JjdZwGp/rifnxKgGxnRFimoCYWYduk/%E1%84%83%E1%85%A2%E1%84%8B%E1%85%AD%E1%86%BC%E1%84%85%E1%85%A3%E1%86%BC%20%E1%84%91%E1%85%A1%E1%84%8B%E1%85%B5%E1%86%AF%20%E1%84%8B%E1%85%A5%E1%86%B8%E1%84%85%E1%85%A9%E1%84%83%E1%85%B3.key?attach=1&amp;amp;knm=tfile.key&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;대용량 파일 업로드.key&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;2.21MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>개발/Java</category>
      <category>http streaming</category>
      <category>HTTP 파일 업로드</category>
      <category>Spring</category>
      <category>Spring Boot</category>
      <category>스프링</category>
      <category>스프링 부트</category>
      <category>파일 업로드</category>
      <author>semtax</author>
      <guid isPermaLink="true">https://semtax.tistory.com/101</guid>
      <comments>https://semtax.tistory.com/101#entry101comment</comments>
      <pubDate>Sun, 4 Jul 2021 15:36:42 +0900</pubDate>
    </item>
    <item>
      <title>Bitfield 로 좋아요 기능 최적화 하기</title>
      <link>https://semtax.tistory.com/100</link>
      <description>&lt;p&gt;Redis Bitfield로 좋아요 기능을 최적화 한 내용입니다.&lt;/p&gt;
&lt;p&gt;참고 바랍니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignCenter&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/4BEE0/btqUtXvL58e/OG91Kk2ykO6ILKExQpmjG1/%E1%84%8C%E1%85%A9%E1%87%82%E1%84%8B%E1%85%A1%E1%84%8B%E1%85%AD%20%E1%84%80%E1%85%B5%E1%84%82%E1%85%B3%E1%86%BC%20%E1%84%80%E1%85%AE%E1%84%92%E1%85%A7%E1%86%AB%E1%84%80%E1%85%AA%20BitField.pdf?attach=1&amp;amp;knm=tfile.pdf&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;좋아요 기능 구현과 BitField.pdf&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;1.70MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>개발/알고리즘</category>
      <category>bitfield</category>
      <category>Count Distinct</category>
      <category>redis</category>
      <category>레디스</category>
      <category>자료구조</category>
      <author>semtax</author>
      <guid isPermaLink="true">https://semtax.tistory.com/100</guid>
      <comments>https://semtax.tistory.com/100#entry100comment</comments>
      <pubDate>Sun, 24 Jan 2021 15:43:03 +0900</pubDate>
    </item>
    <item>
      <title>LSMTree 그리고 HBase</title>
      <link>https://semtax.tistory.com/99</link>
      <description>&lt;p&gt;스터디때 발표한 내용입니다. 공유합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignCenter&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/v235C/btqUo9X2mLk/vzKkbwwIVjKNN0Q5HThPr0/LSMTree_%26amp%3B_HBase.pdf?attach=1&amp;amp;knm=tfile.pdf&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;LSMTree_&amp;amp;amp;_HBase.pdf&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;1.97MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>개발/데이터베이스</category>
      <category>B-Tree</category>
      <category>hbase</category>
      <category>LSMTree</category>
      <category>데이터베이스</category>
      <category>빅데이터</category>
      <category>자료구조</category>
      <author>semtax</author>
      <guid isPermaLink="true">https://semtax.tistory.com/99</guid>
      <comments>https://semtax.tistory.com/99#entry99comment</comments>
      <pubDate>Sun, 24 Jan 2021 15:41:13 +0900</pubDate>
    </item>
    <item>
      <title>위치정보를 빠르게 검색하자 : Geohashing</title>
      <link>https://semtax.tistory.com/98</link>
      <description>&lt;p&gt;개인 프로젝트를 수행하면서, 위치정보(위도, 경도) 가 포함된 데이터를 빠르게 검색해야 할 일이 있어서 정리한 내용을 올려봅니다.&lt;/p&gt;
&lt;p&gt;참고바랍니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignCenter&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/cmQ4U6/btqSZydtZpa/6ICxIMkR9d4iOPwoR43dhK/GIS%20%E1%84%83%E1%85%A6%E1%84%8B%E1%85%B5%E1%84%90%E1%85%A5%20%E1%84%8C%E1%85%A9%E1%84%92%E1%85%AC%20%E1%84%8E%E1%85%AC%E1%84%8C%E1%85%A5%E1%86%A8%E1%84%92%E1%85%AA.pdf?attach=1&amp;amp;knm=tfile.pdf&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;GIS 데이터 조회 최적화.pdf&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;6.62MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>개발/알고리즘</category>
      <category>geohasing</category>
      <category>GIS</category>
      <category>GPS</category>
      <category>hashing</category>
      <category>node.js</category>
      <category>알고리즘</category>
      <category>위치기반 검색</category>
      <category>위치정보</category>
      <category>자료구조</category>
      <category>해싱</category>
      <author>semtax</author>
      <guid isPermaLink="true">https://semtax.tistory.com/98</guid>
      <comments>https://semtax.tistory.com/98#entry98comment</comments>
      <pubDate>Sun, 10 Jan 2021 19:38:59 +0900</pubDate>
    </item>
    <item>
      <title>백분위수를 빠르고 효율적으로 계산하자 : DD-Sketch</title>
      <link>https://semtax.tistory.com/97</link>
      <description>&lt;p&gt;예전에 회사 솔루션 성능개선 위해 리서치 했던 내용을 발표했던 PPT입니다.&lt;/p&gt;
&lt;p&gt;도움이 될것 같아서 공유합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignCenter&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/xuv2Z/btqK7bJ1qYr/28kOhZDFxk7ZvOlCOEvnbk/DD-Sketch.pdf?attach=1&amp;amp;knm=tfile.pdf&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;DD-Sketch.pdf&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;1.10MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>개발/알고리즘</category>
      <category>DD-Sketch</category>
      <category>백분위 수</category>
      <category>통계</category>
      <author>semtax</author>
      <guid isPermaLink="true">https://semtax.tistory.com/97</guid>
      <comments>https://semtax.tistory.com/97#entry97comment</comments>
      <pubDate>Sat, 17 Oct 2020 23:23:29 +0900</pubDate>
    </item>
    <item>
      <title>육각형 아키텍처</title>
      <link>https://semtax.tistory.com/96</link>
      <description>&lt;h1&gt;Hexagonal Architecture&lt;/h1&gt;
&lt;h1&gt;개요&lt;/h1&gt;
&lt;p&gt;이번 포스팅 에서는, 육각형 아키텍처(Hexagonal Architecture) 에 대해서 알아보고 간단한 예제를 통해 실제로 어떻게 육각형 아키텍처를 적용 하는지에 대해서도 알아보도록 하겠습니다.&lt;/p&gt;
&lt;h1&gt;계층화 없는 코드 = 스파게티&lt;/h1&gt;
&lt;p&gt;먼저 육각형 아키텍처를 설명하기 전에, 제가 첫 프로젝트를 진행했을때의 이야기를 해보겠습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;제가 모 기관에서 안드로이드 APK 보안취약점 분석을 수행해주는 서비스를 진행했을때, 저는 모듈화나 아키텍처에 대한 개념이 거의 없었습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;그래서 저는 입력값 검증 로직, 실제 비즈니스 로직, 데이터베이스 저장 로직 들을 전부 컨트롤러에 해당하는 메소드에 몰아넣어서 작성 하였습니다.&lt;/p&gt;
&lt;p&gt;사실 맨 처음에 개발을 할때는 코드가 몇 줄 되지 않아서 엄청나게 편했습니다.&lt;/p&gt;
&lt;p&gt;하지만, 기능이 추가되고 요구사항이 늘어나면서 코드의 크기가 커지면서 점점 코드를 추가하는것이 더욱 힘들어졌습니다.&lt;/p&gt;
&lt;p&gt;무엇보다도, 요구사항이 수정되는 경우 코드를 수정 및 추가하는것이 거의 불가능에 가까운 상황이 일어나게 되었습니다.&lt;/p&gt;
&lt;p&gt;실제로, 파이프를 이용해서 외부 프로세스와 통신을 하여 결과값을 가져온 뒤, 비동기로 작업이 완료되었다고 알려주는 로직을 작성을 하려고 했지만 코드가 너무 복잡해져서 해당 로직을 추가하지 못하였습니다.&lt;/p&gt;
&lt;p&gt;결국 최종발표때, 저희가 목표한 만큼의 퀄리티의 서비스를 만드는것에는 실패하였습니다.&lt;/p&gt;
&lt;p&gt;보통 학부 1~2학년때 수행하는 프로젝트 또는 첫 개인프로젝트를 수행했을시, 위와 같은 상황을 겪은분들은 보통 아래와 같이 코드를 작성했을 것입니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;const express = require(&amp;#39;express&amp;#39;)
const app = express()
const port = 3000

app.get(&amp;#39;/&amp;#39;, (req, res) =&amp;gt; {
   // 비즈니스로직, 입력값 검증, 데이터베이스 CRUD, 
   // 기타 스케쥴링 작업등 전부 여기 1개 메소드에 전부 작성해버리는 경우
   // 거의 한 500~10000줄을 여기다가.........
})

app.listen(port, () =&amp;gt; {
  console.log(`Example app listening at http://localhost:${port}`)
})&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;결국, 위에서 언급한 상황과 같이 해당 코드는 프로젝트가 조금만 커지게 되어도, 코드에 손을 댈 수 없는 상황이 되어버리고 맙니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;여담으로 위와 같은 코드의 패턴들을 보통 SmartUI 라는 이름의 안티패턴 으로 불리고 있습니다. 절대 현업 에서는 저렇게 코드를 작성하지 않기를 바랍니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;또한, 저러한 안좋은 코드들을 보고 스파게티 코드라고 부르기도 합니다.&lt;/p&gt;
&lt;p&gt;당연하게도, 저만 이런 문제를 겪었던것이 아닙니다. &lt;strong&gt;몇십년전 부터 이미 수많은 프로그래머들이 소프트웨어의 위기라는 이름으로 이러한 문제에 대해 많은 고민&lt;/strong&gt;을 하고 있었습니다.&lt;/p&gt;
&lt;p&gt;이때, 사람들은 그 고민을 해결하기 위해 객체지향 프로그래밍(OOP) 이라는 패러다임을 도입하였습니다.&lt;/p&gt;
&lt;p&gt;재미있는 것은, &lt;strong&gt;객체지향 프로그래밍을 도입 하였음에도 이전보다는 생산성이 약간 늘기는 했지만, 생각 보다 재사용성도 잘 되지 않았고, 유지보수도 여전히 힘들었&lt;/strong&gt;습니다. &lt;/p&gt;
&lt;p&gt;결국 객체지향이나 절차지향, 함수 지향 패러다임 이전에 보다 &lt;strong&gt;근본적인 원인&lt;/strong&gt;을 해결하지 못한 것이지요.&lt;/p&gt;
&lt;p&gt;과연 무엇이 원인 이었을까요? 바로 코드 의존성과 결합도 입니다.&lt;br&gt;즉, 코드 의존성(결합도)은 낮추고 응집성은 높여야 된다는 말입니다.&lt;/p&gt;
&lt;p&gt;먼저, 코드 의존성(결합도)란 &lt;strong&gt;하나를 고치기 위해 수많은 코드를 뜯어 고쳐야 하는 상황 입니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;글 맨 위에서 언급한 사례가 바로, 각 코드간의 의존성이 너무 높아서 코드를 수정하지 못하는 사례입니다.&lt;/p&gt;
&lt;p&gt;그렇다면 코드 응집성(Code Cohesion) 이란 무엇일까요? 또 하나의 예시를 들어보도록 합시다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;프로젝트를 하는데, 역시나 이미 코드가 많이 짜여진 상태입니다. &lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;다행히도 당신은 코드 의존성에 대한 개념을 알기 때문에, 모듈을 쪼개면서 개발을 하였습니다. &lt;/p&gt;
&lt;p&gt;그러나 너무 코드를 잘게 쪼갠 나머지 단 200줄 짜리 프로그램인데 클래스가 한 40개정도가 되는 상황이 발생했습니다. &lt;/p&gt;
&lt;p&gt;결국 당신은 겨우 수십줄의 코드를 고치기 위해 10개넘는 클래스를 돌아다니면서 일일히 다 뜯어 고쳐야 되는 상황이 발생했습니다.&lt;/p&gt;
&lt;p&gt;위에서 나온 상황 같이 서로 &lt;strong&gt;유사한 역할을 하는 코드들은 같이 모아둬야 한다는 것&lt;/strong&gt;이 바로 &lt;strong&gt;코드 응집성&lt;/strong&gt;을 말하는 것입니다.&lt;/p&gt;
&lt;p&gt;이러한 관계 없는 코드 의존성(결합도)을 낮추고 관계있는 코드간의 응집성를 높혀서 재사용성과 유지보수, 개발의 생산성을 높이는 주요한 5가지 원칙이 존재합니다. 바로 할리우드 원칙과 SOLID원칙입니다.&lt;/p&gt;
&lt;p&gt;할리우드 원칙은 단 한 문장으로 설명됩니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;You don&amp;#39;t call me, I&amp;#39;ll call You(당신이 나를 부르는게 아니야, 내가 당신을 부르는 거지)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;즉, 프로그램의 전체적인 제어 흐름을 사용자가 아닌 이미 뼈대 코드에 맡기는 것입니다. 이렇게 함으로써, 사용자는 이미 만들어진 흐름에 자기의 코드를 끼워 넣기만 하면 되기 때문에 뼈대 코드 와 사용자의 역할이 분리가 되는것이지요.&lt;/p&gt;
&lt;p&gt;또한, SOLID는 아래의 5가지 원칙을 말하는 것입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Single Responsibility Principle(단일 책임의 원칙)&lt;/li&gt;
&lt;li&gt;Open-Closed Principle(개방 폐쇄 원칙)&lt;/li&gt;
&lt;li&gt;Liskov&amp;#39;s Principle(리스코프의 원리)&lt;/li&gt;
&lt;li&gt;Interface Segregation Principle(인터페이스 분리의 원칙)&lt;/li&gt;
&lt;li&gt;Dependancy Inversion Principle(의존성 역전의 법칙)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;즉 요약하면, 한 모듈은 한가지의 일만 잘 해야하고, 기존 코드를 수정하지 않고 코드를 추가하는것이 두렵지 않아야 하고, 인터페이스로 역할을 분리하여 코드간의 의존성을 낮춰야 된다는 말입니다. &lt;/p&gt;
&lt;p&gt;(사실 리스코프 원칙도 비슷한 관계, 즉 상속관계에 있는 객체 간에 있어서 부모 클래스 대신 자식 클래스로 대체해도 된다는 말이니 코드 유지보수에 결국 도움이 되는 원칙이지요.)&lt;/p&gt;
&lt;p&gt;사실 객체지향 언어가 아닌 절차지향적 언어인 C로도 이러한 5가지 원칙과 할리우드 원칙을 잘 지켜서 프로그래밍 하고 있는 아주 거대한 프로젝트가 존재합니다. 바로 리눅스와 유닉스 커널 입니다.&lt;/p&gt;
&lt;p&gt;아래가 리눅스 커널에서 C로 인터페이스를 구현한 예제입니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;struct file_operations {
    struct module *owner;
    loff_t (*llseek) (struct file *, loff_t, int);
    ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
    ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
    ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
    ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
    int (*iopoll)(struct kiocb *kiocb, bool spin);
  ....
};&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그리고 실제 C로 프로그래밍 하는 사용자는 다음과 같이 프로그래밍 하면 됩니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include&amp;lt;stdio.h&amp;gt; 
#include &amp;lt;fcntl.h&amp;gt; 

int main() 
{ 
  int fd, sz; 
  char *c = (char *) calloc(100, sizeof(char)); 

  fd = open(&amp;quot;foo.txt&amp;quot;, O_RDONLY); 
  if (fd &amp;lt; 0) { perror(&amp;quot;r1&amp;quot;); exit(1); } 

  sz = read(fd, c, 10); 
  printf(&amp;quot;called read(% d, c, 10).  returned that&amp;quot;
         &amp;quot; %d bytes  were read.\n&amp;quot;, fd, sz); 
  c[sz] = &amp;#39;\0&amp;#39;; 
  printf(&amp;quot;Those bytes are as follows: % s\n&amp;quot;, c); 
} &lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;즉, 파일을 어떠한 방식으로(어떤 파일시스템을 쓰는지) 읽고 쓰는지 실제 사용자가 전혀 알지 못하더라도 이와 같은 추상화를 통해 역할을 분리 함으로써 여러가지 디바이스나 파일시스템에 대응해서 I/O 작업을 수행할 수 있는 겁니다.&lt;/p&gt;
&lt;p&gt;이제, 이러한 분리(패키징) 방식에는 어떤 종류 들이 있는지 차근차근 알아보도록 합시다.&lt;/p&gt;
&lt;h1&gt;계층 기반 패키징&lt;/h1&gt;
&lt;p&gt;먼저, 가장 흔히 쓰이는 방식은 통칭 &lt;strong&gt;MVC 아키텍처&lt;/strong&gt; 라고도 불리는 &lt;strong&gt;수평 계층형 아키텍처&lt;/strong&gt; 입니다.&lt;/p&gt;
&lt;p&gt;보통 아래와 같이 controller, model, view 와 같이 패키징을 해서 코드들을 분리합니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;http://xiaoblog.digibrady.com/uploads/node.js-mvc-architecture-folder-layout.jpg&quot; alt=&quot;http://xiaoblog.digibrady.com/uploads/node.js-mvc-architecture-folder-layout.jpg&quot;&gt;&lt;/p&gt;
&lt;p&gt;위의 사진에 나온 각 모듈 별 역할은 아래와 같습니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;패키지(모듈) 이름&lt;/th&gt;
&lt;th&gt;역할&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Controller&lt;/td&gt;
&lt;td&gt;Model과 View를 이어주는 역할을 하는 코드들이 여기 위치함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Model&lt;/td&gt;
&lt;td&gt;비즈니스 로직과 데이터를 읽고 쓰는 코드들이 여기 위치함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;View&lt;/td&gt;
&lt;td&gt;실제 사용자에게 입/출력을 받는 코드들이 여기 위치함&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;먼저 이러한 구조의 장점은, view, controller, model 이렇게 딱 3개로만 분리되기 때문에 소프트웨어의 규모가 작은 서비스의 경우 관리가 용이하다는 장점이 있습니다.&lt;/p&gt;
&lt;p&gt;사실 계층 기반 패키징 항목에서 맨 처음 언급한 &amp;quot;계층 기반 패키징 = MVC&amp;quot; 라는 말은 엄밀히 말해서는 잘못된 말입니다.&lt;/p&gt;
&lt;p&gt;엄밀히 말하면은 MVC 아키텍처가 계층 기반 패키징에 포함된다고 볼 수 있습니다.&lt;br&gt;(일단 이해를 쉽게 하기위해 편의상 MVC = 계층 기반 패키징 이라고 설명했습니다.)&lt;/p&gt;
&lt;p&gt;실제로도 &amp;quot;계층 기반 패키징&amp;quot; 은 보통 &amp;quot;Layered Architecture&amp;quot; 라고 불리고 있습니다. &lt;/p&gt;
&lt;p&gt;굳이, Model, View, Controller 말고도 더 많은 방식으로 쪼갤 수 있습니다. &lt;/p&gt;
&lt;p&gt;그 대표적인 예가 네트워크 시간에 지겹게 배우는 OSI 7계층 이죠.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;사실 계층 기반 패키징 항목에서 맨 처음 언급한 &amp;quot;계층 기반 패키징 = MVC&amp;quot; 라는 말은 엄밀히 말해서는 잘못된 말입니다. &lt;/p&gt;
&lt;p&gt;엄밀히 말하면은 MVC 아키텍처가 계층 기반 패키징에 포함된다고 볼 수 있습니다. (일단 이해를 쉽게 하기위해 편의상 MVC = 계층 기반 패키징 이라고 설명했습니다.) &lt;/p&gt;
&lt;p&gt;실제로도 &amp;quot;계층 기반 패키징&amp;quot; 은 보통 &amp;quot;Layered Architecture&amp;quot; 라고 불리고 있습니다.  &lt;/p&gt;
&lt;p&gt;굳이, Model, View, Controller 말고도 더 많은 방식으로 쪼갤 수 있습니다. &lt;/p&gt;
&lt;p&gt;그 대표적인 예가 네트워크 시간에 지겹게 배우는 OSI 7계층 입니다. &lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;하지만, 이러한 수평 계층 기반의 패키징 방식(아키텍처)은 소프트웨어가 커지고 복잡해지면, 잘게 모듈화 하기가 까다롭다는 단점이 존재합니다.&lt;/p&gt;
&lt;p&gt;또한, 이러한 수평 계층 기반의 아키텍처는 업무 도메인을 구분할 수 있는 방법이 마땅치 않다는 단점이 있습니다.&lt;/p&gt;
&lt;p&gt;그리고, 실제 모델 레이어에 비즈니스 로직이 작성되므로 데이터베이스(DB)와 비즈니스 로직이 강하게 결합되는 단점이 존재합니다.&lt;/p&gt;
&lt;p&gt;즉, 전혀 다른 업무 도메인의 코드 라도, 코드 들을 계층형 아키텍처에 따라 작성하는 경우 무조건 &lt;strong&gt;view, controller, model&lt;/strong&gt; 이 3가지 패키지에만 들어가버리기 때문에 해당 서비스가 어떤 도메인으로 구성 되었는지를 파악하는것이 까다롭다는 문제점이 존재합니다.&lt;/p&gt;
&lt;p&gt;그렇다면 다른 좋은방법이 없을까요?&lt;/p&gt;
&lt;h1&gt;기능 기반 패키징&lt;/h1&gt;
&lt;p&gt;또 다른 방법으로는, &lt;strong&gt;기능 기반 패키징&lt;/strong&gt; 이라는 방법이 있습니다.&lt;/p&gt;
&lt;p&gt;기능 기반 패키징은 서로 연관된 기능이나, 연관된 도메인 개념, 연관된 도메인들의 묶음(Aggregator Root) 을 기반으로 코드들을 패키징 해서 쪼개는 방법이 존재합니다.&lt;/p&gt;
&lt;p&gt;보통 아래와 같이 패키징을 해서 코드를 분리합니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://khalilstemmler.com/img/blog/enterprise-node/mvc/folder-structure.png&quot; alt=&quot;https://khalilstemmler.com/img/blog/enterprise-node/mvc/folder-structure.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;위의 사진에서 보다시피, 기능 기반 패키징은 관련된 도메인(쇼핑, 거래, 결제)나 비슷한 역할을 하는 코드들 끼리 패키징을 하는것을 알 수 있습니다.&lt;/p&gt;
&lt;p&gt;실제로 이와 같이 관련된 도메인을 위주로 개발하는 &lt;strong&gt;DDD(Domain Driven Development)&lt;/strong&gt; 라는 개발방식도 존재합니다.&lt;/p&gt;
&lt;p&gt;하지만 &lt;strong&gt;기능 기반 패키징&lt;/strong&gt; 같은 경우에는, &lt;strong&gt;기술과 실제 비즈니스 로직의 분리&lt;/strong&gt; 를 하기가 약간 까다롭습니다.&lt;/p&gt;
&lt;p&gt;뭔가 위에서 언급한 &lt;strong&gt;수직적 계층화&lt;/strong&gt;와 &lt;strong&gt;기능 기반 패키징&lt;/strong&gt;의 &lt;strong&gt;장점&lt;/strong&gt;만을 취한 방법이 존재할 것 같습니다.&lt;/p&gt;
&lt;p&gt;과연 그러한 방법은 없는 것 일까요?&lt;/p&gt;
&lt;h1&gt;Port &amp;amp; Adapter, Hexagonal Architecture&lt;/h1&gt;
&lt;p&gt;좋습니다, 저희는 &lt;strong&gt;기술과 실제 비즈니스로직의 분리&lt;/strong&gt; 와 &lt;strong&gt;각 도메인 별로 비즈니스로직 분리&lt;/strong&gt; 라는 두마리의 토끼를 동시에 잡고 싶습니다.&lt;/p&gt;
&lt;p&gt;이때, 육각형 아키텍처(Hexagonal architecture, a.k.a Port &amp;amp; Adapter) 라는 아키텍처는 저희에게 좋은 해답이 되어 줄수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.hitsubscribe.com/wp-content/uploads/2018/08/HexagonalArchitecture.png&quot; alt=&quot;https://www.hitsubscribe.com/wp-content/uploads/2018/08/HexagonalArchitecture.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;육각형 아키텍처의 원리는 간단합니다.&lt;/p&gt;
&lt;p&gt;위 그림과 같이 실제 비즈니스 로직이 위치한 도메인 영역과 UI, 데이터 베이스, 입출력 코드와 같은 기술적인 세부사항을 다루는 인프라 영역의 2개 영역으로 나눕니다.&lt;/p&gt;
&lt;p&gt;그런 뒤, 아래 2가지의 제약 조건을 지키게 코드를 작성해주면 됩니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;도메인 영역 → 인프라 영역 으로는 접근 가능 하지만, 반대로는 불가능 하게 구성 해야함&lt;/li&gt;
&lt;li&gt;무조건 포트, 어댑터를 통해서만 도메인 영역에서 인프라 영역으로 접근하게 해야함 &lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;이렇게 아키텍처를 구성함으로써 느슨한 결합을 유지 할 수 있습니다.&lt;/p&gt;
&lt;p&gt;즉, 데이터베이스가 변경되거나, REST API 에서 메시지 큐로 입출력 부분이 바뀌는 경우와 같이, 기술적인 세부사항이 변경 되더라도 기존 도메인 코드의 수정없이 인프라 코드를 추가해주고, 그에 따른 어댑터 및 포트만 구현을 해주면 바로 대응이 된다는 장점이 존재합니다. &lt;/p&gt;
&lt;p&gt;따라서, 수직적 계층화와 기능 기반 패키지 아키텍처의 장점을 동시에 가져갈수 있다는 것입니다. &lt;/p&gt;
&lt;p&gt;또한, 이전에 언급한 수직적 계층화의 단점인 데이터베이스와 비즈니스 로직이 강하게 결합되는 문제 또한 피할 수 있습니다.&lt;/p&gt;
&lt;p&gt;육각형 아키텍처의 세부적인 구조는 아래 그림과 같이 이루어져 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://media.vlpt.us/images/labyu/post/02266a75-8412-4d2b-bf91-60bb6abddc57/image.png&quot; alt=&quot;https://media.vlpt.us/images/labyu/post/02266a75-8412-4d2b-bf91-60bb6abddc57/image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;각 모듈 별 설명은 아래와 같습니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;모듈 명&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;th&gt;비고&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;인바운드 어댑터&lt;/td&gt;
&lt;td&gt;외부에서 들어온 요청을 인바운드 포트를 호출해서 처리&lt;/td&gt;
&lt;td&gt;예시 :  Rest API Endpoint gRPC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;아웃바운드 어댑터&lt;/td&gt;
&lt;td&gt;비즈니스 로직에서 들어온 요청을 외부 애플리케이션/서비스를 호출해서 처리&lt;/td&gt;
&lt;td&gt;예시 :  Database, ORM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;인바운트 포트&lt;/td&gt;
&lt;td&gt;도메인 코드에 접근하기 위한 인터페이스 클래스&lt;/td&gt;
&lt;td&gt;데이터 흐름 :  Endpoint → Domain&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;아웃바운드 포트&lt;/td&gt;
&lt;td&gt;도메인 코드에 접근하기 위한 인터페이스 클래스&lt;/td&gt;
&lt;td&gt;데이터 흐름 :  Domain → outbound Adapter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;비즈니스 로직&lt;/td&gt;
&lt;td&gt;실제 도메인 내용을 처리하는 코드&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;위의 그림과 같이 보통 육각형 안쪽에 도메인 과 관련된 비즈니스 로직이 들어가고, 육각형 바깥에 도메인과 상관이 없는 인프라 코드가 들어가게 됩니다.&lt;/p&gt;
&lt;p&gt;또한, 위에서 설명 하였듯이, 포트와 어댑터를 통해서만 인프라 코드에 접근 가능합니다.&lt;/p&gt;
&lt;p&gt;그리고, 경우에 따라서 포트 및 어댑터를 인바운드, 아웃바운드로 구분해서 구현을 하기도 합니다.&lt;/p&gt;
&lt;p&gt;지금까지 포스팅 읽어주셔서 감사합니다.&lt;/p&gt;
&lt;h1&gt;출처&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;클린 아키텍처, 로버트.마틴.C&lt;/li&gt;
&lt;li&gt;Get Your Hands Dirty on Clean Architecture, Tom Hombergs&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://semtax.tistory.com/22?category=804335&quot;&gt;https://semtax.tistory.com/22?category=804335&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[&lt;a href=&quot;https://velog.io/@labyu/MSA-2%5D&quot;&gt;https://velog.io/@labyu/MSA-2]&lt;/a&gt;(&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>개발/Java</category>
      <category>design pattern</category>
      <category>Hexagonal Architecture</category>
      <category>Port &amp;amp; Adapter</category>
      <category>개발</category>
      <category>디자인 패턴</category>
      <category>아키텍처</category>
      <category>육각형 아키텍처</category>
      <category>포트 &amp;amp; 어댑터</category>
      <author>semtax</author>
      <guid isPermaLink="true">https://semtax.tistory.com/96</guid>
      <comments>https://semtax.tistory.com/96#entry96comment</comments>
      <pubDate>Mon, 5 Oct 2020 10:25:13 +0900</pubDate>
    </item>
    <item>
      <title>AWS RDS 쓰다가 터진 문제점 정리</title>
      <link>https://semtax.tistory.com/95</link>
      <description>&lt;h2&gt;개요&lt;/h2&gt;
&lt;p&gt;​                            &lt;/p&gt;
&lt;p&gt;이번 포스팅에서는 Amazon RDS 에서 발생했던 문제상황 및 주의점 대해서 포스팅을 하려고 한다.&lt;/p&gt;
&lt;p&gt;​                            &lt;/p&gt;
&lt;p&gt;​                                                        &lt;/p&gt;
&lt;h2&gt;문제 상황&lt;/h2&gt;
&lt;p&gt;먼저 대규모의 임시 데이터가 들어있는 xxxx_yyyy_table을 mysql client로 접속해서 DROP 명령어로 삭제하였습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;DROP TABLE xxxx_yyyy_table;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;하지만 삭제하던 도중에 에러가 나서 세션이 끊어졌습니다.&lt;/p&gt;
&lt;p&gt;일단, 다시 접속해서 보니 테이블이 삭제된거 같아서 해당 테이블을 다시 생성을 하려고 했으나 아래와 같은 에러가 나면서 테이블 생성이 실패하였습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;tablespace name `xxxx_yyyy_table` is exists.&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;시도한 삽질들&lt;/h2&gt;
&lt;p&gt;먼저 아래와 같이 단순하게 테이블스페이스(Tablespace)를 삭제 하려고 시도하였습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;DROP TABLESPACE xxxx_yyyy_table;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;하지만 아래와 같은 에러가 나면서 실패하였습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;create tablespace xxxx_yyyy_table add datafile &amp;#39;xxxx_yyyy_table.ibd&amp;#39; ;
ERROR 1227 (42000): Access denied; you need (at least one of) the CREATE TABLESPACE privilege(s) for this operation&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;자료를 찾아보니 AWS Aurora DB 에서는, 아예 데이터베이스 차원에서 테이블스페이스(Tablespace) 에 대한 권한을 막아놓는 다는 사실을 알게 되었습니다.&lt;/p&gt;
&lt;p&gt;그래서, &amp;quot;삭제나 수정은 안되니, 기존에 만들어져있는 테이블 스페이스를 새로만든 테이블에 연결한 뒤, DROP 명령어를 삭제하자는 &amp;quot; 가정을 세웠습니다.&lt;/p&gt;
&lt;p&gt;그래서 자료를 찾아보니 AWS RDS Aurora DB 에서는, INFORMATION_SCHEMA.tables 테이블에 테이블스페이스(Tablespace) 정보를 저장한다고 나와있었습니다.&lt;/p&gt;
&lt;p&gt;따라서, 아래와 같은 방식으로 테이블스페이스 정보를 직접 찾은 뒤, 새로운 테이블을 생성해서 새로운 테이블과 기존의 Tablespace를 연결 한뒤, DROP 명령어를 이용해서 테이블스페이스를 삭제하려고 시도 하였습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;SELECT tablespace_name, table_name FROM xxxx_yyyy_table; // 테이블 스페이스 이름 검색
// 검색한 이름 : innodb_per_table_199
CREATE TABLE xxxx_yyyy_table2 ( ~~~~ ) TABLESPACE `innodb_per_table_199` Engine = InnoDB;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;하지만, 아래와 같은 에러가 나면서 실패하였습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;ERROR 3199(42000) : InnoDB: A general tablespace name cannot start with &amp;#39;innodb_&amp;#39;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;결국 AWS에 직접 연락을해서 조치를 취해줄때까지 밤새면서 계속 대기하였습니다.&lt;/p&gt;
&lt;h2&gt;결론&lt;/h2&gt;
&lt;p&gt;결국, 위와 같은 경우가 발생하면, AWS의 대응만을 기다려야 합니다.&lt;/p&gt;
&lt;p&gt;따라서 대용량의 테이블을 DROP 할때는, 절대 도중에 세션이 끊어지게 하면 안됩니다...&lt;/p&gt;
&lt;p&gt;p.s 일단 InnoDB가 아닌 MyISAM 등으로 테이블을 생성 하면 일단 생성은 되기는 하지만, 성능상 이슈가 발생하므로 주의 해야 합니다.&lt;/p&gt;
&lt;h2&gt;출처&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Amazon 오로라 첫걸음 교육자료&lt;/li&gt;
&lt;li&gt;삽질&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>개발/데이터베이스</category>
      <category>AuroraDB</category>
      <category>aws</category>
      <category>AWS RDS</category>
      <category>mysql</category>
      <category>rds</category>
      <category>tablespace</category>
      <category>데이터베이스</category>
      <category>테이블 스페이스</category>
      <author>semtax</author>
      <guid isPermaLink="true">https://semtax.tistory.com/95</guid>
      <comments>https://semtax.tistory.com/95#entry95comment</comments>
      <pubDate>Mon, 15 Jun 2020 11:53:54 +0900</pubDate>
    </item>
    <item>
      <title>JPA 에서 GeneratedValue 사용할 때 주의점</title>
      <link>https://semtax.tistory.com/94</link>
      <description>&lt;h2&gt;개요&lt;/h2&gt;
&lt;p&gt;이번 포스팅에서는 JPA에서 @GeneratedValue 를 사용할때 주의할 점에 대해서 설명하도록 하겠다.&lt;/p&gt;
&lt;p&gt;특히, 이번시간에는 주의할 점 중에서도 Batch Insert와 관련된 내용을 다루려고 한다.&lt;/p&gt;
&lt;p&gt;​                            &lt;/p&gt;
&lt;p&gt;​                            &lt;/p&gt;
&lt;h2&gt;@GeneratedValue 값 생성 전략&lt;/h2&gt;
&lt;p&gt;​                                                      &lt;/p&gt;
&lt;p&gt;@GenerateValue 는 일반적으로, PRIMARY 키의 기본값을 자동으로 생성할때 사용한다. &lt;/p&gt;
&lt;p&gt;​                            &lt;/p&gt;
&lt;p&gt;​                                                                                         &lt;/p&gt;
&lt;p&gt;대략적으로, 아래와 같은 생성전략이 존재한다. &lt;/p&gt;
&lt;p&gt;​                            &lt;/p&gt;
&lt;p&gt;​                                                                                         &lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;생성 전략&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;GenerationType.IDENTITY&lt;/td&gt;
&lt;td&gt;데이터베이스에 키 생성방법을 위임&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GenerationType.AUTO&lt;/td&gt;
&lt;td&gt;각 데이터베이스 방언에 따라 자동으로 지정(기본 값)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GenerationType.TABLE&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GenerationType.SEQUENCE&lt;/td&gt;
&lt;td&gt;데이터베이스의 시퀸스를 이용해서 키 값을 생성&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;그럼 이제 &lt;strong&gt;@GeneratedValue 를 사용할때 생성 전략에 따라 Batch Insert 를 쓸수 있는지 없는지에&lt;/strong&gt; 대한 관계를 알아보도록 하겠다.&lt;/p&gt;
&lt;h2&gt;Hibernate Batch Insert&lt;/h2&gt;
&lt;p&gt;​                                                                &lt;/p&gt;
&lt;p&gt;​                                                                                                                       &lt;/p&gt;
&lt;p&gt;하이버네이트(Hibernate) 에서는 여러개의 데이터를 한번에 Insert, Update 하게 해주는 기능인 Batch 기능을 지원하고 있다.&lt;/p&gt;
&lt;p&gt;​                                                                                       &lt;/p&gt;
&lt;p&gt;보통 아래와 같이 옵션으로 지정이 가능하다.&lt;/p&gt;
&lt;p&gt;​                                                                                       &lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;  jpa:
    hibernate:
      ddl-auto: create
    properties:
      hibernate:
        generate_statistics: true
        dialect: org.hibernate.dialect.H2Dialect
        show_sql: true
        format_sql: true
        order_inserts: true
        order_updates: true
        jdbc:
          batch_size: 1000&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​                                                             &lt;/p&gt;
&lt;p&gt;그리고, Spring Data JPA 의 saveAll 함수를 사용하면, 여러개의 데이터를 한번에 Insert 또는 Update 할 수 있다.                      &lt;/p&gt;
&lt;p&gt;​                                                             &lt;/p&gt;
&lt;p&gt;하지만, @GeneratedValue 키 값 생성 전략을 Identity나 Auto로 정해놓는 경우 하이버네이트(Hibernate)에서 Batch Insert 기능을 비활성화 시켜놓고 Insert 작업을 수행하게 된다.&lt;/p&gt;
&lt;p&gt;​                                                             &lt;/p&gt;
&lt;p&gt;따라서, saveAll 과 같은 함수를 사용해도 데이터 개수만큼 INSERT 또는 UPDATE 쿼리가 나가게 된다.&lt;/p&gt;
&lt;p&gt;​                                                             &lt;/p&gt;
&lt;p&gt;그렇기 때문에, 만약 한꺼번에 여러개의 데이터를 Insert 또는 Update 해야되는 Entity의 경우 @GeneratedValue의 키값 생성 전략을 GenerationType.SEQUENCE 또는 GenerationType.TABLE 로 설정해놓고 사용하기를 바란다.&lt;/p&gt;
&lt;p&gt;​                                                             &lt;/p&gt;
&lt;p&gt;​                                                              &lt;/p&gt;
&lt;p&gt;​                                                                                                                         &lt;/p&gt;
&lt;h2&gt;출처&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;자바 ORM 표준 JPA 프로그래밍 - 기본편&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://stackoverflow.com/questions/59002107/spring-data-jpa-saveall-not-doing-batch-insert&quot;&gt;https://stackoverflow.com/questions/59002107/spring-data-jpa-saveall-not-doing-batch-insert&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.jboss.org/hibernate/orm/current/userguide/html_single/Hibernate_User_Guide.html#batch-session-batch&quot;&gt;https://docs.jboss.org/hibernate/orm/current/userguide/html_single/Hibernate_User_Guide.html#batch-session-batch&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>개발/Java</category>
      <category>Batch Insert</category>
      <category>BULK INSERT</category>
      <category>Hibernate</category>
      <category>java</category>
      <category>JPA</category>
      <category>ORM</category>
      <category>Spring Boot</category>
      <category>Spring Data JPA</category>
      <category>자바</category>
      <category>하이버네이트</category>
      <author>semtax</author>
      <guid isPermaLink="true">https://semtax.tistory.com/94</guid>
      <comments>https://semtax.tistory.com/94#entry94comment</comments>
      <pubDate>Sun, 7 Jun 2020 21:53:47 +0900</pubDate>
    </item>
    <item>
      <title>데이터베이스 DDL 자동 변환해주는 사이트 : SQLines</title>
      <link>https://semtax.tistory.com/93</link>
      <description>&lt;h2&gt;개요&lt;/h2&gt;
&lt;p&gt;이번 포스팅에서는 특정 데이터베이스에서 작성한 DDL(CREATE TABLE 이나 ADD INDEX) 문 들을 다른 데이터베이스의 DDL로 자동으로 변환해주는 사이트에 대해서 다루려고 한다.&lt;/p&gt;
&lt;h2&gt;사이트 사용법&lt;/h2&gt;
&lt;p&gt;먼저 아래 사이트에 방문을 해주자&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://www.sqlines.com/online&quot;&gt;http://www.sqlines.com/online&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;그리고 왼쪽에 DDL 문들을 입력하고, 사이트에 있는 Source 와 Target을 지정해주면 자동으로 변환이 된다.&lt;/p&gt;
&lt;p&gt;지원 DB 종류도 PostgreSQL, MySQL, MSSQL, Oracle 등등 다양하다.&lt;/p&gt;</description>
      <category>개발/데이터베이스</category>
      <category>db</category>
      <category>ddl</category>
      <category>mssql</category>
      <category>mysql</category>
      <category>oracle</category>
      <category>PostgreSQL</category>
      <category>데이터베이스</category>
      <category>마이그레이션</category>
      <author>semtax</author>
      <guid isPermaLink="true">https://semtax.tistory.com/93</guid>
      <comments>https://semtax.tistory.com/93#entry93comment</comments>
      <pubDate>Sun, 7 Jun 2020 15:19:18 +0900</pubDate>
    </item>
    <item>
      <title>톰캣 에서는 어떻게 JSESSIONID 를 만드는 것일까?</title>
      <link>https://semtax.tistory.com/92</link>
      <description>&lt;h2&gt;개요&lt;/h2&gt;
&lt;p&gt;​                                                                     &lt;/p&gt;
&lt;p&gt;이번 포스팅에서는 톰캣 에서 JSESSIONID를 어떤 방식으로 만드는지에 대해서 포스팅 해보겠습니다.&lt;/p&gt;
&lt;p&gt;​                                    &lt;/p&gt;
&lt;p&gt;​                                                     &lt;/p&gt;
&lt;p&gt;​                                                                                                   &lt;/p&gt;
&lt;h2&gt;왜 하필 톰캣인가?&lt;/h2&gt;
&lt;p&gt;​                                                                    &lt;/p&gt;
&lt;p&gt;​                                                                    &lt;/p&gt;
&lt;p&gt;사실 자바 서블릿은 완전한 구현체가 아닌 일종의 표준 내지 가이드 라인입니다.&lt;/p&gt;
&lt;p&gt;​                                &lt;/p&gt;
&lt;p&gt;즉, 껍데기만 존재하고 실제로는 서블릿 컨테이너를 구현하는 각 컨테이너 구현체 마다 실제적인 구현 방법은 전부 다릅니다.&lt;/p&gt;
&lt;p&gt;​                                                                    &lt;/p&gt;
&lt;p&gt;따라서, JSESSIONID 를 만드는 방식도 각 서블릿 컨테이너 구현체 마다 다릅니다.&lt;/p&gt;
&lt;p&gt;​                                                                    &lt;/p&gt;
&lt;p&gt;그리고, 다른 서블릿 컨테이너 구현체도 많지만.. 그래도 가장 유명(?) 하고 가장 오래된 서블릿 컨테이너 구현체가 톰캣이라, 톰캣을 고르게 되었습니다.&lt;/p&gt;
&lt;p&gt;​                                                                    &lt;/p&gt;
&lt;p&gt;​                                                                    &lt;/p&gt;
&lt;p&gt;​                                                                    &lt;/p&gt;
&lt;h2&gt;그럼 JSESSIONID는 뭐하는데 쓰는 것일까?&lt;/h2&gt;
&lt;p&gt;​                                    &lt;/p&gt;
&lt;p&gt;​                                    &lt;/p&gt;
&lt;p&gt;먼저, JSESSIONID가 무엇인지 알기 전에 아래와 같은 질문을 해봅시다.&lt;/p&gt;
&lt;p&gt;​                                    &lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;세션은 도대체 무엇인가?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;​                                    &lt;/p&gt;
&lt;p&gt;사실 세션은 진짜로 앞뒤 다 떼고 이야기 하면 Key-Value 저장소 즉, 자바의 Map 입니다.&lt;/p&gt;
&lt;p&gt;​                                                                    &lt;/p&gt;
&lt;p&gt;실제로도 세션 구현체 내부에서도 자바의 ConcurrentMap 을 사용해서 세션을 구현하고 있습니다.&lt;/p&gt;
&lt;p&gt;​                                                                                    &lt;/p&gt;
&lt;p&gt;그리고 나면 다음 질문도 해볼 수 있습니다.&lt;/p&gt;
&lt;p&gt;​                                                                                    &lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;그렇다면 사용자가 여러명이면 세션도 여러개일텐데, 어떻게 구현 하는가?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;​                                                                                    &lt;/p&gt;
&lt;p&gt;사실 이 질문도 간단합니다. 대략적으로 아래와 같은 구조를 가지는 자료구조를 만들면 됩니다.&lt;/p&gt;
&lt;p&gt;​                                                                    &lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Map&amp;lt;SessionStorageKey, SessionStorage&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​                                                                                                    &lt;/p&gt;
&lt;p&gt;저 위에 있는 SessionStorageKey 의 역할을 하는것이 바로 &lt;strong&gt;JSESSIONID&lt;/strong&gt; 입니다.&lt;/p&gt;
&lt;p&gt;​                                                                                                                    &lt;/p&gt;
&lt;p&gt;결국, &lt;strong&gt;JSESSIONID&lt;/strong&gt;는 서블릿 컨테이너가 있는 웹서버에 접속한 여러 사용자 각각의 세션 공간을 관리하기 위해 만들어진 일종의 키값이라고 보면 됩니다.&lt;/p&gt;
&lt;p&gt;​                                                                                                    &lt;/p&gt;
&lt;p&gt;코드로 생각해보면 대략적으로 아래 구조라고 생각하시면 됩니다.&lt;/p&gt;
&lt;p&gt;​                                                                    &lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Map&amp;lt;JSESSIONID, Map&amp;lt;AttrubiteKey, Value&amp;gt;&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​                                                                    &lt;/p&gt;
&lt;p&gt;​                                                                    &lt;/p&gt;
&lt;p&gt;이제, 톰캣에서 JSESSIONID를 어떻게 만드는지 알아보도록 합시다.&lt;/p&gt;
&lt;p&gt;​                                                                    &lt;/p&gt;
&lt;p&gt;​                                                                    &lt;/p&gt;
&lt;p&gt;​                                                                    &lt;/p&gt;
&lt;h2&gt;톰캣에서는 어떻게 JSESSIONID 를 만들까?&lt;/h2&gt;
&lt;p&gt;​                                                                            &lt;/p&gt;
&lt;p&gt;​                                                                            &lt;/p&gt;
&lt;p&gt;그럼 이제 톰캣(Tomcat) 에서 &lt;strong&gt;JSESSIONID&lt;/strong&gt; 를 어떻게 만드는지 알아보도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;​                                                                            &lt;/p&gt;
&lt;p&gt;일단, HttpServletRequest 부터 계속 파고들면서 &lt;strong&gt;JSESSIONID&lt;/strong&gt; 를 찾는것도 좋기는 하지만, 귀찮으므로(?) 저 찾는 과정을 생략하고, 어디에 있는지부터 바로 말하도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;​                                                                           &lt;/p&gt;
&lt;p&gt;실제로 JSESSIONID 를 만드는 부분은  &lt;strong&gt;org.apache.catalina.session&lt;/strong&gt; 패키지의 ManagerBase 클래스의 &lt;strong&gt;createSession&lt;/strong&gt; 메소드 에서 &lt;strong&gt;JSESSIONID&lt;/strong&gt; 를 아래와 같이 생성 하게 됩니다.&lt;/p&gt;
&lt;p&gt;​                                &lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;package org.apache.catalina.session;

....


public abstract class ManagerBase extends LifecycleMBeanBase implements Manager {


    ......


        public Session createSession(String sessionId) {

        if ((maxActiveSessions &amp;gt;= 0) &amp;amp;&amp;amp;
                (getActiveSessions() &amp;gt;= maxActiveSessions)) {
            rejectedSessions++;
            throw new TooManyActiveSessionsException(
                    sm.getString(&amp;quot;managerBase.createSession.ise&amp;quot;),
                    maxActiveSessions);
        }

        // Recycle or create a Session instance
        Session session = createEmptySession();

        // Initialize the properties of the new session and return it
        session.setNew(true);
        session.setValid(true);
        session.setCreationTime(System.currentTimeMillis());
        session.setMaxInactiveInterval(getContext().getSessionTimeout() * 60);
        String id = sessionId;
        if (id == null) {
            id = generateSessionId();
        }
        session.setId(id);
        sessionCounter++;

        SessionTiming timing = new SessionTiming(session.getCreationTime(), 0);
        synchronized (sessionCreationTiming) {
            sessionCreationTiming.add(timing);
            sessionCreationTiming.poll();
        }
        return session;
    }


    ......


}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​                                &lt;/p&gt;
&lt;p&gt;​                                &lt;/p&gt;
&lt;p&gt;그리고 위의 코드 처럼 &lt;strong&gt;createSession&lt;/strong&gt; 메소드는 내부적으로 &lt;strong&gt;org.apache.catalina.util&lt;/strong&gt; 패키지의 StandardSessionIdGenerator 클래스를 사용하여 &lt;strong&gt;JSESSIONID&lt;/strong&gt; 를 생성 하게 됩니다.&lt;/p&gt;
&lt;p&gt;​                                                                            &lt;/p&gt;
&lt;p&gt;실제 코드는 아래와 같습니다.&lt;/p&gt;
&lt;p&gt;​                                                                            &lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;package org.apache.catalina.util;

public class StandardSessionIdGenerator extends SessionIdGeneratorBase {

    @Override
    public String generateSessionId(String route) {

        byte random[] = new byte[16];
        int sessionIdLength = getSessionIdLength();

        // Render the result as a String of hexadecimal digits
        // Start with enough space for sessionIdLength and medium route size
        StringBuilder buffer = new StringBuilder(2 * sessionIdLength + 20);

        int resultLenBytes = 0;

        while (resultLenBytes &amp;lt; sessionIdLength) {
            getRandomBytes(random);
            for (int j = 0;
            j &amp;lt; random.length &amp;amp;&amp;amp; resultLenBytes &amp;lt; sessionIdLength;
            j++) {
                byte b1 = (byte) ((random[j] &amp;amp; 0xf0) &amp;gt;&amp;gt; 4);
                byte b2 = (byte) (random[j] &amp;amp; 0x0f);
                if (b1 &amp;lt; 10)
                    buffer.append((char) (&amp;#39;0&amp;#39; + b1));
                else
                    buffer.append((char) (&amp;#39;A&amp;#39; + (b1 - 10)));
                if (b2 &amp;lt; 10)
                    buffer.append((char) (&amp;#39;0&amp;#39; + b2));
                else
                    buffer.append((char) (&amp;#39;A&amp;#39; + (b2 - 10)));
                resultLenBytes++;
            }
        }

        if (route != null &amp;amp;&amp;amp; route.length() &amp;gt; 0) {
            buffer.append(&amp;#39;.&amp;#39;).append(route);
        } else {
            String jvmRoute = getJvmRoute();
            if (jvmRoute != null &amp;amp;&amp;amp; jvmRoute.length() &amp;gt; 0) {
                buffer.append(&amp;#39;.&amp;#39;).append(jvmRoute);
            }
        }

        return buffer.toString();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​                                                                                &lt;/p&gt;
&lt;p&gt;위 코드를 보면, 실제 세션 길이의 2배 하고 20 바이트 더 많은 공간을 버퍼 공간으로 먼저 잡습니다.&lt;/p&gt;
&lt;p&gt;​                                                                &lt;/p&gt;
&lt;p&gt;실제 JSESSION 값을 생성하는 과정은 아래와 같습니다.&lt;/p&gt;
&lt;p&gt;​                                                &lt;/p&gt;
&lt;p&gt;먼저, 16바이트 단위로 랜덤 값을 가지고 와서 문자열 형태의 16진수로 변환합니다. &lt;/p&gt;
&lt;p&gt;다음으로, 라우트 값이 있는 경우, 랜덤값 뒤에 &amp;quot;.&amp;quot; 문자와 같이 라우트 값을 합쳐줍니다. &lt;/p&gt;
&lt;p&gt;마지막으로 jvmRoute 값을 &amp;quot;.&amp;quot; 문자와 같이 합쳐줍니다.&lt;/p&gt;
&lt;p&gt;​                                                                &lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;참고로 저 jvmRoute 값과, route 값의 경우 서블릿 컨테이너에 접속한 사용자를 구분해주는 값이라고 생각하면 됩니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;​                                                                &lt;/p&gt;
&lt;p&gt;그리고 나서 실제 JSESSIONID 값을 반환합니다.&lt;/p&gt;
&lt;p&gt;​                                                                &lt;/p&gt;
&lt;p&gt;다음으로, 실제로 랜덤값을 가지고 오는 부분의 코드 입니다.&lt;/p&gt;
&lt;p&gt;​                                                                &lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;package org.apache.catalina.util;

import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.SecureRandom;

...


....

protected void getRandomBytes(byte bytes[]) {

  SecureRandom random = randoms.poll();
  if (random == null) {
    random = createSecureRandom();
  }
  random.nextBytes(bytes);
  randoms.add(random);
}

...


private SecureRandom createSecureRandom() {

  SecureRandom result = null;

  long t1 = System.currentTimeMillis();
  if (secureRandomClass != null) {
    try {
      // Construct and seed a new random number generator
      Class&amp;lt;?&amp;gt; clazz = Class.forName(secureRandomClass);
      result = (SecureRandom) clazz.getConstructor().newInstance();
    } catch (Exception e) {
      log.error(sm.getString(&amp;quot;sessionIdGeneratorBase.random&amp;quot;,
                             secureRandomClass), e);
    }
  }

  boolean error = false;
  if (result == null) {
    // No secureRandomClass or creation failed. Use SecureRandom.
    try {
      if (secureRandomProvider != null &amp;amp;&amp;amp;
          secureRandomProvider.length() &amp;gt; 0) {
        result = SecureRandom.getInstance(secureRandomAlgorithm,
                                          secureRandomProvider);
      } else if (secureRandomAlgorithm != null &amp;amp;&amp;amp;
                 secureRandomAlgorithm.length() &amp;gt; 0) {
        result = SecureRandom.getInstance(secureRandomAlgorithm);
      }
    } catch (NoSuchAlgorithmException e) {
      error = true;
      log.error(sm.getString(&amp;quot;sessionIdGeneratorBase.randomAlgorithm&amp;quot;,
                             secureRandomAlgorithm), e);
    } catch (NoSuchProviderException e) {
      error = true;
      log.error(sm.getString(&amp;quot;sessionIdGeneratorBase.randomProvider&amp;quot;,
                             secureRandomProvider), e);
    }
  }

  if (result == null &amp;amp;&amp;amp; error) {
    // Invalid provider / algorithm
    try {
      result = SecureRandom.getInstance(&amp;quot;SHA1PRNG&amp;quot;);
    } catch (NoSuchAlgorithmException e) {
      log.error(sm.getString(&amp;quot;sessionIdGeneratorBase.randomAlgorithm&amp;quot;,
                             secureRandomAlgorithm), e);
    }
  }

  if (result == null) {
    // Nothing works - use platform default
    result = new SecureRandom();
  }

  // Force seeding to take place
  result.nextInt();

  long t2 = System.currentTimeMillis();
  if ((t2 - t1) &amp;gt; 100) {
    log.warn(sm.getString(&amp;quot;sessionIdGeneratorBase.createRandom&amp;quot;,
                          result.getAlgorithm(), Long.valueOf(t2 - t1)));
  }
  return result;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​                                &lt;/p&gt;
&lt;p&gt;사실 위의 코드도, 다양한 JVM 버전에서 동작해야 되서 예외 처리 때문에 매우 복잡해 보이지만 결국 랜덤값을 SHA1PRNG 또는 각 플랫폼의 기본 난수생성기를 사용해서 가져 오게 됩니다.&lt;/p&gt;
&lt;p&gt;​                                &lt;/p&gt;
&lt;p&gt;(사실상, 완전히 구형 플랫폼이나 특수한 환경이 아닌 경우, 대부분 SHA1PRNG 알고리즘을 사용해서 랜덤값을 가지고 온다고 생각하면 됩니다.)&lt;/p&gt;
&lt;p&gt;​                                &lt;/p&gt;
&lt;p&gt;그리고 위의 코드를 보고 &amp;quot;어 그냥 Random 클래스에서 랜덤값 쓰면 안되요?&amp;quot; 라고 물을 수 있습니다. &lt;/p&gt;
&lt;p&gt;​                                &lt;/p&gt;
&lt;p&gt;하지만, 암호학적으로 안전한 난수생성기를 쓰지 않는 경우, 사용자나 공격자들이 무식하게 경우의 수를 전부 미리 계산하고 그 결과를 이용해서 Bruteforce 하게 세션값을 위조해버리거나 조작해버리는 경우도 생길 수 있습니다.&lt;/p&gt;
&lt;p&gt;​                                                &lt;/p&gt;
&lt;p&gt;실제로 2012년에 PHP에서 비슷한 취약점이 발견되기도 했었습니다.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://alphaninesec.wordpress.com/2016/10/05/brute-forcing-php-session-ids/&quot;&gt;https://alphaninesec.wordpress.com/2016/10/05/brute-forcing-php-session-ids/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;​                                                                                                            &lt;/p&gt;
&lt;p&gt;​                                            &lt;/p&gt;
&lt;p&gt;​                                                                            &lt;/p&gt;
&lt;p&gt;​                                                                &lt;/p&gt;
&lt;h2&gt;출처&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/apache/tomcat/blob/1d1d835a6784854e26c4fff8e23aef128c105f3c/java/org/apache/catalina/util/StandardSessionIdGenerator.java#L21&quot;&gt;https://github.com/apache/tomcat/blob/1d1d835a6784854e26c4fff8e23aef128c105f3c/java/org/apache/catalina/util/StandardSessionIdGenerator.java#L21&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/apache/tomcat/blob/1d1d835a6784854e26c4fff8e23aef128c105f3c/java/org/apache/catalina/util/SessionIdGeneratorBase.java#L214&quot;&gt;https://github.com/apache/tomcat/blob/1d1d835a6784854e26c4fff8e23aef128c105f3c/java/org/apache/catalina/util/SessionIdGeneratorBase.java#L214&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://cris.joongbu.ac.kr/course/java/api/java/security/SecureRandom.html&quot;&gt;http://cris.joongbu.ac.kr/course/java/api/java/security/SecureRandom.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://alphaninesec.wordpress.com/2016/10/05/brute-forcing-php-session-ids/&quot;&gt;https://alphaninesec.wordpress.com/2016/10/05/brute-forcing-php-session-ids/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://jojoldu.tistory.com/118&quot;&gt;https://jojoldu.tistory.com/118&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>개발/Java</category>
      <category>Java Session</category>
      <category>jsessionid</category>
      <category>Servlet</category>
      <category>Session</category>
      <category>서블릿</category>
      <category>서블릿 내부구조</category>
      <category>서블릿 컨테이너</category>
      <category>세션</category>
      <category>세션 내부구조</category>
      <category>자바 세션</category>
      <author>semtax</author>
      <guid isPermaLink="true">https://semtax.tistory.com/92</guid>
      <comments>https://semtax.tistory.com/92#entry92comment</comments>
      <pubDate>Thu, 28 May 2020 23:49:44 +0900</pubDate>
    </item>
    <item>
      <title>JPA를 이용해서 RDB에 공간정보 저장하기</title>
      <link>https://semtax.tistory.com/91</link>
      <description>&lt;h2&gt;개요&lt;/h2&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;이번 포스팅에서는 JPA를 이용해서 MySQL에 위치정보를 저장하는 법에 대해서 다뤄보도록 하겠다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&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;​&lt;/span&gt;​&lt;/p&gt;
&lt;h2&gt;공간 정보(Geometry 타입)?&lt;/h2&gt;
&lt;p&gt;​&lt;span style=&quot;color: #333333;&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;​&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;MySQL(사실, 다른 RDB에서도) 에서는, GPS좌표나 다각형과 같은 공간/기하 데이터를 저장할 수 있는 Geometry 타입을 제공한다.&lt;/p&gt;
&lt;p&gt;​&lt;span style=&quot;color: #333333;&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;​&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;MySQL과 같은 경우 아래 목록의 타입들을 제공한다&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&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;​&lt;/span&gt;​&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;데이터 타입&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;설명&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;Point&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;좌표 공간에서 한 지점의 위치를 표시&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #333333;&quot;&gt;[ Ex : Point(10,10) ]&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;LineString&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;다수의 점을 연결해주는 선분&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #333333;&quot;&gt;[ Ex : LINESTRING(10 10, 20 25, 15 40) ]&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;Polygon&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;다수의 선분들이 연결되있는 다각형&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #333333;&quot;&gt;[ Ex : POLYGON(10 10, 10 20, 20 20, 20 10, 10 10) ]&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;Multi-Point&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;다수개의 점들의 집합&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #333333;&quot;&gt;[ Ex : MULTIPOINT(10 10, 30 20) ]&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;Multi-LineString&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;다수개의 선분들의 집합&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #333333;&quot;&gt;[ Ex : MULTILINESTRING((10 10, 20 25, 15 40), (50 50, 85 105, 120 160)) ]&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;Multi-Polygon&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;다수개의 다각형들의 집합&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #333333;&quot;&gt;[ Ex : MULTIPOLYGON((10 10, 10 20, 20 20, 20 10, 10 10), (10 10, 10 20, 20 20, 20 10, 10 10)) ]&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;GeomCollection&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;모든 공간데이터들의 집합&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #333333;&quot;&gt;[ Ex : GEOMCOLLECTION(POINT(10,10), LINESTRING(10 10, 20 25, 15 40)) ]&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&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;​&lt;/span&gt;&lt;span style=&quot;color: #333333;&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;​&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;color: #333333;&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;​&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;이제 이러한 공간정보를 스프링에서 제공하는 ORM인 JPA(+hibernate)를 이용해서 저장해보는 예제를 작성해보자.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;해당 예제에서는 Point(위도/경도) 정보를 저장/불러오는 예제를 다루도록 하겠다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;h2&gt;예제&lt;/h2&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;먼저 스프링 프로젝트를 만들고 아래와 같이 Maven에 의존성을 추가해주자.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;lt;project xmlns=&quot;http://maven.apache.org/POM/4.0.0&quot;
         xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;
         xsi:schemaLocation=&quot;http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd&quot;&amp;gt;
    &amp;lt;modelVersion&amp;gt;4.0.0&amp;lt;/modelVersion&amp;gt;

    &amp;lt;groupId&amp;gt;com.semtax.geometryexample&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;geometryexample&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;1.0-SNAPSHOT&amp;lt;/version&amp;gt;

    &amp;lt;dependencies&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-boot-starter-web&amp;lt;/artifactId&amp;gt;
            &amp;lt;version&amp;gt;2.1.6.RELEASE&amp;lt;/version&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-boot-starter-data-jpa&amp;lt;/artifactId&amp;gt;
            &amp;lt;version&amp;gt;2.1.6.RELEASE&amp;lt;/version&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;com.h2database&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;h2&amp;lt;/artifactId&amp;gt;
            &amp;lt;scope&amp;gt;test&amp;lt;/scope&amp;gt;
            &amp;lt;version&amp;gt;1.4.199&amp;lt;/version&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-boot-starter-test&amp;lt;/artifactId&amp;gt;
            &amp;lt;version&amp;gt;2.1.6.RELEASE&amp;lt;/version&amp;gt;
            &amp;lt;scope&amp;gt;test&amp;lt;/scope&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.hibernate&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;hibernate-spatial&amp;lt;/artifactId&amp;gt;
            &amp;lt;version&amp;gt;5.3.10.Final&amp;lt;/version&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.projectlombok&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;lombok&amp;lt;/artifactId&amp;gt;
            &amp;lt;version&amp;gt;1.18.8&amp;lt;/version&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;com.github.gavlyukovskiy&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;p6spy-spring-boot-starter&amp;lt;/artifactId&amp;gt;
            &amp;lt;version&amp;gt;1.5.7&amp;lt;/version&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;mysql&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;mysql-connector-java&amp;lt;/artifactId&amp;gt;
            &amp;lt;version&amp;gt;8.0.19&amp;lt;/version&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;ch.vorburger.mariaDB4j&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;mariaDB4j&amp;lt;/artifactId&amp;gt;
            &amp;lt;version&amp;gt;2.2.3&amp;lt;/version&amp;gt;
        &amp;lt;/dependency&amp;gt;
    &amp;lt;/dependencies&amp;gt;

    &amp;lt;build&amp;gt;
        &amp;lt;sourceDirectory&amp;gt;src&amp;lt;/sourceDirectory&amp;gt;
        &amp;lt;plugins&amp;gt;
            &amp;lt;plugin&amp;gt;
                &amp;lt;artifactId&amp;gt;maven-compiler-plugin&amp;lt;/artifactId&amp;gt;
                &amp;lt;version&amp;gt;3.1&amp;lt;/version&amp;gt;
                &amp;lt;configuration&amp;gt;
                    &amp;lt;source&amp;gt;1.8&amp;lt;/source&amp;gt;
                    &amp;lt;target&amp;gt;1.8&amp;lt;/target&amp;gt;
                &amp;lt;/configuration&amp;gt;
            &amp;lt;/plugin&amp;gt;
            &amp;lt;plugin&amp;gt;
                &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
                &amp;lt;artifactId&amp;gt;spring-boot-maven-plugin&amp;lt;/artifactId&amp;gt;
            &amp;lt;/plugin&amp;gt;

            &amp;lt;plugin&amp;gt;
                &amp;lt;groupId&amp;gt;org.apache.maven.plugins&amp;lt;/groupId&amp;gt;
                &amp;lt;artifactId&amp;gt;maven-surefire-plugin&amp;lt;/artifactId&amp;gt;
                &amp;lt;version&amp;gt;2.22.0&amp;lt;/version&amp;gt;
            &amp;lt;/plugin&amp;gt;
        &amp;lt;/plugins&amp;gt;
    &amp;lt;/build&amp;gt;

&amp;lt;/project&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;공간 데이터 저장을 위해 hibernate의 &lt;b&gt;hibernate-spatial&lt;/b&gt; 라이브러리를 의존성으로 추가했다.&lt;/p&gt;
&lt;p&gt;해당 라이브러리의 버전은 스프링부트의 hibernate 버전에 맞춰주면 된다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;다음으로 아래와 같이 application.properties 를 작성해준다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;spring.datasource.hikari.maximum-pool-size=10

spring.datasource.url=jdbc:mysql://localhost:3306/testdb?useSSL=false&amp;amp;allowPublicKeyRetrieval=true&amp;amp;characterEncoding=UTF-8&amp;amp;serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=root

spring.jpa.hibernate.use-new-id-generator-mappings=false

spring.jpa.hibernate.ddl-auto=create
spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true
spring.jpa.properties.hibernate.dialect=org.hibernate.spatial.dialect.mysql.MySQL56InnoDBSpatialDialect
spring.jpa.properties.hibernate.current_session_context_class=org.springframework.orm.hibernate5.SpringSessionContext

server.port=45000&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;먼저 MySQL 8 이상을 쓰는 경우 위와 같이 datasource.url을 작성해야 에러가 발생하지 않는다.&lt;/p&gt;
&lt;p&gt;(기본적으로 SSL 통신을 사용함 + UTC 시간 지정이 필요)&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;그리고, JPA 구현체로 hibernate를 사용하겠다는 설정과, 기타 설정들을 지정해준다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;다음으로 아래와 같이 위치정보가 포함된 Entity 객체를 작성해준다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;package main.java.entity;

import org.springframework.data.geo.Point;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import java.util.Objects;

@Entity
public class PointEntry {

    @Id
    @GeneratedValue
    private Long id;

    private Point point;

    public PointEntry() {
    }

    public PointEntry(Point point) {
        this.point = point;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public Point getPoint() {
        return point;
    }

    public void setPoint(Point point) {
        this.point = point;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        PointEntry that = (PointEntry) o;
        return Objects.equals(id, that.id) &amp;amp;&amp;amp;
                Objects.equals(point, that.point);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id, point);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;그리고 나서, 위에서 선언한 엔티티(개체)에 대한 Repository 를 작성해준다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;package main.java.repositories;


import main.java.entity.PointEntry;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface PointEntryRepository extends JpaRepository&amp;lt;PointEntry, Long&amp;gt; {
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;다음으로, 실제 위치정보를 저장하고 불러오는 컨트롤러들을 작성해준다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;package main.java.controller;


import main.java.entity.PointEntry;
import main.java.repositories.PointEntryRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.geo.Point;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;
import java.util.Random;


@RestController
public class TestController {
    @Autowired
    PointEntryRepository pointEntryRepository;

    @GetMapping(&quot;/&quot;)
    public List&amp;lt;PointEntry&amp;gt; mainPage(){
        return pointEntryRepository.findAll();
    }

    @GetMapping(&quot;/insert&quot;)
    public String insertPage(){
        Random r = new Random();

        PointEntry pointEntry = new PointEntry(new Point(r.nextDouble(),r.nextDouble()));
        pointEntryRepository.save(pointEntry);
        return &quot;success&quot;;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;위의 컨트롤러에서는, &quot;/insert&quot; 페이지를 통해 랜덤으로 위도/경도 정보를 저장하고 &quot;/&quot; 페이지를 이용해서 지금까지 저장한 모든 위치정보(위도/경도) 들을 출력 해주는 역할을 수행하게 된다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;마지막으로 Main 함수를 아래와 같이 작성해준다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;package main.java;


import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SpringGeometryExample {

    public static void main(String[] args) {
        SpringApplication.run(SpringGeometryExample.class, args);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;이제 실제로 위에서 작성한 예제를 실행해보도록 하자.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;h2&gt;실행 결과&lt;/h2&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;먼저 아래와 같이 /insert 페이지를 연속적으로 방문해서 위도/경도 데이터를 넣어주자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nxs7G/btqEq7UDti2/5zKbrpYErO35RvEQDgayp0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nxs7G/btqEq7UDti2/5zKbrpYErO35RvEQDgayp0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nxs7G/btqEq7UDti2/5zKbrpYErO35RvEQDgayp0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fnxs7G%2FbtqEq7UDti2%2F5zKbrpYErO35RvEQDgayp0%2Fimg.png&quot; width=&quot;100%&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;그리고 아래 페이지를 방문하면 위도/경도 목록들을 얻을 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bcWJyi/btqEsEjEQaE/F5YkDs1M34TtsQDPqaBhW1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bcWJyi/btqEsEjEQaE/F5YkDs1M34TtsQDPqaBhW1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bcWJyi/btqEsEjEQaE/F5YkDs1M34TtsQDPqaBhW1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbcWJyi%2FbtqEsEjEQaE%2FF5YkDs1M34TtsQDPqaBhW1%2Fimg.png&quot; width=&quot;100%&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2&gt;출처&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;http://www.gurubee.net/lecture/2921&quot;&gt;http://www.gurubee.net/lecture/2921&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://mkyong.com/spring-boot/spring-boot-spring-data-jpa-mysql-example/&quot;&gt;https://mkyong.com/spring-boot/spring-boot-spring-data-jpa-mysql-example/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://stackoverflow.com/questions/40240955/why-i-have-this-error-trying-to-use-hibernate-spatial-into-a-spring-boot-project&quot;&gt;https://stackoverflow.com/questions/40240955/why-i-have-this-error-trying-to-use-hibernate-spatial-into-a-spring-boot-project&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.baeldung.com/hibernate-spatial&quot;&gt;https://www.baeldung.com/hibernate-spatial&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>개발/Java</category>
      <category>geometry</category>
      <category>Geometry type</category>
      <category>JPA</category>
      <category>JPA Spatial</category>
      <category>mysql</category>
      <category>Spatial</category>
      <category>Spatial Data</category>
      <category>공간 정보</category>
      <category>공간정보 저장하기</category>
      <category>위도 경도 저장</category>
      <author>semtax</author>
      <guid isPermaLink="true">https://semtax.tistory.com/91</guid>
      <comments>https://semtax.tistory.com/91#entry91comment</comments>
      <pubDate>Tue, 26 May 2020 23:41:51 +0900</pubDate>
    </item>
    <item>
      <title>자바 DocumentBuilder 로 UTF-8 인코딩 된 XML 파싱 하는 법</title>
      <link>https://semtax.tistory.com/90</link>
      <description>&lt;h2&gt;개요&lt;/h2&gt;
&lt;p&gt;​                                              &lt;/p&gt;
&lt;p&gt;이번 포스팅에서는, javax.xml.Parsers.DocumentBuilder 에서 UTF-8로 인코딩된 XML 파일을 파싱하는 법에 대해 알아보도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;​                                              &lt;/p&gt;
&lt;p&gt;​                                              &lt;/p&gt;
&lt;h2&gt;일단 그냥 파싱해보자&lt;/h2&gt;
&lt;p&gt;​                                              &lt;/p&gt;
&lt;p&gt;먼저 DocumentBuilder 를 이용해서 UTF-8로 인코딩 된 XML 파일을 파싱 해봅시다.&lt;/p&gt;
&lt;p&gt;​                                              &lt;/p&gt;
&lt;p&gt;​                                              &lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;String path = &amp;quot;./example1.xml&amp;quot;
File xmlFile = new File(path);

DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder docBuilder = docBuilderFactory.newDocumentBuilder();
Document doc = docBuilder.parse(is);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​                                              &lt;/p&gt;
&lt;p&gt;​                                              &lt;/p&gt;
&lt;p&gt;그럼 다음과 같은 에러가 발생하게 됩니다.&lt;/p&gt;
&lt;p&gt;​                                              &lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;[Fatal Error] :1:1: Content is not allowed in prolog. org.xml.sax.SAXParseException; lineNumber: 1; columnNumber: 1; Content is not allowed in prolog. at com.sun.org.apache.xerces.internal.parsers.DOMParser.parse(DOMParser.java:251) at com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderImpl.parse(DocumentBuilderImpl.java:300) at com.kcs.xml.ParseXMLOld.parseUTF8XML(ParseXMLOld.java:34) at com.kcs.xml.ParseXMLOld.main(ParseXMLOld.java:19)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​                                              &lt;/p&gt;
&lt;p&gt;​                                              &lt;/p&gt;
&lt;p&gt;​                                              &lt;/p&gt;
&lt;p&gt;사실, 일반 아스키 코드 범위내에 있는 문자들만 들어있는 경우 파싱에러가 나지 않습니다.&lt;/p&gt;
&lt;p&gt;하지만, 가끔가다 Non-Ascii 문자들이 있는 경우 위와 같은 에러가 발생하게 됩니다.&lt;/p&gt;
&lt;p&gt;​                                              &lt;/p&gt;
&lt;p&gt;재미있는 것은, 파싱시 주석에 Non-Ascii 문자들이 있는 경우에도 에러가 발생하게 됩니다.&lt;/p&gt;
&lt;p&gt;​                                              &lt;/p&gt;
&lt;p&gt;​                                              &lt;/p&gt;
&lt;p&gt;​                                              &lt;/p&gt;
&lt;h2&gt;UTF-8로 인코딩해서 파싱하는법&lt;/h2&gt;
&lt;p&gt;​                                              &lt;/p&gt;
&lt;p&gt;아래 코드와 같이 UTF-8로 인코딩된 XML문서를 파싱할 수 있습니다.&lt;/p&gt;
&lt;p&gt;​                                              &lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;String path = &amp;quot;./example1.xml&amp;quot;

File xmlFile = new File(path);
InputStream inputStream = new FileInputStream(file);
Reader reader = new InputStreamReader(inputStream,&amp;quot;UTF-8&amp;quot;);
InputSource is = new InputSource(reader);
is.setEncoding(&amp;quot;UTF-8&amp;quot;);

DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder docBuilder = docBuilderFactory.newDocumentBuilder();
Document doc = docBuilder.parse(is);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​                                              &lt;/p&gt;
&lt;p&gt;위 코드를 실행시켜서 파싱하는 경우, 정상적으로 파싱이 되는것을 알 수 있습니다.&lt;/p&gt;
&lt;p&gt;​                                              &lt;/p&gt;
&lt;p&gt;​                                              &lt;/p&gt;</description>
      <category>개발/Java</category>
      <category>Document Builder</category>
      <category>java</category>
      <category>sax parser</category>
      <category>UTF-8</category>
      <category>XML</category>
      <category>XML 파서</category>
      <category>xml 파싱</category>
      <category>인코딩</category>
      <category>자바</category>
      <author>semtax</author>
      <guid isPermaLink="true">https://semtax.tistory.com/90</guid>
      <comments>https://semtax.tistory.com/90#entry90comment</comments>
      <pubDate>Fri, 22 May 2020 22:38:55 +0900</pubDate>
    </item>
    <item>
      <title>안드로이드 universalApk 옵션과 빌드 시, AndroidManifest.xml 파일 위치</title>
      <link>https://semtax.tistory.com/89</link>
      <description>&lt;h2&gt;개요&lt;/h2&gt;
&lt;p&gt;​                                        &lt;/p&gt;
&lt;p&gt;​                          &lt;/p&gt;
&lt;p&gt;이번 포스팅에서는 안드로이드 split, 그리고 universalApk 옵션과 그에 따른 빌드 중간 파일 위치에 대해 다뤄 보도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;​                                        &lt;/p&gt;
&lt;p&gt;(사실 회사에서 버그잡는걸로 삽질하다가 알아낸 내용입니다..)&lt;/p&gt;
&lt;p&gt;​                          &lt;/p&gt;
&lt;p&gt;까먹지 않게 올려봅니다.&lt;/p&gt;
&lt;p&gt;​                                        &lt;/p&gt;
&lt;p&gt;​                                        &lt;/p&gt;
&lt;h2&gt;Android gradle splits, universalApk&lt;/h2&gt;
&lt;p&gt;​                                        &lt;/p&gt;
&lt;p&gt;안드로이드에서, universalApk 옵션은 splits 블록에 포함되어 있으며, 다양한 타깃머신(arm, x86)에 대한 apk, 즉 다중 머신 빌드를 위해 존재합니다.&lt;/p&gt;
&lt;p&gt;​                                        &lt;/p&gt;
&lt;p&gt;해당옵션을 이용해서 타깃 머신별로 빌드 파일 또는 프로젝트를 따로 만들 필요 없이 다양한 환경을 지원하는 apk를 만들 수 있습니다.&lt;/p&gt;
&lt;p&gt;​                                        &lt;/p&gt;
&lt;p&gt;보통 아래와 같이 설정합니다.&lt;/p&gt;
&lt;p&gt;​                          &lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-groovy&quot;&gt;android {
  splits {
    abi {
          // Enables building multiple APKs per ABI.
          enable true

          // By default all ABIs are included, so use reset() and include to specify that we only
          // want APKs for x86 and x86_64.

          // Resets the list of ABIs that Gradle should create APKs for to none.
          reset()

          // Specifies a list of ABIs that Gradle should create APKs for.
          include &amp;quot;x86&amp;quot;, &amp;quot;x86_64&amp;quot;

          // Specifies that we do not want to also generate a universal APK that includes all ABIs.
          universalApk false
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​                                        &lt;/p&gt;
&lt;p&gt;이제 해당 옵션에 따라서 빌드 과정에서 생성된 병합된 AndroidManifest.xml 파일 의 위치가 어떻게 바뀌는지 알아보도록 합시다.&lt;/p&gt;
&lt;p&gt;​                                                      &lt;/p&gt;
&lt;h2&gt;병합된 Manifest 파일&lt;/h2&gt;
&lt;p&gt;​                                      &lt;/p&gt;
&lt;p&gt;안드로이드에서 APK 빌드를 진행할때, APK 파일에는 &lt;code&gt;AndroidManifest.xml&lt;/code&gt; 파일이 하나만 포함됩니다.&lt;/p&gt;
&lt;p&gt;​                                      &lt;/p&gt;
&lt;p&gt;그런데, 안드로이드 스튜디오 프로젝트에는 기본 소스 세트(SourceSets), 빌드 변형(BuildVariants) 및 외부 라이브러리에서 제공하는 여러 파일들이 포함될 수 있습니다. &lt;/p&gt;
&lt;p&gt;​                                      &lt;/p&gt;
&lt;p&gt;따라서 앱을 빌드할 때 모든 manifest 파일을 APK에 패키징되는 단일 manifest 파일로 병합 됩니다.                                          &lt;/p&gt;
&lt;p&gt;​                                      &lt;/p&gt;
&lt;p&gt;이때, universalApk 옵션 여부에 따라서 병합된 Manifest 파일의 위치가 달라지게 됩니다.&lt;/p&gt;
&lt;p&gt;​                                      &lt;/p&gt;
&lt;p&gt;​                                                                                            &lt;/p&gt;
&lt;h2&gt;빌드 중간 파일 위치&lt;/h2&gt;
&lt;p&gt;​                                        &lt;/p&gt;
&lt;p&gt;해당 옵션을 적용하지 않은 경우, android build tool gradle version 3.6 기준으로 아래와 같은 위치에 AndroidManifest.xml이 생성 됩니다.&lt;/p&gt;
&lt;p&gt;​                                        &lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;app/build/intermediates/merged_manifest/&amp;lt;buildType&amp;gt;/&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​                                        &lt;/p&gt;
&lt;p&gt;이때, buildType 는 debug, dev, release 와 같은 app/build.gradle 안에 있는 buildType 이름입니다.&lt;/p&gt;
&lt;p&gt;​                                        &lt;/p&gt;
&lt;p&gt;하지만, universalApk 옵션을 적용한 경우에는 아래위치에 AndroidManifest.xml 이 생성됩니다.&lt;/p&gt;
&lt;p&gt;​                                        &lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;app/build/intermediates/merged_manifest/&amp;lt;buildType&amp;gt;/universal&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​                                     &lt;/p&gt;
&lt;p&gt;​                                     &lt;/p&gt;
&lt;p&gt;​                                     &lt;/p&gt;
&lt;p&gt;​                                     &lt;/p&gt;
&lt;h2&gt;출처&lt;/h2&gt;
&lt;p&gt;​                       &lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.android.com/studio/build/configure-apk-splits&quot;&gt;https://developer.android.com/studio/build/configure-apk-splits&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.android.com/studio/build/manifest-merge&quot;&gt;https://developer.android.com/studio/build/manifest-merge&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>개발/Android</category>
      <category>android studio</category>
      <category>Gradle</category>
      <category>gradle build tools</category>
      <category>Splits</category>
      <category>universalApk</category>
      <category>그래들</category>
      <category>빌드 환경</category>
      <category>안드로이드</category>
      <category>안드로이드 스튜디오</category>
      <category>컴파일러</category>
      <author>semtax</author>
      <guid isPermaLink="true">https://semtax.tistory.com/89</guid>
      <comments>https://semtax.tistory.com/89#entry89comment</comments>
      <pubDate>Fri, 22 May 2020 20:49:11 +0900</pubDate>
    </item>
    <item>
      <title>디자인패턴 공부내용 정리 : 리액터 패턴</title>
      <link>https://semtax.tistory.com/88</link>
      <description>&lt;h2&gt;개요&lt;/h2&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;이번에는, 이벤트 핸들링을 하는 디자인 패턴 중 하나인 리액터 패턴에 대해서 알아보도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;h2&gt;왜 나오게 됬는가?&lt;/h2&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;시스템에서 여러 종류의 이벤트를 동시에 동기적으로 처리하게 될때 어떻게 해야하는지 고민을 하다 나오게 된 패턴입니다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;h2&gt;문제 상황&lt;/h2&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;먼저 아래와 같은 상황을 가정해보도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;당신이 만약 IoT 센서 데이터를 처리하는 서버를 만든다고 가정을 해봅시다.&lt;/p&gt;
&lt;p&gt;이때 센서 데이터들은 언제 들어올지 불확실 하므로 이벤트 드리븐 기반의 서버를 작성해야 한다고 판단하였습니다.&lt;/p&gt;
&lt;p&gt;일단 당신은, 단순하게 무한 루프를 만들고 센서 데이터에 관한 이벤트가 왔나 안왔나 폴링(이벤트가 왔나 안왔나 계속 체크하면서 이벤트가 오면 이벤트에 관한 코드를 실행하게 하기)하는 방식으로 구현하였습니다.&lt;/p&gt;
&lt;p&gt;하지만, 센서의 종류가 너무나도 늘어나면서 각 센서 마다 처리해야 하는 코드를 추가하는게 너무 고역스러운 상황에 놓이게 되었습니다.&lt;/p&gt;
&lt;p&gt;이럴때 당신은 어떠한 선택을 하실건가요?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;다음으로 아래와 같은 상황도 가정 할 수 있습니다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;만약 우리가 GUI로 작동하는 테트리스 게임을 만든다고 가정을 해봅시다.&lt;/p&gt;
&lt;p&gt;이때 사용자의 키보드나 마우스 입력을 받기 위해서는 키보드 혹은 마우스 이벤트를 받아야 하는 상황이 필연적으로 발생하게 됩니다.&lt;/p&gt;
&lt;p&gt;일단 당신은, 단순하게 무한 루프를 만들고 이벤트를 폴링(이벤트가 왔나 안왔나 계속 체크하면서 이벤트가 오면 이벤트에 관한 코드를 실행하게 하기)하는 방식으로 구현하였습니다.&lt;/p&gt;
&lt;p&gt;하지만, 게임이 복잡해지면서 추가해야될 이벤트의 개수가 너무 많아지게 되고 코드를 추가하는게 너무 고역스러운 상황에 놓이게 되었습니다.&lt;/p&gt;
&lt;p&gt;이때 당신은 어떤 선택을 하실건가요?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;또한 위의 있는 문제들을 효율적으로 해결하기 위해서는 아래 4가지 상황 역시 지켜야 합니다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;주의해야 될 사항은 아래와 같습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;이벤트 핸들러의 작업이 Blocking인 작업이면 절대로 안됩니다.(속도가 엄청나게 느려짐)&lt;/li&gt;
&lt;li&gt;불필요한 동기화 로직이나 Context switching, 발생하는 경우 처리율이 엄청 떨어지므로 이 2가지 사항을 가급적 피해야 합니다.&lt;/li&gt;
&lt;li&gt;새로운 이벤트 핸들러를 쉽게 추가할 수 있어야 합니다.&lt;/li&gt;
&lt;li&gt;복잡한 멀티스레딩이나, 동기화 코드에 영향을 최대한 안받게 구현해야 합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;이제 그러면 위의 문제들을 어떻게 해결해야 하는지 알아봅시다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;h2&gt;해결법&lt;/h2&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;해결방법은, 중간에 Dispatcher 라는 객체를 하나 둬서, 이벤트 요청을 미리 매핑된 이벤트 핸들러에 던져버리고, 실제 이벤트에 대한 처리는 이벤트 핸들러가 하도록 외주(?)를 줘버리면 됩니다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;이떄, 이러한 이벤트 핸들러 매핑 작업과 디스패치 객체를 호출하는 수행하는 객체를 리액터(Reactor) 객체라고 합니다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;좀더 구체적으로 설명하면, 아래와 같은 방식으로 처리가 이루어 집니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;먼저 리액터가 시작되기 전에, 사용자가 이벤트 핸들러를 리액터에 등록합니다.&lt;/li&gt;
&lt;li&gt;리액터에서, 사용자의 연결(또는 요청)을 받아서 연결을 demultiplexing 시켜주고 해당 연결을 그에 맞는 핸들러와 매핑(연결)시켜 줍니다.&lt;/li&gt;
&lt;li&gt;이제 실제로 Dispatch 객체에서 dispatch 함수가 실행되면서 사용자의 요청을 받아 이벤트 핸들러가 실행되게 됩니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;그림으로 나타내면 아래와 같습니다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cc10V6/btqEg2dJHq0/QW84hmxS7WAptU13YsAIT1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cc10V6/btqEg2dJHq0/QW84hmxS7WAptU13YsAIT1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cc10V6/btqEg2dJHq0/QW84hmxS7WAptU13YsAIT1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcc10V6%2FbtqEg2dJHq0%2FQW84hmxS7WAptU13YsAIT1%2Fimg.png&quot; width=&quot;100%&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;이제 실제로 Reactor 패턴을 구현해보도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;h2&gt;구현&lt;/h2&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;먼저, 구현에 앞서 Reactor 패턴의 대략적인 구조 및 요소들을 알아봅시다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;먼저 Reactor 패턴은 아래 객체 들로 구성 되어 있습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Reactor : 실제 이벤트를 등록/삭제하고 사용자의 요청을 Dispatcher로 넘겨주는 역할을 하는 객체&lt;/li&gt;
&lt;li&gt;Dispatcher : 실제 사용자의 요청을 받아서 이벤트 핸들러에 넘겨주는 해주는 객체&lt;/li&gt;
&lt;li&gt;Event Handler : 실제 사용자에게 요청을 받아서 처리하는 객체&lt;/li&gt;
&lt;li&gt;Event Handler Map : 이벤트에 대응하는 이벤트 핸들러를 가지고 있는 컨테이너 객체&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;그러면 실제로 코드로 구현을 해보도록 합시다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;먼저 아래와 같이 Reactor 객체를 만들어 줍시다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;package main.java.reactorexample;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.UnknownHostException;

public class Reactor {

    private ServerSocket serverSocket;
    private HandleMap handleMap;

    public Reactor(int port) {
        handleMap = new HandleMap();
        try {
            serverSocket = new ServerSocket(port);
        } catch(IOException e) {
            e.printStackTrace();
        }
    }

    public void startServer(){
        Dispatcher dispatcher = new Dispatcher();

        while(true) {
            dispatcher.dispatch(serverSocket, handleMap);
        }
    }

    public void registerHandler(EventHandler handler) {
        handleMap.put(handler.getHandler(),handler);
    }

    public void removeHandler(EventHandler handler) {
        handleMap.remove(handler.getHandler(), handler);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;위에서 설명했다시피, 리액터 객체는 이벤트 핸들러를 등록/삭제 하는 역할 및 사용자에게 받은 요청을 Dispatcher로 넘겨주는 역할을 수행합니다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;다음으로, Dispatcher 객체를 만들어 줍시다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;package main.java.reactorexample;


import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class Dispatcher {

    private final int HEADER_SIZE = 6;

    public void dispatch(ServerSocket serverSocket, HandleMap handleMap) {
        try{
            Socket socket = serverSocket.accept();
            demultiplex(socket, handleMap);
        }catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void demultiplex(Socket socket, HandleMap handleMap) {
        try {
            InputStream inputStream = socket.getInputStream();

            byte[] buffer = new byte[HEADER_SIZE];
            inputStream.read(buffer);
            String header = new String(buffer);

            handleMap.get(header).handleEvent(inputStream);

        } catch(IOException e) {
            e.printStackTrace();
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;Dispatcher 객체는, 사용자(소켓)으로 부터 요청을 받아서, 사용자의 요청(= 이벤트 종류)에 대응되는 핸들러를 호출하는 역할을 합니다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;해당 예제에서는, demultiplex 함수에서, 사용자의 이벤트 요청에 대응되는 핸들러를 호출하는 역할을 수행합니다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;다음으로, Handler Map 객체를 보도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;pre class=&quot;scala&quot;&gt;&lt;code&gt;package main.java.reactorexample;

import java.util.HashMap;

public class HandleMap extends HashMap&amp;lt;String, EventHandler&amp;gt; {


}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;간단하게, 문자열(이벤트 코드)를 키로 하고, 거기에 대응되는 이벤트 핸들러를 꺼낼 수 있게 HashMap으로 구현하였습니다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;다음으로, EventHandler 인터페이스 입니다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;package main.java.reactorexample;

import java.io.InputStream;

public interface EventHandler {
    public String getHandler();
    public void handleEvent(InputStream inputStream);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;이벤트 핸들러는, 실제로 이벤트를 처리하는 handleEvent 메서드와, 해당 핸들러가 어떠한 이벤트 코드에 해당하는지를 알려주는 getHandler() 메소드로 구성되어 있습니다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;다음으로, 실제 EventHandler를 구현한 실제 구현체들을 짜보도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;package main.java.reactorexample;

import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.StringTokenizer;

public class LGDeviceEventHandler implements EventHandler {

    private static final int DATA_SIZE = 1024;
    private static final int TOKEN_NUM = 5;

    @Override
    public String getHandler() {
        return &quot;0x6001&quot;;
    }

    public void handleEvent(InputStream inputStream) {
        try {
            byte[] buffer = new byte[DATA_SIZE];

            inputStream.read(buffer);
            String data = new String(buffer, StandardCharsets.UTF_8);

            String[] params = new String[TOKEN_NUM];
            StringTokenizer token = new StringTokenizer(data, &quot;|&quot;);

            int i = 0;
            while(token.hasMoreTokens()) {
                params[i] = token.nextToken();
                ++i;
            }
            processDeviceInfo(params);
        }catch(IOException e){
            e.printStackTrace();
        }
    }

    private void processDeviceInfo(String[] params) {
        System.out.println(&quot;LG Device : &quot; + params[0]
            + &quot;\n Device No : &quot; + params[1]
            + &quot;\n Device Protocol &quot; + params[2]
            + &quot;\n Device Name &quot; + params[3]
            + &quot;\n Device Network &quot; + params[4]);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;package main.java.reactorexample;

import java.io.IOException;
import java.io.InputStream;
import java.util.StringTokenizer;

public class SamsungDeviceEventHandler implements EventHandler {

    private static final int DATA_SIZE = 512;
    private static final int TOKEN_NUM = 2;

    @Override
    public String getHandler() {
        return &quot;0x5001&quot;;
    }

    public void handleEvent(InputStream inputStream) {

        try {
            byte[] buffer = new byte[DATA_SIZE];
            inputStream.read(buffer);
            String data = new String(buffer);
            String[] params = new String[TOKEN_NUM];
            StringTokenizer token = new StringTokenizer(data, &quot;|&quot;);

            int i = 0;
            while(token.hasMoreTokens()) {
                params[i] = token.nextToken();
                ++i;
            }

            processDeviceInfo(params);
        }catch(IOException e){
            e.printStackTrace();
        }

    }

    protected void processDeviceInfo(String[] params) {
        System.out.println(&quot;Samsung Device : &quot; + params[0] + &quot;\nDevice No : &quot; + params[1]);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;마지막으로 Main 함수 코드입니다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;package main.java;

import main.java.reactorexample.Dispatcher;
import main.java.reactorexample.LGDeviceEventHandler;
import main.java.reactorexample.Reactor;
import main.java.reactorexample.SamsungDeviceEventHandler;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.UnknownHostException;

public class ReactorExample {

    public static void main(String[] args) {
        int port = 5000;

        Reactor reactor = new Reactor(port);

        reactor.registerHandler(new SamsungDeviceEventHandler());
        reactor.registerHandler(new LGDeviceEventHandler());

        reactor.startServer();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;위와 같이 구현 함으로써, 이전의 브로커 패턴에 비해 더욱 깔끔한 구조로 바뀌었습니다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;특히, switch 문에 이벤트 핸들러를 덕지덕지 추가해야되서 의존성이 너무 높은 구조였지만, 해당 방식으로 구현 함으로써 더욱 깔끔하게 구현이 가능해졌습니다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;h2&gt;예시&lt;/h2&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;해당 패턴을 적용한 가장 대표적인 프레임워크가 있습니다. 바로 Netty가 그 대표적인 예시입니다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;Netty는 자바진영에서 가장 많이 쓰는 대표적인 비동기 네트워크 프레임워크입니다. 특히 트위터나 다른 유수의 대기업에서도 많이 사용하고 있으며, 스프링 진영에서도 많이 사용하고 있습니다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;보통 아래와 같은 방식으로 코드가 작성됩니다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;public final class DiscardServer {
    public static void main(String[] args) throws Exception {
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
                .channel(NioServerSocketChannel.class)
                .handler(new LoggingHandler(LogLevel.INFO))
                .childHandler(new DiscardServerHandler());
            ChannelFuture f = b.bind(8010).sync();
            System.err.println(&quot;Ready for 0.0.0.0:8010&quot;);
            f.channel().closeFuture().sync();
        } finally {
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }
    }
}

class DiscardServerHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(C..H..Context ctx, Object msg) throws Exception {
        ByteBuf buf = (ByteBuf) msg;
        try {
            // discard
        } finally {
            buf.release(); // 이 부분은 두번째 시간에 설명합니다.
        }
    }
    @Override
    public void exceptionCaught(C..H..Context ctx, Throwable cause) {
        cause.printStackTrace();
        ctx.close();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;Netty도 위의 구현 예제와 유사하게 실제 요청을 처리하는 &quot;Handler&quot;, Dispatcher에 해당하는 이벤트 루프, Reactor에 해당하는 &quot;ServerBootstrap&quot; 으로 구성 되어있습니다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;(사실, Netty와 같은 경우 Reactor 패턴 이외에 다른 패턴도 같이 섞어서 사용하고 있습니다. 해당 패턴은 다음 포스팅에서 다룰 예정입니다.)&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;Netty 이외에도 많은 비동기 프레임워크(node.js) 들이 해당 패턴을 알게 모르게 쓰고 있습니다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;h2&gt;위의 구현 에서의 문제점&lt;/h2&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;사실, 문제 상황에서 언급한 3번문제(Event Handler를 추가하기 어려운 문제)는 회피 하였습니다.&lt;/p&gt;
&lt;p&gt;하지만, 해당 패턴을 사용한다고 해서 1번에 대한 문제점을 완벽하게 해결한 것은 아닙니다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;이를 위해서는 다른 방법이 필요합니다. 바로 Proactor라는 패턴을 사용해서 해결이 가능합니다.&lt;/p&gt;
&lt;p&gt;다음 시간에는 Proactor 패턴에 대해서 알아보도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;h2&gt;출처&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Pattern Oriented Software Architecture Vol 1.&lt;/li&gt;
&lt;li&gt;SW마에스트로 10기 아키텍처 수업 교육자료&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.slideshare.net/ConversionMeetup/dejan-pekter-nordeus-reactor-design-pattern&quot;&gt;https://www.slideshare.net/ConversionMeetup/dejan-pekter-nordeus-reactor-design-pattern&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>개발/Java</category>
      <category>Asyncronus Pattern</category>
      <category>design pattern</category>
      <category>Event handling</category>
      <category>netty</category>
      <category>Reactor Pattern</category>
      <category>디자인 패턴</category>
      <category>리액터 패턴</category>
      <category>비동기 패턴</category>
      <category>이벤트 핸들링</category>
      <category>이벤트 핸들링 패턴</category>
      <author>semtax</author>
      <guid isPermaLink="true">https://semtax.tistory.com/88</guid>
      <comments>https://semtax.tistory.com/88#entry88comment</comments>
      <pubDate>Mon, 18 May 2020 23:55:32 +0900</pubDate>
    </item>
    <item>
      <title>디자인패턴 공부내용 정리 : 브로커 패턴</title>
      <link>https://semtax.tistory.com/87</link>
      <description>&lt;h2&gt;개요&lt;/h2&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;이번에는, 아키텍처 패턴 중 하나인 브로커 패턴에 대해서 알아보도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;h2&gt;왜 나오게 됬는가?&lt;/h2&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;브로커 패턴은, 서로 다른 기종의 머신에 분산되어있는 서비스(객체 혹은 컴포넌트)간에 어떻게 협력을 잘 할지 고민하다 나온 패턴입니다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;따라서, 분산 시스템이나 RPC를 구현할때 사용되는 패턴에 많이 사용됩니다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;h2&gt;문제 상황&lt;/h2&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;먼저 한가지 상황을 가정 해봅시다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;당신이 만약에, 대기업에서 IoT 기반의 스마트 홈 네트워크 시스템을 구축한다고 가정합시다.&lt;/p&gt;
&lt;p&gt;그런데, 같이 참여하는 업체가 10개가 넘어가는데다, 임베디드 장비들이 전부 CPU 기종이 다르고 프로토콜도 제멋대로인 상황에 처하였습니다.&lt;/p&gt;
&lt;p&gt;더 황당한것은, 임베디드 장비들이 교체되는 일도 빈번하고, 그에 따라 연결되는 서버나 주소도 자주 바뀐다는 것입니다.&lt;/p&gt;
&lt;p&gt;그런데, 장비들이 교체되도 별다른 설정이 없이 잘 돌아야한다는 요구사항을 받았습니다.&lt;/p&gt;
&lt;p&gt;이때 당신은 어떤 선택을 하시겠습니까?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;위와 같은 상황에 처했을때, 브로커 패턴을 사용할 수 있습니다.&lt;/p&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;li&gt;시스템 컴포넌트가 어디에 위치해있던지 신경쓰지 않고 호출 가능합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;그러면, 위의 문제를 어떻게 해결 할 수 있는지 확인 해 봅시다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;h2&gt;해결법&lt;/h2&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;위와 같은 상황에서는, 서버와 클라이언트 사이에 브로커(Dispatcher) 라는 서비스 컴포넌트를 두어서, 실제 패킷이 해당 컴포넌트를 거쳐서 가게 하면 됩니다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;그리고, 브로커에서는 클라이언트에 패킷을 전달 받아서, 어떠한 종류의 서비스를 요청하는지를 파악하고 해당 서비스에 전달하는 역할을 수행합니다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;또한, 브로커에서 전달한 요청을 응답 받은뒤, 실제 요청을 보낸 클라이언트에 전달을 해줍니다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;먼저 대략적인 구조를 그림으로 나타내면 대략적으로 아래와 같습니다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dsGXtR/btqEd8ySXTk/tvhYks3j9WEKMH4xnNY8Vk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dsGXtR/btqEd8ySXTk/tvhYks3j9WEKMH4xnNY8Vk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dsGXtR/btqEd8ySXTk/tvhYks3j9WEKMH4xnNY8Vk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdsGXtR%2FbtqEd8ySXTk%2FtvhYks3j9WEKMH4xnNY8Vk%2Fimg.png&quot; width=&quot;100%&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cA8pfV/btqEbup3ycD/55zOx4bovCy8rmZzk0I0Qk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cA8pfV/btqEbup3ycD/55zOx4bovCy8rmZzk0I0Qk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cA8pfV/btqEbup3ycD/55zOx4bovCy8rmZzk0I0Qk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcA8pfV%2FbtqEbup3ycD%2F55zOx4bovCy8rmZzk0I0Qk%2Fimg.png&quot; width=&quot;100%&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;보내는 패킷은 아래와 같이 보내게 됩니다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;h3&gt;0x5001|semtax|22&lt;/h3&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;이때, 0x5001은 어떠한 방법으로 요청을 처리하라는 일종의 커맨드이고, 나머지 부분이 데이터의 역할을 하게 됩니다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;이제 실제로 구현을 해보도록 합시다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;h2&gt;구현&lt;/h2&gt;
&lt;p&gt;일단 우리가 구현하려는 예제의 구조는 아래와 같습니다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/SqUQ4/btqEd9xMePc/INbWf3poHrnIp7jnFJ3gK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/SqUQ4/btqEd9xMePc/INbWf3poHrnIp7jnFJ3gK0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/SqUQ4/btqEd9xMePc/INbWf3poHrnIp7jnFJ3gK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSqUQ4%2FbtqEd9xMePc%2FINbWf3poHrnIp7jnFJ3gK0%2Fimg.png&quot; width=&quot;100%&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;먼저, 브로커에 해당하는 Dispatcher 클래스를 아래와 같이 만들어 줍니다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;package main.java.brokerexample;

import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class Dispatcher {

    private final int HEADER_SIZE = 6;

    public void dispatch(ServerSocket serverSocket) {
        try{
            Socket socket = serverSocket.accept();
            demultiplex(socket);
        }catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void demultiplex(Socket socket) {
        try {
            InputStream inputStream = socket.getInputStream();

            byte[] buffer = new byte[HEADER_SIZE];
            inputStream.read(buffer);
            String header = new String(buffer);

            BaseDeviceProtocol baseDeviceProtocol;

            switch(header) {
                case &quot;0x5001&quot;:
                    baseDeviceProtocol = new SamsungDeviceProtocol();
                    break;
                case &quot;0x6001&quot;:
                    baseDeviceProtocol = new LGDeviceProtocol();
                    break;
                default:
                    baseDeviceProtocol = new UnhandledDeviceProtocol();
                    break;
            }

            baseDeviceProtocol.handleEvent(inputStream);
        } catch(IOException e) {
            e.printStackTrace();
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;Dispatcher 클래스는 클라이언트로 부터 래핑된 프로토콜을 받아서, 프로토콜의 헤더가 어떤거냐에 따라 그에 맞는 적절한 핸들러를 호출해주는 역할을 수행합니다.&lt;/p&gt;
&lt;p&gt;(네트워크로 치면 일종의 라우터 역할을 하는거라고 보시면 됩니다. )&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;그리고, 실제로 Dispatcher를 통해서 들어온 프로토콜들을 처리해주는 클래스들을 아래와 같이 만들어 줍시다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;package main.java.brokerexample;

import java.io.InputStream;

public abstract class BaseDeviceProtocol {
    public abstract void handleEvent(InputStream inputStream);
    protected abstract void processDeviceInfo(String[] params);
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;아래의 코드는 적절한 프로토콜을 찾지 못했을때 요청을 처리하는 핸들러 입니다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;pre class=&quot;scala&quot;&gt;&lt;code&gt;package main.java.brokerexample;

import java.io.InputStream;

public class UnhandledDeviceProtocol extends BaseDeviceProtocol {

    @Override
    public void handleEvent(InputStream inputStream) {
        processDeviceInfo(new String[] {&quot;Unknown Device!&quot;});
    }

    @Override
    protected void processDeviceInfo(String[] params) {
        System.out.println(params[0]);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;아래의 코드는 삼성 또는 LG의 프로토콜에 대한 요청을 처리하는 핸들러 입니다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;package main.java.brokerexample;

import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.StringTokenizer;

public class LGDeviceProtocol extends BaseDeviceProtocol{

    private static final int DATA_SIZE = 1024;
    private static final int TOKEN_NUM = 5;

    public void handleEvent(InputStream inputStream) {
        try {
            byte[] buffer = new byte[DATA_SIZE];

            inputStream.read(buffer);
            String data = new String(buffer, StandardCharsets.UTF_8);

            String[] params = new String[TOKEN_NUM];
            StringTokenizer token = new StringTokenizer(data, &quot;|&quot;);

            int i = 0;
            while(token.hasMoreTokens()) {
                params[i] = token.nextToken();
                ++i;
            }
            processDeviceInfo(params);
        }catch(IOException e){
            e.printStackTrace();
        }
    }

    protected void processDeviceInfo(String[] params) {
        System.out.println(&quot;LG Device : &quot; + params[0]
            + &quot;\n Device No : &quot; + params[1]
            + &quot;\n Device Protocol &quot; + params[2]
            + &quot;\n Device Name &quot; + params[3]
            + &quot;\n Device Network &quot; + params[4]);
    }
}


...


package main.java.brokerexample;

import java.io.IOException;
import java.io.InputStream;
import java.util.StringTokenizer;

public class SamsungDeviceProtocol extends BaseDeviceProtocol{

    private static final int DATA_SIZE = 512;
    private static final int TOKEN_NUM = 2;

    public void handleEvent(InputStream inputStream) {

        try {
            byte[] buffer = new byte[DATA_SIZE];
            inputStream.read(buffer);
            String data = new String(buffer);
            String[] params = new String[TOKEN_NUM];
            StringTokenizer token = new StringTokenizer(data, &quot;|&quot;);

            int i = 0;
            while(token.hasMoreTokens()) {
                params[i] = token.nextToken();
                ++i;
            }

            processDeviceInfo(params);
        }catch(IOException e){
            e.printStackTrace();
        }

    }

    protected void processDeviceInfo(String[] params) {
        System.out.println(&quot;Samsung Device : &quot; + params[0] + &quot;\nDevice No : &quot; + params[1]);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;마지막으로 해당 Dispatcher를 호출하는 메인 코드 입니다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;pre class=&quot;swift&quot;&gt;&lt;code&gt;package main.java;

import main.java.brokerexample.Dispatcher;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.UnknownHostException;

public class BrokerExample {

    public static void main(String[] args) {
        int port = 5000;
        System.out.println(&quot;Server ON : &quot; + port);

        try {
            ServerSocket serverSocket = new ServerSocket(port);
            Dispatcher dispatcher = new Dispatcher();

            while(true) {
                dispatcher.dispatch(serverSocket);
            }

        }catch(UnknownHostException e) {
            e.printStackTrace();
        }
        catch(IOException e) {
            e.printStackTrace();
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;h2&gt;실행 방법&lt;/h2&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;먼저 아래와 같이 커맨드 라인을 켜서 실행을 해줍시다&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;pre class=&quot;gherkin&quot;&gt;&lt;code&gt;echo &quot;0x5001|Samsung|22&quot; | nc localhost 5000
^C (Ctrl + C)
echo &quot;0x6001|LG|22|csv|G5|wifi&quot; | nc localhost 5000
^C (Ctrl + C)
echo &quot;0x7001|LG|22|csv|G5|wifi&quot; | nc localhost 5000
^C (Ctrl + C)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;이렇게 실행을 하면 대략적으로 아래와 같은 결과가 나오게 됩니다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;// 삼성 디바이스의 경우
Samsung Device : Samsung
Device No : 22

// LG 디바이스의 경우
LG Device : LG
 Device No : 22
 Device Protocol csv
 Device Name G5
 Device Network wifi

// 나머지 경우
Unknown Device!&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;브로커 패턴은 위와 같이 여러 가지 프로토콜이 들어와야 하는데 일관되게 처리하고 싶을때 유용하게 사용 할 수 있습니다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;h2&gt;예시&lt;/h2&gt;
&lt;p&gt;아파치 Thrift나 protobuf(gRPC) 와 같은 서비스가 이러한 브로커 패턴을 자동으로 만들어주는 서비스 입니다.&lt;/p&gt;
&lt;p&gt;실제로, 아파치 Thrift와 같은 경우 아래와 같이 프로토콜을 명시해주면 자동으로 타겟언어에 맞춰서 브로커 패턴을 따르는 뼈대 코드를 컴파일해서 만들어줍니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;include &quot;shared.thrift&quot;


namespace cl tutorial
namespace cpp tutorial
namespace d tutorial
namespace dart tutorial
namespace java tutorial
namespace php tutorial
namespace perl tutorial
namespace haxe tutorial
namespace netstd tutorial

typedef i32 MyInteger

const i32 INT32CONSTANT = 9853
const map&amp;lt;string,string&amp;gt; MAPCONSTANT = {'hello':'world', 'goodnight':'moon'}

enum Operation {
  ADD = 1,
  SUBTRACT = 2,
  MULTIPLY = 3,
  DIVIDE = 4
}

struct Work {
  1: i32 num1 = 0,
  2: i32 num2,
  3: Operation op,
  4: optional string comment,
}


exception InvalidOperation {
  1: i32 whatOp,
  2: string why
}

service Calculator extends shared.SharedService {
   void ping(),

   i32 add(1:i32 num1, 2:i32 num2),

   i32 calculate(1:i32 logid, 2:Work w) throws (1:InvalidOperation ouch),

   oneway void zip()

}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;실제로 생성되는 코드들은 아래 URL에 존재합니다.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://gitbox.apache.org/repos/asf?p=thrift.git;a=tree;f=tutorial/java;h=a5537b5ca0cd99a80765f55a2df218bed7e50a89;hb=HEAD&quot;&gt;https://gitbox.apache.org/repos/asf?p=thrift.git;a=tree;f=tutorial/java;h=a5537b5ca0cd99a80765f55a2df218bed7e50a89;hb=HEAD&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1589713122754&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-og-type=&quot;website&quot; data-og-title=&quot;ASF Git Repos - thrift.git/tree - tutorial/java/&quot; data-og-description=&quot;&quot; data-og-host=&quot;gitbox.apache.org&quot; data-og-source-url=&quot;https://gitbox.apache.org/repos/asf?p=thrift.git;a=tree;f=tutorial/java;h=a5537b5ca0cd99a80765f55a2df218bed7e50a89;hb=HEAD&quot; data-og-url=&quot;https://gitbox.apache.org/repos/asf?p=thrift.git;a=tree;f=tutorial/java;h=a5537b5ca0cd99a80765f55a2df218bed7e50a89;hb=HEAD&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://gitbox.apache.org/repos/asf?p=thrift.git;a=tree;f=tutorial/java;h=a5537b5ca0cd99a80765f55a2df218bed7e50a89;hb=HEAD&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://gitbox.apache.org/repos/asf?p=thrift.git;a=tree;f=tutorial/java;h=a5537b5ca0cd99a80765f55a2df218bed7e50a89;hb=HEAD&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;&gt;ASF Git Repos - thrift.git/tree - tutorial/java/&lt;/p&gt;
&lt;p class=&quot;og-desc&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;og-host&quot;&gt;gitbox.apache.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2&gt;출처&lt;/h2&gt;
&lt;p&gt;1. Pattern Oriented Software Architecture Vol. 1&amp;nbsp;&lt;br /&gt;2. SW마에스트로, NHN Next 아키텍처 수업 교육자료&lt;/p&gt;</description>
      <category>개발/Java</category>
      <category>Broker Pattern</category>
      <category>Dispatcher</category>
      <category>java</category>
      <category>msa</category>
      <category>디자인 패턴</category>
      <category>분산 시스템</category>
      <category>브로커</category>
      <category>브로커 패턴</category>
      <category>이벤트 핸들러</category>
      <category>자바</category>
      <author>semtax</author>
      <guid isPermaLink="true">https://semtax.tistory.com/87</guid>
      <comments>https://semtax.tistory.com/87#entry87comment</comments>
      <pubDate>Sun, 17 May 2020 19:58:31 +0900</pubDate>
    </item>
    <item>
      <title>디자인패턴 공부내용 정리 : 싱글톤 패턴</title>
      <link>https://semtax.tistory.com/86</link>
      <description>&lt;h2&gt;개요&lt;/h2&gt;
&lt;p&gt;​                        &lt;/p&gt;
&lt;p&gt;이번에는, 생성 패턴 중 하나인 싱글톤 패턴에 대해서 알아보도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;​                        &lt;/p&gt;
&lt;p&gt;​                        &lt;/p&gt;
&lt;h2&gt;왜 나오게 됬는가?&lt;/h2&gt;
&lt;p&gt;​                        &lt;/p&gt;
&lt;p&gt;싱글톤 패턴은, 여러 객체에서 데이터를 공유해서 써야하는데 단 1개만 생성해서 써야할때 어떻게 해야할까 라는 고민에서 나오게 된 패턴입니다. (OOP의 전역변수 라는 느낌으로 생각하시면 됩니다.) &lt;/p&gt;
&lt;p&gt;​                        &lt;/p&gt;
&lt;p&gt;주로, 여러곳에서 쓰이는 설정 정보나, 다른곳에서도 공통적으로 쓰이는 데이터베이스 설정 객체와 같은 정보들을 공유할 때 사용되게 됩니다.&lt;/p&gt;
&lt;p&gt;​                        &lt;/p&gt;
&lt;p&gt;​                                          &lt;/p&gt;
&lt;h2&gt;문제 상황&lt;/h2&gt;
&lt;p&gt;​                        &lt;/p&gt;
&lt;p&gt;먼저 아래의 상황을 한번 가정 해봅시다.&lt;/p&gt;
&lt;p&gt;​                        &lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;당신이 앱을 개발한다고 가정을 해봅시다.&lt;/p&gt;
&lt;p&gt;그런데, 앱에 있는 여러 클래스에서 앱에서 전역으로 쓰이는 설정정보를 읽고 써야하는 상황이 닥쳤습니다.&lt;/p&gt;
&lt;p&gt;이럴때 당신은 어떻게 하겠습니까?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;​                        &lt;/p&gt;
&lt;p&gt;위와 같은 상황에 처했을때 과연 어떻게 해결하면 되는것일까요?&lt;/p&gt;
&lt;p&gt;​                        &lt;/p&gt;
&lt;p&gt;​                                                &lt;/p&gt;
&lt;h2&gt;해결법&lt;/h2&gt;
&lt;p&gt;​                               &lt;/p&gt;
&lt;p&gt;모든 클래스에서 접근 가능한 전역객체를 만들면 됩니다. 또한 객체를 생성할때, 딱 1개만 생성되게 강제를 시키면 됩니다.&lt;/p&gt;
&lt;p&gt;​                        &lt;/p&gt;
&lt;p&gt;​                                          &lt;/p&gt;
&lt;h2&gt;구현&lt;/h2&gt;
&lt;p&gt;​                        &lt;/p&gt;
&lt;p&gt;일단 싱글톤 패턴은 아래와 같이 간단하게 구현이 가능합니다.&lt;/p&gt;
&lt;p&gt;​                        &lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class ConfigContext {

  private static ConfigContext configInstance;

  private ConfigContext() {

  }


  public static synchronized ConfigContext getInstance() {
    if(configInstance == null) {
      configInstance = new ConfigContext();
    }
    return configInstance;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​                        &lt;/p&gt;
&lt;p&gt;먼저,  다른 객체에서 객체 생성을 할 수 없게, private 로 생성자를 만들어 줍니다.&lt;/p&gt;
&lt;p&gt;​                        &lt;/p&gt;
&lt;p&gt;그리고 나서, 실제로 생성된 객체를 반환하는 getInstance() 함수를 구현합니다. 이때, 객체가 생성되지 않은 경우 객체를 생성해서 반환을 해주고 이미 객체가 생성된 경우에는 만들어진 객체를 반환하게 구현합니다.&lt;/p&gt;
&lt;p&gt;​                        &lt;/p&gt;
&lt;p&gt;여기서, synchronized 라는 키워드가 보이는데, 이는 다른 여러 스레드에서 해당 객체에 동시접근하는 경우 Race condition이 발생할 수 있기 때문에 synchronized 를 이용해서 동기화를 해주게 됩니다.        &lt;/p&gt;
&lt;p&gt;​                        &lt;/p&gt;
&lt;p&gt;​                        &lt;/p&gt;
&lt;h2&gt;예시&lt;/h2&gt;
&lt;p&gt;​                        &lt;/p&gt;
&lt;p&gt;일단 싱글톤을 사용하는 예시 코드로 안드로이드(AOSP)의 Choreographer 라는 클래스를 예로 들도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;​                        &lt;/p&gt;
&lt;p&gt;Choreographer 클래스는 실제 모바일 화면의 입출력과 관련된 클래스로 하드웨어의 vsync 신호(화면 수직동기화 신호)를 받고 그에 따른 동작을 세팅할 수 있는 콜백 함수들이 있는 클래스 입니다.&lt;/p&gt;
&lt;p&gt;(실제 코드는 &lt;a href=&quot;https://github.com/aosp-mirror/platform_frameworks_base/blob/master/core/java/android/view/Choreographer.java&quot;&gt;https://github.com/aosp-mirror/platform_frameworks_base/blob/master/core/java/android/view/Choreographer.java&lt;/a&gt;  여기에 있습니다.)&lt;/p&gt;
&lt;p&gt;​                        &lt;/p&gt;
&lt;p&gt;주로, FPS(1초에 몇 프레임이 나오는지)와 같은 정보들을 측정할때 사용하는 클래스입니다.&lt;/p&gt;
&lt;p&gt;​                       &lt;/p&gt;
&lt;p&gt;​                                                            &lt;/p&gt;
&lt;p&gt;안드로이드 에서는 해당 클래스를 통해서만 vsync 신호가 전송되야 되므로, 단 1개의 객체만 생성이 되야 합니다. 따라서, 싱글톤 패턴을 사용하기 적합하다 할 수 있겠습니다.&lt;/p&gt;
&lt;p&gt;​                        &lt;/p&gt;
&lt;p&gt;실제로 Choreographer 클래스는 아래와 같이 싱글톤 객체를 생성합니다.&lt;/p&gt;
&lt;p&gt;​                        &lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public final class Choreographer {
    private static final String TAG = &amp;quot;Choreographer&amp;quot;;

    ...

      private static final ThreadLocal&amp;lt;Choreographer&amp;gt; sSfThreadInstance =
            new ThreadLocal&amp;lt;Choreographer&amp;gt;() {
                @Override
                protected Choreographer initialValue() {
                    Looper looper = Looper.myLooper();
                    if (looper == null) {
                        throw new IllegalStateException(&amp;quot;The current thread must have a looper!&amp;quot;);
                    }
                    return new Choreographer(looper, VSYNC_SOURCE_SURFACE_FLINGER);
                }
            };


      .....

      private Choreographer(Looper looper, int vsyncSource) {
          mLooper = looper;
          mHandler = new FrameHandler(looper);
          mDisplayEventReceiver = USE_VSYNC
            ? new FrameDisplayEventReceiver(looper, vsyncSource)
            : null;
          mLastFrameTimeNanos = Long.MIN_VALUE;

          mFrameIntervalNanos = (long)(1000000000 / getRefreshRate());

          mCallbackQueues = new CallbackQueue[CALLBACK_LAST + 1];
          for (int i = 0; i &amp;lt;= CALLBACK_LAST; i++) {
            mCallbackQueues[i] = new CallbackQueue();
          }
          // b/68769804: For low FPS experiments.
          setFPSDivisor(SystemProperties.getInt(ThreadedRenderer.DEBUG_FPS_DIVISOR, 1));
     }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​                        &lt;/p&gt;
&lt;p&gt;또한, 스프링에서 Bean 으로 등록된 객체들도, 기본적으로 싱글톤 객체로 생성이 됩니다.&lt;/p&gt;
&lt;p&gt;​                        &lt;/p&gt;
&lt;h2&gt;출처&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://refactoring.guru/design-patterns/singleton&quot;&gt;https://refactoring.guru/design-patterns/singleton&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/aosp-mirror/platform_frameworks_base/blob/master/core/java/android/view/Choreographer.java&quot;&gt;https://github.com/aosp-mirror/platform_frameworks_base/blob/master/core/java/android/view/Choreographer.java&lt;/a&gt; &lt;/li&gt;
&lt;/ol&gt;</description>
      <category>개발/Java</category>
      <category>Choreographer</category>
      <category>design pattern</category>
      <category>java</category>
      <category>Singleton pattern</category>
      <category>디자인 패턴</category>
      <category>생성 패턴</category>
      <category>싱글톤</category>
      <category>싱글톤 패턴</category>
      <category>안드로이드</category>
      <category>자바</category>
      <author>semtax</author>
      <guid isPermaLink="true">https://semtax.tistory.com/86</guid>
      <comments>https://semtax.tistory.com/86#entry86comment</comments>
      <pubDate>Sat, 16 May 2020 22:37:01 +0900</pubDate>
    </item>
    <item>
      <title>디자인패턴 공부내용 정리 : 빌더 패턴</title>
      <link>https://semtax.tistory.com/85</link>
      <description>&lt;h2&gt;개요&lt;/h2&gt;
&lt;p&gt;​                            &lt;/p&gt;
&lt;p&gt;이번 포스팅에서는, 생성 패턴 중에 하나인 빌더패턴에 대해 알아보도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;​                            &lt;/p&gt;
&lt;p&gt;​                            &lt;/p&gt;
&lt;h2&gt;왜 나오게 됬는가?&lt;/h2&gt;
&lt;p&gt;​                            &lt;/p&gt;
&lt;p&gt;이 패턴같은 경우, 복잡한 객체를 생성할때 초기화를 단계별(step-by-step)로 해야 할 때, 어떻게 깔끔하게 코드를 작성할 수 있을까를 고민하다 나온 패턴입니다.&lt;/p&gt;
&lt;p&gt;​                            &lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://refactoring.guru/images/patterns/content/builder/builder-en.png&quot; alt=&quot;Builder design pattern&quot;&gt;&lt;/p&gt;
&lt;p&gt;​                            &lt;/p&gt;
&lt;p&gt;대표적인 예로 자동차 조립, 피자 만들기가 있습니다.&lt;/p&gt;
&lt;p&gt;​                            &lt;/p&gt;
&lt;p&gt;이제 실제 예제로 어떤 문제가 있는지 알아보도록 합시다.&lt;/p&gt;
&lt;p&gt;​                            &lt;/p&gt;
&lt;p&gt;​                            &lt;/p&gt;
&lt;h2&gt;문제 상황&lt;/h2&gt;
&lt;p&gt;​                            &lt;/p&gt;
&lt;p&gt;먼저 피자 라는 객체를 만든다고 가정을 해봅시다. 피자에는, 올리브 토핑도 들어 갈 수 있고, 페퍼로니 토핑도 들어 갈 수 있고, 피망, 채소도 들어갈 수 있고 고기도 들어갈 수 있습니다.&lt;/p&gt;
&lt;p&gt;​                            &lt;/p&gt;
&lt;p&gt;하지만, 저 재료들을 무조건 넣어야 되는게 아니고, 선택적으로 넣을 수 있습니다.                            &lt;/p&gt;
&lt;p&gt;​                            &lt;/p&gt;
&lt;p&gt;일단 가장 단순한 방법은 저 위의 케이스를 모두 커버하는 생성자를 만드는것 입니다.&lt;/p&gt;
&lt;p&gt;​                            &lt;/p&gt;
&lt;p&gt;하지만 그런 경우 코드가 아래와 같이 정말로 지저분하게 나오게 됩니다.&lt;/p&gt;
&lt;p&gt;​                            &lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class Pizza {

  private String pepperoni;
  private String paprika;
  private String meat;
  private String olive;
  private String cheese;
  private String bread;

  public Pizza() {

  }

  public Pizza(String bread) {
    this.bread = bread;
  }

  public Pizza(String bread, String cheese) {
    this.bread = bread;
    this.cheese = cheese;
  }

  ...

}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​                            &lt;/p&gt;
&lt;p&gt;그렇다면 이러한 문제를 어떻게 해결 할 수 있을까요?&lt;/p&gt;
&lt;p&gt;​                            &lt;/p&gt;
&lt;p&gt;​                            &lt;/p&gt;
&lt;p&gt;​                            &lt;/p&gt;
&lt;h2&gt;해결법&lt;/h2&gt;
&lt;p&gt;​                            &lt;/p&gt;
&lt;p&gt;사실 이러한 상황에서, 해결법은 아래와 같습니다.&lt;/p&gt;
&lt;p&gt;먼저, 위에서 선언했던 모든 생성자들을, &lt;strong&gt;빌더&lt;/strong&gt; 라는 별도의 클래스로 추출 합니다.                             &lt;/p&gt;
&lt;p&gt;​                            &lt;/p&gt;
&lt;p&gt;여기서, 추출한 각 생성자들을 빌더 클래스의 메소드로 만듭니다. 이때, 빌더 클래스의 메서드들이 빌더 객체 자신을 리턴하게 해서 위에서 선언한 함수들을 연속적으로 호출 할 수 있게 하는 방식으로 구현합니다.&lt;/p&gt;
&lt;p&gt;​                            &lt;/p&gt;
&lt;p&gt;마지막으로, build 메소드를 호출하면 실제로 빌더 클래스에 의해 생성된 객체를 돌려주게 됩니다.&lt;/p&gt;
&lt;p&gt;​                            &lt;/p&gt;
&lt;p&gt;​                            &lt;/p&gt;
&lt;h2&gt;구현&lt;/h2&gt;
&lt;p&gt;​                            &lt;/p&gt;
&lt;p&gt;​                            &lt;/p&gt;
&lt;p&gt;먼저 아래와 같이 모든 초기화할 인자를 받는 생성자를 하나 만들어 줍시다.&lt;/p&gt;
&lt;p&gt;​                            &lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class Pizza {

  private String pepperoni;
  private String paprika;
  private String meat;
  private String olive;
  private String cheese;
  private String bread;

  public Pizza(String bread, String cheese, String pepperoni, String paprika, String meat, String olive) {
    this.bread = bread;
    this.cheese = cheese;
    this.pepperoni = pepperoni;
    this.paprika = paprika;
    this.meat = meat;
    this.olive = olive;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​                            &lt;/p&gt;
&lt;p&gt;그리고 나서 아래와 같이 클래스를 생성해주는 빌더 클래스를 만들어 줍시다.&lt;/p&gt;
&lt;p&gt;​                            &lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class PizzaBuilder {

  private String pepperoni;
  private String paprika;
  private String meat;
  private String olive;
  private String cheese;
  private String bread;

  public PizzaBuilder(String bread, String cheese) {
    this.bread = bread;
    this.cheese = cheese;
  }

  public PizzaBuilder addPaprika(String paprika) {
    this.paprika = paprika;
  }


  public PizzaBuilder addMeat(String meat) {
    this.meat = meat;
  }

  public PizzaBuilder addOlive(String olive) {
    this.olive = olive;
  }

  public PizzaBuilder addPepperoni(String pepperoni) {
    this.pepperoni = pepperoni;
  }

  public Pizza build(){
    return new Pizza(this.bread, this.cheese, this.pepperoni, this.paprika, this.meat, this.olive);
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​                            &lt;/p&gt;
&lt;p&gt;위와 같이, 객체 자신을 리턴해주는 함수들을 만들어서 chaining 하게 함수를 호출해줄 수 있게 하는 아이디어를 사용해서 객체에 값을 설정하게 됩니다.&lt;/p&gt;
&lt;p&gt;​                            &lt;/p&gt;
&lt;p&gt;그리고 나서, build() 함수를 통해서 실제로 객체를 생성하게 됩니다.&lt;/p&gt;
&lt;p&gt;​                            &lt;/p&gt;
&lt;p&gt;사용은 대략 아래와 같이 하면 됩니다.&lt;/p&gt;
&lt;p&gt;​                            &lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class Main {

  public static void main(String[] args) {
    PizzaBuilder builder = new PizzaBuilder(&amp;quot;crust&amp;quot;,&amp;quot;pamasan&amp;quot;);
    Pizza nicePizza = builder.addPaprika(&amp;quot;yellow paprika&amp;quot;)
      .addMeat(&amp;quot;cow meat&amp;quot;)
      .addOlive(&amp;quot;black olive&amp;quot;)
      .addPepperoni(&amp;quot;red pepperoni&amp;quot;)
      .build();
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​                            &lt;/p&gt;
&lt;p&gt;​                            &lt;/p&gt;
&lt;h2&gt;예시&lt;/h2&gt;
&lt;p&gt;​                            &lt;/p&gt;
&lt;p&gt;일단 해당 패턴을 적용한 예시는 많지만, 일단 (안드로이드 개발자 기준으로) 많이 사용해봤던 예제는 Retrofit2 이 아닌가 생각해봅니다.&lt;/p&gt;
&lt;p&gt;​                            &lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://square.github.io/retrofit/&quot;&gt;https://square.github.io/retrofit/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;​                            &lt;/p&gt;
&lt;p&gt;보통 아래 코드와 같이 사용합니다.&lt;/p&gt;
&lt;p&gt;​                            &lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public interface GitHubService {
  @GET(&amp;quot;users/{user}/repos&amp;quot;)
  Call&amp;lt;List&amp;lt;Repo&amp;gt;&amp;gt; listRepos(@Path(&amp;quot;user&amp;quot;) String user);
}

...

Retrofit retrofit = new Retrofit.Builder()
    .baseUrl(&amp;quot;https://api.github.com/&amp;quot;)
    .build();

GitHubService service = retrofit.create(GitHubService.class);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​                            &lt;/p&gt;
&lt;p&gt;위의 URL에서 볼수 있다시피, Retrofit과 같은 경우 Interceptor 설정도 가능하고, 데이터 직렬화/역직렬화 를 할때 JSON 파서를 쓸지, GSON 파서를 쓸지도 고를 수 있습니다.&lt;/p&gt;
&lt;p&gt;​                            &lt;/p&gt;
&lt;p&gt;하지만, 모든 옵션을 필수로 써야되는게 아니기 때문에, 엄청나게 많은 생성자를 만들어야 되는 문제가 생기게 됩니다.&lt;/p&gt;
&lt;p&gt;이때, 위에서 설명했던 빌더 패턴을 사용하는 경우 위와같이 깔끔하게 해결 가능합니다.&lt;/p&gt;
&lt;p&gt;​                            &lt;/p&gt;
&lt;p&gt;​                            &lt;/p&gt;
&lt;p&gt;​                            &lt;/p&gt;
&lt;h2&gt;출처&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;이펙티브 자바(Effective Java) 3판&lt;/li&gt;
&lt;li&gt;Pattern-Oriented Software Architecture, Volume 1&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>개발/Java</category>
      <category>builder pattern</category>
      <category>Creation Pattern</category>
      <category>design pattern</category>
      <category>java design pattern</category>
      <category>디자인 패턴</category>
      <category>빌더 패턴</category>
      <category>생성 패턴</category>
      <category>생성패턴</category>
      <category>자바</category>
      <category>자바 디자인 패턴</category>
      <author>semtax</author>
      <guid isPermaLink="true">https://semtax.tistory.com/85</guid>
      <comments>https://semtax.tistory.com/85#entry85comment</comments>
      <pubDate>Sat, 16 May 2020 18:58:41 +0900</pubDate>
    </item>
    <item>
      <title>통계값을 효율적으로 저장하기 : HdrHistogram</title>
      <link>https://semtax.tistory.com/84</link>
      <description>&lt;h2&gt;개요&lt;/h2&gt;
&lt;p&gt;​                          &lt;/p&gt;
&lt;p&gt;이번 포스팅에서는, P95값이나 평균 값과 같은 통계 지표들을 효율적으로 저장할 수 있는 방법 중에 하나인 HdrHistogram에 대해 알아보도록 하겠다.&lt;/p&gt;
&lt;p&gt;​                          &lt;/p&gt;
&lt;p&gt;​                                                    &lt;/p&gt;
&lt;p&gt;​                          &lt;/p&gt;
&lt;h2&gt;P95와 평균을 저장하는 단순한 방법&lt;/h2&gt;
&lt;p&gt;​                          &lt;/p&gt;
&lt;p&gt;우선 다음과 같은 상황을 가정해보자&lt;/p&gt;
&lt;p&gt;​                          &lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;만약 당신이, 모니터링 시스템을 만든다고 가정을 해보자.&lt;/p&gt;
&lt;p&gt;이때, 수천~수억건의 응답시간 로그 데이터로 부터, 하위 5% 응답속도와 평균응답 속도를 계산해야 한다.&lt;/p&gt;
&lt;p&gt;당신이라면 어떻게 하겠는가? &lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;​                          &lt;/p&gt;
&lt;p&gt;먼저, P95나 평균을 저장하는 단순한 방법은, 데이터를 계속 가지고 있다가 P95값이나 평균 값이 필요할때 마다, 또는 일정 주기마다 데이터들을 정렬해서 P95(하위 5% 값) 이나 평균값을 계산하는 방법이 있다.&lt;/p&gt;
&lt;p&gt;​                          &lt;/p&gt;
&lt;p&gt;하지만, 이러한 방법은 매번 데이터를 전부 들고있어야 하고, 수 많은 데이터를 매번 정렬을 해서 계산을 해야 되기 때문에 매우 느리다.&lt;/p&gt;
&lt;p&gt;​                                                    &lt;/p&gt;
&lt;p&gt;조금 더 나은 방법으로, 슬라이딩 윈도우 기법을 통해 부분적인 데이터만으로 P95나 평균을 계산하는 방법이 있다.&lt;/p&gt;
&lt;p&gt;​                          &lt;/p&gt;
&lt;p&gt;물론 이 방법도 좋은 방법이기는 하지만, 이전데이터들을 전부 버리기 때문에, 이전 데이터에 대한 통계 데이터를 찾는 연산을 수행(이전 24시간의 데이터 통계값 가져오기)하기에는 뭔가 모자란 느낌이다.&lt;/p&gt;
&lt;p&gt;​                          &lt;/p&gt;
&lt;p&gt;뭔가 더 좋은 방법이 있을것 같다. 그렇다면 더 나은방법이 있는것일까?&lt;/p&gt;
&lt;p&gt;​                          &lt;/p&gt;
&lt;p&gt;​                          &lt;/p&gt;
&lt;h2&gt;HdrHistogram의 동작방식&lt;/h2&gt;
&lt;p&gt;​                                                    &lt;/p&gt;
&lt;p&gt;위와 같은 문제점을 해결하기 위해 나온 방식 중 하나가 HdrHistorgram 이라는 방식이다.&lt;/p&gt;
&lt;p&gt;​                                                    &lt;/p&gt;
&lt;p&gt;HDRHistogram 방식을 설명하기 전에, 중학교 1학년 수학시간으로 잠깐 돌아가보자.&lt;/p&gt;
&lt;p&gt;​                                                    &lt;/p&gt;
&lt;p&gt;중학교 1학년때, 히스토그램이라는 통계자료를 배웠을 것이다. 히스토그램의 핵심은 x축에 특정 데이터의 구간을 설정해서 두고, y축에 해당 구간에 속한 데이터의 값을 넣는 방식이다.&lt;/p&gt;
&lt;p&gt;​                                                    &lt;/p&gt;
&lt;p&gt;HDR Histogram은, 이러한 아이디어에서 착안, 마치 히스토그램 처럼 이전에 들어온 데이터들을, 특정 구간대로 끊어서 해당 구간에서의 평균, P95와 같은 백분위 값들을 요약해서 저장하는 방식을 사용해서 데이터를 저장한다.&lt;/p&gt;
&lt;p&gt;​                                                                              &lt;/p&gt;
&lt;p&gt;​                                                                                                        &lt;/p&gt;
&lt;p&gt;​                                                    &lt;/p&gt;
&lt;p&gt;또한, HDRHistogram 과 같은 경우, 사용자가 지정한 구간을, 내부적으로 더욱 세부적으로 나눠서 미리 선언된 버킷을 이용해서저장한 뒤, 해당 버킷에 있는 값을 이용해서 계산하므로 위에서 언급한 슬라이드 윈도우 방식에 비해서 더욱 정확한 통계값을 얻을 수 있다는 장점도 있다. &lt;/p&gt;
&lt;p&gt;​                          &lt;/p&gt;
&lt;p&gt;​                          &lt;/p&gt;
&lt;p&gt;따라서, HDR 히스토그램을 통해 시계열 데이터에 대한 통계값들을 효율적으로 저장 할 수 있다.&lt;/p&gt;
&lt;p&gt;​                          &lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tideways.com/assets/content/blog/hdrhistogram3.png&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;p&gt;​                          &lt;/p&gt;
&lt;p&gt;​                          &lt;/p&gt;
&lt;p&gt;​                          &lt;/p&gt;
&lt;h2&gt;활용&lt;/h2&gt;
&lt;p&gt;​                                                    &lt;/p&gt;
&lt;p&gt;​                          &lt;/p&gt;
&lt;p&gt;보통 시계열 데이터에 대한 통계 값들을 저장할때 많이 사용한다. &lt;/p&gt;
&lt;p&gt;(지난 24시간의 응답속도 평균, 하위 95%, 하위 99.999% 등등)&lt;/p&gt;
&lt;p&gt;​                                                    &lt;/p&gt;
&lt;p&gt;따라서, 시계열 자료구조를 많이 사용하는 모니터링 시스템에 많이 적용할 수 있다.&lt;/p&gt;
&lt;p&gt;​                                                    &lt;/p&gt;
&lt;p&gt;실제로, 프로메테우스와 같은 시계열 DB에서 해당 알고리즘을 채택해서 사용하고 있다.&lt;/p&gt;
&lt;p&gt;​                          &lt;/p&gt;
&lt;p&gt;​                          &lt;/p&gt;
&lt;h2&gt;라이브러리&lt;/h2&gt;
&lt;p&gt;​                          &lt;/p&gt;
&lt;p&gt;다행(?)히도, 많은 언어로 이미 구현이 되어있는 라이브러리가 github에 존재한다.&lt;/p&gt;
&lt;p&gt;C, Java, javascript, python3, C# 등의 많은 언어를 지원한다.&lt;/p&gt;
&lt;p&gt;​                          &lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/HdrHistogram/HdrHistogram/blob/master/README.md&quot;&gt;https://github.com/HdrHistogram/HdrHistogram/blob/master/README.md&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;​                          &lt;/p&gt;
&lt;p&gt;​                                                  &lt;/p&gt;
&lt;h2&gt;출처&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;a href=&quot;https://github.com/HdrHistogram/HdrHistogram/blob/master/README.md&quot;&gt;https://github.com/HdrHistogram/HdrHistogram/blob/master/README.md&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href=&quot;https://groups.google.com/d/topic/prometheus-developers/90HB7vPB6LY&quot;&gt;https://groups.google.com/d/topic/prometheus-developers/90HB7vPB6LY&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href=&quot;https://tideways.com/profiler/blog/developing-a-time-series-database-based-on-hdrhistogram&quot;&gt;https://tideways.com/profiler/blog/developing-a-time-series-database-based-on-hdrhistogram&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>개발/알고리즘</category>
      <category>HDR 히스토그램</category>
      <category>time-series</category>
      <category>데이터 사이언스</category>
      <category>데이터 처리</category>
      <category>데이터베이스</category>
      <category>백분위 구하기</category>
      <category>시계열 데이터</category>
      <category>자료구조</category>
      <category>통계</category>
      <category>확률적 자료구조</category>
      <author>semtax</author>
      <guid isPermaLink="true">https://semtax.tistory.com/84</guid>
      <comments>https://semtax.tistory.com/84#entry84comment</comments>
      <pubDate>Sat, 16 May 2020 15:15:38 +0900</pubDate>
    </item>
    <item>
      <title>스프링부트 에서 카프카 써서 메시지 주고 받아보기</title>
      <link>https://semtax.tistory.com/83</link>
      <description>&lt;h2&gt;개요&lt;/h2&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이번 포스팅에서는, 카프카가 무엇인지와 스프링에서 카프카를 사용하는 방법에 대해서 알아보도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;h2&gt;왜 메시지 큐를 쓰는가?&lt;/h2&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;메시지 큐를 쓰는 이유는 대략적으로 아래와 같습니다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;먼저, 기존의 단순한 동기식 통신 방식에 비해서 더 뛰어난 응답속도를 보여줍니다.&lt;/p&gt;
&lt;p&gt;기존의 동기식 통신 방식은, 사용자로 부터 요청을 받아서 요청을 다 처리할때 까지 Blocking 상태에 빠지게 됩니다.&lt;/p&gt;
&lt;p&gt;즉, 요청이 전부 처리가 되어야 사용자에게 응답을 주게 됩니다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;하지만, 메시지 큐를 이용하는 경우 사용자에게 요청을 받으면 큐에다 집어 넣기만 하면 바로 다음 사용자의 요청을 받아들일 수 있기 때문에 응답속도가 향상되게 됩니다.&lt;/p&gt;
&lt;p&gt;(실제 처리는 쌓여진 큐에서 다른 워커 프로세스가 1개씩 가져가서 처리하는 방식으로 이루어집니다.)&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;실제로, Node.js 와 같은 비동기 방식 아키텍처를 적용한 시스템과 같은 경우 내부적으로 이벤트 큐를 이용하여 비동기를 구현합니다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;또한, 메시지 큐를 이용해서 메시지를 주고 받을 수 있다는 사실과, 객체지향에서의 메서드 호출이 사실 다른 객체에게 해당 동작을 수행하라고 부탁하는 메시지를 보내는거라는 의미를 조합할 수 있습니다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;즉 요약하면, 메시지 큐를 이용해서 서로 다른 이기종/네트워크에 있는 서비스를 마치 함수호출 하듯이 사용 할 수 있습니다. (보통 이러한 방식을 보고 RPC 또는 RPI 방식 이라고 합니다.)&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;이러한 방식을 사용하면 각 기능간의 결합도를 낮출 수 있고, 이 말은 즉 모듈 간의 결합도를 줄일 수 있다는 의미입니다.&lt;/p&gt;
&lt;p&gt;(심지어 각 기능을 서로 다른언어로 짜도 되고, 각 모듈을 서로 다른 서버에 배치할 수도 있습니다!)&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;그리고 이렇게 서비스의 결합을 분리해버리면, 모놀리틱 서비스(즉, 한 프로그램에 모든 기능이 전부 몰려있는 이전 방식)이 서비스가 죽으면 모든기능을 전부 쓰지 못하는데 반해, 위 방식처럼 서비스를 분리하면 한 서비스가 죽어도, 죽어버린 서비스 이외의 나머지 서비스는 동작을 하기때문에 서비스의 안정성이 올라가게 됩니다.&lt;/p&gt;
&lt;p&gt;(사실은, 메시지큐를 관리해야 되는 이슈들 때문에 꼭 그렇지 만은 않습니다 ㅎㅎ...)&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;이렇게 메시지 큐를 잘 사용하면, 대규모 데이터를 처리하거나 응답속도가 중요한 서비스에서 많은 이점을 누릴 수 있습니다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;사실 이러한 메시지 큐 서비스의 종류는 상당히 다양하게 있습니다.&lt;/p&gt;
&lt;p&gt;대표적으로, rabbitMQ 라는 서비스가 있고, 그 외에도 ZeroMQ, ActiveMQ 등의 많은 서비스가 있습니다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;그 중에서, 이번시간에 우리가 다룰 메시지 큐는 카프카 입니다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;h2&gt;카프카의 이점?&lt;/h2&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;카프카가 다른 서비스 대비 이점이 무엇이 있을까요?&lt;/p&gt;
&lt;p&gt;카프카의 장점은 아래와 같습니다.​&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;다른 메시지 큐에 비해 실시간 로그 처리에 특화 되어있음&lt;/li&gt;
&lt;li&gt;파일 시스템에 메시지를 저장함
&lt;ol&gt;
&lt;li&gt;데이터의 영속성이 보장됨&lt;/li&gt;
&lt;li&gt;Sequential 하게 읽고 쓰는 경우, 기존 메시지 큐와 속도차이가 비슷하거나 심지어 더 빠른경우도 있음&lt;/li&gt;
&lt;li&gt;다른 메시지 큐에 비해 메시지 유실 위험이 적고, 에러 복구가 용이함&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;프로토콜이 다른 MQ에 비해서 간단하기 때문에 오버헤드가 적음&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;실제로 아래 그림과 같이 다른 MQ에 비해서 성능이 빠르다고 합니다...&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;217D7945550917BD0B.png&quot; data-origin-width=&quot;850&quot; data-origin-height=&quot;677&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cyo6cg/btqEbQsiGOS/3Rp0dHeCBjgWZa6bCxkdE1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cyo6cg/btqEbQsiGOS/3Rp0dHeCBjgWZa6bCxkdE1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cyo6cg/btqEbQsiGOS/3Rp0dHeCBjgWZa6bCxkdE1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcyo6cg%2FbtqEbQsiGOS%2F3Rp0dHeCBjgWZa6bCxkdE1%2Fimg.png&quot; data-filename=&quot;217D7945550917BD0B.png&quot; data-origin-width=&quot;850&quot; data-origin-height=&quot;677&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;23441B445509177A1F.png&quot; data-origin-width=&quot;850&quot; data-origin-height=&quot;672&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/X6KHk/btqEdj75bIM/pHJllkJ09b6ukUuLGfJ3P1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/X6KHk/btqEdj75bIM/pHJllkJ09b6ukUuLGfJ3P1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/X6KHk/btqEdj75bIM/pHJllkJ09b6ukUuLGfJ3P1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FX6KHk%2FbtqEdj75bIM%2FpHJllkJ09b6ukUuLGfJ3P1%2Fimg.png&quot; data-filename=&quot;23441B445509177A1F.png&quot; data-origin-width=&quot;850&quot; data-origin-height=&quot;672&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;그럼 실제로 스프링에서 카프카를 써보도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;h2&gt;예제&lt;/h2&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;먼저, 카프카를 설치해줍니다.&lt;/p&gt;
&lt;p&gt;맥(OS X) 환경 기준으로 아래와 같이 간편하게 설치 할 수 있습니다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;brew install kafka&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;그리고 아래 명령어를 이용해서 카프카 서비스를 띄워줍시다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;brew service kafka start&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;사실, 아래 명령어를 실행해서 카프카 메시지 큐의 토픽(메시지를 여기로 보내라는 일종의 우체통 같은거라고 생각하면 됩니다.)을 만들 수는 있습니다.&lt;/p&gt;
&lt;pre class=&quot;brainfuck&quot;&gt;&lt;code&gt;$ bin/kafka-topics.sh --create \
  --zookeeper localhost:2181 \
  --replication-factor 1 --partitions 1 \
  --topic mytopic&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;하지만, 뒤에서 설명을 하겠지만, 코드를 이용해서도 토픽을 생성할 수 있습니다.&lt;/p&gt;
&lt;p&gt;(보통 코드를 이용해서 토픽을 생성합니다...)&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;다음으로, Springboot initializer 또는 자기가 쓰고 있는 IDE를 이용해서 스프링 프로젝트를 만들어 줍시다.&lt;/p&gt;
&lt;p&gt;그리고 pom.xml에 아래와 같이 추가해줍니다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;lt;project xmlns=&quot;http://maven.apache.org/POM/4.0.0&quot;
         xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;
         xsi:schemaLocation=&quot;http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd&quot;&amp;gt;
    &amp;lt;modelVersion&amp;gt;4.0.0&amp;lt;/modelVersion&amp;gt;

    &amp;lt;groupId&amp;gt;com.example.kafkaexample&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;kafkaexample&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;1.0-SNAPSHOT&amp;lt;/version&amp;gt;

    &amp;lt;parent&amp;gt;
        &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
        &amp;lt;artifactId&amp;gt;spring-boot-starter-parent&amp;lt;/artifactId&amp;gt;
        &amp;lt;version&amp;gt;2.1.6.RELEASE&amp;lt;/version&amp;gt;
    &amp;lt;/parent&amp;gt;

    &amp;lt;properties&amp;gt;
        &amp;lt;project.build.sourceEncoding&amp;gt;UTF-8&amp;lt;/project.build.sourceEncoding&amp;gt;
        &amp;lt;project.build.outputEncoding&amp;gt;UTF-8&amp;lt;/project.build.outputEncoding&amp;gt;
        &amp;lt;java.version&amp;gt;1.8&amp;lt;/java.version&amp;gt;
    &amp;lt;/properties&amp;gt;


    &amp;lt;dependencies&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-boot-starter&amp;lt;/artifactId&amp;gt;
            &amp;lt;version&amp;gt;2.2.5.RELEASE&amp;lt;/version&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework.kafka&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-kafka&amp;lt;/artifactId&amp;gt;
            &amp;lt;version&amp;gt;2.2.7.RELEASE&amp;lt;/version&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;com.fasterxml.jackson.core&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;jackson-databind&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;
    &amp;lt;/dependencies&amp;gt;

&amp;lt;/project&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;다음으로, kafka의 설정 파일을 application.properties 파일에 아래와 같이 추가 해주자.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;kafka.bootstrapAddress=localhost:9092
message.topic.name=mytopic
greeting.topic.name=greeting
filtered.topic.name=filtered
partitioned.topic.name=partitioned&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;다음으로, 아래와 같이 Kafka의 Topic에 관한 설정을 해준다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;package com.example.kafkaexample.config;


import org.apache.kafka.clients.admin.AdminClientConfig;
import org.apache.kafka.clients.admin.NewTopic;
import org.apache.kafka.common.internals.Topic;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.core.KafkaAdmin;

import java.util.HashMap;
import java.util.Map;

@Configuration
public class KafkaTopicConfig {

    @Value(value = &quot;${kafka.bootstrapAddress}&quot;)
    private String bootstrapAddress;

    @Value(value = &quot;${message.topic.name}&quot;)
    private String topicName;

    @Value(value = &quot;${partitioned.topic.name}&quot;)
    private String partionedTopicName;

    @Value(value = &quot;${filtered.topic.name}&quot;)
    private String filteredTopicName;

    @Value(value = &quot;${greeting.topic.name}&quot;)
    private String greetingTopicName;

    @Bean
    public KafkaAdmin kafkaAdmin() {
        Map&amp;lt;String, Object&amp;gt; configs = new HashMap&amp;lt;&amp;gt;();
        configs.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapAddress);
        return new KafkaAdmin(configs);
    }

    @Bean
    public NewTopic topic1() {
        return new NewTopic(topicName,1,(short)1);
    }

    @Bean
    public NewTopic topic2() {
        return new NewTopic(partionedTopicName, 6, (short) 1);
    }

    @Bean
    public NewTopic topic3() {
        return new NewTopic(filteredTopicName, 1, (short) 1);
    }

    @Bean
    public NewTopic topic4() {
        return new NewTopic(greetingTopicName, 1, (short) 1);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;Kafka-Spring 에서는 위의 코드를 통해서, 코드를 이용해서 프로그래밍 적으로 메시지 큐의 토픽을 생성 할 수 있습니다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;스프링 부트(Kafka-Spring)에서는, 위와 같이 토픽을 생성해주는 함수를 만들고 Bean으로 등록해주면 자동으로 토픽을 생성해서 주입해줍니다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;또한, KafkaAdmin 타입의 생성자를 만들어서, 카프카의 설정정보도 주입이 가능합니다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;그리고 나서, 실제로 메시지를 발행하는 Producer에 관한 설정을 아래 코드와 같이 해줍시다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;package com.example.kafkaexample.config;


import com.example.kafkaexample.Greeting;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.common.serialization.StringSerializer;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.core.DefaultKafkaProducerFactory;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.kafka.core.ProducerFactory;
import org.springframework.kafka.support.serializer.JsonSerializer;

import java.util.HashMap;
import java.util.Map;

@Configuration
public class KafkaProducerConfig {

    @Value(value = &quot;${kafka.bootstrapAddress}&quot;)
    private String bootstrapAddress;

    @Bean
    public ProducerFactory&amp;lt;String, String&amp;gt; producerFactory() {
        Map&amp;lt;String, Object&amp;gt; configProps = new HashMap&amp;lt;&amp;gt;();
        configProps.put(
            ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,
            bootstrapAddress
        );
        configProps.put(
            ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG,
            StringSerializer.class
        );
        configProps.put(
            ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,
            StringSerializer.class
        );

        return new DefaultKafkaProducerFactory&amp;lt;&amp;gt;(configProps);
    }

    @Bean
    public KafkaTemplate&amp;lt;String, String&amp;gt; kafkaTemplate() {
        return new KafkaTemplate&amp;lt;String, String&amp;gt;(producerFactory());
    }

    public ProducerFactory&amp;lt;String, Greeting&amp;gt; greetingProducerFactory() {
        Map&amp;lt;String, Object&amp;gt; configProps = new HashMap&amp;lt;&amp;gt;();
        configProps.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapAddress);
        configProps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
        configProps.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class);
        return new DefaultKafkaProducerFactory&amp;lt;&amp;gt;(configProps);
    }

    @Bean
    public KafkaTemplate&amp;lt;String, Greeting&amp;gt; greetingKafkaTemplate() {
        return new KafkaTemplate&amp;lt;&amp;gt;(greetingProducerFactory());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;먼저, ProducerFactory 객체를 이용해서 각 메시지 종류별로, 메시지를 어디에 보내고, 어떠한 방식으로 처리할것인지를 설정해줍니다. 그리고 카프카에서, 실제 메시지는 KafkaTemplate 이라는 객체에 담겨서 보내지게 됩니다.&lt;/p&gt;
&lt;p&gt;(일종의 편지봉투 라고 보시면 됩니다.)&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;만약 소켓프로그래밍에 익숙하다면, Producer 객체는 소켓 디스크립터고, ProducerFactory는 소켓 디스크립터를 만들어주는 팩토리 메서드 라고 이해하면 편할겁니다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;위의 예제에서는, 2가지 종류의 메시지를 정의 하였습니다.&lt;/p&gt;
&lt;p&gt;(즉, 2가지 종류의 편지봉투를 만들었다고 보시면 됩니다.)&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;다음으로, 실제로 메시지를 가져오는 부분인 Consumer에 대한 코드를 아래와 같이 작성 해줍니다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;package com.example.kafkaexample.config;


import com.example.kafkaexample.Greeting;
import org.springframework.kafka.support.serializer.JsonDeserializer;
import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.common.serialization.StringDeserializer;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.annotation.EnableKafka;
import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory;
import org.springframework.kafka.core.ConsumerFactory;
import org.springframework.kafka.core.DefaultKafkaConsumerFactory;

import java.util.HashMap;
import java.util.Map;

@EnableKafka
@Configuration
public class KafkaConsumerConfig {

    @Value(value = &quot;${kafka.bootstrapAddress}&quot;)
    private String bootstrapAddress;

    public ConsumerFactory&amp;lt;String, String&amp;gt; consumerFactory(String groupId){
        Map&amp;lt;String, Object&amp;gt; props = new HashMap&amp;lt;&amp;gt;();
        props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapAddress);
        props.put(ConsumerConfig.GROUP_ID_CONFIG, groupId);
        props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
        props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
        return new DefaultKafkaConsumerFactory&amp;lt;&amp;gt;(props);
    }

    public ConcurrentKafkaListenerContainerFactory&amp;lt;String, String&amp;gt; kafkaListenerContainerFactory(String groupId) {
        ConcurrentKafkaListenerContainerFactory&amp;lt;String, String&amp;gt; factory = new ConcurrentKafkaListenerContainerFactory&amp;lt;&amp;gt;();
        factory.setConsumerFactory(consumerFactory(groupId));
        return factory;
    }

    @Bean
    public ConcurrentKafkaListenerContainerFactory&amp;lt;String, String&amp;gt; fooKafkaListenerContainerFactory() {
        return kafkaListenerContainerFactory(&quot;foo&quot;);
    }

    @Bean
    public ConcurrentKafkaListenerContainerFactory&amp;lt;String, String&amp;gt; barKafkaListenerContainerFactory() {
        return kafkaListenerContainerFactory(&quot;bar&quot;);
    }

    @Bean
    public ConcurrentKafkaListenerContainerFactory&amp;lt;String, String&amp;gt; headersKafkaListenerContainerFactory() {
        return kafkaListenerContainerFactory(&quot;headers&quot;);
    }

    @Bean
    public ConcurrentKafkaListenerContainerFactory&amp;lt;String, String&amp;gt; partitionsKafkaListenerContainerFactory() {
        return kafkaListenerContainerFactory(&quot;partitions&quot;);
    }

    @Bean
    public ConcurrentKafkaListenerContainerFactory&amp;lt;String,String&amp;gt; filterKafkaListenerContainerFactory() {
        ConcurrentKafkaListenerContainerFactory&amp;lt;String, String&amp;gt; factory = kafkaListenerContainerFactory(&quot;filter&quot;);
        factory.setRecordFilterStrategy(record -&amp;gt; record.value().contains(&quot;world&quot;));
        return factory;
    }


    public ConsumerFactory&amp;lt;String, Greeting&amp;gt; greetingConsumerFactory() {
        Map&amp;lt;String, Object&amp;gt; props = new HashMap&amp;lt;&amp;gt;();
        props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapAddress);
        props.put(ConsumerConfig.GROUP_ID_CONFIG, &quot;greeting&quot;);
        return new DefaultKafkaConsumerFactory&amp;lt;&amp;gt;(props, new StringDeserializer(), new JsonDeserializer&amp;lt;&amp;gt;(Greeting.class));
    }

    @Bean
    public ConcurrentKafkaListenerContainerFactory&amp;lt;String, Greeting&amp;gt; greetingKafkaListenerContainerFactory() {
        ConcurrentKafkaListenerContainerFactory&amp;lt;String, Greeting&amp;gt; factory = new ConcurrentKafkaListenerContainerFactory&amp;lt;String, Greeting&amp;gt;();
        factory.setConsumerFactory(greetingConsumerFactory());
        return factory;
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;위의, Producer 부분과 유사하게 ConsumerFactory 객체를 이용해서 각 메시지 종류별로, 메시지를 어디에서 받고, 어떠한 방식으로 처리할것인지를 설정 해줍니다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;그리고 위의 예제에서, Consumer와 같은 경우, 위에서 설정한 각 Topic 별로 메시지를 어디서/어떻게 받을지를 설정해주는 메서드들을 지정하였습니다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;마지막으로, 위에서 작성한 설정을 기반으로 아래와 같이 메시지 큐에 데이터를 넣고 빼보도록 합시다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;package com.example.kafkaexample;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.kafka.annotation.TopicPartition;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.kafka.support.KafkaHeaders;
import org.springframework.kafka.support.SendResult;
import org.springframework.messaging.handler.annotation.Header;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.util.concurrent.ListenableFuture;
import org.springframework.util.concurrent.ListenableFutureCallback;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

@SpringBootApplication
public class KafkaExampleApplication {
    public static void main(String[] args) throws Exception {
        ConfigurableApplicationContext context = SpringApplication.run(KafkaExampleApplication.class, args);

        MessageProducer producer = context.getBean(MessageProducer.class);
        MessageListener listener = context.getBean(MessageListener.class);

        producer.sendMessage(&quot;Hello, World!&quot;);
        listener.latch.await(10, TimeUnit.SECONDS);


        for (int i = 0; i &amp;lt; 5; i++) {
            producer.sendMessageToPartion(&quot;Hello To Partioned Topic!&quot;, i);
        }
        listener.partitionLatch.await(10, TimeUnit.SECONDS);


        producer.sendMessageToFiltered(&quot;Hello Baeldung!&quot;);
        producer.sendMessageToFiltered(&quot;Hello World!&quot;);
        listener.filterLatch.await(10, TimeUnit.SECONDS);


        producer.sendGreetingMessage(new Greeting(&quot;Greetings&quot;, &quot;World!&quot;));
        listener.greetingLatch.await(10, TimeUnit.SECONDS);

        context.close();
    }

    @Bean
    public MessageProducer messageProducer() {
        return new MessageProducer();
    }

    @Bean
    public MessageListener messageListener() {
        return new MessageListener();
    }

    public static class MessageProducer {

        @Autowired
        private KafkaTemplate&amp;lt;String, String&amp;gt; kafkaTemplate;

        @Autowired
        private KafkaTemplate&amp;lt;String, Greeting&amp;gt; greetingKafkaTemplate;

        @Value(value = &quot;${message.topic.name}&quot;)
        private String topicName;

        @Value(value = &quot;${partitioned.topic.name}&quot;)
        private String partionedTopicName;

        @Value(value = &quot;${filtered.topic.name}&quot;)
        private String filteredTopicName;

        @Value(value = &quot;${greeting.topic.name}&quot;)
        private String greetingTopicName;

        public void sendMessage(String message) {

            ListenableFuture&amp;lt;SendResult&amp;lt;String, String&amp;gt;&amp;gt; future = kafkaTemplate.send(topicName, message);

            future.addCallback(new ListenableFutureCallback&amp;lt;SendResult&amp;lt;String, String&amp;gt;&amp;gt;() {

                @Override
                public void onSuccess(SendResult&amp;lt;String, String&amp;gt; result) {
                    System.out.println(&quot;Sent message=[&quot; + message + &quot;] with offset=[&quot; + result.getRecordMetadata()
                            .offset() + &quot;]&quot;);
                }

                @Override
                public void onFailure(Throwable ex) {
                    System.out.println(&quot;Unable to send message=[&quot; + message + &quot;] due to : &quot; + ex.getMessage());
                }
            });
        }

        public void sendMessageToPartion(String message, int partition) {
            kafkaTemplate.send(partionedTopicName, partition, null, message);
        }

        public void sendMessageToFiltered(String message) {
            kafkaTemplate.send(filteredTopicName, message);
        }

        public void sendGreetingMessage(Greeting greeting) {
            greetingKafkaTemplate.send(greetingTopicName, greeting);
        }
    }

    public static class MessageListener {

        private CountDownLatch latch = new CountDownLatch(3);

        private CountDownLatch partitionLatch = new CountDownLatch(2);

        private CountDownLatch filterLatch = new CountDownLatch(2);

        private CountDownLatch greetingLatch = new CountDownLatch(1);

        @KafkaListener(topics = &quot;${message.topic.name}&quot;, groupId = &quot;foo&quot;, containerFactory = &quot;fooKafkaListenerContainerFactory&quot;)
        public void listenGroupFoo(String message) {
            System.out.println(&quot;Received Messasge in group 'foo': &quot; + message);
            latch.countDown();
        }

        @KafkaListener(topics = &quot;${message.topic.name}&quot;, groupId = &quot;bar&quot;, containerFactory = &quot;barKafkaListenerContainerFactory&quot;)
        public void listenGroupBar(String message) {
            System.out.println(&quot;Received Messasge in group 'bar': &quot; + message);
            latch.countDown();
        }

        @KafkaListener(topics = &quot;${message.topic.name}&quot;, containerFactory = &quot;headersKafkaListenerContainerFactory&quot;)
        public void listenWithHeaders(@Payload String message, @Header(KafkaHeaders.RECEIVED_PARTITION_ID) int partition) {
            System.out.println(&quot;Received Messasge: &quot; + message + &quot; from partition: &quot; + partition);
            latch.countDown();
        }

        @KafkaListener(topicPartitions = @TopicPartition(topic = &quot;${partitioned.topic.name}&quot;, partitions = { &quot;0&quot;, &quot;3&quot; }), containerFactory = &quot;partitionsKafkaListenerContainerFactory&quot;)
        public void listenToParition(@Payload String message, @Header(KafkaHeaders.RECEIVED_PARTITION_ID) int partition) {
            System.out.println(&quot;Received Message: &quot; + message + &quot; from partition: &quot; + partition);
            this.partitionLatch.countDown();
        }

        @KafkaListener(topics = &quot;${filtered.topic.name}&quot;, containerFactory = &quot;filterKafkaListenerContainerFactory&quot;)
        public void listenWithFilter(String message) {
            System.out.println(&quot;Recieved Message in filtered listener: &quot; + message);
            this.filterLatch.countDown();
        }

        @KafkaListener(topics = &quot;${greeting.topic.name}&quot;, containerFactory = &quot;greetingKafkaListenerContainerFactory&quot;)
        public void greetingListener(Greeting greeting) {
            System.out.println(&quot;Recieved greeting message: &quot; + greeting);
            this.greetingLatch.countDown();
        }

    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;코드의 양이 많으니 하나씩 뜯어서 보도록 합시다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;먼저, 메시지를 생성하는 부분입니다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;public static class MessageProducer {

  @Autowired
  private KafkaTemplate&amp;lt;String, String&amp;gt; kafkaTemplate;

  @Autowired
  private KafkaTemplate&amp;lt;String, Greeting&amp;gt; greetingKafkaTemplate;

  @Value(value = &quot;${message.topic.name}&quot;)
  private String topicName;

  @Value(value = &quot;${partitioned.topic.name}&quot;)
  private String partitionedTopicName;

  @Value(value = &quot;${filtered.topic.name}&quot;)
  private String filteredTopicName;

  @Value(value = &quot;${greeting.topic.name}&quot;)
  private String greetingTopicName;

  public void sendMessage(String message) {

    ListenableFuture&amp;lt;SendResult&amp;lt;String, String&amp;gt;&amp;gt; future = kafkaTemplate.send(topicName, message);

    future.addCallback(new ListenableFutureCallback&amp;lt;SendResult&amp;lt;String, String&amp;gt;&amp;gt;() {

      @Override
      public void onSuccess(SendResult&amp;lt;String, String&amp;gt; result) {
        System.out.println(&quot;Sent message=[&quot; + message + &quot;] with offset=[&quot; + result.getRecordMetadata()
                           .offset() + &quot;]&quot;);
      }

      @Override
      public void onFailure(Throwable ex) {
        System.out.println(&quot;Unable to send message=[&quot; + message + &quot;] due to : &quot; + ex.getMessage());
      }
    });
  }

  public void sendMessageToPartion(String message, int partition) {
    kafkaTemplate.send(partitionedTopicName, partition, null, message);
  }

  public void sendMessageToFiltered(String message) {
    kafkaTemplate.send(filteredTopicName, message);
  }

  public void sendGreetingMessage(Greeting greeting) {
    greetingKafkaTemplate.send(greetingTopicName, greeting);
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;먼저, 2가지 종류의 KafkaTemplate을 정의하였습니다.&lt;/p&gt;
&lt;p&gt;(해당 KafkaTemplate 들은 위에서 만든 KafkaProducerConfig에 있는 빈 객체가 대입되게 됩니다.)&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;다음으로, kafkaTemplate를 이용해서 메시지를 전송합니다. 사실 메시지 큐 방식으로 통신하는 경우 필연적으로 비동기 방식으로 통신하기 때문에(메시지가 언제올지 알 수가 없으므로..) 콜백 함수를 등록하게 됩니다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;(여담으로 rabbitMQ도 그렇지만 보통 메시지 큐에서는 저렇게 요청-응답 을 쌍으로 받으려면 보통 수신 큐, 송신 큐 2개를 둬서 통신을 하게 됩니다. 이러한 임시 큐들을 카프카와 같은 메시지 브로커 서비스에서 자동으로 만들어주게 됩니다.)&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;다음으로, 메시지를 받는 부분입니다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;public static class MessageListener {

  private CountDownLatch latch = new CountDownLatch(3);

  private CountDownLatch partitionLatch = new CountDownLatch(2);

  private CountDownLatch filterLatch = new CountDownLatch(2);

  private CountDownLatch greetingLatch = new CountDownLatch(1);

  @KafkaListener(topics = &quot;${message.topic.name}&quot;, groupId = &quot;foo&quot;, containerFactory = &quot;fooKafkaListenerContainerFactory&quot;)
  public void listenGroupFoo(String message) {
    System.out.println(&quot;Received Messasge in group 'foo': &quot; + message);
    latch.countDown();
  }

  @KafkaListener(topics = &quot;${message.topic.name}&quot;, groupId = &quot;bar&quot;, containerFactory = &quot;barKafkaListenerContainerFactory&quot;)
  public void listenGroupBar(String message) {
    System.out.println(&quot;Received Messasge in group 'bar': &quot; + message);
    latch.countDown();
  }

  @KafkaListener(topics = &quot;${message.topic.name}&quot;, containerFactory = &quot;headersKafkaListenerContainerFactory&quot;)
  public void listenWithHeaders(@Payload String message, @Header(KafkaHeaders.RECEIVED_PARTITION_ID) int partition) {
    System.out.println(&quot;Received Messasge: &quot; + message + &quot; from partition: &quot; + partition);
    latch.countDown();
  }

  @KafkaListener(topicPartitions = @TopicPartition(topic = &quot;${partitioned.topic.name}&quot;, partitions = { &quot;0&quot;, &quot;3&quot; }), containerFactory = &quot;partitionsKafkaListenerContainerFactory&quot;)
  public void listenToParition(@Payload String message, @Header(KafkaHeaders.RECEIVED_PARTITION_ID) int partition) {
    System.out.println(&quot;Received Message: &quot; + message + &quot; from partition: &quot; + partition);
    this.partitionLatch.countDown();
  }

  @KafkaListener(topics = &quot;${filtered.topic.name}&quot;, containerFactory = &quot;filterKafkaListenerContainerFactory&quot;)
  public void listenWithFilter(String message) {
    System.out.println(&quot;Recieved Message in filtered listener: &quot; + message);
    this.filterLatch.countDown();
  }

  @KafkaListener(topics = &quot;${greeting.topic.name}&quot;, containerFactory = &quot;greetingKafkaListenerContainerFactory&quot;)
  public void greetingListener(Greeting greeting) {
    System.out.println(&quot;Recieved greeting message: &quot; + greeting);
    this.greetingLatch.countDown();
  }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;먼저, 어떠한 Topic의 메시지를 어떠한 방식으로 받을지를 @KafkaListener를 이용해서 지정해줍니다.&lt;/p&gt;
&lt;p&gt;그리고, KafkaListener를 통해서 특정 파티션의 메시지를 받거나 특정 그룹의 메시지를 받거나 하는등의 설정도 가능합니다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;그리고, Consumer와 같은 경우, 병렬로 메시지를 처리하는 경우도 있기때문에, 동시접근으로 인한 Race Condition과 같은 경우를 막기 위해서 CountDownLatch 라는 함수를 이용해서 접근을 제한하게 됩니다.&lt;/p&gt;
&lt;p&gt;(일종의 세마포어라고 보시면 됩니다.)&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;마지막으로 위에서 정의한 수신/송신 부분을 실제로 사용하는 곳입니다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;public class KafkaExampleApplication {
    public static void main(String[] args) throws Exception {
        ConfigurableApplicationContext context = SpringApplication.run(KafkaExampleApplication.class, args);

        MessageProducer producer = context.getBean(MessageProducer.class);
        MessageListener listener = context.getBean(MessageListener.class);

        producer.sendMessage(&quot;Hello, World!&quot;);
        listener.latch.await(10, TimeUnit.SECONDS);


        for (int i = 0; i &amp;lt; 5; i++) {
            producer.sendMessageToPartion(&quot;Hello To Partioned Topic!&quot;, i);
        }
        listener.partitionLatch.await(10, TimeUnit.SECONDS);


        producer.sendMessageToFiltered(&quot;Hello Baeldung!&quot;);
        producer.sendMessageToFiltered(&quot;Hello World!&quot;);
        listener.filterLatch.await(10, TimeUnit.SECONDS);


        producer.sendGreetingMessage(new Greeting(&quot;Greetings&quot;, &quot;World!&quot;));
        listener.greetingLatch.await(10, TimeUnit.SECONDS);

        context.close();
    }

  ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;실제로 실행해보면 아래와 같이 메시지를 주고받는것을 알 수 있습니다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Sent message=[Hello, World!] with offset=[1]
Received Messasge in group 'bar': Hello, World!
Received Messasge in group 'foo': Hello, World!
Received Messasge: Hello, World! from partition: 0
Received Message: Hello To Partioned Topic! from partition: 0
Received Message: Hello To Partioned Topic! from partition: 3
Recieved Message in filtered listener: Hello Baeldung!
Recieved Message in filtered listener: Hello World!&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;h2&gt;출처&lt;/h2&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://aws.amazon.com/ko/message-queue/benefits/&quot;&gt;https://aws.amazon.com/ko/message-queue/benefits/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.baeldung.com/spring-kafka#consuming-messages&quot;&gt;https://www.baeldung.com/spring-kafka#consuming-messages&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://epicdevs.com/17&quot;&gt;https://epicdevs.com/17&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>개발/Java</category>
      <author>semtax</author>
      <guid isPermaLink="true">https://semtax.tistory.com/83</guid>
      <comments>https://semtax.tistory.com/83#entry83comment</comments>
      <pubDate>Sat, 16 May 2020 00:17:19 +0900</pubDate>
    </item>
    <item>
      <title>안드로이드에서 String.format 쓸 때 주의점</title>
      <link>https://semtax.tistory.com/82</link>
      <description>&lt;h2&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2&gt;개요&lt;/h2&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이번 포스팅에서는, 안드로이드에서 String.format을 쓸 때, 발생했던 문제점을 정리해보고자 한다.&lt;/p&gt;
&lt;h2&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2&gt;문제점&lt;/h2&gt;
&lt;p&gt;보통, 특정 서식으로 이루어진 문자열을 만들때, 자바와 같은 경우 String.format 메서드를 써서 포매팅을 수행하게 된다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;하지만, 안드로이드에서는, String.format를 이용해서 부동소수점을 문자열 포맷팅 할때, 일부 디바이스에서 문제가 발생하게 된다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;대략적으로 아래와 같은 예외가 발생하게 된다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;Fatal Exception: java.lang.NullPointerException
Attempt to get length of null array

java.util.Formatter$FormatSpecifier.addZeros (Formatter.java:3505)
java.util.Formatter$FormatSpecifier.print (Formatter.java:3401)
java.util.Formatter$FormatSpecifier.print (Formatter.java:3332)
java.util.Formatter$FormatSpecifier.printFloat (Formatter.java:2893)
java.util.Formatter$FormatSpecifier.print (Formatter.java:2844)
java.util.Formatter.format (Formatter.java:2523)
java.util.Formatter.format (Formatter.java:2458)
java.lang.String.format (String.java:2770)

.....&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2&gt;해결책&lt;/h2&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;부동 소수점의 경우에는 String.format 대신에, DecimalFormat 클래스를 이용해서 포매팅 하면 된다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;대략적으로 아래와 같이 해주면 된다.&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;final DecimalFormat fpsFormat = new DecimalFormat(&quot;#.0&quot;);
String result = fpsFormat.format(renderFps)&lt;/code&gt;&lt;/pre&gt;</description>
      <category>개발/Android</category>
      <category>DecimalFormat</category>
      <category>format</category>
      <category>formatting</category>
      <category>java</category>
      <category>String.format</category>
      <category>문자열</category>
      <category>문자열 포매팅</category>
      <category>안드로이드</category>
      <category>자바</category>
      <category>포매팅</category>
      <author>semtax</author>
      <guid isPermaLink="true">https://semtax.tistory.com/82</guid>
      <comments>https://semtax.tistory.com/82#entry82comment</comments>
      <pubDate>Wed, 6 May 2020 22:37:18 +0900</pubDate>
    </item>
    <item>
      <title>자바 Uncaught Exception 캐치 하기</title>
      <link>https://semtax.tistory.com/81</link>
      <description>&lt;h2&gt;개요&lt;/h2&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이번 포스팅에서는, 자바의 모든 예외(Exception)을 한번애 전부 캐치 할 수 있는 방법에 대해서 포스팅을 하도록 하겠다.&lt;/p&gt;
&lt;h2&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2&gt;자바 예외 처리&lt;/h2&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;자바에서는 보통 아래와 같이 try-catch 문을 이용해서 에러를 핸들링 할 수 있다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;public class ExampleClass {
  public static void main(String[ ] args) {
    try {
      int[] numbers = {1, 2, 3};
      System.out.println(numbers[10]);
    } catch (Exception e) {
      System.out.println(&quot;Exception is occured!&quot;);
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;또한, throws 를 이용해서 예외를 다른곳으로 전파 할 수도 있다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;(보통 해당 클래스를 호출한곳에서 예외를 처리하게 할때 많이 사용한다.)&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;class ThrowsExample
{ 
    static void fun() 
    { 
        try
        { 
            throw new NullPointerException(&quot;Null pointer reference error!&quot;); 
        } 
        catch(NullPointerException e) 
        { 
            System.out.println(&quot;Caught inside fun().&quot;); 
            throw e; // rethrowing the exception 
        } 
    } 

    public static void main(String args[]) 
    { 
        try
        { 
            fun(); 
        } 
        catch(NullPointerException e) 
        { 
            System.out.println(&quot;Caught in main.&quot;); 
        } 
    } 
} &lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;하지만, 아래와 같이 어떠한 예외도 처리하지 않는 경우에는 어떻게 될까?&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;public class MainClass {
    public static void main(String[] args) {
        int[] arr = new int[10];
        arr[12] = 10;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그떄는 아래와 같은 에러메시지가 발생한다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;Exception in thread &quot;main&quot; java.lang.ArrayIndexOutOfBoundsException: 12
    at MainClass.main(MainClass.java:13)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;신기한것은, 우리는 예외처리를 한 것이 추가한게 없는데도 불구하고 자바에서 위와 같이 예외가 발생했을때, 에러메시지들을 찍어주는 역할을 한다는 것이다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그렇다면 자바에서는 위와 같이, 사용자가 어떠한 예외도 처리하지 않는 경우에 위와 같이 예외를 어떻게 처리할 수 있는 것일까?&lt;/p&gt;
&lt;p&gt;한 번, 해당 방법에 대해서 알아보도록 하자.&lt;/p&gt;
&lt;h2&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2&gt;Uncaught Exception Handler&lt;/h2&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;정답은, 자바의 Thread 클래스에 있는 &lt;b&gt;UncaughtExceptionHandler&lt;/b&gt; 라는 핸들러에 의해 기본적으로 등록된 핸들러에서 나머지 에러를 처리하게 된다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;따로 &lt;b&gt;UncaughtExceptionHandler&lt;/b&gt; 를 설정해주지 않은 경우, 자바에서 기본적으로 정의된 &lt;b&gt;UncaughtExceptionHandler&lt;/b&gt; 가 실행이 된다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;또한, 위의 &lt;b&gt;DefaultUncaughtExceptionHandler&lt;/b&gt; 를 아래와 같이 직접 정의해서 사용 가능하다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;아래와 같이 정의해서 사용 할 수 있다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;public class DefaultExceptionHandler implements Thread.UncaughtExceptionHandler {

    @Override
    public void uncaughtException(Thread t, Throwable e) {

        StackTraceElement[] stackTraceElements = t.getStackTrace();
        StackTraceElement[] exceptionStackTraceElements = e.getStackTrace();

        System.out.println(&quot;Thread Name : &quot; + t.getName());
        System.out.println(&quot;=========================================&quot;);
        for(StackTraceElement element : stackTraceElements) {
            System.out.println(&quot;class name : &quot; + element.getClassName());
            System.out.println(&quot;file name : &quot; + element.getFileName());
            System.out.println(&quot;method name : &quot; + element.getMethodName());
            System.out.println(&quot;line number : &quot; + element.getLineNumber());
        }

        System.out.println(&quot;=========================================&quot;);
        System.out.println(&quot;Exception Message : &quot; + e.getMessage());
        System.out.println(&quot;Exception Localized Message : &quot; + e.getLocalizedMessage());
        System.out.println(&quot;Exception Cause : &quot; + e.getCause());
        for(StackTraceElement element : exceptionStackTraceElements) {
            System.out.println(&quot;=========================================&quot;);
            System.out.println(&quot;exception class name : &quot; + element.getClassName());
            System.out.println(&quot;exception file name : &quot; + element.getFileName());
            System.out.println(&quot;exception method name : &quot; + element.getMethodName());
            System.out.println(&quot;exception line number : &quot; + element.getLineNumber());
            System.out.println(&quot;=========================================&quot;);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위의 UncaughtExpcetionHandler 를 통해, 해당 예외가 발생한 스레드의 전체 스택트레이스, 스레드 이름, 예외가 발생한 함수의 스택 트레이스, 에러메시지, 에러가 발생한 클래스이름, 메소드 이름, 파일 이름 등을 얻을 수 있다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;또한, 자바 디버깅 심볼 데이터가 살아있는 경우, 해당 에러가 발생한 라인 번호도 얻을 수 있다.&lt;/p&gt;
&lt;p&gt;(만약 디버깅 심볼 데이터가 없는 경우 Unknown 이라고 뜬다.)&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위에서 정의한 핸들러를 등록하는 방법은 아래와 같다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;Thread.setDefaultUncaughtExceptionHandler(new DefaultExceptionHandler());&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;아래와 같이 사용해보도록 하자.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;public class MainClass {
    public static void main(String[] args) throws InterruptedException{

        Thread.setDefaultUncaughtExceptionHandler(new DefaultExceptionHandler());

        AnotherTask task1 = new AnotherTask();
        Thread t1 = new Thread(task1);
        t1.start();

        while(true) {

        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그러면 다음과 같은 메시지를 얻을 수 있다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;oxygene&quot;&gt;&lt;code&gt;Thread Name : Thread-0
=========================================
class name : java.lang.Thread
file name : Thread.java
method name : getStackTrace
line number : 1559
class name : DefaultExceptionHandler
file name : DefaultExceptionHandler.java
method name : uncaughtException
line number : 6
class name : java.lang.ThreadGroup
file name : ThreadGroup.java
method name : uncaughtException
line number : 1057
class name : java.lang.ThreadGroup
file name : ThreadGroup.java
method name : uncaughtException
line number : 1052
class name : java.lang.Thread
file name : Thread.java
method name : dispatchUncaughtException
line number : 1959
=========================================
Exception Message : / by zero
Exception Localized Message : / by zero
Exception Cause : null
=========================================
exception class name : AnotherTask
exception file name : AnotherTask.java
exception method name : run
exception line number : 4
=========================================
=========================================
exception class name : java.lang.Thread
exception file name : Thread.java
exception method name : run
exception line number : 748
=========================================&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2&gt;&amp;nbsp;&lt;/h2&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2&gt;활용&lt;/h2&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;보통, Sentry와 같은 에러 트래킹 솔루션에서 위와 같은 방법을 사용해서 에러 트래킹을 수행한다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/getsentry/sentry-java/blob/714dbaa4761173493a6af938e0cd88d63776e98d/sentry/src/main/java/io/sentry/SentryUncaughtExceptionHandler.java&quot;&gt;https://github.com/getsentry/sentry-java/blob/714dbaa4761173493a6af938e0cd88d63776e98d/sentry/src/main/java/io/sentry/SentryUncaughtExceptionHandler.java&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1588658386331&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-og-type=&quot;object&quot; data-og-title=&quot;getsentry/sentry-java&quot; data-og-description=&quot;A Sentry SDK for Java and other JVM languages. Contribute to getsentry/sentry-java development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/getsentry/sentry-java/blob/714dbaa4761173493a6af938e0cd88d63776e98d/sentry/src/main/java/io/sentry/SentryUncaughtExceptionHandler.java&quot; data-og-url=&quot;https://github.com/getsentry/sentry-java&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/fsJkO/hyFVFUvKqt/XbxdNhW5HMjs7vKtpwf8Y0/img.png?width=96&amp;amp;height=96&amp;amp;face=0_0_96_96&quot;&gt;&lt;a href=&quot;https://github.com/getsentry/sentry-java/blob/714dbaa4761173493a6af938e0cd88d63776e98d/sentry/src/main/java/io/sentry/SentryUncaughtExceptionHandler.java&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/getsentry/sentry-java/blob/714dbaa4761173493a6af938e0cd88d63776e98d/sentry/src/main/java/io/sentry/SentryUncaughtExceptionHandler.java&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/fsJkO/hyFVFUvKqt/XbxdNhW5HMjs7vKtpwf8Y0/img.png?width=96&amp;amp;height=96&amp;amp;face=0_0_96_96');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot;&gt;getsentry/sentry-java&lt;/p&gt;
&lt;p class=&quot;og-desc&quot;&gt;A Sentry SDK for Java and other JVM languages. Contribute to getsentry/sentry-java development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>개발/Java</category>
      <category>Error Tracking</category>
      <category>Exception</category>
      <category>java</category>
      <category>Sentry</category>
      <category>Thread</category>
      <category>Uncaught Exception</category>
      <category>예외 처리</category>
      <category>자바</category>
      <category>자바 스레드</category>
      <category>자바 예외</category>
      <author>semtax</author>
      <guid isPermaLink="true">https://semtax.tistory.com/81</guid>
      <comments>https://semtax.tistory.com/81#entry81comment</comments>
      <pubDate>Tue, 5 May 2020 14:58:28 +0900</pubDate>
    </item>
    <item>
      <title>스프링부트로 게시판 만들기 12(完) : API 문서 생성 자동화</title>
      <link>https://semtax.tistory.com/80</link>
      <description>&lt;h2&gt;개요&lt;/h2&gt;
&lt;p&gt;이번 포스팅에서는, Swagger를 이용해서 REST API 문서를 자동으로 생성하는 법을 다루려고 한다.&lt;/p&gt;
&lt;p&gt;​&lt;br&gt;​&lt;br&gt;​&lt;br&gt;​                                &lt;/p&gt;
&lt;h2&gt;문서화의 중요성&lt;/h2&gt;
&lt;p&gt;한가지 상황을 가정해보도록 하자.&lt;/p&gt;
&lt;p&gt;​&lt;br&gt;​ &lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;만약 여러분이 프론트 개발자, 다른 팀에서 만들어진 REST API를 사용 해야하는 입장의 개발자라고 가정을 해보자.&lt;/p&gt;
&lt;p&gt;이때, 프로젝트가 다 됬다고 듣고, API를 사용하려고 봤는데 어떻게 사용하는지에 대한 메뉴얼을 주지 않아서 사용방법을 알아내기 위해 저 API를 개발한 직원을 직접 부르거나 여러분이 직접 저 코드들을 분석해서 사용방법을 알아내느라 1~2주가 소요 되어버렸다. 상상만 해도 정말 끔찍하다.&lt;/p&gt;
&lt;p&gt;이런 상황을 어떻게 극복하면 좋을까?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;​                                &lt;/p&gt;
&lt;p&gt;​                                &lt;/p&gt;
&lt;p&gt;​                                &lt;/p&gt;
&lt;p&gt;​                                &lt;/p&gt;
&lt;p&gt;위와 같은 상황을 방지해주기 위해, 저러한 API를 만들고 나서는 API 문서를 만들어주는것이 필수이다.                                &lt;/p&gt;
&lt;p&gt;​                                &lt;/p&gt;
&lt;p&gt;하지만, 일일히 저러한 API를 직접 적어주는것은 너무나도 귀찮다. 이걸 자동화 하는 방법은 없을까?&lt;/p&gt;
&lt;p&gt;​                                &lt;/p&gt;
&lt;p&gt;다행히 스프링에서는 (사실 어지간한 다른 프레임워크에서도) Swagger 라는 API 문서를 자동으로 만들어주는 도구를 가지고 있다.&lt;/p&gt;
&lt;p&gt;​                                &lt;/p&gt;
&lt;p&gt;이제 이걸 사용하는 방법을 알아보도록 하자.&lt;/p&gt;
&lt;p&gt;(여담으로, swagger 플러그인들을 이용하면 JMeter와 같은 부하테스트 스크립트를 자동으로 만들어 줄수도 있어서 나중에 부하테스트도 손쉽게 할 수 있다)&lt;/p&gt;
&lt;p&gt;​                                &lt;/p&gt;
&lt;p&gt;​                                &lt;/p&gt;
&lt;p&gt;##Swagger 연동&lt;/p&gt;
&lt;p&gt;​                                &lt;/p&gt;
&lt;p&gt;먼저 maven에 아래 라이브러리들을 포함시켜주자.&lt;/p&gt;
&lt;p&gt;​                                &lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;lt;dependency&amp;gt;
  &amp;lt;groupId&amp;gt;io.springfox&amp;lt;/groupId&amp;gt;
  &amp;lt;artifactId&amp;gt;springfox-swagger2&amp;lt;/artifactId&amp;gt;
  &amp;lt;version&amp;gt;2.9.2&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;

&amp;lt;dependency&amp;gt;
  &amp;lt;groupId&amp;gt;io.springfox&amp;lt;/groupId&amp;gt;
  &amp;lt;artifactId&amp;gt;springfox-swagger-ui&amp;lt;/artifactId&amp;gt;
  &amp;lt;version&amp;gt;2.9.2&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​                                &lt;/p&gt;
&lt;p&gt;그리고 나서, 아래와 같이 Config 파일을 작성 해주자.&lt;/p&gt;
&lt;p&gt;​                                &lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;import com.google.common.base.Predicates;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

@Configuration
@EnableSwagger2
public class SwaggerConfig {
    @Bean
    public Docket api(){
        return new Docket(DocumentationType.SWAGGER_2).select()
            .apis(Predicates.not(RequestHandlerSelectors.
                    basePackage(&amp;quot;org.springframework.boot&amp;quot;)))
            .paths(PathSelectors.any()).build();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​                                &lt;/p&gt;
&lt;p&gt;​                                &lt;/p&gt;
&lt;p&gt;이제, 실행을 시켜보도록 하자.&lt;/p&gt;
&lt;p&gt;​                                &lt;/p&gt;
&lt;p&gt;​                                &lt;/p&gt;
&lt;h2&gt;테스트&lt;/h2&gt;
&lt;p&gt;​                                &lt;/p&gt;
&lt;p&gt;일단, 문서를 보기 위해서는 단순하게 아래 URL에 들어가면 된다.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://localhost:8080/swagger-ui.html#/&quot;&gt;http://localhost:8080/swagger-ui.html#/&lt;/a&gt;&lt;br&gt;​&lt;br&gt;​&lt;br&gt;​&lt;br&gt;​&lt;br&gt;그러면 아래와 같은 화면을 볼 수 있다.&lt;/p&gt;
&lt;p&gt;​&lt;br&gt;​&lt;br&gt;​    &lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zRiHy/btqDQcinDTF/i4djhC99kGzHKkg3OO1ns1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zRiHy/btqDQcinDTF/i4djhC99kGzHKkg3OO1ns1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zRiHy/btqDQcinDTF/i4djhC99kGzHKkg3OO1ns1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzRiHy%2FbtqDQcinDTF%2Fi4djhC99kGzHKkg3OO1ns1%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;​&lt;br&gt;​&lt;br&gt;​&lt;br&gt;​                                &lt;/p&gt;
&lt;p&gt;​                                &lt;/p&gt;
&lt;h2&gt;결론&lt;/h2&gt;
&lt;p&gt;​                                &lt;/p&gt;
&lt;p&gt;이제 기본적인 작업은 거의다 끝이 나기는 했다. 사실, 이번 강좌 같은 경우에는 기능추가 라기 보다는 Swagger와 같은 API 문서를 자동화 해주는 걸 알려주고 싶어서 넣은 면도 있다.&lt;/p&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;li&gt;유닛테스트 추가&lt;/li&gt;
&lt;li&gt;프론트엔드 작업&lt;/li&gt;
&lt;li&gt;OAuth&lt;/li&gt;
&lt;li&gt;기타 부가기능(레벨 기능, 비밀글, 권한 등등)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​                                &lt;/p&gt;
&lt;p&gt;사실 위의 것도 후속강의로 포스팅을 하면 좋겠으나, 일단은 여기서 마치도록 하겠다.&lt;/p&gt;
&lt;p&gt;나머지 부분은 여러분들의 과제로 남겨두도록 하겠다...&lt;/p&gt;
&lt;p&gt;​                                &lt;/p&gt;
&lt;p&gt;지금까지 봐주셔서 감사합니다.&lt;/p&gt;
&lt;p&gt;​                                &lt;/p&gt;
&lt;p&gt;​                                &lt;/p&gt;
&lt;p&gt;p.s &lt;/p&gt;
&lt;p&gt;​                                &lt;/p&gt;
&lt;p&gt;지금까지 봐주셔서 감사합니다.&lt;/p&gt;
&lt;p&gt;다음 포스팅은 다른 프로젝트로 찾아뵙도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;프론트와 같은 경우, 나중에 리액트 포스팅을 하면서 진행을 할 수 는 있을것 같다..&lt;/p&gt;</description>
      <category>개발/Java</category>
      <category>API 문서 자동화</category>
      <category>java</category>
      <category>REST API</category>
      <category>SWAGGER</category>
      <category>swagger-ui</category>
      <category>문서화</category>
      <category>스웨거</category>
      <category>스프링 부트</category>
      <category>웹 개발</category>
      <category>자바</category>
      <author>semtax</author>
      <guid isPermaLink="true">https://semtax.tistory.com/80</guid>
      <comments>https://semtax.tistory.com/80#entry80comment</comments>
      <pubDate>Sat, 2 May 2020 16:33:05 +0900</pubDate>
    </item>
    <item>
      <title>스프링부트로 게시판 만들기 11 : 파일 업로드/다운로드 구현</title>
      <link>https://semtax.tistory.com/79</link>
      <description>&lt;h2&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2&gt;개요&lt;/h2&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이번 포스팅에서는, 파일 업로드 및 다운로드 기능을 구현하도록 하겠다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;또한, 구현 하기전에 실제로 파일 업로드/다운로드가 어떻게 이루어지는지에 대해서도 알아보도록 하겠다.&lt;/p&gt;
&lt;h2&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2&gt;웹에서는 파일 업로드 / 다운로드를 어떻게 하는걸까?&lt;/h2&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;기본적으로, HTTP 요청/응답 프로토콜의 생김새는 대충 아래와 같이 생겼다.&lt;/p&gt;
&lt;pre class=&quot;http&quot;&gt;&lt;code&gt;POST /cgi-bin/process.cgi HTTP/1.1
User-Agent: Mozilla/4.0 (compatible; MSIE5.01; Windows NT)
Host: www.tutorialspoint.com
Content-Type: application/x-www-form-urlencoded
Content-Length: length
Accept-Language: en-us
Accept-Encoding: gzip, deflate
Connection: Keep-Alive

licenseID=string&amp;amp;content=string&amp;amp;/paramsXML=string&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위에서 보면 알겠지만, HTTP는 (일반적으로) 요청을 보내고 응답을 바로 받는, TCP나 TLS처럼 상태를 유지하지 않는(Stateless) 프로토콜이다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;따라서 파일의 메타정보와 실제 파일 컨텐츠를 한번에 보내줘야 한다. 하지만, 위의 예제만 보면 &lt;b&gt;한번에 여러 HTTP 요청 또는 응답 을 묶어서 보내는&lt;/b&gt; 기능은 따로 없어보인다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그렇다면 이러한 문제를 어떻게 해결 할 수 있을까?&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;사실, 여러가지 방법이 있기는 하지만, 가장 전형적인 방법으로 HTTP 프로토콜에서는 이러한 문제를 해결하기 위해 HTTP 인코딩(Encoding) 타입 중에 &quot;multipart/form-data&quot; 라는 인코딩 타입을 지원 해준다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;멀티파트 인코딩(multipart/form-data encoding)은 보통 아래와 같은 모양으로 이루어져 있다.&lt;/p&gt;
&lt;pre class=&quot;http&quot;&gt;&lt;code&gt;POST / HTTP/1.1
Host: localhost:8000
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux i686; rv:29.0) Gecko/20100101 Firefox/29.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Cookie: __atuvc=34%7C7; permanent=0; _gitlab_session=226ad8a0be43681acf38c2fab9497240; __profilin=p%3Dt; request_method=GET
Connection: keep-alive
Content-Type: multipart/form-data; boundary=---------------------------9051914041544843365972754266
Content-Length: 554

-----------------------------9051914041544843365972754266
Content-Disposition: form-data; name=&quot;text&quot;

text default
-----------------------------9051914041544843365972754266
Content-Disposition: form-data; name=&quot;file1&quot;; filename=&quot;a.txt&quot;
Content-Type: text/plain

Content of a.txt.

-----------------------------9051914041544843365972754266
Content-Disposition: form-data; name=&quot;file2&quot;; filename=&quot;a.html&quot;
Content-Type: text/html

&amp;lt;!DOCTYPE html&amp;gt;&amp;lt;title&amp;gt;Content of a.html.&amp;lt;/title&amp;gt;

-----------------------------9051914041544843365972754266--&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위에서 볼 수 있다시피, 멀티파트 인코딩은 아래와 같은 구성요소로 이루어져 있다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;구성요소&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;설명&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;boundary=-----------------------------xxxxxxx&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;HTTP 요청안에 합쳐진 여러개의 데이터를 각각 구분하기 위한 구분자&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;Content-Disposition: form-data; name=&quot;text&quot; filename=&quot;origina.jpg&quot;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;HTTP 요청의 일부라는 사실을 알려주기 위한 헤더, 해당 헤더에 각 파트에 해당하는 이름을 name 파라미터를 이용해서 식별할 수 있게 해준다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;Content-Type&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;HTTP 요청 안에 있는 각각의 데이터의 타입을 알려주는 헤더&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이때, 여러개의 HTTP 데이터를 묶어서 보내므로, 각 HTTP 요청의 끝을 구분하기 위해 바운더리가 필요하다는 사실을 금방 유추 할 수 있다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그리고, Content-Disposition 의 name도 HTML의 Form 태그를 사용 해보았다면, Form태그에서 작성했던 name이 그대로 들어간다는 사실도 알 수 있다. 또한, filename 속성을 통해 원래 전송하려고 했던 파일의 이름도 적어서 줄 수 있다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;(여담으로 HTTP응답을 보낼 때, Content-Disposition을, attachment 로 지정하고, filename을 적어주는 경우에, 만약 해당링크를 클릭하는 경우 브라우저에서 &quot;파일을 받으시겠습니까? 또는 다운로드 하기&quot; 라는 버튼을 통해서 파일을 다운 받을 수 있다.)&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;마지막으로, Content-Type도 약간 의아해 할 수는 있으나, Form 태그를 자세히 본다면 type을 이용해서 구분 할 수 있다는 사실을 알 수 있다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2&gt;구현&lt;/h2&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;먼저, 스프링 부트에서 제공하는 파일 업로드/다운로드 기능을 사용하기 위해서는 먼저, 아래와 같이 설정을 해주어야 한다.&lt;/p&gt;
&lt;pre class=&quot;mel&quot;&gt;&lt;code&gt;spring.servlet.multipart.enabled=true

spring.servlet.multipart.file-size-threshold=2KB

spring.servlet.multipart.max-file-size=200MB

spring.servlet.multipart.max-request-size=215MB

file.upload-dir=/Users/semtax/Desktop/uploads&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;다음으로, 아래와 같이 @ConfigurationProperties 를 이용해서, 위에서 작성한 프로퍼티를 등록해준다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties(prefix = &quot;file&quot;)
public class FileStorageProperties {

    private String uploadDir;

    public String getUploadDir() {
        return uploadDir;
    }

    public void setUploadDir(String uploadDir) {
        this.uploadDir = uploadDir;
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;일단, 파일 업로드/다운로드 API를 작성하기 전에 아래와 같이 파일 업로드/다운로드 정보를 저장하는 Entity와 DTO를 작성해주자.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;import javax.persistence.*;

@Entity
public class UploadFile {

    @Id @GeneratedValue
    @Column(name = &quot;upload_file_id&quot;)
    private Long Id;

    @Column
    private String fileName;

    @Column
    private String fileDownloadUri;

    @Column
    private String fileType;

    @Column
    private Long size;

    @ManyToOne
    @JoinColumn(name = &quot;post_id&quot;)
    private Post post;

    public Long getId() {
        return Id;
    }

    public void setId(Long id) {
        Id = id;
    }

    public String getFileName() {
        return fileName;
    }

    public void setFileName(String fileName) {
        this.fileName = fileName;
    }

    public Post getPost() {
        return post;
    }

    public void setPost(Post post) {
        this.post = post;
    }



    public String getFileDownloadUri() {
        return fileDownloadUri;
    }

    public void setFileDownloadUri(String fileDownloadUri) {
        this.fileDownloadUri = fileDownloadUri;
    }

    public String getFileType() {
        return fileType;
    }

    public void setFileType(String fileType) {
        this.fileType = fileType;
    }

    public Long getSize() {
        return size;
    }

    public void setSize(Long size) {
        this.size = size;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;package com.semtax.application.dto;

public class UploadFileResponseDTO {
    private String fileName;
    private String fileDownloadUri;
    private String fileType;
    private long size;

    public UploadFileResponseDTO(String fileName, String fileDownloadUri, String fileType, long size) {
        this.fileName = fileName;
        this.fileDownloadUri = fileDownloadUri;
        this.fileType = fileType;
        this.size = size;
    }

    public String getFileName() {
        return fileName;
    }

    public void setFileName(String fileName) {
        this.fileName = fileName;
    }

    public String getFileDownloadUri() {
        return fileDownloadUri;
    }

    public void setFileDownloadUri(String fileDownloadUri) {
        this.fileDownloadUri = fileDownloadUri;
    }

    public String getFileType() {
        return fileType;
    }

    public void setFileType(String fileType) {
        this.fileType = fileType;
    }

    public long getSize() {
        return size;
    }

    public void setSize(long size) {
        this.size = size;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;다음으로, 실제로 파일 업로드 정보를 저장할 Repository를 아래와 같이 작성해주자.&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;import com.semtax.application.entity.Post;
import com.semtax.application.entity.UploadFile;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;

import java.util.List;

public interface UploadFileInfoRepository extends JpaRepository&amp;lt;UploadFile, Long&amp;gt; {

    @Query(&quot;select f from UploadFile f where post_id = :id&quot;)
    List&amp;lt;UploadFile&amp;gt; findAllByPostId(Long id);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위의 repository 코드와 같은 경우, UploadFile 엔티티에 이미 post_id가 들어가 있고,&lt;/p&gt;
&lt;p&gt;post_id 이외에는 따로 post에서 필요한 내용이 없으므로 굳이 fetch 조인을 해서 가지고 오지 않아도 위와 같이 작성을 해주면 된다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이제, 실제로 파일을 읽고 쓰는 서비스를 작성해보도록 하자.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;아래와 같이 작성해주면 된다.&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;import com.semtax.application.config.FileStorageProperties;
import com.semtax.application.services.exceptions.FileStorageException;
import com.semtax.application.services.exceptions.MyFileNotFoundException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.net.MalformedURLException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;

@Service
public class FileStorageService {

    private final Path fileStorageLocation;


    @Autowired
    public FileStorageService(FileStorageProperties fileStorageProperties) {
        this.fileStorageLocation = Paths.get(fileStorageProperties.getUploadDir())
            .toAbsolutePath().normalize();

        try{
            Files.createDirectories(this.fileStorageLocation);
        }catch(Exception ex){
            throw new FileStorageException(&quot;Could not create the directory where the uploaded files will be stored.&quot;, ex);
        }
    }

    public String storeFile(MultipartFile file){
        String fileName = StringUtils.cleanPath(file.getOriginalFilename());

        try{
            if(fileName.contains(&quot;..&quot;)) {
                throw new FileStorageException(&quot;Sorry! Filename contains invalid path sequence &quot; + fileName);
            }

            Path targetLocation = this.fileStorageLocation.resolve(fileName);
            Files.copy(file.getInputStream(), targetLocation, StandardCopyOption.REPLACE_EXISTING);

            return fileName;
        }catch(IOException ex){
            throw new FileStorageException(&quot;Could not store file &quot; + fileName + &quot;. Please try again!&quot;, ex);
        }
    }

    public Resource loadFileAsResource(String fileName) {
        try{
            Path filePath = this.fileStorageLocation.resolve(fileName).normalize();
            Resource resource = new UrlResource(filePath.toUri());

            if(resource.exists()){
                return resource;
            }else{
                throw new MyFileNotFoundException(&quot;File not found&quot; + fileName);
            }
        }catch(MalformedURLException ex){
            throw new MyFileNotFoundException(&quot;File not found &quot; + fileName, ex);
        }
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;또, 파일 업로드/다운로드를 실패했을때를 대비해서, 예외 상황발생시 던져줄 예외 클래스도 아래와 같이 작성해주자.&lt;/p&gt;
&lt;pre class=&quot;scala&quot;&gt;&lt;code&gt;// 파일 못찾을때

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

@ResponseStatus(HttpStatus.NOT_FOUND)
public class MyFileNotFoundException extends RuntimeException{

    public MyFileNotFoundException(String message) {
        super(message);
    }

    public MyFileNotFoundException(String message, Throwable cause) {
        super(message, cause);
    }
}



// 나머지 경우

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

    public FileStorageException(String message, Throwable cause) {
        super(message, cause);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이제 실제 파일을 업로드/다운로드 할 컨트롤러 코드를 아래와 같이 작성 해주자.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;import com.semtax.application.dto.UploadFileResponseDTO;
import com.semtax.application.entity.Post;
import com.semtax.application.entity.UploadFile;
import com.semtax.application.repository.PostRepository;
import com.semtax.application.repository.UploadFileInfoRepository;
import com.semtax.application.services.FileStorageService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;


import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

@RestController
public class FileController {

    private static final Logger logger = LoggerFactory.getLogger(FileController.class);

    @Autowired
    private FileStorageService fileStorageService;

    @Autowired
    private UploadFileInfoRepository uploadFileInfoRepository;


    @CrossOrigin(origins = &quot;*&quot;, allowedHeaders = &quot;*&quot;)
    @PostMapping(&quot;/post/{id}/uploadFile&quot;)
    public UploadFileResponseDTO uploadFile(@PathVariable Long id, @RequestParam(&quot;file&quot;) MultipartFile file){
        String fileName = fileStorageService.storeFile(file);

        String fileDownloadUri = ServletUriComponentsBuilder.fromCurrentContextPath()
                .path(&quot;/post&quot;)
                .path(&quot;/downloadFile/&quot;)
                .path(fileName)
                .toUriString();

        UploadFile uploadFile = new UploadFile();
        uploadFile.setFileName(fileName);
        uploadFile.setFileDownloadUri(fileDownloadUri);
        uploadFile.setFileType(file.getContentType());
        uploadFile.setSize(file.getSize());

        Post post = new Post();
        post.setId(id);

        uploadFile.setPost(post);

        uploadFileInfoRepository.save(uploadFile);

        return new UploadFileResponseDTO(fileName, fileDownloadUri,
                file.getContentType(), file.getSize());
    }


    @CrossOrigin(origins = &quot;*&quot;, allowedHeaders = &quot;*&quot;)
    @PostMapping(&quot;/post/{id}/uploadMultipleFiles&quot;)
    public List&amp;lt;UploadFileResponseDTO&amp;gt; uploadMultipleFiles(@PathVariable Long id, @RequestParam(&quot;files&quot;) MultipartFile[] files) {
        return Arrays.asList(files)
                .stream()
                .map(file -&amp;gt; uploadFile(id, file))
                .collect(Collectors.toList());
    }

    @CrossOrigin(origins = &quot;*&quot;, allowedHeaders = &quot;*&quot;)
    @GetMapping(&quot;/post/{id}/files&quot;)
    public List&amp;lt;UploadFileResponseDTO&amp;gt; downloadFilesInfoInPost(@PathVariable Long id) {

        List&amp;lt;UploadFile&amp;gt; uploadFiles = uploadFileInfoRepository.findAllByPostId(id);

        return uploadFiles.stream().map(fileInfo -&amp;gt; new UploadFileResponseDTO(
            fileInfo.getFileName(),fileInfo.getFileDownloadUri(),
            fileInfo.getFileType(),fileInfo.getSize())
        ).collect(Collectors.toList());
    }


    @CrossOrigin(origins = &quot;*&quot;, allowedHeaders = &quot;*&quot;)
    @GetMapping(&quot;/post/downloadFile/{fileName:.+}&quot;)
    public ResponseEntity&amp;lt;Resource&amp;gt; downloadFile(@PathVariable String fileName, HttpServletRequest request) {
        Resource resource = fileStorageService.loadFileAsResource(fileName);

        String contentType = null;
        try{
            contentType = request.getServletContext().getMimeType(resource.getFile().getAbsolutePath());
        }catch(IOException ex) {
            logger.info(&quot;Could not determine file type.&quot;);
        }

        if (contentType == null){
            contentType = &quot;application/octet-stream&quot;;
        }

        return ResponseEntity.ok()
                .contentType(MediaType.parseMediaType(contentType))
                .header(HttpHeaders.CONTENT_DISPOSITION, &quot;attachment; filename=\&quot;&quot; + resource.getFilename() +&quot;\&quot;&quot;)
                .body(resource);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위에서 나온 컨트롤러는 대략적으로 &quot;파일 업로드&quot;,&quot;파일 다운로드 경로 얻어오기&quot;, &quot;파일 다운로드&quot; 로 나눌 수 있다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;먼저 uploadFile 함수와 같은 경우에는 사용자가 파일을 업로드 하면 해당 내용을 받아서 먼저 파일로 저장하고, 파일을 다운받을 링크를 생성해서 데이터베이스에 저장한 뒤에, 해당 링크와 파일이름, 파일 사이즈, 파일 타입을 전달해주는것을 알 수 있다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;uploadMultipleFiles 함수도, uploadFile과 비슷하지만, 스트림을 이용하여 여러개의 파일을 업로드한 경우도 처리 할 수 있다는 것을 알 수 있다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;downloadFilesInfoInPost 함수는, 해당 게시물에 속한 첨부파일들의 목록들을 얻을때 사용하는 함수이다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;UploadFileResponse 엔티티 에서, postID를 이용해서 해당 게시물에 속한 첨부파일들의 목록을 얻어낸 뒤, DTO로 매핑해서 반환하는 함수이다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;다음으로 파일 다운로드를 할 때, 위에서 언급했던 내용대로 header(HttpHeaders.CONTENT_DISPOSITION, &quot;attachment; filename=&quot;&quot; + resource.getFilename() +&quot;&quot;&quot;) 를 통해 파일을 다운 받을 수 있게 해주는 것을 알 수 있다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이제 그럼 테스트를 해보도록 하자.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2&gt;테스트&lt;/h2&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;먼저 포스트맨을 켜서 회원가입, 로그인을 하고 포스팅을 1개 생성해주자.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그리고 아래와 같이 테스트를 해주자.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;먼저 파일 업로드를 아래와 같이 해주자.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/k7l0n/btqDPMqStJj/AKuaskPA2nVEmjY7vH5zak/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/k7l0n/btqDPMqStJj/AKuaskPA2nVEmjY7vH5zak/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/k7l0n/btqDPMqStJj/AKuaskPA2nVEmjY7vH5zak/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fk7l0n%2FbtqDPMqStJj%2FAKuaskPA2nVEmjY7vH5zak%2Fimg.png&quot; width=&quot;100%&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;정상적으로 동작한 것을 알 수 있다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그리고, 반환 받은 링크를 그대로 붙여서 다운로드 해보자&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vYQTU/btqDQub5Lr4/qud0REK3rKcQkJQcLK2MzK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vYQTU/btqDQub5Lr4/qud0REK3rKcQkJQcLK2MzK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vYQTU/btqDQub5Lr4/qud0REK3rKcQkJQcLK2MzK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvYQTU%2FbtqDQub5Lr4%2Fqud0REK3rKcQkJQcLK2MzK%2Fimg.png&quot; width=&quot;100%&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;정상적으로 동작한 것을 알 수 있다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그리고 이번에는 여러 파일을 아래와 같이 업로드 해보자&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IULvt/btqDSzJLYwT/QHFHujpbOdMQWoBW3oMGd1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IULvt/btqDSzJLYwT/QHFHujpbOdMQWoBW3oMGd1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IULvt/btqDSzJLYwT/QHFHujpbOdMQWoBW3oMGd1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIULvt%2FbtqDSzJLYwT%2FQHFHujpbOdMQWoBW3oMGd1%2Fimg.png&quot; width=&quot;100%&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그리고 나서 정상적으로 업로드 됬는지 아래와 같이 테스트 해주자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nobG7/btqDQbX3zMH/ht6C4xZiY7FtExyEyfIwB0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nobG7/btqDQbX3zMH/ht6C4xZiY7FtExyEyfIwB0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nobG7/btqDQbX3zMH/ht6C4xZiY7FtExyEyfIwB0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnobG7%2FbtqDQbX3zMH%2Fht6C4xZiY7FtExyEyfIwB0%2Fimg.png&quot; width=&quot;100%&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;실제 링크에 나온대로 다운을 받을 수 있는지도 테스트를 해보자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vovCA/btqDPY5KuBG/9gzuZ4xPsU1wMVVIMUwFSK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vovCA/btqDPY5KuBG/9gzuZ4xPsU1wMVVIMUwFSK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vovCA/btqDPY5KuBG/9gzuZ4xPsU1wMVVIMUwFSK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvovCA%2FbtqDPY5KuBG%2F9gzuZ4xPsU1wMVVIMUwFSK%2Fimg.png&quot; width=&quot;100%&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;정상적으로 된 것을 알 수 있다.&lt;/p&gt;
&lt;h2&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2&gt;결론&lt;/h2&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이제, 대략적인 기능 구현은 다 끝났다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;(물론, 실제로 게시판 등을 만드는 경우에, 몇가지를 더 추가해야 하는게 있기는 하지만 그건 여러분들이 만들기를 바란다..)&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;다음 포스팅에서는, Swagger를 이용하여 자동으로 API 문서를 생성하는 것에 대해서 다루도록 하겠다.&lt;/p&gt;</description>
      <category>개발/Java</category>
      <category>form-encode</category>
      <category>HTTP Encoding</category>
      <category>HTTP 인코딩</category>
      <category>java</category>
      <category>Multipart</category>
      <category>Spring</category>
      <category>스프링</category>
      <category>스프링 부트</category>
      <category>웹</category>
      <category>파일 업로드</category>
      <author>semtax</author>
      <guid isPermaLink="true">https://semtax.tistory.com/79</guid>
      <comments>https://semtax.tistory.com/79#entry79comment</comments>
      <pubDate>Sat, 2 May 2020 16:07:45 +0900</pubDate>
    </item>
    <item>
      <title>스프링부트로 게시판 만들기 10 : 검색기능 추가</title>
      <link>https://semtax.tistory.com/78</link>
      <description>&lt;h2&gt;개요&lt;/h2&gt;
&lt;p&gt;​            &lt;/p&gt;
&lt;p&gt;이번 포스팅에서는, 지난 포스팅에 이어서 페이징 기능에 검색기능을 추가할 예정이다.&lt;/p&gt;
&lt;p&gt;​                       &lt;/p&gt;
&lt;p&gt;정확히는, 제목이나 본문에 검색어가 포함된 게시물을 전부 페이징으로 보여주는 기능을 구현 할 예정이다.&lt;/p&gt;
&lt;p&gt;​                                          &lt;/p&gt;
&lt;p&gt;​                                   &lt;/p&gt;
&lt;p&gt;​            &lt;/p&gt;
&lt;h2&gt;검색기능을 어떻게 구현할까?&lt;/h2&gt;
&lt;p&gt;​            &lt;/p&gt;
&lt;p&gt;그렇다면 검색기능을 어떤 식으로 구현 해야 할까?&lt;/p&gt;
&lt;p&gt;​            &lt;/p&gt;
&lt;p&gt;일단, 스프링 Data JPA나 이런 것들도 결국 SQL(정확히는 JPQL)문을 통해서 데이터를 가져오는것 이라는 생각을 할 수 있다.&lt;/p&gt;
&lt;p&gt;​            &lt;/p&gt;
&lt;p&gt;그렇다면 SQL문으로 위에서 언급한, &amp;quot;제목이나, 본문에&amp;quot; 검색어가 포함된 데이터들을 가져오면 되는 문제를 풀면 된다는 사실을 알 수 있다.&lt;/p&gt;
&lt;p&gt;​            &lt;/p&gt;
&lt;p&gt;데이터베이스 수업시간때 들은 SQL문들을 잘 떠올려 보자. 보통 검색하려는 문자열이 포함된 데이터를 검색할 때, &amp;quot;LIKE&amp;quot; 문을 써서 찾았다는 사실을 기억 할 수 있다.&lt;/p&gt;
&lt;p&gt;​            &lt;/p&gt;
&lt;p&gt;따라서, 아래 쿼리문과 비슷한 쿼리문을 작성하면 검색을 할 수 있다.&lt;/p&gt;
&lt;p&gt;​            &lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mysql&quot;&gt;SELECT * FROM Post WHERE title LIKE %&amp;lt;검색 키워드&amp;gt;% OR content LIKE %&amp;lt;검색 키워드&amp;gt;%&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​            &lt;/p&gt;
&lt;p&gt;그리고, 페이징을 위해서는 검색 결과의 데이터 개수도 알아야 하기 때문에, 검색결과 개수를 세주는 쿼리도 필요하다.&lt;/p&gt;
&lt;p&gt;따라서 아래와 유사한 쿼리문도 추가적으로 작성을 해주어야 한다.&lt;/p&gt;
&lt;p&gt;​            &lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mysql&quot;&gt;SELECT COUNT(*) FROM Post WHERE title LIKE %:keyword% OR content LIKE %:keyword%&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​            &lt;/p&gt;
&lt;p&gt;​            &lt;/p&gt;
&lt;p&gt;이제 위의 내용을 기반으로 실제로 구현을 해보도록 하자&lt;/p&gt;
&lt;p&gt;​         &lt;/p&gt;
&lt;h2&gt;구현&lt;/h2&gt;
&lt;p&gt;아래 코드와 같이 게시물을 가져오는 레포지토리에 아래 메소드를 추가한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;import com.semtax.application.dto.PagingDTO;
import com.semtax.application.entity.Post;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;

public interface PostRepository extends JpaRepository&amp;lt;Post,Long&amp;gt; {

    Page&amp;lt;Post&amp;gt; findAll(Pageable pageable);

    @Query(
        value = &amp;quot;SELECT p FROM Post p WHERE p.title LIKE %:title% OR p.content LIKE %:content%&amp;quot;,
        countQuery = &amp;quot;SELECT COUNT(p.id) FROM Post p WHERE p.title LIKE %:title% OR p.content LIKE %:content%&amp;quot;
    )
    Page&amp;lt;Post&amp;gt; findAllSearch(String title, String content, Pageable pageable);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​            &lt;/p&gt;
&lt;p&gt;위의 코드를 보면 알 수 있듯이, Spring Data JPA 에서는, 레포지토리의 함수에 실제로 실행될 쿼리를 매핑 할 수 있다.&lt;/p&gt;
&lt;p&gt;​            &lt;/p&gt;
&lt;p&gt;​            &lt;/p&gt;
&lt;p&gt;또한, 페이징과 관련된 쿼리와 같은 경우 페이징 할 총 게시물의 갯수와, 실제 값 이 2개를 전부 가져와야 하므로 저 2가지에 해당하는 쿼리를 모두 적어줘야 한다. &lt;/p&gt;
&lt;p&gt;​               &lt;/p&gt;
&lt;p&gt;​                                 &lt;/p&gt;
&lt;p&gt;그런 뒤, 아래처럼 컨트롤러에 실제 페이징 기능을 사용하는 코드를 추가 해준다.&lt;/p&gt;
&lt;p&gt;​                        &lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;package com.semtax.application.controller;


import com.semtax.application.dto.PagingDTO;
import com.semtax.application.entity.Post;
import com.semtax.application.repository.PostRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.web.PageableDefault;
import org.springframework.data.web.PageableHandlerMethodArgumentResolver;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.List;

@RestController
@RequiredArgsConstructor
public class PageController {

    @Autowired
    PostRepository postRepository;

    @CrossOrigin(origins = &amp;quot;*&amp;quot;, allowedHeaders = &amp;quot;*&amp;quot;)
    @GetMapping(&amp;quot;/post/page&amp;quot;)
    public Page&amp;lt;PagingDTO&amp;gt; paging(@PageableDefault(size=5, sort=&amp;quot;createdTime&amp;quot;) Pageable pageRequest) {

        Page&amp;lt;Post&amp;gt; postList = postRepository.findAll(pageRequest);

        Page&amp;lt;PagingDTO&amp;gt; pagingList = postList.map(
                post -&amp;gt; new PagingDTO(
                    post.getId(),post.getTitle(),
                    post.getCreatedBy(), post.getCreatedTime()
                ));

        return pagingList;
    }

    @CrossOrigin(origins = &amp;quot;*&amp;quot;, allowedHeaders = &amp;quot;*&amp;quot;)
    @GetMapping(&amp;quot;/post/page/search&amp;quot;)
    public Page&amp;lt;PagingDTO&amp;gt; searchPaging(
        @RequestParam String title,
        @RequestParam String content,
        @PageableDefault(size=5, sort=&amp;quot;createdTime&amp;quot;) Pageable pageRequest) {

        Page&amp;lt;Post&amp;gt; postList = postRepository.findAllSearch(title,content,pageRequest);

        Page&amp;lt;PagingDTO&amp;gt; pagingList = postList.map(
                post -&amp;gt; new PagingDTO(
                        post.getId(),post.getTitle(),
                        post.getCreatedBy(), post.getCreatedTime()
                ));

        return pagingList;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​                                             &lt;/p&gt;
&lt;p&gt;사실 검색 기능도 어떻게 보면 페이징 기능의 일부라고 가정을 했기 때문에, 해당 컨트롤러에 추가를 하였다.&lt;/p&gt;
&lt;p&gt;​                                                     &lt;/p&gt;
&lt;p&gt;​                                                   &lt;/p&gt;
&lt;h2&gt;테스트&lt;/h2&gt;
&lt;p&gt;​                         &lt;/p&gt;
&lt;p&gt;이제 테스트를 해보도록 하자.&lt;/p&gt;
&lt;p&gt;​                                    &lt;/p&gt;
&lt;p&gt;먼저 Postman을 켜서 회원가입 &amp;gt; 로그인 &amp;gt; 게시물 생성을 해준다.&lt;/p&gt;
&lt;p&gt;​                                &lt;/p&gt;
&lt;p&gt;이때, 검색기능을 테스트 해봐야 하기 때문에 게시물 생성시, &amp;quot;memo33&amp;quot; 이라는 제목을 가진 게시물 5개, &amp;quot;memo88&amp;quot; 이라는 제목을 가진 게시물 5개를 각각 생성 해준다.&lt;/p&gt;
&lt;p&gt;​                                       &lt;/p&gt;
&lt;p&gt;그리고 아래와 같이 페이징 기능을 테스트 해주도록 하자.&lt;/p&gt;
&lt;p&gt;​ &lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bi9DBe/btqDQbjbhnG/LRXmBOQAYtcvxKR9ZLBcH0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bi9DBe/btqDQbjbhnG/LRXmBOQAYtcvxKR9ZLBcH0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bi9DBe/btqDQbjbhnG/LRXmBOQAYtcvxKR9ZLBcH0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbi9DBe%2FbtqDQbjbhnG%2FLRXmBOQAYtcvxKR9ZLBcH0%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;​                                                      &lt;/p&gt;
&lt;p&gt;검색이 잘 되는것을 볼 수 있다.&lt;/p&gt;
&lt;p&gt;​                                                                                 &lt;/p&gt;
&lt;p&gt;​                                                                                         &lt;/p&gt;
&lt;h2&gt;결론&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;​&lt;br&gt;다음 포스팅에서는, 마지막(?) 기능으로 파일 업로드/다운로드 기능을 구현해보도록 하겠다.&lt;br&gt;​         &lt;/p&gt;</description>
      <category>개발/Java</category>
      <category>java</category>
      <category>JPA</category>
      <category>paging</category>
      <category>Spring</category>
      <category>Spring Boot</category>
      <category>검색</category>
      <category>스프링</category>
      <category>스프링 부트</category>
      <category>페이징</category>
      <category>페이징 쿼리</category>
      <author>semtax</author>
      <guid isPermaLink="true">https://semtax.tistory.com/78</guid>
      <comments>https://semtax.tistory.com/78#entry78comment</comments>
      <pubDate>Fri, 1 May 2020 22:41:24 +0900</pubDate>
    </item>
    <item>
      <title>스프링부트로 게시판 만들기 9 : 페이징 기능 구현</title>
      <link>https://semtax.tistory.com/77</link>
      <description>&lt;h2&gt;개요&lt;/h2&gt;
&lt;p&gt;​                  &lt;/p&gt;
&lt;p&gt;이번 포스팅에서는, 게시글 페이징을 구현하는 시간을 가지도록 하겠다.&lt;/p&gt;
&lt;p&gt;​                          &lt;/p&gt;
&lt;p&gt;​              &lt;/p&gt;
&lt;p&gt;​               &lt;/p&gt;
&lt;p&gt;##페이징을 왜 쓰는건가?&lt;/p&gt;
&lt;p&gt;​                          &lt;/p&gt;
&lt;p&gt;​              &lt;/p&gt;
&lt;p&gt;사실, 이 글을 읽는 사람들 중에서 이런 생각을 하는 사람도 있을것이다. &lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;굳이, 페이징 안쓰고 한꺼번에 보여주면 안되는건가? 왜 귀찮게 잘라서 보여주려는거지?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;물론 유저의 숫자도 적고, 데이터의 양이 10개, 100개 처럼 양이 적은 경우에는 굳이 페이징을 안하고 한꺼번에 로딩해서 보여줘도 된다.&lt;/p&gt;
&lt;p&gt;하지만, 유저수가 많아지고, 불러와야 하는 데이터의 양도 많아지고, 처리해야되는 로직이 복잡해진다면 이는 크나큰 성능 병목으로 이어지게 된다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;여담으로, 실제로 필자가 겪어본 서비스 중에, 동시 접속자 수가 3000&lt;del&gt;5000 정도 되는 서비스인데, 페이징 기능이 구현이 안되있어서 로딩에 거의 2&lt;/del&gt;5분 정도가 걸리는 케이스도 경험을 해보았다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;따라서, 저렇게 데이터가 많은 경우에는 어차피 사람은 인지적 한계로 인해 모든 데이터를 한번에 볼수 있는 능력이 없으므로, 쪼개서 데이터를 보여주는것이 성능을 덜 깎아먹는 선택지가 된다.&lt;/p&gt;
&lt;p&gt;그러면 스프링, 그리고 JPA 에서는 어떻게 페이징을 구현하는걸까?                    &lt;/p&gt;
&lt;p&gt;​              &lt;/p&gt;
&lt;p&gt;​               &lt;/p&gt;
&lt;h2&gt;구현 아이디어 떠올려 보기&lt;/h2&gt;
&lt;p&gt;사실 방법은 여러가지가 있긴 하다.&lt;/p&gt;
&lt;p&gt;가장 단순하게 떠올릴 수 있는 방법은 SQL을 이용하는 것인데, SQL로 구현을 한다면 대략 아래와 같이 구현하면 된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mysql&quot;&gt;&amp;quot;SELECT * FROM post LIMIT page_size*(page_num-1), page_size&amp;quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이때, page_size는 한번에 보여줄 개수이고, page_num은 페이지 번호이다.&lt;/p&gt;
&lt;p&gt;조금 더 효울적으로 짠다고 하면, 아래와 같이 짜면 된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mysql&quot;&gt;SELECT * FROM my_table WHERE id &amp;gt; page_size*(page_num-1)-1 LIMIT page_size;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;하지만, 페이징을 해야되는 기준이 복잡하거나 한다면(예를 들어 특정 목록 기준으로 정렬을 해서 보여줘야 된다거나), 저 쿼리를 짜는것이 매우 고역인 작업이 된다.&lt;/p&gt;
&lt;p&gt;다행히도 Spring Data JPA 에서는 페이징을 편하게 해주는 기능을 기본적으로 제공을 해주고 있다.&lt;/p&gt;
&lt;p&gt;그럼 페이징 기능을 실제로 구현을 하면서, 알아보도록 하자.&lt;/p&gt;
&lt;p&gt;##구현&lt;/p&gt;
&lt;p&gt;​         &lt;/p&gt;
&lt;p&gt;Spring Data JPA 에서는, 페이징을 위해서 기본적으로 Page 라는 객체와, Pageable 이라는 객체를 가지고 있다.&lt;/p&gt;
&lt;p&gt;​         &lt;/p&gt;
&lt;p&gt;간단하게 말해서, Pagable 이라는 객체는, 페이징 하는 방법을 기술해놓은 클래스(인터페이스) 라고 보면되고, Page 객체는 실제로 페이징으로 잘려진 객체들을 담고있는 객체라고 생각하면 된다. &lt;/p&gt;
&lt;p&gt;​         &lt;/p&gt;
&lt;p&gt;대략적으로 사용방법은 아래와 같다.&lt;/p&gt;
&lt;p&gt;​         &lt;/p&gt;
&lt;p&gt;먼저, JPA 레포지토리에 Paging 하는 함수를 선언 해준다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// JPA 레포지토리에 Paging 함수 선언
public interface PostRepository extends JpaRepository&amp;lt;Post,Long&amp;gt; {

    Page&amp;lt;Post&amp;gt; findAll(Pageable pageable);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​         &lt;/p&gt;
&lt;p&gt;그리고, 페이징을 위한 DTO를 실제로 작성을 해보도록 하자.&lt;/p&gt;
&lt;p&gt;(Entity를 그대로 넘길 수도 있지만, 이렇게 되면 만약 Entitiy가 변경되는 경우 API의 스펙이 바뀌어 버리므로 사용하는 사람 입장에서는 혼란이 오게 된다.)&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;package com.semtax.application.dto;


import lombok.Data;

import java.time.LocalDateTime;
import java.util.Objects;

@Data
public class PagingDTO {

    private Long id;
    private String title;
    private String createdBy;
    private LocalDateTime createdTime;

    public PagingDTO(Long id, String title, String createdBy, LocalDateTime createdTime) {
        this.id = id;
        this.title = title;
        this.createdBy = createdBy;
        this.createdTime = createdTime;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​         &lt;/p&gt;
&lt;p&gt;다음으로, 실제로 페이징을 사용해주는 코드를 작성 해준다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// 실제로 JPA로 선언된 레포지토리를 사용하는 곳

@RestController
@RequiredArgsConstructor
public class PageController {

    @Autowired
    PostRepository postRepository;

    @CrossOrigin(origins = &amp;quot;*&amp;quot;, allowedHeaders = &amp;quot;*&amp;quot;)
    @GetMapping(&amp;quot;/post/page&amp;quot;)
    public Page&amp;lt;PagingDTO&amp;gt; paging(@PageableDefault(size=5, sort=&amp;quot;createdTime&amp;quot;) Pageable pageRequest) {

        Page&amp;lt;Post&amp;gt; postList = postRepository.findAll(pageRequest);

        Page&amp;lt;PagingDTO&amp;gt; pagingList = postList.map(
                post -&amp;gt; new PagingDTO(
                    post.getId(),post.getTitle(),
                    post.getCreatedBy(), post.getCreatedTime()
                ));

        return pagingList;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​         &lt;/p&gt;
&lt;p&gt;사실, 위와 같이 컨트롤러에 직접 작성하는것보다, 서비스 객체를 따로 만들어서 위에서 선언한 컨트롤러 부분을 서비스 객체에 전부 옮겨주는게 더 좋다.&lt;/p&gt;
&lt;p&gt;마지막으로, 인터셉터에서 페이징 URL 부분을 제외 해주도록 하자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Configuration
public class WebserviceConfig implements WebMvcConfigurer {

    @Autowired
    LoginInterceptor loginInterceptor;

    @Autowired
    PostAuthInterceptor postAuthInterceptor;

    @Autowired
    CommentAuthInterceptor commentAuthInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginInterceptor)
                .excludePathPatterns(&amp;quot;/post/page&amp;quot;)
                .addPathPatterns(&amp;quot;/post/**&amp;quot;)
                .addPathPatterns(&amp;quot;/comment/**&amp;quot;);

        registry.addInterceptor(postAuthInterceptor)
                .excludePathPatterns(&amp;quot;/post/page&amp;quot;)
                .excludePathPatterns(&amp;quot;/post/**/comment/**&amp;quot;)
                .addPathPatterns(&amp;quot;/post/**&amp;quot;);

        registry.addInterceptor(commentAuthInterceptor)
                .addPathPatterns(&amp;quot;/post/**/comment/**&amp;quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​                          &lt;/p&gt;
&lt;p&gt;​              &lt;/p&gt;
&lt;p&gt;​               &lt;/p&gt;
&lt;h2&gt;테스트&lt;/h2&gt;
&lt;p&gt;Postman을 켜서 아래와 같이 실행을 해보자.&lt;/p&gt;
&lt;p&gt;먼저 저번 포스팅과 똑같이 회원가입, 로그인을 해주고 포스팅을 대충 8~10개 정도 생성해주자.&lt;/p&gt;
&lt;p&gt;다음으로 페이징 API에 아래와 같이 실제로 요청을 해주자&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oyAVJ/btqDQbwFcHt/3lQMrfNMKE5Rzw514IXSh1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oyAVJ/btqDQbwFcHt/3lQMrfNMKE5Rzw514IXSh1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oyAVJ/btqDQbwFcHt/3lQMrfNMKE5Rzw514IXSh1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoyAVJ%2FbtqDQbwFcHt%2F3lQMrfNMKE5Rzw514IXSh1%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;정상적으로 페이징이 되는것을 확인 할 수 있다.&lt;/p&gt;
&lt;h2&gt;출처&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;[&lt;a href=&quot;https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%EB%8D%B0%EC%9D%B4%ED%84%B0-JPA-%EC%8B%A4%EC%A0%84/dashboard%5D&quot;&gt;https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%EB%8D%B0%EC%9D%B4%ED%84%B0-JPA-%EC%8B%A4%EC%A0%84/dashboard]&lt;/a&gt;(&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>개발/Java</category>
      <category>java</category>
      <category>JPA</category>
      <category>paging</category>
      <category>Spring</category>
      <category>Spring Boot</category>
      <category>Spring Data JPA</category>
      <category>스프링</category>
      <category>스프링 부트</category>
      <category>자바</category>
      <category>페이징</category>
      <author>semtax</author>
      <guid isPermaLink="true">https://semtax.tistory.com/77</guid>
      <comments>https://semtax.tistory.com/77#entry77comment</comments>
      <pubDate>Fri, 1 May 2020 19:13:58 +0900</pubDate>
    </item>
    <item>
      <title>스프링부트로 게시판 만들기 8 : 권한 체크</title>
      <link>https://semtax.tistory.com/76</link>
      <description>&lt;h2&gt;개요&lt;/h2&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;이번 포스팅에서는, 지난 시간에 구현한 내용들을 기반으로 글을 쓴 사용자/댓글을 단 사용자만 글을 수정, 삭제 할 수 있게 하는 기능을 구현해보도록 하겠다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;h2&gt;Authorization?&lt;/h2&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;먼저 기능 구현에 앞서서, Authorization이 무엇인지 알아보도록 해보자.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;역시나, 위키피디아의 정의를 찾아보면 아래와 같다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;b&gt;Authorization&lt;/b&gt; is the function of specifying access rights/privileges to resources, which is related to &lt;a href=&quot;https://en.wikipedia.org/wiki/Information_security&quot;&gt;information security&lt;/a&gt; and &lt;a href=&quot;https://en.wikipedia.org/wiki/Computer_security&quot;&gt;computer security&lt;/a&gt; in general and to &lt;a href=&quot;https://en.wikipedia.org/wiki/Access_control&quot;&gt;access control&lt;/a&gt; in particular.&lt;a href=&quot;https://en.wikipedia.org/wiki/Authorization#cite_note-1&quot;&gt;[1]&lt;/a&gt; More formally, &quot;to authorize&quot; is to define an access policy&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;요약하면, 허용된 사용자에게만 자원에 접근/수정할 권한을 준다는 의미이다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;즉, 우리가 하려는 일이 결국 글을 쓴 사용자/댓글을 단 사용자만 글을 수정, 삭제를 할 수 있게 하는 것이므로 본질적으로는, Authorization 기능을 구현하는 것과 동일 하다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;그럼 이제 실제로 구현을 해보도록 하자.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;h2&gt;구현&lt;/h2&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;일단, 실제 코드를 작성하기 전에, 코드 작성할 내용을 간단하게 정리해보자.&lt;/p&gt;
&lt;p&gt;대략적으로 아래와 같은 과정을 코드로 작성하면 된다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;로그인한 사용자의 세션에서 ID 값을 꺼내옴&lt;/li&gt;
&lt;li&gt;세션에서 꺼낸 ID와 글쓴이를 비교
&lt;ol&gt;
&lt;li&gt;같은 경우 접근을 허용&lt;/li&gt;
&lt;li&gt;같지 않은 경우, 권한이 없다는 메시지를 보내고 접근을 거부&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;그럼 이제 실제로 코드를 작성해보자.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;먼저 게시글에 대한 Authorization을 수행하는 인터셉터를 만들어 준다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;@Component
public class PostAuthInterceptor implements HandlerInterceptor {

    @Autowired
    PostRepository postRepository;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        String httpMethod = request.getMethod();

        if(httpMethod.equals(&quot;POST&quot;) || httpMethod.equals(&quot;DELETE&quot;)) {
            String sessionItem = (String)request.getSession().getAttribute(Sessions.SESSION_ID);
            Map&amp;lt;?, ?&amp;gt; pathVariables = (Map&amp;lt;?, ?&amp;gt;) request.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
            Long id = Long.parseLong((String)pathVariables.get(&quot;id&quot;));

            Post post = postRepository.findById(id).get();
            String postWriter = post.getCreatedBy();

            if(!postWriter.equals(sessionItem)){
                response.getOutputStream().println(&quot;NOT AUTHORIZE!!&quot;);
                return false;
            }
        }

        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;다음으로, 댓글에 대한 Authorization을 수행하는 인터셉터를 만들어 준다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;@Component
public class CommentAuthInterceptor implements HandlerInterceptor {

    @Autowired
    CommentRepository commentRepository;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        String httpMethod = request.getMethod();

        if(httpMethod.equals(&quot;POST&quot;) || httpMethod.equals(&quot;DELETE&quot;)) {
            String sessionItem = (String)request.getSession().getAttribute(Sessions.SESSION_ID);
            Map&amp;lt;?, ?&amp;gt; pathVariables = (Map&amp;lt;?, ?&amp;gt;) request.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
            Long id = Long.parseLong((String)pathVariables.get(&quot;id&quot;));

            Comment comment = commentRepository.findById(id).get();
            String commentWriter = comment.getCreatedBy();

            if(!commentWriter.equals(sessionItem)){
                response.getOutputStream().println(&quot;NOT AUTHORIZE!!&quot;);
                return false;
            }
        }

        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;그런 뒤, 아래 코드와 같이 위에서 만든 인터셉터들을 등록 해준다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Configuration
public class WebserviceConfig implements WebMvcConfigurer {

    @Autowired
    LoginInterceptor loginInterceptor;

    @Autowired
    PostAuthInterceptor postAuthInterceptor;

    @Autowired
    CommentAuthInterceptor commentAuthInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginInterceptor)
                .excludePathPatterns(&quot;/post/page&quot;)
                .addPathPatterns(&quot;/post/**&quot;)
                .addPathPatterns(&quot;/comment/**&quot;);

        registry.addInterceptor(postAuthInterceptor)
                .excludePathPatterns(&quot;/post/page&quot;)
                .excludePathPatterns(&quot;/post/**/comment/**&quot;)
                .addPathPatterns(&quot;/post/**&quot;);

        registry.addInterceptor(commentAuthInterceptor)
                .addPathPatterns(&quot;/post/**/comment/**&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;이때, 게시글 생성은 로그인한 모든 유저가 할 수 있어야 하므로, 저런 식으로 인터셉터에서 예외처리를 해준다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;그럼 이제 테스트를 해보도록 하자.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;h2&gt;테스트&lt;/h2&gt;
&lt;p&gt;​&lt;br /&gt;​&lt;/p&gt;
&lt;p&gt;포스트맨(Postman)을 켜서 먼저 회원가입으로 계정을 생성한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1xriB/btqDOIIUkcN/yEz9FbqOtAWJRnYEKjoZd1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1xriB/btqDOIIUkcN/yEz9FbqOtAWJRnYEKjoZd1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1xriB/btqDOIIUkcN/yEz9FbqOtAWJRnYEKjoZd1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1xriB%2FbtqDOIIUkcN%2FyEz9FbqOtAWJRnYEKjoZd1%2Fimg.png&quot; width=&quot;100%&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;그런 뒤, 로그인 요청을 날려서 세션값을 받아온 뒤, 게시글을 각각 생성해보자.&lt;br /&gt;​&lt;br /&gt;​&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d4Z1TB/btqDQb4ssuP/dJh5BBKnwKwdlG780YnTf0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d4Z1TB/btqDQb4ssuP/dJh5BBKnwKwdlG780YnTf0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d4Z1TB/btqDQb4ssuP/dJh5BBKnwKwdlG780YnTf0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd4Z1TB%2FbtqDQb4ssuP%2FdJh5BBKnwKwdlG780YnTf0%2Fimg.png&quot; width=&quot;100%&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;​&lt;br /&gt;​&lt;br /&gt;그리고 나서, 아래와 같이 게시글 을 수정 하는 API에 요청을 해보자.&lt;br /&gt;​&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GH5Df/btqDQ776DPK/mu5rfQmuR7qdSTVp5Dflb0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GH5Df/btqDQ776DPK/mu5rfQmuR7qdSTVp5Dflb0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GH5Df/btqDQ776DPK/mu5rfQmuR7qdSTVp5Dflb0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGH5Df%2FbtqDQ776DPK%2Fmu5rfQmuR7qdSTVp5Dflb0%2Fimg.png&quot; width=&quot;100%&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;br /&gt;​&lt;br /&gt;​&lt;/p&gt;
&lt;p&gt;정상적으로 되는 것을 확인 할 수 있다.&lt;/p&gt;
&lt;p&gt;​&lt;br /&gt;​&lt;br /&gt;​&lt;br /&gt;다음으로, 다른 계정을 생성하고, 로그인 요청을 날려서 세션값을 받아온 뒤 첫번째로 만들었던 게시글에 수정 요청을 날려보자.&lt;br /&gt;​&lt;br /&gt;​&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ciwGwa/btqDQT3iwjy/xJhujCJh9yH2THR4f33ty0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ciwGwa/btqDQT3iwjy/xJhujCJh9yH2THR4f33ty0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ciwGwa/btqDQT3iwjy/xJhujCJh9yH2THR4f33ty0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FciwGwa%2FbtqDQT3iwjy%2FxJhujCJh9yH2THR4f33ty0%2Fimg.png&quot; width=&quot;100%&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;​&lt;br /&gt;접근이 거부되는것을 확인 할 수 있다.&lt;br /&gt;​&lt;br /&gt;​&lt;br /&gt;​&lt;/p&gt;
&lt;h2&gt;결론&lt;/h2&gt;
&lt;p&gt;이번 시간에는 접근 제어(권한 체크) 기능을 만들어 보았다.&lt;/p&gt;
&lt;p&gt;해당 포스팅을 조금만 응용하면, 관리자 권한도 구현이 가능하다.&lt;/p&gt;
&lt;p&gt;다음 시간에는, 페이징 기능을 구현해보도록 하겠다.&lt;/p&gt;</description>
      <category>개발/Java</category>
      <category>java</category>
      <category>JPA</category>
      <category>Spring Boot</category>
      <category>spring mvc</category>
      <category>권한 체크</category>
      <category>백엔드 개발</category>
      <category>세션</category>
      <category>스프링 mvc</category>
      <category>스프링 부트</category>
      <category>자바</category>
      <author>semtax</author>
      <guid isPermaLink="true">https://semtax.tistory.com/76</guid>
      <comments>https://semtax.tistory.com/76#entry76comment</comments>
      <pubDate>Fri, 1 May 2020 17:31:23 +0900</pubDate>
    </item>
    <item>
      <title>스프링부트로 게시판 만들기 7 : 데이터 Auditing</title>
      <link>https://semtax.tistory.com/75</link>
      <description>&lt;h2&gt;개요&lt;/h2&gt;
&lt;p&gt;​                  &lt;/p&gt;
&lt;p&gt;이번 포스팅에서는 Data Auditing(데이터 이력 관리) 에 대해서 다루어 보도록 하겠다.&lt;/p&gt;
&lt;p&gt;​                  &lt;/p&gt;
&lt;p&gt;​                  &lt;/p&gt;
&lt;h2&gt;Data Auditing?&lt;/h2&gt;
&lt;p&gt;​                  &lt;/p&gt;
&lt;p&gt;​             &lt;/p&gt;
&lt;p&gt;상황을 1가지 가정해보자, 만약 당신이 게시판 관리자고, 관리자 페이지를 통해서 게시글 댓글목록을 관리 하고 있다고 가정을 해보자.&lt;br&gt;​                  &lt;/p&gt;
&lt;p&gt;​             &lt;/p&gt;
&lt;p&gt;그렇다면 언제 이 글이나 댓글을 작성했는지 라던가, 누가 작성했는지를 알아야, 나중에 차단을 시키던지 다른걸 하던지 관리를 할 수가 있다.&lt;/p&gt;
&lt;p&gt;​                  &lt;/p&gt;
&lt;p&gt;​             &lt;/p&gt;
&lt;p&gt;또한, 사용자에 따라서 글 쓰는 기능을 제한 하는 기능도 위의 Data Auditing이 있어야 가능하다.&lt;br&gt;​                  &lt;/p&gt;
&lt;p&gt;​             &lt;/p&gt;
&lt;p&gt;바로 위와 같은 이유들 때문에, 이러한 데이터 이력관리가 중요하다는 것을 알 수 있다.&lt;br&gt;​                  &lt;/p&gt;
&lt;p&gt;​             &lt;/p&gt;
&lt;p&gt;하지만 이러한 데이터 이력관리를 처음부터 만들기에는 뭔가 귀찮다. 뭔가 누가 만들어 놓은게 있을것같다.&lt;br&gt;​                  &lt;/p&gt;
&lt;p&gt;​             &lt;/p&gt;
&lt;p&gt;다행히도, JPA에서는 이러한 기능을 이미 제공해주고 있다.&lt;br&gt;​                  &lt;/p&gt;
&lt;p&gt;​             &lt;/p&gt;
&lt;p&gt;이제, 어떻게 사용하는지 알아보도록 하겠다.&lt;br&gt;​&lt;br&gt;​&lt;br&gt;​&lt;br&gt;​&lt;br&gt;​                  &lt;/p&gt;
&lt;h2&gt;구현&lt;/h2&gt;
&lt;p&gt;​                  &lt;/p&gt;
&lt;p&gt;​                              &lt;/p&gt;
&lt;p&gt;​&lt;br&gt;먼저 Auditing 정보를 저장하는 Entitiy를 생성해주자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
public class BaseEntity {

    @CreatedDate
    @Column(updatable = false)
    private LocalDateTime createdTime;

    @LastModifiedDate
    private LocalDateTime lastModifiedDate;

    @CreatedBy
    @Column(updatable = false)
    private String createdBy;

    @LastModifiedBy
    private String lastModifiedBy;

    public LocalDateTime getCreatedTime() {
        return createdTime;
    }

    public LocalDateTime getLastModifiedDate() {
        return lastModifiedDate;
    }

    public String getCreatedBy() {
        return createdBy;
    }

    public String getLastModifiedBy() {
        return lastModifiedBy;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​                  &lt;/p&gt;
&lt;p&gt;​             &lt;/p&gt;
&lt;p&gt;위에서, @EntityListeners를 통해서 엔티티의 영속성 컨텍스트 라이프 사이클에 따른 Auditing 정보를 세팅하는 콜백함수를 등록 한다.&lt;br&gt;​                  &lt;/p&gt;
&lt;p&gt;​             &lt;/p&gt;
&lt;p&gt;그리고, @CreateBy, @ModifiedBy, @CreateByDate, @ModifiedByDate 어노테이션을 통해서 Auditing 정보를 추가 해주는걸 알 수 있다.&lt;br&gt;​                  &lt;/p&gt;
&lt;p&gt;​             &lt;/p&gt;
&lt;p&gt;이때, @CreateBy, @ModifiedBy를 붙여준 필드 같은 경우에는, 스프링에서 &amp;quot;AuditorAware&lt;String&gt;&amp;quot; 객체를 구현해주고, 빈으로 등록해주는 방식으로 필드에 값을 주입하게 된다.&lt;/p&gt;
&lt;p&gt;​                  &lt;/p&gt;
&lt;p&gt;​             &lt;/p&gt;
&lt;p&gt;마지막으로, @MappedSuperclass 를 이용해서 다른 Entity에서 선언한 Auditing Entity를 상속을 통해 포함할 수 있게 해준다.&lt;/p&gt;
&lt;p&gt;​                  &lt;/p&gt;
&lt;p&gt;​             &lt;/p&gt;
&lt;p&gt;이렇게 하면 엔티티의 영속성 컨텍스트 라이프 사이클에 맞춰서 Auditing 정보가 등록되게 된다.&lt;/p&gt;
&lt;p&gt;​                  &lt;/p&gt;
&lt;p&gt;​             &lt;/p&gt;
&lt;p&gt;이제 게시글 Entitiy에 아까전에 만들어준 객체를 아래와 같이 상속받으면 된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Entity
public class Post extends BaseEntity{

    @Id @GeneratedValue
    @Column(name = &amp;quot;post_id&amp;quot;)
    private Long Id;

    private String title;
    private String content;


    public Long getId() {
        return Id;
    }

    public void setId(Long id) {
        Id = id;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Post post = (Post) o;
        return Objects.equals(Id, post.Id) &amp;amp;&amp;amp;
                Objects.equals(title, post.title) &amp;amp;&amp;amp;
                Objects.equals(content, post.content);
    }

    @Override
    public int hashCode() {
        return Objects.hash(Id, title, content);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​                  &lt;/p&gt;
&lt;p&gt;​             &lt;/p&gt;
&lt;p&gt;​&lt;br&gt;댓글 Entity도 아래와 같이 추가해준다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Entity
public class Comment extends BaseEntity{

    @Id @GeneratedValue
    @Column(name = &amp;quot;comment_id&amp;quot;)
    private Long Id;

    private String title;
    private String content;

    @ManyToOne
    @JoinColumn(name = &amp;quot;post_id&amp;quot;)
    private Post post;

    public Long getId() {
        return Id;
    }

    public void setId(Long id) {
        Id = id;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    public Post getPost() {
        return post;
    }

    public void setPost(Post post) {
        this.post = post;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Comment comment = (Comment) o;
        return Objects.equals(Id, comment.Id) &amp;amp;&amp;amp;
                Objects.equals(title, comment.title) &amp;amp;&amp;amp;
                Objects.equals(content, comment.content);
    }

    @Override
    public int hashCode() {
        return Objects.hash(Id, title, content);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​                  &lt;/p&gt;
&lt;p&gt;​             &lt;/p&gt;
&lt;p&gt;​                  &lt;/p&gt;
&lt;p&gt;​             &lt;/p&gt;
&lt;p&gt;다음으로, 아래 코드처럼 어노테이션을 이용해서 JPA Auditing 기능을 활성화 하겠다고 설정한다.&lt;br&gt;​                  &lt;/p&gt;
&lt;p&gt;​             &lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@EnableJpaAuditing
@SpringBootApplication
public class SemtaxApplication {
    public static void main(String[] args) {
        SpringApplication.run(SemtaxApplication.class, args);
    }

    @Bean
    public AuditorAware&amp;lt;String&amp;gt; auditorProvider() {
        return () -&amp;gt; {
            ServletRequestAttributes attr
                = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();

            String currentUser = (String)attr.getRequest().getSession().getAttribute(Sessions.SESSION_ID);

            if(currentUser != null)
                return Optional.of(currentUser);
            else
                return Optional.of(&amp;quot;Anonymous&amp;quot;);
        };
    };
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​                  &lt;/p&gt;
&lt;p&gt;위 코드에서, RequestContextHolder 라는 클래스를 사용했는데, 이를 이용해서 Servlet 요청 객체를 가져올수 있다. 이걸 이용해서 서블릿 요청 내에 있는 session 값을 가져 올 수 있다.&lt;/p&gt;
&lt;p&gt;​                  &lt;/p&gt;
&lt;p&gt;(스프링 시큐리티를 쓰는 경우에는 SecurityContext를 통해 세션 정보들을 가져와서 넣어주면 된다.)&lt;/p&gt;
&lt;p&gt;​                  &lt;/p&gt;
&lt;p&gt;​                  &lt;/p&gt;
&lt;h2&gt;테스트&lt;/h2&gt;
&lt;p&gt;​                  &lt;/p&gt;
&lt;p&gt;Postman을 켜서 아래와 같이 테스트를 해보자&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uzJEN/btqDO2NZAFx/4XzSg9QrrUDQfLR9Iwipv1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uzJEN/btqDO2NZAFx/4XzSg9QrrUDQfLR9Iwipv1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uzJEN/btqDO2NZAFx/4XzSg9QrrUDQfLR9Iwipv1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuzJEN%2FbtqDO2NZAFx%2F4XzSg9QrrUDQfLR9Iwipv1%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;추가한 Auditing 정보들(글쓴이, 수정한이, 수정날짜, 생성날짜)이 정상적으로 표시되는것을 알 수 있다.&lt;/p&gt;</description>
      <category>개발/Java</category>
      <category>Auditing</category>
      <category>JPA</category>
      <category>ORM</category>
      <category>Spring</category>
      <category>Spring Boot</category>
      <category>데이터베이스</category>
      <category>스프링</category>
      <category>스프링 부트</category>
      <category>웹 개발</category>
      <category>하이버네이트</category>
      <author>semtax</author>
      <guid isPermaLink="true">https://semtax.tistory.com/75</guid>
      <comments>https://semtax.tistory.com/75#entry75comment</comments>
      <pubDate>Fri, 1 May 2020 17:23:07 +0900</pubDate>
    </item>
    <item>
      <title>스프링부트로 게시판 만들기 6 : 로그인 기능 추가</title>
      <link>https://semtax.tistory.com/74</link>
      <description>&lt;h2&gt;개요&lt;/h2&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;이번 포스팅에서는 지난 포스팅에 구현 한 회원가입 기능에 이어서, 로그인 기능을 구현하도록 하겠다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;추가적으로, 로그인 기능을 구현하면서 인증(Authentication)의 개념과 세션, 그리고 인터셉터에 대해서도 알아보도록 하겠다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;h2&gt;인증(Authentication) 이란?&lt;/h2&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;먼저, 인증이란게 어떠한 개념인지 알아보도록 하자.&lt;/p&gt;
&lt;p&gt;위키피디아에 인증에 대한 개념을 검색하면 아래와 같은 결과가 나온다&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;b&gt;Authentication&lt;/b&gt; is the act of &lt;a href=&quot;https://en.wikipedia.org/wiki/Proof_(truth)&quot;&gt;proving&lt;/a&gt; an &lt;a href=&quot;https://en.wikipedia.org/wiki/Logical_assertion&quot;&gt;assertion&lt;/a&gt;, such as the &lt;a href=&quot;https://en.wikipedia.org/wiki/Digital_identity&quot;&gt;identity&lt;/a&gt; of a computer system user. In contrast with &lt;a href=&quot;https://en.wikipedia.org/wiki/Identity_(philosophy)&quot;&gt;identification&lt;/a&gt;, the act of indicating a person or thing's identity, authentication is the process of verifying that identity. It might involve validating personal &lt;a href=&quot;https://en.wikipedia.org/wiki/Identity_document&quot;&gt;identity documents&lt;/a&gt;, verifying the authenticity of a &lt;a href=&quot;https://en.wikipedia.org/wiki/Website&quot;&gt;website&lt;/a&gt; with a &lt;a href=&quot;https://en.wikipedia.org/wiki/Public_key_certificate&quot;&gt;digital certificate&lt;/a&gt;,&lt;a href=&quot;https://en.wikipedia.org/wiki/Authentication#cite_note-Turner-DigitalAuthentication-Basics-1&quot;&gt;[1]&lt;/a&gt; determining the age of an artifact by &lt;a href=&quot;https://en.wikipedia.org/wiki/Carbon_dating&quot;&gt;carbon dating&lt;/a&gt;, or ensuring that a product or document is not &lt;a href=&quot;https://en.wikipedia.org/wiki/Counterfeit&quot;&gt;counterfeit&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;즉, 요약하면 우리 서비스에 접근하는 사람이 우리가 허가한 사용자 목록에 있는지, 그리고 우리가 허가한 사용자가 맞는지를 검증하는 과정이라고 보면 된다.&lt;/p&gt;
&lt;p&gt;(한마디로, 뷔페집에서 뷔페 입장표 검사해주는 입구하고 직원을 만든다고 생각하면 된다.)&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;대충 아래와 같은 알고리즘을 따르면 된다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;사용자에게 아이디, 비밀번호를 전달 받는다.&lt;/li&gt;
&lt;li&gt;저번 시간에 만든 회원 가입 기능을 이용해서 가입된 정보를 가져온다.&lt;/li&gt;
&lt;li&gt;가입된 정보와 넘겨받은 정보를 비교한다.
&lt;ol&gt;
&lt;li&gt;이때, 패스워드를 해싱해서 비교해야 한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;두개 다 일치하는 경우 로그인 성공, 그렇지 않은 경우 로그인 실패로 처리한다.&lt;/li&gt;
&lt;li&gt;로그인에 성공한 경우 &lt;b&gt;입장표&lt;/b&gt; 에 해당하는것을 사용자에게 넘겨준다&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;그렇다면, 서버에서는 이러한 &lt;b&gt;입장표&lt;/b&gt; 에 해당하는 걸 어떠한 방식으로 구현 할까?&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;이제 그 방법에 대해서 알아보도록 하자.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;h2&gt;세션(Session)?&lt;/h2&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;다시 원론으로 돌아가서, 결국 우리가 만드는 웹 서버도 소프트웨어이고, 소프트웨어는 사실상 데이터(혹은 객체)의 집합체이기 때문에 저러한 입장표도 앞 뒤 다 자르고 이야기 하면 결국 &lt;b&gt;특수한 데이터&lt;/b&gt;이다. 즉, 저러한 입장표에 해당하는 객체와, 저러한 입장표를 저장/관리해주는 객체를 만들어주면 된다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;바로 저 입장표와, 저 입장표를 저장/관리 해주는 객체들을 각각 세션(Session), 세션 저장소(Session Storage) 라고 한다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;다행히도, 스프링 MVC(정확히는 서블릿)에서는 저러한 세션과 관련된 기능이 있기 때문에 가져다 쓰면 된다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;보통 아래와 같은 방식으로 사용한다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;@Component
public class LoginInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        HttpSession httpSession = request.getSession();
        String sessionItem = (String)httpSession.getAttribute(Sessions.SESSION_ID);

        if(sessionItem == null){
            response.getOutputStream().println(&quot;LOGIN REQUIRED!&quot;);
            return false;
        }

        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;그런데, 컨트롤러가 1~2개도 아니고 저런 중복되는 로직을 일일히 손으로 각 컨트롤러마다 전부 적어주면 뭔가 불편하다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;그리고 해당 예제와 같은 경우 프로그램의 규모가 작아서 상관이 없지만, 나중에 프로그램 규모가 커지고, 저 검증로직을 바꿔야 하는 경우가 생길수도 있다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;이때, 일일히 모든 컨트롤러들을 뒤져가면서 전부 고쳐줘야 한다. 정말로 불편하지 않을수가 없다. 뭔가 다른 방법이 필요하다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;사실 위와 같은 문제는 프록시 패턴과 같은 디자인 패턴으로도 풀 수는 있다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;하지만, 스프링 MVC(서블릿) 에서 그것보다 더 좋은 방법을 제공하고 있다. 그 방법을 이제부터 알아보도록 하자.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;h2&gt;인터셉터&lt;/h2&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;스프링 MVC(정확히는 서블릿)에서는, HTTP Request 패킷이 실제 HTTP 패킷을 처리하는 로직(컨트롤러 또는 핸들러)에 접근하기 전에 먼저 앞에서 가로채서 전 처리를 할 수 있는 기능을 지원하고 있다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;해당 기능을, 패킷을 먼저 가로챈다고 해서 인터셉터라고 부른다. 대략적으로 아래와 같이 생겼다.&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;@Component
public class LoginInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        HttpSession httpSession = request.getSession();
        String sessionItem = (String)httpSession.getAttribute(Sessions.SESSION_ID);

        if(sessionItem == null){
            response.getOutputStream().println(&quot;LOGIN REQUIRED!&quot;);
            return false;
        }

        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;그리고, 위에 있는 인터셉터를 설정을 아래와 같이 Configure 클래스에서 등록하는 방식으로 사용한다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Configuration
public class WebserviceConfig implements WebMvcConfigurer {

    @Autowired
    LoginInterceptor loginInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginInterceptor)
                .excludePathPatterns(&quot;/post/page&quot;)
                .addPathPatterns(&quot;/post/**&quot;)
                .addPathPatterns(&quot;/comment/**&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이제 대략적인 내용들을 설명했으니 구현을 해보도록 하자.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;h2&gt;구현&lt;/h2&gt;
&lt;p&gt;먼저 세션 관련된 정보를 편하게 관리하기 위한 래퍼 클래스를 선언해주자.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;package com.semtax.application.util;

public class Sessions {

    public static final String SESSION_ID = &quot;gallerySessionID&quot;;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;위의, SESSION_ID 를 통해 세션에서 값을 가지고 오면 된다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;그런 뒤, 인터셉터를 아래와 같이 만들어 준다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;package com.semtax.application.interceptor;

import com.semtax.application.util.Sessions;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.PrintWriter;


@Component
public class LoginInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        HttpSession httpSession = request.getSession();
        String sessionItem = (String)httpSession.getAttribute(Sessions.SESSION_ID);

        if(sessionItem == null){
            response.getOutputStream().println(&quot;LOGIN REQUIRED!&quot;);
            return false;
        }

        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;그런 뒤 아래와 같이 인터셉터를 등록해준다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;package com.semtax.application.config;

import com.semtax.application.interceptor.CommentAuthInterceptor;
import com.semtax.application.interceptor.LoginInterceptor;
import com.semtax.application.interceptor.PostAuthInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebserviceConfig implements WebMvcConfigurer {

    @Autowired
    LoginInterceptor loginInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginInterceptor)
                .addPathPatterns(&quot;/post/**&quot;)
                .addPathPatterns(&quot;/comment/**&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;h2&gt;테스트&lt;/h2&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;Postman을 켜서 아래와 같이 회원가입을 하고 로그인을 해보자.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;로그인 성공.png&quot; data-origin-width=&quot;2342&quot; data-origin-height=&quot;1374&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dJGHhd/btqDPMD8vqD/NZNirNoRMkvXy0GLVIKn2k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dJGHhd/btqDPMD8vqD/NZNirNoRMkvXy0GLVIKn2k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dJGHhd/btqDPMD8vqD/NZNirNoRMkvXy0GLVIKn2k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdJGHhd%2FbtqDPMD8vqD%2FNZNirNoRMkvXy0GLVIKn2k%2Fimg.png&quot; data-filename=&quot;로그인 성공.png&quot; data-origin-width=&quot;2342&quot; data-origin-height=&quot;1374&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;로그인 성공이 뜨는것을 알 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이번에는 잘못된 패스워드 를 입력해보자&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;로그인 실패.png&quot; data-origin-width=&quot;2332&quot; data-origin-height=&quot;1386&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Gonza/btqDQsZbgE0/WKaSkKAXTKf4wzVetIt4hK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Gonza/btqDQsZbgE0/WKaSkKAXTKf4wzVetIt4hK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Gonza/btqDQsZbgE0/WKaSkKAXTKf4wzVetIt4hK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGonza%2FbtqDQsZbgE0%2FWKaSkKAXTKf4wzVetIt4hK%2Fimg.png&quot; data-filename=&quot;로그인 실패.png&quot; data-origin-width=&quot;2332&quot; data-origin-height=&quot;1386&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;로그인 실패가 뜨는것을 알 수 있다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;h2&gt;결론&lt;/h2&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;이번 시간에는, 로그인 기능, 인증(Authentication)의 개념과 세션, 그리고 인터셉터에 대해서 알아보았다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;여담으로, 저 세션 같은 경우 굳이 스프링에서 제공하는 세션 저장소 에서 관리하지 않고, 레디스나 RDB에 세션 저장소를 직접 만들고 관리 할 수 도 있다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;실제로, SSO(Single Sign On) 기능에서 보통 저런식으로 구현해서 쓰고 있기도 하다. 그리고 , spring session data에서도 해당 기능을 지원하고 있다.&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;다음 시간에는 데이터의 이력을 저장/관리 하는 Auditing에 대해서 알아보도록 하겠다.&lt;/p&gt;</description>
      <category>개발/Java</category>
      <category>java</category>
      <category>Spring Boot</category>
      <category>spring mvc</category>
      <category>세션</category>
      <category>스프링</category>
      <category>스프링 부트</category>
      <category>웹 프로그래밍</category>
      <category>인증</category>
      <category>인터셉터</category>
      <category>자바</category>
      <author>semtax</author>
      <guid isPermaLink="true">https://semtax.tistory.com/74</guid>
      <comments>https://semtax.tistory.com/74#entry74comment</comments>
      <pubDate>Fri, 1 May 2020 17:19:44 +0900</pubDate>
    </item>
    <item>
      <title>스프링부트로 게시판 만들기 5 : 회원가입 기능 추가</title>
      <link>https://semtax.tistory.com/73</link>
      <description>&lt;h2&gt;개요&lt;/h2&gt;
&lt;p&gt;​                               &lt;/p&gt;
&lt;p&gt;이번 포스팅에서는 회원가입 기능을 추가해보도록 하겠다.&lt;/p&gt;
&lt;p&gt;​                  &lt;/p&gt;
&lt;p&gt;​                       &lt;/p&gt;
&lt;h2&gt;설계&lt;/h2&gt;
&lt;p&gt;​                          &lt;/p&gt;
&lt;p&gt; 먼저 회원 정보는 간단하게 아래의 데이터만을 가지고 수행하도록 한다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;아이디&lt;/li&gt;
&lt;li&gt;패스워드&lt;/li&gt;
&lt;li&gt;이메일&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;​                        &lt;/p&gt;
&lt;p&gt;회원 가입 기능은 대략적으로 아래와 같은 흐름을 따라서 만들어 진다.&lt;/p&gt;
&lt;p&gt;​                          &lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;사용자가 서버에 회원 정보를 전송한다.&lt;/li&gt;
&lt;li&gt;서버는 회원 정보를 받고 아래와 같은 작업을 수행한다.&lt;ol&gt;
&lt;li&gt;먼저 데이터베이스에 중복된 아이디가 있는지 확인 한다.&lt;/li&gt;
&lt;li&gt;만약 중복된 아이디가 있는 경우, 이미 있는 회원이라 가입이 안된다는 메시지를 보낸다.&lt;/li&gt;
&lt;li&gt;그렇지 않은 경우 전달 받은 데이터를 데이터베이스에 추가 한다.&lt;ol&gt;
&lt;li&gt;이때, 전달받은 패스워드를 해싱해서 저장한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;회원가입을 성공했다는 메시지를 전달한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;​                                     &lt;/p&gt;
&lt;p&gt;​                                  &lt;/p&gt;
&lt;h2&gt;구현&lt;/h2&gt;
&lt;p&gt;​                                       &lt;/p&gt;
&lt;p&gt;먼저 아래와 같이 실제 회원 정보에 해당하는 Account Entity를 추가 해 보도록 하자.&lt;/p&gt;
&lt;p&gt;​                                        &lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;package com.semtax.application.entity;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import java.util.Objects;

@Entity
public class Account {

    @Id @GeneratedValue
    private Long id;

    private String username;
    private String email;
    private String password;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Account account = (Account) o;
        return id.equals(account.id) &amp;amp;&amp;amp;
                username.equals(account.username) &amp;amp;&amp;amp;
                email.equals(account.email) &amp;amp;&amp;amp;
                password.equals(account.password);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id, username, email, password);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​                              &lt;/p&gt;
&lt;p&gt;​                      &lt;/p&gt;
&lt;p&gt;그리고 나서, 아래와 같이 실제 데이터베이스에 회원 정보를 저장/조회하는 로직을 구현 한다.&lt;/p&gt;
&lt;p&gt;​                  &lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;package com.semtax.application.repository;

import com.semtax.application.entity.Account;
import org.springframework.data.jpa.repository.JpaRepository;

public interface AccountRepository extends JpaRepository&amp;lt;Account,Long&amp;gt; {

    public Account findByUsername(String username);
    public Account findByUsernameAndPassword(String username, String password);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;스프링 데이터 JPA는 위와 같이, 메소드 이름을 스프링 데이터 JPA 규칙에 맞게 지어주면 해당 Entity에 맞게 데이터를 조회/조작 하는 코드를 자동으로 생성해주게 된다.&lt;/p&gt;
&lt;p&gt;(물론 비즈니스 로직이 복잡해지면 레포지토리 코드를 직접 작성해야 한다)&lt;/p&gt;
&lt;p&gt;​                          &lt;/p&gt;
&lt;p&gt;추가적으로, 회원가입 정보를 받기위한 DTO 데이터도 작성해주도록 하자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;package com.semtax.application.dto;

public class AccountDTO {

    private String username;
    private String password;

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​                                     &lt;/p&gt;
&lt;p&gt;그런 뒤, 회원가입 컨트롤러 코드를 아래와 같이 작성해준다.&lt;/p&gt;
&lt;p&gt;​                                       &lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;package com.semtax.application.controller;


import com.semtax.application.entity.Account;
import com.semtax.application.repository.AccountRepository;
import com.semtax.application.util.Hashing;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

@Controller
public class UserRegisterController {

    @Autowired
    AccountRepository accountRepository;

    @CrossOrigin(origins = &amp;quot;*&amp;quot;, allowedHeaders = &amp;quot;*&amp;quot;)
    @PostMapping(&amp;quot;/register&amp;quot;)
    @ResponseBody
    public String registerUser(@RequestBody Account newAccount) {

        String username = newAccount.getUsername();
        String password = Hashing.hashingPassword(newAccount.getPassword());
        String email = newAccount.getEmail();

        if(username.equals(&amp;quot;&amp;quot;) || password.equals(&amp;quot;&amp;quot;) || email.equals(&amp;quot;&amp;quot;))
            return &amp;quot;failed&amp;quot;;

        Account account = new Account();
        account.setUsername(username);
        account.setPassword(password);
        account.setEmail(email);

        if(accountRepository.findByUsername(username) != null)
            return &amp;quot;failed&amp;quot;;

        accountRepository.save(account);

        return &amp;quot;success&amp;quot;;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​                                               &lt;/p&gt;
&lt;p&gt;위의 코드에서 전달받은 패스워드를 해싱해주는것을 알 수 있다.&lt;/p&gt;
&lt;p&gt;이렇게 하는 이유는 이를 통해 만약 데이터베이스가 도난 당하더라도(그러면 안되기는 하지만), &lt;/p&gt;
&lt;p&gt;패스워드 자체를 가지고 있는것은 아니기 때문에, 상대적으로 피해가 덜하게 된다. &lt;/p&gt;
&lt;p&gt;(결국 탈취한 사람이 패스워드를 알아내기 까지에 시간이 소요되므로)&lt;/p&gt;
&lt;p&gt;그리고, 실제로 해싱해주는 래퍼 클래스를 추가해주자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class Hashing {

    public static final String SALT = &amp;quot;!@salt$%^&amp;amp;&amp;quot;;

    public static String hashingPassword(String input) {

        try {
            MessageDigest md = MessageDigest.getInstance(&amp;quot;SHA-256&amp;quot;);
            byte[] hashData = md.digest(input.getBytes(StandardCharsets.UTF_8));
            BigInteger number = new BigInteger(1, hashData);
            StringBuilder hexString = new StringBuilder(number.toString(16));

            while (hexString.length() &amp;lt; 32) {
                hexString.insert(0, &amp;#39;0&amp;#39;);
            }
            return hexString.toString();
        }catch(NoSuchAlgorithmException e) {
            return input;
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;추가 과제(?)&lt;/h2&gt;
&lt;p&gt;사실 위의 코드는 그렇게 까지 좋은 코드는 아니다. 왜냐하면 컨트롤러에 실제 회원가입을 처리하는 로직과 다른 로직들이 섞여있기 때문이다.&lt;/p&gt;
&lt;p&gt;나중에 시간이 나면 저 위에 있는 회원 가입 코드를 서비스 영역으로 빼보는걸 해보는것도 좋을것 같다.              &lt;/p&gt;
&lt;h2&gt;테스트&lt;/h2&gt;
&lt;p&gt;Postman을 켜서 테스트를 해보자.&lt;/p&gt;
&lt;p&gt;먼저 Postman을 켜서 아래와 같이 회원가입 요청을 보내 주자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bL08OF/btqDOHXwqZy/UvqEKQikE7a7TYtby1FGnK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bL08OF/btqDOHXwqZy/UvqEKQikE7a7TYtby1FGnK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bL08OF/btqDOHXwqZy/UvqEKQikE7a7TYtby1FGnK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbL08OF%2FbtqDOHXwqZy%2FUvqEKQikE7a7TYtby1FGnK%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;다음으로, DB를 확인해보자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Ee8bc/btqDRMoRZDS/8Kfvu0fakJ9nVZYXzo6Unk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Ee8bc/btqDRMoRZDS/8Kfvu0fakJ9nVZYXzo6Unk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Ee8bc/btqDRMoRZDS/8Kfvu0fakJ9nVZYXzo6Unk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEe8bc%2FbtqDRMoRZDS%2F8Kfvu0fakJ9nVZYXzo6Unk%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;정상적으로 들어오는것을 확인 할 수 있다.&lt;/p&gt;
&lt;p&gt;​                 &lt;/p&gt;
&lt;h2&gt;결론&lt;/h2&gt;
&lt;p&gt;​                     &lt;/p&gt;
&lt;p&gt;일단, 이번 포스팅에서는 회원 가입 &lt;strong&gt;만&lt;/strong&gt; 하는 기능을 구현하였다.&lt;/p&gt;
&lt;p&gt;​               &lt;/p&gt;
&lt;p&gt;사실 회원 가입 기능 &lt;strong&gt;만&lt;/strong&gt; 있는 경우는, 접근 권한 제어나 접근 권한 부여를 하지 않으므로 그다지 의미가 없다.&lt;/p&gt;
&lt;p&gt;​             &lt;/p&gt;
&lt;p&gt;다음 포스팅에서는 로그인 기능을 구현 하면서, 저 접근 권한을 어떠한 방식으로 구현하는지 알아보도록 하겠다.&lt;/p&gt;</description>
      <category>개발/Java</category>
      <category>java</category>
      <category>JPA</category>
      <category>Spring</category>
      <category>Spring Boot</category>
      <category>spring mvc</category>
      <category>로그인</category>
      <category>스프링</category>
      <category>스프링 부트</category>
      <category>인증</category>
      <category>자바</category>
      <author>semtax</author>
      <guid isPermaLink="true">https://semtax.tistory.com/73</guid>
      <comments>https://semtax.tistory.com/73#entry73comment</comments>
      <pubDate>Fri, 1 May 2020 17:16:55 +0900</pubDate>
    </item>
  </channel>
</rss>