์ํฐํฐ ๋งคํ, ์ฐ๊ด๊ด๊ณ, ์์์ฑ ์ปจํ
์คํธ, N+1 ํด๊ฒฐ(Fetch Join/Batch Size), Spring Data JPA ๋ฐ ์ค๋ฌด ์ฑ๋ฅ ์ต์ ํ ๋ ํผ๋ฐ์ค
## 1. ์ํฐํฐ ๊ธฐ๋ณธ ๋งคํ (Entity Mapping)
```java
@Entity
@Table(name = "users", indexes = {
@Index(name = "idx_user_email", columnList = "email", unique = true)
})
@NoArgsConstructor(access = AccessLevel.PROTECTED) // ๊ธฐ๋ณธ ์์ฑ์ ํ์ (JPA ํ๋ก์์ฉ)
@Getter
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY) // MySQL: AUTO_INCREMENT
private Long id;
@Column(nullable = false, length = 50)
private String name;
@Column(unique = true, updatable = false) // ์์ฑ ํ ๋ณ๊ฒฝ ๋ถ๊ฐ
private String email;
@Enumerated(EnumType.STRING) // ๋ฐ๋์ STRING ์ฌ์ฉ! (ORDINAL ์ ๋ ๊ธ์ง)
private RoleType role;
@Lob // ๋์ฉ๋ ํ
์คํธ (CLOB / BLOB)
private String description;
@Transient // DB ์ปฌ๋ผ ๋งคํ ์ ์ธ (์ธ๋ฉ๋ชจ๋ฆฌ ์์ ๊ณ์ฐ์ฉ)
private boolean isTemporary;
}
```
## 2. ์ฐ๊ด๊ด๊ณ ๋งคํ (Associations: 1:N, N:1, 1:1)
```java
// [๋ค๋์ผ N:1] - ํญ์ ์ฐ๊ด๊ด๊ณ์ ์ฃผ์ธ(FK ๋ณด์ ํ
์ด๋ธ)์ผ๋ก ์ค์
@Entity
public class OrderItem {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// ์ค๋ฌด ์์น: ๋ชจ๋ ์ฐ๊ด๊ด๊ณ๋ LAZY(์ง์ฐ ๋ก๋ฉ)๋ก ์ค์ !
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "order_id") // ์ธ๋ํค ์ปฌ๋ผ๋ช
์ง์
private Order order;
}
// [์ผ๋๋ค 1:N] - ์๋ฐฉํฅ ๋งคํ ์ mappedBy๋ก ๊ฑฐ์ธ ์ค์ (์ฐ๊ด๊ด๊ณ ์ฃผ์ธ X)
@Entity
public class Order {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
private List<OrderItem> orderItems = new ArrayList<>();
// ์ฐ๊ด๊ด๊ณ ํธ์ ๋ฉ์๋ (๊ฐ์ฒด ์๋ฐฉํฅ ์ํ ๋๊ธฐํ)
public void addOrderItem(OrderItem item) {
orderItems.add(item);
item.setOrder(this);
}
}
```
## 3. ์์์ฑ ์ปจํ
์คํธ & ๋ผ์ดํ์ฌ์ดํด (Persistence Context)
```text
[์์์ฑ ์ปจํ
์คํธ์ 4๊ฐ์ง ์ํ]
1. ๋น์์ (New/Transient) : ์ํฐํฐ ๊ฐ์ฒด๋ง ์์ฑ, ์์์ฑ ์ปจํ
์คํธ์ ๋ฌด๊ด (new Member())
2. ์์ (Managed) : em.persist(), find()๋ก ์ปจํ
์คํธ์ ์ ์ฅ ๋ฐ ๊ด๋ฆฌ๋จ
3. ์ค์์ (Detached) : ์ปจํ
์คํธ์์ ๋ถ๋ฆฌ๋์ด ์์์ฑ ๊ธฐ๋ฅ ๋ฏธ์๋ (em.detach(), em.clear())
4. ์ญ์ (Removed) : ์ญ์ ๋๊ธฐ ์ํ (em.remove())
[์์์ฑ ์ปจํ
์คํธ์ ํต์ฌ ์ด์ ]
โข 1์ฐจ ์บ์ : ๋์ผ ํธ๋์ญ์
๋ด ์๋ณ์(@Id) ๊ธฐ์ค ์กฐํ ์ ์ฟผ๋ฆฌ ์๋ต
โข ๋์ผ์ฑ(Identity) ๋ณด์ฅ : a == b ํญ์ true (๋์ผ @Id ๊ฐ์ฒด)
โข ์ฐ๊ธฐ ์ง์ฐ (Write-behind): ์ปค๋ฐ ์์ ๊น์ง INSERT/UPDATE SQL์ ํ์ ๋ชจ์ ์ผ๊ด ์คํ
โข ๋ณ๊ฒฝ ๊ฐ์ง (Dirty Checking): ์ค๋
์ท ๋น๊ต ํ ์ํฐํฐ ์์ ์ ์ปค๋ฐ ์์ ์ ์๋์ผ๋ก UPDATE ์คํ
```
```java
// ์์์ฑ ์ปจํ
์คํธ ์๋ ์กฐ์
em.persist(user); // ๋น์์ โ ์์
em.flush(); // ์ฐ๊ธฐ ์ง์ฐ ์ ์ฅ์์ SQL์ DB์ ์ฆ์ ์ ์ก (๋๊ธฐํ)
em.clear(); // 1์ฐจ ์บ์ ์ ์ฒด ๋น์ฐ๊ธฐ (ํ
์คํธ ๋ฐ ๋๋ ์์
์ ํ์)
```
## 4. ์ง์ฐ ๋ก๋ฉ & N+1 ๋ฌธ์ ํด๊ฒฐ (Lazy vs Eager & Fetch Join)
```text
[N+1 ๋ฐ์ ์์ธ]
๋ฃจํธ ์ฟผ๋ฆฌ 1๊ฑด ์กฐํ ํ, ๋ฃจํ๋ฅผ ๋๋ฉฐ ์ฐ๊ด ์ํฐํฐ ์กฐํ๋ฅผ ์ํด N๋ฒ์ ์ถ๊ฐ ์ฟผ๋ฆฌ๊ฐ ๋ฐ์ํ๋ ํ์
์ฃผ์: @ManyToOne, @OneToOne ๊ธฐ๋ณธ๊ฐ์ EAGER(์ฆ์ ๋ก๋ฉ)์ด๋ฏ๋ก ๋ช
์์ ์ผ๋ก LAZY๋ก ๋ณ๊ฒฝ ํ์!
```
```java
// ํด๊ฒฐ์ฑ
1: JPQL ํ์น ์กฐ์ธ (Fetch Join) - 1ํ inner/left join์ผ๋ก ํ๋ฒ์ ๋ก๋ฉ
@Query("SELECT o FROM Order o JOIN FETCH o.member JOIN FETCH o.delivery")
List<Order> findAllWithMemberAndDelivery();
// ํด๊ฒฐ์ฑ
2: @EntityGraph (์คํ๋ง ๋ฐ์ดํฐ JPA ์ ์ฉ ๊ฐํธ ํ์น ์กฐ์ธ)
@EntityGraph(attributePaths = {"member", "delivery"})
@Query("SELECT o FROM Order o")
List<Order> findAllOrders();
// ํด๊ฒฐ์ฑ
3: ํ์ด์ง ์ ์ปฌ๋ ์
(@OneToMany) ํ์น ์กฐ์ธ ํ๊ณ ๊ทน๋ณต -> default_batch_fetch_size
// application.yml ์ค์ :
// spring.jpa.properties.hibernate.default_batch_fetch_size: 100
// ์ปฌ๋ ์
์กฐํ ์ WHERE parent_id IN (?, ?, ...) ์ฟผ๋ฆฌ๋ก N+1์ 1+1๋ก ์ถ์!
```
## 5. Spring Data JPA Repository ํต์ฌ ์ฟผ๋ฆฌ
```java
public interface UserRepository extends JpaRepository<User, Long> {
// 1. ๋ฉ์๋ ์ด๋ฆ ๊ธฐ๋ฐ ์๋ ์ฟผ๋ฆฌ ์์ฑ
List<User> findByNameAndStatus(String name, UserStatus status);
List<User> findByNameContainingIgnoreCase(String keyword);
boolean existsByEmail(String email);
long countByRole(RoleType role);
// 2. @Query (์ ์ JPQL ์์ฑ ๋ฐ DTO ์ง์ ์กฐํ)
@Query("SELECT u FROM User u WHERE u.role = :role ORDER BY u.createdAt DESC")
List<User> findActiveUsers(@Param("role") RoleType role);
// 3. ๋ฒํฌ ์์ /์ญ์ ์ฐ์ฐ (@Modifying ํ์, 1์ฐจ ์บ์ ์ ๋ฆฌ ์ต์
)
@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query("UPDATE User u SET u.point = u.point + :bonus WHERE u.role = :role")
int giveBonusPoints(@Param("bonus") int bonus, @Param("role") RoleType role);
// 4. ํ์ด์ง ๋ฐ ์ฌ๋ผ์ด์ค (Pageable ์ ๋ฌ)
Page<User> findByStatus(UserStatus status, Pageable pageable);
Slice<User> findByRole(RoleType role, Pageable pageable); // ๋ฌดํ ์คํฌ๋กค์ฉ (totalCount ์ฟผ๋ฆฌ ์๋ต)
}
```
## 6. DTO ํ๋ก์ ์
(DTO Projection & Record)
```java
// 1. Java Record ๊ธฐ๋ฐ ํ๋ก์ ์
DTO
public record UserSummaryDto(Long id, String name, String email) {}
// 2. Repository์์ new ์์ฑ์ ์์ ์ฌ์ฉํ ์ง์ ์กฐํ (์ํฐํฐ ์์ํ ์ค๋ฒํค๋ X)
public interface UserRepository extends JpaRepository<User, Long> {
@Query("SELECT new com.example.dto.UserSummaryDto(u.id, u.name, u.email) " +
"FROM User u WHERE u.status = :status")
List<UserSummaryDto> findSummaryByStatus(@Param("status") UserStatus status);
}
// 3. ์ธํฐํ์ด์ค ๊ธฐ๋ฐ Closed ํ๋ก์ ์
(์คํ๋ง์ด ํ๋ก์ ๊ฐ์ฒด ์๋ ์์ฑ)
public interface UserSummary {
Long getId();
String getName();
}
```
## 7. ๊ณตํต ๋งคํ (@MappedSuperclass & @Embedded)
```java
// 1. ๋ฑ๋ก/์์ ์ผ ๊ณตํต ์ํฐํฐ (Auditing ํ์ฑํ)
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
@Getter
public abstract class BaseTimeEntity {
@CreatedDate
@Column(updatable = false)
private LocalDateTime createdAt;
@LastModifiedDate
private LocalDateTime updatedAt;
}
// 2. ๊ฐ ํ์
์๋ฒ ๋๋ (Embedded Value Type - ๋ถ๋ณ ๊ฐ์ฒด ์ค๊ณ)
@Embeddable
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class Address {
private String city;
private String street;
private String zipcode;
}
// ์ฌ์ฉ์ฒ:
@Embedded
private Address deliveryAddress;
```
## 8. ๋์์ฑ ์ ์ด (Optimistic vs Pessimistic Lock)
```java
// 1. ๋๊ด์ ๋ฝ (Optimistic Lock) - ์ถฉ๋ ๋น๋๊ฐ ๋ฎ์ ๋ ๋ฒ์ ํ๋๋ก ๋์์ฑ ๊ฒ์ฆ
@Entity
public class Product {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Version // ์์ ์ ๋ฒ์ ๋ถ์ผ์น ๋ฐ์ํ๋ฉด OptimisticLockException ๋ฐ์
private Long version;
}
// 2. ๋น๊ด์ ๋ฝ (Pessimistic Lock) - ์ถฉ๋์ด ์ฆ๊ฑฐ๋ ์ฌ๊ณ ์ฐจ๊ฐ ๋ฑ ์ฆ์ DB ๋ฝ ํ์ ์
public interface ProductRepository extends JpaRepository<Product, Long> {
// SELECT ... FOR UPDATE ์คํ
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT p FROM Product p WHERE p.id = :id")
Optional<Product> findByIdWithPessimisticLock(@Param("id") Long id);
}
```
## 9. JPA ํธ๋์ญ์
& OSIV ์ค์
```yaml
# application.yml ๊ถ์ฅ ์ค๋ฌด ์ค์
spring:
jpa:
open-in-view: false # [์ค์] OSIV ๋นํ์ฑํ: DB ์ปค๋ฅ์
๋ญ๋น ๋ฐฉ์ง (Service ๊ณ์ธต๊น์ง๋ง ์์์ฑ ์ ์ง)
hibernate:
ddl-auto: validate # ๋ก์ปฌ: update/create, ์ด์/์คํ
์ด์ง: ๋ฐ๋์ validate ๋๋ none
properties:
hibernate:
format_sql: true
show_sql: false # p6spy ๋๋ ๋ก๊น
๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ฌ์ฉ ๊ถ์ฅ
default_batch_fetch_size: 100 # N+1 ์ํ์ฉ IN ์ฟผ๋ฆฌ ๋ฐฐ์น ์ฌ์ด์ฆ
```
```java
// ์ฝ๊ธฐ ์ ์ฉ ํธ๋์ญ์
์ต์ ํ
@Transactional(readOnly = true)
public List<UserResponse> getUsers() {
// ์ค๋
์ท์ ์์ฑํ์ง ์์ ๋ฉ๋ชจ๋ฆฌ ์ ์ฝ, flush๊ฐ ์คํต๋์ด ์ฑ๋ฅ ๋ํญ ํฅ์!
return userRepository.findAll().stream().map(UserResponse::from).toList();
}
```
## 10. ์ค๋ฌด ํธ๋ฌ๋ธ์ํ
& ํต์ฌ ์ฃผ์์ฌํญ
```text
1. MultipleBagFetchException
- ํ๋์ JPQL ํ์น ์กฐ์ธ์์ ๋ ์ด์์ @OneToMany List ์ปฌ๋ ์
์ ๋์ fetch join ์ ๋ฐ์
- ํด๊ฒฐ: ํ๋๋ง fetch joinํ๊ณ ๋๋จธ์ง๋ default_batch_fetch_size๋ก ๊ฐ์ ธ์ค๊ฑฐ๋ Set์ผ๋ก ๋ณ๊ฒฝ
2. ์ปฌ๋ ์
ํ์น ์กฐ์ธ ์ Paging ๊ธ์ง (HHH000104 ๊ฒฝ๊ณ )
- 1:N ๊ด๊ณ๋ฅผ fetch join ํ๋ฉด์ Pageable์ ๋๊ธฐ๋ฉด ๋ชจ๋ ๋ ์ฝ๋๋ฅผ ๋ฉ๋ชจ๋ฆฌ์ ํผ์ฌ๋ ค ํ์ด์ง ์ํ(OOM ๋ฐ์ ์ํ)
- ํด๊ฒฐ: XToOne ๊ด๊ณ๋ง fetch joinํ๊ณ ์ปฌ๋ ์
์ batch_fetch_size๋ก ์ง์ฐ ๋ก๋ฉ
3. MySQL GenerationType.IDENTITY ์ saveAll() ์ฐ๊ธฐ ์ง์ฐ ๋นํ์ฑํ
- IDENTITY ์ ๋ต์ PK ์์ฑ์ DB์ ์์ํ๋ฏ๋ก persist() ์ฆ์ INSERT ์ฟผ๋ฆฌ๊ฐ ์คํ๋์ด JDBC ๋ฐฐ์น ์ธ์ํธ ๋ถ๊ฐ
- ๋๋ ๋ฐ์ดํฐ(Bulk Insert)๋ JdbcTemplate.batchUpdate() ์ฌ์ฉ ๊ถ์ฅ
4. ์์์ฑ ์ ์ด(Cascade) ๋จ์ฉ ์ฃผ์
- ๋จ์ผ ๋ถ๋ชจ ์ํฐํฐ๊ฐ ์์์ ๋ผ์ดํ์ฌ์ดํด์ ์์ ํ ๊ด๋ฆฌํ ๋๋ง CascadeType.ALL, orphanRemoval = true ์ ์ฉ
```
์๊ฒฌ ๋ฐ ์ง๋ฌธ
0์์ง ๋ฑ๋ก๋ ์๊ฒฌ์ด ์์ต๋๋ค. ์ฒซ ๋ฒ์งธ ๋๊ธ์ ๋จ๊ฒจ๋ณด์ธ์!
๋๊ธ ์์
๋๊ธ ์ญ์