<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>ttt-1-2.log</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Sat, 23 May 2026 10:35:34 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>ttt-1-2.log</title>
            <url>https://velog.velcdn.com/images/ttt-1-2/profile/5646d045-9f8c-481e-95b4-777fce0691ae/social_profile.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. ttt-1-2.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/ttt-1-2" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[JPA10] 객체 지향 쿼리 언어]]></title>
            <link>https://velog.io/@ttt-1-2/10-%EA%B0%9D%EC%B2%B4-%EC%A7%80%ED%96%A5-%EC%BF%BC%EB%A6%AC-%EC%96%B8%EC%96%B4</link>
            <guid>https://velog.io/@ttt-1-2/10-%EA%B0%9D%EC%B2%B4-%EC%A7%80%ED%96%A5-%EC%BF%BC%EB%A6%AC-%EC%96%B8%EC%96%B4</guid>
            <pubDate>Sat, 23 May 2026 10:35:34 GMT</pubDate>
            <description><![CDATA[<p><em>교재: 자바 ORM 표준 JPA 프로그래밍</em> </p>
<p>10장에서 다룰 내용:</p>
<ul>
<li>JPQL</li>
<li>Criteria</li>
<li>QueryDSL</li>
<li>네이티브 SQL</li>
<li>객체지향 쿼리 심화</li>
</ul>
<hr>
<h2 id="1-객체지향-쿼리-소개">1. 객체지향 쿼리 소개</h2>
<p>JPA는 다양한 쿼리 방법을 지원한다. 핵심은 &quot;테이블이 아닌 엔티티 객체를 대상으로 쿼리한다&quot;는 점이다.</p>
<p>JPA가 지원하는 쿼리 방식:</p>
<ul>
<li>JPQL: 가장 기본이 되는 객체지향 쿼리 언어</li>
<li>Criteria: JPQL을 자바 API로 작성하는 방식</li>
<li>QueryDSL: Criteria보다 훨씬 간결하고 직관적인 오픈소스 라이브러리</li>
<li>네이티브 SQL: SQL을 직접 사용</li>
<li>JDBC 직접 사용: JDBC, MyBatis 등을 JPA와 함께 사용</li>
</ul>
<p>실무에서는 JPQL을 기본으로 쓰고, 필요에 따라 Criteria나 QueryDSL을 조합한다.</p>
<h2 id="2-jpql">2. JPQL</h2>
<p>JPQL 문법은 SQL과 유사하다. SELECT, UPDATE, DELETE를 지원한다.</p>
<ul>
<li><em>SELECT 기본 예시</em></li>
</ul>
<pre><code class="language-java">// 기본 조회
SELECT m FROM Member AS m WHERE m.age &gt; 18</code></pre>
<h3 id="typequery-query">TypeQuery, Query</h3>
<p><code>em.createQuery()</code>의 반환 타입에 따라 두 가지로 나뉜다.</p>
<p><strong>TypeQuery</strong>: 반환 타입이 명확할 때</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><strong>Query</strong>: 반환 타입이 여러 개일 때</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; // 결과가 여러 타입이면 Object[]
    System.out.println(&quot;username = &quot; + result[0]);
    System.out.println(&quot;age = &quot; + result[1]);
}</code></pre>
<h3 id="결과-조회-api">결과 조회 API</h3>
<pre><code class="language-java">query.getResultList();    // 결과가 없으면 빈 컬렉션 반환
query.getSingleResult();  // 결과가 정확히 1개일 때 사용</code></pre>
<h3 id="파라미터-바인딩">파라미터 바인딩</h3>
<p><strong>이름 기준 파라미터</strong> (권장):</p>
<pre><code class="language-java">List&lt;Member&gt; members =
    em.createQuery(&quot;SELECT m FROM Member m WHERE m.username = :username&quot;, Member.class)
      .setParameter(&quot;username&quot;, usernameParam)
      .getResultList();</code></pre>
<p><strong>위치 기준 파라미터</strong> (비권장):</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>
<blockquote>
<p>이름 기준 파라미터를 사용하는 것이 훨씬 명확하고 유지보수에 유리하다. 위치 기준은 순서가 바뀌면 버그가 생기기 쉽다.</p>
</blockquote>
<h3 id="프로젝션-projection">프로젝션 (Projection)</h3>
<p>SELECT 절에서 조회할 대상을 지정하는 것을 프로젝션이라 한다.</p>
<pre><code class="language-java">// 엔티티 프로젝션 - 엔티티 전체 조회
SELECT m FROM Member m        // Member 엔티티
SELECT m.team FROM Member m   // 연관 엔티티 Team 조회</code></pre>
<ul>
<li>임베디드 타입 프로젝션:</li>
</ul>
<pre><code class="language-java">String query = &quot;SELECT a FROM Address a&quot;;
// 단, 임베디드 타입은 독립적으로 조회 불가 → 엔티티를 통해 조회해야 함

String query = &quot;SELECT o.address FROM Order o&quot;;</code></pre>
<ul>
<li>스칼라 타입 프로젝션 (단순 값 조회)</li>
</ul>
<pre><code class="language-java">// 단일 타입
List&lt;String&gt; usernames =
    em.createQuery(&quot;SELECT m.username FROM Member m&quot;, String.class)
      .getResultList();

// DISTINCT로 중복 제거
SELECT DISTINCT m.username FROM Member m

// 집합 함수
SELECT AVG(o.orderAmount) FROM Order o</code></pre>
<ul>
<li>여러 값 조회: 반환 타입이 다를 경우 <code>Object[]</code>로 받는다:</li>
</ul>
<pre><code class="language-java">// Object[] 방식
List&lt;Object[]&gt; resultList =
    em.createQuery(&quot;SELECT m.username, m.age FROM Member m&quot;)
      .getResultList();

for (Object[] row : resultList) {
    String username = (String) row[0];
    Integer age     = (Integer) row[1];
}</code></pre>
<ul>
<li><strong>명령어로 DTO 직접 조회</strong> (권장) <code>Object[]</code>보다 DTO로 바로 받는 방식이 더 깔끔하다:</li>
</ul>
<pre><code class="language-java">public class UserDTO {
    private String username;
    private int age;

    public UserDTO(String username, int age) {
        this.username = username;
        this.age = age;
    }
}</code></pre>
<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
    );</code></pre>
<h3 id="페이징-api">페이징 API</h3>
<p>JPA는 페이징을 두 가지 API로 추상화한다. DB마다 다른 페이징 SQL을 자동으로 생성해준다.</p>
<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);  // 조회 시작 위치 (0부터 시작)
query.setMaxResults(20);   // 조회할 데이터 수
query.getResultList();     // 11번째부터 20개 조회</code></pre>
<p>→ DB별로 생성되는 SQL이 다르다. JPA가 알아서 변환해준다.</p>
<h3 id="집합과-정렬">집합과 정렬</h3>
<ul>
<li>집합 함수</li>
</ul>
<pre><code class="language-sql">SELECT
    COUNT(m),    -- 회원 수 (반환: Long)
    SUM(m.age),  -- 나이 합 (반환: Long)
    AVG(m.age),  -- 평균 나이 (반환: Double)
    MAX(m.age),  -- 최대 나이
    MIN(m.age)   -- 최소 나이
FROM Member m</code></pre>
<ul>
<li>GROUP BY / HAVING</li>
</ul>
<pre><code class="language-sql">SELECT t.name, COUNT(m.age), SUM(m.age), AVG(m.age), MAX(m.age), MIN(m.age)
FROM Member m LEFT JOIN m.team t
GROUP BY t.name</code></pre>
<ul>
<li>ORDER BY</li>
</ul>
<pre><code class="language-sql">SELECT t.name, COUNT(m.age) AS cnt
FROM Member m LEFT JOIN m.team t
GROUP BY t.name
ORDER BY cnt</code></pre>
<h3 id="조인-join">조인 (JOIN)</h3>
<ul>
<li>내부 조인 (INNER JOIN)</li>
</ul>
<pre><code class="language-sql">String query = &quot;SELECT m FROM Member m INNER JOIN m.team t WHERE t.name = :teamName&quot;;

List&lt;Member&gt; members = em.createQuery(query, Member.class)
    .setParameter(&quot;teamName&quot;, &quot;팀A&quot;)
    .getResultList();</code></pre>
<ul>
<li>외부 조인 (LEFT OUTER JOIN)</li>
</ul>
<pre><code class="language-sql">SELECT m FROM Member m LEFT [OUTER] JOIN m.team t</code></pre>
<h3 id="페치-조인-fetch-join">페치 조인 (Fetch Join)</h3>
<p>페치 조인은 JPQL에서 성능 최적화를 위해 제공하는 특별한 조인이다. 연관 엔티티를 SQL 한 번에 함께 조회한다. 지연 로딩으로 설정해도 페치 조인을 사용하면 즉시 함께 로딩된다.</p>
<ul>
<li>엔티티 페치 조인:</li>
</ul>
<pre><code class="language-sql">// JPQL
SELECT m FROM Member m JOIN FETCH m.team</code></pre>
<p>→ 실행 SQL:</p>
<pre><code class="language-sql">SELECT M.*, T.*
FROM MEMBER M INNER JOIN TEAM T ON M.TEAM_ID = T.ID</code></pre>
<p>일반 조인과 다르게, 페치 조인은 연관 엔티티까지 함께 SELECT 절에 포함해서 가져온다. <code>member.getTeam()</code>을 호출해도 추가 쿼리가 발생하지 않는다.</p>
<ul>
<li>컬렉션 페치 조인</li>
</ul>
<pre><code class="language-sql">select t from Team t join fetch t.members where t.name = &#39;팀A&#39;</code></pre>
<p>→ 실행 SQL:</p>
<pre><code class="language-sql">SELECT T.*, M.*
FROM TEAM T INNER JOIN MEMBER M ON T.ID = M.TEAM_ID
WHERE T.NAME = &#39;팀A&#39;</code></pre>
<p>일대다 조인이므로 팀A에 회원이 2명이면 결과 row가 2개 나온다. 즉 팀 객체가 중복으로 조회될 수 있다.</p>
<ul>
<li>페치 조인과 DISTINCT: 컬렉션 페치 조인 시 중복 결과를 제거하려면 <code>DISTINCT</code>를 사용한다.</li>
</ul>
<pre><code class="language-sql">select distinct t from Team t join fetch t.members where t.name = &#39;팀A&#39;</code></pre>
<ul>
<li>일반 조인 vs 페치 조인 차이</li>
</ul>
<pre><code class="language-sql">// 일반 조인 → SELECT 절에 Team만 포함, members는 미로딩
select t from Team t join t.members m where t.name = &#39;팀A&#39;

// 페치 조인 → SELECT 절에 Team + Member 모두 포함
select t from Team t join fetch t.members where t.name = &#39;팀A&#39;</code></pre>
<p><strong>페치 조인 한계</strong></p>
<ul>
<li>페치 조인 대상에는 별칭을 줄 수 없다 (Hibernate는 가능하나 권장하지 않음)</li>
<li>둘 이상의 컬렉션을 페치 조인할 수 없다 (MultipleBagFetchException 발생)</li>
<li>컬렉션 페치 조인 시 페이징 API(<code>setFirstResult</code>, <code>setMaxResults</code>)를 사용할 수 없다 → 전체 데이터를 메모리에 올려 페이징하므로 위험</li>
</ul>
<h3 id="경로-표현식">경로 표현식</h3>
<p><code>.</code>을 통해 객체 그래프를 탐색하는 것이다.</p>
<pre><code class="language-sql">select m.username   // 상태 필드
from Member m
    join m.team t   // 단일 값 연관 필드
    join m.orders o // 컬렉션 값 연관 필드
where t.name = &#39;팀A&#39;</code></pre>
<p>경로 표현식의 3가지 종류:</p>
<table>
<thead>
<tr>
<th>종류</th>
<th>예시</th>
<th>특징</th>
</tr>
</thead>
<tbody><tr>
<td>상태 필드</td>
<td><code>m.username</code>, <code>m.age</code></td>
<td>더 이상 탐색 불가</td>
</tr>
<tr>
<td>단일 값 연관</td>
<td><code>m.team</code></td>
<td>묵시적 내부 조인 발생, 추가 탐색 가능</td>
</tr>
<tr>
<td>컬렉션 값 연관</td>
<td><code>m.orders</code></td>
<td>묵시적 내부 조인 발생, 추가 탐색 불가</td>
</tr>
</tbody></table>
<h3 id="서브쿼리">서브쿼리</h3>
<p>JPQL도 SQL처럼 서브쿼리를 지원한다. WHERE, HAVING 절에서 사용 가능하다 (SELECT, FROM 절에서는 불가).</p>
<h3 id="조건식">조건식</h3>
<p>JPQL에서 사용하는 기본 조건식들이다.</p>
<p><strong>타입 표현:</strong></p>
<table>
<thead>
<tr>
<th>종류</th>
<th>예시</th>
</tr>
</thead>
<tbody><tr>
<td>문자</td>
<td><code>&#39;Hello&#39;</code>, <code>&#39;She&#39;&#39;s&#39;</code></td>
</tr>
<tr>
<td>숫자</td>
<td><code>10L</code>, <code>10D</code>, <code>10F</code></td>
</tr>
<tr>
<td>Boolean</td>
<td><code>TRUE</code>, <code>FALSE</code></td>
</tr>
<tr>
<td>Enum</td>
<td><code>jpabook.MemberType.Admin</code></td>
</tr>
<tr>
<td>엔티티 타입</td>
<td><code>TYPE(m) = Member</code></td>
</tr>
</tbody></table>
<h3 id="상속-관계와-type-treat"><strong>상속 관계와 TYPE, TREAT</strong></h3>
<ul>
<li><strong>TYPE</strong>: 상속 관계에서 특정 하위 타입만 조회할 때 사용한다.</li>
</ul>
<pre><code class="language-sql">// Item의 하위 타입 중 Book, Movie만 조회
// JPQL
select i from Item i where type(i) IN (Book, Movie)

// 실행 SQL (단일 테이블 전략 기준)
SELECT i FROM Item i WHERE i.DTYPE in (&#39;B&#39;, &#39;M&#39;)</code></pre>
<ul>
<li><strong>TREAT</strong>: 상속 구조에서 부모 타입을 자식 타입으로 다운캐스팅할 때 사용한다.</li>
</ul>
<pre><code class="language-sql">select i from Item i where treat(i as Book).author = &#39;kim&#39;</code></pre>
<h3 id="사용자-정의-함수-jpa-21">사용자 정의 함수 (JPA 2.1)</h3>
<p>DB 전용 함수나 커스텀 함수를 JPQL에서 사용하고 싶을 때:</p>
<pre><code class="language-sql">// JPQL
select function(&#39;group_concat&#39;, i.name) from Item i

// Dialect에 등록
public class MyH2Dialect extends H2Dialect {
    public MyH2Dialect() {
        registerFunction(&quot;group_concat&quot;,
            new StandardSQLFunction(&quot;group_concat&quot;, StandardBasicTypes.STRING));
    }
}</code></pre>
<h3 id="엔티티-직접-사용">엔티티 직접 사용</h3>
<p>기본 키 값 대신 엔티티를 직접 파라미터로 사용할 수 있다. JPA가 자동으로 PK 비교로 변환한다.</p>
<pre><code class="language-sql">// 엔티티를 직접 파라미터로
String qlString = &quot;select m from Member m where m = :member&quot;;
List resultList = em.createQuery(qlString)
    .setParameter(&quot;member&quot;, member)  // 엔티티 직접 전달
    .getResultList();

// 실행 SQL → PK로 자동 변환
select m from Member m where m.id = ?</code></pre>
<h3 id="named-쿼리-정적-쿼리">Named 쿼리: 정적 쿼리</h3>
<p>JPQL은 동적 쿼리와 정적 쿼리로 나뉜다.</p>
<ul>
<li>동적 쿼리: <code>em.createQuery(&quot;...&quot;)</code>처럼 런타임에 문자열로 만드는 방식</li>
<li>정적 쿼리 (Named 쿼리): 미리 이름을 붙여 정의해두는 방식 → 애플리케이션 로딩 시점에 문법 검증</li>
</ul>
<p>@NamedQuery로 정의:</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>사용:</p>
<pre><code class="language-java">List&lt;Member&gt; resultList = em.createNamedQuery(&quot;Member.findByUsername&quot;, Member.class)
    .setParameter(&quot;username&quot;, &quot;회원1&quot;)
    .getResultList();</code></pre>
<p>XML로 정의하는 방법도 있다:</p>
<pre><code class="language-xml">&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;
&lt;entity-mappings&gt;
    &lt;named-query name=&quot;Member.findByUsername&quot;&gt;
        &lt;query&gt;&lt;![CDATA[
            select m from Member m where m.username = :username
        ]]&gt;&lt;/query&gt;
    &lt;/named-query&gt;

    &lt;named-query name=&quot;Member.count&quot;&gt;
        &lt;query&gt;select count(m) from Member m&lt;/query&gt;
    &lt;/named-query&gt;
&lt;/entity-mappings&gt;</code></pre>
<h2 id="3-criteria">3. Criteria</h2>
<p>JPQL을 자바 코드로 작성하는 API. 컴파일 시점에 오류를 잡을 수 있지만 코드가 복잡하다. 실무에서는 보통 QueryDSL을 더 선호한다.</p>
<p>기본 구조:</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();
CriteriaQuery&lt;Member&gt; cq = cb.createQuery(Member.class);
Root&lt;Member&gt; m = cq.from(Member.class); // FROM

cq.select(m)
  .where(cb.equal(m.get(&quot;username&quot;), &quot;회원1&quot;))  // WHERE
  .orderBy(cb.desc(m.get(&quot;age&quot;)));              // ORDER BY

List&lt;Member&gt; resultList = em.createQuery(cq).getResultList();</code></pre>
<p><code>CriteriaBuilder</code>는 <code>em.getCriteriaBuilder()</code>로 얻는다. <code>Root</code>는 조회 시작점(FROM 절의 엔티티)이다.</p>
<h3 id="조회-select">조회 (SELECT)</h3>
<p>단일 타입 조회:</p>
<pre><code class="language-java">cq.select(m);  // JPQL: 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;));

// 또는 cb.array() 사용
cq.select(cb.array(m.get(&quot;username&quot;), m.get(&quot;age&quot;)));</code></pre>
<p>Tuple 조회 (별칭으로 접근):</p>
<pre><code class="language-java">CriteriaQuery&lt;Tuple&gt; cq = cb.createTupleQuery();
Root&lt;Member&gt; m = cq.from(Member.class);

cq.multiselect(
    m.get(&quot;username&quot;).alias(&quot;username&quot;),
    m.get(&quot;age&quot;).alias(&quot;age&quot;)
);

for (Tuple tuple : em.createQuery(cq).getResultList()) {
    String username = tuple.get(&quot;username&quot;, String.class);
    Integer age     = tuple.get(&quot;age&quot;, Integer.class);
}</code></pre>
<h3 id="group-by--having">GROUP BY / HAVING</h3>
<pre><code class="language-java">// JPQL: select m.team.name, max(m.age), min(m.age) from Member m group by m.team.name

Expression&lt;Integer&gt; maxAge = cb.max(m.&lt;Integer&gt;get(&quot;age&quot;));
Expression&lt;Integer&gt; minAge = cb.min(m.&lt;Integer&gt;get(&quot;age&quot;));

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 min(m.age) &gt; 10</code></pre>
<h3 id="조인-join-1">조인 (JOIN)</h3>
<pre><code class="language-java">// JPQL: select m, t from Member m inner join m.team t where t.name = &#39;팀A&#39;

Root&lt;Member&gt; m = cq.from(Member.class);
Join&lt;Member, Team&gt; t = m.join(&quot;team&quot;, JoinType.INNER); // INNER / LEFT / RIGHT

cq.multiselect(m, t)
  .where(cb.equal(t.get(&quot;name&quot;), &quot;팀A&quot;));</code></pre>
<ul>
<li>페치 조인:</li>
</ul>
<pre><code class="language-java">m.fetch(&quot;team&quot;, JoinType.LEFT);
cq.select(m);</code></pre>
<h3 id="파라미터-바인딩-1">파라미터 바인딩</h3>
<pre><code class="language-java">// JPQL: select m from Member m where m.username = :usernameParam

ParameterExpression&lt;String&gt; usernameParam = cb.parameter(String.class, &quot;usernameParam&quot;);
cq.select(m).where(cb.equal(m.get(&quot;username&quot;), usernameParam));

em.createQuery(cq).setParameter(&quot;usernameParam&quot;, &quot;회원1&quot;).getResultList();</code></pre>
<p>Criteria는 타입 안전하고 동적 쿼리 작성에 강점이 있지만, 코드가 길고 가독성이 떨어진다. 실무에서는 <strong>QueryDSL</strong>이 같은 장점을 훨씬 간결하게 제공하므로 더 많이 사용된다.</p>
<h3 id="동적-쿼리">동적 쿼리</h3>
<p>Criteria의 가장 큰 강점. 조건에 따라 쿼리를 동적으로 조립할 수 있다.</p>
<ul>
<li>JPQL 방식 (문자열 조합 → 지저분):</li>
</ul>
<pre><code class="language-java">StringBuilder jpql = new StringBuilder(&quot;select m from Member m join m.team t&quot;);
List&lt;String&gt; criteria = new ArrayList&lt;&gt;();

if (age != null) criteria.add(&quot; m.age = :age &quot;);
if (username != null) criteria.add(&quot; m.username = :username &quot;);
if (teamName != null) criteria.add(&quot; t.name = :teamName &quot;);

if (!criteria.isEmpty()) jpql.append(&quot; where &quot;);
for (int i = 0; i &lt; criteria.size(); i++) {
    if (i &gt; 0) jpql.append(&quot; and &quot;);
    jpql.append(criteria.get(i));
}</code></pre>
<ul>
<li>Criteria 방식 (타입 안전, 깔끔):</li>
</ul>
<pre><code class="language-java">List&lt;Predicate&gt; predicates = new ArrayList&lt;&gt;();

if (age != null) predicates.add(cb.equal(m.&lt;Integer&gt;get(&quot;age&quot;), cb.parameter(Integer.class, &quot;age&quot;)));
if (username != null) predicates.add(cb.equal(m.get(&quot;username&quot;), cb.parameter(String.class, &quot;username&quot;)));
if (teamName != null) predicates.add(cb.equal(t.get(&quot;name&quot;), cb.parameter(String.class, &quot;teamName&quot;)));

cq.where(cb.and(predicates.toArray(new Predicate[0])));</code></pre>
<h3 id="메타모델-api">메타모델 API</h3>
<p><code>m.get(&quot;age&quot;)</code>처럼 문자열로 필드를 참조하면 오타가 생겨도 컴파일 에러가 안 난다. 메타모델을 사용하면 <code>Member_.age</code>처럼 타입 안전하게 참조할 수 있다.</p>
<pre><code class="language-java">// 메타모델 사용 전
m.&lt;Integer&gt;get(&quot;age&quot;)

// 메타모델 사용 후 (타입 안전)
m.get(Member_.age)</code></pre>
<p>메타모델 클래스(<code>Member_</code>)는 어노테이션 프로세서가 자동 생성한다. 실무에서는 빌드 도구 설정으로 자동화한다.</p>
<h2 id="4-querydsl">4. QueryDSL</h2>
<p>Criteria의 복잡함을 해결한 오픈소스 라이브러리. JPQL과 거의 같은 문법을 자바 코드로 간결하게 작성할 수 있다.</p>
<h3 id="설정">설정</h3>
<pre><code class="language-xml">&lt;dependency&gt;
    &lt;groupId&gt;com.mysema.querydsl&lt;/groupId&gt;
    &lt;artifactId&gt;querydsl-jpa&lt;/artifactId&gt;
    &lt;version&gt;3.6.3&lt;/version&gt;
&lt;/dependency&gt;</code></pre>
<p>QueryDSL도 Q 클래스를 자동 생성해야 한다. <code>mvn compile</code> 후 <code>target/generated-sources</code>에 생성된다.</p>
<h3 id="기본-사용법">기본 사용법</h3>
<pre><code class="language-java">JPAQuery query = new JPAQuery(em);
QMember qMember = new QMember(&quot;m&quot;); // Q 클래스 생성

List&lt;Member&gt; members = query
    .from(qMember)
    .where(qMember.name.eq(&quot;회원1&quot;))
    .orderBy(qMember.name.desc())
    .list(qMember);</code></pre>
<ul>
<li>기본 인스턴스 사용 (권장):</li>
</ul>
<pre><code class="language-java">// static import 활용
import static jpabook.jpashop.domain.QMember.member;

List&lt;Member&gt; members = query
    .from(member)
    .where(member.name.eq(&quot;회원1&quot;))
    .list(member);</code></pre>
<h3 id="검색-조건">검색 조건</h3>
<pre><code class="language-java">// where 절에 and 조건 나열
query.from(item)
    .where(item.name.eq(&quot;좋은상품&quot;), item.price.gt(20000))
    .list(item);

// 다양한 조건 메서드
item.price.between(10000, 20000)       // BETWEEN
item.name.contains(&quot;상품&quot;)             // LIKE &#39;%상품%&#39;
item.name.startsWith(&quot;좋은&quot;)           // LIKE &#39;좋은%&#39;</code></pre>
<h3 id="결과-조회">결과 조회</h3>
<pre><code class="language-java">query.list(item);          // 리스트 반환, 없으면 빈 컬렉션
query.uniqueResult(item);  // 단건, 없으면 null, 2개 이상이면 예외
query.singleResult(item);  // 단건, 없으면 null (uniqueResult와 유사)</code></pre>
<h3 id="페이징--정렬">페이징 / 정렬</h3>
<pre><code class="language-java">query.from(item)
    .where(item.price.gt(20000))
    .orderBy(item.price.desc(), item.stockQuantity.asc())
    .offset(10).limit(20)
    .list(item);</code></pre>
<h3 id="그룹">그룹</h3>
<pre><code class="language-java">query.from(item)
    .groupBy(item.price)
    .having(item.price.gt(1000))
    .list(item);</code></pre>
<h2 id="5-네이티브-sql">5. 네이티브 SQL</h2>
<ul>
<li>JPQL이 아무리 강력해도 결국 한계가 있다. 특정 DB에서만 지원하는 기능, 예를 들어 오라클의 <code>CONNECT BY</code>나 SQL 힌트 같은 건 JPQL로 표현이 안 된다.</li>
</ul>
<p>→ 이럴 때 쓰는 게 네이티브 SQL이다. JPA가 직접 SQL을 날릴 수 있도록 지원해주는 기능이다.</p>
<ul>
<li>JDBC로 직접 SQL을 쓰는 것과 차이가 있다. 네이티브 SQL을 쓰면 영속성 컨텍스트 기능은 그대로 사용할 수 있다. 단점은 특정 DB에 종속적인 쿼리를 작성하게 되므로 DB를 바꾸면 SQL도 바꿔야 한다는 것이다.</li>
</ul>
<h3 id="엔티티-조회">엔티티 조회</h3>
<pre><code class="language-java">String sql = &quot;SELECT ID, AGE, NAME, TEAM_ID FROM MEMBER WHERE AGE &gt; ?&quot;;

Query nativeQuery = em.createNativeQuery(sql, Member.class)
    .setParameter(1, 20);

List&lt;Member&gt; resultList = nativeQuery.getResultList();</code></pre>
<h3 id="값-조회">값 조회</h3>
<p>엔티티가 아닌 여러 컬럼 값을 그냥 받고 싶을 때는 두 번째 파라미터 없이 호출하면 된다.</p>
<pre><code class="language-java">String sql = &quot;SELECT ID, AGE, NAME FROM MEMBER WHERE AGE &gt; ?&quot;;

Query nativeQuery = em.createNativeQuery(sql)
    .setParameter(1, 20);

List&lt;Object[]&gt; resultList = nativeQuery.getResultList();

for (Object[] row : resultList) {
    String id   = (String) row[0];
    int    age  = (Integer) row[1];
    String name = (String) row[2];
}</code></pre>
<h3 id="결과-매핑-사용">결과 매핑 사용</h3>
<p>엔티티와 스칼라 값을 함께 조회하는 복잡한 경우엔 <code>@SqlResultSetMapping</code>을 써서 매핑을 정의한다.</p>
<pre><code class="language-java">@SqlResultSetMapping(
    name = &quot;memberWithOrderCount&quot;,
    entities = {
        @EntityResult(entityClass = Member.class)
    },
    columns = {
        @ColumnResult(name = &quot;ORDER_COUNT&quot;)
    }
)</code></pre>
<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 (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>→ 복잡한 쿼리 결과를 엔티티 + 추가 컬럼으로 나눠서 받을 수 있다.</p>
<h3 id="named-네이티브-쿼리">Named 네이티브 쿼리</h3>
<p>JPQL처럼 이름을 붙여서 관리할 수 있다.</p>
<p>엔티티 클래스에 <code>@NamedNativeQuery</code>로 정의하고 <code>em.createNamedQuery()</code>로 사용한다.</p>
<pre><code class="language-java">@Entity
@NamedNativeQuery(
    name  = &quot;Member.memberSQL&quot;,
    query = &quot;SELECT ID, AGE, NAME, TEAM_ID FROM MEMBER WHERE AGE &gt; ?&quot;,
    resultClass = Member.class
)
public class Member { ... }</code></pre>
<pre><code class="language-java">TypedQuery&lt;Member&gt; nativeQuery =
    em.createNamedQuery(&quot;Member.memberSQL&quot;, Member.class)
      .setParameter(1, 20);</code></pre>
<h3 id="네이티브-sql-정리">네이티브 SQL 정리</h3>
<p>네이티브 SQL은 관리하기 쉽지 않다. 자주 쓰면 특정 DB에 종속적인 쿼리가 늘어나서 이식성이 떨어진다.</p>
<p>→ 최대한 JPQL을 쓰고, 방법이 없을 때만 네이티브 SQL을 쓰는 것이 맞다.</p>
<p>그래도 안 되면 MyBatis 같은 SQL 매퍼를 함께 도입하는 것도 방법이다.</p>
<h2 id="6-객체-지향-쿼리-심화">6. 객체 지향 쿼리 심화</h2>
<h3 id="벌크-연산">벌크 연산</h3>
<p>벌크 연산은 한 번에 여러 엔티티를 수정하거나 삭제할 때 사용한다.</p>
<p>예를 들어 재고가 10개 미만인 모든 상품의 가격을 10% 올리고 싶다면, 엔티티를 하나씩 꺼내서 수정하는 건 비효율적이다.</p>
<pre><code class="language-java">// UPDATE 벌크 연산
String jpql = &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(jpql)
    .setParameter(&quot;stockAmount&quot;, 10)
    .executeUpdate();</code></pre>
<pre><code class="language-java">// DELETE 벌크 연산
String jpql = &quot;delete from Product p &quot;
            + &quot;where p.price &lt; :price&quot;;

int resultCount = em.createQuery(jpql)
    .setParameter(&quot;price&quot;, 100)
    .executeUpdate();</code></pre>
<p>→ <code>executeUpdate()</code>를 사용하고, 영향받은 엔티티 건수를 반환한다.</p>
<h3 id="영속성-컨텍스트와-jpql">영속성 컨텍스트와 JPQL</h3>
<ul>
<li>JPQL 조회 시 동작 방식</li>
</ul>
<pre><code class="language-java">DB 조회 결과 → 엔티티 확인
    ↓
이미 영속성 컨텍스트에 있음? → YES → 기존 엔티티 반환 (DB 결과 버림)
                              → NO  → 새 엔티티 영속성 컨텍스트에 저장 후 반환</code></pre>
<ul>
<li>JPQL과 플러시</li>
</ul>
<p>JPQL 실행 전에 JPA는 자동으로 플러시를 한다. 영속성 컨텍스트에 변경 사항이 있는 상태에서 JPQL로 조회하면, 아직 DB에 반영되지 않은 내용이 누락될 수 있기 때문이다.</p>
<pre><code class="language-java">em.persist(new Product(&quot;상품X&quot;, 1000)); // 아직 DB에 없음

// JPQL 실행 직전에 자동 flush 발생
List&lt;Product&gt; results = em.createQuery(&quot;select p from Product p&quot;, Product.class)
    .getResultList();
// 상품X도 결과에 포함된다</code></pre>
<p>→ <code>FlushModeType.COMMIT</code> 으로 설정하면 플러시 시점을 조정할 수 있다. 다만 이 경우 JPQL 조회 시 누락 문제가 생길 수 있으니 주의해서 사용해야 한다.</p>
<h3 id="스토어드-프로시저-jpa-21">스토어드 프로시저 (JPA 2.1)</h3>
<p>JPA 2.1부터 스토어드 프로시저 호출을 지원한다.</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);
System.out.println(&quot;out = &quot; + out); // 결과 출력</code></pre>
<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(name = &quot;outParam&quot;, mode = ParameterMode.OUT, type = Integer.class)
    }
)
@Entity
public class Member { ... }</code></pre>
<pre><code class="language-java">StoredProcedureQuery spq = em.createNamedStoredProcedureQuery(&quot;multiply&quot;);
spq.setParameter(&quot;inParam&quot;, 100);
spq.execute();

Integer out = (Integer) spq.getOutputParameterValue(&quot;outParam&quot;);</code></pre>
<p>→ 이름 기반으로 관리하면 재사용하기 훨씬 편하다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JPA9] 값 타입]]></title>
            <link>https://velog.io/@ttt-1-2/9-%EA%B0%92-%ED%83%80%EC%9E%85</link>
            <guid>https://velog.io/@ttt-1-2/9-%EA%B0%92-%ED%83%80%EC%9E%85</guid>
            <pubDate>Sun, 17 May 2026 06:13:45 GMT</pubDate>
            <description><![CDATA[<p><em>교재: 자바 ORM 표준 JPA 프로그래밍</em> </p>
<p>9장에서 다룰 내용:</p>
<ul>
<li>기본값 타입</li>
<li>임베디드 타입</li>
<li>컬렉션 타입</li>
</ul>
<hr>
<h2 id="1-기본값-타입">1. 기본값 타입</h2>
<p>기본값 타입은 자바에서 제공하는 기본타입과 유사하다. </p>
<p>예를 들어 <code>int</code>, <code>double</code>, <code>String</code>, <code>Integer</code>처럼 단순한 값을 저장하는 타입을 말한다.  </p>
<h2 id="2-임베디드-타입복합-값-타입">2. 임베디드 타입(복합 값 타입)</h2>
<ul>
<li>새로운 값 타입을 직접 정의해서 사용할 수 있다. JPA에서는 이를 임베디드 타입이라고 한다.</li>
<li>기본 사용법:</li>
</ul>
<pre><code class="language-java">@Entity
public class Member {
    @Id @GeneratedValue
    private Long id;
    private String name;

    // 근무 기간
    @Embedded
    private Period workPeriod;

    // 집 주소
    @Embedded
    private Address homeAddress;
}</code></pre>
<pre><code class="language-java">@Embeddable
public class Period {
    private LocalDate startDate;
    private LocalDate endDate;

    // 의미 있는 메서드도 추가 가능
    public boolean isWork() {
        LocalDate now = LocalDate.now();
        return !now.isBefore(startDate) &amp;&amp; !now.isAfter(endDate);
    }
}</code></pre>
<pre><code class="language-java">@Embeddable
public class Address {
    @Column(name = &quot;city&quot;)
    private String city;
    private String street;
    private String zipcode;
}</code></pre>
<p>임베디드 타입을 사용하면 DB 테이블 구조는 그대로이고, 객체 모델만 더 명확해진다. <code>isWork()</code> 같은 유의미한 메서드를 값 타입 안에 정의할 수 있어 응집도가 높아진다.</p>
<h3 id="임베디드-타입과-테이블-매핑">임베디드 타입과 테이블 매핑</h3>
<p>임베디드 타입을 사용해도 테이블은 하나다. 임베디드 타입 필드들이 Member 테이블 컬럼으로 그냥 펼쳐진다.</p>
<pre><code class="language-sql">CREATE TABLE MEMBER (
    ID         BIGINT NOT NULL,
    NAME       VARCHAR(255),
    START_DATE DATE,
    END_DATE   DATE,
    CITY       VARCHAR(255),
    STREET     VARCHAR(255),
    ZIPCODE    VARCHAR(255),
    PRIMARY KEY (ID)
);</code></pre>
<h3 id="임베디드-타입-중첩">임베디드 타입 중첩</h3>
<p>임베디드 타입 안에 또 다른 임베디드 타입을 넣을 수 있다.</p>
<pre><code class="language-java">@Embeddable
public class Address {
    private String city;
    private String street;

    @Embedded
    private Zipcode zipcode; // 중첩 임베디드
}

@Embeddable
public class Zipcode {
    private String zip;
    private String plusFour;
}</code></pre>
<h3 id="같은-임베디드-타입을-두-번-사용하는-경우-attributeoverride">같은 임베디드 타입을 두 번 사용하는 경우: @AttributeOverride</h3>
<p>하나의 엔티티에 같은 임베디드 타입을 두 번 사용하면 컬럼명이 충돌한다. 이때 <code>@AttributeOverride</code>로 컬럼명을 재정의한다.</p>
<pre><code class="language-java">@Entity
public class Member {
    @Id @GeneratedValue
    private Long id;
    private String name;

    @Embedded
    private 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(name = &quot;COMPANY_ZIPCODE&quot;))
    })
    private Address companyAddress;
}</code></pre>
<h3 id="임베디드-타입과-null">임베디드 타입과 null</h3>
<p>임베디드 타입을 null로 설정하면 매핑된 컬럼들도 모두 null이 된다.</p>
<pre><code class="language-java">member.setAddress(null);
em.persist(member); // city, street, zipcode 모두 null로 저장</code></pre>
<h2 id="3-값-타입과-불변-객체">3. 값 타입과 불변 객체</h2>
<p>값 타입은 공유하면 위험하다.</p>
<ul>
<li>위험한 코드 예제:</li>
</ul>
<pre><code class="language-java">// 위험한 코드!
Address address = new Address(&quot;서울&quot;, &quot;강남대로&quot;, &quot;12345&quot;);

member1.setHomeAddress(address);
member2.setHomeAddress(address); // 같은 인스턴스 공유

member1.getHomeAddress().setCity(&quot;부산&quot;); // member2도 부산이 되어버린다!</code></pre>
<p>→ <strong>해결책</strong>: 값 타입을 불변 객체로 만든다. setter를 제거하고 생성자로만 초기화한다.</p>
<pre><code class="language-java">@Embeddable
public class Address {
    private String city;
    private String street;
    private String zipcode;

    // 기본 생성자 (JPA 필수)
    protected Address() {}

    // 값 변경은 새 객체 생성으로만 가능
    public Address(String city, String street, String zipcode) {
        this.city = city;
        this.street = street;
        this.zipcode = zipcode;
    }

    // getter만 제공, setter 없음
}</code></pre>
<p>값을 변경하고 싶으면 새 객체를 만들어서 교체하는 방식을 사용한다.</p>
<h2 id="4-값-타입의-비교">4. 값 타입의 비교</h2>
<p>값 타입은 <code>==</code>가 아니라 <code>equals()</code>로 비교해야 한다.</p>
<pre><code class="language-java">Address a1 = new Address(&quot;서울&quot;, &quot;강남대로&quot;, &quot;12345&quot;);
Address a2 = new Address(&quot;서울&quot;, &quot;강남대로&quot;, &quot;12345&quot;);

System.out.println(a1 == a2);       // false (다른 인스턴스)
System.out.println(a1.equals(a2));  // true (내용이 같으면)</code></pre>
<p>→ 임베디드 타입에는 반드시 <code>equals()</code>와 <code>hashCode()</code>를 재정의해야 한다.</p>
<pre><code class="language-java">@Embeddable
public class Address {
    ...

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Address)) return false;
        Address address = (Address) o;
        return Objects.equals(city, address.city) &amp;&amp;
               Objects.equals(street, address.street) &amp;&amp;
               Objects.equals(zipcode, address.zipcode);
    }

    @Override
    public int hashCode() {
        return Objects.hash(city, street, zipcode);
    }
}</code></pre>
<h2 id="5-값-타입-컬렉션">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;&gt;();

    @ElementCollection
    @CollectionTable(
        name = &quot;ADDRESS&quot;,
        joinColumns = @JoinColumn(name = &quot;MEMBER_ID&quot;)
    )
    private List&lt;Address&gt; addressHistory = new ArrayList&lt;&gt;();
}</code></pre>
<p>→ 값 타입 컬렉션은 별도의 테이블에 저장된다. 기본 페치 전략은 <strong>LAZY</strong>다.</p>
<pre><code class="language-sql">-- FAVORITE_FOODS 테이블
CREATE TABLE FAVORITE_FOODS (
    MEMBER_ID  BIGINT NOT NULL,
    FOOD_NAME  VARCHAR(255)
);

-- ADDRESS 테이블
CREATE TABLE ADDRESS (
    MEMBER_ID BIGINT NOT NULL,
    CITY      VARCHAR(255),
    STREET    VARCHAR(255),
    ZIPCODE   VARCHAR(255)
);</code></pre>
<h3 id="값-타입-컬렉션-수정">값 타입 컬렉션 수정</h3>
<pre><code class="language-sql">Member member = em.find(Member.class, 1L);

// 음식 수정: 치킨 → 한식
member.getFavoriteFoods().remove(&quot;치킨&quot;);
member.getFavoriteFoods().add(&quot;한식&quot;);

// 주소 수정: 기존 주소 삭제 후 새 주소 추가
member.getAddressHistory().remove(new Address(&quot;서울&quot;, &quot;기존로&quot;, &quot;11111&quot;));
member.getAddressHistory().add(new Address(&quot;서울&quot;, &quot;새로운로&quot;, &quot;22222&quot;));</code></pre>
<p>값 타입 컬렉션은 값이 변경되면 해당 컬렉션 테이블의 데이터를 <strong>전부 삭제하고 다시 저장</strong>한다. 따라서 데이터가 많다면 성능 문제가 생길 수 있다.</p>
<h3 id="값-타입-컬렉션의-제약과-대안">값 타입 컬렉션의 제약과 대안</h3>
<p>값 타입 컬렉션은 편리하지만 제약이 있다.</p>
<ul>
<li>값 타입에는 식별자가 없어서 변경 추적이 어렵다.</li>
<li>변경이 일어나면 관련 테이블의 모든 데이터를 지우고 다시 저장한다.</li>
<li>데이터가 많아지면 성능이 떨어진다.</li>
</ul>
<p>실무에서의 대안: 값 타입 컬렉션 대신 일대다 관계 + 별도 엔티티를 사용한다.</p>
<pre><code class="language-java">@Entity
public class AddressEntity {
    @Id @GeneratedValue
    private Long id;

    @Embedded
    private Address address;
}

// Member에서는
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = &quot;MEMBER_ID&quot;)
private List&lt;AddressEntity&gt; addressHistory = new ArrayList&lt;&gt;();</code></pre>
<p>→ 이렇게 하면 식별자가 생겨서 변경 추적이 가능하고, 성능도 안정적이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JPA8] 프록시와 연관관계 관리]]></title>
            <link>https://velog.io/@ttt-1-2/8-%ED%94%84%EB%A1%9D%EC%8B%9C%EC%99%80-%EC%97%B0%EA%B4%80%EA%B4%80%EA%B3%84-%EA%B4%80%EB%A6%AC</link>
            <guid>https://velog.io/@ttt-1-2/8-%ED%94%84%EB%A1%9D%EC%8B%9C%EC%99%80-%EC%97%B0%EA%B4%80%EA%B4%80%EA%B3%84-%EA%B4%80%EB%A6%AC</guid>
            <pubDate>Sun, 03 May 2026 07:12:26 GMT</pubDate>
            <description><![CDATA[<p><em>교재: 자바 ORM 표준 JPA 프로그래밍</em> </p>
<p>8장에서 다룰 내용:</p>
<ul>
<li>프록시와 즉시로딩, 지연로딩</li>
<li>영속성 전이와 고아 객체</li>
</ul>
<hr>
<h2 id="1-프록시">1. 프록시</h2>
<p>엔티티를 조회할 때 연관된 엔티티가 항상 필요한 것은 아니다.
ex: 회원을 조회할 때 팀 정보는 상황에 따라 사용할 수도 있고 사용하지 않을 수도 있다. 하지만 기본적으로 <code>em.find()</code>를 사용하면 연관된 엔티티까지 함께 조회될 수 있어 비효율이 발생한다.</p>
<p>→ 이 문제를 해결하기 위해 JPA는 지연 로딩을 제공한다. 실제 사용하는 시점까지 데이터베이스 조회를 미루는 방식이다. 이때 실제 엔티티 대신 사용하는 객체를 프록시라 한다.</p>
<h3 id="프록시-기초">프록시 기초</h3>
<p>JPA에서 엔티티 조회 방법:</p>
<ul>
<li><code>em.find()</code>: 즉시 조회, DB 바로 접근</li>
<li><code>em.getReference()</code>: 프록시 반환, DB 조회하지 않음</li>
</ul>
<p>프록시 구조: 프록시는 실제 엔티티를 상속한 객체다. 겉보기에는 동일하게 사용 가능하다. 또한 프록시는 내부에 실제 엔티티 참조(target)를 가진다.</p>
<p>프록시 초기화</p>
<pre><code class="language-java">// MemberProxy 반환
Member member = em.getReference(Member.class, &quot;id1&quot;);
member.getName(); // 1. getName()</code></pre>
<pre><code class="language-java">// 프록시 클래스 예상 코드
class MemberEntity extends Member {
    Member target = null;

    public String getName() {

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

        // 5. target.getName()
        return target.getName();
    }
}</code></pre>
<h3 id="프로시와-식별자">프로시와 식별자</h3>
<p>엔티티를 프록시 조회할 때 식별자(PK) 값을 파라미터로 전달하는데 프록시 객체는 이 식별자 값을 보관한다.</p>
<pre><code class="language-java">Team team = em.getReference(Team.class, &quot;team1&quot;); //식별자 보관
team.getId(); //초기화되지 않음</code></pre>
<h3 id="프록시-확인">프록시 확인</h3>
<p>프록시 초기화 여부 확인 방법: <code>PersistenceUnitUtil.isLoaded()</code></p>
<pre><code class="language-java">boolean isLoad = em.getEntityManagerFactory()
                                .getPersistenceUnitUtil()
                                .isLoaded(entity);</code></pre>
<p>→ 초기화 안됨: false
→ 초기화됨: true</p>
<h2 id="2-즉시-로딩과-지연-로딩">2. 즉시 로딩과 지연 로딩</h2>
<p>연관 엔티티를 언제 조회할지 결정하는 방식이다. 핵심은 “지금 가져올지, 나중에 필요할 때 가져올지”이다.</p>
<h3 id="21-즉시-로딩-eager">2.1 즉시 로딩 (EAGER)</h3>
<p>엔티티를 조회할 때 연관 엔티티도 함께 조회한다. 한 번 조회 시 JOIN을 사용해서 같이 가져오는 경우가 많다. 조회 시점에 이미 데이터가 준비되어 있어서 바로 사용할 수 있다. 하지만 필요하지 않은 데이터까지 가져올 수 있다.</p>
<p>예시 코드</p>
<pre><code class="language-java">@Entity
public class Member {

    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = &quot;TEAM_ID&quot;)
    private Team team;

}</code></pre>
<pre><code class="language-sql">-- 실행 SQL (예시)

SELECT
    M.MEMBER_ID,
    M.TEAM_ID,
    M.USERNAME,
    T.TEAM_ID,
    T.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>
<p>→ member를 조회하는 순간 team도 같이 조회된다. 이후 getTeam() 호출 시 추가 쿼리는 발생하지 않는다.</p>
<h3 id="22-지연-로딩-lazy">2.2 지연 로딩 (LAZY)</h3>
<p>연관 엔티티를 실제 사용할 때 조회한다. 처음에는 프록시 객체만 들어가 있고, 값이 필요한 순간 DB를 조회한다. 불필요한 데이터 조회를 줄일 수 있다.</p>
<p>예시 코드</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>
<pre><code class="language-sql">-- 실행 SQL 흐름

-- 1. member 조회
SELECT *
FROM MEMBER
WHERE MEMBER_ID = &#39;member1&#39;;

-- 2. team 실제 사용 시 조회
SELECT *
FROM TEAM
WHERE TEAM_ID = &#39;team1&#39;;</code></pre>
<p>→ 처음에는 team이 프록시로 들어오고, getName()을 호출하는 순간 실제 team 데이터를 가져온다.</p>
<p>즉시 로딩은 조회 시점에 모두 가져와서 편하지만 비효율이 생길 수 있다. 지연 로딩은 필요한 시점에만 조회해서 성능에 유리하지만, 사용 시점과 영속성 컨텍스트를 신경 써야 한다.</p>
<p>실무에서는 대부분 LAZY를 기본으로 사용하고, 필요한 경우에만 JOIN이나 fetch 전략으로 최적화한다.</p>
<h2 id="3-지연-로딩-활용">3. 지연 로딩 활용</h2>
<h3 id="31-프록시와-컬렉션-래퍼">3.1 프록시와 컬렉션 래퍼</h3>
<p>지연 로딩에서 컬렉션은 일반 List가 아니라 하이버네이트가 제공하는 컬렉션 래퍼로 관리된다. 이 객체는 내부적으로 프록시처럼 동작하며 실제 데이터가 필요할 때 DB를 조회한다.</p>
<pre><code class="language-java">Member member = em.find(Member.class, &quot;member1&quot;);
List&lt;Order&gt; orders = member.getOrders();
System.out.println(orders.getClass().getName());</code></pre>
<p>orders를 조회해도 초기화되지 않는다. 실제 데이터 접근 시점에 초기화된다. <code>member.getOrders().get(0);</code></p>
<p>이 시점에 DB를 조회하여 주문 데이터를 가져온다. 그리고 Order는 Product를 즉시 로딩으로 설정했으므로 함께 로딩된다.</p>
<h3 id="32-jpa-기본-페치-전략">3.2 JPA 기본 페치 전략</h3>
<p>JPA의 기본 전략은 단건 연관은 즉시 로딩, 컬렉션은 지연 로딩이다.</p>
<p>@ManyToOne, @OneToOne → EAGER
@OneToMany, @ManyToMany → LAZY</p>
<p>컬렉션은 데이터 양이 많을 수 있어서 기본적으로 지연 로딩을 사용한다. 실무에서는 대부분 LAZY를 기본으로 두고 필요한 경우만 fetch join으로 최적화한다.</p>
<h3 id="33-컬렉션에-eager-사용-시-주의점">3.3 컬렉션에 EAGER 사용 시 주의점</h3>
<p>컬렉션을 즉시 로딩으로 설정하면 조인으로 인해 데이터가 급격히 증가할 수 있다. 특히 여러 컬렉션을 동시에 조인하면 N × M 형태로 결과가 커진다. 성능 문제가 발생할 수 있다.</p>
<p>또한 컬렉션 즉시 로딩은 항상 OUTER JOIN을 사용한다. 데이터가 없어도 결과를 유지하기 위함이다.</p>
<p>결론적으로 컬렉션에는 EAGER 사용을 지양하고, 필요 시 명시적으로 조회 전략을 조정하는 것이 좋다.</p>
<h2 id="4-영속성-전이-cascade">4. 영속성 전이: CASCADE</h2>
<p>영속성 전이는 부모 엔티티를 저장하거나 삭제할 때 연관된 자식 엔티티도 함께 처리하도록 해주는 기능이다. 즉, 부모만 persist/remove 해도 자식까지 자동으로 처리된다.</p>
<h3 id="41-저장-cascadetypepersist">4.1 저장 (CascadeType.PERSIST)</h3>
<p>cascade 옵션을 사용하면 부모만 저장해도 자식까지 함께 저장된다.</p>
<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>
<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(child2);

    em.persist(parent);

}</code></pre>
<p>→ parent만 persist 해도 child1, child2까지 함께 저장된다.</p>
<h3 id="42-삭제-cascadetyperemove">4.2 삭제 (CascadeType.REMOVE)</h3>
<p>삭제도 동일하게 부모만 제거하면 자식도 함께 삭제된다.</p>
<pre><code class="language-java">Parent parent = em.find(Parent.class, 1L);
em.remove(parent);</code></pre>
<ul>
<li>CascadeType.REMOVE 설정 시 자식까지 자동 삭제된다.</li>
<li>설정이 없으면 FK 제약 때문에 예외가 발생한다.</li>
</ul>
<h3 id="43-cascade-종류">4.3 CASCADE 종류</h3>
<p>CascadeType은 여러 옵션을 제공한다.</p>
<pre><code class="language-java">public enum CascadeType {
    ALL,
    PERSIST,
    MERGE,
    REMOVE,
    REFRESH,
    DETACH
}</code></pre>
<p>여러 옵션을 함께 사용할 수도 있다.</p>
<p><code>cascade = {CascadeType.PERSIST, CascadeType.REMOVE}</code></p>
<h2 id="5-고아-객체">5. 고아 객체</h2>
<p>고아 객체 제거는 부모와의 연관관계가 끊어진 자식 엔티티를 자동으로 삭제하는 기능이다. 즉, 컬렉션에서 참조만 제거하면 DB에서도 DELETE 된다.</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>사용 방식은 컬렉션에서 제거하는 것만으로 충분하다.</p>
<pre><code class="language-java">Parent parent = em.find(Parent.class, id);
parent.getChildren().remove(0);</code></pre>
<p>→ flush 시점에 DELETE FROM CHILD 실행된다.</p>
<p>모든 자식을 제거하려면 컬렉션을 비우면 된다: <code>parent.getChildren().clear();</code></p>
<p>고아 객체 제거는 “더 이상 어떤 곳에서도 참조되지 않는 엔티티를 삭제”하는 개념이다. 따라서 한 부모에 완전히 종속된 경우에만 사용해야 하며, 여러 곳에서 참조되는 엔티티에는 사용하면 안 된다. (보통 @OneToOne, @OneToMany에서 사용)</p>
<p>또한 부모를 삭제하면 자식도 함께 삭제되는데, 이는 CascadeType.REMOVE와 유사하게 동작한다.</p>
<h2 id="6-영속성-전이---고아-객체-생명주기">6. 영속성 전이  + 고아 객체, 생명주기</h2>
<p>Cascade + orphanRemoval을 함께 사용하면 부모가 자식의 전체 생명주기를 관리할 수 있다.</p>
<ul>
<li>자식 추가는 cascade로 처리된다.</li>
</ul>
<pre><code class="language-java">Parent parent = em.find(Parent.class, parentId);
parent.addChild(child1);</code></pre>
<p>→ parent만 관리하면 child 자동 persist</p>
<ul>
<li>자식 삭제는 orphanRemoval로 처리된다.</li>
</ul>
<pre><code class="language-java">Parent parent = em.find(Parent.class, parentId);
parent.getChildren().remove(child1);</code></pre>
<p>→ 컬렉션에서 제거하면 child 자동 delete</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JPA7] 고급 매핑]]></title>
            <link>https://velog.io/@ttt-1-2/7-%EA%B3%A0%EA%B8%89-%EB%A7%A4%ED%95%91</link>
            <guid>https://velog.io/@ttt-1-2/7-%EA%B3%A0%EA%B8%89-%EB%A7%A4%ED%95%91</guid>
            <pubDate>Sun, 03 May 2026 06:35:36 GMT</pubDate>
            <description><![CDATA[<p><em>교재: 자바 ORM 표준 JPA 프로그래밍</em> </p>
<p>7장에서 다룰 고급 매핑은 다음과 같다:</p>
<ul>
<li>상속 관계 패핑</li>
<li>MappedSuperClass</li>
<li>복합 키와 식별 관계 매핑</li>
<li>조인 테이블</li>
<li>엔티티 하나에 여러 테이블 매핑</li>
</ul>
<hr>
<h2 id="1-상속-관계-매핑">1. 상속 관계 매핑</h2>
<p>객체지향에서는 상속 구조를 사용하지만 데이터베이스는 테이블 구조를 사용한다. 그래서 JPA에서는 상속 구조를 테이블로 변환하는 전략이 필요하다.</p>
<p>대표적으로 3가지 전략이 있다.</p>
<h3 id="11-조인-전력">1.1 조인 전력</h3>
<p>부모와 자식 각각 테이블을 만든다. 자식 테이블은 부모의 PK를 FK로 같이 사용한다. 조회 시에는 JOIN이 필요하다.</p>
<p>장점</p>
<ul>
<li>정규화 구조 유지</li>
<li>데이터 중복 없음</li>
<li>외래키 제약 사용 가능</li>
</ul>
<p>단점</p>
<ul>
<li>JOIN 많이 발생 → 성능 저하 가능</li>
<li>INSERT 시 여러 번 쿼리 실행</li>
</ul>
<p>핵심 설정</p>
<pre><code class="language-java">@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn(name = &quot;DTYPE&quot;)</code></pre>
<h3 id="12-단일-테이블-전략">1.2 단일 테이블 전략</h3>
<p>모든 데이터를 하나의 테이블에 저장한다. DTYPE 컬럼으로 타입을 구분한다.</p>
<p>장점</p>
<ul>
<li>JOIN 없음 → 조회 성능 가장 빠름</li>
<li>구조 단순</li>
</ul>
<p>단점</p>
<ul>
<li>사용하지 않는 컬럼은 null 허용</li>
<li>테이블이 커질 수 있음</li>
</ul>
<p>핵심 설정</p>
<pre><code class="language-java">@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = &quot;DTYPE&quot;)</code></pre>
<h3 id="13-구현-클래스마다-테이블-전략">1.3 구현 클래스마다 테이블 전략</h3>
<p>자식 엔티티마다 테이블을 만든다. 각 테이블에 부모 필드까지 모두 포함된다.</p>
<p>장점</p>
<ul>
<li>서브 타입별로 독립적 저장</li>
<li>NOT NULL 제약 사용 가능</li>
</ul>
<p>단점</p>
<ul>
<li>UNION 쿼리 필요 → 성능 안 좋음</li>
<li>비추천 전략</li>
</ul>
<p>핵심 설정</p>
<pre><code class="language-java">@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)</code></pre>
<h2 id="2-mappedsuperclass">2. @MappedSuperClass</h2>
<p><code>@MappedSuperclass</code>는 테이블과 매핑되지 않는 부모 클래스이다. 단순히 공통 필드를 자식 엔티티에 상속하기 위한 용도이다. 데이터베이스에는 테이블이 생성되지 않는다.</p>
<ul>
<li><p>왜 사용하는가: 여러 엔티티에서 공통으로 사용하는 필드가 있다. (ex: id, name 같은 기본 정보. 이런 필드를 부모 클래스에 모아두고 재사용한다)</p>
</li>
<li><p>구조:</p>
<ul>
<li><p>부모 클래스는 @MappedSuperclass로 선언한다.</p>
</li>
<li><p>자식 클래스만 @Entity로 테이블과 매핑된다.</p>
<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>
</li>
</ul>
</li>
</ul>
<p>특징:</p>
<ul>
<li>부모 클래스는 테이블이 없다</li>
<li>자식 엔티티만 테이블이 생성된다</li>
<li>공통 컬럼을 코드 레벨에서 재사용한다</li>
</ul>
<p>주의점:</p>
<ul>
<li>@MappedSuperclass는 엔티티가 아니다</li>
<li>em.find()나 JPQL에서 조회할 수 없다</li>
<li>보통 abstract 클래스로 만든다</li>
</ul>
<p><strong><em>→ 정리: @MappedSuperclass는 테이블 매핑이 아닌 공통 필드 상속용 구조이다.</em></strong></p>
<h2 id="3-복합-키와-식별-관계-매핑">3. 복합 키와 식별 관계 매핑</h2>
<h3 id="31-식별-관계-vs-비식별-관계">3.1 식별 관계 vs 비식별 관계</h3>
<p>테이블 관계는 외래 키가 기본 키에 포함되는지에 따라 나뉜다.</p>
<ul>
<li>식별 관계 (Identifying Relationship)<ul>
<li>부모의 기본 키를 자식이 내려받는다. 자식은 부모 PK를 포함해서 복합 키를 만든다.</li>
<li>PK + FK 구조</li>
</ul>
</li>
<li>비식별 관계 (Non-identifying Relationship)<ul>
<li>부모의 기본 키를 자식이 외래 키로만 사용한다.</li>
<li>자식은 별도의 PK를 가진다.</li>
<li>필수적 관계: FK에 NULL 불가</li>
<li>선택적 관계: FK에 NULL 허용</li>
</ul>
</li>
</ul>
<h3 id="32-복합-키-비식별-관계-매핑">3.2 복합 키: 비식별 관계 매핑</h3>
<p>복합 키는 2개 이상의 컬럼으로 구성된 PK이다. JPA에서는 단순히 @Id 여러 개로 처리할 수 없다 →식별자 클래스를 만들어야 한다.</p>
<p>JPA는 두 가지 방법을 제공한다.</p>
<ul>
<li>@IdClass: DB 중심, 단순</li>
<li>@EmbeddedId: 객체지향, 깔끔</li>
</ul>
<blockquote>
<p><strong>@IdClass</strong></p>
</blockquote>
<p>DB 중심 방식이다. 엔티티에 여러 @Id를 직접 선언한다.</p>
<pre><code class="language-java">@IdClass(ParentId.class)
class Parent {
    @Id String id1;
    @Id String id2;
}</code></pre>
<blockquote>
<p><strong>@EmbeddedId</strong></p>
</blockquote>
<p>객체지향 방식이다. 식별자 클래스를 엔티티 안에 포함한다.</p>
<pre><code class="language-java">@EmbeddedId
private ParentId id;</code></pre>
<h3 id="33-복합-키-식별-관계-매핑">3.3 복합 키: 식별 관계 매핑</h3>
<ul>
<li>식별 관계 구조: 식별 관계에서는 부모 → 자식 → 손자까지 기본 키가 계속 전파된다.</li>
</ul>
<p>ex:</p>
<ul>
<li>Parent</li>
<li>Child (PK = parent_id + child_id)</li>
<li>GrandChild (PK = parent_id + child_id + grandchild_id)</li>
</ul>
<blockquote>
<p>@IdClass로 식별 관계 매핑</p>
</blockquote>
<p>식별 관계에서는 @Id + @ManyToOne을 같이 사용한다.</p>
<p>Child 엔티티 핵심</p>
<pre><code class="language-java">@Id
@ManyToOne
@JoinColumn(name = &quot;PARENT_ID&quot;)
private Parent parent;

@Id
@Column(name = &quot;CHILD_ID&quot;)
private String childId;</code></pre>
<p>ChildId (식별자 클래스)</p>
<pre><code class="language-java">class ChildId {
    private String parent;   // Parent.id
    private String childId;
}</code></pre>
<p>GrandChild까지 확장</p>
<pre><code class="language-java">@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;</code></pre>
<blockquote>
<p>@EmbeddedId + 식별 관계</p>
</blockquote>
<p>이 경우는 @MapsId 사용이 핵심이다.</p>
<p>Child 엔티티</p>
<pre><code class="language-java">@EmbeddedId
private ChildId id;

@MapsId(&quot;parentId&quot;)
@ManyToOne
@JoinColumn(name = &quot;PARENT_ID&quot;)
private Parent parent;</code></pre>
<p>ChildId</p>
<pre><code class="language-java">@Embeddable
class ChildId {
    private String parentId;
    private String id;
}</code></pre>
<p>GrandChild (EmbeddedId)</p>
<pre><code class="language-java">@EmbeddedId
private GrandChildId id;

@MapsId(&quot;childId&quot;)
@ManyToOne
@JoinColumns({
    @JoinColumn(name = &quot;PARENT_ID&quot;),
    @JoinColumn(name = &quot;CHILD_ID&quot;)
})
private Child child;</code></pre>
<h3 id="34-비식별-관계로-구현">3.4 비식별 관계로 구현</h3>
<p><strong><em>구조 변화</em></strong></p>
<p>Before (식별 관계)</p>
<ul>
<li>PK = 부모 PK 포함</li>
</ul>
<p>After (비식별 관계)</p>
<ul>
<li>PK = 독립적인 값</li>
<li>FK = 따로 존재</li>
</ul>
<pre><code class="language-java">@Id @GeneratedValue
private Long id;

@ManyToOne
@JoinColumn(name = &quot;PARENT_ID&quot;)
private Parent parent;</code></pre>
<h3 id="35-일대일-식별-관계">3.5 일대일 식별 관계</h3>
<p>자식 PK = 부모 PK → 복합 키 필요 없음</p>
<pre><code class="language-java">@Entity
class BoardDetail {

    @Id
    private Long boardId;

    @MapsId
    @OneToOne
    @JoinColumn(name=&quot;BOARD_ID&quot;)
    private Board board;
}</code></pre>
<h3 id="36-식별-vs-비식별-관계-정리">3.6 식별 vs 비식별 관계 정리</h3>
<p><strong><em>식별 관계 단점</em></strong></p>
<ul>
<li>PK가 계속 증가</li>
<li>JOIN 복잡</li>
<li>SQL 복잡</li>
<li>변경 어려움</li>
<li>복합 키 필수</li>
</ul>
<p><strong><em>비식별 관계 장점</em></strong></p>
<ul>
<li>단순한 PK</li>
<li>유지보수 쉬움</li>
<li>변경 유연</li>
<li>JPA 사용 편함</li>
</ul>
<h2 id="4-조인-테이블">4. 조인 테이블</h2>
<p>데이터베이스 테이블의 연관관계를 설계하는 방법은 크게 2가지다.</p>
<ul>
<li>조인 컬럼 사용 (외래 키)</li>
<li>조인 테이블 사용</li>
</ul>
<p>조인 컬럼 vs 조인 테이블</p>
<table>
<thead>
<tr>
<th>구분</th>
<th>조인 컬럼</th>
<th>조인 테이블</th>
</tr>
</thead>
<tbody><tr>
<td>구조</td>
<td>단순</td>
<td>복잡</td>
</tr>
<tr>
<td>테이블 수</td>
<td>2개</td>
<td>3개</td>
</tr>
<tr>
<td>null 처리</td>
<td>필요</td>
<td>불필요</td>
</tr>
<tr>
<td>확장성</td>
<td>낮음</td>
<td>높음</td>
</tr>
</tbody></table>
<p><strong>JPA 매핑</strong></p>
<ul>
<li>조인 컬럼</li>
</ul>
<pre><code class="language-java">@ManyToOne
@JoinColumn(name = &quot;LOCKER_ID&quot;)
private Locker locker;</code></pre>
<ul>
<li>조인 테이블</li>
</ul>
<pre><code class="language-java">@ManyToOne
@JoinTable(
    name = &quot;MEMBER_LOCKER&quot;,
    joinColumns = @JoinColumn(name = &quot;MEMBER_ID&quot;),
    inverseJoinColumns = @JoinColumn(name = &quot;LOCKER_ID&quot;)
)
private Locker locker;</code></pre>
<h2 id="5-엔티티-하나에-여러-테이블-매핑">5. 엔티티 하나에 여러 테이블 매핑</h2>
<p>하나의 엔티티를 여러 테이블에 매핑할 수 있다.</p>
<ul>
<li>@SecondaryTable</li>
</ul>
<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
    private Long id;

    private String title;

    @Column(table = &quot;BOARD_DETAIL&quot;)
    private String content;
}</code></pre>
<p>동작 방식:</p>
<ul>
<li><p>BOARD → id, title</p>
</li>
<li><p>BOARD_DETAIL → content</p>
</li>
<li><p>기본 키로 조인</p>
</li>
<li><p>여러 테이블 매핑</p>
</li>
</ul>
<pre><code class="language-java">@SecondaryTables({
    @SecondaryTable(name=&quot;BOARD_DETAIL&quot;),
    @SecondaryTable(name=&quot;BOARD_FILE&quot;)
})</code></pre>
<ul>
<li>단점: 항상 조인 발생, 성능 최적화 어려움</li>
<li>권장 방식: 실무에서는: 엔티티 분리 + 연관관계 사용</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JPA6] 다양한 연관관계 매핑]]></title>
            <link>https://velog.io/@ttt-1-2/6-%EB%8B%A4%EC%96%91%ED%95%9C-%EC%97%B0%EA%B4%80%EA%B4%80%EA%B3%84-%EB%A7%A4%ED%95%91</link>
            <guid>https://velog.io/@ttt-1-2/6-%EB%8B%A4%EC%96%91%ED%95%9C-%EC%97%B0%EA%B4%80%EA%B4%80%EA%B3%84-%EB%A7%A4%ED%95%91</guid>
            <pubDate>Sat, 11 Apr 2026 09:37:11 GMT</pubDate>
            <description><![CDATA[<p><em>교재: 자바 ORM 표준 JPA 프로그래밍</em> </p>
<p>6장은 다양한 연관관계를 다룬다 (다대일, 일대다, 일대일, 다대다).</p>
<hr>
<h2 id="1-다대일">1. 다대일</h2>
<ul>
<li>다대일 관계의 반대 방향은 항상 일대다 관계고 일대다 관계의 방향은 항상 다대일 관계다.<ul>
<li>데이터베이스 테이블: 일(1), 다(N) 관계에서 외래 키는 항상 다쪽에 있다.</li>
<li>객체 양방향 관계: 연관관계의 주인은 항상 다쪽이다.</li>
</ul>
</li>
</ul>
<p>ex: 회원(N), 팀(1)이 있으면 회원 쪽이 연관관계의 주인이다.</p>
<h3 id="11-다대일-단방향-n1">1.1 다대일 단방향 [N:1]</h3>
<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;)
    private Team team;

    // ...
}</code></pre>
<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>→ 회원은 Member.team으로 팀 엔티티를 참조 할 수 있다. 팀에는 회원을 참조하는 필드가 없다. 회원과 팀은 다대일 <strong><em>단방향 연관관계다.</em></strong></p>
<h3 id="12-다대일-양방향-n1-1n">1.2 다대일 양방향 [N:1, 1:N]</h3>
<pre><code class="language-java">// 회원 엔티티

@ManyToOne
@JoinColumn(name=&quot;TEAM_ID&quot;)
private Team team;</code></pre>
<pre><code class="language-java">// 팀 엔티티

@OneToMany(mappedBy=&quot;team&quot;)
private List&lt;Member&gt; members = new ArrayList&lt;Member&gt;();</code></pre>
<ul>
<li>양방향은 키가 있는 쪽이 연관관계의 주인이다.<ul>
<li><code>Member.team</code> : 외래 키(TEAM_ID)를 관리하는 연관관계의 주인이다.</li>
<li><code>Team.members</code> : <code>mappedBy</code>로 매핑된 주인이 아닌 쪽이다. 조회와 객체 그래프 탐색에만 사용한다.</li>
</ul>
</li>
<li>양방향 연관관계는 항상 서로를 참조해야 한다.</li>
</ul>
<h2 id="2-일대다">2. 일대다</h2>
<p>일대다 관계는 다대일 관계의 반대 반향이다.  일대다 관계는 엔티티를 하나 이상 참조할 수 있어서 자바 컬렉션인 Collection, List, Set, Map 중에 하나를 사용해야 한다.</p>
<h3 id="21-일대다-단방향-1n">2.1 일대다 단방향 [1:N]</h3>
<p>하나의 팀은 여러 회원을 참조할 수 있는데 이런 관계를 일대다 관계라 한다. 팀은 회원들을 참조하지만 회원은 팀을 참조하지 않으면 둘의 관계는 <strong><em>일대다 단방향</em></strong> 관계다.</p>
<pre><code class="language-java">// 팀 엔티티

@OneToMany
// 일대다 단방향 관계를 매핑할 때는 @JoinColumn을 명시해야 한다.
@JoinColumn(name=&quot;TEAM_ID&quot;) //  MEMBER 테이블의 TEAM_ID (FK)
private List&lt;Member&gt; members = new ArrayList&lt;Member&gt;()&#39;</code></pre>
<pre><code class="language-java">// 회원
@Id @GeneratedValue
@Column(name=&quot;MEMBER_ID&quot;)
private Long id;

private String username;</code></pre>
<ul>
<li>일대다 단방향 매핑의 단점: 매핑한 객체가 관리하는 외래 키가 다른 테이블에 있다. Team에서 members를 추가하면 Member 테이블의 <code>team_id</code>를 변경해야 해서 INSERT 이후에 추가 UPDATE 쿼리가 발생한다 → 비효율적</li>
</ul>
<blockquote>
<p><strong>일대다 단방향 매핑보다는 다대일 양방향 매핑을 사용하자</strong></p>
</blockquote>
<h3 id="22-일대다-양방향-1n-n1">2.2 일대다 양방향 [1:N, N:1]</h3>
<p>일대다 양방향 매핑은 일반적으로 존재하지 않는다. 양방향 매핑에서 연관관계의 주인은 항상 외래 키를 가진 쪽이고, <code>@OneToMany</code>는 주인이 될 수 없고 <code>@ManyToOne</code>에는 <code>mappedBy</code> 속성이 없다.</p>
<p>다만 아래와 같이 일대다를 주인처럼 사용하는 방식으로 “형식적인 양방향” 구현은 가능하다.</p>
<pre><code class="language-java">// 팀 엔티티

@OneToMany
@JoinColumn(name=&quot;TEAM_ID&quot;)
private List&lt;Member&gt; members = new ArrayList&lt;Member&gt;()&#39;</code></pre>
<pre><code class="language-java">// 회원 엔티티

@ManyToOne
@JoinColumn(name=&quot;TEAM_ID&quot;, insertable=false, updatable=false)
private Team team;</code></pre>
<ul>
<li>단점:<ul>
<li>Member 쪽은 읽기 전용이 되고, 연관관계 관리가 Team에만 집중되어 구조가 비정상적이다.</li>
<li>불필요한 UPDATE 쿼리가 발생해 성능상 비효율이 생긴다.</li>
</ul>
</li>
</ul>
<blockquote>
<p><strong>될 수 있으면 다대일 양방향 매핑을 사용하자!</strong></p>
</blockquote>
<h2 id="3-일대일-11">3. 일대일 [1:1]</h2>
<p>일대일 관계는 양쪽이 서로 하나의 관계만 가지는 구조다. (ex: 회원은 하나의 사물함만 사용하고, 사물함도 하나의 회원에 의해서만 사용된다.)</p>
<ul>
<li>일대일 관계는 그 반대 방향에서도 동일하게 일대일 관계가 된다.</li>
<li>외래 키는 주 테이블이나 대상 테이블 둘 중 어느 곳에나 둘 수 있다.<ul>
<li>주 테이블 외래키: 주 테이블에 외래 키를 두고 대상 테이블을 참조한다. 일반적으로 객체지향 설계와 유사해 관리가 편리하다.</li>
<li>대상 테이블 외래키: 대상 테이블에 외래 키를 두는 방식으로, 데이터베이스 설계 중심으로 볼 때 선택할 수 있다.</li>
</ul>
</li>
</ul>
<h3 id="31-주-테이블에-외래-키">3.1 주 테이블에 외래 키</h3>
<p>객체지향 개발자들은 주 테이블에 외래 키가 있는 것을 선호한다. JPA도 주 테이블에 외래 키가 있으면 더 직관적이고 편리하게 매핑할 수 있다.</p>
<h4 id="단방향">단방향</h4>
<pre><code class="language-java">// Member
@OneToOne
@JoinColumn(name=&quot;LOCKER_ID&quot;)
private Locker locker;

// Locker
@Id @GeneratedValue
@Column(name=&quot;LOCKER_ID&quot;)
private Long id;</code></pre>
<p>객체 매핑에는 <code>@OneToOne</code>을 사용하고, <code>@JoinColumn</code>으로 외래 키를 지정한다. 이 경우 Member 테이블에 <code>LOCKER_ID</code>가 생성되어 Locker를 참조한다.</p>
<p>이 구조는 다대일 단방향과 거의 동일하게 동작하고 관계의 최대 개수가 1:1이라는 점만 다르다.</p>
<h4 id="양방향">양방향</h4>
<pre><code class="language-java">// Member
@OneToOne
@JoinColumn(name=&quot;LOCKER_ID&quot;)
private Locker locker;

// Locker
@OneToOne(mappedBy=&quot;locker&quot;)
private Member member;
</code></pre>
<p>MEMBER 테이블이 외래 키를 가지고 있어서 Member 엔티티에 있는 Member.locker가 연관관계의 주인이다.</p>
<p>반대 매핑인 사물한의 Locker.member는 mappedBy를 선언해서 연관관계의 주인이 아니라고 설정했다.</p>
<h3 id="32-대상-테이블에-외래-키">3.2 대상 테이블에 외래 키</h3>
<h4 id="단방향-1">단방향</h4>
<p>일대일 관계 중 대상 테이블에 외래 키가 있는 단방향 관계는 JPA에서 지원하지 않는다.</p>
<p>JPA2.0부터 일대다 단방향 관계에서 대상 테이블에 외래 키가 있는 매핑을 허용했다. 하지만 일대일 단방향은 이런 매핑을 허용하지 않는다.</p>
<h4 id="양방향-1">양방향</h4>
<pre><code class="language-java">// Member
@OneToOne(mappedBy=&quot;member&quot;)
private Locker locker;

// Locker
@OneToOne
@JoinColumn(name=&quot;MEMBER_ID&quot;)
private Member member;</code></pre>
<p>이 경우 대상 엔티티인 Locker가 연관관계의 주인이 되며, <code>MEMBER_ID</code> 외래 키를 통해 관계를 관리한다. Member는 <code>mappedBy</code>를 사용해 읽기 전용으로 관계를 조회만 한다.</p>
<h2 id="4-다대다-nn">4. 다대다 [N:N]</h2>
<ul>
<li>관계형 데이터베이스는 정규화된 테이블 2개로 다대다 관계를 표현할 수 없다. 그래서 보통 다대다 관계를 일대다, 다대일 관계로 풀어내는 연결 테이블을 사용한다.</li>
<li>객체는 테이블과 다르게 객체 2개로 다대다 관계를 만들 수 있다. ex: 회원 객체는 컬렉션을 사용해서 상품들을 참조하면 되고 반대로 상품들도 컬렉션을 사용해서 회원들을 참조하면 된다.</li>
</ul>
<h3 id="41-다대다-단방향">4.1 다대다: 단방향</h3>
<pre><code class="language-java">// Member
@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>
<pre><code class="language-java">// Product
@Id @Column(name=&quot;PRODUCT_ID&quot;)
private String id;</code></pre>
<p>회원 엔티티와 상품 엔티티를 <code>@ManyToMany</code> 로 매핑했다. 중요한 포인트: <code>@ManyToMany</code> 와 <code>@JoinTable</code> 을 사용해서 연결 테이블을 바로 매핑했다.</p>
<p>속성을 정리하면:</p>
<ul>
<li><code>@JoinTable.name</code> : 연결 테이블 지정</li>
<li><code>@JoinTable.joinColumns</code> : 현재 방향인 회원과 매핑할 조인 컬럼 정보 지정</li>
<li><code>@JoinTable.inverseJoinColumns</code> : 반대 방향인 상품과 매핑할 조인 컬럼 정보를 지정</li>
</ul>
<h3 id="42-다대다-양방향">4.2 다대다: 양방향</h3>
<p>다대다 매핑이므로 역방향도 <code>@ManyToMany</code> 를 사용한다. 양쪽 중 원하는 곳에 mappedBy로 연관관계의 주인을 지정한다.</p>
<pre><code class="language-java">// 역방향 추가
// Product

@ManyToMany(mappedBy=&quot;products&quot;) 
private List&lt;Member&gt; members;</code></pre>
<h3 id="43-다대다-매핑의-한계와-극복-연결-엔티티-사용">4.3 다대다: 매핑의 한계와 극복, 연결 엔티티 사용</h3>
<p><code>@ManyToMany</code></p>
<ul>
<li>장점: 연결 테이블을 자동으로 처리해주므로 도메인 모델이 단순해지고 여러 가지로 편리하다.</li>
<li>한계: 예를 들어 회원이 상품을 주문하면 단순히 회원 ID와 상품 ID만 저장하는 것이 아니라 주문 수량, 주문 날짜와 같은 추가 정보가 필요하다. 하지만 <code>@ManyToMany</code>에서는 연결 테이블이 엔티티로 존재하지 않기 때문에 이러한 컬럼을 매핑할 수 없다.</li>
</ul>
<p>→ 이를 해결하기 위해 연결 테이블을 별도의 엔티티로 만들어 다대다 관계를 일대다, 다대일 관계로 풀어야 한다.</p>
<pre><code class="language-java">// Member
@Id @Column(name=&quot;MEMBER_ID&quot;)
private String id;

@OneToMany(mappedBy = &quot;member&quot;)
private List&lt;MemberProduct&gt; memberProducts;</code></pre>
<pre><code class="language-java">// Product
@Id @Column(name=&quot;PRODUCT_ID&quot;)
private String id;

private String name;</code></pre>
<pre><code class="language-java">// MemberProduct (회원상품 엔티티)

@Id
@ManyToOne
@JoinColumn(name=&quot;MEMBER_ID&quot;)
private Member member; // MemberProductId.member와 연결

@Id
@ManyToOne
@JoinColumn(name=&quot;PRODUCT_ID&quot;)
private Product product; // MemberProductId.product와 연결</code></pre>
<p>위 예제에서 <code>MemberProduct</code> 엔티티는 <code>member</code>와 <code>product</code>를 함께 기본 키로 사용하는 복합 키 구조를 가진다. 즉, <code>MEMBER_ID</code>와 <code>PRODUCT_ID</code>를 조합해 하나의 식별자로 사용하며, 같은 회원과 상품의 조합은 하나만 존재할 수 있다.</p>
<p>회원상품은 회원과 상품의 기본 키를 받아서 자신의 기본 키로 사용한다. 이처럼 부모 엔티티의 기본 키를 자식 엔티티의 기본 키로 함께 사용하는 관계를 식별관계(Identifying Relationship) 라고 한다. </p>
<p>→ 복합 키를 사용하는 방법이 복잡하다.</p>
<h3 id="44-다대다-새로운-기본키-사용">4.4 다대다: 새로운 기본키 사용</h3>
<p>추천하는 방식은 데이터베이스에서 자동으로 생성되는 대리 키(Long 타입)를 기본 키로 사용하는 것이다. 기존의 복합 키 대신 <code>ORDER_ID</code>와 같은 단일 기본 키를 두고, <code>MEMBER_ID</code>, <code>PRODUCT_ID</code>는 외래 키로만 사용한다.</p>
<pre><code class="language-java">// Order
@Id @GeneratedValue
@Column(name=&quot;ORDER_ID&quot;)
private Long id;

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

@ManyToOne
@JoinColumn(name=&quot;PRODUCT_ID&quot;)
private Product product;</code></pre>
<pre><code class="language-java">// Member
@OneToMany(mappedBy=&quot;member&quot;)
private List&lt;Order&gt; orders = new ArrayList&lt;Order&gt;();

// Product
@Id @Column(name=&quot;PRODUCT_ID&quot;)
private String id;
private String name;</code></pre>
<p>이 방식은 엔티티가 독립적인 식별자를 가지게 되어 식별관계가 아닌 비식별관계로 구성된다. 그 결과 매핑이 단순해지고, 코드 작성과 유지보수가 훨씬 수월해진다.</p>
<blockquote>
<p><strong>다대다 연관관계 정리</strong></p>
</blockquote>
<p>다대다 관계를 일대다 다대일 관계로 풀어내기 위해 연결 테이블을 만들 때 식별자를 어떻게 구성할지 선택해야 한다.</p>
<ul>
<li><strong>식별 관계:</strong> 받아온 식별자를 기본 키 + 외래 키로 사용한다.</li>
<li><strong>비식별 관계:</strong> 받아온 식별자는 외래 키로만 사용하고 새로운 식별자를 추가한다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JPA5] 연관관계 매핑 기초]]></title>
            <link>https://velog.io/@ttt-1-2/5-%EC%97%B0%EA%B4%80%EA%B4%80%EA%B3%84-%EB%A7%A4%ED%95%91-%EA%B8%B0%EC%B4%88</link>
            <guid>https://velog.io/@ttt-1-2/5-%EC%97%B0%EA%B4%80%EA%B4%80%EA%B3%84-%EB%A7%A4%ED%95%91-%EA%B8%B0%EC%B4%88</guid>
            <pubDate>Sun, 05 Apr 2026 06:06:18 GMT</pubDate>
            <description><![CDATA[<p><em>교재: 자바 ORM 표준 JPA 프로그래밍</em></p>
<p>5장의 목표는 객체의 참조와 테이블의 외래 키를 매핑하는 것이다.</p>
<p>5장의 핵심 키워드:</p>
<ul>
<li>방향 (direction): 단방향, 양방향이 있다. 방향은 객체관계에만 존재하고 테으블 관계는 항상 양방향이다.</li>
<li>다중성 (multiplicity): 다대일(N:1), 일대다(1:N), 일대일(1:1), 다대다(N:M) 다중성이 있다.</li>
<li>연관관계 주인 (owner): 객체를 양방향 연관관계로 만들면 연관관계의 주인을 정해야 한다.</li>
</ul>
<hr>
<h2 id="1-단방향-연관관계">1. 단방향 연관관계</h2>
<blockquote>
<p><strong>ex: 회원과 팀의 관계를 알아보자</strong></p>
</blockquote>
<ul>
<li>회원과 팀이 잇다</li>
<li>회원은 하나의 팀에만 소속될 수 있다</li>
<li>회원과 팀은 다대일 관계다</li>
</ul>
<blockquote>
<p><strong>객체 연관관계</strong></p>
</blockquote>
<ul>
<li>회원 객체는 Member.team 필드(멤버 볌수)로 팀 객체와 연관관계를 맺는다</li>
<li>회원 객체와 팀 객체: 단방향 관계다.<ul>
<li>회원: Member.team 필드를 통해 팀을 알 수 있다 (member.getTeam()으로 조회)</li>
<li>팀: 회원을 알 수 없다 (team → member 접근 필드 없음)</li>
</ul>
</li>
</ul>
<blockquote>
<p><strong>테이블 연관관계</strong></p>
</blockquote>
<ul>
<li>회원 테이블은: TEAM_ID 외래 키로 팀 테이블과 연관관계를 맺는다.</li>
<li>회원 테이블과 팀 테이블: 양방향 관계.<ul>
<li>회원: TEAM_ID 외래 키를 통해서 회원과 팀을 조인할 수 있다</li>
<li>팀: 회원도 조인할 수 있다</li>
</ul>
</li>
</ul>
<pre><code class="language-sql">-- 회원과 팀을 조인하는 SQL
SELECT *
FROM MEMBER M
JOIN TEAM T ON M.TEAM_ID = T.ID

-- 팀과 회원을 조인하는 SQL
SELECT *
FROM TEAM T
JOIN MEMBER M ON T.ID = M.TEAM_ID</code></pre>
<h3 id="11-순수한-객체-연관관계">1.1 순수한 객체 연관관계</h3>
<ul>
<li>객체는 참조를 사용해서 연관관계를 탐색하는 것이 객체 그래프 탐색이라 한다.</li>
</ul>
<pre><code class="language-java">member1.setTeam(team1);
member2.setTeam(team1);

Team findTeam = member1.getTeam();</code></pre>
<h3 id="12-테이블-연관관계">1.2 테이블 연관관계</h3>
<p>데이터베이스는 외래 키를 사용해서 연관관계를 탐색하는 것이 조인이라 한다.</p>
<pre><code class="language-java">SELECT T.*
FROM MEMBER M
    JOIN TEAM T ON M.TEAM_ID = T.ID
WHERE M.MEMBER_ID = &#39;member1&#39;</code></pre>
<h3 id="13-객체-관계-매핑">1.3 객체 관계 매핑</h3>
<p>ex: 회원 엔티티 매핑</p>
<pre><code class="language-java">@ManyToOne
@JoinColumn(name=&quot;TEAM_ID&quot;)
private Team team;</code></pre>
<ul>
<li>@ManyToOne: 다대일(N:1) 관계. 매핑할 때 이렇게 다중성을 나타내는 어노테이션은 필수다.</li>
<li>@JoinColumn(name=”TEAM_ID”): 조인 컬럼은 외래 키를 매핑할 때 사용한다. name 속성: 매핑할 외래 키 이름. 이 어노테이션은 생략할 수 있다</li>
</ul>
<h3 id="14-joincolumn">1.4 @JoinColumn</h3>
<p>속성:</p>
<ul>
<li>name: 매핑할 외래 키 이름</li>
<li>referencedColumnName: 외래 키가 참조하는 대상 테이블의 컬럼명</li>
<li>foreignKey(DDL): 외래 키 제약조건을 직접 지정. 테이블 생성할 때만 사용</li>
<li>unique, nullable, insertable, updatable, columnDefinition, table: @Column의 속성과 같다</li>
</ul>
<h3 id="15-manytoone">1.5 @ManyToOne</h3>
<p>속성:</p>
<ul>
<li>optional: 연관관계가 필수인지(null 허용 여부)를 설정한다</li>
<li>fetch: 연관 엔티티의 조회 방식을 설정한다 (LAZY 또는 EAGER)</li>
<li>cascade: 연관 엔티티에 대한 영속성 전이 범위를 설정한다</li>
<li>targetEntity: 연관될 엔티티 타입을 지정한다</li>
</ul>
<h2 id="2-연관관계-사용">2. 연관관계 사용</h2>
<h3 id="21-저장">2.1 저장</h3>
<pre><code class="language-java">public void testSave() {

    //팀1 저장
    Team team1 = new Team(&quot;team1&quot;, &quot;팀1&quot;);
    em.persist(team1);

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

    //회원2 저장
    Member member2 = new Member(&quot;member2&quot;, &quot;회원2&quot;);
    member2.setTeam(team1); // 연관관계 설정: member2 -&gt; team1
    em.persist(member2);
}</code></pre>
<h3 id="22-조회">2.2 조회</h3>
<ul>
<li><p>연관관계가 있는 엔티티를 조회하는 방법은 크게 2가지다.</p>
<ul>
<li><p>객체 그래프 탐색</p>
<pre><code class="language-java">Team team = member.getTeam();</code></pre>
</li>
<li><p>객체지향 쿼리 사용 JPQL</p>
<pre><code class="language-java">List&lt;Member&gt; resultList = em.createQuery(jpql, Member.class)
      .setParameter(&quot;teamName&quot;, &quot;팀1&quot;);
      .getResultList();</code></pre>
</li>
</ul>
</li>
</ul>
<h3 id="23-수정">2.3 수정</h3>
<pre><code class="language-java">//회원1에 새로운 팀2 설정
Member member = em.find(Member.class, &quot;member1&quot;);
member.setTeam(team2);</code></pre>
<h3 id="24-연관관계-제거">2.4 연관관계 제거</h3>
<pre><code class="language-java">member1.setTeam(null); //연관관계 제거</code></pre>
<h3 id="25-연관된-엔티티-삭제">2.5 연관된 엔티티 삭제</h3>
<pre><code class="language-java">member1.setTeam(null); //회원1 연관관계 제거
member2.setTeam(null); //회원2 연관관계 제거
em.remove(team); //팀 삭제</code></pre>
<h2 id="3-양방향-연관관계">3. 양방향 연관관계</h2>
<p>회원 → 팀 접근하고 반대 방향 팀 → 회원 접근할 수 있는 양방향 연관관계로 매핑해 볼 것이다.</p>
<h3 id="31-양방향-연관관계-매핑">3.1 양방향 연관관계 매핑</h3>
<p>매핑한 팀 엔티티:</p>
<pre><code class="language-java">@Entity
public class Team {

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

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

}</code></pre>
<ul>
<li><code>mappedBy</code>: 양방향 매핑일 때 사용, 반대 쪽 매핑의 필드 이름을 값으로 주면된다.</li>
</ul>
<h3 id="32-일대다-컬렉션-조회">3.2 일대다 컬렉션 조회</h3>
<pre><code class="language-java">List&lt;Member&gt; members = team.getMembers(); // (팀 -&gt; 회원), 객체 그래프 탐색</code></pre>
<h2 id="4-연관관계의-주인">4. 연관관계의 주인</h2>
<p>복습:</p>
<ul>
<li>테이블: 외래 키 하나로 두 테이블의 연관관계를 관리한다</li>
<li>엔티티: 단방향으로 매핑하면 참조를 하나만 사용</li>
</ul>
<p>→ 엔티티를 양방향 관계로 설정하면 객체의 참조는 둘인데 외래 키는 하나다.</p>
<blockquote>
<p><strong>연관관계의 주인</strong></p>
</blockquote>
<ul>
<li>두 연관관계 중 하나를 연관관계의 주인으로 정해야한다.<ul>
<li>연관관계의 주인만이 데이터베이스 연관관계와 매핑되고 외래 키를 관리(등록, 수정, 삭제)할 수 있다</li>
<li>주인이 아닌 쪽: 읽기만 할 수 있다</li>
</ul>
</li>
</ul>
<blockquote>
<p><strong>연관관계의 주인은 외래 키가 있는 곳</strong></p>
</blockquote>
<p>연관관계의 주인은 외래 키(FK)를 가진 쪽이고 Team–Member 관계에서는 <code>TEAM_ID</code>를 가진 MEMBER가 주인이 된다.</p>
<h2 id="5-양방향-연관관계-저장">5. 양방향 연관관계 저장</h2>
<p>양방향 연관관계는 연관관계의 주인이 외래 키를 관리한다. 주인이 아닌 방향은 값을 설정하지 않아도 데이터베이스에 외래 키 값이 정상 입력된다.</p>
<pre><code class="language-java">team1.getMembers().add(member1); // 무시
team1.getMembers().add(member2); // 무시

member1.setTeam(team1); // 연관관계 설정 
member2.setTeam(team1); // 연관관계 설정</code></pre>
<h2 id="6-양방향-연관관계의-주의점">6. 양방향 연관관계의 주의점</h2>
<p>외래 키 값이 저장되지 않는다면 연관관계의 주인이 아닌 쪽에서 값을 설정했는지 먼저 확인해야 한다.</p>
<h3 id="61-순수한-객체까지-고려한-양방향-연관관계">6.1 순수한 객체까지 고려한 양방향 연관관계</h3>
<p>객체 관점에서 양쪽 방향에 모두 값을 입력해주는 것이 가장 안전하다.</p>
<pre><code class="language-java">member1.setTeam(team1); // 회원 -&gt; 팀
team1.getMembers().add(member1); // 팀 -&gt; 회원</code></pre>
<h3 id="62-연관관계-편의-메소드">6.2 연관관계 편의 메소드</h3>
<p>양방향 연관관계는 양쪽 다 신경 써야 한다. 각각 호출하다 보면 실수로 둘 중 하나만 호출해서 양방향이 깨질 수 있다. 편의 메소드 사용 방법:</p>
<pre><code class="language-java">  private Team team;

  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[[JPA4] 엔티티 매핑]]></title>
            <link>https://velog.io/@ttt-1-2/4-%EC%97%94%ED%8B%B0%ED%8B%B0-%EB%A7%A4%ED%95%91</link>
            <guid>https://velog.io/@ttt-1-2/4-%EC%97%94%ED%8B%B0%ED%8B%B0-%EB%A7%A4%ED%95%91</guid>
            <pubDate>Sun, 05 Apr 2026 04:21:50 GMT</pubDate>
            <description><![CDATA[<p><em>교재: 자바 ORM 표준 JPA 프로그래밍</em> </p>
<p>JPA는 다양한 어노테이션을 지원하는데 크게 4가지로 나눌 수 있다:</p>
<ul>
<li>객체와 테이블 매핑: @Entity, @Table</li>
<li>기본 키 매핑: @Id</li>
<li>필드와 컬럼 매핑: @Column</li>
<li>연관관꼐 패핑: @ManyToOne, @JoinColumn</li>
</ul>
<p>4장에서 객체와 테이블 매핑, 기본 키 매핑, 필드와 컬럼 매핑을 다룰 것이다.</p>
<hr>
<h2 id="1-entity">1. @Entity</h2>
<p>JPA를 사용해서 테이블과 매핑할 클래스는 <code>@Entity</code> 필수로 붙여야 한다.</p>
<ul>
<li>속성: name: 사용할 엔티티 이름 (기본값: 클래스 이름)</li>
<li>주의사항:<ul>
<li>기본 생성자는 필수!</li>
<li>final 클래스, enum, interface, inner 클래스 사용할 수 없다</li>
<li>저장할 필드에 final을 사용하면 안 된다.</li>
</ul>
</li>
</ul>
<pre><code class="language-java">public Member() {} // 직접 만든 기본 생성자

// 임의 생성자
public Member(String name) {
    this.name = name;
}</code></pre>
<h2 id="2-table">2. @Table</h2>
<p>@Table은 엔티티와 매핑할 테이블을 지정한다. </p>
<ul>
<li>속성:<ul>
<li>name: 매핑할 테이블 이름</li>
<li>catalog: catalog 기능이 있는 데이터베이스에서 catalog를 매핑한다</li>
<li>schema: schema 기능이 있는 데이터베이스에서 schema를 매핑한다</li>
<li>uniqueConstraints: DDL 생성시에 유니크 제약 조건을 만든다</li>
</ul>
</li>
</ul>
<h2 id="3-다양한-매핑-사용">3. 다양한 매핑 사용</h2>
<p>요구사항:</p>
<ul>
<li>회원: 일반회원, 관리자로 구분한다</li>
<li>회원 가입일 + 수정일이 있어야 한다</li>
<li>회원 설명 필드 있어야 한다. 이 필드는 길이 제한이 없다</li>
</ul>
<pre><code class="language-java">package jpabook.start;

import javax.persistence.*;
import java.util.Date;

@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;

    //Getter, Setter
    ...
}</code></pre>
<pre><code class="language-java">package jpabook.start;

public enum RoleType {
    ADMIN, USER
}</code></pre>
<h2 id="4-데이터베이스-스키마-사용">4. 데이터베이스 스키마 사용</h2>
<p>JPA는 데이터베이스 스키마를 자동으로 생성하는 기능을 지원한다. 이 기능은 <code>persistence.xml</code> 또는 <code>application.yml</code>에서 설정할 수 있다.</p>
<pre><code class="language-java">&lt;property name=&quot;hibernate.hbm2ddl.auto&quot; value=&quot;create&quot; /&gt;
&lt;property namee=&quot;hibernate.show_sql&quot; value=&quot;true&quot; /&gt;</code></pre>
<ul>
<li>[hibernate.hbm2ddl.auto] 속성:<ul>
<li>create: 기존 테이블을 삭제하고 새로 생성한다</li>
<li>create-drop: 실행 시 생성하고 종료 시 삭제한다</li>
<li>update: 엔티티 변경 사항만 DB에 반영한다</li>
<li>validate: 엔티티와 DB 구조를 비교만 하고 수정하지 않는다</li>
<li>none: 아무 작업도 하지 않는다</li>
</ul>
</li>
</ul>
<h2 id="5-ddl-생성-기능">5. DDL 생성 기능</h2>
<ul>
<li>추가 제약조건이 있다: 회원 이름은 필수로 입력되어야 하고, 10자를 초과하면 안 된다.</li>
</ul>
<pre><code class="language-java">@Column(name=&quot;NAME&quot;, nullable=false, lengeth=10) // 추가 제약조건</code></pre>
<blockquote>
<p>@Column 매핑 정보</p>
</blockquote>
<ul>
<li><p><code>nullable = false</code>
→ DDL에서 <code>NOT NULL</code> 제약 조건을 설정한다.</p>
</li>
<li><p><code>length</code>
→ DDL에서 문자 길이(컬럼 크기)를 설정한다.</p>
</li>
<li><p>추가 제약조건: 유니크</p>
</li>
</ul>
<pre><code class="language-java">@Table(name=&quot;MEMBER&quot;, uniqueConstraints = {@UniqueConstraint( //추가 //**
        name = &quot;NAME_AGE_UNIQUE&quot;,
        columnNames = {&quot;NAME&quot;, &quot;AGE&quot;} )})</code></pre>
<blockquote>
<p>엔티티의 <code>@Column</code> 속성들은 주로 DDL 자동 생성 시에만 사용되고 애플리케이션 실행 로직에는 영향을 주지 않는다. <strong>하지만 엔티티만 보고도 제약조건을 쉽게 파악할 수 있어 코드 가독성과 유지보수에 도움이 된다.</strong></p>
</blockquote>
<h2 id="6-기본-키-매핑">6. 기본 키 매핑</h2>
<p>JPA가 제공하는 데이터베이스 기본 키 생성 전략은 다음과 같다</p>
<ul>
<li>직접 할당: 애플리케이션에서 직접 기본 키 값을 설정한다</li>
<li>자동 생성:<ul>
<li>IDENTITY: DB의 auto increment 기능을 사용해 기본 키를 생성한다</li>
<li>SEQUENCE: DB의 시퀀스를 사용해 기본 키를 생성한다</li>
<li>TABLE: 별도의 키 생성 테이블을 사용해 기본 키를 관리한다</li>
</ul>
</li>
</ul>
<h3 id="61-기본-키-직접-할당전략">6.1 기본 키 직접 할당전략</h3>
<p>기본 키를 직접 할당하려면 다음 코드와 같이 <code>@Id</code> 로 매핑하면 된다.</p>
<pre><code class="language-java">@Id
@Column(name=&quot;id&quot;)
private String id;

Board board = new Board();
board.setID(&quot;id&quot;) // 기본 키 직접할당
em.persist(board);</code></pre>
<h3 id="62-identity-전략">6.2 IDENTITY 전략</h3>
<p>IDENTITY는 기본키 생성을 데이터베이스에 위임하는 전략이다.</p>
<p>IDENTITY 전략을 사용하려면 @GeneratedValue의 strategy 속성값을 GenerationType.IDENTITY로 지정하면 된다. 이전략을 사용하면 JPA는 기본 키 값을 얻어오기 위해 데이터베이스를 추가로 조회한다. </p>
<pre><code class="language-java">@Entity
public class Board {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

}</code></pre>
<pre><code class="language-java">private static void logic(EntityManager em) {
    Board board = new Board();
    em.persist(board);
    System.out.println(&quot;board.id = &quot; + board.getId);
}
// 출력: board.id=1</code></pre>
<h3 id="63-sequence-전략">6.3 SEQUENCE 전략</h3>
<p>시퀀스: 유일한 값을 순서대로 생성하는 특별한 데이터베이스 오브젝트다. SEQUENCE 전략은 이 시퀀스를 사용해서 기본 키를 생성한다.</p>
<ul>
<li>시퀀스 DDL:</li>
</ul>
<pre><code class="language-sql">CREATE TABLE BOARD {
    ID INT NOT NULL PRIMARY KEY,
    DATA VARCHAR(255)
 }

 CREATE SEQUENCE BOARD_SEQ START WITH 1 INCREMENT BY 1;</code></pre>
<ul>
<li>시퀀스 매핑 코드:</li>
</ul>
<pre><code class="language-sql">@Entity
@SequenceGenerator(
    name = &quot;BOARD_SEQ_GENERATOR&quot;,
    sequenceName = &quot;BOARD_SEQ&quot;, //매핑할 db 시퀀스 이름
    initialValue = 1, allocationSize = 1)
public class Board {

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE,
                        generator = &quot;BOARD_SEQ_GENERATOR&quot;)
    private Long id;

    ..
}</code></pre>
<blockquote>
<p><strong>@SequenceGenerator: 는 시퀀스 생성 방식을 정의하는 설정이고, 실제로는 @GeneratedValue에서 generator 이름을 통해 사용된다. 즉, @SequenceGenerator는 정의, @GeneratedValue는 실행 역할을 한다.</strong></p>
</blockquote>
<ul>
<li>name: 시퀀스 생성기를 식별하기 위한 이름이다</li>
<li>sequenceName: 매핑할 데이터베이스 시퀀스 이름이다</li>
<li>initialValue: 시퀀스의 시작 값을 설정한다</li>
<li>allocationSize: 한 번에 가져올 시퀀스 값의 증가 크기를 설정하여 성능을 최적화한다</li>
<li>catalog, schema: 시퀀스가 속한 데이터베이스의 catalog와 schema를 지정한다</li>
</ul>
<h3 id="64--table-전략">6.4  TABLE 전략</h3>
<p>TABLE 전략: 키 생성 전용 테이블을 하나 만들고 여기에 이름과 값으로 사용할 컬럼을 만들어 데이터베이스 시퀀스를 흉낸다.</p>
<ul>
<li>TABLE 전략 키 생성 DDL</li>
</ul>
<pre><code class="language-sql">CREATE table MY_SEQUENCES {
    sequence_name varchar(255) not null,
    next_val bigint,
    primary key (sequence_name)
 }</code></pre>
<ul>
<li>TABLE 전략 매핑 코드</li>
</ul>
<pre><code class="language-sql">@Entity
@TableGenerator(
    name = &quot;BOARD_SEQ_GENERATOR&quot;,
    table = &quot;MY_SEQUENCES&quot;, 
    pkColumnValue = &quot;BOARD_SEQ&quot;, allocationSize = 1)
public class Board {

    @Id
    @GeneratedValue(strategy = GenerationType.TABLE,
                        generator = &quot;BOARD_SEQ_GENERATOR&quot;)
    private Long id;

    ..
}</code></pre>
<h3 id="65-auto-전략">6.5 AUTO 전략</h3>
<p><a href="http://GenerationType.AUTO"><code>GenerationType.AUTO</code></a> 는 선택한 데이터베이스 방언에 따라 IDENTITY, SEQUENCE, TABL 전략 중 하나를 자동으로 선택한다.</p>
<p>ex: 오라클을 선택하면 SEQUENCE, MySQL을 선택하면 IDENTITY를 사용한다.</p>
<h3 id="66-기본-키-매핑-정리">6.6 기본 키 매핑 정리</h3>
<p><code>em.persist()</code> 를 호출한 직후에 발생하는 일을 식별자 할당 전략별로 정리하면 다음과 같다.</p>
<ul>
<li>직적 할당: persist 전에 애플리케이션에서 직접 id 값을 설정해야 한다</li>
<li>SEQUENCE: 시퀀스를 통해 id를 먼저 조회한 후 엔티티에 할당한다</li>
<li>TABLE: 키 생성 테이블에서 id를 조회하고 값을 증가시킨 후 할당한다</li>
<li>IDENTITY: 엔티티를 먼저 DB에 저장한 후 생성된 id 값을 조회한다</li>
</ul>
<h2 id="7-필드와-컬럼-매핑-레퍼런스">7. 필드와 컬럼 매핑: 레퍼런스</h2>
<h3 id="71-column">7.1 @Column</h3>
<p>@Column의 속성:</p>
<ul>
<li>name: 컬럼 이름을 지정한다</li>
<li>insertable: INSERT SQL에 포함할지 여부를 설정한다</li>
<li>updatable: UPDATE SQL에 포함할지 여부를 설정한다</li>
<li>table: 매핑할 테이블을 지정한다</li>
<li>nullable(DDL): NOT NULL 제약 조건을 설정한다</li>
<li>unique(DDL): UNIQUE 제약 조건을 설정한다</li>
<li>columnDefinition(DDL): 컬럼의 DDL을 직접 정의한다</li>
<li>length(DDL): 문자열 컬럼의 길이를 설정한다</li>
<li>precision, scale(DDL): 숫자의 전체 자릿수와</li>
</ul>
<h3 id="72-enumerated">7.2 @Enumerated</h3>
<p>자바의 enum 타입을 매핑할 때 사용한다</p>
<ul>
<li>EnumType.ORDINAL: enum 순서를 데이터베이스에 저장</li>
<li>EnumType.STRING: enum 이름을 데이터베이스에 저장</li>
</ul>
<h3 id="73-temporal">7.3 @Temporal</h3>
<p>날짜 타입(java.util.Date, java.util.Calendar)을 매핑할 때 사용한다.</p>
<ul>
<li>value 속성:<ul>
<li>TemporalType.DATE: 날짜(년,월,일)만 저장한다</li>
<li>TemporalType.TIME: 시간(시,분,초)만 저장한다</li>
<li>TemporalType.TIMESTAMP: 날짜와 시간을 모두 저장한다</li>
</ul>
</li>
</ul>
<h3 id="74-lob">7.4 @Lob</h3>
<p>데이터베이스 BLOB, CLOB 타입과 매핑한다.</p>
<ul>
<li>BLOB: byte[], java.sql.BLOB</li>
<li>CLOB: String, char[], java.sql.CLOB</li>
</ul>
<h3 id="75-transient">7.5 @Transient</h3>
<p>이 필드는 매핑하지 않는다. 데이터베이스에 저장하지 않고 조회하지도 않는다. 객체에 임시로 어떤 값을 보관하고 싶을 때 사용한다.</p>
<pre><code class="language-sql">@Transient
private Integer temp;</code></pre>
<h3 id="76-access">7.6 @Access</h3>
<p>JPA가 엔티티 데이터에 접근하는 방식을 지정한다:</p>
<ul>
<li>필드 접근: AccessType.FIELD로 지정한다. 필드에 직접 접근한다. 필드 접근 권한이 private이어도 접근할 수 있다.</li>
<li>프로퍼티 접근: AccessType.PROPERTY로 지정한다. 접근자 getter를 사용한다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JPA3] 영속성 관리]]></title>
            <link>https://velog.io/@ttt-1-2/3-%EC%98%81%EC%86%8D%EC%84%B1-%EA%B4%80%EB%A6%AC</link>
            <guid>https://velog.io/@ttt-1-2/3-%EC%98%81%EC%86%8D%EC%84%B1-%EA%B4%80%EB%A6%AC</guid>
            <pubDate>Sun, 22 Mar 2026 06:20:56 GMT</pubDate>
            <description><![CDATA[<p><em>교재: 자바 ORM 표준 JPA 프로그래밍</em> </p>
<hr>
<h2 id="1-엔티티-매니저-팩토리와-엔티티-매니저">1. 엔티티 매니저 팩토리와 엔티티 매니저</h2>
<p>하나의 데이터베이스만 사용하는 애플리케이션에서는 보통 EntityManagerFactory를 하나만 생성해서 사용한다.</p>
<pre><code class="language-java">// 공장 생성, 초기 비용이 매우 큼
EntityManagerFactory emf = Persistence.createEntityManagerFactory(&quot;jpabook&quot;);</code></pre>
<p><code>Persistence.createEntityManagerFactory(&quot;jpabook&quot;)</code>를 실행하면 <code>META-INF/persistence.xml</code>에 정의된 설정을 기반으로 EntityManagerFactory가 만들어진다. 이때 데이터베이스 연결 설정을 읽고 커넥션 풀을 준비하며 JPA 관련 초기화 작업이 수행되므로 생성 비용이 상당히 크다.</p>
<p>이후에는 필요할 때마다 EntityManagerFactory를 통해 엔티티 매니저를 생성해서 사용한다.</p>
<pre><code class="language-java">// 엔티티 매니저 생성, 비용이 거의 없음
EntityManager em = emf.createEntityManager();</code></pre>
<p>엔티티 매니저는 실제로 데이터베이스와 CRUD 작업을 수행하는 객체이며 생성 비용이 낮기 때문에 요청 단위로 생성하고 사용 후 종료하는 방식이 일반적이다.</p>
<p>주의: 엔티티 매니저는 여러 스레드에서 동시에 공유해서 사용하면 안 된다.</p>
<h2 id="2-영속성-컨텍스트란">2. 영속성 컨텍스트란?</h2>
<p>영속성 컨텍스트 (persistence context) = 엔티티를 영구 저장하는 환경</p>
<pre><code class="language-java">em.persist(member);</code></pre>
<h2 id="3-엔티티의-생명주기">3. 엔티티의 생명주기</h2>
<p>엔티티는 4가지 상태가 있다:</p>
<p><strong><em>(1) 비영속 (new/transient)</em></strong></p>
<p>엔티티 객체를 생성만 한 상태로, 아직 영속성 컨텍스트와 전혀 관계가 없다. 데이터베이스와 연결되지 않은 순수 객체 상태다.</p>
<pre><code class="language-java">Member member = new Member(); // 비영속 상태
member.setName(&quot;Trang&quot;);</code></pre>
<p><strong><em>(2) 영속 (managed)</em></strong></p>
<p>엔티티 매니저를 통해 영속성 컨텍스트에 저장된 상태다. 이 상태에서는 JPA가 엔티티를 관리하고 변경 사항을 자동으로 감지하여 DB에 반영한다.</p>
<pre><code class="language-java">em.persist(member); // 영속 상태로 전환</code></pre>
<p><strong><em>(3) 준영속 (detached)</em></strong></p>
<p>영속 상태였던 엔티티가 더 이상 영속성 컨텍스트에 의해 관리되지 않는 상태다. 변경을 해도 DB에 반영되지 않는다.</p>
<pre><code class="language-java">em.detach(member); // 준영속 상태</code></pre>
<p><strong><em>(4) 삭제 (removed): 삭제된 상태</em></strong></p>
<p>엔티티가 삭제 대상으로 지정된 상태다. 이후 트랜잭션을 커밋하면 실제 DB에서도 삭제된다.</p>
<pre><code class="language-java">em.remove(member); // 삭제 상태</code></pre>
<h2 id="4-영속성-컨텍스트의-특징">4. 영속성 컨텍스트의 특징</h2>
<h3 id="41-엔티티-조회">4.1 엔티티 조회</h3>
<p>JPA에서 엔티티를 조회할 때는 1차 캐시(영속성 컨텍스트)를 먼저 사용한다.</p>
<p>1차 캐시는 엔티티 매니저 내부에 존재하는 저장 공간이며 같은 트랜잭션 내에서 엔티티를 효율적으로 관리하기 위해 사용된다. 1차 캐시의 키는 식별자 값(ID)이다. 이 식별자 값은 데이터베이스의 기본 키(PK)와 매핑된다.</p>
<p>조회 과정은 다음과 같이 동작한다.</p>
<ol>
<li>먼저 1차 캐시에서 식별자 값으로 엔티티를 조회한다.</li>
<li>1차 캐시에 해당 엔티티가 없으면 데이터베이스에서 조회한다.</li>
<li>조회한 데이터를 기반으로 엔티티 객체를 생성한다.</li>
<li>생성된 엔티티를 1차 캐시에 저장한 후 반환한다.</li>
</ol>
<pre><code class="language-java">// 엔티티를 생성한 상태 (비영속)
Member member = new Member();
member.setId(&quot;member1&quot;);
member.setUsername(&quot;회원1&quot;);

// 엔티티를 영속
em.persist(member); // 1차 캐시에 저장됨

// 1차 캐시 에서 조회
Member findMember = em.find(Member.class, &quot;member1&quot;);</code></pre>
<h3 id="42-엔티티-등록">4.2 엔티티 등록</h3>
<p>엔티티 등록은 새로운 객체를 생성하고 데이터베이스에 저장하는 과정이다. JPA에서는 <code>persist()</code> 메서드를 사용해서 엔티티를 영속 상태로 만든다.</p>
<pre><code class="language-java">Member member = new Member();
member.setName(&quot;Trang&quot;);

em.persist(member); // 영속 상태</code></pre>
<p>엔티티를 <code>persist()</code> 하면 바로 SQL이 실행되는 것이 아니라, 쓰기 지연 SQL 저장소에 INSERT 쿼리가 먼저 저장된다. 이후 트랜잭션을 커밋하는 시점에 SQL이 실제 데이터베이스로 전달된다.</p>
<pre><code class="language-java">tx.commit(); // 이때 INSERT SQL 실행</code></pre>
<p>동작 흐름:</p>
<ol>
<li>엔티티 생성 (비영속 상태)</li>
<li><code>persist()</code> 호출 → 영속 상태 전환</li>
<li>INSERT SQL을 쓰기 지연 저장소에 저장</li>
<li>commit 시점에 DB에 반영</li>
</ol>
<h3 id="43-엔티티-수정">4.3 엔티티 수정</h3>
<p>엔티티 수정은 별도의 update 메서드를 호출하지 않고, 변경 감지(dirty checking)를 통해 자동으로 처리된다.</p>
<p><strong><em>변경 감지 (Dirty Checking)</em></strong></p>
<p>JPA는 엔티티의 변경을 자동으로 감지한다.</p>
<pre><code class="language-java">Member member = em.find(Member.class, 1L);
member.setName(&quot;New Name&quot;); // 값 변경</code></pre>
<p>별도의 update 호출 없이도 트랜잭션 commit 시점에 JPA가 변경을 감지해서 UPDATE SQL을 생성한다.</p>
<p>동작 방식:</p>
<ol>
<li>엔티티를 조회하면 초기 상태를 스냅샷으로 저장한다.</li>
<li>트랜잭션 종료 시점에 현재 값과 비교한다.</li>
<li>변경된 경우에만 UPDATE SQL을 실행한다.</li>
</ol>
<p><strong><em>@DynamicUpdate</em></strong></p>
<pre><code class="language-java">@org.hibernate.annotations.DynamicUpdate</code></pre>
<p>이 옵션을 사용하면 변경된 컬럼만 포함한 UPDATE SQL이 생성된다. 불필요한 컬럼 업데이트를 줄이고 성능을 조금 더 최적화할 수 있다.</p>
<h3 id="44-엔티티-삭제">4.4 엔티티 삭제</h3>
<p>엔티티를 삭제하려면 먼저 삭제 대상 엔티티를 조회해야 한다.</p>
<pre><code class="language-java">Member memberA = em.find(Member.class, &quot;memberA&quot;); // 삭제 대상 조회
em.remove(memberA); // 엔티티 삭제</code></pre>
<h2 id="5-플러시-flush">5. 플러시 (flush)</h2>
<p>flush는 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영하는 과정이다.</p>
<ul>
<li>직접 호출: 개발자가 직접 flush를 호출하고 현재까지의 변경 사항이 즉시 DB에 반영된다.</li>
</ul>
<pre><code class="language-java">em.flush();</code></pre>
<ul>
<li>트랜잭션 커밋 시 자동 호출</li>
</ul>
<pre><code class="language-java">tx.commit();</code></pre>
<p>트랜잭션을 커밋하면 flush가 자동으로 실행된 후 commit이 진행된다. 따라서 대부분의 경우 flush를 직접 호출할 필요는 없다.</p>
<ul>
<li>JPQL 쿼리 실행 시 자동 호출: JPQL을 실행하기 전에 flush가 자동으로 호출된다.</li>
</ul>
<h2 id="6-준영속">6. 준영속</h2>
<p>준영속 상태는 엔티티가 더 이상 영속성 컨텍스트에 의해 관리되지 않는 상태다. 이 상태에서는 엔티티를 수정해도 변경 사항이 DB에 반영되지 않는다.</p>
<h3 id=""></h3>
<blockquote>
<p><strong>detach()</strong></p>
</blockquote>
<pre><code class="language-java">em.detach(member);</code></pre>
<p>특정 엔티티를 준영속 상태로 전환한다. 해당 엔티티만 영속성 컨텍스트에서 분리된다.</p>
<blockquote>
<p><strong>clear()</strong></p>
</blockquote>
<pre><code class="language-java">em.clear();</code></pre>
<p>영속성 컨텍스트를 초기화한다. 관리 중이던 모든 엔티티가 한 번에 준영속 상태가 된다.</p>
<blockquote>
<p><strong>close()</strong></p>
</blockquote>
<pre><code class="language-java">em.close();</code></pre>
<p>영속성 컨텍스트를 종료한다. 이후에는 엔티티 매니저를 더 이상 사용할 수 없다.</p>
<blockquote>
<p><strong>병합</strong></p>
</blockquote>
<p>준영속 상태의 엔티티를 다시 영속 상태로 변형하려면 병합을 사용하면 된다.</p>
<pre><code class="language-java">// 정의
public &lt;T&gt; T merge(T entity);

// merge() 사용 예
Member mergeMember = em.merge(member);</code></pre>
<pre><code class="language-java">// 전체 흐름 예시
// 1. 엔티티 조회 (영속 상태)
Member member = em.find(Member.class, 1L);

// 2. 준영속 상태로 변경
em.detach(member);

// 3. 값 변경 (DB 반영 안 됨)
member.setName(&quot;New Name&quot;);

// 4. merge 호출 → 다시 영속 상태
Member mergedMember = em.merge(member);

// 5. commit 시 UPDATE SQL 실행
tx.commit();</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JPA2] JPA 시작]]></title>
            <link>https://velog.io/@ttt-1-2/2-JPA-%EC%8B%9C%EC%9E%91</link>
            <guid>https://velog.io/@ttt-1-2/2-JPA-%EC%8B%9C%EC%9E%91</guid>
            <pubDate>Sun, 15 Mar 2026 11:48:21 GMT</pubDate>
            <description><![CDATA[<p><em>교재: 자바 ORM 표준 JPA 프로그래밍</em> </p>
<p><em>깃허브:</em> <a href="https://github.com/holyeye/jpabook">https://github.com/holyeye/jpabook</a></p>
<hr>
<p>2026년 3월 기준으로 책 내용을 변경해서 실습을 진행했다: (1) 이클립스 → 인텔리제이 (2) Maven → Gradle</p>
<p>폴더 구조는 다음과 같다:
<img src="https://velog.velcdn.com/images/ttt-1-2/post/2a8a48bd-77af-4bb7-a52c-1b1b5c9e36b6/image.png" alt=""></p>
<h2 id="buildgradle">build.gradle</h2>
<ul>
<li>Gradle: 프로젝트의 의존성과 빌드 설정을 관리하는 파일이다. 어떤 라이브러리를 사용할지, 자바 버전은 무엇인지 등을 여기서 설정한다.</li>
</ul>
<pre><code class="language-java">plugins {
    id &#39;java&#39;
}

group = &#39;hellojpa&#39;
version = &#39;1.0-SNAPSHOT&#39;

repositories {
    mavenCentral()
}

dependencies {
    implementation &#39;org.hibernate:hibernate-core:5.6.15.Final&#39;
    implementation &#39;javax.persistence:javax.persistence-api:2.2&#39;
    runtimeOnly &#39;com.h2database:h2:2.2.224&#39;
}

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(17)
    }
}</code></pre>
<h2 id="persistencexml">persistence.xml</h2>
<p><code>persistence.xml</code>은 JPA 설정 파일이다. 사용할 데이터베이스, 사용할 JPA 구현체, 엔티티 클래스, JPA 동작 옵션 등은 여기에서 설정한다.</p>
<pre><code class="language-java">&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;
&lt;persistence xmlns=&quot;http://xmlns.jcp.org/xml/ns/persistence&quot;
             xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;
             xsi:schemaLocation=&quot;http://xmlns.jcp.org/xml/ns/persistence
             http://xmlns.jcp.org/xml/ns/persistence/persistence_2_2.xsd&quot;
             version=&quot;2.2&quot;&gt;

        // persistence-unit은 JPA 설정 단위이다
    &lt;persistence-unit name=&quot;jpabook&quot;&gt;
                // JPA가 관리할 엔티티 클래스를 등록한다
        &lt;class&gt;hellojpa.Member&lt;/class&gt;

        &lt;properties&gt;

            &lt;!-- database connection --&gt;
            &lt;property name=&quot;javax.persistence.jdbc.driver&quot; value=&quot;org.h2.Driver&quot;/&gt;
            &lt;property name=&quot;javax.persistence.jdbc.url&quot; value=&quot;jdbc:h2:~/test&quot;/&gt;
            &lt;property name=&quot;javax.persistence.jdbc.user&quot; value=&quot;sa&quot;/&gt;
            &lt;property name=&quot;javax.persistence.jdbc.password&quot; value=&quot;&quot;/&gt;

            &lt;!-- Hibernate --&gt;
            &lt;property name=&quot;hibernate.dialect&quot; value=&quot;org.hibernate.dialect.H2Dialect&quot;/&gt;

            &lt;!-- console SQL --&gt;
            &lt;property name=&quot;hibernate.show_sql&quot; value=&quot;true&quot;/&gt;
            &lt;property name=&quot;hibernate.format_sql&quot; value=&quot;true&quot;/&gt;

            &lt;!-- auto create table --&gt;
            &lt;property name=&quot;hibernate.hbm2ddl.auto&quot; value=&quot;create&quot;/&gt;

        &lt;/properties&gt;

    &lt;/persistence-unit&gt;

&lt;/persistence&gt;</code></pre>
<h2 id="member">Member</h2>
<p>이번 실습에서 Member 클래스는 JPA 엔티티 클래스이다. 데이터베이스 테이블과 매핑된다.</p>
<pre><code class="language-java">package hellojpa;

import javax.persistence.*;
import lombok.Getter;
import lombok.Setter;

@Entity
@Table(name = &quot;MEMBER&quot;)
@Getter
@Setter

public class Member {

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

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

    // 매핑 정보가 없는 필드
    private Integer age;
}
</code></pre>
<ul>
<li>@Entity: 이 클래스가 JPA 엔티티임을 나타낸다 → 이 객체는 데이터베이스 테이블과 매핑된다</li>
<li>@Table: 엔티티가 매핑될 테이블 이름을 지정한다</li>
<li>@Id: 기본 키를 지정한다</li>
<li>@Column: 필드와 DB 컬럼을 매핑한다</li>
<li>매핑 어노테이션이 없으면 필드 이름 그대로 컬럼이 생성된다</li>
</ul>
<p>참고: JPA 어노테이션의 패키지는 javax.persistence이다.</p>
<h2 id="jpamain">JpaMain</h2>
<ul>
<li>EntityManagerFactory: EntityManager를 생성한다. 애플리케이션 전체에서 하나만 생성한다.</li>
<li>EntityManager: JPA의 핵심 객체이다. (엔티티 저장, 엔티티 조회, 엔티티 수정, 엔티티 삭제)</li>
<li>트랜잭션: JPA의 모든 데이터 변경 작업은 트랜잭션 안에서 실행해야 한다.<pre><code class="language-java">package hellojpa;
</code></pre>
</li>
</ul>
<p>import javax.persistence.*;
import java.util.List;</p>
<p>public class JpaMain {
    public static void main(String[] args) {</p>
<pre><code>    // 엔티티 매니저 팩토리 - 생성
    EntityManagerFactory emf = Persistence.createEntityManagerFactory(&quot;jpabook&quot;);

    // 엔티티 매니저 - 생성
    EntityManager em = emf.createEntityManager();

    // 트랜잭션 - 획득
    EntityTransaction tx = em.getTransaction();

    try {
        tx.begin();
        logic(em);
        tx.commit();
    } catch (Exception e) {
        tx.rollback();
    } finally {
        em.close();
    }
    emf.close();
}

// 비지니스 로직
private static void logic(EntityManager em) {
    String id = &quot;id1&quot;;
    Member member = new Member();
    member.setId(id);
    member.setUsername(&quot;유정&quot;);
    member.setAge(2);

    // 등록
    em.persist(member);
    // 수정
    member.setAge(20);
    // 한 건 조회
    Member findMember = em.find(Member.class, id);
    System.out.println(&quot;findMember=&quot; + findMember.getUsername() + &quot;, age=&quot; + findMember.getAge());
    // 목록 조회
    List&lt;Member&gt; members = em.createQuery(&quot;select m from Member m&quot;, Member.class).getResultList();
    System.out.println(&quot;members.size=&quot; + members.size());

    // 삭제
    em.remove(member);
}</code></pre><p>}
```</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JPA1] JPA 소개]]></title>
            <link>https://velog.io/@ttt-1-2/1-JPA-%EC%86%8C%EA%B0%9C</link>
            <guid>https://velog.io/@ttt-1-2/1-JPA-%EC%86%8C%EA%B0%9C</guid>
            <pubDate>Sun, 15 Mar 2026 11:45:06 GMT</pubDate>
            <description><![CDATA[<p><em>교재: 자바 ORM 표준 JPA 프로그래밍</em> </p>
<p>JPA를 사용하면 SQL 자체보다 내가 만든 객체에 더 집중할 수 있다. 데이터베이스가 바뀌더라도(MySQL → Oracle 등) 애플리케이션 코드를 크게 바꾸지 않고 유지보수하기 쉬워진다.</p>
<p>예전에는 보통 다음과 같은 흐름으로 개발했다:</p>
<ul>
<li><p>ORM 이전: Java → JDBC → SQL → DB</p>
<p>  개발자가 직접 SQL을 작성하고, 조회 결과를 다시 자바 객체로 하나하나 매핑해야 했다.</p>
</li>
<li><p>ORM 이후: Java 객체 → ORM → SQL → DB</p>
<p>  개발자는 객체 중심으로 개발하고, ORM이 중간에서 SQL 생성과 매핑을 도와준다.</p>
</li>
</ul>
<p>즉, ORM은 <strong>객체와 관계형 데이터베이스 사이의 번역기</strong> 같은 역할을 한다. 자바는 객체를 사용하고 데이터베이스는 테이블을 사용하므로 둘 사이에는 구조 차이가 있다. ORM은 이 차이를 줄여 주어서 개발자가 조금 더 자연스럽게 객체지향적으로 개발할 수 있게 해준다.</p>
<hr>
<h2 id="1-sql을-직접-다룰-때-발생하는-문제점">1. SQL을 직접 다룰 때 발생하는 문제점</h2>
<blockquote>
<p><strong>반복</strong></p>
</blockquote>
<p>ex: 회원 정보를 조회한다고 해보자.</p>
<ol>
<li>조회용 SQL을 직접 작성한다.</li>
<li>JDBC API를 사용해서 SQL을 실행한다.</li>
<li>결과(ResultSet)를 꺼내서 자바 객체에 하나씩 넣는다.</li>
</ol>
<p>아래와 같이 <code>Member</code> 객체와 <code>MemberDAO</code>가 있다고 가정해보자.</p>
<pre><code class="language-java">public class Member {
    private String memberId;
    private String name;
}</code></pre>
<pre><code class="language-java">public class MemberDAO {
    public Member find(String memberId) {
        // DB에서 회원을 조회하는 로직이 들어간다
        return null;
    }
}</code></pre>
<p>겉으로 보면 <code>find()</code> 메서드 하나만 있으면 될 것 같지만 실제 내부에서는 꽤 많은 코드가 필요하다.</p>
<p>ex:  DB 연결 얻기, SQL 문자열 작성, PreparedStatement 생성, 파라미터 바인딩, SQL 실행, ResultSet 조회, 컬럼 값을 꺼내서 <code>Member</code> 객체에 저장, 자원 반납(Connection, Statement, ResultSet 닫기)</p>
<p>→ 비즈니스 로직보다 JDBC 처리 코드가 더 많이 보이는 경우가 많다.</p>
<blockquote>
<p><strong>SQL에 의존적인 개발</strong></p>
</blockquote>
<p>SQL을 직접 다루면 자바 코드가 데이터베이스 구조에 강하게 묶이게 된다.</p>
<p>ex: 회원 연락처를 추가할 때 겉보기에는 필드 하나 추가한 것뿐이지만 실제로는 수정할 것이 많다.</p>
<pre><code class="language-java">// 회원 클래스에 연락처 필드 추가
pulic class Member {
    private String memberId;
    private String name;
    private String tel;
}</code></pre>
<p><em>수정해야 하는 부분</em></p>
<ul>
<li>자바 객체에 필드 추가</li>
<li>DB 테이블에 컬럼 추가</li>
<li>INSERT SQL 수정</li>
<li>UPDATE SQL 수정</li>
<li>SELECT SQL 수정</li>
<li>ResultSet 매핑 코드 수정</li>
</ul>
<blockquote>
<p><strong>계층 분할이 어려워진다</strong></p>
</blockquote>
<p>객체지향적으로 설계하면 보통 다음처럼 생각한다.</p>
<ul>
<li><code>Member</code>는 회원 객체다</li>
<li><code>Team</code>은 팀 객체다</li>
<li>회원은 한 팀에 속할 수 있다</li>
</ul>
<p>자바 코드에서는 자연스럽게 이렇게 표현하고 싶어진다.</p>
<pre><code>member.getTeam();</code></pre><p>하지만 SQL 중심으로 개발하면 그렇게 단순하지 않다.</p>
<p><code>member.getTeam()</code>을 호출한다고 자동으로 팀 정보가 나오는 것이 아니다. 개발자는 먼저 DAO 안을 열어 보고 어떤 SQL이 실행되고 있는지 확인해야 한다. (회원만 조회하는 SQL인지, 팀까지 조인해서 조회하는 SQL인지, 팀은 따로 다시 조회해야 하는지..)</p>
<p>→ 객체를 사용하는 입장에서는 그냥 <code>member.getTeam()</code> 하면 될 것 같지만, 실제로는 DAO 내부 SQL 구현을 알아야만 동작을 예측할 수 있다.</p>
<blockquote>
<p><strong>JPA와 문제 해결</strong></p>
</blockquote>
<p>JPA를 사용하면 개발자가 SQL을 직접 다루기보다 엔티티 객체를 중심으로 개발하게 된다.</p>
<pre><code class="language-java">// 저장 기능
jpa.persist(member);

// 조회 기능
String memberId = &quot;helloId&quot;;
Member member = jpa.find(Member.class, memberId);

// 수정 기능
Member member = jpa.find(Member.class, memberId);
member.setName(&quot;이름변경&quot;)

// 연관된 객체 조회
Member member = jpa.find(Member.class, memberId);
Team team = member.getTeam();</code></pre>
<p>필요한 시점에 JPA가 적절한 SELECT SQL을 실행해서 팀 정보를 가져온다 → 개발자는 “이 시점에 조인 SQL을 직접 날려야 하나?”를 덜 고민하게 된다.</p>
<h2 id="2-패러다임의-불일치">2. 패러다임의 불일치</h2>
<blockquote>
<p><strong>상속</strong></p>
</blockquote>
<ul>
<li>문제점: 객체는 상속 구조를 사용할 수 있지만 데이터베이스에는 상속 개념이 없다. JDBC를 사용하면 insert 시 여러 SQL을 작성해야 하고, 조회할 때는 여러 테이블을 JOIN 해야 한다.</li>
<li>JPA 해결: JPA는 객체 상속 구조를 그대로 저장할 수 있게 해 준다.</li>
</ul>
<blockquote>
<p><strong>연관관계</strong></p>
</blockquote>
<ul>
<li>문제점: 객체는 참조로 관계를 표현하지만 데이터베이스는 외래 키로 관계를 표현한다.</li>
<li>JPA 해결: 객체 참조만 설정하면 JPA가 외래 키를 자동으로 관리한다.</li>
</ul>
<pre><code class="language-java">member.setTeam(team);
em.persist(member);</code></pre>
<blockquote>
<p><strong>객체 그래프 탐색</strong></p>
</blockquote>
<ul>
<li>문제점: JDBC에서는 객체처럼 자유롭게 탐색하기 어렵고 필요한 SQL을 직접 작성해야 한다.</li>
<li>JPA 해결: 객체처럼 자연스럽게 탐색할 수 있다.</li>
</ul>
<pre><code class="language-java">Member member = em.find(Member.class, 1L);
Team team = member.getTeam();</code></pre>
<blockquote>
<p><strong>비교</strong></p>
</blockquote>
<ul>
<li>문제점: DB는 PK 값으로 데이터를 비교하고 객체는 equals()로 객체를 비교한다.</li>
<li>JPA 해결: JPA는 같은 트랜잭션일 때 같은 객체가 조회되는 것을 보장한다.</li>
</ul>
<h2 id="3-jpa란-무엇일까">3. JPA란 무엇일까?</h2>
<ul>
<li>ORM (Object-Relational Mapping): 객체와 관계형 데이터베이스를 매핑한다.</li>
<li>JPA(Java Persistence API)는 자바 진영의 ORM 기술 표준이다. (JPA는 실제 ORM 엔진이 아니라 ORM 기술의 규격이다. 실제 동작은 Hibernate, EclipseLink 같은 구현체가 수행한다.)</li>
</ul>
<p>개발자는 JPA API를 사용하여 객체 중심으로 코드를 작성한다. 그러면 JPA 구현체가 내부적으로 SQL을 생성하여 데이터베이스와 통신한다.</p>
<pre><code class="language-java">개발자 코드
     |
     v
JPA API
(EntityManager, @Entity, @Id ...)
     |
     v
JPA 구현체 (Implementation)
------------------------------
| Hibernate                  |
| EclipseLink                |
| DataNucleus                |
------------------------------
     |
     v
SQL 생성
     |
     v
Database</code></pre>
<p>JPA를 사용하면 생산성 향상, 유지보수 용이성, 객체와 관계형 데이터베이스 사이의 패러다임 불일치 해결 그리고 데이터 접근 추상화를 통한 벤더 독립성 등의 장점을 얻을 수 있다.</p>
<p><strong><em>JPA를 사용하면 SQL 중심 개발에서 벗어나 객체 중심 개발이 가능해진다.</em></strong></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[7] MySQL 마무리]]></title>
            <link>https://velog.io/@ttt-1-2/7-MySQL-%EB%A7%88%EB%AC%B4%EB%A6%AC</link>
            <guid>https://velog.io/@ttt-1-2/7-MySQL-%EB%A7%88%EB%AC%B4%EB%A6%AC</guid>
            <pubDate>Sat, 07 Mar 2026 08:24:12 GMT</pubDate>
            <description><![CDATA[<p>강의: <a href="https://www.inflearn.com/course/database-2-mysql-%EA%B0%95%EC%A2%8C?cid=119293">https://www.inflearn.com/course/database-2-mysql-강좌?cid=119293</a></p>
<p>관계형 데이터베이스(Relational Database)를 제대로 사용하려면 단순히 데이터를 저장하는 것만으로는 부족하다. 실제로 서비스를 운영하다 보면 데이터 조회, 성능, 설계, 안정성까지 모두 고려해야 한다.</p>
<p>대표적으로 중요한 요소는 다음 네 가지다:</p>
<ul>
<li>SQL</li>
<li>Index</li>
<li>Modeling</li>
<li>Backup</li>
</ul>
<hr>
<h2 id="1-sql---데이터베이스의-기본-언어">(1) SQL - 데이터베이스의 기본 언어</h2>
<p>관계형 데이터베이스를 다루기 위해서는 SQL을 이해하고 잘 사용할 수 있어야 한다. SQL은 데이터베이스와 대화하는 언어다.</p>
<p>SQL에는 여러 종류의 명령이 있지만 그 중에서도 가장 중요한 것은 <code>SELECT</code>이다. 실제 서비스에서는 데이터를 저장하는 것보다 데이터를 조회하는 일이 훨씬 많기 때문이다. (ex: 유튜브 영상 업로드 1번에 조회수 수백만 번, 쇼핑몰 상품 등록 1번에 상품 조회 수천 번…)</p>
<p>그래서 데이터베이스에서는</p>
<blockquote>
<p>데이터를 얼마나 빠르고 정확하게 조회할 수 있는지가 매우 중요하다.</p>
</blockquote>
<h2 id="2-index---데이터를-빠르게-찾는-방법">(2) Index - 데이터를 빠르게 찾는 방법</h2>
<p>데이터가 적을 때는 문제가 없지만 데이터가 많아지면 검색 속도가 느려질 수 있다. 이때 사용하는 것이 인덱스이다.</p>
<p>ex: 책에서 특정 내용을 찾는다고 생각해보자</p>
<ul>
<li>인덱스가 없는 경우 처음 페이지부터 끝까지 다 읽어야 한다.</li>
</ul>
<pre><code>page 1 → page 2 → page 3 → ... → page 500</code></pre><ul>
<li>인덱스가 있는 경우 목차에서 바로 찾을 수 있다</li>
</ul>
<pre><code>index → page 327</code></pre><p>데이터베이스에서도 같은 원리가 적용된다. 예를 들어 <code>title</code> 컬럼에 index가 있다면</p>
<pre><code>SELECT * FROM topic WHERE title=&#39;MySQL&#39;;</code></pre><p>이 검색을 훨씬 빠르게 수행할 수 있다.</p>
<h2 id="3-modeling--좋은-데이터-구조-설계">(3) Modeling – 좋은 데이터 구조 설계</h2>
<p>데이터가 많아질수록 테이블 구조를 잘 설계하는 것이 매우 중요하다. 이 과정을 데이터 모델링(Data Modeling)이라고 한다.</p>
<p>ex: 다음과 같은 테이블이 있다고 생각해보자.</p>
<pre><code>user
-------------------
id
name
email
phone
address</code></pre><p>그리고 게시글을 저장하는 테이블이 있다.</p>
<pre><code>post
-------------------
id
title
content
user_name
user_email
user_phone</code></pre><p>이 구조에는 같은 정보가 여러 번 저장되어 있다. 사용자가 전화번호를 바꾸면 user 테이블, post 테이블 둘 다 수정해야 한다. 그래서 보통 다음처럼 설계한다:</p>
<pre><code>user
-------------------
id
name
email
phone</code></pre><pre><code>post
-------------------
id
title
content
user_id</code></pre><p>이렇게 하면 데이터 중복이 줄어들고 관리가 쉬워진다. 이 과정을 정규화(Normalization)라고 한다. 좋은 데이터 모델링은 데이터 중복 감소, 데이터 일관성 유지, 유지보수 편리 같은 장점을 만든다.</p>
<h2 id="4-backup--데이터-보호">(4) Backup – 데이터 보호</h2>
<p>데이터베이스에서 가장 중요한 것 중 하나가 백업이다. 왜냐하면 데이터는 언제든지 사라질 수 있기 때문이다.</p>
<p>ex: 하드디스크 고장, 서버 오류, 실수로 데이터 삭제, 해킹,…</p>
<p>그래서 실제 서비스에서는 정기적으로 백업을 수행한다.</p>
<p>최근에는 <strong>클라우드</strong>를 많이 사용한다. 대표적인 클라우드 서비스: AWS (Amazon Web Services), Google Cloud, Microsoft Azure… 클라우드를 사용하면 자동 백업, 데이터 복구, 안정적인 저장 같은 기능을 쉽게 사용할 수 있다.
<img src="https://velog.velcdn.com/images/ttt-1-2/post/dffa8fb6-ebf0-437f-829a-4f5d6b65e82b/image.png" alt=""></p>
<hr>
<h2 id="마무리">마무리</h2>
<p>웹 애플리케이션을 개발하다 보면 자연스럽게 느끼게 되는 것이 하나 있다. 웹 서비스는 결국 데이터와 함께 돌아간다는 것이다. (ex: 회원가입을 하면 사용자 정보가 저장된다, 게시글을 작성하면 글 내용이 저장된다, 로그인하면 저장된 정보와 비교해서 인증을 한다)</p>
<p>프로그래밍 언어마다 데이터베이스에 연결하기 위한 라이브러리나 API가 존재한다. ex: Python MySQL API, PHP MySQL API, Java MySQL API. 이러한 API를 통해 웹 애플리케이션은 데이터베이스와 연결되어 데이터를 저장하고 조회하며 실제 서비스가 동작하게 된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[6] 더 배워보기]]></title>
            <link>https://velog.io/@ttt-1-2/6-%EB%8D%94-%EB%B0%B0%EC%9B%8C%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@ttt-1-2/6-%EB%8D%94-%EB%B0%B0%EC%9B%8C%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Sat, 07 Mar 2026 08:19:51 GMT</pubDate>
            <description><![CDATA[<p>강의: <a href="https://www.inflearn.com/course/database-2-mysql-%EA%B0%95%EC%A2%8C?cid=119293">https://www.inflearn.com/course/database-2-mysql-강좌?cid=119293</a></p>
<p>이번 강의에서는 관계형 데이터베이스가 왜 필요한지 살펴보고, JOIN을 통해 데이터를 연결하는 방법과 MySQL을 사용하는 기본 환경을 간단히 알아본다.</p>
<hr>
<h2 id="관계형-데이터베이스의-필요성">관계형 데이터베이스의 필요성</h2>
<ul>
<li>유지 보수가 쉽다</li>
<li>중복된 데이터 제거가 쉽다</li>
<li>서로 다른 데이터를 연결할 수 있다 (관계형 데이터베이스의 꽃!)</li>
</ul>
<p>ex: 만약 파일이나 하나의 테이블에 모든 정보를 같이 저장하면 다음과 같이 된다.</p>
<table>
<thead>
<tr>
<th>id</th>
<th>title</th>
<th>content</th>
<th>author</th>
<th>author_email</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>데이터베이스</td>
<td>...</td>
<td>김</td>
<td><a href="mailto:kim@gmail.com">kim@gmail.com</a></td>
</tr>
<tr>
<td>2</td>
<td>자료구조</td>
<td>...</td>
<td>이</td>
<td><a href="mailto:lee@gmail.com">lee@gmail.com</a></td>
</tr>
<tr>
<td>3</td>
<td>정보통신공학</td>
<td>...</td>
<td>이</td>
<td><a href="mailto:lee@gmail.com">lee@gmail.com</a></td>
</tr>
</tbody></table>
<p>이 경우 같은 작성자가 여러 글을 쓰면 작성자 정보가 계속 반복 저장된다.</p>
<p>→ 이메일이 바뀌면 모든 row를 수정해야 한다. 데이터 중복이 많아지고 유지 보수가 어려워진다.</p>
<blockquote>
<p>관계형 데이터베이스에서는 데이터를 테이블로 나누고 관계를 만든다.</p>
</blockquote>
<p><strong>(1) author 테이블</strong></p>
<table>
<thead>
<tr>
<th>author_id</th>
<th>name</th>
<th>email</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>김</td>
<td><a href="mailto:kim@gmail.com">kim@gmail.com</a></td>
</tr>
<tr>
<td>2</td>
<td>이</td>
<td><a href="mailto:lee@gmail.com">lee@gmail.com</a></td>
</tr>
</tbody></table>
<p><strong>(2) post 테이블</strong></p>
<table>
<thead>
<tr>
<th>id</th>
<th>title</th>
<th>author_id</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>데이터베이스</td>
<td>1</td>
</tr>
<tr>
<td>2</td>
<td>자료구조</td>
<td>2</td>
</tr>
<tr>
<td>3</td>
<td>정보통신공학</td>
<td>2</td>
</tr>
</tbody></table>
<p>이렇게 하면 작성자 정보는 한 번만 저장하고 게시글에서는 author_id로 연결한다. 데이터 중복이 줄어들고 관리가 훨씬 쉬워진다.</p>
<p>이처럼 서로 다른 데이터를 연결해 구조적으로 관리할 수 있는 것이 관계형 데이터베이스의 핵심이다.</p>
<h2 id="join---관계형-데이터의-꽃">JOIN - 관계형 데이터의 꽃</h2>
<p>관계형 데이터베이스의 가장 큰 특징은 여러 테이블에 저장된 데이터를 서로 연결해서 조회할 수 있다는 것이다. 이때 사용하는 SQL 구문이 바로 <code>JOIN</code>이다.</p>
<p>ex: 다음과 같이 두 개의 테이블이 있다.</p>
<ul>
<li><p><strong>author 테이블</strong>
<img src="https://velog.velcdn.com/images/ttt-1-2/post/063ee393-0e7b-4aa8-814b-043b8ee34e8e/image.png" alt=""></p>
</li>
<li><p><strong>topic 테이블</strong>
<img src="https://velog.velcdn.com/images/ttt-1-2/post/1b352681-e2d6-4ae0-89fc-d453bfda3e3a/image.png" alt=""></p>
</li>
</ul>
<p>여기서 <code>topic</code> 테이블의 <code>author_id</code>는 작성자 테이블(author)의 id를 참조하는 값이다. 즉 <code>topic.author_id = [author.id](http://author.id/)</code> </p>
<p>→ 이 관계를 이용하면 게시글 정보와 작성자 정보를 함께 조회할 수 있다.</p>
<p>JOIN을 사용하면 여러 테이블에 나누어 저장된 데이터를 연결하여 조회할 수 있다. 대표적인 JOIN의 종류는 다음과 같다:</p>
<ul>
<li>INNER JOIN: 두 테이블 모두에 존재하는 데이터만 조회</li>
<li>LEFT JOIN : 왼쪽 테이블의 모든 데이터 유지</li>
<li>RIGHT JOIN: 오른쪽 테이블의 모든 데이터 유지</li>
</ul>
<pre><code class="language-sql">-- topic 테이블과 author 테이블을 LEFT JOIN으로 연결
SELECT * FROM topic LEFT JOIN author ON topic.author_id=author.id;

-- 필요한 컬럼만 선택해서 조회
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/ttt-1-2/post/327b3aa8-a2fe-455a-b284-11e7cd59ff3b/image.png" alt=""></p>
<p>→ 게시글 정보와 작성자 정보들은 다른 테이블에 저장되지만 <code>JOIN</code> 통해 한눈에 확인할 수 있다.</p>
<h2 id="인터넷과-데이터베이스">인터넷과 데이터베이스</h2>
<p>인터넷은 여러 컴퓨터가 서로 통신할 수 있도록 연결해 주는 네트워크이다. 컴퓨터들은 인터넷을 통해 서로 요청(Request) 과 응답(Response) 을 주고받는다. 대표적인 구조가 바로 Client – Server 구조이다.</p>
<pre><code class="language-sql">Client   ── 요청 ──▶  Server
Client   ◀─ 응답 ──   Server</code></pre>
<p>데이터베이스도 같은 구조로 동작한다.</p>
<pre><code class="language-sql">Database Client ── SQL Query ──▶ Database Server ── 데이터 처리 ──▶ 결과 반환 ──▶ Database Client</code></pre>
<ul>
<li>MySQL Server: 데이터를 저장하고 관리하는 프로그램</li>
<li>MySQL Client: 서버에 접속해서 SQL을 실행하는 프로그램</li>
</ul>
<p>현재 실습에서 사용하고 있는 프로그램은 MySQL Client이다. <code>mysql -uroot -p</code> 로 MySQL 접속하면 영문으로 “Welcome to the MySQL monitor” 볼 수 있다. 여기서 말하는 MySQL monitor는 터미널에서 사용하는MySQL command line client를 의미한다. 즉, 우리가 SQL을 입력하고 결과를 확인하는 CLI(Command Line Interface) 기반 MySQL Client이다.</p>
<p><img src="https://velog.velcdn.com/images/ttt-1-2/post/19ed758f-e537-4415-a41f-0577c5bd1e94/image.png" alt="">
또 다른 MySQL Client로 <strong>MySQL Workbench</strong>가 있다. MySQL Workbench는 GUI기반 MySQL Client이다. 즉, 터미널 대신 그래픽 화면을 통해 데이터베이스를 관리할 수 있는 프로그램이다.</p>
<h2 id="mysql-workbench">MySQL Workbench</h2>
<p>다운로드: <a href="https://www.mysql.com/products/workbench/">https://www.mysql.com/products/workbench/</a></p>
<p>앞에서 사용한 MySQL monitor는 터미널에서 SQL을 직접 입력해야 하는 CLI 방식이었다. 반면 MySQL Workbench는 GUI를 통해 데이터베이스를 관리할 수 있다.</p>
<p>ex: SQL 쿼리를 입력하고 실행하거나 데이터 조회 결과를 표 형태로 바로 확인할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/ttt-1-2/post/e28f2054-db52-41d1-b1a7-96385e11fd9f/image.png" alt="">
<img src="https://velog.velcdn.com/images/ttt-1-2/post/0fc93ef8-dcc4-4f79-8a9e-62312e23b82b/image.png" alt="">
MySQL Workbench를 사용하면 SQL 실행뿐 아니라 데이터베이스 서버의 상태, 성능, 사용자 관리 등 다양한 작업을 한 곳에서 관리할 수 있다.
<img src="https://velog.velcdn.com/images/ttt-1-2/post/369e29f3-0a31-4098-819a-eabb938637e0/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[5] MySQL CRUD]]></title>
            <link>https://velog.io/@ttt-1-2/5-MySQL-CRUD</link>
            <guid>https://velog.io/@ttt-1-2/5-MySQL-CRUD</guid>
            <pubDate>Sat, 07 Mar 2026 07:22:41 GMT</pubDate>
            <description><![CDATA[<p>강의: <a href="https://www.inflearn.com/course/database-2-mysql-%EA%B0%95%EC%A2%8C?cid=119293">https://www.inflearn.com/course/database-2-mysql-강좌?cid=119293</a></p>
<p>이번 강의에서는 데이터베이스에서 가장 기본적인 작업인 CRUD(Create, Read, Update, Delete) 개념을 살펴본다.</p>
<hr>
<p>지난 실습에서 생성한 topic 테이블의 구조를 확인하기 위해 <code>DESC topic</code> 명령어를 사용한다. 실행 결과는 다음과 같다.
<img src="https://velog.velcdn.com/images/ttt-1-2/post/4aa4a07b-3f36-4528-9d32-197395ded409/image.png" alt=""></p>
<h2 id="insert-구문-create">INSERT 구문 (Create)</h2>
<p>새로운 데이터를 테이블에 추가할 때 <code>INSERT</code> 구문을 사용한다.</p>
<pre><code class="language-sql">INSERT INTO topic (title, description, created, author, profile)
VALUES(&#39;MySQL&#39;, &#39;MySQL is ...&#39;, NOW(), &#39;trang&#39;, &#39;developer&#39;);
-- topic 테이블에 새로운 게시글 데이터를 추가한다
-- NOW() : 현재 날짜와 시간을 자동으로 입력한다</code></pre>
<h2 id="select-구문-read">SELECT 구문 (Read)</h2>
<p>데이터를 조회할 때는 <code>SELECT</code> 구문을 사용한다.</p>
<pre><code class="language-sql">-- topic 테이블의 모든 컬럼과 모든 데이터를 조회
SELECT * FROM topic;

-- 필요한 컬럼만 선택해서 조회
SELECT id,title,created,author FROM topic;

-- author가 &#39;trang&#39;인 데이터만 조회
SELECT id,title,created,author FROM topic WHERE author=&#39;trang&#39;;

-- id 기준으로 내림차순 정렬 (최신 글이 먼저 나오게)
SELECT id,title,created,author FROM topic WHERE author=&#39;trang&#39; ORDER BY id DESC;

-- 결과를 3개 row까지만 조회
SELECT * FROM topic LIMIT 3;</code></pre>
<h2 id="update-구문">UPDATE 구문</h2>
<p>기존 데이터를 수정할 때 <code>UPDATE</code> 구문을 사용한다.</p>
<pre><code class="language-sql">-- id가 2인 row의 title과 description을 수정
UPDATE topic SET description=&#39;Oracle is ...&#39;, title=&#39;Oracle&#39; WHERE id=2;</code></pre>
<ul>
<li>주의:  <code>WHERE</code> 조건을 넣지 않으면 테이블의 모든 데이터가 수정될 수 있다.</li>
</ul>
<h2 id="delete-구문">DELETE 구문</h2>
<p>데이터를 삭제할 때는 <code>DELETE</code> 구문을 사용한다.</p>
<pre><code class="language-sql">-- id가 5인 데이터를 삭제
DELETE FROM topic WHERE id=5;</code></pre>
<ul>
<li>주의: <code>WHERE</code> 조건이 없으면 테이블의 모든 데이터가 삭제된다.</li>
</ul>
<hr>
<h2 id="정리">정리</h2>
<p>데이터베이스에서 데이터를 다룰 때 가장 기본이 되는 작업은 CRUD이다.</p>
<pre><code>CREATE → INSERT
READ   → SELECT
UPDATE → UPDATE
DELETE → DELETE</code></pre><p>각각의 SQL 구문을 통해 데이터를 추가하고, 조회하고, 수정하고, 삭제할 수 있다.</p>
<p>또한 <code>UPDATE</code>나 <code>DELETE</code>를 사용할 때는 반드시 <code>WHERE</code> 조건을 확인해야 한다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[4] MySQL 테이블의 생성]]></title>
            <link>https://velog.io/@ttt-1-2/4-MySQL-%ED%85%8C%EC%9D%B4%EB%B8%94%EC%9D%98-%EC%83%9D%EC%84%B1</link>
            <guid>https://velog.io/@ttt-1-2/4-MySQL-%ED%85%8C%EC%9D%B4%EB%B8%94%EC%9D%98-%EC%83%9D%EC%84%B1</guid>
            <pubDate>Sat, 07 Mar 2026 07:15:11 GMT</pubDate>
            <description><![CDATA[<p>강의: <a href="https://www.inflearn.com/course/database-2-mysql-%EA%B0%95%EC%A2%8C?cid=119293">https://www.inflearn.com/course/database-2-mysql-강좌?cid=119293</a></p>
<p>실습 tips: MySQL cheating sheet 검색하면 주요 SQL 문법과 예시를 빠르게 확인할 수 있다.</p>
<hr>
<h2 id="실습-테이블의-생성"><strong>실습: 테이블의 생성</strong></h2>
<p>먼저 사용할 데이터베이스를 선택한다.</p>
<pre><code class="language-sql">USE opentutorials</code></pre>
<p>다음으로 <code>topic</code>이라는 테이블을 생성한다.</p>
<pre><code class="language-sql">CREATE TABLE topic (
  id INT(11) NOT NULL AUTO_INCREMENT,
  title VARCHAR(100) NOT NULL,
  description TEXT NULL,
  created DATETIME NOT NULL,
  author VARCHAR(15) NULL,
  profile VARCHAR(200) NULL,
  PRIMARY KEY(id)
);</code></pre>
<p>이 테이블은 게시글 정보를 저장하는 간단한 구조이다. 각 컬럼의 의미는 다음과 같다:</p>
<p><strong>id</strong></p>
<pre><code class="language-sql">id INT(11) NOT NULL AUTO_INCREMENT,</code></pre>
<ul>
<li><code>INT</code> : 정수형 데이터 타입</li>
<li><code>NOT NULL</code> : 값이 반드시 있어야 한다</li>
<li><code>AUTO_INCREMENT</code> : 새로운 데이터가 추가될 때마다 값이 자동으로 1씩 증가한다. (게시글마다 고유한 id 위해 꼭 넣어 줘야 함!)</li>
</ul>
<p><strong>title</strong></p>
<pre><code class="language-sql">title VARCHAR(100) NOT NULL</code></pre>
<ul>
<li><code>VARCHAR(100)</code> : 최대 100자의 문자열을 저장할 수 있다</li>
<li><code>NOT NULL</code> : 제목은 반드시 있어야 하므로 NULL을 허용하지 않는다</li>
</ul>
<p><strong>description</strong></p>
<pre><code class="language-sql">description TEXT NULL</code></pre>
<ul>
<li><code>TEXT</code> : 긴 문자열을 저장할 수 있는 타입</li>
<li><code>NULL</code> : 값이 없어도 허용한다. 본문은 길어질 수 있기 때문에 <code>VARCHAR</code> 대신 <code>TEXT</code> 타입을 사용한다.</li>
</ul>
<p><strong>created</strong></p>
<pre><code class="language-sql">created DATETIME NOT NULL</code></pre>
<ul>
<li><code>DATETIME</code> : 날짜와 시간을 함께 저장하는 데이터 타입</li>
<li><code>NOT NULL</code> : 게시글이 생성된 시간은 반드시 기록되어야 한다.</li>
</ul>
<p><strong>author</strong></p>
<pre><code class="language-sql">author VARCHAR(15) NULL</code></pre>
<ul>
<li><code>VARCHAR(15)</code> : 최대 15자의 문자열을 저장할 수 있다</li>
<li><code>NULL</code> : 작성자가 없는 경우도 허용한다</li>
</ul>
<p><strong>profile</strong></p>
<pre><code class="language-sql">profile VARCHAR(200) NULL</code></pre>
<ul>
<li><code>VARCHAR(200)</code> : 최대 200자의 문자열을 저장할 수 있다.</li>
</ul>
<p><strong>PRIMARY KEY</strong></p>
<pre><code class="language-sql">PRIMARY KEY(id)</code></pre>
<p><code>PRIMARY KEY</code>는 각 행을 구분하는 유일한 값을 의미한다. 이 테이블에서는 <code>id</code> 컬럼을 기본 키로 사용한다.</p>
<blockquote>
<p><strong>주의점: AUTO_INCREMENT와 PRIMARY KEY</strong></p>
</blockquote>
<p><code>id</code> 컬럼을 보면 <code>AUTO_INCREMENT</code> 옵션이 붙어 있다. <code>AUTO_INCREMENT</code>는 데이터가 추가될 때 값이 자동으로 1씩 증가하도록 하는 기능이다. </p>
<p>→ AUTO_INCREMENT를 사용하는 컬럼은 반드시 KEY로 정의되어야 한다. 보통은 <code>PRIMARY KEY</code> 로 설정한다.</p>
<p>테이블이 잘 생성되었는지 확인하려면 <code>SHOW TABLES</code>  명령어 사용한다. 여기에서 topic 테이블이 보이면 정상적으로 생성된 것이다.
<img src="https://velog.velcdn.com/images/ttt-1-2/post/951acfc2-6799-4fce-b31b-4409fd8ddb57/image.png" alt=""></p>
<hr>
<h2 id="정리">정리</h2>
<p>이번 실습 에서는 다음 내용을 확인했다.</p>
<ul>
<li><code>USE</code> : 사용할 데이터베이스 선택</li>
<li><code>CREATE TABLE</code> : 새로운 테이블 생성</li>
<li><code>VARCHAR</code>, <code>TEXT</code>, <code>DATETIME</code> ,… : 컬럼에 사용할 데이터 타입 지정</li>
<li><code>AUTO_INCREMENT</code> : 데이터 추가 시 값 자동 증가</li>
<li><code>PRIMARY KEY</code> : 각 row를 구분하는 고유 키 설정</li>
<li><code>SHOW TABLES</code> : 생성된 테이블 확인</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[3] MySQL 기본]]></title>
            <link>https://velog.io/@ttt-1-2/3-MySQL-%EA%B8%B0%EB%B3%B8</link>
            <guid>https://velog.io/@ttt-1-2/3-MySQL-%EA%B8%B0%EB%B3%B8</guid>
            <pubDate>Fri, 06 Mar 2026 08:00:41 GMT</pubDate>
            <description><![CDATA[<p>강의: <a href="https://www.inflearn.com/course/database-2-mysql-%EA%B0%95%EC%A2%8C?cid=119293">https://www.inflearn.com/course/database-2-mysql-강좌?cid=119293</a></p>
<h2 id="mysql의-구조">MySQL의 구조</h2>
<p>데이터베이스의 구조를 간단하게 보면 다음과 같다:</p>
<pre><code class="language-bash">Database Server
┌───────────────────────────────────────────────────────────────┐
│                                                               │
│   Database                                                    │
│   ┌───────────────────────────────────────────────┐           │
│   │                                               │           │
│   │   Table            Table            Table     │           │
│   │  ┌───────┐        ┌───────┐        ┌───────┐  │           │
│   │  │ row   │        │ row   │        │ row   │  │           │
│   │  │ row   │        │ row   │        │ row   │  │           │
│   │  │ row   │        │ row   │        │ row   │  │           │
│   │  └───────┘        └───────┘        └───────┘  │           │
│   │                                               │           │
│   └───────────────────────────────────────────────┘           │
│                                                               │
│   Database                                                    │
│   ┌───────────────────────────────────────────────┐           │
│   │   Table        Table        Table             │           │
│   └───────────────────────────────────────────────┘           │
│                                                               │
└───────────────────────────────────────────────────────────────┘</code></pre>
<p>(1) Database Server: 여러 개의 데이터베이스를 관리하는 시스템이다. MySQL 프로그램이 실행되는 환경이라고 볼 수 있다.</p>
<p>(2) Database: 여러 개의 테이블을 포함하는 공간이다.</p>
<p>(2) Table: 실제 데이터가 저장되는 구조다.</p>
<h2 id="mysql-서버-접속">MySQL 서버 접속</h2>
<p>MySQL 서버에 접속하려면 터미널에서 다음 명령어를 사용한다: <code>mysql -u root -p</code></p>
<h2 id="mysql-스키마의-사용">MySQL 스키마의 사용</h2>
<ul>
<li><p>스키마 생성, 삭제:
<img src="https://velog.velcdn.com/images/ttt-1-2/post/8ac21b66-0754-4df1-a699-860216e52d85/image.png" alt=""></p>
</li>
<li><p>스키마 목록 확인:
<img src="https://velog.velcdn.com/images/ttt-1-2/post/f17a1fb3-b412-49ae-9475-e5523d45fa71/image.png" alt=""></p>
</li>
<li><p>스키마 선택해서 사용: 
<img src="https://velog.velcdn.com/images/ttt-1-2/post/e2fe3324-ecf6-40d9-95e6-161d839d58bf/image.png" alt=""></p>
</li>
</ul>
<h2 id="sql과-테이블-구조">SQL과 테이블 구조</h2>
<ul>
<li>SQL: <strong>S</strong>tructured <strong>Q</strong>uery <strong>L</strong>anguage</li>
<li>예를 들어 하나의 테이블은 다음과 같은 구조를 가진다.<pre><code class="language-sql">table
├─ row
│   ├─ column
│   ├─ column
│   └─ column
└─ row</code></pre>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[2] MySQL 서론]]></title>
            <link>https://velog.io/@ttt-1-2/2-MySQL-%EC%84%9C%EB%A1%A0</link>
            <guid>https://velog.io/@ttt-1-2/2-MySQL-%EC%84%9C%EB%A1%A0</guid>
            <pubDate>Fri, 06 Mar 2026 07:06:54 GMT</pubDate>
            <description><![CDATA[<p>강의: <a href="https://www.inflearn.com/course/database-2-mysql-%EA%B0%95%EC%A2%8C?cid=119293">https://www.inflearn.com/course/database-2-mysql-강좌?cid=119293</a></p>
<p>파일 기반으로 데이터를 관리할 때 여러 가지 한계가 있었다. 이 문제를 해결하기 위해 1960년대부터 데이터베이스 연구가 시작된다. 1970년에는 Edgar F. Codd가 IBM에서 관계형 데이터베이스(Relational Database) 개념을 제안한다.</p>
<p>이후 수십 년이 지난 지금까지도 관계형 데이터베이스는 가장 널리 사용되는 데이터베이스 모델이다. (ex: MySQL, Oracle, SQL Server, PostgreSQL, Db2, Access)</p>
<p>MySQL은 무료로 사용할 수 있는 오픈소스 소프트웨어이며 성능도 준수한 관계형 데이터베이스다. 그래서 웹 개발에서 많이 사용되는 데이터베이스 중 하나이자 좋은 대안으로 자주 언급된다.</p>
<h2 id="데이터베이스의-목적">데이터베이스의 목적</h2>
<p>데이터베이스를 이해할 때 스프레드시트와 비교해 보면 이해하기 쉽다.</p>
<p><strong>공통점</strong></p>
<p>(1) 데이터를 표 형태로 저장한다</p>
<p>(2) 필터나 정렬 기능으로 데이터를 정리할 수 있다</p>
<p><strong>차이점</strong></p>
<p>스프레드시트:  사용자가 직접 화면에서 클릭하면서 데이터를 조회하고 수정한다.</p>
<p>데이터베이스 (MySQL): 프로그래밍 언어를 통해 데이터를 조작할 수 있다. 프로그램에서 자동으로 데이터를 처리할 수 있다.</p>
<p><em>→ 스프레드시트는 사람이 직접 다루는 도구 vs 데이터베이스는 프로그램이 데이터를 다루기 위한 시스템이다.</em></p>
<blockquote>
<p>웹 서비스에서는 데이터베이스가 핵심 역할을 한다.
예를 들어 회원가입을 하면 사용자 정보가 데이터베이스에 저장된다. 로그인을 하면 서버가 데이터베이스에서 사용자 정보를 조회한다.
→ 그래서 <strong>데이터베이스는 웹 서비스의 데이터를 관리하는 기반 시스템</strong>이라고 볼 수 있다.</p>
</blockquote>
<h2 id="mysql-설치-macos-환경-기준">MySQL 설치 (macOS 환경 기준)</h2>
<p>아래 링크에 접속하면 다운로드할 수 있다:</p>
<p><a href="https://dev.mysql.com/downloads/mysql/">https://dev.mysql.com/downloads/mysql/</a></p>
<p><strong>설치</strong></p>
<ul>
<li>다운로드한 파일을 실행하면 설치가 시작된다.</li>
<li>주의: Accounts and Roles 화면에서 사용자 비밀번호를 입력해야 한다.</li>
</ul>
<p><strong>설정</strong></p>
<ul>
<li>MySQL 실행 파일이 다음 경로에 설치된다. /usr/local/mysql/bin</li>
<li>터미널에서 MySQL 명령어를 바로 사용하려면 PATH 환경변수에 이 경로를 추가해야 한다.</li>
</ul>
<pre><code class="language-bash"># step 1: 터미널에서 설정 파일을 연다.
nano ~/.zshrc

# step 2: 다음 내용을 추가한다.
export PATH=$PATH:/usr/local/mysql/bin

# step 3: 설정을 적용한다.
source ~/.zshrc

# 이제 터미널에서 다음 명령어로 MySQL에 접속할 수 있다.
mysql -u root -p</code></pre>
<p>이제 <code>Enter password:</code> 에 사용자 지정한 비밀번호를 입력하면 된다
<img src="https://velog.velcdn.com/images/ttt-1-2/post/1a7aad1c-a78a-4524-9dd0-b86a960bad86/image.png" alt=""></p>
<ul>
<li>mysql 클라이언트 종료하기: mysql&gt; exit</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[1] DATABASE 소개 및 본질 알아보기]]></title>
            <link>https://velog.io/@ttt-1-2/1-DATABASE-%EC%86%8C%EA%B0%9C-%EB%B0%8F-%EB%B3%B8%EC%A7%88-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@ttt-1-2/1-DATABASE-%EC%86%8C%EA%B0%9C-%EB%B0%8F-%EB%B3%B8%EC%A7%88-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Fri, 06 Mar 2026 00:58:56 GMT</pubDate>
            <description><![CDATA[<p>강의: 강의: <a href="https://www.inflearn.com/course/database-2-mysql-%EA%B0%95%EC%A2%8C?cid=119293">https://www.inflearn.com/course/database-2-mysql-강좌?cid=119293</a></p>
<p>첫번째 강의에서는 데이터베이스의 기본 개념을 먼저 잡는다.</p>
<h2 id="데이터베이스의-본질">데이터베이스의 본질</h2>
<p>데이터베이스의 핵심은 결국 <strong>CRUD</strong>다.  </p>
<p>입력:</p>
<ul>
<li>생성 → Create</li>
<li>수정 → Update</li>
<li>삭제 → Delete</li>
</ul>
<p>출력:</p>
<ul>
<li>조회 → Read</li>
</ul>
<p>데이터를 생성하고, 읽고, 수정하고, 삭제하는 것이 데이터베이스가 하는 가장 기본적인 작업이다.</p>
<h2 id="file-vs-database">File vs Database</h2>
<p><strong>File</strong></p>
<p>파일로 데이터를 관리하면 구조적으로 다루기 어렵다. (ex: 텍스트 파일을 만들면 문장 순서를 바꾸거나 특정 정보만 따로 보기가 쉽지 않다. 제목, 본문 같은 데이터도 원하는 방식으로 조회하기 어렵다…)</p>
<p><strong>Spreadsheet</strong></p>
<p>엑셀처럼 스프레드시트를 사용하면 조금 나아진다. 필터나 정렬 기능을 통해 데이터를 어느 정도 구조적으로 볼 수 있다.</p>
<p><strong>Database</strong></p>
<p>데이터베이스를 사용하면 데이터를 구조적으로 저장하고 관리할 수 있다. 필요한 데이터만 조회하거나 정렬, 필터링하는 것도 훨씬 쉽게 할 수 있다. 데이터가 많아지고 여러 사람이 함께 사용해야 할 때 특히 유용하다.</p>
<h2 id="database-1을-마치며">Database 1을 마치며</h2>
<p>통계를 보면 2026년 기준으로 Oracle이 1위, MySQL이 2위다. (출처: <a href="https://db-engines.com/en/ranking">https://db-engines.com/en/ranking</a>)
<img src="https://velog.velcdn.com/images/ttt-1-2/post/98c9ba17-e124-4241-8992-473292c05b01/image.png" alt="">
데이터베이스도 종류가 여러 가지 있다. Oracle, MySQL, PostgreSQL 같은 관계형 데이터베이스 있고 MongoDB처럼 관계형이 아닌 데이터베이스도 있다. </p>
<p>그래서 보통은 관계형 데이터베이스 하나 + 비관계형 데이터베이스 하나 이렇게 같이 배워두면 좋다고 한다. <strong>MySQL</strong>은 비교적 배우기 쉬워서 입문용 데이터베이스로 많이 추천된다고 한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Part 3 REST API와 테스트 코드 작성하기 (10-13장)]]></title>
            <link>https://velog.io/@ttt-1-2/Part-3-REST-API%EC%99%80-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%BD%94%EB%93%9C-%EC%9E%91%EC%84%B1%ED%95%98%EA%B8%B0-10-13%EC%9E%A5</link>
            <guid>https://velog.io/@ttt-1-2/Part-3-REST-API%EC%99%80-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%BD%94%EB%93%9C-%EC%9E%91%EC%84%B1%ED%95%98%EA%B8%B0-10-13%EC%9E%A5</guid>
            <pubDate>Fri, 03 Oct 2025 15:42:45 GMT</pubDate>
            <description><![CDATA[<h1 id="10장---rest-api와-json">10장 - REST API와 JSON</h1>
<h2 id="101-rest-api와-json">10.1 REST API와 JSON</h2>
<blockquote>
<p><strong>REST API</strong></p>
</blockquote>
<ul>
<li>REST API (Representational State Transfer API)는 서버의 자원을 클라이언트에 구애받지 않고 사용할 수 있게하는 설계 방식이다</li>
<li>다음과 같이 REST API의 동작을 이해할 수 있다:</li>
</ul>
<pre><code class="language-bash">[클라이언트들]
 PC / 모바일 / 태블릿 / IoT ...
        │
        │  HTTP 요청
        ▼
+------------------+
|   REST API 서버  |
+------------------+
        │  JSON 응답(뷰X, 데이터O) //서버는 화면이 아닌 데이터(JSON) 를 돌려준다.
        ▼
자원(RESOURCE): /articles
   ├─ GET    /articles          → 글 목록 조회
   ├─ GET    /articles/{id}     → 글 단건 조회
   ├─ POST   /articles          → 글 생성
   ├─ PATCH  /articles/{id}     → 글 부분 수정
   └─ DELETE /articles/{id}     → 글 삭제
</code></pre>
<blockquote>
<p><strong>JSON</strong></p>
</blockquote>
<ul>
<li>JSON 데이터는 키(key)와 값(value)으로 구성된 정렬되지 않는 속성(properties)의 집합이다.<ul>
<li>키: 큰따옴표 (””)로 감싼다</li>
<li>값: 문자열인 경우만 큰따옴표 (””)로 감싼다</li>
</ul>
</li>
</ul>
<pre><code class="language-json">{
    &quot;name&quot;: &quot;망고&quot;,
    &quot;breeds&quot;: &quot;골든리트리버&quot;,
    &quot;age&quot;: 2
}</code></pre>
<h2 id="102-rest-api-동작-살펴보기">10.2 REST API 동작 살펴보기</h2>
<p>연습용으로 Talend API Tester 이용해 HTTP 요청을 보내고 돌아온 응답을 확인하겠습니다. {JSON} Placeholder 사이트에서 자원들을 JSON 형식으로 받아 실습해 보겠습니다!</p>
<ul>
<li><a href="https://jsonplaceholder.typicode.com/">{JSON} Placeholder 사이트</a></li>
<li><a href="https://chromewebstore.google.com/detail/talend-api-tester-free-ed/aejoelaoggembcahagimdiliamlcdmfm?hl=ko">Talend API 확장 프로그램</a> 설치</li>
</ul>
<blockquote>
<p><strong>HTTP 상태 코드</strong></p>
</blockquote>
<p>HTTP 상태 코드는 클라이언트가 보낸 요청이 성공/실패했는지 알려주는 코드다. 코드는 100번대부터 500번대까지 5개 그룹으로 나눠 있다.</p>
<p>자세한 내용: <a href="https://developer.mozilla.org/ko/docs/Web/HTTP/Reference/Status">MDN 웹 문서</a></p>
<table>
<thead>
<tr>
<th>1XX (정보)</th>
<th>요청이 수신돼, 처리 중</th>
</tr>
</thead>
<tbody><tr>
<td>2XX (성공)</td>
<td>요청이 정상적으로 처리됨</td>
</tr>
<tr>
<td>3XX (리다이렉션)</td>
<td>요청을 완료하려면 추가 행동이 필요함</td>
</tr>
<tr>
<td>4XX (클라이언트 요청 오류)</td>
<td>클라이언트의 요청이 잘못됨</td>
</tr>
<tr>
<td>5XX (서버 응답 오류)</td>
<td>서버 내부에 에러가 발생해 클라이언트의 요청에 답할 수 없음</td>
</tr>
</tbody></table>
<blockquote>
<p><strong>GET 요청하고 응답받기</strong></p>
</blockquote>
<ul>
<li>Method: GET</li>
<li>URL: <a href="https://jsonplaceholder.typicode.com/posts">https://jsonplaceholder.typicode.com/posts</a></li>
<li>응답: 200 (OK)
<img src="https://velog.velcdn.com/images/ttt-1-2/post/3375e93c-7791-49b2-a112-a3489a795296/image.png" alt=""></li>
</ul>
<blockquote>
<p><strong>POST 요청하고 응답받기</strong></p>
</blockquote>
<ul>
<li>Method: POST</li>
<li>URL: <a href="https://jsonplaceholder.typicode.com/posts">https://jsonplaceholder.typicode.com/posts</a></li>
<li>응답: 201 (OK)
<img src="https://velog.velcdn.com/images/ttt-1-2/post/5394582d-9108-4dd4-887e-7554db7724d3/image.png" alt=""></li>
</ul>
<blockquote>
<p><strong>PATCH 요청하고 응답받기</strong></p>
</blockquote>
<ul>
<li>Method: PATCH</li>
<li>URL: <a href="https://jsonplaceholder.typicode.com/posts/1">https://jsonplaceholder.typicode.com/posts/1</a></li>
<li>응답: 200 (OK)
<img src="https://velog.velcdn.com/images/ttt-1-2/post/9b7d2942-0117-47dd-ae7c-116ce63af650/image.png" alt=""></li>
</ul>
<blockquote>
<p><strong>DELETE 요청하고 응답받기</strong></p>
</blockquote>
<ul>
<li>Method: DELETE</li>
<li>URL: <a href="https://jsonplaceholder.typicode.com/posts/10">https://jsonplaceholder.typicode.com/posts/10</a></li>
<li>응답: 200 (OK)
<img src="https://velog.velcdn.com/images/ttt-1-2/post/afed8acc-d816-4ecf-8c7e-44002fe8dba2/image.png" alt=""></li>
</ul>
<hr>
<h1 id="11장---http와-rest-컨트롤러">11장 - HTTP와 REST 컨트롤러</h1>
<p>Part 2에서 만든 firstproject를 이용해 데이터를 CRUD하기 위해 REST API를 구현해 보겠습니다!</p>
<ul>
<li>REST API:<ul>
<li>REST: HTTP URL로 서버의 자원을 명시하고, HTTP 메소드로 해당 자원에 대해 CRUD하는 것</li>
<li>API: 클라이언트가 서버의 자원을 요청할 수 있도록 서버에서 제공하는 인터페이스</li>
</ul>
</li>
</ul>
<h2 id="rest-api-구현">REST API 구현</h2>
<blockquote>
<p><strong>GET 구현하기</strong></p>
</blockquote>
<pre><code class="language-java">    // GET
  @GetMapping(&quot;/api/articles&quot;)
  public List&lt;Article&gt; index() {
      return articleRepository.findAll();
  }

  @GetMapping(&quot;/api/articles/{id}&quot;)
  public Article show(@PathVariable Long id) {
      return articleRepository.findById(id).orElse(null);
  }</code></pre>
<blockquote>
<p><strong>POST 구현하기</strong></p>
</blockquote>
<ul>
<li>주의점: <code>@RequestBody</code> 추가해야 본문(BODY)을 받아 올 수 있다!</li>
</ul>
<pre><code class="language-java">  // POST
  @PostMapping(&quot;/api/articles&quot;)
  public Article create(@RequestBody ArticleForm dto) {
      Article article = dto.toEntity();
      return articleRepository.save(article);
  }</code></pre>
<blockquote>
<p><strong>PATCH 구현하기</strong></p>
</blockquote>
<ul>
<li>주의점: 일부만 수정한 경우: <code>patch</code> 메소드를 만들어야 한다</li>
</ul>
<pre><code class="language-java">    // PATCH
  @PatchMapping(&quot;/api/articles/{id}&quot;)
  public ResponseEntity&lt;Article&gt; update(@PathVariable Long id, @RequestBody ArticleForm dto) {
      // 1. DTO -&gt; 엔티티 변환하기
      Article article = dto.toEntity();
      // 2. 타깃 조회하기
      Article target = articleRepository.findById(id).orElse(null);
      // 3. 잘못된 요청 처리하기
      if(target == null || id != article.getId()) {
          return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(null);
      }
      // 4. 업데이트 및 정상 응답(200)하기
      target.patch(article); // 기존 데이터에 새 데이터 붙이기
      Article updated = articleRepository.save(target); // 수정 내용 DB에 최종 저장
      return ResponseEntity.status(HttpStatus.OK).body(updated);
  }</code></pre>
<blockquote>
<p><strong>DELETE 구현하기</strong></p>
</blockquote>
<pre><code class="language-java">  // DELETE
  @DeleteMapping(&quot;/api/articles/{id}&quot;)
  public ResponseEntity&lt;Article&gt; delete(@PathVariable Long id) {
      // 1. 대상 찾기
      Article target = articleRepository.findById(id).orElse(null);
      // 2. 잘못된 요청 찾기
      if(target == null) {
          return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(null);
      }
      // 3. 대상 삭제하기
      articleRepository.delete(target);
      return ResponseEntity.status(HttpStatus.OK).build();
  }</code></pre>
<hr>
<h1 id="12장---서비스-계층과-트렌잭션">12장 - 서비스 계층과 트렌잭션</h1>
<h2 id="121-개념">12.1 개념</h2>
<ul>
<li>서비스: 컨트롤러와 리포지토리 사이에서 비즈니스 규칙을 수행하고 흐름을 조정한다. 여러 리포지토리 호출을 묶어 트랜잭션 경계를 만든다</li>
<li>트랜잭션: 여러 데이터 작업을 하나의 논리적 단위로 묶어 전부 성공하면 커밋하고 하나라도 실패하면 롤백한다</li>
</ul>
<p>ex:</p>
<ul>
<li>웨이터 = 컨트롤러: 손님(클라이언트) 주문을 받아 전달한다</li>
<li>주방장 = 서비스: 어떤 메뉴(비즈니스 로직)를 어떤 순서로 만들지 결정하고 전체를 지휘한다. 여러 하위 작업을 하나의 주문 단위(트랜잭션) 로 묶는다</li>
<li>보조요리사 = 리포지토리: 주방장이 지시한 대로 재료 창고(DB) 에서 꺼내고 넣는 CRUD 를 수행한다</li>
<li>트랜잭션 = 한 테이블의 코스 전체: 모든 요리가 완성되어야 서빙한다. 하나라도 문제가 나면 전체 취소(롤백) 하고 처음부터 다시 준비한다</li>
</ul>
<h2 id="122-서비스-계층-만들기">12.2 서비스 계층 만들기</h2>
<p>이전 11장까지 만든 코드를 요청해 보겠습니다!</p>
<p>수정한 부분: </p>
<ul>
<li><code>service</code> package 만들기</li>
<li><code>ArticleApiController</code> 수정하기</li>
</ul>
<p>ex:</p>
<ul>
<li>수정하기 전:</li>
</ul>
<pre><code class="language-java">// controller

  // POST
  @PostMapping(&quot;/api/articles&quot;)
  public Article create(ArticleForm dto) {
      Article article = dto.toEntity();
      return articleRepository.save(article);
  }</code></pre>
<ul>
<li>수정한 후:</li>
</ul>
<pre><code class="language-java">// controller

  // POST
  @PostMapping(&quot;/api/articles&quot;)
  public ResponseEntity&lt;Article&gt; create(@RequestBody ArticleForm dto) {
      Article created = articleService.create(dto);
      return (created != null) ?
              ResponseEntity.status(HttpStatus.OK).body(created) :
              ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
  }

// service

    public Article create(ArticleForm dto) {
            Article article = dto.toEntity();
            if (article.getId() != null) {
                return null;
            }
            return articleRepository.save(article);
    }</code></pre>
<h2 id="123-트랜잭션-맛보기">12.3 트랜잭션 맛보기</h2>
<ul>
<li><code>ArticleApiController</code></li>
</ul>
<pre><code class="language-java">  @PostMapping(&quot;/api/transaction-test&quot;)
  public ResponseEntity&lt;List&lt;Article&gt;&gt; transactionTest (@RequestBody List&lt;ArticleForm&gt; dtos) {
      List&lt;Article&gt; createdList = articleService.createArticles(dtos);
      return (createdList != null) ?
              ResponseEntity.status(HttpStatus.OK).body(createdList) :
              ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
  }</code></pre>
<ul>
<li><code>ArticleService</code></li>
</ul>
<pre><code class="language-java">
  @Transactional
  public List&lt;Article&gt; createArticles(List&lt;ArticleForm&gt; dtos) {
      // 1. dto 묶음을 엔티티 묶음으로 변환하기
      List&lt;Article&gt; articleList = dtos.stream()
              .map(dto -&gt; dto.toEntity())
              .collect(Collectors.toList());
      // 2. 엔티티 묶음을 DB에 저장하기
      articleList.stream()
              .forEach(article -&gt; articleRepository.save(article));
      // 3. 강제 예외 발생시키기
      articleRepository.findById(-1L)
              .orElseThrow(() -&gt; new IllegalArgumentException(&quot;결제 실패!&quot;));
      // 4. 결과 값 반환하기
      return articleList;

  }</code></pre>
<p>→ <code>@Transactional</code>로 메서드 전체를 하나의 트랜잭션으로 묶어 DTO 목록을 엔티티로 변환해 저장한 뒤, 의도적으로 예외를 던져 롤백이 일어나는지 확인한다. 예외가 발생하면 이전에 저장된 작업이 모두 취소되어 DB에 반영되지 않는다!</p>
<hr>
<h1 id="13장---테스트-코드-작성하기">13장 - 테스트 코드 작성하기</h1>
<p>테스트 코드는 보통 3단계로 작성한다:</p>
<ol>
<li>예상 데이터 작성하기</li>
<li>실제 데이터 획득하기</li>
<li>예상 데이터와 실제 데이터 비교해 검증하기</li>
</ol>
<h2 id="테스트-코드-작성하기">테스트 코드 작성하기</h2>
<ul>
<li><p>메소드 오른쪽 누르고 Generate → Test 선택. JUnit5로 테스트를 만들겠습니다!
<img src="https://velog.velcdn.com/images/ttt-1-2/post/cd1c24ec-b533-4aff-b73d-7d02081602d6/image.png" alt=""></p>
</li>
<li><p>ArticleSeriveTest 생성 위치를 확인할 수 있다
<img src="https://velog.velcdn.com/images/ttt-1-2/post/2a71f3f6-d4ff-47f7-b89d-c5612c649dc4/image.png" alt=""></p>
</li>
<li><p>테스트 코드 작성하기:</p>
<pre><code class="language-java">@SpringBootTest
class ArticleServiceTest {
  @Autowired
  ArticleService articleService;

  @Test
  void index() {
      // 1. 예상 데이터
      Article a = new Article(1L, &quot;가가가가&quot;, &quot;1111&quot;);
      Article b = new Article(2L, &quot;나나나나&quot;, &quot;2222&quot;);
      Article c = new Article(3L, &quot;다다다다&quot;, &quot;3333&quot;);
      List&lt;Article&gt; expected = new ArrayList&lt;Article&gt;(Arrays.asList(a,b,c));
      // 2. 실제 데이터
      List&lt;Article&gt; articles = articleService.index();
      // 3. 비교 및 검증
      assertEquals(expected.toString(), articles.toString());
  }
  @Test
  void show_성공() {
      // 1. 예상 데이터
      Long id = 1L;
      Article expected = new Article(id, &quot;가가가가&quot;, &quot;1111&quot;);
      // 2. 실제 데이터
      Article article = articleService.show(id);
      // 3. 비교 및 검증
      assertEquals(expected.toString(), article.toString());
  }

  @Test
  void show_실패() {
      // 1. 예상 데이터
      Long id = -1L;
      Article expected = null;
      // 2. 실제 데이터
      Article article = articleService.show(id);
      // 3. 비교 및 검증
      assertEquals(expected, article);
  }
}</code></pre>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[알고리즘 | 투 포인터, 슬라이딩 윈도우]]></title>
            <link>https://velog.io/@ttt-1-2/%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%ED%88%AC-%ED%8F%AC%EC%9D%B8%ED%84%B0-%EC%8A%AC%EB%9D%BC%EC%9D%B4%EB%94%A9-%EC%9C%88%EB%8F%84%EC%9A%B0</link>
            <guid>https://velog.io/@ttt-1-2/%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%ED%88%AC-%ED%8F%AC%EC%9D%B8%ED%84%B0-%EC%8A%AC%EB%9D%BC%EC%9D%B4%EB%94%A9-%EC%9C%88%EB%8F%84%EC%9A%B0</guid>
            <pubDate>Thu, 02 Oct 2025 08:46:38 GMT</pubDate>
            <description><![CDATA[<p>이 글의 내용은 다음과 같습니다:</p>
<ol>
<li><p>투포인터</p>
</li>
<li><p>1 백준 1253 좋다</p>
</li>
<li><p>슬라이딩 윈도우</p>
</li>
<li><p>1 백준 12891번: DNA 비밀번호</p>
</li>
</ol>
<hr>
이 두 가지 알고리즘의 공통점은 1차원 배열 반복 탐색할 때 시간 복잡 O(N^2) 에서 O(N) 으로 줄일 수 있습니다.

<p>다른 점은 투 포인터의 경우 두 인덱스 start, end를 조건에 따라 독립적으로 이동시키며 구간을 늘리거나 줄여서 배열을 탐색합니다.</p>
<p>반면 슬라이딩 윈도우는 고정 크기의 윈도우를 유지한 채 한 칸씩 이동하면서 들어오는 원소 &amp;&amp; 빠지는 원소만 O(1)로 반영해 문제를 해결합니다.</p>
<hr>

<h1 id="1-투-포인터">1. 투 포인터</h1>
<ul>
<li>투 포인터는 두 개의 포인터 start(left), end(right) 이용해 값을 탐색하는 알고리즘입니다.
<img src="https://velog.velcdn.com/images/ttt-1-2/post/c4b76041-11a2-493d-ae6d-7bd44ff87eee/image.png" alt=""></li>
</ul>
<h2 id="11-1253-좋다">1.1) 1253 좋다</h2>
<p><img src="https://velog.velcdn.com/images/ttt-1-2/post/f973491d-2205-4367-a388-b2bcbad7182d/image.png" alt=""></p>
<ul>
<li><code>sort()</code> 함수를 이용해 배열 정렬해 줍니다</li>
<li>배열 양 끝에서 <code>i(start)=0</code> 와 <code>j(end)=N-1</code> 포인터를 지정해 두 수의 합이 find과 같으면 res 증가 시켰습니다</li>
</ul>
<pre><code class="language-cpp">#include&lt;iostream&gt;
#include&lt;vector&gt;
#include&lt;algorithm&gt;
using namespace std;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(NULL);
    cout.tie(NULL);

    int N;
    cin &gt;&gt; N;
    vector&lt;int&gt; A(N,0);

    for(int i=0;i&lt;N;i++) {
        cin &gt;&gt; A[i];
    }

    sort(A.begin(), A.end());
    int res = 0;

    for(int k=0; k&lt;N; k++) {
        long find = A[k];
        int i = 0, j = N-1;

        while(i &lt; j) {
            if(A[i] + A[j] == find) {
                if(i != k &amp;&amp; j != k) { //서로 다른 수의 합인지 체크
                    res++;
                    break;
                }
                else if (i==k) i++;
                else if (j==k) j--;
            }
            else if(A[i] + A[j] &lt; find) i++;
            else j--;
        }
    }

    cout &lt;&lt; res;
}</code></pre>
<hr>

<h1 id="2-슬라이딩-윈도우">2. 슬라이딩 윈도우</h1>
<p>슬라이딩 윈도우는 투 포인터와 유사한데 윈도우의 크기는 <strong>고정</strong>됩니다.
<img src="https://velog.velcdn.com/images/ttt-1-2/post/9244a807-68a1-450f-9b5f-e0e7503e1ece/image.png" alt=""></p>
<h2 id="21-12891번-dna-비밀번호">2.1) 12891번: DNA 비밀번호</h2>
<ul>
<li><p>문제 요약: 문자열이 {A, C, G, T} 로만 이루어진 DNA 문자열에서 길이 S의 모든 부분문자열을 검사해 각 문자 A, C, G, T가 최소 몇 번 이상 등장해야 한다는 조건을 모두 만족하면 비밀번호로 인정하여 개수를 센다</p>
</li>
<li><p>풀이 과정:</p>
<ul>
<li>Step 1: 체크 배열(checkArr) 저장</li>
<li>Step 2: 윈도우에 포함된 문자로 현재 상태 배열 (myArray) 만든다. myArr &amp;&amp; checkArr 비교</li>
<li>Step 3: 3: 윈도우를 한 칸씩 이동해 myArr 업데이트한다. 계속해서 myArr &amp;&amp; checkArr 비교한다</li>
</ul>
</li>
</ul>
<pre><code class="language-cpp">#include&lt;iostream&gt;
using namespace std;

int checkArr[4];
// myArr[i]: 슬라이딩 윈도우 안에서 각 문자가 나온 현재 개수
int myArr[4];
// check: 4가지 조건(A/C/G/T) 중에서 요구에 도달한 항목 수
int check = 0;
void Add(char c);
void Remove(char c);

void Add(char c) { //새로 들어온 문자를 처리하는 함수
    switch (c) {
        case &#39;A&#39;:
            myArr[0]++;
            if (myArr[0] == checkArr[0]) check++;
            break;
        case &#39;C&#39;:
            myArr[1]++;
            if (myArr[1] == checkArr[1])  check++;
            break;
        case &#39;G&#39;:
            myArr[2]++;
            if (myArr[2] == checkArr[2])  check++;
            break;
        case &#39;T&#39;:
            myArr[3]++;
            if (myArr[3] == checkArr[3])  check++;
            break;
    }
}

void Remove(char c) { //제거할 문자를 처리하는 함수
    switch(c) {
        case &#39;A&#39;:
            if (myArr[0] == checkArr[0]) check--;
            myArr[0]--;
            break;
        case &#39;C&#39;:
            if (myArr[1] == checkArr[1]) check--;
            myArr[1]--;
            break;
        case &#39;G&#39;:
            if (myArr[2] == checkArr[2]) check--;
            myArr[2]--;
            break;
        case &#39;T&#39;:
            if (myArr[3] == checkArr[3]) check--;
            myArr[3]--;
            break;
    }
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(NULL);
    cout.tie(NULL);

    int S, P;
    cin &gt;&gt; S &gt;&gt; P;
    int Result = 0;
    string A;
    cin &gt;&gt; A;

    for(int i=0;i&lt;4;i++) {
        cin &gt;&gt; checkArr[i];
        if(checkArr[i] == 0) {
            check++;
        }
    }

    // 초기 윈도우: [0, P-1] 구간을 먼저 반영
    for(int i=0; i&lt;P; i++) {
        Add(A[i]);
    }
    // 초기 윈도우가 4가지 조건을 모두 만족하면 결과 +1
    if(check == 4) {
        Result++;
    }

    //슬라이딩 윈도우 처리 부분
    for(int i=P; i&lt;S; i++) {
        int j = i-P; // i: 새로 들어오는 문자의 인덱스, j: 빠져나가는 문자의 인덱스
        Add(A[i]);
        Remove(A[j]);
        if (check == 4) {
            Result++;
        }
    }
    cout &lt;&lt; Result &lt;&lt; &quot;\n&quot;;
}</code></pre>
<hr>
<p><em>*이 글은 교재 &quot;Do it! 알고리즘 코딩 테스트 C++&quot; 기반으로 작성했습니다!</em></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Part 2 게시판 CRUD 만들기 (6-9장)]]></title>
            <link>https://velog.io/@ttt-1-2/Part-2-%EA%B2%8C%EC%8B%9C%ED%8C%90-CRUD-%EB%A7%8C%EB%93%A4%EA%B8%B0-6-9%EC%9E%A5</link>
            <guid>https://velog.io/@ttt-1-2/Part-2-%EA%B2%8C%EC%8B%9C%ED%8C%90-CRUD-%EB%A7%8C%EB%93%A4%EA%B8%B0-6-9%EC%9E%A5</guid>
            <pubDate>Sat, 27 Sep 2025 13:15:25 GMT</pubDate>
            <description><![CDATA[<h1 id="6장---게시판-내-페이지-이동하기">6장 - 게시판 내 페이지 이동하기</h1>
<h2 id="61-링크와-리다이렉트">6.1 링크와 리다이렉트</h2>
<ul>
<li>링크(link): 사용자가 <code>&lt;a&gt;</code>나 <code>&lt;form&gt;</code>을 통해 다른 페이지로 이동하겠다는 요청만 전달하고 그 결과 페이지를 그대로 응답받는 단순 내비게이션 기능이다</li>
<li>리다이렉트(redirect): 서버가 요청을 처리한 뒤 ‘다음으로 갈 주소’를 클라이언트에 지시하여 다시 요청하게 만드는 방식이다</li>
</ul>
<h2 id="62-링크와-리다이렉트를-이용해-페이지-연결하기">6.2 링크와 리다이렉트를 이용해 페이지 연결하기</h2>
<ul>
<li>새 글 작성 링크 만들기</li>
</ul>
<pre><code class="language-java">&lt;a href=&quot;/articles/new&quot;&gt;New Article&lt;/a&gt;</code></pre>
<ul>
<li>&lt;목록 페이지&gt; → &lt;목록 페이지&gt; 돌아가기</li>
</ul>
<pre><code class="language-java">&lt;a href=&quot;/articles&quot;&gt;Back&lt;/a&gt;</code></pre>
<ul>
<li><p>&lt;입력 페이지&gt; → &lt;상세 페이지&gt; 이동하기</p>
<ul>
<li><p>리다이렉드: 클라이언트의 요청를 받아 새로운 URL 주소로 재요청하라고 지시하는 것이다</p>
<pre><code class="language-java">return &quot;redirect:URL_주소&quot;;</code></pre>
</li>
</ul>
</li>
</ul>
<ul>
<li>&lt;상세 페이지&gt; → &lt;목록 페이지&gt; 돌아가기</li>
</ul>
<pre><code class="language-java">//show.mustache
&lt;a href=&quot;/articles&quot;&gt;Go to Article List&lt;/a&gt;</code></pre>
<ul>
<li>&lt;목록 페이지&gt; → &lt;상세 페이지&gt; 이동하기</li>
</ul>
<pre><code class="language-java"> &lt;td colspan=&quot;2&quot;&gt;&lt;a href=&quot;/articles/{{id}}&quot;&gt;{{title}}&lt;/td&gt;</code></pre>
<h1 id="7장---데이터-수정-과정">7장 - 데이터 수정 과정</h1>
<p><strong>- 1단계: &lt;수정 페이지&gt; 만들고 기존 데이터 불러오기</strong></p>
<pre><code class="language-java">[목록 페이지] 
    │ (항목 클릭)
    v
[상세 페이지]
    │ (Edit 버튼)
    v
[컨트롤러] ── DB에서 기존 데이터 조회 ──&gt; [모델에 담음]
    │
    v
[수정 폼 페이지 렌더]</code></pre>
<p><strong>- 2단계: 데이터를 수정해 DB에 반영한 후 결과를 볼 수 있게 &lt;상세 페이지&gt;로 리다이렉트하기</strong></p>
<pre><code class="language-java">[수정 폼 페이지] --(Submit: 폼데이터)--&gt; [컨트롤러]
    │                                             
    └─ DTO 생성 → 엔티티로 변환/병합 → 리포지토리.save() → DB 갱신
                                                    │
                                                    v
                                리다이렉트(결과 확인용 페이지 또는 목록)
</code></pre>
<blockquote>
<p>HTTP 메소드</p>
</blockquote>
<ul>
<li>HTTP(HyperText Transfer Protocol)는 웹 서비스에 사용하는 프로토콜이다</li>
<li>HTTP의 대표적인 메서드:<ul>
<li>POST</li>
<li>GET</li>
<li>PATCH(PUT)</li>
<li>DELETE</li>
</ul>
</li>
</ul>
<p><strong><em>CRUD를 위한 SQL 문과 HTTP 메서드</em></strong></p>
<table>
<thead>
<tr>
<th>데이터 관리</th>
<th>SQL</th>
<th>HTTP</th>
</tr>
</thead>
<tbody><tr>
<td>Create</td>
<td>INSERT</td>
<td>POST</td>
</tr>
<tr>
<td>Read</td>
<td>SELECT</td>
<td>GET</td>
</tr>
<tr>
<td>Update</td>
<td>UPDATE</td>
<td>PATCH (PUT)</td>
</tr>
<tr>
<td>Delete</td>
<td>DELETE</td>
<td>DELETE</td>
</tr>
</tbody></table>
<h1 id="8장---게시글-삭제하기-delete">8장 - 게시글 삭제하기: DELETE</h1>
<h2 id="데이터-삭제-과정">데이터 삭제 과정</h2>
<ul>
<li>Step 1: 클라이언트가 HTTP 메서드로 게시글 삭제 요청</li>
<li>Step 2: 요청 받은 컨트롤러는 리파지터리를 통해 DB에 저장된 데이터를 찾아 삭제한다</li>
<li>Step 3: 삭제 완료시 클라이언트를 결과 페이지로 리타이렉트한다</li>
</ul>
<pre><code class="language-java">[상세 페이지] (Delete 클릭)
      │  POST/GET /articles/{id}/delete
      v
[컨트롤러] ── findById(id) → 존재 확인
      │
      ├─ 존재 O → repository.delete(엔티티)
      │
      └─ 존재 X → 아무 것도 안 함
      v
redirect:/articles  (목록)</code></pre>
<pre><code class="language-java">[컨트롤러]
  delete 성공 → redirectAttributes.addFlashAttribute(&quot;msg&quot;, &quot;삭제되었습니다!&quot;)
  └─ redirect:/articles
                 │
                 v
        [목록 페이지 index.mustache]
           {{#msg}} 삭제되었습니다! {{/msg}}
</code></pre>
<h2 id="sql-문으로-직접-db-삭제하기">SQL 문으로 직접 DB 삭제하기</h2>
<pre><code class="language-java">DELETE [FROM] 테이블면 WHERE 조건;</code></pre>
<h1 id="9장---crud와-sql-쿼리-종합">9장 - CRUD와 SQL 쿼리 종합</h1>
<h2 id="91-jpa-로깅-설정하기">9.1 JPA 로깅 설정하기</h2>
<ul>
<li>쿼리(query)란 DB에 정보를 요청하는 구문</li>
<li>SQL 쿼리:<ul>
<li>INSERT (생성)</li>
<li>SELECT (조회)</li>
<li>UPDATE (수정)</li>
<li>DELETE (삭제)</li>
</ul>
</li>
<li>로깅(logging): 시스템이 작동할 때 당시의 상태와 작동 정보를 기록하는 것</li>
</ul>
<pre><code class="language-bash"># resources/application.properties
# JPA 로깅 설정
# 디버그 레벨로 쿼리 출력
logging.level.org.hibernate.SQL=DEBUG

# 쿼리 줄바꿈하기
spring.jpa.properties.hibernate.format_sql=true

# 매개변수 값 보여 주기
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE

# DB URL 설정
# 유니크 URL 생성하지 않기
spring.datasource.generate-unique-name=false
# 고정 URL 설정하기
spring.datasource.url=jdbc:h2:mem:testdb</code></pre>
<ul>
<li><p>로깅 레벨은 7단계가 있다:</p>
<ul>
<li>TRACE(레벨1): DEBUG 레벨보다 더 상세한 정보</li>
<li>DEBUG(레벨2): 응용 프로그램을 디버깅하는 데 필요한 세부 정보</li>
<li>INFRO(레벨3): 응용 프로그램의 순조로운 진행 정보</li>
<li>WARN(레벨4): 잠재적으로 유해한 상황 정보</li>
<li>ERROR(레벨5): 응용 프로그램이 수행할 수 있는 정도의 오류 정보</li>
<li>FATAL(레벨6): 응용 프로그램이 중단될 만한 심각한 오류 정보</li>
<li>OFF(레벨7): 로깅 기능 해제</li>
</ul>
</li>
<li><p>서버 실행하고 쿼리 확인:
<img src="https://velog.velcdn.com/images/ttt-1-2/post/66b39964-26bf-43a1-9d54-d1d177d2cafd/image.png" alt=""></p>
</li>
<li><p>H2 Console 접속:
<img src="https://velog.velcdn.com/images/ttt-1-2/post/b22fd324-6043-4d04-9b82-112dca16b050/image.png" alt=""></p>
</li>
</ul>
<h2 id="92-sql-쿼리-로그-확인하기">9.2 SQL 쿼리 로그 확인하기</h2>
<blockquote>
<p><strong>INSERT 문</strong></p>
</blockquote>
<p>데이터를 생상(Create)할 때 어떤 SQL 쿼리가 동작하는지 확인해 보겠습니다
<img src="https://velog.velcdn.com/images/ttt-1-2/post/5800f202-dd52-4c07-a4eb-8cdcfd320227/image.png" alt=""></p>
<blockquote>
<p><strong>SELECT 문</strong></p>
</blockquote>
<p>데이터를 조회(Read)할 때 어떤 SQL 쿼리가 동작하는지 확인해 보겠습니다
<img src="https://velog.velcdn.com/images/ttt-1-2/post/e521d4c8-50a0-4c04-9e50-93d1ff63c7d1/image.png" alt=""></p>
<blockquote>
<p><strong>UPDATE 문</strong></p>
</blockquote>
<p>데이터를 수정(UPDATE)할 때 어떤 SQL 쿼리가 동작하는지 확인해 보겠습니다
<img src="https://velog.velcdn.com/images/ttt-1-2/post/be1c79f0-f4d4-4b3e-8ff1-51008256cc67/image.png" alt=""></p>
<blockquote>
<p><strong>DELETE 문</strong></p>
</blockquote>
<p>데이터를 삭제(DELETE)할 때 어떤 SQL 쿼리가 동작하는지 확인해 보겠습니다
<img src="https://velog.velcdn.com/images/ttt-1-2/post/554470bf-ead6-4697-aeb4-46b967dfa313/image.png" alt=""></p>
<h2 id="93-기본-sql-쿼리-작성하기">9.3 기본 SQL 쿼리 작성하기</h2>
<ul>
<li>CREATE TABLE 문의 형식은 다음과 같습니다:</li>
</ul>
<pre><code class="language-bash">CREATE TABLE 테이블명 (
    속성명1 자료형,
    속성명2 자료형
    속성명3 자료형,
    PRIMARY KEY (기본키)
);</code></pre>
<ul>
<li>ex: coffee 테이블을 만들어 보겠습니다
<img src="https://velog.velcdn.com/images/ttt-1-2/post/1ab44956-7a5c-490f-8580-e84f915504dd/image.png" alt=""></li>
</ul>
<blockquote>
<p><strong>coffee 데이터 생성하기</strong></p>
</blockquote>
<pre><code class="language-sql">//데이터 생성하기
****
INSERT
INTO    
        coffee
        (id, name, price) 
VALUES
        (1, &#39;아메리카노&#39;, 4100)</code></pre>
<p><img src="https://velog.velcdn.com/images/ttt-1-2/post/7a8fec85-aa50-4fd4-9209-951a4e95a62c/image.png" alt=""></p>
<blockquote>
<p><strong>coffee 데이터 조회하기</strong></p>
</blockquote>
<pre><code class="language-sql">// 데이터 조회하기

SELECT
    id, name, price
FROM
    coffee
WHERE
    id = 3;</code></pre>
<blockquote>
<p><strong>coffee 데이터 수정하기</strong></p>
</blockquote>
<pre><code class="language-sql">UPDATE
    coffee
SET
    price = 9900
WHERE
    id = 4;</code></pre>
<blockquote>
<p><strong>coffee 데이터 삭제하기</strong></p>
</blockquote>
<pre><code class="language-sql">DELETE
FROM
    coffee
WHERE
    id = 4;</code></pre>
]]></description>
        </item>
    </channel>
</rss>