<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>kwh3217.log</title>
        <link>https://velog.io/</link>
        <description>백엔드개발자를 향해서</description>
        <lastBuildDate>Mon, 16 Oct 2023 07:51:09 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <copyright>Copyright (C) 2019. kwh3217.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/be_chobo" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[인증/보안의 기초2 - Cookie와 Session]]></title>
            <link>https://velog.io/@be_chobo/%EC%9D%B8%EC%A6%9D%EB%B3%B4%EC%95%88%EC%9D%98-%EA%B8%B0%EC%B4%882-Cookie%EC%99%80-Session</link>
            <guid>https://velog.io/@be_chobo/%EC%9D%B8%EC%A6%9D%EB%B3%B4%EC%95%88%EC%9D%98-%EA%B8%B0%EC%B4%882-Cookie%EC%99%80-Session</guid>
            <pubDate>Mon, 16 Oct 2023 07:51:09 GMT</pubDate>
            <description><![CDATA[<h3 id="⚫cookie">⚫Cookie</h3>
<p>쿠키는 서버에서 클라이언트에 데이터를 저장하는 방법 중 하나입니다. 그러므로 서버가 원한다면 서버는 클라이언트에서 쿠키를 이용하여 데이터를 가져올 수 있습니다.</p>
<p>쿠키를 이용하는 것은 단순히 서버에서 클라이언트에 쿠키를 전송하는 것만 의미하지 않고 클라이언트에서 서버로 쿠키를 전송하는 것도 포함됩니다.</p>
<p>서버는 쿠키를 이용하여 데이터를 저장하고 원할 때 이 데이터를 다시 불러와 사용할 수 있습니다.
하지만 데이터를 저장한 이후 아무 때나 데이터를 가져올 수 없습니다. 데이터를 저장한 이후 특정 조건들이 만족하는 경우에만 다시 가져올 수 있습니다.</p>
<p>이런 조건들은 쿠키 옵션으로 표현할 수 있습니다.</p>
<ol>
<li><p>Domain
쿠키 옵션에서 도메인은 포트 및 서브 도메인 정보, 세부 경로를 포함하지 않습니다.
쿠키 옵션에서 도메인 정보가 존재한다면 클라이언트에서는 쿠키의 도메인 옵션과 서버의 도메인이 일치해야만 쿠키를 전송할 수 있습니다.</p>
</li>
<li><p>Path
URL이 <a href="http://www.localhost.com:3000/users/login%EC%9D%B8">http://www.localhost.com:3000/users/login인</a> 경우라면 여기에서 Path, 세부 경로는 /users/login이 됩니다.
명시하지 않으면 기본으로 / 으로 설정되어 있습니다.</p>
<p>Path 옵션의 특징은 설정된 path를 전부 만족하는 경우 요청하는 Path가 추가로 더 존재하더라도 쿠키를 서버에 전송할 수 있습니다.
즉 Path가 /users로 설정되어 있고, 요청하는 세부 경로가 /users/helloworld인 경우라면 쿠키 전송이 가능합니다.</p>
</li>
<li><p>MaxAge or Expires
쿠키가 유효한 기간을 정하는 옵션입니다.
MaxAge는 앞으로 몇 초 동안 쿠키가 유효한지 설정하는 옵션입니다.
Expires은 언제까지 유효한지 Date를 지정합니다.</p>
<blockquote>
<p>세션 쿠키: MaxAge 또는 Expires 옵션이 없는 쿠키로, 브라우저가 실행 중일 때 사용할 수 있는 임시 쿠키입니다. 브라우저를 종료하면 해당 쿠키는 삭제됩니다.</p>
</blockquote>
<p>영속성 쿠키: 브라우저의 종료 여부와 상관없이 MaxAge 또는 Expires에 지정된 유효시간만큼 사용가능한 쿠키입니다.</p>
</li>
<li><p>Secure
만약 해당 옵션이 true로 설정된 경우, &#39;HTTPS&#39; 프로토콜을 이용하여 통신하는 경우에만 쿠키를 전송할 수 있습니다.</p>
</li>
<li><p>HttpOnly
자바스크립트에서 브라우저의 쿠키에 접근 여부를 결정합니다.
만약 해당 옵션이 true로 설정된 경우, 자바스크립트에서는 쿠키에 접근이 불가합니다.</p>
<p>명시되지 않는 경우 기본으로 false로 지정되어 있습니다.
만약 이 옵션이 false인 경우 자바스크립트에서 쿠키에 접근이 가능하므로 &#39;XSS&#39; 공격에 취약합니다.</p>
</li>
<li><p>SameSite
Lax: Cross-Site 요청이라면 GET 메서드에 대해서만 쿠키를 전송할 수 있습니다.
Strict: 단어 그대로 가장 엄격한 옵션으로, Cross-Site가 아닌 Same-Site인 경우에만 쿠키를 전송할 수 있습니다.
None: Cross-Site에 대해 가장 관대한 옵션으로 항상 쿠키를 보내줄 수 있습니다. 다만 쿠키 옵션 중 Secure 옵션이 필요합니다.</p>
</li>
</ol>
<p>&amp;nbsp</p>
<h4 id="이러한-옵션들을-토대로-쿠키를-이용해-상태-유지를-합니다">이러한 옵션들을 토대로 쿠키를 이용해 상태 유지를 합니다.</h4>
<p>서버는 클라이언트에 인증정보를 담은 쿠키를 전송하고, 클라이언트는 전달받은 쿠키를 요청과 같이 전송하여 Stateless 한 인터넷 연결을 Stateful 하게 유지할 수 있습니다.</p>
<p>하지만 기본적으로는 쿠키는 오랜 시간 동안 유지될 수 있고, 자바스크립트를 이용해서 쿠키에 접근할 수 있기 때문에 쿠키에 민감한 정보를 담는 것은 위험합니다.</p>
<p>이런 인증정보를 탈취하여 서버에 요청을 보낸다면 서버는 누가 요청을 보낸 건지 상관하지 않고 인증된 유저의 요청으로 취급하기 때문에, 개인 유저 정보 같은 민감한 정보에 접근이 가능합니다.</p>
<p>&amp;nbsp
&amp;nbsp</p>
<h3 id="⚫session">⚫Session</h3>
<p><img src="https://velog.velcdn.com/images/be_chobo/post/b82d2508-d277-4245-a43a-23dd3e3999f8/image.png" alt=""></p>
<h4 id="✔로그인">✔로그인</h4>
<p>사용자가 웹사이트에서 아이디 및 비밀번호를 이용해서 로그인을 시도하면 서버는 인증(Authentication)에 성공했다고 판단할 것입니다.</p>
<p>그렇다면, 다음번에 인증을 필요로 하는 작업(장바구니에 물품 추가)을 요청할 경우, 서버가 &quot;해당 유저는 인증에 성공했음&quot;을 알고 있다면, 유저가 매번 로그인할 필요가 없을 것입니다.</p>
<p>인증에 따라 리소스의 접근 권한(Authorization)이 달라집니다.</p>
<p>사용자가 인증에 성공한 상태는 세션이라고 부릅니다.</p>
<p>서버는 일종의 저장소에 세션을 저장합니다. 주로 in-memory, 또는 세션 스토어(redis 등과 같은 트랜잭션이 빠른 DB)에 저장합니다.</p>
<p>세션이 만들어지면, 각 세션을 구분할 수 있는 세션 아이디도 만들어지는데, 보통 클라이언트에 세션 성공을 증명할 수단으로써 세션 아이디를 전달합니다.</p>
<p>이때 웹사이트에서 로그인을 유지하기 위한 수단으로 쿠키를 사용합니다. 쿠키에는 서버에서 발급한 세션 아이디를 저장합니다</p>
<p>쿠키를 통해 유효한 세션 아이디가 서버에 전달되고, 세션 스토어에 해당 세션이 존재한다면 서버는 해당 요청에 접근 가능하다고 판단합니다. </p>
<p>하지만 쿠키에 세션 아이디 정보가 없는 경우, 서버는 해당 요청이 인증되지 않았음을 알려줍니다.</p>
<p>&amp;nbsp</p>
<h4 id="✔로그아웃">✔로그아웃</h4>
<p>세션 아이디가 담긴 쿠키는 클라이언트에 저장되어 있으며, 서버는 세션을 저장하고 있습니다. 그리고 서버는 그저 세션 아이디로만 인증 여부를 판단합니다.</p>
<blockquote>
<p>쿠키는 세션 아이디, 즉 인증 성공에 대해 증명을 하고 있으므로, 탈취될 때 서버는 해당 요청이 인증된 사용자의 요청이라고 판단합니다.
이것이, 우리가 공공 PC에서 로그아웃해야 하는 이유입니다.</p>
</blockquote>
<p>그러므로 로그아웃은 다음 두 가지 작업을 해야 합니다.</p>
<p>서버 : 세션 정보를 삭제해야 합니다.
클라이언트 : 쿠키를 갱신해야 합니다.</p>
<p>서버가 클라이언트의 쿠키를 임의로 삭제할 수는 없습니다. 대신, set-cookie로 클라이언트에게 쿠키를 전송할 때 세션 아이디의 키값을 무효한 값으로 갱신할 수 있습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[인증/보안의 기초1 - HTTPS와 Hashing]]></title>
            <link>https://velog.io/@be_chobo/%EC%9D%B8%EC%A6%9D%EB%B3%B4%EC%95%88%EC%9D%98-%EA%B8%B0%EC%B4%881-HTTPS%EC%99%80-Hashing</link>
            <guid>https://velog.io/@be_chobo/%EC%9D%B8%EC%A6%9D%EB%B3%B4%EC%95%88%EC%9D%98-%EA%B8%B0%EC%B4%881-HTTPS%EC%99%80-Hashing</guid>
            <pubDate>Mon, 16 Oct 2023 06:59:59 GMT</pubDate>
            <description><![CDATA[<h3 id="⚫https">⚫HTTPS</h3>
<p>현재 대부분의 웹사이트의 경우 HTTPS를 사용하고 있으며, 만약 HTTP를 사용하는 웹사이트에 접속했다면 Not Secure라는 메시지를 표시해 사용자가 해당 웹사이트와의 연결하는 것에 안전성 보장이 없다는 것을 알려줍니다.</p>
<p>HTTPS는 Hyper Text Transfer Protocol Secure Socket layer의 약자입니다. HTTP over SSL(TLS), HTTP over Secure라고 부르기도 합니다.
HTTPS는 HTTP 요청을 SSL 혹은 TLS라는 알고리즘을 이용해, HTTP 통신을 하는 과정에서 데이터를 암호화하여 전송하는 방법입니다.</p>
<p>그렇다면 HTTPS의 특징은 무엇일까요?</p>
<ul>
<li>첫째로는 제 3자가 서버와 클라이언트가 주고받는 정보를 탈취할 수 없도록 하는 것입니다.</li>
</ul>
<p>데이터를 암호화하여 전송하는 HTTPS를 사용한다면 비밀번호와 같은 중요한 데이터가 유출될 가능성이 HTTP보다 현저히 적어집니다. </p>
<p>HTTPS에서는 클라이언트와 서버가 데이터를 암호화하여 주고받기 위해 비대칭키 방식과 대칭키 방식을 혼용하여 사용합니다.</p>
<blockquote>
<p>서버와 클라이언트가 통신할 때, 대칭키 방식은 양쪽이 공통의 비밀 키를 공유하여 데이터를 암호화 및 복호화하는 것, 비대칭키 방식은 각각 공개키와 비밀키(개인키)를 가지고 상대가 나의 공개키로 암호화한 데이터를 개인이 가진 비밀키로 복호화하는 것을 의미합니다.</p>
</blockquote>
<p>HTTPS에서는 대칭키를 주고받을 때는 비대칭키 방식으로 주고받도록 합니다.
또 클라이언트와 서버가 데이터를 주고받을 때는 대칭키를 사용합니다.</p>
<ul>
<li>두번째 HTTPS의 특징은 브라우저가 서버의 응답과 함께 전달된 인증서를 확인할 수 있다는 점입니다.
(인증서는 서버의 신원을 보증하여 우리가 접속한 사이트가 해커가 정교하게 따라 한 가짜 사이트가 아님을 보장해 주는 역할을 합니다)</li>
</ul>
<p>이때 이를 보증할 수 있는 제삼자를 Certificate Authority, CA라고 부릅니다.</p>
<p>CA는 인증서를 발급해 주는 엄격하게 공인된 기관들을 말합니다. 이러한 CA들은 서버의 공개키와 정보를 CA의 비밀키로 암호화하여 인증서를 발급합니다.</p>
<p>서버와 클라이언트 간의 CA를 통해 서버를 인증하는 과정과 데이터를 암호화하는 과정을 아우른 프로토콜을 TLS 또는 SSL이라고 말합니다. (*SSL과 TLS는 사실상 동일한 규약을 뜻하며 SSL이 표준화되며 바뀐 이름이 TLS입니다.)</p>
<p>&amp;nbsp
&amp;nbsp</p>
<h3 id="⚫hashing">⚫Hashing</h3>
<p>해싱은 가장 많이 쓰는 암호화 방식 중 하나입니다.
복호화가 가능한 다른 암호화 방식들과 달리, 해싱은 암호화만 가능합니다.</p>
<p>해싱은 해시 함수(Hash Function)를 사용하여 암호화를 진행하는데</p>
<ol>
<li>항상 같은 길이의 문자열을 리턴합니다.</li>
<li>서로 다른 문자열에 동일한 해시 함수를 사용하면 반드시 다른 결과값이 나옵니다.</li>
<li>동일한 문자열에 동일한 해시 함수를 사용하면 항상 같은 결과값이 나옵니다.</li>
</ol>
<p>&amp;nbsp</p>
<h4 id="❕레인보우-테이블과-솔트salt">❕레인보우 테이블과 솔트(Salt)</h4>
<p>해시 함수를 거치기 이전의 값을 알아낼 수 있도록 기록해 놓은 표인 레인보우 테이블이 존재합니다. 레인보우 테이블에 기록된 값의 경우에는 유출이 되었을 때 해싱을 했더라도 해싱 이전의 값을 알아낼 수 있으므로 보안상 위협이 될 수 있습니다.</p>
<p>이때 활용할 수 있는 것이 <strong>솔트(Salt)</strong>로 해싱 이전 값에 임의의 값을 더해 데이터가 유출되더라도 해싱 이전의 값을 알아내기 더욱 어렵게 만드는 방법입니다.</p>
<p>솔트를 사용하게 되면 해싱 값이 유출되더라도, 솔트가 함께 유출된 것이 아니라면 암호화 이전의 값을 알아내는 것은 불가능에 가깝습니다.</p>
<p>&amp;nbsp</p>
<h4 id="그렇다면-왜-복호화가-불가능한-암호화-방식을-사용하는-걸까">그렇다면 왜 복호화가 불가능한 암호화 방식을 사용하는 걸까?</h4>
<p>해싱의 목적은 데이터 그 자체를 사용하는 것이 아니라, 동일한 값의 데이터를 사용하고 있는지 여부만 확인하는 것이 목적이기 때문입니다.
(예를 들어 비밀번호를 데이터베이스에 저장할때 복호화가 불가능하도록 해싱하여 저장)</p>
<p>민감한 데이터를 다루어야 하는 상황에서 데이터 유출의 위험성은 줄이면서 데이터의 유효성을 검증하기 위해서 사용되는 단방향 암호화 방식입니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[슬라이스 테스트(Slice Test)]]></title>
            <link>https://velog.io/@be_chobo/%EC%8A%AC%EB%9D%BC%EC%9D%B4%EC%8A%A4-%ED%85%8C%EC%8A%A4%ED%8A%B8Slice-Test</link>
            <guid>https://velog.io/@be_chobo/%EC%8A%AC%EB%9D%BC%EC%9D%B4%EC%8A%A4-%ED%85%8C%EC%8A%A4%ED%8A%B8Slice-Test</guid>
            <pubDate>Thu, 12 Oct 2023 08:24:46 GMT</pubDate>
            <description><![CDATA[<h3 id="⚫-슬라이스-테스트란">⚫ 슬라이스 테스트란?</h3>
<p>하나의 애플리케이션은 계층별로 역할이 있고, 계층별로 서로 연동되기 때문에 각각의 계층 별로 잘 동작하는지 테스트를 진행한 후에 마지막으로 통합 테스트를 통해서 계층 간의 연동에 문제가 없는지 확인해야 비로소 개발자의 테스트 작업이 마무리되는 것이라고 할 수 있습니다.</p>
<p>개발자가 각 계층에 구현해 놓은 기능들이 잘 동작하는지 특정 계층만 잘라서(Slice) 테스트하는 것을 슬라이스 테스트(Slice Test)라고 합니다.</p>
<p>&amp;nbsp
&amp;nbsp</p>
<h3 id="⚫-api-계층-테스트">⚫ API 계층 테스트</h3>
<p>API 계층의 테스트 대상은 대부분 클라이언트의 요청을 받아들이는 핸들러인 Controller입니다.</p>
<ul>
<li>Controller 테스트를 위한 테스트 클래스 구조<pre><code>import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;
</code></pre></li>
</ul>
<p>@SpringBootTest       // (1)
@AutoConfigureMockMvc  // (2)
public class ControllerTestDefaultStructure {
        // (3)
    @Autowired
    private MockMvc mockMvc;</p>
<pre><code>    // (4) 
@Test
public void postMemberTest() {
    // given (5) 테스트용 request body 생성

    // when (6) MockMvc 객체로 테스트 대상 Controller 호출

    // then (7) Controller 핸들러 메서드에서 응답으로 수신한 HTTP Status 및 response body 검증 
}</code></pre><p>}</p>
<pre><code>(1)의 @SpringBootTest 애너테이션은 Spring Boot 기반의 애플리케이션을 테스트하기 위한 Application Context를 생성합니다.
Application Context에는 애플리케이션에 필요한 Bean 객체들이 등록되어 있습니다.

(2)의 @AutoConfigureMockMvc 애너테이션은 Controller 테스트를 위한 애플리케이션의 자동 구성 작업을 해줍니다.
Spring Boot의 자동 구성을 통해 애플리케이션의 설정을 손쉽게 사용하듯이 @AutoConfigureMockMvc 애너테이션을 추가함으로써 테스트에 필요한 애플리케이션의 구성이 자동으로 진행됩니다.

(3)의 MockMvc 같은 기능을 사용하기 위해서는 @AutoConfigureMockMvc 애너테이션을 반드시 추가해 주어야 합니다.
(3)에서 DI로 주입받은 MockMvc는 Tomcat 같은 서버를 실행하지 않고 Spring 기반 애플리케이션의 Controller를 테스트할 수 있는 완벽한 환경을 지원해 주는 일종의 Spring MVC 테스트 프레임워크입니다.

MockMvc 객체를 통해 우리가 작성한 Controller를 호출해서 손쉽게 Controller에 대한 테스트를 진행할 수 있습니다.

이로써 Controller를 테스트할 준비가 되었습니다.

이제 (4)와 같이 테스트하고자 하는 Controller 핸들러 메서드의 테스트 케이스를 작성하면 됩니다.

Controller를 테스트하기 위해서는 (5)의 단계에서 테스트용 request body를 직접 만들어 주어야 합니다.
Given-When-Then 패턴에서 Given에 해당됩니다.

(6)에서는 MockMvc 객체를 통해 요청 URI와 HTTP 메서드등을 지정하고, (5)에서 만든 테스트용 request body를 추가한 뒤에 request를 수행합니다.
Given-When-Then 패턴에서 When에 해당됩니다.

(7)에서는 Controller에서 전달받은 HTTP Status와 response body 데이터를 통해 검증 작업을 진행합니다.
Given-When-Then 패턴에서 Then에 해당됩니다.


&amp;nbsp
&amp;nbsp
#### MemberController 테스트

- HTTP Post request에 대한 테스트</code></pre><p>import com.Member.member.dto.MemberDto;
import com.google.gson.Gson;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.transaction.annotation.Transactional;</p>
<p>import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.startsWith;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;</p>
<p>@SpringBootTest
@AutoConfigureMockMvc
class MemberControllerTest {
    @Autowired
    private MockMvc mockMvc;</p>
<pre><code>@Autowired
private Gson gson;

@Test
void postMemberTest() throws Exception {
    // given  (1)
    MemberDto.Post post = new MemberDto.Post(&quot;hgd@gmail.com&quot;,
                                                    &quot;홍길동&quot;,
                                                &quot;010-1234-5678&quot;);
    String content = gson.toJson(post); // (2)

    // when
    ResultActions actions =
            mockMvc.perform(                        // (3)
                                post(&quot;/v11/members&quot;)  // (4)
                                    .accept(MediaType.APPLICATION_JSON) // (5)
                                    .contentType(MediaType.APPLICATION_JSON) // (6)
                                    .content(content)   // (7)
                            );

    // then
    actions
            .andExpect(status().isCreated()) // (8)
            .andExpect(header().string(&quot;Location&quot;, is(startsWith(&quot;/v11/members/&quot;))));  // (9)
}</code></pre><p>}</p>
<pre><code>(참고) Gson 라이브러리를 사용하기 위해서는 build.gradle의 dependencies {...}에 implementation &#39;com.google.code.gson:gson&#39;를 추가해 주어야 합니다.

(When 부분)    
post() 메서드를 통해 HTTP POST METHOD와 request URL을 설정합니다.
accept() 메서드를 통해 클라이언트 쪽에서 리턴 받을 응답 데이터 타입으로 JSON 타입을 설정합니다.
contentType() 메서드를 통해 서버 쪽에서 처리 가능한 Content Type으로 JSON 타입을 설정합니다.
content() 메서드를 통해 request body 데이터를 설정합니다.
request body에 전달하는 데이터는 Gson 라이브러리를 이용해 변환된 JSON 문자열입니다.

(then 부분)
MockMvc의 perform() 메서드는 ResultActions 타입의 객체를 리턴하는데, 이 ResultActions 객체를 이용해서 우리가 전송한 request에 대한 검증을 수행할 수 있습니다.

andExpect() 메서드를 통해 파라미터로 입력한 매처(Matcher)로 예상되는 기대 결과를 검증할 수 있습니다.
status().isCreated()를 통해 response status가 201(Created)인지 매치시키고 있습니다. 즉, 백엔드 측에 리소스인 회원 정보가 잘 생성(저장)되었는지를 검증합니다.
header().string(&quot;Location&quot;, is(startsWith(&quot;/v11/members/&quot;)))을 통해 HTTP header에 추가된 Location의 문자열 값이 “/v11/members/”로 시작하는지 검증합니다.
Location header가 예상하는 값과 일치한다라는 것은 백엔드 측에 리소스(회원 정보)가 잘 생성되었다는 것을 의미합니다.

&amp;nbsp
&amp;nbsp
- HTTP GET request에 대한 테스트</code></pre><p>import com.google.gson.Gson;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.util.UriComponentsBuilder;</p>
<p>import java.net.URI;</p>
<p>import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.startsWith;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;</p>
<p>@Transactional
@SpringBootTest
@AutoConfigureMockMvc
class MemberControllerTest {
    @Autowired
    private MockMvc mockMvc;</p>
<pre><code>@Autowired
private Gson gson;

...
...

@Test
void getMemberTest() throws Exception {
    // =================================== (1) postMember()를 이용한 테스트 데이터 생성 시작
    // given
    MemberDto.Post post = new MemberDto.Post(&quot;hgd@gmail.com&quot;,&quot;홍길동&quot;,&quot;010-1111-1111&quot;);
    String postContent = gson.toJson(post);

    ResultActions postActions =
            mockMvc.perform(
                    post(&quot;/v11/members&quot;)
                            .accept(MediaType.APPLICATION_JSON)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(postContent)
            );
    // =================================== (1) postMember()를 이용한 테스트 데이터 생성 끝

    // (2)
    String location = postActions.andReturn().getResponse().getHeader(&quot;Location&quot;); // &quot;/v11/members/1&quot;

    // when / then
    mockMvc.perform(
                    get(location)      // (3)
                            .accept(MediaType.APPLICATION_JSON)
            )
            .andExpect(status().isOk())    // (4)
            .andExpect(jsonPath(&quot;$.data.email&quot;).value(post.getEmail()))   // (5)
            .andExpect(jsonPath(&quot;$.data.name&quot;).value(post.getName()))     // (6)
            .andExpect(jsonPath(&quot;$.data.phone&quot;).value(post.getPhone()));  // (7)
}</code></pre><p>}</p>
<pre><code>(2)에서는 postMember()의 response에 전달되는 Location header 값을 가져오는 로직입니다.
(2)와 같이 postActions.andReturn().getResponse().getHeader(&quot;Location&quot;)로 접근해서 Location header의 값을 얻어올 수 있습니다.

(5)에서는 jsonPath() 메서드를 통해 response body(JSON 형식)의 각 프로퍼티 중에서 응답으로 전달받는 email 값이 request body로 전송한 email과 일치하는지 검증하고 있습니다.

MockMvcResultMatchers 클래스에서 지원하는 jsonPath()를 사용하면 JSON 형식의 개별 프로퍼티에 손쉽게 접근할 수 있습니다.

&amp;nbsp
&amp;nbsp
### ⚫ 데이터 엑세스 계층 테스트

- 회원 정보 저장 테스트    </code></pre><p>mport com.Member.member.entity.Member;
import com.Member.member.repository.MemberRepository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;</p>
<p>import static org.junit.jupiter.api.Assertions.*;</p>
<p>@DataJpaTest   // (1)
public class MemberRepositoryTest {
    @Autowired
    private MemberRepository memberRepository;   // (2)</p>
<pre><code>@Test
public void saveMemberTest() {
    // given  (3)
    Member member = new Member();
    member.setEmail(&quot;hgd@gmail.com&quot;);
    member.setName(&quot;홍길동&quot;);
    member.setPhone(&quot;010-1111-2222&quot;);

    // when  (4)
    Member savedMember = memberRepository.save(member);

    // then  (5)
    assertNotNull(savedMember); // (5-1)
    assertTrue(member.getEmail().equals(savedMember.getEmail()));
    assertTrue(member.getName().equals(savedMember.getName()));
    assertTrue(member.getPhone().equals(savedMember.getPhone()));
}</code></pre><p>}</p>
<pre><code>spring에서 데이터 액세스 계층을 테스트하기 위한 가장 핵심적인 방법은 바로 @DataJpaTest 애너테이션입니다.

@DataJpaTest 애너테이션을 테스트 클래스에 추가함으로써, MemberRepository의 기능을 정상적으로 사용하기 위한 Configuration을 Spring이 자동으로 해주게 됩니다.

@DataJpaTest 애너테이션은 @Transactional 애너테이션을 포함하고 있기 때문에 하나의 테스트 케이스 실행이 종료되는 시점에 데이터베이스에 저장된 데이터는 rollback 처리됩니다.

즉, 여러 개의 테스트 케이스를 한꺼번에 실행시켜도 하나의 테스트 케이스가 종료될 때마다 데이터베이스의 상태가 초기 상태를 유지한다는 것입니다.

&amp;nbsp
#### &lt;참고&gt;
위는 Spring Data JPA 환경에서의 애너테이션이지만,
Spring JDBC 환경에서는 @JdbcTest, Spring Data JDBC 환경에서는 @DataJdbcTest를 사용하면 손쉽게 데이터 액세스 계층에 대한 테스트를 진행할 수 있습니다.</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[Hamcrest를 사용한 Assertion]]></title>
            <link>https://velog.io/@be_chobo/Hamcrest%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%9C-Assertion</link>
            <guid>https://velog.io/@be_chobo/Hamcrest%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%9C-Assertion</guid>
            <pubDate>Thu, 12 Oct 2023 07:57:55 GMT</pubDate>
            <description><![CDATA[<h3 id="⚫-hamcrest란">⚫ Hamcrest란?</h3>
<p>Hamcrest는 JUnit 기반의 단위 테스트에서 사용할 수 있는 Assertion Framework입니다.</p>
<p>*<em>장점 *</em></p>
<ol>
<li>Assertion을 위한 매쳐(Matcher)가 자연스러운 문장으로 이어지므로 가독성이 향상된다.</li>
<li>테스트 실패 메시지를 이해하기 쉽다.</li>
<li>다양한 Matcher를 제공한다</li>
</ol>
<p>&amp;nbsp
&amp;nbsp</p>
<h3 id="hamcrest-assertion-적용">Hamcrest Assertion 적용</h3>
<p>예시 1</p>
<pre><code>import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;

public class HelloHamcrestTest {

    @DisplayName(&quot;Hello Junit Test using hamcrest&quot;)
    @Test
    public void assertionTest1() {
        String expected = &quot;Hello, JUnit&quot;;
        String actual = &quot;Hello, JUnit&quot;;

        //assertEquals(expected, actual);
        assertThat(actual, is(equalTo(expected)));  
    }
}</code></pre><p>assertThat()의 첫 번째 파라미터는 테스트 대상의 실제 결과 값입니다.
두 번째 파라미터는 기대하는 값입니다. 즉, 이런 값일 거라고 기대(예상)하는 값입니다.</p>
<p>&amp;nbsp
예시 2</p>
<pre><code>import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;

public class AssertionNullHamcrestTest {

    @DisplayName(&quot;AssertionNull() Test&quot;)
    @Test
    public void assertNotNullTest() {
        String currencyName = getCryptoCurrency(&quot;ETH&quot;);

        //assertNotNull(currencyName, &quot;should be not null&quot;);

        assertThat(currencyName, is(notNullValue()));  
        assertThat(currencyName, is(nullValue()));    
    }

    private String getCryptoCurrency(String unit) {
        return CryptoCurrency.map.get(unit);
    }
}</code></pre><p>Not Null이나 Null 테스트를 하기 위해서는 위와 같이 Hamcrest의 <code>is()</code>, <code>notNullValue()</code>나 <code>NullValue()</code> 매쳐를 함께 사용할 수 있습니다.</p>
<p>&amp;nbsp
예시3</p>
<pre><code>import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertThrows;

public class AssertionExceptionHamcrestTest {

    @DisplayName(&quot;throws NullPointerException when map.get()&quot;)
    @Test
    public void assertionThrowExceptionTest() {
        Throwable actualException = assertThrows(NullPointerException.class,
                () -&gt; getCryptoCurrency(&quot;XRP&quot;));   

        assertThat(actualException.getClass(), is(NullPointerException.class));  
    }

    private String getCryptoCurrency(String unit) {
        return CryptoCurrency.map.get(unit).toUpperCase();
    }
}</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[JUnit을 사용한 단위 테스트]]></title>
            <link>https://velog.io/@be_chobo/JUnit%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%9C-%EB%8B%A8%EC%9C%84-%ED%85%8C%EC%8A%A4%ED%8A%B8</link>
            <guid>https://velog.io/@be_chobo/JUnit%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%9C-%EB%8B%A8%EC%9C%84-%ED%85%8C%EC%8A%A4%ED%8A%B8</guid>
            <pubDate>Thu, 12 Oct 2023 07:45:09 GMT</pubDate>
            <description><![CDATA[<h3 id="⚫-단위-테스트를-위한-first-원칙">⚫ 단위 테스트를 위한 FIRST 원칙</h3>
<ul>
<li><p>Fast(빠르게) : 일반적으로 작성한 테스트 케이스는 빨라야 한다는 의미입니다.</p>
</li>
<li><p>Independent(독립적으로) : 각각의 테스트 케이스는 독립적이어야 한다는 의미입니다.
예를 들어, A라는 테스트 케이스를 먼저 실행시킨 후에 다음으로 B라는 테스트 케이스를 실행시켰더니 테스트에 실패하게 된다면 테스트 케이스끼리 독립적이지 않은 것입니다.</p>
</li>
<li><p>Repeatable(반복 가능하도록) : 테스트 케이스는 어떤 환경에서도 반복해서 실행이 가능해야 된다는 의미입니다. 로컬 환경이나 서버 환경에서 실행하든 반복해서 같은 결과를 확인할 수 있어야 합니다.</p>
</li>
<li><p>Self-validating(셀프 검증이 되도록) : 단위 테스트는 성공 또는 실패라는 자체 검증 결과를 보여주어야 한다는 의미입니다.</p>
</li>
<li><p>Timely(시기적절하게) : 단위 테스트는 테스트하려는 기능 구현을 하기 직전에 작성해야 한다는 의미입니다.</p>
</li>
</ul>
<p>&amp;nbsp
&amp;nbsp</p>
<h3 id="⚫-given-when-then-표현-스타일">⚫ Given-When-Then 표현 스타일</h3>
<p>테스트 케이스의 가독성을 높이기 위해 given - when - then 표현 방법을 사용하는 것은 테스트 케이스를 작성하는데 유용한 방법입니다.</p>
<ul>
<li><p>Given
: 테스트를 위한 준비 과정을 명시할 수 있습니다.
: 테스트에 필요한 전제 조건들이 포함된다고 보면 됩니다.
: 테스트 대상에 전달되는 입력 값(테스트 데이터) 역시 Given에 포함됩니다.</p>
</li>
<li><p>When
: 테스트할 동작(대상)을 지정합니다.
: 단위 테스트에서는 일반적으로 메서드 호출을 통해 테스트를 진행하므로 한두 줄 정도로 작성이 끝나는 부분입니다.</p>
</li>
<li><p>Then
: 테스트의 결과를 검증하는 영역입니다.
: 일반적으로 예상하는 값(expected)과 테스트 대상 메서드의 동작 수행 결과(actual) 값을 비교해서 기대한 대로 동작을 수행하는지 검증(Assertion)하는 코드들이 포함됩니다.</p>
</li>
</ul>
<p>&amp;nbsp
&amp;nbsp</p>
<h3 id="⚫-junit이란">⚫ JUnit이란?</h3>
<p>JUnit은 Java 언어로 만들어진 애플리케이션을 테스트하기 위한 오픈 소스 테스트 프레임워크로서 사실상 Java의 표준 테스트 프레임워크라고 봐도 무방합니다.</p>
<h4 id="🟩assertion-메서드-사용">🟩Assertion 메서드 사용</h4>
<ul>
<li>assertEquals() : 기대하는 값과 실제 결과 값이 같은지를 검증<pre><code>import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
</code></pre></li>
</ul>
<p>import static org.junit.jupiter.api.Assertions.assertEquals;</p>
<p>public class HelloJUnitTest {
    @DisplayName(&quot;Hello JUnit Test&quot;)<br>    @Test
    public void assertionTest() {
        String expected = &quot;Hello, JUnit&quot;;
        String actual = &quot;Hello, JUnit&quot;;</p>
<pre><code>    assertEquals(expected, actual); 
}</code></pre><p>}</p>
<pre><code>&amp;nbsp
- assertNotNull() : Null 여부 테스트</code></pre><p>import com.codestates.CryptoCurrency;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;</p>
<p>import static org.junit.jupiter.api.Assertions.assertNotNull;</p>
<p>public class AssertionNotNullTest {</p>
<pre><code>@DisplayName(&quot;AssertionNull() Test&quot;)
@Test
public void assertNotNullTest() {
    String currencyName = getCryptoCurrency(&quot;ETH&quot;);


    assertNotNull(currencyName, &quot;should be not null&quot;);
}

private String getCryptoCurrency(String unit) {
    return CryptoCurrency.map.get(unit);
}</code></pre><p>}</p>
<pre><code>assertNotNull() 메서드의 첫 번째 파라미터는 테스트 대상 객체이고, 두 번째 파라미터는 테스트에 실패했을 때, 표시할 메시지입니다.

&amp;nbsp
- assertThrows() : 예외(Exception) 테스트</code></pre><p>import com.codestates.CryptoCurrency;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;</p>
<p>import static org.junit.jupiter.api.Assertions.*;</p>
<p>public class AssertionExceptionTest {</p>
<pre><code>@DisplayName(&quot;throws NullPointerException when map.get()&quot;)
@Test
public void assertionThrowExceptionTest() {

    assertThrows(NullPointerException.class, () -&gt; getCryptoCurrency(&quot;XRP&quot;));
}

private String getCryptoCurrency(String unit) {
    return CryptoCurrency.map.get(unit).toUpperCase();
}</code></pre><p>}</p>
<pre><code>getCryptoCurrency() 메서드를 호출했을 때, NullPointerException이 발생하는지 테스트하고 있습니다.
assertThrows()의 첫 번째 파라미터에는 발생이 기대되는 예외 클래스를 입력하고, 두 번째 파라미터인 람다 표현식에서는 테스트 대상 메서드를 호출하면 됩니다.

&amp;nbsp
- assertDoesNotThrow() : 예외가 발생하지 않는다고 기대하는 Assertion 메서드</code></pre><p>assertDoesNotThrow(() -&gt; getCryptoCurrency(&quot;XRP&quot;));</p>
<pre><code>
&amp;nbsp
#### 🟩테스트 케이스 실행 전, 전 처리
- @BeforeEach</code></pre><p>import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;</p>
<p>public class BeforeEach1Test {</p>
<pre><code>@BeforeEach
public void init() {
    System.out.println(&quot;Pre-processing before each test case&quot;);
}

@DisplayName(&quot;@BeforeEach Test1&quot;)
@Test
public void beforeEachTest() {

}

@DisplayName(&quot;@BeforeEach Test2&quot;)
@Test
public void beforeEachTest2() {

}</code></pre><p>}</p>
<pre><code>@BeforeEach 애너테이션을 추가한 메서드는 테스트 케이스가 각각 실행될 때마다 테스트 케이스 실행 직전에 먼저 실행되어 초기화 작업 등을 진행할 수 있습니다.

&amp;nbsp
- @BeforeAll
테스트 케이스가 실행되기 전에 딱 한 번만 초기화 작업을 할 수 있도록 해주는 애너테이션입니다.

@BeforeAll 애너테이션을 추가한 메서드는 정적 메서드(static method)여야 합니다.

#### 참고) 테스트 케이스 실행 후, 후처리
@AfterEach, @AfterAll로 앞서 @BeforeEach, @BeforeAll과 동작 방식이 같습니다.

&amp;nbsp
&amp;nbsp
#### 🟩Assumption을 이용한 조건부 테스트
예를들어</code></pre><p>public class AssumptionTest {
    @DisplayName(&quot;Assumption Test&quot;)
    @Test
    public void assumptionTest() {</p>
<pre><code>    assumeTrue(System.getProperty(&quot;os.name&quot;).startsWith(&quot;Windows&quot;));

    System.out.println(&quot;execute?&quot;);
    assertTrue(processOnlyWindowsTask());
}

private boolean processOnlyWindowsTask() {
    return true;
}</code></pre><p>}</p>
<pre><code>위 코드에서 assumetrue() 값이 true이면 아래 로직을 실행하고 아니라면 실행되지 않습니다.

assumeTrue()는 특정 OS 환경 등의 특정 조건에서 선택적인 테스트가 필요하다면 유용하게 사용할 수 있는 JUnit 5의 API입니다.
</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[다중 DB(데이터베이스) 연결]]></title>
            <link>https://velog.io/@be_chobo/%EB%8B%A4%EC%A4%91-DB%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%EC%97%B0%EA%B2%B0</link>
            <guid>https://velog.io/@be_chobo/%EB%8B%A4%EC%A4%91-DB%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%EC%97%B0%EA%B2%B0</guid>
            <pubDate>Mon, 09 Oct 2023 09:52:43 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>본 포스팅은 프로젝트 완료 후, To do로 남겨둔 회원 탈퇴 이관과 관련한 작업을 위해서 다중 데이터베이스 연결과 관련된 포스팅으로 실제 회원 탈퇴와 관련된 정책은 회사마다 다르므로 실제 이관 작업은 생략하고, 다중 데이터베이스를 연결하는 것에 초점을 맞췄습니다.</p>
</blockquote>
<h3 id="⚫다중-연결">⚫다중 연결</h3>
<p>스프링 부트에서는 하나의 데이터베이스에 대해서만 application.properties 변수 설정을 통해 연결이 가능하기 때문에 2개 이상부터는 Config 클래스 작성을 통해서만 연결이 가능합니다.</p>
<p>단순히 application.properties 파일에 2개의 DB 소스를 줄 경우 오류가 발생하고, Config 클래스를 통해 이를 해결해야 합니다.</p>
<p>(이번 포스팅에서는 MY SQL의 데이터베이스 2개를 통해 실습을 진행합니다.)</p>
<p>MYSQL은 내부에 독립적인 DB공간을 제공하기 때문에 내부에 DB 2개를 생성하여 진행합니다.</p>
<p>&amp;nbsp</p>
<h4 id="👍프로젝트-필수-의존성-추가">👍프로젝트 필수 의존성 추가</h4>
<ul>
<li>MYSQL Driver</li>
<li>Spring Data JPA</li>
</ul>
<p>&amp;nbsp</p>
<h3 id="⚫데이터베이스별-패키지-생성">⚫데이터베이스별 패키지 생성</h3>
<p><img src="https://velog.velcdn.com/images/be_chobo/post/4b50e8e7-f960-4b8e-9a96-4d6d4df84f9b/image.png" alt="">
각각의 데이터베이스에 해당하는 Entity와 repository를 담을 패키지를 생성합니다.</p>
<p>&amp;nbsp</p>
<h3 id="⚫db별-config-클래스-작성">⚫DB별 Config 클래스 작성</h3>
<p>연결할 데이터베이스의 개수 만큼 Config 클래스를 작성하면 됩니다.</p>
<ul>
<li>first<pre><code>package com.example.testdatabase.config;
</code></pre></li>
</ul>
<p>import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;</p>
<p>import javax.sql.DataSource;
import java.util.HashMap;</p>
<p>@Configuration
@EnableJpaRepositories(
        basePackages = &quot;com.example.testdatabase.firstdb.repository&quot;,
        entityManagerFactoryRef = &quot;firstEntityManger&quot;,
        transactionManagerRef = &quot;firstTransactionManager&quot;
)
public class FirstDatabaseConfig {</p>
<pre><code>@Primary
@Bean
public PlatformTransactionManager firstTransactionManager() {

    JpaTransactionManager transactionManager = new JpaTransactionManager();
    transactionManager.setEntityManagerFactory(firstEntityManger().getObject());

    return transactionManager;
}

@Primary
@Bean
public LocalContainerEntityManagerFactoryBean firstEntityManger() {

    LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();

    em.setDataSource(firstDataSource());
    em.setPackagesToScan(new String[]{&quot;com.example.testdatabase.firstdb.entity&quot;});
    em.setJpaVendorAdapter(new HibernateJpaVendorAdapter());

    HashMap&lt;String, Object&gt; properties = new HashMap&lt;&gt;();
    properties.put(&quot;hibernate.hbm2ddl.auto&quot;, &quot;update&quot;);

    em.setJpaPropertyMap(properties);

    return em;
}

@Primary
@Bean
public DataSource firstDataSource() {

    return DataSourceBuilder.create()
            .driverClassName(&quot;com.mysql.cj.jdbc.Driver&quot;)
            .url(&quot;jdbc:mysql://아이피:3306/디비?useSSL=false&amp;useUnicode=true&amp;serverTimezone=Asia/Seoul&amp;allowPublicKeyRetrieval=true&quot;)
            .username(&quot;&quot;)
            .password(&quot;&quot;)
            .build();
}</code></pre><p>}</p>
<pre><code>코드와 관련된 설명을 잠깐 해보자면,

일단 @Configuration으로 설정파일임을 명시하고,
@EnableJpaRepositories를 통해 JpaRepository를 활성화 합니다.

@EnableJpaRepositories의 
- basePackages는 JPA 리포지토리 인터페이스가 위치한 패키지를 나타냅니다. 
- entityManagerFactoryRef는 EntityManagerFactory 빈의 이름을 지정합니다. 이 빈은 데이터베이스와 연결됩니다.
- transactionManagerRef는 데이터 소스에 대한 트랜잭션 관리자 빈의 이름을 지정합니다. 이 빈은 데이터베이스 트랜잭션을 관리합니다.

즉, 각 데이터 소스에 대한 EntityManagerFactory와PlatformTransactionManager를 직접 설정해주어야 하는 것입니다.

직접 설정한 firstEntityManger()를 보면</code></pre><p>em.setDataSource(firstDataSource()); //데이터 소스 set
em.setPackagesToScan(new String[]{&quot;com.example.testdatabase.firstdb.entity&quot;}); // entity를 스캔할 패키지를 설정합니다. 이것은 string 배열 값으로 들어갑니다.
em.setJpaVendorAdapter(new HibernateJpaVendorAdapter());  // 어댑터 설정</p>
<pre><code></code></pre><p>//ddl autp 설정을 map에 넣어서 entitiymanger의 jpapropertymap의 넣어줍니다.
HashMap&lt;String, Object&gt; properties = new HashMap&lt;&gt;(); 
properties.put(&quot;hibernate.hbm2ddl.auto&quot;, &quot;update&quot;);</p>
<p>em.setJpaPropertyMap(properties);</p>
<pre><code>이렇게 됩니다.

그 다음 firstTransactionManager()를 보면</code></pre><p>paTransactionManager transactionManager = new JpaTransactionManager();
transactionManager.setEntityManagerFactory(firstEntityManger().getObject()); //위에서 설정한 entitymanger값을 넣어줍니다.</p>
<p>return transactionManager;</p>
<pre><code>
다음 firstDataSource()를 보면</code></pre><p>@Primary
@Bean
public DataSource firstDataSource() {</p>
<pre><code>    //builder패턴을 통해  datasource를 만들어 관련된 설정값들을 지정해 줍니다.
    return DataSourceBuilder.create()
            .driverClassName(&quot;com.mysql.cj.jdbc.Driver&quot;)
            .url(&quot;jdbc:mysql://아이피:3306/디비?useSSL=false&amp;useUnicode=true&amp;serverTimezone=Asia/Seoul&amp;allowPublicKeyRetrieval=true&quot;)
            .username(&quot;&quot;)
            .password(&quot;&quot;)
            .build();
}</code></pre><pre><code>여기서 실행을 시키면 오류가 납니다. 
second의 config와는 다르게 first에는 오류가 나지 않기 위해 @Primary 어노테이션을 각각의 빈에 적어줍니다.

&amp;nbsp
&amp;nbsp
- second</code></pre><p>package com.example.testdatabase.config;</p>
<p>import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;</p>
<p>import javax.sql.DataSource;
import java.util.HashMap;</p>
<p>@Configuration
@EnableJpaRepositories(
        basePackages = &quot;com.example.testdatabase.seconddb.repository&quot;,
        entityManagerFactoryRef = &quot;secondEntityManager&quot;,
        transactionManagerRef = &quot;secondTransactionManager&quot;
)
public class SecondDatabaseConfig {</p>
<pre><code>@Bean
public PlatformTransactionManager secondTransactionManager() {

    JpaTransactionManager transactionManager = new JpaTransactionManager();

    transactionManager.setEntityManagerFactory(secondEntityManager().getObject());

    return transactionManager;
}

@Bean
public LocalContainerEntityManagerFactoryBean secondEntityManager() {

    LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();

    em.setDataSource(secondDataSource());
    em.setPackagesToScan(new String[]{&quot;com.example.testdatabase.seconddb.entity&quot;});
    em.setJpaVendorAdapter(new HibernateJpaVendorAdapter());

    HashMap&lt;String, Object&gt; properties = new HashMap&lt;&gt;();
    properties.put(&quot;hibernate.hbm2ddl.auto&quot;, &quot;update&quot;);

    em.setJpaPropertyMap(properties);

    return em;
}

@Bean
public DataSource secondDataSource() {

    return DataSourceBuilder.create()
            .driverClassName(&quot;com.mysql.cj.jdbc.Driver&quot;)
            .url(&quot;jdbc:mysql://아이피:3306/디비?useSSL=false&amp;useUnicode=true&amp;serverTimezone=Asia/Seoul&amp;allowPublicKeyRetrieval=true&quot;)
            .username(&quot;&quot;)
            .password(&quot;&quot;)
            .build();
}</code></pre><p>}</p>
<pre><code>
second 또한 first config와 동일한 코드이므로 설명은 생략합니다.
</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[[spring mvc] 트랜잭션 적용]]></title>
            <link>https://velog.io/@be_chobo/spring-mvc-%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98-%EC%A0%81%EC%9A%A9</link>
            <guid>https://velog.io/@be_chobo/spring-mvc-%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98-%EC%A0%81%EC%9A%A9</guid>
            <pubDate>Thu, 05 Oct 2023 07:57:27 GMT</pubDate>
            <description><![CDATA[<p>트랜잭션을 적용하는 방법은 크게 두 가지로 나뉩니다.</p>
<p>첫 번째는 작성한 비즈니스 로직에 애너테이션을 추가하는 방식이고, 또 하나는 AOP 방식을 이용해서 비즈니스 로직에서 아예 트랜잭션 적용 코드 자체를 감추는 방식입니다.</p>
<p>여기서는 우선 애너테이션을 사용한 방식만 기록하겠습니다.</p>
<p>&amp;nbsp</p>
<h3 id="👍애너테이션-방식의-트랜잭션-적용">👍[애너테이션 방식의 트랜잭션 적용]</h3>
<h4 id="1-클래스-레벨에-transactional-적용">1. 클래스 레벨에 @Transactional 적용</h4>
<pre><code>@Service
@Transactional   // (1)
public class MemberService {
    private final MemberRepository memberRepository;

    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

    public Member createMember(Member member) {
        verifyExistsEmail(member.getEmail());

        return memberRepository.save(member);
    }

        ...
        ...
}</code></pre><p>위와 같이 하면 해당 클래스에서 memberRepository의 기능을 이용하는 모든 메서드에 트랜잭션이 적용됩니다.</p>
<p>❗JPA 로그 레벨 설정</p>
<pre><code>spring:
  h2:
    console:
      enabled: true
      path: /h2
  datasource:
    url: jdbc:h2:mem:test
  jpa:
    hibernate:
...
...

logging:         # (1) 로그 레벨 설정
  level:
    org:
      springframework:
        orm:
          jpa: DEBUG</code></pre><blockquote>
<p>체크 예외(checked exception)는 @Transactional 애너테이션만 추가해서는 rollback이 되지 않습니다.</p>
</blockquote>
<p>체크 예외의 경우, 말 그대로 체크를 해야 되는 예외입니다. 따라서 캐치(catch) 한 후에 해당 예외를 복구할지 회피할지 등의 적절한 예외 전략을 고민해 볼 필요가 있을 것입니다.</p>
<blockquote>
</blockquote>
<p>만일 별도의 예외 전략을 짤 필요가 없다면 @Transactional(rollbackFor = {SQLException.class, DataFormatException.class})와 같이 해당 체크 예외를 직접 지정해 주거나 언체크 예외(unchecked exception)로 감싸서 rollback이 동작하도록 할 수 있습니다.</p>
<p>&amp;nbsp</p>
<h4 id="2-메서드-레벨에-transactional-적용">2. 메서드 레벨에 @Transactional 적용</h4>
<pre><code>@Service
@Transactional  // (1)
public class MemberService {
    private final MemberRepository memberRepository;

    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

        // (2)
    @Transactional(readOnly = true)
    public Member findMember(long memberId) {
        return findVerifiedMember(memberId);
    }

        ...
        ...
}</code></pre><p>조회 메서드에는 위와 같이 @Transactional(readOnly = true)로 설정하는데 그 이유는 아래와 같습니다.</p>
<p>JPA에서 commit이 호출되면 영속성 컨텍스트가 flush 됩니다.</p>
<p>그런데 @Transactional(readOnly = true)로 설정하면 JPA 내부적으로 영속성 컨텍스트를 flush하지 않습니다.</p>
<p>그리고 읽기 전용 트랜잭션일 경우, 변경 감지를 위한 스냅샷 생성도 진행하지 않습니다.</p>
<p>flush 처리를 하지 않고, 스냅샷도 생성하지 않으므로 불필요한 추가 동작을 줄일 수 있습니다.</p>
<p>즉, 조회 메서드에는 readonly 속성을 true로 지정해서 JPA가 자체적으로 성능 최적화 과정을 거치도록 하는 것이 좋습니다.</p>
<p>&amp;nbsp
&amp;nbsp</p>
<h3 id="⚫트랜잭션-전파">⚫트랜잭션 전파</h3>
<p>트랜잭션 전파란 트랜잭션의 경계에서 진행 중인 트랜잭션이 존재할 때 또는 존재하지 않을 때, 어떻게 동작할 것인지 결정하는 방식을 의미합니다.</p>
<p>트랜잭션 전파는 propagation 애트리뷰트를 통해서 설정할 수 있으며, 대표적으로 아래와 같은 propagation 유형을 사용할 수 있습니다.</p>
<p><strong>1. Propagation.REQUIRED :</strong>  @Transactional 애너테이션의 propagation 애트리뷰트에 지정한 Propagation.REQUIRED 는 일반적으로 가장 많이 사용되는 propagation 유형의 디폴트 값입니다.
진행 중인 트랜잭션이 없으면 새로 시작하고, 진행 중인 트랜잭션이 있으면 해당 트랜잭션에 참여합니다.</p>
<p><strong>2. Propagation.REQUIRES_NEW</strong> : 이미 진행 중인 트랜잭션과 무관하게 새로운 트랜잭션이 시작됩니다. 기존에 진행 중이던 트랜잭션은 새로 시작된 트랜잭션이 종료할 때까지 중지됩니다.</p>
<p><strong>3. Propagation.MANDATORY :</strong> Propagation.REQUIRED는 진행 중인 트랜잭션이 없으면 새로운 트랜잭션이 시작되는 반면, Propagation.MANDATORY는 진행 중인 트랜잭션이 없으면 예외를 발생시킵니다.</p>
<p><strong>4. Propagation.NOT_SUPPORTED :</strong> 트랜잭션을 필요로 하지 않음을 의미합니다. 진행 중인 트랜잭션이 있으면 메서드 실행이 종료될 때까지 진행 중인 트랜잭션은 중지되며, 메서드 실행이 종료되면 트랜잭션을 계속 진행합니다.</p>
<p>*<em>5. Propagation.NEVER : *</em> 트랜잭션을 필요로 하지 않음을 의미하며, 진행 중인 트랜잭션이 존재할 경우에는 예외를 발생시킵니다.</p>
<p>&amp;nbsp</p>
<p>(참고)🤔트랜잭션 격리레벨<br>트랜잭션은 다른 트랜잭션에 영향을 주지 않고, 독립적으로 실행되어야 하는 격리성이 보장되어야 하는데 Spring은 이러한 격리성을 조정할 수 있는 옵션을 @Transactional 애너테이션의 isolation 애트리뷰트를 통해 제공하고 있습니다.</p>
<p>Isolation.DEFAULT : 데이터베이스에서 제공하는 기본 값입니다.</p>
<p>Isolation.READ_UNCOMMITTED : 다른 트랜잭션에서 커밋하지 않은 데이터를 읽는 것을 허용합니다.</p>
<p>Isolation.READ_COMMITTED : 다른 트랜잭션에 의해 커밋된 데이터를 읽는 것을 허용합니다.</p>
<p>Isolation.REPEATABLE_READ : 트랜잭션 내에서 한 번 조회한 데이터를 반복해서 조회해도 같은 데이터가 조회되도록 합니다.</p>
<p>Isolation.SERIALIZABLE : 동일한 데이터에 대해서 동시에 두 개 이상의 트랜잭션이 수행되지 못하도록 합니다.</p>
<p>트랜잭션의 격리 레벨은 일반적으로 데이터베이스나 데이터소스에 설정된 격리 레벨을 따르는 것이 권장되므로, 이러한 격리 레벨이 있다정도의 가벼운 이해만 하면 됩니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[spring mvc] 트랜잭션(Transaction)]]></title>
            <link>https://velog.io/@be_chobo/spring-mvc-%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98Transaction</link>
            <guid>https://velog.io/@be_chobo/spring-mvc-%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98Transaction</guid>
            <pubDate>Fri, 22 Sep 2023 05:16:43 GMT</pubDate>
            <description><![CDATA[<h3 id="⚫트랜잭션이란">⚫트랜잭션이란?</h3>
<p>여러 개의 작업들이 마치 하나의 그룹처럼 묶여서 처리되는 중에 둘 중 하나라도 처리에 실패할 경우 애플리케이션의 신뢰성이 깨지는 상황이 발생할 수 있습니다.</p>
<p>트랜잭션은 여러 개의 작업들을 하나의 그룹으로 묶어서 처리하는 처리 단위인데 애플리케이션의 신뢰성이 깨지는 상황이 발생하면 트랜잭션이라고 부를 수 없습니다.</p>
<p>무조건 여러 개의 작업을 그룹으로 묶는다고 해서 트랜잭션이라고 부를 수 있는 게 아니라 물리적으로는 여러 개의 작업이지만 논리적으로는 마치 하나의 작업으로 인식해서 전부 성공하든가 전부 실패하든가(All or Nothing)의 둘 중 하나로만 처리되어야 트랜잭션의 의미를 가집니다.</p>
<p>이러한 트랜잭션 처리 방식은 애플리케이션에서 사용하는 데이터의 무결성을 보장하는 핵심적인 역할을 합니다.</p>
<blockquote>
<p>데이터의 무결성은 정확성, 일관성, 유효성이 유지되는 것을 의미</p>
</blockquote>
<p>&amp;nbsp
&amp;nbsp</p>
<h3 id="⚫acid-원칙">⚫ACID 원칙</h3>
<ol>
<li><p>원자성(Atomicity)
트랜잭션에서의 원자성이란 작업을 더 이상 쪼갤 수 없음을 의미합니다. 
논리적으로 하나의 작업으로 인식해서 둘 다 성공하든가 둘 다 실패하든가(All or Nothing) 중에서 하나로만 처리되는 것이 보장되어야 합니다.</p>
</li>
<li><p>일관성(Consistency)
일관성은 트랜잭션이 에러 없이 성공적으로 종료될 경우, 비즈니스 로직에서 의도하는 대로 일관성 있게 저장되거나 변경되는 것을 의미합니다.</p>
</li>
<li><p>격리성(Isolation)
격리성은 여러 개의 트랜잭션이 실행될 경우 각각 독립적으로 실행이 되어야 함을 의미합니다.</p>
</li>
<li><p>지속성(Durability)
트랜잭션이 완료되면 그 결과는 지속되어야 한다는 의미입니다.
데이터베이스가 종료되어도 데이터는 물리적인 저장소에 저장되어 지속적으로 유지되어야 한다는 의미입니다.</p>
</li>
</ol>
<p>&amp;nbsp
&amp;nbsp</p>
<h3 id="⚫트랜잭션-커밋commit-롤백rollback">⚫트랜잭션 커밋(commit), 롤백(rollback)</h3>
<p><strong>커밋(commit)</strong> : 모든 작업을 최종적으로 데이터베이스에 반영하는 명령어로써  commit 명령을 수행하면 변경된 내용이 데이터베이스에 영구적으로 저장됩니다.</p>
<p>만약 commit 명령을 수행하지 않으면 작업의 결과가 데이터베이스에 최종적으로 반영되지 않습니다.</p>
<p>commit 명령을 수행하면, 하나의 트랜젝션 과정은 종료하게 됩니다</p>
<p><strong>롤백(rollback)</strong> : 롤백(rollback)은 작업 중 문제가 발생했을 때, 트랜잭션 내에서 수행된 작업들을 취소합니다.
따라서 트랜잭션 시작 이 전의 상태로 되돌아갑니다.</p>
<p>우리가 JPA API를 사용해서 commit을 수행하는 작업은 너무나도 간단한 작업인데, 내부적으로는 아주 복잡한 과정을 거쳐서 최종적으로 commit 명령이 데이터베이스에 전달됩니다. (차후 공부할 것)</p>
<p>&amp;nbsp
&amp;nbsp</p>
<blockquote>
<p>트랜잭션은 사실 데이터베이스에만 한정해서 사용하는 의미는 아닙니다.</p>
</blockquote>
<p>두 개의 작업이 하나의 트랜잭션으로 묶여서 둘 중에 하나라도 실패할 경우 롤백(rollback)이 되어야 할 수도 있습니다</p>
<blockquote>
</blockquote>
<p>이처럼 전혀 다른 타입의 리소스(데이터베이스, 파일, 메시지 등)를 하나의 작업 단위로 묶어서 처리해야 되는 상황에서도 어떤 식으로 트랜잭션을 적용해야 할 지 고민할 수 있습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[spring mvc에서 페이징 처리(페이지네이션)]]></title>
            <link>https://velog.io/@be_chobo/spring-mvc%EC%97%90%EC%84%9C-%ED%8E%98%EC%9D%B4%EC%A7%95-%EC%B2%98%EB%A6%AC%ED%8E%98%EC%9D%B4%EC%A7%80%EB%84%A4%EC%9D%B4%EC%85%98</link>
            <guid>https://velog.io/@be_chobo/spring-mvc%EC%97%90%EC%84%9C-%ED%8E%98%EC%9D%B4%EC%A7%95-%EC%B2%98%EB%A6%AC%ED%8E%98%EC%9D%B4%EC%A7%80%EB%84%A4%EC%9D%B4%EC%85%98</guid>
            <pubDate>Fri, 22 Sep 2023 04:21:39 GMT</pubDate>
            <description><![CDATA[<h3 id="⚫간단한-페이징-처리-방법">⚫간단한 페이징 처리 방법</h3>
<p>&amp;nbsp</p>
<h4 id="🟦controller-에서의-처리">🟦Controller 에서의 처리</h4>
<pre><code>@GetMapping
    public ResponseEntity getOrders(@Positive @RequestParam int page,
                                    @Positive @RequestParam int size) {
        Page&lt;Order&gt; pageOrders = orderService.findOrders(page - 1, size);
        List&lt;Order&gt; orders = pageOrders.getContent();

       return new ResponseEntity&lt;&gt;(new MultiResponseDto&lt;&gt;(mapper.ordersToOrderResponseDtos(orders), pageOrders), HttpStatus.OK);
    }</code></pre><p>@RequestParam으로 page와 size를 받아옵니다.</p>
<p>우리가 사용하고자 하는 Pageable 의 page객체 는 0 부터 시작합니다. 그러므로 controller 단이나 service단에서 page를 -1 처리 해주는 작업이 필요합니다.</p>
<p>(위에서는 controller에서 처리해 주었습니다.)</p>
<p>&amp;nbsp</p>
<h4 id="🟦service-에서의-처리">🟦service 에서의 처리</h4>
<p>service에서의 findOrders 메서드를 보면</p>
<pre><code>public Page&lt;Order&gt; findOrders(int page, int size) {
        return orderRepository.findAll(PageRequest.of(page, size,
                Sort.by(&quot;orderId&quot;).descending()));
    }</code></pre><p>JpaRepository를 사용하여 쿼리 메서드에 Pageable 인터페이스로 파라미터를 넘기면 페이징을 사용할 수 있습니다. 그리고 반환 타입은 Page 인터페이스입니다.</p>
<p>Pageable은 인터페이스이므로 실제로 사용할 때에는 인터페이스를 구현한 PageRequest 객체를 사용합니다.</p>
<p>(PageRequest 생성자의 파라미터는 현재 페이지, 조회할 데이터 수, 정렬 정보를 파라미터로 사용할 수 있습니다.)</p>
<p>&amp;nbsp</p>
<h4 id="🟦추가적으로-페이지와-관련된-클래스를-만들어-준다">🟦추가적으로 페이지와 관련된 클래스를 만들어 준다</h4>
<p>PageInfo </p>
<pre><code>@Getter
@AllArgsConstructor
public class PageInfo {
    private int page;
    private int size;
    private int totalElements;
    private int totalPages;
}</code></pre><p>PaginationResponseDto<T></p>
<pre><code>@Getter
@AllArgsConstructor
public class PaginationResponseDto&lt;T&gt; {
    private List&lt;T&gt; data;
    private PageInfo pageInfo;
}</code></pre><p>위와 같이 데이터 정보와 페이지 정보를 같이 보내주는 작업이 필요합니다.
  이러한 responseDto가 필요하므로 필요에 따라서 쓸 수 있게</p>
<pre><code>public class ResponseDto {
    @Getter
    @AllArgsConstructor
    public static class SingleResponseDto&lt;T&gt; {
        private T data;
    }

    @Getter
    public static class MultipleResponseDto&lt;T&gt; {
        private List&lt;T&gt; data;
        private PageInfo pageInfo;

        public MultipleResponseDto(List&lt;T&gt; data, Page page) {
            this.data = data;
            this.pageInfo = new PageInfo(page.getNumber() + 1, page.getSize(), page.getTotalElements(), page.getTotalPages());
        }
    }

    @Getter
    public static class MultipleInfoResponseDto&lt;T&gt; {
        private T data;
        private PageInfo pageInfo;

        public MultipleInfoResponseDto(T data, Page page) {
            this.data = data;
            this.pageInfo = new PageInfo(page.getNumber() + 1, page.getSize(), page.getTotalElements(), page.getTotalPages());
        }
    }
}</code></pre><p>  PaginationResponseDto을 따로 빼는 것이 아니라
이러한 방식으로 한번에 responseDto를 처리 해주는 것이 편리합니다.</p>
<p>  그래서 controller에서 pageinfo 객체를 생성해서 처리하는 것이 아닌, responseDto에서 page객체에서 필요한 해당 pageInfo의 멤버 값만을  responseEntity에 알맞게 보내 줄 수 있게 처리해 줍니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[JPA 기반 데이터 액세스 계층(4) - Spring Data JPA를 통한 데이터 엑세스 계층 구현]]></title>
            <link>https://velog.io/@be_chobo/JPA-%EA%B8%B0%EB%B0%98-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%95%A1%EC%84%B8%EC%8A%A4-%EA%B3%84%EC%B8%B54-Spring-Data-JPA%EB%A5%BC-%ED%86%B5%ED%95%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%97%91%EC%84%B8%EC%8A%A4-%EA%B3%84%EC%B8%B5-%EA%B5%AC%ED%98%84</link>
            <guid>https://velog.io/@be_chobo/JPA-%EA%B8%B0%EB%B0%98-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%95%A1%EC%84%B8%EC%8A%A4-%EA%B3%84%EC%B8%B54-Spring-Data-JPA%EB%A5%BC-%ED%86%B5%ED%95%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%97%91%EC%84%B8%EC%8A%A4-%EA%B3%84%EC%B8%B5-%EA%B5%AC%ED%98%84</guid>
            <pubDate>Tue, 19 Sep 2023 07:59:42 GMT</pubDate>
            <description><![CDATA[<h3 id="⚫spring-data-jpa">⚫Spring Data JPA</h3>
<p>Spring Data JPA는 Spring Data 패밀리 기술 중 하나로써, JPA 기반의 데이터 액세스 기술을 좀 더 쉽게 사용할 수 있게 해주기 때문에 데이터 액세스 계층의 구현에 있어 개발 시간을 단축시켜 줍니다.</p>
<p>&amp;nbsp</p>
<h3 id="⚡jpa-vs-hibernate-orm-vs-spring-data-jpa">⚡JPA vs Hibernate ORM vs Spring Data JPA</h3>
<ul>
<li><p>JPA의 경우 이름 자체는 Jakarta Persistence API(또는 Java Persistence API)라서 마치 API를 가져다 쓸 수 있는 건가라는 생각이 들 수 있지만 JPA는 엔터프라이즈 Java 애플리케이션에서 관계형 데이터베이스를 사용하기 위해 정해 놓은 표준 스펙(사양 또는 명세, Specification)입니다.</p>
</li>
<li><p>Hibernate ORM은 JPA라는 표준 스펙을 구현한 구현체입니다. 실제 우리가 사용할 수 있는 API라고 보면 됩니다.</p>
</li>
<li><p>Spring Data JPA는 JPA 스펙을 구현한 구현체의 API(일반적으로 Hibernate ORM)를 조금 더 쉽게 사용할 수 있도록 해주는 모듈입니다.</p>
</li>
</ul>
<p>&amp;nbsp
&amp;nbsp</p>
<h3 id="⚫spring-data-jpa의-기술을-적용">⚫Spring Data JPA의 기술을 적용</h3>
<p><strong>1. 엔티티 클래스 정의</strong></p>
<pre><code>@Getter
@Setter
@NoArgsConstructor
@Entity
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long memberId;

    @Column(nullable = false, updatable = false, unique = true)
    private String email;

    @Column(length = 100, nullable = false)
    private String name;

    @Column(length = 13, nullable = false, unique = true)
    private String phone;

    @Enumerated(value = EnumType.STRING)
    @Column(length = 20, nullable = false)
    private MemberStatus memberStatus = MemberStatus.MEMBER_ACTIVE;

    @Column(nullable = false)
    private LocalDateTime createdAt = LocalDateTime.now();

    @Column(nullable = false, name = &quot;LAST_MODIFIED_AT&quot;)
    private LocalDateTime modifiedAt = LocalDateTime.now();

    @OneToMany(mappedBy = &quot;member&quot;)
    private List&lt;Order&gt; orders = new ArrayList&lt;&gt;();

    public Member(String email) {
        this.email = email;
    }

    public Member(String email, String name, String phone) {
        this.email = email;
        this.name = name;
        this.phone = phone;
    }

    public void addOrder(Order order) {
        orders.add(order);
    }

    public enum MemberStatus {
        MEMBER_ACTIVE(&quot;활동중&quot;),
        MEMBER_SLEEP(&quot;휴면 상태&quot;),
        MEMBER_QUIT(&quot;탈퇴 상태&quot;);

        @Getter
        private String status;

        MemberStatus(String status) {
            this.status = status;
        }
    }
}</code></pre><ul>
<li>엔티티에 JPA에 맞는 애너테이션을 새로 추가해야 합니다.</li>
</ul>
<p>&amp;nbsp
&amp;nbsp
<strong>2. 레포지토리 인터페이스 구현</strong></p>
<pre><code>public interface CoffeeRepository extends JpaRepository&lt;Coffee, Long&gt; {
    Optional&lt;Coffee&gt; findByCoffeeCode(String coffeeCode);

    // (2) 수정된 부분
//    @Query(value = &quot;FROM Coffee c WHERE c.coffeeId = :coffeeId&quot;)  // (2-1)
//    @Query(value = &quot;SELECT * FROM COFFEE WHERE coffee_Id = :coffeeId&quot;, nativeQuery = true) // (2-2) 
    @Query(value = &quot;SELECT c FROM Coffee c WHERE c.coffeeId = :coffeeId&quot;)  // (2-3)
    Optional&lt;Coffee&gt; findByCoffee(long coffeeId);
}
</code></pre><ul>
<li>Spring Data JDBC에서의 Repository와 비교했을 때 변경된 부분은 CrudRepository를 상속하는 대신 JpaRepository를 상속한 것 입니다.</li>
</ul>
<p>JpaRepository를 상속하지 않고, CrudRepository를 상속해도 되지만 JpaReposiroty가 JPA에 특화된 더 많은 기능들을 포함하고 있기 때문에 JpaReposiroty를 상속합니다.</p>
<p>&amp;nbsp</p>
<ul>
<li>JPQL을 통한 객체 지향 쿼리 사용<blockquote>
<p>JPA에서는 JPQL이라는 객체 지향 쿼리를 통해 데이터베이스 내의 테이블을 조회할 수 있습니다.</p>
</blockquote>
JPQL은 데이터베이스의 테이블을 대상으로 조회 작업을 진행하는 것이 아니라 엔티티 클래스의 객체를 대상으로 객체를 조회하는 방법입니다.<blockquote>
</blockquote>
JPQL의 문법을 사용해서 객체를 조회하면 JPA가 내부적으로 JPQL을 분석해서 적절한 SQL을 만든 후에 데이터베이스를 조회하고, 조회한 결과를 엔티티 객체로 매핑한 뒤에 반환합니다.</li>
</ul>
<p>JPQL은 객체를 대상으로 한 조회이기 때문에 COFFEE 테이블이 아니라 Coffee 클래스라는 객체를 지정해야 하고, coffee_id라는 열이 아닌 coffeeId 필드를 지정해야 합니다.</p>
<p>따라서 (2-3)의 “SELECT c FROM Coffee c WHERE c.coffeeId = :coffeeId”에서 Coffee는 클래스명이고, coffeeId는 Coffee 클래스의 필드명입니다.</p>
<p>‘c’는 Coffee 클래스의 별칭이기 때문에 “SELECT c FROM~” 와 같이 SQL에서 사용하는 ‘*’이 아니라 ‘c’로 모든 필드를 조회하는 것입니다.</p>
<p>(2-3)은 (2-1)과 같이 ‘SELECT c’를 생략한 형태로 사용이 가능합니다.</p>
<p>&amp;nbsp</p>
<ul>
<li>네이티브 SQL을 통한 조회
Spring Data JDBC에서와 마찬가지로 JPA 역시 네이티브 SQL 쿼리를 작성해서 사용할 수 있습니다.</li>
</ul>
<p>(2-2)의 nativeQuery 애트리뷰트의 값을 ‘true’로 설정하면 value 애트리뷰트에 작성한 SQL 쿼리가 적용됩니다.</p>
<blockquote>
<p>Spring Data JDBC의 @Query 애너테이션 패키지 경로
import org.springframework.data.jdbc.repository.query.Query</p>
</blockquote>
<p>Spring Data JPA의 @Query 애너테이션 패키지 경로
org.springframework.data.jpa.repository.Query</p>
<p>&amp;nbsp
&amp;nbsp
<strong>3. 서비스 클래스 구현</strong></p>
<pre><code>@Service
public class MemberService {
    private final MemberRepository memberRepository;

    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

    public Member createMember(Member member) {
        verifyExistsEmail(member.getEmail());

        return memberRepository.save(member);
    }

    public Member updateMember(Member member) {
        Member findMember = findVerifiedMember(member.getMemberId());

        Optional.ofNullable(member.getName())
                .ifPresent(name -&gt; findMember.setName(name));
        Optional.ofNullable(member.getPhone())
                .ifPresent(phone -&gt; findMember.setPhone(phone));
        // (1) 추가된 부분
        Optional.ofNullable(member.getMemberStatus())
                .ifPresent(memberStatus -&gt; findMember.setMemberStatus(memberStatus));

        // (2) 추가된 부분
        findMember.setModifiedAt(LocalDateTime.now());

        return memberRepository.save(findMember);
    }

    public Member findMember(long memberId) {
        return findVerifiedMember(memberId);
    }

    public Page&lt;Member&gt; findMembers(int page, int size) {
        return memberRepository.findAll(PageRequest.of(page, size,
                Sort.by(&quot;memberId&quot;).descending()));
    }

    public void deleteMember(long memberId) {
        Member findMember = findVerifiedMember(memberId);

        memberRepository.delete(findMember);
    }

    public Member findVerifiedMember(long memberId) {
        Optional&lt;Member&gt; optionalMember =
                memberRepository.findById(memberId);
        Member findMember =
                optionalMember.orElseThrow(() -&gt;
                        new BusinessLogicException(ExceptionCode.MEMBER_NOT_FOUND));
        return findMember;
    }

    private void verifyExistsEmail(String email) {
        Optional&lt;Member&gt; member = memberRepository.findByEmail(email);
        if (member.isPresent())
            throw new BusinessLogicException(ExceptionCode.MEMBER_EXISTS);
    }
}</code></pre><ul>
<li>액세스 기술을 Spring Data JDBC에서 Spring Data JPA로 바꿨다고 해서 실제로 코드 자체가 대폭 변경된 부분은 없습니다.
즉, 애플리케이션이 특정 기술에 강하게 결합되지 않도록 Spring이 추구하는 PSA(일관된 서비스 추상화)를 통해 개발자는 일관된 코드 구현 방식을 유지하도록 하고, 기술의 변경이 필요할 때 최소한의 변경만을 하도록 지원한다는 의미입니다.</li>
</ul>
<p>&amp;nbsp
&amp;nbsp
<strong>4. 기타 기능 추가로 인해 수정 및 추가되는 코드</strong></p>
<pre><code>@Getter
public class MemberPatchDto {
    private long memberId;

    @NotSpace(message = &quot;회원 이름은 공백이 아니어야 합니다&quot;)
    private String name;

    @NotSpace(message = &quot;휴대폰 번호는 공백이 아니어야 합니다&quot;)
    @Pattern(regexp = &quot;^010-\\d{3,4}-\\d{4}$&quot;,
          message = &quot;휴대폰 번호는 010으로 시작하는 11자리 숫자와 &#39;-&#39;로 구성되어야 합니다&quot;)
    private String phone;

    // 회원 상태 값을 사전에 체크하는 Custom Validator를 만들수도 있다.
    private Member.MemberStatus memberStatus;

    public void setMemberId(long memberId) {
        this.memberId = memberId;
    }
}</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[JPA 기반 데이터 액세스 계층(3) - 엔티티 간의 연관 관계 매핑]]></title>
            <link>https://velog.io/@be_chobo/JPA-%EA%B8%B0%EB%B0%98-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%95%A1%EC%84%B8%EC%8A%A4-%EA%B3%84%EC%B8%B53-%EC%97%94%ED%8B%B0%ED%8B%B0-%EA%B0%84%EC%9D%98-%EC%97%B0%EA%B4%80-%EA%B4%80%EA%B3%84-%EB%A7%A4%ED%95%91</link>
            <guid>https://velog.io/@be_chobo/JPA-%EA%B8%B0%EB%B0%98-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%95%A1%EC%84%B8%EC%8A%A4-%EA%B3%84%EC%B8%B53-%EC%97%94%ED%8B%B0%ED%8B%B0-%EA%B0%84%EC%9D%98-%EC%97%B0%EA%B4%80-%EA%B4%80%EA%B3%84-%EB%A7%A4%ED%95%91</guid>
            <pubDate>Mon, 18 Sep 2023 08:48:20 GMT</pubDate>
            <description><![CDATA[<h3 id="⚫단방향-연관-관계">⚫단방향 연관 관계</h3>
<p>예를 들어 Member 클래스와 Order 클래스 관계에서 Member에만 Order 객체를 원소로 포함하는 List 객체를 가지고 있으면, Order를 참조할 수 있습니다.</p>
<p>하지만 이 경우, Order 클래스는 Member 클래스에 대한 참조 값이 없으므로 Order 입장에서는 Member 정보를 알 수 없습니다.</p>
<p>이처럼 한쪽 클래스만 다른 쪽 클래스의 참조 정보를 가지고 있는 관계를 <strong>단방향 연관 관계</strong>라고 합니다.</p>
<p>&amp;nbsp
&amp;nbsp</p>
<h3 id="⚫양방향-연관-관계">⚫양방향 연관 관계</h3>
<p>이번에는 위의 경우와는 다르게</p>
<p>Member 클래스가 Order 객체를 원소로 포함하고 있는 List 객체를 가지고 있고, 그러므로 Order 클래스를 참조할 수 있고, Member는 Order의 정보를 알 수 있습니다.</p>
<p>그리고 Order 클래스 역시 Member 객체를 가지고 있으므로, Member 클래스를 참조할 수 있습니다.</p>
<p>결론적으로 두 클래스가 모두 서로의 객체를 참조할 수 있으므로, Member는 Order 정보를 알 수 있고, Order는 Member 정보를 알 수 있습니다.</p>
<p>이처럼 양쪽 클래스가 서로의 참조 정보를 가지고 있는 관계를 <strong>양방향 연관 관계</strong>라고 합니다.</p>
<blockquote>
<p>JPA는 단방향 연관 관계와 양방향 연관 관계를 모두 지원하는 반면에 Spring Data JDBC는 단방향 연관 관계만 지원합니다.</p>
</blockquote>
<p>&amp;nbsp
&amp;nbsp</p>
<h3 id="⚫일대다-단방향-연관-관계">⚫일대다 단방향 연관 관계</h3>
<p>일대다의 관계란 일(1)에 해당하는 클래스가 다(N)에 해당하는 객체를 참조할 수 있는 관계를 의미합니다.</p>
<p>(일단, 일대다 단방향 매핑은 잘 사용하지 않습니다.)</p>
<p> Member와 Order는 일대다 관계이며, 단방향 관계라는 가정을 하겠습니다.</p>
<p>테이블 간의 관계에서는 일대다 중에서 ‘다’에 해당하는 테이블에서 ‘일’에 해당하는 테이블의 기본키를 외래키로 가집니다.</p>
<p>따라서 ORDERS 테이블이 MEMBER 테이블의 기본키인 member_id를 외래키로 가집니다.</p>
<p>그런데 Order 클래스가 ‘테이블 관계에서 외래키에 해당하는 MEMBER 클래스의 참조값’을 가지고 있지 않기 때문에 일반적인 테이블 간의 관계를 정상적으로 표현하지 못하고 있습니다.</p>
<p>따라서, Order 클래스의 정보를 테이블에 저장하더라도 외래키에 해당하는 MEMBER 클래스의 memberId 값이 없는 채로 저장이 됩니다.</p>
<p>이러한 문제 때문에 일대다 단방향 매핑은 잘 사용하지 않습니다.</p>
<blockquote>
<p>일대다 단방향 매핑 하나만 사용하는 경우는 드물고, 
다대일 단방향 매핑을 먼저 한 후에 필요한 경우, 일대다 단방향 매핑을 추가해서 양방향 연관 관계를 만드는 것이 일반적입니다.</p>
</blockquote>
<p>&amp;nbsp
&amp;nbsp</p>
<h3 id="⚫다대일-연관-관계">⚫다대일 연관 관계</h3>
<p>다대일의 관계란 다(N)에 해당하는 클래스가 일(1)에 해당하는 객체를 참조할 수 있는 관계를 의미합니다.</p>
<p>다대일 단방향 매핑은 ORDERS 테이블이 MEMBER 테이블의 member_id를 외래키로 가지듯이 Order 클래스가 Member 객체를 외래키처럼 가지고 있습니다.</p>
<p>즉, 다대일 단방향 매핑은 테이블 간의 관계처럼 자연스러운 매핑 방식이기 때문에 JPA의 엔티티 연관 관계 중에서 가장 기본으로 사용되는 매핑 방식입니다.</p>
<pre><code>@NoArgsConstructor
@Getter
@Setter
@Entity(name = &quot;ORDERS&quot;)
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long orderId;

    @Enumerated(EnumType.STRING)
    private OrderStatus orderStatus = OrderStatus.ORDER_REQUEST;

    @Column(nullable = false)
    private LocalDateTime createdAt = LocalDateTime.now();

    @Column(nullable = false, name = &quot;LAST_MODIFIED_AT&quot;)
    private LocalDateTime modifiedAt = LocalDateTime.now();


    @ManyToOne   // (1)
    @JoinColumn(name = &quot;MEMBER_ID&quot;)  // (2)
    private Member member;
             .
             .
             .
</code></pre><p>다대일의 연관관계 매핑에서는</p>
<p>먼저 (1)과 같이 @ManyToOne 애너테이션으로 다대일의 관계를 명시합니다.</p>
<p>그리고 (2)와 같이 @JoinColumn 애너테이션으로 ORDERS 테이블에서 외래키에 해당하는 열 이름을 적어줍니다.</p>
<p>다대일 단방향 연관 관계이라면 다(N) 쪽에서만 설정을 해주면 매핑 작업은 끝납니다.</p>
<p>&amp;nbsp</p>
<h4 id="다대일-매핑을-이용한-회원과-주문-정보-저장">다대일 매핑을 이용한 회원과 주문 정보 저장</h4>
<pre><code>@Configuration
public class JpaManyToOneUniDirectionConfig {
    private EntityManager em;
    private EntityTransaction tx;

    @Bean
    public CommandLineRunner testJpaManyToOneRunner(EntityManagerFactory emFactory) {
        this.em = emFactory.createEntityManager();
        this.tx = em.getTransaction();

        return args -&gt; {
            mappingManyToOneUniDirection();
        };
    }

    private void mappingManyToOneUniDirection() {
        tx.begin();
        Member member = new Member(&quot;hgd@gmail.com&quot;, &quot;Hong Gil Dong&quot;,
                &quot;010-1111-1111&quot;);

                // (1)
        em.persist(member);

        Order order = new Order();
        order.addMember(member);     // (2)
        em.persist(order);           // (3)

        tx.commit();

                // (4)
        Order findOrder = em.find(Order.class, 1L);

        // (5) 주문에 해당하는 회원 정보를 가져올 수 있다.
        System.out.println(&quot;findOrder: &quot; + findOrder.getMember().getMemberId() +
                        &quot;, &quot; + findOrder.getMember().getEmail());
    }
}</code></pre><p>우리가 ORDER 테이블에 주문 정보를 저장하는 INSERT 쿼리문에는 MEMBER 테이블의 MEMBER_ID가 외래키로 포함이 될 것입니다.
(2)와 같이 추가되는 member 객체는 이 MEMBER_ID 같은 외래키의 역할을 한다고 생각하면 됩니다.</p>
<p>(5)에서 findOrder.getMember().getMemberId()와 같이 객체를 통해 다른 객체의 정보를 얻을 수 있는 것을 객체 그래프 탐색이라고 합니다.</p>
<p>&amp;nbsp</p>
<h4 id="🟤다대일-매핑에-일대다-매핑-추가">🟤다대일 매핑에 일대다 매핑 추가</h4>
<p>카페 주인 입장에서는 이 주문을 누가 했는지 주문한 회원의 회원 정보를 알아야 할 경우에는 다대일 매핑을 통해 주문한 사람의 정보를 조회할 수 있습니다.</p>
<p>그런데 회원 입장에서는 내가 주문한 주문의 목록을 확인할 수 있어야 할 텐데 다대일 매핑만으로는 member 객체를 통해 내가 주문한 주문 정보인 order 객체들을 조회할 수 없습니다.</p>
<p>이 경우, 다대일 매핑이 되어 있는 상태에서 일대다 매핑을 추가해 <strong>양방향 관계</strong>를 만들어주면 됩니다.</p>
<pre><code>@NoArgsConstructor
@Getter
@Setter
@Entity
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long memberId;

    @Column(nullable = false, updatable = false, unique = true)
    private String email;

    @Column(length = 100, nullable = false)
    private String name;

    @Column(length = 13, nullable = false, unique = true)
    private String phone;

    @Column(nullable = false)
    private LocalDateTime createdAt = LocalDateTime.now();

    @Column(nullable = false, name = &quot;LAST_MODIFIED_AT&quot;)
    private LocalDateTime modifiedAt = LocalDateTime.now();

        // (1)
    @OneToMany(mappedBy = &quot;member&quot;)
    private List&lt;Order&gt; orders = new ArrayList&lt;&gt;();

    public Member(String email) {
        this.email = email;
    }

    public Member(String email, String name, String phone) {
        this.email = email;
        this.name = name;
        this.phone = phone;
    }

    public void addOrder(Order order) {
        orders.add(order);
    }
}</code></pre><blockquote>
<p>일대다 단방향 매핑의 경우에는 mappedBy 애트리뷰트의 값이 필요하지 않습니다.
mappedBy는 참조할 대상이 있어야 하는데 일대다 단방향 매핑의 경우 참조할 대상이 없으니까요.</p>
</blockquote>
<p>mappedBy의 값은 관계를 소유하고 있는 필드를 지정하는 것으로 이해할 수 있습니다.</p>
<p>MEMBER 테이블과 ORDER 테이블의 관계에서 ORDER 테이블의 외래키로 MEMBER 테이블의 기본키 열인 MEMBER_ID의 값을 지정합니다.</p>
<p>그렇다면 Order 클래스에서 외래키의 역할을 하는 필드는 
바로 member 필드입니다.</p>
<p>그렇기 때문에 mappedBy의 값이 “member”가 되는 것입니다.</p>
<blockquote>
<p>mappedBy의 값으로 무얼 지정해야 하나?
(1) 두 객체들 간에 외래키의 역할을 하는 필드는 무엇인가?
(2) 외래키의 역할을 하는 필드는 다(N)에 해당하는 클래스 안에 있다.</p>
</blockquote>
<p>&amp;nbsp
&amp;nbsp</p>
<h3 id="⚫다대다-연관-관계">⚫다대다 연관 관계</h3>
<p>주문(Order)과 커피(Coffee)의 관계는 다대다 관계입니다.</p>
<p>하나의 주문에 여러 개의 커피가 속할 수 있고, 하나의 커피는 여러 주문에 속할 수 있으니 다대다 관계인 것입니다.</p>
<p>JPA에서 다대다에 해당하는 엔티티 클래스는 테이블 설계 시, 중간에 테이블을 하나 추가해서 두 개의 일대다 관계를 만들어주는 것이 일반적인 방법입니다.</p>
<p>&amp;nbsp
&amp;nbsp</p>
<h3 id="⚫일대일-연관관계">⚫일대일 연관관계</h3>
<p>일대일 연관 관계 매핑은 다대일 단방향 연관 관계 매핑과 매핑 방법은 동일합니다.</p>
<p>단지 @ManyToOne 애너테이션이 아닌 @OneToOne 애너테이션을 사용한다는 차이만 있습니다.</p>
<p>일대일 단방향 매핑에 양방향 매핑을 추가하는 방법도 다대일에 일대다 매핑을 추가하는 방식과 동일합니다.</p>
<p>단, @OneToOne 애너테이션을 사용합니다.</p>
<p>&amp;nbsp
&amp;nbsp</p>
<h3 id="👍엔티티-간의-연관-관계-매핑-권장-방법">👍엔티티 간의 연관 관계 매핑 권장 방법</h3>
<ul>
<li>일대다 매핑은 사용하지 않습니다.</li>
<li>제일 먼저 다대일 단방향 매핑부터 적용합니다.</li>
<li>다대일 단방향 매핑을 통해 객체 그래프 탐색으로 조회할 수 없는 정보가 있을 경우, 그때 비로소 양방향 매핑을 적용합니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[JPA 기반 데이터 액세스 계층(2) - 엔티티 매핑]]></title>
            <link>https://velog.io/@be_chobo/JPA-%EA%B8%B0%EB%B0%98-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%95%A1%EC%84%B8%EC%8A%A4-%EA%B3%84%EC%B8%B52-%EC%97%94%ED%8B%B0%ED%8B%B0-%EB%A7%A4%ED%95%91</link>
            <guid>https://velog.io/@be_chobo/JPA-%EA%B8%B0%EB%B0%98-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%95%A1%EC%84%B8%EC%8A%A4-%EA%B3%84%EC%B8%B52-%EC%97%94%ED%8B%B0%ED%8B%B0-%EB%A7%A4%ED%95%91</guid>
            <pubDate>Fri, 15 Sep 2023 08:35:39 GMT</pubDate>
            <description><![CDATA[<h3 id="엔티티와-테이블-간의-매핑">엔티티와 테이블 간의 매핑</h3>
<pre><code>@Entity
@Table
public class Member {
    @Id
    private Long memberId;
}</code></pre><p>@Entity 매핑 애너테이션을 이용해 엔티티 클래스와 테이블을 매핑합니다.
@Entity 애너테이션을 붙이면 JPA 관리 대상 엔티티가 됩니다.</p>
<pre><code>@Entity(name = &quot;USERS&quot;) // (1)
@Table(name = &quot;USERS&quot;) // (2)
public class Member {
    @Id
    private Long memberId;
}</code></pre><p>@Entity attribute</p>
<ul>
<li>name<ul>
<li>엔티티 이름을 설정할 수 있습니다.</li>
</ul>
</li>
</ul>
<p>@Table attribute</p>
<ul>
<li>name<ul>
<li>테이블 이름을 설정할 수 있습니다.</li>
<li>@Table 애너테이션은 옵션이며, 추가하지 않을 경우 클래스 이름을 테이블 이름으로 사용합니다.</li>
<li>주로 테이블 이름이 클래스 이름과 달라야 할 경우에 추가합니다.</li>
</ul>
</li>
</ul>
<p>&amp;nbsp
** 추가적으로 
@Table 애너테이션은 옵션이지만 @Entity 애너테이션과 @Id 애너테이션은 필수입니다.**</p>
<p><strong>파라미터가 없는 기본 생성자는 필수로 추가합니다.
Spring Data JPA의 기술을 적용할 때 기본 생성자가 없는 경우 에러가 발생하는 경우가 있기 때문에 기본 생성자는 습관적으로 추가하는 것이 좋습니다.</strong></p>
<p>&amp;nbsp
&amp;nbsp</p>
<h3 id="기본키-매핑">기본키 매핑</h3>
<p>데이터베이스의 테이블에 기본키 설정은 필수라고 할 수 있습니다.</p>
<p><strong>JPA에서 지원하는 기본키 생성 전략</strong></p>
<ul>
<li><p>기본키 직접 할당
애플리케이션 코드 상에서 기본키를 직접 할당해주는 방식입니다.</p>
</li>
<li><p>기본키 자동 생성</p>
<ul>
<li>IDENTITY<ul>
<li>기본키 생성을 데이터베이스에 위임하는 전략입니다.</li>
<li>데이터베이스에서 기본키를 생성해 주는 대표적인 방식은 MySQL의 AUTO_INCREMENT 기능을 통해 자동 증가 숫자를 기본키로 사용하는 방식이 있습니다.  </li>
</ul>
</li>
<li>SEQUENCE<ul>
<li>데이터베이스에서 제공하는 시퀀스를 사용해서 기본키를 생성하는 전략입니다.  </li>
</ul>
</li>
<li>TABLE<ul>
<li>별도의 키 생성 테이블을 사용하는 전략입니다.</li>
</ul>
</li>
</ul>
</li>
</ul>
<p>즉, 해당 방식들은 사용하는 DB에 의존합니다.
ex)MySQL은 IDENTITY 사용, Oracle은 SEQUENCE 사용</p>
<p>&amp;nbsp<br><strong>1. IDENTITY 전략</strong></p>
<pre><code>@NoArgsConstructor
@Getter
@Entity
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY) // (1)
    private Long memberId;

    public Member(Long memberId) {
        this.memberId = memberId;
    }
}</code></pre><p>IDENTITY 기본키 생성 전략을 설정하려면 위와 같이 @GeneratedValue 애너테이션의 strategy 애트리뷰트의 값을 GenerationType.IDENTITY로 지정해 주면 됩니다.</p>
<p>IDENTITY 전략은 데이터베이스에서 기본키를 자동으로 대신 생성해 줍니다.</p>
<p>&amp;nbsp
<strong>2. SEQUENCE 전략</strong></p>
<pre><code>@NoArgsConstructor
@Getter
@Entity
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)  // (1)
    private Long memberId;

    public Member(Long memberId) {
        this.memberId = memberId;
    }
}</code></pre><p>SEQUENCE 전략을 사용하기 위해서는 @GeneratedValue(strategy = GenerationType.SEQUENCE)를 지정하면 됩니다.</p>
<p>엔티티가 영속성 컨텍스트에 저장되기 전에 데이터베이스가 시퀀스에서 기본키에 해당하는 값을 제공할 것입니다.</p>
<p>&amp;nbsp
<strong>3. AUTO 전략</strong>
 @Id 필드에 @GeneratedValue(strategy = GenerationType.AUTO)를 지정하면 JPA가 데이터베이스의 Dialect에 따라서 적절한 전략을 자동으로 선택합니다.</p>
<p>(Dialect는 표준 SQL 등이 아닌 특정 데이터베이스에 특화된 고유한 기능을 의미합니다.
만일 JPA가 지원하는 표준 문법이 아닌 특정 데이터베이스에 특화된 기능을 사용할 경우 Dialect가 처리해 줍니다.)</p>
<p>&amp;nbsp
&amp;nbsp</p>
<h3 id="필드멤버-변수와-열-간의-매핑">필드(멤버 변수)와 열 간의 매핑</h3>
<pre><code>@NoArgsConstructor
@Getter
@Entity
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long memberId;

        // (1)
    @Column(nullable = false, updatable = false, unique = true)
    private String email;

        ...
        ...

    public Member(String email) {
        this.email = email;
    }
}</code></pre><p>@Column 애너테이션은 필드와 열을 매핑해 주는 애너테이션입니다. 그런데 만약 @Column 애너테이션이 없고, 필드만 정의되어 있다면 JPA는 기본적으로 이 필드가 테이블의 열과 매핑되는 필드라고 간주하게 됩니다. 또한, @Column 애너테이션에 사용되는 애트리뷰트의 값은 디폴트 값이 모두 적용됩니다.</p>
<p>@Column attribute</p>
<ul>
<li>nullable<ul>
<li>열에 null 값을 허용할지 여부를 지정합니다.</li>
<li>디폴트 값은 true입니다.</li>
<li>email 주소는 일반적으로 회원 정보에서 ID로 많이 사용되며, 따라서 필수 항목이기 때문에 nullable 값을 false로 지정합니다.</li>
</ul>
</li>
<li>updatable<ul>
<li>열 데이터를 수정할 수 있는지 여부를 지정합니다.</li>
<li>디폴트 값은 true입니다.</li>
<li>여기서는 email 주소가 사용자 ID 역할을 한다고 가정하고 한번 등록되면 수정이 불가능하도록 하기 위해서 updatable 값을 false로 지정했습니다.</li>
</ul>
</li>
<li>unique<ul>
<li>하나의 열에 unique 유니크 제약 조건을 설정합니다.</li>
<li>디폴트 값은 false입니다.</li>
<li>email의 경우 고유한 값이어야 하므로 unique 값을 true로 지정했습니다.</li>
</ul>
</li>
</ul>
<p>&amp;nbsp
<strong>@Column 애너테이션이 생략되었거나 애트리뷰트가 기본값을 사용할 경우 주의 사항</strong>
int나 long 같은 원시 타입일 경우, @Column 애너테이션이 생략되면 기본적으로 nullable=false입니다.</p>
<p>그러나 nullable에 대한 명시적인 설정 없이 단순히 @Column 애너테이션만 추가하면 nullable=true가 기본값이 되기 때문에 테이블에는 int price not null로 열이 설정되는 것이 아니라 int price와 같이 설정이 될 것입니다.</p>
<p>따라서 개발자가 의도하는 바가 int price not null일 경우에는 @Column(nullable=false)라고 명시적으로 지정하든가 아예 @Column 애너테이션 자체를 사용하지 않는 것이 권장됩니다.</p>
<h4 id="엔티티-클래스에서-발생한-예외-처리">엔티티 클래스에서 발생한 예외 처리</h4>
<p>계층은 다르지만 엔티티 클래스 필드의 설정으로 인해 발생한 예외는 API 계층의 DTO 클래스에서 발생한 예외와 마찬가지로 일종의 유효성(Validation) 검증이라고 볼 수 있습니다.</p>
<p>엔티티 클래스에서 발생한 예외는 API 계층까지 전파되므로 API 계층의 GlobalExceptionAdvice에서 캐치(catch) 한 후, 처리할 수 있습니다.</p>
<pre><code>@NoArgsConstructor
@Getter
@Setter
@Entity
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long memberId;

    @Column(nullable = false, updatable = false, unique = true)
    private String email;

        // (1)
    @Column(length = 100, nullable = false)
    private String name;

    @Column(length = 13, nullable = false, unique = true)
    private String phone;

    @Column(nullable = false)
    private LocalDateTime createdAt = LocalDateTime.now();   // (2)

    // (3)
    @Column(nullable = false, name = &quot;LAST_MODIFIED_AT&quot;)
    private LocalDateTime modifiedAt = LocalDateTime.now();

    // (4)
    @Transient
    private String age;

        public Member(String email) {
        this.email = email;
    }

      public Member(String email, String name, String phone) {
        this.email = email;
        this.name = name;
        this.phone = phone;
    }
}</code></pre><p>@Column 애너테이션의 추가 attribute</p>
<ul>
<li>length<ul>
<li>열에 저장할 수 있는 문자 길이를 지정할 수 있습니다.</li>
<li>디폴트 값은 255입니다.</li>
</ul>
</li>
<li>name<ul>
<li>name 애트리뷰트를 생략하면 엔티티 클래스 필드의 이름으로 열이 생성되지만 (3)과 같이 name 애트리뷰트에 별도의 이름을 지정해서 엔티티 클래스 필드명과 다른 이름으로 열을 생성할 수 있습니다.</li>
</ul>
</li>
</ul>
<p>&amp;nbsp
(2)는 회원 정보가 등록될 때의 시간 및 날짜를 매핑하기 위한 필드입니다.</p>
<ul>
<li>java.util.Date, java.util.Calendar 타입으로 매핑하기 위해서는 @Temporal 애너테이션을 추가해야 LocalDateTime 타입일 경우, @Temporal 애너테이션은 생략 가능합니다.</li>
<li>LocalDateTime은 열의 TIMESTAMP 타입과 매핑됩니다.</li>
<li>회원 정보가 등록되는 시간 정보를 필드에 전달하기 위해 createdAt 필드에 LocalDateTime.now() 메서드로 현재 시간을 입력하고 있습니다.</li>
</ul>
<p>&amp;nbsp
(4)와 같이 @Transient 애너테이션을 필드에 추가하면 테이블 열과 매핑하지 않겠다는 의미로 JPA가 인식합니다.</p>
<ul>
<li>따라서 데이터베이스에 저장도 하지 않고, 조회할 때 역시 매핑되지 않습니다.</li>
<li>@Transient은 주로 임시 데이터를 메모리에서 사용하기 위한 용도로 사용됩니다.</li>
</ul>
<p>&amp;nbsp</p>
<pre><code>@NoArgsConstructor
@Getter
@Setter
@Entity(name = &quot;ORDERS&quot;)
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long orderId;

    // (1)
    @Enumerated(EnumType.STRING)
    private OrderStatus orderStatus = OrderStatus.ORDER_REQUEST;

    @Column(nullable = false)
    private LocalDateTime createdAt = LocalDateTime.now();

    @Column(nullable = false, name = &quot;LAST_MODIFIED_AT&quot;)
    private LocalDateTime modifiedAt = LocalDateTime.now();

    public enum OrderStatus {
        ORDER_REQUEST(1, &quot;주문 요청&quot;),
        ORDER_CONFIRM(2, &quot;주문 확정&quot;),
        ORDER_COMPLETE(3, &quot;주문 완료&quot;),
        ORDER_CANCEL(4, &quot;주문 취소&quot;);

        @Getter
        private int stepNumber;

        @Getter
        private String stepDescription;

        OrderStatus(int stepNumber, String stepDescription) {
            this.stepNumber = stepNumber;
            this.stepDescription = stepDescription;
        }
    }
}</code></pre><p>@Enumerated 애너테이션</p>
<ul>
<li>enum 타입과 매핑할 때 사용하는 애너테이션입니다</li>
<li>EnumType.ORDINAL : enum의 순서를 나타내는 숫자를 테이블에 저장합니다.</li>
<li>EnumType.STRING : enum의 이름을 테이블에 저장합니다.</li>
</ul>
<p>주의할것은
EnumType.ORDINAL로 지정할 경우, 기존에 정의되어 있는 enum 사이에 새로운 enum 하나가 추가된다면 그때부터 테이블에 이미 저장되어 있는 enum 순서 번호와 enum에 정의되어 있는 순서가 일치하지 않게 되는 문제가 발생합니다.</p>
<p>따라서 처음부터 이런 문제가 발생하지 않도록 EnumType.STRING을 사용하는 것을 권장하고 있습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[JPA 기반 데이터 액세스 계층(1) - JPA란?]]></title>
            <link>https://velog.io/@be_chobo/JPA-%EA%B8%B0%EB%B0%98-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%95%A1%EC%84%B8%EC%8A%A4-%EA%B3%84%EC%B8%B51-JPA%EB%9E%80</link>
            <guid>https://velog.io/@be_chobo/JPA-%EA%B8%B0%EB%B0%98-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%95%A1%EC%84%B8%EC%8A%A4-%EA%B3%84%EC%B8%B51-JPA%EB%9E%80</guid>
            <pubDate>Fri, 15 Sep 2023 07:15:00 GMT</pubDate>
            <description><![CDATA[<h3 id="jpa란">JPA란</h3>
<p>JPA(Java Persistence API)는 Java 진영에서 사용하는 ORM(Object-Relational Mapping) 기술의 표준 사양(또는 명세, Specification)입니다.</p>
<p>표준 사양(또는 명세)이라는 의미는 다시 말하면 Java의 인터페이스로 사양이 정의되어 있기 때문에 JPA라는 표준 사양을 구현한 구현체는 따로 있다는 것을 의미합니다.</p>
<p>&amp;nbsp</p>
<h3 id="hibernate-orm">Hibernate ORM</h3>
<p>JPA 표준 사양을 구현한 구현체로는 Hibernate ORM, EclipseLink, DataNucleus 등이 있습니다.</p>
<p>Hibernate ORM은 JPA에서 정의해 둔 인터페이스를 구현한 구현체로써 JPA에서 지원하는 기능 이외에 Hibernate 자체적으로 사용할 수 있는 API 역시 지원하고 있습니다.</p>
<p>&amp;nbsp</p>
<h3 id="데이터-액세스-계층에서의-jpa">데이터 액세스 계층에서의 JPA</h3>
<p>데이터 액세스 계층에서 JPA는 데이터 액세스 계층의 상단에 위치합니다.</p>
<p>데이터 저장, 조회 등의 작업은 JPA를 거쳐 JPA의 구현체인 Hibernate ORM을 통해서 이루어지며 Hibernate ORM은 내부적으로 JDBC API를 이용해서 데이터베이스에 접근하게 됩니다.</p>
<p>&amp;nbsp</p>
<h3 id="영속성-컨텍스트persistence-context">영속성 컨텍스트(Persistence Context)</h3>
<p>ORM은 객체(Object)와 데이터베이스 테이블의 매핑을 통해 엔티티 클래스 객체 안에 포함된 정보를 테이블에 저장하는 기술입니다.</p>
<p>JPA에서는 테이블과 매핑되는 엔티티 객체 정보를 영속성 컨텍스트(Persistence Context)라는 곳에 보관해서 애플리케이션 내에서 오래 지속되도록 합니다.</p>
<p>그리고 이렇게 보관된 엔티티 정보는 데이터베이스 테이블에 데이터를 저장, 수정, 조회, 삭제하는 데 사용됩니다.</p>
<p>영속성 컨텍스트에는 1차 캐시라는 영역과 쓰기 지연 SQL 저장소라는 영역이 있습니다.</p>
<p>JPA API 중에서 엔티티 정보를 영속성 컨텍스트에 저장(persist)하는 API를 사용하면 영속성 컨텍스트의 1차 캐시에 엔티티 정보가 저장됩니다.</p>
<p>&amp;nbsp
&amp;nbsp</p>
<h3 id="jpa-api를-이용한-영속성-컨텍스트-이해">JPA API를 이용한 영속성 컨텍스트 이해</h3>
<h4 id="jpa-api-사용-하기-위한-사전-준비">JPA API 사용 하기 위한 사전 준비</h4>
<p><strong>1. build.gradle 설정</strong></p>
<pre><code>dependencies {

    implementation &#39;org.springframework.boot:spring-boot-starter-data-jpa&#39; 
    .
    .
}</code></pre><p>위와 같이 추가하면 기본적으로 Spring Data JPA 기술을 포함해서 JPA API를 사용할 수 있습니다.</p>
<p>&amp;nbsp
<strong>2. JPA설정(application.yml)</strong></p>
<pre><code>spring:
  h2:
    console:
      enabled: true
      path: /h2     
  datasource:
    url: jdbc:h2:mem:test
  jpa:
    hibernate:
      ddl-auto: create  # (1) 스키마 자동 생성
    show-sql: true      # (2) SQL 쿼리 출력</code></pre><p>(1)과 같이 설정을 추가해 주면 우리가 JPA에서 사용하는 엔티티 클래스를 정의하고 애플리케이션 실행 시, 이 엔티티와 매핑되는 테이블을 데이터베이스에 자동으로 생성해 줍니다.</p>
<p>즉, Spring Data JDBC에서는 schema.sql 파일을 이용해 테이블 생성을 위한 스키마를 직접 지정해 주어야 했지만 JPA에서는 (1)의 설정을 추가하면 JPA가 자동으로 데이터베이스에 테이블을 생성해 줍니다.</p>
<p>(2)와 같이 설정을 추가해 주면 JPA의 동작 과정을 이해하기 위해 JPA API를 통해서 실행되는 SQL 쿼리를 로그로 출력해 줍니다.</p>
<p>&amp;nbsp
<strong>3. 영속성 컨텍스트에 엔티티 저장</strong></p>
<pre><code>@Configuration
public class JpaBasicConfig {
    private EntityManager em;


    @Bean
    public CommandLineRunner testJpaBasicRunner(EntityManagerFactory emFactory) { // (1)
        this.em = emFactory.createEntityManager();  // (2)

        return args -&gt; {
            example01();
        };
    }

    private void example01() {
        Member member = new Member(&quot;hgd@gmail.com&quot;);

        // (3)
        em.persist(member);

                // (4)
        Member resultMember = em.find(Member.class, 1L);
        System.out.println(&quot;Id: &quot; + resultMember.getMemberId() + &quot;, email: &quot; + 
                resultMember.getEmail());
    }
}</code></pre><p>JPA의 영속성 컨텍스트는 EntityManager 클래스에 의해서 관리되는데 이 EntityManager 클래스의 객체는 (1)과 같이 EntityManagerFactory 객체를 Spring으로부터 DI 받을 수 있습니다.</p>
<p>(2)와 같이 EntityManagerFactory의 createEntityManager() 메서드를 이용해서 EntityManager 클래스의 객체를 얻을 수 있습니다.
이제 이 EntityManager 클래스의 객체를 통해서 JPA의 API 메서드를 사용할 수 있습니다.</p>
<p>(3)과 같이 persist(member) 메서드를 호출하면 영속성 컨텍스트에 member 객체의 정보들이 저장됩니다.</p>
<p>em.persist(member)를 호출하면 1차 캐시에 member 객체가 저장되고, 이 member 객체는 쓰기 지연 SQL 저장소에 INSERT 쿼리 형태로 등록이 됩니다.</p>
<p>그런데, em.persist(member)를 호출할 경우, 영속성 컨텍스트에 member 객체를 저장하지만 실제 테이블에 회원 정보를 저장하지는 않습니다.</p>
<p>&amp;nbsp
<strong>4. 영속성 컨텍스트와 테이블에 엔티티 저장</strong></p>
<pre><code>@Configuration
public class JpaBasicConfig {
    private EntityManager em;
    private EntityTransaction tx;

    @Bean
    public CommandLineRunner testJpaBasicRunner(EntityManagerFactory emFactory) {
        this.em = emFactory.createEntityManager();

        // (1)
        this.tx = em.getTransaction();

        return args -&gt; {
            example02();
        };
    }

    private void example02() {
        // (2)
        tx.begin();
        Member member = new Member(&quot;hgd@gmail.com&quot;);

        // (3)
        em.persist(member);

        // (4)
        tx.commit();

        // (5)
        Member resultMember1 = em.find(Member.class, 1L);

        System.out.println(&quot;Id: &quot; + resultMember1.getMemberId() + &quot;, email: &quot; + resultMember1.getEmail());

        // (6)
        Member resultMember2 = em.find(Member.class, 2L);

        // (7)
        System.out.println(resultMember2 == null);

    }
}</code></pre><p>(1)에서는 EntityManager를 통해서 Transaction 객체를 얻습니다. JPA에서는 이 Transaction 객체를 기준으로 데이터베이스의 테이블에 데이터를 저장합니다.</p>
<p>(4)와 같이 tx.commit()을 호출하는 시점에 영속성 컨텍스트에 저장되어 있는 member 객체를 데이터베이스의 테이블에 저장합니다.</p>
<p>(5)에서 em.find(Member.class, 1L)을 호출하면 (3)에서 영속성 컨텍스트에 저장한 member 객체를 1차 캐시에서 조회합니다.
1차 캐시에 member 객체 정보가 있기 때문에 별도로 테이블에 SELECT 쿼리를 전송하지 않습니다.</p>
<p>(6)에서는 영속성 컨텍스트에서 식별자 값이 2L인 member 객체가 존재하지 않기 때문에 테이블에 직접 SELECT 쿼리를 전송합니다</p>
<p>tx.commit()을 했기 때문에 member에 대한 INSERT 쿼리는 실행되어 쓰기 지연 SQL 저장소에서 사라집니다.</p>
<p>즉, </p>
<ul>
<li>em.persist()를 호출하면 영속성 컨텍스트의 1차 캐시에 엔티티 클래스의 객체가 저장되고, 쓰기 지연 SQL 저장소에 INSERT 쿼리가 등록된다.</li>
<li>tx.commit()을 하는 순간 쓰기 지연 SQL 저장소에 등록된 INSERT 쿼리가 실행되고, 실행된 INSERT 쿼리는 쓰기 지연 SQL 저장소에서 제거된다.</li>
<li>em.find()를 호출하면 먼저 1차 캐시에서 해당 객체가 있는지 조회하고, 없으면 테이블에 SELECT 쿼리를 전송해서 조회한다.</li>
</ul>
<p>&amp;nbsp
<strong>5. 영속성 컨텍스트와 테이블에 엔티티 업데이트</strong></p>
<pre><code>@Configuration
public class JpaBasicConfig {
    private EntityManager em;
    private EntityTransaction tx;

    @Bean
    public CommandLineRunner testJpaBasicRunner(EntityManagerFactory emFactory) {
        this.em = emFactory.createEntityManager();
        this.tx = em.getTransaction();

        return args -&gt; {
             example04();
        };
    }

    private void example04() {
       tx.begin();
       em.persist(new Member(&quot;hgd1@gmail.com&quot;));    // (1)
       tx.commit();    // (2)


       tx.begin();
       Member member1 = em.find(Member.class, 1L);  // (3)
       member1.setEmail(&quot;hgd1@yahoo.co.kr&quot;);       // (4)
       tx.commit();   // (5)
    }
}</code></pre><p>(3)과 같이 (2)에서 테이블에 저장된 member 객체를 영속성 컨텍스트의 1차 캐시에서 조회합니다.
테이블에서 조회하는 것이 아니고, 영속성 컨텍스트의 1차 캐시에 이미 저장된 객체가 있기 때문에 영속성 컨텍스트에서 조회합니다.</p>
<p>(4)에서 setter 메서드로 이메일 정보를 변경합니다.
여기서 중요한 사실은 em.update() 같은 JPA API가 있을 것 같지만 (4)와 같이 setter 메서드로 값을 변경하기만 하면 업데이트 로직은 완성이 됩니다.</p>
<p>tx.commit()을 실행하면 쓰기 지연 SQL 저장소에 등록된 UPDATE 쿼리가 실행이 됩니다.</p>
<blockquote>
<p><strong>UPDATE 쿼리가 실행이 되는 과정</strong>
-&gt; 영속성 컨텍스트에 엔티티가 저장될 경우에는 저장되는 시점의 상태를 그대로 가지고 있는 스냅샷을 생성합니다.
변경된 값이 있으면 쓰기 지연 SQL 저장소에 UPDATE 쿼리를 등록하고 UPDATE 쿼리를 실행합니다.</p>
</blockquote>
<p>&amp;nbsp
<strong>6. 영속성 컨텍스트와 테이블의 엔티티 삭제</strong></p>
<pre><code>@Configuration
public class JpaBasicConfig {
    private EntityManager em;
    private EntityTransaction tx;

    @Bean
    public CommandLineRunner testJpaBasicRunner(EntityManagerFactory emFactory) {
        this.em = emFactory.createEntityManager();
        this.tx = em.getTransaction();

        return args -&gt; {
            example05();
        };
    }

    private void example05() {
        tx.begin();
        em.persist(new Member(&quot;hgd1@gmail.com&quot;));  // (1)
        tx.commit();    //(2)

        tx.begin();
        Member member = em.find(Member.class, 1L);   // (3)
        em.remove(member);     // (4)
        tx.commit();     // (5)
    }
}</code></pre><p>(4)에서 em.remove(member)을 통해 영속성 컨텍스트의 1차 캐시에 있는 엔티티를 제거를 요청합니다.</p>
<p>(5)에서 tx.commit()을 실행하면 영속성 컨텍스트의 1차 캐시에 있는 엔티티를 제거하고, 쓰기 지연 SQL 저장소에 등록된 DELETE 쿼리가 실행이 됩니다.</p>
<p>&amp;nbsp
(참고)
-EntityManager의 flush() API</p>
<p>tx.commit() 메서드가 호출되면 JPA 내부적으로 em.flush() 메서드가 호출되어 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[JDBC 기반 데이터 액세스 계층(4) - Spring Data JDBC에서의 엔티티와 레포지토리]]></title>
            <link>https://velog.io/@be_chobo/jdbc</link>
            <guid>https://velog.io/@be_chobo/jdbc</guid>
            <pubDate>Mon, 04 Sep 2023 07:58:24 GMT</pubDate>
            <description><![CDATA[<p>Java에서 테이블의 외래키(Foreign key)를 표현하는 일반적인 방법: 클래스의 객체 참조 리스트(List)</p>
<p>테이블 간의 관계는 외래키라는 연결 요소가 있어서 직관적입니다.
그런데 클래스들 간에는 외래키라는 연결 요소가 없습니다. 대신에 클래스들은 객체 간에 참조가 가능하기 때문에 이 객체 참조를 사용해서 외래키의 기능을 대신합니다</p>
<h3 id="spring-data-jdbc에서의-애그리거트aggregate-객체-매핑">Spring Data JDBC에서의 애그리거트(Aggregate) 객체 매핑</h3>
<h4 id="애그리거트-객체-매핑-규칙">애그리거트 객체 매핑 규칙</h4>
<p>  (1) 모든 엔티티 객체의 상태는 애그리거트 루트를 통해서만 변경할 수 있다.</p>
<p>  Ex) 배달음식의 주소 변경시 음식이 완성이 되서 이미 배달중인 경우의 문제</p>
<p>&amp;nbsp
(2) 하나의 동일한 애그리거트 내에서의 엔티티 객체 참조</p>
<ul>
<li>동일한 하나의 애그리거트 내에서는 엔티티 간에 객체로 참조한다.</li>
</ul>
<p>&amp;nbsp
(3) 애그리거트 루트 대 애그리거트 루트 간의 엔티티 객체 참조</p>
<ul>
<li>애그리거트 루트 간의 참조는 객체 참조 대신에 ID로 참조한다.</li>
</ul>
<p>Ex) order 엔티티에서  long memberId로 외래키를 표현할수도 있지만,
Spring Data JDBC에서는 AggregateReference 라는 클래스를 이용해 private AggregateReference&lt;Member, Long&gt; memberId; 와 같이 외래키를 표현할 수도 있습니다.</p>
<ul>
<li><p>1대1 또는 1대N 관계일 때 테이블 간의 외래키 방식과 동일하다.</p>
</li>
<li><p>N대 N 관계일 때는 외래키 방식인 ID 참조와 객체 참조 방식이 함께 사용된다.</p>
<pre><code>@Getter
@Setter
@Table(&quot;ORDERS&quot;)
public class Order {
  @Id
  private long orderId;

  // 테이블 외래키처럼 memberId를 추가한다.
  private long memberId;

  // (1)
  @MappedCollection(idColumn = &quot;ORDER_ID&quot;)
  private Set&lt;OrderCoffee&gt; orderCoffees = new LinkedHashSet&lt;&gt;();

      ...
      ...
}</code></pre><p>@MappedCollection(idColumn = &quot;ORDER_ID&quot;, keyColumn = &quot;ORDER_COFFEE_ID&quot;) 은 엔티티 클래스 간에 연관 관계를 맺어주는 정보를 의미합니다.</p>
</li>
</ul>
<p><strong>idColumn</strong> 애트리뷰트는 자식 테이블에 추가되는 외래키에 해당되는 열명을 지정합니다.</p>
<p>ORDERS 테이블의 자식 테이블은 ORDER_COFFEE 테이블이고 이 ORDER_COFFEE 테이블은 ORDERS 테이블의 기본키인 ORDER_ID 열의 값을 외래키로 가집니다.</p>
<p><strong>keyColumn</strong> 애트리뷰트는 외래키를 포함하고 있는 테이블의 기본키 열명을 지정합니다.</p>
<p>ORDERS 테이블의 자식 테이블인 ORDER_COFFEE 테이블의 기본키는 ORDER_COFFEE_ID 이므로, keyColumn의 값이 ‘ORDER_COFFEE_ID’인 것입니다</p>
<blockquote>
<p>point!</p>
</blockquote>
<ol>
<li>N대 N의 관계를 —&gt; 1대 N, N대 1의 관계로 변경</li>
<li>1대 N, N대 1의 관계를 OrderCoffee 같이 중간에서 ID를 참조하게 해주는 클래스를 통해 다시 1대 N대1의 관계로 변경</li>
</ol>
<p>&amp;nbsp
&amp;nbsp</p>
<h3 id="리포지토리repository-인터페이스-정의">리포지토리(Repository) 인터페이스 정의</h3>
<pre><code>public interface MemberRepository extends CrudRepository&lt;Member, Long&gt; {

      Optional&lt;Member&gt; findByEmail(String email);
}</code></pre><p>Spring Data JDBC에서는 CrudRepository라는 인터페이스를 제공해주고 있으며, 이 CrudRepository의 기능을 사용하기 위해서 MemberRepository가 CrudRepository를 상속하고 있습니다.</p>
<p>우리는 이 CrudRepository 인터페이스를 통해서 편리하게 데이터를 데이터베이스의 테이블에 저장, 조회, 수정, 삭제할 수 있습니다.</p>
<p>CrudRepository&lt;Member, Long&gt;에서 Member는 Member 엔티티 클래스를 가리키며, Long은 Member 엔티티 클래스에서 @Id 애너테이션이 붙은 멤버 변수의 타입을 가리킵니다.</p>
<p>Spring Data JDBC에서는 ‘find + By + SQL 쿼리문에서 WHERE 절의 열명 + (WHERE 절 열의 조건이 되는 데이터) ’ 형식으로 쿼리 메서드(Query Method)를 정의하면 조건에 맞는 데이터를 테이블에서 조회합니다.</p>
<p>정의한 쿼리 메서드(Query Method)는 내부적으로 아래의 SQL 쿼리문으로 변환되어 데이터베이스의 MEMBER 테이블에 질의를 보냅니다.</p>
<p>SELECT &quot;MEMBER&quot;.&quot;NAME&quot; AS &quot;NAME&quot;, &quot;MEMBER&quot;.&quot;PHONE&quot; AS &quot;PHONE&quot;, &quot;MEMBER&quot;.&quot;EMAIL&quot; AS &quot;EMAIL&quot;, &quot;MEMBER&quot;.&quot;MEMBER_ID&quot; AS &quot;MEMBER_ID&quot; FROM &quot;MEMBER&quot; <strong>WHERE &quot;MEMBER&quot;.&quot;EMAIL&quot; = ?</strong></p>
<blockquote>
<p>쿼리 메서드(Query Method)
: Spring Data JDBC에서는 쿼리 메서드를 이용해서 SQL 쿼리문을 사용하지 않고 데이터베이스에 질의를 할 수 있습니다.
기본적인 사용법은 ‘find + By + SQL 쿼리문에서 WHERE 절의 열명 + (WHERE 절 열의 조건이 되는 데이터) ’ 형식이며, WHERE 절의 조건 열을 여러 개 지정하고 싶다면 ‘And’를 사용하면 됩니다.</p>
</blockquote>
<p>만약 복잡한 쿼리문을 작성하고자 한다면 @Query 애너테이션을 사용합니다.
@Query 애너테이션은 쿼리 메서드명을 기준으로 SQL 쿼리문을 생성하는 것이 아니라 개발자가 직접 쿼리문을 작성해서 질의를 할 수 있도록 해줍니다.</p>
<p>그리고 MemberRepository 인터페이스는 정의했지만 인터페이스의 구현 클래스는 별도로 구현을 한 적이 없습니다.</p>
<p>-&gt; Spring Data JDBC에서 내부적으로 Java의 리플렉션 기술과 Proxy 기술을 이용해서 MemberRepository 인터페이스의 구현 클래스 객체를 생성해 줍니다.</p>
<p>&amp;nbsp
&amp;nbsp</p>
<h4 id="optionalofnullable을-사용하는-이유">Optional.ofNullable(…)을 사용하는 이유</h4>
<p>파라미터로 전달받은 member 객체는 클라이언트 쪽에서 사용자가 이름 정보나 휴대폰 정보를 선택적으로 수정할 수 있기 때문에 name 멤버 변수가 null일 수 도 있고, phone 멤버 변수가 null일 수도 있습니다.</p>
<p>이처럼 멤버 변수 값이 null일 경우에는 Optional.of()가 아닌 Optional.ofNullable()을 이용해서 null 값을 허용할 수 있습니다.</p>
<p>따라서 값이 null이더라도 NullPointerException이 발생하지 않고, 다음 메서드인 ifPresent() 메서드를 호출할 수 있습니다.</p>
<p>수정할 값이 있다면(name 또는 phone 멤버 변수의 값이 null이 아니라면) ifPresent() 메서드 내의 코드가 실행이 되고, 수정할 값이 없다면 (name 또는 phone 멤버 변수의 값이 null이라면) 아무 동작도 하지 않습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[JDBC 기반 데이터 액세스 계층(3) - Spring Data JDBC 기반의 도메인 엔티티 및 테이블 설계]]></title>
            <link>https://velog.io/@be_chobo/JDBC-%EA%B8%B0%EB%B0%98-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%95%A1%EC%84%B8%EC%8A%A4-%EA%B3%84%EC%B8%B53-Spring-Data-JDBC-%EA%B8%B0%EB%B0%98%EC%9D%98-%EB%8F%84%EB%A9%94%EC%9D%B8-%EC%97%94%ED%8B%B0%ED%8B%B0-%EB%B0%8F-%ED%85%8C%EC%9D%B4%EB%B8%94-%EC%84%A4%EA%B3%84</link>
            <guid>https://velog.io/@be_chobo/JDBC-%EA%B8%B0%EB%B0%98-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%95%A1%EC%84%B8%EC%8A%A4-%EA%B3%84%EC%B8%B53-Spring-Data-JDBC-%EA%B8%B0%EB%B0%98%EC%9D%98-%EB%8F%84%EB%A9%94%EC%9D%B8-%EC%97%94%ED%8B%B0%ED%8B%B0-%EB%B0%8F-%ED%85%8C%EC%9D%B4%EB%B8%94-%EC%84%A4%EA%B3%84</guid>
            <pubDate>Thu, 31 Aug 2023 08:25:40 GMT</pubDate>
            <description><![CDATA[<h3 id="ddddomain-driven-design란">DDD(Domain Driven Design)란?</h3>
<hr>
<p>DDD(Domain Driven Design)는 도메인 주도 설계로, 용어의 의미 그대로 도메인 위주의 설계 기법을 의미합니다.</p>
<h3 id="도메인domain이란">도메인(Domain)이란?</h3>
<hr>
<p>도메인이란 용어는 주로 비즈니스적인 어떤 업무 영역과 관련이 있습니다.
도메인 지식(Domain Knowledge)들을 서비스 계층에서 비즈니스 로직으로 구현해야 하는 것입니다.</p>
<h3 id="애그리거트aggregate란">애그리거트(Aggregate)란?</h3>
<hr>
<p>애그리거트(Aggregate)란 비슷한 업무 도메인들의 묶음을 말합니다.</p>
<p>즉, 한마디로 비슷한 범주의 연관된 업무들을 하나로 그룹화해놓은 그룹이라고 생각하면 됩니다.</p>
<h3 id="애그리거트-루트aggregate-root란">애그리거트 루트(Aggregate Root)란?</h3>
<hr>
<p>애그리거트 안에는 1개 이상의 도메인들이 있는데, 각각의 애그리거트에는 해당 애그리거트를 대표하는 도메인이 존재합니다.</p>
<p>이처럼 하나의 애그리거트를 대표하는 도메인을 DDD에서는 애그리거트 루트(Aggregate Root)라고 합니다.</p>
<h3 id="애그리거트-루트-선정-기준">애그리거트 루트 선정 기준</h3>
<hr>
<p>각 애그리거트 내의 도메인들 중에서 다른 모든 도메인들과 직간접적으로 연관이 되어 있는 도메인들을 발견할 수 있습니다.</p>
<p>그러한 도메인이 애그리거트 루트가 됩니다.</p>
<p>데이터베이스의 테이블 간 관계로 보자면, 애그리거트 루트는 부모 테이블이 되고, 애그리거트 루트가 아닌 다른 도메인들은 자식 테이블이 되는 셈입니다.</p>
<p>즉, 애그리거트 루트(Aggregate Root)의 기본키 정보를 다른 도메인들이 외래키 형태로 가지고 있다고 볼 수 있습니다.</p>
<blockquote>
<p>관계형 데이터베이스에서 A 테이블의 기본키를 B 테이블이 가지고 있다면 A는 부모 테이블이 되고, B는 자식 테이블이 됩니다. B인 자식 테이블이 가지고 있는 A 테이블의 기본키를 외래키라고 합니다.</p>
</blockquote>
<p>&amp;nbsp</p>
<h3 id="애플리케이션-도메인-엔티티-및-테이블-설계">애플리케이션 도메인 엔티티 및 테이블 설계</h3>
<p><strong>1. 도메인에서 애그리거트 루트 찾기</strong></p>
<p>*<em>2. 애그리거트 간의 관계
*</em></p>
<ul>
<li>회원 정보(Member)와 주문 정보(Orders)의 관계(1 대 N)<ul>
<li>한 명의 회원은 여러 번 주문을 할 수 있습니다.</li>
</ul>
</li>
<li>주문 정보(Orders)와 커피 정보의 관계(N 대 N)<ul>
<li>하나의 주문은 여러 종류의 커피를 가질 수 있습니다.</li>
<li>하나의 커피는 여러 건의 주문에 속할 수 있습니다.</li>
</ul>
</li>
<li>N 대 N의 관계는 일반적으로 1 대 N, N 대 1의 관계로 재 설계 되기 때문에 아래와 같이 변경됩니다.<ul>
<li>주문 정보(Orders)와 주문 커피 정보(Order_Coffee): 1 대 N<ul>
<li>주문 커피 정보(Order_Coffee)와 커피 정보(Coffee): N 대 1</li>
</ul>
</li>
</ul>
</li>
</ul>
<p>*<em>3. 엔티티 클래스 간의 관계
*</em>
데이터베이스 테이블 간의 관계는 외래키를 통해 맺어지지만 클래스 간의 관계는 객체의 참조를 통해 맺어집니다.</p>
<ul>
<li><p>회원(Member) 엔티티 클래스</p>
<ul>
<li>Member 클래스와 Order의 관계는 1 대 N의 관계이기 때문에 Member 클래스에 List<Order>가 추가되었습니다.</li>
<li>한 명의 회원은 여러 건의 주문을 할 수 있습니다. 따라서 하나의 Member 클래스에 여러 건의 주문이 포함된 List<Order>를 멤버 변수로 추가했습니다.</li>
</ul>
</li>
<li><p>주문(Order) 엔티티 클래스</p>
<ul>
<li>Order 클래스와 Coffee 클래스는 N 대 N의 관계를 가지기 때문에 N 대 N의 관계를 1 대 N의 관계로 만들어주는 List<OrderCoffee>를 멤버 변수로 추가했습니다.</li>
</ul>
</li>
<li><p>커피(Coffee) 엔티티 클래스</p>
<ul>
<li>Coffee클래스와 Order 클래스는 N 대 N의 관계를 가지기 때문에 N 대 N의 관계를 1 대 N의 관계로 만들어주는 List<OrderCoffee>를 멤버 변수로 추가했습니다.</li>
</ul>
</li>
<li><p>주문_커피(OrderCoffee) 테이블</p>
<ul>
<li>Order 클래스와 Coffee 클래스가 N 대 N 관계이므로 두 클래스의 관계를 각각 1 대 N의 관계로 만들어주기 위한 OrderCoffee 클래스가 추가되었습니다.</li>
<li>주문하는 커피가 한 잔 이상일 수 있기 때문에 quantity(주문 수량) 멤버 변수를 추가했습니다</li>
</ul>
</li>
</ul>
<blockquote>
<p> 애그리거트 루트(Aggregate Root)는 Spring Data JDBC가 DDD와 밀접한 관련이 있기 때문에 도메인 모델을 잘 정의하고 애그리거트 루트(Aggregate Root)를 찾아야 합니다.
이 애그리거트 루트(Aggregate Root)를 통해 프로그래밍 코드 상에서의 구현 방법이 달라집니다.</p>
</blockquote>
<p><strong>4. 데이터베이스 테이블 설계</strong></p>
<p>  도메인 엔티티 클래스 간의 관계는 객체의 참조로 이루어지지만 테이블 간의 관계는 여러분이 이미 알고 있는 외래키(Foreign key) 참조로 이루어집니다.</p>
<blockquote>
<p> ‘ORDER’는 SQL 쿼리문에서 테이블의 로우(Row)를 정렬하기 위해 사용하는 ‘ORDER BY’라는 예약어에 사용되기 때문에 테이블이 생성될 때 에러를 발생할 수 있습니다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[JDBC 기반 데이터 액세스 계층(2) - Spring Data JDBC란?]]></title>
            <link>https://velog.io/@be_chobo/JDBC-%EA%B8%B0%EB%B0%98-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%95%A1%EC%84%B8%EC%8A%A4-%EA%B3%84%EC%B8%B52-Spring-Data-JDBC%EB%9E%80</link>
            <guid>https://velog.io/@be_chobo/JDBC-%EA%B8%B0%EB%B0%98-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%95%A1%EC%84%B8%EC%8A%A4-%EA%B3%84%EC%B8%B52-Spring-Data-JDBC%EB%9E%80</guid>
            <pubDate>Thu, 31 Aug 2023 07:41:29 GMT</pubDate>
            <description><![CDATA[<h3 id="데이터-엑세스-기술-유형">데이터 엑세스 기술 유형</h3>
<p>Spring에서 사용할 수 있는 대표적인 데이터 액세스 기술에는 mybatis, Spring JDBC, Spring Data JDBC, JPA, Spring Data JPA 등이 있습니다.</p>
<p>&amp;nbsp</p>
<h3 id="sql-중심-기술">SQL 중심 기술</h3>
<ul>
<li>mybatis</li>
<li>Spring JDBC</li>
</ul>
<p>SQL 중심 기술은 애플리케이션에서 데이터베이스에 접근하기 위해 SQL 쿼리문을 애플리케이션 내부에 직접적으로 작성하는 것이 중심이 되는 기술입니다.</p>
<p>mybatis의 경우, SQL Mapper라는 설정 파일이 존재하는데 이 SQL Mapper에서 SQL 쿼리문을 직접적으로 작성합니다.</p>
<p>작성된 SQL 쿼리문을 기반으로 데이터베이스의 특정 테이블에서 데이터를 조회한 후, Java 객체로 변환해 주는 것이 mybatis의 대표적인 기술적 특징입니다.</p>
<p>Spring JDBC의 경우에도 JdbcTemplate이라는 템플릿 클래스를 사용하는데 Java 코드에 SQL 쿼리문이 직접적으로 포함이 되어 있습니다.</p>
<p>&amp;nbsp</p>
<h3 id="객체-중심-기술">객체 중심 기술</h3>
<p>객체(Object) 중심 기술은 데이터를 SQL 쿼리문 위주로 생각하는 것이 아니라 모든 데이터를 객체(Object) 관점으로 바라보는 기술입니다.</p>
<p>즉, 객체 중심 기술은 데이터베이스에 접근하기 위해서 SQL 쿼리문을 직접적으로 작성하기보다는 데이터베이스의 테이블에 데이터를 저장하거나 조회할 경우, Java 객체를 이용해 애플리케이션 내부에서 이 Java 객체를 SQL 쿼리문으로 자동 변환 한 후에 데이터베이스의 테이블에 접근합니다.</p>
<p>이러한 객체(Object) 중심의 데이터 액세스 기술을 <strong>ORM(Object-Relational Mapping</strong>)이라고 합니다.</p>
<p>Java에서 대표적인 ORM 기술이 바로 <strong>JPA(Java Persistence API)</strong>입니다.</p>
<p>&amp;nbsp</p>
<h3 id="spring-data-jdbc란">Spring Data JDBC란?</h3>
<p>Spring Data JDBC는 JPA처럼 ORM 기술을 사용하지만 JPA의 기술적 복잡도를 낮춘 기술입니다.</p>
<p>2018년에 1.0 버전이 처음 릴리스되었기 때문에 기술의 역사가 아직 짧은 편입니다. 따라서 현재도 기능 업그레이드가 꾸준히 이루어지고 있지만 아직까지는 JPA보다 상대적으로 적게 사용되고 있습니다.</p>
<p>하지만 애플리케이션의 규모가 상대적으로 크지 않고, 복잡하지 않을 경우에는 Spring Data JDBC가 뛰어난 생산성을 보여줄 거라 기대합니다.</p>
<p>&amp;nbsp</p>
<h3 id="spring-data-jdbc를-사용하기-위한-사전-준비">Spring Data Jdbc를 사용하기 위한 사전 준비</h3>
<p><strong>1. 의존 라이브러리 추가</strong>
<img src="https://velog.velcdn.com/images/be_chobo/post/8c716fc7-0004-42e8-97a2-a1a85aa08d7c/image.png" alt=""></p>
<p>Spring Data JDBC를 사용하기 위해 ‘spring-boot-starter-data-jdbc’를 추가했습니다.</p>
<p>그리고 데이터베이스에서 데이터를 관리할 것이므로 개발 환경에서 손쉽게 사용할 수 있는 인메모리(In-memory) DB인 H2를 사용하기 위해 의존 라이브러리 설정에 추가했습니다.
(로컬 테스트 환경에서는 인메모리(In-memory) DB 사용을 권장한다)</p>
<blockquote>
<p><strong>인메모리 DB란?</strong>
인메모리(In-memory) DB는 이름 그대로 메모리 안에 데이터를 저장하는 데이터베이스입니다.
인메모리(In-memory) DB는 애플리케이션이 실행되는 동안에만 데이터를 저장하고 있기 때문에 애플리케이션 실행을 중지했다가 다시 실행시키면 인메모리(In-memory) DB안에 저장되어 있던 데이터는 모두 사라지게 됩니다.</p>
</blockquote>
<p>&amp;nbsp
<strong>2. application.yml 파일에 H2 Browser 활성화 설정 추가</strong></p>
<p>Spring에서는 application.properties 또는 application.yml 파일을 통해 Spring에서 사용하는 다양한 설정 정보들을 입력할 수 있습니다.</p>
<p>(.yml 파일은 애플리케이션의 설정 정보를 depth 별로 입력할 수 있는 더 나은 방법을 제공합니다.)</p>
<pre><code>spring:
  h2:
    console:
      enabled: true</code></pre><p>위와 같이 설정하면 웹 브라우저 상(H2 콘솔)에서 H2 DB에 접속한 후, 데이터베이스를 관리할 수 있습니다.</p>
<p>로그에서 확인한 URL 컨텍스트인 ‘/h2-console’을 복사해서 웹 브라우저에 주소를(localhost:8080/h2-console) 입력합니다.</p>
<p>그리고 애플리케이션 로그에 출력된 ‘jdbc:h2:mem:26d0d5d3-dcef-47f8-8e6b-297853bdcffg3’ 을 [JDBC URL]이라는 항목에 복사/붙여넣기 한 후, [Connect] 버튼을 클릭합니다.</p>
<p>그러면 H2 DB에 정상적으로 접속할 수 있습니다.</p>
<blockquote>
<p><strong>H2 DB 디폴트 설정의 문제점</strong> : H2 DB는 애플리케이션을 재시작할 때마다 애플리케이션 로그에 출력되는 JDBC URL이 매번 랜덤하게 바뀌기 때문에 매번 랜덤하게 변경된 JDBC URL을 다시 입력하는 것은 상당히 불편합니다.</p>
</blockquote>
<p>이 문제는 application.yml 파일에 H2에 대한 추가 설정을 함으로써 해결할 수 있습니다.</p>
<p>&amp;nbsp
<strong>3. H2 DB 설정 추가</strong></p>
<pre><code>spring:
  h2:
    console:
      enabled: true
      path: /h2     # (1) Context path 변경
  datasource:
    url: jdbc:h2:mem:test     # (2) JDBC URL 변경</code></pre><p>(1)에서는 H2 콘솔의 접속 URL Context path를 조금 더 간결하게 ‘/h2’로 설정했습니다.</p>
<p>(2)에서는 JDBC URL이 매번 랜덤하게 바뀌지 않도록 ‘jdbc:h2:mem:test’로 설정했습니다.</p>
<p>이제 ‘localhost:8080/h2’로 접속한 뒤 [JDBC URL] 항목에 ‘jdbc:h2:mem:test’을 입력하고 [Connect] 버튼을 클릭하면 접속이 될 것입니다.</p>
<p>&amp;nbsp
<strong>4. H2 DB에 MESSAGE 테이블 생성</strong>
데이터를 저장할 테이블을 H2 DB에 생성하지 않았습니다.</p>
<pre><code>spring:
  h2:
    console:
      enabled: true
      path: /h2     
  datasource:
    url: jdbc:h2:mem:test
  sql:
    init:
      schema-locations: classpath*:db/h2/schema.sql   // (1) 테이블 생성 파일 경로</code></pre><p>Spring에서는 (1)과 같이 테이블 생성을 위한 SQL 문이 추가된 ‘schema’라는 파일명으로 .sql 파일의 경로를 지정해 주면 이 schema.sql 파일에 있는 스크립트를 읽어서 애플리케이션 실행 시, 데이터베이스에 테이블을 자동으로 생성해 줍니다.</p>
<p>인메모리 DB를 사용할 경우, 애플리케이션이 실행될 때마다 schema.sql 파일의 스크립트가 매번 실행된다는 사실 또한 기억하기 바랍니다.</p>
<p>schema.sql 파일은 ‘src/main/resources/db/h2’ 디렉토리 내에 위치해 있습니다.</p>
<pre><code>CREATE TABLE IF NOT EXISTS MESSAGE (
    message_id bigint NOT NULL AUTO_INCREMENT,
    message varchar(100) NOT NULL,
    PRIMARY KEY (message_id)
);</code></pre><p> ‘message_id’는 MESSAGE 테이블의 Primary key이고 AUTO_INCREMENT를 지정했기 때문에 데이터가 insert될 때마다 자동으로 증가됩니다.</p>
<p>즉, 애플리케이션 쪽에서 데이터베이스에 데이터를 insert할 때 ‘message_id’ 열에 해당하는 값을 지정해주지 않아야 한다는 의미입니다.</p>
<p>그리고 MESSAGE 테이블은 Message 클래스 명과 매핑되고 ‘message_id’ 열은 Message 클래스의 messageId 멤버 변수와 매핑됩니다.</p>
<p>‘message’ 열은 Message 클래스의 message 멤버 변수와 매핑됩니다.</p>
<p><strong>이렇듯 ORM(Object-Relational Mapping)에서는 객체의 멤버 변수와 데이터베이스 테이블의 열이 대부분 1대1로 매핑이 됩니다.</strong></p>
<p>&amp;nbsp
&amp;nbsp</p>
<h3 id="정리--spring-data-jdbc-적용-순서">정리 : Spring Data JDBC 적용 순서</h3>
<ol>
<li>build.gradle에 사용할 데이터베이스를 위한 의존 라이브러리를 추가합니다.</li>
<li>application.yml 파일에 사용할 데이터베이스에 대한 설정을 합니다.</li>
<li>‘schema.sql’ 파일에 필요한 테이블 스크립트를 작성합니다.</li>
<li>application.yml 파일에서 ‘schema.sql’ 파일을 읽어서 테이블을 생성할 수 있도록 초기화 설정을 추가합니다.</li>
<li>데이터베이스의 테이블과 매핑할 엔티티(Entity) 클래스를 작성합니다.</li>
<li>작성한 엔티티 클래스를 기반으로 데이터베이스의 작업을 처리할 Repository 인터페이스를 작성합니다.</li>
<li>작성된 Repository 인터페이스를 서비스 클래스에서 사용할 수 있도록 DI 합니다.</li>
<li>DI 된 Repository의 메서드를 사용해서 서비스 클래스에서 데이터베이스에 CRUD 작업을 수행합니다.</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[JDBC 기반 데이터 액세스 계층(1) - JDBC란?]]></title>
            <link>https://velog.io/@be_chobo/JDBC-%EA%B8%B0%EB%B0%98-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%95%A1%EC%84%B8%EC%8A%A4-%EA%B3%84%EC%B8%B51-JDBC%EB%9E%80</link>
            <guid>https://velog.io/@be_chobo/JDBC-%EA%B8%B0%EB%B0%98-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%95%A1%EC%84%B8%EC%8A%A4-%EA%B3%84%EC%B8%B51-JDBC%EB%9E%80</guid>
            <pubDate>Thu, 31 Aug 2023 06:21:08 GMT</pubDate>
            <description><![CDATA[<h3 id="jdbc란">JDBC란?</h3>
<p>JDBC(Java Database Connectivity)는 Java 기반 애플리케이션의 코드 레벨에서 사용하는 데이터를 데이터베이스에 저장 및 업데이트하거나 반대로 데이터베이스에 저장된 데이터를 Java 코드 레벨에서 사용할 수 있도록 해주는 Java에서 제공하는 표준 사양(명세)입니다.</p>
<p>JDBC API를 사용해서 다양한 벤더(Oracle, MS SQL, MySQL 등)의 데이터베이스와 연동할 수 있습니다.</p>
<p>JDBC는 Java 기반의 애플리케이션에서 사용하는 데이터 액세스 기술의 기본이 되는 저수준(low level) API입니다.</p>
<p>Spring에서는 JDBC API를 직접적으로 사용하기보다는 Spring Data JDBC나 Spring Data JPA 같은 기술을 제공함으로써 개발자들이 조금 더 편리하게 데이터 액세스 로직을 구현할 수 있도록 해줍니다.</p>
<p>&amp;nbsp</p>
<h3 id="jdbc의-동작-흐름">JDBC의 동작 흐름</h3>
<blockquote>
<p>JAVA 애플리케이션  --&gt; JDBC API --&gt; JDBC 드라이버 --&gt; 데이터베이스</p>
</blockquote>
<p>Java 애플리케이션에서 JDBC API를 이용해 적절한 데이터베이스 드라이버를 로딩한 후, 데이터베이스와 인터랙션 한다</p>
<p>&amp;nbsp</p>
<h3 id="jdbc-api-사용-흐름">JDBC API 사용 흐름</h3>
<p><img src="https://velog.velcdn.com/images/be_chobo/post/acf6d061-99c6-496d-be33-ea051e0ecef1/image.png" alt=""></p>
<ol>
<li><p>JDBC 드라이버 로딩
사용하고자 하는 JDBC 드라이버를 로딩합니다. JDBC 드라이버는 DriverManager라는 클래스를 통해서 로딩됩니다.</p>
</li>
<li><p>Connection 객체 생성
JDBC 드라이버가 정상적으로 로딩되면 DriverManager를 통해 데이터베이스와 연결되는 세션(Session)인 Connection 객체를 생성합니다.</p>
</li>
<li><p>Statement 객체 생성
Statement 객체는 작성된 SQL 쿼리문을 실행하기 위한 객체로써 객체 생성 후에 정적인 SQL 쿼리 문자열을 입력으로 가집니다.</p>
</li>
<li><p>Query 실행
생성된 Statement 객체를 이용해서 입력한 SQL 쿼리를 실행합니다.</p>
</li>
<li><p>ResultSet 객체로부터 데이터 조회
실행된 SQL 쿼리문에 대한 결과 데이터 셋입니다.</p>
</li>
<li><p>ResultSet 객체 Close, Statement 객체 Close, Connection 객체 Close
JDBC API를 통해 사용된 객체들은 사용 이후에 사용한 순서의 역순으로 차례로 Close를 해주어야 합니다.</p>
</li>
</ol>
<p>&amp;nbsp</p>
<h3 id="connection-pool이란">Connection Pool이란?</h3>
<p>JDBC API를 사용해서 데이터베이스와의 연결을 위한 Connection 객체를 생성하는 작업은 비용이 많이 드는 작업 중 하나입니다.</p>
<p>따라서 애플리케이션 로딩 시점에 Connection 객체를 미리 생성해 두고 애플리케이션에서 데이터베이스에 연결이 필요할 경우, Connection 객체를 새로 생성하는 것이 아니라 미리 만들어 둔 Connection 객체를 사용함으로써 애플리케이션의 성능을 향상할 수 있습니다.</p>
<p>이처럼 데이터베이스 Connection을 미리 만들어서 보관하고 애플리케이션이 필요할 때 이 Connection을 제공해 주는 역할을 하는 Connection 관리자를 바로 <strong>Connection Pool</strong>이라고 합니다.</p>
<p>(Spring Boot 2.0 이전 버전에는 Apache 재단의 오픈 소스인 Apache Commons DBCP(Database Connection Pool, DBCP)를 주로 사용했지만 Spring Boot 2.0부터는 성능면에서 더 나은 이점을 가지고 있는 <strong>HikariCP를 기본 DBCP</strong>로 채택했습니다.)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring MVC에서의 예외처리(3) - 비즈니스 로직에 대한 예외처리]]></title>
            <link>https://velog.io/@be_chobo/Spring-MVC%EC%97%90%EC%84%9C%EC%9D%98-%EC%98%88%EC%99%B8%EC%B2%98%EB%A6%AC3-%EB%B9%84%EC%A6%88%EB%8B%88%EC%8A%A4-%EB%A1%9C%EC%A7%81%EC%97%90-%EB%8C%80%ED%95%9C-%EC%98%88%EC%99%B8%EC%B2%98%EB%A6%AC</link>
            <guid>https://velog.io/@be_chobo/Spring-MVC%EC%97%90%EC%84%9C%EC%9D%98-%EC%98%88%EC%99%B8%EC%B2%98%EB%A6%AC3-%EB%B9%84%EC%A6%88%EB%8B%88%EC%8A%A4-%EB%A1%9C%EC%A7%81%EC%97%90-%EB%8C%80%ED%95%9C-%EC%98%88%EC%99%B8%EC%B2%98%EB%A6%AC</guid>
            <pubDate>Sat, 26 Aug 2023 08:50:36 GMT</pubDate>
            <description><![CDATA[<p>Java에서는 throw 키워드를 사용해서 예외를 메서드 바깥으로 던질 수 있습니다.
던져진 예외는 메서드 바깥 즉, 메서드를 호출한 지점으로 던져지게 되는 것입니다.</p>
<p>서비스 계층의 메서드는 API 계층인 Controller의 핸들러 메서드가 이용하므로 서비스 계층에서 던져진 예외는 Controller의 핸들러 메서드 쪽에서 잡아서 처리할 수 있습니다.</p>
<p>그런데 이미 전에 Controller에서 발생하는 예외를 Exception Advice에서 처리하도록 공통화해두었으니 서비스 계층에서 던진 예외 역시 Exception Advice에서 처리하면 됩니다.</p>
<p>&amp;nbsp</p>
<h4 id="서비스-계층에서-예외-던지기throw">서비스 계층에서 예외 던지기(throw)</h4>
<pre><code>@Service
public class MemberService {
    ...
        ...

    public Member findMember(long memberId) {
        // TODO should business logic

                // (1)
        throw new RuntimeException(&quot;Not found member&quot;);
    }

        ...
        ...
}</code></pre><p>throw 키워드를 사용하여 RuntimeException 객체에 적절한 예외 메시지를 포함한 후에 메서드 밖으로 던졌습니다.</p>
<p>&amp;nbsp</p>
<h4 id="globalexceptionadvice-예외-잡기catch">GlobalExceptionAdvice 예외 잡기(catch)</h4>
<pre><code>@RestControllerAdvice
public class GlobalExceptionAdvice {
    ...
        ...

        // (1)
    @ExceptionHandler
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public ErrorResponse handleResourceNotFoundException(RuntimeException e) {
        System.out.println(e.getMessage());

                // 수정 필요

        return null;
    }
}</code></pre><p>RuntimeException을 잡아서(catch) 처리하기 위한 handleResourceNotFoundException() 메서드를 추가했습니다.</p>
<p>MemberController의 getMember() 핸들러 메서드에 요청을 보내면 MemberService에서 RuntimeException을 던지고, GlobalExceptionAdvice의 handleResourceNotFoundException() 메서드가 이 RuntimeException을 잡아서 예외 메시지인 “Not found member”를 콘솔에 출력할 것입니다.</p>
<p>서비스 계층에서 RuntimeException을 그대로 던지고(throw), Exception Advice에서 RuntimeException을 그대로 잡는 것(catch)은 예외의 의도가 명확하지 않으며, 구체적으로 어떤 예외가 발생했는지에 대한 예외 정보를 얻는 것이 어렵습니다.</p>
<p>그래서 사용자 정의 예외를 사용해야 합니다.</p>
<p>&amp;nbsp
&amp;nbsp</p>
<h3 id="사용자-정의-예외custom-exception-사용">사용자 정의 예외(Custom Exception) 사용</h3>
<ul>
<li><p><strong>예외코드 정의</strong></p>
<pre><code>public enum ExceptionCode {
  MEMBER_NOT_FOUND(404, &quot;Member Not Found&quot;);

  @Getter
  private int status;

  @Getter
  private String message;

  ExceptionCode(int status, String message) {
      this.status = status;
      this.message = message;
  }
}</code></pre><p>ExceptionCode를 enum으로 정의하면 비즈니스 로직에서 발생하는 다양한 유형의 예외를 enum에 추가해서 사용할 수 있습니다.</p>
</li>
</ul>
<p>&amp;nbsp</p>
<ul>
<li><p><strong>BusinessLogicException 구현</strong></p>
<pre><code>public class BusinessLogicException extends RuntimeException {
  @Getter
  private ExceptionCode exceptionCode;

  public BusinessLogicException(ExceptionCode exceptionCode) {
      super(exceptionCode.getMessage());
      this.exceptionCode = exceptionCode;
  }
}</code></pre><p>서비스 계층에서 사용할 BusinessLogicException이라는 Custom Exception을 정의합니다.</p>
</li>
</ul>
<p>BusinessLogicException은 RuntimeException을 상속하고 있으며 ExceptionCode를 멤버 변수로 지정하여 생성자를 통해서 조금 더 구체적인 예외 정보들을 제공해 줄 수 있습니다.</p>
<p>그리고 상위 클래스인 RuntimeException의 생성자(super)로 예외 메시지를 전달해 줍니다.</p>
<p><strong>BusinessLogicException은 서비스 계층에서 개발자가 의도적으로 예외를 던져야 하는 다양한 상황에서 ExceptionCode 정보만 바꿔가며 던질 수 있습니다.</strong></p>
<p>&amp;nbsp</p>
<ul>
<li><p><strong>서비스 계층에 BusinessLogicException 적용</strong></p>
<pre><code>@Service
public class MemberService {
  ...
      ...

  public Member findMember(long memberId) {
      // TODO should business logic

              // (1)
      throw new BusinessLogicException(ExceptionCode.MEMBER_NOT_FOUND);
  }

  ...
      ...
}</code></pre></li>
</ul>
<p>&amp;nbsp</p>
<ul>
<li><p><strong>Exception Advice에서 BusinessLogicException 처리</strong></p>
<pre><code>@RestControllerAdvice
public class GlobalExceptionAdvice {
  ...
      ...

  @ExceptionHandler
  public ResponseEntity handleBusinessLogicException(BusinessLogicException e) {
      System.out.println(e.getExceptionCode().getStatus());
      System.out.println(e.getMessage());

              // ErrorResponse 수정

      return new ResponseEntity&lt;&gt;(HttpStatus.valueOf(e.getExceptionCode().getStatus()));
  }
}</code></pre></li>
<li><p><em>메서드 명 변경*</em>
: 먼저 메서드 명이 서비스 계층의 비즈니스 로직 처리에서 발생하는 예외를 처리하는 것을 목적으로 하기 때문에 메서드 명이 handleBusinessLogicException으로 변경되었습니다.</p>
</li>
</ul>
<p><strong>메서드 파라미터 변경</strong>
: RuntimeException을 파라미터로 전달받던 것을 BusinessLogicException을 전달받는 것으로 변경되었습니다.</p>
<p><strong>@ResponseStatus(HttpStatus.NOT_FOUND) 제거</strong>
: @ResponseStatus 애너테이션은 고정된 HttpStatus를 지정하기 때문에 BusinessLogicException과 같이 다양한 Status를 동적으로 처리할 수 없으므로 ResponseEntity를 사용해서 HttpStatus를 동적으로 지정하도록 변경했습니다.</p>
<blockquote>
<p>한 가지 유형으로 고정된 예외를 처리할 경우에는 @ResponseStatus로 HttpStatus를 지정해서 사용하면 되고, BusinessLogicException처럼 다양한 유형의 Custom Exception을 처리하고자 할 경우에는 ResponseEntity를 사용하면 됩니다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring MVC에서의 예외처리(2) - @RestControllerAdvice]]></title>
            <link>https://velog.io/@be_chobo/Spring-MVC%EC%97%90%EC%84%9C%EC%9D%98-%EC%98%88%EC%99%B8%EC%B2%98%EB%A6%AC2-RestControllerAdvice</link>
            <guid>https://velog.io/@be_chobo/Spring-MVC%EC%97%90%EC%84%9C%EC%9D%98-%EC%98%88%EC%99%B8%EC%B2%98%EB%A6%AC2-RestControllerAdvice</guid>
            <pubDate>Fri, 18 Aug 2023 09:01:09 GMT</pubDate>
            <description><![CDATA[<h3 id="restcontrolleradvice를-사용한-예외-처리-공통화">@RestControllerAdvice를 사용한 예외 처리 공통화</h3>
<p>특정 클래스에 @RestControllerAdvice 애너테이션을 추가하면 여러 개의 Controller 클래스에서 @ExceptionHandler, @InitBinder 또는 @ModelAttribute가 추가된 메서드를 공유해서 사용할 수 있습니다.</p>
<blockquote>
<p>@InitBinder와 @ModelAttribute 애너테이션은 JSP, Thymeleaf 같은 서버 사이드 렌더링(SSR, Server Side Rendering) 방식에서 주로 사용되는 방식입니다.</p>
</blockquote>
<p>이 말의 의미를 예외 처리 관점에서 설명하자면,
@RestControllerAdvice 애너테이션을 추가한 클래스를 이용하면 예외 처리를 공통화할 수 있다는 것입니다.</p>
<p>&amp;nbsp</p>
<p><strong>Controller 클래스에서 @ExceptionHandler 로직 제거</strong> : 이전 Controller에 구현된 @ExceptionHandler가 추가된 메서드들은 모두 제거합니다.</p>
<p>&amp;nbsp
<strong>ExceptionAdvice 클래스 정의</strong></p>
<pre><code>@RestControllerAdvice
public class GlobalExceptionAdvice {

}</code></pre><p>예외를 처리할 ExceptionAdvice 클래스에 @RestControllerAdvice 애너테이션을 추가하면 이 클래스는 이제 Controller 클래스에서 발생하는 예외를 도맡아서 처리하게 됩니다.</p>
<p>&amp;nbsp
<strong>Exception 핸들러 메서드 구현</strong> : GlobalExceptionAdvice 클래스에서 처리할 Exception 핸들러 메서드를 구현</p>
<pre><code>@RestControllerAdvice
public class GlobalExceptionAdvice {
        // (1)
    @ExceptionHandler
    public ResponseEntity handleMethodArgumentNotValidException(
            MethodArgumentNotValidException e) {
        final List&lt;FieldError&gt; fieldErrors = e.getBindingResult().getFieldErrors();

        List&lt;ErrorResponse.FieldError&gt; errors =
                fieldErrors.stream()
                        .map(error -&gt; new ErrorResponse.FieldError(
                                error.getField(),
                                error.getRejectedValue(),
                                error.getDefaultMessage()))
                        .collect(Collectors.toList());

        return new ResponseEntity&lt;&gt;(new ErrorResponse(errors), HttpStatus.BAD_REQUEST);
    }

        // (2)
    @ExceptionHandler
    public ResponseEntity handleConstraintViolationException(
            ConstraintViolationException e) {
        // TODO should implement for validation

        return new ResponseEntity&lt;&gt;(HttpStatus.BAD_REQUEST);
    }
}</code></pre><p>이처럼 @RestControllerAdvice 애너테이션을 이용해서 예외 처리를 공통화하면 각 Controller마다 추가되는 @ExceptionHandler 로직에 대한 중복 코드를 제거하고, Controller의 코드를 단순화할 수 있습니다.</p>
<p>&amp;nbsp
<strong>ErrorResponse 수정</strong>
위 코드에서 URI 변수로 넘어오는 값의 유효성 검증에 대한 에러(ConstraintViolationException) 처리는 아직 구현되지 않았습니다.</p>
<p>ErrorResponse 클래스가 ConstraintViolationException에 대한 Error Response를 생성할 수 있도록 ErrorResponse 클래스를 수정해 보도록 하겠습니다.</p>
<pre><code>@Getter
public class ErrorResponse {
    private List&lt;FieldError&gt; fieldErrors; // (1)
    private List&lt;ConstraintViolationError&gt; violationErrors;  // (2)

        // (3)
    private ErrorResponse(List&lt;FieldError&gt; fieldErrors, List&lt;ConstraintViolationError&gt; violationErrors) {
        this.fieldErrors = fieldErrors;
        this.violationErrors = violationErrors;
    }

        // (4) BindingResult에 대한 ErrorResponse 객체 생성
    public static ErrorResponse of(BindingResult bindingResult) {
        return new ErrorResponse(FieldError.of(bindingResult), null);
    }

        // (5) Set&lt;ConstraintViolation&lt;?&gt;&gt; 객체에 대한 ErrorResponse 객체 생성
    public static ErrorResponse of(Set&lt;ConstraintViolation&lt;?&gt;&gt; violations) {
        return new ErrorResponse(null, ConstraintViolationError.of(violations));
    }

        // (6) Field Error 가공
    @Getter
    public static class FieldError {
        private String field;
        private Object rejectedValue;
        private String reason;

        private FieldError(String field, Object rejectedValue, String reason) {
            this.field = field;
            this.rejectedValue = rejectedValue;
            this.reason = reason;
        }

        public static List&lt;FieldError&gt; of(BindingResult bindingResult) {
            final List&lt;org.springframework.validation.FieldError&gt; fieldErrors =
                                                        bindingResult.getFieldErrors();
            return fieldErrors.stream()
                    .map(error -&gt; new FieldError(
                            error.getField(),
                            error.getRejectedValue() == null ?
                                            &quot;&quot; : error.getRejectedValue().toString(),
                            error.getDefaultMessage()))
                    .collect(Collectors.toList());
        }
    }

        // (7) ConstraintViolation Error 가공
    @Getter
    public static class ConstraintViolationError {
        private String propertyPath;
        private Object rejectedValue;
        private String reason;

        private ConstraintViolationError(String propertyPath, Object rejectedValue,
                                   String reason) {
            this.propertyPath = propertyPath;
            this.rejectedValue = rejectedValue;
            this.reason = reason;
        }

        public static List&lt;ConstraintViolationError&gt; of(
                Set&lt;ConstraintViolation&lt;?&gt;&gt; constraintViolations) {
            return constraintViolations.stream()
                    .map(constraintViolation -&gt; new ConstraintViolationError(
                            constraintViolation.getPropertyPath().toString(),
                            constraintViolation.getInvalidValue().toString(),
                            constraintViolation.getMessage()
                    )).collect(Collectors.toList());
        }
    }
}</code></pre><p>수정된 ErrorResponse는 총 두 개의 예외 유형을 처리해서 Error Response에 포함할 수 있습니다.</p>
<p>첫 번째가 DTO 클래스의 유효성 검증에서 발생하는 MethodArgumentNotValidException에 대한 Error Response이고</p>
<p>두 번째는 URI의 변수 값 검증에서 발생하는 ConstraintViolationException에 대한 Error Response입니다.</p>
<blockquote>
<p><strong>of() 메서드</strong>
of() 메서드는 Java 8의 API에서도 흔히 볼 수 있는 네이밍 컨벤션(Naming Convention)입니다.
주로 객체 생성 시 어떤 값들의(of~) 객체를 생성한다는 의미에서 of() 메서드를 사용합니다.</p>
</blockquote>
<p>&amp;nbsp
<strong>GlobalExceptionAdvice 클래스를 수정</strong></p>
<pre><code>@RestControllerAdvice
public class GlobalExceptionAdvice {
    @ExceptionHandler
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ErrorResponse handleMethodArgumentNotValidException(
            MethodArgumentNotValidException e) {
        final ErrorResponse response = ErrorResponse.of(e.getBindingResult());

        return response;
    }

    @ExceptionHandler
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ErrorResponse handleConstraintViolationException(
            ConstraintViolationException e) {
        final ErrorResponse response = ErrorResponse.of(e.getConstraintViolations());

        return response;
    }
}</code></pre><p> 전에는 ErrorResponse 객체를 ResponseEntity로 래핑해서 리턴한 반면 위 코드에서는 ResponseEntity가 사라지고 ErrorResponse 객체를 바로 리턴하고 있습니다.</p>
<p>그리고 @ResponseStatus 애너테이션을 이용해서 HTTP Status를 HTTP Response에 포함하고 있습니다.</p>
<blockquote>
<p><strong>@RestControllerAdvice = @ControllerAdvice + @ResponseBody</strong>
@RestControllerAdvice 애너테이션은 @ControllerAdvice의 기능을 포함하고 있으며, @ResponseBody의 기능 역시 포함하고 있기 때문에 JSON 형식의 데이터를 Response Body로 전송하기 위해서 ResponseEntity로 데이터를 래핑 할 필요가 없다는 사실을 기억해두길 바랍니다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring MVC에서의 예외처리(1) -@ExceptionHandler]]></title>
            <link>https://velog.io/@be_chobo/Spring-MVC%EC%97%90%EC%84%9C%EC%9D%98-%EC%98%88%EC%99%B8%EC%B2%98%EB%A6%AC</link>
            <guid>https://velog.io/@be_chobo/Spring-MVC%EC%97%90%EC%84%9C%EC%9D%98-%EC%98%88%EC%99%B8%EC%B2%98%EB%A6%AC</guid>
            <pubDate>Fri, 18 Aug 2023 08:21:43 GMT</pubDate>
            <description><![CDATA[<p>DTO 유효성 검증에서 클라이언트 요청 데이터의 유효성 검증에 실패할 경우 
받는 Response Body의 내용만으로는 요청 데이터 중에서 어떤 항목이 유효성 검증에 실패했는지 알 수가 없습니다.
<img src="https://velog.velcdn.com/images/be_chobo/post/047c0416-608f-4284-9ee8-114875d44d4e/image.png" alt=""></p>
<p>클라이언트 쪽에서 에러메시지를 조금 더 구체적으로 친절하게 알 수 있도록 바꾸는 작업이 필요합니다.</p>
<p>&amp;nbsp</p>
<h3 id="exceptionhandler를-이용한-controller-레벨에서의-예외-처리">@ExceptionHandler를 이용한 Controller 레벨에서의 예외 처리</h3>
<blockquote>
<p>Spring에서의 예외는 애플리케이션에 문제가 발생할 경우, 이 문제를 알려서 처리하는 것뿐만 아니라 유효성 검증에 실패했을 때와 같이 이 실패를 하나의 예외로 간주하여 이 예외를 던져서(throw) 예외 처리를 유도합니다.</p>
</blockquote>
<p><strong>MemberController에 @ExceptionHandler 적용</strong></p>
<pre><code>@RestController
@RequestMapping(&quot;/v6/members&quot;)
@Validated
@Slf4j
public class MemberControllerV6 {
    ...
        ...

    @PostMapping
    public ResponseEntity postMember(@Valid @RequestBody MemberPostDto memberDto) {
        Member member = mapper.memberPostDtoToMember(memberDto);

        Member response = memberService.createMember(member);

        return new ResponseEntity&lt;&gt;(mapper.memberToMemberResponseDto(response),
                HttpStatus.CREATED);
    }

        ...
        ...

    @ExceptionHandler
    public ResponseEntity handleException(MethodArgumentNotValidException e) {

        final List&lt;FieldError&gt; fieldErrors = e.getBindingResult().getFieldErrors();


        return new ResponseEntity&lt;&gt;(fieldErrors, HttpStatus.BAD_REQUEST);
    }
}</code></pre><p>위 코드를 설명하면</p>
<ol>
<li><p>RequestBody에 유효하지 않은 요청 데이터가 포함되어 있어 유효성 검증에 실패하고, MethodArgumentNotValidException이 발생</p>
</li>
<li><p>MemberController에는 @ExceptionHandler 애너테이션이 추가된 예외 처리 메서드인 handleException()이 있기 때문에 유효성 검증 과정에서 내부적으로 던져진 MethodArgumentNotValidException을 handleException() 메서드가 전달받음.</p>
</li>
<li><p>MethodArgumentNotValidException 객체에서 getBindingResult().getFieldErrors()를 통해 발생한 에러 정보를 확인</p>
</li>
<li><p>얻은 에러 정보를 ResponseEntity를 통해 Response Body로 전달</p>
</li>
</ol>
<p>&amp;nbsp
&amp;nbsp
에러 메시지를 구체적으로 전송해 주기 때문에 클라이언트 입장에서는 이제 어느 곳에 문제가 있는지를 구체적으로 알 수 있게 되었습니다.</p>
<pre><code>[
    {
        &quot;codes&quot;: [
            &quot;Email.memberPostDto.email&quot;,
            &quot;Email.email&quot;,
            &quot;Email.java.lang.String&quot;,
            &quot;Email&quot;
        ],
        &quot;arguments&quot;: [
            {
                &quot;codes&quot;: [
                    &quot;memberPostDto.email&quot;,
                    &quot;email&quot;
                ],
                &quot;arguments&quot;: null,
                &quot;defaultMessage&quot;: &quot;email&quot;,
                &quot;code&quot;: &quot;email&quot;
            },
            [],
            {
                &quot;arguments&quot;: null,
                &quot;defaultMessage&quot;: &quot;.*&quot;,
                &quot;codes&quot;: [
                    &quot;.*&quot;
                ]
            }
        ],
        &quot;defaultMessage&quot;: &quot;올바른 형식의 이메일 주소여야 합니다&quot;,
        &quot;objectName&quot;: &quot;memberPostDto&quot;,
        &quot;field&quot;: &quot;email&quot;,
        &quot;rejectedValue&quot;: &quot;hgd@&quot;,
        &quot;bindingFailure&quot;: false,
        &quot;code&quot;: &quot;Email&quot;
    }
]</code></pre><p>그런데 클라이언트 입장에서는 의미를 알 수 없는 정보를 전부 포함한 Response Body 전체 정보를 굳이 다 전달받을 필요는 없어 보입니다.</p>
<p>-&gt; 에러 정보를 기반으로 한 Error Response 클래스를 만들어서 필요한 정보만 담은 후에 클라이언트 쪽에 전달해 주면 됩니다.</p>
<p>&amp;nbsp
&amp;nbsp
<strong>ErrorResponse 클래스 적용</strong></p>
<pre><code>@Getter
@AllArgsConstructor
public class ErrorResponse {
    // (1)
    private List&lt;FieldError&gt; fieldErrors;

    @Getter
    @AllArgsConstructor
    public static class FieldError {
        private String field;
        private Object rejectedValue;
        private String reason;
    }
}</code></pre><p><strong>ErrorResponse를 사용하도록 MemberController의 handleException() 메서드 수정</strong></p>
<pre><code>...

    @ExceptionHandler
    public ResponseEntity handleException(MethodArgumentNotValidException e) {

        final List&lt;FieldError&gt; fieldErrors = e.getBindingResult().getFieldErrors();


        List&lt;ErrorResponse.FieldError&gt; errors =
                fieldErrors.stream()
                            .map(error -&gt; new ErrorResponse.FieldError(
                                error.getField(),
                                error.getRejectedValue(),
                                error.getDefaultMessage()))
                            .collect(Collectors.toList());

        return new ResponseEntity&lt;&gt;(new ErrorResponse(errors), HttpStatus.BAD_REQUEST);
    }
}</code></pre><p>필요한 정보들만 선택적으로 골라서 ErrorResponse.FieldError 클래스에 담아서 List로 변환 후, List&lt;ErrorResponse.FieldError&gt;를 ResponseEntity 클래스에 실어서 전달하고 있습니다.</p>
<p>&amp;nbsp
&amp;nbsp</p>
<h3 id="exceptionhandler의-단점">@ExceptionHandler의 단점</h3>
<ol>
<li><p>각각의 Controller 클래스에서 @ExceptionHandler 애너테이션을 사용하여 Request Body에 대한 유효성 검증 실패에 대한 에러 처리를 해야 되므로 각 Controller 클래스마다 코드 중복이 발생합니다.</p>
</li>
<li><p>Controller에서 처리해야 되는 예외가 유효성 검증 실패에 대한 예외(MethodArgumentNotValidException)만 있는 것이 아니기 때문에 하나의 Controller 클래스 내에서 @ExceptionHandler를 추가한 에러 처리 핸들러 메서드가 늘어납니다.</p>
</li>
</ol>
]]></description>
        </item>
    </channel>
</rss>