<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>&amp;quot;꾸준하게, 지속하자 &amp;quot;</title>
    <link>https://ulsanboy.tistory.com/</link>
    <description>
꾸준하게, 지속하자
끝까지 하자  
</description>
    <language>ko</language>
    <pubDate>Tue, 7 Jul 2026 16:45:29 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>최동훈1</managingEditor>
    <image>
      <title>&amp;quot;꾸준하게, 지속하자 &amp;quot;</title>
      <url>https://tistory1.daumcdn.net/tistory/4744029/attach/c0386cbf3cc541a8976d0c2bcaf07527</url>
      <link>https://ulsanboy.tistory.com</link>
    </image>
    <item>
      <title>DFS를 이용한 순열과 조합의 차이</title>
      <link>https://ulsanboy.tistory.com/184</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;백문이 불여일타 ! 코드로써 설명한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;DFS의 파라미터로 Start를 넣느냐 안넣는냐의 차이. (permutaion은 0부터 시작)&lt;br /&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;순열의 경우&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1724600345441&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package 이코테.DFSBFS;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Scanner;

public class 연산자끼워넣기 {
    static int[] arr;
    static ArrayList&amp;lt;String&amp;gt; calculate = new ArrayList&amp;lt;&amp;gt;();

    static ArrayList&amp;lt;ArrayList&amp;lt;String&amp;gt;&amp;gt; output = new ArrayList&amp;lt;ArrayList&amp;lt;String&amp;gt;&amp;gt;();

    static boolean[] visit;
    static int plus;
    static int minus;
    static int multiplex;
    static int divide;
    //7:37~ 8: 41
    public static void main(String[] args) {

        Scanner s = new Scanner(System.in);

        int N = s.nextInt();

         arr = new int[N];
        visit = new boolean[N];
        Arrays.fill(visit, false);

        for (int i = 0; i &amp;lt; N; i++) {
            arr[i]=s.nextInt();
        }
         plus=s.nextInt();
         minus=s.nextInt();
         multiplex=s.nextInt();
         divide=s.nextInt();

        for (int i = 0; i &amp;lt; plus; i++) {
            calculate.add(&quot;+&quot;);
        }
        for (int i = 0; i &amp;lt; minus; i++) {
            calculate.add(&quot;-&quot;);
        }
        for (int i = 0; i &amp;lt; multiplex; i++) {
            calculate.add(&quot;*&quot;);
        }
        for (int i = 0; i &amp;lt; divide; i++) {
            calculate.add(&quot;/&quot;);
        }

        dfs(new int[calculate.size()], calculate.size(), 0);
        int max=-(int)1e9;
        int min=(int)1e9;

        for (int i = 0; i &amp;lt; output.size(); i++) {

            int sum = arr[0];
            //하나의 경우의 수별 최대 최소 계산
            ArrayList&amp;lt;String&amp;gt; list = output.get(i);

            for (int j = 1; j &amp;lt; arr.length; j++) {
                String order = list.get(j - 1);
                if (order.equals(&quot;+&quot;)) {
                    sum+=arr[j];
                } else if (order.equals(&quot;-&quot;)) {
                    sum-=arr[j];
                } else if (order.equals(&quot;*&quot;)) {
                    sum *= arr[j];
                } else if (order.equals(&quot;/&quot;)) {
                    if (sum &amp;lt; 0) {
                        sum = Math.abs(sum);
                        sum=-(sum/arr[j]);
                    }
                    else {

                        sum/=arr[j];
                    }
                }
            }
            max = Math.max(sum, max);
            min = Math.min(sum, min);

        }

        System.out.println(max);
        System.out.println(min);

    }
    // 순열 개산하기
    public static void dfs(int[] now, int end, int depth){
        //0 ~ end 범위에서 now.length 개의 인덱스를 뽑아서 나열하는 경우의 수 계산
        //(calculate 리스트의 범위) depth가 증가하는 상한은 now.length 개.

        if (depth == now.length) {
            //하나의 경우의 수 씩 해당 인덱스 값에 대응하는 연산자를 삽입.
            ArrayList&amp;lt;String&amp;gt; tmp = new ArrayList&amp;lt;&amp;gt;();
            for (int index = 0; index &amp;lt; now.length; index++) {

                int idx = now[index];

                tmp.add(calculate.get(idx));
            }
            output.add(tmp);
            return;
        }
        for (int i = 0; i &amp;lt; end; i++) {
            if (!visit[i]) {
                visit[i]=true;

                //여기서 나는 뽑아야되는 범위(Calculate 리스트)의 인덱스를 저장할 것임.
                //해당 '인덱스' 값으로 now 배열 완성시 Calculate에서 다시 조회해서 최종 output에 저장.
                now[depth] = i;
                //now -&amp;gt; 하나의 경우의 수 를 저장하기위한 배열
                //실수 주의 !!! depth는 조합을 뽑는 배열의 인덱스임.
                dfs(now, end, depth + 1);
                visit[i]=false;
            }


        }

    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;조합의 경우&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1724600395594&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package 이코테.DFSBFS;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Scanner;
import java.util.StringTokenizer;

public class 연구소 {

    public static ArrayList&amp;lt;Node&amp;gt; empty = new ArrayList&amp;lt;&amp;gt;();
    public static int N;
    public static int M;
    public static int[][] map;
    public static boolean[] visit;

    public static ArrayList&amp;lt;Node&amp;gt; virus = new ArrayList&amp;lt;&amp;gt;();
    public static ArrayList&amp;lt;ArrayList&amp;lt;Node&amp;gt;&amp;gt; combination = new ArrayList&amp;lt;ArrayList&amp;lt;Node&amp;gt;&amp;gt;();
    //6:00~
    public static void main(String[] args) throws IOException {
        Scanner s = new Scanner(System.in);

         N = s.nextInt();
         M = s.nextInt();

         map=new int[N][M];


        for (int i = 0; i &amp;lt; N; i++) {
            for (int j = 0; j &amp;lt; M; j++) {
                int input=s.nextInt();
                map[i][j]=input;
                if (input == 0) {
                    empty.add(new Node(i, j));
                    continue;
                }
                if (input == 2) {
                    virus.add(new Node(i, j));
                    continue;
                }
            }

        }
        visit = new boolean[empty.size() + 1];
        Arrays.fill(visit,false);

        int max=-(int)1e9;
        int[] now=new int[3];
        Arrays.fill(now, -1);
        combi(now,0, empty.size(),0);
//        System.out.println(&quot;조합목록&quot;);
//        for (int i = 0; i &amp;lt; combination.size(); i++) {
//
//            System.out.println(combination.get(i).toString());
//        }



        for (int i = 0; i &amp;lt; combination.size(); i++) {
            int[][] temp=new int[N][M];
            for (int c = 0; c &amp;lt; map.length; c++) {

                for (int k = 0; k &amp;lt; map[c].length; k++) {
                    temp[c][k]= map[c][k];
//                    System.out.print(temp[c][k]+&quot; &quot;);
                }
            }

            //한 조합별로 벽 세우기
            for (int j = 0; j &amp;lt; combination.get(i).size(); j++) {
                Node node = combination.get(i).get(j);

                temp[node.x][node.y]=1;
            }

            for (Node virus : virus) {

                dfs(virus.x, virus.y,temp);

            }
            max=Math.max(count(temp),max);


        }
        System.out.println(max);



    }

    public static int count(int[][] tmp) {
        int cnt=0;
        for(int i=0;i&amp;lt;tmp.length;i++){
            for (int j = 0; j &amp;lt; tmp[i].length; j++) {
                if (tmp[i][j] == 0) {
                    cnt++;
                }
            }
        }
        return cnt;
    }

    public static void dfs(int x, int y,int[][] tmp) {
        //매우 중요, 시뮬레이션 처럼, 시작 점을 &quot;이동&quot; 하는 것이 아님.
        //시작점은 무조건 고정이고, nx,ny룰 큐에 넣거나 재귀호출 하는 것임.
        //이유: 재귀 호출이 끝나고 return 되었을때, 처음 시작점인 x,y 값이
        //변화해 버림.


        int[] dx={-1,0,+1,0};//상 우 하 좌
        int[] dy={0,+1,0,-1};

        for (int i = 0; i &amp;lt; 4; i++) {
            int nx=x+dx[i];
            int ny=y+dy[i];

            if(nx&amp;lt;0||ny&amp;lt;0||nx&amp;gt;=N||ny&amp;gt;=M){
                continue;
            }
            if (tmp[nx][ny] == 0) { //빈방일 경우에만 방문.

                tmp[nx][ny]=2;

                dfs(nx,ny,tmp);
            }

        }
        return;


    }
    public static void combi(int[] now, int start, int end, int depth){
        //start ~ end 까지의 범위에서 3개를 뽑는 조합.(depth는 뽑는 조합의 인덱스임)
        if (depth == now.length) {
            //3개를 다 뽑았으면 저장 후 return,

            ArrayList&amp;lt;Node&amp;gt; temp = new ArrayList&amp;lt;&amp;gt;();
            for (int i = 0; i &amp;lt; now.length; i++) {
                temp.add(empty.get(now[i]));
            }
            combination.add(temp);

            return;
        }

        for (int i = start; i &amp;lt; end; i++) {
            if (!visit[i]) {
                visit[i]=true;
                now[depth] = i;
                combi(now,i+1,end,depth+1);
                visit[i]=false;
            }
        }
    }
    static class Node{
        int x;
        int y;
        public Node(int x,int y){
            this.x=x;
            this.y=y;
        }
        @Override
        public String toString(){
            return &quot;( &quot;+x+&quot; )&quot;+&quot;( &quot;+y+&quot; )&quot;;

        }
    }
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>학교 수업 정리/과제 정리</category>
      <author>최동훈1</author>
      <guid isPermaLink="true">https://ulsanboy.tistory.com/184</guid>
      <comments>https://ulsanboy.tistory.com/184#entry184comment</comments>
      <pubDate>Mon, 26 Aug 2024 00:40:38 +0900</pubDate>
    </item>
    <item>
      <title>RefreshToken은 왜 Redis를 사용해 관리할까? (with. RTR 방식)</title>
      <link>https://ulsanboy.tistory.com/183</link>
      <description>&lt;h2 id=&quot;1.%20%E2%80%9C%EC%99%9C%3F%E2%80%9D%20%F0%9F%A4%94-1&quot; style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://pgmjun.tistory.com/125#1.%20%E2%80%9C%EC%99%9C%3F%E2%80%9D%20%F0%9F%A4%94-1&quot;&gt;1. &amp;ldquo;왜?&amp;rdquo;  &lt;/a&gt;&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;우리는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;MySQL&lt;/b&gt;과 같은 일반 DB를 다룰 줄 아는데 사람들은&lt;br /&gt;&lt;b&gt;왜&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;굳이 리프레시 토큰을&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Redis&lt;/b&gt;에 저장하여 사용하는지 이유가 궁금했으며,&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;왜&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;다들 Redis를 사용해서 리프레시 토큰을 저장하는 지 궁금했습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;2.%C2%A0Redis%EA%B0%80%20%EB%AD%94%EB%8D%B0%3F-1&quot; style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://pgmjun.tistory.com/125#2.%C2%A0Redis%EA%B0%80%20%EB%AD%94%EB%8D%B0%3F-1&quot;&gt;2.&amp;nbsp;Redis가 뭔데?&lt;/a&gt;&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;레디스는 디스크가 아닌 메모리에 데이터를 저장하는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;In-Memory 방식&lt;/b&gt;의 데이터베이스입니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 id=&quot;2-1.%20In-Memory%3F-1&quot; style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://pgmjun.tistory.com/125#2-1.%20In-Memory%3F-1&quot;&gt;2-1. In-Memory?&lt;/a&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;In-Memory 데이터베이스&lt;/b&gt;는 MySQL과 같은 다른 일반 DB들처럼&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;SSD, HDD&lt;/b&gt;와 같은 보조기억장치가 아닌, 프로세서가 직접 액세스할 수 있는 컴퓨터의 주 메모리인&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;RAM&lt;/b&gt;에 데이터를 저장합니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 id=&quot;2-2.%20%EB%94%94%EC%8A%A4%ED%81%AC%EC%97%90%20%EC%A0%80%EC%9E%A5%ED%95%98%EB%8A%94%20%EA%B2%83%EA%B3%BC%20%EB%AC%B4%EC%8A%A8%EC%B0%A8%EC%9D%B4%EA%B0%80%20%EC%9E%88%EB%8A%94%EB%8D%B0%3F-1&quot; style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://pgmjun.tistory.com/125#2-2.%20%EB%94%94%EC%8A%A4%ED%81%AC%EC%97%90%20%EC%A0%80%EC%9E%A5%ED%95%98%EB%8A%94%20%EA%B2%83%EA%B3%BC%20%EB%AC%B4%EC%8A%A8%EC%B0%A8%EC%9D%B4%EA%B0%80%20%EC%9E%88%EB%8A%94%EB%8D%B0%3F-1&quot;&gt;2-2. 디스크에 저장하는 것과 무슨차이가 있는데?&lt;/a&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;특정 프로그램을 실행하면, 아래와 같이 컴퓨터는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;보조기억장치&lt;/b&gt;에 저장된 데이터를&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;RAM&lt;/b&gt;으로 불러와&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;CPU&lt;/b&gt;가 해당 데이터를 처리하는 과정으로 실행됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-06-13 오후 1.29.54.png&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;188&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dWFdUG/btsIuHDGHcy/TISi5KEIWs5T5kecSDLui1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dWFdUG/btsIuHDGHcy/TISi5KEIWs5T5kecSDLui1/img.png&quot; data-alt=&quot;Disk-RAM-CPU 플로우&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dWFdUG/btsIuHDGHcy/TISi5KEIWs5T5kecSDLui1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdWFdUG%2FbtsIuHDGHcy%2FTISi5KEIWs5T5kecSDLui1%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;188&quot; data-filename=&quot;스크린샷 2023-06-13 오후 1.29.54.png&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;188&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Disk-RAM-CPU 플로우&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #ffffff; color: #353638; text-align: center;&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;여기서 장점이 확연히 보이지 않나요?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;RAM에 데이터를 저장하여 사용하게 되면&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;보조기억장치에서 데이터를 Load하는 비용이 절약&lt;/b&gt;됩니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;때문에 인메모리 데이터베이스의 읽기 및 쓰기 연산은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;기존 디스크 기반 데이터베이스보다 훨씬 빠릅니다.&lt;/b&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #ffffff; color: #353638; text-align: center;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;하지만 보조기억장치와는 다르게 RAM은 전원이 끊어지면 데이터가 전부 지워지는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;휘발성 메모리(Volatile Memory)&lt;/b&gt;라는 특징이 있다!&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;3.%20Redis%EC%9D%98%20%ED%8A%B9%EC%A7%95-1&quot; style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://pgmjun.tistory.com/125#3.%20Redis%EC%9D%98%20%ED%8A%B9%EC%A7%95-1&quot;&gt;3. Redis의 특징&lt;/a&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;Key-Value의 형태의 데이터베이스이기 때문에 적은 메모리로도 데이터를 저장할 수 있으며, 작성 속도가 빠름&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;Key-Value 형태를 가지고 있기 때문에 키를 알고 있다면 조회 성능이&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;O(1)&lt;/b&gt;까지 나온다는 장점을 가짐&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;인메모리 DB 방식으로 빠르게 접근이 가능&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;휘발성인 In-Memory DB는 영구적으로 저장될 필요가 없는 Refresh token을 관리하기에 충분&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;캐시처럼 데이터 만료일을 정할 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 id=&quot;3-1.%20Redis%EC%9D%98%20%ED%8A%B9%EC%A7%95%EC%9D%B4%20Refresh%20Token%20%EB%B3%B4%EA%B4%80%EC%97%90%20%EC%9D%B4%EC%A0%90%EC%9D%B4%20%EB%90%98%EB%8A%94%20%EC%9D%B4%EC%9C%A0-1&quot; style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://pgmjun.tistory.com/125#3-1.%20Redis%EC%9D%98%20%ED%8A%B9%EC%A7%95%EC%9D%B4%20Refresh%20Token%20%EB%B3%B4%EA%B4%80%EC%97%90%20%EC%9D%B4%EC%A0%90%EC%9D%B4%20%EB%90%98%EB%8A%94%20%EC%9D%B4%EC%9C%A0-1&quot;&gt;3-1. Redis의 특징이 Refresh Token 보관에 이점이 되는 이유&lt;/a&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;이러한 Redis의 특징을 토대로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Refresh Token&lt;/b&gt;을 보관하는 것이 적합한 이유를 설명해보자면 아래와 같습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal; background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li style=&quot;list-style-type: decimal;&quot;&gt;우선 RDB와는 다르게 데이터의 만료일을 지정할 수 있습니다. 이를&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #374151; text-align: start;&quot;&gt;&lt;b&gt;TTL(Time-To-Live)&lt;/b&gt;이라고 하는데,&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;이를 토큰의 만료일과 똑같이 맞춰두어 관리하면 토큰이 만료되면&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Redis&lt;/b&gt;에서도 토큰이 삭제되도록 하여 데이터를 효율적으로 관리할 수 있습니다.&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: decimal;&quot;&gt;대체로 30분~2시간 단위로 갱신하는 JWT Access Token은 새롭게 갱신하기 위해 Refresh Token이 필요합니다.&lt;br /&gt;이렇게 호출의 빈도가 대체적으로 높은 Refresh Token은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;RDB&lt;/b&gt;에 저장하는 것보다&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;In-Memory DB&lt;/b&gt;에 저장해두고 사용하는 것이 훨씬 속도가 빠르고, 토큰&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;ReIssue&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;로직의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;병목현상&lt;/b&gt;&lt;span&gt;을 방지할 수 있다습니다.&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: decimal;&quot;&gt;모든 기술 사용에는 트레이드 오프가 존재합니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;RDB&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;vs&lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;In-Memory DB&lt;/b&gt;의 트레이드 오프는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;속도&lt;/b&gt;와&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;안정성&lt;/b&gt;이라고 생각합니다.&lt;br /&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;&lt;b&gt;In-Memory&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;방식은 굉장히 뛰어난 조회 속도를 얻을 수 있어 호출의 빈도가 높은 토큰&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;ReIssue&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;로직의 병목현상을 방지할 수 있습니다. 하지만&amp;nbsp;&lt;/span&gt;&lt;b&gt;In-Memory DB&lt;/b&gt;는 휘발성 메모리이기 때문에 전원이 끊어지면 데이터가 전부 지워집니다.&lt;br /&gt;그런데 단지&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Refresh Token&lt;/b&gt;만을 관리하기 위해&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Redis&lt;/b&gt;를 사용한다면 이 트레이드 오프는 손실보다 이득이 더 클 수도 있습니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Refresh Token&lt;/b&gt;이 모두 삭제되었을 때 발생하는 가장 큰 사건은 모든 유저가 재로그인을 해야한다는 점인데, 웬만해선 전원이 끊어질 일이 자주 발생하지 않고 때문에 유저 경험에 그리 크리티컬하게 작용하지 않는다고 생각합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;4.%20%EA%B7%B8%EB%9F%BC%20RefreshToken%EC%9D%80%20%EC%99%9C%20DB%EC%97%90%20%EC%A0%80%EC%9E%A5%ED%95%B4%EC%84%9C%20%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94%20%EA%B1%B4%EB%8D%B0%3F-1&quot; style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://pgmjun.tistory.com/125#4.%20%EA%B7%B8%EB%9F%BC%20RefreshToken%EC%9D%80%20%EC%99%9C%20DB%EC%97%90%20%EC%A0%80%EC%9E%A5%ED%95%B4%EC%84%9C%20%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94%20%EA%B1%B4%EB%8D%B0%3F-1&quot;&gt;4. 그럼 RefreshToken은 왜 DB에 저장해서 사용하는 건데?&lt;/a&gt;&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;우선 리프레시 토큰과 엑세스 토큰에 대해 알아보겠습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;&lt;b&gt;엑세스 토큰:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;서버에&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #c0d1e7;&quot;&gt;&lt;b&gt;API를 직접 요청&lt;/b&gt;(Access)&lt;/span&gt;할 때 사용&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;&lt;b&gt;리프레시 토큰&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;엑세스 토큰이 만료되었을 때, 엑세스 토큰을&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #c0d1e7;&quot;&gt;&lt;b&gt;재발급&lt;/b&gt;(Refresh)&lt;/span&gt;할 때 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;이 둘이 분리된 이유는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;보안&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;토큰을 탈취당하면 다른 사람이 내 계정의 권한을 사용하여 서비스를 마음대로 사용할 위험이 있기 때문에 절대 노출되면 안됩니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;JWT 토큰은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Stateless&lt;/b&gt;라는 HTTP의 특징에 의해 한번 발급되면 그 뒤로 토큰의 상태에 대해 관리할 수 없으며,&lt;br /&gt;그렇다보니 회원이 토큰을 통해 요청을 보낸 건지, 해커가 토큰을 탈취해서 요청을 보낸 건지 알 수 없습니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #ffffff; color: #353638; text-align: center;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;때문에 유저 인증 역할을 가진 엑세스 토큰은 대체로 만료시간을 짧게 잡습니다. (보통 30분 ~ 2시간 정도로 생각하는데, 서비스에 따라 다릅니다)&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;이러한 엑세스 토큰이 만료되면 리프레시 토큰을 통해 새로운 엑세스 토큰을 발급받게 됩니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;때문에 리프레시 토큰은 엑세스 토큰보다 훨씬 긴 만료시간을 가집니다. (저는 보통 1주 ~ 1개월 정도로 생각하는데, 서비스에 따라 다를 수 있습니다)&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 id=&quot;4-1.%20%EA%B7%B8%EB%9F%BC%20%ED%86%A0%ED%81%B0%EC%9D%B4%20%ED%83%88%EC%B7%A8%EB%8B%B9%ED%95%98%EB%A9%B4%20%EC%96%B4%EB%96%BB%EA%B2%8C%20%EB%90%A0%EA%B9%8C%EC%9A%94%3F-1&quot; style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://pgmjun.tistory.com/125#4-1.%20%EA%B7%B8%EB%9F%BC%20%ED%86%A0%ED%81%B0%EC%9D%B4%20%ED%83%88%EC%B7%A8%EB%8B%B9%ED%95%98%EB%A9%B4%20%EC%96%B4%EB%96%BB%EA%B2%8C%20%EB%90%A0%EA%B9%8C%EC%9A%94%3F-1&quot;&gt;4-1. 그럼 토큰이 탈취당하면 어떻게 될까요?&lt;/a&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;엑세스 토큰만 탈취 당한다면 30분 ~ 2시간 정도만 유저의 권한을 마음대로 이용하고 그 뒤엔 사용할 수 없지만,&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;엑세스 토큰과 리프레쉬 토큰이 모두 탈취 당하면 최악의 경우 해커는 계속해서 엑세스 토큰을 재발급하며 유저의 권한을 악용할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 id=&quot;4-2.%C2%A0%EC%9D%B4%EB%A5%BC%20%EB%B0%A9%EC%A7%80%ED%95%98%EA%B8%B0%20%EC%9C%84%ED%95%B4%20%EC%9A%B0%EB%A6%AC%EB%8A%94%20Refresh%20Token%EC%9D%84%20DB%EC%97%90%20%EC%A0%80%EC%9E%A5%ED%95%B4%EB%86%93%EA%B3%A0%20%EC%82%AC%EC%9A%A9%ED%95%A9%EB%8B%88%EB%8B%A4.-1&quot; style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://pgmjun.tistory.com/125#4-2.%C2%A0%EC%9D%B4%EB%A5%BC%20%EB%B0%A9%EC%A7%80%ED%95%98%EA%B8%B0%20%EC%9C%84%ED%95%B4%20%EC%9A%B0%EB%A6%AC%EB%8A%94%20Refresh%20Token%EC%9D%84%20DB%EC%97%90%20%EC%A0%80%EC%9E%A5%ED%95%B4%EB%86%93%EA%B3%A0%20%EC%82%AC%EC%9A%A9%ED%95%A9%EB%8B%88%EB%8B%A4.-1&quot;&gt;4-2.&amp;nbsp;이를 방지하기 위해 우리는 Refresh Token을 DB에 저장해놓고 사용합니다.&lt;/a&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;우선 위의 말 처럼&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;데이터베이스(Redis)&lt;/b&gt;에&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;유저 정보&lt;/b&gt;와&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Refresh Token&lt;/b&gt;을 저장하여 사용합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;Key:&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;유저 고유정보 (ex. member Entity ID)&lt;/b&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;Value:&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;RefreshToken&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;그리고 이와 함께&amp;nbsp;&lt;b&gt;Refresh Token Rotation(RTR)&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;방식으로 리프레쉬 토큰을 관리하도록 설정합니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;RTR&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;방식은 RefreshToken을 DB에서 관리할 때 적용할 수 있는 방식입니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;플로우를 살펴볼까요?&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot;&gt;(좌) 토큰 저장 및 갱신 플로우 / (우) RefreshToken 만료 시 플로우&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;1979&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/XTrDL/btsIvPndXxF/XQsNMl5MBdb99UhojZIeuk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/XTrDL/btsIvPndXxF/XQsNMl5MBdb99UhojZIeuk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/XTrDL/btsIvPndXxF/XQsNMl5MBdb99UhojZIeuk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXTrDL%2FbtsIvPndXxF%2FXQsNMl5MBdb99UhojZIeuk%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;1979&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;1979&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;1682&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cDzw84/btsIuwhZ5K2/cQ3lEriUokcoIoq8bKih3k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cDzw84/btsIuwhZ5K2/cQ3lEriUokcoIoq8bKih3k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cDzw84/btsIuwhZ5K2/cQ3lEriUokcoIoq8bKih3k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcDzw84%2FbtsIuwhZ5K2%2FcQ3lEriUokcoIoq8bKih3k%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;1682&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;1682&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Access Token&lt;/b&gt;이 만료되어 토큰을 갱신시키는 시점에&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;클라이언트는 토큰 갱신을 위해 토큰 갱신 API 에 AccessToken, RefreshToken을 담아 전송합니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;전달받은 토큰의 유효성 검사가 완료되면,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;AccessToken&lt;/b&gt;을 통해 유저 정보(member PK)를 얻고,&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;DB에서 유저 정보에 해당하는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Refresh Token&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;을 조회해옵니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이때&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;'&lt;/b&gt;&lt;b&gt;DB에서 조회해온&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Refresh Token&lt;b&gt;'&lt;/b&gt;&lt;/b&gt;과 클라이언트에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;'&lt;/b&gt;&lt;b&gt;요청에 담아 보낸 Refresh Token'&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;이 동일하다면 토큰을 재발급합니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;새로 발급된 Access Token, RefreshToken은 클라이언트 측에 전송되며, 새로운 RefreshToken은 DB에 갱신됩니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이렇게 관리하는 경우&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;u&gt;Refresh Token을&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;1회용&lt;/b&gt;으로 사용&lt;/u&gt;할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;그렇다면 어떻게 RTR 방식으로 어떻게 탈취로 인한 피해를 방지할 수 있는걸까요?&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;탈취 플로우를 통해 살펴보겠습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;1. 로그인 시에 유저는 Access Token과 Refresh Token을 응답받습니다.&lt;br /&gt;(이때 Redis에 Refresh Token이&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;MemberId(Key):RefreshToken(Value)&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;형태로 저장됩니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;2. 이때 유저에게 AccessToken, Refresh Token을 전달하는 과정에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;해커가 토큰을 탈취&lt;/b&gt;합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #ffffff; color: #353638; text-align: center;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;3. 해커는 탈취한 AccessToken을 통해 유저의 권한을 사용하던 중 토큰이 만료되었습니다.&lt;br /&gt;(이때 당연히 같은 토큰을 사용하는 유저도 만료가 됩니다.)&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;이때&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;유저가 먼저 토큰을 갱신하는 경우(4-1)&lt;/b&gt;와&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;해커가&lt;/b&gt;&amp;nbsp;&lt;b&gt;먼저 토큰을 갱신하는 경우(4-2)&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;로 두 가지 상황이 있을 수 있습니다.&lt;/blockquote&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;4-1. 이때 유저가 먼저 토큰을 갱신하는 경우, DB의 RefreshToken도 갱신되며&lt;br /&gt;이후에 해커가 &amp;nbsp;토큰 갱신을 요청했을 때 DB에 저장된 RefreshToken과 해커가 갱신을 위해 요청에 담아보낸 RefreshToken이 달라서 토큰 갱신에 실패합니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;4-2. 해커가 먼저 토큰을 갱신하는 경우, DB의 RefreshToken도 갱신되며&lt;br /&gt;이후에 유저가 토큰 갱신을 요청했을 때 DB에 저장된 RefreshToken과 유저가 갱신을 위해 요청에 담아보낸 RefreshToken이 달라서 토큰 갱신에 실패합니다.&lt;br /&gt;그럼 유저는 다시 로그인해야 하며, 재로그인 시에 DB의 RefreshToken도 갱신되어 공격자가 가진 RefreshToken과 DB의 RefreshToken의 상태가 달라집니다.&lt;br /&gt;떄문에 해커는 다음 토큰 갱신에서 실패하게 되며 유저의 권한을 악용할 수 없게됩니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이렇듯 RTR 방식으로 RefreshToken을 관리하게 되면&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;해커가 Refresh Token을 탈취해도, 실제 회원의 Access Token이 만료되어 토큰을 갱신하는 순간 Redis의 Refresh Token이 변경되어 해커가 탈취한 이전의 Refresh Token은 만료되어 해커는 더이상 토큰을 재발급 받을 수 없습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f4f4f6; color: #9999a1; text-align: left;&quot;&gt;물론 실제 회원이 토큰 탈취 이후, 한동안 로그인을 하지 않게 되면 그 동안은 해커가 계속해서 회원의 권한을 악용할 수 있습니다.때문에&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;HTTPS&lt;/b&gt;&lt;span style=&quot;background-color: #f4f4f6; color: #9999a1; text-align: left;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;설정이나&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;httpOnly, Secure&lt;/b&gt;&lt;span style=&quot;background-color: #f4f4f6; color: #9999a1; text-align: left;&quot;&gt;&amp;nbsp;설정을 한 쿠키로 JWT 토큰을 발급하는 등의 방법으로&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #f4f4f6; color: #9999a1; text-align: left;&quot;&gt;통신 과정의 안전성을 고려해야 하며, 토큰 탈취 가능성을 낮추기 위해 노력해야합니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style1&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 id=&quot;5.%20Redis%EB%8F%84%20%40Transactional%EB%A1%9C%20%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98%20%EC%84%A4%EC%A0%95%EC%9D%B4%20%EA%B0%80%EB%8A%A5%ED%95%9C%EA%B0%80%EC%9A%94%3F-1&quot; style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://pgmjun.tistory.com/125#5.%20Redis%EB%8F%84%20%40Transactional%EB%A1%9C%20%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98%20%EC%84%A4%EC%A0%95%EC%9D%B4%20%EA%B0%80%EB%8A%A5%ED%95%9C%EA%B0%80%EC%9A%94%3F-1&quot;&gt;5. Redis도 @Transactional로 트랜잭션 설정이 가능한가요?&lt;/a&gt;&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;Spring Data Redis의&lt;span style=&quot;color: #333333; text-align: start;&quot; data-token-index=&quot;1&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://docs.spring.io/spring-data/redis/reference/redis/transactions.html&quot; data-token-index=&quot;2&quot;&gt;&lt;span&gt;공식 문서&lt;/span&gt;&lt;/a&gt;를 살펴본 결과&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;Redis는 트랜잭션을&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;RedisConfig&lt;/b&gt;의 추가적인 설정을 통해&amp;nbsp;&lt;b&gt;@Transactional&lt;/b&gt;로 JPA와 하나의 트랜잭션으로 묶어 사용이 가능하다는 점을 알게 되었습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1720614572558&quot; class=&quot;arduino&quot; style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;template.opsForValue().set(&quot;thing1&quot;, &quot;thing2&quot;);

template.opsForValue().get(&quot;thing1&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 JPA와 같이 영속성 컨텍스트의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;i&gt;&lt;u&gt;1차 캐시와 같은 기능을 지원하지 않기 때문에&lt;/u&gt;&lt;/i&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;문제가 발생할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 위와 같은 상황에서는 thing1이 set() 메서드로 Redis에 Insert된 후 트랜잭션이&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;EXEC(commit)&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;되지 않았기 때문에&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;template.opsForValue().get(&quot;thing1&quot;);&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;의 결과가 null이 됩니다.&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;JPA는 DB에 flush 되지 않아도 1차 캐시에 정보가 담겨서,&lt;br /&gt;조회 시에 1차 캐시의 정보를 가져올 수 있지만 Redis는 1차 캐시라는 것이 존재하지 않아서&lt;br /&gt;DB에 커밋되기 전에는 하나의 트랜잭션 내에서 발생한 변경을 조회해올 수 없습니다.&lt;/blockquote&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;자세한 내용은 아래 공식문서에서 참고하실 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-data/redis/reference/redis/transactions.html&quot;&gt;https://docs.spring.io/spring-data/redis/reference/redis/transactions.html&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style5&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 id=&quot;%EA%B8%80%EC%9D%84%20%EB%A7%88%EC%B9%98%EB%A9%B0-1&quot; style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://pgmjun.tistory.com/125#%EA%B8%80%EC%9D%84%20%EB%A7%88%EC%B9%98%EB%A9%B0-1&quot;&gt;글을 마치며&lt;/a&gt;&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;개발을 하다보며 느낀 것은 항상 100% 좋은 기술이나 방법은 없다는 점입니다. 제가 말씀드린 부분 이외에도 여러분들이 아쉽다고 느끼신 부분들이 있다면 꼭 추가해서 더욱 개선된 프로그램을 만들어가는 개발자로 성장하실 수 있으면 좋겠습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;긴 글 읽어주셔서 감사합니다!&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;reference&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://puleugo.tistory.com/154&quot;&gt;https://puleugo.tistory.com/154&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://kth990303.tistory.com/431&quot;&gt;https://kth990303.tistory.com/431&lt;/a&gt;&lt;/p&gt;</description>
      <category>redis</category>
      <category>rtr</category>
      <author>최동훈1</author>
      <guid isPermaLink="true">https://ulsanboy.tistory.com/183</guid>
      <comments>https://ulsanboy.tistory.com/183#entry183comment</comments>
      <pubDate>Wed, 10 Jul 2024 21:32:38 +0900</pubDate>
    </item>
    <item>
      <title>☄️쿠석쿠석 서비스를 출시하며, feat. 성능 개선</title>
      <link>https://ulsanboy.tistory.com/180</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt; 쿠석쿠석 서비스란 ?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제가 작년부터 기획하였던 건대의 장소 공유 서비스 입니다 !! 그 전까지 개발자로만 프로젝트를 참여해보았지만, 순수하게 사람들의 문제점을 해결하고자, 아이디어를 기획하고, 팀을 셋업하고, 회의를 주관하며 팀 을 이끌어본 경험은 처음이었기에 저 자신이 자랑스러웠습니다(많이컷다 최동훈..ㅎㅎㅎ) 제가 만들고자 했던 서비스라서 저에게 있어서 자식과 같은 서비스 입니다 !! ㅎㅎ.. 부족한 팀장이였지만, 다들 잘 따라와줘서 세상에 나올수 있었던 거 같습니다 !! ㅎㅎ&amp;nbsp; 약 3개월동안 제가 과거에 프로젝트들의 경험을 발판삼아서, 최대한 &lt;b&gt;&quot;애자일 스러운&quot; 팀 문화&lt;/b&gt;를 만들려고 노력했던거 같습니다.. ㅎㅎㅎ 매주 스크럼를 진행하며, 각 주차별 스프린트 회고를 진행하였고, 각 직군과 디자이너 분의 의사결정을 최대한 존중하며, 개발을 해 나갔던거 같습니다 !! ㅎㅎ 다시한번 끝까지 잘 따라와준 프론트 정현님, 디자이너 희수님, 백엔드 해리님께 감사합니다 ㅎㅎ&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; 베포 도메인 : &lt;a href=&quot;https://www.kusukmap.com/&quot;&gt;쿠석쿠석 (kusukmap.com)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  깃허브 주소 : &lt;a href=&quot;https://github.com/KONKUK-MAP-Service&quot;&gt;KONKUK-MAP-Service (github.com)&lt;/a&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt; 그간의 기록들&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Hsw11/btsGT7w7FAO/0YTaZJ27ngRAB88zF8Q590/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Hsw11/btsGT7w7FAO/0YTaZJ27ngRAB88zF8Q590/img.png&quot; data-origin-width=&quot;598&quot; data-origin-height=&quot;962&quot; data-is-animation=&quot;false&quot; width=&quot;331&quot; height=&quot;532&quot; style=&quot;width: 49.9865%; margin-right: 10px;&quot; data-widthpercent=&quot;50.57&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Hsw11/btsGT7w7FAO/0YTaZJ27ngRAB88zF8Q590/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHsw11%2FbtsGT7w7FAO%2F0YTaZJ27ngRAB88zF8Q590%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;598&quot; height=&quot;962&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bdYZV7/btsGUVDckJ2/7YE6lCGG3skmUDWnVgduK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bdYZV7/btsGUVDckJ2/7YE6lCGG3skmUDWnVgduK1/img.png&quot; data-origin-width=&quot;551&quot; data-origin-height=&quot;907&quot; data-is-animation=&quot;false&quot; width=&quot;369&quot; height=&quot;607&quot; style=&quot;width: 48.8507%;&quot; data-widthpercent=&quot;49.43&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bdYZV7/btsGUVDckJ2/7YE6lCGG3skmUDWnVgduK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbdYZV7%2FbtsGUVDckJ2%2F7YE6lCGG3skmUDWnVgduK1%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;551&quot; height=&quot;907&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt; 쿠석쿠석 카드뉴스 &lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/djZvxt/btsGUUEhh3q/rXL3y8vx1CTScKlp8Olkx0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/djZvxt/btsGUUEhh3q/rXL3y8vx1CTScKlp8Olkx0/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;1080&quot; data-filename=&quot;쿠석쿠석 설명서 1.png&quot; width=&quot;337&quot; style=&quot;width: 32.5581%; margin-right: 10px;&quot; data-widthpercent=&quot;33.33&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/djZvxt/btsGUUEhh3q/rXL3y8vx1CTScKlp8Olkx0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdjZvxt%2FbtsGUUEhh3q%2FrXL3y8vx1CTScKlp8Olkx0%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;1080&quot; height=&quot;1080&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b5addQ/btsGTzOfAjg/Kw1f3udCudjixVgaY2Dca0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b5addQ/btsGTzOfAjg/Kw1f3udCudjixVgaY2Dca0/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;1080&quot; data-filename=&quot;쿠석쿠석 설명서 2.png&quot; width=&quot;287&quot; height=&quot;287&quot; style=&quot;width: 32.5581%; margin-right: 10px;&quot; data-widthpercent=&quot;33.33&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b5addQ/btsGTzOfAjg/Kw1f3udCudjixVgaY2Dca0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb5addQ%2FbtsGTzOfAjg%2FKw1f3udCudjixVgaY2Dca0%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;1080&quot; height=&quot;1080&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bcHtl3/btsGUMfhb4P/AnKOBCJ1PVxpGngjkkC7gK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bcHtl3/btsGUMfhb4P/AnKOBCJ1PVxpGngjkkC7gK/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;1080&quot; data-filename=&quot;쿠석쿠석 설명서 3.png&quot; width=&quot;372&quot; height=&quot;372&quot; style=&quot;width: 32.5581%;&quot; data-widthpercent=&quot;33.34&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bcHtl3/btsGUMfhb4P/AnKOBCJ1PVxpGngjkkC7gK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbcHtl3%2FbtsGUMfhb4P%2FAnKOBCJ1PVxpGngjkkC7gK%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;1080&quot; height=&quot;1080&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt; 출시후 사용자들의 반응&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;우선&amp;nbsp;지난주&amp;nbsp;학내&amp;nbsp;커뮤니티&amp;nbsp;에브리타임에&amp;nbsp;첫&amp;nbsp;출시후&amp;nbsp;반응이&amp;nbsp;좋았습니다..&amp;nbsp;ㅎㅎㅎ&amp;nbsp;그런데&amp;nbsp;사용자분들이&amp;nbsp;우리의&amp;nbsp;핵심&amp;nbsp;기능인&amp;nbsp;&quot;사진&quot;&amp;nbsp;과&amp;nbsp;관련되어서&amp;nbsp;속도가&amp;nbsp;느리다&amp;nbsp;라는&amp;nbsp;피드백이&amp;nbsp;있어서&amp;nbsp;해당&amp;nbsp;응답속도를&amp;nbsp;개선해&amp;nbsp;보았습니다. &lt;br /&gt;&lt;a href=&quot;https://everytime.kr/426789/v/339376642&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://everytime.kr/426789/v/339376642&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;995&quot; data-origin-height=&quot;540&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b450z9/btsGSNsSqlc/QOHyoWddQrpsKSAs9epjH0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b450z9/btsGSNsSqlc/QOHyoWddQrpsKSAs9epjH0/img.png&quot; data-alt=&quot;쿠석쿠석 출시후 건대 학우들의 반응 !! 너무 감사합니다 ㅠㅠ&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b450z9/btsGSNsSqlc/QOHyoWddQrpsKSAs9epjH0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb450z9%2FbtsGSNsSqlc%2FQOHyoWddQrpsKSAs9epjH0%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;995&quot; height=&quot;540&quot; data-origin-width=&quot;995&quot; data-origin-height=&quot;540&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;쿠석쿠석 출시후 건대 학우들의 반응 !! 너무 감사합니다 ㅠㅠ&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt; 속도개선까지의 아이디어&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;저의&amp;nbsp;가장&amp;nbsp;큰&amp;nbsp;주안점은&amp;nbsp;분산된&amp;nbsp;서버&amp;nbsp;환경에서의&amp;nbsp;이미지&amp;nbsp;파일&amp;nbsp;제공&amp;nbsp;이였습니다.&amp;nbsp;!&amp;nbsp;최근&amp;nbsp;정보처리기사&amp;nbsp;실기&amp;nbsp;공부를하고&amp;nbsp;있었는데,&amp;nbsp;전세계적으로&amp;nbsp;분산되어서&amp;nbsp;파일을&amp;nbsp;저장하고,&amp;nbsp;해당&amp;nbsp;지역별로&amp;nbsp;가장&amp;nbsp;가까운&amp;nbsp;서버에서&amp;nbsp;파일을&amp;nbsp;제공&amp;nbsp;받게&amp;nbsp;한다면,&amp;nbsp;해당&amp;nbsp;이미지의&amp;nbsp;응답속도가&amp;nbsp;개선되지&amp;nbsp;않을까&amp;nbsp;?&amp;nbsp;라는&amp;nbsp;너무&amp;nbsp;막연한&amp;nbsp;착안점을&amp;nbsp;얻었습니다. &lt;br /&gt;그다음&amp;nbsp;구글링과&amp;nbsp;스택&amp;nbsp;오버플로우&amp;nbsp;검색을&amp;nbsp;해&amp;nbsp;보았습니다. &lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt; CDN&amp;nbsp;을&amp;nbsp;이용한&amp;nbsp;성능개선&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;다행히도, CDN 이라는 키워드를 찾을수 있었고, CDN 을 이용한 캐싱을 한다면 응답속도를 조금 더 최적화 할수 있지 않을까라는 가설을 세웠습니다. &lt;br /&gt;CDN 에 대해 간략해 설명하자면, &lt;br /&gt;&lt;br /&gt;CDN (콘텐츠 전송 네트워크, content delivery network) 간단히 말하면 지리적으로 분산된 여러 개의 서버로 사용자에게 웹 콘텐츠를 효율적으로 제공할 수 있는 서버의 분산 네트워크입니다. &lt;br /&gt;&lt;br /&gt;좀&amp;nbsp;더&amp;nbsp;자세히&amp;nbsp;설명하자면&amp;nbsp;사용자의&amp;nbsp;물리적&amp;nbsp;위치와&amp;nbsp;가까운&amp;nbsp;엣지(Edge)&amp;nbsp;서버가&amp;nbsp;있고,&amp;nbsp;이는&amp;nbsp;파일&amp;nbsp;복사본을&amp;nbsp;임시로&amp;nbsp;저장하는&amp;nbsp;프로세스인&amp;nbsp;캐싱을&amp;nbsp;사용합니다.&amp;nbsp;그렇게&amp;nbsp;캐시된&amp;nbsp;웹&amp;nbsp;콘텐츠를&amp;nbsp;전송함으로써&amp;nbsp;전송&amp;nbsp;속도를&amp;nbsp;높이게&amp;nbsp;되고,&amp;nbsp;사용자는&amp;nbsp;콘텐츠가&amp;nbsp;로딩될&amp;nbsp;때까지&amp;nbsp;기다릴&amp;nbsp;필요가&amp;nbsp;없게&amp;nbsp;되는&amp;nbsp;것입니다. &lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1117&quot; data-origin-height=&quot;337&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/4XT3X/btsGUPprasa/Ddf5yJbD6XWuPYi3Eny9A1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/4XT3X/btsGUPprasa/Ddf5yJbD6XWuPYi3Eny9A1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/4XT3X/btsGUPprasa/Ddf5yJbD6XWuPYi3Eny9A1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F4XT3X%2FbtsGUPprasa%2FDdf5yJbD6XWuPYi3Eny9A1%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;1117&quot; height=&quot;337&quot; data-origin-width=&quot;1117&quot; data-origin-height=&quot;337&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt; Cloud&amp;nbsp;Front&amp;nbsp;를&amp;nbsp;이용한&amp;nbsp;CDN&amp;nbsp;서버&amp;nbsp;생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;우선&amp;nbsp;AWS&amp;nbsp;CloudFront&amp;nbsp;를&amp;nbsp;생성하고,&amp;nbsp;현재&amp;nbsp;쿠석쿠석에서&amp;nbsp;보유하고&amp;nbsp;있는&amp;nbsp;이미지&amp;nbsp;파일&amp;nbsp;버컷을&amp;nbsp;등록해&amp;nbsp;주었습니다.&amp;nbsp; &lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;285&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RGveV/btsGUQBVqIr/qdyk2PYoR5giYjuljjcJ60/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RGveV/btsGUQBVqIr/qdyk2PYoR5giYjuljjcJ60/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RGveV/btsGUQBVqIr/qdyk2PYoR5giYjuljjcJ60/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRGveV%2FbtsGUQBVqIr%2Fqdyk2PYoR5giYjuljjcJ60%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;285&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;285&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;또한&amp;nbsp;Dto에서&amp;nbsp;프론트로&amp;nbsp;응답해주는&amp;nbsp;코드에&amp;nbsp;기존&amp;nbsp;S3의&amp;nbsp;url이&amp;nbsp;아니라&amp;nbsp;&lt;b&gt;CloudFront&lt;/b&gt;의&amp;nbsp;Url이&amp;nbsp;반환되도록&amp;nbsp;문자열&amp;nbsp;처리를&amp;nbsp;해주었습니다. &lt;br /&gt;&lt;br /&gt;기존 반환 url : &lt;a href=&quot;https://kusuk-image-bucket.s3.ap-northeast-2.amazonaws.com/konkuk/5b241b6f-8a31-45e9-9453-4fade5107e52.jpg&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://kusuk-image-bucket.s3.ap-northeast-2.amazonaws.com/konkuk/5b241b6f-8a31-45e9-9453-4fade5107e52.jpg&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;변경 후url&amp;nbsp; : &lt;a href=&quot;https://drooj3d6yc2tf.cloudfront.net/konkuk/5b241b6f-8a31-45e9-9453-4fade5107e52.jpg&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://drooj3d6yc2tf.cloudfront.net/konkuk/5b241b6f-8a31-45e9-9453-4fade5107e52.jpg&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt; CDN&amp;nbsp;적용전&amp;nbsp;반응속도&amp;nbsp;비교&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;현재&amp;nbsp;반응속도&amp;nbsp;(&amp;nbsp;62ms&amp;nbsp;)&amp;nbsp;(고화질&amp;nbsp;사진&amp;nbsp;4.2MB&amp;nbsp;기준) &lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1023&quot; data-origin-height=&quot;85&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cBtDLn/btsGVcrcYLT/P4sxWKbZGKBG08xm64UNak/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cBtDLn/btsGVcrcYLT/P4sxWKbZGKBG08xm64UNak/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cBtDLn/btsGVcrcYLT/P4sxWKbZGKBG08xm64UNak/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcBtDLn%2FbtsGVcrcYLT%2FP4sxWKbZGKBG08xm64UNak%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;1023&quot; height=&quot;85&quot; data-origin-width=&quot;1023&quot; data-origin-height=&quot;85&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt; 성능&amp;nbsp;개선후&amp;nbsp;반응속도&amp;nbsp;(&lt;b&gt;46.77%&amp;nbsp;&lt;/b&gt;개선)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;그런데 CDN 을 CLOUDFLONT를 이용하여 S3 버킷을 프록시 서버로 만든뒤에, 실험을 해보니 실제로도 같은사진( 고화질 4.2MB ) 기준 &lt;b&gt;46.77 %&lt;/b&gt; 정도 의성능 개선이 이루어 졌습니다 !! &lt;b&gt;62ms -&amp;gt; 33ms&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;991&quot; data-origin-height=&quot;92&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OYkTW/btsGTecwHUM/9Hd0vjYP8C3hVDcbIbA55k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OYkTW/btsGTecwHUM/9Hd0vjYP8C3hVDcbIbA55k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OYkTW/btsGTecwHUM/9Hd0vjYP8C3hVDcbIbA55k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOYkTW%2FbtsGTecwHUM%2F9Hd0vjYP8C3hVDcbIbA55k%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;991&quot; height=&quot;92&quot; data-origin-width=&quot;991&quot; data-origin-height=&quot;92&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt; 감격스러운 &lt;b&gt;Hit from cloudfron&lt;/b&gt;t &lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;실제로&amp;nbsp;해당&amp;nbsp;속도&amp;nbsp;개선이&amp;nbsp;네트워크&amp;nbsp;상황에&amp;nbsp;따른&amp;nbsp;우연인지&amp;nbsp;확인하기&amp;nbsp;위해&amp;nbsp;&lt;b&gt;HTTP&amp;nbsp;응답&amp;nbsp;헤더&amp;nbsp;값&lt;/b&gt;을&amp;nbsp;확인해&amp;nbsp;보니&amp;nbsp;해당&amp;nbsp;S3에&amp;nbsp;저장된&amp;nbsp;이미지&amp;nbsp;파일이&amp;nbsp;&lt;b&gt;CDN&lt;/b&gt; 서버를 통해 캐싱되어서&amp;nbsp;&lt;b&gt;Cache HIT&lt;/b&gt;&amp;nbsp;라는 단어를 발견할 수 있었습니다 !!!! &lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;998&quot; data-origin-height=&quot;461&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjQtDu/btsGVdKsuzI/lKWitGD7ARIczcWseAUuOK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjQtDu/btsGVdKsuzI/lKWitGD7ARIczcWseAUuOK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjQtDu/btsGVdKsuzI/lKWitGD7ARIczcWseAUuOK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbjQtDu%2FbtsGVdKsuzI%2FlKWitGD7ARIczcWseAUuOK%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;998&quot; height=&quot;461&quot; data-origin-width=&quot;998&quot; data-origin-height=&quot;461&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;아래의&amp;nbsp;응답해더에&amp;nbsp;보면&amp;nbsp;현대&amp;nbsp;대한민국에서&amp;nbsp;가장&amp;nbsp;가까운&amp;nbsp;서버에서&amp;nbsp;캐싱되어있던&amp;nbsp;파일이&amp;nbsp;Hit&amp;nbsp;되어서&amp;nbsp;빠르게&amp;nbsp;전송된&amp;nbsp;것을&amp;nbsp;확인할수&amp;nbsp;있었습니다&amp;nbsp;!!!&amp;nbsp; &lt;br /&gt;&lt;br /&gt;&lt;br /&gt;결국 내 가설이 맞았습니다 ㅠㅠㅠ !! ㅎㅎㅎㅎ 실제로 &lt;b&gt;Cache HIT from cloudfront&lt;/b&gt;라는 단어를 보니 신기하고 뿌듯하군요... &lt;br /&gt;사용자들이&amp;nbsp;준&amp;nbsp;피드백&amp;nbsp;하나하나가&amp;nbsp;너무&amp;nbsp;고마웠고,&amp;nbsp;앞으로도&amp;nbsp;지속적으로&amp;nbsp;성능&amp;nbsp;개선해&amp;nbsp;나가야겠습니다..&amp;nbsp;ㅎㅎㅎㅎ &lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;</description>
      <category>개발 프로젝트</category>
      <category>kusukmap.com</category>
      <category>건대</category>
      <category>쿠석쿠석</category>
      <category>팀장</category>
      <author>최동훈1</author>
      <guid isPermaLink="true">https://ulsanboy.tistory.com/180</guid>
      <comments>https://ulsanboy.tistory.com/180#entry180comment</comments>
      <pubDate>Thu, 25 Apr 2024 01:03:55 +0900</pubDate>
    </item>
    <item>
      <title>플리보따리 서비스를 개발하며 -2024-04-04</title>
      <link>https://ulsanboy.tistory.com/177</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;플리보따리 란&amp;nbsp; ?&amp;nbsp;&lt;/h2&gt;
&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;도메인 : &lt;a href=&quot;http://www.plypockets.com&quot; target=&quot;_self&quot;&gt;&lt;span&gt;www.plypockets.com&lt;/span&gt;&lt;/a&gt;&lt;br /&gt;Git hub : &lt;a href=&quot;https://github.com/Playlist-pack&quot; target=&quot;_self&quot;&gt;&lt;span&gt;플리보따리 (github.com)&lt;/span&gt;&lt;/a&gt;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;안녕하세요 구독자 여러분 !!&lt;br /&gt;제가 역대 한 프로젝트중 가장 애정을 가지고 진행했던 서비스에 대해서 소개해 드리겠습니다 !! ☺️&lt;br /&gt;플리보따리 서비스는 &lt;b&gt;선물과 함께 노래를 전송하는 익명 편지 서비스&lt;/b&gt; 입니다 !  &amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cgJdNg/btsF3mHKdGo/kdUMkr83k8CpdwY4eWKOZK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cgJdNg/btsF3mHKdGo/kdUMkr83k8CpdwY4eWKOZK/img.png&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;2316&quot; style=&quot;width: 32.5581%;&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cgJdNg/btsF3mHKdGo/kdUMkr83k8CpdwY4eWKOZK/img.png&quot; alt=&quot;&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcgJdNg%2FbtsF3mHKdGo%2FkdUMkr83k8CpdwY4eWKOZK%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;1080&quot; height=&quot;2316&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bDsdVk/btsF12cfOMf/hc56hu0lQ4Xvhmh5j2e1i1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bDsdVk/btsF12cfOMf/hc56hu0lQ4Xvhmh5j2e1i1/img.jpg&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;2316&quot; style=&quot;width: 32.5581%;&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bDsdVk/btsF12cfOMf/hc56hu0lQ4Xvhmh5j2e1i1/img.jpg&quot; alt=&quot;&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbDsdVk%2FbtsF12cfOMf%2Fhc56hu0lQ4Xvhmh5j2e1i1%2Fimg.jpg&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; loading=&quot;lazy&quot; width=&quot;1080&quot; height=&quot;2316&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dGtB0S/btsF3eiH62R/kGadyeu53FHF6KL0HPKju0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dGtB0S/btsF3eiH62R/kGadyeu53FHF6KL0HPKju0/img.png&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;2316&quot; style=&quot;width: 32.5581%;&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dGtB0S/btsF3eiH62R/kGadyeu53FHF6KL0HPKju0/img.png&quot; alt=&quot;&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdGtB0S%2FbtsF3eiH62R%2FkGadyeu53FHF6KL0HPKju0%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;1080&quot; height=&quot;2316&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;플리보따리 모바일 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;이화여대 디자인 동아리 &amp;ldquo;UXO&amp;rdquo; 와 협업하여 만들었어요 !!&lt;br /&gt;&amp;nbsp;&lt;br /&gt;저도 전문 디자이너분들과 첫 협업 경험이어서 정말 많이 배웠고, 저랑 전혀 상관없는 직군들과 소통하는 법을 배운거 같았습니다!! &lt;br /&gt;제가 이 프로젝트를 진행하며 많이 배웠고 신경쓰고자 했던 부분은 바로&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트코드 와 클린코드&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;였어요 !!  &lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;이번 프로젝트에서 반복문 대신 람다와 스트림을 활용한 자바8 부터 추가된 함수형 언어의 특징들을 적극적으로 이용해서 가독성 을 높이고 서로의 코드를 알기 쉽게 만든거 같습니당 !!  &lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;또 이번 프로젝트에서 예외처리의 중요성을 다시금 느끼며, 커스텀 예외처리와 handler 기능을 각 Api 마다 모두 적용해보며, 예외 상황에 맞는 status code 반환으로 프론트엔드 분들과 원할하게 협업을 이루어지게 하였습니당 ..!  &lt;br /&gt;&amp;nbsp;&lt;br /&gt;또 Enum 타입으로 관리해서 실 사용시 전혀 무결성을 최대한 지키도록 구현 하였습니다 !! ㅎㅎ&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Getter
@AllArgsConstructor
public enum HttpExceptionCode {
    UNEXPECTED_EXCEPTION(HttpStatus.INTERNAL_SERVER_ERROR,  &quot;예상치 못한 오류가 발생했습니다.&quot;),
    INVALID_ARGUMENT(HttpStatus.BAD_REQUEST,  &quot;올바르지 않은 값이 전달되었습니다.&quot;),
    REQUEST_NOT_FOUND(HttpStatus.NOT_FOUND, &quot;요청을 찾을 수 없습니다.&quot;),
    INTERNAL_SERVER_EXCEPTION(
            HttpStatus.INTERNAL_SERVER_ERROR, &quot;서버 내부에서 오류가 발생했습니다.&quot;),
    PLAYLISTPACK_NOT_FOUND(HttpStatus.NOT_FOUND, &quot;유저의 플리보따리를 찾을 수 없습니다.&quot;),
    CONTENT_NOT_FOUND(HttpStatus.NOT_FOUND, &quot;요청한 컨텐츠를 찾을 수 없습니다.&quot;),
    USER_NOT_FOUND(HttpStatus.NOT_FOUND, &quot;요청한 사용자를 찾을 수 없습니다.&quot;),
    USER_ALREADY_VOTED(HttpStatus.BAD_REQUEST, &quot;요청한 사용자는 이미 투표에 참여하였습니다.&quot;),

    SPOTIFYSONG_NOT_FOUND(HttpStatus.NOT_FOUND, &quot;해당 ID의 노래가 존재하지 않습니다.&quot;);

    private final HttpStatus httpStatus;
    private final String message;

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;현재도 운영중이라 실제 유저가 있으니, 고객들이 주는 피드백을 바탕으로 성능개선 경험도 쌓게 되었어요 !! ㅎㅎ&lt;br /&gt;&amp;nbsp;&lt;br /&gt;우선 제가 맡은 부분을 소개해 드리자면 ,&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;맡은 부분&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;회원 도메인&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;회원가입과 로그인&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;음악 도메인&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;노래검색, 노래 미리듣기, 노래 실시간 차트&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;인프라&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;GithubAction, AWS CodeDeploy, AWS S3 를 이용한 CI/CD&lt;/li&gt;
&lt;li&gt;배포관련 이슈&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;였어요 !! ㅎㅎ&lt;br /&gt;&amp;nbsp;&lt;br /&gt;특히 제가 프로젝트를 진행할 당시 처음으로 CI/CD 를 구축해 보았어요 !! ️&lt;br /&gt;&amp;nbsp;&lt;br /&gt;그런데 원래는 ㅠ&lt;br /&gt;젠킨스와 도커 조합으로 하고 있었지만, AWS 프리티어 에서 젠킨스가 너무 느리게 작동해서 도저히 쓸수 없는 지경이 되더라구요 ㅠㅠㅠ  &lt;br /&gt;그래서 결국 깃허브 액션을 이용했습니다 ㅠㅠㅠ&lt;br /&gt;&amp;nbsp;&lt;br /&gt;도커와 도커컴포즈 쿠버네티스는 이후 진행한 직짱건강 프로젝트에서 구축 해 보았어요 !! ㅎㅎㅎ 다음의 Episode 를 기대해 주세요 !! ☺️&lt;br /&gt;&amp;nbsp;&lt;br /&gt;또한 유저 데이터를 보고 음악 검색 페이지 에서 가장 많은 시간을 소비한다는 데이터를 보고&lt;br /&gt;음악 검색 api 속도를 &lt;b&gt;1.1초에서 0.4초로 70.44% 개선&lt;/b&gt;하였어요 !!  &lt;br /&gt;제 코드로 인해 여러 사용자들의 시간이 단축되었다는 사실이 너무 뿌듯했답니다   !!&lt;br /&gt;&amp;nbsp;&lt;br /&gt;과정 : &lt;a href=&quot;https://github.com/Playlist-pack/Server/pull/66&quot; target=&quot;_self&quot;&gt;&lt;span&gt;♻️ [refactor] : spotify API 성능 개선&amp;nbsp;&lt;/span&gt;&lt;/a&gt;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;특히 중간에 크게 기획의도를 변경해야 할 일이 생겼는데, 저번 2023 인프콘에서 들었던, &amp;ldquo;계획에 대응하기 보단 변화에 대응하길&amp;rdquo; 이라는 &lt;b&gt;애자일방법론이&lt;/b&gt; 떠올랐던 순간이였습니다 ㅎㅎ&lt;/p&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;성과&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;무려 월간활성유저수&lt;b&gt; MAU 764&lt;/b&gt; 규모의 서비스로 성장하였어요!!  &lt;br /&gt;제가 만든 코드가 수백명이 사용하다니.. 진짜 테스트코드와 QA의 중요성을 느꼈습니다 ㅠㅠ&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;성과 &lt;b&gt;MAU764&lt;/b&gt;명 &lt;b&gt;MPV 42000&lt;/b&gt;달성&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;GoogleAnalatics4 MAU&lt;/b&gt; 분석&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bJZx5O/btsF025lJ30/HOYs7QeSK3zwp452wQaAT1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bJZx5O/btsF025lJ30/HOYs7QeSK3zwp452wQaAT1/img.png&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;2316&quot; style=&quot;width: 32.5581%;&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bJZx5O/btsF025lJ30/HOYs7QeSK3zwp452wQaAT1/img.png&quot; alt=&quot;&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbJZx5O%2FbtsF025lJ30%2FHOYs7QeSK3zwp452wQaAT1%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;1080&quot; height=&quot;2316&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bes3u5/btsF1IkLjSn/NaiT839hceJoSRRdavS1L0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bes3u5/btsF1IkLjSn/NaiT839hceJoSRRdavS1L0/img.png&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;2316&quot; style=&quot;width: 32.5581%;&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bes3u5/btsF1IkLjSn/NaiT839hceJoSRRdavS1L0/img.png&quot; alt=&quot;&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbes3u5%2FbtsF1IkLjSn%2FNaiT839hceJoSRRdavS1L0%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;1080&quot; height=&quot;2316&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cx1ZXQ/btsF0lD8IDp/ZQ6PTjKRIq6vF6rYJUqEw0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cx1ZXQ/btsF0lD8IDp/ZQ6PTjKRIq6vF6rYJUqEw0/img.png&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;2316&quot; style=&quot;width: 32.5581%;&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cx1ZXQ/btsF0lD8IDp/ZQ6PTjKRIq6vF6rYJUqEw0/img.png&quot; alt=&quot;&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcx1ZXQ%2FbtsF0lD8IDp%2FZQ6PTjKRIq6vF6rYJUqEw0%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;1080&quot; height=&quot;2316&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;구글 애널리틱스를 이용한 유저 데이터 분석&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;DAU 분석&lt;/h3&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;특히 &lt;b&gt;뷰저블&lt;/b&gt; 이라는 유저 데이터 분석 툴을 처음 활용해 보았는데, 뭔가 GA4보다도 사용자 친화적 ? 으로 만들어진거 같아서 사용자의 데이터를 수집하기 좋았어요 ㅎㅎ&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2400&quot; data-origin-height=&quot;1102&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bBqTRQ/btsF3epsVRJ/kBqPMc14p6ArOxUZVcaJE1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bBqTRQ/btsF3epsVRJ/kBqPMc14p6ArOxUZVcaJE1/img.png&quot; data-alt=&quot;뷰저블 사용자 데이터&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bBqTRQ/btsF3epsVRJ/kBqPMc14p6ArOxUZVcaJE1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbBqTRQ%2FbtsF3epsVRJ%2FkBqPMc14p6ArOxUZVcaJE1%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;2400&quot; height=&quot;1102&quot; data-origin-width=&quot;2400&quot; data-origin-height=&quot;1102&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;뷰저블 사용자 데이터&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;또 실제 유저가 있으니, 고객들이 주는 피드백을 바탕으로 성능개선 경험도 쌓게 되었어요 !! ㅎㅎ&lt;/p&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;회고&lt;/h3&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;개발이 모두 완료되고, 함께 개발하였던 백엔드 개발자 &amp;lsquo;안나&amp;rsquo; 님과 주 2회씩 계속 저희 코드에 대한 코드 리뷰를 하는 시간을 가졌어요. 정말 뜻깊었던 순간이였고, 서로 &amp;ldquo;왜? &amp;ldquo; 를 질문하며 코드를 외울 정도로 서로의 코드에 대한 질문을 하고 답을 구하는 과정을 거쳤던거 같아요 !! ㅎㅎ&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;472&quot; data-origin-height=&quot;216&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b26VaT/btsF2L8UjPO/ksPQhVsZC6zb4lsNTbgxv0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b26VaT/btsF2L8UjPO/ksPQhVsZC6zb4lsNTbgxv0/img.png&quot; data-alt=&quot;겸손하신 안나님 !&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b26VaT/btsF2L8UjPO/ksPQhVsZC6zb4lsNTbgxv0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb26VaT%2FbtsF2L8UjPO%2FksPQhVsZC6zb4lsNTbgxv0%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;472&quot; height=&quot;216&quot; data-origin-width=&quot;472&quot; data-origin-height=&quot;216&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;겸손하신 안나님 !&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;이때 경험이 서로 개발자로 성장하는데 큰 성장을 할수 있었던거 같아서 안나님께 커피를 선물해 드렸답니다!!  &lt;br /&gt;&amp;nbsp;&lt;br /&gt;&lt;b&gt; 마무리&lt;/b&gt;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;이번 프로젝트로 쌓은 실력이 제가 혼자 2년간 스프링을 공부하며 쌓았던 실력보다 훨씬 더 성장한거 같아요..! 이번 프로젝트를 계기로, 타 프로젝트에서 테크리드도 해보았어요!! 이건 추후에 다시 포스팅 할게요 ㅎㅎㅎ&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;2316&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cXU2U4/btsF2PwHsA2/6yKCcL9KkQmOJTFsQKobZ1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cXU2U4/btsF2PwHsA2/6yKCcL9KkQmOJTFsQKobZ1/img.jpg&quot; data-alt=&quot;제플리보따리에들어있는선물&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cXU2U4/btsF2PwHsA2/6yKCcL9KkQmOJTFsQKobZ1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcXU2U4%2FbtsF2PwHsA2%2F6yKCcL9KkQmOJTFsQKobZ1%2Fimg.jpg&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; loading=&quot;lazy&quot; width=&quot;249&quot; height=&quot;534&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;2316&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;제플리보따리에들어있는선물&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;저의 플리보따리 랍니다 !! ㅎㅎ&lt;br /&gt;&amp;nbsp;&lt;br /&gt;또 저에게 노래와 편지를 보내고 싶다면, &lt;a href=&quot;http://www.plypockets.com/%EC%B5%9C%EB%8F%99%ED%9B%88&quot; target=&quot;_self&quot;&gt;&lt;span&gt;www.plypockets.com/최동훈&lt;/span&gt;&lt;/a&gt; 으로 선물을 보내주세요 ! ㅎㅎ 익명(닉네임 설정이 가능하지만) 이라서 평소에 못했던 말들도 해주셔도 됩니당 ㅎㅎ&lt;br /&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>개발 프로젝트</category>
      <category>github</category>
      <category>instagram</category>
      <category>plypockets</category>
      <category>Spring</category>
      <category>ulsandongun</category>
      <category>www.plypockets.com</category>
      <category>개발자</category>
      <category>최동훈</category>
      <category>플리보따리</category>
      <author>최동훈1</author>
      <guid isPermaLink="true">https://ulsanboy.tistory.com/177</guid>
      <comments>https://ulsanboy.tistory.com/177#entry177comment</comments>
      <pubDate>Sat, 23 Mar 2024 20:03:25 +0900</pubDate>
    </item>
    <item>
      <title>[mysql] : java.sql.sqlexception: access denied for user 'ubuntu'@'172.31.35.55' (using password: yes) 오류 해결기</title>
      <link>https://ulsanboy.tistory.com/176</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;너무 힘들었다. 도커로 cicd 구축할떄부터 마주했던 애러였다. 도커는결국 RDS로 못쓰고,&amp;nbsp; EC2 내에 mysql을 설치해서 EC2내부 mysql 주소 와 비밀번호, user: root 로 접속하니 잘 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 나는 RDS를 사용하고 싶었다. 플리보따리 에서 cicd 구축할때는 해당 오류가 나오지 않았는데 왜이런지 모르겠다.&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;해당 오류를 해결한다고 장장 20시간이 넘게 걸린거 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;너무 힘들었지만 끝까지 포기하지않고 온갖 자료,블로그를 100개는 본거 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;드디어 해냈다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;828&quot; data-origin-height=&quot;538&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yjRv9/btsE9DyjEmD/serHYM7ewzQxlkzPr4hAu0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yjRv9/btsE9DyjEmD/serHYM7ewzQxlkzPr4hAu0/img.png&quot; data-alt=&quot;애러코드&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yjRv9/btsE9DyjEmD/serHYM7ewzQxlkzPr4hAu0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyjRv9%2FbtsE9DyjEmD%2FserHYM7ewzQxlkzPr4hAu0%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;828&quot; height=&quot;538&quot; data-origin-width=&quot;828&quot; data-origin-height=&quot;538&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;애러코드&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;해당 오류는 EC2에 CodeDeoploy로 인해서 jar 파일이 실행된 뒤, 스프링 실행로그를 확인해서 나왔다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;애러의 실마리 : 설마하는 마음에 DB의 User를 새로 생성하니 애러 코드가 조금 바뀐걸 알수 있었다.&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-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;831&quot; data-origin-height=&quot;542&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bTE0Lu/btsFbgCmlRG/ogDj04OKkloKdQgPR3q6WK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bTE0Lu/btsFbgCmlRG/ogDj04OKkloKdQgPR3q6WK/img.png&quot; data-alt=&quot;to data base 'kusuk'으로 바뀐 모습&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bTE0Lu/btsFbgCmlRG/ogDj04OKkloKdQgPR3q6WK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbTE0Lu%2FbtsFbgCmlRG%2FogDj04OKkloKdQgPR3q6WK%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;831&quot; height=&quot;542&quot; data-origin-width=&quot;831&quot; data-origin-height=&quot;542&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;to data base 'kusuk'으로 바뀐 모습&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그로 인해서 생성한 USer에 DB 스키마에 대한 권한을 부여하면 되겠다는 생각이 들었다. 결국 해당 방법으로 성공했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론은 Mysql의 사용자(User)를 추가해주고, 해당 사용자에게 권한을 부여하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1020&quot; data-origin-height=&quot;1145&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RJLrY/btsE95gWiv6/zSGwDRlvfqJl5sMjjdKeJ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RJLrY/btsE95gWiv6/zSGwDRlvfqJl5sMjjdKeJ1/img.png&quot; data-alt=&quot;Datagrip에서 접속할려는 DB에 사용자를 추가하는 모습&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RJLrY/btsE95gWiv6/zSGwDRlvfqJl5sMjjdKeJ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRJLrY%2FbtsE95gWiv6%2FzSGwDRlvfqJl5sMjjdKeJ1%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;1020&quot; height=&quot;1145&quot; data-origin-width=&quot;1020&quot; data-origin-height=&quot;1145&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Datagrip에서 접속할려는 DB에 사용자를 추가하는 모습&lt;/figcaption&gt;
&lt;/figure&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;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;&lt;span style=&quot;color: #cc7832;&quot;&gt;GRANT ALL PRIVILEGES ON &lt;/span&gt;&lt;span style=&quot;color: #ffc66d;&quot;&gt;*&lt;/span&gt;.* &lt;span style=&quot;color: #cc7832;&quot;&gt;TO &lt;/span&gt;&lt;span style=&quot;color: #6a8759;&quot;&gt;'ubuntu'&lt;/span&gt;@&lt;span style=&quot;color: #6a8759;&quot;&gt;'172.31.35.55' &lt;/span&gt;&lt;span style=&quot;color: #cc7832;&quot;&gt;IDENTIFIED BY &lt;/span&gt;&lt;span style=&quot;color: #6a8759;&quot;&gt;'vdongv1620' &lt;/span&gt;&lt;span style=&quot;color: #cc7832;&quot;&gt;WITH GRANT OPTION;&lt;br /&gt;&lt;/span&gt;&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;을 사용하니 또 AccessDenied 가 떴다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 일단 User를 생성한뒤에, 권한을 수정하는 방식으로 해야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;806&quot; data-origin-height=&quot;878&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cgUBlX/btsFb5UDRK0/DXcfBdOa7eu8viKkIKA0m1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cgUBlX/btsFb5UDRK0/DXcfBdOa7eu8viKkIKA0m1/img.png&quot; data-alt=&quot;여기 클릭한다음에 권한설정&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cgUBlX/btsFb5UDRK0/DXcfBdOa7eu8viKkIKA0m1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcgUBlX%2FbtsFb5UDRK0%2FDXcfBdOa7eu8viKkIKA0m1%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;806&quot; height=&quot;878&quot; data-origin-width=&quot;806&quot; data-origin-height=&quot;878&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;여기 클릭한다음에 권한설정&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기를 클릭한 다음에 권한을 +버튼을 눌러서 추가한 다음에, Create,Delete,Insert,Select ,Alter 권한을 추가해주면 된다.&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-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;750&quot; data-origin-height=&quot;898&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/buVQCO/btsE8PMtVYi/pdPy4CZJJJZBim26E3rku0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/buVQCO/btsE8PMtVYi/pdPy4CZJJJZBim26E3rku0/img.png&quot; data-alt=&quot;권한추가&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/buVQCO/btsE8PMtVYi/pdPy4CZJJJZBim26E3rku0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbuVQCO%2FbtsE8PMtVYi%2FpdPy4CZJJJZBim26E3rku0%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;750&quot; height=&quot;898&quot; data-origin-width=&quot;750&quot; data-origin-height=&quot;898&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;권한추가&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 권한을 추가하면 EC2 내에서 RDS 가 연결되어있는 정보가 application.yml에 존재 하더라도 AccessDenied 오류가 나지 않고, 해당 정보로 그대로 RDS에 접속하여 스프링이 실행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;</description>
      <category>끝까지</category>
      <category>해냈다!</category>
      <author>최동훈1</author>
      <guid isPermaLink="true">https://ulsanboy.tistory.com/176</guid>
      <comments>https://ulsanboy.tistory.com/176#entry176comment</comments>
      <pubDate>Thu, 22 Feb 2024 03:51:10 +0900</pubDate>
    </item>
    <item>
      <title>static 필드에 @Autowired(의존성 주입) 사용하기 (2024-02-09)</title>
      <link>https://ulsanboy.tistory.com/175</link>
      <description>&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;겉으로 보기에는 이상이 없는 코드였는데 (물론 이건 내가 실력이 없고 무지했기에 아무런 이상이 없는 코드처럼 보였던 것이다.) NullPointerException이 뜨며 에러가 뜨는 상황이 발생했다.&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그래서 디버그 모드로 들어가 하나씩 살펴보던 중 이상한 부분을 발견했다.&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;바로 @Autowired를 사용한 service 혹은 dao가 null을 저장하고 있었다.&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;null을 저장하고 있던 필드는 static으로 선언된 정적 필드였는데, 이 상황에서 내가 생각했던 것은 정적 필드는 @Autowired를 사용하지 못한다는 생각이었다.&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;찾아보니 역시나 정적 필드는 @Autowired를 사용할 수 없었고, 이 부분을 모두 바꾸려면 일정을 맞추지 못할 것 같기에 static 필드에 @Autowired를 사용하는 방법을 찾아야 했다.&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;우선 static 필드에 @Autowired를 사용하지 못하는 이유는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;정적 필드가 클래스 로더에 의해 인스턴스화 될 때 아직 Spring Context는 로드되지 않았기 때문에 Spring 프레임워크에 의해 관리되는 @Autowired가 동작하지 않는 것&lt;/b&gt;이었다.&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 방법을 해결하기 위한 방법은 두 가지가 있었다.&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;1.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;@PostConstruct&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;사용하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1707145962851&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Autowired
private FooObject nonStaticObj;

private static FooObject staticObj;

@PostConstruct
private void initStatic() {
  staticObj = this.nonStaticObj;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;위 코드와 같이&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;@PostConsturct 어노테이션&lt;/b&gt;을 사용해주면 된다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;나는 Java와 Spring에 대한 지식이 그렇게 깊지 않기 때문에 @PostConstruct의 기능이 무엇인지 알지 못했었고, 일반적인 생성자와 같은 기능을 하는 줄 알았다.&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 찾아보니 @PostConstruct는 Spring에 의존적이지 않은 Java의 기능으로,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;의존성 주입이 끝난 이후의 작업이 필요할 경우&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;@PostConsturct를 사용해야 한다고 한다.&lt;/p&gt;
&lt;div style=&quot;color: #333333; text-align: center;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;지금까지 그런 상황을 보지 않아 몰랐었는데 생성자가 동작하는 시점에는 의존성 주입이 되지 않은 상태라고 한다.&lt;/p&gt;
&lt;div style=&quot;color: #333333; text-align: center;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;또한,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;@PostConstruct가 적용된 메서드는 Bean LifeCycle에서 한 번만 수행되는 것이 보장&lt;/b&gt;되므로 생성자의 호출이 여러 번 발생하는 것을 원하지 않을 경우 사용하기 유용한 것 같다.&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;다만, 나는 Java 8 환경에서 작업을 했기 때문에 상관이 없었지만 @PostConstruct는 Java 9부터 Deprecated 되었다고 한다.&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Java 9 이상의 환경에서는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;InitializingBean의 afterPropertiesSet()을 구현&lt;/b&gt;하여 사용하면 될 것 같다.&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1707145986415&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class TestObject implements InitializingBean {
  @Override
  public void afterPropertiesSet() {
    ...
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;실행 순서는 @PostConstruct가 우선 실행되고 afterPropertiesSet()이 실행된다고 하니 알아두면 좋을 것 같다.&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;2.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;@Autowired 생성자&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;사용하기&lt;/h2&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;@PostConsturct 말고&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;@Autowired 생성자&lt;/b&gt;를 사용해서도 정적 필드에 의존성을 주입시킬 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1707146059957&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
public class TestObject {
  private static FooObject fooObj;
  
  @Autowired
  private TestObject(FooObject fooObj) {
    this.fooObj = fooObj;
  }
  
  // 혹은 @Autowired setter를 이용할 수도 있다.
  @Autowired
  public void setFooObj(FooObject fooObj) {
    this.fooObj = fooObj;
  } 
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;나는 1번의 방법으로 사용하였지만 위와 같은 코드로 사용하여도 static 필드에 의존성 주입을 사용할 수 있다고 한다.&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;물론 나는 기존 코드를 수정해야 하는 상황이었고, 전체적으로 수정하기에는 일정이 촉박하여 이렇게 static 필드에 의존성 주입을 사용하는 방법을 찾아 해결하였지만, 찾아보니 Spring의 제어를 벗어난 방식이기 때문에 가급적 사용을 자제하는 것이 좋다고 한다.&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;추신;&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;앞의 포스트에도 좋은 의존주입 방식은 생성자주입방식이라고 한다. 특히 @Lombok의 RequiredArgsConstuctor과 연계되서 스프링에서도 추천한다고 한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;생성자 주입 방식으로 불변성을 보장하자&lt;/span&gt;&lt;/blockquote&gt;</description>
      <category>@Autowired</category>
      <author>최동훈1</author>
      <guid isPermaLink="true">https://ulsanboy.tistory.com/175</guid>
      <comments>https://ulsanboy.tistory.com/175#entry175comment</comments>
      <pubDate>Fri, 9 Feb 2024 01:15:58 +0900</pubDate>
    </item>
    <item>
      <title>@Value애너테이션과 스프링 빈 생명주기 오류(2024-02-05)</title>
      <link>https://ulsanboy.tistory.com/174</link>
      <description>&lt;h2 id=&quot;issue&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;Issue&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;플리보따리 서비스에서 SpotifyApi관련 유틸을 만들고있던 과정에 발생한 일이다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;SpotifyConfig 클래스를 리팩토링 하고있었다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;간단히 설명하자면,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SpotifyApi를 생성할려면 개발자 페이지에서 발급받은 클라이언트 키와 비밀번호를 가지고 있어야 한다.&lt;/li&gt;
&lt;li&gt;클래스는 하나의 역할을 가진다는 클린코드 철학으로 나는 SpotifyApi 관련 기능들을 SpotifyConfig 내에 맡겼다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;본론으로 들어가서&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;아래 코드을 보면&lt;/p&gt;
&lt;pre id=&quot;code_1707128072614&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import javax.annotation.PostConstruct;

@Component
@Slf4j
public class SpotifyConfig {

    @Value(&quot;${spotify.registration.client-id}&quot;)
    private String CLIENT_ID;

    @Value(&quot;${spotify.registration.client-secret}&quot;)
    private String CLIENT_SECRET;

    private SpotifyApi spotifyApi;

    private static String accessToken;
    private static Instant accessTokenExpiration;
    
    private SpotifyApi spotifyApi 
    = new SpotifyApi.Builder().setClientId(CLIENT\_ID).setClientSecret(CLIENT\_SECRET).build();
//추가 메서드 생략
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;나는 문제없이 동작할거라 예상했었지만 NPE가 발생하는것이였다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;생각해보니&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;@Value의 필드 주입시점을 잘 모르고 있었다.&lt;br /&gt;@Value 어노테이션이 필드에 붙었다면&lt;br /&gt;의존관계 주입 시점에 동작한다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;추측하건데 @Component 가 붙은 SpotifyConfig 의 생성자 호출 시점은 스프링의 빈이 생성되는 시점일것이다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;참고로 스프링 빈의 생명주기는&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;스프링 컨테이너 생성&lt;/li&gt;
&lt;li&gt;스프링 빈의 생성 -&amp;gt; 스프링의 모든 빈의 필드값 초기화(생성자 이용)&lt;/li&gt;
&lt;li&gt;의존관계 주입-&amp;gt; 이때 @Value 값 주입&lt;/li&gt;
&lt;li&gt;초기화 콜백&lt;/li&gt;
&lt;li&gt;사용&lt;/li&gt;
&lt;li&gt;소멸전 콜백&lt;/li&gt;
&lt;li&gt;스프링 종료&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, 2번의 빈의 생성시점에 SpotifyConfig 생성자가 호출되고 key 값은 null 이며,&lt;br /&gt;2번이 지나간 후에야 key 값에 환경변수가 초기화된다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그러므로 SpotifyConfig(빈객체)의 필드인&lt;/p&gt;
&lt;pre id=&quot;code_1707128391236&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private SpotifyApi spotifyApi 
    = new SpotifyApi.Builder().setClientId(CLIENT\_ID).setClientSecret(CLIENT\_SECRET).build();&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;는 null 이며 NPE 가 발생하는것이다.&lt;/p&gt;
&lt;h2 id=&quot;solution&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;Solution&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;자 빈의 생명주기로 인해 null 이 들어간다는걸 알았다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;해결법은 바로 코드로 알아보자&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;해결방법 1.&lt;/p&gt;
&lt;pre id=&quot;code_1707128780167&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
@Slf4j
public class SpotifyConfig {

    private String CLIENT_ID;

    private String CLIENT_SECRET;

    private SpotifyApi spotifyApi;

    private static String accessToken;
    private static Instant accessTokenExpiration;

    public SpotifyConfig( @Value(&quot;${spotify.registration.client-id}&quot;) String id
    , @Value(&quot;${spotify.registration.client-secret}&quot;) String password)
    
    {
       this.CLIENT_ID=id;
       this.CLIENT_SECRET=password;
       this.spotifyApi 
    = new SpotifyApi.Builder().setClientId(CLIENT\_ID).setClientSecret(CLIENT\_SECRET).build();
    }
    
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;위의 코드로 리팩토링을 하였다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;자 그럼 위의 코드를 보면 생성자 매개변수에 @Value 어노테이션을 넣었다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;결론적으로&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Spring 이 빈 객체를 생성할 때 먼저 객체 생성에 사용할 생성자를 찾는다.&lt;/li&gt;
&lt;li&gt;인수가 필요한 생성자가 발견되면 Spring은 컨테이너에서 해당 인수를 찾아 객체를 생성하기전에 주입한다.&lt;/li&gt;
&lt;li&gt;생성자 매개변수 @Value 어노테이션이 달린 경우 Spring은 객체를 생성하기 전에 속성 파일에 지정된 값을 주입한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;해서 빈생성시에 @Value 어노테이션의 값을 환경변수 값으로 초기화 할수있다.&lt;/p&gt;
&lt;h4 id=&quot;정리하자면&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;정리하자면&lt;/h4&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;@Value 필드 주입의 경우는 스프링 빈이 생성자로 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;빈 생성 후(2.과정)&lt;/b&gt; &lt;b&gt;의존관계 주입시(3.과정)에&lt;/b&gt;&lt;/span&gt; 필드값이 초기화가 되므로 스프링의 빈 생명주기로 인하여 null 값이 필연적으로 들어가게 된다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;스프링의 빈 생성자에서 환경변수의 값을 초기화 하려면 생성자 매개변수나 @PostConstruct 애너테이션을 사용하거나 애초에 생성자에 @Value 어노테이션을 붙이자.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;End&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이슈 해결 PR&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;: &lt;a href=&quot;https://github.com/Playlist-pack/Server/pull/66&quot;&gt;♻️ [refactor] : spotify API 성능 개선 by ulsandonghun &amp;middot; Pull Request #66 &amp;middot; Playlist-pack/Server (github.com)&lt;/a&gt;&lt;/p&gt;</description>
      <category>@Value</category>
      <category>스프링 빈 생명주기</category>
      <author>최동훈1</author>
      <guid isPermaLink="true">https://ulsanboy.tistory.com/174</guid>
      <comments>https://ulsanboy.tistory.com/174#entry174comment</comments>
      <pubDate>Mon, 5 Feb 2024 19:34:28 +0900</pubDate>
    </item>
    <item>
      <title>람다와 스트림만 사용하자(feat. CleanCode) 2024-01-30</title>
      <link>https://ulsanboy.tistory.com/172</link>
      <description>&lt;p style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;세뇌하자. 나는 람다와 스트림만 사용하는 프로그래머다.. ㅎ &lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;251&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dAljXC/btsDZ27pS2Y/aKWPFcUiAVTKDGsXyjil41/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dAljXC/btsDZ27pS2Y/aKWPFcUiAVTKDGsXyjil41/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dAljXC/btsDZ27pS2Y/aKWPFcUiAVTKDGsXyjil41/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdAljXC%2FbtsDZ27pS2Y%2FaKWPFcUiAVTKDGsXyjil41%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;251&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;251&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #5c5c5c; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;람다식(Lambda Expression) &lt;/h3&gt;
&lt;h4 style=&quot;background-color: #ffffff; color: #5c5c5c; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;문법&lt;/h4&gt;
&lt;p style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;문법은 간단합니다. 코드를 보는게 이해가 더 빠를 것이라고 생각됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1706283879353&quot; class=&quot;cpp&quot; style=&quot;background-color: #f6f7f8; color: #555555; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public int sum(int a, int b) {
    return a + b;
}

//람다식 변환
(a, b) -&amp;gt; a + b;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 style=&quot;background-color: #ffffff; color: #5c5c5c; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;장점&lt;/span&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #5c5c5c; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;매우 깔끔하고 알아보기도 쉽다.&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;동작을 사용하기 위해서 클래스를 정의할 필요도 없다.&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;메소드의 리턴 타입도 없고, 메서드의 이름도 없다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;람다와 함께 사용할 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;매소드 이름없이 작성할 수 있기 때문에&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;익명 함수(Anonymous Function)&lt;/b&gt;라고도 불립니다. 메서드의 이름도 리턴 타입도 없지만 뭐하는 식인지는 쉽게 알 수 있다는 장점 덕분에 java8부터 추가된 것이라고 생각됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #5c5c5c; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;스트림(Stream) API &lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;자바 8 이후부터 추가된 stream은 for문을 사용한 반복문이 갖는 단점들을 해결해줄 수 있다. for문이 갖는 단점으로는 쉽게 indent가 증가하고 if문 같은 분기문이 쉽게 발생한다는 점이다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 style=&quot;background-color: #ffffff; color: #5c5c5c; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;문법&lt;/span&gt;&lt;/h4&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;stream().중개연산.단말연산&lt;/blockquote&gt;
&lt;p style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;문법은 아래의 코드처럼 stream() 생성을 하고 중개 연산 결과들을 계속 반환하면서 이어가다 단말 연산을 이용해 멈춘다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1706283879355&quot; class=&quot;reasonml&quot; style=&quot;background-color: #f6f7f8; color: #555555; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;ArrayList&amp;lt;RacingCar&amp;gt; winners = new ArrayList&amp;lt;&amp;gt;();

for (RacingCar racingCar : racingCars) {
    if (racingCar.isSamePosition(racingCarOfMaxPosition)) {
        winners.add(racingCar);
    }
}
return winners;

// stream api를 활용한 식
private final List&amp;lt;RacingCar&amp;gt; racingCars = new ArrayList&amp;lt;&amp;gt;();

return racingCars.stream()
                .filter(racingCar -&amp;gt; racingCar.isSamePosition(racingCarOfMaxPosition))
                .collect(Collectors.toCollection(ArrayList::new));&lt;/code&gt;&lt;/pre&gt;
&lt;h4 style=&quot;background-color: #ffffff; color: #5c5c5c; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;장점&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #5c5c5c; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;위 코드만 봐도 알듯이 코드의 길이가 확연히 줄어든다.&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;indent도 줄일 수 있어서 가독성이 좋다.&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;원본 데이터로부터 데이터를 읽기만 할 뿐, 원본 데이터 자체를 변경하지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 style=&quot;background-color: #ffffff; color: #5c5c5c; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;중개 연산 매소드&lt;/span&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;725&quot; data-origin-height=&quot;177&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IUuE9/btsDZ1HrSae/R4H1vou1CWglI2hHliAkWK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IUuE9/btsDZ1HrSae/R4H1vou1CWglI2hHliAkWK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IUuE9/btsDZ1HrSae/R4H1vou1CWglI2hHliAkWK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIUuE9%2FbtsDZ1HrSae%2FR4H1vou1CWglI2hHliAkWK%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;725&quot; height=&quot;177&quot; data-origin-width=&quot;725&quot; data-origin-height=&quot;177&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span data-alt=&quot;중개 연산&quot; data-lightbox=&quot;lightbox&quot; data-url=&quot;https://blog.kakaocdn.net/dn/eb83Kl/btrtBPpiXBb/kEb1LcKxNiMdDYWONLFYW0/img.png&quot;&gt;&lt;/span&gt;중개 연산&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #5c5c5c; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;skip(long n) : 해당 스트림의 첫 번째 요소부터 전달된 개수만큼의 요소를 제외한 나머지 요소만으로 이루어진 새로운 스트림을 반환&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 style=&quot;background-color: #ffffff; color: #5c5c5c; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;단말 연산 매소드&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;723&quot; data-origin-height=&quot;128&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GAQwN/btsD0Kerjh7/y3k7bkJP7RNoQvQazoU3X1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GAQwN/btsD0Kerjh7/y3k7bkJP7RNoQvQazoU3X1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GAQwN/btsD0Kerjh7/y3k7bkJP7RNoQvQazoU3X1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGAQwN%2FbtsD0Kerjh7%2Fy3k7bkJP7RNoQvQazoU3X1%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;723&quot; height=&quot;128&quot; data-origin-width=&quot;723&quot; data-origin-height=&quot;128&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span data-alt=&quot;단말 연산&quot; data-lightbox=&quot;lightbox&quot; data-url=&quot;https://blog.kakaocdn.net/dn/cx0WMG/btrtuvlqIU8/Nx4WSFAZErx1Z0CSKWVE31/img.png&quot;&gt;&lt;/span&gt;단말 연산&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #5c5c5c; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;anyMatch : 하나라도 조건을 만족하는 요소가 있다면 True 반환&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;allMatch : 모든 조건을 만족한다면&lt;span&gt;&amp;nbsp;&lt;/span&gt;있다면 True 반환&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;noneMatch : 모든 조건을 만족하지 않는다면 True 반환&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;findFirst() : 처음으로 해당 요소를 찾았을때 Optional로 객체 반환&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;findAny() : 전체를 돌고 해당 요소 하나라도 찾았을때&lt;span&gt;&amp;nbsp;&lt;/span&gt;Optional로 객체 반환&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;max() : 해당 스트림의 요소중 가장 큰 값을 가지는 요소를 참조하는 Optional로 객체 반환&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;min() : 가장 작은 값을 가지는 요소를 참조하는 Optional로 객체 반환&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;sum(), average() : 합, 평균값에 대하여 반환&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1706283879356&quot; class=&quot;less&quot; style=&quot;background-color: #f6f7f8; color: #555555; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;Stream.of(&quot;3&quot;, &quot;1&quot;, &quot;4&quot;, &quot;2&quot;, &quot;5&quot;, &quot;5&quot;) // 문자열 스트림 생성
        .map(Integer::parseInt) // 문자열 스트림을 정수형 스트림으로 변환
        .sorted() // 정렬
        .distinct() // 중복제거
        .limit(3) // 갯수를 3개로 제한
        .collect(Collectors.toList()) // 리스트로 변환 =&amp;gt; {1, 2, 3} : 단말 연산
        .stream() // 다시 정수형 값을 갖는 스트림으로 변환
        .filter(x -&amp;gt; x &amp;gt; 1) // 1보다 큰 값만 갖도록 필터링함 {2, 3}
        .forEach(System.out::println); // 2와 3만 출력됨&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;background-color: #ffffff; color: #5c5c5c; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;Reference&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #5c5c5c; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;&lt;a href=&quot;https://sehun-kim.github.io/sehun/java-lambda-stream/#4&quot;&gt;https://sehun-kim.github.io/sehun/java-lambda-stream/#4&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;figure id=&quot;og_1706283879356&quot; style=&quot;background-color: #ffffff; color: #5c5c5c; text-align: start;&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/3YYUK/hyNqEP0cr4/avl6ksZya5UvZvWbmXxkMK/img.png?width=746&amp;amp;height=454&amp;amp;face=0_0_746_454&quot; data-og-url=&quot;https://sehun-kim.github.io/sehun/sehun/java-lambda-stream/&quot; data-og-source-url=&quot;https://sehun-kim.github.io/sehun/java-lambda-stream/#4&quot; data-og-host=&quot;sehun-kim.github.io&quot; data-og-description=&quot;Java 8부터 추가 된 Stream과 Lambda에 대해 알아보자.&quot; data-og-title=&quot;[JAVA] 예제로 배워보는 Stream &amp;amp; Lambda&quot; data-og-type=&quot;article&quot; data-ke-align=&quot;alignCenter&quot; data-ke-type=&quot;opengraph&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://sehun-kim.github.io/sehun/java-lambda-stream/#4&quot; data-source-url=&quot;https://sehun-kim.github.io/sehun/java-lambda-stream/#4&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/3YYUK/hyNqEP0cr4/avl6ksZya5UvZvWbmXxkMK/img.png?width=746&amp;amp;height=454&amp;amp;face=0_0_746_454');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;[JAVA] 예제로 배워보는 Stream &amp;amp; Lambda&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; style=&quot;color: #909090;&quot; data-ke-size=&quot;size16&quot;&gt;Java 8부터 추가 된 Stream과 Lambda에 대해 알아보자.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; style=&quot;color: #909090;&quot; data-ke-size=&quot;size16&quot;&gt;sehun-kim.github.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #5c5c5c; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;&lt;a href=&quot;https://ahndding.tistory.com/23&quot;&gt;https://ahndding.tistory.com/23&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;figure id=&quot;og_1706283879357&quot; style=&quot;background-color: #ffffff; color: #5c5c5c; text-align: start;&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/A8VKX/hyNrBjObzT/uuTY3O109say8dckKtlrK0/img.png?width=431&amp;amp;height=99&amp;amp;face=0_0_431_99,https://scrap.kakaocdn.net/dn/cixInD/hyNrAyqOOO/xAZHmR4q0ImKT8sNKQgJ70/img.png?width=431&amp;amp;height=99&amp;amp;face=0_0_431_99,https://scrap.kakaocdn.net/dn/bHZMhR/hyNrLzXgy9/qBWxcfbetPqGFb8Xwiont0/img.jpg?width=960&amp;amp;height=960&amp;amp;face=0_0_960_960&quot; data-og-url=&quot;https://ahndding.tistory.com/23&quot; data-og-source-url=&quot;https://ahndding.tistory.com/23&quot; data-og-host=&quot;ahndding.tistory.com&quot; data-og-description=&quot;Stream이란? Java8부터 지원하는 Stream은 컬렉션, 배열등에 대해 저장되어있는 요소들을 하나씩 참조하며 반복적인 처리를 가능케하는 기능이다. 이러한 Stream을 이용한다면 불필요한 for문과 그 안&quot; data-og-title=&quot;[Java] Java 8 Stream이란?&quot; data-og-type=&quot;article&quot; data-ke-align=&quot;alignCenter&quot; data-ke-type=&quot;opengraph&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://ahndding.tistory.com/23&quot; data-source-url=&quot;https://ahndding.tistory.com/23&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/A8VKX/hyNrBjObzT/uuTY3O109say8dckKtlrK0/img.png?width=431&amp;amp;height=99&amp;amp;face=0_0_431_99,https://scrap.kakaocdn.net/dn/cixInD/hyNrAyqOOO/xAZHmR4q0ImKT8sNKQgJ70/img.png?width=431&amp;amp;height=99&amp;amp;face=0_0_431_99,https://scrap.kakaocdn.net/dn/bHZMhR/hyNrLzXgy9/qBWxcfbetPqGFb8Xwiont0/img.jpg?width=960&amp;amp;height=960&amp;amp;face=0_0_960_960');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;[Java] Java 8 Stream이란?&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; style=&quot;color: #909090;&quot; data-ke-size=&quot;size16&quot;&gt;Stream이란? Java8부터 지원하는 Stream은 컬렉션, 배열등에 대해 저장되어있는 요소들을 하나씩 참조하며 반복적인 처리를 가능케하는 기능이다. 이러한 Stream을 이용한다면 불필요한 for문과 그 안&lt;/p&gt;
&lt;p class=&quot;og-host&quot; style=&quot;color: #909090;&quot; data-ke-size=&quot;size16&quot;&gt;ahndding.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>기록/JAVA</category>
      <category>람다와 스트림</category>
      <author>최동훈1</author>
      <guid isPermaLink="true">https://ulsanboy.tistory.com/172</guid>
      <comments>https://ulsanboy.tistory.com/172#entry172comment</comments>
      <pubDate>Tue, 30 Jan 2024 01:45:32 +0900</pubDate>
    </item>
    <item>
      <title>@NotNull VS @Column(nullable = false)</title>
      <link>https://ulsanboy.tistory.com/173</link>
      <description>&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;한줄 결론 : 둘다 ddl 생성될때, Not null 제약조건을 걸어준다. 그러나 @Column은 실제 쿼리가 날라간 뒤에 DB에서 예외를 발생하고 @Notnull은 쿼리가 날라가기 전에 예외를 처리해준다.&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;오늘은 다음의 고민 때문에 글을 작성하게 되었다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #555555; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc; color: #666666;&quot;&gt;JPA에서 DDL을 자동으로 생성할 수 있는데, 이 때 not null 옵션은 어떻게 붙이나?&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #666666;&quot;&gt;JPA의 엔티티 객체에 @NotNull 검증 어노테이션을 주면 어떻게 되나?&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #666666;&quot;&gt;@NotNull 대신 @NotEmpty나 @NotBlank를 쓰면 어떻게 되나?&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #666666;&quot;&gt;엔티티에 검증 로직을 붙이면 어떻게 작동하나?&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style2&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;JPA의 DDL 생성과 제약조건 매핑&lt;/h2&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;JPA는 데이터베이스 스키마를 자동으로 생성하는 기능을 지원한다.&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;엔티티로 삼을 객체에 @Entity 어노테이션을 붙이고,&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;추가적으로 여러 매핑 정보를 엔티티의 필드 위에 추가하여, 자동 생성되는 DDL에 제약조건을 추가할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1706388903620&quot; class=&quot;less&quot; style=&quot;color: #555555; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Entity
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(name = &quot;email&quot;, nullable = false)
    private String email;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 위처럼 어노테이션을 통해 정보를 매핑해주면 어떨까?&lt;/p&gt;
&lt;pre id=&quot;code_1706388903621&quot; class=&quot;sql&quot; style=&quot;color: #555555; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;create table book (
    id bigint generated by default as identity,
    isbn varchar(255) not null,
    primary key (id)
)&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;위처럼 자동 생성된 SQL문이 실행될 것이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style2&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;nullable=false vs @NotNull&lt;/h2&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 때, 내가 위 코드에 적은 내용 중&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;nullable = false&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;라는 내용이 있다.&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;nullable은 @Column의 속성 중 하나로, 기본값은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;true&lt;/b&gt;이다.&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;값을 false로 설정해 주면, 해당 필드는 DDL 생성 시 not null이라는 조건이 붙은 채로 생성된다.&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그런데, 누군가는 이런 생각을 할 수도 있겠다.&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;저렇게 써주면 DB에는 null이 들어갈 수 없지만, 엔티티의 필드에 넣는 건 가능하니까 위험하지 않을까?&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;엔티티에 Spring Bean Validation을 써서 검증하면 어떨까?&quot;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;코드를 먼저 보자.&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1706388903622&quot; class=&quot;less&quot; style=&quot;color: #555555; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Entity
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(name = &quot;email&quot;, nullable = false)
    @NotNull
    private String email;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;어노테이션이 줄줄이 들어가니 보기가 힘들어진다!&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그런데 여기서 알아낸 재미난 사실이 있다.&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;위&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;@Column에서 nullable=false를 제거해도, @NotNull이 붙어있으면 DDL 생성 시 not null로 생성&lt;/b&gt;된다는 것!!&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2021-02-18 오후 3.50.34.png&quot; data-origin-width=&quot;514&quot; data-origin-height=&quot;98&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b1msBn/btsDZ2Gy172/H0eMXxsDxKK4VYG3Kc6PsK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b1msBn/btsDZ2Gy172/H0eMXxsDxKK4VYG3Kc6PsK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b1msBn/btsDZ2Gy172/H0eMXxsDxKK4VYG3Kc6PsK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb1msBn%2FbtsDZ2Gy172%2FH0eMXxsDxKK4VYG3Kc6PsK%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;514&quot; height=&quot;98&quot; data-filename=&quot;스크린샷 2021-02-18 오후 3.50.34.png&quot; data-origin-width=&quot;514&quot; data-origin-height=&quot;98&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span data-alt=&quot;이렇게만 써줬는데&quot; data-lightbox=&quot;lightbox&quot; data-url=&quot;https://blog.kakaocdn.net/dn/IHYEi/btqXJVVTVlu/ASj2D9EkOFjnOOwMoHMmvK/img.png&quot;&gt;&lt;/span&gt;이렇게만 써줬는데&lt;span data-alt=&quot;이게 not null로 알아서 바뀌네?&quot; data-lightbox=&quot;lightbox&quot; data-url=&quot;https://blog.kakaocdn.net/dn/bbaBKA/btqXLem12MA/Z7oSHjSHPsPBqaRsjKtUJk/img.png&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2021-02-18 오후 3.50.41.png&quot; data-origin-width=&quot;560&quot; data-origin-height=&quot;68&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pzlbA/btsD3IUjoZs/u37QxOkJa7diqraWQ1kc40/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pzlbA/btsD3IUjoZs/u37QxOkJa7diqraWQ1kc40/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pzlbA/btsD3IUjoZs/u37QxOkJa7diqraWQ1kc40/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpzlbA%2FbtsD3IUjoZs%2Fu37QxOkJa7diqraWQ1kc40%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;560&quot; height=&quot;68&quot; data-filename=&quot;스크린샷 2021-02-18 오후 3.50.41.png&quot; data-origin-width=&quot;560&quot; data-origin-height=&quot;68&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span data-alt=&quot;이게 not null로 알아서 바뀌네?&quot; data-lightbox=&quot;lightbox&quot; data-url=&quot;https://blog.kakaocdn.net/dn/bbaBKA/btqXLem12MA/Z7oSHjSHPsPBqaRsjKtUJk/img.png&quot;&gt;&lt;/span&gt;이게 not null로 알아서 바뀌네?&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이게 어떻게 가능할까? 다행히 이에 대해 다룬&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #04beb8;&quot; href=&quot;https://www.baeldung.com/hibernate-notnull-vs-nullable&quot;&gt;포스팅&lt;/a&gt;을 찾았다.&lt;/p&gt;
&lt;figure id=&quot;og_1706388903624&quot; style=&quot;color: #555555; text-align: start;&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dAdrXa/hyJjLK6k4m/K0pABm0HvdWWOdw9VsBIkK/img.jpg?width=952&amp;amp;height=498&amp;amp;face=0_0_952_498,https://scrap.kakaocdn.net/dn/kkLAG/hyJjVfTKxk/DpreK2sSYgkOERr4USEQCK/img.jpg?width=952&amp;amp;height=498&amp;amp;face=0_0_952_498&quot; data-og-url=&quot;https://www.baeldung.com/hibernate-notnull-vs-nullable&quot; data-og-source-url=&quot;https://www.baeldung.com/hibernate-notnull-vs-nullable&quot; data-og-host=&quot;www.baeldung.com&quot; data-og-description=&quot;A quick and practical overview of @NotNull and @Column(nullable = false) in Java.&quot; data-og-title=&quot;Hibernate @NotNull vs @Column(nullable = false) | Baeldung&quot; data-og-type=&quot;article&quot; data-ke-type=&quot;opengraph&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://www.baeldung.com/hibernate-notnull-vs-nullable&quot; data-source-url=&quot;https://www.baeldung.com/hibernate-notnull-vs-nullable&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dAdrXa/hyJjLK6k4m/K0pABm0HvdWWOdw9VsBIkK/img.jpg?width=952&amp;amp;height=498&amp;amp;face=0_0_952_498,https://scrap.kakaocdn.net/dn/kkLAG/hyJjVfTKxk/DpreK2sSYgkOERr4USEQCK/img.jpg?width=952&amp;amp;height=498&amp;amp;face=0_0_952_498');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;Hibernate @NotNull vs @Column(nullable = false) | Baeldung&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; style=&quot;color: #909090;&quot; data-ke-size=&quot;size16&quot;&gt;A quick and practical overview of @NotNull and @Column(nullable = false) in Java.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; style=&quot;color: #909090;&quot; data-ke-size=&quot;size16&quot;&gt;www.baeldung.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;여기에서 이에 대한 언급을 하고 있다.&lt;/p&gt;
&lt;table style=&quot;color: #666666; text-align: center; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;How's that possible?&lt;/span&gt;&lt;br /&gt;&lt;span&gt;As it turns out,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;out of the box, Hibernate translates the bean validation annotations&lt;br /&gt;applied to the entities into the DDL schema metadata.&lt;br /&gt;&lt;/b&gt;(...)&lt;b&gt;&lt;br /&gt;&lt;/b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;However, if, for any reason, we want&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;to disable this Hibernate feature, all we need to do is to sethibernate.validator.apply_to_ddlproperty to&amp;nbsp;false.&lt;br /&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Hibernate는 엔티티에 적용된 Bean Validation 어노테이션 역시 DDL로 변환한다는 것!&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;만약 이 기능을 비활성화하려면, application.properties에 다음 한 줄을 추가하면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1706388903624&quot; class=&quot;stylus&quot; style=&quot;color: #555555; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;spring.jpa.properties.hibernate.validator.apply_to_ddl=false&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style3&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;인용한 글에서는, 결론적으로 nullable = false 보다 @NotNull을 추천하고 있다.&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;@NotNull&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;어노테이션을 쓰면, 데이터베이스에 SQL 쿼리를 보내기 전에 예외가 발생한다.&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;JPA의 Repository 인터페이스가 잘못된 Entity를 저장할 때, ConstraintViolationException을 발생시킨다.&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;(그 때문에, @Valid나 @Validated 없이도 엔티티를 자연스럽게 검증할 수 있다)&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;물론 값이 검증에 맞지 않아도 객체를 생성하는 것은 가능했다.&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;또한, Bean Validation의 트리거가 EntityManager가 Flush된 이후이므로,&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;원하는 타이밍에 맞지 않아 의도치 않은 오류를 맞을 수도 있다.&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그러나&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;nullable=false&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;로 @Column 어노테이션에 속성을 붙이면,&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;null을 넣은 엔티티를 생성하면 생성이 된 뒤 Repository에 전달되고,&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 값이 DB에 넘어간 뒤에 예외가 발생해 위험한 오류를 맞을 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이렇게 둘의 장단점을 비교해 보았을 때,&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;@NotNull을 써서 DB와 서버 모두의 안정성을 챙기는 것이 좋겠다고 생각했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style2&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;다른 Validation은?&lt;/h2&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그런데, 여기서 하나 궁금한 게 생겼다.&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Spring Bean Validation에서는 @NotNull 외에도, 이와 비슷한 어노테이션을 제공한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #555555; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: inherit; color: #666666;&quot;&gt;@NotNull : null이 들어갈 경우 예외를 발생시킨다.&lt;/li&gt;
&lt;li style=&quot;list-style-type: inherit; color: #666666;&quot;&gt;@NotEmpty : 문자열에 null이나 &quot;&quot;가 들어가면 예외를 발생시킨다. NotNull의 문자열 버전 상위호환.&lt;/li&gt;
&lt;li style=&quot;list-style-type: inherit; color: #666666;&quot;&gt;@NotBlank : 문자열에 null, &quot;&quot;, &quot; &quot;가 들어가면 예외를 발생시킨다. NotEmpty의 상위호환.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;위의 @NotEmpty와 @NotBlank는, 문자열에만 적용할 수 있는 검증 어노테이션이다.&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;내가 단순하게 생각했을 때에는, 이 둘도 null을 금지하니까 not null 옵션을 자동으로 붙여주겠지? 라고 생각했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2021-02-18 오후 3.22.10.png&quot; data-origin-width=&quot;522&quot; data-origin-height=&quot;464&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OuKf6/btsD4uuzwql/Mry9FcAo881U3kM7v1y8bk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OuKf6/btsD4uuzwql/Mry9FcAo881U3kM7v1y8bk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OuKf6/btsD4uuzwql/Mry9FcAo881U3kM7v1y8bk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOuKf6%2FbtsD4uuzwql%2FMry9FcAo881U3kM7v1y8bk%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;393&quot; height=&quot;464&quot; data-filename=&quot;스크린샷 2021-02-18 오후 3.22.10.png&quot; data-origin-width=&quot;522&quot; data-origin-height=&quot;464&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span data-alt=&quot;종류별로 하나씩 써봤다.&quot; data-lightbox=&quot;lightbox&quot; data-url=&quot;https://blog.kakaocdn.net/dn/bg8yXs/btqXN9S771X/4sK86QBBO61CGmC6eBwjjK/img.png&quot;&gt;&lt;/span&gt;종류별로 하나씩 써봤다.&lt;span data-alt=&quot; name과 content는 not null 옵션이 들어가지 않았다!!&quot; data-lightbox=&quot;lightbox&quot; data-url=&quot;https://blog.kakaocdn.net/dn/Tp6pt/btqXJXl1Nj7/QsVRICT7cgqKZt56r33CZk/img.png&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2021-02-18 오후 3.22.23.png&quot; data-origin-width=&quot;768&quot; data-origin-height=&quot;722&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cd4GRm/btsD3NVstx6/fKmkes8kROUQDVOV4jzbs0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cd4GRm/btsD3NVstx6/fKmkes8kROUQDVOV4jzbs0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cd4GRm/btsD3NVstx6/fKmkes8kROUQDVOV4jzbs0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcd4GRm%2FbtsD3NVstx6%2FfKmkes8kROUQDVOV4jzbs0%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;536&quot; height=&quot;722&quot; data-filename=&quot;스크린샷 2021-02-18 오후 3.22.23.png&quot; data-origin-width=&quot;768&quot; data-origin-height=&quot;722&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span data-alt=&quot; name과 content는 not null 옵션이 들어가지 않았다!!&quot; data-lightbox=&quot;lightbox&quot; data-url=&quot;https://blog.kakaocdn.net/dn/Tp6pt/btqXJXl1Nj7/QsVRICT7cgqKZt56r33CZk/img.png&quot;&gt;&lt;/span&gt; name과 content는 not null 옵션이 들어가지 않았다!!&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그런데 당황스러운 상황 발생. 저 둘은 not null을 붙여주지 않는다!!&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;왜일까 찾아본 결과 여러 가지 정보를 얻을 수 있었다.&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;아래는 그 사이 했던 삽질의 기록이다.&lt;/p&gt;
&lt;div style=&quot;color: #555555; text-align: start;&quot; data-text-less=&quot;닫기&quot; data-text-more=&quot;더보기&quot; data-ke-type=&quot;moreLess&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;&lt;a style=&quot;color: #909090;&quot;&gt;더보기&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;다만, 이것은 설득력이 크지 않다. 그래서 이유를 찾기 위해, hibernate validator 페이지를 확인했다.&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;Hibernate 공식 문서를 읽어보다&lt;/h2&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a style=&quot;color: #04beb8;&quot; href=&quot;https://hibernate.org/validator/&quot;&gt;Hibernate의 공식 validator 페이지&lt;/a&gt;에서는 @NotEmpty를 써야하는 상황에서 다른 방법을 권하는 것을 확인했다.&lt;/p&gt;
&lt;figure id=&quot;og_1706388903626&quot; style=&quot;color: #555555; text-align: start;&quot; data-og-image=&quot;&quot; data-og-url=&quot;https://hibernate.org/validator/&quot; data-og-source-url=&quot;https://hibernate.org/validator/&quot; data-og-host=&quot;hibernate.org&quot; data-og-description=&quot;Express validation rules in a standardized way using annotation-based constraints and benefit from transparent integration with a wide variety of frameworks.&quot; data-og-title=&quot;The Bean Validation reference implementation. - Hibernate Validator&quot; data-og-type=&quot;website&quot; data-ke-type=&quot;opengraph&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://hibernate.org/validator/&quot; data-source-url=&quot;https://hibernate.org/validator/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('&amp;quot;&amp;quot;');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;The Bean Validation reference implementation. - Hibernate Validator&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; style=&quot;color: #909090;&quot; data-ke-size=&quot;size16&quot;&gt;Express validation rules in a standardized way using annotation-based constraints and benefit from transparent integration with a wide variety of frameworks.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; style=&quot;color: #909090;&quot; data-ke-size=&quot;size16&quot;&gt;hibernate.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;pre id=&quot;code_1706388903627&quot; class=&quot;less&quot; style=&quot;color: #555555; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class Car {

   @NotNull
   private String manufacturer;

   @NotNull
   @Size(min = 2, max = 14)
   private String licensePlate;

   @Min(2)
   private int seatCount;

   // ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;위의 코드는 공식 페이지의 메인에서 보여주는 예시 코드이다.&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;@NotBlank나 @NotEmpty를 통해 not null 옵션을 조절하는 대신,&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;@NotNull을 통해 DB에 not null 옵션을 주고 @Size를 통해 저장할 때 서버에서 추가로 검증을 하도록 한다.&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;왜 @NotBlank와 @NotEmpty를 지원하지 않는지 조금 이해가 되는 느낌이었다.&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;추측하기로는, 일관성을 위해&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;@NotNull에 대해서만 not null 옵션으로 번역&lt;/b&gt;해 주고,&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그 외의 것은 번역하지 않는 것으로 보인다.&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;@NotEmpty와 @NotBlank는 문자열에만 적용되는 특수한 검증 어노테이션인 만큼, 이해가 가는 부분이다.&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;여러분들도 @Column 대신 검증 어노테이션을 쓰려고 한다면, @NotNull 외의 다른 것을 쓰지 않도록 주의!!&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;*&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #04beb8;&quot; href=&quot;https://docs.jboss.org/hibernate/stable/validator/reference/en-US/html_single/#validator-checkconstraints-orm&quot;&gt;동일 문서의 ORM 관련 파트&lt;/a&gt;에서는 앞서 언급한 자동 변환에 대해 보다 자세히 설명하고 있다. 읽어보시길.&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;* 210221 내용 추가&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;공식 문서에서는 @NotBlank와 @NotEmpty의 경우&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;Hibernate metadata impact가 없도록 설정되어 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그에 비해 @NotNull은 not null 옵션이&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;들어가도록 설정되어 있었다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2021-02-21 오후 3.45.48.png&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;818&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dcoeRH/btsDZ3Fs19j/QItrgPzHpE3REidUibbwSk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dcoeRH/btsDZ3Fs19j/QItrgPzHpE3REidUibbwSk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dcoeRH/btsDZ3Fs19j/QItrgPzHpE3REidUibbwSk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdcoeRH%2FbtsDZ3Fs19j%2FQItrgPzHpE3REidUibbwSk%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;818&quot; data-filename=&quot;스크린샷 2021-02-21 오후 3.45.48.png&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;818&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style2&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;그러면, @NotEmpty와 @NotBlank는 쓰지 않는게 좋나?&lt;/h2&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;무조건 그렇다고 생각하지는 않는다.&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;만약 JPA의 DDL 자동 생성을 사용하지 않지만, 엔티티의 값을 검증하고 싶다면&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;문자열에 한해서는 @NotEmpty와 @NotBlank가 더 유리하다고 생각한다.&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;(예 : flyway를 이용해 DB 쿼리를 관리해서 DDL 자동 생성을 쓰지 않는 경우)&lt;/p&gt;
&lt;hr data-ke-style=&quot;style2&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;테스트를 통한 검증 로직 확인&lt;/h2&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이제 테스트를 통해 직접 확인해 보려고 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1706388903628&quot; class=&quot;css&quot; style=&quot;color: #555555; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;package com.booksdiary.domain;

import lombok.*;
import org.hibernate.validator.constraints.ISBN;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import javax.persistence.*;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Positive;
import javax.validation.constraints.Size;
import java.time.LocalDateTime;

@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@EntityListeners(AuditingEntityListener.class)
@Entity
public class Book {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @CreatedDate
    private LocalDateTime createdAt;

    @ISBN
    private String isbn;

    @NotNull
    @Size(min=1)
    private String name;

    @NotBlank
    private String content;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;먼저, Book이라는 클래스를 사용한다. (실제 프로젝트에서 쓰는 클래스에서 필드만 줄였다)&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이제 테스트 코드를 만들어 준다.&lt;/p&gt;
&lt;pre id=&quot;code_1706388903630&quot; class=&quot;livescript&quot; style=&quot;color: #555555; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;package com.booksdiary.domain;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.NullAndEmptySource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.validation.annotation.Validated;

import javax.validation.ConstraintViolationException;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import java.time.LocalDateTime;

import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;

@DataJpaTest
public class BookTest {
    @Autowired
    private BookRepository bookRepository;

    @DisplayName(&quot;책을 생성하여 저장하면, 올바르게 수행된다.&quot;)
    @Test
    void saveTest() {
        Book book = Book.builder()
                .isbn(&quot;978-3-16-148410-0&quot;)
                .name(&quot;카프카&quot;)
                .content(&quot;내용&quot;)
                .build();

        Book savedBook = bookRepository.save(book);
        assertThat(savedBook.getName()).isEqualTo(&quot;카프카&quot;);
        assertThat(savedBook.getIsbn()).isEqualTo(&quot;978-3-16-148410-0&quot;);
    }

    @DisplayName(&quot;예외 테스트 : 비어있거나 null인 이름이 들어오면 예외가 발생한다.&quot;)
    @NullAndEmptySource
    @ParameterizedTest
    void invalidNameTest(String invalidName) {
        Book book = Book.builder()
                .isbn(&quot;978-3-16-148410-0&quot;)
                .name(invalidName)
                .content(&quot;내용&quot;)
                .build();

        assertThatThrownBy(() -&amp;gt; bookRepository.save(book))
                .isInstanceOf(ConstraintViolationException.class);
    }

    @DisplayName(&quot;예외 테스트 : ISBN 넘버가 아닌 값이 들어오면 예외가 발생한다.&quot;)
    @Test
    void invalidISBNTest() {
        Book book = Book.builder()
                .isbn(&quot;12345&quot;)
                .name(&quot;카프카&quot;)
                .content(&quot;내용&quot;)
                .build();
        assertThatThrownBy(() -&amp;gt; bookRepository.save(book))
                .isInstanceOf(ConstraintViolationException.class);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;학습 테스트인 만큼 엄격하게 작성하지는 않았다.&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;javax.validation 뿐 아니라, hibernate에서 제공하는 validation 역시 사용해 보았다.&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어, @ISBN은 isbn 양식에 맞지 않는 값을 Repository를 통해 저장할 때 예외를 발생시킨다!&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2021-02-18 오후 6.29.24.png&quot; data-origin-width=&quot;542&quot; data-origin-height=&quot;388&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yhiqZ/btsD7otKuGn/1ISOiuUH5TRWhKmzku8K40/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yhiqZ/btsD7otKuGn/1ISOiuUH5TRWhKmzku8K40/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yhiqZ/btsD7otKuGn/1ISOiuUH5TRWhKmzku8K40/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyhiqZ%2FbtsD7otKuGn%2F1ISOiuUH5TRWhKmzku8K40%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;402&quot; height=&quot;388&quot; data-filename=&quot;스크린샷 2021-02-18 오후 6.29.24.png&quot; data-origin-width=&quot;542&quot; data-origin-height=&quot;388&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span data-alt=&quot;테스트는 무난히 통과~&quot; data-lightbox=&quot;lightbox&quot; data-url=&quot;https://blog.kakaocdn.net/dn/compwz/btqXLfzOCnV/4ehaaeYT68nCTx0TXepoa0/img.png&quot;&gt;&lt;/span&gt;테스트는 무난히 통과~&lt;/p&gt;
&lt;hr data-ke-style=&quot;style2&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;DTO에 검증 어노테이션을 달아서 잘못된 값이 오는 걸 막는 건 이해가 된다.&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그런데 왜 엔티티에까지 이렇게 검증 로직을 달아야 하나?&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;내가 생각한 이유는 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #555555; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc; color: #666666;&quot;&gt;의도치 않게 엔티티에&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;잘못된 값&lt;/b&gt;을 넣어도,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Repository에 저장할 때 예외를 발생&lt;/b&gt;시켜 더 큰 문제를 막을 수 있다.&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #666666;&quot;&gt;&lt;b&gt;@NotNull 어노테이션을 통해 @Column의 nullable을 대체&lt;/b&gt;할 수 있다.&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #666666;&quot;&gt;다른 사람이 코드를 봤을 때,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;어떤 제약 조건이 있는지 파악&lt;/b&gt;하기 쉽다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;물론 단점도 존재한다. 제일 큰 단점은&lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;코드가 더러워 보인다&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;라는 것.&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;어노테이션 때문에 스프링 하기 싫다는 사람도 많으니 뭐.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style2&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;처음엔 @NotNull을 달았는데 왜 DB에 제약조건이 생기지? 로 시작된 글이었다.&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;쓰다 보니 공부할 게 점점 늘어서 장황한 글이 되어버렸다.&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;참고한 글은 다음과 같다.&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Spring Boot에서의 Bean Validation :&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #04beb8;&quot; href=&quot;https://jongmin92.github.io/2019/11/18/Spring/bean-validation-1/&quot;&gt;링크&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Hibernate Validator 가이드 :&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #04beb8;&quot; href=&quot;https://docs.jboss.org/hibernate/stable/validator/reference/en-US/html_single/#validator-integration&quot;&gt;링크&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Hibernate Validator 공식 문서 :&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #04beb8;&quot; href=&quot;https://docs.jboss.org/hibernate/validator/6.0/api/index.html?org/hibernate/validator/constraints/NotEmpty.html&quot;&gt;링크&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Hibernate @NotNull vs @Column(nullable = false)&amp;nbsp;:&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #04beb8;&quot; href=&quot;https://www.baeldung.com/hibernate-notnull-vs-nullable&quot;&gt;링크&lt;/a&gt;&lt;/p&gt;
&lt;p id=&quot;a416&quot; style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;JavaBean Validation과 Hibernate Validator 그리고 Spring Boot :&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #04beb8;&quot; href=&quot;https://medium.com/@SlackBeck/javabean-validation%EA%B3%BC-hibernate-validator-%EA%B7%B8%EB%A6%AC%EA%B3%A0-spring-boot-3f31aee610f5&quot;&gt;링크&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Validation 어디까지 해봤니?(Toast MeetUp!) :&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #04beb8;&quot; href=&quot;https://meetup.toast.com/posts/223&quot;&gt;링크&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;nullable = false와 @NotNull의 차이점 :&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;a style=&quot;color: #04beb8;&quot; href=&quot;https://bottom-to-top.tistory.com/14&quot;&gt;링크&lt;/a&gt; &lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;잘못된 내용이 있다면 지적 부탁드립니다.&lt;/i&gt;&lt;/p&gt;</description>
      <category>@Column(nullable=false)</category>
      <category>@NotNull</category>
      <author>최동훈1</author>
      <guid isPermaLink="true">https://ulsanboy.tistory.com/173</guid>
      <comments>https://ulsanboy.tistory.com/173#entry173comment</comments>
      <pubDate>Sun, 28 Jan 2024 05:57:25 +0900</pubDate>
    </item>
    <item>
      <title>클린코드 책 리뷰및 요약(2024-01-27)</title>
      <link>https://ulsanboy.tistory.com/171</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이 책을 읽기전, 혹은 클린코드에 대한 개념을 알게되기 전에 내가썼던 코드는 쓰레기 같았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발은 나혼자하는 것이 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;남과 같이 하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 내가 책을 읽으며 앞으로 코딩을 하며 지켜야 할 것들을 정리해 보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span style=&quot;color: #f15f5f;&quot;&gt;1. 클린코드 핵심 요약 및 정리&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;hr data-ke-style=&quot;style5&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;[ 1. 객체의 생성에도 유의미한 이름을 사용하라 ]&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;객체의 생성자가 오버로딩 되는 경우 어떠한 값으로 어떻게 생성되는지 정보가 부족할 수 있다. 그러므로 이러한 경우에는 정적 팩토리 메소드를 사용하는 것이 보다 명확한 코드를 작성하게 해 줄 것이다. 하지만 구현을 드러내는 이름은 피하는 것이 좋다. (p32 참고)&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1614907572049&quot; class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// 두 번째 인자가 무엇인지 파악이 어렵다.
Product product = new Product(&quot;사과&quot;, 10000);

// 이름을 부여하여 두 번째 인자를 명확하게 파악할 수 있다.
Product product = Product.withPrice(&quot;사과&quot;, 10000);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;[ 2. 함수는 하나의 역할만 해야한다. ]&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;함수는 지정된 이름 아래에서 한 단계 수준의 추상화 수준을 유지해야 한다. 그것이 하나의 역할 및 기능만을 하는 것이다. 또는 의미 있는 다른 함수로 추출가능한 부분이 없다면 그것 역시 하나의 역할 및 기능만을 수행하고 있는 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;예를 들어 다음과 같은 Switch 문은 정말 흔히 작성하는 코드이지만, 많은 문제를 내포하고 있다. (p48 참고)&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1614907732471&quot; class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public Money calculatePay(Employee e) throws InvalidEmployeeType {
  switch(e.type) {
    case COMMISSIONED:
      return calculateCommisionedPay(e);
    case HOURLY:
      return calculateHourlyPay(e);
    case SALARIED:
      return calculateSalariedPay(e);
    default:
      throw new InvalidEmployeeType(e.type);
  }
  
}&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;span&gt;하지만 위의 코드는 다음과 같은 문제점 목록을 가지고 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span&gt;함수가 너무 길다. 새로운 직원 타입이 추가되면 더 길어질 것이다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;한 가지 작업만을 수행하지 않는다. 해딩 직원이 어느 타입인지 확인하고 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;SRP를 위반한다. 새로운 직원 타입이 추가되어도 임금을 계산하는 함수를 변경해야 한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;OCP를 위반한다. &lt;span style=&quot;color: #333333;&quot; data-remove=&quot;true&quot;&gt;새로운 직원 타입이 추가되면 새로운 임금 계산 로직을 위하 코드를 변경해야 한다.&lt;/span&gt;&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;유사한 함수가 계속 파생될 수 있다. 이러한 직원의 타입에 따른 코드는 다른 곳에 중첩될 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이를 해결하기 위해서는 Employee를 추상클래스로 만들고, 직원 유형에 따른 하위 클래스를 선언하도록 하자. 물론 하위 객체를 생성하기 위한 switch문은 불가피하다. 하지만 그래도 유사한 함수마다 분기해주는 것을 처리해줄 수 있으며, 위의 문제점 중 상당수를 해결할 수 있을 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1614908799374&quot; class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public abstract class Employee {
    public abstract int calculatePay();
    public abstract void deliverPay();
}

public class EmployeeFactory {

    public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType {
        switch (r.type) {
            case COMMISSIONED:
                return new CommissionedEmployee(r);
            case HOURLY:
                return HourlyEmployee(r);
            case SALARIED:
                return SalariedEmployee(r);
            default:
                throw new InvalidEmployeeType(r.type);
        }
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;[ 3. 명령과 조회를 분리하라(Command와 Query의 분리) ]&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;앞서 설명한대로 함수는 뭔가를 수행하거나 뭔가를 조회하거나 하나의 역할만을 해야 한다. 두 개의 역할을 동시에 하면 이상한 함수가 탄생하게 된다. 예를 들어 다음과 같이 key값이 존재하는지 확인하고, 존재하지 않으면 데이터를 추가하여 성공하면 true 실패하면 false를 반환하는 함수가 있다고 하자.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1614909552007&quot; class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public boolean set(String attribute, String value);

if(set(&quot;username&quot;, &quot;MangKyu&quot;)) {

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 코드를 접한 사람은 이 함수가 key가 존재하는 경우 overwrite하는지 혹은 존재하지 않을 경우에만 업데이트 하는지 등 자세한 내용을 알 수 없을 것이다. 그 이유는 위의 함수가 명령과 조회를 한번에 처리하기 때문이다. 그렇기 때문에 위의 함수를 분리하여 다음와 같이 작성해주는 것이 명확하다. &lt;span style=&quot;color: #333333;&quot; data-remove=&quot;true&quot;&gt;(p56 참고)&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1614910105875&quot; class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public boolean attributeExists(String attribute);
public boolean setAttribute(String attribute, String value);

if(attributeExists(&quot;username&quot;)) {
    setAttribute(&quot;username&quot;, &quot;MangKyu&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;[ 4. 오류코드 보다는 예외를 활용하자 ]&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오류코드를 반환하면 그에 따른 분기가 일어나게 되고, 또 분기가 필요한 경우 중첩되기 마련이다.&lt;span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1614911382881&quot; class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public Status deletePage(Page page) {
    if(deletePage(page) == E_OK) {
        if(registry.deleteReference(page.name) == E_OK) {
            if(configKeys.deleteKey(page.name.makeKey()) == E_OK) {
                log.info(&quot;page deleted&quot;);
                return E_OK;
            } else {
                log.error(&quot;config key not deleted&quot;);
            }
        } else {
            log.error(&quot;reference not deleted&quot;);
        }
    } else {
        log.error(&quot;page not deleted&quot;);
    }
    return E_ERROR;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 해결하기 위해 각각의 함수에서 예외를 발생시켜 잡는다면 코드를 더욱 간결하게 작성할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1614911676008&quot; class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public void deletePage(Page page) {
    try {
        deletePage(page);
        registry.deleteReference(page.name);
        configKeys.deleteKey(page.name.makeKey());
    } catch (Exception e) {
        log.error(e.getMessage());
    }
}&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;그리고 이렇게 처리하면 try-catch문이 생기게 되는데, 이 역시 분리하는 것이 코드를 이해하기 쉽게 도와줄 것이다. &lt;span style=&quot;color: #333333;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot; data-remove=&quot;true&quot;&gt;(p58 참고)&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1614912041055&quot; class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public void deletePage(Page page) {
    try {
        deletePageAndAllReferences(page);
    } catch (Exception e) {
        log.error(e.getMessage());
    }
}

public void deletePageAndAllReferences(Page page) throws Exception {
    deletePage(page);
    registry.deleteReference(page.name);
    configKeys.deleteKey(page.name.makeKey());
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;[ 5. 여러 예외가 발생하는 경우 Wrapper 클래스로 감싸자 ]&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부 라이브러리를 이용하면 다양한 예외 클래스를 마주하게 된다. 그리고 이러한 예외들을 처리하려면 다음과 같이 상당히 번거로워 진다. &lt;span style=&quot;color: #333333;&quot; data-remove=&quot;true&quot;&gt;(p135 참고)&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1614912914277&quot; class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;ACMEPort port = new ACMEPort(12);
try {
    port.open();
} catch (DeviceResponseException e) {
    log.error(e.getMessage());
} catch (ATM1212UnlockedException e) {
    log.error(e.getMessage());
} catch (GMXError e) {
    log.error(e.getMessage());
} finally {
    ...
}&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;이러한 상황에서 Wrapper 클래스를 이용해 감싸면 효율적으로 예외 처리를 할 수 있다. (p136 참고)&lt;/p&gt;
&lt;pre id=&quot;code_1614913559709&quot; class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;LocalPort port = new LocalPort(12);
try {
    port.open();
} catch (PortDeviceFailure e) {
    log.error(e.getMessage());
} finally {
    ...
}

public class LocalPort {

    private ACMEPort innerPort;

    public LocalPort(int portNumber) {
        this.innerPort = new ACMEPort(portNumber);
    }

    public void open() {
        try {
            innerPort.open();
        } catch (DeviceResponseException e) {
            throw new PortDeviceFailure(e);
        } catch (ATM1212UnlockedException e) {
            throw new PortDeviceFailure(e);
        } catch (GMXError e) {
            throw new PortDeviceFailure(e);
        }
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;[ 6. 테스트 코드의 작성 ]&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;TDD(Test-Driven Development)는 실제 코드를 짜기 전에 단위 테스트를 먼저 작성하는 기법으로, 이를 통해 유연성, 유지보수성, 재사용성을 제공받을 수 있다. TDD의 핵심 규칙 3가지는 다음과 같다. (p155 참고)&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;실패하는 단위 테스트를 작성할 때까지 실제 코드를 작성하지 않는다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;컴파일은 실패하지 않으면서 실행이 실패하는 정도로만 단위 테스트를 작성한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;현재 실패하는 테스트를 통과할 정도로만 실제 코드를 작성한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;실제 코드를 변경한다는 것은 잠재적인 버그가 발생할 수 있음을 내포하고 있다. 테스트 코드가 있다면 변경된 코드를 검증함으로써 이를 해결할 수 있다. 그리고 실제 코드가 변경되면 테스트 코드 역시 변경해주어야 하는데, 이러한 이유로 테스트 코드 역시 가독성있게 작성하는 것이 필요하다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그리고 테스트 코드를 작성할 때에는 다음을 준수하는 것이 좋다.&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;1개의 테스트 함수에 대해 assert를 최소화하라&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;1개의 테스트 함수는 1가지 개념 만을 테스트하라&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;또한 깨끗한 테스트 코드는 First라는 5가지 규칙을 따른다.&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;Fast: 테스트는 빠르게 동작하여 자주 돌릴 수 있어야 한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;Independent: 각각의 테스트는 독립적이며 서로 의존해서는 안된다. &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;Repeatable: 어느 환경에서도 반복 가능해야 한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;Self-Validating: 테스트는 성공 또는 실패로 bool 값으로 결과를 내어 자체적으로 검증되어야 한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;Timely: 테스트는 적시에 즉, 테스트하려는 실제 코드를 구현하기 직전에 구현해야 한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;[ 7. 클래스의 최소화 ]&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클래스 역시 함수와 마찬가지로 간결하게 작성하는 것이 중요하다. 함수는 물리적 크기를 측정했다면 클래스는 몇개의 역할 또는 책임을 갖는지를 척도로 활용하며, 단일 책임 원칙에 따라 1가지 책임만을 가져야 한다. (p173 참고)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;[ 8. 클래스의 응집도 ]&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체 지향 관련 공부를 하면 높은 응집도와 낮은 결합도라는 얘기를 듣는다. 여기서 응집도란 클래스의 메소드와 변수가 얼마나 의존하여 사용되는지를 의미한다. (p177 참고)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 다음과 같은 Stack 클래스는 size()를 제외한 모든 함수에서 두 인스턴스 변수를 사용하므로 응집도가 아주 높다.&lt;/p&gt;
&lt;pre id=&quot;code_1619250890216&quot; class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class Stack {
    
    private int topOfStack=0;
    private List&amp;lt;Integer&amp;gt; elements = new LinkedList&amp;lt;&amp;gt;();
    
    public int size() {
        return topOfStack;
    }
    
    public void push(int element) {
        topOfStack++;
        elements.add(element);
    }
    
    public int pop() throws PoppedWhenEmpty{
        if(topOfStack == 0) {
            throw new PoppedWhenEmpty();
        }
        int element = elements.get(--topOfStack);
        elements.remove(topOfStack);
        return element;
    }
    
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;[ 9. 변경하기 쉬운 클래스 ]&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요구사항은 수시로 변하기 때문에, 변경하기 쉬운 클래스를 만드는 것이 중요하다. 변경하기 쉬운 클래스는 기본적으로 단일 책임 원칙을 지켜야 한다. 또한 구현체 보다는 추상체에 의존하여야 한다. 그리고 결국 핵심은 다형성이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이와 관련된 자세한 예제를 참고하고 싶다면 p186과 p189에서 참고하도록 하자.&lt;/p&gt;
&lt;pre id=&quot;code_1619251027112&quot; class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;abstract public class SQL {
    public SQL(String table, Column[] columns)
    abstract public String generate();
}

public class CreateSQL extends SQL {
    public CreateSQL(String table, Column[] columns)
    @Override public String generate()
}

public class SelectSQL extends SQL {
    public SelectSQL(String table, Column[] columns)
    @Override public String generate()
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;[ 10. 설계 품질을 높여주는 4가지 규칙 ]&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;모든 테스트를 실행하라: 테스트가 쉬운 코드를 작성하다 보면 SRP를 준수하고, 더 낮은 결합도를 갖는 설계를 얻을 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;중복을 제거하라: 깔끔한 시스템을 만들기 위해 단 몇 줄이라도 중복을 제거해야 한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;프로그래머의 의도를 표현하라: 좋은 이름, 작은 클래스와 메소드의 크기, 표준 명칭, 단위 테스트 작성 등을 통해 이를 달성할 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;클래스와 메소드의 수를 최소로 줄여라: 클래스와 메소드를 작게 유지함으로써 시스템 크기 역시 작게 유지할 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;2~4는 리팩토링 과정에 해당한다. 2~4의 작업은 모든 테스트케이스를 작성한 후에 코드와 클래스를 정리하기 때문에 안전하다. (p216 참고)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;[ 11. 변경하기 쉬운 클래스 ]&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시로 작성된 클린코드(p247) 역시 바로 나온 것이 아니고, 반복적인 정리 끝에 나온 코드라고 한다. 결국 좋은 코드를 만들기 위해서는 많은 시간과 노력이 필요한 것이다. 앞으로 좋은 코드를 작성하기 위해 시간과 노력을 투자하는 것을 아끼지 말도록 하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;[ 12. 디미터 법칙 ]&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;디미터의 법칙은 어떤 모듈이 호출하는 객체의 속사정을 몰라야 한다는 것이다. 그렇기에&amp;nbsp;&lt;/span&gt;&lt;span&gt;객체는 자료를 숨기고 함수를 공개해야 한다. 만약 자료를 그대로 노출하면 내부 구조가 드러나 결합도가 높아지게 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;다음의 코드는 지나치게 객체의 속사정에 깊이 관여하고 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1618364536898&quot; class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;final String outputDir = FileManager.getInstance().getOptions().getModule().getAbsolutePath();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 코드는 다음과 같이 나누는 것이 좋다.&lt;/p&gt;
&lt;pre id=&quot;code_1618364678175&quot; class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;Options options = ctxt.getOptions();
File scratchDir = opts.getScratchDir();
final String outputDir = scratchDir.getAbsolutePath();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 만약 위의 클래스들인 자료 구조라면 괜찮다. 하지만 객체라면 내부 구조를 숨겨야 하므로 한번 더 수정을 해주어야 한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부 코드를 자세히 살펴보니 위의 코드로 절대 경로를 얻는 이유는 임시 파일을 생성하기 위해서다. 그렇기에 우리는 위의 코드를 다음과 같이 임시 파일을 생성하라는 메세지를 보내는 것이 좋다.&lt;/p&gt;
&lt;pre id=&quot;code_1618365060103&quot; class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;BufferedOutputStream bos = ctxt.createScartchFileStream(classFileName);&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;</description>
      <category>기록/JAVA</category>
      <category>cleancode</category>
      <author>최동훈1</author>
      <guid isPermaLink="true">https://ulsanboy.tistory.com/171</guid>
      <comments>https://ulsanboy.tistory.com/171#entry171comment</comments>
      <pubDate>Sat, 27 Jan 2024 00:24:25 +0900</pubDate>
    </item>
  </channel>
</rss>