<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>summeryoung_.log</title>
        <link>https://velog.io/</link>
        <description>이불 밖은 위험해.</description>
        <lastBuildDate>Sun, 31 May 2026 04:05:49 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>summeryoung_.log</title>
            <url>https://velog.velcdn.com/images/summeryoung_/profile/b2b943f3-68e2-43fd-8838-78ddb0dd8635/image.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. summeryoung_.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/summeryoung_" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[자바 ORM 표준 JPA 프로그래밍] 10주차 스터디]]></title>
            <link>https://velog.io/@summeryoung_/%EC%9E%90%EB%B0%94-ORM-%ED%91%9C%EC%A4%80-JPA-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-10%EC%A3%BC%EC%B0%A8-%EC%8A%A4%ED%84%B0%EB%94%94</link>
            <guid>https://velog.io/@summeryoung_/%EC%9E%90%EB%B0%94-ORM-%ED%91%9C%EC%A4%80-JPA-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-10%EC%A3%BC%EC%B0%A8-%EC%8A%A4%ED%84%B0%EB%94%94</guid>
            <pubDate>Sun, 31 May 2026 04:05:49 GMT</pubDate>
            <description><![CDATA[<h1 id="10장-객체지향-쿼리-언어">10장. 객체지향 쿼리 언어</h1>
<h2 id="104-querydsl">10.4 QueryDSL</h2>
<p>JPA Criteria는 문자가 아닌 코드로 JPQL을 작성하기에 문법 오류를 컴파일 단계에서 잡을 수 있고, IDE 자동완성 기능의 도움을 받을 수 있다는 장점이 존재하지만, 단점은 너무 복잡하고 어렵다는 점이다.</p>
<p><strong>쿼리를 문자가 아닌 코드로 작성해도 쉽고, 간결하며 그 모양도 쿼리와 비슷하게 개발할 수 있는</strong> 프로젝트가 <strong>QueryDSL</strong>. </p>
<h3 id="1-querydsl-설정">(1) QueryDSL 설정</h3>
<h4 id="필요-라이브러리">필요 라이브러리</h4>
<ul>
<li>querydsl-jpa: QueryDSL JPA 라이브러리</li>
<li>querydsl-apt: 쿼리 타입 생성 시 필요한 라이브러리</li>
</ul>
<h4 id="환경설정">환경설정</h4>
<p>QueryDSL을 사용하기 위해서는 Criteria 메타 모델처럼 엔티티 기반의 <strong>쿼리 타입</strong>이라는 쿼리용 클래스를 생성해야함. </p>
<p>쿼리 타입 생성용 플러글인은 <code>pom.xml</code>에 추가해야함.</p>
<h3 id="2-시작">(2) 시작</h3>
<pre><code class="language-java">public void queryDSL() {
    EntityManager em = emf.createEntityManager();

    JPAQuery query = new JPAQuery(em);
    QMember qMember = new QMember(&quot;m&quot;);
    List&lt;Member&gt; members = query.from(qMember)
        .where(qMember.name.eq(&quot;회원1&quot;))
        .orderBy(qMember.name.desc())
        .list(qMember);

}</code></pre>
<p>QueryDSL 사용을 위해서는 JPAQuery 객체를 생성해야하는데, 이때 <strong>엔티티매니저를 생성자에게 넘겨</strong>준다.</p>
<h4 id="기본-q-생성">기본 Q 생성</h4>
<p>쿼리타입은 사용이 편리하도록 기본 인스턴스를 보관하고 있음. 다만, 같은 엔티티를 조인하거나 같은 엔티티를 서브쿼리에 사용하면 같은 별칭이 사용되기에 이때는 별칭을 직접 지정해서 사용해야함</p>
<pre><code class="language-java">public class QMember extends EntityPathBase&lt;Member&gt; {
    public static final QMember member = new QMember(&quot;member1&quot;);
}

QMember qMember = new QMember(&quot;m&quot;); //직접 지정
QMember qMember = QMember.member; //기본 인스턴스 사용</code></pre>
<p>쿼리 타입의 기본 인스턴스를 사용하면 <code>import static</code>을 활용해 코드를 더 간결하게 작성할 수 있음.</p>
<pre><code class="language-java">import static jpabook.jpashop.domain.QMember.member; //기본 인스턴스

public void basic() {
    EntityManager em = emf.createEntityManager();
    ...
}</code></pre>
<h3 id="3-검색-조건-쿼리">(3) 검색 조건 쿼리</h3>
<p>QueryDSL의 기본 쿼리기능</p>
<pre><code class="language-java">JPAQuery query = new JPAQuery(em);
QItem item = QItem.item;
List&lt;Item&gt; list = query.from(item)
    .where(item.name.eq(&quot;좋은상품&quot;).and(item.price.gt(20000)))
    .list(item); //조회할 프로젝션 지정</code></pre>
<p>QueryDSL의 where절에는 and나 or을 사용할 수 있음. 또한 다음처럼 여러 검색 조건을 사용해도 되며 이때에는 and 연산이 가능하다.</p>
<p>쿼리 타입의 필드는 필요한 대부분의 메소드를 명시적으로 제공한다.</p>
<h3 id="4-결과-조회">(4) 결과 조회</h3>
<p>쿼리 작성 이후 결과 조회 메소드를 호출하면 실제 데이터베이스를 조회한다. 보통 <code>uniqueResult()</code>나 <code>list()</code>를 사용하고 파라미터로 프로젝션 대상을 넘겨줌.</p>
<ul>
<li><code>uniqueResult()</code>: 조회 결과가 한 건일 때 사용. 조회 결과가 없으면 null을 반환하고 결과가 하나 이상이면 예외 발생</li>
<li><code>singleResult()</code>: <code>uniqueResult()</code>와 같지만 결과가 하나 이상이면 처음 데이터를 반환</li>
<li><code>list()</code>: 결과가 하나 이상일 때 사용하면, 없을 때는 빈 컬렉션을 반환.</li>
</ul>
<h3 id="5-페이징과-정렬">(5) 페이징과 정렬</h3>
<pre><code class="language-java">QItem item = QItem.item;

query.from(item)
    .where(item.price.gt(20000))
    .orderBy(item.price.desc(), item.stockQuantity.asc())
    .list(item);</code></pre>
<p>정렬은 <code>orderBy</code>를 사용하는데 쿼리 타입이 제공하는 <code>asc()</code>, <code>desc()</code>를 사용. <strong>페이징의 경우는 <code>offset</code>과 <code>limit</code>을 적절히 조합해서 사용하면 됨</strong>.</p>
<p>페이징은 <code>restrict()</code> 메소드에 QueryModifiers를 파라미터로 사용해도 됨.</p>
<pre><code class="language-java">QueryModifiers queryModifiers = new QueryModifiers(20L, 10L); //limit, offset
List&lt;Item&gt; list =
    query.from(item)
    .restrict(queryModifiers)
    .list(item);</code></pre>
<p>실제 페이징 처리를 위해서는 검색된 전체 데이터의 수를 알아야함. 이때는 <code>list()</code> 대신 <code>listResults()</code>를 사용함.</p>
<pre><code class="language-java">SearchResults&lt;Item&gt; result = 
    query.from(item)
    .where(item.price.gt(10000))
    .offset(10).limit(20)
    .listResults(item);

long total = result.getTotal(); //검색된 전체 데이터 수
long limit = result.getLimit();
long offset = result.getOffset();
List&lt;Item&gt; results = result.getResults(); //조회된 데이터</code></pre>
<p><code>listResults()</code>를 사용하면 전체 데이터 조회를 위한 <code>count</code> 쿼리를 한 번 더 실행함. 그리고 <code>SearchResults</code>를 반환하는데 이 객체에서 전체 데이터 수를 조회할 수 있음.</p>
<h3 id="6-그룹">(6) 그룹</h3>
<p>그룹은 <code>groupBy</code>를 사용하고, 그룹화된 결과를 제한하기 위해서는 <code>having</code>을 사용하면 됨.</p>
<pre><code class="language-java">query.from(item)
    .groupBy(item.price)
    .having(item.price.get(1000))
    .list(item);</code></pre>
<h3 id="7-조인">(7) 조인</h3>
<p>조인은 <code>innerJoin(join)</code>, <code>leftJoin</code>, <code>rightJoin</code>, <code>fullJoin</code>을 사용할 수 있고 추가로 JPQL의 on과 성능 최적화를 위한 <code>fetch</code> 조인 역시 사용할 수 있음.</p>
<pre><code class="language-java">QOrder order = QOrder.order;
QMember member =QMember.member;
QOrderItem orderItem = QOrderItem.orderItem;

query.from(order)
    .join(order.member, member)
    .leftJoin(order.orderItems, orderItem)
    .list(order);

//조인 join에 사용
query.from(order)
    .leftJoin(order.orderItems, orderItem)
    .on(orderItem.count.gt(2))
    .list(order);
//페치 조인 사용
query.from(order)
    .innerJoin(order.member, member).fetch()
    .leftJoin(order.orderItems, orderItem).fetch()
    .list(order);</code></pre>
<h3 id="8-서브쿼리">(8) 서브쿼리</h3>
<p>서브쿼리는 JPASubQuery를 생성해서 사용함. 서브 쿼리의 결과가 하나이면 <code>unique()</code>, 여러 건이면 <code>list()</code>를 사용.</p>
<pre><code class="language-java">QItem item = QItem.item;
QItem itemSub = new QItem(&quot;itemSub&quot;);

query.from(item)
    .where(item.price.eq(
            new JPAQuery().from(itemSub).unique(itemSub.price.max())
            ))
    .list(item);</code></pre>
<h3 id="9-프로젝션과-결과반환">(9) 프로젝션과 결과반환</h3>
<p><code>select</code>절에 조회 대상을 지정하는 것을 <strong>프로젝션</strong>이라함.</p>
<h4 id="프로젝션-대상이-하나일-때">프로젝션 대상이 하나일 때</h4>
<pre><code class="language-java">QItem item = QItem.item;
List&lt;String&gt; result = query.from(item).list(item.name);

for (String name : result) {
    System.out.println(&quot;name = &quot;+name);
}</code></pre>
<h4 id="여러-컬럼-반환과-튜플">여러 컬럼 반환과 튜플</h4>
<p>프로젝션 대상으로 여러 필드를 선택하면 QueryDSL은 기본으로 Tuple이라는 Map과 비슷한 내부 타입을 사용함. 조회 결과는 <code>tuple.get()</code> 메소드에 조회한 쿼리 타입을 지정하면 됨.</p>
<pre><code class="language-java">QItem item = QItem.item;

List&lt;Tuple&gt; result = query.from(item).list(item.name, item.price);

for (Tuple tuple : result) {
    System.out.println(&quot;name = &quot;+ tuple.get(item.name));
    System.out.println(&quot;price = &quot;+tuple.get(item.price));
}</code></pre>
<h4 id="빈-생성">빈 생성</h4>
<p>쿼리 결과를 엔티티가 아닌 <strong>특정 객체로</strong> 받고 싶으면 <strong>빈 생성(bean population)</strong> 기능을 사용. QueryDSL은 아래와 같은 방법들을 제공함.</p>
<ul>
<li>프로퍼티 접근</li>
<li>필드 접근</li>
<li>생성자 사용</li>
</ul>
<p>원하는 방법의 지정을 위해서는 Projections를 사용하면 됨.</p>
<h3 id="10-수정-삭제-배치-쿼리">(10) 수정, 삭제 배치 쿼리</h3>
<p>QueryDSL도 수정, 삭제 같은 배치 쿼리를 지원함. JPQL 배치 쿼리 같이 영속성 컨텍스트를 무시하고 데이터베이스를 직접 쿼리하는 부분에 유의해야함.</p>
<p>수정 배치 쿼리</p>
<pre><code class="language-java">QItem item = QItem.item;
JPAUpdateClause updateClause = new JPAUpdateClause(em, item);
long count = updateClause.where(item.name.eq(&quot;책&quot;))
    .set(item.price, item.price.add(100))
    .execute();</code></pre>
<p>삭제 배치 쿼리</p>
<pre><code class="language-java">QItem item = QItem.item;
JPADeleteClause deleteClause = new JPADeleteCluase(em, item);
long count = deleteClause.where(item.name.eq(&quot;책&quot;))
    .execute();</code></pre>
<h3 id="11-동적-쿼리">(11) 동적 쿼리</h3>
<p><code>BooleanBuilder</code>를 사용하면 특정 조건에 따라 동적 쿼리를 편하게 생성할 수 있음.</p>
<pre><code class="language-java">SearchParam param = new SearchParam();
param.setName(&quot;개발자&quot;);
param.setPrice(10000);

QItem item = QItem.item;

BooleanBuilder builder = new BooleanBuilder();
if (StringUtils.hasText(param.getName())) {
    builder.and(item.name.contains(param.getName())));
...
}
</code></pre>
<h3 id="12-메소드-위임">(12) 메소드 위임</h3>
<p>메소드 위임 기능 사용 시 쿼리 타입에 검색 조건을 직접 정의할 수 있음</p>
<pre><code class="language-java">public class ItemExpression {
    @QueryDelegate(Item.class)
    public static BooleanExpression isExpensive(QItem item, Integer price) {
        return item.price.gt(price);
    }
}</code></pre>
<p>메소드 위임 기능을 사용하기 위해서는 우선 <strong>정적 메소드</strong>를 만들고 <code>@QueryDelegate</code> 어노테이션에 속성으로 이 기능을 적용할 엔티티를 지정해야함.</p>
<p>정적 메소드의 첫 번째 파라미터에는 대상 엔티티의 쿼리 타입을 지정하고, 나머지는 필요한 파라미터를 정의함.</p>
<h2 id="105-네이티브-sql">10.5 네이티브 SQL</h2>
<p>JPQL은 표준 SQL이 지원하는 대부분의 문법과 SQL 함수들을 지원하지만 특정 데이터베이스 종속적인 기능은 지원하지 않음.</p>
<ul>
<li>특정 데이터베이스만 지원하는 함수, 문법, SQL 쿼리 힌트</li>
<li>인라인 뷰, UNION, INTERSECT</li>
<li>스토어드 프로시저</li>
</ul>
<p>때로는 특정 데이터베이스에 종속적인 기능이 필요하기에 JPA는 특정 데이터베이스 종속적인 기능ㅇ르 사용할 수 있는 다양한 방법을 열어둠.</p>
<h3 id="1-네이티브-sql-사용">(1) 네이티브 SQL 사용</h3>
<p>네이티브 쿼리의 API는 다음 3가지가 존재</p>
<pre><code class="language-java">//결과 타입 정의
public Query createNativeQuery (String sqlString, Class resultClass);

//결과 타입을 정의할 수 없을 때
public Query createNativeQuery(String sqlString);

//결과 매핑 사용
public Query createNativeQuery(String sqlString, String resultSetMapping); </code></pre>
<h4 id="엔티티-조회">엔티티 조회</h4>
<p>네이티브 SQL은 <code>em.createNativeQuery(SQL, 결과 클래스)</code>를 사용함. 첫 번째 파라미터에 네이티브 SQL을 입력하고, 두 번째는 조회할 엔티티 클래스의 타입을 입력함.</p>
<p><strong>네이티브 SQL로 SQL만 직접 사용할 뿐, 나머지는 JPQL을 사용할 때와 같음. 조회한 엔티티 역시 영속성 컨텍스트에서 관리</strong>됨.</p>
<h4 id="값-조회">값 조회</h4>
<pre><code class="language-java">String sql = &quot;SELECT ID, AGE, NAME, TEAM_ID &quot;+
            &quot;FROM MEMBER WHERE AGE &gt; ?&quot;;
Query nativeQuery = em.createNativeQuery(sql).setParameter(1,10);

List&lt;Object[]&gt; resultList = nativeQuery.getResultList();
for (Object[] row : resultList) {
    ...
}</code></pre>
<p>위에서는 엔티티로 조회하지 않고 단순히 값으로 조회함. 이렇게 여러 값으로 조회하기 위해서는 <code>em.createNativeQuery(SQL)</code>의 두 번째 파라미터를 사용하지 않으면 됨. JPA는 조회한 값들을 Object[]에 담아서 반환함. 여기서는 스칼라 값들을 조회했을 뿐이기에 영속성 컨텍스트가 관리하지 않음. JDBC로 데이터를 조회한 것과 같음.</p>
<h4 id="결과-매핑-사용">결과 매핑 사용</h4>
<p>엔티티와 스칼라 값을 함께 조회하는 것처럼 매핑이 복잡해지면 <code>@SqlResultSetMapping</code>을 정의해서 결과 매핑을 사용해야함.</p>
<pre><code class="language-java">String sql = &quot;SELECT M.ID , AGE, NAME, TEAM_ID, I.ORDER_COUNT &quot;+
            &quot;FROM MEMBER M&quot; +
            &quot;LEFT JOIN &quot; +
            &quot;         (SELECT IM.ID, COUNT(*) AS ORDER_COUNT &quot;+
            &quot;         FROM ORDERS O, MEMBER IM &quot;+
            &quot;       WHERE O.MEMBER_ID = IM.ID) I &quot; +
            &quot;ON M.ID = I.ID&quot;;

Query nativeQuery = em.createNativeQuery(sql, &quot;memberWithOrderCount&quot;);</code></pre>
<p><code>em.createNativeQuery(sql, &quot;memberWithOrderCount&quot;);</code>의 두 번째 파라미터에 결과 매핑 정보의 이름이 사용됨.</p>
<p>결과 매핑 정의</p>
<pre><code class="language-java">@Entity
@SqlResultSetMapping(name = &quot;memberWithOrderCount&quot;,
    entities = {@EntityResult (entityClass = Member.class)},
    columns = {@ColumnResult (name = &quot;ORDER_COUNT&quot;)})
public class Member {...}</code></pre>
<p><code>memberWithOrderCount</code>의 결과 매핑을 보면 회원 엔티티와 ORDER_COUNT 컬럼을 매핑함. <code>entities</code>, <code>columns</code>라는 이름에서 알 수 있듯, 여러 엔티티와 여러 컬럼을 매핑할 수 있음.</p>
<p><code>@FieldResult</code>를 사용해 컬럼명과 필드명을 직접 매핑하며, 해당 설정은 엔티티 필드에 정의한 <code>@Column</code>보다 앞섬. 불편한 점은 해당 어노테이션을 한 번이라도 사용하면, 전체 필드를 해당 어노테이션을 사용해 매핑해야한다.</p>
<p>두 엔티티 조회의 컬럼명이 중복될 때에도 역시 <code>@FieldResult</code>를 사용해야함.</p>
<h3 id="2-named-네이티브-sql">(2) Named 네이티브 SQL</h3>
<p>JPQL처럼 네이티브 SQL도 Named 네이티브 SQL을 사용해 정적 SQL을 작성할 수 있음.</p>
<pre><code class="language-java">@Entity
@NamedNativeQuery (
    name = &quot;Member.memberSQL&quot;,
    query = &quot;SELECT ID, AGE, NAME, TEAM_ID&quot; +
            &quot;FROM MEMBER WHERE AGE &gt; ?&quot;,
            resultClass = Member.class
)
public class Member {...}
)</code></pre>
<p><code>@NamedNativeQuery</code>로 Named 네이티브 SQL을 등록.</p>
<pre><code class="language-java">TypedQuery&lt;Member&gt; nativeQuery = 
    em.createNamedQuery(&quot;Member.memberSQL&quot;, Member.class)
    .setParameter(1, 20L);</code></pre>
<h4 id="namednativequery"><code>@NamedNativeQuery</code></h4>
<ul>
<li>name: 네임드 쿼리 이름 (필수)</li>
<li>query: SQL 쿼리(필수)</li>
<li>hints: 벤더 종속적인 힌트</li>
<li>resultClass: 결과 클래스</li>
<li>resultSetMapping: 결과 매핑 사용</li>
</ul>
<h3 id="3-네이티브-sql-xml에-정의">(3) 네이티브 SQL XML에 정의</h3>
<p>XML에 정의할 때는 순서를 지켜야하는데 <code>&lt;named-native-query&gt;</code>를 먼저 정의하고 <code>&lt;sql-result-set-mapping&gt;</code>를 정의해야함.</p>
<h3 id="5-스토어드-프로시저">(5) 스토어드 프로시저</h3>
<p>JPA 2.1부터 스토어드 프로시저를 지원함.</p>
<h4 id="스토어드-프로시저-사용">스토어드 프로시저 사용</h4>
<p>: 단순히 입력값을 두 배로 증가시켜주는 <code>proc_multiply</code>라는 스토어드 프로시저가 있을 때, 해당 프로시저는 첫 번째 파라미터로 값을 입력받고, 두 번째 파라미터로 결과를 반환함.</p>
<p>JPA로 해당 프로시저를 호출하기 위해서는 아래와 같다.</p>
<pre><code class="language-java">StoredProcedureQuery spq = em.createStoredProcedureQuery(&quot;proc_multiply&quot;);

spq.registerStoredProcedureParameter(1, Integer.class, ParameterMode.IN);
spq.registerStoredProcedureParameter(2, Integer.class, ParameterMode.OUT);

spq.setParameter(1, 100);
spq.execute();

Integer out = (Integer)spq.getOutputParameterValue(2);</code></pre>
<p>스토어드 프로시저를 사용하기 위해서는 <code>em.createStoredProcedureQuery()</code> 메소드에 사용할 스토어드 프로시저 이름을 입력하면 됨. 그리고 <code>registerStoredProcedureParameter()</code> 메소드를 사용해 프로시저에서 사용할 파라미터를 순서, 타입, 파라미터 모드 순으로 정의함.</p>
<p>사용 가능한 파라미터 모드는 아래와 같음.</p>
<pre><code class="language-java">public enum ParameterMode {
    IN, //INPUT 파라미터
    INOUT, //INPUT, OUTPUT 파라미터
    OUT, //OUTPUT 파라미터
    REF_CURSOR //CURSOR 파라미터
}</code></pre>
<p>파라미터 순서 대신 이름을 사용하는 방법 역시 존재함.</p>
<h4 id="named-스토어드-프로시저-사용">Named 스토어드 프로시저 사용</h4>
<p>스토어드 프로시저 쿼리에 이름을 부여해 사용하는것을 Named 스토어드 프로시저라함.</p>
<pre><code class="language-java">@NamedStoredProcedureQuery (
    name = &quot;multiply&quot;,
    procedureName = &quot;proc_multiply&quot;,
    parameters = {
        @StoredProcedureParameter(name = &quot;inParam&quot;, mode = ParameterMode.IN, type = Integer.class),
        @StoredProcedureParameter(naem = &quot;outParam&quot;, mode = ParameterMode.OUT, type = Integer.class)
    }
)
@Entity
public class Member {...}</code></pre>
<p>XML에 정의한 Named 스토어드 프로시저는 <code>em.createNamedStoredProcedureQuery()</code> 메소드에 등록한 Named 스토어드 프로시저 이름을 파라미터로 사용해 찾아올 수 있음.</p>
<hr>
<h2 id="106-객체지향-쿼리-심화">10.6 객체지향 쿼리 심화</h2>
<h3 id="1-벌크-연산">(1) 벌크 연산</h3>
<p>엔티티 수정을 위해서는 영속성 컨텍스트의 변경 감지 기능이나 병합을 사용하며, 삭제를 위해서는 <code>EntityManager.remove()</code> 메소드를 사용함. 다만, 이 방법으로 수 백개 이상의 엔티티를 하나씩 처리하기에는 시간이 너무 오래 걸림.</p>
<p>이럴 때에 <strong>여러 건을 한 번에 수정하거나 삭제하는 벌크 연산</strong>을 사용하면 됨.</p>
<pre><code class="language-java">String qlString = 
    &quot;update Product p &quot;+
    &quot;set p.price = p.price * 1.1 &quot;+
    &quot;where p.stockAmount &lt; :stockAmount&quot;;

int resultCount = em.createQuery(qlString)
                    .setParameter(&quot;stockAmount&quot;, 10)
                    .executeUpdate();</code></pre>
<p>벌크연산은 <code>executeUpdate()</code> 메소드를 사용함. 해당 메소드는 벌크 연산으로 영향을 받은 엔티티 건수를 반환한다. 삭제 연산 역시 같은 메소드를 사용한다.</p>
<h4 id="벌크-연산의-주의점">벌크 연산의 주의점</h4>
<p>벌크 연산 사용 시, 해당 연산이 <strong>영속성 컨텍스트를 무시하고 데이터베이스에 직접 쿼리한다는 점에 주의해야함</strong>.</p>
<p>이런 문제를 해결하기 위한 방법으로는 아래의 방법들이 존재한다.</p>
<ul>
<li><p><code>em.refresh()</code> 사용:
  벌크 연산을 수행한 직후 정확한 엔티티를 사용하기 위해서는 <code>em.refresh()</code>를 사용해 데이터베이스에서 해당 엔티티를 다시 조회한다.</p>
</li>
<li><p>벌크 연산 먼저 실행
  가장 실용적인 해결책은 벌크 연산을 <strong>가장 먼저 실행</strong>하는 것. 이 경우에는 벌크 연산 실행 후, 엔티티를 조회하기에 이미 벌크 연산으로 변경된 엔티티를 조회하게 되기에 괜찮다.</p>
</li>
<li><p>벌크 연산 수행 후 영속성 컨텍스트 초기화
  <strong>영속성 컨텍스트 초기화</strong>를 통해 남아있는 엔티티를 제거하는 것 역시 좋은 방법이다. 초기화 이후에는 데이터베이스에서 엔티티를 조회해 온다.</p>
</li>
</ul>
<h3 id="2-영속성-컨텍스트와-jpql">(2) 영속성 컨텍스트와 JPQL</h3>
<h4 id="쿼리-후-영속-상태인-것과-아닌-것">쿼리 후 영속 상태인 것과 아닌 것</h4>
<p>JPQL의 조회 대상으로는 엔티티, 임베디드 타입, 값 타입 등 다양한 종류가 있다. JPQL로 엔티티 조회 시, 영속성 컨텍스트에서 관리되지만, 만약 <strong>엔티티가 아니라면 영속성 컨텍스트에서 관리되지 않는다</strong>.</p>
<p>예로는 임베디드 타입의 경우 조회해 값을 변경해도 영속성 컨텍스트가 관리하지 않기에 변경 감지에 의한 수정이 발생하지 않는다. 만약 엔티티를 조회하게 되면, 엔티티가 가지고 있는 임베디드 타입은 함께 수정되기는 한다.</p>
<h4 id="jpql로-조회한-엔티티와-영속성-컨텍스트">JPQL로 조회한 엔티티와 영속성 컨텍스트</h4>
<ul>
<li>JPQL로 조회한 엔티티는 영속 상태</li>
<li>영속성 컨텍스트에 이미 존재하는 엔티티가 있으면 기존 엔티티를 반환함.</li>
</ul>
<h4 id="find-vs-jpql">find() vs JPQL</h4>
<p><code>em.find()</code> 메소드는 엔티티를 영속성 컨텍스트에서 먼저 찾고 없으면 데이터베이스에서 찾음. 따라서 해당 엔티티가 영속성 컨텍스트에 있으면 메모리에서 바로 찾기에 성능상 이점이 존재한다.</p>
<p>반면 JPQL은 <strong>항상 데이터베이스에 SQL을 실행해 결과를 조회한다.</strong></p>
<blockquote>
<p>JPQL의 특징</p>
</blockquote>
<ul>
<li>JPQL은 항상 데이터베이스를 조회</li>
<li>JPQL로 조회한 엔티티는 영속 상태</li>
<li>영속성 컨텍스트에 이미 존재하는 엔티티가 있으면 기존 엔티티를 반환함.</li>
</ul>
<h3 id="3-jpql과-플러시-모드">(3) JPQL과 플러시 모드</h3>
<p>플러시: 영속성 컨텍스트의 변경 내역을 데이터베이스에 동기화하는 것. JPA는 플러시가 일어날 때 영속성 컨텍스트에 등록, 수정, 삭제한 엔티티를 찾아 INSERT, UPDATE, DELETE SQL을 만들어 데이터베이스에 반영함.</p>
<p>플러시 호출을 위해서는 <code>em.flush()</code> 메소드를 직접 사용해도 되지만 보통 플러시 모드에 따라 커밋하기 직전이나 쿼리 실행 직전에 자동으로 플러시를 호출됨.</p>
<h4 id="쿼리와-플러시-모드">쿼리와 플러시 모드</h4>
<p>JPQL은 영속성 컨텍스트에 있는 데이터를 고려하지 않고 데이터베이스에서 데이터를 조회함. 따라서 JPQL을 실행하기 전에 영속성 컨텍스트의 내용을 데이터베이스에 반영해야함. 그렇지 않을 경우 의도치 않은 결과 발생 가능.</p>
<p><code>setFlushMode()</code>를 통해 플러시 모드를 설정할 수 있음.</p>
<h4 id="플러시-모드-최적화">플러시 모드 최적화</h4>
<p><code>FlushModeType.COMMIT</code> 모드는 트랜잭션을 커밋할 때만 플러시하고 쿼리를 실행할 때는 플러시하지 않음. 따라서 JPA 쿼리를 사용할 때 영속성 컨텍스트에는 있지만, 아직 데이터베이스에 반영하지 않은 데이터를 조회할 수 없다. 이런 상황은 잘못하면 데이터 무결성에 심각한 피해를 줄 수 있음.</p>
<p>JPA를 사용하지 않고 JDBC를 직접 사용해 SQL을 실행할 때에도 플러시 모드를 고민해야함. 즉, 쿼리 실행 직전에 <code>em.flush()</code>를 통해 영속성 컨텍스트의 내용을 데이터베이스에 동기화하는 것이 안전함.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[데이터베이스 팀프로젝트 3주차 작업로그]]></title>
            <link>https://velog.io/@summeryoung_/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%ED%8C%80%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-3%EC%A3%BC%EC%B0%A8-%EC%9E%91%EC%97%85%EB%A1%9C%EA%B7%B8</link>
            <guid>https://velog.io/@summeryoung_/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%ED%8C%80%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-3%EC%A3%BC%EC%B0%A8-%EC%9E%91%EC%97%85%EB%A1%9C%EA%B7%B8</guid>
            <pubDate>Tue, 26 May 2026 10:01:03 GMT</pubDate>
            <description><![CDATA[<h1 id="3주차-작업로그">3주차 작업로그</h1>
<h1 id="1-주요-기능-sql-쿼리-작성하기">1. 주요 기능 SQL 쿼리 작성하기</h1>
<aside>

<h4 id="동작-시나리오-상품">동작 시나리오: 상품</h4>
<ul>
<li>회원은 상품을 전체 조회할 수 있다. ✔️</li>
<li>회원은 키워드 검색을 통해 상품을 전체 조회할 수 있다. ✔️<ul>
<li>카테고리를 설정하여 상품을 조회할 수 있다. ✔️</li>
<li>가격순(오름/내림차순)을 설정하여 상품을 조회할 수 있다. ✔️</li>
</ul>
</li>
<li>회원은 상품 내에서 옵션별 선택지를 조회하고 선택할 수 있다. ✔️</aside>

</li>
</ul>
<h4 id="상품-목록-전체-조회">상품 목록 전체 조회</h4>
<pre><code class="language-sql">SELECT *
FROM product
ORDER BY created_at DESC;</code></pre>
<h4 id="상품-키워드-조회">상품 키워드 조회</h4>
<pre><code class="language-sql">SELECT *
FROM product 
WHERE product_name LIKE CONCAT(&#39;%&#39;, **{검색어}**, &#39;%&#39;)
ORDER BY product_name;</code></pre>
<h4 id="상품-카테고리별-조회">상품 카테고리별 조회</h4>
<pre><code class="language-sql">SELECT *
FROM product
WHERE category_id = **{카테고리 ID}**
ORDER BY product_name;</code></pre>
<h4 id="상품-가격순별오름차순내림차순-조회">상품 가격순별(오름차순/내림차순) 조회</h4>
<pre><code class="language-sql">SELECT *
FROM product
ORDER BY price ASC;</code></pre>
<pre><code class="language-sql">SELECT *
FROM product
ORDER BY price DESC;</code></pre>
<h4 id="개별-상품별--상세보기-존재하는-옵션-확인하기">개별 상품별  상세보기 (존재하는 옵션 확인하기)</h4>
<pre><code class="language-sql">SELECT pd.product_detail_id, od.option_type_id, od.option_detail_id
FROM product AS p JOIN 
         product_detail pd ON p.product_id = pd.product_id JOIN 
         product_option op ON pd.product_detail_id = op.product_detail_id JOIN 
         option_detail od ON op.option_detail_id = od.option_detail_id
WHERE p.product_id = **{클릭한 상품ID}**;</code></pre>
<h4 id="개별-상품별-옵션-선택하기">개별 상품별 옵션 선택하기</h4>
<pre><code class="language-sql">SELECT pd.product_detail_id
FROM product_detail pd 
JOIN product_option op ON pd.product_detail_id = op.product_detail_id
WHERE pd.product_id = **{상품ID}**
          AND op.option_detail_id IN (**{선택한 옵션상세ID(1)}**, **{선택한 옵션상세ID(2)}**)
GROUP BY pd.product_detail_id
HAVING COUNT(DISTINCT op.option_detail_id) = **{선택한 옵션 개수}**;</code></pre>
<aside>

<h4 id="동작-시나리오-장바구니">동작 시나리오: 장바구니</h4>
<ul>
<li>회원은 물건(옵션 상세까지 지정 후) 장바구니에 물건을 담을 수 있다.<ul>
<li>옵션 지정하지 않을 수 못 담도록 설정해야함</li>
</ul>
</li>
<li>회원은 장바구니의 물건의 수량을 증가 및 감소시킬 수 있다.</li>
<li>회원은 장바구니의 물건을 삭제할 수 있다.</aside>

</li>
</ul>
<h4 id="장바구니에-물건-담기">장바구니에 물건 담기</h4>
<pre><code class="language-sql">INSERT INTO cart_item(&quot;member_id&quot;, &quot;product_detail_id&quot;, &quot;quantity&quot;)
VALUES (?, ?, ?);</code></pre>
<h4 id="장바구니-물건-수량-증가감소">장바구니 물건 수량 증가/감소</h4>
<pre><code class="language-sql">UPDATE cart_item
SET quantity = quantity + 1
WHERE member_id = ? AND product_detail_id = ?;</code></pre>
<pre><code class="language-sql">UPDATE cart_item
SET quantity = quantity - 1
WHERE member_id = ? AND product_detail_id = ?;</code></pre>
<h4 id="장바구니-물건-삭제">장바구니 물건 삭제</h4>
<pre><code class="language-sql">DELETE FROM cart_item
WHERE member_id = ? AND product_detail_id = ?;</code></pre>
<hr>
<h1 id="2-통계-관련-sql-쿼리-작성하기">2. 통계 관련 SQL 쿼리 작성하기</h1>
<h4 id="상품별-판매량-조회">상품별 판매량 조회</h4>
<pre><code class="language-sql">SELECT p.product_id, SUM(pd.sales) AS total_sales
FROM product p JOIN
     product_detail pd ON p.product_id = pd.product_id
GROUP BY p.product_id;</code></pre>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/835b4c1e-a5b9-4059-933f-4b366d1bb6ee/image.png" alt=""></p>
<h4 id="상품상세별-판매량-조회">상품상세별 판매량 조회</h4>
<pre><code class="language-sql">SELECT pd.product_detail_id,
        MAX(CASE WHEN od.option_type_id=1 THEN od.option_value END) AS size_option,
        MAX(CASE WHEN od.option_type_id=2 THEN od.option_value END) AS color_option,
        SUM(pd.sales) AS total_sales
FROM product_detail pd JOIN
     product_option op ON pd.product_detail_id = op.product_detail_id JOIN
     option_detail od ON op.option_detail_id = od.option_detail_id
GROUP BY pd.product_detail_id;</code></pre>
<ul>
<li><p>작성시 AI 도움 받음</p>
<p>  질문: product_detail_id마다의 옵션이 행으로 분리되어 여러 개의 옵션이 붙으면 2-3행이 나오는 것을 어떻게 처리할지?</p>
<ul>
<li>행으로 분리되어있는것을 → 열로 피벗 이동</li>
<li>CASE WHEN을 사용해서 옵션 타입별로 우선 새로운 새로 열로 만듦.<ul>
<li>이때 앞에 MAX()를 사용해서 GROUP BY를 사용할 때, 널과 값이 존재하는 경우에는 존재하는 값을 선택하도록 설정.</li>
</ul>
</li>
</ul>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/b2fe6aa9-621d-4c45-b4da-61d084d0a0c8/image.png" alt=""></p>
<ul>
<li>product 테이블까지 조인해주면 어떤 상품의 어떤 옵션들이 얼마나 팔렸는지 조회 가능</li>
</ul>
<pre><code class="language-sql">SELECT p.product_id, pd.product_detail_id,
            MAX(CASE WHEN od.option_type_id=1 THEN od.option_value END) AS size_option,
            MAX(CASE WHEN od.option_type_id=2 THEN od.option_value END) AS color_option,
      SUM(pd.sales) AS total_sales
FROM product p JOIN
         product_detail pd ON p.product_id = pd.product_id JOIN
         product_option op ON pd.product_detail_id = op.product_detail_id JOIN
     option_detail od ON op.option_detail_id = od.option_detail_id
GROUP BY pd.product_detail_id;</code></pre>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/642b8df3-a781-4e08-9f9b-ddb7f9bed64e/image.png" alt=""></p>
<h4 id="월별-매출-조회">월별 매출 조회</h4>
<pre><code class="language-sql">SELECT MONTH(order_date) AS month, SUM(total_price) AS total_income
FROM orders
WHERE YEAR(order_date) = 2026
GROUP BY MONTH(order_date)
ORDER BY month;</code></pre>
<h4 id="전월-대비-매출">전월 대비 매출</h4>
<pre><code class="language-sql">WITH month_income AS (
    SELECT MONTH(order_date) AS month, SUM(total_price) AS total_income
    FROM orders
    WHERE YEAR(order_date) = 2026
    GROUP BY MONTH(order_date)
    ORDER BY month
)

SELECT month, total_income AS &quot;월 매출&quot;,
       LAG(total_income, 1, 0) OVER (ORDER BY month) AS &quot;전월매출&quot;,
       total_income - LAG(total_income, 1, 0) OVER (ORDER BY month) AS &quot;전월 대비 증감&quot;
FROM month_income;</code></pre>
<ul>
<li>CTE를 통해 우선 위에서 사용한 월별 매출을 임시 테이블처럼 사용할 수 있도록 구성</li>
<li>윈도우 함수 LAG를 통해 직전 행을 가져올 수 있도록 구성</li>
</ul>
<hr>
<h1 id="3-더미-데이터-넣어서-테스트하기">3. 더미 데이터 넣어서 테스트하기</h1>
<ul>
<li><input checked="" disabled="" type="checkbox"> 더미 데이터 생성</li>
<li><input checked="" disabled="" type="checkbox"> INSERT (삽입)</li>
<li><input checked="" disabled="" type="checkbox"> SELECT (조회)</li>
<li><input checked="" disabled="" type="checkbox"> UPDATE (수정)</li>
<li><input checked="" disabled="" type="checkbox"> DELETE (삭제)</li>
</ul>
<hr>
<h2 id="0-더미-데이터-datasql">0. 더미 데이터 (data.sql)</h2>
<pre><code class="language-sql">-- [제조업체]
INSERT INTO manufacturer (manufacturer_id, company_name, owner)
VALUES (1, &#39;ZARA&#39;, &#39;자라 소유주&#39;);
INSERT INTO manufacturer (manufacturer_id, company_name, owner)
VALUES (2, &#39;H&amp;M&#39;, &#39;H&amp;M 소유주&#39;);
INSERT INTO manufacturer (manufacturer_id, company_name, owner)
VALUES (3, &#39;UNIQLO&#39;, &#39;UNIQLO 소유주&#39;);
INSERT INTO manufacturer (manufacturer_id, company_name, owner)
VALUES (4, &#39;Nike&#39;, &#39;Nike 소유주&#39;);
INSERT INTO manufacturer (manufacturer_id, company_name, owner)
VALUES (5, &#39;Adidas&#39;, &#39;Adidas 소유주&#39;);

-- [카테고리]
INSERT INTO category (category_id, category_name)
VALUES (1, &#39;자켓&#39;);
INSERT INTO category (category_id, category_name)
VALUES (2, &#39;티셔츠&#39;);
INSERT INTO category (category_id, category_name)
VALUES (3, &#39;바지&#39;);
INSERT INTO category (category_id, category_name)
VALUES (4, &#39;스커트&#39;);
INSERT INTO category (category_id, category_name)
VALUES (5, &#39;아우터&#39;);
INSERT INTO category (category_id, category_name)
VALUES (6, &#39;니트&#39;);

-- [상품]
INSERT INTO product (product_id, manufacturer_id, product_name, price, category_id, image_url)
VALUES (1, 1, &#39;트위드 자켓&#39;, 150000, 1, &#39;http://img/zara-tweed-jacket&#39;);
INSERT INTO product (product_id, manufacturer_id, product_name, price, category_id, image_url)
VALUES (2, 1, &#39;플리츠 미디 스커트&#39;, 89000, 4, &#39;http://img/zara-pleats-skirt&#39;);
INSERT INTO product (product_id, manufacturer_id, product_name, price, category_id, image_url)
VALUES (3, 1, &#39;리넨 블라우스&#39;, 65000, 2, &#39;http://img/zara-linen-blouse&#39;);
INSERT INTO product (product_id, manufacturer_id, product_name, price, category_id, image_url)
VALUES (4, 2, &#39;오버사이즈 티셔츠&#39;, 29900, 2, &#39;http://img/hm-oversized-tee&#39;);
INSERT INTO product (product_id, manufacturer_id, product_name, price, category_id, image_url)
VALUES (5, 2, &#39;슬림핏 청바지&#39;, 49900, 3, &#39;http://img/hm-slim-jeans&#39;);
INSERT INTO product (product_id, manufacturer_id, product_name, price, category_id, image_url)
VALUES (6, 3, &#39;히트텍 롱슬리브&#39;, 19900, 2, &#39;http://img/uniqlo-heattech&#39;);
INSERT INTO product (product_id, manufacturer_id, product_name, price, category_id, image_url)
VALUES (7, 3, &#39;후리스 집업&#39;, 59900, 5, &#39;http://img/uniqlo-fleece-zip&#39;);
INSERT INTO product (product_id, manufacturer_id, product_name, price, category_id, image_url)
VALUES (8, 4, &#39;드라이핏 반팔&#39;, 45000, 2, &#39;http://img/nike-dri-fit&#39;);
INSERT INTO product (product_id, manufacturer_id, product_name, price, category_id, image_url)
VALUES (9, 5, &#39;트랙 재킷&#39;, 89000, 1, &#39;http://img/adidas-track-jacket&#39;);
INSERT INTO product (product_id, manufacturer_id, product_name, price, category_id, image_url)
VALUES (10, 5, &#39;조거 팬츠&#39;, 69000, 3, &#39;http://img/adidas-jogger-pants&#39;);
</code></pre>
<pre><code class="language-sql">-- [상품상세]
-- 트위드 자켓 (product_id=1)
INSERT INTO product_detail (product_detail_id, product_id, stock_quantity, surcharge, sales, image_url)
VALUES (1, 1, 30, 0, 10, &#39;http://img/zara-tweed-jacket-white-s&#39;);
INSERT INTO product_detail (product_detail_id, product_id, stock_quantity, surcharge, sales, image_url)
VALUES (2, 1, 25, 0, 20, &#39;http://img/zara-tweed-jacket-white-m&#39;);
INSERT INTO product_detail (product_detail_id, product_id, stock_quantity, surcharge, sales, image_url)
VALUES (3, 1, 20, 0, 15, &#39;http://img/zara-tweed-jacket-black-m&#39;);
INSERT INTO product_detail (product_detail_id, product_id, stock_quantity, surcharge, sales, image_url)
VALUES (4, 1, 15, 0, 5, &#39;http://img/zara-tweed-jacket-black-l&#39;);
-- 플리츠 미디 스커트 (product_id=2)
INSERT INTO product_detail (product_detail_id, product_id, stock_quantity, surcharge, sales, image_url)
VALUES (5, 2, 40, 0, 30, &#39;http://img/zara-pleats-skirt-beige-s&#39;);
INSERT INTO product_detail (product_detail_id, product_id, stock_quantity, surcharge, sales, image_url)
VALUES (6, 2, 35, 0, 25, &#39;http://img/zara-pleats-skirt-beige-m&#39;);
INSERT INTO product_detail (product_detail_id, product_id, stock_quantity, surcharge, sales, image_url)
VALUES (7, 2, 30, 0, 20, &#39;http://img/zara-pleats-skirt-black-m&#39;);
INSERT INTO product_detail (product_detail_id, product_id, stock_quantity, surcharge, sales, image_url)
VALUES (8, 2, 20, 0, 10, &#39;http://img/zara-pleats-skirt-navy-l&#39;);
-- 등...</code></pre>
<h2 id="1-조회-select">1. 조회 (SELECT)</h2>
<h3 id="1-상품별-옵션-조회하기">(1) 상품별 옵션 조회하기</h3>
<p>각 상품별로 어떤 옵션들이 있는지 조회</p>
<ul>
<li>상품상세(product_detai)과 옵션 상세(option_detail)을 둘을 연결하는 테이블인 상품옵션 조합(product_option) 테이블과 함께 조인해서 연관 있는 상품상세-옵션상세가 연결되도록 우선 설정.</li>
<li>그 후에 WHERE에 조건을 설정하여 찾으려는 상품의 id를 조회</li>
<li>해당 상품(id)와 연관된 옵션들의 목록을 볼 수 있음.</li>
<li>재고가 남아있는 상품(상세)들만 조회되도록 WHERE 절에 조건 추가.</li>
</ul>
<pre><code class="language-sql">SELECT *
FROM product_detail as pd JOIN
         product_option as pop ON pd.product_detail_id = pop.product_detail_id JOIN
     option_detail as od ON pop.option_detail_id = od.option_detail_id
WHERE pd.product_id = **{선택한 상품ID}**
            AND **pd.stock &gt; 0**;</code></pre>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/c8ce71ab-97a6-4d05-a927-cc12e5f4bd60/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/b2f5b5e9-dbc4-420c-8eb0-725ec279fd64/image.png" alt=""></p>
<h3 id="2-상품-옵션-타입별사이즈색상-조회하기">(2) 상품 옵션 타입별(사이즈/색상) 조회하기</h3>
<p>상품 안에서 색상/사이즈 내역을 조회해서 출력하기 위한 쿼리문</p>
<pre><code class="language-sql">SELECT *
FROM product_detail as pd JOIN
         product_option as pop ON pd.product_detail_id = pop.product_detail_id JOIN
     option_detail as od ON pop.option_detail_id = od.option_detail_id
WHERE pd.product_id = **{선택한 상품ID}** 
            AND option_type_id = **{선택한 옵션종류ID}**;</code></pre>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/4dd3072d-e6cd-4219-90da-624d8c669d9f/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/a6d2ba05-32b3-49d2-821e-fb789ce44cf3/image.png" alt=""></p>
<h3 id="3-카테고리별-상품-조회">(3) 카테고리별 상품 조회</h3>
<pre><code class="language-sql">SELECT *
FROM product as p JOIN
     category as c ON p.category_id = c.category_id
WHERE c.category_name = &#39;자켓&#39;;</code></pre>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/8eea0d06-56f5-4d75-b5d7-9ac51cdf2c1a/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/6bef26b8-fe46-4ce4-9538-280d7554678a/image.png" alt=""></p>
<h2 id="3-업데이트-update">3. 업데이트 (UPDATE)</h2>
<h3 id="1-상품-변경">(1) 상품 변경</h3>
<h4 id="상품가격-업데이트">상품가격 업데이트</h4>
<pre><code class="language-sql">UPDATE product
SET price = **{새로운 가격}**
WHERE product_id = **{변경하려는 상품}**;</code></pre>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/95c6cd6e-59d8-470e-9986-d8a720a83b67/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/09b2e335-be0b-49f2-852c-0b4c20429090/image.png" alt=""></p>
<h4 id="2-상품상세-판매수량-업데이트">(2) 상품상세 판매수량 업데이트</h4>
<pre><code class="language-sql">UPDATE product_detail
SET sales = {업데이트되는 판매량}
WHERE product_detail_id = {업데이트되는 상품상세ID};</code></pre>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/cb3bb36b-8420-4cb8-b4b5-db10de45e2ea/image.png" alt=""></p>
<h4 id="3-상품-옵션-변경">(3) 상품 옵션 변경</h4>
<pre><code class="language-sql">UPDATE product
SET category_id = **{새로운 옵션ID}**
WHERE product_id = **{바꾸려는 상품ID}**;</code></pre>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/3007d228-553f-4235-bd97-bcf5b20948d3/image.png" alt="">
<img src="https://velog.velcdn.com/images/summeryoung_/post/85d558c3-344a-48fb-896f-13e7fcf5af3a/image.png" alt=""></p>
<ul>
<li>존재하지 않는 카테고리의 아이디로 설정해 업데이트하려고 하면 불가능. 에러를 일으킴.</li>
</ul>
<h4 id="4-상품-옵션-테이블에서의-행-삭제">(4) 상품-옵션 테이블에서의 행 삭제</h4>
<p>상품-옵션 테이블의 행을 삭제하는 것은 “해당 상품의 옵션 종류 중 하나를 삭제하는 것을 의미”</p>
<pre><code class="language-sql">DELETE FROM product_option
WHERE product_detail_id = **{상품 상세ID}** 
            AND option_detail_id = **{삭제하려는 옵션상세ID}**;</code></pre>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/08dee03d-85f0-48a3-ae1f-b156baaabee8/image.png" alt=""></p>
<h2 id="4-삭제-delete">4. 삭제 (DELETE)</h2>
<h4 id="1-상품의-삭제--상품-삭제의-삭제">(1) 상품의 삭제 &amp; 상품 삭제의 삭제</h4>
<p>상품을 삭제했을 때, 상품을 외래키로 갖는 상품 상세 등이 어떻게 동작하는지를 확인.</p>
<ul>
<li>product_detail 테이블의 외래키에 ON DELTE CASCADE를 설정해놨기 때문에 상품이 삭제되면 관련된 옵션 등의 데이터를 가지고 있던 상품 상세 데이터들도 삭제</li>
</ul>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/32fd3628-6248-4769-9499-6e6c704e9302/image.png" alt=""></p>
<ul>
<li>뿐만 아니라 이렇게 product_detail의 데이터가 삭제될 경우 해당 데이터를 참조하던 상품-옵션 테이블에서 해당 데이터를 참조하던 행들 역시 ON DELETE CASCADE를 적용해 삭제되도록 구성.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/d3c95041-2b95-4ea0-9dae-0b88d3033c42/image.png" alt=""></p>
<h2 id="5-삽입-insert">5. 삽입 (INSERT)</h2>
<h4 id="상품-테이블-삽입">상품 테이블 삽입</h4>
<ul>
<li>정상 삽입
<img src="https://velog.velcdn.com/images/summeryoung_/post/83cdf8aa-99e7-4e7c-8a0d-2d7719949636/image.png" alt=""></li>
</ul>
<ul>
<li>기본키(PK) 중복 불가: 중복된 기본키로 새로운행 삽입 시 에러
<img src="https://velog.velcdn.com/images/summeryoung_/post/4d4af06d-ee70-43bf-8087-9b39940075de/image.png" alt=""></li>
</ul>
<ul>
<li>가격 널값일 때: 삽입 에러
<img src="https://velog.velcdn.com/images/summeryoung_/post/694950ed-ab98-4d33-8cc8-1c451c35898a/image.png" alt=""></li>
</ul>
<h4 id="상품-상세-테이블">상품 상세 테이블</h4>
<ul>
<li>정상 삽입
<img src="https://velog.velcdn.com/images/summeryoung_/post/51888d61-8856-45c3-bcca-23be1ee2ceab/image.png" alt=""></li>
</ul>
<ul>
<li>product_id(외래키) 지정하지 않을 시 삽입 불가 (에러)
<img src="https://velog.velcdn.com/images/summeryoung_/post/3b143540-9b10-4daf-a0bc-b8bba11f26be/image.png" alt=""></li>
</ul>
<ul>
<li>stock_quantity, surcharge, sales 등 0 이상이어야하는 값에 음수 대입시 삽입 불가(에러)
<img src="https://velog.velcdn.com/images/summeryoung_/post/df5d7485-d4a7-424f-9bb2-696408dc670f/image.png" alt="">
<img src="https://velog.velcdn.com/images/summeryoung_/post/cc4a7bd5-7cb6-4fcd-a196-84aa67db6a31/image.png" alt="">
<img src="https://velog.velcdn.com/images/summeryoung_/post/0cde2b51-fa66-4618-a0a5-a56250906805/image.png" alt=""></li>
</ul>
<ul>
<li>이미지 URL 생략하여 삽입: 이미지URL은 NULL값을 허용하므로 생략해서 삽입 가능
<img src="https://velog.velcdn.com/images/summeryoung_/post/5b909408-5b37-423a-b6df-c38d5abab038/image.png" alt=""></li>
</ul>
<h4 id="상품-옵션-테이블">상품-옵션 테이블</h4>
<ul>
<li>정상 삽입
<img src="https://velog.velcdn.com/images/summeryoung_/post/5cd29687-5cf5-4251-8b3f-4721e3c3b415/image.png" alt=""></li>
</ul>
<ul>
<li>옵션 상세ID/상품상세ID 생략: 두 외래키의 널값을 허용하지 않으므로 삽입 불가 (에러)</li>
</ul>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/9769d12c-f5b3-4fdd-a3aa-f66aa8d6e61d/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/e75900ea-ac76-4deb-be57-302ad8e53e75/image.png" alt=""></p>
<ul>
<li>외래키 무결성: 존재하지 않는 product_detail_id 또는 option_detail_id를 삽입하는 것은 불가 (에러)</li>
</ul>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/acccc852-cbf8-4126-a0ef-3929ca470636/image.png" alt=""></p>
<ul>
<li><input checked="" disabled="" type="checkbox"> 더미 데이터 생성</li>
<li><input checked="" disabled="" type="checkbox"> INSERT (삽입)</li>
<li><input checked="" disabled="" type="checkbox"> SELECT (조회)</li>
<li><input checked="" disabled="" type="checkbox"> UPDATE (수정)</li>
<li><input checked="" disabled="" type="checkbox"> DELETE (삭제)</li>
</ul>
<hr>
<h3 id="0-더미-데이터-datasql-1">0. 더미 데이터 (data.sql)</h3>
<pre><code class="language-sql">-- [옵션 종류]
INSERT INTO option_type (option_type_id, option_type_name)
VALUES (1, &#39;사이즈&#39;);
INSERT INTO option_type (option_type_id, option_type_name)
VALUES (2, &#39;색상&#39;);

-- [옵션 상세]
INSERT INTO option_detail (option_detail_id, option_type_id, option_value)
VALUES (1, 1, &#39;XS&#39;);
INSERT INTO option_detail (option_detail_id, option_type_id, option_value)
VALUES (2, 1, &#39;S&#39;);
INSERT INTO option_detail (option_detail_id, option_type_id, option_value)
VALUES (3, 1, &#39;M&#39;);
INSERT INTO option_detail (option_detail_id, option_type_id, option_value)
VALUES (4, 1, &#39;L&#39;);
INSERT INTO option_detail (option_detail_id, option_type_id, option_value)
VALUES (5, 1, &#39;XL&#39;);
INSERT INTO option_detail (option_detail_id, option_type_id, option_value)
VALUES (6, 2, &#39;화이트&#39;);
INSERT INTO option_detail (option_detail_id, option_type_id, option_value)
VALUES (7, 2, &#39;블랙&#39;);
INSERT INTO option_detail (option_detail_id, option_type_id, option_value)
VALUES (8, 2, &#39;네이비&#39;);
INSERT INTO option_detail (option_detail_id, option_type_id, option_value)
VALUES (9, 2, &#39;베이지&#39;);
INSERT INTO option_detail (option_detail_id, option_type_id, option_value)
VALUES (10, 2, &#39;그레이&#39;);</code></pre>
<h3 id="1-삽입-insert">1. 삽입 (INSERT)</h3>
<h4 id="1-옵션-종류-테이블">(1) 옵션 종류 테이블</h4>
<ul>
<li>정상 삽입
<img src="https://velog.velcdn.com/images/summeryoung_/post/c0f4182b-6723-4938-958f-f89d3ced4e61/image.png" alt=""></li>
</ul>
<ul>
<li>‼️ 문제점: 옵션 종류의 이름(분류) 역시 중복이면 안되는데 UNIQUE 설정이 되어있지 않아 중복으로 들어가는 상황</li>
</ul>
<p>⇒ option_type_name을 UNIQUE가 되도록 (option_type_id, option_type_name)을 복합키로 구성하도록 변경</p>
<pre><code class="language-sql">CREATE TABLE option_type
(
    option_type_id   BIGINT      NOT NULL AUTO_INCREMENT,
    option_type_name VARCHAR(30) NOT NULL,

    PRIMARY KEY (option_type_name, option_type_id)
);</code></pre>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/5faa585c-3046-4b54-993f-b13d92f57e52/image.png" alt="">
<img src="https://velog.velcdn.com/images/summeryoung_/post/c7d8dcd9-0dfb-4b5a-abf2-afc4a9ca3c8b/image.png" alt=""></p>
<h4 id="2-옵션-상세-테이블">(2) 옵션 상세 테이블</h4>
<ul>
<li>정상삽입</li>
<li>‼️ 문제:  (option_type, option_value)를 UNIQUE로 지정해야, 중복된 옵션 상세 내용이 들어가지 않음</li>
</ul>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/e0aef000-d3f3-4f3d-829f-de984c5ec184/image.png" alt="">
<img src="https://velog.velcdn.com/images/summeryoung_/post/489804a8-b707-40cb-bec9-b2a74b152aee/image.png" alt=""></p>
<ul>
<li>‼️ 문제: option_type_id에 널값을 허용하여 종류를 구분하지 않은 채로 옵션 상세 값이 들어감 ⇒ 외래키 널값 허용 금지하기</li>
</ul>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/494d9380-a253-47c5-b070-2c636af0c3ea/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/647b37b9-24a1-423d-802d-ab728b56c323/image.png" alt=""></p>
<p>⇒ ❓ 의문: PK(대리키)를 제외하고 두 속성을 합쳐서 UNIQUE인 제약이 있어도 괜찮을지?</p>
<h3 id="2-조회-select">2. 조회 (SELECT)</h3>
<h4 id="1-옵션-종류별-존재하는-옵션-조회">(1) 옵션 종류별 존재하는 옵션 조회</h4>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/f9d02ac5-8068-4b6f-9ab6-89515eb43921/image.png" alt=""></p>
<h3 id="3-수정-update">3. 수정 (UPDATE)</h3>
<h4 id="1-옵션-종류option-type-테이블-업데이트">(1) 옵션 종류(option type) 테이블 업데이트</h4>
<ul>
<li>정상 업데이트</li>
</ul>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/93015187-f18f-4ede-9c2e-c72de9f72d6d/image.png" alt=""></p>
<ul>
<li>‼️ 문제: option_type_name을 널값을 금지하긴했지만, 이게 문자열이 공백인거랑은 또 달라서 공백으로도 업데이트가 되는 부분 ⇒ check constraint로 추가하기 (또는 spring에서 @Not Blank 어노테이션 사용하기)</li>
</ul>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/11e4de3e-3cd0-4346-a9b2-085da8f5d093/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/6064f8d9-624d-4188-aafd-c43af2938eb9/image.png" alt=""></p>
<h4 id="2-옵션-상세option-detail-테이블-업데이트">(2) 옵션 상세(option detail) 테이블 업데이트</h4>
<ul>
<li>정상 업데이트</li>
</ul>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/68a698b3-fb9d-42ff-a51a-c1fd8796bc41/image.png" alt=""></p>
<ul>
<li>‼️문제: 옵션 종류와 마찬가지로 공백 문자열 허용하는 경우가 존재 ⇒ CHECK + CONSTRAINT 사용해서 수정</li>
</ul>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/a44e0ad1-3ae5-4cad-adc5-b916f4b1d875/image.png" alt=""></p>
<h3 id="4-삭제-delete-1">4. 삭제 (DELETE)</h3>
<h4 id="옵션-종류-테이블">옵션 종류 테이블</h4>
<ul>
<li>옵션 상세에서 참조하는 행을 삭제 ⇒ 삭제 불가 (금지 RESTRICTED)</li>
</ul>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/00270c05-ddc3-494c-adc4-7761b76c33db/image.png" alt=""></p>
<h4 id="옵션-상세-테이블">옵션 상세 테이블</h4>
<ul>
<li>정상 삭제: 자식 테이블이라 추가적인 제약은 없음.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/938c57d2-0f38-4a2f-a8ac-2ce6e231749e/image.png" alt=""></p>
<hr>
<h1 id="4-트리거프로시저함수">4. 트리거/프로시저/함수</h1>
<h3 id="리뷰-신고-트랜잭션--프로시저">리뷰 신고 (트랜잭션 + 프로시저)</h3>
<ul>
<li>리뷰 신고 시, review_report 테이블에 추가</li>
<li>review 테이블에 신고 횟수를 기록하기 위한 컬럼 추가 (수정사항)</li>
<li>리뷰 신고 테이블에 추가 → 리뷰에 신고 횟수 증가 이 부분을 하나의 트랜잭션으로 생성</li>
<li>트랜잭션 중간에 에러가 생겼을 때, 롤백 시키기 위한 방법을 고민하다 AI한테 물어봤는데 SQLEXCEPTION  발생시 빠져나가 롤백하도록 BEGIN 바로 아래에 구성.</li>
</ul>
<pre><code class="language-sql">DELIMITER $$

CREATE PROCEDURE report_review (
    IN r_user_id BIGINT,
    IN r_review_id BIGINT,
    IN r_reason VARCHAR(255)
) 
BEGIN
    DECLARE EXIT HANDLER FOR SQLEXCEPTION
    BEGIN
        ROLLBACK;
    END;

    START TRANSACTION;

    INSERT INTO review_report (review_id, reporter_member_id, report_reason, report_status)
    VALUES(r_review_id, r_user_id, r_reason, &#39;WAITING&#39;);

    UPDATE review
    SET report_count = report_count+1
    WHERE review_id = r_review_id;

    COMMIT;
END $$
DELIMITER ;</code></pre>
<h3 id="장바구니-아이템-추가-프로시저">장바구니 아이템 추가 (프로시저)</h3>
<pre><code class="language-sql">DELIMITER $$

CREATE PROCEDURE add_cart (
    IN p_member_id BIGINT,
    IN p_product_id BIGINT,
    IN p_quantity INT
) 
BEGIN
    DECLARE exist INT;

    DECLARE EXIT HANDLER FOR SQLEXCEPTION
    BEGIN
        ROLLBACK;
    END;

    START TRANSACTION;

    SELECT COUNT(*) INTO exist
    FROM cart_item
    WHERE member_id = p_member_id AND product_detail_id = p_product_id;

    IF exist &gt; 0 THEN
        UPDATE cart_item
        SET quantity = quantity + p_quantity
        WHERE member_id = p_member_id AND product_detail_id = p_product_id;
    ELSE
        INSERT INTO cart_item (member_id, product_detail_id, quantity)
        VALUES (p_member_id, p_product_id, p_quantity);
    END IF;

    COMMIT;
END $$
DELIMITER ;</code></pre>
<h3 id="장바구니-재고-확인-트리거">장바구니 재고 확인 (트리거)</h3>
<pre><code class="language-sql">DELIMITER $$

CREATE TRIGGER check_stock_insert
BEFORE INSERT ON cart_item
FOR EACH ROW
BEGIN
    DECLARE stock INT;

    SELECT stock_quantity INTO stock
    FROM product_detail
    WHERE product_detail_id = NEW.product_detail_id;

    IF NEW.quantity &gt; stock
    THEN SIGNAL SQLSTATE &#39;45000&#39;
         SET MESSAGE_TEXT = &#39;상품 재고가 부족합니다.&#39;;
    END IF;
END $$

CREATE TRIGGER check_stock_update
BEFORE UPDATE ON cart_item
FOR EACH ROW
BEGIN
    DECLARE stock INT;

    SELECT stock_quantity INTO stock
    FROM product_detail
    WHERE product_detail_id = NEW.product_detail_id;

    IF NEW.quantity &gt; stock
    THEN SIGNAL SQLSTATE &#39;45000&#39;
         SET MESSAGE_TEXT = &#39;상품 재고가 부족합니다.&#39;;
    END IF;
END $$
DELIMITER ;</code></pre>
<h3 id="장바구니-총-결제-금액-계산-함수">장바구니 총 결제 금액 계산 (함수)</h3>
<pre><code class="language-sql">DELIMITER $$

CREATE FUNCTION get_cart_price (
    p_member_id BIGINT
) 
RETURNS INT
DETERMINISTIC
BEGIN
    DECLARE total_price INT;

    SELECT IFNULL(SUM(price*quantity),0) INTO total_price
    FROM cart_item
    WHERE member_id = p_member_id;

    RETURN total_price;

END $$
DELIMITER ;

SELECT get_cart_price(5) AS total_price;</code></pre>
<hr>
<h1 id="5-상품-crud-작성하기">5. 상품 CRUD 작성하기</h1>
<h4 id="코드">코드</h4>
<p><a href="https://github.com/26-1-db-course-project/db-project-e-commerce/tree/main/backend/src/main/java/db/project/ecommerce/product">db-project-e-commerce/backend/src/main/java/db/project/ecommerce/product at main · 26-1-db-course-project/db-project-e-commerce</a></p>
<h2 id="1-폴더-분리">1. 폴더 분리</h2>
<ul>
<li>폴더는 controller, domain(entity), dto(response/request), service, repository로 분리하여 진행하였습니다.
<img src="https://velog.velcdn.com/images/summeryoung_/post/212df928-536e-476b-ac98-736f861810f1/image.png" alt=""></li>
</ul>
<ul>
<li>SQL 쿼리 공유
<img src="https://velog.velcdn.com/images/summeryoung_/post/041a5a84-5ebe-451c-b5f0-deef2db8555a/image.png" alt=""></li>
</ul>
<h2 id="2-controller">2. Controller</h2>
<ul>
<li>상품 생성 (POST)</li>
<li>상품 조회 (GET)<ul>
<li>상품 목록 조회 (일반)</li>
<li>상품 카테고리별 조회</li>
<li>상품 가격순 조회</li>
<li>상품 개별 조회</li>
</ul>
</li>
<li>상품 가격 수정 (PATCH)</li>
<li>상품 삭제 (DELETE)</li>
</ul>
<h4 id="코드-1">코드</h4>
<pre><code class="language-java">@Controller
@RequiredArgsConstructor
@RequestMapping(&quot;/products&quot;)
public class ProductController {
    private final ProductService productService;

    //TODO: 상품 생성
    @PostMapping
    public ResponseEntity&lt;ProductResponse&gt; createProduct(@RequestBody CreateProductRequest request) {
        ProductResponse response = productService.createProduct(request);

        return ResponseEntity.status(HttpStatus.CREATED).body(response);
    }

    //TODO: 상품 목록 조회 (가격순 정렬)
    @GetMapping
    public ResponseEntity&lt;ProductListResponse&gt; getProductList(@RequestParam(defaultValue = &quot;productName,desc&quot;) String sortBy) {
        ProductListResponse response = productService.getProductList(sortBy);

        return ResponseEntity.ok(response);
    }

    //TODO: 카테고리별 상품 목록 조회 (가격순 정렬)
    @GetMapping(&quot;/category/{categoryId}&quot;)
    public ResponseEntity&lt;ProductListResponse&gt; getProductListByCategory(@PathVariable(&quot;categoryId&quot;) Long categoryId,
                                                                        @RequestParam(defaultValue = &quot;productName,desc&quot;) String sortBy) {
        ProductListResponse response = productService.getProductListByCategory(categoryId, sortBy);

        return ResponseEntity.ok(response);
    }

    //TODO: 상품 개별 조회
    @GetMapping(&quot;/{productId}&quot;)
    public ResponseEntity&lt;ProductResponse&gt; getProductDetail(@PathVariable(&quot;productId&quot;) Long productId) {
        ProductResponse response = productService.getProductDetail(productId);

        return ResponseEntity.ok(response);
    }

    //TODO: 상품 검색
    @GetMapping(&quot;/search&quot;)
    public ResponseEntity&lt;ProductListResponse&gt; searchProduct(@RequestBody SearchProduct request,
                                                         @RequestParam(defaultValue = &quot;productName,desc&quot;) String sortBy) {
        ProductListResponse response = productService.searchProduct(request, sortBy);

        return ResponseEntity.ok(response);

    }

    //TODO: 상품 가격 업데이트
    @PatchMapping(&quot;/{productId}&quot;)
    public ResponseEntity&lt;Void&gt; updateProductPrice(@PathVariable(&quot;productId&quot;) Long productId,
                                                   @RequestBody UpdateProductPrice request) {
        productService.updateProductPrice(productId, request);

        return ResponseEntity.ok().build();
    }

    //TODO: 상품 삭제
    @DeleteMapping(&quot;/{productId}&quot;)
    public ResponseEntity&lt;Void&gt; deleteProduct(@PathVariable(&quot;productId&quot;) Long productId) {
        productService.deleteProduct(productId);

        return ResponseEntity.ok().build();
    }
}</code></pre>
<h2 id="3-domainentity">3. Domain(Entity)</h2>
<h3 id="category">Category</h3>
<pre><code class="language-java">@Entity
@Getter
@NoArgsConstructor
public class Category {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = &quot;category_id&quot;)
    private Long id;

    @Column(name = &quot;category_name&quot;)
    private String category;

    @Builder
    public Category(String category) {
        this.category = category;
    }
}</code></pre>
<h3 id="manufacturer">Manufacturer</h3>
<pre><code class="language-java">@Entity
@Getter
@NoArgsConstructor
public class Manufacturer {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = &quot;manufacturer_id&quot;)
    private Long id;

    @Column (name = &quot;company_name&quot;)
    private String company;

    @Column (name = &quot;owner&quot;)
    private String owner;

    @Builder
    public Manufacturer(String company, String owner) {
        this.company = company;
        this.owner = owner;
    }
}
</code></pre>
<h3 id="product">Product</h3>
<pre><code class="language-java">@Entity
@Getter
@NoArgsConstructor
public class Product {
    @Id
    @GeneratedValue (strategy = GenerationType.IDENTITY)
    @Column(name = &quot;product_id&quot;)
    private Long id;

    @ManyToOne (fetch = FetchType.LAZY)
    @JoinColumn(name = &quot;manufacturer_id&quot;)
    private Manufacturer manufacturer;

    @Column(name = &quot;product_name&quot;)
    private String productName;

    @Column(name = &quot;price&quot;)
    private int price;

    @OneToOne
    @JoinColumn(name = &quot;category_id&quot;)
    private Category category;

    @Column (name = &quot;image_url&quot;)
    private String imageUrl;

    @Builder
    public Product(Manufacturer manufacturer, String name, int price, Category category, String imageUrl) {
        this.manufacturer = manufacturer;
        this.productName = name;
        this.category = category;
        this.price = price;
        this.imageUrl = imageUrl;
    }

    public void updatePrice(int price) {
        this.price = price;
    }

}</code></pre>
<h2 id="4-dto">4. Dto</h2>
<h3 id="createproductrequest">CreateProductRequest</h3>
<pre><code class="language-java">@Getter
@AllArgsConstructor
public class CreateProductRequest {
    private Long manufacturerId;
    private String productName;
    private int price;
    private Long categoryId;
    private String imageUrl;
}</code></pre>
<h3 id="searchproduct">SearchProduct</h3>
<pre><code class="language-java">@Getter
@AllArgsConstructor
public class SearchProduct {
    private String keyword;
}</code></pre>
<h3 id="updateproductprice">UpdateProductPrice</h3>
<pre><code class="language-java">@Getter
@AllArgsConstructor
public class UpdateProductPrice {
    private int price;
}</code></pre>
<h3 id="productlistresponse">ProductListResponse</h3>
<pre><code class="language-java">@Getter
@Builder
@AllArgsConstructor
public class ProductListResponse {
    private List&lt;ProductResponse&gt; productResponseList;
    private Long productCount;

    public static ProductListResponse of (List&lt;Product&gt; products) {
        return ProductListResponse.builder()
                .productResponseList(products.stream().map(ProductResponse::of).toList())
                .productCount((long) products.size())
                .build();
    }
}</code></pre>
<h3 id="productresponse">ProductResponse</h3>
<pre><code class="language-java">@Getter
@Builder
@AllArgsConstructor
public class ProductResponse {
    private Long productId;
    private String productName;
    private int price;
    private String manufacturer;
    private String category;
    private String imageUrl;

    public static ProductResponse of (Product product) {
        return ProductResponse.builder()
                .productId(product.getId())
                .productName(product.getProductName())
                .price(product.getPrice())
                .category(product.getCategory().getCategory())
                .manufacturer(product.getManufacturer().getCompany())
                .imageUrl(product.getImageUrl())
                .build();
    }
}
</code></pre>
<h2 id="5-repository">5. Repository</h2>
<ul>
<li>검색 관련 쿼리만 JPQL을 사용하여 작성하였습니다.</li>
</ul>
<pre><code class="language-java">public interface ProductRepository extends JpaRepository&lt;Product, Long&gt; {
    List&lt;Product&gt; findAllByCategory(Category category, Sort sort);
    @Query(&quot;select p from Product p where p.productName LIKE %:keyword%&quot; )
    List&lt;Product&gt; searchByName(@Param(&quot;keyword&quot;)String keyword, Sort sort);
}</code></pre>
<h2 id="6-service">6. Service</h2>
<h4 id="코드-2">코드</h4>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
public class ProductService {

    private final ManufacturerRepository manufacturerRepository;
    private final CategoryRepository categoryRepository;
    private final ProductRepository productRepository;

    //TODO: 상품 생성
    @Transactional
    public ProductResponse createProduct(CreateProductRequest request) {
        Manufacturer manufacturer = getManufacturer(request.getManufacturerId());
        Category category = getCategory(request.getCategoryId());

        Product newProduct = Product.builder()
                .manufacturer(manufacturer)
                .name(request.getProductName())
                .price(request.getPrice())
                .category(category)
                .imageUrl(request.getImageUrl())
                .build();

        productRepository.save(newProduct);

        return ProductResponse.of(newProduct);
    }

    //TODO: 상품 목록조회 (일반)
    @Transactional(readOnly = true)
    public ProductListResponse getProductList(String sortBy) {
        Sort sort = createSort(sortBy);
        List&lt;Product&gt; productList = productRepository.findAll(sort);

        return ProductListResponse.of(productList);
    }

    //TODO: 상품 목록조회 (카테고리)
    @Transactional(readOnly = true)
    public ProductListResponse getProductListByCategory(Long categoryId, String sortBy) {
        Category category = getCategory(categoryId);
        Sort sort = createSort(sortBy);
        List&lt;Product&gt; productList = productRepository.findAllByCategory(category, sort);

        return ProductListResponse.of(productList);
    }

    //TODO: 상품 개별조회
    @Transactional(readOnly = true)
    public ProductResponse getProductDetail(Long productId) {
        Product product = getProduct(productId);

        return ProductResponse.of(product);
    }

    //TODO: 상품 가격 수정
    @Transactional
    public void updateProductPrice(Long productId, UpdateProductPrice request) {
        Product product = getProduct(productId);
        product.updatePrice(request.getPrice());
    }

    //TODO: 상품 삭제
    @Transactional
    public void deleteProduct(Long productId) {
        Product product = getProduct(productId);
        productRepository.delete(product);
    }

    //TODO: 상품 검색
    @Transactional(readOnly = true)
    public ProductListResponse searchProduct(SearchProduct request, String sortBy) {
        Sort sort = createSort(sortBy);
        List&lt;Product&gt; productList = productRepository.searchByName(request.getKeyword(),sort);

        return ProductListResponse.of(productList);
    }

    private Product getProduct(Long productId) {
        return productRepository.findById(productId)
                .orElseThrow(() -&gt; new CustomException(ErrorCode.NOT_FOUND));
    }
    private Manufacturer getManufacturer(Long manufacturerId) {
        return manufacturerRepository.findById(manufacturerId)
                .orElseThrow(() -&gt; new CustomException(ErrorCode.NOT_FOUND));
    }

    private Category getCategory (Long categoryId) {
        return categoryRepository.findById(categoryId)
                .orElseThrow(() -&gt; new CustomException(ErrorCode.NOT_FOUND));
    }

    //AI 도움.
    private Sort createSort (String sortBy) {
        try {
            String[] sortParams = sortBy.split(&quot;,&quot;);
            String property = sortParams[0];
            String direction = sortParams[1];

            if (direction.equalsIgnoreCase(&quot;asc&quot;)) {
                return Sort.by(Sort.Direction.ASC, property);
            } else {
                return Sort.by(Sort.Direction.DESC, property);
            }
        } catch (Exception e) {
            return Sort.by(Sort.Direction.DESC, &quot;productName&quot;);
        }
    }

}</code></pre>
<h2 id="7-테스트-화면">7. 테스트 화면</h2>
<h3 id="1-상품-목록-조회-일반-get">(1) 상품 목록 조회: 일반 (GET)</h3>
<h4 id="url-products">URL: /products</h4>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/58b6da70-90dc-45a0-9e0d-900d2a3896e6/image.png" alt=""></p>
<hr>
<h3 id="2-상품-목록-조회-카테고리별-get">(2) 상품 목록 조회: 카테고리별 (GET)</h3>
<h4 id="url-productcategorycategoryid">URL: /product/category/{categoryId}</h4>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/4b2f0ded-7a5c-43e6-b574-a3af96c35506/image.png" alt=""></p>
<hr>
<h3 id="3-상품-목록-조회-가격순-내림차순-get">(3) 상품 목록 조회 (가격순: 내림차순) (GET)</h3>
<h4 id="url-productssortbypricedesc">URL: /products?sortBy=price,desc</h4>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/cdf451f5-bff6-48c3-9924-f1ea319b6e38/image.png" alt=""></p>
<hr>
<h3 id="4-상품-목록-조회-가격순-오름차순-get">(4) 상품 목록 조회 (가격순: 오름차순) (GET)</h3>
<h4 id="url-productssortbypriceasc">URL: /products?sortBy=price,asc</h4>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/fd019b6a-c7cb-46b7-ab6d-51b6ae23ba27/image.png" alt=""></p>
<hr>
<h3 id="5-상품-개별-조회-get">(5) 상품 개별 조회 (GET)</h3>
<h4 id="url-productsproductid">URL: /products/{productId}</h4>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/180be5ff-b62b-4c6a-8ab4-c27166e0fd10/image.png" alt=""></p>
<hr>
<h3 id="6-상품-생성-post">(6) 상품 생성 (POST)</h3>
<h4 id="url-products-1">URL: /products</h4>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/b3ce1614-5987-4d1f-9bd0-b675b771751e/image.png" alt=""></p>
<hr>
<h3 id="7-상품-가격-수정">(7) 상품 가격 수정</h3>
<h4 id="url-productsproductid-1">URL: /products/{productId}</h4>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/56fec434-c8e5-4e32-b007-eeddc2995302/image.png" alt=""></p>
<hr>
<h3 id="8-상품-삭제">(8) 상품 삭제</h3>
<h4 id="url-productsproductid-2">URL: /products/{productId}</h4>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/525263c4-667c-4961-85f9-7f3acb0427f5/image.png" alt=""></p>
<hr>
<h3 id="9-상품-키워드-검색">(9) 상품 키워드 검색</h3>
<h4 id="url-productssearch">URL: /products/search</h4>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/d89d1901-59fb-4385-9059-009a361bf9a7/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[자바 ORM 표준 JPA 프로그래밍] 9주차 스터디]]></title>
            <link>https://velog.io/@summeryoung_/%EC%9E%90%EB%B0%94-ORM-%ED%91%9C%EC%A4%80-JPA-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-9%EC%A3%BC%EC%B0%A8-%EC%8A%A4%ED%84%B0%EB%94%94</link>
            <guid>https://velog.io/@summeryoung_/%EC%9E%90%EB%B0%94-ORM-%ED%91%9C%EC%A4%80-JPA-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-9%EC%A3%BC%EC%B0%A8-%EC%8A%A4%ED%84%B0%EB%94%94</guid>
            <pubDate>Sun, 24 May 2026 06:01:33 GMT</pubDate>
            <description><![CDATA[<h1 id="10장-객체지향-쿼리-언어-1">10장. 객체지향 쿼리 언어 (1)</h1>
<p>JPA는 복잡한 검색 조건을 사용해 엔티티 객체를 조회할 수 있는 다양한 쿼리 기술을 지원한다.</p>
<p>JPQL은 가장 중요한 객체지향 쿼리 언어이다. Criteria나 QueryDSL은 결국 JPQL을 편리하게 사용하도록 도와주는 기술이다.</p>
<h2 id="101-객체지향-쿼리-소개">10.1 객체지향 쿼리 소개</h2>
<p><code>EntityManager.find()</code> 메소드를 사용하면 식별자로 엔티티 하나를 조회할 수 있다. 이렇게 조회한 엔티티에 객체 그래프 탐색을 사용하면 연관된 엔티티들을 찾을 수 있다. </p>
<ul>
<li>식별자로 조회: <code>EntityManager.find()</code></li>
<li>객체 그래프 탐색: 예) <code>a.getB().getC()</code></li>
</ul>
<p>하지만 위의 방법으로는 현실적이고 복잡한 검색이 어렵다.</p>
<h4 id="jpql의-특징">JPQL의 특징</h4>
<p>데이터베이스 테이블이 아닌 <strong>엔티티 객체를 대상으로 개발</strong>하기에 검색 역시 테이블이 아닌 엔티티 객체를 대상으로 하는 방법.</p>
<ul>
<li>테이블이 아닌 <strong>객체를 대상으로 검색하는 객체지향 쿼리</strong></li>
<li>SQL을 추상화해서 특정 데이터베이스 SQL에 의존하지 않음.</li>
</ul>
<p>SQL: 데이터베이스 테이블을 대상으로 하는 데이터 중심의 쿼리
JPQL: 엔티티 객체를 대상으로하는 객체지향 쿼리. JPA는 JPQL을 분석해 적절한 SQL을 만들어 데이터베이스를 조회 -&gt; 그 결과로 엔티티 객체를 생성해 반환한다.</p>
<p>+) JPA가 공식 지원하는 기능</p>
<ul>
<li>JPQL</li>
<li>Criteria 쿼리: JPQL을 편하게 작성하도록 도와주는 API, 빌더 클래스 모음</li>
<li>네이티브 SQL: JPA에서 JPQL 대신 직접 SQL 사용 가능</li>
</ul>
<p>+) JPA가 공식 지원은 하지 않는 기능</p>
<ul>
<li>QueryDSL: Criteria 쿼리처럼 JPQL을 편하게 작성하도록 도와주는 빌더 클래스 모음. 비표준 오픈소스 프레임워크.</li>
<li>JDBC 직접 사용/MyBatis 같은 SQL 매퍼 프레임워크.</li>
</ul>
<h3 id="1-jpql-소개">(1) JPQL 소개</h3>
<p>JPQL은 <strong>엔티티 객체를 조회하는 객체지향 쿼리</strong>로 문법은 SQL과 비슷하고 ANSI 표준 SQL이 제공하는 기능을 유사하게 지원.</p>
<p>JPQL은 SQL을 추상화해서 특정 데이터베이스에 의존하지 않는다. 그리고 데이터베이스 방언만 변경하면 JPQL을 수정하지 않아도 자연스럽게 데이터베이스를 변경할 수 있다.</p>
<p>또한 JPQL은 SQL보다 간결하다. 엔티티 직접 조회, 묵시적 조인, 다형성 지원으로인해 SQL보다 코드가 간결하다.</p>
<p>예) 회원 엔티티 (회원 이름이 kim인 엔티티를 조인)</p>
<pre><code class="language-java">@Entity (name = &quot;Member&quot;)
public class Member {
    @Column (name = &quot;name&quot;)
    private String username;
}

String jpql = &quot;select m from Member as m where m.username = &#39;kim&#39;&quot;;
List&lt;Member&gt; resultList = em.createQuery(jpql, Member.class).getReulstList();</code></pre>
<h3 id="2-criteria-쿼리-소개">(2) Criteria 쿼리 소개</h3>
<p><strong>Criteria</strong>: JPQL을 생성하는 빌더 클래스. 장점은 문자가 아닌 <code>query.select(m).where(...)</code> 처럼 프로그래밍 코드로 JPQL을 작성할 수 있다는 점.</p>
<p>문자열 기반 쿼리와는 다르게 <strong>컴파일 시점에 오류를 발견 가능</strong>.</p>
<blockquote>
<p><strong>Criteria로 작성한 JPQL의 장점</strong></p>
</blockquote>
<ul>
<li>컴파일 시점에 오류를 발견할 수 있음</li>
<li>IDE를 사용하면 코드 자동완성을 지원함</li>
<li>동적 쿼리를 작성하기 편함</li>
</ul>
<p>JPA의 경우 2.0부터 Criteria를 지원함.
예)</p>
<pre><code class="language-java">CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery&lt;Member&gt; query = cb.createQuery(Member.class);

//루트 클래스 (조회를 시작할 클래스)
Root&lt;Member&gt; m = query.from(Member.class);

//쿼리 생성
CriteriaQuery&lt;Member&gt; cq = query.select(m).where(cb.equal(m.get(&quot;username&quot;), &quot;kim&quot;));
List&lt;Member&gt; resultList = em.createQuery(cq).getResultList();</code></pre>
<p>위를 참고하면 쿼리를 문자가 아닌 코드로 작성한 것을 알 수 있다. 위에서는 <code>m.get(&quot;username&quot;)</code>을 문자로 작성했지만, <strong>메타 모델</strong>을 사용하면 문자가 아니라 코드로 해당 부분을 작성할 수 있다.</p>
<p><strong>메타 모델 API</strong>: 자바가 제공하는 어노테이션 프로세서 기능 사용 시, 어노테이션을 분석하여 클래스를 생성 가능함. JPA의 경우 이 기능을 사용해 Member 엔티티 클래스로 부터 Member_라는 Criteria 전용 클래스를 생성하는데 이것을 이르는 말.</p>
<p>메타 모델을 사용하면 온전히 코드만 사용해서 쿼리를 작성할 수 있음.</p>
<pre><code class="language-java">m.get(&quot;username&quot;) -&gt; m.get(Member_.username)</code></pre>
<ul>
<li>Criteria가 가진 장점은 많지만, 이를 모두 상쇄할 만큼 복잡하고 장황함. 사용이 불편한 것과 동시에 Criteria로 작성한 코드는 한눈에 잘 들어오지 않는다는 단점 역시 존재한다.</li>
</ul>
<h3 id="3-querydsl-소개">(3) QueryDSL 소개</h3>
<p><strong>QueryDSL</strong>: Criteria처럼 JPQL 빌더의 역할을 함. 장점은 코드 기반이면서도 단순하고 사용이 쉽다. 또한 작성한 코드 역시 JPQL과 비슷해 한눈에 들어온다.  다만 JPA 표준이 아니고 오픈소스 프로젝트이다.</p>
<pre><code class="language-java">JPAQuery query = new JPAQuery(em);
QMember member = QMember.member;

List&lt;Member&gt; members = 
    query.from(member)
    .where(member.username.eq(&quot;kim&quot;))
    .list(member);</code></pre>
<p>QueryDSL 역시 어노테이션 프로세서를 통해 쿼리 전용 클래스를 만들어야함. QMember는 여기서 Member 엔티티 클래스를 기반으로 생성한 QueryDSL 쿼리 전용 클래스.</p>
<h3 id="4-네이티브-sql-소개">(4) 네이티브 SQL 소개</h3>
<p>JPA는 SQL을 직접 사용할 수 있는 기능을 제공하는데 이것을 &quot;네이티브 SQL&quot;이라고 함.</p>
<p>JPQL을 사용한다해도 가끔 특정 데이터베이스에 의존하는 기능을 사용하게 될 때가 존재한다.
예) 오라클 데이터베이스에서만 쓰는 CONNECT BY 등의 사용</p>
<p>다만 이런 특정 데이터베이스의 기능은 표준화되어있지 않기에 JPQL에서 사용할 수 없다. 이때 네이티브 SQL을 사용하면 된다.</p>
<pre><code class="language-java">String sql = &quot;SELECT id, age, team_id, name FROM member WHERE name = &quot;kim&quot;;
List&lt;Member&gt; resultList = em.createNativeQuery(sql, Member.class).getResultList();</code></pre>
<h3 id="5-jdbc의-직접-사용--마이바티스-같은-sql-매퍼-프레임워크-사용">(5) JDBC의 직접 사용 / 마이바티스 같은 SQL 매퍼 프레임워크 사용</h3>
<p>JDBC 커넥션에 직접 접근하려할 때 JPA는 JDBC 커텍션을 획득하는 API를 제공하지 않기에, JPA 구현체가 제공하는 방법을 사용해야함.</p>
<pre><code class="language-java">Session session = entityManager.unwrap(Session.class);
session.doWork(new Work () {
    @Override
    public void execute(Connection connection) throws SQLException {
        //work...
    }
});</code></pre>
<ul>
<li>JPA의 EntityManager에서 하이버네이트 Session을 우선 구현해야함</li>
<li>이후 Session의 doWork() 메소드를 호출해야함.</li>
</ul>
<p><strong>JDBC나 마이바티스를 JPA와 함께 사용하면 영속성 컨텍스트를 적절한 시점에 강제로 플러시(flush)해야함.</strong> 둘 모두 JPA를 우회해서 데이터베이스에 접근하기에 JPA는 인식할 수 없음. 따라서 영속성 컨텍스트와 데이터베이스를 불일치 상태로 만들어 데이터 무결성을 훼손하게 될 수 있으니 주의해야함.</p>
<ul>
<li>참고: 스프링프레임워크 사용시 JPA와 마이바티스를 쉽게 통합 가능함. 또한 스프링프레임워크의 AOP를 적절히 활용해 JPA를 우회해 데이터베이스에 접근하는 메소드를 호출할 때마다 영속성 컨텍스트를 플러시하면 위에서 언급한 문제도 해결 가능.</li>
</ul>
<hr>
<h2 id="102-jpql">10.2 JPQL</h2>
<blockquote>
<h4 id="jpql의-특징-1">JPQL의 특징</h4>
</blockquote>
<ul>
<li>객체지향 쿼리 언어. 즉, 테이블 대상으로 쿼리하는 것이 아니라 <strong>엔티티 객체</strong>를 대상으로 쿼리</li>
<li>JPQL은 SQL을 추상화하여 특정 데이터베이스 SQL에 의존하지 않음.</li>
<li>JPQL은 결국 SQL로 변환됨.</li>
</ul>
<h3 id="1-기본-문법과-쿼리-api">(1) 기본 문법과 쿼리 API</h3>
<p>JPQL도 SQL과 비슷하게 SELECT, UPDATE, DELETE 문을 사용할 수 있음. 엔티티 저장 시에는 EntityManager.persist() 메소드를 사용하면 되기에 INSERT문은 없음.</p>
<pre><code>select_문 ::=
    select_절
    from_절
    [where_절]
    [groupby_절]
    [having_절]
    [orderby_절]
update_문 :: = update_절 [where_절]
delete_문 :: = delete_절 [where_절]</code></pre><ul>
<li>JPQL에서 UPDATE, DELETE문은 벌크 연산이라고 함.</li>
</ul>
<h4 id="select문">SELECT문</h4>
<pre><code class="language-java">SELECT m FROM Meber AS m WHERE m.username = &quot;Hello&quot;</code></pre>
<ul>
<li>대소문자 구분:
엔티티 속성은 대소문자를 구분. 반면 JPQL 키워드(SELECT, FROM, AS)는 대소문자를 구분하지 않음.</li>
<li>엔티티 이름: 
JPQL에서 사용한 Member는 클래스명이 아닌 <strong>엔티티명</strong>이다. 엔티티명의 경우 <code>@Entity(name = &quot;XXX&quot;);</code>를 통해 설정할 수 있음. 그러지 않을 경우 클래스명을 기본값으로 사용. 기본값을 사용하는 것이 추천됨.</li>
<li><strong>별칭은 필수</strong>
<code>Member AS m</code>을 살펴보면 Member에 m이라는 별칭을 부여했는데 JPQL의 경우에는 별칭이 필수적임. 별칭이 없으면 오류 발생.</li>
</ul>
<h4 id="typequery-query">TypeQuery, Query</h4>
<p>작성한 JPQL의 실행을 위해서는 쿼리 객체가 필요하다. 이때 쿼리 객체로는 <strong><code>TypeQuery</code></strong>와 <strong><code>Query</code></strong>가 존재함. 반환 타입을 명확히 지정할 수 있을 때, TypeQuery 객체를 사용하고 그렇지 않을 때는 Query 객체를 사용하면 된다.</p>
<p>예)</p>
<pre><code class="language-java">TypedQuery&lt;Member&gt; query = em.createQuery(&quot;SELECT m FROM Member m&quot;, Member.class);

List&lt;Member&gt; resultList = query.getResultList();
for (Member member: resultList) {
    System.out.println(&quot;member = &quot; + member);
}</code></pre>
<p><code>em.createQuery()</code>의 두 번째 파라미터에 반환할 타입을 지정할 때는 TypeQuery를, 그렇지 않을 때에는 Query를 반환함. 조회 대상이 Member 엔티티이기에 대상 타입이 명확한데 이럴 때에는 TypeQuery를 사용할 수 있는 것이다.</p>
<pre><code class="language-java">Query query = em.createQuery(&quot;SELECT m.username, m.age FROM Member m&quot;);
List resultList = query.getResultList();

for (Object o: resultList) {
    Object [] result = (Object[]) o;
    System.out.println(&quot;username = &quot;+result[0]);
    System.out.println(&quot;age = &quot; + result[1]);
}</code></pre>
<p>위의 경우에는 조회대상이 String 타입과 Integer 타입으로 조회 대상 타입이 명확하지 않다. SELECT 절에서 여러 엔티티나 컬럼을 선택하는 경우에는 <strong>Query 객체</strong>를 사용해야함.</p>
<p>Query 객체는 SELECT절의 조회 대상이 예제처럼 둘 이상이면 Object[]를 반환, 하나면 Object를 반환함.</p>
<h4 id="결과-조회">결과 조회</h4>
<p>아래 메소드를 호출해 실제 쿼리를 실행 및 데이터베이스를 조회하게된다.</p>
<ul>
<li><p><code>query.getResultList()</code>: 결과를 예제로 반환. 결과가  없으면 빈 컬렉션을 반환.</p>
</li>
<li><p><code>query.getSingleResult()</code>: 결과가 정확히 하나일 때 사용.</p>
<ul>
<li>결과가 없으면 <code>..NoResultException</code>예외 발생</li>
<li>결과가 1개보다 많으면 <code>..NonUniqueResultException</code>예외 발생</li>
</ul>
</li>
</ul>
<p>이때 <code>getSingleResult()</code>는 결과가 정확히 1개가 아니면 예외를 발생함.</p>
<h3 id="2-파라미터-바인딩">(2) 파라미터 바인딩</h3>
<p>JDBC는 위치 기준 파라미터 바인딩만 지원하지만 JPQL은 이름 기준 파라미터 바인딩 역시 지원함.</p>
<h4 id="이름-기준-파라미터">이름 기준 파라미터</h4>
<p>이름 기준 파라미터: 파라미터를 <strong>이름으로 구분</strong>하는 방법. 이름 기준 파라미터는 <strong>앞에 :를 사용</strong>.</p>
<p><code>:username</code>이라는 이름 기준 파라미터를 정의하고 <code>query.setParameter()</code>에서 <code>username</code>이라는 이름으로 파라미터를 바인딩.</p>
<p>JPQL API는 대부분 메소드 체인 방식으로 설계되어 있어 연속해서 작성할 수 있음.</p>
<pre><code class="language-java">List&lt;Member&gt; members = 
em. createQuery(&quot;SELECT m FROM Member m where m.username = :username, Member.class)
    .setParameter(&quot;username&quot;, usernameParam)
    .getResult();</code></pre>
<h4 id="위치-기준-파라미터">위치 기준 파라미터</h4>
<p>위치 기준 파라미터를 사용하기 위해서는 <code>?</code> 다음에 위치값을 주면 된다. 위치 값은 단 1부터 시작한다.</p>
<pre><code class="language-java">List&lt;Member&gt; members =
    em.createQuery(&quot;SELECT m FROM Member m where m.username = ?1&quot;, Member.class)
        .setParameter(1, usernameParam)
        .getResultList();</code></pre>
<p>위치 기준 파라미터는 <strong>방식보다는 이름 기준 파라미터 바인딩 방식을 사용하는 것이 더 명확하다</strong>.</p>
<h3 id="3-프로젝션">(3) 프로젝션</h3>
<p><strong>프로젝션</strong>: SELECT절에 조회할 대상을 지정하는 것.</p>
<blockquote>
<p><strong>SELECT</strong> {프로젝션 대상} <strong>FROM</strong></p>
</blockquote>
<h4 id="엔티티-프로젝션">엔티티 프로젝션</h4>
<pre><code class="language-java">SELECT m FROM Member m //회원
SELECT m.team FROM Member m //팀</code></pre>
<p>처음은 회원을 조회, 두 번째에는 회원과 연관된 팀을 조회하는데 둘 다 엔티티를 프로젝션 대상으로 사용함. 쉽게 생각하면 원하는 객체를 바로 조회한 것인데 컬럼을 하나하나 나열해서 조회해야하는 SQL과는 차이가 존재한다.</p>
<p>이렇게 <strong>조회한 엔티티는 영속성 컨텍스트에서 관리</strong>된다.</p>
<h4 id="임베디드-타입-프로젝션">임베디드 타입 프로젝션</h4>
<p>JPQL에서 임베디드 타입은 엔티티와 거의 비슷하게 사용됨. 임베디드 타입은 조회의 시작점이 될 수 없다는 제약이 존재.</p>
<pre><code class="language-java">String query = &quot;SELECT a FROM Address a&quot;;</code></pre>
<p>따라서 위의 코드는 잘못된 쿼리이며 아래처럼 사용해야함.</p>
<pre><code class="language-java">String query = &quot;SELECT o.address FROM Order o&quot;;
List&lt;Address&gt; address = em.createQuery(query, Address.class).getResultList();</code></pre>
<p><strong>임베디드 타입은 엔티티 타입이 아닌 값 타입. 이렇게 직접 조회한 임베디드 타입은 영속성 컨텍스트에서 관리되지 않음.</strong></p>
<h4 id="스칼라-타입-프로젝션">스칼라 타입 프로젝션</h4>
<p>스칼라 타입: 숫자, 문자, 날짜와 같은 기본 데이터 타입.</p>
<pre><code class="language-java">List&lt;String&gt; username = 
    em.createQuery(&quot;SELECT username FROM Member m&quot;, String.class)
    .getResultList();</code></pre>
<p>중복 데이터 제거 시에는 DISTINCT를 사용.</p>
<pre><code class="language-java">SELECT DISTINCT username FROM Member m</code></pre>
<p>다음과 같은 통계 쿼리도 주로 스칼라  타입으로 조회한다.</p>
<pre><code class="language-java">Double orderAmountAvg = em.createQuery(&quot;SELECT AVG(o.orderAmount) FROM Order o&quot;, Double.class)
    . getSingleResult();</code></pre>
<h4 id="여러-값-조회">여러 값 조회</h4>
<p>엔티티를 대상으로 조회하면 편리하겠지만, 꼭 필요한 데이터만 선택해 조회해야할 때가 존재함. 이럴 때에는 TypeQuery는 사용할 수 없기에 Query를 사용해야함.</p>
<pre><code class="language-java">Query query =
    em.createQuery(&quot;SELECT m.username, m.age FROM Member m&quot;);
List resultList = query.getResultList();

Iterator iterator = resultList.iterator();
while (iterator.hasNext()) {
    Object[] row = (Object[]) iterator.next();
    String username = (String) row[0];
    Integer age = (Integer) row[1];
}</code></pre>
<p>제너릭에 Obejct[]를 사용하면 아래처럼 더 간결하게 개발할 수 있다.</p>
<pre><code class="language-java">List&lt;Object[]&gt; resultList =
    em.createQuery(&quot;SELECT m.username, m.age FROM Member m&quot;)
    .getResult();

for (Object[] row : resultList) {
    String username = (String) row[0]
}</code></pre>
<p>스칼라 뿐만 아니라 엔티티 타입 역시 여러 값을 함께 조회할 수 있다.</p>
<h4 id="new-명령어">NEW 명령어</h4>
<p>앞에서는 여러 필드를 프로젝션할 경우 타입 지정이 불가해 TypeQuery를 사용하지 못해 Object[]를 반환받았지만, 실제 애플리케이션에서는 Object[]를 직접 사용하지 않고 UserDTO와 같은 객체로 변환해 사용하게 된다.</p>
<pre><code class="language-java">List&lt;UserDTO&gt; userDTOs = new ArrayList&lt;&gt;();
for (Object[] row:resultList) {
    UserDTO userDTO = new UserDTO((String) row[0], (Integer) row[1]);
    userDTOs.add(userDTO);
}

return userDTOs;

public class UserDTO {
    private String username;
    private int age;

    public UserDTO (String username, int age) {
        this.username = username;
        this.age = age;
    }
}</code></pre>
<p>NEW 명령어를 사용하는 예)</p>
<pre><code class="language-java">TypedQuery&lt;UserDTO&gt; query = 
    em.createQuery(&quot;SELECT new jpabook.jpql.UserDTO(m.username, m.age)
                    FROM Member m&quot;, UserDTO.class);
    List&lt;UserDTO&gt; resultList = query.getResultList();</code></pre>
<p>SELECT 이후에 NEW 명령어를 사용 시 반환받을 클래스를 지정할 수 있는데, 이 클래스의 생성자에 JPQL 조회 결과를 넘겨줄 수 있다. 또한 NEW 명령어를 사용해 클래스로 TypeQuery를 사용할 수 있어 지루한 객체 변환 작업을 줄일 수 있음.</p>
<p><strong>주의사항</strong></p>
<ul>
<li>패키지명을 포함한 전체 클래스명을 입력해야함</li>
<li>순서와 타입이 일치하는 생성자가 필요.</li>
</ul>
<h3 id="4-페이징-api">(4) 페이징 API</h3>
<p>페이지 처리용 SQL을 작성하는 것은 귀찮기도 하고 데이터베이스마다 이 페이징 처리 문법이 다르다는 문제가 존재함.</p>
<ul>
<li><code>setFirstResult(int startPosition)</code>: 조회 시작 위치(0부터 시작)</li>
<li><code>setMaxResult(int maxResult)</code>: 조회할 데이터 수</li>
</ul>
<pre><code class="language-java">TypedQuery&lt;Member&gt; query = 
    em.createQuery(&quot;SELECT m FROM Member m ORDER BY m.username DESC&quot;, Member.class);

query.setFirstResult(10);
query.setMaxResults(20);
query.getResultList();</code></pre>
<p>데이터베이스마다 다른 페이징 처리를 같은 API로 처리할 수 있는 것은 데이터베이스 방언 덕분이다. </p>
<h4 id="mysql의-페이징">MySQL의 페이징</h4>
<pre><code class="language-sql">SELECT
    M.ID AS ID,
    M.AGE AS AGE,
    M.TEAM_ID AS TEAM_ID, 
    M.NAME AS NAME
FROM MEMBER M
ORDER BY M.NAME DESC LIMIT ?,?</code></pre>
<p>데이터베이스마다 SQL이 다르고, 오라클과 SQLServer의 경우에는 페이징 쿼리를 따로 공부해야 작성할 수 있을 정도로 복잡함. ?에 바인딩하는 값 역시도 데이터베이스마다 다르며 적절한 값을 입력해야함.</p>
<h3 id="5-집합과-정렬">(5) 집합과 정렬</h3>
<p>집합 =&gt; 집합함수와 함께 통계 정보를 구할 때 사용.</p>
<h4 id="집계함수">집계함수</h4>
<ul>
<li>COUNT: 결과의 수를 구함. 반환은 Long 타입.</li>
<li>MAX, MIN: 최대, 최소 값을 구함. 문자, 숫자, 날짜 등에 사용.</li>
<li>AVG: 평균값을 구함. 숫자타입만 사용할 수 있음. 반환은 Double 타입.</li>
<li>SUM: 합을 구함. 숫자타입만 사용 가능하면 반환은 정수합, 소수합.. 등.</li>
</ul>
<h4 id="집계함수-사용-시-참고사항">집계함수 사용 시 참고사항</h4>
<ul>
<li>널(NULL)값은 무시하기에 통계에 잡히지 않음. (DISTINCT 정의가 되어있어도)</li>
<li>값이 없는데 SUM, AVG, MAX, MIN을 사용하면 널값을 반환. COUNT의 경우에는 0.</li>
<li>DISTINCT를 집계함수 안에 사용하면 중복값을 제거하고 집합을 구할 수 있음.</li>
<li>DISTINCT를 COUNT에서 사용할 때 임베디드 타입은 지원x</li>
</ul>
<h4 id="group-by-having">GROUP BY, HAVING</h4>
<ul>
<li>GROUP BY: 통계 데이터를 구할 때 특정 그룹끼리 묶어줌.</li>
<li>HAVING: GROUP BY로 그룹화한 통계 데이터를 기준으로 필터링.</li>
</ul>
<p>이런 쿼리들을 리포팅 쿼리/통계 쿼리라함. 결과가 많을 때에는 통계 결과만 저장하는 테이블을 별도로 만들어 두고 사용자가 적은 새벽에 통계 쿼리를 실행해 결과를 보관하는 것이 좋음.</p>
<h4 id="정렬-order-by">정렬 (ORDER BY)</h4>
<p>ORDER BY: 결과를 정렬할 때 사용. 내림차순(DESC) 또는 오름차순(ASC) 선택.</p>
<h3 id="6-jpql-조인">(6) JPQL 조인</h3>
<p>JPQL의 조인은 SQL과 기능은 같고 문법만 약간 다르다.</p>
<h4 id="내부조인">내부조인</h4>
<p>내부조인은 <code>INNER JOIN</code>을 사용. INNER는 생략이 가능하다.</p>
<pre><code class="language-java">String teamName = &quot;팀A&quot;;
String query = &quot;SELECT m FROM Member m INNER JOIN m.team t &quot; + &quot;WHERE t.name = :teamName&quot;;
List&lt;Member&gt; members = em.createQuery(query, Member.class)
    .setParameter(&quot;teamName&quot;. teamName);
    .getResultList();</code></pre>
<p>회원과 팀 내부 조인을 통해 팀A에 소속된 회원을 조회하는 JPQL 예)</p>
<pre><code class="language-java">SELECT m
FROM Member m INNER JOIN m.team t
where t.name = :teamName</code></pre>
<p>JPQL 조인의 특징</p>
<ul>
<li>연관 필드를 사용함: <code>m.team</code>이 예로, 연관 필드는 <strong>다른 엔티티와 연관관계를 가지기 위해 사용하는 필드를 말함.</strong></li>
<li>FROM Member m: 회원을 선택하고 m이라는 별칭 부여</li>
<li>Member m JOIN m.team t: 회원이 가지고 있는 연관 필드로 팀과 조인. 조인한 팀에 t라는 별칭 부여.</li>
</ul>
<p>JPQL 조인을 SQL 조인처럼 사용하면 문법 오류 발생. <strong>JPQL은 JOIN 명령어 다음 조인할 객체의 연관 필드를 사용</strong>.</p>
<h4 id="외부조인">외부조인</h4>
<pre><code class="language-java">SELECT m
FROM Member m LEFT [OUTER] JOIN m.team t</code></pre>
<h4 id="컬렉션-조인">컬렉션 조인</h4>
<p>일대다 관계 또는 다대다관계처럼 컬렉션을 사용하는 곳에 조인하는 것을 <strong>컬렉션 조인</strong>이라함.</p>
<ul>
<li>[회원 -&gt; 팀]으로의 조인: 다대일 조인이면서 단일값 연관 필드를 사용.</li>
<li>[팀 -&gt; 회원]으로의 조인: 일대다 조인이면서 컬렉션값 연관 필드를 사용.</li>
</ul>
<pre><code class="language-java">SELECT t,m FROM Team t LEFT JOIN t.members m</code></pre>
<p><code>t LEFT JOIN t.members</code>는 팀이 보유한 회원목록을 <strong>컬렉션 값 연관 필드로 외부조인</strong></p>
<h4 id="세타-조인">세타 조인</h4>
<p>WHERE절을 사용해 세타 조인이 가능함. <strong>세타 조인은 내부 조인만을 지원</strong>. 세타 조인 사용시에는 전혀 관계없는 엔티티도 조인할 수 있음.</p>
<pre><code class="language-java">select count(m) from Member m, Team t
where m.username = t.name</code></pre>
<h4 id="join-on">JOIN ON</h4>
<p>조인할 때 ON절을 지원. ON절 사용 시에는 조인 대상을 필터링하고 조인할 수 있음.</p>
<ul>
<li>참고: 내부조인의 ON 절은 WHERE절을 사용할 때와 결과가 같기에 보통 ON절은 외부 조인에서만 사용함.</li>
</ul>
<pre><code class="language-java">select m, t from Member m
left join m.team t on t.name = &#39;A&#39;</code></pre>
<p><code>t.name = &#39;A&#39;</code>로 조인시점에 조인 대상을 필터링함.</p>
<h4 id="페치조인">페치조인</h4>
<p>페치(fetch)조인: SQL의 조인의 종류가 아닌 JPQL에서 성능 최적화를 위해 제공하는 기능. 연관된 엔티티나 컬렉션을 한 번에 같이 조회하는 기능으로 join fetch 명령어를 통해 사용 가능.</p>
<ul>
<li>엔티티 페치조인<pre><code class="language-java">select m 
from Member m join fetch m.team</code></pre>
</li>
</ul>
<p>join 뒤에 fetch: 연관된 엔티티나 컬렉션을 함께 조회. (여기서는 회원과 팀을 함께 조회). JPQL 조인과 다라ㅡ게 m.team 뒤에 별칭이 없는데 <strong>페치 조인은 별칭을 사용할 수 없음.</strong></p>
<p>엔티티 페치 조인 JPQL에서 select m을 통해 회원 엔티티만 선택하였지만, <strong>실행된 SQL에 따르면 회원과 연관된 팀도 함께 조회</strong>된 것을 볼 수 있음.</p>
<pre><code class="language-java">String jpql = &quot;select m from Member m join fetch m.team&quot;;

List&lt;Member&gt; members = em.createQuery(jpql, Member.class)
    .getResultList();

for (Member member:members) {
    System.out.println(&quot;username = &quot;+member.getUsername() + &quot;,&quot; + 
    &quot;teamname = &quot; +member.getTeam().name());
}</code></pre>
<p>회원을 조회할 때 페치 조인을 사용해 팀도 함께 조회했기에 <strong>연관 팀은 프록시가 아닌 실제 엔티티</strong>. 따라서 연관된 팀을 사용해도 지연로딩이 일어나지 않음. 또한, 실제 엔티티이기에 회원 엔티티가 영속성 컨텍스트에서 분리되어 준영속 상태가 되어도 연관된 팀을 조회할 수 있음.</p>
<h4 id="컬렉션-페치-조인">컬렉션 페치 조인</h4>
<p>일대다 관계인 컬렉션을 페치 조인</p>
<pre><code class="language-java">select t
from Team t join fetch t.members
where t.name = &#39;팀A&#39;</code></pre>
<p>팀을 조회하면서 페치 조인을 사용해 연관된 회원 컬렉션(t.members)도 함께 조회.</p>
<p>컬렉션 페치 조인한 JPQL에서 select t로 팀만 선택했는데 실행 SQL을 보면 팀과 연관된 회원 역시 함께 조회함.  또한 TEAM 테이블에서 팀A는 하나지만 회원 테이블과 조인하면서 결과가 증가하여 팀A가 2건 조회됨.</p>
<h4 id="페치-조인과-distinct">페치 조인과 DISTINCT</h4>
<ul>
<li>SQL의 DISTINCT는 중복된 결과를 제거하는 명령. JPQL의 DISTINCT 명령어는 SQL에 DISTINCT를 추가하는 것은 물론, 애플리케이션에서 한 번 더 중복을 제거.<pre><code class="language-java">select distinct t
from Team t join fetch t.members
where t.name = &#39;팀A&#39;</code></pre>
</li>
</ul>
<h4 id="페치-조인과-일반-조인의-차이">페치 조인과 일반 조인의 차이</h4>
<p>JPQL은 <strong>결과를 반환할 때 연관관계까지 고려하지 않음.</strong> 팀과 회원을 조인한 후에 팀을 조회한다고 해도, 회원 컬렉션은 조회하지 않음. 즉, 프록시나 아직 초기화하지 않은 컬렉션 래퍼를 반환함.</p>
<p>반면, 페치 조인은 <strong>연관된 엔티티까지 함께 조회</strong>함.</p>
<h4 id="페치-조인의-특징과-한계">페치 조인의 특징과 한계</h4>
<p>페치 조인을 사용할 때에는 SQL 한 번으로 연관된 엔티티들을 함께 조회해 SQL 호출 횟수를 줄여 성능을 최적화할 수 있음.</p>
<p><strong>글로벌 로딩 전략</strong>: 엔티티에 직접 적용하는 로딩 전략은 애플리케이션 전체에 영향을 미치기에 글로벌 로딩 전략이라 부름. 글로벌 로딩 전략을 지연로딩으로 설정해도 JPQL에서 페치 조인을 사용하면 페치조인을 적용해 함께 조회함.</p>
<blockquote>
<p>최적화를 위해 <strong>글로벌 로딩 전략을 즉시 로딩으로 설정</strong>하면 애플리케이션 전체에서 항상 <strong>즉시 로딩</strong>이 발생함. 일부 빠를 수는 있지만, 전체적으로 볼 때, 사용하지 않은 엔티티를 자주 로딩하기에 성능에 <strong>악영향을 미칠 수 있음.</strong> 
따라서 <strong>글로벌 로딩 전략은 될 수 있으면 지연 로딩을 사용하고 최적화가 필요할 때 페치 조인을 하는 것이 효과적</strong>.</p>
</blockquote>
<ul>
<li><p>페치 조인의 한계</p>
<ul>
<li><p>페치 조인 대상에는 별칭을 줄 수 없음.</p>
</li>
<li><p>둘 이상의 컬렉션을 페치할 수는 없음. 구현체에 따라 가능할 때도 있지만, 이때 카티시안 곱이 만들어지기도 하기에 주의해야함.</p>
</li>
<li><p>컬렉션을 페치 조인하면 페이징 API를 사용할 수 없음.</p>
<ul>
<li>컬렉션(일대다)이 아닌 단일값 연관필드들은 페치 조인을 해도 페이징 API 사용 불가.</li>
<li>하이버네이트에서 컬렉션을 페치 조인하고 페이징 API를 사용하면 경고 로그를 남기며 메모리에서 페이징 처리를 함. 데이터가 적으면 상관없지만, 데이터가 많으면 성능 이슈와 메모리 초과 예외가 발생할 수 있어 위험함.</li>
</ul>
</li>
</ul>
</li>
</ul>
<p>=&gt; 페치 조인은 성능 최적화에 유용해 실무에서 자주 사용한다. 객체 그래프를 유지할 때 특히 효과적이지만, 여러 테이블을 조인해 엔티티가 가진 모양이 전혀 다른 결과를 내야할 때는 억지로 페치 조인을 사용하기 보다는 여러 테이블에서 필요한 필드들만 조회해 DTO로 반환하는 것이 더 효과적일 수 있음.</p>
<h3 id="8-경로-표현식">(8) 경로 표현식</h3>
<p>경로표현식: .(점)을 찍어 객체 그래프를 탐색하는 것을 말함.</p>
<h4 id="용어-정리">용어 정리</h4>
<ul>
<li><strong>상태 필드</strong>: 단순히 값을 저장하기 위한 필드 (필드 또는 프로퍼티)</li>
<li><strong>연관 필드</strong>: 연관관계를 위한 필드. 임베디드 타입 포함 (필드 또는 프로퍼티)</li>
<li>단일 값 연관 필드: <code>@ManyToOne</code>, <code>@OneToOne</code> 대상 엔티티</li>
<li>컬렉션 값 연관 필드: <code>@OneToMany</code>, <code>@ManyToMany</code></li>
</ul>
<pre><code class="language-java">@Entity
public class Member {
    @Id @GeneratedValue
    private Long id; 

    @Column (name = &quot;name&quot;)
    private String username; //상태필드
    private Integer age; //상태필드

    @ManyToOne(...)
    private Team team; //연관필드

    @OneToMany(...)
    private List&lt;Order&gt; orders; //연관필드
}</code></pre>
<h4 id="경로표현식과-특징">경로표현식과 특징</h4>
<p>JPQL에서 경로 표현식을 사용해 경로 탐색을 하기 위해서는 아래 3가지 경로에 따라 어떤 특징이 있는지 이해해야함.</p>
<ul>
<li><p><strong>상태 필드 경로</strong>: 경로 탐색의 끝으로 더는 탐색이 불가능함.</p>
</li>
<li><p><strong>단일 값 연관 경로</strong>: 묵시적으로 내부 조인이 발생. 단일 값 연관 경로는 계속 탐색 가능.</p>
</li>
<li><p><strong>컬렉션 값 연관 경로</strong>: 묵시적으로 내부 조인이 발생하며 더는 탐색할 수 없다. FROM절에서 조인을 통해 별칭을 얻는 경우에는 별칭으로 탐색이 가능하다.</p>
</li>
<li><p>상태 필드 경로 탐색
<code>select m.username, m.age from Member m</code></p>
</li>
<li><p>단일 값 연관 경로 탐색
<code>select o.member from Order o</code></p>
</li>
</ul>
<p><strong>묵시적 조인</strong>: 단일 값 연관 필드로 경로 탐색을 하면 SQL에서 내부 조인이 일어나는 것을 이르는 말. 묵시적 조인은 모두 내부 조인.</p>
<ul>
<li><p>명시적 조인: JOIN을 직접 적어주는 것
<code>SELECT m FROM Member m JOIN m.team t</code></p>
</li>
<li><p>묵시적 조인: <strong>경로 표현식에 의해</strong> 묵시적으로 조인이 일어나는 것. 내부 조인만 가능하다.
<code>SELECT m.team FROM Member m</code></p>
</li>
<li><p>컬렉션 값 연관 경로 탐색
JPQL을 다루면서 가장 많이 하는 실수. <code>t.members</code>처럼 <strong>컬렉션까지는 경로탐색이 가능</strong>하지만, <code>t.members.username</code>처럼 컬렉션에서 경로 탐색을 시작하는 것은 허락하지 않음.
컬렉션에서 경로 탐색을 하려면 <strong>조인을 사용해 새로운 별칭을 획득해야함</strong></p>
</li>
</ul>
<h4 id="경로-탐색을-사용한-묵시적-조인-시-주의사항">경로 탐색을 사용한 묵시적 조인 시 주의사항</h4>
<p>경로 탐색 사용 시 묵시적 조인이 발생해 SQL에서 내부 조인이 발생할 수 있음.
주의사항:</p>
<ul>
<li>항상 <strong>내부조인</strong>이다.</li>
<li>컬렉션은 경로 탐색의 끝. 컬렉션에서 경로 탐색을 하려면 명시적으로 조인해 별칭을 얻어야함.</li>
<li>경로 탐색은 주로 SELECT, WHERE 절에서 사용하지만 묵시적 조인으로 인해 SQL의 FROM절에 영향을 줌.</li>
</ul>
<p>=&gt; 성능이 중요할 때는 분석이 쉽도록 묵시적 조인보다 명시적 조인을 사용하는 것이 낫다.</p>
<h3 id="9-서브쿼리">(9) 서브쿼리</h3>
<p>JPQL에서도 서브쿼리를 지원해준다. 하지만 제약이 존재한다.
서브쿼리를 WHERE, HAVING 절에서만 사용할 수 있고, SELECT, FROM 절에서는 사용이 불가하다.</p>
<h4 id="서브쿼리-함수">서브쿼리 함수</h4>
<ul>
<li><p>[NOT] EXISTS: 서브쿼리에 결과가 존재(또는 존재하지 않으면) 참.</p>
</li>
<li><p>{ALL | ANY | SOME}: 비교 연산자와 함께 사용하며</p>
<ul>
<li>ALL: 조건을 모두 만족하면 참</li>
<li>ANY 또는 SOME: 같은 의미로 조건을 하나라도 만족하면 참.</li>
</ul>
</li>
<li><p>[NOT] IN: 서브 쿼리의 결과 중 하나라도 같은 것이 있으면 참. IN은 서브쿼리가 아닌 곳에서도 사용 가능.</p>
</li>
</ul>
<h3 id="10-조건식">(10) 조건식</h3>
<h4 id="타입표현">타입표현</h4>
<ul>
<li>문자: 작은 따옴표 사이에 표현. 작은 따옴표 표현을 위해서는 작은 따옴표를 두 번 사용.</li>
<li>숫자: L(Long), D(Double), F(Float) 지정.</li>
<li>날짜: DATE{d &#39;yyyy-mm-dd&#39;} TIME{t &#39;hh-mm-ss&#39;}</li>
<li>Boolean: TRUE, FALSE</li>
<li>Enum: 패키지명 포함한 전체 이름 예) jpabook.MemberType.Admin</li>
<li>엔티티 타입: 엔티티 타입을 표현. 예) TYPE(m) = Member</li>
</ul>
<h4 id="연산자-우선순위">연산자 우선순위</h4>
<ol>
<li>경로 탐색 연산: (.)</li>
<li>수학 연산: +, -, *, /,</li>
<li>비교 연산: =, &gt;=, &gt;, &lt;=, &lt;</li>
<li>논리 연산: AND, NOT, OR</li>
</ol>
<h4 id="컬렉션-식">컬렉션 식</h4>
<p>컬렉션에서만 사용하는 특별한 기능.</p>
<ul>
<li>빈 컬렉션 비교식
{컬렉션 연관 경로} IS [NOT] EMPTY</li>
</ul>
<p>컬렉션은 컬렉션 식만 사용할 수 있음. </p>
<ul>
<li>컬렉션 멤버 식
{엔티티/값} [NOT] MEMBER [OF] {컬렉션 값 연관 경로}</li>
</ul>
<h4 id="스칼라-식">스칼라 식</h4>
<p>스칼라는 숫자, 문자, 날짜, case, 엔티티 타입 같은 기본적인 타입을 말함.
수학식, 문자함수, 수학함수, 날짜함수 등을 사용할 수 있음.</p>
<h4 id="case-식">CASE 식</h4>
<p>특정 조건에 따라 분기할 때 사용하며 4가지가 존재함.</p>
<ul>
<li>기본 CASE</li>
<li>심플 CASE</li>
<li>COALESCE</li>
<li>NULLIF</li>
</ul>
<p><strong>기본 case</strong>:</p>
<pre><code class="language-sql">CASE
    {WHEN 조건식 THEN 스칼라식}
    ELSE 스칼라식
END</code></pre>
<p><strong>심플 case</strong>:</p>
<pre><code class="language-sql">CASE {조건대상}
    WHEN 스칼라식 THEN 스칼라식
    ELSE 스칼라식
END</code></pre>
<p><strong>COALESCE</strong>:</p>
<pre><code class="language-sql">    COALESCE(스칼라식, 스칼라식,...)</code></pre>
<p>스칼라식을 차례대로 조회해 null이 아니면 반환</p>
<p><strong>NULLIF</strong></p>
<pre><code class="language-sql">NULLIF (스칼라식, 스칼라식)</code></pre>
<p>두 값이 같으면 널을 반환하고, 다름녀 첫 번째 값을 반환 보통 집합함수와 함께 사용.</p>
<h3 id="11-다형성-쿼리">(11) 다형성 쿼리</h3>
<p>JPQL로 부모 엔티티 조회 시 그 자식 엔티티도 함께 조회함.</p>
<h4 id="type">TYPE</h4>
<p>TYPE은 엔티티의 상속 구조에서 조회 대상을 특정 자식 타입으로 한정할 때 주로 사용.</p>
<p>예)</p>
<pre><code class="language-java">select i from Item i
where type(i) IN (Book, Movie)</code></pre>
<p>이는 JPA 2.1에 추가된 기능인데 <strong>자바의 타입 캐스팅</strong>과 비슷하다. 상속 구조에서 부모 타입을 특정 자식 타입으로 다룰 때 사용한다. </p>
<h3 id="12-사용자-정의-함수-호출">(12) 사용자 정의 함수 호출</h3>
<p>JPA 2.1부터 사용자 정의 함수를 지원한다.
예)</p>
<pre><code class="language-java">function_invocation ::= FUNCTION (function_name {, function_arg}*)</code></pre>
<pre><code class="language-java">select function(&#39;group_concat&#39;, i.name) from Item i</code></pre>
<p>하이버네이트 구현체 사용시에는 방언 클래스를 사용해 구현하고 사용할 데이터베이스 함수를 미리 등록해야함.</p>
<h3 id="13-기타-정리">(13) 기타 정리</h3>
<ul>
<li>enum은 비교연산만 지원</li>
<li>임베디드 타입은 비교를 지원하지 않음</li>
</ul>
<h4 id="empty-string">EMPTY STRING</h4>
<p>JPA 표준은 &#39;&#39;을 길이가 0인 empty string으로 정했지만, 데이터베이스에 따라 &#39;&#39;를 null로 사용하기도 하니 확인하고 사용해야함.</p>
<h4 id="null">NULL</h4>
<ul>
<li>조건을 만족하는 데이터가 하나도 없을 때</li>
<li>NULL은 안ㄹ 수 없는 값으로 모든 수학적 계산의 결과가 NULL이 됨.</li>
<li>Null == Null은 알 수 없는 값.</li>
<li>Null is Null은 참.</li>
</ul>
<h3 id="14-엔티티-직접-사용">(14) 엔티티 직접 사용</h3>
<h4 id="기본키-값">기본키 값</h4>
<p>객체 인스턴스는 참조값으로 식별하고 테이블 로우는 기본키 값으로 식별.
따라서 JPQL에서 엔티티 객체를 직접 사용하면 SQL에서는 해당 엔티티의 기본키 값을 사용함.</p>
<pre><code class="language-java">select count(m.id) from Member m //엔티티 아이디를 사용
select count(m) from Member m //엔티티 직접 사용</code></pre>
<p>아래처럼 엔티티의 별칭을 직접 넘겨주게되면 JPQL이 SQL로 변환될 때, 해당 엔티티의 기본키를 사용한다.</p>
<h4 id="외래키-값">외래키 값</h4>
<p>특정 팀에 소속된 회원을 찾기위해 팀의 기본키 값을 파라미터로 하여 탐색을 하면, <code>m.team</code>(멤버의 팀) 부분이 외래키와 매핑되어있기에 회원과 팀 사이에 묵시적 조인 없이 데이터를 가져올 수 있다. </p>
<h3 id="15-named-쿼리-정적-쿼리">(15) Named 쿼리: 정적 쿼리</h3>
<p>JPQL 쿼리는 크게 동적 쿼리와 정적 쿼리로 나눌 수 있음.</p>
<ul>
<li><strong>동적 쿼리</strong>: em.createQuery(&quot;select ...&quot;)처럼 JPQL을 문자로 완성해 직접 넘기는 것을 동적 쿼리라함.</li>
<li><strong>정적 쿼리</strong>: 미리 정의한 쿼리에 이름을 부여해 필요할 때 사용할 수 있는데 이것을 Named 쿼리라함. 이는 한 번 정의하면 변경할 수 없는 정적인 쿼리.</li>
</ul>
<p>Named 쿼리는 애플리케이션 로딩 시점에 JPQL 문법을 체크하고 미리 파싱해둔다. 따라서 오류를 빨리 확인할 수 있고, 사용 시점에 파싱된 결과를 재사용하므로 성능상의 이점도 존재한다. 또한 Named 쿼리는 변하지 않는 정적 SQL이 생성되므로 데이터베이스 조회 성능 최적화에도 도움이 됨.</p>
<p>Named 쿼리는 <code>@NamedQuery</code> 어노테이션을 사용해 자바 코드에 작성하거나 XML 문서에 작성할 수 있음.</p>
<h4 id="named-쿼리를-어노테이션에-정의">Named 쿼리를 어노테이션에 정의</h4>
<p>Named 쿼리는 <strong>쿼리에 이름을 부여해 사용하는 방법</strong>. </p>
<pre><code class="language-java">@Entity
@NamedQuery {
    name = &quot;Member.findByUsername&quot;,
    query = &quot;select m from Member m where m.username = :username&quot;)
} public class Member {

}</code></pre>
<p><code>@NamedQuery.name</code>에 쿼리 이름을 부여하고, <code>@NamedQuery.query</code>에 사용할 쿼리를 입력.</p>
<p>하나의 엔티티에 2개 이상의 Named 쿼리를 정의하려면 <code>@NamedQueries</code> 어노테이션을 사용하면 됨.</p>
<ul>
<li><p><code>@NamedQuery</code> 어노테이션</p>
<ul>
<li>lockMode: 쿼리 실행 시 락을 건다.</li>
<li>hints: JPA 구현체에게 제공하는 힌트로 2차 캐시를 다룰 때 사용하거나 한다.</li>
</ul>
</li>
</ul>
<h4 id="named-쿼리를-xml에-정의">Named 쿼리를 XML에 정의</h4>
<p>JPA에서 어노테이션으로 작성할 수 있는 것은 XML로도 작성할 수 있음. 어노테이션을 사용하는 것이 직관적이고 편리한편이지만, Named 쿼리 작성 시에는 XML을 사용하는 것이 직관적이고 편리함.</p>
<p>xml에 정의한 후에는 MEAT-INF/persistence.xml에 코드를 추가해 주어야함.</p>
<pre><code class="language-java">&lt;persistence-unit name=&quot;jpabook&quot;&gt;
    &lt;mapping-file&gt;META-INF/ormMember.xml&lt;/mappingfile&gt;</code></pre>
<hr>
<h2 id="103-criteria">10.3 Criteria</h2>
<p>Criteria 쿼리는 JPQL을 자바 코드로 작성하도록 도와주는 빌더 클래스 API. 이를 사용하면 문자가 아닌 코드로 JPQL을 작성하기 때문에 문법 오류를 <strong>컴파일 단계에서</strong> 잡을 수 있고 , 문자 기반의 JPQL보다 동적 쿼리를 안전하게 생성한다는 장점이 존재. 다만 코드가 복잡하고 장황해 이해가 힘들다는 단점 역시 존재한다.</p>
<h3 id="1-criteria-기초">(1) Criteria 기초</h3>
<p>예)</p>
<pre><code class="language-java">//JPQL: select m from Member m

CriteriaBuilder cb = em.getCriteriaBuilder(); //Criteria 쿼리 빌더
CriteriaQuery&lt;Member&gt; cq =cb.createQuery(Member.class)

Root&lt;Member&gt; m = cq.from(Member.class);
cq.select(m);

TypedQuery&lt;Member&gt; query = em.createQuery(cq);
List&lt;Member&gt; members = query.getResultList();</code></pre>
<p>모든 회원 엔티티를 조회하는 JPQL의 Criteria로 작성.</p>
<ul>
<li>Criteria 쿼리 생성을 위해서는 Criteria 빌더가 필요. 빌더는 EntityManger 또는 EntityManagerFactory에서 얻을 수 있음.</li>
<li>Criteria 쿼리 빌더에서 Criteria 쿼리르 새엇ㅇ. 이때 <strong>반환 타입 지정 가능</strong>.</li>
<li>FROM 절을 생성. 반환된 값은 Criteria에서 사용하는 특별한 별칭. m을 조회의 시작이라는 의미로 <strong>쿼리 루트</strong>라함.</li>
<li>SELECT 절을 생성.</li>
</ul>
<p>아래는 WHERE절과 ORDER BY를 넣어주는 경우.</p>
<pre><code class="language-java">//JPQL : select m from Member m
//         where m.username = &#39;회원1&#39;
//         order by m.age desc

CriteriaBuilder cb = em.getCriteriaBuilder(); //Criteria 쿼리 빌더 생성

//Criteria 생성, 반환 타입 지정
CriteriaQuery&lt;Member&gt; cq = cb.createQuery(Member.class);

Root&lt;Member&gt; m = cq.from(Member.class); //FROM절-쿼리루트 반환

//검색 조건 정의
Predicate usernameEqual = cb.equal(m.get(&quot;username&quot;), &quot;회원1&quot;);

//정렬 조건 정의
javax.persistence.criteria.Order ageDesc = cb.desc(m.get(&quot;age&quot;));

//쿼리 생성
cq.select(m) //SELECT절
  .where(usernameEqual) //WHERE절 생성
  .orderBy(ageDesc); //ORDER BY절 생성


TypedQuery&lt;Member&gt; query = em.createQuery(cq);
List&lt;Member&gt; members = query.getResultList();</code></pre>
<p><strong>쿼리 루트와 별칭</strong></p>
<pre><code> **쿼리 루트와 별칭**
- Root&lt;Member&gt; m = cq.from(Member.class): 여기서 m이 쿼리 루트.
  조회의 시작점이 되며, Criteria에서 사용되는 특별한 별칭. 이 별칭은 엔티티에서만 부여할 수 있음.

Criteria는 코드로 JPQL을 완성하는 도구이기에 경로 표현식 역시 존재한다.
- `m.get(&quot;username&quot;)`은 JPQL의 `m.username`과 같음.
- `m.get(&quot;team&quot;).get(&quot;name&quot;)`는 JPQL의 `m.team.name`과 같음.</code></pre><h3 id="2-criteria-쿼리-생성">(2) Criteria 쿼리 생성</h3>
<p>CriteriaBuilder.createQuery() 메소드를 사용해 Criteria 쿼리를 생성하면 됨.</p>
<pre><code class="language-java">public interface CriteriaBuilder {
    CriteriaQuery&lt;ObjecT&gt; createQuery(); //조회값 반환 타입

    &lt;T&gt; CriteriaQuery&lt;T&gt; createQuery(Class&lt;T&gt; resultClass);
    CriteriaQuery&lt;Tuple&gt; createTupleQuery(); //조회값 반환 타입 Tuple
}</code></pre>
<p>Criteria 쿼리 생성 시에는 <strong>파라미터로 쿼리 결과에 대한 반환 타입을 지정</strong>할 수 있음. CriteriaQuery 생성 시 Member.class를 반환 타입으로 지정한다면, em.createQuery(cq)에서 반환 타입을 지정하지 않아도 됨.</p>
<ul>
<li>반환타입을 지정할 수 없거나 반환 타입이 둘 이상일 때는 타입을 지정하지 않고 Object로 반환을 받으면 됨.</li>
<li>반환 타입이 둘 이상이면 Object[]를 사용하는 것이 편리하다.</li>
<li>반환 타입을 튜플로 받고 싶을 때에서는 튜플을 사용하면 <code>CreateQuery&lt;Tuple&gt;</code>을 사용하면 된다.</li>
</ul>
<h3 id="3-조회">(3) 조회</h3>
<p>SELECT 절을 만드는 부분인 <code>select()</code>에 관련한 내용이다.</p>
<pre><code class="language-java">public interface CriteriaQuery&lt;T&gt; extends AbstractQuery&lt;T&gt; {
    //한 건 지정
    CriteriaQuery&lt;T&gt; select(Selection&lt;? extends T&gt; selection);

    //여러 건 지정
    CriteriaQuery&lt;T&gt; multiselct(Selection&lt;?&gt; ... selection)s;

    //여러 건 지정
    CriteriaQuery&lt;T&gt; multiselect(List&lt;Selection&lt;?&gt;&gt; selectionList);
}</code></pre>
<h4 id="조회-대상을-한-건-여러-건-지정">조회 대상을 한 건, 여러 건 지정</h4>
<p>select에 조회 대상을 하나만 지정할 때는 아래처럼 작성함.</p>
<pre><code class="language-java">cq.select(m);</code></pre>
<p>조회 대상을 여러 건 지정하려면 multiselect를 사용한다.</p>
<pre><code class="language-java">//JPQL: select m.username, m.age
cq.multiselect(m.get(&quot;username&quot;), m.get(&quot;age&quot;));</code></pre>
<p>여러 건을 지정할 때는 <code>cb.array</code>를 사용해도 된다.</p>
<pre><code class="language-java">CriteriaBuilder cb =em.getCriteriaBuilder();
//JPQL: select m.username, m.age
cq.select (cb.array(m.get(&quot;username&quot;), m.get(&quot;age&quot;)) );</code></pre>
<h4 id="distinct">DISTINCT</h4>
<p>distinct는 select, multiselect 뒤에 distinct(true)를 사용하면 됨.</p>
<pre><code class="language-java">//JPQL: select distinct m.username, m.age
cq.multiselect(m.get(&quot;username&quot;), m.get(&quot;age&quot;)).distinct(true);</code></pre>
<h4 id="new-construct">NEW, construct()</h4>
<p>JPQL에서 select new 생성자() 구문을 Criteria에서는 cb.construct(클래스 타입, ...)로 사용함.</p>
<pre><code class="language-java">&lt;Y&gt; CompoundSelection&lt;Y&gt; construct(Class&lt;Y&gt; resultClass,
Selection&lt;?&gt; ... selections)</code></pre>
<pre><code class="language-java">//JPQL: select new jpabook.domain.MemberDTO(m.username, m.age)
//from Member m

CriteriaQuery&lt;MemberDTO&gt; cq = cb.createQuery(MemberDTO.class);
Root&lt;Member&gt; m = cq.from(Member.class);

cq.select(cb.construct(MemberDTO.class), m.get(&quot;username&quot;),
m.get(&quot;age&quot;)));

TypedQuery&lt;MemberDTO&gt; query = em.createQuery(cq);
List&lt;MemberDTO&gt; resultList = query.getResultList();</code></pre>
<h4 id="튜플">튜플</h4>
<p>Criteria는 Map과 비슷한 튜플이라는 특별한 반환 객체를 제공한다.</p>
<p>튜플을 사용하기 위해서는 <code>cb.createTupleQuery()</code> 또는 <code>cb.createQuery(Tuple.class)</code>로 Criteria를 생성.</p>
<p>튜플은 이름 기반이기에 순서 기반인 Object[]보다 안전하며, <code>tuple.getElements()</code> 같은 메소드를 사용해 현재 튜플의 별칭과 자바 타입 역시 조회할 수 있다.</p>
<p>튜플을 사용해 <strong>엔티티도 조회</strong>할 수 있으면 튜플 사용시에는 <strong>별칭을 필수</strong>로 주어야한다.</p>
<h3 id="4-집합">(4) 집합</h3>
<h4 id="group-by">GROUP BY</h4>
<pre><code class="language-java">cq.groupBy(m.get(&quot;team&quot;).get(&quot;name&quot;));</code></pre>
<h4 id="having">HAVING</h4>
<pre><code class="language-java">cq.multiselect(m.get(&quot;team&quot;).get(&quot;name&quot;), maxAge, minAge)
    .groupBy(m.get(&quot;team&quot;).get(&quot;name&quot;))
    .having(cb.gt(minAge, 10)); //HAVING</code></pre>
<h3 id="5-정렬">(5) 정렬</h3>
<p>정렬 조건 역시 Criteria 빌더를 통해 생성할 수 있다.</p>
<pre><code class="language-java">//cb.desc(...) 또는 cb.asc()로 생성

cq.select(m)
    .where(agetGt)
    .orderBy(cb.desc(m.get(&quot;age&quot;)));</code></pre>
<h3 id="6-조인">(6) 조인</h3>
<p>조인 <code>join()</code> 메소드와 JoinType 클래스를 사용한다.</p>
<pre><code class="language-java">public enum JoinType {
    INNER,
    LEFT,
    RIGHT
}</code></pre>
<p>예)</p>
<pre><code class="language-java">Join&lt;Member, Team&gt; t = m.join(&quot;team&quot;, JoinType.INNER); //내부조인</code></pre>
<ul>
<li>외부조인의 경우 <code>JoinType.LEFT</code>로 설정</li>
<li>FETCH JOIN의 경우 <code>m.fetch(&quot;team&quot;, JoinType.LEFT);</code>로 사용.</li>
</ul>
<h3 id="7-서브-쿼리">(7) 서브 쿼리</h3>
<h4 id="간단한-서브-쿼리">간단한 서브 쿼리</h4>
<p>메인 쿼리와 서브 쿼리 간의 관련이 없는 단순한 서브쿼리</p>
<pre><code class="language-java">CriteriaBuilder cb =em.getCriteriaBuilder();
CriteriaQuery&lt;Member&gt; mainQuery = cb.createQuery(Member.class);

//서브쿼리
Subquery&lt;Double&gt; subQuery = mainQuery.subquery(Double.class);
Root&lt;Member&gt; m2 = subQuery.from(Member.class);
subQuery.select(cb.avg(m2.&lt;Integer&gt;get(&quot;age&quot;)));</code></pre>
<h4 id="상호-관련-서브-쿼리">상호 관련 서브 쿼리</h4>
<p>메인 쿼리와 서브 쿼리 간의 서로 관련이 있을 때의 서브 쿼리는 <strong>서브쿼리에서 메인 쿼리의 정보를 사용하려면 메인 쿼리에서 사용한 별칭을 얻어야함</strong>. 서브 쿼리는 메인 쿼리의 Root나 Join을 통해 생성된 별칭을 받아 사용한다.</p>
<pre><code class="language-java">.where(cb.equal(subM.get(&quot;username&quot;), m.get(&quot;username&quot;)));</code></pre>
<h3 id="8-in-식">(8) IN 식</h3>
<p>IN식은 Criteria 빌더에서 <code>in {...}</code> 메소드를 사용함</p>
<pre><code class="language-java">CriteriaBuilder cb =em.getCriteriaBuilder();
CriteriaQuery&lt;Member&gt; query = cb.createQuery(Member.class);
Root&lt;Member&gt; m = cq.from(Member.class);

cq.select (m)
  .where(cb.in(m.get(&quot;username&quot;))
  .value(&quot;회원1&quot;)
  .value(&quot;회원2&quot;));</code></pre>
<h3 id="9-case-식">(9) CASE 식</h3>
<p>CASE 식에는 <code>selectCase()</code> 메소드와 <code>when()</code>, <code>otherwise()</code> 메소드를 사용함</p>
<pre><code class="language-java">Root&lt;Member&gt; m = cq.from(Member.class);

cq.multiselect(
    m.get(&quot;username&quot;),
    cb.selectCase()
        .when(cb.ge(m.&lt;Integer&gt;get(&quot;age&quot;), 60), 600)
        .when(cb.le(m.&lt;Integer&gt;get(&quot;age&quot;), 15), 500)
        .otherwise(100) );</code></pre>
<h3 id="10-파라미터-정의">(10) 파라미터 정의</h3>
<p>JPQL에서 <strong>:PARAM1</strong>처럼 파라미터를 정의했듯이 Criteria도 파라미터를 정의할 수 있음.</p>
<pre><code class="language-java">CriteriaBuilder cb =em.getCriteriaBuilder();
CriteriaQuery&lt;Member&gt; query = cb.createQuery(Member.class);
Root&lt;Member&gt; m = cq.from(Member.class);

//정의
cq.select(m)
    .where(cb.equal(m.get(&quot;username&quot;), cb.parameter(String.class, &quot;usernameParam&quot;)));

List&lt;Member&gt; resultList = em.createQuery(cq)
    .setParameter(&quot;usernameParam&quot;, &quot;회원1&quot;) //바인딩
    .getResultList();</code></pre>
<ul>
<li><code>cb.parameter(타입, 파라미터 이름)</code> 메소드를 사용해 파라미터를 정의</li>
<li><code>setParameter(&quot;usernameParam&quot;, &quot;회원1&quot;)</code>을 사용해 파라미터에 사용할 값을 바인딩.</li>
</ul>
<h3 id="11-네이티브-함수-호출">(11) 네이티브 함수 호출</h3>
<p>네이티브 SQL 함수 호출을 위해서는 <code>cb.function()</code> 메소드를 사용하면 됨.</p>
<pre><code class="language-java">Root&lt;Member&gt; m = cq.from(Member.class);
Expression&lt;Long&gt; function = cb.function(&quot;SUM&quot;, Long.class, m.get(&quot;age&quot;));
cq.select(function);</code></pre>
<h3 id="12-동적-쿼리">(12) 동적 쿼리</h3>
<p><strong>동적 쿼리</strong>: 다양한 검색 조건에 따라 실행 시점에 쿼리를 생성하는 것을 말함.</p>
<p>동적 쿼리는 문자 기반인 JPQL보다는 코드 기반인 Criteria로 작성하는 것이 더 편리함.</p>
<p>Criteria로 동적 쿼리를 구성하면 최소 공백이나, where, and의 위치로 인한 에러가 발생하지 않기에 동적 쿼리 한정 Criteria 작성이 편리할 수 있다. 다만, 장황하고 복잡하다는 단점은 여전히 존재한다.</p>
<h3 id="13-함수-정리">(13) 함수 정리</h3>
<p>Criteria는 JPQL의 빌더 역할을 하기에 <strong>JPQL 함수를 코드로 지원</strong>한다.
대부분은 CriteriaBuilder에 정의되어 있다.</p>
<p><strong>조건함수</strong>: and(), or(), not(), equal(), notEqual(), lt() = lessThan(), bewteen(), isTrue(), in(), not(in()) 등</p>
<p><strong>스칼라와 기타 함수</strong>: sum(), prod(), neg(), some(), any() 등</p>
<p><strong>집합 함수</strong>: avg(), max(), min(), count() 등</p>
<h3 id="14-criteria-메타-모델-api">(14) Criteria 메타 모델 API</h3>
<p>Criteria는 코드 기반이기에 컴파일 시점에 오류를 발견할 수 있다. 다만 <code>m.get(&quot;age&quot;)</code>라는 코드를 작성 할 때 &#39;age&#39;는 문자이기에 이런 부분을 잘못 적는 것은 컴파일 시점에 에러를 발견하지 못한다.</p>
<p>이런 부분까지 코드로 작성하기 위해 <strong>메타 모델 API</strong>를 사용한다.</p>
<pre><code class="language-java">cq.select(m)
    .where(cb.gt(m.get(Member_.age), 20))
    .orderBy(cb.desc(m.get(Member_.age)));</code></pre>
<p>다만 위를 적용하기 위해서는 <strong>메타 모델 클래스</strong>가 필요하다.</p>
<pre><code class="language-java">@GeneratedValue(value = &quot;org.hibernate.jpmodelgen.JPAMetaModelEntityProcessor&quot;)
@StaticMetamodel(Member.class)
public abstract class Member {
    public static volatile SingularAttribute&lt;Member, Long&gt; id;
    public static volatile SingularAttribute&lt;Member, String&gt; username;
    public static volatile SingularAttribute&lt;Member, Integer&gt; age;
    public static volatile SingularAttribute&lt;Member,Order&gt; orders;
    public static volatile SingularAttribute&lt;Member,Team&gt; team;
}</code></pre>
<p>위의 클래스를 표준(CANONICAL) 메타 모델 클래스라 하며 줄여서 메타 모델이라 한다.</p>
<p>Member_ 메타 모델 클래스는 Member 엔티티를 기반으로 만들어야한다. 다만, 개발자가 직접 이런 복잡한 코드를 작성하지는 않고, 코드 자동 생성기가 엔티티 클래스를 기반으로 메타 모델 클래스들을 만들어준다.</p>
<h4 id="코드-생성기-설정">코드 생성기 설정</h4>
<p>코드 생성기는 보통 Maven, Gradle 같은 빌드 도구들을 사용해서 실행한다</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[자바 ORM 표준 JPA 프로그래밍] 8주차 스터디]]></title>
            <link>https://velog.io/@summeryoung_/%EC%9E%90%EB%B0%94-ORM-%ED%91%9C%EC%A4%80-JPA-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-8%EC%A3%BC%EC%B0%A8-%EC%8A%A4%ED%84%B0%EB%94%94</link>
            <guid>https://velog.io/@summeryoung_/%EC%9E%90%EB%B0%94-ORM-%ED%91%9C%EC%A4%80-JPA-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-8%EC%A3%BC%EC%B0%A8-%EC%8A%A4%ED%84%B0%EB%94%94</guid>
            <pubDate>Sat, 16 May 2026 10:29:53 GMT</pubDate>
            <description><![CDATA[<h1 id="09장-값-타입">09장. 값 타입</h1>
<p>JPA의 데이터 타입</p>
<ul>
<li><p>엔티티 타입:</p>
<ul>
<li><code>@Entity</code>로 정의하는 객체</li>
<li>식별자를 통해 지속해서 추적 가능</li>
</ul>
</li>
<li><p>값 타입: </p>
<ul>
<li>int, Integer, String처럼 단순히 값으로 사용하는 자바 기본 타입 또는 객체</li>
<li>식별자가 없고 문자/숫자같은 속성만 있어 추적이 불가능함.</li>
</ul>
</li>
</ul>
<blockquote>
<h4 id="값-타입의-종류">값 타입의 종류</h4>
<p><strong>기본값 타입</strong>: 자바 기본 타입(int, double), 래퍼 클래스(Integer), String
<strong>임베디드 타입</strong>: 복합 값 타입
<strong>컬렉션 값 타입</strong></p>
</blockquote>
<h2 id="91-기본값-타입">9.1 기본값 타입</h2>
<pre><code class="language-java">@Entity
public class Member {
    @Id @GeneratedValue
    private Long id;

    private String name;
    private int age;
}</code></pre>
<h4 id="값-타입">값 타입</h4>
<ul>
<li>Member의 String, int가 <strong>값 타입</strong>에 해당 </li>
<li>Member 엔티티는 id라는 식별자 값을 가지고, 생명주기 역시 있지만, 값 타입인 name, age속성은 식별자값도 없고 생명주기 역시 회원 엔티티에 의존하게 된다.</li>
<li>회원 엔티티 인스턴스를 제거하게되면 name, age 역시 사라지게 된다. </li>
<li>값 타입은 공유해서는 안된다.
  예) 다른 회원 엔티티의 이름을 변경하는데 나의 이름까지 변경됨.</li>
</ul>
<hr>
<h2 id="92-임베디드-타입복합-타입">9.2 임베디드 타입(복합 타입)</h2>
<p><strong>임베디드 타입</strong>: 새로운 값 타입을 정의해서 사용하는 것을 말함.</p>
<pre><code class="language-java">@Entity
public class Member {
    @Id @GeneratedValue
    private Long id;

    private String name;

    @Temporal(TemporalType.DATE) java.util.Date startDate;
    @Temporal(TemporalType.DATE) java.util.Date endDate;

    private String city;
    private String street;
    private String zipcode;
}</code></pre>
<p>위의 회원 엔티티는 단순히 정보를 풀어둔 것에 불과한다. 회원이 상세한 데이터를 그대로 가지고 있는 상태는 <strong>객체지향적이지 않아</strong> 응집력을 떨어뜨린다.</p>
<p>해결: 근무기간, 주소 타입 같은 타입을 가지도록 <strong>임베디드 타입</strong>을 사용한다.</p>
<pre><code class="language-java">@Entity
public class Member {

    @Id @GeneratedValue
    private Long id;

    private String name;

    @Embedded Period workPeriod;
    @Embedded Address homeAddress;
}

//기간 임베디드 타입
@Embeddable
public class Period {

    @Temporal (TemporalType.DATE) java.util.startDate;
    @Temporal (TemporalType.DATE) java.util.Date endDate;

    public boolean isWork (Date dat) {}
}

//주소 임베디드 타입
@Embeddable
public class Address {
    @Column (name=&quot;city&quot;) //매핑할 컬럼 정의 가능
    private String city;

    private String city;
    private String zipcode;
}</code></pre>
<ul>
<li><code>startDate</code>와 <code>endDate</code>를 합해서 Period(기간) 클래스를 만듦</li>
<li><code>city</code>, <code>street</code>, <code>zipcode</code>를 합해 Address(주소) 클래스를 만듦</li>
</ul>
<p>새로 정의한 값 타입들을 <strong>재사용</strong>을 할 수 있고 <strong>응집도 역시 높음</strong>.
해당 값 타입만 사용하는 메소드 역시 만들 수 있음.</p>
<h4 id="임베디드-타입을-사용하기-위한-어노테이션">임베디드 타입을 사용하기 위한 어노테이션</h4>
<ul>
<li><code>@Embeddable</code>: 값 타입을 정의하는 곳에 표시</li>
<li><code>@Embedded</code>: 값 타입을 사용하는 곳에 표시</li>
</ul>
<p>또한 임베디드 타입은 <strong>기본 생성자가 필수</strong>적이다. 임베디드 타입을 포함한 모든 값 타입은 엔티티의 생명주기에 의존하기 때문에 UML로 엔티티-임베디드 타입의 관계를 표현하면 <strong>컴포지션 관계</strong>가 됨.</p>
<h3 id="1-임베디드-타입과-테이블-매핑">(1) 임베디드 타입과 테이블 매핑</h3>
<p>임베디드 타입 = 엔티티의 값. 따라서 값이 속한 엔티티의 테이블에 매핑.
임베디드 타입은 <strong>객체와 테이블을 세밀하게 매핑</strong>하는 것이 가능하다. 잘 설계한 ORM 애플리케이션의 경우는 매핑한 테이블의 수보다 클래스의 수가 더 많다.</p>
<p>ORM을 사용하지 않고 개발하게되면 테이블 컬럼과 객체 필드를 대부분 1:1로 매핑하게 된다. 근무기간이나 주소같은 값 타입 클래스를 만들어 개발하기에는 SQL을 직접 다룰 때 테이블 하나에 여러 클래스를 매핑하는 등 복잡한 과정이 발생하기 때문이다. 다만, ORM을 사용해 이런 반복적인 작업을 JPA에 위임하고 객체지향 모델 설계에 집중할 수 있다.</p>
<h3 id="2-임베디드-타입과-연관관계">(2) 임베디드 타입과 연관관계</h3>
<p>임베디드 타입의 경우 <strong>값 타입을 포함하거나 엔티티를 참조</strong>할 수 있다. </p>
<pre><code class="language-java">@Entity
public class Member {
    @Embedded Address address;
    @Embedded PhoneNumber phoneNumber;
}

@Embeddable
public class Address{
    String street;
    String city;
    String state;
    @Embedded Zipcode zipcode;
}

@Embeddable
public class Zipcode {
    String zip;
    String plusFour;
}

@Embeddable
public class PhoneNumber {
    String areaCode;
    String localNumber;
    @ManyToOne
    PhoneServiceProvider provider;
}

@Entity
public class PhoneServiceProvider {
    @Id String name;
}</code></pre>
<p>위를 보면 값 타입에 해당하는 Address가 값 타입인 Zipcode를 포함하고 있고, 값 타입 PhoneNumber가 엔티티 타입인 PhoneServiceProvider를 참조한다.</p>
<h3 id="3-attributeoverride-속성-재정의">(3) <code>@AttributeOverride</code>: 속성 재정의</h3>
<p>임베디드 타입에 정의한 매핑정보를 재정의하기 위해서는 엔티티에 <code>@AttributeOverride</code>를 사용하면 된다. </p>
<p>예) 회원에게 주소가 하나 더 필요할 때</p>
<pre><code class="language-java">@Entity
public class Member {
    @Id @GeneratedValue
    privaet Long id;

    private String name;

    @Embedded Address homeAddress;
    @Embedded Address companyAddress;
}

위의 코드에서 주소를 추가하는 것은 쉽지만 문제는 **테이블에 매핑하는 컬럼명이 중복**된다. 이때는 `@AttributeOverrides`를 사용해 매핑정보를 재정의해야한다.

```java
@Entity
public class Member {
    @Id @GeneratedValue
    private Long id;

    private String name;

    @Embedded Address homeAddress;

    @Embedded 
    @AttributeOverrides ({
        @AttributeOverride(name=&quot;city&quot;, column=@Column(name=&quot;COMPANY_CITY&quot;)),
        @AttributeOverride(name=&quot;street&quot;, column=@Column(name=&quot;COMPANY_STREET&quot;)),
        @AttributeOverride(name=&quot;zipcode&quot;, column=@Column(nmae=&quot;COMPANY_ZIPCODE&quot;)
    })
    Address companyAddress;
}</code></pre>
<p><code>@AttributeOverride</code>를 사용했을 때, 어노테이션을 너무 많이 사용하여 엔티티 코드가 지저분해진다는 점이 있다. 하지만 한 엔티티에 같은 임베디드 타입을 중복해서 사용하는 일은 많지 않다.</p>
<h3 id="4-임베디드-타입과-null">(4) 임베디드 타입과 null</h3>
<p>임베디드 타입이 null일 경우에는 <strong>매핑한 컬럼 값이 모두 null</strong>이 된다.</p>
<pre><code class="language-java">member.setAddress(null);
em.persist(member);</code></pre>
<hr>
<h2 id="93장-값-타입과-불변-객체">9.3장 값 타입과 불변 객체</h2>
<p>값 타입은 복잡한 객체를 단순화하기 위해 만든 개념.</p>
<h3 id="1-값-타입-공유-참조">(1) 값 타입 공유 참조</h3>
<p>임베디드 타입 같은 값 타입을 여러 엔티티에서 공유하게되면 위험하다.</p>
<pre><code class="language-java">member1.setHomeAddress(new Address(&quot;OldCity&quot;));
Address address = member1.getHomeAddress();

address.setCity(&quot;NewCity&quot;);
member2.setHomeAddress(address);</code></pre>
<p>문제점: 회원2에 새로운 주소를 할당할 때 회원1의 주소를 그대로 참조하여 사용. -&gt; 회원2의 주소만 NewCity로 바뀌는 것이 아니라 회원1의 주소 역시 NewCity로 변경되어버림.</p>
<p>이유: 회원1과 회원2가 같은 인스턴스를 참조하기 때문에 영속성 컨텍스트는 회원1과 회원2 둘 모두 city 속성이 변경되었다고 판단하여 각각을 UPDATE SQL을 실행한다. <strong>이런 공유 참조로 인한 버그는 찾아내기가 어렵다.</strong> </p>
<p><strong>부작용</strong>: 뭔가 수정했는데 전혀 예상하지 못한 곳에서 문제가 발생하는 것. 방지를 위해서는 값을 복사해 사용하면 된다.</p>
<h3 id="2-값-타입-복사">(2) 값 타입 복사</h3>
<p>값 타입의 실제 인스턴스 값을 공유하는 것은 위험하기에 <strong>값을 복사해서 사용</strong>해야한다.</p>
<pre><code class="language-java">member1.setHomeAddress(new Address(&quot;OldCity&quot;));
Address address = member1.getHomeAddress();

Address newAddress = address.clone();

newAddress.setCity(&quot;NewCity&quot;);
member2.setHomeAddress(newAddress);</code></pre>
<p>회원2에 새로운 주소를 할당하기 위해 <code>clone()</code> 메소드를 만들었는데, 이 메소드는 스스로를 복사해 반환하도록 구현되어있다. 즉, 회원1의 주소 인스턴스를 복사하여 사용한다.</p>
<p>위의 코드는 의도대로 회원2의 주소만 NewCity로 변경하게된다. 또한 영속성 컨텍스트 역시 회원2의 주소만 변경된 것으로 판단하여 회원2에 대해서만 UPDATE SQL을 실행하게된다.</p>
<p>이렇게 <strong>값을 항상 복사해서 사용</strong>하게되면 공유 참조로 인한 부작용을 피할 수 있다. </p>
<h4 id="문제1">[문제1]</h4>
<p>하지만, 문제는 <strong>임베디드 타입처럼 직접 정의한 값 타입은 자바의 기본 타입이 아니라 객체 타입</strong>이라는 점이다.</p>
<p>자바의 기본 타입은 항상 값을 복사해서 전달하지만, 객체 타입의 경우에는 항상 <strong>참조값</strong>을 전달한다. 즉, 두 객체가 <strong>같은 인스턴스를 공유 참조</strong>하는 일이 발생한다.</p>
<h4 id="해결1">[해결1]</h4>
<p>객체를 대입할 때마다 <strong>인스턴스를 복사해 대입하면 공유 참조를 피할 수 있다.</strong> </p>
<hr>
<h4 id="문제2">[문제2]</h4>
<p>복사하지 않고 원본의 참조값을 직접 넘기는 것을 막을 수 있는 방법이 없다. 자바에서는 대입하려는 것이 값 타입인지의 여부를 신경 쓰지 않고, 자바 기본 타입인 경우 값을 복사하고 객체일 경우 참조를 넘긴다.</p>
<h4 id="해결2">[해결2]</h4>
<p>즉, <strong>객체의 공유 참조는 피할 수 없다</strong>. 따라서 해결책이 필요한데 가장 단순한 방법은 객체의 값을 수정하지 못하게 막으면 된다.</p>
<p>예를 들면 Address 객체의 setCity() 같은 수정자 메소드를 모두 제거하는 것이다. 이렇게 되면 공유 참조를 해도 값을 변경하지 못하기에 부작용의 발생을 막을 수 있다.</p>
<h3 id="3-불변-객체">(3) 불변 객체</h3>
<p><strong>객체를 불변하게 만들면 값을 수정할 수 없기에 부작용을 원천 차단할 수 있다. 따라서 값 타입은 될 수 있으면 불변 객체로 설계해야한다.</strong></p>
<p>불변 객체: 한 번 만들면 절대 변경할 수 없는 객체로 조회는 가능하지만 수정은 불가능.</p>
<p>하지만 불변 객체 역시 객체이기에 인스턴스의 참조 값 공유를 피할 수는 없음. 다만, 참조값을 공유하더라도 인스턴스의 값을 수정할 수 없기에 부작용은 발생하지 않음.</p>
<p>구현방법: 생성자로만 값을 설정하고 수정자를 만들지 않는다.</p>
<p>예) Address</p>
<pre><code class="language-java">@Embeddable
public class Address {
    private String city;

    protected Address () {this.city = city}

    //접근자(Getter)는 노출.
    public String getCity() {return city;}
}</code></pre>
<p>불변 객체의 사용</p>
<pre><code class="language-java">Address address = member1.getHomeAddress();
Address newAddress = new Address(address.getCity());
member2.setHomeAddress(newAddress);</code></pre>
<p>위의 Address는 불변객체로 값을 수정할 수 없기에 공유해도 부작용이 발생하지 않는다. 만약 값을 수정해야한다면, <strong>새로운 객체</strong>를 만들어 사용해야한다.</p>
<p>+) Integer, String은 자바가 제공하는 대표적인 불변 객체.</p>
<hr>
<h2 id="94-값-타입의-비교">9.4 값 타입의 비교</h2>
<pre><code class="language-java">int a = 10;
int b = 10;

Address a = new Address(&quot;서울시&quot;, &quot;종로구&quot;, &quot;1번지&quot;);
Address b = new Address(&quot;서울시&quot;, &quot;종로구&quot;, &quot;1번지&quot;);</code></pre>
<ul>
<li>int a의 숫자 10과 int b의 숫자 10은 같다고 표현</li>
<li>Address a와 Address b는 같다고 표현</li>
</ul>
<p>자바가 제공하는 객체 비교</p>
<ul>
<li><strong>동일성 비교</strong>: 인스턴스의 참조값을 비교하며 <code>==</code>를 사용.</li>
<li><strong>동등성 비교</strong>: 인스턴스의 값을 비교, <code>equals()</code> 사용.</li>
</ul>
<p>위의 코드에서 Address 값 타입을 <code>a==b</code>로 동일성 비교를 하는 경우 둘은 서로 다른 인스턴스 이기에 결과가 거짓이된다.</p>
<p>다만, 값 타입의 경우는 <strong>인스턴스가 달라도 그 안의 값이 같으면 같은 것으로 봐야한다.</strong> 그렇기에 값 타입 비교를 위해서는 <strong>동등성 비교</strong>를 해야한다. 이를 위해서는 Address의 <code>equals()</code> 메소드를 재정의해야한다.</p>
<p>값 타입의 <code>equals()</code> 메소드를 재정의할 때는 보통 모든 필드의 값을 비교하도록 구현한다.</p>
<hr>
<h2 id="95-값-타입-컬렉션">9.5 값 타입 컬렉션</h2>
<p>값 타입을 하나 이상 저장하려면 컬렉션에 보관하고 <code>@ElementCollection</code>, <code>@CollectionTable</code> 어노테이션을 사용하면 된다.</p>
<pre><code class="language-java">@Entity
public class Member {

    @Id @GeneratedValue
    private Long id;

    @Embedded
    private Address homeAddress;

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

    @ElementCollection
    @CollectionTable(name = &quot;ADDRESS&quot;, joinColumns = @JoinColumn(name = &quot;MEMBER_ID&quot;))
    private List&lt;Address&gt; addressHistory = new ArrayList&lt;Address&gt;();
}</code></pre>
<p>값 타입 컬렉션을 사용하는 <code>favoriteFoods</code>, <code>addressHistory</code>에 <code>@ElementCollection</code>을 지정하였다. </p>
<p><code>favoriteFoods</code>의 경우 기본값 타입인 String 컬렉션을 가진다. 이를 데이터베이스 테이블로 매핑해야하는데 관계형 데이터베이스의 테이블은 컬럼 안에 컬렉션을 포함할 수는 없다.</p>
<p>따라서 별도의 테이블을 추가하고 <code>@CollectionTable</code>를 사용해 추가한 테이블을 매핑해야한다. 또한 만약 <code>favoriteFoods</code>처럼 값으로 사용되는 컬럼이 하나일 때는 <code>@Column</code>을 사용해 컬럼명을 지정할 수 있다.</p>
<p>addressHistory는 임베디드 타입인 Address를 컬렉션으로 가진다. 이것 역시 마찬가지로 별도의 테이블을 사용해야한다. 테이블의 매핑정보는 <code>@AtrributeOverride</code>를 사용해서 재정의할 수 있음.</p>
<h3 id="1-값-타입-컬렉션-사용">(1) 값 타입 컬렉션 사용</h3>
<pre><code class="language-java">Member member = new Member();

//임베디드 값 타입
member.setHomeAddress(new Address(&quot;통영&quot;, &quot;몽돌해수욕장&quot;, &quot;660-123&quot;);

//기본값 타입 컬렉션
member.getFavoriteFoods().add(&quot;짬뽕&quot;);
member.getFavoriteFoods().add(&quot;짜장&quot;);
member.getFavoriteFoods().add(&quot;탕수육&quot;);

//임베디드 값 타입 컬렉션
member.getAddressHistory().add(new Address(&quot;서울&quot;, &quot;강남&quot;, &quot;123-123&quot;));
member.getAddressHistory().add(new Address(&quot;서울&quot;, &quot;강북&quot;, &quot;000-000&quot;));

em.persist(member);</code></pre>
<p>등록하는 코드를 보면 마지막에 member 엔티티만 영속화하였다. JPA는 이때 member 엔티티의 값 타입 역시 함께 저장한다. 실제 데이터베이스에 실행되는 INSERT SQL은 아래와 같다.</p>
<ul>
<li>member: INSERT SQL 1번</li>
<li>member.homeAddress: 컬렉션이 아닌 임베디드 값 타입이므로 회원테이블을 저장하는 SQL에 포함됨</li>
<li>member.favoriteFoods: INSERT SQL 3번</li>
<li>member.addressHistory: INSERT SQL 2번</li>
</ul>
<p>따라서 em.persist() 한 번으로 총 6번의 INSERT SQL을 실행하게 된다.</p>
<p><strong>값 타입 컬렉션 역시 조회 시 fetch 전략을 선택할 수 있는데, 기본은 LAZY이다.</strong></p>
<p>예) 지연로딩으로 모두 설정했다고 가정하고 아래킝 코드를 실행</p>
<pre><code class="language-java">Member member = em.find(Member.class, 1L);
Address homeAddress = member.getHomeAddress();
Set&lt;String&gt; favoriteFoods = member.getFavoriteFoods(); //LAZY

for (String favoriteFood : favoriteFoods) {
    System.out.println(&quot;favFood: &quot;+favoriteFood);
}

List&lt;Address&gt; addressHistory = member.getAddressHistory(); //LAZY

addressHistory.get(0);</code></pre>
<ul>
<li>member: 회원을 조회한다. 이때 임베디드 값인 homeAddress도 함께 조회한다. SELECT SQL을 한 번 호출.</li>
<li>member.homeAddress: 앞에서 회원 조회 시 같이 조회해둔다.</li>
<li>member.favoriteFoods: LAZY로 설정하여 실제 컬렉션 사용 시 SELECT SQL을 1번 호출.</li>
<li>member.addressHistory: LAZY로 설정하여 실제 컬렉션 사용 시 SELECT SQL을 1번 호출.</li>
</ul>
<p>예) 값 타입 컬렉션의 수정</p>
<pre><code class="language-java">Member member = em.find(Member.class, 1L);

member.setHomeAddress(new Address(&quot;새로운도시&quot;, &quot;신도시1&quot;, &quot;123456&quot;);

Set&lt;String&gt; favoriteFoods = member.getFavoriteFoods();
favoriteFoods.remove(&quot;탕수육&quot;);
favoriteFoods.add(&quot;치킨&quot;);

List&lt;Address&gt; addressHistory = member.getAddressHistory();
addressHistory.remove(new Address(&quot;서울&quot;, &quot;기존 주소&quot;, &quot;123-123-&quot;);
addressHistory.add(new Address(&quot;새로운 도시&quot;, &quot;새로운 주소&quot;, &quot;123-456&quot;);</code></pre>
<ul>
<li>임베디드 값 타입 수정: homeAddress 임베디드 값 타입은 MEMBER 테이블과 매핑하였으므로 MEMBER 테이블만 UPDATE하게된다. 사실 Member 엔티티를 수정하는 것과 같음.</li>
<li>기본값 타입 컬렉션 수정: 탕수육을 치킨으로 수정하기 위해서는 탕수육을 제거하고 치킨을 추가해야한다. 자바의 String 타입은 수정이 불가하다.</li>
<li>임베디드 값 타입 컬렉션 수정: 값 타입은 불변해야하기에 기존 주소를 삭제하고 새로운 주소를 등록하는 방식을 취한다. 값 타입은 <code>equals()</code>와 <code>hashcode</code>를 반드시 구현해야한다.</li>
</ul>
<h3 id="2-값-타입-컬렉션의-제약사항">(2) 값 타입 컬렉션의 제약사항</h3>
<p>엔티티는 식별자가 있어 값을 변경하여도 식별자로 데이터베이스에 저장된 원본 데이터를 쉽게 찾아 변경할 수 있지만, <strong>값 타입은 식별자 개념이 없고 단순한 값들의 모음이기에 값을 변경해버리면 데이터베이스에 저장된 원본 데이터를 찾기 어렵다</strong>.</p>
<p>특정 엔티티 하나에 소속된 값 타입은 값이 변경되어도 자신이 소속된 엔티티를 데이터베이스에서 찾고 값을 변경하면된다. 다만, <strong>값 컬렉션은 보관된 값 타입들을 별도의 테이블에 보관하기에 여기에 보관된 값 타입의 값이 변경되면 데이터베이스에 있는 원본 데이터를 찾기 어렵다.</strong></p>
<p>따라서 JPA 구현체들을 값 타입 컬렉션에 변경 사항이 발생하면 값 타입 컬렉션이 매핑된 테이블의 연관된 모든 데이터를 삭제하고, 현재 값 타입 컬렉션 객체에 있는 모든 값을 데이터베이스에 다시 저장한다.</p>
<p>따라서 실무에서 <strong>값 타입 컬렉션이 매핑된 테이블에 데이터가 많다면 값 타입 컬렉션 대신 일대다 관계를 고려</strong>해야함.</p>
<p>+) 값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 기본키를 구성해야한다. 따라서 데이터베이스 기본 키 제약 조건으로 인해 컬럼으로 null을 입력할 수 없고, 같은 값을 중복해서 저장할 수 없는 제약도 존재한다.</p>
<p>위의 문제를 해결하기 위해서는 <strong>값 타입 컬렉션을 만들기 위해서는 새로운 엔티티를 만들어 일대다 관계로 설정하면 된다.</strong> 여기에 추가로 영속성 전이 + 고아 객체 제거 기능을 적용하여 값 타입 컬렉션처럼 사용할 수 있다.</p>
<pre><code class="language-java">@Entity
public class AddressEntity {
    @Id
       @GeneratedValue
    private Long id;

    @Embedded Address address;
}</code></pre>
<p>설정은 아래와 같이 하면 된다.</p>
<pre><code class="language-java">@OneToMany (cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = &quot;MEMBER_ID&quot;)
private List&lt;AddressEntity&gt; addressHistory = new ArrayList&lt;AddressEntity&gt;();</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[데이터베이스 팀프로젝트 2주차 작업로그]]></title>
            <link>https://velog.io/@summeryoung_/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%ED%8C%80%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%9E%91%EC%97%85%EB%A1%9C%EA%B7%B8-2</link>
            <guid>https://velog.io/@summeryoung_/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%ED%8C%80%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%9E%91%EC%97%85%EB%A1%9C%EA%B7%B8-2</guid>
            <pubDate>Thu, 14 May 2026 09:15:32 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><strong>5월 14일 작업로그</strong></p>
</blockquote>
<ul>
<li>ERD 및 논리적 설계 수정</li>
<li>함수 종속성 다이어그램</li>
<li>정규화</li>
</ul>
<h2 id="1-erd-및-논리적-설계-수정">1. ERD 및 논리적 설계 수정</h2>
<ul>
<li><p>장바구니 상품에 가격 속성이 있었는데 없애는게 나아 그 부분을 수정하여 논리적 설계도 그거에 맞춰 수정하였다.</p>
<p>  ⇒ 주문 시에는 주문 시점의 가격을 따로 기록할 필요가 있는데 장바구니는 상품의 가격이 변할 때마다 그거에 맞춰 변하는거니까 가격을 기록하지 않고 그냥 가져와서 보여주는게 좋을 듯 했다.</p>
</li>
</ul>
<h2 id="2-함수-종속성-다이어그램">2. 함수 종속성 다이어그램</h2>
<ul>
<li><p>이상현상: 기본키가 아니면서 결정자인 속성(비후보키 결정자 속성)이 릴레이션에 존재할 때 발생</p>
<p>  ⇒ 릴레이션을 결정자인 속성이 기본키가 되도록 분해해 이상현상 제거</p>
</li>
</ul>
<h4 id="상품">상품</h4>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/4098bfda-0293-49d5-b573-3206afae7d98/image.png" alt=""></p>
<ul>
<li>다른 부분에서 기본키 이외에 결정자인 속성이 존재하지 않는 것으로 보인다.</li>
<li>상품-옵션 조합 릴레이션 같은 경우는 상품별 옵션이 뭐가 있는지 살펴보는 용도라 상품상세ID로 옵션을 하나 결정할 수 없고, 옵션으로도 하나를 결정할 수 없는 관계라 이게 괜찮은 건지..? 잘 모르겠다.</li>
</ul>
<h4 id="장바구니">장바구니</h4>
<img src="https://velog.velcdn.com/images/summeryoung_/post/488fab76-db88-4981-939f-101a2c43df69/image.png" width=60%>


<ul>
<li>이걸 그리면서 계속 또 고민인건 장바구니가 진짜 필요할지…? 속성도 하나만 갖는데 그게 회원ID라 필요가 없는거 아닌가?하는 생각이 계속든다.</li>
</ul>
<h2 id="3-정규화">3. 정규화</h2>
<h4 id="제1정규형">제1정규형</h4>
<ul>
<li><input disabled="" type="checkbox"> 릴레이션의 모든 속성이 더 이상 분해되지 않는 원자값만 가질 때</li>
</ul>
<h4 id="제2정규형">제2정규형</h4>
<ul>
<li><input disabled="" type="checkbox"> 릴레이션이 제1정규형에 속할 것</li>
<li><input disabled="" type="checkbox"> 기본키가 아닌 모든 속성이 기본키에 완전 함수 종속될 것</li>
</ul>
<p>+) 완전 함수 종속: A → B 종속성이 성립할 때, B가 A의 속성 전체에 함수 종속하고, 부분집합 속성에 함수 종속하지 않을 경우.</p>
<p>+) 부분 함수 종속(불완전 함수 종속): A의 속성 일부를 제거해도 종속성이 성립하는 경우.</p>
<h4 id="제3정규형">제3정규형</h4>
<ul>
<li><input disabled="" type="checkbox"> 릴레이션이 제2정규형에 속할 것</li>
<li><input disabled="" type="checkbox"> 기본키가 아닌 모든 속성이 기본키에 비이행적 함수 종속</li>
</ul>
<p>+) 이행적 함수 종속: 릴레이션을 구성하는 3개의 속성 집합 X, Y, Z에 대해 X→ Y,  Y→Z가 존재하면 X→Z가 성립</p>
<p>+) 비이행적 함수 종속: 속성이 기본키에 직접적으로만 종속되고, 다른 속성을 거쳐 종속되지 않은 경우</p>
<h4 id="bcnf-정규형">BCNF 정규형</h4>
<ul>
<li><input disabled="" type="checkbox"> 릴레이션의 함수종속성 X→Y가 성립할 때, 모든 결정자 X가 후보키</li>
</ul>
<hr>
<ol>
<li>상품관련 정규화: <a href="https://www.notion.so/35f565fee5c08005a9ccc453999f1e06?pvs=21">상품 정규화 노션</a> </li>
<li>장바구니 관련 정규화: <a href="https://www.notion.so/35f565fee5c0806e81bcf5f734ec9010?pvs=21">장바구니 정규화 노션</a> </li>
</ol>
<p>위에서 정리한 체크리스트? 기반으로 일단 각각 정규화를 진행했다.</p>
<p>정규화를 하면서 깨달은건 장바구니의 함수 종속성 다이어그램을 잘못 그렸었다. </p>
<img src="https://velog.velcdn.com/images/summeryoung_/post/8149db9b-c43b-4fe1-bdc2-e270180e7c88/image.png" width=60%>


<p>위처럼 수정했는데, 일전에 논리적 설계할 때 (회원ID, 상품상세ID)를 UNIQUE로 설정했었는데 이거를 까먹고 있어서 수정하고 정규화를 체크했다.</p>
<h2 id="4-물리적-설계-테스트">4. 물리적 설계 (테스트)</h2>
<h4 id="1-옵션-관련-문제">(1) 옵션 관련 문제</h4>
<img src="https://velog.velcdn.com/images/summeryoung_/post/30a29a27-1bde-476b-8ddd-847fe5abd00b/image.png" width=40%>


<ul>
<li><p>정규화 확인하기 힘들거 같아서 그냥 한 번 테이블 예시를 써봤는데? 옵션 종류와 옵션 상세를 이렇게 나누는게 좋은지 안좋은지 잘 모르겠다.</p>
<ul>
<li><p>과한 거 같기도한데…? 또 막상 합치자니 옵션종류ID로 옵션값 결정이 안될 것 같고</p>
<ul>
<li>(옵션상세ID, 옵션종류ID) → 이렇게 해야 결정이 되고?</li>
</ul>
</li>
<li><p>그렇다고 완전 하나의 거대한 테이블로 만들면 중복 데이터가 많아질 것 같다.</p>
<ul>
<li>하나의 테이블에 합치면 거기에 S-연청 이런식의 데이터가 여러 개의 상품마다 계속 중복으로 들어갈 것 같아서 또 분리해야하는 거 같아지는…?</li>
</ul>
<p>⇒ 우선은 분리해서 가져가는게 나아 보여서 그렇게 하기로 했다.</p>
</li>
</ul>
</li>
</ul>
<h4 id="2-상품옵션조합-테이블-문제">(2) 상품옵션조합 테이블 문제</h4>
<img src="https://velog.velcdn.com/images/summeryoung_/post/1ded7f41-6c16-42bc-bc0b-15a29e7f6231/image.png" width=30%>


<ul>
<li>상품마다의 옵션을 연결하는걸 위에 릴레이션으로 만들어서 그때그때 사용해서 조인하려고 했다. 상품상세하고 옵션상세가 다대다 관계니까 일단 릴레이션으로 빼긴 빼야할 것 같았다.</li>
<li>그런데? 러프하게 데이터 넣고 보니까, 상품 하나에 S 빨강, S 파랑, M 빨강, M 파랑 이렇게 한 종류의 옵션이 아니라 여러 옵션이 들어갈 수 있었다.</li>
<li>잘못된 줄 알았는데 또 막상 실제 상품명이랑 옵션명을 적어놓고 보니까 괜찮게 돌아갈 것 같았다.</li>
</ul>
<img src="https://velog.velcdn.com/images/summeryoung_/post/e2cbd5bc-860e-4d8d-87b6-48fc9c397eee/image.png" width=30%>


<p>⇒ 좀 완전하게 하고 DDL 작성하고 하려고 했는데 이런걸 확인하려면 일단 작성하고 직접 쿼리 써서 테스트해보는게 나을 것 같아서 계획을 바꾸는게 좋을 것 같다…</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[데이터베이스 팀프로젝트 1주차 작업로그]]></title>
            <link>https://velog.io/@summeryoung_/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%ED%8C%80%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%9E%91%EC%97%85%EB%A1%9C%EA%B7%B8-1</link>
            <guid>https://velog.io/@summeryoung_/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%ED%8C%80%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%9E%91%EC%97%85%EB%A1%9C%EA%B7%B8-1</guid>
            <pubDate>Thu, 14 May 2026 08:47:18 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>5월 12일 작업로그</p>
</blockquote>
<ul>
<li>개념적 설계 (ERD) 작업</li>
<li>논리적 설계</li>
</ul>
<h2 id="1-개념적-설계-erd-작업">1. 개념적 설계 (ERD 작업)</h2>
<h3 id="상품-개체">상품 개체</h3>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/1c44754e-d06b-4ad5-8af0-63ccc3251a94/image.png" alt=""></p>
<ul>
<li><p>기존에 상품 개체 하나 정도에 카테고리 개체 하나 이렇게 있던 상태에서 장바구니와 옵션을 추가하겠다고 생각하며, 꽤 수정을 많이 하게 되었다.</p>
</li>
<li><p>우선 카테고리 테이블을 만들어, 쇼핑몰 안에 있는 모든 상품들을 카테고리 테이블 내의 범주에서 분류할 수 있고, 카테고리 ID 정도만 외래키로 두는 것을 생각하고 ERD를 그렸다.</p>
</li>
<li><p>옵션을 처음 만들었을 때는 상품 아래 속성에 바로 작성하였는데, 추후에 옵션의 종류(예: 색상/크기)와 종류에 따른 실제 상세 옵션/옵션값(예: 진청/청/하양, S/M/L/XL) 등이 존재할 수 있기에 각각을 테이블로 분류해야할 것 같다는 생각이 들었다</p>
<p>  → 옵션 종류 테이블, 옵션 상세 테이블을 일단? 개체로 만들어두고 다시 상품을 먼저 완성하기로 했다.</p>
</li>
<li><p>옵션-상품 관계를 어떻게 해야할까? 하는 생각을 많이 했다. 처음에는 상품의 외래키 속성 정도면 충분할 것 같았는데? 그러면 사실 상품명/기본가격/제조업체 같은 내용의 중복이 많아진다.</p>
<p>  → 옵션-상품은 다대다 관계일거같아 테이블로 하나 빼는게 좋아보였다. 매핑 테이블 식으로 “<strong>상품 옵션 조합</strong>” 테이블을 만들었다.</p>
</li>
<li><p>쇼핑몰 블록/화면에 뜨는 상품하고, 실제 주문하는 상품이 다르기에(예: 청바지 → 검은 청바지 M) 상품 테이블과 또 별개로 실제 주문하는 상품(주문상품?)이 될 “<strong>상품 상세</strong>” 개체를 만들었다.</p>
</li>
<li><p>상품 상세 개체를 만든 후에는 상품 아래에 있던 재고 수량, 이미지, 판매량 등이 상품 상세 쪽, 즉 실제 판매되는 상품 쪽으로 옮겨가는게 좋을 것 같아 상품 상세쪽으로 속성을 옮겨 수정해주었다.</p>
</li>
</ul>
<aside>

<h4 id="상품-관련-정리">상품 관련 정리</h4>
<ul>
<li>상품: 대표 상품(청바지, 후드집업) 등의 정보 → 옵션 제외 <strong>공통적인 정보를 담아둠</strong></li>
<li>상품 상세: 실제 판매되는 상품=장바구니에 들어가서 결제될 상품. (연한 청바지 S 같이) → 상품을 외래키로 가지고, 재고, 옵션에 따른 추가금, 판매량 등 판매 관련 실제 정보가 존재하고, 매핑 테이블을 통해 옵션과 조합될 개체</li>
<li>상품 옵션 조합: 상품상세와 옵션(상세값)을 각각 외래키로 가지는 매핑 테이블. 둘이 복합키를 구성.</li>
<li>카테고리: 상품(대표 상품/공통 상품)이 어느 카테고리에 속하는지 여부를 결정. 상품 테이블에서 외래키로 소유.</aside>

</li>
</ul>
<h3 id="옵션-개체">옵션 개체</h3>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/12cde601-3f64-408f-8a93-9a7b388c9624/image.png" alt=""></p>
<ul>
<li><p>큰 옵션 범주(사이즈/색상)과 안으로 들어가 세부적인 값을 가질 옵션 상세를 우선은 나누었다.</p>
<p>  → 현재 크기의 쇼핑몰? 단위에서는 사실 굳이 필요할까 싶긴했는데, 비즈니스 로직을 생각하면 색상 → 검정/하양 이렇게 선택하니까 큰 범주를 테이블로 빼두면 구현하는 것도 좋을 것 같았다.</p>
</li>
<li><p>약한 개체가 여전히 헷갈리긴하지만, 옵션상세가 단순히 옵션값으로만 구별된다기보다는 옵션종류+실제 그 옵션값 이렇게 분리되어야할 것 같아서 처음에는 복합키를 생각했는데 이래저래 구현하는 것 생각하고 외래키로 가져야하는거 생각하면 대리키를 두는게 좋을 것 같아 옵션ID라는 대리키를 두고 PK로 삼는게 좋아보여서 그렇게 ERD를 설계했다.</p>
</li>
</ul>
<aside>

<h4 id="옵션-관련-정리">옵션 관련 정리</h4>
<ul>
<li>옵션 종류: 큰 옵션 범주. 예) 사이즈, 색상 …</li>
<li>옵션 상세: 종류 내의 세부적인 옵션. 예) S/M/L/XL, 카키, 검정, 하양</aside>

</li>
</ul>
<h3 id="장바구니-개체">장바구니 개체</h3>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/927f8942-9b20-4a30-a3e2-cb3c88b48f2e/image.png" alt=""></p>
<ul>
<li>장바구니 자체는 사실 회원이 1:1로 소유하는 것이기에 굳이? 개체/테이블로 만들어야할까하는 생각이 들었는데 일단은…확장 가능성?도 있을 수 있고 해서 만들어보긴했는데 여전히 실제 필요할까? 싶긴하다.</li>
<li>장바구니 상품은 장바구니 안에 들어가는 상품의 데이터를 기록하기 위한 테이블/개체가 필요할 거라고 생각해 만들었는데..? 사실 처음에는 주문 상세같은 쪽에서 컬럼을 하나 두어 상태컬럼으로 장바구니 이런 식으로 두는 것도 방법이지 않을까?했는데 장바구니 특성상 실제 넣었다가 주문하지 않을 수도 있고 하기에 일단 분리하여 관리하는 쪽이 좋을 것 같았다.</li>
<li>장바구니 담았을 때의 가격도 일단 가지게 했는데 필요할지..? 상품상세ID를 이용해서 조인으로 가져올 수 있을 것 같긴한데..? 근데 또 주문마다 조인해서 가져오려면 양이 많아질 것 같아서 아직 고민이다.</li>
</ul>
<aside>

<h4 id="장바구니-관련-정리">장바구니 관련 정리</h4>
<ul>
<li><p>장바구니 종류: 실질적으로는 회원ID 1:1 매핑.</p>
</li>
<li><p>장바구니 상품: 장바구니 안에 담기는 상품들에 대한 정보. 수량이나, 상품 상세(실제 판매 상품), 담긴 장바구니(=회원 ID) 등이 존재하고, 이 모든걸 대리키(장바구니 상품 ID)로 관리.</p>
<p>  ⇒ 가격이 굳이 있어야할까…?는 아직 미정…</p>
</li>
</ul>
</aside>

<h2 id="2-논리적-설계">2. 논리적 설계</h2>
<aside>

<h4 id="상품-논리적-설계">상품 논리적 설계</h4>
<h4 id="상품">상품</h4>
<p><strong>상품 (상품ID(PK), 제조업체ID(FK), 상품명, 가격, 카테고리(FK), 이미지)</strong></p>
<ul>
<li>상품ID (INT): PK, NOT NULL, AUTO INCREMENT</li>
<li>제조업체ID (INT): FK, NOT NULL<ul>
<li>제조업체ID 참조</li>
</ul>
</li>
<li>상품명 (VARCHAR): NOT NULL</li>
<li>가격 (INT): NOT NULL</li>
<li>카테고리: FK, NOT NULL<ul>
<li>카테고리ID 참조</li>
</ul>
</li>
<li>이미지 (VARCHAR)<ul>
<li>이미지 URL 정보</li>
</ul>
</li>
</ul>
<h4 id="상품상세">상품상세</h4>
<p><strong>상품상세 (상품상세ID(PK), 상품ID(FK), 재고수량, 추가금, 판매량, 이미지)</strong></p>
<ul>
<li>상품상세ID (INT): PK, NOT NULL, AUTO INCREMENT</li>
<li>상품ID: FK, NOT NULL<ul>
<li>상품ID 참조</li>
</ul>
</li>
<li>재고수량 (BIGINT): NOT NULL, DEFAULT 0</li>
<li>추가금 (BIGINT): NOT NULL, DEFAULT 0</li>
<li>판매량 (BIGINT): NOT NULL, DEFAULT 0</li>
<li>이미지 (VARCHAR)<ul>
<li>이미지 URL 정보</li>
</ul>
</li>
</ul>
<h4 id="상품옵션-조합">상품옵션 조합</h4>
<p><strong>상품옵션 조합 (상품상세ID(PK, FK), 옵션ID(PK,FK))</strong></p>
<ul>
<li>상품상세ID (INT): PK, FK, NOT NULL<ul>
<li>외래키, 복합키</li>
<li>상품상세 참조</li>
</ul>
</li>
<li>옵션ID (INT): PK, FK, NOT NULL<ul>
<li>외래키, 복합키</li>
<li>옵션상세 참조</li>
</ul>
</li>
</ul>
<h4 id="카테고리">카테고리</h4>
<p><strong>카테고리 (카테고리ID(PK), 카테고리명)</strong></p>
<ul>
<li>카테고리ID (INT): PK, NOT NULL, AUTO INCREMENT</li>
<li>카테고리명 (VARCHAR): NOT NULL</li>
</ul>
<h4 id="제조업체">제조업체</h4>
<p><strong>제조업체 (업체번호(PK), 업체명, 소유자)</strong></p>
<ul>
<li>업체번호 (INT): PK, AI, NOT NULL</li>
<li>업체명 (VARCHAR): NOT NULL</li>
<li>소유자 (VARCHAR)</li>
</ul>
<h4 id="장바구니-논리적-설계">장바구니 논리적 설계</h4>
<h4 id="장바구니">장바구니</h4>
<p><strong>장바구니 ( 회원ID(PK, FK) )</strong></p>
<ul>
<li>회원ID (INT) : PK, FK<ul>
<li>식별관계</li>
<li>약한 개체</li>
</ul>
</li>
</ul>
<h4 id="장바구니-상품">장바구니 상품</h4>
<p><strong>장바구니 상품 (장바구니상품ID(PK), 장바구니ID(FK), 상품상세ID(FK), 담은수량, 가격)</strong></p>
<ul>
<li>장바구니 상품ID (INT): PK, NOT NULL, AUTO INCREMENT</li>
<li>회원ID (INT): FK<ul>
<li>장바구니ID 참조 (실제로는 회원ID)</li>
</ul>
</li>
<li>상품상세ID (INT): FK<ul>
<li>상품상세ID 참조</li>
</ul>
</li>
<li>담은수량 (BIGINT): NOT NULL, DEFAULT 1</li>
<li>가격 (BIGINT): NOT NULL</li>
</ul>
<h4 id="옵션-논리적-설계">옵션 논리적 설계</h4>
<h4 id="옵션-종류">옵션 종류</h4>
<p><strong>옵션 종류 (옵션 종류ID(PK), 옵션 종류명)</strong></p>
<ul>
<li>옵션 종류ID (INT): PK, NOT NULL, AUTO INCREMENT</li>
<li>옵션 종류명 (VARCHAR): NOT NULL</li>
</ul>
<h4 id="옵션-상세">옵션 상세</h4>
<p><strong>옵션 상세 (옵션상세ID(PK), 옵션 종류ID(FK), 옵션값)</strong></p>
<ul>
<li>옵션상세ID (INT): PK, NOT NULL, AUTO INCREMENT</li>
<li>옵션 종류ID (INT): FK<ul>
<li>옵션 종류ID 참조</li>
</ul>
</li>
<li>옵션값 (VARCHAR): NOT NULL</li>
</ul>
</aside>

<ul>
<li>양이 많아서 토글로 정리합니당</li>
<li>ERD 짜면서 고민했던 부분들이라 슥슥 작성하긴했는데 NOT NULL 외에 UNIQUE 등 다른 제약을 걸 부분들이 있을 수도 있을 것 같다는…생각이 들긴합니다. (VARCHAR 제한?)</li>
</ul>
<h2 id="3-sql-설정">3. SQL 설정</h2>
<p>깃허브 + 스프링 프로젝트 사용해서 일단 협업을 진행할 생각이었어서 sql 코드 공유를 어떻게 할 지? 찾아봤는데 resources 아래에 application.yaml 파일을 잘 설정하고 data.sql, schema.sql에 sql 코드 작성하면 잘 돌릴 수 있을 것 같았다.</p>
<p>사용자명하고 비밀번호는 올리면 안되니까 application-local.yaml로 분리하는 방식으로 가져가고, application.yaml 파일 설정한 후에 깃허브에 data.sql, schema.sql 파일까지 올려서 설정하였다.</p>
<p>이렇게 하면 schema.sql에 DDL 작성하여서 공유하면 각자 노트북에서 이걸 돌려도 같은 결과를 얻을 수 있을 것 같아서 이런식으로 진행하면 좋을 것 같았다. data.sql에 더미 데이터 관련 insert 쿼리 등을 올리면 되는 식이다.</p>
<h4 id="applicationyaml">application.yaml</h4>
<pre><code class="language-yaml">spring:
  profiles:
    active: local
  application:
    name: {이름}

  sql:
    init:
      mode: always

  jpa:
    defer-datasource-initialization: true
    hibernate:
      ddl-auto: none
    properties:
      show_sql: true
      format_sql: true
      highlight_sql: true</code></pre>
<h4 id="application-localyaml">application-local.yaml</h4>
<pre><code class="language-yaml">spring:
  datasource:
    url: jdbc:mysql://localhost:3306/{스키마이름}?useSSL=false&amp;useUnicode=true&amp;characterEncoding=UTF-8&amp;serverTimezone=UTC&amp;createDatabaseIfNotExist=true&amp;allowPublicKeyRetrieval=true
    username: &#39;사용자이름&#39;
    password: &#39;비밀번호&#39;
    driver-class-name: com.mysql.cj.jdbc.Driver</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[자바 ORM 표준 JPA 프로그래밍] 7주차 스터디]]></title>
            <link>https://velog.io/@summeryoung_/%EC%9E%90%EB%B0%94-ORM-%ED%91%9C%EC%A4%80-JPA-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-7%EC%A3%BC%EC%B0%A8-%EC%8A%A4%ED%84%B0%EB%94%94</link>
            <guid>https://velog.io/@summeryoung_/%EC%9E%90%EB%B0%94-ORM-%ED%91%9C%EC%A4%80-JPA-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-7%EC%A3%BC%EC%B0%A8-%EC%8A%A4%ED%84%B0%EB%94%94</guid>
            <pubDate>Sat, 09 May 2026 09:56:05 GMT</pubDate>
            <description><![CDATA[<h1 id="08장프록시">08장.프록시</h1>
<blockquote>
</blockquote>
<ul>
<li><strong>프록시와 지연로딩, 즉시로딩</strong>: 객체가 데이터베이스에 저장되어 연관된 객체를 마음껏 탐색하기 어려운 상황에서 JPA 구현체들은 이 문제를 해결하기 위해 프록시를 사용함. 프록시를 사용하면 연관된 객체를 처음부터 데이터베이스에서 조회하는 것이 아니라 <strong>실제 사용하는 시점</strong>에 데이터베이스에서 조회할 수 있음. 다만, 자주 함께 사용하는 객체들은 조인을 사용해 함께 조회하는 것이 효과적임.</li>
<li><strong>영속성 전이와 고아 객체</strong>: JPA는 연관된 객체를 함께 저장하거나 함께 삭제할 수 있는 영속성 전이와 고아 객체 제거라는 편리한 기능을 제공함.</li>
</ul>
<hr>
<h2 id="81-프록시">8.1 프록시</h2>
<p>엔티티 조회 시 연관된 엔티티들이 항상 사용되지는 않는다.</p>
<p><strong>예) 회원 엔티티 조회 시 연관된 팀 엔티티의 사용 여부</strong></p>
<ul>
<li><p>회원과 팀 정보를 출력하는 비즈니스 로직</p>
<pre><code class="language-java">public void printUserAndTeam(String memberId) {
  Member member = em.find(Member.class, memberId);
  Team team = member.getTeam();

  System.out.println(&quot;회원이름: &quot;+member.getUsername());
  System.out.println(&quot;소속팀: &quot;+team.getName());
}</code></pre>
</li>
<li><p>회원 정보만 출력하는 비즈니스 로직</p>
<pre><code class="language-java">public String printUser(String memberId) {
  Member member = em.find(Member.class, memberId);
  System.out.println(&quot;회원 이름: &quot;+member.getUsername());
}</code></pre>
</li>
</ul>
<p><code>printUsername()</code>의 경우 memberId를 사용해 회원 엔티티와 팀을 모두 출력하는 반면, <code>printUser()</code>의 경우 회원 엔티티만을 출력한다.</p>
<p>이때 <code>printUser()</code> 메소드는 회원 엔티티만 사용하기에 <code>em.find()</code>로 회원 엔티티를 조회할 때 회원과 연관된 팀 엔티티까지 데이터베이스에서 함께 조회해 두는 것은 비효율적이다.</p>
<p>따라서 JPA는 이런 문제 해결을 위해 <strong>엔티티가 실제 사용될 때까지 데이터베이스 조회를 지연하는 방법</strong>을 제공하는데 이것을 <strong>지연 로딩</strong>이라고 한다.</p>
<p>즉, <code>team.getName()</code>처럼 엔티티 값을 실제로 사용할 때 데이터베이스에서 필요한 데이터를 조회하는 것이다.</p>
<p>이런 지연 로딩 기능에서 실제 엔티티 객체 대신, 데이터베이스 조회를 <strong>지연할 수 있는 가짜 객체</strong>를 <strong>프록시 객체</strong>라고 한다.</p>
<h3 id="1-프록시-기초">(1) 프록시 기초</h3>
<p>JPA에서 식별자로 엔티티 하나를 조회할 때에는 <strong><code>EntityManager.find()</code></strong>를 사용한다. 해당 메소드는 영속성 컨텍스트에 엔티티가 없으면 데이터베이스를 조회한다.</p>
<pre><code class="language-java">Member member = em.find(Member.class, &quot;member1&quot;);</code></pre>
<p>즉, 위처럼 엔티티를 직접 조회하면 조회한 <strong>엔티티의 사용여부와 관계없이 데이터베이스를 조회</strong>하게된다. </p>
<p>만일 데이터베이스 조회를 실제 사용 시점까지 미룰 때는 <strong><code>EntityManager.getReference()</code></strong> 메소드를 사용한다. 이때 JPA는 데이터베이스를 조회하지 않고, 실제 엔티티 객체도 생성하지 않는 대신 데이터베이스 접근을 위임한 <strong>프록시 객체</strong>를 위임한다.</p>
<h4 id="프록시-객체의-초기화">프록시 객체의 초기화</h4>
<p>프록시 객체의 초기화: 실제 엔티티가 사용될 때 데이터베이스를 조회해서 실제 엔티티 객체를 생성하는 것</p>
<pre><code class="language-java">//MemberProxy 변환
Member member = em.getReference(Member.class, &quot;id1&quot;);
member.getName();</code></pre>
<pre><code class="language-java">class MemberProxy extends Member {
    Member target = null;

    public String getName() {
        if (target == null) {
            //2. 초기화 요청
            //3. DB 조회
            //4. 실제 엔티티 생성 및 참조 보관
            this.target = ...;
        }

        //5. target.getName();
        return target.getName();
    }
}</code></pre>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/7ae4dd4c-4a94-4094-a51e-9523bfbb5454/image.png" alt=""></p>
<ol>
<li>프록시 객체에 <code>member.getName()</code>을 호출해서 실제 데이터를 조회한다.</li>
<li>프록시 객체는 실제 엔티티가 생성되어 있지 않으면 영속성 컨텍스트에 실제 엔티티 생성을 요청하는데, 이를 초기화라한다.</li>
<li>영속성 컨텍스트는 데이터베이스를 조회해 실제 엔티티 객체를 생성한다.</li>
<li>프록시 객체는 생성된 실제 엔티티 객체의 참조를 Member target 변수에 보관한다.</li>
<li>프록시 객체는 실제 엔티티 객체의 <code>getName()</code>을 호출해서 결과를 반환</li>
</ol>
<h4 id="프록시의-특징">프록시의 특징</h4>
<ul>
<li>실제 클래스를 상속받아 만들어지기에 실제 클래스와 겉모양이 동일하다. </li>
<li>즉, 사용할 때는 진짜 객체인지 프록시 객체인지 구분하지 않고 사용해도 된다.</li>
<li>실제 객체에 대한 <strong>참조</strong>를 보관한다.</li>
<li>프록시 객체의 메소드를 호출하면 프록시 객체는 실제 객체의 메소드를 호출한다.</li>
<li>프록시 객체는 <strong>처음 사용 시 한 번만 초기화</strong>됨.</li>
<li>프록시 객체를 초기화했다고, <strong>프록시 객체가 실제 엔티티로 바뀌는 것은 아니다</strong>. 프록시 객체가 초기화되며, 프록시 객체를 통해 실제 엔티티에 접근할 수 있다.</li>
<li>프록시 객체는 원본 엔티티를 상속받은 객체이므로 <strong>타입 체크 시에 주의해 사용</strong>해야한다.</li>
<li>영속성 컨텍스트에 찾는 엔티티가 이미 있으면 데이터베이스를 조회할 필요가 없기에 <code>em.getReference()</code>를 호출해도 프록시가 아닌 실제 엔티티를 반환한다.</li>
<li>초기화는 <strong>영속성 컨텍스트의 도움</strong>을 받아야한다. 따라서 <strong>준영속 상태의 프록시를 초기화하면 문제가 발생</strong>한다.</li>
</ul>
<h4 id="준영속-상태와-초기화">준영속 상태와 초기화</h4>
<pre><code class="language-java">//MemberProxy 반환
Member member = em.getReference(Member.class, &quot;id1&#39;);
transaction.commit();
em.close(); //영속성 컨텍스트 종료

member.getName(); //준영속 상태 초기화 시도 -&gt; 예외발생</code></pre>
<p><code>em.close()</code>를 통해 영속성 컨텍스트를 종료하게되면, <code>member</code>는 준영속 상태가 된다. 이때 <code>member.getName()</code>을 호출하면 프록시를 초기화해야하는데 영속성 컨텍스트가 없기에 실제 엔티티 조회가 불가해 예외가 발생한다.</p>
<h3 id="2-프록시와-식별자">(2) 프록시와 식별자</h3>
<p>엔티티를 프록시로 조회할 때 <strong>식별자(PK)값을 파라미터로 전달</strong>하는데 <strong>프록시 객체는 이 식별자값을 보관</strong>한다.</p>
<pre><code class="language-java">Team team = em.getReference(Team.class, &quot;team1&quot;); //식별자 보관
team.getId(); //초기화X</code></pre>
<p>프록시 객체는 식별자를 이미 가지고 있기에, 식별자 값을 조회해도 프록시 초기화가 발생하지는 않는다. 단, 엔티티 접근방식을 프로퍼티(<code>@Access(AccessType.PROPERTY</code>)로 설정한 경우에만 초기화를 하지 않는다.</p>
<p>만약 엔티티 접근방식을 필드(<code>@Access(AccessType.FIELD)</code>)로 설정하면 JPA는 <code>getId()</code> 메소드가 id만 조회하는 메소드인지 다른 필드까지 활용해서 어떤 일을 하는 메소드인지 알지 못하기에 프록시 객체를 초기화한다.</p>
<p>프록시는 다음처럼 연관관계 설정 시 유용하게 사용할 수 있다.</p>
<pre><code class="language-java">Member member = em.find(Member.class, &quot;member1&quot;);
Team team = em.getReference(Team.class, &quot;team1&quot;); //SQL 실행X
member.setTeam(team);</code></pre>
<p>연관관계 설정 시에는 식별자값만 사용하기에 프록시를 사용하게되면 데이터베이스 접근 횟수를 줄일 수 있음. 연관관계 설정 시에는 엔티티 접근 방식을 필드로 설정해도 프록시를 초기화하지 않는다.</p>
<h3 id="3-프록시-확인">(3) 프록시 확인</h3>
<p>JPA에서 제공하는 *<em><code>PersistenceUnitUtil.isLoaded(Object entity)</code> *</em> 메소드를 사용하면 프록시 인스턴스의 초기화 여부를 확인할 수 있음. 아직 초기화되지 않은 프록시 인스턴스는 false를 반환한다. 이미 초기화되었거나 프록시 인스턴스가 아닐 때에는 true를 반환한다.</p>
<pre><code class="language-java">boolean isLoaded = em.getEntityManagerFactory()
                    .getPersistenceUnitUtil().isLoaded(entity);
//또는 boolean isLoad = emf.getPersistenceUnitUtil().isLoaded(entity);</code></pre>
<p>조회한 엔티티가 진짜 엔티티인지 프록시로 조회한 것인지를 확인하기 위해서는 <strong>클래스명</strong>을 직접 출력하면 된다. <code>...javassist...</code>라고 적혀있으면 프록시인 것을 알 수 있다. 출력 결과는 프록시 생성 라이브러리에 따라 달라질 수 있다.</p>
<pre><code class="language-java">System.out.println(&quot;memberProxy =         qw&quot;+member.getClass().getName());</code></pre>
<hr>
<h2 id="82-즉시-로딩과-지연로딩">8.2 즉시 로딩과 지연로딩</h2>
<p>프록시 객체는 주로 <strong>연관된 엔티티를 지연로딩</strong>하기 위해 사용한다.</p>
<pre><code class="language-java">Member member = em.find(Member.class, &quot;member1&quot;);
Team team = member.getTeam(); //객체 그래프 탐색
System.out.println(team.getName()); //팀 엔티티 사용</code></pre>
<p>JPA는 개발자가 연관된 <strong>엔티티의 조회 시점을 선택</strong>할 수 있도록 두 가지 방법을 제공한다.</p>
<ul>
<li><p><strong>즉시로딩</strong>: 엔티티를 조회할 때 연관도니 엔티티도 함께 조회한다</p>
<ul>
<li>설정방법: <code>@ManyToOne(fetch = FetchType.EAGER)</code></li>
</ul>
</li>
<li><p><strong>지연로딩</strong>: 연관된 엔티티를 실제 사용할 때 조회</p>
<ul>
<li>설정방법: <code>@ManyToOne(fetch = FetchType.LAZY)</code></li>
</ul>
</li>
</ul>
<h3 id="1-즉시-로딩">(1) 즉시 로딩</h3>
<p>즉시 로딩 사용 시, <code>@ManyToOne</code>의 <code>fetch</code> 속성을 <code>FetchType.EAGER</code>로 설정한다.</p>
<pre><code class="language-java">@Entity
public class Member {
    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = &quot;TEAM_ID&quot;)
       private Team team;
}

Member member = em.find(Member.class, &quot;member1&quot;);
Team team = member.getTeam(); //객체 그래프 탐색</code></pre>
<p>회원과 팀을 즉시 로딩으로 설정하였기에, <code>em.find(Member.class, &quot;member1&quot;);</code>로 회원을 조회하는 순간 팀도 함께 조회한다. </p>
<p>이때 쿼리를 2번 실행하지 않고, JPA 구현체는 <strong>즉시 로딩 최적화를 위해 가능하면 조인 쿼리를 사용</strong>한다. 즉, 회원과 팀을 조인해 쿼리 하나로 두 엔티티를 모두 조회한다.</p>
<pre><code class="language-sql">SELECT
    M.MEMBER_ID AS MEMBER_ID,
    M.TEAM_ID AS TEAM_ID,
    M.USERNAME AS USERNAME,
    T.TEAM_ID AS TEAM_ID,
    T.NAME AS NAME
FROM MEMBER M LEFT OUTER JOIN 
     TEAM T ON M.TEAM_ID = T.TEAM_ID
WHERE M.MEMBER_ID=&#39;member1&#39;;</code></pre>
<h3 id="2-지연-로딩">(2) 지연 로딩</h3>
<p>지연 로딩 사용을 위해서는, <code>@ManyToOne</code>의 <code>fetch</code> 속성을 <code>FetchType.LAZY</code>로 지정한다.</p>
<pre><code class="language-java">@Entity
public class Member {
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = &quot;TEAM_ID&quot;)
    private Team team;
    //...
}</code></pre>
<p>회원과 팀을 지연 로딩으로 설정했기에 <code>em.find(Member.class, &quot;member1&quot;);</code> 호출 시, 회원만 조회하고 팀은 조회하지 않는다. 대신 조회한 회원의 team 멤버변수에 <strong>프록시 객체</strong>를 넣어둔다.</p>
<pre><code class="language-java">Team team = member.getTeam(); //프록시 객체</code></pre>
<p>반환되는 팀 객체는 프록시 객체로, 이 프록시 객체는 실제 사용까지 데이터 로딩을 미루게된다.</p>
<h3 id="3-즉시-로딩-지연-로딩-정리">(3) 즉시 로딩, 지연 로딩 정리</h3>
<p>연관된 엔티티를 처음부터 모두 영속성 컨텍스트에 올려두는 것은 현실적이지 않다. 다만, 필요할 때마다 SQL을 실행해 연관된 엔티티를 지연 로딩하는 것도 최적화 관점에서 보면 좋지 않다고 한다.</p>
<p>예를 들어 애플리케이션 로직에서 회원과 팀 엔티티를 같이 사용하는 경우가 많을 때면, SQL 조인을 사용해 둘을 한 번에 조회하는 것이 더 효율적이다.</p>
<hr>
<h2 id="83-지연-로딩-활용">8.3 지연 로딩 활용</h2>
<blockquote>
<p><strong>사내 주문 관리 시스템</strong></p>
</blockquote>
<ul>
<li>회원은 팀 하나에만 소속할 수 있다. (N:1)</li>
<li>회원은 여러 주문 내역을 가진다. (1:N)</li>
<li>주문내역은 상품정보를 가진다. (N:1)<br> </li>
<li><em>애플리케이션 로직*</em></li>
<li>회원과 연관된 팀은 자주 함께 사용되어, 즉시로딩으로 설정.</li>
<li>회원과 연관된 주문은 가끔 사용되어, 지연로딩으로 설정.</li>
<li>주문과 연관된 상품은 자주 함께 사용되어, 즉시로딩으로 설정.</li>
</ul>
<pre><code class="language-java">@Entity
public class Member {
    @Id
    private String id;
    private String username;
    private Integer age;

    @ManyToOne(fetch = FetchType.EAGER)
    private Team team;

    @OneToMany(mappedBy = &quot;member&quot;, fetch=FetchType.LAZY)
    private List&lt;Order&gt; orders;
}</code></pre>
<p>회원과 팀의 연관관계를 <code>FetchType.EAGER</code>로 설정하여 즉시 로딩되도록 설정한다. 외에 회원과 주문 내역의 경우에는 <code>FetchType.LAZY</code>로 설정해 실제 사용될 때까지 로딩을 지연한다.</p>
<p>회원 조회 시의 SQL은 아래와 같다.</p>
<pre><code class="language-sql">SELECT 
    member.id AS MEMBERID,
    member.age AS AGE,
    member.team_id AS TEAM_ID,
    member.username AS USERNAME,
       team.id AS TEAMID,
    team.name AS NAME
FROM member member LEFT OUTER JOIN
     team team on member.team_id = team1_.ID
WHERE member0_.ID = &#39;member1&#39;;
</code></pre>
<p>회원과 팀이 즉시 로딩으로 설정되었기 때문에, 하이버네이트는 조인 쿼리를 만들어 회원과 팀을 한 번에 조회한다. 반면, 회원과 주문 내역은 지연 로딩으로 설정했기에 결과를 프록시로 조회하게된다. 따라서 위의 SQL에는 전혀 나타나지 않는다. </p>
<p>회원 조회 후 <code>member.getTeam()</code>을 호출하게되면, 이미 로딩된 팀 엔티티를 반환하게 된다.</p>
<h3 id="1-프록시와-컬렉션-래퍼">(1) 프록시와 컬렉션 래퍼</h3>
<p><strong>컬렉션 래퍼</strong>: 하이버네이트는 엔티티를 영속 상태로 만들 때 엔티티에 컬렉션이 있으면 컬렉션을 추적하고 관리할 목적으로 원본 컬렉션을 하이버네이트가 제공하는 내장 컬렉션으로 변경하는 것.</p>
<p>엔티티 지연 로딩 시, 프록시 객체를 사용해 지연로딩을 처리하게되지만, 컬렉션의 경우에는 컬렉션 래퍼가 지연 로딩을 처리해주게됨.</p>
<p>컬렉션의 경우에는 <code>member.getOrders()</code>를 호출해도 컬렉션 초기화가 발생하지 않고 <strong>컬렉션에서 <code>member.getOrders().get(0);</code> 처럼 실제 데이터를 조회</strong>할 때에 데이터베이스를 조회해서 초기화하게된다.</p>
<h3 id="2-jpa-기본-패치-정략">(2) JPA 기본 패치 정략</h3>
<p>fetch 속성의 기본 설정값은 아래와 같다.</p>
<ul>
<li><code>@ManyToOne</code>, <code>@OneToOne</code>: 즉시로딩</li>
<li><code>@OneToMany</code>, <code>@ManyToMany</code>: 지연로딩</li>
</ul>
<p>JPA 기본 fetch 전략은 연관된 엔티티가 하나면 즉시로딩, 컬렉션이면 지연로딩을 사용하게된다. 컬렉션을 로딩하는 것은 비용이 많이 들고 잘못할 때, 너무 많은 데이터를 로딩하게 될 수 있기 때문이다.</p>
<p>추천되는 방법은 <strong>모든 연관관계에서 지연로딩을 사용</strong>하는 것이었다. 그리고 추후 애플리케이션 개발이 어느정도 완료되었을 때, 실제 사용하는 상황을 보고 꼭 필요한 상황에 즉시 로딩을 사용하도록 최적화하면 된다.</p>
<h3 id="3-컬렉션에서-fetchtypeeager-사용시-주의점">(3) 컬렉션에서 FetchType.EAGER 사용시 주의점</h3>
<ul>
<li><p>컬렉션을 <strong>하나 이상 즉시 로딩</strong>하는 것은 권장되지 않는다.</p>
<ul>
<li>컬렉션과 조인하는 것은 데이터베이스 테이블로 보면 <strong>일대다 조인</strong>으로 일대다 조인은 결과 데이터가 다쪽에 있는 수만큼 증가된다.</li>
<li>서로 다른 컬렉션 2개 이상을 조인할 때는 너무 많은 데이터를 반환할 수 있어 애플리케이션 성능 저하가 발생할 수 있다.</li>
</ul>
</li>
<li><p>컬렉션 즉시 로딩 때는 항상 <strong>외부 조인</strong>을 사용한다.</p>
</li>
</ul>
<h4 id="fetchtypeeager-설정과-조인-전략-정리">FetchType.EAGER 설정과 조인 전략 정리</h4>
<ul>
<li><p><code>@ManyToOne</code>, <code>@OneToOne</code>:</p>
<ul>
<li><code>optional = false</code>: 내부 조인</li>
<li><code>optional = true</code>: 외부 조인</li>
</ul>
</li>
<li><p><code>@OneToMany</code>, <code>@ManyToMany</code></p>
<ul>
<li><code>optional = false</code>: 외부조인</li>
<li><code>optional = true</code>: 외부조인</li>
</ul>
</li>
</ul>
<hr>
<h2 id="84-영속성-전이-cascade">8.4 영속성 전이: CASCADE</h2>
<p><strong>영속성 전이</strong>: 특정 엔티티를 영속 상태로 만들 때, 연관된 엔티티도 함께 영속 상태로 만들 때 사용. JPA의 경우 CASCADE 옵션을 통해 영속성 전이를 제공한다.</p>
<pre><code class="language-java">@Entity
public class Parent {
    @Id @GeneratedValue
    private Long id;

    @OneToMany(mappedBy = &quot;parent&quot;)
    private List&lt;Child&gt; children = new ArrayList&lt;&gt;();
}

@Entity
public class Child {
    @Id @GeneratdValue
    private Long id;

    @ManyToOne
    private Parent parent;
}</code></pre>
<p>부모 하나에 자식 둘을 저장하는 경우</p>
<pre><code class="language-java">Parent parent = new Parent();
em.persist(parent);

Child child1 = new Child();
child1.setParent(parent);
parent.getChildren().add(child1);
em.persist(child1);

Child child2 = new Child();
child2.setParent(parent);
parent.getChildren().add(child2);
em.persist(child2);</code></pre>
<p><strong>JPA에서 엔티티를 저장할 때 연관된 모든 엔티티는 영속 상태</strong>여야함. 그렇기에 부모 엔티티를 영속 상태로 만들고 자식 엔티티도 각각 영속 상태로 만든다.</p>
<p>이때 영속성 전이를 사용하면 부모만 영속 상태로 만들면, 연관된 자식까지 한 번에 영속 상태로 만들어야함.</p>
<h3 id="1-영속성-전이-저장">(1) 영속성 전이: 저장</h3>
<pre><code class="language-java">@Entity
public class Parent {
    @OneToMany(mappedBy = &quot;parent&quot;, cascade = CascadeType.PERSIST)
    private List&lt;Child&gt; children = new ArrayList&lt;&gt;();
}</code></pre>
<p> <code>cascade = CascadeType.PERSIST</code> 옵션을 통해 부모를 영속화할 때 연관된 자식들도 함께 영속화하는 설정. 해당 옵션을 적용할 때, 부모와 자식 엔티티를 한 번에 영속화할 수 있음.</p>
<h4 id="cascade-저장">CASCADE 저장</h4>
<pre><code class="language-java"> private static void saveWithCascade(EntityManager em) {
     Child child1 = new Child();
    Child child2 = new Child();

    Parent parent = new Parent();
    child1.setParent(parent);
    child2.setParent(parent);
    parent.getChildren().add(child1);
    parent.getChildren().add(chlid2);

    //부모 저장, 연관된 자식들 저장
    em.persist(parent);
 }</code></pre>
<p> 부모만 영속화하면, <code>CascadeType.PERSIST</code>로 설정한 자식 엔티티까지 함게 영속화해서 저장.</p>
<p> 영속성 전이의 경우에는 연관관계 매핑과는 관련이 없다. 단지 <strong>엔티티를 영속화할 때 연관된 엔티티도 같이 영속화하는 편리함을 제공</strong>한다. </p>
<h3 id="2-영속성-전이-삭제">(2) 영속성 전이: 삭제</h3>
<p>부모와 자식 엔티티를 모두 제거할 때는 각각의 엔티티를 하나씩 제거해야한다.</p>
<pre><code class="language-java">Parent findParent = em.find(Parent.class, 1L);
Child findChild1 = em.find(Child.class, 1L);
Child findChild2 = em.find(Child.class, 2L);

em.remove(findChild1);
em.remove(findChild2);
em.remove(findParent);</code></pre>
<p>영속성 전이는 엔티티를 삭제할 때도 사용할 수 있음. <code>CascadeType.REMOVE</code>로 설정하게되면 부모 엔티티만 삭제하면 연관된 자식 엔티티도 함께 삭제.</p>
<p>이 경우 DELETE SQL을 3번 실행해 부모와 연관된 자식을 모두 삭제한다.  삭제 순서는 외래키 제약조건을 고려해 자식을 먼저 삭제한 후에 부모를 삭제하게된다. </p>
<p>만약 <code>CascadeType.REMOVE</code>를 설정하지 않고 코드를 실행하면 부모 엔티티만 삭제된다. 하지만, 데이터베이스의 부모 열을 삭제하면 외래키 제약조건으로 인해 외래키 무결성 예외가 발생한다.</p>
<h3 id="3-cascade의-종류">(3) CASCADE의 종류</h3>
<p>CascadeType은 다양한 옵션이 존재한다.</p>
<pre><code class="language-java">public enum CascadeType {
    ALL,
    PERSIST,
    MERGE,
    REMOVE,
    REFRESH,
    DETACH
}</code></pre>
<p>위의 속성 중 여러 개를 같이 사용할 수 있다.</p>
<hr>
<h2 id="85-고아-객체">8.5 고아 객체</h2>
<p>고아 객체 제거: JPA가 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제하는 기능을 제공하는 것을 말함.</p>
<p>해당 기능을 통해 ** 부모 엔티티의 컬렉션에서 자식 엔티티의 참조만 제거하면 자식 엔티티가 자동삭제**되도록할 수 있다.</p>
<pre><code class="language-java">@Entity
public class Parent {
    @Id @GeneratedValue
    private Long id;

    @OneToMany (mappedBy = &quot;parent&quot;, orphanRemoval = true)
    private List&lt;Child&gt; children = new ArrayList&lt;&gt;();
}</code></pre>
<p>고아 객체 기능 활성화를 위해서는 컬렉션에 <code>orphanRemoval = true</code>를 설정한다. 이후에는 <strong>컬렉션에서 제거한 엔티티는 자동으로 삭제</strong>된다. 이 기능은 영속성 컨텍스트 플러시할 때 적용되기에 플러시 시점에 DELETE SQL이 실행된다.</p>
<p>모든 자식 엔티티를 제거하기 위해서는 컬렉션을 비우면 된다.</p>
<ul>
<li><p>고아 객체 제거 기능: <strong>참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 보고 삭제하는 기능</strong>.</p>
<ul>
<li>해당 기능은 <strong>참조하는 곳이 하나</strong>일 때에만 사용해야한다.</li>
<li>삭제한 엔티티를 다른 곳에서도 참조하면 문제가 발생한다.</li>
</ul>
</li>
</ul>
<hr>
<h2 id="86-영속성-전이--고아-객체-생명-주기">8.6 영속성 전이 + 고아 객체, 생명 주기</h2>
<p><code>CascadeType.ALL</code> + <code>orphanRemoval</code> = true를 동시에 사용하게되면?</p>
<ul>
<li>일반적으로 엔티티는 <code>EntityManger.persist()</code>를 통해 영속화됨.</li>
<li>이후 <code>EntityManager.remove()</code>를 통해 제거됨
=&gt; 즉, 엔티티 스스로 생명주기를 관리하게된다.</li>
</ul>
<p>만약 위의 두 옵션을 모두 활성화하게되면, <strong>부모 엔티티를 통해 자식의 생명 주기를 관리</strong>할 수 있게된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[자바 ORM 표준 JPA 프로그래밍] 6주차 스터디]]></title>
            <link>https://velog.io/@summeryoung_/%EC%9E%90%EB%B0%94-ORM-%ED%91%9C%EC%A4%80-JPA-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-6%EC%A3%BC%EC%B0%A8-%EC%8A%A4%ED%84%B0%EB%94%94</link>
            <guid>https://velog.io/@summeryoung_/%EC%9E%90%EB%B0%94-ORM-%ED%91%9C%EC%A4%80-JPA-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-6%EC%A3%BC%EC%B0%A8-%EC%8A%A4%ED%84%B0%EB%94%94</guid>
            <pubDate>Sat, 02 May 2026 16:53:55 GMT</pubDate>
            <description><![CDATA[<h1 id="07장-고급매핑">07장. 고급매핑</h1>
<blockquote>
<h4 id="내용">내용</h4>
</blockquote>
<ul>
<li><strong>상속 관계 매핑</strong>: 객체의 상속 관계를 데이터베이스에서 매핑하는 방법</li>
<li><strong><code>@MappedSuperclass</code></strong>: 등록일, 수정일 같이 여러 엔티티에서 공통으로 사용하는 매핑 정보만 상속받고 싶을 때 사용.</li>
<li><strong>복합키와 식별 관계 매핑</strong>: 데이터베이스의 식별자가 하나 이상일 때 매핑하는 방법 및 데이터베이스 설계의 식별관계와 비식별 관계</li>
<li><strong>조인 테이블</strong>: 외래 키 이외에 연관 테이블을 두어 연관관계를 연결하는 방법. 연결 테이블을 매핑하는 방법을 다룸</li>
<li><strong>엔티티 하나에 여러 테이블 매핑</strong></li>
</ul>
<hr>
<h2 id="71-상속-관계-매핑">7.1 상속 관계 매핑</h2>
<h4 id="슈퍼타입-서브타입-관계">슈퍼타입 서브타입 관계</h4>
<p>관계형 데이터베이스에는 상속이라는 개념이 존재하지 않음. 위의 모델링 기법이 그나마 객체의 상속 개념과 가장 유사하다.</p>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/0772aa3e-5eba-4cc8-ada5-2344fe3b73b3/image.png" alt=""></p>
<p>즉, ORM에서의 상속 관계 매핑은 객체의 상속 구조와 데이터베이스 슈퍼타입 서브타입 관계를 매핑하는 것.</p>
<h4 id="물리-모델로-구현하는-3가지-방법">물리 모델로 구현하는 3가지 방법</h4>
<ol>
<li>각각의 테이블로 변환: 슈퍼타입과 서브타입을 모두 테이블로 만든 후 조인을 사용하는 방법. (조인 전략)</li>
<li>통합 테이블로 변환: 테이블 하나를 사용해서 통합. (단일 테이블 전략)</li>
<li>서브타입 테이블로 변환: 서브타입마다 하나의 테이블을 생성. (구현 클래스마다 테이블 전략)</li>
</ol>
<hr>
<h3 id="1-조인-전략">1. 조인 전략</h3>
<p>엔티티 각각을 모두 테이블로 만들고 자식 테이블이 부모 테이블을 기본 키를 받아 &quot;기본키 + 외래키&quot;로 사용하는 전략. 따라서 조회 시 조인을 자주 사용한다.</p>
<p>해당 전략 사용 시 주의점: 객체는 타입으로 구분이 가능하지만, 테이블은 타입을 구분하지 못하기에 <strong>타입 구분 컬럼 추가</strong> 필요. <code>DTYPE</code> 컬럼을 구분 컬럼으로 사용.</p>
<pre><code class="language-java">//슈퍼타입 테이블
@Entity
@Interitance (strategy = InheritanceType.JOINED)
@DiscriminatorColumn (name = &quot;DTYPE&quot;)
public abstract class Item {
    @Id @GeneratedValue
    @Column (name = &quot;ITEM_ID&quot;)
    private Long id;

    prviate String name;
    private int price;
}

//서브타입 테이블
@Entity
@DiscriminatorValue(&quot;A&quot;)
public class Album extends Item {
    private String artist;
}

//서브타입 테이블
@Entity
@DiscriminatorValue(&quot;M&quot;)
public class Movie extends Item {
    private String director;
    private String actor;
}</code></pre>
<ul>
<li><code>@Inheritance (strategy = InheritanceType.JOINED)</code>: 상속 매핑은 부모 클래스에 <code>@Inheritance</code>를 사용. 매핑 전략 지정에서 위에서는 조인 전략을 사용하므로 <code>Inheritance.JOINED</code> 선택.</li>
<li><code>@DiscriminatorColumn(name=&quot;DTYPE&quot;)</code>: 부모 클래스에 구분 컬럼을 지정. 해당 컬럼으로 자식 테이블을 구분.</li>
<li><code>@DiscriminatorValue(&quot;M&quot;)</code>: 엔티티 저장 시 구분 컬럼에 입력할 값을 지정.</li>
</ul>
<p>기본적으로 자식 테이블을 부모 테이블의 ID 컬럼명을 그대로 사용하게 되는데, 만약 이를 변경하기 위해서는 <code>@PrimaryKeyJoinColumn</code>을 사용.</p>
<pre><code class="language-java">@Entity
@DiscriminatorValue(&quot;B&quot;)
@PrimaryKeyJoinColumn(name = &quot;BOOK_ID&quot;) //ID 재정의
public class Book extends Item {
    private String author;
    private String isbn;
}</code></pre>
<blockquote>
<h3 id="조인전략-정리">조인전략 정리</h3>
</blockquote>
<h4 id="장점">장점:</h4>
<ul>
<li>테이블의 정규화</li>
<li>외래키 참조 무결성 제약조건 활용 가능</li>
<li>저장공간의 효율적 사용<h4 id="단점">단점:</h4>
</li>
<li>조회 시 조인이 많이 사용되어 성능이 저하될 수 있음</li>
<li>조회 쿼리가 복잡함</li>
<li>데이터 등록 시 INSERT SQL을 두 번 실행해야함<h4 id="특징">특징:</h4>
</li>
<li>JPA 표준 명세는 구분 컬럼을 사용하도록 하지만, 하이버네이트 포함 몇몇 구현체는 구분 컬럼 없이도 동작<h4 id="관련-어노테이션">관련 어노테이션:</h4>
</li>
<li><code>@PrimaryKeyJoinColumn</code>, <code>@DiscriminatorColumn</code>, <code>@DiscriminatorValue</code></li>
</ul>
<hr>
<h3 id="2-단일-테이블-전략">2. 단일 테이블 전략</h3>
<p>테이블을 하나만 사용하며, 구분 컬럼으로 어떤 자식 데이터가 저장되었는지 구분. 조회 시 조인을 사용하지 않기 때문에 일반적으로 가장 빠른 편.</p>
<p>자식 엔티티가 매핑한 컬럼은 모두 널(null)을 허용해야한다는 주의점이 존재.</p>
<pre><code class="language-java">@Entity
@Inheritance (strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn (name = &quot;DTYPE&quot;)
public abstract class Item {
    @Id @GeneratedValue
    @Column (name = &quot;ITEM_ID&quot;)
    private Long id;

    private String name;
    private int price;
}

@Entity
@DiscriminatorValue(&quot;A&quot;)
public class Album extends Item {...}

@Entity
@DiscriminatorValue(&quot;M&quot;)
public class Movie extends Item {...}

@Entity
@DiscriminatorValue(&quot;B&quot;)
public class Book extends Item {...}</code></pre>
<p><code>InheritanceType.SINGLE_TABLE</code>로 지정 시 단일 테이블 전략을 사용한다는 의미로 테이블 하나에 모든 것을 통합하기 때문에 <strong>구분 컬럼을 필수</strong>로 사용해야함.</p>
<blockquote>
<h3 id="단일테이블-전략-정리">단일테이블 전략 정리</h3>
</blockquote>
<h4 id="장점-1">장점:</h4>
<ul>
<li>조인이 필요없기에 일반적으로 조회 성능이 빠름</li>
<li>조회 쿼리가 단순함<h4 id="단점-1">단점:</h4>
</li>
<li>자식 엔티티가 매핑한 컬럼은 모두 null을 허용해야함</li>
<li>단일 테이블에 모든 것을 저장하기에 테이블이 커질 수 있음. 따라서 상황에 따라 조회 성능이 떨어질 수 있음<h4 id="특징-1">특징:</h4>
</li>
<li>구분 컬럼을 반드시 사용해야함. <code>@DiscriminatorColumn</code>을 꼭 설정해야함</li>
<li><code>@DiscriminatorValue</code>를 지정하지 않을 시, 기본으로 엔티티 이름을 사용함</li>
</ul>
<hr>
<h3 id="3-구현-클래스마다-테이블-전략">3. 구현 클래스마다 테이블 전략</h3>
<p>자식 엔티티마다 별개의 테이블을 만들고, 자식 테이블 각각에 필요한 컬럼이 모두 존재하는 방식이다.</p>
<pre><code class="language-java">@Entity
@Inheritance (strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class Item {
    @Id @GeneratedValue
    @Column(name = &quot;ITEM_ID&quot;)
    private Long id;

    private String name;
    private int price;
}

@Entity
public class Album extends Item {...}

@Entity
public class Movie extends Item {...}

@Entity
public class Book extends Item {...}
</code></pre>
<p><code>InheritanceType.TABLE_PER_CLASS</code>로 지정하여 구현 클래스마다 테이블 전략을 사용. 해당 전략은 자식 엔티티마다 테이블을 만든다. 일반적으로는 추천하지 않는 전략이라고 한다.</p>
<blockquote>
<h3 id="구현-클래스마다-테이블-전략-정리">구현 클래스마다 테이블 전략 정리</h3>
</blockquote>
<h4 id="장점-2">장점:</h4>
<ul>
<li>서브타입을 구분해 처리 시 효과적</li>
<li><code>not null</code> 제약 조건을 사용할 수 있음<h4 id="단점-2">단점:</h4>
</li>
<li>여러 자식 테이블을 함께 조회할 때 성능이 느림. (UNION 사용 필요)</li>
<li>자식 테이블을 통합해서 쿼리하기 어려움<h4 id="특징-2">특징</h4>
</li>
<li>구분 컬럼을 사용하지 않음</li>
</ul>
<hr>
<h2 id="72-mappedsuperclass">7.2 <code>@MappedSuperclass</code></h2>
<h3 id="mappedsuperclass"><code>@MappedSuperclass</code></h3>
<p>앞서 부모클래스와 자식 클래스를 모두 데이터베이스 테이블과 매핑했는데, 만약 부모 클래스의 테이블과 매핑하지 않고, 부모 클래스를 상속받는 자식 클래스에게 매핑정보만 제공할 때는 <code>@MappedSuperclass</code>를 사용하면 됨.</p>
<p>비유하자면 추상클래스와 비슷하고 <code>@Entity</code> 어노테이션이 실제 테이블과 매핑되는 반면, <code>@MappedSuperclass</code>의 경우 실제 테이블과 매핑되지 않는다는 특징이 있다. 즉, 이는 단순히 <strong>매핑 정보를 상속</strong>할 목적으로만 사용됨.</p>
<img src="https://velog.velcdn.com/images/summeryoung_/post/dca4e731-3562-4e4e-96f4-c0b351b480a9/image.png" width=70%>

<pre><code class="language-java">@MappedSuperclass
public abstract class BaseEntity {
    @Id @GeneratedValue
    private Long id;
    private String name;
}

@Entity
public class Member extends BaseEntity {
    private String email;
}

@Entity
public class Seller extends BaseEntity {
    private String shopName;
}</code></pre>
<p><code>BaseEntity</code>에 객체들의 공통 매핑 정보를 정의하고, 자식 엔티티들은 상속을 통해 <code>BaseEntity</code>의 매핑 정보를 물려받는다. </p>
<p>이때 <code>BaseEntity</code>는 테이블과 매핑할 필요 없고, 자식 엔티티에게 공통으로 사용되는 매핑 정보만 제공하면 되기에 <code>@MappedSuperclass</code>를 사용한다.</p>
<h3 id="attributeoverrides-associationoverrides"><code>@AttributeOverrides</code>, <code>@AssociationOverrides</code></h3>
<p>부모로부터 물려받은 매핑 정보를 재정의하기 위해서 <code>@AttributeOverrides</code>를 사용하며, 연관관계를 재정의할 때는 <code>@AssociationOverrides</code>를 사용한다.</p>
<pre><code class="language-java">@Entity
@AttributeOverrides({
    @AttributeOverride(name=&quot;id&quot;, column=@Column(name =&quot;MEMBER_ID&quot;)),
    @AttributeOverride(name=&quot;name&quot;, column=@Column(name=&quot;MEMBER_NAME&quot;))
})</code></pre>
<h4 id="특징-정리">특징 정리</h4>
<ul>
<li>테이블과 매핑되지 않고 자식 클래스에 엔티티의 매핑 정보를 상속하기 위해 사용</li>
<li><code>@MappedSuperclass</code>로 지정한 클래스는 엔티티가 아니므로 em.find()나 JPQL에서 사용할 수 없음</li>
<li>해당 클래스를 직접 생성해 사용할 일은 거의 없기에 추상 클래스로 만드는 것이 권장됨.</li>
</ul>
<p>등록일자, 수정일자, 등록자, 수정자 같은 여러 엔티티에서 공통으로 사용하는 속성을 효과적으로 관리할 수 있다.</p>
<hr>
<h2 id="73-복합키와-식별관계-매핑">7.3 복합키와 식별관계 매핑</h2>
<h3 id="1-식별-관계-vs-비식별관계">1. 식별 관계 vs 비식별관계</h3>
<p>두 관계는 <strong>외래키가 기본키에 포함되는지의 여부</strong>에 따라 식별 관계와 비식별관계로 구분할 수 있다.</p>
<h4 id="식별-관계">식별 관계:</h4>
<p>부모 테이블의 기본키를 내려받아 자식 테이블의 기본키+외래키로 사용하는 관계</p>
<h4 id="비식별-관계">비식별 관계:</h4>
<p>부모 테이블의 기본키를 받아서 자식 테이블의 외래키로만 사용하는 관계</p>
<p>이 비식별 관계는 외래키에 널(NULL)을 허용하는지에 따라 다시 비식별 관계와 선택적 비식별 관계로 나뉨.</p>
<ul>
<li><strong>필수적 비식별 관계</strong>: 외래키에 NULL을 허용하지 않는다. 연관관계를 필수적으로 맺어야함</li>
<li><strong>선택적 비식별 관계</strong>: 외래키에 NULL을 허용한다. 연관관계를 맺을지 말지를 선택할 수 있다.</li>
</ul>
<p>최근에는 비식별 관계를 주로 사용하고 반드시 필요한 곳에만 식별관계를 사용하는 추세로 JPA는 둘 모두 지원한다.</p>
<hr>
<h3 id="2-복합키-비식별-관계-매핑">2. 복합키: 비식별 관계 매핑</h3>
<p>기본키를 구성하는 컬럼이 하나일 때</p>
<pre><code class="language-java">@Entity
public class Hello {
    @Id 
    private String id;
}</code></pre>
<p>둘 이상의 컬럼으로 구성된 복합 기본키의 경우, 식별자를 둘 이상 사용하기 위해서는 <strong>식별자 클래스</strong>를 별도로 만들어야한다. </p>
<p>이는 JPA가 영속성 컨텍스트에 엔티티를 보관할 때 엔티티의 식별자 키로 사용하는데, 이 식별자 구분을 위해 <code>equals</code>와 <code>hashCode</code>를 사용해 동등성 비교를 하기 때문이다. 식별자 필드가 2개 이상이면 별도 식별자 클래스를 만들고 거기에 이 동등성 비교를 구현해야한다.</p>
<p>이런 복합키를 위해 JPA는 <code>@IdClass</code>와 <code>@EmbeddedId</code> 2가지 방법을 제공한다. <code>@IdClass</code>는 관계형 데이터베이스에 가까운 방법이고, <code>@EmbeddedId</code>가 객체지향에 가까운 방법이라고한다.</p>
<h4 id="idclass"><code>@IdClass</code></h4>
<pre><code class="language-java">@Entity
@IdClass(ParentId.class)
public class Parent {
    @Id 
    @Column (name = &quot;PARENT_ID1&quot;)
    private String id1;

    @Id
    @Column (name = &quot;PARENT_ID2&quot;)
    private String id2;

    private String name;
}</code></pre>
<p>각각의 기본키 컬럼을 <code>@Id</code>로 매핑한 후, <code>@IdClass</code>를 이용해 각 클래스를 식별자 클래스로 지정함.</p>
<pre><code class="language-java">public class ParentId implements Serializable {
    private String id1;
    private String id2;

    public ParentId() {}

    public ParentId(String id1, String id2) {
        this.id1 = id1;
        this.id2 = id2;
    }

    @Override
    public boolean equals (Object o) {...}

    @Override
    public boolean equals hashCode () {...}
}</code></pre>
<p><code>@IdClass</code> 사용 시 식별자 클래스는 아래 조건을 만족해야함.</p>
<ul>
<li>식별자 클래스의 속성명과 엔티티에서 사용하는 식별자의 속성명이 같아야함</li>
<li><code>Serializable</code> 인터페이스를 구현해야함</li>
<li><code>equals</code>, <code>hashCode</code>를 구현해야함</li>
<li>기본 생성자가 있어야함</li>
<li>식별자 클래스는 public이어야함</li>
</ul>
<pre><code class="language-java">Parent parent = new Parent();
parent.setId1(&quot;myId1&quot;);
parent.setId2(&quot;myId2&quot;);
parent.setName(&quot;parentName&quot;);
em.persist(parent);</code></pre>
<p>식별자 클래스가 없어도 <code>em.persist()</code> 호출 시 영속성 컨텍스트에 엔티티를 등록하기 전 내부에서 <code>Parent.id1</code>, <code>Parent.id2</code> 값을 사용해 식별자 클래스를 생성하고 영속성 컨텍스트의 키로 사용하게 됨.</p>
<p>복합키로 조회할 경우 아래와 같이 이루어진다.</p>
<pre><code class="language-java">ParentId parentId = new ParentId(&quot;myId1&quot;, &quot;myId2&quot;);
Parent parent = em.find(Parent.clss, parentId);</code></pre>
<p>식별자 클래스를 통해서 엔티티를 조회할 수 있다.</p>
<p>자식 클래스를 추가하는 코드는 아래와 같다.</p>
<pre><code class="language-java">@Entity
public class Child {
    @Id
    private String id;

    @ManyToOne
    @JoinColumns ({
        @JoinColumn(name = &quot;PARENT_ID1&quot;, referencedColumnName = &quot;PARENT_ID1&quot;),
        @JoinColumn(name = &quot;PARENT_ID2&quot;, referencedColumnName = &quot;PARENT_ID2&quot;)
    })
    private Parent parent;
}</code></pre>
<p>부모 테이블의 기본키 컬럼이 복합키이므로 자식 테이블의 외래키도 복합키가 된다. 즉, 외래키 매핑 시 여러 컬럼을 매핑해야하므로 <code>@JoinColumns</code> 어노테이션을 사용해야하고 각각의 외래키를 <code>@JoinColumn</code>으로 매핑해야한다.</p>
<hr>
<h4 id="embededid"><code>@EmbededId</code></h4>
<pre><code class="language-java">@Entity
public class Parent {
    @EmbeddedId
    private ParentId id;

    private String name;
}</code></pre>
<p>Parent 엔티티에서 식별자 클래스를 직접 사용하고 <code>@EmbeddedId</code> 어노테이션을 적어주면 된다. 식별자 클래스는 아래와 같다.</p>
<pre><code class="language-java">@Embeddable
public class ParentId implements Serializable {
    @Column (name = &quot;PARENT_ID1&quot;)
    private String id1;

    @Column (name = &quot;PARENT_ID2&quot;)
    private String id2;

    //equals와 hashcode 구현
}</code></pre>
<p><code>@EmbeddedId</code>를 적용한 식별자 클래스의 경우에는 식별자 클래스에 기본키를 직접 매핑하게 된다.</p>
<p><code>@EmbeddedId</code>를 적용한 식별자 클래스의 경우 아래 조건을 만족해야한다.</p>
<ul>
<li><code>@Embeddable</code> 어노테이션을 붙여줘야한다</li>
<li><code>Serializable</code> 인터페이스를 구현해야한다</li>
<li>equals, hashcode를 구현해야한다</li>
<li>기본 생성자가 필요하다</li>
<li>식별자 클래스는 public이어야한다</li>
</ul>
<p>엔티티를 저장하는 코드를 보면 아래와 같다</p>
<pre><code class="language-java">Parent parent = new Parent();
ParentId parentId = new ParentId(&quot;myId1&quot;, &quot;myId2&quot;);
parent.setId(parentId);
parent.setName(&quot;parentName&quot;);
em.persist(parent);</code></pre>
<p>엔티티를 조회하는 코드는 역시 parentId (식별자 클래스)를 직접 사용한다.</p>
<pre><code class="language-java">ParentId parentId = new ParentId(&quot;myId1&quot;, &quot;myId2&quot;);
Parent parent = em.find(Parent.class, parentId);</code></pre>
<h3 id="복합-키와-equals-hashcode">복합 키와 <code>equals()</code>, <code>hashCode()</code></h3>
<p>복합키를 사용할 때는 <code>equals()</code>와 <code>hashCode()</code>를 필수적으로 구현해야한다.</p>
<pre><code class="language-java">ParentId id1 = new parentId();
id1.setId1(&quot;myId1&quot;);
id2.setId2(&quot;myId2&quot;);

ParentId id2 = new parentId();
id2.setId2(&quot;myId1&quot;);
id2.setId2(&quot;myId2&quot;);

id1.equals(id2);</code></pre>
<p>위의 경우 id1과 id2 인스턴스 둘 모두 같은 값을 가짐에도 다른 인스턴스이기에 <code>equals()</code>를 사용하였을 때 거짓이 된다. 이는 기본으로 제공되는 <code>equals()</code>는 인스턴스 참조값 비교(동일성 비교)를 하기 때문이다.</p>
<p>영속성 컨텍스트는 엔티티의 식별자를 키로 사용해 엔티티를 관리하기에 동등성 비교가 지켜지지 않으면 예상과 다른 엔티티가 조회되거나 엔티티를 찾을 수 없는 문제가 발생하기에 복합키에서의 <code>equals()</code>와 <code>hashCode()</code> 구현은 필수적이다.</p>
<p>+) 복합키에는 GeneratedValue를 사용할 수 없다. 복합키를 구성하는 여러 컬럼 중 하나에도 역시 마찬가지이다.</p>
<hr>
<h3 id="3-복합키-식별관계-매핑">3. 복합키: 식별관계 매핑</h3>
<p>식별관계에서 자식 테이블은 부모 테이블의 기본키를 포함해 복합키를 구성해야하기에 <code>@IdClass</code>나 <code>@EmbeddedId</code>를 사용해 식별자를 매핑해야함.</p>
<h4 id="idclass와-식별관계"><code>@IdClass</code>와 식별관계</h4>
<pre><code class="language-java">//부모
public class Parent {
    @Id @Column (name = &quot;PARENT_ID&quot;)
    private String id;
    private String name;
}

//자식
@Entity
@IdClass(ChildId.class)
public class Child{
    @Id
    @ManyToOne
    @JoinColumn (name = &quot;PARENT_ID&quot;)
    public Parent parent;

    @Id @Column(name = &quot;CHILD_ID&quot;)
    private String childId;

    private String name;
}

//자식 ID
public class ChildId implements Serializable {
    private String parent; //Child.parent 매핑
    private String childId; //Child.childId 매핑

    //equals, hashCode 매핑
}

//손자
@Entity
@IdClass(GrandChildId.class)
public class GrandChild {
    @Id
    @ManyToOne
    @JoinColumns ({
        @JoinColumn(name = &quot;PARENT_ID&quot;),
        @JoinColumn(name = &quot;CHILD_ID&quot;)
    })
    private Child child;

    @Id @Column(name = &quot;GRANDCHILD_ID&quot;)
    private String id;

    private String name;
}

//손자ID
public class GrandChildId implements Serializable {
    private ChildId child; //GrandChild.child 매핑
    private String id; //GrandChild.id 매핑

    //equals, hashCode 매핑
}</code></pre>
<p>식별 관계는 기본키와 외래키를 같이 매핑해야함. 따라서 식별자 매핑인 <code>@Id</code>와 연관관계 매핑인 <code>@ManyToOne</code>을 함께 사용해야한다.</p>
<pre><code class="language-java">@Id
@ManyToOne
@JoinColumn(name = &quot;PARENT_ID&quot;)
public Parent parent;</code></pre>
<p>Child 엔티티의 parent 필드를 보면 <code>@Id</code>를 통해 기본키로 매핑하면서 동시에 <code>@ManyToOne</code>과 <code>@JoinColumn</code>으로 외래키를 같이 매핑한다.</p>
<hr>
<h4 id="embeddedid와-식별-관계"><code>@EmbeddedId</code>와 식별 관계</h4>
<p><code>@EmbeddedId</code>로 식별 관계를 구성할 땐 <code>@MapsId</code>를 사용한다.</p>
<pre><code class="language-java">//부모
@Entity
public class Parent {
    @Id @Column (name = &quot;PARENT_ID&quot;)
    private String id;

    private String name;
}

//자식
@Entity
public class Child {
    @EmbeddedId
    private ChildId id;

    @MapsId(&quot;parentId&quot;) //ChildId.parentId 매핑
    @ManyToOne
    @JoinColumn (name = &quot;PARENT_ID&quot;)
    public Parent parent;

    private String name;
}

//자식 ID
@Embeddable
public class ChildId implements Serializable {

    private String parentId; //@MapsId(&quot;parentId&quot;)로 매핑

    @Column(name = &quot;CHILD_ID&quot;)
    private String id;

    //equals, hashCode ...
}

//손자
@Entity
public class GrandChild {
    @EmbededId
    private GrandChild id;

    @MapsId(&quot;childId&quot;) //GrandChildId.childId 매핑
    @ManyToOne
    @JoinColumns({
        @JoinColumn(name=&quot;PARENT_ID&quot;),
        @JoinColumn(name=&quot;CHILD_ID&quot;)
    })
    private Child child;

    private String name;
}

//손자ID
@Embeddable
public class GrandChildId implements Serializable {
    private ChildId childId; //@MapsId(&quot;childId&quot;)로 매핑

    @Column(name = &quot;GRANDCHILD_ID&quot;)
    private String id;

    //equals, hashCode...
}</code></pre>
<p><code>@MapsId</code>는 외래키와 매핑한 연관관계를 기본키에도 매핑한다는 의미이다. <code>@MapsId</code>의 속성값은 <code>@EmbeddedId</code>를 사용한 식별자 클래스의 기본키 필드를 지정하면 된다.</p>
<hr>
<h3 id="4-비식별-관계로의-구현">4. 비식별 관계로의 구현</h3>
<pre><code class="language-java">@Entity
public class Parent {
    @Id @GeneratedValue
    @Column (name = &quot;PARENT_ID&quot;)
    private Long id;
    private String name;
}

@Entity
public class Child {
    @Id @GeneratedValue
    private String name;

    @ManyToOne
    @JoinColumn(name = &quot;PARENT_ID&quot;)
    private Parent parent;
}

//손자
@Entity
public class GrandChild {
    @Id @GeneratedValue
    @Column(name = &quot;GRANDCHILD_ID&quot;)
    private Long id;
    private String name;

    @ManyToOne
    @JoinColumn(name = &quot;CHILD_ID&quot;)
    private Child child;
}</code></pre>
<p>복합키가 없으니 복합키 클래스를 만들지 않아도 된다.</p>
<hr>
<h3 id="5-일대일-식별-관계">5. 일대일 식별 관계</h3>
<p>일대일 식별관계의 경우, 자식테이블의 기본키 값으로 부모테이블의 기본키값 만을 사용한다. 따라서 부모테이블의 기본키가 복합키가 아니라면, 자식 테이블의 기본키를 복합키로 따로 구성할 필요가 없다.</p>
<pre><code class="language-java">//부모
@Entity
public class Board {
    @Id @GeneratedValue
    @Column(name = &quot;BOARD_ID&quot;)
    private Long id;

    private String title;

    @OneToOne(mappedBy = &quot;board&quot;)
    private BoardDetail boardDetail;
}

//자식
@Entity
public class BoardDetail{
    @Id
    private Long boradId;

    @MapsId //BoardDetail.boardId 매핑
    @OneToOne
    @JoinColumn (name =&quot;BOARD_ID&quot;)
    private Board board;

    private String content;
}</code></pre>
<p>BoardDetail처럼 식별자가 단순히 컬럼 하나일 경우 <code>@MapsId</code>를 사용하고 속성값은 비워두면 된다. 이때 <code>@MapsId</code>는 <code>@Id</code>를 사용해 식별자로 지정한 BoardDetail.boardId와 매핑되게 된다.</p>
<pre><code class="language-java">public void save(){
    Board board = new Board();
    board.setTitle(&quot;제목&quot;)
    em.persist(board);

    BoardDetail boardDetail = new BoardDetail();
    boardDetail.setContent(&quot;내용&quot;);
    boardDetail.setBoard(board);
    em.persist(boardDetail);
}</code></pre>
<hr>
<h3 id="6-식별-비식별-관계의-장단점">6. 식별, 비식별 관계의 장단점</h3>
<p>데이터베이스 설계 관점에서는 비식별 관계를 더 선호한다.</p>
<ul>
<li>식별관계는 부모 테이블이 기본키를 자식 테이블로 전파하면서 <strong>자식 테이블의 기본키 컬럼이 점점 늘어나게된다</strong>. 이는 조인 시 SQL이 복잡해지고, 기본키 인덱스가 불필요하게 커지는 문제를 초래한다.</li>
<li>식별관계는 <strong>2개 이상의 컬럼을 합해 복합 기본키를 만들어야하는 경우가 많다</strong>.</li>
<li>식별 관계 사용시 기본키로 비즈니스 의미가 있는 자연키 컬럼을 조합하는 경우가 많은 반면, 비식별 관계의 기본키는 비즈니스와 전혀 상관없는 <strong>대리키를 주로 사용</strong>한다. <strong>비즈니스 요구사항은 변할 수 있기에 이런 자연키 컬럼들이 자식에, 손자까지 전파되면 변경하기 힘들다는 단점</strong>이 존재한다.</li>
<li>식별관계는 비식별 관계보다 <strong>테이블 구조가 유연하지 못하다</strong>.</li>
</ul>
<p>객체 관계 매핑의 관점에서는 아래 이유로 비식별 관계를 선호한다.</p>
<ul>
<li>일대일 관계 제외, 식별관계는 2개 이상의 컬럼을 묶은 <strong>복합 기본키</strong>를 사용한다. JPA에서 <strong>복합키는 별도의 복합 키 클래스를 만들어서 사용</strong>해야하기에 더 많은 노력이 필요하다.</li>
<li>비식별 관계의 기본키는 주로 대리키를 사용하는데 JPA는 <code>@GeneratedValue</code> 처럼 대리키 생성을 위한 편리한 방법을 제공한다.</li>
</ul>
<p>식별 관계를 사용할 때의 장점도 존재한다. 기본키 인덱스를 활용하기 좋고, 상위 테이블들의 기본키 컬럼을 자식, 손자 테이블들이 가지고 있기에 특정 상황에서는 조인 없이 하위 테이블만으로도 검색을 완료할 수 있다.</p>
<hr>
<h2 id="74-조인-테이블">7.4 조인 테이블</h2>
<p>데이터베이스 테이블의 연관관계 설계 방법 2가지.</p>
<ul>
<li>조인 컬럼 사용 (외래키)</li>
<li>조인 테이블 사용 (테이블 사용)</li>
</ul>
<h4 id="조인-컬럼">조인 컬럼</h4>
<blockquote>
<p>예) 회원과 사물함이 있을 때, 각 테이블에 데이터를 등록했다가, 회원이 원할 때 사물함을 선택할 수 있다.</p>
</blockquote>
<ul>
<li>외래키에 널을 허용하는 선택적 비식별 관계를 택할 경우, 항상 외부 조인을 사용해야한다</li>
<li>실수로 내부 조인을 사용하게 되면 관계가 없는 회원은 조회되지 않는다.</li>
<li>회원과 사물함이 만약 가끔 관계를 맺게되면 외래 키 값 대부분이 null로 저장되는 단점이 있음</li>
</ul>
<h4 id="조인-테이블-사용">조인 테이블 사용</h4>
<ul>
<li><p>조인 테이블의 경우 <strong>연관관계를 관리하는 조인 테이블</strong>을 추가하고, 두 테이블의 외래키를 가지고 연관관계를 관리한다. 즉, 회원과 사물함에는 연관관계를 관리하기 위한 외래키 컬럼이 존재하지 않는다.</p>
</li>
<li><p>회원과 사물함 데이터를 각각 등록하고, 회원이 원할 때 사물함을 선택하면 조인 테이블에만 값을 추가하면 된다.</p>
</li>
<li><p>조인 테이블의 단점은 테이블을 하나 추가해야한다는 점으로 관리해야하는 테이블이 늘어나고, 회원과 사물함 테이블을 조인하기 위해 조인 테이블까지 추가로 조인해야한다는 단점이 있다.</p>
</li>
</ul>
<p>따라서 기본은 조인 컬럼을 사용하고 필요 시에만 조인 테이블을 사용하는 것이 권장된다.</p>
<hr>
<h3 id="1-일대일-조인-테이블">1. 일대일 조인 테이블</h3>
<p>일대일 관계를 만들기 위해서는 조인 테이블의 외래키 컬럼 각각에 총 2개의 유니크 제약조건을 걸어야한다.</p>
<pre><code class="language-java">//부모
@Entity
public class Parent {
    @Id @GeneratedValue
    @Column(name = &quot;PARENT_ID&quot;)
    private Long id;
    private String name;

    @OneToOne
    @JoinTable(name = &quot;PARENT_CHILD&quot;,
        joinColumns = @JoinColumn(name = &quot;PARENT_ID&quot;),
        inverseJoinColumns = @JoinColumn(name=&quot;CHILD_ID&quot;)
        )
    private Child child;
}

//자식
@Entity
public class Child {
    @Id @GeneratedValue
    @Column (name = &quot;CHILD_ID&quot;)
    private Long id;
    private String name;
}</code></pre>
<p>부모 엔티티의 경우 <code>@JoinColumn</code>을 대신해 <code>@JoinTable</code>을 사용함.</p>
<h4 id="jointable의-속성"><code>@JoinTable</code>의 속성</h4>
<ul>
<li>name: 매핑할 조인 테이블의 이름</li>
<li>joinColumns: 현재 엔티티를 참조하는 외래키</li>
<li>inverseJoinColumns: 반대방향 엔티티를 참조하는 외래키</li>
</ul>
<p>양방향으로 매핑하기 위해서는 아래의 코드를 추가하면 된다.</p>
<pre><code class="language-java">public class Child {
    @OneToOne(mappedBy=&quot;child&quot;)
    private Parent parent;
}</code></pre>
<hr>
<h3 id="2-일대다-조인-테이블">2. 일대다 조인 테이블</h3>
<p>일대다 관계를 만들기 위해서는 조인 테이블의 컬럼 중 다와 관련된 커럼인 CHILD_ID에 유니크 제약조건을 걸어야한다. 일대다 단방향 관계로 매핑하면 아래와 같다.</p>
<pre><code class="language-java">//부모
@Entity
public class Parent {
    @Id @GeneratedValue
    @Column(name = &quot;PARENT_ID&quot;)
    private Long id;
    private String name;

    @OneToMany
    @JoinTable(name=&quot;PARENT_CHILD&quot;,
        joinColumns = @JoinColumn(name = &quot;PARENT_ID&quot;),
        inverseJoinColumns = @JoinColumn(name = &quot;CHILD_ID&quot;)
   )
   private List&lt;Child&gt; child = new ArrayList&lt;Chlid&gt;();
}

//자식
@Entity
public class Child {
    @Id @GeneratedValue
    @Column(name = &quot;CHILD_ID&quot;)
    private Long id;
    private String name;
}</code></pre>
<hr>
<h3 id="3-다대일-조인-테이블">3. 다대일 조인 테이블</h3>
<p>다대일의 경우 일대다에서 방향만 반대일 뿐, 조인 테이블의 모양은 일대다와 같다.</p>
<pre><code class="language-java">//부모
@Entity
public class Parent {
    @Id @GeneratedValue
    @Column (name = &quot;PARENT_ID&quot;)
    private Long id;
    private String name;

    @OneToMany(mappedBy = &quot;parent&quot;)
    private List&lt;Child&gt; child = new ArrayList&lt;&gt;();
}

//자식
@Entity
public class Child {
    @Id @GeneratedValue
    @Column(name = &quot;CHILD_ID&quot;)
    private Long id;
    private String name;

    @ManyToOne(optional = false)
    @JoinTable (name = &quot;PARENT_CHILD&quot;,
                joinColumns = @JoinColumn(name =&quot;CHILD_ID&quot;),
                inverseColumns = @JoinColumn(name=&quot;PARENT_ID&quot;)
    private Parent parent;
}</code></pre>
<hr>
<h3 id="4-다대다-조인-테이블">4. 다대다 조인 테이블</h3>
<p>다대다 관계를 만들기 위해서는 조인테이블의 두 컬럼을 합해서 하나의 복합 유니크 제약조건을 걸어야한다. </p>
<pre><code class="language-java">//부모
@Entity
public class Parent {
    @Id @GeneratedValue
    @Column(name = &quot;PARENT_ID&quot;)
    private Long id;
    private String name;

    @ManyToMany
    @JoinTable(name = &quot;PARENT_CHLID&quot;,
            joinColumns = @JoinColumn(name = &quot;PARENT_ID&quot;),
            inverseColumns = @JoinColumn(name = &quot;CHILD_ID&quot;)
       )
    private List&lt;Child&gt; child = new ArrayList&lt;&gt;();
}

//자식
@Entity
public class Child {
    @Id @GeneratedValue
    @Column(name = &quot;CHILD_ID&quot;)
    private Long id;
    private String name;
}</code></pre>
<p>조인 테이블에 다른 컬럼을 추가하게 되면 <code>@JoinTable</code> 전략을 사용할 수 없게된다. 대신 새로운 엔티티를 만들어서 조인 테이블과 매핑해야한다.</p>
<hr>
<h2 id="75-엔티티-하나에-여러-테이블-매핑">7.5 엔티티 하나에 여러 테이블 매핑</h2>
<p><code>@SecondaryTable</code>을 사용하면 하나의 엔티티에 여러 테이블을 매핑할 수 있다. (다만, 잘 사용하지는 않는다고 한다.</p>
<pre><code class="language-java">@Entity
@Table(name =&quot;BOARD&quot;)
@SecondaryTable (name = &quot;BOARD_DETAIL&quot;,
    pkJoinColumns = @PrimaryKeyJoinColumn(name = &quot;BOARD_DETAIL_ID&quot;))
public class Board{
    @Id @GeneratedValue 
    @Column (name = &quot;BOARD_ID&quot;)
    private Long id;

    private String title;

    @Column (table = &quot;BOARD_DETAIL&quot;)
    private String content;
}</code></pre>
<p>Board 엔티티의 경우 <code>@Table</code>을 통해 BOARD 테이블과 매핑한 후, <code>@SecondaryTable</code>을 통해 BOARD_DETAIL 테이블을 추가로 매핑한다.</p>
<h4 id="secondarytable-속성"><code>@SecondaryTable</code> 속성</h4>
<ul>
<li><code>.name</code>: 매핑할 다른 테이블의 이름.</li>
<li><code>.pkJoinColumns</code>: 매핑할 다른 테이블의 기본키 컬럼 속성.</li>
</ul>
<p>다만 최적화를 위해서는, 여러 테이블을 하나의 엔티티에 매핑하는 것보다는 테이블 당 엔티티를 각각 만들어 일대일 매핑하는 것이 권장된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[데이터베이스 6주차 내용 정리]]></title>
            <link>https://velog.io/@summeryoung_/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-6%EC%A3%BC%EC%B0%A8-%EB%82%B4%EC%9A%A9-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@summeryoung_/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-6%EC%A3%BC%EC%B0%A8-%EB%82%B4%EC%9A%A9-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Sat, 11 Apr 2026 03:04:59 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>이론 정리 및 실습을 하나의 파일에 정리하였습니다. </p>
</blockquote>
<h1 id="3-뷰">3. 뷰</h1>
<h2 id="3-1-뷰view의-생성">3-1. 뷰(View)의 생성</h2>
<p><strong>뷰(view)</strong>: 하나 이상의 테이블을 합해 만드는 <strong>가상의 테이블</strong></p>
<ul>
<li>실제 데이터는 저장하지 않고, SELECT 쿼리의 결과에 이름을 붙여 재사용하는 객체<pre><code class="language-sql">CREATE VIEW &lt;뷰의 이름&gt; [열이름...]
AS &lt;SELECT문...&gt;</code></pre>
</li>
</ul>
<h4 id="특징">특징</h4>
<ul>
<li>데이터 보안- 특정 컬럼만 노출하도록 설정 가능</li>
<li>재사용성: 복잡한 쿼리를 뷰를 통해 재사용하며 단순화 가능</li>
<li>독립성: 기본 테이블의 구조를 변경해도 뷰를 통해 동일한 인터페이스를 제공</li>
<li>읽기 전용이 기본이지만, 일부의 경우에는 테이블 업데이트 역시 가능</li>
</ul>
<p>예) 뷰의 생성</p>
<pre><code class="language-sql">CREATE VIEW view_book
AS SELECT *
   FROM book
   WHERE bookname LIKE &#39;%축구%&#39;;</code></pre>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/f37294c0-bd18-416b-8a1c-6135616be174/image.png" alt="">
schemas 아래에 있는 view에서 생성된 뷰를 확인할 수 있다.</p>
<p>뷰의 생성 후에는 아래처럼 설정한 뷰의 이름을 통해 해당 뷰를 사용할 수 있음</p>
<pre><code class="language-sql">CREATE VIEW order_books
AS SELECT B.bookid, B.bookname, B.price, B.publisher
   FROM book B JOIN
        orders O ON B.bookid =O.bookid;

SELECT publisher, SUM(price) AS &#39;출판사별 수익&#39;
FROM order_books 
GROUP BY publisher;</code></pre>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/ba0dc832-7030-41e0-89d4-c17fdff70222/image.png" alt=""></p>
<h3 id="뷰의-장점-및-특징">뷰의 장점 및 특징</h3>
<h4 id="장점">장점</h4>
<ul>
<li>편리성 및 재사용성: 자주 사용되는 복잡한 질의를 뷰로 미리 정의해두고 사용할 수 있음</li>
<li>보안성: 사용자별로 필요한 데이터만 선별하여 보여줄 수 있고, 중요한 질의의 경우, 질의 내용을 암호화하는 것도 가능
예) 개인정보(주민번호)나 급여, 건강 같은 민감 정보를 제외한 뷰를 생성해 사용</li>
<li>독립성: 원본 테이블의 구조가 변해도 응용(사용)에 영향을 주지 않도록함</li>
<li><blockquote>
<p>논리적 데이터 독립성 제공 방법</p>
</blockquote>
</li>
</ul>
<h4 id="특징-1">특징</h4>
<ul>
<li><strong>원본 데이터 값에 따라 같이</strong> 뷰의 값이 변함</li>
<li>독립적인 인덱스 생성이 어려움</li>
<li>삽입/삭제/갱신 연산에 많은 제약 존재 (읽기 전용이 대다수)</li>
</ul>
<hr>
<h2 id="3-2-뷰의-수정">3-2. 뷰의 수정</h2>
<h3 id="replace">REPLACE</h3>
<p>뷰의 수정을 위해서는 <code>CREATE VIEW</code> 문에 <code>REPLACE</code> 명령을 추가.</p>
<pre><code class="language-sql">CREATE OR REPLACE VIEW &lt;뷰 이름&gt;
AS &lt;SELECT 문&gt;</code></pre>
<p>생성하거나 수정한다는 의미.</p>
<p>예) 앞에서 사용한 뷰에서 주문고객 정보까지 뷰에 포함하도록 수정</p>
<pre><code class="language-sql">CREATE OR REPLACE VIEW order_books
AS SELECT B.bookid, B.bookname, B.price, B.publisher, C.custid
   FROM book B JOIN
        orders O ON B.bookid = O.bookid JOIN
        customer C ON C.custid = O.custid;</code></pre>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/7c654688-7650-4faf-9ae7-24f35a029469/image.png" alt=""></p>
<h3 id="drop">DROP</h3>
<p>뷰를 삭제하기 위해서는 <code>DROP</code> 명령어를 사용함</p>
<pre><code class="language-sql">DROP VIEW &lt;뷰 이름&gt; </code></pre>
<p>예) </p>
<pre><code class="language-sql">DROP VIEW order_books;</code></pre>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/dac2c4ed-97d4-4e17-9635-1f8bc4068f07/image.png" alt="">
왼쪽의 Views에서 order_books가 사라진 것을 볼 수 있다.</p>
<hr>
<h2 id="3-3-뷰의-활용">3-3. 뷰의 활용</h2>
<blockquote>
<p>일반적으로 <strong>뷰는 읽기 전용</strong>이지만,. <strong>DISTINCT, GROUP BY</strong> 등의 제약이 없다면 변경이 가능하다</p>
</blockquote>
<h3 id="뷰의-활용-insert-update-delete-문">뷰의 활용: INSERT, UPDATE, DELETE 문</h3>
<ul>
<li>뷰에 대한 삽입/수정/삭제 연산은 제한적으로 수행된다.<ul>
<li>실제로 기본 테이블에 연산이 수행되니, 결과적으로 기본 테이블이 변경되는 것</li>
<li>변경 가능한 뷰와 불가능한 뷰가 존재한다</li>
</ul>
</li>
</ul>
<blockquote>
<h4 id="변경이-불가능한-뷰의-특징">변경이 불가능한 뷰의 특징</h4>
</blockquote>
<ul>
<li>기본 테이블의 <strong>기본키를 구성하는 속성이 포함되어 있지 않은</strong> 뷰</li>
<li>기본 테이블에서 <strong>NOT NULL로 지정된 속성이 포함되어있지 않은</strong> 뷰</li>
<li>기존 테이블에 있던 내용이 아닌 <strong>집계함수로 새로 계산된 내용을 포함</strong>하는 뷰</li>
<li>DISTINCT 키워드를 포함해 정의한 뷰</li>
<li>GROUP BY 절을 포함해 정의한 뷰</li>
<li>여러 개의 테이블을 조인해 정의한 뷰는 변경이 불가능한 경우가 많음.</li>
</ul>
<p>예) 변경이 불가능한 뷰의 예</p>
<img src="https://velog.velcdn.com/images/summeryoung_/post/e9306cc2-0c8e-4cc6-b61d-35a4197a521d/image.png" width=60%>

<p>View1</p>
<pre><code class="language-sql">CREATE VIEW view1
AS SELECT 제품번호, 재고량, 제조업체
   FROM 제품;
</code></pre>
<p>기본 테이블의 기본키인 제품번호를 포함하고 있기에 변경 가능. 
(기본키 속성은 NOT NULL)</p>
<p>View2</p>
<pre><code class="language-sql">CREATE VIEW view2
AS SELECT 제품명, 재고량, 제조업체
   FROM 제품;</code></pre>
<p>기본 테이블의 기본 키를 구성하는 속성이 포함되어 있지 않아 변경이 불가능함.
만약, 변경을 허용하는 경우 기본 키 속성의 값이 NULL이 되기에 안됨.</p>
<h4 id="뷰에-데이터-삽입하기">뷰에 데이터 삽입하기</h4>
<pre><code class="language-sql">INSERT INTO view1 VALUES (&#39;p08&#39;, 1000, &#39;신선식품&#39;);</code></pre>
<p>INSERT문은 기존 테이블에 작성하듯이 작성한다.</p>
<p>이 연산은 실제로는 <strong>기본 테이블(제품 테이블)</strong>에 수행 되기에 새로운 제품에 대한 튜플이 제품 테이블에 삽입된다.</p>
<p>** 슬라이드 60p. 실습12**
(1) 판매가격이 20,000원 이상인 도서의 도서번호, 도서이름, 고객이름, 출판사, 판매가격을 보여주는 highorders 뷰 생성하기</p>
<pre><code class="language-sql">CREATE VIEW highorders
AS SELECT B.bookid, B.bookname, C.name, B.publisher, B.price
   FROM book B JOIN
        orders O ON B.bookid = O.bookid JOIN
        customer C ON C.custid = O.custid
   WHERE B.price &gt;= 20000;</code></pre>
<p>(2) 생성한 뷰를 이용하여 판매된 도서의 이름과 고객의 이름을 출력하는 SQL문 작성하기</p>
<pre><code class="language-sql">SELECT bookname, name
FROM highorders;</code></pre>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/c92b031a-0625-4217-af5f-9a1f9dcecbcf/image.png" alt=""></p>
<p>(3) highorders 뷰를 변경하고자한다. 판매가격 속성을 삭제하는 명령을 수행하시오. 삭제 후 (2)번 SQL문 재수행.</p>
<pre><code class="language-sql">CREATE OR REPLACE VIEW highorders
AS SELECT B.bookid, B.bookname, C.name, B.publisher
   FROM book B JOIN
        orders O ON B.bookid = O.bookid JOIN
        customer C ON C.custid = O.custid
   WHERE B.price &gt;= 20000;</code></pre>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/c1aa0be9-9a14-4956-ac7f-5c4aaa3d533e/image.png" alt=""></p>
<hr>
<h1 id="4-인덱스">4. 인덱스</h1>
<h2 id="4-1-데이터베이스의-물리적-저장">4-1. 데이터베이스의 물리적 저장</h2>
<p>데이터베이스의 경우 각 DBMS 만의 고유한 방식으로 테이블에 저장되는 데이터를 저장함.</p>
<img src="https://velog.velcdn.com/images/summeryoung_/post/d4cbdbae-cffc-4220-b6b5-2a4a28a13d7b/image.png" width=60%>

<p>DBMS가 하드디스크에 데이터를 저장하고 읽어오면 속도가 느리기에, 주기억장치에 사용하는 공간 중 일부를 버퍼 풀로 만들어 사용함. 데이터 검색 시 이 버퍼 풀에 저장된 데이터를 우선 읽어들여 작업을 진행함.</p>
<p><code>SHOW VARIABLES LIKE &#39;datadir&#39;</code>를 통해 데이터베이스가 저장된 위치를 알아볼 수 있음</p>
<hr>
<h2 id="4-2-인덱스와-b-tree">4-2. 인덱스와 B-tree</h2>
<h3 id="인덱스">인덱스</h3>
<p>데이터를 쉽고 빠르게 찾을 수 있도록 만든 데이터 구조로, 일반적인 RDBMS의 인덱스는 대부분 B-tree(B+Tree) 구조로 이루어져 있음.</p>
<h3 id="b-tree">B-tree</h3>
<ul>
<li><strong>데이터 검색 시간</strong>을 단축하기 위한 자료구조</li>
<li>B-tree의 각 <strong>노드는 키값과 포인터</strong>를 가짐</li>
<li>루트노드, 내부노드, 리프노드로 구성</li>
<li>리프노드가 같은 레벨에 존재하는 균형 트리</li>
<li>리프노드에는 해당 데이터의 저장 위치에 대응하는 정보가 있어 빠르게 검색할 수 있음.</li>
<li>B Treed와 B-Tree는 balanced tree.</li>
</ul>
<img src="https://velog.velcdn.com/images/summeryoung_/post/79524279-9e38-4a32-aa0b-2157eb2fc782/image.png" width=80%>

<ul>
<li>B-tree에서의 검색: 루트 노드에서부터 값을 비교 -&gt; 중간 단계(내부 노드)에서 해당 노드를 찾음 -&gt; 최종적으로 마지막 레벨인 리프 노드에 도달</li>
</ul>
<h4 id="인덱스의-특징">인덱스의 특징</h4>
<ul>
<li>인덱스는 테이블에서 한 개 이상의 속성을 이용해 생성</li>
<li><strong>빠른 검색</strong>과 효율적인 레코드 접근이 가능</li>
<li>순서대로 정렬된 속성과 데이터의 위치만 보유하기에 테이블보다 작은 공간 차지</li>
<li>저장된 값들은 테이블의 부분집합이 됨</li>
<li>데이터에서 수정, 삭제 등의 변경이 발생하면 인덱스를 재구성해야함.</li>
</ul>
<hr>
<h2 id="4-3-mysql의-인덱스">4-3. MySQL의 인덱스</h2>
<p>MySQL의 인덱스는 <strong>클러스터 인덱스와 보조 인덱스</strong>로 나뉨</p>
<h3 id="클러스터-인덱스">클러스터 인덱스</h3>
<ul>
<li>테이블 자체가 인덱스 구조로 되어있어, 데이터가 인덱스 키 순서대로 물리적 저장됨</li>
<li>기본키 생성 시 자동으로 생성</li>
</ul>
<h4 id="특징-2">특징</h4>
<ul>
<li><strong>테이블 당 하나만 존재</strong></li>
<li>키 값이 정렬되어 특정값, 범위 검색에 유리</li>
<li>기본키 기반 검색이 매우 빠름</li>
<li>검색 성능은 뛰어나지만, 키 변경/삽입 시 성능 부담</li>
</ul>
<h3 id="보조-인덱스">보조 인덱스</h3>
<ul>
<li>클러스터 인덱스가 아닌 모든 인덱스</li>
<li>InnoDB에서는 PK(기본키)가 클러스터 인덱스이고, 나머지는 모두 보조 인덱스임.</li>
</ul>
<h4 id="특징-3">특징</h4>
<ul>
<li><p><strong>데이터 자체를 정렬하지 않고, 테이블 당 여러 개를 만들 수 있음</strong></p>
</li>
<li><p>인덱스 키와 해당 레코드의 기본키값을 저장해 검색될 수 있게 함</p>
<ul>
<li>즉, 인덱스의 리프 노드 = 테이블 상 데이터 위치를 지정하는 row id</li>
</ul>
</li>
</ul>
<h3 id="mysql-인덱스">MySQL 인덱스</h3>
<p>클러스터 인덱스와 보조 인덱스를 동시에 사용하는 검색.</p>
<p>예) 기본키인 <code>bookid</code>는 클러스터 인덱스, 외에 추가적으로 <code>bookname</code>을 보조 인덱스로 설정</p>
<h4 id="비교-정리">비교 정리</h4>
<table>
<thead>
<tr>
<th align="center">인덱스 명칭</th>
<th align="center">설명/생성 예</th>
</tr>
</thead>
<tbody><tr>
<td align="center"><strong>클러스터 인덱스</strong></td>
<td align="center">- <strong>기본적인 인덱스</strong>. 테이블 생성 시 기본 키 지정하면, 기본 키에 대해 클러스터 인덱스를 생성. <br> - 기본키 지정하지 않을 시에는 먼저 나오는 UNIQUE 속성에 대해 생성 <br> - 기본키/UNIQUE 모두 없을 시에는 MySQL 자체 생성 행번호로 생성 <br> - <strong>테이블 당 1개만 생성</strong></td>
</tr>
<tr>
<td align="center"><strong>보조 인덱스</strong></td>
<td align="center">- 클러스터 인덱스 외 모든 인덱스. 각 레코도는 보조 인덱스의 속성과 기본키 속성값을 가짐 <br> - 보조 인덱스 검색 -&gt; 기본 키 속성을 찾음 -&gt; 이후 클러스터 인덱스로 가서 해당 레코드를 찾는 방식. <br> -<strong>테이블 당 여러 개를 생성할 수 있음</strong></td>
</tr>
</tbody></table>
<hr>
<h2 id="4-4-인덱스의-생성">4-4. 인덱스의 생성</h2>
<p>의미없이 인덱스를 생성할 경우에는 오히려 검색속도가 더 느려지고 공간이 낭비됨</p>
<blockquote>
<h4 id="인덱스-생성-전-고려사항">인덱스 생성 전 고려사항</h4>
</blockquote>
<ul>
<li>인덱스는 <strong>WHERE절</strong>에서  자주 사용되는 속성이어야함</li>
<li>인덱스는 <strong>JOIN</strong>에 자주 사용되는 속성이어야함</li>
<li>단일 테이블에 인덱스가 많으면 속도가 느려질 수 있음 (테이블 당 4-5개 권장)</li>
<li>속성이 가공되는 경우에는 사용X
  예) YEAR(birth) = 2026</li>
<li>속성의 선택도가 낮을 때 유리 ( 즉, 속성의 모든 값이 다른 경우)
  예) 주민번호 - 선택도가 낮음 (고유한 값, 중복 없음), <pre><code>  성별 - 선택도 높음 (중복 많음)</code></pre></li>
</ul>
<h3 id="인덱스-생성-방법">인덱스 생성 방법</h3>
<pre><code class="language-sql">CREATE [UNIQUE] INDEX [인덱스 이름]
ON &lt;테이브 이름&gt; (컬럼 [ASC|DESC], ...)</code></pre>
<ul>
<li>UNIQUE: 테이블 속성값에 대해 중복이 없는 유일한 인덱스를 생성하는 것</li>
<li>ASC |DESC: 컬럼 값의 정렬 방식 의미.</li>
</ul>
<p>예)</p>
<pre><code class="language-sql">CREATE INDEX ix_Book ON book(publisher, price);</code></pre>
<h4 id="인덱스-미사용">인덱스 미사용</h4>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/e725d16b-ca6a-4f82-8ebf-2cb9afbb339d/image.png" alt=""></p>
<h4 id="인덱스-사용">인덱스 사용</h4>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/c6676c08-333d-454e-9933-b356a416ed30/image.png" alt=""></p>
<hr>
<h2 id="4-5-인덱스-재구성과-삭제">4-5. 인덱스 재구성과 삭제</h2>
<h3 id="인덱스--재구성">인덱스  재구성</h3>
<ul>
<li><p>B-tree 인덱스의 경우 데이터 수정/삭제/삽입이 잦으면 노드의 갱신이 주기적으로 일어나 단편화 현상이 일어남</p>
</li>
<li><p>)<strong>단편화(Fragmentation)</strong>: 데이터가 저장된 공간이 효율적으로 채워지지 않고 군데군데 빈틈(구멍)이 생겨 성능이 떨어지는 현상</p>
</li>
<li><p>이 경우, ANALYZE 문법을 통해 인덱스를 다시 생성해줌</p>
</li>
</ul>
<pre><code class="language-sql">ANALYZE TALBE book;</code></pre>
<h3 id="인덱스-삭제">인덱스 삭제</h3>
<pre><code class="language-sql">DROP INDEX ix_Book ON book;</code></pre>
<hr>
<h1 id="1-데이터베이스-프로그래밍의-개념">1. 데이터베이스 프로그래밍의 개념</h1>
<h3 id="데이터베이스-프로그래밍">데이터베이스 프로그래밍</h3>
<ul>
<li>DBMS에 데이터를 정의하고 저장된 데이터를 읽어와 데이터를 변경하는 프로그램을 작성하는 과정</li>
<li>데이터베이스 언어인 SQL을 포함하는 점이 일반 프로그래밍과의 차이점.</li>
</ul>
<h3 id="방법">방법</h3>
<ul>
<li>SQL 전용 언어 사용</li>
<li>일반 프로그래밍 언어에 SQL 삽입해 사용하기</li>
<li>웹 프로그래밍 언어에 SQL 삽입해 사용하기</li>
<li>4GL: 데이터베이스 관리 가능 비주얼 프로그래밍 기능을 갖춘 GUI 기반 소프트웨어 개발 도구 사용</li>
</ul>
<h4 id="데이터베이스-응용-시스템---하드웨어---운영체제---dbms--프로그램-환경으로-계층화">데이터베이스 응용 시스템 -&gt; &#39;하드웨어 - 운영체제 - DBMS -프로그램 환경&#39;으로 계층화</h4>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/866fbfab-8360-47f2-ab99-2b4bbb21dd26/image.png" alt=""></p>
<hr>
<h1 id="2-저장-프로그램">2. 저장 프로그램</h1>
<h2 id="2-1-저장-프로그램">2-1. 저장 프로그램</h2>
<h3 id="저장-프로그램">저장 프로그램</h3>
<p>프로그램 로직을 <strong>프로시저</strong>로 구현해 객체 형태로 사용</p>
<ul>
<li>프로시저가 정의된 후 MySQL(DBMS)에 저장되기에 저장 프로그램이라 함</li>
<li>MySQL에서 저장프로그램을 정의하는과정:
  프로그램 정의 -&gt; 실행 -&gt; 실행 결과 -&gt; 개체확인</li>
</ul>
<h3 id="create-procedure">CREATE PROCEDURE</h3>
<ul>
<li>프로시저는 <strong>선언부와 실행부(BEGIN-END)</strong>로 구성</li>
<li>선언부: 변수와 매개변수 선언, 실행부: 프로그램 로직 구현</li>
<li>매개변수: 저장 프로시저가 호출될 때 해당 프로시저에 전달되는 값</li>
<li>변수: 저장 프로시저/트리거 내에서 사용되는 값</li>
<li>제어문 사용 가능.</li>
</ul>
<blockquote>
<h4 id="저장프로그램의-제어문">저장프로그램의 제어문</h4>
</blockquote>
<ul>
<li><code>DELIMITER</code>: 구문 종료 기호를 설정</li>
<li><code>BEGIN-END</code>: 프로그램문을 블록화. 중첩 가능</li>
<li><code>IF-ELSE</code>: 조건의 검삭 결과에 따라 문장 실행</li>
<li><code>RETURN</code>: 프로시저 종료 및 상태값 반환.</li>
<li><code>LOOP</code>: LEAVE 문을 만나기 전까지 LOOP 반복</li>
<li><code>WHILE</code>: 조건이 참일 경우 WHILE문읠 블록 실행</li>
<li><code>REPEAT</code>: 조건이 참일 경우 REPEAT 문의 블록 실행</li>
</ul>
<h3 id="프로시저procedure-실습">프로시저(PROCEDURE) 실습</h3>
<ul>
<li>DELIMITER 뒤에 오는 기호는 자유롭게 지정 가능 
  예: $$, //, ### 등</li>
</ul>
<h4 id="프로시저-생성">프로시저 생성</h4>
<pre><code class="language-sql">DELIMITER $$

CREATE PROCEDURE getAge(
    -- 선언부
    IN p_id INt,
    OUT p_name VARCHAR(50),
    OUT p_age INT
)

-- 실행부
BEGIN
    SELECT 이름, 나이 INTO p_name, p_age -- SELECT 후, 해당 값 전달 위치 지정해줘야함
    FROM person
    WHERE id = p_id; -- 쿼리의 마지막이니 ;(세미콜론) 필요
END $$ 

DELIMITER ;</code></pre>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/2ac3482a-5dc1-42e2-a9b5-a00ba9c7f67c/image.png" alt=""></p>
<h4 id="생성한-프로시저-사용">생성한 프로시저 사용</h4>
<pre><code class="language-sql">CALL getAge(2, @NAME, @age);
SELECT @NAME, @age;</code></pre>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/d42f62e8-56e9-40be-bd93-55de19274b2e/image.png" alt=""></p>
<hr>
<h2 id="2-2-트리거">2-2. 트리거</h2>
<h3 id="트리거">트리거</h3>
<p>테이블에 대한 <strong>특정 동작</strong>(INSERT, UPDATE, DELETE)이 수행될 때 <strong>자동으로 실행되는 저장된 프로그램</strong></p>
<h4 id="사용목적">사용목적</h4>
<ul>
<li><strong>데이터 무결성 유지</strong>: 잘못된 값 입력 방지, 자동 검증</li>
<li>자동 처리: 로그 기록, 변경 이력 관리</li>
<li>비즈니스 규칙 적용: 특정 조건 만족 시 자동 계산, 알림 처리</li>
<li>연관 테이블 동기화: 다른 테이블에 자동 반영</li>
</ul>
<h4 id="주의사항">주의사항</h4>
<ul>
<li>트리거는 자동 실행되기에 디버깅이 어렵고, 잘못 작성 시 성능 저하 우려</li>
<li>너무 많은 로직을 트리거에 넣으면 관리가 어려워짐</li>
<li>트리거는 트랜잭션과 밀접한 관련이 있기에 롤백 시 함께 취소됨.</li>
</ul>
<h3 id="트리거-실습">트리거 실습</h3>
<p>예) person 테이블에 새로운 행이 삽입될 때, 같은 이름이 이미 존재하면 로그 테이블에 기록하는 트리거 생성</p>
<pre><code class="language-sql">DELIMITER $$
CREATE TRIGGER check_duplicate_name
BEFORE INSERT ON person -- person 테이블에 삽입하기 전에
FOR EACH ROW
BEGIN
    DECLARE cnt INT;

    SELECT COUNT(*) INTO cnt
    FROM person
    WHERE 이름 = NEW.이름;

    IF cnt &gt; 0 THEN
        INSERT INTO person_alert (이름, action_time, message)
        VALUES (NEW.이름, NOW(), &#39;동명이인&#39;);
    END IF;

END $$
DELIMITER ;</code></pre>
<img src="https://velog.velcdn.com/images/summeryoung_/post/6e181bd9-4829-4233-9dc8-62c3455b4de9/image.png" width=50%>

<p>MySQLWorkbench의 경우 트리거를 생성한 테이블 아래에 생성된 트리거가 보인다.</p>
<h4 id="테스트-이름이-중복인-튜플-넣기">테스트: 이름이 중복인 튜플 넣기</h4>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/76849c5d-7194-4b69-be70-6547eecc8e42/image.png" alt=""></p>
<hr>
<h2 id="2-3-사용자-정의-함수">2-3. 사용자 정의 함수</h2>
<h3 id="사용자-정의-함수">사용자 정의 함수</h3>
<p>개발자가 직접 작성하여 SQL 내에서 호출할 수 있는 함수.</p>
<ul>
<li>기본 제공 함수로 해결하기 어려운 로직을 캡슐화할 때 유용</li>
<li>SQL에서 자주 쓰는 계산이나 로직을 자신이 만든 함수로 등록해두고 필요할 때 호출</li>
</ul>
<h4 id="종류">종류</h4>
<ul>
<li>스칼라 함수: MySQL에서 일반적</li>
<li>테이블 반환 함수</li>
<li>인라인 함수</li>
</ul>
<h4 id="장점-1">장점</h4>
<ul>
<li>SQL 코드 간결화</li>
<li>유지보수에 용이</li>
<li>재사용성 증가</li>
</ul>
<h4 id="단점">단점</h4>
<ul>
<li>잘못 사용 시 성능 저하 가능</li>
</ul>
<h3 id="사용자-정의-함수-실습">사용자 정의 함수 실습</h3>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/cabe8874-2f41-41de-82c6-40bccbb0146e/image.png" alt=""></p>
<pre><code class="language-sql">DELIMITER //
CREATE FUNCTION fnGetAge (birthdate DATE)
RETURNS INT
DETERMINISTIC
BEGIN
    DECLARE age INT;
    RETURN TIMESTAMPDIFF(YEAR, birthdate, CURDATE());
END //
DELIMITER ;

SELECT fnGetAge(&#39;2004-08-13&#39;) AS age;</code></pre>
<hr>
<h2 id="2-4-저장-프로그램의-특징">2-4. 저장 프로그램의 특징</h2>
<h4 id="프로시저">프로시저</h4>
<p>여러 SQL문의 묶음으로 단순 반복 업무에 권장. 복잡한 로직을 DB에 몰아넣으면 안됨.</p>
<h4 id="트리거-1">트리거</h4>
<p>특정 이벤트가 발생할 때 자동으로 실행. 디버깅, 예측 불가능한 성능 저하 문제 존재.</p>
<h4 id="사용자-정의-함수-1">사용자 정의 함수</h4>
<p>입력을 받아 단일값 또는 테이블 반환. 복잡한 연산을 함수로 감싸면 쿼리 최적화를 방해할 수 있음.</p>
<h3 id="비교">비교</h3>
<table>
<thead>
<tr>
<th align="center">구분</th>
<th align="center">프로시저</th>
<th align="center">트리거</th>
<th align="center">사용자 정의 함수</th>
</tr>
</thead>
<tbody><tr>
<td align="center">정의 방법</td>
<td align="center"><code>CREATE PROCEDURE</code></td>
<td align="center"><code>CREATE TRIGGER</code></td>
<td align="center"><code>CREATE FUNCTION</code></td>
</tr>
<tr>
<td align="center">호출 방법</td>
<td align="center"><code>CALL</code> 문으로 직접 호출</td>
<td align="center"><code>INSERT</code>, <code>DELETE</code>, <code>UPDATE</code> 문 실행 시 자동 실행</td>
<td align="center"><code>SELECT</code> 문에 포함됨</td>
</tr>
<tr>
<td align="center">기능 차이</td>
<td align="center">SQL문으로 할 수 없는 복잡한 로직 수행</td>
<td align="center">기본값 제공, 데이터 제약 준수, SQL 뷰의 수정, 참조무결성 작업 수행</td>
<td align="center">속성값을 가공해 반환, SQL문 내에서 직접 사용</td>
</tr>
</tbody></table>
<br>

<h3 id="실제-사용-예시">실제 사용 예시</h3>
<h4 id="트리거-2">트리거</h4>
<ul>
<li>히스토리(감사) 로그 기록: 고객 등급/결제 상태 변경 시 로그 테이블에 기록</li>
<li>통계 데이터 실시간 갱신: 게시글 증가 시, 총 게시글 수 증가</li>
</ul>
<h4 id="프로시저-1">프로시저</h4>
<ul>
<li>복잡한 주문/결제 처리: 주문 내역 생성 -&gt; 재고 차감 -&gt; 포인트 적립 -&gt; 쿠폰 사용 처리&#39;</li>
<li>배치 데이터 처리: 매일 새벽 2시, 유효기간 지난 미사용 쿠폰 일괄 만료 상태 변경</li>
</ul>
<h4 id="함수">함수</h4>
<ul>
<li>비즈니스 로직 계산: 상품 가격&amp;고객 등급 -&gt; 최종 할인 가격 반환</li>
<li>데이터 포맷팅/마스킹: 01012345678 입력시 010-1234-5678로 변환 또는 이름 홍*동으로 마스킹</li>
</ul>
<hr>
<h1 id="3-데이터베이스-연동-프로그래밍">3. 데이터베이스 연동 프로그래밍</h1>
<h4 id="java-주요-클래스">Java 주요 클래스</h4>
<ul>
<li><p><code>DriverManager</code>: </p>
<ul>
<li><code>getConnection()</code></li>
<li>DBMS 드라이버를 로드하고 연결 객체 생성</li>
</ul>
</li>
<li><p><code>Connection</code>: </p>
<ul>
<li><code>createStatement()</code>, <code>prepareStatement()</code>, <code>close()</code></li>
<li>특정 데이터베이스와 연결 세션을 관리. SQL 실행 객체 생성</li>
</ul>
</li>
<li><p><code>Statement</code>: </p>
<ul>
<li><code>executeQuery()</code>, <code>executeUpdate()</code></li>
<li>SQL문을 DB에 전달. 캐싱되지 않기에 정적 SQL 실행 시 주로 사용.</li>
</ul>
</li>
<li><p><code>PreparedStatement</code>: </p>
<ul>
<li><code>set...()</code>, <code>executeQuery()</code>, <code>executeUpdate()</code> </li>
<li>SQL을 미리 컴파일해 성능을 높이고, 매개변수를 사용해 보안 강화(SQL Injection 방지)</li>
</ul>
</li>
<li><p><code>ResultSet</code></p>
<ul>
<li><code>next()</code>, <code>get...()</code>, <code>close()</code></li>
<li>SELECT 쿼리 실행 결과를 테이블 형태로 저장, 커서를 이동시켜 데이터를 한 줄씩 조회</li>
</ul>
</li>
<li><p><code>SQLException</code></p>
<ul>
<li><code>getMessage()</code>, <code>getErrorCode()</code></li>
<li>DB 연동 과정에서 발생하는 예외상황에 대한 정보 제공</li>
</ul>
</li>
</ul>
<h3 id="연동">연동</h3>
<h4 id="연동-순서">연동 순서</h4>
<p>드라이버 로드 -&gt; Connection 생성 -&gt; Statement 생성 및 실행 -&gt; ResultSet 처리 -&gt; 자원 반납.</p>
<h4 id="preparedstatment-권장">PreparedStatment 권장</h4>
<p>보안, 가독성, 성능의 이유로 권장됨</p>
<h4 id="자원-반납try-with-resources">자원 반납(Try-with-resources)</h4>
<p><code>close()</code>를 일일이 호출하지 않아도 자동으로 자원을 닫아주는 구문.</p>
<hr>
<h2 id="solid-원칙">SOLID 원칙</h2>
<h3 id="solid란">SOLID란?</h3>
<p>SOLID  원칙이란 변화에 강하고 재사용에 유리한 클래스 구조를 만드는 법칙이자 원칙이라고한다. 앞에서 정리한 클린코드를 위해서 지켜야하는 법칙과도 같다. 이 원칙을 지키면서 코딩하면, 유지보수 및 확장이 편리하며 재사용성이 높고 테스트가 쉬운 코드를 작성할 수 있다.</p>
<table>
<thead>
<tr>
<th align="center"></th>
<th align="center"></th>
<th align="center"></th>
</tr>
</thead>
<tbody><tr>
<td align="center">S</td>
<td align="center">Single Responsibility</td>
<td align="center">단일 책임 원칙</td>
</tr>
<tr>
<td align="center">O</td>
<td align="center">Open/Closed</td>
<td align="center">개방-폐쇄 원칙</td>
</tr>
<tr>
<td align="center">L</td>
<td align="center">Liskov Substitution</td>
<td align="center">리스코프 치환 원칙</td>
</tr>
<tr>
<td align="center">I</td>
<td align="center">Interface Segregation</td>
<td align="center">인터페이스 분리 원칙</td>
</tr>
<tr>
<td align="center">D</td>
<td align="center">Dependency Inversion</td>
<td align="center">의존성 역전 원칙</td>
</tr>
</tbody></table>
<h3 id="jdbc에서의-solid">JDBC에서의 SOLID</h3>
<h4 id="srp단일-책임-원칙">SRP(단일 책임 원칙)</h4>
<p>하나의 클래스는 하나의 책임만을 가져야함.
DAO, DTO, View, Controller로 분리해야함</p>
<h4 id="ocp-개방-폐쇄-원칙">OCP (개방-폐쇄 원칙)</h4>
<p>확장에는 열려있고, 변경에는 닫혀있어야함
인터페이스 기반 DAO</p>
<h4 id="lsp-리스코프-치환-원칙">LSP (리스코프 치환 원칙)</h4>
<p>자식 클래스는 부모 클래스를 대체할 수 있어야함.
다형성 활용.</p>
<h4 id="isp-인터페이스-분리-원칙">ISP (인터페이스 분리 원칙)</h4>
<p>불필요한 인터페이스 의존은 피해야함.
역할별 인터페이스, DAO 인터페이스 정의</p>
<h4 id="dip-의존성-역전-원칙">DIP (의존성 역전 원칙)</h4>
<p>고수준 모듈은 저수준 모듈에 의존하면 안됨.
Service -&gt; DAO 인터페이스 주입.</p>
<h2 id="dao와-dto">DAO와 DTO</h2>
<p>View(입출력), Controller(흐름 제어), Service(비즈니스 로직 처리)와 함께 사용됨.
DAO에서 DB와 관련된 작업이 처리되거나 테이블의 데이터와 매핑되며, DTO는 이런 데이터를 전달하거나 받을 때 사용함.</p>
<h3 id="daomodel">DAO(Model)</h3>
<p>DB 접근 전담 CRUD 처리 클래스(JDBC를 사용하여 SQL 실행)로 DAO 인터페이스를 도입할수도 있음.
DB와 직접 통신한다.
예) 학생 정보 객체 </p>
<h3 id="dtodata-transter-object">DTO(Data Transter Object)</h3>
<p>데이터를 담아 다른계층으로 전달. 가볍고 단순해야 함.</p>
<hr>
<h2 id="jdbc-실습">JDBC 실습</h2>
<h3 id="jdbc">JDBC</h3>
<p>: 자바 애플리케이션이 데이터베이스에 연결하고 SQL문을 실행할 수 있도록 제공됟는 표준 API</p>
<p>과거 DB마다 접속 방법이 달랐으나, JDBC라는 통일된 인터페이스와 각 DB 제조사의 JDBC 드라이버 제공으로 <strong>JDBC 드라이버만 교체</strong>하면 자바 코드의 변경 없이 다양한 종류의 데이터베이스와 통신이 가능해짐.</p>
<h3 id="1-driver-라이브러리-추가">1. Driver 라이브러리 추가</h3>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/df172217-dba0-4c5a-b149-53cfa52f8891/image.png" alt=""></p>
<h3 id="2-데이터-베이스-연결">2. 데이터 베이스 연결</h3>
<pre><code class="language-java">String url = &quot;jdbc:mysql://127.0.0.1:3307/madangdb&quot;;
String username = &quot;&quot;;
String password = &quot;&quot;;

try (Connection connection = DriverManager.getConnection(url, username, password)) {
    ...
} catch (SQLException e) {
    System.out.println(&quot;DB연결 실패&quot;);
}
</code></pre>
<h3 id="3-sql문-실행">3. SQL문 실행</h3>
<ul>
<li>Statement/PreparedStatement</li>
<li>Connection 객체로부터 SQL을 실행할 수 있는 <code>Statement</code> 또는 <code>PreparedStatement</code> 객체를 생성.</li>
<li><code>statement</code>: 정적인 SQL문 실행에 사용</li>
<li><code>preparedStatement</code>: 성능과 보안 때문에 사용 권장.</li>
</ul>
<h3 id="4-결과처리-resultset">4. 결과처리 (ResultSet)</h3>
<ul>
<li>SELECT문을 실행한 후 반환되는 <code>ResultSet</code> 객체는 조회된 데이터의 집합</li>
<li><code>next()</code> 메소드를 호출해 한 행씩 이동하며 데이터를 읽어옴.</li>
</ul>
<hr>
<h2 id="jdbc-실습-결과">JDBC 실습 결과</h2>
<p>madangdb에서 만든 book, customer 데이터베이스에서 select 및 insert 쿼리 실행.</p>
<pre><code class="language-java">import java.sql.*;

public class Main {
    public static void main(String[] args) {
        String url = &quot;jdbc:mysql://127.0.0.1:3307/madangdb&quot;;
        String username = &quot;&quot;;
        String password = &quot;&quot;;

        try (Connection connection = DriverManager.getConnection(url, username, password)) {
            String selectSql = &quot;SELECT * FROM book&quot;;

            try (Statement stmt = connection.createStatement();
            ResultSet rs = stmt.executeQuery(selectSql)) {
                while (rs.next()) {
                    int bookid = rs.getInt(&quot;bookid&quot;);
                    String bookname = rs.getString(&quot;bookname&quot;);
                    String publisher = rs.getString(&quot;publisher&quot;);
                    int price = rs.getInt(&quot;price&quot;);

                    String result = &quot;bookid: &quot;+ bookid;
                    result += &quot; bookname: &quot;+ bookname;
                    result += &quot; publisher: &quot;+ publisher;
                    result += &quot; price: &quot;+price;

                    System.out.println(result);
                }
            }

            String insertSql = &quot;INSERT INTO customer (custid, name, address, phone) VALUES(?,?,?,?)&quot;;
            try (PreparedStatement pstmt = connection.prepareStatement(insertSql)) {
                pstmt.setInt(1,13);
                pstmt.setString(2, &quot;김이화&quot;);
                pstmt.setString(3, &quot;대한민국 서울&quot;);
                pstmt.setString(4,&quot;010-1111-2222&quot;);
                pstmt.executeUpdate();
            }
        } catch (SQLException e){
            System.out.println(&quot;연결 오류&quot;);
        }
    }
}</code></pre>
<h4 id="실습코드-사진">실습코드 사진</h4>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/63e59f3f-9ee1-43ef-b68d-a8761fab1fbd/image.png" alt=""></p>
<h3 id="실습-결과">실습 결과</h3>
<h4 id="select-쿼리">select 쿼리</h4>
<img src="https://velog.velcdn.com/images/summeryoung_/post/5e12c2e0-7f17-48b9-abf8-014b3aa7f0f6/image.png" width=80%>

<h4 id="insert-쿼리">insert 쿼리</h4>
<pre><code class="language-java">pstmt.setInt(1,13);
                pstmt.setString(2, &quot;김이화&quot;);
                pstmt.setString(3, &quot;대한민국 서울&quot;);
                pstmt.setString(4,&quot;010-1111-2222&quot;);
                pstmt.executeUpdate();</code></pre>
<img src="https://velog.velcdn.com/images/summeryoung_/post/bac2cbe9-1105-4c7a-969f-11c0b7d38000/image.png" width=70%>

]]></description>
        </item>
        <item>
            <title><![CDATA[데이터베이스 5주차 SQL 문제 풀이]]></title>
            <link>https://velog.io/@summeryoung_/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-5%EC%A3%BC%EC%B0%A8-SQL-%EB%AC%B8%EC%A0%9C-%ED%92%80%EC%9D%B4</link>
            <guid>https://velog.io/@summeryoung_/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-5%EC%A3%BC%EC%B0%A8-SQL-%EB%AC%B8%EC%A0%9C-%ED%92%80%EC%9D%B4</guid>
            <pubDate>Sat, 04 Apr 2026 09:20:27 GMT</pubDate>
            <description><![CDATA[<blockquote>
<h4 id="q1-클래스-문제1-거래-상태에-따른-출력-및-날짜-비교">Q1. 클래스 문제1: 거래 상태에 따른 출력 및 날짜 비교</h4>
</blockquote>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/8e5fdbee-3409-4c33-9e32-c2aed313e49d/image.png" alt=""></p>
<pre><code class="language-sql">SELECT board_id, writer_id, title, price,
       CASE status
        WHEN &#39;SALE&#39; THEN &#39;판매중&#39;
        WHEN &#39;RESERVED&#39; THEN &#39;예약중&#39;
        WHEN &#39;DONE&#39; THEN &#39;거래완료&#39;
        ELSE status
       END AS STATUS
FROM used_goods_board
WHERE created_date = &#39;2022-10-05&#39;
ORDER BY board_id DESC;</code></pre>
<p>중고 거래 게시글 중에서 생성일자를 기준으로 한 번 거르고, 거래 상태에 따라 판매중/예약중/거래완료를 표기하는 문제였다.</p>
<p>날짜를 비교하는 부분은 날짜 그대로 비교하는게 성능적으로 더 좋다기에 <code>created_date = &#39;2022-10-05&#39;</code>와 같이 날짜 그대로 두고 비교하였다.</p>
<p>또 거래 상태가 SALE/RESERVED/DONE일 때에 따라 다른 식으로 표기해줘야하는 부분은 <code>CASE WHEN</code>을 사용했다. <code>WHEN</code> 뒤에 조건식을 써도 되지만, 단일값의 경우가 동일한지 (<code>=</code> 이 연산 비교) 에는 C언어나 다른 언어의 <code>switch</code>문처럼 CASE에 속성을 적고, WHEN 뒤에 값을 적어주면 되기에 그렇게 작성해주었다. </p>
<hr>
<blockquote>
<h4 id="q2-클래스-문제2-10월-생성된-글-및-댓글-조회">Q2. 클래스 문제2: 10월 생성된 글 및 댓글 조회</h4>
</blockquote>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/7134880d-ba94-4e51-8300-ad7c41d2cc58/image.png" alt=""></p>
<pre><code class="language-sql">SELECT B.title, B.board_id, 
       R.reply_id, R.writer_id, R.contents, 
       DATE_FORMAT(R.created_date, &#39;%Y-%m-%d&#39;)
FROM used_goods_board B JOIN
     used_goods_reply R ON
     B.board_id = R.board_id
WHERE MONTH(B.created_date) = &#39;10&#39;
ORDER BY R.created_date ASC, B.title;</code></pre>
<p>댓글 테이블과 게시글 테이블이 있었고, 댓글 테이블에서 게시글 id를 외래키로 가지면서 관계를 맺고 있는 식이었다.</p>
<p>10월에 만들어진 게시글과 그 댓글 등의 정보를 조회해서 출력하는 문제여서 일단은 <code>MONTH()</code>를 써서 비교했는데...? 이게 연도 조건이 있었던걸로 기억하는데 이렇게 풀면 연도는 사실 상관없이 10월에 만든 글이면 다 출력될텐데...? 어떻게 통과가 된 것 같다.</p>
<p>실제로는 <code>WHERE MONTH(B.created_date) = &#39;10&#39;</code> 이런식으로 쓰지 않고 그래서
<code>B.created_date BEWTEEN &#39;2022-10-01&#39; AND &#39;2022-10-31&#39;</code> 이렇게 쓰는게 더 좋을 것 같다. 또 찾아보니까, <code>B.created_date LIKE &#39;2022-10%&#39;</code> 이렇게 써도 10월인 날짜들만 걸러지는 것 같은데 이건 문자열로 비교하는거라 성능이 별로...다.</p>
<hr>
<blockquote>
<h4 id="q3-클래스-문제3">Q3. 클래스 문제3:</h4>
</blockquote>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/6c19d450-30a2-45d1-bb33-e9449c779fa5/image.png" alt=""></p>
<pre><code class="language-sql">-- 코드를 입력하세요
SELECT B.book_id, A.author_name, 
       DATE_FORMAT(B.published_date, &#39;%Y-%m-%d&#39;) AS PUBLISHED_DATE
FROM book B JOIN
     author A ON
     B.author_id = A.author_id
WHERE B.category =  &#39;경제&#39;
ORDER BY B.published_date;</code></pre>
<p>크게 복잡한건 없었고, 테이블 두 개를 JOIN으로 잘 연결한 후에 카테고리가 경제인 것들만 남기고, 날짜 형식만 요구대로 잘 조정해주었다.</p>
<p>날짜 형식은 <code>DATE_FORMAT()</code> 사용해서 연월일만 출력하도록 조정해주었다. </p>
<hr>
<blockquote>
<h4 id="q4-클래스-문제4-상품의-코드-앞-자리-두-개를-기준으로-분류">Q4. 클래스 문제4: 상품의 코드 앞 자리 두 개를 기준으로 분류</h4>
</blockquote>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/5175eda2-247e-40e5-86b9-7372b721a1c2/image.png" alt=""></p>
<pre><code class="language-sql">SELECT LEFT(product_code,2) AS category, 
       COUNT(DISTINCT product_id) AS product
FROM product P
GROUP BY category
ORDER BY category;</code></pre>
<p>처음에는 순간 앞 자리 두 개로 어떻게 분류해야하지? 했고, 약간 테이블을 하나 만들거나 서브 쿼리를 만들어야하나 생각했는데, <code>LEFT()</code>를 써서 분리하고 이걸 기준으로 그루핑하고, 정렬하면 되는 것이었다. 정렬은 SELECT 후에 진행되니 SELECT에서 만든 별칭으로 정렬해도 되고.</p>
<p><code>LEFT(속성,2)</code> 이렇게 해서 우선 상품 코드의 앞 두 개를 카테고리로 SELECT에서 정의해주었다.</p>
<p>그리고 위에서는 GROUP BY에 별칭 사용하긴 했는데 MySQL이나 MariaDB에서는 이렇게 GROUP BY에서 SELECT에서 사용한 별칭을 사용하는걸 허용하는데 Oracle에서는 허용하지 않는다고 하니 만약에 표준적으로 사용해야하는걸 생각하면</p>
<pre><code class="language-sql">GROUP BY LEFT(product_code,2)</code></pre>
<p>이렇게 적어야할 것 같았는데, Oracle에서는 LEFT도 없다고 하니? 그것조차 SUBSTR으로 바꾸어 주어야할 것 같다...</p>
<hr>
<blockquote>
<h4 id="q5-클래스-문제5-상품-출고-날짜를-기준으로-출력">Q5. 클래스 문제5: 상품 출고 날짜를 기준으로 출력</h4>
</blockquote>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/daa05c23-c4c9-4880-9242-79af79684b11/image.png" alt=""></p>
<pre><code class="language-sql">SELECT order_id, product_id, COALESCE(out_date,&#39; &#39;) AS out_date,
        CASE
        WHEN out_date &lt;= &#39;2022-05-01&#39; THEN &#39;출고완료&#39;
        WHEN out_date IS NULL THEN &#39;출고미정&#39;
        ELSE &#39;출고대기&#39;
        END AS &#39;출고여부&#39;
FROM food_order
ORDER BY order_id;</code></pre>
<p>날짜 기준으로 출고 여부를 잘 출력해줘야하는 문제였는데, 일단 처음에 위에처럼 쿼리를 작성해서 여러번 틀렸다.</p>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/e985a20c-3687-4335-934f-4b1ecccdea44/image.png" alt=""></p>
<pre><code class="language-sql">SELECT order_id, product_id, DATE_FORMAT(out_date, &#39;%Y-%m-%d&#39;) AS out_date,
        CASE
        WHEN out_date &lt;= &#39;2022-05-01&#39; THEN &#39;출고완료&#39;
        WHEN out_date IS NULL THEN &#39;출고미정&#39;
        WHEN out_date &gt; &#39;2022-05-01&#39; THEN &#39;출고대기&#39;
        END AS &#39;출고여부&#39;
FROM food_order
ORDER BY order_id;</code></pre>
<p>코드를 바꿔보면서 위처럼 작성해야하는걸 깨달았는데. 일단 CASE WHEN으로 구분하는건 괜찮았고, NULL일 때 출고 미정인 부분들은 틀린게 없었는데 <code>out_date</code> 출력할 때 문제가 있었다.</p>
<ol>
<li>널 값을 굳이 다른 값으로 채울 필요가 없었다.</li>
<li>날짜 형식 지정을 잘 해줘야했다.</li>
</ol>
<p>처음에는 <code>COALESCE</code>를 사용해서 널인 경우에는 공백을 출력하도록 해줘야할 것 같아 그렇게 했는데 이렇게 하면 DATE_FORMAT을 하지 않아도 한 것처럼 연월일이 출력되어서 계속 그대로 돌렸는데, 일단 포맷 형식을 지정해줘야했었고, 공백을 출력하게 지정하지 않아도 됐다. 널은 왠지 널이라고 쓰여있어야할 것 같아서 그냥 공백이길래 &#39; &#39; 이걸 지정해줘서 문제였다.</p>
<p>다른게 문제인줄 알고 바꾸다가 CASE절도 바꾸어줬는데 마지막에 출고 대기는 그냥 전처럼 ELSE를 쓰는게 나을 것 같기도하다.</p>
<hr>
<blockquote>
<h4 id="q6-음식-종류별-즐겨찾기가-가장-많은-식당-출력">Q6. 음식 종류별 즐겨찾기가 가장 많은 식당 출력</h4>
</blockquote>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/f127983d-d0e5-4e62-9dfa-f8b1139d307f/image.png" alt=""></p>
<pre><code class="language-sql">SELECT food_type, rest_id, rest_name, favorites
FROM rest_info R1
WHERE favorites &gt;= (SELECT MAX(favorites)
                 FROM rest_info R2
                 WHERE R1.food_type = R2.food_type)
ORDER BY food_type DESC;</code></pre>
<p>이 문제가 음식 종류별로 이제 즐겨찾기가 가장 많은 행(식당)을 찾고 -&gt; 그 행(식당)의 정보를 출력하는 거였는데 이 즐겨찾기가 가장 많은 행은 <code>MAX()</code>로 찾는다해도, 그것과 비교를하려면 임시 테이블을 둬야할지 서브쿼리를 써야할 지 고민했다. 그리고 윈도우 함수 써서 rank 매기는 것도 방법일 것 같았다. (근데 서브쿼리가 훨씬 나을 것 같았다. 행 몇 개 두는 테이블을 만들 필요는 없을 것 같았다.)</p>
<p>WHERE절에 서브쿼리를 두었는데 (중첩질의) 처음에는 어떻게 <strong>조건에 부합하는 행(종류별 즐겨찾기가 가장 많은 그 행)만 뽑아낼 수 있을까</strong> 고민을 많이 했다.</p>
<p>상관질의 사용해서 상위 쿼리의 음식 종류와 하위 쿼리 음식 종류가 같은 것들로 1차로 조건 설정을 해주고, 그 안에서 이제 즐겨찾기 수를 토대로 걸러주면 됐다. 처음에는 <code>ALL</code>을 써서 했는데 없이 비교문만 사용해도 잘 돌아갔다.</p>
<p>하위 쿼리에서 해당 음식 종류의 최대 즐겨찾기 수를 찾고 그것과 상위 쿼리의 즐겨찾기 수를 행마다 비교하며 각 음식 종류의 즐겨찾기 수가 가장 많은 음식점 정보를 잘 출력하면 됐다.</p>
<hr>
<blockquote>
<h4 id="q7-클래스-문제7-렌트카-대여-가능-여부-출력">Q7. 클래스 문제7: 렌트카 대여 가능 여부 출력</h4>
</blockquote>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/899aa9d7-3538-4013-af31-4a3bd03ec559/image.png" alt=""></p>
<pre><code class="language-sql">SELECT car_id,
        MAX(CASE 
            WHEN &#39;2022-10-16&#39; BETWEEN start_date AND end_date
            THEN &#39;대여중&#39;
            ELSE &#39;대여 가능&#39;
        END) AS availability
FROM car_rental_company_rental_history
GROUP BY car_id
ORDER BY car_id DESC;</code></pre>
<p>차량 대여 기록이 엄청 많고, 빌렸던걸 또 빌리고 막 하는 기록들이 많아서 막상 테이블을 출력해보면 동일한 아이디가 대여중/대여 가능이 번갈아서 막 여러 번 나왔었다.</p>
<p>이걸 동일한 ID의 여러 기록들/값들 중에 어떻게 걸러내야하지? 라는 생각이 들었다. 그루핑하자는 생각이 들었는데 이 경우에는 대여중/대여 가능을 어떻게 선택할 조건을 설정할 수 없으니 랜덤하게 나와버린다.</p>
<p><code>car_id</code>를 기준으로 그룹화를 하고, SELECT절에서 CASE WHEN을 사용해 날짜 기준으로 조건의 날짜가 start_date나 end_date 사이에 있으면 대여중, 아니면 대여 가능을 반환하도록 했는데, 위의 문제는 해결하지 못했다.</p>
<p>이거 관련해서 AI한테 물어봤을 때는 &#39;대여중&#39; &#39;대여 가능&#39; 중에서 대여 중이 MAX를 썼을 때 더 크니까(뒤) 그렇게 하면 그루핑 후에 대여중+대여가능이 모두 뜰 경우 대여중으로 설정된다고 한다. 그래서 기존에 썼던 코드에서 SELECT의 CASE WHEN을 MAX로 감쌌는데 잘 돌아갔다.</p>
<p>약간 꼼수같은 방법이라 다른 사람들 풀이가 궁금해서 찾아봤는데 CASE WHEN 안에서 또 SELECT를 사용해서 IN으로 이제 하나라도 start_date와 end_date 사이에 해당 날짜가 끼여있는지 확인하면 해결하면 되는 것 같았다.</p>
<p>아래는 다시 풀었을 때...인데 의외로 또 간단하다.</p>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/1192f3e1-d180-44f3-87ec-1b5264000d25/image.png" alt=""></p>
<pre><code class="language-sql">SELECT car_id,
       CASE
            WHEN car_id IN (SELECT car_id
                            FROM CAR_RENTAL_COMPANY_RENTAL_HISTORY
                            WHERE &#39;2022-10-16&#39; 
                                   BETWEEN start_date AND end_date )
            THEN &#39;대여중&#39;
            ELSE &#39;대여 가능&#39;
       END AS availability
FROM CAR_RENTAL_COMPANY_RENTAL_HISTORY C
GROUP BY car_id
ORDER BY car_id DESC;</code></pre>
<hr>
<blockquote>
<h4 id="q8-클래스-문제8-1월-판매된-작가별-카테고리별-총액-출력">Q8. 클래스 문제8: 1월 판매된 작가별-카테고리별 총액 출력</h4>
</blockquote>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/0c39f65b-3c01-41bd-b11f-1e85928ef034/image.png" alt=""></p>
<pre><code class="language-sql">WITH sales AS (
    SELECT A.author_id, A.author_name,
       B.category,
       SUM(S.sales)*B.price AS total_sales
    FROM book_sales S JOIN
     book B ON S.book_id = B.book_id JOIN
     author A ON A.author_id = B.author_id
    WHERE MONTH(S.sales_date) = &#39;1&#39;
    GROUP BY B.category, S.book_id
)

SELECT author_id, author_name,
       category,
       SUM(total_sales) AS total_sales
FROM sales
GROUP BY author_id, category
ORDER BY author_id, category DESC;</code></pre>
<p>위에서는 CTE 사용해서 풀어보긴했는데 사실 안써도 풀 수 있는 문제였다.</p>
<p>일단은 작가와 카테고리 별로 묶어서 총 판매액을 출력하면 되는 문제였는데 그룹화를 어떤 기준으로 할 지 고민했었다.</p>
<p>이 문제에서도 위에처럼 일단 월 설정하는거는 다시해야할 것 같다.</p>
<p>일단은 위에 문제를 풀 때는 카테고리별로 묶고, 그 안에서 책의 아이디 별로 묶어서 해당 책이 얼마나 팔렸는지를 먼저 기록하고, 그걸 토대로 다시 작가 아이디, 카테고리별로 묶었는데 이거를 사실 그냥 <code>GROUP BY author_id, category, book_id</code>로 하면 됐다.</p>
<p>근데 이걸 처음에 했다가 위처럼 테이블을 하나 더 만든 이유는 <code>SUM()</code>을 사용해서 계산할 때, <code>book_id</code> 별로 다른 가격을 반영해서 총액 계산하는게 어려워서 였다.</p>
<p><code>SUM(S.sales)</code> 이렇게 해서 판매한 개수를 얻어오고 이 뒤에 곱셈을 해서 다른 가격 반영하는게 쉽지 않았는데 <code>SUM(S.sales*B.price)</code> 이런식으로? <code>SUM()</code> 안에 하나가 아니라 수식을 넣어도 되는걸 다 풀고 찾아보다 알았다....</p>
<p>다시 풀게 되면 아래처럼 풀 것 같다.
<img src="https://velog.velcdn.com/images/summeryoung_/post/a70fb3e5-5d10-47b4-9482-2fe35c10937c/image.png" alt=""></p>
<pre><code class="language-sql">SELECT A.author_id, A.author_name, B.category,
       SUM(S.sales * B.price) AS total_sales
FROM book B JOIN
     book_sales S ON B.book_id = S.book_id JOIN
     author A ON B.author_id = A.author_id
WHERE S.sales_date BETWEEN &#39;2022-01-01&#39; AND &#39;2022-01-31&#39;
GROUP BY A.author_id, B.category
ORDER BY author_id, category DESC;</code></pre>
<p>약간 SUM을 하게되면, 그룹한것들을 총 합계를 구하는거니까 <code>SUM(S.sales*B.price)</code>를 하게 되면 이제 행별로 각각 곱한 후에 -&gt; 작가&amp;카테고리별로 묶었으니 그 기준으로 다 합하고 행 줄이기. 이런식으로 진행되는 것 같다.</p>
<hr>
<blockquote>
<h4 id="q9-클래스-문제9-게시글의-수가-3개-이상인-고객-정보-출력">Q9. 클래스 문제9: 게시글의 수가 3개 이상인 고객 정보 출력</h4>
</blockquote>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/78953fdc-98eb-4480-a0fb-2cf192593dd4/image.png" alt=""></p>
<pre><code class="language-sql">WITH users AS (
    SELECT U.user_id AS user_id
    FROM used_goods_board B JOIN
        used_goods_user U ON
        B.writer_id = U.user_id
    GROUP BY U.user_id
    HAVING COUNT(DISTINCT B.board_id) &gt;= 3)

SELECT U.user_id, U.nickname, 
       CONCAT(U.city,&#39; &#39;,U.street_address1,&#39; &#39;, U.street_address2) AS &#39;전체주소&#39;,
       CONCAT(LEFT(TLNO,3),&#39;-&#39;,SUBSTR(TLNO,4,4),&#39;-&#39;,RIGHT(TLNO,4)) AS &#39;전화번호&#39;
FROM used_goods_user U LEFT JOIN
     users ON 
     U.user_id = users.user_id
WHERE users.user_id IS NOT NULL
ORDER BY U.user_id DESC;</code></pre>
<p>위에도 CTE 사용하기는 하는데, 사실 그럴 필요는 없었을 것 같긴하다. 그냥 분리하면 아래를 더 간단하게 쓸 수 있을 것 같았다...?</p>
<p>일단 위에서 3번 이상 게시글을 올린 사용자의 아이디만 테이블에 남겼다. 그룹화하고, <code>HAVING</code> 사용해서 이 부분은 풀었다.</p>
<p>그 후에 그렇게 만든 테이블을 기존 테이블하고 조인한 뒤에 널값 아닌 사용자의 정보들만 출력했다. 사실 CTE 만드는건 불필요한데 이번주 내용에서 배운 거라 사용하고 조인도 사용해봤다. </p>
<p>실제로 다시 풀게되면 그냥 바로 GROUP BY -&gt; HAVING 쓴 뒤에 SELECT에서 조건대로 처리할 것 같다. 아래처럼. </p>
<p>문제에서 신경써서 처리할 부분은 <code>CONCAT()</code> 써서 요구조건대로 문자열 만드는 부분이었다. 전화 번호 사이에 &#39;-&#39; 넣는 것이나 주소 연결하는 것만 잘 처리해주었다. (시키는대로 하면 되는데 그 조건이 은근 자잘해서 귀찮고 또 완전 안똑같으면 틀려서 몇 번 다시 풀었다.)</p>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/ad23e531-fdc5-426b-8c6b-35ea54010307/image.png" alt=""></p>
<pre><code class="language-sql">SELECT U.user_id, U.nickname,
       CONCAT(U.city, &#39; &#39;, U.street_address1, &#39; &#39;, U.street_address2) AS &#39;전체주소&#39;,
       CONCAT(LEFT(U.tlno,3), &#39;-&#39;, SUBSTR(U.tlno,4,4), &#39;-&#39;, RIGHT(U.tlno,4)) AS &#39;전화번호&#39;
FROM used_goods_board B JOIN
     used_goods_user U ON B.writer_id = U.user_id
GROUP BY B.writer_id
HAVING COUNT(DISTINCT B.board_id) &gt;= 3
ORDER BY U.user_id DESC;</code></pre>
<p>CTE 만들지 않고 풀면 위와 같다. 이게 더 좋은 방법인 것 같긴하다, 위는 그냥 억지로 쓴 거...같고? 주소 연결할 때 공백을 빼먹어서 몇 번 다시 풀었었다....</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[데이터베이스 5주차 내용 정리]]></title>
            <link>https://velog.io/@summeryoung_/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-5%EC%A3%BC%EC%B0%A8-%EB%82%B4%EC%9A%A9-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@summeryoung_/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-5%EC%A3%BC%EC%B0%A8-%EB%82%B4%EC%9A%A9-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Sat, 04 Apr 2026 05:36:17 GMT</pubDate>
            <description><![CDATA[<h1 id="1-내장함수-null-비교문">1. 내장함수, NULL, 비교문</h1>
<h2 id="1-1-sql-내장함수">1-1. SQL 내장함수</h2>
<ul>
<li><strong>상수나 속성 이름</strong>을 입력값으로 받아 <strong>단일 값</strong>을 결과로 반환</li>
<li>모든 내장함수는 사용 시 유효한 입력값을 받아야함</li>
<li><strong>SELECT절, WHERE절, UPDATE절</strong>에서 모두 사용 가능</li>
</ul>
<hr>
<h3 id="1-숫자-함수">(1) 숫자 함수</h3>
<ul>
<li>SQL문에서 수학의 기본적인 사칙 연산자와 나머지 연산자 기호를 그대로 사용</li>
<li>MySQL은 이러한 연산자 중 사용 빈도가 높은 것을 <strong>내장 함수</strong> 형태로 제공</li>
</ul>
<blockquote>
<ul>
<li><strong>ABS(숫자)</strong>: 숫자의 절댓값을 계산</li>
</ul>
</blockquote>
<ul>
<li><strong>CEIL(숫자), FLOOR(숫자)</strong>: 올림/내림</li>
<li><strong>ROUND(숫자, m)</strong>: 숫자의 반올림. m은 기준수.</li>
</ul>
<h4 id="abs-함수">ABS 함수</h4>
<ul>
<li>절댓값 구하는 함수<pre><code class="language-sql">SELECT ABS(-78), ABS(78);</code></pre>
<img src="https://velog.velcdn.com/images/summeryoung_/post/f2bd879e-35e9-43c5-a5a6-c8e97c6256e0/image.png" width=80%>

</li>
</ul>
<hr>
<h4 id="round-함수">ROUND 함수</h4>
<ul>
<li>반올림한 값을 구하는 함수<pre><code class="language-sql">SELECT ROUND(4.12345, 3);</code></pre>
<img src="https://velog.velcdn.com/images/summeryoung_/post/e5b79ce9-1425-4677-97a6-74fe6fe4aafa/image.png" alt=""></li>
</ul>
<h4 id="숫자함수의-연산">숫자함수의 연산</h4>
<ul>
<li>숫자함수에는 <strong>직접 숫자를 입력</strong>하거나 <strong>열 이름을 사용</strong>할 수 있음</li>
<li>여러 함수를 <strong>복합적으로 사용</strong>할 수도 있음</li>
</ul>
<pre><code class="language-sql">SELECT custid &#39;고객번호&#39;, ROUND(SUM(saleprice)/COUNT(*), -2) &#39;평균금액&#39;
FROM orders
GROUP BY custid;</code></pre>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/8eb02322-5261-4695-a999-3012a88dcf37/image.png" alt=""></p>
<hr>
<h3 id="2-문자함수">(2) 문자함수</h3>
<blockquote>
<h4 id="문자값-반환-함수">문자값 반환 함수</h4>
</blockquote>
<ul>
<li><strong>CONCAT(s1, s2)</strong>: 두 문자열을 연결</li>
<li><strong>LOWER(s), UPPER(s)</strong>: 대상 문자열을 모두 소문자로/대문자로 변환</li>
<li><strong>SUBSTR(s,n,k)</strong>: 대상 문자열을 지정된 자리에서부터, 지정된 길이만큼 잘라서 반환</li>
<li><strong>TRIM(c FROM s)</strong>: 대상 문자열의 양쪽에서 지정된 문자를 삭제. (문자열만 넣을 시 기본으로 공백 제거)</li>
</ul>
<blockquote>
<h4 id="숫자값-반환-함수">숫자값 반환 함수</h4>
</blockquote>
<ul>
<li><strong>LENGTH(s)</strong>: 대상 문자열의 바이트를 반환 (알파벳은 1바이트, 한글은 3바이트)</li>
<li><strong>CHAR_LENGTH(s)</strong>: 문자열의 문자 수를 반환</li>
</ul>
<h4 id="replace-함수">REPLACE 함수</h4>
<ul>
<li>문자열을 치환하는 함수<pre><code class="language-sql">SELECT bookid, REPLACE(bookname, &#39;야구&#39;, &#39;농구&#39;) bookname, publisher, price
FROM book;</code></pre>
<img src="https://velog.velcdn.com/images/summeryoung_/post/f0860463-da75-4628-9d3f-9376c3c0c301/image.png" alt=""></li>
</ul>
<h4 id="length-char_length-함수">LENGTH, CHAR_LENGTH 함수</h4>
<ul>
<li><code>LENGTH()</code>: 바이트 수를 가져오는 함수</li>
<li><code>CHAR_LENGTH()</code>: 문자의 수를 가져오는 함수</li>
</ul>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/25395853-9428-4f7e-b317-604f7dc6eb05/image.png" alt=""></p>
<h4 id="이름-가리기masking-실습">이름 가리기(masking) 실습</h4>
<pre><code class="language-sql">SELECT CONCAT(LEFT(name,1), &#39;*&#39;,RIGHT(name,1)) AS marked_name
FROM customer;</code></pre>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/304f4660-cbd3-472e-b5ac-6021e6c33877/image.png" alt=""></p>
<pre><code class="language-java">SELECT
    CASE
        WHEN CHAR_LENGTH(name) = 2
            THEN CONCAT(LEFT(name,1),&#39;*&#39;)
        WHEN CHAR_LENGTH(name) &gt; 2
            THEN CONCAT(LEFT(name,1), REPEAT(&#39;*&#39;,CHAR_LENGTH(name)-2), RIGHT(name,1))
        ELSE name
    END
FROM customer;</code></pre>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/0c58796f-6269-46c9-a45d-38245370781a/image.png" alt=""></p>
<hr>
<h3 id="3-날짜시간-함수">(3) 날짜/시간 함수</h3>
<ul>
<li>날짜와 시간 부분을 나타내는 <strong><code>format</code></strong>으로 표기</li>
<li><code>format</code>은 날짜 형식 지정자로 날짜와 시간 부분을 표기하기 위해 특별한 규칙을 가짐.</li>
</ul>
<blockquote>
</blockquote>
<ul>
<li><code>STR_TO_DATE(string format)</code>: 문자열 데이터를 날짜형으로 반환</li>
<li><code>DATE_FORMAT(date format)</code>: 날짜형 데이터를 문자열로 반환</li>
<li><code>ADDDATE(date interval)</code>: DATE형의 날짜에서 INTERVAL 지정한 시간만큼 더함</li>
<li><code>DATE(date)</code>: DATE형의 날짜 부분을 반환</li>
<li><code>DATEDIFF(date1, date2)</code>: DATE형의 date1-date2 날짜 차이를 반환함</li>
<li><code>SYSDATE</code>: DBMS 상의 오늘 날짜를 반환.</li>
</ul>
<ul>
<li>DBMS마다 함수 이름과 동작이 다름</li>
<li>날짜형 데이터는 &#39;-&#39;와 &#39;+&#39;를 사용하여 원하는 날짜로부터 이전(-)과 이후(+)를 계산할 수 있음</li>
</ul>
<blockquote>
<p><strong>📌 날짜, 시간함수 사용 주의점</strong><br></p>
</blockquote>
<ol>
<li>DBMS마다 이름과 동작, 의미가 다름</li>
<li>타임존 문제</li>
</ol>
<ul>
<li><code>NOW()</code>나 <code>CURRENT_TIMESTAMP</code>는 DB 서버의 타임존을 기준으로 반환.</li>
<li>서버와 사용자가 다른 지역에 있으면 시간이 어긋날 수 있으므로, 필요시 따로 맞춰줘야함.</li>
</ul>
<ol start="3">
<li>날짜 포맷 출력: <strong><code>DATE_FORMAT()</code></strong>(MySQL) <strong><code>TO_CHAR()</code></strong>(Oracle/Postgres) 포맷 지정</li>
<li><strong>NULL 처리</strong>: </li>
</ol>
<ul>
<li>날짜 컬럼이 NULL이면 함수 적용 시 에러가 발생</li>
<li>기본값으로 설정해둬야함: <code>IFNULL()</code>, <code>COALESCE()</code> 사용해서 널 처리.</li>
</ul>
<ol start="5">
<li>성능 고려: select에서 조회용으로 사용할 때</li>
</ol>
<ul>
<li>다른 부분에서 속성을 함수를 사용해 변환하면 <strong>인덱스에 저장된 값이 아니라 함수 결과를 새로 계산</strong>하여 <strong>테이블 풀 스캔</strong>을 할 가능성이 높음</li>
<li>테이블 풀스캔의 경우 성능이 떨어짐</li>
<li><code>WHEN DATE(order_date) = &#39;2026-04-02&#39;</code> 보다 <code>WHEN order_date &gt;= &#39;2026-03-24 00:00:00&#39; AND order_date &lt; &#39;2026-03-39 00:00:00&#39;</code>가 좋음.</li>
</ul>
<span style="font-size:15px; color:gray">
+ [ MariaDB에서 `NOW()`와 `SYSDATE()`] <br>
NOW(): 쿼리 실행 시작 시점의 시간을 반환. 같은 쿼리 내에서는 항상 동일한 값으로 유지
SYSDATE(): 함수 호출 순간의 시스템 시간을 반환. 같은 쿼리 내에서도 호출 시점마다 값이 달라짐.
</span>

<h4 id="1-adddatedate-interval">(1) ADDDATE(date, interval)</h4>
<p><code>ADDDATE(&#39;날짜&#39;, &#39;INTERVAL 수치단위&#39;)</code></p>
<ul>
<li>지정한 날짜에 <strong>일(day) 또는 시간(interval)</strong>을 더해 새로운 날짜를 반환하는 함수</li>
</ul>
<pre><code class="language-sql">SET @value = &#39;2024-04-01&#39;;

SELECT ADDDATE(@value, INTERVAL -10 DAY) &quot;BEFORE&quot;, ADDDATE(@value, INTERVAL 10 DAY) &quot;AFTER&quot;;</code></pre>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/0f73b1c9-e746-4d47-ac48-18ae84a0918a/image.png" alt=""></p>
<h4 id="2-format의-주요-지정자">(2) format의 주요 지정자</h4>
<p><strong>1. 요일</strong></p>
<ul>
<li><code>%w</code>: 요일 순서 (0~6, Sunday=0)</li>
<li><code>%W</code>: 요일 (Sunday~Saturday)</li>
<li><code>%a</code>: 요일의 약자(Sun~Sat)<br>

</li>
</ul>
<p><strong>2. 날짜</strong></p>
<ul>
<li><code>%d</code>: 한 달 중 날짜 (00~31)</li>
<li><code>%j</code>: 1년 중 날짜 (001~366)<br>

</li>
</ul>
<p><strong>3. 시간</strong></p>
<ul>
<li><code>%h</code>: 12시간 (0~12)</li>
<li><code>%H</code>: 24시간 (0~24)</li>
<li><code>%i</code>: 분 (0~59)</li>
<li><code>%s</code>: 초(0~59)<br>

</li>
</ul>
<p><strong>4. 월</strong></p>
<ul>
<li><code>%m</code>: 월 순서 (01~12, January = 01)</li>
<li><code>%M</code>: 월 이름 (January ~ December)</li>
<li><code>%b</code>: 월 이름 약어(Jan~Dec)</li>
</ul>
<br>

<p><strong>5. 연도</strong></p>
<ul>
<li><code>%Y</code>: 4자리 연도</li>
<li><code>%y</code>: 4자리 연도의 마지막 2자리</li>
</ul>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/f87ee528-21cf-489e-ac88-673aa1e9b648/image.png" alt=""></p>
<h4 id="3-str_to_date-함수-date_format-함수">(3) STR_TO_DATE 함수, DATE_FORMAT 함수</h4>
<ul>
<li><code>STR_TO_DATE</code>: CHAR 형(문자열)으로 저장된 날짜를 DATE형으로 변환</li>
<li><code>DATE_FORMAT</code>: 날짜형을 문자형으로 변환함</li>
</ul>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/2ed3003e-8d13-490a-bf59-6face4998514/image.png" alt=""></p>
<h4 id="4-sysdate-함수">(4) SYSDATE 함수</h4>
<ul>
<li>데이터베이스에 설정된 현재 날짜와 시간을 반환하는 함수</li>
</ul>
<hr>
<h2 id="1-2-null-값-처리">1-2. NULL 값 처리</h2>
<h3 id="널null-값">널(NULL) 값</h3>
<ul>
<li><p>아직 <strong>지정되지 않은 값</strong>, 즉 값을 알 수도 없고 적용할 수도 없음</p>
</li>
<li><p>&#39;0&#39;이나 빈 문자  또는 공백과는 다른 특별한 값으로 **비교연산자로 비교할 수도 없고, 연산 수행의 결과도 NULL로 반환됨.</p>
</li>
<li><p>NULL 값에 대한 연산 및 집계함수:</p>
<ul>
<li><strong>NULL + 숫자</strong>의 연산 결과는 <strong>NULL</strong></li>
<li>집계 함수를 사용할 때에 <strong>NULL이 포함된 행은 집계에서 제외</strong> (해당 행이 하나도 없을 경우, SUM, AVG의 결과는 NULL, COUNT 함수의 결과는 0이됨)</li>
</ul>
</li>
</ul>
<br>

<h3 id="ifnull-함수">IFNULL 함수</h3>
<ul>
<li><p>NULL 값을 다른 값으로 대치하여 연산하거나 다른 값으로 출력</p>
</li>
<li><p><code>IFNULL(속성, 값)</code>의 형태로 사용하며 속성값이 널일때 &#39;값&#39;으로 대치한다.</p>
</li>
<li><p>MySQL, MariaDB에서 사용</p>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/6d11f5b0-dcde-4b14-a4cc-9a2969c9209a/image.png" alt=""></p>
</li>
</ul>
<h3 id="coalesce-함수">COALESCE 함수</h3>
<ul>
<li>표준 SQL에서 지원</li>
<li><code>COALESCE(인자1, 인자2,...)</code>: 여러 개의 인자 중 널값이 아닌 첫 번째 값을 반환하며 <code>IFNULL</code>과 마찬가지로 <code>COALESCE(속성, &#39;값&#39;)</code>으로 작성하면 널 값을 &#39;값&#39;으로 채워서 출력할 수 있음.</li>
</ul>
<hr>
<h2 id="1-3-행번호-출력">1-3. 행번호 출력</h2>
<h3 id="set">SET</h3>
<ul>
<li>MySQL에서는 <strong>변수</strong>는 이름 앞에 <strong><code>@</code></strong> 기호를 붙이며, <strong>치환문에는 SET과 <code>:=</code> 기호를 사용</strong>한다.</li>
</ul>
<pre><code class="language-sql">SET @seq:=0;

SELECT (@seq := @seq+1) &#39;순번&#39;, custid, name, phone
FROM customer
WHERE @seq &lt; 2;</code></pre>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/72944518-6a7a-456d-b292-c23a3ae63b00/image.png" alt=""></p>
<p>위처럼 적었을 때는 결과가 생각한 것처럼 잘 나오는데, 좀 다르게 <code>@seq &lt;2</code>인 경우에만 출력하려고 하면, 예상과는 다르게 아래처럼 나온다.</p>
<pre><code class="language-sql">SET @seq:=0;

SELECT (@seq := @seq+1) &#39;순번&#39;, custid, name, phone
FROM customer
WHERE @seq &gt; 2;</code></pre>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/1a928ae9-7720-43d1-a40f-38659d5e7e5b/image.png" alt=""></p>
<p>이유는 쿼리 실행 시 동작 순서와 관련이 있다.</p>
<p><code>@seq</code>를 증가시켜주는 부분이 SELECT에 있어서 인데, WHERE절에서 조건을 체크하고 그 행이 조건에 맞으면 이제 SELECT로 가게되는데, 위의 경우에는 처음부터 <code>@seq</code>가 조건에 맞지 않아 SELECT절로 이동하지 않으면서 <code>@seq</code>가 그 뒤의 행들에서도 전혀 증가되지 않고 계속 0으로 남아있게 된다.</p>
<p>그래서 만약에 위에처럼 쿼리를 작성하고 싶으면 아래처럼 <code>@seq</code> 증가시켜주는 부분을 WHERE 절에서 하는 하도록 작성해주면 되었다.</p>
<p>이게 쿼리 실행했을 때 <strong>각 행 별로</strong> 이제 쿼리의 내용대로 돌아가면서? 실행? 조건 체크? 등이 이루어지는 식이고 그 행 별로 이제 적용이 되는? 느낌이었다.</p>
<pre><code class="language-sql">SET @seq:=0;

SELECT (@seq) &#39;순번&#39;, custid, name, phone
FROM customer
WHERE (@seq := @seq+1) &gt; 2;</code></pre>
<hr>
<h2 id="1-4-case-when">1-4 CASE WHEN</h2>
<ul>
<li><strong>조건에 따라 다른 값을 반환</strong></li>
<li>IF-THNE-ELSE와 비슷하게 동작하며 <strong>집계함수와 함께 쓰거나 출력 컬럼을 가공</strong>할 때 유용하다</li>
</ul>
<pre><code class="language-sql">CASE
    WHEN 조건1 THEN 결과1 #WHEN: 조건 지정
    WHEN 조건2 THEN 결과2 #THEN: 조건이 참이면 반환할 값
    ...
    ELSE 기본값 #ELSE: 모든 조건이 거짓일 때 반환할 기본값
END AS 별칭 #END: CASE문 종료</code></pre>
<p>예) 국내 거주자/국외 거주자 출력</p>
<pre><code class="language-sql">SELECT custid, name,
    CASE
        WHEN address LIKE &#39;%대한민국%&#39;
        THEN &quot;국내&quot;
        ELSE &quot;국외&quot;
    END AS &quot;국적&quot;, phone
FROM customer;</code></pre>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/ba26ec45-a864-4409-84f4-3fd4801db554/image.png" alt=""></p>
<hr>
<h1 id="2-부속질의">2. 부속질의</h1>
<h2 id="2-1-부속질의-서브쿼리">2-1. 부속질의 (=서브쿼리)</h2>
<ul>
<li>하나의 SQL문 안에 <strong>다른 SQL문이 중첩</strong>된 질의</li>
<li>주로 메인 쿼리의 조건에 따라 서브 쿼리의 결과를 가져와서 메인 쿼리에서 사용하는 용도로 활용</li>
<li>다른 테이블에서 가져온 데이터로 현재 테이블의 정보를 찾거나 가공하는 등의 작업 수행 가능</li>
<li>조인을 사용하는 방법도 있지만 서브 쿼리가 더 유리한 경우에 서브 쿼리를 사용</li>
</ul>
<blockquote>
<p><strong>부속 질의가 유리할 때</strong></p>
</blockquote>
<ul>
<li><strong>필터링 대상이 매우 적을 때</strong>: 서브 쿼리의 결과값이 단 하나임이 보장되고, 메인 테이블의 양이 방대할 때</li>
<li><strong>복잡한 집계가 포함될 때</strong>: 조인으로 풀면 중복 데이터가 너무 많이 발생해 계산이 꼬이는 경우.<br></li>
<li><code>EXPLAIN</code>을 사용하면 쿼리가 훑고간 행의 수, 실제 남은 데이터의 비율, 인덱스를 사용하는(using index) 아니면 풀테이블 스캔(using filesort)를 하는지 등을 확인해 볼 수 있음.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/25bf3c9d-7b50-4ff7-b7d1-083037d1c9bc/image.png" alt=""></p>
<h4 id="부속질의의-종류">부속질의의 종류</h4>
<table>
<thead>
<tr>
<th align="center">부속질의</th>
<th align="center">실무 용어</th>
<th align="center">설명</th>
</tr>
</thead>
<tbody><tr>
<td align="center">WHERE 부속질의</td>
<td align="center"><strong>중첩질의</strong></td>
<td align="center">WHERE 절에서 술어와 같이 사용되며 결과를 한정. <br> <strong>상관 또는 비상관</strong> 형태</td>
</tr>
<tr>
<td align="center">SELECT 부속질의</td>
<td align="center"><strong>스칼라 부속질의</strong></td>
<td align="center">SELECT 절에서 사용되며 <strong>단일값</strong>을 반환</td>
</tr>
<tr>
<td align="center">FROM 부속질의</td>
<td align="center"><strong>인라인 뷰</strong></td>
<td align="center">FROM 절에서 결과를 뷰 형태로 반환</td>
</tr>
</tbody></table>
<br>

<h3 id="where-부속질의">WHERE 부속질의</h3>
<ul>
<li>중첩질의. 보통 데이터를 <strong>선택 하는 조건 혹은 술어</strong>와 같이 사용.</li>
<li>비교/집합/한정/존재 연산자와 사용.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/ad06486d-49ae-4659-9495-37a1d7abf0dd/image.png" alt=""></p>
<h4 id="비교연산자">비교연산자</h4>
<ul>
<li>비교 연산자 사용 시 <strong>부속질의가 반드시 단일 행, 단일 열을 반환</strong>해야하며, 아닐 경우에는 질의를 처리할 수 없다.</li>
<li>주질의 대상 열 값과 결과 값을 비교 연산자에 적용하여, 참인 경우에만 주질의의 해당 열을 출력한다.</li>
</ul>
<h4 id="in-not-in-집합-연산자">IN, NOT IN (집합 연산자)</h4>
<ul>
<li>IN/NOT IN 연산자는 <strong>주질의의 속성값이 부속질의에서 제공한 결과 집합에 있는지/없는지 확인</strong>하는 역할</li>
<li>주질의는 WHERE절에 사용되는 속성값을 부속질의의 결과 집합과 비교해 하나라도 있으면 참이 됨</li>
</ul>
<h4 id="all-someany한정-연산자">ALL, SOME/ANY(한정 연산자)</h4>
<ul>
<li>하나의 결과값이 아니라 여러 결과값과 비교할 수 있게 해줌</li>
<li>ALL: 모든 결과값보다~ (크거나 작다 등 비교연산자 사용)</li>
<li>SOME/ANY: 반환한 결과값 중 하나라도 조건 만족</li>
</ul>
<pre><code class="language-sql">#예시
SELECT 이름 FROM 학생 
WHERE 키 &gt; ALL (SELECT 키 FROM 학생 WHERE 학년 = 3);

SELECT 이름 FROM 학생 
WHERE 키 &gt; ANY (SELECT 키 FROM 학생 WHERE 학년 = 3);</code></pre>
<p>예) </p>
<pre><code class="language-sql">#동작하지 않는 경우
SELECT *
FROM customer
WHERE custid = (SELECT custid
                FROM orders);

 #수정
 SELECT *
FROM customer
WHERE custid IN (SELECT custid
                FROM orders);</code></pre>
<blockquote>
<p><strong>부속질의 vs 상관질의</strong></p>
</blockquote>
<ul>
<li>부속질의: 단순히 쿼리 안에 들어간 SELECT문으로 독립적으로 실행할 수 있음</li>
<li>상관질의: 부속질의 중 <strong>메인 쿼리의 컬럼을 참조</strong>하여 메인 쿼리의 <strong>각 행마다 실행</strong>되는 경우.</li>
</ul>
<h4 id="exists-not-exists-존재-연산자">EXISTS, NOT EXISTS (존재 연산자)</h4>
<ul>
<li>데이터의 존재 여부를 확인<pre><code class="language-sql">WHERE [NOT] EXISTS (부속질의)</code></pre>
</li>
<li>메인 쿼리와 서브 쿼리가 상관 부속질의의 관계일 때 둘 사이의 연결 관계를 잘 설정해줘야한다</li>
</ul>
<pre><code class="language-sql">SELECT *
FROM customer C
WHERE EXISTS (SELECT custid
                     FROM orders O
                     WHERE C.custid = O.custid);</code></pre>
<pre><code>위에서는 `C.custid = O.custid`로 둘을 연결해주었다.</code></pre><p><img src="https://velog.velcdn.com/images/summeryoung_/post/1cdcba20-4191-4f31-b263-fa967e36e0ba/image.png" alt=""></p>
<hr>
<h2 id="2-2-스칼라-부속질의-select-부속질의">2-2. 스칼라 부속질의 (SELECT 부속질의)</h2>
<ul>
<li>부속질의의 결과 값을 <strong>단일 행, 단일 열의 스칼라 값</strong>으로 반환</li>
<li>만약 결과 값이 다중 행이거나 다중 열이면 DBMS는 어떤 행/열을 출력해야하는지 몰라 에러를 출력</li>
<li>결과값이 없는 경우에는 NULL 출력</li>
<li>SELECT 절에서 사용하니 고객별 주문 총횟수/총액과 같은 부분을 출력할 때 사용, 이때 하나의 행에 출력되는 값이니 여러 개일 수 없는 느낌...?</li>
</ul>
<p>예)
<strong>1. GROUP BY 없이</strong></p>
<pre><code class="language-sql">SELECT custid, (SELECT COUNT(*)
                FROM orders O
                WHERE C.custid = O.custid) AS &quot;주문 횟수&quot;
FROM customer C;</code></pre>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/49b42732-f3c2-4d63-8439-2b9797b6b10a/image.png" alt=""></p>
<ul>
<li>WHERE 절에서 고객의 주문을 찾고, <strong>주문이 없는 경우 결과가 0</strong>이 되는데 이때 <code>GROUP BY</code>가 없이 <code>COUNT()</code>를 쓰기에 대상이 없더라도 무조건 0이 됨</li>
<li>결과: 주문 안 한 고객의 옆에는 0<br>

</li>
</ul>
<p><strong>2. GROUP BY 있는 상태</strong></p>
<pre><code class="language-sql">SELECT custid, (SELECT COUNT(*)
                FROM orders O
                WHERE C.custid = O.custid
                GROUP BY custid) AS &quot;주문 횟수&quot;
FROM customer C;</code></pre>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/3cb7c97c-cee1-4245-8f22-2210bacbf67a/image.png" alt=""></p>
<ul>
<li>WHERE절 이후 그룹별로 묶으려고하는데, 이때 주문이 없으면 WHERE절에서 남은 데이터가 하나도 없기에 그룹 자체가 만들어지지 않음</li>
<li>그룹이 없으면 <code>COUNT()</code>의 대상도 없기에 아무 행도 반환하지 않음.</li>
</ul>
<br>

<h4 id="update-문에서의-스칼라-부속질의select-부속질의">UPDATE 문에서의 스칼라 부속질의(SELECT 부속질의)</h4>
<ul>
<li>스칼라 부속질의(SELECT 부속질의)는 UPDATE문에서도 사용할 수 있음</li>
</ul>
<pre><code class="language-sql">SET SQL_SAFE_UPDATES = 0;

UPDATE orders
SET bookname = (SELECT bookname
                FROM book
                WHERE book.bookid = orders.bookid);

SELECT *
FROM orders;</code></pre>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/9dbd32bf-8a63-48a6-b3e5-72b1fec5d1c9/image.png" alt=""></p>
<hr>
<h2 id="2-3-인라인-뷰-from-부속질의">2-3. 인라인 뷰 (FROM 부속질의)</h2>
<ul>
<li>FROM 절에서 사용되는 부속질의</li>
<li><strong>뷰: 기존 테이블로부터 일시적으로 만들어지는 가상의 테이블</strong></li>
</ul>
<p>예)</p>
<pre><code class="language-sql">SELECT C.name, SUM(o.saleprice) &#39;total&#39;
FROM (SELECT custid, name
      FROM customer
      WHERE custid &lt;= 2) C,
      orders O
WHERE C.custid = O.custid
GROUP BY C.name;</code></pre>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/42d5825b-50f9-4236-a8d6-3b7e50e5de12/image.png" alt=""></p>
<hr>
<h2 id="2-4-cte-common-table-expression-with">2-4. CTE (Common Table Expression, WITH)</h2>
<h3 id="with-cte명-as-select">WITH CTE명 AS (SELECT)</h3>
<ul>
<li>복잡한 SQL 쿼리 내에서 <strong>일시적인 결과 집합 (임시테이블)</strong>을 정의하여, 가독성을 높이고 쿼리를 구조화</li>
<li>메인 쿼리에서 일반 테이블처럼 <strong>재사용</strong>하거나, <strong>재귀 쿼리</strong> 구현에 활용됨</li>
<li>쿼리 실행에만 존재하며 데이터베이스에 영구적으로 저장되지 않음.</li>
</ul>
<pre><code class="language-sql">WITH CTE이름 AS (
    SELECT...
)
#,나 ; 적어주지 않고

#메인 쿼리는 이 아래
SELECT ...
FROM CTE이름;</code></pre>
<p>예)</p>
<pre><code class="language-sql">WITH sales_summary AS (
    SELECT o.custid, c.name, SUM(o.saleprice) AS total_sales
    FROM orders o
         JOIN customer c
         ON o.custid = c.custid
    GROUP BY o.custid
)

SELECT custid, name, total_sales
FROM sales_summary
ORDER BY total_sales DESC;</code></pre>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/139a1efa-adf3-4c37-a417-b5239ee54209/image.png" alt=""></p>
<hr>
<h2 id="2-5-윈도우-함수">2-5. 윈도우 함수</h2>
<h3 id="sql-윈도우-함수">SQL 윈도우 함수</h3>
<p>: 테이블의 <strong>행과 행 간의 관계를 정의하여 데이터를 윈도우(틀)로 그룹화하여 사용하는 함수</strong></p>
<ul>
<li>각 행에 대한 <strong>집계나 순위 계산 결과를 추가</strong>하여 사용</li>
<li><strong>GROUP BY와 달리 행의 개수를 유지하면서 그룹 내 계산 결과를 각 행에 표시</strong></li>
<li>복잡한 조인(JOIN) 없이 행 간 계산, 순위, 누적합계 등을 구할 때 유용</li>
<li><strong><code>OVER()</code></strong> 절과 함께 사용되어 PARTITION BY와 ORDER BY로 범위와 순서 지정</li>
</ul>
<p><span style="font-size:15px"> GROUP BY랑 비슷한데 행이 없어지지 않는다... <br></p>
<p><code>GROUP BY</code>를 하는 경우에는 여러 개의 행이 그룹별로 묶여 사라지지만, 윈도우 함수를 쓰는 경우에는 행 옆에 계산 결과를 붙여주는 식.</span></p>
<blockquote>
<p>SELECT <strong>함수명() OVER (PARTITION BY 컬럼명 ORDER BY 컬럼명)</strong>
FROM 테이블명;
<br></p>
</blockquote>
<ul>
<li><strong>PARTITION BY</strong>: 계산을 수행할 그룹을 나눔</li>
<li><strong>ORDER BY</strong>: 그 그룹 안에서 계산을 수행할 순서</li>
</ul>
<h3 id="주요-윈도우-함수-유형">주요 윈도우 함수 유형</h3>
<h4 id="1-순위함수">1. 순위함수</h4>
<ul>
<li><code>ROW_NUMBER()</code>: 줄 세우기 (1,2,3,4...)</li>
<li><code>RANK()</code>: 공동 순위만큼 건너뛰기 (1,2,2,3...)</li>
<li><code>DENSE_RANK()</code>: 공동순위가 있어도 촘촘하게 (1,2,2,3,...)</li>
</ul>
<pre><code class="language-sql">SELECT B.publisher, B.bookname, 
       SUM(O.saleprice) AS total_sales, 
       DENSE_RANK() OVER (PARTITION BY B.publisher ORDER BY SUM(O.saleprice) DESC) AS rank_in_publisher
FROM orders O 
     JOIN book B 
     ON O.bookid = B.bookid
GROUP BY B.publisher, B.bookname
ORDER BY B.publisher, rank_in_publisher;</code></pre>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/048e67e9-0346-4907-967c-c912595e0809/image.png" alt=""></p>
<h4 id="2-집계함수">2. 집계함수</h4>
<ul>
<li><p>누적 합계를 구할 때 편함.</p>
</li>
<li><p>순위함수(Ranking): ROW_NUMBER(), RANK(), DENSE_RANK()</p>
</li>
<li><p>집계함수(Aggregate): SUM(), AVG(), COUNT(), MAX(), MIN()</p>
</li>
<li><p>분석/값 함수(Value): LEAD(), LAG(), FIRST_VALUE(), LAST_VALUE()...</p>
</li>
</ul>
<p>예) 출판사별로 saleprice 누적합 구하기</p>
<pre><code class="language-sql">SELECT B.publisher, B.bookname,
       SUM(SUM(O.saleprice)) OVER (PARTITION BY B.publisher 
                                   ORDER BY SUM(O.saleprice), B.bookname) AS &#39;출판사별 총 판매액&#39;
FROM orders O 
     JOIN book B 
     ON O.bookid = B.bookid
GROUP BY B.publisher, B.bookname
ORDER BY B.publisher, `출판사별 총 판매액`;</code></pre>
<p>orders와 book 조인 -&gt; 출판사, 책 이름 별로 그루핑 <code>출판사-책이름-그 책의 총판매액: SUM(O.saleprice)</code> -&gt; 윈도우 함수 안에서 <code>PARTITION BY</code>를 통해 출판사별로 분리, 출판사별 <code>SUM(O.saleprice)</code> 계산. -&gt; <code>ORDER BY</code> 통해 책별 총판매액 &amp; 책이름으로 정렬 (같은 가격이어도 분리되어서 나오게) -&gt; 외부 <code>ORDER BY</code>에 윈도우 함수 결과 속성 추가해서 정렬결과대로 보기.</p>
<p><code>SUM(SUM(O.saleprice)</code></p>
<ul>
<li>안쪽 SUM: GROUP BY 결과로 나온 <strong>책 한 권의 합계</strong></li>
<li>바깥 SUM: 윈도우 함수가 만드는 <strong>출판사 내의 누적 합계</strong>
<img src="https://velog.velcdn.com/images/summeryoung_/post/1352b061-0ecc-4187-99a6-c3004895c9ae/image.png" alt=""></li>
</ul>
<h4 id="3-분석값-함수">3. 분석/값 함수</h4>
<ul>
<li>이전 행이나 다음 행의 데이터를 가져올 때</li>
<li>JOIN 없이도 어제와 오늘의 차이를 계산할 수 있음</li>
<li><code>LAG(컬럼)</code>: 현재 행보다 이전(뒤에있는) 행의 값을 가져옴 (과거)</li>
<li><code>LEAD(컬럼)</code>: 현재 행보다 다음(앞에있는) 행의 값을 가져옴 (미래)</li>
</ul>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/e6583a8d-f432-49e0-a34d-13e76bee05a8/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[자바 ORM 표준 JPA 프로그래밍] 5주차 스터디]]></title>
            <link>https://velog.io/@summeryoung_/%EC%9E%90%EB%B0%94-ORM-%ED%91%9C%EC%A4%80-JPA-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-5%EC%A3%BC%EC%B0%A8-%EC%8A%A4%ED%84%B0%EB%94%94</link>
            <guid>https://velog.io/@summeryoung_/%EC%9E%90%EB%B0%94-ORM-%ED%91%9C%EC%A4%80-JPA-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-5%EC%A3%BC%EC%B0%A8-%EC%8A%A4%ED%84%B0%EB%94%94</guid>
            <pubDate>Tue, 31 Mar 2026 06:16:55 GMT</pubDate>
            <description><![CDATA[<h1 id="6장-다양한-연관관계-매핑">6장. 다양한 연관관계 매핑</h1>
<p>엔티티의 연관관계 매핑 시에는 아래 3가지를 고려하게된다.</p>
<ul>
<li>다중성</li>
<li>단방향, 양방향</li>
<li>연관관계의 주인</li>
</ul>
<p>두 엔티티가 <strong>일대일 관계</strong>인지 <strong>일대단 관계</strong>인지 다중성을 고려하여야한다. 단방향일 때는 괜찮지만, <strong>양방향일 때는 연관관계의 주인을 정해야</strong>한다.</p>
<h4 id="다중성">다중성</h4>
<ul>
<li>다대일 (<code>@ManyToOne</code>)</li>
<li>일대다 (<code>@OneToMany</code>)</li>
<li>일대일 (<code>@OneToOne</code>)</li>
<li>다대다 (<code>@ManyToMany</code>)</li>
</ul>
<h4 id="단방향-양방향">단방향, 양방향</h4>
<p>테이블은 외래 키 하나로 조인을 사용해 양방향으로 쿼리가 가능하기에 방향이라는 개념이 없다. 다만, <strong>객체</strong>는 <strong>참조 필드를 가진 객체만 연관 객체를 조회</strong>할 수 있다.</p>
<ul>
<li>단방향: 객체 관계에서 한 쪽만 참조하는 것을 말함</li>
<li>양방향: 객체 양쪽이 서로를 참조하는 것을 말함</li>
</ul>
<h4 id="연관관계의-주인">연관관계의 주인</h4>
<p>데이터베이스에서는 외래 키 하나로 두 테이블이 연관관계를 맺는다. 이때 <strong>외래 키는 하나</strong>이다. 데이터베이스에 맞춰 객체를 양방향으로 매핑하는 경우에는 다만, 이 외래 키를 관리하는 곳이 <strong>2곳</strong>이 되어버린다.</p>
<p>이 관리를 위해 <strong>연관관계의 주인</strong>을 설정해야한다. 대부분은 <strong>외래 키를 가지고 있는 테이블의 엔티티</strong>가 관계의 주인이된다.</p>
<hr>
<h2 id="61-다대일">6.1 다대일</h2>
<p>다대일의 관계의 반대는 항상 일대다이다. 데이터베이스에서 테이블의 일(1),  다(N) 관계에서 <strong>외래 키는 항상 다(N)</strong>쪽에 있기에 다쪽이 <strong>연관관계의 주인</strong>이다.</p>
<h3 id="다대일-단방향-n1">다대일 단방향 [N:1]</h3>
<h4 id="member">Member</h4>
<pre><code class="language-java">@Entity
public class Member {
    @Id
    @Column(name = &quot;MEMBER_ID&quot;)
    private String id;

    private String username;

    @ManyToOne
    @JoinColumn(name = &quot;TEAM_ID&quot;)
    private Team team;
}</code></pre>
<h4 id="team">Team</h4>
<pre><code class="language-java">@Entity
public class Team {
    @Id @GeneratedValue
    @Column(name = &quot;TEAM_ID&quot;)
    private Long id;

    private String name;
}</code></pre>
<p>회원은 <code>Member.team</code>으로 팀 엔티티를 참조할 수 있지만, 그 반대로 팀에서는 회원을 참조하는 필드가 없는 위의 상황을 <strong>다대일 단방향 관계</strong>라고 한다.</p>
<p><code>@ManyToOne</code> 어노테이션과 함께 <code>@JoinColumn (name = &quot;TEAM_ID&quot;)</code>를 사용해 <code>Member.team</code> 필드를 <code>TEAM_ID</code> 외래 키와 매핑한다. 그렇기에 회원의 필드로 회원 테이블의 외래키를 관리하게된다.</p>
<h3 id="다대일-양방향-n1">다대일 양방향 [N:1]</h3>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/abfeee0e-1040-43a3-9bfd-5153558c7039/image.png" alt=""></p>
<p>다대일 양방향의 객체 연관관계에서 <strong>실선이 연관관계의 주인</strong>이고, <strong>점선은 연관관계의 주인이 아니다</strong>.</p>
<ul>
<li><p>양방향은 <strong>외래 키가 있는 쪽이 연관관계의 주인</strong>
일대다와 다대일 연관관계에서는 항상 <strong>다(N)에 외래키가 존재</strong>. JPA는 외래 키를 관리할 때 연관관계의 주인만 사용하며, 주인이 아닌 엔티티의 필드는 조회를 위한 JPQL 또는 객체 그래프 탐색을 위해서 사용.</p>
</li>
<li><p>양방향 연관관계는 <strong>항상 서로를 참조</strong>
양방향 연관관계는 항상 서로를 참조해야하는데, 한 쪽만 참조하는 경우에는 이 연관관계가 성립하지 않는다. <strong>항상 서로를 참조하게 하기 위해서는 연관관계 편의 메소드를 작성</strong>하는 것이 좋다. <br><br><code>setTeam()</code> 또는 <code>addMembers()</code> 같은 메소드가 이런 편의 메소드이다. 편의 메소드는 양쪽에 다 작성하는 경우 무한루프에 빠질 수 있기에 주의해야한다.</p>
</li>
</ul>
<hr>
<h2 id="62-일대다">6.2 일대다</h2>
<p>일대다 관계는 다대일 관계의 반대 방향이다. 다만, <strong>일대다 관계는 엔티티를 하나 이상 참조할 수 있기에 자바 컬렉션 중 하나를 사용</strong>해야한다.</p>
<h3 id="일대다-단방향-1n">일대다 단방향 [1:N]</h3>
<p>하나의 팀이 여러 회원을 참조할 수 있는 이런 관계를 일대다 관계라한다. 만약 팀은 회원들은 참조하지만, 회원이 팀을 참조하지 않으면 이를 두고 일대다 단방향이라한다.
<img src="https://velog.velcdn.com/images/summeryoung_/post/daf0b6ba-3829-4465-a8d9-b2918fe28656/image.png" alt=""></p>
<p>일대다 단방향의 경우에는 팀 엔티티의 <code>Team.members</code>를 통해 회원 테이블의 <code>TEAM_ID</code> 외래 키를 관리하게 된다. 보통은 자신이 매핑한 테이블의 외래 키를 관리하는데 여기서는 반대쪽 테이블의 외래 키를 관리하게 된다. 이는 특이한 양상이다.</p>
<pre><code class="language-java">@Entity
public clas Team{
    @Id @GeneratedValue
    @Column(name = &quot;TEAM_ID&quot;)
    private Long id;

    private String name;

    @OneToMany
    @JoinColumn(name = &quot;TEAM_ID&quot;) //MEMBER 테이블의 TEAM_ID (FK)
    private List&lt;Member&gt; members = new ArrayList&lt;&gt;();
}</code></pre>
<p><strong>일대다 단방향 관계의 매핑을 위해서는 <code>@JoinColumn</code>을 명시</strong>해야한다. 그렇지 않으면 JPA는 연결 테이블을 중간에 두고, <strong>연관관계를 관리하는 조인 테이블 전략</strong>을 기본으로 사용해 매핑한다.</p>
<h4 id="일대다-단방향의-단점">일대다 단방향의 단점:</h4>
<p>매핑한 객체가 관리하는 외래 키가 다른 테이블에 있다는 점</p>
<p>자신이 관리하는 테이블에 외래 키가 있으면, 엔티티의 저장과 연관관계 처리를 INSERT SQL 한 번으로 처리할 수 있는 것과 다르게 다른 테이블에 있다면, UPDATE SQL을 추가로 실행해야함.</p>
<p>예를 들면 <code>Member</code> 엔티티는 <code>Team</code> 엔티티를 모르고, 연관관계 정보는 <code>Team</code> 에서 관리하기에 <code>Member</code> 를 저장할 때에는 <code>TEAM_ID</code>에 아무 값도 저장되지 않고 <code>Team</code> 엔티티를 저장할 때, 참조값을 확인해 회원 테이블의 <code>TEAM_ID</code> 외래 키를 업데이트한다.</p>
<p>위와 같은 단점이 있기에 <strong>일대일 단방향</strong> 매핑보다는 <strong>다대일 양방향 매핑</strong>을 사용하는 것이 좋다.</p>
<h3 id="일대다-양방향-1n-n1">일대다 양방향 [1:N, N:1]</h3>
<p>일대다 양방향 매핑은 존재하지 않기에 다대일 양방향 매핑을 사용해야한다.  정확하게는 <code>@OneToMany</code>는 양방향 매핑에서 연관관계의 주인일 수 없다.</p>
<p><code>@ManyToOne</code> - <code>@OneToMany</code> 둘 중에 연관관계의 주인은 항상 다 쪽인 <code>@ManyToOne</code>을 사용하는 곳이다. 따라서 <code>mappedBy</code> 속성은 <code>@ManyToOne</code>에만 존재한다.</p>
<p>일대다 양방향 매핑이 완전히 불가능한 것은 아니고, 일대다 단방향 매핑 반대편에 같은 외래 키를 사용하는 다대일 단방향 매핑을 읽기 전용으로 추가하면 된다.</p>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/bec628d1-86e4-451e-870c-dcc5e50e3418/image.png" alt=""></p>
<pre><code class="language-java">@Entity
public class Team {
    @Id @GeneratedValue
    @Column(name = &quot;TEAM_ID&quot;)
    private Long id;

    private String name;

    @OneToMany
    @JoinColumn (name = &quot;TEAM_ID&quot;)
    private List&lt;Member&gt; members = new ArrayList&lt;Member&gt;();
}</code></pre>
<pre><code class="language-java">@Entity
public class Member {
    @Id @GeneratedValue
    @Column (name = &quot;MEMBER_ID&quot;)
    private Long id;

    private String username;

    @ManyToOne
    @JoinColumn (name = &quot;TEAM_ID&quot;, insertable = false,
        updatable = false)
    private Team team;
}</code></pre>
<p>일대다 단방향 매핑의 반대편에 다대일 단방향 매핑을 (멤버쪽에) 추가한다. 이때 일대다 단방향 매핑과 같이 외래 키 컬럼을 매핑하는 대신 둘 다 같은 키를 관리하는 문제를 방지하고자 다대일 쪽에는 <code>insertable=false</code>와 <code>updatable = false</code>로 설정에 <strong>읽기만 가능</strong>하게 설정한다.</p>
<p>하지만 이 경우 일대다 단방향 매핑이 가지는 단점을 그대로 가지기에 <strong>다대일 양방향 매핑을 사용</strong>하는게 좋다.</p>
<hr>
<h2 id="63-일대일-11">6.3 일대일 [1:1]</h2>
<p>일대일 관계는 양쪽이 서로 하나의 관계만 가짐. </p>
<ul>
<li>일대일 관계는 그 반대도 일대일 관계</li>
<li>테이블 관계에서 일대다, 다대일은 항상 다쪽이 외래키를 가지지만, <strong>일대일 관계는 주 테이블이나 대상 테이블 둘 중 어느 곳이나 외래키를 가질 수 있음</strong></li>
</ul>
<h3 id="주-테이블에-외래-키">주 테이블에 외래 키</h3>
<p>객체지향 개발자들이 주로 해당 방식을 선호하며, JPA에서 역시 주 테이블에 외래 키가 있으면 좀 더 편리하게 매핑할 수 있음.</p>
<h4 id="단방향">단방향</h4>
<p>회원과 사물함을 예로 뒀을 때.
<img src="https://velog.velcdn.com/images/summeryoung_/post/d5fbbfb9-872d-4b66-967e-49e08ef2fd28/image.png" alt=""></p>
<h4 id="member-1">Member</h4>
<pre><code class="language-java">@Entity
public class Member {
    @Id @GeneratedValue
    @Column (name = &quot;MEMBER_ID&quot;)
    private Long id;

    private String username;

    @OneToOne
    @JoinColumn(name = &quot;LOCKER_ID&quot;)
    private Locker locker;
}</code></pre>
<h4 id="locker">Locker</h4>
<pre><code class="language-java">@Entity
public class Locker {
    @Id @GeneratedValue
    @Column (name = &quot;LOCKER_ID&quot;)
    private Long id;

    private String name;
}</code></pre>
<ul>
<li><code>@OneToOne</code>: 일대일 관계이기에 객체 매핑에 해당 어노테이션 사용. </li>
<li><code>LOCKER_ID</code>: 외래 키에 유니크 제약 조건(UNI) 추가</li>
<li>해당 관계는 다대일 단방향과 거의 비슷.</li>
</ul>
<h4 id="양방향">양방향</h4>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/f26eb2fc-f70b-42fa-b3fe-b8b5df4329f1/image.png" alt=""></p>
<p>단방향일 때와 나머지는 동일하고, 매핑이 없던 <code>Locker</code> 엔티티 내에 매핑을 해주고, 양방향 관계일 때는 관계의 주인을 설정해줘야하기에 <code>Locker</code> 엔티티에 <code>mappedBy</code>를 설정해줘, 주인이 아님을 명시한다.</p>
<pre><code class="language-java">@Entity
public class Locker {
    ...

    @OneToOne (mappedBy = &quot;locker&quot;)
    private Member member;
}</code></pre>
<h3 id="대상-테이블에-외래-키">대상 테이블에 외래 키</h3>
<h4 id="단방향-1">단방향</h4>
<p>JPA에서는 아래처럼 일대일 관계 중 대상 테이블에 외래 키가 있는 단방향 관계는 지원하지 않는다. 단방향 관계를 Locker -&gt; Member 방향으로 수정하거나, 양방향 관계로 만들어 Locker를 연관관계의 주인으로 설정해야한다.</p>
<p>JPA 2.0부터는 일대다 단방향 관계에서 대상 테이블에 외래 키가 있는 매핑을 허용했지만, 일대일 단방향은 허용되지 않는다.
<img src="https://velog.velcdn.com/images/summeryoung_/post/fcd913cb-85e5-43cc-85d4-7ac9587cd28f/image.png" alt=""></p>
<h4 id="양방향-1">양방향</h4>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/6095ca8b-fd08-4b88-8f76-9d81bf17aae2/image.png" alt=""></p>
<h4 id="member-2">Member</h4>
<pre><code class="language-java">@Entity
public class Member {
    @Id @GeneratedValue
    @Column (name = &quot;MEMEBER_ID&quot;)
    private Long id;

    private String username;

    @OneToOne (mappedBy = &quot;member&quot;)
    private Locker locker;
}</code></pre>
<h4 id="locker-1">Locker</h4>
<pre><code class="language-java">public class Locker {
    @Id @GeneratedValue
    @Column (name = &quot;LOCKER_ID&quot;)
    private Long id;

    private String name;

    @OneToOne
    @JoinColumn (name = &quot;MEMBER_ID&quot;)
    private Member member;
}</code></pre>
<p>일대일 매핑에서 대상 테이블에 외래 키를 두고 싶을 때는 이렇게 <strong>양방향</strong>으로 매핑해야한다. 주 엔티티인 <code>Member</code> 엔티티 대신, 대상 엔티티인 <code>Locker</code> 를 연관관계의 주인으로 만들어 <code>LOCKER</code> 테이블의 외래 키를 관리하도록 한다.</p>
<hr>
<h2 id="64-다대다-nn">6.4 다대다 [N:N]</h2>
<p>관계형 데이터베이스에서 <strong>정규화된 테이블 2개로 다대다 관계를 표현할 수는 없다</strong>. 따라서 중간에 <strong>연결 테이블</strong>을 추가해줘야한다.</p>
<blockquote>
<ul>
<li>회원과 상품 사이의 관계</li>
</ul>
</blockquote>
<ul>
<li>둘은 한 회원이 여러 상품과 관계가 있을 수 있음</li>
<li>한 상품이 여러 회원과 관계가 있을 수 있음</li>
</ul>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/b9e3c063-5e7b-419a-9e88-83c7627034b5/image.png" alt=""></p>
<p>하지만 또 객체는 테이블과 다르게 객체 2개로 <strong>다대다 관계</strong>를 만들 수 있다. 예로 회원 객체는 컬렉션을 사용해 상품들을 참조하고, 반대로 상품들도 컬렉션ㅇ르 사용해 회원들을 참조하면된다.</p>
<h3 id="다대다-단방향">다대다: 단방향</h3>
<h4 id="member-3">Member</h4>
<pre><code class="language-java">@Entity
public class Member {
    @Id @Column (name = &quot;MEMBER_ID&quot;)
    private String id;

    private String username;

    @ManyToMany
    @JoinTable (name = &quot;MEMBER_PRODUCT&quot;,
        joinColumns = @JoinColumn(name = &quot;MEMBER_ID&quot;),
        inverseJoinColumns = @JoinColumn (name = &quot;PRODUCT_ID&quot;))
    private List&lt;Product&gt; products = new ArrayList&lt;Product&gt;();
}</code></pre>
<h4 id="product">Product</h4>
<pre><code class="language-java">@Entity
public class Product {
    @Id @Colun (name = &quot;PRODUCT_ID&quot;)
    private String id;

    private String name;
}</code></pre>
<p><strong><code>@ManyToMany</code></strong>: 회원 엔티티와 상품 엔티티를 매핑.
<strong><code>@JoinTable</code></strong>: <code>@ManyToMany</code>와 함께 사용해서 <strong>연결 테이블을 바로 매핑</strong>하여, 회원과 상품을 연결하는 회원_상품 엔티티 없이 매핑.</p>
<blockquote>
<h4 id="jointable의-속성"><code>@JoinTable</code>의 속성</h4>
</blockquote>
<ul>
<li><code>@JoinTable.name</code>: 연결 테이블을 지정.</li>
<li><code>@JoinTable.joinColumns</code>: 현재 <strong>방향</strong>인 회원과 <strong>매핑할 조인 컬럼 정보</strong> 지정. 여기서는 MEMBER_ID</li>
<li><code>JoinTable.inverseJoinColumns</code>: <strong>반대 방향</strong>인 상품과 <strong>매핑할 조인 컬럼 정보</strong> 지정. 여기서는 PRODUCT_ID.</li>
</ul>
<h4 id="저장">저장</h4>
<pre><code class="language-java">public void save() {
    Product productA = new Product();
    productA.setId(&quot;productA&quot;);
    productA.setName(&quot;상품A&quot;);
    em.persist(productA); //영속 상태 등록

    Member member1 = new Member();
    member1.setId(&quot;member1&quot;);
    member1.setUsername(&quot;회원1&quot;);
    member1.getProducts().add(productA); //연관관계 설정
    em.persist(member1); //영속 상태 등록
}</code></pre>
<p>회원과 상품의 연관관계를 설정했기에 회원1을 저장하면 <strong>연결 테이블에도 값이 저장</strong>됨.</p>
<h4 id="조회탐색">조회/탐색</h4>
<pre><code class="language-java">public void find() {
    Member member = em.find(Member.class, &quot;member1&quot;);
    List&lt;Product&gt; products = member.getProducts(); //객체 그래프 탐색
    for (Product product : products) {
        System.out.println(&quot;product.name =&quot; + product.getName());
    }
}</code></pre>
<p>SQL에서는 알아서 조인을 사용해서 연관된 상품들을 조회를 해준다. <code>@ManyToMany</code>를 사용해 매핑해주면 JPA에서 알아서 SQL을 적합하게 만들어낸다.</p>
<h3 id="다대다-양방향">다대다: 양방향</h3>
<p>다대다 매핑이기에 역방향도 <code>@ManyToMany</code>를 사용하고, 연관관계의 주인을 지정하기 위해 <code>mappedBy</code>를 사용해주면된다.</p>
<pre><code class="language-java">@Entity
public class Product {
    @Id
    private String id;

    @ManyToMany (mappedBy = &quot;products&quot;) //역방향 추가
    private List&lt;Member&gt; members;
}</code></pre>
<p>다대다 양방향 연관관계는 멤버 -&gt; 상품, 상품 -&gt; 멤버 양쪽에 추가를 해줘야하는데 이를 위해서 <strong>연관관계 편의 메소드</strong>를 추가해서 관리하는 것이 편하다.</p>
<pre><code class="language-java">products.add(product);
product.getMembers().add(this);</code></pre>
<p>양방향 연관관계에서 연관관계 편의 메소드를 추가했듯 다대다 양방향에서도 위처럼 한 군데에서 양쪽 객체(엔티티) 모두에 관계를 설정/업데이트 해주는 것이 좋다.</p>
<h3 id="다대다-매핑의-한계와-극복-연결-엔티티-사용">다대다: 매핑의 한계와 극복, 연결 엔티티 사용</h3>
<p><strong><code>@ManyToMany</code></strong> 사용 시 연결 테이블을 자동으로 처리해주기에 도메인 모델이 단순해지고 여러모로 편리하지만, <strong>실무에서 사용하기에는 한계</strong>가 있다고한다.</p>
<p>예로는 회원이 상품을 주문하면 연결 테이블에 주문 회원, 상품 아이디 외에 주문 수량이나 날짜 등의 컬럼이 필요하다.</p>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/c3006b87-c0a1-4329-ae25-264e4d7a4b27/image.png" alt=""></p>
<p>만약 이렇게 된다면 더 이상 <code>@ManyToMany</code>를 사용할 수 없다. 주문 엔티티나 상품 엔티티에 추가한 컬럼을 매핑할 수 없기 (= 필드의 값으로 가져올 수 있는 방법이 없다)</p>
<p>따라서 연결 테이블에 매핑하는 연결 엔티티를 만들고 추가 컬럼을 해당 엔티티에 매핑해야한다.</p>
<h4 id="member-4">Member</h4>
<pre><code class="language-java">@Entity
public class Member {
    @Id @Column (name = &quot;MEMBER_ID&quot;)
    private String id;

    //역방향
    @OneToMany(mappedBy = &quot;member&quot;)
    private List&lt;MemberProduct&gt; memberProducts;
}</code></pre>
<h4 id="product-1">Product</h4>
<pre><code class="language-java">@Entity
public class Product {
    @Id @Column (name = &quot;PRODUCT_ID&quot;)
    private String id;

    private String name;
}</code></pre>
<p>위의 코드에서는 상품 엔티티 -&gt; 회원상품 엔티티로 탐색 기능이 필요하지 않다고 생각해 연관관계를 두지 않았다.</p>
<h4 id="memberproduct">MemberProduct</h4>
<pre><code class="language-java">@Entity
@IdClass (MemberProductId.class)
public class MemberProduct {
    @Id
    @ManyToOne
    @JoinColumn (name = &quot;MEMEBER_ID&quot;)
    private Member member;

    @Id
    @ManyToOne
    @JoinColumn(name = &quot;PRODUCT_ID&quot;)
    private Product product;

    private int order;
}</code></pre>
<ul>
<li>기본 키를 매핑하는 <code>@Id</code>와 외래 키를 매핑하는 <code>@JoinColumn</code>을 동시에 사용해 기본 키+외래 키를 한 번에 매핑한다.</li>
<li><strong><code>@IdClass</code></strong>를 사용해 <strong>복합 기본 키</strong>를 매핑</li>
</ul>
<br>

<p><strong>복합 기본 키</strong>:
회원 상품 엔티티의 기본키는 <code>MEMBER_ID</code>와 <code>PRODUCT_ID</code> 둘로 이루어진 복합 기본 키이다. JPA에서 복합 키를 사용하기 위해서는 <strong>별도의 식별자 클래스</strong>를 만들고, <strong><code>@IdClass</code></strong>를 사용해 식별자 클래스를 지정해야한다.</p>
<ul>
<li>복합 키는 <strong>별도의 식별자 클래스</strong>로 만들어야함</li>
<li><strong><code>Serializable</code></strong>을 구현해야함</li>
<li><code>equals</code>와 <code>hasCode</code> 메소드를 구현해야함</li>
<li>기본 생성자가 있어야함</li>
<li>식별자 클래스는 public이어야함</li>
<li><code>@IdClass</code>를 사용하는 방법 외에 <code>@EmbeddedId</code>를 사용하는 방법 역시 있다.</li>
</ul>
<h4 id="memberproductid">MemberProductId</h4>
<pre><code class="language-java">public class MemberProductId implements Serializable {
    private String member; //MemberProductId.member와 연결
    private String product; //MemberProduct.product와 연결

    @Override
    public boolean equals (Object o) {...}
    @Override
    public int hashCode() {...}


}</code></pre>
<h4 id="식별-관계">식별 관계:</h4>
<p>회원상품에서 회원과 상품의 기본 키를 받아 자신의 기본 키로 사용하는 경우처럼, <strong>부모 테이블의 기본 키를 받아서 자신의 기본 키 + 외래 키</strong>로 사용하는 것을 데이터베이스 용어로 식별 관계라고 한다.</p>
<h4 id="저장-1">저장</h4>
<pre><code class="language-java">Member member 1 = new Member();
member1.setId(&quot;member1&quot;);
member1.setUserName(&quot;회원1&quot;);
em.persist(member1);

Product productA = new Product();
productA.setId(&quot;productA&quot;);
productA.setName(&quot;상품1&quot;);
em.persist(productA);

MemberProduct memberProdcut = new MemberProdcut();
memberProduct.setMember(member1); //주문회원 연관관계 설정
memberProduct.setProduct(productA);// 주문상품 연관관계 설정
memberProduct.setOrderAmount(2);

em.persist(memberProduct);</code></pre>
<p>위에서는 회원 상품 엔티티를 만들면서 연관된 회원 엔티티와 상품 엔티티를 설정한다. 회원상품 엔티티는 데이터베이스에 저장될 때 연관된 회원의 식별자와 상품의 식별자를 가져와 자신의 기본 키 값으로 사용.</p>
<h4 id="조회">조회</h4>
<pre><code class="language-java">MemberProduct memberProduct = em.find(MemberProduct.class, memberProductId);

Member member = memberProduct.getMember();
Product product = memberProduct.getProduct();</code></pre>
<p><strong>복합 키는 항상 식별자 클래스</strong>를 만들어야한다. 이렇게 생성한 식별자 클래스를 사용해 엔티티를 조회한다. </p>
<h3 id="다대다-새로운-기본-키-사용">다대다: 새로운 기본 키 사용</h3>
<p>복합 키를 사용하기 위해 식별자 클래스를 사용하는 것은 복잡하기에 이를 사용하지 않는 전략 역시 존재한다. <strong>데이터베이스에서 자동으로 생성해주는 대리 키</strong>를 사용하는 것이다.</p>
<p>장점은 간편하고 영구히 쓸 수 있고, 비즈니스에 의존하지 않는다는 점이다. 또한, ORM 시에 복합 키를 만들지 않아도 되기에 위에 있는 문제를 해결할 수 있다.
<img src="https://velog.velcdn.com/images/summeryoung_/post/93e61cfa-9dd4-4786-8f0c-d1e83b8f5128/image.png" alt=""></p>
<p>위를 보면 <code>ORDER_ID</code>라는 새로운 기본 키를 하나 만들고 <code>MEMBER_ID</code>와 <code>PRODUCT_ID</code>는 외래 키로만 사용한다.</p>
<pre><code class="language-java">@Entity
public class Order {
    @Id @GeneratedValue
    @Column (name = &quot;ORDER_ID&quot;)
    private Long Id;

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

    private int orderAmount;
}</code></pre>
<p>대리키를 사용하기에 이전에서 본 식별 관계와 복합 키를 사요하는 것보다 매핑이 단순하고 쉽다.</p>
<h4 id="저장-2">저장</h4>
<pre><code class="language-java">//회원과 상품은 이전과 동일
Member member 1 = new Member();
member1.setId(&quot;member1&quot;);
member1.setUserName(&quot;회원1&quot;);
em.persist(member1);

Product productA = new Product();
productA.setId(&quot;productA&quot;);
productA.setName(&quot;상품1&quot;);
em.persist(productA);

//주문 저장
Order order = new Order();
order.setMember(member1); //주문회원 연관관계 설정
order.setProduct(productA);//주문상품 연관관계 설정
order.setOrderAmount(2); //주문 수량
em.persist(order);</code></pre>
<h4 id="조회-1">조회</h4>
<pre><code class="language-java">Long orderId = 1L;
Order order = em.find(Order.class, orderId);

Member member = order.getMember();
Product product = order.getProduct();</code></pre>
<p>식별자 클래스를 사용하지 않으면 훨씬 더 단순한 코드를 통해 저장 및 조회가 가능하다.</p>
<h3 id="다대다-연관관계-정리">다대다 연관관계 정리</h3>
<p>다대다 관계를 일대다&amp;다대일 관계로 풀기 위해서는 <strong>연결 테이블을 만들 때 식별자를 어떻게 구성</strong>할지 선택해야한다.</p>
<blockquote>
<p><strong>식별 관계</strong>: 받아온 식별자를 기본 키 + 외래 키로 사용
<strong>비식별 관계</strong>: 받아온 식별자는 외래 키로만 사용하고, 새로운 식별자를 추가.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[자바 ORM 표준 JPA 프로그래밍] 4주차 스터디]]></title>
            <link>https://velog.io/@summeryoung_/%EC%9E%90%EB%B0%94-ORM-%ED%91%9C%EC%A4%80-JPA-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-4%EC%A3%BC%EC%B0%A8-%EC%8A%A4%ED%84%B0%EB%94%94</link>
            <guid>https://velog.io/@summeryoung_/%EC%9E%90%EB%B0%94-ORM-%ED%91%9C%EC%A4%80-JPA-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-4%EC%A3%BC%EC%B0%A8-%EC%8A%A4%ED%84%B0%EB%94%94</guid>
            <pubDate>Mon, 30 Mar 2026 21:01:11 GMT</pubDate>
            <description><![CDATA[<h1 id="5장-연관관계-매핑-기초">5장. 연관관계 매핑 기초</h1>
<p>엔티티들은 대부분 다른 엔티티와 연관관계가 있다. 이때 자바의 객체는 <strong>참조(주소)</strong>를 사용해 관계를 맺고, 테이블은 <strong>외래 키</strong>를 사용해 관계를 맺는다. 이 둘은 앞에서 나왔듯이 특징이 꽤 다르기에 <strong>객체의 참조와 테이블의 외래 키를 매핑하는 것</strong>이 ORM(객체 관계 매핑)에서 가장 까다로운 부분이다.</p>
<blockquote>
<h3 id="키워드-정리">키워드 정리</h3>
<p><strong>방향</strong>: <strong>단방향</strong>과 <strong>양방향</strong>이 존재. 방향은 <strong>객체관계에만 존재</strong>하고 <strong>테이블 관계는 항상 양방향</strong>이다.
<br> <strong>다중성</strong>: <strong>다대일, 일대다, 일대일, 다대다</strong> 다중성이 존재한다. <br>
<strong>연관관계의 주인</strong>: 객체를 양방향 연관관계로 만들 때에는 연관관계의 주인을 설정해줘야한다.</p>
</blockquote>
<hr>
<h2 id="51-단방향-연관관계">5.1 단방향 연관관계</h2>
<blockquote>
<ul>
<li>회원과 팀</li>
</ul>
</blockquote>
<ul>
<li>회원은 <strong>하나의 팀에만 소속</strong>될 수 있음</li>
<li>회원과 팀은 <strong>다대일</strong>관계
<span style="font-size:15px" >즉, 하나의 팀에는 여러 회원이 속할 수 있지만, 각 회원은 하나의 팀에 속해야함. </span></li>
</ul>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/a492914e-ce52-4ee3-a002-9caca3f81623/image.png" alt=""></p>
<h3 id="객체와-연관관계">객체와 연관관계</h3>
<ul>
<li>회원 객체는 <code>Member.team</code> 필드로 팀 객체와 연관관계를 맺음</li>
<li>회원 객체와 팀 객체는 <strong>단방향 관계</strong>
<span style="font-size:15px" > 멤버 객체에서는 <code>member.getTeam()</code> 과 같이 팀을 알 수 있지만, 반대로 팀에서는 어떤 멤버들이 팀에 들어있는지 확인할 수 없음 </span></li>
<li><strong>객체 그래프 탐색</strong>: 객체에서 참조를 이용해 연관관계를 탐색하는 것을 말함.</li>
</ul>
<h3 id="테이블-연관관계">테이블 연관관계</h3>
<ul>
<li>회원 테이블은 <code>TEAM_ID</code> 외래 키를 통해  팀 테이블과 연관관계를 매음</li>
<li>회원 테이블과 팀 테이블은 <strong>양방향 관계</strong>
<span style="font-size:15px" > 외래키를 사용해 회원과 팀을 조인할 수도 있고, 그 반대 역시 할 수 있다. </span></li>
<li><strong>조인</strong>: 외래키를 이용해 연관관계를 탐색하는 것을 말함.</li>
</ul>
<h4 id="객체와-테이블-연관관계의-차이점">객체와 테이블 연관관계의 차이점</h4>
<p>객체의 참조를 통한 연관관계는 항상 <strong>단방향</strong>이다. 즉, 이 연관관계를 양방향으로 만들기 위해서는 양쪽에 필드를 추가해 참조를 보관해야한다. 즉, 양방향 관계가 아니라 <strong>서로 다른 단방향 관계 2개</strong>라고도 할 수 있다. 이와 다르게 테이블은 외래 키 하나만으로 양방향으로 조인할 수 있다.</p>
<h3 id="객체-관계-매핑">객체 관계 매핑</h3>
<p>Member에서 회원 엔티티를 <strong>매핑</strong>하고, Team에서 팀 엔티티를 <strong>매핑</strong>.</p>
<ul>
<li>객체 연관관계: 회원 객체의 <code>Member.team</code> 필드 사용</li>
<li>테이블 연관관계: 회원 테이블의 <code>MEMBER_TEAM_ID</code> 외래키 컬럼 사용</li>
</ul>
<p>여기서 두 관계의 <code>Member.team</code>과 <code>MEMBER_TEAM_ID</code>를 매핑하는 것이 중요하다</p>
<h4 id="member">Member</h4>
<pre><code class="language-java">@Entity
public class Member {

    @Id
    @Column (name=&quot;MEMBER_ID&quot;)
    private String id;

    private String username;

    //연관관계 매핑
    @ManyToOne
    @JoinColumn(name = &quot;TEAM_ID&quot;)
    private Team team;

    //연관관계 설정
    public void setTeam (Team team) {
        this.team = team;
    }
}
</code></pre>
<h4 id="team">Team</h4>
<pre><code class="language-java">@Entity
public class Team {
    @Id
    @Column
    private String id;

    private String name;
}
</code></pre>
<h4 id="manytoone"><code>@ManyToOne</code></h4>
<p><strong>다대일(N:1)</strong> 관계라는 매핑 정보로 연관관계 매핑 시에는 <strong>다중성</strong>을 나타내는 어노테이션이 필수적이다.</p>
<h4 id="joincolumnnameteam_id"><code>@JoinColumn(name=&quot;TEAM_ID&quot;)</code></h4>
<p>조인 컬럼은 <strong>외래 키를 매핑</strong>할 때 사용하며 <code>name</code> 속성에는 매핑할 외래 키 이름을 지정한다. 해당 어노테이션은 생략이 가능하고, 속성은 아래와 같다.</p>
<ul>
<li><code>name</code>:  매핑할 외래 키 이름</li>
<li><code>referencedColumnName</code>: 외래 키가 참조하는 대상 테이블의 컬럼명</li>
<li><code>unique</code>, <code>nullable</code>, <code>insertable</code>...등</li>
</ul>
<hr>
<h2 id="52-연관관계-사용">5.2 연관관계 사용</h2>
<p>아래는 연관관계를 등록, 수정, 삭제, 조회하는 내용이다.</p>
<h3 id="저장">저장</h3>
<pre><code class="language-java">Team team1 = new Team(&quot;team1&quot;, &quot;팀1&quot;);
em.persist(team1);

Member member1 = new Member(&quot;member1&quot;, &quot;회원1&quot;);
member1.setTeam(team1); //연관관계 설정
em.persist(member1);

Member member2 = new Member(&quot;member2&quot;, &quot;회원2&quot;);
member2.setTeam(team1); //연관관계 설정
em.persist(member2);</code></pre>
<p>회원 엔티티가 팀 엔티티를 참조하고 저장하면, JPA는 참조한 팀의 식별자를 외래키로 사용해 적절한 등록 쿼리를 생성한다.</p>
<h3 id="조회">조회</h3>
<p>연관관계가 있는 엔티티를 조회하는 방법은 크게 2가지 이다</p>
<ul>
<li>객체 그래프 탐색</li>
<li>객체 지향 쿼리 사용 (JPQL)</li>
</ul>
<p><strong>객체 그래프 탐색</strong>
<code>member.getTeam()</code>을 사용해 <code>member</code>와 관련된 <code>team</code> 엔티티를 조회할 수 있음.</p>
<p><strong>객체지향 쿼리(JPQL) 사용</strong>
JPQL도 조인을 지원하는데 SQL과 문법은 조금 다르다.</p>
<pre><code class="language-java">String jpql = &quot;select m from Member m join m.team t where&quot; +
    &quot;t.name=:teamName&quot;;

List&lt;Member&gt; resultList = em.createQuery(jpql, Member.class)
    .setParameter(&quot;teamName&quot;, &quot;팀1&quot;);

for (Member member : resultList) {
    System.out.println(&quot;[query] member.username=&quot; 
        + member.getUsername());
}</code></pre>
<p><strong><code>from Member m join m.team t</code></strong>
 :회원이 팀과 관계를 가지고 있는 <strong>필드(<code>m.team</code>)</strong>를 통해 <code>Member</code>와 <code>Team</code>을 조인하였다. 이후 <code>where</code>절을 통해서 조인한 <code>t.name</code>을 검색조건으로 사용해 팀1에만 속한 팀을 검색한 것이다.</p>
<p> <strong><code>:teamName</code></strong>
 : <code>:</code>로 시작하는 것은 <strong>파라미터를 바인딩받는 문법</strong>이다.</p>
<h3 id="수정">수정</h3>
<pre><code class="language-java">Team team2 = new Team(&quot;team2&quot;, &quot;팀2&quot;);
em.persist(team2);

Member member = em.find(Member.class, &quot;member1&quot;);
member.setTeam(team2);</code></pre>
<p>수정의 경우에는 다른 메소드가 없기 때문에 엔티티를 <strong>조회</strong>한 후에 엔티티의 값을 변경해두면 트랜잭션 커밋 시에 플러시가 일어나며 변경 감지 기능이 작동한다.</p>
<h3 id="연관관계-제거">연관관계 제거</h3>
<p>회원1을 팀에 소속하지 않도록 연관관계를 제거</p>
<pre><code class="language-java">Member member1 = em.find(Member.class, &quot;member1&quot;);
member1.setTeam(null); //연관관계 제거</code></pre>
<p>엔티티를 조회한 후 수정하여 연관관계를 제거한다.</p>
<h3 id="연관된-엔티티-삭제">연관된 엔티티 삭제</h3>
<p>연관된 엔티티를 삭제하기 위해서는 <strong>기존의 연관관계를 먼저 제거</strong>한 후에 삭제해야한다. 그렇지 않으면 <strong>외래 키 제약조건</strong>으로 인해 데이터베이스에서 오류가 발생한다.</p>
<p>예를 들면 팀1에 회원1,2가 소속되어있다고 할 때, 팀1을 삭제하기 위해서는 이 둘 사이의 연관관계를 끊어줘야한다.</p>
<pre><code class="language-java">member1.setTeam(null);
member2.setTeam(null);
em.remove(team);</code></pre>
<hr>
<h2 id="53-양방향-연관관계">5.3 양방향 연관관계</h2>
<p>위에서는 회원에서 팀으로만 접근하는 <strong>다대일 단방향</strong> 매핑을 만들었다. 해당 장에서는 반대인 팀에서 회원으로 접근하는 관계를 추가해본다.</p>
<p>회원과 팀이 <strong>다대일 관계</strong>인데 반해, 팀과 회원은 <strong>일대다 관계</strong>이다. 여러 연관관계를 맺을 수 있기에 <strong>컬렉션</strong>을 사용해야한다. 
<span style="color:gray; font-size:15px" > 팀1에 여러 명의 회원들이 속해있을 수 있기에, 이 여러 명의 회원들을 관리하기 위해서 리스트/컬렉션을 사용해야한다. </span></p>
<p>데이터베이스 입장에서는 원래부터 외래 키 하나로 양방향 조회가 가능하기에 추가할 부분이 없다.</p>
<h3 id="양방향-연관관계-매핑">양방향 연관관계 매핑</h3>
<pre><code class="language-java">@Entity
public class Team {

    @Id
    @Column(name = &quot;TEAM_ID&quot;)
    private String id;

    private String name;

    @OneToMany (mappedBy = &quot;team&quot;)
    private List&lt;Member&gt; members = new ArrayList&lt;Member&gt;();
}</code></pre>
<h4 id="onetomany"><code>@OneToMany</code>:</h4>
<p>팀과 회원은 <strong>일대다 관계</strong>이기에, 팀 엔티티에는 컬렉션인 <code>List&lt;Member&gt; members</code>를 추가하고 어노테이션 역시 위의 <code>@OneToMany</code>를 사용한다.</p>
<p>여기서 <code>mappedBy</code> 속성은 <strong>양방향 매핑</strong>일 경우 사용하는 것으로 <strong>반대쪽 매핑의 필드 이름</strong>을 값으로 주면 된다. 위에서는 반대의 매핑이 <code>Member.team</code>이므로 <code>team</code>을 준다.</p>
<h3 id="일대다-컬렉션-조회">일대다 컬렉션 조회</h3>
<p>팀에서 회원 컬렉션(리스트)로 객체 그래프 탐색을 사용해 조회한 회원들을 출력하게된다</p>
<pre><code class="language-java">Team team = em.find(Team.class, &quot;team1&quot;);
List&lt;Member&gt; members = team.getMembers(); // 팀-&gt;회원 객체 그래프 탐색

for (Member member : members) {
    System.out.println(&quot;member.username = &quot;+ member.getUsername());
}</code></pre>
<hr>
<h2 id="54-연관관계의-주인">5.4 연관관계의 주인</h2>
<p><code>mappedBy</code>를 보면 회원 엔티티를 매핑할 때에는 사용하지 않은 반면, 팀 엔티티의 매핑에서만 사용하였다. 이 필요성을 해당 장에서 설명한다.</p>
<p>앞서 설명된 <strong>객체와 테이블</strong>의 연관관계 차이 때문에 JPA에서는 두 객체 연관관계 중 하나를 정해 <strong>테이블의 외래키를 관리하기 위한 연관관계의 주인</strong>을 정하게된다.</p>
<h3 id="양방향-매핑의-규칙-연관관계의-주인">양방향 매핑의 규칙: 연관관계의 주인</h3>
<p>양방향 연관관계 매핑 시 <strong>두 연관관계 중 하나를 연관관계의 주인으로 정해야한다.</strong> </p>
<ul>
<li>연관관계의 주인만 데이터베이스 연관관계와 매핑.</li>
<li>외래키를 관리(등록, 수정, 삭제). </li>
<li>주인이 아닌 쪽은 읽기만 가능하다.</li>
</ul>
<p>이 주인을 정하기 위해 <code>mappedBy</code> 속성을 사용하게된다.</p>
<ul>
<li>주인은 <code>mappedBy</code> 속성을 사용하지 않는다</li>
<li>주인이 아니라면, <code>mappedBy</code>를 통해 <strong>연관관계 주인</strong>을 저장해야함</li>
</ul>
<p>즉, <strong>연관관계의 주인을 정하는 것 = 외래 키 관리자 설정</strong>과도 같다. 앞서서는 <code>Member</code>가 키 관리자이자 연관관계의 주인이다.</p>
<h3 id="연관관계의-주인--외래키가-있는-곳">연관관계의 주인 = 외래키가 있는 곳</h3>
<p>위의 코드에서는 회원 테이블이 외래키를 갖고 있기에 <code>Member.team</code>이 이 연관관계의 주인이 된다. </p>
<ul>
<li>연관관계 주인이 아닌 <code>Team.members</code>에는 <code>mappedBy = &quot;team&quot;</code> 속성을 사용해 주인이 아님을 설정</li>
<li><code>mappedBy</code>의 값은 연관관계 주인인 엔티티의 필드</li>
<li>연관관계의 주인이 아닌 팀은 <strong>외래키를 읽기만 가능하고 변경하지 못함</strong></li>
</ul>
<pre><code class="language-java">@OneToMany (mappedBy = &quot;team&quot;)
private List&lt;Member&gt; members = new ArrayList&lt;&gt;();
</code></pre>
<hr>
<h2 id="55-양방향-연관관계-저장">5.5 양방향 연관관계 저장</h2>
<p>양방향 연관관계에서 연관관계의 주인이 외래 키를 관리하기에 <strong>주인이 아닌 방향은 값을 설정하지 않아도, 데이터베이스에 외래 키 값이 정상 입력</strong>된다.</p>
<pre><code class="language-java">team1.getMembers().add(member1); //무시</code></pre>
<p>즉, 위와 같은 코드가 필요하다고 생각되어도 결국 무시되고, 이 값은 외래 키 값에 영향을 주지 못한다.</p>
<hr>
<h2 id="56-양방향-연관관계의-주의점">5.6 양방향 연관관계의 주의점</h2>
<p><strong>연관관계의 주인에는 값을 입력하지 않고, 주인이 아닌 곳에만 값을 입력</strong>하는 것. 데이터베이스에 외래 키 값이 정상적으로 저장되지 않기에 주의해야한다.</p>
<p>이 경우 외래키에 널값이 입력된다.</p>
<h3 id="순수한-객체까지-고려한-양방향-연관관계">순수한 객체까지 고려한 양방향 연관관계</h3>
<p>어차피 연관관계의 주인이 아닌쪽에는 값을 저장해도 데이터베이스에 반영되지 않기에 저장할 필요가 없다. 하지만, <strong>객체 관점에서는 양쪽 방향에 모두 값을 입력하는 것이 가장 안전하다.</strong></p>
<pre><code class="language-java">...
member1.setTeam(team1); //연관관계: member1 -&gt; team1
team1.getMembers().add(member1); // 연관관계: team1 -&gt; member1

member2.setTeam(team1); //연관관계: member2 -&gt; team1
team1.getMembers().add(member2); //연관관계: team1 -&gt; member2</code></pre>
<p>이렇게 양쪽 모두에 관계를 설정해준 경우에만 후에 필요에 의해 팀에 있는 인원 수를 출력한다거나, 팀에 있는 팀원 목록을 출력할 때 객체로 접근할 수 있다.</p>
<h4 id="전체">전체</h4>
<pre><code class="language-java">Team team1 = new Team(&quot;team1&quot;, &quot;팀1&quot;);
em.persist(team1);

Member member1 = new Member(&quot;member1&quot;, &quot;회원1&quot;);

//양방향 연관관계 설정
member1.setTeam(team1); //연관관계 설정 member1 -&gt; team1
team1.getMembers().add(member1); //연관관계 설정 team1 -&gt; member1
em.persist(member1); //영속상태로 만듦.</code></pre>
<p>양쪽에 연관관계를 위처럼 설정해야 <strong>순수 객체 상태</strong>에서도 동작하며, 테이블의 <strong>외래 키에도 정상 입력</strong>된다. 테이블의 외래 키 값은 연관관계 주인인 <code>Member.team</code>의 값이 사용된다.</p>
<h3 id="연관관계-편의-메소드">연관관계 편의 메소드</h3>
<p>양방향 연관관계에서는 결국 명시적으로 양쪽에 값을 써주고 관리하는 불편함이 있고, 실수의 여지가 많다.</p>
<pre><code class="language-java">member.setTeam(team);
team.getMembers().add(member);</code></pre>
<p>양방향 관계에서 두 코드를 하나처럼 사용하는 것이 안전하기에 <code>Member</code> 클래스의 메소드를 수정해 코드를 리팩토링할 수 있다.</p>
<pre><code class="language-java">public class Member{
    private Team team;

    public void setTeam(Team team) {
        this.team = team.
        team.getMembers().add(this);
    }
}</code></pre>
<p>메소드 하나로 양쪽에 모두 값을 설정하도록 하면 실수나 빼먹을 가능성이 줄어든다.</p>
<h3 id="연관관계-편의-메소드-작성-시-주의사항-연관관계-제거">연관관계 편의 메소드 작성 시 주의사항: 연관관계 제거</h3>
<p>앞선 <code>setTeam()</code> 메소드에서는 팀을 변경해주어도 이전 팀의 멤버에서 변경한 팀원을 조회할 수 있다는 문제가 존재한다.</p>
<pre><code class="language-java">member1.setTeam(teamA);
member1.setTeam(teamB);
Member findMember = teamA.getMember(); //여전히 멤버1 조회 가능</code></pre>
<p>즉, 팀을 변경할 때 <strong>관계를 제거하지 않았다</strong>. 연관관계를 <strong>변경할 때는 기존 연관관계를 삭제</strong>하는 코드를 추가해줘야한다. 따라서 앞의 <code>setTeam()</code>을 아래와 같이 수정해줘야한다.</p>
<pre><code class="language-java">public void setTeam(Team team) {
    //이전의 연관관계 삭제 (팀이 있었다면 해당 팀의 컬렉션에서 멤버 삭제
    if (this.team != null) {
        this.team.getMembers().remove(this);
    }

    this.team = team;
    team.getMembers().add(this);
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[자바 ORM 표준 JPA 프로그래밍] 3주차 스터디]]></title>
            <link>https://velog.io/@summeryoung_/%EC%9E%90%EB%B0%94-ORM-%ED%91%9C%EC%A4%80-JPA-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-3%EC%A3%BC%EC%B0%A8-%EC%8A%A4%ED%84%B0%EB%94%94</link>
            <guid>https://velog.io/@summeryoung_/%EC%9E%90%EB%B0%94-ORM-%ED%91%9C%EC%A4%80-JPA-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-3%EC%A3%BC%EC%B0%A8-%EC%8A%A4%ED%84%B0%EB%94%94</guid>
            <pubDate>Sun, 29 Mar 2026 07:41:37 GMT</pubDate>
            <description><![CDATA[<h1 id="4장-엔티티-매핑">4장. 엔티티 매핑</h1>
<p>JPA에서는 <strong>매핑 어노테이션</strong>을 사용해 엔티티와 테이블을 매핑한다. 다양한 매핑 어노테이션들은 아래와 같이 구분해 볼 수 있다. </p>
<blockquote>
<ul>
<li>객체와 테이블 매핑: <code>@Entity</code>, <code>@Table</code></li>
</ul>
</blockquote>
<ul>
<li>기본 키 매핑: <code>@Id</code></li>
<li>필드와 컬럼 매핑: <code>@Column</code></li>
<li>연관관계 매핑: <code>@ManyToOne</code>, <code>@JoinColumn</code></li>
</ul>
<p>매핑 정보는 어노테이션 외에도 XML을 사용해 구성할 수도 있다고 한다.</p>
<hr>
<h2 id="41-entity">4.1 @Entity</h2>
<h4 id="entity"><code>@Entity</code>:</h4>
<p>JPA를 사용해 <strong>테이블과 매핑할 클래스</strong>에 붙이는 어노테이션으로 필수적이다. 이 어노테이션이 붙은 클래스는 JPA가 관리를 하게 된다.</p>
<pre><code class="language-java">@Entity
public class Member {}</code></pre>
<table>
<thead>
<tr>
<th align="center">속성</th>
<th align="center">기능</th>
</tr>
</thead>
<tbody><tr>
<td align="center"><strong>name</strong></td>
<td align="center">JPA에서 사용할 <strong>엔티티 이름</strong>을 정함. 기본값은 클래스 이름.</td>
</tr>
</tbody></table>
<p><code>@Entity</code> 어노테이션 사용 시 주의사항이 있다.</p>
<ul>
<li><strong>기본 생성자</strong> 필수</li>
<li>final 클래스나 enum, interface, inner 클래스에는 사용 불가</li>
<li>저장할 필드에 final 사용 불가</li>
</ul>
<hr>
<h2 id="42-table">4.2 @Table</h2>
<h4 id="table"><code>@Table</code>:</h4>
<p>엔티티와 <strong>매핑할 테이블</strong>을 지정해주는 어노테이션이다. 생략될 시에는 매핑한 엔티티 이름을 테이블 이름으로 사용한다.</p>
<table>
<thead>
<tr>
<th align="center">속성</th>
<th align="center">기능</th>
</tr>
</thead>
<tbody><tr>
<td align="center"><strong>name</strong></td>
<td align="center">매핑할 테이블의 이름</td>
</tr>
<tr>
<td align="center">catalog</td>
<td align="center">catalog 기능이 있는 DB에서 catalog를 매핑</td>
</tr>
<tr>
<td align="center">schema</td>
<td align="center">schema 기능이 있는 DB에서 schema를 매핑</td>
</tr>
</tbody></table>
<pre><code class="language-java">@Entity
@Table(name=&quot;MEMEBER&quot;)
public class Member {}</code></pre>
<hr>
<h2 id="43-다양한-매핑-사용">4.3 다양한 매핑 사용</h2>
<blockquote>
<h4 id="요구사항">요구사항</h4>
</blockquote>
<ol>
<li>회원은 일반 회원과 관리자로 구분</li>
<li>회원 가입일과 수정일 존재</li>
<li>회원을 설명할 수 있는 필드 존재 (길이 제한 없음)</li>
</ol>
<pre><code class="language-java">@Entity
@Table(name=&quot;MEMBER&quot;)
public class Member {
    @Id
    @Column(name = &quot;ID&quot;)
    private String id;

    @Column(name=&quot;NAME&quot;)
    private String username;

    private Integer age;

    @Enumerated(EnumType.STRING)
    private RoleType roleType;

    @Temporal (TemporalType.TIMESTAMP)
    private Date createdDate;

    @Temporal (TemporalType.TIMESTAMP)
    private Date lastModifiedDate;

    @Lob
    private String description;
}

public enum RoleType {
    ADMIN, USER
}</code></pre>
<h4 id="enumerated"><strong><code>@Enumerated</code></strong>:</h4>
<p>자바의 <code>enum</code>을 사용해 회원의 타입을 구분한다. <strong>자바의 <code>enum</code>을 사용하려면 <code>@Enumerated</code> 어노테이션으로 매핑해야한다.</strong></p>
<h4 id="temporal"><code>@Temporal</code>:</h4>
<p>자바의 날짜 타입을 사용할 때 매핑하는 어노테이션.</p>
<h4 id="lob"><code>@LOB</code>:</h4>
<p>CLOB, BLOB 타입의 매핑을 위해 사용하는 어노테이션. 위에서는 길이 제한이 없는 회원 설명 필드의 매핑을 위해 사용됐다.</p>
<hr>
<h2 id="44-데이터베이스-스키마-자동-생성">4.4 데이터베이스 스키마 자동 생성</h2>
<p>JPA에서는 클래스 매핑 정보를 통해 어떤 테이블의 어떤 컬럼을 사용하는지 알 수 있다. 이 매핑 정보와 데이터베이스 방언을 사용해 JPA에서는 <strong>데이터베이스 스키마를 생성</strong>할 수 있다.</p>
<p>스키마 자동 생성 기능을 사용하기 위해서는 persistence.xml에 아래의 속성을 추가해줘야한다.</p>
<pre><code class="language-xml">&lt;property name = &quot;hibernate.hbm2ddl.auto&quot; value=&quot;create&quot; /&gt;</code></pre>
<p>해당 속성은 애플리케이션 실행 시점에 <strong>데이터베이스 테이블을 자동으로 생성</strong>하는데, 이때 기존의 테이블을 <strong>삭제</strong>하고 <strong>다시 생성</strong>한다. 위의 속성에는 <code>create</code>, <code>create-drop</code>, <code>update</code>, <code>validate</code>, <code>none</code>이 존재한다. </p>
<p>다만 DLL을 수정하는 옵션은 운영서버에서 절대 사용하면 안된다. 운영 중인 데이터베이스의 테이블이나 컬럼을 삭제하는 일이 벌어질 수 있다.</p>
<p>스키마 자동 생성을 통해 자동으로 생성되는 DDL(데이터 정의어)는 지정한 데이터베이스 방언에 따라 달라진다. 다만, 스키마 자동 생성 기능의 DDL은 완벽하지 않으니 운영환경에서의 사용은 지양하는 것이 좋다.</p>
<hr>
<h2 id="45-ddl-생성-기능">4.5 DDL 생성 기능</h2>
<p>스키마 자동 생성을 통해 만들어지는 DDL에 <strong>제약조건</strong>을 추가할 수 있다.</p>
<pre><code class="language-java">@Entity
@Table (name=&quot;MEMBER&quot;, 
        uniqueConstraints = {@UniqueConstraint(
            name = &quot;NAME_AGE_UNIQUE&quot;,
            columnNames = {&quot;NAME&quot;, &quot;AGE&quot;} 
       )})
public class Member {
    @Id
    @Column (name=&quot;ID&quot;)
    private String id;

    @Column(name=&quot;NAME&quot;, nullable=false, length=10)
    private String username;
}</code></pre>
<ul>
<li><code>nullable</code>: 해당 컬럼에 <code>null</code>이 들어가지 않도록 설정할 수 있음. (<code>not null</code>)</li>
<li><code>length</code>: 해당 컬럼의 데이터의 길이를 제한하는 제약 조건을 설정할 수 있음.</li>
<li><code>uniqueConstraints</code>: 유니크 제약조건을 설정. 위의 코드에서는 NAME과 AGE를 유니크한 컬럼이 되도록 설정.</li>
</ul>
<hr>
<h2 id="46-기본-키-매핑">4.6 기본 키 매핑</h2>
<h4 id="id"><code>@Id</code></h4>
<p>기본키(primary key)의 매핑을 위해 사용하는 어노테이션으로. 기본 키를 <strong>애플리케이션에서 직접 할당</strong>할 때 사용한다.</p>
<blockquote>
<h4 id="jpa-제공-데이터베이스-기본--키-생성-전략">JPA 제공 데이터베이스 기본  키 생성 전략</h4>
</blockquote>
<ol>
<li><strong>직접 할당</strong>: 기본 키를 애플리케이션에서 직접 할당</li>
<li><strong>자동 생성</strong>: 대리 키 사용 방식<ul>
<li><code>IDENTITY</code>: 기본 키 생성을 데이터베이스에 위임</li>
<li><code>SEQUENCE</code>: 데이터베이스 시퀀스를 사용해 기본 키를 할당</li>
<li><code>TABLE</code>: 키 생성 테이블을 이용</li>
</ul>
</li>
</ol>
<p>자동 생성 전략이 다양한 이유는 데이터베이스 벤더마다 지원하는 방식이 다르기 때문. MySQL은 기본 키 값을 자동으로 채우는 <code>AUTO_INCREMENT</code> 기능을 제공.</p>
<h3 id="1-기본-키-직접-할당-전략">(1) 기본 키 직접 할당 전략</h3>
<p><code>@Id</code>: 기본 키 직접 할당을 위해서는 해당 어노테이션을 사용하면 된다. 적용 가능한 자바 타입으로는 기본형, 래퍼(Wrapper)형,  문자열, 날짜 등이 있다.</p>
<p>해당 방식은 <code>em.persist()</code>를 사용해 엔티티를 저장하기 전 <strong>애플리케이션에서 기본 키를 직접 할당</strong>하는 방식이다.</p>
<pre><code class="language-java">Board board = new Board();
board.setId(&quot;id1&quot;);
em.persist(board);</code></pre>
<p>만약 식별자 값 없이 저장하게 되면 예외가 발생한다.</p>
<h3 id="2-identity-전략">(2) IDENTITY 전략</h3>
<p>IDENTITY 전략은 기본 키 생성을 <strong>데이터베이스에 위임</strong>하는 전략이다. 
<span style="color:gray; font-size:80%"> 주로 MySQL, PostgreSQL, SQL Server, DB2에서 사용한다고한다. 예를 들면 MySQL의 <code>AUTO_INCREMENT</code> 기능을 통해 데이터베이스에서 식별자 값을 할당하지 않아도 자동으로 생성해준다. </span></p>
<p>데이터베이스에서 값을 저장할 때 식별자 값을 저장하지 않아도 순서대로 값을 채워주게된다.</p>
<p>해당 전략은 데이터베이스에 값을 저장하고 나서야 기본 키 값을 구할 수 있을 때 사용한다. 직접 할당 전략과는 다르게 <strong><code>@Id</code> 어노테이션과 함께 <code>@GeneratedValue</code> 어노테이션을 사용하고 식별자 생성 전략을 선택</strong>해야할 필요성이 있다. </p>
<p>IDENTITY 전략에서는 <code>@GeneratedValue</code>의 strategy 속성 값을 <strong><code>GenerationType.IDENTITY</code></strong>로 지정하면 된다. JPA에서는 그럼 기본 키 값을 얻어오기 위해 데이터베이스를 추가로 조회하게된다.</p>
<pre><code class="language-java">@Id
@GeneratedValue( strategy = GenerationType.IDENTITY)
private Long id;</code></pre>
<p>이때 식별자 값은 데이터를 데이터베이스에 저장하는 시점에 데이터베이스가 생성한 값을 JPA가 조회하는 식으로 얻어온다. </p>
<p>영속 상태가 되려면 식별자 값이 필요하기에 이 전략에서는 <code>em.persist()</code> 호출 즉시 INSERT SQL이 데이터베이스에 전달되어, 트랜잭션을 지원하는 쓰기 지연이 동작하지 않는다.</p>
<h3 id="3-sequence-전략">(3) SEQUENCE 전략</h3>
<p>데이터베이스 시퀀스는 <strong>유일한 값을 순서대로 생성하는 특별한 데이터베이스 오브젝트</strong>이다. 해당 전략을 사용하기 위해서는 시퀀스를 생성해야한다.
<span style="color:gray; font-size:80%"> 오라클, PostgreSQL, DB2, H2 데이터베이스에서 사용할 수 있다.</span></p>
<pre><code class="language-sql">CREATE SEQUENCE BOARD_SEQ START WITH 1 INCREMENT BY 1;</code></pre>
<p>위 처럼 시퀀스를 생성하고,</p>
<pre><code class="language-java">@SequenceGenerator (
    name = &quot;BOARD_SEQ_GENERATOR&quot;,
    sequenceName = &quot;BOARD_SEQ&quot;, //매핑할 데이터베이스 시퀀스 이름
    initialValue = 1, allocationSize = 1)

@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE,
                generator = &quot;BOARD_SEQ_GENERATOR&quot;)
private Long id;</code></pre>
<h4 id="sequencegenerator"><code>@SequenceGenerator</code>:</h4>
<p>시퀀스 생성기를 등록하기 위한 어노테이션으로 사용할 <strong>데이터베이스 시퀀스 매핑</strong>을 위해 사용한다. <strong><code>sequenceName</code></strong>을 앞에서 생성한 데이터베이스 시퀀스의 이름으로 지정한다.</p>
<p>JPA에서는 이 <strong>시퀀스 생성기</strong>를 실제 데이터베이스의 <code>BOARD_SEQ</code> 시퀀스와 매핑하게된다. </p>
<p>시퀀스 전략의 경우 <code>em.persist()</code> 호출 시 데이터베이스 시퀀스를 사용해 식별자를 조회한다. 이렇게 조회한 식별자를 엔티티에 할당하고 이 엔티티를 영속성 컨텍스트에 저장하기에, 이후 <strong>트랜잭션을 커밋하고 플러시할 때 엔티티를 데이터베이스에 저장</strong>할 수 있다.</p>
<h3 id="4-table-전략">(4) TABLE 전략</h3>
<p>테이블 전략은 키 생성 전용 테이블을 하나 만들어 이름과 값으로 사용할 컬럼을 만들어 데이터베이스 시퀀스를 흉내내는 전략이다. 이 경우에도 시퀀스 전략처럼 키 생성 용도로 사용할 테이블을 먼저 만들어야한다.</p>
<pre><code class="language-java">@TableGenerator (
    name = &quot;BOARD_SEQ_GENERATOR&quot;,
    table = &quot;MY_SEQUENCES&quot;,
    pkColumnValue = &quot;BOARD_SEQ&quot;, allocationSize = 1
)

@Id
@GeneratedValue (strategy = GenerationType.TABLE,
                generator = &quot;BOARD_SEQ_GENERATOR&quot;)
private Long id;</code></pre>
<h4 id="tablegenerator"><code>@TableGenerator</code>:</h4>
<p>테이블 키 생성기를 등록하기 위한 어노테이션. 테이블 키 생성기의 이름을 등록하고, 미리 생성해둔 키 생성용 테이블을 매핑한다.</p>
<p>테이블 전략 사용을 위해서는 <code>@GeneratedValue</code>의 strategy를 위처럼 설정해줘야한다.</p>
<p><span style="color:gray; font-size:80%"> 처음에 읽었을 때는 감이 잘 안왔는데 결과 테이블을 살펴보니, 특정 테이블 하나만을 위한 키가 아니라 여러 테이블의 키를 한 번에 여기서 관리하는 느낌이다. 예를 들면 게시글 테이블의 다음 값이 5, 댓글은 10, 이런 식으로 모아서 관리하고 여기서 찾아서 기본 키를 할당하는 듯 하다.</span></p>
<h3 id="5-auto-전략">(5) AUTO 전략</h3>
<p><code>GenerationType.AUTO</code>를 사용해 전략을 지정해주면 데이터베이스 방언에 따라 앞서 나온 <code>IDENTITY</code>, <code>SEQUENCE</code>, <code>TABLE</code> 전략 중 하나를 자동으로 선택하게된다.</p>
<p>이 전략의 장점은 <strong>데이터베이스를 변경해도 코드를 수정할 필요가 없다</strong>는 점이다. </p>
<p>다만, <code>SEQUENCE</code>나 <code>TABLE</code> 전략이 선택될 경우에는 시퀀스나 테이블을 미리 만들어두어야한다. 이 부분도 스키마 자동 생성기능을 사용하면 하이버네이트에서 기본값을 사용해 적절한 시퀀스나 테이블을 만들어주긴한다.</p>
<blockquote>
<h4 id="정리">정리</h4>
</blockquote>
<ul>
<li>직접할당: <code>em.persist()</code> 호출 전 애플리케이션에서 식별자 값을 직접 할당. 없을 경우 예외 발생</li>
<li>SEQUENCE: 시퀀스에서 식별자 값을 획득 후 영속성 컨텍스트에 저장</li>
<li>TABLE: 시퀀스 생성용 테이블에서 식별자 값 획득 후 영속성 컨텍스트에 저장</li>
<li>IDENTITY: 데이터베이스에 엔티티를 저장해 식별자 값 획득 후 영속성 컨텍스트에 저장</li>
</ul>
<p>식별자 선택을 할 때에는 <strong>자연 키 보다는 대리 키를 사용</strong>하는 편이 좋다고 한다. 자연 키는 아무리 잘 고른다 해도 예기치 못하게 변경이 일어날 수 있는 상황이 많고, 널값이 들어가는 경우도 있기 때문이다.</p>
<hr>
<h2 id="47-필드와-컬럼-매핑">4.7 필드와 컬럼 매핑</h2>
<h4 id="column"><code>@Column</code></h4>
<p><strong>객체 필드를 테이블의 컬럼에 매핑</strong>하는 어노테이션으로 가장 많이 사용. 속성 중에서는 <code>name</code>, <code>nullable</code>이 주로 사용된다.</p>
<h4 id="enumerated-1"><code>@Enumerated</code></h4>
<p><strong>자바의 <code>enum</code> 타입</strong>을 매핑할 때 사용된다.</p>
<ul>
<li><code>EnumType.ORDINAL</code>: enum에 정의된 순서대로 데이터베이스에 인덱스 같은 값이 저장된다.</li>
<li><code>EnumType.STRING</code>: enum에 정의된 이름이 그대로 데이터베이스에 저장된다. 수정이나 순서 변경 등이 발생해도 안전하다는 장점이 존재하지만 저장되는 크기가 크다.</li>
</ul>
<h4 id="temporal-1"><code>@Temporal</code></h4>
<p><strong>날짜 타입(<code>java.util.Date</code>, <code>java.tuil.Calendar</code>)을 매핑</strong>할 때 사용된다. 해당 어노테이션 생략 시 자바의 <code>Date</code>와 유사한 <code>timestamp</code>로 정의되게 된다.</p>
<h4 id="lob-1"><code>@Lob</code></h4>
<p>지정할 수 있는 속성이 없다. 다만, <strong>매핑 필드 타입이 문자면 CLOB으로 나머지는 BLOB</strong>으로 매핑된다.</p>
<h4 id="transient"><code>@Transient</code></h4>
<p>해당 필드는 <strong>매핑을 하지 않는다.</strong> 데이터베이스에 저장하지 않고 조회하지 도 않는, 객체에서 임시로 어떤 값을 보관하기 위해서 사용하는 어노테이션이다.</p>
<h4 id="access"><code>@Access</code></h4>
<p>JPA가 엔티티의 데이터에 접근하는 방식을 지정한다.</p>
<ul>
<li>필드 접근: <code>AccessType.FIELD</code>로 지정하며 필드에 직접 접근한다.</li>
<li>프로퍼티 접근: <code>AccessType.PROPERTY</code>로 지정하며 <strong>접근자</strong>(<code>getter</code>)를 사용한다.</li>
</ul>
<pre><code class="language-java">//필드 접근
@Entity
@Access (AccessType.FIELD)
public class Member{
    @Id
    private String id;
}</code></pre>
<p>위의 경우 <code>@Id</code>를 필드에 붙여주었기에 <code>@Access (AccessType.FIELD)</code>로 설정한 것과 같기에 어노테이션을 생략해도 된다.</p>
<pre><code class="language-java">//프로퍼티 접근
@Entity
@Access (AccessType.PROPERTY)
public class Member{
    private String id;

    private String data1;

    @Id
    public String getData1() {
        return data1;
    }
}</code></pre>
<p>위의 경우 <code>@Id</code>를 프로퍼티에 붙여주었기에 <code>@Access (AccessType.PROPERTY)</code>로 설정한 것과 같아 어노테이션을 생략해도 된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[데이터베이스 4주차 SQL 문제 풀이]]></title>
            <link>https://velog.io/@summeryoung_/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-4%EC%A3%BC%EC%B0%A8-SQL-%EB%AC%B8%EC%A0%9C-%ED%92%80%EC%9D%B4</link>
            <guid>https://velog.io/@summeryoung_/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-4%EC%A3%BC%EC%B0%A8-SQL-%EB%AC%B8%EC%A0%9C-%ED%92%80%EC%9D%B4</guid>
            <pubDate>Fri, 27 Mar 2026 10:27:57 GMT</pubDate>
            <description><![CDATA[<blockquote>
<h3 id="q1">Q1.</h3>
</blockquote>
<h4 id="프로그래머스-클래스-문제-등록한-방이-2개-이상인-헤비-유저의-등록-건수-출력">프로그래머스 클래스 문제: 등록한 방이 2개 이상인 헤비 유저의 등록 건수 출력</h4>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/6d54e4ee-c2c3-4731-818a-f5d5fdbf66a2/image.png" alt=""></p>
<p>처음에 서브쿼리를 안쓰고 문제를 풀 수 있나 생각해봤는데 <code>HOST_ID</code>를 기준으로 그룹을 만들어서 <code>COUNT</code>를 해야하는데 그룹을 만든 이상, 해당 호스트 아이디의 다른 튜플들을 출력할 수 없을 것 같아 결국 서브 쿼리를 사용했다.</p>
<p>하위 쿼리에서 우선 <code>HOST_ID</code>를 기준으로 그룹을 만들었을 때 <code>COUNT(*)</code>의 개수가 2 이상인 것들의 <code>HOST_ID</code>를 선택해 이 호스트 아이디와 일치하는 튜플들만 상위 쿼리에서 출력하도록 하였다.</p>
<p>처음에 <code>HOST_ID</code>가 아니라 <code>ID</code>를 하위 쿼리에서 셀렉트해서 잘못 출력했었다. <code>ID</code>를 기준으로하면, <code>HOST_ID</code>로 그룹화했을 때 하나의 등록 ID만 남으니 그렇게 하면 안됐던 것 같다.</p>
<ul>
<li>풀고 나서 찾아보니 <code>EXISTS</code> 로도 풀 수 있다는거 같아서 찾아보고 풀어보려했는데? 약간 감이 잘 안왔다. 그냥 서브쿼리가 아니라 상관 부속질의(그러니까 상하 관계있는? 서로 연결된?) 그런 서브쿼리가 필요했다.</li>
</ul>
<pre><code class="language-sql">SELECT ID, NAME, HOST_ID
FROM PLACES P1
WHERE EXISTS (
    SELECT *
    FROM PLACES P2
    GROUP BY HOST_ID
    HAVING COUNT(*) &gt;= 2
)
ORDER BY ID;</code></pre>
<p>처음에는 위에처럼 썼는데 의외로 에러는 안났다. 대신 당연히 답은 틀렸는데 이유를 찾아보니 <strong>두 쿼리 사이의 연결관계</strong>가 설정되지 않아서 였다. 쿼리를 보면, 상위 쿼리의 행을 가지고 들어가지 않으니 하위 쿼리에서 전체 <code>PLACES</code> 테이블을 <code>HOST_ID</code>를 기준으로 그룹화하고 하나라도 <code>HAVING</code>의 조건을 만족하면 참을 반환하고 있었다.</p>
<pre><code class="language-sql">SELECT * 
FROM PLACES P1 
WHERE EXISTS ( 
    SELECT * 
    FROM PLACES P2 
    WHERE P1.HOST_ID = P2.HOST_ID 
    GROUP BY HOST_ID 
    HAVING COUNT(*) &gt;= 2 ) 
ORDER BY ID;</code></pre>
<p>이걸 고쳐서 위에처럼 연결관계를 설정해주었다. <code>WHERE</code> 절을 사용해서 상위 쿼리에서 가지고 들어온 행의 <code>HOST_ID</code>와 일치하는 경우의 전체 등록 수만을 기준으로 <code>EXISTS</code>를 판단할 수 있도록 수정했다.</p>
<hr>
<blockquote>
<h3 id="q2">Q2.</h3>
</blockquote>
<h4 id="프로그래머스-클래스-문제-입양되지-않은-동물-중-보호소에-들어온지-오래된-3마리-출력">프로그래머스 클래스 문제: 입양되지 않은 동물 중 보호소에 들어온지 오래된 3마리 출력</h4>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/91197d83-ced0-4ac9-9121-2b5c1dfb1437/image.png" alt=""></p>
<p>다른 부분은 풀 수 있었는데 처음에 <strong>가장 오래된 3마리</strong>를 어떻게 출력해야할까?였다. 우선은 <code>DATETIME</code>으로 정렬하는것까지는 알았는데 딱 3마리 출력하는걸 모르겠어서 찾아봤다.</p>
<p><code>LIMIT</code>을 사용하면 출력할 행의 수를 제한할 수 있었다. 이걸 사용해서 앞에 짜두었던 SQL문에서 제한을 3줄만 출력하도록 제한해 문제를 풀었다.</p>
<p>입양되지 않은 동물을 찾는 것은 전에는 서브쿼리 사용했었는데, 이번에는 입양된 동물들 테이블 기준 외부 조인을 사용해서 합친 테이블에서 <code>ANIMAL_OUT</code> 테이블의 속성이 널인 것을 이용해서 <code>WHERE</code> 문을 통해 구했다.</p>
<hr>
<blockquote>
<h3 id="q3">Q3.</h3>
</blockquote>
<h4 id="사이버캠퍼스-문제-입양시각-구하기">사이버캠퍼스 문제: 입양시각 구하기</h4>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/f0af7dc1-6ae7-43d8-9127-f97d491c3664/image.png" alt=""></p>
<p><code>DATETIME()</code>은 <code>연-월-일 시:분:초</code>를 모두 갖는데, 이런 데이터를 <strong><code>HOUR()</code></strong> 함수에 넣게되면 시간만을 얻을 수 있다.</p>
<p>조건에 따라 테이블에 시간을 출력하기 위해 <code>HOUR(DATETIME)</code>을 통해 시간만 얻어낸 것을 출력명을 지정해주고, 해당 속성명을 사용해 아래에서도 그룹화를 진행했다. <code>WHERE</code>절의 조건에서는 SELECT에서 사용한 별칭을 사용할 수 없기에 따로 함수를 사용했다.</p>
<p>문제 풀다가 기왕 정리하는김에 날짜 데이터 다루는 것과 관련된 내용 찾을 겸 정리를 했다.</p>
<blockquote>
<h4 id="날짜-데이터타입">날짜 데이터타입</h4>
<p><strong>DATE</strong>: 날짜 정보를 갖는 타입. <strong>&#39;YYYY-MM-DD&#39;</strong> 형식 사용
<strong>TIME</strong>: 시간 정보를 갖는 타입. <strong>&#39;YYYY-MM-DD&#39;</strong> 형식 사용
<strong>DATETIME</strong>: 날짜와 시간 정보를 모두 갖는 타입. <strong>&#39;YYYY-MM-DD YYYY-MM-DD&#39;</strong> 형식 사용 <br> </p>
</blockquote>
<h4 id="사용">사용</h4>
<p>SELECT나 WHERE 절에서 <strong><code>YEAR()</code>, <code>MONTH()</code>, <code>DAY()</code>, <code>HOUR()</code>, <code>MINUTE()</code>, <code>SECOND()</code></strong> 를 통해 사용하면 각각 연/월/일/시/분/초로 출력 형식을 정하거나 조건을 확인할 수 있다.</p>
<p>정리하다가 궁금해진거는 <code>SELECT</code>에서 설정한 별칭을 왜 <code>GROUP BY</code>에서 사용할 수 있을까였다. <code>ORDER BY</code>는 <code>SELECT</code> 이후에 실행되니까 그렇다고쳐도, <code>GROUP BY</code>는 이전에 실행되니까...?</p>
<p>=&gt; 찾아봤을 때는 실행되는 순서는 저게 맞고, 실제로는 사용이 안되지만? MySQL 같은 현대적인 DB 엔진에서 저런 부분을 허용해주는 경우가 있다고 한다. 오라클 같은 데에서는 못 쓸 수도 있다고...? </p>
<p>위 상황을 생각해서 아래처럼 수정을 했다. WHERE 절에서 사용한 것처럼 <code>HOUR()</code>안에 넣어주었다.
<img src="https://velog.velcdn.com/images/summeryoung_/post/1d35cf98-b771-4db4-801f-4ff27b1c6da1/image.png" alt=""></p>
<hr>
<blockquote>
<h3 id="q4">Q4.</h3>
</blockquote>
<h4 id="null-처리하기">NULL 처리하기</h4>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/abdcf5f4-be18-4d2c-ac66-d27d68653893/image.png" alt=""></p>
<p>앞의 다른 문제랑 비슷하게 조건은 처리할 수 있었다. NULL 값을 처리하는 방법을 몰랐었는데 클래스에 있는 문제 풀면서 익혔던 부분이라 <code>IFNULL</code>을 사용해서 처리했다.</p>
<blockquote>
<h4 id="정리">정리</h4>
<p><strong><code>IFNULL()</code></strong>: 해당 컬럼의 값이 널일 때, 다른 값을 출력하도록함</p>
</blockquote>
<pre><code class="language-sql">SELECT IFNULL(컬럼명,&quot;NULL일 때 대체값)
FROM ...</code></pre>
<p><br><strong><code>COALESCE()</code></strong>: 지정한 표현식들 중 널값이 아닌 첫 번째 값을 반환</p>
<pre><code class="language-sql">SELECT COALESCE(컬럼명1, 컬럼명2,....)
SELECT COALESCE(컬럼명1, &quot;컬러명1이 널값일 때 대체값&quot;)</code></pre>
<p><code>COALESCE</code> 써서도 아래처럼 풀었는데 똑같은 형식으로 사용하면 됐다.
<img src="https://velog.velcdn.com/images/summeryoung_/post/afb25bfd-a9e4-4c1d-adee-7da89b52077e/image.png" alt=""></p>
<hr>
<blockquote>
<h3 id="q5">Q5.</h3>
</blockquote>
<h4 id="datetime에서-date로-형-변환">DATETIME에서 DATE로 형 변환</h4>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/430c03e4-7b0a-4d42-9e4e-f63b9d44bec1/image.png" alt=""></p>
<p><code>DATETIME</code>의 출력 형식을 바꾸는 것 중에 <code>DATE_FORMAT</code>을 사용해서 풀 수 있는 문제였다. 다른 부분은 비슷하고, <code>SELECT</code>에서 포맷만 설정해주었다.</p>
<blockquote>
<h4 id="date_format-정리">DATE_FORMAT 정리</h4>
<p><code>DATE_FORMAT(날짜, 형식)</code>과 같은 식으로 넣어서 사용해준다. 형식을 지정하는 기호들은 아래와 같은 것들이 있다.
<code>%Y</code>: 4자리 년도, <code>%y</code>: 2자리 년도
<code>%m</code>: 숫자 월, <code>%d</code>: 일자
<code>%H-%i-%S</code>: 시(24시간)-분-초
<code>%I</code>: 시간(12시간)
<code>%T</code>: hh:mm:SS</p>
</blockquote>
<hr>
<blockquote>
<h3 id="q6">Q6.</h3>
</blockquote>
<h4 id="중성화-여부-파악하기">중성화 여부 파악하기</h4>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/61ef3b10-7a8f-4872-abd2-8958e98ae71f/image.png" alt=""></p>
<p><code>CASE WHEN</code>을 사용해서 풀이한 문제였다. <code>CASE WHEN</code>을 사용할 줄 몰라서 일단 찾아봤는데 찾았는데도 계속 오류가 나서 뭐가 문제일까 고민했다.</p>
<p>나중에 다른 방식을 사용해서 풀고 찾아보니 일전에는 계속 </p>
<pre><code class="language-sql">CASE SEX_UPON_INTAKE
    WHEN LIKE &#39;Neutered%&#39;...</code></pre>
<p>이런식으로 썼었는데 <code>LIKE</code>을 사용할 때는 <code>CASE</code> 컬럼(단순형)... 형식을 쓰면 안되고 검색형을 사용해야한다고 한다. (검색형은 <code>CASE</code> 뒤에 컬럼을 바로 쓰는게 아니라, <code>WHEN</code> 뒤에 조건식을 쓰는 형식이다.</p>
<pre><code class="language-sql">CASE &quot;여기는 비우고&quot;
    WHEN SEX_UPON_INTAKE LIKE &#39;Neutered%&#39;...</code></pre>
<p>이렇게 조건식을 쓰기.</p>
<blockquote>
<h4 id="case-when">CASE WHEN</h4>
<p>SELECT (<strong>CASE</strong> (컬럼명/값)
    <strong>WHEN</strong> (값: 단, 단순형에서는 무조건 = 비교만 가능) <strong>THEN</strong>
    WHEN ... THEN
    <strong>END</strong>) AS (새로운 컬럼명)
<br>
SELECT (<strong>CASE</strong>
    <strong>WHEN</strong> (조건식) <strong>THEN</strong>
    ...
    <strong>END</strong>) AS (새로운 컬럼명)</p>
</blockquote>
<hr>
<blockquote>
<h3 id="q7">Q7.</h3>
</blockquote>
<h4 id="solvesql-우유와-요거트가-담긴-장바구니">solvesql: 우유와 요거트가 담긴 장바구니</h4>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/8797f11b-85f7-4e2b-bf9d-8ca9ba892ff1/image.png" alt=""></p>
<p>일단은 문제 보고 바로 생각난 풀이는 진짜 비효율적인? 그런데 제일 쉬운 그런 풀이였다. 맞긴 맞았는데 이것보다 좋은 방법이 있는 것 같아서 고민하다가 찾아봤다.</p>
<p><code>GROUP BY</code>하고 <code>HAVING</code>을 사용하면 더 효율적으로 풀 수 있을 것 같긴했는데 어떤 식으로 사용해야할 지 잘 감이 안왔는데, <code>WHERE</code>절에서 먼지 일단 우유와 요거트가 든 행만 남긴다 -&gt; 그리고 <code>GROUP BY</code>를 통해 ID 별로 묶고, <code>HAVING</code> 절에서 개수가 2 이상인 것(우유랑 요거트 행만 남겼으니까 2이상이면 무조건 이 둘이다 단, <code>COUNT(DISTINCT)</code>를 사용해서 우유가 2개 이런 경우는 제외시켜준다.)</p>
<p>처음에 <code>GROUP BY</code>하고 <code>HAVING</code>을 써야할 것 같았는데, 이러면 다른 상품의 개수도 포함된다고 생각하고 그냥 직관적으로 바로 생각나는걸로 풀었는데 진짜 <code>WHERE</code>에서 조건을 사용해서 한 번 걸러주면 되는데 이 생각을 못했다. 그리고 <code>DISTINCT</code> 사용해서 또 걸러내는 것도 생각을 못했었다. </p>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/8de52732-5831-44db-8299-625e008b7b0c/image.png" alt=""></p>
<p>위의 방법대로 푼 방식은 위와 같다. 훨씬 효율적이고 쿼리 길이도 짧다.</p>
<hr>
<blockquote>
<p><strong>후기?</strong>
문제 풀다가 중간쯤에 깨달은 건데? 이거 소문자로 써도 괜찮았는데 왜 계속 대문자로 썼지 싶다(컬럼명). 아무생각없이 문제가 대문자라 계속 대문자로 썼는데 소문자로 써도 돌아갔다 생각해보니.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[데이터베이스 4주차 내용 정리]]></title>
            <link>https://velog.io/@summeryoung_/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-4%EC%A3%BC%EC%B0%A8-%EB%82%B4%EC%9A%A9-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@summeryoung_/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-4%EC%A3%BC%EC%B0%A8-%EB%82%B4%EC%9A%A9-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Fri, 27 Mar 2026 07:07:12 GMT</pubDate>
            <description><![CDATA[<h1 id="데이터-조작어-dml">데이터 조작어 (DML)</h1>
<h2 id="부속질의">부속질의</h2>
<p>: SELECT문 안에 <strong>또 다른 SELECT문을 포함</strong>하는 질의</p>
<h3 id="부속-질의문서브-쿼리">부속 질의문(서브 쿼리)</h3>
<p>: 다른 SELECT문 안에 들어 있는 SELECT문</p>
<ul>
<li><p>괄호로 묶어서 작성하며, <strong>ORDER BY</strong>를 사용할 수 없음</p>
</li>
<li><p>하나의 행을 결과로 반환하기도 하고, 하나 이상의 행을 결과로 반환하기도함.</p>
</li>
<li><p>부속 질의문을 먼저 수행하고 그 결과를 바탕으로 상위 질의 문 수행</p>
</li>
<li><p>부속 질의문과 상위 질의문을 <strong>연결하는 연산자</strong> 필요</p>
<ul>
<li>단일 행의 경우 <strong>비교 연산자</strong> 사용이 가능하지만, 다중 행의 경우는 불가능</li>
</ul>
</li>
</ul>
<br>

<h3 id="연결-연산자">연결 연산자</h3>
<ol>
<li>비교 연산자: <code>=</code>, <code>&gt;</code>, <code>&lt;</code>, <code>&gt;=</code>, <code>&lt;=</code> (단일 행 부속 질의문의 경우 사용 가능)</li>
<li><code>IN</code>, <code>NOT IN</code>: 부속 질의문의 <strong>결과값</strong> 중 <strong>일치하는 것의 존재 여부</strong>에 따라 검색조건 참/거짓 반환.</li>
<li><code>EXISTS</code>, <code>NOT EXISTS</code>: 부속 질의문의 <strong>결과 값이 하나라도 존재하는지의 여부</strong>에 따라 검색조건 참/거짓 반환</li>
<li><code>ALL</code>: 부속 질의문읠 결과값 모두와 비교해 결과가 참일 때</li>
<li><code>ANY</code> 또는 <code>SOME</code>: 부속 질의문의 결과값 중 하나라도 비교해 결과가 참일 때.
 (<code>ALL</code>, <code>ANY</code>, <code>SOME</code>은 비교연산자와 함께 사용)</li>
</ol>
<br>

<p>예) 대한미디어에서 출판한 도서를 구매한 고객의 이름 나타내기</p>
<pre><code class="language-sql">SELECT name
FROM customers
WHERE custid IN ( 
        SELECT custid
        FROM orders
        WHERE bookid IN ( 
              SELECT bookid
              FROM books
              WHERE publisher = &#39;대한미디어&#39;)));</code></pre>
<p>서브쿼리를 사용해도 좋지만 <code>JOIN</code>을 사용하여서도 같은 결과를 낼 수 있다.
아래는 <code>(INNER) JOIN</code>을 사용해 같은 </p>
<pre><code class="language-sql">SELECT C.name
FROM customers AS C
    JOIN orders AS O ON C.custid = O.custid) 
    JOIN books AS B ON O.bookid = B.bookid
WHERE B.publisher = &#39;대한미디어&#39;;
</code></pre>
<h3 id="상관연결-부속질의">상관(연결) 부속질의</h3>
<ul>
<li>부속 질의 간에는 <strong>상하 관계</strong>가 있으며, 상위 부속질의와 하위 부속질의가 독립적이지 않고 <strong>서로 관련을 맺고 있음</strong></li>
<li>상위 쿼리와 하위 쿼리는 <strong>서로 의존적</strong>이며 상위 쿼리의 특정 행 값이 하위 쿼리 조건에 사용됨</li>
<li>일반적인 부속질의와 달리 <strong>행 단위로 반복 실행</strong>됨</li>
</ul>
<p>예) 출판사별로 출판사의 평균 도서 가격보다 비싼 도서 구하기</p>
<pre><code class="language-sql">SELECT bookname
FROM books B1
WHERE B1.price &gt; (
        SELECT avg(price)
        FROM books B2
        WHERE B1.publisher = B2.publisher));</code></pre>
<p>위의 코드를 살펴보면 같은 테이블인 books를 <code>B1</code>, <code>B2</code>로 별칭을 각각 설정해주고 있는데 이는 <strong>상위 쿼리와 하위 쿼리가 독립적이지 않아서</strong>이다. </p>
<p>상위 쿼리에서 행을 하나씩 가져와 하위 쿼리에서 반복하며 상위 쿼리 행의 책의 출판사의 책들의 값의 평균을 구해주면 그것과 상위 쿼리 행의 값을 비교하는 식으로 돌아가기에, 하위 쿼리에서 상위 쿼리의 내용을 가져와야하는데 이때 <strong>구분</strong>을 위해서 필요하다. </p>
<p><span style="color:gray; font-size:80%"> 반복문이 아닌데 반복문을 실행하는 것처럼 상위 쿼리의 한 행마다 이제 하위 쿼리가 실행된다는 부분이 처음에는 잘 받아들여지지 않았는데 직접 쳐보니 조금 더 와닿는다.</span></p>
<p>위의 코드도 <code>JOIN</code>을 쓰는 식으로 수정해보면 아래와같다.</p>
<pre><code class="language-sql">SELECT bookname
FROM books B1
    JOIN (SELECT publisher, AVG(price) AS price
          FROM books
          GROUP BY publisher) B2
    ON B1.publisher = B2.publisher
WHERE B1.price &gt; B2.price;</code></pre>
<h3 id="집합-연산-union-minus-intersect">집합 연산: UNION, MINUS, INTERSECT</h3>
<ul>
<li>SQL문의 결과는 테이블로 나타남</li>
<li>테이블 간의 집합 연산을 사용해 합집합, 차집합, 교집합을 구할 수 있음</li>
</ul>
<h4 id="union">UNION</h4>
<ul>
<li><code>UNION ALL</code>은 <strong>중복을 포함</strong>하여 모든 결과를 구함</li>
</ul>
<p>예) Customer 테이블에 대한민국에 거주하는 고객의 이름 집합과 도서를 주문한 고객의 이름 집합의 합집합 구하기</p>
<pre><code class="language-sql">SELECT name
FROM customers
WHERE address LIKE &#39;%대한민국%&#39;
UNION
SELECT customers.name
FROM customers
    JOIN orders 
    ON customers.custid = orders.custid;</code></pre>
<h4 id="minus-interset">MINUS, INTERSET</h4>
<ul>
<li>MySQL에는 MINUS와 INTERSECT 연산자가 없어 <strong>NOT IN</strong>과 <strong>IN</strong> 연산자를 사용함</li>
</ul>
<p>예) 대한민국에 거주하는 고객의 이름에서 도서를 주문한 고객의 이름을 제외하고 나타내기</p>
<pre><code class="language-sql">SELECT C.name
FROM customers AS C
WHERE C.address LIKE &#39;%대한민국%&#39; AND    
      C.custid NOT IN (
              SELECT custid
            FROM orders);</code></pre>
<h4 id="exist-not-exist">EXIST, NOT EXIST</h4>
<ul>
<li><strong>상관 부속질의문</strong>의 형식으로 부속질의의 결과가 존재하는지 여부를 확인하는 연산자</li>
<li>부속질의문이 한 행이라도 반환하면 참, NOT EXISTS의 경우는 반환행이 하나도 존재하지 않으면 참</li>
</ul>
<p>예) 주문이 있는 고객의 이름과 주소를 나타내기</p>
<pre><code class="language-sql">SELECT name, address
FROM customers C
WHERE EXIST (
        SELECT custid
        FROM orders O
        ON C.custid = O.custid));</code></pre>
<hr>
<h1 id="데이터-정의어-ddl">데이터 정의어 (DDL)</h1>
<h2 id="create">CREATE</h2>
<h3 id="create-table">CREATE TABLE</h3>
<p>테이블을 생성하며, <strong>속성과 속성에 관한 제약</strong>을 정의한다.
<strong>기본키 및 외래키를 정의</strong>하는 명령어이다.</p>
<blockquote>
<p><strong>CREATE TABLE</strong> 테이블 이름 (
    &lt;<em>속성명</em>&gt; &lt;<em>데이터 타입</em>&gt; &lt;<em>제약...</em>&gt;
    <strong>PRIMARY KEY</strong>(<em>속성명</em> )
    <strong>FOREIGN KEY</strong> (<em>속성명</em> )
);</p>
</blockquote>
<br>

<h3 id="외래키-지정">외래키 지정</h3>
<ul>
<li><strong>참조 무결성 제약조건</strong> 유지를 위해 참조되는 테이블에서 튜플 삭제 시 처리방법을 지정하는 옵션<blockquote>
<p><strong>ON DELETE CASCADE</strong>: 관련 튜플을 함께 삭제</p>
</blockquote>
</li>
<li><em>ON DELETE SET NULL*</em>: 관련 튜플의 외래키 값을 NULL로 변경</li>
<li><em>ON DELETE RESTRICT*</em>: 부모행이 참조되고 있으면 삭제/수정 불가 (기본값)</li>
<li><em>ON DELETE NO ACTION*</em>: SQL 표준 키워드로 RESTRICT와 동일.</li>
</ul>
<p>예)</p>
<pre><code class="language-sql">CREATE TABLE post (
    post_id INT NOT NULL AUTO_INCREMENT,
    board_id INT NOT NULL,
    content TEXT,
    createdAt DATETIME NOT NULL,
    PRIMARY KEY(post_id),
    FOREIGN KEY(board_id) REFERENCES board ON DELETE CASCADE
);</code></pre>
<h2 id="alter">ALTER</h2>
<p>: 생성된 테이블의 <strong>속성 변경 및 속성에 관한 제약 변경</strong> 또는 <strong>기본키 및 외래키</strong>를 변경</p>
<blockquote>
<p><strong>ALTER TABLE</strong> &lt;<em>테이블명</em>&gt;
<strong>ADD</strong> &lt;<em>속성이름</em>&gt; &lt;<em>데이터타입</em>&gt;
<strong>DROP COLUMN</strong> &lt;<em>속성이름</em>&gt;
<strong>ALTER COLUMN</strong> &lt;<em>속성이름</em>&gt; &lt;<em>데이터타입</em>&gt;
<strong>ADD PRIMARY KEY</strong>(<em>속성이름</em> )</p>
</blockquote>
<pre><code class="language-sql">ALTER TABLE post
    ADD updatedAt DATETIME
       DROP COLUMN createdAt;</code></pre>
<h2 id="drop">DROP</h2>
<p>: <strong>테이블을 삭제</strong>하는 명령. 테이블의 구조와 데이터를 모두 삭제함.</p>
<p>데이터만 삭제하려고 할 때는 <strong>DELETE</strong>를 사용하며, 삭제할 테이블을 <strong>참조하는 테이블</strong>이 있을 때에는 삭제가 수행되지 않음.</p>
<p>위의 경우 </p>
<ul>
<li>해당 테이블을 참조하고 있는 테이블부터 삭제</li>
<li>관련된 외래키 제약조건 먼저 삭제</li>
</ul>
<p>를 통해 테이블을 삭제할 수 있다.</p>
<blockquote>
<p><strong>DROP TABLE</strong> &lt;<em>테이블 이름</em>&gt;</p>
</blockquote>
<hr>
<h1 id="데이터-조작어dml">데이터 조작어(DML)</h1>
<h2 id="insert">INSERT</h2>
<p>: 테이블에 새로운 튜플을 삽입하는 명령어</p>
<blockquote>
<p><strong>INSERT INTO</strong> &lt;<em>테이블 이름</em>&gt; (<em>속성 리스트</em> )
    <strong>VALUES</strong> (<em>값 리스트</em> )<br>
    <span style=font-size:80%>속성 리스트를 작성한 순서대로(<strong>일대일대응</strong>) 값 리스트를 작성해줘야함
    속성리스트는 생략 가능하지만, 테이블 정의 시 지정한 속성의 순서대로 값이 삽입되기에 주의해야함. </span></p>
</blockquote>
<p><code>SELECT</code> 문을 사용해서도 작성할 수 있는데 이 경우는 <strong>다른 테이블의 값을 긁어와 넣는 식</strong>.</p>
<p>예)</p>
<pre><code class="language-sql">INSERT INTO post (post_id, board_id, content, createdAt)
VALUES (1, 4, &#39;글입니다.&#39;, &#39;2026-03-27 16:00:10&#39;);

---생략한 속성들에는 NULL 또는 DEFAULT로 설정한 값 입력.
INSERT INTO post (post_id, board_id)
VALUES (5, 3);</code></pre>
<h2 id="update">UPDATE</h2>
<p>: 특정 속성값을 수정하는 명령으로 다른 테이블의 속성값을 이용할 수 있음.</p>
<blockquote>
<p><strong>UPDATE</strong> &lt;<em>테이블 이름</em>&gt;
<strong>SET</strong> <em>속성이름</em> = <em>값</em>
<strong>WHERE</strong> <em>조건</em> <br>
<span style=font-size:80%> WHERE 절은 생략 가능하지만, 이 경우 테이블에 존재하는 모든 튜플을 수정하게된다. </span></p>
</blockquote>
<p>예)</p>
<pre><code class="language-sql">UPDATE post
SET content = &quot;ringing siren&quot;
WHERE post_id = 5;</code></pre>
<h2 id="delete">DELETE</h2>
<p>: 테이블에 있는 기존 튜플을 삭제하는 명령</p>
<blockquote>
<p><strong>DELETE FROM</strong> &lt;<em>테이블 이름</em>&gt;
<strong>WHERE</strong> <em>조건</em>
<br><span style=font-size:80%> UPDATE문과 동일하게 WHERE 절은 생략 가능하지만, 이 경우에는 테이블에 존재하는 모든 튜플을 삭제하게 된다. </span></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[인프런 데이터베이스] 3주차 스터디]]></title>
            <link>https://velog.io/@summeryoung_/%EC%9D%B8%ED%94%84%EB%9F%B0-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-3%EC%A3%BC%EC%B0%A8-%EC%8A%A4%ED%84%B0%EB%94%94</link>
            <guid>https://velog.io/@summeryoung_/%EC%9D%B8%ED%94%84%EB%9F%B0-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-3%EC%A3%BC%EC%B0%A8-%EC%8A%A4%ED%84%B0%EB%94%94</guid>
            <pubDate>Mon, 23 Mar 2026 13:22:24 GMT</pubDate>
            <description><![CDATA[<blockquote>
<h3 id="인프런-데이터베이스-강의-섹션6-섹션7-기록">인프런 데이터베이스 강의 (섹션6-섹션7) 기록</h3>
</blockquote>
<h2 id="관계형-데이터베이스">관계형 데이터베이스</h2>
<p>두 개의 테이블에 분산해서 저장하고 읽어와 출력에는 이를 합쳐서 보여줄 수 있음. 
중복을 제거할 수 있다는 것이 장점.</p>
<p><code>RENAME TABLE</code>: 테이블 이름 수정 가능</p>
<p>author라는 테이블을 기존의 topic 테이블에서 따로 분리해 만들어 중복하여 저장하는 author의 프로필이나 이름을 생략. 대신 <code>author_id</code>를 <code>topic</code> 테이블에 <code>author</code> 대신 넣어줌.</p>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/67370292-0c6d-40b0-a8b4-24ffa6e0e2df/image.png" width=70%>)</p>
<p>아래와 같은 형태로 출력됨.</p>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/1a4c2243-a705-403c-b231-36266b1777ee/image.png" width=70%>)</p>
<h3 id="조인join">조인(JOIN)</h3>
<p>두 개의 분리된 테이블을 하나의 형태로 합쳐서 출력할 수 있음. 
위의 테이블에서 연결 고리는 <code>author_id</code>라고 할 수 있음.</p>
<pre><code class="language-sql">SELECT * FROM topic LEFT JOIN author ON topic.author_id = author.id;</code></pre>
<p>topic의 author_id와 author 테이블의 id값이 같은 것을 기준으로 두 테이블을 합쳐서 출력함.</p>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/6ce289e8-03e2-4175-8b2d-188dab61b8ad/image.png" alt=""></p>
<p>author의 아이디나, author 아이디를 생략하려면 <code>SELECT</code> 문의 뒤에 보여주고 싶은 속성명만 적어준다.</p>
<pre><code class="language-sql">SELECT topic.id, title, description, created, name, profile  FROM topic LEFT JOIN author ON topic.author_id = author
.id;</code></pre>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/7c3dcc37-36f0-4fbe-890b-4bd34f0755fb/image.png" alt="">
하지만 위의 예시처럼 <code>id</code>를 선택할 경우 해당 <code>id</code>가 topic 테이블의 속성을 말하는지, author 테이블의 속성을 말하는지 모르기 때문에 <strong>&lt;테이블명&gt;.&lt;속성명&gt;</strong>의 형식으로 확실하게 명시하여준다.</p>
<p>또, 출력되는 표의 속성명을 다르게 수정하고 싶을 때는 <code>AS</code>를 사용해 별칭을 만들어줄 수 있다.</p>
<pre><code class="language-sql">SELECT topic.id AS topic_id, title, description, created, name, profile  FROM topic LEFT JOIN author ON topic.author
_id = author.id;</code></pre>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/7b0c87ac-d58f-4547-92f4-04dfaa31ae72/image.png" alt=""></p>
<p>이렇게 테이블을 분리하게되면, <strong>수정 및 유지 보수에서 이점을</strong> 얻을 수 있음.</p>
<h2 id="인터넷과-데이터베이스의-관계">인터넷과 데이터베이스의 관계</h2>
<h3 id="데이터베이스-서버">데이터베이스 서버</h3>
<p>MySQL을 설치하면 데이터베이스 클라이언트와 데이터베이스 서버 2가지를 설치하게됨. 데이터베이스 서버에 실제로 데이터가 저장되며 데이터베이스 클라이언트 쪽에서 이 데이터베이스 서버에 접속할 수 있음.</p>
<h3 id="데이터베이스-클라이언트">데이터베이스 클라이언트</h3>
<p>데이터베이스 서버는 직접 다룰 수 없고 <strong>반드시 데이터베이스 클라이언트</strong>를 사용해야함. 터미널에서 지금껏 사용하던 것은 CLI를 사용해 접근하는 데이터베이스 클라이언트 중 하나인 <code>mysql-monitor</code> 였음.</p>
<p><code>mysql-monitor</code>의 장점(CLI):</p>
<ul>
<li>명령어를 사용해서 제어</li>
<li>mysql을 설치하면 같이 설치되며 어디에서나 실행할 수 있음.</li>
</ul>
<p>MySQL Workbench는 GUI 형식의 데이터베이스 클라이언트.</p>
<pre><code class="language-bash">./mysql -uroot -p -h localhost</code></pre>
<p>-<code>-h</code>: 접속하려는 데이터베이스 서버의 주소를 적어주는 부분</p>
<ul>
<li><code>localhost</code> = <code>127.0.0.1</code>: 스스로의 컴퓨터를 가리키는 뜻.</li>
</ul>
<h3 id="인터넷">인터넷</h3>
<p>동작을 위해서는 최소 2대의 컴퓨터가 필요함. 인터넷은 컴퓨터들이 모여 이루는 사회라고 볼 수 있음.</p>
<p>한 대의 컴퓨터는 다른 컴퓨터에게 정보를 <strong>요청</strong>하고, 다른 컴퓨터는 정보를 <strong>응답</strong>함. 요청하는 쪽을 <strong>클라이언트</strong>, 응답하는 쪽은 <strong>서버</strong>라고 함.</p>
<p>웹에 비유를 하면 웹 브라우저(웹 클라이언트)가 웹 서버 측에 요청을 보내게됨.</p>
<h2 id="mysql-workbench">MySQL Workbench</h2>
<p>SQL문을 생성해서 서버로 전달하는 것이 모든 서버 클라이언트들의 동작.</p>
<p>아래처럼 GUI를 사용해 스키마를 새로 만들 수 있는데 이  역시도 결국 SQL문이 만들어지고 실행되는 형식.</p>
<img src="https://velog.velcdn.com/images/summeryoung_/post/6b2fadb4-2886-49db-a887-d9b550dfc5ac/image.png" width=70%>

<img src="https://velog.velcdn.com/images/summeryoung_/post/9413ee77-be2d-495d-9042-363c6efedfe8/image.png" width=70%>


<p>아래 처럼 테이블 생성 시 속성들을 GUI를 사용해 설정할 수 있게된다.</p>
<img src="https://velog.velcdn.com/images/summeryoung_/post/6355feb5-f782-44c4-b7a7-cfd5083bf34a/image.png" width=70%>

<img src="https://velog.velcdn.com/images/summeryoung_/post/31b429c9-de80-48a6-8e91-e7fb0b8d77ab/image.png" width=70%>

<h2 id="추가">추가</h2>
<p>정보가 많아지며 생기는 문제점들이 존재함. 데이터가 많아질수록 무언가를 찾을 때 시간이 오래 걸리는 문제들이 존재함.</p>
<h3 id="index">index</h3>
<p>이를 해결하기 위해 사용자들이 자주 검색하는 <strong>컬럼(속성)에 색인(index)</strong>을 걸어줌. 이렇게 색인을 걸 경우, 데이터가 들어올 때 데이터베이스가 해당 컬럼의 데이터를 잘 정렬해 정리해둠. 후에 요청 시 빠르게 응답할 수 있음.</p>
<h3 id="modeling">modeling</h3>
<p>테이블을 효율적으로 잘 설계해야함. 정규화, 비정규화 등. 데이터가 많아지면서 이를 관리/설계해야할 필요성을 느낄 시 공부.</p>
<h3 id="backup">backup</h3>
<p>데이터가 날라가면 문제가 발생하기에 잘 백업해두어야함. 하드 디스크의 고장 등의 문제가 있을 수 있기에 정보를 안전하게 잘 보장해야함. <strong>데이터를 복제해서 보관</strong>하여 백업하는 것이 중요.</p>
<p>mysqldump나 binary log를 찾아보기</p>
<h3 id="cloud">cloud</h3>
<p>컴퓨터를 데이터베이스 서버로 쓰지 않고, 거대한 회사가 운영하는 인프라 위의 컴퓨터를 임대해서 사용하는 것이 <strong>클라우드 컴퓨팅</strong>. 이는 <strong>원격 제어</strong>를 통해 먼 곳에 있는 컴퓨터를 제어. 백업 등을 알아서 관리해주기에 편리함.</p>
<p>예) AWS, Google Cloud, AZURE 등</p>
<hr>
<h2 id="3주차-추가-과제">3주차 추가 과제</h2>
<h3 id="테이블-생성">테이블 생성</h3>
<p>passenger와 plane 두 개의 테이블을 생성.</p>
<pre><code class="language-sql">CREATE TABLE passenger (
    passenger_id INT(11) NOT NULL AUTO_INCREMENT,
    last_name VARCHAR(50) NOT NULL,
    first_name VARCHAR(50) NOT NULL,
    nationality VARCHAR(50) NOT NULL,
    plane INT(11) NULL,
    PRIMARY KEY(passenger_id)
);

CREATE TABLE plane(
    plane_id INT PRIMARY KEY AUTO_INCREMENT,
    departure VARCHAR(45) NOT NULL,
    arrival VARCHAR(45) NOT NULL,
    departure_time DATETIME NOT NULL,
    gate INT NOT NULL,
    meal BOOLEAN
);</code></pre>
<p><img src="
https://velog.velcdn.com/images/summeryoung_/post/e7101c0d-9442-42df-84a8-168338008e81/image.png" width=70%></p>
<img src="https://velog.velcdn.com/images/summeryoung_/post/8f78f7e6-973b-41ae-b786-e8cc31783605/image.png" width=70%>
<br>

<h3 id="데이터-삽입">데이터 삽입</h3>
<pre><code class="language-sql">INSERT INTO passenger (last_name, first_name, nationality, plane)
VALUES (&quot;김&quot;, &quot;퍼비&quot;, &quot;한국&quot;, 1);
INSERT INTO passenger (last_name, first_name, nationality, plane)
VALUES (&quot;Smith&quot;, &quot;Oliver&quot;, &quot;호주&quot;, 4);
...

INSERT INTO plane (departure, arrival, departure_time, gate, meal)
VALUES (&quot;서울&quot;, &quot;로마&quot;, &quot;2023-03-30 12:10:00&quot;, 57, true);
INSERT INTO plane (departure, arrival, departure_time, gate, meal)
VALUES (&quot;서울&quot;, &quot;오사카&quot;, &quot;2023-04-14 09:35:00&quot;, 9, false);
...</code></pre>
<img src="https://velog.velcdn.com/images/summeryoung_/post/2913eab4-c144-4120-964d-5da92862319e/image.png" width=70%>

<p><img src="https://velog.velcdn.com/images/summeryoung_/post/81cff682-28fb-4980-905d-1588353befd5/image.png" alt=""></p>
<h3 id="조인join-활용-쿼리-조회">조인(JOIN) 활용 쿼리 조회</h3>
<pre><code class="language-sql">SELECT passenger_id, last_name, first_name, nationality, departure, arrival,
departure_time, gate, meal
FROM passenger 
    LEFT JOIN plane
    ON passenger.plane = plane.plane_id;</code></pre>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/9b652a83-5f3d-4036-9f1d-08dc3211b26c/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[자바 ORM 표준 JPA 프로그래밍] 2주차 스터디]]></title>
            <link>https://velog.io/@summeryoung_/%EC%9E%90%EB%B0%94-ORM-%ED%91%9C%EC%A4%80-JPA-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-2%EC%A3%BC%EC%B0%A8-%EC%8A%A4%ED%84%B0%EB%94%94</link>
            <guid>https://velog.io/@summeryoung_/%EC%9E%90%EB%B0%94-ORM-%ED%91%9C%EC%A4%80-JPA-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-2%EC%A3%BC%EC%B0%A8-%EC%8A%A4%ED%84%B0%EB%94%94</guid>
            <pubDate>Sun, 22 Mar 2026 06:47:58 GMT</pubDate>
            <description><![CDATA[<h1 id="3장-영속성-관리">3장. 영속성 관리</h1>
<blockquote>
<p>JPA가 제공하는 기능</p>
</blockquote>
<ol>
<li><strong>엔티티와 테이블을 매핑</strong>하는 설계 부분</li>
<li><strong>매핑한 엔티티를 실제 사용</strong>하는 부분</li>
</ol>
<p><strong>엔티티 매니저(Entity Manager)</strong>: 엔티티를 저장/수정/삭제/조회하는 등 엔티티와 관련된 모든 일을 처리.</p>
<h2 id="31-엔티티-매니저-팩토리와-엔티티-매니저">3.1 엔티티 매니저 팩토리와 엔티티 매니저</h2>
<h3 id="엔티티-매니저-팩토리-entitymanagerfactory">엔티티 매니저 팩토리 (EntityManagerFactory)</h3>
<p>데이터베이스를 하나만 사용하는 경우 애플리케이션은 보통 <code>EntityManagerFactory</code>를 <strong>하나만</strong> 생성.</p>
<pre><code class="language-java">EntityManagerFactory emf = 
    Persistence.createEntityManagerFactory(&quot;jpabook&quot;);</code></pre>
<p><code>Persistence.createEntityManagerFactory</code> 를 호출할 경우 META-INF/<strong>persistence.xml</strong>의 정보를 바탕으로 엔티티 매니저 팩토리를 생성하게된다.</p>
<p><code>&quot;jpabook&quot;</code>의 부분에는 persistence.xml에 정의되어 있는 persistence-unit의 이름을 적어주면 된다.</p>
<h3 id="엔티티-매니저-entitymanager">엔티티 매니저 (EntityManager)</h3>
<p>엔티티 매니저 팩토리 생성 후에는 필요할 때마다 엔티티 매니저 팩토리에서 <strong>엔티티 매니저</strong>를 생성하면된다.</p>
<pre><code class="language-java">//엔티티 매니저 생성
EntityManager em = emf.createEntityManager();</code></pre>
<p>엔티티 매니저 팩토리는 만드는데에 비용이 큰 반면, 엔티티 매니저를 만드는 비용이 적다. 또, <strong>엔티티 매니저 팩토리는 여러 스레드가 동시에 접근해도 안전</strong>하지만, <strong>엔티티 매니저는 여러 스레드가 동시에 접근하면 동시성 문제가 발생하기에</strong> 스레드 간에 절대 공유하면 안된다.</p>
<p>엔티티 매니저는 데이터베이스의 연결이 꼭 필요한 시점까지는 커넥션을 얻지 않고, <strong>트랜잭션을 시작할 때 커넥션을 획득</strong>한다.</p>
<p>대부분의 JPA 구현체들은 엔티티 매니저 팩토리를 생성할 때 커넥션 풀을 만든다. 데이터베이스 접속 관련 정보는 persistence.xml에 존재한다. </p>
<hr>
<h2 id="32-영속성-컨텍스트">3.2 영속성 컨텍스트</h2>
<p><strong>영속성 컨텍스트(persistence context</strong>)란 엔티티를 영구저장하는 환경인데, 엔티티 매니저로 엔티티를 저장/조회하는 경우 <strong>엔티티는 영속성 컨텍스트에 엔티티를 보관하고 관리</strong>한다.</p>
<p><code>persist()</code> 메소드를 사용해 엔티티를 저장하는 것도 엔티티 매니저를 이용해 엔티티를 <strong>영속성 컨텍스트에 저장</strong>하는 것이다.</p>
<p>영속성 컨텍스트는 논리적인 개념으로 엔티티 매니저를 생성할 때 하나 만들어지며, 엔티티 매니저를 통해 접근하거나 관리할 수 있다.</p>
<hr>
<h2 id="33-엔티티의-생명주기">3.3 엔티티의 생명주기</h2>
<blockquote>
<p><strong>엔티티의 4가지 상태</strong></p>
</blockquote>
<ul>
<li>비영속: 영속성 컨텍스트와 전혀 관계가 없는 상태</li>
<li>영속: 영속성 컨텍스트에 저장된 상태</li>
<li>준영속: 영속성 컨텍스트에 저장되었다가 분리된 상태</li>
<li>삭제: 삭제된 상태</li>
</ul>
<h3 id="비영속">비영속</h3>
<p>순수한 객체상태로 아직 저장하지 않아 영속성 컨텍스트 및 데이터베이스와 모두 관련 이 없는 상태를 말함. 주로 객체 생성 직후 상태.</p>
<pre><code class="language-sql">Member m = new Member();
m.setId(&quot;mem1&quot;);
m.setUsername(&quot;회원1&quot;);
//아직 비영속 상태</code></pre>
<h3 id="영속">영속</h3>
<p>엔티티 매니저를 통해 <strong>엔티티를 영속성 컨텍스트에 저장</strong>한 상태로 <strong>영속성 컨텍스트가 관리</strong>하는 엔티티를 말함.</p>
<p><code>em.find()</code>나 JPQL을 사용해 조회하는 엔티티 모두 영속성 컨텍스트가 관리하는 영속 상태.</p>
<pre><code class="language-java">em.persist(m);</code></pre>
<h3 id="준영속">준영속</h3>
<p>영속성 컨텍스트가 관리하던 영속 상태의 엔티티를 영속성 컨텍스트가 관리하지 않게된 상태를 말한다.</p>
<ul>
<li><code>em.detach()</code>: 엔티티를 준영속 상태로 전화</li>
<li><code>em.close()</code>: 영속성 컨텍스트 닫기. 엔티티들을 준영속 상태로 바꿈.</li>
<li><code>em.clear()</code>: 영속성 컨텍스트 초기화. 엔티티들을 준영속 상태로 바꿈.</li>
</ul>
<h3 id="삭제">삭제</h3>
<p>엔티티를 영속성 컨텍스트는 물론 데이터베이스에서도 삭제</p>
<pre><code class="language-java">em.remove(m);</code></pre>
<hr>
<h2 id="34-영속성-컨텍스트의-특징">3.4 영속성 컨텍스트의 특징</h2>
<h3 id="특징">특징</h3>
<ul>
<li><p>식별자와 값: 
영속성 컨텍스트는 엔티티를 식별자 값(<code>@Id</code>로 테이블의 기본키와 매핑한 값)으로 구분하기에 <strong>식별자값이 반드시 필요</strong>.</p>
<br></li>
<li><p>데이터베이스에의 저장: 
JPA는 보통 <strong>트랜잭션을 커밋하는 순간</strong> 영속성 컨텍스트에 새로 저장된 엔티티를 데이터베이스에 반영. 이를 플러시(flush)라 부름.</p>
<br></li>
<li><p>영속성 컨텍스트에서 엔티티 관리 시의 장점</p>
<ul>
<li>1차 캐시</li>
<li>동일성 보장</li>
<li>트랜잭션을 지원하는 쓰기 지연</li>
<li>변경 감지</li>
<li>지연 로딩</li>
</ul>
</li>
</ul>
<h3 id="엔티티-조회">엔티티 조회</h3>
<blockquote>
<p><strong>1차 캐시</strong>: 영속성 컨텍스트가 내부에 가지고 있는 캐시를 이르는 말. 영속 상태의 엔티티는 모두 이곳에 저장됨.</p>
</blockquote>
<pre><code class="language-java">//엔티티 생성(비영속 상태)
Member m = new Memeber();
m.setId(&quot;mem1&quot;);
m.setUsername(&quot;회원1&quot;);

//엔티티를 영속
em.persist(m);
</code></pre>
<p>위 코드에서는 회원 엔티티를 영속성 컨텍스트에 저장하긴 했지만, <strong>아직 데이터베이스에 저장X</strong></p>
<p>1차 캐시의 키는 <strong>식별자 값</strong>으로 데이터베이스의 기본키(primary key)와 매핑되어 있음. 즉, <strong>영속성 컨텍스트에 데이터를 저장/조회하는 기준</strong>은 모두 데이터베이스의 <strong>기본키값</strong>.</p>
<pre><code class="language-java">//엔티티 조회
Memeber mem = em.find(Memeber.class, &quot;mem1&quot;);</code></pre>
<p>엔티티 조회를 위한 메소드인 <code>find()</code>의 첫번째 파라미터는 <strong>엔티티의 클래스 타입</strong>이고, 두번째는 <strong>엔티티의 식별자값</strong>이다. <code>em.find()</code> 호출 시 엔티티를 <strong>1차 캐시에서 먼저</strong> 찾아보고, 없으면 <strong>데이터베이스에서 조회</strong>하는 식이다.</p>
<p>데이터베이스에서 조회할 때는, 1차 캐시에서 없는 엔티티이므로 데이터베이스 조회 후 <strong>엔티티를 생성, 1차캐시에 저장</strong>한 후에 <strong>영속 상태</strong>의 엔티티를 반환하는 식으로 동작한다.</p>
<blockquote>
<p><strong>영속 엔티티의 동일성 보장</strong></p>
</blockquote>
<pre><code class="language-java">Member a = em.find(Member.class, &quot;mem1&quot;);
Member b = em.find(Member.class, &quot;mem2&quot;);</code></pre>
<p>위의 코드에서 엔티티 인스턴스는 동일성을 가짐. (=즉, 실제 인스턴스가 같다.)</p>
<p>그 이유는 <code>em.find()</code>를 호출하게되면 영속성 컨텍스트가 <strong>1차 캐시에 있는 같은 엔티티 인스턴스를 반환</strong>하기 때문이다. 이는 성능상의 이점과 엔티티의 동일성을 보장해준다는 장점이 있다.</p>
<h3 id="엔티티-등록">엔티티 등록</h3>
<blockquote>
<p><strong>트랜잭션을 지원하는 쓰기 지연</strong></p>
</blockquote>
<pre><code class="language-java">EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
//엔티티 매니저는 데이터 변경 시 트랜잭션을 시작해야함
transaction.begin();

...

transaction.commit();//트랜잭션 커밋</code></pre>
<p>엔티티 매니저는 트랜잭션을 커밋하기 직전까지 데이터베이스에 엔티티를 저장하지 않고 <strong>내부 쿼리 저장소</strong>에 INSERT SQL을 모아둔다. 이를 <strong>트랜잭션 커밋 시</strong> 데이터베이스에 한 번에 보냄으로 <strong>트랜잭션을 지원하는 쓰기 지연</strong>을 한다.</p>
<p>트랜잭션 커밋 시 엔티티 매니저는 우선 <strong>영속성 컨텍스트를 플러시(flush)</strong>함. </p>
<blockquote>
<p><strong>플러시(flush)</strong>: 영속성 컨텍스트의 변경 내용을 데이터베이스에 동기화하는 작업으로 등록/수정/삭제한 엔티티를 데이터베이스에 반영. 즉, <strong>SQL 저장소의 쿼리들을 데이터베이스에 전송</strong>해준다.</p>
</blockquote>
<h3 id="엔티티-수정">엔티티 수정</h3>
<p><strong>SQL 수정 쿼리의 문제점</strong>:
SQL을 사용하게 되면 변경사항 등이 발생했을 때마다, 또 수정 사항이 속성 중 어떤 것이냐에 따라 등 다양한 수정 쿼리를 작성해야하고, 특정 요소의 수정이 빠지거나 했을 때 오류가 발생할 수 있는 오류가 존재하는 등의 문제가 있다.</p>
<p>수정 쿼리가 많아지고 비즈니스 로직 분석을 위해 SQL을 확인해야하며 비즈니스 로직이 SQL에 의존하게 된다는 문제 존재.</p>
<blockquote>
<p>** 변경 감지 **</p>
</blockquote>
<p>JPA를 통해 엔티티를 수정할 때에는 <strong>엔티티를 조회하고 데이터만 변경</strong>하면 된다. <code>em.update()</code> 같은 메소드는 없고, 변경사항이 데이터베이스에 자동으로 반영되는데 이 기능을 <strong>변경 감지</strong>라고 한다.</p>
<blockquote>
<p><strong>스냅샷</strong>: JPA가 영속성 컨텍스트에 엔티티를 보관할 때 <strong>최초 상태를 복사해 저장해두는 것</strong>을 말함.</p>
</blockquote>
<p>이 스냅샷과 플러시(flush) 시점의 엔티티를 비교해 변경된 엔티티를 찾을 수 있고, 변경사항이 있는 엔티티의 경우 수정 쿼리를 생성해 쓰기 지연 SQL 저장소에 전송하게된다.</p>
<p>이런 변경감지는 <strong>영속 상태</strong>의 엔티티에만 해당되며 준영속 상태의 경우에는 영속성 컨텍스트의 관리를 받지 않기에 값을 변경해도 데이터베이스에 반영되지 않는다.</p>
<h3 id="엔티티-삭제">엔티티 삭제</h3>
<p>엔티티 삭제를 위해서는 엔티티 조회가 우선되어야한다. 삭제 역시 즉시 데이터베이스에서 삭제하는 것이 아니라 <strong>쓰기 지연 SQL 저장소</strong>에 저장되었다가 <strong>트랜잭션 커밋 시 플러시가 호출되며 전달</strong>되는 식이다.</p>
<p><code>em.remove()</code> 호출 순간 엔티티는 영속성 컨텍스트에서 제거되니 재사용하지 않는 것이 좋다.</p>
<hr>
<h2 id="35-플러시flush">3.5 플러시(flush)</h2>
<p><strong>플러시(flush)</strong>는 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영하는 것을 말함. </p>
<blockquote>
<p><strong>플러시할 때 일어나는 일</strong></p>
</blockquote>
<ol>
<li>변경 감지 동작: 영속성 컨텍스트의 모든 엔티티를 <strong>스냅샷과 비교</strong>해 수정된 엔티티 탐색</li>
<li>쓰기 지연 SQL 저장소의 쿼리를 데이터베이스에 전송</li>
</ol>
<p>영속성 엔티티를 플러시하는 방법은 아래 3가지가 있음.</p>
<p><strong>1- <code>em.flush()</code> 직접 호출</strong>:
엔티티 매니저의 <code>flush()</code> 메소드를 호출해 영속성 컨텍스트를 강제 플러시. 다른 프레임워크와 JPA를 함께 사용하거나 테스트 외에는 잘 사용하지 않음.</p>
<p><strong>2-트랜잭션 커밋 시 플러시 자동 호출</strong>
데이터베이스에 변경내용을 SQL에 전달하지 않고 트랜잭션만 커밋하면 데이터가 데이터베이스에 반영되지 않음. 따라서 <strong>트랜잭션 커밋 전 플러시를 호출해 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영</strong>해야함. 이때문에 JPA에서는 트랜잭션 커밋 시 자동으로 플러시를 호출함.</p>
<p><strong>3-JPQL 쿼리 실행 시 플러시 자동 호출</strong>
JPQL은 SQL로 변환되어 데이터베이스에서 엔티티를 조회하는데 이때 앞서 변경된 영속성 컨텍스트가 데이터베이스에 반영되어 있지 않으면 결과가 잘못되기에 <strong>쿼리 실행 전 영속성 컨텍스트를 플러시</strong>해줘야함. 트랜잭션 커밋과 같은 이유로 JPQL 쿼리 실행 시에도 플러시가 자동 호출됨.</p>
<ul>
<li><code>FlushModeType.AUTO</code>: 커밋/쿼리 실행 시 자동 플러시(기본값)</li>
<li><code>FlushModeType.COMMIT</code>: 커밋 시에만 플러시</li>
</ul>
<p>플러시한다해서 영속성 컨텍스트에 보관된 엔티티를 지우는 것은 아니다.</p>
<hr>
<h2 id="36-준영속">3.6 준영속</h2>
<p><strong>준영속 상태</strong>: 영속성 컨텍스트가 관리하는 영속 상태의 엔티티가 <strong>영속성 컨텍스트에서 분리</strong>된 것을 말함. 준영속 상태에서는 <strong>영속성 컨텍스트가 제공하는 기능을 사용할 수 없음</strong>.</p>
<blockquote>
<p><strong>준영속 상태로 만드는 방법</strong></p>
</blockquote>
<p><strong>1-<code>detach()</code></strong>
메소드 호출 시 1차 캐시와 쓰기 지연 SQL 저장소에 있는 해당 엔티티 관리를 위한 모든 정보가 제거된다. 쓰기 지연 SQL 저장소의 INSET SQL조차 사라지기에 데이터베이스에 저장조차 되지 않을 수 있다. </p>
<p><strong>2-<code>clear()</code></strong>
<code>em.clear()</code> 메소드는 영속성 컨텍스트를 초기화하는 메소드로 해당 영속성 컨텍스트 내 <strong>모든 엔티티를 준영속 상태</strong>로 만듦. 모든 것이 초기화 되는 것이기에 영속성 컨텍스트를 제거하고 새로 만든 것과 같다.</p>
<p><strong>3-close()</strong>
영속성 컨텍스트 종료 시 해당 영속성 컨텍스트가 관리하던 영속성 상태의 엔티티가 <strong>모두 준영속 상태</strong>가 됨.</p>
<blockquote>
<p><strong>준영속 상태의 특징</strong></p>
</blockquote>
<ul>
<li>비영속 상태에 가까움: 1차 캐시, 쓰기 지연, 변경 감지, 지연 로딩 포함 영속성 컨텍스트가 제공하는 어떤 기능도 동작하지 않음</li>
<li>식별자값을 가짐: 비영속과는 달리 준영속 상태는 한 번 영속 상태였으므로 <strong>반드시 식별자 값을 가지고 있음</strong></li>
<li>지연 로딩 불가: 실체 객체 대신 프록시 객체를 로딩해두고 해당 객체 실제 사용 시 영속성 컨텍스트를 통해 데이터를 불러오는 것이 불가능함.</li>
</ul>
<blockquote>
<p><strong>병합(merge)</strong></p>
</blockquote>
<p>준영속 상태의 엔티티를 <strong>다시 영속 상태</strong>로 변경하는 방법으로 준영속 상태의 엔티티를 받아 <strong>새로운 영속상태의 엔티티를 반환</strong>한다. 받은 준영속 엔티티를 영속 상태로 바꾸는 것이 아니라 새로 만들어 반환하는 식이다.</p>
<p><strong>과정</strong>
1: <code>merge()</code> 실행
2: 파라미터로 넘어온 <strong>준영속 엔티티</strong>의 식별자 값으로 1차 캐시에서 엔티티 조회 
    2-1: 1차캐시에 존재하지 않을 시 데이터베이스에서 조회한 후 1차 캐시에 저장
3: 조회한 영속 엔티티에 member 엔티티의 값을 채워넣음
4: 영속 엔티티를 반환</p>
<p><code>em.contains()</code>의 메소드는 영속성 컨텍스트가 파라미터로 넘어온 엔티티를 관리하고 있는지 확인할 수 있는 메소드이다.</p>
<p><strong>비영속 병합</strong>: 병합은 비영속 엔티티도 영속 상태로 만들 수 있는 메소드이다. 파라미터로 넘어온 엔티티의 식별자 값으로 영속성 컨텍스트와 데이터베이스를 조회하고 둘 모두에서 없으면 <strong>새로운 엔티티를 생성해 반환</strong>한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[인프런 데이터베이스] 2주차 스터디]]></title>
            <link>https://velog.io/@summeryoung_/%EC%9D%B8%ED%94%84%EB%9F%B0-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-2%EC%A3%BC%EC%B0%A8-%EC%8A%A4%ED%84%B0%EB%94%94</link>
            <guid>https://velog.io/@summeryoung_/%EC%9D%B8%ED%94%84%EB%9F%B0-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-2%EC%A3%BC%EC%B0%A8-%EC%8A%A4%ED%84%B0%EB%94%94</guid>
            <pubDate>Sun, 22 Mar 2026 02:27:41 GMT</pubDate>
            <description><![CDATA[<blockquote>
<h3 id="인프런-데이터베이스-강의-섹션4-섹션5-기록">인프런 데이터베이스 강의 (섹션4-섹션5) 기록</h3>
</blockquote>
<h1 id="테이블의-생성">테이블의 생성</h1>
<p>표를 만드는 부분의 SQL을 직접 짜는 경우는 적다고한다. 데이터베이스의 장점은 컬럼에 데이터 타입을 강제(지정) 할 수 있음<br>
만드려는 표: <code>id</code>, <code>title</code>, <code>description</code>, <code>created</code>, <code>author</code>, <code>profile</code></p>
<pre><code class="language-sql">CREATE TABLE topic(
    -&gt;  id INT(11) NOT NULL AUTO_INCREMENT,
    -&gt;  title VARCHAR(100) NOT NULL,
    -&gt;  description TEXT NULL,
    -&gt;  created DATETIME NOT NULL,
    -&gt;  author VARCHAR(15) NULL,
    -&gt;  profile VARCHAR(200) NULL,
    -&gt;  PRIMARY KEY(id));</code></pre>
<ul>
<li><code>id INT(11)</code>: 뒤의 11의 의미는 11자리만 <strong>출력</strong>되게 하겠다는 의미(저장X). <code>INT</code>는 해당 열의 데이터 타입이 정수형이라는 뜻.</li>
<li><code>NOT NULL</code>: 해당 열의 정보가 널(NULL)이면 안된다는 의미. 해당 열에 값이 없으면 튜플을 추가하지 않고 거절함.</li>
<li><code>AUTO_INCREMENT</code>: 값이 자동으로 증가한다는 의미. id 값은 중복되면 안되기 때문에 자동으로 1씩 증가되도록 처리.</li>
<li><code>title VARCHAR(100)</code>: 100자리의 캐릭터까지만 저장한다는 의미. 그 뒤는 자른다.</li>
<li><code>description TEXT NULL</code>: 해당 값은 널이어도 되기에 <code>NULL</code> 지정.</li>
<li><code>DATETIME</code>: 연월일과 시간을 모두 기록하는 형식의 데이터 타입.</li>
<li><code>PRIMARY KEY</code>(기본키): 각 행을 구별할 수 있는 중요한 키로 중복을 방지함.</li>
</ul>
<img src="https://velog.velcdn.com/images/summeryoung_/post/eb532597-9c5a-46c8-a549-b1a39cc78ea3/image.png" width=80%>

<ul>
<li><code>SET PASSWORD</code>: 비밀번호 설정.</li>
</ul>
<hr>
<h1 id="sql의-crud">SQL의 CRUD</h1>
<p>CRUD는 <strong>C</strong>reate, <strong>R</strong>ead, <strong>U</strong>pdate, <strong>D</strong>elete의 약자.</p>
<h2 id="create">CREATE</h2>
<h3 id="insert">INSERT</h3>
<p>데이터를 추가하는 것을 create의 부분이라 할 수 있음.</p>
<blockquote>
<p><strong>INSERT INTO</strong> &lt;테이블명&gt; (속성1, 속성2, ...)
<strong>VALUES</strong> (값1, 값2...);</p>
</blockquote>
<p><code>SHOW DATABASE</code>: 데이터베이스들을 볼 수 있음
<code>SHOW TABLES</code>: 데이터베이스에 있는 테이블들을 볼 수 있음
<img src="https://velog.velcdn.com/images/summeryoung_/post/084a8c3e-77d8-4f0a-84ef-3c2b03a44d8d/image.png" width=70%></p>
<p><code>DESC &lt;테이블명&gt;</code>: 테이블의 속성과 정보를 살펴볼 수 있음.
<img src="https://velog.velcdn.com/images/summeryoung_/post/d8911a98-9e78-4fb2-8971-f7c890190ebe/image.png" width=70%></p>
<hr>
<pre><code class="language-sql">INSERT INTO topic (title, description, created, author, profile)
VALUES (&#39;MySQL&#39;, &#39;MySQL is...&#39;, NOW(), &#39;egoing&#39;, &#39;developer&#39;);</code></pre>
<img src="https://velog.velcdn.com/images/summeryoung_/post/f2d1c8a5-3e3d-4359-b2be-717d8a937d72/image.png">
- `id`: 값을 직접 입력해주지 않아도 자동으로 증가하기 때문에 생략.
- 속성의 순서와 값을 나열하는 순서가 동일해야함
- `NOW()`: 현재 시간을 자동으로 넣어줌.



<p><img src="https://velog.velcdn.com/images/summeryoung_/post/26fc54c4-3543-4b4d-a132-37b343da3232/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/3e4bbd7a-3e37-41ac-bc4c-526748c5846d/image.png" alt=""></p>
<ul>
<li><code>DROP &lt;테이블명&gt;</code>: 기존에 있던 테이블을 삭제할 수 있음.</li>
</ul>
<hr>
<h2 id="read">READ</h2>
<h3 id="select-문">SELECT 문</h3>
<p>SELECT 구문 뒤에 <strong>프로젝션</strong>, 즉 속성명을 적어주면 해당 속성(열)만 출력됨.</p>
<img src="https://velog.velcdn.com/images/summeryoung_/post/bb473d9e-bd2d-4af4-8201-af2d89604e7c/image.png" width=70%>

<h3 id="where-문">WHERE 문</h3>
<p>특정 값에 해당하는 행만 볼 때 사용하며 위치는 <strong>FROM</strong> 다음에 온다.
<img src="https://velog.velcdn.com/images/summeryoung_/post/e3312ecc-7340-45cf-8534-90277869ff6d/image.png" alt=""></p>
<h3 id="order-by-문">ORDER BY 문</h3>
<p>특정 값을 기준으로 정렬(오름차순/내림차순)을 할 때 사용. <code>DESC</code>를 사용하면 내림차순이고 <code>ASC</code> 혹은 생략하면 오름차순으로 정렬해준다.
<img src="https://velog.velcdn.com/images/summeryoung_/post/20218622-b738-46e2-a7d8-e17b96d248e7/image.png" alt=""></p>
<h3 id="limit">LIMIT</h3>
<p>방대한 데이터를 한 번에 다 가져와버리면 문제가 생길 수 있기에 가져와서 출력하는 데이터(행)의 양을 제한해준다.</p>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/f6f82a0b-c113-4911-9a85-ab3f11c25f8c/image.png" alt=""></p>
<hr>
<h2 id="update">UPDATE</h2>
<blockquote>
<p><strong>UPDATE</strong> &lt;테이블명&gt;
<strong>SET</strong> &lt;속성명&gt; = &lt;변경할값&gt;
<strong>WHERE</strong> &lt;수정할 행&gt;</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/dfc19a4a-9514-4088-ad54-2893ab7f1436/image.png" alt=""></p>
<ul>
<li>WHERE문을 빠뜨리면 해당 속성의 모든 값이 바뀌니 절대 주의.</li>
</ul>
<hr>
<h2 id="delete">DELETE</h2>
<blockquote>
<p><strong>DELETE</strong> 
<strong>FROM</strong> &lt;테이블명&gt;
<strong>WHERE</strong> &lt;조건&gt;</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/a2a6ff9f-c552-4d44-95dc-b914497e12b8/image.png" alt=""></p>
<ul>
<li>WHERE의 조건을 생략하면 테이블의 모든 행이 날아가버리니 주의.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[데이터베이스 3주차 SQL 문제 풀이]]></title>
            <link>https://velog.io/@summeryoung_/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-3%EC%A3%BC%EC%B0%A8-SQL-%EB%AC%B8%EC%A0%9C-%ED%92%80%EC%9D%B4</link>
            <guid>https://velog.io/@summeryoung_/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-3%EC%A3%BC%EC%B0%A8-SQL-%EB%AC%B8%EC%A0%9C-%ED%92%80%EC%9D%B4</guid>
            <pubDate>Fri, 20 Mar 2026 11:55:00 GMT</pubDate>
            <description><![CDATA[<h2 id="sql-기본-문제-풀이">SQL 기본 문제 풀이</h2>
<blockquote>
<p><strong>Q1. 도서번호가 1인 도서의 이름을 검색하시오.</strong></p>
</blockquote>
<pre><code class="language-sql">SELECT bookname
FROM book
WHERE bookid = 1;</code></pre>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/748457d8-44fb-4df4-be20-a82e67af81ee/image.png" alt=""></p>
<blockquote>
<p><strong>Q2. 가격이 20,000원 이상인 도서의 이름을 검색하시오.</strong></p>
</blockquote>
<pre><code class="language-sql">SELECT bookname
FROM book
WHERE price &gt;= 20000;</code></pre>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/e27df6a4-937a-43c8-8c26-3d0a6b90d8a9/image.png" alt=""></p>
<blockquote>
<p>*<em>Q3. 고객 &#39;박지성&#39;의 총 구매액을 구하시오. *</em></p>
</blockquote>
<pre><code class="language-sql">SELECT SUM(saleprice)
FROM orders AS O
    JOIN customer AS C
    ON O.custid = C.custid
WHERE name = &#39;박지성&#39;;</code></pre>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/53529379-4de3-4712-a8a2-327461d56e57/image.png" alt=""></p>
<span style="color:gray; font-size:80%">
  처음에 <code>SELECT SUM(price)</code>를 사용했는데 <code>price</code>라는 속성이 없다고 떠서 조인 사용하면`O.price`로 어떤 테이블의 속성인지 지정해줘야하나 싶어 수정.

<p>  <br> 그 뒤에 또 위에 적은대로 테이블명 적어서 수정했는데 여전히 <code>O.price</code>가 없다는 에러가 떠서 다시 생각해보니 속성명 자체가 price가 아니라 saleprice여서 수정 (<code>O.saleprice</code>로 안적고 속성명만 적어도 되지만 <code>O.saleprice</code>로 적어도 잘 동작한다.) 
</span></p>
<blockquote>
<p><strong>Q4. 고객 &#39;박지성&#39;이 구매한 도서의 수를 구하시오.</strong></p>
</blockquote>
<pre><code class="language-sql">SELECT COUNT(*)
FROM orders AS O
    JOIN customer AS C
    ON O.custid = C.custid
WHERE name = &#39;박지성&#39;;</code></pre>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/2812cd4b-af87-4432-824a-dd8a1c627769/image.png" alt=""></p>
<blockquote>
<p>*<em>Q5. 5. 고객 &#39;박지성&#39;이 구매한 도서의 출판사 수를 구하시오. (서브쿼리를 활용할 것) *</em></p>
</blockquote>
<pre><code class="language-sql">SELECT COUNT(DISTINCT publisher) AS &#39;구매 도서 출판사 수&#39;
FROM orders AS O
    JOIN book AS B
    ON O.bookid = B.bookid
WHERE custid IN (
    SELECT custid
    FROM customer
    WHERE name = &#39;박지성&#39;
);</code></pre>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/72826870-cd39-4204-bf42-a6bd0c95f6c5/image.png" alt=""></p>
<p>조인 안쓰고? 아래처럼도 풀 수 있을 것 같다.</p>
<pre><code class="language-sql">SELECT COUNT(DISTINCT publisher)
FROM book
WHERE bookid IN (
    SELECT bookid
    FROM orders
    WHERE custid IN (
        SELECT custid
        FROM customer
        WHERE name = &#39;박지성&#39;
));</code></pre>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/6f9f039d-d5af-4854-abbc-6346a37022c6/image.png" alt=""></p>
<blockquote>
<p><strong>Q7. 고객 &#39;박지성&#39;이 구매하지 않은 도서의 이름을 검색하시오.</strong></p>
</blockquote>
<pre><code class="language-sql">SELECT *
FROM book
WHERE bookid NOT IN (
    SELECT bookid
    FROM customer AS C
        JOIN orders AS O
        ON C.custid = O.custid
    WHERE name=&#39;박지성&#39;
);</code></pre>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/82546928-05cc-4c6d-ab9b-4e755bd3a5cb/image.png" alt=""></p>
<p>제대로 조회했는지 확인 위해서 일단 전체 조회를 위에처럼 해주고, SELECT에서 책 이름만 선택하도록 수정해 아래처럼 작성해주었다.</p>
<pre><code class="language-sql">SELECT bookname
FROM book
WHERE bookid NOT IN (
    SELECT bookid
    FROM customer AS C
        JOIN orders AS O
        ON C.custid = O.custid
    WHERE name=&#39;박지성&#39;
);</code></pre>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/177f218a-fd97-4d1e-83ee-ca341b7caa79/image.png" alt=""></p>
<hr>
<h2 id="선택-프로그래머스-sql-문제">(선택) 프로그래머스 SQL 문제</h2>
<blockquote>
<p><strong>Q. 강원도에 위치한 생산공장 목록 출력하기</strong></p>
</blockquote>
<pre><code class="language-sql">-- 코드를 입력하세요
SELECT FACTORY_ID, FACTORY_NAME, ADDRESS
FROM FOOD_FACTORY
WHERE ADDRESS LIKE &#39;%강원도%&#39;
ORDER BY FACTORY_ID;</code></pre>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/9fbda337-f7dd-40bc-904b-3c21abcf52df/image.png" alt=""></p>
<blockquote>
<p><strong>Q. 고양이와 개는 몇 마리 있을까?</strong></p>
</blockquote>
<pre><code class="language-sql">SELECT ANIMAL_TYPE, COUNT(DISTINCT ANIMAL_ID) AS count
FROM ANIMAL_INS
WHERE ANIMAL_TYPE = &#39;Cat&#39; OR ANIMAL_TYPE = &#39;Dog&#39;
GROUP BY ANIMAL_TYPE
ORDER BY ANIMAL_TYPE;</code></pre>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/9c23b1a1-5b59-4f3a-9dc2-833058aa6a0f/image.png" alt=""></p>
<blockquote>
<p><strong>Q. 재구매가 일어난 상품과 회원리스트 구하기</strong></p>
</blockquote>
<pre><code class="language-sql">-- 코드를 입력하세요
SELECT USER_ID, PRODUCT_ID
FROM ONLINE_SALE
GROUP BY USER_ID, PRODUCT_ID
HAVING COUNT(*) &gt;= 2
ORDER BY USER_ID ASC, PRODUCT_ID DESC;</code></pre>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/6daaf92a-3269-4789-923a-16c1c597476d/image.png" alt=""></p>
]]></description>
        </item>
    </channel>
</rss>