<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>dereck-jun.log</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Thu, 20 Mar 2025 17:56:57 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>dereck-jun.log</title>
            <url>https://velog.velcdn.com/images/dereck-jun/profile/fe7fa25e-3f1e-4246-ae8e-3b68572b960a/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. dereck-jun.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/dereck-jun" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[플러스 주차 트러블 슈팅]]></title>
            <link>https://velog.io/@dereck-jun/%ED%94%8C%EB%9F%AC%EC%8A%A4-%EC%A3%BC%EC%B0%A8-%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85</link>
            <guid>https://velog.io/@dereck-jun/%ED%94%8C%EB%9F%AC%EC%8A%A4-%EC%A3%BC%EC%B0%A8-%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85</guid>
            <pubDate>Thu, 20 Mar 2025 17:56:57 GMT</pubDate>
            <description><![CDATA[<h1 id="들어가기-전에">들어가기 전에</h1>
<p>벌써 최종 프로젝트가 눈 앞까지 왔다. 아직도 배워야 할 것이 많은데 벌써 최종 프로젝트라니.. 걱정이 조금 앞선다. 심지어 과제를 끝내고 난 뒤부터는 프로젝트만큼 열심히 하지 않는 것 같다. 마음만 급해지는게 아닌가 싶다.</p>
<p>개인 과제를 진행하면서 확실히 query 작성을 잘 못한다는 것을 깨닫게 되었다. 아무래도 팀 프로젝트마다 항상 유저 관련 도메인을 맡아서 하다보니 상대적으로 복잡한 쿼리를 만들 일이 없어서 그런 것 같다. 지금이라도 알아차렸으니 알고리즘 문제 풀이를 하면서 틈틈히 sql 문제도 풀어야 겠다고 느꼈다.</p>
<p>특히 기술도 기술이지만 기술을 원활하게 배우고 적절하게 사용하기 위해선 기반이 잘 되어 있어야 한다고 생각한다.. 지금까지 기술을 사용하기 위해서 열심히 달렸으니 프로젝트 이후부턴 비어있는 기본 지식들을 채워 넣는 것에 집중해야할 것 같다.</p>
<h1 id="트러블-슈팅">트러블 슈팅</h1>
<h2 id="코드-개선-퀴즈---jpa-이해">코드 개선 퀴즈 - JPA 이해</h2>
<p>JPQL을 사용해서 동적 쿼리를 만드는 문제였다. 생각보다 이 문제를 푸는 데 시간이 오래 걸렸다. 특히 <code>LocalDate</code>를 <code>LocalDateTime</code>으로 만드는 것과 JQPL 자체 로직 문제로 인해 대부분의 시간이 걸렸다.</p>
<p>결국 다음과 같이 해결했다.</p>
<h3 id="localdate---localdatetime으로-변환">LocalDate -&gt; LocalDateTime으로 변환</h3>
<pre><code class="language-java">LocalDateTime startDateTime = startDate != null ? startDate.atStartOfDay() : null;
LocalDateTime endDateTime = endDate != null ? endDate.atTime(23, 59, 59) : null;</code></pre>
<p><code>request</code>에서 빈 값이 들어오면 <code>null</code>로 보내고, 아니라면 해당 날짜의 시작과 끝으로 설정해서 보낸 뒤에 처리하게 했다.</p>
<h3 id="jpql-문제">JPQL 문제</h3>
<p>여긴 그냥 JPQL 자체를 잘못 작성해서 발생했다. 특히 괄호를 적절하게 사용하지 못해서 예외가 발생한 적도 있고, 값이 존재하는지 존재하지 않는지에 대해서 확인하는 로직은 <code>null</code>일 경우가 더 먼저(<code>=왼쪽</code>)있어야 한다는 것도 알게 되었다.</p>
<h2 id="spring-security-적용-후-테스트-코드-실패">Spring Security 적용 후 테스트 코드 실패</h2>
<p>시큐리티를 적용한 뒤에 같은 문제가 반복해서 발생했었다. 의존 주입에 대한 문제였고, <code>@Value</code>를 적은 곳에서 동시에 발생했다.</p>
<h3 id="import-사용">@Import 사용</h3>
<p>테스트 클래스에 <code>@Import({PropertyConfig.class, JwtUtil.class, JwtAuthenticationFilter.class})</code>를 추가해줬다.</p>
<blockquote>
<p>여담으로 <code>JwtAuthenticationFilter.class</code>는 없어도 잘 돌아갔다..</p>
</blockquote>
<h3 id="test-안의-resources-제거">test 안의 resources 제거</h3>
<p><code>Import</code>를 적용했는데도 같은 이유로 실패해서 또 삽질을 했다. <code>@Import</code>에 클래스를 넣었다가 뺐다가...</p>
<p><code>@Import</code>를 했는데도 같은 예외가 발생했었다. 결국 test 안에 만들어 둔 <code>resources</code> 폴더를 전부 삭제했었다. </p>
<p>의외로 그랬더니 성공했다.</p>
<p>아마 Config 주입에서 충돌이 일어난 것이 아닐까..</p>
<h2 id="querydsl-설정-문제">QueryDSL 설정 문제</h2>
<pre><code class="language-groovy">// querydsl
implementation &#39;com.querydsl:querydsl-jpa:5.0.0&#39;</code></pre>
<p>querydsl 내부 클래스들을 읽어오지 못했다. jakarta로 되어있던 것까지 확인했는데도 문제가 생겨서 검색을 해보니 위의 의존성 주입이 잘못되어 있던 것이었다.</p>
<pre><code class="language-groovy">// querydsl
implementation &#39;com.querydsl:querydsl-jpa:5.0.0:jakarta&#39;</code></pre>
<p>뒤에 버전을 명시하는 것처럼 jakarta를 붙여줬더니 해결됐다.</p>
<h2 id="대용량-데이터-처리">대용량 데이터 처리</h2>
<p>배치를 돌려서 값을 넣고 for 문으로 뒤에 현재 <code>i</code> 값을 뒤에 붙여서 닉네임과 이메일등을 만들도록 하면 된다는 것까진 알고 있었지만 막상 구현하려니 손이 움직이질 않아서 검색을 했다. 값을 넣는 방법은 많았지만 내가 원하는 답이 없어서 배치 설정을 하고 for 문을 돌려서 값을 넣었다.</p>
<p>하지만 1시간이 지나도 완료가 안돼서 확인해보니 로직이 꼬여서 백만 건 * 백만 건 그 이상이 발생하게 된 것이었다!</p>
<p>로직 정상화 이후 9분 32초만에 끝나게 되었다.</p>
<blockquote>
<p>여담으로 인덱싱만 했는데도 300ms에서 12ms로 줄어든 것을 보고 인덱싱에 대한 중요성을 느꼈다.</p>
</blockquote>
<h1 id="references">References</h1>
<p>깃허브 내 issue에 references를 최대한 작성했습니다.</p>
<ul>
<li><a href="https://github.com/dereck-jun/spring-plus/issues">https://github.com/dereck-jun/spring-plus/issues</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[값 타입]]></title>
            <link>https://velog.io/@dereck-jun/%EA%B0%92-%ED%83%80%EC%9E%85</link>
            <guid>https://velog.io/@dereck-jun/%EA%B0%92-%ED%83%80%EC%9E%85</guid>
            <pubDate>Tue, 04 Mar 2025 17:46:25 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>김영한님의 JPA 강의를 듣고 정리한 내용입니다.</p>
</blockquote>
<h1 id="값-타입">값 타입</h1>
<h2 id="jpa의-데이터-타입-분류">JPA의 데이터 타입 분류</h2>
<p>엔티티 타입</p>
<ul>
<li><code>@Entity</code>로 정의하는 객체</li>
<li>데이터가 변해도 식별자로 지속해서 추적 가능<ul>
<li>ex) 회원 엔티티의 필드 값을 변경해도 식별자로 인식할 수 있다.</li>
</ul>
</li>
</ul>
<p>값 타입</p>
<ul>
<li><code>int</code>, <code>Integer</code>, <code>String</code> 처럼 단순히 값으로 사용하는 자바 기본 타입이나 객체</li>
<li>식별자가 없고 값만 있으므로 변경 시 추적 불가<ul>
<li>ex) <code>int num = 10; -&gt; num = 20</code> 처럼 값을 변경하면 완전히 다른 값으로 대체됨</li>
</ul>
</li>
</ul>
<h2 id="값-타입-분류">값 타입 분류</h2>
<ul>
<li>기본 값 타입<ul>
<li>자바 기본 타입(<code>int</code>, <code>double</code>)</li>
<li>래퍼 클래스(<code>Integer</code>, <code>Long</code>)</li>
<li><code>String</code></li>
</ul>
</li>
<li>임베디드 타입(<code>embedded type</code>, 복합 값 타입)</li>
<li>컬렉션 값 타입(<code>collection value type</code>)</li>
</ul>
<h3 id="기본-값-타입">기본 값 타입</h3>
<ul>
<li>생명 주기를 엔티티에 의존한다<ul>
<li>ex) 회원을 삭제하면 안에 있는 필드도 함께 삭제</li>
</ul>
</li>
<li>값 타입은 공유하면 안됨<ul>
<li>ex) 회원 이름 변경 시 다른 회원의 이름도 함께 변경되면 안됨 (나쁜 의미로서의 사이드 이펙트)</li>
</ul>
</li>
</ul>
<blockquote>
<p>자바의 기본 타입은 절대 공유되지 않는다.</p>
<ul>
<li>기본 타입은 절대 공유되지 않는다.</li>
<li>기본 타입은 항상 값을 복사한다. (참조 X)</li>
<li>래퍼 클래스나 String 같은 특수한 클래스는 공유 가능한 객체지만 변경은 안된다 (값을 변경할 방법이 없음)</li>
</ul>
</blockquote>
<h3 id="임베디드-타입">임베디드 타입</h3>
<p>임베디드 타입은 엔티티의 값이다. 따라서 크게 의미를 가질 필요가 없다. </p>
<p>임베디드 타입 사용 전과 후에 <strong>매핑하는 테이블은 같다.</strong></p>
<p>객체와 테이블을 아주 세밀하게 매핑하는 것이 가능하다.</p>
<blockquote>
<p>잘 설계한 ORM 애플리케이션은 매핑한 테이블의 수보다 클래스의 수가 더 많다.</p>
</blockquote>
<br/>

<p>특징</p>
<ul>
<li>새로운 값 타입을 직접 정의할 수 있음</li>
<li>JPA는 임베디드 타입이라 한다.</li>
<li>주로 기본 값 타입을 모아서 만들어서 복합 값 타입이라고도 함</li>
<li><code>int</code>, <code>String</code>과 같은 값 타입이다.</li>
<li>임베디드 타입의 값이 <code>null</code>이면 매핑한 컬럼의 값은 모두 <code>null</code>이다.</li>
</ul>
<br/>

<p>장점</p>
<ul>
<li>재사용이 가능하다</li>
<li>클래스 내에서 응집도가 높다</li>
<li>해당 값 타입만 사용하는 의미 있는 메서드를 만들 수 있다</li>
<li>임베디드 타입을 포함한 모든 값 타입은, 값 타입을 소유한 엔티티에 생명 주기에 의존한다</li>
</ul>
<h4 id="사용법">사용법</h4>
<ul>
<li><code>@Embeddable</code>: 값 타입을 정의하는 곳에 표시</li>
<li><code>@Embedded</code>: 값 타입을 사용하는 곳에 표시</li>
<li>기본 생성자 필수</li>
</ul>
<br/>

<h4 id="임베디드-타입과-연관관계">임베디드 타입과 연관관계</h4>
<p>엔티티는 임베디드 타입의 값을 가질 수 있고, 임베디드 타입의 값은 임베디드 타입의 값이나 엔티티를 가질 수 있다.</p>
<pre><code>ENTITY -&gt; VALUE -&gt; VALUE   // 엔티티 -&gt; 임베디드 -&gt; 임베디드
ENTITY -&gt; VALUE -&gt; ENTITY  // 엔티티 -&gt; 임베디드 -&gt; 엔티티 </code></pre><p><code>@Column</code>을 사용해서 이름 변경 등도 가능</p>
<br/>

<h4 id="attributeoverride-속성-재정의">@AttributeOverride: 속성 재정의</h4>
<p>한 엔티티에서 같은 값 타입을 사용하게 되면 컬럼명이 중복되는 문제가 생긴다.</p>
<p>이때 사용할 수 있는 어노테이션이 <code>@AttributeOverrides</code>, <code>@AttributeOverride</code>이다.</p>
<p>아래는 <code>Address</code> 클래스를 동시에 사용하고 있을 때 <code>@AttributeOverrides</code>로 속성 재정의를 하는 방법이다.</p>
<pre><code class="language-java">@Entity
class Member {
    ...
    @Embedded
    private Address homeAddress;

    @Embedded
    @AttributeOverrides({
        @AttributeOverride(name = &quot;city&quot;, column = @Column(name = &quot;WORK_CITY&quot;)),
        @AttributeOverride(name = &quot;street&quot;, column = @Column(name = &quot;WORK_STREET&quot;)),
        @AttributeOverride(name = &quot;zipcode&quot;, column = @Column(name = &quot;WORK_ZIPCODE&quot;))
    })
    private Address workAddress;
}</code></pre>
<br/>


<h3 id="값-타입-컬렉션">값 타입 컬렉션</h3>
<ul>
<li>값 타입을 하나 이상 저장할 때 사용한다.<ul>
<li>셀렉트 박스처럼 단순하고, 추적할 필요도 없고, 값이 바뀌어도 <code>update</code> 할 필요가 없을 때 사용한다.</li>
</ul>
</li>
<li><code>@ElementCollection</code>, <code>@CollectionTable</code> 어노테이션을 사용해서 매핑하면 된다.</li>
<li>데이터베이스 컬렉션을 같은 테이블에 저장할 수 없다.<ul>
<li>컬렉션의 경우 일대다 개념이기 때문에 DB 안에 한 테이블로 컬렉션을 넣을 수 있는 방법이 없다.</li>
<li>따라서 별도의 테이블로 풀어내야 함</li>
<li>즉, 컬렉션을 저장하기 위한 별도의 테이블이 필요하다는 뜻임</li>
</ul>
</li>
<li>값 타입 컬렉션도 지연 로딩 전략을 사용한다.</li>
</ul>
<br/>

<blockquote>
<p>값 타입 컬렉션은 영속성 전이(CASCADE) + 고아 객체 제거 기능을 필수로 가진다고 볼 수 있다.</p>
</blockquote>
<br/>

<h4 id="매핑-방법">매핑 방법</h4>
<pre><code class="language-java">@Entity
class Member {
    ...

    @ElementCollection  
    @CollectionTable(name = &quot;FAVORITE_FOOD&quot;, joinColumns = 
        @JoinColumn(name = &quot;MEMBER_ID&quot;)
    )  
    @Column(name = &quot;FOOD_NAME&quot;)  
    private Set&lt;String&gt; favoriteFoods = new HashSet&lt;&gt;();  

    @ElementCollection  
    @CollectionTable(name = &quot;ADDRESS&quot;, joinColumns = @JoinColumn(name = &quot;MEMBER_ID&quot;))  
    private List&lt;Address&gt; addressHistory = new ArrayList&lt;&gt;();
}</code></pre>
<p><code>@ElementCollection</code>으로 컬렉션 값 타입이라는 것을 명시하고, <code>@CollectionTable</code>로 자동으로 생성할 컬렉션 테이블의 이름과 어떤 식별자와 조인해서 사용할 것인지를 넣어주면 된다.</p>
<p>이때 <code>@Column</code>을 사용해서 테이블에 들어갈 컬럼의 이름을 변경할 수 있다.</p>
<br/>

<h4 id="저장">저장</h4>
<pre><code class="language-java">Member member = new Member();
member.setName(&quot;member1&quot;);
member.setHomeAddress(new Address(&quot;homeCity&quot;, &quot;homeStreet&quot;, &quot;10001&quot;));

member.getFavoriteFoods().add(&quot;치킨&quot;);
member.getFavoriteFoods().add(&quot;피자&quot;);
member.getFavoriteFoods().add(&quot;탕수육&quot;);

member.getAddressHistory().add(new Address(&quot;oldCity1&quot;, &quot;st1&quot;, &quot;10002&quot;));
member.getAddressHistory().add(new Address(&quot;oldCity2&quot;, &quot;st2&quot;, &quot;10004&quot;));

em.persist(member);</code></pre>
<br/>

<h4 id="조회">조회</h4>
<p>저장 예제 코드에서 이어지는 부분이라고 생각하고 보자.</p>
<pre><code class="language-java">em.flush();
em.clear();

// 지연 로딩 전략으로 인해 Member만 조회됨
Member findMember = em.find(Member.class, member.getId());

// addressHistory 사용되는 시점에 로딩
List&lt;Address&gt; addressHistory = findMember.getAddressHistory();
for (Address address: addressHistory) {
    // 출력문
}

// favoriteFoods 사용되는 시점에 로딩
Set&lt;String&gt; favoriteFoods = findMember.getFavoriteFoods();
for (String favoriteFood : favoriteFoods) {
    // 출력문
}</code></pre>
<br/>

<h4 id="수정">수정</h4>
<p>저장 예제 코드에서 이어지는 부분이라고 생각하고 보자.</p>
<pre><code class="language-java">em.flush();
em.clear();

Member findMember = em.find(Member.class, member.getId());

// homeCity -&gt; newCity
// findMember.getHomeAddress().setCity(&quot;newCity&quot;); &lt;- 사이드 이펙트로 큰일날 수 있음
findMember.setHomeAddress(new Address(&quot;newCity&quot;, &quot;newStreet&quot;, &quot;12345&quot;));

// 치킨 -&gt; 제육볶음
findMember.getFavoriteFoods().remove(&quot;치킨&quot;);  // equals 재정의 안되어 있으면 안됨
findMember.getFavoriteFoods().add(&quot;제육볶음&quot;);</code></pre>
<p>다른 곳은 크게 어려움이 없는데 수정에서 알아둬야 하는 점이 조금 있다.</p>
<p>먼저 <code>homeAddress</code>를 수정하는 부분을 보자. 코드에 주석으로도 적혀 있지만 단순하게 <code>setter</code>로 값을 변경했다간 사이드 이펙트 문제가 생길 수도 있다. </p>
<p>따라서 객체를 항상 불변 객체로 만들어 놓고, 값을 변경할 땐 아예 새로운 인스턴스를 만들어서 넣어줘야 한다. 만약 이전 코드를 재사용하고 싶다면 저장할 때 <code>Address</code>를 <code>setHomeAddress()</code> 안에서 사용하지 말고, 따로 빼서 객체를 만들어 주고 넣어주면 된다.</p>
<pre><code class="language-java">Address address = new Address(...);

Member member = new Member();
member.setHomeAddress(address);

// 이후 변경 시
member.setHomeAddress(new Address(&quot;newCity&quot;, address.getStreet(), address.getZipcode()));</code></pre>
<p>그 다음은 <code>favoriteFoods</code> 안에 있는 값을 제거하고 새로 추가하는 부분이다. 여기서 <code>remove()</code>의 인자로 보내는 &quot;치킨&quot;이라는 값을 찾으려면 내부적으로 <code>equals()</code>가 동작을 하게 된다.</p>
<p>이때 <code>equals()</code>를 재정의 하지 않았을 경우 &quot;치킨&quot;이라는 값과 동등한 값을 찾지 못해서 삭제가 되지 않게 되니 꼭 재정의 해주도록 하자.</p>
<br/>

<h4 id="제약-사항">제약 사항</h4>
<p>값 타입은 엔티티와 다르게 식별자 개념이 없다. 식별자라는 것이 생기면 그때부턴 값 타입이 아닌 엔티티가 되기 때문이다.</p>
<p>값은 변경되면 추적이 어렵다. 그래서 사이드 이펙트 관련 버그를 찾아내기 힘들다.</p>
<p>값 타입 컬렉션에 변경 사항이 발생하면, 주인 엔티티와 연관된 모든 데이터를 삭제하고, 값 타입 컬렉션에 있는 현재 값을 모두 다시 저장해야 한다. 쉽게 말해 객체를 불변 객체로 만들어서 관리해야 한다는 뜻이다.</p>
<p>값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 기본 키를 구성해야 한다. 따라서 <code>null</code>값이 있거나 중복으로 저장하면 안된다. (<code>null</code> 제약, <code>unique</code> 제약)</p>
<br/>

<h4 id="값-타입-컬렉션-대안">값 타입 컬렉션 대안</h4>
<p>실무에서는 상황에 따라 값 타입 컬렉션 대신에 일대다 관계를 고려하는 것이 나을 수도 있다. 일대다 관계를 위한 엔티티를 만들고, 여기에 값 타입을 사용하는 것이다. </p>
<blockquote>
<p>&quot;엔티티로 wrapping 해서 값 타입을 엔티티로 승급시킨다&quot; </p>
</blockquote>
<pre><code class="language-java">@Entity
@Table(name = &quot;ADDRESS&quot;)
class AddressEntity {
    @Id @GeneratedValue
    private Long id;

    private Address address;
}

@Entity
class Member {
    ...
    @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
    @JoinColumn(name = &quot;MEMBER_ID&quot;)
    private List&lt;AddressEntity&gt; addressHistory = new ArrayList&lt;&gt;();
}</code></pre>
<br/>

<blockquote>
<p>값을 변경하지 않는다고 해도 DB의 쿼리 자체를 다른 쪽에서 시작해서 가지고 와야 하거나 하는 경우는 전부 엔티티라고 보면 된다.</p>
<ul>
<li>ex) 주소가 사라져도 이력이 남아야 한다. (엔티티)</li>
</ul>
</blockquote>
<br/>

<h2 id="값-타입의-비교">값 타입의 비교</h2>
<p>값 타입 비교를 위해선 먼저 동일성 비교와 동등성 비교의 차이를 알아야 한다.</p>
<ul>
<li>동일성(identity) 비교: 인스턴스의 참조 값을 비교, <code>==</code> 를 사용한다.</li>
<li>동등성(equivalence) 비교: 인스턴스의 값을 비교, <code>equals()</code>를 사용한다.</li>
</ul>
<p>값 타입은 동등성 비교를 해야하기 때문에 값 타입의 <code>equals()</code>를 적절하게 재정의해야 한다. 주로 모든 필드를 다 재정의해야 한다.</p>
<p>재정의를 하지 않고 <code>equals()</code> 사용 시 값이 같아도 <code>false</code>가 나온다.</p>
<ul>
<li><code>equals()</code>의 기본은 <code>==</code> 비교이기 때문</li>
</ul>
<pre><code class="language-java">// equals() 재정의 전
Address address1 = new Address(&quot;city&quot;, &quot;street&quot;, &quot;10001&quot;);
Address address2 = new Address(&quot;city&quot;, &quot;street&quot;, &quot;10001&quot;);

System.out.println(&quot;address1 equals address2: &quot;, (address1.equals(address2))); // false</code></pre>
<p>재정의 시에는 자동으로 만들어 주는 대로 하는 것이 좋고, 때에 따라선 값의 비교를 필드 접근이 아닌 <code>getter</code>로 호출하거나 <code>getClass()</code> 대신 <code>instanceof</code>를 사용해야 될 수도 있다.</p>
<pre><code class="language-java">@Override  
public boolean equals(Object o) {  
    if (this == o) return true;  
    if (o == null || getClass() != o.getClass()) return false;  
    Address address = (Address) o;  
    return Objects.equals(city, address.city) &amp;&amp; Objects.equals(street, address.street) &amp;&amp; Objects.equals(zipcode, address.zipcode);  
}

// 또는

@Override  
public boolean equals(Object o) {  
    if (this == o) return true;  
    if (o == null || getClass() != o.getClass()) return false;  
    Address address = (Address) o;  
    return Objects.equals(getCity(), address.getCity()) &amp;&amp; Objects.equals(getStreet(), address.getStreet()) &amp;&amp; Objects.equals(getZipcode(), address.getZipcode());  
}</code></pre>
<h1 id="references">References</h1>
<ul>
<li><a href="https://www.inflearn.com/course/ORM-JPA-Basic">https://www.inflearn.com/course/ORM-JPA-Basic</a></li>
<li><a href="https://velog.io/@gentledot/ddd-aggregate">https://velog.io/@gentledot/ddd-aggregate</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring 심화 주차 개인 과제]]></title>
            <link>https://velog.io/@dereck-jun/Spring-%EC%8B%AC%ED%99%94-%EC%A3%BC%EC%B0%A8-%EA%B0%9C%EC%9D%B8-%EA%B3%BC%EC%A0%9C</link>
            <guid>https://velog.io/@dereck-jun/Spring-%EC%8B%AC%ED%99%94-%EC%A3%BC%EC%B0%A8-%EA%B0%9C%EC%9D%B8-%EA%B3%BC%EC%A0%9C</guid>
            <pubDate>Thu, 27 Feb 2025 02:24:45 GMT</pubDate>
            <description><![CDATA[<h1 id="들어가기-전에">들어가기 전에</h1>
<p>처음으로 과제를 마무리하는데 시간이 남지 않았던 것 같다. 특히 AOP를 구현하기 위해서 기본 개념을 공부하느라 시간을 좀 많이 잡아먹었고, 계속 조느라 집중을 잘 못한 것 같다. </p>
<p>결국 5단계와 6단계 중 하날 더 신경써서 하는 방법 밖에 없었는데, 테스트 코드가 더 중요하지 않을까 싶어서 테스트 코드에 더 시간을 많이 쏟아서 5단계를 제대로 마무리하지 못한 것 같아 아쉽다.</p>
<p>앞으로 팀 프로젝트를 하면서 다음 자격증 공부를 병행해야 하는데 잘할 수 있을지 모르겠다..</p>
<h1 id="lv-5-해결-과정">Lv 5 해결 과정</h1>
<h2 id="a-도메인의-서비스가-b-도메인의-리포지토리를-의존">A 도메인의 서비스가 B 도메인의 리포지토리를 의존</h2>
<ol>
<li>[문제 인식 및 정의]<ul>
<li>A 도메인의 서비스가 B 도메인의 리포지토리를 의존하고 있음</li>
</ul>
</li>
<li>[해결 방안]<ol>
<li>[의사결정 과정]<ul>
<li>순환 참조 오류를 피할 순 있으나 단일 책임 원칙을 준수하는 것이 둘 중 더 중요하다고 판단.</li>
</ul>
</li>
<li>[해결 과정]<ul>
<li>각 서비스를 Write/Read로 나눠서 순환 참조를 피함과 동시에 타 도메인의 서비스를 의존하도록 바꿈</li>
</ul>
</li>
<li>[trade-off]<ul>
<li>유저를 찾을 수 없을 때 어떤 유저를 찾을 수 없는지에 대한 자세한 처리가 어려워 진다. (자세한 예외 처리를 할 수 없다)<ul>
<li><code>등록하려고 하는 담당자 유저가 존재하지 않습니다.</code> → <code>유저를 찾을 수 없습니다.</code></li>
</ul>
</li>
</ul>
</li>
</ol>
</li>
<li>[해결 완료]<ol>
<li>[회고]<ul>
<li>이제 각 도메인의 Repository는 본인의 Service 들과만 의존해서 단일 책임 원칙을 지킬 수 있게 되었다.
<img src="https://velog.velcdn.com/images/dereck-jun/post/8d8fe5e0-601d-45f2-a0c4-b3723a2a27bb/image.png" alt=""></li>
</ul>
</li>
</ol>
</li>
</ol>
<h2 id="회원-탈퇴-기능이-미구현">회원 탈퇴 기능이 미구현</h2>
<ol>
<li>[문제 인식 및 정의]<ul>
<li>회원 탈퇴 기능이 없음</li>
</ul>
</li>
<li>[해결 방안]<ol>
<li>[의사결정 과정]<ul>
<li>탈퇴 기능이 없어도 기능(서비스를 이용하고, 이용하지 않는 것) 문제는 없으나 앞으로 이용하지 않는 사용자의 경우엔 탈퇴시키는 것이 앞으로의 관리 측면에서 더 나을 것 같다고 판단</li>
</ul>
</li>
<li>[해결 과정]<ul>
<li>UserController와 UserService에 <code>deleteUser()</code> 추가</li>
</ul>
</li>
</ol>
</li>
<li>[해결 완료]<ol>
<li>[회고]<ul>
<li>회원 탈퇴가 생기면서 탈퇴를 원하는 사용자의 경우 탈퇴를 함으로써 (현재는 soft-delete가 적용되어 있지 않기 때문에) 탈퇴 이후 해당 사용자의 정보를 관리할 필요가 없어졌다.</li>
</ul>
</li>
<li>[전후 데이터 비교]<pre><code>![](https://velog.velcdn.com/images/dereck-jun/post/3cb071fe-9085-4a3c-aa5e-9009747f3ea2/image.png)</code></pre></li>
</ol>
</li>
</ol>
<h2 id="timestamped의-각-필드가-nullable-함">Timestamped의 각 필드가 nullable 함.</h2>
<ol>
<li>[문제 인식 및 정의]<ul>
<li>Timestamped의 각 필드가 nullable 함.</li>
</ul>
</li>
<li>[해결 방안]<ol>
<li>[의사결정 과정]<ul>
<li>어차피 JPA가 자동으로 해당 값을 채워주는데 굳이 nullable 하게 둘 필요가 없다고 판단함.</li>
<li>또한 null 허용은 보수적으로 적용하는 것이 맞다고 생각함.</li>
</ul>
</li>
<li>[해결 과정]<ul>
<li><code>nullable = false</code>를 추가</li>
</ul>
</li>
</ol>
</li>
<li>[해결 완료]<ol>
<li>[회고]<ul>
<li>이전보다 null 값에 대해 안전해졌다. </li>
<li>테스트 코드 상에서의 불편함이 생길 수도 있을 것 같다.</li>
</ul>
</li>
<li>[전후 데이터 비교]
<img src="https://velog.velcdn.com/images/dereck-jun/post/98b53a39-4d54-4afb-90d1-f6fb1d05bd51/image.png" alt=""></li>
</ol>
</li>
</ol>
<h2 id="예외-발생-시-메시지의-언어가-통일되지-않음">예외 발생 시 메시지의 언어가 통일되지 않음</h2>
<ol>
<li>[문제 인식 및 정의]<ul>
<li>예외 발생 시 메시지의 언어가 통일되지 않음</li>
</ul>
</li>
<li>[해결 방안]<ol>
<li>[의사결정 과정]<ul>
<li>한국어로 통일하고, 이후 필요한 순간에 메시지 국제화를 통해 여러 언어로 사용할 수 있도록 만드는 것이 나을 것 같다고 판단함.</li>
</ul>
</li>
<li>[해결 과정]<ul>
<li>영어로 적힌 모든 에러 메시지를 최대한 같은 의미의 한국어로 치환.</li>
</ul>
</li>
</ol>
</li>
<li>[해결 완료]<ol>
<li>[회고]<ul>
<li>이후 메시지 국제화 적용이 필요할 것 같음</li>
<li>문자열로 관리하는 것이 아닌 Enum으로 관리하는 것이 더 편할 것 같고, 테스트에서도 유용하게 사용될 것 같음</li>
</ul>
</li>
</ol>
</li>
</ol>
<h2 id="일정-수정과-삭제가-미구현">일정 수정과 삭제가 미구현</h2>
<ol>
<li>[문제 인식 및 정의]<ul>
<li>일정 수정과 삭제가 구현되어 있지 않음</li>
</ul>
</li>
<li>[해결 방안]<ol>
<li>[의사결정 과정]<ul>
<li>일정은 언제든지 바뀔 수 있고, 사라질 수도 있기 때문에 수정과 삭제 기능이 구현되어 있어야 한다고 생각함.</li>
</ul>
</li>
<li>[해결 과정]<ul>
<li>일정 수정과 삭제 구현</li>
</ul>
</li>
</ol>
</li>
<li>[해결 완료]<ol>
<li>[회고]<ul>
<li>일정 수정과 삭제가 생겨서 더 이상 오타, 완료, 불발된 일정을 수정하거나 삭제할 수 있게 되면서 깔끔하게 관리할 수 있게 되었다.<ul>
<li>우선 순위에 따라 할 일을 조회하고, 할 일을 마치면 완료 상태로 바꾸는 등의 추가 기능도 구현할 수 있을 것 같다.</li>
</ul>
</li>
</ul>
</li>
<li>[전후 데이터 비교]
 <img src="https://velog.velcdn.com/images/dereck-jun/post/d45a1955-0dd2-4cd5-8668-462129681423/image.png" alt=""></li>
</ol>
</li>
</ol>
<h2 id="예외-발생-시-메시지가-어울리지-않음">예외 발생 시 메시지가 어울리지 않음</h2>
<ol>
<li><p>[문제 인식 및 정의]</p>
<ul>
<li>lv 3-2의 3번 케이스 해결 시 <code>todo.getUser()</code> 의 값이 <code>null</code> 일 경우 발생하는 예외 발생 시 메시지가 어울리지 않음.</li>
</ul>
</li>
<li><p>[해결 방안]</p>
<ol>
<li>[의사결정 과정]<ul>
<li>해당 할 일을 만든 유저가 없을 경우 발생하는 것이기 때문에 &quot;담당자를 등록하려고 하는 유저와 일정을 만든 유저가 유효하지 않습니다.”보단 “일정을 만든 유저를 찾을 수 없습니다.” 등의 메시지가 더 어울리는 것 같다고 생각</li>
</ul>
</li>
<li>[해결 과정]<ul>
<li>메시지 수정</li>
</ul>
</li>
</ol>
</li>
<li><p>[해결 완료]</p>
<ol>
<li>[회고]<ul>
<li>상황에 더 알맞는 예외 메시지를 보낼 수 있게 되었다.</li>
</ul>
</li>
<li>[전후 데이터 비교]
 <img src="https://velog.velcdn.com/images/dereck-jun/post/e8931ab8-e955-4dcc-9dfe-3e520fe55266/image.png" alt=""></li>
</ol>
</li>
</ol>
<h2 id="입력-값-검증">입력 값 검증</h2>
<ol>
<li>[문제 인식 및 정의]<ul>
<li>회원가입 시 비밀번호 조건이 없고, <code>@Email</code> 어노테이션의 경우 너무 허술함 (<code>@</code> 만 있으면 통과)</li>
</ul>
</li>
<li>[해결 방안]<ol>
<li>[의사결정 과정]<ul>
<li>회원가입의 경우엔 비밀번호 변경과 마찬가지로 조건을 명시해줘야 한다고 생각함.</li>
<li><code>@Email</code> 어노테이션의 경우 단순히 <code>@</code> 만 적으면 조건을 만족하기 때문에 더욱 정교한 이메일 형식 적용을 위해 <code>@Pattern</code>을 사용해서 커스텀으로 만들어 줘야 겠다고 생각함.</li>
</ul>
</li>
<li>[해결 과정]<ul>
<li>비밀번호 변경과 동일한 패턴 적용</li>
<li>이메일의 경우<ul>
<li>로컬 파트는 영문 대소문자, 숫자, 하이픈, 언더스코어 중 하나 이상을 포함해야 하고, 온점이나 기타 특수문자는 허용하지 않음</li>
<li>도메인 부분은 영문 대소문자, 숫자, 온점 및 하이픈을 사용할 수 있음.</li>
<li>마지막 온점 뒤에 2자 이상 6자 이하의 영문만 허용</li>
</ul>
</li>
</ul>
</li>
</ol>
</li>
<li>[해결 완료]<ol>
<li>[회고]<ul>
<li>더 정확한 수준의 이메일 검증이 가능하게 되었다.</li>
</ul>
</li>
<li>[전후 데이터 비교]
<img src="https://velog.velcdn.com/images/dereck-jun/post/0d59a5bb-bf0d-4f34-9d7f-956ee027a96a/image.png" alt=""></li>
</ol>
</li>
</ol>
<h2 id="controller의-path-구조-변경">Controller의 Path 구조 변경</h2>
<ol>
<li>[문제 인식 및 정의]<ul>
<li>CommentController 등의 requestUri가 계층적인 구조를 띄고 있음. 이런 경우라면 TodoController 쪽에 같이 위치하는 것이 나을 것 같음.</li>
</ul>
</li>
<li>[해결 방안]<ol>
<li>[의사결정 과정]<ul>
<li>하지만 단일 책임 원칙을 기준으로 바라본다면 이는 CommentController에 위치하는 것이 맞다고 생각함.</li>
<li>Manager 도메인도 같은 맥락으로 수정함</li>
</ul>
</li>
<li>[해결 과정]<ul>
<li>RequestMapping으로 <code>&quot;/comments&quot;</code> 를 공통으로 받게 다시 설계<ul>
<li>생성: <code>/comments</code> (RequestDto에 todoId를 추가로 받도록 함)</li>
<li>조회: <code>/comments?todoId={todoId}</code> (RequestParam으로 받도록 함)</li>
<li>수정: <code>/comments/{commentId}</code></li>
<li>삭제: <code>/comments/{commentId}</code></li>
</ul>
</li>
</ul>
</li>
</ol>
</li>
<li>[해결 완료]<ol>
<li>[회고]<ul>
<li>일정에 병합되어야 할 것 같던 path가 댓글 중심으로 바뀌면서 명확해진 것 같다. (다른 도메인도 마찬가지)</li>
</ul>
</li>
</ol>
</li>
</ol>
<h2 id="persistanceconfig-클래스의-존재-의의">PersistanceConfig 클래스의 존재 의의</h2>
<ol>
<li>[문제 인식 및 정의]<ul>
<li>PersistanceConfig가 단순히 <code>@EnableJpaAuditing</code>을 설정하기 위해서 존재함</li>
</ul>
</li>
<li>[해결 방안]<ol>
<li>[의사결정 과정]<ul>
<li>실행 클래스에 적용해도 되는 것을 굳이 클래스를 더 만들어서 관리할 필요가 없다고 판단함</li>
</ul>
</li>
<li>[해결 과정]<ul>
<li>실행 클래스에 해당 어노테이션을 적용하고 PersistanceConfig 클래스 제거</li>
</ul>
</li>
</ol>
</li>
<li>[해결 완료]<ol>
<li>[회고]<ul>
<li>의미 없는 클래스를 하나 줄일 수 있게 되었다.</li>
<li>(추가) 테스트 코드 작성 시 실행 클래스에 <code>@EnableJpaAuditing</code>을 붙이게 되면 추가적인 작업이 필요하다는 것을 알게 되었다.</li>
</ul>
</li>
<li>[전후 데이터 비교]
<img src="https://velog.velcdn.com/images/dereck-jun/post/47973208-97e2-4201-84c2-ed5dd5f2dc99/image.png" alt="">
<img src="https://velog.velcdn.com/images/dereck-jun/post/89d33ea9-ec3b-4d76-ba2d-30f3a3f3ee7d/image.png" alt=""></li>
</ol>
</li>
</ol>
<h2 id="입력-값-예외-처리-시-불필요한-내용까지-응답">입력 값 예외 처리 시 불필요한 내용까지 응답</h2>
<ol>
<li>[문제 인식 및 정의]<ul>
<li>GlobalExceptionHandler에서 <code>@Valid</code>를 잡을 때의 메시지에 필요없는 내용까지 출력되고 있었음</li>
</ul>
</li>
<li>[해결 방안]<ol>
<li>[의사결정 과정]<ul>
<li>응답을 받을 때 필요한 내용만 받을 수 있도록 조치가 필요하다고 판단함.</li>
</ul>
</li>
<li>[해결 과정]<ul>
<li><code>getBindingResult().getFieldError().getDefaultMessage()</code> 를 통해 기본 메시지 또는 명시적으로 입력한 메시지를 출력할 수 있도록 변경함</li>
</ul>
</li>
</ol>
</li>
<li>[해결 완료]<ol>
<li>[회고]<ul>
<li>응답 메시지에 불필요한 내용을 제거해서 서버의 비용을 줄이고, 더 명확한 예외 메시지를 보낼 수 있게 되었다.</li>
</ul>
</li>
<li>[전후 데이터 비교]
<img src="https://velog.velcdn.com/images/dereck-jun/post/d48ad9b5-ab07-48f5-a06e-32829bea36b8/image.png" alt=""></li>
</ol>
</li>
</ol>
<h2 id="config-폴더-정리">config 폴더 정리</h2>
<ol>
<li>[문제 인식 및 정의]<ul>
<li>config 폴더에 설정이 아닌 다른 종류의 클래스도 존재함</li>
</ul>
</li>
<li>[해결 방안]<ol>
<li>[의사결정 과정]<ul>
<li>각 클래스의 종류나 역할에 따라 리팩토링이 필요하다고 판단함</li>
</ul>
</li>
<li>[해결 과정]<ul>
<li>클래스가 사용되는 곳이나 역할에 따라 리팩토링을 진행함</li>
</ul>
</li>
</ol>
</li>
<li>[해결 완료]<ol>
<li>[회고]<ul>
<li>config엔 설정 관련 클래스만 위치하도록 해서 필요한 클래스를 더 빠르게 찾을 수 있게 된 것 같다. (가시성의 증가)</li>
</ul>
</li>
<li>[전후 데이터 비교]
<img src="https://velog.velcdn.com/images/dereck-jun/post/988eb712-cfc8-4464-89f7-af37a99d1078/image.png" alt=""></li>
</ol>
</li>
</ol>
<h1 id="트러블-슈팅">트러블 슈팅</h1>
<h2 id="요청-본문-로깅-처리-하기">요청 본문 로깅 처리 하기</h2>
<p>AOP를 사용해서 Lv4 과제를 해결하는 도중 요청 본문에 대한 로그를 찍어야 했었다. 처음엔 필터를 따로 만들어서 순서를 올리면 될까 싶었지만 해결되지 않았었다. </p>
<p>결국 현재 있는 필터에서 가장 빨리 요청을 받는 <code>JwtFilter</code>에 request를 재사용하기 위해 ContentCachingRequestWrapper로 감싸준 뒤 보내줬다.
<img src="https://velog.velcdn.com/images/dereck-jun/post/8168f552-a8b3-496e-a5a5-b7eabfc9f122/image.png" alt=""></p>
<p>&quot;재사용하기 위해&quot;라고 말한 이유는 요청 본문을 <code>getInputStream()</code>은 일회용이기 때문에 요청 로그를 찍기 위해선 말 그대로 재사용을 해야 하기 때문이다.</p>
<p>이후 LogTraceAspect 클래스에서 현재 요청 정보를 스레드 로컬 방식으로 저장하기 위해 아래와 같이 적용해줬다. 또한 execution을 사용한 포인트컷보다 커스텀 어노테이션을 만들어서 적용하는 편이 더 간편할 것 같아서 어노테이션이 있는 곳에 어드바이스를 적용하도록 했다.</p>
<p><img src="https://velog.velcdn.com/images/dereck-jun/post/7d9179b0-4391-4a45-8c4f-ebdeb950bee6/image.png" alt=""></p>
<p>AOP를 적용한 전체 코드는 다음과 같다.</p>
<pre><code class="language-java">@Slf4j
@Aspect
@Component
public class LogTraceAspect {

    @Pointcut(&quot;@annotation(org.example.expert.domain.common.annotation.LogTrace)&quot;)
    public void loggerPointcut() {
    }

    @Around(&quot;loggerPointcut()&quot;)
    public Object doLogTrace(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
        final ContentCachingRequestWrapper cachingRequest = (ContentCachingRequestWrapper) request;

        String traceId = UUID.randomUUID().toString().substring(0, 8);
        String requestBody = getRequestBody(cachingRequest);
        if (!StringUtils.hasText(requestBody)) {
            requestBody = &quot;empty&quot;;
        }
        String method = cachingRequest.getMethod();
        LocalDateTime requestTime = LocalDateTime.now();

        // setAttribute()로 저장한 값을 읽어옴
        Long userId = (Long) cachingRequest.getAttribute(&quot;userId&quot;);

        log.info(toRequestLog(traceId, method, userId, cachingRequest.getRequestURL(), requestTime, requestBody));

        Object result = proceedingJoinPoint.proceed();  // proxy 객체가 target 객체의 메서드를 호출하고 나온 result
        if (!StringUtils.hasText((CharSequence) result)) {
            result = &quot;empty&quot;;
        }

        log.info(toResponseLog(traceId, method, cachingRequest.getRequestURL(), userId, requestTime, result));
        return result;
    }

    // 요청 본문 읽어오기
    private String getRequestBody(ContentCachingRequestWrapper cachingRequest) {
        String requestBody = &quot;&quot;;
        try {
            requestBody = new String(cachingRequest.getContentAsByteArray(), cachingRequest.getCharacterEncoding());
        } catch (UnsupportedEncodingException uee) {
            throw new InvalidRequestException(&quot;Logging 과정에서 에러가 발생했습니다.&quot;);
        }
        return requestBody;
    }

    public String toRequestLog(
        String traceId,
        String method,
        Long userId,
        StringBuffer requestUrl,
        LocalDateTime requestTime,
        String requestBody
    ) {
        return String.format(
            &quot;%n========== HTTP REQUEST LOG ==========%n&quot; +
                &quot;Trace ID        : %s%n&quot; +
                &quot;HTTP Method     : %s%n&quot; +
                &quot;Request URI     : %s%n&quot; +
                &quot;Request User ID : %s%n&quot; +
                &quot;Request Time    : %s%n&quot; +
                &quot;Request Body    : %s%n&quot; +
                &quot;======================================&quot;,
            traceId, method, requestUrl, userId, requestTime, requestBody
        );
    }

    public String toResponseLog(
        String traceId,
        String method,
        StringBuffer requestUrl,
        Long userId,
        LocalDateTime requestTime,
        Object result
    ) {
        return String.format(
            &quot;%n========== HTTP RESPONSE LOG ==========%n&quot; +
                &quot;Trace ID        : %s%n&quot; +
                &quot;HTTP Method     : %s%n&quot; +
                &quot;Request URI     : %s%n&quot; +
                &quot;Request User ID : %s%n&quot; +
                &quot;Request Time    : %s%n&quot; +
                &quot;Response Body   : %s%n&quot; +
                &quot;======================================&quot;,
            traceId, method, requestUrl, userId, requestTime, result
        );
    }
}</code></pre>
<p>요청부터 응답까지 걸린 시간도 넣고 싶었지만 그건 필터에 들어왔을 때부터 나가기 직전까지를 구하는 것이 아니면 의미가 없다고 생각해서 제외하게 되었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[영속성 전이와 고아 객체]]></title>
            <link>https://velog.io/@dereck-jun/%EC%98%81%EC%86%8D%EC%84%B1-%EC%A0%84%EC%9D%B4%EC%99%80-%EA%B3%A0%EC%95%84-%EA%B0%9D%EC%B2%B4</link>
            <guid>https://velog.io/@dereck-jun/%EC%98%81%EC%86%8D%EC%84%B1-%EC%A0%84%EC%9D%B4%EC%99%80-%EA%B3%A0%EC%95%84-%EA%B0%9D%EC%B2%B4</guid>
            <pubDate>Tue, 25 Feb 2025 17:24:04 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>김영한님의 JPA 강의를 듣고 정리한 내용입니다.</p>
</blockquote>
<h1 id="영속성-전이와-고아-객체">영속성 전이와 고아 객체</h1>
<h2 id="영속성-전이---cascade">영속성 전이 - CASCADE</h2>
<p>특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 만들고 싶을 때 사용한다. 하나의 부모가 자식을 관리할 때 주로 사용한다.</p>
<ul>
<li>ex) 부모 엔티티를 저장할 때 자식 엔티티도 함께 저장<ul>
<li>게시판에 첨부파일 경로 -&gt; 한 게시판에서만 관리를 함</li>
</ul>
</li>
</ul>
<p>다음과 같은 경우에 사용한다.</p>
<ol>
<li><strong>부모와 자식의 라이프 사이클이 거의 동일할 때</strong></li>
<li><strong>단일 엔티티에 완전히 종속적일 때</strong></li>
</ol>
<p>연관관계 매핑 속성으로 <code>cascade</code>를 추가해서 설정할 수 있다.</p>
<ul>
<li>ex) <code>OneToMany(mappedBy = &quot;...&quot;, cascade = CascadeType.ALL)</code></li>
</ul>
<p>CASCADE는 다음과 같은 속성이 있다.</p>
<ul>
<li>ALL: 모든 CASCADE를 적용</li>
<li>PERSIST: 엔티티를 영속화할 때, 연관된 엔티티도 함께 유지</li>
<li>REMOVE: 엔티티를 제거할 때, 연관된 엔티티도 모두 제거</li>
<li>MERGE: 엔티티 상태를 병합할 때, 연관된 엔티티도 모두 병합</li>
<li>REFRESH: 상위 엔티티를 새로고침할 때, 연관된 엔티티도 모두 새로고침</li>
<li>DETACH: 부모 엔티티를 준영속화하면, 연관 엔티티도 준영속화 시킴</li>
</ul>
<h3 id="주의">주의</h3>
<p>영속성 전이는 연관관계를 매핑하는 것과 아무 관련이 없다. 엔티티를 영속화할 때 연관된 엔티티도 함께 영속화하는 편리함을 제공할 뿐이다.</p>
<p>위에 설명한 것과 같이 소유자가 하나일 때만 사용해야 한다. 소유자가 둘 이상일 때 사용하면 의도하지 않은 방향으로 전이될 수 있음.</p>
<ul>
<li>ex) 부모 A, B에 자식 C가 있고, <code>A-C</code>, <code>B-C</code>로 <code>CASCADE</code> 되어 있을 때 <code>A</code> 삭제 시 <code>C</code>까지 삭제되어 의도치 않게 <code>B</code>에 연결되어있는 <code>C</code> 값이 <code>null</code>로 바뀌게 되는 참사 발생</li>
</ul>
<h2 id="고아-객체">고아 객체</h2>
<p>부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제하는 기능이다.</p>
<p><code>orphanRemoval</code> 속성으로 자동 삭제 여부를 결정할 수 있다.</p>
<pre><code class="language-java">@Entity
class Parent {
    @Id @GeneratedValue
    private Long id;
    private String name;

    @OneToMany(..., orphanRemoval = true)
    @JoinColumn(name = &quot;parent&quot;)
    private List&lt;Child&gt; children = new ArrayList&lt;&gt;();
    ...
}</code></pre>
<p><code>orphanRemoval</code> 속성이 켜져있을 경우 <code>List</code>에서 값이 사라지면 해당 자식 엔티티도 삭제된다.</p>
<pre><code class="language-java">Child child1 = new Child();  
child1.setName(&quot;child1&quot;);  

Child child2 = new Child();  
child2.setName(&quot;child2&quot;);  

Parent parent = new Parent();  
parent.setName(&quot;parent1&quot;);  
parent.addChild(child1);  
parent.addChild(child2);  

em.persist(parent);  
em.flush();  
em.clear();  

List&lt;Child&gt; children1 = parent.getChildren(); 

for (Child child : children1) {  
    System.out.println(&quot;child = &quot; + child.getName());  // 1
}  

Parent findParent = em.find(Parent.class, parent.getId());  
findParent.getChildren().remove(0);  
List&lt;Child&gt; children2 = findParent.getChildren();  

for (Child child : children2) {  
    System.out.println(&quot;child = &quot; + child.getName());  // 2
}</code></pre>
<p>이 경우 <code>1</code>번에선 2개의 항목이 나오지만 <code>2</code>번에선 <code>index = 0</code>에 해당하는 객체가 삭제돼서 1개의 항목만 나온다.</p>
<h3 id="주의-1">주의</h3>
<p>참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 보고 삭제하는 기능이기 때문에 참조하는 곳이 하나일 때 사용하거나 특정 엔티티가 개인 소유할 때 사용한다.</p>
<p><code>@OneToMany</code> 또는 <code>@OneToOne</code>만 사용 가능하다.</p>
<p>개념적으로 부모를 제거하면 자식은 고아가 된다. 따라서 고아 객체 제거 기능을 활성화 하면, 부모를 제거할 때 자식도 함께 제거된다. 이것은 <code>CascadeType.REMOVE</code>처럼 동작한다. </p>
<h2 id="영속성-전이--고아-객체의-생명주기">영속성 전이 + 고아 객체의 생명주기</h2>
<p>영속성 전이와 고아 객체를 더하는 것은 <code>cascade = CascadeType.ALL</code> + <code>orphanRemoval = true</code>의 의미이다.</p>
<p>위의 둘을 전부 켜게 되면 다음과 같은 의미를 지닌다.</p>
<ul>
<li>스스로 생명주기를 관리하는 엔티티는 <code>em.persist()</code>로 영속화, <code>em.remove()</code>로 제거한다. </li>
<li>두 옵션을 전부 켰기 때문에 부모 엔티티를 통해 자식의 생명주기를 관리할 수 있다.</li>
<li>도메인 주도 설계(DDD)의 Aggregate Root 개념을 구현할 때 유용하다.</li>
</ul>
<blockquote>
<p>참고: <a href="https://velog.io/@gentledot/ddd-aggregate">도메인 주도 설계 (3) - 애그리거트</a></p>
</blockquote>
<h1 id="references">References</h1>
<ul>
<li><a href="https://www.inflearn.com/course/ORM-JPA-Basic">https://www.inflearn.com/course/ORM-JPA-Basic</a></li>
<li><a href="https://velog.io/@gentledot/ddd-aggregate">https://velog.io/@gentledot/ddd-aggregate</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[지연 로딩과 즉시 로딩]]></title>
            <link>https://velog.io/@dereck-jun/%EC%A7%80%EC%97%B0-%EB%A1%9C%EB%94%A9%EA%B3%BC-%EC%A6%89%EC%8B%9C-%EB%A1%9C%EB%94%A9</link>
            <guid>https://velog.io/@dereck-jun/%EC%A7%80%EC%97%B0-%EB%A1%9C%EB%94%A9%EA%B3%BC-%EC%A6%89%EC%8B%9C-%EB%A1%9C%EB%94%A9</guid>
            <pubDate>Mon, 24 Feb 2025 12:06:47 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>김영한님의 JPA 강의를 보고 정리한 내용입니다.</p>
</blockquote>
<h1 id="지연-로딩과-즉시-로딩">지연 로딩과 즉시 로딩</h1>
<h2 id="지연-로딩">지연 로딩</h2>
<p>지연 로딩의 경우 연관관계 매핑 어노테이션에 <code>fetch = FetchType.LAZY</code> 속성을 추가해서 선언해준다. 연관관계 어노테이션 중 <code>*ToMany</code>에 해당하는 어노테이션의 기본 값이다.</p>
<ul>
<li>ex) <code>@OneToMany(fetch = FetchType.LAZY)</code></li>
</ul>
<p>지연 로딩을 사용하게 되면 <strong>객체를 DB가 아닌 프록시</strong>에서 가져온다. 프록시에서 객체를 가져올 경우 <strong>실제로 사용(초기화)되기 전까지 조회 쿼리를 사용하지 않는다.</strong></p>
<p>예를 들어 <code>Member</code>와 <code>Team</code> 클래스가 있고, <code>Member</code>에 <code>@ManyToOne(fetch = FetchType.LAZY)</code>로 <code>Team</code>과 연관관계를 맺고 있다고 가정해보자.</p>
<pre><code class="language-java">@Entity
class Member {
    @Id @GeneratedValue
    private Long id;
    private String name;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = &quot;team_id&quot;)
    private Team team;
    ...
}</code></pre>
<p>지연 로딩의 경우 <code>Member</code> 객체를 저장하고, <code>member</code>를 조회하게 되면 <code>member</code>는 DB에서 조회를 하고, 조회된 <code>member</code>안의 <code>Team</code> 객체는 프록시로 가져온다.</p>
<p>그리고 <code>Team</code>을 실제로 사용할 때 DB에서 조회 후 가져오게 된다.</p>
<p><code>member</code> 객체를 조회할 때 굳이 <code>team</code> 객체를 조회할 필요가 없으므로 DB에 쿼리가 한 번만 나가게 돼서 성능상의 이점을 얻는다.</p>
<p>하지만 지연 로딩 역시 후에 서술할 <code>N+1</code>의 문제가 있다</p>
<h2 id="즉시-로딩">즉시 로딩</h2>
<p>즉시 로딩의 경우 연관관계 매핑 어노테이션에 <code>fetch = FetchType.EAGER</code> 속성을 추가해서 선언해준다. 연관관계 어노테이션 중 <code>*ToOne</code>에 해당하는 어노테이션의 기본 값이다.</p>
<ul>
<li>ex) <code>@ManyToOne(fetch = FetchType.EAGER)</code></li>
</ul>
<p>즉시 로딩을 사용하면 무조건 조회하는 시점에 필요한 값들이 모두 들어가 있어야 한다.</p>
<p>예를 들어 <code>Member</code>와 <code>Team</code> 클래스가 있고, <code>Member</code>에 <code>@ManyToOne(fetch = FetchType.EAGER)</code>로 <code>Team</code>과 연관관계를 맺고 있다고 가정해보자.</p>
<pre><code class="language-java">@Entity
class Member {
    @Id @GeneratedValue
    private Long id;
    private String name;

    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = &quot;team_id&quot;)
    private Team team;
    ...
}</code></pre>
<p>즉시 로딩의 경우 <code>Member</code> 객체를 저장하고, <code>member</code>를 조회하게 되면 <code>Team</code> 객체의 사용 여부와 관계없이 조회 시점에 <code>team</code>이 무조건 들어가 있어야 하기 때문에 조회 시 두 엔티티를 조인해서 가져온다.</p>
<h2 id="어떤-방법을-사용해야-할까">어떤 방법을 사용해야 할까?</h2>
<p>비즈니스 로직에서 두 객체가 동시에 사용되는 빈도가 높다면 즉시 로딩을 사용할 것 같다. 하지만 그것은 어디까지나 이론에서 적용되는 것이고, 실제 실무에선 <strong>무조건 지연 로딩을 사용</strong>한다고 한다.</p>
<ul>
<li>즉시 로딩 사용 시 예상치 못한 SQL 문제 발생</li>
<li>즉시 로딩은 JPQL에서 <code>N+1</code>의 문제를 발생시킴<ul>
<li>다음과 같은 JPQL을 사용했다고 가정<ul>
<li><code>em.createQuery(&quot;select m from Member m&quot;, Member.class).getResultList()</code></li>
</ul>
</li>
<li>이때 실제 SQL은 이렇게 실행될 것이다.<ul>
<li><code>SQL: select * from Member</code></li>
</ul>
</li>
<li>그럼 <code>Member</code> 객체를 전부 다 가져오게 되는데 이때 <code>Member</code> 내부를 보니 <code>Team</code>에 대한 매핑이 즉시 로딩으로 되어있는 것을 확인하고 SQL을 한 번 더 실행<ul>
<li><code>SQL: select * from Team where ...</code></li>
</ul>
</li>
<li>그런데 이 쿼리가 <code>Member</code>의 개수만큼 나감</li>
<li>그래서 <code>Member</code>의 개수 <code>N</code>에 최초 <code>Member</code>를 조회하는 쿼리까지 합한 <code>N+1</code>번 쿼리가 실행됨</li>
</ul>
</li>
</ul>
<p>결론은 모든 연관관계에 지연 로딩을 사용하고, JPQL 패치 조인이나, 엔티티 그래프 기능을 사용하자.</p>
<blockquote>
<p>참고: <a href="https://velog.io/@jinyoungchoi95/JPA-%EB%AA%A8%EB%93%A0-N1-%EB%B0%9C%EC%83%9D-%EC%BC%80%EC%9D%B4%EC%8A%A4%EA%B3%BC-%ED%95%B4%EA%B2%B0%EC%B1%85">https://velog.io/@jinyoungchoi95/JPA-%EB%AA%A8%EB%93%A0-N1-%EB%B0%9C%EC%83%9D-%EC%BC%80%EC%9D%B4%EC%8A%A4%EA%B3%BC-%ED%95%B4%EA%B2%B0%EC%B1%85</a></p>
</blockquote>
<h1 id="references">References</h1>
<ul>
<li><a href="https://velog.io/@jinyoungchoi95/JPA-%EB%AA%A8%EB%93%A0-N1-%EB%B0%9C%EC%83%9D-%EC%BC%80%EC%9D%B4%EC%8A%A4%EA%B3%BC-%ED%95%B4%EA%B2%B0%EC%B1%85">https://velog.io/@jinyoungchoi95/JPA-%EB%AA%A8%EB%93%A0-N1-%EB%B0%9C%EC%83%9D-%EC%BC%80%EC%9D%B4%EC%8A%A4%EA%B3%BC-%ED%95%B4%EA%B2%B0%EC%B1%85</a></li>
<li><a href="https://www.inflearn.com/course/ORM-JPA-Basic">https://www.inflearn.com/course/ORM-JPA-Basic</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[프록시]]></title>
            <link>https://velog.io/@dereck-jun/%ED%94%84%EB%A1%9D%EC%8B%9C</link>
            <guid>https://velog.io/@dereck-jun/%ED%94%84%EB%A1%9D%EC%8B%9C</guid>
            <pubDate>Fri, 21 Feb 2025 12:40:39 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>김영한님의 JPA 강의를 보고 정리한 내용입니다.</p>
</blockquote>
<h1 id="프록시">프록시</h1>
<p>프록시는 다음과 같은 질문에서 시작한다.</p>
<blockquote>
<p>&quot;Member 엔티티를 조회할 때 Team 엔티티도 함께 조회해야 할까?&quot;</p>
</blockquote>
<p>질문에 대한 해답은 &quot;실제 필요한 비즈니스 로직에 따라 다르다&quot;이다. 비즈니스 로직에서 필요하지 않을 때도 있는데, 만약 항상 <code>Team</code>을 같이 조회한다면 이는 낭비일 것이다.</p>
<p>JPA는 위와 같은 행동(낭비)를 방지하기 위해 <strong>지연 로딩</strong>과 <strong>프록시</strong>로 해결한다.</p>
<h2 id="프록시-기초">프록시 기초</h2>
<p>뒤에 나올 지연 로딩을 이해하려면 프록시의 개념을 완벽히 이해해야 한다.</p>
<p>프록시는 쉽게 말해 <strong>실제 객체를 상속한 가짜 클래스</strong>이다. 프록시 객체는 실제 클래스와 겉 모양이 같고, 클라이언트의 입장에선 내가 호출한 것이 프록시 객체인지 실제 객체인지 알 수 없다.</p>
<p>또한 <strong>프록시 객체는 실제 객체의 참조(<code>target</code>)를 보관</strong>한다. 프록시 객체의 메서드를 호출하면 프록시 객체는 실제 객체(<code>target</code>)의 메서드를 호출한다.</p>
<p>JPA에는 <code>em.find()</code> 말고 <code>em.getReference()</code> 메서드도 제공한다.</p>
<ul>
<li><code>em.find()</code>: DB를 통해 실제 엔티티 객체를 조회하는 메서드</li>
<li><code>em.getReference()</code>: 프록시 엔티티 객체를 조회하는 메서드</li>
</ul>
<p><code>em.find()</code>로 객체를 조회하면 데이터베이스에 바로 쿼리가 나간다.</p>
<ul>
<li>지금 당장 필요하지 않아도 바로 쿼리 나감
하지만 <code>em.getReference()</code>로 객체를 조회하면 실제 객체가 사용되는 순간에 DB로 쿼리가 나가게 된다.</li>
<li>진짜 필요한 순간에 쿼리 나감<ul>
<li><code>실제 객체가 사용되는 순간 == 실제로 필요한 시점</code></li>
</ul>
</li>
</ul>
<h2 id="프록시-객체의-초기화-과정">프록시 객체의 초기화 과정</h2>
<blockquote>
<p>상황: <code>em.getReference()</code>로 <code>Member</code> 프록시 객체를 조회한 뒤에 <code>member.getName()</code>으로 강제 호출 시 내부에선 어떻게 동작하는지?</p>
</blockquote>
<ol>
<li><code>em.getReference()</code>로 프록시 객체를 조회한 뒤에 <code>member.getName()</code>으로 강제 호출을 하면</li>
<li><code>Member</code> 프록시 객체의 <code>target</code> 값이 처음에 없기 때문에 JPA가 영속성 컨텍스트에 초기화를 요청한다.</li>
<li>영속성 컨텍스트는 실제 DB를 조회한 뒤에</li>
<li>실제 엔티티 객체를 생성하고</li>
<li>프록시 객체의 <code>target</code>과 연결시켜준다.<ul>
<li>프록시 내부에는 <code>Member target</code> 이라는 멤버 변수가 있다고 생각</li>
</ul>
</li>
<li>그래서 <code>getName()</code>을 했을 때 프록시 객체 내부의 <code>target.getName()</code>을 통해 <code>Member</code>에 있는 <code>member.getName()</code>이 반환이 됨.</li>
</ol>
<h2 id="프록시의-특징">프록시의 특징</h2>
<ul>
<li><strong>프록시 객체는 처음 사용할 때 한 번만 초기화 된다.</strong></li>
<li>프록시 객체를 초기화할 때, <strong>프록시 객체가 실제 엔티티로 바뀌는 것이 아니라 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근이 가능</strong>해진다. </li>
<li>프록시 객체는 원본 엔티티를 상속받는다. 따라서 타입 체크 시 주의해야 한다.<ul>
<li><code>==</code> 비교 대신 <code>instance of</code> 사용</li>
</ul>
</li>
<li>영속성 컨텍스트에 찾는 엔티티가 이미 있으면 <code>em.getReference()</code>를 호출해도 실제 엔티티가 반환된다.<ul>
<li>1차 캐시에 이미 올라가 있기 때문에 굳이 프록시를 거칠 필요가 없음</li>
<li>JPA는 한 트랜잭션 내에서 같은 식별자에 대한 객체 조회는 동일성을 보장</li>
<li>반대의 경우에도 동일하게 작동한다.<ul>
<li>프록시 먼저 호출 후 DB를 통해 조회하는 <code>em.find()</code> 사용해도 <code>select</code> 쿼리는 나가지만 프록시 반환됨 -&gt; 동일성 보장을 위함</li>
</ul>
</li>
</ul>
</li>
<li>영속성 컨텍스트의 도움을 받을 수 없는 <strong>준영속 상태일 때, 프록시를 초기화하면 문제가 발생</strong>한다.<ul>
<li>상당히 자주 만나는 문제</li>
<li><code>org.hibernate.LazyInitializationException</code> 예외를 터트림</li>
<li><code>em.detach()</code>, <code>em.clear()</code>, <code>em.close()</code> 모두 같은 에러 발생</li>
</ul>
</li>
</ul>
<h2 id="프록시-확인">프록시 확인</h2>
<p>프록시 인스턴스의 초기화 여부 확인</p>
<ul>
<li><code>emf.getPersistenceUnitUtil().isLoaded(Object entity)</code></li>
</ul>
<p>프록시 클래스 확인 방법</p>
<ul>
<li><code>entity.getClass().getName()</code></li>
</ul>
<p>프록시 강제 초기화 (하이버네이트에서 제공)</p>
<ul>
<li><code>org.hibernate.Hibernate.initialize(entity)</code></li>
</ul>
<blockquote>
<p>JPA 표준 상 강제 초기화는 없기 때문에 강제 호출을 해야 한다. <br/>
강제 호출(실제 객체의 메서드를 사용해서 초기화): <code>entity.getXxx()</code></p>
</blockquote>
<h1 id="references">References</h1>
<ul>
<li><a href="https://www.inflearn.com/course/ORM-JPA-Basic">https://www.inflearn.com/course/ORM-JPA-Basic</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[상속관계 매핑과 @MappedSuperClass]]></title>
            <link>https://velog.io/@dereck-jun/%EC%83%81%EC%86%8D%EA%B4%80%EA%B3%84-%EB%A7%A4%ED%95%91</link>
            <guid>https://velog.io/@dereck-jun/%EC%83%81%EC%86%8D%EA%B4%80%EA%B3%84-%EB%A7%A4%ED%95%91</guid>
            <pubDate>Thu, 20 Feb 2025 11:29:06 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>김영한님의 JPA 강의를 보고 정리한 내용입니다.</p>
</blockquote>
<h1 id="상속관계-매핑">상속관계 매핑</h1>
<p>객체지향에는 클래스끼리 상속이 가능하다. 하지만 관계형 데이터베이스는 상속관계를 지원하지 않는다. </p>
<p>그 대신 <strong>상속 관계와 비슷한 논리 모델링 기법인 슈퍼타입, 서브타입 관계를 통해 객체의 상속 관계를 매핑</strong>할 수 있다.</p>
<p>슈퍼타입, 서브타입을 실제 테이블로 구현하는 방법엔 3가지 방법이 있다.</p>
<ol>
<li>조인 전략</li>
<li>단일 테이블 전략</li>
<li>구현 클래스마다 테이블 전략</li>
</ol>
<p>위의 3가지 방법 모두 JPA의 어노테이션인 <code>@Inheritance(strategy = InheritanceType.XXX)</code>를 통해 구현할 수 있다. 이때 <strong>기본 값은 단일 테이블 전략</strong>이다.</p>
<ul>
<li><code>JOINED</code></li>
<li><code>SINGLE_TABLE</code></li>
<li><code>TABLE_PER_CLASS</code></li>
</ul>
<p>구분자 컬럼(<code>DTYPE</code>)이 필요한 경우 부모 클래스에 <code>@DiscriminatorColumn</code>으로 <code>DTYPE</code>을 설정할 수 있고 <code>name</code> 속성으로 <code>DTYPE</code>의 컬럼명을 바꿀 수 있다. 기본 값은 <code>DTYPE</code>이다.</p>
<p>자식 클래스엔 <code>@DiscriminatorValue</code> 어노테이션으로 <code>DTYPE</code>이 저장되는 값을 바꿀 수 있다. 기본 값은 클래스명이다.</p>
<p>어떤 전략을 사용하든지 크게 바뀌는 코드는 없다. 단지 어노테이션만 바꿔주면 JPA가 알아서 해당하는 전략에 맞게 테이블을 매핑해준다.</p>
<h2 id="조인-전략">조인 전략</h2>
<p>조인 전략은 <strong>슈퍼타입, 서브타입 논리 모델을 각각 테이블로 옮긴 방식</strong>이다. 테이블이 구분되어 있기 때문에 데이터를 조회할 때 조인이 필요해서 조인 전략이라고 부른다.</p>
<p>조인 테이블 전략은 <strong>구분자 컬럼이 반드시 필요하진 않지만</strong> 구분자 컬럼을 설정하는 것이 부모 테이블 조회만 해도 어떤 데이터 타입을 가지고 있는지 확인할 수 있어서 <strong>설정하는 것이 좋다</strong>.</p>
<h3 id="장단점">장/단점</h3>
<p><strong>장점</strong></p>
<ul>
<li>테이블이 정규화 된다.</li>
<li>외래 키 참조 무결성 제약 조건을 활용할 수 있다.</li>
<li>저장 공간 효율화<ul>
<li>정규화가 되어있기 때문</li>
</ul>
</li>
</ul>
<p><strong>단점</strong></p>
<ul>
<li>조회 시 조인을 많이 사용하기에 성능이 저하됨</li>
<li>조회 쿼리가 복잡함</li>
<li>데이터 저장 시 <code>insert</code> 쿼리 2번 호출</li>
</ul>
<p>단점으로 성능이 떨어질 수 있다고 했지만 <strong>데이터가 엄청 많은게 아닌 이상 성능에 대한 이슈가 있지는 않다</strong>. </p>
<p><strong>조인 전략은 상속 관계를 매핑하는 전략 중 데이터베이스의 관점에서 가장 정규화된 깔끔하고 정석적인 선택</strong>이다.</p>
<h2 id="단일-테이블-전략">단일 테이블 전략</h2>
<p>단일 테이블 전략은 이름 그대로 <strong>하나의 테이블에 모든 데이터를 몰아넣는 전략</strong>이다.</p>
<p>조인 전략에서는 구분자 컬럼이 필수는 아니었지만 단일 테이블 전략은 하나의 테이블에 모든 데이터가 들어가기 때문에 <strong>구분자 컬럼이 필수</strong>다. (개발자가 지정해주지 않아도 JPA가 자동으로 만들어 준다)</p>
<h3 id="장단점-1">장/단점</h3>
<p><strong>장점</strong></p>
<ul>
<li>조인이 필요 없다<ul>
<li>일반적인 상황에서 조회 성능이 빠름</li>
<li>조회 쿼리가 단순함</li>
</ul>
</li>
</ul>
<p><strong>단점</strong></p>
<ul>
<li>자식 엔티티에 매핑한 컬럼은 모두 <code>null</code> 값을 허용해야 한다.<ul>
<li>데이터 무결성 입장에서 애매함</li>
</ul>
</li>
<li>테이블 컬럼의 길이가 커지기 때문에 조인 전략과 비교해서 성능이 더 느려질 수도 있다.</li>
</ul>
<p>조인 전략과 비교해서 성능이 더 느려지려면 임계점을 넘어야 하는데 보통 넘을 일은 없다.</p>
<p><strong>단일 테이블 전략</strong>은 조인 전략과는 다르게 테이블 하나만 사용하기 때문에 가볍고, 덜 복잡한 프로젝트와 잘 어울리는 전략이다. <strong>프로젝트가 덜 복잡하고, 바뀌지 않을 것 같다면 추천</strong>한다.</p>
<h2 id="구현-클래스마다-테이블-전략">구현 클래스마다 테이블 전략</h2>
<p>조인 전략과 좀 비슷하지만 <strong>부모 객체를 없애버리고, 부모 객체에 있는 속성들을 자식 객체에 내리는 전략</strong>이다. </p>
<p>이때 부모 객체는 테이블이 만들어지면 해당 객체만 독단적으로 쓰는 일도 있다는 것이기 때문에 아예 만들어 지지 않게 추상 클래스로 정의한다.</p>
<h3 id="장단점-2">장단점</h3>
<p><strong>장점</strong></p>
<ul>
<li>서브타입을 명확하기 구분해서 처리할 때 효과적</li>
<li><code>not null</code> 제약 조건 사용 가능</li>
</ul>
<p><strong>단점</strong></p>
<ul>
<li>여러 자식 테이블을 함께 조회할 때 성능이 느림<ul>
<li>여러 테이블을 조회할 때 <code>UNION</code> 쿼리를 사용함</li>
</ul>
</li>
<li>자식 테이블을 종합해서 쿼리하기 어려움<ul>
<li>뭘 묶어낼 수 있는게 있어야 통합이 가능한데 묶어낼 수 있는 것이 없음<ul>
<li>ex) 시스템에 새로운 타입이 추가돼서 정산 로직을 다시 짜야 함</li>
<li>ex) 정산을 할 때 한 테이블만 정산해서 되는게 아니라 각 테이블에서 모두 정산을 해야 함</li>
</ul>
</li>
</ul>
</li>
</ul>
<p><strong>구현 클래스마다 테이블 전략</strong>은 변경이라는 관점에서 볼 때 매우 안좋다. 시스템에 <strong>새로운 것이 추가 될 때마다 굉장히 많은 걸 뜯어내야 한다</strong>.</p>
<p>구현 클래스마다 테이블 전략은 DBA와 개발자 모두 선호하지 않는 전략이다. 선택을 하게 되면 먼 미래에 언젠가 큰 후회를 하게 된다. 따라서 사용하지 않도록 하자. </p>
<h1 id="mappedsuperclass">@MappedSuperClass</h1>
<p>공통 매핑 정보가 필요할 때 사용한다. 주로 등록일, 수정일, 등록자, 수정자 같은 전체 엔티티에서 공통으로 적용하는 정보를 모을 때 사용한다.</p>
<p>아래 예시를 보면 공통으로 들어가는 정보인 <code>createdBy</code>, <code>createdAt</code>, <code>lastModifiedBy</code>, <code>lastModifiedAt</code>를 <code>BaseEntity</code> 클래스에 모아서 사용하는 것을 볼 수 있다.</p>
<pre><code class="language-java">@Getter
@MappedSuperClass
public abstract class BaseEntity {
    private String createdBy;
    private LocalDateTime createdAt;
    private String lastModifiedBy;
    private LocalDateTime lastModifiedAt;
}

@Entity
public class Post extends BaseEntity {
    ...
}</code></pre>
<p><code>@MappedSuperClass</code>는 상속관계 매핑이 아니니 헷갈리지 말자.</p>
<p><code>@MappedSuperClass</code>는 엔티티가 아니라 그저 속성만 내려주는 클래스이다. 그래서 해당 어노테이션이 적용되어 있는 클래스는 테이블과 매핑이 되지 않고, 부모 클래스를 상속받는 자식 클래스에 매핑 정보만 제공한다.</p>
<p>같은 맥락으로 해당 어노테이션이 적용된 클래스의 타입으로는 조회와 검색이 불가능하다. 이것도 상속관계 매핑과 다른 점이라고 볼 수 있다. (상속관계 매핑은 가능)</p>
<p>해당 클래스는 직접 사용할 일이 없으므로 <strong>추상 클래스로 만드는 것을 권장</strong>한다.</p>
<blockquote>
<p><code>@Entity</code> 클래스는 같은 <code>@Entity</code> 클래스나 <code>@MappedSuperClass</code>로 지정한 클래스만 상속 가능하다.</p>
</blockquote>
<h1 id="references">References</h1>
<ul>
<li><a href="https://www.inflearn.com/course/ORM-JPA-Basic">https://www.inflearn.com/course/ORM-JPA-Basic</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[연관관계 매핑 (2)]]></title>
            <link>https://velog.io/@dereck-jun/%EC%97%B0%EA%B4%80%EA%B4%80%EA%B3%84-%EB%A7%A4%ED%95%91-2</link>
            <guid>https://velog.io/@dereck-jun/%EC%97%B0%EA%B4%80%EA%B4%80%EA%B3%84-%EB%A7%A4%ED%95%91-2</guid>
            <pubDate>Wed, 19 Feb 2025 14:44:44 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>김영한님의 JPA 강의를 보고 정리한 내용입니다.</p>
</blockquote>
<h1 id="연관관계-매핑-2">연관관계 매핑 2</h1>
<h2 id="연관관계-정의-규칙">연관관계 정의 규칙</h2>
<p>연관관계를 매핑할 때, 생각해야 할 것은 크게 3가지가 있다.</p>
<ol>
<li>방향 - 단/양방향<ul>
<li>무조건 양방향 연관관계를 가지면 안될까?</li>
<li>양방향 매핑 정리</li>
</ul>
</li>
<li>연관관계의 주인 - 연관관계에서 관리 주체<ul>
<li>왜 연관관계의 주인을 지정해야 할까</li>
<li>연관관계 매핑에서 자주하는 실수</li>
</ul>
</li>
<li><strong>다중성</strong><ul>
<li><strong>일대일(1:1)</strong></li>
<li><strong>일대다(1:N)</strong></li>
<li><strong>다대일(N:1)</strong></li>
<li><strong>다대다(N:M)</strong></li>
</ul>
</li>
</ol>
<blockquote>
<p>&quot;연관관계 매핑 2&quot;에서는 다중성에 대한 내용이 있음</p>
</blockquote>
<h2 id="다중성">다중성</h2>
<p>데이터베이스를 기준으로 다중성을 결정한다. 이때 연관관계는 대칭성을 가진다.</p>
<ul>
<li>일대다(1:N) &lt;--&gt; 다대일(N:1)</li>
<li>일대일(1:1) &lt;--&gt; 일대일(1:1)</li>
<li>다대다(N:M) &lt;--&gt; 다대다(M:N)</li>
</ul>
<p>이때 일대다 관계과 다대일 관계는 똑같은 내용이 아니라 각각 일을 주인으로 두었을 때의 관점과 다를 주인으로 두었을 때의 관점에서 봤을 때의 내용이다.</p>
<h3 id="다대일n1">다대일(N:1)</h3>
<p>다대일 관계는 다쪽에서 외래 키를 관리하는 형태이고, 가장 많이 사용하는 연관관계이다. 반대는 일대다 관계이다. </p>
<p>회원(Member)와 팀(Team)으로 다대일 관계를 만들어 보자.</p>
<ul>
<li>요구사항<ul>
<li>한 팀에는 여러 회원이 들어갈 수 있다.</li>
<li>한 회원은 한 개의 팀에만 들어갈 수 있다.</li>
</ul>
</li>
</ul>
<p><strong>단방향 다대일 예제 코드</strong></p>
<pre><code class="language-java">@Entity
public class Member {
    @Id @GeneratedValue
    @Column(name = &quot;member_id&quot;)
    private Long id;
    private String name;

    @ManyToOne
    @JoinColumn(name = &quot;team_id&quot;)
    private Team team;

    ...
}

@Entity
public class Team {
    @Id @GeneratedValue
    @Column(name = &quot;team_id&quot;)
    private Long id;
    private String teamName;

    ...
}</code></pre>
<p>다대일이기 때문에 <code>Member</code> 클래스 쪽에 <code>@ManyToOne</code>을 사용해서 해당 클래스가 다(Many)라는 것을 지정하면서 <code>@JoinColumn(name = &quot;team_id&quot;)</code>로 <code>Team</code> 클래스와 어떤 컬럼을 기준으로 조인할 것인지를 정해줬다.</p>
<p><strong>양방향 다대일 예제 코드</strong></p>
<pre><code class="language-java">@Entity
public class Member {
    @Id @GeneratedValue
    @Column(name = &quot;member_id&quot;)
    private Long id;
    private String name;

    @ManyToOne
    @JoinColumn(name = &quot;team_id&quot;)
    private Team team;

    ...
}

@Entity
public class Team {
    @Id @GeneratedValue
    @Column(name = &quot;team_id&quot;)
    private Long id;
    private String teamName;

    @OneToMany(mappedBy = &quot;team&quot;)
    private List&lt;Member&gt; members = new ArrayList&lt;&gt;();

    ...
}</code></pre>
<p>양방향의 경우 일(ToOne)에 해당하는 <code>Team</code> 클래스에 <code>@OneToMany</code>를 추가하고 연관관계의 주인을 <code>mappedBy</code>로 지정해준다. 이때 주인 객체에서 사용하는 변수명을 지정해주면 된다. </p>
<p>이번 예제 코드의 경우 <code>Team</code> 클래스의 주인인 <code>Member</code> 클래스가 <code>Team</code>에 대한 변수로 <code>team</code>을 사용하고 있기 때문에 <code>mappedBy = &quot;team&quot;</code>으로 지정해줬다.</p>
<h3 id="일대다1n">일대다(1:N)</h3>
<p>여기서는 앞서 말한대로 일(One)쪽이 연관관계의 주인일 때를 말한 것이다. 즉, 일에서 외래 키를 관리하겠다는 말이다.</p>
<p>객체와 테이블의 차이 때문에 반대편 테이블의 외래 키를 관리하는 특이한 구조이다.</p>
<p>추가로 <code>@JoinColumn</code>을 꼭 사용해야 한다. 그렇지 않으면 <code>@JoinTable</code> 방식을 사용하게 된다.</p>
<ul>
<li>자동으로 중간 테이블을 하나 추가하게 됨 (기본 값임)</li>
</ul>
<p>표준 스펙에서 지원은 하지만 권장하지는 않는다.</p>
<h4 id="일대다-단방향">일대다 단방향</h4>
<p><strong>예제 코드</strong></p>
<pre><code class="language-java">@Entity
public class Member {
    @Id @GeneratedValue
    @Column(name = &quot;member_id&quot;)
    private Long id;
    private String name;

    ...
}

@Entity
public class Team {
    @Id @GeneratedValue
    @Column(name = &quot;team_id&quot;)
    private Long id;
    private String teamName;

    @OneToMany
    @JoinColumn(name = &quot;team_id&quot;) // DB 관점에서 조인할 컬럼
    private List&lt;Member&gt; members = new ArrayList&lt;&gt;();

    ...
}</code></pre>
<p>예제 코드를 기준으로 일대다 관계는 <code>Team</code> 클래스의 <code>members</code> 값을 바꿨을 때 다른 테이블에 있는 외래 키를 <code>update</code> 해줘야 한다.</p>
<blockquote>
<p>아까 일에서 외래 키를 관리한다고 했잖아요</p>
</blockquote>
<p>위에서 말한 외래 키의 관리는 JPA가 조회, 저장, 수정, 삭제 등을 하는 작업에서의 주체를 말한 것이고, 여기서 말한 다른 테이블에 있는 외래 키는 데이터베이스의 관점에서의 외래 키를 말한 것이다. </p>
<p>코드에서 <code>@OneToMany</code>를 사용한다고 그게 데이터베이스까지 영향을 미치는 것은 아니다. 해당 어노테이션은 JPA가 어떤 객체를 중심으로 관리할 것인가를 나타낸 것이다. </p>
<p>실제 사용을 아래와 같이 할 수 있다.</p>
<pre><code class="language-java">Member member = new Member();  
member.setName(&quot;memberA&quot;);  

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

Team team = new Team();  
team.setName(&quot;teamA&quot;);  
team.getMembers().add(member);  

em.persist(team);  // 실행 2</code></pre>
<p><code>실행 1</code>에서는 별 문제 없이 <code>insert</code> 쿼리가 나가는 것을 알 수 있다. </p>
<p>하지만 <code>실행 2</code>가 문제다.</p>
<p><code>실행 2</code>에선 <code>team</code>을 <code>insert</code>하고 <code>member</code>를 <code>update</code>하는 쿼리가 나간다. 이때 <code>Member</code>에 실제 외래 키가 존재하기 때문에 <code>Team</code>에서 <code>Member</code>에 외래 키를 수정하려면 조인 후 <code>update</code> 쿼리를 날려야만 수정할 수 있는 문제가 생긴 것이다.</p>
<ul>
<li><code>Team</code>을 수정했는데 <code>Member</code>가 수정이 되는 기적을 보게 됨</li>
<li><code>insert</code> 쿼리 실행 후 <code>update</code> 쿼리를 실행하게 되는 낭비가 발생<ul>
<li>성능상 이슈는 크게 없으나 낭비가 발생하는 것은 팩트</li>
</ul>
</li>
</ul>
<p>일대다 단방향이 필요한 경우라도 유지보수의 측면에서 바라볼 때 다대일 관계로 매핑하는 것이 더 수월하기에 다대일 방식을 추천한다.</p>
<h4 id="일대다-단방향-관계-정리">일대다 단방향 관계 정리</h4>
<p><strong>일대다 단방향 매핑의 단점</strong></p>
<ul>
<li>엔티티가 관리하는 외래 키가 다른 테이블에 있다.<ul>
<li>그냥 이것 하나만으로도 어마어마한 단점</li>
</ul>
</li>
<li>연관관계 관리를 위해 추가적인 <code>update</code> 쿼리가 실행된다.</li>
</ul>
<p>결론적으로 일대다 단방향 매핑보다는 객체적으로 설계가 덜 깔끔해지는 손해를 보더라도 다대일 양방향 매핑을 사용하는 것이 더 좋다. (그냥 일대다를 쓰지 말자)</p>
<blockquote>
<p>일대다 양방향 매핑은 없나요?</p>
</blockquote>
<p>일대다 양방향 매핑이라는 것은 공식적인 스펙으로 존재하진 않는다. 하지만 야매로 된다.</p>
<p>위의 일대다 단방향 예제 코드에서 <code>Member</code> 클래스의 내용만 추가했다.</p>
<p><strong>일대다 양방향 예제 코드</strong></p>
<pre><code class="language-java">@Entity
public class Member {
    ...

    @ManyToOne
    @JoinColumn(name = &quot;team_id&quot;, insertable = false, updatable = false)
    private Team team;

    ...
}
</code></pre>
<p>코드를 보면 일대다의 대칭이기 때문에 <code>@ManyToOne</code>을 추가했다. </p>
<p>그 다음이 중요한데 <code>@JoinColumn</code>으로 조인할 테이블의 <code>id</code>를 설정해준 뒤에 <code>insertable</code>, <code>updatable</code> 속성을 <code>false</code>로 만들어서 매핑은 되어있고 값도 쓰는데, 최종적으로 <code>insert/update</code>를 하지 않는 것이다.</p>
<p>정말 필요할 때가 아주 가끔 있겠지만 양방향으로 객체 참조가 필요한 경우 그냥 <strong>다대일 양방향 매핑을 사용</strong>하도록 하자.</p>
<h3 id="일대일11">일대일(1:1)</h3>
<p>일대일 관계는 그 반대도 일대일이다. 그래서 주 테이블이나 대상 테이블 중 어느 곳이든지 외래 키를 설정할 수 있다. </p>
<p>주 테이블이 <code>Member</code>라고 가정했을 때 외래 키를 <code>Member</code>에 넣어도 되고, 주 테이블은 아니지만 <code>Team</code>에 외래 키를 넣어도 되는 것이다. </p>
<p>추가로 외래 키에 데이터베이스 유니크 제약 조건을 추가해야 일대일 관계가 성립한다. 사실 굳이 제약 조건을 넣지 않아도 할 수는 있지만 그러면 애플리케이션 관리를 매우 잘해야 한다.</p>
<p>데이터베이스 입장에서는 외래 키에 데이터베이스 유니크 제약 조건이 추가가 된게 일대일 관계가 된다.</p>
<h4 id="일대일-주-테이블-단방향">일대일 주 테이블 단방향</h4>
<p><strong>일대일 주 테이블 단방향 예제 코드</strong></p>
<pre><code class="language-java">@Entity
public class Member {
    @Id @GeneratedValue
    @Column(name = &quot;member_id&quot;)
    private Long id;
    private String name;

    @OneToOne
    @JoinColumn(name = &quot;locker_id&quot;)
    private Locker locker;
}

@Entity
public class Locker {
    @Id @GeneratedValue
    @Column(name = &quot;locker_id&quot;)
    private Long id;
    private int number;
}</code></pre>
<p>코드를 보면 알겠지만 다대일 단방향 매핑과 어노테이션만 다르지 거의 똑같다는 것을 알 수 있다.</p>
<h4 id="일대일-주-테이블-양방향">일대일 주 테이블 양방향</h4>
<p><strong>일대일 주 테이블 양방향 예제 코드</strong></p>
<pre><code class="language-java">@Entity
public class Member {
    @Id @GeneratedValue
    @Column(name = &quot;member_id&quot;)
    private Long id;
    private String name;

    @OneToOne
    @JoinColumn(name = &quot;locker_id&quot;)
    private Locker locker;
}

@Entity
public class Locker {
    @Id @GeneratedValue
    @Column(name = &quot;locker_id&quot;)
    private Long id;
    private int number;

    @OneToOne(mappedBy = &quot;locker&quot;)
    private Member member;
}</code></pre>
<p>마찬가지로 다대일 양방향 매핑처럼 외래 키가 있는 곳이 주인이기 때문에 반대쪽에 <code>mappedBy</code>를 적용해서 읽기 전용으로 만들어 준다.</p>
<h4 id="일대일-대상-테이블-단방향">일대일 대상 테이블 단방향</h4>
<p>불가능하다. 지원도 안되고 방법이 없다.</p>
<h4 id="일대일-대상-테이블-양방향">일대일 대상 테이블 양방향</h4>
<p>이 경우는 논란의 여지가 있다. 어떤 테이블에서 외래 키를 관리하는게 좋을 것인가를 생각해봐야 한다.</p>
<p>테이블의 경우 한 번 생성되면 변경이 매우 어렵다. 하지만 야속하게도 비즈니스 로직은 언제든 바뀔 수 있다. </p>
<p>만약 <code>Member</code>가 여러 개의 <code>Locker</code>를 가질 수 있게 변경되었다면 <code>Locker</code>에 외래 키가 있는 것이 변경에 유연하다.</p>
<p>하지만 비즈니스에서 <code>Member</code>는 웬만하면 조회를 해와야 한다. 이런 입장에서 바라보았을 땐 별 다른 조인 없이 <code>member.getLocker()</code>로 <code>locker</code>에 대한 값을 확인할 수 있다는 것이 장점이 된다.</p>
<p>결론적으로 종합적으로 판단하고 결정해야 한다는 것이고, 코드 상으로 바라보았을 땐 일대일 주 테이블 양방향을 할 때와 똑같이 하면 된다.</p>
<h4 id="일대일-정리">일대일 정리</h4>
<ul>
<li>주 테이블에 외래 키<ul>
<li>주 객체가 대상 객체의 참조를 가지는 것처럼 주 테이블에 외래 키를 두고 대상 테이블을 찾는 방식<ul>
<li>장점: 주 테이블만 조회해도 대상 테이블에 데이터까지 확인 가능</li>
<li>단점: 값이 없으면 외래 키에 <code>null</code> 허용</li>
</ul>
</li>
<li>객체 지향 개발자 선호</li>
<li>JPA 매핑 편리</li>
</ul>
</li>
<li>대상 테이블에 외래 키<ul>
<li>대상 테이블에 외래 키 존재<ul>
<li>장점: 주 테이블과 대상 테이블을 일대일에서 일대다 관계로 변경했을 때 테이블 구조 유지 가능</li>
<li>단점: 프록시 기능의 한계로 지연 로딩으로 설정해도 항상 즉시 로딩됨<ul>
<li><code>Member</code>를 로딩할 때 무조건 <code>Locker</code>를 뒤져서 확인해야 한다.</li>
<li>어차피 쿼리가 나가게 됨 -&gt; 값의 존재 여부를 알기 때문에 프록시를 만들 이유가 없음</li>
<li>따라서 지연 로딩으로 설정해도 대상 테이블에 외래 키가 있으면 무조건 즉시 로딩이 된다. (지연 로딩 설정해도 쿼리만 한 번 더 나감)</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<h3 id="다대다nm">다대다(N:M)</h3>
<p>관계형 데이터데이스는 정규화된 테이블 2개로 다대다 관계를 표현할 수 없기 때문에 사용하면 안된다.</p>
<p>대신 연결 테이블을 추가해서 일대다, 다대일 관계로 풀어내야 한다.</p>
<p>여기서 문제는 객체는 컬렉션을 사용해서 객체 2개로 다대다 관계를 풀어낼 수 있다. 따라서 ORM은 중간 테이블을 임의로 만들어서 매핑을 해준다.</p>
<p>설명만 들으면 &quot;자동으로 매핑을 해준다니 이거 완전 좋은거 아닌가요?&quot; 라고 생각할 수 있기 때문에 왜 안되는지 알아보자</p>
<h4 id="다대다-단양방향">다대다 단/양방향</h4>
<p><strong>다대다 단/양방향 예제 코드</strong></p>
<pre><code class="language-java">@Entity
public class Member {
    @Id @GeneratedValue
    @Column(name = &quot;member_id&quot;)
    private Long id;
    private String name;

    @ManyToMany
    @JoinTable(name = &quot;member_product&quot;)
    private List&lt;Product&gt; products = new ArrayList&lt;&gt;();    
}

@Entity
public class Product {
    @Id @GeneratedValue
    @Column(name = &quot;product_id&quot;)
    private Long id;
    private String name;

    // 양방향의 경우 추가
    // @ManyToMany(mappedBy = &quot;products&quot;)
    // private List&lt;Member&gt; members = new ArrayList&lt;&gt;();
}</code></pre>
<p>예제 코드에선 <code>@JoinTable</code>을 사용해서 생성되는 테이블의 이름을 지정해줬지만 해당 어노테이션을 빼도 중간 테이블을 임의로 생성해준다. </p>
<p>그 외 나머지는 다른 방법들과 동일하다.</p>
<p>그렇다면 왜 실무에서 사용하면 안될까?</p>
<h4 id="다대다-매핑의-한계">다대다 매핑의 한계</h4>
<p>다대다 관계의 경우 데이터베이스로는 풀어낼 수 없는 관계이기 때문에 중간(연결) 테이블을 임의로 생성한다고 설명했었다. </p>
<p>하지만 중간 테이블은 단순히 연결만 하고 끝나지 않는다. 중간 테이블에는 <strong>추가적인 비즈니스 로직이 들어가는데 이런 로직을 추가할 수가 없다.</strong> 또한 중간 테이블이 숨겨져 있기 때문에 <strong>나도 모르는 복잡한 조인 쿼리가 발생</strong>하게 된다.</p>
<p>위와 같은 이유로 실무에서 사용하면 안되는 것이다.</p>
<h4 id="다대다-한계-극복">다대다 한계 극복</h4>
<p>그럼 이런 문제를 어떻게 해결할 수 있을까?</p>
<p>바로 <code>@ManyToMany</code>를 <code>@OneToMany</code> + <code>@ManyToOne</code>으로 바꾸고, 중간 테이블을 엔티티로 승격하는 것이다.</p>
<p>그렇다면 이런 형태의 매핑이 이뤄질 것이다.</p>
<pre><code class="language-java">@Entity
public class Member {
    @Id @GeneratedValue
    @Column(name = &quot;member_id&quot;)
    private Long id;
    private String name;

    @OneToMany(mappedBy = member)
    private List&lt;MemberProduct&gt; memberProducts = new ArrayList&lt;&gt;();    
}

@Entity
public class MemberProduct {
    @Id @GeneratedValue
    @Column(name = &quot;member_product_id&quot;)
    private Long id;

    @ManyToOne
    @JoinColumn(name = &quot;member_id&quot;)
    private Member member;

    @ManyToOne
    @JoinColumn(name = &quot;product_id&quot;)
    private Product product;
}

@Entity
public class Product {
    @Id @GeneratedValue
    @Column(name = &quot;product_id&quot;)
    private Long id;
    private String name;

    @OneToMany(mappedBy = &quot;product&quot;)
    private List&lt;MemberProduct&gt; memberProducts = new ArrayList&lt;&gt;();
}</code></pre>
<p>이렇게 되면 중간 테이블에 내가 원하는 비즈니스 로직들을 추가해서 구현할 수 있게 된다.</p>
<p>번외로 <code>MemberProduct</code>의 PK를 지금처럼 따로 만들어 주는 것이 아닌 <code>member_id</code>와 <code>product_id</code>를 묶어서 PK로 설정하고 각각 FK로 쓰는 방법도 존재한다. </p>
<p>어떤 방식을 사용할 것인지는 고민이 필요하지만 따로 <code>id</code>를 두고, 필요에 따라 제약 조건을 추가하는 것이 운영적 측면에서 더 도움이 될 수도 있다.</p>
<p>결론은 다대다는 사용하지 말자.</p>
<h1 id="references">References</h1>
<ul>
<li><a href="https://www.inflearn.com/course/ORM-JPA-Basic">https://www.inflearn.com/course/ORM-JPA-Basic</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[연관관계 매핑 (1)]]></title>
            <link>https://velog.io/@dereck-jun/%EC%97%B0%EA%B4%80%EA%B4%80%EA%B3%84-%EB%A7%A4%ED%95%91-1</link>
            <guid>https://velog.io/@dereck-jun/%EC%97%B0%EA%B4%80%EA%B4%80%EA%B3%84-%EB%A7%A4%ED%95%91-1</guid>
            <pubDate>Tue, 18 Feb 2025 12:09:28 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>김영한님의 JPA 강의를 보고 정리한 내용입니다.</p>
</blockquote>
<h1 id="연관관계-매핑-1">연관관계 매핑 1</h1>
<h2 id="연관관계-정의-규칙">연관관계 정의 규칙</h2>
<p>연관관계를 매핑할 때, 생각해야 할 것은 크게 3가지가 있다.</p>
<ol>
<li><strong>방향 - 단/양방향</strong><ul>
<li><strong>무조건 양방향 연관관계를 가지면 안될까?</strong></li>
<li><strong>양방향 매핑 정리</strong></li>
</ul>
</li>
<li><strong>연관관계의 주인 - 연관관계에서 관리 주체</strong><ul>
<li><strong>왜 연관관계의 주인을 지정해야 할까</strong></li>
<li><strong>연관관계 매핑에서 자주하는 실수</strong></li>
</ul>
</li>
<li>다중성<ul>
<li>일대일(1:1)</li>
<li>일대다(1:N)</li>
<li>다대일(N:1)</li>
<li>다대다(N:M)</li>
</ul>
</li>
</ol>
<blockquote>
<p>&quot;연관관계 매핑 1&quot;에서는 방향과 연관관계의 주인에 대한 내용이 있음</p>
</blockquote>
<h2 id="방향">방향</h2>
<p>데이터베이스 테이블은 외래 키 하나로 양 쪽 테이블 조인이 가능하다. 따라서 데이터베이스는 방향을 나눌 필요가 없다. </p>
<p>하지만 객체는 참조용 필드가 있는 객체만 다른 객체를 참조하는 것이 가능하다. 그렇기 때문에 두 객체 사이에 하나의 객체만 참조용 필드를 갖고 참조하면 <strong>단방향 관계</strong>, 두 객체 모두가 각각 참조용 필드를 갖고 참조하면 <strong>양방향 관계</strong>라고 한다.</p>
<p>엄밀하게는 양방향 관계는 없고, 두 객체가 서로 단방향 참조를 가져서 양방향 관계처럼 사용하고 있다고 말하는 것이다.</p>
<p>JPA를 사용하여 데이터베이스와 패러다임을 맞추기 위해서는 객체는 어떤 방향으로 연관관계를 가질 지 선택해야 한다. 이때 선택은 비즈니스 로직에서 어떤 객체가 참조가 필요한 지 생각해보면 된다.</p>
<ul>
<li><code>Member</code>와 <code>Team</code>이 있을 때<ul>
<li><code>member.getTeam()</code>: <code>Member</code>가 <code>Team</code>을 참조</li>
<li><code>team.getMember</code>: <code>Team</code>이 <code>Member</code>를 참조</li>
</ul>
</li>
</ul>
<p>이렇게 비즈니스 로직에 맞게 선택 했는데 두 객체가 서로 단방향 참조를 했다면 양방향 연관관계가 되는 것이다.</p>
<h3 id="무조건-양방향-연관관계를-가지면-안될까">무조건 양방향 연관관계를 가지면 안될까?</h3>
<p>객체 관점에서 양방향 연관관계를 가지면 오히려 복잡해질 수 있다.</p>
<p>예시로 일반적인 비즈니스 애플리케이션에서 사용자(User) 엔티티는 많은 테이블과 연관관계를 맺게 된다. 이런 경우에 모든 엔티티를 양방향으로 설계하게 되면 사용자 엔티티는 엄청나게 복잡해질 것이다.</p>
<p>그리고 다른 엔티티들도 불필요한 연관관계 매핑으로 인해 복잡성이 증가할 수 있다.</p>
<h3 id="양방향-매핑-정리">양방향 매핑 정리</h3>
<ul>
<li>단방향 매핑만으로도 이미 연관관계 매핑은 완료되어야 한다.<ul>
<li>설계라는 관점에서만 봤을 때 단방향으로 끝내라는 말</li>
</ul>
</li>
<li>양방향 매핑은 반대 방향으로 조회 기능이 추가된 것일 뿐이다.<ul>
<li>조회 기능 = 객체 그래프 탐색</li>
</ul>
</li>
<li>JPQL에서 역방향으로 탐색할 일이 많다.<ul>
<li>(하지만) 단방향 매핑을 한 뒤에 필요할 때 양방향을 추가해도 된다. <ul>
<li>어차피 테이블에 영향을 주지 않음</li>
<li>연관관계 편의 메서드 예시를 활용해서 추가하도록 한다.</li>
</ul>
</li>
</ul>
</li>
</ul>
<h4 id="연관관계-편의-메서드-예시">연관관계 편의 메서드 예시</h4>
<pre><code class="language-java">class Member {
    ...
    @ManyToOne
    @JoinColumn(name = &quot;team_id&quot;)
    private Team team;

    public void changeTeam(Team team) {
        this.team = team;
        team.getMembers().add(this);  // 해당 메서드 호출 시 자동으로 동기화
    }
}</code></pre>
<p>비즈니스 로직이 정말 복잡하면 값을 체크하는 로직을 포함하도록 하자.</p>
<ul>
<li><code>null</code> 체크 로직</li>
<li>기존 값 변경 시 필요한 로직 </li>
</ul>
<h2 id="연관관계의-주인">연관관계의 주인</h2>
<p>두 객체가 양방향(단방향 2개) 관계를 맺을 때, 연관관계의 주인을 지정해야 한다. 연관관계의 주인을 지정하는 것은 두 단방향 관계 중, 제어의 권한을 갖는 실질적인 관계가 어떤 것인지 JPA에게 알려준다고 생각하면 된다. </p>
<ul>
<li>제어의 권한: 외래 키를 비롯한 테이블 레코드를 조회, 저장, 수정, 삭제 처리하는 것</li>
</ul>
<p>연관관계의 주인이 아닌 쪽에 <code>mappedBy</code> 속성으로 주인을 지정한다. 쉽게 말하면 외래 키가 존재하는 곳이 주인이고, 외래 키가 없는 쪽이 주인이 아니기 때문에 외래 키가 없는 쪽에 <code>mappedBy</code>로 주인을 지정해주면 된다.</p>
<p>연관관계의 주인을 정할 때 비즈니스 로직을 기준으로 주인을 선택하면 안된다. <strong>연관관계의 주인은 외래 키의 위치를 기준으로 정해야 한다.</strong></p>
<p>연관관계의 주인은 연관관계를 갖는 두 객체 사이에서 제어를 할 수 있지만, 연관관계의 주인이 아니면 조회만 가능하다.</p>
<h3 id="왜-연관관계의-주인을-지정해야-할까">왜 연관관계의 주인을 지정해야 할까?</h3>
<p>객체에서 양방향으로 매핑을 하게 되면 테이블과 매핑을 담당하는 JPA에 혼란을 주게 된다. 어떤 객체에서 수정을 할 때 외래 키를 수정할 지를 결정하기 어렵기 때문이다.</p>
<p>그래서 두 객체 간 연관관계의 주인을 정해서 명확하게 어떤 객체에서 수정을 할 때만 외래 키를 수정하겠다고 정하는 것이다.</p>
<h3 id="연관관계-매핑에서-자주하는-실수">연관관계 매핑에서 자주하는 실수</h3>
<h4 id="실수-1-mappedby-미사용">실수 1. <code>mappedBy</code> 미사용</h4>
<p>주인이 아닌 쪽에 <code>mappedBy</code>로 주인을 정하지 않는 실수를 많이 한다.</p>
<pre><code class="language-java">Team team = new Team();
team.setName(&quot;teamA&quot;);
em.persist(team);

Member member = new Member();
member.setName(&quot;memberA&quot;);

// Team class에 List&lt;Member&gt; members = new ArrayList&lt;&gt;(); 가 있다고 가정
team.getMembers().add(member);</code></pre>
<p>위 코드처럼 역방향만 연관관계를 설정했을 경우 <code>member</code> 객체에는 <code>team</code>의 <code>id</code> 값이 <code>null</code>로 들어갈 것이다. </p>
<h4 id="실수-2-순수-객체-상태를-고려하지-않음">실수 2. 순수 객체 상태를 고려하지 않음</h4>
<p>데이터베이스에 외래 키가 있는 테이블을 수정하려면 연관관계의 주인만 변경하는 것이 맞지만, 객체의 관점에선 둘 다 변경해주는 것이 좋다. 두 참조를 사용하는 순수한 객체는 데이터 동기화가 필요하기 때문이다.</p>
<ul>
<li>데이터 동기화 예시는 연관관계 편의 메서드 예시를 참고</li>
</ul>
<p>양쪽 어느 곳이든지 데이터 동기화를 위한 코드를 작성해도 상관 없지만 양쪽 모두에 동기화 코드를 작성하는 것은 무한 루프에 걸릴 수도 있으니 한 쪽에만 작성하도록 한다. 해당 코드는 방향대로 작성하든지 역방향에서 작성하든지 상관 없다. (상황에 따라 변경)</p>
<p>또한 단순 <code>setXxx</code> 형식의 메서드 네이밍은 해당 로직이 중요하다는 인식을 주지 못하기 때문에 이름을 바꿔 주는 것이 좋다.</p>
<ul>
<li>ex) <code>setTeam(Team team) -&gt; changeTeam(Team team)</code></li>
</ul>
<h4 id="실수-3-양방향-매핑-시-무한-루프">실수 3. 양방향 매핑 시 무한 루프</h4>
<p><code>toString()</code>이나 Lombok, JSON 생성 라이브러리 등을 생성했을 때 무한 루프에 걸릴 수 있다.</p>
<p>예시로 <code>toString()</code>을 생성했을 때를 보자.</p>
<pre><code class="language-java">@Override  
public String toString() {  
    return &quot;Member{&quot; +  
            &quot;id=&quot; + id +  
            &quot;, name=&#39;&quot; + name + &#39;\&#39;&#39; +  
            &quot;, team=&quot; + team +  
            &#39;}&#39;;  
}
---
@Override  
public String toString() {  
    return &quot;Team{&quot; +  
            &quot;id=&quot; + id +  
            &quot;, name=&#39;&quot; + name + &#39;\&#39;&#39; +  
            &quot;, members=&quot; + members +  
            &#39;}&#39;;  
}</code></pre>
<p>각 <code>toString()</code>안에 있는 <code>team</code>과 <code>members</code>는 각 클래스의 <code>toString()</code>을 호출한다는 뜻이다. 결론적으로 양쪽에서 무한으로 <code>toString()</code>을 호출하게 돼서 <code>StackOverflowError</code>가 발생하게 된다.</p>
<p>Lombok이나 JSON 생성 라이브러리 또한 마찬가지로 양방향이 걸려 있으면 엔티티를 JSON으로 변환하는 순간 무한 루프에 빠져버릴 수 있다. </p>
<p>특히 컨트롤러에서 엔티티를 그대로 반환할 때 주의하도록 한다.</p>
<ul>
<li>컨트롤러에서 엔티티를 그대로 반환했을 때 생길 수 있는 일<ul>
<li>무한 루프<ol>
<li>&quot;<code>Member</code> JSON으로 반환</li>
<li>근데 <code>Member</code> 안에 <code>Team</code>이 있네?</li>
<li><code>Team</code>을 JSON으로 반환</li>
<li>근데 <code>Team</code>안에 <code>Members</code>가 있네?&quot;</li>
<li>1 ~ 4 무한 반복</li>
</ol>
</li>
<li>엔티티의 변경<ul>
<li>엔티티를 API에 반환해버리면 나중에 그 엔티티를 변경하는 순간 API 스펙이 변경되는 것</li>
</ul>
</li>
</ul>
</li>
</ul>
<p>해결 방법에는 Lombok 등의 라이브러리 사용 시 <code>toString()</code>은 사용하지 않도록 하고, 컨트롤러에선 엔티티 반환 대신 DTO 객체를 만들어서 반환하도록 하자.</p>
<h1 id="references">References</h1>
<ul>
<li><a href="https://www.inflearn.com/course/ORM-JPA-Basic">https://www.inflearn.com/course/ORM-JPA-Basic</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[기본 키 매핑]]></title>
            <link>https://velog.io/@dereck-jun/%EA%B8%B0%EB%B3%B8-%ED%82%A4-%EB%A7%A4%ED%95%91</link>
            <guid>https://velog.io/@dereck-jun/%EA%B8%B0%EB%B3%B8-%ED%82%A4-%EB%A7%A4%ED%95%91</guid>
            <pubDate>Mon, 17 Feb 2025 18:47:13 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>김영한님의 JPA 강의를 보고 정리한 내용입니다.</p>
</blockquote>
<h1 id="기본-키-매핑">기본 키 매핑</h1>
<p>기본 키 매핑 어노테이션에는 <code>@Id</code>와 <code>@GeneratedValue</code>가 있다.</p>
<h2 id="기본-키-매핑-방법">기본 키 매핑 방법</h2>
<p>기본 키 매핑 방법에는 개발자가 PK 값을 직접 할당하는 <strong>직접 할당</strong>과 원하는 키 생성 전략을 선택해서 데이터베이스가 자동으로 PK 매핑을 하는 <strong>자동 생성</strong>이 있다.</p>
<h3 id="직접-할당">직접 할당</h3>
<p><code>@Id</code>만을 사용한다. 위에서 설명했던 것처럼 개발자가 직접 PK 값을 설정해서 저장하고 싶은 경우에 사용한다.</p>
<h3 id="자동-생성">자동 생성</h3>
<p><code>@Id</code>와 <code>@GeneratedValue</code>를 사용한다. 이때 각 데이터베이스마다 PK 값을 자동 생성하는 전략이 다르기 때문에 몇몇 옵션을 가지고 있다.</p>
<h2 id="자동-생성-전략">자동 생성 전략</h2>
<p>자동 생성 전략에는 <code>IDENTITY</code>, <code>SEQUENCE</code>, <code>TABLE</code>, <code>AUTO</code>가 있다.</p>
<h3 id="identity-전략">IDENTITY 전략</h3>
<ul>
<li>기본 키 생성 방법을 데이터베이스에 위임하는 것이다.<ul>
<li>영속성 컨텍스트에서 데이터베이스로 <code>sql</code>을 넘길 때 id 값을 <code>null</code>로 넘긴다.</li>
</ul>
</li>
<li>주로 <code>MySQL</code>, <code>PostgreSQL</code>, <code>SQL Server</code>, <code>DB2</code>에서 사용한다.<ul>
<li>ex) <code>MySQL</code> - <code>AUTO_INCREMENT</code></li>
</ul>
</li>
</ul>
<p>JPA는 보통 트랜잭션 커밋 시점에 <code>insert sql</code>을 실행한다. 하지만 <code>IDENTITY</code> 전략의 경우 데이터베이스에서 값을 설정하기 때문에 쿼리 실행 전까지 id 값이 <code>null</code>이 된다. 이 말은 영속성 컨텍스트의 1차 캐시에 저장할 수 없다는 말이 된다. </p>
<p>따라서 <code>IDENTITY</code> 전략의 경우 <code>em.persist()</code> 시점에 즉시 <code>insert sql</code>을 실행하고, JPA에서 내부적으로 해당 id 값을 데이터베이스에서 조회해서 가지고 온다. 그래서 영속성 컨텍스트에 저장이 된다.</p>
<p>결론적으로 데이터베이스에 <code>insert</code>하는 시점에 이 값을 바로 알 수 있다.</p>
<h3 id="sequence-전략">SEQUENCE 전략</h3>
<ul>
<li>데이터베이스 시퀀스는 유일한 값을 순서대로 생성하는 특별한 데이터베이스 오브젝트이다.</li>
<li>주로 <code>Oracle</code>, <code>PostgreSQL</code>, <code>DB2</code>, <code>H2</code> 데이터베이스에서 사용한다.</li>
<li><code>@SequenceGenerator</code>와 함께 사용할 수 있다.</li>
</ul>
<p><code>@SequenceGenerator</code> 속성</p>
<ul>
<li>name<ul>
<li>실제 <code>@Id</code> 필드에서 참조할 이름</li>
<li>필수로 입력해야 한다.</li>
</ul>
</li>
<li>sequenceName<ul>
<li>실제 데이터베이스에 생성되는 시퀀스 객체 이름</li>
<li>기본 값은 <code>hibernate_sequence</code>이고, 각 테이블마다 시퀀스를 따로 관리할 수 있음</li>
</ul>
</li>
<li>allocationsSize<ul>
<li>데이터베이스에서 가져오는 시퀀스 호출에 증가하는 값의 크기 (성능 최적화)<ul>
<li>여러 웹 서버가 있어도 동시성 문제 없이 다양한 문제 해결 가능</li>
<li>웹 서버를 내리는 시점에 가지고 있던 값이 날라간다.</li>
</ul>
</li>
<li>기본 값이 50이기 때문에 시퀀스 값이 하나씩 증가하도록 설정되어 있으면 이 값을 반드시 1로 설정해야 한다.</li>
</ul>
</li>
<li>initialValue<ul>
<li>DDL 생성 시에만 사용된다. DDL을 생성할 때 처음 시작하는 수를 지정</li>
<li>기본 값은 1</li>
</ul>
</li>
</ul>
<p><code>SEQUENCE</code> 전략 또한 <code>IDENTITY</code> 전략과 마찬가지로 id 값을 따로 설정하지 않는다. 하지만 <code>IDENTITY</code> 전략과는 다르게 <code>flush</code>가 바로 실행되지는 않는다. </p>
<p>대신 설정한 시퀀스에서 초기 값 또는 <code>next_val</code>의 값을 가져와서 해당 객체에 id 값을 넣은 뒤 영속성 컨텍스트에 저장한다.</p>
<p>결론적으로 PK 값만 얻어온 뒤에 실제로 <code>flush</code>는 커밋 직전에 수행되는 것이다. </p>
<blockquote>
<p>[!info] SEQUENCE 전략의 최적화
<code>SEQUENCE</code> 전략을 사용하고 <code>initialValue = 1</code>, <code>allocationsSize = 50</code> 인 경우 데이터베이스에서 시퀀스를 보면 <code>현재 값: -49</code>, <code>증가: 50</code>으로 되어있는 것을 알 수 있다.</p>
<p>그 이유는 보통 <code>call next value</code>를 한 번 호출해서 나온 값을 사용하는데, 처음 호출했을 때 1이 되기를 기대하기 때문이다. 이후 한번 더 호출을 해서 DB의 SEQ 값을 51로 맞추게 된다. (처음은 더미를 호출한 것이라고 생각)</p>
<p>SEQ 값을 51로 맞춘 이후부터는 DB에서 SEQ 값을 호출하지 않고, DB로부터 받아온 50개의 SEQ 값을 메모리에 캐싱받아 작동하게 된다. (메모리에서 작동한다는 말)</p>
<p>대신 51번을 만나는 순간 그 다음 50개를 확보해야 하기 때문에 <code>call next value</code>가 한번 호출된다.</p>
<p>값을 크게 잡아도 상관 없지만 웹 서버를 내리는 시점에 확보한 SEQ 값이 날라가기 때문에 그 숫자만큼 공백이 생기고, 다 사용하지 못하고 내려간 시점에서 낭비가 발생한 것이 되게 된다.</p>
</blockquote>
<h3 id="table-전략">TABLE 전략</h3>
<ul>
<li>키 생성 전용 테이블을 하나 만들어서 데이터베이스 시퀀스를 흉내내는 전략</li>
<li>특정 데이터베이스에 의존적이지 않다.<ul>
<li>어느 데이터베이스에나 사용할 수 있다.</li>
<li>성능 이슈가 존재한다.</li>
</ul>
</li>
<li>해당 전략 사용 시 <code>jpa-ddl-auto</code> 설정이 되어있지 않다면 시퀀스 테이블 생성이 선행돼야 한다.</li>
<li><code>@TableGenerator</code>와 함께 사용할 수 있다.</li>
</ul>
<p><code>@TableGenerator</code> 속성</p>
<ul>
<li>name<ul>
<li>실제 <code>@Id</code> 필드에서 참조할 이름</li>
<li>필수로 입력해야 한다.</li>
</ul>
</li>
<li>table<ul>
<li>시퀀스 생성 용 테이블 이름을 설정</li>
</ul>
</li>
<li>pkColumnName<ul>
<li>시퀀스 컬럼 이름을 설정</li>
<li>기본 값은 <code>sequence_name</code></li>
</ul>
</li>
<li>valueColumnName<ul>
<li>시퀀스 값 컬럼 이름을 설정</li>
<li>기본값은 <code>next_val</code></li>
</ul>
</li>
<li>pkColumnValue<ul>
<li>키로 사용할 값의 이름을 설정</li>
<li>기본 값은 엔티티 이름</li>
</ul>
</li>
<li>initialValue<ul>
<li>초기 값, 마지막으로 생성된 값이 기준</li>
<li>기본 값은 0</li>
</ul>
</li>
<li>allocationSize<ul>
<li>데이터베이스에서 가져오는 시퀀스 호출에 증가하는 값의 크기 (성능 최적화)</li>
<li>기본 값이 50이기 때문에 시퀀스 값이 하나씩 증가하도록 설정되어 있으면 이 값을 반드시 1로 설정해야 한다.</li>
</ul>
</li>
<li>catalog<ul>
<li>데이터베이스 <code>catalog</code> 이름</li>
</ul>
</li>
<li>scheme<ul>
<li>데이터베이스 <code>scheme</code> 이름</li>
</ul>
</li>
<li>uniqueConstraints (DDL)<ul>
<li>유니크 제약 조건을 지정할 수 있음</li>
</ul>
</li>
</ul>
<h3 id="auto-전략">AUTO 전략</h3>
<p>데이터베이스에 따라 자동으로 위의 3가지 전략 중 하나를 선택한다. 데이터베이스를 바꿔도 수정할 필요는 없지만 <code>SEQUENCE</code>나 <code>TABLE</code>의 경우 시퀀스나 키 테이블을 미리 생성해야 하는 것에 주의가 필요하다. </p>
<p><code>jpa-ddl-auto</code> 설정이 되어있다면 <code>hibernate</code>가 알아서 생성해 준다.</p>
<h2 id="권장하는-식별자-전략">권장하는 식별자 전략</h2>
<p>기본 키 제약 조건에 대해서 생각을 먼저 해보는 것이 필요하다. 기본 키 제약 조건은 <code>null</code>이면 안되고, 변하면 안된다. (유일성)</p>
<p>하지만 이 조건을 만족하는 자연 키는 찾기 어렵기 때문에 대리 키(대체 키)를 사용하는 것이 좋다.</p>
<ul>
<li>주민등록번호 역시 기본 키로는 적절하지 않다.</li>
<li>전화번호 역시 마찬가지</li>
</ul>
<p>권장하는 값은 &quot;<strong>Long 타입 + 대체 키 + 키 생성 전략 사용</strong>&quot;이다. 이때 <code>long</code> 타입이 아니라 참조 타입인 <code>Long</code>인 것에 주의</p>
<ul>
<li>데이터가 생성되는 시점에서 해당 값이 할당된다는 것이며, 도메인 객체의 id는 특정 시점에 존재할 수도 있고 존재하지 않을 수도 있다.  </li>
<li>그렇기 때문에 <code>long</code>이 아닌 <code>Long</code>을 사용하며, 다만 이 변수가 <code>not null</code> 이 보장된다면 <code>long</code>을 사용하는 것이 더 좋다.</li>
</ul>
<h1 id="references">References</h1>
<ul>
<li><a href="https://www.inflearn.com/course/ORM-JPA-Basic">https://www.inflearn.com/course/ORM-JPA-Basic</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[필드와 컬럼 매핑]]></title>
            <link>https://velog.io/@dereck-jun/%ED%95%84%EB%93%9C%EC%99%80-%EC%BB%AC%EB%9F%BC-%EB%A7%A4%ED%95%91</link>
            <guid>https://velog.io/@dereck-jun/%ED%95%84%EB%93%9C%EC%99%80-%EC%BB%AC%EB%9F%BC-%EB%A7%A4%ED%95%91</guid>
            <pubDate>Fri, 14 Feb 2025 11:50:21 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>김영한님의 JPA 강의를 보고 정리한 내용입니다.</p>
</blockquote>
<h1 id="필드와-컬럼-매핑">필드와 컬럼 매핑</h1>
<p>자세한 내용으로 들어가기에 앞서 간단하게 어떤 어노테이션이 있는지 알아보자</p>
<ul>
<li><code>@Column</code>: 컬럼 매핑</li>
<li><code>@Temporal</code>: 날짜 타입 매핑</li>
<li><code>@Enumurated</code>: enum 타입 매핑</li>
<li><code>@Lob</code>: BLOB, CLOB 타입 매핑</li>
<li><code>@Transient</code>: 특정 필드를 컬럼과 매핑하지 않음</li>
<li><code>@Access</code>: JPA가 엔티티에 접근하는 방식 설정</li>
</ul>
<h2 id="column">@Column</h2>
<p>엔티티의 필드를 테이블의 컬럼과 매핑한다.</p>
<h3 id="속성">속성</h3>
<ul>
<li>name<ul>
<li>필드와 매핑할 테이블의 컬럼 이름</li>
<li>기본 값은 객체의 필드 이름</li>
</ul>
</li>
<li>insertable/updatable<ul>
<li>등록/변경 가능 여부를 나타냄</li>
<li><code>false</code>여도 데이터베이스에서 강제로 변경은 가능하지만 JPA를 쓸 때는 반영 안됨</li>
<li>기본 값은 <code>true</code></li>
</ul>
</li>
<li>nullable(DDL)<ul>
<li><code>null</code> 값의 허용 여부를 설정한다. <code>false</code>로 설정하면 DDL 생성 시에 <code>not null</code> 제약조건이 붙는다.</li>
</ul>
</li>
<li>unique(DDL)<ul>
<li><code>@Table</code>의 <code>uniqueConstraints</code>와 같지만 한 컬럼에 간단히 유니크 제약 조건을 걸 때 사용한다.</li>
<li>하지만 컬럼에서 유니크 제약 조건을 거는 것은 그리 선호하지 않는 방법이다.<ul>
<li>제약조건의 이름이 무작위로 설정됨 -&gt; <code>uniqueConstraints</code>는 이름 설정 가능</li>
</ul>
</li>
</ul>
</li>
<li>columnDefinition(DDL)<ul>
<li>데이터베이스의 컬럼 정보를 직접 줄 수 있다. (자료형, 길이, 기본값 설정 가능)<ul>
<li><code>varchar(100) default &#39;EMPTY&#39;</code></li>
</ul>
</li>
<li>특정 데이터베이스의 종속적인 옵션들도 넣을 수 있다.</li>
<li>기본 값은 필드의 자바 타입과 방언 정보를 사용</li>
</ul>
</li>
<li>length(DDL)<ul>
<li>문자 길이 제약조건, <code>String</code> 타입에만 사용한다.</li>
<li>기본 값은 255</li>
</ul>
</li>
<li>precision/scale (DDL)<ul>
<li><code>BigDecimal</code> 타입에서 사용한다. (<code>BigInteger</code>도 사용할 수 있다)</li>
<li>참고로 <code>double</code>, <code>float</code> 타입에는 적용되지 않는다.</li>
<li><code>precision</code>은 소수점을 포함한 전체 자릿수를 정한다.<ul>
<li>기본 값은 19 (합쳐서 19개의 숫자)</li>
</ul>
</li>
<li><code>scale</code>은 소수의 자릿수를 정한다.<ul>
<li>기본 값은 2 (소수점 하위 2번째 자리)</li>
</ul>
</li>
</ul>
</li>
</ul>
<h2 id="enumurated">@Enumurated</h2>
<p><code>enum</code> 타입을 매핑할 때 사용한다.</p>
<h3 id="속성-1">속성</h3>
<p><code>@Enumurated</code>의 기본 값은 <code>EnumType.ORDINAL</code>이다.</p>
<ul>
<li>EnumType.ORDINAL: <code>enum</code>의 순서대로 데이터베이스에 저장<ul>
<li>선언된 순서를 데이터베이스에 반영하여 저장한다.</li>
<li><code>USER, ADMIN</code>으로 선언했을 경우<ul>
<li>0: <code>USER</code></li>
<li>1: <code>ADMIN</code></li>
</ul>
</li>
</ul>
</li>
<li>EnumType.STRING: <code>enum</code>의 이름을 문자열로 데이터베이스에 저장<ul>
<li>선언된 이름을 데이터베이스에 반영하여 저장한다.</li>
</ul>
</li>
</ul>
<h3 id="주의">주의</h3>
<p><code>@Enumurated</code>를 사용 시 옵션으로 <code>ORDINAL</code>을 사용하지 않도록 한다. </p>
<pre><code class="language-java">public enum RoleType {  
    USER, ADMIN  
}</code></pre>
<p>처음엔 위의 방식으로 <code>RoleType</code>을 선언하고 있다가 나중에 <code>GUEST</code>라는 <code>RoleType</code>이 새로 생겼다고 가정해보자.</p>
<pre><code class="language-java">public enum RoleType {  
    GUEST, USER, ADMIN  
}</code></pre>
<p>사람마다 다를 순 있겠지만 <code>GUEST</code>라는 값을 <code>ADMIN</code> 옆에 위치시키진 않을 것이다. 이렇게 새로운 <code>RoleType</code>을 선언한 뒤에 새로운 데이터가 들어온다면 기존에 데이터베이스에 있던 값은 어떻게 변할까?</p>
<p>간단하게 <code>id</code>, <code>name</code>, <code>role_type</code>을 컬럼으로 갖는 <code>Member</code> 테이블이 존재한다고 가정하고, 해당 테이블 안에 <code>USER</code>와 <code>ADMIN</code>을 <code>RoleType</code>으로 갖는 데이터가 각각 하나씩 존재한다고 했을 때 저장된 값은 다음과 같다.</p>
<p><code>GUEST</code> 선언 전</p>
<pre><code class="language-java">Member member1 = new Member();  
member1.setId(1L);  
member1.setName(&quot;A&quot;);  
member1.setRoleType(RoleType.USER);  
em.persist(member1);  

Member member2 = new Member();  
member2.setId(2L);  
member2.setName(&quot;B&quot;);  
member2.setRoleType(RoleType.ADMIN);  
em.persist(member2);  

tx.commit();</code></pre>
<p><img src="https://github.com/dereck-jun/TIL/raw/main/media/JPA/ordinal_ex_1.png" alt="ordinal_ex_1.png"> </p>
<br/>

<p><code>GUEST</code> 선언 후</p>
<pre><code class="language-java">Member member3 = new Member();  
member3.setId(3L);  
member3.setName(&quot;C&quot;);  
member3.setRoleType(RoleType.GUEST);  
em.persist(member3);  

tx.commit();</code></pre>
<p><img src="https://github.com/dereck-jun/TIL/raw/main/media/JPA/ordinal_ex_2.png" alt="ordinal_ex_2.png"> </p>
<p>예시 코드와 결과를 보면 enum의 순서는 바뀌었지만 이미 저장된 데이터는 바뀌지 않아서 결국 어떤 역할을 가지고 있는지 알 수 없게 된다. 따라서 데이터 마이그레이션 등을 수행해야 하는 불상사가 생기게 된다.</p>
<p>비록 <code>ORDINAL</code>보다 데이터를 더 많이 차지하는 것은 <code>STRING</code>이지만 위험을 가지고 운영을 하는 것보단 <code>STRING</code> 방식으로 저장하는 것이 더 나은 선택이다.</p>
<h2 id="temporal">@Temporal</h2>
<p>아래의 날짜 타입을 매핑할 때 사용한다.</p>
<ul>
<li><code>java.util.Date</code></li>
<li><code>java.util.Calendar</code></li>
</ul>
<blockquote>
<p><code>LocalDate</code> 또는 <code>LocalDateTime</code>을 사용하는 경우 데이터베이스 저장 시 자동으로 <code>date</code>와 <code>timestamp</code>로 매핑되어 생성되기 때문에 굳이 <code>@Temporal</code> 어노테이션을 사용할 필요가 없다.</p>
</blockquote>
<h3 id="속성-2">속성</h3>
<ul>
<li><code>TemporalType.TIME</code>: 시간을 데이터베이스 <code>time</code> 타입과 매핑</li>
<li><code>TemporalType.TIMESTAMP</code>: 날짜 + 시간을 데이터베이스 <code>timestamp</code> 타입과 매핑</li>
</ul>
<h2 id="lob">@Lob</h2>
<p>데이터베이스의 <code>BLOB</code>, <code>CLOB</code> 타입과 매핑할 때 사용한다.</p>
<p><code>@Lob</code>은 따로 지정할 수 있는 속성이 없다. 매핑하는 필드가 문자면 <code>CLOB</code>으로 매핑하고 그렇지 않다면 <code>BLOB</code>으로 매핑한다.</p>
<ul>
<li>BLOB(Binary Large OBject)<ul>
<li>이진 데이터를 저장할 때 사용되며, 주로 이미지, 오디오, 비디오, 바이너리 데이터 등을 저장할 때 사용된다.</li>
<li>자바 자료형으로 분류<ul>
<li><code>byte[]</code></li>
<li><code>java.sql.BLOB</code> </li>
</ul>
</li>
</ul>
</li>
<li>CLOB(Character Large OBject)<ul>
<li>문자 기반의 큰 객체를 나타내며, 주로 텍스트 데이터를 저장할 떄 사용된다.</li>
<li>자바 자료형으로 분류<ul>
<li><code>String</code></li>
<li><code>char[]</code></li>
<li><code>java.sql.CLOB</code></li>
</ul>
</li>
</ul>
</li>
</ul>
<h2 id="transient">@Transient</h2>
<p>엔티티의 필드를 매핑하고 싶지 않을 때 사용하는 어노테이션이다. 저장 및 조회도 하지 않기 때문에 메모리에서 사용하고 버려지는 값으로 사용할 수 있다.</p>
<h2 id="access">@Access</h2>
<p>JPA가 엔티티 데이터에 접근하는 방식을 지정한다.</p>
<h3 id="속성-3">속성</h3>
<ul>
<li><code>AccessType.FIELD</code><ul>
<li>필드에 직접 접근하며 <code>private</code>로 선언했더라도 접근이 가능하다.</li>
<li><code>@Id</code>가 필드에서 선언되었을 경우 해당 접근 방식은 생략이 가능하다.</li>
</ul>
</li>
<li><code>AccessType.PROPERTY</code><ul>
<li>접근자(getter/setter)를 통해서 접근이 가능하다.</li>
<li><code>@Id</code>가 프로퍼티에서 선언되었을 경우 해당 접근 방식은 생략 가능하다.</li>
</ul>
</li>
</ul>
<h1 id="references">References</h1>
<ul>
<li><a href="https://www.inflearn.com/course/ORM-JPA-Basic">https://www.inflearn.com/course/ORM-JPA-Basic</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[객체와 테이블 매핑]]></title>
            <link>https://velog.io/@dereck-jun/%EA%B0%9D%EC%B2%B4%EC%99%80-%ED%85%8C%EC%9D%B4%EB%B8%94-%EB%A7%A4%ED%95%91</link>
            <guid>https://velog.io/@dereck-jun/%EA%B0%9D%EC%B2%B4%EC%99%80-%ED%85%8C%EC%9D%B4%EB%B8%94-%EB%A7%A4%ED%95%91</guid>
            <pubDate>Thu, 13 Feb 2025 10:09:42 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>김영한님의 JPA 강의를 보고 정리한 내용입니다.</p>
</blockquote>
<h1 id="객체와-테이블-매핑">객체와 테이블 매핑</h1>
<p>JPA에서 제일 중요하게 봐야 되는 두 가지 중 하나는 JPA가 내부적으로 어떤 매커니즘으로 동작하는지에 대한 <strong>매커니즘적인 측면</strong>과 객체와 관계형 데이터베이스를 어떻게 매핑을 해서 사용하는지에 대한 <strong>정적인 측면</strong>으로 나뉜다. 쉽게 말하면 <strong>영속성 컨텍스트</strong>와 <strong>엔티티 매핑</strong>의 두 가지로 나뉜다고 볼 수 있을 것 같다.</p>
<p><strong>엔티티 매핑</strong>에는 다음과 같은 매핑 어노테이션이 있다. </p>
<ul>
<li>객체와 테이블 매핑<ul>
<li><code>@Entity</code></li>
<li><code>@Table</code></li>
</ul>
</li>
<li>필드와 컬럼 매핑<ul>
<li><code>@Column</code></li>
</ul>
</li>
<li>기본 키 매핑<ul>
<li><code>@Id</code></li>
</ul>
</li>
<li>연관관계 매핑<ul>
<li><code>@ManyToOne</code></li>
<li><code>@JoinColumn</code></li>
</ul>
</li>
</ul>
<h2 id="entity">@Entity</h2>
<ul>
<li><code>@Entity</code>가 붙은 클래스는 JPA가 관리하는 엔티티이다. 해당 어노테이션이 붙지 않으면 JPA와는 전혀 관계없는 그냥 내가 마음대로 쓰고 싶은 클래스라고 볼 수 있다.</li>
<li>JPA를 사용해서 테이블과 매핑할 클래스는 <code>@Entity</code>가 필수이다.</li>
</ul>
<blockquote>
<p><strong>주의</strong></p>
</blockquote>
<ul>
<li>기본 생성자 필수 (파라미터가 없는 <code>public</code> 또는 <code>protected</code> 생성자)</li>
<li><code>enum</code>, <code>interface</code>, <code>inner class</code>, <code>final class</code> 사용 불가</li>
<li>저장할 필드에 <code>final</code> 사용 불가</li>
</ul>
<h3 id="속성">속성</h3>
<ul>
<li><code>name</code><ul>
<li>JPA에서 사용할 엔티티의 이름을 지정한다. (JPA가 내부적으로 구분하는 이름)</li>
<li>기본 값은 클래스의 이름을 그대로 사용하는 것이다.</li>
<li>같은 클래스의 이름이 없으면 가급적 기본 값을 사용한다.</li>
</ul>
</li>
</ul>
<h2 id="table">@Table</h2>
<ul>
<li><code>@Table</code>은 엔티티와 매핑할 테이블을 지정한다.</li>
</ul>
<h3 id="속성-1">속성</h3>
<ul>
<li><code>name</code><ul>
<li>매핑할 테이블의 이름을 설정</li>
<li>기본 값은 엔티티의 이름을 그대로 사용하는 것</li>
</ul>
</li>
<li><code>catalog</code><ul>
<li>데이터베이스 <code>catalog</code> 매핑</li>
</ul>
</li>
<li><code>schema</code><ul>
<li>데이터베이스 <code>schema</code> 매핑</li>
</ul>
</li>
<li><code>uniqueConstraints</code><ul>
<li>DDL 생성 시 유니크 제약 조건 생성</li>
</ul>
</li>
</ul>
<h2 id="데이터베이스-스키마-자동-생성">데이터베이스 스키마 자동 생성</h2>
<ul>
<li>DDL을 애플리케이션 실행 시점에 자동 생성<ul>
<li>객체에서 매핑을 다 해놓으면 애플리케이션 사용할 때 필요하면 테이블을 다 만들어준다.</li>
</ul>
</li>
<li>테이블 중심 -&gt; 객체 중심</li>
<li>데이터베이스 방언을 통해 데이터베이스에 맞는 적절한 DDL을 생성해 준다.</li>
<li>이렇게 생성된 DDL은 개발 장비에서만 사용해야 한다.</li>
<li>생성된 DDL은 운영 서버에서는 사용하지 않거나, 적절히 다듬은 후 사용한다.</li>
</ul>
<h3 id="속성-2">속성</h3>
<p><code>persistence.xml</code> 설정 파일에 <code>hibernate.hbm2ddl.auto</code>의 값으로 아래의 속성을 적용한다. <code>application.properties</code>에 설정할 경우 <code>spring.jpa.hibernate.ddl-auto</code>로 설정할 수 있다.</p>
<ul>
<li>create<ul>
<li>애플리케이션 생성 시 기존 테이블 삭제 후 다시 생성한다.</li>
<li><code>애플리케이션 실행 -&gt; drop -&gt; create -&gt; 애플리케이션 종료</code></li>
</ul>
</li>
<li>create-drop<ul>
<li>create와 같으나 종료 시점에 테이블을 drop 한다.</li>
<li><code>애플리케이션 실행 -&gt; drop -&gt; create -&gt; drop -&gt; 애플리케이션 종료</code></li>
</ul>
</li>
<li>update<ul>
<li>기존 내용은 유지한 채 변경되는 것만 반영한다. (운영 DB에는 사용하면 안됨)<ul>
<li>필드 추가 -&gt; <code>alter table ... add column ...</code></li>
</ul>
</li>
<li>단, 필드 삭제의 경우는 반영하지 않는다.<ul>
<li>필드 삭제 -&gt; <code>...</code> (반영 X)</li>
</ul>
</li>
</ul>
</li>
<li>validate<ul>
<li>엔티티와 테이블이 정상 매핑되었는지만 확인한다.</li>
<li><code>필드 추가 -&gt; 실행 -&gt; 에러 발생 (엔티티와 테이블의 매핑 정보가 다름)</code></li>
</ul>
</li>
<li>none (관례)<ul>
<li>사용하지 않는다.</li>
<li><code>value = &quot;asdjfakjhdsfklj&quot;</code> 적는 것과 똑같지만 관례상 <code>none</code>으로 적는다. <ul>
<li>매칭되는 것이 없기 때문에 실행이 안되는 것</li>
</ul>
</li>
</ul>
</li>
</ul>
<blockquote>
<p><strong><a href="https://www.inflearn.com/community/questions/273750/application-yml-vs-persistence-xml?srsltid=AfmBOop9zwKHgBbx7tgxmWI5RXoTHkkPqBn7dhwoaMUPVZAgmDakOeIP"><code>persistence.xml</code>과 <code>application.properties</code>의 차이가 뭘까?</a></strong>
<code>persistence.xml</code>은 JPA가 사용하는 설정 파일인 반면, <code>application.properties</code>은 스프링 부트가 사용하는 설정 파일이다. 하지만 <code>persistence.xml</code>을 따로 만들지 않고, <code>application.properties</code>에 설정해도 정상적으로 동작하는 이유는 스프링 부트가 내부에서 JPA를 만들 때 <code>persistence.xml</code>이 없어도 동작하도록 구현되어 있기 때문이다. 따라서 스프링과 JPA를 함께 사용한다면 <code>application.properties</code>을 사용하자.</p>
</blockquote>
<h3 id="주의">주의</h3>
<ul>
<li>운영 장비에는 절대 <code>create</code>, <code>create-drop</code>, <code>update</code> 사용하면 안된다.</li>
<li>개발 초기에는 <code>create</code> 또는 <code>update</code></li>
<li>테스트 서버는 <code>update</code> 또는 <code>validate</code></li>
<li>스테이징과 운영 서버는 <code>none</code></li>
</ul>
<p>결국 이런 웹 애플리케이션에서 사용하는 DBMS 계정은 <code>alter</code>나 <code>drop</code>을 못하도록 <strong>계정을 분리</strong>하는 것이 맞다.</p>
<h2 id="ddl-생성-기능">DDL 생성 기능</h2>
<p>데이터베이스 스키마 자동 생성과 다른 것이다. (내가 잘 몰랐던 내용이라 넣어봄)</p>
<ul>
<li>제약 조건 추가: 회원 이름은 필수, 10자 제한<ul>
<li><code>@Column(nullable = false, length = 10)</code></li>
</ul>
</li>
<li>유니크 제약 조건 추가<ul>
<li><code>@Table(uniqueConstraints = {@UniqueConstraint(name = &quot;NAME_AGE_UNIQUE&quot;, columnNames = {&quot;NAME&quot;, &quot;AGE&quot;})})</code> </li>
<li><code>@Column(unique = true)</code></li>
</ul>
</li>
<li><a href="https://www.inflearn.com/community/questions/847958/16-25%EC%B4%88-%EB%B6%80%ED%84%B0%EC%9D%98-%EC%84%A4%EB%AA%85%EC%9D%B4-%EC%9D%B4%ED%95%B4%EA%B0%80-%EA%B0%80%EC%A7%80-%EC%95%8A%EC%8A%B5%EB%8B%88%EB%8B%A4">DDL 생성 기능은 DDL을 자동 생성할 때만 사용되고 JPA의 실행 로직에는 영향을 주지 않는다</a><ul>
<li>JPA의 런타임 시 기능은 결국 DB와 연결하여 <code>create</code>, <code>update</code>, <code>insert</code>, <code>delete</code> 쿼리를 날리는 것과 관련있다.</li>
<li>이런 점에서 봤을 때, <code>@Table</code>의 <code>name</code> 속성을 바꾸면 JPA는 해당 <code>name</code>에 있는 테이블명에 <code>create</code>, <code>update</code>, <code>insert</code>, <code>delete</code> 쿼리를 날리기 때문에 런타임 기능에 영향을 준다고 한 것 같다.</li>
<li>반면에 <code>@Column</code>의 속성값의 경우 DDL 생성이 켜져있을 때, 처음 애플리케이션 실행 시에만 DDL에서 작동할 뿐, JPA의 기능을 활용하는 런타임에서는 사용하지 않는다는 의미인 것 같다.</li>
</ul>
</li>
</ul>
<h1 id="references">References</h1>
<ul>
<li><a href="https://www.inflearn.com/course/ORM-JPA-Basic">https://www.inflearn.com/course/ORM-JPA-Basic</a></li>
<li><a href="https://www.inflearn.com/community/questions/847958/16-25%EC%B4%88-%EB%B6%80%ED%84%B0%EC%9D%98-%EC%84%A4%EB%AA%85%EC%9D%B4-%EC%9D%B4%ED%95%B4%EA%B0%80-%EA%B0%80%EC%A7%80-%EC%95%8A%EC%8A%B5%EB%8B%88%EB%8B%A4">https://www.inflearn.com/community/questions/847958/16-25%EC%B4%88-%EB%B6%80%ED%84%B0%EC%9D%98-%EC%84%A4%EB%AA%85%EC%9D%B4-%EC%9D%B4%ED%95%B4%EA%B0%80-%EA%B0%80%EC%A7%80-%EC%95%8A%EC%8A%B5%EB%8B%88%EB%8B%A4</a></li>
<li><a href="https://www.inflearn.com/community/questions/273750/application-yml-vs-persistence-xml?srsltid=AfmBOop9zwKHgBbx7tgxmWI5RXoTHkkPqBn7dhwoaMUPVZAgmDakOeIP">https://www.inflearn.com/community/questions/273750/application-yml-vs-persistence-xml?srsltid=AfmBOop9zwKHgBbx7tgxmWI5RXoTHkkPqBn7dhwoaMUPVZAgmDakOeIP</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[플러시]]></title>
            <link>https://velog.io/@dereck-jun/%ED%94%8C%EB%9F%AC%EC%8B%9C</link>
            <guid>https://velog.io/@dereck-jun/%ED%94%8C%EB%9F%AC%EC%8B%9C</guid>
            <pubDate>Wed, 12 Feb 2025 11:54:49 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>김영한님의 JPA 강의를 보고 정리한 내용입니다.</p>
</blockquote>
<h1 id="flush-란">Flush 란?</h1>
<p><strong>영속성 컨텍스트의 현재 변경 사항을 데이터베이스에 반영하는 행위</strong>를 말한다. 쉽게 말해, 영속성 컨텍스트에 쌓인 쓰기 지연 SQL 저장소의 쿼리를 데이터베이스에 전송함으로써, <strong>영속성 컨텍스트와 데이터베이스를 동기화하는 행위</strong>라고 할 수 있다.</p>
<p>이때 <code>flush</code>를 한다고 해서 영속성 컨텍스트를 비우는 것은 아니다. <strong>1차 캐시와 쓰기 지연 SQL 저장소 모두 비워지지 않는다. 단순히 변경 내용을 데이터베이스에 동기화하는 작업일 뿐</strong>이다. </p>
<p><code>flush</code>는 결국 <strong>트랜잭션이라는 개념이 있기 때문에 가능한 것</strong>이다. 쿼리를 따로 보내든지, 모아서 한번에 보내든지 <strong>커밋 직전에 동기화</strong>하면 되기 때문이다.</p>
<h2 id="flush-발생">Flush 발생</h2>
<ul>
<li><code>변경 감지(Dirty Checking)</code>가 일어난다.</li>
<li>수정된 엔티티를 <code>쓰기 지연 SQL 저장소</code>에 등록한다. </li>
<li>저장소 내의 쿼리를 데이터베이스에 전송한다.</li>
</ul>
<p>이때 <code>flush()</code>가 발생한다고 해서 데이터베이스 트랜잭션이 커밋되는 것은 아니다. 단순히 저장소 안에 있는 쿼리들이 데이터베이스에 전송되는 것이지 실제로 반영되는 시점은 <code>commit()</code> 시점이다.</p>
<blockquote>
<p>예시를 들자면 DBMS에서 트랜잭션을 사용할 때도 커밋이 되기 전까지는 실제로 데이터베이스에 반영되지 않는다. 그리고 커밋이 수행된 후에 실제 데이터베이스에 트랜잭션의 작업 내용이 반영 되는 것과 동일하다.</p>
</blockquote>
<h2 id="영속성-컨텍스트를-flush-하는-방법">영속성 컨텍스트를 Flush 하는 방법</h2>
<ul>
<li><code>em.flush()</code> - 직접 호출</li>
<li>트랜잭션 커밋 - 자동 호출</li>
<li>JPQL 쿼리 실행 - 자동 호출</li>
</ul>
<h2 id="flush-모드-옵션">Flush 모드 옵션</h2>
<ul>
<li><code>em.setFlushMode(FlushModeType.AUTO)</code>를 사용한다.<ul>
<li><code>FlushModeType.AUTO</code>: 커밋이나 쿼리를 실행할 때 <code>flush</code> 실행 (기본 값)</li>
<li><code>FlushModeType.COMMIT</code>: 커밋할 때만 <code>flush</code> 실행</li>
</ul>
</li>
</ul>
<h1 id="references">References</h1>
<ul>
<li><a href="https://www.inflearn.com/course/ORM-JPA-Basic">https://www.inflearn.com/course/ORM-JPA-Basic</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[일정 관리 애플리케이션 심화]]></title>
            <link>https://velog.io/@dereck-jun/%EC%9D%BC%EC%A0%95-%EA%B4%80%EB%A6%AC-%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EC%8B%AC%ED%99%94</link>
            <guid>https://velog.io/@dereck-jun/%EC%9D%BC%EC%A0%95-%EA%B4%80%EB%A6%AC-%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EC%8B%AC%ED%99%94</guid>
            <pubDate>Tue, 11 Feb 2025 15:28:49 GMT</pubDate>
            <description><![CDATA[<h1 id="들어가기-전에">들어가기 전에</h1>
<p>확실히 JDBC를 사용하다가 JPA를 사용하니 똑같은 쿼리를 작성해주지 않아도 돼서 상당히 편했다. 하지만 기능을 제대로 알고 있지 않을 경우 골치 아픈 일이 발생한다는 것을 다시 한 번 느끼게 되었다. 자세한 사항은 아래에 기술하였다.</p>
<p>단순 기능 구현 자체는 쉬워졌지만 어떻게 설계할 지 고민하는 시간이 늘어나는 것 같다. 심지어 검색을 해도 사람마다 생각하는 바가 달라서 정하기 쉽지 않았다.</p>
<p>또한 Git에 대한 공부를 더 해야할 것 같았다. 이번에 브랜치를 잘못 관리해서 PR을 올렸을 때 하나의 브랜치에 모든 정보가 다 섞여서 repo를 다시 만들어서 노가다를 하게 되었는데 다신 하고 싶지 않는 경험이었다.. (특히 팀 프로젝트에서 이런 실수를 했다고 생각하면..)</p>
<h1 id="잡다한-내용">잡다한 내용</h1>
<h2 id="springjpaopen-in-view">spring.jpa.open-in-view</h2>
<p>설정을 안한다고 해서 문제 발생하진 않았던 것 같다. 하지만 매번 <code>WARN</code>이 나와서 이번에서야 검색을 하게 되었다.</p>
<p><code>spring.jpa.open-in-view</code>를 따로 설정하지 않았다면 스프링 부트 애플리케이션 시작 시 아래와 같은 메세지가 뜬다.</p>
<pre><code class="language-cmd">[startup] [ ] 17:22:33.916 [main] WARN  o.s.b.a.o.j.JpaBaseConfiguration$JpaWebConfiguration - 
spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during 
view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning</code></pre>
<p>이 메세지는 스프링 부트에선 <code>spring.jpa.open-in-view</code>를 <code>true</code>로 설정하고 있는데 이는 OSIV(<code>Open Session In View</code>) 측면에서 매우 부적절하다고 한다. </p>
<p>정말 간단히 말하면 Controller나 View에서도 지연로딩이 가능하게 만들어 주는 것이다. 즉, 영속성 컨텍스트의 생존 범위가 굉장히 넓어지는 것이다. </p>
<p>부적절하다고 하는 이유는 데이터베이스 커넥션 리소스를 사용하기 때문이다. Controller에서 외부 API를 호출하면 외부 API 대기 시간만큼 커넥션 리소스를 반환하지 못하게 돼서 트래픽이 많은 경우에는 꺼둔다고 한다.</p>
<p>하지만 OSIV를 <code>false</code>로 설정한 경우에도 단점은 존재한다. 우선 영속성 컨텍스트의 범위가 좁혀지기 때문에 모든 지연로딩을 트랜잭션 내부에서 처리해야 한다는 것이다. 트랜잭션이 끝난 뒤 지연로딩을 하려고 하면 <code>LazyInitializationException</code> 발생한다.</p>
<p>OSIV를 종료하는 방법은 <code>spring.jpa.open-in-view</code> 설정을 <code>false</code>로 바꿔주면 된다.</p>
<pre><code class="language-yml">spring:
  jpa:
    open-in-view: false</code></pre>
<blockquote>
<p>간단한 애플리케이션이기 때문에 굳이 <code>false</code>로 설정할 필요는 없었지만, 트랜잭션 외부에서 지연로딩하는 코드가 없다고 판단하여 <code>false</code> 설정하였다. </p>
<p>OSIV와 Transactional, 영속성 컨텍스트에 대한 개념을 더 알아본 뒤 생각이 바뀌면 나중에 바꿀 예정이다.</p>
</blockquote>
<h2 id="데이터베이스-정보-환경-변수에-저장">데이터베이스 정보 환경 변수에 저장</h2>
<p>이전 과제 피드백에 있던 내용이라 바로 적용했다. 하나 생각하지 못했던 것은 진짜 환경 변수에 저장하고 불러오려고 했다는 것이다. 뭔가 이상하다고 생각이 들어서 검색을 해봤고, 쉽게 방법을 찾을 수 있었다.</p>
<ol>
<li><p><code>application.yml</code>에 가져올 환경 변수 세팅</p>
<pre><code class="language-yml">spring:
 datasource:
   driver-class-name: com.mysql.cj.jdbc.Driver
   url: jdbc:mysql://localhost:${MYSQL_PORT}/${MYSQL_DB}
   username: ${MYSQL_USERNAME}
   password: ${MYSQL_PASSWORD}</code></pre>
<p><code>${KEY}</code>를 통해 환경 변수에 저장된 <code>KEY</code>에서 값을 가져온다.</p>
</li>
<li><p>실행 -&gt; 구성 편집 (Run -&gt; Edit Configurations...)
  <img src="https://velog.velcdn.com/images/dereck-jun/post/50751cb6-29c5-4222-ba2d-43e91f8be910/image.png" alt=""></p>
<p> 또는 애플리케이션 실행 버튼 옆의 이름이나 ...을 클릭</p>
<p> <img src="https://velog.velcdn.com/images/dereck-jun/post/b1fb3d19-c4d8-4e1b-8f53-2b931e2a0333/image.png" alt=""></p>
<p> <img src="https://velog.velcdn.com/images/dereck-jun/post/bc89bf8d-08cd-48f3-9451-eda184372771/image.png" alt=""></p>
</li>
</ol>
<ol start="3">
<li><p>스프링 부트 실행 클래스를 선택 -&gt; 옵션 수정(Modify options) -&gt; 환경 변수(Environment variables)</p>
<p> <img src="https://velog.velcdn.com/images/dereck-jun/post/1f6590b6-924a-4767-b930-b3068988cbd7/image.png" alt=""></p>
</li>
</ol>
<ol start="4">
<li><p>환경 변수 추가</p>
<p>이때 <code>key1=value1;key2=value2;...</code> 방식으로 직접 작성해도 되고, 아래처럼 해도 된다.</p>
<p><img src="https://velog.velcdn.com/images/dereck-jun/post/ea52be7b-c0a5-4062-a8ed-45ccf55bdf32/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/dereck-jun/post/464487cc-dbe8-4247-bba9-8f516cb1c294/image.png" alt=""></p>
</li>
</ol>
<h2 id="로그-필터-적용">로그 필터 적용</h2>
<p>필터를 사용하는 만큼 모든 요청과 응답에 대한 로그를 필터를 통해 찍어보고 싶었다. </p>
<p><img src="https://velog.velcdn.com/images/dereck-jun/post/48d476ef-6934-4a13-8a63-2d6c0159a87a/image.png" alt=""></p>
<p><code>Filter</code>를 구현한 로그 필터 클래스를 만들고 <code>doFilter</code>를 구현했다. 로그인 필터에서도 동일한 값을 사용하기 위해 <code>MDC.put()</code>으로 값을 저장하고, <code>chain.doFilter(request, response)</code> 앞뒤에 로그를 찍어주었다.</p>
<p>응답 로그 이후엔 <code>MDC.clear()</code>를 통해 저장된 값을 초기화했다.</p>
<pre><code class="language-xml">&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; ?&gt;

&lt;configuration&gt;

    &lt;appender name=&quot;CONSOLE&quot; class=&quot;ch.qos.logback.core.ConsoleAppender&quot;&gt;
        &lt;layout class=&quot;ch.qos.logback.classic.PatternLayout&quot;&gt;
            &lt;Pattern&gt;[%X{request_id:-startup}] [%X{http_method} %X{request_uri}] %d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n&lt;/Pattern&gt;
        &lt;/layout&gt;
    &lt;/appender&gt;

    &lt;root level=&quot;info&quot;&gt;
        &lt;appender-ref ref=&quot;CONSOLE&quot; /&gt;
    &lt;/root&gt;
&lt;/configuration&gt;</code></pre>
<p><code>logback.xml</code>을 만들어서 로그가 찍힐 때 패턴을 정의해줬다. 고유한 ID 값을 <code>UUID</code>를 통해 만들어서 넣고, HTTP Method와 Request URI 값을 확인할 수 있도록 구성했다.</p>
<h2 id="page-interface의-직렬화">Page Interface의 직렬화</h2>
<p>일정 전체 조회에 페이징을 적용하고 있던 중 콘솔에 <code>WARN</code> 표시가 나왔다.</p>
<pre><code class="language-cmd">[2b441087-aae7-4098-94a2-ffc8d4b114c7] [GET /api/v1/schedules] 19:30:00.644 [http-nio-8080-exec-6] WARN  o.s.d.w.c.SpringDataJacksonConfiguration$PageModule$WarningLoggingModifier - Serializing PageImpl instances as-is is not supported, meaning that there is no guarantee about the stability of the resulting JSON structure!
    For a stable JSON structure, please use Spring Data&#39;s PagedModel (globally via @EnableSpringDataWebSupport(pageSerializationMode = VIA_DTO))
    or Spring HATEOAS and Spring Data&#39;s PagedResourcesAssembler as documented in https://docs.spring.io/spring-data/commons/reference/repositories/core-extensions.html#core.web.pageables.</code></pre>
<p>번역하면 <code>PageImpl 인스턴스를 있는 그대로 직렬화하는 것은 지원되지 않으므로 결과 JSON 구조의 안정성에 대해 보장할 수 없습니다!</code> 라는 뜻인데 말로는 이해가 안될 수 있으니 <a href="https://docs.spring.io/spring-data/commons/reference/repositories/core-extensions.html#core.web.page.config">여기</a>에 있는 어노테이션을 적용시키기 전과 후의 결과 차이를 보면 다음과 같다.</p>
<pre><code class="language-json">// 적용시키기 전
{
    &quot;status&quot;: &quot;OK&quot;,
    &quot;data&quot;: {
        &quot;content&quot;: [
            {
                &quot;scheduleId&quot;: 8,
                &quot;title&quot;: &quot;타이틀&quot;,
                &quot;body&quot;: &quot;본문&quot;,
                &quot;createdDateTime&quot;: &quot;2025-02-11T14:47:59.864844&quot;,
                &quot;updatedDateTime&quot;: &quot;2025-02-11T14:51:41.952323&quot;,
                &quot;username&quot;: &quot;tester&quot;,
                &quot;commentCount&quot;: 0
            },
            // 결과들...
        ],
        &quot;pageable&quot;: {
            &quot;pageNumber&quot;: 0,
            &quot;pageSize&quot;: 10,
            &quot;sort&quot;: {
                &quot;empty&quot;: false,
                &quot;sorted&quot;: true,
                &quot;unsorted&quot;: false
            },
            &quot;offset&quot;: 0,
            &quot;paged&quot;: true,
            &quot;unpaged&quot;: false
        },
        &quot;last&quot;: true,
        &quot;totalPages&quot;: 1,
        &quot;totalElements&quot;: 3,
        &quot;first&quot;: true,
        &quot;size&quot;: 10,
        &quot;number&quot;: 0,
        &quot;sort&quot;: {
            &quot;empty&quot;: false,
            &quot;sorted&quot;: true,
            &quot;unsorted&quot;: false
        },
        &quot;numberOfElements&quot;: 3,
        &quot;empty&quot;: false
    },
    &quot;message&quot;: &quot;일정 전체 조회 성공&quot;,
    &quot;timestamp&quot;: &quot;2025-02-11T19:40:56.4995927&quot;
}</code></pre>
<pre><code class="language-json">// 적용시킨 후
{
    &quot;status&quot;: &quot;OK&quot;,
    &quot;data&quot;: {
        &quot;content&quot;: [
            {
                &quot;scheduleId&quot;: 8,
                &quot;title&quot;: &quot;타이틀&quot;,
                &quot;body&quot;: &quot;본문&quot;,
                &quot;createdDateTime&quot;: &quot;2025-02-11T14:47:59.864844&quot;,
                &quot;updatedDateTime&quot;: &quot;2025-02-11T14:51:41.952323&quot;,
                &quot;username&quot;: &quot;tester&quot;,
                &quot;commentCount&quot;: 0
            },
            // 결과들...
        ],
        &quot;page&quot;: {
            &quot;size&quot;: 10,
            &quot;number&quot;: 0,
            &quot;totalElements&quot;: 3,
            &quot;totalPages&quot;: 1
        }
    },
    &quot;message&quot;: &quot;일정 전체 조회 성공&quot;,
    &quot;timestamp&quot;: &quot;2025-02-11T19:43:02.4308486&quot;
}</code></pre>
<p>확실히 더 깔끔해졌다. 또한 내부 JavaDoc을 보면 다음과 같이 나와있는 것을 확인할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/dereck-jun/post/1cb383b2-c9f3-492f-a427-7a894c83d193/image.png" alt=""></p>
<blockquote>
<p>해당 어노테이션을 사용하기 위해선 Spring 3.2 이상이 필요하다고 한다.</p>
</blockquote>
<h1 id="트러블-슈팅">트러블 슈팅</h1>
<h2 id="sqlrestriction-사용으로-인한-문제점"><code>@SQLRestriction</code> 사용으로 인한 문제점</h2>
<p>현재 각 엔티티에는 소프트 딜리트 방식을 손쉽게 구현 및 사용하기 위한 다음과 같은 어노테이션이 클래스에 정의되어 있다.</p>
<pre><code class="language-java">@SQLDelete(sql = &quot;update users set deleted_date_time = current_timestamp where user_id = ?&quot;)
@SQLRestriction(value = &quot;deleted_date_time is null&quot;)</code></pre>
<p>예시로 유저가 삭제되면 실제로 삭제하는 것이 아니라 <code>@SQLDelete</code> 안의 <code>sql</code>을 실행해서 <code>deleted_date_time</code> 값에 <code>current_timestamp</code>를 찍어주고, 조회를 할 땐 <code>deleted_date_time</code>에 값이 없는 것만 필터링해서 가져올 수 있도록 해주는 것이다.</p>
<p>이후 Postman을 활용해서 테스트를 진행했고, 문제는 유저 탈퇴 이후 처음 알게 되었다. </p>
<p>회원가입, 일정 단건 조회 등등에서 다양한 예외가 발생했다. </p>
<pre><code class="language-cmd">[Exception]: Unable to find com.example.schedule.user.entity.User with id 1
[Exception]: could not execute statement [Duplicate entry &#39;tester@test.com&#39; for key &#39;users.UK6dotkott2kjsp8vw4d0m25fb7&#39;] [insert into users (created_date_time,deleted_date_time,email,password,updated_date_time,username) values (?,?,?,?,?,?)]; SQL [insert into users (created_date_time,deleted_date_time,email,password,updated_date_time,username) values (?,?,?,?,?,?)]; constraint [users.UK6dotkott2kjsp8vw4d0m25fb7]</code></pre>
<p>ControllerAdvice에 정의한 <code>Exception</code>을 주석처리 한 뒤 다시 실행시켜 봤다.</p>
<pre><code class="language-cmd">jakarta.persistence.EntityNotFoundException: Unable to find com.example.schedule.user.entity.User with id 1
java.sql.SQLIntegrityConstraintViolationException: Duplicate entry &#39;tester@test.com&#39; for key &#39;users.UK6dotkott2kjsp8vw4d0m25fb7&#39;</code></pre>
<p>하나는 엔티티를 찾을 수 없다고 나오고, 다른 하나는 unique 제약 조건을 걸었는데 값이 겹친다고 나온다. </p>
<p>중단점을 찍어서 자세히 확인해보면 엔티티를 찾을 수 없다고 한 경우엔 탈퇴한 유저가 작성한 일정을 단건 조회 등을 할 경우 발생했고, 정확히는 <code>Schedule</code>을 <code>ScheduleDto</code>로 매핑하는 도중 <code>Schedule</code> 엔티티에서 <code>User</code> 값을 <code>getter</code>로 가져오면서 발생했다.</p>
<p>회원가입의 경우엔 탈퇴한 유저와 같은 <code>email</code> 또는 <code>username</code>을 가지고 등록을 시도할 경우 발생했고, 정확히는 중복 체크 구문을 전부 문제 없이 통과하고, 새로운 <code>User</code> 엔티티를 생성한 뒤에 데이터베이스에 저장 시도할 때 예외가 발생했다.</p>
<p><code>@SQLRestriction</code> 의 문제라고 생각이 들어서 제거하고, 서비스에서 필요한 로직에 따라서  <code>deleted_date_time</code> 값에 따라 처리할 수 있도록 바꾸면 해결될 것이라 생각했다.</p>
<p><img src="https://velog.velcdn.com/images/dereck-jun/post/449d4d1a-a327-43c4-bfb7-8feb48664a18/image.png" alt=""></p>
<p><code>deleted_date_time</code> 필요 여부에 따라서 JPQL을 직접 만들었다. <code>existsXxx</code>의 경우엔 회원가입 등에서 탈퇴한 유저의 정보까지 비교할 수 있도록 <code>deleted_date_time</code>까지 확인하도록 했다.</p>
<p>그리고 정상적으로 원하는 예외를 발생시킬 수 있게 되었다.</p>
<pre><code class="language-json">// 탈퇴한 유저의 이메일이나 사용자명과 중복될 경우 바꾸기 전까진 Exception에 핸들링 되었음
{
    &quot;status&quot;: &quot;CONFLICT&quot;,
    &quot;errorDetails&quot;: [
        {
            &quot;field&quot;: &quot;email&quot;,
            &quot;message&quot;: &quot;중복된 이메일입니다.&quot;,
            &quot;code&quot;: &quot;DUPLICATED&quot;
        },
        {
            &quot;field&quot;: &quot;username&quot;,
            &quot;message&quot;: &quot;중복된 사용자명입니다.&quot;,
            &quot;code&quot;: &quot;DUPLICATED&quot;
        }
    ],
    &quot;timestamp&quot;: &quot;2025-02-11T15:45:38.3245642&quot;
}</code></pre>
<pre><code class="language-json">// 일정이 탈퇴한 유저와 연결되어 있는 경우 바꾸기 전까진 Exception에 핸들링 되었음
{
    &quot;status&quot;: &quot;NOT_FOUND&quot;,
    &quot;errorDetails&quot;: [
        {
            &quot;field&quot;: null,
            &quot;message&quot;: &quot;요청에 해당하는 일정을 찾을 수 없습니다.&quot;,
            &quot;code&quot;: &quot;NOT_FOUND&quot;
        }
    ],
    &quot;timestamp&quot;: &quot;2025-02-11T14:48:52.9056308&quot;
}</code></pre>
<h2 id="json-직렬화-문제">JSON 직렬화 문제</h2>
<p><code>LoginFilter</code>에서 세션의 값을 확인하고, 없을 경우 바로 예외를 던져서 처리하고자 했다. 하지만 간과하지 못한 사실이 하나 있었다.</p>
<p>바로 Filter의 위치이다. Filter는 WAS 단에서 확인하기 때문에 예외가 발생해도 Spring Context 바깥에서 발생한 예외이기 때문에 정상적으로 처리할 수 없었다. 하지만 <code>try-catch</code>를 통해 바로 응답을 내려줄 수 있었다.</p>
<p><img src="https://velog.velcdn.com/images/dereck-jun/post/e566b7bb-d385-4d47-be1e-e1c29c04fa9a/image.png" alt=""></p>
<p>현재 응답 실패에 대한 공통 응답 클래스를 사용하고 있기 때문에 필터에서 내리는 응답도 마찬가지로 구조를 맞춰서 응답을 하려고 했다.</p>
<p>처음 생각한 방식은 무식하게 전부 적어주는 것이었다. 어차피 발생하는 예외는 응답 실패 시간을 제외하면 항상 동일하기 때문에 가장 먼저 생각하게 되었다. 하지만 JSON 응답 구조를 하나하나 만드는 것은 너무 비효율적이라고 생각이 들어 검색을 했고, <code>ObjectMapper</code>를 통해 객체를 JSON 문자열로 바꿀 수 있다고 보게 되었다.</p>
<p><img src="https://velog.velcdn.com/images/dereck-jun/post/2b9cb1b8-38a2-48c3-87c8-759202bfbffc/image.png" alt=""></p>
<p>그래서 바로 적용한 뒤에 테스트를 돌려보니...</p>
<pre><code class="language-cmd">com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Java 8 date/time type `java.time.LocalDateTime` not supported by default: add Module &quot;com.fasterxml.jackson.datatype:jackson-datatype-jsr310&quot; to enable handling (through reference chain: com.example.schedule.global.common.response.ErrorResponse[&quot;timestamp&quot;])</code></pre>
<p>생각하지도 못한 예외가 발생했다. 해당 예외를 번역하면 <code>Java 8 날짜/시간 유형 java.time.LocalDateTime이 기본적으로 지원되지 않음: &quot;com.fasterxml.jackson.datatype:jackson-datatype-jsr310&quot; 모듈을 추가하여 처리를 활성화합니다.</code> 라는 의미였다. 해결하기 위해서 여러 곳을 확인하고 추가해봤지만 유의미한 결과를 보인 방법은 다음과 같이 <code>ObjectMapper</code>에 추가 설정을 하는 방법이었다.</p>
<p><img src="https://velog.velcdn.com/images/dereck-jun/post/712c8df5-9e25-40b1-9137-3be51b2c53b1/image.png" alt=""></p>
<p><code>ObjectMapper</code> 객체를 생성하고, <code>objectMapper.registerModule(new JavaTimeModule()).disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);</code>을 설정해서 날짜 및 시간 클래스(<code>LocalDate</code>, <code>LocalDateTime</code> 등)를 직렬화/역직렬화할 수 있도록 지원하는 모듈을 등록하고, JSON으로 출력되는 날짜 정보를 표준화된 문자열 포맷으로 제공할 수 있게 <code>SerializationFeature.WRITE_DATES_AS_TIMESTAMPS</code>를 <code>disable</code> 처리해주는 것으로 해결했다.</p>
<pre><code class="language-json">{
    &quot;status&quot;: &quot;UNAUTHORIZED&quot;,
    &quot;errorDetails&quot;: [
        {
            &quot;field&quot;: null,
            &quot;message&quot;: &quot;로그인이 필요한 서비스입니다. 로그인을 해주세요.&quot;,
            &quot;code&quot;: &quot;UN_AUTHORIZED&quot;
        }
    ],
    &quot;timestamp&quot;: &quot;2025-02-11T16:46:28.7494056&quot;
}</code></pre>
<blockquote>
<p><code>ObjectMapper</code>의 사용 방법이 바뀐다고 하는데 정확한 사용 방법을 몰라서 현재 작성일 기준 사용할 수 있는 코드를 우선 사용했다.</p>
</blockquote>
<h2 id="json-직렬화-시-동일-필드-직렬화">JSON 직렬화 시 동일 필드 직렬화</h2>
<p>응답 실패에 대한 공통 응답 클래스에 필드로 위치하는 <code>ErrorDetail</code> 클래스에서 JSON 직렬화 시 응답 필드를 동일하게 설정하기 위해서 <code>@JsonProperty(value = &quot;code&quot;)</code>를 통해 동일한 이름으로 설정했다.</p>
<p><img src="https://velog.velcdn.com/images/dereck-jun/post/9f043ae1-2513-4d8d-a93f-6369093023d8/image.png" alt=""></p>
<p>하지만 테스트를 실행해보니...</p>
<pre><code class="language-cmd">com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Conflicting getter definitions for property &quot;code&quot;: com.example.schedule.global.common.exception.ErrorDetail#getCode() vs com.example.schedule.global.common.exception.ErrorDetail#getErrorCode() (through reference chain: com.example.schedule.global.common.response.ErrorResponse[&quot;errorDetails&quot;]-&gt;java.util.ImmutableCollections$List12[0])</code></pre>
<p>또 다른 예외가 날 반겨주었다. 간단한 설명을 하자면 <code>code</code> 값에 대한 <code>getter</code>의 충돌로 인해 문제가 생겼다는 의미이다. 그리고 JSON은 직렬화 시 접근 제한자에 따라서 직렬화하는 방식이 다른데 private의 경우 getter를 통해 직렬화를 하게 된다는 사실을 알게 되었다.</p>
<p>그래서 위의 코드에서 명시적으로 getter를 하나 설정해줬다.</p>
<pre><code class="language-java">@JsonProperty(&quot;code&quot;)
public String getSerializedCode() {
    return code != null
        ? code : (errorCode != null ? errorCode.name() : null);
}</code></pre>
<p>이렇게 되면 직렬화 시 getter 메서드가 우선적으로 사용되기 때문에 위의 메서드를 호출하여 JSON의 <code>code</code> 프로퍼티 값을 생성하게 된다. 따라서 이름을 똑같게 만들어도 접근 제한자 상에서 우위를 가지는 public에 우선적으로 사용되게 되어서 정상적으로 결과를 응답할 수 있게 되는 것이다.</p>
<h1 id="references">References</h1>
<ul>
<li><a href="https://docs.spring.io/spring-data/commons/reference/repositories/core-extensions.html#core.web.page.config">https://docs.spring.io/spring-data/commons/reference/repositories/core-extensions.html#core.web.page.config</a> ([Spring] Page Interface의 직렬화)</li>
<li><a href="https://gony-dev.tistory.com/33">https://gony-dev.tistory.com/33</a> ([Spring] Page Interface의 직렬화)<br /></li>
<li><a href="https://minoolian.github.io/tech/Objectmapper-serialize.html">https://minoolian.github.io/tech/Objectmapper-serialize.html</a> (LocalDateTime Json 직렬화 시 문제)</li>
<li><a href="https://pjh3749.tistory.com/281">https://pjh3749.tistory.com/281</a> (Jackson 라이브러리 기본 기능 정리)<br /></li>
<li><a href="https://velog.io/@hyunsoo730/spring-Data-JPA-%ED%8E%98%EC%9D%B4%EC%A7%95-%EC%A0%81%EC%9A%A9">https://velog.io/@hyunsoo730/spring-Data-JPA-%ED%8E%98%EC%9D%B4%EC%A7%95-%EC%A0%81%EC%9A%A9</a> (JPA 페이징 적용)<br /></li>
<li><a href="https://www.inflearn.com/community/questions/137740/orphanremoval%EA%B3%BC-cascade%EC%9D%98-%EA%B4%80%EA%B3%84?srsltid=AfmBOoqYLsqNiuJzj99m3XKubSw3Bb1TXPtqxk6jZvBy4OFlP2M51lK1">https://www.inflearn.com/community/questions/137740/orphanremoval%EA%B3%BC-cascade%EC%9D%98-%EA%B4%80%EA%B3%84?srsltid=AfmBOoqYLsqNiuJzj99m3XKubSw3Bb1TXPtqxk6jZvBy4OFlP2M51lK1</a> (orphanRemoval과 cascade의 관계)<br /></li>
<li><a href="https://beaniejoy.tistory.com/96">https://beaniejoy.tistory.com/96</a> (filter 관련)<br /></li>
<li><a href="https://mangkyu.tistory.com/266">https://mangkyu.tistory.com/266</a> (logback mdc 관련 정보)</li>
<li><a href="https://logback.qos.ch/manual/mdc.html">https://logback.qos.ch/manual/mdc.html</a> (logback mdc 관련 정보)</li>
<li><a href="https://github.com/qos-ch/logback/blob/master/logback-examples/src/main/resources/chapters/mdc/simpleMDC.xml">https://github.com/qos-ch/logback/blob/master/logback-examples/src/main/resources/chapters/mdc/simpleMDC.xml</a> (logback mdc 관련 정보)<br /></li>
<li><a href="https://mchch.tistory.com/282">https://mchch.tistory.com/282</a> (database 정보 환경 변수에 저장하기)<br /></li>
<li><a href="https://mand2.github.io/spring/spring-boot/1/">https://mand2.github.io/spring/spring-boot/1/</a> (spring.jpa.open-in-view 로그 오류 해결하기)</li>
<li><a href="https://stackoverflow.com/questions/30549489/what-is-this-spring-jpa-open-in-view-true-property-in-spring-boot">https://stackoverflow.com/questions/30549489/what-is-this-spring-jpa-open-in-view-true-property-in-spring-boot</a> (spring.jpa.open-in-view 로그 오류 해결하기)</li>
<li><a href="https://hstory0208.tistory.com/entry/SpringJPA-OSIV-%EC%A0%84%EB%9E%B5%EC%9D%B4%EB%9E%80-%EC%96%B8%EC%A0%9C-%EC%82%AC%EC%9A%A9%ED%95%B4%EC%95%BC-%ED%95%A0%EA%B9%8C">https://hstory0208.tistory.com/entry/SpringJPA-OSIV-%EC%A0%84%EB%9E%B5%EC%9D%B4%EB%9E%80-%EC%96%B8%EC%A0%9C-%EC%82%AC%EC%9A%A9%ED%95%B4%EC%95%BC-%ED%95%A0%EA%B9%8C</a> (OSIV 전략이란 언제 사용해야 할까)</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[영속성 컨텍스트]]></title>
            <link>https://velog.io/@dereck-jun/%EC%98%81%EC%86%8D%EC%84%B1-%EC%BB%A8%ED%85%8D%EC%8A%A4%ED%8A%B8</link>
            <guid>https://velog.io/@dereck-jun/%EC%98%81%EC%86%8D%EC%84%B1-%EC%BB%A8%ED%85%8D%EC%8A%A4%ED%8A%B8</guid>
            <pubDate>Tue, 11 Feb 2025 13:57:26 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>김영한님의 JPA 강의를 보고 정리한 내용입니다.</p>
</blockquote>
<h2 id="영속성-컨텍스트란">영속성 컨텍스트란?</h2>
<ul>
<li>JPA를 이해하는데 가장 중요한 용어</li>
<li>&quot;<strong>엔티티를 영구 저장하는 환경</strong>&quot;이라는 뜻</li>
<li><code>EntityManager.persist(entity)</code>로 객체를 영속화한다.</li>
<li>DB에 저장한다는 것이 아니라 영속성 컨텍스트를 통해서 엔티티를 영속성화 한다는 뜻<ul>
<li>엔티티를 영속성 컨텍스트라는 곳에 저장한다는 말</li>
</ul>
</li>
<li>EntityManager 안에 영속성 컨텍스트라는 눈에 보이지 않는 어떤 공간이 생긴다고 이해하면 됨</li>
</ul>
<h2 id="entitymanagerfactory--entitymanager">EntityManagerFactory / EntityManager</h2>
<h3 id="entitymanagerfactory-특징">EntityManagerFactory 특징</h3>
<blockquote>
<ol>
<li>생성 비용이 크다</li>
<li>생성 비용이 크기 때문에 데이터베이스당 하나만 만들어서 쓰레드간 공유한다.</li>
<li>Thread-safe 하다</li>
</ol>
</blockquote>
<h3 id="entitymanager-특징">EntityManager 특징</h3>
<blockquote>
<ol>
<li>persist(), find(), remove() 등을 사용해 Entity를 관리한다.</li>
<li>Thread-safe 하지 않다.</li>
<li>생성 시 데이터베이스 연결이 꼭 필요한 시점까지 커넥션을 얻지 않는다.</li>
</ol>
</blockquote>
<h2 id="엔티티의-생명-주기">엔티티의 생명 주기</h2>
<ul>
<li>비영속 (new/transient): 영속성 컨텍스트와 전혀 관계가 없는 새로운 상태</li>
<li>영속 (managed): 영속성 컨텍스트에 관리되는 상태</li>
<li>준영속 (detached): 영속성 컨텍스트에 저장되었다가 분리된 상태</li>
<li>삭제 (removed): 삭제된 상태</li>
</ul>
<h3 id="비영속">비영속</h3>
<p>JPA와 전혀 관계없는 상태일 때 비영속 상태라고 한다. 아래 코드는 객체를 생성만 했을 뿐 JPA와는 전혀 관련없다(<code>=객체를 생성한 상태</code>).</p>
<pre><code class="language-java">// 객체를 생성한 상태 (비영속)
Member member = new Member();
member.setId(100L);
member.setName(&quot;member1&quot;);</code></pre>
<h3 id="영속">영속</h3>
<p>객체를 생성한 후 <code>EntityManager.persist()</code>에 객체를 저장한 상태를 말한다.</p>
<pre><code class="language-java">// 객체를 생성한 상태 (비영속)
Member member = new Member();
member.setId(100L);
member.setName(&quot;member1&quot;);

EntityManager em = emf.createEntityManager();
em.getTransaction().begin();

// 객체를 저장한 상태 (영속)
em.persist(member);</code></pre>
<p>이때 <code>em.persist()</code>의 경우에만 영속이 되는 것이 아니라, <code>em.find()</code>를 했을 때 <strong>1차 캐시에 없는 데이터를 찾아서 1차 캐시에 해당 정보가 저장된 경우도 영속 상태</strong>가 된다.</p>
<h3 id="준영속--삭제">준영속 / 삭제</h3>
<p><strong>준영속 상태</strong></p>
<ul>
<li>영속 상태의 엔티티가 영속성 컨텍스트에서 분리(detached)된 것이다. 한마디로 그냥 다 빼버리는 것이다.</li>
<li>준영속 상태의 경우 영속성 컨텍스트가 제공하는 기능을 사용 못한다.</li>
</ul>
<pre><code class="language-java">Member findMember = em.find(Member.class, 101L);  // ID = 101L 객체가 영속 상태로 변경
findMember.setName(&quot;ASDF&quot;);  // 더티 체킹으로 update 쿼리가 날라갈 예정
em.detach(findMember);  // 이었으나 준영속 상태로 변경 (JPA에서 관리 안함)

tx.commit();  // select 문만 전송되고 update 문은 전송되지 않음</code></pre>
<p><strong>준영속 상태로 만드는 방법</strong></p>
<ul>
<li><code>em.detach()</code> - 특정 엔티티만 준영속 상태로 전환</li>
<li><code>em.clear()</code> - 영속성 컨텍스트를 완전히 초기화</li>
<li><code>em.close()</code> - 영속성 컨텍스트 종료</li>
</ul>
<p><strong>삭제의 경우</strong></p>
<ul>
<li>커밋이 되지 않은 영속 객체를 삭제할 경우 준영속과 같은 상태가 된다. </li>
<li>이미 데이터베이스에 저장된 객체를 찾아서 삭제할 경우 해당 데이터가 삭제된다.</li>
</ul>
<pre><code class="language-java">// 객체를 생성한 상태 (비영속)
Member member = new Member();
member.setId(100L);
member.setName(&quot;member1&quot;);

EntityManager em = emf.createEntityManager();
em.getTransaction().begin();

// 객체를 저장한 상태 (영속)
em.persist(member);

// 객체를 영속성 컨텍스트에서 분리한 상태 (준영속)
em.detach(member);

// 객체를 삭제한 상태 (삭제)
Member findMember = em.find(Member.class, 1L);
em.remove(findMember);</code></pre>
<h2 id="영속성-컨텍스트의-이점">영속성 컨텍스트의 이점</h2>
<h3 id="1차-캐시">1차 캐시</h3>
<ul>
<li>1차 캐시는 영속 상태의 엔티티가 실제로 저장되는 곳이다</li>
<li>메모리에 <code>Map&lt;ID, Entity&gt;</code>의 형태로 존재한다고 생각</li>
<li>객체 생성 후 영속화하면 1차 캐시에 저장된다.</li>
<li>이때 객체를 조회할 경우 데이터베이스를 조회하기 전 1차 캐시 먼저 조회를 한다.</li>
<li>1차 캐시에 없을 경우 데이터베이스에 조회 후 1차 캐시에 저장한 뒤 해당 객체를 반환한다.</li>
</ul>
<blockquote>
<p>결론: 1차 캐시를 통해 성능상 이점을 가진다.</p>
</blockquote>
<h3 id="동일성identity-보장">동일성(Identity) 보장</h3>
<ul>
<li>동일성(Identity): 실제 인스턴스의 참조 값이 같다. (==)</li>
<li>동등성(Equality): 실제 값이 같다. (equals)</li>
</ul>
<blockquote>
<p>결론: (같은 트랜잭션 내에서) 같은 식별자에 대한 객체 조회는 동일성을 보장해준다.</p>
</blockquote>
<h3 id="트랜잭션을-지원하는-쓰기-지연-transactional-write-behind">트랜잭션을 지원하는 쓰기 지연 (transactional write-behind)</h3>
<ul>
<li>여러 데이터를 저장한다는 가정 하에 <code>EntityManager</code>는 쓰기 지연 SQL 저장소에 <code>insert sql</code>을 쌓아둔다.<ul>
<li>이때 저장소의 크기는 <code>&lt;property name=&quot;hibernate.jdbc.batch_size&quot; value=&quot;10&quot;/&gt;</code>로 지정할 수 있다.</li>
<li>지정한 <code>value</code> 마다 <code>insert query</code>가 실행된다.</li>
<li><code>batch-size</code>에 대한 제한이 없으면 <code>OutOfMemoryException</code>이 발생할 수도 있고, 메모 관리 측면에서도 효율적이지 않다.</li>
<li>자세한 사항은 <a href="https://cheese10yun.github.io/jpa-batch-insert/#jpa-with-batch-insert-code-1">여기</a>를 참조</li>
</ul>
</li>
<li>이후 커밋 시점에 비로소 모든 쿼리를 데이터베이스에 보내게 된다. (flush)</li>
<li>(문제가 없다면) 커밋이 정상적으로 수행된다.</li>
</ul>
<blockquote>
<p>결론: 영속 시점에 쿼리를 바로 데이터베이스에 보내는 것이 아니라 커밋 직전까지 <code>쓰기 지연 SQL 저장소</code>에 모아둔 뒤 한번에 보내기 때문에 성능상 이점이 있다.  </p>
</blockquote>
<h3 id="변경-감지-dirty-checking">변경 감지 (Dirty Checking)</h3>
<ul>
<li>객체 수정 시 별도의 메서드 없이 객체를 수정할 수 있는 이유이다.<ol>
<li>JPA는 객체를 영속성 컨텍스트에 보관 시 스냅샷을 만들게 된다. (백업 데이터 같은 느낌)</li>
<li><code>flush()</code> 시점에 스냅샷과 객체를 비교해서 변경된 객체를 찾는다. 이때 영속성 컨텍스트 안에 있는 객체만 변경 감지 기능의 대상이 된다.</li>
<li>변경된 객체가 존재할 경우, 수정 쿼리를 쓰기 지연 SQL 저장소에 저장한다.</li>
<li>커밋 시점에 저장소 안에 있는 <code>sql</code>을 데이터베이스에 보낸다. (<code>flush()</code> 끝)</li>
<li>(문제가 없을 경우) 트랜잭션이 정상적으로 종료(커밋)된다.</li>
</ol>
</li>
</ul>
<h3 id="지연-로딩-lazy-loading">지연 로딩 (Lazy Loading)</h3>
<p>이후 추가 예정</p>
<h1 id="references">References</h1>
<ul>
<li><a href="https://www.inflearn.com/course/ORM-JPA-Basic">https://www.inflearn.com/course/ORM-JPA-Basic</a></li>
<li><a href="https://cheese10yun.github.io/jpa-batch-insert/#jpa-with-batch-insert-code-1">https://cheese10yun.github.io/jpa-batch-insert/#jpa-with-batch-insert-code-1</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[SQL 중심적인 개발의 문제점]]></title>
            <link>https://velog.io/@dereck-jun/SQL-%EC%A4%91%EC%8B%AC%EC%A0%81%EC%9D%B8-%EA%B0%9C%EB%B0%9C%EC%9D%98-%EB%AC%B8%EC%A0%9C%EC%A0%90</link>
            <guid>https://velog.io/@dereck-jun/SQL-%EC%A4%91%EC%8B%AC%EC%A0%81%EC%9D%B8-%EA%B0%9C%EB%B0%9C%EC%9D%98-%EB%AC%B8%EC%A0%9C%EC%A0%90</guid>
            <pubDate>Mon, 10 Feb 2025 13:05:06 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>김영한님의 JPA 강의를 보고 정리한 내용입니다.</p>
</blockquote>
<h1 id="sql-중심적인-개발의-문제점">SQL 중심적인 개발의 문제점</h1>
<p>SQL 중심적인 개발에는 여러 문제점이 있다.</p>
<h2 id="1-무한-반복-지루한-코드">1. 무한 반복, 지루한 코드</h2>
<ul>
<li>CRUD 쿼리 무한 반복. 객체에 필드가 추가되면 쿼리에 변경점을 하드 코딩해줘야 한다.</li>
</ul>
<h2 id="2-패러다임의-불일치">2. 패러다임의 불일치</h2>
<ul>
<li>객체 지향의 목표와 RDB의 패러다임에 차이 때문에 문제가 생긴다.<ul>
<li>객체 지향: 추상화, 캡슐화, 상속, 다형성 등 시스템의 복잡성을 제어할 수 있는 다양한 장치들을 제공</li>
<li>RDB: 데이터를 중심으로 구조화되어 있음. (다양한 도구가 존재하지 않음)</li>
</ul>
</li>
<li>RDB가 인식할 수 있는 것은 SQL. 결국 객체를 SQL로 짜야 한다.<ul>
<li>객체 -&gt; SQL 매핑 작업 -&gt; RDB 저장</li>
<li>위의 매핑 작업을 할 수 있는 것은 개발자 뿐. <code>개발자 == SQL Mapper</code>라고 할 정도</li>
</ul>
</li>
</ul>
<h2 id="3-객체와-rdb의-차이">3. 객체와 RDB의 차이</h2>
<h3 id="상속">상속</h3>
<p>객체에는 상속 관계가 있지만 RDB에는 상속 관계가 없다. 대신 상속 관계와 유사한 모델인 <code>슈퍼타입/서브타입</code>이 존재한다.</p>
<p><img src="https://github.com/dereck-jun/TIL/blob/main/media/JPA/rdb_sup_sub_type.png?raw=true" alt="rdb_sup_sub_type.png"></p>
<p><code>album</code>을 DB에 저장한다고 하면 <code>item 테이블 삽입 sql</code>, <code>album 테이블 삽입 sql</code> 총 2개를 작성해야 한다. album을 조회한다고 해도 <code>join</code>으로 <code>album</code>과 <code>item</code> 테이블을 조회해서 나온 결과 값을 일일이 각 객체의 필드 값에 넣어줘야 한다.</p>
<p>이는 개발자가 실수할 가능성을 높이게 되고, 생산성의 저하로 이어지게 된다.</p>
<p>하지만 자바 컬렉션을 사용하듯이 사용하게 된다면 훨씬 편하게 사용할 수 있을 것이다.</p>
<pre><code class="language-java">list.add(album);  // 저장

Album album = list.get(album.getId());  // 조회

Item item = list.get(album.getId());  // 다형성 활용</code></pre>
<h3 id="연관-관계">연관 관계</h3>
<p><code>Member</code> 객체는 <code>Member.team</code> 필드에 <code>Team</code> 객체의 참조를 보관해서 <code>Team</code> 객체와 관계를 맺는다. 이 참조 필드에 접근해 <code>Member</code>와 연관된 <code>Team</code>을 조회할 수 있다. </p>
<p>반면 <code>MEMBER</code> 테이블은 <code>MEMBER_ID</code> 외래 키 컬럼을 사용해서 <code>TEAM</code>과 관계를 맺는다.  이 외래키를 사용해 <code>TEAM</code> 테이블과 조인하면 <code>MEMBER</code> 테이블과 연관된 <code>TEAM</code> 테이블을 조회할 수 있다. </p>
<p>여기서 발생하는 문제는 객체의 참조 방향이다. 객체 연관 관계의 경우 <code>member.getTeam</code>으로 참조 가능하지만 반대로 <code>Team.getMember()</code>는 불가능하다. 반면 테이블은 어느 쪽에서든 조인을 사용할 수 있다.</p>
<h2 id="4-모델링에서의-문제">4. 모델링에서의 문제</h2>
<p>객체는 참조를 통해 관계를 맺는다. </p>
<pre><code class="language-java">class Member {
    Long memberId;
    Team team;
    String name;

    ...
}

class Team {
    Long teamId;
    String name;

    ...
}</code></pre>
<p>그런데 위 코드처럼 객체 모델을 사용하면 테이블에 저장하거나 조회하기 쉽지 않다. 객체 모델을 <code>Team</code> 객체로 DB는 <code>Team</code>을 <code>team_id</code>로 저장하기 때문이다. 이런 차이가 있어 개발자가 중간에서 변환 역할을 해야 한다.</p>
<pre><code class="language-java">Member member = new Member();
member.setName(&quot;memberA&quot;);
...  // 추가적인 회원 관련 정보 추가

Team team = new Team();
team.setName(&quot;teamA&quot;);
...  // 추가적인 팀 관련 정보 추가

member.setTeam(team);  // 회원과 팀 관계 설정
</code></pre>
<p>이런 과정은 모두 패러다임 불일치를 해결하기 위해 소모되는 비용이다.</p>
<h2 id="5-객체-그래프-탐색">5. 객체 그래프 탐색</h2>
<p>객체에서 회원이 소속된 팀을 조회할 때 다음처럼 참조해서 사용하면 연괸된 팀을 찾을 수 있다. 이것을 <strong>객체 그래프 탐색</strong>이라고 한다.</p>
<p><img src="https://github.com/dereck-jun/TIL/raw/main/media/JPA/entity_graph.png" alt="entity_graph.png"></p>
<pre><code class="language-java">Team team = member.getTeam();

member.getOrder().getOrderItem();</code></pre>
<p>객체는 마음대로 객체 그래프를 탐색할 수 있어야 한다. 그런데 DB에서는 객체를 조회할 때 <code>Tember</code>와 <code>Team</code>의 데이터만 조회했다면 <code>member.getOrder()</code>의 값은 <code>null</code>이 된다. </p>
<p>SQL을 직접 다루면 처음 실행하는 SQL문에 따라 객체 그래프의 탐색이 한정된다. 이는 개발자에겐 큰 제약이며, 비즈니스 로직에 따라 사용하는 객체 그래프가 다른데 언제 끊어질 지 모를 객체 그래프를 함부로 탐색할 순 없다.</p>
<h2 id="6-객체-비교에서-차이">6. 객체 비교에서 차이</h2>
<p>DB는 기본 키의 값으로 각 행을 구분한다. 반면, 객체는 동일성 비교와 동등성 비교의 두 가지 방법이 있다. </p>
<pre><code class="language-java">public Member getMember(Long memberId) {
    String sql = &quot;select * from Member where member_id = ?&quot;;
    ...
    // JDBC API, SQL 실행
    return new Member(...);
}</code></pre>
<pre><code class="language-java">Long memberId = 100L;
Member member1 = member.getMember(memberId);
Member member2 = member.getMember(memberId);

member1 == member2  // false</code></pre>
<p>같은 <code>sql</code>, 같은 <code>member_id</code>로 조회했는데도 두 객체가 <strong>동일</strong>하지 않다. 그 이유는 내용은 같지만 <code>new</code>를 사용해서 새로운 인스턴스로 만들었기 때문이다. 만약 객체를 컬렉션에 보관했다면 동일성 비교에 성공했을 것이다. </p>
<p>이런 패러다임 불일치를 해결하기 위해 같은 <code>row</code>를 조회할 때마다 같은 인스턴스를 반환하도록 구현하는 것은 쉽지 않다. 여기에 트랜잭션이 동시에 실행되는 상황까지 고려하면 문제는 더 어려워질 것이다.</p>
<h1 id="references">References</h1>
<ul>
<li><a href="https://www.inflearn.com/course/ORM-JPA-Basic">https://www.inflearn.com/course/ORM-JPA-Basic</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Validation (2)]]></title>
            <link>https://velog.io/@dereck-jun/Validation-2</link>
            <guid>https://velog.io/@dereck-jun/Validation-2</guid>
            <pubDate>Wed, 05 Feb 2025 07:42:21 GMT</pubDate>
            <description><![CDATA[<h1 id="bean-validation">Bean Validation</h1>
<h2 id="bean-validation-개요">Bean Validation 개요</h2>
<p><code>Bean Validation</code>은 특정한 구현체가 아니라 <code>Bean Validation 2.0(JSR-380)</code>이라는 기술 표준이다. 쉽게 말하면 검증 어노테이션과 여러 인터페이스의 모음이며 마치 JPA 기술 표준이고 그 구현체로 하이버네이트가 있는 것과 같다.</p>
<p><code>Bean Validaion</code>을 구현한 기술 중에 일반적으로 사용하는 구현체는 <code>하이버네이트 Validator</code>이다. 이름에 하이버네이트가 붙었지만 ORM과는 관련없다.</p>
<blockquote>
<p>하이버네이트 Validator 관련 링크
공식 사이트: <a href="http://hibernate.org/validator/">http://hibernate.org/validator/</a>
공식 메뉴얼: <a href="https://docs.jboss.org/hibernate/validator/6.2/reference/en-US/html_single/">https://docs.jboss.org/hibernate/validator/6.2/reference/en-US/html_single/</a>
검증 어노테이션 모음: <a href="https://docs.jboss.org/hibernate/validator/6.2/reference/en-US/html_single/#validator-defineconstraints-spec">https://docs.jboss.org/hibernate/validator/6.2/reference/en-US/html_single/#validator-defineconstraints-spec</a></p>
</blockquote>
<h2 id="의존-관계-추가">의존 관계 추가</h2>
<pre><code class="language-groovy">implementation &#39;org.springframework.boot:spring-boot-starter-validation`</code></pre>
<blockquote>
<p><strong>참고</strong>
<code>jakarta.validation.constraints.NotNull</code>
<code>org.hibernate.validator.constraints.Range</code></p>
<p><code>jakarta.validation</code>으로 시작하면 특정 구현에 관계없이 제공되는 표준 인터페이스이고, <code>org.hibernate.validator</code>로 시작하면 하이버네이트 validator 구현체를 사용할 때만 제공되는 검증 기능이다. 실무에서 대부분 하이버네이트 validator를 사용하므로 자유롭게 사용 가능하다.</p>
</blockquote>
<h2 id="동작-원리">동작 원리</h2>
<h3 id="valid">@Valid</h3>
<p><code>@Valid</code>의 경우 <code>ArgumentResolver</code>에 의해 처리된다. </p>
<p>대표적으로 <code>@RequestBody</code>에서 JSON 메시지를 객체로 변환해주는 작업을 <code>ArgumentResolver</code>의 구현체인 <code>RequestResponseBodyMethodProcessor</code>가 처리하며 이 내부에 <code>@Valid</code>로 시작하는 어노테이션이 있을 경우에 유효성 검사를 진행한다. </p>
<p><code>@ModelAttribute</code>의 경우엔 <code>ModelAttributeMethodProcessor</code>에 의해 <code>@Valid</code>가 처리된다.</p>
<p>그리고 검증에 오류가 있다면 <code>MethodArgumentNotValidException</code> 예외가 발생하게 된다.</p>
<p><code>@Valid</code>는 기본적으로 Controller에서만 동작하며 기본적으로 다른 계층에서는 검증이 되지 않는다.</p>
<h3 id="validated">@Validated</h3>
<p>입력 파라미터의 유효성 검증은 컨트롤러에서 최대한 처리하고 넘겨주는 것이 좋다. 하지만 개발을 하다보면 불가피하게 다른 곳에서 파라미터를 검증해야 할 수 있다.</p>
<p>이를 위해 Spring에서는 AOP 기반으로 메서드의 요청을 가로채서 유효성 검증을 진행해주는 <code>@Validated</code>를 제공하고 있다. <code>@Validated</code>는 JSR 표준 기술이 아니며 Spring Framework에서 제공하는 어노테이션 및 기능이다.</p>
<p>클래스에 <code>@Validated</code>를 붙여주고, 유효성을 검증할 메서드의 파라미터에 <code>@Valid</code>를 붙여주면 된다.</p>
<pre><code class="language-java">@Service
@Validated
public class ItemService {
    public void saveItem(@Valid SaveItemRequest request) {
        ...
    }
}</code></pre>
<p>유효성 검증에 실패하면 <code>ConstraintViolationException</code>이 발생한다. <code>@Validated</code>는 AOP 기반으로 메서드 요청을 인터셉터하여 처리된다. <code>@Validated</code>를 클래스 레벨에 선언하면 해당 클래스에 유효성 검증을 위한 AOP의 어드바이스 또는 인터셉터(<code>MethodValidationInterceptor</code>)가 등록된다. </p>
<p>따라서 <code>@Validated</code>를 사용하면 계층에 무관하게 Spring Bean이라면 유효성 검증을 진행할 수 있다. </p>
<h3 id="spring-boot-32-이후">Spring Boot 3.2 이후</h3>
<p>스프링 부트 3.2(Spring Framework 6.1)부터 Spring MVC와 WebFlux에서 유효성 검사를 위한 <code>@Constraint</code> 관련 어노테이션을 기본적으로 지원하도록 개선되었다. 컨트롤러를 위한 파라미터를 생성하는 <code>ArgumentResolver</code>들이 모두 동작하고, 컨트롤러의 메서드 호출이 준비되었을 때 유효성 검사가 진행된다. 스프링은 이를 <code>MethodValidator</code>라고 부른다.</p>
<p>스프링의 <code>MethodValidator</code> 관련 기능을 활용하기 위해서는 다음의 조건들이 충족되면 된다.</p>
<ol>
<li>컨트롤러에 <code>@Validated</code>를 통한 AOP 기반 검증이 존재하지 않음</li>
<li><code>LocalValidatorFactoryBean</code>과 같은 <code>jakarta.validation.Validator</code> 타입의 빈이 등록됨</li>
<li>메서드 파라미터 유효성 검증 어노테이션이 붙어있음</li>
</ol>
<pre><code class="language-java">@RestController
public class HelloController {
    @GetMapping(&quot;/hello&quot;)
    public String hello(@RequestParam @Length(min = 1) String name) {
        ...
    }
}</code></pre>
<p>이를 통해 기존에는 불가능했던 방식으로도 동작이 가능해진다. 왜냐하면 파라미터가 모두 준비된 이후에 파라미터에 붙어있는 유효성 검사 어노테이션을 파싱하여 유효성 검사를 진행하기 때문이다.</p>
<h2 id="스프링-통합-전">스프링 통합 전</h2>
<h3 id="검증기-생성">검증기 생성</h3>
<pre><code class="language-java">ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();</code></pre>
<p>위 코드와 같이 검증기를 생성한다. 이후 스프링과 통합하면 직접 코드를 작성하지 않기 때문에 &#39;이렇게 사용하는구나&#39; 정도만 참고하자.</p>
<h3 id="검증-실행">검증 실행</h3>
<pre><code class="language-java">Set&lt;ConstraintViolation&lt;Item&gt;&gt; violations = validator.validate(item);</code></pre>
<p>검증 대상(<code>item</code>)을 직접 검증기에 넣고 그 결과를 받는다. <code>Set</code>에는 <code>ConstraintViolation</code>이라는 검증 오류가 담긴다. 따라서 결과가 비어있으면 검증 오류가 없는 것이다.</p>
<h2 id="스프링-통합-후">스프링 통합 후</h2>
<h3 id="스프링-mvc는-어떻게-bean-validator를-사용할까">스프링 MVC는 어떻게 Bean Validator를 사용할까?</h3>
<p>스프링 부트가 <code>spring-boot-starter-validation</code> 라이브러리를 넣으면 자동으로 <code>BeanValidation</code>를 인지하고 스프링에 통합한다.</p>
<p><code>LocalValidatorFactoryBean</code>을 글로벌 Validator로 등록한다. 이 Validator는 <code>@NotNull</code> 같은 어노테이션을 보고 검증을 수행한다. 이렇게 글로벌 Validator가 적용되어 있기 때문에 <code>@Valid</code>, <code>@Validated</code>만 적용하면 된다. 만약 검증 오류가 발생하면 <code>FieldError</code>, <code>ObjectError</code>를 생성해서 <code>BindingResult</code>에 담아준다.</p>
<h3 id="검증-순서">검증 순서</h3>
<ol>
<li><code>@ModelAttribute</code> 각각의 필드에 타입 변환 시도<ol>
<li>성공하면 다음으로</li>
<li>실패하면 <code>typeMismatch</code>로 <code>FieldError</code> 추가</li>
</ol>
</li>
<li>Validator 적용</li>
</ol>
<p><code>BeanValidator</code>는 바인딩에 실패한 필드는 <code>BeanValidation</code>을 적용하지 않는다. 타입 변환에 성공해서 바인딩에 성공한 필드여야 <code>BeanValidation</code> 적용이 의미가 있기 때문이다.</p>
<blockquote>
<p><strong>주의</strong>
글로벌 Validator를 직접 등록하면 스프링 부트는 <code>BeanValidator</code>를 글로벌 Validator로 등록하지 않는다.</p>
</blockquote>
<h2 id="에러-코드">에러 코드</h2>
<h3 id="메시지-등록">메시지 등록</h3>
<p><code>BeanValidation</code>이 기본으로 제공하는 오류 메시지를 좀 더 자세히 변경하고 싶으면 메시지를 등록하면 된다.</p>
<pre><code class="language-properties"># errors.properties
NotBlank={0} 공백 X
Range={0}, {2} ~ {1} 허용
Max={0}, 최대 {1}</code></pre>
<p><code>{0}</code>은 필드명이고, <code>{1}</code>, <code>{2}</code>는 각 어노테이션 마다 다르다.</p>
<h3 id="beanvalidation-메시지-찾는-순서">BeanValidation 메시지 찾는 순서</h3>
<ol>
<li>생성된 메시지 코드 순서대로 <code>messageSource</code>에서 메시지 찾기</li>
<li>어노테이션의 <code>message</code> 속성 사용<ul>
<li><code>@NotBlank(message = &quot;{0}은 공백일 수 없습니다.&quot;)</code></li>
</ul>
</li>
<li>라이브러리가 제공하는 기본 값 사용<ul>
<li><code>공백일 수 없습니다.</code></li>
</ul>
</li>
</ol>
<h2 id="오브젝트-오류">오브젝트 오류</h2>
<p><code>BeanValidation</code>에서 특정 필드가 아닌 해당 오브젝트 관련 오류는 <code>@ScriptAssert()</code>를 사용할 수도 있다.</p>
<pre><code class="language-java">@Data
@ScriptAssert(
    lang = &quot;javascript&quot;, script = &quot;this.price * this.quantity &gt;= 10000&quot;
)
public class Item {
    ...
}</code></pre>
<p>하지만 실제로 사용해보면 제약이 많고 복잡하며 실무에서는 검증 기능이 해당 객체의 범위를 넘어서는 경우들도 종종 등장할 때 대응이 어렵기 때문에 오브젝트 오류(글로벌 오류)의 경우 <code>@ScriptAssert</code>를 억지로 사용하는 것보다 오브젝트 오류 관련 부분만 자바 코드로 작성하는 것을 권장한다.</p>
<h2 id="http-메시지-컨버터">HTTP 메시지 컨버터</h2>
<p><code>@Valid</code>, <code>@Validated</code>는 <code>HttpMessageConverter</code>(<code>@RequestBody</code>)에도 적용할 수 있다. </p>
<p>API의 경우 3가지 경우를 나누어 생각해야 한다.</p>
<ul>
<li>성공 요청</li>
<li>실패 요청: JSON을 객체로 생성하는 것 자체가 실패함</li>
<li>검증 오류 요청: JSON을 객체로 생성하는 것은 성공했지만, 검증에서 실패함</li>
</ul>
<blockquote>
<p><strong>참고</strong>
<code>@ModelAttribute</code>는 HTTP 요청 파라미터(URL, 쿼리 스트링, POST Form)를 다룰 때 사용한다.
<code>@RequestBody</code>는 HTTP Body의 데이터를 객체로 변환할 때 사용한다. 주로 API JSON 요청을 다룰 때 사용한다.</p>
</blockquote>
<h1 id="references">References</h1>
<ul>
<li>스프링 MVC 2편 - 백엔드 웹 개발 활용 기술</li>
<li><a href="https://mangkyu.tistory.com/174">https://mangkyu.tistory.com/174</a></li>
<li><a href="https://mangkyu.tistory.com/379">https://mangkyu.tistory.com/379</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Validation (1)]]></title>
            <link>https://velog.io/@dereck-jun/Validation-1</link>
            <guid>https://velog.io/@dereck-jun/Validation-1</guid>
            <pubDate>Tue, 04 Feb 2025 15:23:03 GMT</pubDate>
            <description><![CDATA[<h1 id="bindingresult">BindingResult</h1>
<p>Spring에서 기본적으로 제공되는 검증 오류를 보관하는 객체로 주로 사용자 입력 폼을 검증할 때 많이 쓰이고 <code>FieldError</code>와 <code>ObjectError</code>를 보관한다.</p>
<blockquote>
<p><a href="https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/validation/BindingResult.html">BindingResult docs</a>
바인딩 결과를 나타내는 일반 인터페이스. </p>
<p>오류 등록 기능을 위해 오류 인터페이스를 확장하여 유효성 검사기를 적용할 수 있도록 하고 바인딩별 분석 및 모델 구축을 추가합니다.</p>
<p><code>DataBinder.getBindingResult()</code> 메서드를 통해 얻은 <code>DataBinder</code>의 결과 홀더 역할을 합니다. 예를 들어 단위 테스트의 일부로 유효성 검사기를 호출하는 등 <code>BindingResult</code> 구현을 직접 사용할 수도 있습니다.</p>
</blockquote>
<p><code>BindingResult</code>는 인터페이스이고, <code>Errors</code> 인터페이스를 상속받고 있다. 실제 넘어오는 구현체는 <code>BeanPropertyBindingResult</code> 라는 것인데, <code>Errors</code>와 <code>BindingResult</code>를 모두 구현하고 있기 때문에 <code>Errors</code>를 사용해도 되지만 <code>BindingResult</code>는 추가 기능들을 제공하기 때문에 <code>BindingResult</code>를 많이 사용한다.</p>
<p><code>BindingResult</code>가 있으면 <code>@ModelAttribute</code>에 데이터 바인딩 시 오류가 발생해도 컨트롤러가 호출된다.</p>
<p>컨트롤러에서 <code>BindingResult</code>는 검증해야 할 객체인 <code>target</code> 바로 다음에 온다. 따라서 <code>BindingResult</code>는 이미 본인이 검증해야 할 객체인 <code>target</code>을 알고 있다.</p>
<blockquote>
<p><strong><code>@ModelAttribute</code>에 바인딩 시 타입 오류가 발생하면?</strong></p>
<ol>
<li><code>BindingResult</code>가 있을 경우: 오류 정보(<code>FieldError</code>)를 <code>BindingResult</code>에 담아서 컨트롤러를 정상 호출한다.</li>
<li><code>400 오류</code>가 발생하면서 컨트롤러가 호출되지 않고, 오류 페이지로 이동한다.</li>
</ol>
</blockquote>
<h2 id="bindingresult에-검증-오류를-적용하는-3가지-방법">BindingResult에 검증 오류를 적용하는 3가지 방법</h2>
<ol>
<li><code>@ModelAttribute</code>의 객체에 타입 오류 등으로 바인딩이 실패하는 경우 스프링이 <code>FieldError</code>를 생성해서 <code>BindingResult</code>에 넣어준다.</li>
<li>개발자가 직접 넣어준다.</li>
<li><code>Validator</code> 사용</li>
</ol>
<h2 id="fielderror--objecterror">FieldError &amp; ObjectError</h2>
<h3 id="fielderror---필드-오류">FieldError - 필드 오류</h3>
<pre><code class="language-java">// 사용 예시
if(!StringUtils.hasText(item.getItemName())) {
    bindingResult.addError(new FieldError(&quot;item&quot;, &quot;itemName&quot;, &quot;상품 이름은 필수입니다.&quot;));
}

// FieldError 생성자 (1)
public FieldError(String objectName, String field, String defaultMessage) { ... }

// FieldError 생성자 (2)
public FieldError(
    String objectName, String field, @Nullable Object rejectedValue, 
    boolean bindingFailure, @Nullable String[] codes, 
    @Nullable Object[] arguments, @Nullable String defaultMessage) { ... }</code></pre>
<p>필드에 오류가 있으면 <code>FieldError</code> 객체를 생성해서 <code>bindingResult</code>에 담아두면 된다.</p>
<ul>
<li><code>objectName</code>: <code>@ModelAttribute</code> 이름</li>
<li><code>field</code>: 오류가 발생한 필드 이름</li>
<li><code>defaultMessage</code>: 오류 기본 메시지</li>
</ul>
<p>파라미터 목록은 다음과 같다.</p>
<ul>
<li><code>objectName</code>: 오류가 발생한 객체의 이름</li>
<li><code>field</code>: 오류 필드</li>
<li><code>rejectedValue</code>: 사용자가 입력한 값(거절된 값)</li>
<li><code>bindingFailure</code> 타입 오류 같은 바인딩 실패인지, 검증 실패인지 구분 값</li>
<li><code>codes</code>: 메시지 코드</li>
<li><code>arguments</code>: 메시지에서 사용하는 인자</li>
<li><code>defaultMessage</code>: 기본 오류 메시지</li>
</ul>
<h3 id="objecterror---글로벌-오류">ObjectError - 글로벌 오류</h3>
<pre><code class="language-java">// 사용 예시
bindingResult.addError(new ObjectError(&quot;item&quot;, &quot;가격 * 수량의 합은 10,000원 이상이어야 합니다. 현재 값 = &quot; + resultPrice));

// ObjectError 생성자 (1)
public ObjectError(String objectName, String defaultMessage) { ... }

// ObjectError 생성자 (2)
public ObjectError(
    String objectName, @Nullable String[] codes, 
    @Nullable Object[] arguments, @Nullable String defaultMessage) { ... }</code></pre>
<p>특정 필드를 넘어서는 오류가 있으면 <code>ObjectError</code> 객체를 생성해서 <code>bindingResult</code>에 담아두면 된다.</p>
<ul>
<li><code>objectName</code>: <code>@ModelAttribute</code> 이름</li>
<li><code>codes</code>: 메시지 코드</li>
<li><code>arguments</code>: 메시지에서 사용하는 인자</li>
<li><code>defaultMessage</code>: 기본 오류 메시지</li>
</ul>
<h2 id="rejectvalue-reject">rejectValue(), reject()</h2>
<p><code>BindingResult</code>가 제공하는 <code>rejectValue()</code>, <code>reject()</code>를 사용하면 <code>FieldError</code>, <code>ObjectError</code>를 직접 생성하지 않고, 깔끔하게 검증 오류를 다룰 수 있다.</p>
<h3 id="rejectvalue">rejectValue()</h3>
<pre><code class="language-java">void rejectValue(
    @Nullable String field, String errorCode, 
    @Nullable Object[] errorArgs, @Nullable String defaultMessage);</code></pre>
<ul>
<li><code>field</code>: 오류 필드명</li>
<li><code>errorCode</code>: 오류 코드<ul>
<li>이 오류 코드는 메시지에 등록된 코드가 아니라 <code>messageResolver</code>를 위한 코드임</li>
</ul>
</li>
<li><code>errorArgs</code>: 오류 메시지에서 <code>{0}</code>을 치환하기 위한 값</li>
<li><code>defaultMessage</code>: 오류 메시지를 찾을 수 없을 때 사용하는 기본 메시지</li>
</ul>
<pre><code class="language-java">bingResult.rejectValue(&quot;price&quot;, &quot;range&quot;, new Object[]{1000, 1000000}, null)</code></pre>
<p>앞에서 <code>BindingResult</code>는 어떤 객체를 대상으로 검증하는지 <code>target</code>을 이미 알고 있다고 했다. (<code>target</code> 뒤에 <code>BindingResult</code>가 위치하기 때문) 따라서 <code>target</code>에 대한 정보는 없어도 된다.</p>
<h3 id="reject">reject()</h3>
<pre><code class="language-java">void reject(String errorCode, @Nullable Object[] errorArgs, @Nullable String defaultMessage);</code></pre>
<ul>
<li><code>field</code>: 오류 필드명</li>
<li><code>errorCode</code>: 오류 코드<ul>
<li>이 오류 코드는 메시지에 등록된 코드가 아니라 <code>messageResolver</code>를 위한 코드임</li>
</ul>
</li>
<li><code>errorArgs</code>: 오류 메시지에서 <code>{0}</code>을 치환하기 위한 값</li>
<li><code>defaultMessage</code>: 오류 메시지를 찾을 수 없을 때 사용하는 기본 메시지</li>
</ul>
<pre><code class="language-java">bingResult.rejectValue(&quot;price&quot;, &quot;range&quot;, new Object[]{1000, 1000000}, null)</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[일정 관리 애플리케이션]]></title>
            <link>https://velog.io/@dereck-jun/%EC%9D%BC%EC%A0%95-%EA%B4%80%EB%A6%AC-%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85</link>
            <guid>https://velog.io/@dereck-jun/%EC%9D%BC%EC%A0%95-%EA%B4%80%EB%A6%AC-%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85</guid>
            <pubDate>Mon, 03 Feb 2025 08:08:23 GMT</pubDate>
            <description><![CDATA[<h1 id="들어가기-전에">들어가기 전에</h1>
<p>오랜만에 스프링을 해서 그런가 과제 시작하고 하루에서 이틀 동안은 손에 잘 안잡혔던 것 같다. 특히 생각이 잘 안나는 <code>JDBC</code> 만을 사용해서 구현해야 하다보니 더 안잡혔던 것 같다.</p>
<p>그래도 과제를 하면서 사용해본 적 없던 <code>MapStruct</code>나 <code>Swagger</code>를 사용해보려고 시도해봤다. <code>MapStruct</code> 같은 경우 어찌저찌 사용을 했지만 <code>Swagger</code>의 경우엔 공통 응답 클래스를 따로 만들어서 그런가 원하는대로 응답 구조가 만들어지지 않아서 결국 Excel에 API 명세서를 작성했다.</p>
<p>나중에 좀 더 찾아보고 적용시켜보는 것을 목표로 해봐야겠다.</p>
<h1 id="알게-된-내용">알게 된 내용</h1>
<h2 id="카디널리티의-종류와-의미">카디널리티의 종류와 의미</h2>
<ol>
<li><p>Zero or One</p>
<ul>
<li>의미: 한 엔티티가 다른 엔티티와 0개 또는 1개의 관계를 가질 수 있음</li>
<li>예시: 사용자(User)가 프로필 사진(Profile Picture)을 가질 수 있지만, 필수는 아닐 경우</li>
</ul>
</li>
<li><p>Zero or Many</p>
<ul>
<li>의미: 한 엔티티가 다른 엔티티와 0개 이상(여러 개)의 관계를 가질 수 있음</li>
<li>예시: 사용자가 작성한 게시글(Post)이 없을 수도 있고, 여러 개일 수도 있는 경우</li>
</ul>
</li>
<li><p>One or Many (1 또는 N)</p>
<ul>
<li>의미: 한 엔터티가 다른 엔터티와 최소 1개 이상의 관계를 가질 때</li>
<li>예시: 주문(Order)은 반드시 하나 이상의 상품(Item)을 포함해야 하는 경우</li>
</ul>
</li>
<li><p>One Only (정확히 1)</p>
<ul>
<li>의미: 한 엔터티가 다른 엔터티와 정확히 1개의 관계를 가질 때</li>
<li>예시: 주민등록번호는 반드시 하나의 사용자와만 연결되는 경우</li>
</ul>
</li>
<li><p>Many (N)</p>
<ul>
<li>의미: 한 엔터티가 다른 엔터티와 여러 개의 관계를 가질 수 있고, 이때 최소값은 명시되지 않음</li>
<li>예시: 하나의 카테고리에 여러 개의 상품이 속할 수 있는 경우</li>
</ul>
</li>
<li><p>One (1)</p>
<ul>
<li>의미: 한 엔터티가 다른 엔터티와 최소 1개의 관계를 가질 수 있고, 이때 최대값은 명시되지 않음</li>
<li>예시: 학생(Student)이 반드시 하나의 학급(Class)에 속해야 하는 경우</li>
</ul>
</li>
</ol>
<h2 id="jackson-어노테이션">Jackson 어노테이션</h2>
<p><code>Jackson</code>은 자바 진영의 다양한 형식의 데이터를 지원하는 <code>Data processing</code> 툴이다. 스트림 방식으로 속도가 빠르고 유연하며 다양한 서드 파티 데이터 타입을 지원한다. 또한 어노테이션 방식으로 메타 데이터를 기술할 수 있다.</p>
<h3 id="어노테이션">어노테이션</h3>
<ul>
<li><code>@JsonIgnoreProperties</code>: 직렬화, 역직렬화 시 제외할 속성을 지정.</li>
<li><code>@JsonIgnore</code>: 멤버 변수 위에 선언해서 제외 처리</li>
<li><code>@JsonProperty</code>: Json으로 변환 시 사용할 이름 (DB 컬럼과 이름이 다르거나 API 응답과 이름이 다르지만 매핑해야 하는 경우)</li>
<li><code>@JsonInclude</code>: 값 존재 유무에 따라 직렬화 시 동작을 지정<ul>
<li>ALWAYS: 속성의 값에 의존하지 않고 항상 포함 (<code>default</code>)</li>
<li>NON_NULL: null이 아닌 값을 갖는 속성만 포함</li>
<li>NON_ABSENT: null과 참조 유형의 없음 값(<code>Optional</code>, <code>AtomicReference</code>)을 제외하고 포함</li>
<li>NON_EMPTY: null, 없음, 빈 문자열, 빈 컨테이너를 제외한 값 (NON_NULL과 NON_ABSENT 모두에 의해 제외되는 값을 뺀 나머지 값)</li>
</ul>
</li>
</ul>
<h2 id="github-readme-꼼수">GitHub README 꼼수</h2>
<p>ERD 등의 사진 파일을 올려야 할 경우 따로 image 폴더를 만들어서 두지 않고도 사진 파일을 사용할 수 있다.</p>
<ol>
<li>GitHub 프로젝트의 <code>Issues</code> 탭으로 들어간다.</li>
<li><code>New Issue</code>를 누른다.</li>
<li>업로드하고자 하는 이미지를 내용 칸으로 드래그한 뒤 놓는다.</li>
<li>잠시 기다리면 마크 다운 형식의 이미지 업로드 형식으로 이미지의 경로가 나온다.</li>
<li>그대로 복사/붙여넣기를 하면 된다.</li>
</ol>
<h1 id="트러블-슈팅">트러블 슈팅</h1>
<h2 id="페이지네이션">페이지네이션</h2>
<p>사실 페이지네이션은 해본 적이 없어서 알게된 내용으로 가야하지 않을까 싶지만.. 일단 적어본다.</p>
<p>페이지네이션은 DB 상으론 <code>limit</code>와 <code>offset</code>을 사용하면 되는데 <code>limit</code>는 결과 중 처음부터 얼마나 가져올 것인지를 정하는 것이고, <code>offset</code>은 어디서부터 가져올 것인지를 정하는 것이다. </p>
<p>이때 <code>offset</code>의 경우 입력받은 <code>page</code> 값에서 <code>1</code>만큼 빼준 뒤 입력받은 <code>size</code>를 곱해주면 된다. (<code>int offset = (page - 1) * size</code>)</p>
<blockquote>
<p><code>-1</code>을 하는 이유는 사용자가 볼 땐 <code>1 페이지</code>지만 실제로는 <code>0 페이지</code>이기 때문이다.</p>
</blockquote>
<p>전체 일정의 개수를 알려주기 위해서 repository에 <code>count</code> 메서드를 추가했다. 그리고 전체 일정 개수를 활용해서 전체 페이지 수 또한 계산 후 넘겨줬다.</p>
<pre><code class="language-java">    @Override
    public PageDto&lt;ScheduleWithAuthor&gt; findAll(GetSchedulesRequest request) {
        List&lt;ScheduleWithAuthor&gt; resultList = scheduleRepository.findAll(request.getAuthorId(), request.getSelectedDate(), request.getPage(), request.getSize());

        long totalCount = scheduleRepository.count();
        int totalPages = (int) Math.ceil((double) totalCount / request.getSize());
        return new PageDto&lt;&gt;(resultList, request.getPage(), request.getSize(), totalCount, totalPages);
    }</code></pre>
<p>따로 <code>PageDto</code> 객체를 만들어서 반환하도록 했다. 또한 다른 메서드와는 달리 작성자의 이름이 포함되어야 하기 때문에 작성자의 이름을 반환받기 위해 repository 폴더에 <code>ScheduleWithAuthor</code>를 만들어서 repository의 반환값으로 사용했다.</p>
<h2 id="rowmapper">RowMapper</h2>
<p>repository에서 <code>RowMapper</code>를 사용할 때 컴파일 에러가 났었었다.</p>
<pre><code class="language-java">    private RowMapper&lt;Schedule&gt; scheduleRowMapper() {
        return (rs, rowNum) -&gt; {
            Schedule.builder()
                .id(rs.getLong(&quot;schedule_id&quot;))
                .authorId(rs.getLong(&quot;author_id&quot;))
                .todo(rs.getString(&quot;todo&quot;))
                .password(rs.getString(&quot;password&quot;))
                .createdAt(rs.getTimestamp(&quot;created_at&quot;).toLocalDateTime())
                .lastUpdated(rs.getTimestamp(&quot;last_updated&quot;).toLocalDateTime())
                .build();
        }  // error!: return 문 누락
    }</code></pre>
<p>알고보니 <code>@Builder</code> 사용 시 리턴이 없기 때문에 람다 본문을 만들 필요가 없었었다.</p>
<pre><code class="language-java">    private RowMapper&lt;Schedule&gt; scheduleRowMapper() {
        return (rs, rowNum) -&gt;
            Schedule.builder()
                .id(rs.getLong(&quot;schedule_id&quot;))
                .authorId(rs.getLong(&quot;author_id&quot;))
                .todo(rs.getString(&quot;todo&quot;))
                .password(rs.getString(&quot;password&quot;))
                .createdAt(rs.getTimestamp(&quot;created_at&quot;).toLocalDateTime())
                .lastUpdated(rs.getTimestamp(&quot;last_updated&quot;).toLocalDateTime())
                .build();
    }</code></pre>
<h2 id="requestparam">@RequestParam</h2>
<p>일정 전체 조회 시 QueryParameter로 작성자 ID, 선택한 날짜, 페이지, 사이즈 값을 받아오게 되는데 이것들을 Request 객체로 만들어서 한 번에 가져오려고 했고, 별 생각 없이 <code>@RequestParam</code>을 사용했다.</p>
<pre><code class="language-java">    @GetMapping
    @Override
    public ApiResponse&lt;PageDto&lt;ScheduleWithAuthor&gt;&gt; findAllSchedules(@Valid @RequestParam GetSchedulesRequest request) {  // error!
        PageDto&lt;ScheduleWithAuthor&gt; authorDtoPageDto = scheduleService.findAll(request);
        return ApiResponse.success(OK, authorDtoPageDto, &quot;일정 전체 조회 성공&quot;);
    }</code></pre>
<p>하지만 요청 전송 시 <code>MissingServletRequestParameterException</code>이 발생했고 파라미터 <code>request</code>를 찾을 수 없다고 나왔다.</p>
<p>찾아보니 <code>@RequestParam</code>은 단일 파라미터를 매핑하기 때문에 <code>GetSchedulesRequest</code> 타입의 <code>request</code>라는 이름을 찾는 것이었다. 당연히 그런 타입은 없고, QueryParameter에도 <code>request</code>라는 이름이 없으니 예외가 발생하는 것이었다.</p>
<p>그래서 여러 파라미터를 읽는 경우에 사용하는 <code>@ModelAttribute</code>를 사용해서 해결하게 되었다.</p>
<pre><code class="language-java">    @GetMapping
    @Override
    public ApiResponse&lt;PageDto&lt;ScheduleWithAuthor&gt;&gt; findAllSchedules(@Valid @ModelAttribute GetSchedulesRequest request) {
        PageDto&lt;ScheduleWithAuthor&gt; authorDtoPageDto = scheduleService.findAll(request);
        return ApiResponse.success(OK, authorDtoPageDto, &quot;일정 전체 조회 성공&quot;);
    }</code></pre>
<h2 id="delete-구현">DELETE 구현</h2>
<pre><code class="language-java">@Getter
@RequiredArgsConstructor
public class DeleteScheduleRequest {
    @NotBlank
    private final String password;
}</code></pre>
<p>일정 삭제 시 <code>Password</code> 필드 하나만을 가지고 있는 Request를 보내게 되는데, 이때 <code>JSON -&gt; Object</code>로의 역직렬화가 제대로 되지 않아서 예외가 발생했다. 찾아보니 필드가 하나인 경우 <code>Delegating</code> 방식과 <code>Properties</code> 방식 중 어떤 것을 사용해야 할 지 몰라서 발생한 에러라고 한다.</p>
<pre><code class="language-java">@Getter
public class DeleteScheduleRequest {
    @NotBlank
    private final String password;

    @JsonCreator
    public DeleteScheduleRequest(String password) {
        this.password = password;
    }
}</code></pre>
<p>여러 해결 방법이 있었지만 나는 <code>@JsonCreator</code>를 사용해서 문제를 해결했다. 기본 생성자를 사용하지 않기 때문에 <code>final</code>을 제거할 필요가 없어서 선택했다.</p>
<p>이건 조금 다른 얘긴데 DTO에 대해서 튜터님께 여쭤보러 갔었을 때였는데 튜터님이 DELETE 코드를 잠깐 보시더니 리소스 삭제는 정말 신중하게 해야하고, 삭제를 한 뒤에도 tracking이 가능해야 한다고 말씀을 해주셨다. </p>
<p>그래서 내 생각에 가장 간단한 방법이라고 느낀 soft-delete 방식을 사용하게 되었다.</p>
<pre><code class="language-sql"># 기존 삭제 구문
delete from schedules where schedule_id = 1

# soft-delete 구문
update schedules set is_active = false where schedule_id = 1</code></pre>
<p>repository에서 기존 삭제 구문처럼 실제로 삭제하는 것이 아니라 상태를 나타내는 <code>is_active</code>를 추가한 뒤에 삭제 시 <code>false</code>로 바꿔주고, 다른 조회 구문에서 <code>where is_active = true</code>를 추가하게 되면 실제로 삭제가 된 것처럼 <code>is_active</code>가 <code>false</code>인 row를 제외하고 조회하게 된다.</p>
<blockquote>
<p>로그 파일을 따로 만들어서 tracking을 하는 방법도 있다.</p>
</blockquote>
<h2 id="password-암호화">Password 암호화</h2>
<p>일정 등록 시 <code>Password</code>를 등록하고, 일정 수정이나 삭제 시 <code>Password</code>를 확인해서 일치할 경우 기능이 수행되도록 한다. 이때 <code>Password</code> 암호화를 하지 않을 경우 DB에서 조회 시 사용자의 <code>password</code>가 저장된 상태 그대로 전부 보이기 때문에 보안에 좋지 않다.</p>
<p>따라서 <code>PasswordEncoder</code>를 사용하려고 했다. 실제로 <code>spring-boot-starter-security</code> 의존성 사용 시 <code>PasswordEncoder</code> 인터페이스를 빈으로 등록해서 구현체를 사용할 수 있다는 것을 알고 있었지만 <code>security</code> 사용 시 <code>filterChain</code>을 설정해줘야 하는 번거로움이 있어서 고민을 했었다.</p>
<p>하지만 <code>spring-security-crypto</code>를 사용하게 되면 별도의 <code>filterChain</code> 설정 없이 <code>PasswordEncoder</code>만 사용할 수 있다는 것을 알게 되었다.</p>
<pre><code class="language-java">implementation &#39;org.springframework.security:spring-security-crypto&#39;</code></pre>
<p>추가로 비밀번호 검증 시 단순하게 <code>passwordEncoder.encode(request.getPassword()).equals(schedule.getPassword())</code>를 사용하면 안된다. 입력한 값이 실제로 같더라도 암호화하는 과정에서 값이 달라지기 때문에 비밀번호 검증 시에는 <code>passwordEncoder.matches(request.getPassword(), schedule.getPassword())</code>를 사용해야 한다.</p>
<h2 id="메서드-추출">메서드 추출</h2>
<p>메서드 추출 단축키(<code>ctrl + alt + m</code>)가 갑자기 안먹혔을 때가 있었다. 처음엔 메서드 추출이 안되는 구문인 줄 알았지만 다른 구문도 마찬가지로 메서드 추출 단축키가 안먹혔었다.</p>
<p>왜 그런지 알아보니 최근에 그래픽 카드 업데이트를 하면서 Geforce Experience도 같이 다운받았는데 해당 기능 중 게임 내 오버레이 기능 중 메서드 추출 단축키와 겹치는 것이 있어서 먹히지 않았던 것이었다.</p>
<p>해당 기능을 비활성화해서 해결했다.</p>
<h2 id="단건-조회-시-optional-반환">단건 조회 시 Optional 반환</h2>
<p>객체 단건 조회 시 <code>null</code>을 반환받지 않도록 하기 위해서 <code>Optional</code>로 반환 받으려고 했다.</p>
<p>하지만 JDBC에서 <code>Optional</code>로 반환받아본 적이 없어서 조금 찾아보게 되었고, 알게 된 방법이 있다.</p>
<pre><code class="language-java">    @Override
    public Optional&lt;Schedule&gt; findById(Long scheduleId) {
        String sql = &quot;select * from schedules where schedule_id = :scheduleId and is_active = true&quot;;
        SqlParameterSource param = new MapSqlParameterSource(&quot;scheduleId&quot;, scheduleId);

        List&lt;Schedule&gt; schedules = jdbcTemplate.query(sql, param, scheduleRowMapper());
        return schedules.stream().findFirst();
    }</code></pre>
<p>바로 단건 조회여도 <code>query()</code>를 사용해서 <code>List</code>로 값을 가져오도록 하고, <code>return schedules.stream().findFirst()</code>를 통해 <code>Optional</code>로 반환하도록 했다.</p>
<blockquote>
<p><code>findFirst()</code>는 Optional을 반환한다</p>
</blockquote>
<h2 id="applicationyml">application.yml</h2>
<p>데이터베이스 연결 정보를 숨기기 위해서 프로필을 분리했다. </p>
<pre><code class="language-yml">spring:
  config:
    import:
      - classpath:application-dev.yml</code></pre>
<p>사실 다른 방법도 있지만 단순 과제였기 때문에 프로필을 분리해서 데이터베이스 정보가 있는 파일을 <code>.gitignore</code>에 추가하는 방식으로 선택했다.</p>
<h1 id="references">References</h1>
<p><a href="https://coding-business.tistory.com/34">https://coding-business.tistory.com/34</a> (오류 메시지)</p>
<p><a href="https://jiwondev.tistory.com/251">https://jiwondev.tistory.com/251</a> (DTO는 어떤 레이어에 포함되어야 하는가)</p>
<p><a href="https://jong-bae.tistory.com/80">https://jong-bae.tistory.com/80</a> ([MapStruct] @Mapping 활용하기)</p>
<p><a href="https://rk1993.tistory.com/196">https://rk1993.tistory.com/196</a> (MySQL 날짜 더하기)
<a href="https://phpschool.com/gnuboard4/bbs/board.php?bo_table=qna_db&amp;wr_id=224393&amp;sca=&amp;sfl=wr_subject%7C%7Cwr_content&amp;stx=date&amp;sop=and">https://phpschool.com/gnuboard4/bbs/board.php?bo_table=qna_db&amp;wr_id=224393&amp;sca=&amp;sfl=wr_subject%7C%7Cwr_content&amp;stx=date&amp;sop=and</a> (날짜 범위 설정)
<a href="https://insight-bgh.tistory.com/246">https://insight-bgh.tistory.com/246</a> (MySQL 타임존 설정)</p>
<p><a href="https://worthpreading.tistory.com/83">https://worthpreading.tistory.com/83</a> (깃허브 리드미에 이미지 올릴 때 꼼수)</p>
<p><a href="https://inpa.tistory.com/entry/ERD-CLOUD-%E2%98%81%EF%B8%8F-ERD-%EB%8B%A4%EC%9D%B4%EC%96%B4%EA%B7%B8%EB%9E%A8%EC%9D%84-%EC%98%A8%EB%9D%BC%EC%9D%B8%EC%97%90%EC%84%9C-%EA%B7%B8%EB%A0%A4%EB%B3%B4%EC%9E%90">https://inpa.tistory.com/entry/ERD-CLOUD-%E2%98%81%EF%B8%8F-ERD-%EB%8B%A4%EC%9D%B4%EC%96%B4%EA%B7%B8%EB%9E%A8%EC%9D%84-%EC%98%A8%EB%9D%BC%EC%9D%B8%EC%97%90%EC%84%9C-%EA%B7%B8%EB%A0%A4%EB%B3%B4%EC%9E%90</a> (ERD Cloud 사용 방법)</p>
<p><a href="https://velog.io/@westreed/Spring-application.yml-%EA%B4%80%EB%A6%AC%ED%95%98%EA%B8%B0">https://velog.io/@westreed/Spring-application.yml-%EA%B4%80%EB%A6%AC%ED%95%98%EA%B8%B0</a> (application.yml 관리하기)</p>
<p><a href="https://kong-dev.tistory.com/236">https://kong-dev.tistory.com/236</a> (Json 필드 매핑 관련)</p>
<p><a href="https://promisingmoon.tistory.com/215">https://promisingmoon.tistory.com/215</a> (security 없이 PasswordEncoder 추가)</p>
<p><a href="https://rainbow-flavor.tistory.com/12">https://rainbow-flavor.tistory.com/12</a> (메서드 추출 안될 때)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[AWS] AWS 아키텍처 및 생태계 (2)]]></title>
            <link>https://velog.io/@dereck-jun/AWS-AWS-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98-%EB%B0%8F-%EC%83%9D%ED%83%9C%EA%B3%84-2</link>
            <guid>https://velog.io/@dereck-jun/AWS-AWS-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98-%EB%B0%8F-%EC%83%9D%ED%83%9C%EA%B3%84-2</guid>
            <pubDate>Mon, 27 Jan 2025 13:13:49 GMT</pubDate>
            <description><![CDATA[<h1 id="4-performance-efficiency">4. Performance Efficiency</h1>
<p>성능 효율성은 컴퓨팅 리소스를 사용해 시스템 요구 사항을 충족하고, 수요가 바뀌고, 기술이 발전함에 따라 효율성을 유지하는 기능을 포함한다.</p>
<h2 id="설계-원칙">설계 원칙</h2>
<ul>
<li>첨단 기술의 대중화<ul>
<li>고급 기술을 대중화해서 사용할 수 있도록 하면 제품 개발에도 도움이 되므로 계속 흐름을 파악해야 함</li>
</ul>
</li>
<li>몇 분 안에 전 세계에 배포<ul>
<li>여러 리전에 배포를 해야 한다면 며칠이 아니라 몇 분 안에 배포해야 함</li>
<li>여러 지역에 쉽게 배포해야 한다는 의미이기도 함</li>
</ul>
</li>
<li>서버리스 아키텍처 사용<ul>
<li>서버 관리 부담 방지를 위한 서버리스 아키텍처 사용</li>
</ul>
</li>
<li>더 자주 실험하기<ul>
<li>간편한 비교 테스트를 수행해야 함</li>
<li>서버리스 아키텍처로 실험해서 제대로 작동하는지 확인해 볼 수 있음</li>
</ul>
</li>
<li>기계적 동조<ul>
<li>모든 AWS 서비스에 대해 알고 있어야 함</li>
<li>변경 사항이 발생하면 솔루션 아키텍처가 극적으로 변경될 수 있기 때문</li>
</ul>
</li>
</ul>
<h2 id="aws-서비스에서의-성능-효율성">AWS 서비스에서의 성능 효율성</h2>
<ul>
<li>사용자에게 적합한 서비스를 선택<ul>
<li><code>Lambda</code>: 서버리스가 필요</li>
<li><code>Auto Scaling</code>: 더 많은 EC2에 사용</li>
<li><code>EBS</code>: 디스크가 필요하지만 <code>gp2</code>, <code>io1</code> 을 사용해서 성능을 관리할 때</li>
<li><code>S3</code>: 전 세계적 확장에 사용</li>
<li><code>RDS</code>: 데이터베이스를 프로비저닝하고, Aurora로 마이그레이션 할 때 사용</li>
</ul>
</li>
<li>성능 검토<ul>
<li>생성하기 전에 필요한 것을 정확히 얻는지 확인: <code>CloudFormation</code></li>
<li>모든 성능 개선 사항의 업데이트: <code>AWS News Blog</code></li>
</ul>
</li>
<li>성능 모니터링<ul>
<li>성능이 좋은지 확인하는 방법: <code>CloudWatch Alarms</code>, <code>CloudWatch metrics</code>, <code>CloudWatch</code></li>
<li><code>Lambda</code>: 병목 되지 않고 애플리케이션의 람다 함수가 최소 시간 내에 실행되도록 함</li>
</ul>
</li>
<li>절충<ul>
<li><code>RDS</code> 와 <code>Aurora</code> 사용</li>
<li><code>ElastiCache</code>: 캐시에 오래된 데이터가 있지만 성능을 개선할 것인지, 일래스티 캐시 사용 없이 최신 버전을 얻을 것인지 절충해야 함</li>
<li><code>Snowball</code>: 데이터를 클라우드에서 확인하고 모든 네트워크 용량을 사용하거나, 데이터를 트럭으로 옮기고 일주일 안에 받을지 절충해야 함</li>
<li><code>CloudFront</code> 엣지 주변의 것을 캐싱하고, 전 세계에 배포하면 하루 동안 캐시돼서 웹사이트 업데이트 릴리스 시 사용자들이 새 버전을 얻는 데 시간이 걸릴 수 있음</li>
</ul>
</li>
</ul>
<h1 id="5-cost-optimization">5. Cost Optimization</h1>
<ul>
<li>비용 최적화는 비즈니스 가치를 창출하는 시스템을 최저의 가격으로 실행하는 능력이며 매우 합리적인 접근 방식이다.</li>
</ul>
<h2 id="비용-최적화-설계-원칙">비용 최적화 설계 원칙</h2>
<ul>
<li>소비 모델 채택<ul>
<li>서비스를 사용한 만큼만 비용 지불<ul>
<li><code>Lambda</code> → 사용하지 않으면 비용을 지불하지 않음</li>
<li><code>데이터베이스</code> → 사용하지 않아도 비용을 지불 (프로비저닝 했기 때문)</li>
</ul>
</li>
</ul>
</li>
<li>전반적인 효율성 측정<ul>
<li>CloudWatch를 사용하여 리소스를 효과적으로 사용하고 있는지 측정하는 것</li>
</ul>
</li>
<li>데이터 센터 운영 비용 지출 중단<ul>
<li>AWS가 인프라를 대신 관리해주고, 사용자가 조직의 애플리케이션과 시스템에 집중할 수 있도록 해 줌</li>
</ul>
</li>
<li>비용을 분석하고 어디에 지출되고 있는지 파악<ul>
<li>태그를 사용하여 각 애플리케이션의 비용을 추적해서 비용을 최적화할 수 있음</li>
</ul>
</li>
<li>애플리케이션 수준의 관리 서비스를 사용하여 소유 비용 절감<ul>
<li><strong>관리형 서비스는 클라우드 규모로 운영</strong>되기 때문에 트랜잭션 또는 서비스당 비용을 낮출 수 있음</li>
</ul>
</li>
</ul>
<h2 id="aws-서비스에서의-비용-최적화">AWS 서비스에서의 비용 최적화</h2>
<ul>
<li>지출 인식<ul>
<li><code>Budgets</code>, <code>Cost and Usage Report</code>, <code>Cost Explorer</code>, <code>Reserved Instance Reporting</code><ul>
<li>예약한 인스턴스를 실제로 사용하는지 파악할 수 있도록 해줌</li>
</ul>
</li>
</ul>
</li>
<li>리소스 비용 효율성<ul>
<li><code>Spot instance</code>, <code>Reserved instance</code>, <code>S3 Glacier</code><ul>
<li>비용 효율적인 리소스를 사용할 수 있는지 파악하는 것</li>
</ul>
</li>
</ul>
</li>
<li>수요/공급의 일치<ul>
<li><code>Auto Scaling</code>:  프로비저닝이 과잉되지 않도록 함</li>
<li><code>Lambda</code>: 서버리스 인프라 구조를 사용 중인 경우 고려</li>
</ul>
</li>
<li>시간 경과에 따른 최적화<ul>
<li><code>Trusted Advisor</code>, <code>Cost and Usage Report</code>, <code>AWS News Blog</code><ul>
<li>최적화에 필요한 정보를 얻을 수 있음</li>
</ul>
</li>
</ul>
</li>
</ul>
<h1 id="6-sustainability">6. Sustainability</h1>
<ul>
<li>지속 가능성은 클라우드 워크로드 실행이 환경에 미치는 영향을 최소화하는 데 중점을 둔다.</li>
</ul>
<h2 id="지속-가능성-설계-원칙">지속 가능성 설계 원칙</h2>
<ul>
<li>영향력 이해<ul>
<li>성과 지표를 설정하고 개선점을 평가하는 것으로 영향을 이해</li>
</ul>
</li>
<li>지속 가능성 목표 수립<ul>
<li>장기 목표와 목표 ROI(투자 수익률)를 세워 목표를 달성</li>
</ul>
</li>
<li>서비스 활용 극대화<ul>
<li>에너지 효율을 높이고 환경에 미치는 영향을 조절하기 위함</li>
</ul>
</li>
<li>더 효율적인 하드웨어 및 소프트웨어 제품을 예측해서 채용<ul>
<li>AWS의 인프라 최적화를 통해 새로운 것을 사용하면 효율이 올라갈 수 있음</li>
</ul>
</li>
<li>관리형 서비스 사용<ul>
<li>지속 가능성 모범 사례를 자동화하는 데 도움이 됨<ul>
<li>공유 서비스: 인프라의 양을 줄임</li>
<li>관리형 서비스: 자주 액세스하지 않는 데이터를 콜드 스토리지로 옮기고 컴퓨팅 용량을 조정함</li>
</ul>
</li>
</ul>
</li>
<li>클라우드 워크로드의 후속 영향 줄이기<ul>
<li>고객이 서비스를 사용하기 위해 지속적으로 디바이스를 업그레이드해야 할 필요를 줄여야 함</li>
</ul>
</li>
</ul>
<h2 id="aws-서비스에서의-지속-가능성">AWS 서비스에서의 지속 가능성</h2>
<ul>
<li>서버리스 제품을 비롯한 AWS의 지속 가능성에 도움이 되는 서비스<ul>
<li><code>EC2 Auto Scaling</code>, <code>Lambda</code>, <code>Fargate</code></li>
<li>기본적으로 태스크에 적절한 만큼의 컴퓨팅만을 수행</li>
</ul>
</li>
<li>AWS 용량 컴퓨팅 사용 시 에너지 효율 확인이 가능<ul>
<li><code>Cost Explorer AWS Graviton 2</code>, <code>EC2 T instance</code>, <code>@Spot instance</code></li>
</ul>
</li>
<li>스토리지 관련 비용 최적화<ul>
<li><code>EFS-IA</code>, <code>S3 Glacier</code>, <code>EBS Cold HDD volumes</code></li>
</ul>
</li>
<li>데이터가 적절한 계층에 있도록 만듦<ul>
<li><code>S3 Lifecycle Configurations</code>, <code>S3 Intelligent Tiering</code>, <code>Data Lifecycle Manager</code></li>
</ul>
</li>
<li>데이터베이스 (Read Local, Write Global)<ul>
<li><code>RDS Read Replicas</code>, <code>Aurora Global DB</code>, <code>DynamoDB Global Table</code>, <code>CloudFront</code></li>
</ul>
</li>
</ul>
<h1 id="aws-well-architected-tool">AWS Well-Architected Tool</h1>
<ul>
<li>6대 원칙을 기준으로 아키텍처를 검토하고 모범 사례를 선정할 수 있다.</li>
</ul>
<h2 id="작동-방식">작동 방식</h2>
<ol>
<li>워크로드를 선택해 질문에 답한다.</li>
<li>6대 원칙을 기준으로 한 답과 사용자의 답변을 비교한다.</li>
<li>조언을 얻고 영상, 문서, 보고서 등을 받을 수 있다.</li>
</ol>
<h1 id="aws-customer-carbon-footprint-tool">AWS Customer Carbon Footprint Tool</h1>
<ul>
<li>AWS 사용으로 인해 발생하는 탄소 배출량을 추적, 측정, 검토 및 예측하는 데 사용되는 도구이다.</li>
<li>지속 가능성 목표를 달성해야 하는 경우 매우 유용하다.<ul>
<li>탄소 배출량, 절감량 탄소 배출량이 많은 서비스를 파악할 수 있음</li>
<li>시간 경과에 따른 배출량을 추적하고 AWS 계정에 사용된 100% 재생 에너지의 경로를 확인할 수 있음</li>
</ul>
</li>
</ul>
<h1 id="aws-cloud-adoption-framework">AWS Cloud Adoption Framework</h1>
<ul>
<li>CAF는 기본적으로 전자책이고 백서이다.</li>
<li>서비스는 아니지만 AWS의 혁신적인 사용을 통해 투어 디지털 혁신을 위한 포괄적인 계획을 수립하고 실행할 수 있도록 지원한다.</li>
<li>AWS 전문가들이 AWS 모범 사례와 수천 명의 고객으로부터 얻은 교훈을 활용하여 만든 솔루션이다.</li>
<li>AWS CAF는 성공적인 클라우드 전환을 뒷받침하는 특정 조직 역량을 식별하고, 기능을 6가지 관점으로 재구성한 것이다.<ul>
<li>비즈니스</li>
<li>사람</li>
<li>거버넌스</li>
<li>플랫폼</li>
<li>보안</li>
<li>운영</li>
</ul>
</li>
</ul>
<h2 id="caf-관점-및-기본-비즈니스-역량">CAF 관점 및 기본 비즈니스 역량</h2>
<ul>
<li>비즈니스 역량은 3개의 기둥으로 구성되어 있다.<ol>
<li>비즈니스 관점<ul>
<li>클라우드 투자를 통해 디지털 혁신 목표의 달성과 비즈니스 성과를 가속화할 수 있도록 도와줌<ul>
<li>전략 관리, 포트폴리오 관리, 혁신 관리, 제품 관리, 전략 파트너십, 데이터 수익화, 비즈니스 인사이트 및 데이터 과학</li>
</ul>
</li>
</ul>
</li>
<li><strong>사람 관점</strong><ul>
<li><strong>비즈니스라는 큰 테두리 안에서 기술과 비즈니스를 연결하는 가교 역할을 함</strong></li>
<li>지속적 성장을 위한 학습이 조직 문화로 빠르게 뿌리내리고 조직이 변화가 일상적인 비즈니스가 되는 곳으로 진화하도록 도와줌</li>
<li>따라서 사람을 통해 문화, 조직 구조, 리더십 및 인력에 초점을 맞출 수 있음<ul>
<li>조직 문화 발전, 혁신적 리더십, 클라우드 숙련도, 인력 혁신, 변화 가속화, 조직 설계 및 조직적 정비</li>
</ul>
</li>
</ul>
</li>
<li>거버넌스 관점<ul>
<li>조직이 누리는 이점을 극대화하고, 혁신과 관련된 위험을 최소화 하면서 클라우드 계획을 조율할 수 있도록 도와줌<ul>
<li>프로그램 및 포트폴리오 관리, 혜택 관리, 위험 관리, 클라우드 재무 관리, 애플리케이션 포트폴리오 관리, 데이터 거버넌스 및 데이터 큐레이션</li>
</ul>
</li>
</ul>
</li>
</ol>
</li>
</ul>
<h2 id="caf-관점-및-기반-역량-기술-역량">CAF 관점 및 기반 역량 기술 역량</h2>
<ul>
<li>기술 역량은 플랫폼, 보안 및 운영 관점으로 구성되어 있다.<ol>
<li>플랫폼 관점<ul>
<li>엔터프라이즈급의 확장 가능한 혼합형 클라우드 플랫폼을 구축하여 기존 워크로드를 현대화하고 새로운 클라우드 네이티브 솔루션을 구현할 수 있도록 도와줌</li>
<li>개별적인 역량은 아래와 같음<ul>
<li>플랫폼 아키텍처, 데이터 아키텍처, 플랫폼 엔지니어링, 데이터 엔지니어링, 프로비저닝 및 오케스트레이션, 최신 애플리케이션 개발, 지속적 통합 및 지속적 배포</li>
</ul>
</li>
</ul>
</li>
<li>보안 관점<ul>
<li>데이터 및 클라우드 워크로드의 기밀성, 무결성 및 가용성을 확보하는 데 도움이 됨</li>
<li>개별적인 역량은 아래와 같음<ul>
<li>보안, 거버넌스, 보안 보증, ID 및 액세스 관리, 위협 탐지, 취약성 관리, 인프라 준비 상태, 데이터 보호, 애플리케이션 보안 및 인시던트 대응</li>
</ul>
</li>
</ul>
</li>
<li>운영 관점<ul>
<li>클라우드 서비스를 비즈니스 요구 사항을 충족하는 수준으로 제공할 수 있도록 도와줌</li>
<li>개별적인 역량은 아래와 같음<ul>
<li>가시성, 이벤트 관리, 인시던트 및 문제 관리, 변경 및 릴리스 관리, 성능 및 용량 관리, 구성 관리, 패치 관리, 가용성 및 연속성 관리, 애플리케이션 관리</li>
</ul>
</li>
</ul>
</li>
</ol>
</li>
</ul>
<blockquote>
<p>각각의 역량이 어떤 관점 아래 속해있으며 실제로 이해하고 있는지를 시험을 통해 알아보고자 하는 것이기 때문에 위의 6가지 관점을 반드시 기억해야 한다.</p>
</blockquote>
<h2 id="aws-caf---혁신-도메인">AWS CAF - 혁신 도메인</h2>
<ul>
<li>기술<ul>
<li>클라우드를 사용하여 레거시 인프라 애플리케이션 데이터 및 분석 플랫폼을 이전하고 현대화하는 것</li>
</ul>
</li>
<li>프로세스<ul>
<li>비즈니스 운영을 디지털화, 자동화 및 최적화하는 것이며 이를 위해선 다음 두 가지가 필요함<ol>
<li>클라우드의 도움을 받아 새로운 데이터 및 분석 플랫폼을 활용하여 실행 가능한 인사이트를 창출하는 것</li>
<li>머신 러닝을 사용하여 고객 서비스 경험을 개선하는 것</li>
</ol>
</li>
</ul>
</li>
<li>조직<ul>
<li>운영 모델을 어떻게 재정립하느냐의 문제<ul>
<li>제품과 가치 흐름을 중심으로 팀을 재편해야 함</li>
<li>애자일 방법론을 활용하여 변화에 신속하게 대응하고 지속적인 발전을 이뤄내야 함</li>
</ul>
</li>
</ul>
</li>
<li>제품<ul>
<li>제품과 서비스 및 수익 모델과 같은 새로운 가치 제안을 창출하여 비즈니스 모델을 재정립하는 것</li>
</ul>
</li>
</ul>
<h2 id="aws-caf---혁신-단계">AWS CAF - 혁신 단계</h2>
<ul>
<li>비전 수립 단계 (비즈니스 기회 식별)<ul>
<li>클라우드를 채택하여 혁신 기회를 식별하고 디지털 혁신의 기반을 구축함으로써 비즈니스 성과가 어떻게 가속화될 것인가를 구체적으로 제시해야 함</li>
</ul>
</li>
<li>정렬 단계 (비즈니스 기회를 CAF 관점과 비교하여 격차를 찾고 실행 계획을 세움)<ul>
<li>지금까지 살펴본 6가지 CAF 관점을 검토하여 필요한 역량과 현재 역량 사이의 격차를 파악하고 실행 계획을 도출하는 단계</li>
</ul>
</li>
<li>실행 단계<ul>
<li>실제로 파일럿 프로젝트를 구축하고 이를 프로덕션 환경에 제공하여 점진적으로 증가하는 비즈니스 가치를 입증</li>
</ul>
</li>
<li>확장 단계<ul>
<li>원하는 비즈니스 혜택을 실천하면서 파일럿 이니셔티브를 원하는 규모로 확장</li>
</ul>
</li>
</ul>
<blockquote>
<p>시험과 관련하여, <strong>어떤 관점이 프레임워크의 일부분에 해당하는지 여부</strong>와 <strong>어떤 역량이 특정 관점에 속하는지 여부를 식별</strong>하는 질문, 마지막으로 <strong>혁신 도메인</strong>과 <strong>해당 단계</strong>에 대한 문제도 출제될 것이다.</p>
</blockquote>
<h1 id="aws-right-sizing">AWS Right Sizing</h1>
<ul>
<li>클라우드는 탄력적이고 언제든지 인스턴스 유형을 바꿀 수 있기 때문에 가장 강력한 인스턴스를 선택하는 것이 좋다.</li>
<li>Right Sizing은 <strong>최저 비용으로 인스턴스의 유형이나 크기를 워크로드의 성능과 용량의 요구 사항에 일치시키는 프로세스</strong>이다.<ul>
<li>클라우드에선 스케일 업이 쉽기 때문에 크기가 올바른지 확인하는 것</li>
<li>따라서 <strong>작은 크기로 시작해 올바른 크기를 찾아야 함</strong></li>
</ul>
</li>
<li>배포한 인스턴스를 지속적으로 확인하는 프로세스이다.<ul>
<li>용량이나 성능을 손상하지 않고 지표를 통해 제거하거나 축소할 수 있는 기회를 식별</li>
<li>결과적으로 비용 절감</li>
</ul>
</li>
<li>Right Sizing는 두 가지 순간에 매우 중요하다.<ol>
<li><strong>클라우드 마이그레이션 직전</strong><ul>
<li>기업이 클라우드로 마이그레이션 할 때 가장 큰 인스턴스 크기로 두고 잊어버리는 경우가 많기 때문</li>
</ul>
</li>
<li><strong>클라우드 마이그레이션이 끝나도 달에 한 번 정도 지속적으로 해야 함</strong><ul>
<li>요구 사항이 변경되고 올바른 크기로 늘릴지, 줄일지 확인하는 것이 중요하기 때문</li>
</ul>
</li>
</ol>
</li>
<li><code>CloudWatch</code>, <code>Cost Explorer Trusted Advisor</code> 와 같은 크기 조정을 돕는 도구가 있다.</li>
</ul>
<h1 id="aws-생태계">AWS 생태계</h1>
<p>AWS를 둘러싼 생태계에는 많은 무료 리소스가 있다. 예를 들어 블로그나 커뮤니티 기반의 포럼, 백서와 가이드도 있다. 예시로 Well-Architected 프레임워크는 백서 중 하나이다.</p>
<ul>
<li>AWS Blogs: <a href="https://aws.amazon.com/blogs/aws/">https://aws.amazon.com/blogs/aws/</a></li>
<li>AWS Forums: <a href="https://forums.aws.amazon.com/index.jspa">https://forums.aws.amazon.com/index.jspa</a></li>
<li>AWS Whitepapers &amp; Guides: <a href="https://aws.amazon.com/whitepapers">https://aws.amazon.com/whitepapers</a></li>
<li>AWS Partner Solutions: <a href="https://aws.amazon.com/quickstart">https://aws.amazon.com/quickstart</a><ul>
<li>AWS 클라우드에서 애플리케이션 및 워크로드를 AWS 모범 사례에 따라 자동으로 배포할 수 있음</li>
<li>템플릿을 사용하여 프로덕션 환경을 매우 빠르게 구축할 수 있음</li>
</ul>
</li>
</ul>
<h1 id="aws-iq">AWS IQ</h1>
<ul>
<li>IQ는 프로젝트에 도움을 줄 전문가를 빠르게 찾는 데 사용된다.</li>
<li>인증을 받은 (제 3자) 전문가를 많이 확보하여 온디맨드 프로젝트 작업에 투입할 수 있게 해 주는 것이다.</li>
<li>IQ는 화상 회의, 계약 관리, 안전한 협업 및 통합 청구 기능을 제공한다.</li>
</ul>
<h2 id="iq-고객의-경우">IQ 고객의 경우</h2>
<p><img src="https://velog.velcdn.com/images/dereck-jun/post/f2f463c6-49b9-480a-81c8-b3c7fb1a6ab3/image.png" alt=""></p>
<ol>
<li>자신의 프로젝트에 대한 설명이 포함된 요청을 제출하게 된다.</li>
<li>받은 응답을 검토하고 요구 사항 및 일정 등을 토대로 전문가와 연결된다.</li>
<li>몇몇 후보자를 선정하고 나서 요금이나 업무 경험 등을 토대로 전문가를 선택할 수 있다.</li>
<li>해당 전문가에게 사용자의 계정에 대한 액세스 권한을 안전하게 부여하여 안전하게 협업을 진행할 수 있다.</li>
<li>마지막으로, 전문가의 작업에 만족할 경우 마일스톤을 해제할 수 있고 그러면 마일스톤에 대한 비용이 사용자의 AWS 청구서에 직접 부과된다.</li>
</ol>
<h2 id="iq-전문가인-경우">IQ 전문가인 경우</h2>
<p><img src="https://velog.velcdn.com/images/dereck-jun/post/c3dfe477-0775-4f4c-985c-5db17daea832/image.png" alt=""></p>
<ol>
<li>본인 소개, 사진, 자격증 정보 등이 포함된 프로필을 생성하면 고객과 연결된다.</li>
<li>제안을 시작하고, 제안이 수락되면 안전하게 작업을 수행한다.</li>
<li>마일스톤이 해제된 후에 대가를 지급받게 된다.</li>
</ol>
<h1 id="aws-repost">AWS re:Post</h1>
<ul>
<li>도움을 받을 수 있는 또 다른 옵션은 re:Post를 사용하는 것으로 re:Post는 사실상 포럼이다.</li>
<li>궁금한 사항에 대한 답변을 찾고, 질문을 하고, 모범 사례를 찾고, 그룹에 참여할 수도 있는 커뮤니티 포럼이다.</li>
<li>AWS에 대한 기술적인 질문에 대해 많은 사람들이 답변하고, 전문가가 그 답변을 검토하는 방식으로 AWS가 관리하는 Q&amp;A 서비스이다.</li>
<li>작동 방식은 스택 오버플로우와 유사하다.</li>
<li>re:Post는 프리 티어의 일부로 액세스하는 데 비용이 들지 않는다.</li>
<li><strong>시간이 촉박한 질문에 대해서나 긴급한 조치가 필요한 경우에는 적합하지 않다.</strong></li>
</ul>
<h1 id="aws-knowledge-center">AWS Knowledge Center</h1>
<ul>
<li>AWS에 관해 가장 빈번하고 일반적인 질문과 요청을 찾을 수 있는 곳이다.<ul>
<li>인기 있는 서비스, 분석, 애플리케이션 통합 등 다양한 범주가 있음</li>
</ul>
</li>
<li>시험 응시자의 입장에서 볼 때 매우 일반적인 질문에 대한 답이나 모범 용례를 찾을 수 있다.</li>
</ul>
<h1 id="aws-managed-services">AWS Managed Services</h1>
<ul>
<li>AWS Managed Services(이하 AMS)는 실제로 AWS에서 인프라 구조 및 애플리케이션 지원을 제공하는 사람들로 구성된 팀이다.</li>
<li>따라서 AWS에는 서비스를 제공하는 전문가 팀이 있으며 이를 AWS 팀 즉, AMS 라고 한다.</li>
<li>AMS는 고객의 인프라 구조를 관리 및 운영하여 인프라 구조의 보안, 신뢰성 및 가용성이 보장되도록 하고, 조직이 일상적인 유지 관리 작업에서 벗어나 비즈니스 목표에 집중할 수 있도록 도와준다.</li>
<li>AMS는 완전 관리형 서비스이며 변경 요청, 모니터링, 패치 관리, 보안 및 백업 서비스와 같은 일반적인 활동을 AWS가 처리한다.</li>
<li>모범 사례를 구현하고 인프라 구조를 유지 관리하여 운영상의 부담과 위험을 줄여준다.</li>
<li>AMS는 연중무휴 24시간 운영되며 고객은 클라우드 환경을 시작하기만 하면 된다.</li>
<li>AMS를 통해 얻을 수 있는 이점은 다음과 같다.<ul>
<li>보안 강화</li>
<li>자동화에 집중 가능</li>
<li>규정 준수 강화</li>
<li>운영 비용 감소</li>
<li>관리의 간소화 및 원활한 혁신</li>
</ul>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/dereck-jun/post/ae5388d3-6e19-4ac6-a19a-8d05f00a4f5b/image.png" alt=""></p>
<ul>
<li>AMS는 고객이 클라우드 환경의 기초를 구축하고 그 기초 위에서 시스템을 유지, 확장 또는 이전할 수 있도록 한다.</li>
<li>위의 과정에서 고객에게 필요한 지원을 제공한다.</li>
<li>그 다음 고객이 클라우드에서 인프라 구조를 운영할 수 있도록 도와준다.</li>
</ul>
<h1 id="refenrence">Refenrence</h1>
<ul>
<li><a href="https://www.udemy.com/course/best-aws-certified-cloud/learn/lecture/29390176?start=0#content">https://www.udemy.com/course/best-aws-certified-cloud/learn/lecture/29390176?start=0#content</a></li>
</ul>
]]></description>
        </item>
    </channel>
</rss>