<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>anak_2.log</title>
        <link>https://velog.io/</link>
        <description>do programming yourself</description>
        <lastBuildDate>Wed, 25 Sep 2024 02:13:15 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>anak_2.log</title>
            <url>https://velog.velcdn.com/images/anak_2/profile/54aed347-c4c1-4952-9a6e-0c167fb65d45/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. anak_2.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/anak_2" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[싱글 스레드의 강력함, IO Multiplexing]]></title>
            <link>https://velog.io/@anak_2/%EC%8B%B1%EA%B8%80-%EC%8A%A4%EB%A0%88%EB%93%9C%EC%9D%98-%EA%B0%95%EB%A0%A5%ED%95%A8-IO-Multiplexing</link>
            <guid>https://velog.io/@anak_2/%EC%8B%B1%EA%B8%80-%EC%8A%A4%EB%A0%88%EB%93%9C%EC%9D%98-%EA%B0%95%EB%A0%A5%ED%95%A8-IO-Multiplexing</guid>
            <pubDate>Wed, 25 Sep 2024 02:13:15 GMT</pubDate>
            <description><![CDATA[<h4 id="📚-글을-쓰게-된-이유">📚 글을 쓰게 된 이유</h4>
<p> 최근 Redis나 Node.js의 Event Loop 그리고 톰캣의 NIO 등, 최신 기술인데 싱글 스레드로 작동하는 것이 신기했다. 아무리 멀티 스레드의 Context Switching 같은 오버헤드가 크다 해도 요즘 발달한 CPU 스케줄링을 잘 활용하여 멀티 스레드가 유리하다 생각했기 때문이다.
 그리고 조금 찾아보니 IO Multiplexing 기술의 강력함에 대해 흥미를 가져 글을 정리하게 됐다.</p>
<p> 간단하게 한 줄 요약하자면, IO 같은 무거운 작업은 전문가가 맡도록 하자!</p>
<h3 id="하드웨어에서-multiplexer-mux-demultiplexer-demux">하드웨어에서 Multiplexer (Mux), Demultiplexer (Demux)</h3>
<p>멀티플렉서라고 이미 하드웨어에서 여러 채널의 정보를 하나의 장치가 처리하도록 만든 기술이 있다. 이 이름을 따서 IO Multiplexing 기술이라 부른다고 한다.</p>
<ul>
<li>아날로그 또는 디지털 신호를 하나의 채널에서 수신해서 여러 출력선 중 하나로 전달</li>
</ul>
<p><img src="https://velog.velcdn.com/images/anak_2/post/8618ee90-91be-41d6-9b7b-ab8a69ddc2bc/image.gif" alt=""></p>
<ul>
<li><p>장점</p>
<ul>
<li><p>비용 절감</p>
<p>  하나의 연결마다 연결선이 필요한 것이 아닌 여러 연결을 하나의 채널에서 처리할 수 있으므로 비용이 효율적이다</p>
</li>
</ul>
</li>
<li><p>이를 응용하면 여러 신호를 관리하는 <strong>Bus 구조</strong>가 있다</p>
</li>
</ul>
<h3 id="socket-이란---everything-is-a-file">Socket 이란? - “Everything Is a File”</h3>
<ul>
<li>Linux/Unix 에선 Socket도 하나의 FD 인터페이스로 관리된다고 하여 나온 말</li>
<li>Low Level File Handling 기반으로 Socket 기반 데이터 송수신이 가능함</li>
<li>즉, <strong>I/O 작업은</strong> Server 내에서 읽기/쓰기 뿐만 아니라 서버-클라이언트 간 네트워크 통신에도 적용되는 개념이다.</li>
</ul>
<h2 id="linux-계열의-multiplexing-기법">Linux 계열의 Multiplexing 기법</h2>
<h3 id="1-기본-개념">1. 기본 개념</h3>
<ul>
<li>IO 작업은 user space에서 직접 수행할 수 없기 때문에 user process가 kernel에 IO 작업을 “요청” 하고 “응답”을 받는 구조</li>
<li>응답을 어떤 순서로 받는지 (Synchronous , Asynchronous)
어떤 타이밍에 받는지 (Blocking , Non-blocking) 에 따라 여러 모델로 분류</li>
</ul>
<h3 id="synchronous-동기">Synchronous, 동기</h3>
<ul>
<li>모든 IO 요청, 응답이 순서가 있다 → 작업의 순서를 보장</li>
<li>일련의 Pipeline을 준수하는 구조에서 효율적</li>
</ul>
<p><img src="https://velog.velcdn.com/images/anak_2/post/fe3ab8e7-0ede-498d-8c01-68ed79341830/image.png" alt=""></p>
<h3 id="asynchronous-비동기">Asynchronous, 비동기</h3>
<ul>
<li>kernel에 IO 작업을 요청해두고 다른 작업 처리 가능 → 작업의 순서 보장하지 않음</li>
<li>작업 완료를 kernel space에서 통보해줌</li>
<li>각 작업들이 독립적, 작업 별 지연이 큰 경우 효율적</li>
</ul>
<p><img src="https://velog.velcdn.com/images/anak_2/post/6d8b7448-d3ba-4ff7-ba70-c4ea1105e80d/image.png" alt=""></p>
<h3 id="blocking-블로킹">Blocking, 블로킹</h3>
<ul>
<li>요청한 작업이 끝나는 것을 기다리다가 응답 결과를 반환받음</li>
<li>작업을 기다림</li>
</ul>
<h3 id="non-blocking-논-블로킹">Non-Blocking, 논 블로킹</h3>
<ul>
<li>작업 요청 이후 결과를 필요할 때 전달받음</li>
<li>작업을 기다리지 않음</li>
<li>중간중간 상태 확인 가능 (Polling)</li>
</ul>
<h3 id="sync-async-block-non-block-혼동">Sync, Async, Block, Non-Block 혼동</h3>
<ul>
<li>어떤 작업을 기다리느라 Blocking된 동안 다른 작업이 끼어들 수 있으면 Asynchronous</li>
</ul>
<h2 id="io-모델-종류">IO 모델 종류</h2>
<p><img src="https://velog.velcdn.com/images/anak_2/post/b2a52efe-2db4-4a2c-8e20-598b319516b6/image.png" alt=""></p>
<ul>
<li>위에 IO 멀티플렉싱이 Asnyc - Blocking 으로 소개되지만, 구현 방식에 따라 구분이 달라질 수 있다.</li>
</ul>
<h3 id="synchronous-blocking-io">Synchronous Blocking IO</h3>
<ul>
<li>가장 흔한 IO 모델, user space에서 process가 kernel에게 IO를 요청 (system call)한 후, kernel의 작업 결과 반환까지 중단된 채 대기<ul>
<li>이때 user space의 process는 CPU를 점유하지 않은 채 대기</li>
</ul>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/anak_2/post/63ea51ef-a669-4ef9-ab0d-e75ab8a6a5bd/image.png" alt=""></p>
<h3 id="synchronous-non-blocking-io">Synchronous Non-Blocking IO</h3>
<ul>
<li>Non-block 동작을 위해 socket 생성 시 <code>O_NONBLOCK</code> 옵션을 줘서 구성</li>
<li>socket으로 IO system call을 하게 되면 block되는 것이 아닌 <strong>즉시 결과를 반환받음</strong><ul>
<li>읽을 데이터가 없으면 -1</li>
<li>일반적으로 EAGAIN , EWOULDBLOCK</li>
<li>Synchronous 동작이기에 기다리지는 않지만, 처리할 소켓 상태를 계속 물어보는 Busy Waiting (폴링 방식)</li>
</ul>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/anak_2/post/6e6736e6-e2ac-4155-8f72-a69450ffe3f5/image.png" alt=""></p>
<ul>
<li><p>user process는 여전히 IO 완료만 기다리며 context switching만 빈번하게 일어남</p>
</li>
<li><p>Synchronous 모델에서 여러 작업을 동시에 처리하려면 멀티쓰레드로 동작해야 한다. 하지만 IPC나 동기화 (Semaphore, Mutex 등)을 고려해야 하기 때문에 복잡함</p>
</li>
<li><p>이 때문에 위 방식보다 <strong>Multiplexing , 다중화 기법</strong>이 각광받음❗</p>
</li>
</ul>
<h3 id="io-multiplexing-멀티플렉싱-다중화">IO Multiplexing (멀티플렉싱, 다중화)</h3>
<ul>
<li><p>&quot;파일&quot;이란 유저 공간에서 커널 공간에 진입하는 인터페이스(다리) 역할</p>
</li>
<li><p>Multiplexing (다중화) : 여러 개의 신호나 데이터 스트림을 하나의 채널에서 관리하는 하드웨어 기법에서 영감을 받음 (저비용 고효율) </p>
<ul>
<li>즉, IO Multiplexing이란 소프트웨어에서 “<strong>한 프로세스가 여러 파일(File)을 관리</strong>” 하는 기법</li>
</ul>
</li>
<li><p>Server-Client 구조에서 server에서 여러 socket을 관리하여 클라이언트가 접근할 수 있게 구성된다. <strong>socket</strong> 또한 IP와 PORT 메타 정보를 가지는 파일</p>
<ul>
<li>이러한 socket을 기존엔 클라이언트가 요청을 하면 서버가 쓰레드를 할당해서 처리 -&gt; 클라이언트 요청마다 쓰레드를 할당하는 비효율적인 쓰레드 사용, </li>
</ul>
</li>
<li><p style= "color: red">IO Multiplexing 기술은 하나의 스레드가 소켓의 FD를 감시하고 관리해주고 바로 처리할 수 있는 상태만 골라준다.</p>

</li>
</ul>
<p><img src="https://velog.velcdn.com/images/anak_2/post/002bbf97-98e0-447f-8d91-17268d8ca17f/image.png" alt=""></p>
<ul>
<li>어떤 상태로 대기하냐에 따라 select, poll, epoll (linux), kqueue (bsd), iocp (windows) 기법이 있다</li>
</ul>
<p><strong>Asynchronous Blocking IO 모델</strong></p>
<ul>
<li>Block? Non-Block 아닌가?<ul>
<li>처음 Read()의 응답이 즉각적으로 미완료 상태를 반환하여 Non-blocking socket 동작을 보여주지만, kernel이 미완료 응답을 반환하면 user process에서 FD가 준비가 될 때까지  대기하므로 Application 레벨에서 Blocking</li>
</ul>
</li>
</ul>
<p>** 정리하자면, IO Multiplexing 기법으로 여러 IO 작업을 따로 맡겨서 관리할 수 있다.**</p>
<p>출처: <a href="https://notes.shichao.io/unp/ch6/">https://notes.shichao.io/unp/ch6/</a></p>
<h3 id="select">select()</h3>
<ul>
<li><p>fd_set을 통해 최대 1024개의 FD를 관리</p>
</li>
<li><p>특정 FD가 사용 가능한 지 확인하기 위해 모든 FD를 루프돌아야함</p>
</li>
<li><p>select 함수가 동기인지 비동기인지 검사하는 간단한 TCP 서버 코드</p>
<pre><code class="language-c">  #include &lt;stdio.h&gt;
  #include &lt;stdlib.h&gt;
  #include &lt;string.h&gt;
  #include &lt;unistd.h&gt;
  #include &lt;arpa/inet.h&gt;
  #include &lt;sys/socket.h&gt;
  #include &lt;sys/time.h&gt;
  #include &lt;sys/select.h&gt;
  #include &lt;time.h&gt;

  #define BUF_SIZE 100

  // 간단한 TCP 서버를 구현한 C 프로그램
  // @function 동시에 여러 클라이언트와 통신할 수 있고,
  // 받은 데이터를 다시 클라이언트에게 돌려주는(echo) 기능 수행
  int main(int argc, char *argv[])
  {
      int serv_sock, clnt_sock;                // fd : file descriptor
      struct sockaddr_in serv_addr, clnt_addr; // server and client address
      struct timeval timeout;
      fd_set reads, cpy_reads; // collection of file descriptor
      int fd_max, fd_num;
      socklen_t addr_size; // size of client socket
      int i, str_len;

      char buf[BUF_SIZE];

      // num of arg should 2
      if (argc != 2)
      {
          printf(&quot;Usage : ./program_name &lt;port&gt;\n&quot;);
          exit(1);
      }

      // make server socket (IPv4)
      serv_sock = socket(PF_INET, SOCK_STREAM, 0);
      if (serv_sock == -1) // if making socket fail
      {
          perror(&quot;error : failed socket()&quot;);
      }

      // 서버 주소 설정 및 소켓 바인딩
      memset(&amp;serv_addr, 0, sizeof(serv_addr));  // 메모리 초기화
      serv_addr.sin_family = AF_INET;            // 주소 체계 저장
      serv_addr.sin_port = htons(atoi(argv[1])); // 인자로 받은 port 번호

      // 소켓에 주소 할당
      // sockaddr* : sockaddr_in / sockaddr_un 이든 형변환
      if (bind(serv_sock, (struct sockaddr *)&amp;serv_addr, sizeof(serv_addr)) == -1)
      {
          perror(&quot;error : failed bind()&quot;);
          return 0;
      }

      // 연결 대기 및 큐 생성
      if (listen(serv_sock, 5) == -1)
      { // 클라이언트 연결 대기 &amp; 요청 queue에 저장
          perror(&quot;error : failed listen()&quot;);
          return 0;
      }

      // select I/O
      FD_ZERO(&amp;reads);           // fd_set 구조체를 0으로 초기화
      FD_SET(serv_sock, &amp;reads); // 서버 소켓을 fd_set에 추가하여 select() 함수에서 감시할 수 있도록 설정
      fd_max = serv_sock;        // 현재 열려있는 fd 중 가장 큰 값 저장

      // 클라이언트 연결 처리
      while (1)
      {
          // 이전 상태 저장
          cpy_reads = reads;
          timeout.tv_sec = 5;
          timeout.tv_usec = 50000;

          // 시간 측정을 위해 현재 시간을 기록
          printf(&quot;Before select: %ld seconds\n&quot;, time(NULL));

          // 소켓에 입력이 있는지 감시
          // select(nfds, readfds, write, err, timeout)
          // @param nfds 감시할 fd 최대값 + 1
          // @param &amp;cpy_reads 감시할 fd_set
          // @param &amp;timeout 타임아웃 설정
          if ((fd_num = select(fd_max + 1, &amp;cpy_reads, 0, 0, &amp;timeout)) == -1)
          {
              printf(&quot;fd_num : %d\n&quot;, fd_num);
              perror(&quot;select() error&quot;);
              break;
          }

          // select 함수가 끝난 후의 시간 출력
          printf(&quot;After select: %ld seconds\n&quot;, time(NULL));

          // bit 값이 1인 필드 없음 = 발견된 read data 없음
          if (fd_num == 0)
              continue;

          // 발견되면 fd 다 훑음 = O(n)
          for (i = 0; i &lt; fd_max + 1; i++)
          {
              // select가 반환한 fd 중 입력이 있는 소켓 탐색
              if (FD_ISSET(i, &amp;cpy_reads))
              {
                  if (i == serv_sock)
                  { // data 발생한 fd 찾으면
                      printf(&quot;putin serv_sock\n&quot;);
                      addr_size = sizeof(clnt_addr);

                      // queue에서 연결 요청 하나씩 꺼내서 해당 client와 server socket 연결
                      clnt_sock = accept(serv_sock, (struct sockaddr *)&amp;clnt_addr, &amp;addr_size);
                      FD_SET(clnt_sock, &amp;reads);
                      if (fd_max &lt; clnt_sock)
                          // loop 돌아야 하므로 fd 큰쪽으로 맞춤
                          fd_max = clnt_sock;
                      printf(&quot;connected client : %d \n&quot;, clnt_sock);
                  }
                  else
                  {
                      str_len = read(i, buf, BUF_SIZE); // 클라이언트로부터 데이터 수신
                      if (str_len &lt;= 0)
                      {
                          FD_CLR(i, &amp;reads);
                          close(i);
                          printf(&quot;close client : %d \n&quot;, i);
                      }
                      else
                      {
                          // client로echo 응답
                          write(i, buf, str_len);
                      }
                  }
              }
          }
      }
      close(serv_sock);
      return 0;
  }</code></pre>
</li>
</ul>
<h3 id="pselect">pselect()</h3>
<ul>
<li>timeval 구조체로 timeout을 관리하던 것과 달리 timespec 구조체로 구현되어 나노초까지 컨트롤</li>
<li>select와 달리 sigmask 인자 추가로 signal에 의한 비정상 동작을 방지</li>
</ul>
<h3 id="poll">poll()</h3>
<ul>
<li>관리 가능한 최대 FD 수가 1024에서 무한 개로 검사</li>
<li>select는 모든 FD를 루프도는 반면, poll은 실제 FD 개수만큼 루프를 돔</li>
</ul>
<h3 id="ppoll">ppoll()</h3>
<ul>
<li>select - pselect 와 마찬가지로 timeout과 signal 처리 로직 개선</li>
</ul>
<h3 id="epoll-linux">epoll (linux)</h3>
<ul>
<li>FD 관리 무한 , 앞에와 달리 FD 상태를 kernel이 관리하여 상태가 바뀐 것을 통지 해줌<ul>
<li>fd_set을 검사하기 위해 루프를 돌 필요가 없어졌다!</li>
<li>변화가 감지된 FD의 수가 아닌 FD 목록을 반환</li>
</ul>
</li>
<li>Level-Triggered<ul>
<li>이벤트 발생한 이후 계속 버퍼에 저장해서 보관</li>
</ul>
</li>
<li>Edge-Triggered<ul>
<li>이벤트 발생했을 때만 통보</li>
</ul>
</li>
</ul>
<h2 id="asynchronous-non-blocking-io-aio">Asynchronous Non-Blocking IO (AIO)</h2>
<p><img src="https://velog.velcdn.com/images/anak_2/post/c3d43e35-5e51-426b-b6a4-0771ba152fa4/image.png" alt=""></p>
<ul>
<li>굉장히 효율적인 통신을 구현할 수 있을 것 같다!! → 실제에서 잘 안보이는 이유<ul>
<li>IO 작업을 위한 thread 유지, 관리 비용 + 확장성 떨어짐 + 완료 통지 방식에 대한 문제가 있다고 한다.</li>
</ul>
</li>
</ul>
<h3 id="결론-싱글-스레드임에도-처리-속도가-빠를-수-있는-이유">결론: 싱글 스레드임에도 처리 속도가 빠를 수 있는 이유</h3>
<ul>
<li>비동기 IO</li>
<li>프로그램이 파일에게 열도록 요청 → 커널이 엑세스 권한 부여 → 글로벌 파일 테이블에 항목 생성 → 소프트웨어에 해당 파일의 위치를 제공</li>
<li>시스템에서 열려 있는 모든 파일에 대해 하나 이상의 File Descriptor가 존재하는데, 이를 관리해주는 전용 채널을 만드는 것이다.</li>
</ul>
<h3 id="네트워크-io-multiplexing-model">네트워크 IO Multiplexing model</h3>
<p><img src="https://velog.velcdn.com/images/anak_2/post/1fe5345c-1da3-45c3-811f-e32c4a97c6d6/image.png" alt=""></p>
<ul>
<li><p>이 과정은 I/O Multiplexing 방식에서, 다수의 소켓 중 하나 이상의 소켓이 읽기 가능한 상태가 되었을 때 이벤트를 받기 위한 전형적인 방식이다.
select는 다수의 소켓에 대해 어떤 것이 데이터를 읽을 준비가 되었는지 확인하는 시스템 호출이고, 데이터가 준비되면 recvfrom 호출을 통해 커널에서 애플리케이션의 버퍼로 데이터를 복사해 온다.</p>
</li>
<li><p>두 번의 블로킹이 발생하는데, 첫 번째는 select 호출에서 데이터가 준비될 때까지 대기하는 것, 두 번째는 recvfrom 호출에서 데이터를 복사하는 동안의 대기이다.</p>
</li>
<li><p>이 과정을 통해 애플리케이션은 효율적으로 네트워크 I/O 작업을 수행할 수 있게 되며, 특히 다수의 소켓에 대한 비동기 작업을 수행할 때 유용하다고 한다.</p>
</li>
</ul>
<br>

<ul>
<li>출처</li>
</ul>
<p><a href="https://ko.wikipedia.org/wiki/%EB%A9%80%ED%8B%B0%ED%94%8C%EB%A0%89%EC%84%9C#%EB%B9%84%EC%9A%A9_%EC%A0%88%EA%B0%90">https://ko.wikipedia.org/wiki/멀티플렉서#비용_절감</a></p>
<p><a href="https://blog.naver.com/n_cloudplatform/222189669084">https://blog.naver.com/n_cloudplatform/222189669084</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[자바 멀티쓰레드와 동시성 종류, 간단한 응용 코드 (기초)]]></title>
            <link>https://velog.io/@anak_2/%EC%9E%90%EB%B0%94-%EB%A9%80%ED%8B%B0%EC%93%B0%EB%A0%88%EB%93%9C%EC%99%80-%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%A2%85%EB%A5%98-%EA%B0%84%EB%8B%A8%ED%95%9C-%EC%9D%91%EC%9A%A9-%EC%BD%94%EB%93%9C-%EA%B8%B0%EC%B4%88</link>
            <guid>https://velog.io/@anak_2/%EC%9E%90%EB%B0%94-%EB%A9%80%ED%8B%B0%EC%93%B0%EB%A0%88%EB%93%9C%EC%99%80-%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%A2%85%EB%A5%98-%EA%B0%84%EB%8B%A8%ED%95%9C-%EC%9D%91%EC%9A%A9-%EC%BD%94%EB%93%9C-%EA%B8%B0%EC%B4%88</guid>
            <pubDate>Fri, 30 Aug 2024 08:06:11 GMT</pubDate>
            <description><![CDATA[<h3 id="이-글의-목적">이 글의 목적</h3>
<blockquote>
<p>📚
자바 멀티쓰레드의 동시성을 해결하는 각 기능의 심화과정을 다루기 전에 가볍게 이용해보고 테스트를 통해 사용 방식을 익혀봅니다</p>
</blockquote>
<p>자바 멀티쓰레드를 사용하면 반드시 겪는 <strong>동시성 문제</strong>가 있다.
동시성 문제는 쓰레드마다 객체나 변수를 조작할 때 서로가 최신값을 모르고 접근 &amp; 변경하기 때문에 발생하는 문제고, 주로 쓰레드마다 가지는 로컬 변수가 아닌 전역 변수나 객체의 필드에서 자주 발생하기 때문에 주의하자</p>
<h3 id="volatile">Volatile</h3>
<ul>
<li><strong>volatile</strong> 를 사용하는 이유 - <strong>Thread의 가시성 문제</strong><ul>
<li>자바 메모리 모델에서 쓰레드는 각자의 캐시를 사용한다</li>
<li>한 쓰레드가 자신의 캐시에 있는 전역 변수의 값을 변경해도 다른 쓰레드는 그 값을 모른다</li>
<li>“volatile 키워드”는 변수의 값을 메인 메모리에 직접 쓰고 읽도록 강제하여, 모든 쓰레드가 항상 최신 값을 보도록 보장</li>
</ul>
</li>
</ul>
<pre><code class="language-java">public class NonVolatileExample {

    private boolean stopRequested = false;
    private volatile boolean stopVolatileRequested = false;
    Logger logger = Logger.getLogger(NonVolatileExample.class.getName());

    @Test
    void testNonVolatile() throws InterruptedException {
        Thread backgroundThread = new Thread(() -&gt; {
            logger.log(INFO, &quot;Background Thread: &quot; + Thread.currentThread().getName() + &quot;, ID: &quot; + Thread.currentThread().getId());
            while (!stopRequested) {
                // 무한 루프 - stopRequested가 true가 될 때까지 반복
            }
            logger.log(INFO, &quot;Thread stopped.&quot;);
        });

        backgroundThread.start();

        // 메인 스레드가 1초 동안 대기 후 stopRequested를 true로 설정
        Thread.sleep(1000);
        logger.log(INFO, &quot;Main thread set stopRequested to true.&quot;);
        stopRequested = true;

        logger.log(INFO, &quot;Main Thread: &quot; + Thread.currentThread().getName() + &quot;, ID: &quot; + Thread.currentThread().getId());
        Thread.sleep(1000);
    }

    @Test
    void testVolatile() throws InterruptedException {
        Thread backgroundThread = new Thread(() -&gt; {
            while (!stopVolatileRequested) {
                // 무한 루프 - stopRequested가 true가 될 때까지 반복
            }
            logger.log(INFO, &quot;Thread stopped.&quot;);
        });

        backgroundThread.start();

        // 메인 스레드가 1초 동안 대기 후 stopRequested를 true로 설정
        Thread.sleep(1000);
        stopVolatileRequested = true;

        Thread.sleep(1000);
    }
}</code></pre>
<ul>
<li>위 코드에서 testNonVolatile() 의 경우 다른 쓰레드에서 stopRequested 플래그를 true로 업데이트했지만, 쓰레드의 캐시 이슈로 인해 무한 루프에서 빠져나오지 못하는 경우가 발생한다</li>
</ul>
<h3 id="동시성-v1---race-condition-방지">동시성 V1 - Race Condition 방지</h3>
<p>쓰레드가 서로 하나의 값에 접근해서 바꾸는 상태를 경합 상태 (Race Condition)라 한다. Race Condition은 어떻게 해결할 수 있을까?</p>
<ul>
<li><p><strong>volatile</strong> 로 가능할까?</p>
</li>
<li><p><strong>synchronized</strong> 은 무엇인가?</p>
<ul>
<li><strong>모니터 락 (Monitor Lock)</strong><ul>
<li>여러 스레드가 동일한 자원 (메서드 또는 코드 블록)에 접근할 때 한 번에 하나의 스레드만 접근할 수 있도록 <strong>모니터 락</strong>을 사용하여 보장</li>
</ul>
</li>
<li><strong>가시성 보장</strong><ul>
<li>synchronized 블록을 탈출할 때, 쓰레드의 모든 변경 사항이 메모리에 커밋된다</li>
</ul>
</li>
</ul>
<ul>
<li><p><strong>Atomic</strong> 은 무엇인가?</p>
<ul>
<li><p><strong>원자성 보장</strong></p>
<ul>
<li><p>Atomic 클래스는 원자성 보장하는 메서드 제공</p>
</li>
<li><p>Atomic 클래스의 변수는 <strong>volatile</strong>로 선언
  <img src="https://velog.velcdn.com/images/anak_2/post/b64cb39a-7f44-4a41-9248-d46d5a7a7b8b/image.png" alt=""></p>
<p> <img src="https://velog.velcdn.com/images/anak_2/post/730d0597-63d4-4a4d-bcd0-2c717ef1b00d/image.png" alt=""></p>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<h3 id="race-condition-테스트-코드">Race Condition 테스트 코드</h3>
<p>각 방식을 ExecutorService를 통해 동시에 실행했을 때 Counter의 필드가 예상값이 나오는지 테스트해보자</p>
<pre><code class="language-java">public class ExecutorTest {

    private ExecutorService executorService;
    private final Counter counter = new Counter();

    @BeforeEach
    void init() {
        executorService = Executors.newFixedThreadPool(5);
        counter.init();
    }

    @Test
    void multiFail() {
        for(int i = 0; i &lt; 1000; i++){
            executorService.execute(counter::increment);
        }

        assertThat(counter.count).isNotEqualTo(1000);
    }

    @Test
    void syncTest(){
        for(int i = 0; i &lt; 1000; i++){
            executorService.execute(counter::syncIncrement);
        }
        shutdownAndAwaitTermination();
        assertThat(counter.count).isEqualTo(1000);
    }

    @Test
    void volatileTest() {
        for(int i = 0; i &lt; 1000; i++){
            executorService.execute(counter::volatileIncrement);
        }
        shutdownAndAwaitTermination();
        assertThat(counter.volatileCount).isEqualTo(1000);
    }

    @Test
    void atomicTest() {
        for(int i = 0; i &lt; 1000; i++){
            executorService.execute(counter::atomicIncrement);
        }
        shutdownAndAwaitTermination();
        assertThat(counter.atomicInteger.get()).isEqualTo(1000);
    }

    // ⚠️ executorService를 shutDown() 으로 종료해도 
    // 이미 할당받은 task를 마무리를 해야 정상적으로 예상한 결과가 나온다
    // shutdownAndAwaitTermination() 으로 soft landing 구현
    private void shutdownAndAwaitTermination() {
        executorService.shutdown();
        try {
            if(!executorService.awaitTermination(60, TimeUnit.SECONDS)) {
                executorService.shutdownNow();
                if (!executorService.awaitTermination(60, TimeUnit.SECONDS))
                    System.err.println(&quot;Pool did not terminate&quot;);
            }
        } catch (Exception e){
            e.printStackTrace();
        }
    }

    private static class Counter {
        private int count = 0;
        private volatile int volatileCount = 0;
        private final AtomicInteger atomicInteger = new AtomicInteger(0);

        public void init() {
            count = 0;
            volatileCount = 0;
            atomicInteger.set(0);
        }
        public void increment(){
            count++;
        }

        public void volatileIncrement(){
            volatileCount++;
        }

        public synchronized void syncIncrement(){
            count++;
        }

        public void atomicIncrement(){
            atomicInteger.incrementAndGet();
        }
    }
}
</code></pre>
<ul>
<li><p>synchronized 방식과 Atomic 방식은 모두 예상한 결과가 나온다.</p>
</li>
<li><p>하지만 <strong>volatile 방식은 문제가 발생했다</strong></p>
<ul>
<li><p><strong>volatile 키워드가 쓰레드간 값을 접근할 때 메모리에서 읽는 것은 보장해주지만 _원자성_을 보장하진 않는다.</strong></p>
<p><img src="https://velog.velcdn.com/images/anak_2/post/e28389a8-6f42-4c1f-a5c1-2ebad5cc49b3/image.png" alt=""></p>
<ul>
<li>intellij 에선 친절히 경고를 날려준다</li>
</ul>
</li>
</ul>
</li>
<li><p>언제 volatile을 써야하나?</p>
<ul>
<li>volatile은 간단히 가시성을 보장하기 때문에 다른 고비용의 동기화보다 저렴하다.</li>
<li>간단한 플래그 변수, 싱글톤 패턴, 동시에 쓰기 작업이 발생하는 Race Condition 이 적을 때</li>
</ul>
</li>
<li><p>AtomicInteger나 synchronized를 사용할 때 <code>volatile</code> 키워드를 사용하지 않아도 변수의 최신 값을 보장받는 이유</p>
<ul>
<li>JVM의 동기화 보장 덕분</li>
<li>synchronized를 사용하면 캐시 값을 사용하지 않고 메모리의 최신 값 참조</li>
<li>AtomicInteger를 사용하면 내부적으로 volatile 변수와 CAS (Compare-And-Swap) 연산을 사용하여 원자적 연산을 수행</li>
</ul>
</li>
<li><p><strong>Volatile 증가 수정 &amp; 테스트</strong></p>
<ul>
<li>아래와 같이 수정하면 예상대로 count가 출력된다</li>
</ul>
</li>
</ul>
<pre><code class="language-java">@Test
void volatileSyncTest() {
      for(int i = 0; i &lt; 10000; i++){
          executorService.execute(counter::volatileSyncIncrement);
      }
      shutdownAndAwaitTermination();
      assertThat(counter.volatileCount).isEqualTo(expectedValue);
}

// Counter.class
public void volatileSyncIncrement(){
    synchronized (this) {
        volatileCount++;
    }
}</code></pre>
<h3 id="동시성-v2---lock을-이용">동시성 V2 - Lock을 이용</h3>
<ul>
<li>ReentrantLock<ul>
<li>명시적 Lock을 이용</li>
</ul>
</li>
<li>lock() 기능 테스트</li>
<li>tryLock() 기능 테스트</li>
<li>Condition 기능 테스트 </li>
</ul>
<pre><code class="language-java">public class LockTest {

    private ExecutorService executorService;
    private final Counter counter = new Counter();
    private final int expectedValue = 10000;
    private final int forLoopCount = 10000;

    @BeforeEach
    void init() {
        executorService = Executors.newFixedThreadPool(5);
        counter.init();
    }

    @Test
    @DisplayName(&quot;일반 Reentrant Lock&quot;)
    void lockTest() {
        for (int i = 0; i &lt; forLoopCount; i++) {
            executorService.execute(counter::incrementWithLock);
        }
        shutdownAndAwaitTermination();

        assertThat(counter.getCount()).isEqualTo(expectedValue);
    }

    @Test
    @DisplayName(&quot;tryLock() 테스트&quot;)
    void tryLockTest() {
        for (int i = 0; i &lt; forLoopCount; i++) {
            executorService.execute(() -&gt; {
                boolean success = counter.tryIncrementWithLock();
                if (!success) {
                    // 락을 획득하지 못했을 때의 처리
                    System.out.println(&quot;Failed to increment due to lock unavailability.&quot;);
                }
            });
        }
        shutdownAndAwaitTermination();

        System.out.println(&quot;Final count: &quot; + counter.getCount());
        assertThat(counter.getCount()).isLessThanOrEqualTo(expectedValue);
    }

    @Test
    @DisplayName(&quot;멀티쓰레드에서 각 쓰레드가 짝수일 때와 홀수일 때 count를 증가시키도록 Condition 설정&quot;)
    void conditionLockTest() {
        for (int i = 0; i &lt; 1000; i++) {
            executorService.execute(counter::incrementIfOdd);
            executorService.execute(counter::incrementIfEven);
        }

        shutdownAndAwaitTermination();

        assertThat(counter.getCount()).isEqualTo(2000);
    }

    private void shutdownAndAwaitTermination() {
        executorService.shutdown();
        try {
            if (!executorService.awaitTermination(60, TimeUnit.SECONDS)) {
                executorService.shutdownNow();
                if (!executorService.awaitTermination(60, TimeUnit.SECONDS))
                    System.err.println(&quot;Pool did not terminate&quot;);
            }
        } catch (InterruptedException e) {
            executorService.shutdownNow();
            Thread.currentThread().interrupt();
        }
    }

    public static class Counter {
        private int count = 0;
        private final ReentrantLock lock = new ReentrantLock();
        private final Condition lockAvailable = lock.newCondition();

        public void init() {
            count = 0;
        }

        public void incrementWithLock() {
            lock.lock();
            try {
                count++;
            } finally {
                lock.unlock();
            }
        }

        public boolean tryIncrementWithLock() {
            boolean isLocked = false;
            try {
                isLocked = lock.tryLock();
                if (isLocked) {
                    count++;
                    return true;  // 작업 성공
                } else {
                    // 다른 작업 수행 가능
                    System.out.println(&quot;락 획득 실패. 다른 작업 진행&quot;);
                    return false;  // 작업 실패
                }
            } finally {
                if (isLocked) {
                    lock.unlock();  // 락이 성공적으로 획득된 경우에만 해제
                }
            }
        }

        public void incrementIfEven() {
            String threadName = Thread.currentThread().getName();
            try {
                lock.lock();
                if (count % 2 != 0) {
                    System.out.println(threadName+&quot;: count가 짝수이길 기다림&quot;);
                    // 잠금 상태가 아닌데 await method를 호출하면 IllegalMonitorStateException 발생
                    boolean timeOut = lockAvailable.await(1, TimeUnit.SECONDS);

                    if(timeOut) {
                        System.out.println(threadName+&quot;: 짝수 대기 시간 만료&quot;);
                    }
                }

                count++;
                lockAvailable.signal();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            } finally {
                if(lock.isHeldByCurrentThread()){
                    lock.unlock();
                }
            }
        }

        public void incrementIfOdd() {
            String threadName = Thread.currentThread().getName();
            try {
                lock.lock();
                if (count % 2 == 0) {  // count가 짝수일 때만 홀수로 만듦
                    System.out.println(threadName+&quot;: count가 홀수이길 기다림&quot;);
                    boolean timeOut = lockAvailable.await(1, TimeUnit.SECONDS);

                    if(timeOut) {
                        System.out.println(threadName+&quot;: 홀수 대기 시간 만료&quot;);
                    }
                }

                count++;
                lockAvailable.signal();  // 다른 대기 중인 스레드를 깨웁니다.
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            } finally {
                lock.unlock();  // 락을 해제합니다.
            }
        }

        public int getCount() {
            return count;
        }
    }
}</code></pre>
<ul>
<li><p><strong>ReentrantLock</strong>의 장점</p>
<ul>
<li><p>재진입이 가능한 Lock</p>
</li>
<li><p><strong>명시적 Lock</strong>을 이용해 직관적인 코드 구조</p>
</li>
<li><p><strong>tryLock()</strong> 을 이용가능</p>
<ul>
<li><p>쓰레드가 락 획득에 성공하지 못해도 대기하지 않고 다른 작업을 수행할 수 있다 (비동기로 개선 가능)</p>
<p><img src="https://velog.velcdn.com/images/anak_2/post/4a364a95-1c4b-406d-a29a-bf656849f789/image.png" alt=""></p>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<pre><code>    - 1000번 반복 중 락 4번 획득 실패해서 최종적으로 996 출력 확인
- **조건 변수 (Condition) ** 지원
    - Lock의 **newCondition()** 으로 Condition 획득 가능
    - Condition 객체를 이용해 Object 클래스의 wait(), notify(), notifyAll() 메서드보다 더 유연한 동기화 매커니즘 지원
    - 특정 조건을 **await()** 으로 기다릴 수 있다. 
        - await()을 호출하면 획득했던 lock 을 반환하고 ready state (대기) 상태로 돌아간다. (Blocking)
        - 주의⚠️

            **lock**을 획득하지 못한 Thread가 **await()** 을 호출하면 **IllegalMonitorStateException** 가 발생한다

    - **singal()** 으로 대기 중인 다른 스레드 하나를 깨워서 이어서 처리할 수 있도록 한다
    - **singalAll()** 으로 대기 중인 다른 스레드 모두를 깨워서 이어서 처리할 수 있도록 한다</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[ConcurrentModificationException과 for문 성능 개선]]></title>
            <link>https://velog.io/@anak_2/ConcurrentModificationException%EA%B3%BC-for%EB%AC%B8-%EC%84%B1%EB%8A%A5-%EA%B0%9C%EC%84%A0</link>
            <guid>https://velog.io/@anak_2/ConcurrentModificationException%EA%B3%BC-for%EB%AC%B8-%EC%84%B1%EB%8A%A5-%EA%B0%9C%EC%84%A0</guid>
            <pubDate>Thu, 29 Aug 2024 07:41:01 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><strong>컬렉션 객체를 순회하면서 특정 값을 삭제하고 싶을 때 어떻게 처리할까</strong></p>
</blockquote>
<h3 id="동시성-문제">동시성 문제?</h3>
<ul>
<li>컬렉션 객체에서 특정 값을 삭제하고 싶을 때 발생했던 런타임 문제</li>
<li><strong>향상된 for문</strong><ul>
<li><strong>ConcurrentModificationException</strong> 문제가 발생한다</li>
</ul>
</li>
</ul>
<p>왜 동시성 문제일까? class 파일을 분석해보자</p>
<ul>
<li><strong>향상된 for문 문법을 포함하는 class 파일</strong>
<img src="https://velog.velcdn.com/images/anak_2/post/599c5158-73de-4d8d-b5d1-345b94398f06/image.png" alt=""></li>
</ul>
<p>  위 코드에서 향상된 for문이 Iterator 객체로 호출되어 while에서 hasNext() 로 바뀐 로직을 볼 수 있다</p>
<p>  &amp;nbsp그런데 remove를 호출하는 부분에서 루프를 돌고있는 객체에서 삭제하는 부분이 있다. 이 부분이 문제가 되어 동시성 문제가 발생했을 것이라 예측하고, Iterator와 remove(obj) 부분을 더 살표보자<br>
  <strong>[iterator] 를 살펴보자</strong></p>
<ul>
<li><p>ArrayList에서 iterator()는 내부클래스인 <strong>Itr</strong>를 반환한다</p>
</li>
<li><p><strong>Itr 클래스</strong>는 클래스 변수로 리스트의 데이터 변경 여부를 체크하고 있다</p>
<ul>
<li><p>Itr는 ArrayList 내에 선언된 내부 클래스이다</p>
<pre><code class="language-java"> // ArrayList.class
 public Iterator&lt;E&gt; iterator() {
     return new Itr();
 }</code></pre>
<pre><code class="language-java"> private class Itr implements Iterator&lt;E&gt; {
     int cursor; // next를 호출했을 때 반환할 element의 index
     int lastRet = -1; // 마지막 element의 index
     int exepctedModCount = modCount; // List가 수정된 횟수

     Itr() {}
     ...
 }</code></pre>
</li>
</ul>
<ul>
<li><strong>Iterator의 remove는 ArrayList에 구현되어있고, fastRemove를 호출해서 삭제한다</strong></li>
<li><strong>fastRemove</strong>는 modCount를 변형시킨다</li>
</ul>
<p><img src="https://velog.velcdn.com/images/anak_2/post/d09713f7-7bfb-4310-9de2-a1b9f188ec80/image.png" alt="">
<img src="https://velog.velcdn.com/images/anak_2/post/91a6cfe3-9390-449d-aecf-a3ab5faa77e4/image.png" alt=""></p>
</li>
</ul>
<pre><code>- Iterator의 next() 메서드로 다음 element를 가져오려고 시도할 때 **modCount 변환 여부를 체크**하여 에러가 발생한다

    ![](https://velog.velcdn.com/images/anak_2/post/aa5e0f71-c783-4f92-9da4-daf05eed31b6/image.png)</code></pre><p>checkForComodification()에서 modCount가 변한 여부를 체크한다
<img src="https://velog.velcdn.com/images/anak_2/post/5657a6ce-bbc0-4530-b760-8947f665d142/image.png" alt=""></p>
<ul>
<li>위와 같은 이유로 remove(int index) 나 remove(Object obj) 방식 대신에 iterator에 선언된 remove() 함수로 원본 Collection에 지장없이 삭제를 해야한다</li>
</ul>
<p><img src="https://velog.velcdn.com/images/anak_2/post/1801974b-c70c-4eda-8620-81cd9eafd61b/image.png" alt=""></p>
<h3 id="동시성-이슈이므로-concurrent-패키지로-해결도-가능하다">동시성 이슈이므로 Concurrent 패키지로 해결도 가능하다</h3>
<ul>
<li><p>CopyOnWriteArrayList 의 remove</p>
<p><img src="https://velog.velcdn.com/images/anak_2/post/b833f342-739d-4122-b46e-8fdc2afd60b9/image.png" alt=""></p>
</li>
</ul>
<h3 id="concurrentmodificationexception은-개발자의-실수로-발생할-수-있다-그러므로-회피가-아닌-다른-방법을-찾아보자">ConcurrentModificationException은 개발자의 실수로 발생할 수 있다. 그러므로 회피가 아닌 다른 방법을 찾아보자</h3>
<ul>
<li>순회할 객체를 불변 객체인 <code>unmodifiableList</code> 로 감싸자</li>
<li>그리고 불변객체에서 원하는 값만 추출하거나 제외해서 새로운 객체를 만드는 <strong>filter 메서드</strong>를 사용하자</li>
</ul>
<h3 id="for문의-처리-성능을-높이고-싶다면">for문의 처리 성능을 높이고 싶다면</h3>
<ul>
<li>사실 멀티쓰레드를 사용했을 때 하나의 객체를 동시에 접근하는 것은 동시성 이슈에서 자유롭지 못하다.</li>
<li>그러므로 위에서 고려한 것을 조합해서 for문과 멀티쓰레드를 2가지 방식으로 설계해봤다</li>
<li><strong>ExcutorService &amp; synchronized</strong></li>
</ul>
<pre><code class="language-java">@Test
void enhancedForLoop(){
    ExecutorService executor = Executors.newFixedThreadPool(4);
    List&lt;String&gt; copyTarget = new CopyOnWriteArrayList&lt;&gt;(targetList);

    for (String str : copyTarget) {
        executor.submit(() -&gt; {
            if (str.equals(&quot;a&quot;)) {
                synchronized (copyTarget) {
                    copyTarget.remove(str);
                }
            }
        });
    }
    executor.shutdown(); // 새로운 작업을 받아들이지 않고, 이미 실행된 작업들을 모두 실행한 후 종료
    while (!executor.isTerminated()) {
        // 모든 작업이 끝날 때까지 기다림
    }
    assertThat(copyTarget.size()).isEqualTo(300000);
}</code></pre>
<ul>
<li><strong>parallelStream</strong></li>
</ul>
<pre><code class="language-java">@Test
void enhancedUnmodifiableForLoop() {
    long startTime = System.currentTimeMillis();
    List&lt;String&gt; unmodifiableList = Collections.unmodifiableList(targetList);
    List&lt;String&gt; afterFilter = unmodifiableList.parallelStream().filter(str -&gt; {
//            System.out.println(Thread.currentThread().getName() + &quot; processed &quot; + str);
        if(str.equals(&quot;a&quot;)){
            return false;
        }
        return true;
    }).toList();
    assertThat(afterFilter.size()).isEqualTo(300000);
    long endTime = System.currentTimeMillis();
    System.out.println(&quot;Execution time: &quot; + (endTime - startTime) + &quot; ms&quot;);
}</code></pre>
<ul>
<li>테스트는 4만건의 String List에서 For문을 돌며 1만건의 String을 삭제하는 작업이었다.</li>
</ul>
<p>iterator의 remove() 로 처리한 결과는 2993ms 였고</p>
<p>parallelStream으로 처리한 결과 71ms 로</p>
<p>결과적으로 97% 성능을 향상시켰다</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Git History 관리에 유용한 git rebase]]></title>
            <link>https://velog.io/@anak_2/Git-History-%EA%B4%80%EB%A6%AC%EC%97%90-%EC%9C%A0%EC%9A%A9%ED%95%9C-git-rebase</link>
            <guid>https://velog.io/@anak_2/Git-History-%EA%B4%80%EB%A6%AC%EC%97%90-%EC%9C%A0%EC%9A%A9%ED%95%9C-git-rebase</guid>
            <pubDate>Wed, 21 Aug 2024 04:36:47 GMT</pubDate>
            <description><![CDATA[<h3 id="git-rebase">Git Rebase</h3>
<ul>
<li><p>git rebase란 무엇이며 왜 사용하는가?</p>
<ul>
<li><p>[git rebase란?]</p>
<p>프로젝트를 깃으로 관리한다면, 브랜치 가지치기와 합치는 과정은 필수이다. 그리고 현재 브랜치를 다른 브랜치로 합치는 명령어는 <code>merge</code> 와 <code>rebase</code> 가 있다.</p>
</li>
<li><p>[git rebase와 merge의 차이]</p>
<p><code>merge</code> 는 두 브랜치를 합치는 가장 쉬운 방법으로, 두 브랜치의 가장 마지막 커밋 두 개 (C3, C4) 를 공통 조상에 합치는 3-way Merge로 새로운 커밋 (C2)를 만들어 낸다.</p>
</li>
</ul>
</li>
</ul>
<blockquote>
<p><code>rebase</code> 는 두 브랜치를 합치는 다른 방법으로, 두 브랜치의 공통 커밋(Common Commit)으로 이동하고 나서, 다른 브랜치의 마지막 커밋(Another Last Commit) 으로 이동한다. 그리고 Another Last Commit 에서부터 현재 브랜치의 Common Commit 부터 현재 브랜치의 마지막 Commit까지 Diff들을 차례로 해결해나간다</p>
</blockquote>
<h3 id="git-rebase-실습">git rebase 실습</h3>
<p><strong>merge와 rebase의 차이를 실습으로 살펴보자</strong></p>
<p>이번 실습은</p>
<p>master 브랜치에서 2개의 브랜치 (branch-a, branch-b) 를 분기하고,
 master와 분기한 브랜치에서 각각 commit을 2개씩 날린 후, 
변경 내역들을 master에 합치는 과정을 진행한다.</p>
<p>각 commit은 hello.txt 파일에 강제로 Merge Conflict을 일으키게 진행했다.</p>
<p><strong>[merge]</strong></p>
<p><img src="https://velog.velcdn.com/images/anak_2/post/802102ee-d20c-4cd8-b37c-670101c77f39/image.png" alt=""></p>
<p>현재 상태를 Git 그래프로 보면 다음과 같다.</p>
<p><img src="https://velog.velcdn.com/images/anak_2/post/7f81de4f-2367-4820-9fd8-ae36005f226f/image.png" alt=""></p>
<p>이제 master 브랜치에서 branch-a 와 branch-b 를 각각 합치고 충돌도 해결하면 위와 같이 2개의 Merege 커밋이 생기는 것을 볼 수 있다</p>
<p><strong>[rebase]</strong></p>
<p><img src="https://velog.velcdn.com/images/anak_2/post/a97e4cc7-68cb-4c6d-8df4-a268903ce3d1/image.png" alt=""></p>
<p>rebase 또한 동일한 실습으로 위와 같은 환경에서 시작한다.
rebase에서는 충돌해결 과정을 자세히 보기 위해 다음과 같이 파일을 수정했다</p>
<ul>
<li>맨 처음에 “init master” 문장에서 master, branch-a, branch-b 분기</li>
<li>master 브랜치에서 <strong>각 커밋마다 같은 문장을 수정</strong>, 2번 진행</li>
<li>branch-a 브랜치에서  <strong>각 커밋마다 같은 문장을 수정</strong>, 2번 진행</li>
<li>branch-b 도 branch-a와 동일하게 진행</li>
</ul>
<p>[그림1]</p>
<p><img src="https://velog.velcdn.com/images/anak_2/post/d29890ca-3880-4707-92a5-17d4e0b03716/image.png" alt=""></p>
<p>먼저 git checkout branch-a 로 이동해서, branch-a 에서 <code>git rebase master</code> 명령어를 이용해, master와 공통 조상으로 이동 (init master) 로 이동한 뒤, master의 마지막 커밋인 “commit2 master” 에 대해 rebase를 진행한다.
만약 git bash를 사용한다면 <code>(branch-a | REBASE 1/2)</code> 라는 텍스트를 살펴볼 수 있다.
이 말은 즉, rebase하려는 master의 마지막 커밋인 “commit2 master”와 branch-a의 첫 번째 커밋인 “commit a”의 충돌이 난 것이다.
이를 해결하고 다시 <code>git rebase --continue</code> 를 진행하면</p>
<p>[그림2]</p>
<p><img src="https://velog.velcdn.com/images/anak_2/post/bc936939-d49a-4e5d-83bc-9eefba74973e/image.png" alt=""></p>
<p>다시 위와 충돌이 나면서 git bash 로 보면 <code>(branch-a | REBASE 2/2)</code> 라는 텍스트를 살펴볼 수 있을 것이다. 이는 branch-a 의 2번째 커밋과 rebase하려는 커밋의 충돌을 해결 중이라는 것이다.</p>
<p>근데 위에서 주의깊게 살펴볼 점은, branch-a 의 두 번째 커밋은 “commit1 a” 라는 내용을 지우고 “commit2 a” 라는 내용을 썼는데, 아까 충돌을 해결하는 과정에서 “commit1 a” 라는 내용을 [그림1]에서 충돌 해결하는 과정에서 다시 살려버렸다는 것이다. </p>
<p>[그림3]</p>
<p><img src="https://velog.velcdn.com/images/anak_2/post/22c02db2-ca4c-497f-b49f-b1218aeaed45/image.png" alt=""></p>
<p>그래서 위와 같이 다시 수정한 다음 <code>git rebase --continue</code> 를 진행하고 rebase를 마무리한다.</p>
<p>[그림4]</p>
<p><img src="https://velog.velcdn.com/images/anak_2/post/93c7f9e6-f371-45e1-96ef-481647d23bde/image.png" alt=""></p>
<p>위 그림과 같이 master의 마지막 커밋에 이어서 커밋이 날라간 것이 됩니다. 이게 가능한 이유는rebase는 새로운 커밋을 만들기 때문이다.</p>
<p>이제 master 브랜치를 rebase한 branch-a 로 fast-forward하도록 merge한다 (master 브랜치에서 <code>git merge branch-a</code>)</p>
<p>그 다음 branch-b로 이동해서 <code>git rebase master</code> 를 입력한다.</p>
<p>[그림5]</p>
<p><img src="https://velog.velcdn.com/images/anak_2/post/901e7f5a-479a-4ec4-9bd2-738632cda42b/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/anak_2/post/e18d0ada-681c-4c91-acd9-deaeb2863e3a/image.png" alt=""></p>
<p>branch-a 와 마찬가지로 충돌을 해결한다.</p>
<p>[그림6]</p>
<p><img src="https://velog.velcdn.com/images/anak_2/post/85cc6fb2-c6e8-4080-94d5-e8e07fd65ae6/image.png" alt=""></p>
<p>최종적으로 공통 커밋에서 시작한 브랜치들을 [그림6] 처럼 이어붙인 커밋으로 히스토리를 관리할 수 있다.</p>
<h3 id="git-rebase의-장단점">git rebase의 장단점</h3>
<ul>
<li><strong>git rebase 장점</strong></li>
</ul>
<p>가장 큰 장점은 <code>merge</code> 에 비해 커밋 히스토리를 깔끔하게 관리할 수 있다는 점이다. 커밋들의 인과관계가 하나이기 때문에 직관적으로 보기에 좋다.</p>
<p>하지만 실제로 팀 프로젝트에 git rebase를 다 같이 적용하면서 어려웠던 경험은 다음과 같다.</p>
<ul>
<li><strong>git rebase 단점</strong></li>
</ul>
<p>공통 커밋에서 출발한 이후로 적었다 지웠다를 많이 반복한 코드의 경우, <code>rebase</code> 로 충돌 해결할 때, 이전에 내가 삭제했던 코드가 다시 살아나서 충돌해결하는 경우가 많다는 것이다.</p>
<p>위에서 [그림2]와 [그림3] 을 보면 “commit1 a” 라는 커밋과 “commit2 a”라는 커밋 2개 모두 master 브랜치와 충돌이 나서 해결하는 과정을 보면, 내가 의도한 마지막 코드는 “commit2 a”인데 이전에 rebase 충돌을 해결하는 과정에서 깜빡하고 “commit1 a” 코드를 살려서 충돌 해결을 했기 때문에 이후 <code>REBASE (2/2)</code> 에서 <strong>죽은 코드를 살려버린 실수</strong>를 했다.</p>
<p>위와 같은 1줄 충돌은 실수를 잡기 쉽지만, 이게 여러 파일을 건드리고 복잡한 로직이 연관된 경우, 내가 작성한 코드에도 불구하고 헷갈릴 수 있다. </p>
<ul>
<li><strong>git rebase를 사용하면서 마주쳤던 어려운 점</strong></li>
</ul>
<p>실제로 프로젝트에 rebase로 합치자는 규칙을 적용했지만, 충돌 해결 과정에서 실수할 가능성이 컸기 때문에 
작은 단위의 PR로 쪼개거나, 수정할 파일을 분리하여 충돌을 최소화하자는 합의가 필요했다.</p>
<h3 id="git-rebase시-주의할-점❗">git rebase시 주의할 점❗</h3>
<blockquote>
<p>⚠️이미 공개 저장소에 Push 한 커밋을 Rebase 하지 마라⚠️</p>
</blockquote>
<p><code>git rebase</code>를 사용하면 내가 커밋했던 기록을 바꾼다
 원격 저장소에 push한 브랜치를 다시 다른 브랜치에 rebase를 하게 될 경우, 새로운 커밋으로 바뀌기 때문에 다른 브랜치가 고생하게 된다.</p>
<p>예시로 다음 사례를 만들어봤다. </p>
<p>1.** master** 에서 커밋 2개를 만들고 원격 저장소에 Push한다.
2. <strong>branch-a</strong> 가 <strong>master</strong> 의 HEAD에서 커밋 1개를 만든다.
3. <strong>branch-b</strong> 가 <strong>master</strong> 를 rebase하고 커밋 2개를 만든다.
4. <strong>master</strong> 가 <strong>branch-b</strong> 를 rebase한다.
    - ⚠️이미 원격 저장소에 Push된 <code>commit1 master</code> 와 <code>commit2 and push master</code>를 <br><strong>branch-b</strong>와 합치면서 새로운 커밋으로 또 만든다</p>
<p><img src="https://velog.velcdn.com/images/anak_2/post/cb1a6ae0-7387-45f0-8bf9-e9b8fae4e4b4/image.png" alt=""></p>
<ol start="5">
<li><p><strong>branch-a</strong>가 <strong>master</strong> 에 다시 rebase하면, 기존 master의 <code>commit1 master</code>와 <code>commit2 and push master</code>가 다른 커밋으로 바뀌었기 때문에 다시 충돌을 해결해야하는 일이 발생한다.</p>
<p><img src="https://velog.velcdn.com/images/anak_2/post/4f399692-cbfb-4fdd-8f3a-012c1a5fd964/image.png" alt="">
<img src="https://velog.velcdn.com/images/anak_2/post/5bda6dbe-b2b1-424e-9b7a-fe0f5caf5b37/image.png" alt="">
위에서 보듯이,** branch-a** 는 기존 master의 커밋이었던 <code>commit master</code> 부터 충돌이 발생했다</p>
</li>
</ol>
<p>출처: <a href="https://git-scm.com/book/ko/v2/Git-%EB%B8%8C%EB%9E%9C%EC%B9%98-Rebase-%ED%95%98%EA%B8%B0">https://git-scm.com/book/ko/v2/Git-브랜치-Rebase-하기</a></p>
<h3 id="결론">결론</h3>
<p><code>git rebase</code> 를 충돌이 많이 없을 경우 조심해서 사용한다면 히스토리를 깔끔하게 관리하기 정말 좋은 방법이다. 하지만 실제 프로젝트를 <code>git rebase</code> 로만 관리하려 했지만, 이전에 삭제했던  히스토리를 깔끔하게 관리하다가 코드가 꼬일 경우는 과감하게 포기하고 <code>git merge</code> 를 사용하는 것을 추천한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[NestJS 기본 공부]]></title>
            <link>https://velog.io/@anak_2/NestJS-%EA%B8%B0%EB%B3%B8-%EA%B3%B5%EB%B6%80</link>
            <guid>https://velog.io/@anak_2/NestJS-%EA%B8%B0%EB%B3%B8-%EA%B3%B5%EB%B6%80</guid>
            <pubDate>Fri, 02 Aug 2024 05:34:43 GMT</pubDate>
            <description><![CDATA[<ul>
<li>Nest JS는 Javascript의 프레임워크로, 웹 백엔드 개발을 용이하게 해주는 역할을 한다.</li>
</ul>
<p>💡<strong>사전 준비</strong></p>
<ul>
<li>NestJS 프로젝트를 실행할 수 있는 에디터 설치 (VScode 추천)</li>
<li>16버전 이상의 Node.js 설치</li>
</ul>
<p>✔️<strong>사전 지식 (필수 아님)</strong></p>
<ul>
<li>스프링 기초 이상의 지식</li>
</ul>
<h1 id="nest-js-세팅">Nest JS 세팅</h1>
<ul>
<li>Nest는 TS와 JS 모두 호환. 최신 언어 기능을 사용하기 위해 Javascript와 사용하려면 Babel 컴파일러가 필요.</li>
<li>npm을 이용해 초기 nest js 프로젝트 세팅하기</li>
</ul>
<pre><code class="language-bash">$ npm i -g @nestjs/cli
$ nest new project-name</code></pre>
<ul>
<li>설치가 끝난 후 프로젝트 구조 설명</li>
</ul>
<pre><code>- node_modules
- src
    - app.controller.ts // 기본 컨트롤러, 단일 라우트
    - app.controller.spec.ts // 컨트롤러에 대한 단위 테스트 파일
    - app.module.ts // 애플리케이션의 루트 모듈
    - app.service.ts // 기본 서비스, 단일 메서드
    - main.ts // 애플리케이션 진입 파일, NestFactory를 사용하여 Nest 애플리케이션 인스턴스 생성
- test</code></pre><pre><code class="language-tsx">// main.ts

import { NestFactory } from &#39;@nestjs/core&#39;;
import { AppModule } from &#39;./app.module&#39;;

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(3000); // 3000번 포트
}
bootstrap();</code></pre>
<ul>
<li><p><code>main.ts</code> 가 <strong>bootstrap  (애플리케이션 초기화 + 비동기적 실행, Spring의 run과 비슷)</strong></p>
</li>
<li><p>****<code>NestFactory</code> 가 객체를 생성하는데 사용하는 클래스</p>
<ul>
<li>create() 함수로 애플리케이션 객체를 반환</li>
</ul>
</li>
<li><p>다음 명령어로 실행 가능</p>
</li>
</ul>
<pre><code class="language-tsx">$ npm run start</code></pre>
<ul>
<li>빌드 속도를 높이려면 <code>npm run -- -b swc</code> 사용,  <a href="https://docs.nestjs.com/recipes/swc">swc 설명</a></li>
<li>파일 변경 탐지로 재컴파일 하고싶으면 <code>npm run start:dev</code></li>
</ul>
<h1 id="controllers">Controllers</h1>
<ul>
<li>컨트롤러의 역할은 클라이언트의 HTTP 요청을 받아 적절한 응답을 반환<ul>
<li>스프링과 비슷하기 때문에 MVC 패턴을 보면 될듯함</li>
</ul>
</li>
<li>기본 컨트롤러는 클래스 형식이며, <strong>데코레이터</strong>를 사용한다.<ul>
<li>데코레이터는 <code>@Controller()</code>, <code>@Get()</code>와 같이 스프링의 어노테이션 역할</li>
</ul>
</li>
</ul>
<h3 id="routing">Routing</h3>
<ul>
<li>어떤 컨트롤러가 어떤 요청을 받는지 제어</li>
<li>스프링의 Mapping 어노테이션과 비슷</li>
</ul>
<pre><code class="language-tsx">import { Controller, Get } from &#39;@nestjs/common&#39;;

@Controller(&#39;cats&#39;)
export class CatsController {

  @Get()
  findAll(): string {
    return &#39;This action returns all cats&#39;;
  }
}</code></pre>
<ul>
<li>Controller 데코레이터가 cats 경로에 요청 처리</li>
<li>모든 라우트가 /cats 로 경로 시작</li>
<li>위 코드에선 /cats 로 Http Get 요청을 보내면 findAll() 메서드 에서 처리됨</li>
</ul>
<h3 id="응답response-조작">응답(response) 조작</h3>
<ul>
<li>Standard (기본, 추천)<ul>
<li>객체나 배열과 같은 경우 자동으로 JSON으로 직렬화</li>
<li>원시 타입의 경우 해당 값만 보냄</li>
<li>Http 상태 코드는 <code>@HttpCode()</code> 로 변경 가능</li>
</ul>
</li>
<li>Library (Express와 같은 타 라이브러리)<ul>
<li><code>@Res()</code> 와 같은 데코레이터를 주입하면 기본 응답을 무시하고 커스텀하여 사용도 가능</li>
</ul>
</li>
</ul>
<h3 id="request-object">Request object</h3>
<ul>
<li><code>@Req()</code> 데코레이터를 파라미터에 이용</li>
</ul>
<pre><code class="language-tsx">import { Controller, Get, Req } from &#39;@nestjs/common&#39;;
import { Request } from &#39;express&#39;;

@Controller(&#39;cats&#39;)
export class CatsController {
  @Get()
  findAll(@Req() request: Request): string {
    return &#39;This action returns all cats&#39;;
  }
}</code></pre>
<ul>
<li><p>다양한 요청 객체에 접근 가능 - <a href="https://docs.nestjs.com/controllers#request-object">공식문서 Request Object</a></p>
</li>
<li><p>요청 데코레이터 테스트 코드</p>
<pre><code class="language-tsx">  import { 
    Controller,
    Get,
    Post,
    Req,
    Res,
    Session,
    Param,
    Body,
    Query,
    Headers,
    Ip,
    HostParam,
    HttpCode,
  } from &#39;@nestjs/common&#39;;
  import { AppService } from &#39;./app.service&#39;;
  import { Request, Response } from &#39;express&#39;;

  @Controller()
  export class AppController {
    constructor(private readonly appService: AppService) {}

    @Get()
    getHello(): string {
      return this.appService.getHello();
    }

    @Get(&#39;test/:id&#39;)
    exampleGetMethod(
      @Req() req: Request,
      @Res() res: Response,
      @Session() session: any,
      @Param(&#39;id&#39;) id: string,
      @Query(&#39;name&#39;) query: any,
      @Headers() headers: any,
      @Ip() ip: string,
      @HostParam() hosts: any,
    ): void {
      // 각 요청 데이터를 출력
      console.log(&#39;Request URL:&#39;, req.url);
      console.log(&#39;Session:&#39;, session);
      console.log(&#39;Param id:&#39;, id);
      console.log(&#39;Query:&#39;, query);
      console.log(&#39;Headers:&#39;, headers);
      console.log(&#39;IP Address:&#39;, ip);
      console.log(&#39;Hosts:&#39;, hosts);

      // 응답 보내기
      res.status(200).send(&#39;success&#39;);
    }

    @HttpCode(201)
    @Post(&#39;test&#39;)
    examplePostMethod(
      @Req() req: Request,
      @Session() session: any,
      @Param(&#39;id&#39;) id: string,
      @Body() body: any,
      @Query() query: any,
      @Headers() headers: any,
      @Ip() ip: string,
      @HostParam() hosts: any,
    ) {
      // 각 요청 데이터를 출력
      console.log(&#39;Request URL:&#39;, req.url);
      console.log(&#39;Body:&#39;, body);

      // 응답 보내기
      return &#39;post success&#39;;
    }
  }
</code></pre>
</li>
<li><p>응답 결과</p>
<pre><code class="language-tsx">  // GET 요청
  Request URL: /test/1?name=kim
  Session: undefined
  Param id: 1
  Query: kim
  Headers: {
    &#39;user-agent&#39;: &#39;PostmanRuntime/7.40.0&#39;,
    accept: &#39;*/*&#39;,
    &#39;postman-token&#39;: &#39;...&#39;,
    host: &#39;localhost:3000&#39;,
    &#39;accept-encoding&#39;: &#39;gzip, deflate, br&#39;,
    connection: &#39;keep-alive&#39;
  }
  IP Address: ::1
  Hosts: {}

  // POST 요청
  Request URL: /test
  Body: { name: &#39;kim&#39; }</code></pre>
<p>  <img src="https://prod-files-secure.s3.us-west-2.amazonaws.com/50bb9724-5408-41be-8af3-4e1e8a699b16/7cd28908-c76f-4026-a592-87ee65d08267/Untitled.png" alt="Untitled"></p>
<p>  <img src="https://prod-files-secure.s3.us-west-2.amazonaws.com/50bb9724-5408-41be-8af3-4e1e8a699b16/83d3589a-8d44-4e34-95f8-859f87b692d7/Untitled.png" alt="Untitled"></p>
</li>
</ul>
<h3 id="sub-domain-routing">Sub-Domain Routing</h3>
<ul>
<li>특정 도메인에서만 요청할 수 있도록 Sub-Domain 설정 가능</li>
</ul>
<pre><code class="language-tsx">@Controller({ host: &#39;admin.example.com&#39; })
export class AdminController {
  @Get(&#39;api/admin&#39;)
  getAdminApi(): string {
    return &#39;This is the admin API&#39;;
  }
}</code></pre>
<h3 id="reqeust-payload">Reqeust Payload</h3>
<ul>
<li>DTO를 이용한 데이터 스키마 이용한 요청 Body 받기</li>
</ul>
<pre><code class="language-tsx">export class CreateCatDto {
  name: string;
  age: number;
  breed: string;
}

import { Controller, Post, Body } from &#39;@nestjs/common&#39;;
import { CreateCatDto } from &#39;./create-cat.dto&#39;;

@Controller(&#39;cats&#39;)
export class CatsController {
  @Post()
  async create(@Body() createCatDto: CreateCatDto) {
    return &#39;This action adds a new cat&#39;;
  }
}
</code></pre>
<h1 id="providers">Providers</h1>
<ul>
<li>Service, Repository, Factory, Helper 클래스 등등이 Provider 역할</li>
<li>다른 클래스에 주입 (DI) 될 수 있다</li>
</ul>
<h3 id="services">Services</h3>
<ul>
<li>CatService를 만들어서 CatsController에 주입하는 과정</li>
<li>CatService는 데이터를 저장하고, 가져오는 역할</li>
</ul>
<pre><code class="language-tsx">// cats.service.ts
import { Injectable } from &#39;@nestjs/common&#39;;
import { Cat } from &#39;./interfaces/cat.interface&#39;;

@Injectable()
export class CatsService {
  private readonly cats: Cat[] = [];

  create(cat: Cat) {
    this.cats.push(cat);
  }

  findAll(): Cat[] {
    return this.cats;
  }
}</code></pre>
<ul>
<li><code>@Injectable()</code> 을 사용하여 Nest IoC 컨테이너에서 관리할 수 있는 클래스임을 선언</li>
</ul>
<pre><code class="language-tsx">// cats.controller.ts
import { Controller, Get, Post, Body } from &#39;@nestjs/common&#39;;
import { CreateCatDto } from &#39;./dto/create-cat.dto&#39;;
import { CatsService } from &#39;./cats.service&#39;;
import { Cat } from &#39;./interfaces/cat.interface&#39;;

@Controller(&#39;cats&#39;)
export class CatsController {
  constructor(private catsService: CatsService) {}

  @Post()
  async create(@Body() createCatDto: CreateCatDto) {
    this.catsService.create(createCatDto);
  }

  @Get()
  async findAll(): Promise&lt;Cat[]&gt; {
    return this.catsService.findAll();
  }
}</code></pre>
<ul>
<li>생성자에 <code>private</code> 를 붙여서 chatService를 선언과 동시에 주입받을 수 있도록 함<ul>
<li>이 방법을 사용하면 자동으로 catsService를 클래스 프로퍼티로 선언하고 초기화한다 ❗</li>
</ul>
</li>
</ul>
<h3 id="optional-providers">Optional providers</h3>
<ul>
<li>주입받지 못해도 오류를 발생시키지 않도록하는 데코레이터. 주입받을 것이 없어도 오류가 발생하지 않는다 (null 또는 undefined로 남는다)</li>
</ul>
<pre><code class="language-tsx">import { Injectable, Optional, Inject } from &#39;@nestjs/common&#39;;

@Injectable()
export class HttpService&lt;T&gt; {
  constructor(@Optional() @Inject(&#39;HTTP_OPTIONS&#39;) private httpClient: T) {}
}</code></pre>
<h3 id="property-based-injection">Property-based injection</h3>
<ul>
<li>속성에서 주입받는 방법</li>
</ul>
<pre><code class="language-tsx">import { Injectable, Inject } from &#39;@nestjs/common&#39;;

@Injectable()
export class HttpService&lt;T&gt; {
  @Inject(&#39;HTTP_OPTIONS&#39;)
  private readonly httpClient: T;
}</code></pre>
<ul>
<li>다만 생성자 주입이 더 가시성이 좋고 명시적이기 때문에 <strong>생성자 주입 방식을 선호</strong>한다</li>
</ul>
<h3 id="provider-registration">Provider registration</h3>
<ul>
<li>CatsService (Provider) 를 정의했고 CatsController 에 주입한다고 표시했으므로, 서비스를 Nest에 등록하여 인젝션을 수행할 수 있도록 해야한다.</li>
</ul>
<pre><code class="language-tsx">// app.module.ts
import { Module } from &#39;@nestjs/common&#39;;
import { CatsController } from &#39;./cats/cats.controller&#39;;
import { CatsService } from &#39;./cats/cats.service&#39;;

@Module({
  controllers: [CatsController],
  providers: [CatsService],
})
export class AppModule {}</code></pre>
<ul>
<li><code>@Moduel</code> 데코레이터를 통해 공급자 배열에 서비스 추가</li>
</ul>
<h1 id="modules">Modules</h1>
<ul>
<li>모듈은 <code>@Module()</code> 데코레이터가 붙은 클래스</li>
<li><strong>Root Module</strong>이 하위 Module들을 관리하고 하위 Module은 각 Feature Module을 가진다.</li>
<li><strong>providers</strong>: Nest Injector에 의해 인스턴스화되고, 현재 모듈에서 공유될 provider 집합</li>
</ul>
<pre><code class="language-tsx">// cats/cats.module.ts (하위 모듈)
import { Module } from &#39;@nestjs/common&#39;;
import { CatsController } from &#39;./cats.controller&#39;;
import { CatsService } from &#39;./cats.service&#39;;

@Module({
  controllers: [CatsController],
  providers: [CatsService],
})
export class CatsModule {}</code></pre>
<pre><code class="language-tsx">// app.module.ts (루트 모듈)
import { Module } from &#39;@nestjs/common&#39;;
import { CatsModule } from &#39;./cats/cats.module&#39;;

@Module({
  imports: [CatsModule],
})
export class AppModule {}</code></pre>
<h3 id="shared-modules">Shared modules</h3>
<ul>
<li>어떤 객체를 다른 모듈에서도 공유해서 쓰고싶을 때, 아래와 같이 <code>exports</code> 을 배열에 추가하면 CatsModule을 import하는 곳에서 CatsService를 사용할 수 있다.</li>
</ul>
<pre><code class="language-tsx">import { Module } from &#39;@nestjs/common&#39;;
import { CatsController } from &#39;./cats.controller&#39;;
import { CatsService } from &#39;./cats.service&#39;;

@Module({
  controllers: [CatsController],
  providers: [CatsService],
  exports: [CatsService]
})
export class CatsModule {}</code></pre>
<h3 id="global-modules">Global modules</h3>
<ul>
<li>모듈을 여러 군데에서 필요하다면 Angular Provider인 <code>@Global()</code> 데코레이터 사용. 헬퍼, 데이터 모듈과 같은 여러 곳에서 필요한 것에 유용할 수 있다.</li>
<li>Global 모듈은 루트나 코어 모듈에 한 번만 등록되어야한다.</li>
</ul>
<pre><code class="language-tsx">import { Module, Global } from &#39;@nestjs/common&#39;;
import { CatsController } from &#39;./cats.controller&#39;;
import { CatsService } from &#39;./cats.service&#39;;

@Global()
@Module({
  controllers: [CatsController],
  providers: [CatsService],
  exports: [CatsService],
})
export class CatsModule {}</code></pre>
<h1 id="sql-typeorm">SQL (TypeORM)</h1>
<ul>
<li>TypeORM , postgresql 설치</li>
</ul>
<pre><code class="language-tsx">$ npm install --save typeorm pg</code></pre>
<ul>
<li>postgresql 설치</li>
</ul>
<h3 id="데이터베이스와-연결하기">데이터베이스와 연결하기</h3>
<ul>
<li>new DataSource().initialize() 를 이용해서 데이터베이스와 연결, DataSource의 매개변수로 DB 연결 정보 넣기</li>
</ul>
<pre><code class="language-tsx">import { DataSource } from &#39;typeorm&#39;;

export const databaseProviders = [
  {
    provide: &#39;DATA_SOURCE&#39;,
    useFactory: async () =&gt; {
      const dataSource = new DataSource({
        type: &#39;mysql&#39;,
        host: &#39;localhost&#39;,
        port: 3306,
        username: &#39;root&#39;,
        password: &#39;root&#39;,
        database: &#39;test&#39;,
        entities: [
            __dirname + &#39;/../**/*.entity{.ts,.js}&#39;,
        ],
        synchronize: true,
      });

      return dataSource.initialize();
    },
  },
];</code></pre>
<ul>
<li><strong>initialize()</strong> 는 Promise를 반환하기 때문에 async로 실행해야한다.</li>
<li><strong>entities</strong>는 테이블과 매핑되는 엔티티 파일의 경로</li>
<li><strong>synchronize</strong>를 true로 하면 데이터베이스 스키마가 엔티티 정의와 자동으로 동기화</li>
</ul>
<h3 id="데이터베이스-provider-접근">데이터베이스 Provider 접근</h3>
<ul>
<li>databaseProvider는 DatabaseModule로 감싸서 외부에 노출시키는게 관례이다.</li>
</ul>
<pre><code class="language-tsx">import { Module } from &#39;@nestjs/common&#39;;
import { databaseProviders } from &#39;./database.providers&#39;;

@Module({
  providers: [...databaseProviders],
  exports: [...databaseProviders],
})
export class DatabaseModule {}</code></pre>
<ul>
<li>이제 <code>@Inject()</code> 데코레이터를 이용해 DATA_SOURCE 객체를 주입할 수 있다.</li>
</ul>
<h3 id="repository-pattern">Repository pattern</h3>
<ul>
<li>스프링과 유사하게 데이터베이스와 밀접한 클래스를 Repository로 분리,
아래 예시를 통해 학습할 수 있다.</li>
<li>Photo 디렉토리를 대표하는 PhotoModule이 있다.</li>
<li>Photo Entity 생성</li>
</ul>
<pre><code class="language-tsx">// photo.entity.ts
import { Entity, Column, PrimaryGeneratedColumn } from &#39;typeorm&#39;;

@Entity()
export class Photo {
  @PrimaryGeneratedColumn()
  id: number;

  @Column({ length: 500 })
  name: string;

  @Column(&#39;text&#39;)
  description: string;

  @Column()
  filename: string;

  @Column(&#39;int&#39;)
  views: number;

  @Column()
  isPublished: boolean;
}</code></pre>
<pre><code class="language-tsx">// photo.provider.ts
import { DataSource } from &#39;typeorm&#39;;
import { Photo } from &#39;./photo.entity&#39;;

export const photoProviders = [
  {
    provide: &#39;PHOTO_REPOSITORY&#39;,
    useFactory: (dataSource: DataSource) =&gt; dataSource.getRepository(Photo),
    inject: [&#39;DATA_SOURCE&#39;],
  },
];</code></pre>
<ul>
<li>참고: 위에 <strong>useFactory</strong>와 같은 <strong>Factory Provider</strong>를 이용한다면 <code>@Injectable</code> 데코레이터가 없어도 inject 배열에 있는 객체를 의존성 매개변수에 주입받을 수 있다. (userFactory를 사용해 Photo 엔티티에 대한 Repository 반환)</li>
<li>이제 <code>PHOTO_REPOSITORY</code>를 주입할 수 있다</li>
</ul>
<pre><code class="language-tsx">// photo.service.ts
import { Injectable, Inject } from &#39;@nestjs/common&#39;;
import { Repository } from &#39;typeorm&#39;;
import { Photo } from &#39;./photo.entity&#39;;

@Injectable()
export class PhotoService {
  constructor(
    @Inject(&#39;PHOTO_REPOSITORY&#39;)
    private photoRepository: Repository&lt;Photo&gt;,
  ) {}

  async findAll(): Promise&lt;Photo[]&gt; {
    return this.photoRepository.find();
  }
}</code></pre>
<ul>
<li>생성한 Provider와 Service를 모듈에 등록</li>
</ul>
<pre><code class="language-tsx">// photo.module.ts
import { Module } from &#39;@nestjs/common&#39;;
import { DatabaseModule } from &#39;../database/database.module&#39;;
import { photoProviders } from &#39;./photo.providers&#39;;
import { PhotoService } from &#39;./photo.service&#39;;

@Module({
  imports: [DatabaseModule],
  providers: [
    ...photoProviders,
    PhotoService,
  ],
})
export class PhotoModule {}</code></pre>
<h3 id="injectrepository-데코레이터">@InjectRepository 데코레이터</h3>
<ul>
<li>InjectRepository 데코레이터를 쓴다면 Provider를 생략할 수 있다</li>
</ul>
<pre><code class="language-tsx">
@Injectable()
export class PhotoService {
  constructor(
    @InjectRepository(Photo)
    private photoRepository: Repository&lt;Photo&gt;,
  ) {}
}</code></pre>
<h3 id="typeorm의-entitymanager-querybuilder---sql-작성하기">TypeORM의 EntityManager, QueryBuilder - SQL 작성하기</h3>
<pre><code class="language-tsx">import { Injectable } from &#39;@nestjs/common&#39;;
import { InjectRepository } from &#39;@nestjs/typeorm&#39;;
import { Repository, EntityManager } from &#39;typeorm&#39;;
import { MyEntity } from &#39;./my.entity&#39;;

@Injectable()
export class AppService {
  constructor(
    @InjectRepository(MyEntity)
    private myEntityRepository: Repository&lt;MyEntity&gt;,
    private entityManager: EntityManager,
  ) {}

  // EntityManager 사용
  async runCustomQueryWithEntityManager() {
    const rawData = await this.entityManager.query(&#39;SELECT * FROM my_entity&#39;);
    return rawData;
  }

  // QueryBuilder 사용
  async runCustomQueryWithQueryBuilder() {
    const rawData = await this.myEntityRepository
      .createQueryBuilder(&#39;entity&#39;)
      .select(&#39;entity.id&#39;)
      .addSelect(&#39;entity.name&#39;)
      .where(&#39;entity.age &gt; :age&#39;, { age: 25 })
      .getRawMany();
    return rawData;
  }
}
</code></pre>
<h1 id="간단한-crud-프로젝트">간단한 CRUD 프로젝트</h1>
<ul>
<li><p>유저를 생성, 조회, 수정, 삭제 하는 간단한 프로젝트를 진행하여 앞에서 배웠던 내용들을 복습해봅니다.</p>
<p>  <a href="https://github.com/Anak-2/nest-project">https://github.com/Anak-2/nest-project</a></p>
</li>
<li><p>전체 코드는 위를 보면 확인할 수 있습니다. 아래 내용은 코드를 작성하면서 겪었던 트러블 슈팅과 헷갈렸던 부분을 정리하며, 위에서 부족했던 내용을 보충합니다.</p>
</li>
</ul>
<h3 id="srcuser-폴더">src/user 폴더</h3>
<pre><code class="language-tsx">// user.controller.ts
@Controller(&#39;user&#39;)
export class UserController {
    constructor(private readonly userService: UserService) {}

    @Post(&#39;create&#39;)
    create(@Body() userCreateRequest: UserCreateRequest): void {
        this.userService.doCreate(userCreateRequest);
    }

    @Get(&#39;read/:id&#39;)
    async read(@Param(&#39;id&#39;) id: number): Promise&lt;UserReadResponse&gt; {
        return await this.userService.doRead(id);
    }

    @Patch(&#39;update&#39;)
    update(@Body() userNameUpdateRequest: UserNameUpdateRequest): void {
        return this.userService.doUpdate(
            userNameUpdateRequest.id,
            userNameUpdateRequest.phone,
        );
    }

    @Delete(&#39;delete/:id&#39;)
    async delete(@Param(&#39;id&#39;) id: number): Promise&lt;boolean&gt; {
        return await this.userService.doDelete(id);
    }
}
</code></pre>
<ul>
<li>클라이언트의 요청을 받아서 로직을 매핑해주는 Controller입니다. 스프링과 유사한 부분이 많아 자세한 설명은 넘기겠습니다.</li>
<li>DTO를 이용해 요청 데이터를 매핑합니다.</li>
</ul>
<pre><code class="language-tsx">// user.service.ts
@Injectable()
export class UserService {
    @Inject(&#39;USER_REPOSITORY&#39;)
    private userRepository: Repository&lt;UserEntity&gt;;

    doCreate(userCreateRequest: UserCreateRequest): void {
        // console.log(userCreateRequest instanceof UserCreateRequest);
        // 자바스크립트는 클래스의 인스턴스가 아닌 단순한 객체를 넘겨주기 때문에 class transfromer를 이용해야 클래스 내부의 메서드를 이용할 수 있다
        const userCreateClass = plainToClass(
            UserCreateRequest,
            userCreateRequest,
        );
        this.userRepository.save(userCreateClass.toEntity());
    }

    async doRead(id: number): Promise&lt;UserReadResponse&gt; {
        const userEntity = await this.userRepository.findOne({ where: { id } });
        const userReadResponse = new UserReadResponse();
        userReadResponse.name = userEntity.name;
        userReadResponse.phone = userEntity.phone;
        return userReadResponse;
    }

    // update 함수는 첫 번째 인수 조건에 맞는 Entity를 찾아서 두 번째 인수의 Object 값으로 업데이트
    doUpdate(id: number, phone: string): void {
        const phoneObj = { phone: phone };
        this.userRepository.update({ id: id }, phoneObj);
    }

    async doDelete(id: number): Promise&lt;boolean&gt; {
        return (await this.userRepository.delete({ id: id })) ? true : false;
    }
}</code></pre>
<ul>
<li><strong>트러블 슈팅</strong><ol>
<li>doCreate 부분에 userCreateRequest DTO를 매개변수로 받아서, 클래스 내에 정의한 toEntity() 함수를 이용해 UserEntity로 만들려했습니다.
하지만 Typescript임에도 불구하고 자바스크립트는 단순 객체만 넘겨받는 성질 때문에 class transformer 라이브러리의 plainToClass 함수를 이용해야했습니다.</li>
<li>Typescript에선 async 함수의 반환값은 Promise 객체로 감싸야합니다.</li>
<li>TypeORM의 repository 기본 제공 함수의 기능으로 복잡한 쿼리가 가능한지 연구가 필요합니다.</li>
<li>‘USER_REPOSITORY’ 를 주입받을 때 오타가 나서 주입받지 못한 오류가 있었습니다. 매직 스트링을 사용하지 않고 상수로 관리하길 권장합니다.</li>
</ol>
</li>
<li>공식문서에서 Database보다 TypeORM을 우선 학습했더니 @InjectRepository 데코레이터 방식이 아닌 Provider로 주입받은 방식을 이용했습니다. 더 간편한 것은 @InjectRepository 방식인 것 같으니 바꿔보는 과정이 필요합니다.</li>
</ul>
<pre><code class="language-tsx">// user.provider.ts
export const userProviders = [
    {
        provide: &#39;USER_REPOSITORY&#39;,
        useFactory: (dataSource: DataSource) =&gt;
            dataSource.getRepository(UserEntity),
        inject: [&#39;DATA_SOURCE&#39;],
    },
];</code></pre>
<ul>
<li>provider를 이용해 USER_REPOSITORY를 주입할 수 있도록 생성해줍니다.</li>
<li>useFactory를 이용하면 @Inject 데코레이터를 생략하고 객체를 주입받을 수 있습니다. 주입할 객체는 inject 배열에 주입할 순서를 지켜서 적으면 됩니다.</li>
</ul>
<pre><code class="language-tsx">// user.module.ts
@Module({
    imports: [DatabaseModule],
    controllers: [UserController],
    providers: [UserService, ...userProviders],
    exports: [UserService],
})
export class UserModule {}

// app.module.ts
@Module({
    imports: [UserModule, DatabaseModule],
    controllers: [AppController],
    providers: [AppService],
})
export class AppModule {}</code></pre>
<ul>
<li>UserModule을 만들어서 AppModule에 등록해줘야 api가 클라이언트에 노출됩니다. ⭐</li>
<li>imports를 이용해 외부 모듈을 가져와서 주입받을 수 있도록 합니다.</li>
<li>UserService와 userProviders를  providers에 넣어서 주입 컨테이너에서 관리하도록 만듭니다.</li>
<li>UserService를 외부에서도 주입받을 수 있도록 exports 합니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[도메인 모델 순수성 vs 도메인 모델 완전성 (DDD Trilemma)]]></title>
            <link>https://velog.io/@anak_2/%EB%8F%84%EB%A9%94%EC%9D%B8-%EB%AA%A8%EB%8D%B8-%EC%88%9C%EC%88%98%EC%84%B1-vs-%EB%8F%84%EB%A9%94%EC%9D%B8-%EB%AA%A8%EB%8D%B8-%EC%99%84%EC%A0%84%EC%84%B1-DDD-Trilemma</link>
            <guid>https://velog.io/@anak_2/%EB%8F%84%EB%A9%94%EC%9D%B8-%EB%AA%A8%EB%8D%B8-%EC%88%9C%EC%88%98%EC%84%B1-vs-%EB%8F%84%EB%A9%94%EC%9D%B8-%EB%AA%A8%EB%8D%B8-%EC%99%84%EC%A0%84%EC%84%B1-DDD-Trilemma</guid>
            <pubDate>Fri, 02 Aug 2024 05:32:51 GMT</pubDate>
            <description><![CDATA[<h3 id="도메인-모델-완전성">도메인 모델 완전성</h3>
<ul>
<li>예를 들어 사용자 이메일 변경</li>
</ul>
<pre><code class="language-java">public class User {

    private String email;

    public Result changeEmail(String newEmail){
        if(Company.isEmailCorporate(newEmail) == false)
            return Result.Failure(&quot;Incorrect email domain&quot;);

        email = newEmail;
        return Result.Success();
    }
}

public class Company {

    public boolean isEmailCorporate(String email){
        ...
    }
}

public class UserController {

    public String changeEmail(int userId, String newEmail){
        User user = userRepository.getById(userId);
        Result result = user.changeEmail(newEmail);
        if(result.isFailure) return &quot;fail&quot;;
        return &quot;ok&quot;;
    }
}</code></pre>
<p>위와 같이 도메인에 애플리케이션의 비즈니스 로직이 모두 포함되는 것을 풍부한 도메인(Rich Domain) , 완전한 도메인이라 부른다.</p>
<p>위와 같은 도메인의 특징은, 도메인 로직 단편화(Domain Logic Fragmentation) 가 일어나지 않는다.</p>
<p><strong>도메인 로직 단편화란? -</strong> 도메인 계층이 아닌 다른 계층에 도메인 로직이 있는 경우</p>
<p>위 예에서 UserController에 로직을 포함하지 않고 조합하는 역할만 한다.</p>
<h3 id="도메인-모델-순수성">도메인 모델 순수성</h3>
<ul>
<li>또 다른 비즈니스 규칙을 구현한다 가정, 사용자 이메일을 변경하기 전에 새 이메일이 이미 사용 중인지 확인</li>
<li>이메일 고유성 확인 로직 필요</li>
</ul>
<pre><code class="language-java">public class UserController {

    public String changeEmail(int userId, String newEmail){

        User existingUser = userRepository.getByEmail(newEmail);
        if(existingUser != null &amp;&amp; existingUser.id != userId){
            return &quot;email is already taken&quot;;
        }
        User user = userRepository.getById(userId);
        Result result = user.changeEmail(newEmail);
        if(result.isFailure) return &quot;fail&quot;;
        return &quot;ok&quot;;
    }
}</code></pre>
<ul>
<li>위의 도메인 로직 단편화가 일어났다. 컨트롤러에 도메인 관련 비즈니스 로직 하나가 추가됐기 때문</li>
<li>도메인 모델 완전성을 복원하려면 로직을 User 클래스에 이동 필요</li>
</ul>
<pre><code class="language-java">public class User {

    private String email;

    public Result changeEmail(String newEmail, UserRepository repository){
        if(Company.isEmailCorporate(newEmail) == false)
            return Result.Failure(&quot;Incorrect email domain&quot;);

        User existingUser = repository.getByEmail(newEmail);
        if(existingUser != null &amp;&amp; existingUser.id != userId){
            return &quot;email is already taken&quot;;
        }

        email = newEmail;
        return Result.Success();
    }
}</code></pre>
<ul>
<li>이 버전의 경우 도메인 모델 단편화는 제거하지만, <strong>도메인 모델 순수성</strong>을 희생한다.<ul>
<li>User가 데이터베이스에 접근하게 하므로 실패</li>
</ul>
</li>
<li>도메인 모델 순수성이란? 도메인 클래스는 기본 유형이나 다른 도메인 클래스에만 의존해야 한다. (도메인 계층끼리)</li>
<li>여기서 도메인 모델 완전성과 도메인 모델 순수성 사이의 선택이 나온다</li>
</ul>
<h3 id="트릴레마">트릴레마</h3>
<ul>
<li>애플리케이션 성능을 포기하며 도메인 모델 순수성과 완전성을 챙길 수 있다</li>
</ul>
<pre><code class="language-java">public class User {

    private String email;

    public Result changeEmail(String newEmail, List&lt;User&gt; allUsers){
        if(Company.isEmailCorporate(newEmail) == false)
            return Result.Failure(&quot;Incorrect email domain&quot;);

        boolean existingUser = allUsers.any(x -&gt; x.email == newEmail);
        if(existingUser != null &amp;&amp; existingUser.id != userId){
            return &quot;email is already taken&quot;;
        }

        email = newEmail;
        return Result.Success();
    }
}</code></pre>
<ul>
<li><p>모든 검증이 도메인 계층에 있고, 순수하며 완벽하다</p>
</li>
<li><p>여기서 <strong>트릴레마</strong>가 작용한다. 다음 세 가지 속성을 모두 가질 수는 없다</p>
<ul>
<li><strong>도메인 모델 완전성</strong> - 도메인 로직은 도메인에만 존재한다</li>
<li><strong>도메인 모델 순수성</strong> - 도메인 계층에 외부 종속성이 없다</li>
<li><strong>성능</strong> - 불필요한 프로세스 호출이 없다</li>
</ul>
</li>
<li><p>3가지 옵션 , 각각 2가지 속성만 충족</p>
<ul>
<li>모든 외부 읽기, 쓰기를 포함 → 성능 포기</li>
<li>도메인 계층에 외부 종속성 주입 → 순수성 포기</li>
<li>도메인 계층과 컨트롤러 간 프로세스를 분리 → 완전성 포기</li>
</ul>
</li>
<li><p>도메인 완전성보단 순수성을 선택 권장 → 도메인 로직 단편화가 덜하다.</p>
</li>
<li><p>컨트롤러와 도메인에 비즈니스 로직을 분할해야하는 이유</p>
<ul>
<li>DDD 관점: 도메인이 “핵심”이기 때문에 애플리케이션의 복잡성을 핵심에서 해결하는 것이 좋다</li>
<li>함수형 프로그래밍 : 함수를 투명하게 만들어서 애플리케이션의 기능적 핵심에서 숨겨진 입력, 출력을 피하는 것 (데이터베이스 쿼리)</li>
<li>단위 테스트 : 순수 도메인 모델이 테스트 가능한 도메인 모델을 의미. 외부 종속성이 있다면 Mock, Stub을 설정해야 하므로 테스트 유지 관리가 힘듬</li>
</ul>
</li>
</ul>
<p>출처 : <a href="https://enterprisecraftsmanship.com/posts/domain-model-purity-completeness/">https://enterprisecraftsmanship.com/posts/domain-model-purity-completeness/</a></p>
<p><a href="https://parkmuhyeun.github.io/etc/java/2023-12-16-Domain/">https://parkmuhyeun.github.io/etc/java/2023-12-16-Domain/</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[깃 협업에서 이슈(Issue)와 풀리퀘스트(PR)을 템플릿 (template)으로 관리하기]]></title>
            <link>https://velog.io/@anak_2/%EA%B9%83-%ED%98%91%EC%97%85%EC%97%90%EC%84%9C-%EC%9D%B4%EC%8A%88Issue%EC%99%80-%ED%92%80%EB%A6%AC%ED%80%98%EC%8A%A4%ED%8A%B8PR%EC%9D%84-%ED%85%9C%ED%94%8C%EB%A6%BF-template%EC%9C%BC%EB%A1%9C-%EA%B4%80%EB%A6%AC%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@anak_2/%EA%B9%83-%ED%98%91%EC%97%85%EC%97%90%EC%84%9C-%EC%9D%B4%EC%8A%88Issue%EC%99%80-%ED%92%80%EB%A6%AC%ED%80%98%EC%8A%A4%ED%8A%B8PR%EC%9D%84-%ED%85%9C%ED%94%8C%EB%A6%BF-template%EC%9C%BC%EB%A1%9C-%EA%B4%80%EB%A6%AC%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sun, 17 Mar 2024 05:37:37 GMT</pubDate>
            <description><![CDATA[<h2 id="깃-협업이-중요한-이유">깃 협업이 중요한 이유</h2>
<blockquote>
<p>혼자 개발할 경우엔 자기가 어떤 기능을 구현을 완료했고, 어떤 문제가 있는지 잘 알기 때문에 깃 관리의 필요성이 체감되지 않는다.
하지만 협업을 하고, 개발 팀 규모가 커질 수록 자신이 하고있는 일을 적극적으로 표시할 수 있어야 원할한 팀워크가 진행된다.
그래서 협업에서 가장 기초적으로 신경써야할 부분은, <strong>Git 관리</strong>라고 생각한다.
<strong>깃 이슈 관리, 깃 풀리퀘스트 관리, 깃 코드 리뷰, 깃 커밋 메시지 컨벤션, 깃 브랜치 전략</strong> 등 다양한 약속 중 이번 게시글에선 깃 이슈와 풀리퀘스트 템플릿에 관하여 글을 작성하고자 한다</p>
</blockquote>
<h2 id="템플릿을-도입한-이유">템플릿을 도입한 이유</h2>
<ul>
<li>프로젝트에서 기능별이나, 수정이 필요한 이슈를 생성하고, 이슈에 대한 브랜치를 생성해서 해결한 뒤 PR을 날리기로 결정</li>
<li>템플릿을 정하지 않으면 매번 이슈나 PR을 생성할 때 마다 중요한 정보를 깜빡하고 못 적을 경우가 많다.</li>
<li>템플릿을 통해 통일된 정보를 이슈마다 볼 수 있으므로 프로젝트 관리 측면에서 좋다.</li>
</ul>
<h2 id="템플릿-생성하는-방법">템플릿 생성하는 방법</h2>
<h3 id="깃헙에서-템플릿-생성">깃헙에서 템플릿 생성</h3>
<h3 id="이슈-템플릿">이슈 템플릿</h3>
<p>   <strong>방법 1.</strong> 깃 프로젝트에서 Settings → Set up templates 클릭 <br>
    <strong>방법 2.</strong> 또는  직접 프로젝트의 최상단 디렉토리에서 .github 폴더를 생성한 뒤 ISSUE_TEMPLATE 폴더를 생성한 후, 아래에 이슈 템플릿으로 사용할 md 파일 생성
<img src="https://velog.velcdn.com/images/anak_2/post/e547bcf0-60dc-4243-a542-23056204b57a/image.png" alt="">
기능 추가 이슈인 Feature request 템플릿 선택
  <img src="https://velog.velcdn.com/images/anak_2/post/a80a7618-0601-4384-b904-6f7b9047a202/image.png" alt="">
 원하는 템플릿 제목과 어디에 쓰이는 지, 템플릿의 내용은 무엇인지 markdown 문법으로 작성
   <img src="https://velog.velcdn.com/images/anak_2/post/fcf9653f-cafd-4156-ae44-6be3797d010f/image.png" alt="">
 <strong>이슈를 생성해보자!</strong>
<img src="https://velog.velcdn.com/images/anak_2/post/7e8fac37-f322-453c-8af9-0e8822e6f088/image.png" alt="">
<img src="https://velog.velcdn.com/images/anak_2/post/820a336c-a062-45bc-b8ed-0a798a27a48c/image.png" alt="">
이슈 템플릿으로 시작했을 때 모습
<img src="https://velog.velcdn.com/images/anak_2/post/2020767f-e8fd-491c-a2f3-0af6621d954d/image.png" alt="">
이슈에 대해 라벨을 이쁘게 꾸며주면 어떤 이슈인지 제목과 라벨만으로 느낌이 확 온다! 라벨도 새로 만들어줘서 세팅해주면 좋다
<img src="https://velog.velcdn.com/images/anak_2/post/3060c28c-a445-474f-b998-5b368cc0410b/image.png" alt="">
생성된 이슈를 확인해보자 (이쁜 라벨!)
<img src="https://velog.velcdn.com/images/anak_2/post/8420a3c5-4b31-48c5-9fd4-c5cc2093d00c/image.png" alt="">
작업 내용은 todo 처럼 관리해주면 위와 같이 n of m tasks 처럼 진행도를 표시해준다
<img src="https://velog.velcdn.com/images/anak_2/post/4ea393ee-7f17-40b3-9b55-1dc94c37143c/image.png" alt=""></p>
<h3 id="풀리퀘스트-템플릿-생성">풀리퀘스트 템플릿 생성</h3>
<p>아까 생성해준 .github 폴더 아래에 PULL_REQUEST_TEMPLATE.md 라는 이름의 파일을 생성해서 내용을 적어주자
<img src="https://velog.velcdn.com/images/anak_2/post/b2921cf2-e35b-4a5f-933e-a1bd96ca65ac/image.png" alt=""></p>
<p>잘 생성했는지 테스트해보자
<img src="https://velog.velcdn.com/images/anak_2/post/bb096dfd-bdc0-452e-8131-4094aa9f47a4/image.png" alt="">
<img src="https://velog.velcdn.com/images/anak_2/post/41acd21c-bfbe-41f8-a823-60e3e77e8926/image.png" alt=""></p>
<p><strong>꿀팁</strong> ‼️
closes #{이슈번호} 를 적어줄 경우 merge될 때 이슈가 자동으로 닫힌다<img src="https://velog.velcdn.com/images/anak_2/post/10b70f3f-711f-4346-8d5d-f945775bc510/image.png" alt=""></p>
<h3 id="템플릿-공유-좋아요♥️-부탁합니다">템플릿 공유 (좋아요♥️ 부탁합니다!)</h3>
<ul>
<li>이슈 템플릿<pre><code></code></pre></li>
</ul>
<hr>
<p>name: Feature request
about: Suggest an idea for this project
title: &quot;[FEAT]&quot;
labels: &#39;&#39;
assignees: &#39;&#39;</p>
<hr>
<hr>
<p>name: Feature Template
about: 기능 추가
title: &quot;[FEAT]&quot;
labels: &#39;&#39;
assignees: &#39;&#39;</p>
<hr>
<h2 id="⚙️어떤-기능인가요">⚙️어떤 기능인가요?</h2>
<blockquote>
<p>추가하려는 기능에 대해 간결하게 설명해주세요</p>
</blockquote>
<h2 id="🔎작업-상세-내용">🔎작업 상세 내용</h2>
<ul>
<li><input disabled="" type="checkbox"> TODO</li>
<li><input disabled="" type="checkbox"> TODO</li>
<li><input disabled="" type="checkbox"> TODO</li>
</ul>
<h2 id="💫참고할만한-자료선택">💫참고할만한 자료(선택)</h2>
<pre><code>- 풀리퀘스트 템플릿</code></pre><h2 id="️⃣연관된-이슈">#️⃣연관된 이슈</h2>
<blockquote>
<p>ex) #이슈번호, #이슈번호</p>
</blockquote>
<h2 id="📝작업-내용">📝작업 내용</h2>
<blockquote>
<p>이번 PR에서 작업한 내용을 간략히 설명해주세요(이미지 첨부 가능)</p>
</blockquote>
<h3 id="스크린샷-선택">스크린샷 (선택)</h3>
<h2 id="💬리뷰-요구사항선택">💬리뷰 요구사항(선택)</h2>
<blockquote>
<p>리뷰어가 특별히 봐주었으면 하는 부분이 있다면 작성해주세요</p>
<p>ex) 메서드 XXX의 이름을 더 잘 짓고 싶은데 혹시 좋은 명칭이 있을까요?</p>
</blockquote>
<pre><code>
</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[윈도우 VSCode 기본 터미널 변경하는 방법
(How to Change a default terminal in Window Environment)]]></title>
            <link>https://velog.io/@anak_2/%EC%9C%88%EB%8F%84%EC%9A%B0-VSCode-%EA%B8%B0%EB%B3%B8-%ED%84%B0%EB%AF%B8%EB%84%90-%EB%B3%80%EA%B2%BD%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95How-to-Change-a-default-terminal-in-Window-Environment</link>
            <guid>https://velog.io/@anak_2/%EC%9C%88%EB%8F%84%EC%9A%B0-VSCode-%EA%B8%B0%EB%B3%B8-%ED%84%B0%EB%AF%B8%EB%84%90-%EB%B3%80%EA%B2%BD%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95How-to-Change-a-default-terminal-in-Window-Environment</guid>
            <pubDate>Tue, 05 Sep 2023 12:12:44 GMT</pubDate>
            <description><![CDATA[<h3 id="바꾸게-된-계기">바꾸게 된 계기</h3>
<p>Powershell 기반의 Window 터미널을 쓰기엔 Linux 기반 명령어를 배우고, 앞으로도 사용할 일이 많기 때문에 바꾸고 싶었다.
또한 vscode 에서 python 의 컴파일러 설정을 .venv 폴더로 지정했기 때문에, .venv 환경에서 커맨드 명령어를 작성하려면 vscode 의 default terminal 을 변경해야했다.</p>
<h3 id="목표">목표</h3>
<p>Powershell 에서 Git Bash 로 기본 터미널 변경하기</p>
<h3 id="전제-조건">전제 조건</h3>
<p>Git Bash 와 Vscode 가 설치되어있어야 한다.</p>
<h3 id="바꾸는-방법">바꾸는 방법</h3>
<ul>
<li><p>첫번 째 방법 (가장 쉬움)
윈도우 사용자 기준 단축키로 <code>Ctrl+Shift+P</code> 누르고
<code>select default profile</code> 검색, drop-down 에서 원하는 터미널로 변경하기</p>
</li>
<li><p>두번 째 방법
터미널 탭에 <code>+</code> 옆 아래 방향 화살표 눌러서 <code>select default profile</code> 선택
Git Bash 선택</p>
</li>
<li><p>세번 째 방법
File -&gt; Preferences -&gt; Settings 들어가서
<code>Terminal › Integrated › Default Profile: Windows</code> 검색, Git Bash 선택</p>
</li>
<li><p>네번 째 방법
<code>settings.json</code> 파일에 다음 추가</p>
</li>
</ul>
<pre><code class="language-python">{
  &quot;terminal.integrated.profiles.windows&quot;:{&quot;Git Bash&quot;:{&quot;path&quot;:&quot;C:\\Program Files\\Git\\bin\\bash.exe&quot;},  },
  &quot;terminal.integrated.defaultProfile.windows&quot;: &quot;Git Bash&quot;
}</code></pre>
<p>path 변수에 본인의 Git Bash 위치 적기</p>
<h3 id="적용된-모습">적용된 모습</h3>
<p><img src="https://velog.velcdn.com/images/anak_2/post/61754de8-bcd2-40ad-9339-f2d02e3f9536/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring Boot With Redis CRUD Example]]></title>
            <link>https://velog.io/@anak_2/Spring-Boot-With-Redis-CRUD-Example</link>
            <guid>https://velog.io/@anak_2/Spring-Boot-With-Redis-CRUD-Example</guid>
            <pubDate>Thu, 31 Aug 2023 06:51:25 GMT</pubDate>
            <description><![CDATA[<h3 id="왜-redis를-사용하는가"><strong>왜 Redis를 사용하는가?</strong></h3>
<ul>
<li><p>NoSQL 스토리지 시스템은, 수평적 확장성과 속도를 위해 기존 RDBMS에 대한 대안을 제공.</p>
</li>
<li><p>그 중 Redis는 Spring Data 가 지원하는 Key-Value 저장소 중 하나이다. 그리고 다른 NoSQL 들이 disk 나 SSD 에 저장하는 것과 달리, Redis 는 in-memory 에 저장한다. </p>
</li>
<li><p>in-memory 저장 덕분에, Redis 는 데이터 접근 속도가 매우 매우 빠르다 (ms 단위라고 함).</p>
</li>
</ul>
<p><strong>Memory SnapShot</strong></p>
<p>메모리에 있는 데이터를 스냅샷을 찍어 통째로 디스크에 옮겨준다. 그리고 필요할 때 다시 메모리에 올려준다.</p>
<hr>
<h3 id="redis-설정">Redis 설정</h3>
<p>Redis 설치 완료된 상태에서</p>
<p>(Redis Window 설치 - <a href="https://aljjabaegi.tistory.com/616">https://aljjabaegi.tistory.com/616</a>)</p>
<ul>
<li><strong>build.gradle 설정</strong></li>
</ul>
<pre><code class="language-java">// redis
implementation &#39;org.springframework.boot:spring-boot-starter-data-redis&#39;</code></pre>
<ul>
<li><strong>Redis에 연결</strong></li>
</ul>
<p>Redis와 Spring을 사용할 때 첫 번째 작업은, IoC 컨테이너를 통해 저장소를 연결한다.</p>
<p>RedisConnection 은 RedisConnectionFactory 를 통해 생성된다.</p>
<p>RedisConnectionFactory 는 크게 2가지 방식으로 구현된다. 하나는 Jedis , 나머지 하나는 Lettuce Redice Clients 이다. 이 중 Jedis 는 Redis 공식 문서에 Java 추천 방법으로 나와있다.</p>
<p>하지만 Lettuce 가 SPring Data Redis 를 구현하고, thread-safe , asynchronous 하기 때문에 reactive applications 에서 쓰기 좋다.</p>
<ul>
<li><strong>Redis Configuration</strong></li>
</ul>
<p>다음은 Lettuce 연결 팩토리를 생성하는 방법이다</p>
<pre><code class="language-java">@Configuration
class AppConfig {

    private String redisHost = ...;
    private int redisPort = ...;

    // Creating Connection with Redis
  @Bean
  public LettuceConnectionFactory redisConnectionFactory() {

    return new LettuceConnectionFactory(new RedisStandaloneConfiguration(redisHost, redisPort));
  }

    // Creating RedisTemplate for Entity &#39;Employee&#39;
    @Bean
  public RedisTemplate&lt;String, Employee&gt; redisTemplate() {
      RedisTemplate&lt;String, Employee&gt; redisTemplate = new RedisTemplate&lt;&gt;();
      redisTemplate.setConnectionFactory(redisConnectionFactory());
      return redisTemplate;
  }
}</code></pre>
<p>Employee 를 Entity 로 사용할 것이므로 다음과 같이 RedisTemplate 를 명시해주자. (Employee 대신 Object 로 할 경우도 상관 X)</p>
<h3 id="redis-를-이용한-crud-예제"><strong>Redis 를 이용한 CRUD 예제</strong></h3>
<p>redis 를 이용해 CRUD 할 Entity 를 먼저 만들자</p>
<pre><code class="language-java">@Data
@NoArgsConstructor
@AllArgsConstructor
// No @Entity concept here
public class Employee implements Serializable {

    private static final long serialVersionUID = -7817224776021728682L;

    private Integer empId;
    private String empName;
    private Double empSalary;
}</code></pre>
<p>Redis 를 사용하기 때문에 @Entity 를 사용하지 않는다. 하지만 Redis 의 엔티티로 사용하려면 <strong>Serializable 인터페이스를 꼭 구현해야한다. (Employee 데이터를 하나의 Employee 테이블 같이 묶기 위해서 필요하다.)</strong></p>
<p>Serializable 을 안 쓴다면 Key 값에 Employee 들을 구분하기 위해 <strong>Employee:id</strong> 이렇게 넣어야한다.</p>
<hr>
<pre><code class="language-java">public interface EmployeeRepository {

    void saveEmployee(Employee emp);
    Employee getOneEmployee(Integer id);
    void updateEmployee(Employee emp);
    Map&lt;Integer, Employee&gt; getAllEmployees();
    void deleteEmployee(Integer id);
    void saveAllEmployees(Map&lt;Integer, Employee&gt; map);
}</code></pre>
<p>Employee 에서 사용할 DB 기능들을 정리해 놓은 인터페이스</p>
<hr>
<pre><code class="language-java">@Repository
public class EmployeeRepositoryImpl implements EmployeeRepository{

    private final String hashReference = &quot;Employee&quot;;

    @Resource(name = &quot;redisTemplate&quot;) // 빨간 줄 무시
    private HashOperations&lt;String, Integer, Employee&gt; hashOperations;

    @Override
    public void saveEmployee(Employee emp) {
        hashOperations.putIfAbsent(hashReference, emp.getEmpId(), emp);
    }

    @Override
    public void saveAllEmployees(Map&lt;Integer, Employee&gt; map) {
        hashOperations.putAll(hashReference, map);
    }

    @Override
    public Employee getOneEmployee(Integer id) {
        return hashOperations.get(hashReference, id);
    }

    @Override
    public void updateEmployee(Employee emp) {
        hashOperations.put(hashReference, emp.getEmpId(), emp);
    }

    @Override
    public Map&lt;Integer, Employee&gt; getAllEmployees() {
        return hashOperations.entries(hashReference);
    }

    @Override
    public void deleteEmployee(Integer id) {
        hashOperations.delete(hashReference, id);
    }
}</code></pre>
<p><strong>@Resource</strong> 를 이용해서 생성한 redisTemplate 빈을 이름으로 찾는다. (@Autowired 와의 차이점) 그리고  HashOperations 에 주입시켜준다. (원래는 <code>redisTemplate.opsForHash()</code> 로 생성 가능)</p>
<hr>
<pre><code class="language-java">@Component
public class RedisOperationsRunner implements CommandLineRunner {

    @Autowired
    private EmployeeRepository employeeRepository;

    @Override
    public void run(String... args) throws Exception {

        //saving one employee
        employeeRepository.saveEmployee(new Employee(500, &quot;Emp0&quot;, 2150.0));

        //saving multiple employees
        employeeRepository.saveAllEmployees(
                Map.of( 501, new Employee(501, &quot;Emp1&quot;, 2396.0),
                        502, new Employee(502, &quot;Emp2&quot;, 2499.5),
                        503, new Employee(503, &quot;Emp4&quot;, 2324.75)
                )
        );

        //modifying employee with empId 503
        employeeRepository.updateEmployee(new Employee(503, &quot;Emp3&quot;, 2325.25));

        //deleting employee with empID 500
        employeeRepository.deleteEmployee(500);

        //retrieving all employees
        employeeRepository.getAllEmployees().forEach((k,v)-&gt; System.out.println(k +&quot; : &quot;+v));

        //retrieving employee with empID 501
        System.out.println(&quot;Emp details for 501 : &quot;+employeeRepository.getOneEmployee(501));
    }
}</code></pre>
<p>CommandLineRunner 를 구현하면 Spring Application 에 포함되는 순간 자동 실해될 수 있도록 해준다.</p>
<p>순서는 @Ordered 어노테이션을 이용해 지정할 수 있다.</p>
<p>🥳실행 결과</p>
<p><img src="https://velog.velcdn.com/images/anak_2/post/33224026-72a8-4701-816b-e854142f569c/image.png" alt=""></p>
<p>지금까지 Redis 를 스프링 부트에서 사용하는 방법에 대해 알아봤다.</p>
<p>이를 이용해 JWT 토큰 관리를 Redis 로 하도록 바꿔보는 토이 프로젝트를 진행해보자</p>
<p>더 다양한 예제 - <a href="https://blog.kingbbode.com/25">https://blog.kingbbode.com/25</a></p>
<h3 id="redis-cli-명령어-정리">Redis cli 명령어 정리</h3>
<p>출처 - <a href="https://freeblogger.tistory.com/10">https://freeblogger.tistory.com/10</a></p>
<p>공식 문서 - <a href="https://redis.io/docs/ui/cli/">https://redis.io/docs/ui/cli/</a></p>
<h3 id="마주했던-오류들">마주했던 오류들</h3>
<ul>
<li>Reason: Failed to determine a suitable driver class</li>
</ul>
<p>Spring Boot 는 프로젝트 시작 시 연결할 수 있는 DB 가 있나 체크한다. 그러므로 아무 DB라도 연결해놓아야 한다</p>
<ul>
<li>Unauthorized: 401</li>
</ul>
<p>Spring Security 를 포함해서 빌드했는데 Spring Boot 를 실행할 때 자동으로 실행돼서 요청 URL 에 대해 인증이 없던 상태라 발생한 오류였다.</p>
<p>참고 :</p>
<p> <a href="https://javatechonline.com/spring-boot-redis-crud-example/#Jedis_Connector_JedisConnectionFactory">https://javatechonline.com/spring-boot-redis-crud-example/#Jedis_Connector_JedisConnectionFactory</a></p>
<p><a href="https://www.woolog.dev/backend/spring-boot/spring-boot-redis-dessert-simple/">https://www.woolog.dev/backend/spring-boot/spring-boot-redis-dessert-simple/</a></p>
<p>Redis 공식 - <a href="https://redis.io/docs/clients/java/">https://redis.io/docs/clients/java/</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TestConfiguration 사용하기, @Configuration 과 차이]]></title>
            <link>https://velog.io/@anak_2/TestConfiguration-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0-Configuration-%EA%B3%BC-%EC%B0%A8%EC%9D%B4</link>
            <guid>https://velog.io/@anak_2/TestConfiguration-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0-Configuration-%EA%B3%BC-%EC%B0%A8%EC%9D%B4</guid>
            <pubDate>Tue, 29 Aug 2023 12:18:47 GMT</pubDate>
            <description><![CDATA[<h4 id="🚩이-글의-목표">🚩이 글의 목표</h4>
<h4 id="testconfiguration-사용법을-익히고-configuration-과-차이에-대해-이해하고-대신-사용하는-이유에-대한-설명"><strong>@TestConfiguration 사용법을 익히고, @Configuration 과 차이에 대해 이해하고, 대신 사용하는 이유에 대한 설명</strong></h4>
<hr>
<p>Test 에 사용할 클래스를 만들 때는 빈으로 주입 받거나, 객체로 직접 생성해야한다.</p>
<p>다음 방법들을 통해 어떻게 클래스의 인스턴스를 만들어서 사용할지 살펴보자</p>
<p><img src="https://velog.velcdn.com/images/anak_2/post/22161869-c721-4cb1-af3a-6d8f7dd96033/image.png" alt=""></p>
<p>위와 같은 구조의 프로젝트일 때, Test 클래스에서 <strong>MemberFacade</strong> 를 생성하려면 MemberService 와 LogRepository 를 주입 받아야한다.</p>
<h3 id="매-테스트마다-memberfacade-객체-생성하기">매 테스트마다 MemberFacade 객체 생성하기</h3>
<p>딱 봐도 비효율 적이다! 이런 보일러 플레이트는 지양하자</p>
<h3 id="memberfacde-클래스에-component-를-붙여서-스프링-부트가-빈으로-생성하게-시키기">MemberFacde 클래스에 @Component 를 붙여서 스프링 부트가 빈으로 생성하게 시키기</h3>
<pre><code class="language-java">@RequiredArgsConstructor
@Slf4j
@Component // 빈으로 생성하도록 표시
public class MemberFacade {
    private final MemberService memberService;
    private final LogRepository logRepository;
        ...
}</code></pre>
<pre><code class="language-java">// Test Class
        ...
    @Autowired
    MemberService memberService;
    @Autowired
    MemberRepository memberRepository;
    @Autowired
    LogRepository logRepository;
    @Autowired
    MemberFacade memberFacade;
        ...</code></pre>
<p>이 방법에 문제는 없지만, </p>
<p><strong>단위 테스트</strong>를 위해선 외부에 의존성을 끊어야하기 때문에 좋은 방법이 아니다.</p>
<p>그리고 생성된 Bean의 재정의가 불가능하다. (@Configuration 과 @ComponenetScan 차이!)</p>
<p>또한 스프링 컨테이너가 너무 많은 빈을 관리하게 되면, 매번 빈을 호출할 때 더 많은 빈을 순회해야 되므로 성능상 안 좋을 것이라 예상된다.</p>
<h3 id="testconfiguration-으로-테스트에서-빈-생성하도록-설정">@TestConfiguration 으로 테스트에서 빈 생성하도록 설정</h3>
<pre><code class="language-java">// Test Class
@Slf4j
@SpringBootTest
public class MyTest{
        @Autowired
        MemberService memberService;
        @Autowired
        MemberRepository memberRepository;
        @Autowired
        LogRepository logRepository;
        @Autowired
        MemberFacade memberFacade;

        @TestConfiguration // 빈으로 생성하도록 설정 표시
        public static class Config{

            @Bean
            public static MemberFacade makeMemberFacade(MemberService memberService, LogRepository logRepository){
                return new MemberFacade(memberService, logRepository);
            };
        }
        ...
}</code></pre>
<p>위 방법은 Test 할 시에만 빈을 생성해서 관리하기 때문에 기존 애플리케이션 성능에도 지장없이 테스트할 수 있다.</p>
<p>또한 @TestConfiguration 을 이용한다면, 외부와 연결도 끊은 mock service 를 사용할 수 있다.</p>
<p>⚠️주의</p>
<p><strong>@Configuration 을 static 내부 클래스로  빈 생성 시</strong> MemberService, LogRepository 빈을 찾을 수 없다는 오류가 발생한다. </p>
<p>이는 <strong>@TestConfiguration</strong> 은 @TestComponent를 정의했기 때문에 Spring Boot 의 comonent scaning 에서 제외되기 때문이다. </p>
<pre><code class="language-java">@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
@TestComponent
public @interface TestConfiguration {
}</code></pre>
<p>@Configuration 을 사용한다면 다음과 같이 외부로 클래스를 빼줘야한다 </p>
<pre><code class="language-java">@Configuration
class Config{
    @Bean
    public MemberFacade makeMemberFacade(MemberService memberService, LogRepository logRepository){
        return new MemberFacade(memberService, logRepository);
    };
}

@Slf4j
@SpringBootTest
@Import(Config.class)
public class MyTest{

    @Autowired
    MemberService memberService;
    @Autowired
    MemberRepository memberRepository;
    @Autowired
    LogRepository logRepository;
    @Autowired
    MemberFacade memberFacade;
        ...
}</code></pre>
<p>참고: <a href="https://reflectoring.io/spring-boot-testconfiguration/">https://reflectoring.io/spring-boot-testconfiguration/</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Sprint Data JPA 와 Querydsl 이용해서 Custom Repository 만들기]]></title>
            <link>https://velog.io/@anak_2/Sprint-Data-JPA-%EC%99%80-Querydsl-%EC%9D%B4%EC%9A%A9%ED%95%B4%EC%84%9C-Custom-Repository-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@anak_2/Sprint-Data-JPA-%EC%99%80-Querydsl-%EC%9D%B4%EC%9A%A9%ED%95%B4%EC%84%9C-Custom-Repository-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Sun, 20 Aug 2023 15:34:04 GMT</pubDate>
            <description><![CDATA[<h2 id="custom-repository-">Custom Repository ?</h2>
<p> Spring Data Jpa 가 제공하는 기본 쿼리 메서드들 이외에도 복잡한 쿼리는 우리가 직접 작성해야할 일이 많다. 그렇다면 Service 에선 어떤 Repository 를 싱글톤으로 생성해야 할까?</p>
<p><strong>인터페이스는 다중 상속이 가능하다는 성질을 이용하자!</strong></p>
<p> 기존 JpaRepsitory 를 상속받는 Repository 에서 추가로 Custom Repository 를 상속받을 수 있도록 할 수 있다.</p>
<p>아래 그림을 보고 구조를 눈에 익히자</p>
<p><img src="https://velog.velcdn.com/images/anak_2/post/feab7972-2c48-48f7-8597-a0c4b53983c1/image.png" alt=""></p>
<ul>
<li><strong>Custom Repository 약속</strong></li>
</ul>
<p>일종의 약속인데, 추가로 상속 받을 인터페이스를 구현하는 클래스는 <code>_____Impl</code> 이란 이름을 가진다.</p>
<p>이를 안 지킬 시 다음과 같은 오류를 맞닥뜨린다.</p>
<blockquote>
</blockquote>
<p>Caused by: java.lang.IllegalArgumentException: Failed to create query for method public abstract java.util.List hello.itemservice.repository.custom.ItemRepositoryCustom.findItemByCondition(hello.itemservice.repository.ItemSearchCond)! No property &#39;condition&#39; found for type &#39;Item&#39;!</p>
<p>또한 인터페이스와 구현 클래스는 <code>_____Impl</code> 부분을 제외하곤 똑같도록 만들어줘야한다.</p>
<p>뒤에 Impl 대신 다른 것으로 붙이려면</p>
<pre><code class="language-java">@SpringBootApplication
@EnableJpaRepositories(repositoryImplementationPostfix = &quot;Default&quot;)
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}</code></pre>
<p><strong>@EnableJpaRepositories(repositoryImplementationPostfix=”원하는 접미사”)</strong> 로 설정하면 된다</p>
<h2 id="customrepository-적용해보기">CustomRepository 적용해보기</h2>
<pre><code class="language-java">public interface JpaItemRepository extends MyItemRepository, JpaRepository&lt;Item, Long&gt; {

    Optional&lt;Item&gt; findById(Long id);

}</code></pre>
<p>JpaItemRepository 는 JpaRepository 이외에 커스텀으로 만든MyItemRepository 또한 상속 받을 것이다</p>
<pre><code class="language-java">public interface MyItemRepository {

    List&lt;Item&gt; findAll(ItemSearchCond cond);

    void update(Long itemId, ItemUpdateDto updateParam);
}</code></pre>
<p>Custom Repository 의 인터페이스, 구현해야 할 명세서 역할</p>
<pre><code class="language-java">@Transactional
public class MyItemRepositoryImpl implements MyItemRepository {

    private final EntityManager em;
    private final JPAQueryFactory query; // Querydsl 을 위한 변수

    public MyItemRepositoryImpl(EntityManager em){
        this.em = em;
        query = new JPAQueryFactory(em);
    }

    public BooleanExpression likeName(String itemName){

        if(StringUtils.hasText(itemName)){
            return QItem.item.itemName.contains(itemName);
        }
        return null;
    }

    public BooleanExpression maxPrice(Integer maxPrice){

        if(maxPrice != null){
            return QItem.item.price.loe(maxPrice);
        }
        return null;
    }

    @Override
    public List&lt;Item&gt; findAll(ItemSearchCond cond) {
            String itemName = cond.getItemName();
            Integer maxPrice = cond.getMaxPrice();

            QItem item = QItem.item;

            List&lt;Item&gt; items = query
                    .select(item)
                    .from(item)
                    .where(likeName(itemName), maxPrice(maxPrice))
                    .fetch();

            return items;
    }

    @Override
    public void update(Long itemId, ItemUpdateDto updateParam) {
        Item findItem = em.find(Item.class, itemId);
        findItem.setItemName(updateParam.getItemName());
        findItem.setPrice(updateParam.getPrice());
        findItem.setQuantity(updateParam.getQuantity());
    }
}</code></pre>
<p>Custom Repository 를 구현하는 클래스. 반드시 인터페이스와 <code>_____Impl</code> 을 제외하고는 이름을 맞춰주자. (안 맞춰줘서 Querydsl 빈 생성 오류로 꽤나 고생했다)</p>
<h2 id="custom-basic-repository-만들기">Custom Basic Repository 만들기</h2>
<p>Custom Basic Repository 는 CrudRepository 메서드를 오버라이드 하는 것이다.</p>
<p>나만의 save() 메서드를 커스텀해보면서 익숙해지자</p>
<pre><code class="language-java">public interface MyBasicRepository&lt;T&gt; {
    &lt;S extends T&gt; S save (S entity);
}</code></pre>
<p>메서드를 정의할 인터페이스</p>
<pre><code class="language-java">@RequiredArgsConstructor
@Transactional
public class MyBasicRepositoryImpl&lt;T&gt; implements MyBasicRepository&lt;T&gt;{

    private final EntityManager entityManager;

    @Override
    public &lt;S extends T&gt; S save(S entity) {
        System.out.println(&quot;============== Customized Save ==============&quot;);
        entityManager.persist(entity);
        entityManager.flush();
        return entity;
    }
}</code></pre>
<p>인터페이스에 정의한 메서드를 구현할 클래스</p>
<pre><code class="language-java">public interface JpaItemRepository extends MyItemRepository, MyBasicRepository&lt;Item&gt;, JpaRepository&lt;Item, Long&gt; {

    Optional&lt;Item&gt; findById(Long id);

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

<p>** 주의할 점 ⚠️ **
인터페이스를 상속할 때 제네릭 타입을 구체적으로 명시해줘야 한다</p>
<p>인터페이스를 기존 JpaRepository&lt;Item, Long&gt;과 같이 상속하기 때문에 Custom Basic Repository 도 제네릭 타입을 구체적으로 명시해줘야한다.</p>
<p>아니면 이런 오류 발생</p>
<blockquote>
<p>`save(S)&#39; in &#39;hello.itemservice.repository.custom.MyBasicRepository&#39; clashes with &#39;save(S)&#39; in &#39;org.springframework.data.repository.CrudRepository&#39;; both methods have same erasure, yet neither overrides the other</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/anak_2/post/6000b278-a717-4907-b0cd-d772f3bd7d20/image.png" alt=""></p>
<p><code>==== customized save ====</code> 가 출력되는 것 확인 가능!</p>
<p>** 깊게 공부하려다 뻘짓으로 된 여담 **</p>
<blockquote>
<p>⚠️ CRUD Repository 쿼리 메서드인 <strong>findById</strong> 도 오버라이딩 할려 시도했다. 
하지만 CRUD repository 와 Custom Repository 가 <strong>동일한 이름의 메서드지만 다른 반환, 매개변수 타입을 가져기 때문에 발생한 자바 컴파일 오류인 “ambiguous method” </strong>도 있었고, 
이 모호성을 피하기 위해 CRUD repository 의 findById 와 동일한 매개변수로 만들도록 제네릭 타입을 사용, Spring Data JPA repo 에 findById 생성했지만, 이를 구현하는 클래스에서 데이터 접근 기술 들 중 매개변수로 Class&lt;T&gt; 를 받는 메소드들이 많아서 포기! 
(ex. entitymanager.find(Class&lt;T&gt; entityClass, Object primaryKey) )</p>
</blockquote>
<blockquote>
<p>중간에 포기한 이유는, 기존 CRUD repository의 쿼리 메서드를 재선언 하는 것은 좋은 방법이 아니기 때문이다. 개발을 편하게 하기 위해 쿼리 메서드가 생긴 것인데 오버라이드 하지 말고, 만약 정말 커스텀한 <strong>findById</strong> 기능이 필요하다면 메서드명을 <strong>myFindById</strong> 이렇게 바꾼 것을 사용하자.</p>
</blockquote>
<p><a href="https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#repositories.custom-implementations">출처 - Spring Data JPA - Reference Documentation</a></p>
<p><a href="https://www.petrikainulainen.net/programming/spring-framework/spring-data-jpa-tutorial-adding-custom-methods-into-all-repositories/">참고 - Spring Data JPA Tutorial: Adding Custom Methods to All Repositories</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Error creating bean with name 'querydslPredicateArgumentResolver']]></title>
            <link>https://velog.io/@anak_2/Error-creating-bean-with-name-querydslPredicateArgumentResolver</link>
            <guid>https://velog.io/@anak_2/Error-creating-bean-with-name-querydslPredicateArgumentResolver</guid>
            <pubDate>Sun, 20 Aug 2023 14:42:10 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>에러 발생 원인 ❗
Querydsl 빈을 생성하는데 문제가 발생했다 (다양한 원인 존재)</p>
</blockquote>
<p>나의 경우 JpaRepository 와 함께 Querydsl 을 이용한 CustomRepostitory 를 상속 받도록 만들다 발생했다.</p>
<p>구현 클래스와 인터페이스의 이름을 맞추지 않아서 빈 생성할 때 꼬였나보다.</p>
<pre><code class="language-java">public interface MyItemRepository {

    List&lt;Item&gt; findAll(ItemSearchCond cond);

    void update(Long itemId, ItemUpdateDto updateParam);
}</code></pre>
<p style="color:red"><strong>아래 코드가 문제 발생했던 원인!</strong></p>

<pre><code class="language-java">@Transactional
public class MyRepositoryImpl implements MyItemRepository {
    ...
}</code></pre>
<p style="color:red">해결: MyRepositoryImpl -> MyItemRepositoryImpl 로 바꿔서 구현 클래스와 인터페이스가 이름이 일치하도록 만들어주자</p>

<pre><code class="language-java">public interface JpaItemRepository extends MyItemRepository, JpaRepository&lt;Item, Long&gt; {

    Optional&lt;Item&gt; findById(Long id);

}</code></pre>
<p><strong>위에 문제 말고도 CustomRepository 의 끝 (접미사) 을 Impl 로 맞추지 않아서 발생하기도 한다.</strong></p>
<p><strong>전체 에러 내용은 다음과 같다</strong>
(하나가 꼬이니까 뒤에 모든 빈 생성이 꼬여서 오류가 복잡하다)</p>
<blockquote>
<p><strong>Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name &#39;querydslPredicateArgumentResolver&#39;</strong> defined in class path resource [org/springframework/data/web/config/QuerydslWebConfiguration.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.data.web.querydsl.QuerydslPredicateArgumentResolver]: Factory method &#39;querydslPredicateArgumentResolver&#39; threw exception; nested exception is org.springframework.beans.factory.<strong>BeanCreationException: Error creating bean with name &#39;querydslBindingsFactory&#39; defined in class path resource ** 
[org/springframework/data/web/config/QuerydslWebConfiguration.class]: Initialization of bean failed; nested exception is org.springframework.beans.factory.</strong>BeanCreationException: Error creating bean with name &#39;jpaItemRepository&#39;** defined in hello.itemservice.repository.custom.JpaItemRepository defined in @EnableJpaRepositories declared on JpaRepositoriesRegistrar.EnableJpaRepositoriesConfiguration: Invocation of init method failed; nested exception is org.springframework.data.repository.query.QueryCreationException: Could not create query for public abstract java.util.List hello.itemservice.repository.custom.MyItemRepository.findAll(hello.itemservice.repository.ItemSearchCond)! Reason: Failed to create query for method public abstract java.util.List hello.itemservice.repository.custom.MyItemRepository.findAll(hello.itemservice.repository.ItemSearchCond)! No property &#39;findAll&#39; found for type &#39;Item&#39;!; nested exception is java.lang.IllegalArgumentException: Failed to create query for method public abstract java.util.List hello.itemservice.repository.custom.MyItemRepository.findAll(hello.itemservice.repository.ItemSearchCond)! <strong>No property &#39;findAll&#39; found for type &#39;Item&#39;!</strong></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[No qualifying bean of type 'com.querydsl.jpa.impl.JPAQueryFactory' available]]></title>
            <link>https://velog.io/@anak_2/No-qualifying-bean-of-type-com.querydsl.jpa.impl.JPAQueryFactory-available</link>
            <guid>https://velog.io/@anak_2/No-qualifying-bean-of-type-com.querydsl.jpa.impl.JPAQueryFactory-available</guid>
            <pubDate>Sun, 20 Aug 2023 14:20:54 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>에러 발생 이유 ❗
JPAQuery 빈을 생성하기 위한 JPAQueryFactory 클래스를 찾지 못했다</p>
</blockquote>
<p>** 내 코드에서 발생했던 문제 **
평소 @RequiredArgsConstructor 로 생성자 주입으로 받는 습관 때문에 JPAQueryFactory 도 똑같이 스프링이 주입한다 생각했다.</p>
<p>하지만 JPAQueryFactory 를 생성할 때 EntityManager 를 매개변수로 넘겨줘야 하므로 생성자로 EntityManager 를 주입받고 생성하도록 하자</p>
<h2 id="해결-방법">해결 방법</h2>
<pre><code class="language-java">@Repository
@Transactional
public class MyItemRepositoryImpl implements MyItemRepository {

    private final EntityManager em;
    private final JPAQueryFactory query;

    public MyItemRepositoryImpl(EntityManager em){
        this.em = em;
        query = new JPAQueryFactory(em);
    }
    ...</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[Test 에서 Lombok 사용]]></title>
            <link>https://velog.io/@anak_2/Test-%EC%97%90%EC%84%9C-Lombok-%EC%82%AC%EC%9A%A9</link>
            <guid>https://velog.io/@anak_2/Test-%EC%97%90%EC%84%9C-Lombok-%EC%82%AC%EC%9A%A9</guid>
            <pubDate>Sat, 19 Aug 2023 05:38:31 GMT</pubDate>
            <description><![CDATA[<p>Gradle 5.4.1 이상 부터 보이는 오류라한다.</p>
<p>기본 파일과 다르게 테스트 파일에서 lombok을 사용하려면 build.gradle에 
다음 설정을 추가해줘야한다</p>
<pre><code class="language-java">//    lombok 설정
    compileOnly &#39;org.projectlombok:lombok&#39;
    annotationProcessor &#39;org.projectlombok:lombok&#39;
    testCompileOnly &#39;org.projectlombok:lombok&#39;
    testAnnotationProcessor &#39;org.projectlombok:lombok&#39;</code></pre>
<pre><code class="language-java">// 정상 작동하는 Test 파일
@DataJpaTest
@ExtendWith(SpringExtension.class)
@Slf4j
class MyTest {</code></pre>
<p>출처 <a href="https://stackoverflow.com/questions/59426699/junit-not-working-with-lombok-annotation-processing-doesnt-seem-to-work-for-t">JUnit not working with Lombok</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Notion 페이지 내 링크 (Anchor Link) 만들기]]></title>
            <link>https://velog.io/@anak_2/Notion-%ED%8E%98%EC%9D%B4%EC%A7%80-%EB%82%B4-%EB%A7%81%ED%81%AC-Anchor-Link-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@anak_2/Notion-%ED%8E%98%EC%9D%B4%EC%A7%80-%EB%82%B4-%EB%A7%81%ED%81%AC-Anchor-Link-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Mon, 14 Aug 2023 06:44:56 GMT</pubDate>
            <description><![CDATA[<blockquote>
<h3 id="왜-필요한가-❓">왜 필요한가 ❓</h3>
<p>글 까지 써가며 정리할 기능은 아니라 생각할 수 있지만, 
요즘 노션을 자주 작성하면서 긴 페이지를 보기 쉽게 정리하고 싶었다. 
원하는 블럭 위치로 링크를 걸어서 바로 내용을 볼 수 있도록 실습해보자!</p>
</blockquote>
<h3 id="목표-index-토글-에서-원하는-목차로-이동하는-링크-만들기">목표: Index 토글 에서 원하는 목차로 이동하는 링크 만들기</h3>
<hr>
<p><img src="https://velog.velcdn.com/images/anak_2/post/dddaed17-85b4-43b4-9866-b205ab9f0316/image.png" alt="">
<strong>1. &quot;MyBatis 적용&quot; 을 클릭시 MyBatis 적용 블럭 위치로 이동하도록 만들것이다.</strong></p>
<p><img src="https://velog.velcdn.com/images/anak_2/post/12746bd7-fe62-49f0-892f-9013d3f87d21/image.png" alt="">
<strong>2. 이동할 블럭의 설정에서 &quot;Copy link to block&quot; 클릭</strong>
<img src="https://velog.velcdn.com/images/anak_2/post/06479d51-42fe-4c71-a590-52e510cf1b3b/image.png" alt="">
<strong>3. 링크를 걸어줄 블럭에 &quot;↗️Link&quot; 클릭하기</strong>
<img src="https://velog.velcdn.com/images/anak_2/post/edc58134-bc77-4ca7-afd0-c60814d95f43/image.png" alt="">
<strong>4. 복사한 주소 붙여넣기 </strong>
<img src="https://velog.velcdn.com/images/anak_2/post/f338f403-7a36-4ad9-a2db-4675f8035492/image.png" alt="">
<strong>5. 결과 모습</strong></p>
<p><a href="https://notioninvoice.com/blog/anchor-links-in-a-notion-page">출처: Anchor links in a Notion page</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Java annotations 이란? 동작 원리 설명 + 활용 (커스텀 어노테이션)]]></title>
            <link>https://velog.io/@anak_2/Java-annotations-%EC%9D%B4%EB%9E%80-%EC%84%A4%EB%AA%85-%ED%99%9C%EC%9A%A9</link>
            <guid>https://velog.io/@anak_2/Java-annotations-%EC%9D%B4%EB%9E%80-%EC%84%A4%EB%AA%85-%ED%99%9C%EC%9A%A9</guid>
            <pubDate>Sun, 13 Aug 2023 12:05:48 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>이 글의 목표 🚩
annotation 이 무엇인지, 어떻게 작동하는지 이해
annotation 속성 공부
annotation 커스텀 해보기</p>
</blockquote>
<h3 id="annotation-을-이해해야-하는-이유-📖">Annotation 을 이해해야 하는 이유 📖</h3>
<hr>
<p> <strong>Java Annotation</strong> 을 처음 배울 때는 @Override 같이 소스 코드에 영향을 주지 않는 메타데이터 정보를 추가하는 정도라 생각했다. </p>
<p> 하지만, 스프링 공부를 할 수록 Annotation 만 붙이면 Validation 검사나 WebServlet 자동 매핑 getter, setter 자동 생성 등 <strong>보일러 플레이트</strong>를 획기적으로 줄여주는 역할을 해주면서, 마법같은 일이 벌어졌다.</p>
<p> 원리를 알기 위해 정의한 파일에 들어가 코드를 봐도 은닉된 코드가 많았기 때문에 해석하기에 난해했다. 그래서 컴파일 시간에 무슨 일이 일어나는지 대충 알고 넘어갔다.</p>
<p> 하지만 자바 공부를 할 수록 더 다양한 어노테이션이 등장했고, 심지어 <strong>@Benchmark</strong> 같이 성능 측정하는 곳에서도 다양하게 쓰이는 것을 보고, CS 지식도 중요하지만, 내 코드가 뒤에서 어떻게 작동하는지 이해하는 것이 더 필요하다 생각하여, 이번 기회에 정리하고 지나가고자 한다.</p>
<h2 id="annotation-이해와-작동-원리">Annotation 이해와 작동 원리</h2>
<hr>
<p>자바 어노테이션은 JDK5에 나온 문법으로, 메타데이터 (information)를 우리의 소스 코드 (클래스나 메서드 등) 에 붙이는 용도로 XML 주석이나 Marker Interface 를 대체하는 역할이다.</p>
<ul>
<li><p>Annotation은 ‘@’ 로 시작한다</p>
</li>
<li><p>Annotation은 컴파일된 프로그램을 바꾸지 않는다</p>
</li>
<li><p>Annotation은 클래스 이름 앞에 @interface를 붙여서 생성할 수 있다.</p>
<p>  Example: <code>{접근제어자} @interface {클래스 이름} {}</code> 으로 생성 가능하다</p>
</li>
</ul>
<p>Annotation 을 정의해서 클래스, 메서드, 필드 등에 붙이면, getClass() 로 런타임 클래스를 가져와 리플렉션을 이용해 Annotation이 있는지 없는지에 따라 처리하는 로직을 생성해서 Annotation을 처리한다. 
무슨 말인지 밑에 커스텀 어노테이션 부분에서 느낌을 받아보자.</p>
<h3 id="annotation-카테고리"><strong>Annotation 카테고리</strong></h3>
<ol>
<li>Marker Annotations</li>
<li>Single value Annotations</li>
<li>Full Annotations</li>
</ol>
<p><strong>Category 1: Marker Annotations</strong></p>
<p>매개변수가 없이 마크 표시만 하는 Annotation을 의미한다. 필요한 곳에 적기만 하면 충분하다. <strong>@Override</strong> 가 예시이다</p>
<p><strong>Category 2: Single value Annotations</strong></p>
<p>하나의 멤버를 가지는 Annotation을 의미한다. 사용할 때 멤버에 값을 넣어줘야 한다. 값을 넣을 멤버 이름은 생략 가능하다.</p>
<p>Example: <code>@TestAnnotation(&quot;testing&quot;);</code></p>
<p><strong>Category 3: Full Annotations</strong></p>
<p>여러 개의 멤버를 가지는 Annotation을 의미한다</p>
<p>Example: <code>@TestAnnotation(owner=&quot;kim&quot;, value=&quot;developer&quot;);</code></p>
<h2 id="annotation-속성-공부">Annotation 속성 공부</h2>
<p><img src="https://velog.velcdn.com/images/anak_2/post/a7ba2091-97cc-47e5-b7b5-cd7c112a6e63/image.png" alt=""></p>
<p>위 그림에서 <strong>Meta Annotations</strong> 은 어노테이션을 만들기 위한 어노테이션이다.</p>
<h3 id="target"><strong>@Target</strong></h3>
<p>Annotation이 어디에 위치할 수 있는지 제한해준다 (class, interface, constructor, etc.</p>
<p><img src="https://velog.velcdn.com/images/anak_2/post/be2d6617-bba0-4aff-8658-966a688ea151/image.png" alt=""></p>
<pre><code class="language-java">@Target(ElementType.TYPE)
@interface CustomAnnotation {}</code></pre>
<p>위의 경우 CustomAnnotation 은 Class, Interface, Enumeration에만 붙일 수 있다.</p>
<pre><code class="language-java">package com.example.annotation.lombok;

import java.lang.annotation.*;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface ClassAnnotation {
    String value() default &quot;Can annotate a class&quot;;
}

@Target({ElementType.METHOD, ElementType.TYPE,
        ElementType.ANNOTATION_TYPE,
        ElementType.CONSTRUCTOR})
@Retention(RetentionPolicy.RUNTIME)
@interface MultipleElementTypeAnnotation {

    String value() default &quot;Can annotate a class, method, &quot;
            + &quot;annotation, or constructor&quot;;
}

@ClassAnnotation
public class LombokStudy {

    public static void main(String[] args) throws Exception {
        LombokStudy obj = new LombokStudy();

        Annotation a[] = obj.getClass().getAnnotations();

        System.out.println(a[0]);

        Class&lt;?&gt; className = Class.forName(&quot;com.example.annotation.lombok.LombokStudy&quot;);
        Annotation b[] = className.getMethod(&quot;myMethod&quot;)
                .getAnnotations();

        System.out.println(b[0]);
    }

// @ClassAnnotation 붙일 시 컴파일 오류 (빨간색 밑줄) 발생
    @MultipleElementTypeAnnotation
    public void myMethod() {
    }
}</code></pre>
<h3 id="retention">@Retention</h3>
<p>코드를 실행 할 때 언제 이 Annotation 이 없앨 지 결정하는 meta-annotation 이다.</p>
<ul>
<li><p><strong>@Retention(RetentionPolicy.SOURCE)</strong></p>
<p>  runtime 때 없앤다</p>
</li>
<li><p><strong>@Retention(RetentionPolicy.CLASS)</strong></p>
<p>  .class 파일엔 적혀있지만 runtime 때 없앤다. 특별히 Retention을 지정하지 않았다면 이게 Default</p>
</li>
<li><p><strong>@Retention(RetentionPolicy.RUNTIME)</strong></p>
<p>  runtime 때까지 접근할 수 있다. 위에 예시 <strong>코드에서 getAnnotations() 로 접근할 수 있는 이유이다</strong></p>
</li>
</ul>
<p>자주 사용하는 <strong>@Getter, @RequiredArgsConstructor</strong> 모두 SOURCE 에 해당한다.</p>
<p><img src="https://velog.velcdn.com/images/anak_2/post/31b3688c-2cf9-493d-b0cd-2b4979c8f2ad/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/anak_2/post/8a97a8e6-811c-464e-96f6-e0e01d5b0089/image.png" alt=""></p>
<p>그래서 다음과 같이 Customer 클래스가 선언됐을 때, build/classes 아래에 컴파일 된 <strong>.class 파일을 보면 Annotation 대신 추가된 코드가 적혀있는 것을 확인할 수 있다!</strong></p>
<pre><code class="language-java">@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
@interface RetentionClassAnnotation{
    String value() default &quot;Can not access during runtime&quot;;
}

public class LombokStudy {

    public static void main(String[] args) throws Exception {

        Class&lt;?&gt; className = Class.forName(&quot;com.example.annotation.lombok.LombokStudy&quot;);

        Annotation c[] = className.getMethod(&quot;myMethod2&quot;)
                .getAnnotations();

        System.out.println(c.length); // 0 출력
    }

    @RetentionClassAnnotation
    public void myMethod2(){

    }
}</code></pre>
<p>위와 같이 <strong>@RetentionClassAnnotation</strong> 을 만들었다면 .class 파일에선 @RetentionClassAnnotation 이 보이지만 runtime 때 사라져서 getAnnotations() 로 가져올 시 길이 0 을 출력한다 (반환 0개)</p>
<h3 id="suppresswarnins">@SuppressWarnins</h3>
<p>컴파일러의 경고를 억제하도록 알려주는 Annotation 이다.</p>
<p><a href="https://www.ibm.com/docs/ko/radfws/9.6.1?topic=code-excluding-warnings">IBM @SuppressWarnings 문서</a></p>
<h3 id="documented">@Documented</h3>
<p>Javadoc이 만들어질 때 Annotation 이 표시되도록 만드는 Annotation 이다.</p>
<p><a href="https://www.geeksforgeeks.org/java-documented-annotations/">geeksforgeeks @Documentation 예시</a></p>
<h3 id="inherited">@Inherited</h3>
<p>subclass 가 @Inherited를 붙인 Annotation을 상속받을 수 있도록 표시하는 Annotation 이다.</p>
<p><a href="https://www.geeksforgeeks.org/inherited-annotations-in-java/">geeksforgeeks @Inherited 예시</a></p>
<h2 id="annotation-커스텀-해보기">Annotation 커스텀 해보기</h2>
<hr>
<p><strong>만들어 볼 것</strong></p>
<p><code>@LogExecutionTime</code> 이라는 커스텀 어노테이션의 역할은, 이 어노테이션을 붙인 메서드의 실행시간을 측정하는 기능이다. 메서드의 실행 시점에 따라 처리가 필요하므로 Aspect 를 이용했다.</p>
<p>다음 절차대로 만들어보자</p>
<ol>
<li>Meta Annotation 을 이용해 Custom Annotation 만들자</li>
<li><code>@Aspect</code> 를 이용해 <code>@LogExecutionTime</code> 를 붙인 메서드의 로직을 처리하자</li>
<li><code>@LogExecutionTime</code> 을 붙인 메서드를 만들기 위해 스프링 빈을 생성하자</li>
<li>Custom Annotation을 붙인 메서드를 실행시키기 위해 <code>ApplicationContext</code> 를 이용해서 빈을 가져오자</li>
</ol>
<pre><code class="language-java">// LogExecutionTime.java

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogExecutionTime {
}
</code></pre>
<p><code>@LogExecution</code> 은 메서드에 붙이는 Annotation 으로 런타임 때 getAnnotation() 으로 접근할 수 있도록 Retention 설정 </p>
<pre><code class="language-java">// ExampleAspect.java

@Aspect
@Component
@Slf4j
public class ExampleAspect {

    @Around(&quot;@annotation(com.example.annotation.aop.LogExecutionTime)&quot;)
    public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {

        log.info(&quot;Call LogExecutionTime&quot;);

        long start = System.currentTimeMillis();

        Object proceed = joinPoint.proceed();

        long executionTime = System.currentTimeMillis() - start;

        log.info(joinPoint.getSignature() + &quot; executed in &quot; + executionTime + &quot;ms&quot;);

        return proceed;
    }
}</code></pre>
<p><code>ExampleAspect</code> 클래스는 <code>@LogExecution</code> 을 붙인 메서드를 AOP 를 이용해 로직을 처리하는 클래스이다. <code>@Component</code> 를 붙여서 빈으로 등록되도록 했다</p>
<pre><code class="language-java">// LombokStudy.java

// AOP 가 Spring Bean 에서 작동하기 때문에 Service 를 붙여서 빈으로 등록되도록 했다
@Service 
public class LombokStudy {

    @LogExecutionTime
    public void checkExecutionTime() throws InterruptedException {
        Thread.sleep(2000);
    }</code></pre>
<p><code>LombokStudy</code> 클래스는 <code>@LogExecutionTime</code> 을 사용해보는 메서드를 가지는 클래스이다.</p>
<pre><code class="language-java">// AnnotationApplicationTests.java

@SpringBootTest
class AnnotationApplicationTests {

    @Autowired
    ApplicationContext ac; 

    @Test
    void test1() throws Exception{
        LombokStudy lombokStudy = ac.getBean(&quot;lombokStudy&quot;, LombokStudy.class);
        lombokStudy.checkExecutionTime();
    }

}</code></pre>
<p>Test 해보기 위해 Test 클래스에서 진행했다. </p>
<p><strong>테스트 결과</strong>
<img src="https://velog.velcdn.com/images/anak_2/post/281abb3d-bd2b-481f-a556-ce093ff786a7/image.png" alt=""></p>
<p><strong>여담</strong> ➕</p>
<p>AOP 에서 HttpServletRequest, Response 를 쓰고싶으면 다음 방식을 이용하자</p>
<pre><code class="language-java">HttpServletRequest request = ((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getRequest();

HttpServletResponse response = ((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getResponse();</code></pre>
<p>출처: <a href="https://postitforhooney.tistory.com/entry/SpringAOPAOP%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EB%A9%B4%EC%84%9C-%EC%83%9D%EA%B8%B0%EB%8A%94-%EB%AC%B8%EC%A0%9C%EC%A0%90%ED%95%B4%EA%B2%B0%EC%B1%85">AOP 에서 HTTP 이용하기</a></p>
<p>진행하면서 겪었던 문제⚠️ 들을 정리해보고자 한다.</p>
<p><strong>첫 째로</strong>, 처음 LombokStudy 로 메서드를 체크하려 했을 때 new LombokStudy() 로 인스턴스를 만들어서 checkExecutionTime() 메서드를 실행했는데 아무 반응이 없었다. 
이유는 <strong>스프링 컨테이너의 빈은 싱글톤 패턴</strong> 이기 때문에 내가 new 로 생성한 LombokStudy 는 스프링 빈이 아니었기 때문에 @Aspect 가 정상 작동을 안 했던 것이다.</p>
<p><strong>두 번째로</strong>, main 메서드에서 ApplicationContext 로 lombokStudy 빈을 가져오려 하니 </p>
<p><span style="color:red">Exception in thread &quot;main&quot; java.lang.IllegalStateException: org.springframework.context.annotation.AnnotationConfigApplicationContext@427a12b6 has not been refreshed yet</span> 예외가 발생했다
이 오류의 원인은 Application Context 를 Autowired로 빈을 가져오지 않았기 때문에 발생한 문제였다.
자세한 ApplicationContext 의 refresh 과정은
<a href="https://mangkyu.tistory.com/214">망나니개발자 SpringBoot 소스 코드 분석</a> 이 곳에 잘 설명돼있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Java JMH 라이브러리를 활용한 성능테스트]]></title>
            <link>https://velog.io/@anak_2/Java-JMH-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EC%84%B1%EB%8A%A5%ED%85%8C%EC%8A%A4%ED%8A%B8</link>
            <guid>https://velog.io/@anak_2/Java-JMH-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EC%84%B1%EB%8A%A5%ED%85%8C%EC%8A%A4%ED%8A%B8</guid>
            <pubDate>Wed, 09 Aug 2023 09:14:04 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>** 실행 환경 **
Gradle 7.5
Java 17
Intellij</p>
</blockquote>
<ul>
<li>Maven 으로 설정한 JMH 는 많이 보였기 때문에 Gradle 로 진행했습니다</li>
</ul>
<h3 id="jmh-사용-목적-🚩">JMH 사용 목적 🚩</h3>
<p>어떤 자바 메서드를 개선했다고 가정했을 때, 이를 메서드 단위로 수치화하여 어느 정도의 개선이 이루어졌는지 계산해야한다. 이를 가능하게 하는 방법 중에</p>
<p>첫 째로, System.currentTimeMillis() 이나 System.nanoTime() 메서드를 통해 시작과 끝의 차이를 계산하여 성능을 비교하는 방법이 있다. </p>
<p>둘 째로, ** JMH (Java Microbenchmark Harness) ** 는 자바 메서드의 성능을 측정하기 위한 라이브러리로, 어노테이션만으로 성능을 측정할 수 있기 때문에 보일러 플레이트 없이 정확하게 측정할 수 있다.</p>
<h3 id="디렉토리-구조-🌲">디렉토리 구조 🌲</h3>
<p><img src="https://velog.velcdn.com/images/anak_2/post/bb7c624d-ee61-459c-b4d5-c1c31f71ba92/image.png" alt=""></p>
<p>src 폴더 아래에 jmh 디렉토리에 벤치마크할 코드가 있어야 한다. 
설치할 ** jmh plugin ** 이 벤치마크에 필요한 설정들을 자동으로 생성해서 src 아래의 jmh 폴더에서 벤치마크할 코드를 찾는다.</p>
<p>jmh 플러그인을 사용하면 별도의 프로젝트를 만들지 않고도 기존 소스를 쉽게 테스트 할 수 있다. 그렇기 때문에 벤치마크 소스 파일을 src/main/java 대신 src/jmh/java 에 넣어야 하는 이유이다.</p>
<h3 id="buildgradle-설정-⚙️">build.gradle 설정 ⚙️</h3>
<pre><code class="language-java">plugins {
    id &#39;java&#39;
    id &quot;me.champeau.jmh&quot; version &quot;0.7.1&quot;
}

dependencies {
    jmh &#39;org.openjdk.jmh:jmh-core:0.9&#39;
    jmh &#39;org.openjdk.jmh:jmh-generator-annprocess:0.9&#39;
}

jmh{
    fork = 1
    warmupIterations = 1
    iterations = 1
}</code></pre>
<p>jmh 플러그인은 자동으로 자기가 사용할 버젼의 org.openjdk.jmh 의 jmh-core 와 jmh-generator-annprocess 를 설치하기 때문에 밑에 dependencies 안의 코드는 작성하지 않아도 된다. 만약 임의로 지정한다면 지정한 JMH 버전이 대신 설치된다.
** <span style="color:red"> 참고로, 0.9 버전이 호환이 안되는지 빌드할 때 계속 오류가 나서 지우고 실행했다 </span> **
** 또한, ** jmh 0.7.1 버전의 플러그인은 JMH 1.3.6 을 사용한다고 한다.</p>
<p>jmh { } 코드 안에는 벤치마크 할 때 ** Configuration oprions ** 을 설정할 수 있다.</p>
<p>더 자세한건 <a href="https://github.com/melix/jmh-gradle-plugin">jmh-gradle-plugin</a> 를 참고!</p>
<h3 id="테스트-할-java-코드-💻">테스트 할 JAVA 코드 💻</h3>
<pre><code class="language-java">@State(Scope.Benchmark)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@BenchmarkMode(Mode.All)
public class ParallelStreamBenchmark {

    private static final long N = 10_000_000L;

//    Benchmark method should return something and no parameter requires
    @Benchmark
    public long iterateSum(){
        long sum = 0;
        for(long i = 1L; i &lt;= N; i++){
            sum += i;
        }
        return sum;
    }

    @Benchmark
    public long parallelSum(){
        return Stream.iterate(1L,i -&gt; i+1)
                .limit(N)
                .parallel() // make stream parallel
                .reduce(0L, Long::sum);
    }

//    public static void main(String[] args) throws Exception {
//        org.openjdk.jmh.Main.main(args);               // 벤치마킹 시작 --&gt; 필요 X!
//    }

}</code></pre>
<p>코드를 대강 설명하자면, 순차적으로 합을 구한 iterateSum 과 병렬로 합을 구한 parallelSum 의 효율을 계산한 코드이다.</p>
<p>벤치마크할 메서드들의 특징으로, ** 매개변수가 없어야되고 리턴 값이 void가 아니어야한다! **
매개변수를 사용하는 대신 @Setup 을 이용하여 변수 값을 초기화할 수 있다.
리턴값이 void라면 대신 Blackhole 객체를 매개변수에서 생성해서 consume 메서드를 이용해야한다. 
<a href="https://stackoverflow.com/questions/73144982/testing-void-methods-with-jmh">참고</a></p>
<p><strong>@State(Scope.Benchmark)</strong>
같은 Benchmark 끼리는 같은 객체를 공유한다. 
이걸 이용하면 멀티쓰레드 환경일 때 성능을 측정하기 좋다</p>
<p><strong>@OutputTimeUnit(TimeUnit.MICROSECONDS)</strong>
벤치마킹 결과를 보여줄 단위를 설정한다. 위 설정은 ms 로 보여준다</p>
<p><strong>@BenchmarkMode(Mode.All)</strong>
JMH 의 어떤 벤치마크를 실행할지 지정해준다</p>
<p><strong>@Benchmark</strong>
벤치마킹할 메서드에 붙인다.</p>
<p>더 다양한 설정과 예제 코드는 여기서 <a href="https://jenkov.com/tutorials/java-performance/jmh.html">java-performance jmn</a> 확인 가능하다.</p>
<h3 id="benchmark-실행-🥳">Benchmark 실행 🥳</h3>
<p>Benchmark 하기 위해선 gradle 을 이용해 빌드해야 결과를 볼 수 있다.
gradlew 가 있는 루트 디렉토리로 이동해서
<code>./gradlew jmh</code> 를 입력하면 빌드가 시작되면서 벤치마킹 결과를 볼 수 있다</p>
<p><img src="https://velog.velcdn.com/images/anak_2/post/b9208162-4afc-4297-a830-ae225071e827/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/anak_2/post/2343a57e-7042-4228-881a-186d2a05e6bd/image.png" alt=""></p>
<p>생성된 jar 파일은 {프로젝트}/build/libs 위치에 jar 파일이 있고
<code>java -jar {jar파일명}.jar</code> 로 실행해서 빌드 결과를 볼 수 있다.</p>
<p>위 결과를 해석해보자면</p>
<p>처리율, Throughput (thrp):</p>
<p>iterateSum: ≈ 10^-4 ops/us
parallelSum: ≈ 10^-6 ops/us
이 결과는 iterateSum 메소드가 parallelSum 메소드에 비해 처리량이 더 높다는 것을 의미한다. 즉, iterateSum이 더 많은 연산을 단위 시간 내에 처리하고 있다.</p>
<p>이 이유로는 단순한 계산에 parallelSum의 병렬 처리가 오히려 <strong>오버헤드</strong>가 되기 때문이다 (❗⚠️병렬은 여러 쓰레드가 각자 할 일을 나눈 것 뿐이지, 정말 같은 시간에 작동하는 것이 아니기 때문에 <strong>Context Switching</strong> 비용이 든다!!) </p>
<p>Average Time (avgt):</p>
<p>iterateSum: 3275.248 μs/op
parallelSum: 292894.506 μs/op
iterateSum이 연산당 평균 소요 시간이 훨씬 적기 때문에 성능이 더 빠르다.</p>
<p>샘플 결과 (Sample):</p>
<p>iterateSum 메소드의 샘플 분포를 보면,
50번째 백분위(p0.50): 3080.192 μs/op
95번째 백분위(p0.95): 3842.048 μs/op
99번째 백분위(p0.99): 5052.088 μs/op</p>
<p>iterateSum의 처리 시간이 대부분의 경우 일정한 범위 안에 있고, 99번째 백분위수에서도 크게 증가하지 않음을 보여줍니다.</p>
<p>Single Shot (ss):</p>
<p>parallelSum: 645015.100 μs/op
parallelSum의 경우 한 번의 연산에 걸린 시간이 매우 길고, 병렬 처리에 대한 오버헤드가 상당히 크다는 것을 의미한다.</p>
<blockquote>
<p>왜 IterateSum이 수행 결과가 더 많지?<br>
그 이유는, 벤치마크는 지정된 시간 동안 메서드를 반복해서 실행하면서 성능을 측정하는데, iterateSum은 한 번 실행되는 데 걸리는 시간이 짧기 때문에 더 많은 실행 횟수를 기록한 것이다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[innerHtml 사용시 event listener 사라짐 해결]]></title>
            <link>https://velog.io/@anak_2/innerHtml-%EC%82%AC%EC%9A%A9%EC%8B%9C-event-listener-%EC%82%AC%EB%9D%BC%EC%A7%90-%ED%95%B4%EA%B2%B0</link>
            <guid>https://velog.io/@anak_2/innerHtml-%EC%82%AC%EC%9A%A9%EC%8B%9C-event-listener-%EC%82%AC%EB%9D%BC%EC%A7%90-%ED%95%B4%EA%B2%B0</guid>
            <pubDate>Wed, 12 Jul 2023 10:40:58 GMT</pubDate>
            <description><![CDATA[<blockquote>
<h3 id="발생한-문제-🚩">발생한 문제 🚩</h3>
<p><strong>element.innerHtml += `html 코드`</strong> 를 사용했더니 element 안에 있던 요소들에게 JS 파일에서 동적으로 준 action event listener 들이 초기화됐다!</p>
</blockquote>
<h3 id="문제-발생의-원인">문제 발생의 원인</h3>
<p><a href="https://developer.mozilla.org/ko/docs/Web/API/Element/innerHTML">MDN innerHtml 문서</a>에 따르면 innerHtml 사용시 안에 있던 요소들이 새로운 html 로 바뀌는 것이므로 JS 파일에서 동적으로 줬던 event listener 들이 다시 덮어 씌워지는 문제였다</p>
<h3 id="해결방법">해결방법</h3>
<p>요소를 추가하려면 <code>isertAdjacentHtml()</code> 을 사용하기를 권장하는 말이 있다.</p>
<h3 id="추가로-innerhtml-주의할-점">추가로 innerHtml 주의할 점</h3>
<p>공식 문서를 읽다보니, innerHtml 을 사용자에게 입력값을 받은 것을 입력하는 경우, 사용자가 Javascript code를 적어서 Cross Site Script (XSS) 공격을 할 수 있으므로 되도록 사용자에게 받는 값을 쓰지 않도록 보안에 신경쓰라는 경고 문구가 있었다.</p>
<p><code>ex) Element.innerHtml = &lt;script&gt;alert(&quot;해킹 위험!&quot;)&lt;/script&gt;</code></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring Error] - 
Class HttpMediaTypeNotAcceptableException,
406 Not Acceptable
]]></title>
            <link>https://velog.io/@anak_2/Spring-Error-Class-HttpMediaTypeNotAcceptableException406-Not-Acceptable</link>
            <guid>https://velog.io/@anak_2/Spring-Error-Class-HttpMediaTypeNotAcceptableException406-Not-Acceptable</guid>
            <pubDate>Mon, 10 Jul 2023 13:03:45 GMT</pubDate>
            <description><![CDATA[<blockquote>
<h4 id="발생한-문제-🚩">발생한 문제 🚩</h4>
<p>Http 응답을 생성하는 과정 중에 요청 한 클라이언트에서 받지 못하는 데이터 타입을 반환하려 할 때 발생하는 오류!</p>
</blockquote>
<h3 id="문제였던-부분">문제였던 부분</h3>
<p>반환하는 값이 Response DTO였는데, 이 곳에 Getter 메서드가 없었기 때문에 HttpMessageConverter 가 적절한 JSON 객체로 반환할 수 있도록 만들지 못 했기 때문이었다.</p>
<pre><code class="language-java">@Getter // 추가해주기!
public class ResponseDTO{
}</code></pre>
<h3 id="406-error-이란">406 Error 이란?</h3>
<p>HTTP 의 406 Not Acceptable 는 클라이언트 에러 응답 코드로 흔하게 발생하진 않는다. 서버가 요청에 대한 <a href ="https://developer.mozilla.org/ko/docs/Web/HTTP/Status/406">콘텐츠 협상</a> 헤더에 정의된 <strong>허용 가능한 값 목록</strong>과 일치하는 응답을 생성할 수 없을 때 나타나는 오류이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[InvalidDataAccessApiUsageException: org.hibernate.TransientPropertyValueException 문제]]></title>
            <link>https://velog.io/@anak_2/InvalidDataAccessApiUsageException-org.hibernate.TransientPropertyValueException-%EB%AC%B8%EC%A0%9C</link>
            <guid>https://velog.io/@anak_2/InvalidDataAccessApiUsageException-org.hibernate.TransientPropertyValueException-%EB%AC%B8%EC%A0%9C</guid>
            <pubDate>Thu, 06 Jul 2023 12:57:33 GMT</pubDate>
            <description><![CDATA[<blockquote>
<h3 id="발생한-문제-🚩">발생한 문제 🚩</h3>
<p>대부분 영속성 컨텍스트에서 프록시 객체가 초기화되지 않았던 문제!!</p>
<p>필자의 경우 RequestDTO 를 설계할 때 ManyToOne 관계를 가진 엔티티의 RequestDTO 를 함께 받도록 설계했는데, DTO 를 Entity 로 바꾸는 설계에서
연관관계를 가진 DTO 를 DB에서 꺼낸게 아닌 RequestDTO 를 자신만 Entity 로 바꾸고 FK로 이어진 부모 Entity 는 안 가져왔기 때문에 발생한 것이다.</p>
</blockquote>
<h3 id="문제-배경-정리">문제 배경 정리</h3>
<p>문제의 세 엔티티는 RequestUserDTO &lt;-&gt; RequestScheduleDTO &lt;-&gt; RequestTodoDTO 가 [User] 1 &lt;-&gt; * [Schedule] 1 &lt;-&gt; * [Todo] 관계로 연결되어있습니다.</p>
<pre><code class="language-java">// ----------------------- User Entity
public class User {

    @Id @GeneratedValue
    private Long id;

    private String username;
    private String password;
    private String email;

    private String provider;
    private String providerId;

    @CreationTimestamp
    private Timestamp createDate;

    @Enumerated(EnumType.STRING)
    @Builder.Default
    private Role role = Role.USER;

    @OneToMany(mappedBy = &quot;user&quot;, cascade=CascadeType.ALL, orphanRemoval = true)
    @Builder.Default
    private List&lt;Schedule&gt; scheduleList = new ArrayList&lt;&gt;();

    public void updatePassword(String password){
        this.password = password;
    }

    public void updateEmail(String email){
        this.email = email;
    }

    public String getRoleKey(){
        return this.role.getKey();
    }

    public void createScheduleList(List&lt;Schedule&gt; scheduleList) {
        this.scheduleList = scheduleList;
    }

    public User(String username, String password){
        this.username = username;
        this.password = password;
    }
}
// ----------------------- Schedule Entity
public class Schedule {
    @Id @GeneratedValue
    private long id;
    @CreationTimestamp
    private Timestamp createdDate;
    @NonNull
    private String title;

    @ManyToOne(fetch=FetchType.LAZY)
    @JoinColumn(name=&quot;user_id&quot;)
    private User user;

    @OneToMany(mappedBy=&quot;schedule&quot;, cascade=CascadeType.ALL, orphanRemoval = true)
    private List&lt;Todo&gt; todoList;

    @Builder.Default
    private boolean isPublic = false;

}

// ----------------------- Todo Entity
public class Todo {
    @Id @GeneratedValue
    private Long id;
    @CreationTimestamp
    private Timestamp createdDate;
    private String title;
    private String content;
    private boolean isFinished;
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = &quot;schedule_id&quot;)
    private Schedule schedule;
    private String hyperLink;


}
</code></pre>
<p>(문제 이해를 돕기 위해 세 Entity 들을 첨부했습니다)</p>
<p>여기서 TodoReuqestDTO 에 ScheduleRequestDTO 를 가지고 있는 형태로 DTO 를 설계했었는데, TodoRequestDTO 를 Entity 로 바꿀 때 ScheduleRequestDTO 또한 schedule 로 바꿔서 Todo 에 전달해야 했습니다.</p>
<pre><code class="language-java">public class TodoRequestDto {

    private String title;
    private String content;
    private boolean isFinished;
    private ScheduleWithTodoRequest scheduleWithTodoRequest;

    public Todo toEntity(){
        Schedule schedule = scheduleWithTodoRequest.toEntity();
        Todo todo = Todo.builder()
                .content(content)
                .title(title)
//                InvalidDataAccessApiUsageException: org.hibernate.TransientPropertyValueException 발생!
//                .toEntity() 로 생성된 Schedule 은 영속성 컨텍스트에서 꺼낸 객체가 아니기 때문이다.
//                해결 방법 -&gt; DTO 에서 Entity 로 바꿀 때 연관관계에 있는 객체는 꼭 영속성 컨텍스트 에서 가져오도록 하자
                .schedule(schedule)
                .build();
        return todo;
    }
}
</code></pre>
<h3 id="문제점">문제점!</h3>
<p style="color:red">위에서 발생한 문제는 Schedule 을 Java 객체로만 바꿨을 뿐 RDB 에서 꺼내온 값이 아니기 때문에 프록시 객체인 상태입니다! </p>
 해결하고자 Schedule 을 영속성 컨텍스트에 넣었어도, 부모 엔티티인 User 또한 조회해야 했기 때문에 매우 번거롭습니다. ( @ManyToOne ( cascade = CascadeType.ALL ) 로 부모도 끌어올 순 있긴 합니다.)

<h3 id="결론">결론</h3>
<h4 id="개인적으로-제일-좋은-해결-방법">개인적으로 제일 좋은 해결 방법</h4>
<p>이런 경우 Schedule 을 무리하게 RDB 에서 조회하여 넣을 순 있겠지만, 계층별 역할이 망가진다 생각하여, 앞으로 RequestDTO 에서 부모나 자식 Entity 가 필요하다면 PK 로 받아서 다른 곳에서 조회하는 것이 좋겠다고 생각을 바꿨습니다. </p>
<p>위의 경우 <code>private ScheduleWithTodoRequest scheduleWithTodoRequest;</code>
부분을 <code>Long scheduleId</code> 로 바꾼 것이 되겠죠.</p>
<pre><code class="language-java">public class TodoRequestDto {

    private String title;
    private String content;
    private boolean isFinished;
    private Long scheduleId;

    // scheduleId 를 이용해서 toEntity 를 호출한 메서드에서 Schedule 매개변수로 넣어주기
    public Todo toEntity(Schedule schedule){
        Todo todo = Todo.builder()
                .content(content)
                .title(title)
                .schedule(schedule)
                .build();
        return todo;
    }</code></pre>
<p> ✅여담으로, Schedule 만 필요한데 양방향 연관관계가 걸렸기 때문에 User 까지 조회하여 Schedule 의 User 필드에 넣게되겠죠. 이런 단점 때문에, 비록 일반적인 흐름과는 맞지 않지만 <strong>단방향 연관관계</strong>를 사람들이 더 선호하는 이유가 되겠습니다.</p>
]]></description>
        </item>
    </channel>
</rss>