<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>곰팡이 먼지연구소</title>
    <link>https://gompangs.tistory.com/</link>
    <description>Java Backend Developer</description>
    <language>ko</language>
    <pubDate>Sun, 26 Jul 2026 21:52:01 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>@곰팡</managingEditor>
    <image>
      <title>곰팡이 먼지연구소</title>
      <url>https://tistory1.daumcdn.net/tistory/1958377/attach/f0214068c35e4ba8b910307400d6989a</url>
      <link>https://gompangs.tistory.com</link>
    </image>
    <item>
      <title>Spring Redis Template Transaction</title>
      <link>https://gompangs.tistory.com/entry/Spring-Redis-Template-Transaction</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Spring에서 사용하는 Transaction을 RedisTemplate에도 쓰고 싶을 때가 있는데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;Redis에서 지원하는 Transaction의 구현이 기존에 익히 알고있던 RDBMS(MySQL, MS-SQL...) 과는 동작이 달라서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정확한 동작방식을 알고 있어야 로직을 짜는데 도움이 될 것 같아서 작성해본다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Trsnaction&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;익히 알고있는 트랜잭션은 데이터의 불일치를 방지하고, 트랜잭션 내에 있는 로직이 원자성으로 실행됨을 보장해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예컨데 아래와 같은 로직이 있다고 치자.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;@Transactional
    void logic(User user, List&amp;lt;Role&amp;gt; roles){
        userService.save(user);
        roleService.save(roles);
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;User 엔티티와 Role 엔티티는 1:N 관계라 가정하고,&lt;br /&gt;User가 저장될 때, Role도 저장되어야 한다고 해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 위와 같은 코드에서는 user와 roles 저장에 대한 atomic operation이 보장이 된다.&lt;br /&gt;즉, user는 저장이 됐는데 roles가 저장이 안됐을 경우&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;user에 대한 저장부분을 rollback 하는 로직으로 구성이 되어 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대부분의 RDBMS에서는 트랜잭션 관련된 명령을 지원하는데, MySQL 기준으로 정상 동작 시 COMMIT 명령을 수행하고&lt;br /&gt;로직 상 Unchecked Exception이 발생하게 되면 rollback을 수행하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 코드는 사실상 아래와 같이 동작한다고 봐도 된다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;-- BEGIN TRANSACTION
@Transactional
    void logic(User user, List&amp;lt;Role&amp;gt; roles){
        userService.save(user); -- COMMIT
        roleService.save(roles); -- COMMIT
    }
-- END TRANSACTION&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 항상 트랜잭션의 전파범위(propagation) 와 격리레벨(isolation)을 잘 지정해주어야&lt;br /&gt;비즈니스 로직 상 문제가 발생했을 때 어느범위까지 롤백할 것인지를 정할 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Redis Transaction&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RDBMS 와는 달리 Redis 에도 Transaction을 보장할 수 있는 방법이 제공되는데, 좀 유의해야할 사항이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RedisTemplate를 사용한 Transaction은 롤백을 지원하지 않는다.&lt;/p&gt;
&lt;pre class=&quot;gams&quot;&gt;&lt;code&gt;MULTI
SET keyA &quot;A&quot;
SET keyB &quot;B&quot;
EXEC&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;와 같은 명령을 전송하게 되면 레디스에서는 A와 B에 대한 key set 명령어를 한번에 처리해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;고려해야 할 부분은, rollback이 명시적으로 없기 때문에 값을 반영한 뒤 특정 부분만 롤백이 안된다는 부분이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis Command 중 &lt;code&gt;MULTI-EXEC&lt;/code&gt; 라는 구문의 명령을 제공하는데, 이는 여러개의 명령 실행의 원자성을 지켜주는 명령어이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;방법은 2가지가 있는데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) 직접 RedisTemplate의 multi-exec 구문을 작성하는 방법&lt;/p&gt;
&lt;pre class=&quot;processing&quot;&gt;&lt;code&gt;        Object txResults = redisTemplate.execute(new SessionCallback&amp;lt;List&amp;lt;Object&amp;gt;&amp;gt;() {
            public List&amp;lt;Object&amp;gt; execute(RedisOperations operations) throws DataAccessException {
                operations.multi();
                operations.opsForSet().add(&quot;key&quot;, &quot;value1&quot;);

                // This will contain the results of all operations in the transaction
                return operations.exec();
            }
        });&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2) &lt;code&gt;@Transactional&lt;/code&gt; 어노테이션으로 method에 걸어주는 방법.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2번을 주로 사용하게 될 것인데, 상황에 따라 직접 제어가 필요하게 되면 1번 또한 꼭 쓰게되는 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1번을 기준으로 설명을 하게 되면 예컨데 또 예를 들어서 아래와 같은 코드가 있다고 가정하자.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;@Transactional
    void updateUser(User user, Role role){
        redisTemplate.opsForValue().set(user.getId(), user);
        redisTemplate.opsForValue().set(role.getId(), role);
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드를 실행하면 아마도 아래와 같은 Redis Command가 날아갈 것이다&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;MULTI
SET userId Object(user)
SET roleId Object(role)
EXEC&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 원하던대로 잘 동작을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, 트랜잭션 범위 내에 데이터를 가져오는 비즈니스 로직이 섞여있다면 어떨까?&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;    @Transactional
    void updateUser(User user, Role role) {
        User existsUser = redisTemplate.opsForValue().get(user.getId());
        if (existsUser == null) throw new UserPrincipalNotFoundException(user.getId());

        redisTemplate.opsForValue().set(user.getId(), user);

        redisTemplate.opsForValue().set(role.getId(), role);
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 비즈니스 로직 상 검증하는 부분의 로직이 들어가게 된다면 과연 제대로 동작할까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 실행해보면 계속 &lt;code&gt;UserPrincipalNotFoundException&lt;/code&gt;이 발생할 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis에 데이터가 정확히 존재하는데도 말이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜그럴까? 코드를 뜯어보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;    /**
     * Get the value of {@code key}.
     *
     * @param key must not be {@literal null}.
     * @return {@literal null} when used in pipeline / transaction.
     * @see &amp;lt;a href=&quot;https://redis.io/commands/get&quot;&amp;gt;Redis Documentation: GET&amp;lt;/a&amp;gt;
     */
    @Nullable
    V get(Object key);&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;return {@literal null} when used in pipeline / transaction.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분을 간과하게 되면 로직이 잘 맞다고 생각하는 데도 불구하고 계속적인 value의 null 문제를 겪을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션 범위에 있는 get 메서드는 null을 반환한다고 되어 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 null을 반환할까? 이는 아까 위에서 언급했던 &lt;code&gt;multi-exec&lt;/code&gt; 구문과 연관되어 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring에서 Redis Transaction 처리를 하게 되면 메서드 전체에 &lt;code&gt;multi-exec&lt;/code&gt;가 걸리게 되고&lt;br /&gt;그 사이에 get을 해온다면 exec 구문이 끝난 뒤에 return이 되기 때문에 의미가 없어져서 null을 리턴한다고 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 Redis Transaction을 다룰 때에는 Transaction의 propagation 및 내부 로직에 저렇게 무언가 get해서 검증하는 로직이 겹치지 않도록 미리 validate를 하고 원자성 보장이 필요한 update 혹은 insert 부분에만 transaction을 지정하는 게 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;propagation도 또한 중요한데, 아래와 같은 경우는 어떨까?&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// UserController
    @Transactional
    void updateUser(User user, Role role) {
        // processing incoming requests...
        userService.update(user, role);
    }

 ...

// UserService
    void update(){
        User existsUser = redisTemplate.opsForValue().get(user.getId());
        if (existsUser == null) throw new UserPrincipalNotFoundException(user.getId());
        redisTemplate.opsForValue().set(user.getId(), user);
        redisTemplate.opsForValue().set(role.getId(), role);
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분명 service에는 Transaction이 없기 때문에 당연히 정상동작 할 것이라고 예상하겠지만..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;상위 레벨에 Transaction이 걸려 있기에 이 또한 get command 시 null이 return된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드를 수정하자면 아래와 같겠다&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;    @Transactional
    void updateUser(User user, Role role) {
        // processing incoming requests...

        // validation
        User existsUser = redisTemplate.opsForValue().get(user.getId());
        if (existsUser == null) throw new UserPrincipalNotFoundException(user.getId());

        // update
        userService.update(user, role);
    }

    void update(){
        redisTemplate.opsForValue().set(user.getId(), user);
        redisTemplate.opsForValue().set(role.getId(), role);
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런식으로 Read 로직과 Write로직을 분리해야 생각한대로 코드가 동작할 수 있으니&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;Redis Transaction을 사용할 땐 참고하도록 하면 좋겠다.&lt;/p&gt;</description>
      <category>Development/Spring</category>
      <category>redis</category>
      <category>Redis Transaction</category>
      <category>RedisTemplate</category>
      <category>Spring</category>
      <category>spring transaction</category>
      <category>transaction</category>
      <author>@곰팡</author>
      <guid isPermaLink="true">https://gompangs.tistory.com/141</guid>
      <comments>https://gompangs.tistory.com/entry/Spring-Redis-Template-Transaction#entry141comment</comments>
      <pubDate>Thu, 9 Sep 2021 23:25:53 +0900</pubDate>
    </item>
    <item>
      <title>Spring R2DBC + MySQL</title>
      <link>https://gompangs.tistory.com/entry/Spring-R2DBC-MySQL</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;Spring R2DBC&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring 에서 사용되는 JDBC Datasource는 항상 Blocking 방식으로 제공되어졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 Query를 수행하고 그에 대한 결과를 받기까지는 항상 Blocking이 되어 해당 쓰레드가 대기하는 문제점이 존재한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Reactive Framework가 주로 사용되어 진지 꽤 지났음에도 불구하고 Spring에서 사용하는 Datasource는 아직도 예전의 Servlet의 쓰레드 모델에 멈춰있다는게 항상 답답함을 지울 수 없는 부분이었는데, 인큐베이팅 되던 모듈이 드디어 1.0.0 버전으로 릴리즈 되면서 Spring을 사용하면서도 Async-Nonblokcing Datasource를 함께 사용할 수 있다는 뜻이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 곧, Spring 5 Feature인 Webflux에서 성능적인 측면/Latency의 측면에서 꽤 큰 이점을 볼 수 있는 부분이 되겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WebFlux를 사용해도 Spring을 사용하는 주된 사용자들은 DB와의 연결을 걸어놓았을 텐데, DB가 Blocking 인 경우 결국 Non-Blocking의 이점을 하나도 못얻기 때문에 R2DBC의 이점은 크다고 말할 수 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;R2DBC는 다음과 같은 뜻을 내포하고 있다고 한다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/r2dbc/&quot;&gt;Reactive Relational Database Connectivity&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Features&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;R2DBC 드라이버는 &lt;code&gt;@Configuration&lt;/code&gt; 를 통해 Java-based 설정을 가능하게 한다(기존 Datasource 형태로 &lt;code&gt;@Bean&lt;/code&gt;등록)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DatabaseClient&lt;/code&gt; 는 POJO 형태로 Object Relation Mapping(ORM) 을 제공하며, 생산성 향상에 도움을 준다&lt;/li&gt;
&lt;li&gt;Exception 발생 시, Spring 에서 제공되는 Runtime Exception으로 자동 변환해줌
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기존에 제공되던 DataAccessException ... 등과 같은&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Spring 컨벤션에 사용되는 여러가지 Rich한 특성을 제공한다&lt;/li&gt;
&lt;li&gt;어노테이션 기반의 메타데이터를 매핑 가능하며, 확장이 가능하도록 한다&lt;/li&gt;
&lt;li&gt;커스텀 쿼리(&lt;code&gt;@Query&lt;/code&gt; 등과 같이) 를 포함한 Repository의 쿼리들(CRUD)을 자동으로 R2DBC 방식으로 구현해준다&lt;/li&gt;
&lt;li&gt;현재 구현된 RDB
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;MySQL의 경우 github인걸로 보아, 아직 Pivotal팀에서 제공되는 공식 드라이버는 없는 모양임&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/r2dbc/r2dbc-postgresql&quot;&gt;Postgres&lt;/a&gt; (&lt;code&gt;io.r2dbc:r2dbc-postgresql&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/r2dbc/r2dbc-h2&quot;&gt;H2&lt;/a&gt; (&lt;code&gt;io.r2dbc:r2dbc-h2&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/r2dbc/r2dbc-mssql&quot;&gt;Microsoft SQL Server&lt;/a&gt; (&lt;code&gt;io.r2dbc:r2dbc-mssql&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/mirromutth/r2dbc-mysql&quot;&gt;MySQL&lt;/a&gt; (&lt;code&gt;com.github.mirromutth:r2dbc-mysql&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/jasync-sql/jasync-sql&quot;&gt;jasync-sql MySQL&lt;/a&gt; (&lt;code&gt;com.github.jasync-sql:jasync-r2dbc-mysql&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Initialize&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래의 디펜던시를 &lt;code&gt;pom.xml&lt;/code&gt; 에 넣어주도록 하자&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;혹은, &lt;a href=&quot;https://start.spring.io/&quot;&gt;Spring Initializer&lt;/a&gt; 를 사용해서 R2DBC(Experimental) 을 골라서 프로젝트를 세팅해도 된다(이게 더 편함)&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필자는 Spring Boot(Reactive) + R2DBC + MySQL Driver +&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;dependencyManagement&amp;gt;
  &amp;lt;dependencies&amp;gt;
    &amp;lt;dependency&amp;gt;
      &amp;lt;groupId&amp;gt;io.r2dbc&amp;lt;/groupId&amp;gt;
      &amp;lt;artifactId&amp;gt;r2dbc-bom&amp;lt;/artifactId&amp;gt;
      &amp;lt;version&amp;gt;${r2dbc-releasetrain.version}&amp;lt;/version&amp;gt;
      &amp;lt;type&amp;gt;pom&amp;lt;/type&amp;gt;
      &amp;lt;scope&amp;gt;import&amp;lt;/scope&amp;gt;
    &amp;lt;/dependency&amp;gt;
  &amp;lt;/dependencies&amp;gt;
&amp;lt;/dependencyManagement&amp;gt;

&amp;lt;dependencies&amp;gt;

  &amp;lt;!-- other dependency elements omitted --&amp;gt;

  &amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.springframework.data&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring-data-r2dbc&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;1.0.0.BUILD-SNAPSHOT&amp;lt;/version&amp;gt;
  &amp;lt;/dependency&amp;gt;

  &amp;lt;!-- a R2DBC driver --&amp;gt;
  &amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;io.r2dbc&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;r2dbc-h2&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;Arabba-RELEASE&amp;lt;/version&amp;gt;
  &amp;lt;/dependency&amp;gt;

&amp;lt;/dependencies&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 &lt;code&gt;application.properties&lt;/code&gt; 에 아래 프로퍼티를 넣어서 쿼리를 디버깅해보자.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;logging.level.org.springframework.data.r2dbc=DEBUG&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;R2DBC용 데이터베이스 프로퍼티 설정을 해준다(DB는 위에 나열된 것 중 아무거나 세팅하고 접속.)&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 용이라면 H2가 가장 편리하긴 하다(필자는 MySQL 테스트를 위해 MySQL로 설정.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원래라면 익숙한.. &lt;code&gt;jdbc:mysql://&lt;/code&gt; ... 이 아닌 &lt;code&gt;r2dbc:mysql:&lt;/code&gt; ... 으로 시작한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;spring.r2dbc.url=r2dbc:mysql://localhost:3306/some_db?useUnicode=true&amp;amp;characterEncoding=utf8
spring.r2dbc.username=gompang
spring.r2dbc.password=gompang&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;까지 설정하게 되면 R2DBC로 DB연결을 완료할 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Sample Code&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에 테스트로 만들어놨던 데이터를 불러와보자. (DB 형태별로 상이하겠지만, 그냥 필자기준에서 테스트코드이다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;User 테이블을 불러올 예정이다&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;@Data
public class User {
    private String username;
    private String password;
    private String authorities;
    private boolean locked;
    private String ip;
    private String user_agent;
    private Date creation;
    private Date last_access;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Repository 를 하나 만들어준다&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;@Repository
public interface UserRepository extends R2dbcRepository&amp;lt;User, String&amp;gt; {
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Repository를 갖다 쓰는 Service를 또 하나 만들어주자&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Service
@Slf4j
public class UserService {

    private final UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public void test() {
        Flux&amp;lt;User&amp;gt; allUser = userRepository.findAll();
        List&amp;lt;User&amp;gt; blocked = allUser.collectList().block();
        blocked.forEach(item -&amp;gt; log.info(&quot;user : {}&quot;, item));
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하는일이 너무 간단하긴 한데, repository에서 모든 유저 데이터를 찾은 뒤, for-each로 하나씩 찍는부분이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단, 여기서 주목할 점이 findAll()할 경우 기존에 &lt;code&gt;Optional&amp;lt;List&amp;lt;User&amp;gt;&amp;gt;&lt;/code&gt; 혹은 &lt;code&gt;List&amp;lt;User&amp;gt;&lt;/code&gt; 로 넘어오던 부분이 &lt;code&gt;Flux&lt;/code&gt; 로 변경이 되었다(!)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 말은 즉슨, &lt;code&gt;WebFlux&lt;/code&gt;로 해당 데이터를 바로 내려줄 수도 있다는 뜻이며, &lt;code&gt;Flux&lt;/code&gt;가 제공하는 Rich한 기능들을 다 사용할 수 있다는 말이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 로직상에서는 block을 해서 결과를 다 기다리지만, 실제 내부 로직상 동작은 Async/Non-Blocking하게 데이터들을 가져오도록 구현이 되어 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기왕 보는김에 &lt;code&gt;SimpleR2dbcRepository&lt;/code&gt;의 &lt;code&gt;findAll()&lt;/code&gt;에 대해서 탐구해보자&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;public class SimpleR2dbcRepository&amp;lt;T, ID&amp;gt; implements ReactiveCrudRepository&amp;lt;T, ID&amp;gt; {
  ...
  public Flux&amp;lt;T&amp;gt; findAll() {
    return this.databaseClient.select().from(this.entity.getJavaType()).fetch().all();
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;databaseClient&lt;/code&gt; 는 현재 Spring property에 값을 주어서 자동으로 Auto-configurated되었는데, 원래는 아래와 같이 생성된다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;ConnectionFactory connectionFactory = ConnectionFactories.get(&quot;r2dbc:h2:mem:///test?options=DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE&quot;);

DatabaseClient client = DatabaseClient.create(connectionFactory);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트로 H2 DB를 연결하는 예제이고, &lt;code&gt;ConnectionFactory&lt;/code&gt; 기반으로 &lt;code&gt;DatabaseClient&lt;/code&gt; 를 생성하게 된다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부적인 method들이 다 Mono 혹은 Flux로 이루어져 있어서, 해당 기능들을 Wrapping해서 Repository로 제공해주도록 되어 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Performance&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 이게 실제로 JDBC에 비해 성능적으로 이점을 봐야 좋은것 아니겠는가.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 JDBC는 쿼리마다 Thread를 만들어서 수행해야하기 때문에 자원이 비효율적이라, 그것을 DBCP로 풀어낸 것인데(Tomcat, Hikari ... 등)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Reactive 하다는 것은 Async한 Task들을 EventLoop방식으로 Queue에 쌓아두고, Thread를 보다 효율적으로 사용하는 것으로 &lt;code&gt;자원&lt;/code&gt; 및 &lt;code&gt;성능&lt;/code&gt; 둘다 강점이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금은 자원에 대한 부분보다 &lt;code&gt;성능&lt;/code&gt; 이 얼마나 강점이 있나를 한번 확인해보자(자원같은 경우는 로컬PC에서 테스트하기엔 좀 어려운 부분이 있으니..)&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;측정 기준 : 소요시간 / TPS(초당 처리건수)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Prepare&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;10만건 Row를 랜덤으로 넣어버릴 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;saveAll()이 Flux이기 때문에 바로 종료되고 작업이 진행이 안되므로, block을 시켜서 다 들어가는지 확인한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라고 하여 성능테스를 진행하려 했으나, 진행중 문제가 발생한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;    @Test
    @Rollback(value = false)
    void put() {
        List&amp;lt;User&amp;gt; users = new ArrayList&amp;lt;&amp;gt;();
        log.info(&quot;Start&quot;);
        IntStream.range(0, 100000).forEach(i -&amp;gt; {
            User user = new User();
            user.setUsername(UUID.randomUUID().toString() + System.currentTimeMillis());
            user.setPassword(UUID.randomUUID().toString() + System.currentTimeMillis());
            user.setLocked(false);
            users.add(user);
        });
        log.info(&quot;End&quot;);
        Flux&amp;lt;User&amp;gt; userFlux = userRepository.saveAll(users);
        userFlux.collectList().block();
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에러가 발생한다&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;java.lang.IllegalStateException: Required identifier property not found for class dev.gompang.r2dbcsample.domains.User!&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;@Data
public class User {
    @Id
    private String username;
    private String password;
    private String authorities;
    private boolean locked;
    private String ip;
    private String user_agent;
    private Date creation;
    private Date last_access;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PK에 &lt;code&gt;@Id&lt;/code&gt; 어노테이션을 붙여준다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 위의 에러는 없어지는데, 이번에는 다른 문제가 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이상하게 Insert를 안하고 Update 를 수행하다 에러가 난다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;org.springframework.dao.TransientDataAccessResourceException: Failed to update table [user]. Row with Id [8e68bb90-f947-4028-b448-2217572dd8b41575798663957] does not exist.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;save()&lt;/code&gt; 를 뜯어보자.&lt;/p&gt;
&lt;pre class=&quot;livescript&quot;&gt;&lt;code&gt;return this.entity.isNew(objectToSave) ? this.databaseClient.insert().into(this.entity.getJavaType()).table(this.entity.getTableName()).using(objectToSave).map(this.converter.populateIdIfNecessary(objectToSave)).first().defaultIfEmpty(objectToSave) : this.databaseClient.update().table(this.entity.getJavaType()).table(this.entity.getTableName()).using(objectToSave).fetch().rowsUpdated().handle((rowsUpdated, sink) -&amp;gt; {
  ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;this.entity.isNew(objectToSave)&lt;/code&gt; 를 가지고 새로 생성되는 오브젝트인지, Update할 오브젝트인지 확인을 하는 모양이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PersistentEntityInformation에 구현이 되있는 isNew()를 보자.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;public boolean isNew(T entity) {
        return this.persistentEntity.isNew(entity);
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 persistentEntity를 봐야하겠다. 또 들어가보자&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;public class BasicPersistentEntity&amp;lt;T, P extends PersistentProperty&amp;lt;P&amp;gt;&amp;gt; implements MutablePersistentEntity&amp;lt;T, P&amp;gt; {
  ...
    public boolean isNew(Object bean) {
        this.verifyBeanType(bean);
        return ((IsNewStrategy)this.isNewStrategy.get()).isNew(bean);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 PersistentEntityIsNewStrategy의 isNew()가 있다(언제까지 들어갈거냐)&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;    public boolean isNew(Object entity) {
        Object value = this.valueLookup.apply(entity);
        if (value == null) {
            return true;
        } else if (this.valueType != null &amp;amp;&amp;amp; !this.valueType.isPrimitive()) {
            return value == null;
        } else if (Number.class.isInstance(value)) {
            return ((Number)value).longValue() == 0L;
        } else {
            throw new IllegalArgumentException(String.format(&quot;Could not determine whether %s is new! Unsupported identifier or version property!&quot;, entity));
        }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이쯤이면 정신이 몽롱해진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어찌된 영문인지 isNew() 에서 false가 나오니까 UPDATE를 수행하려고 하는 것 같다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;왜?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;궁금해서 R2DBC 깃헙 이슈로 올려서 확인해보고자 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/spring-projects/spring-data-r2dbc/issues/275#event-2947401404&quot;&gt;질문한 이슈&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1631198602198&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;TransientDataAccessResourceException(Failed to update table) issue &amp;middot; Issue #275 &amp;middot; spring-projects/spring-data-r2dbc&quot; data-og-description=&quot;Hi. I've some problem when I using R2DBC Spring boot version &amp;lt;parent&amp;gt; &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt; &amp;lt;artifactId&amp;gt;spring-boot-starter-parent&amp;lt;/artifactId&amp;amp;g...&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/spring-projects/spring-data-r2dbc/issues/275#event-2947401404&quot; data-og-url=&quot;https://github.com/spring-projects/spring-data-r2dbc/issues/275&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/XQ4c6/hyLyVxkkLO/7b94cTo63HECxJth5kTtS1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/spring-projects/spring-data-r2dbc/issues/275#event-2947401404&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/spring-projects/spring-data-r2dbc/issues/275#event-2947401404&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/XQ4c6/hyLyVxkkLO/7b94cTo63HECxJth5kTtS1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;TransientDataAccessResourceException(Failed to update table) issue &amp;middot; Issue #275 &amp;middot; spring-projects/spring-data-r2dbc&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Hi. I've some problem when I using R2DBC Spring boot version  org.springframework.boot spring-boot-starter-parent&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;답변 :&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Because you've provided the &lt;code&gt;@Id&lt;/code&gt;. The library needs to figure out, whether the row is new or whether it should exist. See &lt;a href=&quot;https://github.com/spring-projects/spring-data-r2dbc/issues/232&quot;&gt;#232&lt;/a&gt; and &lt;a href=&quot;https://github.com/spring-projects/spring-data-r2dbc/issues/253&quot;&gt;#253&lt;/a&gt; for further reference.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;@Id&lt;/code&gt; 를 줬기 때문이고, 이는 라이브러리에서 새로 생성되었는지 아니면 기존에 있었던건지를 파악해야 하기 때문이다. 라고 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 여기까지만 해도 잘 이해가 안갔는데, 결국 원인이라고 밝혀진건, 위의 #232랑 #253번 링크에서 밝혀졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미, 나와 같은 문제를 겪고 있던 사람들이 있었고 #253번 이슈를 참고해보면 답변이 아래와 같이 달려있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Thanks for report. The mentioned exception is signaled when a &lt;code&gt;save(&amp;hellip;)&lt;/code&gt; determines that the object is not new and if the &lt;code&gt;UPDATE&lt;/code&gt; statement executes without updating a row. We added this behavior with &lt;a href=&quot;https://github.com/spring-projects/spring-data-r2dbc/issues/232&quot;&gt;#232&lt;/a&gt; if an &lt;code&gt;UPDATE&lt;/code&gt; returns zero rows were affected by a change to avoid updates that do not exist on the database (e.g. object with an assigned Id is considered not new which is a common bug when someone assumes the row should be inserted).&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;We haven't considered that MySQL has slightly different behavior by default. The MySQL docs say:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;For &lt;code&gt;UPDATE&lt;/code&gt; statements, the affected-rows value by default is the number of rows actually changed. If you specify the &lt;code&gt;CLIENT_FOUND_ROWS&lt;/code&gt; flag to mysql_real_connect() when connecting to mysqld, the affected-rows value is the number of rows &amp;ldquo;found&amp;rdquo;; that is, matched by the &lt;code&gt;WHERE&lt;/code&gt; clause.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요약 : MySQL 드라이버에서 해당 기능을 지원하는지 확인해봐라. R2DBC에서 Insert와 Update를 판단하는 방법은, UDPATE를 했을 때 &lt;code&gt;zero rows&lt;/code&gt; 가 return 되면(즉, 업데이트 할 게 없다라는 뜻) Update를 수행하고 아니면 Insert를 하는 방식으로 구현이 되어 있던 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Found Row&lt;/code&gt;와 &lt;code&gt;Affected row&lt;/code&gt;는 틀리다. 검색된 데이터와, 실제 영향 받는 데이터가 틀리기 때문에 이러한 세부적인 구현에 문제가 있었던 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제가 되는 소스코드를 확인해보자. (&lt;a href=&quot;https://github.com/mirromutth/r2dbc-mysql/blob/v0.8.0.RELEASE/src/main/java/dev/miku/r2dbc/mysql/constant/Capabilities.java#L29&quot;&gt;링크&lt;/a&gt;)&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;public final class Capabilities {
  // 다른 소스코드는 중략

//    public static final int FOUND_ROWS = 2; // Return found rows instead of affected rows, should not enable this.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;FOUND_ROWS&lt;/code&gt; 라는 필드가 아예 주석처리 되어있다. 심지어 should not enable this 라고 적혀있기 까지..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 현재 사용했던 0.8.0.RELEASE버전에서 구현이 저렇게 되어 있었다..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면.. master branch 는 어떨까 해서 확인해보니&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;/**
 * Values for the capabilities flag bitmask used by the MySQL Client/Server Protocol.
 */
public final class Capabilities {
  // 다른 소스코드는 중략
    /**
     * Use found/touched rows instead of changed rows for affected rows, should enable this.
     */
    private static final int FOUND_ROWS = 2;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예? 주석처리 풀고, should enable this 라는 설명까지(?) 붙여있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;found/affected rows에 대한 기능을 지원하기 위해 준비중인 것으로 보인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼, 실제 master branch 를 fork해서(뭔가 점점 산으로 가고 있다.) &lt;code&gt;FOUND_ROWS&lt;/code&gt; 의 주석이 풀린버전을 사용해본다면 문제가 해결되는지 궁금해지기 시작했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;github에서 repo 를 fork 한 뒤, 최신버전 기준(0.8.1) 을 clone한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후, &lt;code&gt;mvn clean package&lt;/code&gt; 로 해당 프로젝트를 빌드해서 jar파일을 프로젝트에 추가한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자 이제 실행시켜보자&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;org.springframework.dao.TransientDataAccessResourceException: Failed to update table [user]. Row with Id [a0c8f013-13c8-4543-a71f-6e31b78794441579092477804] does not exist.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;여전하다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아까의 문제에서 아래와 같이 언급을 했는데, 저 상수를 enable하면 뭔가 될것같이 말했는데, 저 상수만 enable하는것이 아닌 R2DBC에서 또 옵션을 연동하는 작업을 해줘야 하나보다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;It should be enabled by default if this will affect Spring Data R2DBC behavior. I will change it soon.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2019/12/9에 will change it soon 이라고 말했으니.. 좀 더 기다려봐야 할 것 같다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;UPDATE(2020/11/30)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오랜만에 다시 dependency version up 후 다시 시도를 해보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Repository&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;public interface UserRepository extends R2dbcRepository&amp;lt;User, Integer&amp;gt; {
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;User Domain&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Data
public class User {
    @org.springframework.data.annotation.Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column
    private String name;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;으로 테스트해봤을때&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1023&quot; data-origin-height=&quot;314&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dI9sS6/btqOvQQChBS/ZERRePBIIdmKLi4SHynZY0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dI9sS6/btqOvQQChBS/ZERRePBIIdmKLi4SHynZY0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dI9sS6/btqOvQQChBS/ZERRePBIIdmKLi4SHynZY0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdI9sS6%2FbtqOvQQChBS%2FZERRePBIIdmKLi4SHynZY0%2Fimg.png&quot; data-origin-width=&quot;1023&quot; data-origin-height=&quot;314&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동작이 잘 되는것을 알 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이로써 insert.. 같은 기본적인 operation에 대해 동작은 하는것으로 보인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변경점이 꽤 많았을 듯 한데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Initializer에서 MySQL + JPA + R2DBC를 선택할 시 아래와 같은 디펜던시를 설정해준다&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;    &amp;lt;dependencies&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-boot-starter-data-jpa&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-boot-starter-data-r2dbc&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-boot-starter-webflux&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;

        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;dev.miku&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;r2dbc-mysql&amp;lt;/artifactId&amp;gt;
            &amp;lt;scope&amp;gt;runtime&amp;lt;/scope&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;mysql&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;mysql-connector-java&amp;lt;/artifactId&amp;gt;
            &amp;lt;scope&amp;gt;runtime&amp;lt;/scope&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.projectlombok&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;lombok&amp;lt;/artifactId&amp;gt;
            &amp;lt;optional&amp;gt;true&amp;lt;/optional&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-boot-starter-test&amp;lt;/artifactId&amp;gt;
            &amp;lt;scope&amp;gt;test&amp;lt;/scope&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;io.projectreactor&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;reactor-test&amp;lt;/artifactId&amp;gt;
            &amp;lt;scope&amp;gt;test&amp;lt;/scope&amp;gt;
        &amp;lt;/dependency&amp;gt;
    &amp;lt;/dependencies&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;dev.miku의 mysql 드라이버를 기본적으로 사용하게..(공식 드라이버는 아니지만, 공식사이트에서 지원하는 비공식 드라이버.. 같은거랄까) 되어있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;757&quot; data-origin-height=&quot;571&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QY8VR/btqOxrCX9tV/1SEICOqkaazgWCQktC1BqK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QY8VR/btqOxrCX9tV/1SEICOqkaazgWCQktC1BqK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QY8VR/btqOxrCX9tV/1SEICOqkaazgWCQktC1BqK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQY8VR%2FbtqOxrCX9tV%2F1SEICOqkaazgWCQktC1BqK%2Fimg.png&quot; data-origin-width=&quot;757&quot; data-origin-height=&quot;571&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;0.8.0.RC2 이전 버전으로 테스트중이었는데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;0.8.0.RC2 버전에서 &quot;Fixed capabilities flag with flags mask for remove all unknown flags&quot; 라는 버그 수정항목이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 언급한 Capabilities 클래스의 필드들이 수정된 것으로 보이며, Failed Update... 의 에러는 이 버전 이후로 수정되지 않았을까 싶다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;818&quot; data-origin-height=&quot;645&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sWYZw/btqOFjYnr2m/lJLYhAmYRmpJItnOtOalk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sWYZw/btqOFjYnr2m/lJLYhAmYRmpJItnOtOalk0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sWYZw/btqOFjYnr2m/lJLYhAmYRmpJItnOtOalk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsWYZw%2FbtqOFjYnr2m%2FlJLYhAmYRmpJItnOtOalk0%2Fimg.png&quot; data-origin-width=&quot;818&quot; data-origin-height=&quot;645&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 디펜던시를 선택하면, 문제없이 잘 동작한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만, 도중에 r2dbc가 jpa랑은 다른 spring data 로 변경이 된 듯 한데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Id의 어노테이션 사용 시 java.persistence의 &lt;b&gt;@Id 말고, springframework.data.annotation.Id 이걸 사용해야 한다&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇지 않으면 위에서 나왔던 Required identifier property not found for class 에러를 볼 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;P.S&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;findAll() 과 같은, &lt;code&gt;SELECT&lt;/code&gt; 류 쿼리는 문제없이 잘 동작한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원래 의도였던 JDBC와의 성능비교를 하려던 참에 무산이 되어 허무하긴한데, 그래도 아직 개발중인 단계에서 이런 부분 삽질을 하게되어 공유하고자 포스팅을 해본다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;R2DBC와 MySQL을 사용하는 부분은 아직 지양해야 할 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정규 RELEASE가 나오게 되면 그때 다시 연동해보자.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;P.S 2&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자매품으로 &lt;a href=&quot;https://github.com/jasync-sql/jasync-sql&quot;&gt;jasync-sql&lt;/a&gt; 이라는 라이브러리도 개발중인데,실제 Native Driver가 아닌 Wrapped Driver라서 실제 Spring R2DBC에서 사용하는 &lt;code&gt;ConnectionFactory&lt;/code&gt; 를 지원해주지 않는 듯 하다(잠깐 맛보기로 봤을 땐..)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DatabaseClient를 직접 사용해서 쿼리를 작성해야 하는데, 이런 경우는 처음에 사용했던 &lt;code&gt;mirromutth/r2dbc-mysql&lt;/code&gt; 와 별반 차이가 없다. 왜냐면 &lt;code&gt;mirromutth/r2dbc-mysql&lt;/code&gt; 도 DatabaseClient 를 직접 사용하면 insert를 명시적으로 함수호출하여 사용할 수 있기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;R2DBC에서 지원하는 드라이버들의 집합은 아래 사이트에서 확인가능하다.&lt;br /&gt;&lt;a href=&quot;https://r2dbc.io/&quot;&gt;https://r2dbc.io/&lt;/a&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;r2dbc-mysql - Native driver implemented for MySQL.&lt;/li&gt;
&lt;li&gt;jasync-sql - R2DBC wrapper for Java &amp;amp; Kotlin Async Database Driver for MySQL and PostgreSQL written in Kotlin.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Development/Database</category>
      <category>jasync</category>
      <category>mirromutth</category>
      <category>MySQL</category>
      <category>R2DBC</category>
      <category>reactive</category>
      <category>Spring</category>
      <category>TransientDataAccessResourceException</category>
      <author>@곰팡</author>
      <guid isPermaLink="true">https://gompangs.tistory.com/138</guid>
      <comments>https://gompangs.tistory.com/entry/Spring-R2DBC-MySQL#entry138comment</comments>
      <pubDate>Mon, 30 Nov 2020 17:12:16 +0900</pubDate>
    </item>
    <item>
      <title>Spring RequestContextHolder</title>
      <link>https://gompangs.tistory.com/entry/Spring-RequestContextHolder</link>
      <description>&lt;h1&gt;RequestContextHolder&lt;/h1&gt;
&lt;h2&gt;개요&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;RequestContextHolder&lt;/code&gt; 는 Spring에서 전역으로 Request에 대한 정보를 가져오고자 할 때 사용하는 유틸성 클래스이다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;주로, Controller가 아닌 Business Layer 등에서 Request 객체를 참고하려 할 때 사용한다.&lt;/p&gt;
&lt;p&gt;Request Param이라던지.. UserAgent 라던지.. 매번 method의 call param으로 넘기기가 애매할 때 주로 쓰인다.&lt;/p&gt;
&lt;p&gt;아래처럼 호출하면 같은 &lt;code&gt;Request Thread&lt;/code&gt; 범위에 있는 경우 요청정보를 얻어낼 수 있다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;RequestContextHolder.getRequestAttributes()&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;다만, 위에처럼 사용하면 Attribute만 얻어올 수 있으므로 아래와 같이 Wrapping해서 사용한다&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;HttpServletRequest servletRequest = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이렇게 되면 익히 알고 있는 Cookie, Header ... 등의 정보를 얻을 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;1.png&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;306&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bfK0mK/btqFpLu87OY/DacmZPicu4gDREft1f6wl0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bfK0mK/btqFpLu87OY/DacmZPicu4gDREft1f6wl0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bfK0mK/btqFpLu87OY/DacmZPicu4gDREft1f6wl0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbfK0mK%2FbtqFpLu87OY%2FDacmZPicu4gDREft1f6wl0%2Fimg.png&quot; data-filename=&quot;1.png&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;306&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2&gt;생성&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;RequestContextHolder&lt;/code&gt;는 언제 생성되는지, 그리고 어디서 호출하던 관계없이 Request 정보를 얻을 수 있는지에 대해 알아보자.&lt;/p&gt;
&lt;p&gt;일단 이 클래스가 초기화되는건 &lt;code&gt;Servlet&lt;/code&gt; 이 생성될 때 이다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;즉, Http Request가 오는 시점에 생성 및 초기화가 되어지고 Business Layer를 거친 뒤 Servlet 이 destroy될 때 clean되고 있다.&lt;/p&gt;
&lt;p&gt;내부 필드로 가지고 있는 놈들을 살펴보자.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;private static final boolean jsfPresent = ClassUtils.isPresent(&quot;javax.faces.context.FacesContext&quot;, RequestContextHolder.class.getClassLoader());
    private static final ThreadLocal&amp;lt;RequestAttributes&amp;gt; requestAttributesHolder = new NamedThreadLocal(&quot;Request attributes&quot;);
    private static final ThreadLocal&amp;lt;RequestAttributes&amp;gt; inheritableRequestAttributesHolder = new NamedInheritableThreadLocal(&quot;Request context&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;static으로 선언되었기에 클래스 생성과 동시에 만들어지고, &lt;code&gt;Servlet&lt;/code&gt;이 요청/종료 될 때 마다 일일히 값을 채워넣고, 없애는 작업을 한다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Http 요청이 오게 되면 &lt;code&gt;FrameworkServlet&lt;/code&gt; 클래스의 &lt;code&gt;processRequest&lt;/code&gt; 라는 메서드가 호출된다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;protected final void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        long startTime = System.currentTimeMillis();
        Throwable failureCause = null;
        LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext();
        LocaleContext localeContext = this.buildLocaleContext(request);
        RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();
        ServletRequestAttributes requestAttributes = this.buildRequestAttributes(request, response, previousAttributes);
        WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
        asyncManager.registerCallableInterceptor(FrameworkServlet.class.getName(), new FrameworkServlet.RequestBindingInterceptor());
        this.initContextHolders(request, localeContext, requestAttributes);

        try {
            this.doService(request, response);
        } catch (IOException | ServletException var16) {
            failureCause = var16;
            throw var16;
        } catch (Throwable var17) {
            failureCause = var17;
            throw new NestedServletException(&quot;Request processing failed&quot;, var17);
        } finally {
            this.resetContextHolders(request, previousLocaleContext, previousAttributes);
            if (requestAttributes != null) {
                requestAttributes.requestCompleted();
            }

            this.logResult(request, response, (Throwable)failureCause, asyncManager);
            this.publishRequestHandledEvent(request, response, startTime, (Throwable)failureCause);
        }

    }&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;여기에 눈여겨 봐야 할 부분은 아래 부분이다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes requestAttributes = this.buildRequestAttributes(request, response, previousAttributes);
...
this.initContextHolders(request, localeContext, requestAttributes);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이전 attribute가 남아있는지 확인하고 새로운 requestAttributes를 만든다.&lt;/p&gt;
&lt;p&gt;그리고 ContextHolder를 초기화시킨다. &lt;code&gt;initContextHolders&lt;/code&gt; 를 살펴보자.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;private void initContextHolders(HttpServletRequest request, @Nullable LocaleContext localeContext, @Nullable RequestAttributes requestAttributes) {
        if (localeContext != null) {
            LocaleContextHolder.setLocaleContext(localeContext, this.threadContextInheritable);
        }

        if (requestAttributes != null) {
            RequestContextHolder.setRequestAttributes(requestAttributes, this.threadContextInheritable);
        }

    }&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위에서 attribute를 만들었으므로(이전 request attribute가 남아있는 경우는 null로 return하기 때문에, 무조건 새로운 requestAttribute가 만들어진다고 봐도 된다 예외처리로 보임)&lt;/p&gt;
&lt;p&gt;&lt;code&gt;RequestContextHolder&lt;/code&gt; 에 값이 생성되게 된다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이렇게 되면 같은 Request Thread, 즉 Tomcat Thread에서는 어디서든 static한 값을 꺼내 쓸 수 있게 되는 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;2.png&quot; data-origin-width=&quot;727&quot; data-origin-height=&quot;94&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bZEmbe/btqFnvVrrr2/ZxWXwaYGBQ0CtdX11r5TR0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bZEmbe/btqFnvVrrr2/ZxWXwaYGBQ0CtdX11r5TR0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bZEmbe/btqFnvVrrr2/ZxWXwaYGBQ0CtdX11r5TR0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbZEmbe%2FbtqFnvVrrr2%2FZxWXwaYGBQ0CtdX11r5TR0%2Fimg.png&quot; data-filename=&quot;2.png&quot; data-origin-width=&quot;727&quot; data-origin-height=&quot;94&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;기본적인 Tomcat을 예로 들면 &lt;code&gt;nio-8080-exec-1&lt;/code&gt; 과 같은 쓰레드가 생성되면서 Servlet을 서빙한다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;@Component&lt;/code&gt;, &lt;code&gt;@Service&lt;/code&gt; , &lt;code&gt;@Repository&lt;/code&gt; 등은 Business Layer로 Spring Container에 등록되어 같은 쓰레드에서 동작하므로 잘 동작이 된다.&lt;/p&gt;
&lt;h2&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2&gt;동작 원리&lt;/h2&gt;
&lt;p&gt;동작은 매우 간단하다. static한 ThreadLocal에 값을 Write/Read 하는 방식이다. 위에서 같은 쓰레드에서는 값을 꺼내쓸 수 있다고 말한 이유도 이러한 맥락이다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그러나, 다른 쓰레드(new Thread, 혹은 executor를 사용한 ThreadPool에서의 참조 등) 에서는 &lt;code&gt;RequestContextHolder&lt;/code&gt; 의 Request값을 꺼내 쓸 수 없다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;왜냐면 새로운 쓰레드를 생성하는 순간 &lt;code&gt;DispatcherServlet&lt;/code&gt; 의 범위에서 벗어나서 새로운 쓰레드가 생성되기 때문이다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;간혹, 아래와 같은 코드를 짠다고 생각해보자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;3.png&quot; data-origin-width=&quot;771&quot; data-origin-height=&quot;144&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lNVGC/btqFnZIixnk/siDLIGOmliTryIDQE0PE0K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lNVGC/btqFnZIixnk/siDLIGOmliTryIDQE0PE0K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lNVGC/btqFnZIixnk/siDLIGOmliTryIDQE0PE0K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlNVGC%2FbtqFnZIixnk%2FsiDLIGOmliTryIDQE0PE0K%2Fimg.png&quot; data-filename=&quot;3.png&quot; data-origin-width=&quot;771&quot; data-origin-height=&quot;144&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;RequestUtils에는 간단히 static method로 &lt;code&gt;RequestContextHolder&lt;/code&gt;를 가져오는 기능만 한다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;public class RequestUtils {

    public static HttpServletRequest getRequest() {
        return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이럴 경우, 새로운 쓰레드가 생성되면서 예상했던 동작이 잘 되지 않는다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;4.png&quot; data-origin-width=&quot;773&quot; data-origin-height=&quot;48&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cYtRpi/btqFnOUyJ1y/ihChkUrmFpyO2Jj1PQppVK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cYtRpi/btqFnOUyJ1y/ihChkUrmFpyO2Jj1PQppVK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cYtRpi/btqFnOUyJ1y/ihChkUrmFpyO2Jj1PQppVK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcYtRpi%2FbtqFnOUyJ1y%2FihChkUrmFpyO2Jj1PQppVK%2Fimg.png&quot; data-filename=&quot;4.png&quot; data-origin-width=&quot;773&quot; data-origin-height=&quot;48&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;Spring Container 밖의 쓰레드가 생성되었기 때문에 참조를 하지 못하는 것이다.&lt;/p&gt;
&lt;p&gt;이는 &lt;code&gt;new Thread()&lt;/code&gt; 와 같이 쓰레드를 새로 만들던, ThreadPool을 이용해서 만들던 동일한 결과가 나타난다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;만약 &lt;code&gt;CompletableFuture&lt;/code&gt; 라던지, 새로운 쓰레드를 생성해서 병렬성을 높이고자 할 때 &lt;code&gt;RequestContextHolder&lt;/code&gt; 를 사용해서 Request의 쿠키라던지 값을 참조하고자 할 땐 어떻게 할까?&lt;/p&gt;
&lt;h2&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2&gt;구글링을 맹신하지 말자&lt;/h2&gt;
&lt;p&gt;이러한 주제로 필자도 &lt;code&gt;CompletableFuture&lt;/code&gt; 안에서 &lt;code&gt;RequestContextHolder&lt;/code&gt; 를 사용하고자 했으나, 의도대로 동작하지 않아서 구글링을 시작했다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;CompletableFuture&lt;/code&gt; 안에서 참조하고자 하면 아래와 같은 Exception이 발생한다&lt;/p&gt;
&lt;pre class=&quot;vbscript&quot;&gt;&lt;code&gt;java.lang.IllegalStateException: No thread-bound request found: Are you referring to request attributes outside of an actual web request, or processing a request outside of the originally receiving thread? If you are actually operating within a web request and still receive this message, your code is probably running outside of DispatcherServlet: In this case, use RequestContextListener or RequestContextFilter to expose the current request.&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;use RequestContextListener or RequestContextFilter to expose the current request.&lt;/p&gt;
&lt;p&gt;라고 되어 있어서 얼핏보면 이 클래스들을 사용하면 뭔가 될 것 같이 보인다.&lt;/p&gt;
&lt;h3&gt;RequestContextListener&lt;/h3&gt;
&lt;p&gt;request를 expose하려면 이 리스너를 사용하라고 한다&lt;/p&gt;
&lt;p&gt;그래서 나온 구글링 결과가 아래처럼 사용을 하면 된다고 한다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;public class InheritableRequestContextListener extends RequestContextListener {
    private static final String REQUEST_ATTRIBUTES_ATTRIBUTE =
            InheritableRequestContextListener.class.getName() + &quot;.REQUEST_ATTRIBUTES&quot;;

    @Override
    public void requestInitialized(ServletRequestEvent requestEvent) {
        if (!(requestEvent.getServletRequest() instanceof HttpServletRequest)) {
            throw new IllegalArgumentException(&quot;Request is not an HttpServletRequest: &quot; + requestEvent.getServletRequest());
        } else {
            HttpServletRequest request = (HttpServletRequest) requestEvent.getServletRequest();
            ServletRequestAttributes attributes = new ServletRequestAttributes(request);
            request.setAttribute(REQUEST_ATTRIBUTES_ATTRIBUTE, attributes);
            LocaleContextHolder.setLocale(request.getLocale());
            RequestContextHolder.setRequestAttributes(attributes, true);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;차이점을 보면, RequestContext Event가 들어왔을 때, 해당 이벤트를 잡아서 Wrapping 한 뒤&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;RequestContextHolder.setRequestAttributes(attributes, true);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;요 방법을 사용해보라는 것이다.&lt;/p&gt;
&lt;p&gt;뒤에 2번째오는 true의 인자는 &lt;code&gt;boolean inheritable&lt;/code&gt; 의 값인데, ThreadLocal을 Inheritable 하게 만들것인가에 대한 true/false이다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;요것도 될법한데 결국엔 동작하지 않았다. (이유는 아래에서 설명)&lt;/p&gt;
&lt;h3&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3&gt;RequestContextFilter&lt;/h3&gt;
&lt;p&gt;이번엔 &lt;code&gt;RequestContextFilter&lt;/code&gt; 를 생성한 뒤, &lt;code&gt;DispatcherServlet&lt;/code&gt; 이 생성될 때 필터를 끼워넣으라는 방식이다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;public void setThreadContextInheritable(boolean threadContextInheritable) {
        this.threadContextInheritable = threadContextInheritable;
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위에서의 RequestContextListener 처럼 inheriable값이 있어서 이것 또한 동작해볼법 했다.&lt;/p&gt;
&lt;p&gt;하지만 이것 또한 실패했다.&lt;/p&gt;
&lt;h3&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3&gt;왜?&lt;/h3&gt;
&lt;p&gt;단순히 구글링한 결과에 대해 의문을 가질 게 아니라, 프레임워크 내부 동작을 파악하다보니 원인을 알 수 있었다.&lt;/p&gt;
&lt;p&gt;아까 FrameworkServlet 의 로직을 보다보면 실마리가 있다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위에 두 &lt;code&gt;RequestContextListener&lt;/code&gt; 와 &lt;code&gt;RequestContextFilter&lt;/code&gt; 는 DispatcherServlet의 &lt;code&gt;processRequest&lt;/code&gt; 메서드가 호출되기 이전에 먼저 불린다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그래서 필터와 리스너에 inheritable값을 전달을 해줘도, DispatcherServlet의 init 하는 부분에서 덮어씌워지게 되는 것이다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;this.initContextHolders(request, localeContext, requestAttributes);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;결국 이 method가 호출되면서 &lt;code&gt;Servlet&lt;/code&gt; 이 초기화 된다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;private void initContextHolders(HttpServletRequest request, @Nullable LocaleContext localeContext, @Nullable RequestAttributes requestAttributes) {
        if (localeContext != null) {
            LocaleContextHolder.setLocaleContext(localeContext, this.threadContextInheritable);
        }

        if (requestAttributes != null) {
            RequestContextHolder.setRequestAttributes(requestAttributes, this.threadContextInheritable);
        }

    }&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;값을 세팅하는 부분에 보면 &lt;code&gt;this.threadContextInheritable&lt;/code&gt; 을 참조해서 세팅하고 있다.&lt;/p&gt;
&lt;p&gt;기본값을 보니 &lt;code&gt;false&lt;/code&gt; 로 설정되어 있다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이 값을 true로 바꾸면 Servlet Thread 밖에서도 호출할 때 잘 동작하지 않을까?&lt;/p&gt;
&lt;p&gt;&lt;code&gt;threadContextInheritable&lt;/code&gt; 값을 바꾸려면 DispatcherServlet이 생성될 때 변경해줘야 한다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;public class SpringStudyApplication implements WebApplicationInitializer {
    public static void main(String[] args) {
        SpringApplication.run(SpringStudyApplication.class, args);
    }

    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
        AnnotationConfigWebApplicationContext applicationContext = new AnnotationConfigWebApplicationContext();
        DispatcherServlet dispatcherServlet = new DispatcherServlet(applicationContext);
        dispatcherServlet.setThreadContextInheritable(true);

        ServletRegistration.Dynamic dispatcher = servletContext.addServlet(&quot;dispatcherServlet&quot;, dispatcherServlet);
        dispatcher.setLoadOnStartup(1);
        dispatcher.addMapping(&quot;/&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Spring Boot의 경우는 위와 &lt;code&gt;ServletContextInitializer&lt;/code&gt; 인터페이스를 구현해서 같이 초기화 할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;WebApplicationInitializer&lt;/code&gt; 를 써도 동일한 듯 하지만, Spring Boot에서는 DispatcherServlet을 설정하고자 할 때 &lt;code&gt;ServletContextInitializer&lt;/code&gt; 를 사용한다고 한다.&lt;/p&gt;
&lt;p&gt;(&lt;a href=&quot;https://github.com/spring-projects/spring-boot/issues/522&quot;&gt;https://github.com/spring-projects/spring-boot/issues/522&lt;/a&gt;)&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;DispatcherServlet dispatcherServlet = new DispatcherServlet(applicationContext);
dispatcherServlet.setThreadContextInheritable(true);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;대망의 실마리를 잡을 마지막 방법이다.&lt;/p&gt;
&lt;p&gt;DispatcherServlet이 생성될 때 &lt;code&gt;setThreadContextInheritable&lt;/code&gt; 을 true로 주는 방법이다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이렇게 되면 매번 Request가 생성될 때 마다 &lt;code&gt;RequestContextHolder&lt;/code&gt;의 setAttribute를 호출할 때 마다 inheritable 값을 &lt;code&gt;true&lt;/code&gt; 로 전달해줄 것이다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;간단하게 Controller 하나 만들어서 테스트를 해보면....&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@RestController
@Slf4j
public class BaseController {

    private static final Executor CACHED_THREAD_POOL = Executors.newCachedThreadPool();

    @GetMapping(&quot;/&quot;)
    public ResponseEntity index() throws ExecutionException, InterruptedException {
        log.info(&quot;in controller {} {}&quot;, RequestUtils.getRequest().getRequestURL(), Thread.currentThread());

        CompletableFuture&amp;lt;HttpServletRequest&amp;gt; stringCompletableFuture = CompletableFuture.supplyAsync(() -&amp;gt; {
            log.info(&quot;in completable future {}&quot;, RequestUtils.getRequest().getRequestURL());
            return RequestUtils.getRequest();
        }, CACHED_THREAD_POOL);

        HttpServletRequest req2 = stringCompletableFuture.get();

        log.info(&quot;{}&quot;, req2);

        return ResponseEntity.ok(req2.getRequestURL());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;로깅만 남기고, request url이 CompletableFuture 안에서 잘 가져왔는지 확인해보자&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;5.png&quot; data-origin-width=&quot;1064&quot; data-origin-height=&quot;52&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lKcgl/btqFoRiw42t/M6P1qba1uXXou8Z0KxLxbk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lKcgl/btqFoRiw42t/M6P1qba1uXXou8Z0KxLxbk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lKcgl/btqFoRiw42t/M6P1qba1uXXou8Z0KxLxbk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlKcgl%2FbtqFoRiw42t%2FM6P1qba1uXXou8Z0KxLxbk%2Fimg.png&quot; data-filename=&quot;5.png&quot; data-origin-width=&quot;1064&quot; data-origin-height=&quot;52&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Controller에서 가져온 &lt;code&gt;RequestContextHolder&lt;/code&gt; 의 값과, CompletableFuture안에서 가져온 값이 동일하게 출력되는걸 알 수 있다. (Thread가 다름에도 불구하고)&lt;/p&gt;
&lt;p&gt;이는 ThreadLocal을 생성할 때 Inheritable값을 true로 주게 되면, 부모에 set된 값을 child thread에서도 같이 복사해서 가져오는 로직이기 때문이다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;아무 설정이 없다면, &lt;code&gt;RequestContextHolder&lt;/code&gt; 의 Request객체는 Servlet Thread 안에서만 사용이 가능하다.&lt;/p&gt;
&lt;h3&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3&gt;결론&lt;/h3&gt;
&lt;p&gt;Spring Servlet의 동작 및 ThreadLocal의 Inheritable한지 여부 등에 대해서 확실히 공부할 수 있게 되었다.&lt;/p&gt;
&lt;p&gt;p.s 구글링에 나오는 해답이 무조건적인 해답이 아니다. 코드가 의도대로 수행이 안되면 그 코드를 수행하는 상위단계(위 문제에서는 &lt;code&gt;DispatcherServlet&lt;/code&gt; ) 의 로직이 어떤지 확인해보도록 하자.&lt;/p&gt;</description>
      <category>Development/Spring</category>
      <category>CompletableFuture</category>
      <category>inheritableRequestAttributesHolder</category>
      <category>RequestContextHolder</category>
      <category>Spring</category>
      <category>Spring Boot</category>
      <category>ThreadLocal</category>
      <author>@곰팡</author>
      <guid isPermaLink="true">https://gompangs.tistory.com/140</guid>
      <comments>https://gompangs.tistory.com/entry/Spring-RequestContextHolder#entry140comment</comments>
      <pubDate>Sun, 5 Jul 2020 15:28:44 +0900</pubDate>
    </item>
    <item>
      <title>Hashtable에 관해서</title>
      <link>https://gompangs.tistory.com/entry/Hashtable%EC%97%90-%EA%B4%80%ED%95%B4%EC%84%9C</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;개요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HashMap과 비슷한 Collection이지만, &lt;code&gt;Thread-safe&lt;/code&gt; 한 특징이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Thread-safe&lt;/code&gt; 하게 동작을 보장하려면 여러가지 방법이 있지만, 그 중 가장 성능이 안좋은 &lt;code&gt;synchronized&lt;/code&gt; block을 통해 객체 lock을 걸어 동기화를 보장하는 방법을 사용하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한번 Hashtable을 살펴보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;550&quot; data-origin-height=&quot;221&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bkgoTZ/btqEKe7QHLT/N1keE34GGUulPfyvkmgU0K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bkgoTZ/btqEKe7QHLT/N1keE34GGUulPfyvkmgU0K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bkgoTZ/btqEKe7QHLT/N1keE34GGUulPfyvkmgU0K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbkgoTZ%2FbtqEKe7QHLT%2FN1keE34GGUulPfyvkmgU0K%2Fimg.png&quot; data-origin-width=&quot;550&quot; data-origin-height=&quot;221&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;public class Hashtable&amp;lt;K,V&amp;gt;
    extends Dictionary&amp;lt;K,V&amp;gt;
    implements Map&amp;lt;K,V&amp;gt;, Cloneable, java.io.Serializable {
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특이하게 &lt;code&gt;Dictionary&lt;/code&gt; 라는 abstract class를 상속받고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른데 쓰는곳이 있나해서 찾아보니, Hashtable에서만 사용하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구조를 보다보니 일반적인 &lt;code&gt;Map&amp;lt;K,V&amp;gt;&lt;/code&gt; 과 특징이 비슷해서 Map에게 밀린것인건가(?)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(라고 하기엔 &lt;code&gt;@since 1.0&lt;/code&gt; 라고 코멘트 달린것으로 보아, 예전 JDK 초창기에 사용하려다가 deprecated 되고 있는 추상클래스이지 싶다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여튼 일반적인 &lt;code&gt;Map&lt;/code&gt; 인터페이스를 구현하고 있으므로, Map의 사용법과 동일하다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구현 내용을 들여다보자.&lt;/p&gt;
&lt;pre class=&quot;processing&quot;&gt;&lt;code&gt;public synchronized int size() {
        return count;
    }

public synchronized boolean isEmpty() {
        return count == 0;
    }
...
public synchronized V get(Object key) {
        Entry&amp;lt;?,?&amp;gt; tab[] = table;
        int hash = key.hashCode();
        int index = (hash &amp;amp; 0x7FFFFFFF) % tab.length;
        for (Entry&amp;lt;?,?&amp;gt; e = tab[index] ; e != null ; e = e.next) {
            if ((e.hash == hash) &amp;amp;&amp;amp; e.key.equals(key)) {
                return (V)e.value;
            }
        }
        return null;
    }

public synchronized V put(K key, V value) {
        // Make sure the value is not null
        if (value == null) {
            throw new NullPointerException();
        }

        // Makes sure the key is not already in the hashtable.
        Entry&amp;lt;?,?&amp;gt; tab[] = table;
        int hash = key.hashCode();
        int index = (hash &amp;amp; 0x7FFFFFFF) % tab.length;
        @SuppressWarnings(&quot;unchecked&quot;)
        Entry&amp;lt;K,V&amp;gt; entry = (Entry&amp;lt;K,V&amp;gt;)tab[index];
        for(; entry != null ; entry = entry.next) {
            if ((entry.hash == hash) &amp;amp;&amp;amp; entry.key.equals(key)) {
                V old = entry.value;
                entry.value = value;
                return old;
            }
        }

        addEntry(hash, key, value, index);
        return null;
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보다시피 Map을 구현하긴 했지만.. 병렬환경에서의 &lt;code&gt;Thread-safe&lt;/code&gt; 를 &lt;code&gt;synchronized&lt;/code&gt; 키워드를 통해 보장하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;멀티쓰레드에서의 동작이 보장되나 확인해보자.&lt;/p&gt;
&lt;pre class=&quot;html xml&quot; data-ke-language=&quot;html&quot;&gt;&lt;code&gt;static Map&amp;lt;Integer, Integer&amp;gt; hashtable = new Hashtable&amp;lt;&amp;gt;();

    @Test
    void hashTableTest() throws InterruptedException {

        CountDownLatch countDownLatch = new CountDownLatch(3);

        Runnable thread1 = () -&amp;gt; {
            for (int i = 0; i &amp;lt; 10000; i++) {
                hashtable.put(i, i);
            }
            countDownLatch.countDown();
            log.info(&quot;thread1 finish&quot;);
        };

        Runnable thread2 = () -&amp;gt; {
            for (int i = 10000; i &amp;lt; 20000; i++) {
                hashtable.put(i, i);
            }
            countDownLatch.countDown();
            log.info(&quot;thread2 finish&quot;);
        };

        Runnable thread3 = () -&amp;gt; {
            for (int i = 20000; i &amp;lt; 30000; i++) {
                hashtable.put(i, i);
            }
            countDownLatch.countDown();
            log.info(&quot;thread3 finish&quot;);
        };

        new Thread(thread1).start();
        new Thread(thread2).start();
        new Thread(thread3).start();

        countDownLatch.await();

        log.info(&quot;hashtable size : {}&quot;, hashtable.size());
    }
// Console
thread3 finish
thread1 finish
thread2 finish
hashtable size : 30000&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기대했던 size대로 잘 데이터가 들어갔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 thread-safe 보장이 안되는 HashMap을 사용해본다면 어떻게 될까.&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;static Map&amp;lt;Integer, Integer&amp;gt; hashtable = new HashMap&amp;lt;&amp;gt;();
...
hashtable size : 28023&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예상한 3만개가 들어가지 않고 개수가 안맞는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;put&lt;/code&gt; 테스트만 해본 결과고 만약 &lt;code&gt;get&lt;/code&gt; 과 &lt;code&gt;put&lt;/code&gt;을 동시에 호출하게 된다면 아래와 같은 Exception을 만날 수 있다.&lt;/p&gt;
&lt;pre class=&quot;gams&quot;&gt;&lt;code&gt;// thread를 하나 더 만들어서 keySet을 순회해보자.
Runnable thread4 = () -&amp;gt; {
            Set&amp;lt;Integer&amp;gt; keys = hashtable.keySet();
            for (Integer key : keys) {
                hashtable.get(key);
            }
            countDownLatch.countDown();
            log.info(&quot;thread4 finish&quot;);
        };&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;ConcurrentModificationException&lt;/code&gt; 을 만날 수 있는데, 이는 &lt;code&gt;thread-safe&lt;/code&gt; 하지 않은 Collection에 접근과 수정/삽입이 동시에 일어날 경우 발생한다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;Exception in thread &quot;Thread-4&quot; java.util.ConcurrentModificationException
    at java.base/java.util.HashMap$HashIterator.nextNode(HashMap.java:1584)
    at java.base/java.util.HashMap$KeyIterator.next(HashMap.java:1607)
    at dev.gompang.springstudy.SomeTest.lambda$hashTableTest$5(SomeTest.java:77)
    at java.base/java.lang.Thread.run(Thread.java:832)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 Exception이 발생하는 로직은 아래와 같다.&lt;/p&gt;
&lt;pre class=&quot;processing&quot;&gt;&lt;code&gt;final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
... 데이터 삽입
++modCount;
        if (++size &amp;gt; threshold)
            resize();
...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 때 &lt;code&gt;modCount&lt;/code&gt; 라는게 있는데, 이를 활용해서 데이터를 조회하거나 수정할때 변경점이 있는지를 확인한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 forEach를 살펴보면&lt;/p&gt;
&lt;pre class=&quot;gradle&quot;&gt;&lt;code&gt;public final void forEach(Consumer&amp;lt;? super K&amp;gt; action) {
            Node&amp;lt;K,V&amp;gt;[] tab;
            if (action == null)
                throw new NullPointerException();
            if (size &amp;gt; 0 &amp;amp;&amp;amp; (tab = table) != null) {
                int mc = modCount;
                for (Node&amp;lt;K,V&amp;gt; e : tab) {
                    for (; e != null; e = e.next)
                        action.accept(e.key);
                }
                if (modCount != mc)
                    throw new ConcurrentModificationException();
            }
        }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Map에 들어간 element를 순회하기 전의 modCount와 순회한 뒤에 modCount가 같지 않으면 &lt;code&gt;ConcurrentModificationException&lt;/code&gt; 를 발생시킨다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(forEach 뿐만 아니라 Map Operation 곳곳에서 사용된다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;ConcurrentModificationException&lt;/code&gt; 의 경우는 &lt;code&gt;Thread-safe&lt;/code&gt; 하지 않은 Collection을 병렬 쓰레드에서 사용할 경우 자주 마주칠 수 있으니, 사용하는 환경에 따라 &lt;code&gt;Thread-safe&lt;/code&gt; 한 Collection을 꼭 골라 쓰도록 하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;java.util 패키지에 포함된 Collection 유틸성 클래스인 &lt;code&gt;Collections&lt;/code&gt; 에는 이미 생성한 Collection을 &lt;code&gt;Thread-safe&lt;/code&gt; 하게 변경해주는 기능을 제공한다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;Collections.synchronizedMap()
Collections.synchronizedCollection()
Collections.synchronizedList()
...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이들의 공통점은, 기존 생성된 Collection의 element를 다 복사한 후, &lt;code&gt;synchronized&lt;/code&gt; 키워드가 붙은 Collection으로 변경(새로 생성) 해서 만들어준다는 것이다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;public E get(int index) {
    synchronized (mutex) {return list.get(index);}
}
public E set(int index, E element) {
    synchronized (mutex) {return list.set(index, element);}
}
public void add(int index, E element) {
    synchronized (mutex) {list.add(index, element);}
}
public E remove(int index) {
    synchronized (mutex) {return list.remove(index);}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동기화 키로 사용하는 lock object는 &lt;code&gt;this&lt;/code&gt; 즉, 생성한 Collection Object를 지칭한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(모든 Operation이 발생할때마다 전역 lock을 이용해 동기화를 보장한다.)&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;SynchronizedCollection(Collection&amp;lt;E&amp;gt; c) {
            this.c = Objects.requireNonNull(c);
            mutex = this;
        }&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Development/Java</category>
      <category>HashMap</category>
      <category>Hashtable</category>
      <category>java</category>
      <category>Synchronized</category>
      <author>@곰팡</author>
      <guid isPermaLink="true">https://gompangs.tistory.com/139</guid>
      <comments>https://gompangs.tistory.com/entry/Hashtable%EC%97%90-%EA%B4%80%ED%95%B4%EC%84%9C#entry139comment</comments>
      <pubDate>Wed, 10 Jun 2020 02:12:19 +0900</pubDate>
    </item>
    <item>
      <title>맥 한영 전환속도 개선하기(with Karabiner-Elements)</title>
      <link>https://gompangs.tistory.com/entry/%EB%A7%A5-%ED%95%9C%EC%98%81-%EC%A0%84%ED%99%98%EC%86%8D%EB%8F%84-%EA%B0%9C%EC%84%A0%ED%95%98%EA%B8%B0with-Karabiner-Elements</link>
      <description>&lt;p&gt;맥에서 한영전환을 기본값으로 쓰면 Caps Lock을 사용하게 되는데,&lt;/p&gt;
&lt;p&gt;이게 여간 반응속도가 느린게 아니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;주석 달다가, 코딩 하다가 하다보면.. 어느샌가 public ㅔulibc 이러면서.. 빡쳐있는 모습을 발견할 수 있다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이럴때 쓸 수 있는 좋은 팁이다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://wooooooak.github.io/tip/2019/03/01/%EB%A7%A5_%ED%95%9C%EC%98%81%ED%82%A4_%EC%A0%84%ED%99%98_%EC%86%8D%EB%8F%84_%EA%B0%9C%EC%84%A0/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;귀찮으니 자세한건 여기에서 설명&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1579009027880&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-og-type=&quot;website&quot; data-og-title=&quot;맥 한영 전환 속도 개선하기 (capslock 사용) &amp;middot; 쾌락코딩&quot; data-og-description=&quot;맥 한영 전환 속도 개선하기 (capslock 사용) 01 Mar 2019 | macOS mojave에서는 한영 전환을 capslock으로 한다. capslock으로 한영 전환을 하다보면 윈도우와는 달리 딜레이가 존재한다. 한글로 타이핑을 하다가 capslock을 누른 직후 영어 타이핑을 쳐도 여전히 한글이 쳐질때가 많다. 자세히는 모르겠지만 capslock 고유의 기능과 한영 전환 기능 두 개가 존재하기 때문에 생기는 딜레이인것 같다. 이 딜레이를 없애&quot; data-og-host=&quot;wooooooak.github.io&quot; data-og-source-url=&quot;https://wooooooak.github.io/tip/2019/03/01/%EB%A7%A5_%ED%95%9C%EC%98%81%ED%82%A4_%EC%A0%84%ED%99%98_%EC%86%8D%EB%8F%84_%EA%B0%9C%EC%84%A0/&quot; data-og-url=&quot;https://wooooooak.github.io/tip/2019/03/01/%EB%A7%A5_%ED%95%9C%EC%98%81%ED%82%A4_%EC%A0%84%ED%99%98_%EC%86%8D%EB%8F%84_%EA%B0%9C%EC%84%A0/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/zIh3D/hyEzIL5CxB/J1KydKOhPCKiES8fKS7Pw0/img.png?width=1560&amp;amp;height=1420&amp;amp;face=994_875_1050_935,https://scrap.kakaocdn.net/dn/8WbNQ/hyEzK4cedu/2zIzRi2XNddPFKia2eHYvK/img.png?width=1560&amp;amp;height=1386&amp;amp;face=0_0_1560_1386,https://scrap.kakaocdn.net/dn/ySV4Q/hyEzPYLCt0/LG45NYh4KuvPHnk9SdbdU0/img.png?width=1560&amp;amp;height=1386&amp;amp;face=0_0_1560_1386&quot;&gt;&lt;a href=&quot;https://wooooooak.github.io/tip/2019/03/01/%EB%A7%A5_%ED%95%9C%EC%98%81%ED%82%A4_%EC%A0%84%ED%99%98_%EC%86%8D%EB%8F%84_%EA%B0%9C%EC%84%A0/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://wooooooak.github.io/tip/2019/03/01/%EB%A7%A5_%ED%95%9C%EC%98%81%ED%82%A4_%EC%A0%84%ED%99%98_%EC%86%8D%EB%8F%84_%EA%B0%9C%EC%84%A0/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/zIh3D/hyEzIL5CxB/J1KydKOhPCKiES8fKS7Pw0/img.png?width=1560&amp;amp;height=1420&amp;amp;face=994_875_1050_935,https://scrap.kakaocdn.net/dn/8WbNQ/hyEzK4cedu/2zIzRi2XNddPFKia2eHYvK/img.png?width=1560&amp;amp;height=1386&amp;amp;face=0_0_1560_1386,https://scrap.kakaocdn.net/dn/ySV4Q/hyEzPYLCt0/LG45NYh4KuvPHnk9SdbdU0/img.png?width=1560&amp;amp;height=1386&amp;amp;face=0_0_1560_1386');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot;&gt;맥 한영 전환 속도 개선하기 (capslock 사용) &amp;middot; 쾌락코딩&lt;/p&gt;
&lt;p class=&quot;og-desc&quot;&gt;맥 한영 전환 속도 개선하기 (capslock 사용) 01 Mar 2019 | macOS mojave에서는 한영 전환을 capslock으로 한다. capslock으로 한영 전환을 하다보면 윈도우와는 달리 딜레이가 존재한다. 한글로 타이핑을 하다가 capslock을 누른 직후 영어 타이핑을 쳐도 여전히 한글이 쳐질때가 많다. 자세히는 모르겠지만 capslock 고유의 기능과 한영 전환 기능 두 개가 존재하기 때문에 생기는 딜레이인것 같다. 이 딜레이를 없애&lt;/p&gt;
&lt;p class=&quot;og-host&quot;&gt;wooooooak.github.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;위 링크에서 자세히 설명해주겠지만, 주요 포인트는, Karabiner-Elements 라는 오픈소스를 사용해서&lt;/p&gt;
&lt;p&gt;맥의 특정 단축키를 바꿔서 매핑하는 방법이다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Caps Lock을 그대로 사용하게 되면 시스템에서 처리하는 속도가 왠지모르게 느린데(동일 키에 대한 이벤트가 많이 발생하나보다)&lt;/p&gt;
&lt;p&gt;특정 안쓰는 키(필자의 경우는 F6같은 경우는 잘 안쓰는거 같아서 F6으로 설정했다. 안쓰는 function 키로 잡으면 좋을 듯)&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;맥 설치파일(.dmg)&lt;/p&gt;
&lt;p&gt;(온라인에 악성코드 뿌리고 하는 악취미는 없기에, 믿고 쓰셔도 된다)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignCenter&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/voOWW/btqBeAFaAKe/f9adLGLwyKZidtOz8GixG1/Karabiner-Elements-12.8.4.dmg.zip?attach=1&amp;amp;knm=tfile.zip&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;Karabiner-Elements-12.8.4.dmg.zip&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;8.28MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;P.S&lt;/p&gt;
&lt;p&gt;현재 &lt;a href=&quot;https://pqrs.org/osx/karabiner/&quot;&gt;https://pqrs.org/osx/karabiner/&lt;/a&gt; 가 접근이 잘 안되는데, 미리 설치파일(.dmg) 을 올려 두고자 하면서.. 겸사겸사 팁 포스팅&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;P.S&lt;/p&gt;
&lt;p&gt;이 글 쓰자마자 사이트가 복구되었다..(?)&lt;/p&gt;</description>
      <category>caps lock</category>
      <category>맥</category>
      <category>맥 한영전환</category>
      <category>한영전환</category>
      <author>@곰팡</author>
      <guid isPermaLink="true">https://gompangs.tistory.com/137</guid>
      <comments>https://gompangs.tistory.com/entry/%EB%A7%A5-%ED%95%9C%EC%98%81-%EC%A0%84%ED%99%98%EC%86%8D%EB%8F%84-%EA%B0%9C%EC%84%A0%ED%95%98%EA%B8%B0with-Karabiner-Elements#entry137comment</comments>
      <pubDate>Tue, 14 Jan 2020 22:41:43 +0900</pubDate>
    </item>
    <item>
      <title>HashMap 에 대하여</title>
      <link>https://gompangs.tistory.com/entry/HashMap-%EC%97%90-%EB%8C%80%ED%95%98%EC%97%AC</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;개요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java의 &lt;code&gt;HashMap&lt;/code&gt; 에 대한 공부.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자주 쓰지만, 실제로 어떻게 동작하는지에 대한 이해가 필요하고 Java에서 뿐만 아니라, NoSQL 등의 경우에도 사용되는 &lt;code&gt;Hash&lt;/code&gt;의 개념과 Map의 개념에 대해 알아보자.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Hash&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Hash&lt;/code&gt; 는 특정 input 값이 주어졌을 때 항상 동일값을 보장해주는 값이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주로 &lt;code&gt;SHA256&lt;/code&gt; &lt;code&gt;MD5&lt;/code&gt;등과 같은 해시 알고리즘에 의해 많이 알려져 있다. 차이점이라고 하면 Hash를 할 때 충돌(collision)이 발생하게 되는데, 그런 충돌을 얼마나 더 효율적으로 방지할 것인가에 대한 정도가 되겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java에서 사용하는 &lt;code&gt;String&lt;/code&gt; 클래스의 해시 함수를 살펴보자.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hash != HashCode&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hash : 해시 함수&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HashCode : 해시 해서 나온 값을 정수화 시킨 것&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;    public int hashCode() {
        int h = hash;
        if (h == 0 &amp;amp;&amp;amp; value.length &amp;gt; 0) {
            hash = h = isLatin1() ? StringLatin1.hashCode(value)
                                  : StringUTF16.hashCode(value);
        }
        return h;
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;String이 Latin인 경우와 UTF-16 인 경우를 분기해서 해시를 계산하도록 되어 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UTF-16을 기준으로 더 보게 되면 아래와 같다.&lt;/p&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;    public static int hashCode(byte[] value) {
        int h = 0;
        int length = value.length &amp;gt;&amp;gt; 1;
        for (int i = 0; i &amp;lt; length; i++) {
            h = 31 * h + getChar(value, i);
        }
        return h;
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java String의 내부 구조는 &lt;code&gt;char[]&lt;/code&gt; 로 이루어져 있다. 또한 이는 JVM의 constant pool에 저장되어 byte[] 데이터를 가질 수 있게 되는데, 해당 값으로 해시를 진행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]&lt;/code&gt; 과 같은 꼴로 해시코드를 구하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 해시코드는 대부분 String마다 다름을 보장하지만, 그렇지 않을때도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히나, HashMap의 경우에 Key가 되는 값이 String만 되는것이 아니라, Object가 되기 때문에 항상 &lt;code&gt;같음&lt;/code&gt; 을 보장할 순 없다는 소리다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;해시 충돌(Hash Collision)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약, 해시가 충돌하면 어떤일이 발생할까&lt;/p&gt;
&lt;pre class=&quot;processing&quot;&gt;&lt;code&gt;Object a = new Object();
Object b = new Object();
HashMap&amp;lt;Object, String&amp;gt; map = new HashMap&amp;lt;&amp;gt;();

map.put(a, &quot;a&quot;);
map.put(b, &quot;b&quot;);

// map의 entry를 출력한 결과는 어떻게될까?
map.forEach((k,v) -&amp;gt; {
  System.out.println(&quot;key : &quot; + k + &quot; value :&quot; + v);
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 a,b를 새로 생성하고, 객체를 Map에 그대로 담아보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 두 객체가 다르겠지만, 만약에 같게 된다면 map에는 a와 b의 해시코드가 같으니 마지막 값인 b로 덮어씌워지게 될 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(실제로 HashMap에 위와 같이 넣게 되면 a,b가 충돌해도 그대로 존재한다. 왜 인지는 아래에서 확인하자.)&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://upload.wikimedia.org/wikipedia/commons/thumb/5/58/Hash_table_4_1_1_0_0_1_0_LL.svg/1280px-Hash_table_4_1_1_0_0_1_0_LL.svg.png&quot; alt=&quot;img&quot; width=&quot;848&quot; height=&quot;650&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 그림이 해시함수를 수행했을 때, 값이 충돌이 발생한 경우를 보여주는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;John Smith&lt;/code&gt; 와 &lt;code&gt;Sandra Dee&lt;/code&gt; 가 같은 해시값을 나타내는 02가 나오게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 경우, 값이 날아가는걸 막아야하는데 크게 두가지의 방법이 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Open Addressing&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해시의 충돌이 발생했을 때 충돌이 발생한 index의 뒤를 찾아서 빈 공간에 데이터를 넣는 방법이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빈 공간을 탐색하는 방법에는 &lt;code&gt;Linear Probing&lt;/code&gt; , &lt;code&gt;Quadratic Probing&lt;/code&gt; 등의 방법이 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Seperate Chaining&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Java HashMap에서 사용하는 방법이다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;충돌이 발생하는 key에 대한 값을 1개만 두지 말고, &lt;code&gt;LinkedList&lt;/code&gt; 와 같은 방식으로 리스트를 넣는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉,&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;HashMap&amp;lt;String, String&amp;gt; map = new HashMap&amp;lt;&amp;gt;();
map.put(&quot;a&quot;, &quot;b&quot;);
map.put(&quot;c&quot;, &quot;d&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;와 같이 넣게 되면 내부적으로는&lt;/p&gt;
&lt;pre class=&quot;processing&quot;&gt;&lt;code&gt;public boolean put(String key, String value){
  LinkedList valueChain = getValue(key); // key값으로 linkedList를 가져와봄
  if(valueChain == null){
    valueChain = new LinkedList&amp;lt;&amp;gt;(); // 없으면 새로 생성
  }
  valueChain.add(value);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 넣게 되는 것이다(실제 로직은 저보다 복잡하지만, 단순화를 시켰다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게되면 Hash를 해서 가져왔을 경우에 시간복잡도가 원래 &lt;code&gt;O(1)&lt;/code&gt; 이지만, 최악의 경우 LinkedList를 전부 순회를 해야할 수도 있으므로 &lt;code&gt;O(n)&lt;/code&gt; 까지 갈 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 사용할 때 대부분의 경우는 &lt;code&gt;O(1)&lt;/code&gt; 에 가져오겠지만, 충돌이 많은 경우는 성능이 더 안좋을 수도 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Map&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 해시와 해시 충돌에 대해서 알아봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 Java HashMap의 구현에 대해서 조금 더 파보도록 하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Map의 인터페이스는 아래와 같다&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;public interface Map&amp;lt;K, V&amp;gt; {
  V put(K key, V value);
  V get(Object key);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 밖에 유틸성 메서드가 많지만, 매우 간략하게 보자면 위와 같은 메서드가 제공이 되어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Key&lt;/code&gt; 와 &lt;code&gt;Value&lt;/code&gt; 를 매핑할 수 있는 자료구조인 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Map의 구현체로는 HashMap, TreeMap, LinkedHashMap 등이 있다. 각기 다른 구조로 구성이 되어 있는데 이번엔 기본이 되는 HashMap에 대해서만 알아보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HashMap같은 경우는 associate array로도 불리우는데, 최초 bucket의 크기가 정해져 있고 데이터가 추가될수록 이를 증가시켜서 커지는 구조로 되어 있다.&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;static final int DEFAULT_INITIAL_CAPACITY = 1 &amp;lt;&amp;lt; 4; // aka 16&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최초 크기는 16개의 bucket으로 생성이 되게 된다&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;static final float DEFAULT_LOAD_FACTOR = 0.75f;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 &lt;code&gt;Load Factor&lt;/code&gt; 라는 개념의 값이 있는데, HashMap의 생성자로 특별히 지정을 하지 않는 이상 75%의 load factor를 가지고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 말은, 위의 최초 생성 크기가 16개의 bucket으로 시작한다고 했는데 16의 75%인 12개의 bucket을 사용하게 된다면 그 크기를 2배로 늘린 32개의 bucket으로 증가시킨다는 말이다.&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;static final int TREEIFY_THRESHOLD = 8;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분이 흥미로운데 아까 말했듯이 Java에서는 seperate chaning 방식을 사용하므로, 해시의 충돌이 일어났을 때 LinkedList를 사용해서 해결을 한다고 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데 LinkedList의 특성상 탐색이 느린 단점(&lt;code&gt;O(n)&lt;/code&gt;) 때문에 성능의 저하가 되니, 이를 TREEIFY 임계점을 지정하여 동일한 key에 대해 충돌이 자주 발생할 경우(기본 8개) 이를 Tree구조로 변경을 한다는 점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TreeNode로 변경을 할 경우 &lt;code&gt;O(n)&lt;/code&gt; 에서 탐색 시 &lt;code&gt;O(log n)&lt;/code&gt; 으로 개선이 가능한 부분을 적용한 것이다.&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;static final int UNTREEIFY_THRESHOLD = 6;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Map의 삭제 혹은 크기 재조절 시 해당 LinkedList 혹은 TreeNode의 element개수가 또 적어질수도 있는데, 이런 경우에 6개 이하의 element가 될 경우 탐색시간이 그리 소요되지 않으니 LinkedList로 다시 변경하는 임계점이 존재한다.&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 데이터노드의 hashCode를 계산하는 부분이 위와는 또 사뭇 다른데, 잘 보면 key에 대한 hash와 추가적으로 value에 대한 hash값을 XOR한 값을 hashCode로 사용하는 것을 알 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 key로만 hashing했을 경우 충돌이 발생하는 케이스를 더 줄이고자 value까지 hashing을 해서 XOR하는 알고리즘으로 구현이 되어 있다(보조해시함수)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇기에 대~부분 충돌이 잘 발생안할것으로 예상하지만 내부적으로 충돌발생 시 어떤 동작을 취하는지는 알아두면 좋을것 같다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Thread-safety&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HashMap의 경우 Thread-safe하지 않다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 경우 사용하는 대안이 &lt;code&gt;HashTable&lt;/code&gt; 과 &lt;code&gt;ConcurrentHashMap&lt;/code&gt; 이 존재하는데, 이 부분은 뒤이어 차이점에 대한 포스팅을 할 예정이다.&lt;/p&gt;</description>
      <category>Development/Java</category>
      <category>hash</category>
      <category>Hash Collision</category>
      <category>HashMap</category>
      <category>java</category>
      <category>Java HashMap</category>
      <category>Open Addressing</category>
      <category>Seperate Chaning</category>
      <author>@곰팡</author>
      <guid isPermaLink="true">https://gompangs.tistory.com/133</guid>
      <comments>https://gompangs.tistory.com/entry/HashMap-%EC%97%90-%EB%8C%80%ED%95%98%EC%97%AC#entry133comment</comments>
      <pubDate>Tue, 14 May 2019 01:35:24 +0900</pubDate>
    </item>
    <item>
      <title>Prometheus 를 알아보자</title>
      <link>https://gompangs.tistory.com/entry/Prometheus-%EB%A5%BC-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90</link>
      <description>&lt;h2&gt;개요&lt;/h2&gt;
&lt;p&gt;Prometheus 는 오픈소스 모니터링 솔루션이다. (&lt;a href=&quot;https://prometheus.io/&quot;&gt;https://prometheus.io/&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;크게 Prometheus 서버와, 메트릭 정보를 export할 export node로 구성이 되어 있다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bA2of0/btqt84adAmz/CGjiMe9GK1hHdkDvkkwusk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bA2of0/btqt84adAmz/CGjiMe9GK1hHdkDvkkwusk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bA2of0/btqt84adAmz/CGjiMe9GK1hHdkDvkkwusk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbA2of0%2Fbtqt84adAmz%2FCGjiMe9GK1hHdkDvkkwusk%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;구조가 좀 일반적인 모니터링 시스템과는 다르다는걸 느낄수가 있는데, 원래는 대부분의 솔루션이 메트릭을 수집할 때 메트릭이 발생한 서버에서 중앙 서버로 데이터를 전송해주는 구조로(push) 되어 있지만, 프로메테우스 같은 경우는 신기하게도 중앙서버(prometheus server)에서 메트릭을 polling 해서 가져가도록 구성이 되어 있다.&lt;/p&gt;
&lt;h3&gt;장점&lt;/h3&gt;
&lt;p&gt;이 솔루션을 쓰게 되면 얻을 수 있는 장점은, 아무래도 pull 방식의 구조를 채택함으로써 모든 메트릭에 대한 데이터를 중앙 서버로 보내지 않아도 된다는 점이다. 대부분의 모니터링 시스템 구조는 push방식으로 메트릭이 발생하면 해당 메트릭을 서버로 보내는 방식을 채택하고 있는데, 만약 부하가 높은 상황에서 그런 메트릭들을 수집하기 위해 전송하는 부분이 생긴다면? 부하와 더불어서 실제 어플리케이션에 문제를 유발할 수 있는 fail point가 생기는 것이다&lt;/p&gt;
&lt;p&gt;Prometheus의 특성 상, 모든 데이터를 수집하지 않고 일정 주기(default 15s)로 발생하는 메트릭을 수집하여 추이나 모니터링을 &lt;b&gt;어플리케이션에 무리 없이&lt;/b&gt; 하기 때문에 이러한 부분에서 매력적인 솔루션이라고 말할 수 있다.&lt;/p&gt;
&lt;p&gt;또한 현재 Prometheus를 사용하고 있는 Vendor와 open-source들이 상당히 많다. 구조가 복잡하지 않고 간단하기 때문에 특정 솔루션에 대한 export를 하는 것이 어렵지 않다. 특히 Kubernetes와 Docker환경에서의 서비스의 메트릭을 수집하고 분석하는 대에 집중적으로 많이 사용이 되고 있다.&lt;/p&gt;
&lt;h3&gt;단점&lt;/h3&gt;
&lt;p&gt;Scale-out이 안된다(정확히 말하자면 억지로 되지만, 이것은 사실상 clustering이 아니라고 본다) 문서에 적혀있기를 Prometheus를 여러대에 구성해서 사용하려면 Prometheus에 Prometheus를 연결해서.. Hierarchy 구조를 만들어서 사용을 하면 된다 라고 하는데! 세상에 너무 지저분한 구조인 것 같다.&lt;/p&gt;
&lt;p&gt;대부분 Clustering이라 함은.. Host를 여러대 두고 gossip을 통해 서로를 discovery 하고, 데이터를 sharding 및 replication을 해서 특정 노드가 죽어도 H/A(High Availability)를 보장해주는게 되어야 하는데 Prometheus 같은 경우는 그런 고가용성을 지원한다는 개념보다는 간단한 구조로 손쉽게 사용을 할 수 있다에 목적이 있는 것 같다.&lt;/p&gt;
&lt;p&gt;또한, 위의 장점이 또 단점이 될 수 있는 부분인데 모든 메트릭을 전송하지 않기 때문에 사실상 &quot;추이&quot;를 보는데는 좋지만 APM(Application Performance Monitoring)과 같이 발생한 모든 로그를 추적하고 문제가 발생했을 때 이를 검색해서 어떤 일이 있었는지의 원인을 밝히고자 했을때는 적합하지 않은 솔루션이다.&lt;/p&gt;
&lt;h3&gt;정리&lt;/h3&gt;
&lt;p&gt;따라서 위의 장점만 가지고도 서비스 모니터링에 부합하는지를 잘 체크를 해서 도입을 해야할 필요성이 있다. 장/단점이 확실한 솔루션이다보니 push방식의 비슷한 솔루션들에 비해서는 모호한 부분은 없는 것 같다고 생각이 된다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;서버 내부 모듈&lt;/h2&gt;
&lt;h3&gt;Retrival&lt;/h3&gt;
&lt;p&gt;메트릭을 수집할 대상 서버에 접근해서(HTTP) 메트릭을 가져오거나, 아니면 Pushgateway를 통해서 접근할 수 없는 곳에 있는 데이터(inner server, firewall 내부의 metric 등)를 가져오는 등의 역할을 하게 된다&lt;/p&gt;
&lt;p&gt;Pushgateway는 쉽게말해 Proxy Forwarding을 해서 접근할 수 없는 곳에 데이터가 존재하는 경우 이때 사용할 수 있는 대안이라고 보면 된다. 사내망에 데이터가 있어서 외부에서 scrape를 하고싶어도 접근이 안되는 경우 말이다. 아무래도 메트릭 발생 서버 -&amp;gt; 메트릭 집계 서버 가 아닌 메트릭 집계서버가 메트릭 발생 서버에 직접 접근을 해서 데이터를 가져가기 때문에 존재하는 개념인 듯 하다.&lt;/p&gt;
&lt;h3&gt;TSDB(Time-series Database)&lt;/h3&gt;
&lt;p&gt;이렇게 가져온 데이터를 저장하고, 시간의 흐름에 따라 조회를 할 수 있어야 하므로 시-계열 데이터(time-series) 를 저장할 수 있는 저장소가 prometheus 내부에 구현이 되어 있다.&lt;/p&gt;
&lt;p&gt;데이터를 저장하는 방법은 Local Storage, Remote Storage를 이용하는 방법 두 가지가 존재한다. 대부분 Local Storage를 쓰는걸로 default로 사용하겠지만 필요에 따라서 원격지에 있는 서버에 데이터를 저장해서 사용을 한다고 한다&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://image.slidesharecdn.com/copyofprometheusstorage1-160127133731/95/prometheus-storage-9-638.jpg?cb=1453901940&quot; alt=&quot;prometheus architecture&quot; width=&quot;630&quot; height=&quot;394&quot; /&gt;&lt;/p&gt;
&lt;p&gt;내부 저장소는 Google에서 만든 &lt;a href=&quot;https://github.com/google/leveldb&quot;&gt;Level DB&lt;/a&gt;를 사용하는걸로 보인다 light-weight의 Key-value 저장소이며 전반적으로 뛰어난 성능을 보이기에 여러군데서 차용해서 쓰는것 같다.(프로메테우스도)&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://image.slidesharecdn.com/copyofprometheusstorage1-160127133731/95/prometheus-storage-7-638.jpg?cb=1453901940&quot; alt=&quot;prometheus metric schema&quot; width=&quot;633&quot; height=&quot;396&quot; /&gt;&lt;/p&gt;
&lt;p&gt;프로메테우스의 메트릭은 위와 같이 수집이 되는데&lt;/p&gt;
&lt;pre class=&quot;lsl&quot;&gt;&lt;code&gt;메트릭명{필드1=값, 필드2=값} 샘플링데이터&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;실제로 timestamp값은 프로메테우스 서버 내부에서 저장될 때 같이 넣게 되고 export하는 부분에서는 메트릭명-샘플링데이터와 같은 형태로 보여지게 된다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://3.bp.blogspot.com/-eduU9hfXlCY/WhJT2x6oFqI/AAAAAAAAff8/sUiEeCflN-E9oY5Vn98ypNgx-fYlxv4IgCLcBGAs/s1600/Micrometer-Boot2.png&quot; alt=&quot;spring boot micrometer metric(exported)&quot; width=&quot;851&quot; height=&quot;881&quot; /&gt;&lt;/p&gt;
&lt;p&gt;이게 Spring boot에 Prometheus export 라이브러리를 붙인 모습인데, 보다시피 많은수의 메트릭이 발생을 하게 되고, 이를 text/html 방식으로 특정 url(대부분 /metrics)로 export를 해두게 되면, prometheus 서버가 이를 긁어가서 데이터를 저장하는 구조이다.&lt;/p&gt;
&lt;h3&gt;HTTP Server&lt;/h3&gt;
&lt;p&gt;prometheus에 저장된 데이터를 조회하기 위해서는 내부적으로 HTTP 서버가 필요하다. 따라서 prometheus는 데이터를 가져가기 위한 프로토콜로 HTTP REST API를 제공하고, 직접 API를 통해 데이터를 가져가던지, Web UI 대시보드에서 데이터를 조회한다던지, Grafana를 통해 더욱 자세하고 깔끔한 데이터 시각화를 할 수 있다&lt;/p&gt;
&lt;h3&gt;Web UI&lt;/h3&gt;
&lt;p&gt;원래는 시각화를 위한 툴을 붙여서 보는게 더 좋지만, 그렇게 하지 않아도 기본적으로 제공하는 Dashboard가 있다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.callicoder.com/assets/images/post/large/spring-boot-actuator-metrics-dashboard-prometheus-graph.jpg&quot; alt=&quot;prometheus dashboard&quot; width=&quot;850&quot; height=&quot;532&quot; /&gt;&lt;/p&gt;
&lt;p&gt;위와 같이 Bootstrap으로 작성된듯한 투박한 대시보드가 제공이 되고, 간단하게 보고싶은 메트릭에 대한 그래프를 볼 수 있는 정도이다. 한 대시보드에 여러개의 메트릭의 추이를 보고싶다던지, 다른 그래프 형식으로 보고싶다던지 하는 것들은 지원이 되지 않으며 이는 Grafana를 통해 시각화가 가능하니 그쪽을 보도록 하자.&lt;/p&gt;
&lt;h3&gt;Alert Manager&lt;/h3&gt;
&lt;p&gt;모니터링 시스템이니, 문제가 발생하면 이를 알람으로 보내주는 역할도 있어야한다. Alertmanager는 Prometheus에서 문제가 발생했다고 생각되는 시점에 slack, hipchat 등을 통해 알람을 보내준다.&lt;/p&gt;
&lt;p&gt;알람을 거는 기준은 Rule을 작성해서 load시키는 방식으로 동작하는데&lt;/p&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;expr: job:request_latency_seconds:mean5m{job=&quot;myjob&quot;} &amp;gt; 0.5&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;와 같이 expression을 작성하는 것으로 알람을 전송할 수 있다. 특정 메트릭의 값이 어느정도 선(threshold)을 넘는다거나, 낮아진다거나 하는 메트릭을 보고 판단을 할 수 있다.&lt;/p&gt;
&lt;p&gt;다만, 이 또한 Grafana를 사용하게 된다면 Grafana에서도 동일하게 알람매니저를 제공을 하고 있는데, 아무래도 Grafana쪽이 사용하기 더 쉽고 직관적이기 때문에 이걸 직접 사용할까는 싶지만, 그래도 expression을 이용해서 더 복잡한 조건을 걸어서 알람을 노티해주는 방식이 있다는건 좋다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://pracucci.com/assets/2017-04-01-kubecon-2017-alerts-24e7dcdc29996e951730bb67937e4c2a685b554ffd88f502a8b6e650cd42025f.png&quot; alt=&quot;&quot; width=&quot;837&quot; height=&quot;440&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Prometheus Server 설치&lt;/h2&gt;
&lt;h3&gt;Installation&lt;/h3&gt;
&lt;p&gt;일단 client를 만져보기 전에 prometheus 서버를 설치를 해보도록 하자.&lt;/p&gt;
&lt;p&gt;다운로드는 &lt;a href=&quot;https://prometheus.io/download/#prometheus&quot;&gt;https://prometheus.io/download/#prometheus&lt;/a&gt; 에서 할 수 있다&lt;/p&gt;
&lt;p&gt;prometheus 서버 말고도 각종 exporter가 있다(node js, alertmanager 등).. 여기서는 prometheus server만 받아서 맞는 운영체제 버전으로 받자&lt;/p&gt;
&lt;p&gt;본인은 odroid의 armv7을 사용하고 있어서 해당 바이너리로 받게 되었고,&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;wget https://github.com/prometheus/prometheus/releases/download/v2.5.0/prometheus-2.5.0.linux-armv7.tar.gz
tar xvfz prometheus-2.5.0.linux-armv7.tar.gz
cd prometheus-2.5.0.linux-armv7
./prometheus&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;로 바로 실행을 할 수 있다. 다만 지금은 서버가 뜨긴 하지만 아무런 메트릭을 수집하지 않고있다. 이는 prometheus.yml에서 scape를 할 target을 지정하지 않았기 때문이다. 지금은 단순히 9090포트로 접근을 해보면 Prometheus의 대시보드를 볼 수 있을것이다&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.joyfulbikeshedding.com/images/2018/prometheus-42e58d9a.png&quot; alt=&quot;prometheus 대시보드 메트릭 확인&quot; width=&quot;852&quot; height=&quot;435&quot; /&gt;&lt;/p&gt;
&lt;p&gt;대시보드에서 무언가를 보기 위해서는 일단 prometheus.yml 의 내용을 보도록 하자&lt;/p&gt;
&lt;h3&gt;prometheus.yml&lt;/h3&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;# my global config
global:
  scrape_interval:     15s # Set the scrape interval to every 15 seconds. Default is every 1 minute.
  evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute.
  # scrape_timeout is set to the global default (10s).

# Alertmanager configuration
alerting:
  alertmanagers:
  - static_configs:
    - targets:
      # - alertmanager:9093

# Load rules once and periodically evaluate them according to the global 'evaluation_interval'.
rule_files:
  # - &quot;first_rules.yml&quot;
  # - &quot;second_rules.yml&quot;

# A scrape configuration containing exactly one endpoint to scrape:
# Here it's Prometheus itself.
scrape_configs:
  # The job name is added as a label `job=&amp;lt;job_name&amp;gt;` to any timeseries scraped from this config.
  - job_name: 'prometheus'

    # metrics_path defaults to '/metrics'
    # scheme defaults to 'http'.

    static_configs:
    - targets: ['localhost:9090']&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;설정이 다른 모니터링 서비스들에 비해서는 매우 간단한 편이다. (모든 설정이 간편한건 아니다. default로 설정 되어있는 config가 간단해서 그렇지, 실제로 모든 config의 field를 보면 상당히 양이 많다. 자세한건 &lt;a href=&quot;https://prometheus.io/docs/prometheus/latest/configuration/configuration/&quot;&gt;https://prometheus.io/docs/prometheus/latest/configuration/configuration/&lt;/a&gt;)&lt;/p&gt;
&lt;h4&gt;config 설명&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;global.scrape_interval : 몇 초 단위로 메트릭을 수집할 지를 결정(15초로 기본으로 되어 있는데, 이를 수정하여 1초단위로 수집을 할 지.. 10초 단위로 수집할지.. 1분 단위로 수집할지를 결정할 수 있다)&lt;/li&gt;
&lt;li&gt;global.evaluation_interval : 이건 alerting rule을 어느 주기로 evaluate를 할 것인지에 대한 설정이다. evaluate를 하게되면 그 결과를 바탕으로 (inactive, pending, firing) 의 세 state로 구분이 되고 그 상태를 기반으로 alertmanager에서 알람을 발송할 지 안할지를 결정할 수 있다
&lt;ul&gt;
&lt;li&gt;alertmanager의 rule은 위에서 언급한대로 expression을 사용해서 설정을 할 수 있다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;alerting : alertmanager의 target port를 지정할 수 있다. prometheus를 실행하면 alertmanager가 자동으로 같이 뜨는게 아니라, alertmanager를 download해서 같이 구동을 시켜줘야 한다&lt;/li&gt;
&lt;li&gt;rule_files : expression으로 구성된 rule file의 path를 지정해준다. 복수개의 파일을 지정할 수 있다&lt;/li&gt;
&lt;li&gt;scrape_config : 이부분이 중요하다.
&lt;ul&gt;
&lt;li&gt;job_name : 메트릭을 수집해서 구분을 할 네이밍을 지정&lt;/li&gt;
&lt;li&gt;static_configs.targets : 실제 메트릭을 수집할 서버의 주소를 지정한다. 리스트로 구성이 되어 있으며 여러개의 호스트를 지정할 수 있다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;간단하게 static_configs의 target만 고쳐서 prometheus를 띄워보도록 하자&lt;/p&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;- targets: ['192.168.0.3:9090']&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lautq/btqubR8nXAs/OqbMTff5zgvPGQjLZJ2Km1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lautq/btqubR8nXAs/OqbMTff5zgvPGQjLZJ2Km1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lautq/btqubR8nXAs/OqbMTff5zgvPGQjLZJ2Km1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Flautq%2FbtqubR8nXAs%2FOqbMTff5zgvPGQjLZJ2Km1%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;서버가 잘 실행이 되면 위와 같이 &lt;b&gt;&lt;i&gt;Server is ready to receive web requests.&lt;/i&gt;&lt;/b&gt; 라는 메세지가 나오게 된다.&lt;/p&gt;
&lt;p&gt;이제 메트릭을 수집하기 위해 sample client를 만들어봐야하는데, 간단하게 spring boot로 구성을 해서 띄워보도록 하겠다. 샘플 프로젝트는 github에 올려놨으니 보고 참고를 하면 좋을 것 같다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;spring boot sample project : &lt;a href=&quot;https://github.com/Gompangs/prometheus-client-sample&quot;&gt;https://github.com/Gompangs/prometheus-client-sample&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Spring Boot Sample Client(Java)&lt;/h2&gt;
&lt;h3&gt;프로젝트 생성&lt;/h3&gt;
&lt;p&gt;pom.xml에 아래의 dependency를 추가하게 되면 prometheus client가 추가가 되게 된다.&lt;/p&gt;
&lt;p&gt;사실 이런 방식 말고 spring boot용 prometheus client가 존재를 해서 그걸 dependency로 가져오게 되면 훨씬 간단하게 바로 사용을 할 수 있는데, 어떻게 돌아가는지를 직접 보면서 metric을 export를 해보기 위해서 client 모듈을 직접 가져와서 사용해보도록 하자.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;참고. Spring boot용 client는 actuator를 활용해서 구현이 되어 있다(&lt;a href=&quot;https://www.callicoder.com/spring-boot-actuator-metrics-monitoring-dashboard-prometheus-grafana/&quot;&gt;https://www.callicoder.com/spring-boot-actuator-metrics-monitoring-dashboard-prometheus-grafana/&lt;/a&gt;) 를 참고하면 연동방법에 대해 나와 있다.&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;!-- The client --&amp;gt;
&amp;lt;dependency&amp;gt;
  &amp;lt;groupId&amp;gt;io.prometheus&amp;lt;/groupId&amp;gt;
  &amp;lt;artifactId&amp;gt;simpleclient&amp;lt;/artifactId&amp;gt;
  &amp;lt;version&amp;gt;0.6.0&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&amp;lt;!-- Hotspot JVM metrics--&amp;gt;
&amp;lt;dependency&amp;gt;
  &amp;lt;groupId&amp;gt;io.prometheus&amp;lt;/groupId&amp;gt;
  &amp;lt;artifactId&amp;gt;simpleclient_hotspot&amp;lt;/artifactId&amp;gt;
  &amp;lt;version&amp;gt;0.6.0&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&amp;lt;!-- Exposition HTTPServer--&amp;gt;
&amp;lt;dependency&amp;gt;
  &amp;lt;groupId&amp;gt;io.prometheus&amp;lt;/groupId&amp;gt;
  &amp;lt;artifactId&amp;gt;simpleclient_httpserver&amp;lt;/artifactId&amp;gt;
  &amp;lt;version&amp;gt;0.6.0&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;dependency를 추가하고, spring boot dependency를 추가하고나서 Java bean config쪽을 구성해보자&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Configuration
@Slf4j
public class PrometheusConfig {
    @PostConstruct
    public void init(){
        DefaultExports.initialize(); // Export JVM Metrics into http endpoint
    }

    @Bean
    public HTTPServer prometheusServer() throws IOException {
        log.info(&quot;promethues server has started&quot;);
        return new HTTPServer(9090);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;보면 그냥 configuration 클래스가 로딩될 때, DefaultExports로 initialize를 해주고, prometheus 서버를 띄워주는 역할만 하고 있다.&lt;/p&gt;
&lt;p&gt;이렇게 구성해서 띄워보면, 실제로 9090포트로 prometheus metric exporter가 구동이 되게 된다. 접근해보면 아래와 같은 페이지가 나오게 된다&lt;/p&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;# HELP jvm_memory_bytes_used Used bytes of a given JVM memory area.
# TYPE jvm_memory_bytes_used gauge
jvm_memory_bytes_used{area=&quot;heap&quot;,} 8.0815512E7
jvm_memory_bytes_used{area=&quot;nonheap&quot;,} 4.3263472E7
# HELP jvm_memory_bytes_committed Committed (bytes) of a given JVM memory area.
# TYPE jvm_memory_bytes_committed gauge
jvm_memory_bytes_committed{area=&quot;heap&quot;,} 1.6777216E8
jvm_memory_bytes_committed{area=&quot;nonheap&quot;,} 4.5481984E7
# HELP jvm_memory_bytes_max Max (bytes) of a given JVM memory area.
# TYPE jvm_memory_bytes_max gauge
jvm_memory_bytes_max{area=&quot;heap&quot;,} 1.908932608E9
jvm_memory_bytes_max{area=&quot;nonheap&quot;,} -1.0
# HELP jvm_memory_bytes_init Initial bytes of a given JVM memory area.
# TYPE jvm_memory_bytes_init gauge
jvm_memory_bytes_init{area=&quot;heap&quot;,} 1.34217728E8
jvm_memory_bytes_init{area=&quot;nonheap&quot;,} 2555904.0
# HELP jvm_memory_pool_bytes_used Used bytes of a given JVM memory pool.
# TYPE jvm_memory_pool_bytes_used gauge
jvm_memory_pool_bytes_used{pool=&quot;Code Cache&quot;,} 7142976.0
jvm_memory_pool_bytes_used{pool=&quot;Metaspace&quot;,} 3.1820632E7
jvm_memory_pool_bytes_used{pool=&quot;Compressed Class Space&quot;,} 4299864.0
jvm_memory_pool_bytes_used{pool=&quot;PS Eden Space&quot;,} 5.7521336E7
jvm_memory_pool_bytes_used{pool=&quot;PS Survivor Space&quot;,} 8909040.0
jvm_memory_pool_bytes_used{pool=&quot;PS Old Gen&quot;,} 1.4385136E7
...&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;값을 보게 되면 JVM에 사용되는 메트릭에 대한 정보가 export되었다는 걸 알 수 있다&lt;/p&gt;
&lt;p&gt;실제 DefaultExports.initialize(); 가 어떻게 구현되었는지를 보면 어떤 정보가 export되는지 볼 수 있는데&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;    public static synchronized void initialize() {
        if (!initialized) {
            (new StandardExports()).register();
            (new MemoryPoolsExports()).register();
            (new MemoryAllocationExports()).register();
            (new BufferPoolsExports()).register();
            (new GarbageCollectorExports()).register();
            (new ThreadExports()).register();
            (new ClassLoadingExports()).register();
            (new VersionInfoExports()).register();
            initialized = true;
        }

    }&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;메모리와 쓰레드, 그리고 GC등에 대한 정보를 가져와서 등록하는 부분이 있는걸 볼 수 있다.&lt;/p&gt;
&lt;p&gt;이제 JVM에 관련된 메트릭 말고 실제 원하는 메트릭을 export를 해봐야 하는데, 그러기 위해서는 export를 할 수 있는 metric의 타입이 어떤것들이 있는지부터 알아보고 가야한다&lt;/p&gt;
&lt;p&gt;상세 설명 및 정보는 공식 홈페이지에(&lt;a href=&quot;https://prometheus.io/docs/concepts/metric_types/&quot;&gt;https://prometheus.io/docs/concepts/metric_types/&lt;/a&gt;) 있다.&lt;/p&gt;
&lt;p&gt;크게 4가지의 메트릭 타입이 존재한다.&lt;/p&gt;
&lt;h4&gt;Counter&lt;/h4&gt;
&lt;p&gt;값을 나타내는 메트릭인데, &quot;증가&quot;만 할 수 있는 메트릭이라고 보면 된다. 카운터는 값을 증감시키거나, 0으로 초기화해서 다시 시작하거나 하는 기능만 지원을 한다. 그렇기 때문에 좀 제한적인 부분에만 사용을 할 수 있을 것 같은데, 아래와 같은 경우에 사용이 가능하지 않을까 싶다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;http total send bytes&lt;/li&gt;
&lt;li&gt;http total request&lt;/li&gt;
&lt;li&gt;running time&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Gauge&lt;/h4&gt;
&lt;p&gt;게이지는 카운터와 개념은 같지만, 값이 &quot;증감&quot;하거나 &quot;감소&quot;할 수 있다. 따라서 하나의 값을 나타내는 경우에는 대부분 게이지를 사용을 해서 구현을 하면 될 것 같다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;temperature&lt;/li&gt;
&lt;li&gt;current cpu usage&lt;/li&gt;
&lt;li&gt;current thread count&lt;/li&gt;
&lt;li&gt;...&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Histogram&lt;/h4&gt;
&lt;p&gt;특정 기간동안 측정된 값을 표현할 때 사용한다. 모든 메트릭 데이터의 합계를 제공하고 quantile을 계산(0.95, 0.99 ...) 할 수 있도록 지원한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;TPS(Transaction per second)&lt;/li&gt;
&lt;li&gt;특정 기간동안의 집계나 API 콜 수 등을 보고싶을때&lt;/li&gt;
&lt;li&gt;bucket 단위로 quantile을 집계한다.
&lt;ul&gt;
&lt;li&gt;default : .005, .01, .025, .05, .075, .1, .25, .5, .75, 1, 2.5, 5, 7.5, 10.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1591808065447&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 아래와 같이 특정 bucket에 해당하는 값을 집계한다
request_duration_bucket{le=&quot;0.005&quot;,} 0.0
request_duration_bucket{le=&quot;0.01&quot;,} 0.0
request_duration_bucket{le=&quot;0.025&quot;,} 0.0
request_duration_bucket{le=&quot;0.05&quot;,} 0.0
request_duration_bucket{le=&quot;0.075&quot;,} 0.0
request_duration_bucket{le=&quot;0.1&quot;,} 0.0
request_duration_bucket{le=&quot;0.25&quot;,} 0.0
request_duration_bucket{le=&quot;0.5&quot;,} 0.0
request_duration_bucket{le=&quot;0.75&quot;,} 0.0
request_duration_bucket{le=&quot;1.0&quot;,} 0.0
request_duration_bucket{le=&quot;2.5&quot;,} 0.0
request_duration_bucket{le=&quot;5.0&quot;,} 1.0
request_duration_bucket{le=&quot;7.5&quot;,} 1.0
request_duration_bucket{le=&quot;10.0&quot;,} 3.0
request_duration_bucket{le=&quot;+Inf&quot;,} 3.0&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Summary&lt;/h4&gt;
&lt;p&gt;히스토그램과 비슷하지만, &amp;phi;-quantiles을 지원한다는 점이 차이점이고 슬라이딩 시간단위의 윈도우를 계산할때 사용한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Histogram과 동일하게 특정 기간동안의 집계나, Request duration, response size 등을 모니터링 하고자 할 때 유용하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1591808179467&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Histogram과의 차이점은, bucket에 메트릭을 담는것이 아닌
// count,sum을 제공하고, 등록한 quantile을(custom하게 여러개 생성할 수 있다) 사용한다는 점이다.
request_duration_summary{quantile=&quot;0.95&quot;,} 7.4632192
request_duration_summary_count 5.0
request_duration_summary_sum 27.338737899999998&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Gauge 사용해보기&lt;/h3&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@RestController
@Slf4j
public class SampleController {
    private Gauge sampleGauge;

    @PostConstruct
    public void init() {
        sampleGauge = Gauge.build(&quot;sample&quot;, &quot;sample metrics&quot;).register();
    }

    @GetMapping
    public String sample() {
        sampleGauge.inc();
        return &quot;sample&quot;;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Gauge를 사용해봤는데, 위와 같이 사용을 하면 된다. Gauge의 build를 통해서 메트릭의 이름을 지정해주고 이를 register(); 해주면 해당 메트릭이 등록이 되게 된다.&lt;/p&gt;
&lt;p&gt;그리고 sample을 호출할때마다 sampleGauge의 값이 inc가 된다.&lt;/p&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;# HELP sample sample metrics
# TYPE sample gauge
sample 0.0&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;처음에 spring boot를 띄우게 되면 9090포트로 위와같은 메트릭이 export가 되게 되는데, localhost:8080으로 sample을 호출을 하게 되면 위의 sample값이 1.0 , 2.0 ... 와 같이 변하게 된다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;Counter.build(&quot;someCounter&quot;, &quot;Some counter&quot;).register();&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Counter의 경우에도 위와같이 build -&amp;gt; register를 하게 되면 metric에 등록이 되게 된다.&lt;/p&gt;
&lt;p&gt;간단한 gauge랑 counter의 경우는 위와같이 쓰면 되지만 실제로 많이 사용하게 될 메트릭들은 대부분 histogram과 summary일텐데 이건 공부가 좀더 필요해보인다. spring boot prometheus client(micrometer)를 기본으로 export하면 대부분 summary를 많이 사용하는 걸로 보인다(histogram은 안쓰고 summary만 씀)&lt;/p&gt;
&lt;h3&gt;Spring boot exporter&lt;/h3&gt;
&lt;p&gt;custom하게 metric을 export하는건 위에서 아주 살짝 맛만 봤는데, 실제로 잘 구현된 metric은 어떻게 구성되는지를 한번 알아보자. spring boot의 경우 actuator를 사용하면 별도의 설정없이 tomcat, jvm, spring에 관련된 정보를 export할 수 있다. (&lt;a href=&quot;https://dzone.com/articles/monitoring-using-spring-boot-2-prometheus-and-graf&quot;&gt;https://dzone.com/articles/monitoring-using-spring-boot-2-prometheus-and-graf&lt;/a&gt;)&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;!-- Spring boot actuator to expose metrics endpoint --&amp;gt;
&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring-boot-starter-actuator&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;
&amp;lt;!-- Micormeter core dependecy  --&amp;gt;
&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;io.micrometer&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;micrometer-core&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;
&amp;lt;!-- Micrometer Prometheus registry  --&amp;gt;
&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;io.micrometer&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;micrometer-registry-prometheus&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;pom.xml에 위의 dependency를 import하고&lt;/p&gt;
&lt;pre class=&quot;cmake&quot;&gt;&lt;code&gt;#Metrics related configurations
management.endpoint.metrics.enabled=true
management.endpoints.web.exposure.include=*
management.endpoint.prometheus.enabled=true
management.metrics.export.prometheus.enabled=true&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;application.properties에 위와 같은 config를 넣어주게 되면 localhost:8080/actuator/prometheus로 metric이 export되게 된다&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그리고 grafana에서 보여지게 될 tag를 붙여줘야 한다&lt;/p&gt;
&lt;p&gt;@Configuration 클래스를 하나 만들어서 아래의 bean을 등록해주자&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Configuration
@Slf4j
public class PrometheusCustomConfig {
    @Bean
    MeterRegistryCustomizer&amp;lt;MeterRegistry&amp;gt; metricsCommonTags() {
        return registry -&amp;gt; registry.config().commonTags(&quot;application&quot;, &quot;PROMETHEUS-SAMPLE-SERVER&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이렇게 해야 grafana에서 여러개의 scrape target이 있을때 tag를 가지고 구분을 할 수 있게 된다.&lt;/p&gt;
&lt;pre class=&quot;accesslog&quot;&gt;&lt;code&gt;# HELP http_server_requests_seconds  
# TYPE http_server_requests_seconds summary
http_server_requests_seconds_count{exception=&quot;None&quot;,method=&quot;GET&quot;,outcome=&quot;SUCCESS&quot;,status=&quot;200&quot;,uri=&quot;/actuator/prometheus&quot;,} 1.0
http_server_requests_seconds_sum{exception=&quot;None&quot;,method=&quot;GET&quot;,outcome=&quot;SUCCESS&quot;,status=&quot;200&quot;,uri=&quot;/actuator/prometheus&quot;,} 0.083506614
http_server_requests_seconds_count{exception=&quot;None&quot;,method=&quot;GET&quot;,outcome=&quot;SUCCESS&quot;,status=&quot;200&quot;,uri=&quot;/&quot;,} 5.0
http_server_requests_seconds_sum{exception=&quot;None&quot;,method=&quot;GET&quot;,outcome=&quot;SUCCESS&quot;,status=&quot;200&quot;,uri=&quot;/&quot;,} 0.094335788
http_server_requests_seconds_count{exception=&quot;None&quot;,method=&quot;GET&quot;,outcome=&quot;SUCCESS&quot;,status=&quot;200&quot;,uri=&quot;/**/favicon.ico&quot;,} 4.0
http_server_requests_seconds_sum{exception=&quot;None&quot;,method=&quot;GET&quot;,outcome=&quot;SUCCESS&quot;,status=&quot;200&quot;,uri=&quot;/**/favicon.ico&quot;,} 0.042993164
http_server_requests_seconds_count{exception=&quot;None&quot;,method=&quot;GET&quot;,outcome=&quot;SUCCESS&quot;,status=&quot;200&quot;,uri=&quot;/actuator&quot;,} 1.0
http_server_requests_seconds_sum{exception=&quot;None&quot;,method=&quot;GET&quot;,outcome=&quot;SUCCESS&quot;,status=&quot;200&quot;,uri=&quot;/actuator&quot;,} 0.073100047&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;한 예로, http server의 RPS(Request per second)의 경우에 위와 같이 export를 해주는데, 보면 uri와 httpStatus, method 등을 나타내준다. 이를 시간별로 sum, count값을 보여주므로 그래프에 표현하게 되면 시간대별 HTTP 호출 그래프를 그릴 수 있다(Grafana)&lt;/p&gt;
&lt;h2&gt;Prometheus Server에 메트릭 수집하기&lt;/h2&gt;
&lt;p&gt;위 서버를 구동시켜두고(8080포트) prometheus에서 이 서버로의 metric을 수집해보도록 config파일을 바꿔보자(공유기 내부 네트워크라서 192.168.0.x 로 설정을 했지만, 실 서비스에서는 prometheus서버가 metric export를 하는 서버에 http(80)으로 접근이 가능해야 수집이 가능하다)&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그리고 spring boot metric exporter를 사용하게 되면 scrape path가 달라져야 하는데, 기본으로는 host 주소를 적은 뒤 &quot;/metrics&quot;에서 긁어오지만 spring boot의 경우는 /actuator/prometheus에서 가져오기 때문에 아래와 같이 설정을 바꿔줘야 한다&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;# my global config
global:
  scrape_interval:     1s # Set the scrape interval to every 15 seconds. Default is every 1 minute.
  evaluation_interval: 1s # Evaluate rules every 15 seconds. The default is every 1 minute.
  # scrape_timeout is set to the global default (10s).

# Alertmanager configuration
alerting:
  alertmanagers:
  - static_configs:
    - targets:
      # - alertmanager:9093

# Load rules once and periodically evaluate them according to the global 'evaluation_interval'.
rule_files:
  # - &quot;first_rules.yml&quot;
  # - &quot;second_rules.yml&quot;

# A scrape configuration containing exactly one endpoint to scrape:
# Here it's Prometheus itself.
scrape_configs:
  # The job name is added as a label `job=&amp;lt;job_name&amp;gt;` to any timeseries scraped from this config.
  - job_name: 'prometeus-sample-server'
    # metrics_path defaults to '/metrics'
    # scheme defaults to 'http'.
    metrics_path: '/actuator/prometheus'
    static_configs:
            - targets: ['192.168.0.3:8080']
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;보면 scrape_configs에 metrics_path가 설정되었는데, 기본으로는 metrics지만 이를 actuator/prometheus로 변경해주는 부분이다. 이렇게 설정하고 잘 실행이 되면 prometheus dashboard를 가서 확인해보자&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/KiUVC/btquaPDrwgM/PvQQ3UdrmZWziFzZyfDgI1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/KiUVC/btquaPDrwgM/PvQQ3UdrmZWziFzZyfDgI1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/KiUVC/btquaPDrwgM/PvQQ3UdrmZWziFzZyfDgI1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKiUVC%2FbtquaPDrwgM%2FPvQQ3UdrmZWziFzZyfDgI1%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;아무거나(http_server_requests_second를 골랐다) 선택해서 execute를 한 뒤 그래프를 확인해보면 위와같이 잘 나오는걸 알 수 있다.&lt;/p&gt;
&lt;h2&gt;Grafana에서 그래프 연동하기&lt;/h2&gt;
&lt;p&gt;사실 이부분은 손이 매우많이 가기도 하고 어려운 부분이지만, 다행히도 이를 간편화해서 대시보드를 만들어놓으신분들이 있으셔서 grafana 공식 홈페이지에서 이를 받아서 사용할 수 있다&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://docs.grafana.org/installation/debian/&quot;&gt;http://docs.grafana.org/installation/debian/&lt;/a&gt; 에서 다운로드를 받아서 서버에 설치하자&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://grafana.com/api/dashboards/4701/images/5594/image&quot; alt=&quot;Grafana JVM Micrometer 대시보드&quot; width=&quot;850&quot; height=&quot;2248&quot; /&gt;&lt;/p&gt;
&lt;p&gt;이런 훌륭한 대시보드를 미리 만들어뒀으니 갖다 써보자.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;써보려고 하는 대시보드는JVM(Micrometer) 이다(&lt;a href=&quot;https://grafana.com/dashboards/4701&quot;&gt;https://grafana.com/dashboards/4701&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;&lt;b&gt;Datasource 등록하기&lt;/b&gt; : &lt;a href=&quot;http://docs.grafana.org/guides/getting_started/&quot;&gt;http://docs.grafana.org/guides/getting_started/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;Dashboard import하기&lt;/b&gt; : &lt;a href=&quot;http://docs.grafana.org/reference/export_import/&quot;&gt;http://docs.grafana.org/reference/export_import/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이 대시보드는 현재 Spring boot에서 내보내는 metric을 Prometheus 서버가 scrape해서 데이터를 가져왔고, 저장된 데이터를 기준으로 Grafana에서 보여주고 있는 것이다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;사실 간단한 연동이라고 해서 진행을 했지만 도중에 어려운 부분이 하나씩 있을테니 가이드를 보면서 차근차근 도전해보는게 좋다. 사실 Prometheus로 시작했지만, 끝 마무리는 Grafana로 맺게 되는데.. Grafana를 약식으로 소개해서 그렇지 이 하나만 가지고도 여러개의 포스팅이 나올 수 있을만큼 강력하고 간단한 오픈소스 대시보드 솔루션이다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이렇게 구성을 해두면 서비스를 하면서 문제가 생길만한 포인트를 사전에 알아본다던지, 현재 문제가 발생하는 부분이 어디에서 나는지(어떤 API가 현재 몇 TPS, Response time은 괜찮은지) 에 대한 정보를 얻을 수 있게 된다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;아무래도 devOps같은걸 하다보면 실제 개발만 하고 끝나는게 아니라 운영과 모니터링을 해야 하는데 그런 부분에서 Prometheus와 Grafana는 이런 정보를 얻기에 정말 간편한 구조를 채택하고 있음이 분명하고, 잘 활용을 한다면 높은 SLA 를 유지하는데 도움이 될 것이다.&lt;/p&gt;</description>
      <category>Development/Dev-ops, Monitoring</category>
      <category>prometheus</category>
      <category>Prometheus Monitoring</category>
      <category>Prometheus 모니터링</category>
      <author>@곰팡</author>
      <guid isPermaLink="true">https://gompangs.tistory.com/129</guid>
      <comments>https://gompangs.tistory.com/entry/Prometheus-%EB%A5%BC-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90#entry129comment</comments>
      <pubDate>Sat, 6 Apr 2019 11:32:43 +0900</pubDate>
    </item>
    <item>
      <title>Spring Password Encoder</title>
      <link>https://gompangs.tistory.com/entry/Spring-Password-Encoder</link>
      <description>&lt;p&gt;Spring에서는 인증/권한인가 등의 처리가 필요할 때 사용하라고 만든 &lt;code&gt;Spring Security&lt;/code&gt; 패키지가 존재한다.&lt;/p&gt;
&lt;p&gt;그 중 유저가 입력하는 Password를 암호화해서 저장하는 방법에 대해서 알아보자&lt;/p&gt;
&lt;p&gt;아, 그 전에 패스워드를 저장할 때 사용하는 알고리즘을 먼저 봐야 하는데 일단 패스워드는 무조건 &lt;code&gt;단방향&lt;/code&gt; 암호화/해싱을 사용해야 한다. 한번 encode된 패스워드는 다시 복호화를 할 수 없도록 해야 하고(AES,RSA,DES&amp;hellip; 등의 양방향 암호화를 사용하면 안된다는 뜻이다) 이를 비교하는 로직만 같은지 아닌지만 판단할 수 있게 만들어야 한다.&lt;/p&gt;
&lt;p&gt;이를 지키지 않을 경우 최악은 DB에 저장된 유저의 패스워드가 다 복호화 되어 개인정보가 털리던.. 혹은 결제와 관련된 경우 직접적인 타격을 받게될 수도 있다.&lt;/p&gt;
&lt;p&gt;혹여나, 지금이라도 패스워드를 AES 등으로 저장하여 사용하고 있다면 &lt;b&gt;당장 해싱하는 방향으로 바꾸도록 하자&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;그럼 유저가 패스워드를 잃어버렸을때 찾아주는 방법이 없지않냐! 할 수 있는데, 잘 생각해보면 우리 주위의 포털사이트나 웹사이트에서는 이미 &quot;패스워드 변경 메일&quot; 혹은 SMS등으로 인증을 해서 &quot;새로운 비밀번호&quot; 를 입력받도록 유도를 하고 있음을 알 수 있다.&lt;/p&gt;
&lt;p&gt;한번 encode된 패스워드를 다시 복호화할 이유가 없다는건 이런 해결책이 있기 때문이기도 하고, 완벽한 보안은 없으면서도 그나마 강력한 보안성을 유지하기 위해서는 위와 같은 방법을 취하는게 옳은 방법이다.&lt;/p&gt;
&lt;h1&gt;PasswordEncoder Interface&lt;/h1&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;public interface PasswordEncoder {
    String encode(CharSequence var1);

    boolean matches(CharSequence var1, String var2);

    default boolean upgradeEncoding(String encodedPassword) {
        return false;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;인터페이스를 먼저 보면, 간단하게 &lt;code&gt;encode()&lt;/code&gt;와 &lt;code&gt;matches()&lt;/code&gt;로 구성되어 있다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;encode&lt;/code&gt; 는 실제로 패스워드를 암호화 할 때 사용하고&lt;/p&gt;
&lt;p&gt;&lt;code&gt;matches&lt;/code&gt; 는 사용자에게 입력받은 패스워드를 비교하고자 할 때 사용한다&lt;/p&gt;
&lt;p&gt;default 메서드로 구현된 &lt;code&gt;upgradeEncoding&lt;/code&gt; 은 기본적으로 false를 리턴하나, Custom하게 구현할 경우 이를 기반으로 더 강력한 암호화를 실행할 것인지에 대한 로직처리를 해주면 될 것 같다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;upgradeEncoding() : true if the encoded password should be encoded again for better security, else false.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://gompangs.github.io/images/image-20190227211117809.png&quot; alt=&quot;PasswordEncoder 구현체 종류&quot; width=&quot;801&quot; height=&quot;284&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Spring에서 기본적으로 제공되는 PasswordEncoder의 종류는 위와 같은데&lt;/p&gt;
&lt;p&gt;&lt;code&gt;BCryptPasswordEncoder&lt;/code&gt;, &lt;code&gt;DelegatingPasswordEncoder&lt;/code&gt;, &lt;code&gt;SCryptPasswordEncoder&lt;/code&gt;, &lt;code&gt;Pbkdf2PasswordEncoder&lt;/code&gt; 가 decrepated 되지 않은 PasswordEncoder 이므로 이들 클래스를 살펴보도록 하자.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;BCryptPasswordEncoder&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;Implementation of PasswordEncoder that uses the BCrypt strong hashing function. Clients can optionally supply a &quot;strength&quot; (a.k.a. log rounds in BCrypt) and a SecureRandom instance. The larger the strength parameter the more work will have to be done (exponentially) to hash the passwords. The default value is 10.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Bcrypt&quot;&gt;BCrypt&lt;/a&gt; 라는 해시 함수를 사용한 구현체이다. 단순히 해시를 하는것 뿐만 아니라 &lt;code&gt;Salt&lt;/code&gt; 를 넣는 작업까지 하므로, 입력값이 같음에도 불구하고 매번 다른 encoded된 값을 return 해주게 된다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;    public String encode(CharSequence rawPassword) {
        String salt;
        if (this.strength &amp;gt; 0) {
            if (this.random != null) {
                salt = BCrypt.gensalt(this.strength, this.random);
            } else {
                salt = BCrypt.gensalt(this.strength);
            }
        } else {
            salt = BCrypt.gensalt();
        }
        // salt를 generate하고 rawPassword와 salt를 이용해 hashing을 한다
        return BCrypt.hashpw(rawPassword.toString(), salt);
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그러므로 Bcrypt를 이용했을 경우에는 &lt;code&gt;matches&lt;/code&gt; 함수를 잘 확인하고 사용하는게 좋다(입력값이 같아도 매번 출력물이 다르기 때문에 equal로 비교하려고 하면 패스워드가 계속 일치하지 않는 상황을 겪게될 수 있다)&lt;/p&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;        String input = &quot;hello world&quot;;
        String encoded = passwordEncoder.encode(input);

        // true
        Assert.assertTrue(passwordEncoder.matches(input, encoded));

        // false
        Assert.assertEquals(passwordEncoder.encode(input), passwordEncoder.encode(input));&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&quot;hello world&quot; 라는 문자열이 있을 때 이를 encode한 값을 encoded에 넣고 테스트를 해보면, matches 로 확인을 했을 경우에는 예상대로 &lt;code&gt;true&lt;/code&gt; 가 리턴되지만, equals 로 확인을 해보면 값이 &lt;code&gt;false&lt;/code&gt;로 리턴이 되는 것을 알 수 있다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;참고 : matches(rawPassword, encoded) 이다. rawPassword parameter에 암호화된 값을 넣지 말고, 사용자가 입력한 패스워드를 그대로 넣어주도록 하자&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;&amp;nbsp;&lt;/h1&gt;
&lt;h1&gt;DelegatingPasswordEncoder&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;A password encoder that delegates to another PasswordEncoder based upon a prefixed identifier.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;PasswordEncoder를 여러개 선언한 뒤, 상황에 맞게 골라쓸 수 있도록 지원하는 Encoder이다.&lt;/p&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt; String idForEncode = &quot;bcrypt&quot;;
 Map encoders = new HashMap&amp;lt;&amp;gt;();
 encoders.put(idForEncode, new BCryptPasswordEncoder());
 encoders.put(&quot;noop&quot;, NoOpPasswordEncoder.getInstance());
 encoders.put(&quot;pbkdf2&quot;, new Pbkdf2PasswordEncoder());
 encoders.put(&quot;scrypt&quot;, new SCryptPasswordEncoder());
 encoders.put(&quot;sha256&quot;, new StandardPasswordEncoder());
 PasswordEncoder passwordEncoder = new DelegatingPasswordEncoder(idForEncode, encoders);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위와 같이 선언을 하게 되면 PasswordEncoder에 선언된 &lt;code&gt;bcrypt&lt;/code&gt;, &lt;code&gt;noop&lt;/code&gt;, &lt;code&gt;pbkdf2&lt;/code&gt;, &lt;code&gt;scrypt&lt;/code&gt;, &lt;code&gt;sha256&lt;/code&gt; PasswordEncoder들이 들어가게 되고, 마지막에 선언했을 때 idForEncode를 bcrypt로 줬으니 최종으로 encoding이 되는 값은 bcrypt로 해싱이 된 패스워드가 return이 된다. 그리고 맨 앞에 prefix로 암호화를 한 방법이 붙는다&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;{bcrypt}$2a$10$UemKUf.cijGeJz6CJ/81auJKQVU0syWTJq2O.UGQXga9G.SCCKDR.&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위와 같이 맨 앞에 prefix로 저렇게 붙게 되어서, 어떤 암호화를 사용해서 encode를 했는지 구분하는 목적인것 같다.&lt;/p&gt;
&lt;p&gt;여러가지의 패스워드 암호화를 뒀을 때 매번 DI를 해서 안써도 되니 그럴때 사용하면 유용할 것으로 추측..&lt;/p&gt;
&lt;p&gt;matches를 호출하는건 인터페이스가 동일하다. prefix가 들어간 password를 앞에서 알아서 짤라서 맞는 encode방식으로 변환을 해준다.&lt;/p&gt;
&lt;h1&gt;&amp;nbsp;&lt;/h1&gt;
&lt;h1&gt;SCryptPasswordEncoder&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;Implementation of PasswordEncoder that uses the SCrypt hashing function. Clients can optionally supply a cpu cost parameter, a memory cost parameter and a parallelization parameter.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;좀 신기한놈인데, Scrypt 해시 함수를 사용하고, 추가적으로 cpu와 memory의 cost parameter를 넘길수 있게 되어있다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Scrypt&lt;/code&gt;는 실제로 &lt;a href=&quot;https://en.wikipedia.org/wiki/Salsa20&quot;&gt;Salsa20&lt;/a&gt; 라는 해싱방식을 사용한다고 한다.&lt;/p&gt;
&lt;p&gt;그리고 왠지모르게 Javadoc에 이 패스워드 해싱방식을 추천하지 않는다고 쓰여있다(..?). 해당 글을 읽다보니 보안문제가 존재하지만 그래도 여전히 쓸만하다&amp;hellip; 라는 느낌의 분석이 있었다(&lt;a href=&quot;https://blog.ircmaxell.com/2014/03/why-i-dont-recommend-scrypt.html&quot;&gt;https://blog.ircmaxell.com/2014/03/why-i-dont-recommend-scrypt.html&lt;/a&gt;)&lt;/p&gt;
&lt;h1&gt;&amp;nbsp;&lt;/h1&gt;
&lt;h1&gt;Pbkdf2PasswordEncoder&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;A &lt;code&gt;PasswordEncoder&lt;/code&gt; implementation that uses PBKDF2 with a configurable number of iterations and a random 8-byte random salt value&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;이름이 매우 특이한데, &lt;a href=&quot;https://en.wikipedia.org/wiki/PBKDF2&quot;&gt;PBKDF2&lt;/a&gt; 라는 해싱을 사용하는 이놈의 이름은(&lt;b&gt;Password-Based Key Derivation Function 2&lt;/b&gt;) 의 뜻을 가지고 있다. &lt;a href=&quot;https://d2.naver.com/helloworld/318732&quot;&gt;네이버 D2 포스팅에도 한번 소개가 되었었다&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;이 encoder는 해싱+salt를 동시에 진행한다(Bcrypt와 동일) 다만 차이점은 아래와 같다&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Pbkdf2PasswordEncoder : salt가 8bytes 랜덤 값이다&lt;/li&gt;
&lt;li&gt;BCryptPasswordEncoder : salt가 72bytes 랜덤값이다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;그렇기 때문에 bcrypt가 좀더 안전(하다고 말하기엔 좀 그렇지만, collision이 발생할 수학적 확률이 더 낮다고 볼 수 있다) 하기 때문에 Spring의 기본 PasswordEncoder는 bcrypt를 권장하는 것으로 보인다&lt;/p&gt;
&lt;h1&gt;&amp;nbsp;&lt;/h1&gt;
&lt;h1&gt;Conclusion&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;BCryptPasswordEncoder&lt;/code&gt; 혹은 &lt;code&gt;Pbkdf2PasswordEncoder&lt;/code&gt; 를 사용하자 인 것 같은데,&lt;/p&gt;
&lt;p&gt;JMH Benchmark를 돌려본 결과 Pbkdf2의 성능이 상대적으로 낮게 측정이 되었다.&lt;/p&gt;
&lt;p&gt;Pbkdf2가 좀더 심플하고, Random으로 받는 key bytes도 작아서 좋은 성능이 나올줄 알았는데 의외였다.&lt;/p&gt;
&lt;p&gt;&lt;b&gt;Throughput : scrypt &amp;gt; bcrypt &amp;gt;&amp;gt;&amp;gt; pbkdf2&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;평균 소모되는 operation 별 avg time또한 위와 같다.&lt;/p&gt;
&lt;p&gt;단일 쓰레드에서 암호화 되는 &lt;code&gt;encode()&lt;/code&gt; 만 테스트해본 결과이다.&lt;/p&gt;
&lt;p&gt;&lt;b&gt;테스트 코드&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@State(Scope.Benchmark)
public class EncodeTest {


    private BCryptPasswordEncoder bCryptPasswordEncoder;
    private SCryptPasswordEncoder sCryptPasswordEncoder;
    private Pbkdf2PasswordEncoder pbkdf2PasswordEncoder;
    private final String targetString = &quot;HELLO WORLD ENCODE TEST&quot;;

    @Setup(Level.Invocation)
    public void setUp() {
        bCryptPasswordEncoder= new BCryptPasswordEncoder();
        sCryptPasswordEncoder = new SCryptPasswordEncoder();
        pbkdf2PasswordEncoder = new Pbkdf2PasswordEncoder();
    }

    @Benchmark
    @Fork(value = 1, warmups = 1)
    @BenchmarkMode(Mode.All)
    public void bcrypt(){
        bCryptPasswordEncoder.encode(targetString);
    }

    @Benchmark
    @Fork(value = 1, warmups = 1)
    @BenchmarkMode(Mode.All)
    public void scrypt(){
        sCryptPasswordEncoder.encode(targetString);
    }

    @Benchmark
    @Fork(value = 1, warmups = 1)
    @BenchmarkMode(Mode.All)
    public void pbkdf2(){
        pbkdf2PasswordEncoder.encode(targetString);
    }

    public static void main(String[] args) throws IOException, RunnerException {
        org.openjdk.jmh.Main.main(args);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;b&gt;테스트 결과&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;# Run complete. Total time: 00:14:06

Benchmark                           Mode  Cnt   Score    Error  Units
EncodeTest.bcrypt                  thrpt   20  12.021 &amp;plusmn;  0.038  ops/s
EncodeTest.pbkdf2                  thrpt   20   2.279 &amp;plusmn;  0.015  ops/s
EncodeTest.scrypt                  thrpt   20  16.036 &amp;plusmn;  0.143  ops/s
EncodeTest.bcrypt                   avgt   20   0.083 &amp;plusmn;  0.001   s/op
EncodeTest.pbkdf2                   avgt   20   0.440 &amp;plusmn;  0.002   s/op
EncodeTest.scrypt                   avgt   20   0.066 &amp;plusmn;  0.002   s/op
EncodeTest.bcrypt                 sample  241   0.085 &amp;plusmn;  0.001   s/op
EncodeTest.bcrypt:bcrypt&amp;middot;p0.00    sample        0.082            s/op
EncodeTest.bcrypt:bcrypt&amp;middot;p0.50    sample        0.083            s/op
EncodeTest.bcrypt:bcrypt&amp;middot;p0.90    sample        0.087            s/op
EncodeTest.bcrypt:bcrypt&amp;middot;p0.95    sample        0.091            s/op
EncodeTest.bcrypt:bcrypt&amp;middot;p0.99    sample        0.117            s/op
EncodeTest.bcrypt:bcrypt&amp;middot;p0.999   sample        0.168            s/op
EncodeTest.bcrypt:bcrypt&amp;middot;p0.9999  sample        0.168            s/op
EncodeTest.bcrypt:bcrypt&amp;middot;p1.00    sample        0.168            s/op
EncodeTest.pbkdf2                 sample   60   0.439 &amp;plusmn;  0.004   s/op
EncodeTest.pbkdf2:pbkdf2&amp;middot;p0.00    sample        0.430            s/op
EncodeTest.pbkdf2:pbkdf2&amp;middot;p0.50    sample        0.434            s/op
EncodeTest.pbkdf2:pbkdf2&amp;middot;p0.90    sample        0.452            s/op
EncodeTest.pbkdf2:pbkdf2&amp;middot;p0.95    sample        0.455            s/op
EncodeTest.pbkdf2:pbkdf2&amp;middot;p0.99    sample        0.462            s/op
EncodeTest.pbkdf2:pbkdf2&amp;middot;p0.999   sample        0.462            s/op
EncodeTest.pbkdf2:pbkdf2&amp;middot;p0.9999  sample        0.462            s/op
EncodeTest.pbkdf2:pbkdf2&amp;middot;p1.00    sample        0.462            s/op
EncodeTest.scrypt                 sample  328   0.063 &amp;plusmn;  0.001   s/op
EncodeTest.scrypt:scrypt&amp;middot;p0.00    sample        0.059            s/op
EncodeTest.scrypt:scrypt&amp;middot;p0.50    sample        0.061            s/op
EncodeTest.scrypt:scrypt&amp;middot;p0.90    sample        0.071            s/op
EncodeTest.scrypt:scrypt&amp;middot;p0.95    sample        0.073            s/op
EncodeTest.scrypt:scrypt&amp;middot;p0.99    sample        0.081            s/op
EncodeTest.scrypt:scrypt&amp;middot;p0.999   sample        0.090            s/op
EncodeTest.scrypt:scrypt&amp;middot;p0.9999  sample        0.090            s/op
EncodeTest.scrypt:scrypt&amp;middot;p1.00    sample        0.090            s/op
EncodeTest.bcrypt                     ss        0.090            s/op
EncodeTest.pbkdf2                     ss        0.824            s/op
EncodeTest.scrypt                     ss        0.355            s/op&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Development/Spring</category>
      <category>Bcrypt</category>
      <category>Password Encoder</category>
      <category>pbkdf2</category>
      <category>scrypt</category>
      <category>Spring</category>
      <category>Spring Password Encoder</category>
      <author>@곰팡</author>
      <guid isPermaLink="true">https://gompangs.tistory.com/128</guid>
      <comments>https://gompangs.tistory.com/entry/Spring-Password-Encoder#entry128comment</comments>
      <pubDate>Sat, 6 Apr 2019 11:18:26 +0900</pubDate>
    </item>
    <item>
      <title>Dispatcher Servlet</title>
      <link>https://gompangs.tistory.com/entry/Dispatcher-Servlet</link>
      <description>&lt;p&gt;&lt;img src=&quot;https://gompangs.github.io/images/huiyV.jpg&quot; alt=&quot;DispatcherServlet 구조&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/servlet/DispatcherServlet.html&quot;&gt;DispatcherServlet Javadoc 원문 설명&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;DispatcherServlet은 Spring의 모든 HTTP Call을 클라이언트(browser, http client …) 받아서 View 혹은 데이터를 내려주는 역할을 한다. 그리고 Handler를 등록해서 편리하게 url 매핑을 하거나, Exception Handling을 하는 것을 지원한다.&lt;/p&gt;
&lt;p&gt;Spring에서 제공하는 servlet은 유연한 interface를 가지고 있고, 적절한 adapter class를 사용해서 이를 handling할 수 있으며, MVC Framework의 기본적인 기능을 제공한다.&lt;/p&gt;
&lt;p&gt;MVC Pattern을 먼저 알아봐야 할 것 같은데,&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i.stack.imgur.com/BYaCl.png&quot; alt=&quot;mvc pattern&quot;&gt;&lt;/p&gt;
&lt;p&gt;이 아키텍쳐가 기본적인 MVC Pattern이며, 아래의 구성요소를 가지고 있다&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;Controller&lt;/code&gt; : 유저가 사용하는 url로 구성이 되며, business logic을 처리해서 model에 값을 전달한다&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;Model&lt;/code&gt; : Controller에서 처리된 데이터를 View로 내려주기 위해서 사용된다&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;View&lt;/code&gt; : User에게 실제적으로 보여질 데이터를 처리한다&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;매번 이런 패턴을 익히 들었을 법 한데, 실제로 Spring에서 이러한 패턴이 어떻게 활용되는지를 알아보자&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Controller&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;@Controller
public class ExampleController {

    @GetMapping(&amp;quot;/foo&amp;quot;)
    @ResponseBody
    public ResponseEntity foo(Bar bar){
        // write logic of processing `foo`
        return ResponseEntity.ok().body(new Bar());
    }    
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Controller는 간단하게 위와 같이 나타낼 수 있다. 유저가 요청한 url에 대한 mapping(&lt;code&gt;/foo&lt;/code&gt;)에 대해서 요청을 처리 받고, 해당 logic을 처리한 다음에 값을 return해주는 역할을 담당한다.&lt;/p&gt;
&lt;p&gt;위의 경우는 &lt;code&gt;@ResponseBody&lt;/code&gt; Annotation이 달려있기 때문에 View를 return하는게 아니라, Bar 라는 객체를 JSON으로 wrapping 해서 내려주는 케이스인데, Spring에서 View를 내려주려면 아래와 같이 해야 한다&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Controller
public class ViewController {
    @GetMapping(&amp;quot;/foo/view&amp;quot;)
    public String foo() {
        return &amp;quot;foo&amp;quot;;
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;위와 같이 코드를 작성하게 되면 Spring 내부에서 &lt;code&gt;ViewResolver&lt;/code&gt;가 동작해서 &lt;code&gt;foo&lt;/code&gt; 라는 view를 찾아서 View를 유저에게 return을 해주게 된다. 공식적으로 지원하게 되는 View template는 JSP, Thymeleaf, FreeMarker, Groovy, Jade4j, Velocity, JMustache, Pebble 이 있다.&lt;/p&gt;
&lt;p&gt;DispatcherServlet에서는 &lt;code&gt;@Controller&lt;/code&gt; 어노테이션이 달린 클래스나, &lt;code&gt;@RequestMapping&lt;/code&gt; 어노테이션이 달린 메서드가 기본적으로 auto-scan 되고 해당 url 혹은 beanName을 기반으로 등록이 된다. 이는 &lt;code&gt;ApplicationContext&lt;/code&gt; 에 추가가 되며(&lt;code&gt;@Bean&lt;/code&gt; 으로 등록이 된다는 말), &lt;code&gt;RequestMappingHandlerMapping&lt;/code&gt;이 초기화 될 때 ApplicationContext에 추가된 &lt;code&gt;@Controller&lt;/code&gt; 에 해당하는 handler들을 모두 불러와서 해당 url에 맞게 DispatcherServlet에 등록이 되게 된다.&lt;/p&gt;
&lt;p&gt;Exception resolution strategy가 존재하는데 &lt;a href=&quot;https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerExceptionResolver.html&quot;&gt;&lt;code&gt;ExceptionHandlerExceptionResolver&lt;/code&gt;&lt;/a&gt; &lt;a href=&quot;https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/servlet/mvc/annotation/ResponseStatusExceptionResolver.html&quot;&gt;&lt;code&gt;ResponseStatusExceptionResolver&lt;/code&gt;&lt;/a&gt;, &lt;a href=&quot;https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolver.html&quot;&gt;&lt;code&gt;DefaultHandlerExceptionResolver&lt;/code&gt;&lt;/a&gt;. 등을 사용해서 기본적인 Exception Handling을 한다. Whitelabel Error 라고 하면서 나오는 에러는 이 Resolver들에 의해서 Exception이 Catch되어 나타나게 되는 페이지이다.&lt;/p&gt;
&lt;p&gt;그 밖에도 View를 찾기 위한 &lt;a href=&quot;https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/servlet/ViewResolver.html&quot;&gt;&lt;code&gt;ViewResolver&lt;/code&gt;&lt;/a&gt; , Multipart를 Handling하기 위한 &lt;a href=&quot;https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/multipart/MultipartResolver.html&quot;&gt;&lt;code&gt;MultipartResolver&lt;/code&gt;&lt;/a&gt; , 접근하는 사용자의 locale을 알기 위한 &lt;a href=&quot;https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/servlet/LocaleResolver.html&quot;&gt;&lt;code&gt;LocaleResolver&lt;/code&gt;&lt;/a&gt;. 그리고 페이지의 테마를 위한(?) &lt;a href=&quot;https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web//ThemeResolver.html&quot;&gt;&lt;code&gt;ThemeResolver&lt;/code&gt;&lt;/a&gt; 정도가 기본으로 제공되는 Resolver 라고 보면 되겠다.&lt;/p&gt;
&lt;p&gt;이런식으로 기본적인 MVC패턴을 이용해서 웹서비스를 하기 위한 기본적인 핸들러들이 등록이 되어 있으며, 필요에 따라 Customizing 하거나 Interface를 이용해서 새로 만들어서 추가를 하여 사용하기도 한다.&lt;/p&gt;
&lt;p&gt;그럼 실제로 Request가 들어왔을 때 어떤 call stack으로 호출이 되는지 한번 알아보자&lt;/p&gt;
&lt;p&gt;임의로 Controller에서 RuntimeException을 throw해봤다. (&amp;quot;/&amp;quot; url 을 GET으로 호출)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;         ┌───────────────────────────┐
         │  java.lang.Thread.run()   │
         └───────────────────────────┘
                       │
                       ▼
┌─────────────────────────────────────────────┐
│java.util.concurrent.ThreadPoolExecutor.run()│
└─────────────────────────────────────────────┘
                       │
                       ▼
┌─────────────────────────────────────────────┐
│  undertow.server.HttpServerExchange.run()   │
└─────────────────────────────────────────────┘
                       │
                       ▼
┌─────────────────────────────────────────────┐
│      undertow.servlet.handleRequest()       │
└─────────────────────────────────────────────┘
                       │
                       ▼
┌─────────────────────────────────────────────┐
│  AbstractSecurityContextAssociationHandler  │ // SpringSecurity로 인해 호출됨
└─────────────────────────────────────────────┘
                       │
                       ▼
┌─────────────────────────────────────────────┐
│       AuthenticationMechanismsHandler       │ // SpringSecurity 인증flow 
└─────────────────────────────────────────────┘
                       │
                       ▼
┌─────────────────────────────────────────────┐
│          ServletDispatchingHandler          │ // 드디어 이때 call
└─────────────────────────────────────────────┘
                       │
                       ▼
┌─────────────────────────────────────────────┐
│               FilterChainImpl               │ // Charset, Security filter 호출
└─────────────────────────────────────────────┘
                       │
                       ▼
┌─────────────────────────────────────────────┐
│          ServletHandler.service()           │ 
└─────────────────────────────────────────────┘
                       │
                       ▼
┌─────────────────────────────────────────────┐
│            HttpServlet.service()            │ // undertow -&amp;gt; spring servlet
└─────────────────────────────────────────────┘
                       │
                       ▼
┌─────────────────────────────────────────────┐
│          FrameworkServlet.doGet()           │ // doGet()을 호출한다
└─────────────────────────────────────────────┘
                       │
                       ▼
┌─────────────────────────────────────────────┐
│      FrameworkServlet.processRequest()      │ // 최종
└─────────────────────────────────────────────┘&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;아마 상황에 따라 호출되는 filter와 handler가 다르겠지만, 기본적으로는 위와 같이 호출이 되는걸 볼 수 있다 (spring boot tomcat -&amp;gt; undertow로 교체하여 호출해본 결과)&lt;/p&gt;
&lt;p&gt;원래 Spring boot에 오기 전에 Spring Framework를 사용했을 때는 web.xml을 수정해야 했던적이 있었다. web.xml에 url을 등록해서 해당 url로 들어왔을때의 Servlet Handler를 직접 지정해주는 방식을 사용했는데, 현재는 대부분 DispatcherServlet을 따로 설정하지 않고 기본적으로 모든 Request에 대해서 요청을 받는 DispatcherServlet을 1개 두고, 뒤에 Handler(&lt;code&gt;@Controller&lt;/code&gt;) 를 둬서 처리하는 방식이 일반적이다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;web-app&amp;gt;

    &amp;lt;servlet&amp;gt;
        &amp;lt;servlet-name&amp;gt;example&amp;lt;/servlet-name&amp;gt;
        &amp;lt;servlet-class&amp;gt;org.springframework.web.servlet.DispatcherServlet&amp;lt;/servlet-class&amp;gt;
        &amp;lt;load-on-startup&amp;gt;1&amp;lt;/load-on-startup&amp;gt;
    &amp;lt;/servlet&amp;gt;

    &amp;lt;servlet-mapping&amp;gt;
        &amp;lt;servlet-name&amp;gt;example&amp;lt;/servlet-name&amp;gt;
        &amp;lt;url-pattern&amp;gt;*.form&amp;lt;/url-pattern&amp;gt;
    &amp;lt;/servlet-mapping&amp;gt;

&amp;lt;/web-app&amp;gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;예전에는 위와 같이 DispatcherServlet을 servlet으로 등록한 뒤, 해당 servlet을 직접 mapping(servlet-mapping) 해주는 작업을 해주곤 했었다..&lt;/p&gt;
&lt;p&gt;번외로 Spring에서 기본적으로 지원하는 Servlet 엔진으로는 &lt;code&gt;Tomcat&lt;/code&gt;, &lt;code&gt;Undertow&lt;/code&gt;, &lt;code&gt;Jetty&lt;/code&gt; 가 있다. (기본으로는 Embedded Tomcat이 사용된다) 인터페이스를 정의해두고 내부적으로 돌아가는 Setvlet Engine은 이를 구현함으로써 다양한 ServletEngine으로 교체하여 사용할 수 있다는 장점이 있다.&lt;/p&gt;</description>
      <category>Development/Spring</category>
      <category>Dispatcher</category>
      <category>servlet</category>
      <category>Spring</category>
      <category>Spring Dispatcher Servlet</category>
      <author>@곰팡</author>
      <guid isPermaLink="true">https://gompangs.tistory.com/127</guid>
      <comments>https://gompangs.tistory.com/entry/Dispatcher-Servlet#entry127comment</comments>
      <pubDate>Sat, 6 Apr 2019 11:13:29 +0900</pubDate>
    </item>
    <item>
      <title>HTTP에 관해서</title>
      <link>https://gompangs.tistory.com/entry/HTTP%EC%97%90-%EA%B4%80%ED%95%B4%EC%84%9C</link>
      <description>&lt;h1&gt;HTTP&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;HTTP&lt;/code&gt; 는 HyperText Transfer Protocol 의 약자로, 오늘날의 인터넷에서 주로 사용하는 데이터를 송/수신하기 위한 프로토콜이다. 최초의 HTTP를 이용한 데이터 송수신은&lt;code&gt;GET&lt;/code&gt; 방식의 &lt;code&gt;HTML&lt;/code&gt; 을 위주로 이루어졌으나(문서를 표현하기 위해서) 이는 추후에 여러 메소드 및 미디어 타입이 추가됨에 따라 확장을 했다.&lt;/p&gt;
&lt;p&gt;HTTP는 프로토콜이기 때문에 최초 버전에서부터 현재까지의 유의미한 버전이 존재한다.&lt;/p&gt;
&lt;p&gt;근데 놀랍게도 대부분의 프로토콜이 큰 변화를 겪는 반면에, HTTP의 경우는 버저닝이 많지가 않아서 크게 세 가지 정도로 히스토리를 정리할 수 있다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;HTTP 1.0&lt;/li&gt;
&lt;li&gt;HTTP 1.1&lt;/li&gt;
&lt;li&gt;HTTP 2.0&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;HTTP/1.0&lt;/h2&gt;
&lt;p&gt;HTTP는 원래 0.9v 부터 시작되었다고 하지만, 사실상 1.0버전이 상용화되어 1996년부터 사용되기 시작했다.&lt;/p&gt;
&lt;p&gt;위에 언급했다시피, 최초는 HTML 문서를 송/수신하기 위한 &lt;code&gt;GET&lt;/code&gt; Method 와 &lt;code&gt;HTML&lt;/code&gt; 파일을 위주로 통신이 이루어졌다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://gompang.dev/content/images/2019/03/HTTP_RequestMessage.png&quot; alt=&quot;HTTP_RequestMessage&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;br /&gt;Request Header부분과 이를 new line으로 개행한 뒤 body를 보내는 것이 HTTP 1.0의 프로토콜이다.(1.1 또한 같다)&lt;/p&gt;
&lt;h2&gt;HTTP/1.1&lt;/h2&gt;
&lt;p&gt;HTTP 1.1 같은 경우는 HTTP 1.0의 단순한 구조를 확장하는 방식으로 설계가 되었는데, 현재 사용되는 HTTP Method 및 각종 미디어 타입에 대한 지원 등이 이때 추가가 되게 된다.&lt;/p&gt;
&lt;h3&gt;&lt;a href=&quot;https://tools.ietf.org/html/rfc7230&quot;&gt;RFC 7230&lt;/a&gt;, &lt;i&gt;HTTP/1.1: Message Syntax and Routing&lt;/i&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;URL&lt;/code&gt; 을 나타내는 방법과 이를 송/수신하는 방법에 대한 스펙을 정의&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Connection&lt;/code&gt; 을 관리하는 방법과 이를 파이프라이닝 하는 방법에 대한 내용&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Compress&lt;/code&gt; 에 관련된 내용(gzip, deflate, compress) &amp;hellip; 와 관련된 내용&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;a href=&quot;https://tools.ietf.org/html/rfc7231&quot;&gt;RFC 7231&lt;/a&gt;, &lt;i&gt;HTTP/1.1: Semantics and Content&lt;/i&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;HTTP Method&lt;/code&gt; 정의가 추가되었다(&lt;code&gt;GET&lt;/code&gt;, &lt;code&gt;POST&lt;/code&gt; , &lt;code&gt;PUT&lt;/code&gt;, &lt;code&gt;DELETE&lt;/code&gt; &amp;hellip;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;HTTP Status Code&lt;/code&gt; 에 대한 정의가 추가되었다(2xx, 3xx, 4xx, 5xx)&lt;/li&gt;
&lt;li&gt;Content의 타입의 확장을 고려한 &lt;code&gt;Accept&lt;/code&gt;, &lt;code&gt;Charset&lt;/code&gt; 등의 Negotiation과 관련된 헤더가 추가되었다.&lt;/li&gt;
&lt;li&gt;이 밖에도 많은 헤더와 스펙에 관련된 내용이 다수 있는데, HTTP/1.1의 주된 변경점이 이 RFC문서에 담겨있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;a href=&quot;https://tools.ietf.org/html/rfc7232&quot;&gt;RFC 7232&lt;/a&gt;, &lt;i&gt;HTTP/1.1: Conditional Requests&lt;/i&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Last-Modified&lt;/code&gt; 와 &lt;code&gt;ETag&lt;/code&gt; 에 관련된 내용이다.&lt;/li&gt;
&lt;li&gt;어떻게보면 RFC 7234의 캐싱부분과 비슷한데, 브라우저가 &lt;code&gt;Last-Modified&lt;/code&gt; 의 시간과 서버로 요청하는 &lt;code&gt;If-Modified-Since&lt;/code&gt; Header의 시간을 비교해서 데이터가 새로 갱신이 되지 않았을 경우 &lt;code&gt;304 Not Modified&lt;/code&gt; 를 return하며 데이터를 전송하지 않고 이전에 캐싱해뒀던 데이터를 꺼내주게 된다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ETag&lt;/code&gt; 는 정적 컨텐츠의 변경 여부를 판별할 수 있는 값을 나타내어 &lt;code&gt;Last-Modified&lt;/code&gt; 처럼 컨텐츠를 캐싱할 때 사용하도록 스펙을 정의했다. &lt;code&gt;If-None-Match&lt;/code&gt; 헤더와 같이 사용하며, 정적 컨텐츠를 요청하는 응답에 &lt;code&gt;ETag&lt;/code&gt; 를 내려주고, 이를 가지고 정적 컨텐츠가 변경이 있었는지의 여부를 판단하여 &lt;code&gt;304 Not Modified&lt;/code&gt; 를 사용한다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Nginx&lt;/code&gt; 나 &lt;code&gt;Apache&lt;/code&gt; 같은 웹서버에서는 이를 자동으로 지원하여, 정적 컨텐츠(css, js, img &amp;hellip;) 에 대해서는 ETag를 자동으로 달아주는 기능을 제공하기도 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;a href=&quot;https://tools.ietf.org/html/rfc7233&quot;&gt;RFC 7233&lt;/a&gt;, &lt;i&gt;HTTP/1.1: Range Requests&lt;/i&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;206 Partial Content&lt;/code&gt; 와 관련된 내용으로 ,큰 데이터를 요청할 때 한번에 전송하는게 아닌 여러 Chunk단위로 쪼개서&lt;/li&gt;
&lt;li&gt;데이터를 송/수신하는 방법에 대한 스펙을 정의했다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;a href=&quot;https://tools.ietf.org/html/rfc7234&quot;&gt;RFC 7234&lt;/a&gt;, &lt;i&gt;HTTP/1.1: Caching&lt;/i&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;HTTP의 캐싱에 대한 스펙이 추가가 되었다.&lt;/li&gt;
&lt;li&gt;현재 사용하고 있는 Web server(&lt;code&gt;Apache&lt;/code&gt;, &lt;code&gt;Nginx&lt;/code&gt; &amp;hellip;) 의 서버들 혹은 어플리케이션에서 직접 제공하는 캐싱기능에 대한 내용이다.
&lt;ul&gt;
&lt;li&gt;주로 &lt;code&gt;Cache-Control: max-age=&amp;lt;seconds&amp;gt;&lt;/code&gt; 와 같은 Header를 추가해서 얼마만큼 캐싱을 할 것인지를 정해서 사용을 한다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;a href=&quot;https://tools.ietf.org/html/rfc7235&quot;&gt;RFC 7235&lt;/a&gt;, *HTTP/1.1: Authentication&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://mdn.mozillademos.org/files/14689/HTTPAuth.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;HTTP Endpoint의 인증을 위한 Basic Authentication 방식이 추가가 되었고, 실제 이를 기반으로 여러 인증수단이 발전되어 현재 사용중이다(&lt;code&gt;OAuth&lt;/code&gt;, &lt;code&gt;OpenID&lt;/code&gt;, &lt;code&gt;API-KEY Authentication&lt;/code&gt; &amp;hellip;)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;큰 개념은 요청 Header에 인증정보를 넣어 유효한 요청인지를 확인하는 방법을 제공하는 것이다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;혹은, 아래와 같이 username과 password를 넣어서 endpoint를 호출하는 방식도 존재한다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;https://username:password@www.example.com/&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;위와 같은 여러 스펙의 추가로 인해서, HTTP/1.0 과는 현저히 많은 데이터를 표현하고 송/수신하는데 문제가 없을 정도로 최초 설계를 잘 하였고, 반면에 대부분의 프로토콜은 많은 변경으로 점차 안정적으로 변해가는데, 추후 확장을 고려한 스펙을 미리 설계를 해놓은 것은 지금에 와서 봐도 납득할만한 스펙이라고 생각이 되었다.&lt;/p&gt;
&lt;p&gt;이 뒤에 HTTP/1.2 등이 나오지 않고 바로 HTTP/2.0으로 넘어가게 되는것을 봐도 그 뒤로 특별한 무언가가 추가되지 않아도 될 만큼의 프로토콜인 듯 하다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2&gt;HTTP/2.0(&lt;a href=&quot;https://developers.google.com/web/fundamentals/performance/http2/?hl=ko&quot;&gt;Google 설명자료&lt;/a&gt;)&lt;/h2&gt;
&lt;p&gt;HTTP/2.0은 비교적 최근에 나온 프로토콜 규약으로, 2015년에 발표가 되었다. (2012년도부터 제안이 있었다고 한다 이는 Google이 개발한 &lt;code&gt;SPDY&lt;/code&gt; 라는 프로토콜을 기준으로 높은 성능의 HTTP로 개선을 하기 위한 제안이라고 보여진다)&lt;/p&gt;
&lt;p&gt;HTTP/1.1만큼의 파워풀한 스펙은 없지만, 오늘날의 많은 데이터를 처리하기 위한 효율적인 &lt;code&gt;데이터 파이프라이닝&lt;/code&gt; 과 &lt;code&gt;높은 처리율&lt;/code&gt; 을 위주로 설계가 된 것 같다.&lt;/p&gt;
&lt;p&gt;주요 문제점들은 비효율적인 데이터 송/수신에 있다.&lt;/p&gt;
&lt;p&gt;기술 자체에 문제가 있는것은 아니지만, 데이터가 필요할때마다 매번 서버로의 Connection을 요청하고 Response를 기다려서 받아야하는 단점이 존재했고 실제로 이와 같은 스펙으로 인하여 주고 받는 데이터가 많아지면 많아질 수록 중복되는 Header와 Blocking 때문에 성능상의 단점이 많이 부각되어 왔었다.&lt;/p&gt;
&lt;p&gt;HTTP/2.0에서는 이러한 데이터를 주고 받는 부분에서의 비효율적인 부분을 개선하기 위해 등장했다.&lt;/p&gt;
&lt;p&gt;주요 개념으로는 &lt;code&gt;Stream&lt;/code&gt;, &lt;code&gt;Message&lt;/code&gt;, &lt;code&gt;Frame&lt;/code&gt; 이 있다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;i&gt;스트림&lt;/i&gt;: 구성된 연결 내에서 전달되는 바이트의 양방향 흐름이며, 하나 이상의 메시지가 전달될 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;i&gt;메시지&lt;/i&gt;: 논리적 요청 또는 응답 메시지에 매핑되는 프레임의 전체 시퀀스입니다.&lt;/li&gt;
&lt;li&gt;&lt;i&gt;프레임&lt;/i&gt;: HTTP/2에서 통신의 최소 단위이며 각 최소 단위에는 하나의 프레임 헤더가 포함됩니다. 이 프레임 헤더는 최소한으로 프레임이 속하는 스트림을 식별합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Server Push&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://developers.google.com/web/fundamentals/performance/http2/images/push01.svg?hl=ko&quot; alt=&quot;HTTP/2.0 Server Push&quot; width=&quot;855&quot; height=&quot;333&quot; /&gt;&lt;/p&gt;
&lt;p&gt;HTTP/1.1에서는 단방향(Request -&amp;gt; Response) 로의 데이터 송/수신이 이루어졌다면, HTTP/2.0에서는 위와 같이 서버에서 데이터를 보낼 수 있는 Server Push 기능이 추가가 되었다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;PUSH_PROMISE&lt;/code&gt; 프레임을 통해 서버에서 클라이언트로 데이터를 보내겠다는 PROMISE를 한 뒤, 이를 Response가 보내지기 전에 먼저 보낸다.&lt;/p&gt;
&lt;p&gt;클라이언트는 이를 수신한 뒤에 어떤 리소스가 올 지를 예측할 수 있고, 해당 리소스가 오게 되면&lt;/p&gt;
&lt;h3&gt;Prioritize&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://developers.google.com/web/fundamentals/performance/http2/images/stream_prioritization01.svg?hl=ko&quot; alt=&quot;HTTP/2.0 Prioritize&quot; width=&quot;855&quot; height=&quot;361&quot; /&gt;&lt;/p&gt;
&lt;p&gt;위와 같은 Server Push에서만 봐도, 프레임의 순서는 매우 중요하다. Response가 가기 전에 먼저 보내야 할 프레임(&lt;code&gt;PUSH_PROMISE&lt;/code&gt;)이 있기도 하고 나중에 보내도 될 프레임(&lt;code&gt;DATA FRAME&lt;/code&gt;)이 존재하기도 하기 때문에 모든 송수신 데이터에 대해서 우선순위를 지정해서 통신을 하게 된다. 이는 위의 예 말고도, 여러 데이터에 대한 가중치를 계산을 하여 순서를 정하게 된다.&lt;/p&gt;
&lt;h3&gt;Pipelining, HOL(Head of Line) Blocking 문제 해결&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;HTTP/1.1의 경우는 데이터를 얻기 위해서는 이전의 데이터가 다 전송이 되어야 다음 데이터를 전송하는 구조로 되어 있다. &lt;code&gt;Keep-Alive&lt;/code&gt; 를 사용하면 1.1을 사용해도 데이터를 파이프라이닝을 할 수 있지만, HTTP/2.0 스펙에서는 서버와의 연결이 된 한 Stream에서는 모든 요청이 pipelining되어 처리된다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;아래 sequence에서 보다시피 데이터를 송/수신하는데 걸리는 시간이 많이 줄어들게 되며, 이전 데이터를 받는걸 기다리는 대기시간(Head Of Line) 이 없어지게 된다.&lt;/p&gt;
&lt;img src=&quot;https://cdn-images-1.medium.com/max/1600/1*H_KToW3hIhbSNjDktKtwJQ.png&quot; alt=&quot;HTTP HOL Blocking&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Multiplexing&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://developers.google.com/web/fundamentals/performance/http2/images/multiplexing01.svg?hl=ko&quot; alt=&quot;Http/2.0 Multiplexing&quot; width=&quot;856&quot; height=&quot;237&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Multiplexing의 경우는 서버와의 연결을 맺은 뒤, HTTP 메세지를 프레임단위로 쪼개서 송/수신하는 방식으로 원래는 HTTP/1.1에서 여러 Connection을 맺은 뒤 데이터를 주고 받아야 하는데, pipelining 과 같이 한 채널을 통해 데이터를 쪼개서 보내는 방식을 의미한다(사실 pipelining과 multiplexing의 차이에 대해서는 잘 모르겠다.)&lt;/p&gt;
&lt;h3&gt;Compression&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://developers.google.com/web/fundamentals/performance/http2/images/header_compression01.svg?hl=ko&quot; alt=&quot;HTTP/2.0 Compression&quot; width=&quot;853&quot; height=&quot;718&quot; /&gt;&lt;/p&gt;
&lt;p&gt;HTTP/1.1의 비효율적인 부분 중 하나는 중복적인 헤더로 인한 송수신 데이터의 크기가 크다는 것에 있다.&lt;/p&gt;
&lt;p&gt;아무래도 똑같은 리소스를 주고 받는데도 같은 데이터를 계속해서 보내고 받는것은 비효율적일 수 밖에 없다.&lt;/p&gt;
&lt;p&gt;HTTP/2.0에서는 위와 같이, 이미 받은 데이터에 대해서는 implicit 하게 데이터를 받은 것으로 간주하여, 새로운 프레임에 대한 데이터만 비교하여 받게 된다. 이를 통해 전송되는 헤더의 크기가 85% ~ 88% 정도 감소했다고 한다(&amp;hellip; 엄청나다)&lt;/p&gt;
&lt;h3&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3&gt;Performance&lt;/h3&gt;
&lt;p&gt;성능 얘기를 안할 수 없는데, HTTP/1.1에다가 &lt;code&gt;Keep-Alive&lt;/code&gt; 를 통해 파이프라이닝을 해서 HTTP/2.0과 비교를 하게 되면 성능이 드라마틱하게 차이가 나지는 않는다.&lt;/p&gt;
&lt;p&gt;HTTP/1.1을 쌩으로 사용하여 HTTP/2.0과 비교를 했을 경우는.. 엄청난 성능차이를 보인다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.deliciousbrains.com/content/uploads/2016/12/16102436/http1-waterfall.png&quot; alt=&quot;http1-waterfall&quot; width=&quot;856&quot; height=&quot;464&quot; /&gt;&lt;/p&gt;
&lt;p&gt;위는 HTTP/1.1을 사용했을 때의 웹 페이지 로딩 결과이다.&lt;/p&gt;
&lt;p&gt;보면 알겠지만, 어떤 데이터를 받기 위해서는 이전에 보낸 데이터의 전송이 끝난 경우에만 데이터를 보내게 된다(간혹 같이 전송이 시작된 경우도 있는데, 이는 HTTP/1.1의 Pipelining을 이용해 비슷한 류의 데이터끼리 묶어서 전송하기 때문이다)&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.deliciousbrains.com/content/uploads/2016/12/16102440/http2-waterfall.png&quot; alt=&quot;http2-waterfall&quot; width=&quot;853&quot; height=&quot;462&quot; /&gt;&lt;/p&gt;
&lt;p&gt;HTTP/2.0의 경우에는 위와같이 데이터가 병렬로 동시에 송수신이 진행이 된다.&lt;/p&gt;
&lt;p&gt;이러한 차이로 성능상에서는 HTTP/2.0이 더 많은 요청을 동시에 처리할 수 있는 여건이 된다.&lt;/p&gt;
&lt;p&gt;다만 무조건 HTTP/2.0을 쓰면 빨라지고 좋아진다.. 는 아니다&lt;/p&gt;
&lt;p&gt;실 서비스에서 Tomcat9에 HTTP/2.0을 적용해서 테스트를 해본 결과 열 수 있는 최대 Connection이 어느정도 정해져있고, Stream을 열 때 마다 많은 메모리를 소모하여 무조건 많은 부하가 들어온다고 해서 이를 만병통치약처럼 해결을 해주지는 않는다. 적절한 튜닝과 설정값의 변경으로 서비스 적용을 하는 것이 좋아보인다.&lt;/p&gt;
&lt;p&gt;여담으로, &lt;code&gt;HTTP&lt;/code&gt; 에 HTTP/2.0를 사용을 하면 &lt;code&gt;h2c&lt;/code&gt; 라는 프로토콜로 통신을 하게 되고, &lt;code&gt;HTTPS&lt;/code&gt; 에 HTTP/2.0을 사용하게 되면 &lt;code&gt;h2&lt;/code&gt; 라는 프로토콜로 통신을 하게 된다.&lt;/p&gt;
&lt;p&gt;직접 테스트를 해보려면&lt;/p&gt;
&lt;p&gt;&lt;br /&gt;&lt;a href=&quot;https://imagekit.io/demo/HTTP2-vs-HTTP1&quot;&gt;https://imagekit.io/demo/HTTP2-vs-HTTP1&lt;/a&gt; 를 방문해서 이미지가 로드되는 속도를 비교해보도록 하자. HTTP/2.0의 경우에 더 빠른 속도를 보이는 것을 알 수 있다.&lt;/p&gt;</description>
      <category>Development/Web</category>
      <category>http</category>
      <category>HTTP/1.1</category>
      <category>HTTP/2.0</category>
      <author>@곰팡</author>
      <guid isPermaLink="true">https://gompangs.tistory.com/126</guid>
      <comments>https://gompangs.tistory.com/entry/HTTP%EC%97%90-%EA%B4%80%ED%95%B4%EC%84%9C#entry126comment</comments>
      <pubDate>Sat, 6 Apr 2019 11:11:11 +0900</pubDate>
    </item>
  </channel>
</rss>