2025-04-08T19:56:24

This commit is contained in:
2025-04-08 19:56:24 +09:00
parent a75a1dbd0f
commit eef061c1c9
100 changed files with 18639 additions and 0 deletions

View File

@@ -0,0 +1,97 @@
아래는 "엔티티와 테이블 매핑"에 대해 예시와 함께 설명하는 글입니다. 롬복(Lombok)을 활용해 코드를 간결하게 작성했습니다.
---
### 엔티티와 테이블 매핑
JPA(Java Persistence API)에서 엔티티(Entity)는 데이터베이스의 테이블과 매핑되는 객체입니다. 즉, 객체지향 프로그래밍의 클래스를 관계형 데이터베이스의 테이블 구조에 연결하는 역할을 합니다. 이를 통해 개발자는 SQL 쿼리를 직접 작성하지 않고도 객체를 통해 데이터를 조작할 수 있습니다. 엔티티와 테이블 매핑은 `@Entity`, `@Table`, `@Id`, `@Column` 같은 어노테이션을 사용해 정의됩니다.
#### 기본 매핑 규칙
- `@Entity`: 클래스가 엔티티임을 나타냅니다. JPA가 이 클래스를 관리 대상으로 인식합니다.
- `@Table`: 엔티티가 매핑될 데이터베이스 테이블을 지정합니다. 생략하면 클래스 이름이 테이블 이름으로 사용됩니다.
- `@Id`: 엔티티의 기본 키(Primary Key)를 지정합니다.
- `@Column`: 필드가 매핑될 테이블의 컬럼을 지정합니다. 생략하면 필드 이름이 컬럼 이름으로 사용됩니다.
#### 예시: 회원(Member) 엔티티
아래는 회원 정보를 관리하는 `Member` 엔티티를 롬복을 사용해 작성한 예시입니다.
```java
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Column;
import jakarta.persistence.Table;
@Entity
@Table(name = "members") // 테이블 이름 지정
@Getter
@Setter
@NoArgsConstructor // 기본 생성자 (JPA 요구 사항)
public class Member {
@Id // 기본 키
@Column(name = "member_id") // 컬럼 이름 지정
private Long id;
@Column(name = "username", nullable = false) // null 불가 컬럼
private String name;
@Column(name = "email", unique = true) // 유니크 제약 조건
private String email;
// 추가 생성자 (편의를 위해)
public Member(Long id, String name, String email) {
this.id = id;
this.name = name;
this.email = email;
}
}
```
#### 코드 설명
1. **`@Entity``@Table`**
- `@Entity`는 이 클래스가 JPA 엔티티임을 나타냅니다.
- `@Table(name = "members")`는 이 엔티티가 `members`라는 테이블에 매핑됨을 지정합니다. 생략하면 클래스 이름(`Member`)이 기본 테이블 이름으로 사용됩니다.
2. **`@Id``@Column`**
- `id` 필드는 `@Id`로 기본 키임을 선언합니다.
- `@Column(name = "member_id")``id` 필드가 `members` 테이블의 `member_id` 컬럼에 매핑됨을 나타냅니다.
- `name` 필드는 `@Column(name = "username", nullable = false)`로, `username` 컬럼에 매핑되며 `NULL` 값을 허용하지 않습니다.
- `email` 필드는 `@Column(name = "email", unique = true)`로, `email` 컬럼에 매핑되며 중복 값을 허용하지 않는 유니크 제약 조건을 가집니다.
3. **롬복 활용**
- `@Getter``@Setter`로 getter와 setter 메서드를 자동 생성합니다.
- `@NoArgsConstructor`로 JPA가 요구하는 기본 생성자를 생성합니다. JPA는 엔티티 객체를 생성한 후 리플렉션을 통해 필드 값을 채우므로 기본 생성자가 필수입니다.
#### 매핑된 테이블 구조
위 엔티티에 해당하는 데이터베이스 테이블은 다음과 같습니다:
```sql
CREATE TABLE members (
member_id BIGINT PRIMARY KEY,
username VARCHAR(255) NOT NULL,
email VARCHAR(255) UNIQUE
);
```
#### 동작 방식
- JPA는 `Member` 객체를 저장(`persist`)하면 `members` 테이블에 데이터를 삽입합니다.
- 예를 들어, `new Member(1L, "홍길동", "hong@example.com")` 객체를 저장하면 테이블에 다음과 같은 레코드가 추가됩니다:
```
member_id: 1, username: "홍길동", email: "hong@example.com"
```
#### 추가 팁
- 필드와 컬럼 이름이 동일하다면 `@Column`은 생략 가능합니다.
- `@GeneratedValue`를 `@Id`와 함께 사용하면 기본 키를 자동 생성할 수 있습니다(예: `AUTO_INCREMENT`).
---
이처럼 엔티티와 테이블 매핑은 JPA의 핵심 기능으로, 객체와 데이터베이스 간의 자연스러운 연결을 가능하게 합니다. 다음 장에서는 연관관계 매핑(예: `@OneToMany`, `@ManyToOne`)을 다루며 더 복잡한 구조를 살펴보겠습니다.
---
이 글이 책의 스타일에 맞는지, 더 추가하거나 수정할 내용이 있는지 알려주시면 반영하겠습니다!

View File

@@ -0,0 +1,140 @@
아래는 "기본 키 매핑"에 대해 예시와 함께 설명하는 글입니다. 롬복을 사용해 코드를 간결하게 유지했습니다.
---
### 기본 키 매핑
JPA에서 기본 키(Primary Key)는 엔티티를 식별하는 고유한 값으로, 데이터베이스 테이블의 각 레코드를 구분하는 데 사용됩니다. JPA는 `@Id` 어노테이션을 통해 기본 키를 지정하며, 기본 키 값을 자동으로 생성하는 다양한 전략을 제공합니다. 이를 "기본 키 매핑"이라고 부르며, `@GeneratedValue`와 함께 사용해 효율적으로 관리할 수 있습니다.
#### 기본 키 매핑의 주요 어노테이션
- **`@Id`**: 필드가 엔티티의 기본 키임을 나타냅니다. 모든 엔티티는 반드시 하나의 `@Id`를 가져야 합니다.
- **`@GeneratedValue`**: 기본 키 값을 자동 생성하도록 설정합니다. `strategy` 속성을 통해 생성 전략을 지정할 수 있습니다.
#### 기본 키 생성 전략
JPA는 네 가지 주요 기본 키 생성 전략을 제공합니다:
1. **`GenerationType.AUTO`**
- JPA 구현체(예: Hibernate)가 데이터베이스에 맞는 최적의 전략을 자동 선택합니다. 기본값이며, 환경에 따라 다르게 동작할 수 있습니다.
2. **`GenerationType.IDENTITY`**
- 데이터베이스의 `AUTO_INCREMENT` 기능을 사용합니다. MySQL, PostgreSQL 등에서 흔히 사용됩니다.
3. **`GenerationType.SEQUENCE`**
- 데이터베이스 시퀀스를 사용합니다. Oracle, PostgreSQL 등에서 지원됩니다.
4. **`GenerationType.TABLE`**
- 별도의 테이블을 만들어 기본 키 값을 관리합니다. 데이터베이스 종류에 상관없이 동작하지만 성능이 느릴 수 있습니다.
#### 예시: 기본 키 매핑
아래는 `Product` 엔티티를 통해 기본 키 매핑을 보여주는 예시입니다. 롬복을 사용해 코드를 간략화했습니다.
```java
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Column;
@Entity
@Getter
@Setter
@NoArgsConstructor
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY) // 자동 증가 전략
@Column(name = "product_id")
private Long id;
@Column(name = "product_name")
private String name;
@Column(name = "price")
private int price;
// 편의를 위한 생성자
public Product(String name, int price) {
this.name = name;
this.price = price;
}
}
```
#### 코드 설명
1. **`@Id``@GeneratedValue`**
- `id` 필드는 `@Id`로 기본 키임을 선언합니다.
- `@GeneratedValue(strategy = GenerationType.IDENTITY)`는 MySQL의 `AUTO_INCREMENT`처럼 데이터베이스가 기본 키 값을 자동으로 증가시키도록 설정합니다.
2. **동작 방식**
- `Product` 객체를 생성하고 저장(`persist`)하면, `id` 값은 개발자가 직접 지정하지 않아도 데이터베이스가 자동으로 1, 2, 3… 순으로 생성합니다.
- 예를 들어, `new Product("노트북", 1500000)`을 저장하면 데이터베이스에 다음과 같은 레코드가 삽입됩니다:
```
product_id: 1, product_name: "노트북", price: 1500000
```
#### 매핑된 테이블 구조
위 엔티티에 해당하는 테이블은 다음과 같습니다:
```sql
CREATE TABLE product (
product_id BIGINT PRIMARY KEY AUTO_INCREMENT,
product_name VARCHAR(255),
price INT
);
```
#### 시퀀스 사용 예시
만약 Oracle 데이터베이스를 사용한다면 `GenerationType.SEQUENCE`를 사용할 수 있습니다. 아래는 그 예시입니다.
```java
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.SequenceGenerator;
@Entity
@Getter
@Setter
@NoArgsConstructor
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "order_seq")
@SequenceGenerator(name = "order_seq", sequenceName = "ORDER_SEQ", allocationSize = 1)
@Column(name = "order_id")
private Long id;
@Column(name = "order_amount")
private int amount;
public Order(int amount) {
this.amount = amount;
}
}
```
- **`@SequenceGenerator`**: `ORDER_SEQ`라는 데이터베이스 시퀀스를 정의합니다. `allocationSize`는 시퀀스 증가 단위를 지정합니다.
- 테이블과 별도로 `ORDER_SEQ` 시퀀스가 생성되며, `id` 값은 이 시퀀스에서 가져옵니다.
#### 선택 기준
- **`IDENTITY`**: 간단하고 성능이 좋아 MySQL 환경에서 추천됩니다.
- **`SEQUENCE`**: 대량 삽입 시 성능이 우수하며, Oracle 같은 시퀀스 지원 DB에서 적합합니다.
- **`AUTO`**: 데이터베이스에 의존하지 않고 유연하게 사용하려 할 때 유용합니다.
- **`TABLE`**: 데이터베이스 독립성을 보장해야 할 때 사용되지만, 실무에서는 드물게 선택됩니다.
#### 주의사항
- `@GeneratedValue`를 사용하지 않으면 개발자가 직접 기본 키 값을 설정해야 합니다.
- 기본 키는 `Long`, `Integer` 같은 래퍼 타입을 사용하는 것이 좋습니다. 기본형(`long`, `int`)은 0으로 초기화되므로 의도치 않은 동작을 방지할 수 있습니다.
---
기본 키 매핑은 JPA에서 데이터의 고유성을 보장하는 핵심 요소입니다. 적절한 생성 전략을 선택하면 개발 편의성과 성능을 모두 확보할 수 있습니다. 다음 장에서는 관계 매핑을 통해 엔티티 간 연결을 다뤄보겠습니다.
---
이 글이 책의 흐름에 적합한지, 추가 설명이나 수정이 필요하면 말씀해 주세요!

View File

@@ -0,0 +1,236 @@
아래는 "관계 매핑"에 대해 예시와 함께 설명하는 글입니다. 롬복을 사용해 코드를 간결하게 작성했습니다.
---
### 관계 매핑
JPA에서 관계 매핑(Relationship Mapping)은 엔티티 간의 연관성을 정의하는 방법으로, 객체지향 프로그래밍과 관계형 데이터베이스의 다리를 잇는 핵심 기능입니다. 데이터베이스에서는 외래 키(Foreign Key)를 통해 테이블 간 관계를 표현하지만, JPA에서는 어노테이션(`@OneToMany`, `@ManyToOne` 등)을 사용해 객체 간 관계를 자연스럽게 매핑합니다. 이를 통해 개발자는 SQL 조인을 직접 작성하지 않고도 연관된 데이터를 쉽게 조회하고 관리할 수 있습니다.
#### 관계 매핑의 주요 유형
JPA는 네 가지 주요 관계 매핑을 지원합니다:
1. **1:1 (One-to-One)**: 한 엔티티가 다른 엔티티와 단일 관계를 가짐.
2. **1:N (One-to-Many)**: 한 엔티티가 여러 엔티티와 관계를 가짐.
3. **N:1 (Many-to-One)**: 여러 엔티티가 한 엔티티와 관계를 가짐.
4. **N:M (Many-to-Many)**: 여러 엔티티가 서로 여러 엔티티와 관계를 가짐.
#### 주요 어노테이션
- **`@OneToMany`**: 1:N 관계를 정의.
- **`@ManyToOne`**: N:1 관계를 정의.
- **`@OneToOne`**: 1:1 관계를 정의.
- **`@ManyToMany`**: N:M 관계를 정의.
- **`@JoinColumn`**: 외래 키 컬럼을 지정.
#### 예시 1: 1:N과 N:1 관계 (팀과 회원)
아래는 `Team``Member` 엔티티 간의 1:N 및 N:1 관계를 보여주는 예시입니다.
```java
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Column;
import java.util.ArrayList;
import java.util.List;
@Entity
@Getter
@Setter
@NoArgsConstructor
public class Team {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "team_id")
private Long id;
@Column(name = "team_name")
private String name;
@OneToMany(mappedBy = "team") // 1:N 관계, Member의 team 필드에 의해 매핑
private List<Member> members = new ArrayList<>();
public Team(String name) {
this.name = name;
}
}
```
```java
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.Column;
@Entity
@Getter
@Setter
@NoArgsConstructor
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "member_id")
private Long id;
@Column(name = "username")
private String name;
@ManyToOne // N:1 관계
@JoinColumn(name = "team_id") // 외래 키 컬럼 지정
private Team team;
public Member(String name, Team team) {
this.name = name;
this.team = team;
}
}
```
#### 코드 설명
1. **`Team` (1:N)**
- `@OneToMany(mappedBy = "team")`: `Team`은 여러 `Member`를 가질 수 있으며, 관계의 주인은 `Member``team` 필드입니다. `mappedBy`는 양방향 매핑에서 사용되며, 외래 키를 직접 관리하지 않음을 나타냅니다.
2. **`Member` (N:1)**
- `@ManyToOne`: `Member`는 하나의 `Team`에 속합니다.
- `@JoinColumn(name = "team_id")`: `Member` 테이블에 `team_id`라는 외래 키 컬럼이 생성됩니다.
#### 매핑된 테이블 구조
- `Team` 테이블:
```sql
CREATE TABLE team (
team_id BIGINT PRIMARY KEY AUTO_INCREMENT,
team_name VARCHAR(255)
);
```
- `Member` 테이블:
```sql
CREATE TABLE member (
member_id BIGINT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(255),
team_id BIGINT,
FOREIGN KEY (team_id) REFERENCES team(team_id)
);
```
#### 동작 방식
- `Team team = new Team("개발팀");`
- `Member member = new Member("홍길동", team);`
- `team.getMembers().add(member);`
- 저장 시 `Member` 테이블에 `team_id`가 `Team`의 `team_id`를 참조하는 레코드가 삽입됩니다.
#### 예시 2: N:M 관계 (학생과 강의)
N:M 관계는 중간 테이블을 통해 구현됩니다.
```java
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.ManyToMany;
import jakarta.persistence.Column;
import java.util.ArrayList;
import java.util.List;
@Entity
@Getter
@Setter
@NoArgsConstructor
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "student_id")
private Long id;
@Column(name = "student_name")
private String name;
@ManyToMany
private List<Course> courses = new ArrayList<>();
public Student(String name) {
this.name = name;
}
}
```
```java
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.ManyToMany;
import jakarta.persistence.Column;
import java.util.ArrayList;
import java.util.List;
@Entity
@Getter
@Setter
@NoArgsConstructor
public class Course {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "course_id")
private Long id;
@Column(name = "course_name")
private String name;
@ManyToMany(mappedBy = "courses")
private List<Student> students = new ArrayList<>();
public Course(String name) {
this.name = name;
}
}
```
#### 코드 설명
- `@ManyToMany`: `Student`와 `Course`는 다대다 관계입니다.
- JPA는 자동으로 중간 테이블(예: `student_course`)을 생성해 외래 키를 관리합니다.
#### 매핑된 테이블 구조
- `student_course` (중간 테이블):
```sql
CREATE TABLE student_course (
student_id BIGINT,
course_id BIGINT,
FOREIGN KEY (student_id) REFERENCES student(student_id),
FOREIGN KEY (course_id) REFERENCES course(course_id)
);
```
#### 주의사항
- **양방향 매핑**: `mappedBy`를 사용해 관계의 주인을 명확히 지정해야 중복 매핑을 방지합니다.
- **지연 로딩(Lazy Loading)**: `@OneToMany`와 `@ManyToMany`는 기본적으로 지연 로딩이 적용되며, 필요 시 `fetch = FetchType.EAGER`로 즉시 로딩을 설정할 수 있습니다.
- **N:M의 한계**: 실무에서는 N:M 대신 중간 엔티티를 만들어 1:N, N:1로 분리하는 경우가 많습니다.
---
관계 매핑은 JPA의 강력한 기능으로, 객체 간 관계를 데이터베이스에 자연스럽게 반영합니다. 다음 장에서는 성능 최적화를 위한 페치 전략과 N+1 문제를 다뤄보겠습니다.
---
책의 흐름에 맞는지, 추가 예시나 설명이 필요하면 말씀해 주세요!

View File

@@ -0,0 +1,139 @@
아래는 "JPA 생명주기와 영속성 컨텍스트"에 대해 설명하는 글입니다. 예시를 포함하되, 롬복은 사용하지 않고 명확한 이해를 돕기 위해 기본적인 코드 스타일을 유지했습니다.
---
### JPA 생명주기와 영속성 컨텍스트
JPA에서 엔티티는 특정한 생명주기를 거치며 관리됩니다. 이 생명주기는 영속성 컨텍스트(Persistence Context)라는 JPA의 핵심 개념과 밀접하게 연관되어 있습니다. 영속성 컨텍스트는 엔티티의 상태를 추적하고 관리하며, 데이터베이스와의 동기화를 담당하는 환경입니다. 이를 통해 JPA는 객체와 데이터베이스 간의 매핑을 효율적으로 처리합니다.
#### 영속성 컨텍스트란?
영속성 컨텍스트는 엔티티 매니저(EntityManager)가 엔티티를 관리하는 논리적인 공간입니다. 엔티티가 영속성 컨텍스트에 포함되면 JPA가 해당 엔티티의 상태를 추적하고, 트랜잭션이 커밋될 때 데이터베이스에 반영합니다. 주요 특징은 다음과 같습니다:
- **1차 캐시**: 조회한 엔티티를 메모리에 저장해 동일 트랜잭션 내에서 재사용 가능.
- **변경 감지(Dirty Checking)**: 엔티티의 변경 사항을 자동으로 감지해 SQL을 생성.
- **쓰기 지연(Write Behind)**: 트랜잭션 커밋 시점에 변경 사항을 한꺼번에 데이터베이스에 반영.
#### 엔티티 생명주기
엔티티는 네 가지 상태를 거칩니다:
1. **비영속(Transient)**
- 엔티티 객체가 생성되었지만, 영속성 컨텍스트나 데이터베이스와 연결되지 않은 상태.
- JPA가 전혀 관리하지 않음.
2. **영속(Managed)**
- 영속성 컨텍스트에 의해 관리되는 상태.
- `persist()``find()` 메서드로 엔티티가 영속성 컨텍스트에 포함됨.
3. **준영속(Detached)**
- 영속성 컨텍스트에서 분리된 상태.
- `detach()``close()`로 인해 더 이상 관리되지 않음.
4. **삭제(Removed)**
- 영속성 컨텍스트와 데이터베이스에서 삭제되도록 예약된 상태.
- `remove()` 호출 후 트랜잭션 커밋 시 삭제됨.
#### 생명주기 예시
아래는 `Book` 엔티티를 통해 생명주기를 설명하는 코드입니다.
```java
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
@Entity
public class Book {
@Id
private Long id;
private String title;
public Book() {} // JPA 기본 생성자 요구
public Book(Long id, String title) {
this.id = id;
this.title = title;
}
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
}
```
```java
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.Persistence;
public class JpaLifecycleExample {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpaExample");
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
// 1. 비영속 상태
Book book = new Book(1L, "JPA 기초");
System.out.println("상태: 비영속");
// 2. 영속 상태
em.persist(book);
System.out.println("상태: 영속");
// 영속성 컨텍스트에서 동일 객체 재사용 (1차 캐시)
Book foundBook = em.find(Book.class, 1L);
System.out.println("동일 객체: " + (book == foundBook)); // true
// 변경 감지
book.setTitle("JPA 심화"); // 별도 update 호출 없이 변경 감지됨
// 3. 삭제 상태
em.remove(book);
System.out.println("상태: 삭제 예약");
// 트랜잭션 커밋 - 데이터베이스 반영
em.getTransaction().commit();
System.out.println("커밋 완료");
// 4. 준영속 상태
em.detach(book); // 이미 삭제되었으므로 효과 없음
em.close();
// 새로운 트랜잭션에서 준영속 상태 확인
EntityManager em2 = emf.createEntityManager();
em2.getTransaction().begin();
book.setTitle("JPA 고급"); // 영속성 컨텍스트가 없으므로 반영 안 됨
em2.getTransaction().commit();
em2.close();
emf.close();
}
}
```
#### 코드 설명
1. **비영속 상태**
- `Book book = new Book(1L, "JPA 기초");`: 객체는 생성되었지만 JPA와 무관합니다.
2. **영속 상태**
- `em.persist(book)`: 영속성 컨텍스트에 포함되며, 1차 캐시에 저장됩니다.
- `em.find(Book.class, 1L)`: 데이터베이스 조회 없이 캐시에서 반환됩니다.
3. **변경 감지**
- `book.setTitle("JPA 심화")`: 영속 상태의 엔티티는 변경이 감지되어 커밋 시 자동으로 `UPDATE` 쿼리가 실행됩니다.
4. **삭제 상태**
- `em.remove(book)`: 엔티티가 삭제 예약되며, 커밋 시 `DELETE` 쿼리가 실행됩니다.
5. **준영속 상태**
- `em.detach(book)` 또는 `em.close()`: 영속성 컨텍스트에서 분리되어 더 이상 관리되지 않습니다.
#### 영속성 컨텍스트의 이점
- **성능 향상**: 1차 캐시로 동일 트랜잭션 내 반복 조회를 줄임.
- **자동 동기화**: 변경 감지로 개발자가 직접 SQL을 작성할 필요 없음.
- **트랜잭션 보장**: 쓰기 지연으로 트랜잭션 내 변경을 일괄 처리.
#### 주의사항
- 영속성 컨텍스트는 트랜잭션 범위에 따라 생명주기가 달라집니다. 트랜잭션이 끝나면 컨텍스트도 종료됩니다(기본적으로).
- 준영속 상태의 엔티티는 변경 감지가 적용되지 않으므로, 필요 시 `merge()`로 다시 영속 상태로 전환해야 합니다.
---
JPA의 생명주기와 영속성 컨텍스트는 엔티티 관리의 핵심입니다. 이를 이해하면 데이터베이스 작업을 더 효율적으로 처리할 수 있습니다. 다음 장에서는 트랜잭션 관리와 실무 활용을 다뤄보겠습니다.
---
책의 흐름에 맞는지, 추가 예시나 설명이 필요하면 말씀해 주세요!

View File

@@ -0,0 +1,121 @@
아래는 "엔티티 설계 시 고려사항"에 대해 설명하는 글입니다. 실무에서의 경험과 JPA의 특성을 반영해 실용적인 내용을 담았습니다.
---
### 엔티티 설계 시 고려사항
JPA를 사용해 엔티티를 설계할 때는 단순히 테이블과 객체를 매핑하는 것을 넘어, 객체지향 원칙, 성능, 유지보수성, 그리고 데이터베이스의 특성을 모두 고려해야 합니다. 잘못된 설계는 코드 복잡성을 높이거나 성능 문제를 일으킬 수 있으므로, 초기 설계 단계에서 신중한 접근이 필요합니다. 아래는 엔티티 설계 시 주요 고려사항입니다.
#### 1. 객체지향 원칙 준수
엔티티는 데이터베이스 테이블뿐만 아니라 객체지향의 개념을 반영해야 합니다.
- **캡슐화**: 필드를 `private`으로 설정하고, getter/setter를 통해 접근을 제어합니다. 불필요한 setter는 제거해 데이터 무결성을 보호합니다.
- **도메인 주도 설계(DDD)**: 엔티티가 단순한 데이터 홀더가 아니라 비즈니스 로직을 포함하도록 설계합니다. 예를 들어, `Order` 엔티티에 주문 상태 변경 메서드를 추가합니다.
- **불변성**: 가능하면 생성 시점에 값을 설정하고 변경을 최소화합니다(예: `@Setter` 대신 생성자 사용).
#### 2. 식별자(기본 키) 전략
기본 키는 엔티티의 고유성을 보장하며, 설계 시 다음을 고려합니다:
- **자연 키 vs 대리 키**: 주민번호 같은 자연 키는 고유하지만 변경 가능성이 있으므로, 의미 없는 대리 키(예: `Long` 타입 ID)를 사용하는 것이 일반적입니다.
- **생성 전략**: `@GeneratedValue``IDENTITY``SEQUENCE`를 선택합니다. 대량 삽입이 많다면 `SEQUENCE`가 유리할 수 있습니다.
- **타입 선택**: `Long` 같은 래퍼 타입을 사용해 `null` 가능성을 명확히 하고, 기본형(`long`)의 0 초기화 문제를 피합니다.
#### 3. 연관관계 설계
엔티티 간 관계는 JPA의 강점 중 하나지만, 잘못 설계하면 성능 저하나 복잡성이 증가합니다.
- **단방향 vs 양방향**: 가능하면 단방향 매핑을 우선 사용합니다. 양방향은 필요 시에만 추가하고, `mappedBy`로 관계의 주인을 명확히 지정합니다.
- **지연 로딩(Lazy Loading)**: `@OneToMany``@ManyToMany`는 기본적으로 지연 로딩을 사용해 불필요한 데이터 조회를 방지합니다. 필요 시 `fetch = FetchType.EAGER`를 고려하되, N+1 문제를 주의합니다.
- **N:M 관계**: 실무에서는 중간 엔티티를 만들어 1:N, N:1로 분리하는 것이 유지보수와 확장성 측면에서 유리합니다.
#### 4. 성능 최적화
엔티티 설계가 성능에 미치는 영향은 크므로 다음을 고려합니다:
- **필드 수 최소화**: 불필요한 컬럼은 추가하지 않습니다. 예를 들어, 임시 계산 값은 엔티티에 저장하지 않고 DTO로 처리합니다.
- **인덱스 활용**: 자주 조회되는 컬럼(예: 외래 키, 검색 조건)에 `@Index`를 추가하거나 데이터베이스에 직접 설정합니다.
- **배치 크기 설정**: `@BatchSize`를 사용해 1:N 관계 조회 시 N+1 문제를 완화합니다.
#### 5. 데이터 무결성과 제약 조건
데이터베이스와 엔티티 간 일관성을 유지하려면 제약 조건을 반영해야 합니다:
- **`@Column(nullable = false)`**: 필수 입력 필드를 명시합니다.
- **`@Column(unique = true)`**: 고유 제약 조건을 설정합니다(예: 이메일).
- **Cascade 옵션**: `@OneToMany(cascade = CascadeType.ALL)`로 연관 엔티티를 자동 관리할 수 있지만, 과도한 사용은 예기치 않은 삭제를 유발할 수 있으니 주의합니다.
#### 6. 값 타입 활용
단순 데이터는 값 타입(Embedded Type)으로 설계해 재사용성과 가독성을 높입니다.
- 예: 주소(`Address`)를 별도 클래스로 분리.
```java
@Embeddable
public class Address {
private String city;
private String street;
// getter, constructor
}
@Entity
public class Member {
@Id
private Long id;
@Embedded
private Address address;
}
```
- 값 타입은 불변 객체로 설계해 부작용을 방지합니다.
#### 7. 실무적 현실성
실무에서는 이상적인 설계와 현실적 제약 사이에서 균형을 맞춰야 합니다:
- **기존 테이블 매핑**: 레거시 데이터베이스와의 호환성을 위해 테이블 구조를 그대로 반영할 수 있습니다. `@Table`과 `@Column`으로 조정합니다.
- **버전 관리**: `@Version`을 사용해 낙관적 락(Optimistic Locking)을 적용하면 동시성 문제를 방지할 수 있습니다.
- **테스트 용이성**: 엔티티가 복잡해지면 단위 테스트 작성이 어려워지므로, 단순하고 명확하게 유지합니다.
#### 8. 확장성과 유지보수성
미래의 요구사항 변화를 예측해 유연한 설계를 추구합니다:
- **상속 매핑**: `@Inheritance`로 `SINGLE_TABLE`이나 `JOINED` 전략을 사용해 엔티티 계층을 설계합니다. 예: `Item`과 하위 클래스 `Book`, `Electronics`.
- **공통 속성 추출**: `@MappedSuperclass`로 공통 필드(예: 생성일, 수정일)를 상속받아 재사용합니다.
#### 예시: 실무적 엔티티 설계
```java
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Column;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Embedded;
@Entity
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "order_id")
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id", nullable = false)
private Member member;
@Embedded
private Address deliveryAddress;
@Column(name = "order_date", nullable = false)
private LocalDateTime orderDate;
// 비즈니스 로직
public void cancel() {
if (deliveryAddress != null) {
throw new IllegalStateException("배송 중에는 취소 불가");
}
// 상태 변경 로직
}
// 생성자, getter
}
```
#### 설계 포인트
- `member`는 지연 로딩으로 성능 최적화.
- `deliveryAddress`는 값 타입으로 재사용성 확보.
- `cancel()` 메서드로 도메인 로직 포함.
---
엔티티 설계는 JPA 활용의 첫걸음이자 성공적인 프로젝트의 기반입니다. 위 고려사항을 바탕으로 객체와 데이터베이스 간 균형을 맞춘다면, 유지보수성과 성능을 모두 충족하는 설계를 완성할 수 있습니다. 다음 장에서는 연관관계 매핑 심화와 실무 패턴을 다뤄보겠습니다.
---
책의 흐름에 맞는지, 추가 내용이나 수정이 필요하면 말씀해 주세요!

View File

@@ -0,0 +1,253 @@
아래는 "상속 관계 매핑"에 대해 설명하는 글입니다. JPA의 상속 매핑 전략을 예시와 함께 다루며, 실무에서의 활용성을 고려해 작성했습니다.
---
### 상속 관계 매핑
JPA에서 상속 관계 매핑(Inheritance Mapping)은 객체지향 프로그래밍의 상속 개념을 데이터베이스 테이블에 반영하는 방법입니다. 객체지향 설계에서 부모 클래스와 자식 클래스로 계층 구조를 구성하듯, JPA는 이를 테이블 구조에 매핑해 데이터의 계층적 특성을 관리할 수 있게 합니다. 이를 위해 JPA는 세 가지 주요 전략을 제공하며, 각 전략은 장단점이 있어 상황에 맞게 선택해야 합니다.
#### 상속 매핑 전략
1. **단일 테이블 전략 (SINGLE_TABLE)**
- 모든 클래스(부모, 자식)의 데이터를 하나의 테이블에 저장합니다.
- 식별자(Discriminator Column)를 사용해 각 레코드를 구분합니다.
2. **조인 전략 (JOINED)**
- 부모 클래스와 자식 클래스 데이터를 별도의 테이블로 분리하고, 필요 시 조인으로 연결합니다.
3. **테이블 per 클래스 전략 (TABLE_PER_CLASS)**
- 각 클래스가 독립적인 테이블을 가지며, 부모 테이블 없이 자식 테이블만 존재합니다.
#### 주요 어노테이션
- **`@Inheritance`**: 상속 전략을 지정합니다. (`strategy` 속성으로 `SINGLE_TABLE`, `JOINED`, `TABLE_PER_CLASS` 설정)
- **`@DiscriminatorColumn`**: 단일 테이블 전략에서 구분 컬럼을 정의합니다.
- **`@DiscriminatorValue`**: 각 자식 클래스의 구분 값을 지정합니다.
#### 1. 단일 테이블 전략 (SINGLE_TABLE)
모든 필드가 하나의 테이블에 저장되며, 간단하고 조회 성능이 우수합니다.
```java
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Inheritance;
import jakarta.persistence.InheritanceType;
import jakarta.persistence.DiscriminatorColumn;
import jakarta.persistence.DiscriminatorValue;
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "item_type")
public abstract class Item {
@Id
private Long id;
private String name;
private int price;
// 생성자, getter, setter
public Item() {}
public Item(Long id, String name, int price) {
this.id = id;
this.name = name;
this.price = price;
}
public Long getId() { return id; }
public String getName() { return name; }
public int getPrice() { return price; }
}
@Entity
@DiscriminatorValue("BOOK")
public class Book extends Item {
private String author;
public Book() {}
public Book(Long id, String name, int price, String author) {
super(id, name, price);
this.author = author;
}
public String getAuthor() { return author; }
}
@Entity
@DiscriminatorValue("ELECTRONICS")
public class Electronics extends Item {
private String brand;
public Electronics() {}
public Electronics(Long id, String name, int price, String brand) {
super(id, name, price);
this.brand = brand;
}
public String getBrand() { return brand; }
}
```
- **테이블 구조**:
```sql
CREATE TABLE item (
id BIGINT PRIMARY KEY,
name VARCHAR(255),
price INT,
item_type VARCHAR(31), -- 구분 컬럼
author VARCHAR(255), -- Book 전용
brand VARCHAR(255) -- Electronics 전용
);
```
- **장점**: 조인 없이 단일 테이블에서 조회 가능, 성능 우수.
- **단점**: 자식 클래스 필드가 모두 포함되므로 `NULL` 값이 많아질 수 있음, 테이블 크기 증가.
#### 2. 조인 전략 (JOINED)
부모와 자식 클래스가 별도 테이블로 분리되며, 필요 시 조인합니다.
```java
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Inheritance;
import jakarta.persistence.InheritanceType;
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
public abstract class Item {
@Id
private Long id;
private String name;
private int price;
// 생성자, getter, setter (위와 동일)
}
@Entity
public class Book extends Item {
private String author;
// 생성자, getter (위와 동일)
}
@Entity
public class Electronics extends Item {
private String brand;
// 생성자, getter (위와 동일)
}
```
- **테이블 구조**:
```sql
CREATE TABLE item (
id BIGINT PRIMARY KEY,
name VARCHAR(255),
price INT
);
CREATE TABLE book (
id BIGINT PRIMARY KEY,
author VARCHAR(255),
FOREIGN KEY (id) REFERENCES item(id)
);
CREATE TABLE electronics (
id BIGINT PRIMARY KEY,
brand VARCHAR(255),
FOREIGN KEY (id) REFERENCES item(id)
);
```
- **장점**: 테이블이 정규화되어 공간 효율적, 데이터 무결성 유지 쉬움.
- **단점**: 조회 시 조인이 필요해 성능 저하 가능성 있음.
#### 3. 테이블 per 클래스 전략 (TABLE_PER_CLASS)
각 클래스가 독립적인 테이블을 가집니다.
```java
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Inheritance;
import jakarta.persistence.InheritanceType;
@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class Item {
@Id
private Long id;
private String name;
private int price;
// 생성자, getter, setter (위와 동일)
}
@Entity
public class Book extends Item {
private String author;
// 생성자, getter (위와 동일)
}
@Entity
public class Electronics extends Item {
private String brand;
// 생성자, getter (위와 동일)
}
```
- **테이블 구조**:
```sql
CREATE TABLE book (
id BIGINT PRIMARY KEY,
name VARCHAR(255),
price INT,
author VARCHAR(255)
);
CREATE TABLE electronics (
id BIGINT PRIMARY KEY,
name VARCHAR(255),
price INT,
brand VARCHAR(255)
);
```
- **장점**: 각 테이블이 독립적이어서 구조 단순.
- **단점**: 부모 클래스(`Item`)로 조회 시 UNION 쿼리가 발생해 성능 저하, 다형성 활용 어려움.
#### 선택 기준
- **`SINGLE_TABLE`**: 데이터가 많지 않고, 조회 성능이 중요한 경우 적합. 예: 소규모 상품 카탈로그.
- **`JOINED`**: 데이터 정규화와 무결성이 중요하거나, 자식 클래스의 필드가 많을 때 유리. 예: 복잡한 계층 구조.
- **`TABLE_PER_CLASS`**: 드물게 사용되며, 레거시 시스템 호환성이나 특정 요구사항에서 고려. 실무에서는 권장되지 않음.
#### 주의사항
- **기본 생성자**: JPA는 리플렉션을 사용하므로 모든 클래스에 기본 생성자가 필요합니다.
- **다형성**: `Item` 타입으로 조회 시 자식 객체가 반환되지만, `TABLE_PER_CLASS`에서는 제한적입니다.
- **Discriminator**: `SINGLE_TABLE`에서는 필수, `JOINED`에서는 선택 사항입니다.
#### 실무 예시
```java
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "vehicle_type")
public abstract class Vehicle {
@Id
private Long id;
private String manufacturer;
// 생성자, getter
}
@Entity
@DiscriminatorValue("CAR")
public class Car extends Vehicle {
private int seats;
// 생성자, getter
}
@Entity
@DiscriminatorValue("TRUCK")
public class Truck extends Vehicle {
private double loadCapacity;
// 생성자, getter
}
```
- 차량(`Vehicle`) 계층을 단일 테이블로 관리하며, `vehicle_type`으로 구분합니다.
---
상속 관계 매핑은 객체지향 설계를 데이터베이스에 반영하는 강력한 도구입니다. 프로젝트 요구사항과 성능 목표에 따라 적절한 전략을 선택하면 유연성과 효율성을 모두 확보할 수 있습니다. 다음 장에서는 값 타입과 임베디드 타입을 다뤄보겠습니다.
---
책의 흐름에 맞는지, 추가 예시나 수정이 필요하면 말씀해 주세요!

View File

@@ -0,0 +1,223 @@
아래는 "값 타입과 임베디드 타입의 활용"에 대해 설명하는 글입니다. JPA의 값 타입(Value Type)과 임베디드 타입(Embedded Type)을 실무 관점에서 다루며, 예시를 포함해 구체적으로 작성했습니다.
---
### 값 타입과 임베디드 타입의 활용
JPA에서 값 타입(Value Type)은 엔티티와 달리 독립적인 생명주기를 가지지 않고, 특정 엔티티에 속해 그 엔티티와 함께 생성되고 소멸되는 데이터입니다. 값 타입은 단순 데이터(예: 기본 타입)와 복합 데이터(임베디드 타입)로 나뉘며, 특히 임베디드 타입은 객체지향 설계의 재사용성과 가독성을 높이는 데 유용합니다. 이를 잘 활용하면 코드 중복을 줄이고 도메인 모델을 더 풍부하게 표현할 수 있습니다.
#### 값 타입의 종류
1. **기본 값 타입 (Basic Value Type)**
- 자바의 기본형(`int`, `boolean`)과 래퍼 클래스(`Integer`, `String`) 등.
- 엔티티 필드에 직접 매핑되며, 데이터베이스 컬럼에 저장됩니다.
2. **임베디드 타입 (Embedded Type)**
- 사용자 정의 클래스를 값 타입으로 사용하며, `@Embeddable``@Embedded`로 정의합니다.
- 여러 필드를 묶어 논리적 단위를 형성합니다.
3. **컬렉션 값 타입 (Collection Value Type)**
- `List`, `Set` 등으로 값 타입을 여러 개 관리합니다(여기서는 생략하고 별도 장에서 다룰 수 있음).
#### 임베디드 타입의 정의와 활용
임베디드 타입은 `@Embeddable`로 정의된 클래스를 엔티티 내에서 `@Embedded`로 사용합니다. 주로 주소, 기간, 좌표 같은 복합 데이터를 표현할 때 유용합니다.
#### 예시 1: 주소(Address) 임베디드 타입
```java
import jakarta.persistence.Embeddable;
@Embeddable
public class Address {
private String city;
private String street;
private String zipcode;
// JPA를 위한 기본 생성자
public Address() {}
public Address(String city, String street, String zipcode) {
this.city = city;
this.street = street;
this.zipcode = zipcode;
}
// getter만 제공해 불변성 보장
public String getCity() { return city; }
public String getStreet() { return street; }
public String getZipcode() { return zipcode; }
}
```
```java
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Embedded;
@Entity
public class Member {
@Id
private Long id;
private String name;
@Embedded
private Address address;
public Member() {}
public Member(Long id, String name, Address address) {
this.id = id;
this.name = name;
this.address = address;
}
// getter, setter
public Long getId() { return id; }
public String getName() { return name; }
public Address getAddress() { return address; }
public void setAddress(Address address) { this.address = address; }
}
```
- **테이블 구조**:
```sql
CREATE TABLE member (
id BIGINT PRIMARY KEY,
name VARCHAR(255),
city VARCHAR(255),
street VARCHAR(255),
zipcode VARCHAR(255)
);
```
- **설명**: `Address`는 독립적인 엔티티가 아니라 `Member`에 속한 값 타입입니다. 데이터베이스에서는 `Member` 테이블에 포함되며, 별도 테이블이 생성되지 않습니다.
#### 예시 2: 기간(Period) 임베디드 타입
```java
import jakarta.persistence.Embeddable;
import java.time.LocalDate;
@Embeddable
public class Period {
private LocalDate startDate;
private LocalDate endDate;
public Period() {}
public Period(LocalDate startDate, LocalDate endDate) {
this.startDate = startDate;
this.endDate = endDate;
}
public LocalDate getStartDate() { return startDate; }
public LocalDate getEndDate() { return endDate; }
}
```
```java
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Embedded;
@Entity
public class Project {
@Id
private Long id;
private String title;
@Embedded
private Period duration;
public Project() {}
public Project(Long id, String title, Period duration) {
this.id = id;
this.title = title;
this.duration = duration;
}
// getter, setter
}
```
- **테이블 구조**:
```sql
CREATE TABLE project (
id BIGINT PRIMARY KEY,
title VARCHAR(255),
start_date DATE,
end_date DATE
);
```
#### 임베디드 타입의 장점
1. **재사용성**: `Address`나 `Period` 같은 타입을 여러 엔티티에서 재사용 가능.
2. **가독성**: 관련 필드를 논리적으로 묶어 도메인 의미를 명확히 표현.
3. **불변성**: setter를 제거하고 생성자로만 값을 설정하면 부작용 방지.
4. **코드 간소화**: 공통 로직(예: 주소 유효성 검사)을 임베디드 타입에 추가 가능.
#### 활용 팁
- **컬럼명 커스터마이징**: 동일한 임베디드 타입을 여러 번 사용할 경우 충돌을 피하기 위해 `@AttributeOverrides`를 사용합니다.
```java
@Entity
public class Member {
@Id
private Long id;
@Embedded
private Address homeAddress;
@Embedded
@AttributeOverrides({
@AttributeOverride(name = "city", column = @Column(name = "work_city")),
@AttributeOverride(name = "street", column = @Column(name = "work_street")),
@AttributeOverride(name = "zipcode", column = @Column(name = "work_zipcode"))
})
private Address workAddress;
// 생성자, getter
}
```
- `homeAddress`와 `workAddress`가 동일 테이블에서 다른 컬럼명으로 매핑됩니다.
- **null 처리**: 임베디드 타입이 `null`이면 모든 필드가 `null`로 저장됩니다. 이를 활용해 선택적 데이터를 표현할 수 있습니다.
- **비즈니스 로직 추가**: 예를 들어, `Period`에 `isActive()` 메서드를 추가해 현재 날짜가 기간 내인지 확인 가능.
#### 주의사항
- **생명주기 의존성**: 임베디드 타입은 엔티티에 종속적이므로 독립적으로 저장하거나 조회할 수 없습니다.
- **불변성 권장**: 값 타입은 변경 시 기존 객체를 교체하는 방식으로 관리해야 부작용을 줄입니다(예: `setAddress(new Address(...))`).
- **성능 고려**: 임베디드 타입이 지나치게 복잡하면 테이블 구조가 비대해질 수 있으니 적절히 분리합니다.
#### 실무 활용 예시
주문 엔티티에서 배송 정보와 결제 정보를 임베디드 타입으로 분리:
```java
@Embeddable
public class DeliveryInfo {
private String receiver;
private String address;
// 생성자, getter
}
@Embeddable
public class PaymentInfo {
private String method;
private int amount;
// 생성자, getter
}
@Entity
public class Order {
@Id
private Long id;
@Embedded
private DeliveryInfo delivery;
@Embedded
private PaymentInfo payment;
// 생성자, getter
}
```
---
값 타입과 임베디드 타입은 JPA에서 도메인 모델을 풍부하게 만들고, 객체지향 설계를 데이터베이스에 효과적으로 반영하는 도구입니다. 이를 활용하면 코드의 응집도를 높이고 유지보수성을 강화할 수 있습니다. 다음 장에서는 성능 최적화와 관련된 주제를 다뤄보겠습니다.
---
책의 흐름에 맞는지, 추가 예시나 수정이 필요하면 말씀해 주세요!

View File

@@ -0,0 +1,167 @@
아래는 "JPA 성능 최적화"에 대해 설명하는 글입니다. 실무에서 자주 발생하는 문제와 해결 방법을 중심으로, 예시와 함께 구체적으로 작성했습니다.
---
### JPA 성능 최적화
JPA는 객체와 관계형 데이터베이스를 매핑하며 개발 생산성을 높여주지만, 잘못 사용하면 성능 저하를 초래할 수 있습니다. 성능 최적화는 불필요한 데이터베이스 호출을 줄이고, 쿼리 실행을 효율화하며, 메모리 사용을 최적화하는 데 초점을 맞춥니다. 아래는 JPA 성능 최적화의 주요 기법과 실무 적용 방법을 다룹니다.
#### 1. N+1 문제와 해결 방법
**N+1 문제**는 연관 관계를 조회할 때 발생하는 대표적인 성능 문제입니다. 예를 들어, `Team``Member`의 1:N 관계에서 모든 팀을 조회한 뒤 각 팀의 회원을 개별적으로 조회하면, 초기 쿼리 1번(N개의 `Team` 조회) + N번(`Member` 조회)이 발생합니다.
- **해결 방법 1: 페치 조인(Fetch Join)**
연관된 엔티티를 한 번의 쿼리로 함께 조회합니다.
```java
@Entity
public class Team {
@Id
private Long id;
private String name;
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
// getter
}
@Entity
public class Member {
@Id
private Long id;
private String name;
@ManyToOne
@JoinColumn(name = "team_id")
private Team team;
// getter
}
```
```java
// JPQL로 페치 조인
String jpql = "SELECT t FROM Team t JOIN FETCH t.members";
List<Team> teams = entityManager.createQuery(jpql, Team.class).getResultList();
```
- 결과: 단일 쿼리로 `Team`과 `Member`를 함께 조회(`SELECT t.*, m.* FROM team t INNER JOIN member m ON t.id = m.team_id`).
- **해결 방법 2: 배치 크기 설정 (@BatchSize)**
지연 로딩(Lazy Loading) 시 한 번에 여러 엔티티를 조회합니다.
```java
@OneToMany(mappedBy = "team")
@BatchSize(size = 100)
private List<Member> members;
```
- `size=100`으로 설정하면 100개의 `Team`에 대한 `Member`를 한 번의 IN 쿼리로 조회합니다.
#### 2. 지연 로딩(Lazy Loading) 활용
기본적으로 연관 관계는 지연 로딩으로 설정해 불필요한 데이터 조회를 방지합니다.
- `@ManyToOne`, `@OneToOne`: 기본값은 즉시 로딩(EAGER)이므로 `fetch = FetchType.LAZY`로 변경.
- `@OneToMany`, `@ManyToMany`: 기본값이 지연 로딩(LAZY)이므로 유지.
```java
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "team_id")
private Team team;
```
- **주의**: 지연 로딩 사용 시 프록시 객체가 반환되며, 트랜잭션 밖에서 접근하면 `LazyInitializationException`이 발생할 수 있음.
#### 3. 페치 조인과 일반 조인의 적절한 사용
- **페치 조인**: 연관 데이터를 즉시 로드할 때 사용. 데이터 중복이 발생할 수 있으니 페이징(`setFirstResult`, `setMaxResults`)과 함께 사용 금지.
- **일반 조인**: 필터링이나 조건에만 사용하며, 엔티티 로드는 지연 로딩에 의존.
#### 4. 읽기 전용 쿼리 설정
수정할 필요 없는 조회 쿼리는 영속성 컨텍스트의 관리 부담을 줄이기 위해 읽기 전용으로 설정합니다.
- **JPQL**:
```java
entityManager.createQuery("SELECT m FROM Member m", Member.class)
.setHint("org.hibernate.readOnly", true)
.getResultList();
```
- **효과**: 변경 감지(Dirty Checking)와 스냅샷 생성이 생략되어 메모리와 성능 개선.
#### 5. 배치 처리와 대량 데이터 관리
대량 데이터를 삽입/수정/삭제할 때는 JPA의 기본 동작(영속성 컨텍스트 사용)이 비효율적일 수 있습니다.
- **방법**: JDBC 배치나 벌크 연산 사용.
```java
// 벌크 업데이트
String jpql = "UPDATE Member m SET m.age = m.age + 1 WHERE m.team.id = :teamId";
entityManager.createQuery(jpql)
.setParameter("teamId", teamId)
.executeUpdate();
entityManager.clear(); // 영속성 컨텍스트 초기화 필수
```
- **주의**: 벌크 연산은 영속성 컨텍스트를 무시하므로, 이후 작업 전에 `clear()`나 `refresh()`로 동기화 필요.
#### 6. 캐시 활용
JPA는 1차 캐시와 2차 캐시를 제공하며, 이를 활용해 성능을 높일 수 있습니다.
- **1차 캐시**: 트랜잭션 내에서 동일 엔티티를 재조회할 때 데이터베이스 접근을 줄임(기본 제공).
- **2차 캐시**: 애플리케이션 범위에서 캐시를 공유. Hibernate 사용 시 `@Cacheable`과 캐시 제공자(Ehcache, Redis 등)를 설정.
```java
@Entity
@Cacheable
@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class Product {
@Id
private Long id;
private String name;
// getter
}
```
- **적용 시점**: 자주 조회되고 변경이 드문 데이터(예: 카테고리 목록).
#### 7. 쿼리 최적화
- **필요한 필드만 조회**: 엔티티 전체 대신 DTO로 필요한 데이터만 조회.
```java
public class MemberDto {
private Long id;
private String name;
public MemberDto(Long id, String name) {
this.id = id;
this.name = name;
}
// getter
}
List<MemberDto> result = entityManager.createQuery(
"SELECT new com.example.MemberDto(m.id, m.name) FROM Member m", MemberDto.class)
.getResultList();
```
- **인덱스 활용**: 자주 검색되는 컬럼에 인덱스 추가(데이터베이스 레벨 또는 `@Index`).
#### 8. 트랜잭션 범위 최소화
영속성 컨텍스트는 트랜잭션 범위에서 동작하므로, 불필요하게 긴 트랜잭션은 메모리 사용과 성능 저하를 유발합니다.
- **방법**: 비즈니스 로직을 간결히 하고, 읽기 전용 작업은 트랜잭션 없이 처리(`@Transactional(readOnly = true)`).
#### 실무 예시
```java
@Repository
public class MemberRepository {
@PersistenceContext
private EntityManager em;
@Transactional(readOnly = true)
public List<Member> findMembersWithTeam() {
return em.createQuery("SELECT m FROM Member m JOIN FETCH m.team", Member.class)
.setHint("org.hibernate.readOnly", true)
.getResultList();
}
@Transactional
public void bulkUpdateAge(Long teamId) {
em.createQuery("UPDATE Member m SET m.age = m.age + 1 WHERE m.team.id = :teamId")
.setParameter("teamId", teamId)
.executeUpdate();
em.clear();
}
}
```
#### 주의사항
- **N+1 디버깅**: 쿼리 로그를 활성화해(`spring.jpa.show-sql=true`) 발생 여부 확인.
- **캐시 동기화**: 2차 캐시 사용 시 데이터 정합성을 유지하기 위해 적절한 전략 선택.
- **테스트**: 성능 개선 후 부작용(예: 데이터 불일치)을 검증.
---
JPA 성능 최적화는 애플리케이션 규모와 요구사항에 따라 달라지며, 문제 원인을 분석하고 적절한 기법을 적용하는 것이 중요합니다. 다음 장에서는 트랜잭션 관리와 실무 활용을 다뤄보겠습니다.
---
책의 흐름에 맞는지, 추가 내용이나 수정이 필요하면 말씀해 주세요!

107
docs/jpa/12_jpa_auditing.md Normal file
View File

@@ -0,0 +1,107 @@
JPA Auditing에 대해 설명하자면, 이는 Java Persistence API(JPA)를 사용해 데이터베이스 엔티티의 생성 및 수정과 같은 이벤트를 자동으로 추적하고 기록하는 기능입니다. 주로 애플리케이션에서 데이터의 변경 이력을 관리하거나, 누가 언제 데이터를 생성하거나 수정했는지와 같은 메타데이터를 유지할 때 유용합니다. Spring Data JPA에서는 이 기능을 간편하게 구현할 수 있도록 지원합니다.
### JPA Auditing의 핵심 개념
JPA Auditing은 엔티티의 생명주기 이벤트(예: 생성, 수정)를 감지해 특정 필드에 값을 자동으로 채워주는 메커니즘을 제공합니다. 이를 통해 개발자가 수동으로 매번 값을 설정하지 않아도 되므로 코드가 간결해지고 실수를 줄일 수 있습니다. 대표적으로 사용되는 어노테이션은 다음과 같습니다:
- `@CreatedDate`: 엔티티가 처음 생성된 날짜와 시간을 기록합니다.
- `@LastModifiedDate`: 엔티티가 마지막으로 수정된 날짜와 시간을 기록합니다。
- `@CreatedBy`: 엔티티를 생성한 사용자를 기록합니다.
- `@LastModifiedBy`: 엔티티를 마지막으로 수정한 사용자를 기록합니다.
이 어노테이션들은 Spring Data JPA에서 제공되며, Auditing 기능을 활성화해야 동작합니다.
### 설정 방법
JPA Auditing을 사용하려면 몇 가지 설정이 필요합니다. Spring Boot 환경을 기준으로 설명하겠습니다.
1. **Auditing 활성화**
Spring Boot 애플리케이션에서 `@EnableJpaAuditing` 어노테이션을 설정 클래스(예: `@Configuration`이 붙은 클래스)나 메인 애플리케이션 클래스에 추가합니다.
```java
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
@Configuration
@EnableJpaAuditing
public class JpaConfig {
}
```
2. **Auditable 엔티티 정의**
엔티티 클래스에 Auditing 관련 필드와 어노테이션을 추가합니다. 보통 공통 속성을 추상 클래스로 만들어 재사용합니다.
```java
import jakarta.persistence.EntityListeners;
import jakarta.persistence.MappedSuperclass;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import java.time.LocalDateTime;
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseEntity {
@CreatedDate
private LocalDateTime createdDate;
@LastModifiedDate
private LocalDateTime lastModifiedDate;
// Getter, Setter
}
```
- `@MappedSuperclass`: 이 클래스를 상속받는 엔티티에 필드가 포함되도록 합니다.
- `@EntityListeners(AuditingEntityListener.class)`: Auditing 기능을 수행하는 리스너를 지정합니다.
3. **실제 엔티티에서 상속**
```java
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
@Entity
public class User extends BaseEntity {
@Id
private Long id;
private String name;
// Getter, Setter
}
```
4. **생성자/수정자 정보 추가 (선택)**
`@CreatedBy`, `@LastModifiedBy`를 사용하려면 현재 사용자 정보를 제공하는 `AuditorAware` 인터페이스를 구현해야 합니다.
```java
import org.springframework.data.domain.AuditorAware;
import java.util.Optional;
public class AuditorAwareImpl implements AuditorAware<String> {
@Override
public Optional<String> getCurrentAuditor() {
// 현재 로그인한 사용자 정보를 반환 (예: Spring Security 사용 시)
return Optional.of("admin");
}
}
```
그리고 이 빈을 등록합니다:
```java
@Bean
public AuditorAware<String> auditorProvider() {
return new AuditorAwareImpl();
}
```
### 동작 원리
- 엔티티가 저장(`persist`)되거나 수정(`merge`)될 때, `AuditingEntityListener`가 동작합니다.
- `@CreatedDate`와 `@CreatedBy`는 엔티티가 처음 저장될 때만 값이 설정되고, 이후에는 변경되지 않습니다.
- `@LastModifiedDate`와 `@LastModifiedBy`는 엔티티가 수정될 때마다 갱신됩니다.
### 장점
- **코드 간소화**: 반복적인 날짜/사용자 설정 코드를 제거합니다.
- **일관성 유지**: 모든 엔티티에 동일한 방식으로 이력을 관리할 수 있습니다.
- **유지보수 용이**: 공통 로직을 한 곳에서 관리하므로 변경이 쉬워집니다.
### 주의점
- **시간대 설정**: `@CreatedDate`, `@LastModifiedDate`에 사용할 시간 타입(예: `LocalDateTime`)과 애플리케이션의 시간대 설정을 일치시켜야 합니다.
- **AuditorAware 구현**: 사용자 정보를 동적으로 가져오려면 보안 컨텍스트(예: Spring Security)와 연동하는 것이 일반적입니다.
JPA Auditing은 특히 대규모 프로젝트에서 데이터 변경 추적을 체계적으로 관리할 때 빛을 발합니다. Spring Data JPA의 강력한 기능 중 하나로, 적절히 활용하면 개발 생산성을 크게 높일 수 있습니다!

178
docs/jpa/13_paging.md Normal file
View File

@@ -0,0 +1,178 @@
JPA에서 페이지 처리는 대량의 데이터를 효율적으로 조회하고 사용자에게 필요한 만큼만 보여주기 위해 사용되는 기능입니다. 특히 웹 애플리케이션에서 목록 조회 시, 모든 데이터를 한 번에 가져오는 대신 페이징을 통해 데이터를 분할해서 처리하는 것이 일반적입니다. Spring Data JPA는 이를 간편하게 구현할 수 있도록 `PagingAndSortingRepository``Pageable` 인터페이스를 제공합니다. 아래에서 JPA의 페이지 처리에 대해 자세히 설명하겠습니다.
---
### 페이지 처리란?
페이지 처리는 데이터베이스에서 조회한 결과를 작은 단위(페이지)로 나누어 반환하는 기법입니다. 예를 들어, 게시판에 1,000개의 글이 있다면 한 페이지에 10개씩 보여주고, 사용자가 원하는 페이지를 선택해 해당 데이터만 조회합니다. 이를 통해 성능 최적화와 사용자 경험 개선을 동시에 달성할 수 있습니다.
---
### Spring Data JPA에서의 페이지 처리
Spring Data JPA는 페이지 처리를 위해 다음과 같은 주요 구성 요소를 제공합니다:
1. **Pageable 인터페이스**
- 페이지 번호, 페이지 크기, 정렬 조건 등을 정의합니다.
- `PageRequest` 클래스를 통해 구현체를 생성합니다.
- 예: `PageRequest.of(0, 10)`는 첫 번째 페이지(0부터 시작)에 10개의 데이터를 요청합니다.
2. **Page<T> 반환 타입**
- 조회된 데이터와 함께 페이징 관련 메타데이터(총 페이지 수, 총 데이터 수 등)를 제공합니다.
- 주요 메서드:
- `getContent()`: 현재 페이지의 데이터 목록
- `getTotalElements()`: 전체 데이터 개수
- `getTotalPages()`: 전체 페이지 수
- `getNumber()`: 현재 페이지 번호
3. **PagingAndSortingRepository**
- Spring Data JPA에서 제공하는 기본 레포지토리 인터페이스로, 페이징과 정렬 기능을 지원합니다.
- `findAll(Pageable pageable)` 메서드를 통해 페이징된 데이터를 조회할 수 있습니다.
---
### 페이지 처리 구현 방법
Spring Boot와 Spring Data JPA를 기준으로 페이지 처리 구현 과정을 단계별로 설명하겠습니다.
#### 1. 엔티티와 레포지토리 정의
```java
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
@Entity
public class Post {
@Id
private Long id;
private String title;
private String content;
// Getter, Setter
}
```
```java
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
public interface PostRepository extends JpaRepository<Post, Long> {
Page<Post> findAll(Pageable pageable); // 기본 제공 메서드와 동일
}
```
#### 2. 서비스 계층에서 페이지 처리
```java
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Service;
@Service
public class PostService {
private final PostRepository postRepository;
public PostService(PostRepository postRepository) {
this.postRepository = postRepository;
}
public Page<Post> getPosts(int page, int size) {
Pageable pageable = PageRequest.of(page, size); // 페이지 번호(0부터), 페이지 크기
return postRepository.findAll(pageable);
}
}
```
#### 3. 컨트롤러에서 결과 반환
```java
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.data.domain.Page;
@RestController
public class PostController {
private final PostService postService;
public PostController(PostService postService) {
this.postService = postService;
}
@GetMapping("/posts")
public Page<Post> getPosts(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size) {
return postService.getPosts(page, size);
}
}
```
#### 4. 실행 결과
- 요청: `GET /posts?page=0&size=10`
- 응답: JSON 형태로 현재 페이지의 데이터와 페이징 정보가 반환됩니다.
```json
{
"content": [
{"id": 1, "title": "Post 1", "content": "Content 1"},
{"id": 2, "title": "Post 2", "content": "Content 2"},
...
],
"pageable": {
"pageNumber": 0,
"pageSize": 10,
...
},
"totalElements": 100,
"totalPages": 10,
"number": 0
}
```
---
### 정렬 추가하기
`Pageable`은 정렬(Sorting)도 지원합니다. 예를 들어, 제목 기준으로 오름차순 정렬을 추가하려면:
```java
Pageable pageable = PageRequest.of(0, 10, Sort.by("title").ascending());
Page<Post> posts = postRepository.findAll(pageable);
```
컨트롤러에서 요청 파라미터로 정렬 조건을 받을 수도 있습니다:
```java
@GetMapping("/posts")
public Page<Post> getPosts(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(defaultValue = "title") String sortBy) {
Pageable pageable = PageRequest.of(page, size, Sort.by(sortBy).ascending());
return postService.getPosts(pageable);
}
```
---
### 커스텀 쿼리와 페이지 처리
특정 조건으로 데이터를 조회하면서 페이징을 적용하려면 `@Query` 어노테이션이나 메서드 이름을 사용합니다.
```java
public interface PostRepository extends JpaRepository<Post, Long> {
Page<Post> findByTitleContaining(String keyword, Pageable pageable);
@Query("SELECT p FROM Post p WHERE p.content LIKE %:keyword%")
Page<Post> searchByContent(@Param("keyword") String keyword, Pageable pageable);
}
```
---
### 장점
- **성능 최적화**: 필요한 데이터만 조회하므로 메모리와 DB 부하를 줄입니다.
- **사용자 경험 개선**: 대량 데이터를 한 번에 로드하지 않고, 페이지 단위로 제공합니다.
- **유연성**: 정렬, 필터링 등과 결합해 다양한 요구사항을 처리할 수 있습니다.
### 주의점
- **페이지 번호**: Spring Data JPA는 페이지 번호가 0부터 시작합니다(예: 0은 첫 페이지).
- **쿼리 최적화**: 페이징 쿼리는 `OFFSET`과 `LIMIT`을 사용하므로, 데이터가 많아질수록 성능 저하가 발생할 수 있습니다. 이 경우 인덱스 사용이나 커서 기반 페이징을 고려해야 합니다.
- **총 개수 계산**: `Page` 객체는 `totalElements`를 계산하기 위해 별도의 `COUNT` 쿼리를 실행합니다. 성능에 민감한 경우 `Slice`를 사용하는 것도 대안입니다(`totalElements`와 `totalPages`를 제공하지 않음).
---
### 결론
JPA의 페이지 처리는 Spring Data JPA를 통해 매우 간단하면서도 강력하게 구현할 수 있습니다. `Pageable`과 `Page`를 활용하면 데이터 조회를 효율적으로 관리하고, 웹 애플리케이션에서 필수적인 페이징 기능을 손쉽게 제공할 수 있습니다. 대규모 데이터를 다룰 때 필수적인 이 기능을 잘 활용하면 성능과 사용자 경험 모두를 만족시킬 수 있습니다!

72
docs/jpa/README.md Normal file
View File

@@ -0,0 +1,72 @@
스프링부트의 JPA에 관한 책을 위한 목차를 제안해 드리겠습니다. 이 목차는 초보자부터 중급 개발자까지 이해할 수 있도록 기본 개념부터 실무 활용까지 다루며, 체계적인 학습 흐름을 고려했습니다.
---
### 목차: 스프링부트 JPA 완벽 가이드
#### 1장. JPA와 스프링부트 소개
- 1.1 ORM과 JPA란 무엇인가?
- 1.2 스프링부트와 JPA의 만남
- 1.3 이 책의 목표와 구성
#### 2장. 개발 환경 설정
- 2.1 스프링부트 프로젝트 생성
- 2.2 JPA 의존성 추가
- 2.3 데이터베이스 설정 (H2, MySQL 등)
- 2.4 기본 애플리케이션 설정과 테스트
#### 3장. JPA 기초
- 3.1 엔티티와 테이블 매핑
- 3.2 기본 키 매핑 전략
- 3.3 관계 매핑의 이해 (1:1, 1:N, N:M)
- 3.4 JPA의 생명주기와 영속성 컨텍스트
#### 4장. 스프링 데이터 JPA
- 4.1 스프링 데이터 JPA란?
- 4.2 Repository 인터페이스 활용
- 4.3 쿼리 메서드 작성법
- 4.4 @Query 어노테이션과 커스텀 쿼리
#### 5장. 엔티티 설계와 매핑
- 5.1 엔티티 설계 시 고려사항
- 5.2 연관관계 매핑 심화
- 5.3 상속 관계 매핑 (SINGLE_TABLE, JOINED 등)
- 5.4 값 타입과 임베디드 타입 활용
#### 6장. JPA 성능 최적화
- 6.1 N+1 문제와 해결 방법
- 6.2 페치 조인(Fetch Join)과 지연 로딩(Lazy Loading)
- 6.3 배치 처리와 대량 데이터 관리
- 6.4 캐시 활용 (1차 캐시, 2차 캐시)
#### 7장. 트랜잭션 관리
- 7.1 트랜잭션의 기본 개념
- 7.2 스프링부트에서 트랜잭션 설정
- 7.3 @Transactional 어노테이션 활용
- 7.4 트랜잭션 롤백과 예외 처리
#### 8장. 실무에서의 JPA
- 8.1 스프링부트와 JPA로 REST API 구축
- 8.2 DTO와 엔티티 분리 전략
- 8.3 데이터베이스 마이그레이션 (Flyway, Liquibase)
- 8.4 테스트 코드 작성 (단위 테스트, 통합 테스트)
#### 9장. 고급 주제
- 9.1 JPA와 QueryDSL 통합
- 9.2 멀티 데이터소스 설정
- 9.3 스프링부트와 JPA의 이벤트 처리
- 9.4 JPA Auditing으로 엔티티 관리
#### 10장. 문제 해결과 모범 사례
- 10.1 흔한 JPA 실수와 디버깅
- 10.2 실무에서 유용한 설계 패턴
- 10.3 성능 모니터링과 튜닝 팁
#### 부록
- 부록 A. JPA 관련 주요 어노테이션 정리
- 부록 B. 샘플 프로젝트 코드
- 부록 C. 추가 학습 자료 및 참고 문헌
---
이 목차는 JPA의 기초부터 실무 적용까지 자연스럽게 이어지도록 설계되었습니다. 필요에 따라 특정 장을 더 세분화하거나, 독자 타겟(초급/중급)에 맞춰 내용을 조정할 수 있습니다. 추가로 강조하고 싶은 주제나 방향성이 있다면 말씀해 주세요!