트랜잭션 전파

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

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

  • 트랜잭션이 여러 계층 또는 메서드에서 어떻게 처리될지 정의한다.(@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

연관관계 매핑

📌 JPA 연관관계 매핑을 통해 데이터베이스 테이블 간의 관계를 객체 지향적으로 표현하여 엔티티 클래스들 간의 관계를 설정한다. JPA를 통해 연관관계를 매핑하면 SQL을 직접 작성하지 않고도 객체 간의 관계를 활용하여 쉽게 데이터를 조회하고 조작할 수 있다.

 

 

1 : N 단방향

📌 한 엔티티가 @OneToMany를 통해 여러 엔티티와 관계를 맺는 경우를 말한다. 이 경우 연관관계의 주인은 1에서 가지고 있다.

  • 1의 Entity가 외래 키(FK)를 관리한다. (연관관계의 주인)
  • DB 입장에서는 항상 외래 키가 N 쪽에 위치해야 한다. (설계상 불가)
    • 1(Company)이 N(Tutor)의 외래 키를 수정해야 한다.
더보기
@Entity
@Table(name = "tutor")
public class Tutor {

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

    private String name;

    public Tutor() {
    }

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

}
@Entity
@Table(name = "company")
public class Company {

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

    private String name;

    @OneToMany
    @JoinColumn(name = "company_id")
    private List<Tutor> tutors = new ArrayList<>();

    public Company() {
    }

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

    public List<Tutor> getTutors() {
        return tutors;
    }
}
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 {
						// Tutor 생성 및 persist
            Tutor tutor = new Tutor("wonuk" );
            em.persist(tutor);
						
						// Company 생성 및 persist
            Company company = new Company("sparta");
            // Tutor 테이블에 추가
            company.getTutors().add(tutor);
            em.persist(company);

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

        emf.close();
    }
}

 

@JoinColumn 미사용

@Entity
@Table(name = "company")
public class Company {

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

    private String name;

    @OneToMany
//    @JoinColumn(name = "company_id") 주석처리
    private List<Tutor> tutors = new ArrayList<>();

    public Company() {
    }

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

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public List<Tutor> getTutors() {
        return tutors;
    }
}
  • 실행결과
    • 기존 테이블 삭제 후 실행(충돌 방지)

  • @JoinColumn 을 사용하지 않으면 중간 테이블 방식을 사용한다.
    • 사용 필수

 

 

1 : N 양방향

📌 양방향 연관 관계는 하나의 엔티티가 다른 엔티티와 관계를 맺고 그 반대 방향에서도 서로 참조가 가능하도록 설정한 관계이다.

 

  • Tutor의 참조용 객체 Company는 읽기 전용 매핑이어야 한다.
  • 만약, 양쪽에서 수정이 가능하면 예측이 불가능해진다.
  • 1:N 양방향 연관관계의 주인은 Company 이다.
@Entity
@Table(name = "tutor")
public class Tutor {

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

    private String name;

    @ManyToOne
    @JoinColumn(name = "company_id", insertable = false, updatable = false)
    private Company company;

    public Tutor() {
    }

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

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }
}
  • 연관관계의 주인이 되지 않도록 insertable = false, updatable = false 설정
  • 1:N 단방향과 같은 이유로 N:1 양방향을 쓰면된다.

 

1 : 1 단방향

📌 두 Entity가 @OneToOne 을 통해 서로 관계를 맺는 경우를 말한다.

  • 외래 키(FK)의 주인을 선택할 수 있다.(Tutor로 가정)
  • 외래 키에 유니크 제약조건이 필요하다.(1:1)
@Entity
@Table(name = "address")
public class Address {

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

    private String name;

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

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

    private String name;

    @ManyToOne
    @JoinColumn(insertable = false, updatable = false)
    private Company company;

    @OneToOne
    @JoinColumn(name = "address_id", unique = true)
    private Address address;

    public Tutor() {
    }

    public Tutor(String name) {
        this.name = name;
    }
    
    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }
}

 

  • 실행결과
    • 기존 테이블 삭제 후 실행(충돌 방지)

 

대상 테이블에 외래 키

  • Tutor Entity의 Address로 Address 테이블의 tutor_id는 관리하지 못한다.
  • 애초에 JPA가 지원하지 않는다.

 

1 : 1 양방향

  • N:1 양방향 연관관계와 유사하다.
  • 외래 키(FK)가 있는 곳이 연관관계의 주인
@Entity
@Table(name = "address")
public class Address {

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

    private String name;

    // 읽기 전용
    @OneToOne(mappedBy = "address")
    private Tutor tutor;

}
  • 실행결과는 N:1의 양방향과 같이 읽기 전용으로 동작한다.

대상 테이블에 외래 키 양방향

  • 1:1연관관계에서 자신이 가지고 있는 외래 키는 자신이 관리해야 한다.
  • Tutor Entity의 Address 는 참조(읽기 전용)객체 이다.

 

1 : 1 연관관계 외래 키

📌 1:1 연관관계에서 외래 키는 양쪽 모두가 관리할 수 있다.

 

  • 1:1 연관관계 외래 키
    • 둘중 어떤 테이블을 사용해도 무방하다.
    • 단, 테이블은 한번 만들어지면 변경이 어렵다.
  • 요구사항 변경
    • 한명의 Tutor가 여러개의 Address를 가질 수 있다.
    • 기존 테이블

수정 테이블

  • UNIQUE 제약조건만 지우면 된다.
  • N:1 연관관계로 자연스럽게 변경이 가능하다.
  • 단, 양방향으로 만들어야한다.

 

개발자 관점

  • Tutor 테이블에서 접근할 확률이 높으니 Tutor 테이블이 외래 키(FK)를 가지면 편하다.
  • 성능상 이점이 생긴다.
  • 명확한 1:1 연관관계라면 해당 방법을 선택하면 된다.
  • 결국 여러가지 방법 중 장점과 단점을 비교하여 선택하면 된다.

 

외래 키 위치 장단점

  1. 주 테이블
    • 장점
      • JPA로 객체 지향적인 개발이 가능해진다.
      • 주 테이블만 조회해도 대상 테이블을 조회할 수 있다.
    • 단점
      • 대상 테이블에 값이 없다면 null이 허용된다.
        • 데이터 무결성을 지키지 못한다.
      • 대상 테이블의 데이터를 참조하기 때문에 삭제될 때 외래 키 값을 처리하는 관리 필요
  2. 대상 테이블
    • 장점
      • 데이터베이스 무결성 보장
      • 주 테이블과 대상 테이블의 연관관계 변경 시 테이블 구조가 유지된다.
    • 단점
      • 조회 성능이 떨어진다.
      • 연관관계 매핑 설정이 복잡하다.
      • 지연 로딩으로 설정해도 즉시 로딩된다.(중요)

 

N : M 연관관계

📌 두 Entity가 @ManyToMany를 통해 서로 다수의 관계를 가진다.

  • 조심해서 사용해야 한다.

 

N : M 단방향

@Entity
@Table(name = "language")
public class Language {

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

    private String name;

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

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

    private String name;

    @ManyToOne
    @JoinColumn(insertable = false, updatable = false)
    private Company company;

    @OneToOne
    @JoinColumn(name = "address_id", unique = true)
    private Address address;

    @ManyToMany
    @JoinTable(
            name = "tutor_language",
            joinColumns = @JoinColumn(name = "tutor_id"),
            inverseJoinColumns = @JoinColumn(name = "language_id")
    )
    private List<Language> languages = new ArrayList<>();

    public Tutor() {
    }

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

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }
}
  • 실행결과
    • 기존 테이블 삭제 후 실행(충돌 방지)

 

 

N : M 양방향

@Entity
@Table(name = "language")
public class Language {

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

    private String name;

    @ManyToMany(mappedBy = "languages")
    private List<Tutor> tutors = new ArrayList<>();

}

@OneToOne 양방향 처럼 동작하지만 중간 테이블이 생성된다.

 

 

N : M 매핑의 문제점

📌 @ManyToManyN:M 연관관계 설정을 하게되면 편리해 보이지만 실제로 사용하기 까다롭다.

 

  1. 실제 설계에서는 level, license 와 같은 추가적인 데이터가 필요하다.
    • @ManyToMany에서 사용할 수 없다.
  2. 중간 테이블이 숨겨져 있어서 생각하지 못한 SQL Query가 실행된다.
  3. tutor_id, language_id 를 묶어서 PK로 설정된다.
    • PK가 종속적이면 사이드 이펙트가 생긴다.
    • PK 값은 비지니스적으로 의미없는 Long값으로 설정하는것이 유연성에 좋다.

 

문제점 해결

  • 중간 테이블을 실제 Entity로 만들어서 관리하면 된다.
  • @ManyToMany
    • @OneToMany
    • @ManyToOne
@Entity
@Table(name = "tutor_language")
public class TutorLanguage {

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

    @ManyToOne
    @JoinColumn(name = "tutor_id")
    private Tutor tutor;

    @ManyToOne
    @JoinColumn(name = "language_id")
    private Language language;

    private Integer level;

    private String license;

}
@Entity
@Table(name = "language")
public class Language {

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

    private String name;

    @OneToMany(mappedBy = "language")
    private List<TutorLanguage> tutorLanguages = new ArrayList<>();

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

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

    private String name;

    @ManyToOne
    @JoinColumn(insertable = false, updatable = false)
    private Company company;

    @OneToOne
    @JoinColumn(name = "address_id", unique = true)
    private Address address;

    @OneToMany(mappedBy = "tutor")
    private List<TutorLanguage> tutorLanguages = new ArrayList<>();

    public Tutor() {
    }

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

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }
}
  • 실행결과
    • 기존 테이블 삭제 후 실행(충돌 방지)
    • <class>org.example.entity.TutorLanguage</class> **주석제거

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

[JPA] Proxy  (0) 2025.01.14
[JPA] 상속관계 매핑  (0) 2025.01.13
[JPA] Spring Data JPA  (0) 2025.01.07
[JPA] 연관관계 Mapping  (0) 2025.01.06
[JPA] Entity  (1) 2025.01.05

Spring Boot와 JPA

📌 Spring Boot는 JPA 설정을 자동으로 구성해 주어 JPA를 쉽게 사용할 수 있도록 도와준다. 기본적으로 필요한 EntityManagerFactory와 TransactionManager를 자동으로 설정하고 데이터베이스 관련 설정을 application.properties 파일에서 간단히 지정할 수 있게 해준다.

 

public static void main(String[] args) {
    // EntityManagerFactory 생성
    EntityManagerFactory emf = Persistence.createEntityManagerFactory("test");
    // EntityManager 생성
    EntityManager em = emf.createEntityManager();
    // Transaction 생성
    EntityTransaction transaction = em.getTransaction();

    // 트랜잭션 시작
    transaction.begin();

    try {
        // 비영속
        Tutor tutor = new Tutor(1L, "wonuk", 100);

        System.out.println("persist 전");
        // 영속
        em.persist(tutor);
        System.out.println("persist 후");

        // transaction이 commit되며 실제 SQL이 실행된다.
        transaction.commit();
    } catch (Exception e) {
        // 실패 -> 롤백
        e.printStackTrace();
        transaction.rollback();
    } finally {
        // 엔티티 매니저 연결 종료
        em.close();
    }
    
    emf.close();

}
  • 기존의 방식이다.
  • 직접 트랜잭션을 사용한다.(JPA는 기본적으로 하나의 트랜잭션 안에서 기능을 수행한다)
  • 직접 EntityManagerFactory 와 EntityManager 를 생성하여 사용한다.
  • 직접 close() 하여 연결을 종료 해야한다.

 

Spring Boot와 JPA

  • JPA는 Spring에 종속적인 것이 아니다.
  • Spring Boot 에서 JPA를 사용하기 위해서는 build.gradle에 의존성 추가가 필요하다.
    • spring-boot-starter-data-jpa
    • 필요한 JPA 설정과 Entity 관리를 자동으로 해준다.
  • 자동으로 내부에서 EntityManagerFactory 를 하나만 생성해서 관리(싱글톤)한다.
    • 자동으로 Bean으로 등록된다.
    • 직접 만들지 않아도 된다.
    • 직접 연결을 close() 하지 않아도 된다.
    • application.properties 에 설정된 DB 정보로 생성된다.
  • @PersistenceContext를 통해 자동으로 생성된 EntityManager를 주입받아 사용할 수 있다.
@Repository
public class TutorRepository {
    
    @PersistenceContext
    private EntityManager em;

    public void save(Tutor tutor) {
        em.persist(tutor);
    }

    public Tutor findById(Long id) {
        return em.find(Tutor.class, id);
    }

    public List<Tutor> findAll() {
        return em.createQuery("SELECT * FROM tutor", Tutor.class).getResultList();
    }

    public void delete(Tutor tutor) {
        em.remove(tutor);
    }
}
  • EntityManager 는 Spring Data JPA에서 관리하여 직접 관리하지 않아도된다.

 

 

Spring Data JPA

📌 Spring Data JPA는 Spring Framework에서 제공하는 모듈로 JPA를 쉽게 사용할 수 있도록 지원한다. 이를 통해 데이터베이스와 상호작용을 간편하게 구현할 수 있고 코드를 간소화할 수 있다.

 

Spring Data JPA 특징

  1. JPA 추상화 Repository 제공
    • CrudRepository, JpaRepository 인터페이스를 제공한다.
    • SQL이나 EntityManager를 직접 호출하지 않아도 기본적인 CRUD 기능을 손쉽게 구현할 수 있다.
  2. JPA 구현체와 통합
    • 일반적으로 Hibernate를 통해 자동으로 SQL이 생성된다.
  3. QueryMethods
    • Method 이름만으로 SQL을 자동으로 생성한다.
    • @Query 를 사용하여 JPQL 또는 Native Query를 정의할 수 있다.
      • 복잡한 SQL을 직접 구현할 때 사용
  4. 트랜잭션 관리와 LazyLoading
    • 트랜잭션 기능을 Spring과 통합하여 제공한다.
    • 연관된 Entity를 필요할 때 로딩하는 지연로딩 기능을 지원한다.

 

 

SimpleJpaRepository

📌 Spring Data JPA의 기본 Repository 구현체로 JpaRepository 인터페이스의 기본 메서드들을 실제로 수행하는 클래스이다. 내부적으로 EntityManager를 사용하여 JPA Entity를 DB에 CRUD 방식으로 저장하고 관리하는 기능을 제공한다.

  • Spring Data JPA는 JpaRepository 인터페이스를 구현한 클래스를 자동으로 생성한다.
    • 기본적으로 SimpleJpaRepository를 구현체로 사용한다.
public interface MemberRepository extends JpaRepository<Member, Long> {
}
  1. Repository를 interface로 선언한다.
  2. JpaRepository<"@Entity 클래스", "@Id 데이터 타입"> 상속
  • 내부동작
    • Spring이 실행되면서 JpaRepository 인터페이스를 상속받은 인터페이스가 있다면, 해당 인터페이스의 정보를 토대로 SimpleJpaRepository 를 생성하고 Bean으로 등록한다.
    • 인터페이스의 구현 클래스를 직접 만들지 않아도 JpaRepository 의 기능을 사용할 수 있다.
    • 개발자가 직접 SimpleJpaRepository를 사용하거나 참조할 필요는 없다.

  • save() : 대상 Entity를 DB 테이블에 저장한다.
  • findAll() : Entity에 해당하는 테이블의 모든 데이터를 조회한다.
  • delete() : 대상 Entity를 데이터베이스에서 삭제한다.
  • 이외에도 수많은 기능(Paging, Sorting 등)이 있다.

 

 

Query Methods

📌 Spring Data JPA에서 메서드 이름을 기반으로 데이터베이스 쿼리를 자동 생성하는 기능이다. 직접 SQL을 작성하지 않고도 복잡한 쿼리를 쉽게 수행할 수 있게된다.

  • Spring Data JPA에서 메서드 이름을 기반으로 SQL을 자동으로 생성하는 기능
  • JpaRepository는 findAll(), save()와 같은 기본적인 기능만 제공한다.
  • 실제 Application 개발에는 상황에 따라 조건에 맞는 메서드가 필요하다.
public interface MemberRepository extends JpaRepository<Member, Long> {
		// Query Methods
    Member findByNameAndAddress(String name, String address);
}
// 자동으로 생성되어 실제로 실행되는 SQL
SELECT * FROM member WHERE name = ? AND address = ?;
  • JpaRepository의 제네릭에 선언된 Entity와 매핑되는 테이블의 SQL이 생성된다.
  • 개발자가 규칙에 맞게 메서드를 선언하면 SimpleJpaRepository에서 구현된다.
  • 해석
    1. find : Entity에 매핑된 테이블(member)을 조회한다.
    2. ByName : 조건은 member 테이블의 name 필드이다.
    3. AndAddress : 또다른 조건은 member 테이블의 address 필드이다.
 

JPA Query Methods :: Spring Data JPA

By default, Spring Data JPA uses position-based parameter binding, as described in all the preceding examples. This makes query methods a little error-prone when refactoring regarding the parameter position. To solve this issue, you can use @Param annotati

docs.spring.io

정답은 언제나 공식문서에 있다.

 

 

 

JPA Auditing

📌 엔티티의 생성 및 수정 시간을 자동으로 관리해주는 기능입니다. 이를 통해 개발자는 엔티티가 언제 생성되고 수정되었는지를 자동으로 추적할 수 있다.

 

모든 클래스에 생성 시간, 수정 시간 추가

@Entity
public class User {
		@Id
		private Long id;
		private String name;
		private String address;
		// 생성 시간
		private LocalDateTime createdAt;
		// 수정 시간
		private LocalDateTime updatedAt;
}

@Entity
public class Item {
		@Id
		private Long id;
		private String name;
		private String description;
		// 생성 시간
		private LocalDateTime createdAt;
		// 수정 시간
		private LocalDateTime updatedAt;
}
  • 모든 Entity가 생성 시간, 수정 시간에 대한 연산을 수행해야 한다.
  • JPA Auditing은 이러한 불편함을 해결해준다.
    • 개발자는 반복되는 불편함을 참지 않는다.

 

JPA Auditing 적용예시

@MappedSuperclass
public class BaseEntity{
		@Column(updatable = false)
		private LocalDateTime createdAt;
		private LocalDateTime updatedAt;

		@PrePersist
		public void prePersist(){
			LocalDateTime now = LocalDateTime.now();
			created_at = now;
			updated_at = now;
		}
	
		@PreUpdate
	  public void preUpdate() {
	    updated_at = LocalDateTime.now();
	  }
}

 

순수 JPA도 Auditing을 사용할 수 있지만 Spring Data JPA에서는 더 쉽게 사용할 수 있다.

 

 

Spring Data JPA Auditing Annotation

 

 

  • 적용하기

@EnableJpaAuditing

@EnableJpaAuditing
@SpringBootApplication
public class SpringDataJpaApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringDataJpaApplication.class, args);
    }

}
  • Spring Data JPA에서 Auditing을 적용할 때 사용하는 어노테이션
  • 일반적으로 Spring Boot를 실행하는 Application 클래스 상단에 선언한다.

  • Spring Boot로 만들어진 프로젝트에 기본적으로 생성되는 Application 클래스

BaseEntity 생성

@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseEntity {

    @CreatedDate
    @Column(updatable = false)
    @Temporal(TemporalType.TIMESTAMP)
    private LocalDateTime createdAt;

    @LastModifiedDate
    @Temporal(TemporalType.TIMESTAMP)
    private LocalDateTime modifiedAt;
}
  • updatable = false 설정으로 생성 시간이 수정되지 못하게 설정한다.

BaseEntity 상속

@Entity
public class User extends BaseEntity{
    @Id
    private Long id;
    private String name;
}
  • createdAt, modifedAt 필드를 가지게 된다.
  • 상속받는 것만으로 생성, 수정 시간을 자동으로 생성할 수 있다.

 

 

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

[JPA] 상속관계 매핑  (0) 2025.01.13
[JPA] 연관관계  (0) 2025.01.12
[JPA] 연관관계 Mapping  (0) 2025.01.06
[JPA] Entity  (1) 2025.01.05
[JPA] 영속성 컨텍스트  (2) 2025.01.04

단방향

📌 단방향 연관관계는 객체 간의 관계가 한쪽에서만 참조 = 일방통행될 수 있는 관계를 말한다. 설정이 단순하고 유지 관리가 쉬우며 불필요한 데이터 접근을 방지할 수 있다.

 

 

 

데이터베이스 중심 객체 설계

  • FK 값은 Tutor가 가지고 있다.
    • Tutor만 참조할 수 있다.
    • N:1, 다대일 연관관계, 가장 많이 사용된다.
    • 여러명(N)의 Tutor가 어떤 Company(1)에 소속 되어있는지 설정할 수 있다.
@Entity
@Table(name = "tutor")
public class Tutor {
		@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
		private Long id;
		
		private String name;
		
		@Column(name = company_id)
		private Long companyId;
		
		// 기본 생성자, getter/setter
}
@Entity
@Table(name = "company")
public class Company {
		@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
		private Long id;
		
		private String name;
		
		// 기본 생성자, getter/setter
}
// Company 생성 및 persist
Company company = new Company("sparta");
em.persist(company);

// Tutor 생성, setCompanyId, persist
Tutor tutor = new Tutor("wonuk");
tutor.setCompanyId(company.getId());
em.persist(tutor);

// IDENTITY 전략을 사용하면 persist()이후 PK를 바로 조회할 수 있다.
Tutor findTutor = em.find(Tutor.class, tutor.getId());

// 조회한 Tutor의 CompanyId로 Company 조회
Long companyId = findTutor.getCompanyId();
Company findCompany = em.find(Company.class, companyId);
  • 객체 지향적인 코드를 작성할 수 없다.
  • Java Collection을 사용하는 것처럼 tutor.getCompany() 를 사용하지 못한다.

 

객체 지향 객체 설계

  • 객체는 다른 객체를 참조한다.
  • N:1 관계는 @ManyToOne, @JoinColumn을 사용한다.
  • 코드예시
@Entity
@Table(name = "tutor")
public class Tutor {
		@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
		private Long id;
		
		private String name;
		// N:1 단방향 연관관계 설정
		@ManyToOne
		@JoinColumn(name = "company_id")
		private Company company;
		
		// 기본 생성자, getter/setter
}
// Company 생성 및 persist
Company company = new Company("sparta");
em.persist(company);

// Tutor 생성, setCompany, persist
Tutor tutor = new Tutor("wonuk");
tutor.setCompany(company);
em.persist(tutor);

// IDENTITY 전략을 사용하면 persist()이후 PK를 바로 조회할 수 있다.
Tutor findTutor = em.find(Tutor.class, tutor.getId());

// 조회한 Tutor의 Company 조회
Company findCompany = findTutor.getCompany();
  • Tutor의 FK와 Company의 PK를 @JoinColumn으로 매핑한다.
  • Java Collection을 사용하는 것처럼 tutor.getCompany() 를 사용할 수 있다.

 

 

양방향

📌 양방향 연관관계는 객체 간의 관계가 양쪽에서 서로를 참조할 수 있는 관계를 의미한다. 이를 통해 양쪽에서 데이터를 쉽게 접근할 수 있지만 관계를 관리할 때 한쪽에서만 연관관계를 설정하거나 삭제하지 않도록 주의가 필요하다.

테이

  • 테이블에 변화는 없다.
    • Tutor 테이블의 FK로 Company 테이블에 JOIN
    • Company 테이블의 PK로 Tutor 테이블에 JOIN
  • 사실상 테이블의 연관관계에는 방향의 개념이 없다.

객체

@Entity
@Table(name = "tutor")
public class Tutor {
		@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
		private Long id;
		
		private String name;
		// N:1 단방향 연관관계 설정
		@ManyToOne
		@JoinColumn(name = "company_id")
		private Company company;
		
		// 기본 생성자, getter/setter
}
@Entity
@Table(name = "company")
public class Company {
		@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
		private Long id;
		
		private String name;
		
		// null을 방지하기 위해 ArrayList로 초기화 한다.(관례)
		@OneToMany(mappedBy = "company")
		private List<Tutor> tutors = new ArrayList<>();
		
		// 기본 생성자, getter/setter
}
  • 양방향 연관관계 설정을 위해 mappedBy 속성을 설정한다.
    • Tutor의 company 필드와 매핑된다.
// Company 생성 및 persist
Company company = new Company("sparta");
em.persist(company);

// Tutor 생성, setCompany, persist
Tutor tutor = new Tutor("wonuk");
tutor.setCompany(company);
em.persist(tutor);

// IDENTITY 전략을 사용하면 persist()이후 PK를 바로 조회할 수 있다.
Tutor findTutor = em.find(Tutor.class, tutor.getId());

// 조회한 Tutor의 Company 조회
Company findCompany = findTutor.getCompany();

// Company에 속한 Tutor List 조회
List<Tutor> tutors = findCompany.getTutors();
  • 반대 방향으로 객체 그래프를 탐색할 수 있다.

 

 

양방향 연관관계의 주인

📌 mappedBy는 JPA 양방향 연관관계 설정 시 사용되는 속성으로 두 엔티티 간의 관계에서 연관관계의 주인이 아닌 쪽에 선언한다. 이를 통해 외래 키 관리 책임을 주인 엔티티에 두고 매핑이 중복되지 않도록 한다.

 

테이블 연관관계

  • Tutor ↔ Company (양방향)
  • 테이블은 방향이 없다

객체 연관관계

  • Tutor → Company N:1 연관관계 (단방향)
  • Company → Tutor 1:N 연관관계 (단방향)
  • 객체 연관관계는 결국 단방향 연관관계 두개로 양방향이 설정된다.

양방향 연관관계의 주인

  1. Tutor의 Company를 수정할 때 FK가 수정
  2. Company의 Tutor를 수정할 때 FK가 수정
  • Tutor가 새로운 Company를 간다면?
    • Tutor의 참조값 Company 수정
    • Company의 참조값 List tutors 수정
    • DB 입장에서는 FK만 수정되면 된다. = mappedBy가 없는 쪽이 수정되면 된다.
  • 결국, 둘 중 하나로만 외래 키를 관리해야 한다.
  • 연관관계의 주인 선정 기준
    • 항상 FK가 있는 곳을 연관관계의 주인으로 지정한다.
    • Company가 주인인 경우
      • Company를 수정할 때 Tutor를 Update하는 SQL이 실행
      • 두번의 SQL이 실행되어야 한다. 혼동되기 쉽다.

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

[JPA] 연관관계  (0) 2025.01.12
[JPA] Spring Data JPA  (0) 2025.01.07
[JPA] Entity  (1) 2025.01.05
[JPA] 영속성 컨텍스트  (2) 2025.01.04
[JPA] 페러다임 불일치 문제 [객체 관계형 데이터베이스]  (1) 2025.01.02

Entity

📌 클래스에 @Entity가 있다면 JPA가 관리하는 Entity로 만들어진다.

  • db 테이블 매핑되는 자바 객체

 

persistence.xml

📌 JPA 관련 설정

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.2"
             xmlns="http://xmlns.jcp.org/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_2.xsd">
    <persistence-unit name="test"> <!-- createEntityManagerFactory에서 사용할 이름 -->
        <class>org.entity.Tutor</class>
        <properties>
            <property name="jakarta.persistence.jdbc.driver" value="com.mysql.cj.jdbc.Driver"/>
            <!-- DB 아이디 -->
            <property name="jakarta.persistence.jdbc.user" value="root"/>
            <!-- DB 비밀번호 -->
            <property name="jakarta.persistence.jdbc.password" value="1111"/>
            <!-- 스키마 -->
            <property name="jakarta.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/sparta"/> 
						<!-- 스키마 자동생성 속성 -->
            <property name="hibernate.hbm2ddl.auto" value="create" />
						
						<!-- Hibernate가 DB에 전송하는 DDL, DML SQL을 콘솔에 출력한다. -->
            <property name="hibernate.show_sql" value="true"/>
            <property name="hibernate.format_sql" value="true"/>
            <property name="hibernate.use_sql_comments" value="true"/>
            
            <!-- 한꺼번에 전송될 SQL 수량 설정 -->
<!--            <property name="hibernate.jdbc.batch_size" value="10"/>-->
        </properties>
    </persistence-unit>
	</persistence>

 

 

@Entity

@Entity(name = "Tutor") // 기본 값, name 속성은 생략하면 된다.
@Table(name = "tutor")
public class Tutor {

    // PK
    @Id
    private Long id;

    // 필드
    private String name;

    // 기본 생성자
    public Tutor() {
    }

    // 쉽게 사용하기 위해 생성자 추가
    public Tutor(Long id, String name) {
        this.id = id;
        this.name = name;
    }
}
  • JPA를 사용하여 객체를 테이블과 매핑할 때 사용한다.(필수)
  • PK 값이 필수이다.(@Id 사용)
  • 기본 생성자가 필수이다.
  • final, enum, interface, inner 클래스에는 사용할 수 없다.
  • 필드에 final 키워드를 사용할 수 없다.
  • 속성
    • name
      • Entity 이름 지정
      • 기본 값은 클래스 이름과 같다.
      • 혼동을 방지하기 위해 기본 값을 사용(생략)하면 된다.

 

@Table

@Entity
@Table(name = "tutor")
public class Tutor {
}
  • 속성
    • name
      • Entity와 매핑할 테이블 이름을 지정
      • 기본 값은 Entity 이름(Tutor)을 사용
    • catalog
      • 데이터베이스 catalog 매핑
    • schema
      • 데이터베이스 schema 매핑
    • uniqueConstraints
      • DDL 생성 시 유니크 제약 조건 설정

 

JPA 실행

public static void main(String[] args) {
    EntityManagerFactory emf = Persistence.createEntityManagerFactory("test");
    EntityManager em = emf.createEntityManager();
    EntityTransaction transaction = em.getTransaction();

    transaction.begin();

    try {
            
        Tutor tutor = new Tutor(1L, "wonuk");
        em.persist(tutor);

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

 

 

hibearnate.hbm2ddl.auto

📌 SessionFactory가 생성될때 스키마 DDL을 db로 검증하고 내보내는 기능을 상황에 따라 다르게 설정할수 있도록 하는 프로퍼티

  • SessionFactory  = 도메인 모델을 데이터베이스로 맵핑하는 thread-safe하고 immutable한 표현이다.
  • JPA는 Application 로딩 시점에 DDL을 자동으로 생성하는 기능을 지원한다. 방언(dialect)을 사용하여 Entity Mapping만 하여도 데이터베이스에 맞는 적절한 DDL이 생성된다.
  • 실무에서는 validate 혹은 none 을 사용하고 개발 단계에서는 상황에 맞게 사용하면 된다.

 

public class Main {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("test");
        EntityManager em = emf.createEntityManager();
        EntityTransaction transaction = em.getTransaction();

        transaction.begin();

        try {

            Tutor tutor = new Tutor(1L, "wonuk");
            em.persist(tutor);

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

        emf.close();
    }
}

 

<property name="hibernate.hbm2ddl.auto" value="create" />
<property name="hibernate.hbm2ddl.auto" value="create-drop" />
<property name="hibernate.hbm2ddl.auto" value="update" />
<property name="hibernate.hbm2ddl.auto" value="validate" />

 

 

 

제약조건 설정

📌 테이블에 입력되는 데이터가 사용자가 원하는 조건을 만족하는 데이터만 입력되는 것을 보장하는 것

  • JPA를 사용하면 DDL 생성 시 제약조건을 설정할 수 있다. 실행 로직과는 별개로 DDL 생성시에만 활용된다.

 

DDL 자동생성 제약조건 설정

  1. @Column
    • unique : 유니크, 기본값 false
    • nullable : 필수 여부, 기본값 true
    • length : 길이
@Column(unique = true, length = 20, nullable = false)

DDL 자동 생성 시 제약 조건이 설정된다.

 

  1. @Table
    • uniqueConstraints : 유니크, 이름을 직접 설정할 수 있다.
@Table(uniqueConstraints = {@UniqueConstraints
				(
					name = "name_unique", 
					columnNames= {"name"}
				)
		}
)
// 클래스

 

 

필드 매핑

📌 JPA로 관리되는 클래스인 Entity의 필드는 테이블의 컬럼과 매핑된다.

@Entity
@Table(name = "board")
public class Board {
    @Id
    private Long id;

    // @Column을 사용하지 않아도 자동으로 매핑된다.
    private Integer view;

    // 객체 필드 이름과 DB 이름을 다르게 설정할 수 있다.
    @Column(name = "title")
    private String bigTitle;

    // DB에는 기본적으로 enum이 없다.
    @Enumerated(EnumType.STRING)
    private BoardType boardType;

    // VARCHAR()를 넘어서는 큰 용량의 문자열을 저장할 수 있다.
    @Column(columnDefinition = "longtext")
    private String contents;

    // 날짜 타입 DATE, TIME, TIMESTAMP를 사용할 수 있다.
    @Temporal(TemporalType.TIMESTAMP)
    private Date createdDate;

    @Temporal(TemporalType.TIMESTAMP)
    private Date lastModifiedDate;
    
    @Transient
    private int count;

    public Board() {
    }

}

 

 

사용되는 Annotation

 

 

@Column 속성

속성 설명 Default
name
객체 필드와 매핑할 테이블의 컬럼 이름
객체 필드 이름
nullable
DDL 생성 시 null 값의 허용 여부 설정
true(허용)
unique
DDL 생성 시 하나의 컬럼에 유니크 제약조건을 설정
 
columnDefinition
DDL 생성 시 데이터베이스 컬럼 정보를 직접 설정할 수 있다.
 
length
DDL 생성 시 문자 길이 제약조건 설정 단, String만 사용 가능
255
insertable
설정된 컬럼의 INSERT 가능 여부
true
updatable
설정된 컬럼의 UPDATE 가능 여부
true

 

 

@Enumerated

  • 기본 설정인 ORDINAL을 사용하면 0, 1 과 같은 순서가 저장된다.

 

EnumType.ORDINAL을 사용하면 Enum 값이 추가될 때 마다 순서가 바뀌기 때문에 실제로 사용하지 않는다.

 

 

@Temporal

 

 

기본 키

📌 JPA Entity를 생성할 때 기본키는 필수로 생성해야 한다.

 

  • 사용되는 Annotation
    1. @Id
      • 수동 생성
Tutor tutor = new Tutor(1L, "wonuk");
  1. @GeneratedValue
    • 자동 생성
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

 

PK 생성 전략

  • 영속성 컨텍스트는 PK가 필수이다.
    • 가장 권장되는 방식은 Long Type의 기본 키를 사용하는 것
  • strategy 속성
    • GenerationType
      1. IDENTITY : MySQL, PostgreSQL에서 사용, 데이터베이스가 PK 자동 생성
      2. SEQUENCE : Oracle에서 사용, @SequenceGenerator 와 함께 사용
      3. TABLE : 키 생성용 테이블을 사용, @TableGenerator 와 함께 사용
      4. AUTO : dialect에 따라 자동 지정, 기본값
        • MySQL이면 IDENTITY, Oracle이면 SEQUENCE 로 설정
  • 실행 결과

 

영속성 컨텍스트(PersistenceContext)

📌 Entity 객체를 영속성 상태로 관리하는 일종의 캐시 역할을 하는 공간으로 여기에 저장된 Entity는 데이터베이스와 자동으로 동기화되며 같은 트랜잭션 내에서는 동일한 객체가 유지된다.

  • 영속성 상태 = JPA(Java Persistence API)에서 **엔티티(Entity)**가 EntityManager에 의해 관리되는 상태

  • 논리적인 개념
    • 눈에 보이지 않는 공간이 생긴다.
  • Entity Manager 를 통해서 영속성 컨텍스트에 접근한다.
    • EntityManager.persist(entity);
  • Entity(객체)를 영속성 컨텍스트에 영속(저장)한다.

 

 

Entity

📌 데이터베이스의 테이블과 매핑되는 Java 클래스를 의미합니다.

  • 데이터베이스에서 Entity 저장할 수 있는 데이터의 집합을 의미한다.
  • JPA에서 Entity란 데이터베이스의 테이블을 나타내는 클래스를 의미한다.

 


1. Entity의 특징

  1. 데이터베이스 테이블과 매핑:
    • Entity는 데이터베이스 테이블과 1:1로 매핑됩니다.
    • Java 객체의 필드는 데이터베이스 테이블의 컬럼에 매핑됩니다.
  2. 필수 어노테이션:
    • @Entity: 클래스를 Entity로 지정.
    • @Id: 기본 키(Primary Key)로 사용할 필드 지정.
  3. 영속성 관리:
    • Entity는 EntityManager에 의해 영속성 상태가 관리됩니다.
  4. POJO(Plain Old Java Object):
    • Entity는 일반 Java 클래스처럼 동작하며, 특별한 상속이나 인터페이스 구현 없이 단순히 JPA의 규약을 따릅니다.

2. Entity 클래스 생성 예제

1) 기본 구조

import jakarta.persistence.*;

@Entity // 이 클래스를 Entity로 지정
@Table(name = "users") // 데이터베이스 테이블 이름 지정
public class User {

    @Id // 기본 키 지정
    @GeneratedValue(strategy = GenerationType.IDENTITY) // 자동 증가 값
    private Long id;

    @Column(nullable = false) // 필수 컬럼 지정
    private String name;

    @Column(unique = true) // 유니크 제약 조건
    private String email;

    // 기본 생성자
    public User() {}

    // Getters and Setters
    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }
}

3. 주요 어노테이션

1) @Entity

  • 클래스를 JPA Entity로 선언.
  • 필수 어노테이션.
@Entity
public class Product {
    // 필드와 메서드 정의
}

2) @Table

  • 매핑할 데이터베이스 테이블 이름을 지정.
  • 생략 시 클래스 이름이 테이블 이름으로 사용.
@Table(name = "products")

3) @Id

  • 기본 키(Primary Key)로 사용할 필드를 지정.
@Id
private Long id;

4) @GeneratedValue

  • 기본 키의 값을 자동 생성.
  • strategy 속성으로 생성 전략을 지정:
    • IDENTITY: 데이터베이스가 자동 증가 값을 제공.
    • SEQUENCE: JPA가 시퀀스를 사용해 기본 키 값 생성.
    • TABLE: 키 생성 테이블 사용.
    • AUTO: 데이터베이스에 맞는 전략을 자동 선택.
@GeneratedValue(strategy = GenerationType.IDENTITY)

5) @Column

  • 테이블의 컬럼에 매핑.
  • 속성:
    • name: 컬럼 이름 지정.
    • nullable: NULL 허용 여부.
    • unique: 유니크 제약 조건.
    • length: 문자열 길이 제한.
@Column(name = "user_name", nullable = false, length = 50)

6) @Transient

  • 특정 필드를 데이터베이스에 매핑하지 않음.
@Transient
private String temporaryData;

4. Entity의 활용

1) 데이터 저장

persist() 메서드를 사용하여 데이터를 저장.

User user = new User();
user.setName("John");
user.setEmail("john@example.com");

EntityManager em = entityManagerFactory.createEntityManager();
em.getTransaction().begin();
em.persist(user); // INSERT 쿼리 실행
em.getTransaction().commit();

2) 데이터 조회

find() 메서드를 사용하여 기본 키를 기준으로 데이터를 조회.

User user = em.find(User.class, 1L); // SELECT 쿼리 실행
System.out.println(user.getName());

3) 데이터 수정

영속 상태의 엔티티를 변경하면 변경 사항이 자동으로 데이터베이스에 반영(Dirty Checking).

em.getTransaction().begin();
user.setName("Updated Name"); // Dirty Checking으로 UPDATE 실행
em.getTransaction().commit();

4) 데이터 삭제

remove() 메서드를 사용하여 엔티티를 삭제.

em.getTransaction().begin();
em.remove(user); // DELETE 쿼리 실행
em.getTransaction().commit();

5. Entity와 데이터베이스 간 매핑

1) 필드와 컬럼 매핑

  • 엔티티 필드는 데이터베이스 컬럼에 매핑됩니다.
@Column(name = "user_email", nullable = false)
private String email;

2) 테이블 매핑

  • 엔티티 클래스는 테이블과 매핑됩니다.
@Table(name = "users")
@Entity
public class User {
    @Id
    private Long id;
    @Column(nullable = false)
    private String name;
}

3) 관계 매핑

  • JPA는 엔티티 간의 관계를 지원하며, 주요 관계 매핑 어노테이션:
    • @OneToOne
    • @OneToMany
    • @ManyToOne
    • @ManyToMany

예: @OneToMany 관계 매핑

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

    @OneToMany(mappedBy = "user")
    private List<Order> orders;
}

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

    @ManyToOne
    @JoinColumn(name = "user_id")
    private User user;
}

6. Entity의 장점

  1. 객체 지향 데이터 모델링:
    • 데이터베이스 테이블을 Java 객체로 매핑하여 객체 중심으로 설계 가능.
  2. SQL 자동 생성:
    • Hibernate나 JPA가 적절한 SQL을 자동으로 생성.
  3. 데이터베이스 독립성:
    • 엔티티를 작성할 때 데이터베이스에 의존하지 않음.
  4. 생산성 향상:
    • 데이터 접근 코드 작성이 단순화되고 유지보수가 쉬움.

7. 주의사항

  1. 기본 생성자 필수:
    • JPA는 리플렉션을 사용하므로 기본 생성자가 반드시 필요.
  2. Serializable 구현 권장:
    • 엔티티는 종종 네트워크나 파일로 전달되므로 Serializable을 구현하는 것이 좋음.
  3. Equals와 HashCode 재정의:
    • 엔티티를 컬렉션에서 사용할 경우, @Id 기반으로 equals()와 hashCode()를 재정의.

결론

Entity는 JPA의 핵심 요소로, 데이터베이스와의 상호작용을 객체 중심으로 단순화하고 효율적으로 설계할 수 있게 합니다. 올바른 매핑과 상태 관리를 통해 데이터베이스와 객체 간의 매끄러운 통신이 가능합니다. 추가적으로 관계 매핑이나 성능 최적화 방법에 대해 더 알고 싶다면 말씀해주세요!

 

 

Entity 생명주기

 

  1. 비영속(new/transient)
    • 영속성 컨텍스트가 모르는 새로운 상태
    • 데이터베이스와 전혀 연관이 없는 객체
  2. 영속(managed)
    • 영속성 컨텍스트에 저장되고 관리되고 있는 상태
    • 데이터베이스와 동기화되는 상태
  3. 준영속(detached)
    • 영속성 컨텍스트에 저장되었다가 분리되어 더 이상 기억하지 않는 상태
  4. 삭제(removed)
    • 영속성 컨텍스트에 의해 삭제로 표시된 상태
    • 트랜잭션이 끝나면 데이터베이스에서 제거

 

엔티티의 생명주기를 현실 세계의 사람과 행정 시스템에 비유해서 설명해 보겠습니다.


1. 비영속 상태 (New/Transient)

  • 비유:
    • 새로 태어난 아기 같은 상태입니다.
    • 아직 주민등록도 되어 있지 않고, 정부 시스템(행정 시스템)과 아무 연관이 없습니다.
    • 단순히 메모리에만 존재하는 객체이며, 데이터베이스에 저장되지 않은 상태입니다.
  • :
    User user = new User(); // 비영속 상태
    user.setName("John");
    user.setEmail("john@example.com");
    
    • User 객체는 만들어졌지만, 아직 데이터베이스와는 아무 관련이 없습니다.

2. 영속 상태 (Managed)

  • 비유:
    • 주민등록이 완료된 상태입니다.
    • 행정 시스템(영속성 컨텍스트)에 등록되어 정부가 그 사람을 기억하고 관리합니다.
    • 만약 사람이 이사(데이터 변경)를 하면, 시스템에도 자동으로 반영됩니다. (자동 동기화)
  • 특징:
    • 영속성 컨텍스트가 이 객체를 관리하며, 변경 사항이 자동으로 데이터베이스에 반영됩니다.
  • :
    • em.persist(user)를 호출하면 영속성 컨텍스트가 user를 관리하기 시작합니다.
    • 이후 user.setName("Updated John")처럼 객체가 변경되면, 데이터베이스에도 자동으로 반영됩니다.
public static void main(String[] args) {
    // EntityManagerFactory 생성
    EntityManagerFactory emf = Persistence.createEntityManagerFactory("test");
    // EntityManager 생성
    EntityManager em = emf.createEntityManager();
    // Transaction 생성
    EntityTransaction transaction = em.getTransaction();

    // 트랜잭션 시작
    transaction.begin();

    try {
        // 비영속
        Tutor tutor = new Tutor(1L, "wonuk", 100);

        System.out.println("persist 전");
        // 영속
        em.persist(tutor);
        System.out.println("persist 후");

        // transaction이 commit되며 실제 SQL이 실행된다.
        transaction.commit();
    } catch (Exception e) {
        // 실패 -> 롤백
        e.printStackTrace();
        transaction.rollback();
    } finally {
        // 엔티티 매니저 연결 종료
        em.close();
    }
    
    emf.close();

}

em.persist() 가 호출되며 영속 상태가 된다.

 

  • persist 후 출력 이후에 SQL이 실행된다.
  • 트랜잭션 Commit 시점에 SQL이 실행된다.

 


3. 준영속 상태 (Detached)

  • 비유:
    • 주민등록이 말소된 상태입니다.
    • 한때 행정 시스템(영속성 컨텍스트)에 등록되었지만, 더 이상 관리되지 않습니다.
    • 이 사람의 정보가 행정 시스템과는 연관이 없으므로, 정보를 업데이트해도 정부 시스템에는 반영되지 않습니다.
  • 특징:
    • 객체는 메모리에 있지만, 영속성 컨텍스트와의 연결이 끊어져 데이터베이스와 동기화되지 않습니다.
  • :
    • detach() 메서드는 객체를 영속성 컨텍스트에서 분리하여 준영속 상태로 만듭니다.
    • 객체를 변경해도 데이터베이스에 영향을 주지 않습니다.
    • 영속성 컨텍스트가 제공하는 기능을 사용하지 못한다.
    1. em.detach()
      • 특정 Entity만 준영속 상태로 변경한다.
    2. em.clear()
      • 영속성 컨텍스트를 초기화 한다.
    3. em.close()
      • 영속성 컨텍스트를 종료한다.

4. 삭제 상태 (Removed)

  • 비유:
    • 사망신고를 한 상태입니다.
    • 행정 시스템(영속성 컨텍스트)에 의해 삭제로 표시되었으며, 트랜잭션이 완료되면 데이터베이스(정부 기록)에서도 완전히 삭제됩니다.
  • 특징:
    • 삭제 상태로 표시되면, 트랜잭션이 완료되는 시점에 데이터베이스에서 삭제됩니다.
  • :
    • remove() 메서드를 호출하면, 해당 객체는 삭제 상태로 표시되고, 트랜잭션이 끝나면 데이터베이스에서 제거됩니다.
  • em.remove(user); // 삭제 상태로 전환

5. 전체 흐름을 비유로 설명

  • 비영속 상태: "아직 주민등록이 안 된 상태의 아기"
    • 시스템(영속성 컨텍스트)과 전혀 연관이 없음.
  • 영속 상태: "주민등록이 완료된 상태"
    • 정부가 해당 사람을 등록하고 관리.
    • 변경 사항(주소, 이름 등)이 자동으로 기록됨.
  • 준영속 상태: "주민등록 말소 상태"
    • 과거에 등록되었지만 더 이상 정부 시스템에서 관리하지 않음.
    • 개인이 어디에 이사 가든 정부는 알 수 없음.
  • 삭제 상태: "사망신고 상태"
    • 정부 시스템에 더 이상 존재하지 않으며, 기록에서 삭제될 예정.

6. 실제 코드 흐름 예제

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

// 영속 상태로 전환
EntityManager em = entityManagerFactory.createEntityManager();
em.getTransaction().begin();
em.persist(user); // 영속 상태
user.setName("Updated John"); // 변경 감지(Dirty Checking)

// 준영속 상태로 전환
em.detach(user);
user.setName("Detached John"); // 데이터베이스에 반영되지 않음

// 삭제 상태로 전환
em.getTransaction().begin();
em.remove(user); // 삭제 상태
em.getTransaction().commit(); // 데이터베이스에서 제거

7. 요약

상태  비유  특징 
비영속 주민등록이 안 된 상태 단순 객체 생성, 데이터베이스와 연관 없음.
영속 주민등록 완료된 상태 영속성 컨텍스트가 관리하며 데이터베이스와 동기화됨.
준영속 주민등록 말소된 상태 더 이상 영속성 컨텍스트가 관리하지 않으며 데이터베이스와 동기화되지 않음.
삭제 사망신고된 상태 데이터베이스에서 삭제로 표시되며, 트랜잭션 완료 시 실제로 제거됨.

 

 

1차 캐시

📌 엔티티를 영속성 컨텍스트에 저장할 때 생성되는 메모리 내 캐시이다. 엔티티는 먼저 1차 캐시에 저장되고 이후 같은 엔티티를 요청하면 DB를 조회하지 않고 1차 캐시에서 데이터를 반환하여 성능을 높일 수 있다.

 

  • 영속성 컨텍스트의 1차 캐시에 저장된다.
// 비영속
Tutor tutor = new Tutor(1L, "wonuk", 100);

// 영속, 1차 캐시에 저장
em.persist(tutor);
  • 영속된 Entity 조회

  • Database가 아닌 1차 캐시에 저장된 Entity를 먼저 조회한다.
// 1차 캐시에서 조회
Tutor findTutor = em.find(Tutor.class, 1L);

 

public static void main(String[] args) {
    // EntityManagerFactory 생성
    EntityManagerFactory emf = Persistence.createEntityManagerFactory("test");
    // EntityManager 생성
    EntityManager em = emf.createEntityManager();
    // Transaction 생성
    EntityTransaction transaction = em.getTransaction();

    // 트랜잭션 시작
    transaction.begin();

    try {
        // 비영속
        Tutor tutor = new Tutor(1L, "wonuk", 100);

        // 영속
        System.out.println("persist 전");
        em.persist(tutor);
        System.out.println("persist 후");

        Tutor findTutor = em.find(Tutor.class, 1L);

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

        // transaction이 commit되며 실제 SQL이 실행된다.
        transaction.commit();
    } catch (Exception e) {
        // 실패 -> 롤백
        e.printStackTrace();
        transaction.rollback();
    } finally {
        // 엔티티 매니저 연결 종료
        em.close();
    }
    emf.close();
}

실행결과

  • 1차 캐시의 Entity를 조회한다.
    • 조회 SQL이 실행되지 않는다.
  • 트랜잭션 Commit 시점에 INSERT SQL이 실행된다.
**트랜잭션(Transaction)**은 데이터베이스 작업에서 데이터의 일관성, 무결성을 보장하기 위해 논리적으로 묶여있는 작업 단위를 의미합니다.

 

 

 

데이터베이스에 저장된 데이터 조회

  • 1차 캐시는 동일한 트랜잭션 안에서만 사용이 가능하다.
  • 요청이 들어오고 트랜잭션이 종료되면 영속성 컨텍스트는 삭제된다.
  • 코드예시
    • xml <property name="hibernate.hbm2ddl.auto" value="none" /> 설정
      • DDL을 자동으로 생성하지 않는다.
public static void main(String[] args) {
    // EntityManagerFactory 생성
    EntityManagerFactory emf = Persistence.createEntityManagerFactory("test");
    // EntityManager 생성
    EntityManager em = emf.createEntityManager();
    // Transaction 생성
    EntityTransaction transaction = em.getTransaction();

    // 트랜잭션 시작
    transaction.begin();

    try {

        // 데이터베이스에서 조회 후 1차 캐시에 저장
        Tutor findTutor = em.find(Tutor.class, 1L);
        // 1차 캐시에서 조회
        Tutor findCacheTutor = em.find(Tutor.class, 1L);

        // transaction이 commit되며 실제 SQL이 실행된다.
        transaction.commit();
    } catch (Exception e) {
        // 실패 -> 롤백
        e.printStackTrace();
        transaction.rollback();
    } finally {
        // 엔티티 매니저 연결 종료
        em.close();
    }
}

실행결과 : 조회 SQL이 한번만 실행된다.

 

동일성 보장

📌 동일한 트랜잭션 안에서 특정 엔티티를 여러 번 조회해도 항상 같은 객체 인스턴스를 반환한다. 영속성 컨텍스트는 1차 캐시를 사용하여 같은 엔티티를 중복 조회해도 동일한 객체를 참조하게 하여 일관성을 유지한다.

 

  • 동일한 트랜잭션 내에서 조회된 Entity는 같은 인스턴스를 반환한다.
  • DB에 저장된 데이터를 조회하여 1차 캐시에 저장한다.
  • 1차 캐시에 저장된 데이터를 조회한다.
  • 코드예시

  • Database에 저장된 데이터가 있는 상태
public static void main(String[] args) {
    // EntityManagerFactory 생성
    EntityManagerFactory emf = Persistence.createEntityManagerFactory("test");
    // EntityManager 생성
    EntityManager em = emf.createEntityManager();
    // Transaction 생성
    EntityTransaction transaction = em.getTransaction();

    // 트랜잭션 시작
    transaction.begin();

    try {

        // DB 조회, 1차 캐시에 저장
        Tutor findTutor1 = em.find(Tutor.class, 1L);
        // 1차 캐시 조회
        Tutor findTutor2 = em.find(Tutor.class, 1L);

        System.out.println("findTutor1 == findTutor2 : " + findTutor1.equals(findTutor2));

        // transaction이 commit되며 실제 SQL이 실행된다.
        transaction.commit();
    } catch (Exception e) {
        // 실패 -> 롤백
        e.printStackTrace();
        transaction.rollback();
    } finally {
        // 엔티티 매니저 연결 종료
        em.close();
    }
    emf.close();
}

 

  • 조회 SQL이 한번만 실행된다.
  • 마치 Java Collection에서 객체를 조회하듯이 사용할 수 있다.

 

쓰기 지연

📌 엔티티 객체의 변경 사항을 DB에 바로 반영하지 않고 트랜잭션이 커밋될 때 한 번에 반영하는 방식으로 이를 통해 성능을 최적화하고 트랜잭션 내에서의 불필요한 DB 쓰기 작업을 최소화한다.

 

  • 예시 코드
    • xml <property name="hibernate.hbm2ddl.auto" value="create" /> 설정
      • Entity와 매핑된 테이블을 삭제 후 새로 생성한다.
    • 코드예시
public static void main(String[] args) {
    // EntityManagerFactory 생성
    EntityManagerFactory emf = Persistence.createEntityManagerFactory("test");
    // EntityManager 생성
    EntityManager em = emf.createEntityManager();
    // Transaction 생성
    EntityTransaction transaction = em.getTransaction();

    // 트랜잭션 시작
    transaction.begin();

    try {
        System.out.println("트랜잭션 시작");
        Tutor tutor1 = new Tutor(1L, "wonuk1", 100);
        Tutor tutor2 = new Tutor(2L, "wonuk2", 200);

        em.persist(tutor1);
        em.persist(tutor2);

        System.out.println("트랜잭션 Commit 전");
        // transaction이 commit되며 실제 SQL이 실행된다.
        transaction.commit();
        System.out.println("트랜잭션 Commit 후");
    } catch (Exception e) {
        // 실패 -> 롤백
        e.printStackTrace();
        transaction.rollback();
    } finally {
        // 엔티티 매니저 연결 종료
        em.close();
    }
    emf.close();
}
  • 트랜잭션이 Commit된 이후에 SQL이 실행된다.
  • 실행결과

  • 여러개의 SQL을 하나씩 나누어 보낸다.
  • hibernate.jdbc.batch_size
    • 여러개의 SQL을 여러번 보내는 것이 아니라 합쳐서 하나로 보낸다.
    • 통신 비용을 줄여서 성능을 개선할 수 있다.

 

변경 감지(Dirty Checking)

📌 영속성 컨텍스트가 엔티티의 초기 상태를 저장하고 트랜잭션 커밋 시점에 현재 상태와 비교해 변경 사항이 있는지 확인하는 기능이다.

 

  • Database에 저장된 데이터가 있는 상태
  • xml <property name="hibernate.hbm2ddl.auto" value="none" /> 설정
public static void main(String[] args) {
    // EntityManagerFactory 생성
    EntityManagerFactory emf = Persistence.createEntityManagerFactory("test");
    // EntityManager 생성
    EntityManager em = emf.createEntityManager();
    // Transaction 생성
    EntityTransaction transaction = em.getTransaction();

    // 트랜잭션 시작
    transaction.begin();

    try {
            
        Tutor tutor = em.find(Tutor.class, 1L);
        tutor.setName("수정된 이름");

        // Java Collection을 사용하면 값을 수정하고 다시 저장하지 않는다.
        // em.persist(tutor);

        System.out.println("트랜잭션 Commit 전");
        // transaction이 commit되며 실제 SQL이 실행된다.
        transaction.commit();
        System.out.println("트랜잭션 Commit 후");
    } catch (Exception e) {
        // 실패 -> 롤백
        e.printStackTrace();
        transaction.rollback();
    } finally {
        // 엔티티 매니저 연결 종료
        em.close();
    }
    emf.close();
}

  • em.persist(tutor); 로 저장하지 않아도 update SQL이 실행된다.
  • Entity를 변경하고자 할 때 em.persist() 를 사용하지 않아야 실수를 방지한다.

em.remove() 를 통해 Entity를 삭제할 때도 위와 같은 방식으로 동작한다. DELETE SQL이 트랜잭션 Commit 시점에 실행된다.

 

 

flush

📌 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영하는 기능으로, 변경된 엔티티 정보를 SQL로 변환해 데이터베이스에 동기화한다. 트랜잭션 커밋 시 자동으로 실행되지만 특정 시점에 데이터베이스 반영이 필요할 때 수동으로 호출할 수도 있다.

 

flush 사용 방법

  1. 자동 호출
    • 트랜잭션이 Commit 되는 시점에 자동으로 호출된다.
  2. 수동 호출
    • em.flush() 를 통해 수동으로 호출할 수 있다.
  • 코드예시
    • xml <property name="hibernate.hbm2ddl.auto" value="create" /> 설정
      • 기존 테이블을 삭제(DROP) 후 다시 생성(CREATE)한다.
public static void main(String[] args) {
    // EntityManagerFactory 생성
    EntityManagerFactory emf = Persistence.createEntityManagerFactory("test");
    // EntityManager 생성
    EntityManager em = emf.createEntityManager();
    // Transaction 생성
    EntityTransaction transaction = em.getTransaction();

    // 트랜잭션 시작
    transaction.begin();

    try {

        Tutor tutor = new Tutor(1L, "wonuk", 100);
        em.persist(tutor);

        // flush 수동 호출
        em.flush();

        System.out.println("트랜잭션 Commit 전");
        // transaction이 commit되며 실제 SQL이 실행된다.
        transaction.commit();
    } catch (Exception e) {
        // 실패 -> 롤백
        e.printStackTrace();
        transaction.rollback();
    } finally {
        // 엔티티 매니저 연결 종료
        em.close();
    }
    emf.close();
}

 

트랜잭션이 Commit되기 전에 SQL이 실행된다.

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

[JPA] 연관관계  (0) 2025.01.12
[JPA] Spring Data JPA  (0) 2025.01.07
[JPA] 연관관계 Mapping  (0) 2025.01.06
[JPA] Entity  (1) 2025.01.05
[JPA] 페러다임 불일치 문제 [객체 관계형 데이터베이스]  (1) 2025.01.02

객체와 관계형 데이터베이스

📌 객체는 클래스를 통해 만들어지며 속성(field)와 기능(method)를 포함하며 관계형 데이터베이스는 데이터를 테이블 형식으로 표현하며 각 테이블은 열(column)과 행(row)으로 구성된다.

 

객체 지향 언어 Java

  • 객체를 저장할 수 있는 다양한 종류의 Database
    1. RDB(무결성, 일관성)
    2. NoSQL
    3. File
    4. 기타 등등..
  • 관계형 데이터베이스와 객체 지향의 패러다임 불일치 문제가 발생한다.

 

관계형 DB에 객체 저장 시 발생하는 문제점

 

1. 관계형 DB와 객체 간의 구조적 차이

  1. 객체 지향 vs. 관계형 모델:
    • 객체지향 언어(Java, Python 등)는 객체를 중심으로 설계되며, 상태(필드)와 행동(메서드)을 결합.
    • 관계형 데이터베이스는 테이블과 행(row)을 기반으로 데이터를 저장하며, 테이블 간 관계(Primary Key, Foreign Key)를 정의.
  2. 데이터 표현 차이:
    • 객체는 복잡한 데이터 구조를 포함할 수 있지만, 데이터베이스 테이블은 정규화된 단순 구조로 데이터를 저장.
  3. 매핑 복잡성:
    • 객체를 RDB 테이블에 맞게 변환(매핑)하는 과정에서 많은 문제가 발생.

 

 

2. CRUD 반복 문제

객체 지향 프로그래밍에서 관계형 데이터베이스와 상호작용할 때 리소스별 CRUD 작업을 반복적으로 작성해야 하는 문제가 자주 발생합니다.

예제 상황

  • 애플리케이션에 User, Product, Order라는 객체가 존재.
  • 각각의 객체에 대해 CRUD(Create, Read, Update, Delete) 작업이 필요.

문제점

  1. 반복적인 SQL 작성:
    • 각 리소스마다 유사한 SQL 쿼리(CRUD)를 작성해야 함.
    • 예:
      • INSERT INTO users ...
      • SELECT * FROM users ...
      • UPDATE users SET ...
      • DELETE FROM users WHERE ...
  2. 코드 중복:
    • 객체를 저장하거나 읽을 때 매번 SQL을 수동으로 작성해야 하며, 이 과정에서 비슷한 코드가 반복됨.
  3. 유지보수 어려움:
    • 리소스가 추가되거나 변경될 때, 관련 SQL이나 매핑 코드를 모두 수정해야 함.
  4. 변경에 따른 리스크:
    • 데이터베이스 스키마가 변경되면, 관련된 모든 CRUD 코드를 수정해야 하므로 버그 발생 가능성이 증가.

 

INSERT, UPDATE, SELECT, DELETE

  • Java Object to SQL
  • SQL to Java Object

 

 

 

패러다임 불일치 문제

📌 객체에서는 상속과 다형성을 통해 객체 관계를 표현할 수 있지만 RDB는 이 개념을 직접 지원하지 않고 별도 매핑이 필요하다. 또한, 객체는 참조로 관계를 표현하고 RDB는 JOIN을 사용하여 관계를 결합한다.

 

 

상속

DB는 상속관계가 없다.

관계형 DB에서는 Data를 슈퍼타입, 서브타입 관계로 설정한다.

  • Tutor 저장

  • Tutor 조회

  • 각각의 객체별로 JOIN SQL 작성, 객체 생성 및 데이터 세팅이 필요하다.
  • 까다롭기 때문에 DB에 저장할 객체는 상속 관계를 사용하지 않는다.

 

연관관계

📌 테이블의 연관관계는 외래 키를 사용한다.

 

📌  객체의 연관관계는 참조를 사용한다

 

연관탐색

📌 객체 지향 언어에서 데이터와 동작을 함께 캡슐화하는 방식과, RDB가 데이터를 정규화된 테이블에 관계 중심으로 저장하는 방식의 차이에서 발생한다. 이로 인해 객체를 데이터베이스에 저장하거나 조회할 때 복잡한 매핑과 변환이 필요해지고 코드의 복잡성과 개발자의 부담이 증가한다.

**정규화(Normalization)**는 관계형 데이터베이스 설계에서 데이터를 중복 없이 체계적으로 저장하기 위해 데이터를 여러 테이블로 나누고, 각 테이블 간에 적절한 관계를 정의하는 과정입니다.

 

  1. 상속과 유사한 관계의 데이터베이스를 구축한 경우 상위 아키텍처는 다음 계층을 믿고 사용가능해야한다.
  2. Entity가 믿고 쓸 수 있는 상속인지 확인해야한다. productRepository.findById();의 신뢰성 확인이 필요하다.
  3. 매번 신뢰성 확인이 필요하니 SQL Query가 굉장히 무거워진다.
  4. 진정한 의미의 계층 분할이 어렵다.
  5. 필요 없는 데이터도 항상 함께 조회된다. (연관된거 싹다 들고온다)

 

 

객체의 비교

Product product1 = productRepository.findById(productId);
Product product2 = productRepository.findById(productId);

product1 == product2; // false
  • 데이터는 같지만, 새로운 인스턴스기 때문에 객체의 주소값이 다르다.

 

 

결론

  • 객체 지향적으로 설계하면 코드가 점점 복잡해진다.
    • SQL Query, Mapping이 까다롭다.
  • Collection처럼 객체가 관리된다면 편리해진다.
// Entity 신뢰 가능
Product product = list.get(productId);
Category category = product.getCategory();

// 비교 가능
Product product1 = list.get(productId);
Product product2 = list.get(productId);

product1 == product2; // true
  • JPA(Java Persistence API) 등장
    • 마치 Java의 Collection처럼 객체를 저장하고 사용할 수 있게 해준다.
    • JPA를 사용하면 패러다임 불일치 문제를 모두 해결할 수 있다.

 

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

[JPA] 연관관계  (0) 2025.01.12
[JPA] Spring Data JPA  (0) 2025.01.07
[JPA] 연관관계 Mapping  (0) 2025.01.06
[JPA] Entity  (1) 2025.01.05
[JPA] 영속성 컨텍스트  (2) 2025.01.04

+ Recent posts