<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>HiiWee's Devlog</title>
    <link>https://hiiwee.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Wed, 27 May 2026 19:15:13 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>HiiWee</managingEditor>
    <image>
      <title>HiiWee's Devlog</title>
      <url>https://tistory1.daumcdn.net/tistory/6071163/attach/bb690cdc0f274806b222daf1db10f5bd</url>
      <link>https://hiiwee.tistory.com</link>
    </image>
    <item>
      <title>Let's Go(lang)</title>
      <link>https://hiiwee.tistory.com/49</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이제 나는 Go를 쓴다.. Bye Java and Let's Go!&lt;/p&gt;</description>
      <category>일상</category>
      <category>go</category>
      <category>golang</category>
      <author>HiiWee</author>
      <guid isPermaLink="true">https://hiiwee.tistory.com/49</guid>
      <comments>https://hiiwee.tistory.com/49#entry49comment</comments>
      <pubDate>Fri, 28 Feb 2025 01:14:14 +0900</pubDate>
    </item>
    <item>
      <title>나는 방금 우아한테크캠프 7기에서 2주를 보냈다.</title>
      <link>https://hiiwee.tistory.com/48</link>
      <description>&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/wpdlzhf159/post/9c23770e-0793-4954-bade-700d233c777c/image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;글을 쓰기 전에 언제부터 우아한테크캠프와의 인연이 시작됐었는지 궁금하여 메일함을 뒤져봤는데 2024.4.3 우아한테크캠프 7기에 첫 지원을 했던 기록이 있었습니다. 그때까지만 해도 합격할 수 있을까 하는 생각은 하나도 하지 못했는데, &lt;b&gt;지금은 어느덧 교육을 시작한 지 2주라는 시간이 흘렀습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;우테캠은 어떤 곳인가?&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지원할 때까지만 해도 우아한형제들에서 올린 공고를 제외하곤 정보가 거의 없었습니다. 백엔드 교육은 작년 6기가 처음이었고, 뽑는 인원도 20명 내외로 알고 있었기에 인터넷에선 몇 개 안 되는 후기 글밖에 없었습니다. 교육이 시작되고 2주가 지난 지금 당장 우테캠은 어떤 곳이냐고 물어본다면 &lt;code&gt;주어진 짧은 요구사항 속에서 개개인의 저마다의 합리적인 이유를 가지고 미션을 해결하기 위해 고군분투하는 우아한형제들 작은집 7층 망령들의 노력기,,&lt;/code&gt; 정도라고 말할 수 있을 것 같습니다. &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;진행하는 미션에 관해 얘기해 보면 우리가 해결해야 하는 미션은 달성해야 하는 목표는 있지만, 중간 과정에 대한 정답은 없었습니다. 흠,, 처음에는 정답이 있어 보였는데 진행하면서 정답이 없다는 걸 느꼈습니다. 우리의 마스터 호눅스님이 했던 말을 빌려보자면 &lt;code&gt;여러분들 정답을 찾으려고 하시는데 여러분들의 생각이 중요한 거지 정답이 중요한 게 아니에요~&lt;/code&gt;와 같은 뉘앙스로 말씀하셨던 기억이 납니다. (정확한 문장은 아니었고,, 저는 그냥 그런 느낌이었다!) 그래서 미션을 진행하면서 내가 짜는 코드 한 줄 한 줄에 대해 저 스스로 &lt;code&gt;왜 이렇게 짜는거야?&lt;/code&gt; 라고 되뇌이며 미션을 진행하려 노력했던 기억이 납니다. &lt;b&gt;2주 정도 지난 지금 자신에게 질문하는 경험은 내가 지금 무엇을 모르고, 아는지 판단할 때 많은 도움이 되었습니다.&lt;/b&gt; 앞으로의 미션에서도 계속 저 자신에게 끊임없이 물어보면서 진행할 것 같습니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우테캠에서 만난 사람들은 모두 열정열정맨이었습니다.&lt;br /&gt;미션을 진행하다가, 옆자리 동료와 서로 비슷한 고민을 했던 부분에 대해서 얘기하고 있으면 어느새 해당 주제에 관심 있는 사람들이 모여 저마다의 의견을 내기도 했고, &lt;b&gt;미션 외적으로도 끊임없이 얘기하며 서로서로 인사이트를 주는 모습&lt;/b&gt;을 보기도 했습니다. 저 또한 하나의 미션이 마감되는 날에 일찍 집에 가지 않고 동료들과 남아서 몇 시간이고 떠들면서 왜 잡담이 경쟁력이 되는지 그 의미를 조금은 이해할 수 있었습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞으로도 많은 잡담을 나눠보려고 하는데 글을 보신 동료분들 중 혹시 저와 잡담을 나누고 싶으신 분이 있다면 언제나 기꺼이 응하도록 하겠습니다! &lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;많이 떠들고, 도와주고, 도움받자!&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우테캠을 진행하면서 제가 지키려고 하는 작은 슬로건입니다.&lt;br /&gt;(사실 지금까지 제 모습을 돌아보면서 대충 이런 느낌인 것 같아서 방금 만들었습니다 )&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 다른 사람들이 우테캠 이전에 해왔던 것들이나, 미션을 진행하면서 내가 생각하지 못했던 부분들을 동료들은 간단하게 생각해 내는 걸 보면서 보면서 기가 죽기도 했었고, 미션을 진행하면서도 구현에 급급하여 미션의 본질은 무엇일지, 내가 사용하는 기술은 그 속에서 어떻게 동작하는지에 대한 고민을 많이 하지 못해 아쉬웠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, 동료들과 친해지고 많은 얘기를 나누면서 내가 도움을 줄 수 있는 부분은 도와주고, 내가 부족한 부분은 도움을 받아 가면서 우테캠은 혼자 하는 게 아니었다는 걸 라는 걸 깨달았습니다. &lt;b&gt;실제로 고민되고 모르는 부분이 있었을 때 혼자 머리를 싸매고 고민하는 것보단 비슷한 고민이 있는 동료들과 실컷 떠들고 오는 게 훨씬 도움 됐던 경험&lt;/b&gt;이 있었습니다. ㅎㅎ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 제가 하고 싶은 말은! &lt;code&gt;혼자서 모든 것을 해결하지 않아도 괜찮다!&lt;/code&gt;라고 말하고 싶습니다. 그리고 주변에 열정열정맨들이 넘치기 때문에, 저 또한 언제든지 도와주고, 도움받게 된다면 이게 곧 함께 성장하는 경험을 만들어가는 게 아닐까 하는 생각이 듭니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;남는 것은 기록&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 주, 한 주 지날수록 과거에 했던 미션은 조금씩 희미해지고, 새로운 미션에 대한 고민은 많아졌습니다. 미션 외에도 다양한 활동들을 진행하면서 여러 가지 컨텍스트들이 마구 쏟아지는 현 상황 속에서 저는 열심히 기록해야겠다는 결정을 내렸습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 기록하지 않으면 이틀 정도만 지나면 머릿속에서 잊혀 갔고, 잠실의 모 CTO님의 말을 빌리자면 많은 열과 압력을 가하면서 탄소 덩어리가 다이아몬드가 되는 과정이라고 하는데 저는 이런 과정이 꽤 고되다고 느껴졌기에 살아남기 위한 발버둥 중 하나로 기록을 선택한 것 일지도 모르겠습니다 ㅎㅎ,,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 제가 활동하고, 공부하고, 느꼈던 부분을 최대한 기록에 남겨가며 남은 기간도 열심히 기록하는 사람이 되려고 합니다! 여행에 가면 남는 것은 사진이듯, 우테캠에 가면 남는 것은 기록이지 않을까요?!&lt;br /&gt;(사진도 남기면 재밌겠네요!)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;끝으로!&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요즘은 &lt;code&gt;어떻게 해야 잘 성장할 수 있을까?&lt;/code&gt;와 같은 고민이 많습니다. 정말 좋은 환경에서 교육받고 있기에 모든 것을 다 흡수하고, 이해하고, 내 것으로 만들고 싶다는 욕심이 있지만, 매우 힘들다는 것도 잘 알고 있습니다. 그래서 나는 어떤 사람인지, 무엇을 얻어가고 싶은지 조금은 구체적이고 진지하게 고민해 보려고 합니다. 또 다른 동료들의 생각도 궁금하기에 이곳저곳 물어보기도 할 것 같습니다. &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;고작 2주밖에 안 됐지만, 쓸 말이 많은 게 신기하네요 남은 8주도 후회 없이 열심히! 잘 보내겠습니다! 긴 글 읽어주셔서 감사합니다!  &amp;zwj;♂️&lt;/p&gt;</description>
      <category>우아한테크캠프 7기</category>
      <category>우아한테크캠프</category>
      <category>우아한테크캠프 7기</category>
      <category>회고</category>
      <author>HiiWee</author>
      <guid isPermaLink="true">https://hiiwee.tistory.com/48</guid>
      <comments>https://hiiwee.tistory.com/48#entry48comment</comments>
      <pubDate>Mon, 8 Jul 2024 01:17:38 +0900</pubDate>
    </item>
    <item>
      <title>[Tomcat 디버깅 해보기] mapper.writeValue() 이후 response.setStatus()를 하면 안되는 이유</title>
      <link>https://hiiwee.tistory.com/46</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;과거 개발 디코 채널에서 response.setStatus()를 적용했지만 응답이 무조건 200 OK로 내려진다는 이슈에 대한 질문이 있었습니다. (지금은 채널이 사라짐 ㅠ)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 테스트 해본 결과 정말 200으로밖에 응답이 내려지지 않았고, 그 이유에 대해서 간단히 디버깅해보고 설명해보고자 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예제 코드는 아래 저장소에서 확인할 수 있습니다!&lt;br /&gt;&lt;a href=&quot;https://github.com/HiiWee/hiiwee-lab/tree/master/basic-spring-boot&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/HiiWee/hiiwee-lab/tree/master/basic-spring-boot&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✅ 문제 코드 및 실행 결과&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;문제 코드&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 코드는 그때 상황을 발생시키기 위해 비슷하게 작성한 예시 코드입니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;BasicController&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Slf4j
@Controller
public class BasicController {

    private final ObjectMapper objectMapper;

    public BasicController(final ObjectMapper objectMapper) {
        this.objectMapper = objectMapper;
    }

    @GetMapping(&quot;/basic&quot;)
    public void basic(final HttpServletResponse response) throws IOException {
        MessageResponse messageResponse = new MessageResponse(&quot;response message to Response Body.&quot;);
        objectMapper.writeValue(response.getOutputStream(), messageResponse);
        response.setStatus(400);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;MessageRespone - 간단한 응답 객체&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;@Getter
public class MessageResponse {

    private String message;

    private MessageResponse() {
    }

    public MessageResponse(final String message) {
        this.message = message;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;테스트 코드 및 실행 결과&lt;/h3&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class BasicApiTest {

    @LocalServerPort
    private int port;

    @BeforeEach
    void setPort() {
        RestAssured.port = port;
    }

    @DisplayName(&quot;mapper write 이후 status를 set하면 적용되지 않는다.&quot;)
    @Test
    void afterWriteToMapper_statusWillNeverChange() {
        // given &amp;amp; when
        ExtractableResponse&amp;lt;Response&amp;gt; response = RestAssured.given().log().all()
                .contentType(ContentType.TEXT)
                .when()
                .get(&quot;/basic&quot;)
                .then().log().all()
                .extract();
        MessageResponse messageResponse = response.body().jsonPath().getObject(&quot;.&quot;, MessageResponse.class);

        // then
        assertAll(
                () -&amp;gt; assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()),
                () -&amp;gt; assertThat(messageResponse.getMessage()).isEqualTo(&quot;response message to Response Body.&quot;)
        );
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/wpdlzhf159/post/5ac99b90-bc54-4aa9-ad14-1ba04a6be9b7/image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/wpdlzhf159/post/62d94cfc-495b-442c-a624-a905199f2076/image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기대 응답&lt;br /&gt;&lt;code&gt;GET /basic&lt;/code&gt;요청이 Response Body에 직접 메시지를 출력하고, statusCode는 400 Bad Request로 응답되는것을 기대&lt;/li&gt;
&lt;li&gt;실제 응답응답 메시지는 정상, 응답 코드가 200 OK&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✅ 원인 찾기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;setStatus() 디버깅&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/wpdlzhf159/post/54fa0b09-ae25-4408-8ee5-d27b7cba3a2b/image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위처럼 디버깅 포인트를 걸고 테스트를 디버그 모드로 실행해보면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/wpdlzhf159/post/8a087172-604d-4fe7-913a-187c0f4b1283/image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같이 isCommitted() 메서드에서 true를 반환하여 입력한 400이라는 status가 set 되는것을 막는것을 알 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;isCommitted() 메서드 내부를 살펴보면 &lt;code&gt;org.apache.catalina.connector.Response&lt;/code&gt;의 isAppCommitted() 메서드가 실질적인 커밋 체크를 하고 있습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public boolean isAppCommitted() {
    return this.appCommitted || isCommitted() || isSuspended() ||
           ((getContentLength() &amp;gt; 0) &amp;amp;&amp;amp; (getContentWritten() &amp;gt;= getContentLength()));
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 검사는 현재 app이 커밋 되었는지, response object가 커밋 되었는지, response에 쓰여진 content-length 0보다 크지만 실제 쓰여진 바이트수가 response content-lenght보다 큰지 여부를 통해 boolean 값을 계산합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;@Override
public boolean isCommitted() {
    return getCoyoteResponse().isCommitted();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 중에서 response object가 커밋되었는지 확인하는 isCommitted()메서드는&lt;br /&gt;&lt;code&gt;org.apache.coyote.Response&lt;/code&gt;(속칭 CoyoteResponse) 객체의 커밋상태를 확인합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지 살펴보면 응답 상태를 set하려 했지만, 어떤 이유에선지 이미 commit되어 있는 상태라는 걸 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;coyoteResponse&lt;/code&gt;에는 setCommitted()라는 메서드가 존재하므로 해당 부분을 디버깅해 어디에서 commit flag가 true로 set 되는지 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;org.apache.coyote.Reponse의 setCommitted() 디버깅&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/wpdlzhf159/post/8f531c95-6120-4633-813e-64fa63d7196f/image.png&quot; alt=&quot;&quot; /&gt;&lt;img src=&quot;https://velog.velcdn.com/images/wpdlzhf159/post/710bfb0b-4620-4923-b310-52e460b8ea39/image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 디버깅 포인트를 걸고 실행해보면 BasicController에서 objectMapper를 통해 writeValue()를 호출할때 내부적으로 response에 응답 객체를 쓰고, commit을 true로 set함을 알 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스택 트레이스를 따라가면 아래와 같이 동작합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;응답 데이터를 outputBuffer 씀&lt;/li&gt;
&lt;li&gt;close 작업시 내부에서 flush를 통해 버퍼에 있는 데이터를 실제 response로 옮기는 작업을 시작&lt;/li&gt;
&lt;li&gt;먼저 Response Header 작업이 끝났다는 신호를 보내줌&lt;/li&gt;
&lt;li&gt;Http11OutputBuffer에게 헤더의 유효성을 검증하고, 헤더를 Response에 작성 및 commit하도록 합니다.&lt;/li&gt;
&lt;li&gt;이때 내부적으로 coyoteResponse는 commit 플래그가 set 됩니다.&lt;/li&gt;
&lt;li&gt;초기 outputBuffer에 작성한 응답 데이터를 write함&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 위의 작업 이후에 setStatus를 호출하게 되면 이미 commit이 true값으로 set 되어있으므로 응답 코드를 설정할 수 없게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✅ 결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;setStatus는 반드시 response에 응답 데이터를 write 하기 이전에 하자!&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Security를 사용할때 successHandler 혹은 failHandler에서 직접 Response에 응답 데이터 및 코드를 내려주는 상황이 종종 있습니다. 이럴때에 이 부분을 잘 알고있다면 적어도 응답 코드가 바뀌지 않는 문제상황을 피할 수 있지 않을까요?!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;잘못되거나 부족한 부분이 있다면 언제든지 댓글로 알려주시면 감사합니다!&lt;/code&gt;&lt;/p&gt;</description>
      <category>Spring/Debugging Spring</category>
      <category>http status</category>
      <category>objectmapper</category>
      <category>response</category>
      <category>spring boot</category>
      <category>tomcat</category>
      <author>HiiWee</author>
      <guid isPermaLink="true">https://hiiwee.tistory.com/46</guid>
      <comments>https://hiiwee.tistory.com/46#entry46comment</comments>
      <pubDate>Wed, 23 Aug 2023 17:30:54 +0900</pubDate>
    </item>
    <item>
      <title>[Java] Java Generic (2) - 타입 소거 및 제한 사항</title>
      <link>https://hiiwee.tistory.com/45</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;지난 시간에는 제네릭의 종류와, 사용법에 대해서 알아봤습니다.&lt;br /&gt;이번시간에는 제네릭에 대해 컴파일러가 수행하는 타입 소거와, 공식문서에서 제안하는 제네릭 사용시 제안 사항에 대해서 알아보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✅ Type Erasure(타입 소거)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타입 소거는 제네릭의 사용 방식이라기 보단 컴파일러가 제네릭을 대하는 방식이라고 생각할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제네릭은 컴파일시 엄격한 유형 검사를 제공합니다. 자바 컴파일러는 제네릭 구현을 위해 Type Erasure 기능을 적용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타입 소거에 대한 규칙은 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;제네릭 타입의 모든 타입 매개변수를 해당 경계 혹은 Object 타입으로 교체합니다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;제네릭 타입을 제거한 후 타입이 일치하지 않는다면 타입 캐스팅을 합니다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;확장된 제네릭 타입의 다형성을 보존하기 위해 브릿지 메서드를 생성합니다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타입 소거는 매개변수화된 유형에 대해 새 클래스가 생성되지 않도록 보장하므로 제네릭은 런타임 오버헤드가 발생하지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;제네릭 타입 소거(첫번째 규칙)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같은 Box 제네릭 클래스가 있습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class Box&amp;lt;T&amp;gt; {
    private T value;

    public T getValue() {
        return value;
    }

    public void setValue(final T value) {
        this.value = value;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Box 클래스를 컴파일하고 바이트코드를 살펴보면 타입 매개변수 T를 Object 타입으로 변경합니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// class version 55.0 (55)
// access flags 0x21
// signature &amp;lt;T:Ljava/lang/Object;&amp;gt;Ljava/lang/Object;
// declaration: chapter21/Box&amp;lt;T&amp;gt;
public class chapter21/Box {

  // compiled from: Box.java

  // access flags 0x2
  // signature TT;
  // declaration: value extends T
  private Ljava/lang/Object; value

    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Box&amp;lt;T&amp;gt;&lt;/code&gt;에서 T는 언바운드 타입이므로 default로 &lt;code&gt;Object 클래스로 변경&lt;/code&gt;됩니다. 만약 타입 매개변수가 바인딩된 경우(Upper or Lower) 해당 타입으로 바인딩합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바운드된 타입인 Box를 아래와 같이 정의하면&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class Box&amp;lt;T extends Integer&amp;gt; {
    private T value;

    public T getValue() {
        return value;
    }

    public void setValue(final T value) {
        this.value = value;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같이 T의 타입이 Integer로 정의된 바이트코드를 볼 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// class version 55.0 (55)
// access flags 0x21
// signature &amp;lt;T:Ljava/lang/Integer;&amp;gt;Ljava/lang/Object;
// declaration: chapter21/Box&amp;lt;T extends java.lang.Integer&amp;gt;
public class chapter21/Box {

  // compiled from: Box.java

  // access flags 0x2
  // signature TT;
  // declaration: value extends T
  private Ljava/lang/Integer; value

    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 타입 소거의 과정은 제네릭 메서드에서도 적용됩니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public static &amp;lt;T&amp;gt; int count(T[] anArray, T elem) {
    int cnt = 0;
    for (T e : anArray)
        if (e.equals(elem))
            ++cnt;
        return cnt;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바 컴파일러는 위의 코드에서 &lt;code&gt;T 타입&lt;/code&gt;이 언바운드 타입이므로 Object로 대체합니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public static int count(Object[] anArray, Object elem) {
    int cnt = 0;
    for (Object e : anArray)
        if (e.equals(elem))
            ++cnt;
        return cnt;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;일치하지 않는 타입에 대한 타입 캐스팅(두번째 규칙)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같이 &lt;code&gt;List&amp;lt;String&amp;gt;&lt;/code&gt; 타입의 리스트에 요소를 추가하고, 꺼내는 코드가 있습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;List&amp;lt;String&amp;gt; list = new ArrayList&amp;lt;&amp;gt;();
list.add(&quot;hello&quot;);
System.out.println(list.get(0));&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 코드의 디컴파일된 클래스 파일을 살펴보면 다음과 같이 String으로 명시적인 형변환이 적용됩니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;List&amp;lt;String&amp;gt; list = new ArrayList();
list.add(&quot;hello&quot;);
System.out.println((String)list.get(0));&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://stackoverflow.com/questions/339699/java-generics-type-erasure-when-and-what-happens&quot;&gt;Java generics type erasure: when and what happens? - Stackoverflow&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;타입 소거 및 브릿지 메서드의 효과(세번째 규칙)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타입 소거로 인해 예상치 못한 상황이 발생할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 상황에 대비해 자바 컴파일러는 브릿지 메서드라고 하는 합성 메서드를 생성하여 예상치 못한 상황을 해결합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node 클래스와 해당 클래스를 확장하는 MyNode 클래스가 다음과 같이 존재합니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class Node&amp;lt;T&amp;gt; {

    public T data;

    public Node(T data) { this.data = data; }

    public void setData(T data) {
        System.out.println(&quot;Node.setData&quot;);
        this.data = data;
    }
}

public class MyNode extends Node&amp;lt;Integer&amp;gt; {
    public MyNode(Integer data) { super(data); }

    public void setData(Integer data) {
        System.out.println(&quot;MyNode.setData&quot;);
        super.setData(data);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매개변수화된 클래스, 인터페이스(제네릭 타입)을 확장하여 컴파일할때 컴파일러는 타입 소거 프로세스의 일부로 브릿지 메서드를 생성합니다. 위에서 작성한 Node와 MyNode 클래스는 타입 소거에 의해 다음과 같은 형태로 변경됩니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class Node {

    public Object data;

    public Node(Object data) { this.data = data; }

    public void setData(Object data) {
        System.out.println(&quot;Node.setData&quot;);
        this.data = data;
    }
}

public class MyNode extends Node {

    public MyNode(Integer data) { super(data); }

    public void setData(Integer data) {
        System.out.println(&quot;MyNode.setData&quot;);
        super.setData(data);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node는 T가 언바운드 타입이므로 Object로 변경되고, MyNode는 Integer 타입으로 바운딩 되어 있으므로 Integer로 변환됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 타입 소거 이후 Node, MyNode의 setData메서드는 메서드 시그니처가 달라지며 Node.setData를 MyNode.setData가 재정의하지 않게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 문제를 해결하기 위해 타입 소거 이후에도 제네릭 타입의 다형성을 유지하기 위한 브릿지 메서드를 컴파일러가 생성합니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class MyNode extends Node {

    public MyNode(Integer data) { super(data); }

        // Bridge method generated by the compiler
    public void setData(Object data) {
        setData((Integer) data);
    }

    public void setData(Integer data) {
        System.out.println(&quot;MyNode.setData&quot;);
        super.setData(data);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MyNode.setData(Object)는 MyNode.setData(Integer)로 위임하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Reifiable Types&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바에서는 Runtime에 타입 정보를 완전히 사용할 수 있는 타입들을 &lt;code&gt;Reifiable Types&lt;/code&gt;라고 말합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Reifiable Types의 종류는 다음과 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Primitive types&lt;/li&gt;
&lt;li&gt;Non-Generic Types&lt;/li&gt;
&lt;li&gt;Unbounded Wildcards&lt;/li&gt;
&lt;li&gt;Raw Types&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Unbounded Wildcards는 애초에 타입에 대한 정보 자체가 Unknown으로 취급되므로 컴파일시에 Type Erasure 한다고 해도 잃을 정보가 없기에 Reifiable Types에 포함됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Non-Reifiable Types&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Non-Reifiable Types는 컴파일 시 타입 소거에 의해 타입 정보가 제거된 유형을 말합니다.&lt;br /&gt;따라서 런타임시에 타입 정보가 남아있지 않게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Non-Reifiable Types의 종류는 다음과 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Generic Type(T, Unbounded Wildcards 제외)&lt;/li&gt;
&lt;li&gt;Parameterized Type(&lt;code&gt;List&amp;lt;Number&amp;gt;&lt;/code&gt;, &lt;code&gt;ArrayList&amp;lt;String&amp;gt;&lt;/code&gt; 등)&lt;/li&gt;
&lt;li&gt;상한, 하한 경계가 있는 Parameterized Type(&lt;code&gt;List&amp;lt;? extends Nubmer&amp;gt;&lt;/code&gt; 등)&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식문서에는 Non-Reifiable Types을 사용할때 발생할 수 있는 힙 오염(Heap Pollution)에 대해서 설명하고 있으나 현재에서는 생략합니다. 자세한 내용은 해당 문서를 참조하시면 됩니다!&lt;br /&gt;&lt;a href=&quot;https://docs.oracle.com/javase/tutorial/java/generics/nonReifiableVarargsType.html&quot;&gt;https://docs.oracle.com/javase/tutorial/java/generics/nonReifiableVarargsType.html&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✅ Restrictions On Generics(제네릭 제한사항)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제네릭을 효과적으로 사용하기 위해서는 몇가지 제한사항을 고려하면서 사용해야 합니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;원시 타입으로 제네릭 타입을 인스턴스화 할 수 없습니다.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원시 타입을 사용할 수 없는 이유로는 타입 소거와도 관련이 있습니다. 타입 소거의 첫번째 규칙은 &lt;b&gt;&lt;code&gt;제네릭 타입의 모든 타입 매개변수를 해당 경계 혹은 Object 타입으로 교체합니다.&lt;/code&gt;&lt;/b&gt; 입니다. 하지만 원시 타입은 Object 클래스의 하위 타입이 아니므로 제네릭에서 사용할 수 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 다음과 같은 코드는 불가능합니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;List&amp;lt;int&amp;gt; ints = new ArrayList&amp;lt;&amp;gt;();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;타입 매개변수를 이용해 인스턴스화 할 수 없습니다.&lt;/h3&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public static &amp;lt;E&amp;gt; void append(List&amp;lt;E&amp;gt; list) {
    E elem = new E();  // compile-time error
    list.add(elem);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 제네릭 메서드에서 타입 매개변수로 지정된 &lt;code&gt;E&lt;/code&gt;를 가지고 직접적으로 인스턴스화 시킬 수 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인스턴스화가 정말 필요하다면 리플렉션을 이용해 할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;php&quot;&gt;&lt;code&gt;public static &amp;lt;E&amp;gt; void append(List&amp;lt;E&amp;gt; list, Class&amp;lt;E&amp;gt; cls) throws Exception {
    E elem = cls.newInstance();   // OK
    list.add(elem);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;클래스 변수로 타입 매개변수를 이용할 수 없다.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클래스의 정적 필드는 클래스의 모든 non-static 객체가 공유하는 클래스 레벨의 변수이므로 타입 매개변수 유형은 정적 필드로 둘 수 없습니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;public class MobileDevice&amp;lt;T&amp;gt; {
    private static T os; // 불가능

    // ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약에 위의 코드가 허락된다면 다음 코드는 충돌하게 됩니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;MobileDevice&amp;lt;Smartphone&amp;gt; phone = new MobileDevice&amp;lt;&amp;gt;();
MobileDevice&amp;lt;Pager&amp;gt; pager = new MobileDevice&amp;lt;&amp;gt;();
MobileDevice&amp;lt;TabletPC&amp;gt; pc = new MobileDevice&amp;lt;&amp;gt;();

// os 변수에는 어떤 타입이 할당 되어야 할지 알 수 없습니다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Parameterized Type에 캐스트 혹은 instanceof 사용은 불가능하다.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바 컴파일러는 제네릭 코드의 모든 타입 매개변수를 지웁니다. 따라서 런타임시 제네릭 타입에서 어떤 Parameterized Type이 사용되는지 알 수 없습니다. 따라서 다음 코드는 컴파일 에러가 발생합니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public static &amp;lt;E&amp;gt; void rtti(List&amp;lt;E&amp;gt; list) {
    if (list instanceof ArrayList&amp;lt;Integer&amp;gt;) {  // compile-time error
        // ...
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 메서드로 전달될 수 있는 매개변수의 parameterized type은 다양합니다. (&lt;code&gt;ArrayList&amp;lt;Integer&amp;gt;&lt;/code&gt;, &lt;code&gt;ArrayList&amp;lt;String&amp;gt;&lt;/code&gt; 등) 런타임시 타입 매개변수를 추적하지 않으므로 &lt;code&gt;ArrayList&amp;lt;Integer&amp;gt;&lt;/code&gt;와 &lt;code&gt;ArrayList&amp;lt;String&amp;gt;&lt;/code&gt;은 런타임에서 구분할 수 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 코드에서 최선의 변환은 Unbounded wildcards를 이용하는 방식입니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public static void rtti(List&amp;lt;?&amp;gt; list) {
    if (list instanceof ArrayList&amp;lt;?&amp;gt;) {  // compile-time error
        // ...
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 parameterized type은 unbounded wildcards 매개변수를 이용하지 않는 한 형변환을 할 수 없습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;List&amp;lt;Integer&amp;gt; li = new ArrayList&amp;lt;&amp;gt;();
List&amp;lt;Number&amp;gt;  ln = (List&amp;lt;Number&amp;gt;) li;  // compile-time error&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 컴파일러가 타입 매개변수가 항상 유효하다는것을 알고 있는 몇몇 경우에는 형변환을 허용합니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;List&amp;lt;String&amp;gt; l1 = new ArrayList&amp;lt;&amp;gt;();
ArrayList&amp;lt;String&amp;gt; l2 = (ArrayList&amp;lt;String&amp;gt;)l1;  // OK&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Parameterized Types 배열을 생성할 수 없습니다.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 코드는 컴파일 에러가 발생합니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;List&amp;lt;Integer&amp;gt;[] arrayOfLists = new List&amp;lt;Integer&amp;gt;[2];  // compile-time error&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 특정 타입의 배열에 다른 타입의 요소가 삽입되면 컴파일 에러가 발생합니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;Object[] strings = new String[2];
strings[0] = &quot;hi&quot;;   // OK
strings[1] = 100;    // An ArrayStoreException is thrown.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 매개변수화된 타입 배열을 생성할 수 있다고 가정하면 다음 코드도 가능해야 합니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;Object[] stringLists = new List&amp;lt;String&amp;gt;[2];
stringLists[0] = new ArrayList&amp;lt;String&amp;gt;();
stringLists[1] = new ArrayList&amp;lt;Integer&amp;gt;(); // ArrayStoreException이 발생해야 함&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 &lt;code&gt;stringLists[1] = new ArrayList&amp;lt;Integer&amp;gt;();&lt;/code&gt; 하지만 위의 코드는 해당 요소를 직접 꺼내어 다른 액세스 하는 작업을 하지 않는 이상 런타임에서 감지되지 못할것 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Parameterized Types를 예외에서 이용할 수 없습니다.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제네릭 클래스는 &lt;code&gt;Throwable&lt;/code&gt; 클래스를 상속받을 수 없습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;class MathException&amp;lt;T&amp;gt; extends Exception { /* ... */ }    // compile-time error
class MathException&amp;lt;T&amp;gt; extends Throwable { /* ... */ }    // compile-time error&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 제네릭 메서드에서도 타입 매개변수의 인스턴스를 catch할 수 없습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public static &amp;lt;T extends Exception, J&amp;gt; void execute(List&amp;lt;J&amp;gt; jobs) {
    try {
        for (J job : jobs)
            // ...
    } catch (T e) {   // compile-time error
        // ...
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 throw 절에서는 타입 매개변수를 이용할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;class Parse&amp;lt;T extends Exception&amp;gt; {
        public void parse(File file) throw T {...} // 가능!&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;타입 소거 이후 동일한 메서드 시그니처를 갖는 메서드를 2개 이상 둘 수 없다(메서드 오버로드 안됨)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 컴파일러에 의한 타입 소거가 이루어졌을때 동일한 메서드 시그니처를 갖는 2개의 메서드가 있다며 컴파일 에러가 발생합니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class Example {
    public void print(Set&amp;lt;String&amp;gt; strSet) { }
    public void print(Set&amp;lt;Integer&amp;gt; intSet) { }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 마치면서&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바에서 제네릭을 왜 도입시켰을까를 조금 이해할 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제네릭은 강력한 타입 검사를 지원해주지만, 잘못 알고 사용하면 양날의 검과 같은 문법인것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 조금 많이 막막했던 제네릭이 어느정도 형태가 보이는것 같아서 뿌듯하기도 하네요 ㅎㅎ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;글을 읽으시면서 잘못된 부분이나 수정해야 할 부분이 있다면 언제든지 댓글로 알려주시면 감사하겠습니다!!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  References&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.oracle.com/javase/tutorial/java/generics/index.html&quot;&gt;Oracle Docs - Generics&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://wisdom-and-record.tistory.com/134&quot;&gt;Type Erasure Deep Dive - 어썸오 개발블로그&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://cla9.tistory.com/52&quot;&gt;Reification - 북극 펭귄&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Java</category>
      <category>Generic</category>
      <category>Java</category>
      <author>HiiWee</author>
      <guid isPermaLink="true">https://hiiwee.tistory.com/45</guid>
      <comments>https://hiiwee.tistory.com/45#entry45comment</comments>
      <pubDate>Mon, 12 Jun 2023 03:51:36 +0900</pubDate>
    </item>
    <item>
      <title>[Java] Java Generic (1) - 기본적인 제네릭의 사용!</title>
      <link>https://hiiwee.tistory.com/44</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;제네릭은 익숙하듯 하다가도, 시간이 지나면 내가 제대로 알고있나? 라는 생각이 들었던 부분이었습니다. 자바 뿐만 아니라 여러 언어에서도 타입에 대한 안정성을 보장하기 위해 제네릭을 차용해 사용하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무백스 스터디를 진행하면서 공식문서와 레퍼런스를 통해 공부한 제네릭의 모든 내용을 정리해보려고 합니다!!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✅ 제네릭은 왜 사용할까?&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;컴파일 타임에 강력한 유형 검사를 할 수 있습니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;자바 컴파일러는 제네릭을 사용한 코드가 type safety를 위반한다면 오류를 발생시킵니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;형 변환을 제거할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1686509867929&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 제네릭 사용 x
List list = new ArrayList();
list.add(&quot;hello&quot;);
String s = (String) list.get(0);

// 제네릭 사용 O
List&amp;lt;String&amp;gt; list = new ArrayList&amp;lt;String&amp;gt;();
list.add(&quot;hello&quot;);
String s = list.get(0);   // no cast&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✅ Generic Types&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제네릭 타입은 &lt;code&gt;클래스, 인터페이스&lt;/code&gt;에 매개변수화된 타입을 말합니다. 제네릭 타입은 다음과 같이 정의하여 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;class name&amp;lt;T1, T2, &amp;hellip;, Tn&amp;gt; {&amp;hellip;}&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타입변수는 아무 이름을 지정해도되며, 위와같이 여러개의 타입 변수를 나열할 수 있습니다. 일반적으로 타입변수의 명명 규칙은 다음과 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&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;E&lt;/td&gt;
&lt;td&gt;Element(Java 컬렉션 프레임워크에서 광범위하게 사용됩니다.)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;K&lt;/td&gt;
&lt;td&gt;Key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;V&lt;/td&gt;
&lt;td&gt;Value&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;숫자&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T&lt;/td&gt;
&lt;td&gt;타입(클래스, 인터페이스&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;S, U, V&lt;/td&gt;
&lt;td&gt;여러개의 타입 변수에서 2, 3, 4번째 타입을 지칭할때 사용됩니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;단일 제네릭 타입 사용 예시&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제네릭 타입을 사용하여 초기에 설명했던 제네릭을 사용하면서 얻을 수 있는 이점을 알아보겠습니다!&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class Box {
    private Object object;

    public Object getObject() {
        return object;
    }

    public void setObject(final Object object) {
        this.object = object;
    }
}

class BoxGeneric&amp;lt;T&amp;gt; {
    private T t;

    public T getT() {
        return t;
    }

    public void setT(final T t) {
        this.t = t;
    }
}

class BoxApplication {
    public static void main(String[] args) {
        // use non-generic Box class
        Box box = new Box();
        box.setObject(&quot;안녕하세요!&quot;); // String을 제외한 모든 타입이 set될 수 있다.
        Object object = box.getObject(); // Object를 반환하므로 type safety하지 않다.
        String greeting = (String) object; // 별도의 캐스팅이 필요
        System.out.println(greeting);

        // use generic Box class
        BoxGeneric&amp;lt;String&amp;gt; boxGeneric = new BoxGeneric&amp;lt;&amp;gt;(); // 애초에 String이라는 타입으로 지정했으므로 type-safety함
        boxGeneric.setT(&quot;안녕하세요!&quot;);
        String greetingGeneric = boxGeneric.getT(); // 응답값이 String
        System.out.println(greetingGeneric);
    }
}
// [실행결과]
// 안녕하세요!
// 안녕하세요!&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Box 클래스의 Object를 사용하므로 경우 원시 타입이 아니라면 원하는 타입을 set할 수 있습니다. 하지만, get을 할때도 Object를 반환하게 됩니다. 따라서 컴파일 타임에서 어떤 클래스가 반환됐는지 확인할 수 없고, 별도의 캐스팅이 필요하며 캐스팅하는 타입이 올바른지에 대한 검증을 런타임에 맡길 수 밖에 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면에 BoxGeneric 클래스는 &lt;code&gt;Box&amp;lt;String&amp;gt;&lt;/code&gt;으로 참조 변수의 타입에서 어떤 타입을 T로 보유할지 선언합니다. 따라서 set, get 동작에서 String이 아닌 타입을 전달하거나 반환받으려고 하면 컴파일 타임에서 해당 오류를 발견할 수 있습니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;new BoxGeneric&amp;lt;&amp;gt;()&lt;/code&gt;에서 &lt;code&gt;&amp;lt;&amp;gt;&lt;/code&gt;만 사용하고 String을 생략할 수 있는 이유는 Java7에 도입된 &lt;code&gt;타입 추론&lt;/code&gt;에 의해 가능합니다! &lt;code&gt;&amp;lt;&amp;gt;&lt;/code&gt;는 비공식 적으로 다이아몬드라고 부르기도 합니다!&lt;br /&gt;또한 &lt;code&gt;Box&amp;lt;T&amp;gt;&lt;/code&gt;와 같이 사용되는 T를 &lt;code&gt;타입 매개변수(Type Parameter)&lt;/code&gt;라고 지칭하고, &lt;code&gt;Box&amp;lt;Integer&amp;gt;&lt;/code&gt;와 같이 T의 타입을 선언한 Integer는 &lt;code&gt;타입 인자(Type Argument)&lt;/code&gt;라고 부릅니다!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;복수의 제네릭 타입 사용 예시&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기에 설명했듯이 제네릭 타입은 복수개도 선언할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;interface Pair&amp;lt;K, V&amp;gt; {
    public K getKey();

    public V getValue();
}

public class OrderedPair&amp;lt;K, V&amp;gt; implements Pair&amp;lt;K, V&amp;gt; {

    private K key;
    private V value;

    public OrderedPair(K key, V value) {
        this.key = key;
        this.value = value;
    }

    public K getKey() {
        return key;
    }

    public V getValue() {
        return value;
    }
}

class OrderedPairApplication {
    public static void main(String[] args) {
        OrderedPair&amp;lt;Integer, String&amp;gt; numberNamePair = new OrderedPair&amp;lt;&amp;gt;(1, &quot;one&quot;);
        Integer key = numberNamePair.getKey();
        String value = numberNamePair.getValue();

        System.out.println(key + &quot; &quot; + value); 
    }
}
// [실행결과]
// 1 one&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;K, V는 타입 변수 명명 규칙에서 Key, Value를 의미합니다. 따라서 명시적으로 한쌍의 키와 값을 저장하는 Pair 클래스를 생성합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;매개변수화된 타입&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;OrderedPair&amp;lt;K, V&amp;gt;&lt;/code&gt;에서 K또는 V를 매개변수화된 타입으로 둘 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;OrderedPair&amp;lt;String, Box&amp;lt;Integer&amp;gt;&amp;gt; p = new OrderedPair&amp;lt;&amp;gt;(&quot;primes&quot;, new Box&amp;lt;Integer&amp;gt;(...));&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Raw Types&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Raw Types는 &lt;code&gt;제네릭 클래스 혹은 인터페이스를 타입 인자없이&lt;/code&gt; 선언한 경우를 말합니다. 예를들어 위에서 생성한 BoxGeneric 클래스의 경우 &lt;code&gt;&amp;lt;T&amp;gt;&lt;/code&gt; 타입 변수를 지정하지 않고 참조 변수를 선언하면 raw types가 됩니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;BoxGeneric&amp;lt;String&amp;gt; boxGeneric = new BoxGeneric&amp;lt;&amp;gt;();
BoxGeneric rawTypes = new BoxGeneric();

rawTypes = boxGeneric; // 타입 인자가 String인 boxGeneric을 Raw Types 변수에 할당
rawTypes.setT(89);
Object t = rawTypes.getT(); // Raw Types는 Object로 반환한다.
System.out.println(t);

// 실행결과
/// 89&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;rawTypes.getT()를 했을때 Object가 반환되는 이유는 Type Erasure가 동작하여 타입 변수가 정해지지 않았다면 Object로 반환됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✅ Generic Methods&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제네릭 메소드는 자체적으로 타입 변수를 도입하는 메서드입니다. 제네릭 타입과 유사하지만, 지정한 타입 변수의 범위가 해당 제네릭 메소드로 제한됩니다. 정적, 비정적 메서드와 더불어 제네릭 클래스의 생성자도 적용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;static, non-static method에서 제네릭 메소드 정의하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정적, 일반 메서드에서 제네릭 메소드를 정의하려면&lt;br /&gt;반환 유형 앞에 &lt;code&gt;&amp;lt;T&amp;gt;&lt;/code&gt;와 같이 타입 변수를 지정해야 합니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// Pair가 동일한지 비교하는 유틸 클래스
class PairUtil {
    public static &amp;lt;K, V&amp;gt; boolean isSame(Pair&amp;lt;K, V&amp;gt; pair1, Pair&amp;lt;K, V&amp;gt; pair2) {
        return pair1.getKey().equals(pair2.getKey())
                &amp;amp;&amp;amp; pair1.getValue().equals(pair2.getValue());
    }
}

class OrderedPairApplication {
    public static void main(String[] args) {
        OrderedPair&amp;lt;Integer, String&amp;gt; numberNamePair1 = new OrderedPair&amp;lt;&amp;gt;(1, &quot;one&quot;);
        OrderedPair&amp;lt;Integer, String&amp;gt; numberNamePair2 = new OrderedPair&amp;lt;&amp;gt;(1, &quot;one&quot;);

        boolean same1 = PairUtil.&amp;lt;Integer, String&amp;gt;isSame(numberNamePair1, numberNamePair2);
        boolean same2 = PairUtil.isSame(numberNamePair1, numberNamePair2);
        System.out.println(same1);
        System.out.println(same2);
    }
}
// 실행 결과
// true
// true&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;&amp;lt;Integer, String&amp;gt;isSame(&amp;hellip;)&lt;/code&gt;과 같이 제네릭 메서드의 타입 인자를 명시적으로 지정할 수 있지만, 타입 추론이 가능하므로 이를 생략할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;생성자에 제네릭 적용하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클래스가 제네릭 클래스인지 아닌지 여부에 상관없이 생성자에도 제네릭을 지정할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class TypeInference {
    static class MyClass&amp;lt;X&amp;gt;{
        public &amp;lt;T&amp;gt; MyClass(T t) {
            System.out.println(&quot;hello&quot; + t);
        }
    }

    public static void main(String[] args) {
        MyClass&amp;lt;String&amp;gt; myClass = new MyClass&amp;lt;&amp;gt;(32);
    }
}
// 실행 결과
// hello32&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✅ Bounded Type Parameters&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제한된 타입 변수는 제네릭 타입을 지정하는 인수에 대해서 특정 범위까지 제한할 수 있는 기능을 제공합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;선언하는 방법으로는 타입 매개변수의 &lt;code&gt;이름&lt;/code&gt;, &lt;code&gt;extends 키워드&lt;/code&gt;, &lt;code&gt;상한값&lt;/code&gt;을 차례대로 나타냅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;&amp;lt;T extends Number&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Generic Method에서 사용&lt;/h3&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;class BoxGeneric&amp;lt;T&amp;gt; {
    private T t;

    public T getT() {
        return t;
    }

    public void setT(final T t) {
        this.t = t;
    }

    // Bounded Type Parameters를 사용한 generic method
    public &amp;lt;U extends Number&amp;gt; void inspect(U u){
        System.out.println(&quot;T: &quot; + t.getClass().getName());
        System.out.println(&quot;U: &quot; + u.getClass().getName());
    }

    public static void main(String[] args) {
        BoxGeneric&amp;lt;Integer&amp;gt; boxGeneric = new BoxGeneric&amp;lt;&amp;gt;();
        boxGeneric.setT(30);
        boxGeneric.inspect(50);
        // boxGeneric.inspect(&quot;text&quot;); 
                // method inspect in class BoxGeneric&amp;lt;T&amp;gt; cannot be applied to given types;
    }
}
// 실행 결과
T: java.lang.Integer
U: java.lang.Integer&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;inspect 메서드는 제네릭 메서드입니다. 제네릭의 타입 변수를 보면 임의의 U 타입은 Number 클래스이거나, 하위 클래스여야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Number 클래스는 정수형 Wrapper 클래스들이 상속받아 구현하고 있으므로 다음 클래스들 + Number 클래스의 객체들만이 해당 인자 u로 전달될 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 &lt;code&gt;boxGeneric.inspect(&amp;rdquo;text&amp;rdquo;);&lt;/code&gt;가 불가능한 이유는 String 클래스가 Number 클래스의 하위 클래스가 아니기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Bounded Type Parameters에서 사용되는 extends 키워드는 단순히 상속관계 뿐만 아니라 인터페이스의 구현 관계에서도 적용될 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 Runnable 인터페이스를 구현한 하위 객체들만 제한된 타입 변수로 두고 싶다면 다음과 같이 사용할 수 있습니다.&lt;br /&gt;&lt;code&gt;&amp;lt;T extends Runnable&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Generic Types에서 사용&lt;/h3&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;class BoxGeneric&amp;lt;T extends Number&amp;gt; {
    private T t;

    public T getT() {
        return t;
    }

    public void setT(final T t) {
        this.t = t;
    }

    // Bounded Type Parameters를 사용한 generic method
    public &amp;lt;U extends Number&amp;gt; void inspect(U u){
        System.out.println(t.intValue());
        System.out.println(&quot;T: &quot; + t.getClass().getName());
        System.out.println(&quot;U: &quot; + u.getClass().getName());
    }

    public static void main(String[] args) {
        BoxGeneric&amp;lt;Integer&amp;gt; boxGeneric = new BoxGeneric&amp;lt;&amp;gt;();
        boxGeneric.setT(30);
        boxGeneric.inspect(50);
    }
}
// 실행 결과
30
T: java.lang.Integer
U: java.lang.Integer&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;inspect 메서드에서 첫줄에서 Number 클래스에 정의되어 있는 intValue() 메서드를 호출합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생각해보면 이미 제한된 타입 변수를 통해 타입 인수로 들어올 수 있는 클래스의 상한선이 정해졌으므로(extends) 상한 클래스의 메서드는 당연히 사용할 수 있어야 하고, 실제로도 사용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;여러 타입을 바운드하기(Multiple Bounds)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제한된 타입 변수에는 여러 타입을 제한하도록 둘 수 있습니다.&lt;br /&gt;extends 키워드 뒤에 타입을 &lt;code&gt;&amp;amp;&lt;/code&gt; 를 이용해 나열하여 여러 타입을 제한할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단 몇가지 조건이 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;타입 인수로 지정할때는 반드시 지정된 타입들을 모두 구현한 클래스여야 합니다.&lt;/li&gt;
&lt;li&gt;클래스는 최대 1개만 지정할 수 있고, 인터페이스는 2개이상 지정할 수 있습니다.&lt;/li&gt;
&lt;li&gt;클래스와 인터페이스를 나열한다면 반드시 클래스를 먼저 지정해야 합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;interface A {

}

interface B {

}

class Box implements A, B {
    private Object object;

    public Object getObject() {
        return object;
    }

    public void setObject(final Object object) {
        this.object = object;
    }
}

class BoxGeneric&amp;lt;T extends Box &amp;amp; A &amp;amp; B&amp;gt; { // A &amp;amp; Box &amp;amp; B 는 컴파일 오류 발생
    private T t;

    public T getT() {
        return t;
    }

    public void setT(final T t) {
        this.t = t;
    }

    public static void main(String[] args) {
        BoxGeneric&amp;lt;Box&amp;gt; boxGeneric = new BoxGeneric&amp;lt;&amp;gt;();
        boxGeneric.setT(new Box());
        System.out.println(boxGeneric.getT());
    }
}
// 실행 결과
chapter21.Box@129a8472&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;재귀적 타입 제한(Recursive Type Bounded)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;재귀적 타입 제한은 타입 매개변수가 자기 자신을 포함하는 수식에 의해 한정됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 Comparable 인터페이스에서 많이 사용됩니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public static &amp;lt;T&amp;gt; int countGreaterThan(T[] anArray, T elem) {
    int count = 0;
    for (T e : anArray)
        if (e &amp;gt; elem)  // compiler error
            ++count;
    return count;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 제네릭 메서드는 &lt;code&gt;if (e &amp;gt; elem)&lt;/code&gt; 구문에서 컴파일 에러가 발생합니다. 대소 비교연산자는 boolean을 제외한 기본 자료형 타입에서만 사용가능합니다. 하지만, T는 타입을 나타내므로 비교연산자를 사용할 수 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(&lt;code&gt;&amp;lt;T extends Integer&amp;gt;&lt;/code&gt;와 같이 Wrapper를 정의해도 타입이므로 연산자를 사용할 수 없습니다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 해결하기 위해서 Comparable 인터페이스를 구현합니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public static &amp;lt;T extends Comparable&amp;lt;T&amp;gt;&amp;gt; int countGreaterThan(T[] anArray, T elem) {
    int count = 0;
    for (T e : anArray)
        if (e.compareTo(elem) &amp;gt; 0)
            ++count;
    return count;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;&amp;lt;T extends Comparable&amp;lt;T&amp;gt;&amp;gt;&lt;/code&gt;는 T라는 타입은 반드시 Comparable 인터페이스를 구현한 클래스임이 보장되므로, compareTo 메서드를 통해 비교연산을 할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✅ Generic&amp;rsquo;s subtyping&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 클래스를 확장하게 되면 부모 클래스 참조 변수를 통해 자식 클래스의 인스턴스를 참조할 수 있습니다. 이는 클래스간의 관계와 인터페이스를 구현한 관게에서 모두 적용되며, 이런 원리를 통해 Dependency Injection을 하기도 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제네릭에서도 타입 인수로 지정한 타입과 호환되는 다른 타입이 있다면 해당 타입을 이용할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;Box&amp;lt;Number&amp;gt; box = new Box&amp;lt;Number&amp;gt;();
box.add(new Integer(10));   // OK
box.add(new Double(10.1));  // OK&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 다음과 같은 경우에는 subtyping을 지원하지 않습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public void boxTest(Box&amp;lt;Number&amp;gt; n) { /* ... */ }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;boxTest(&amp;hellip;) 메서드의 매개인자로 &lt;code&gt;Box&amp;lt;Integer&amp;gt;&lt;/code&gt; 혹은 &lt;code&gt;Box&amp;lt;Double&amp;gt;&lt;/code&gt;을 넘겨줄 수 있을것 같지만 &lt;code&gt;Box&amp;lt;Integer&amp;gt;&lt;/code&gt;, &lt;code&gt;Box&amp;lt;Double&amp;gt;&lt;/code&gt;은 &lt;code&gt;Box&amp;lt;Number&amp;gt;&lt;/code&gt;의 하위 유형이 아니기 때문에 불가능합니다. 즉 3개의 타입은 모두 다른 존재 입니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/wpdlzhf159/post/f3909b95-f764-4679-8436-6f65ce172be7/image.png&quot; alt=&quot;&quot; width=&quot;459&quot; height=&quot;305&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 제네릭 클래스, 인터페이스를 상속관계로 정의하고 싶다면 인터페이스 및 클래스를 확장시키면 됩니다.여기서 확장시킨다는것의 의미는 interface extends를 말하며, 클래스의 경우 abstract class를 통해 확장시킬 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/wpdlzhf159/post/1f99bc38-64c5-4e12-8c47-35ed57d3d9f0/image.png&quot; alt=&quot;&quot; width=&quot;348&quot; height=&quot;275&quot; /&gt;&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;interface PayloadList&amp;lt;E,P&amp;gt; extends List&amp;lt;E&amp;gt; {
  void setPayload(int index, P val);
  ...
}

 abstract class PayloadList&amp;lt;E,P&amp;gt; implements List&amp;lt;E&amp;gt; {
  abstract void setPayload(int index, P val);
  ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 정의하면 PayloadList는 List의 서브타입이 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/wpdlzhf159/post/3378e10f-6593-4dfa-a5f8-0c1e061a4bf1/image.png&quot; alt=&quot;&quot; width=&quot;692&quot; height=&quot;169&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✅ Wildcards&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;와일드카드는 &lt;code&gt;?&lt;/code&gt;로 사용되며 unknown 타입을 의미합니다. 일반적으로 다양한 상황에서 사용되지만 제네릭 메서드 호출, 제네릭 클래스의 인스턴스 생성, 슈퍼타입에서 타입인자로 와일드카드를 사용하지 않기를 권고하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Unbounded Wildcards&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;?&lt;/code&gt; 하나만 사용하게 되면 바인딩되지 않은 와일드카드가 됩니다. unbounded wildcards가 유용하게 사용되는 시나리오는 두 개 정도 존재합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Object 클래스에서 제공하는 기능을 사용하여 구현하는 메서드&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;제네릭 클래스의 타입 인자에 의존하지 않는 메서드의 경우(List.size()나 List.clear()와 같은 메서드)&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 List타입의 모든 요소를 출력해야 하는 메서드의 경우 다음과 같이 unbounded wildcards를 이용할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;public static void printList(List&amp;lt;?&amp;gt; elements) {
        for (Object element : elements) {
                System.out.println(element);
        }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 elements 인자의 타입이 만약 &lt;code&gt;List&amp;lt;Object&amp;gt;&lt;/code&gt;라면 &lt;code&gt;List&amp;lt;Integer&amp;gt;&lt;/code&gt;, &lt;code&gt;List&amp;lt;Double&amp;gt;&lt;/code&gt;과 같이 Object가 아닌 타입들은 인자로 전달될 수 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;&amp;lt;?&amp;gt;&lt;/code&gt;는 마치 &lt;code&gt;&amp;lt;Object&amp;gt;&lt;/code&gt;처럼 보이기도 합니다. 하지만, &lt;code&gt;List&amp;lt;Object&amp;gt;&lt;/code&gt;로 예시를 둘 경우 해당 객체 및 모든 객체들을 리스트에 삽입할 수 있지만, &lt;code&gt;List&amp;lt;?&amp;gt;&lt;/code&gt;에는 오직 null만을 삽입할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 ?라는 의미가 Unknown이라는 의미로 타입이 정의되지 않았고, 어떤 타입도 확실하지 않기 때문입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Uppder Bounded Wildcards&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;와일드 카드를 통해 상한 경계를 지정할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 Number, Integer, Double 타입에서 모두 이용할 수 있는 메서드를 만들고 싶다면 다음과 같이 메서드를 만들 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;class Test {

    void printList(List&amp;lt;? extends Number&amp;gt; values) {
        for (Number value : values) {
            System.out.println(value);
        }
    }

    public static void main(String[] args) {
        Test test = new Test();
        List&amp;lt;Integer&amp;gt; integers = new ArrayList&amp;lt;&amp;gt;(List.of(1, 2, 3));
        test.printList(integers);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;? extends Number&lt;/code&gt;는 해당 unknown 타입이 최대 Number 클래스이거나 Number 클래스의 자식이어야 함을 나타내는 타입의 상한을 지정합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같이 자식관계(혹은 부모관계)를 갖는 클래스들이 있을때&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;class MyGrandParent {

}

class MyParent extends MyGrandParent {

}

class MyChild extends MyParent {

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 메서드를 실행시키게 되면 컴파일 에러가 발생합니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;void printCollection(Collection&amp;lt;? extends MyParent&amp;gt; c) {
    // 컴파일 에러: java: incompatible types: capture#1 of ? extends chapter21.MyParent cannot be converted to chapter21.MyChild
    for (MyChild e : c) {
        System.out.println(e);
    }

    for (MyParent e : c) {
        System.out.println(e);
    }

    for (MyGrandParent e : c) {
        System.out.println(e);
    }

    for (Object e : c) {
        System.out.println(e);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 의미상 &lt;code&gt;? extends MyParent&lt;/code&gt; 라는것이 MyParent의 하위 클래스들은 모두 허용이 되어야 하지 않을까? 생각될 수 있습니다. 의미적으로 클래스의 상한선에 제한을 두는것이기도 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 MyParent의 자식 클래스인 MyChild를 이용해 출력하려고 하면 컴파일 에러가 발생합니다.&lt;br /&gt;우선 &lt;code&gt;? extends MyParent&lt;/code&gt;가 가능한 타입은 MyParent와 unknown한 모든 MyParent의 자식 클래스입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 unknown child class라는 것의 의미는 다시말해 자식의 타입중에서 어떤 타입인지 확실하지 않다는 의미가 됩니다. 즉, 해당 타입이 MyChild 일수도 있지만 아닐수도 있다는 의미입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 적어도 MyParent타입이 된다는것은 확실하므로 해당 타입 혹은 해당 부모 타입으로 꺼내는 것은 문제가 없습니다.&lt;br /&gt;(인터페이스 타입으로 인터페이스의 구현체를 참조하는 경우를 생각해보면 동일합니다!)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 가지고 있는 원소를 사용 혹은 소모(consume)하여 컬렉션에 추가하는 경우에는 이야기가 조금 달라집니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;void addElement(Collection&amp;lt;? extends MyParent&amp;gt; c) {
    c.add(new MyChild());        // 불가능(컴파일 에러)
    c.add(new MyParent());       // 불가능(컴파일 에러)
    c.add(new MyGrandParent());  // 불가능(컴파일 에러)
    c.add(new Object());         // 불가능(컴파일 에러)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;? extends MyParen&lt;/code&gt;타입인 c는 MyParent이거나 모든 MyParent의 자식 클래스 중 하나입니다. 결국 MyParent 혹은 MyParent의 하위 타입 중에서 어떤 타입인지 결정할 수 없기 때문에 위의 모든 add가 컴파일에러가 발생합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MyGrandParent가 안되는 이유도 MyParent보다 더 상위 타입이 되므로 더 구체적인 클래스(MyParent)가 덜 구체적인 클래스(MyGrandParent)를 참조할 수 없기에 불가능합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 위의 코드와 같이 원소를 소모하는 경우에는 상한 경계가 아닌 하한 경계를 지정해 최소한 MyParent 타입임을 보장시키면 문제를 해결할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Lower Bounded Wildcards&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하한 경계 와일드카드는 unknown type을 특정 타입 또는 해당 타입의 상위 타입으로 제한 하는데 &lt;code&gt;? super A&lt;/code&gt; 와 같이 작성합니다. 상한 경계와 하한 경계모두 와일드카드를 통해 지정할 수 있지만, 둘 다 지정할 수는 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상한 경계와 마찬가지로 원소를 사용(consume)하여 컬렉션에 추가하는 코드를 하한 경계로 지정해보겠습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;void addElement(Collection&amp;lt;? super MyParent&amp;gt; c) {
    c.add(new MyChild());
    c.add(new MyParent());
    c.add(new MyGrandParent());  // 불가능(컴파일 에러)
    c.add(new Object());         // 불가능(컴파일 에러)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;? super MyParent&lt;/code&gt;를 통해 컬렉션 c가 갖는 타입은 적어도 MyParent의 부모 타입들이 됩니다. 따라서 MyParent이거나 MyParent의 자식 타입들은 안전하게 컬렉션에 추가할 수 있습니다.(덜 구체적인것이 구체적인것을 참조)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, 부모 타입인 경우에는 부모 타입중에서 어떤 타입인지 확실하지 않으므로 불가능합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 컬렉션에서 값을 꺼내서 원소를 만드는 produce하는 코드를 살펴보겠습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;void printCollection(Collection&amp;lt;? super MyParent&amp;gt; c) {
        // 컴파일 에러
        for (MyChild e : c) {
            System.out.println(e);
        }
                // 컴파일 에러
        for (MyParent e : c) {
            System.out.println(e);
        }
                // 컴파일 에러
        for (MyGrandParent e : c) {
            System.out.println(e);
        }

        for (Object e : c) {
            System.out.println(e);
        }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;c안에 존재하는 원소는 MyParent 타입이거나 부모 타입이지만, 해당 타입들 중에서 어떤 타입인지 특정지을 수 없습니다. 따라서 모든 부모 타입인 경우에서 컴파일 에러가 발생합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 구체적인 자식 타입이 덜 구체적인 MyParent 타입을 참조할 수 없으므로 자식 타입또한 컴파일 에러가 발생합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면에 모든 타입은 Object의 하위 타입이므로 예외적으로 Object는 c의 원소들을 참조할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;PECS(Producer-Extends, Consumer-Super) 공식&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이펙티브 자바에서는 PECS 공식을 사용하기를 권장합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 와일드카드 타입의 객체를 생성 및 만들게되면 extends를, 갖고 있는 객체를 컬렉션에 사용 또는 소비(consume)하게 되면 super를 사용하라는 공식입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;void printCollection(Collection&amp;lt;? extends MyParent&amp;gt; c) {
    for (MyParent e : c) {
        System.out.println(e);
    }
}

void addElement(Collection&amp;lt;? super MyParent&amp;gt; c) {
    c.add(new MyParent());
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;printCollection&lt;/code&gt; 메서드의 경우 컬렉션에서 원소를 꺼내면서 와일드카드 타입의 객체들을 생성하고 있습니다.(produce)&lt;br /&gt;&lt;code&gt;addElement&lt;/code&gt; 메서드의 경우 컬렉션에 원소를 추가함으로써 객체를 사용(consume)하고 있습니다.&lt;br /&gt;따라서 전자의 경우에는 extends가 후자의 경우에는 super를 사용하는것이 적절합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;와일드카드와 서브타입&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제네릭 타입은 단순히 타입 간의 부모, 자식 관계가 있다고 해서 서로 연관되지 않습니다.&lt;br /&gt;하지만, 와일드카드를 사용하면 제네릭 타입 간의 관계를 만들 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 같이 부모, 자식 간의 클래스 관계가 존재할때&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;class MyParent {

}

class MyChild extends MyParent {

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같이 부모 타입으로 자식 타입을 참조할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;MyChild child = new MyChild();
MyParent parent = child; // 가능&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 다음과 같이 제네릭을 사용하는 코드는 불가능합니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;List&amp;lt;MyChild&amp;gt; childs = new ArrayList&amp;lt;&amp;gt;();
List&amp;lt;MyParent&amp;gt; parents = childs; // 컴파일 에러&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;List&amp;lt;Child&amp;gt;&lt;/code&gt;와 &lt;code&gt;List&amp;lt;Parent&amp;gt;&lt;/code&gt; 사이의 공통적인 상위 타입은 &lt;code&gt;List&amp;lt;?&amp;gt;&lt;/code&gt;와 같이 Unknown 타입으로 정의할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;List&amp;lt;? extends MyChild&amp;gt; childs = new ArrayList&amp;lt;&amp;gt;();
List&amp;lt;? extends MyParent&amp;gt; parents = childs; // 가능 &amp;lt;? extends MyChild&amp;gt;는 &amp;lt;? extends MyParent&amp;gt;의 서브타입 입니다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;와일드카드 캡처와 헬퍼 메서드&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴파일러는 와일드카드의 유형을 유추하기도 합니다. &lt;code&gt;List&amp;lt;?&amp;gt;&lt;/code&gt;와 같이 정의했어도 컴파일러는 특정 유형을 유추하게 됩니다. 이를 와일드카드 캡처라고 합니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;void foo(List&amp;lt;?&amp;gt; i) {
    i.set(0, i.get(0)); // java: incompatible types: java.lang.Object cannot be converted to capture#1 of ?
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴파일러는 i 를 &lt;code&gt;Object 타입인것으로 간주하고 처리&lt;/code&gt;합니다. List.set메서드에서는 컴파일러가 해당 리스트에 삽입하는 객체의 유형을 확인할 수 없기때문에 컴파일 에러가 발생합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 이를 와일드카드 헬퍼 메서드를 통해 해결할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;void foo(List&amp;lt;?&amp;gt; i) {
    fooHelper(i);
}

// 와일드카드를 캡쳐할 수 있습니다.
private &amp;lt;T&amp;gt; void fooHelper(List&amp;lt;T&amp;gt; l) {
    l.set(0, l.get(0));
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;fooHelper 제네릭 메서드는 와일드카드를 캡처하는 메서드입니다. 이를 통해 와일드카드의 타입을 사용자가 임의로 지정하여 문제를 해결할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;공식문서에서 제공하는 와일드카드 가이드라인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;언제 상한 경계 와일드카드를 사용할지 혹은 하한 경계 와일드카드를 사용해야할지 구분하기 어렵고 혼란스럽습니다. 오라클 공식문서에서는 이런 부분에서 간단하게 가이드라인을 제공해주고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;In variable&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;in&lt;/code&gt; 변수는 코드에 데이터를 제공합니다. 만약 &lt;code&gt;copy(src, dest)&lt;/code&gt;와 같은 복사메서드에 2개의 인수가 있다면 &lt;code&gt;src&lt;/code&gt;는 복사할 데이터를 제공하므로 &lt;code&gt;in 변수&lt;/code&gt;가 됩니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Out variable&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;out&lt;/code&gt; 변수는 다른 곳에서 사용할 데이터를 보유합니다. &lt;code&gt;dest 인수&lt;/code&gt;가 데이터를 받아들이게 되므로 out 변수가 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 변수가 존재할때 다음과 같이 가이드라인을 제공합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;In 변수는 extends 키워드를 사용하여 상한 와일드카드로 정의합니다.&lt;/li&gt;
&lt;li&gt;out 변수는 super 키워드를 사용해 하한 와일드카드로 정의합니다.&lt;/li&gt;
&lt;li&gt;Object 클래스에 정의된 메서드를 사용해 In 변수에 액세스할 수 있는 경우, 제한없는 와일드카드(?)를 사용합니다.&lt;/li&gt;
&lt;li&gt;코드가 In, Out 변수를 모두 액세스 해야 하는 경우 와일드카드를 사용하지 않습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;위 가이드라인은 메서드 반환 유형에는 적용하지 않습니다. (와일드카드를 반환 유형으로 사용하면 코드를 사용하는 사용자가 와일드카드를 처리해주어야 합니다.)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  References&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.oracle.com/javase/tutorial/java/generics/index.html&quot;&gt;Oracle Docs - Generics&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://mangkyu.tistory.com/241&quot;&gt;[Java] 제네릭과 와일드카드 타입에 대해 쉽고 완벽하게 이해하기 - 망나니개발자&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://medium.com/@joongwon/java-java%EC%9D%98-generics-604b562530b3&quot;&gt;Java의 Generics - 백중원&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Java</category>
      <category>Generic</category>
      <category>Java</category>
      <author>HiiWee</author>
      <guid isPermaLink="true">https://hiiwee.tistory.com/44</guid>
      <comments>https://hiiwee.tistory.com/44#entry44comment</comments>
      <pubDate>Mon, 12 Jun 2023 03:33:00 +0900</pubDate>
    </item>
    <item>
      <title>[스터디2] 마무리!</title>
      <link>https://hiiwee.tistory.com/43</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;JSCODE에서 진행했던 백엔드 프로젝트 클래스는 종료됐습니다!&lt;br /&gt;(아직 미션은 전부 구현하지 못했지만 스터디가 끝나서 회고록을 부랴부랴 작성합니다.. 남은 미션은 계속 진행할 예정입니다. ㅠㅠ!)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스터디에 참여하기 전에 간단한 토이 프로젝트를 만들고 있었습니다.&lt;br /&gt;사실 예전부터 만들어 오던 것이었기에 스터디에 참여하게 되면 또다시 프로젝트의 기한이 늘어날 것 같아 참여할지 말지에 대한 고민이 많았습니다. 결과적으로는 참여하길 정말 잘했다는 생각입니다. ㅎ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;아쉽다 아쉬워..&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스터디를 진행하면서 정말 열심히 했다고 자부할 수 있지만, 그런데도 아쉬운 부분이 조금 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째로는 시간 약속을 지키지 못한 부분이 컸습니다.&lt;br /&gt;미션 자체는 어렵지 않았으나, 미션을 진행하기 위해서 먼저 공부를 하거나, 혹은 현재 만든 부분에서 보완하거나 리팩토링해야 하는 부분들은 없을지 고민하고 변경하는 시간이 길어졌습니다. 결과적으로는 지금 미션을 전부 진행하지 못했지만, 이렇게 회고록을 쓰고 있습니다.  &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금은 단순히 스터디니까 미션을 완수하지 못한다면 온전히 제 손해지만, 마감 기한이 있었던 프로젝트였다면 우리 팀원 전부가 손해를 보는 일이 생기니.. 시간은 반드시 지켜야겠다는 생각입니다... 반성합니다 ㅠ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;나는 강제성이 있어야 잘 되더라고요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래도 스터디를 진행하면서 만족스러운 부분도 많았습니다.&lt;br /&gt;개인적으로 진행하던 프로젝트에서는 새롭게 적용해보고 싶은 기술이 있다면 정말 개념이란 개념은 다 찾아보고, 이리 재보고 저리 재보던 시간이 정말 많았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스터디를 진행하면서는 제한된 시간내에서 새로운 기술이든 알든 기술이든 적용해야 하므로 우선은 구현해야 하는 게 1순위였습니다. 물론 개념 공부도 같이 해주면 더더욱 좋지만, 이런 강제성이 적용되니 생각보다 빠르게 결과물이 나오고, 나온 결과물에서 배우는 점이 정말 많았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 Elastic Beanstalk를 통해 무중단 배포를 진행했을 때, 진행되는 과정의 절반 이상은 잘 이해하지 못했습니다. 어떻게든 돌아가게 하려고 고민하고 삽질했던 시간이 있었지만, 결과적으로 배포에 성공할 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하나의 프로세스를 A to Z까지 완료하고 나니까 세부적인 프로세스들의 내용은 자세히 알지 못해도 어떤 흐름으로 진행되는지 이해할 수 있었습니다. 진행되는 전체적인 프로세스들은 경험해 봤으니 이제는 각 세부 단계들이 어떤 식으로 진행되는지 개념적으로 공부하면 됩니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 혼자서 무엇을 하더라도 스스로 강제성을 부여하고 진행해보려고 합니다 ㅎㅎ&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로운 기술을 &lt;code&gt;강제성&lt;/code&gt;을 통해 일단 돌아가게 만들고, 전체 프로세스를 경험했다면 한 30% 정도 이해한 것이라고 생각합니다. 경험을 먼저 해봤다면 그 후에 부족한 부분을 채우기 위한 개념적인 공부도 꼭 필요하다고 생각됩니다. 저는 일단 완성 시키고 나면, 특히 엄청 힘들게 고생하면서 완성했다면 애증의 관계처럼 꼴도 보기 싫어지는 경우가 종종 있었습니다... 하지만 거기서 그치지 않고 반드시 개념적인 공부를 하여 더욱 깊은 기술적 지식을 얻는 노력을 해야겠습니다!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;내가 작성하는 코드는 이유가 있어야 한다!&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스터디를 진행하면서 스스로 마음먹은 부분이 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 작성하는 코드가 어떤 의미인지 알고 있어야 하며, 해당 코드에 대해 남들이 납득할만한 이유를 꼭 생각하고 고민해봐야 한다는 조건이었는데 스터디를 진행하며 큰 도움을 얻을 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 이미 알고 있는 것을 구현할 때보다 새로운 기술을 적용할 때 정말 많은 도움이 됐습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;옛날이라면 단순히 구현을 위해서 복사 붙여넣기를 통해 구현했겠지만, 지금은 단순히 복붙이라도 복붙하는 코드에 대해 어떤 흐름인지, 각 라인은 무엇을 의미하는지 생각해보고, 그래도 이해가 안 된다면 최소한의 이해를 위해 공부했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의식적인 마인드 새로고침은 구현을 완료했을 때 단순히 구현만 한 것보다 훨씬 많은 이해를 할 수 있게 해줬습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;더 잘하고 싶습니다!&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 스터디는 정말 많은 사람이 참여했습니다. 모든 사람의 코드를 읽어본 것은 시간이 날 때면 다른 스터디원들의 코드를 보면서 그분들의 생각과 열정을 간접적으로 경험할 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 많은 사람과 하나의 목적을 가지고 열심히 진행해보니 더 잘하고 싶다는 욕심이 생겼습니다.&lt;br /&gt;요즘 들어 마음속에 고민이 많았는데, 고민만 가지기보단 일단 행동으로 옮겨야겠다는 생각도 듭니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쉽지는 않겠지만, 꾸준히 노력해야겠습니다 ㅎㅎ&lt;/p&gt;</description>
      <category>스터디/Spring Boot 스터디</category>
      <category>스터디</category>
      <category>회고</category>
      <author>HiiWee</author>
      <guid isPermaLink="true">https://hiiwee.tistory.com/43</guid>
      <comments>https://hiiwee.tistory.com/43#entry43comment</comments>
      <pubDate>Fri, 2 Jun 2023 22:47:17 +0900</pubDate>
    </item>
    <item>
      <title>[스터디2] 테스트 리팩토링 및 1:N, N:M관계의 추가 (5회차)</title>
      <link>https://hiiwee.tistory.com/42</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;5회차 미션은 1:N 관계와 N:M관계에 대한 새로운 도메인의 추가 및 요구사항이 추가됐습니다.&lt;br /&gt;또한 리소스를 조회하는 로직이 아닌 저장, 수정, 삭제하는 부분에 대해서 사용자의 로그인 및 인증/인가 작업이 필요했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제는 익명게시판이 아니라 실명게시판이 됐습니다 ㅎㅎ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설명하지 않은 모든 코드는 다음 PR에 있습니다!&lt;br /&gt;&lt;a href=&quot;https://github.com/JSCODE-EDU/project-class-HiiWee/pull/12&quot;&gt;https://github.com/JSCODE-EDU/project-class-HiiWee/pull/12&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✅ 추가된 요구사항에 따른 ERD 변화&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/JSCODE-EDU/project-class-HiiWee/assets/66772624/305d622b-5b5c-4d5c-9700-080d4a355ce6&quot; alt=&quot;board-project (1)&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회원과 댓글, 게시글과 댓글은 모두 1:N의 연관관계를 맺습니다.&lt;br /&gt;1명 이상의 회원은 1개 이상의 게시글을 좋아요할 수 있으므로, 이는 N:M관계가 됩니다.&lt;br /&gt;N:M관계를 표현할 수 도 있지만, 편의를 위해 중간 테이블인 &lt;code&gt;POST_LIKE&lt;/code&gt; 테이블을 생성했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 실제 JPA로 매핑해보면 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// MEMBER
@Entity
@EntityListeners(AuditingEntityListener.class)
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = &quot;member_id&quot;)
    private Long id;

    @Embedded
    private Email email;

    @Embedded
    private Password password;

    @CreatedDate
    private LocalDateTime createdAt;

    protected Member() {
    }
}

// POST
@Entity
@EntityListeners(AuditingEntityListener.class)
public class Post {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = &quot;post_id&quot;)
    private Long id;

    @Embedded
    private Title title;

    @Embedded
    private Content content;

    @CreatedDate
    private LocalDateTime createdAt;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = &quot;member_id&quot;)
    private Member member;

    protected Post() {
    }
}

// COMMENT
@Entity
@EntityListeners(AuditingEntityListener.class)
public class Comment {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = &quot;comment_id&quot;)
    private Long id;

    private Content content;

    @CreatedDate
    private LocalDateTime createdAt;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = &quot;member_id&quot;)
    private Member member;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = &quot;post_id&quot;)
    private Post post;

    protected Comment() {
    }
}

// POST_LIKE
@Entity
public class PostLike {

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

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = &quot;member_id&quot;)
    private Member member;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = &quot;post_id&quot;)
    private Post post;

    protected PostLike() {
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재는 단순히 단방향 연관관계만 맺어주었습니다. 개인적으로 양방향 연관관계는 실제 로직을 작성하면서 필요하다고 생각될때 그때 추가해주는 편이고, 초기 연관관계를 맺을때는 추가하지 않고 있습니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✅ 저장, 수정, 삭제 로직 JWT 인가 작업&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저장, 수정, 삭제 로직에서 JWT 인가 작업이 필요한 부분은&lt;br /&gt;게시글 저장, 게시글 수정, 게시글 삭제, 댓글 작성 기능에서 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;AuthInterceptor를 수정하여 GET 조회 요청은 통과시키기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 GET 메소드를 통해 조회하는 로직을 제외한 나머지 로직들은 전부 인가 작업이 필요하므로 기존에 작성한 AuthInterceptor를 약간 수정하여 이를 적용할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;@Component
public class AuthInterceptor implements HandlerInterceptor {

    private final JwtTokenProvider jwtTokenProvider;

    public AuthInterceptor(final JwtTokenProvider jwtTokenProvider) {
        this.jwtTokenProvider = jwtTokenProvider;
    }

    @Override
    public boolean preHandle(final HttpServletRequest request, final HttpServletResponse response,
                             final Object handler) {
        if (CorsUtils.isPreFlightRequest(request) || isGetRequest(request)) {
            return true;
        }
        if (notExistHeader(request)) {
            throw new JwtException(String.format(&quot;인증을 위한 헤더를 찾을 수 없습니다.:%d&quot;, NO_AUTHORIZATION_HEADER.value()));
        }
        if (JwtTokenExtractor.extractAccessToken(request) == null) {
            throw new JwtException(String.format(&quot;헤더의 포멧이 일치하지 않습니다.:%d&quot;, INVALID_HEADER_FORMAT.value()));
        }
        jwtTokenProvider.validateToken(JwtTokenExtractor.extractAccessToken(request));

        return true;
    }

    private boolean isGetRequest(final HttpServletRequest request) {
        return request.getMethod().equalsIgnoreCase(HttpMethod.GET.name());
    }

    private boolean notExistHeader(final HttpServletRequest request) {
        String authorizationHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
        return Objects.isNull(authorizationHeader);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;isGetRequest()를 추가해 Request Http Method가 GET이라면 그대로 통과시켜주도록 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 WebMvcConfig에서 전체 로직에 대해 해당 인터셉터를 통과하도록 구현하면 됩니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;    @Override
    public void addInterceptors(final InterceptorRegistry registry) {
        registry.addInterceptor(authInterceptor)
                .addPathPatterns(&quot;/**&quot;)
                .excludePathPatterns(&quot;/login&quot;)
                .excludePathPatterns(&quot;/members/signup&quot;);
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;로그인, 회원가입 로직에 대해서는 제외를 시켜주어야 합니다.&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;게시글 저장, 수정, 삭제에 JWT 인가 적용하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변경되는 로직이 거의 동일하므로, 게시글 저장을 예시로 설명하겠습니다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;// 게시글 저장 - Controller
    @PostMapping(&quot;/posts&quot;)
    public ResponseEntity&amp;lt;PostSaveResponse&amp;gt; createPost(@Login final AuthInfo authInfo,
                                                       @Valid @RequestBody final PostSaveRequest postSaveRequest) {
        PostSaveResponse saveResponse = postService.createPost(authInfo, postSaveRequest);
        return ResponseEntity.status(HttpStatus.CREATED).body(saveResponse);
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 시간에 만들어주었던 ArgumentResolver를 이용해 @Login이 붙은 매개인자를 해당 resolver가 resolving 작업을 해주어 Authorization 헤더 payload에서 사용자의 id값을 가져와 AuthInfo 객체를 인자로 념겨줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를통해 Request Header에서 수동으로 JWT를 파싱하지 않아도 단순히 애노테이션만으로 간단하게 payload를 파싱해올 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;    @Transactional
    public PostSaveResponse createPost(final AuthInfo authInfo, final PostSaveRequest postSaveRequest) {
        Member member = findMember(authInfo);
        Post post = Post.builder()
                .title(postSaveRequest.getTitle())
                .content(postSaveRequest.getContent())
                .member(member)
                .build();
        Post savedPost = postRepository.save(post);
        return PostSaveResponse.createPostSuccess(savedPost.getId());
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;받아온 AuthInfo 객체의 id값을 통해 사용자를 조회합니다. 이때 존재하지 않는 사용자에 대한 검증은 findMember() 메서드에서 진행하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PostSaveRequest의 값이 정상적으로 검증된다면 Post객체가 생성되는데 이때 Member를 Post객체에 지정해주어 해당 게시글은 특정 Member가 작성했음을 명시합니다.(연관관계)&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설명하지 않은 게시글 수정, 삭제의 경우에도 위와 비슷하게 AuthInfo 객체를 통해 진행됩니다!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✅ 인수테스트에서 테스트용 데이터 직접 초기화 하기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기존 인수테스트의 문제&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/wpdlzhf159/post/e4ef9e5c-3404-4ccc-9a25-42432902b1d1/image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 인수테스트는 위와 같이 &lt;code&gt;@DirtiesContext&lt;/code&gt;를 사용하고 있었습니다.&lt;br /&gt;또한 내부 요소로 &lt;code&gt;BEFORE_EACH_TEST_METHOD&lt;/code&gt;를 사용하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 인수테스트의 각 테스트 직전에 ApplicationContext를 매번 새로운 컨텍스트를 구성한다는 의미가 됩니다. 이 설정으로 인해 각 테스트는 서로 영향을 주지 않는다는 이점이 있지만, 다른 테스트에 비해 속도가 상당히 느리다는 단점이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더하여 JWT 토큰 인증 작업이 생기면서, 테스트 시작전에 미리 사용자를 등록해놓는다면 단순히 로그인 작업을 통해 토큰만 그때 그때 생성하면 되므로 DB에 직접 테스트 데이터를 초기화 놓을 필요가 있었습니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 @DirtiesContext를 제거하고, 인수테스트에서 사용되는 DB를 매 테스트마다 직접 초기화해보겠습니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;DatabaseCleaner&lt;/h3&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Component
public class DatabaseCleaner implements InitializingBean { // (1)

    @PersistenceContext
    private EntityManager entityManager;

    private List&amp;lt;String&amp;gt; tableNames;

    @Override
    public void afterPropertiesSet() { // (2)
        this.tableNames = entityManager.getMetamodel()
                .getEntities().stream()
                .filter(e -&amp;gt; e.getJavaType().getAnnotation(Entity.class) != null)
                .map(e -&amp;gt; CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, e.getName()))
                .collect(Collectors.toList());
    }

    @Transactional
    public void clear() { // (3)
        entityManager.flush();
        // 제약 조건 무효화 - 데이터를 지울때 외래키, 유일키 등의 제약조건에 영향을 받지 않게 함
        entityManager.createNativeQuery(&quot;SET REFERENTIAL_INTEGRITY FALSE&quot;).executeUpdate();

        // 테이블을 돌면서 데이터 TRUNCATE, 컬럼 ID 시작 값을 1로 초기화
        for (String tableName : tableNames) {
            entityManager.createNativeQuery(&quot;TRUNCATE TABLE &quot; + tableName).executeUpdate();
            entityManager.createNativeQuery(
                    &quot;ALTER TABLE &quot; + tableName + &quot; ALTER COLUMN &quot; + tableName + &quot;_ID RESTART WITH 1&quot;).executeUpdate();
        }

        // 무효화한 제약 조건 다시 TRUE로
        entityManager.createNativeQuery(&quot;SET REFERENTIAL_INTEGRITY TRUE&quot;).executeUpdate();
    }


    @Transactional
    public void insertInitialData() { // (4)
        entityManager.createNativeQuery(
                        &quot;insert into member(email, password, created_at) values('valid01@mail.com', '!qwer123', CURRENT_TIMESTAMP())&quot;)
                .executeUpdate();
        entityManager.createNativeQuery(
                        &quot;insert into member(email, password, created_at) values('valid02@mail.com', '!qwer123', CURRENT_TIMESTAMP())&quot;)
                .executeUpdate();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;(1): DatabaseCleaner는 &lt;code&gt;InitializingBean&lt;/code&gt; 인터페이스를 구현합니다. 이는 모든 빈 팩토리의 초기화 이후 DatabaseCleaner를 동작시키기 위함입니다!&lt;/li&gt;
&lt;li&gt;(2): 해당 메소드는 &lt;code&gt;InitializingBean&lt;/code&gt; 인터페이스가 제공하는 메소드로 모든 빈 프로퍼티 설정 이후에 해당 메서드가 동작하게 됩니다. 여기서는 &lt;code&gt;@Entity&lt;/code&gt;가 붙은 클래스의 이름을 가져와서 DB 규칙에 맞게 TableName -&amp;gt; table_name과 같이 변경하여 DB 조회에서 사용할 수 있도록 합니다.&lt;/li&gt;
&lt;li&gt;(3): 현재 DB 스키마에 적용되어 있는 외래키, 유일키와 같은 제약조건을 무효화 합니다. 이후 (2)에서 구한 Table 이름을 가지고 해당 테이블의 모든 튜플들을 TRUNCATE 작업을 하여 최초 테이블이 생성된 상태로 돌립니다. 이후 AUTO_INCREMENT되는 id값의 시작값을 1로 초기화 합니다. 마지막으로 무효화한 제약조건을 다시 TRUE로 변경해 제약조건이 적용되도록 합니다.&lt;/li&gt;
&lt;li&gt;(4): 커스텀하게 사용될 데이터를 초기화 합니다. 여기서는 사용자의 등록을 미리 하기 위해 2명의 사용자를 미리 등록합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;미리 등록한 사용자를 통해 토큰 발급 - TokenFixture&lt;/h3&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class TokenFixture {

    private static final String AUTHORIZATION_PREFIX = &quot;Bearer &quot;;

    public static String getMemberToken() {
        LoginRequest loginRequest = LoginRequest.builder()
                .email(&quot;valid01@mail.com&quot;)
                .password(&quot;!qwer123&quot;)
                .build();
        return AUTHORIZATION_PREFIX + httpPost(loginRequest, &quot;/login&quot;)
                .jsonPath()
                .getObject(&quot;.&quot;, TokenResponse.class)
                .getToken();
    }

    public static String getOtherMemberToken() {
        LoginRequest loginRequest = LoginRequest.builder()
                .email(&quot;valid02@mail.com&quot;)
                .password(&quot;!qwer123&quot;)
                .build();
        return AUTHORIZATION_PREFIX + httpPost(loginRequest, &quot;/login&quot;)
                .jsonPath()
                .getObject(&quot;.&quot;, TokenResponse.class)
                .getToken();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TokenFixture는 위에서 미리 등록한 사용자의 정보를 이용해 login request 작업을 통해 Token을 발급받고 반환하는 util 클래스입니다. 이를 통해 인수테스트에서 인증/인가가 필요한 부분에서 편리하게 사용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;로그인을 한 사용자만 게시글을 작성할 수 있다&lt;/code&gt;&lt;/p&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;    @DisplayName(&quot;게시글 작성을 할 수 있다.&quot;)
    @Test
    void createPost() {
        // given
        String token = getMemberToken();

        // when
        ExtractableResponse&amp;lt;Response&amp;gt; response = httpPostWithAuthorization(postSaveRequest1, &quot;/posts&quot;, token);
        PostSaveResponse postSaveResponse = response.jsonPath().getObject(&quot;.&quot;, PostSaveResponse.class);

        //then
        assertAll(
                () -&amp;gt; assertThat(response.statusCode()).isEqualTo(HttpStatus.CREATED.value()),
                () -&amp;gt; assertThat(postSaveResponse.getSavedId()).isEqualTo(1L),
                () -&amp;gt; assertThat(postSaveResponse.getMessage()).isEqualTo(&quot;게시글 작성을 완료했습니다.&quot;)
        );
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 테스트의 흐름을 보면&lt;br /&gt;&lt;code&gt;사용자의 로그인&lt;/code&gt; -&amp;gt; &lt;code&gt;토큰 발급&lt;/code&gt; -&amp;gt; &lt;code&gt;발급된 토큰을 통해 게시글 작성&lt;/code&gt; 과 같은 흐름으로 인수테스트가 진행 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;리팩토링 결과&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;리팩토링 이전 테스트 완료 시간 (약 13초 ~)&lt;br /&gt;
&lt;div&gt;&lt;img src=&quot;https://velog.velcdn.com/images/wpdlzhf159/post/0406a08b-9b6a-4a6f-85f2-c5387cb5fd80/image.png&quot; alt=&quot;&quot; /&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;리팩토링 이후 테스트 완료 시간 (약 6초 ~)&lt;br /&gt;
&lt;div&gt;&lt;img src=&quot;https://velog.velcdn.com/images/wpdlzhf159/post/4da84956-c0b6-4baa-8e37-446a472df92b/image.png&quot; alt=&quot;&quot; /&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 테스트에서는 큰 차이가 없어보이지만, 반복테스트가 아닌 테스트에서는 거의 10배 정도의 속도차이가 납니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;❗️트러블 슈팅1 - 인수테스트 ExtractableResponse&amp;lt;Response&amp;gt; 객체에서 json to object가 안되는 현상&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문제 상황&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// 문제가 되는 코드
private TokenResponse getMemberToken() {
    LoginRequest loginRequest = LoginRequest.builder()
            .email(&quot;valid01@mail.com&quot;)
            .password(&quot;!qwer123&quot;)
            .build();
    return httpPost(loginRequest, &quot;/login&quot;)
            .jsonPath()
            .getObject(&quot;.&quot;, TokenResponse.class);
}

// 예외 메시지
Cannot construct instance of `com.example.anonymousboard.auth.dto.TokenResponse` (although at least one Creator exists): cannot deserialize from Object value (no delegate- or property-based Creator)
at [Source: (String)&quot;{&quot;token&quot;:&quot;eyJhbGciOiJIUzUxMiJ9.eyJpZCI6MSwiaWF0IjoxNjg1Mjg0OTAzLCJleHAiOjE2ODUyODg1MDN9.FWcppuALNQHevUnS_hauCSYXxSWzON5fANUvEOZxOfHpC_hiI82rfGBPKpariBAPuvSIhI_UkW_Nelx7FGlLDA&quot;}&quot;; line: 1, column: 2]
com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot construct instance of `com.example.anonymousboard.auth.dto.TokenResponse` (although at least one Creator exists): cannot deserialize from Object value (no delegate- or property-based Creator)
at [Source: (String)&quot;{&quot;token&quot;:&quot;eyJhbGciOiJIUzUxMiJ9.eyJpZCI6MSwiaWF0IjoxNjg1Mjg0OTAzLCJleHAiOjE2ODUyODg1MDN9.FWcppuALNQHevUnS_hauCSYXxSWzON5fANUvEOZxOfHpC_hiI82rfGBPKpariBAPuvSIhI_UkW_Nelx7FGlLDA&quot;}&quot;; line: 1, column: 2]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문제 원인&lt;/b&gt;&lt;br /&gt;jsonPath().getObject()에서는 json을 Object로 역직렬화하는 코드입니다.&lt;br /&gt;기본적으로 역직렬화시에는 기본생성자가 필요하지만, jackson-module-parameter-nmaes 모듈이 기본생성자가 없어도 다른 생성자로 대체해 역직렬화를 가능케 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 TokenResponse 객체의 경우 하나의 필드를 가지고 있고, 하나의 필드만을 가지고 있는 경우에는 예외가 발생합니다. 이를 해결하기 위해서는 매개변수 생성자에 &lt;code&gt;@JsonCreator&lt;/code&gt; 애노테이션을 붙여주거나 기본생성자를 명시적으로 작성해주면 됩니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;해결&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Getter
public class TokenResponse {

    private String token;

    private TokenResponse() { // (1) 기본 생성자를 명시적으로 작성하거나
    }

    @JsonCreator // (2) 매개변수 생성자에 해당 애노테이션을 붙여준다.
    @Builder
    private TokenResponse(final String token) {
        this.token = token;
    }

    public static TokenResponse from(final String token) {
        return new TokenResponse(token);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✅ 특정 게시글 조회 - 모든 댓글(댓글 내용, 작성 시간, 작성자 이메일)을 같이 조회&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;댓글 작성 로직은 간단한 insert이므로 따로 작성하지 않았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요구사항에서 단건 게시글을 조회하게 되면 해당 게시글에 달린 모든 댓글을 조회해야 합니다. 단, 댓글 작성자의 이메일도 포함되어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 단건 게시글 조회 로직은 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;    public PostResponse findPostById(final Long postId) {
        Post post = postRepository.findById(postId)
                .orElseThrow(PostNotFoundException::new);
        return PostResponse.from(post);
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단일 게시글을 조회해 dto로 변환해 컨트롤러에게 반환합니다. 우리는 여기에 댓글이라는 정보도 포함시켜 주어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 댓글 정보를 같이 담아줄 dto를 새로 만들었습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Getter
public class PostDetailResponse {

    private final Long id;
    private final String title;
    private final String content;
    private final LocalDateTime createdAt;
    private final List&amp;lt;CommentResponse&amp;gt; comments;

    @Builder
    private PostDetailResponse(final Long id, final String title, final String content, final LocalDateTime createdAt,
                               final List&amp;lt;CommentResponse&amp;gt; comments) {
        this.id = id;
        this.title = title;
        this.content = content;
        this.createdAt = createdAt;
        this.comments = comments;
    }

    public static PostDetailResponse of(final Post post, final List&amp;lt;Comment&amp;gt; comments) {
        List&amp;lt;CommentResponse&amp;gt; commentResponses = comments.stream()
                .map(CommentResponse::from)
                .collect(Collectors.toList());
        return PostDetailResponse.builder()
                .id(post.getId())
                .title(post.getTitle())
                .content(post.getContent())
                .createdAt(post.getCreatedAt())
                .comments(commentResponses)
                .build();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정적 메서드를 보면 게시글과, 해당 게시글의 모든 댓글을 받아오고 각각 dto로 변환시켜 줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 댓글을 dto로 변환시키는 &lt;code&gt;CommentResponse::from&lt;/code&gt; 로직을 살펴보면 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Getter
public class CommentResponse {

    private final String content;
    private final LocalDateTime createdAt;
    private final String email;

    @Builder
    private CommentResponse(final String content, final LocalDateTime createdAt, final String email) {
        this.content = content;
        this.createdAt = createdAt;
        this.email = email;
    }

    public static CommentResponse from(final Comment comment) {
        return CommentResponse.builder()
                .content(comment.getContent())
                .createdAt(comment.getCreatedAt())
                .email(comment.getWriterEmail())
                .build();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;comment.getWriterEmail()&lt;/code&gt;은 &lt;code&gt;Comment&lt;/code&gt;의 Member 객체에서 email값을 반환합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 dto를 구성했으니 이제 단건 게시글 조회시 모든 댓글을 조회해보겠습니다!&lt;br /&gt;우선 단건 조회 서비스 로직은 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;    public PostDetailResponse findPostDetailById(final Long postId) {
        Post post = postRepository.findById(postId)
                .orElseThrow(PostNotFoundException::new);
        List&amp;lt;Comment&amp;gt; comments = commentRepository.findCommentsByPost(post);
        return PostDetailResponse.of(post, comments);
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CommentRepository를 통해 모든 댓글을 조회하고, dto에게 게시글과 함께 넘겨주며 응답 객체를 생성합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 로직에서 한가지 주의해야 할 부분이 있습니다. comments 리스트에는 N개의 댓글 객체가 존재하고 내부의 @ManyToOne으로 맺은 연관관계는 모두 Lazy Loading으로 설정되어 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, PostDetailResponse을 생성할때 내부에서 N번의 반복을 통해 Member email을 조회하게 되며 N번의 Member 조회 쿼리가 발생합니다. 즉 &lt;code&gt;N+1 문제&lt;/code&gt;가 발생합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;❗️트러블 슈팅2 - 댓글리스트에서 회원 조회시 발생하는 N+1 문제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;commentRepository.findCommentsByPost(..)&lt;/code&gt;를 단순 쿼리 메서드로 구성하고 조회하고 응답 dto를 생성하게 되면 다음과 같이 N번의 회원 조회 쿼리가 발생합니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;2023-06-01 01:24:25.711 DEBUG 79710 --- [nio-8080-exec-2] org.hibernate.SQL                        : 
    select
        member0_.member_id as member_i1_1_0_,
        member0_.created_at as created_2_1_0_,
        member0_.email as email3_1_0_,
        member0_.password as password4_1_0_ 
    from
        member member0_ 
    where
        member0_.member_id=?
2023-06-01 01:24:25.713 DEBUG 79710 --- [nio-8080-exec-2] org.hibernate.SQL                        : 
    select
        member0_.member_id as member_i1_1_0_,
        member0_.created_at as created_2_1_0_,
        member0_.email as email3_1_0_,
        member0_.password as password4_1_0_ 
    from
        member member0_ 
    where
        member0_.member_id=?
2023-06-01 01:24:25.714 DEBUG 79710 --- [nio-8080-exec-2] org.hibernate.SQL                        : 
    select
        member0_.member_id as member_i1_1_0_,
        member0_.created_at as created_2_1_0_,
        member0_.email as email3_1_0_,
        member0_.password as password4_1_0_ 
    from
        member member0_ 
    where
        member0_.member_id=?
2023-06-01 01:24:25.715 DEBUG 79710 --- [nio-8080-exec-2] org.hibernate.SQL                        : 
    select
        member0_.member_id as member_i1_1_0_,
        member0_.created_at as created_2_1_0_,
        member0_.email as email3_1_0_,
        member0_.password as password4_1_0_ 
    from
        member member0_ 
    where
        member0_.member_id=?
2023-06-01 01:24:25.717 DEBUG 79710 --- [nio-8080-exec-2] org.hibernate.SQL                        : 
    select
        member0_.member_id as member_i1_1_0_,
        member0_.created_at as created_2_1_0_,
        member0_.email as email3_1_0_,
        member0_.password as password4_1_0_ 
    from
        member member0_ 
    where
        member0_.member_id=?
2023-06-01 01:24:25.718 DEBUG 79710 --- [nio-8080-exec-2] org.hibernate.SQL                        : 
    select
        member0_.member_id as member_i1_1_0_,
        member0_.created_at as created_2_1_0_,
        member0_.email as email3_1_0_,
        member0_.password as password4_1_0_ 
    from
        member member0_ 
    where
        member0_.member_id=?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 해결하기 위한 방식으로는 &lt;code&gt;fetch join + distinct 키워드&lt;/code&gt;를 선택했습니다.&lt;br /&gt;단건 게시글 조회시 조회하는 댓글의 수는 제한이 없으며(페이징x), 단순히 전체를 조회하면 되므로 간단하게 해결할 수 있는 fetch join을 이용하게 됐습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;전체 댓글 조회 JPQL&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;    @Query(&quot;SELECT distinct c from Comment c left join fetch c.member where c.post = :post&quot;)
    List&amp;lt;Comment&amp;gt; findCommentsByPost(Post post);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 fetch join을 이용하게 되면 N번의 Member.email을 조회하더라도 다음과 같이 1번의 쿼리만 발생합니다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;2023-06-01 01:33:08.863 DEBUG 79751 --- [nio-8080-exec-2] org.hibernate.SQL                        :
select
        post0_.post_id as post_id1_2_0_,
        post0_.content as content2_2_0_,
        post0_.created_at as created_3_2_0_,
        post0_.member_id as member_i5_2_0_,
        post0_.title as title4_2_0_ 
    from
        post post0_ 
    where
        post0_.post_id=?
2023-06-01 01:33:08.931 DEBUG 79751 --- [nio-8080-exec-2] org.hibernate.SQL                        : 
    select
        distinct comment0_.comment_id as comment_1_0_0_,
        member1_.member_id as member_i1_1_1_,
        comment0_.content as content2_0_0_,
        comment0_.created_at as created_3_0_0_,
        comment0_.member_id as member_i4_0_0_,
        comment0_.post_id as post_id5_0_0_,
        member1_.created_at as created_2_1_1_,
        member1_.email as email3_1_1_,
        member1_.password as password4_1_1_ 
    from
        comment comment0_ 
    left outer join
        member member1_ 
            on comment0_.member_id=member1_.member_id 
    where
        comment0_.post_id=?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를통해 댓글 조회시 발생하는 N+1문제를 해결할 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  마치면서&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5회차 미션의 내용 자체의 난이도는 높지 않았다고 생각됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, 인증/인가가 추가되면서 그에따른 테스트에 대한 변동이 정말 많아졌습니다.. 리팩토링과 최적화를하는데 정말 하루종일 고민했던것 같습니다..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의도치 않은 많은 시간을 쓰게되면서 정말 프로젝트의 설계가 중요하다는 생각이 들었습니다.&lt;br /&gt;현재 프로젝트는 어찌보면 3일에 한번씩 요구사항이 변경된다고 볼 수 있습니다. 변경되는 요구사항은 기존 로직들의 변경을 의미했고, 이는 기존에 작성한 테스트 코드들의 일부가 무용지물이 될 수 있다는 의미이기도 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개인적으로 테스트 코드 또한 유지보수 해야하는 산출물이라고 생각하므로 로직이 변경되면 그때그때 테스트 코드들도 같이 변경해주었습니다. 결론은 요구사항이 계속해서 변경되는것만큼 프로젝트 진행에 차질이 생기는일이 없다는 생각이 듭니다..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 프로젝트를 진행하게 된다면, 조금 귀찮고 지루하더라도 프로젝트의 설계 단계를 정말 철저하게 준비하여 요구사항의 변동을 최소화 해야겠다는 생각이 듭니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더하여 변동되는 요구사항에 유연하게 대응할 수 있는 코드를 작성하는 능력도 키워야함을 느꼈습니다.&lt;br /&gt;현재 제가 작성하는 코드는 새로운 요구사항이 추가될수록, 유연하게 대응하지 못하는것 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/wpdlzhf159/post/a38db283-ded5-444e-8dfd-9c3f9d1a70fe/image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 로직은 게시글 단건조회와 게시글 수정 이후 단건 조회에 사용되는 2개의 게시글 단건 조회 비즈니스 로직입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실상 댓글이라는 값의 유무 차이지만, 그로인해 비즈니스 로직이 나뉘게 되고, 또 각각의 비즈니스 로직이 뷰 로직과 강하게 의존되는것 같  보입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약에 새로운 요구사항이 추가되고, 새롭게 반환해야 한다면 위와 같은 구조에서는 단순히 새로운 뷰 로직을 만들고, 그것에 의존되는 새로운 비즈니스 로직을 만들어야 할 것 같다는 생각입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 구조를 조금 더 재사용 가능하고, 유연한 구조로 만들려면 어떻게 해야할지 많은 고민이 들었던 미션이었습니다..!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;레퍼런스&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a title=&quot;jackson을 이용한 data binding 이해하기&quot; href=&quot;https://beaniejoy.tistory.com/76&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;jackson을 이용한 data binding 이해하기&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;color: #2e2e2e; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://newwisdom.tistory.com/95&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;@DirtiesContext로 무거워진&lt;span&gt;&amp;nbsp;&lt;/span&gt;인수 테스트 시간을 줄이는 실험을 해봅시다&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://velog.io/@jinyoungchoi95/JPA-%EB%AA%A8%EB%93%A0-N1-%EB%B0%9C%EC%83%9D-%EC%BC%80%EC%9D%B4%EC%8A%A4%EA%B3%BC-%ED%95%B4%EA%B2%B0%EC%B1%85#%EC%9D%BC%EB%B0%98%EC%A0%81%EC%9D%B8-fetch-join&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;JPA 모든 N+1 발생 케이스과 해결책&lt;/a&gt;&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;&amp;nbsp;&lt;/div&gt;</description>
      <category>스터디/Spring Boot 스터디</category>
      <category>JPA</category>
      <category>N+1</category>
      <category>spring boot</category>
      <category>스터디</category>
      <category>인수 테스트</category>
      <category>테스트 코드 최적화</category>
      <author>HiiWee</author>
      <guid isPermaLink="true">https://hiiwee.tistory.com/42</guid>
      <comments>https://hiiwee.tistory.com/42#entry42comment</comments>
      <pubDate>Mon, 29 May 2023 05:08:00 +0900</pubDate>
    </item>
    <item>
      <title>[스터디2] JWT 활용한 회원가입, 로그인 기능 구현 (4회차)</title>
      <link>https://hiiwee.tistory.com/41</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;4회차 미션은 회원 엔티티를 추가하고 &lt;code&gt;회원가입&lt;/code&gt;, &lt;code&gt;로그인&lt;/code&gt;, &lt;code&gt;내 정보 조회&lt;/code&gt;기능을 새롭게 추가해야 합니다.&lt;br /&gt;로그인을 할때는 JWT를 사용하며 내 정보 조회시 요청 헤더에 반드시 유효한 토큰의 정보가 있어야 합니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설명하지 않은 모든 코드는 다음 PR에 있습니다!&lt;br /&gt;&lt;a href=&quot;https://github.com/JSCODE-EDU/project-class-HiiWee/pull/9&quot;&gt;https://github.com/JSCODE-EDU/project-class-HiiWee/pull/9&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 회원가입 구현&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;회원가입 기능 요구사항&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;기능 사항&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;input checked=&quot;checked&quot; disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; 회원가입 시 &lt;code&gt;이메일&lt;/code&gt;, &lt;code&gt;패스워드&lt;/code&gt;를 받아서, DB에 &lt;code&gt;이메일&lt;/code&gt;, &lt;code&gt;패스워드&lt;/code&gt;, &lt;code&gt;회원 가입 시간&lt;/code&gt;을 저장해야 한다.&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;checked&quot; disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; 유저에 대한 정보가 저장될 때, &lt;code&gt;id&lt;/code&gt;(PK, primary key)도 같이 Auto-increment 형식으로 저장돼야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;검증 사항&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;input checked=&quot;checked&quot; disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; &lt;code&gt;이메일&lt;/code&gt;에 반드시 &lt;code&gt;@&lt;/code&gt;가 1개만 포함되어 있어야 한다.&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;checked&quot; disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; &lt;code&gt;이메일&lt;/code&gt;에 공백이 포함될 수 없다.&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;checked&quot; disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; 중복된 &lt;code&gt;이메일&lt;/code&gt;이 존재할 수 없다.&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;checked&quot; disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; &lt;code&gt;패스워드&lt;/code&gt;에 공백이 포함될 수 없다.&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;checked&quot; disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; &lt;code&gt;패스워드&lt;/code&gt;는 8자 이상 15자 이하여야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Member 엔티티&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회원가입을 구현하기 위한 Member 엔티티는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Entity
@EntityListeners(AuditingEntityListener.class)
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = &quot;member_id&quot;)
    private Long id;

    @Embedded
    private Email email;

    @Embedded
    private Password password;

    @CreatedDate
    private LocalDateTime createdAt;

    protected Member() {
    }

    @Builder
    private Member(final String email, final String password) {
        this.email = Email.from(email);
        this.password = Password.from(password);
    }

    public Long getId() {
        return id;
    }

    // Java Bean Property 규약을 지키자!
    public String getEmail() {
        return email.getValue();
    }

    public String getPassword() {
        return password.getValue();
    }

    public LocalDateTime getCreatedAt() {
        return createdAt;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;JPQL을 쿼리 메소드로 변경하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회원가입을 구현하면서 이미 존재하는 이메일을 검색할때 작성했던 쿼리 메소드에서 특정 속성을 찾을 수 없다는 오류를 만났었습니다! 사실 원인은 간단했지만, 게시글 검색에서도 이와 동일한 이슈가 있었기에 정리해 봅니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// 다음과 같이 사용하길 원함
        Optional&amp;lt;Member&amp;gt; findByEmailValueAndPasswordValue(String email, String password);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전에 JPQL로 작성했던 Post의 Title 값 객체의 String 값으로 키워드를 조회하는 미션이 있었습니다.&lt;br /&gt;이번에도 회원가입시 이미 존재하는 email 검증을 위해 Email 값 객체의 String값으로 존재하는 메일을 조회해야 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, ApplicationContextException이 발생하면서 쿼리 메소드로 변환할때 emailValue라는 필드를 찾을 수 없다는 예외를 만났습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;IllegalArgumentException: Failed to create query for method public abstract java.util.Optional
com.example.anonymousboard.member.repository.MemberRepository.findByEmailValueAndPasswordValue(java.lang.String,java.lang.String)! 
Unable to locate Attribute  with the the given name [emailValue] on this ManagedType [com.example.anonymousboard.member.domain.Member]&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/wpdlzhf159/post/6459ff36-2cf5-48f0-ba98-1e8fbe77022c/image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오류의 원인은 단순했지만, 이유가 불분명 했습니다. Email은 위와 같이 값 객체로 선언하고 있기에 객체 탐색을 하게 된다면 email -&amp;gt; value가 맞았는데 여기서는 emailValue라는 하나의 속성을 찾지 못한다는 오류가 발생했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 다음 문서에서 원인을 유추할 수 있었습니다. &lt;a href=&quot;https://www.baeldung.com/the-persistence-layer-with-spring-data-jpa#1-automatic-custom-queries&quot;&gt;https://www.baeldung.com/the-persistence-layer-with-spring-data-jpa#1-automatic-custom-queries&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA의 쿼리 메소드는 getter, setter가 없고 필드만 존재하는 엔티티도 조회할 수 있다. 하지만, getter를(setter도?)정의하게 되면 Spring Data Jpa는 해당 getter를 이용해 속성의 이름을 유추하고 있었기에 getEmailValue에서 Java Bean Property 규약에 의해 emailValue라는 값을 찾으려고 하기 때문에 예외가 발생했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 단순하게 getter의 이름을 getEmail로 실제 필드명과 일치하도록 변경하면 깔끔하게 쿼리 메소드로 로직을 구현할 수 있었습니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/wpdlzhf159/post/79af216d-9ca2-4f89-97fd-50e0ffe8b482/image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;회원 이메일, 비밀번호 검증시 정규표현식 이용&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/wpdlzhf159/post/e9aed179-b510-406c-8677-cf435193128e/image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회원가입을 할때는 위와 같이 비밀번호 확인과 일치하는지와, 유니크한 이메일인지를 확인하는 절차 외에도&lt;br /&gt;이메일 형식, 패스워드 형식에 대한 검증사항이 존재합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;검증사항에 해당되는 내용이 크게 복잡하지 않았으므로, 정규 표현식을 이용해 간단하게 검증할 수 있습니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;이메일 검증&lt;/b&gt;&lt;br /&gt;
&lt;div&gt;&lt;img src=&quot;https://velog.velcdn.com/images/wpdlzhf159/post/84eba9a2-3460-4a07-9bd2-ed887aaf2f81/image.png&quot; alt=&quot;&quot; /&gt;&lt;/div&gt;
&lt;br /&gt;&lt;code&gt;^[a-z]{1}[a-z0-9_\\.]+@[a-z\\.]+\\.[a-zA-Z]+$&lt;/code&gt;&lt;br /&gt;첫글자는 반드시 영어로 시작되어야 하며, 1개의 &lt;code&gt;@&lt;/code&gt;, &lt;code&gt;.&lt;/code&gt;가 필요합니다. 아이디 형식은 소문자 + 숫자만 가능합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;비밀번호 검증&lt;/b&gt;&lt;br /&gt;
&lt;div&gt;&lt;img src=&quot;https://velog.velcdn.com/images/wpdlzhf159/post/da7f6208-c771-4043-af63-980d67226e59/image.png&quot; alt=&quot;&quot; /&gt;&lt;/div&gt;
&lt;br /&gt;&lt;code&gt;^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&amp;amp;])[A-Za-z\d@$!%*#?&amp;amp;]{8,15}$&lt;/code&gt;&lt;br /&gt;8 ~ 15글자로 제한되며 반드시 1개이상의 영어, 숫자, 지정된 특수문자(@$!%*#?&amp;amp;)가 포함되어야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. JWT를 이용한 로그인&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;로그인 요구사항&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;기능 사항&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;input checked=&quot;checked&quot; disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; 로그인 시 &lt;code&gt;이메일&lt;/code&gt;, &lt;code&gt;패스워드&lt;/code&gt; 값을 받는다.&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;checked&quot; disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; 로그인에 성공했을 때, JWT를 활용해 Access Token 값을 응답해야 한다.&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;checked&quot; disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; JWT의 payload에는 사용자의 &lt;code&gt;id&lt;/code&gt;(PK, primary key)가 반드시 담겨있어야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;JWT 개요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JWT는 Json Web Token으로 세션기반 로그인과 달리 토큰 기반 인증을 하므로, 서버에 사용자의 상태를 굳이 저장하지 않아도 되기에 stateless한 방식입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JWT는 &lt;code&gt;Header&lt;/code&gt;, &lt;code&gt;Payload&lt;/code&gt;, &lt;code&gt;Signature&lt;/code&gt;로 구성되어 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Header: 토큰의 타입, JWT 서명을 생성할때 사용되는 알고리즘을 포함합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code class=&quot;language-json&quot;&gt;{
&quot;typ&quot;: &quot;JWT&quot;,
&quot;alg&quot;: &quot;HS512&quot; }&lt;/code&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Payload: 사용자에 대한 정보인 Claim을 담습니다. JWT에서 제공하는 형식도 있지만, 사용자가 커스텀하게 Claim을 만들어 담을 수 있습니다.&lt;br /&gt;
&lt;div&gt;&lt;img src=&quot;https://velog.velcdn.com/images/wpdlzhf159/post/a62666fd-4205-431b-804d-51800c12f6cc/image.png&quot; alt=&quot;&quot; /&gt;&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;standard claims&lt;br /&gt;&lt;img src=&quot;https://velog.velcdn.com/images/wpdlzhf159/post/1b8fb6c5-f5cf-4e5f-9dce-9b24e51d7f45/image.png&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Signature: header를 인코딩한 값, payload를 인코딩 한 값을 합친후 백엔드 서버에서 들고있는 개인키를 통해 암호화 되어있습니다. 결국 서버에 존재하는 개인키를 통해서만 암호화를 풀 수 있습니다.&lt;br /&gt;
&lt;div&gt;&lt;img src=&quot;https://velog.velcdn.com/images/wpdlzhf159/post/5c73fea5-03d9-44ee-980e-cfa90e417e84/image.png&quot; alt=&quot;&quot; /&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 특징 때문에 악성 사용자가 임의로 Header나 Payload값을 변경하여 요청을 보내게 된다면 signature를 다시 디코딩하여 입력받은 Header와 Payload와 비교했을때 다른 값을 가지게 되므로 인증이 불가능해 집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;JWT 의존성 추가&lt;/h3&gt;
&lt;pre class=&quot;clean&quot;&gt;&lt;code&gt;    implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
    runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
    runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;jjwt-api가 실제 코드레벨에서 구현할때 사용되며 나머지 두 개의 의존성은 런타임에 사용됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;로그인 요청부터 응답까지의 흐름 분석&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JWT를 사용하기 앞서 간단하게 로그인 요청의 흐름을 분석하겠습니다.&lt;br /&gt;어찌보면 JWT라는 개념이 처음 사용하는 사람의 입장에서는 상당히 난해하다고 느껴질 수 있습니다. 저 또한 그랬었기에, 현재 구현하고자 하는 로그인 로직의 흐름을 분석해보고 하나씩 구현해보겠습니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;code&gt;로그인 요청&lt;/code&gt;: 사용자의 로그인 요청(이메일, 비밀번호)이 들어옵니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;이메일 검증&lt;/code&gt;: 사용자의 이메일과 비밀번호에 해당되는 회원이 존재하는지 검증합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;토큰 생성&lt;/code&gt;: 2.를 정상적으로 통과했다면 jwt토큰을 생성합니다. 이때 2에서 조회한 사용자의 &lt;code&gt;id&lt;/code&gt;값을 jwt payload에 담습니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;응답 생성&lt;/code&gt;: 생성한 토큰을 통해 응답을 생성하고 클라이언트에게 반환합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1, 2, 4의 경우 기본적인 비즈니스 로직의 흐름이므로 3에 대해서 좀 더 자세히 알아보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;토큰 생성&lt;/h3&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// AuthService.createToken(...)
    public TokenResponse createToken(final LoginRequest loginRequest) {
        Member member = memberRepository.findByEmailValueAndPasswordValue(loginRequest.getEmail(),
                loginRequest.getPassword()).orElseThrow(LoginFailedException::new);
        String accessToken = jwtTokenProvider.createAccessToken(member.getId());
        return TokenResponse.from(accessToken);
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정상적으로 이메일, 패스워드를 통해 사용자를 정상적으로 조회할 수 있다면 JwtTokenProvider에서 사용자의 id값을 넘겨주면서 토큰을 생성하도록 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;// application-local.yml
security:
  jwt:
    token:
      secret-key: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIiLCJuYW1lIjoiSm9obiBEb2UiLCJpYXQiOjE1MTYyMzkwMjJ9.ih1aovtQShabQ7l0cINw4k1fagApg3qLWiB8Kt59Lno
      expire-length:
        access: 3600000&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Slf4j
@Component
public class JwtTokenProvider {

    private static final String AUTHORIZATION_ID = &quot;id&quot;;

    private final Key signingKey; // (1)
    private final long validityInMilliseconds; // (2)

    // (3)
    public JwtTokenProvider(@Value(&quot;${security.jwt.token.secret-key}&quot;) final String signingKey,
                            @Value(&quot;${security.jwt.token.expire-length.access}&quot;) final long validityInMilliseconds) {
        byte[] keyBytes = signingKey.getBytes(StandardCharsets.UTF_8);
        this.signingKey = Keys.hmacShaKeyFor(keyBytes); // (4)
        this.validityInMilliseconds = validityInMilliseconds;
    }

    public String createAccessToken(final Long id) {
        Date now = new Date();
        Date validity = new Date(now.getTime() + validityInMilliseconds);
        return Jwts.builder() // (5)
                .claim(AUTHORIZATION_ID, id)
                .setIssuedAt(now)
                .setExpiration(validity)
                .signWith(signingKey)
                .compact();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;(1): 서버에 저장된 SecretKey를 java.base.Key라는 객체로 저장합니다.&lt;/li&gt;
&lt;li&gt;(2): 생성된 AccessToken의 유효시간을 가집니다.&lt;/li&gt;
&lt;li&gt;(3): 생성자를 통해 application.yml에서 프로퍼티를 가져옵니다.&lt;/li&gt;
&lt;li&gt;(4): jjwt-api의 keys를 통해 지정된 키 바이트 배열을 기반으로 HMAC-SHA 알고리즘에 사용할 새 SecretKey 인스턴스를 생성합니다. 이때 지정되는 알고리즘의 종류로는 매개인자에 넘어온 키의 길이에 따라 &lt;code&gt;SHA-512&lt;/code&gt;, &lt;code&gt;SHA-384&lt;/code&gt;, &lt;code&gt;SHA-256&lt;/code&gt;중에 하나로 결정됩니다.&lt;/li&gt;
&lt;li&gt;(5): JwtBuilder를 통해 실제 Token을 발행합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;setIssuedAt&lt;/code&gt;: 토큰 발행 시간(현재시간)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;setExpiration&lt;/code&gt;: 토큰 파기 시간(현재시간 + 토큰 유지 시간)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;signWith&lt;/code&gt;: 서버에 존재하는 개인키로 Signature를 만듭니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;compact&lt;/code&gt;: 토큰 발행&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;발행된 토큰은 TokenResponse라는 응답 객체에 감싸지고, Response Body에 담겨져 클라이언트에게 응답하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자의 로그인이 1시간뒤면 무조건 파기되므로, 이를 방지하기 위해 AccessToken보다 유효시간이 훨씬 긴 RefreshToken을 만들기도 합니다. RefreshToken은 액세스 토큰의 유효시간이 지났고, 아직 RefreshToken의 유효시간이 남아있다면 사용자의 액세스 토큰을 새로 만들어 클라이언트에게 응답하게 됩니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이떄 Refresh Token은 DB에 저장하여 보관해야 합니다. 성능적인 이점을 얻기 위해 여기서 Redis를 도입해 Refresh Token을 캐싱해 사용하기도 합니다!(잘은 모르지만..)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Postman을 통해 회원가입 및 로그인을 하게되면 아래 JWT 토큰을 응답받게 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/wpdlzhf159/post/9a468e38-b5d5-421e-9687-54cecd757746/image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;jwt.io에 생성된 토큰값과 서버의 개인키를 붙여넣기하여 결과를 보면 &lt;code&gt;Signature Verified&lt;/code&gt;로 인증된 토큰임을 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/wpdlzhf159/post/4cb524c5-782b-45e5-8074-c0c1d3e8d086/image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 내 정보 조회&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;내 정보 조회 요구사항&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;기능 사항&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;input checked=&quot;checked&quot; disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; 사용자가 요청을 보낼 때 Header에 JWT 토큰을 넘기도록 한다.&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;checked&quot; disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; 응답값에는 &lt;code&gt;id&lt;/code&gt;, &lt;code&gt;이메일&lt;/code&gt;, &lt;code&gt;회원 가입 시간&lt;/code&gt;이 포함되어야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;검증 사항&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;input checked=&quot;checked&quot; disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; Header에 JWT 토큰이 담겨있지 않다면 에러로 응답한다.&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;checked&quot; disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; Header에 담겨있는 JWT 토큰이 올바르지 않거나 조작되었다면 에러로 응답한다.&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;checked&quot; disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; Header에 담겨있는 JWT 토큰의 만료기간이 지났다면 에러로 응답한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내 자신의 정보를 조회하는건 기본적으로 로그인된 사용자만이 조회할 수 있습니다. 현재 로그인은 JWT로 구현했으므로, 로그인이 필요한 인증에서 클라이언트는 서버에서 받은 토큰을 헤더에 포함시켜서 요청을 보내야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 프로젝트는 클라이언트를 구현하고 있지 않으므로, 클라이언트가 요청 헤더에 토큰값을 같이 보내준다고 가정하고 진행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;내 정보 조회 요청부터 응답까지의 흐름 분석&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인과 마찬가지로 내 정보 조회 요청부터 응답까지의 흐름을 분석해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;사용자의 로그인&lt;/code&gt;: 선행조건으로 반드시 로그인 한 상태여야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;code&gt;내 정보 조회 요청&lt;/code&gt;: &lt;code&gt;Authorization&lt;/code&gt;이라는 헤더에 &quot;Bearer &quot; prefix가 붙은 생성된 토큰값을 넣어서 내 정보 조회 요청을 합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;토큰 유효성 확인&lt;/code&gt;: 토큰이 없거나, 올바르지 않거나(조작), 유효시간이 지난 토큰의 경우와 같이 다양한 상황에서 토큰의 유효성이 보장되지 않을 수 있기 때문에 Handler에 가기전에 토큰의 유효성을 검증해야 합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Payload에서 사용자 id값 꺼내기&lt;/code&gt;: 올바른 토큰임이 보장됐다면 토큰을 서버의 개인키로 복호화하여 id값을 꺼내어 handler에게 전달해야 합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;사용자 id를 통해 DB조회&lt;/code&gt;: id값은 PK이므로 해당 값을 이용해 DB조회를 합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;응답 객체 만들고 응답 완료&lt;/code&gt;: 모든 상황이 정상적이었다면 패스워드와 같은 민감정보는 제외하고 클라이언트에게 내 정보에 대한 응답을 넘겨줍니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Security를 사용하고 있지 않으므로, 수동으로 토큰에 대한 유효성과, Payload에서 사용자의 id값을 꺼내야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;토큰에 대한 유효성 검사&lt;/b&gt;&lt;br /&gt;우선 토큰에 대한 유효성 검증으로는 Spring Interceptor를 이용했습니다.&lt;br /&gt;인터셉터의 preHandle()메서드는 디스패처 서블릿 이후, 요청 핸들러 호출 이전에 실행되므로 토큰을 검증하기 적합하고, 더 나아가 ControllerAdvice를 통해 인터셉터에서 발생한 예외를 공통적으로 처리할 수 있습니다. 또한 내 정보 조회 뿐만아니라 여러곳에서 인증/인가에 대한 작업이 추가될 수 있으므로 공통적인 인터페이스를 둘 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;토큰에서 id값 꺼내오기&lt;/b&gt;&lt;br /&gt;토큰에서 id값을 꺼내는 과정역시 여러곳에서 사용될 수 있습니다. (내가 쓴 게시글 조회, 내 댓글 조회 등) 따라서 ArgumentResolver를 이용해 공통적으로 사용할 수 있는 인터페이스를 두었습니다.&lt;br /&gt;커스텀한 애노테이션이 붙은 Argument라면 해당 resolver를 호출하도록 하는 방식으로 사용됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가적으로 Interceptor의 preHandle 호출 이후 실제 handler가 호출되기전에 ArgumentResolver가 호출되므로 ArgumentResolver까지 로직이 진행됐다면 이미 인증/인가를 마친 요청이라고 판단되므로, ArgumentResolver에서 별도의 검증 작업은 수행하지 않았습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 인터셉터와 ArgumentResolver는 다음과 같이 WebMvcConfig에 등록합니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    private static final String ALLOWED_METHOD_NAMES = &quot;GET,HEAD,POST,DELETE,TRACE,OPTIONS,PATCH,PUT&quot;;

    private final AuthInterceptor authInterceptor;
    private final JwtTokenProvider jwtTokenProvider;

    public WebMvcConfig(final AuthInterceptor authInterceptor, final JwtTokenProvider jwtTokenProvider) {
        this.authInterceptor = authInterceptor;
        this.jwtTokenProvider = jwtTokenProvider;
    }

    @Override
    public void addCorsMappings(final CorsRegistry registry) {
        registry.addMapping(&quot;/**&quot;)
                .allowedOrigins(&quot;http://localhost:3000&quot;)
                .allowedMethods(ALLOWED_METHOD_NAMES.split(&quot;,&quot;));
    }

    // 인터셉터 적용 및 적용 uri, 제외 uri설정
    @Override
    public void addInterceptors(final InterceptorRegistry registry) {
        registry.addInterceptor(authInterceptor)
                .addPathPatterns(&quot;/members/me&quot;)
                .excludePathPatterns(&quot;/login&quot;)
                .excludePathPatterns(&quot;/members/signup&quot;)
                .excludePathPatterns(&quot;/posts/**&quot;);
    }

    // ArgumentResolver 등록
    @Override
    public void addArgumentResolvers(final List&amp;lt;HandlerMethodArgumentResolver&amp;gt; resolvers) {
        resolvers.add(authenticationPrincipalArgumentResolver());
    }

    // Bean 수동 등록
    @Bean
    public AuthenticationPrincipalArgumentResolver authenticationPrincipalArgumentResolver() {
        return new AuthenticationPrincipalArgumentResolver(jwtTokenProvider);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;AuthInterceptor&lt;/h3&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;@Component
public class AuthInterceptor implements HandlerInterceptor {

    private final JwtTokenProvider jwtTokenProvider; //(1)

    public AuthInterceptor(final JwtTokenProvider jwtTokenProvider) {
        this.jwtTokenProvider = jwtTokenProvider;
    }

    @Override
    public boolean preHandle(final HttpServletRequest request, final HttpServletResponse response,
                             final Object handler) {
        if (CorsUtils.isPreFlightRequest(request)) { // (2)
            return true;
        }
        if (notExistHeader(request)) { // (3)
            throw new JwtException(String.format(&quot;인증을 위한 헤더를 찾을 수 없습니다.:%d&quot;, NO_AUTHORIZATION_HEADER.value()));
        }
        if (JwtTokenExtractor.extractAccessToken(request) == null) { // (4)
            throw new JwtException(String.format(&quot;헤더의 포멧이 일치하지 않습니다.:%d&quot;, INVALID_HEADER_FORMAT.value()));
        }
        jwtTokenProvider.validateToken(JwtTokenExtractor.extractAccessToken(request));  // (5)

        return true;
    }

    private boolean notExistHeader(final HttpServletRequest request) {
        String authorizationHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
        return Objects.isNull(authorizationHeader);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;(1): JwtTokenProvider에 대한 의존성을 주입받는다.&lt;/li&gt;
&lt;li&gt;(2): CORS 정책에 의거해 만약 들어오는 요청이 Preflight 요청이라면 통과된다.&lt;/li&gt;
&lt;li&gt;(3): Authorization 헤더가 존재하는지의 여부를 확인한다.&lt;/li&gt;
&lt;li&gt;(4): 토큰 extractor를 통해 헤더가 &quot;Bearer &quot;로 시작하는지 검사한다.(따로 설명하진 않겠습니다.)&lt;/li&gt;
&lt;li&gt;(5): JwtTokenProvider에게 토큰이 유효한지 검사한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// JwtTokenProvider.validateToken()
    public void validateToken(final String token) {
        try {
            Jwts.parserBuilder()
                    .setSigningKey(signingKey)
                    .build()
                    .parseClaimsJws(token);
        } catch (UnsupportedJwtException e) {
            log.info(&quot;Unsupported JWT token : {}&quot;, token);
            throw new JwtException(String.format(&quot;지원하지 않는 JWT 토큰 형식:%d&quot;, UNSUPPORTED_JWT.value()));
        } catch (ExpiredJwtException e) {
            log.info(&quot;Expired JWT token : {}&quot;, token);
            throw new JwtException(String.format(&quot;토큰 기한 만료:%d&quot;, EXPIRED_JWT.value()));
        } catch (MalformedJwtException e) {
            log.info(&quot;Invalid JWT token : {}&quot;, token);
            throw new JwtException(String.format(&quot;유효하지 않은 JWT 토큰:%d&quot;, MALFORMED_JWT.value()));
        } catch (SignatureException e) {
            log.info(&quot;Invalid JWT signature : {}&quot;, token);
            throw new JwtException(String.format(&quot;잘못된 JWT 시그니처:%d&quot;, INVALID_SIGNATURE.value()));
        }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 로직은 예외에 작성된 로직을 살펴보면 금방 이해할 수 있습니다. parseClaimJws(...)메서드는 내부적으로 아래와 같은 이미 정의되어 있는 jwt 예외를 발생시키므로 적절하게 이용하면 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/wpdlzhf159/post/b590a96c-9ed9-4120-b070-27f3757ea950/image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 발생되는 예외에 대한 적절한 처리를 할 수 있도록 모두 JwtException으로 예외를 다시 던져주고 있으며 ControllerAdvice에서는 다음과 같이 예외를 처리하고 있습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;    @ExceptionHandler(JwtException.class)
    public ResponseEntity&amp;lt;ErrorResponse&amp;gt; handleJwtException(final JwtException e) {
        String[] messageAndErrorCode = e.getMessage().split(&quot;:&quot;);
        return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
                .body(ErrorResponse.builder()
                        .message(messageAndErrorCode[0])
                        .errorCode(Integer.parseInt(messageAndErrorCode[1]))
                        .build()
                );
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;AuthenticationPrincipalArgumentResolver&lt;/h3&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;public class AuthenticationPrincipalArgumentResolver implements HandlerMethodArgumentResolver {

    private final JwtTokenProvider tokenProvider;

    public AuthenticationPrincipalArgumentResolver(final JwtTokenProvider tokenProvider) {
        this.tokenProvider = tokenProvider;
    }

    // (1)
    @Override
    public boolean supportsParameter(final MethodParameter parameter) {
        return parameter.hasParameterAnnotation(Login.class);
    }

    // (2)
    @Override
    public Object resolveArgument(final MethodParameter parameter, final ModelAndViewContainer mavContainer,
                                  final NativeWebRequest webRequest, final WebDataBinderFactory binderFactory) {
        HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
        String token = JwtTokenExtractor.extractAccessToken(request);
        return tokenProvider.getParsedAuthInfo(token);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;(1): 해당 파라미터에 &lt;code&gt;@Login&lt;/code&gt;애노테이션이 붙어있는지 확인하고 boolean값을 반환한다. 이를 통해 해당 ArgumentResolver가 resolving 할 수 있는 파라미터인지 판단합니다.&lt;/li&gt;
&lt;li&gt;(2): &lt;code&gt;Authorization&lt;/code&gt;헤더에서 토큰값을 꺼내고, TokenProvider를 토큰에서 id값을 뽑아 AuthInfo 객체를 받아오고 리턴합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// JwtTokenProvider의 getParsedAuthInfo 메서드
    public AuthInfo getParsedAuthInfo(final String token) {
        try {
            Jws&amp;lt;Claims&amp;gt; claimsJws = Jwts.parserBuilder()
                    .setSigningKey(signingKey)
                    .build()
                    .parseClaimsJws(token);
            return AuthInfo.from(claimsJws.getBody().get(AUTHORIZATION_ID));
        } catch (ExpiredJwtException e) {
            return AuthInfo.from(e.getClaims().get(AUTHORIZATION_ID));
        }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위 메서드에서는 token값에서 id값을 꺼내고 id를 이용해 AuthInfo 객체를 생성합니다. ArgumentResolver는 인터셉터 이후 실행된다. 따라서 간발의 차이로 토큰의 유효시간이 끝났다는 예외가 발생할 수 있지만, 이미 AuthInterceptor에서 유효한 토큰이라고 검증했으므로, 해당 예외가 발생해도 예외에서 claim을 꺼내와서 객체를 생성하고 반환합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;내 정보 조회 비즈니스 로직&lt;/h3&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// MemberController
    @GetMapping(&quot;/members/me&quot;)
    public ResponseEntity&amp;lt;MyInfoResponse&amp;gt; findMyInfo(@Login AuthInfo authInfo) { // (1)
        MyInfoResponse myInfoResponse = memberService.findMyInfo(authInfo);
        return ResponseEntity.ok(myInfoResponse);
    }

// MemberService
    public MyInfoResponse findMyInfo(final AuthInfo authInfo) {
        Member member = findMemberObject(authInfo);
        return MyInfoResponse.from(member); // (3)
    }
    // (2)
    private Member findMemberObject(final AuthInfo authInfo) {
        return memberRepository.findById(authInfo.getId())
                .orElseThrow(MemberNotFoundException::new);
    }&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;(1): Custom ArgumentResolver는 @Login을 보고 Argument resolving 지원 여부를 판단하고 resolving한 Argument는 AuthInfo 객체가 됩니다.&lt;/li&gt;
&lt;li&gt;(2): id를 통해 회원를 조회합니다. 없다면 not found 예외가 발생합니다.&lt;/li&gt;
&lt;li&gt;(3): 조회한 회원에서 패스워드와 같은 민감정보는 제외하고 응답을 생성합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AuthInterceptor에서 정상적으로 토큰에 대한 검증이 이루어졌고, ArgumentResolver를 통해 요청한 사용자가 누구인지 AuthInfo객체를 통해 받아왔다면 해당 유저를 db에서 조회하고 패스워드를 제외한 값을 응답한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마치면서&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에 JWT를 사용할때는 Spring Security를 걷어내고 사용해봤습니다!&lt;br /&gt;오직 JWT만을 사용하게 되면서 해당 기술의 동작방식과 흐름을 이해할 수 있었던 게기가 됐습니다 ㅎㅎ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 기존 스프링에서 제공하는 훌륭한 기술들로도 충분히 깔끔한 로직을 만들 수 있다는것을 깨달았습니다. 앞으로 새로운 기술의 도입이 필요할때, 기존의 방식으로는 구성할 수 있지 않을까란 고민을 한 번 해보게 될 것 같습니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;레퍼런스&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/jwtk/jjwt&quot;&gt;jjwt 깃허브&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc7519&quot;&gt;RFC 7519&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://velog.io/@jinyoungchoi95/JWTJson-Web-Token-%EC%9D%B8%EC%A6%9D%EB%B0%A9%EC%8B%9D&quot;&gt;JWT(Json Web Token) 인증방식 - Velog&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>스터디/Spring Boot 스터디</category>
      <category>JSON Web Token</category>
      <category>jwt</category>
      <category>로그인</category>
      <category>스터디</category>
      <author>HiiWee</author>
      <guid isPermaLink="true">https://hiiwee.tistory.com/41</guid>
      <comments>https://hiiwee.tistory.com/41#entry41comment</comments>
      <pubDate>Mon, 22 May 2023 11:06:06 +0900</pubDate>
    </item>
    <item>
      <title>[스터디2] Nginx와 Elastic Beanstalk을 통한 간단한 무중단 배포 해보기! (3회차)</title>
      <link>https://hiiwee.tistory.com/40</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;3회차 미션은 지금까지 만들었던 익명 게시판을 배포하는 것입니다!&lt;br /&gt;배포 환경은 AWS Elastic Beanstalk 이용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 Elastic Beanstalk은 EC2, RDS, S3 환경을 동시에 제공해줍니다.&lt;br /&gt;더 나아가 프로비저닝, 로드 밸런싱, Auto Scaling과 같은 기능을 Beanstalk가 제공해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Elastic Beanstalk은 기본적으로 &lt;a href=&quot;https://docs.aws.amazon.com/ko_kr/elasticbeanstalk/latest/dg/nodejs-platform-proxy.html&quot;&gt;Nginx를 이용한 리버스 프록싱을 지원&lt;/a&gt;해줍니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전시간까지는 단순히 CRDU API와 API DOCS를 만들었습니다.&lt;br /&gt;이번 시간은 실제 배포를 위한 Spring Boot에서의 설정과 배포 시스템을 구성하여 무중단 배포를 할 수 있도록 진행해보겠습니다!&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/wpdlzhf159/post/8237e6bd-1c2a-4dda-8257-ec058a38e5c0/image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 Elastic Beanstalk을 생성하려면 EC2 인스턴스를 생성하고 키페어를 발급받아야 합니다!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Cors 설정하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React.js를 사용한다면 &lt;a href=&quot;http://localhost:3000%EC%9D%84&quot;&gt;http://localhost:3000을&lt;/a&gt; 사용합니다.&lt;br /&gt;Tomcat 서버는 기본적으로 &lt;a href=&quot;http://localhost:8080%EC%9D%84&quot;&gt;http://localhost:8080을&lt;/a&gt; 사용하므로&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Protocol, Host는 동일하지만 Port가 달라지므로 서로 다른 출처 Cross Origin으로 간주됩니다.&lt;br /&gt;브라우저는 이들 중 하나라도 다르면 차단됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 이를 해결하는 방안이 Cross-Origin Resource Sharing으로 Cors라고 합니다. 또한 브라우저는 기본적으로 동일 출러(Same-Origin Policy)를 따르고 있으므로 react에서 톰캣으로 보낸 요청이 막히게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Cors를 적용하기 위해서는 Spring Boot에서의 설정을 많이 이용하는데 아무래도 인터페이스와 메소드 하나만 구현하면 간단하게 적용할 수 있기 때문인것 같습니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;WebMvcConfigurer&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot는 WebMvcConfigurer 인터페이스를 제공해주는데 해당 인터페이스에서는 Spring MVC에 대한 설정을 커스텀하게 적용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 &lt;code&gt;void addCorsMappings(CorsRegistry registry)&lt;/code&gt; 메소드를 오버라이딩하여 Cors 정책을 적용할 컨트롤러 매핑과 요청을 허용할 클라이언트의 주소 및 허용 HTTP Method등 다양한 설정을 할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    private static final String ALLOWED_METHOD_NAMES = &quot;GET,HEAD,POST,DELETE,TRACE,OPTIONS,PATCH,PUT&quot;;
    @Override
    public void addCorsMappings(final CorsRegistry registry) {
        registry.addMapping(&quot;/**&quot;) (1)
                .allowedOrigins(&quot;http://localhost:3000&quot;) (2) 
                .allowedMethods(ALLOWED_METHOD_NAMES.split(&quot;,&quot;)); (3)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;(1): Cors 정책을 모든 컨트롤러 매핑에 적용합니다.&lt;/li&gt;
&lt;li&gt;(2): React에서 사용되는 3000번 포트를 허용합니다. (별도의 도메인이 있다면 해당 도메인을 적용)&lt;/li&gt;
&lt;li&gt;(3): &lt;code&gt;ALLOWD_METHOD_NAMES&lt;/code&gt;에 작성한 모든 HTTP Method를 허용합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Spring Boot 환경변수 분리하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포를 한다면, 기존에 사용하던 로컬 개발 환경이 아닌 배포를 위한 새로운 환경을 구성해야 합니다.&lt;br /&gt;Spring Boot는 application.yml 혹은 properties를 통해 쉽게 개발환경과 배포환경을 분리할 수 있는 기능을 제공합니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 yml 파일을 사용할 수 도있지만, 3개의 yml파일을 구성해 각각, 로컬 환경, 개발 환경, 운영 환경으로 구성해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;로컬, 개발, 운영환경 나누기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 application.yml은 다음과 같습니다&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/jscode-spring-class?characterEncoding=UTF-8
    username: hoseok
    password: 1234

  jpa:
    hibernate:
      ddl-auto: create
      naming:
        physical-strategy: org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy
    properties:
      hibernate:
        format_sql: true
        jdbc:
          time_zone: UTC
    database-platform: org.hibernate.dialect.MySQL5InnoDBDialect

logging:
  level:
    org:
      hibernate:
        SQL: debug
        type: trace&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로컬 MySQL DB를 이용하고 있으며 ddl-auto가 create로 배포환경에는 적합하지 않습니다.&lt;br /&gt;운영환경을 적용하려면 몇가지를 적용하거나 변경해주어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;spring: datasource: url, username, password&lt;/code&gt;: RDS를 이용하므로 RDS에 맞게 적용시켜줘야 합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;jpa: hibernate: ddl-auto: none&lt;/code&gt;: 운영환경에서는 절대 DB 테이블이 초기화되면 안됩니다!!&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;application.yml&lt;/b&gt;: Spring Boot가 제일 먼저 읽고 어떤 profile을 읽을지 설정한다. prod로 설정했으므로 application-prod.yml 파일을 이용해 Spring Boot가 시작됩니다!&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;spring:
  profiles:
    active: prod&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;application-local.yml&lt;/b&gt;: 로컬 환경에서 사용될 설정입니다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/jscode-spring-class?characterEncoding=UTF-8
    username: hoseok
    password: 1234

  jpa:
    hibernate:
      ddl-auto: create
      naming:
        physical-strategy: org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy
    properties:
      hibernate:
        format_sql: true
        jdbc:
          time_zone: UTC
    database-platform: org.hibernate.dialect.MySQL5InnoDBDialect

logging:
  level:
    org:
      hibernate:
        SQL: debug
        type: trace&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;application-dev.yml&lt;/b&gt;: 개발 환경에서 사용됩니다.(현재 상황에서는 나누는것의 의미가 거의 없고, 연습을 위해 나눴습니다!)&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;spring:
  datasource:
    url: jdbc:h2:mem:testdb;MODE=MYSQL
    username: sa
    password:

  jpa:
    hibernate:
      ddl-auto: none
      naming:
        physical-strategy: org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy
    properties:
      hibernate:
        format_sql: true
        jdbc:
          time_zone: Asia/Seoul
    database-platform: org.hibernate.dialect.MySQL5InnoDBDialect

logging:
  level:
    org:
      hibernate:
        SQL: debug
        type: trace&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;application-prod.yml&lt;/b&gt;: 배포시 운영환경에서 사용될 설정입니다. Elastic Beanstalk에서 설정한 key, value 값을 입력하기 위해서 민감한 정보에 대해서는 노출시키지 않습니다! 배포환경에 민감 정보에 대한 관리 방법에 대한 자세한 내용은 다음 &lt;a href=&quot;https://kukim.tistory.com/150&quot;&gt;블로그&lt;/a&gt;를 참고하시면 좋습니다!&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;spring:
  datasource:
    url: jdbc:mysql://${rds.endpoint}:${rds.port}/${rds.schema}?characterEncoding=UTF-8
    username: ${rds.username}
    password: ${rds.password}

  jpa:
    hibernate:
      ddl-auto: none
      naming:
        physical-strategy: org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy
    properties:
      hibernate:
        format_sql: true
        jdbc:
          time_zone: Asia/Seoul
    database-platform: org.hibernate.dialect.MySQL5InnoDBDialect

logging:
  level:
    org:
      hibernate:
        SQL: debug
        type: trace&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. IAM 계정 등록 및 Elastic Beanstalk을 위한 권한 설정&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;IAM 계정 등록&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Elastic Beanstalk을 통해 배포를 하려면 IAM 계정을 등록하고 권한을 설정해야 합니다!&lt;br /&gt;해당 과정은 AWS 공식문서에서 자세히 설명하고 있으므로 다음 과정을 착실히 따라가면 생성할 수 있습니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.aws.amazon.com/ko_kr/singlesignon/latest/userguide/getting-started.html&quot;&gt;https://docs.aws.amazon.com/ko_kr/singlesignon/latest/userguide/getting-started.html&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Elastic Beanstalk에 사용할 권한 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IAM 설정에 들어가서 역할 탭의 &lt;code&gt;역할 만들기&lt;/code&gt;를 통해 역할!&lt;br /&gt;을 생성한다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/wpdlzhf159/post/487027a9-755b-4fca-ac2a-32f937c43ef8/image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Elastic Beanstalk가 자동 설정해주는 EC2에 적용하기 위해 EC2를 선택한다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/wpdlzhf159/post/9f701b89-7731-49eb-ad7f-b40a0be729c4/image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;beanstalk를 검색해서 상위 3개의 정책을 선택하고 다음을 누른다. 이후 이름을 설정하고 생성을 완료하면 된다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/wpdlzhf159/post/18b11fb2-15a6-4cad-ae96-09da24b3eecf/image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/wpdlzhf159/post/3c1ff8b9-64ee-4428-a569-abd0f689d977/image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. RDS, Elastic Beanstalk 생성하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Elastic Beanstalk을 이용하면 RDS까지 통합적으로 생성할 수 있지만, 개인적으로는 분리하는 편이 관리하기가 쉽다고 느껴져서 RDS와 Elastic Beanstalk을 분리했습니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;RDS 생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 RDS를 생성하는 과정은 워낙 간단하므로, 따로 설명하진 않겠습니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RDS도 AWS 프리티어 기간에서 무료로 이용할 수 있지만, 과금을 방지하기 위해 몇가지 해야할 설정이 있습니다! 해당 설정을 고려한 RDS 생성 글이 존재하므로 다음 글을 참고하시면 좋을것 같습니다!&lt;br /&gt;&lt;a href=&quot;https://velog.io/@shawnhansh/AWS-RDSmySql-%ED%94%84%EB%A6%AC%ED%8B%B0%EC%96%B4-%EC%83%9D%EC%84%B1%ED%95%98%EA%B8%B0&quot;&gt;AWS RDS(mySql) 프리티어 생성하기&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Elastic Beanstalk 생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 서버 환경으로 설정하고, 애플리케이션 이름을 작성합니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/wpdlzhf159/post/7b5b58fa-6a6d-4a90-bd40-e2aa77ee9911/image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;플랫폼은 Java이며, Java11을 선택합니다. (현재 프로젝트 기준)&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/wpdlzhf159/post/15c7d563-3c7b-44e4-aba2-7045218218b2/image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프리셋은 사용자 지정 구성으로 설정해야 무중단 배포를 위한 로드 밸런서를 설정할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/wpdlzhf159/post/fa4baf11-2d4c-41e6-ad0c-0ce158109e26/image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Elastic Beanstalk을 처음 생성하면 &lt;code&gt;새 서비스 역할 생성 및 사용&lt;/code&gt;을 선택하여 등록하고 사전에 생성한 EC2 인스턴스의 키페어를 등록합니다. EC2 인스턴스 프로파일은 위에서 만들었던 &lt;code&gt;역할 권한&lt;/code&gt;을 선택하고 다음을 누릅니다!&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/wpdlzhf159/post/781b66d2-f9c1-4cc6-a7e0-0f72b7e58642/image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3단계에서는 네트워킹 및 DB를 설정합니다. 우리는 별도의 RDS를 구성했으므로 건너뛰어도 무관합니다!&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/wpdlzhf159/post/03515881-e2e5-496f-b52b-d74a6539ffb5/image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;루트 볼륨은 기본값을 사용하고 보안 그룹은 현재 애플리케이션에서 이용하는 호스트에 대해 인바운드 아웃바운드 규칙을 적용한 보안 그룹을 이용합니다!&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/wpdlzhf159/post/71e68102-0b69-4553-833e-7d266432b23e/image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/wpdlzhf159/post/cf3dc1ed-8957-4ff7-b40a-d2c4a75ad55e/image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오토 스케일링 그룹의 환경 유형은 &lt;code&gt;밸런싱된 로드&lt;/code&gt;를 선택합니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/wpdlzhf159/post/a6f95c3f-2f24-4863-a154-b0482bf72f5f/image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로드 밸런서는 애플리케이션 로드 밸런서(ALB)를 이용합니다.&lt;br /&gt;로드 밸런서에 대해 자세한 내용은 알지 못하지만, 예정된 이벤트가 아닌 갑작스런 트래픽에 대응하기에 유용합니다. 로드 밸런서에 대한 리스너를 활성화 합니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/wpdlzhf159/post/c3349328-68a1-45c4-bb8e-e475e747bc0a/image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 배포 정책으로 추가 배치를 사용한 롤링을 이용합니다. 따라서 배포시에 신규 인스턴스에 신규 버전을 올리고, 해당 인스턴스가 문제가 없다면 기존 인스턴스를 죽이는 방식이다. 1개의 인스턴이며 배치 크기가 100프로 이므로 해당 설정을 사용합니다!&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/wpdlzhf159/post/6c985f49-3a5b-4875-b575-fbe994799209/image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하단에 환경속성이 존재하는데, 여기서 RDS에 대한 설정을 key, value로 등록할 수 있습니다. 애플리케이션을 배포하게 되면 여기서 등록한 환경속성은 &lt;code&gt;application-prod.yml&lt;/code&gt;에서 &lt;code&gt;${}&lt;/code&gt;와 설정한 내용을 비교하여 적절하게 매핑해줍니다! 이를 통해 민감한 속성을 GitHub에 노출하지 않을 수 있습니다!&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/wpdlzhf159/post/25ba34f2-e4bf-4c73-86a0-284ba1617bb9/image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성을 완료하면 5 ~ 10분의 시간동안 Elastic Beanstalk 애플리케이션 및 환경을 구성합니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. IAM 인증키를 받아서 GitHub에 등록하기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;IAM 인증키 생성하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Beanstalk 설정이 완료됐다면 GitHub Actions에서 Elastic Beanstalk에 접근할 수 있도록 인증키를 발급받아야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인증키를 발급 받기 위해선 IAM에서 별도의 사용자를 생성해야 합니다! 아래 화면에서 사용자 추가를 누르고 사용자 이름을 생성합니다! (aws-eb-admin은 본인이 배포를 진행하면서 미리 만들어 둔 사용자 입니다!)&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/wpdlzhf159/post/a5a17f39-90c2-420a-9672-5b791a7ec0d9/image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/wpdlzhf159/post/23d57146-fc26-4983-bb1c-19be1741bfff/image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;권한 설정에서 직접 정책 연결을 통해 &lt;code&gt;AdministratorAccess-AWSElasticBeanstalk&lt;/code&gt; 권한을 부여 합니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/wpdlzhf159/post/d3928a54-5842-4166-bad0-88a4904dc7c0/image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성한 사용자를 구분할 수 있도록 &lt;code&gt;Name&lt;/code&gt; 태그를 추가하고 사용자를 생성합니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/wpdlzhf159/post/ece7e36d-b161-4303-b24c-4a580fb0012a/image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자 목록에서 우리가 생성한 사용자를 클릭하여 &lt;code&gt;보안 자격 증명&lt;/code&gt; 탭을 클릭하여 하단에 &lt;code&gt;액세스 키&lt;/code&gt; 항목에서 &lt;code&gt;액세스 키 만들기&lt;/code&gt;를 클릭합니다.&lt;br /&gt;이후 &lt;code&gt;AWS 컴퓨팅 서비스에서 실행되는 애플리케이션&lt;/code&gt; 항목을 선택합니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/wpdlzhf159/post/eb39ed4f-be55-4d62-a053-13d208a242d1/image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/wpdlzhf159/post/05c22437-d051-47c8-ae2d-4962ac18893b/image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설명 태그 값에 대한 입력을 완료하고 액세스 키를 만들면 액세스 키와 value가 생성된것을 확인할 수 있습니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;GitHub Actions에 등록하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 생성한 액세스 키와 밸류를 GitHub Actions에 등록하려면 우선 리포지토리의 &lt;code&gt;Settings&lt;/code&gt;의 &lt;code&gt;Secrets and variables&lt;/code&gt;를 들어갑니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이곳에서 &lt;code&gt;AWS_ACCESS_KEY_ID&lt;/code&gt;로 생성한 액세스 키의 id를 입력하고, &lt;code&gt;AWS_SECRET_ACCESS_KEY&lt;/code&gt;에 실제 키 값을 입력하여 등록합니다. 이렇게 함으로써 GitHub Actions에서 배포동작시 해당 값을 이용해 Elastic Beanstalk에 접근할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/wpdlzhf159/post/76860aa1-680c-40a7-8b54-29a7457e1cf6/image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. GitHub Actions 스크립트 파일 작성&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;브랜치, Java, 빌드 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 프로젝트 리포지토리에서 최상단 디렉토리에 &lt;code&gt;.github/workflows/deploy.yml&lt;/code&gt; 생성을 하고 아래의 기본 스크립트를 추가한다.&lt;/p&gt;
&lt;pre class=&quot;http&quot;&gt;&lt;code&gt;name: anonymous-board

on:
  push:
    branches:
      - main # (1) 트리거 브랜치 설정: 해당 브랜치에 push되면 스크립트 실행됨
    paths: 'anonymous-board/**' # 해당 디렉토리 하위에 gradlew가 존재하므로
  workflow_dispatch: # (2) 수동 실행: main 브랜치 push외에도 수동으로 해당 스크립트를 실행가능하도록 한다.
defaults:
  run:
    working-directory: anonymous-board # 해당 디렉토리 하위에 gradlew가 존재하므로 시작 디렉토리를 조정한다.
jobs:
  build:
    runs-on: ubuntu-latest # (3) Github Action 스크립트가 작동될 OS 환경 지정

    steps:
      - name: Checkout
        uses: actions/checkout@v2 # (4) 프로젝트 코드 체크아웃

      - name: Set up JDK 11
        uses: actions/setup-java@v1 # (5)  GitHub Action이 실행될 OS에 Java 설치
        with:
          java-version: 11

      - name: Grant execute permission for gradlew
        run: chmod +x ./gradlew # (6) gradle wrapper를 실행할 수 있도록 실행권한 줌
        shell: bash

      - name: Build with Gradle
        run: ./gradlew clean build # (7) gradle wrapper를 통해 해당 프로젝트를 build함
        shell: bash

            - name: Get current time
              uses: 1466587594/get-current-time@v2 
              id: current-time
              with:
                format: YYYY-MM-DDTHH-mm-ss # (8) action은 기존의 Momentjs를 지원하므로 동일한 포맷을 사용함
                utcOffset: &quot;+09:00&quot;  # (9) UTC기준 +9시간이 한국시간 KST가 된다. &lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;번호 순서대로 읽어보면 위의 스크립트가 어떤 동작을 하는지 이해가 갈 것 입니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;빌드된 jar 파일을 Beanstalk에 배포하기 위한 zip파일로 만들기&lt;/h3&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;      - name: Generate deployment package # (1)
        run: |
          mkdir -p deploy
          cp build/libs/*.jar deploy/application.jar
          cp Procfile deploy/Procfile
          cp -r .ebextensions deploy/.ebextensions
          cp -r .platform deploy/.platform
          cd deploy &amp;amp;&amp;amp; zip -r deploy.zip .&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Gradle Build를 통해 만들어진 jar파일을 Beanstalk에 배포하기 위해 deploy.zip파일로 만들어주는 스크립트 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빌드가 끝나면 &amp;rarr; Jar파일명을 application.jar로 변경&lt;br /&gt;(매 빌드마다 jar의 파일명이 버전과 타임스탬프로 파일명이 교체됨, 따라서 매번 달라질 파일명을 찾기보다는 하나로 통일해 사용)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;build/libs/*.jar&lt;/code&gt;는 해당 디렉토리에 존재하는 빌드된 jar 파일을 &lt;code&gt;deploy/application.jar&lt;/code&gt;로 변경한다. 이때 주의점이 있는데 Spring Boot 2.5 버전 이상부터는 빌드를 하게 되면 &lt;code&gt;~plain.jar&lt;/code&gt;라는 jar 파일이 하나 더 생성되므로 다음 설정을 build.gradle에 추가해 생성을 방지한다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;jar {
    enabled = false
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가되는 스크립트에서 이용되는 Procfile, .ebextensions, .platform은 이후에 좀 더 자세히 설명하겠습니다!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Beanstalk Deploy 플러그인을 통해 Elastic Beanstalk 배포 설정 스크립트 작성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/marketplace/actions/beanstalk-deploy&quot;&gt;Beanstalk Deploy&lt;/a&gt;는 GitHub Actions를 통해 Beanstalk 배포를 편리하게 도와주는 플러그인 입니다! 이를 이용하면 편하게 배포 코드를 작성할 수 있습니다. 따라서 다음 스크립트를 추가해줍니다!&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;- name: Deploy to EB # (2)
    uses: einaregilsson/beanstalk-deploy@v14
    with:
    aws_access_key: ${{ secrets.AWS_ACCESS_KEY_ID }} # 우리가 Secret 키로 설정한 값들
    aws_secret_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
    application_name: MyApplicationName # Beanstalk에서 생성한 애플리케이션의 이름
    environment_name: MyApplication-Environment # Beanstalk에서 생성한 환경의 이름
    version_label: github-action-${{steps.current-time.outputs.formattedTime}}
          region: ap-northeast-2
          deployment_package: anonymous-board/deploy/deploy.zip&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Beanstalk 플러그인을 사용하는데 이때 생성한 IAM 인증키가 이곳에서 사용됩니다.&lt;br /&gt;Beanstalk가 플러그인을 통해 배포될때마다 유니크한 버전을 갖기 위해 시간을 추가했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한가지 주의해야 하는 부분은 &lt;code&gt;deployment_package&lt;/code&gt; 입니다. 현재 프로젝트는 anonymous-board라는 디렉토리 하위에 실제 프로젝트 코드들이 존재합니다. 따라서 초기 deploy.yml 설정에서 &lt;code&gt;working-directory: anonymous-board # 해당 디렉토리 하위에 gradlew가 존재하므로 시작&lt;/code&gt;과 같이 실제 동작 디렉토리를 설정해주었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 Beanstalk 플러그인을 사용하는 부분에서는 해당 설정이 적용되지 않으므로 정확한 프로젝트 경로(anonymous-board 디렉토리가 포함)를 작성해야 합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;완성된 deploy.yml&lt;/h3&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;name: anonymous-board
on:
  push:
    branches:
      - main # (1) 실습하시는분들은 master로 하시면 됩니다. (저는 별도 브랜치로 지정)
    paths: 'anonymous-board/**'
  workflow_dispatch: # (2) 수동 실행
defaults:
  run:
    working-directory: anonymous-board
jobs:
  build:
    runs-on: ubuntu-latest # (3)

    steps:
      - name: Checkout
        uses: actions/checkout@v2 # (4)

      - name: Set up JDK 11
        uses: actions/setup-java@v1 # (5)
        with:
          java-version: 11

      - name: Grant execute permission for gradlew
        run: chmod +x ./gradlew # (6)
        shell: bash

      - name: Build with Gradle
        run: ./gradlew clean build # (7)
        shell: bash

      - name: Get current time
        uses: 1466587594/get-current-time@v2
        id: current-time
        with:
          format: YYYY-MM-DDTHH-mm-ss # (1)
          utcOffset: &quot;+09:00&quot;

      - name: Generate deployment package # (1)
        run: |
          mkdir -p deploy
          cp build/libs/*.jar deploy/application.jar
          cp Procfile deploy/Procfile
          cp -r .ebextensions deploy/.ebextensions
          cp -r .platform deploy/.platform
          cd deploy &amp;amp;&amp;amp; zip -r deploy.zip .

      - name: Deploy to EB
        uses: einaregilsson/beanstalk-deploy@v21
        with:
          aws_access_key: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws_secret_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          application_name: my-eb-server-final
          environment_name: anonymous-board
          version_label: github-action-${{steps.current-time.outputs.formattedTime}}
          region: ap-northeast-2
          deployment_package: anonymous-board/deploy/deploy.zip&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. Beanstalk 애플리케이션 구성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 &lt;code&gt;name: Genereate deployment package&lt;/code&gt; 작업에서 언급되었던 &lt;code&gt;Prockfile&lt;/code&gt;, &lt;code&gt;.platform/nginx/nginx.conf&lt;/code&gt;, &lt;code&gt;.ebextensions/00-makeFiles.config&lt;/code&gt;에 대해서 애플리케이션을 구성해봅시다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 부분도 잘 모르지만 특히 현재 진행할 부분은 추가 공부가 필요합니다. 단순히 해당 코드가 어떤 역할이겠다는 어림짐작은 있지만, 정확한 의미는 잘 알지 못합니다 ㅠ&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 위의 3개의 파일은 프로젝트에서 아래와 같이 위치하고 있습니다!&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/wpdlzhf159/post/efe3f31b-4868-4225-b7e2-489d742986f4/image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;.ebextensions&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Beanstalk는 시스템의 대부분을 AWS에서 자동 구성을 해주므로 EC2에서 직접 설치할때 처럼 사용할 수 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 Custom하게 사용할 수 있도록 설정하려면 &lt;code&gt;.ebextensions&lt;/code&gt; 디렉토리를 이용합니다.&lt;br /&gt;해당 디렉토리에 &lt;code&gt;.config&lt;/code&gt; 파일 확장 명을 가진 YAML 혹은 JSON 형태의 설정 코드를 두면 그에 맞춰 Beanstalk 배포시/환경 재구성시 사용됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 만들고자 하는 Custom 기능은 애플리케이션 실행 스크립트(&lt;code&gt;java -jar application.jar&lt;/code&gt;)를 생성하는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조금더 구체적으로 Beanstalk이 GitHub Actions로 전달 받은 zip파일(배포파일)이 압축이 풀리고 난후 내부의 특정 파일을 어떤 파라미터로 설정하는지 설정하는 스크립트 입니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;.ebextension/00-makeFiles.config&lt;/code&gt;&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;files:
    &quot;/sbin/appstart&quot; :
        mode: &quot;000755&quot;
        owner: webapp
        group: webapp
        content: |
            #!/usr/bin/env bash
            JAR_PATH=/var/app/current/application.jar

            # run app
            killall java
            java -Dfile.encoding=UTF-8 -jar $JAR_PATH&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;/sbin&lt;/code&gt;아래에 스크립트 파일을 두면 전역에서 실행 가능합니다.&lt;/li&gt;
&lt;li&gt;따라서 해당 디렉토리 아래 appstart라는 스크립트 파일을 생성하고&lt;/li&gt;
&lt;li&gt;권한은 755(소유자만 모든 것(쓰기, 읽기, 실행)이 가능하고 그 외 사용자의 경우는 읽기, 실행은 가능하나 쓰기는 불가능)를 주고 사용자는 &lt;code&gt;webapp&lt;/code&gt;으로 하여 &lt;code&gt;content&lt;/code&gt;내용을 가진 스크립트 파일이 생성됩니다.&lt;/li&gt;
&lt;li&gt;이곳에서 만들어진 &lt;code&gt;/sbin/appstart&lt;/code&gt; 스크립트 파일은 &lt;code&gt;Procfile&lt;/code&gt;에서 실행됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Procfile&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Beanstalk 입장에선 배포 애플리케이션의 실행은 Procfile의 실행을 의미합니다.&lt;br /&gt;이미 실행 스크립트는 위의 &lt;code&gt;00-makeFiles.config&lt;/code&gt;에서 생성한 &lt;code&gt;sbin/appstart&lt;/code&gt; 스크립트를 실행하기만 하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 다음과 같이 추가합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Procfile&lt;/code&gt;&lt;/p&gt;
&lt;pre class=&quot;avrasm&quot;&gt;&lt;code&gt;web: appstart&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;.platform/nginx/nginx.conf&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 파일은 Elastic Beanstalk에서 리버스 프록시를 담당하는 Nginx에 대한 설정을 합니다.&lt;br /&gt;이미 ALB(Application Load Balancer)를 통해 무중단 배포를 하도록 구성했습니다.&lt;br /&gt;따라서 여기서 설정하는 Nginx는 무중단 배포를 위한 것이 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Nginx는 임베디드 톰캣으로 클라이언트의 요청을 보내는 역할만을 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;.platform/nginx/nginx.conf&lt;/code&gt;&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;user                    nginx;
error_log               /var/log/nginx/error.log warn;
pid                     /var/run/nginx.pid;
worker_processes        auto;
worker_rlimit_nofile    33282;

events {
    use epoll;
    worker_connections  1024;
}

http {
  include       /etc/nginx/mime.types;
  default_type  application/octet-stream;

  log_format  main  '$remote_addr - $remote_user [$time_local] &quot;$request&quot; '
                    '$status $body_bytes_sent &quot;$http_referer&quot; '
                    '&quot;$http_user_agent&quot; &quot;$http_x_forwarded_for&quot;';

  include       conf.d/*.conf;

  map $http_upgrade $connection_upgrade {
      default     &quot;upgrade&quot;;
  }

  upstream springboot {
    server 127.0.0.1:8080;
    keepalive 1024;
  }

  server {
      listen        80 default_server;

      location / {
          proxy_pass          http://springboot;
          proxy_http_version  1.1;
          proxy_set_header    Connection          $connection_upgrade;
          proxy_set_header    Upgrade             $http_upgrade;

          proxy_set_header    Host                $host;
          proxy_set_header    X-Real-IP           $remote_addr;
          proxy_set_header    X-Forwarded-For     $proxy_add_x_forwarded_for;
      }

      access_log    /var/log/nginx/access.log main;

      client_header_timeout 60;
      client_body_timeout   60;
      keepalive_timeout     60;
      gzip                  off;
      gzip_comp_level       4;

      # Include the Elastic Beanstalk generated locations
      include conf.d/elasticbeanstalk/healthd.conf;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 Nginx에 대해 자세히 알지 못하므로 복사/붙여넣기 수준으로 구성했습니다 ㅎㅎ..&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 모든 설정이 완료됐고, 실제 애플리케이션 배포 단계가 남아있습니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 애플리케이션 배포&lt;code&gt;&lt;/code&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;deploy.yml&lt;/code&gt;에서 main 브랜치에 push됐을때 자동으로 배포 스크립트가 실행되도록 설정했으므로, 실제 main 브랜치에 다음과 같이 merge 합니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/wpdlzhf159/post/6ec0a511-9091-4412-af3a-781ff50f92c6/image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 GitHub Actions를 보면 배포 스크립트가 실행됩니다!&lt;br /&gt;아래 Deploy to EB 과정의 로그들이 실제 Elastic Beanstalk의 로그에도 동일하게 나타납니다!&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/wpdlzhf159/post/af5b68cd-b1eb-4a57-bf36-4b4d761d26b6/image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포 단계가 완료되고 Elastic Beanstalk을 보게되면 도메인이 존재하는데 해당 도메인을 가지고 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/wpdlzhf159/post/40490edd-61a4-4d38-a694-2ac34254dec7/image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Postman을 통해 실제 게시글 생성 API 요청을 보내게 되면 정상적으로 게시글이 생성됨을 알 수 있습니다!!&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/wpdlzhf159/post/95f4ccad-7d92-4faf-af1e-2e04b1254a76/image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;어려웠던 부분, 시간을 들였던 부분&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포 과정을 진행하면서 IAM 설정 및 권한 설정에 대한 어려움이 있었습니다..&lt;br /&gt;설정 자체는 어렵지 않았지만, Elastic Beanstalk을 설정없이 생성하려 했던 삽질의 시간이 정말 길었습니다..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 참고한 레퍼런스와 실제 내 프로젝트의 디렉토리 구조가 다르다 보니 그런 차이점들을 인지하고 변경을 주는 부분에서도 많은 시간이 들었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;많은 삽질이 있었지만, 결론은 정말 간단했던.. 하지만 삽질로 인해 조금 단단해지지 않았을까 생각합니다 ㅎㅎ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단하게 무중단 배포를 진행하면서 공부해야 할 키워드들도 늘었습니다.&lt;br /&gt;(&lt;code&gt;Nginx&lt;/code&gt;, &lt;code&gt;Linux&lt;/code&gt;, &lt;code&gt;GitHub Actions 스크립트&lt;/code&gt;, &lt;code&gt;Elastic Beanstalk에 대한 전반적인 이해&lt;/code&gt;)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고 레퍼런스&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.jboss.org/hibernate/orm/6.0/userguide/html_single/Hibernate_User_Guide.html#best-practices-logging&quot;&gt;Hibernate ORM Docs: best practices logging - Hibernate Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://velog.io/@stbpiza/Spring-Boot-%ED%99%98%EA%B2%BD-%EB%B6%84%EB%A6%AC%ED%95%98%EA%B8%B0&quot;&gt;[Spring Boot] 환경 분리하기 - Valog&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://velog.io/@hwsa1004/AWS-RDS-%ED%94%84%EB%A6%AC%ED%8B%B0%EC%96%B4%EC%97%90%EC%84%9C-RDS-%EA%B3%BC%EA%B8%88-%EB%B0%A9%EC%A7%80%ED%95%98%EA%B8%B0&quot;&gt;[AWS RDS] 프리티어에서 RDS 과금 방지하기 - Velog&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.aws.amazon.com/ko_kr/singlesignon/latest/userguide/getting-started.html&quot;&gt;AWS IAM identity Center 시작하기 - AWS Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://repost.aws/questions/QUx_V0HSWxRJSRndVaSlsvOQ/elastic-bean-stalk-environment-is-not-getting-created#ANwB9EGj4QTHycEXVvji4eEg&quot;&gt;Elastic Beanstalk environment is not getting created - AWS QnA&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://jane514.tistory.com/6&quot;&gt;AWS Elastic Beanstalk으로 개발서버를 구축해본 이야기 - Tistory&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.aws.amazon.com/ko_kr/singlesignon/latest/userguide/getting-started.html&quot;&gt;IAM 계정 등록하기 - AWS Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://velog.io/@yyong3519/EB%EC%97%90-SpringBoot-%EB%B0%B0%ED%8F%AC1-EB-%EA%B8%B0%EB%B3%B8-%EC%84%B8%ED%8C%85&quot;&gt;EB에 SpringBoot 배포(1) - EB 기본 세팅 - Velog&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://jojoldu.tistory.com/543&quot;&gt;1. Github Action &amp;amp; AWS Beanstalk 배포하기 - Github Action으로 빌드하기 - 향로님 블로그&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://jojoldu.tistory.com/549&quot;&gt;2. Github Action &amp;amp; AWS Beanstalk 배포하기 - profile=local로 배포하기 - 향로님 블로그&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>스터디/Spring Boot 스터디</category>
      <category>aws</category>
      <category>elastic beanstalk</category>
      <category>Github Actions</category>
      <category>nginx</category>
      <category>무중단 배포</category>
      <author>HiiWee</author>
      <guid isPermaLink="true">https://hiiwee.tistory.com/40</guid>
      <comments>https://hiiwee.tistory.com/40#entry40comment</comments>
      <pubDate>Thu, 18 May 2023 04:39:50 +0900</pubDate>
    </item>
    <item>
      <title>[스터디2] 익명 게시판 유효성 검사, 예외 처리 및 API 문서 만들기 (2회차)</title>
      <link>https://hiiwee.tistory.com/39</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;2회차 미션은 크게 유효성 검사와 예외 처리 부분과 API 문서를 만드는 2개의 요구사항이 주어졌습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2회차 미션을 진행한 전체 코드는 다음 PR에서 확인할 수 있습니다.&lt;br /&gt;&lt;a href=&quot;https://github.com/JSCODE-EDU/project-class-HiiWee/pull/4&quot;&gt;https://github.com/JSCODE-EDU/project-class-HiiWee/pull/4&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  유효성 검사 및 예외 처리 요구사항 분석&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 미션에서 임의로 게시글과 제목에 대해 정했던 유효성 검사에서 약간의 변동이 있었습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;게시글 작성 기능
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;제목은 1글자 이상 15글자 이하여야 한다. (기존에는 200글자까지 허용)&lt;/li&gt;
&lt;li&gt;내용은 1글자 이상 1000글자 이하여야 한다. (기존에는 5000자까지 허용)&lt;/li&gt;
&lt;li&gt;제목은 공백으로만 이루어질 수는 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;게시글 검색 기능
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;검색 키워드는 공백을 제외한 1글자 이상이어야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에 작성한 내용들이 있었기에 옵션과 테스트 코드를 변경해주면 간단하게 요구사항을 적용할 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;@NotBlank, @NotEmpty, @NotNull의 차이점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1번 게시글 작성 기능 요구사항을 만들면서, 게시글 내용에 대해서는 공백을 허용하고자 했습니다.&lt;br /&gt;따라서 RequestDto에서 request body 값에 대한 검증이 필요했고, 이때 @NotBlank -&amp;gt; @NotEmpty로의 변경이 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@NotBlank, @NotEmpty, @NotNull는 Bean Validation에서 제공하는 애노테이션으로 사용자의 입력값 검증을 쉽게 할 수 있게 도와주는 애노테이션 입니다. 정말 많이 사용되는 애노테이션입니다. 이름이 상당히 유사하지만, 제공하는 기능은 꽤 차이가 있으므로 확실하게 알고 사용해야 합니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;@NotBlank&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@NotBlank 애노테이션에 대한 설명은 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;@NotBlank 애노테이션이 달린 요소는 null이 아니어야 하며 공백이 아닌 문자를 하나 이상 포함해야 합니다.&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, @NotBlank는 공백과 null 모두 허용하지 않고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 애노테이션이 붙은 입력값에 대해 &lt;code&gt;null&lt;/code&gt;, &lt;code&gt;&quot;&quot;&lt;/code&gt;, &lt;code&gt;&quot;   &quot;&lt;/code&gt; 3가지 모두 허용하고 있지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;@NotNull&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@NotNull 애노테이션에 대한 설명은 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;@NotNull 애노테이션이 달린 요소는 null이 아니어야 하며 이는 모든 type에 허용됩니다.&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 해당 애노테이션이 달려있는 것이 타입이라면 해당 요소는 null이면 안됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 &lt;code&gt;&quot;null&quot;&lt;/code&gt;로 들어오는 입력값은 허용하지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;@NotEmpty&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@NotEmpty에 대한 설명은 다음과 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;@NotEmpty 애노테이션이 달린 요소는 null이거나 비어 있으면 안됩니다.&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 &lt;code&gt;null&lt;/code&gt;, &lt;code&gt;&quot;&quot;&lt;/code&gt;을 허용하지 않습니다. 공백으로 이루어진 &lt;code&gt;&quot;  &quot;&lt;/code&gt;과 같은 값은 허용됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제약의 강도는 @NotBlank &amp;gt; @NotEmtpy &amp;gt; @NotNull의 순서로 정해집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 구현하고자 하는 기능은 제목은 공백, null이 모두 허용되면 안되므로 @NotBlank를 사용하고, 내용의 경우 공백의 입력까지는 허용되지만 null값이나 빈 값은 허용하지 않으므로 @NotEmpty를 달아주는것이 적절 했습니다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Getter
public class PostSaveRequest {

    @NotBlank(message = &quot;제목을 반드시 입력해야 합니다.&quot;)
    private String title;

    @NotEmpty(message = &quot;내용을 반드시 입력해야 합니다.&quot;)
    private String content;

    private PostSaveRequest() {
    }

    @Builder
    private PostSaveRequest(final String title, final String content) {
        this.title = title;
        this.content = content;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  게시글 API 문서화 하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;게시글 API 문서 작성 요구사항은 다음과 같습니다. 사실상 지금까지 만든 모든 API를 문서화하면 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/JSCODE-EDU/project-class-HiiWee/assets/66772624/a368debc-b441-422c-9f78-fdefd86d33ae&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 API에 반드시 포함되어야 할 내용들은 아래와 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/JSCODE-EDU/project-class-HiiWee/assets/66772624/c837f1c7-6177-4f97-bd34-5439e68860bf&quot; alt=&quot;image&quot; width=&quot;489&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제가 선택한 문서화 도구는 Spring Rest Docs으로 API 문서를 자동화 할 수 있다는 장점이 있었고, Swagger와 달리 애플리케이션 코드에 어떠한 변화도 없으며 테스트 코드를 반드시 작성해야만 사용할 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 스터디를 진행하면서 테스트 코드에 많은 집중을 했기에 자연스럽게 관심을 가지게 되어 적용하게 됐습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Spring Rest Docs 의존성 설정하기&lt;/h3&gt;
&lt;pre class=&quot;gams&quot;&gt;&lt;code&gt;plugins {
    id 'java'
    id 'org.springframework.boot' version '2.7.11'
    id 'io.spring.dependency-management' version '1.0.15.RELEASE'
    id 'org.asciidoctor.jvm.convert' version &quot;3.3.2&quot;
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

configurations {
    asciidoctorExtensions
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

ext {
    set('snippetsDir', file(&quot;build/generated-snippets&quot;))    // snippet이 생성되는 위치
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-validation'
    implementation 'org.springframework.boot:spring-boot-starter-web'

    asciidoctorExtensions 'org.springframework.restdocs:spring-restdocs-asciidoctor'

    annotationProcessor 'org.projectlombok:lombok'
    compileOnly 'org.projectlombok:lombok'

    runtimeOnly 'com.h2database:h2'
    runtimeOnly 'com.mysql:mysql-connector-j'

    testImplementation 'io.rest-assured:rest-assured:4.4.0'
    testImplementation 'io.rest-assured:spring-mock-mvc:4.4.0'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
}

tasks.named('test') {
    outputs.dir snippetsDir
    useJUnitPlatform()
}

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

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

task createDocument(type: Copy) {
    dependsOn asciidoctor

    from file(&quot;build/docs/asciidoc&quot;)
    into file(&quot;src/main/resources/static&quot;)
}

build {
    dependsOn createDocument
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의존성 설정 및 빌드 설정에 대한 자세한 내용은 다음 글에서 자세히 설명했습니다. &lt;a href=&quot;https://hiiwee.tistory.com/35#%E2%9C%85%20Spring%20Rest%20Docs%EB%A5%BC%20%ED%86%B5%ED%95%9C%20API%20%EB%AC%B8%EC%84%9C%20%EC%9E%90%EB%8F%99%ED%99%94%20%EB%B0%8F%20%ED%85%8C%EC%8A%A4%ED%8A%B8-1&quot;&gt;[MyLittleBlog] 회원가입 웹 계층 구현하기 &lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;MockMvc vs RestAssured&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Rest Docs를 생성하는 테스트는 MockMvc 테스트로 결정했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 코드에선 별도의 인수테스트를 진행하고 있기에 기존 Controller 단위 테스트와 병합하기 위해 MockMvc를 선택했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 기존 테스트에서 Rest Docs를 작성하는 부분만 추가해주면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;테스트 작성하기&lt;/h3&gt;
&lt;pre class=&quot;scala&quot;&gt;&lt;code&gt;@WebMvcTest(PostController.class)
@ExtendWith(RestDocumentationExtension.class) // (1)
public class PostControllerTest {

    @Autowired
    MockMvc mockMvc;

    @Autowired
    ObjectMapper objectMapper;

    @MockBean
    PostService postService;

    @BeforeEach
    void setUp(WebApplicationContext webApplicationContext, RestDocumentationContextProvider restDocumentation) {
        // (2)
        mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext)
                .apply(documentationConfiguration(restDocumentation))
                .build();
    }

    @DisplayName(&quot;게시글 작성을 하면 201을 반환한다.&quot;)
    @Test
    void createPost() throws Exception {
        // given
        PostSaveResponse saveResponse = PostSaveResponse.createPostSuccess(1L);
        PostSaveRequest post = PostSaveRequest.builder()
                .title(&quot;게시글 제목 입니다.&quot;)
                .content(&quot;게시글 내용 입니다.&quot;)
                .build();
        given(postService.createPost(any(PostSaveRequest.class))).willReturn(saveResponse);

        // when
        ResultActions result = mockMvc.perform(post(&quot;/posts&quot;)
                .content(objectMapper.writeValueAsString(post))
                .contentType(MediaType.APPLICATION_JSON_VALUE));

        // then
        result.andExpectAll(status().isCreated(),
                jsonPath(&quot;$.savedId&quot;).value(1L),
                jsonPath(&quot;$.message&quot;).value(&quot;게시글 작성을 완료했습니다.&quot;)
        // (3)
        ).andDo(
                document(&quot;post/create/success&quot;, // (4)
                        getDocumentRequest(), // (5)
                        getDocumentResponse(), // (6)
                        requestFields( // (7)
                                fieldWithPath(&quot;title&quot;).type(JsonFieldType.STRING).description(&quot;게시글 제목&quot;)
                                        .attributes(getConstraints(&quot;constraints&quot;, &quot;제목은 앞뒤 공백 제외 1 ~ 15자 사이여야 합니다.&quot;)),
                                fieldWithPath(&quot;content&quot;).type(JsonFieldType.STRING).description(&quot;게시글 내용&quot;)
                                        .attributes(getConstraints(&quot;constraints&quot;, &quot;내용은 공백 포함 1 ~ 1000자 사이여야 합니다.&quot;))
                        ),
                        responseFields( // (8)
                                fieldWithPath(&quot;savedId&quot;).type(JsonFieldType.NUMBER).description(&quot;저장된 게시글 id&quot;),
                                fieldWithPath(&quot;message&quot;).type(JsonFieldType.STRING).description(&quot;게시글 저장 성공 메시지&quot;)
                        )
                )
        );
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;(1) JUnit5d에서는 Spring Rest Docs를 이용하면 해당 애노테이션을 붙여주어야 합니다.&lt;/li&gt;
&lt;li&gt;(2) 모든 테스트를 시작하기 전에 webApplicationContext와 restDocumentation을 MockMvc에 설정해줍니다. (&lt;a href=&quot;https://docs.spring.io/spring-restdocs/docs/2.0.4.RELEASE/reference/html5/#getting-started-documentation-snippets-setup&quot;&gt;https://docs.spring.io/spring-restdocs/docs/2.0.4.RELEASE/reference/html5/#getting-started-documentation-snippets-setup&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;(3) : 위의 코드까지는 테스트 코드이고 아래 코드부터 실제 api에 대한 snippet을 생성합니다. (snippet은 일종의 api 문서 조각이라고 생각하면 됩니다.)&lt;/li&gt;
&lt;li&gt;(4) : snippet이 생성되는 위치를 지정합니다. post(게시글)/create(작성)/success(성공)을 의미합니다.&lt;/li&gt;
&lt;li&gt;(5) ~ (6) : 직접 커스텀한 별도의 유틸 메소드로 api 문서에서 호출 URL을 커스텀하고, 전체적인 요청과 응답을 &quot;이쁘게&quot;출력하기 위한 설정을 합니다. &lt;a href=&quot;https://github.com/JSCODE-EDU/project-class-HiiWee/blob/feat/day2/anonymous-board/src/test/java/com/example/anonymousboard/util/ApiDocumentUtils.java&quot;&gt;코드&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;(7) : 현재 테스트에서 request fields의 이름과 필드에 대한 설명(.description())을 지정합니다.&lt;/li&gt;
&lt;li&gt;(8) : 현재 테스트에서 response fields의 이름과 필드에 대한 설명(.description())을 지정합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;asciidoc 문서 작성하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 작성학 테스트를 실행하면 build/generated-snippets/ 폴더에 위의 (4)에서 지정한 디렉토리가 생성되고 내부에 다음과 같은 adoc 파일들이 존재합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/JSCODE-EDU/project-class-HiiWee/assets/66772624/19075090-bbad-42b5-ad3b-d2d1a2d06af5&quot; alt=&quot;image&quot; width=&quot;225&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주어진 요구사항에는 요청과 응답 형태에서 상당히 많은 정보를 요구하고 있지만&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/JSCODE-EDU/project-class-HiiWee/assets/66772624/bb9b22fa-60b4-4605-864a-14cfec2ffb22&quot; alt=&quot;image&quot; width=&quot;363&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 게시글 작성은 body params와 응답 형태만을 만족합니다. 이를 이용해 API 문서를 작성해보겠습니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 build.gradle 설정에서 index.html로 변경할 index.adoc의 위치를 &lt;code&gt;src/docs/asciidoc/&lt;/code&gt;으로 지정했기때문에 해당 위치에 &lt;code&gt;index.adoc&lt;/code&gt;파일을 생성합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 index.adoc 파일에서 다음과 같이 asciidoc을 작성합니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/JSCODE-EDU/project-class-HiiWee/assets/66772624/09e458a2-011a-4d22-aeca-52d0bb701572&quot; alt=&quot;image&quot; width=&quot;806&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;operation은 snippet을 조금 더 쉽게 추가할 수 있게 도와주는 기능으로 다음 의존성이 별도로 필요합니다. (&lt;code&gt;org.springframework.restdocs:spring-restdocs-asciidoctor&lt;/code&gt;)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;내부 snippet으로 요청과 응답 필드를 지정하고, 실제 request와 response를 지정하게되면 다음과 같이 렌더링 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/JSCODE-EDU/project-class-HiiWee/assets/66772624/3d786647-46b2-492d-a672-1ddb9d24357f&quot; alt=&quot;image&quot; width=&quot;723&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하여 간단하게 게시글 작성에 대한 성공 api 문서를 작성했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 렌더링 결과를 보면 제약사항이나 필수값에 대한 표는 현재 글에서 따로 언급하지 않았습니다. 해당 내용은 다음 글에서 더욱 자세히 설명하고 있습니다.&lt;br /&gt;&lt;a href=&quot;https://techblog.woowahan.com/2597/&quot;&gt;https://techblog.woowahan.com/2597/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 request fields, response fields 외에도 여러 속성 (헤더, 파라미터, pathvriable)등 다양한 속성들을 Spring Rest Docs로 지정할 수 있습니다. 그에 대한 내용은 스프링 공식문서에 자세히 설명되어 있습니다.&lt;br /&gt;&lt;a href=&quot;https://docs.spring.io/spring-restdocs/docs/2.0.4.RELEASE/reference/html5/#documenting-your-api&quot;&gt;https://docs.spring.io/spring-restdocs/docs/2.0.4.RELEASE/reference/html5/#documenting-your-api&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  마치면서&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Rest Docs를 처음 사용해본지 얼마 되지 않아 두 번째로 처음부터 Sprint Rest Docs에 대한 설정을 진행했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확실히 처음 접했을때보다 수월하게 했던 부분들도 있었고, 레퍼런스를 따라하는것만이 아니라 어느정도 이해를 하면서 코드를 작성할 수 있었습니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음부터 모든것을 이해하면 좋겠지만, Spring Rest Docs만 해도 알아야 할 내용들이 상당히 많았으며 코드양도 만만치 않았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 한 사이클씩 여러번 돌리다 보면 점점 더 익숙해지지 않을까 하는 생각이 듭니다 ㅎㅎ&lt;/p&gt;</description>
      <category>스터디/Spring Boot 스터디</category>
      <category>api 문서</category>
      <category>spring boot</category>
      <category>spring rest docs</category>
      <category>스터디</category>
      <author>HiiWee</author>
      <guid isPermaLink="true">https://hiiwee.tistory.com/39</guid>
      <comments>https://hiiwee.tistory.com/39#entry39comment</comments>
      <pubDate>Mon, 15 May 2023 06:28:52 +0900</pubDate>
    </item>
  </channel>
</rss>