DB Lock 이란?

📌 DB Lock은 여러 트랜잭션이 동시에 같은 자원에 접근할 때 데이터 무결성(정합성)을 보장하기 위해 사용되는 메커니즘

  • 쉽게 말해, 어떤 사용자가 데이터를 사용하고 있는 동안에는 다른 사용자가 그 데이터를 동시에 수정하면 안 되기 때문에, ‘잠금’을 걸어서 충돌(Conflict)을 방지
  • 여러 사용자가 동시에 DB를 사용하는 경우 같은 데이터를 동시에 수정하려고 시도하는 경우 충돌 제어 및 데이터의 일관성을 보장하기 위해 사용한다.

 

언제 Lock을 사용하는가?

  • 동시에 읽기/쓰기가 빈번하게 일어나는 중요한 테이블에 대해, 데이터 무결성을 엄격히 보장해야 할 때.
  • 은행 이체, 재고 관리, 주문 처리와 같이 동시에 발생하면 안 되는 시나리오를 제어할 때.

  • 불필요한 락은 성능 저하를 유발할 수 있으므로, 적절한 수준에서 사용하는 것이 중요
  • DeadLock발생 여부를 확인해야함!

간단히 말해서 에러를 순환하는 상태가 지속된다.

 

Lock의 생명주기

1 트랜잭션 시작!

  • DB와의 트랜잭션을 시작
  • 아직 특정 자원에 대한 락이 획득된 상태는 🙅

2 Lock 획득!

  1. 쿼리 요청
  2. Lock 체크
  3. 대기 혹은 Lock 획득

3 트랜잭션 종료!

  1. 트랜잭션 Commit 혹은 RollBack
  2. 락 해제

 

Lock 종류

1. 낙관적 락 (충돌이 발생하지 않을 것이라고 가정)

  • 데이터베이스의 락을 사용하는 것이 아닌 application 레벨에서 버전 관리
  • 특정 작업을 수행하기 전에 별도의 락(lock)을 걸지 않고, 작업 완료 시점에서 데이터의 변경 여부를 확인하여 충돌 여부를 판단 (@Version 활용)
  • 데이터 충돌이 거의 없을것이라고 가정한 경우 사용
    • LockModeType.OPTIMISTIC로 적용
  • 충돌 시 ObjectOptimisticLockingFailureException 발생

 

2. 비관적 락 (충돌이 자주 발생할 것이라고 가정)

2.1 공유락(Shared Lock, S Lock)

  • 여러 트랜잭션이 동시에 데이터를 읽기할 수 있지만, 쓰기 하려면 공유락을 해제하고 배타락으로 변경
  • 다른 트랜잭션이 쓰기하려 하면 대기 상태
    • LockModeType.PESSIMISTIC_READ로 적용

2.2 배타락(Exclusive Lock, X Lock)

  • 오직 한 트랜잭션만 해당 데이터를 읽거나 쓸 수 있음
  • 다른 트랜잭션이 접근하려 하면 대기 상태
    • LockModeType.PESSIMISTIC_WRITE로 적용

 

3. 분산 락

  • 여러 인스턴스나 분산 환경에서 락을 설정
  • 데이터베이스에 직접 Lock을 걸지 않고, 외부에서 권한을 받아 처리
  • Redis, **Zookeeper 등... 을 활용하여 적용**

 

낙관적 락 (Optimistic Lock)

 

1. 낙관적 락이란?

  • 데이터의 충돌이 드물다고 가정하고, 충돌이 발생했을 때만 문제를 해결하는 방식.
  • 일반적으로 버전 번호(version)를 활용하여 데이터를 갱신할 때 충돌을 감지.

2. 특징

  1. 비교적 낮은 충돌 가능성 전제:
    • 동시에 데이터를 갱신하는 상황이 드물거나, 충돌 가능성이 낮은 환경에서 효과적.
  2. 병렬성 보장:
    • 별도의 잠금(lock) 없이 데이터를 읽고, 이후 업데이트 시점에서만 충돌 여부를 확인.
  3. 성능 우선:
    • 자원을 차단하지 않기 때문에, 읽기 작업과 병렬 처리에서 높은 성능을 보임.

3. 작동 방식

  1. 데이터 조회:
    • 데이터와 함께 버전 번호를 가져옴.
  2. 수정 및 업데이트 요청:
    • 데이터를 수정한 후, 저장 요청 시 버전 번호를 포함.
  3. 버전 검증:
    • 데이터베이스에서 현재 데이터의 버전 번호와 요청에 포함된 버전 번호를 비교.
  4. 성공/실패 여부:
    • 동일하면 업데이트 성공 → 버전 번호 증가.
    • 다르면 충돌 발생으로 간주하고 예외 처리.

4. 예제

4.1 데이터베이스 테이블 예제

CREATE TABLE example_table (
    id BIGINT PRIMARY KEY,
    data VARCHAR(255),
    version INT NOT NULL
);

4.2 JPA와 낙관적 락

import jakarta.persistence.*;

@Entity
public class ExampleEntity {

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

    private String data;

    @Version // 버전 관리용 필드
    private Integer version;

    // getters and setters
}

4.3 낙관적 락 업데이트 코드

import org.springframework.stereotype.Service;

import jakarta.persistence.EntityManager;
import jakarta.persistence.OptimisticLockException;

@Service
public class ExampleService {

    private final EntityManager entityManager;

    public ExampleService(EntityManager entityManager) {
        this.entityManager = entityManager;
    }

    public void updateData(Long id, String newData) {
        try {
            ExampleEntity entity = entityManager.find(ExampleEntity.class, id);
            entity.setData(newData);
            entityManager.merge(entity);
        } catch (OptimisticLockException e) {
            System.out.println("낙관적 락 충돌 발생!");
            // 충돌 처리 로직 추가
        }
    }
}

5. 장단점

장점 단점
병렬성 보장: 별도의 잠금 없이 병렬 작업 가능. 충돌이 빈번하면 롤백 비용 증가.
성능 최적화: 리소스를 차단하지 않으므로 성능 향상. 충돌 발생 시 복구 로직 필요.
충돌이 적은 환경에서 매우 적합. 충돌 발생 시 사용자가 직접 재시도하거나 해결해야 함.
데이터 읽기와 병렬 처리 시 높은 효율성. 데이터 변경이 많은 환경에서는 비효율적일 수 있음.

6. 낙관적 락 vs 비관적 락

  낙관적 락 (Optimistic Lock)   비관적 락 (Pessimistic Lock)
충돌 가능성 낮은 환경에서 적합. 높은 환경에서 적합.
잠금 방식 잠금 사용 안 함 (버전 번호로 충돌 감지). 데이터 읽기 시점에서 데이터에 잠금 적용.
성능 충돌 가능성이 낮으면 성능 우수. 잠금으로 인해 동시 처리량 감소.
복구 비용 충돌 발생 시 롤백과 재시도 비용 발생. 잠금으로 인해 충돌 방지 가능하나 대기 시간 증가.
사용 예시 읽기 작업이 많고 쓰기 작업이 적은 환경. 동시에 여러 사용자가 쓰기 작업을 자주 수행하는 환경.

7. 사용 사례

  1. 읽기 작업이 많은 환경:
    • 예: 상품 상세 페이지의 재고 확인.
  2. 충돌 가능성이 낮은 환경:
    • 예: 사용자 개인 설정 업데이트.
  3. 트랜잭션이 짧은 작업:
    • 예: 간단한 상태 변경 작업.

8. 학습하면 좋은 추가 주제

  1. JPA의 @Version 어노테이션 활용:
    • 실무에서의 구체적 활용 방법과 사례.
  2. 비관적 락(Pessimistic Lock):
    • 낙관적 락과 비교되는 개념 학습.
  3. 분산 시스템에서의 락 구현:
    • Redis나 Zookeeper를 활용한 분산 락 구현.
  4. 데드락과 충돌 처리 전략:
    • 충돌 및 데드락 방지를 위한 설계 패턴.

 

 

비관적 락 (Pessimistic Lock)

 

1. 비관적 락이란?

  • 데이터 충돌이 자주 발생할 것으로 예상되는 환경에서, 데이터를 수정하거나 읽기 전에 잠금을 걸어 충돌을 방지하는 방법.
  • 트랜잭션 동안 다른 트랜잭션이 데이터에 접근하지 못하도록 차단.
  • 주로 데이터베이스에서 제공하는 락 메커니즘(예: SELECT ... FOR UPDATE)을 사용.

2. 특징

  1. 잠금을 통해 충돌 방지:
    • 데이터 수정 전 잠금을 걸어 다른 트랜잭션의 접근을 차단.
  2. 강력한 일관성 보장:
    • 동시에 같은 데이터를 수정하려는 작업 간 충돌을 완전히 방지.
  3. 성능 비용 증가:
    • 트랜잭션이 오래 지속되면 잠금으로 인해 다른 트랜잭션이 대기 상태가 됨.

3. 작동 방식

  1. 데이터 조회 시 잠금:
    • 데이터를 읽으면서 **배타적 잠금(Exclusive Lock)**을 설정.
  2. 잠금 해제:
    • 트랜잭션이 종료될 때 잠금 해제.
  3. 다른 트랜잭션 처리:
    • 잠금이 해제될 때까지 다른 트랜잭션은 대기 상태.

4. 비관적 락 구현 방법

4.1 데이터베이스에서 직접 사용

  • SQL 예제:
-- 데이터 수정 전 잠금 설정
SELECT * FROM example_table
WHERE id = 1
FOR UPDATE;

4.2 JPA에서 비관적 락

  • JPA 예제:
import jakarta.persistence.LockModeType;
import jakarta.persistence.EntityManager;

public ExampleEntity findByIdWithLock(Long id, EntityManager entityManager) {
    return entityManager.find(ExampleEntity.class, id, LockModeType.PESSIMISTIC_WRITE);
}
  • Spring Data JPA 사용:
@Repository
public interface ExampleRepository extends JpaRepository<ExampleEntity, Long> {

    @Lock(LockModeType.PESSIMISTIC_WRITE)
    @Query("SELECT e FROM ExampleEntity e WHERE e.id = :id")
    ExampleEntity findByIdWithLock(@Param("id") Long id);
}

4.3 LockModeType 종류

LockModeType 설명
PESSIMISTIC_READ 데이터를 읽는 동안 다른 트랜잭션에서 수정하지 못하도록 잠금.
PESSIMISTIC_WRITE 데이터를 수정하거나 읽는 동안 다른 트랜잭션에서 수정/읽기 모두 차단.
PESSIMISTIC_FORCE_INCREMENT 데이터 수정 여부와 관계없이 버전 번호를 강제로 증가시키는 잠금.

5. 장단점

장점 단점
일관성 보장: 데이터 충돌을 확실히 방지. 성능 저하: 트랜잭션 대기 시간이 늘어나 성능 저하 가능.
단순한 구현: 잠금 메커니즘을 사용하여 충돌 방지 로직 간소화. 데드락 위험: 여러 트랜잭션이 서로 대기하면서 데드락 발생 가능.
변경이 빈번한 환경에 적합. 병렬 처리 저하: 동시 처리 성능 감소.

6. 낙관적 락 vs 비관적 락 비교

  낙관적 락 (Optimistic Lock)  비관적 락 (Pessimistic Lock)
충돌 가능성 충돌이 낮은 환경에 적합. 충돌이 잦은 환경에 적합.
잠금 방식 잠금을 사용하지 않음, 버전 번호로 충돌 감지. 데이터 접근 시점에 잠금을 사용해 다른 트랜잭션 차단.
성능 낮은 충돌 환경에서는 성능 우수. 잠금으로 인해 동시 처리 성능 저하.
충돌 처리 비용 충돌 발생 시 롤백 비용 발생. 충돌 발생이 없으므로 롤백 비용이 없음.
데드락 가능성 없음. 있음 (잠금으로 인해 데드락 발생 가능).
적용 사례 읽기 작업이 많고 쓰기 작업이 적은 환경. 쓰기 작업이 잦고 데이터 충돌 가능성이 높은 환경.

7. 사용 사례

  1. 은행 시스템:
    • 계좌 잔액 수정처럼 데이터의 정확성이 중요한 작업.
  2. 재고 관리:
    • 재고 수량을 줄이거나 늘릴 때 다른 트랜잭션이 동일 데이터를 수정하지 못하도록 차단.
  3. 예약 시스템:
    • 좌석 예약, 호텔 방 예약 등에서 동일 리소스를 중복 예약하지 않도록 잠금.

8. 학습하면 좋은 추가 주제

  1. 트랜잭션 격리 수준:
    • READ UNCOMMITTED, READ COMMITTED, REPEATABLE READ, SERIALIZABLE의 차이.
  2. 데드락 방지 전략:
    • 트랜잭션 순서 지정, 타임아웃 설정 등.
  3. 낙관적 락과 비관적 락의 적절한 사용 전략:
    • 각 락의 장단점을 고려한 실무 적용 사례 분석.
  4. 분산 환경에서의 락 관리:
    • Redis, ZooKeeper 등 분산 락 시스템 학습.

 

분산 락 (Distributed Lock)

1. 분산 락이란?

  • 분산 시스템 환경에서 공유 자원에 대한 동시 접근을 제어하기 위해 사용하는 락 메커니즘.
  • 동시에 여러 노드(서버)가 공유 자원에 접근하려는 상황에서 데이터의 무결성과 일관성을 보장.

2. 특징

  1. 멀티 노드 환경:
    • 여러 노드가 동일한 자원에 접근 가능.
    • 분산 락을 통해 자원을 독점적으로 사용할 노드를 제어.
  2. 데이터 일관성 보장:
    • 데이터의 충돌 방지 및 무결성 유지.
  3. 중앙 관리:
    • 락 상태를 중앙에서 관리하거나 분산 환경에서 합의 프로토콜을 사용.

3. 동작 원리

  1. 락 생성:
    • 특정 자원에 대해 락 생성 요청.
    • 락이 성공하면 해당 노드는 자원을 독점적으로 사용 가능.
  2. 락 유지:
    • 락을 일정 시간 동안 유지.
    • 필요 시 갱신(Renewal) 가능.
  3. 락 해제:
    • 자원 사용이 끝나면 락을 해제.
    • 타임아웃으로 자동 해제될 수도 있음.

4. 분산 락 구현 방식

4.1 데이터베이스 기반

  • 방법: 데이터베이스 테이블에 락 상태를 저장.
  • SQL 예제:
-- 락 생성
INSERT INTO distributed_lock (resource, lock_time) VALUES ('RESOURCE_ID', NOW());

-- 락 확인
SELECT * FROM distributed_lock WHERE resource = 'RESOURCE_ID';

-- 락 해제
DELETE FROM distributed_lock WHERE resource = 'RESOURCE_ID';
  • 장점:
    • 별도 도구 없이 쉽게 구현 가능.
    • 트랜잭션과 연동하여 락 관리.
  • 단점:
    • 성능 저하(데이터베이스 부하 증가).
    • 대규모 시스템에서는 비효율적.

4.2 Redis 기반

  • 방법: Redis의 SETNX 명령어를 사용하여 락 생성.
  • 예제:
# 락 생성
SET resource:lock "LOCKED" NX PX 10000

# 락 확인
GET resource:lock

# 락 해제
DEL resource:lock
  • 장점:
    • 빠른 속도.
    • TTL(Time-To-Live) 설정으로 자동 해제 가능.
  • 단점:
    • 단일 노드 Redis 사용 시 SPOF(Single Point of Failure) 발생 가능.
    • 클러스터 환경에서는 Redlock 알고리즘 필요.

4.3 ZooKeeper 기반

  • 방법: 분산 락의 상태를 ZNode에 저장하여 관리.
  • 예제:
# ZNode 생성 (락 획득)
create /locks/resource

# 락 해제
delete /locks/resource
  • 장점:
    • 강력한 일관성 보장.
    • 분산 환경에 최적화.
  • 단점:
    • 설정 및 관리 복잡.
    • ZooKeeper 설치 필요.

5. Redlock 알고리즘 (Redis 기반 분산 락)

  • Redis 클러스터 환경에서 락의 일관성을 보장하는 알고리즘.
  1. 여러 Redis 노드에 동일한 키로 락 생성 요청.
  2. 과반수 이상의 노드에서 락 성공 시 락 획득.
  3. TTL을 설정하여 자동으로 락이 해제되도록 설정.
  4. 락 해제 시 모든 노드에서 락 해제.

6. 장단점

장점 단점
데이터 충돌 및 중복 작업 방지. 락 구현 및 관리 복잡도 증가.
고성능 분산 환경에서 데이터 무결성 보장. 락 생성/해제 시 네트워크 지연으로 인한 성능 저하 가능.
다양한 도구를 활용한 구현 가능(데이터베이스, Redis, ZooKeeper 등). 잘못된 락 설정 시 데드락 발생 가능.

7. 분산 락 사용 사례

  1. 동시에 한 번만 실행해야 하는 작업:
    • 예: 배치 처리, 데이터 마이그레이션.
  2. 중복 실행 방지:
    • 예: 주문 처리, 결제 처리.
  3. 리소스 공유:
    • 예: 파일 업로드/다운로드, 재고 관리.

8. 학습하면 좋은 추가 주제

  1. 락 컨텐츠 관리 전략:
    • TTL 설정, 자동 해제 로직 구현.
  2. ZooKeeper와 Redis의 비교:
    • 각 기술의 장단점 및 사용 사례 분석.
  3. 분산 시스템에서의 일관성 모델:
    • 강한 일관성, 최종적 일관성(Final Consistency) 개념 이해.
  4. Redlock 알고리즘 심화:
    • 클러스터 환경에서의 락 일관성 보장 방법.

 

 

격리 수준

 

트랜잭션 격리 수준 (Transaction Isolation Levels)


1. 트랜잭션 격리 수준이란?

  • 트랜잭션 간의 상호작용에서 발생할 수 있는 데이터 충돌 문제를 방지하기 위해 데이터베이스가 제공하는 동시성 제어 메커니즘.
  • 각 격리 수준은 데이터 일관성과 동시성 성능 간의 트레이드오프를 제공.

2. 주요 격리 수준과 특징

격리 수준 Dirty Read Non-Repeatable Read Phantom Read 특징
READ UNCOMMITTED 허용 허용 허용 - 커밋되지 않은 데이터를 읽을 수 있음(Dirty Read). - 가장 낮은 격리 수준으로 동시성 성능은 높지만 데이터 일관성이 낮음.
READ COMMITTED 방지 허용 허용 - 커밋된 데이터만 읽을 수 있음. - Oracle, SQL Server의 기본 격리 수준. - Non-Repeatable Read 가능.
REPEATABLE READ 방지 방지 허용 - 동일 트랜잭션 내에서 항상 같은 데이터 읽기 보장. - MySQL(InnoDB)의 기본 격리 수준.
SERIALIZABLE 방지 방지 방지 - 모든 트랜잭션을 순차적으로 실행하는 것처럼 동작. - 가장 높은 격리 수준으로, 동시성 성능이 낮지만 데이터 일관성 보장.

3. 주요 개념

  1. Dirty Read:
    • 다른 트랜잭션이 아직 커밋되지 않은 변경 사항을 읽는 것.
    • 예시: A 트랜잭션이 데이터를 수정하고 커밋하지 않은 상태에서 B 트랜잭션이 수정된 데이터를 읽음.
  2. Non-Repeatable Read:
    • 한 트랜잭션에서 같은 데이터를 두 번 읽었을 때, 값이 달라지는 현상.
    • 원인: 다른 트랜잭션에서 데이터를 수정하고 커밋한 경우.
  3. Phantom Read:
    • 한 트랜잭션에서 동일한 조건으로 데이터를 조회했을 때, 새로운 데이터가 추가되거나 삭제되는 현상.
    • 원인: 다른 트랜잭션에서 데이터를 추가/삭제한 경우.

4. 격리 수준별 예제

4.1 READ UNCOMMITTED

-- A 트랜잭션
START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;

-- B 트랜잭션
START TRANSACTION;
SELECT balance FROM accounts WHERE id = 1; -- Dirty Read 발생

4.2 READ COMMITTED

-- A 트랜잭션
START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;

-- B 트랜잭션
START TRANSACTION;
SELECT balance FROM accounts WHERE id = 1; -- 커밋되지 않았으므로 읽을 수 없음

4.3 REPEATABLE READ

-- A 트랜잭션
START TRANSACTION;
SELECT balance FROM accounts WHERE id = 1; -- 1000
UPDATE accounts SET balance = balance - 100 WHERE id = 1;

-- B 트랜잭션
START TRANSACTION;
SELECT balance FROM accounts WHERE id = 1; -- A 트랜잭션의 변경 사항 보이지 않음

4.4 SERIALIZABLE

-- A 트랜잭션
START TRANSACTION;
SELECT balance FROM accounts WHERE id = 1;

-- B 트랜잭션
START TRANSACTION;
SELECT balance FROM accounts WHERE id = 1; -- 대기 상태 (A 트랜잭션 종료 후 실행)

5. 장단점 요약

격리 수준 장점  단점
READ UNCOMMITTED 높은 동시성, 성능 우수 데이터 무결성 저하, Dirty Read 발생 가능
READ COMMITTED Dirty Read 방지 Non-Repeatable Read 발생 가능
REPEATABLE READ Non-Repeatable Read 방지 Phantom Read 발생 가능
SERIALIZABLE 데이터 일관성 보장, Phantom Read 방지 성능 저하, 동시성 처리 어려움

6. 사용 사례

  1. READ UNCOMMITTED:
    • 로그 데이터 분석, 캐시 처리 등 데이터 일관성이 덜 중요한 경우.
  2. READ COMMITTED:
    • 일반적인 OLTP 시스템에서 기본 격리 수준으로 적합.
  3. REPEATABLE READ:
    • 은행 시스템, 재고 관리 등 데이터 수정이 빈번한 환경.
  4. SERIALIZABLE:
    • 강력한 일관성이 필요한 환경. 예: 금융 거래 시스템, 예약 시스템.

7. 학습하면 좋은 추가 주제

  1. 트랜잭션 관리:
    • Spring에서의 트랜잭션 관리 (@Transactional)와 격리 수준 설정.
  2. 트랜잭션과 락:
    • 비관적 락과 낙관적 락의 적용 방식.
  3. MVCC (Multi-Version Concurrency Control):
    • MySQL, PostgreSQL에서 사용되는 동시성 제어 방식.
  4. 데드락 방지 전략:
    • 순서 지정, 타임아웃 설정 등을 통한 데드락 방지.

 

JPA 간단 정리

더보기

JPA 요약 정리


1. JPA란?

  • Java Persistence API: 자바 애플리케이션과 데이터베이스 간 매핑 및 상호작용을 위한 표준 인터페이스.
  • 주요 역할:
    • 자바 객체와 데이터베이스 테이블 간 자동 매핑.
    • SQL 작성 없이 객체 중심으로 데이터 처리.

2. JPA를 사용하는 이유

이유 설명
SQL 대신 자바 코드로 작업 복잡한 SQL 대신 간단한 자바 코드로 데이터 저장 및 조회 가능.
객체 지향적 데이터 처리 데이터베이스 테이블을 객체로 변환하여 컬렉션처럼 다룰 수 있음.
유지보수성 향상 데이터베이스 구조 변경 시 엔티티 수정만으로 코드 유지보수 가능.
데이터베이스 독립성 특정 데이터베이스(MySQL, Oracle 등)에 종속되지 않음.
비즈니스 로직에 집중 가능 CRUD 작업 자동화로 반복적인 SQL 작업 제거.

3. JPA에서의 연관관계

연관관계 설명 예제
1 : 1 한 테이블의 행이 다른 테이블의 하나의 행과만 연결됨. 사용자(User) ↔ 사용자 상세(UserDetail)
1 : N 한 테이블의 행이 다른 테이블의 여러 행과 연결됨. 회원(Member) ↔ 주문(Order)
N : M 한 테이블의 여러 행이 다른 테이블의 여러 행과 연결됨. 학생(Student) ↔ 강의(Course) (중간 테이블 필요)

4. Fetch Type (로딩 전략)

Fetch Type 특징 기본값
Lazy 연관 데이터를 실제로 사용하는 시점에 조회. @OneToMany, @ManyToMany
Eager 엔티티 조회 시 연관 데이터를 즉시 조회. @ManyToOne, @OneToOne

5. JPA 코드 예제

@Entity
@Getter
@Setter
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @OneToMany(mappedBy = "member", fetch = FetchType.LAZY) // Lazy 로딩
    private List<Order> orders = new ArrayList<>();
}

@Entity
@Getter
@Setter
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String product;

    @ManyToOne(fetch = FetchType.EAGER) // Eager 로딩
    @JoinColumn(name = "member_id")
    private Member member;
}

요약

JPA의 정의 자바 애플리케이션과 데이터베이스 간 매핑 및 상호작용을 위한 표준 인터페이스.
사용 이유 SQL 없이 자바 코드로 데이터 처리, 유지보수 용이, 데이터베이스 독립성 보장.
연관관계 1:1, 1:N, N:M 관계를 객체로 매핑 가능.
Fetch Type 연관 데이터 로딩 시점을 제어하는 전략. Lazy(지연 로딩), Eager(즉시 로딩).

장점

  1. 코드 간결화: 객체 중심 개발로 SQL 관리 부담 감소.
  2. 생산성 향상: 반복 작업 제거로 개발 속도 증가.
  3. 데이터베이스 독립성: 특정 DBMS에 종속되지 않음.

 

N + 1 문제란 무엇인가

 

1. N + 1 문제란?

  • 정의:
    • 데이터베이스에서 부모-자식 관계 데이터를 가져올 때, 비효율적으로 많은 쿼리가 실행되는 문제.
    • 주로 지연 로딩(FetchType.LAZY)을 사용하는 상황에서 발생.
  • N + 1의 의미:
    • 1: 부모 데이터를 조회하는 1개의 쿼리.
    • N: 부모마다 자식 데이터를 조회하기 위한 N개의 쿼리.

2. N + 1 문제 예시

상황 설명

  • 데이터베이스에 부모 테이블(Parent)과 자식 테이블(Child)이 있고, 한 부모는 여러 자식을 가질 수 있음.
  • 목표: 모든 부모와 관련된 자식 데이터를 조회.

테이블 구조

  • Parent 테이블
id name
1 엄마
2 아빠
  • Child 테이블
id parent_id name
1 1
2 1 아들
3 2 막내딸

1. 기본 쿼리 흐름

-- 부모 데이터를 조회 (1번 실행)
SELECT * FROM parent;

-- 부모 각각에 대해 자식 데이터를 조회 (N번 실행)
SELECT * FROM child WHERE parent_id = 1;
SELECT * FROM child WHERE parent_id = 2;

2. 실행 결과

  • 부모가 100명이라면:
    • 1번 부모 조회 쿼리 실행.
    • 100번 자식 조회 쿼리 실행.
    • 총 101번의 쿼리 실행.

3. N + 1 문제 확인 방법

Spring Boot에서 SQL 로그 활성화

spring:
  jpa:
    properties:
      hibernate:
        show_sql: true     # 실행 쿼리 표시
        format_sql: true   # 쿼리 포맷팅

SQL 로그 예시

-- 부모 조회
SELECT * FROM parent;

-- 부모별 자식 데이터 조회
SELECT * FROM child WHERE parent_id = 1;
SELECT * FROM child WHERE parent_id = 2;

4. 왜 N + 1 문제가 문제인가?

문제점 설명
성능 저하 필요 이상으로 많은 쿼리가 실행되어 애플리케이션이 느려짐.
데이터베이스 부하 증가 트래픽이 많은 환경에서는 데이터베이스에 큰 부담을 줄 수 있음.
비효율적인 리소스 사용 동일한 데이터를 반복적으로 조회하므로 CPU, 메모리 등의 리소스가 낭비됨.

 

즉시 로딩 & 지연 로딩

더보기

1. 즉시 로딩 (Eager Loading)

  • 정의:
    • 엔티티를 조회할 때 연관된 엔티티를 즉시 함께 조회하는 로딩 방식.
    • 연관 데이터를 미리 가져와서 사용할 준비를 함.
  • 특징:
    • FetchType.EAGER를 설정하면 즉시 로딩이 적용됨.
    • 부모 엔티티와 자식 엔티티가 JOIN 쿼리로 한 번에 조회됨.
    • 모든 연관 데이터를 항상 가져오므로 필요하지 않은 데이터까지 로드될 수 있음.
  • 장점:
    • 데이터를 미리 가져오므로 추가적인 데이터베이스 쿼리가 발생하지 않음.
    • 애플리케이션에서 데이터를 바로 사용할 수 있어 편리.
  • 단점:
    • 불필요한 데이터까지 로드될 수 있어 성능 저하가 발생.
    • 연관된 데이터가 많거나 관계가 깊을 경우 쿼리가 복잡해지고 메모리 사용량 증가.

즉시 로딩 예제

@Entity
public class Parent {
    @Id
    @GeneratedValue
    private Long id;

    private String name;

    @OneToMany(mappedBy = "parent", fetch = FetchType.EAGER) // 즉시 로딩 설정
    private List<Child> children;
}

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

    private String name;

    @ManyToOne
    @JoinColumn(name = "parent_id")
    private Parent parent;
}

// 실행
List<Parent> parents = parentRepository.findAll(); // 부모와 자식을 함께 로드

실행 쿼리

SELECT p.*, c.*
FROM parent p
LEFT JOIN child c
ON p.id = c.parent_id; -- 부모와 자식 데이터를 한 번의 쿼리로 로드

2. 지연 로딩 (Lazy Loading)

  • 정의:
    • 엔티티를 조회할 때 연관된 엔티티를 필요한 시점에 조회하는 로딩 방식.
    • 데이터를 사용할 때 별도의 쿼리가 실행되어 로드됨.
  • 특징:
    • FetchType.LAZY를 설정하면 지연 로딩이 적용됨.
    • 부모 엔티티를 조회할 때 자식 엔티티는 바로 로드되지 않고, Proxy 객체로 대체.
    • 자식 데이터가 실제로 필요할 때 데이터베이스 쿼리가 실행.
  • 장점:
    • 필요한 데이터만 로드하므로 메모리 사용량 감소.
    • 연관 데이터가 많거나 사용 빈도가 낮은 경우 성능 최적화 가능.
  • 단점:
    • 데이터가 필요할 때마다 추가적인 쿼리 발생.
    • 여러 연관 데이터가 필요한 경우 N+1 문제가 발생할 수 있음.

지연 로딩 예제

@Entity
public class Parent {
    @Id
    @GeneratedValue
    private Long id;

    private String name;

    @OneToMany(mappedBy = "parent", fetch = FetchType.LAZY) // 지연 로딩 설정
    private List<Child> children;
}

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

    private String name;

    @ManyToOne
    @JoinColumn(name = "parent_id")
    private Parent parent;
}

// 실행
List<Parent> parents = parentRepository.findAll(); // 부모만 로드
for (Parent parent : parents) {
    System.out.println(parent.getChildren()); // 자식 데이터를 사용할 때 쿼리 실행
}

실행 쿼리

  1. 부모 조회:
    SELECT * FROM parent;
    
  2. 자식 조회 (사용 시):
    SELECT * FROM child WHERE parent_id = 1;
    SELECT * FROM child WHERE parent_id = 2;
    

3. 즉시 로딩 vs 지연 로딩 비교

  즉시 로딩 (Eager Loading)  지연 로딩 (Lazy Loading)
데이터 로드 시점 부모 엔티티를 조회할 때, 연관 데이터도 함께 로드. 연관 데이터는 실제로 접근할 때 로드.
쿼리 실행 방식 부모와 자식을 JOIN 쿼리로 한 번에 조회. 부모를 조회 후, 연관 데이터는 개별 쿼리로 조회.
성능 적은 쿼리로 모든 데이터를 가져오지만, 불필요한 데이터까지 로드. 필요한 데이터만 가져오므로 메모리 사용량 감소.
N+1 문제 발생 여부 없음. 연관 데이터 사용 시 N+1 문제 발생 가능.
적합한 상황 - 자식 데이터가 항상 필요한 경우- 간단한 관계 - 자식 데이터가 드물게 필요- 대량 데이터 처리 시
설정 방식 fetch = FetchType.EAGER fetch = FetchType.LAZY

4. 결론

  • 즉시 로딩 (Eager):
    • 연관 데이터를 항상 함께 로드해야 하는 경우 사용.
    • 간단한 관계에 적합하지만, 관계가 복잡하거나 대량 데이터를 처리할 때는 성능 저하 가능.
  • 지연 로딩 (Lazy):
    • 연관 데이터를 사용할 일이 적거나, 데이터가 많아 메모리 효율성을 고려해야 하는 경우 적합.
    • N+1 문제가 발생할 수 있으므로 Fetch Join, Batch Size 등 최적화 방법을 함께 적용해야 함.

 



N + 1 문제 해결 방법

5.1 FetchType.EAGER로 변경

해결 원리

  • 부모 데이터를 불러올 시에 자식 데이터도 한번에 다 '즉시' 전부 불러옴, 기존의 지연 로딩의 경우 필요한 경우에 불러오기 때문에 데이터를 다시 요청해야하지만, 즉시 로딩의 경우 이미 데이터를 다 불러왔기에 더이상 불러올 필요가 없다.
더보기

FetchType.EAGER로 N+1 문제 해결이 되는 이유


1. N+1 문제의 원인

  • FetchType.LAZY (지연 로딩):
    • 부모 데이터를 조회한 후, 각 부모의 자식 데이터를 별도의 쿼리로 가져옴.
    • 즉, 부모 1번 조회 + 자식 N번 조회 → N+1 쿼리 발생.
    @OneToMany(mappedBy = "parent", fetch = FetchType.LAZY) // 기본 설정
    private List<Child> children;
    
    • 실행 쿼리 예시:
      SELECT * FROM parent; -- 부모 데이터 조회 (1회)
      SELECT * FROM child WHERE parent_id = 1; -- 부모 1번 자식 조회 (1회)
      SELECT * FROM child WHERE parent_id = 2; -- 부모 2번 자식 조회 (1회)
      -- 부모 N개일 경우 총 N+1번 쿼리 실행
      

2. FetchType.EAGER가 작동하는 방식

  • FetchType.EAGER (즉시 로딩):
    • 부모 데이터를 조회할 때 연관된 자식 데이터를 함께 조회.
    • 부모와 자식 데이터를 한 번의 JOIN 쿼리로 가져오므로 쿼리 실행 횟수가 줄어듦.
    @OneToMany(mappedBy = "parent", fetch = FetchType.EAGER) // 즉시 로딩 설정
    private List<Child> children;
    
    • 실행 쿼리 예시:
    • SELECT p.*, c.* FROM parent p LEFT JOIN child c ON p.id = c.parent_id; -- 부모와 자식을 JOIN하여 한 번에 조회
    • 결과:
      • 부모와 자식 데이터를 한 번에 가져오기 때문에 N+1 문제가 발생하지 않음.

3. 해결 원리

  • Lazy vs Eager의 차이:
    • Lazy 로딩은 데이터를 필요로 하는 시점에 별도의 쿼리를 실행하여 자식 데이터를 가져옴 → N+1 문제 발생.
    • Eager 로딩은 부모 데이터를 조회하는 시점에 연관된 자식 데이터를 미리 가져옴쿼리 실행 횟수 감소.
  • 쿼리 실행 구조:
    • Lazy: 부모를 조회한 후, 자식 데이터를 부모 개수만큼 별도로 조회 → 여러 번의 쿼리 발생.
    • Eager: 부모와 자식을 JOIN하여 한 번의 쿼리로 데이터 조회 → 쿼리 횟수 감소.

4. FetchType.EAGER의 특징

즉시 로딩 부모 데이터를 조회할 때, 자식 데이터를 함께 가져옴.
쿼리 최적화 부모-자식 관계를 JOIN으로 처리해 쿼리 횟수를 최소화.
간단한 설정 엔티티에서 fetch = FetchType.EAGER 설정으로 간편하게 적용.

5. 예제 코드와 쿼리 차이

Lazy 로딩

@OneToMany(mappedBy = "parent", fetch = FetchType.LAZY)
private List<Child> children;
  • 실행 쿼리:
    SELECT * FROM parent; -- 부모 데이터 조회
    SELECT * FROM child WHERE parent_id = 1; -- 부모 1번의 자식 데이터 조회
    SELECT * FROM child WHERE parent_id = 2; -- 부모 2번의 자식 데이터 조회
    

Eager 로딩

@OneToMany(mappedBy = "parent", fetch = FetchType.EAGER)
private List<Child> children;
  • 실행 쿼리:
    SELECT p.*, c.*
    FROM parent p
    LEFT JOIN child c ON p.id = c.parent_id; -- 부모와 자식 데이터를 한 번에 조회
    

6. FetchType.EAGER가 N+1 문제를 해결하는 이유

  • Lazy 로딩:
    • 부모 데이터를 먼저 조회하고, 자식 데이터를 필요로 할 때마다 별도 쿼리 실행 → N+1 쿼리 발생.
  • Eager 로딩:
    • 부모와 자식 데이터를 한 번의 JOIN 쿼리로 가져오기 때문에, 추가 쿼리 실행 없이 모든 데이터를 한 번에 로드 → N+1 문제 해결.

7. EAGER의 한계

  • 불필요한 데이터 로드:
    • 항상 연관 데이터를 즉시 로드하므로, 필요 없는 데이터도 로드될 가능성.
  • 메모리 사용량 증가:
    • 대량의 데이터가 JOIN으로 로드되면 메모리 사용량이 증가할 수 있음.
  • 복잡한 관계에서 성능 저하:
    • JOIN이 여러 테이블에 걸쳐 발생하면 쿼리 성능이 저하될 가능성.

8. 결론

  • FetchType.EAGER는 부모와 자식 데이터를 한 번의 JOIN 쿼리로 로드하여 N+1 문제를 해결.
  • 하지만 항상 즉시 로드되기 때문에 불필요한 데이터까지 가져올 가능성이 있어, Fetch Join이나 @EntityGraph 같은 대안도 상황에 따라 활용해야 함.

추가적으로 궁금한 점이 있으면 말씀해주세요! 😊

적용 방법

  • 기본적으로 Lazy로 설정된 관계를 EAGER로 변경.
@Entity
public class Parent {
    @Id
    @GeneratedValue
    private Long id;

    private String name;

    @OneToMany(mappedBy = "parent", fetch = FetchType.EAGER) // 즉시 로딩
    private List<Child> children;
}

실행 쿼리

-- 부모와 자식 데이터를 즉시 조회
SELECT p.*, c.* 
FROM parent p 
LEFT JOIN child c 
ON p.id = c.parent_id;

장점

  • 쿼리 횟수가 감소하여 성능 개선.

단점

  • 필요하지 않은 데이터까지 즉시 조회되어 메모리 사용량 증가.

5.2 Fetch Join 사용

해결 원리

  • 애초에 연관된 일부의 데이터만 미리 설정해 즉시 한번에 불러옴

적용 방법

  • JPQL에서 JOIN FETCH를 사용하여 연관 데이터를 함께 조회.
@Query("SELECT p FROM Parent p JOIN FETCH p.children")
List<Parent> findAllWithChildren();

실행 쿼리

SELECT p.*, c.* 
FROM parent p 
LEFT JOIN child c 
ON p.id = c.parent_id;

장점

  • N + 1 문제 해결.
  • 필요한 데이터만 효율적으로 조회.

5.3 @EntityGraph 사용

해결 원리

  • JPA에서 Fetch Join과 유사한 방식으로 작동하여 N+1 문제를 해결
  • 엔티티 간의 연관 관계를 명시적으로 지정해 즉시 로딩(Eager)을 적용.

적용 방법

  • JPA의 **@EntityGraph**를 활용하여 연관 데이터를 Fetch Join 방식으로 조회.
@EntityGraph(attributePaths = {"children"})
@Query("SELECT p FROM Parent p")
List<Parent> findAllWithChildren();

장점

  • Fetch Join과 동일한 효과를 제공.
  • 코드 재사용성이 높음.

5.4 Batch Size 설정

해결 원리

  • 지연 로딩(Lazy Loading)을 유지하면서 데이터를 묶어서 한 번에 조회하도록 최적화.
  • JPQL을 재작성할 필요 없이 애노테이션으로 간단히 설정 가능.

적용 방법

  • Lazy 로딩을 유지하면서 여러 건의 데이터를 한 번에 가져오도록 Batch Size 설정.

Spring 설정

spring:
  jpa:
    properties:
      hibernate:
        default_batch_fetch_size: 10

실행 쿼리

-- 부모 조회
SELECT * FROM parent;

-- 자식 데이터 조회 (10건씩 묶어서 실행)
SELECT * FROM child WHERE parent_id IN (1, 2, 3, ..., 10);

장점

  • Lazy 로딩 유지.
  • 데이터 조회 최적화.

6. 해결 방법 비교

N + 1 문제 해결 방법 비교 정리

특징 FetchType.EAGER Fetch Join EntityGraph Batch Size
쿼리 실행 횟수 1회 + N회 (부모 1회, 자식 N회) 1회 1회 N / Batch Size (Batch Size만큼 묶어서 조회)
로딩 방법 Eager (즉시 로딩) Eager (즉시 로딩) Eager (즉시 로딩) Lazy (지연 로딩)
설정 난이도 간단 (예: fetch = FetchType.EAGER) 쿼리 재작성 필요 간단 (예: @EntityGraph 애노테이션 사용) 간단 (전역 설정 또는 개별 설정 가능)
유연성 낮음 (항상 Eager 로딩) 중간 (필요한 경우에만 Fetch Join 적용 가능) 중간 (필요한 엔티티에만 적용 가능) 높음 (Lazy 로딩 유지, 필요 데이터만 로드 가능)
주요 단점 불필요한 데이터 항상 로드, 관계가 깊으면 성능 저하 복잡한 관계나 대량 데이터에서 성능 저하 복잡한 관계나 대량 데이터에서 성능 저하 IN 절의 비효율성, Lazy로 인해 예측 불가 쿼리 발생
적합한 상황 간단한 관계 데이터, 자주 참조되는 연관 데이터 관계가 간단하고 즉시 로드가 필요한 경우 관계가 단순하고 재사용성이 중요한 경우 대량 데이터 조회 시 Lazy 로딩 유지가 필요한 경우

각 방법에 대한 설명

1. FetchType.EAGER

  • 설명:
    • 연관된 데이터를 즉시 로드.
    • 간단한 설정으로 사용 가능.
  • 적합한 상황:
    • 연관 데이터가 많지 않고, 자주 참조되는 경우.
  • 한계:
    • 항상 데이터를 로드하므로 불필요한 성능 저하 가능.

2. Fetch Join

  • 설명:
    • JPQL의 JOIN FETCH를 사용해 연관 데이터를 함께 조회.
    • 쿼리 실행 횟수를 최소화.
  • 적합한 상황:
    • 관계가 단순하고, 대량 데이터가 아닌 경우.
  • 한계:
    • 복잡한 관계에서는 쿼리 성능이 저하될 수 있음.

3. EntityGraph

  • 설명:
    • JPA의 @EntityGraph를 사용해 Fetch Join을 간편하게 적용.
    • 쿼리 동작 방식을 코드와 분리하여 재사용성을 높임.
  • 적합한 상황:
    • 관계가 단순하고, 코드 재사용성을 높이고 싶은 경우.
  • 한계:
    • 복잡한 데이터 관계에서는 성능 저하 가능.

4. Batch Size

  • 설명:
    • Lazy 로딩 유지하면서 연관 데이터를 묶어서 한 번에 조회.
    • 전역 설정 또는 개별 설정 가능.
  • 적합한 상황:
    • 대량 데이터 조회 시 Lazy 로딩 유지가 필요한 경우.
  • 한계:
    • IN 절의 비효율성과 예측 불가능한 쿼리 발생 가능.

결론

  • FetchType.EAGER: 간단한 관계에서 데이터가 항상 필요할 경우 적합.
  • Fetch Join: 즉시 로드가 필요하고 관계가 간단한 경우.
  • EntityGraph: Fetch Join과 유사하지만 재사용성을 고려할 때 유용.
  • Batch Size: Lazy 로딩을 유지하면서 대량 데이터를 효율적으로 처리하고 싶을 때.

7. 결론

  • 상황에 맞는 해결 방법을 선택:
    • 데이터가 많지 않은 경우: FetchType.EAGER.
    • 특정 쿼리 최적화: Fetch Join or @EntityGraph.
    • 대량의 Lazy 로딩 데이터 최적화: Batch Size.

 

'DB > JPA ( Java Persistence API )' 카테고리의 다른 글

[JPA] Lock (동시성)  (1) 2025.02.24
[JPA] SpringData JPA 심화  (0) 2025.01.28
[JPA] 테이블 객체  (0) 2025.01.27
[JPA] 쿼리 파일 만들기 (QueryMapper)  (0) 2025.01.26
[JPA] 데이터베이스 연결 (Driver)  (1) 2025.01.25

QueryDSL

QueryDSL 소개

  • Entity의 매핑 정보를 활용하여 쿼리를 객체 또는 함수로 작성.
  • 주요 구성:
    • Q클래스: Entity의 쿼리 전용 클래스.
    • JPAQueryFactory: Q클래스를 사용하여 쿼리를 생성 및 실행.

QueryDSL 적용 방법

  1. JPAQueryFactory Bean 등록:
  2. @Configuration public class JPAConfiguration { @PersistenceContext private EntityManager entityManager; @Bean public JPAQueryFactory jpaQueryFactory() { return new JPAQueryFactory(entityManager); } }
  3. 쿼리 작성:
  4. @Autowired private JPAQueryFactory queryFactory; public List<User> selectUserByUsernameAndPassword(String username, String password) { QUser user = QUser.user; return queryFactory.selectFrom(user) .where(user.username.eq(username) .and(user.password.eq(password))) .fetch(); }

예제: 멘션된 쓰레드 조회

  • 조건: 내가 멘션된 쓰레드, 정렬 기준은 멘션된 시간 기준 내림차순.
  • 출력: 채널명, 작성자 정보, 본문, 이모지 정보, 댓글 정보 등.
public List<ThreadDetails> findMentionedThreads(Long userId) {
    QThread thread = QThread.thread;
    QMention mention = QMention.mention;

    return queryFactory
        .select(
            Projections.bean(ThreadDetails.class,
                thread.channel.name.as("channelName"),
                thread.user.username.as("authorName"),
                thread.user.profileImageUrl.as("authorProfileImage"),
                thread.message.as("message"),
                thread.emotions.any().body.as("emotions"),
                thread.comments.any().message.as("commentMessages")
            ))
        .from(thread)
        .join(thread.mentions, mention)
        .where(mention.user.id.eq(userId))
        .orderBy(mention.createdAt.desc())
        .fetch();
}

 

 

Auditing

Auditing 소개

  • 엔티티 생성 및 수정 시 자동으로 작성자와 시간을 기록.

적용 방법

  1. @EnableJpaAuditing 추가:
  2. @EnableJpaAuditing(auditorAwareRef = "userAuditorAware") @SpringBootApplication public class Application {}
  3. 공통 Base 클래스 작성:
  4. @MappedSuperclass @EntityListeners(AuditingEntityListener.class) public class BaseTimeEntity { @CreatedDate private LocalDateTime createdAt; @LastModifiedDate private LocalDateTime modifiedAt; @CreatedBy @ManyToOne private User createdBy; @LastModifiedBy @ManyToOne private User modifiedBy; }
  5. AuditorAware 구현:
  6. @Service public class UserAuditorAware implements AuditorAware<User> { @Override public Optional<User> getCurrentAuditor() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication == null || !authentication.isAuthenticated()) return Optional.empty(); return Optional.of(((UserDetailsImpl) authentication.getPrincipal()).getUser()); } }

 

Dynamic Insert/Update

Dynamic Insert

  • @DynamicInsert:
    • Insert 쿼리에서 null 값을 제외한 필드만 포함.
@DynamicInsert
public class User {
    private String username;
    private String password;
}

적용 전:

insert into users (username, password, id) values (?, ?, ?)

적용 후:

insert into users (username, id) values (?, ?)

Dynamic Update

  • @DynamicUpdate:
    • Update 쿼리에서 변경된 값만 포함.
@DynamicUpdate
public class User {
    private String username;
    private String password;
}

적용 전:

update users set username=?, password=? where id=?

적용 후:

update users set username=? where id=?

요약

기능  설명  적용 예시
QueryDSL - 객체 기반으로 쿼리를 작성하여 가독성과 유지보수성 향상.- JPAQueryFactory와 Q클래스 활용. - 사용자 검색: selectFrom(user).where(user.username.eq("John")).fetch();
Auditing - 엔티티 생성/수정 시 작성자와 시간을 자동 기록. - @CreatedBy, @LastModifiedBy를 통해 작성자 자동 기록.
Dynamic 설정 - Insert/Update 시 null 값을 제외한 필드만 처리.- 성능 최적화. - @DynamicInsert, @DynamicUpdate

 

'DB > JPA ( Java Persistence API )' 카테고리의 다른 글

[JPA] Lock (동시성)  (1) 2025.02.24
[JPA] N + 1 문제란 무엇인가  (0) 2025.02.14
[JPA] 테이블 객체  (0) 2025.01.27
[JPA] 쿼리 파일 만들기 (QueryMapper)  (0) 2025.01.26
[JPA] 데이터베이스 연결 (Driver)  (1) 2025.01.25

테이블 객체 다루는법

 

[1] Cascade (영속성 전이)

항목  설명  예시 코드
사용 위치 - 연관관계 주인의 반대편에 설정.- 주로 부모 엔티티(@OneToMany, @OneToOne)에 설정. java<br>@OneToMany(cascade = CascadeType.ALL)<br>private List<File> files;<br>
사용 조건 - 연관된 엔티티의 라이프사이클이 동일하거나 유사할 때 사용.- 현재 엔티티에서만 전이되도록 설정. 예: 게시글과 첨부파일, 게시글 삭제 시 첨부파일도 삭제됨.
옵션 종류 - ALL: 모든 상태 전이를 포함.- PERSIST: 저장 전이.- REMOVE: 삭제 전이.- MERGE: 병합 전이.- REFRESH: 갱신 전이.- DETACH: 비영속 전이. java<br>@OneToOne(cascade = {CascadeType.PERSIST, CascadeType.REMOVE})<br>private Profile profile;<br>
주요 고려 사항 - 다른 엔티티에서 동일한 대상에 대해 영속성 전이 설정 금지.- 불필요한 전이 설정은 데이터 불일치 및 성능 저하를 초래할 수 있음.  

Cascade 옵션 상세 설명

옵션 설명
ALL - 모든 상태 전이를 포함.동일한 라이프사이클을 가진 엔티티에 대해 사용.
PERSIST - 부모 엔티티를 저장하면, 연관된 엔티티도 함께 저장.
REMOVE - 부모 엔티티를 삭제하면, 연관된 엔티티도 함께 삭제.
MERGE - 부모 엔티티가 병합될 때, 연관된 엔티티도 병합.
REFRESH - 부모 엔티티를 새로고침(갱신)하면, 연관된 엔티티도 새로고침.
DETACH - 부모 엔티티가 비영속 상태로 전환되면, 연관된 엔티티도 비영속 상태로 전환.

예제: 게시글과 첨부파일 관계

@Entity
public class Post {

    @Id
    @GeneratedValue
    private Long id;

    private String title;

    @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<File> files = new ArrayList<>();

    public void addFile(File file) {
        files.add(file);
        file.setPost(this);
    }

    public void removeFile(File file) {
        files.remove(file);
        file.setPost(null);
    }
}

@Entity
public class File {

    @Id
    @GeneratedValue
    private Long id;

    private String fileName;

    @ManyToOne
    @JoinColumn(name = "post_id")
    private Post post;
}

주요 사용 시나리오

  1. 게시글 삭제 시 첨부파일도 삭제:
    • CascadeType.REMOVE를 사용해 부모 엔티티(Post) 삭제 시 자식 엔티티(File)도 삭제.
  2. 게시글 저장 시 첨부파일도 저장:
    • CascadeType.PERSIST를 사용해 Post 저장 시 File도 저장.
  3. 연관된 엔티티가 독립적으로 관리되지 않도록 제한:
    • 동일 라이프사이클 보장.

주의사항

  1. Cascade의 과도한 사용 금지:
    • 모든 연관관계에 설정 시 데이터 무결성 문제 발생 가능.
  2. ALL 사용 신중:
    • 모든 상태 전이를 강제하므로, 필요하지 않은 전이가 포함될 수 있음.
  3. 단방향 설정 추천:
    • 불필요한 양방향 전이는 복잡도를 높이고 성능을 저하시킬 수 있음.

 

[2] orphanRemoval (고아 객체 제거) 

항목  설명  예시 코드
사용 위치 - @OneToMany 또는 @OneToOne 관계에서 부모 엔티티에 설정. java<br>@OneToMany(mappedBy = "parent", orphanRemoval = true)<br>private List<Child> childList;<br>
사용법 - 부모 엔티티에서 자식 엔티티를 제거할 때, 고아 객체(매핑 정보가 없는 자식)를 자동으로 삭제. java<br>Parent parent = em.find(Parent.class, id);<br>parent.getChildList().remove(0); // 자식 엔티티 삭제<br>
Cascade.REMOVE와 차이 - Cascade.REMOVE: 부모 엔티티를 삭제하면 연관된 자식 엔티티도 삭제.- orphanRemoval: 부모의 리스트에서 자식 엔티티를 제거하면 자식 엔티티도 삭제. - 두 옵션을 함께 설정하면 자식 엔티티의 생명주기가 부모와 완전히 동일.
옵션 - true: 고아 객체 제거 활성화.- false: 고아 객체 제거 비활성화. java<br>@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true)<br>private List<Child> children;<br>
추천 설정 - orphanRemoval = true + Cascade.ALL 조합 사용.- 자식 엔티티의 생명주기를 부모와 동일하게 관리.- 자식의 독립적인 Repository 없이 관리 가능.  

주요 사용 시나리오

  1. 부모-자식 관계에서 고아 객체 처리:
    • 부모 엔티티에서 자식 엔티티를 리스트에서 제거하면, 매핑 정보가 없는 자식(고아 객체)을 자동으로 삭제.
    • 예: 게시글에서 첨부파일 제거 시, DB에서도 해당 첨부파일 삭제.
  2. 자식의 라이프사이클 부모에 종속:
    • Cascade.ALL과 함께 사용하여 자식 엔티티가 부모의 라이프사이클에 완전히 종속되도록 설정.
    • 자식 엔티티의 생명주기를 부모 엔티티에서만 관리 가능.

예제 코드

1. 고아 객체 제거 설정

@Entity
public class Parent {

    @Id
    @GeneratedValue
    private Long id;

    @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Child> childList = new ArrayList<>();

    public void addChild(Child child) {
        childList.add(child);
        child.setParent(this);
    }

    public void removeChild(Child child) {
        childList.remove(child);
        child.setParent(null);
    }
}

@Entity
public class Child {

    @Id
    @GeneratedValue
    private Long id;

    @ManyToOne
    @JoinColumn(name = "parent_id")
    private Parent parent;
}

2. 사용 예제

Parent parent = em.find(Parent.class, parentId);
Child child = parent.getChildList().get(0);

// 자식 엔티티를 리스트에서 제거하면 자동으로 삭제됨
parent.removeChild(child);
em.flush(); // delete 쿼리 실행

장점 및 주의사항

장점 주의사항
- 자식 엔티티의 생명주기를 부모와 동일하게 관리 가능.- 자식 엔티티의 독립적인 Repository가 불필요.- 매핑 테이블에서 많이 사용. - 연관 관계의 복잡성 증가 가능.- 자식 엔티티가 다른 엔티티에서도 참조된다면, orphanRemoval 설정은 사용하면 안 됨.- 부모-자식 관계가 명확할 때만 사용 권장.

 

 

[3] Fetch (조회 시점) 

항목  설명 예시 코드
사용 위치 - 연관관계를 맺는 엔티티에서 @ElementCollection, @ManyToMany, @OneToMany, @ManyToOne, **@OneToOne**에 설정 가능. java<br>@OneToMany(fetch = FetchType.LAZY)<br>private List<Comment> comments;<br>
사용법 - 기본적으로 LAZY 설정 후 필요 시 fetch join으로 조회.- 항상 함께 조회되는 관계는 EAGER 설정. java<br>@OneToOne(fetch = FetchType.EAGER)<br>private Profile profile;<br>
옵션 (FetchType) - EAGER: 부모 조회 시 연관된 자식도 즉시 조회.- LAZY: 연관된 자식은 실제로 필요할 때 조회. java<br>@ManyToOne(fetch = FetchType.LAZY)<br>@JoinColumn(name = "parent_id")<br>private Parent parent;<br>
Query 활용 - fetch join: 기본 설정이 LAZY일 때도 즉시 로딩 가능. java<br>SELECT p FROM Post p JOIN FETCH p.comments WHERE p.id = :id;<br>

FetchType 옵션 비교

옵션  설명  장점 단점
EAGER - 즉시 로딩.- 부모 엔티티 조회 시 자식 엔티티도 함께 조회. - 편리함: 한 번의 조회로 연관 데이터 로드.- 항상 필요한 경우 적합. - 불필요한 데이터 로드로 성능 저하.- N+1 문제 유발 가능.
LAZY - 지연 로딩.- 부모 엔티티 조회 시 자식은 실제로 필요할 때 조회. - 성능 최적화: 실제 필요한 경우에만 데이터 조회.- 불필요한 데이터 로드 방지. - 필요 시 추가 쿼리 발생.- fetch join 필요 시 복잡성 증가.

추천 설정 및 사용 예제

기본 설정

  • 대부분의 연관관계는 **LAZY**로 설정:
    • 불필요한 데이터 로드를 방지.
    • 필요 시 명시적으로 fetch join을 통해 데이터 조회.

항상 함께 쓰이는 관계

  • EAGER 설정:
    • 예: @OneToOne에서 프로필과 사용자 관계처럼 항상 함께 조회되어야 하는 경우.

실제 사용 예제

1. FetchType 설정 예제

@Entity
public class User {

    @Id
    @GeneratedValue
    private Long id;

    @OneToOne(fetch = FetchType.EAGER)
    private Profile profile; // 즉시 로딩

    @OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
    private List<Post> posts; // 지연 로딩
}

2. Fetch Join을 활용한 즉시 로딩

// 기본적으로 FetchType.LAZY 설정
@Entity
public class Post {

    @Id
    @GeneratedValue
    private Long id;

    @OneToMany(mappedBy = "post", fetch = FetchType.LAZY)
    private List<Comment> comments;
}

// Fetch Join으로 LAZY 관계 즉시 로딩
String query = "SELECT p FROM Post p JOIN FETCH p.comments WHERE p.id = :id";
TypedQuery<Post> typedQuery = em.createQuery(query, Post.class);
typedQuery.setParameter("id", postId);
Post post = typedQuery.getSingleResult();

주의사항

  1. N+1 문제:
    • EAGER 설정 시, 연관된 자식 엔티티를 매번 추가로 조회해 성능 문제가 발생할 수 있음.
    • 해결책: fetch join 활용.
  2. 불필요한 로딩 방지:
    • 필요하지 않은 데이터를 EAGER로 설정하지 않도록 주의.
  3. 실무 권장 사항:
    • 대부분 LAZY 설정 후, 필요 시 명시적으로 fetch join 사용.
    • **EAGER**는 정말 항상 함께 조회되는 관계에만 설정.

 

 

테이블 객체로 자동 쿼리 생성하기

SprintData Common 의 CRUDRepository + PagingAndSortingRepository 이 쿼리기능을 제공

 

[1] JPA Repository 쿼리 기능 

항목  설명 예시 코드
기본 정의 - Repository: Marker Interface, 자체 기능 없음.- JpaRepository: CRUD, 페이징, 정렬 기능 제공. java<br>public interface UserRepository extends JpaRepository<User, Long> {}<br>
등록 방식 - JpaRepository는 **@EnableJpaRepositories**로 자동 등록.- 구현체로 **SimpleJpaRepository**가 사용. java<br>@SpringBootApplication<br>@EnableJpaRepositories<br>public class Application {}<br>
기능 차이 (기존 vs 새로운) - 기존 Repository:Raw JPA 기반의 기본 CRUD 기능 제공.- JpaRepository:CRUD, 페이징, 정렬 등 제공. java<br>@Repository<br>public class UserRepository { ... }<br>// JpaRepository로 변경<br>public interface UserRepository extends JpaRepository<User, Long> {}<br>

주요 내용

기존 Repository

  • 설정: @Repository 애노테이션을 붙여 사용.
  • 기능:
    • Raw JPA 기반의 CRUD 구현.
    • 직접 EntityManager 사용 필요.
    • DB별 예외 처리를 수동으로 구현.

JpaRepository

  • 설정: JpaRepository<Entity, ID>를 상속.
  • 기능:
    • CRUD, 페이징, 정렬 기능 내장.
    • Spring Data JPA에 의해 구현체(SimpleJpaRepository)가 자동으로 생성 및 등록.
    • 더 적은 코드로 복잡한 기능 구현 가능.

코드 비교: 기존 Repository vs JpaRepository

기존 Repository

@Repository
public class UserRepository {

    @PersistenceContext
    private EntityManager entityManager;

    public User insertUser(User user) {
        entityManager.persist(user);
        return user;
    }

    public User selectUser(Long id) {
        return entityManager.find(User.class, id);
    }
}

JpaRepository로 변경

public interface UserRepository extends JpaRepository<User, Long> {
}

JpaRepository의 주요 제공 기능

  1. CRUD 기능:
    • save(): 엔티티 저장/수정.
    • findById(): 기본 키로 엔티티 조회.
    • delete(): 엔티티 삭제.
  2. 페이징 및 정렬:
    • findAll(Pageable pageable): 페이징 조회.
    • findAll(Sort sort): 정렬된 목록 조회.
  3. 커스텀 쿼리 작성:
    • 메서드 이름 기반 쿼리 생성 (findByName).
    • JPQL 또는 네이티브 쿼리 작성 가능 (@Query).

Spring Data JPA의 장점

  1. 자동 빈 등록:
    • @EnableJpaRepositories를 통해 자동으로 Repository 구현체 등록.
  2. 간결한 코드:
    • 구현체 없이 인터페이스만으로 복잡한 기능 제공.
  3. 일관된 예외 처리:
    • 데이터 접근 계층의 일관된 예외 체계.

결론

  • 기존 @Repository 방식은 코드가 길고 직접 EntityManager를 사용해야 하지만,
  • JpaRepository를 사용하면 CRUD, 페이징, 정렬 등 다양한 기능을 더 간단하게 구현 가능.

 

 

[2] JpaRepository 쿼리 사용 방법 

항목  설명  예시 코드
접두어 - 쿼리 동작을 지정.- Find, Get, Query, Count 등 사용 가능. java<br>List<User> findByName(String name);<br>long countByName(String name);<br>
도입부 - 중복 제거(Distinct), 첫 N개만 조회(First(N), Top(N)) 등. java<br>List<User> findDistinctByName(String name);<br>List<User> findTop3ByName(String name);<br>
프로퍼티 표현식 - 속성 경로를 언더스코어로 연결하여 작성.- 예: Person.Address.ZipCode → findByAddress_ZipCode java<br>List<User> findByAddress_ZipCode(String zipCode);<br>
조건식 - 연산 조건 설정 (IgnoreCase, Between, LessThan, GreaterThan, Like, Contains). java<br>List<User> findByNameIgnoreCase(String name);<br>List<User> findByAgeBetween(int minAge, int maxAge);<br>
정렬 조건 - `OrderBy{프로퍼티}Asc Desc`.
리턴 타입 - E, Optional<E>, List<E>, Page<E>, Slice<E>, Stream<E>. java<br>Page<User> findByName(String name, Pageable pageable);<br>Slice<User> findByName(String name, Pageable pageable);<br>
매개변수 - 페이징: Pageable 사용.- 정렬: Sort 사용. java<br>List<User> findByName(String name, Pageable pageable);<br>List<User> findByName(String name, Sort sort);<br>

실습 예제 및 설명

기본 쿼리

List<User> findByNameAndPassword(String name, String password); // 조건식
List<User> findByNameIgnoreCase(String name);                  // 대소문자 무시
List<Person> findByNameOrderByAgeDesc(String name);            // 정렬

페이징 처리

Pageable firstPage = PageRequest.of(0, 3);  // 첫 페이지, 페이지 크기 3
Pageable sortedByName = PageRequest.of(1, 5, Sort.by("name")); // 두 번째 페이지, 정렬 추가

Page<User> pageResult = userRepository.findByName("John", firstPage);
  • Page: 페이징 정보와 전체 데이터 수를 포함.
  • Slice: 다음 페이지 여부만 확인 가능.

정렬 처리

// 정렬만 수행
List<Product> productsSortedByName = productRepository.findAll(Sort.by("name").ascending());

// 페이징과 정렬 함께
Pageable sortedByPriceAndName = PageRequest.of(0, 5, Sort.by("price").descending().and(Sort.by("name")));
Page<Product> pageResult = productRepository.findAll(sortedByPriceAndName);

스트림 사용

try (Stream<User> userStream = userRepository.readAllByNameNotNull()) {
    userStream.forEach(user -> System.out.println(user.getName()));
}
  • 스트림 사용 후 자원 해제를 위해 try-with-resources 권장.

주요 특징 및 추천 활용

  1. 기본 설정:
    • 메서드 이름만으로도 대부분의 쿼리 구현 가능.
    • 명확하고 간결한 코드 작성 가능.
  2. 페이징과 정렬:
    • 페이징 시 PageSlice 선택적으로 사용.
    • 복합 정렬 시 **Sort**를 활용.
  3. 스트림:
    • 대량 데이터 처리 시 유용.

 

[3] JpaRepository 효율적으로 사용하는 방법 

항목  설명 예시 코드
1. Optional 제거하기 - findById()의 반환값인 Optional을 직접 처리하지 않도록 기본 메서드를 정의하여 비즈니스 로직에서 간결하게 처리. java<br>default User findUserById(Long id) {<br> return findById(id).orElseThrow(() -><br> new DataNotFoundException("User not found"));<br>}<br>
2. 메서드명 간소화하기 - 복잡한 쿼리 메서드명을 기본 메서드로 감싸 간결하고 명확한 이름으로 제공. java<br>default List<Product> findProductsByCategoryAndPriceRange(String category, BigDecimal min, BigDecimal max) {<br> return findAllByCategoryAndPriceGreaterThanEqualAndPriceLessThanEqualOrderByPriceAsc(category, min, max);<br>}<br>
3. 비즈니스 로직 통합 - Repository의 기본 메서드들을 조합하여 고차 작업을 처리하는 메서드 제공.- 데이터베이스와의 상호작용을 간소화. java<br>default void updateUserContact(Long userId, String newContact) {<br> findById(userId).ifPresent(user -> {<br> user.setContact(newContact);<br> save(user);<br> });<br>}<br>

상세 설명 및 추천 활용

1. Optional 제거하기

  • 문제: Optional 처리로 인해 코드가 복잡해질 수 있음.
  • 해결: 기본 메서드를 활용하여 orElseThrow()로 예외를 처리.
  • 추천 시나리오:
    • 엔티티가 반드시 존재해야 하는 경우.

2. 메서드명 간소화하기

  • 문제: 긴 메서드명으로 인해 가독성이 떨어질 수 있음.
  • 해결: 복잡한 메서드를 기본 메서드로 감싸서 간결한 이름 제공.
  • 추천 시나리오:
    • 복잡한 조건이 포함된 쿼리 메서드.

3. 비즈니스 로직 통합

  • 문제: 데이터베이스 작업과 비즈니스 로직이 분리되지 않으면 중복 코드 발생.
  • 해결: 기본 메서드로 데이터 처리 및 로직 결합.
  • 주의사항:
    • Repository는 데이터 접근 계층에 집중하고, 비즈니스 로직은 서비스 계층에서 처리하는 것이 일반적.
    • 간단한 작업에 한해 Repository에서 처리.

예제 코드

1. Optional 제거

public interface UserRepository extends JpaRepository<User, Long> {

    default User findUserById(Long id) {
        return findById(id).orElseThrow(() -> 
            new DataNotFoundException("User not found with id: " + id));
    }
}

2. 메서드명 간소화

public interface ProductRepository extends JpaRepository<Product, Long> {

    // 긴 메서드 이름
    List<Product> findAllByCategoryAndPriceGreaterThanEqualAndPriceLessThanEqualOrderByPriceAsc(String category, BigDecimal minPrice, BigDecimal maxPrice);

    // 간단한 메서드 이름
    default List<Product> findProductsByCategoryAndPriceRange(String category, BigDecimal minPrice, BigDecimal maxPrice) {
        return findAllByCategoryAndPriceGreaterThanEqualAndPriceLessThanEqualOrderByPriceAsc(category, minPrice, maxPrice);
    }
}

3. 비즈니스 로직 통합

public interface UserRepository extends JpaRepository<User, Long> {

    default void updateUserContact(Long userId, String newContact) {
        findById(userId).ifPresent(user -> {
            user.setContact(newContact);
            save(user);
        });
    }
}

추천 사용 전략

  1. Optional 제거:
    • findById() 반환값을 비즈니스 로직에서 간결하게 처리.
  2. 메서드명 간소화:
    • 복잡한 조건이 포함된 메서드를 간단하고 읽기 쉽게 변환.
  3. 비즈니스 로직 통합:
    • 간단한 작업에 한해 Repository에서 직접 처리. 복잡한 로직은 서비스 계층에 분리.

 

테이블 객체로 페이지 조회하기

 

[1] JpaRepository 페이징 및 정렬 

페이징 처리 프로세스

  1. PageRequest 생성:
    • PageRequest.of(page, size)로 페이지 번호와 크기를 설정.
    • 정렬이 필요한 경우 추가적으로 Sort를 전달.
  2. 메서드 호출:
    • Pageable 객체를 JpaRepository 메서드에 전달.
    • 반환값은 Page<T>, Slice<T>, 또는 List<T>로 설정 가능.
  3. 응답 처리:
    • Page<T> 또는 Slice<T> 객체에서 메타정보와 데이터를 추출하여 비즈니스 로직 처리.

주요 클래스 및 메서드

Pageable 생성

  • 기본 생성:
    Pageable pageable = PageRequest.of(0, 10); // 페이지 번호: 0, 페이지 크기: 10
    
  • 정렬 추가:
    Pageable pageable = PageRequest.of(0, 10, Sort.by("name").ascending());
    

Pageable 주요 메서드

메서드 설명

getTotalPages() 전체 페이지 수 반환.
getTotalElements() 전체 요소 수 반환.
getNumber() 현재 페이지 번호 반환.
getSize() 페이지 크기 반환.
getContent() 현재 페이지의 데이터(List<T>) 반환.
hasNext() 다음 페이지 존재 여부 반환.
isFirst() 현재 페이지가 첫 페이지인지 반환.

Page 응답 예제

{
  "content": [
    { "id": 1, "username": "User1", "address": "Korea", "age": 25 },
    { "id": 2, "username": "User2", "address": "USA", "age": 30 }
  ],
  "pageable": {
    "pageNumber": 0,
    "pageSize": 2,
    "sort": { "sorted": false, "unsorted": true }
  },
  "totalPages": 10,
  "totalElements": 20,
  "first": true,
  "last": false
}

페이징 반환 타입 비교

반환 타입 설명 장점 단점

Page - 전체 페이지 및 요소 수 포함.- 게시판 형태. - 전체 정보 확인 가능.- 페이징 UI에 적합. - 추가적인 카운트 쿼리 발생.
Slice - 다음 페이지 여부만 확인 가능.- "더보기" 형태. - 카운트 쿼리 없음.- 성능 최적화. - 전체 정보 제공 불가.
List - 단순 목록 반환.- 정렬 가능. - 쿼리 단순화.- 카운트 쿼리 없음. - 페이지 메타정보 제공 불가.

정렬 처리

컬럼 값 기준 정렬

Sort sort = Sort.by("name").ascending();
Pageable pageable = PageRequest.of(0, 10, sort);
Page<User> page = userRepository.findAll(pageable);

다중 정렬

Sort sort = Sort.by("name").ascending().and(Sort.by("age").descending());
Pageable pageable = PageRequest.of(0, 10, sort);
Page<User> page = userRepository.findAll(pageable);

Alias를 기준으로 정렬

@Query("SELECT u.user_name AS userName FROM User u WHERE u.address = :address")
List<User> findByAddress(@Param("address") String address, Sort sort);

// 호출 시
List<User> users = userRepository.findByAddress("Korea", Sort.by("userName").ascending());

SQL 함수 기반 정렬

List<User> users = userRepository.findByUsername(
    "user", JpaSort.unsafe("LENGTH(password)").descending());

실습 코드

페이징 처리

Pageable pageable = PageRequest.of(1, 5, Sort.by("name").descending());
Page<User> page = userRepository.findByAddress("Korea", pageable);

List<User> users = page.getContent();
long totalElements = page.getTotalElements();
int totalPages = page.getTotalPages();

Slice 처리

Pageable pageable = PageRequest.of(1, 5, Sort.by("name").ascending());
Slice<User> slice = userRepository.findByAddress("Korea", pageable);

List<User> users = slice.getContent();
boolean hasNext = slice.hasNext();

추천 사용 전략

  1. Page와 Slice:
    • 게시판: Page<T>를 사용하여 전체 정보 제공.
    • "더보기" 기능: Slice<T>로 성능 최적화.
  2. 정렬:
    • 간단한 정렬: Sort 사용.
    • 복잡한 정렬: SQL 함수와 Alias 활용.
  3. 최적화:
    • 카운트 쿼리 비용이 큰 경우 Slice 또는 List로 대체.

 

 

MyBatis

   
정의 SQL Mapper의 한 종류로, JDBC 프로그래밍을 단순화하고 SQL과 코드를 분리.
주요 목적 - 반복되는 JDBC 코드 간소화- SQL과 애플리케이션 코드를 분리 관리.
특징 - SQL 쿼리 중심의 데이터 처리.- XML 파일과 어노테이션으로 SQL 관리.
장점 - SQL 작성에 유연성.- 빠른 개발 가능.- 코드와 SQL 분리로 유지보수 용이.
한계점 - SQL을 직접 작성해야 하므로 피로도 증가.- CRUD 코드 반복 발생.- DB 및 테이블에 종속적.
적합한 경우 - 데이터베이스 중심의 애플리케이션.- SQL에 세부적인 제어가 필요한 프로젝트.
부적합한 경우 - 객체 중심 설계(OOP)가 중요한 프로젝트.- 테이블 변경이 잦은 프로젝트.

 

 

MyBatis 의 동작

더보기

(1) ~ (3)은 응용 프로그램 시작시 수행되는 프로세스입니다.

(1) 응용 프로그램이 SqlSessionFactoryBuilder를 위해 SqlSessionFactory를 빌드하도록 요청합니다.

 

(2) SqlSessionFactoryBuilder는 SqlSessionFactory를 생성하기 위한 Mybatis 구성 파일을 읽습니다.

 

(3) SqlSessionFactoryBuilder는 Mybatis 구성 파일의 정의에 따라 SqlSessionFactory를 생성합니다.

 

 

(4) ~ (10)은 클라이언트의 각 요청에 대해 수행되는 프로세스입니다.

(4) 클라이언트가 응용 프로그램에 대한 프로세스를 요청합니다.

 

(5) 응용 프로그램은 SqlSessionFactoryBuilder를 사용하여 빌드된 SqlSessionFactory에서 SqlSession을 가져옵니다.

 

(6) SqlSessionFactory는 SqlSession을 생성하고 이를 애플리케이션에 반환합니다.

 

(7) 응용 프로그램이 SqlSession에서 매퍼 인터페이스의 구현 개체를 가져옵니다.

 

(8) 응용 프로그램이 매퍼 인터페이스 메서드를 호출합니다.

 

(9) 매퍼 인터페이스의 구현 개체가 SqlSession 메서드를 호출하고 SQL 실행을 요청합니다.

 

(10) SqlSession은 매핑 파일에서 실행할 SQL을 가져와 SQL을 실행합니다.

 

  • SqlSession Factory Builder (1), (2), (3)
    • MyBatis 설정 파일을 읽어와서
    • 설정정보 기반으로 SqlSession Factory 를 생성하는 빌더 객체
  • MyBatis Config File (2)
    • 매핑해줄 객체가 들어있는 패키지 경로와
    • Mapping File 목록을 지정해주는 설정 파일
<!-- /resources/mybatis-config.xml -->
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <typeAliases>
        <package name="com.thesun4sky.querymapper.domain"/>
    </typeAliases>
    <mappers>
        <mapper resource="mapper/UserMapper.xml"/>
    </mappers>
</configuration>

 

  • SqlSession Factory (3), (5), (6)
    • 설정에 맞게 SqlSession 을 생성하여 들고있는 객체
  • SqlSession (6), (7), (9), (10)
    • Mapping File 에서 쿼리를 조회해서 쿼리를 수행하고 응답을 받아올 수 있는 세션 객체
  • Mapper Interface (8), (9)
    • DB 에서 조회하는 객체와 Java 프로그램의 객체간에 인터페이스를 정의하는 객체
    • 방법1. Dao 클래스 정의
      • SqlSession 를 직접적으로 사용하는 방법
      • SqlSession 멤버 변수로 사용하며 쿼리파일 수행 요청
// UserDao.java
import org.apache.ibatis.session.SqlSession;
import org.springframework.stereotype.Component;

import com.thesun4sky.querymapper.domain.User;

@Component
public class UserDao {

  // SqlSession 멤버 변수로 사용하며 쿼리파일 수행 요청
  private final SqlSession sqlSession;

  public UserDao(SqlSession sqlSession) {
    this.sqlSession = sqlSession;
  }

  public User selectUserById(long id) {
    return this.sqlSession.selectOne("selectUserById", id);
  }

}

 

  • 장점
    • 쿼리문 실행 전에 넣어줄 매개변수와 쿼리 결과값의 변형을 정의할 수 있다.
    • Namespace를 내 마음대로 둘 수 있다.
    • .xml 파일의 쿼리문 id와 mapper 메소드명을 일치시킬 필요가 없다.
  • 단점
    • Sqlsession 객체를 주입받아야 하며, 쿼리문 실행 시 항상 호출해야 한다.
    • 쿼리문 호출 시 sqlsession에 .xml 파일의 namespce와 쿼리문 id를 매개변수로 넘겨야한다.
  • 방법2. Mapper Interface 정의
    • SqlSession 를 간접적으로 사용하는 방법
    • ibatis 에서 구현해주는 org.apache.ibatis.annotations.Mapper 어노테이션을 사용하면 sqlSession 를 사용하여 자동으로 호출해줌
// UserMapper.java
@Mapper
public interface UserMapper {

  User selectUserById(@Param("id") Long id);

}
  • 장점
    • 메소드의 내부 구현이 불필요하다.
    • Sqlsession 객체 주입이 불펼요하다.
    • .xml 파일의 쿼리문 id와 mapper 메소드 명이 일치한다.
  • 단점
    • .xml의 Namespace가 실제 Mapper.java 위치를 가르켜야 한다.
    • 메소드 내부 정의가 불가능하다.
  • Mapping File (10)
    • SqlSession 가 실행하는 쿼리가 담긴 파일
    • 정의된 인터페이스에 기반해서 수행할 쿼리를 담아두고
    • 쿼리 수행결과를 어떤 인터페이스 매핑할지 정의해놓은 파일
<!-- UserMapper.xml -->
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.thesun4sky.querymapper.mapper.UserMapper">
    <select id="selectUserById" resultType="User">
        select id, name from users where id = #{id}
    </select>
</mapper>

 

 

 

쿼리 코드 만들기 (JpaRepository)

QueryMapper 의 DB의존성 및 중복 쿼리 문제로 ORM 이 탄생했다.

  • ORM 은 DB의 주도권을 뺏어왔다고 표현해도 과언이 아닙니다.
  • ORM 은 DAO 또는 Mapper 를 통해서 조작하는것이 아니라 테이블을 아예 하나의 객체(Object)와 대응시켜 버립니다.
  • 말이 쉽지…. 객체지향(Object) 을 관계형 데이터베이스(Relation) 에 매핑(Mapping) 한다는건 정말 많은 난관이 있습니다.

 

ORM 문제점

문제점  객체의 특징 RDB의 특징 해결방법
상속의 문제 객체 간 상속관계 가능. 테이블 간 상속관계 없음, 독립적으로 존재. - 매핑 정보에 상속 정보 추가.예: @OneToMany, @ManyToOne
관계 문제 객체는 참조를 통해 관계를 가짐.다대다 관계 가능. 외래키(FK)로 관계 설정.다대다 관계는 매핑 테이블 필요. - 매핑 정보에 방향 정보 추가.예: @JoinColumn, @MappedBy
탐색 문제 참조로 객체 탐색 가능.콜렉션 순회 가능. 참조 시 추가 쿼리 또는 Join 발생. - 탐색 시점 관리.예: @FetchType, fetchJoin()
밀도 문제 객체의 멤버 크기가 클 수 있음. 기본 데이터 타입만 존재. - 큰 객체는 테이블로 분리.예: @Embedded
식별성 문제 hashCode 또는 equals() 메서드로 식별. PK로만 식별 가능. - PK를 객체 ID로 설정.예: @Id, @GeneratedValue, EntityManager로 관리.

 

ORM 해결책

  • 영속성 컨텍스트와 쓰기 지연
   
영속성이란? 데이터를 프로그램 종료 후에도 유지하기 위해 DB나 파일에 저장하여 영구적으로 보존하는 특성.
영속성 상태 1. 비영속 (new/transient) : 객체가 영속성 컨텍스트에 포함되지 않은 상태.
2. 영속 (managed) : 객체가 영속성 컨텍스트에 저장되어 관리되는 상태.
3. 준영속 (detached) : 영속성 컨텍스트에서 분리된 상태.
4. 삭제 (removed) : 객체가 삭제되어 영속성 컨텍스트와 DB에서 제거된 상태.
상태 전환 메서드 new > (비영속상태) > persist(),merge() > (영속성 컨텍스트에 저장된 상태) > flush() > (DB에 쿼리가 전송된 상태) > commit() > (DB에 쿼리가 반영된 상태)
쓰기 지연이란? - flush() 호출 전까지 SQL 쿼리를 영속성 컨텍스트에 모아두었다가, 한 번에 DB로 전송하는 최적화 메커니즘.
쓰기 지연 발생 시점 - 트랜잭션 중 객체 생성, 수정, 삭제 시.
- flush() 호출 전까지 쿼리를 최적화하여 보관.
쓰기 지연 효과 - 여러 동작을 모아 쿼리를 한번에 전송하여 최소화.
- 생성/수정/삭제 작업의 중간 상태가 발생하더라도 실제 DB에는 최적화된 쿼리만 전송.
- 불필요한 쿼리 전송 방지.
주의점 - GenerationType.IDENTITY 사용 시, 쓰기 지연이 적용되지 않음.
- 이유: IDENTITY 전략은 키 생성 시점에 단일 쿼리가 필요하며, 외부 트랜잭션 간의 키 중복을 방지하기 위해 즉시 DB에 반영됨.


예제 코드의 흐름

단계  동작 설명
1. 객체 생성 새로운 객체 생성 (new 상태).
2. 엔티티 매니저 생성 영속성 컨텍스트를 관리할 엔티티 매니저 생성.
3. 트랜잭션 시작 데이터의 무결성을 보장하기 위해 트랜잭션 시작.
4. 객체 저장 persist() 호출로 객체를 영속성 컨텍스트에 저장.
5. flush() 호출 영속성 컨텍스트의 SQL 쿼리를 DB로 전송 (commit 시 자동 수행).
6. commit() 호출 DB에 쿼리를 최종 반영.
7. 자원 해제 엔티티 매니저 및 팩토리 자원 반환 (close()).

 

ORM 을 사용하는 가장 쉬운 방법 : JpaRepository

 

💁‍♂️ Repository vs JpaRepository

  • 기존 Repository
    • @Repository 을 클래스에 붙인다.
    • @Component 어노테이션을 포함하고 있어서 앱 실행시 생성 후 Bean으로 등록된다.
    • 앞서배운 Repository 기본 기능만 가진 구현체가 생성된다. (DB별 예외처리 등)
  • 새로운 JpaRepository
    • JpaRepository<Entity,ID> 인터페이스를 인터페이스에 extends 붙인다.
      • @NoRepositoryBean 된 ****상위 인터페이스들의 기능을 포함한 구현체가 프로그래밍된다. (@NoRepositoryBean = 빈생성 막음 →상속받으면 생성돼서 사용가능)
        • JpaRepository (마스터 셰프): 데이터 액세스를 위한 핵심 기능의 종합적인 요리책(기능) 을 제공합니다.
        • @NoRepositoryBean 인터페이스 (셰프): 각 인터페이스는 특정 데이터 액세스 방법을 제공하는 전문적인 기술 또는 레시피를 나타냅니다.
        • JpaRepository 상속: 마스터 셰프의 요리책과 셰프의 전문성을 얻습니다.
      • SpringDataJpa 에 의해 엔티티의 CRUD, 페이징, 정렬 기능 메소드들을 가진 빈이 등록된다. (상위 인터페이스들의 기능) 

 

  • Repository 와 JpaRepository 를 통해 얼마나 간단하게 구현하게 될지 미리 확인해볼까요?
    • Repository 샘플
      • EntityManager 멤버변수를 직접적으로 사용
// UserRepository.java
@Repository
public class UserRepository {

  @PersistenceContext
  EntityManager entityManager;

  public User insertUser(User user) {
    entityManager.persist(user);
    return user;
  }

  public User selectUser(Long id) {
    return entityManager.find(User.class, id);
  }
}
  • JpaRepository 샘플
    • EntityManager 멤버변수를 간접적으로 사용
// UserRepository.java
public interface UserRepository extends JpaRepository<User, Long> {
  // 기본 메서드는 자동으로 만들어짐
}

 

 

테이블 객체 이해하기

도메인 모델과 테이블 설계

더보기

 

도메인 관계 요약 표

도메인  관계 설명 관계 유형
User - 채널과 양방향 관계.- 다른 도메인과 단방향 관계. - User ↔ Channel (양방향).
Channel - 유저와 다대다 관계.- 대화가 이루어지는 채널. - Channel ↔ User (다대다).
Thread - 채널 내 대화 쓰레드.- 댓글(Comment), 이모지(Emotion), 멘션(Mention)과 관계. - Thread ↔ Comment (일대다).- Thread ↔ Emotion (다대다).- Thread ↔ Mention (다대다).
Comment - 쓰레드 내 댓글.- 쓰레드와 다대일 관계.- 이모지(Emotion), 멘션(Mention)과 관계. - Comment ↔ Thread (다대일).- Comment ↔ Emotion (다대다).- Comment ↔ Mention (다대다).
Emotion - 쓰레드, 댓글 내 이모지.- 쓰레드와 댓글 각각과 다대다 관계. - Emotion ↔ Thread (다대다).- Emotion ↔ Comment (다대다).
Mention - 쓰레드, 댓글 내 멘션.- 쓰레드와 댓글 각각과 다대다 관계. - Mention ↔ Thread (다대다).- Mention ↔ Comment (다대다).

관계 예시

  1. User ↔ Channel:
    • 한 명의 유저는 여러 채널에 참여할 수 있고, 한 채널은 여러 유저를 가질 수 있음 (다대다).
  2. Thread ↔ Comment:
    • 한 쓰레드는 여러 댓글을 가질 수 있지만, 댓글은 하나의 쓰레드에만 속함 (다대일).
  3. Thread ↔ Emotion / Comment ↔ Emotion:
    • 여러 이모지가 하나의 쓰레드 또는 댓글에 추가될 수 있고, 하나의 이모지는 여러 쓰레드나 댓글에 사용될 수 있음 (다대다).
  4. Thread ↔ Mention / Comment ↔ Mention:
    • 여러 멘션이 하나의 쓰레드 또는 댓글에 포함될 수 있고, 멘션은 여러 쓰레드나 댓글에서 참조될 수 있음 (다대다).

이 구조는 소셜 미디어나 채팅 애플리케이션에서 자주 볼 수 있는 관계를 모델링합니다.

 

 

Raw JPA 테이블 타입 매핑 기능 

애노테이션  설명  예시 코드
@Entity - 객체 관점에서의 이름.- 기본값으로 클래스명을 사용.- JQL에서 사용되는 이름. @Entity public class User { ... }
@Table - RDB 테이블의 이름을 지정.- @Entity의 이름과 다르게 설정 가능.- SQL에서 사용되는 이름. @Table(name = "users")
@Id - 엔티티의 주 키(Primary Key) 매핑.- 기본 타입(primitive) 및 Date, BigDecimal, BigInteger 사용 가능. @Id private Long id;
@GeneratedValue - 주 키 생성 전략을 지정.- 기본값: AUTO.- 선택 가능: TABLE, SEQUENCE, IDENTITY. @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column - 컬럼 속성 지정.- 주요 옵션: unique, nullable, length, columnDefinition. @Column(name = "email", unique = true, length = 255)
@Temporal - 날짜/시간 타입 매핑.- Date, Calendar 타입만 지원. @Temporal(TemporalType.DATE)
@Transient - 데이터베이스 컬럼으로 매핑하지 않을 필드에 사용. @Transient private String tempData;

 

JPA Anotation 특징 및 활용 요약

애노테이션  특징  활용
@Entity - 클래스가 JPA에서 관리하는 엔티티임을 선언.- 데이터베이스 테이블과 매핑. - JPA가 관리할 객체를 정의할 때 사용.- 예: @Entity public class User { ... }
@Table - 엔티티와 매핑되는 RDB 테이블 이름을 명시적으로 설정.- 생략 시 기본값은 클래스명. - 엔티티 이름과 테이블 이름이 다를 때 사용.- 예: @Table(name = "users")
@Id - 엔티티의 **기본 키(Primary Key)**를 지정. - 필수 설정.- 예: @Id private Long id;
@GeneratedValue - 기본 키의 생성 전략을 지정.- AUTO, IDENTITY, SEQUENCE, TABLE 지원. - 데이터베이스에 적합한 키 생성 방식을 설정.- 예: @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column - 컬럼 속성을 세부적으로 지정.- 주요 속성: unique, nullable, length, columnDefinition. - 컬럼의 제약 조건 설정.- 예: @Column(name = "email", unique = true, nullable = false, length = 255)
@Temporal - 날짜/시간 데이터를 DB에 적합한 타입으로 매핑.- DATE, TIME, TIMESTAMP 지원. - java.util.Date 또는 Calendar 사용 시.- 예: @Temporal(TemporalType.TIMESTAMP)
@Transient - 비영속 필드를 선언.- 해당 필드는 DB에 매핑되지 않음. - DB에 저장되지 않고, 비즈니스 로직에서만 사용되는 데이터 정의.- 예: @Transient private String tempData;

예제 코드

@Entity
@Table(name = "users")
public class User {

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

    @Column(name = "email", unique = true, nullable = false, length = 255)
    private String email;

    @Temporal(TemporalType.TIMESTAMP)
    private Date createdAt;

    @Transient
    private String tempData; // DB에 매핑되지 않음
}

 

 

Raw JPA 필드 타입 매핑 기능

구분 설명 관련 애노테이션 예시 코드
기본 타입 - 문자열, 날짜, 불리언 등의 기본 데이터 타입.- 사이즈 제한, 필드명 지정 등 옵션 설정 가능. - @Column- @Enumerated java<br>@Column(name = "email", length = 255)<br>private String email;<br>@Enumerated(EnumType.STRING)<br>private UserType userType;<br>
Composite Value 타입 - 여러 필드를 하나의 값 객체로 묶어 매핑.- 복합 객체를 필드로 적용 가능. - @Embeddable- @Embedded- @AttributeOverrides, @AttributeOverride java<br>@Embeddable<br>public class Address {<br> private String city;<br> private String street;<br>}<br>@Embedded<br>@AttributeOverrides({...})<br>
Collection Value 타입 - 기본 타입 또는 Composite Value 타입의 컬렉션을 매핑.- 주로 @ElementCollection으로 구현. - @ElementCollection java<br>@ElementCollection<br>private List<String> tags = new ArrayList<>();<br>

주요 활용 및 예제

1. 기본 타입 매핑

@Entity
public class User {

    @Id
    @GeneratedValue
    private Long id;

    @Column(name = "email", length = 255, nullable = false)
    private String email;

    @Enumerated(EnumType.STRING) // Enum 값은 STRING으로 매핑
    private UserType userType;
}

2. Composite Value 타입 매핑

@Embeddable
public class Address {
    private String city;
    private String street;
}

@Entity
public class Member {

    @Id
    @GeneratedValue
    private Long id;

    @Embedded
    @AttributeOverrides({
        @AttributeOverride(name = "city", column = @Column(name = "home_city")),
        @AttributeOverride(name = "street", column = @Column(name = "home_street"))
    })
    private Address homeAddress;

    @Embedded
    @AttributeOverrides({
        @AttributeOverride(name = "city", column = @Column(name = "company_city")),
        @AttributeOverride(name = "street", column = @Column(name = "company_street"))
    })
    private Address companyAddress;
}

생성되는 테이블:

CREATE TABLE MEMBER (
  MEMBER_ID BIGINT NOT NULL AUTO_INCREMENT,
  NAME VARCHAR(255) NOT NULL,
  home_city VARCHAR(255) NOT NULL,
  home_street VARCHAR(255) NOT NULL,
  company_city VARCHAR(255) NOT NULL,
  company_street VARCHAR(255) NOT NULL,
  PRIMARY KEY (MEMBER_ID)
);

3. Collection Value 타입 매핑

@Entity
public class Product {

    @Id
    @GeneratedValue
    private Long id;

    @ElementCollection
    @CollectionTable(name = "product_tags", joinColumns = @JoinColumn(name = "product_id"))
    @Column(name = "tag")
    private List<String> tags = new ArrayList<>();
}

생성되는 테이블:

CREATE TABLE product_tags (
  product_id BIGINT NOT NULL,
  tag VARCHAR(255),
  PRIMARY KEY (product_id, tag)
);

특징 및 활용 요약

  • 기본 타입: 일반적인 데이터 저장에 사용되며, 설정 옵션으로 제약조건 지정.
  • Composite Value 타입: 코드의 응집도를 높이고 복합 데이터를 쉽게 관리.
  • Collection Value 타입: 여러 값 관리에 유용하지만, 대규모 데이터에서는 다대일 연관관계를 선호.

 

테이블 객체 만들기

User Entity 만들어보기

  • id, username, password 를 가지는 User Entity 를 만들어 봅니다.
// User.java
// lombok
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@ToString

// jpa
@Entity
@Table(name = "users")
public class User {

  /**
   * 컬럼 - 연관관계 컬럼을 제외한 컬럼을 정의합니다.
   */
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Column(name = "id")
  private Long id;

  private String username;

  private String password;

  /**
   * 생성자 - 약속된 형태로만 생성가능하도록 합니다.
   */
  @Builder
  public User(String username, String password) {
    this.username = username;
    this.password = password;
  }

  /**
   * 연관관계 - Foreign Key 값을 따로 컬럼으로 정의하지 않고 연관 관계로 정의합니다.
   */
  @OneToMany
  @Exclude
  private Set<UserChannel> userChannel;

  /**
   * 연관관계 편의 메소드 - 반대쪽에는 연관관계 편의 메소드가 없도록 주의합니다.
   */

  /**
   * 서비스 메소드 - 외부에서 엔티티를 수정할 메소드를 정의합니다. (단일 책임을 가지도록 주의합니다.)
   */
  public void updateUserName(String username) {
    this.username = username;
  }

  public void updatePassword(String password) {
    this.password = password;
  }
}

기타 추천 플러그인

Key PromoterX

  • 단축키 알림

Presentation Assistant

  • 알림 이쁘게 보여주기

 

테이블 객체끼리 관계만들기

 

Raw JPA 연관관계 매핑 기능 요약 표

애노테이션  설명  주요 속성 예시 코드
@OneToOne - 1:1 관계를 매핑.
- 단방향 및 양방향 매핑 가능.- 테이블 분리 여부를 신중히 검토.
- mappedBy: 연관관계 주인 지정.- cascade: 영속성 전이 설정.- fetch: 기본 EAGER. java<br>@OneToOne @JoinColumn(name = "LOCKER_ID") private Locker locker;<br>@OneToOne(mappedBy = "locker") private Member member;<br>
@OneToMany - 1:N 관계를 매핑.
- 단방향 사용 시 비효율적.
- 양방향은 @ManyToOne과 함께 사용.
- mappedBy: 연관관계 주인 필드 지정.- fetch: 기본 LAZY.- cascade: 영속성 전이. java<br>@OneToMany(mappedBy = "parent") private List<Child> childList;<br>@ManyToOne @JoinColumn(name = "parent_id") private Parent parent;<br>
@ManyToOne - N:1 관계를 매핑.
- 가장 많이 사용하는 연관관계.
- JoinColumn과 함께 사용.
- optional: 연관 객체 필수 여부.- fetch: 기본 EAGER, 실무에서는 LAZY 권장.- cascade: 영속성 전이. java<br>@ManyToOne @JoinColumn(name = "parent_id") private Parent parent;<br>
@JoinColumn - 외래키 매핑 시 사용.
- 주로 @ManyToOne과 함께 사용.
- name: 외래키 이름.- referencedColumnName: 참조 대상 컬럼.- unique, nullable, columnDefinition. java<br>@JoinColumn(name = "parent_id") private Parent parent;<br>
@ManyToMany - N:M 관계 매핑.
- 중간 매핑 테이블은 자동 생성.
- 실무에서는 사용을 지양.
- mappedBy: 양방향 매핑 시 주인 지정.- joinTable: 중간 매핑 테이블 이름 지정. java<br>@ManyToMany @JoinTable(name = "parent_child", joinColumns = @JoinColumn(name = "parent_id"), inverseJoinColumns = @JoinColumn(name = "child_id")) private List<Parent> parents;<br>

주요 연관관계 예제

1. @OneToOne 단방향 매핑

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

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

@Entity
public class Locker {
    @Id
    @GeneratedValue
    private Long id;
}

2. @OneToMany와 @ManyToOne 양방향 매핑

@Entity
public class Parent {
    @Id
    @GeneratedValue
    private Long id;

    @OneToMany(mappedBy = "parent")
    private List<Child> childList;
}

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

    @ManyToOne
    @JoinColumn(name = "parent_id")
    private Parent parent;
}

3. @ManyToMany 매핑

@Entity
public class Parent {
    @Id
    @GeneratedValue
    private Long id;

    @ManyToMany(mappedBy = "parents")
    private List<Child> childs;
}

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

    @ManyToMany
    @JoinTable(
        name = "parent_child",
        joinColumns = @JoinColumn(name = "parent_id"),
        inverseJoinColumns = @JoinColumn(name = "child_id")
    )
    private List<Parent> parents;
}

실무 권장 사항

  1. @ManyToMany 지양: 중간 매핑 테이블을 직접 정의하여 관리.
  2. fetch 기본값 조정: 대부분의 관계를 LAZY로 설정.
  3. 단방향 vs 양방향: 필요에 따라 양방향 관계를 신중히 설정.

 

 

 

 

 

 

📌 

  •  

 

 

 

📌 

  •  

 

 

 

📌 

  •  

 

 

 

📌 

  •  

 

 

 

📌 

  •  

 

 

 

📌 

  •  

 

 

 

📌 

  •  

 

 

 

📌 

  •  

 

 

 

📌 

  •  

 

 

 

📌 

  •  

 

 


 

 

 🐳 

  •  

 

 

 

 🐳 

  •  

 

 

 

 🐳 

  •  

 

 


 

소제목 

🧩 부모 타입 변수 = 자식 타입 객체; 는 자동으로 부모 타입으로 변환이 일어납니다.

 


 

소제목 

🎵 클래스가 설계도라면 추상 클래스는 미완성된 설계도입니다.

'DB > JPA ( Java Persistence API )' 카테고리의 다른 글

[JPA] SpringData JPA 심화  (0) 2025.01.28
[JPA] 테이블 객체  (0) 2025.01.27
[JPA] 데이터베이스 연결 (Driver)  (1) 2025.01.25
[JPA] JPA와 Transaction  (0) 2025.01.17
[JPA] 지연로딩, 즉시로딩  (0) 2025.01.15

1. 데이터베이스 드라이버의 역할 및 종류

항목 설명 예시/종류
드라이버 역할 애플리케이션과 데이터베이스 간의 통신을 중개. 애플리케이션 요청을 데이터베이스가 이해할 수 있는 언어로 변환. 우체부나 통역사의 역할과 유사.
드라이버 종류 데이터베이스별로 다른 드라이버가 필요. - Oracle: oracle.jdbc.OracleDriver- MySQL: com.mysql.jdbc.Driver- H2: org.h2.Driver


2. 데이터베이스 드라이버 동작 방식

단계 설명 비유
연결 초기화 드라이버가 데이터베이스와 연결. 네트워크 정보, 인증 자격 증명을 사용. 항공기✈️가 관제탑에 이륙 요청.
SQL 전송 및 실행 애플리케이션의 SQL 명령을 데이터베이스가 이해할 수 있도록 변환하여 처리. 외국어를 현지 언어로 번역하는 통역사 역할.
결과 처리 데이터베이스가 반환한 결과를 애플리케이션이 이해할 수 있도록 변환하여 전달. 관제탑이 비행기 착륙을 안전하게 안내.
연결 종료 데이터베이스 연결을 해제하여 자원을 정리하고 시스템 초기화. 항공기✈️가 공항 게이트에 도킹.

[1]. Spring Boot 의 JDBC 라이브러리

  • Spring Boot와 JDBC: Spring Boot는 데이터베이스 연결을 쉽게 구성할 수 있도록 다양한 JDBC 드라이버를 지원합니다. 이를 통해 개발자는 복잡한 설정 없이 데이터베이스와의 연결을 쉽게 구성할 수 있습니다.
  • **spring-boot-starter-jdbc**는 Spring Boot 프로젝트에서 JDBC를 통해 데이터베이스와 상호작용하기 위해 사용되는 스타터 패키지입니다.
    • 이 스타터 패키지는 데이터베이스 작업을 수행하는 데 필요한 주요 의존성과 자동 구성 기능을 제공합니다. 데이터베이스와의 연결을 쉽고 빠르게 구성할 수 있도록 도와주는 브릿지 역할을 합니다.
    • 주요 포함 내용
      1. JDBC API 지원: JDBC API를 통해 SQL 데이터베이스에 접근하고 작업을 수행할 수 있습니다.
      2. DataSource 구성: 데이터 소스 연결을 위한 기본적인 설정을 자동으로 구성합니다. 이는 데이터베이스 연결을 관리하는 데 필수적인 요소입니다.
      3. JdbcTemplate: **JdbcTemplate**은 Spring의 핵심 클래스 중 하나로, JDBC 작업의 많은 번거로움을 줄여 줍니다. SQL 쿼리 실행, 결과 세트 처리, 예외 처리 등을 단순화합니다.
    • 주요 장점 내용
      1. 간소화된 데이터베이스 연결: DataSource 설정과 JdbcTemplate 사용을 통해 복잡한 JDBC 코드를 간소화할 수 있습니다.
      2. 자동 구성: Spring Boot의 자동 구성 기능은 개발자가 데이터베이스 연결에 필요한 대부분의 구성을 자동으로 처리할 수 있도록 합니다.
      3. 효율적인 예외 처리: Spring의 **DataAccessException**을 통해 JDBC에서 발생하는 예외를 Spring의 일관된 예외 체계로 변환합니다.

[2]. JDBC

  • JDBC 드라이버란?: JDBC(Java Database Connectivity)는 자바 애플리케이션에서 데이터베이스에 접근할 수 있도록 하는 API입니다. JDBC 드라이버는 이 API를 구현하여, 자바 애플리케이션과 특정 데이터베이스 간의 연결을 가능하게 합니다.
  • JDBC 드라이버 타입: JDBC 드라이버는 네 가지 유형(Type 1, 2, 3, 4)이 있으며, 각각의 특징과 사용 환경에 따라 선택할 수 있습니다. Type 4 드라이버(순수 자바 드라이버)가 가장 일반적으로 사용됩니다.
  • 문장 그대로 Java 앱과 DB 를 연결시켜주기 위해 만들어진 기술입니다.
  • 그렇기 때문에 JPA 도 이 기술을 사용하여 구현되어 있습니다. ✅
  • JDBC Driver 는 여러타입의 DB 와 연결할 수 있는 기능을 제공합니다.
  • JDBC 실습
    • DriverManager 를 통해서 Connection(연결) 을 생성하여 쿼리를 요청할 수 있는 상태를 만들어주고
    • Statement(상태) 를 생성하여 쿼리를 요청하고
    • ResultSet(결과셋) 을 생성해 쿼리 결과를 받아옵니다.

 

[3]. JDBC Template (QueryMapper)

  • JDBC 로 직접 SQL을 작성했을때의 문제
    • SQL 쿼리 요청시 중복 코드 발생
    • DB별 예외에 대한 구분 없이 Checked Exception (SQL Exception) 처리
    • Connection, Statement 등.. 자원 관리를 따로 해줘야함
      • 자원 해제 안해주면 메모리 꽉차서 서버가 죽음
  • 이 문제 해결을 위해 처음으로 Persistence Framework 등장!
    • Persistence Framework 는 2가지가 있다.
      • SQL Mapper : JDBC Template, MyBatis 👈 요게 먼저나옴
      • ORM : JPA, Hibernate
  • SQL Mapper (QueryMapper)
    • SQL ↔ Object
    • SQL 문과 객체(Object)의 필드를 매핑하여 데이터를 객채화

JDBC Template (RowMapper)

  • SQL Mapper 첫번째 주자로 JDBCTemplate 에 RowMapper 탄생
    • 쿼리 수행 결과와 객채 필드 매핑
    • RowMapper 로 응답필드 매핑코드 재사용
    • Connection, Statement, ResultSet 반복적 처리 대신 해줌
    • 😵‍💫 But, 결과값을 객체 인스턴스에 매핑하는데 여전히 많은 코드가 필요함

3. JDBC 주요 객체 및 역할

객체  역할  사용 예시
Connection 데이터베이스와의 연결을 생성 및 관리. Connection conn = DriverManager.getConnection(url, username, password);
Statement SQL 명령어를 데이터베이스에 전달하는 역할. Statement stmt = conn.createStatement();
PreparedStatement Statement의 확장. 파라미터화된 SQL 쿼리를 효율적으로 실행하며 SQL Injection 방지. PreparedStatement pstmt = conn.prepareStatement("INSERT INTO users VALUES (?, ?)");
ResultSet 쿼리 실행 결과를 저장하고 처리. ResultSet rs = stmt.executeQuery("SELECT * FROM users");

4. JDBC Template 주요 특징

항목  설명
중복 코드 제거 Connection, Statement, ResultSet과 같은 반복적인 코드 제거.
자원 관리 자동화 자원의 자동 반환을 통해 메모리 누수를 방지.
예외 처리 간소화 JDBC의 Checked Exception(SQL Exception)을 Spring의 일관된 예외 체계로 변환.
RowMapper 제공 SQL 결과를 객체와 매핑하는 기능을 제공하여 결과 매핑 코드를 단순화.

5. JDBC Template vs. SQL Mapper vs. ORM

  JDBC Template SQL Mapper (MyBatis 등) ORM (JPA/Hibernate)
주요 특징 SQL 문을 직접 작성하지만 반복적인 작업을 단순화. SQL 문과 객체 필드를 매핑하여 데이터 객체화. 데이터베이스 테이블과 객체 간의 매핑을 자동화.
장점 반복 작업 제거, 코드 간결화. SQL 활용이 유연하며 성능 최적화 가능. 객체 중심의 설계, 개발 생산성 향상.
단점 SQL 코드가 여전히 필요. XML 설정 파일 및 복잡한 매핑 관리가 필요. 초기 학습 곡선이 가파르며 성능 최적화가 어려움.

6. PreparedStatement vs Statement

  Statement  PreparedStatement
SQL Injection 방지 지원하지 않음. 변수 바인딩으로 Injection 방지.
성능 매번 구문 분석(parse) 수행. SQL 구문 분석 결과를 캐싱하여 성능 향상.
사용 방식 SQL 전체를 코드에 작성. SQL에 파라미터를 설정하여 실행.
예시 코드 stmt.executeQuery("SELECT * FROM users WHERE id = " + id); pstmt.setInt(1, id); pstmt.executeQuery();

 

'DB > JPA ( Java Persistence API )' 카테고리의 다른 글

[JPA] 테이블 객체  (0) 2025.01.27
[JPA] 쿼리 파일 만들기 (QueryMapper)  (0) 2025.01.26
[JPA] JPA와 Transaction  (0) 2025.01.17
[JPA] 지연로딩, 즉시로딩  (0) 2025.01.15
[JPA] Proxy  (0) 2025.01.14

트랜잭션 전파

📌 하나의 트랜잭션이 다른 트랜잭션 내에서 어떻게 동작할지를 결정하는 규칙으로 여러 개의 트랜잭션이 포함된 시스템에서 특정 작업이 다른 작업에 어떻게 영향을 미칠지를 정의한다.

  • 현재 클래스의 트랜잭션과 다른 클래스의 트랜잭션을 교통정리 한다.

  • 트랜잭션이 여러 계층 또는 메서드에서 어떻게 처리될지 정의한다.(@Transactional)
  • propagation 속성을 통해 트랜잭션의 동작 방식을 제어할 수 있다.
  • 다양한 비즈니스 요구 사항에 맞춰 복잡한 트랜잭션 흐름을 유연하게 설계할 수 있도록 돕는다.
  • 데이터 무결성과 비지니스 로직의 안정성을 보장할 수 있다.

 

  • 코드 예시
    • REQUIRED(Default) 사용
@Service
@RequiredArgsConstructor
public class MemberService {
    
    private final PointPolicy pointPolicy;

    @Transactional
    public void signUp(Member member) {
        // 회원 등록
        memberRepository.save(member);

        // 포인트 지급
        pointPolicy.addPoints(member.getId(), 100);
    }
}

@Component
public class PointPolicy {
    public void addPoints(Long memberId, int points) {
        // 포인트 지급 로직
        pointRepository.save(new Point(memberId, points));
    }
}
  • signUp() 메서드에 @Transactional 을 통해 트랜잭션 설정
  • 하위 addPoints() 메서드에 트랜잭션이 전파된다.
  • 하위 메서드가 실패하면 롤백된다.
    • 포인트 지급 로직에서 문제가 발생해도 회원 등록은 롤백된다.
  • 트랜잭션 동작

 

  • 트랜잭션 전파 종류
    • propagation 속성
      1. REQUIRED(Default)
        • 기존 트랜잭션이 있다면 기존 트랜잭션을 사용한다.
        • 기존 트랜잭션이 없다면 트랜잭션을 새로 생성한다.
      2. REQUIRES_NEW
        • 항상 새로운 트랜잭션을 시작하고, 기존의 트랜잭션은 보류한다.
        • 두 트랜잭션은 독립적으로 동작한다.
      3. SUPPORTS
        • 기존 트랜잭션이 있으면 해당 트랜잭션을 사용한다.
        • 기존 트랜잭션이 없으면 트랜잭션 없이 실행한다.
      4. NOT_SUPPORTED
        • 기존 트랜잭션이 있어도 트랜잭션을 중단하고 트랜잭션 없이 실행된다.
      5. MANDATORY
        • 기존 트랜잭션이 반드시 있어야한다.
        • 트랜잭션이 없으면 실행하지 않고 예외를 발생시킨다.
      6. NEVER
        • 트랜잭션 없이 실행되어야 한다.
        • 트랜잭션이 있으면 예외를 발생시킨다.
      7. NESTED
        • 현재 트랜잭션 내에서 중첩 트랜잭션을 생성한다.
        • 중첩 트랜잭션은 독립적으로 롤백할 수 있다.
        • 기존 트랜잭션이 Commit되면 중첩 트랜잭션도 Commit 된다.

 

  • REQUIRES_NEW
    • 회원 가입과 동시에 회원에게 포인트를 지급해야 하는 경우
@Service
@RequiredArgsConstructor
public class MemberService {

    private final PointPolicy pointPolicy;

    @Transactional
    public void signUp(Member member) {
        // 회원 등록
        memberRepository.save(member);

        // 포인트 지급
        pointPolicy.addPoints(member.getId(), 100);
    }
}

@Component
public class PointPolicy {

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void addPoints(Long memberId, int points) {
        // 포인트 지급 로직
        pointRepository.save(new Point(memberId, points));
    }
    
}
  • 요구사항 : 포인트 지급에 **실패**해도 회원가입은 완료되어야 한다.
  • REQUIRES_NEW 설정(독립적인 트랜잭션 유지)
    • 포인트 지급 로직에서 문제가 발생해도 회원 등록은 롤백되지 않는다.
  • 트랜잭션 동작

 

 

트랜잭션

**트랜잭션(Transaction)**은 데이터베이스에서 작업의 논리적인 단위를 의미합니다. 트랜잭션은 하나의 작업 단위를 구성하며, 이 단위가 완전히 성공하거나 실패해야 데이터의 무결성을 보장합니다.


트랜잭션의 특징 (ACID 속성)

  1. 원자성 (Atomicity):
    • 트랜잭션 내의 모든 작업이 모두 성공하거나 모두 실패해야 합니다.
    • 일부 작업만 실행되고 나머지가 실패하면, 전체 작업을 취소(롤백)하여 데이터 일관성을 유지합니다.
  2. 일관성 (Consistency):
    • 트랜잭션이 성공적으로 완료되면, 데이터베이스가 항상 일관성 있는 상태로 유지됩니다.
    • 예: 은행 송금 시, 한 계좌에서 돈을 빼면 다른 계좌에 같은 금액이 추가되어야 함.
  3. 고립성 (Isolation):
    • 여러 트랜잭션이 동시에 실행될 경우, 각 트랜잭션은 서로 독립적으로 실행되어야 합니다.
    • 한 트랜잭션의 중간 결과가 다른 트랜잭션에 노출되지 않음.
  4. 지속성 (Durability):
    • 트랜잭션이 커밋된 후에는 영구적으로 데이터베이스에 반영되어야 합니다.
    • 서버가 중단되거나 시스템 장애가 발생해도 데이터는 손실되지 않습니다.

트랜잭션의 상태

  1. 활성 (Active):
    • 트랜잭션이 시작되고 작업이 진행 중인 상태.
  2. 부분 완료 (Partially Committed):
    • 트랜잭션의 마지막 명령이 실행되었지만, 아직 커밋되지 않은 상태.
  3. 완료 (Committed):
    • 트랜잭션이 성공적으로 완료되어 데이터베이스에 반영된 상태.
  4. 실패 (Failed):
    • 트랜잭션이 오류로 인해 중단된 상태.
  5. 철회 (Aborted):
    • 트랜잭션이 실패하거나 취소되어 롤백된 상태.

트랜잭션의 처리 과정

  1. 트랜잭션 시작:
    • 트랜잭션을 시작하여 작업 단위를 정의.
  2. 작업 실행:
    • 트랜잭션 내에서 여러 데이터베이스 작업(쿼리, 삽입, 업데이트, 삭제 등)을 실행.
  3. 커밋 또는 롤백:
    • 모든 작업이 성공하면 커밋하여 데이터베이스에 변경 사항을 반영.
    • 작업 중 오류가 발생하면 롤백하여 변경 사항을 취소.

Spring에서의 트랜잭션 관리

Spring은 프록시 기반 AOP를 사용하여 트랜잭션 관리를 제공합니다. Spring의 트랜잭션 관리 기능은 선언적 방식과 프로그래밍 방식으로 사용 가능합니다.

1. 선언적 트랜잭션 관리

Spring에서는 @Transactional 어노테이션을 사용하여 선언적으로 트랜잭션을 관리할 수 있습니다.

@Service
public class UserService {

    @Transactional
    public void createUser(User user) {
        userRepository.save(user);
        emailService.sendWelcomeEmail(user); // 예외 발생 시 전체 롤백
    }
}
  • @Transactional이 붙은 메서드는 트랜잭션이 시작되고, 예외 발생 시 롤백됩니다.
  • 커밋은 메서드 실행이 성공적으로 종료되면 수행됩니다.

2. 프로그래밍 방식 트랜잭션 관리

프로그래밍 방식으로 PlatformTransactionManager를 사용하여 트랜잭션을 직접 관리할 수 있습니다.

@Service
public class UserService {

    @Autowired
    private PlatformTransactionManager transactionManager;

    public void createUser(User user) {
        TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());

        try {
            userRepository.save(user);
            emailService.sendWelcomeEmail(user);
            transactionManager.commit(status); // 성공 시 커밋
        } catch (Exception e) {
            transactionManager.rollback(status); // 실패 시 롤백
        }
    }
}

트랜잭션 전파 수준 (Propagation)

트랜잭션 전파(Propagation)는 트랜잭션이 다른 메서드 호출 시 어떻게 동작할지 정의합니다.

전파 속성 설명

REQUIRED 기본값. 기존 트랜잭션이 있으면 참여하고, 없으면 새 트랜잭션 생성.
REQUIRES_NEW 항상 새 트랜잭션을 생성. 기존 트랜잭션은 일시 중단.
NESTED 중첩 트랜잭션을 생성. 롤백은 부모 트랜잭션과 독립적.
MANDATORY 기존 트랜잭션이 없으면 예외 발생.
SUPPORTS 트랜잭션이 있으면 참여, 없으면 트랜잭션 없이 실행.
NOT_SUPPORTED 항상 트랜잭션 없이 실행. 기존 트랜잭션은 일시 중단.
NEVER 트랜잭션 없이 실행하며, 기존 트랜잭션이 있으면 예외 발생.

트랜잭션 격리 수준 (Isolation)

트랜잭션 격리 수준은 동시에 실행되는 트랜잭션 간의 상호작용을 제어합니다.

격리 수준 설명

DEFAULT 데이터베이스의 기본 격리 수준을 따름.
READ_UNCOMMITTED 다른 트랜잭션이 커밋하지 않은 데이터도 읽을 수 있음. (Dirty Read 가능)
READ_COMMITTED 다른 트랜잭션이 커밋한 데이터만 읽을 수 있음.
REPEATABLE_READ 같은 트랜잭션 내에서 동일 데이터를 반복적으로 읽어도 동일한 결과를 보장.
SERIALIZABLE 가장 높은 격리 수준. 트랜잭션을 순차적으로 실행하여 충돌 방지.

트랜잭션 롤백 조건

Spring의 @Transactional은 기본적으로 런타임 예외가 발생할 때 롤백합니다.

롤백 예외 설정

@Transactional(rollbackFor = Exception.class)
public void process() {
    // Exception 발생 시 롤백
}

롤백 제외 설정

@Transactional(noRollbackFor = CustomException.class)
public void process() {
    // CustomException 발생 시 롤백하지 않음
}

트랜잭션의 장점

  1. 데이터 무결성 보장:
    • 작업 도중 오류가 발생해도 데이터 손상 방지.
  2. 동시성 제어:
    • 여러 사용자가 동시에 데이터베이스에 접근해도 데이터 충돌 방지.
  3. 복잡한 작업 관리:
    • 여러 작업 단위를 하나의 트랜잭션으로 묶어 관리 가능.
  4. 자동화된 관리:
    • Spring의 트랜잭션 관리를 통해 선언적으로 간단하게 설정 가능.

정리

**트랜잭션(Transaction)**은 하나의 논리적인 작업 단위를 정의하며, 데이터베이스의 무결성과 일관성을 보장합니다. Spring은 선언적 트랜잭션(@Transactional)을 제공하여 개발자가 쉽게 트랜잭션을 관리할 수 있도록 돕습니다.

핵심 개념:

  1. ACID 속성: 원자성, 일관성, 고립성, 지속성.
  2. Spring의 지원: 선언적(@Transactional), 프로그래밍 방식.
  3. 전파와 격리 수준: 트랜잭션의 실행 방식을 제어.

'DB > JPA ( Java Persistence API )' 카테고리의 다른 글

[JPA] 쿼리 파일 만들기 (QueryMapper)  (0) 2025.01.26
[JPA] 데이터베이스 연결 (Driver)  (1) 2025.01.25
[JPA] 지연로딩, 즉시로딩  (0) 2025.01.15
[JPA] Proxy  (0) 2025.01.14
[JPA] 상속관계 매핑  (0) 2025.01.13

Lazy Loading

📌 지연 로딩(Lazy Loading)은 데이터를 실제로 사용할 때 데이터베이스에서 조회하는 방식

 

JPA의 지연로딩

  • fetch 속성 사용
    • FetchType.LAZY : 지연로딩
  • 지연로딩을 사용하면 Proxy 객체를 조회한다.
  • 연관된 객체(Company)를 매번 함께 조회하는것은 낭비인 경우에 사용한다.
@Entity
@Table(name = "tutor")
public class Tutor {

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

    private String name;
		
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "company_id")
    private Company company;

    public Tutor() {
    }

    public Tutor(String name) {
        this.name = name;
    }

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public Company getCompany() {
        return company;
    }

    public void setCompany(Company company) {
        this.company = company;
    }
}
public class FetchTypeLazyMain {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("entity");

        EntityManager em = emf.createEntityManager();

        EntityTransaction transaction = em.getTransaction();

        transaction.begin();

        try {

            Company company = new Company("sparta");
            em.persist(company);

            Tutor tutor = new Tutor("wonuk");
            tutor.setCompany(company);
            em.persist(tutor);

            // 영속성 컨텍스트 초기화
            em.flush();
            em.clear();

            // em.find()
            Tutor findTutor = em.find(Tutor.class, tutor.getId());

            transaction.commit();
        } catch (Exception e) {
            transaction.rollback();
            e.printStackTrace();
        } finally {
            em.close();
        }

        emf.close();
    }
}

Tutor만 조회한다.

 

getCompany()

System.out.println("findTutor.getCompany().getClass() = " + findTutor.getCompany().getClass());

 

실행결과

Proxy로 조회한다.

 

getCompany().getName()

System.out.println("findTutor.getCompany().getName() = " + findTutor.getCompany().getName());

 

실행결과

  • 실제 값에 접근할 때 조회 SQL이 실행된다.
  • 실제 Company 의 값을 사용하는 시점에 초기화(DB 조회)된다.
  • 지연 로딩을 사용하면 연관된 객체를 Proxy로 조회한다.

 

Eager Loading

📌 즉시 로딩(Eager Loading)은 엔티티를 조회할 때 연관된 데이터까지 모두 한 번에 로드하는 방식

 

  • JPA의 즉시 로딩
    • fetch 속성 사용
      • FetchType.EAGER : 즉시 로딩
    • Proxy 객체를 조회하지 않고 한 번에 연관된 객체까지 조회한다.
    • 연관된 객체(Company)를 매번 함께 조회하는것이 효율적인 경우에 사용한다.
@Entity
@Table(name = "tutor")
public class Tutor {

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

    private String name;

    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = "company_id")
    private Company company;

    public Tutor() {
    }

    public Tutor(String name) {
        this.name = name;
    }

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public Company getCompany() {
        return company;
    }

    public void setCompany(Company company) {
        this.company = company;
    }
}
public class FetchTypeEagerMain {

    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("entity");

        EntityManager em = emf.createEntityManager();

        EntityTransaction transaction = em.getTransaction();

        transaction.begin();

        try {

            Company company = new Company("sparta");
            em.persist(company);

            Tutor tutor = new Tutor("wonuk");
            tutor.setCompany(company);
            em.persist(tutor);

            // 영속성 컨텍스트 초기화
            em.flush();
            em.clear();

            // em.find()
            Tutor findTutor = em.find(Tutor.class, tutor.getId());

            // getCompany()
            System.out.println("findTutor.getCompany().getClass() = " + findTutor.getCompany().getClass());

            // getCompany().getName()
            System.out.println("findTutor.getCompany().getName() = " + findTutor.getCompany().getName());

            transaction.commit();
        } catch (Exception e) {
            transaction.rollback();
            e.printStackTrace();
        } finally {
            em.close();
        }

        emf.close();
    }
    
}

JOIN을 사용해 한번의 SQL로 모두 조회하기 때문에 Proxy가 필요없다.

 

 

즉시 로딩 주의점

 

 

코드 예시(N+1 문제)

List<Tutor> tutorList = em.createQuery("select t from Tutor t", Tutor.class).getResultList();

 

실행결과

  • 조회 SQL이 N+1번 실행된다.
    • 처음 실행된 최초 SQL Query : 1(Tutor)
    • 연관된 객체 조회 SQL Query : N(Company)
  • JPQL은 SQL이 그대로 변환되어 조회된 Tutor 만큼 EAGER로 설정된 Company가 함께 조회된다.
  • em.find() 는 JPA가 내부적으로 최적화한다.

N+1 문제 해결 방법

  • 모든 연관관계를 LAZY로 설정한다.
  1. JPQL fetch join : Rumtime에 원하는 Entity를 함께 조회할 수 있다.(대부분 사용)
  2. @EntityGraph
  3. @BatchSize
  4. Native Query

 

'DB > JPA ( Java Persistence API )' 카테고리의 다른 글

[JPA] 데이터베이스 연결 (Driver)  (1) 2025.01.25
[JPA] JPA와 Transaction  (0) 2025.01.17
[JPA] Proxy  (0) 2025.01.14
[JPA] 상속관계 매핑  (0) 2025.01.13
[JPA] 연관관계  (0) 2025.01.12

Entity 조회

📌 em.getReference()는 JPA의 EntityManager에서 제공하는 메서드로 특정 엔티티의 프록시 객체를 반환한다. 지연 로딩(Lazy Loading)을 활용해 데이터베이스 조회를 미루고 실제로 엔티티의 속성에 접근할 때만 데이터베이스를 조회하도록 한다.

 

@Entity
@Table(name = "tutor")
public class Tutor {

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

    private String name;

    @ManyToOne
    @JoinColumn(name = "company_id")
    private Company company;

    public Tutor() {
    }

    public Tutor(String name) {
        this.name = name;
    }

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }
    
    public Company getCompany() {
        return company;
    }

    public void setCompany(Company company) {
        this.company = company;
    }
}
// 1. tutor의 company를 함께 조회하는 경우
Tutor findTutor = em.find(Tutor.class, 1L);

String tutorName = findTutor.getName();
Company tutorCompany = findTutor.getCompany();

System.out.println("tutorName = " + tutorName);
System.out.println("tutorCompany.getName() = " + tutorCompany.getName());
// 2. tutor만 조회하는 경우
Tutor findTutor = em.find(Tutor.class, 1L);

String tutorName = findTutor.getName();

System.out.println("tutorName = " + tutorName);
  1. Tutor를 조회할 때 Company 를 함께 조회
    • Company를 매번 함께 조회하는것은 낭비이다.
  2. Tutor만 조회
    • Company 조회를 위해 추가적인 조회 SQL이 실행되어야 한다.
  • 이때 프록시를 사용하여 효율적으로 관리할 수 있다.

 

 

Proxy

📌 JPA에서 엔티티 객체의 지연 로딩(Lazy Loading)을 지원하기 위해 사용하는 대리 객체로 실제 엔티티 객체를 생성하거나 데이터베이스에서 값을 읽어오지 않고도 엔티티의 참조를 사용할 수 있다.

  • 대리자 또는 중간 대리 객체를 의미

  • 데이터베이스 조회를 지연하는 가짜(Proxy) 객체를 조회한다.
    • 실제 Entity와 == 비교 실패, instanceof 사용
  • target : 진짜 객체의 참조를 보관한다.

 

Proxy 객체 초기화

  1. em.getReference() : 프록시 객체 조회
  2. getName() : 프록시 객체의 getName() 호출
  3. JPA가 영속성 컨텍스트에 target 초기화 요청
  4. 실제 DB 조회
  5. Entity 생성
  6. target의 getName() 호출

 

Proxy 특징

  • 최초로 사용(실제 Entity에 접근)할 때 한 번만 초기화된다.
  • 프록시 객체를 통해 실제 Entity에 접근할 수 있다.
  • em.getReference() 호출 시 영속성 컨텍스트에 Entity가 존재하면 실제 Entity가 반환된다.
  • 준영속 상태에서 프록시를 초기화하면 LazyInitializationException 예외가 발생한다.
Tutor proxyTutor = em.getReference(Tutor.class, tutor.getId());
System.out.println("proxyTutor.getClass() = " + proxyTutor.getClass());

// 준영속 상태
em.detach(proxyTutor);

proxyTutor.getName();

 

실행결과

  • detach() : 영속성 컨텍스트가 관리하지 않는다.
  • 영속성 컨텍스트를 통해 도움을 받아야만 실제 Entity에 접근이 가능하다.
  • 실제 JPA 개발에서 가장 많이 마주치는 Exception

 

영속성 컨텍스트 (Persistence Context)

영속성 컨텍스트는 JPA(Java Persistence API)에서 엔티티 객체를 **영구 저장소(데이터베이스)**에 저장하거나 관리하는 중간 작업 영역을 의미합니다. 쉽게 말해, 엔티티 객체를 관리하는 JPA의 메모리 공간입니다.


영속성 컨텍스트의 주요 특징

  1. 엔티티 관리:
    • 영속성 컨텍스트는 엔티티 객체를 영속성 상태로 관리합니다.
    • 관리 중인 엔티티는 변경 사항이 자동으로 데이터베이스에 반영됩니다.
  2. 1차 캐시:
    • 영속성 컨텍스트는 엔티티를 1차 캐시에 저장하여, 동일한 엔티티를 데이터베이스에서 다시 조회하지 않도록 최적화합니다.
  3. 엔티티 동일성 보장:
    • 동일한 영속성 컨텍스트 내에서는 동일한 엔티티 객체를 공유(동일성 보장)합니다.
  4. 변경 감지:
    • 영속성 컨텍스트는 관리 중인 엔티티의 변경 사항을 감지하여 데이터베이스에 자동으로 반영합니다.
  5. 쓰기 지연:
    • 트랜잭션 커밋 시점에 변경 사항을 한꺼번에 데이터베이스에 반영하여 성능을 최적화합니다.
  6. 지연 로딩:
    • 필요한 시점에만 데이터베이스에서 데이터를 로드하여 효율적으로 자원을 사용합니다.

영속성 컨텍스트의 상태

  1. 비영속 (Transient):
    • 영속성 컨텍스트에서 관리되지 않는 상태.
    • 데이터베이스와 전혀 관련이 없는 상태.
    User user = new User(); // 비영속 상태
    user.setName("John");
    
  2. 영속 (Persistent):
    • 영속성 컨텍스트에 의해 관리되는 상태.
    • 데이터베이스와 연동되며 변경 사항이 자동으로 반영됩니다.
    User user = new User();
    user.setName("John");
    entityManager.persist(user); // 영속 상태
    
  3. 준영속 (Detached):
    • 영속성 컨텍스트에서 관리되지 않지만, 이전에 영속 상태였던 엔티티.
    • 데이터베이스와 연동되지 않습니다.
    entityManager.detach(user); // 준영속 상태
    
  4. 삭제 (Removed):
    • 삭제 요청이 되어 데이터베이스에서 삭제될 예정인 상태.
    • 트랜잭션이 커밋되면 데이터베이스에서 삭제됩니다.
    entityManager.remove(user); // 삭제 상태
    

영속성 컨텍스트의 주요 기능

1. 1차 캐시

  • 영속성 컨텍스트는 엔티티를 1차 캐시에 저장합니다.
  • 동일한 엔티티를 반복적으로 조회할 경우, 데이터베이스 대신 1차 캐시에서 데이터를 가져옵니다.
User user1 = entityManager.find(User.class, 1L); // DB 조회
User user2 = entityManager.find(User.class, 1L); // 1차 캐시에서 조회

2. 엔티티 동일성 보장

  • 동일한 영속성 컨텍스트에서는 동일한 엔티티 객체를 반환합니다.
User user1 = entityManager.find(User.class, 1L);
User user2 = entityManager.find(User.class, 1L);

System.out.println(user1 == user2); // true (같은 객체)

3. 변경 감지 (Dirty Checking)

  • 엔티티의 필드 값이 변경되면 영속성 컨텍스트가 이를 감지하고, 트랜잭션 커밋 시점에 데이터베이스에 반영합니다.
User user = entityManager.find(User.class, 1L);
user.setName("Updated Name"); // 변경 감지

entityManager.flush(); // 변경 사항 반영

4. 쓰기 지연 (Write-Behind)

  • 데이터베이스에 변경 사항을 즉시 반영하지 않고, 트랜잭션 커밋 시점에 한꺼번에 반영합니다.
entityManager.persist(user1);
entityManager.persist(user2);

// SQL 실행은 트랜잭션 커밋 시점에 발생
transaction.commit();

5. 지연 로딩 (Lazy Loading)

  • 연관된 엔티티를 실제로 사용할 때까지 데이터베이스에서 로드하지 않습니다.
User user = entityManager.find(User.class, 1L);
List<Order> orders = user.getOrders(); // 이 시점에 DB에서 로드

영속성 컨텍스트의 동작 예시

엔티티 상태 전환

@Entity
public class User {
    @Id
    @GeneratedValue
    private Long id;

    private String name;
}

// 비영속 상태
User user = new User();
user.setName("John");

// 영속 상태
entityManager.persist(user);

// 준영속 상태
entityManager.detach(user);

// 삭제 상태
entityManager.remove(user);

트랜잭션과 영속성 컨텍스트

  1. 영속성 컨텍스트는 트랜잭션 범위 내에서 관리됩니다.
    • 트랜잭션이 종료되면 영속성 컨텍스트도 종료됩니다.
  2. 트랜잭션이 커밋되면 데이터베이스와 동기화됩니다.
    • 엔티티의 변경 사항이 데이터베이스에 반영됩니다.

영속성 컨텍스트의 장점

  1. 성능 최적화:
    • 1차 캐시와 쓰기 지연을 통해 데이터베이스 접근 횟수를 줄임.
  2. 변경 감지:
    • 엔티티의 변경 사항을 자동으로 반영하여 코드 단순화.
  3. 지연 로딩:
    • 필요한 시점에만 데이터를 로드하여 리소스 사용을 최소화.
  4. 엔티티 동일성 보장:
    • 동일한 트랜잭션 내에서 같은 데이터를 일관되게 처리 가능.

영속성 컨텍스트를 활용한 주요 작업 흐름

  1. 엔티티 생성 및 영속화:
    • 새 엔티티 객체를 생성하고, 영속성 컨텍스트에 추가.
  2. 엔티티 조회:
    • 데이터베이스에서 데이터를 가져와 영속성 컨텍스트에 저장.
  3. 변경 사항 감지:
    • 엔티티의 필드 값이 변경되면 자동으로 감지하여 반영.
  4. 엔티티 삭제:
    • 영속성 컨텍스트에서 엔티티를 제거하고, 데이터베이스에서도 삭제.

정리

영속성 컨텍스트는 JPA가 엔티티 객체를 관리하고, 데이터베이스와의 상호작용을 효율적으로 처리하기 위해 사용하는 핵심 메커니즘입니다.

  • 핵심 기능: 1차 캐시, 변경 감지, 쓰기 지연, 지연 로딩.
  • 장점: 성능 최적화, 코드 단순화, 데이터 일관성 보장.

쉽게 말해, 영속성 컨텍스트는 JPA가 엔티티 객체를 "관리"하고 "동기화"하는 공간입니다!

 

'DB > JPA ( Java Persistence API )' 카테고리의 다른 글

[JPA] JPA와 Transaction  (0) 2025.01.17
[JPA] 지연로딩, 즉시로딩  (0) 2025.01.15
[JPA] 상속관계 매핑  (0) 2025.01.13
[JPA] 연관관계  (0) 2025.01.12
[JPA] Spring Data JPA  (0) 2025.01.07

테이블 전략

📌 JPA에서 엔티티 상속 구조를 데이터베이스 테이블에 매핑하는 방법을 말한다. JPA는 엔티티의 상속 구조를 처리하기 위해 3가지의 테이블 전략을 제공하며 각각의 전략은 데이터 저장 방식과 성능에 차이가 있으므로 프로젝트의 요구사항에 맞게 선택할 수 있다.

  • 관계형 데이터베이스의 테이블에는 상속 관계가 없다.

 

dtype

@DiscriminatorColumn의 dtype은 **JPA (Java Persistence API)**에서 **싱글 테이블 상속 전략(Single Table Inheritance)**을 사용할 때, 엔티티의 타입을 구분하는 데 사용되는 컬럼을 의미합니다. dtype 컬럼은 테이블 내의 데이터가 어떤 엔티티 타입에 해당하는지 식별하기 위해 자동으로 생성됩니다.

 

+ 엔티티 : 데이터베이스 테이블과 매핑되는 자바 클래스


@DiscriminatorColumn의 역할

  1. 싱글 테이블 상속 전략에서 엔티티 유형을 구분하기 위해 사용.
    • 싱글 테이블 상속 전략: 하나의 테이블에 상속 관계를 가진 모든 엔티티 데이터를 저장.
    • 각 엔티티 타입을 식별할 수 있도록 테이블에 구분 컬럼(Discriminator Column)이 필요.
  2. 기본적으로 DiscriminatorColumn의 이름은 **DTYPE**이며, 이는 JPA의 관례입니다.
  3. @DiscriminatorColumn을 사용하면 이 컬럼의 이름이나 기타 설정을 변경할 수 있습니다.

기본 dtype 컬럼의 생성

예를 들어, 다음과 같은 상속 구조가 있다고 가정합니다.

엔티티 상속 구조

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "dtype")
public abstract class Animal {
    @Id
    @GeneratedValue
    private Long id;
    private String name;
}

@Entity
@DiscriminatorValue("DOG")
public class Dog extends Animal {
    private String breed;
}

@Entity
@DiscriminatorValue("CAT")
public class Cat extends Animal {
    private int livesLeft;
}

결과 테이블

이 코드로 인해 데이터베이스에 다음과 같은 테이블이 생성됩니다:

ID NAME dtype BREED LIVESLEFT

1 Max DOG Husky NULL
2 Whiskers CAT NULL 9
  • dtype: 데이터를 어떤 엔티티(Dog, Cat)로 매핑할지 식별하기 위한 컬럼.
  • DiscriminatorValue로 지정한 값이 dtype 컬럼에 저장됩니다.

@DiscriminatorColumn의 주요 속성

  • name:
    • dtype 컬럼의 이름을 설정합니다.
    • 기본값: "DTYPE".
  • length:
    • dtype 컬럼의 길이를 설정합니다.
    • 기본값: 31.
  • discriminatorType:
    • 컬럼의 데이터 타입을 설정합니다.
    • 기본값: STRING.
    • 다른 옵션: CHAR, INTEGER.

예: 이름과 타입 변경

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "animal_type", discriminatorType = DiscriminatorType.STRING, length = 50)
public abstract class Animal {
    @Id
    @GeneratedValue
    private Long id;
    private String name;
}

결과:

ID NAME animal_type BREED LIVESLEFT

1 Max DOG Husky NULL
2 Whiskers CAT NULL 9

Discriminator 컬럼이 필요한 이유

  • JPA는 테이블의 데이터를 읽어 올 때, dtype 컬럼의 값을 기준으로 어떤 서브클래스(엔티티)로 변환할지 결정합니다.
  • @DiscriminatorValue에 지정된 값을 사용하여 데이터를 매핑합니다.

기본값: DTYPE

  • JPA는 관례적으로 **DTYPE**이라는 이름을 기본값으로 사용합니다.
  • @DiscriminatorColumn을 명시하지 않으면 DTYPE 컬럼이 자동으로 생성됩니다.

정리

  • dtype 컬럼:
    • 싱글 테이블 상속 전략에서 엔티티 타입을 식별하기 위한 컬럼.
    • JPA의 기본 관례로 이름은 DTYPE.
  • @DiscriminatorColumn:
    • dtype 컬럼의 이름, 데이터 타입, 길이 등을 변경할 수 있는 어노테이션.
  • 사용 이유:
    • 테이블 내 데이터와 엔티티 타입 간의 매핑을 정확히 하기 위해 필요.

 

JPA의 테이블 전략

📌 JPA는 모든 전략으로 테이블을 구현할 수 있도록 지원한다.

 

Annotation

  • @Inheritance(strategy = InheritanceType.${전략})
    1. JOINED : 조인
    2. SINGLE_TABLE : 단일 테이블(Default)
    3. TABLE_PER_CLASS : 구현 클래스
  • @DiscriminatorColumn(name = "dtype")
    • dtype 컬럼을 생성한다(관례).
    • 이름 변경이 가능하다.
    • 기본 값 : DTYPE
  • @DiscriminatorValue("${값}")
    • dtype 값 지정
    • 기본 값 : 클래스 이름
@Entity
@Table(name = "product")
@DiscriminatorColumn(name = "dtype")
public abstract class Product {

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

    private String name;

    private BigDecimal price;

    public Product() {
    }

    public Product(String name, BigDecimal price) {
        this.name = name;
        this.price = price;
    }

}
@Entity
@Table(name = "book")
@DiscriminatorValue(value = "B")
public class Book extends Product {

    private String author;

    public Book() {
    }

    public Book(String author, String name, BigDecimal price) {
        super(name, price);
        this.author = author;
    }
}
@Entity
@Table(name = "coat")
@DiscriminatorValue(value = "C")
public class Coat extends Product {

    private Integer size;

}
public class Main {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("entity");

        EntityManager em = emf.createEntityManager();

        EntityTransaction transaction = em.getTransaction();

        transaction.begin();

        try {

            Book book = new Book("wonuk", "spring-advanced", BigDecimal.TEN);
            em.persist(book);

            transaction.commit();
        } catch (Exception e) {
            transaction.rollback();
        } finally {
            em.close();
        }

        emf.close();
    }
}

 

 

JOINED

@Entity
@Table(name = "product")
@DiscriminatorColumn(name = "dtype")
@Inheritance(strategy = InheritanceType.JOINED)
public abstract class Product {

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

    private String name;

    private BigDecimal price;

    public Product() {
    }

    public Product(String name, BigDecimal price) {
        this.name = name;
        this.price = price;
    }

    public Long getId() {
        return id;
    }
}
public class Main {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("entity");

        EntityManager em = emf.createEntityManager();

        EntityTransaction transaction = em.getTransaction();

        transaction.begin();

        try {

            Book book = new Book("wonuk", "spring-advanced", BigDecimal.TEN);
            em.persist(book);

            em.flush();
            em.clear();

            Book findBook = em.find(Book.class, book.getId());


            transaction.commit();
        } catch (Exception e) {
            transaction.rollback();
        } finally {
            em.close();
        }

        emf.close();
    }
}

 

 

TABLE_PER_CLASS

@Entity
@Table(name = "product")
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class Product {

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

    private String name;

    private BigDecimal price;

    public Product() {
    }

    public Product(String name, BigDecimal price) {
        this.name = name;
        this.price = price;
    }

    public Long getId() {
        return id;
    }
}

 

 

TABLE_PER_CLASS 문제점

public class Main {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("entity");

        EntityManager em = emf.createEntityManager();

        EntityTransaction transaction = em.getTransaction();

        transaction.begin();

        try {

            Book book = new Book("wonuk", "spring-advanced", BigDecimal.TEN);
            em.persist(book);

            em.flush();
            em.clear();

            Product findProduct = em.find(Product.class, book.getId());

            transaction.commit();
        } catch (Exception e) {
            transaction.rollback();
        } finally {
            em.close();
        }

        emf.close();
    }

 

테이블 전략 장단점

  • JOINED
    • 장점
      1. 테이블 정규화
      2. 외래 키 참조 무결성
      3. 저장공간 효율
    • 단점
      1. 조회시 JOIN을 많이 사용한다.
      2. 데이터 저장시 INSERT SQL 이 2번 호출된다.
      3. SQL Query가 복잡하여 성능이 저하될 수 있다.
  • SINGLE_TABLE
    • 장점
      1. JOIN을 사용하지 않는다.
      2. 실행되는 SQL이 단순하다.
    • 단점
      1. 자식 Entity가 매핑한 컬럼은 모두 null을 허용한다.
      2. 단일 테이블에 모든 것을 저장하므로 테이블이 커질 수 있다.
      3. 상황에 따라서 조회 성능이 오히려 느려질 수 있다.
  • TABLE_PER_CLASS
    • 테이블끼리 연관짓기 힘들다, 사용하지 않는것을 권장한다.
    • 장점
      1. 자식 클래스를 명확하게 구분해서 처리할 수 있다.
      2. not null 제약조건 사용이 가능하다.
    • 단점
      1. 여러 자식 테이블을 함께 조회할 때 성능이 느리다.
      2. 부모 객체 타입으로 조회할 때 모든 테이블을 조회해야 한다.
  • 선택 기준
    • 비지니스적으로 복잡하다 = JOINED
      • 객체 지향적인 개발에 어울리는 방법
    • 단순하고 확장 가능성이 없다 = SINGLE_TABLE
    • 두 방법의 장단점을 구분하여 상황에 맞는 선택을 해야한다.

 

'DB > JPA ( Java Persistence API )' 카테고리의 다른 글

[JPA] 지연로딩, 즉시로딩  (0) 2025.01.15
[JPA] Proxy  (0) 2025.01.14
[JPA] 연관관계  (0) 2025.01.12
[JPA] Spring Data JPA  (0) 2025.01.07
[JPA] 연관관계 Mapping  (0) 2025.01.06

+ Recent posts