<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>조금씩, 꾸준히, 자주</title>
        <link>https://velog.io/</link>
        <description>블로그 이사갔어요. https://jinny-l.tistory.com/</description>
        <lastBuildDate>Sun, 31 Mar 2024 12:30:02 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>조금씩, 꾸준히, 자주</title>
            <url>https://velog.velcdn.com/images/jinny-l/profile/51e3397a-ac58-495b-9997-1c79d9b87323/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. 조금씩, 꾸준히, 자주. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/jinny-l" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[Spring + S3] 비동기로 이미지 업로드 속도 개선]]></title>
            <link>https://velog.io/@jinny-l/spring-s3-async-image-upload</link>
            <guid>https://velog.io/@jinny-l/spring-s3-async-image-upload</guid>
            <pubDate>Sun, 31 Mar 2024 12:30:02 GMT</pubDate>
            <description><![CDATA[<h1 id="💬-들어가며">💬 들어가며</h1>
<p>이전에 중고 거래 플랫폼 프로젝트를 하면서 동기로 처리하고 있던 이미지 업로드 속도를 개선하기 위해 비동기로 개선했던 적이 있는데, 취준하느라 미루었던 기록을 이제 작성하고자 한다.</p>
<br/>

<hr>
<br/>

<h1 id="📸-비동기로-이미지-업로드-속도-개선">📸 비동기로 이미지 업로드 속도 개선</h1>
<h2 id="문제점">문제점</h2>
<p>해당 프로젝트는 당근 마켓과 같이 중고 거래 상품을 등록할 때 이미지를 등록할 수 있고,
이미지는 S3에 업로드되어 저장하고 있었다.</p>
<p>그런데 다음 코드 처럼 이미지 업로드 시 순차적으로 S3에 저장하고 있기 때문에 
여러 개의 이미지를 업로드하는 경우, 속도가 매우 느렸다.</p>
<blockquote>
<p><strong>ImageService.java</strong>
<img src="https://velog.velcdn.com/images/jinny-l/post/d88805a5-2192-4b7a-b410-0155a0c4018e/image.png" alt=""></p>
</blockquote>
<p>포스트맨으로 여러 번 확인했을 때 응답을 받기까지 평균 5~6초의 시간이 소요되었다.
테스트한 이미지는 3MB 크기의 10개 이미지였다. </p>
<blockquote>
<p><img src="https://velog.velcdn.com/images/jinny-l/post/636e5220-6d99-4797-8bd9-09fbfa597e63/image.png" alt=""></p>
</blockquote>
<p>이미지 업로드를 순차적으로 할 필요는 없다고 생각했기 때문에 속도를 개선하기 위해 생각이 났던 방법은 비동기 처리였다.</p>
<br/>

<hr>
<br/>

<h2 id="async-적용">@Async 적용</h2>
<p>비동기 처리를 처음해보기 때문에 <code>@Async</code> 어노테이션을 공부하고 적용해보았다.
<code>Spring</code>에서는 <code>@Async</code> 어노테이션으로 비동기 처리를 지원한다.</p>
<p>@Async를 적용하기 위해서 다음과 같은 순서로 진행했다.</p>
<blockquote>
<ol>
<li>비동기 처리에 필요한 스레드 풀을 만들기 위한 Config 파일 작성</li>
<li>메서드에 <code>@Async</code> 적용</li>
</ol>
</blockquote>
<br/>

<h3 id="1-비동기-처리에-필요한-스레드-풀을-만들기-위한-config-파일-작성">1. 비동기 처리에 필요한 스레드 풀을 만들기 위한 Config 파일 작성</h3>
<ul>
<li><code>@Async</code>를 적용하기 위해서는 <code>@EnableAsync</code>를 <code>Config</code> 클래스 혹은 프로그램 <code>Main</code> 메서드에 붙여줘야 한다.</li>
<li>어디에 붙이든 똑같이 동작하지만, 개인적으로 <code>Main</code> 메서드에 어노테이션이 덕지덕지 붙는게 싫어서 <code>Config</code>에 붙여주었다.</li>
</ul>
<pre><code class="language-java">@EnableAsync // 필수!
@Configuration
public class AsyncConfig {

    @Bean(name = &quot;imageUploadExecutor&quot;)
    public Executor imageUploadExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();

        executor.setThreadGroupName(&quot;imageUploadExecutor&quot;);
        executor.setCorePoolSize(10);
        executor.setMaxPoolSize(20);
        executor.setQueueCapacity(50);
        executor.initialize();

        return executor;
    }
}</code></pre>
<blockquote>
<p>💡참고: </p>
<ul>
<li><code>setCorePoolSize()</code>: 동시에 실행시킬 쓰레드의 개수를 설정</li>
<li><code>setMaxPoolSize()</code>: 스레드 풀의 최대 사이즈 설정</li>
<li><code>setQueueCapacity()</code>: 스레드 풀 큐의 사이즈 설정 <ul>
<li><code>corePoolSize</code> 개수를 넘어서는 <code>task</code>가 들어왔을 때 <code>queue</code>에 해당 <code>task</code>가 쌓인다.</li>
</ul>
</li>
</ul>
</blockquote>
<p>해당 프로젝트에서는 이미지를 최대 10개까지 업로드할 수 있기 때문에 <code>corePoolSize</code>를 10으로 설정했는데,
<code>maxPoolSize</code>랑 <code>queueCapacity</code>는 어떻게 설정하는게 좋을지 모르겠어서 우선 예제 따라서 설정해주었다.</p>
<br/>

<h3 id="2-메서드에-async-적용">2. 메서드에 <code>@Async</code> 적용</h3>
<p>그리고 이미지 업로드하는 메서드에 <code>@Async</code> 어노테이션을 적용했다</p>
<p><strong>ImageUploader.java</strong></p>
<pre><code class="language-java">    @Async(&quot;imageUploadExecutor&quot;)
    public URL uploadImage(MultipartFile multipartFile) {
        long startTime = System.currentTimeMillis();

        String uuid = UUID.randomUUID().toString();

        ObjectMetadata metadata = new ObjectMetadata();
        metadata.setContentLength(multipartFile.getSize());
        metadata.setContentType(multipartFile.getContentType());

        try {
            amazonS3.putObject(bucket, uuid, multipartFile.getInputStream(), metadata);
        } catch (IOException e) {
            throw new RuntimeException();
        }

        long endTime = System.currentTimeMillis();
        log.info(&quot;{} - 실행 시간: {}&quot;, Thread.currentThread().getName(), endTime - startTime);

        return amazonS3.getUrl(bucket, uuid);
    }</code></pre>
<p>과연.. 속도가 얼마나 개선되었을까 두근거리며 포스트맨으로 요청을 날렸다.</p>
<br/>

<h3 id="3-1-invalid-return-type-for-async-method-에러-발생">3-1. Invalid return type for async method 에러 발생</h3>
<p><del>(짠! 어림도 없지 ㅋㅋ)</del></p>
<p>에러 메시지 보니까 <code>return</code> 타입이 잘못된 것 같은데, 찾아보니까 <code>@Async</code>는 <code>return value</code>가 <code>void</code>인 경우만 적용 가능하다.</p>
<p><img src="https://velog.velcdn.com/images/jinny-l/post/adaa80cf-f93a-4086-92ef-856344b6f85b/image.png" alt=""></p>
<p>그런데 우리 프로젝트는 이미지를 업로드하고 <code>URL</code>을 프론트에게 응답해줘야 했기 때문에 <code>void</code>면 안된다.</p>
<p>그렇담 어떻게 해야할까?
<code>return value</code>가 필요하다면 <code>CompletableFuture</code>를 사용해야 한다.</p>
<br/>

<h3 id="3-2-메서드에-completablefuture-적용">3-2. 메서드에 CompletableFuture 적용</h3>
<p><strong>ImageUploader.java</strong></p>
<pre><code class="language-java">    @Async(&quot;imageUploadExecutor&quot;)
    public CompletableFuture&lt;URL&gt; uploadImage(MultipartFile multipartFile) {
        CompletableFuture&lt;URL&gt; future = new CompletableFuture&lt;&gt;();

        // 이미지 업로드 로직 생략...

        future.complete(amazonS3.getUrl(bucket, uuid));

        return future;
    }</code></pre>
<p><strong>ImageService.java</strong></p>
<pre><code class="language-java">    private List&lt;URL&gt; uploadImages(List&lt;MultipartFile&gt; files) {
        List&lt;CompletableFuture&lt;URL&gt;&gt; futures = new ArrayList&lt;&gt;();

        for (MultipartFile file : files) {
            futures.add(imageUploader.uploadImage(file));
        }

        List&lt;URL&gt; urls = new ArrayList&lt;&gt;();
        futures.forEach(future -&gt;
                urls.add(future.join())
        );

        return urls;
    }</code></pre>
<blockquote>
<p>⚠️ 주의:
<code>@Async</code>는 <code>AOP</code> 기반으로 작동되기 때문에 다음과 같은 경우 비동기 처리가 되지 않는다.</p>
<ol>
<li>프록시를 생성해야 하기 때문에 private 메서드에서는 작동하지 않는다.</li>
<li>마찬가지 이유로 동일 클래스에서 내부 호출하는 메서드에서는 작동하지 않는다.</li>
<li>Bean으로 관리되지 않고 있는 경우</li>
</ol>
<p>다행(?)히 프로젝트 구조상 <code>ImageService</code>랑 <code>ImageUpload</code>가 분리되어 있었서 정상적으로 적용되었지만 꼭 주의해서 사용하자!</p>
</blockquote>
<p>이젠 되겠지...?</p>
<br/>

<hr>
<br/>

<h2 id="결과">결과!!</h2>
<h3 id="포스트맨">포스트맨</h3>
<p>우와... 속도가 진짜 빨라졌다.
평균 <code>5.5</code>초 걸리던 업로드 속도가 평균 <code>0.75</code>초로 개선되었다.</p>
<p>동기 비동기 처리 속도가 이렇게 차이날 줄 몰랐다..</p>
<blockquote>
<p><img src="https://velog.velcdn.com/images/jinny-l/post/e3801fab-2505-4d1b-a3e4-9e5891d220be/image.png" alt=""></p>
</blockquote>
<blockquote>
<p><img src="https://velog.velcdn.com/images/jinny-l/post/3edc2f1d-f777-496a-9135-4929638a9da7/image.png" alt=""></p>
</blockquote>
<br/>

<h3 id="스레드-로그">스레드 로그</h3>
<p>실제로 비동기로 처리되고 있는지 로그도 확인해보았는데
멀티 스레드로 잘 동작하고 있었다. 🙂</p>
<p><img src="https://velog.velcdn.com/images/jinny-l/post/fff54480-8eb6-4107-be33-6539ac7385e2/image.png" alt=""></p>
<hr>
<h1 id="🔗-참고">🔗 참고</h1>
<ul>
<li><a href="https://velog.io/@cotchan/AsyncConfigurer%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EC%97%AC-return-value%EC%9E%88%EB%8A%94-Async-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0">https://velog.io/@cotchan/AsyncConfigurer%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EC%97%AC-return-value%EC%9E%88%EB%8A%94-Async-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0</a></li>
<li><a href="https://mangkyu.tistory.com/263">https://mangkyu.tistory.com/263</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Github Actions] error copy file to dest 에러]]></title>
            <link>https://velog.io/@jinny-l/Github-Actions-error-copy-file-to-dest-error</link>
            <guid>https://velog.io/@jinny-l/Github-Actions-error-copy-file-to-dest-error</guid>
            <pubDate>Tue, 06 Feb 2024 08:08:02 GMT</pubDate>
            <description><![CDATA[<h1 id="💣-trouble-shooting">💣 Trouble Shooting</h1>
<h2 id="상황">상황</h2>
<p><code>Github Actions</code>으로 배포할 때 <code>scp-action</code>을 통해 EC2로 파일을 전달하는 과정에서 에러가 발생했다.</p>
<p>이전까지 배포가 잘 되었고 에러 메시지만 보고서는 무슨 상황인지 파악이 어려워서 우선 에러 메시지로 구글링을 해보았다.</p>
<p><strong>에러 메시지</strong></p>
<p><img src="https://velog.velcdn.com/images/jinny-l/post/f3c4d1a7-dfd6-40d2-b547-30f83dfac3b4/image.png" alt=""></p>
<pre><code>error copy file to dest: ***, error message: Process exited with status 1

drone-scp error: error copy file to dest: ***, error message: Process exited with status 1</code></pre><br/>

<h2 id="원인">원인</h2>
<p>결론적으로 말하면 EC2에 디스크 용량이 꽉차서 파일 전달이 불가능한 것이었다.</p>
<p><a href="https://github.com/appleboy/scp-action/issues/69">Github Issue</a> 내용을 보다보니 저장공간 얘기가 있어서 &quot;혹시...?&quot; 나 했는데 공간이 100프로 차있었다.</p>
<blockquote>
<p><img src="https://velog.velcdn.com/images/jinny-l/post/e5705314-f86b-406e-b1cd-90f5107e840c/image.png" alt=""></p>
</blockquote>
<p> EC2 디스크 용량... 🤣
<img src="https://velog.velcdn.com/images/jinny-l/post/87f312c4-e0fa-416d-9885-61a4428e096d/image.png" alt=""></p>
<blockquote>
<p>💡 참고:</p>
<ul>
<li>EC2 디스크 용량 확인하는 명령어:<pre><code class="language-shell">df -h</code></pre>
</li>
<li>df는 disk free의 약자이고 h는 human의 약자다.</li>
<li>즉 디스크 용량을 사람이 보기 좋게 표기해준다. </li>
<li><code>-h</code> 옵션을 사용하지 않으면 M(메가), G(기가)등의 단위 표시가 되지 않는다.</li>
</ul>
</blockquote>
<br/>

<h2 id="해결">해결</h2>
<p>우선 디스크 확보를 하기 위한 방법을 찾아보았다.</p>
<h3 id="디스크-용량-확보-방법">디스크 용량 확보 방법</h3>
<p><strong>1. 필요하지 않은 패키지 삭제</strong></p>
<pre><code class="language-shell">sudo apt autoremove --purge</code></pre>
<p>이 명령어는 시스템에서 더 이상 필요하지 않은 패키지들을 자동으로 제거하고, 
그 패키지들과 관련된 설정 파일들도 함께 제거한다. </p>
<br/>

<p><strong>2. 캐시 정리</strong></p>
<pre><code class="language-shell">sudo apt-get autoclean</code></pre>
<p>이 명령어는 시스템에서 다운로드한 패키지 파일 중에서 사용되지 않는 오래된 버전의 파일을 정리한다.</p>
<br/>

<h3 id="write-error---write-28-no-space-left-on-device-에러-발생">Write error - write (28: No space left on device) 에러 발생</h3>
<p>하지만 나 같은 경우, 2번 방법으로는 디스크 용량을 충분히 확보하지 못했고 1번 방법은 에러가 발생했다.
디스크 용량이 부족해서 명령어 실행조차 불가능했던 것이다.</p>
<p><strong>에러 메시지</strong>
<img src="https://velog.velcdn.com/images/jinny-l/post/75fe3b83-7742-400e-bf59-fefb4658aa85/image.png" alt=""></p>
<pre><code>E: Write error - write (28: No space left on device)
E: IO Error saving source cache
E: The package lists or status file could not be parsed or opened.</code></pre><p>그래서 우선 <code>/var</code> 디렉토리에서 사용하지 않는데 용량을 크게 차지하는 파일을 삭제 후 명령어를 실행해서 문제를 해결했다.</p>
<blockquote>
<p>💡 참고:</p>
<ul>
<li>현재 디렉토리의 각 항목에 대한 디스크 사용량을 요약하여 표시하는 명령어<pre><code class="language-shell">sudo du -sh *</code></pre>
<img src="https://velog.velcdn.com/images/jinny-l/post/73094d41-15f4-47ea-b08e-dacda1246257/image.png" alt=""></li>
</ul>
</blockquote>
<hr>
<h1 id="🔗-reference">🔗 Reference</h1>
<ul>
<li><a href="https://m.blog.naver.com/hongganz/222437398559">https://m.blog.naver.com/hongganz/222437398559</a></li>
<li><a href="https://install-django.tistory.com/36">https://install-django.tistory.com/36</a></li>
<li><a href="https://suhyunsim.github.io/2021-09-27/Linux-disk">https://suhyunsim.github.io/2021-09-27/Linux-disk</a></li>
<li><a href="https://blog.naver.com/baek2sm/222198250764">https://blog.naver.com/baek2sm/222198250764</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring/JPA] Unable to locate class 에러]]></title>
            <link>https://velog.io/@jinny-l/SpringJPA-Unable-to-locate-class-error</link>
            <guid>https://velog.io/@jinny-l/SpringJPA-Unable-to-locate-class-error</guid>
            <pubDate>Fri, 08 Dec 2023 07:46:07 GMT</pubDate>
            <description><![CDATA[<h1 id="💣-trouble-shooting">💣 Trouble Shooting</h1>
<h2 id="상황">상황</h2>
<p><code>static inner class</code>를 <code>JPQL</code> <code>@Query</code>를 통해 조회하는 쿼리를 작성했는데 다음과 같은 에러를 마주했다.</p>
<h3 id="쿼리">쿼리</h3>
<pre><code class="language-java">@Query(&quot;SELECT new woowa.promotion.dto.response.CouponGroupSimpleResponse.CouponGroupSimpleDto(&quot;
            + &quot;couponGroup.id, couponGroup.title) FROM CouponGroup couponGroup &quot;
            + &quot;WHERE (:cursor = 0L OR couponGroup.id &lt; :cursor) &quot;
            + &quot;ORDER BY couponGroup.id DESC&quot;
    )</code></pre>
<h3 id="에러-메시지">에러 메시지</h3>
<p><img src="https://velog.velcdn.com/images/jinny-l/post/247af35d-82b4-4dbb-8bc4-d9472c2ed816/image.png" alt=""></p>
<pre><code>org.springframework.beans.factory.UnsatisfiedDependencyException: 
Error creating bean with name ...

...

Caused by: org.springframework.data.repository.query.QueryCreationException: 
Could not create query for public abstract java.util.Optional ...
Reason: Validation failed for query for method public abstract java.util.Optional

...

Caused by: java.lang.IllegalArgumentException: 
org.hibernate.hql.internal.ast.QuerySyntaxException: Unable to locate class 

...</code></pre><br/>

<hr>
<br/>

<h2 id="해결">해결</h2>
<p>처음에는 대소문자 문제나 오타가 있는줄 알고 쿼리를 눈빠지게 봤는데 문제가 없었다.</p>
<p>찾아보니 <code>inner class</code>의 경우 <code>$</code>로 표시해야 한다고 한다.</p>
<pre><code class="language-java">@Query(&quot;SELECT new woowa.promotion.dto.response.CouponGroupSimpleResponse$CouponGroupSimpleDto(&quot;
            // 생략
    )</code></pre>
<p>참고로 쿼리에 <code>$</code> 표시를 사용하게 되면 빨간줄이 뜨는데 무시하면 된다.</p>
<p><img src="https://velog.velcdn.com/images/jinny-l/post/d3eb0260-0081-45ed-85e0-596300b2e95e/image.png" alt=""></p>
<p>관련해서 Jetbrains 커뮤니티에 <a href="https://youtrack.jetbrains.com/issue/IDEA-298020">이슈</a>가 있는데 아직 해결이 안된 것 같다.</p>
<br/>

<hr>
<br/>

<h1 id="🔗-참고-링크">🔗 참고 링크</h1>
<ul>
<li><a href="https://stackoverflow.com/questions/11388840/java-compiled-classes-contain-dollar-signs/11388863#11388863">https://stackoverflow.com/questions/11388840/java-compiled-classes-contain-dollar-signs/11388863#11388863</a></li>
<li><a href="https://stackoverflow.com/questions/35503917/hql-and-inner-classes-such-as-builders">https://stackoverflow.com/questions/35503917/hql-and-inner-classes-such-as-builders</a></li>
<li><a href="https://docs.jboss.org/hibernate/orm/3.5/reference/en/html_single/#mapping-declaration-class">https://docs.jboss.org/hibernate/orm/3.5/reference/en/html_single/#mapping-declaration-class</a></li>
<li><a href="https://velog.io/@kwg527/Query%EB%A1%9C-Dto-%EC%A1%B0%ED%9A%8C-%EC%8B%9C-%EB%82%B4%EB%B6%80%ED%81%B4%EB%9E%98%EC%8A%A4-%EC%A0%91%EA%B7%BC-%EB%B0%A9%EB%B2%95">https://velog.io/@kwg527/Query%EB%A1%9C-Dto-%EC%A1%B0%ED%9A%8C-%EC%8B%9C-%EB%82%B4%EB%B6%80%ED%81%B4%EB%9E%98%EC%8A%A4-%EC%A0%91%EA%B7%BC-%EB%B0%A9%EB%B2%95</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring + Nginx + SSL] Invalid character found in method name,  SSL handshake failed 오류]]></title>
            <link>https://velog.io/@jinny-l/Spring-Nginx-SSL-Invalid-character-found-in-method-name-error</link>
            <guid>https://velog.io/@jinny-l/Spring-Nginx-SSL-Invalid-character-found-in-method-name-error</guid>
            <pubDate>Tue, 05 Dec 2023 08:45:43 GMT</pubDate>
            <description><![CDATA[<h1 id="💣-trouble-shooting">💣 Trouble Shooting</h1>
<h2 id="상황">상황</h2>
<p>팀프로젝트를 하며 처음으로 <code>SSL</code> 인증서를 발급받아 <code>https</code>로 배포를 해보았는데 
API 요청을 받았을 때 이런 오류를 맞이하게 되었다.</p>
<p>너무 이상했던 것은 이 오류가 뜨기 전 API 요청과 응답이 정상적으로 되고 있었고, 
당시 프론트/백 모두 휴식 기간을 가지며 코드를 수정한 적이 없었는데 갑자기 오류가 발생했다.</p>
<h3 id="spring-에러-로그">Spring 에러 로그</h3>
<p><img src="https://velog.velcdn.com/images/jinny-l/post/f4b5026f-3f24-4d3b-a7e3-38b9a23819ed/image.png" alt=""></p>
<pre><code class="language-java">o.apache.coyote.http11.Http11Processor   : Error parsing HTTP request header
 Note: further occurrences of HTTP request parsing errors will be logged at DEBUG level.

java.lang.IllegalArgumentException: Invalid character found in method name 
// 생략
. HTTP method names must be tokens</code></pre>
<p>구글링을 해보니 대부분 <strong>&quot;https가 활성화되지 않았는데 https로 요청하면 발생할 수 있는 오류로, http로 요청하면 해결된다&quot;</strong> 고 하는데 우리 상황에는 맞지 않은 것 같았다.</p>
<p>왜냐면 프론트/백 모두 코드를 수정하지 않았고 오류가 뜨기 전까지는 https로 통신했기 때문이다.</p>
<p>그래도 혹시 몰라 <code>http</code>로도 요청해보았는데 그래도 안됐다.
아 참고로 <code>Postman</code>으로는 <code>http/https</code> 모두 통신 가능했다.</p>
<p>도대체 뭐가 문제일까...?</p>
<p><code>WAS</code> 앞단에 <code>Nginx</code>가 있어 <code>Nginx</code> 에러 로그도 살펴보았다.</p>
<h3 id="nginx-에러-로그">Nginx 에러 로그</h3>
<p><img src="https://velog.velcdn.com/images/jinny-l/post/20eb6d1c-1cce-48d7-a31a-de576ac0eee1/image.png" alt=""></p>
<pre><code class="language-shell">SSL_do_handshake() failed (SSL: error:141CF06C:SSL routines:tls_parse_ctos_key_share:bad key share) while SSL handshaking</code></pre>
<p><code>Spring</code> 단의 에러 로그만 봤을 때는 도대체 무슨 오류인지 감이 잡히지 않았는데, 
<code>Nginx</code> 오류를 보니 <code>SSL</code> 관련 오류인 것 같았다.</p>
<p>에러 메시지로 구글링을 해보고 있던 차에 프론트 분이 웹브라우저에서 뜨는 에러 메시지도 전달주셨다.</p>
<h3 id="웹-브라우저-에러-메시지">웹 브라우저 에러 메시지</h3>
<p>웹 브라우저에서 이런 오류 메시지가 떴다고 한다.
(아쉽게도 스크린샷은 없었다 ㅠㅠ)</p>
<pre><code class="language-shell">ERR_SSL_VERSION_OR_CIPHER_MISMATCH</code></pre>
<p>이 에러 메시지까지 더해지니 <code>SSL</code> 관련 오류가 확실해보였다.
더불어 <code>CIPHER</code>라는 키워드까지 얻을 수 있었다!</p>
<br/>

<hr>
<br/>

<h2 id="원인">원인</h2>
<p><code>ERR_SSL_VERSION_OR_CIPHER_MISMATCH</code> 오류의 원인은 다음과 같다.</p>
<blockquote>
<ol>
<li>보안에 취약한 <code>SSL</code> 프로토콜을 사용하고 있거나, 취약한 암호묶음을 사용하고 있을 경우</li>
<li>서버에서 응답한 <code>Cipher Suites</code>(암호화 형식)을 브라우저에서 처리 할 수 없는 경우</li>
</ol>
</blockquote>
<p>일단 보안에 취약한 <code>SSL</code> 프로토콜을 사용하고 있지는 않았다.
크롬 브라우저 기준 <code>TLS 1.0</code> 혹은 <code>TLS 1.1</code>의 경우 발생할 수 있으나,
우리 서버에서는 <code>TLS 1.3</code>을 사용하고 있었다.</p>
<p>그래서 <code>Cipher Suites</code>나 암호 묶음 관련 오류가 아닐까 추측했다.
아니면 클라이언트-서버의 프로토콜이 호환되지 않거나, 아니면 암호화 알고리즘 협상 과정에서 실패한 것으로 생각이 든다.
(<code>Cipher Suites</code> 관련해서는 공부를 좀 더 해보고 별도로 다시 정리할 예정이다.)</p>
<br/>

<hr>
<br/>

<h2 id="해결">해결</h2>
<p>관련해서 <a href="https://stackoverflow.com/questions/65854933/nginx-ssl-error141cf06cssl-routinestls-parse-ctos-key-sharebad-key-share">스택오버플로우 글</a>을 참고해서 <code>Nginx</code>에 설정을 추가하니 해결되었다.</p>
<p>오류 해결은 됐지만 원인이 정확하게 파악이 되지 않아서 찝찝한 트러블 슈팅이었다.
<code>https</code>, <code>SSL</code>, <code>Nignx</code> 옵션, <code>Cipher</code> 등과 관련해서 공부를 좀 더 해봐야겠다.</p>
<pre><code>server {
    // 생략

    ssl_prefer_server_ciphers   on;

    location / {
         // 생략
    }
}</code></pre><p><strong><code>ssl_prefer_server_ciphers</code> 옵션</strong>
SSL/TLS 프로토콜에서 사용할 암호화 알고리즘을 지정하는 Nginx 옵션이다.
이 옵션은 클라이언트-서버 간 통신할 때 어떤 알고리즘을 사용할지에 대한 우선순위를 설정한다.</p>
<p>일반적으로 보안을 위해 <code>on</code>으로 설정하는 것이 권장된다.</p>
<ul>
<li><p><strong><code>on</code>:</strong></p>
<ul>
<li>서버 측의 암호화 알고리즘 우선순위에 따라 클라이언트의 제안을 무시하고 서버 측의 알고리즘을 사용</li>
</ul>
</li>
<li><p><strong><code>off</code>:</strong></p>
<ul>
<li>클라이언트가 제안한 알고리즘 중에서 서버가 가장 강력한 알고리즘을 선택</li>
</ul>
</li>
</ul>
<br/>

<hr>
<br/>


<h1 id="🔗-참고-링크">🔗 참고 링크</h1>
<ul>
<li><a href="https://serverfault.com/questions/905011/nginx-ssl-do-handshake-failed-ssl-error1417d18cssl/905019">https://serverfault.com/questions/905011/nginx-ssl-do-handshake-failed-ssl-error1417d18cssl/905019</a></li>
<li><a href="https://intro0517.tistory.com/200">https://intro0517.tistory.com/200</a></li>
<li><a href="https://1004lucifer.blogspot.com/2018/11/chrome-errsslversionorciphermismatch.html">https://1004lucifer.blogspot.com/2018/11/chrome-errsslversionorciphermismatch.html</a></li>
<li><a href="https://www.hostinger.com/tutorials/ssl-version-cipher-mismatch-error">https://www.hostinger.com/tutorials/ssl-version-cipher-mismatch-error</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Java] 동시성 테스트]]></title>
            <link>https://velog.io/@jinny-l/Java-Concurrency-Test</link>
            <guid>https://velog.io/@jinny-l/Java-Concurrency-Test</guid>
            <pubDate>Mon, 04 Dec 2023 08:38:12 GMT</pubDate>
            <description><![CDATA[<h1 id="💬-들어가며">💬 들어가며</h1>
<p>쿠폰 프로모션 프로젝트를 진행하며 쿠폰 발급 시 동시성 문제가 발생하는지 확인하는 테스트 코드를 작성해야 했다.
그런데 그동안 순차적으로 진행되는 테스트 코드만 작성해보아서 동시성 테스트 코드를 어떻게 작성하는지 알지 못했다.
그래서 동시성 테스트 코드는 어떻게 작성하는지 공부하고 그 내용을 정리해보려고 한다.</p>
<blockquote>
<p>프로젝트 레포지토리: <a href="https://github.com/woowa-coupons/woowa-coupons">https://github.com/woowa-coupons/woowa-coupons</a></p>
</blockquote>
<br/>

<hr>
<br/>

<h2 id="⌨️-쿠폰-발급-코드">⌨️ 쿠폰 발급 코드</h2>
<p>참고차 현재 프로젝트에서 쿠폰을 발급하는 코드의 일부를 발췌해 왔다.
쿠폰 발급 시, 프로모션의 조건을 확인하고 회원이 발급받을 수 있는 조건을 확인한 후 쿠폰을 발급한다.</p>
<pre><code class="language-java">    @Transactional
    public void issueCoupon(CouponIssueRequest request, Member member) {
        // 프로모션의 조건을 찾고
        List&lt;PromotionOption&gt; promotionOptions = promotionOptionRepository.findByPromotionId(request.promotionId());

        // 회원이 발급받을 수 있는 쿠폰 조건 확인
        CouponGroup allMatchedCouponGroup = promotionOptions.stream()
                .filter(promotionOption -&gt; isMemberSatisfied(member, promotionOption.getConditions()))
                .map(this::getCouponGroups)
                .findFirst()
                .flatMap(couponGroups -&gt; couponGroups.stream()
                        .filter(this::hasRemainCoupon)
                        .filter(couponGroup -&gt; !isExpiredCouponGroup(couponGroup))
                        .filter(couponGroup -&gt; !isAlreadyIssued(member, couponGroup))
                        .findFirst())
                .orElseThrow(() -&gt; new ApiException(CouponGroupException.NOT_FOUND));

        // 쿠폰 발급
        issueCouponInCouponGroup(allMatchedCouponGroup, member);
    }</code></pre>
<p>쿠폰이 발급되면 잔여 수량에서 -1을 한다.</p>
<pre><code class="language-java">    @Builder
    private Coupon() {
        // 생략
    }

    public void issue() {
        if (this.remainQuantity &lt;= 0) {
            throw new ApiException(CouponException.EXHAUSTED);
        }
        this.remainQuantity--;
    }</code></pre>
<br/>

<hr>
<br/>

<h2 id="✔️-동시성-테스트">✔️ 동시성 테스트</h2>
<p>그러면 이제 여러 회원이 동시에 쿠폰을 발급하면 잔여 수량만큼 발급되는지 확인이 필요하다.
동시성 테스트를 위한 방법을 찾아보았는데 <code>CountDownLatch</code>와 <code>ExecutorService</code>를 활용한 예제 코드가 많았다.</p>
<p>우선 예제 코드를 먼저 보고 <code>CountDownLatch</code>와 <code>ExecutorService</code>에 대해 알아보자.</p>
<br/>

<h3 id="테스트-코드">테스트 코드</h3>
<p>테스트 코드 흐름을 정리해보면 다음과 같다.</p>
<blockquote>
<ol>
<li><code>ExecutorService</code>를 통해 스레드 풀 생성</li>
<li>생성된 스레드 풀에서 비동기로 쿠폰 발급 요청</li>
<li>요청이 성공했을 경우 <code>successCount</code> +1, <code>실패했을 경우 failCount</code> +1</li>
<li><code>CountDownLatch</code>를 통해 비동기 작업이 끝났는지 확인</li>
<li>테스트 결과 학인</li>
</ol>
</blockquote>
<pre><code class="language-java">    @Test
    void issueCoupon() throws InterruptedException {
        // ---- 테스트를 위한 데이터 준비 ----
        // given
        Long promotionId = savePromotion(); // 쿠폰 발급을 위해 프로모션 생성

        int couponAmount = CouponFixture.추석_쿠폰_신규.getInitialQuantity(); // 쿠폰 개수
        int memberCount = couponAmount + 100; // 동시 요청하는 회원수

        ExecutorService executorService = Executors.newFixedThreadPool(30); // 스레드 풀 생성
        CountDownLatch latch = new CountDownLatch(memberCount); // CountDownLatch 생성

        AtomicInteger successCount = new AtomicInteger();
        AtomicInteger failCount = new AtomicInteger();

        // 쿠폰 발급에 필요한 회원 가입
        List&lt;Member&gt; members = new ArrayList&lt;&gt;();
        for (int i = 0; i &lt; memberCount; i++) {
            Member member = 랜덤_회원_가입();
            members.add(member);
        }

        // ---- 쿠폰 발급 ----
        // when
        for (int i = 0; i &lt; memberCount; i++) {
            Member member = members.get(i);

            executorService.execute(() -&gt; {
                try {
                    memberCouponService.issueCoupon(new CouponIssueRequest(promotionId), member);
                    successCount.incrementAndGet(); // 쿠폰 발급에 성공하면 successCount 증가
                } catch (Exception e) {
                    failCount.incrementAndGet(); // 쿠폰 발급에 실패하면 failCount 증가
                } finally {
                    latch.countDown();
                }
            });
        }

        latch.await();

        // ---- 테스트 결과 확인 ----
        // then
        List&lt;MemberCoupon&gt; memberCoupons = supportRepository.findAll(MemberCoupon.class);

        assertThat(memberCoupons.size()).isEqualTo(couponAmount); // 발급 받은 쿠폰 수가 발급된 쿠폰 개수와 일치하는지 확인
        assertThat(successCount.get()).isEqualTo(couponAmount);
        assertThat(failCount.get()).isEqualTo(100);
    }</code></pre>
<br/>

<h3 id="executorservice">ExecutorService</h3>
<blockquote>
<ul>
<li>공식 문서 링크: <ul>
<li><a href="https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ExecutorService.html">https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ExecutorService.html</a></li>
</ul>
</li>
<li>Baeldung 링크:<ul>
<li><a href="https://www.baeldung.com/java-executor-service-tutorial">https://www.baeldung.com/java-executor-service-tutorial</a></li>
</ul>
</li>
</ul>
</blockquote>
<p><code>ExecutorService</code>는 <code>java.util.concurrent</code>에서 제공하는 클래스다.</p>
<p>공식 문서에 따르면 <code>ExecutorService</code>는 <code>Executor</code>를 상속받은 인터페이스로, 동시에 여러 작업들을 싱행시키는 메서드를 제공하고 하나 이상의 비동기 작업 진행 상태를 추적하고 관리할 수 있는 메서드를 제공해주는 클래스이다.</p>
<blockquote>
<p><em>An Executor that provides methods to manage termination and methods that can produce a Future for tracking progress of one or more asynchronous tasks.</em></p>
</blockquote>
<p>비동기 작업을 지원하는 메서드 일부를 살펴보면 <code>execute()</code>와 <code>submit()</code>이 있다.</p>
<ul>
<li><strong><code>execute()</code>:</strong><ul>
<li>인자: <code>Runnable</code> 인터페이스</li>
<li>리턴 타입: <code>void</code></li>
</ul>
</li>
</ul>
<ul>
<li>*<em><code>submit()</code>: *</em><ul>
<li>인자: <code>Runnable</code>과 <code>Callable</code> 인터페이스</li>
<li>리턴 타입: <code>Future</code> 객체</li>
</ul>
</li>
</ul>
<p>테스트 코드 예제를 찾아보았을 때 두가지 메서드를 많이 사용하고 있었는데,
작업 결과가 필요하면 <code>submit()</code>을 쓰고 비동기 작업만 실행할 것이라면 <code>execute()</code>를 사용하면 될 것 같다는 생각이다.</p>
<blockquote>
<p>💡 참고:</p>
<ul>
<li><code>execute()</code>는 <code>Executor</code> 인터페이스에 정의 되어 있고, <code>submit()</code>은 <code>ExecutorService</code> 인터페이스에 정의되어 있다.</li>
<li><code>ExecutorService</code>는 <code>Executor</code>를 상속받았기 때문에 두 메서드 모두 호출할 수 있다.</li>
</ul>
</blockquote>
<br/>

<h3 id="countdownlatch">CountDownLatch</h3>
<blockquote>
<ul>
<li>공식 문서 링크: <ul>
<li><a href="https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/CountDownLatch.html">https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/CountDownLatch.html</a></li>
</ul>
</li>
<li>Baeldung 링크:<ul>
<li><a href="https://www.baeldung.com/java-countdown-latch">https://www.baeldung.com/java-countdown-latch</a></li>
</ul>
</li>
</ul>
</blockquote>
<p><code>CountDownLatch</code>도 <code>java.util.concurrent</code>에서 제공하는 클래스다.</p>
<p>공식 문서에 따르면 <code>CountDownLatch</code>는 1개 혹은 그 이상의 스레드가 다른 스레드의 작업이 완료될 때까지 기다릴 수 있게 도와주는 클래스이다.</p>
<blockquote>
<p><em>A synchronization aid that allows one or more threads to wait until a set of operations being performed in other threads completes.</em></p>
</blockquote>
<br/>

<h4 id="countdownlatch-메서드"><code>CountDownLatch</code> 메서드</h4>
<p>테스트 코드에 활용한 메서드를 살펴보자.</p>
<ul>
<li><strong>생성자</strong><ul>
<li>인자로 <code>Latch</code> 숫자 전달</li>
</ul>
</li>
</ul>
<pre><code class="language-java">CountDownLatch latch = new CountDownLatch(memberCount);</code></pre>
<br/>

<ul>
<li><strong><code>countDown()</code></strong><ul>
<li>호출 시, <code>Latch</code> 숫자가 1씩 감소</li>
</ul>
</li>
</ul>
<pre><code class="language-java">latch.countDown();</code></pre>
<br/>

<ul>
<li><strong><code>await()</code></strong><ul>
<li><code>Latch</code>의 숫자가 0이 될 때까지 대기</li>
</ul>
</li>
</ul>
<pre><code class="language-java">latch.await();</code></pre>
<br/>

<h4 id="동시성-테스트할-때-왜-countdownlatch가-필요할까">동시성 테스트할 때 왜 <code>CountDownLatch</code>가 필요할까?</h4>
<p>쿠폰 발급을 비동기 작업으로 여러 스레드에서 처리하면 모든 스레드의 작업이 언제 끝났는지 알 수가 없다.
따라서 <code>CountDownLatch</code>를 활용해 모든 스레드의 작업이 끝났는지 확인이 필요하다.</p>
<p><code>CountDownLatch</code>를 생성할 때 <code>Latch</code> 수를 작업 횟수만큼 설정하고, 작업이 한번 실행할 때마다 <code>latch.countDown()</code>를 호출한다.</p>
<p><code>latch.await()</code>를 통해 작업이 끝날 때까지 대기 후, <code>Assertions</code>으로 테스트 결과를 확인한다.</p>
<br/>

<hr>
<br/>

<h1 id="🔗-참고">🔗 참고</h1>
<ul>
<li><a href="https://parkadd.tistory.com/114">https://parkadd.tistory.com/114</a></li>
<li><a href="https://velog.io/@mohai2618/%EB%8F%99%EC%8B%9C%EC%84%B1-%ED%99%98%EA%B2%BD-%ED%85%8C%EC%8A%A4%ED%8A%B8%ED%95%98%EA%B8%B0">https://velog.io/@mohai2618/%EB%8F%99%EC%8B%9C%EC%84%B1-%ED%99%98%EA%B2%BD-%ED%85%8C%EC%8A%A4%ED%8A%B8%ED%95%98%EA%B8%B0</a></li>
<li><a href="https://codechacha.com/ko/java-countdownlatch/">https://codechacha.com/ko/java-countdownlatch/</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[우아한테크코스 6기] 3주차 미션 회고 - 로또(feat. EnumMap)]]></title>
            <link>https://velog.io/@jinny-l/woowa-precourse-lotto-reflection</link>
            <guid>https://velog.io/@jinny-l/woowa-precourse-lotto-reflection</guid>
            <pubDate>Tue, 07 Nov 2023 14:12:30 GMT</pubDate>
            <description><![CDATA[<h1 id="💬-들어가며">💬 들어가며</h1>
<p>이번주 미션은 로또 게임이다.
내가 작성한 코드와 PR은 밑에 링크에서 확인할 수 있다.</p>
<blockquote>
<ul>
<li>로또 미션 레포지토리: <a href="https://github.com/jinny-l/java-lotto-6">https://github.com/jinny-l/java-lotto-6</a></li>
<li>로또 미션 PR: <a href="https://github.com/woowacourse-precourse/java-lotto-6/pull/262">https://github.com/woowacourse-precourse/java-lotto-6/pull/262</a></li>
</ul>
</blockquote>
<p>메일에 보면 이번주 미션의 목표는 &quot;클래스 분리&quot;와 &quot;단위 테스트 작성&quot;이었다.
이전에도 로또 미션은 한번 구현해보았는데, 오히려 저번보다 이번에 머리가 더 아팠다.</p>
<p>이전 2번의 미션에 비해 이번 미션은 더 잘할 것이라고 생각했는데 코드를 짜면서 절망의 계곡에 와버렸다,,
객체지향은... 알면 알수록 어렵다.</p>
<blockquote>
<p><img src="https://velog.velcdn.com/images/jinny-l/post/2d951de3-e646-4e13-9cd8-b83624f05ce7/image.png" alt=""></p>
</blockquote>
<p>각설하고 회고를 시작해보겠다.</p>
<br/>

<hr>
<br/>

<h1 id="🔍-공부한-내용과-고민들">🔍 공부한 내용과 고민들</h1>
<h2 id="원시값-랩핑">원시값 랩핑</h2>
<p>클래스의 책임 분리를 더 잘해보기 위한 방법을 찾아보다가 &quot;원시값 랩핑&quot;에 대한 키워드가 많이 보였다.</p>
<p>그래서 로또 숫자 하나를 의미하는 <code>Integer</code>를 랩핑해 
우테코에서 제공한 <code>Lotto</code> 클래스의 <code>List&lt;Integer&gt;</code>를 <code>List&lt;LottoNumber&gt;</code>로 변경하였다.</p>
<p>원시값을 포장하기 전에는 <code>Lotto</code>가 <code>List</code>의 각 요소들이 1~45 사이의 숫자인지 검증하고, 길이가 6인지도 검증하고 등등 유효성 검증을 많이 해야 했다.</p>
<p>원시값을 포장하니 <code>LottoNumber</code>가 일부 유효성 검증을 책임지게 되면서 확실히 책임이 분리되는 것을 느꼈다.</p>
<p>단점 아닌 단점(?)으로는 원시값이 포장되었다 보니 값을 출력하기 위해 <code>toString()</code>을 <code>OverRiding</code>해야 했다.
아니면 <code>getter</code>를 사용하면 되는데 출력을 위한 메서드로는 <code>getter</code> 보다는 <code>toString()</code>를 더 깔끔하다고 느껴졌다.</p>
<p><strong>LottoNumber.class</strong></p>
<pre><code class="language-java">public record LottoNumber(int value) {

    public LottoNumber {
        // 생략
    }

    @Override
    public String toString() {
        return String.valueOf(value);
    }
}</code></pre>
<br/>

<h2 id="일급-컬렉션">일급 컬렉션</h2>
<p>또한 클래스 분리를 위해 일급 컬렉션을 활용했다.
원시값을 랩핑하고 일급 컬렉션을 활용하다보니 다음과 같은 구조가 되었다.</p>
<blockquote>
<ul>
<li><code>LottoNumber</code>: 1~45 사이의 숫자를 랩핑한 클래스(숫자 범위 검증 등 담당)</li>
<li><code>Lotto</code>: <code>List&lt;LottoNumber&gt;</code>를 랩핑한 클래스(중복 숫자 검증 등 담당)</li>
<li><code>Lottos</code>: <code>List&lt;Lotto&gt;</code>를 랩핑한 클래스</li>
</ul>
</blockquote>
<p>그런데 <code>Lottos</code>가 갑자기 붕 떠버렸다.
<code>Lottos</code>는 단순히 컬렉션을 랩핑하고, 내부에는 별다른 로직이 없다.</p>
<p>만약 고객이 구매할 수 있는 로또 장수에 제한이 있는 등 제약사항이 있다면 <code>Lottos</code>도 도메인 로직이 생기겠지만, 
현재 상태에서는 제약사항이 없기에 오버프로그래밍한 것은 아닌지 고민이 되었다.  </p>
<p>계속 고민을 하다가 일급 컬렉션의 장점 중 하나는 “컬렉션에 이름을 붙일 수 있다는 것”이라고 생각이 들었고 오버프로그래밍이라면 이로부터 파생될 수 있는 문제점도 직접 느껴보는 것이 좋을 것 같아서 
일단은 해당 클래스를 유지하는 것으로 결론을 내렸다. </p>
<h2 id="메서드-책임에-대한-고민">메서드 책임에 대한 고민</h2>
<h3 id="수익률을-계산하는-책임은-누구한테-있는건데">수익률을 계산하는 책임은 누구한테 있는건데...?</h3>
<p>처음에는 <code>Service</code> 없이 <code>Controller</code>에서 도메인 호출만 해서 프로그램을 구현했다.
그런데 그러다보니 누구에게도 속하기 애매한 로직이 생겼다. 바로 수익률을 계산하는 로직이다.</p>
<p>특히 이 두개 클래스 중 어디에 속해야 하는 로직인지 고민을 엄청 많이 했다.</p>
<blockquote>
<ul>
<li><code>Money</code>: &quot;구입 금액&quot;을 책임지는 도메인</li>
<li><code>LottoResult</code>: &quot;당첨 결과&quot;를 책임지는 도메인</li>
</ul>
</blockquote>
<p><code>Money</code>는 구입 금액을 알고 있고, <code>LottoResult</code>는 당첨 금액에 대해 알고 있다.
수익률을 계산하려면 이 두 금액에 대해 모두 알고 있어야 하는데... <code>getter</code>로 둘다 꺼내와서 외부에서 계산해야 하나 고민하다가 이 두 객체를 필드로 갖고 있는 <code>Profit</code>이라는 객체를 만들었다.</p>
<p>두 개의 객체 어디에도 속하기 애매한 로직인것 같고,
그렇다고 굳이 객체를 만들어야 하나 싶기도 하고... 
그렇다고 <code>Controller</code>나 <code>Service</code>에 <code>getter</code>로 값을 꺼내서 직접 계산하기에는 도메인 로직이 외부에 있고! 🤯
내가 느끼기에는 어디에 위치하기가 참 애매한 로직이었다.</p>
<p>고민했던 방법 중에 그나마 제일 났다고 판단한 방법은 그나마 <code>Profit</code> 객체를 만드는 것이었다.
(다른 더 좋은 방법은 없는지 미션 제출 시점까지, 끝까지 고민했던 부분인데 프리코스 끝나고 다시 찬찬히 봐야겠다.)</p>
<h2 id="enummap과-저장순서">EnumMap과 저장순서</h2>
<p>로또 당첨 정보 관련 정보들을 모아 <code>Enum</code>으로 구현했다.</p>
<p><strong>Rank.class</strong></p>
<pre><code class="language-java">public enum Rank {

    FIFTH(3, false, 5_000),
    FOURTH(4, false, 50_000),
    THIRD(5, false, 1_500_000),
    SECOND(5, true, 30_000_000),
    FIRST(6, false, 2_000_000_000),
    NO_RANK(0, false, 0);

    public final int matchedCount;
    public final boolean matchesBonusNumber;
    public final int prize;

    Rank(int matchedCount, boolean matchesBonusNumber, int prize) {
        this.matchedCount = matchedCount;
        this.matchesBonusNumber = matchesBonusNumber;
        this.prize = prize;
    }

    // 이하 생략
}</code></pre>
<p>해당 정보를 <code>view</code>에서 다음과 같이 출력하기 위해 당첨 정보를 <code>LottoResult</code> 클래스에 <code>EnumMap</code>을 활용하여 저장했다.</p>
<p>그리고 출력 시 사용하기 위해 <code>Collections.unmodifiableMap()</code>에 감싸서 값을 넘겨주고 있었는데 
순서 보장에 대한 의문이 들었다.</p>
<p><strong>출력 예시</strong></p>
<pre><code>3개 일치 (5,000원) - 1개
4개 일치 (50,000원) - 0개
5개 일치 (1,500,000원) - 0개
5개 일치, 보너스 볼 일치 (30,000,000원) - 0개
6개 일치 (2,000,000,000원) - 0개
총 수익률은 62.5%입니다.</code></pre><p><strong>LottoResult.class</strong></p>
<pre><code class="language-java">public class LottoResult {

    private final Map&lt;Rank, Long&gt; results = new EnumMap&lt;&gt;(Rank.class);

    public LottoResult(Map&lt;Rank, Long&gt; results) {
        // 생략
    }

    public Map&lt;Rank, Long&gt; getResults() {
        return Collections.unmodifiableMap(results);
    }

    // 생략
}
</code></pre>
<h3 id="enummap의-저장-순서"><code>EnumMap</code>의 저장 순서</h3>
<p><code>EnumMap</code>은 <code>Enum</code>에 정의된 순서로 저장된다. 
그런데 <code>Collections.unmodifiableMap()</code>으로 감싸도 순서가 보장이 될까?</p>
<p>출력할 때마다 순서가 바뀐다면 <code>LinkedHashMap</code>과 같은 다른 대안을 생각해야 되니까
얼릉 내부 코드를 살펴보자.</p>
<p><img src="https://velog.velcdn.com/images/jinny-l/post/6e9131d1-56fe-4e88-ac53-0c55c6046537/image.png" alt=""></p>
<p>두번째의 private 메서드를 살펴보면 <code>UnmodifiableMap</code>이라는 구현체를 반환할 때 인자로 받는 <code>Map</code>을 참조하고 있다.</p>
<p>그리고 스크린샷의 1504 라인쪽 메서드를 보면 삭제하거나 추가하는 등 
기존 <code>Map</code>을 수정하지 못하도록 막고 있는 것을 확인할 수 있다.</p>
<p>즉 원본 객체인 EnumMap을 그대로 참조하고 있기 때문에 순서가 보장된다.!
(이 말은 반대로 하면 원본 데이터가 바뀌면 참조하는 데이터도 순서가 바뀐다는 뜻이다.)</p>
<br/>

<hr>
<br/>

<h1 id="👋-마치며">👋 마치며</h1>
<p>미션 구현하면서 엄청 많은 고민들을 했는데, 이렇게 또 회고하면서 정리해보니 별로 많게 안 느껴지도 하는 것이...
코딩 능력과 더불어 글 쓰기 능력도 키워야겠다는 생각이 든다.</p>
<p>어느새 날씨가 쌀쌀해졌고, 프리코스도 절반 이상을 지나 이제 1개의 미션만 남겨두고 있다.
마지막까지 후회없이 최선을 다하자.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[우아한테크코스 6기] 2주차 미션 회고 - 자동차 경주(feat.DTO)]]></title>
            <link>https://velog.io/@jinny-l/woowa-precourse-6-racing-car-reflection</link>
            <guid>https://velog.io/@jinny-l/woowa-precourse-6-racing-car-reflection</guid>
            <pubDate>Wed, 01 Nov 2023 09:47:48 GMT</pubDate>
            <description><![CDATA[<h1 id="💬-들어가며">💬 들어가며</h1>
<p>이번주차 미션은 자동차 경주 게임이었다.
미션 요구사항과 필자가 작성한 코드는 밑에 링크에서 확인할 수 있다.</p>
<blockquote>
<ul>
<li>자동차 경주 미션 레포지토리: <ul>
<li><a href="https://github.com/jinny-l/java-racingcar-6">https://github.com/jinny-l/java-racingcar-6</a></li>
</ul>
</li>
<li>PR:<ul>
<li><a href="https://github.com/woowacourse-precourse/java-racingcar-6/pull/672">https://github.com/woowacourse-precourse/java-racingcar-6/pull/672</a></li>
</ul>
</li>
</ul>
</blockquote>
<p>이번주 미션을 진행하면서 구조 설계에 대해 많은 고민을 했던 것 같다.</p>
<p>구현하다보니 <code>Service</code>와 <code>Controller</code>는 비슷한 역할을 했고 역할을 나누는 것이 
오히려 오버 프로그래밍인 것 같다는 생각이 들어서 이번 미션에는 <code>Service</code>없이 <code>domain</code>과 <code>controller</code>로 구성하게 되었다.</p>
<p>그럼 회고를 시작해보자!!</p>
<br/>

<hr>
<br/>

<h1 id="🔍-공부한-내용과-고민">🔍 공부한 내용과 고민</h1>
<h2 id="테스트-코드">테스트 코드</h2>
<p>테스트 라이브러리의 존재는 알고 있었지만 
그동안 이런저런 이유로 학습을 미뤄왔었는데 이참에 기술부채를 좀 해결해보았다.</p>
<ul>
<li><p>일단은 <code>Junit</code>과 <code>AssertJ</code>에 대해 공부를 했고, 
많은 사람들이 가독성 측면에서 <code>AssertJ</code>를 더 선호한다는 것을 알게되었다.</p>
</li>
<li><p>여러 개의 <code>Assertion</code>이 있을 경우,  테스트 코드가 실패해도 모두 검증하고 결과를 보여줄 수 있는 방법에 대해 찾아보다가 <code>assertAll()</code>과 <code>SoftAssertions</code>의 차이점의 대해 공부했다.</p>
</li>
</ul>
<br/>

<hr>
<br/>

<h2 id="결과-출력을-위한-dto-사용">결과 출력을 위한 Dto 사용</h2>
<p>우선 Dto 사용 이유에 대해 이해를 돕기 위해 내가 했던 고민과 프로젝트 구조에 대해 간단하게 설명을 해보겠다.</p>
<h3 id="고민">고민</h3>
<p>사실 별거 아닌 고민일 수도 있지만,
회차별 결과를 바로바로 출력할지 경주가 끝날 때까지 결과를 모아두었다 한꺼번에 출력할지 고민했다.</p>
<p><strong>실행 결과 예시</strong></p>
<pre><code class="language-java">실행 결과
pobi : -
woni : 
jun : -

pobi : --
woni : -
jun : --

pobi : ---
woni : --
jun : ---</code></pre>
<p>게임이 좀 더 확장된다면 유저가 쫄깃한(?) 게임을 위해 라운드를 한번씩 누르며 게임을 진행할 수도 있겠지만
그런 요구사항은 없었기에 일단 주어진 그 자체로 생각하기로 했다.</p>
<p>한번씩 출력을 한다해도 사람이 시간 간극을 느끼지 못할정도로 동시에 출력이 될 것으로 생각했다.
그리고 <code>System.out.println()</code>은 무겁기 때문에 호출 횟수를 줄이고 싶었다.</p>
<p>그래서 <code>List</code>에 결과를 담아두었다가 결과를 한꺼번에 출력하게 되었는데,
그 과정에서 여러가지 문제점이 발생했다. 😅</p>
<br/>

<h3 id="프로젝트-구조">프로젝트 구조</h3>
<p>최종 프로젝트 구조는 다음과 같다.</p>
<blockquote>
<ul>
<li><p>도메인:</p>
<ul>
<li><code>Car.java</code>: 경주 게임에 참여하는 자동차 1대를 의미하는 클래스</li>
<li><code>Cars.java</code>: 경주 게임에 참여하는 자동차 여러 대를 담당하는 클래스</li>
<li><code>Game.java</code>: 경주 게임을 담당하는 클래스</li>
</ul>
</li>
<li><p>Dto</p>
<ul>
<li><code>RoundResult.java</code>: 경주 라운드별 결과를 담당하는 클래스</li>
</ul>
</li>
</ul>
</blockquote>
<p><strong>Game.java</strong></p>
<ul>
<li>Cars와 경주 횟수가 정해져야 Game 객체가 생성되고, <code>List&lt;RoundResult&gt;</code>에 모든 게임 결과가 저장된다.</li>
<li>결과를 출력할 때는 <code>List&lt;RoundResult&gt;</code>를 <code>StringBuilder</code>로 붙여서 한번에 출력한다.</li>
</ul>
<p>(<code>List&lt;RoundResult&gt;</code>도 <code>Repository</code>로 따로 분리할까하다가 오버프로그래밍인 것 같아서 <code>Game</code> 도메인 안에 두었다.)</p>
<pre><code class="language-java">public class Game { // 이해를 돕기 위해 첨부한 코드로, 생략된 부분이 있다.

    private final Cars cars;
    private final int numberOfAttempts;
    private final List&lt;RoundResult&gt; results;

    public Game(Cars cars, int numberOfAttempts) {
        this.cars = cars;
        this.numberOfAttempts = numberOfAttempts;
        this.results = new ArrayList&lt;&gt;();
    }

    public void moveCars() {
        for (int i = 0; i &lt; numberOfAttempts; i++) {
            cars.moveAll(getMovingNumbers());
            saveResult();
        }
    }

    private void saveResult() {
        results.add(RoundResult.from(cars));
    }
}</code></pre>
<br/>

<p>Dto가 만들어지기 까지 다음과 같은 시도가 있었다.</p>
<h3 id="첫번째-시도-listmapstring-integer">첫번째 시도: List&lt;Map&lt;String, Integer&gt;&gt;</h3>
<p><code>Dto</code>를 만들기 전에는 자동차 이름과, 이동한 자리를 <code>Map&lt;String, Integer&gt;</code>에 저장하고,
회차별 구분을 위해 <code>Map&lt;String, Integer&gt;</code>를 랩핑한 클래스를 만들었다.</p>
<p>그런데 코드 짤 때는 생각치도 못했던 문제를 테스트를 하면서 발견했는데, 
<code>HashMap</code>이라서 자동차들의 출력 순서가 보장되지 않는 문제가 있었다.</p>
<p><strong>RoundResult.java</strong></p>
<pre><code class="language-java">public record RoundResult(Map&lt;String, Integer&gt; carsPositions) {

    public static RoundResult from(Cars cars) {
        return new RoundResult(cars.getCars().stream()
                .collect(Collectors.toUnmodifiableMap(
                        Car::getName,
                        Car::getPosition
                )));
    }
}</code></pre>
<br/>

<h3 id="두번째-시도-listlinkedhashmapstring-integer">두번째 시도: List&lt;LinkedHashMap&lt;String, Integer&gt;&gt;</h3>
<p>출력 순서를 보장하기 위해 <code>LinkedHashMap</code>으로 변경했는데, <code>Car</code>로부터 변환을 위해 스트림의 <code>merge function</code>을 사용해야 했다.</p>
<p><code>merge function</code>에 대해 아직 완벽하게 이해가 되지 않아서 이해하지 못하는 코드를 쓰고 싶지 않았다.
(회고를 하면서 다시 보니 스트림이 오히려 <code>for</code>문 보다 가독성이 떨어지는 것 같아서 <code>for</code>문을 사용해도 좋았을 것 같다.)</p>
<p>그리고 외부에서 볼 때 Map의 <code>String</code>, <code>Integer</code>가 각각 무엇을 의미하는지 안보이는 것도 가독성이 떨어진다고 생각이 들었다.</p>
<pre><code class="language-java">public record RoundResult(LinkedHashMap&lt;String, Integer&gt; carsPositions) {

    public static RoundResult from(Cars cars) {
        LinkedHashMap&lt;String, Integer&gt; carsPositions = cars.getCars().stream()
                .collect(Collectors.toMap(
                        Car::getName,
                        Car::getPosition,
                        (existing, replacement) -&gt; existing,
                        LinkedHashMap::new
                ));
        return new RoundResult(carsPositions);
    }
}
</code></pre>
<br/>

<h3 id="세번째-시도-dto-생성">세번째 시도: Dto 생성</h3>
<p>앞선 두가지 문제점을 해결하기 위한 방법을 고민하다가 
문득 최근에 공부했던 inner class를 활용한 Dto를 만들어보면 어떨까하는 생각이 들었다.</p>
<blockquote>
<p><a href="https://velog.io/@jinny-l/spring-dto-management-inner-class">[Spring] DTO 관리 - Inner Class</a></p>
</blockquote>
<p><code>Dto</code> 구현을 통해 앞서 말한 두가지 문제점을 해결해보았다.</p>
<ul>
<li>출력 시 순서 보장</li>
<li>각 필드가 무엇을 의미하는지 표현</li>
</ul>
<pre><code class="language-java">
public record RoundResult(CarsDto carsDto) {

    public static RoundResult from(Cars cars) {
        return new RoundResult(CarsDto.from(cars));
    }

    public record CarsDto(List&lt;CarDto&gt; carDtos) {

        private static CarsDto from(Cars cars) {
            return new CarsDto(cars.getCars().stream()
                    .map(CarDto::from)
                    .toList());
        }
    }

    public record CarDto(String name, int position) {

        private static CarDto from(Car car) {
            return new CarDto(car.getName(), car.getPosition());
        }
    }
}
</code></pre>
<br/>

<hr>
<br/>

<h1 id="👋-마치며">👋 마치며</h1>
<p>자동차 경주 미션을 어느정도 마무리하고 우테코 커뮤니티에서 서로 PR 리뷰를 하였는데,
모든 사람들이 정말 열정이 가득했다.!</p>
<p>그 열정 속에서 남들 코드 보면서 내가 생각치 못한 부분도 배울 수 있었고,
내 코드에 대한 피드백을 통해 내가 작성한 코드에 대해 한번 더 돌아볼 수 있어서 좋았다!</p>
<p>그리고 점점 체력이 떨어지는 것을 느끼는데, 
이번주 회고도 떨어진 체력만큼 퀄리티가 좋지 않다고 느껴졌다..🥲</p>
<p>앞으로 꾸준히 코딩도 하고 글도 잘 쓰려면 체력 관리를 잘 해야겠다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[우아한테크코스 6기] 1주차 미션 회고 - 숫자 야구(feat.  랜덤 상황 테스트)]]></title>
            <link>https://velog.io/@jinny-l/woowa-precourse-6-baseball-reflection</link>
            <guid>https://velog.io/@jinny-l/woowa-precourse-6-baseball-reflection</guid>
            <pubDate>Tue, 24 Oct 2023 13:35:42 GMT</pubDate>
            <description><![CDATA[<h1 id="💬-들어가며">💬 들어가며</h1>
<p>우아한테크코스 6기 프리코스에 참여하게 되어 앞으로 4주간을 기록하고자 한다.</p>
<p>이번 1주차 미션은 근본의 숫자 야구 게임이었다.
필자가 작성한 코드는 밑에 첨부한 링크에서 확인 가능하다.</p>
<blockquote>
<p><a href="https://github.com/jinny-l/java-baseball-6">숫자 야구 Github Repository</a></p>
</blockquote>
<p>최근까지 Spring으로 프로젝트를 하다가 순수 Java로만 미션을 진행하니까 색다른 느낌이기도 하고 재미있었다. ㅎㅎ</p>
<p>1주차에는 기본에 충실하면서 남들이 읽기 쉬운 코드를 작성하는 것이 나만의 목표였다.
(과연 읽기 쉬운 코드를 짰는지...는 우테코 디스코드를 통해 PR 리뷰 요청을 해 피드백을 받아볼 예정이다.!)</p>
<blockquote>
<p>✏️ <strong>PR Review</strong>
블로그 보고 들어오시는 분도 있을 것 같아 개인 레포지토리에 리뷰용 PR을 올렸습니다.
↓↓ PR 리뷰는 언제나 환영입니다! ↓↓
<a href="https://github.com/jinny-l/java-baseball-6/pull/1">https://github.com/jinny-l/java-baseball-6/pull/1</a></p>
</blockquote>
<p>그리고 미션을 구현하면서 전반적인 설계나, 테스트 등 다양한 다양한 고민을 하게 되었다.</p>
<p>본글은 미션을 진행한 과정과 고민등에 대해 기록하고자 한다.
그럼 회고를 시작해보자!</p>
<br/>

<hr>
<br/>

<h1 id="🏃🏻-미션-진행-과정">🏃🏻 미션 진행 과정</h1>
<h2 id="✨-기능-목록-작성">✨ 기능 목록 작성</h2>
<p>&quot;기능을 구현하기 전 <code>docs/README.md</code>에 구현할 기능 목록을 정리해 추가한다.&quot;라는 요구사항이 있어 
우선 기능 목록을 작성하고 기능 구현을 시작했다.</p>
<p>처음부터 모든 목록을 다 작성한 것은 아니고 구현하면서 누락한 내용이 있으면 그때그때 추가했다.</p>
<p>그리고 커밋할 때 구현한 기능은 to do 박스에 체크하면서 진행했다.</p>
<p>사실 정신없이 구현하다보면 어디까지 구현이 되었고, 어느 부분이 남았는지 헤맬때가 있었다.
머리속에 다음에 뭐 해야하지?라는 갈고리(🤯)가 생겼는데, 이때 <strong>기능 목록</strong>이 길잡이가 되어주었다.</p>
<p>기능 목록에 따라 하나 하나 구현하다보니 기능 구현이 완료되었다.</p>
<blockquote>
<p><strong>README.md</strong>
(캡쳐하기에 너무 길어서 일부 내용만 캡쳐했다. 원본은 <a href="https://github.com/jinny-l/java-baseball-6/tree/jinny-l/docs">여기서</a> 확인할 수 있다.)</p>
<p><img src="https://velog.velcdn.com/images/jinny-l/post/6553825b-84c7-4585-a9f3-a89b07cb20b6/image.png" alt=""></p>
</blockquote>
<br/>

<hr>
<br/>

<h2 id="✅-테스트-케이스-목록-작성">✅ 테스트 케이스 목록 작성</h2>
<p>기능 구현을 다하고 기능 확인을 위해 테스트 코드를 작성했다.</p>
<p>테스트 코드 작성 전에도 기능 목록 작성한 것과 같이 <code>README</code>에 미리 테스트 케이스 목록을 만들었다.</p>
<p>테스트 코드를 짜다보면 어떤 케이스를 테스트했는지, 어떤 케이스를 테스트 안했는지 한눈에 안 보이기도 하고
코드를 짜면서 케이스를 생각하기 어렵다보니 목록을 짜게 되었다.</p>
<p>아직 테스트 코드 짜는 게 어렵지만...
테스트 코드를 짤 때도 케이스가 미리 정리되어 있다보니 한결 수월하게 진행할 수 있었던 것 같다.</p>
<blockquote>
<p><strong>README.md</strong>
(캡쳐하기에 너무 길어서 일부 내용만 캡쳐했다. 원본은 <a href="https://github.com/jinny-l/java-baseball-6/tree/jinny-l/docs">여기서</a> 확인할 수 있다.)</p>
<p><img src="https://velog.velcdn.com/images/jinny-l/post/66c10f21-3344-4b52-bf33-237cf2ea317a/image.png" alt=""></p>
</blockquote>
<br/>

<hr>
<br/>

<h1 id="🔍-공부한-내용과-고민">🔍 공부한 내용과 고민</h1>
<h2 id="☕️-java-17">☕️ Java 17</h2>
<p>작년 우테코 요구사항은 <code>Java 11</code>이었는데, 올해는 <code>Java 17</code>로 바뀌었다.
지금까지 <code>Java 11</code>만 썼는데 <code>Java 17</code>을 사용하기 위해 변경점을 공부했다.!</p>
<p>그리고 변경점만 공부하는 것보다는 올해에는 왜 요구사항이 바뀌었고 <code>Java 17</code>을 써야 하는지 
알고 쓰는 것이 좋을 것 같아서 관련 내용을 공부하고 블로깅하였다.</p>
<blockquote>
<p><a href="https://velog.io/@jinny-l/Java-17">[Java] Java 17을 사용해야 하는 이유와 Java 17 변경점</a></p>
</blockquote>
<p>다음 미션에는 <code>Java 17</code>에 추가된 기능을 하나씩 사용해볼 예정이다!</p>
<br/>

<hr>
<br/>

<h2 id="🧐-랜덤으로-생성된-숫자는-어떻게-테스트하지">🧐 랜덤으로 생성된 숫자는 어떻게 테스트하지?</h2>
<p>숫자 야구는 1부터 9까지 서로 다른 수로 이루어진 3자리의 수로 이루어져 있는데
미션 요구사항을 보면 이 숫자는 제공된 <code>Randoms</code> API를 활용하여 랜덤 값을 추출하여 만들어야 한다.</p>
<p>그때부터 고민이 시작되었다... 🤔</p>
<blockquote>
<p>컴퓨터가 생성한 숫자 야구가 있고, 플레이어의 입력 값에 대한 결과(스트라이크, 볼, 낫싱)를 테스트해야 하는데... 
숫자가 랜덤으로 생성되면 어떻게 테스트하지..? </p>
</blockquote>
<p>랜덤으로 생성된 숫자를 테스트하는 방법에 대해 찾아보다가 좋은 블로그 글을 발견하게 되어 인사이트를 얻게 되었다!</p>
<blockquote>
<p>🔗 <strong>참고한 블로그:</strong></p>
<ul>
<li><a href="https://velog.io/@jhp1115/%EC%9E%90%EB%8F%99%EC%B0%A8-%EA%B2%BD%EC%A3%BC-%EA%B2%8C%EC%9E%84-%EA%B5%AC%ED%98%84-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%BD%94%EB%93%9C%EC%97%90%EC%84%9C%EC%9D%98-%ED%8C%81#4-%ED%85%8C%EC%8A%A4%ED%8A%B8-%ED%95%98%EA%B8%B0-%EC%96%B4%EB%A0%A4%EC%9A%B4-%EB%B6%80%EB%B6%84%EC%9D%84-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EA%B0%80%EB%8A%A5%ED%95%98%EA%B2%8C-%EB%B0%94%EA%BE%B8%EB%8A%94-%EB%B2%95">https://velog.io/@jhp1115/자동차-경주-게임-구현-테스트-코드에서의-팁#4-테스트-하기-어려운-부분을-테스트-가능하게-바꾸는-법</a></li>
</ul>
</blockquote>
<p>바로 인테페이스로 추상화하는 것이다..!</p>
<ul>
<li><strong>랜덤 숫자 생성기를 인터페이스로 추상화하고</strong></li>
<li><strong><code>main</code>에서는 실제로 랜덤으로 숫자를 생성하는 클래스를 구현하고</strong></li>
<li><strong><code>test</code>에서는 의도한 숫자를 생성하는 클래스를 구현하면 테스트하기 수월해진다.</strong></li>
</ul>
<p>코드로 자세하게 알아보자.</p>
<h3 id="1-랜덤-숫자-생성기를-인터페이스로-추상화한다">1. 랜덤 숫자 생성기를 인터페이스로 추상화한다.</h3>
<p><strong>NumberGenerator.java</strong></p>
<pre><code class="language-java">@FunctionalInterface
public interface NumberGenerator {

    List&lt;Integer&gt; generate();
}
</code></pre>
<br/>

<h3 id="2-각-상황에-맞게-인터페이스를-구현한다">2. 각 상황에 맞게 인터페이스를 구현한다.</h3>
<p><strong>RandomNumberGenerator.java</strong> - <code>main</code>에서 사용하는 구현체
: 랜덤 숫자 3개를 리턴하게 구현</p>
<pre><code class="language-java">@Override
    public List&lt;Integer&gt; generate() {
        List&lt;Integer&gt; numbers = new ArrayList&lt;&gt;();
        while (numbers.size() &lt; BASEBALL_LENGTH) {
            int randomNumber = Randoms.pickNumberInRange(BASEBALL_START_NUMBER, BASEBALL_END_NUMBER);
            if (!numbers.contains(randomNumber)) {
                numbers.add(randomNumber);
            }
        }
        return numbers;
    }</code></pre>
<p><strong>TestNumberGenerator.java</strong> - <code>test</code>에서 사용하는 구현체
: 생성자 생성 시 입력받은 값을 그대로 리턴하게 구현</p>
<pre><code class="language-java">
public static class TestNumberGenerator implements NumberGenerator {

        private final List&lt;Integer&gt; numbers;

        TestNumberGenerator(List&lt;Integer&gt; numbers) {
            this.numbers = numbers;
        }

        @Override
        public List&lt;Integer&gt; generate() {
            return numbers;
        }
    }</code></pre>
<br/>

<h3 id="3-의도한-테스트를-진행한다">3. 의도한 테스트를 진행한다.</h3>
<p>다음 코드는 실제로 제출한 미션 코드에서 일부를 발췌한 내용이다.
필자는 <code>Service</code> 클래스에서 랜덤 숫자를 생성해 <code>Baseball</code>을 생성하게끔 구현해서 <code>Service</code> 테스트를 했는데, 각자 상황에 맞게 응용하면 될 듯 하다!</p>
<pre><code class="language-java">public class GameServiceTest {

    private GameService service;

    @BeforeEach
    void setUp() {
        // ↓ 의도한 숫자를 생성하는 TestNumberGenerator 생성(컴퓨터 Baseball이 1, 2 ,3으로 생성됨)
        service = new GameService(new TestNumberGenerator(List.of(1, 2, 3)));
        service.startGame();
    }

    @DisplayName(&quot;낫싱 상황일 때 맞는 결과를 리턴한다.&quot;)
    @Test
    void get_nothing_game_result() {
        // given
        service.setPlayerBaseball(List.of(4, 5, 6)); // 플레이어 Baseball을 4, 5, 6으로 생성

        // when
        GameResult gameResult = service.getGameResult();

        // then
        assertThat(gameResult).isEqualTo(GameResult.nothing()); // 낫싱 결과 예상
    }

    // ↓ 테스트용 NumberGenerator 구현
    public static class TestNumberGenerator implements NumberGenerator {

        private final List&lt;Integer&gt; numbers;

        TestNumberGenerator(List&lt;Integer&gt; numbers) {
            this.numbers = numbers;
        }

        @Override
        public List&lt;Integer&gt; generate() {
            return numbers;
        }
    }
}</code></pre>
<br/>

<hr>
<br/>

<h1 id="👋-마치며">👋 마치며</h1>
<p>미션도 해야하고, 공부한 내용 정리하고, 회고까지 하니까 1주일이 뚝딱이다. 🥲</p>
<p>마치기 전에 다음주에 개선하고 싶은 점을 쓰고 회고를 마무리하려고 한다.
이번주 미션하면서 다음 미션에는 꼭 개선해야지 했던 것이 있는데... 바로 테스트 코드이다.</p>
<p>기능 구현이 모두 완료되고 나서 테스트 코드를 짜니까 기능이 제대로 동작하는지 바로바로 확인이 어려웠고,
기능 구현 → 기능 확인 간의 호흡이 너무 긴 느낌이 들었다.</p>
<p>다음 목표는 1기능 1테스트 코드 짜는것을 목표로 진행하려고 한다!</p>
<blockquote>
<p>이번 미션에 참여한 분들 모두 고생하셨습니다! 🙇🏻</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Java] Java 17을 사용해야 하는 이유와 Java 17 변경점]]></title>
            <link>https://velog.io/@jinny-l/Java-17</link>
            <guid>https://velog.io/@jinny-l/Java-17</guid>
            <pubDate>Mon, 23 Oct 2023 14:37:02 GMT</pubDate>
            <description><![CDATA[<h2 id="💬-들어가며">💬 들어가며</h2>
<p>우아한테크코스 6기 프리코스에 참여하게 되었는데, 프로그래밍 요구사항이 <code>Java 17</code>이었다.</p>
<p>처음 Java를 공부하면서 지금까지 <code>Java 11</code>을 썼는데
최근 시작한 팀프로젝트도 팀원이 <code>Java 17</code>을 써보자해서 겸사겸사 <code>Java 17</code>에 대해 공부하고 내용을 정리하고자 한다! 💪</p>
<hr>
<br/>

<h2 id="☕️-왜-java-17을-사용해야-할까">☕️ 왜 Java 17을 사용해야 할까?</h2>
<blockquote>
<p>💡 참고:</p>
</blockquote>
<ul>
<li><code>Java 17</code>은 2021년 9월에 공개된 LTS 버전으로 Oracle JDK 기준 2029년 9월까지 지원된다.</li>
<li>공식 문서: <a href="https://openjdk.org/projects/jdk/17">https://openjdk.org/projects/jdk/17</a></li>
</ul>
<p>우선 Java 17을 사용해야 하는 이유에 대해 알아보자.</p>
<h3 id="java-lts-버전의-기술-지원-기간">Java LTS 버전의 기술 지원 기간</h3>
<p><code>Java 17</code> 이전의 LTS 버전은 <code>Java 8</code>과 <code>Java 11</code>이 대표적이다.
그런데 현재 서비스하고 있는 많은 레거시 프로젝트이 대부분 <code>Java 8</code>을 사용하고 있다.</p>
<p><code>Jetbrains</code>에서 제공하는 통계만 봐도 <code>Java 8</code>의 점유율이 앞도적인 것을 확인할 수 있다.</p>
<blockquote>
<ul>
<li><a href="https://www.jetbrains.com/ko-kr/lp/devecosystem-2021/java/#Java_which-versions-of-java-do-you-regularly-use">2021년 Java 버전별 점유율 - Jetbrains</a>
<img src="https://velog.velcdn.com/images/jinny-l/post/537ac284-ca26-47f6-9ed8-10d805558131/image.png" alt=""></li>
</ul>
</blockquote>
<p>이에 따라 현재 프로젝트들의 기술 지원을 위해 <code>Java 8</code>은 11보다 더 긴 지원 시간을 갖게 되었다.</p>
<ul>
<li>Java 버전별 기술 지원 기간:</li>
</ul>
<table>
<thead>
<tr>
<th>Java 버전</th>
<th>지원 종료 일자</th>
</tr>
</thead>
<tbody><tr>
<td>Java 8</td>
<td>~2030.12</td>
</tr>
<tr>
<td>Java 11</td>
<td>~2026.09</td>
</tr>
<tr>
<td>Java 17</td>
<td>~2029.09</td>
</tr>
</tbody></table>
<br/>


<h3 id="java-17을-사용해야-하는-이유">Java 17을 사용해야 하는 이유</h3>
<p><code>Java 17</code>을 사용하게 된 계기는 앞서 얘기한 바와 같이 개인적으로 학습이 주된 이유였다.</p>
<p>현업자인 제이든이 작성하신 <a href="https://techblog.gccompany.co.kr/%EC%9A%B0%EB%A6%AC%ED%8C%80%EC%9D%B4-jdk-17%EC%9D%84-%EB%8F%84%EC%9E%85%ED%95%9C-%EC%9D%B4%EC%9C%A0-ced2b754cd7">블로그</a> 글을 보면 현업에서는 다음과 같은 이유로 사용한다고 한다.</p>
<blockquote>
<ol>
<li>신규 버전을 위한 대비<ul>
<li>회사들은 <code>Java 8</code>의 지원 종료일이 다가옴에 따라 새로운 버전으로 마이그레이션을 준비해야 하는 상황</li>
<li>8 → 17 이후 버전으로 바로 마이그레이션하기에는 리스크가 있으니, 17버전의 기술 적응을 완료된 상태로 마이그레션을 하면 영향이 최소화 될 것</li>
</ul>
</li>
<li>다음 세대 플랫폼 호환을 위한 준비<ul>
<li><code>SpringBoot 3.0</code>부터 <code>Java 17</code> 이상을 지원하여 다음 세대 플랫폼 호환 준비도 주된 이유</li>
</ul>
</li>
</ol>
</blockquote>
<p>실제로 2022년 Java 버전별 점유율을 보면 <code>Java 8</code>은 감소하고 <code>Java 17</code>은 크게 증가한 것을 확인할 수 있다.</p>
<blockquote>
<ul>
<li><a href="https://www.jetbrains.com/ko-kr/lp/devecosystem-2022/java/#Java_which-versions-of-java-do-you-regularly-use">2022년 Java 버전별 점유율 - Jetbrains</a>
<img src="https://velog.velcdn.com/images/jinny-l/post/f5529d8a-0b69-4dc9-a6f1-4e4bd151db08/image.png" alt=""></li>
</ul>
</blockquote>
<hr>
<br/>

<h2 id="✨-java-17의-변경점">✨ Java 17의 변경점</h2>
<p>그럼 <code>Java 11</code>과 비교했을 때 <code>Java 17</code>의 변경점에 대해 알아보자.
참고로 해당 글에서는 모든 변경점을 다루지 않고 아래 몇가지 추가된 기능만 추려서 다룰 예정이다.</p>
<blockquote>
<ul>
<li>Text Block</li>
</ul>
</blockquote>
<ul>
<li>Record</li>
<li>Sealed Classes</li>
<li>Switch Expression</li>
<li>Stream.toList()</li>
</ul>
<h3 id="text-block">Text Block</h3>
<p><strong>Java 11</strong></p>
<p><code>Java 11</code> 버전에서는 <code>Json</code> 형식의 문자열을 다음과 같이 표현해야 했다.
한눈에 봐도 가독성이 매우 안좋은 것을 확인할 수 있다.</p>
<pre><code class="language-java">String jsonString = &quot;{\n&quot; +
    &quot;  \&quot;name\&quot;: \&quot;Jinny\&quot;,\n&quot; +
    &quot;  \&quot;age\&quot;: 20\n&quot; +
    &quot;}&quot;;</code></pre>
<p><strong>Java 17</strong></p>
<p><code>Java 17</code>에서는 텍스트 블록을 제공해서 3개의 큰 따옴표로 랩핑해서 표현할 수 있다.</p>
<pre><code class="language-java">String jsonString = &quot;&quot;&quot;
        {
          &quot;name&quot;: &quot;Jinny&quot;,
          &quot;age&quot;: 20
        }
        &quot;&quot;&quot;;</code></pre>
<h3 id="record">Record</h3>
<p><strong>Java 11</strong></p>
<p>롬복 어노테이션이 편리성을 제공해주긴 하지만...
<code>Dto</code> 클래스를 만들 때 여러 보일러 플레이트 코드를 추가해야 했다.</p>
<pre><code class="language-java">public class Dto {

    private final int data;

    public Dto(int data) {
        this.data = data;
    }

    public int getData() {
        return data;
    }
}</code></pre>
<p><strong>Java 17</strong></p>
<p><code>Java 17</code>에서는 <code>Record</code>를 활용하면 불필요한 코드를 제거할 수 있고, 클래스 자체로 명확한 의도를 표현할 수 있다.</p>
<pre><code class="language-java">public record Dto(
    int data
) {
}</code></pre>
<blockquote>
<p>💡 <strong>Record 특징:</strong></p>
</blockquote>
<ul>
<li>멤버변수는 private final로 선언된다.</li>
<li>필드별 getter가 자동으로 생성된다.</li>
<li>equals, hashcode, toString이 자동으로 생성된다.</li>
<li>기본생성자는 제공하지 않으므로 필요한 경우 직접 생성해야 한다.</li>
<li>final 클래스이므로 다른 클래스를 상속하거나/상속시킬 수 없다.</li>
<li>private final fields 이외의 인스턴스 필드를 선언할 수 없다.</li>
</ul>
<h3 id="sealed-class">Sealed Class</h3>
<ul>
<li><code>Sealed</code> 클래스는 상속하거나(extends), 구현(implements) 할 클래스를 지정해두고, 해당 클래스들만 상속/구현이 가능하도록 제한하는 기능이다.</li>
<li>이에 따라 개발자는 <code>Sealed</code> 클래스 코드만 봐도 어떤 클래스가 구현/상속했는지 쉽게 파악할 수 있게 되었다.</li>
<li>또한, 의도치 않은 클래스가 상속받았을 경우, 컴파일 시점에 에러를 체크할 수 있어 의도치 않은 실수를 방지할 수 있다.</li>
</ul>
<pre><code class="language-java">// Parent.java
public sealed class Parent permits Son, Daughter {
    ...
}


// Son.java
// permits 로 선언된 class 만 Parent class 를 상속할 수 있다.
public sealed class Son extends Parent {}</code></pre>
<blockquote>
<p><strong>Sealed Class 특징:</strong></p>
</blockquote>
<ul>
<li>super-class 에 <code>sealed</code> 키워드를 사용한다.</li>
<li><code>permits</code> 키워드 뒤에 해당 클래스를 상속받을 <code>sub-class</code>를 선언한다.</li>
<li><code>sealed</code> 된 클래스를 활용하기 위해서는 같은 모듈 혹은 같은 패키지 안에 존재 해야한다.</li>
</ul>
<h3 id="switch-expression">Switch Expression</h3>
<p><strong>Java 11</strong></p>
<p>기존의 <code>switch</code>문은 다수의 <code>case</code>와 <code>break</code>가 존재하며 불필요한 중복 코드가 발생하고 있다.</p>
<pre><code class="language-java">public static void printDayOfWeek(String dayOfWeek) {
    switch (dayOfWeek) {
        case &quot;Monday&quot;:
        case &quot;Tuesday&quot;:
        case &quot;Wednesday&quot;:
        case &quot;Thursday&quot;:
        case &quot;Friday&quot;:
            System.out.println(&quot;평일입니다.&quot;);
            break;
        case &quot;Saturday&quot;:
        case &quot;Sunday&quot;:
            System.out.println(&quot;주말입니다.&quot;);
            break;
    }</code></pre>
<p><strong>Java 17</strong>
변경된 문법에서는 똑같은 의미를 훨씬 간결하게 표현하고 있다.</p>
<pre><code class="language-java">public static void printDayOfWeek(String dayOfWeek) {
    switch (dayOfWeek) {
        case &quot;Monday&quot;, &quot;Tuesday&quot;, &quot;Wednesday&quot;, &quot;Thursday&quot;, &quot;Friday&quot; 
            -&gt; System.out.println(&quot;평일입니다.&quot;);
        case &quot;Saturday&quot;, &quot;Sunday&quot;
            -&gt; System.out.println(&quot;주말입니다.&quot;);
    }</code></pre>
<blockquote>
<p><strong>변경된 Switch문 문법:</strong></p>
</blockquote>
<ul>
<li>-&gt; 화살표를 사용해 실행문을 나타낸다.</li>
<li>조건들을 <code>,</code>를 이용해 나열할 수 있다.</li>
<li><code>break</code>를 사용하지 않는다.</li>
</ul>
<p>또한 출력문 안에서 사용이 가능해졌다.</p>
<pre><code class="language-java">public static void printDayOfWeek(String dayOfWeek) {
    System.out.println(
            switch (dayOfWeek) {
                case &quot;Monday&quot;, &quot;Tuesday&quot;, &quot;Wednesday&quot;, &quot;Thursday&quot;, &quot;Friday&quot; -&gt; System.out.println(&quot;평일입니다.&quot;);
                case &quot;Saturday&quot;, &quot;Sunday&quot; -&gt; System.out.println(&quot;주말입니다.&quot;);
                default -&gt; &quot;디폴트 값입니다.&quot;;
            }
        );
    }
</code></pre>
<h3 id="streamtolist">Stream.toList()</h3>
<p>기존에 <code>Collectors.toList()</code>를 <code>.toList()</code>로 표현할 수 있게 되었다.</p>
<p><strong>Java 11</strong></p>
<pre><code class="language-java">List&lt;Integer&gt; numbers = List.of(1, 2, 3, 4, 5);

List&lt;Integer&gt; result = numbers.stream()
        .filter(number -&gt; number &gt; 1)
        .collect(Collectors.toList());</code></pre>
<p><strong>Java 17</strong></p>
<pre><code class="language-java">List&lt;Integer&gt; numbers = List.of(1, 2, 3, 4, 5);

List&lt;Integer&gt; result = numbers.stream()
        .filter(number -&gt; number &gt; 1)
        .toList(); // 변경
System.out.println(result);</code></pre>
<blockquote>
<p>🔍 <strong>그렇다면 두 메서드는 완전 동일한 것으로 볼 수 있을까?</strong>
<code>JavaDocs</code>를 살펴보면 두 메서드의 return type이 다르다.</p>
<p><strong><code>Collectors.toList()</code></strong>
: <code>ArrayList</code>를 리턴하고 있으며, 불변 타입이 아니다.</p>
<p><img src="https://velog.velcdn.com/images/jinny-l/post/424e028d-ea43-4c15-9c8d-66d2d9dd590c/image.png" alt=""></p>
<p><strong><code>toList()</code></strong>
:불변 <code>List</code>를 리턴한다.</p>
<p><img src="https://velog.velcdn.com/images/jinny-l/post/88d0d549-9262-4833-a77e-2d71be308853/image.png" alt=""></p>
</blockquote>
<blockquote>
<p>🔍 <strong>그렇다면 <code>toList()</code>는 Collectors.toUnmodifiableList() 과 동일할까?</strong></p>
<ul>
<li>JavaDocs에 따르면 <code>Collectors.toUnmodifiableList()</code>는 내부에서 <code>null</code> 체크를 하고 있지만, <code>toList()</code>는 null 체크를 하지 않아 null 값이 들어갈 수 있다.</li>
</ul>
<blockquote>
<p><em>The returned Collector disallows null values and will throw NullPointerException if it is presented with a null value.</em></p>
<p><em>번역: 반환된 Collector는 null 값을 허용하지 않으며, null 값을 전달받으면 NullPointerException을 throw합니다.</em></p>
</blockquote>
<p><strong><code>Collectors.toUnmodifiableList()</code></strong></p>
<p><img src="https://velog.velcdn.com/images/jinny-l/post/4f9368bc-5e19-4a34-8723-e55b3a8527c1/image.png" alt=""></p>
</blockquote>
<hr>
<h2 id="🔗-참고-자료">🔗 참고 자료:</h2>
<ul>
<li><a href="https://velog.io/@ililil9482/Java17%EC%9D%84-%EA%B3%A0%EB%A0%A4%ED%95%B4%EC%95%BC%ED%95%A0%EA%B9%8C">https://velog.io/@ililil9482/Java17%EC%9D%84-%EA%B3%A0%EB%A0%A4%ED%95%B4%EC%95%BC%ED%95%A0%EA%B9%8C</a></li>
<li><a href="https://techblog.gccompany.co.kr/%EC%9A%B0%EB%A6%AC%ED%8C%80%EC%9D%B4-jdk-17%EC%9D%84-%EB%8F%84%EC%9E%85%ED%95%9C-%EC%9D%B4%EC%9C%A0-ced2b754cd7">https://techblog.gccompany.co.kr/%EC%9A%B0%EB%A6%AC%ED%8C%80%EC%9D%B4-jdk-17%EC%9D%84-%EB%8F%84%EC%9E%85%ED%95%9C-%EC%9D%B4%EC%9C%A0-ced2b754cd7</a></li>
<li><a href="https://binux.tistory.com/146">https://binux.tistory.com/146</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Nginx + Spring] 413 Request Entity Too Large 오류]]></title>
            <link>https://velog.io/@jinny-l/Nginx-Spring-413-Request-Entity-Too-Large-Error</link>
            <guid>https://velog.io/@jinny-l/Nginx-Spring-413-Request-Entity-Too-Large-Error</guid>
            <pubDate>Mon, 09 Oct 2023 04:06:53 GMT</pubDate>
            <description><![CDATA[<h1 id="413-request-entity-too-large-오류">413 Request Entity Too Large 오류</h1>
<h2 id="상황">상황</h2>
<p>현재 프로젝트의 요구사항 중 &quot;이미지 업로드 기능&quot;이 있어 기능 개발 후 로컬에서 테스트를 하고 배포를 하니 413 오류가 발생했다.</p>
<p>파일 1, 2개 정도는 업로드가 되는데 3~4개 정도를 넘어가면 오류가 발생했다.</p>
<p>설정 파일에 분명 업로드 가능한 파일 사이즈를 넉넉히 설정했는데 말이다.</p>
<p><strong>application.yml 파일 설정</strong></p>
<pre><code class="language-yml">  servlet:
    multipart:
      enabled: true
      max-file-size: 30MB
      max-request-size: 30MB</code></pre>
<h2 id="원인-및-해결">원인 및 해결</h2>
<p>서버에서 오류 로그를 살펴보았는데 로그가 안찍혀 있었다.
로그가 안찍혔으니 요건 또 분명히 <code>Nginx</code> 설정 관련 문제겠거니 싶었다.</p>
<p>찾아보니, 파일 업로드 크기를 따로 설정해주지 않으면 기본 설정이 1M라고 한다.
<del>(그러니 파일 3</del>4개 이상 업로드하면 오류가 발생했지...ㅠ)~~</p>
<p>다음과 같이 설정을 추가하니 오류가 해결되었다.</p>
<pre><code class="language-shell">http {
    client_max_body_size 30M;
}</code></pre>
<blockquote>
<p>💡 참고:
만약 Nginx 설정을 추가했는데 501 에러가 발생한다면 
위와 같이 application 설정에 multipart 파일의 업로드 크기와 관련된 설정을 했는지 확인해야 한다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[React + Nginx + Spring] WS Handshake 오류: "Handshake failed due to invalid Upgrade header: null"]]></title>
            <link>https://velog.io/@jinny-l/WS-Handshake-Error-Handshake-failed-due-to-invalid-Upgrade-header-null</link>
            <guid>https://velog.io/@jinny-l/WS-Handshake-Error-Handshake-failed-due-to-invalid-Upgrade-header-null</guid>
            <pubDate>Mon, 09 Oct 2023 03:45:50 GMT</pubDate>
            <description><![CDATA[<h1 id="ws-handshake-오류">WS Handshake 오류</h1>
<h2 id="상황">상황</h2>
<p>현재 프로젝트의 개발 서버 환경은 <code>EC2</code>로, <code>Spring Boot + NGINX</code>로 구성되어 있다.
프론트가 개발 서버로 <code>Stomp</code> 통신을 위해 <code>Web Socket Handshake</code> 요청을 보내는데 문제가 생겼다.</p>
<br/>

<h2 id="문제">문제</h2>
<ol>
<li>프론트에서 <code>Handshake</code> 요청 시 백엔드로 넘어오기 전에 Fail 오류 발생
(웹브라우저에서 오류 로그를 볼 수 있으나 백엔드 서버에서는 아무런 로그가 찍혀있지 않았음)</li>
<li>1번 문제를 해결하고 나니 <code>&quot;Handshake failed due to invalid Upgrade header: null&quot;</code> 오류 발생
(백엔드 서버에 로그 찍힘)
<img src="https://velog.velcdn.com/images/jinny-l/post/6fc3f608-2655-4bc7-8512-b876f06b4d1d/image.png" alt=""></li>
</ol>
<br/>

<h2 id="고민">고민</h2>
<p>일단 상황을 돌아보면서 원인의 근원지를 추측해보았다.</p>
<ul>
<li>프론트의 문제인지, 프론트 -&gt; 백엔드로 넘어오는 사이에 모종의 이유로 유실된 것인지 파악이 필요</li>
<li>local에서 <a href="https://apic.app/online/#/tester">Apic</a>으로 테스트 했을 때는 문제가 없었는데 서버로 올라갔을 때 문제가 발생했음</li>
</ul>
<p>그래서 일단 프론트 문제가 아니고 프론트 -&gt; Nginx -&gt; 백엔드로 요청하면서 발생한 문제로, 
1번 문제는 로그가 안 찍혀서 Nginx 이슈이고, 2번 문제는 백엔드 문제라고 생각이 들었다.</p>
<br/>

<h2 id="원인-및-해결">원인 및 해결</h2>
<h3 id="1-handshake-요청-시-백엔드로-넘어오기-전에-fail-오류-발생">1. <code>Handshake</code> 요청 시 백엔드로 넘어오기 전에 Fail 오류 발생</h3>
<p><strong>원인</strong>
이 문제는 프론트 코드와도 관련이 있는 것으로 생각이 드는데, 예제 코드에는 대부분 다음과 같이 <code>SockJS</code> 연결 위에 <code>Stomp</code> 프로토콜을 설정했다.</p>
<p>그런데 우리 프론트는 <code>Stomp</code> 클라이언트를 설정하고 생성하는 코드밖에 없었다. </p>
<blockquote>
<p>예제 코드)</p>
</blockquote>
<pre><code class="language-javascript">var sockJS = new SockJS(&quot;/ws&quot;);
var ws = Stomp.over(sockJS);</code></pre>
<blockquote>
<p>우리 프론트 코드)</p>
</blockquote>
<pre><code class="language-javascript">import { Client } from &quot;@stomp/stompjs”;
const client = new Client();</code></pre>
<p><strong>해결</strong>
반면 백엔드 설정은 <code>SockJS</code> 연결 위에 <code>Stomp</code> 프로토콜을 설정하고 있었다.
프론트엔드 백엔드 환경이 달라서 에러가 발생한다고 생각이 들어 백엔드 설정을 바꿔보았다.</p>
<p>(프론트엔드 설정 바꿔보는 테스트도 해보고 싶었는데, 시간 부족으로 다음에 프론트분이랑 같이 테스트 해보기로 했다. 테스트한 결과가 나오면 블로그도 업데이트할 예정이다.)</p>
<pre><code class="language-java">@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint(&quot;/ws&quot;)
                .setAllowedOriginPatterns(&quot;*&quot;);
                // .withSockJS(); // 해당 옵션 삭제
    }</code></pre>
<br/>

<h3 id="2-handshake-failed-due-to-invalid-upgrade-header-null-오류">2. &quot;Handshake failed due to invalid Upgrade header: null&quot; 오류</h3>
<p>첫번째 문제 해결 후 <code>handshake</code>에 실패했다.
오류 로그를 보니 업그레이드 <code>header</code>가 없다는 것인데, trace 원인도 안찍히고 딸랑 저것만 찍혀있다.</p>
<p><img src="https://velog.velcdn.com/images/jinny-l/post/67699f9a-a861-49f4-a2b6-b6f9fe16808b/image.png" alt=""></p>
<p>지금까지의 경험상 로컬에서 문제가 없는데 배포하면서 문제가 발생했다면, 특히 <code>http header</code> 관련이면 <code>Nginx</code> 설정 관련 오류일 가능성이 높아서 공식 문서를 찾아보았다.</p>
<p><strong>원인</strong>
<a href="http://nginx.org/en/docs/http/websocket.html">Nginx 공식 문서</a>에 따르면 다음과 같다.</p>
<blockquote>
<p>&quot;Upgrade&quot;는 홉별 헤더이므로 클라이언트에서 프록시 서버로 전달되지 않습니다. 
리버스 프록시에서는 클라이언트가 프록시 서버를 알지 못하며 프록시 서버에서 특별한 처리가 필요합니다.
프록시 서버가 클라이언트의 <code>WebSocket</code>으로의 프로토콜 전환 의도를 알기 위해서는 이러한 헤더를 명시적으로 전달해야 합니다.</p>
</blockquote>
<pre><code class="language-shell">location /chat/ {
    proxy_pass http://backend;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection &quot;upgrade&quot;;
}</code></pre>
<p>즉, &quot;Upgrade&quot; 및 &quot;Connection&quot;을 포함한 홉별 헤더는 클라이언트에서 프록시 서버로 전달되지 않기 때문에 
handshake 과정에서 헤더를 찾지 못해 발생한 문제로 Nginx 설정에 위 내용을 추가하니 해결이 되었다.</p>
<h1 id="참고">참고</h1>
<ul>
<li><a href="http://nginx.org/en/docs/http/websocket.html">http://nginx.org/en/docs/http/websocket.html</a></li>
<li><a href="https://7357.tistory.com/300">https://7357.tistory.com/300</a></li>
<li><a href="https://velog.io/@myway00/Handshake-failed-due-to-invalid-Upgrade-header-null">https://velog.io/@myway00/Handshake-failed-due-to-invalid-Upgrade-header-null</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] DTO 관리 - Inner Class]]></title>
            <link>https://velog.io/@jinny-l/spring-dto-management-inner-class</link>
            <guid>https://velog.io/@jinny-l/spring-dto-management-inner-class</guid>
            <pubDate>Mon, 02 Oct 2023 12:31:02 GMT</pubDate>
            <description><![CDATA[<h1 id="🧐-고민">🧐 고민</h1>
<p>프로젝트 규모가 커질수록 <code>DTO</code>가 많아져 관리 방법에 대한 고민이 생겼다.
특히 <code>Json</code> 응답에 랩핑된 형태의 개수만큼 DTO가 늘어났다.</p>
<p>예를 들면 다음과 같은 응답이 형태가 있다면, 
<code>SellerDto</code>, <code>CategoryDto</code>, <code>AddressDto</code>를 만들어야 한다.</p>
<pre><code class="language-json">
    &quot;product&quot;: {
        &quot;seller&quot;: { 
            &quot;id&quot;: 1,
            &quot;nickname&quot;: &quot;나판매자ㅋ&quot;

        },
        &quot;category&quot;: {
            &quot;id&quot;: 7,
            &quot;name&quot;: &quot;가구/인테리어&quot;
        },
        &quot;address&quot;: {
            &quot;id&quot;: 1,
            &quot;name&quot;: &quot;역삼 1동&quot;
        },
        &quot;title&quot;: &quot;자전거&quot;,
        &quot;contents&quot;: &quot;싸게 팔아요&quot;
    }
</code></pre>
<p>최종적으로<code>ProductDto</code>을 만들고 DTO를 조합하면 최소 4개의 파일이 필요하다.</p>
<p>완성된 클래스 형태는 대략 다음과 같겠다.</p>
<p><strong>ProductDto</strong></p>
<pre><code class="language-java">public class ProductDto {

    private final SellerDto seller;
    private final CategoryDto category;
    private final AddressDto address;
    private final String title;
    private final String contents;

    }</code></pre>
<p><strong>SellerDto</strong></p>
<pre><code class="language-java">public class SellerDto {

    // 생략...

    }
</code></pre>
<p><strong>CategoryDto</strong></p>
<pre><code class="language-java">public class CategoryDto {

    // 생략...

    }
</code></pre>
<p><strong>AddressDto</strong></p>
<pre><code class="language-java">public class AddressDto {

    // 생략...

    }
</code></pre>
<hr>
<br/>

<h2 id="👎🏻-문제점">👎🏻 문제점</h2>
<p>이렇게 DTO가 늘어나면서 내가 느낀 문제점은 다음과 같다.</p>
<h3 id="1-랩핑된-형태가-많아질-수록-파일이-늘어난다">1. 랩핑된 형태가 많아질 수록 파일이 늘어난다.</h3>
<ul>
<li>단순히 파일이 늘어나는 것이 문제가 아니라 2번 3번 문제가 1번 문제로 인해 파생된다.</li>
</ul>
<h3 id="2-비슷한-역할을-하지만-응답-형태가-다른-dto가-많은데-클래스-이름-짓기가-힘들다">2. 비슷한 역할을 하지만 응답 형태가 다른 DTO가 많은데, 클래스 이름 짓기가 힘들다.</h3>
<ul>
<li><code>ProductDetail</code>, <code>ProductSummary</code> 이후에는 <code>ProductLessDetail</code> 이렇게 할 것인가?<ul>
<li><del>마치 최종, 최최종, 최최최종처럼...</del></li>
</ul>
</li>
<li>아님 CRUD에 따라 <code>ProductCreate</code>, <code>ProductRead</code>, <code>ProductUpdate</code>, <code>ProductDelete</code> 이렇게 이름 짓고 이후에는?</li>
<li><code>VO</code>도 있다면 <code>ProductVo</code>랑 <code>ProductDto</code>의 역할이 더더욱 헷갈린다.</li>
</ul>
<h3 id="3-어떤-dto가-어디에서-쓰이고-있는지-헷갈린다">3. 어떤 DTO가 어디에서 쓰이고 있는지 헷갈린다.</h3>
<ul>
<li>프로젝트 규모가 작을 때는 문제가 되지 않았는데, 
규모가 커지고 수정사항이 생길 때 어떤 DTO가 어디서 조합되어서 사용되고 있는지 찾는데 시간이 오래 걸린다.</li>
</ul>
<hr>
<br/>

<h2 id="⚡️-개선-방법">⚡️ 개선 방법</h2>
<p><code>DTO</code> 관리 방법을 찾아보다보니 <code>Inner Class</code>를 활용하는 방법이 있어서 프로젝트에 도입해보았다.</p>
<p><code>Inner Class</code>를 활용하면 여러개의 <code>DTO</code> 하나의 클래스에서 깔끔하게 관리할 수 있다.
수정사항이 생기면 여러 곳을 돌아다니면서 수정할 필요가 없고 위에서 내가 느꼈던 문제점을 대부분 해결할 수 있었다.</p>
<p>또 <code>Request</code>, <code>Response</code> 내용이 많지 않을 경우에도 활용해보았는데 보기에도 깔끔하고 관리하기에도 좋았다.</p>
<h3 id="1-여러개의-dto를-inner-class로-관리">1. 여러개의 DTO를 Inner Class로 관리</h3>
<pre><code class="language-java">public class ProductDto {

    private final String title;
    private final String contents;
    private final SellerDto seller;
    private final CategoryDto category;
    private final AddressDto address;

    // 생성자, getter, setter 등 생략

    private static class SellerDto {
        private int id;
        private String nickname;

        // 생성자, getter, setter 등 생략
    }

    private static class CategoryDto {
        private int id;
        private String name;

        // 생성자, getter, setter 등 생략
    }

    private static class AddressDto {
        private int id;
        private String name;

        // 생성자, getter, setter 등 생략
    }
}</code></pre>
<h3 id="2-request--resonse를-inner-class로-관리">2. Request &amp; Resonse를 Inner Class로 관리</h3>
<p>실제로 프로젝트에서 사용했던 코드인데, 회원의 프로필 이미지 수정 요청 및 응답을 한 곳에서 관리했다.</p>
<pre><code class="language-java">public class MemberProfileImgUpdateDto {

    @Getter
    public static class Request {

        private final MultipartFile newProfileImg;

        public Request(MultipartFile newProfileImg) {
            this.newProfileImg = newProfileImg;
        }
    }

    @Getter
    public static class Response {

        private final String updatedImgUrl;

        public Response(String updatedImgUrl) {
            this.updatedImgUrl = updatedImgUrl;
        }
    }
}</code></pre>
<p>서비스나 컨트롤러에서는 다음과 같이 사용하면 된다.</p>
<pre><code class="language-java">    public MemberProfileImgUpdateDto.Response updateMemberProfileImg() {
        // 로직 생략...
        return new MemberProfileImgUpdateDto.Response(newImageUrl);
    }</code></pre>
<hr>
<br/>

<h2 id="💬-소결">💬 소결</h2>
<p><code>Inner Class</code> 로 <code>DTO</code> 관리를 하면서 아직까지 추가적인 문제점은 느끼지 못했다.</p>
<p>다만 여러 곳에서 똑같은 형태로 사용하는 <code>DTO</code>가 <code>Inner Class</code>로 존재한다면 재사용하기 애매한 감이 있다.
이럴 경우 공용 <code>DTO</code>로 분리를 해도 되고, 사실 여러 곳에서 공용으로 사용한다고 꼭 재사용할 필요는 없다고 생각한다.</p>
<p>왜냐면 추후 한 곳에서만 요구하는 정보가 달라질 경우가 있을 수 있다.
물론 개발자는 중복 코드를 싫어하지만, 이때 의도치 않게 여러 곳이 부수적으로 변경될 이슈가 있으니 
정보가 똑같아도 DTO가 여러개 존재해도 괜찮다고 생각한다.</p>
<p>아마 프로젝트 규모가 커지면 결국에 <code>Inner Class</code> 로 관리를 하더라도 이런저런 문제점이 생길 것 같은데,
그때 또 고민해보고 개선을 해보겠다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] Jasypt로 yml 파일 암호화 및 암호화 키를 환경변수로 저장하는 방법]]></title>
            <link>https://velog.io/@jinny-l/spring-jasypt-encrypt-yml-and-store-encryption-key-as-environment-variable</link>
            <guid>https://velog.io/@jinny-l/spring-jasypt-encrypt-yml-and-store-encryption-key-as-environment-variable</guid>
            <pubDate>Sun, 03 Sep 2023 09:17:11 GMT</pubDate>
            <description><![CDATA[<h1 id="💬-들어가기-전에">💬 들어가기 전에</h1>
<p>팀 프로젝트를 하면서 협업을 하면서 관리해야 하는 yml 파일이 늘어나며
yml 파일 관리의 필요성과 중요성을 느꼈다.</p>
<p>지금까지 코드스쿼드 팀 미션을 4번 진행하면서 yml 파일 관리하는 방법도 점차 성장한 것 같아
그 과정을 기록해 보려고 한다.</p>
<p>참고로 사용하는 방법은 본문에 있으니, 급하면 밑의 <a href="https://velog.io/@jinny-l/spring-jasypt-encrypt-yml-and-store-encryption-key-as-environment-variable#1-bean%EC%9C%BC%EB%A1%9C-jasypt-%EC%95%94%ED%98%B8%ED%99%94-%EC%84%A4%EC%A0%95">본문</a>부터 보면 된다.</p>
<p>4번의 팀프로젝트 과정을 요약해보면 다음과 같다.</p>
<br/>

<h2 id="☕️-첫번째-팀-프로젝트---kiosk-미션">☕️ 첫번째 팀 프로젝트 - <a href="https://github.com/codesquad-gwanaksan/kiosk-max">Kiosk 미션</a></h2>
<h3 id="yml-관리-방법">yml 관리 방법</h3>
<ul>
<li>github에 올리지 않고 각자 <code>local</code>에서 관리</li>
<li>사용하는 기술이 많지 않아 <code>yml</code>에는 DB 정보밖에 없어서 딱히 불편함은 느끼지 못했다.</li>
</ul>
<h3 id="단점">단점</h3>
<ul>
<li>하지만 관리해야 하는 <code>yml</code> 파일이 늘어날 수록, 또 팀원이 늘어날 수록 파일 관리가 어려운 단점이 있다.</li>
</ul>
<br/>

<h2 id="✅-두번째-팀-프로젝트---todo-app-미션">✅ 두번째 팀 프로젝트 - <a href="https://github.com/codesuqad-team3-to-do-list/todo-max">Todo App 미션</a></h2>
<h3 id="yml-관리-방법-1">yml 관리 방법</h3>
<ul>
<li><a href="https://velog.io/@jinny-l/spring-jasypt-encrypt-yml-and-store-encryption-key-as-environment-variable#1-bean%EC%9C%BC%EB%A1%9C-jasypt-%EC%95%94%ED%98%B8%ED%99%94-%EC%84%A4%EC%A0%95"><code>Jasypt</code>을 활용하여 파일 암호화 - <code>Bean</code>으로 설정</a></li>
<li>본문에서 다룰 내용인데 <code>Jasypt</code>로 파일 암호화를 해서 Github 레포지토리에 올렸다.</li>
</ul>
<h3 id="첫번째-프로젝트-때의-단점-해결">첫번째 프로젝트 때의 단점 해결</h3>
<ul>
<li>이렇게 하면 별도로 파일 관리를 하지 않아도 되고 
팀원이 모두 같은 버전의 파일을 사용하고 있다는 보장을 할 수 있다.</li>
</ul>
<h3 id="문제-배포-시-복호화-키-주입하는-방법">문제: 배포 시 복호화 키 주입하는 방법</h3>
<p>local에서는 <code>Jasypt</code> 복호화 키를 <code>IntelliJ</code>에서 환경변수로 설정해서 사용하였지만
배포 시에 <code>Jasypt</code>의 암호화 키를 어떻게 넣어줘야 하는지 몰라서 헤맸다. </p>
<p><a href="https://velog.io/@jinny-l/codesquad-retrospective-ToDo-App-team-project#1-2-%EC%83%88%EB%A1%9C-%EC%8B%9C%EB%8F%84%ED%95%B4-%EB%B3%B8-%EA%B2%83">ToDo App 팀 프로젝트 회고 글</a>에 작성했던 내용인데 
jar 실행할 때 복호화 키를 주입하는 방법을 몰라서 찾아보다가 <code>Java vm option</code>에 대해 알게 되었다.</p>
<p>이때는 <code>vm option</code>이 뭐가 있는지 몰라서 
결국엔 서버에 미리 <code>application.yml</code> 작성해놓고 대체하는 방법을 사용했지만 다음 프로젝트 때 개선했다.</p>
<blockquote>
<p><em><a href="https://velog.io/@jinny-l/codesquad-retrospective-ToDo-App-team-project">ToDo App 팀 프로젝트 회고 글</a> 일부 내용:</em>
<img src="https://velog.velcdn.com/images/jinny-l/post/aa99ce66-1032-45c4-b8f7-ce6a0ef575e3/image.png" alt=""></p>
</blockquote>
<br/>

<h2 id="📈-세번째-팀-프로젝트---issue-tracker-미션">📈 세번째 팀 프로젝트 - <a href="https://github.com/eojjeogo-jeojjeogo/issue-tracker-max">Issue Tracker 미션</a></h2>
<h3 id="yml-관리-방법-2">yml 관리 방법</h3>
<ul>
<li><a href="https://velog.io/@jinny-l/spring-jasypt-encrypt-yml-and-store-encryption-key-as-environment-variable#2-yml%EB%A1%9C-jasypt-%EC%95%94%ED%98%B8%ED%99%94-%EC%84%A4%EC%A0%95"><code>Jasypt</code>을 활용하여 파일 암호화 - <code>yml</code>로 설정</a></li>
<li>이전 프로젝트와 똑같이 <code>Jasypt</code> 기술을 활용하여 내용 암호화를 진행했지만 
이번에는 <code>Bean</code>으로 등록하지 않고 <code>yml</code>로 설정해서 사용했다.</li>
</ul>
<h3 id="두번째-프로젝트-때의-문제-해결">두번째 프로젝트 때의 문제 해결</h3>
<p>두번째 프로젝트 때 배포 시 복호화 키 주입하는 방법을 몰라 헤맸는데
<code>vm option</code>을 더 공부해보니 배포할 때 <code>-Djasypt.encryptor.password={시크릿키}</code>를 <code>option</code>으로 주면된다.</p>
<br/>

<h2 id="🍆-네번째-팀-프로젝트---second-hand-미션">🍆 네번째 팀 프로젝트 - <a href="https://github.com/masters2023-project-team05-second-hand/second-hand-max-be-b">Second Hand 미션</a></h2>
<p>지금 막 시작한 프로젝트인데 <code>Github Actions</code>와 <code>docker-compose</code>로 배포를 진행하고 있다.</p>
<h3 id="문제-환경-변수-관리">문제: 환경 변수 관리</h3>
<p>앞서 경험한 것들을 총 집합해서 구성했지만 <code>Dockerfile</code>에 시크릿키를 전달해야 하는 이슈가 있다.</p>
<p>물론 <code>docker-compose.yml</code> 파일을 통해 변수를 직접 주입해주면 되지만 
<code>docker-compose.yml</code>도 버전 관리를 위해 <code>Github</code>에 올리고 있으므로 
<code>Github</code>에는 환경변수를 작성하지 않은 파일을 올려야 했고, 서버에서는 직접 변수를 입력해줘야 하는 번거로움이 있다. </p>
<p>그래서 <code>docker-compose</code> 파일에 시크릿키를 직접 작성하지 않을 수 있는 방법을 찾아보았다.</p>
<p>서버(EC2)에 환경변수 파일을 만들어 놓고 변수를 주입받으면 된다.</p>
<p>이 내용도 <a href="https://velog.io/@jinny-l/spring-jasypt-encrypt-yml-and-store-encryption-key-as-environment-variable#3-%EC%84%9C%EB%B2%84-%ED%99%98%EA%B2%BD%EB%B3%80%EC%88%98-%EC%84%A4%EC%A0%95">본문</a>에서 다룰 예정이다.</p>
<hr>
<br/>

<h1 id="1-bean으로-jasypt-암호화-설정">1. <code>Bean</code>으로 <code>Jasypt</code> 암호화 설정</h1>
<blockquote>
<p>공식 홈페이지: <a href="http://www.jasypt.org/">http://www.jasypt.org/</a></p>
</blockquote>
<h2 id="1-1-buildgradle에-의존성-추가">1-1. build.gradle에 의존성 추가</h2>
<pre><code class="language-java">implementation &#39;com.github.ulisesbocchio:jasypt-spring-boot-starter:3.0.5&#39;</code></pre>
<h2 id="1-2-bean-등록">1-2. Bean 등록</h2>
<ul>
<li><code>Config</code> 클래스 작성</li>
</ul>
<pre><code class="language-java">
@Configuration
class JasyptConfig {

//    @Value(&quot;${jasypt.encryptor.password}&quot;) 외부 환경변수를 어노테이션으로 주입받아도 되고 아래처럼 설정하는 방법도 있다.
//    private String JASTYPTKEY;

    @Bean(&quot;jasyptStringEncryptor&quot;)
    public StringEncryptor stringEncryptor() {

        PooledPBEStringEncryptor encryptor = new PooledPBEStringEncryptor();
        SimpleStringPBEConfig config = new SimpleStringPBEConfig();

        // !!주의: 암복호화에 사용되는 키로 1-4번 과정에서 설정하는 변수명이랑 동일해야 한다. 
        config.setPassword(System.getenv(&quot;JASYPTKEY&quot;)); 

        config.setAlgorithm(&quot;PBEWITHMD5ANDDES&quot;); // 암호화 알고리즘
        config.setPoolSize(&quot;1&quot;);                 // 암호화 인스턴스 pool
        config.setKeyObtentionIteration(&quot;1000&quot;); // 반복할 해싱 횟수
        config.setSaltGeneratorClassName(&quot;org.jasypt.salt.RandomSaltGenerator&quot;); // salt 생성 클래스
        config.setStringOutputType(&quot;base64&quot;);    // 인코딩 방식

        encryptor.setConfig(config);
        return encryptor;
    }
}</code></pre>
<blockquote>
<p>💡 참고:</p>
<ul>
<li><code>saltGenerator</code>를 지정하지 않으면 <code>default</code> 값으로 <code>RandomSaltGenerator</code>를 사용한다.</li>
<li>고정된 <code>salt</code> 값을 사용하려면 <code>StringFixedSaltGenerator</code>를 사용하면 된다.<ul>
<li>이 경우 암호값이 동일하고 복호화 시에도 기존에 설정한 <code>salt</code>와 동일한 값을 지정해야 복호화가 가능하다.</li>
</ul>
</li>
</ul>
</blockquote>
<p>이 외에도 <code>config</code> 설정이 더 있는데 보다 상세한 내용은 아래 URL에서 확인 가능하다.</p>
<ul>
<li><a href="https://github.com/ulisesbocchio/jasypt-spring-boot#use-you-own-custom-encryptor">https://github.com/ulisesbocchio/jasypt-spring-boot#use-you-own-custom-encryptor</a></li>
</ul>
<h2 id="1-3-내용-암호화">1-3. 내용 암호화</h2>
<ul>
<li><a href="https://www.devglan.com/online-tools/jasypt-online-encryption-decryption">암호화 사이트</a>에서 암호화를 진행하고 나온 결과를 <code>yml</code> 파일에 <code>ENC()</code>로 감싸서 작성한다.</li>
</ul>
<p><strong>application.yml 예시</strong></p>
<pre><code class="language-yml">spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: ENC(URcBC2us4EiLrKbeiOdQIGnOz8USQI3Ct6VO9M0geIwetzGFzoMKJPupbvYgrv95uy8+2vdcRKq/xNfo9n367v9y/dFFHwSghtNYo5BX27iJpvCMoY4F+tXmV6RBjy6AlXyCifu+QMorBD+EpA7c2jDGZh3OQx6Y)
    username: ENC(aaAZtXujmwi47WvLzuFZEA==)
    password: ENC(msP9S4m29/R0bw6IN3Q6Kuasi4h4UENu)</code></pre>
<h2 id="1-4-jar-실행-시-암복호화키-전달">1-4. Jar 실행 시 암복호화키 전달</h2>
<h3 id="intellij">IntelliJ</h3>
<ul>
<li>환경변수 추가</li>
</ul>
<ol>
<li><p>인텔리제이 상단에 <code>Edit Configurations...</code> 클릭
<img src="https://velog.velcdn.com/images/jinny-l/post/319b9b2c-72b7-493f-8017-515327cec215/image.png" alt=""></p>
</li>
<li><p>Environment variables 영역에 변수 작성
<img src="https://velog.velcdn.com/images/jinny-l/post/b3129217-814f-4c0c-b495-b0fe874e934e/image.png" alt=""></p>
</li>
</ol>
<h3 id="스크립트">스크립트</h3>
<pre><code class="language-shell"> java -jar -DJASYPTKEY=secret-key app.jar</code></pre>
<hr>
<br/>

<h1 id="2-yml로-jasypt-암호화-설정">2. <code>yml</code>로 <code>Jasypt</code> 암호화 설정</h1>
<h2 id="2-1-의존성-추가">2-1. 의존성 추가</h2>
<pre><code class="language-java">implementation &#39;com.github.ulisesbocchio:jasypt-spring-boot-starter:3.0.5&#39;</code></pre>
<h2 id="2-2-yml-설정-파일-작성">2-2. yml 설정 파일 작성</h2>
<pre><code class="language-yml">jasypt:
  encryptor:
    iv-generator-classname: org.jasypt.iv.NoIvGenerator
    algorithm: PBEWithMD5AndTripleDES
    password: # 2-3번 과정때 사용하고 이후 지워야 함</code></pre>
<blockquote>
<p>⚠️ 주의:</p>
<ul>
<li><code>password</code>에 암복화에 사용될 키를 작성하는데 해당 내용은 2-3 과정에 필요하다. </li>
<li>공개된 곳에 올릴 때는 지워야 한다.</li>
</ul>
</blockquote>
<h2 id="2-3-내용-암호화">2-3. 내용 암호화</h2>
<p><code>Bean</code>으로 등록했을 때처럼 암호화 사이트에서 암호화해도 되는데 해당 방법은 위에서 사용했으니
이번에는 다른 방법으로 암호화해보겠다.</p>
<p>출력에 필요한 클래스 파일을 임시로 하나 생성해서 암호화하는 방법이다.</p>
<ul>
<li>임시로 클래스 하나 생성</li>
</ul>
<pre><code class="language-java">@SpringBootTest
class SecondHandApplicationTests {

    @Autowired
    StringEncryptor stringEncryptor;

    @Test
    void encrypt() {
        String encrypt = stringEncryptor.encrypt(&quot;secret-key&quot;);
        System.out.println(&quot;encrypt = &quot; + encrypt);
    }
}</code></pre>
<ul>
<li>출력된 내용을 yml 파일에 작성
<img src="https://velog.velcdn.com/images/jinny-l/post/40ffb5dc-71e2-4930-a590-75e03dee2c56/image.png" alt=""></li>
</ul>
<h2 id="2-4-jar-실행-시-암복호화키-전달">2-4. Jar 실행 시 암복호화키 전달</h2>
<h3 id="intellij-1">IntelliJ</h3>
<ul>
<li>2-2번 과정에서 삭제한 password를 전달해야 한다.</li>
</ul>
<ol>
<li><p>인텔리제이 상단에 <code>Edit Configurations...</code> 클릭</p>
</li>
<li><p><code>Modify options</code> &gt; <code>Add VM options</code> 클릭
<img src="https://velog.velcdn.com/images/jinny-l/post/17fce9df-5508-4af5-acea-a6e40ba3c5fb/image.png" alt="">
<img src="https://velog.velcdn.com/images/jinny-l/post/59a828c5-0161-422a-bf5b-2aa248edb64d/image.png" alt=""></p>
</li>
<li><p><code>vm options</code>에 <code>password</code> 작성</p>
</li>
</ol>
<pre><code class="language-shell">-Djasypt.encryptor.password=key</code></pre>
<p><img src="https://velog.velcdn.com/images/jinny-l/post/812271ab-f9b0-43b5-a254-5b2bcaa457b1/image.png" alt=""></p>
<h3 id="스크립트-1">스크립트</h3>
<pre><code class="language-shell">java -jar -Djasypt.encryptor.password=$JASYPT_PASSWORD app.jar</code></pre>
<hr>
<br/>

<h1 id="3-서버-환경변수-설정">3. 서버 환경변수 설정</h1>
<h2 id="3-1-환경변수-설정">3-1. 환경변수 설정</h2>
<p>서버에 환경변수를 미리 작성해두고 배포 시 활용할 수 있다.</p>
<p><strong>1. <code>Vim</code>으로 <code>bash_profile</code>을 생성한다.</strong></p>
<pre><code class="language-shell">vi ~/.bash_profile</code></pre>
<p><strong>2. 해당 파일에 환경변수 작성</strong></p>
<pre><code class="language-shell">export MY_NAME=Jinny
export JASYPT_PASSWORD=secret</code></pre>
<p><strong>3. 설정 적용</strong></p>
<pre><code class="language-shell">source ~/.bash_profile</code></pre>
<p><strong>4. 변수 적용 확인</strong></p>
<pre><code class="language-shell">echo $MY_NAME</code></pre>
<blockquote>
<ul>
<li>잘 적용되었다면 아래와 같이 변수가 잘 출력되는 모습을 볼 수 있다.
<img src="https://velog.velcdn.com/images/jinny-l/post/0b7d9f71-3301-4f7e-a4e9-54e6f72693f1/image.png" alt=""></li>
</ul>
</blockquote>
<h2 id="3-2-docker-배포-시-환경변수-활용">3-2. Docker 배포 시 환경변수 활용</h2>
<p><strong>1. <code>Dockerfile</code> 작성</strong> </p>
<ul>
<li>도커파일에 아래와 같이 변수를 작성하고 docker-compose 파일을 통해 주입받는다.<pre><code class="language-shell"># 생략...
ENTRYPOINT [&quot;java&quot;, &quot;-jar&quot;, &quot;-Djasypt.encryptor.password=${JASYPT_PASSWORD}&quot;, &quot;/app.jar&quot;]
</code></pre>
</li>
</ul>
<pre><code>
**2. `docker-compose.yml` 작성** 
- `docker-compose` 파일 내의 변수는 `.bash_profile`의 변수로부터 전달 받는다.
- 즉 다음과 같이 전달되는 것이다.
    - `.bash_profile` 내의 변수 → `docker-compose` → `Dockerfile`

```shell
services:
  was:
    container_name: 
    image: 
    environment:
      - JASYPT_PASSWORD=${JASYPT_PASSWORD}</code></pre><blockquote>
<p>⚠️ 참고: 
그런데 어쩔 때는 환경 변수가 잘 적용되고 어쩔 때는 적용이 안될 때가 있다.
구글링을 해보니 이런 글을 찾았다. 뭔가 도커 버그가 아닌가 싶다.</p>
<blockquote>
<p><em>만약 도커를 사용하여 컨테이너 실행 시 환경변수를 사용하고싶다면 버전은 3.0.4를 필수로 사용해야한다. 3.0.3으로 한참을 시도했는데 안되다가 3.0.4버전 사용 시 잘 실행되었다.</em>
출처: <a href="https://velog.io/@rnjsals1575/Jasypt%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%9C-%EB%AF%BC%EA%B0%90%EC%A0%95%EB%B3%B4-%EC%95%94%ED%98%B8%ED%99%94">https://velog.io/@rnjsals1575/Jasypt%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%9C-%EB%AF%BC%EA%B0%90%EC%A0%95%EB%B3%B4-%EC%95%94%ED%98%B8%ED%99%94</a></p>
</blockquote>
</blockquote>
<hr>
<h1 id="🧞♂️-소결">🧞‍♂️ 소결</h1>
<p><code>Jasypt</code>를 사용하면서 느낀점은...
지금처럼 팀미션을 진행할 때 사용하는 것은 나쁘지 않은 것 같다.</p>
<p>하지만 암호화는 항상 뚤릴 위험이 있기 때문에 실제 실무에 들어가면 잘 사용하지 않을 것 같다. 
내가 사장이면 안쓴다.!</p>
<p>그렇다면 또 무슨 방법이 있냐!!하면 <code>Github</code>의 <code>submodule</code>을 활용하는 것인데
아직 사용해보지 못해서 기회가 되면 블로그글을 작성해보겠다.</p>
<hr>
<h1 id="참고">참고:</h1>
<ul>
<li>팀원 Ape, Albert의 지식 공유</li>
<li>Jasypt 설정 관련<ul>
<li><a href="https://luvstudy.tistory.com/67">https://luvstudy.tistory.com/67</a></li>
<li><a href="https://kitty-geno.tistory.com/160">https://kitty-geno.tistory.com/160</a></li>
<li><a href="https://yjkim97.tistory.com/64">https://yjkim97.tistory.com/64</a></li>
</ul>
</li>
<li>배포 서버 환경 변수 설정 관련<ul>
<li><a href="https://bibi6666667.tistory.com/319">https://bibi6666667.tistory.com/319</a> </li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[코드스쿼드] Max 19~20주차 - ToDo App 팀 프로젝트]]></title>
            <link>https://velog.io/@jinny-l/codesquad-retrospective-ToDo-App-team-project</link>
            <guid>https://velog.io/@jinny-l/codesquad-retrospective-ToDo-App-team-project</guid>
            <pubDate>Mon, 24 Jul 2023 11:53:22 GMT</pubDate>
            <description><![CDATA[<p><em>Max 19주차: 23-07-10 ~ 23-07-14</em>
<em>Max 20주차: 23-07-17 ~ 23-07-21</em></p>
<hr>
<h1 id="✅-두번째-팀-프로젝트">✅ 두번째 팀 프로젝트</h1>
<p>첫번째 프로젝트가 끝나고 1주일간의 방학을 갖고 두번째 팀프로젝트가 시작되었다.
두번째 팀 프로젝트는 Todo App을 만드는 것이었다.</p>
<p>하기에 첨부한 이미지는 Todo App의 메인페이지인데 
Todo App은 흔히 알고 있는 Trello 같은 칸반 보드 형태의 To do List를 관리하는 프로그램이다.</p>
<p><img src="https://github.com/codesuqad-team3-to-do-list/todo-max/assets/86359180/f3a73a60-476b-4da6-9c66-2c3502fa6173" alt="메인 페이지"></p>
<p>그리고 유저가 어떤 행동을 했는지 확인할 수 있는 <code>사용자 활동 기록</code>을 볼 수 있는 것이 Main Feature다.</p>
<p><img src="https://github.com/codesuqad-team3-to-do-list/todo-max/assets/108214590/05f23f93-cf55-491f-ab93-8c3df534cf79" alt="사용자활동기록"></p>
<p>보다 상세한 내용은 아래 깃허브 레포지토리에서 확인 가능하다.</p>
<blockquote>
<p>Github: <a href="https://github.com/codesuqad-team3-to-do-list/todo-max">To do App 프로젝트</a></p>
</blockquote>
<hr>
<br/>

<h2 id="1-🧐-회고">1. 🧐 회고</h2>
<p>요번 글은 프로젝트 관련 고민 포인트에 대한 내용이 길어 회고는 간단하게 언급하고 넘어가려 한다.</p>
<h3 id="1-1-새로-공부한-내용">1-1. 새로 공부한 내용</h3>
<p><strong>preflight</strong></p>
<ul>
<li>CORS 에러가 발생하면서 <code>preflight</code>에 대해 공부했다.</li>
</ul>
<p><strong>JWT</strong></p>
<ul>
<li>로그인 기능을 구현하며 <code>JWT</code>에 대해 공부했다.</li>
<li>하지만 아직 <code>JWT</code> 시그니처 암호화는 잘 이해하고 있지 못하다.</li>
<li>HS256 개념, 암복호화, 대칭키, 단방향/양방향 암호화에 대해 더 공부가 필요하다.</li>
<li>어떤 상황에 세션 기반 인증을 하고 어떤 상황에 토큰 기반 인증을 선택하는지 공부가 더 필요하다.</li>
</ul>
<br/>

<h3 id="1-2-새로-시도해-본-것">1-2. 새로 시도해 본 것</h3>
<p><strong>Redis</strong></p>
<ul>
<li>데이터/로그 캐싱을 위해 <code>Redis</code>를 처음 사용해봤다.</li>
</ul>
<p><strong>jayspt</strong></p>
<ul>
<li><code>application.properties</code> 파일 암호화를 위해 <code>jayspt</code>을 사용했다.</li>
<li><code>CI/CD 사용 금지</code>가 프로젝트 요구사항이어서 배포 스크립트만 작성했는데 스크립트에 복호화 암호를 쓰지 않고 복호화하는 것이 고민이었다.
(스크립트를 깃허브에 올리거라 복호화 암호를 써놓으면 암호화 의미가 없기 때문)</li>
<li>방법을 찾지 못해서 서버에 미리 <code>application.yml</code> 파일을 만들어 놓고 <code>jar</code> 파일 돌릴 때 다음과 같이 <code>option</code>을 추가했다.</li>
</ul>
<pre><code>  --Dspring.config.location=~/app/application.yml</code></pre><p><strong>슬라이싱</strong></p>
<ul>
<li>조회 최적화 관련 고민을 하다가 팀원의 의견으로 활동 기록 조회 기능을 슬라이싱으로 구현해보았다.</li>
<li>위에 gif 보면 스크롤 할 때마다 살짝 끊기는 느낌인데 그때 api 요청을 보내서 n개씩 데이터를 받아오는 식이다.</li>
<li>자세히 쓰고 싶은데 캡쳐해둔 것이 없어서 살짝 언급하고 지나가겠다. 🥹</li>
</ul>
<br/>

<h3 id="1-3-아쉬운-점">1-3. 아쉬운 점</h3>
<p>요번 프로젝트는 공부해야 할 것도 많고 
밑에 써놓은 고민포인트 논의 등으로 인해 시간이 엄청 엄청 부족해서 디테일을 많이 버려야 했다.</p>
<p><strong>테스트 코드</strong></p>
<ul>
<li>테스트 코드 작성을 아예 못했다.</li>
<li>JWT 테스트를 하려면 header에 유효한 토큰을 넣어줘야 API가 정상 작동 할텐데 어떻게 테스트해야 할지 고민해보지 못했다.</li>
</ul>
<p><strong>보안 관련 고민</strong></p>
<ul>
<li>회원가입 시 비밀번호 암호화 등 보안 관련된 것은 아예 고민도 못 했다.</li>
</ul>
<p><strong>미구현 기능</strong></p>
<ul>
<li>JWT의 꽃은 <strong>로그아웃 기능</strong>이라고 느꼈는데 로그인 기능만 구현하고 로그아웃 기능은 구현 못했다.</li>
<li>로그인을 여러번 하면서 토큰을 재발급 받으면 이전 토큰을 계속 사용할 수 있는 점이 문제가 없을까?에 대한 고민이 있었는데 깊게 생각하지 못했다.</li>
</ul>
<br/>

<h3 id="14-프론트와-협업">1.4 프론트와 협업</h3>
<p>두번의 팀프로젝트를 하면서 프론트(리액트)에 대해 협업 관련 이해도가 조금 생긴 것 같다.</p>
<p><strong>리액트 build 결과는 정적 파일이다?(feat. S3 배포)</strong>
(잘못 이해한 부분이 있다면 댓글로 알려주세용 🥲)</p>
<ul>
<li>첫번째 프로젝트 때 프론트 서버를 어떻게 배포해야 할지 이런저런 고민을 하다가 EC2에 배포를 했다.</li>
<li>프론트 서버 배포를 하면서 <code>build</code>한 파일이 <code>main.ts</code>인가 <code>main.html</code>인가 파일이 꼴랑? 하나 생겼던 것으로 기억한다. </li>
<li>이때 프론트분들한테 질문을 했었는데 리액트의 특징이 화면이 새로 고침 되지 않고(새로운 url로 가서 화면을 새로 랜더링하는 것이 아니라) 현재 페이지에서 다른 페이지로 렌더링을 다시 해주는 방식이라고 알게되었다.</li>
<li>그래서 요번에는 AWS <code>S3</code>의 정적 웹 호스팅 기능을 이용해 프론트 서버를 배포해보았다.
(이해도가 없었으면 리액트가 왜 S3로 배포가 가능한지 이해하지 못했을 것 같다.)</li>
</ul>
<p><strong>컴포넌트는 상태 값을 갖고 있다.</strong></p>
<ul>
<li>프론트에서 어떤 값을 들고 있고, 어떤 데이터를 보내줘야 하는지에 대한 감이 조..금 생긴 것 같다.</li>
</ul>
<p>+) 
해결하지 못한 이슈가 마음에 걸려 팀원인 <a href="https://github.com/crtEvent">Ape</a>랑 프로젝트 끝나고 에러를 해결했다.
고쳐졌는지 확인하기 위해 프론트 코드를 하나씩 떼다가 퍼즐 조각을 맞춰 api 정상 응답을 확인했는데, 요 과정도 프론트에 대한 이해도를 높여준 것 같다.</p>
<hr>
<br/>

<h2 id="2-🤯-고민-포인트">2. 🤯 고민 포인트</h2>
<p>프론트엔트, 백엔드 가릴 것 없이 요번 프로젝트의 최대 난제는 
카드를 <code>동일 컬럼 안에서 위 아래로 움직이는 기능</code>과 <code>다른 컬럼 간 이동하는 기능</code>이었다.</p>
<p>이 이슈 하나로 고려해야 할 것이 굉장히 많았다.
우리 팀의 주된 고민은 다음 두가지였다.</p>
<blockquote>
<ol>
<li>카드 이동 로직을 어떻게 구현할 것인가?</li>
<li>카드 CRUD + 카드 이동 시 DB 접근이 빈번하게 일어나는데 어떻게 DB 접근 횟수를 줄일 수 있을까?</li>
</ol>
</blockquote>
<br/>

<h3 id="2-1-카드-이동-로직">2-1. 카드 이동 로직</h3>
<p>** ① <code>Linked List</code> 방식**</p>
<p>맨 처음에 나왔던 의견은 <code>Linked List</code>처럼 각 카드가 앞뒤 카드 id를 갖고 있고, 
이동 시 <code>Linked List</code> 처럼 앞뒤 카드 id를 변경해주는 것이다.</p>
<p>해당 방법은 밑에 이유로 탈락했다.
(+ 효율적이지도 않은데 2번 3번 방식에 비해 드는 시간과 짜야하는 코드량이 몇배일 것으로 예상되었다.)</p>
<blockquote>
<p>🤔 우려되는 점:</p>
</blockquote>
<ul>
<li>정보 업데이트 시에는 소수 데이터만 변경하면 되지만, 메인 페이지에서 전체를 조회하게 되면 정렬을 해야 한다.</li>
<li>그럼 카드 이동 시마다 매번 DB에서 데이터를 꺼내온 다음, 
전체 카드 정보를 탐색해서 전체 정렬을 해줘야 하는데, 효율적이지 않다.<ul>
<li>만약 유저가 계<del>~</del>속 카드를 이동한다면? </li>
<li>카드 수가 많<del>~</del>이 늘어난다면?</li>
</ul>
</li>
</ul>
<p>** ② 클라이언트에서 전체 정보 전달**</p>
<p>카드가 이동되면 각 카드의 id와 순서 정보를 통으로 서버에 전달해주면
DB에 해당 user의 전체 카드 순서 정보를 전부 업데이트하는 방법이다.</p>
<p>제일 원시적이지만 제일 간단한 방법이다.</p>
<p>하지만 해당 방법도 우려되는 점이 있었다.</p>
<blockquote>
<p>🤔 우려되는 점:</p>
<ul>
<li>카드 이동 시, 카드 개수만큼 DB를 업데이트 해야 한다.</li>
<li>이동 요청이 엄청 많다면?</li>
<li>카드 개수가 엄청 많다면?</li>
<li>회원이 엄청 많다면?</li>
</ul>
</blockquote>
<p>** ③ Positioning 방식** (정확히 어떤 방식으로 불리는지 모르겠으나 팀내에서 포지셔닝으로 불렀다.)</p>
<p>밑에 그림을 참고해보면 카드는 일정 간격의 가중치를 갖고 있고, 
이동 시마다 위, 아래 카드의 평균값을 갖게 된다.</p>
<p>이렇게 되면 카드 이동 시 위아래 카드 가중치 값 조회를 위한 <strong>2개 데이터의</strong> <code>SELECT</code>과, 
이동할 카드의 포지션 값 변경을 위한 <strong>1개 데이터의</strong> <code>UPDATE</code>만 하면 된다.</p>
<blockquote>
<p><img src="https://velog.velcdn.com/images/jinny-l/post/9a1b5556-92ba-4aab-af2f-a526c591d590/image.png" alt="">
출처: 팀원 <a href="https://github.com/crtEvent">Ape</a>가 그린 그림</p>
</blockquote>
<p>이때 우려했던 점은 값이 점점 줄어 들다가 
어느 시점이 되면 순서에 따른 값이 맞지 않는 순간이 올텐데 
&quot;이 순간&quot;을 어떻게 체크하냐가 관건이었다.</p>
<p>여러 논의 끝에 카드 이동 시마다, 위의 카드의 가중치를 조회해서 1차이가 나면 
전체 데이터의 간격을 1000으로 다시 조정하기로 했다.</p>
<p>이때 해당 컬럼의 카드 전체가 업데이트 되지만, 어쩌다 한번 발생하는 것이고
분할 상환 분석 이런 개념으로 보면 괜찮다는 생각이 들었다.</p>
<p>(개인적으로 수학을 못해서 가중치가 잘못되는 또다른 경우의 수가 있을 것 같아서 아직까지 조금 찝찝한 면은 있다.)</p>
<p>+) <em>카드 이동 로직 고민 시 참고했던 <a href="https://jaehoney.tistory.com/82">블로그</a></em>
<br/></p>
<h3 id="2-2-db-접근-횟수를-어떻게-줄일-것인가">2-2. DB 접근 횟수를 어떻게 줄일 것인가?</h3>
<p>카드 이동 로직과 제일 많이 고민하고 논의했던 부분은 <strong>&quot;DB 접근 횟수를 어떻게 줄일 수 있는가?&quot;</strong>였다.</p>
<p>카드 CRUD 와 카드 이동이 발생할 때마다 DB에 접근하고,
또 모든 액션이 발생할 때마다 <strong>사용자 활동 기록</strong>을 보여주기 위해 로그를 DB에 쌓아야 했다.</p>
<p>로컬 스토리지에 데이터를 일정 시간 저장 후 서버에 요청하는 등 다양한 방식을 고려했으나
결론적으로는 <strong><code>write back</code> 방식의 <code>Redis</code>를 사용해서 데이터를 캐싱</strong>하기로 했다.
(JWT 토큰을 이용한 로그인 기능이 요구사항에 있어서, TTL을 설정해 Refresh token과 만료된 Access Token을 저장/처리하기 위함도 있었다.)</p>
<br/>

<p>...라는 원대한 꿈을 꾸었지만 결론적으로 서버 배포 한번 해보고 <code>Redis</code> 관련 코드를 모두 들어냈다.</p>
<p>이유는 카드 이동 로직 관련 논의에 시간을 많이 쏟기도 했고, 
JWT도 새로 공부해서 구현해야 했는데 Redis에 대한 이해도 부족과 공부할 시간 부족으로 
낯선 에러를 처리할 수가 없었다... 🥲</p>
<p>나중에 참고차 에러 메시지를 적어두려 한다.</p>
<p><strong>에러메시지 1</strong></p>
<ul>
<li>left pop을 할 때 데이터가 없으면 발생하는 것으로 추측된다.</li>
<li>그런데 Redis 데이터를 확인했을 때 데이터가 들어있었다.<pre><code class="language-java">Error in execution; 
nested exception is io.lettuce.core.RedisCommandExecutionException: 
ERR wrong number of arguments for &#39;lpop&#39; command</code></pre>
</li>
</ul>
<p><strong>에러메시지 2</strong></p>
<ul>
<li>구글링해보면 <code>@scheduled</code> 어노테이션이랑 <code>@transactional</code> 어노테이션이랑 같은 클래스에서 사용하면 나타나는 에러라고 한다.</li>
<li>확인차 어노테이션 하나를 제거했는데도 동일한 에러가 발생했다.</li>
<li>그리고 두 어노테이션을 같이 쓰면 안되는 이유에 대해서도 공부해봐야 할 것 같다.</li>
</ul>
<pre><code>Unexpected error occurred in scheduled task</code></pre><br/>


<p><code>Redis</code>는 다음에 다시 시도해 보는 것으로!!</p>
<hr>
<br/>

<h2 id="3-🔥-trouble-shooting">3. 🔥 Trouble Shooting</h2>
<p>요번 프로젝트를 진행하면서 Trouble Shooting한 내용은 블로그에 별도로 정리해놓았다.</p>
<h3 id="3-1-cors">3-1. CORS</h3>
<ul>
<li>로그인 <code>filter</code> 기능 구현 후 CORS 설정이 먹히지 않다가 이런저런 설정을 하면서 해결이 되긴 했는데 원인을 정확히 모르겠다.</li>
<li>Spring이랑 Http 흐름에 대해 공부를 더 해봐야 할 것 같다.</li>
</ul>
<blockquote>
<p><a href="https://velog.io/@jinny-l/Spring-filter-CORS-Error">[Spring] filter 기능 구현 후 CORS 에러가 발생하는 이슈</a></p>
</blockquote>
<h3 id="3-2-jwt">3-2. JWT</h3>
<ul>
<li>JWT 토큰 파싱하면서 오류가 있어서 삽질을 반나절했는데 대단한(?) 원인은 아니었다.</li>
<li>개발자는 눈을 크게 떠야 한다.</li>
</ul>
<blockquote>
<p><a href="https://velog.io/@jinny-l/JWT-SignatureException-Error">[JWT] SignatureException 에러</a></p>
</blockquote>
<h3 id="3-3-s3">3-3. S3</h3>
<ul>
<li>S3 정적 호스팅을 기능을 처음 경험해봤는데, 새로운 기능을 알게되어서 좋았다.</li>
</ul>
<blockquote>
<p><a href="https://velog.io/@jinny-l/AWS-S3-web-hosting-reload-404-NoSuchKey-error">[AWS] S3 정적 호스팅 페이지 새로고침 시 404 NoSuchKey 에러가 발생하는 이슈</a></p>
</blockquote>
<hr>
<br/>

<h2 id="4-🧹-세-줄-정리">4. 🧹 세 줄 정리</h2>
<ul>
<li>투두앱 쉽게 보다가 카드 이동에 큰코 다쳤다.</li>
<li>구현 못한 기능과 디테일을 챙기지 못한 점이 아쉽다.</li>
<li>블로그 글을 쓰면서 원인을 모르겠는 것들이 많아 공부가 필요하다고 적어놓은 것이 많다.
공부를 한다고 하지만 기술 부채가 쌓이고 있어서 반성해야겠다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] filter 기능 구현 후 CORS 에러가 발생하는 이슈]]></title>
            <link>https://velog.io/@jinny-l/Spring-filter-CORS-Error</link>
            <guid>https://velog.io/@jinny-l/Spring-filter-CORS-Error</guid>
            <pubDate>Fri, 21 Jul 2023 16:29:09 GMT</pubDate>
            <description><![CDATA[<h1 id="spring-filter-기능-구현-후-cors-에러가-발생하는-이슈">Spring filter 기능 구현 후 CORS 에러가 발생하는 이슈</h1>
<ul>
<li>팀 프로젝트를 진행하며 <code>Spring Filter</code>를 사용하여 로그인 기능 구현</li>
<li>CORS는 Configuration으로 설정</li>
</ul>
<blockquote>
<p>보다 상세한 코드는 <a href="https://github.com/codesuqad-team3-to-do-list/todo-max">Gihub</a>에서 확인 가능하다.</p>
</blockquote>
<p><strong><code>Configuration</code> 코드</strong></p>
<ul>
<li><p>모든 <code>url</code>, <code>origin</code>, <code>method</code>를 해용해준 상태</p>
<pre><code class="language-java">@Configuration  
public class WebConfig implements WebMvcConfigurer {  

  @Override  
  public void addCorsMappings(CorsRegistry registry) {  
      registry.addMapping(&quot;/**&quot;)  
       .allowedOrigins(&quot;*&quot;)  
       .allowedMethods(&quot;*&quot;)  
       .maxAge(3000);  
  }  
}</code></pre>
</li>
</ul>
<p><strong><code>Filter</code> 코드</strong></p>
<pre><code class="language-java">public class JwtAuthorizationFilter implements Filter {

    // 생략

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws ServletException, IOException {

        HttpServletRequest httpServletRequest = (HttpServletRequest) request;

        // api의 화이트리스트 여부 체크
        if(whiteListCheck(httpServletRequest.getRequestURI())){
            chain.doFilter(request, response);
            return;
        }

        // request header에 토큰 포함 여부 체크
        if(!isContainToken(httpServletRequest)){
            sendErrorApiResponse(response, new MalformedJwtException(&quot;&quot;));
            return;
        }

        // 필터 로직 진행
        try {
            String token = getToken(httpServletRequest);
            Claims claims = jwtProvider.getClaims(token);
            request.setAttribute(&quot;memberId&quot;, claims.get(&quot;memberId&quot;));
            chain.doFilter(request, response);
        } catch (RuntimeException e) {
            sendErrorApiResponse(response, e);
        }
    }
}</code></pre>
<blockquote>
<p>단계 별로 에러가 발생해서 단계별 <code>Trouble Shooting</code> 과정을 정리했다.</p>
</blockquote>
<hr>
<br/>

<h2 id="1-첫-화면인-login-페이지는-cors-에러가-발생하지-않지만-로그인-완료-이후-cors-에러-발생">1) 첫 화면인 login 페이지는 CORS 에러가 발생하지 않지만 로그인 완료 이후 CORS 에러 발생</h2>
<h3 id="상황">상황</h3>
<ul>
<li><code>POST</code> 요청인 <code>login</code>은 <code>CORS</code> 에러가 발생하지 않는데, 
로그인 완료 후 <code>GET</code>요청으로 메인 페이지(<code>api/columns</code>)로 리다이렉트되면서 <code>CORS</code> 에러 발생</li>
</ul>
<h3 id="에러-메시지">에러 메시지</h3>
<blockquote>
<p><img src="https://i.imgur.com/drdQSj5.png" alt="">
<img src="https://i.imgur.com/r2lc7Qa.png" alt=""></p>
<pre><code>Access to fetch at &#39;http://localhost:8080/api/columns&#39; 
from origin &#39;http://localhost:5173&#39; has been blocked by CORS policy: 
Response to preflight request doesn&#39;t pass access control check: 
No &#39;Access-Control-Allow-Origin&#39; header is present on the requested resource. 
If an opaque response serves your needs, set the request&#39;s mode to &#39;no-cors&#39; to fetch the resource with CORS disabled.</code></pre></blockquote>
<pre><code>
### 의문
- CORS 에러가 날 거면 다 같이 나지 왜 따로 따로 나는가?
- 왜 `preflight` 요청은 `CORS` 에러가 안 뜨고 본요청만 `CORS` 에러가 발생하는가?

### 시도한 해결 방법
- 팀 프로젝트를 시작하기 전에 WAS 미션을 진행하며 `Spring` 구조에 대해 공부하면서 봤던 구조가 생각났다.
&gt; ![](https://i.imgur.com/kMX17WR.png)
출처: https://ee-22-joo.tistory.com/20
- `Filter`가 `Dispatcher Servlet` 실행되기 전에 먼저 실행되니까 `WebMvcConfigurer`의 설정이 적용이 안되는 것 같다는 생각이 들었다.
  ~~- 근데 왜 login 페이지 진입은 되는 것인지? 이해할 수가 없네...(답답)~~
- 무튼 그래서 &quot;`Login Filter` 이전에 `CORS Filter`를 넣어주면 되지 않을까?&quot;라는 생각이 들어 `WebConfig`를 삭제하고 `CORS Filter`를 추가했다.

**`FilterConfig` 코드**
```java
@Configuration  
public class FilterConfig {  

    // 추가한 필터
    @Bean  
    public FilterRegistrationBean corsFilter() {  
        FilterRegistrationBean&lt;Filter&gt; filterRegistrationBean = new FilterRegistrationBean&lt;&gt;();  
        filterRegistrationBean.setFilter(new CorsFilter());  
        filterRegistrationBean.setOrder(1);  // 첫번째 순서로 넣어주기
        return filterRegistrationBean;  
    }  

    // 기존 필터
    @Bean  
    public FilterRegistrationBean jwtAuthorizationFilter(ObjectMapper mapper) {  
        FilterRegistrationBean&lt;Filter&gt; filterRegistrationBean = new  
                FilterRegistrationBean&lt;&gt;();  
        filterRegistrationBean.setFilter(new JwtAuthorizationFilter(mapper));  
        filterRegistrationBean.setOrder(2);  // 순서 1에서 2로 변경
        return filterRegistrationBean;  
    }  

}</code></pre><p><strong><code>CorsFilter</code> 코드</strong></p>
<pre><code class="language-java">public class CorsFilter implements Filter {  

    @Override  
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)  
            throws IOException, ServletException, IOException {  
        HttpServletResponse response = (HttpServletResponse) res;  
        response.setHeader(&quot;Access-Control-Allow-Origin&quot;, &quot;*&quot;);  
        response.setHeader(&quot;Access-Control-Allow-Methods&quot;, &quot;POST, OPTIONS, GET, DELETE, PUT&quot;);  
        response.setHeader(&quot;Access-Control-Max-Age&quot;, &quot;3600&quot;);  
        response.setHeader(&quot;Access-Control-Allow-Headers&quot;, &quot;Authorization, x-requested-with, origin, content-type, accept&quot;);  
        chain.doFilter(req, res);  
    }  

}</code></pre>
<hr>
<br/>

<h2 id="2-cors-filter-→-login-filter-설정-이후에도-동일한-에러-발생">2) CORS Filter → Login Filter 설정 이후에도 동일한 에러 발생</h2>
<h3 id="상황-1">상황</h3>
<ul>
<li>1)번 상황이랑 동일</li>
</ul>
<p>api 요청할 때 <code>preflight</code> 요청이 계속 발생하여 <code>preflight</code>에 대해 공부하고 다시 Trouble Shooting 시작</p>
<h3 id="원인추측">원인(추측)</h3>
<ul>
<li><code>preflight</code> 요청은 <code>header</code>에 <code>authorization</code> 헤더가 없어서 인증을 하지 못해 <code>401</code> 에러가 발생하고, 이로 인해 본 요청에서 무슨 오류가 생긴게 아닐까 생각했다.</li>
</ul>
<h3 id="의문">의문</h3>
<ul>
<li><code>preflight</code> 요청이 <code>401</code>이 발생했다는 것은 <code>CORS</code>는 통과한 것으로 이해된다.<ul>
<li>로그를 찍었을 때 Login Filter까지 들어온 것을 확인했기 때문이다.
<img src="https://i.imgur.com/qL1uLrK.png" alt=""></li>
</ul>
</li>
<li>그러면 본요청도 <code>CORS</code> 에러가 나지 않아야 하는 것이 정상이 아닌가?<ul>
<li>본요청은 <code>request</code> 메시지를 파싱하는 순서가 다른가? </li>
<li>예를 들면 header를 파싱하는 과정에서 <code>allow-origin</code> header를 <code>authorization</code> header 보다 늦게 파싱해서? <del>(설마.. 브라우저가 얼마나 똑똑한데...)</del></li>
</ul>
</li>
</ul>
<h3 id="시도한-해결-방법">시도한 해결 방법</h3>
<ul>
<li><code>OPTIONS</code>로 <code>preflight</code> 요청이 올 경우 필터 통과하는 코드 추가</li>
</ul>
<pre><code class="language-java">
  @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws ServletException, IOException {

            HttpServletRequest httpServletRequest = (HttpServletRequest) request;

        // OPTIONS로 api 요청 시 필터 통과
        if (httpServletRequest.getMethod().equals(&quot;OPTIONS&quot;)) {   
            return;
        }

        // 이하 코드 생략...

            // api의 화이트리스트 여부 체크

            // request header에 토큰 포함 여부 체크

             // 필터 로직 진행
        }
</code></pre>
<h3 id="결과">결과</h3>
<ul>
<li>여기까지 하고나니 <code>CORS 에러</code>는 없어졌지만 매우 찝찝하다.
<img src="https://i.imgur.com/zqOJ5Wo.png" alt=""></li>
<li>왜냐면 이게 왜 &quot;돼&quot;? 하는 상황이기 때문이다.</li>
<li>과정을 상세히 기록하고 <code>Spring</code>과 <code>http 요청 흐름</code>에 대한 지식을 더 쌓고 의문을 해결해야겠다.</li>
</ul>
<blockquote>
<p>💡 참고:</p>
<ul>
<li><code>401</code> 에러가 난 부분은 JWT 인증 관련 에러인데 해당 에러에 대한 Trouble Shooting 과정은 블로그 글 → <a href="https://velog.io/@jinny-l/JWT-SignatureException-Error">[JWT] SignatureException 에러</a>에서 확인 가능하다.</li>
</ul>
</blockquote>
<hr>
<p>글을 작성하다가 문득 팀원이 공유해주신 <a href="https://mangkyu.tistory.com/221">블로그 글</a>을 다시 보면서
<code>Filter</code>를 <code>Bean</code>으로 설정하면 <code>Spring</code>에서 <code>Filter</code>를 관리해주니까
<code>CorsFilter</code>를 구현하지 않고 처음 설정했던 <code>WebMvcConfigure</code>를 설정해주면 먹히지 않을까라는 생각이 갑자기 들었다.</p>
<p>지금은 팀프로젝트가 끝나 서버도 내려가고 당장 쌓인일이 많아서
3일 뒤 새로 시작하는 팀프로젝트 때 꼭 확인해봐야겠다.</p>
<hr>
<ul>
<li>참고: <ul>
<li><a href="https://velog.io/@gillog/Spring-CORS-preFlightOPTIONS-Method-Interceptor-%EA%B2%80%EC%A6%9D-%EB%A1%9C%EC%A7%81-%EC%A0%9C%EC%99%B8%ED%95%B4%EC%A3%BC%EA%B8%B0Access-to-XMLHttpRequest-at-https-from-origin-https-has-been-blocked-by-CORS-policy-Response-to-preflight-request-doesnt-pass-access-control-check-It-does-not-have-HTTP-ok-status">https://velog.io/@gillog/Spring-CORS-preFlightOPTIONS-Method-Interceptor-%EA%B2%80%EC%A6%9D-%EB%A1%9C%EC%A7%81-%EC%A0%9C%EC%99%B8%ED%95%B4%EC%A3%BC%EA%B8%B0Access-to-XMLHttpRequest-at-https-from-origin-https-has-been-blocked-by-CORS-policy-Response-to-preflight-request-doesnt-pass-access-control-check-It-does-not-have-HTTP-ok-status</a></li>
<li><a href="https://wonit.tistory.com/572">https://wonit.tistory.com/572</a></li>
<li><a href="https://junhokims.tistory.com/29">https://junhokims.tistory.com/29</a></li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JWT] SignatureException 에러]]></title>
            <link>https://velog.io/@jinny-l/JWT-SignatureException-Error</link>
            <guid>https://velog.io/@jinny-l/JWT-SignatureException-Error</guid>
            <pubDate>Fri, 21 Jul 2023 15:18:49 GMT</pubDate>
            <description><![CDATA[<h1 id="signatureexception-에러">SignatureException 에러</h1>
<h2 id="상황">상황</h2>
<ul>
<li>팀 프로젝트를 진행하며 <code>JWT</code>로 로그인 기능 구현 </li>
<li>클라이언트에서 서버로 API 요청 시 <code>SignatureException</code> 에러 발생</li>
</ul>
<h2 id="에러-메시지">에러 메시지</h2>
<p><img src="https://i.imgur.com/B6XfMfV.png" alt=""></p>
<pre><code class="language-java">io.jsonwebtoken.security.SignatureException: 
JWT signature does not match locally computed signature. 
JWT validity cannot be asserted and should not be trusted.</code></pre>
<h2 id="원인">원인</h2>
<ul>
<li><code>JWT</code> 파싱 오류</li>
<li>클라이언트에서 <code>JSON.Stringfy()</code>로 토큰을 감싸서 전송
<img src="https://i.imgur.com/zu2TbS6.png" alt=""></li>
<li>헤더에 담긴 토큰을 잘 보면 <code>&quot;&quot;</code>로 감싸져 있다.</li>
</ul>
<h2 id="해결">해결</h2>
<ul>
<li><code>JSON.Stringfy()</code> 코드 제거</li>
</ul>
<p>처음에 <code>JWT</code> 토큰 발행 시 넣었던 <code>Signature</code>와 <code>Claim</code> 파싱할 때 넣은 토큰의 암호화를 잘못했나 생각이 들어 온갖 삽질을 했던 것은 안비밀</p>
<p><del>트러블 슈팅하면서 계속 봤는데 따옴표가 자연스러웠다...</del></p>
<p><del>머리가 혼탁해지면 잠깐 쉬다오고</del>
<del>눈을 크게 뜨고 보자,,,</del></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[AWS] S3 정적 호스팅 페이지 새로고침 시 404 NoSuchKey 에러가 발생하는 이슈]]></title>
            <link>https://velog.io/@jinny-l/AWS-S3-web-hosting-reload-404-NoSuchKey-error</link>
            <guid>https://velog.io/@jinny-l/AWS-S3-web-hosting-reload-404-NoSuchKey-error</guid>
            <pubDate>Fri, 21 Jul 2023 15:03:00 GMT</pubDate>
            <description><![CDATA[<h1 id="s3-정적-호스팅-페이지-새로고침-시-404-nosuchkey-에러가-발생하는-이슈">S3 정적 호스팅 페이지 새로고침 시 404 NoSuchKey 에러가 발생하는 이슈</h1>
<h2 id="상황">상황</h2>
<ul>
<li>팀 프로젝트를 진행하면서 리액트 서버는 S3, API 서버는 EC2로 배포 진행</li>
<li>로그인 이후 진입한 페이지에서 새로 고침하면 <code>404 NoSuchKey 에러</code> 발생</li>
</ul>
<h2 id="원인">원인</h2>
<ul>
<li>S3 도메인은 <code>index.html</code> 과 매핑되어 있음</li>
<li>로그인 이후 진입한 페이지 url은 <code>http://{도메인}/main</code> 과 매핑</li>
<li><code>http://{도메인}/main</code> 과 매핑된 리소스가 없기 때문에 에러 발생</li>
</ul>
<blockquote>
<p>🤔 <strong>그렇다면 왜 로그인 이후 에러가 발생하지 않았을까?</strong></p>
<ul>
<li><code>React</code>는 페이지 이동 시 새로운 url로 이동 후 화면을 새로 고침하는 것이 아닌 현재 페이지에서 렌더링을 재진행하는 방식</li>
<li>따라서 <code>http://{도메인}/</code>에서 로그인 이후 <code>http://{도메인}/main</code>으로 이동할 때 실제로 url이 리다이렉트된 것이 아니라 <code>http://{도메인}/</code>에 원래 매핑되어 있던 <code>index.html</code> 파일에서 화면만 다시 랜더링되어서 에러가 발생하지 않은 것으로 추측됨</li>
</ul>
</blockquote>
<h2 id="해결">해결</h2>
<ul>
<li><code>AWS S3</code>에서 S3에서 발생한 에러를 특정 리소스와 매핑할 수 있는 기능 제공</li>
<li>에러 페이지도 <code>index.html</code>로 지정하여 새로고침 해서 에러가 발생하면 <code>index.html</code>로 연결되게 설정</li>
</ul>
<p><img src="https://velog.velcdn.com/images/jinny-l/post/912a8287-9e80-4ef7-ae06-a877902047fe/image.png" alt=""></p>
<hr>
<ul>
<li>참고:<ul>
<li><a href="https://github.com/bluelion2/Project-issue-repo/issues/22">https://github.com/bluelion2/Project-issue-repo/issues/22</a></li>
<li>팀원 <a href="https://github.com/crtEvent">Ape</a>의 지식</li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[코드스쿼드] Max 16~17주차 - Kiosk 팀 프로젝트]]></title>
            <link>https://velog.io/@jinny-l/codesquad-retrospective-kiosk-team-project</link>
            <guid>https://velog.io/@jinny-l/codesquad-retrospective-kiosk-team-project</guid>
            <pubDate>Thu, 06 Jul 2023 06:29:48 GMT</pubDate>
            <description><![CDATA[<p><em>Max 16주차: 23-06-19 ~ 23-06-23</em>
<em>Max 17주차: 23-06-26 ~ 23-06-30</em></p>
<p>이번 회고를 하기 전에... 10주차 ~ 15주차 회고가 없는데 
약 한달 간 자율 학습에 가까운 시간을 가지면서 회고보단 급한 기술 부채를 쳐냈다.
또 블로그보단 옵시디언에 공부한 내용을 많이 기록하면서 블로깅 습관을 잃어버린 것 같다.
다시 열심히 회고를 해보려고 한다! 💪</p>
<hr>
<h1 id="👥-첫번째-팀-프로젝트">👥 첫번째 팀 프로젝트</h1>
<p>드디어 부트캠프의 꽃(?) 팀 프로젝트가 시작되었다.
팀 프로젝트는 카페 키오스크를 구현하는 것이었는데
첫 번째 프로젝트이고 2주간의 미션이라 그런지 엄청 어려운 요구사항은 없었다.</p>
<p>자세한 내용은 아래 깃허브 레포지토리에서 확인 가능하다.</p>
<blockquote>
<p>Github: <a href="https://github.com/codesquad-gwanaksan/kiosk-max">카페 키오스크 프로젝트</a></p>
</blockquote>
<p>총평을 해보자면 프론트와의 협업이 처음이라 초반에는 헤맸지만
이런저런 시행착오를 겪으면서 개선방향에 대해 생각해볼 수 있었다.</p>
<p>필자는 인프라랑 일부 API 개발을 담당했는데
이번 회고는 프로젝트를 진행하면서 겪었던 이슈와 Trouble Shooting 내용 위주가 될 것 같다.</p>
<p>조금 큰 이슈들은 위에 적어둔 <a href="https://github.com/codesquad-gwanaksan/kiosk-max/wiki">레포지토리의 Wiki</a>에 정리해두어서 Wiki에 없는 보다 자잘한 이슈들 위주로 회고할 예정이다.</p>
<hr>
<h2 id="1-🛠️-인프라">1. 🛠️ 인프라</h2>
<p><img src="https://velog.velcdn.com/images/jinny-l/post/04f7a92a-870c-405d-96f7-386c5fd16a1f/image.png" alt=""></p>
<p>우선 인프라 구조는 이렇게 잡았다.
아직 인프라를 많이 접해보지 못했고, 각각의 장단점을 모르기 때문에 
구글링해보았을 때 현재 내 지식수준에서 이해가 가능하고 적용할 수 있는 구조를 택했다.</p>
<br/>

<p align="center">           • • •</p>

<h3 id="1-프론트-배포의-시행착오">1) 프론트 배포의 시행착오</h3>
<p>처음엔 잘 몰라서 백엔드만 배포하면 될줄 알고 있어서,
프론트엔드 분들이 <code>firebase</code>로 배포를 해주셨는데 두가지 문제가 있었다.</p>
<p>** 1. CORS 문제** </p>
<ul>
<li><p>프론트는 <code>firebase</code>로 배포하고, 백엔드는 <code>AWS EC2</code>로 배포했기 때문에 당연히(?)도 CORS 문제가 발생했다.
(지금 생각해보면 당연한데 그때만 해도 엄청난 에러였다. 🥲)</p>
</li>
<li><p>구글링을 해보니 스프링으로 설정 가능한 부분이어서 코드를 추가해보니 API 요청과 응답은 문제가 없는 듯 했다.</p>
</li>
<li><p>다만 이부분은 당시에 <code>*</code>로 모든 외부 IP에 대해 허용을 했었는데,
만약 나중에 또 이런 상황이 있다면 보안 문제로 인해 특정 도메인 혹은 IP에만 허용하도록 해야겠다고 생각했다.</p>
</li>
</ul>
<p>** 2. <code>https</code> 통신 문제 **</p>
<ul>
<li><p><code>firebase</code>로 배포하면 프론트는 <code>https</code>로 배포가 되는데 백엔드는 도메인이 없기 때문에 <code>http</code>였다.</p>
</li>
<li><p>찾아보니까 https에서 http로의 API 요청은 브라우저에서 차단한다.</p>
</li>
<li><p>크롬의 경우, 브라우저 설정을 통해 차단을 풀 수는 있지만 보안 경고가 계속 뜬다.</p>
</li>
<li><p>이를 해결하려면 SSL 인증서가 필요한데, SSL 인증서를 발급받으려면 도메인이 필요하기 때문에 불가능했다.</p>
</li>
<li><p>백엔드에서 프론트엔드 배포도 같이 하는 것이 좋을 것 같아서 백엔드에서 모든 배포를 하기로 했다.</p>
</li>
</ul>
<br/>
<p align="center">• • •</p>

<h3 id="2-프론트엔드-배포">2) 프론트엔드 배포</h3>
<p>처음에는 프론트엔드 배포를 한다는 것이 막막했다.
당시 월말이라 AWS 프리티어 사용량이 거의 남지 않아서 EC2 하나밖에 사용할 수 없는 상황이었다.</p>
<p>지금 생각해보면 바보같을 수도 있는데, 은연중에 EC2 하나당 서버 하나만 돌릴 수 있다고 생각했던 것 같다.</p>
<p>그리고 또 당시<del>(라고 해봤자 2주전...이지만 배포하면서 포트에 대한 이해도가 갑자기 높아졌다.)</del> 포트에 대한 개념이 부족해서
같은 서버에 넣는다고 해도 어떻게 통신하지? 했던 의문이 있었다.</p>
<br/>

<p><strong>서버 하나에 프론트 서버 백엔드 서버 배포하기</strong></p>
<p>무튼 일단 <code>t2.micro</code>인 EC2에 서버 2개를 돌려보고
부딪히면서 이슈를 해결해야겠다고 생각했다.</p>
<p>프론트엔드 분 도움을 받아 리액트 서버를 돌리기 위한 3가지 명령어를 배우고 서버에 띄어봤다.</p>
<pre><code class="language-shell">npm install
npm run build
npm start</code></pre>
<p><del>물론 이 사이에 이런저런 이슈들이 많았다...
리액트를 처음 빌드하다보니, <code>package.json</code> 을 지워버리기도 하고
<code>node module</code> 폴더가 없어서 서버가 안 돌아간다던지...ㅎ</del></p>
<p>근데 API 요청할 때 서버에서 응답을 못하고 있었다.</p>
<p>이유를 못찾고 있었는데
<code>NGINX</code>가 궁금해서 새벽에 혼자 사용법 찾아보면서 이런저런 설정을 해보다가
API 요청 URL의 Proxy_Pass를 <code>xxx.xxx.xx.xx:8080</code> 이런식으로 설정해줬는데 갑자기 잘 되는 것이었다.!!</p>
<p>그렇다 원인은 리액트에서 API 요청할 때 포트 번호가 없었던 것이었다...</p>
<p>(그런데 firebase에서 배포했을 때는 어떻게 정상적으로 응답이 된거지..?)</p>
<p>이슈 해결하고 또 문득 들었던 생각이 Elastic IP를 사용하지 않아서 IP가 매번 바뀌고 리액트랑 스프링이랑 같은 서버 안에 있으니까 굳이 IP를 적지 않고 localhost:8080으로 보내면 요청이 받아질 것이라고 생각이 들었는데 왜인지 실패했다..</p>
<p>이건 이유를 차차 알아봐야겠다.</p>
<hr>
<h2 id="2-🧑🏻💻-api-개발">2. 🧑🏻‍💻 API 개발</h2>
<p>팀원분이 개발한 API에서 엔티티나 DTO를 사용해야 하는데 <code>@Getter</code>가 없었어서 값을 못 불러오는 등
자잘한 이슈를 제외하면 큰 에러도 없었고 API 개발은 생각보다 무난했던 것 같다.</p>
<p>언급할만 이슈는 기획서 변경과 <code>@Scheduled</code> 어노테이션 관련 이슈가 있다.</p>
<br/>

<p align="center">• • •</p>

<h3 id="1-기획서-변경">1) 기획서 변경</h3>
<p>기능 요구사항 중 &quot;실시간 판매량을 집계하여 BEST 메뉴를 선정하여 카테고리 상단에 노출&quot;하는 요구사항이 있었다.</p>
<p>팀원들이랑 여러방면으로 논의를 했었는데, 
카페에서 실시간 판매량을 집계하여 노출하는 것의 목적과 의미가 서버 부하 대비 없는 것 같았다.</p>
<p>실시간 판매 노출 대상은 고객이고, 베스트 메뉴는 고객의 의사결정을 돕는 것이다.
특정 메뉴를 의도적으로 노출해 판매량을 늘리는게 아니라 실제 판매량을 집계하여 노출하는 것이라면
굳이 고객 구매 1회당 서버에 요청을 보내 BEST 메뉴를 계속 갱신하는 것과
전날의 합산 통계를 노출하는 것의 큰 차이는 없다고 판단이 들었다.</p>
<p>실제 상황에서도 기획서를 무조건 따르는게 아니라 논의를 통해 더 나은 방향으로 나아가는 것이 맞다고 생각이 들어 요구사항을 &quot;전날 판매 기준으로 집계&quot;로 변경하였다.</p>
<br/>

<p align="center">• • •</p>

<h3 id="2-scheduled-어노테이션-관련-이슈">2) <code>@Scheduled</code> 어노테이션 관련 이슈</h3>
<p>전날 판매량 기준으로 집계하는 것을 매일 수동으로 할 수는 없으니 매일 00시에 자동으로 DB가 갱신되도록 하는 방법을 찾아야했다.</p>
<p>우리 팀이 찾은 방법으로는 다음과 같았다. </p>
<ul>
<li><code>trigger</code></li>
<li><code>procedure</code></li>
<li><code>@Scheduled</code> 어노테이션 활용</li>
</ul>
<p>이전에 호눅스 수업에서 <code>procedure</code>는 레거시 프로젝트에 많이 남아 있다고 했다.
요새는 DB의 책임과 어플리케이션의 책임을 분리한다고 들었던 기억이 있었다.</p>
<p>필자는 자동 집계는 DB의 책임이 아니고 비즈니스 로직이라고 생각이 들었고
비즈니스 로직은 어플리케이션 단에서 책임지는 것이 맞다고 생각이 들었다.</p>
<p>무튼 다음과 같이 코드를 짰는데, 자동 집계가 안되는 이슈가 있었다.</p>
<pre><code class="language-java">@Repository
public class JdbcOrdersRepositoryImpl implements OrdersRepository {

    private static int today;
    private static Long orderNumber = 0L; // 주문 번호는 매일 0으로 초기화되어야 한다.

    static {
        dailyReset(); // 날짜가 바뀌었는지 확인하고 주문번호를 0으로 리셋한다.
        setToday();   // 날짜가 바뀌었는지 확인할 수 있도록 오늘 날짜를 바꿔준다.
    }

    @Scheduled(cron = &quot;0 0 0 * * *&quot;, zone = &quot;Asia/Seoul&quot;)
    public static void setToday() {
        today = ZonedDateTime.now().toLocalDate().getDayOfMonth();
    }

    @Scheduled(cron = &quot;0 0 0 * * *&quot;, zone = &quot;Asia/Seoul&quot;)
    public static void dailyReset() {
        LocalDate current = ZonedDateTime.now().toLocalDate();
        if (current.getDayOfMonth() - today &gt;= 1) {
            orderNumber = 0L;
        }
    }</code></pre>
<p>알고보니 <code>@Scheduled</code> 어노테이션을 작동시키기 위해서는 <code>@EnableScheduling</code>을 스프링 부트 실행파일에 선언해야 했다.</p>
<p>다음과 같이 <code>@EnableScheduling</code>을 선언해주면 스케줄러가 잘 작동한다.</p>
<pre><code class="language-java">@SpringBootApplication
@EnableScheduling
public class KioskWebApplication {

    public static void main(String[] args) {
        SpringApplication.run(KioskWebApplication.class, args);
    }
}</code></pre>
<hr>
<h2 id="3-🤔-개선해보고-싶은-것과-고민들">3. 🤔 개선해보고 싶은 것과 고민들</h2>
<p>프로젝트를 진행하면서 개선해보고 싶은 것과 고민들이 있었는데
기록해놓고 차차 해결하고자 한다.</p>
<br/>
<p align="center">• • •</p>

<h3 id="1-백엔드-팀원-간-협업-관련">1) 백엔드 팀원 간 협업 관련</h3>
<p>백엔드 팀원이 4명이었고 요구사항이 작아 API 개발을 잘게 쪼개다보니
Class를 최대한 겹치지 않게 짰는데도 어쩔 수 없이 개발하면서 팀원 간 코드 의존성이 생겼다.</p>
<p>이러면서 Git 충돌이 났었는데, 충돌이 안나게 협업을 어떻게 하면 좋을지 고민이었다.
지금 당장 생각나는건 인터페이스를 만들고 인터페이스 기반으로 작업하는 것이다.</p>
<br/>
<p align="center">• • •</p>

<h3 id="2-시간-관련-테스트">2) 시간 관련 테스트</h3>
<p><code>@Scheduled</code> 어노테이션이 잘 작동하는지 확인하기 위해서 테스트 코드를 짜는데 어려움이 있었다.
시간을 의도적으로 조작해야 했는데, 방법을 잘 모르겠어서 리플렉션을 사용했는데 맞지 않는 방법인 것 같았다.</p>
<pre><code class="language-java">     @Test
    @DisplayName(&quot;자정이 되면 주문번호가 0으로 초기화됩니다.&quot;)
    public void dailyReset() throws NoSuchFieldException, IllegalAccessException {

        // given
        Field today = ordersRepository.getClass().getDeclaredField(&quot;today&quot;);
        today.setAccessible(true);
        today.set(ordersRepository, yesterday.getDayOfMonth());

       // 생략...
    }
</code></pre>
<br/>
<p align="center">• • •</p>

<h3 id="3-이미지-요청-관련">3) 이미지 요청 관련</h3>
<p>현재 구현된 바로는 키오스크에서 표시되는 음료 이미지가 <code>AWS S3</code>에 저장되어 있고
키오스크에서 주문이 완료되고 메인 페이지로 돌아갈 때마다 서버로 <code>GET</code> 요청을 보내서 이미지를 불러온다.</p>
<p>개인적으로 매번 이렇게 불러오는게 매우 비효율적이라고 생각이 들었다.
키오스크가 한번 부팅될 때 최초 1회 이미지를 다 불러와서 이후에는 서버 요청 없이 이미지를 사용하는 것이 좋다고 생각이 들었다.</p>
<p>그리고 여러 페이지에 동일한 이미지가 다양한 사이즈로 표시되는데 한 개의 이미지로 프론트에서 사이즈 조절하는 것보다 뭔가 다른 좋은 방법이 없을까 고민이 되었다.</p>
<p>이 두 고민은 꼭 해결해서 다음 혹은 다다음 프로젝트에 적용해보고 싶다.</p>
<br/>
<p align="center">• • •</p>

<h3 id="4-join-여러-개-vs-select-여러-번">4) <code>join</code> 여러 개 vs <code>select</code> 여러 번</h3>
<p>예를 들면 Entity <code>ABC</code>가 있는데 필드 <code>A</code>는 table A, <code>B</code>는 table B, <code>C</code>는 table C에 있다고 하면</p>
<p><code>join</code>을 여러 번 해서 한번에 Entity를 만드는게 좋을까?
아님 <code>select</code> 절로 여러번 쿼리를 날려서 조합을 해 Entity를 만드는게 좋을까?</p>
<p><code>join</code>을 몇번 하느냐, 테이블에 데이터 양이 얼마나 많은가에 따라 달라지겠지만
아직 잘 모르겠다.</p>
<br/>
<p align="center">• • •</p>

<h3 id="5-explain으로-쿼리-성능-개선해보기">5) <code>Explain</code>으로 쿼리 성능 개선해보기</h3>
<p>다음 프로젝트 때 꼭 시도해볼 것!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[ gradlew permission denied 이슈]]></title>
            <link>https://velog.io/@jinny-l/gradlew-permission-denied-issue</link>
            <guid>https://velog.io/@jinny-l/gradlew-permission-denied-issue</guid>
            <pubDate>Wed, 05 Jul 2023 19:00:01 GMT</pubDate>
            <description><![CDATA[<h2 id="gradlew-permission-denied-이슈">gradlew permission denied 이슈</h2>
<h3 id="상황">상황</h3>
<ul>
<li>Githun Actions 테스트 하면서 gradlew permission denied 이슈 발생</li>
</ul>
<h3 id="원인">원인</h3>
<ul>
<li>협업 시 Springboot 세팅을 해주신 팀원분의 개발환경이 Window였다.</li>
<li>Window 환경에서 작업해서 소스를 push하면 파일 생성 시 기본 권한이 <code>644</code>로 생성된다.</li>
<li>644는 루트 사용자, 일반 사용자, 그룹 모두 실행 권한이 없기 때문에 권한을 추가해줘야 한다.</li>
</ul>
<h3 id="해결">해결</h3>
<ul>
<li>+x 옵션 또는 755 권한으로 실행 권한을 추가해주자.<pre><code>chmod +x ./gradlew</code></pre></li>
<li>혹은 git 명령어를 통해 권한을 변경할 수 있다.<pre><code>git update-index --add --chmod=+x gradlew</code></pre></li>
</ul>
<blockquote>
<p>💡참고:
아래 명령어를 사용하면 git 인덱스에 잡혀있는 파일의 권한을 확인할 수 있다.</p>
<pre><code>git ls-tree HEAD</code></pre></blockquote>
<hr>
<ul>
<li>참고:<ul>
<li><a href="https://may9noy.tistory.com/160">https://may9noy.tistory.com/160</a></li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[웹서버와 WAS란?]]></title>
            <link>https://velog.io/@jinny-l/%EC%9B%B9%EC%84%9C%EB%B2%84%EC%99%80-WAS%EB%9E%80</link>
            <guid>https://velog.io/@jinny-l/%EC%9B%B9%EC%84%9C%EB%B2%84%EC%99%80-WAS%EB%9E%80</guid>
            <pubDate>Mon, 19 Jun 2023 03:54:05 GMT</pubDate>
            <description><![CDATA[<h1 id="들어가기-전에">들어가기 전에...</h1>
<h2 id="web이란">Web이란?</h2>
<ul>
<li>클라이언트와 서버가 인터넷을 통해 정보를 상호작용(정보 공유, 검색 등)을 할 수 있게 하는 서비스</li>
<li>웹의 3요소:<ul>
<li>HTML(응답)</li>
<li>HTTP(통신 방법)</li>
<li>URL(요청하는 주소)</li>
</ul>
</li>
</ul>
<p><img src="https://i.imgur.com/cfS5dhY.png" alt=""></p>
<h2 id="web의-구조-동작-원리">Web의 구조, 동작 원리</h2>
<blockquote>
<p>💡 잠깐!
먼저 생각해보면 좋은 것(면접 단골 질문):
웹 브라우저에 <a href="http://www.google.com">www.google.com</a> 을 입력하면 무슨 일이 일어날까?</p>
</blockquote>
<p><img src="https://i.imgur.com/T6jCHUg.png" alt=""></p>
<ol>
<li>클라이언트가 브라우저 입력창에 <a href="http://www.google.com">http://www.google.com</a> 을 입력한다.</li>
<li><code>DNS 서버</code>에서 IP 주소를 확인한다.<ul>
<li>클라이언트의 컴퓨터에 있는 로컬 DNS 캐시라는 임시 저장소의 캐시를 확인하여 해당 도메인 이름에 대한 IP 주소가 이미 저장되어 있는지 확인</li>
<li>로컬 DNS 캐시에 도메인 이름에 대한 IP 주소가 없는 경우, 클라이언트는 DNS 서버에 질의 전송. (이때, 미리 설정된 기본 DNS 서버의 IP 주소를 사용)</li>
<li>해당 DNS 서버에도 없을 경우, 다른 DNS 서버로 전송</li>
</ul>
</li>
<li>클라이언트(브라우저)가 서버에 <code>Request Message</code>를 전송한다.</li>
<li>서버는 적절한 내용을 담아 <code>Response Message</code>를 전송한다.</li>
<li>브라우저는 응답을 렌더링해서 사용자에게 보여준다.</li>
</ol>
<p><strong>여기서 Web Server와 WAS는 3번~4번 사이의 과정에 있다.</strong></p>
<hr>
<h1 id="1-web-server와-was">1. Web Server와 WAS</h1>
<p><img src="https://i.imgur.com/Oogao5c.png" alt=""></p>
<h2 id="1-web-server">1) Web Server</h2>
<ul>
<li>HTTP 기반으로 동작, <strong>정적 리소스</strong> 제공</li>
<li>클라이언트로부터 HTTP 요청을 받아 HTML 문서나 각종 리소스를 전달하는 서버</li>
<li>동적인 요청이 들어왔을 때, 컨테이너로 보내주는 역할</li>
<li>예: NGINX, APACHE </li>
</ul>
<blockquote>
<p>💡 정적 리소스:</p>
<ul>
<li>HTML, CSS, JS, 이미지, 영상 등</li>
<li>요청 인자 값에 상관없이 달라지지 않는 컨텐츠</li>
<li>어느 사용자 요청이든 항상 동일한 컨텐츠</li>
</ul>
</blockquote>
<p><strong>Web Server의 기능을 정리해보면 다음과 같다.</strong></p>
<ul>
<li>클라이언트로부터 HTTP 요청을 받을 수 있다.</li>
<li>정적 컨텐츠 요청 시 정적 컨텐츠를 제공할 수 있다.</li>
<li>동적 컨텐츠 요청시 WAS로 전달하여 WAS가 처리한 결과를 클라이언트에 전달할 수 있다.</li>
</ul>
<h2 id="2-web-container">2) Web Container</h2>
<ul>
<li>컨테이너는 동적인 데이터들을 처리하여 정적인 페이지로 생성해주는 소프트웨어 모듈<ul>
<li>예: 톰캣, Jetty 등</li>
</ul>
</li>
<li>웹 컨테이너 중 Servlet을 관리할 수 있는 컨테이너를 Servlet Container라고 한다.<ul>
<li>예: 톰캣</li>
</ul>
</li>
</ul>
<h2 id="3-web-application-server">3) Web Application Server</h2>
<ul>
<li>HTTP 기반으로 동작</li>
<li>웹 서버 기능 포함 - 정적 리소스 제공 가능</li>
<li>프로그램 코드를 실행해서 애플리케이션 로직 수행<ul>
<li>동적 HTML, HTTP API</li>
<li>서블릿, JSP, 스프링 MVC</li>
</ul>
</li>
<li>예: 톰캣</li>
</ul>
<p><strong>WAS의 기능을 정리해보면 다음과 같다.</strong></p>
<ul>
<li>클라이언트로부터 HTTP 요청을 받을 수 있다. (대부분의 WAS는 Web Server 내장)</li>
<li>요청에 맞는 정적 컨텐츠를 제공할 수 있다.</li>
<li>DB 조회나 다양한 로직 처리를 통해 동적 컨텐츠를 제공할 수 있다.</li>
</ul>
<h2 id="web-server-was-비슷한데">Web Server?? WAS?? 비슷한데?</h2>
<ul>
<li>둘의 용어가 모호하긴 한다.</li>
<li>웹 서버도 프로그램을 실행하는 기능이 있는 경우도 있고 WAS도 웹 서버의 기능을 제공하기도 한다.</li>
</ul>
<p><strong>그래서 자바에서는 뭐가 WAS일까??</strong>
→ 서블릿 컨테이너 기능을 제공하면 WAS라고 한다.
<del>(하지만... 서블릿 없이 자바코드를 실행하는 서버 프레임워크도 있음)</del></p>
<blockquote>
<p>💬 개인적인 생각:
자료를 찾아보면서 헷갈렸던 부분은 WAS의 scope였다.
WAS는 Web Server와 Web Container로 구성되어 있다고 하는데
일반적으로 Web Server는 Web Server로, Web Container는 WAS라고 부르는 것 같다.
아니면 Web Server를 따로 두기 때문에 그런 것일까?</p>
</blockquote>
<h2 id="참고-왜-톰캣을-아파치-톰캣이라고-부를까">참고: 왜 톰캣을 아파치 톰캣이라고 부를까?</h2>
<ul>
<li>톰캣(WAS)은 편의를 위해 아파치의 기능(웹서비스 데몬, Httpd)을 포함하고 있다.</li>
<li>즉, 톰캣이 아파치의 일부 기능을 제공해주기 때문에 합쳐서 부른다.</li>
</ul>
<p><img src="https://i.imgur.com/B8zbBQ0.png" alt=""></p>
<hr>
<h1 id="2-웹-시스템-구성">2. 웹 시스템 구성</h1>
<p><strong>부제: WAS가 Web Server 기능도 제공하는데 왜 Web Server를 따로 둘까?</strong></p>
<h2 id="1-was-db로만-구성">1) WAS, DB로만 구성</h2>
<ul>
<li>WAS는 정적 리소스, 애플리케이션 로직 모두 제공 가능</li>
<li>따라서 WAS, DB만으로 시스템 구성이 가능하지만 다음과 같은 우려가 있음<ul>
<li>WAS가 너무 많은 역할을 담당, 서버 과부하 우려</li>
<li>정적 리소스 서빙 때문에 비싼 어플리케이션 로직 수행이 어려울 수 있음</li>
<li>WAS 장애 시, 오류 화면 노출이 불가능</li>
</ul>
</li>
</ul>
<p><img src="https://i.imgur.com/3pTbP10.png" alt=""></p>
<h2 id="2-web-was-db로-구성">2) WEB, WAS, DB로 구성</h2>
<ul>
<li>웹 서버: <ul>
<li>정적 리소스 처리</li>
<li>동적인 처리 필요 시, WAS에 요청 위임</li>
</ul>
</li>
<li>WAS: <ul>
<li>어플리케이션 처리 전담</li>
</ul>
</li>
</ul>
<p><img src="https://i.imgur.com/AhqfYMW.png" alt=""></p>
<h2 id="was가-web-server-기능도-제공하는데-왜-web-server를-따로-둘까">WAS가 Web Server 기능도 제공하는데 왜 Web Server를 따로 둘까?</h2>
<p>Web Server를 따로 둘 때 다음과 같은 장점이 있다.</p>
<p><strong>장점</strong></p>
<ul>
<li><p>책임 분할을 통해 서버 부하를 방지할 수 있다.</p>
<ul>
<li>WAS는 DB 조회 등 페이지를 만들기 위한 다양한 로직을 처리하는데, 단순한 정적 콘텐츠를 WAS에서 제공한다면 다른 작업에 사용하는 리소스들로 인해 지연이 생겨날 수 있다.</li>
<li>정적 컨텐츠는 Web Server, 동적 컨텐츠는 WAS가 담당하여 서버 부하를 방지한다.</li>
</ul>
</li>
<li><p>효율적인 리소스 관리가 가능하다.</p>
<ul>
<li>정적 리소스가 많이 사용되면 Web 서버 증설 </li>
<li>애플리케이션 리소스가 많이 사용되면 WAS 증설<blockquote>
<p><img src="https://i.imgur.com/n6HR9Oc.png" alt=""></p>
</blockquote>
</li>
</ul>
</li>
<li><p>장애 대응이 가능하다.</p>
<ul>
<li>정적 리소스만 제공하는 웹 서버는 잘 죽지 않음</li>
<li>애플리케이션 로직이 동작하는 WAS 서버는 잘 죽음</li>
<li>WAS, DB 장애시 WEB 서버가 오류 화면 제공 가능<blockquote>
<p><img src="https://i.imgur.com/xNfbQ5L.png" alt=""></p>
</blockquote>
</li>
</ul>
</li>
<li><p>여러 대의 WAS 로드밸런싱 </p>
<ul>
<li>WAS가 처리해야 하는 요청을 여러 WAS가 나누어서 처리할 수 있도록 설정</li>
<li>대용량 웹 어플리케이션의 경우, Web Server와 WAS를 분리하여 무중단 운영을 위한 장배 극복에 쉽게 대응할 수 있다.</li>
</ul>
</li>
<li><p>여러 대의 WAS Health Check</p>
<blockquote>
<p>💡 Health Check란?
Health Check: 서버에 주기적으로 HTTP 요청을 보내 서버의 상태를 확인
<img src="https://i.imgur.com/Q9gz8Vd.png" alt=""></p>
</blockquote>
</li>
<li><p>(물리적으로 분리하여) 보안 강화</p>
<ul>
<li>리버스 프록시를 통해 실제 서버를 외부에 노출하지 않을 수 있다.</li>
<li>SSL에 대한 암복호화 처리에 Web Server를 사용한다.</li>
<li>공격에 대해 Web Server를 앞단에 두어 중요한 정보가 담긴 DB나 로직까지 (WAS까지) 전파되지 못하게 한다.</li>
</ul>
</li>
</ul>
<p><strong>그래서 일반적으로 Web Server, WAS, DB로 웹 시스템을 구성한다.</strong></p>
<blockquote>
<p>💡 참고: By 토비
단지 정적 리소스 처리의 성능만을 위해서라면 굳이 톰캣 앞에 Apache Httpd를 두는 것은 불필요하다.
오히려 메모리만 많이 먹어 관리부담은 커지고, 불필요한 부하만 걸릴 뿐이다.</p>
<p>물론 Httpd의 다른 기능이나 모듈을 사용해야 할 필요가 있다면 그때는 Httpd를 앞에 두고 사용해야겠지만. </p>
<p>예를 들어 하나의 서버에서 PHP애플리케이션과 자바 애플리케이션을 함께 사용하거나, Httpd 서버를 간단한 로드밸런싱을 위해서 사용해야 하는 경우라면 Httpd를 앞에 두고 톰캣을 연결해서 사용하도록 하면 될 것이다.</p>
</blockquote>
<hr>
<h1 id="reference">Reference</h1>
<ul>
<li><a href="https://titanic1997.tistory.com/28">https://titanic1997.tistory.com/28</a></li>
<li><a href="https://carmack-kim.tistory.com/93">https://carmack-kim.tistory.com/93</a></li>
<li><a href="https://jobc.tistory.com/114">https://jobc.tistory.com/114</a></li>
<li><a href="https://inpa.tistory.com/entry/TOMCAT-%E2%9A%99%EF%B8%8F-%EC%84%A4%EC%B9%98-%EC%84%A4%EC%A0%95-%EC%A0%95%EB%A6%AC">https://inpa.tistory.com/entry/TOMCAT-%E2%9A%99%EF%B8%8F-%EC%84%A4%EC%B9%98-%EC%84%A4%EC%A0%95-%EC%A0%95%EB%A6%AC</a></li>
<li><a href="https://velog.io/@kdhyo/Apache-Tomcat-%EB%91%98%EC%9D%B4-%EB%AC%B4%EC%8A%A8-%EC%B0%A8%EC%9D%B4%EC%A7%80">https://velog.io/@kdhyo/Apache-Tomcat-%EB%91%98%EC%9D%B4-%EB%AC%B4%EC%8A%A8-%EC%B0%A8%EC%9D%B4%EC%A7%80</a></li>
<li><a href="https://www.youtube.com/watch?v=mcnJcjbfjrs&amp;t=29s">https://www.youtube.com/watch?v=mcnJcjbfjrs&amp;t=29s</a></li>
<li><a href="https://www.youtube.com/watch?v=F_vBAbjj4Pk&amp;t=87s">https://www.youtube.com/watch?v=F_vBAbjj4Pk&amp;t=87s</a></li>
<li>인프런: 모든 개발자를 위한 HTTP 웹 기본 지식</li>
</ul>
]]></description>
        </item>
    </channel>
</rss>