<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>KIM_TABLE_NEXT</title>
        <link>https://velog.io/</link>
        <description>개발하는 기록자</description>
        <lastBuildDate>Mon, 26 Aug 2024 02:55:39 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>KIM_TABLE_NEXT</title>
            <url>https://velog.velcdn.com/images/kim_table_next/profile/c5dc845d-6fa5-44a8-ae0a-a2197fcaf30d/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. KIM_TABLE_NEXT. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/kim_table_next" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Scanner vs BufferedReader]]></title>
            <link>https://velog.io/@kim_table_next/Scanner-vs-BufferedReader</link>
            <guid>https://velog.io/@kim_table_next/Scanner-vs-BufferedReader</guid>
            <pubDate>Mon, 26 Aug 2024 02:55:39 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>오랜만에 알고리즘 문제를 다시 풀기 시작하면서 플랫폼을 변경하였다. (프로그래머스 -&gt; 백준)</p>
</blockquote>
<p>변경하게 되면서 <strong>문제의 입력값을 직접 처리해야하는 과정</strong>이 추가되었다.</p>
<blockquote>
<h2 id="프로그래머스따로-입력처리를-하지-않아도됨">프로그래머스(따로 입력처리를 하지 않아도됨)</h2>
<p><img src="https://velog.velcdn.com/images/kim_table_next/post/ad7d11f6-16aa-49a0-b6e2-4b39e88783eb/image.png" alt=""></p>
</blockquote>
<blockquote>
<h2 id="백준깔-끔">백준(깔-끔)</h2>
<p><img src="https://velog.velcdn.com/images/kim_table_next/post/73f08657-73e3-4333-8310-104352ca9cc4/image.png" alt=""></p>
</blockquote>
<p>그렇게 되면서 이에 대해 알아본 결과 주로 두가지 방법으로 입력값을 받는것 같다.</p>
<h1 id="scanner">Scanner</h1>
<blockquote>
<p>Scanner 클래스는 입력받은 데이터(바이트)를 다양한 타입으로 변환하여 반환하는 클래스이다. 간단하게 기본형과 String 타입을 정규표현식을 사용해 파싱(parse)할 수 있다.</p>
</blockquote>
<h2 id="scanner의-특징">Scanner의 특징</h2>
<blockquote>
<ul>
<li>java.util 패키지에 속한다. (java.util.Scanner)
공백(띄어쓰기) 및 개행(줄 바꿈)을 기준으로 읽는다.(&#39; &#39;, &#39;\t&#39;, &#39;\r&#39;, &#39;\n&#39; 등)</li>
</ul>
</blockquote>
<ul>
<li>원하는 타입으로 읽을 수 있다.</li>
<li>버퍼의 사이즈가 1024byte(1KB) 이다.</li>
<li>UnChecked(Runtime) Exception으로 별도로 예외 처리를 명시할 필요가 없다.</li>
<li>Thread unsafe 성질을 지니기에 멀티스레드 환경에서 문제가 발생할 수 있다.</li>
<li>데이터를 입력받을 경우 즉시 사용자에게 전송되며 입력받을 때마다 전송되어야 하기에 많은 시간이 소요된다.</li>
</ul>
<h2 id="scanner-예시">Scanner 예시</h2>
<blockquote>
<pre><code class="language-java">import java.util.Scanner;
...
Scanner sc = new Scanner(System.in);
String st = sc.nextLine();</code></pre>
</blockquote>
<pre><code>위와 같이 System.in을 통해 Scanner 객체를 생성한다.

### System.in이란?
&gt;사용자로부터 입력을 받기 위한 입력 스트림이다.
Scanner 클래스뿐 아니라 다른 입력 클래스들도 System.in을 통해 사용자 입력을 받아야 한다.

# BufferedReader
&gt;데이터를 한번에 읽어와 버퍼에 보관한 후 버퍼에서 데이터를 읽어오는 방식으로 동작하는 클래스이다. 즉 사용자가 입력한 문자 스트림을 읽는 것(read) 라고 한다.

## 여기서 버퍼(buffer)란?
&gt;데이터를 한 곳에서 다른 한 곳으로 전송하는 동안 일시적으로 해당 데이터를 보관하는 임시 메모리 영역이다. 주로 입출력 속도 향상을 위해 버퍼를 사용한다.
자바에서는 버퍼를 BufferedReader와 BufferedWriter라는 클래스를 제공하여 다룰 수 있다.

## BufferedReader의 특징
&gt;- java.io 패키지에 속한다. (import java.io.BufferedReader)
- 데이터를 파싱하지 않고 String으로만 읽고 가져온다.
- 버퍼의 사이즈가 8192byte(8KB)이다.
- Checked Exception으로 반드시 예외 처리를 명시해야한다.(I/O Exception을 throw하거나 try/catch 해야한다.)
- Thread safe 성질을 지니기에 멀티스레드 환경에서도 안전하다.
- 버퍼가 가득차거나 개행문자가 나타나면 버퍼의 내용을 한번에 프로그램으로 전달하기에 Scanner보다 소요되는 시간을 절약할 수 있다.

## BufferedReader 예시
&gt;```java
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.IOException;
...
public static void main(String[] args) throws IOException {
  BufferReader br = new BufferedReader(InputStreamReader(System.in));
  String st = br.readLine();
  int a = Integer.parseInt(br.readLine());
  int b = Integer.parseInt(st);
}</code></pre><p>위와 같이 BufferedReader는 매개변수로 InputStreamReader를 사용하여 객체를 생성한다.
또한 패키지의 경우 java.io 하위의 BufferedReader, InputStreamReader 와 예외처리를 위한 IOException 를 사용하며 모두 간단히 java.io.* 로 모두 포함할 수 있다.</p>
<h3 id="inputstreamreader-란">InputStreamReader 란?</h3>
<blockquote>
<p>문자 기반의 보조 스트림으로써 바이트 기반 스트림을 문자 기반 스트림으로 연결시켜 주는 역할을 한다.</p>
</blockquote>
<h1 id="scanner-vs-bufferedreader">Scanner Vs BufferedReader</h1>
<h2 id="scanner는-bufferedreader보다-타입에-구애받지-않는다">Scanner는 BufferedReader보다 타입에 구애받지 않는다.</h2>
<blockquote>
<p>BufferedReader는 String 형식으로만 읽고 저장하기에 형변환을 위한 추가적인 코드 작성이 불가피한 반면에 Scanner는 원하는 타입으로 읽고 파싱할 수 있다.
Scanner의 경우 int, long, short, float, double의 경우 nextInt(),nextLong(),nextShort(),nextFloat(), nextDouble()과 같은 함수들을 사용할 수 있다.
그런데 BufferedReader는 readLine() 함수만을 사용한다.</p>
</blockquote>
<h2 id="bufferedreader는-scanner보다-효율적인-메모리-용량을-가진다">BufferedReader는 Scanner보다 효율적인 메모리 용량을 가진다.</h2>
<blockquote>
<p>BufferedReader의 버퍼 메모리가 8KB로 Scanner의 버퍼 메모리 1KB보다 크기에 많은 입력이 있을 경우 더 효율적이다.
다만, BufferedReader의 경우 일단 큰 메모리를 잡아먹게 된다.</p>
</blockquote>
<h2 id="bufferedreader는-scanner보다-안전하다">BufferedReader는 Scanner보다 안전하다.</h2>
<blockquote>
<p>Scanner는 Thread-unsafe 하기에 멀티스레드 환경에서 안전하지 않지만 BufferedReader는 안전하다.
스레드 간 Scanner는 공유할 수 없지만 BufferedReader는 공유할 수 있다.- 동기화를 지원하는 BufferedReader는 싱글스레드인 Scanner보다 약간 느린데, Scanner의 경우 정규식을 사용하여 입력을 받으므로 BufferedReader가 문자열을 더욱 빠르게 입력받을 수 있다.</p>
</blockquote>
<h2 id="bufferedreader가-scanner보다-실행-속도가-빠르다">BufferedReader가 Scanner보다 실행 속도가 빠르다.</h2>
<blockquote>
<p><img src="https://velog.velcdn.com/images/kim_table_next/post/4f717789-ef6f-47bf-8c9f-276dbe28605c/image.png" alt="">
백준에서 작성된 입력속도 비교표를 확인해보면 BufferedReader는 0.68585초, Scanner는 4.8448초가 소요되며 둘의 시간차이가 생각보다 크다.
왜 빠를까? 여기선 CPU를 고려해야 하는데 문자가 입력될 때마다 CPU가 하나하나 입출력을 하는 것보다 버퍼에 어느정도 쌓아두고 가득차거나 개행이 일어날 때마다 입출력을 처리하는 것이 훨씬 효율적이다.</p>
</blockquote>
<h1 id="결론">결론</h1>
<blockquote>
<p>찾아보며 느낀점은 Scanner가 BufferedReader에 비해 조금 더 편리하지만,
메모리와 시간을 더 많이 잡아먹는다는 느낌을 받았다.
최적화를 위해서 BufferedReader를 사용하는쪽으로 해야겠다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Project - 최종프로젝트 회고(+앞으로의 계획)]]></title>
            <link>https://velog.io/@kim_table_next/Project-%EC%B5%9C%EC%A2%85%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EA%B7%BC%ED%99%A9</link>
            <guid>https://velog.io/@kim_table_next/Project-%EC%B5%9C%EC%A2%85%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EA%B7%BC%ED%99%A9</guid>
            <pubDate>Tue, 04 Jun 2024 16:21:11 GMT</pubDate>
            <description><![CDATA[<h1 id="프로젝트-소개">프로젝트 소개</h1>
<blockquote>
<p><strong><a href="https://desert-reaction-b4e.notion.site/9214f6f8cf224808a83a6679c61baf67">&gt;&gt;프로젝트 소개 링크&lt;&lt;</a></strong>
다른 프로젝트들과 다르게 최종프로젝트인 만큼
노션에 <strong>아주 예쁘게</strong> 프로젝트 소개를 정리하였다.</p>
</blockquote>
<h1 id="수상내역">수상내역</h1>
<blockquote>
<p><img src="https://velog.velcdn.com/images/kim_table_next/post/64cdaa6a-3725-4862-8856-d507742aaf85/image.png" alt="">
내부 투표에서 무려 50%가 넘는 득표율로 우수 프로젝트 상을 받게되었다.
사실 예상도 못했는데,매우 기분이 좋았다.
<img src="https://velog.velcdn.com/images/kim_table_next/post/f94ae83b-2ffa-40d0-957f-612c71f2ed18/image.png" width="50%" height="50%">상품으로 스프링 로고 키캡을 받았다.
<strong>나중에 취업하게되면 회사에서 사용하려고 고이 모셔두고 있다!</strong></p>
</blockquote>
<h1 id="회고">회고</h1>
<blockquote>
<p>최종프로젝트가 끝이났다.
참 많은영역에 대해 도전하였고,
처음 목표였던,<br>
<strong>후회없는 프로젝트 하기</strong><br>
성공한것 같다.<br>
사실 프로젝트가 끝난지 한달가량 지났다.
공백기 동안 졸업문제를 해결하고자 TOPCIT 시험을 응시하였고, 결과는 6월 17일에 나온다.
그리고 토익스피킹 시험도 응시하였다. 결과는 6월 7일에 나온다.<br>
두 시험 결과가 졸업 요건에 충족했으면 한다.
그리고 이제는 7월중에 있는 정보처리기사 시험을 준비하려고 한다.
<br>
부트캠프에서 실무 역량을 키웠다.
이제는 지식을 조금 쌓아보고싶어서 정보처리기사 시험을 준비하면서 코딩테스트, 면접등 준비해보려고 한다!!</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring - 테스트코드 심화편 (Mock, Stub)]]></title>
            <link>https://velog.io/@kim_table_next/Spring-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%85%8C%EC%8A%A4%ED%8A%B8%EC%BD%94%EB%93%9C</link>
            <guid>https://velog.io/@kim_table_next/Spring-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%85%8C%EC%8A%A4%ED%8A%B8%EC%BD%94%EB%93%9C</guid>
            <pubDate>Tue, 23 Apr 2024 06:29:02 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/kim_table_next/post/645c6fea-3ae3-4f46-bdd1-899ab650aa1a/image.png" alt=""></p>
<h1 id="til">TIL</h1>
<blockquote>
<ul>
<li>최종프로젝트 28일차, 이제 슬슬 마무리 단계에 돌입했다.</li>
</ul>
</blockquote>
<ul>
<li>테스트 코드 작성을 맡으면서 만난 문제와 해결을 하여서 기록하려고 한다.</li>
</ul>
<h2 id="mock-객체와-가설stub-세우기">@Mock 객체와 가설(Stub) 세우기</h2>
<blockquote>
<p><img src="https://velog.velcdn.com/images/kim_table_next/post/db37669d-96a2-4af4-aa67-6cc5ecb87dc8/image.png" alt=""></p>
</blockquote>
<ul>
<li>Mock 객체는 가짜로 등록되는 객체이기 때문에 해당 객체를 이용하여 하는 모든것에 대해 미리 결과를 정의해줘야한다.</li>
<li>위 코드에서도 60번째 줄에 <code>categoryRepository.save</code>가 <code>TEST_CATEGORY1</code>을 반환할 것이다라고 미리 정의해줬다.<ul>
<li><code>any()</code>는 &quot;어떤게 들어오던&quot; 같은 의미이며, 조금 더 세분화 하여 <code>anyString()</code>, <code>anyList()</code>등등 형식을 지정할 수 있다.
<img src="https://velog.velcdn.com/images/kim_table_next/post/cbd7e9d9-2d53-4a00-9b2b-af614c6a0d41/image.png" alt=""></li>
</ul>
</li>
<li>가설을 세워 줬다면, 테스트 하려는 부분을 실행하고, <code>verify</code>와 <code>assertEquals</code>등으로 검증한다.<ul>
<li>위의 코드에서는 <code>verify</code>를 통해 <code>categoryRepository</code>에 <code>save</code>가 1번일어났는지 검사하고,</li>
<li>결과값과 요청값을 비교하였다.</li>
</ul>
</li>
<li>테스트코드의 전반적인 구성인 이렇게 된다. (가설설정 -&gt; 테스트 -&gt; 검증)</li>
</ul>
<h2 id="가설stub-세우면서-만난-문제들">가설(Stub) 세우면서 만난 문제들</h2>
<blockquote>
<ul>
<li>테스트코드의 전반적인 구성을 알아보았고, 다음은 구성중 하나인 
가설을 설정하는 부분에서 만난 문제들이다.</li>
</ul>
</blockquote>
<ul>
<li>전반적으로 새롭게 알게된 중요한 점은 이거였다.<h4 id="가설-설정에는-가설-순서도-영향을-미친다">가설 설정에는 가설 순서도 영향을 미친다.</h4>
</li>
</ul>
<h3 id="unnecessarystubbingexception">UnnecessaryStubbingException</h3>
<blockquote>
<p><img src="https://velog.velcdn.com/images/kim_table_next/post/4df1cf3d-844f-4f88-9888-3baca272d61b/image.png" alt=""></p>
</blockquote>
<ul>
<li>가설을 세웠지만 <strong>사용하지 않는 가설일 경우</strong> 발생하는 에러이다.</li>
<li>처음에는 가설을 <code>Mock 객체</code>가 채워주지 못하는 부분을 설정해주는 것이라고만 생각했는데,
생각보다 빡세게 적용되었다. <strong>(기본 설정이 strict stubbing)</strong></li>
<li>해결 방법으로는 두가지가 있다.<ul>
<li><ol>
<li>사용하지 않는 가설을 제거한다.</li>
</ol>
</li>
<li><ol start="2">
<li>Loose stubbing으로 변경한다. (<code>lenient</code>)</li>
</ol>
</li>
</ul>
</li>
</ul>
<h3 id="loose-stubbing-lenient">Loose Stubbing (lenient)</h3>
<blockquote>
<p><img src="https://velog.velcdn.com/images/kim_table_next/post/e7f92b6a-a1b7-4fd5-b9e4-d918012ce8e4/image.png" alt=""></p>
</blockquote>
<ul>
<li>63번째 코드처럼 <code>Mockito.lenient.when(가설 상황).thenReturn(반환값)</code>처럼 코드를 작성하면, <strong>가설이 필요하면 사용하고, 필요없다면 그냥 넘어간다. (Loose stubbing)</strong></li>
</ul>
<h3 id="redistemplate-nullpointerexception">RedisTemplate NullPointerException</h3>
<blockquote>
<p><img src="https://velog.velcdn.com/images/kim_table_next/post/f5b15f17-4d0e-44a8-a09b-f51aabfc40f9/image.png" alt=""></p>
</blockquote>
<ul>
<li>테스트를 하려는 입찰 관련 코드에는 <code>RedisTemplate.opsForValue().get()</code>을 통해 값을 가져오는 부분이 있다.</li>
<li>그래서 이 부분도 가설을 설정해주었는데, 에러가 발생했다.
<img src="https://velog.velcdn.com/images/kim_table_next/post/bb6cc6a5-0db1-48ff-abbc-baa00e4caf93/image.png" alt="">
<img src="https://velog.velcdn.com/images/kim_table_next/post/8b3d4c85-3201-4e3f-8af0-1f689fad5a93/image.png" alt=""></li>
<li>NullPointerException이 발생하였다. 원인은 <code>RedisTemplate.opsForValue().get()</code>을 하기 위해서는 <code>RedisTemplate.opsForValue()</code>값이 있어야 하는데 이부분이 Null이어서 발생했다.
<img src="https://velog.velcdn.com/images/kim_table_next/post/b7384164-28fe-4b08-8132-d1a18f48f3f7/image.png" alt=""></li>
<li>그래서 위 코드처럼 <code>RedisTemplate.opsForValue()</code>값을 가설로 세워주어 해결했다고 생각했는데,
테스트를 시행할때마다 전부 통과하거나, 일부만 통과하는 증상이 생겼다.</li>
<li>전혀 이해가 되지 않는 상황이어서 원인이 뭘까 정말 많은 시행착오를 겪었다..<ul>
<li>모두 동일한 가설에서 결과비교만 다르게 구현해놓았는데 일부만 통과했었다.
(심지어 매번 통과하는 테스트가 달라짐)</li>
</ul>
</li>
<li>찾아낸 원인이 확실하진 않지만, 전에 블로그를 찾아보다가 GitActions의 Redis 설정에 대해 찾아보고 적용해놓은 적이 있었는데, 이를 제거했더니 해결되었다. <a href="https://velog.io/@hwsa1004/STUDY-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%BD%94%EB%93%9C%EB%A5%BC-%EC%9E%91%EC%84%B1%ED%95%98%EB%A9%B4%EC%84%9C#4-redis%EB%A5%BC-%ED%85%8C%EC%8A%A4%ED%8A%B8-%ED%95%98%EC%8B%A0%EB%8B%A4%EA%B3%A0%EC%9A%94-%EA%B7%B8%EB%9F%AC%EB%A9%B4-%EC%9D%B4%EA%B1%B0-%EC%93%B0%EC%84%B8%EC%9A%94">Embedded-redis 블로그</a>
<img src="https://velog.velcdn.com/images/kim_table_next/post/48143a1e-e469-46c0-8961-1de34b5f08e4/image.png" alt=""></li>
<li>gradle에서 embedded-redis를 제거했더니 해결되었다. 아무래도 이게 원인이 아니었을까..</li>
<li><strong>개인적으로 코딩을 시작한 이후 가장 이해가 되지 않는 오류였다.</strong></li>
</ul>
<h2 id="테스트-결과">테스트 결과</h2>
<blockquote>
<p><img src="https://velog.velcdn.com/images/kim_table_next/post/9c124fd8-2da6-4d68-8edc-7e16a1fcff02/image.png" alt=""></p>
</blockquote>
<ul>
<li>구현한 28개의 테스트 모두 통과하는것을 확인하며 매우 희열을 느꼈다.</li>
<li>테스트 결과를 보기 좋게 묶는 방법도 찾아보고 적용하였다.
<img src="https://velog.velcdn.com/images/kim_table_next/post/fd4b14d1-9343-49d1-99f1-b0667a745d6f/image.png" alt=""></li>
<li><code>@TestClassOrder(ClassOrderer.OrderAnnotation.class)</code>를 통해 <code>@Nested</code>클래스들의 순서를 적용하고,
<img src="https://velog.velcdn.com/images/kim_table_next/post/bbda4fb7-e94a-40f8-a8dd-c6c2fc3d979a/image.png" alt=""></li>
<li><code>@Nested</code>클래스에는 <code>@TestMethodOrder(MethodOrderer.OrderAnnotation.class)</code>를 통해 테스트들의 순서를 적용해주었다.</li>
<li>한가지 아쉬운점은 <code>@Nested</code>클래스에 <code>@DisplayName</code>을 통해 숫자도 같이 작성했는데,
숫자는 적용이 되지 않는다고 한다.
<img src="https://velog.velcdn.com/images/kim_table_next/post/75a21607-e07b-43c8-aa73-d6a0273e7957/image.png" alt=""></li>
<li>GithubActions를 통해 CI를 하기위해 test.yml도 작성해주었다.
<img src="https://velog.velcdn.com/images/kim_table_next/post/c26ea214-a2c9-415a-bb80-d2360809560a/image.png" alt=""></li>
<li>성공적으로 테스트가 완료되었는데, 한가지 아쉬운점이 있었다.</li>
<li><code>Task: test</code>에 테스트 몇개 중 몇개가 통과했는지가 나오지 않았다. (실패하면 몇개중 몇개 실패했는지는 나옴)</li>
<li>그래서 인터넷을 찾아본 결과 테스트 결과를 출력하는 방법을 찾아서 적용해보았다.<pre><code class="language-java">tasks.named(&#39;test&#39;){
  useJUnitPlatform()
  testLogging {
      afterSuite { testDescriptor, testResult -&gt;
          if (testDescriptor.parent == null) {
              println &quot;Results: ${testResult.resultType} (${testResult.testCount} tests, ${testResult.successfulTestCount} successes, ${testResult.failedTestCount} failures, ${testResult.skippedTestCount} skipped)&quot;
          }
      }
  }
}</code></pre>
</li>
<li><code>build.gradle</code>에 해당 코드를 추가하면 끝이다!
<img src="https://velog.velcdn.com/images/kim_table_next/post/a2450dfc-f13b-4db4-bfa6-a8858c513935/image.png" alt=""></li>
<li>테스트 결과와 몇개의 테스트중 몇개를 통과했는지 잘 나온다!</li>
</ul>
<h1 id="오늘의-회고">오늘의 회고</h1>
<blockquote>
<p>테스트 코드를 작성하면서 프로젝트 전반적인 모든 코드들에 대해 이해할 수 있어서
좋은 경험이었다.
다만 작성하면서 든 생각은 코드를 먼저 구현하고 테스트 코드를 구현하다보니
이미 구현된 코드에 테스트코드를 맞추는 느낌이 조금 들었다.
TDD(테스트코드 기반 개발방법)도 기회가 된다면 시도 해보는것도 좋을 것 같다.
진짜 지금 생각해도 NullPointerException문제는 정말 이해가 되지 않는다..
어떻게 빌드할때마다 결과가 다르게 나올 수 있었을까
일단은 기존 Redis와 Embedded-Redis간의 충돌로 그렇지 않았을까 라는 결론을 내렸다.
이제 테스트 코드 작성도 끝났고, 배포와 발표만 남은 상황이다.
끝까지 최선을 다해야겠다!</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring - @ModelAttribute, Paging 캐싱]]></title>
            <link>https://velog.io/@kim_table_next/Spring-ModelAttribute-Paging-%EC%BA%90%EC%8B%B1</link>
            <guid>https://velog.io/@kim_table_next/Spring-ModelAttribute-Paging-%EC%BA%90%EC%8B%B1</guid>
            <pubDate>Tue, 16 Apr 2024 12:04:21 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/kim_table_next/post/39b594d2-62a1-401e-b55b-f257bdb6359f/image.png" alt=""></p>
<h1 id="til">TIL</h1>
<blockquote>
<ul>
<li>최종프로젝트 21일차, 중간발표도 있었고 많은게 지나갔다.</li>
</ul>
</blockquote>
<ul>
<li>프로젝트를 진행하면서 몇가지 문제와 해결을 하여서 기록하려고 한다.</li>
</ul>
<h2 id="controller-청소하기">Controller 청소하기</h2>
<blockquote>
<ul>
<li>Paging을 구현하면서 인자로 받을게 많아졌다.
<code>페이지번호</code>, <code>사이즈</code>, <code>정렬기준</code>, <code>정렬방향</code> + <code>검색어</code>, <code>카테고리</code>, <code>상태</code></li>
</ul>
</blockquote>
<ul>
<li>그렇다 보니 Controller의 함수가 상당히 지저분해졌다.
<img src="https://velog.velcdn.com/images/kim_table_next/post/bd48abdc-c7f0-4e9a-a370-288f5aba22a1/image.png" alt=""></li>
<li>이런느낌이었다.. 위 사진에 <code>@RequestBody</code>로 <code>검색어</code>, <code>카테고리</code>, <code>상태</code>를 받는 상황이었다.<br></li>
<li>다른 웹서비스를 참고해보았는데, <code>검색어</code>, <code>카테고리</code>, <code>상태</code>등 검색옵션또한 URL에 포함하여 요청을 보내는것을 확인했다.
<img src="https://velog.velcdn.com/images/kim_table_next/post/e66ffaa9-8c22-4422-8238-70dd862067e3/image.png" alt=""></li>
<li>이런 느낌으로 <code>@RequestParam</code>이 7개가 되는 상황이 발생하였다.</li>
<li>이대로는 너무 지저분한것 같아서 방법을 찾다가 <code>@ModelAttribute</code>를 알게 되었다.</li>
</ul>
<h3 id="modelattribute">@ModelAttribute</h3>
<blockquote>
<ul>
<li><code>@ModelAttribute</code>는 클라이언트로부터 일반 HTTP 요청 파라미터나 multipart/form-data 형태의 파라미터를 받아 객체로 사용하고 싶을 때 이용된다.</li>
</ul>
</blockquote>
<ul>
<li><code>@ModelAttribute</code>는 &quot;가장 적절한&quot; 생성자를 찾아 객체를 생성 및 초기화한다.</li>
<li>객체 생성 및 초기화 &gt; Data Binding &gt; Validation 순서로 진행된다.<ul>
<li>Data Binding은 getter/setter가 존재하는 변수에 한해서 이루어진다.
<img src="https://velog.velcdn.com/images/kim_table_next/post/bac778fb-9cfd-4897-addc-3c96e79facf3/image.png" alt=""></li>
</ul>
</li>
<li><code>@ModelAttribute</code>를 사용해서 이렇게 깔끔해졌다.<ul>
<li><code>제목</code>, <code>카테고리</code>, <code>상태</code>는 <code>SearchRequest</code>를 통해 받고, 나머지는 <code>Pageable</code>을 통해 받았다.</li>
</ul>
</li>
</ul>
<h2 id="이미지-업로드">이미지 업로드</h2>
<blockquote>
<ul>
<li>프로젝트에 이미지를 업로드 하는 부분도 있는데, 과연 이미지는 Controller에서 어떻게 받아야할까
찾아보았다.
<img src="https://velog.velcdn.com/images/kim_table_next/post/cf6745c7-a104-4026-bf39-127d5ef338c9/image.png" alt=""></li>
</ul>
</blockquote>
<ul>
<li><code>MultipartFile</code>로 받으면 된다!</li>
</ul>
<h2 id="paging-캐싱-처리하기">Paging 캐싱 처리하기</h2>
<blockquote>
<ul>
<li>지난번 글에서 캐싱처리에서 직렬화, 역직렬화가 무엇인지 다루었다.
<img src="https://velog.velcdn.com/images/kim_table_next/post/50d04030-0bfd-45f9-9c9d-e7ea90b1b1cd/image.png" alt=""></li>
</ul>
</blockquote>
<ul>
<li>Paging을 캐싱처리하던도중 오류가 발생하였는데, 찾아보니 PageImpl이 기본생성자를 갖고있지 않아서 직렬화가 불가능해서 발생한 오류인것으로 파악했다.
<img src="https://velog.velcdn.com/images/kim_table_next/post/68b3a230-a8f3-4287-8c35-59644beee6b3/image.png" alt=""></li>
<li>그래서 PageIm <T>를 상속받는 RestPage <T>를 만들었다. 만들면서 직렬화도 가능하게 생성자도 만들어주었다.</li>
</ul>
<h1 id="오늘의-회고">오늘의 회고</h1>
<p>중간 발표가 끝났다. 발표를 굉장히 잘했다고 칭찬받아서 기분이 매우 좋다.
특히, 프로젝트 아키텍처에 대해서 극찬을 받아서 매우매우 기분이 좋다.
  <img src="https://velog.velcdn.com/images/kim_table_next/post/0cdb1923-0c63-405e-965e-366c459ad8a9/image.png" alt="">
이제 남은건 유저테스트와 최종발표인데, 생각보다 시간이 참 빠른것 같다.
  남은 기간도 최선을 다해서 성공적은 프로젝트가 되도록 노력해야겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring - 간편결제 API(TossPayments) 적용하기]]></title>
            <link>https://velog.io/@kim_table_next/Spring-%EA%B0%84%ED%8E%B8%EA%B2%B0%EC%A0%9C-APITossPayments-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@kim_table_next/Spring-%EA%B0%84%ED%8E%B8%EA%B2%B0%EC%A0%9C-APITossPayments-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 09 Apr 2024 12:01:48 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/kim_table_next/post/f3b07ec9-3451-4852-957c-19c067f3235a/image.png" alt=""></p>
<h1 id="til">TIL</h1>
<h2 id="tosspayments-적용하기">TossPayments 적용하기</h2>
<blockquote>
<ul>
<li>최종프로젝트 15일차, 오늘은 경매가 끝나고 수행되는 결제 시스템을 구현하였다.</li>
</ul>
</blockquote>
<ul>
<li>결제 API로 TossPayments를 적용하였다.</li>
</ul>
<h2 id="tosspayments-결제-흐름도">TossPayments 결제 흐름도</h2>
<blockquote>
<p><img src="https://velog.velcdn.com/images/kim_table_next/post/e4b7a29f-ff1a-4c89-aa1a-c353c3b40fdc/image.png" alt=""></p>
</blockquote>
<ul>
<li><ol>
<li><code>Client</code>는 <code>Toss</code>에게 결제 요청을 한다.</li>
</ol>
<ul>
<li>결제 요청에는 <code>clientKey</code>, <code>orderId(주문번호)</code>, <code>amount(가격)</code>등을 포함한다.</li>
<li>결제 요청은 URL을 통해 이루어지지 않고, <code>Toss</code>의 <code>결제 SDK</code>를 통해 이루어진다.</li>
</ul>
</li>
<li><ol start="2">
<li><code>Toss</code>는 결제 요청에 대한 <code>인가코드(paymentKey)</code>와 <code>orderId</code>, <code>amount</code>등을 <code>Client</code>에게 반환한다.</li>
</ol>
</li>
<li><ol start="3">
<li><code>Client</code>는 <code>Toss</code>에게 반환받은 <code>paymentKey</code>, <code>orderId</code>, <code>amount</code>를 <code>서버</code>에 전달한다.</li>
</ol>
</li>
<li><ol start="4">
<li><code>서버</code>는 <code>Client</code>에게 받은 <code>paymentKey</code>, <code>orderId</code>, <code>amount</code>를 통해 <code>Toss</code>에게 결제 승인 요청을 한다.</li>
</ol>
</li>
<li><ol start="5">
<li><code>Toss</code>는 <code>서버</code>에게 <code>결제 승인 정보(TossPayment)</code> 반환한다.</li>
</ol>
</li>
<li><ol start="6">
<li><code>서버</code>는 <code>Toss</code>에게 반환받은 <code>결제 승인 정보</code>를 DB에 저장하고, <code>Client</code>에게 반환해준다.<br></li>
</ol>
</li>
<li>결제과정의 전체적인 흐름은 이와 같다. 이제 단계별로 코드로 구현해보자.</li>
</ul>
<h3 id="1-client의-결제-요청-toss의-인가코드-반환">1. Client의 결제 요청, Toss의 인가코드 반환</h3>
<blockquote>
<ul>
<li>프론트엔드에 대한 지식은 많이 부족하지만, 최종프로젝트의 프론트엔드가 React로 구성되기에, React기준으로 설명하려고 한다.<br>
</li>
</ul>
</blockquote>
<ul>
<li>🖥️ SDK 준비 및 요청<pre><code>#설치 커맨드
npm install @tosspayments/payment-sdk --save</code></pre></li>
<li>설치가 완료되었다면 컴포넌트에 아래와 같이 import를 해주고, loadTossPayments()를 통해 UI 및 결체요청을 진행하면된다.<pre><code class="language-java">import { loadTossPayments } from &quot;@tosspayments/payment-sdk&quot;;
&gt;
//페이호출 메소드
const handlePayment = (subject) =&gt; {
const random = new Date().getTime() + Math.random(); //난수생성
const randomId = btoa(random); //난수를 btoa(base64)로 인코딩한 orderID
&gt;
if (subject === &quot;카드&quot;) { //간편결제 함수 실행
  loadTossPayments(client_id).then(tossPayments =&gt; {
    tossPayments.requestPayment(subject, {
      amount: reserv.amount, //주문가격
      orderId: `${randomId}`, //문자열 처리를 위한 ``사용
      orderName: orderName(), //결제 이름(여러건일 경우 복수처리)
      customerName: &#39;테스트&#39;, //판매자, 판매처 이름
      successUrl: process.env.REACT_APP_TOSS_SUCCESS,
      failUrl: process.env.REACT_APP_TOSS_FAIL,
    })
  });
}
}</code></pre>
</li>
<li>🖥️ RedirectUrl<pre><code>https://{ORIGIN}/success?paymentKey={PAYMENT_KEY}&amp;orderId={ORDER_ID}&amp;amount={AMOUNT}</code></pre></li>
<li>해당 코드로 결제요청을 하면, Toss API에서 paymentKey, orderId, amount를 포함하여 RedirectUrl의 파라미터로 전달해준다. 이때 해당 정보를 포함하는 객체를 생성하여 Spring 서버로 요청을하는 것이다.</li>
</ul>
<h3 id="2-서버의-결제-승인-요청-toss의-결제-승인-정보-반환">2. 서버의 결제 승인 요청, Toss의 결제 승인 정보 반환</h3>
<blockquote>
<ul>
<li>서버는 Client로 부터 받은 paymentKey, orderId, amount를 통해 Toss에게 결제 승인 요청을 한다.</li>
</ul>
</blockquote>
<pre><code class="language-javascript">curl --request POST \
  --url https://api.tosspayments.com/v1/payments/confirm \
  --header &#39;Authorization: Basic dGVzdF9za196WExrS0V5cE5BcldtbzUwblgzbG1lYXhZRzVSOg==&#39; \
  --header &#39;Content-Type: application/json&#39; \
  --data &#39;{&quot;paymentKey&quot;:&quot;5zJ4xY7m0kODnyRpQWGrN2xqGlNvLrKwv1M9ENjbeoPaZdL6&quot;,&quot;orderId&quot;:&quot;a4CWyWY5m89PNh7xJwhk1&quot;,&quot;amount&quot;:15000}&#39;</code></pre>
<p><img src="https://velog.velcdn.com/images/kim_table_next/post/39ec182a-c0ac-4eb1-b21b-aa7e564b9d0e/image.png" alt=""></p>
<ul>
<li>나는 다음과 같이 구현하였다.</li>
<li><code>Toss</code>에게 결제 승인 요청을 하고, 반환받은 결제 승인 정보를 <code>TossPayment</code> 객체에 저장하여 
기존 결제 테이블을 업데이트 하고, 업데이트 된 결제 정보를 반환해주도록 구현하였다.</li>
<li><code>Toss</code>가 반환해주는 결제 승인 정보의 예시는 다음과 같다. (엄청나게 많다)<pre><code>{
&quot;mId&quot;: &quot;tosspayments&quot;,
&quot;lastTransactionKey&quot;: &quot;9C62B18EEF0DE3EB7F4422EB6D14BC6E&quot;,
&quot;paymentKey&quot;: &quot;5EnNZRJGvaBX7zk2yd8ydw26XvwXkLrx9POLqKQjmAw4b0e1&quot;,
&quot;orderId&quot;: &quot;MC4wODU4ODQwMzg4NDk0&quot;,
&quot;orderName&quot;: &quot;토스 티셔츠 외 2건&quot;,
&quot;taxExemptionAmount&quot;: 0,
&quot;status&quot;: &quot;DONE&quot;,
&quot;requestedAt&quot;: &quot;2024-02-13T12:17:57+09:00&quot;,
&quot;approvedAt&quot;: &quot;2024-02-13T12:18:14+09:00&quot;,
&quot;useEscrow&quot;: false,
&quot;cultureExpense&quot;: false,
&quot;card&quot;: {
  &quot;issuerCode&quot;: &quot;71&quot;,
  &quot;acquirerCode&quot;: &quot;71&quot;,
  &quot;number&quot;: &quot;12345678****000*&quot;,
  &quot;installmentPlanMonths&quot;: 0,
  &quot;isInterestFree&quot;: false,
  &quot;interestPayer&quot;: null,
  &quot;approveNo&quot;: &quot;00000000&quot;,
  &quot;useCardPoint&quot;: false,
  &quot;cardType&quot;: &quot;신용&quot;,
  &quot;ownerType&quot;: &quot;개인&quot;,
  &quot;acquireStatus&quot;: &quot;READY&quot;,
  &quot;receiptUrl&quot;: &quot;https://dashboard.tosspayments.com/receipt/redirection?transactionId=tviva20240213121757MvuS8&amp;ref=PX&quot;,
  &quot;amount&quot;: 1000
},
&quot;virtualAccount&quot;: null,
&quot;transfer&quot;: null,
&quot;mobilePhone&quot;: null,
&quot;giftCertificate&quot;: null,
&quot;cashReceipt&quot;: null,
&quot;cashReceipts&quot;: null,
&quot;discount&quot;: null,
&quot;cancels&quot;: null,
&quot;secret&quot;: null,
&quot;type&quot;: &quot;NORMAL&quot;,
&quot;easyPay&quot;: {
  &quot;provider&quot;: &quot;토스페이&quot;,
  &quot;amount&quot;: 0,
  &quot;discountAmount&quot;: 0
},
&quot;easyPayAmount&quot;: 0,
&quot;easyPayDiscountAmount&quot;: 0,
&quot;country&quot;: &quot;KR&quot;,
&quot;failure&quot;: null,
&quot;isPartialCancelable&quot;: true,
&quot;receipt&quot;: {
  &quot;url&quot;: &quot;https://dashboard.tosspayments.com/receipt/redirection?transactionId=tviva20240213121757MvuS8&amp;ref=PX&quot;
},
&quot;checkout&quot;: {
  &quot;url&quot;: &quot;https://api.tosspayments.com/v1/payments/5EnNZRJGvaBX7zk2yd8ydw26XvwXkLrx9POLqKQjmAw4b0e1/checkout&quot;
},
&quot;currency&quot;: &quot;KRW&quot;,
&quot;totalAmount&quot;: 1000,
&quot;balanceAmount&quot;: 1000,
&quot;suppliedAmount&quot;: 909,
&quot;vat&quot;: 91,
&quot;taxFreeAmount&quot;: 0,
&quot;method&quot;: &quot;카드&quot;,
&quot;version&quot;: &quot;2022-11-16&quot;
}</code></pre></li>
</ul>
<h2 id="toss-payments-api-프론트없이-테스트하기">Toss payments API 프론트없이 테스트하기</h2>
<h3 id="테스트중-발생한-문제-프론트의-부재">테스트중 발생한 문제 (프론트의 부재)</h3>
<blockquote>
<ul>
<li>이제 API 잘 작동하는지 테스트를 해봐야 한다.</li>
</ul>
</blockquote>
<ul>
<li>다행히도 토스 payments에서 API 테스트 기능을 제공했다. <a href="https://docs.tosspayments.com/reference/test">API 테스트</a></li>
<li><strong>그런데 한가지 문제에 직면했다.</strong>
<img src="https://velog.velcdn.com/images/kim_table_next/post/f73b73e9-c570-447f-af9a-ec6d9df0fdb7/image.png" alt="">
<img src="https://velog.velcdn.com/images/kim_table_next/post/5d4f4ece-42d6-4720-b1f7-07fac6d45da3/image.png" alt=""></li>
<li><strong>아무리 찾아봐도 paymentKey를 생성하는 테스트가 없었다.</strong></li>
<li>한마디로 Client와 Toss간의 테스트가 없었다.<br></li>
<li>사실 서버측에서 결제 승인 요청을 보내는 테스트는 토스에서 지원해주는 API테스트 툴을 사용하지 않고, 내가 구현한 코드가 잘 작동하는지 테스트해야 했다.</li>
<li>내가 구현한 코드를 테스트하는건 Client가 서버에 요청하듯 Postman을 통해 테스트하면 되는데, <strong>문제는 테스트에 필요한 인가코드(paymentKey)를 생성 할 수 없다는 것이다.</strong></li>
<li>Client가 Toss에 요청을 보낼때 SDK로만 작동하기에, <strong>프론트를 모르는 나에게는 끔찍한 소식이었다.</strong></li>
</ul>
<h3 id="테스트-툴을-통한-인가코드-만들기">테스트 툴을 통한 인가코드 만들기</h3>
<blockquote>
<ul>
<li><img src="https://velog.velcdn.com/images/kim_table_next/post/e9d43e3a-e0fc-4694-8deb-ef8610d7cf4e/image.png" alt=""></li>
</ul>
</blockquote>
<ul>
<li>토스 payments 개발자 센터에 샌드박스라는 것이 있었다. <a href="https://developers.tosspayments.com/sandbox">토스 payments 개발자 센터 샌드박스</a>
<img src="https://velog.velcdn.com/images/kim_table_next/post/0699537c-014d-4fe5-816b-404406e44ea1/image.png" alt=""></li>
<li>웹에서 결제 API를 쉽게 설명하기 위해서 존재하는 예시를 보여주는 기능이었다.</li>
<li><strong>오고가는 네트워크 요청을 뜯어보면 인가코드를 건질 수 있지 않을까 해서 시도해보았다.</strong>
<img src="https://velog.velcdn.com/images/kim_table_next/post/879c5b7c-5734-4501-869f-93cbe1017303/image.png" alt=""></li>
<li>성공했다! 결제 요청이 끝나고 logs에서 <strong>Toss가 Client에게 반환해주는 내용을 건질 수 있었다!</strong></li>
<li>다행히 프론트엔드를 구현하지 않고 테스트 할 수 있는 방법을 찾아서 다행이다.</li>
</ul>
<h3 id="참고자료">참고자료</h3>
<blockquote>
<ul>
<li><a href="https://docs.tosspayments.com/reference#%EA%B2%B0%EC%A0%9C">토스 payments 개발자 센터</a></li>
</ul>
</blockquote>
<ul>
<li><a href="https://velog.io/@ung6860/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%86%A0%EC%8A%A4-%ED%8E%98%EC%9D%B4%EB%A8%BC%EC%B8%A0#%EF%B8%8F-api-%EC%97%B0%EB%8F%99-%EC%A4%80%EB%B9%84">[프로젝트] 간편결제 API(TossPayments)</a></li>
</ul>
<h1 id="오늘의-회고">오늘의 회고</h1>
<p>오늘은 토스 결제 API를 적용하고, 테스트 해보았다.
개발자를 위한 실제로 결제가 되지는 않는 API였지만, 결제가 완료되면 핸드폰으로 알림까지 오는걸 보고 놀랐다.
결제 API를 간단하게 적용하도록 토스에서 지원해줘서 어렵지 않게 적용할 수 있었던것 같다.
외부 API를 적용하는것이 굉장히 재미있었다!
<img src="https://velog.velcdn.com/images/kim_table_next/post/c8309b8c-bd5d-4fc9-9e13-cd6908ba5d1d/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring - QueryDSL(+Paging, OrderSpecifier)]]></title>
            <link>https://velog.io/@kim_table_next/Spring-QueryDSLPaging-OrderSpecifier</link>
            <guid>https://velog.io/@kim_table_next/Spring-QueryDSLPaging-OrderSpecifier</guid>
            <pubDate>Fri, 05 Apr 2024 15:13:00 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/kim_table_next/post/993525f3-633b-4e21-aedd-ce25d74b80af/image.png" alt=""></p>
<h1 id="til">TIL</h1>
<h2 id="querydsl로-paging-정렬-구현하기">QueryDSL로 Paging, 정렬 구현하기</h2>
<blockquote>
<ul>
<li>최종프로젝트 11일차, 어제는 QueryDSL을 통해 동적쿼리로 상품의 검색을 구현했다면,
오늘은 QueryDSL을 통한 페이징과 정렬을 구현했다.</li>
</ul>
</blockquote>
<ul>
<li>이전에 구현했던 상품 검색 쿼리에 페이징과 정렬을 추가하고자 한다.</li>
<li>페이징 요소는 페이지, 사이즈, 정렬할 기준, 오름차순/내림차순이 있다.<ul>
<li>정렬할 요소에는 입찰자수(bitCount), 현재가격(highestPrice), 조회수(viewCount)가 있다.</li>
<li>이전의 구현했던 상품 검색에 상태값이 추가되었다.</li>
</ul>
</li>
<li>그리고 메인페이지에서 접속한 시간대에 추천 게시물을 노출해주는 API를 추가하였다.</li>
</ul>
<h2 id="구현한-코드">구현한 코드</h2>
<blockquote>
<p><img src="https://velog.velcdn.com/images/kim_table_next/post/a0f95c3d-2e8c-4a8c-80b4-28ba90cbf5a9/image.png" alt=""></p>
</blockquote>
<ul>
<li><code>.offset</code>을 통해 몇번째 페이지를 반환할지 결정하고,</li>
<li><code>.limit</code>를 통해 한 페이지에 몇개의 상품을 반환할지 결정한다.</li>
<li><code>.orderBy</code>를 통해 정렬을 진행한다.</li>
</ul>
<h3 id="pageable">Pageable</h3>
<blockquote>
<p><img src="https://velog.velcdn.com/images/kim_table_next/post/ba4b21d7-ee47-4d5c-b023-7ae36cefad3b/image.png" alt=""></p>
</blockquote>
<ul>
<li><code>option(정렬할 기준)</code>과 <code>direction(오름차순/내림차순)</code>으로 <code>Sort</code>객체를 만들고,</li>
<li><code>페이지</code>와 <code>사이즈</code>와 <code>Sort</code>객체로 위 함수에서 사용되는 Pageable을 만든다.</li>
<li>사실상 정렬과 페이징에 필요한 모든것을 포함하고있다.</li>
</ul>
<h3 id="orderspecifier">OrderSpecifier</h3>
<blockquote>
<ul>
<li>얼핏 봐서는 Pageable이 정렬과 페이징에 필요한 모든것을 포함하고 있기에 바로 페이징과 정렬을 할 수 있을것같지만, 정렬을 하기위해서는 한번 더 가공이 필요하다.
<img src="https://velog.velcdn.com/images/kim_table_next/post/fec93815-71f1-4b36-b192-816f21fb3036/image.png" alt=""></li>
</ul>
</blockquote>
<ul>
<li><code>pageable</code>에서 <code>direction</code> 과 <code>정렬할 기준(여기서는 property)</code>로 <code>OrderSpecifier</code>를 만든다.<ul>
<li>정렬할 기준이 없거나 설계하지 않은 값인겨우는 <code>조회수</code>로 정렬하도록 구현하였다.</li>
</ul>
</li>
<li>이후 <code>OrderSpecifier</code>와 <code>.orderBy()</code>를 사용하여 정렬을 수행한다.</li>
</ul>
<h2 id="구현을-하며-발생한-문제">구현을 하며 발생한 문제</h2>
<blockquote>
<ul>
<li>로직을 성공적으로 구현을 했다고 생각했는데, 문제가 발생했다.</li>
</ul>
</blockquote>
<ul>
<li><strong>입력한 <code>오름차순/내림차순</code> 값과 상관없이, 항상 내림차순으로 정렬을 하는 문제였다.</strong><ul>
<li>처음에는 <strong>Paging과 정렬을 처음 구현해보기에 정렬과 페이징로직에 문제가 있는줄알고 한참을 헤맸다.</strong></li>
<li><strong>GPT에게도 물어봤지만, 답을 찾지 못했다.</strong>
<img src="https://velog.velcdn.com/images/kim_table_next/post/8dee27cd-cc18-412b-901b-aef10429c7b6/image.png" alt=""><br></li>
<li>열심히 디버깅을 하던중에 한가지 실마리를 찾았다.</li>
</ul>
</li>
<li><code>isASC</code> 필드에 <code>true/false</code> 어떠한 값을 입력하던 <code>false</code>로 저장되는것을 발견했다.<ul>
<li>그렇다, <code>isASC</code> 필드에 계속하여 <code>false</code>가 저장되어서 오름차순의 반대, 즉, 내림차순으로만 정렬이 이루어 진것이었다.</li>
</ul>
</li>
</ul>
<h3 id="왜-값이-들어가지-않았을까-json-바인딩">왜 값이 들어가지 않았을까? (JSON 바인딩)</h3>
<blockquote>
<ul>
<li>그렇다면 왜 제대로된 값이 들어가지 않았을까?</li>
</ul>
</blockquote>
<ul>
<li>부트캠프의 튜터님께 여쭤보았고, 원인과 해답을 얻었다.
<img src="https://velog.velcdn.com/images/kim_table_next/post/a33df31e-028d-4519-b703-60e3e3d64490/image.png" alt=""></li>
<li>바로 필드명인 <code>isASC</code>가 문제였다.</li>
<li>요청값인 JSON값을 바인딩하여 optionRequest객체를 생성하는데, <code>is</code>라는 접두사 때문에 바인딩이 이루어 지지 않은것이었다.</li>
<li>정말 생각치도 못한 원인이었어서 굉장히 신선하면서도 화가났다. 그래도 로직이 틀리지 않았다는것에 행복했다.</li>
</ul>
<h2 id="추천-게시물-보여주기">추천 게시물 보여주기</h2>
<blockquote>
<p><img src="https://velog.velcdn.com/images/kim_table_next/post/128366e2-a230-4b6f-ab0b-62d2f973402a/image.png" alt=""></p>
</blockquote>
<ul>
<li>새로운 기능으로 접속 시간대에 따른 추천 상품을 보여주는 기능을 추가하였다.</li>
<li>시간대와 추천 로직은 다음과 같다.</li>
<li>경매는 매일 오전9시 ~ 오후 6시에 진행된다.</li>
<li>경매전(0시~9시)<ul>
<li>당일 경매가 진행될 상품</li>
</ul>
</li>
<li>경매중(9시~18시)<ul>
<li>당일 경매가 진행중인 상품</li>
</ul>
</li>
<li>경매후(18시~0시)<ul>
<li>당일 경매가 종료된 상품</li>
</ul>
</li>
<li>LocalDateTime을 통해 API가 호출된 시점에서 연산을 통해 조회 condition을 만들고,
이를 QueryDSL을 통해 조회에 적용시켰다.
<img src="https://velog.velcdn.com/images/kim_table_next/post/19e7f32a-6590-46a9-9301-6d7a02b957cc/image.png" alt=""></li>
<li>바뀐 로직에 맞춰서 캐시는 날짜와, condition을 key로 구성하였다.</li>
</ul>
<h1 id="오늘의-회고">오늘의 회고</h1>
<blockquote>
<p>QueryDSL이라는걸 처음 시도할때만해도,
<strong>JPA에서 함수 이름만 잘 구성해주면 알아서 해주는데, 왜 굳이 QueryDSL을 사용하지?</strong>
했었는데, 한번 써보니까 훨씬 내가 원하는 방향으로 만들 수 있어서 굉장히 마음에 든다
오늘 QueryDSL에 대해 찾아보다가 보았던 유튜브 영상이다.
평소에 유튜브 알고리즘에 많이 나와서 즐겨보던 유튜버의 강의인줄 알았는데, <strong>유튜버가 아니라 배달의 민족 개발자셨다.. 굉장히 당황을 많이했다.</strong>
영상에서 기억에 남는건 <strong>ORM 과 QueryDSL을 적재적소에 활용하자</strong>는 말이 기억에 남는다.
그리고 이분의 말중에 인상깊었던 말을 인용하면서 오늘 글을 마치도록 하겠다.
기억보다는, 기록으로.
<a href="https://www.youtube.com/watch?v=zMAX7g6rO_Y&amp;t=509s">[우아콘2020] 수십억건에서 QUERYDSL 사용하기</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring - JPQL VS QueryDSL(+ BooleanExpression)]]></title>
            <link>https://velog.io/@kim_table_next/Spring-JPQL-VS-QueryDSL-BooleanExpression</link>
            <guid>https://velog.io/@kim_table_next/Spring-JPQL-VS-QueryDSL-BooleanExpression</guid>
            <pubDate>Thu, 04 Apr 2024 02:40:21 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/kim_table_next/post/01f05d8b-aaa8-4055-919c-65ac86643887/image.png" alt=""></p>
<h1 id="til">TIL</h1>
<h2 id="jpql-vs-querydsl">JPQL VS QueryDSL</h2>
<blockquote>
<ul>
<li>최종프로젝트 10일차, 내가 맡은 상품쪽 로직을 작성하다가 Query문을 수정할 상황이 발생했다.</li>
</ul>
</blockquote>
<h2 id="기존-상품-검색-로직">기존 상품 검색 로직</h2>
<blockquote>
<pre><code class="language-java">    public List&lt;ItemResponse&gt; searchItem(String itemName) {
        List&lt;Item&gt; itemList = itemRepository.findAll();
        List&lt;ItemResponse&gt; itemResponseList = new ArrayList&lt;&gt;();
</code></pre>
</blockquote>
<pre><code>    for (Item item : itemList) {
        if (item.getName().contains(itemName)) {
            itemResponseList.add(new ItemResponse(item));
        }
    }</code></pre><blockquote>
</blockquote>
<pre><code>    return itemResponseList;
}</code></pre><pre><code>- 기존의 상품 검색 로직이다.
  - **DB에 존재하는 모든 상품을 불러오고**
  - 그 목록에서 **검색어가 포함된 상품만을 반환해주고 있다.**
- 검색어와 상관없이 검색을 수행할때 항상 모든 상품을 불러오는것이 **굉장히 비효율적으로 보였다.** 
- DB에 접근하여 Query문을 통해 검색어를 포함한 상품만을 불러오고 싶었다.
  +추가로 **검색어로 시작하는 상품만을 불러오고 싶었다. (구글 검색창 처럼)**
  ![](https://velog.velcdn.com/images/kim_table_next/post/aaf517c3-d615-4aec-aa7d-2d4145ef1d09/image.png)
- 방법을 찾아보던중, JPQL과 QueryDSL을 찾게되었다.

### JPQL
&gt;- JPQL은 JPA의 일부로 **Query를 Table이 아닌 객체(=엔티티) 기준으로 작성**하는 객체지향 쿼리 언어라고 정의할 수 있다.
- JPQL은 **객체를 기준으로 모든 것이 움직이기 때문에** 개발할 때, Table에 매핑되는 객체가 반드시 존재해야 하며 당연하게도 검색할 때도 Table이 아닌 객체를 대상으로 검색해야 한다.
&lt;br&gt;
- JPQL의 특징
  - SQL을 추상화한 JPA의 객체지향 쿼리
  - Table이 아닌 Entity 객체를 대상으로 개발
  - Entity와 속성은 대소문자 구분 (PERSON &lt;&gt; person)
  - 별칭(alias) 사용 필수
  &lt;br&gt;
- JPQL을 실제로 구현하는 방식에는 크게 두 가지 방식이 있다. 
&gt;- 1. EntityManager
```java
@Autowired
EntityManager em;
TypedQuery&lt;User&gt; tq = em.createQuery(&quot;select p from Person p where p.FirstName = :firstName and p.LastName = :lastName&quot;, Person.class);
tq.setParameter(&quot;firstName&quot;, &quot;tablenext&quot;);
tq.setParameter(&quot;lastName&quot;, &quot;kim&quot;); &gt;
List&lt;Person&gt; personList = tq.getResultList();</code></pre><blockquote>
<ul>
<li><ol start="2">
<li>repository interface</li>
</ol>
</li>
</ul>
</blockquote>
<pre><code class="language-java">public interface PersonRepository extends JpaRepository&lt;Person, Long&gt;{
&gt;
    /*    변수 바인딩 시, ?시퀀스 사용하는 경우 */
   @Query(&quot;select p from Person p where p.firstName = ?1 and p.lastName = ?2&quot;)
   Person findPerson(String firstName, String lastName);
&gt;  
   /*    변수 바인딩 시, :이름 사용하는 경우 */
   @Query(&quot;select p from Person p where p.firstName = :firstName and p.lastName = :lastName&quot;)
   Person findPerson2(@Param(&quot;firstName&quot;) String firstName, @Param(&quot;lastName&quot;) String lastName);
}</code></pre>
<ul>
<li><p>두 가지 방식은 구현 방식에 있어서 차이점이 명확하지만,
 공통점이자 문제점으로 <strong>쿼리를 String 형태로 작성하고 있다.</strong></p>
<br></li>
<li><p>JPQL의 문제점을 정리하면 다음과 같다.</p>
<ul>
<li>JPQL의 쿼리는 <strong>문자열(=String) 형태이기 때문에 개발자 의존적 형태</strong></li>
<li><strong>Compile 단계에서 Type-Check가 불가능</strong></li>
<li>RunTime 단계에서 오류 발견 가능 (장애 risk 상승)</li>
</ul>
<h3 id="querydsl">QueryDSL</h3>
<blockquote>
<ul>
<li>위의 JPQL의 문제점을 보완하기 위해 나온 것이 QueryDSL이다.</li>
</ul>
</blockquote>
</li>
<li><p>QueryDSL은 정적 타입을 이용해서 SQL, JPQL을 코드로 작성할 수 있도록 도와주는 오픈소스 빌더 API이다.</p>
<br></li>
<li><p>QueryDSL을 사용하는 목적은 뚜렷하다.</p>
<ul>
<li>기존 방식(Mybatis, JPQL, etc..)은 <strong>모두 문자열(=String) 형태로 쿼리가 작성되었고 이로 인해 Compile 단계에서 Type-Check 불가했다.</strong></li>
<li>이러한 risk를 줄이기 위해 QueryDSL이 등장했고 이를 통해 <strong>Compile 단계에서 Type-check가 가능해진 것이다.</strong><br></li>
</ul>
</li>
<li><p>QueryDSL을 사용하는 방법은 다음과 같다.
<img src="https://velog.velcdn.com/images/kim_table_next/post/1b14ef03-02ff-4f75-833f-b2616c4488bb/image.png" alt=""></p>
</li>
<li><p>QueryDSL은 모든 쿼리에 대한 내용이 <strong>함수 형태로 제공된다.</strong></p>
</li>
<li><p>그렇기 때문에 오류가 존재할 시, <strong>Compile 단계에서 바로 확인 가능하며 이에 따른 후속 조치가 가능</strong>하기 때문에 그만큼 risk가 줄어들게 되는 것이다.</p>
<br></li>
<li><p>QueryDSL의 특징</p>
<ul>
<li>문자가 아닌 코드로 작성</li>
<li><strong>Compile 단계에서 문법 오류를 확인 가능</strong></li>
<li>코드 자동 완성 기능 활용 가능</li>
<li>동적 쿼리 구현 가능<br>

</li>
</ul>
</li>
</ul>
<h3 id="boolean-expression">Boolean Expression</h3>
<blockquote>
<ul>
<li>QueryDSL을 통해 검색을 구현하면서, 고민이 생겼다.</li>
</ul>
</blockquote>
<ul>
<li>검색을 하는 방법으로 이름검색과 카테고리 검색이 있는데,</li>
<li>둘중 하나만 입력해도 작동이 되도록 구현하고 싶었다.<ul>
<li>처음에는, 이름을 통한 <strong>검색과 카테고리를 통한 검색 API를 분리</strong>해야 하나 고민해보았고,
API를 통합하되, <strong>케이스를 나누어서 Query문을 3가지로 작성해야되나 고민해보았다.</strong></li>
<li>고민해보면서 알게된게 바로 Boolean Expression이다.<br></li>
<li>Boolean Expression은 다음과 같다.
<img src="https://velog.velcdn.com/images/kim_table_next/post/7e674b0e-70b1-40d6-bc5a-f91bb0423f78/image.png" alt=""></li>
<li>코드를 보면 단순한 조건에 따라 반환을 해주는것으로 보이지만,</li>
</ul>
</li>
<li><em>BooleanExpression은 null 반환 시 자동으로 조건절에서 제거 된다.*</em></li>
</ul>
<h3 id="querydsl의-like-contains">QueryDSL의 Like, Contains</h3>
<blockquote>
<ul>
<li>위의 코드를 보면 qItem.name.like(itemname  + &quot;%&quot;) 처럼 like문을 리턴해주고있다.</li>
</ul>
</blockquote>
<ul>
<li>like문은 한마디로 정리하자면 Java의 equals와 비슷하다.<ul>
<li>like(str)은 쿼리가 나갈 때 str자체가 나가기 때문에 정확하게 일치해야한다.</li>
<li>이를 통해 %연산을 선택할 수 있다. (itemname+&quot;%&quot;의 경우 itemname으로 시작하는 문자열)</li>
</ul>
</li>
<li>나는 구글의 검색창처럼 itemname으로 시작하는 상품을 조회하고자 하여 itemname+&quot;%&quot;로 구성하였다.
<img src="https://velog.velcdn.com/images/kim_table_next/post/aaf517c3-d615-4aec-aa7d-2d4145ef1d09/image.png" alt=""></li>
<li>Contains는 Java의 contains와 유사하다.<ul>
<li>contains(str)은 쿼리가 나갈 때 %str%가 나간다.</li>
</ul>
</li>
</ul>
<h1 id="오늘의-회고">오늘의 회고</h1>
<p>프로젝트의 일일회고를 작성할 시간이 없을때가 있어서 그날그날 발생한 일에 대해 글을 적는것은 힘들것으로 보인다.
  매일매일 그날의 일을 기록하기 보단, 내용을 기록하는것에 초점을 두려고 한다.
  어제는 하루종일 ECS를 통한 서비스 배포와 CI/CD를 적용해보려고 시도했는데,
  모르는 개념들이 매우 많아서(VPC, Nat 게이트웨이, 서브넷, 라우팅테이블, 등등..) 결국 배포에 실패했다.
  나중에 이에 대해서도 공부해보고, 왜 실패하였는가, 그리고 성공해보려고 한다.
  아직은 AWS친구들이 익숙치 않은것 같다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Git - CI/CD (Github Actions)]]></title>
            <link>https://velog.io/@kim_table_next/Spring-CICD-Github-Actions</link>
            <guid>https://velog.io/@kim_table_next/Spring-CICD-Github-Actions</guid>
            <pubDate>Tue, 02 Apr 2024 13:50:38 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/kim_table_next/post/9c9cdaa3-210b-4e26-8f2c-e80766bb942b/image.png" alt=""></p>
<h1 id="til">TIL</h1>
<h2 id="최종-프로젝트-8일차">최종 프로젝트 8일차</h2>
<blockquote>
<ul>
<li>최종프로젝트 8일차, 오늘은 Docker와 Github Action을 이용한 CI/CD 구축에 대해서 공부했다.</li>
</ul>
</blockquote>
<h2 id="cicd란-무엇일까">CI/CD란 무엇일까</h2>
<blockquote>
<ul>
<li>흔히, CI/CD는 DevOps 엔지니어의 핵심 업무라고 한다.</li>
</ul>
</blockquote>
<ul>
<li>도대체 CI/CD가 무엇이길래 핵심 업무일까?</li>
</ul>
<h3 id="ci-continuous-integration">CI (Continuous Integration)</h3>
<blockquote>
<p><img src="https://velog.velcdn.com/images/kim_table_next/post/2368910e-7604-4254-bc28-9da991c6bf2b/image.png" alt=""></p>
</blockquote>
<ul>
<li>CI는 Continuous Integration 즉, <strong>지속적인 통합</strong>이라는 의미이다.
지속적인 통합이란, 어플리케이션의 <strong>새로운 코드 변경 사항이 정기적으로 빌드 및 테스트 되어 공유 레포지토리에 통합히는 것</strong>을 의미한다. (가능하다면 하루에 여러번까지)</li>
<li>현재는 기능 추가 시마다 commit을 통해 repository에 업데이트를 하는데,
  개발자가 다수일 경우, <strong>repository에 수많은 commit이 쌓이게 된다.</strong></li>
<li>기능별로 빌드/테스트/병합을 하려면 상당히 번거로울 것이다.
  이런 상황에서, <strong>자동화된 빌드&amp;테스트는 원천 소스코드의 충돌 등을 방어하는 장점을 제공</strong>한다.<h4 id="ci의-핵심-목표는버그를-신속하게-찾아-해결하고-소프트웨어의-품질을-개선하고">CI의 핵심 목표는,버그를 신속하게 찾아 해결하고, 소프트웨어의 품질을 개선하고,</h4>
<h4 id="새로운-업데이트의-검증-및-릴리즈의-시간을-단축시키는-것에-있다">새로운 업데이트의 검증 및 릴리즈의 시간을 단축시키는 것에 있다.</h4>
</li>
</ul>
<h3 id="cd-continuous-delivery--continuous-deployment">CD (Continuous Delivery &amp; Continuous Deployment)</h3>
<blockquote>
<p><img src="https://velog.velcdn.com/images/kim_table_next/post/5c922951-9428-4806-8fd3-47a63c69e508/image.png" alt=""></p>
</blockquote>
<ul>
<li>CD는 Continuous Delivery 혹은 Continuous Depolyment 두 용어 모두의 축약어이다.</li>
<li>해석하자면, <strong>지속적인 서비스 제공 혹은 지속적인 배포</strong> 라는 의미이다.</li>
<li><strong>Continuous Delivery</strong>는 공유 레포지토리로 자동으로 Release 하는 것이고,</li>
<li><strong>Continuous Deployment</strong>는 Production 레벨까지 자동으로 deploy 하는 것을 의미한다.
CI가 새로운 소스코드의 빌드, 테스트, 병합까지를 의미하였는데,
CD는 개발자의 변경 사항이 <strong>레포지토리를 넘어, 고객의 프로덕션(Production) 환경까지 릴리즈 되는 것을 의미한다.</strong></li>
</ul>
<h2 id="github-actions">Github Actions</h2>
<blockquote>
<ul>
<li>CI/CD의 기능을 알아보았다. 구현만 된다면, 개발단계를 굉장히 편리하게 해주는 기능이다.</li>
</ul>
</blockquote>
<ul>
<li>그렇다면 어떻게 CI/CD를 구축할 수 있을까? 이번 글은 Github Action을 통해 구축해보았다.</li>
<li><strong>GitHub Actions</strong>는 GitHub 플랫폼에서 제공하는 자동화 및 지속적 통합/지속적 배포(CI/CD) 서비스이다.</li>
<li>Github Actions의 구성 요소는 <code>Workflow</code>, <code>Event</code>, <code>Jobs</code>, <code>Actions</code>가 있다.</li>
<li><strong>Workflow</strong><ul>
<li>한개 이상의 job을 실행할 수 있는 자동화된 작업</li>
<li>yaml 파일로 저장되며 <code>event</code>에 의해 실행 된다.</li>
</ul>
</li>
<li><strong>Event</strong><ul>
<li>workflow 실행을 발동시키는 특정한 활동</li>
<li><code>push event</code>, <code>pull request event</code>, <code>issue event</code> 등이 있다.</li>
</ul>
</li>
<li><strong>Jobs</strong><ul>
<li>한가지 러너안에서 실행되는 여러가지 <code>step</code>들의 모음</li>
<li>각각의 step들은 일종의 <code>shell script</code> 처럼 실행된다.</li>
<li>Step들은 <strong>순서에 따라 실행되며 Step 끼리 데이터들을 공유할 수 있다.</strong></li>
<li>Job은 다른 Job에 의존관계를 가질 수 있으며, <strong>병렬 실행도 가능하다.</strong></li>
</ul>
</li>
<li><strong>Actions</strong><ul>
<li>복잡하고 자주 반복되는 작업을 정의한 <strong>커스텀 어플리케이션</strong></li>
<li>workflow 파일 안에서 자주 <strong>반복되는 코드를 미리 정의해 코드의 양을 줄일 수 있다.</strong></li>
</ul>
</li>
</ul>
<h3 id="github-actions-적용하기">Github Actions 적용하기</h3>
<blockquote>
<ul>
<li>Github Actions를 사용하기 위해서는 프로젝트 폴더의 <code>.github/workflows/</code>디렉토리에
yaml 파일을 작성해주면 된다.</li>
</ul>
</blockquote>
<ul>
<li><strong>yaml 파일의 구조는 다음과 같다.</strong><pre><code class="language-yml">name: Deploy
&gt;
on:
workflow_dispatch:
push:
  branches:
    - main
&gt;
jobs:
deploy:
  runs-on: ubuntu-latest
  steps:
    - name: Checkout
      uses: actions/checkout@v3
    - name: Set up JDK 17
      uses: actions/setup-java@v2
      with:
        java-version: &#39;17&#39;
        distribution: &#39;adopt&#39;
    - name: Grant execute permission for gradlew
      run: chmod +x ./gradlew
    - name: gradlew bootJar
      run: ./gradlew bootJar
    - name: copy jar to server
      uses: appleboy/scp-action@master
      with:
        host: ${{ secrets.SSH_HOST }}
        username: ec2-user
        key: ${{ secrets.SSH_KEY }}
        port: 22
        source: &quot;./build/libs/*.jar&quot;
        target: &quot;~&quot;
        strip_components: 2
&gt;
    - name: SSH Commands
      uses: appleboy/ssh-action@v0.1.6
      with:
        host: ${{ secrets.SSH_HOST }}
        username: ec2-user
        key: ${{ secrets.SSH_KEY }}
        port: 22
        script_stop: true
        script: |
          kill -9 $(ps -ef | grep java | head -n 1 | awk &#39;{print $2}&#39;)
          nohup java -jar *.jar</code></pre>
</li>
<li>name<ul>
<li>workflow의 name을 정의</li>
<li>선택사항이며 깃허브 저장소의 깃허브 액션 탭에서 workflow의 이름을 보여준다.</li>
</ul>
</li>
<li>on<ul>
<li>해당 workflow를 실행시키는 이벤트를 정의</li>
<li>위 코드에서는 main브랜치에 push 이벤트가 발생했을 때 workflow가 실행되도록 정의</li>
</ul>
</li>
<li>jobs<ul>
<li>job의 이름을 정의(위 코드에서는 deploy)</li>
<li>runs-on : 어떤 호스트에서 실행될지 정의 - ubuntu 가상 머신에서 실행 되도록 정의</li>
</ul>
</li>
<li>Steps<ul>
<li>uses: actions/checkout@v3 - 해당 레포지토리를 pull 받고 이동하는 action 대부분의         workflow에서 사용</li>
<li>uses: actions/setup-java@v2 - java를 설치하는 action으로 가상머신안에는 대부분의 프로그래밍 언어가 설치되어 있지 않기 때문에 프로젝트 실행에 필요한 언어들을 action을 통해 다운</li>
<li>run: ./gradlew bootJar - run 키워드를 통해 러너가 실행되는 서버에서 명령어를 실행
(위 코드에서는 gradlew 권한 변경과 jar 파일 생성)</li>
</ul>
</li>
</ul>
<h2 id="github-actions를-통한-cicd-적용">Github Actions를 통한 CI/CD 적용</h2>
<blockquote>
<p><img src="https://velog.velcdn.com/images/kim_table_next/post/5001ca01-0e64-4c1a-8c49-9913a027e07c/image.png" alt=""></p>
</blockquote>
<ul>
<li>CI를 적용한 모습이다.</li>
<li>Pull Request시 Github Actions를 통해 자동으로 테스트를 수행한다!
<img src="https://velog.velcdn.com/images/kim_table_next/post/19f503c3-ad1f-4105-a9ef-3fbc2bb8368f/image.png" alt="">
<img src="https://velog.velcdn.com/images/kim_table_next/post/29e837cb-842b-4c35-a57f-aabb8fb3b059/image.png" alt=""></li>
<li>CD가 적용된 모습이다.</li>
<li>상세정보를 확인해보면 AWS의 콘솔에서 빌드되는것도 확인할 수 있다!</li>
<li>적용을 하면, 이벤트가 발생할때마다 Actions 탭에서 자동으로 yml에 적힌 내용을 수행하는데,
이것으로 상당히 많은 부분을 자동화 할 수 있었다! (테스트, 배포 등등)</li>
</ul>
<h1 id="오늘의-회고">오늘의 회고</h1>
<p>오늘은 Github Actions를 사용하여 CI/CD를 구현해 보았다.
AWS에 서버를 배포해본 경험이 없었어서 모든게 새로웠는데, 게다가 이것을 자동으로 해주는게 너무 신기했다. CI/CD를 구축하기 위해 yml파일을 작성하는 수고가 들지만,
나중을 생각하면 무조건 구축하는것이 이득인 CI/CD였다.
최종 프로젝트에도 CI/CD를 적용하여 PR시 자동으로 테스트와 배포를 해주는것을 추가해봐야겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring - 캐시(Cache), 직렬화(Serialization)]]></title>
            <link>https://velog.io/@kim_table_next/Spring-%EC%BA%90%EC%8B%9CCache-QueryDSL</link>
            <guid>https://velog.io/@kim_table_next/Spring-%EC%BA%90%EC%8B%9CCache-QueryDSL</guid>
            <pubDate>Thu, 28 Mar 2024 17:29:38 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/kim_table_next/post/e5e99543-2046-41b0-b1e4-9899a2250535/image.png" alt=""></p>
<h1 id="til">TIL</h1>
<h2 id="최종프로젝트-3일차">최종프로젝트 3일차</h2>
<blockquote>
<ul>
<li>최종프로젝트 3일차. 오늘은 내가 담당한 상품 도메인에 조회속도를 향상시키고자 캐시를 적용하였다.</li>
</ul>
</blockquote>
<ul>
<li>지난 게시글에서 Redis를 이용한 캐싱에 대해 잠깐 다뤘었는데, 이번에는 직접 적용하면서 겪은 내용과 함께 조금 더 자세하게 다뤄보려고 한다.</li>
</ul>
<h2 id="캐싱이란">캐싱이란?</h2>
<blockquote>
<ul>
<li>일반적으로 모든 데이터는 DB에 저장이 되고, CRUD를 수행할때 DB를 거쳐서 수행하게 된다.
이것이 일반적인 구조이지만, DB에는 치명적인 단점이 있다. <strong>DB는 HDD라는점.</strong>
HDD의 특성상 SSD보다 느리기에, 속도적인 단점이 있다. <strong>메모리의 속도와 용량은 반비례하는법</strong></li>
</ul>
</blockquote>
<ul>
<li>이를 보완하기위해 등장한것이 바로 캐싱이다.<ul>
<li>사실, SSD로 DB를 구성하는것이 제일 좋겠지만, 그렇게되면 <strong>가격이 엄청나게 비쌀것이다.</strong></li>
</ul>
</li>
<li><em>그래서 캐싱이 등장했다.*</em></li>
<li>캐싱은 원본 데이터와는 별개로 <strong>자주 쓰이는 데이터(Hot Data)</strong>들을 복사해둘 캐시 공간을 마련한다. 캐시 공간은 상수 시간 등 낮은 시간 복잡도로 접근 가능한 곳
즉, <strong>접근 시간이 원본 데이터에 접근하는 속도보다 훨씬 빠르게 접근 가능한 곳</strong>을 주로 사용한다.</li>
</ul>
<h2 id="캐시-작동-방식">캐시 작동 방식</h2>
<blockquote>
<ul>
<li><ol>
<li>데이터를 달라는 요청이 들어오면, 원본 데이터가 담긴 곳에 접근하기 전에 <strong>먼저 캐시 내부부터 찾는다.</strong></li>
</ol>
</li>
</ul>
</blockquote>
<ul>
<li><ol start="2">
<li>캐시에 원하는 데이터가 없거나(Cache), 너무 오래되어 최신성을 잃었으면(Expiration) 그때서야 <strong>원본 데이터가 있는 곳에 접근하여 데이터를 가져온다.</strong> 이때 데이터를 가져오면서 캐시에도 해당 데이터를 복사하거나 혹은 갱신한다.</li>
</ol>
</li>
<li><ol start="3">
<li>캐시에 원하는 데이터가 있으면 원본 데이터가 있는 공간에 접근하지 않고 캐시에서 바로 해당 데이터를 제공한다. (Cache hit)</li>
</ol>
</li>
<li><ol start="4">
<li>캐시 공간은 작으므로, 공간이 모자라게 되면 <strong>안 쓰는 데이터부터 삭제하여 공간을 확보한다.</strong></li>
</ol>
</li>
</ul>
<h2 id="캐싱-전략">캐싱 전략</h2>
<blockquote>
<p>캐싱전략은 크게 <code>Look aside Cache</code>와 <code>Write Back</code> 2가지 방식이 있다.
또한 Write에는 Cache 데이터 저장 여부에 따라 <code>Write-Around</code>, <code>Write-Through</code> 2가지가 있다.</p>
</blockquote>
<h3 id="1-look-aside-cachelazy-loading">1. Look aside Cache(Lazy Loading)</h3>
<blockquote>
<p><img src="https://velog.velcdn.com/images/kim_table_next/post/aa90d85b-c1d6-42e4-a573-953b67db6d3c/image.png" alt=""></p>
</blockquote>
<ul>
<li>Lazy Loading의 장점은, Cache에 데이터가 저장되어 있다면,</li>
<li><em>DB에 접근하는 대신 Cache에서 호출함으로써 부하를 줄일 수 있다.*</em> 
Redis가 다운되더라도 바로 <strong>장애로 이어지지 않고 DB에서 데이터를 가지고 올 수 있다.</strong></li>
</ul>
<h3 id="2-write-back">2. Write Back</h3>
<blockquote>
<p><img src="https://velog.velcdn.com/images/kim_table_next/post/202231e7-4a84-44f3-804c-e16d85a490ec/image.png" alt=""></p>
</blockquote>
<ul>
<li><p>Write Back은, <strong>쓰기가 굉장히 빈번할 때 사용한다.</strong></p>
</li>
<li><p><em>DB(Disk)에 바로 저장하지 않고 Cache에 먼저 저장한다.*</em>
그리고 <strong>특정 시점에 Cache 데이터를 DB에 저장한다.</strong> 
DB에 바로바로 저장하는 방식에 비해 DB부하가 줄어든다.
지금 진행하는 최종 프로젝트도 경매 시스템이기에 쓰기가 굉장히 빈번하여 나쁘지 않아보인다.
Write하는 방법에도 <code>Write-Around</code>, <code>Write-Through</code> 방식이 있다.<br><br>
<img src="https://velog.velcdn.com/images/kim_table_next/post/733a26be-71fd-4f45-8c0f-2763396bb488/image.png" alt=""></p>
</li>
<li><p><strong>1. Write-Around</strong></p>
<ul>
<li>Write-Around는 <strong>Cache를 거치지 않고 무조건 DB에 저장을 한다.</strong></li>
<li>그리고 조회 시, <strong>cache miss의 경우에만 DB에서 데이터를 가지고 와서 조회한다.</strong></li>
<li><strong>DB에 발생한 변화가 조회에는 적용이 안될 수 있다. 아마도 특수한 상황에서 쓸 수 있을것 같다.</strong>
<img src="https://velog.velcdn.com/images/kim_table_next/post/018afc40-b224-47ad-8adf-68e9a8b310b9/image.png" alt=""></li>
</ul>
</li>
<li><p><strong>2. Write-Through</strong></p>
<ul>
<li>Write-Through는 DB에 데이터를 저장할 때, <strong>Cache에도 같이 저장하는 것이다.</strong> </li>
<li>장점으로 Cache는 항상 최신의 데이터를 가지고 있지만,</li>
<li><em>저장을 2번 한다는 점*</em>이라는 단점이 존재한다.</li>
</ul>
<h2 id="redis">Redis</h2>
<blockquote>
<ul>
<li>이전에 동시성 제어 게시글에서 언급됬던 Redis가 캐싱을 해준다.
사실 동시성 제어도 Redis가 싱글쓰레드인 점을 이용하여 하나씩 DB에 넘겨주는것.</li>
</ul>
</blockquote>
<ul>
<li><code>Redis</code>의 경우 <code>Memcached</code>와는 달리 다양한 컬렉션(Collection)을 사용한다. </li>
<li>Redis의 컬렉션은 <strong>원자성(Atomic)을 보장</strong>하기 때문에 자원 경쟁을 피할 수 있어</li>
<li><em>트랜잭션 경합의 영향을 덜 받는다.*</em></li>
<li>Redis의 자료구조는 다음과 같다.
<img src="https://velog.velcdn.com/images/kim_table_next/post/10e753e7-192c-47f2-8653-43fa4f60bdb3/image.png" alt=""></li>
</ul>
</li>
</ul>
<h2 id="redis를-이용한-캐싱-적용">Redis를 이용한 캐싱 적용</h2>
<blockquote>
<ul>
<li>Spring에서 다음과 같은 어노테이션으로 캐싱을 적용할 수 있다.</li>
</ul>
</blockquote>
<h3 id="1-cacheable캐시-채우기">1. @Cacheable(캐시 채우기)</h3>
<blockquote>
<p><img src="https://velog.velcdn.com/images/kim_table_next/post/957d35ca-b9b0-4c5c-8064-9a25784baebb/image.png" alt=""></p>
</blockquote>
<ul>
<li><code>@Cacheable(value = &quot;캐시 이름&quot;, key = &quot;키&quot;)</code>라고 생각하면 된다.
(Redis는 <strong>key-value로 데이터가 이루어져 있다.</strong>)</li>
<li>뒷 부분의 <code>cacheManager</code>는 어떤 cacheManager를 사용할 지 명시해준다.
(별도로 지정하지 않으면 default로 설정된 cacheManager를 사용하게 된다.)</li>
<li>마지막으로 <code>unless = &quot;#result == null&quot;</code>은 null값은 저장하지 않겠다는 의미이다.<ul>
<li>해당 코드는 <code>searchRequest</code>라는 DTO를 통해 key를 구성하는데, DTO를 key로 넣기위해서는
<img src="https://velog.velcdn.com/images/kim_table_next/post/b2e9eccf-983d-4485-9767-4c7be0ff8c25/image.png" alt=""></li>
</ul>
</li>
<li><code>@ToString</code> 어노테이션을 붙여주면 된다.
<img src="https://velog.velcdn.com/images/kim_table_next/post/2176ea9a-dcea-4537-91d9-c6c6d392b6af/image.png" alt="">
<img src="https://velog.velcdn.com/images/kim_table_next/post/0bc81362-acf9-458f-aa54-d286f6b7d17a/image.png" alt=""></li>
<li>그러면 이런식으로 저장이 된다.</li>
</ul>
<h3 id="2-cacheevict캐시-제거">2. @CacheEvict(캐시 제거)</h3>
<blockquote>
<p><img src="https://velog.velcdn.com/images/kim_table_next/post/57fff30c-373a-4c48-8c90-2d3cecc560e4/image.png" alt=""></p>
</blockquote>
<ul>
<li>@CacheEvict(value = &quot;삭제할 캐시 이름&quot;, allEntries = true(모든걸 지우겠다는 뜻))</li>
<li>해당 코드는 상품이 생성될때마다 캐시를 제거하도록 구현하였다.(캐시에 생성되는 상품이 없기에)</li>
</ul>
<h3 id="3-cacheput캐시-업데이트">3. @CachePut(캐시 업데이트)</h3>
<blockquote>
<p><img src="https://velog.velcdn.com/images/kim_table_next/post/be884977-d8ce-4e8e-8ba1-7b8d6de0c774/image.png" alt=""></p>
</blockquote>
<ul>
<li>@CachePut 어노테이션을 사용하면 캐시를 업데이트 할 수 있다.</li>
<li>하지만 내가  @CacheEvict를 사용한 이유는, 검색어 기반으로 key가 만들어지기에</li>
<li><em>선택적으로 업데이트 하기보단 삭제 후 조회 시 캐시 저장으로 최신화를 구현하였다.*</em></li>
</ul>
<h3 id="4-caching캐시-그룹화">4. @Caching(캐시 그룹화)</h3>
<blockquote>
<p><img src="https://velog.velcdn.com/images/kim_table_next/post/0ffa1f9b-6da4-4e98-b2cf-14daf7417df9/image.png" alt=""></p>
</blockquote>
<ul>
<li>@Caching 어노테이션을 사용하면 같은 종류의 캐시 메서드를 여러개 그룹화 할 수 있다.</li>
</ul>
<h2 id="직렬화--역직렬화">직렬화 &amp; 역직렬화</h2>
<blockquote>
<ul>
<li>Redis를 사용하면서 문제를 하나 마주했다.</li>
</ul>
</blockquote>
<ul>
<li><strong>조회를 처음 할때는 조회가 되었는데, 그 다음부터는 조회가 되지 않았다.</strong></li>
<li>이러한 문제가 왜 발생했나 찾아보다가, <strong><code>직렬화/역직렬화</code></strong>가 이유였음을 깨닫고 찾아보았다.</li>
</ul>
<h3 id="직렬화serialization와-역직렬화deserialization">직렬화(Serialization)와 역직렬화(Deserialization)</h3>
<blockquote>
<ul>
<li><strong>직렬화 :</strong> 객체들의 데이터를 <strong>연속적인 데이터(스트림)로 변형하여 전송 가능한 형태</strong>로 만드는 것</li>
</ul>
</blockquote>
<ul>
<li><strong>역직렬화 :</strong> 직렬화된 데이터를 다시 <strong>객체의 형태</strong>로 만드는 것</li>
<li><strong>객체 데이터를 통신하기 쉬운 포멧(Byte,CSV,Json..) 형태</strong>로 만들어주는 작업을 직렬화라고 볼 수 있고, 역으로, <strong>포멧(Byte,CSV,Json..) 형태에서 객체로 변환하는 과정</strong>을 역직렬화라고 할 수 있겠다.</li>
</ul>
<h3 id="왜-직렬화역직렬화가-필요할까">왜 직렬화/역직렬화가 필요할까?</h3>
<blockquote>
<p><img src="https://velog.velcdn.com/images/kim_table_next/post/e86b9f20-7cbf-42c6-9c2b-cb4606e6fe21/image.png" alt=""></p>
</blockquote>
<ul>
<li><p>자바에는 원시타입(Primitive Type)이 byte,short,int,long,float,double,boolean,char 총 8가지가 있다.
그리고 그 외 객체들은 주소값을 갖는 참조형 타입이다.</p>
</li>
<li><p><strong>원시타입은 stack에서 값 그 자체로 갖고있다</strong>
외부로 데이터를 전달할때, 값을 일정한 형식의 raw byte 형태로 변경하여 전달할 수 있다.</p>
</li>
<li><p>하지만 <strong>객체의 경우 실제로 Heap 영역에 존재하고 스택에서는 Heap 영역에 존재하는 객체의 주소(메모리 주소)를 갖고 있다.</strong></p>
<ul>
<li>프로그램이 종료되거나 객체가 쓸모없다고 판단되면 <strong>Heap 영역에 있던 데이터는 제거된다.</strong>
따라서 본인 메모리에서도 <strong>데이터가 사라진다.</strong></li>
<li>따라서 <strong>이 주소값의 데이터(실체)를 Primitive 한 값 형식 데이터로 변환하는 작업을 거친 후</strong>, 전달해야한다.</li>
</ul>
<h2 id="왜-redis-캐시를-사용하는데-전송이-필요할까">왜 Redis 캐시를 사용하는데 전송이 필요할까?</h2>
<blockquote>
<ul>
<li>둘다 같은 PC에서 작동중이지만, Redis와 Spring서버는 <strong>엄연히 다른 서버이다.</strong>
따라서 데이터에대한 작업을 위해 직렬화/역직렬화가 필요한것이다!</li>
</ul>
</blockquote>
<ul>
<li>그렇다면 직렬화/역직렬화를 어떻게 구현할까?</li>
</ul>
<h3 id="jdkserializationredisserializer">JdkSerializationRedisSerializer</h3>
<blockquote>
<ul>
<li>JdkSerializationRedisSerializer 는 Default로 적용되는 Serializer로 기본 자바 직렬화 방식을 사용한다.</li>
</ul>
</blockquote>
</li>
<li><p>자바에서는 Serializable 인터페이스만 구현하면 <strong>별도의 작업 없이 사용가능하다.
하지만 여러가지 단점이 있다.</strong></p>
<ul>
<li>SerialVersionUID 설정을 하지 않으면 클래스의 기본 해시값을 SerialVersionUID 로 사용한다. 따라서 <strong>클래스 구조가 조금이라도 변경시 SerialVersionUID 가 달라서 역직렬화에 실패할 수 있다.</strong></li>
<li>만약 개발자가 주의를 가지고 SerialVersionUID 를 설정한다고 하여도, <strong>클래스 내부 필드 타입이 변경되면 역시 역직렬화가 실패할 수 있다.</strong></li>
<li>기본적으로 타입에 대한 정보 등 클래스 메타 정보들을 가지고 있기 때문에 <strong>직렬화시 용량이 비대해진다.</strong></li>
</ul>
</li>
</ul>
<h3 id="genericjackson2jsonredisserializer">GenericJackson2JsonRedisSerializer</h3>
<blockquote>
<ul>
<li>GenericJackon2JsonSerializer 는 <strong>Class Type 을 지정할 필요 없이 자동으로 객체를 Json 형식으로 직렬화해주는 장점</strong>이 있다.
하지만 <strong>직렬화된 데이터가 Class Type 을 포함한다는 단점이 있다.</strong></li>
</ul>
</blockquote>
<h3 id="jackson2jsonredisserializer">Jackson2JsonRedisSerializer</h3>
<blockquote>
<ul>
<li>Jackson2JsonRedisSerializer 은 <strong>@class 필드를 포함하지 않고 Json 으로 저장해준다.</strong>
하지만 <strong>항상 Class Type 정보를 Serializer 에 함께 지정해주어야한다.</strong>
이는 앞서 살펴보았던 GenericJackson2JsonRedisSerializer와 반대 특징을 가진다.</li>
</ul>
</blockquote>
<h3 id="stringredisserializer">StringRedisSerializer</h3>
<blockquote>
<ul>
<li>결론부터 말하자면 StringRedisSerialier 를 직렬화 구현체로 선택하는 것이 <strong>가장 단점을 최소화할 수 있는 방법이다.</strong></li>
</ul>
</blockquote>
<ul>
<li>StringRedisSerializer 는 <strong>String 값을 그대로 저장하는 Serializer 이다.</strong>
그렇기에 Json 타입으로 별도로 변환하는 과정이 필요하지만,</li>
<li><em>앞서 살펴본 직렬화 구현체들의 단점을 보완할 수 있다.*</em></li>
</ul>
<h2 id="조회가-한번만-되었던-문제의-이유와-해결">조회가 한번만 되었던 문제의 이유와 해결</h2>
<blockquote>
<p><img src="https://velog.velcdn.com/images/kim_table_next/post/957d35ca-b9b0-4c5c-8064-9a25784baebb/image.png" alt=""></p>
</blockquote>
<ul>
<li>조회가 한번만 되었던 이유를 알게 되었다.</li>
<li><ol>
<li>처음 조회를 할때는 DB에서 조회를 하여 조회가 가능했지만,</li>
</ol>
</li>
<li><strong>2. 두번째 조회부터는 캐시에서 값을 가져와야하는데, 이부분이 되지 않았던 것이다.</strong></li>
<li>왜 안되었을까 생각해보았고, 결과를 찾았다.
<img src="https://velog.velcdn.com/images/kim_table_next/post/0d1445ee-6121-451b-8a12-423a48e275e9/image.png" alt=""></li>
<li>역직렬화를 하기위해서 다시 객체를 생성해야하는데, 생성자가 구현되어있지 않았기 때문이다.</li>
<li>@AllArgsConstructor 어노테이션을 추가하여 해결하였다.</li>
</ul>
<h2 id="캐싱-부하-테스트">캐싱 부하 테스트</h2>
<blockquote>
<ul>
<li>25개의 상품을 만들고, 1000명의 유저가 각각 10번씩 조회와 수정하는 상황을 테스트 해보았다.</li>
</ul>
</blockquote>
<h3 id="캐싱을-적용하지않았을때조회만-했을경우">캐싱을 적용하지않았을때(조회만 했을경우)</h3>
<blockquote>
<p><img src="https://velog.velcdn.com/images/kim_table_next/post/d56b4d2f-675e-48ec-9c9a-63d01d400254/image.png" alt=""></p>
</blockquote>
<h3 id="캐싱을-적용했을때조회만-했을경우">캐싱을 적용했을때(조회만 했을경우)</h3>
<blockquote>
<p><img src="https://velog.velcdn.com/images/kim_table_next/post/2df2ed4d-27e3-4d27-8719-d83d0043b6cb/image.png" alt=""></p>
</blockquote>
<ul>
<li><strong>약 30%정도 실행시간이 단축되었다.</strong></li>
</ul>
<h3 id="캐싱을-적용하지않았을때조회수정">캐싱을 적용하지않았을때(조회+수정)</h3>
<blockquote>
<p><img src="https://velog.velcdn.com/images/kim_table_next/post/dfd57609-6167-44b9-b1a3-eb3909c3fbfe/image.png" alt=""></p>
</blockquote>
<h3 id="캐싱을-적용했을때조회만-했을경우-1">캐싱을 적용했을때(조회만 했을경우)</h3>
<blockquote>
<p><img src="https://velog.velcdn.com/images/kim_table_next/post/5cf4ba3b-923b-4a31-8ab7-24bb10dd7ce4/image.png" alt=""></p>
</blockquote>
<ul>
<li><strong>약 50%정도 실행시간이 단축되었다.</strong></li>
</ul>
<h1 id="오늘의-회고">오늘의 회고</h1>
<p>오늘은 Redis를 이용한 캐싱 적용을 하면서, 우연치 않게 문제를 만나서 해결하는 과정에서
상당한 수확을 얻은것 같다.
문제의 원인을 파악하기 위해 개념적으로 접근하였고,
개념을 공부하다 보니 문제의 해결법까지 도달했다.
또 성장한것 같아서 기분이 좋다.
그리고 사실 수정할때마다 캐시를 삭제하기에, 캐싱을 적용해도 성능이 그렇게 다르지 않을것이라 예상했는데, 이정도로 성능이 다르게 나올지는 몰랐다.
<strong>캐싱이 DB에 비해서 비용이 굉장히 비싼데, 값어치를 톡톡히 하는것 같다.</strong>
내일은 개인적인 사정으로 프로젝트에 참여하지 못하게 되었다.
참여하지 못하는 만큼, 주말을 통해 꾸준히 성장해보려고 한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[AWS EC2(Elastic Compute Cloud)]]></title>
            <link>https://velog.io/@kim_table_next/Spring-AWS-EC2Elastic-Compute-Cloud</link>
            <guid>https://velog.io/@kim_table_next/Spring-AWS-EC2Elastic-Compute-Cloud</guid>
            <pubDate>Wed, 27 Mar 2024 14:13:05 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/kim_table_next/post/4bc888bf-b354-4c42-8b08-3a8346447a3c/image.png" alt=""></p>
<h1 id="til">TIL</h1>
<h2 id="aws-ec2elastic-compute-cloud란">AWS EC2(Elastic Compute Cloud)란?</h2>
<blockquote>
<ul>
<li>아마존 웹 서비스(AWS)에서  제공하는 <strong>클라우드 컴퓨팅 서비스</strong>이다.</li>
</ul>
</blockquote>
<ul>
<li>한마디로 서버용 컴퓨터를 원격으로 빌리는 것이다.</li>
<li>이름이 <strong>Elastic(탄력있는)</strong>인 이유는 <strong>성능과 용량을 자유롭게 설정할 수 있고, 사용한 만큼 비용을 지불</strong>하기 때문이다.</li>
<li>이름이 EC2인 이유는 <strong>E</strong>lastic <strong>C</strong>ompute <strong>C</strong>loud 뒤에 C가 2개있어서 그런것이 아닐까 하는 생각이다.</li>
</ul>
<h2 id="ec2를-사용하는-이유">EC2를 사용하는 이유</h2>
<blockquote>
<ul>
<li>사실 서버를 구동하는건 <strong>일반 컴퓨터에서도 충분히 가능</strong>하다.
(다들 마인크래프트 서버를 열어본 경험이 있을것이다)</li>
</ul>
</blockquote>
<ul>
<li>그렇다면 EC2의 장점은 무엇일까<br>
<img src="https://velog.velcdn.com/images/kim_table_next/post/90c1608b-4865-44d9-8e8f-026d56dc0690/image.png" alt=""><br></li>
<li><ol>
<li>물리 서버를 도입하는 과정보다 <strong>훨씬 간편하고 빠르다</strong>.</li>
</ol>
</li>
<li><ol start="2">
<li>서버라는게 트래픽이 얼마나 올지 모르기때문에 갑자기 트래픽이 몰릴경우 추가로 장비를 구입해야 하는데, EC2는 <strong>클릭 몇 번으로 서버를 증설할 수 있다.</strong></li>
</ol>
</li>
<li><ol start="3">
<li>그리고 반대의 상황으로 장비를 빵빵하게 구비해놓았는데 트래픽이 적다면? <strong>엄청난 비용손해</strong>.
하지만 EC2는 <strong>클릭 몇번으로 서버를 증감하여 비용절감을 할 수 있다</strong>.</li>
</ol>
</li>
</ul>
<h2 id="ec2-인스턴스">EC2 인스턴스</h2>
<blockquote>
<ul>
<li>보통 EC2로 서버를 구축하면 <code>EC2 인스턴스</code>라는 단어를 자주 볼 수 있다.</li>
</ul>
</blockquote>
<ul>
<li><strong>EC2 인스턴스를 생성한다</strong>는 것은 <strong>AMI을 토대로 운영체제, CPU, RAM 혹은 런타임 등이 구성된 컴퓨터를 빌리는 것</strong>이다. 한마디로 <strong>EC2 인스턴스 생성 = 서버컴퓨터 빌리기</strong>
<img src="https://velog.velcdn.com/images/kim_table_next/post/fe3d6954-e994-4c49-b3fa-711bf8b35d19/image.png" alt=""></li>
</ul>
<h3 id="amiamazon-machine-image">AMI(Amazon Machine Image)</h3>
<blockquote>
<ul>
<li>AMI는 소프트웨어의 구성이 기재된 탬플릿이다. <a href="https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/finding-an-ami.html">AMI(Amazon Machine Image)</a><br>
![](https://velog.velcdn.com/images/kim_table_next/post/1f5445fb-4c49-4672-a02c-3702f42b4f68/image.png)
<br>
</li>
</ul>
</blockquote>
<ul>
<li>AMI를 토대로 원하는 <strong>운영체제, 런타임, RAM, 용량, CPU등이 이미 세팅된 EC2 인스턴스를 생성할 수 있다</strong>. <strong>한마디로 주문서 같은 셈</strong>.</li>
<li>사용 할 수 있는 대표적인 OS<ul>
<li>Amazon Linux2</li>
<li>CentOS</li>
<li>Red Hat Enterprise Linux</li>
<li>Windows Server</li>
<li>Ubuntu</li>
</ul>
</li>
</ul>
<h2 id="ec2-인스턴스-유형">EC2 인스턴스 유형</h2>
<blockquote>
<ul>
<li>다시 EC2 인스턴스 이야기로 돌아가서, EC2 인스턴스의 유형을 알아보도록 하겠다.</li>
</ul>
</blockquote>
<ul>
<li>EC2 인스턴스에는 굉장히 많은 종류가 있다.(699개나 있다)<br>
<img src="https://velog.velcdn.com/images/kim_table_next/post/28d7efc5-ff1b-4f60-9a5b-e878a5645f0d/image.png" alt=""><br></li>
<li>이름의 구조가 심상치 않은데, 의미는 다음과 같다.
<br><img src="https://velog.velcdn.com/images/kim_table_next/post/a5b11a80-3161-4661-92ca-75cb434a1bb2/image.png" alt=""><br></li>
<li><strong>t4g.large</strong>를 예로 들자면 이런 의미이다.<ul>
<li>타입 : <strong>t</strong></li>
<li>세대 : <strong>4세대</strong></li>
<li>추가기능 : <strong>d</strong></li>
<li>인스턴스 크기 : <strong>large</strong></li>
</ul>
</li>
<li>그렇다면 타입마다 다른점은 뭘까?
<br><img src="https://velog.velcdn.com/images/kim_table_next/post/9bafcdfe-707b-4304-aefc-ab33f0ab246b/image.png" alt=""><br></li>
<li>타입은 크게 <strong>범용, 컴퓨팅 최적화, 메모리 최적화, 저장 최적화</strong>로 나뉘고, 또 세부적으로 더 나뉜다.</li>
<li><strong>범용</strong><ul>
<li><strong>M</strong> : 범용(vCPU 1개 / 4GB 메모리)</li>
<li><strong>A</strong> : ARM 기반</li>
<li><strong>T</strong> : 버스트가 가능한 CPU</li>
<li><strong>Mac</strong> : macOS</li>
</ul>
</li>
<li><strong>컴퓨팅 최적화</strong><ul>
<li><strong>C</strong> : 컴퓨팅 최적화</li>
<li><strong>F</strong> : FPGA용</li>
<li><strong>G</strong> : GPU용</li>
<li><strong>INF</strong> : 머신러닝용</li>
</ul>
</li>
<li><strong>메모리 최적화</strong><ul>
<li><strong>R</strong> : 초대형 메모리용</li>
<li><strong>X</strong> : 랜덤 엑세스 메모리용</li>
<li><strong>P</strong> : 프리미엄 GPU용</li>
<li><strong>Z</strong> : 고주파수용</li>
</ul>
</li>
<li><strong>저장 최적화</strong><ul>
<li><strong>H</strong> : HDD용(최대 16TB)</li>
<li><strong>I</strong> : NVMe용</li>
<li><strong>D</strong> : 고밀도 스토리지용(48TB)<br></li>
</ul>
</li>
<li>이와같이 굉장히 많은 타입이 있다.</li>
<li><strong>서버의 특성마다 특화된 타입으로 EC2 인스턴스를 구성</strong>하는게 중요할 것으로 보인다.</li>
</ul>
<h2 id="ec2-인스턴스-요금유형">EC2 인스턴스 요금유형</h2>
<blockquote>
<ul>
<li>EC2는 <strong>타입도 여러가지인데, 요금유형도 여러가지다.</strong><del>(과연 인스턴스 경우의 수가 몇일까)</del><br>
<img src="https://velog.velcdn.com/images/kim_table_next/post/31fb33ac-bb8b-4672-8ad4-0c80c49a2f94/image.png" alt=""><br></li>
</ul>
</blockquote>
<ul>
<li><strong>온 디맨드 인스턴스 (On Demand Instance)</strong> : 필요할 때 바로 생성해서 사용할 수 있는 방식. </li>
<li><em>3가지 방식 중 요금이 가장 비싸다*</em>(원래 피시방도 후불요금이 더 비싼것 처럼)<ul>
<li><strong>공유 인스턴스(Shared tenancy)</strong> : 하나의 물리적인 서버에 여러 개의 EC2인스턴스가 실행 → 다른 인스턴스가 서버 자원을 많이 소모한다면 현재 인스턴스의 성능에 영향이 있을 수 있다</li>
<li><strong>전용 인스턴스(Dedicated tenacy)</strong> : 하나의 물리적인 서버에 하나의 EC2인스턴스가 실행 → 서버 내 다른 인스턴스가 없으므로 영향 X, <strong>공유 인스턴스 방식보다 비싸다</strong><br></li>
</ul>
</li>
<li><strong>스팟 인스턴스(Spot Instance)</strong> : <strong>경매 방식의 인스턴스.</strong> 인스턴스의 스펙을 설정하고 원하는 가격을 입력하여 입찰하면 높게 입찰한 사람한테 인스턴스가 할당된다. 해당 스펙의 인스턴스를 다른 사람이 더 높은 가격으로 입찰했다면 <strong>내가 가지고 있는 인스턴스는 종료된다 (세상은 냉정한법)</strong><br></li>
<li><strong>예약 인스턴스 (Reserved Instacne)</strong> : 일정한 예약금을 <strong>선불</strong>로 내면 인스턴스를 <strong>1년 또는 3년동안 예약할 수 있으며 시간당 요금이 대폭 할인</strong>된다. 온 디맨드 인스턴스와 마찬가지로 공유 인스턴스, 전용인스턴스로 나뉜다<ul>
<li><strong>Light 사용률 예약 인스턴스</strong> : 모든 예약 인스턴스 중에서 선결제 금액이 가장 저렴하다. 사용시간이 많지 않을 때 유용하다 → 몇 달만 사용하는 개발 및 테스트와 단기 프로젝트에 적합</li>
<li><strong>Medium 사용률 예약 인스턴스</strong> : Light보다 선결제 금액은 비싸지만 시간당 요금이 저렴하다. → 거의 항상 실행하지만 사용량에 약간의 변화가 있을 때 유용!</li>
<li><strong>Heavy 사용률 예약 인스턴스</strong> : Medium보다 선결제 금액은 비싸지만 모든 예약 인스턴스 중에서 시간당 요금이 가장 저렴하다. → 24시간 상시 가동되어야하는 출시된 제품에 유리!</li>
</ul>
</li>
</ul>
<h2 id="인스턴스-수명-주기과정">인스턴스 수명 주기(과정)</h2>
<blockquote>
<ul>
<li>AMI로부터 실행이 되고나서 종료될 때까지 EC2가 거치는 과정을 말한다</li>
</ul>
</blockquote>
<ul>
<li><p>Amazon EC2 인스턴스는 시작한 순간부터 종료될 때까지 <strong>다양한 상태로 전환</strong>된다<br>
<img src="https://velog.velcdn.com/images/kim_table_next/post/4f9a9669-527c-4b1c-8a83-4c825a3781a5/image.png" alt=""><br></p>
<ul>
<li><strong>1. pending state</strong></li>
<li>제일 처음 AMI가 실행이 되면 준비 상태를 말한다.</li>
<li>EC2를 가동하기 위해서 <strong>가상머신, ENI, EBS 등이 준비되는 과정</strong>이다.<br></li>
<li><strong>2. running state</strong></li>
<li>실제로 EC2를 사용할수 있는 상태를 말한다.</li>
<li>running 상태에서 할수 있는 것 3가지가 있는데 다음과 같다.<br></li>
<li><strong>2-1. 중지</strong></li>
<li>인스턴스를 잠깐 멈춰두는 것</li>
<li>중지 중에는 <strong>인스턴트 요금 미청구</strong></li>
<li>단 EBS 요금, 다른 구성 요소(Elastic IP 등)은 청구</li>
<li>중지 후 재 시작 할때 퍼블릭 IP가 변경됨 <strong>(프라이빗IP는 변경X, 해결하려면 탄력적 IP 사용)</strong></li>
<li><strong>EBS를 사용하는 인스턴스만 중지 가능</strong><br></li>
<li><strong>2-2. 재부팅</strong></li>
<li>인스턴스를 다시 시작 하는 것</li>
<li>중지하고 다시 시작과는 달리, 재부팅 시 퍼블릭IP 변동 X</li>
<li><strong>최대 절전모드</strong></li>
<li><strong>메모리 내용을 보존해서 재 시작시 중단지점에서 시작</strong>할 수 있는 정지모드</li>
<li>어떤 프로그램을 실행시켰을 때 데이터를 하드디스크에서만 가져오는 것이 아니라 메모리에 올려놓는 것</li>
<li><strong>컴퓨터/노트북의 최대 절전 모드와 같은 원리라고 보면 된다.</strong> 우리가 만일 프로그램을 이용하다 만일 프로그램이 켜 상태를 유지하면서 잠시 노트북을 꺼야한다면 최대 절전을 한다. 그리고 다시 노트북을 켰을때 아예 OS 재부팅되는게 아니라, 프로그램이 이어서 돌아가게 된다.<br></li>
<li><strong>3. shutting-down state</strong></li>
<li>인스턴스 종료 중</li>
<li>설정에 따라 EBS도 같이 종료 시킬 수도 있고 EBS는 남기고 인스턴스만 종료 할 수 있다.<br></li>
<li><strong>4. terminated state</strong></li>
<li>완전히 종료, 인스턴스가 영구적으로 삭제된다</li>
<li>인스턴스 미사용시 중지시켜두기만 해도 <strong>많은 비용 절감 할수 있다.</strong></li>
</ul>
<h2 id="ebselastic-block-store">EBS(Elastic Block Store)</h2>
<blockquote>
<ul>
<li>인스턴스 생명 주기에서 EBS가 많이 언급되었는데, EBS는 뭘까?</li>
</ul>
</blockquote>
<ul>
<li>EBS란 <strong>EC2 인스턴스에 장착하여 사용할 수 있는 가상 저장 장치이다.</strong></li>
<li>일반적인 하드디스크나 SSD처럼 인식되기에, <strong>원하는 크기와 성능을 설정할 수 있다.</strong></li>
</ul>
</li>
<li><p>Block은 블록 장치로, 
** Unix/Linux 계열 OS에서 일정한 크기(Block)단위로 읽고 쓰는 저장 장치를 부르는 말이다.**</p>
</li>
</ul>
<h1 id="오늘의-회고">오늘의 회고</h1>
<p>AWS 친구들(EC2, Lambda, S3, RDS, DynamoDB, ElastiCache)중에 처음으로 EC2에 대해 공부해봤는데
생각보다 이렇게 분량이 많을줄 몰랐다. <strong>그래도 알고 쓰는것과 모르고 쓰는것은 엄청난 차이니까</strong>
차근차근 공부해봐야겠다. 하루하루 성장하는거같아서 기분이 좋다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring - 도커(Docker)]]></title>
            <link>https://velog.io/@kim_table_next/Spring-%EB%8F%84%EC%BB%A4Docker</link>
            <guid>https://velog.io/@kim_table_next/Spring-%EB%8F%84%EC%BB%A4Docker</guid>
            <pubDate>Tue, 26 Mar 2024 14:23:25 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/kim_table_next/post/f0c87261-d619-4d5d-be1c-83f0bb90ddb7/image.png" alt=""></p>
<h1 id="til">TIL</h1>
<h2 id="도커docker란">도커(Docker)란?</h2>
<blockquote>
<ul>
<li><strong><code>도커(Docker)</code></strong> : 리눅스 컨테이너에 리눅스 어플리케이션을 <strong><code>프로세스 격리기술</code></strong>을 사용하여 더 쉽게 컨테이너로 실행하고 관리할 수 있게 해주는 오픈소스 프로젝트이다.</li>
<li><em><code>도커 엔진(Docker Engine)</code>*</em> : 컨테이너를 생성하고 관리하는 주체로서 이 자체로도 컨테이너를 제어할 수 있고 다양한 기능을 제공하는 도커의 프로젝트이다.</li>
</ul>
</blockquote>
<h2 id="도커-컨테이너docker-container">도커 컨테이너(Docker Container)</h2>
<blockquote>
<ul>
<li><strong><code>컨테이너(Container)</code></strong>는 어플리케이션을 다른 환경에서 빠르고 안정적으로 실행 할 수 있도록 <strong><code>코드 + 모든 종속성</code></strong>을 패키징하는 표준 소프트웨어 단위이다.</li>
</ul>
</blockquote>
<ul>
<li><strong><code>도커 컨테이너 이미지(Docker Container Image)</code></strong>는 <code>코드, 런타임, 시스템 도구, 시스템 라이브러리 및 설정</code> 등 <strong>어플리케이션을 실행하는 데 필요한 모든 것</strong>을 포함하는 
경량의 독립 실행형 소프트웨어 패키지이다.
[<a href="https://www.docker.com/resources/what-container/">Docker 래퍼런스</a>]</li>
</ul>
<h2 id="가상머신virtual-machine-vs-도커-컨테이너docker-container">가상머신(Virtual Machine) VS 도커 컨테이너(Docker Container)</h2>
<blockquote>
<p>도커 컨테이너는 <strong>어플리케이션을 다른 환경에서 실행할 수 있도록 해주는것</strong>이라고 생각한다.
그런데, 우리가 평소에 <strong>다른 환경을 구동하기위해 가상머신을 이용</strong>할때가 있는데,</p>
</blockquote>
<h4 id="왜-도커를-사용할까"><strong>왜 도커를 사용할까?</strong></h4>
<p><img src="https://velog.velcdn.com/images/kim_table_next/post/b1c9b298-0dee-4dd0-967d-d42c9eec2a0c/image.png" alt=""></p>
<h3 id="도커-컨테이너를-사용하는-이유">도커 컨테이너를 사용하는 이유</h3>
<blockquote>
<p><img src="https://velog.velcdn.com/images/kim_table_next/post/d769fc68-2d4a-495c-bc3e-4dc0b8f2705e/image.png" alt=""></p>
</blockquote>
<ul>
<li><strong><code>컨테이너</code></strong>는 기본 운영체제 커널을 동일한 시스템의 다른 컨테이너와 공유하는 격리된 환경에서 어플리케이션을 실행하는 방법이다. (위에서 설명한 <strong><code>프로세스 격리기술</code></strong>)</li>
<li><code>종속성, 필요한 라이브러리 및 바이너리와 함께 어플리케이션</code>을 <strong><code>컨테이너 이미지</code></strong>라고 하는 독립된 패키지로 패키지화하여 여러 컴퓨팅 환경에 쉽게 배포할 수 있다.</li>
<li>가상머신과는 다르게, <strong>하이퍼바이저 위에서 여러 개의 게스트 OS를 싱행하는 대신,
호스트 OS 커널을 사용하여 격리된 여러 개의 컨테이너를 실행한다.</strong></li>
<li>게스트 OS가 아닌 호스트 OS에 컨테이너형 가상화 소프트웨어를 설치하기때문에,</li>
<li><em>기존 가상화에 비해 더 가볍고 효율적이다. (컨테이너의 장점)*</em><h4 id="설치-같은-번거로움을-줄이고-성능도-좋아진다">설치 같은 번거로움을 줄이고, 성능도 좋아진다!</h4>
</li>
</ul>
<h3 id="도커-컨테이너-만드는법">도커 컨테이너 만드는법</h3>
<blockquote>
<p><img src="https://velog.velcdn.com/images/kim_table_next/post/7626961c-8d9c-48eb-b458-f741a0f7693f/image.png" alt=""></p>
</blockquote>
<ul>
<li><ol>
<li><code>docker build</code> 명령어를 통해서 <strong>Docker File을 Docker Image로 만든다.</strong></li>
</ol>
</li>
<li><ol start="2">
<li><code>docker run</code> 명령어를 통해서 <strong>Docker Image를 Docker Container로 만든다.</strong></li>
</ol>
</li>
</ul>
<h1 id="오늘의-회고">오늘의 회고</h1>
<p>로컬에서만 개발하다가 최종 프로젝트에 오면서 배포에 대한 기술이 필요해짐에 따라
배포에 관련된 기술들을 익힐 필요가 생겼다.. (S3, ELB, Docker, 메세지큐 등등..)
처음 접해보는 기술이라 프로젝트를 진행하는데에 차질이 없도록
하루에 1개씩이라도 기술을 찾아보며 익혀야겠다</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Project] Spring - 심화프로젝트(SCV)]]></title>
            <link>https://velog.io/@kim_table_next/Project-Spring-%EC%8B%AC%ED%99%94%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8SCV</link>
            <guid>https://velog.io/@kim_table_next/Project-Spring-%EC%8B%AC%ED%99%94%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8SCV</guid>
            <pubDate>Mon, 25 Mar 2024 01:42:20 GMT</pubDate>
            <description><![CDATA[<h3 id="📄-개요">📄 개요</h3>
<hr>
<p><img src="https://github.com/KIM-TABLE-NEXT/SCV/assets/54929479/de912569-7bb3-48cf-9ccc-401321c7ef37" alt="image"></p>
<ul>
<li><strong>프로젝트 이름</strong> : <strong>SCV (Sparta Coding Virtual workspace)</strong></li>
<li><strong>프로젝트 제작기간</strong> : 2024.03.18 ~ 2024.03.25</li>
<li><strong>프로젝트 설명</strong> : 
여러 사용자들이 프로젝트를 관리하고, 팀원들과 협력하여 작업을 효율적으로 할당하고 처리하는데 도움을 주는 협업 플랫폼<br></li>
</ul>
<h3 id="⚙개발환경">⚙개발환경</h3>
<hr>
<blockquote>
<p><img src="https://velog.velcdn.com/images/kim_table_next/post/57864d0f-ff8d-4af9-822f-d09fe58a5939/image.png" alt=""></p>
</blockquote>
<h3 id="👩🏼🤝👩🏼멤버-구성">👩🏼‍🤝‍👩🏼멤버 구성</h3>
<hr>
<table>
<tbody>
<tr>
<td align="center"><a href="https://github.com/KIM-TABLE-NEXT"><img src="https://avatars.githubusercontent.com/u/54929479?v=4" width="100px;" alt="김상엽"/><br /><sub><b> 김상엽 </b></sub></a><br /></td>
      <td align="center"><a href="https://github.com/SerenityZenDev"><img src="https://avatars.githubusercontent.com/u/74538745?v=4" width="100px;" alt="전석배"/><br /><sub><b> 전석배 </b></sub></a><br /></td>
<td align="center"><a href="https://github.com/zapzookj"><img src="https://avatars.githubusercontent.com/u/154570825?v=4" width="100px;" alt="이종원"/><br /><sub><b> 이종원 </b></sub></a><br /></td>
  <td align="center"><a href="https://github.com/harrisbang2"><img src="https://avatars.githubusercontent.com/u/84154173?v=4" width="100px;" alt="방세훈"/><br /><sub><b> 방세훈 </b></sub></a><br /></td>
    </tr>
  </tbody>
</table>

<ul>
<li>김상엽 - 카드, 댓글 도메인</li>
<li>전석배 - 보드 도메인</li>
<li>이종원 - 컬럼 도메인</li>
<li>방세훈 - 유저 도메인</li>
</ul>
<h3 id="📐-와이어-프레임">📐 와이어 프레임</h3>
<hr>
<blockquote>
<p><img src="https://github.com/KIM-TABLE-NEXT/SCV/assets/54929479/99f2df6d-345f-4f7f-a334-4101c36969a6" alt="스크린샷 2024-03-22 171249"></p>
</blockquote>
<h3 id="🗂️-erd-entity-relationship-diagram">🗂️ ERD (Entity Relationship Diagram)</h3>
<hr>
<blockquote>
<p><img src="https://github.com/KIM-TABLE-NEXT/SCV/assets/54929479/2dcc1d92-8d9e-48ef-8c2a-4542fc214636" alt="스크린샷 2024-03-22 170900"></p>
</blockquote>
<h3 id="📜api">📜API</h3>
<hr>
<blockquote>
<p><img src="https://github.com/KIM-TABLE-NEXT/SCV/assets/54929479/d25a47bd-b93f-4f30-ba45-bcd6ac82c907" alt="스크린샷 2024-03-22 171110">
<img src="https://github.com/KIM-TABLE-NEXT/SCV/assets/54929479/b5ee811d-49ee-4116-9c8a-7aad8835513d" alt="스크린샷 2024-03-22 171115">
<img src="https://github.com/KIM-TABLE-NEXT/SCV/assets/54929479/842f2c7b-566c-4b49-85fe-3017c60c85fa" alt="스크린샷 2024-03-22 171126">
<img src="https://github.com/KIM-TABLE-NEXT/SCV/assets/54929479/d74c62be-43bc-430f-bfd6-b96ad512fcfd" alt="스크린샷 2024-03-22 171134">
<img src="https://github.com/KIM-TABLE-NEXT/SCV/assets/54929479/956d3d17-74dc-4b5a-983c-5332323e4ec8" alt="스크린샷 2024-03-22 171143">
<img src="https://github.com/KIM-TABLE-NEXT/SCV/assets/54929479/68b01c9d-d982-4b71-be93-593622dcb227" alt="스크린샷 2024-03-22 171148">
<img src="https://github.com/KIM-TABLE-NEXT/SCV/assets/54929479/c4df1aa7-b4a0-482b-a107-8db2092d2f84" alt="스크린샷 2024-03-22 171154"></p>
</blockquote>
<h1 id="프로젝트-회고">프로젝트 회고</h1>
<p>이번 프로젝트는 이전 프로젝트들과 다르게 <code>Redis</code>를 적용시켜보았는데,
기존 DB를 통한 구동속도도 나름 빠르다고 생각했는데, <code>Redis</code>는 넘사벽이었다.
<img src="https://velog.velcdn.com/images/kim_table_next/post/b36f759e-1f82-4f7e-b75f-4a3b08708989/image.png" alt="">
<img src="https://velog.velcdn.com/images/kim_table_next/post/5afb0263-032f-462c-a188-c89dd2f779fb/image.png" alt="">
<code>Redis</code>를 이용해 <code>캐싱</code>처리를 하니 수행시간이 <strong>10배가 빨라졌다.</strong></p>
<h3 id="redis를-이용한-캐싱과-분산락을-적용했다는-점이-이번-프로젝트의-차별점이다"><strong>Redis를 이용한 캐싱과 분산락을 적용했다는 점이 이번 프로젝트의 차별점이다!</strong></h3>
<p>이제 내일부터 최종프로젝트가 시작되는데 어떤 주제를 할지 아직 감이 잡히지 않는다.
그래도 여정의 끝이 보이는것 같아 기분이 좋다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring - 동시성 제어 테스트 코드 작성하기(Redisson, H2)]]></title>
            <link>https://velog.io/@kim_table_next/Spring-%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%A0%9C%EC%96%B4-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%BD%94%EB%93%9C-%EC%9E%91%EC%84%B1%ED%95%98%EA%B8%B0Redisson-H2</link>
            <guid>https://velog.io/@kim_table_next/Spring-%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%A0%9C%EC%96%B4-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%BD%94%EB%93%9C-%EC%9E%91%EC%84%B1%ED%95%98%EA%B8%B0Redisson-H2</guid>
            <pubDate>Thu, 21 Mar 2024 13:46:38 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/kim_table_next/post/2ed0c525-6cc7-4dce-8eed-b4f14c7a92fa/image.png" alt=""></p>
<h1 id="til">TIL</h1>
<h2 id="동시성-제어-테스트redisson을-통한-분산-락">동시성 제어 테스트(Redisson을 통한 분산 락)</h2>
<blockquote>
<pre><code class="language-java">    @Transactional
    public CardStatusResponse updateCard(Long cardId, CardUpdateRequest cardUpdateRequest, User user) {
        Card card = getCardById(cardId);
</code></pre>
</blockquote>
<pre><code>    if(!user.getId().equals(card.getOwner().getId()))
        throw new IllegalArgumentException(&quot;해당 카드를 수정할 권한이 없습니다.&quot;);</code></pre><blockquote>
</blockquote>
<pre><code>    RLock lock = redissonClient.getFairLock(&quot;card:&quot; + cardId);
    try{
        if(lock.tryLock(10, 60, TimeUnit.SECONDS)){
            try{
                card.update(cardUpdateRequest);
            } finally {
                lock.unlock();
            }
        }
        else{
            throw new IllegalArgumentException(&quot;다른 유저가 이미 수정중입니다.&quot;);
        }
    } catch (InterruptedException e){
        Thread.currentThread().interrupt();
    }
    return new CardStatusResponse(201, &quot;OK&quot;, card.getId());
}</code></pre><pre><code>- `Redisson`을 사용하여 카드 업데이트 함수에 `분산락`을 적용하였다.

## 테스트 코드
&gt;```java
    @Test
    public void updateCardConcurrencyTest() throws InterruptedException{
        User user = new User(1L);
        int numberOfThreads = 10;
        // 쓰레드 생성
        ExecutorService executorService = Executors.newFixedThreadPool(numberOfThreads);
        // 주어진 수 만큼 이벤트를 기다림
        CountDownLatch latch = new CountDownLatch(numberOfThreads);
&gt;
        for (int i = 1; i &lt;= numberOfThreads; i++) {
            // 각 쓰레드에서 사용할 요청 생성
            CardUpdateRequest cardUpdateRequest = new CardUpdateRequest(1L, &quot;Update Name &quot; + i,
                &quot;Update Description &quot; + i, &quot;Update Color &quot; + i, &quot;startDate&quot;, &quot;endDate&quot;);
&gt;
            int finalI = i;
            executorService.submit(() -&gt; {
                try {
                    System.out.println(finalI + &quot;번째 쓰레드 접근 시작&quot;);
                    cardService.updateCard(1L, cardUpdateRequest, user);
                } finally {
                    latch.countDown();
                    System.out.println(finalI + &quot;번째 쓰레드 접근 종료&quot;);
                }
            });
        }
&gt;
        latch.await(); // 모든 쓰레드의 작업이 완료될 때까지 대기
        executorService.shutdown();
    }</code></pre><ul>
<li>10개의 <code>쓰레드</code>에서 동시에 카드를 수정하도록 테스트 코드를 구현하였다.</li>
</ul>
<h2 id="테스트-결과">테스트 결과</h2>
<blockquote>
<p><img src="https://velog.velcdn.com/images/kim_table_next/post/ba6a4f2e-05e3-44d1-87d6-9803c1663560/image.png" alt=""></p>
</blockquote>
<ul>
<li>쓰레드들이 순차적으로 접근하면서 테스트가 잘 수행되었다.</li>
<li>하지만 문제가 있다.
<img src="https://velog.velcdn.com/images/kim_table_next/post/e90aa978-9011-4f42-8036-c98d8634b07c/image.png" alt=""></li>
<li>테스트가 끝나면 DB에 테스트 결과가 남아버린다.</li>
<li>그래서 매번 테스트를 다시 진행할때 마다 Drop Table을 했었다.</li>
</ul>
<h2 id="h2-database">H2 Database</h2>
<blockquote>
<ul>
<li>H2 Database는 자바로 작성된 관계형 데이터베이스 관리 시스템이다.</li>
</ul>
</blockquote>
<ul>
<li>특징으로는 <code>In-Memory</code> 데이터베이스이기 때문에 <strong>실행할때마다 초기화된다!</strong></li>
</ul>
<h2 id="h2-in-memory-db">H2 in Memory DB</h2>
<blockquote>
<ul>
<li>H2의 적용법은 간단하다.</li>
</ul>
</blockquote>
<ul>
<li>application.properties에 연결해주고, 의존성 추가만 해주면 끝이다!</li>
<li>평소에는 <code>MySQL</code>로 작동하지만 테스트를 진행할때는 <code>H2</code>로 작동하게 하려고 한다.</li>
<li>테스트용 환경을 따로 구축하기 위해서는 <strong>test 디렉토리에 application.properties를 따로 만들어주면 test할때만 적용된다!</strong></li>
</ul>
<h2 id="테스트-코드에-h2-적용하기">테스트 코드에 H2 적용하기</h2>
<blockquote>
<p><img src="https://velog.velcdn.com/images/kim_table_next/post/01e5e383-9577-4ad8-92d4-2460c98ff93e/image.png" alt=""></p>
</blockquote>
<ul>
<li>테스트용 application.properties만 만들어주면 적용은 끝이다!</li>
</ul>
<h2 id="테스트-결과h2-적용후">테스트 결과(H2 적용후)</h2>
<blockquote>
<p><img src="https://velog.velcdn.com/images/kim_table_next/post/556815fc-8e4d-4dad-8e28-5306f5d02c5c/image.png" alt=""></p>
</blockquote>
<ul>
<li>테스트는 이전과 동일하게 동작하지만 <code>MySQL</code>이 아닌 <code>H2</code>로 동작하기에 DB에 아무것도 남지 않는다.</li>
</ul>
<h1 id="오늘의-회고">오늘의 회고</h1>
<p>사실 분산락을 통해 동시성 제어하는 기능을 구현하였지만
이번 프로젝트에는 딱히 동시성을 제어할 부분이 없었다.
그래도 이제는 할줄 안다는것에 의미를 두려고 한다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring - 동시성 제어 실험(Lock, Redis)]]></title>
            <link>https://velog.io/@kim_table_next/Spring-%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%A0%9C%EC%96%B4-%EC%8B%A4%ED%97%98Lock-Redis</link>
            <guid>https://velog.io/@kim_table_next/Spring-%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%A0%9C%EC%96%B4-%EC%8B%A4%ED%97%98Lock-Redis</guid>
            <pubDate>Wed, 20 Mar 2024 11:27:19 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/kim_table_next/post/aa52703a-2e01-4af2-a0ae-9f75c8ae88bb/image.png" alt=""></p>
<h1 id="til">TIL</h1>
<h2 id="동시성-제어">동시성 제어</h2>
<blockquote>
<p>오늘은 동시성 제어에 도전해 보았다.
여태까지 구현했던 코드들은 <strong>로컬서버에서 나 혼자 구동하기 때문에</strong>
딱히 동시성에 대해 생각해보지 않았다.
하지만 최종프로젝트가 다가오면서, 동시성 제어에 도전해보았다.</p>
</blockquote>
<h3 id="설정한-상황선착순-쿠폰-100개-이벤트">설정한 상황(선착순 쿠폰 100개 이벤트)</h3>
<blockquote>
<p><code>Counter</code> 라는 <code>entity</code>에 <code>쿠폰 개수(count)</code>가 <code>100</code>으로 가정하고,
쿠폰을 수령하면 <code>count</code>를 <code>1 감소</code>시키는 로직을 구현하였다.</p>
</blockquote>
<h3 id="1-동시성-고려-x-기존에-구현한-방식">1. 동시성 고려 X (기존에 구현한 방식)</h3>
<blockquote>
<pre><code class="language-java">    @Transactional
    public void decreaseCount() {
        Counter counter = counterRepository.findById(1L).orElseThrow();
        counter.setCount(counter.getCount() - 1);
        counterRepository.save(counter);
    }</code></pre>
</blockquote>
<pre><code>- `Counter`를 DB에서 불러와서
- `count`의 값을 1 감소시키고
- 다시 DB에 `Counter`를 저장했다.

### 1-1. 테스트 결과 (기존에 구현한 방식)
&gt;```java
    @Test @DisplayName(&quot;RDBMS를 사용했을 시 동시성 문제가 발생&quot;)
    void concurrencyTest() {
        System.out.println(&quot;\n\n\n\n[concurrencyTest]&quot;);
        IntStream.range(0, 100).parallel().forEach(i -&gt; counterService.decreaseCount());
        counterService.printCount();
    }</code></pre><ul>
<li><code>decreaseCount()</code>를 반복해서 100번 수행한다면 <strong>당연히 <code>count</code>는 100번 감소하여 0이 될 것이다.</strong></li>
<li>하지만 <strong>병렬로 100번을 동시에 수행</strong>한다면 과연 count는 0이 될까?
<img src="https://velog.velcdn.com/images/kim_table_next/post/c0e0d0b2-e828-44e2-94bf-03badf6ac87a/image.png" alt=""></li>
<li>동시성 문제로 인해서 <strong>0이 아닌 83</strong>이 나오는걸 확인할 수 있다.</li>
<li><strong>감소된 <code>count</code>가 저장되기전에 다른곳에서 조회를 해서</strong> 문제가 발생하는 것으로 보인다.</li>
</ul>
<h3 id="2-동시성-고려-시도-1객체를-여러개-만들고-삭제하기">2. 동시성 고려 시도 1(객체를 여러개 만들고 삭제하기)</h3>
<blockquote>
<ul>
<li>1번에서 실패한 상황을 조금 바꿔보았다.</li>
</ul>
</blockquote>
<ul>
<li>이번에는 <code>Counter</code>객체를 5개 만들고</li>
<li><code>count</code>값이 100인 <code>Counter</code>를 DB에서 불러와서</li>
<li>삭제하고 <code>&quot;내꺼!&quot;</code>를 출력하도록 구현해보았다.</li>
<li>삭제로 한 이유는 삭제를 시도할때 <strong>이미 삭제가 되었다면 <code>Exception</code>이 발생해서 출력이 안될것이라고 예상했기 때문이다.</strong>
<img src="https://velog.velcdn.com/images/kim_table_next/post/a4ea58df-9816-4233-9cba-82d7a94a3aee/image.png" alt="">
<img src="https://velog.velcdn.com/images/kim_table_next/post/d8037b4d-11fa-43d5-90d4-42202fe8e891/image.png" alt=""></li>
</ul>
<h3 id="2-1-테스트-결과-객체를-여러개-만들고-삭제하기">2-1. 테스트 결과 (객체를 여러개 만들고 삭제하기)</h3>
<blockquote>
<p><img src="https://velog.velcdn.com/images/kim_table_next/post/fc42794a-083f-4cdf-b89d-2230c5e85261/image.png" alt=""></p>
</blockquote>
<ul>
<li>나는 분명 쿠폰을 5개만 준비했지만.. 8명이 받아갔다.</li>
<li>심지어 실패했을때 출력하도록한 <code>&quot;까비!&quot;</code> 또한 하나도 출력되지 않았다.</li>
<li>예상이 빗나간 이유를 검색해 보니, <code>JPA</code>에서는 삭제할 <code>Entity</code>가 없어도 <code>Exception</code>을 발생시키지 않는다고 한다. 만약 발생시키고 싶다면 삭제 함수를 오버라이딩해서 예외를 발생하도록 추가해야 한다고 한다!
<img src="https://velog.velcdn.com/images/kim_table_next/post/8e58012c-cf55-4b54-8399-b81fcd442bd9/image.png" alt=""></li>
</ul>
<h3 id="3-동시성-고려-시도-2-lock">3. 동시성 고려 시도 2 (Lock)</h3>
<blockquote>
<ul>
<li>이번에는 <code>Lock</code>을 통해 동시성 문제를 해결해보았다.</li>
</ul>
</blockquote>
<pre><code class="language-java">    public void decreaseCountUsingLock() {
        RLock lock = redissonClient.getFairLock(LOCK_KEY);
        try {
            boolean isLocked = lock.tryLock(10, 60, TimeUnit.SECONDS);
            if (isLocked) {
                try {
                    Counter counter = counterRepository.findById(1L).orElseThrow();
                    counter.setCount(counter.getCount() - 1);
                    counterRepository.save(counter);
                } finally {
                    lock.unlock();
                }
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }</code></pre>
<ul>
<li>간단히 설명하자면 <code>10초</code>동안 락이 풀릴때까지 기다리고, </li>
<li><code>60초</code>가 지나면 락이 자동으로 풀리게 설정하였다.</li>
<li>물론 함수가 끝나면 바로 락이 풀린다.</li>
</ul>
<h3 id="3-1-테스트-결과-lock">3-1. 테스트 결과 (Lock)</h3>
<blockquote>
<p><img src="https://velog.velcdn.com/images/kim_table_next/post/b9c4ef65-7133-4aff-b215-05a7462a2fd8/image.png" alt=""></p>
</blockquote>
<ul>
<li><code>Lock</code>을 걸어서 100개의 시도가 순서대로 처리되어서 성공적으로 쿠폰이 <code>0개</code> 남았음을 확인할 수 있었다!</li>
</ul>
<h3 id="4-동시성-고려-시도-3-redis">4. 동시성 고려 시도 3 (Redis)</h3>
<blockquote>
<ul>
<li>앞의 <code>Lock</code>을 활용할때도 <code>Redis</code>를 사용하였지만,</li>
</ul>
</blockquote>
<ul>
<li>이번에는 <strong><code>Redis</code>가 <code>싱글쓰레드</code>라는 특징</strong>을 활용하여 동시성을 제어해보겠다.<pre><code class="language-java">  public void decrementCounter() {
      ValueOperations&lt;String, String&gt; ops = stringRedisTemplate.opsForValue();
      ops.decrement(&quot;counter&quot;);
  }</code></pre>
</li>
</ul>
<h3 id="4-1-테스트-결과-redis">4-1. 테스트 결과 (Redis)</h3>
<blockquote>
<p><img src="https://velog.velcdn.com/images/kim_table_next/post/af0fe93e-5012-4b2a-8cb9-806929efed52/image.png" alt=""></p>
</blockquote>
<ul>
<li><code>Redis</code>가 싱글쓰레드라는 특징을 갖고있어서, 100개의 시도가 순서대로 처리되어서 성공적으로 쿠폰이 <code>0개</code>남았다!</li>
</ul>
<h1 id="오늘의-회고">오늘의 회고</h1>
<p>여태까지 동시성 문제에 대해 고려하지 않았었다.
왜냐면 여태까지 진행했던 프로젝트들은 모두 로컬 서버에서 혼자 돌렸기때문에
내가 한명인 이상 동시성 문제가 발생할 수 없고, 정말 천문학적인 트래픽이 아니라면 동시에 처리될 확률이 엄청나게 낮기 때문이다.
그런데 생각해보면 평소에 배달대행앱을 자주 사용하는데, 이러한 선착순 이벤트를 구현한다면 동시성 문제를 반드시 해결해야한다는 생각이 들어서 오늘 실험을 해보았다.</p>
<p>실험을 하면서 가장 충격받은점은, <strong>JPA는 삭제에 실패해도 예외를 발생시키지 않는다는 점이다.</strong></p>
<p><strong>평소에 굉장히 편리해서 아주 좋아했던 내 친구 JPA였는데,
이러한 허점이 있다는 사실을 처음 알았다.</strong></p>
<p>오늘 사용했던 <strong><code>Redis</code></strong>에 대해서는 사실 많이 아는것이 없다..(오늘 처음봤다)
추후에 <code>Redis</code>와 친해진 뒤에, <code>Redis</code>에 대한 정리글을 올릴 예정이다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[프로그래머스] - 정수 삼각형(Bottom-up)]]></title>
            <link>https://velog.io/@kim_table_next/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EC%A0%95%EC%88%98-%EC%82%BC%EA%B0%81%ED%98%95Bottom-up</link>
            <guid>https://velog.io/@kim_table_next/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EC%A0%95%EC%88%98-%EC%82%BC%EA%B0%81%ED%98%95Bottom-up</guid>
            <pubDate>Mon, 18 Mar 2024 09:30:33 GMT</pubDate>
            <description><![CDATA[<h1 id="정수-삼각형">정수 삼각형</h1>
<blockquote>
<p>문제 설명
<img src="https://velog.velcdn.com/images/kim_table_next/post/ab240240-0ab1-44b9-8304-9b4a9fc8303c/image.png" alt=""></p>
</blockquote>
<p>위와 같은 삼각형의 꼭대기에서 바닥까지 이어지는 경로 중, 거쳐간 숫자의 합이 가장 큰 경우를 찾아보려고 합니다. 아래 칸으로 이동할 때는 대각선 방향으로 한 칸 오른쪽 또는 왼쪽으로만 이동 가능합니다. 예를 들어 3에서는 그 아래칸의 8 또는 1로만 이동이 가능합니다.</p>
<blockquote>
</blockquote>
<p>삼각형의 정보가 담긴 배열 <code>triangle</code>이 매개변수로 주어질 때, 거쳐간 숫자의 최댓값을 return 하도록 solution 함수를 완성하세요.</p>
<h1 id="해결한-로직">해결한 로직</h1>
<blockquote>
<pre><code class="language-java">class Solution {
    public int solution(int[][] triangle) {
        int answer = 0;
        int stage = triangle.length-2;
        int index = 0;
        for(int i=stage; i&gt;-1; i--)
            for(int j = 0; j&lt;triangle[i].length; j++){
                if(triangle[i+1][j]&gt;=triangle[i+1][j+1])
                    triangle[i][j] += triangle[i+1][j];
                else
                    triangle[i][j] += triangle[i+1][j+1];
            }
                return triangle[0][0];
    }
}</code></pre>
</blockquote>
<pre><code>- 처음에는 방향이 잡히지 않아 모든 경우의 수를 고려하는 로직을 구현하려고 하였는데,
**재귀함수를 짜면서 이건 잘못된 길이 분명함을 깨달았다. 
(최악의 경우 2^500개의 경우의 수가 나온다)**&lt;br&gt;&lt;br&gt;
- 그래서 어떻게 해야할까 고민하다가 삼각형의 밑에서 부터 경우의 수를 줄이며 올라가는 방식을 생각해냈다.
![](https://velog.velcdn.com/images/kim_table_next/post/37a76a7f-b8e7-4150-806f-3139b6a2c834/image.png)
- 해결을 하고 찾아보니 문제 해결법 중, Bottom-up 방식이 바로 이러한 방식이라고 한다.

## Bottom-up 방식
&gt;- 우리말로 `상향식`이라고도 한다. 가장 작은 문제들부터 답을 찾아 가면서 마지막에는 전체 문제의 답을 구하는 방식이다.
- 보통 반복문을 이용해 구현하게 된다. 재귀 호출을 하지 않으므로 시간과 메모리의 사용량이 상대적으로 적다는 이점이 있다.
![](https://velog.velcdn.com/images/kim_table_next/post/fd5edcae-a923-423a-b5c4-ca98a079a75a/image.png)

# 오늘의 회고
오늘은 심화프로젝트 설계와 알고리즘 문제(정수 삼각형)를 풀었다
풀다가 정말 기발한 아이디어라고 생각해서 신났는데,
찾아봤더니 유명한 알고리즘 문제풀이법 중 하나라고 해서 조금 실망했다.
그래도 2^500가지의 경우의 수를 보지않는 효율적인 로직을 생각해내서 뿌듯하다.

</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[[프로그래머스] - 귤 고르기(Collections.reverse)]]></title>
            <link>https://velog.io/@kim_table_next/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EA%B7%A4-%EA%B3%A0%EB%A5%B4%EA%B8%B0Collections.reverse</link>
            <guid>https://velog.io/@kim_table_next/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EA%B7%A4-%EA%B3%A0%EB%A5%B4%EA%B8%B0Collections.reverse</guid>
            <pubDate>Fri, 15 Mar 2024 02:24:56 GMT</pubDate>
            <description><![CDATA[<h1 id="귤-고르기">귤 고르기</h1>
<blockquote>
<p>문제 설명
경화는 과수원에서 귤을 수확했습니다. 경화는 수확한 귤 중 <code>k</code>개를 골라 상자 하나에 담아 판매하려고 합니다. 그런데 수확한 귤의 크기가 일정하지 않아 보기에 좋지 않다고 생각한 경화는 귤을 크기별로 분류했을 때 서로 다른 종류의 수를 최소화하고 싶습니다.</p>
</blockquote>
<p>예를 들어, 경화가 수확한 귤 8개의 크기가 <code>[1, 3, 2, 5, 4, 5, 2, 3]</code> 이라고 합시다. 경화가 귤 6개를 판매하고 싶다면, 크기가 1, 4인 귤을 제외한 여섯 개의 귤을 상자에 담으면, 귤의 크기의 종류가 2, 3, 5로 총 3가지가 되며 이때가 서로 다른 종류가 최소일 때입니다.</p>
<blockquote>
</blockquote>
<p>경화가 한 상자에 담으려는 귤의 개수 <code>k</code>와 귤의 크기를 담은 배열 <code>tangerine</code>이 매개변수로 주어집니다. 경화가 귤 <code>k</code>개를 고를 때 크기가 서로 다른 종류의 수의 최솟값을 return 하도록 solution 함수를 작성해주세요.</p>
<h1 id="해결한-로직">해결한 로직</h1>
<blockquote>
</blockquote>
<pre><code class="language-java">import java.util.*;
&gt;
class Solution {
    public int solution(int k, int[] tangerine) {
        int answer = 0;
        Map&lt;Integer, Integer&gt; map = new HashMap&lt;&gt;();
        &gt;
        for(int orange : tangerine){
            if(map!=null&amp;&amp;map.containsKey(orange))
                map.put(orange, map.get(orange)+1);
            else
                map.put(orange, 1);
        }
        &gt;
        Set&lt;Integer&gt; keySet = map.keySet();
        List&lt;Integer&gt; size = new ArrayList&lt;&gt;();
        for(int key : keySet)
            size.add(map.get(key));
        &gt;
        Collections.sort(size);
        Collections.reverse(size);
        for(int s : size){
            if(k&gt;s){
                k = k-s;
                answer++;
            }
            else if(k&lt;=s){
                answer++;
                break;
            }
        }
       &gt;     
        return answer;
    }
}</code></pre>
<ul>
<li><code>Map</code>에 귤의 &lt;사이즈, 개수&gt;를 저장하고, 개수가 많은 사이즈의 귤 부터 채우는 방식을 생각했다.</li>
<li>그런데 그러려면 <strong>귤 리스트를 내림차순으로 정렬해야한다.</strong></li>
<li><code>Comparator</code>를 오버라이딩해서 구현해야 하나 생각했다가, 
<code>Collections.sort</code>를 통해 오름차순으로 정렬하고, 리스트를 뒤집는 방법을 생각했다.</li>
<li><strong><code>Collections.reverse</code>를 사용하면 리스트를 뒤집을 수 있다.</strong>
<img src="https://velog.velcdn.com/images/kim_table_next/post/4945c9d3-49cb-4b3c-aeb7-7c76bb334e18/image.png" alt=""></li>
</ul>
<h1 id="오늘의-회고">오늘의 회고</h1>
<p>평소에 구현할때는 잘 안쓰는 기능들이지만,
알고리즘 문제에서는 아주 쏠쏠한 녀석들이 참 많다.
많이 풀면서 이 녀석들과 친해져야겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[프로그래머스] - 시소 짝꿍(Double, Long)]]></title>
            <link>https://velog.io/@kim_table_next/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EC%8B%9C%EC%86%8C-%EC%A7%9D%EA%BF%8DDouble-Long</link>
            <guid>https://velog.io/@kim_table_next/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EC%8B%9C%EC%86%8C-%EC%A7%9D%EA%BF%8DDouble-Long</guid>
            <pubDate>Thu, 14 Mar 2024 03:10:53 GMT</pubDate>
            <description><![CDATA[<h1 id="시소-짝궁">시소 짝궁</h1>
<blockquote>
<p>문제 설명
어느 공원 놀이터에는 시소가 하나 설치되어 있습니다. 이 시소는 중심으로부터 <code>2(m)</code>, <code>3(m)</code>, <code>4(m)</code> 거리의 지점에 좌석이 하나씩 있습니다.
이 시소를 두 명이 마주 보고 탄다고 할 때, 시소가 평형인 상태에서 각각에 의해 시소에 걸리는 토크의 크기가 서로 상쇄되어 완전한 균형을 이룰 수 있다면 그 두 사람을 시소 짝꿍이라고 합니다. 즉, 탑승한 사람의 무게와 시소 축과 좌석 간의 거리의 곱이 양쪽 다 같다면 시소 짝꿍이라고 할 수 있습니다.
사람들의 몸무게 목록 <code>weights</code>이 주어질 때, 시소 짝꿍이 몇 쌍 존재하는지 구하여 return 하도록 solution 함수를 완성해주세요.</p>
</blockquote>
<h1 id="오버플로우-문제long으로-해결">오버플로우 문제(Long으로 해결)</h1>
<blockquote>
<pre><code class="language-java">import java.util.*;
</code></pre>
</blockquote>
<p>class Solution {
    public long solution(int[] weights) {
        long answer = 0;
        Map&lt;Long, Integer&gt; weightMap = new HashMap&lt;Long, Integer&gt;();
        for(int weight : weights){
            Long weight_long = Long.valueOf(weight);
            if(weightMap.containsKey(weight_long))
                weightMap.put(weight_long, weightMap.get(weight_long)+1);
            else
                weightMap.put(weight_long,1);
        }
        &gt;
        for(Long weight_long : weightMap.keySet()){ 
            if(weightMap.containsKey(weight_long)) // 1 : 1 nC2
                answer+= weightMap.get(weight_long)<em>(weightMap.get(weight_long)-1)/2;
            &gt;<br>            if(weightMap.containsKey(weight_long</em>2/3)) // 2m : 3m
                answer+= weightMap.get(weight_long)<em>weightMap.get(weight_long</em>2/3);
            &gt;
            if(weightMap.containsKey(weight_long/2)) // 2m : 4m
                answer+= weightMap.get(weight_long)<em>weightMap.get(weight_long/2);
            &gt;
            if(weightMap.containsKey(weight_long</em>3/4)) // 3m : 4m
                answer+= weightMap.get(weight_long)<em>weightMap.get(weight_long</em>3/4);</p>
<blockquote>
</blockquote>
<pre><code>    }
    &gt;
    return answer;
}</code></pre><p>}</p>
<pre><code>- 짝꿍 쌍을 구하는 과정에서 `answer`는 `long`형 이기에 문제가 없다고 판단하였는데,
**`Long += Integer * Integer` 의 형태였어서 오버플로우 문제가 있었다.**
- 무게를 저장하는 `Map`을 `&lt;Long, Integer&gt;`에서 `&lt;Long, Long&gt;`으로 변경하여 해결하였다.
&gt;
```java
        Map&lt;Long, Long&gt; weightMap = new HashMap&lt;Long, Long&gt;(); //&lt;Long, Long&gt;으로 변경
        for(int weight : weights){
            Long weight_long = Long.valueOf(weight);
            if(weightMap.containsKey(weight_long))
                weightMap.put(weight_long, weightMap.get(weight_long)+1);
            else
                weightMap.put(weight_long,1L);
        }</code></pre><p><img src="https://velog.velcdn.com/images/kim_table_next/post/9560ca29-7cb0-4798-9996-fdada084f5d9/image.png" alt=""></p>
<ul>
<li><strong>아마도 테스트 12~15에서 오버플로우가 발생했나보다</strong></li>
</ul>
<h2 id="소수점-문제double로-해결">소수점 문제(Double로 해결)</h2>
<blockquote>
<ul>
<li><strong>하지만 여전히 테스트 4~11은 통과하지 못하고있다.</strong></li>
</ul>
</blockquote>
<ul>
<li>로직에 어떤 문제가 있나 고민<strong>(1시간정도 고민했다)</strong>해본 결과 나눗셈을 하는 과정에서 소수점을 고려하지 않는 문제점이 있었다.
<img src="https://velog.velcdn.com/images/kim_table_next/post/75a0e15d-8b7e-457d-9d79-94d551fff970/image.png" alt=""></li>
<li>무게를 저장하는 <code>Map</code>을 <code>&lt;Long, Long&gt;</code>에서 <code>&lt;Double, Long&gt;</code>으로 변경하여 해결하였다.<pre><code>      Map&lt;Double, Long&gt; weightMap = new HashMap&lt;Double, Long&gt;();
      for(int weight : weights){
          Double weight_long = Double.valueOf(weight);
          if(weightMap.containsKey(weight_long))
              weightMap.put(weight_long, weightMap.get(weight_long)+1);
          else
              weightMap.put(weight_long,1L);
      }</code></pre><img src="blob:https://velog.io/889bd3da-45e2-40fe-ac04-5f72c85e2ca2" alt="업로드중.."></li>
</ul>
<h1 id="오늘의-회고">오늘의 회고</h1>
<p>이번 문제는 학창시절에 경우의 수 문제 푸는거를 떠올려서 경우의 수 문제를 풀듯이 접근하였다.
그런데 <strong>자료구조형을 선택하는 과정에서 소수점과 오버플로우를 고려하지 못해서 시간이 좀 걸렸다.</strong> 돌이켜 생각해보면 오버플로우와 소수점문제가 <strong>발생한다는 사실을 생각하지 못해서</strong> 발생한 문제였다.</p>
<p>알고리즘 문제를 많이 풀면서 경험을 많이 쌓아야 겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[프로그래머스] - 디펜스 게임 (PriorityQueue)]]></title>
            <link>https://velog.io/@kim_table_next/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EB%94%94%ED%8E%9C%EC%8A%A4-%EA%B2%8C%EC%9E%84</link>
            <guid>https://velog.io/@kim_table_next/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EB%94%94%ED%8E%9C%EC%8A%A4-%EA%B2%8C%EC%9E%84</guid>
            <pubDate>Wed, 13 Mar 2024 02:22:52 GMT</pubDate>
            <description><![CDATA[<h1 id="디펜스-게임">디펜스 게임</h1>
<blockquote>
<p>문제 설명
준호는 요즘 디펜스 게임에 푹 빠져 있습니다. 디펜스 게임은 준호가 보유한 병사 <code>n</code>명으로 연속되는 적의 공격을 순서대로 막는 게임입니다. 디펜스 게임은 다음과 같은 규칙으로 진행됩니다.</p>
</blockquote>
<p>준호는 처음에 병사 n명을 가지고 있습니다.
매 라운드마다 <code>enemy[i]</code>마리의 적이 등장합니다.
남은 병사 중 <code>enemy[i]</code>명 만큼 소모하여 <code>enemy[i]</code>마리의 적을 막을 수 있습니다.
예를 들어 남은 병사가 7명이고, 적의 수가 2마리인 경우, 현재 라운드를 막으면 7 - 2 = 5명의 병사가 남습니다.
남은 병사의 수보다 현재 라운드의 적의 수가 더 많으면 게임이 종료됩니다.
게임에는 무적권이라는 스킬이 있으며, 무적권을 사용하면 병사의 소모없이 한 라운드의 공격을 막을 수 있습니다.
무적권은 최대 <code>k</code>번 사용할 수 있습니다.
준호는 무적권을 적절한 시기에 사용하여 최대한 많은 라운드를 진행하고 싶습니다.</p>
<blockquote>
</blockquote>
<p>준호가 처음 가지고 있는 병사의 수 <code>n</code>, 사용 가능한 무적권의 횟수 <code>k</code>, 매 라운드마다 공격해오는 적의 수가 순서대로 담긴 정수 배열 <code>enemy</code>가 매개변수로 주어집니다. 준호가 몇 라운드까지 막을 수 있는지 return 하도록 solution 함수를 완성해주세요.</p>
<h1 id="시도했던-로직">시도했던 로직</h1>
<blockquote>
</blockquote>
<pre><code class="language-java">import java.util.*;
&gt;
class Solution {
    public int solution(int n, int k, int[] enemy) { 
        int answer = 0;
        int[] skip = new int[k];
        if(k&gt;enemy.length)
            return enemy.length;
        for(int i=0; i&lt;k; i++)
            skip[i] = enemy[i];
        answer = k;
        if(k&gt;1)
        Arrays.sort(skip);
        for(int i=k; i&lt;enemy.length;i++){
            if(skip[0]&lt;enemy[i]){
                n = n-skip[0];
                if(n&lt;0)
                    break;
                answer++;
                skip[0] = enemy[i];
                if(k&gt;1&amp;&amp;enemy[i]&gt;skip[1])
                    Arrays.sort(skip);
            }
            else if(n &gt;= enemy[i]){
                n = n - enemy[i];
                answer++;
            }
            else if(n&lt;enemy[i])
                break;
        }
        return answer;
    }
}</code></pre>
<ul>
<li>배열에 무적권을 사용한 라운드의 병사 수를 기록하고, 배열의 최소값을 활용하여 최적의 경우의 수를 찾도록 구현하였는데, 최소값을 찾는 과정에서 배열을 정렬하게되는데, 이 때문에 시간초과가 발생하였다. <strong>그래서 배열 정렬을 최소화 하도록 코드를 계속 수정해도 시간초과는 막을 수 없었다.</strong>
<img src="https://velog.velcdn.com/images/kim_table_next/post/9fd29a51-9384-4047-bb46-9019c45c9db7/image.png" alt=""></li>
</ul>
<h2 id="해결한-로직priorityqueue으로-해결">해결한 로직(PriorityQueue으로 해결)</h2>
<blockquote>
<ul>
<li>배열로 구현하는 것 보다 시간이 빠른 방법을 찾아보고,
Priority Queue(우선 순위 큐)를 사용하는 방법을 알게 되었다.</li>
</ul>
</blockquote>
<pre><code class="language-java">import java.util.*;
&gt;
class Solution {
    public int solution(int n, int k, int[] enemy) {
        int answer = enemy.length;
        Queue&lt;Integer&gt; pq = new PriorityQueue&lt;&gt;(Collections.reverseOrder());
&gt;
        int my = n;
        int card = k;
        for (int i = 0; i &lt; enemy.length; i++) {
            my -= enemy[i];
            pq.add(enemy[i]);
&gt;
            if (my &lt; 0) {
                if (card &gt; 0 &amp;&amp; !pq.isEmpty()) {
                    my += pq.poll();
                    card--;
                } else {
                    answer = i;
                    break;
                }
            }
        }
&gt;
        return answer;
    }
}</code></pre>
<ul>
<li><code>PriorityQueue</code>란 <code>우선순위 큐</code>로써 <strong>일반적인 큐의 구조 FIFO(First In First Out)를 가지면서</strong>, 데이터가 <strong>들어온 순서대로 데이터가 나가는 것이 아닌 우선순위를 먼저 결정</strong>하고 <strong>그 우선순위가 높은 데이터가 먼저 나가는 자료구조</strong>이다.</li>
<li>데이터가 들어가면서 정렬이 동시에 되는 형태이기 때문에 기존의 배열을 정렬하는 로직보다 훨씬 시간이 빠르다.
<img src="https://velog.velcdn.com/images/kim_table_next/post/d4c628a6-0525-4366-9d0c-68c3c4de8d07/image.png" alt=""></li>
</ul>
<h1 id="오늘의-회고">오늘의 회고</h1>
<p>물론, 알고리즘 문제를 많이 풀진 않았지만, 여태까지는 시간복잡도에 걸려본 경험이 없었는데,
이 문제를 풀면서 느꼈다.
<strong>알고리즘 문제를 많이 풀면서 경험을 쌓고, 다양한 자료구조형들에 익숙해 져야겠다.</strong></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring - ORM의 성장 과정]]></title>
            <link>https://velog.io/@kim_table_next/Spring-ORM-%EC%9D%98-%EC%84%B1%EC%9E%A5-%EA%B3%BC%EC%A0%95</link>
            <guid>https://velog.io/@kim_table_next/Spring-ORM-%EC%9D%98-%EC%84%B1%EC%9E%A5-%EA%B3%BC%EC%A0%95</guid>
            <pubDate>Fri, 08 Mar 2024 11:27:00 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/kim_table_next/post/71ab9aef-c2b2-4913-ab1b-c1023f83f265/image.png" alt=""></p>
<h1 id="til">TIL</h1>
<h2 id="릴레이션관계형-데이터베이스를-객체로-매핑-하려는-이유">릴레이션(관계형 데이터베이스)를 객체로 매핑 하려는 이유</h2>
<blockquote>
<ul>
<li>객체 지향 프로그래밍의 장점을 활용할 수 있음</li>
</ul>
</blockquote>
<ul>
<li>이를 통해, 비즈니스 로직 구현 및 테스트 구현이 편리함</li>
<li>각종 디자인 패턴 사용하여 성능 개선 가능</li>
<li>코드 재사용<br>
그러나 생각처럼 쉽지않다.. 여러가지 문제가 발생한다
왜냐하면 **객체와 RDB의 특성이 너무 다르다.**

</li>
</ul>
<h2 id="orm-이-해결해야하는-문제점-과-해결책">ORM 이 해결해야하는 문제점 과 해결책</h2>
<h3 id="1-상속의-문제">1. 상속의 문제</h3>
<blockquote>
<ul>
<li>객체 : 객체간에 멤버변수나 상속관계를 맺을 수 있다.</li>
</ul>
</blockquote>
<ul>
<li>RDB : 테이블들은 상속관계가 없고 모두 독립적으로 존재한다.<br></li>
<li><em>해결방법 : 매핑정보에 상속정보를 넣어준다. (<code>@OneToMany</code>, <code>@ManyToOne</code>)*</em></li>
</ul>
<h3 id="2-관계-문제">2. 관계 문제</h3>
<blockquote>
<ul>
<li>객체 : 참조를 통해 관계를 가지며 방향을 가진다. (다대다 관계도 있음)</li>
</ul>
</blockquote>
<ul>
<li>RDB : 외래키(FK)를 설정하여 Join 으로 조회시에만 참조가 가능하다. (즉, 다대다는 매핑 테이블 필요)<br></li>
<li><em>해결방법 : 매핑정보에 방향정보를 넣어준다. (<code>@JoinColumn</code>, <code>@MappedBy</code>)*</em></li>
</ul>
<h3 id="3-탐색-문제">3. 탐색 문제</h3>
<blockquote>
<ul>
<li>객체 : 참조를 통해 다른 객체로 순차적 탐색이 가능하며 콜렉션도 순회한다.</li>
</ul>
</blockquote>
<ul>
<li>RDB : 탐색시 참조하는 만큼 추가 쿼리나, Join 이 발생하여 비효율적이다.<br></li>
<li><em>해결방법 : 매핑/조회 정보로 참조탐색 시점을 관리한다.(<code>@FetchType</code>, <code>fetchJoin()</code>)*</em></li>
</ul>
<h3 id="4-밀도-문제">4. 밀도 문제</h3>
<blockquote>
<ul>
<li>객체 : 멤버 객체크기가 매우 클 수 있다.</li>
</ul>
</blockquote>
<ul>
<li>RDB : 기본 데이터 타입만 존재한다.<br></li>
<li><em>해결방법 : 크기가 큰 멤버 객체는 테이블을 분리하여 상속으로 처리한다. (<code>@embedded</code>)*</em></li>
</ul>
<h3 id="5-식별성-문제">5. 식별성 문제</h3>
<blockquote>
<ul>
<li>객체 : 객체의 hashCode 또는 정의한 equals() 메소드를 통해 식별</li>
</ul>
</blockquote>
<ul>
<li>RDB : PK 로만 식별<br></li>
<li><em>해결방법 : PK 를 객체 Id로 설정하고 EntityManager는 해당 값으로 객체를 식별하여 관리 한다.(<code>@Id</code>,<code>@GeneratedValue</code> )*</em></li>
</ul>
<h2 id="대신-orm-이-얻은-최적화-방법">대신, ORM 이 얻은 최적화 방법</h2>
<blockquote>
<p><img src="https://velog.velcdn.com/images/kim_table_next/post/f93f7bbb-1dcf-4c3d-98a1-884dbf25a11f/image.png" alt=""></p>
</blockquote>
<ul>
<li>1차 캐시, 2차 캐시를 사용하여 최적화를 했다.</li>
<li>캐싱 기능은 객체지향 프로그래밍이 가진 가장 큰 장점이다.</li>
</ul>
<h3 id="1차-캐시">1차 캐시</h3>
<blockquote>
<ul>
<li>영속성 컨텍스트 내부에는 엔티티를 보관하는 저장소가 있는데 이를 1차 캐시라고 한다.</li>
</ul>
</blockquote>
<ul>
<li>일반적으로 트랜잭션을 시작하고 종료할 때까지만 1차 캐시가 유효하다.</li>
<li>1차 캐시는 한 트랜잭션 계속해서 원본 객체를 넘겨준다.</li>
</ul>
<h3 id="2차-캐시">2차 캐시</h3>
<blockquote>
<ul>
<li>애플리케이션 범위의 캐시로, 공유 캐시라고도 하며, 애플리케이션을 종료할 때 까지 캐시가 유지된다.</li>
</ul>
</blockquote>
<ul>
<li>2차 캐시는 캐시 한 객체 원본을 넘겨주지 않고 복사본을 만들어서 넘겨준다.</li>
<li>복사본을 주는 이유는 여러 트랜잭션에서 동일한 원본객체를 수정하는일이 없도록 하기 위해서이다.</li>
<li>Entity에 <code>@Cacheable</code> 적용 후, yml파일에 설정을 추가해주면 사용할 수 있다.<pre><code class="language-yml"># application.yml
&gt;
spring.jpa.properties.hibernate.cache.use_second_level_cache: true
# 2차 캐시 활성화합니다.
&gt;
spring.jpa.properties.hibernate.cache.region.factory_class: XXX
# 2차 캐시를 처리할 클래스를 지정합니다.
&gt;
spring.jpa.properties.hibernate.generate_statistics: true
# 하이버네이트가 여러 통계정보를 출력하게 해주는데 캐시 적용 여부를 확인할 수 있습니다.</code></pre>
</li>
<li><code>sharedCache.mode</code> 설정을 통해서도 가능하다<pre><code class="language-yml"># appplication.yml
&gt;
spring.jpa.properties.javax.persistence.sharedCache.mode: ENABLE_SELECTIVE</code></pre>
<blockquote>
<p><strong>cache mode 종류</strong></p>
<table>
<thead>
<tr>
<th>ALL</th>
<th>모든 엔티티를 캐시합니다.</th>
</tr>
</thead>
<tbody><tr>
<td>NONE</td>
<td>캐시를 사용하지 않습니다.</td>
</tr>
<tr>
<td>ENABLE_SELECTIVE</td>
<td>Cacheable(true)로 설정된 엔티티만 캐시를 적용합니다.</td>
</tr>
<tr>
<td>DISABLE_SELECTIVE</td>
<td>모든 엔티티를 캐시하는데 Cacheable(false)만 캐시하지 습니다.</td>
</tr>
<tr>
<td>UNSPECIFIED</td>
<td>JPA 구현체가 정의한 설정을 따릅니다</td>
</tr>
</tbody></table>
</blockquote>
</li>
</ul>
<h1 id="오늘의-회고">오늘의 회고</h1>
<p>오늘은 ORM이 등장하면서 어떠한 문제가 있었고, 이를 어떻게 해결했는지를 알아보았다.
공부하다 보니 평소에 JPA에서 쓰고있던 어노테이션들이 이러한 이유로 사용된다는걸 알아서 신기했다.
공부하면 할 수록 느낀점은 관계형 데이터베이스를 객체처럼 쓸 수 있게 해주는 JPA가 정말 편리한거 같다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring - ORM의 탄생 배경 (JDBC, Query Mapper)]]></title>
            <link>https://velog.io/@kim_table_next/Spring-ORM-%EC%9D%98-%ED%83%84%EC%83%9D-%EB%B0%B0%EA%B2%BD-JDBC-Query-Mapper</link>
            <guid>https://velog.io/@kim_table_next/Spring-ORM-%EC%9D%98-%ED%83%84%EC%83%9D-%EB%B0%B0%EA%B2%BD-JDBC-Query-Mapper</guid>
            <pubDate>Thu, 07 Mar 2024 10:18:48 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/kim_table_next/post/20d232f4-3ef8-4d66-94a9-9918b01203ea/image.png" alt=""></p>
<h1 id="til">TIL</h1>
<h2 id="jdbcjava-database-connectivity">JDBC(Java Database Connectivity)</h2>
<blockquote>
<ul>
<li>문장 그대로 Java 앱과 DB 를 연결시켜주기 위해 만들어진 기술이다.</li>
</ul>
</blockquote>
<ul>
<li>그렇기 때문에 JPA 도 이 기술을 사용하여 구현되어 있다. </li>
<li>JDBC Driver 는 여러타입의 DB 와 연결할 수 있는 기능을 제공한다.
<img src="https://velog.velcdn.com/images/kim_table_next/post/4f1173d7-7f93-46ae-95a7-84c393758cc7/image.png" alt=""></li>
<li><code>JDBC Driver Manager</code> 는 런타임 시점에
  -<code>Connection(연결)</code> 을 생성하여 쿼리를 요청할 수 있는 상태를 만들어주고
  -<code>Statement(상태)</code> 를 생성하여 쿼리를 요청하게 해주고
  -<code>ResultSet(결과셋)</code> 을 생성해 쿼리 결과를 받아올 수 있게 해준다.<ul>
<li><strong>꼭 사용후에는 각각 <code>close()</code> 를 호출해서 자원 해제를 시켜줘야 한다 (자원 관리)</strong>
<img src="https://velog.velcdn.com/images/kim_table_next/post/946518c5-b608-47ea-9a0d-55eb4e5d794f/image.png" alt=""><br>
> - JDBC를 통해서 INSERT와 SELECT를 해보았다 **(JPA를 사용했을때 보다 상당히 귀찮다)**
```java
public class JDBCTest {
>
@Test
@DisplayName("JDBC DB 연결 실습")
void jdbcTest() throws SQLException {
// given
>
String url = "jdbc:postgresql://localhost:5432/messenger";
String username = "tablenext";
String password = "pass";
>
// when
try (Connection connection = DriverManager.getConnection(url, username, password)) {
try {
  String creatSql = "CREATE TABLE ACCOUNT (id SERIAL PRIMARY KEY, username varchar(255), password varchar(255))";
  try (PreparedStatement statement = connection.prepareStatement(creatSql)) {
    statement.execute();
  }
} catch (SQLException e) {
  if (e.getMessage().equals("ERROR: relation \"account\" already exists")) {
    System.out.println("ACCOUNT 테이블이 이미 존재합니다.");
  } else {
    throw new RuntimeException();
  }
}
}
>
// then
  // DB 확인
}
>
@Test
@DisplayName("JDBC 삽입/조회 실습")
void jdbcInsertSelectTest() throws SQLException {
// given
String url = "jdbc:postgresql://localhost:5432/messenger";
String username = "tablenext";
String password = "pass";
>
// when
try (Connection connection = DriverManager.getConnection(url, username, password)) {
System.out.println("Connection created: " + connection);
>
String insertSql = "INSERT INTO ACCOUNT (id, username, password) VALUES ((SELECT coalesce(MAX(ID), 0) + 1 FROM ACCOUNT A), 'user1', 'pass1')";
try (PreparedStatement statement = connection.prepareStatement(insertSql)) {
  statement.execute();
}
>
// then
String selectSql = "SELECT * FROM ACCOUNT";
try (PreparedStatement statement = connection.prepareStatement(selectSql)) {
  var rs = statement.executeQuery();
  while (rs.next()) {
    System.out.printf("%d, %s, %s", rs.getInt("id"), rs.getString("username"),
        rs.getString("password"));
  }
}
}
}
>
@Test
@DisplayName("JDBC DAO 삽입/조회 실습")
void jdbcDAOInsertSelectTest() throws SQLException {
// given
AccountDAO accountDAO = new AccountDAO();
>
// when
var id = accountDAO.insertAccount(new AccountVO("new user", "new password"));
>
// then
var account = accountDAO.selectAccount(id);
assert account.getUsername().equals("new user");
}
}
```

</li>
</ul>
</li>
</ul>
<h2 id="jdbc의-문제점-querymapper의-등장">JDBC의 문제점, QueryMapper의 등장</h2>
<blockquote>
<ul>
<li>JDBC 로 직접 SQL을 작성했을때의 문제<ul>
<li>SQL 쿼리 요청시 중복 코드 발생</li>
<li>DB별 예외에 대한 구분 없이 Checked Exception (SQL Exception) 처리</li>
<li>Connection, Statement 등.. 자원 관리를 따로 해줘야함<ul>
<li>안해주면 메모리 꽉차서 서버가 죽음</li>
</ul>
</li>
</ul>
</li>
</ul>
</blockquote>
<ul>
<li>이 문제 해결을 위해 처음으로 <code>Persistence Framework</code> 등장<ul>
<li><code>Persistence Framework</code> 는 2가지가 있다.<ul>
<li><strong>SQL Mapper</strong> : <code>JDBC Template</code>, <code>MyBatis</code> 👈 요게 먼저나옴</li>
<li><strong>ORM</strong> : <code>JPA</code>, <code>Hibernate</code></li>
</ul>
</li>
</ul>
</li>
<li><strong>SQL Mapper (QueryMapper)</strong><ul>
<li>SQL ↔ Object</li>
<li>SQL 문과 객체(Object)의 필드를 매핑하여 데이터를 객채화</li>
</ul>
</li>
</ul>
<h2 id="jdbc-template">JDBC Template</h2>
<blockquote>
<ul>
<li>SQL Mapper 첫번째 주자로 <code>JDBCTemplate</code> 탄생<ul>
<li>쿼리 수행 결과와 객채 필드 매핑</li>
<li>RowMapper 로 응답필드 매핑코드 재사용</li>
<li>Connection, Statement, ResultSet 반복적 처리 대신 해줌</li>
<li>But, 결과값을 객체 인스턴스에 매핑하는데 여전히 많은 코드가 필요함
<img src="https://velog.velcdn.com/images/kim_table_next/post/ab3e2b27-0b76-49a0-b023-81e2631eb175/image.png" alt=""></li>
</ul>
</li>
</ul>
</blockquote>
<pre><code class="language-java">@JdbcTest // Jdbc Slice Test
@AutoConfigureTestDatabase(replace = Replace.NONE) // 테스트용 DB 쓰지 않도록
@Rollback(value = false) // Transactional 에 있는 테스트 변경은 기본적으론 롤백 하도록 되어있다.
public class JDBCTemplateTest {
&gt;
  @Autowired
  private JdbcTemplate jdbcTemplate;
&gt;
  @Test
  @DisplayName(&quot;SQL Mapper - JDBC Template 실습&quot;)
  void sqlMapper_JDBCTemplateTest() {
    // given
    var accountTemplateDAO = new AccountTemplateDAO(jdbcTemplate);
&gt;
    // when
    var id = accountTemplateDAO.insertAccount(new AccountVO(&quot;new user2&quot;, &quot;new password2&quot;));
&gt;
    // then
    var account = accountTemplateDAO.selectAccount(id);
    assert account.getUsername().equals(&quot;new user2&quot;);
  }
}</code></pre>
<blockquote>
<ul>
<li>AccountTemplateDAO (Data Access Object)</li>
</ul>
</blockquote>
<pre><code class="language-java">package me.whitebear.jpastudy.jdbc.template;
&gt;
import me.whitebear.jpastudy.jdbc.vo.AccountVO;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.jdbc.support.KeyHolder;
import org.springframework.stereotype.Repository;
&gt;
@Repository // Java 패키지에 있었다면 JdbcTemplate 생성자 주입받음
public class AccountTemplateDAO {
&gt;
  private final JdbcTemplate jdbcTemplate;
&gt;
  public AccountTemplateDAO(JdbcTemplate jdbcTemplate) {
    this.jdbcTemplate = jdbcTemplate;
  }
&gt;
  //SQL 관련 명령어
  private final String ACCOUNT_INSERT = &quot;INSERT INTO account(ID, USERNAME, PASSWORD) &quot;
      + &quot;VALUES((SELECT coalesce(MAX(ID), 0) + 1 FROM ACCOUNT A), ?, ?)&quot;;
  // coalesce 은 Postgresql 용 IFNULL
  private final String ACCOUNT_GET = &quot;SELECT * FROM account WHERE ID = ?&quot;;
&gt;
  //CRUD 기능의 메소드 구현
  //계정 등록
  public Integer insertAccount(AccountVO vo) {
    // 기본 코드 (응답값은 생성된 갯수)
    // return jdbcTemplate.update(ACCOUNT_INSERT, vo.getUsername(), vo.getPassword());
&gt;
    // id 값을 받아오기 위한 코드
    KeyHolder keyHolder = new GeneratedKeyHolder();
&gt;
    jdbcTemplate.update(connection -&gt; {
      var ps = connection
          .prepareStatement(ACCOUNT_INSERT, new String[]{&quot;id&quot;});
      ps.setString(1, vo.getUsername());
      ps.setString(2, vo.getPassword());
      return ps;
    }, keyHolder);
&gt;
    return (Integer) keyHolder.getKey();
  }
&gt;
  //계정 조회
  public AccountVO selectAccount(Integer id) {
    return jdbcTemplate.queryForObject(ACCOUNT_GET, new AccountRowMapper(), id);
  }
&gt;
}</code></pre>
<h2 id="mybatis">MyBatis</h2>
<blockquote>
<ul>
<li>SQL Mapper 두번째 주자로 <code>MyBatis</code> 탄생<ul>
<li>반복적인 JDBC 프로그래밍을 단순화</li>
<li>SQL 쿼리들을 XML 파일에 작성하여 코드와 SQL 을 분리!</li>
<li>But, 결국 SQL을 직접 작성하는것은 피곤하다…(DB 기능에 종속적)</li>
<li>But, 테이블마다 비슷한 CRUD 반복, DB타입 및 테이블에 종속적이다.
<img src="https://velog.velcdn.com/images/kim_table_next/post/b46ec101-7d0f-41da-ad46-0c1a906270cf/image.png" alt=""></li>
</ul>
</li>
</ul>
</blockquote>
<pre><code class="language-java">@SpringBootTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Import(DBConfiguration.class)
public class MyBatisTest {
&gt;
  // Mapper 클래스를 받으려면 mapper.xml 빌드 해야하고, 그러려면 main 으로 옮겨서 해야함...
  @Autowired
  AccountMapper accountMapper;
&gt;
  @Autowired
  AccountMapperV2 accountMapperV2;
&gt;
  @Test
  @DisplayName(&quot;SQL Mapper - MyBatis 실습&quot;)
  void sqlMapper_MyBatisTest() {
    // given
&gt;
    // when
    accountMapper.insertAccount(new AccountMyBatisVO(&quot;new user3&quot;, &quot;new password3&quot;));
    var account = accountMapper.selectAccount(1);
&gt;
    // then
    assert !account.getUsername().isEmpty();
  }
&gt;
  @Test
  @DisplayName(&quot;SQL Mapper - MyBatis V2 실습&quot;)
  void sqlMapper_MyBatisV2Test() {
    // given
&gt;
    // when
    accountMapperV2.insertAccount(new AccountMyBatisVO(&quot;new user4&quot;, &quot;new password4&quot;));
    var account = accountMapperV2.selectAccount(1);
&gt;
    // then
    assert !account.getUsername().isEmpty();
  }
}</code></pre>
<blockquote>
<ul>
<li>AccountMapper.xml</li>
</ul>
</blockquote>
<pre><code class="language-xml">&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;
&lt;!DOCTYPE mapper PUBLIC &quot;-//mybatis.org//DTD Mapper 3.0//EN&quot;
  &quot;http://mybatis.org/dtd/mybatis-3-mapper.dtd&quot;&gt;
&gt;
&lt;!--
    [템플릿 설명]
    - 해당 파일은 SQL 문을 작성하는 곳입니다.
--&gt;
&lt;mapper namespace=&quot;me.whitebear.jpastudy.mybatis.mapper.AccountMapper&quot;&gt;
&gt;
  &lt;select id=&quot;selectAccount&quot; resultType=&quot;me.whitebear.jpastudy.mybatis.vo.AccountMyBatisVO&quot;&gt;
    SELECT id,
           username,
           password
    FROM account
    WHERE id = #{id}
  &lt;/select&gt;
&gt;
  &lt;insert id=&quot;insertAccount&quot; parameterType=&quot;me.whitebear.jpastudy.mybatis.vo.AccountMyBatisVO&quot;&gt;
    INSERT INTO account(username, password)
    VALUES (#{username}, #{password});
  &lt;/insert&gt;
&gt;
&lt;/mapper&gt;</code></pre>
<h2 id="querymapper-문제점-orm의-탄생">QueryMapper 문제점, ORM의 탄생</h2>
<blockquote>
<ul>
<li><code>QueryMapper</code> 의 DB의존성 및 중복 쿼리 문제점</li>
</ul>
</blockquote>
<ul>
<li><code>ORM</code> 은 DB의 주도권을 뺏어왔다고 표현해도 과언이 아니다.</li>
<li><code>ORM</code> 은 <code>DAO</code> 또는 <code>Mapper</code> 를 통해서 조작하는것이 아니라 테이블을 아예 하나의 객체(Object)와 대응시켜 버린다.</li>
<li>말이 쉽지…. <code>객체지향(Object)</code> 을 <code>관계형 데이터베이스(Relation)</code> 에 <code>매핑(Mapping)</code> 한다는건 정말 많은 난관이 있다.
<img src="https://velog.velcdn.com/images/kim_table_next/post/eb74ed1b-3f86-4d33-9d4d-dce21beebc9c/image.png" alt=""></li>
</ul>
<h1 id="오늘의-회고">오늘의 회고</h1>
<p>JDBC, JDBC Template, MyBatis를 통해 평소에 JPA를 사용하여 했던 DB실습을 해보았는데,
하면 할 수 록 JPA가 굉장히 편하다는걸 느꼈다.
하지만 JPA를 사용하지 않는 기업들이 많기에..
쓰는법을 익히는 김에 등장 배경도 알아보았다.
<strong><del>(꼭 JPA를 사용하는 기업에 취업해야지)</del></strong></p>
]]></description>
        </item>
    </channel>
</rss>