<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>0_0_yoon.log</title>
        <link>https://velog.io/</link>
        <description>꾸준하게 쌓아가자</description>
        <lastBuildDate>Mon, 03 Feb 2025 09:52:15 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>0_0_yoon.log</title>
            <url>https://images.velog.io/images/0_0_yoon/profile/67ac1cb6-b12f-4fcc-8fe4-e30e06073af5/kwstar_hero-100624509-large.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. 0_0_yoon.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/0_0_yoon" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[SSH 환경변수 설정]]></title>
            <link>https://velog.io/@0_0_yoon/SSH-%ED%99%98%EA%B2%BD%EB%B3%80%EC%88%98-%EC%84%A4%EC%A0%95</link>
            <guid>https://velog.io/@0_0_yoon/SSH-%ED%99%98%EA%B2%BD%EB%B3%80%EC%88%98-%EC%84%A4%EC%A0%95</guid>
            <pubDate>Mon, 03 Feb 2025 09:52:15 GMT</pubDate>
            <description><![CDATA[<h2 id="문제상황">문제상황</h2>
<p>자바앱에서 환경변수를 읽을 수 없었다.
스프링 서버(OCI)에 SSH 로 접속한뒤 환경변수를 설정하고 자바앱을 실행하도록 했다. 그 과정중에 자바앱에서 환경변수를 읽지 못하는 문제가 발생했다.</p>
<pre><code class="language-yaml">...
export OCI_OBJECT_STORAGE_USER_ID=&#39;${OCI_OBJECT_STORAGE_USER_ID}&#39;; \
export REDIS_HOST=&#39;${REDIS_HOST}&#39;; \
export REDIS_PASSWORD=&#39;${REDIS_PASSWORD}&#39;; \
export SSL_KEY_STORE_PATH=&#39;${SSL_KEY_STORE_PATH}&#39;; \
export SSL_KEY_STORE_PASSWORD=&#39;${SSL_KEY_STORE_PASSWORD}&#39;; \

sudo nohup java -Dspring.profiles.active=prod -jar /opt/tag/tag-1.0.0.jar &gt; /dev/null 2&gt;&amp;1 &amp;&quot;</code></pre>
<h2 id="원인">원인</h2>
<p>export 명령어를 실행한 사용자와 nohup 명령어를 실행한 사용자가 달랐기 때문이다.</p>
<p>export 를 실행한 사용자는 opc 였고 nohup 은 위의 스크립트와 같이 관리자 계정으로 실행했다.</p>
<p>기본적으로 환경변수는 사용자간 공유될 수 없다. 그 이유는 다른 사용자의 환경변수에 접근할 수 있다면 보안 문제를 야기하기 때문이다.</p>
<h2 id="해결">해결</h2>
<p>sudo 뒤에 -E(preserve environment) 옵션을 추가하여 환경변수가 공유되도록 했다.</p>
<p>sudo 를 사용한 이유는 해당 스프링앱을 443 포트로 실행하기 위함이다. OS 에서는 1024 번 이하의 포트를 사용하기 위해서는 관리자 권한을 요구하도록 구현되어 있다.  그 이유는 1024 번 이하에 주요 포트들(22, 80, 443 등 )이 포진해 있기 때문이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Oracle Free Tier Review] 간헐적 메모리 부하]]></title>
            <link>https://velog.io/@0_0_yoon/Oracle-Free-Tier-Review-%EA%B0%84%ED%97%90%EC%A0%81-%EB%A9%94%EB%AA%A8%EB%A6%AC-%EB%B6%80%ED%95%98</link>
            <guid>https://velog.io/@0_0_yoon/Oracle-Free-Tier-Review-%EA%B0%84%ED%97%90%EC%A0%81-%EB%A9%94%EB%AA%A8%EB%A6%AC-%EB%B6%80%ED%95%98</guid>
            <pubDate>Thu, 23 Jan 2025 14:58:38 GMT</pubDate>
            <description><![CDATA[<h2 id="문제상황">문제상황</h2>
<p>간헐적으로 백엔드 서버가 먹통이 됨</p>
<h2 id="원인">원인</h2>
<p>dnf가 메타데이터 캐시를 갱신하는 동안 메모리 사용량이 급격하게 증가해서 접속이 안되는 문제 발생</p>
<h2 id="해결과정">해결과정</h2>
<p>OCI 메모리 사용량 보드 및 시스템 로그를 통해 원인 유추.
갱신주기를 24시간으로 설정함
새벽 3시에 업데이트가 진행되도록 설정했다.</p>
<h4 id="고려사항">고려사항</h4>
<p>갱신주기의 기본값은 1시간이였다.
메타데이터 캐시 갱신 과정이 많은 메모리를 사용하지 않도록 하기 위해서는 dnf의 메타데이터 캐시 갱신 빈도를 줄여야했다.
이에 따라 최신 기능 및 보안 업데이트가 늦게 반영된다.
꼭 필요한 경우에 수동으로 업데이트를 진행해야한다.</p>
<h4 id="수정된-etcdnfdnfconf-파일-코드">수정된 /etc/dnf/dnf.conf 파일 코드</h4>
<p>[main]
metadata_timer_sync=86400  # 24시간 (초 단위)</p>
<p>sudo yum install -y libcgroup-tools
sudo cgcreate -g memory:dnfgroup
sudo cgset -r memory.limit_in_bytes=500M dnfgroup</p>
<p>crontab -e
0 3 * * * /usr/bin/cgexec -g memory:dnfgroup /usr/bin/dnf makecache --timer</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[이미지 리사이징]]></title>
            <link>https://velog.io/@0_0_yoon/%EC%9D%B4%EB%AF%B8%EC%A7%80-%EB%A6%AC%EC%82%AC%EC%9D%B4%EC%A7%95</link>
            <guid>https://velog.io/@0_0_yoon/%EC%9D%B4%EB%AF%B8%EC%A7%80-%EB%A6%AC%EC%82%AC%EC%9D%B4%EC%A7%95</guid>
            <pubDate>Thu, 16 May 2024 02:56:46 GMT</pubDate>
            <description><![CDATA[<h2 id="문제-상황">문제 상황</h2>
<p>이미지 업로드 기능에 용량 제한이 없으며 이를 그대로 s3 에 업로드 한다.</p>
<h3 id="예상되는-문제">예상되는 문제</h3>
<p>용량이 큰 이미지를 업로드 하면 응답 속도가 느려(클라이언트에서 직접 s3 에 업로드 하도록 구현되어 있다) 사용자 경험이 나빠지고 서버 관리 비용이 늘어난다.</p>
<p>또한 해당 이미지를 최초 조회할 경우에도 응답 속도가 느려 사용자 경험이 나빠질 것이다.</p>
<h2 id="해결">해결</h2>
<p>클라이언트에서 해당 이미지를 리사이징, 포맷 변경, 품질 설정을 한 뒤 s3 에 업로드 하도록 구현했다. 따라서 이전과 같이 사용자는 다양한 포맷과 용량의 이미지를 제한 없이 업로드 요청 할 수 있다.</p>
<h2 id="해결-과정">해결 과정</h2>
<p>tag 에서 제공하는 가장 큰 이미지는 170 x 170 픽셀 이다. 이와 비슷한 이미지 크기를 사용하는 유명 서비스의 이미지 용량은 아래와 같았다.</p>
<p>유튜브 채널 프로필 사진</p>
<p>크기: 160 x 160 픽셀</p>
<p>용량: 7kb ~ 18kb</p>
<p>인스타 프로필 사진</p>
<p>크기: 150 x 150 픽셀</p>
<p>용량: 5kb</p>
<p>그래서 이미지 리사이징 뿐만 아니라 가장 압축률이 뛰어난 WebP 로 포맷을 변경하고 사용자가 불편을 느끼지 않는 선에서 품질을 낮추기로 했다.</p>
<ol>
<li>이미지 크기 줄이기</li>
<li>WebP 로 포맷 변경하기</li>
<li>품질 낮추기(손실 압축)</li>
</ol>
<p>이미지 리사이징에 HTML5 Canvas API 를 사용했다. 사용자 이미지의 가로, 세로 길이 중 더 긴 길이를 170px 에 맞춰 리사이징 했다.</p>
<p>이를 Blob 으로 변환했고 이때 포맷 설정과 품질 조절을 했다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[리액트 양방향 데이터 흐름 구현]]></title>
            <link>https://velog.io/@0_0_yoon/%EB%A6%AC%EC%95%A1%ED%8A%B8-%EC%96%91%EB%B0%A9%ED%96%A5-%EB%8D%B0%EC%9D%B4%ED%84%B0-%ED%9D%90%EB%A6%84-%EA%B5%AC%ED%98%84</link>
            <guid>https://velog.io/@0_0_yoon/%EB%A6%AC%EC%95%A1%ED%8A%B8-%EC%96%91%EB%B0%A9%ED%96%A5-%EB%8D%B0%EC%9D%B4%ED%84%B0-%ED%9D%90%EB%A6%84-%EA%B5%AC%ED%98%84</guid>
            <pubDate>Wed, 15 May 2024 14:58:24 GMT</pubDate>
            <description><![CDATA[<h2 id="문제상황">문제상황</h2>
<p>부모 컴포넌트와 자식 컴포넌트 간의 상호작용이 필요한 경우.</p>
<p>하나의 input 요소를 사용해서 감사메세지와 댓글 및 대댓글을  작성할 수 있도록 구현하는 과정이였다. 이때 input 요소는 감사메세지 컴포넌트가 가지고 있다.</p>
<p>감사메세지(부모 컴포넌트)에서 댓글 및 대댓글(자식 컴포넌트)에게 데이터 전달하는 상황</p>
<p>부모 컴포넌트에서 작성된 댓글 및 대댓글의 응답이 성공이면 작성한 댓글 과 대댓글을 볼 수 있도록 자식 컴포넌트의 특정 함수 호출을 유발할 필요가 있었다.</p>
<p>댓글 및 대댓글(자식 컴포넌트)에서 감사메세지(부모 컴포넌트)에게 데이터 전달하는 상황</p>
<p>대댓글을 구현하기 위해 자식 컴포넌트의 답글 달기를 누르면 부모 컴포넌트의 input 요소의 값을 변경(<code>#${댓글작성자 emil}</code>)해줄 필요가 있었다.</p>
<h2 id="해결과정">해결과정</h2>
<p><a href="https://react.dev/learn/thinking-in-react">https://react.dev/learn/thinking-in-react</a></p>
<p>리액트에서는 명시적인 데이터 흐름을 유지하기 위해 양방향 데이터 흐름을 가진 경우에도 props 를 사용을 권장한다.</p>
<p>부모 컴포넌트에서 자식 컴포넌트 데이터 흐름 추가</p>
<p>자식 컴포넌트 Props 에 부모 컴포넌트의 상태값을 받을수 있도록 추가했다. 부모 컴포넌트에서 댓글 및 대댓글 작성 응답이 성공이면 상태를 변경시켜 자식 컴포넌트의 특정 함수를 호출을 유발하도록 구현했다.</p>
<p>역방향 데이터 흐름 추가</p>
<p>자식 컴포넌트 Props 에 콜백을 추가하여 답글 달기를 누르면 해당 콜백을 실행시켜 부모 컴포넌트의 상태를 변경하도록 구현했다.</p>
<h2 id="문제상황-1">문제상황</h2>
<p>accessToken(Context api 로 관리) 의 상태가 변경됐는데(새로고침 또는 주소창에 url 로 페이지를 직접 요청하여 accessToken 이 재발급 되는 상황) 하위 컴포넌트인 ProfileNavigation 이 의도한 내용대로 랜더링되지 않았다.</p>
<p>원인:  ProfileNavigation 에서 profileUrl 조회 api 함수를 마운트될때만 호출하도록 설정되어 있었기 때문에 속성 값이 변해도 profileUrl 조회 api 함수를 호출 하지 않았다.</p>
<h2 id="해결">해결</h2>
<ol>
<li>ProfileNavigation 내부에서 useEffect 의 의존배열에 accessToken 을 추가해서 accessToken 속성이 변경되면 profileUrl 조회 api 함수 호출하도록 구현한다.</li>
<li>accessToken 을 관리하는 context 에 isLoading(accessToken 재발급 함수 완료 여부를 나타내는) 상태 값을 추가하여 accessToken 재발급 함수가 완료된 후에 컴포넌트 마운트를 시킨다.</li>
</ol>
<p>1번의 경우 최초에 defaultImage 가 잠깐 나왔다가 회원의 프로필 사진으로 변경된다.</p>
<p>2번의 경우  accessToken 재발급 함수가 완료될때 까지 loading.. 문구를 보여주고 완료되면 컴포넌트를 마운트 시켜 defaultImage 또는 회원의 프로필 사진을 보여준다.</p>
<p>2번이 사용자 경험측면에서 더 자연스럽다고 판단하여 2번으로 구현했다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[@JsonFormat 과 TimeZone 설정]]></title>
            <link>https://velog.io/@0_0_yoon/JsonFormat-%EA%B3%BC-TimeZone-%EC%84%A4%EC%A0%95</link>
            <guid>https://velog.io/@0_0_yoon/JsonFormat-%EA%B3%BC-TimeZone-%EC%84%A4%EC%A0%95</guid>
            <pubDate>Tue, 13 Feb 2024 11:57:12 GMT</pubDate>
            <description><![CDATA[<p>문제상황: DB 에 저장된 시간대가 UTC 기준으로 저장되고 조회됨.</p>
<p>해결방법: application.yml 파일에 spring.jpa.properties.hibernate.jdbc.time_zone = <code>Asia/Seoul</code> 설정하면 된다.</p>
<ul>
<li>역할: 데이터베이스에 날짜와 시간을 저장하거나 조회할 때 해당 시간대를 기준으로 적용된다.</li>
</ul>
<p>또 다른 문제상황 : 서버를 클라우드 환경에 배포한뒤 시간을 조회할 경우 UTC 기준으로 조회됨</p>
<p>원인: JPA 가 DB 에서 조회해온 시간값을 LocalDateTime 으로 변환시키는 과정에서 LocalDateTime 의 기준 시간이 UTC 로 설정되어 있었기 때문이다.</p>
<p>해결방법: 도커 파일에 해당 명령어를 <em>`-*Duser.timezone=Asia</em>/*Seoul` 추가함</p>
<p>해결과정:</p>
<p>문제 상황시 BaseEntity의 코드</p>
<pre><code class="language-kotlin">@MappedSuperclass
@EntityListeners(AuditingEntityListener::class)
abstract class BaseEntity {
    @CreatedDate
    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = &quot;yyyy-MM-dd HH:mm:ss&quot;, timezone = &quot;Asia/Seoul&quot;)
    lateinit var createdAt: LocalDateTime

    @LastModifiedDate
    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = &quot;yyyy-MM-dd HH:mm:ss&quot;, timezone = &quot;Asia/Seoul&quot;)
    lateinit var modifiedAt: LocalDateTime
}
</code></pre>
<p>@JsonFormat 의 인자로 timezone = &quot;Asia/Seoul&quot; 설정을 해줬기 때문에 정상 동작을 예상했지만</p>
<p>@JsonFormat 의 경우  MessageConverter 에 의해 직렬화 및 역직렬화가 될 때 (Jackson 라이브러리가 사용될때) 사용되는 어노테이션이다. 즉 서버와 데이터베이스 사이에서 데이터를 주고 받을때는 사용되지 않는다.</p>
<p><img src="https://velog.velcdn.com/images/0_0_yoon/post/87685bc7-237c-4767-b4c1-f74739ebe185/image.png" alt=""></p>
<p>때문에 다른 방법을 찾았다.</p>
<ol>
<li>JVM 환경에 종속적인 LocalDateTime 대신 <strong>ZonedDateTime 사용하기</strong><ol>
<li>JPA 는 LocalDateTime 과는 다르게 <strong>ZonedDateTime 의 직렬화 및 역직렬화 지원을 하지 않기 때문에 직접 AttributeConverter 를 구현해야 한다.</strong></li>
</ol>
</li>
<li>jar 실행시 JVM의 시간대 설정 옵션 커맨드 추가(<strong>-Duser.timezone=Asia/Seoul</strong>)</li>
</ol>
<p>간편한 2 번 방법을 선택.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[레디스를 사용한 통합 테스트]]></title>
            <link>https://velog.io/@0_0_yoon/%EB%A0%88%EB%94%94%EC%8A%A4%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%9C-%ED%86%B5%ED%95%A9-%ED%85%8C%EC%8A%A4%ED%8A%B8</link>
            <guid>https://velog.io/@0_0_yoon/%EB%A0%88%EB%94%94%EC%8A%A4%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%9C-%ED%86%B5%ED%95%A9-%ED%85%8C%EC%8A%A4%ED%8A%B8</guid>
            <pubDate>Sun, 10 Dec 2023 10:54:22 GMT</pubDate>
            <description><![CDATA[<h2 id="문제상황">문제상황</h2>
<p>테스트 환경에서 임베디드 레디스를 사용해서 테스트를 진행.
예외 메세지: Unable to connect to Redis<br>org.springframework.data.redis.RedisConnectionFailureException: Unable to connect to Redis</p>
<pre><code>// build.gradle
testImplementation &#39;it.ozimov:embedded-redis:0.7.2&#39;</code></pre><pre><code class="language-java">// 테스트 코드
@SpringBootTest(webEnvironment = WebEnvironment.DEFINED_PORT)
public class OauthServiceTest {

    @Autowired
    private OauthService oauthService;

    @Test
    void 로그인_한다() {
        final LoginResponse loginResponse = oauthService.login(&quot;testCode&quot;);
        Assertions.assertThat(loginResponse).usingRecursiveComparison()
                .isNotNull();
    }
}

// ozimov 임베디드 레디스 사용
import redis.embedded.RedisServer;

@Configuration
public class EmbeddedRedisConfig {

    private RedisServer redisServer;

    @PostConstruct
    public void redisServer() {
        redisServer = new RedisServer();
    }

    @PreDestroy
    public void stopRedis() {
        if (redisServer != null) {
            redisServer.stop();
        }
    }
}</code></pre>
<h2 id="원인">원인</h2>
<p>ozimov 임베디드 레디스는 arm 아키텍처를 지원하지 않는다.(kstyrc 마찬가지로 지원하지 않는다)</p>
<pre><code class="language-java">package redis.embedded.util;

public enum Architecture {
    x86,
    x86_64
}</code></pre>
<h2 id="해결">해결</h2>
<h3 id="1-임베디드-레디스를-사용한다">1. 임베디드 레디스를 사용한다.</h3>
<ol>
<li>Apple Silicon 을 지원하는 레디스를 설치한다.</li>
<li>해당 레디스 바이너리 파일을 프로젝트에 복사한다.</li>
<li>2 번의 복사한 파일을 임베디드 레디스에 오버라이딩한다.</li>
</ol>
<p><a href="https://github.com/kstyrc/embedded-redis/issues/127#issuecomment-1049823658">자세한 방법</a>
장점: 최초 한 번만 레디스를 설치하면 그후에 사용되는 아키텍처(x86, arm)에 영향을 받지 않고 
레디스 테스트 환경을 구성할 수 있다.
단점: 임베디드 레디스 라이브러리(ozimov, kstyrc)는 이제 더 이상 서비스되지 않는다.</p>
<h3 id="2-개발환경에-레디스를-직접-설치한다">2. 개발환경에 레디스를 직접 설치한다.</h3>
<p>장점: 별도의 라이브러리에 대한 의존성이 없다.
단점: 개발환경에 레디스를 설치해야한다.</p>
<h3 id="3-개발환경에-도커를-설치후-테스트-컨테이너를-사용한다">3. 개발환경에 도커를 설치후 테스트 컨테이너를 사용한다.</h3>
<ol>
<li><p>개발환경에 도커를 설치한 후 실행한다.</p>
</li>
<li><p>아래 의존성을 추가한다.</p>
<pre><code>// build.gradle
testImplementation &#39;org.testcontainers:testcontainers:1.19.3&#39;
testImplementation &#39;org.testcontainers:junit-jupiter:1.19.3&#39;
testImplementation &#39;org.springframework.boot:spring-boot-testcontainers&#39;</code></pre></li>
<li><p>testcontainers 와 springframework 에서 지원하는 어노테이션을 적절히 사용한다.</p>
<pre><code class="language-java">@SpringBootTest(webEnvironment = WebEnvironment.DEFINED_PORT)
@Testcontainers // 컨테이너의 생명주기를 자동으로 관리해준다
public class OauthServiceTest {

 @Autowired
 private OauthService oauthService;

 @Container // 사용할 컨테이너를 선언한다
 @ServiceConnection // 컨테이너와의 연결정보를 자동으로 설정해준다, 스프링부트 3.1 이상 부터 지원한다
 static public GenericContainer redis = new GenericContainer(DockerImageName.parse(&quot;redis:5.0.3-alpine&quot;))
         .withExposedPorts(6379);

 @Test
 void 로그인_한다() {
     final LoginResponse loginResponse = oauthService.login(&quot;testCode&quot;);
     Assertions.assertThat(loginResponse).usingRecursiveComparison()
             .isNotNull();
 }
}</code></pre>
<p>장점: 확장성이 뛰어나다. 레디스 뿐만 아니라 데이터베이스 또한 별도의 설치없이 테스트 환경을 구축할 수 있기 때문에
단점: 테스트 시간이 비교적 오래걸린다. 로컬에 레디스를 설치해서 테스트 할 경우 평균 0.78 초, 테스트컨테이너 사용시 평균 1.19 초가 걸린다. 52.5% 로 꽤나 차이가 난다. 
개발환경에 도커를 설치해야한다.</p>
</li>
</ol>
<p>참고: 
<a href="https://java.testcontainers.org/quickstart/junit_5_quickstart/">https://java.testcontainers.org/quickstart/junit_5_quickstart/</a>
<a href="https://spring.io/blog/2023/06/23/improved-testcontainers-support-in-spring-boot-3-1">https://spring.io/blog/2023/06/23/improved-testcontainers-support-in-spring-boot-3-1</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Jackson 역직렬화]]></title>
            <link>https://velog.io/@0_0_yoon/Jackson-%EC%97%AD%EC%A7%81%EB%A0%AC%ED%99%94</link>
            <guid>https://velog.io/@0_0_yoon/Jackson-%EC%97%AD%EC%A7%81%EB%A0%AC%ED%99%94</guid>
            <pubDate>Wed, 15 Nov 2023 11:56:39 GMT</pubDate>
            <description><![CDATA[<h2 id="문제상황">문제상황</h2>
<p>인수테스트에서 응답을 역직렬화하는 과정에서 아래와 같은 예외가 발생했다.
2023-11-15 20:35:45.875  WARN 54421 --- [o-auto-1-exec-1] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.http.converter.HttpMessageNotWritableException: Could not write JSON: Cannot invoke &quot;java.lang.Long.longValue()&quot; because &quot;this.tableGroupId&quot; is null; nested exception is com.fasterxml.jackson.databind.JsonMappingException: Cannot invoke &quot;java.lang.Long.longValue()&quot; because &quot;this.tableGroupId&quot; is null (through reference chain: kitchenpos.dto.response.TableGroupResponse[&quot;orderTables&quot;]-&gt;java.util.ArrayList[0]-&gt;kitchenpos.dto.response.OrderTableResponse[&quot;tableGroupId&quot;])]</p>
<p>문제가 된 dto 의 코드.</p>
<pre><code class="language-java">public class OrderTableResponse {

    private long id;
    private Long tableGroupId;
    private int numberOfGuests;
    private boolean empty;

    // 생략

    public long getTableGroupId() {
        return this.tableGroupId;
    }
}</code></pre>
<h2 id="원인">원인</h2>
<p>Jackson 이 직렬화 및 역직렬화하는 과정에 대한 이해부족.
Jackson 은 클래스 정보를 조사해서(클래스 파일의 멤버 필드, getter setter 메서드, 생성자 등) 프로퍼티 정보를 저장한다. 그리고 이를 기반으로 직렬화 및 역직렬화 한다. 이때 접근제어자가 public 인 경우에만 프로퍼티 정보를 저장하도록 되어있다. 따라서 문제가 발생한 dto 경우 long 타입의 tableGroupId 이름을 가진 프로퍼티 정보가 저장됨을 예상할 수 있다. 여기에 null 값 할당을 시도했기 때문에 예외가 발생한 것이다.</p>
<h2 id="해결">해결</h2>
<p>별다른 코드를 추가하지 않고 해결하는 방법은 두 가지가 있다.
첫 번째 멤버 필드의 접근제어자를 public 으로 넓힌다.
두 번째 getter 메서드의 반환 타입을 Long 타입으로 수정한다.
멤버 필드의 외부 접근을 허용하면 부수 작용이 발생할 수 있으므로 두 번째 해결 방법을 선택했다.</p>
<pre><code class="language-java">public class OrderTableResponse {

    private long id;
    private Long tableGroupId;
    private int numberOfGuests;
    private boolean empty;

    // 생략

    public Long getTableGroupId() {
        return this.tableGroupId;
    }
}</code></pre>
<p>참고: <a href="https://velog.io/@0_0_yoon/Jackson-ObjectMapper-%EC%84%A4%EC%A0%95-%EB%B3%80%EA%B2%BD%ED%95%98%EA%B8%B0#%ED%95%B4%EA%B2%B0-%EA%B3%BC%EC%A0%95">https://velog.io/@0_0_yoon/Jackson-ObjectMapper-%EC%84%A4%EC%A0%95-%EB%B3%80%EA%B2%BD%ED%95%98%EA%B8%B0#%ED%95%B4%EA%B2%B0-%EA%B3%BC%EC%A0%95</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[JPA N + 1 문제 해결하기]]></title>
            <link>https://velog.io/@0_0_yoon/JPA-N-1-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@0_0_yoon/JPA-N-1-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 09 Oct 2023 23:58:05 GMT</pubDate>
            <description><![CDATA[<h2 id="문제상황">문제상황</h2>
<p>JPA 를 사용할 때 특정한 상황에서 추가적인 쿼리가 실행된다.
이때 DB ConnectionPool(Spring 의 경우 HikariCP 사용)은 한정 자원이므로 DB Connection 사용은 줄이는 게 좋다. </p>
<h2 id="원인">원인</h2>
<p>JPA 의 @JoinColumn 을 사용하여 연관관계 객체를 삽입할 때 추가적인 쿼리가 실행된다.(Fetch Type 과 상관없이 발생)
예를 들어 메뉴가 메뉴 상품과 함께 조회되어야 한다면 아래와 같이 연관 관계를 맺어준다.</p>
<pre><code class="language-java">@Entity
class Menu {
    // 생략
    @OneToMany
    @JoinColumn(name = &quot;menu_id&quot;)
    private List&lt;MenuProduct&gt; menuProducts;
}

@Entity
class MenuProduct {
    // 생략
}</code></pre>
<p>이때 메뉴 전체를 조회한 뒤(1) 각 메뉴의 메뉴 제품에 접근할 때 추가 쿼리가 실행된다(N).(메뉴 전체 조회 쿼리 한 번, 조회된 메뉴의 메뉴 제품을 삽입하기 위한 쿼리 N 번)</p>
<h3 id="datajpatest-주의사항">@DataJpaTest 주의사항</h3>
<p><img src="https://velog.velcdn.com/images/0_0_yoon/post/b3f0a0e6-bc6e-40c1-b0cd-d6f0afeee41f/image.png" alt="">
하나의 테스트 메서드 안에서 n+1 문제를 확인하려 할 때 테스트를 위한 세팅(given 절)을 마친 뒤에 영속성 컨텍스트를 비워줘야 한다. 그렇지 않으면 지연 로딩이 걸리지 않고 1차 캐시에 있는 객체들이 재사용되어 n+1 문제를 확인할 수 없다.
<img src="https://velog.velcdn.com/images/0_0_yoon/post/deed54a1-21a9-4051-989d-d4332d96adaf/image.png" alt="">
당연히 통합테스트, 인수테스트에서는 해당 사항 없음, 서로 다른 트랜잭션에서 실행되므로.</p>
<h2 id="해결">해결</h2>
<h3 id="join-fetch-사용">join fetch 사용</h3>
<pre><code class="language-java">@Query(&quot;SELECT DISTINCT m from Menu m join fetch m.memuProducts&quot;)
List&lt;Menu&gt; findAll();</code></pre>
<p>위와 같이 @Query 에 직접 JPQL 을 작성한다. 이때 join fetch 을 사용한다. JPQL 의 join fetch 를 사용하게 되면 join 의 대상이 되는 엔티티도 영속화된다.(일반 join 의 경우 당연히 영속화되지 않는다)</p>
<h4 id="중복제거">중복제거</h4>
<p>1 : N 관계에서 1 에 해당하는 엔티티를 조회할 때 주의할 점이 있다.
우리가 1 을 조회할 때 1 에 N 이 당연히 포함되는 관계지만 DB 에서는 inner join 해서 조회하기 때문에 N 의 개수만큼(1*N,곱집합) 튜플이 조회된다. 즉 메뉴를 1 개 조회하더라도 메뉴에 포함된 메뉴 제품이 3 개라면 메뉴가 3 개 조회된다.  이 때 DISTINCT 키워드를 사용해서 중복을 제거하면 해결할 수 있다.(DISTINCT 는 애플리케이션 레벨에서 적용된다)</p>
<h3 id="entitygraph-사용">@EntityGraph 사용</h3>
<pre><code class="language-java">@EntityGraph(attributePaths = &quot;menuProducts&quot;)
List&lt;Menu&gt; findAll();</code></pre>
<p>@EntityGraph 를 사용하면 join fetch 와 마찬가지로 필요 엔티티를 join 해서 조회한다.(join fetch 의 경우 inner join, @EntityGraph 의 경우 left outer join 이 실행된다)</p>
<h2 id="정리">정리</h2>
<p>연산에 필요한 쿼리와 연관관계를 맺어주기 위한 쿼리를 한 번에 실행함으로써 DBConnection 의 사용을 줄인다. 그러면 우리 서버는 더 많은 연산(DB 접근이 필요한 연산)을 처리할 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Virtual Thread (feat. Java 21)]]></title>
            <link>https://velog.io/@0_0_yoon/Virtual-Thread-feat.-Java-21</link>
            <guid>https://velog.io/@0_0_yoon/Virtual-Thread-feat.-Java-21</guid>
            <pubDate>Wed, 06 Sep 2023 04:31:42 GMT</pubDate>
            <description><![CDATA[<h2 id="서론">서론</h2>
<p>지난달 Final RC 가 끝난 Java 21 은 9월 19일에 출시된다. 새로운 기능 중 이번에 정식으로 제공되는 Virtual Thread 에 대해서 알아봤다.(19,20 에서 preview 기능으로 제공)</p>
<h2 id="본론">본론</h2>
<h3 id="virtual-thread-가-만들어진-배경">Virtual Thread 가 만들어진 배경</h3>
<h4 id="thread-per-request">Thread Per Request</h4>
<p>10개의 요청을 동시에 처리하여 초당 200개의 요청을 처리할 수 있는 애플리케이션이 있다고 가정해 보자. 사용자 수가 늘어 초당 2,000개의 요청을 처리해야 한다면 100개의 요청을 동시에 처리할 수 있도록 해야 한다. 그러면 서버는 스레드 수를 늘려 문제없이 요청을 처리할 수 있다. 나중에 사용자수가 더 늘어 초당 20,000개의 요청을 처리해야 하는 경우 마찬가지로 서버는 스레드 수를 늘려 문제없이 요청을 처리할 수 있을까? 아쉽게도 그럴 수 없다. 자바에서 생성할 수 있는(polling 할 수 있는) 스레드 수는 한계가 있다. 그 이유는 OS 스레드를 기반으로 1:1 대응되어 만들어져서 생성할 수 있는 스레드 수가 서버 하드웨어에 한정되기 때문이다.</p>
<h4 id="async">Async</h4>
<p>그렇다면 어떻게 처리량을 늘릴 수 있을까?
자바 애플리케이션은 기본적으로 하나의 요청을 하나의 스레드가 처리한다. 즉 요청을 처리하면서 네트워크 I/O, 파일 I/O 등과 같은 다른 I/O 작업이 발생하면 해당 스레드는 blocking 된다.(외부 작업이 끝날 때까지 자바 스레드는 대기하게 된다) 이런 대기 상태의 스레드를 다시 새로운 요청을 받을 수 있도록 하면 한정된 하드웨어를 최대한 활용해서 처리량을 높일 수 있다. 하지만 이 방법에는 문제점이 있다. </p>
<ol>
<li>스레드의 Context switching 이 자주 발생</li>
</ol>
<ul>
<li>다른 I/O 작업이 발생하면 해당 스레드는 poll 로 반환되고 다른 스레드가 다음 요청을 처리한다. 즉 Thread Per Request 보다 빈번하게 Context switching 되어 오버헤드가 발생한다.</li>
</ul>
<ol start="2">
<li>디버깅이 어려움(프로그램 동작을 이해하기 어렵다)</li>
</ol>
<ul>
<li>Thread Per Request 의 경우 예외가 발생하면 요청을 처리한 스레드 1개만 확인하면 됐지만 비동기로 동작하면 1개의 요청을 N 개의 스레드가 처리하게 되므로 N 개의 스레드를 일일이 확인해야 한다.<h4 id="virtual-thread">Virtual Thread</h4>
애플리케이션의 처리량을 늘리는 동시에 위와 같은 문제점을 해결하고자 JDK 개발자들은 가상 스레드를 만들었다. 가상 스레드는 플랫폼 스레드(기존의 자바 스레드)와 다르게 OS 스레드와 연결되어 있지 않다. 즉 가상 스레드는 일반적인 인스턴스이기 때문에 생성 비용이 저렴하다.<br>때문에 요청을 처리하는 도중 다른 I/O 작업이 발생하면 해당 가상 스레드는 blocking 되고 새로운 가상 스레드 객체를 만들어 다음 요청을 처리한다. 
그리고 요청을 처리하는 도중 CPU 에서 계산을 수행하는 동안에만 플랫폼 스레드를 사용한다. 즉 플랫폼 스레드 측에서 봤을 때 1개의 플랫폼 스레드를 N 개의 가상 스레드가 사용한다.(둘을 같이 놓고 보면 많은 수(M)의 가상 스레드가 더 적은 수(N)의 OS 스레드에서 실행된다)</li>
</ul>
<ol>
<li>스레드의 Context switching 이 자주 발생하는 문제 해결</li>
</ol>
<ul>
<li>다른 I/O 작업이 발생하면 새로운 가상 스레드를 만들어 요청을 처리한다.
가상 스레드는 일반적인 인스턴스이기 때문에 Context switching 이 발생하지 않는다. 플랫폼 스레드의 경우 OS 의 스케줄러에 의존하는 반면, 가상 스레드의 경우 JDK 의 자체 스케줄러에 의존한다. </li>
</ul>
<ol start="2">
<li>디버깅이 어려운 문제 해결 -&gt; 1개의 가상 스레드가 1개의 요청을 처리한다.</li>
</ol>
<ul>
<li>다른 I/O 작업이 발생하면 해당 가상 스레드는 blocking 됐다가 작업이 완료되면 해당 가상 스레드가 이어서 요청을 처리한다.</li>
</ul>
<h3 id="결론">결론</h3>
<p>비유하자면 물리주소는 메모리 크기에 한정되지만 CPU 가 사용하는 논리주소는 메모리 크기에 한정되지 않는 것처럼(메모리 + 스왑영역) OS 스레드는 하드웨어(메모리 + 스왑영역)에 의해 한정되지만, 자바 런타임의 가상 스레드는 OS 스레드에 한정되지 않는다.
Virtual Thread 를 사용하면 OS 스레드를 blocking 없이 효율적으로 활용해서 애플리케이션의 처리량을 늘릴 수 있으며, Virtual Thread 는 요청과 1대1로 대응되기 때문에 프로그램 흐름을 쉽게 파악할 수 있다는 이점이 있다.</p>
<p>참고 <a href="https://openjdk.org/jeps/444">https://openjdk.org/jeps/444</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[REST API]]></title>
            <link>https://velog.io/@0_0_yoon/REST-API</link>
            <guid>https://velog.io/@0_0_yoon/REST-API</guid>
            <pubDate>Tue, 25 Jul 2023 01:25:24 GMT</pubDate>
            <description><![CDATA[<h1 id="서론">서론</h1>
<p>REST API 는 HTTP 메서드로 행위를 표현하고 자원을 URI 로 표현해서 만든 API 라고 알고 있었다. 하지만 여기에 몇 가지 제약사항이 더 있다고 들었던 적이 있다. 그래서 이번에 과연 REST 는 무엇이며, 왜 만들어졌는지, REST API 를 만들기 위한 제약사항은 무엇인지, 제약사항을 지켜서 만든 REST API 는 어떤 이점이 있는지 알아보기 위해 REST 를 정의한 <a href="https://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm">로이 필딩의 논문</a>, <a href="https://ko.wikipedia.org/wiki/REST">위키백과</a> 를 보며 정리해 봤다.</p>
<h1 id="본론">본론</h1>
<h2 id="rest-란">REST 란?</h2>
<p><a href="https://ko.wikipedia.org/wiki/REST">웹을 위한 아키텍처이다.</a> 왜 아키텍처에 REST 라는 이름을 붙였을까?
REST 는 대표 상태 이동 즉 웹 페이지에서 링크를 통한 다른 웹 페이지로의 이동이다. 로이 필딩은 이와 같은 웹 애플리케이션의 동작 방식을 연상시키기 위해서 REST 라는 이름을 붙였다고 한다.</p>
<blockquote>
<p> &quot;Representational State Transfer&quot;라는 이름은 잘 설계된 웹 애플리케이션의 작동 방식, 즉 사용자가 링크(상태 전환)를 선택하여 애플리케이션을 진행하는 웹 페이지 네트워크(가상 상태 머신)의 이미지를 연상시키기 위한 것입니다. 
-로이 필딩</p>
</blockquote>
<h2 id="rest-의-탄생-배경">REST 의 탄생 배경</h2>
<p>웹 사용량이 늘어나면서 기존 아키텍처의 문제가 드러났고 이를 해결하고 기존 아키텍처와의 호환성을 유지하기 위해서 웹 프로토콜 표준 아키텍처(REST) 가 만들어졌다.
예상과 달리 웹 사용량이 급속도로 증가하면서 인터넷 인프라의 용량을 빠르게 초과했다.
인터넷 엔지니어링 태스크포스는 이미 널리 배포된 아키텍처에 새로운 기능을 도입하고 기존 아키텍처와의 호환성을 유지하기 위해 기존 아키텍처의 문제(확장성, 공유캐싱 및 중개자 지원의 제한)를 식별하고 해당 문제를 해결하기 위한 일련의 표준을 정했다.</p>
<h2 id="rest-의-통일된-인터페이스">REST 의 통일된 인터페이스</h2>
<p>인터페이스를 만듦으로써 전체 시스템 아키텍처가 단순화되고 상호 작용의 가시성이 향상된다. 서버와 클라이언트가 독립적으로 발전할 수 있다. 예를 들어 크롬을 업데이트시켜도 구글에 접속할 수 있다. 구글이 웹페이지를 변경했다고 크롬을 업데이트할 필요가 없다. 그러나 단점은 정보가 특정 응용 프로그램의 요구 사항에 맞는 형식이 아닌 표준화된 형식으로 전송되기 때문에 균일한 인터페이스가 효율성을 저하한다.</p>
<h3 id="제약사항">제약사항</h3>
<h4 id="자원의-식별">자원의 식별</h4>
<p>초기 웹 아키텍처에서는 URI 에서 Resource 를 문서 식별자(네트워크상의 문서 위치에 따른 식별자)로 정의했다. 하지만 이 경우 해당 문서가 수정될 때마다 식별자가 변경된다는 문제점, 서버가 문서만을 제공하는 게 아니기(Get 이외의 요청에 대한 응답, JSON 형식의 응답) 때문에 개념을 식별하도록 정의했다.</p>
<blockquote>
<p>REST의 리소스 정의는 식별자가 가능한 한 자주 변경되지 않아야 한다는 간단한 전제를 기반으로 합니다. 
-로이 필딩</p>
</blockquote>
<p><strong>즉 서버 개발자는 (변경이 발생하지 않을)개념을 식별하도록 URI 를 구성해야 한다.</strong></p>
<h4 id="표현을-통한-리소스-조작">표현을 통한 리소스 조작</h4>
<p>어떻게 사용자가 하이퍼텍스트 링크를 통해 자원에 접근, 조작, 전송할 수 있을까? REST 는 식별된 리소스를 표현하도록 항목을 정의해서 해결한다. 
즉 사용자가 리소스를 조작할 때 리소스 식별자에 의해 정의된 인터페이스를 통해 표현을 전송하여 조작된다.
여기서 표현이란 바이트 시퀀스와 해당 바이트를 설명하는 메타데이터로 구성된다.</p>
<p><strong>즉 서버 개발자는 클라이언트의 요청을 올바르게 처리하기 위해서 인터페이스에 맞도록(올바른 HTTP 메서드, Content Type 등의 메타데이터) API 를 구성해야 한다.</strong></p>
<h4 id="자기-설명-메시지">자기 설명 메시지</h4>
<p>요청 내 호스트 식별 부족, 메세지 제어 데이터와 표현 메타데이터 간의 구문적 구별 실패 등 초기 HTTP 는 자체 설명적이지 못했다. REST 에서는 이 문제점들을 해결하고자 HTTP 메시지는 자신을 어떻게 처리해야 하는지에 대한 충분한 정보를 포함하도록 했다.</p>
<p>** 즉 서버 개발자는 JSON 형식의 응답을 하는 경우에도 충분한 정보를 포함하도록 해야한다.(JSON 의 키값을 정의한 문서를 응답에 포함)**</p>
<h4 id="hateoashypermedia-as-the-engine-of-application-state">HATEOAS(Hypermedia as the engine of application state)</h4>
<p>사용자는 애플리케이션이나 서버와 상호 작용하는 방법에 대한 사전 지식이 필요하지 않다. 그저 하이퍼링크를 통해 관련된 자원에 접근한다.
<strong>즉 서버 개발자는 JSON 형식의 응답을 하는 경우에도 관련된 자원에 접근할 수 있는 링크를 포함해야 한다.</strong></p>
<h1 id="결론">결론</h1>
<p>웹 사용자가 빠르게 늘어나면서 발생된 인프라 자원의 부족, 기존 아키텍쳐의 문제점을 해결하고 기존 아키텍쳐와의 호환성을 지키기 위해서 새로운 웹 표준 아키텍쳐를 만들었는데 이것이 바로 REST 다.
결과적으로 웹은 REST 로 인해 독립성과 확장성, 대기 시간과 네트워크 통신을 최소화했다.
웹에서 사용할 API 를 REST 의 인터페이스 조건에 맞게 만들면 REST 의 이점을 누릴 수 있다.(클라이언트와 서버가 독립적인 발전을 할 수 있다)</p>
<blockquote>
<p>REST는 구성 요소 구현의 독립성과 확장성을 최대화하는 동시에 대기 시간과 네트워크 통신을 최소화하려고 시도하는 조정된 아키텍처 제약 세트입니다. 
-로이 필딩</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Jackson] ObjectMapper 설정 변경하기]]></title>
            <link>https://velog.io/@0_0_yoon/Jackson-ObjectMapper-%EC%84%A4%EC%A0%95-%EB%B3%80%EA%B2%BD%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@0_0_yoon/Jackson-ObjectMapper-%EC%84%A4%EC%A0%95-%EB%B3%80%EA%B2%BD%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sat, 01 Apr 2023 07:30:04 GMT</pubDate>
            <description><![CDATA[<h3 id="문제상황">문제상황</h3>
<p>응답 객체의 필드 이름을 isSelected(boolean 타입, private, Lombok @Getter 사용)로 지었는데 해당 객체를 사용하는 API 응답 값에서 isSelected 가 아닌 selected 로 출력되는 문제가 있었다.
<img src="https://velog.velcdn.com/images/0_0_yoon/post/03c549f9-03f6-4766-bb67-2dc2fe0ae3fd/image.png" alt=""></p>
<h3 id="원인">원인</h3>
<p>Jackson(POJOPropertiesCollector) 은 클래스 정보를 조사해서(클래스 파일의 멤버 필드, getter setter 메서드, 생성자 등) 프로퍼티 정보를 저장한다. 그리고 이를 기반으로 직렬화 및 역직렬화 한다. 멤버 필드와 getter 메서드는 접근제어자가 public 인 경우에만 프로터피 정보를 저장하도록 되어있다.(조금 더 자세한 내용은 해결 과정에서 후술) 그러므로 문제 상황에서 멤버 필드의 isSelected 는 프로퍼티로 저장되지 않고 getter 메서드인 isSelected 가 프로퍼티로 저장된다. 이때 getter 메서드의 경우 prefix 를 제거하고 저장하기 때문에 selected 로 출력된 것이다.</p>
<h3 id="해결">해결</h3>
<p>해결법에 대한 아이디어는 공식 문서에서 얻을 수 있었다.</p>
<blockquote>
<p>기본 ObjectMapper를 완전히 대체하려면 해당 유형의 @Bean을 정의하고 @Primary로 표시하거나 빌더 기반 접근 방식을 선호하는 경우 Jackson2ObjectMapperBuilder @Bean을 정의하십시오. 두 경우 모두 이렇게 하면 ObjectMapper의 모든 자동 구성이 비활성화됩니다.
<a href="https://docs.spring.io/spring-boot/docs/current/reference/html/howto.html#howto.spring-mvc.customize-jackson-objectmapper">링크</a></p>
</blockquote>
<p>isSelected 를 출력하기 위해서 두 가지를 설정이 필요하다.
첫 번째 멤버 필드의 접근제어자가 private 인 경우에도 프로퍼티로 저장되도록 해야 할 것, 
두 번째 isGetter 메서드의 경우 프로퍼티로 저장되지 않도록 해야 할 것</p>
<pre><code class="language-java">@Configuration
public class ObjectMapperConfig {

    @Bean
    @Primary
    public ObjectMapper customizeObjectMapper() {
        return new ObjectMapper().setVisibility(
                VisibilityChecker.Std
                        .defaultInstance()

                        // public 만 허용하는 설정에서 private 도 허용하도록 설정
                        .withFieldVisibility(Visibility.ANY)
        ).setAccessorNaming(
                new DefaultAccessorNamingStrategy
                        .Provider()

                        // isGetter 메서드는 조사하지 않도록 설정
                        // null 을 넣은 이유는 해결과정 2 번의 findNameForIsGetter 메서드 참고
                        .withIsGetterPrefix(null)
        );
    }</code></pre>
<p><img src="https://velog.velcdn.com/images/0_0_yoon/post/92232f85-4dfd-4cc4-aab8-98b807f9a56f/image.png" alt=""></p>
<h3 id="해결-과정">해결 과정</h3>
<p>원인을 파악하기 위해 디버거로 Jackson 라이브러리의 직렬화 과정을 살펴봤고 코드의 흐름을 파악한 뒤 변경해야 할 설정 지점을 찾아냈다.</p>
<p>Jackson 라이브러리에서 프로퍼티 정보를 수집하는 역할은 POJOPropertiesCollector(com.fasterxml.jackson.databind.introspect)가 한다.(collectAll 메서드를 호출해서 수집한다)</p>
<ol>
<li>POJOPropertiesCollector 동작 과정을 살펴보면 필드, 메서드 순서로 프로퍼티 정보를 수집한다. 존재하는 필드, getter, setter 메서드의 이름들을 조사해서 일단 프로퍼티 후보군에(props) 저장한다.(이름이 중복인 경우를 제외하고 일단 다 저장한다, 2번 코드 POJOPropertiesCollector 의 _property 메서드 참고) 그 뒤에 후보 선별 과정을 거친다.</li>
</ol>
<pre><code class="language-java">public class POJOPropertiesCollector {

    // 생략

    protected void collectAll() {
            LinkedHashMap&lt;String, POJOPropertyBuilder&gt; props = new LinkedHashMap&lt;String, POJOPropertyBuilder&gt;();

          // 후보 등록
            _addFields(props);
            _addMethods(props);

            // 생략

          // 선별 과정
          _removeUnwantedProperties(props);
          _removeUnwantedAccessor(props);

          // 생략

    }

       // 생략 

    protected void _addMethods(Map&lt;String, POJOPropertyBuilder&gt; props) {
        for (AnnotatedMethod m : _classDef.memberMethods()) {
            int argCount = m.getParameterCount();
            if (argCount == 0) { 
                _addGetterMethod(props, m, _annotationIntrospector);
            } else if (argCount == 1) { 
                _addSetterMethod(props, m, _annotationIntrospector);
            } else if (argCount == 2) { 
                if (Boolean.TRUE.equals(_annotationIntrospector.hasAnySetter(m))) {
                    if (_anySetters == null) {
                        _anySetters = new LinkedList&lt;AnnotatedMethod&gt;();
                    }
                    _anySetters.add(m);
                }
            }
        }
    }
}</code></pre>
<ol start="2">
<li>문제상황을 기준으로 _addGetterMethod 메서드를 살펴봤다.
getter 메서드를 조사할 때 두 가지를 조사하는데 여기서 AccessorNamingStrategy 를 상속받은 DefaultAccessorNamingStrategy 가 사용된다.(해당하는 변수 이름은 _accessorNaming, 아래 코드 참고)
첫 번째 DefaultAccessorNamingStrategy 의 findNameForRegularGetter 메서드를 호출해서 메서드 이름에 get 이 포함되어 있다면 메서드 이름에서 get 을 제외한 문자열을 반환한다.
두 번째 같은 객체의 findNameForIsGetter 메서드를 호출해서 메서드 이름에 is 가 포함되어 있다면 메서드 이름에서 is 를 제외한 문자열을 반환한다.
문제 상황의 경우 boolean 타입, Lombok 의 @Getter 를 사용했으므로 getter 메서드 이름은 isSelected 이다. 따라서 selected 문자열이 반환됨을 예상할 수 있다. 반환된 문자열은 implName 라는 지역 변수에 저장되고 POJOPropertiesCollector 의 _property 메서드를 호출해서 implName 값을 프로퍼티 후보 이름으로 저장한다.</li>
</ol>
<pre><code class="language-java">public class POJOPropertiesCollector {

    // 생략

    protected void _addGetterMethod(Map&lt;String, POJOPropertyBuilder&gt; props,
            AnnotatedMethod m, AnnotationIntrospector ai) {

        // 생략

        if (!nameExplicit) {
            implName = ai.findImplicitPropertyName(m);
            if (implName == null) {
                implName = _accessorNaming.findNameForRegularGetter(m, m.getName());
            }
            if (implName == null) {
                implName = _accessorNaming.findNameForIsGetter(m, m.getName());
                if (implName == null) {
                    return;
                }
                visible = _visibilityChecker.isIsGetterVisible(m);
            } else {
                visible = _visibilityChecker.isGetterVisible(m);
            }
        } 

        // 생략

        _property(props, implName).addGetter(m, pn, nameExplicit, visible, ignore);
    }

    protected POJOPropertyBuilder _property(Map&lt;String, POJOPropertyBuilder&gt; props,
            String implName) {
        POJOPropertyBuilder prop = props.get(implName);
        if (prop == null) {
            prop = new POJOPropertyBuilder(_config, _annotationIntrospector, _forSerialization,
                    PropertyName.construct(implName));
            props.put(implName, prop);
        }
        return prop;
    }
}


public class DefaultAccessorNamingStrategy extends AccessorNamingStrategy {

    // 생략

    @Override
    public String findNameForRegularGetter(AnnotatedMethod am, String name) {
        if ((_getterPrefix != null) &amp;&amp; name.startsWith(_getterPrefix)) {
            if (&quot;getCallbacks&quot;.equals(name)) {
                if (_isCglibGetCallbacks(am)) {
                    return null;
                }
            } else if (&quot;getMetaClass&quot;.equals(name)) {
                if (_isGroovyMetaClassGetter(am)) {
                    return null;
                }
            }
            return _stdBeanNaming
                    ? stdManglePropertyName(name, _getterPrefix.length())
                    : legacyManglePropertyName(name, _getterPrefix.length());
        }
        return null;
    }

    @Override
    public String findNameForIsGetter(AnnotatedMethod am, String name) {
        if (_isGetterPrefix != null) {
            final Class&lt;?&gt; rt = am.getRawType();
            if (rt == Boolean.class || rt == Boolean.TYPE) {
                if (name.startsWith(_isGetterPrefix)) {
                    return _stdBeanNaming
                            ? stdManglePropertyName(name, 2)
                            : legacyManglePropertyName(name, 2);
                }
            }
        }
        return null;
    }
}</code></pre>
<ol start="3">
<li><p>2번에서 프로퍼티 후보 이름을 저장할 때 그 후보에 관한 여러 가지 정보들을 같이 저장한다.(실제로 후보 정보는 POJOPropertyBuilder 에 저장된다) 그중에 visible 이라는 변수를 눈여겨보자.(2 번의 _addGetterMethod 메소드 참고) </p>
<pre><code class="language-java">public class POJOPropertyBuilder extends BeanPropertyDefinition implements Comparable&lt;POJOPropertyBuilder&gt; {

 protected final PropertyName _name;

 // 사용되지 않는 후보 정보는 null 값이 들어간다.
 protected Linked&lt;AnnotatedField&gt; _fields;
 protected Linked&lt;AnnotatedMethod&gt; _getters;

 // 생략

 public void addField(AnnotatedField a, PropertyName name, boolean explName, boolean visible, boolean ignored) {
     _fields = new Linked&lt;AnnotatedField&gt;(a, _fields, name, explName, visible, ignored);
 }

 public void addGetter(AnnotatedMethod a, PropertyName name, boolean explName, boolean visible, boolean ignored) {
     _getters = new Linked&lt;AnnotatedMethod&gt;(a, _getters, name, explName, visible, ignored);
 }

 //생략
</code></pre>
</li>
</ol>
<p>}</p>
<pre><code>  - visible 의 경우 POJOPropertiesCollector 가 필드와 메서드를 조사할 때 값이 할당된다. VisibilityChecker(POJOPropertiesCollector 가 멤버 필드로 가지고 있다) 가 **미리 저장돼있는 값을 기준**(아래의 사진 참고)으로 프로터피 후보들을 판별한 뒤 결과를 visible 지역변수에 저장한다.(2번 코드의 _addGetterMethod 메소드 참고) 아래의 두 번째 사진을 보면 4개의 프로퍼티 후보가 저장돼있는데 이 중에 isSelectd  이름의 후보는 visible 값이 false 임을 확인할 수 있다.(멤버 필드인 isSelected 의 접근 제어자가 private 이고 설정된 값은 public 만 허용하고 있으므로 false 다)
  ![](https://velog.velcdn.com/images/0_0_yoon/post/ebd63156-08fb-4a23-b245-b39e299003a0/image.png)
![](https://velog.velcdn.com/images/0_0_yoon/post/5e9d4acf-ca5d-42ea-ad18-eb61eeb69575/image.png)




 4. 후보 등록을 마치고 POJOPropertiesCollector 의 _removeUnwantedProperties 메서드를 호출한다.(1 번 코드의 collectAll() 참고) 후보들이 가지고 있는 visible 값들을 확인해서 true 값이 없을 때 후보군에서 제거한다.

```java
protected void _removeUnwantedProperties(Map&lt;String, POJOPropertyBuilder&gt; props) {
        Iterator&lt;POJOPropertyBuilder&gt; it = props.values().iterator();
        while (it.hasNext()) {
            POJOPropertyBuilder prop = it.next();

            if (!prop.anyVisible()) {
                it.remove();
                continue;
            }

            // 생략
         }
}</code></pre><ol start="5">
<li>위와 같은 과정으로 내가 의도했던 isSelected 는 POJOPropertiesCollector 에 의해 삭제되고(_removeUnwantedProperties 메서드로 삭제된다) selected 가 응답 값으로 나오게 됐다.</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[ContentCachingRequestWrapper, ContentCachingResponseWrapper]]></title>
            <link>https://velog.io/@0_0_yoon/ContentCachingRequestWrapper-ContentCachingResponseWrapper-wxndicpk</link>
            <guid>https://velog.io/@0_0_yoon/ContentCachingRequestWrapper-ContentCachingResponseWrapper-wxndicpk</guid>
            <pubDate>Thu, 17 Nov 2022 06:37:51 GMT</pubDate>
            <description><![CDATA[<h3 id="문제상황">문제상황</h3>
<p>인터셉터에서 로깅 할 때 Requset, Response 의 Body 가 출력되지 않았다.</p>
<pre><code class="language-java">    @Override
    public boolean preHandle(final HttpServletRequest request, final HttpServletResponse response, final Object handler) {
        apiTimer.start();

        final String body = new String(new ContentCachingRequestWrapper(request).getContentAsByteArray());
        log.info(REQUEST_LOG_FORMAT, request.getMethod(), request.getRequestURI(), request.getHeader(&quot;Authorization&quot;),
                body);
        return true;
       }

    @Override
    public void afterCompletion(final HttpServletRequest request, final HttpServletResponse response, final Object handler, final Exception ex) {
        apiTimer.stop();

        final ContentCachingResponseWrapper contentCachingResponseWrapper = new ContentCachingResponseWrapper(response);
        final String responseBody = new String(contentCachingResponseWrapper.getContentAsByteArray());
        final int queryCount = apiQueryCounter.getCount();
        log.info(RESPONSE_LOG_FORMAT, response.getStatus(), request.getMethod(), request.getRequestURI(),
                queryCount, apiTimer.getLastTaskTimeMillis(), responseBody);
    }</code></pre>
<h3 id="원인">원인</h3>
<p>먼저 ContentCachingRequestWrapper, ContentCachingResponseWrapper 를 사용한 이유는 요청, 응답의 본문 내용(InputStream, OutputStream)을 읽고 쓰게 되면 그 내용들을 다시 읽어올 수 없기 때문이다.</p>
<p><strong>문제 원인의 첫 번째는 Request Body 가 캐싱 되기 전에 사용한 점이다.</strong> ContentCachingRequestWrapper 는 Request Body 가 읽혔을 때 캐싱 된다. 문제상황 코드를 기준으로 캐싱 되는 시점은 인터셉터를 거친 후 ArgumentResolver 의 MessegeConverter(MappingJackson2HttpMessageConverter) 에서 Request Body 가 읽혔을 때이다.</p>
<blockquote>
<p>이 클래스는 콘텐츠를 읽을 때만 캐시하는 인터셉터 역할을 하지만 그렇지 않으면 콘텐츠를 읽지 않습니다. 즉, 요청 콘텐츠가 사용되지 않으면 콘텐츠가 캐시되지 않으며 getContentAsByteArray() 를 통해 검색할 수 없습니다.
ContentCachingRequestWrapper API 문서</p>
</blockquote>
<p>왜 생성할 때 캐싱 되도록 구현하지 않고 위와 같이 구현한 걸까?
Spring 은 요청을 처리하는 계약된 절차를 가지고 있는데 캐싱 된 내용을 가져오는 기능은 매우 구체적이며(구체 클래스의 메서드) Spring 과 계약되지 않은 부분이기 때문이라고 생각한다.</p>
<p><strong>두 번째 원인은 이미 응답한 뒤에 ContentCachingResponseWrapper 를 사용한 점이다.</strong> 
쥐도 새도 모르게 언제 응답 된 걸까?
ReturnValueHandler 의 MessageConverter(MappingJackson2HttpMessageConverter) writeInternal 메서드가 호출될 때 응답된다.</p>
<h3 id="해결">해결</h3>
<p>첫 번째 원인을 해결하기 위해서는 ArgumentResolver 의 MessegeConverter 에 실제 요청이 아닌 ContentCachingRequestWrapper 를 넘겨줘야 한다. DispatcherServlet 이 전달 받는 요청 객체를 ContentCachingRequestWrapper 로 바꿔치기해야 하는데 인터셉터에서는 할 수 없다.(인터셉터에서는 단지 인자로 넘겨받기만 한다)
반면 필터는 필터 체인에 의해서 순서대로 동작하며 이때 호출하는 곳에서 요청, 응답 객체를 넘겨받기 때문에 다음에 사용될 요청, 응답 객체를 교체할 수 있다.
두 번째 원인 또한 응답 객체도 필터에서 ContentCachingResponseWrapper 로 교체함으로써 해결할 수 있다.</p>
<pre><code class="language-java">@Slf4j
@Component
public class LoggingFilter extends OncePerRequestFilter {

   @Override
   protected void doFilterInternal(final HttpServletRequest request, final HttpServletResponse response, final FilterChain filterChain)
            throws ServletException, IOException {
        final ContentCachingRequestWrapper cachingRequest = new ContentCachingRequestWrapper(request);
        final ContentCachingResponseWrapper cachingResponse = new ContentCachingResponseWrapper(response);
        apiTimer.start();
        filterChain.doFilter(cachingRequest, cachingResponse);
        apiTimer.stop();
        logRequestAndResponse(cachingRequest, cachingResponse);
        cachingResponse.copyBodyToResponse();
   }
        // 생략
}</code></pre>
<p>교체된 ContentCachingResponseWrapper 는 Inner Class 로 ServletOutputStream 를 구현한 ResponseServletOutputStream 을 가지고 있다. 이 ResponseServletOutputStream 이 ReturnValueHandler 의 MessageConverter(MappingJackson2HttpMessageConverter) 에서 응답 본문을 캐싱하도록 동작한다.(원래라면 이때 클라이언트로 응답이 전송된다) 그래서 copyBodyToResponse 메서드를 호출해서 캐싱 된 본문(직렬화된 상태)을 실제 클라이언트에게 응답한다.</p>
<blockquote>
<p>캐시된 본문 콘텐츠를 응답에 복사합니다.
ContentCachingResponseWrapper 의 copyBodyToResponse 메서드 </p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[RefreshToken(feat. RTR)]]></title>
            <link>https://velog.io/@0_0_yoon/RefreshTokenfeat.-RTR</link>
            <guid>https://velog.io/@0_0_yoon/RefreshTokenfeat.-RTR</guid>
            <pubDate>Tue, 15 Nov 2022 01:43:44 GMT</pubDate>
            <description><![CDATA[<h3 id="문제상황">문제상황</h3>
<p>F12 프로젝트에서 인가를 JWT 기반으로 구현했다.
백엔드에서 1 시간 유효기간을 가진 JWT 기반 토큰을 발급해 주면 프론트엔드는 이를 세션 스토리지에 저장해서 사용했다.
이때 문제점은 두 가지였다.
첫 번째 사용자가 1 시간마다 로그인을 다시 해야 하는 점,
두 번째 보안 문제, 세션 스토리지는 자바스크립트로 읽어 올 수 있다. 즉 해당 토큰이 제3의 악성 사용자에 의해 탈취(XSS)당할 위험이 있다.
세 번째 탈취 사실을 알게 되어도 서버에서 별다른 조치를 할 수 없다.</p>
<h3 id="원인">원인</h3>
<p>첫 번째 추가적인 조치 없이 JWT 기반 토큰 자체만으로 인가를 구현한 점,
두 번째 토큰을 세션 스토리지에 저장한 점</p>
<h3 id="해결">해결</h3>
<p>AccessToken(실제 인가 필요 API 에 사용되는 토큰), RefreshToken(AccessToken 재발급에 사용되는 토큰), RefreshToken Rotation(RefreshToken 한번 사용 후 교체) 을 사용해서 더 안전하고 효율적인 통신을 할 수 있도록 구현했다.<br>먼저 기존의 JWT 기반 토큰은 AccessToken 으로 삼아 유효기간 1 시간 동일하게 설정해서 JWT 의 장점을 그대로 가져갔다. 프론트엔드와 협의해서 세션 스토리지가 아닌 자바스크립트의 변수로 저장하도록 했다. 이유는 아래와 같다. AccessToken 이 실제 인가와 관련된 모든 API 에 사용되기 때문에 안전한 곳에 저장되어야 한다. 로컬, 세션 스토리지는 XSS 공격에, 쿠키의 경우 CSRF 공격에 노출되기 쉽다. 하지만 토큰을 내부 변수로 저장함으로써 악성 스크립트(프론트엔드 코드를 보지않고는 토큰을 어디에 저장하는지 알 수 없다)자체를 만들기 어려우며(XSS 예방), 악성 사이트에 의한 불미스러운 요청을 막을 수 있다.(CSRF 예방, 다만 애초에 백엔드에서 계약된 프론트엔드의 Origin 만 허용하도록 설정했기 때문에 악성 사이트의 요청을 막는다)
RefreshToken 은 UUID 로 구현했다. 기존 JWT 기반 토큰의 단점인 서버에서 토큰을 컨트롤할 수 없다는 점을 해결하려고 했기 때문에 굳이 JWT 를 사용할 필요가 없었다.
RefreshToken 의 유효기간은 사용자 편의를 위해 14일로 설정했다.
RefreshToken 과 사용자 ID 를 서버에 저장해서 서버 측에서 토큰을 컨트롤 할 수 있도록 했다.<br>발급받은 RefreshToken 은 쿠키에 저장했다. 그 이유는 아래와 같다. 로컬, 세션 스토리지는 XSS 공격에 노출되기 쉬웠고 AccessToken 과 마찬가지로 JS 변수로 저장하면 페이지를 새로고침하거나 창을 닫는다면 저장된 내용이 사라진다. 즉 자동 로그인을 구현할 수 없다. 그래서 CSRF 공격에 취약할 수 있다는 점을 감안하고 쿠키에 저장했다.(CSRF 공격을 받더라도 RefreshToken 은 AccessToken 재발급에만 사용되기 때문에 별다른 피해는 없을 것이다, 그리고 공격자는 서버와 계약된 ORIGIN 이 아니기 때문에AccessToken 이 포함된 응답을 받을 수 없다) 또한 생?쿠키는 XSS 공격에 안전하지 않기 때문에 아래와 같이 httpOnly, secure 설정을 활성화했다.</p>
<pre><code class="language-java">private ResponseCookieBuilder createTokenCookieBuilder(final String value) {
        return ResponseCookie.from(REFRESH_TOKEN, value)

                // 해당 쿠키는 자바스크립트로 접근할 수 없다.
                .httpOnly(true)

                // HTTPS 로 통신하는 경우에만 쿠키를 전송한다.
                .secure(true)
                .path(&quot;/&quot;)

                // 클라이언트측에서 Origin 과 Origin 에 링크된 사이트에만 쿠키를 전송한다.
                // 기본 설정이 SameSite.LAX (생략가능)
               .sameSite(SameSite.LAX.attributeValue());
}</code></pre>
<p>이에 더해 RefreshToken 이 한 번 사용되면 폐기하도록 (RefreshToken 으로 새 AccessToken 을 발급 받을때 RefreshToken 도 새 RefreshToken 으로 발급한다) 해서 이미 사용된 RefreshToken 의 재사용(토큰 탈취)을 서버에서 인지할 수 있으며 이에 따른 후속 조치가 가능해진다.
이렇게 RefreshToken 을 구현함으로써 기존의 토큰 방식보다 안전하고 편리한 인가 방식을 제공 할 수 있었다. 또한 세션 방식과 비교해봐도 AccessToken 의 유효기간 동안은 DB 서버에 접근하지 않으므로 보다 효율적인 통신을 할 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[submodule 적용하기]]></title>
            <link>https://velog.io/@0_0_yoon/submodule-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@0_0_yoon/submodule-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 10 Nov 2022 07:05:58 GMT</pubDate>
            <description><![CDATA[<h3 id="문제상황">문제상황</h3>
<p>프로젝트에 포함된 Credential 관련 내용(DataSource, 인증, 인가에 관련된 설정 등)들이 Public Repo 에 그대로 노출될 위험이 있다.</p>
<h3 id="해결">해결</h3>
<h4 id="첫-번째-해결-방법-jasyptjava-simplified-encryption-암호화-라이브러리-사용">첫 번째 해결 방법 Jasypt(Java Simplified Encryption) 암호화 라이브러리 사용</h4>
<p>장점: 설정 정보들이 암호화 되어 있어서 암화화할때 사용된키값만 잘 관리한다면 노출되어도 큰 문제가 없다.</p>
<p>단점: 프로덕션 코드에 암호화를 위한 코드를 추가해야한다.</p>
<h4 id="두-번째-해결-방법-submoduleprivate-repository-사용">두 번째 해결 방법 Submodule(private repository) 사용</h4>
<p>장점: 별도로 프로덕션 코드에 추가되는게 없다.</p>
<p>단점: 외부에 대한 접근을 막을 수 있지만 내부 사용자끼리 실수로 유출되는 건 막을수 없다.</p>
<p>프로덕션 코드에 변경없이 한 곳에서 설정 정보들을 관리하기 위해 서브 모듈을 사용하기로 했다.</p>
<h4 id="서브-모듈-적용-과정">서브 모듈 적용 과정</h4>
<ol>
<li>Private Repository 생성<ol>
<li>Organization을 생성한다.(organization member를 추가해준다)
<img src="https://velog.velcdn.com/images/0_0_yoon/post/784bd22e-646e-41d4-a442-7e1285f7eed7/image.png" alt=""></li>
<li>생성한 Organization 에서 Private Repository를 생성한다.</li>
<li>생성한 Private Repository 에서 Credential 설정 정보를 가진 YML 파일을 추가한다.</li>
</ol>
</li>
<li>프로젝트 저장소 에 서브 모듈 추가하기<ol>
<li>프로젝트 저장소 Branch 에서 명령어를 실행<pre><code> // default branch를 기준으로 서브 모듈을 등록한다.
 git submodule add {private_repository_URL}</code></pre><pre><code> // 특정 브랜치를 기준으로 서브 모듈을 추가한다.
 git submodule add -b {branch_name} {private_repository_URL}</code></pre></li>
<li>서브 모듈 파일과 메인 프로젝트 연결하기(build.gradle 파일에 설정추가)<pre><code>     // 서브 모듈의 파일을 복사한다
     task copySubmodule(type: Copy) {
         copy {
             from &#39;security&#39;
             include &quot;*.yml&quot;
             into &#39;src/main/resources&#39;
         }
     }</code></pre><ol start="3">
<li>서브 모듈 추가한 내용을 Commit, Push 하면 끝<h4 id="다른-팀원-로컬에-서브-모듈-가져오기">다른 팀원 로컬에 서브 모듈 가져오기</h4>
프로젝트 저장소를 Clone 해서 로컬로 가져올 경우 포함되있는 서브모듈의 파일들은 가져오지 않는다.<pre><code>// private repository 라서 권한이 없으면 해당 명령어를 실행 할 수 없다
git submodule init
</code></pre></li>
</ol>
</li>
</ol>
</li>
</ol>
<p>// clone submodules 
git submodule update</p>
<p>```</p>
<h4 id="서브-모듈-내용을-수정한-경우">서브 모듈 내용을 수정한 경우</h4>
<p>프로젝트 저장소 보다 먼저 Push 또는 Pull을 해야 한다. 만약 프로젝트 저장소에 Push/Pull 하고 서브 모듈을 Push/Pull 하면 예상치 못한 오류가 발생한다. 그 이유는 프로젝트 저장소에는 서브 모듈을 그대로 가지지 않고 Path, Url, Commit 정보만 저장하기 때문이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Tomcat]]></title>
            <link>https://velog.io/@0_0_yoon/Tomcat</link>
            <guid>https://velog.io/@0_0_yoon/Tomcat</guid>
            <pubDate>Tue, 08 Nov 2022 02:16:28 GMT</pubDate>
            <description><![CDATA[<p>우테코에서 <a href="https://github.com/woowacourse/jwp-dashboard-http/pull/292">톰캣 구현 미션</a>을 하면서 구성요소와 동작 과정을 짧게 정리했다.</p>
<h4 id="톰캣was-의-역할">톰캣(WAS) 의 역할</h4>
<ol>
<li>서블릿 객체 관리(생성, 초기화, 호출, 종료)</li>
<li>HttpServletRequest, HttpServletResponse 생성</li>
</ol>
<h4 id="톰캣-컴포넌트-구성">톰캣 컴포넌트 구성</h4>
<ol>
<li><p>Coyote(HTTP Component)</p>
<ol>
<li>톰캣에 TCP를 통한 프로토콜 지원</li>
<li>Coyote는 HTTP 1.1 및 2 프로토콜을 웹 서버로 지원하는 Tomcat용 커넥터 구성 요소. 이를 통해 명목상 Java 서블릿 또는 JSP 컨테이너인 Catalina가 로컬 파일을 HTTP 문서로 제공하는 일반 웹 서버로도 작동할 수 있다. Coyote는 특정 포트에서 서버로 들어오는 연결을 수신하고 Tomcat 엔진에 요청을 전달하여 요청을 처리하고 요청하는 클라이언트에 응답을 보낸다.</li>
<li>Coyote는 HTTP 1.1 및 2 프로토콜을 웹 서버로 지원하는 Tomcat용 커넥터 구성 요소.</li>
</ol>
</li>
<li><p>Catalina(Servlet Container)</p>
<ol>
<li>Java Servlet을 호스팅하는 환경</li>
</ol>
</li>
<li><p>Jasper(JSP Engine)</p>
<ol>
<li>실제 JSP 페이지의 요청을 처리하는 Servlet</li>
</ol>
</li>
</ol>
<h4 id="동작-과정">동작 과정</h4>
<ol>
<li>HTTP 요청을 Coyote 에서 받아서 Catalina로 전달한다.</li>
<li>Catalina에서 전달받은 HTTP 요청을 처리할 웹 어플리케이션을 찾는다.</li>
<li>요청된 Servlet을 통해 생성된 jsp 파일들이 호출될 때, Jasper이 Validation Check / Complie 등을 수행한다.</li>
<li>Coyote가 HTTP요청을 받으면 Catalina 서블릿 컨테이너에서 요청중에서 java웹 어플리케이션을 해석하는데, 그중에 jsp에 관한 요청 일때 Jasper가 담당해서 처리.
<img src="https://velog.velcdn.com/images/0_0_yoon/post/bc5107c6-0a76-4de5-be97-3ad69577434d/image.png" alt=""></li>
</ol>
<p>참고
<a href="https://en.wikipedia.org/wiki/Apache_Tomcat">https://en.wikipedia.org/wiki/Apache_Tomcat</a>
<a href="https://medium.com/@js230023/jasper%EC%99%80-catalina-ea11a337945f">https://medium.com/@js230023/jasper와-catalina-ea11a337945f</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Decorator pattern vs Subclassing]]></title>
            <link>https://velog.io/@0_0_yoon/Decorator-pattern-vs-Subclassing</link>
            <guid>https://velog.io/@0_0_yoon/Decorator-pattern-vs-Subclassing</guid>
            <pubDate>Wed, 02 Nov 2022 01:00:21 GMT</pubDate>
            <description><![CDATA[<p>새로운 기능을 추가할 경우 사용할 수 있는 디자인 패턴이다. 
subclassing(상속) 을 사용하는 경우보다 더 효율적일 가능성이 있다.</p>
<p>Subclassing 을 하는 경우</p>
<ol>
<li><p>기존의 클래스를 상속하는 경우 상위 클래스가 변경되면 하위 클래스가 영향을 받을수 있다.</p>
<pre><code class="language-java">public class SpaceEditor implements Editor {

private final String content;

public SpaceEditor(final String content){
  this.content = content;
}

@override
String edit() {
  return content.trim();
}
}
</code></pre>
</li>
</ol>
<p>public class LowerCaseSpaceEditor extends SpaceEditor {</p>
<pre><code>public LowerCaseSpaceEditor(final String content){
    super(content);
   }

@override
String edit(){
    return super.edit()
        .toLowerCase();
}</code></pre><p>}</p>
<p>// SpaceEditor 의 생성자 인자 타입을 변경할 경우
public class SpaceEditor implements Editor {</p>
<pre><code>private final Content content;

public SpaceEditor(final Content content){
    this.content = content;
}

@override
String edit() {
    return content.removeSpace();
}</code></pre><p>}</p>
<p>// 하위 클래스에서도 변경이 발생한다
public class LowerCaseSpaceEditor extends SpaceEditor {</p>
<pre><code>public LowerCaseSpaceEditor(final Content content){
    super(content);
   }

@override
String edit(){
    return super.edit()
        .toLowerCase();
}</code></pre><p>}</p>
<pre><code>2. 추가할 기능 개수 만큼 상속 클래스가 늘어난다.
``` java
// 소문자 변환, 문자열 자르기, 소문자 변환과 문자열 자르기 기능이 필요한 경우 3개의 클래스를 추가한다

public class LowerCaseSpaceEditor extends SpaceEditor {

    public LowerCaseSpaceEditor(final String content){
        super(content);
       }

    @override
    String edit() {
        return super.edit()
            .toLowerCase();
    }
}

public class SubStringSpaceEditor extends SpaceEditor {

    public SubStringSpaceEditor(final String content){
        super(content);
       }

    @override
    String edit() {
        return super.edit()
            .subString(0);
    }
}

public class SubStringLowerCaseSpaceEditor extends SpaceEditor {

    public SubStringLowerCaseSpaceEditor(final String content){
        super(content);
       }

    @override
    String edit() {
        return super.edit()
            .toLowerCase()
            .subString(0);
    }
}</code></pre><p>Decorator pattern 을 적용한 경우</p>
<ol>
<li><p>Base class 가 변경되어도 추가 기능을 구현한 클래스에는 영향이 없다.</p>
<pre><code class="language-java">public class SpaceEditor implements Editor {

 private final String content;

 public SpaceEditor(final String content){
     this.content = content;
 }

 @override
 String edit() {
     return content.trim();
 }
}
</code></pre>
</li>
</ol>
<p>public class LowerCaseEditor extends EditorDecorator {</p>
<pre><code>public LowerCaseEditor(final Editor editor) {
    super(editor);
}

@override
String edit(){
    return super.edit()
        .toLowerCase();
}</code></pre><p>}</p>
<p>// SpaceEditor 의 생성자 인자 타입을 변경할 경우
public class SpaceEditor implements Editor {</p>
<pre><code>private final Content content;

public SpaceEditor(final Content content){
    this.content = content;
}

@override
String edit() {
    return content.removeSpace();
}</code></pre><p>}</p>
<p>// 변경이 발생하지 않는다
public class LowerCaseEditor extends EditorDecorator {</p>
<pre><code>public LowerCaseEditor(final Editor editor) {
    super(editor);
}

@override
String edit(){
    return super.edit()
        .toLowerCase();
}</code></pre><p>}</p>
<pre><code>
2. 각 기능들을 조합해서 사용할 수 있다.
``` java
// 소문자 변환, 문자열 자르기, 소문자 변환과 문자열 자르기 기능이 필요한 경우 2 개의 클래스를 추가한다
public class SpaceEditor implements Editor {

    private final String content;

    public SpaceEditor(final String content){
        this.content = content;
    }

    @override
    String edit() {
        return content.trim();
    }
}

public class LowerCaseEditor extends EditorDecorator {

    public LowerCaseEditor(final Editor editor) {
        super(editor);
    }

    @override
    String edit(){
        return super.edit()
            .toLowerCase();
    }
}

public class SubStringEditor extends EditorDecorator {

    public SubStringEditor(final Editor editor) {
        super(editor);
    }

    @override
    String edit(){
        return super.edit()
            .subString(0);
    }
}

// 소문자 변환과 문자열 자르기 기능은 아래와 같이 구현할 수 있다
final Editor editor = new SubStringEditor(
    new LowerCaseEditor(new SpaceEditor(&quot;content&quot;))
);
editor.edit();
</code></pre><p>참고: 
<a href="https://en.wikipedia.org/wiki/Decorator_pattern">https://en.wikipedia.org/wiki/Decorator_pattern</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[페이징 성능 개선]]></title>
            <link>https://velog.io/@0_0_yoon/%ED%8E%98%EC%9D%B4%EC%A7%95-%EC%84%B1%EB%8A%A5-%EA%B0%9C%EC%84%A0</link>
            <guid>https://velog.io/@0_0_yoon/%ED%8E%98%EC%9D%B4%EC%A7%95-%EC%84%B1%EB%8A%A5-%EA%B0%9C%EC%84%A0</guid>
            <pubDate>Tue, 01 Nov 2022 02:38:19 GMT</pubDate>
            <description><![CDATA[<h3 id="문제-상황">문제 상황</h3>
<pre><code class="language-java">@Query(&quot;select r from Review r join fetch r.member join fetch r.product&quot;)
Slice&lt;Review&gt; findPageBy(final Pageable pageable);</code></pre>
<p>fetch join 을 통해 N+1 문제를 해결하고 성능테스트를 해본 결과 최근 리뷰 조회 시 평균 latency 가 4 초.<br>개선이 필요했다. </p>
<h3 id="원인">원인</h3>
<h4 id="offset-을-사용한-페이징-방식">offset 을 사용한 페이징 방식</h4>
<p>offset 을 사용하면 항상 첫 번째 row 부터 읽어온다.(그 뒤에 필요한 row 를 제외한 나머지는 버린다) 즉 데이터가 쌓일수록 마지막에 가까운 페이지를 요청할수록 읽어올 데이터가 늘어난다.</p>
<h3 id="해결">해결</h3>
<h4 id="offset-사용-653-ms">offset 사용 653 ms</h4>
<h4 id="offset-사용커버링-인덱스-98ms">offset 사용(커버링 인덱스) 98ms</h4>
<h4 id="no-offset-50ms">no-offset 50ms</h4>
<h4 id="테스트-환경">테스트 환경</h4>
<ul>
<li>테스트 DB: Mysql(local)</li>
<li>테스트 테이블: review 10만 row 저장</li>
<li>조회 조건: 5만 번째 row 부터 50개 조회</li>
</ul>
<p>당연히 no-offset 이 가장 빠른 속도를 냈다.
페이징 구현 시 먼저 no-offset 사용을 고려해 본다. 만약 사용할 조건이 되지 않는다면 offset 방식(커버링 인덱스)으로 구현하도록 한다.</p>
<h4 id="1-offset-을-사용하지-않는다">1. offset 을 사용하지 않는다.</h4>
<p>애초에 문제가 되는 offset 을 사용하지 않는 방법이다. 클라이언트에게 마지막으로 응답받은 row 번호를 받아 그 이후의 필요한 데이터에만 접근하는 것이다. 당연히 너무 좋은 방법이지만 두 가지 조건을 충족해야 사용할 수 있다.</p>
<h5 id="1-조회-기준이-중복-없이-순서를-매길-수-있어야-한다-즉-중복이-가능한-조회-기준에는-no-offset-을-적용할-수-없다">1. 조회 기준이 중복 없이 순서를 매길 수 있어야 한다. 즉 중복이 가능한 조회 기준에는 no offset 을 적용할 수 없다.</h5>
<h5 id="2-프론트엔드와-협의가-필요하다-페이지-번호를-사용하지-않기-때문에-ui-가-페이지-번호-대신--more-로-변경된다">2. 프론트엔드와 협의가 필요하다. 페이지 번호를 사용하지 않기 때문에 UI 가 페이지 번호 대신 +, more 로 변경된다.</h5>
<p>최근 리뷰 조회의 경우 id 를 가지고 중복 없이 순서를 매길수 있고, 프론트엔드에서 무한 스크롤을 사용하기 때문에 offset 을 사용하지 않고 페이징 구현을 할 수 있다.</p>
<pre><code class="language-java">public List&lt;Review&gt; findPageBy(final Long reviewId, final int pageSize) {
        return jpaQueryFactory.selectFrom(review)
                .where(ltReviewId(reviewId))
                .innerJoin(review.member, member)
                .fetchJoin()
                .innerJoin(review.product, product)
                .fetchJoin()
                .orderBy(review.id.desc())
                .limit(pageSize)
                .fetch();
}

private BooleanExpression ltReviewId(final Long reviewId) {
        if (reviewId == null) {
            return null;
        }
        return review.id.lt(reviewId);
}</code></pre>
<h4 id="2-커버링-인덱스를-사용한다">2. 커버링 인덱스를 사용한다.</h4>
<p>offset 사용 시 불필요한 데이터 조회 비용을 최소화하기 위해서 먼저 첫 row 부터 요청한 row 까지의 리뷰를 id 만 조회하도록 한 뒤(PK 는 클러스터 인덱스로 자동 등록, 즉 커버링 인덱스 적용됨) 실제 필요한 row 의 데이터만 조회하도록 구현했다.</p>
<pre><code class="language-java">public Slice&lt;Review&gt; findByPage(final Pageable pageable) {
    final JPAQuery&lt;Long&gt; coveringIndexQuery = jpaQueryFactory.select(review.id)
        .from(review)
        .offset(pageable.getOffset())
        .limit(pageable.getPageSize() + 1)
        .orderBy(makeOrderSpecifiers(review, pageable));

    final Slice&lt;Long&gt; reviewIds = toSlice(pageable, coveringIndexQuery.fetch());

    if (reviewIds.isEmpty()) {
        return new SliceImpl&lt;&gt;(Collections.emptyList(), pageable, false);
    }

    final JPAQuery&lt;Review&gt; query = jpaQueryFactory.selectFrom(review)
        .where(review.id.in(reviewIds.getContent()))
        .innerJoin(review.member, member)
        .fetchJoin()
        .innerJoin(review.product, product)
        .fetchJoin()
        .orderBy(makeOrderSpecifiers(review, pageable));
       return new SliceImpl&lt;&gt;(query.fetch(), pageable, reviewIds.hasNext());
}</code></pre>
<p>참고
    <a href="https://jojoldu.tistory.com/528">https://jojoldu.tistory.com/528</a>
    <a href="https://jojoldu.tistory.com/529">https://jojoldu.tistory.com/529</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[토비의 스프링 테스트]]></title>
            <link>https://velog.io/@0_0_yoon/%ED%86%A0%EB%B9%84%EC%9D%98-%EC%8A%A4%ED%94%84%EB%A7%81-%ED%85%8C%EC%8A%A4%ED%8A%B8</link>
            <guid>https://velog.io/@0_0_yoon/%ED%86%A0%EB%B9%84%EC%9D%98-%EC%8A%A4%ED%94%84%EB%A7%81-%ED%85%8C%EC%8A%A4%ED%8A%B8</guid>
            <pubDate>Sun, 16 Oct 2022 09:39:45 GMT</pubDate>
            <description><![CDATA[<p>이번 기회에 스프링으로 프로젝트를 하면서 스프링 테스트에 대해 정리하고자 토비의 스프링 2장 테스트를 읽어봤다. 다음은 정리한 내용들이다. 틀린 부분이 있다면 피드백 부탁드립니다!</p>
<blockquote>
<p>스프링이 개발자에게 제공하는 중요한 가치중 하나가 테스트이다.</p>
</blockquote>
<h3 id="테스트의-유용성">테스트의 유용성</h3>
<p>테스트란 내가 예상하고 의도했던 대로 코드가 정확히 동작하는지를 확인해서, 만든 코드를 확신할 수 있게 해주는 작업이다. 또한 테스트의 결과가 원하는 대로 나오지 않는 경우에는 코드나 설계에 결함이 있음을 알 수 있다. 이를 통해 코드의 결함을 제거해가는 작업, 일명 디버깅을 거치게 되고, 결국 최종적으로 테스트가 성공하면 모든 결함이 제거됐다는 확신을 얻을 수 있다.</p>
<h3 id="userdaotest의-특징">UserDaoTest의 특징</h3>
<pre><code class="language-java">public class UserDaoTest {
    public static void main(String[] args) throws SQLException {
        ApplicationContext context = new GenericXmlApplicationContext(&quot;applicationContext.xml&quot;);

        UserDao dao = context.getBean(&quot;userDao&quot;, UserDao.class);

        User user = new User();
        user.setId(&quot;user&quot;);
        user.setName(&quot;칙촉&quot;);
        user.setPassword(&quot;password&quot;);

        dao.add(user);

        System.out.println(user.getId() + &quot;등록 성공&quot;);

        User user2 = dao.get(user.getId());
        System.out.println(user2.getName());
        System.out.println(user.getPassword());

        System.out.println(user2.getId() + &quot;조회 성공&quot;);
    }         
}</code></pre>
<ul>
<li>main 메서드를 통해서 테스트를 진행한다.</li>
<li>테스트 결과를 콘솔로 출력한다.</li>
</ul>
<h3 id="웹을-통한-dao-테스트-방법의-문제점">웹을 통한 DAO 테스트 방법의 문제점</h3>
<p>MVC 프레젠테이션 계층을 구현한다. 테스트용 웹 애플리케이션을 서버에 띄우고 요청을 보내본다.</p>
<p>이 방법은 배보다 배꼽이 더 크다;</p>
<ol>
<li>DAO 테스트를 위해 다른 계층을 구현해야 한다.</li>
<li>디버깅하기 힘들다.<ol>
<li>하나의 테스트를 수행하는 데 참여하는 클래스와 코드가 너무 많기 때문이다.</li>
<li>심지어 서버의 설정 상태까지 모두 테스트에 영향을 끼친다.</li>
</ol>
</li>
</ol>
<h3 id="작은-단위의-테스트">작은 단위의 테스트</h3>
<h4 id="개념">개념</h4>
<p>   여기서 단위는 충분히 하나의 관심에 집중해서 효율적으로 테스트할 만한 범위의 단위이다. 크게는 사용자 관리 기능을 모두 통틀어서 하나의 단위로 볼 수 있고, 작게 보자면 메서드 하나만 가지고 하나의 단위로 볼 수 있다. 일반적으로 단위는 작을수록 좋다.</p>
<p>   외부의 리소스에 의존하는 테스트는 단위 테스트가 아니다.</p>
<p>   Dao 단위 테스트의 경우에 DB에 의존적인, 즉 테스트마다 DB가 격리되지 않는다면 단위 테스트라고 보기 어렵다.</p>
<h4 id="필요성">필요성</h4>
<ol>
<li>예외가 발생해도 그 이유를 찾는 데 많은 시간이 걸릴 수 있다. </li>
<li>빠르게 피드백 받을 수 있다.</li>
<li>해당 기능에 확신을 갖을 수 있다.</li>
</ol>
<h3 id="자동수행-테스트-코드">자동수행 테스트 코드</h3>
<p>테스트가 코드를 통해 자동으로 실행된다. 즉 UserDaoTest 를 실행하면 자동으로 테스트가 실행된다. 만약 앞서 나온 웹을 통해 테스트를 수행한다면 코드가 아닌 개발자 스스로가 테스트를 수행해야한다.</p>
<p>자동수행 테스트 코드의 장점은 자주 반복할 수 있다는 것이다.</p>
<h3 id="지속적인-개선과-점진적인-개발을-위한-테스트">지속적인 개선과 점진적인 개발을 위한 테스트</h3>
<p>작은 단계를 거치는 동안 테스트를 수행해서 확신을 가지고 코드를 변경해갔기 때문에 전체적으로 코드를 개선하는 작업에 속도가 붙고 더 쉬워졌다.</p>
<p>또한 UserDao의 기능을 추가하려고 할 때도 미리 만들어둔 테스트 코드는 유용하게 쓰일 수 있다. 기존에 만들어뒀던 기능들이 새로운 기능을 추가하느라 수정한 코드에 영향을 받지 않고 여전히 잘 동작하는지를 확인할 수도 있다.</p>
<h2 id="userdaotest의-문제점">UserDaoTest의 문제점</h2>
<ol>
<li>수동 확인 작업의 번거로움</li>
<li>실행 작업의 번거로움</li>
</ol>
<h2 id="userdaotest-개선">UserDaoTest 개선</h2>
<h3 id="테스트-검증의-자동화">테스트 검증의 자동화</h3>
<pre><code class="language-java">// 수정 전 테스트 코드
System.out.println(user2.getName());
System.out.println(user2.getPassword());
System.out.println(user2.getId() + &quot;조회 성공&quot;);</code></pre>
<pre><code class="language-java">// 수정 후 테스트 코드
if (!user.getName().equals(user2.getName())) {
     System.out.println(&quot;테스트 실패 (name)&quot;);
}
else if (!user.getPassword().equals(user2.getPassword())){
     System.out.println(&quot;테스트 실패 (password)&quot;);
}
else {
    System.out.println(&quot;조회 테스트 성공&quot;);
}</code></pre>
<h2 id="테스트의-효율적인-수행과-결과-관리">테스트의 효율적인 수행과 결과 관리</h2>
<p>Main 메서드로 만든 테스트의 한계를 극복하기 위해서 JUnit 사용</p>
<ol>
<li>일정한 패턴을 가진 테스트를 만들 수 있다.</li>
<li>많은 테스트를 간단히 실행시킬 수 있다.</li>
<li>테스트 결과를 종합해서 볼 수 있다.</li>
<li>테스트가 실패한 곳을 빠르게 찾을 수 있다.</li>
</ol>
<h3 id="junit-테스트로-전환">JUnit 테스트로 전환</h3>
<p>JUnit 은 프레임워크이다. 프레임워크는 개발자가 만든 클래스에 대한 제어 권한을 넘겨받아서 주도적으로 애플리케이션의 흐름을 제어한다.</p>
<h3 id="테스트-메소드-전환">테스트 메소드 전환</h3>
<p>main 메서드는 제어권을 직접 갖는다는 의미이다. JUnit 프레임워크를 사용하기 위해서 일반 메서드로 옮겨야한다. 이때 두 가지 조건이 붙는다.</p>
<ol>
<li>메소드가 Public 으로 선언돼야 한다.(이제는 default 접근자도 사용 가능함)</li>
<li>@Test  어노테이션을 붙여줘야한다.</li>
</ol>
<pre><code class="language-java">public class UserDaoTest {

    @Test 
    public void andAndGet() throws SQLException {
        ApplicationContext context = new GenericXmlApplicationContext(&quot;applicationContext.xml&quot;);

        UserDao dao = context.getBean(&quot;userDao&quot;, UserDao.class);

        // 생략
    }
}</code></pre>
<h3 id="검증-코드-전환">검증 코드 전환</h3>
<pre><code class="language-java">if (!user.getName()).equals(user2.getName()){...}

// 이 if 문장의 기능을 JUnit 이 제공해주는 assertThat 이라는 스태틱 메서드를 이용해 다음과 같이 변경할 수 있다.

assertThat(user2.getName()).is(user.getName()));</code></pre>
<p>JUnit은 예외가 발생하거나 assertThat 메서드에서 실패하지 않고 테스트 메서드의 실행이 완료되면 테스트가 성공했다고 인식한다.</p>
<h3 id="junit-테스트-실행">JUnit 테스트 실행</h3>
<pre><code class="language-java">import org.junit.runner.JUnitCore;
...
public static void main(String[] args) {
    JUnitCore.main(&quot;springbook.user.dao.UserDaoTest&quot;);
}</code></pre>
<h2 id="개발자를-위한-테스팅-프레임워크-junit">개발자를 위한 테스팅 프레임워크 JUnit</h2>
<h3 id="junit-테스트-실행-방법">JUnit 테스트 실행 방법</h3>
<h3 id="ide">IDE</h3>
<p>IDE에 내장된 JUnit 테스트 지원 도구를 사용한다.</p>
<h3 id="빌드-툴">빌드 툴</h3>
<p>개발자 개인별로는 IDE 에서 JUnit 도구를 활용해 테스트를 실행하는 게 가장 편리하다. 그런데 여러 개발자가 만든 코드를 모두 통합해서 테스트를 수행해야 할 때도 있다. 이런 경우에는 서버에서 모든 코드를 가져와 통합하고 빌드한 뒤에 테스트를 수행하는 것이 좋다. 이때는 빌드 스크립트를 이용해 JUnit 테스트를 실행하고 그 결과를 메일 등으로 통보받는 방법을 사용하면 된다.</p>
<h2 id="테스트-결과의-일관성">테스트 결과의 일관성</h2>
<p>코드에 변경사항이 없다면 테스트는 항상 동일한 결과를 내야 한다.</p>
<p>deleteAll  메서드를 만들어서 테스트가 등록한 사용자 정보를 삭제하도록 구현함 → 매번 수동으로 DB 데이터를 지워주는 부분을 자동화함</p>
<p>getCount 메서드를 만들어서 USER 테이블의 레코드 개수를 조회한다  → 위에서 만든 deleteAll 이 잘 동작했는지 검증한다.</p>
<pre><code class="language-java">@Test
public void addAndGet() throws SQLException {
    ...

    dao.deleteAll();
    assertThat(dao.getCount(), is(0));
}</code></pre>
<pre><code class="language-java">User user = new User();
user.setId(&quot;id&quot;);
user.setName(&quot;칙촉&quot;);
user.setPassword(&quot;password&quot;);

dao.add(user);
assertThat(dao.getCount(), is(1));

User user2 = dao.get(user.getId());

assertThat(user2.getName(), is(user.getName()));
assertThat(user2.getPassword(), is(user.getPassword()));</code></pre>
<h3 id="동일한-결과를-보장하는-테스트">동일한 결과를 보장하는 테스트</h3>
<p>위의 코드와 다르게 테스트를 마치기 직전에 데이터를 지워도 동일한 테스트 결과를 얻을 수 있다. 하지만 해당 테스트 실행 이전에 다른 이유로 USER 테이블에 데이터가 들어가 있다면 테스트가 실패할 수도 있다.</p>
<p>스프링은 DB를 사용하는 코드를 테스트하는 경우 매우 편리한 테스트 방법을 제공해준다.</p>
<p>@DataJpaTest, @JdbcTest 는</p>
<p>@Transactional 어노테이션이 있어서 자동으로 롤백시켜준다.(테스트 코드에서 @Transactional 어노테이션이 있으면 select 이외의 모든 쿼리는 롤백 대상이된다)</p>
<h3 id="테스트-주도-개발">테스트 주도 개발</h3>
<p>실패한 테스트를 성공시키기 위한 목적이 아닌 코드는 만들지 않는다. → 오버엔지니어링을 방지할 수 있다.</p>
<p>테스트가 실패하면 설계한 대로 코드가 만들어지지 않았음을 바로 알 수 있다 → 즉각적인 피드백을 받을 수 있다.</p>
<p>중요성 코드를 만들고 나서 시간이 많이 지나면 테스트를 만들기가 귀찮아진다.또, 작성한 코드가 많기 때문에 무엇을 테스트해야 할지 막막할 수도 있다.</p>
<h3 id="테스트-코드-개선">테스트 코드 개선</h3>
<p>JUnit이 테스트를 수행하는 방식</p>
<ol>
<li>테스트 클래스에서 @Test 가 붙은 public 이고 void 형이며 파라미터가 없는 테스트 메서드를 모두 찾는다.</li>
<li>테스트 클래스의 오브젝트를 하나 만든다.</li>
<li>@Before 가 붙은 메서드가 있으면 실행한다.</li>
<li>@Test 가 붙은 메서드를 하나 호출하고 테스트 결과를 저장해둔다.</li>
<li>@After 가 붙은 메서드가 있으면 실행한다.</li>
<li>나머지 테스트 메서드에 대해 2~5 번을 반복한다.</li>
<li>모든 테스트의 결과를 종합해서 돌려준다.</li>
</ol>
<p>각 테스트가 서로 영향을 주지 않고 독립적으로 실행됨을 확실히 보장해주기 위해 매번 새로운 오브젝트를 만들게 했다. 덕분에 멤버 필드도 부담없이 사용할 수 있다.</p>
<h3 id="픽스처">픽스처</h3>
<p>테스트 클래스에서 자주 사용되는 값, 오브젝트들은 멤버필드로 선언해서 사용한다. 어차피 매번 새로운 테스트 오브젝트가 만들어지니까 멤버필드에서 바로 초기화해도 상관없다. 하지만 픽스처 생성 로직이 흩어져 있는 것 보다는 모여 있는 편이 나을 테니 @Before 메서드를 이용</p>
<pre><code class="language-java">public class UserDaoTest {
    private UserDao dao;
    private User user1;
    private User user2;
    private User user3;

    @Before
    public void serUp() {
        ...
        this.user1 = new User(&quot;id1&quot;, &quot;영&quot;, &quot;password&quot;);
        this.user2 = new User(&quot;id2&quot;, &quot;윤&quot;, &quot;password&quot;);
        this.user3 = new User(&quot;id3&quot;, &quot;안&quot;, &quot;password&quot;);
    }
    ...
}</code></pre>
<h2 id="스프링-테스트-적용">스프링 테스트 적용</h2>
<p>현재 테스트 코드에는 몇가지 문제가 있다. ApplicationContext 를 여러번 만들고 있다. 추후에 빈이 많아지고 복잡해지면 ApplicationContext 생성에 적지 않은 시간이 걸릴 수 있다. ApplicationContext 만들어질때 모든 싱글톤 빈 오브젝트를 초기화하기 때문이다.</p>
<p>또 한 가지 문제는 ApplicationContext가 초기화될 떄 어떤 빈은 독자적으로 많은 리소스를 할당하거나 독립적인 스레드를 띄우기도 한다는 점이다. 이런 경우에는 테스트를 마칠 때마다 ApplicationContext 내의 빈이 할당한 리소스 등을 깔끔하게 정리해주지 않으면 다음 테스트에서 새로운 ApplicationContext가 만들어지면서 문제를 일으킬 수도 있다.</p>
<p>→ @BeforeClass 를 사용해서 스태틱 필드에 ApplicationContext를 저장한다. @BeforeClass 는 테스트 클래스 전체에 걸쳐 딱 한 번만 실행된다. But 더 편리하게 할 수 있다.</p>
<h3 id="테스트를-위한-applicationcontext-관리">테스트를 위한 ApplicationContext 관리</h3>
<p>스프링은 JUnit을 이용하는 테스트 컨텍스트 프레임워크를 제공한다. 테스트 컨텍스트의 지원을 받으면 간단한 어노테이션 설정만으로 테스트에서 필요로 하는 ApplicationContext를 만들어서 모든 테스트가 공유하게 할 수 있다.(추가 ApplicationContext는 초기화할 때 자기 자신도 빈으로 등록한다. 그래서 Autowired 를 통해 주입받을 수 있는것)</p>
<pre><code class="language-java">@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(location=&quot;/applicationContext.xml&quot;)
public class UserDaoTest {
    @Autowired
    private ApplicationContext context;

    ...

    @Before
    public void setUp() {
        this.dao = this.context.getBean(&quot;userDao&quot;, UserDao.class);
        ...
    }
}</code></pre>
<p>@RunWith: JUnit 프레임워크의 테스트 실행 방법을 확장할 때 사용한다. SpringJUnit4ClassRunner라는 JUnit용 테스트 컨텍스트 프레임워크 확장 클래스를 지정해주면 JUnit이 테스트를 진행하는 중에 테스트가 사용할 ApplicationContext를 만들고 관리하는 작업을 진행해준다.</p>
<p>@ContextConfiguration: 자동으로 만들어줄 ApplicationContext 설정파일 위치를 지정한 것이다.</p>
<p>결과: 하나의 테스트 클래스 내의 테스트 메소드는 같은 ApplicationContext를 공유해서 사용할 수 있게 됨</p>
<p>(추가: 최초 ApplicationContext가 초기화될때 bean 이 초기화되는게 아니라 getBean을 호출할때 초기화된다. )</p>
<h3 id="테스트-클래스의-컨텍스트-공유">테스트 클래스의 컨텍스트 공유</h3>
<p>여러 개의 테스트 클래스가 있는데 모두 같은 설정파일을 가진 ApplicationContext를 사용한다면, 스프링은 테스트 클래스 사이에서도 ApplicationContext를 공유하게 해준다. → 테스트 전체에 걸쳐서 단 한개의 ApplicationContext만 만들어져 사용됨, 테스트 성능이 대폭 향상됨</p>
<h3 id="autowired">@Autowired</h3>
<p>별도의 DI 설정 없이 필드의 <strong>타입정보</strong>를 이용해 빈을 자동으로 가져올 수 있다.(추가: 타입이 여러개라면 변수의 이름과 같은 이름의 빈이 있는지 확인한다, 변수 이름으로도 찾을수 없을 경우 예외 발생) ApplicationContext가 가지고 있는 빈을 DI 받을수 있다면 굳이 ApplicationContext를 주입 받는게 아니라 우리가 사용할 UserDao를 직접 DI 받도록 함</p>
<h3 id="테스트-코드에-의한-di">테스트 코드에 의한 DI</h3>
<p>테스트용 DB에 연결해주는 DataSource를 테스트 내에서 직접 만들어서 사용함, applicationContext.xml 파일의 설정정보를 따라 구성한 오브젝트를 가져와 의존관계를 강제로 변경</p>
<pre><code class="language-java">@DirtiesContext
public class UserDaoTest {
    @Autowired
    UserDao userDao;

    @Before
    public void setUp() {
        ...
        DataSource dataSource = new SingleConnectionDataSource(
            &quot;jdbc:mysql://localhost/testdb&quot;, &quot;spring&quot;, &quot;book&quot;, true
        );
    }
    ...
}</code></pre>
<pre><code class="language-java">  //  코드에 의한 수동 DI
  dao.setDataSource(dataSource);</code></pre>
<p>But DirtiesContext 는 매번 ApplicationContext를 새로 만듦!</p>
<h4 id="메서드-레벨의-dirtiescontext-사용하기">메서드 레벨의 @DirtiesContext 사용하기</h4>
<p>@DirtiesContext 는 클래스에만 적용할 수 있는 건 아니다. 하나의 메서드에서만 컨텍스트 상태를 변경한다면 메서드 레벨에 @DirtiesContext 를 붙여주는 편이 낫다. 해당 메서드의 실행이 끝나고 나면 이후에 진행되는 테스트를 위해 변경된 애플리케이션 컨텍스트는 폐기되고 새로운 애플리케이션 컨텍스트가 만들어진다.</p>
<h3 id="테스트를-위한-별도의-di-설정">테스트를 위한 별도의 DI 설정</h3>
<p>아예 테스트에서 사용될 DataSource 클래스가 빈으로 정의된 테스트 전용 설정파일을 따로 만들어두는 방법을 이용.</p>
<h3 id="컨테이너-없는-di-테스트">컨테이너 없는 DI 테스트</h3>
<p>스프링 컨테이너 없이 테스트 코드의 수동 DI 만을 이용</p>
<pre><code class="language-java">public class UserDaoTest {
    // @Autowired 가 없다
    UserDao dao;
    ...

    @Before
    public void setUp() {
        ...
        // 객체 생성 및 관계설정을 직접 해준다
        dao = new UserDao();
        DataSource dataSource = new SingleConnectionDataSource(
            &quot;jdbc:mysql://localhost/testdb&quot;, &quot;spring&quot;, &quot;book&quot;, true
        );
        dao.setDataSource(dataSource);
    }
}</code></pre>
<h4 id="침투적-기술과-비침투적-기술">침투적 기술과 비침투적 기술</h4>
<p>침투적 기술은 기술을 적용했을 때 애플리케이션 코드에 기술 관련 API 가 등장하거나, 특정 인터페이스나 클래스를 사용하도록 강제하는 기술을 말한다. 침투적 기술을 사용하면 애플리케이션 코드가 해당 기술에 종속되는 결과를 가져온다. 반면에 비침투적인 기술은 애플리케이션 로직을 담은 코드에 아무런 영향을 주지 않고 적용이 가능하다. 따라서 기술에 종속적이지 않은 순수한 코드를 유지할 수 있게 해준다. 스프링은 이런 비침투적인 기술의 대표적이 예다. 그래서 스프링 컨테이너 없는 DI 테스트도 가능한 것이다.</p>
<h3 id="di-를-이용한-테스트-방법-선택">DI 를 이용한 테스트 방법 선택</h3>
<p>컨테이너 없는 DI 테스트</p>
<p>항상 스프링 컨테이너 없이 테스트할 수 있는 방법을 가장 우선적으로 고려하자. 이 방법이 테스트 수행 속도가 가장 빠르고 테스트 자체가 간결하다.</p>
<p>@RunWith, @ContextConfiguration</p>
<p>여러 오브젝트와 복잡한 의존관계를 갖고 있는 오브젝트를 테스트해야 할 경우</p>
<p>@DirtiestContext</p>
<p>예외적인 의존관계를 강제로 구성해서 테스트해야 할 경우</p>
<h2 id="학습-테스트로-배우는-스프링">학습 테스트로 배우는 스프링</h2>
<h3 id="학습-테스트의-장점">학습 테스트의 장점</h3>
<ol>
<li>다양한 조건에 따른 기능을 손쉽게 확인해볼 수 있다.</li>
<li>학습 테스크 코드를 개발 중에 참고할 수 있다.</li>
<li>프레임워크나 제품을 업그레이드할 때 호환성 검증을 도와준다.</li>
<li>테스트 자성에 대한 좋은 훈련이 된다.</li>
<li>새로운 기술을 공유하는 과정이 즐거워진다.</li>
</ol>
<p>예시: </p>
<pre><code class="language-java">public class JUnitTest {
    static JUnitTest testObject;

    @Test
    public void test1() {
        assertThat(this, is(not(sameInstance(testObject))));
        testObject = this;
    }

    @Test
    public void test2() {
        assertThat(this, is(not(sameInstance(testObject))));
        testObject = this;
    }

    @Test
    public void test3() {
        assertThat(this, is(not(sameInstance(testObject))));
        testObject = this;
    }
}</code></pre>
<p>sameInstatnce: 동일성을 비교한다.
예시의 결과로 알 수 있는점은 JUnit 이 @Test 메서드 마다 객체를 생성한다는 것이다.</p>
<h3 id="버그-테스트">버그 테스트</h3>
<p>코드에 오류가 있을 때 그 오류를 가장 잘 드러내줄 수 있는 테스트를 말한다.</p>
<p>QA 과정, 또는 사용자 버그가 발생한 경우에 무턱대로 코드를 뒤져가면서 수정하려고 하기보다는 먼저 버그 테스트를 만들어보는 편이 유용하다.</p>
<h3 id="장점">장점</h3>
<ol>
<li>테스트의 완성도를 높여준다.</li>
<li>버그의 내용을 명확하게 분석하게 해준다.</li>
<li>기술적인 문제를 해결하는 데 도움이 된다.</li>
</ol>
<h4 id="동등분할">동등분할</h4>
<p>같은 결과를 내는 값의 범위를 구분해서 각 대표 값으로 테스트를 하는 방법을 말한다. 어떤 작업의 결과의 종류가 true, false 또는 예외발생 세 가지라면 각 결과를 내는 입력 값이나 상황의 조합을 만들어 모든 경우에 대한 테스트를 해보는 것이 좋다.</p>
<h4 id="경계값-분석">경계값 분석</h4>
<p>에러는 동등분할 범위의 경계에서 주로 많이 발생한다는 특징을 이용해서 경계의 근처에 있는 값을 이용해 테스트하는 방법이다. 보통 숫자의 입력 값인 경우 0 이나 그 주변 값 또는 정수의 최대값, 최소값 등으로 테스트해보면 도움이 될 때가 많다.</p>
<h2 id="정리">정리</h2>
<ul>
<li>테스트는 자동화돼야하고, 빠르게 실행할 수 있어야 한다. → 검증을 개발자가 아닌 로직하도록 함, 웹 테스트 보다는 작은 단위로 테스트해서 즉각적인 피드백을 받도록 한다.</li>
<li>main 메서드 대신 JUnit 프레임워크를 이용한 테스트 작성이 편리하다. → 여러 편의 메서드를 지원한다.</li>
<li>테스트 결과는 일관성이 있어야 한다. 코드의 변경 없이 환경이나 테스트 실행 순서에 따라서 결과가 달라지면 안 된다. → DB 데이터를 완전 지워줌 → 테스트 DB 분리</li>
<li>테스트는 포괄적으로 작성해야 한다. 충분한 검증을 하지 않는 테스트는 없는 것보다 나쁠 수 있다.</li>
<li>코드 작성과 테스트 수행 간격이 짧을수록 효과적이다.</li>
<li>테스트하기 쉬운 코드가 좋은 코드다. → 하나의 기능만을 가진 코드를 작성</li>
<li>테스트를 먼저 만들고 테스트를 성공시키는 코드를 만들어가는 테스트 주도 개발 방법도 유용하다.</li>
<li>테스트 코드도 애플리케이션 코드와 마찬가지로 적절한 리팩토링이 필요하다.</li>
<li>@Before, @After를 사용해서 테스트 메소드들의 공통 준비 작업과 정리 작업을 처리할 수 있다.</li>
<li>스프링 테스트 컨텍스트 프레임워크를 이용하면 테스트 성능을 향상시킬 수 있다.</li>
<li>동일한 설정파일을 사용하는 테스트는 하나의 애플리케이션 컨텍스트를 공유한다.</li>
<li>@Autowired 를 사용하면 컨텍스트의 빈을 테스트 오브젝트에 DI 할 수 있다.</li>
<li>기술의 사용 방법을 익히고 이해를 돕기 위해 학습 테스트를 작성하자.</li>
<li>오류가 발견될 경우 그에 대한 버그 테스트를 만들어두면 유용하다.</li>
</ul>
<h3 id="세줄-정리">세줄 정리</h3>
<ul>
<li><p>spring 사용자들아 웹 서비스 만들때 웹 테스트 같이 덩어리로 테스트 하지말고 좀 더 작은 단위로 테스트해라.</p>
</li>
<li><p>단위 테스트할때 JUnit 프레임워크 써.</p>
</li>
<li><p>TDD 한번 해봐.</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[ExceptionHandler 에서 @CookieValue 를 사용할 수 없는 이유]]></title>
            <link>https://velog.io/@0_0_yoon/ExceptionHandler-%EC%97%90%EC%84%9C-CookieValue-%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%A0-%EC%88%98-%EC%97%86%EB%8A%94-%EC%9D%B4%EC%9C%A0</link>
            <guid>https://velog.io/@0_0_yoon/ExceptionHandler-%EC%97%90%EC%84%9C-CookieValue-%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%A0-%EC%88%98-%EC%97%86%EB%8A%94-%EC%9D%B4%EC%9C%A0</guid>
            <pubDate>Sat, 17 Sep 2022 01:29:32 GMT</pubDate>
            <description><![CDATA[<h3 id="문제상황">문제상황</h3>
<p><img src="https://velog.velcdn.com/images/0_0_yoon/post/31fe6c0f-8974-46a3-b84e-903773eab8ad/image.png" alt="">
RefreshToken 이 유효하지 않은 경우 예외를 던지고 쿠키가 삭제되도록 구현했다.
Controller 에서 사용했던 것 처럼 @CookieValue 를 사용해 쿠키를 인자로 받으려고 했지만 해당 테스트가 계속 실패했다.(해당 ExceptionHandler가 동작하지 않았다)</p>
<pre><code class="language-java">    @ExceptionHandler(RefreshTokenNotFoundException.class)
    public ResponseEntity&lt;ExceptionResponse&gt; handleRefreshTokenNotFoundException(final UnauthorizedException e,
                                                                                 final HttpServletResponse response,
                                                                                 @CookieValue(&quot;refreshToken&quot;) final Cookie cookie) {
        cookie.setMaxAge(0);
        response.addCookie(cookie);
        return handleUnauthorizedException(e);
    }</code></pre>
<h3 id="원인">원인</h3>
<p>Controller 와 다르게 ExceptionHandler 에서는 @CookieValue를 지원하는 ServletCookieValueMethodArgumentResolver 를 가지고 있지 않다. 그래서 Cookie 로 바인딩하려고 할 때 처리할 ArgumentResolver 를 찾을 수 없어서 테스트가 실패했던 것이다.(아래 사진 참고)</p>
<p><img src="https://velog.velcdn.com/images/0_0_yoon/post/1424b275-42a6-4834-b4a5-6fc9a5e1ed64/image.png" alt="">
아래 코드와 같이 ExceptionHandler 는 Controller 처럼 다양한 argumentResolver 들을 지원해 주지 않는다.</p>
<pre><code class="language-java">// ExceptionHandlerExceptionResolver
protected List&lt;HandlerMethodArgumentResolver&gt; getDefaultArgumentResolvers() {
        List&lt;HandlerMethodArgumentResolver&gt; resolvers = new ArrayList&lt;&gt;();

        // Annotation-based argument resolution
        resolvers.add(new SessionAttributeMethodArgumentResolver());
        resolvers.add(new RequestAttributeMethodArgumentResolver());

        // Type-based argument resolution
        resolvers.add(new ServletRequestMethodArgumentResolver());
        resolvers.add(new ServletResponseMethodArgumentResolver());
        resolvers.add(new RedirectAttributesMethodArgumentResolver());
        resolvers.add(new ModelMethodProcessor());

        // Custom arguments
        if (getCustomArgumentResolvers() != null) {
            resolvers.addAll(getCustomArgumentResolvers());
        }

        // Catch-all
        resolvers.add(new PrincipalMethodArgumentResolver());

        return resolvers;
    }</code></pre>
<pre><code class="language-java">// RequestMappingHandlerAdapter
private List&lt;HandlerMethodArgumentResolver&gt; getDefaultArgumentResolvers() {
    List&lt;HandlerMethodArgumentResolver&gt; resolvers = new ArrayList&lt;&gt;(30);

    // Annotation-based argument resolution
    resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), false));
    resolvers.add(new RequestParamMapMethodArgumentResolver());
    resolvers.add(new PathVariableMethodArgumentResolver());
    resolvers.add(new PathVariableMapMethodArgumentResolver());
    resolvers.add(new MatrixVariableMethodArgumentResolver());
    resolvers.add(new MatrixVariableMapMethodArgumentResolver());
    resolvers.add(new ServletModelAttributeMethodProcessor(false));
    resolvers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice));
    resolvers.add(new RequestPartMethodArgumentResolver(getMessageConverters(), this.requestResponseBodyAdvice));
    resolvers.add(new RequestHeaderMethodArgumentResolver(getBeanFactory()));
    resolvers.add(new RequestHeaderMapMethodArgumentResolver());
    resolvers.add(new ServletCookieValueMethodArgumentResolver(getBeanFactory()));
    resolvers.add(new ExpressionValueMethodArgumentResolver(getBeanFactory()));
    resolvers.add(new SessionAttributeMethodArgumentResolver());
    resolvers.add(new RequestAttributeMethodArgumentResolver());

    // Type-based argument resolution
    resolvers.add(new ServletRequestMethodArgumentResolver());
    resolvers.add(new ServletResponseMethodArgumentResolver());
    resolvers.add(new HttpEntityMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice));
    resolvers.add(new RedirectAttributesMethodArgumentResolver());
    resolvers.add(new ModelMethodProcessor());
    resolvers.add(new MapMethodProcessor());
    resolvers.add(new ErrorsMethodArgumentResolver());
    resolvers.add(new SessionStatusMethodArgumentResolver());
    resolvers.add(new UriComponentsBuilderMethodArgumentResolver());
    if (KotlinDetector.isKotlinPresent()) {
        resolvers.add(new ContinuationHandlerMethodArgumentResolver());
    }

    // Custom arguments
    if (getCustomArgumentResolvers() != null) {
        resolvers.addAll(getCustomArgumentResolvers());
    }

    // Catch-all
    resolvers.add(new PrincipalMethodArgumentResolver());
    resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), true));
    resolvers.add(new ServletModelAttributeMethodProcessor(true));

    return resolvers;
}</code></pre>
<h3 id="해결">해결</h3>
<p>@CookieValue 대신 스프링에서 지원하는 WebUtils 를 사용해서 쿠키값을 가져왔다.</p>
<pre><code class="language-java">    @ExceptionHandler(RefreshTokenNotFoundException.class)
    public ResponseEntity&lt;ExceptionResponse&gt; handleRefreshTokenNotFoundException(final UnauthorizedException e,
                                                                                 final HttpServletRequest request,
                                                                                 final HttpServletResponse response) {
        final Cookie cookie = WebUtils.getCookie(request, &quot;refreshToken&quot;);
        cookie.setMaxAge(0);
        response.addCookie(cookie);
        return handleUnauthorizedException(e);
    }</code></pre>
<h3 id="추가">추가</h3>
<p>ExceptionHandler 와 같이 쓰이는 ControllerAdvice 는 어디서 동작할까?
스프링에서 예외가 발생하면(Dispatcher Servlet 이 Handler 를 찾고 실행시키는 과정에서 예외가 발생하는 경우) HandlerExceptionResolver 가 예외를 처리한다.</p>
<p><img src="https://velog.velcdn.com/images/0_0_yoon/post/47a65484-62e6-4ae3-98d0-6cd9c91fe9c4/image.png" alt=""></p>
<p>아래 사진과 같이 HandlerExceptionResolver 중에서 HandlerExceptionResolverComposite 안에 있는 ExceptionHandlerAdviceCache 에 있다.
<img src="https://velog.velcdn.com/images/0_0_yoon/post/5d2610b4-3f0a-46c3-ba38-e3c3db5e5546/image.png" alt="">
정리하면 HandlerExceptionResolvers(ArrayList) 는 DefaultErrorAttributes 와 HandlerExceptionResolverComposite(ArrayList)을 가지고 있다. 이 중 HandlerExceptionResolverComposite 안에 있는 Resolver(ExceptionHandlerExceptionResolver) 중 ExceptionHandlerAdviceCache(LinkedHashMap)에 들어있다.
<img src="https://velog.velcdn.com/images/0_0_yoon/post/27c1b087-0dbf-4e71-aa4d-fe8e2ed6fd40/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[HTTP Cache]]></title>
            <link>https://velog.io/@0_0_yoon/HTTP-Cache</link>
            <guid>https://velog.io/@0_0_yoon/HTTP-Cache</guid>
            <pubDate>Sat, 03 Sep 2022 19:02:30 GMT</pubDate>
            <description><![CDATA[<p>우테코의 HTTP 수업 내용과 실제 스프링에 적용하는 과정을 정리했다.</p>
<h3 id="캐시의-생명-주기cache-control">캐시의 생명 주기(cache-control)</h3>
<p><img src="https://velog.velcdn.com/images/0_0_yoon/post/b744f41a-8e54-4a8a-ae6e-e92393f83332/image.png" alt=""></p>
<p><strong>no-cache(강제 재검증) vs no-store(캐싱을 하지않음)</strong></p>
<p>cache-control에 no-cache 로 설정하면 캐시를 사용하지만 항상 서버(프록시서버, 리버스 프록시서버, CDN, 서버)에 재검증 요청을 보내게된다.</p>
<p><img src="https://velog.velcdn.com/images/0_0_yoon/post/eacfb003-c215-4a98-9a4c-4cafd2dbf7fb/image.png" alt=""></p>
<p>이런 강제 재검증을 해야하는 경우 max-age=0, must-revalidate 를 사용하는 경우도 있다. HTTP1.1 이전 버전에서는 no-cache를 제대로 처리를 못했기 때문에 사용했던 방법이다. </p>
<p>no-store 는 아예 캐싱하지 않겠다는 설정이다. 브라우저에서 백 포워드 캐시를 활용하지 못한다.(페이지 뒤로가기, 앞으로가기)</p>
<p>no-store 보다는 no-cache 사용을 추천하며 no-cache와 private을 같이 사용하면 클라이언트에서는 항상 최신 버전을 유지할 수 있다.
</br>
<strong>private vs public</strong></p>
<p>cache-control에 private 으로 설정하게 되면 오로지 웹 브라우저에서만 캐싱할 수 있음을 나타낸다. 중간 서버에서는 캐싱하지 않는다.</p>
<p> 사용시 주의할 점은 서버의 응답에 authorization 헤더가 있으면 private Cache에 저장되지 않는다.</p>
<p>public는 중간 서버에 캐싱할 수 있음을 나타낸다.
</br>
<strong>캐시의 유효기간(max-age)</strong></p>
<p>최대 설정할 수 있는 기간은 1년이다.</p>
<p>캐시 유효기간을 표시할때 Expires를 쓰는 경우가 있지만 HTTP1.1 이전에 쓰던 방식이다.</p>
<p><strong>s-maxage</strong></p>
<p>중간 서버에서만 적용되는 max-age 값을 설정하기 위해 s-maxage 값을 사용할 수 있다.</p>
<p>만약 s-maxage=31536000, max-age=0 과 같이 설정하면 CDN에서는 1년동안 캐시되지만 브라우저에서는 매번 재검증 요청을 보내도록 설정할 수 있다.</p>
<p>이때 s-maxage가 유효하다면 클라이언트에서는 항상 CDN 에 재검증을 요청할 것이다.</p>
<p>서버에서 배포할 일이 생긴다면 CDN invalidation을 수행해서 캐시를 지운뒤에 새로운 버전을 CDN에 캐싱한다.</p>
<p>CDN을 사용한다면 s-maxage를 사용해서 서버의 부하를 줄일수 있다.(재검증 요청을 CDN에서 받는다) 다만 서버에서 배포할 일이 생긴다면 직접 CDN invalidation을 수행해서 CDN에 최신 버전에 대한 캐싱이 이뤄지도록 해야한다.
</br></p>
<p><strong>재검증(Conditional Request)</strong></p>
<p>캐시의 유효기간이 만료됐다면 클라이언트는 재검증 요청을 보낸뒤 기존 캐시를 재사용할지 결정한다. 이 때 두가지 방법으로 재검증을 할 수 있다.</p>
<p><strong>If-Modified-Since vs ETag/If-None-Match</strong></p>
<p>If-Modified-Since는 Last-Modifed를 비교해서 유효성 검증을 한다. 이때 Last-Modifed는 초 단위로 헤더에 저장되기 때문에 간발의 차이로 서버에서 최신 데이터를 받아오지 못하는 경우가 생길 수 있다. Etag 헤더는 해시 값을 저장하므로 나노초와 같이 더 미세한 간격까지 처리할 수 있다.</p>
<p>ETag/If-None-Match는 시간이 아닌 해시값으로 유효성 검증을 하기 때문에 클라이언트는 항상 최신 데이터를 빠르게 받아올 수 있다.</p>
<p>Last-Modified 와 ETag 를 같이 쓰는 경우가 있다. 이 경우 재검증을 할때 당연히 ETag를 사용하며 Last-Modified는 캐싱 외에 크롤러에게 마지막 수정 시간을 알려주어 크롤링 빈도를 조정할 수 있다.
</br></p>
<h3 id="휴리스틱-캐싱">휴리스틱 캐싱</h3>
<p>만약 응답에 Cache-Control, Expires 헤더가 들어있지 않다면 브라우저는 휴리스틱을 사용하여 자체적으로 캐시 유효기간을 계산한다.</p>
<p>휴리스틱 캐싱을 사용하지 않는 이유는 서버입장에서 휴리스틱 캐싱이 이뤄진 브라우저의 해당 캐시를 제거할 방법이 없다. 즉 클라이언트가 서버에서 최신 데이터를 받아올 수 없게 된다.</p>
<p>즉 효과적으로 캐시를 사용하기 위해서는 Cache-Control를 사용해서 휴리스틱 캐싱이 발생하지 않도록 한다.
</br></p>
<h3 id="캐시-무효화cache-busting">캐시 무효화(Cache Busting)</h3>
<p>캐시 무효화란 브라우저가 캐시에서 이전 파일을 검색하지 않고 서버에 새 파일을 요청하도록 하는 행위를 말한다.</p>
<p>js, css 같은 정적 파일의 캐시 유효기간을 1년으로 설정한다. 캐시는 URL별로 관리되기 때문에 URL이 바뀌지 않는 이상 브라우저는 서버에 요청을 보내지 않는다.</p>
<p>만약 js, css 파일이 수정되면 서버에서 URL에 버저닝을 더해준다.</p>
<p>클라이언트에서는 Main resources(html)에 대해 항상 재검증 요청을 보낸다. html 파일에 태그돼 있는 새로운 버전의 정적 파일을 캐싱하게 된다. </p>
<p>Main resources는 no-cache, private 으로 관리해서 항상 서버의 최신 버전을 바라볼 수 있도록 설정하고 js, css 같은 정적파일들은 캐시 유효기간을 최대한 길게 설정함으로써 캐시를 효율적으로 관리할 수 있다. </p>
<p>CDN을 사용할 경우(public) s-maxage를 이용해서 기존에 서버가 받던 재검증 요청을 CDN이 받도록 설정할 수 있다. 다만 새로운 버전이 배포될 경우 직접 CDN invalidation을 해줘야한다.</p>
<h3 id="spring에-적용해보기">Spring에 적용해보기</h3>
<p><strong>Cache-Control 설정하는 법</strong></p>
<ul>
<li><p>컨트롤러에서 캐시 제어</p>
<ul>
<li><code>ResponseEntity</code> 사용</li>
</ul>
<pre><code class="language-java">@GetMapping(&quot;/home/{name}&quot;)
@ResponseBody
public ResponseEntity&lt;String&gt; hello(@PathVariable String name) {
    CacheControl cacheControl = CacheControl.noCache()
    return ResponseEntity.ok()
      .cacheControl(cacheControl)
      .body(&quot;name: &quot; + name);
}</code></pre>
</br>

<ul>
<li><code>HttpServletResponse</code> 사용(View name 응답)<pre><code class="language-java">@GetMapping(value = &quot;/home/{name}&quot;)
public String home(@PathVariable String name, final HttpServletResponse response) {
String cacheControl = CacheControl.noCache().getHeaderValue();
response.addHeader(&quot;Cache-Control&quot;, cacheControl);
return &quot;home&quot;;
}</code></pre>
</br>
</li>
</ul>
</li>
<li><p>정적 리소스(js, css)에 대한 캐시 제어</p>
<ul>
<li><p>ResourceHandler</p>
<pre><code>Spring Boot에서는 기본적으로 정적 리소스 요청을 처리할 수 있는 `ResourceHttpRequestHandler` 를 제공한다.</code></pre><p>  이 핸들러는 클래스 경로에 있는 /static, /public, /resources, /META-INF/resources 디렉토리에서 정적 콘텐츠를 제공한다.</p>
<ul>
<li><a href="https://github.com/spring-projects/spring-framework/blob/main/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandler.java">https://github.com/spring-projects/spring-framework/blob/main/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandler.java</a></li>
</ul>
<ul>
<li><a href="https://www.baeldung.com/spring-mvc-static-resources">https://www.baeldung.com/spring-mvc-static-resources</a></br>

</li>
</ul>
<p><code>WebMvcConfigurer</code> 의 <code>addResourceHandlers</code> 메서드 재정의</p>
<p>URL 패턴을 구성하고 파일의 특정 위치에 매핑하기만 하면 된다.
둘 이상의 위치에서 리소스를 찾으려면 <code>addResourceLocations</code> 메서드를 통해서 여러 위치를 포함시킬 수 있다.</p>
<pre><code class="language-java">@Override
public void addResourceHandlers(final ResourceHandlerRegistry registry) {
  registry.addResourceHandler(&quot;/resources/**&quot;).addResourceLocations(&quot;/resources/&quot;)
    .setCacheControl(CacheControl.maxAge(Duration.ofDays(365)));
}</code></pre>
</br></li>
</ul>
</li>
<li><p>인터셉터에서 캐시 제어</p>
<p> <code>WebContentInterceptor</code> 를 사용</p>
<pre><code class="language-java"> @Override
 public void addInterceptors(InterceptorRegistry registry) {
     WebContentInterceptor interceptor = new WebContentInterceptor();
     interceptor.addCacheMapping(CacheControl.noCache().cachePrivate(), &quot;/login/*&quot;);
     registry.addInterceptor(interceptor);
 }</code></pre>
</li>
</ul>
<p><a href="https://www.baeldung.com/spring-mvc-cache-headers">https://www.baeldung.com/spring-mvc-cache-headers</a>
    </br></p>
<p><strong>Etag 설정하는 법</strong></p>
<ul>
<li><p><code>ShallowEtagHeaderFilter</code> 사용</p>
<ul>
<li><p>Filter를 등록하는 3가지 방법</p>
<ol>
<li>spring의 @Compoent 사용<ul>
<li>URL 패턴 매핑이 불가하다.</li>
</ul>
<ol start="2">
<li>servelt의 @WebFilter 사용</li>
</ol>
<ul>
<li>필터가 특정 URL 패턴에만 적용되도록 할 수 있다.</li>
<li>순서는 지정할 수 없다.</li>
</ul>
<ol start="3">
<li>FilterRegistrationBean 사용</li>
</ol>
<ul>
<li>config 파일을 만들어서 사용한다.</li>
<li>URL 패턴, 순서 설정이 가능하다.
Filter를 세밀하게 다룰때 FilterRegistrationBean를 이용하면 편리하다.</br>
FilterRegistrationBean 을 통해서 ShallowEtagHeaderFilter를 빈으로 등록한다.

</li>
</ul>
</li>
</ol>
<pre><code class="language-java">  @Bean
  public FilterRegistrationBean&lt;ShallowEtagHeaderFilter&gt; shallowEtagHeaderFilter() {
      final FilterRegistrationBean&lt;ShallowEtagHeaderFilter&gt; registration = new FilterRegistrationBean&lt;&gt;();
      registration.setFilter(new ShallowEtagHeaderFilter());
      registration.addUrlPatterns(&quot;resources/static/*&quot;);
      return registration;
  }</code></pre>
<p>ShallowEtagHeaderFilter는 <code>DigestUtils</code> 를 사용해서 eTag 값을 생성하는데 이때 MD5 해싱 알고리즘을 사용한다. </p>
<pre><code class="language-java">protected String generateETagHeaderValue(InputStream inputStream, boolean isWeak) throws IOException {
      // length of W/ + &quot; + 0 + 32bits md5 hash + &quot;
      StringBuilder builder = new StringBuilder(37);
      if (isWeak) {
          builder.append(&quot;W/&quot;);
      }
      builder.append(&quot;\&quot;0&quot;);
      DigestUtils.appendMd5DigestAsHex(inputStream, builder);
      builder.append(&#39;&quot;&#39;);
      return builder.toString();
  }</code></pre>
</br>


</li>
</ul>
</li>
</ul>
<ul>
<li><p>ResponseEntity의 eTag() 메서드 사용</p>
<pre><code class="language-java">  @GetMapping(&quot;/etag&quot;)
  public ResponseEntity etag() {
      return ResponseEntity.ok()
              .eTag(&quot;eTag&quot;)
              .build();
  }</code></pre>
</li>
</ul>
<p><a href="https://www.baeldung.com/etags-for-rest-with-spring">https://www.baeldung.com/etags-for-rest-with-spring</a></p>
</br>

<p><strong>응답 압축하기</strong></p>
<p>   HTTP 응답을 압축하면 웹 사이트의 성능을 높일 수 있다.</p>
<p>   스프링 부트 설정을 통해 gzip과 같은 HTTP 압축 알고리즘을 적용시킬 수 있다.</p>
<p>   HTTP response Compression은 Jetty, Tomcat, Reacor Netty, Undertow 에서 지원된다.</p>
<p>   설정 방법</p>
<pre><code class="language-jsx">    // application.properties 
    server:
      compression:
        enabled: true</code></pre>
<p>  응답 길이와 content-type 에 대해서 기본으로 설정된 값이 있다.
응답 길이는 2048 바이트 이상일것, content-type은 text/html, text/xml, text/plain, text/css, text/javascript, application/javascript, application/json, application/xml 의 경우에 압축이 이루어진다.</p>
<p> 각각의 설정은 아래의 방법으로 설정할 수 있다. 
         - server: compression: min-response-size: {size}
    - server: compression: mime-types: {content-type}</p>
<p>공부해볼것: 구버전의 js, css 파일들은 어떻게 관리될까?<br></br>
참고: 구구강의
      우테코 크루 클레이, 블링, 필즈와의 대화
    <a href="https://toss.tech/article/smart-web-service-cache">https://toss.tech/article/smart-web-service-cache</a>
    <a href="https://paulcalvano.com/2018-03-14-http-heuristic-caching-missing-cache-control-and-expires-headers-explained/">https://paulcalvano.com/2018-03-14-http-heuristic-caching-missing-cache-control-and-expires-headers-explained/</a></p>
]]></description>
        </item>
    </channel>
</rss>