프레임워크(Framework)/JPA

[JPA] 기본 키 매핑 - 엔티티 매핑(Entity Mapping) - 2

잇트루 2022. 11. 29. 20:25
반응형
본 내용은 온라인 강의 사이트 인프런의 김영한 님의 강의 내용이 포함되어 있습니다.
'자바 ORM 표준 JPA 프로그래밍 - 기본편'
 

자바 ORM 표준 JPA 프로그래밍 - 기본편 - 인프런 | 강의

JPA를 처음 접하거나, 실무에서 JPA를 사용하지만 기본 이론이 부족하신 분들이 JPA의 기본 이론을 탄탄하게 학습해서 초보자도 실무에서 자신있게 JPA를 사용할 수 있습니다., - 강의 소개 | 인프런

www.inflearn.com

 

Intro

엔티티 매핑과 연관관계 매핑

JPA를 이용해 데이터베이스의 테이블과 상호 작용(데이터 저장, 수정, 조회, 삭제 등) 하기 위해 먼저 해야 하는 작업은 데이터베이스 테이블과 엔티티 클래스 간의 매핑 작업이다.

 

엔티티 매핑 작업은 객체와 테이블 간의 매핑, 기본키 매핑, 필드와 컬럼 간의 매핑, 엔티티 간의 연관 관계 매핑 등으로 나눌 수 있다.

 

특히 엔티티 간의 연관 관계 매핑은 JPA에서 가장 어려우면서도 중요하다.

 

엔티티 매핑

  • 객체와 테이블 간의 매핑 : @Entity, @Table
  • 필드와 컬럼 간의 매핑 : @Column
  • 기본 키 매핑 : @Id
  • 연관관계 매핑 : @ManyToOne, @JoinColumn, @OneToMany, @ManyToMany

 

기본키 매핑 - @Id

데이터베이스의 테이블에 기본키 설정은 필수이다.

JPA에서는 기본적으로 @Id 어노테이션을 추가한 필드가 기본 키 컬럼이 된다.

 

JPA 기본키 생성 전략

  1. 기본키 직접 할당 : 애플리케이션 코드 상에서 기본키를 직접 할당해 주는 방식이다.
  2. 기본키 자동 생성
    • IDENTITY
      • 기본키 생성을 데이터베이스에 위임하는 전략이다.
      • 데이터베이스에서 기본키를 생성해주는 대표적이 방식은 MySQL의 AUTO_INCREMENT 기능을 통해 자동 증가 숫자를 기본키로 사용하는 방식이 있다.
    • SEQUENCE
      • 데이터베이스에서 제공하는 시퀀스를 사용해서 기본키를 생성하는 전략이다.
    • TABLE
      • 별도의 키 생성 테이블을 사용하는 전략이다.

 

기본 키 직접 할당

기본 키를 직접 할당하는 경우 @Id만 사용한다.

@NoArgsConstructor
@Getter
@Setter
public class Member {
    @Id // 기본키 직접 할당
    private Long memberId;

    public Member(Long memberId) {
        this.memberId = memberId;
    }
}

엔티티 클래스의 기본키를 지정할 필드에 @Id 어노테이션을 붙이는 것으로 기본키를 직접 할당할 수 있다.

 

@Configuration
public class JpaIdDirectMappingConfig {
    private EntityManager em;
    private EntityTransaction tx;

    @Bean
    public CommandLineRunner testJpaSingleMappingRunner(EntityManagerFactory emFactory){
        this.em = emFactory.createEntityManager();
        this.tx = em.getTransaction();

        return args -> {
            tx.begin();
            em.persist(new Member(1L));  // 데이터 저장
            tx.commit();
            Member member = em.find(Member.class, 1L);

            System.out.println("# memberId: " + member.getMemberId());
        };
    }
}

기본키 직접 할당 이후 데이터를 저장할 수 있다.

 

IDENTITY 전략

IDENTITY는 기본 키 생성하는 것을 데이터베이스에 위임하는 전략이다.

MySQL 데이터베이스에 사용하는 방식으로 AUTO_INCREMENT와 동일하다.

@NoArgsConstructor
@Getter
@Entity
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY) // IDENTITY
    private Long memberId;

    public Member(Long memberId) {
        this.memberId = memberId;
    }
}

IDENTITY 기본키 생성 전략을 설정하기 위해 @GeneratedValue 어노테이션의 strategy 속성을 GenerationType.IDENTITY로 지정해야 한다.

 

이후 데이터베이스에서 기본키를 대신 생성하게 된다.

@Configuration
public class JpaIdIdentityMappingConfig {
    private EntityManager em;
    private EntityTransaction tx;

    @Bean
    public CommandLineRunner testJpaSingleMappingRunner(EntityManagerFactory emFactory){
        this.em = emFactory.createEntityManager();
        this.tx = em.getTransaction();

        return args -> {
            tx.begin();
            em.persist(new Member()); // 기본키를 따로 지정하지 않고 저장
            tx.commit();
            Member member = em.find(Member.class, 1L); // memberId로 조회

            System.out.println("# memberId: " + member.getMemberId());
        };
    }
}

해당 데이터를 저장할 때, 별도의 기본키 값을 할당하지 않아도 데이터베이스에서 자동으로 기본키를 할당하고 저장해 준다.

 

참고 사항

IDENTITY 전략은 기본 키 생성을 데이터베이스에 위임하기 때문에 데이터베이스에 저장이 되어야만 기본 키가 생성된다.

하지만, JPA는 보통 트랜잭션 커밋 시점에 INSERT SQL을 실행하게 된다.

  • AUTO_INCREMENT는 데이터베이스에 INSERT SQL을 실행한 이후에 ID 값을 알 수 있다.

 

이러한 문제를 해결하기 위해 IDENTITY 전략에서는 persist()가 실행하는 시점에 즉시 INSERT SQL을 실행하고 DB에서 식별자를 조회하도록 하고 있다.

Member member = new Member();
member.setUsername("Hello");

System.out.println("member.getId() = " + member.getId());
em.persist(member);
System.out.println("member.getId() = " + member.getId());

tx.commit(); // 보통은 커밋 시에 INSERT SQL 실행
// 쿼리 실행 시점 출력
...
member.getId() = null
Hibernate: 
    /* insert hellojpa.Member
        */ insert 
        into
            Member
            (id, name) 
        values
            (default, ?)
member.getId() = 1
...

 

SEQUENCE 전략

SEQUENCE는 데이터베이스 시퀀스 오브젝트를 사용하여 기본 키를 자동으로 생성하는 전략이다.

Oracle 데이터베이스에서 사용하는 방식이다.

@NoArgsConstructor
@Getter
@Entity
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)  // (1)
    private Long memberId;

    public Member(Long memberId) {
        this.memberId = memberId;
    }
}

SEQUENCE 전략을 사용하기 위해서는 @GeneratedValue 어노테이션의 strategy 속성을 GenerationType.SEQUENCE로 지정해야 한다.

 

이는 데이터베이스 시퀀스를 사용하여 기본키를 생성하게 된다.

@Configuration
public class JpaIdIdSequenceMappingConfig {
    private EntityManager em;
    private EntityTransaction tx;

    @Bean
    public CommandLineRunner testJpaSingleMappingRunner(EntityManagerFactory emFactory){
        this.em = emFactory.createEntityManager();
        this.tx = em.getTransaction();

        return args -> {
            tx.begin();
            em.persist(new Member());  // 기본키를 따로 지정하지 않고 저장
            Member member = em.find(Member.class, 1L); // memberId로 조회
            System.out.println("# memberId: " + member.getMemberId());
            tx.commit();

        };
    }
}

IDENTITY 전략과 유사하게 기본키를 지정하지 않고도 Member 객체를 생성하면서 memberId를 통해 조회할 수 있다.

하지만 SEQUENCE 전략은 엔티티가 영속성 컨텍스트에 저장되기 전에 데이터베이스 시퀀스에서 기본키에 해당하는 값을 제공하게 된다.

 

@SequenceGenerator

@SequenceGenerator를 통해 테이블마다 다른 시퀀스로 관리할 수 있다.

@Entity
@SequenceGenerator(
        name = "MEMBER_SEQ_GENERATOR",
        sequenceName = "MEMBER_SEQ", //매핑할 데이터베이스 시퀀스 이름
        initialValue = 1, allocationSize = 1)
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "MEMBER_SEQ_GENERATOR")
    private Long memberId;

    public Member(Long memberId) {
        this.memberId = memberId;
}
  • name : 식별자 생성기 이름을 지정하는 것으로 필수로 작성해야 한다.
  • sequenceName : 데이터베이스에 등록되어 있는 시퀀스 이름을 지정한다. 지정하지 않을 시 기본 값으로 hinernate_sequence라는 이름을 가지게 된다.
  • initialValue : DDL 생성 시에만 사용되는 속성으로, 시퀀스 DDL을 생성할 때 처음 시작하는 수를 지정한다.
  • allocationSize : 시퀀스 한 번 호출에 증가하는 수를 지정하는 속성으로 기본값은 50을 가진다. 데이터베이스 시퀀스 값이 하나씩 증가하도록 설정되어 있으면 이 값을 반드시 1로 설정해야 한다.
  • catalog, schema : 데이터베이스 catalog와 schema의 이름을 지정한다.

 

참고사항

SEQUENCE 전략은 데이터베이스의 시퀀스 오브젝트를 사용하기 때문에 데이터베이스로부터 시퀀스 값을 가져와야 한다.

 

따라서 SEQUENCE 전략은 persist() 실행 시 데이터베이스로부터 증가된 시퀀스 값을 가져올 수 있도록 쿼리문이 실행된다. 그 이후 commit이 실행되는 시점에 쿼리문을 보낸다.

member.setUsername("Hello");

System.out.println("member.getId() = " + member.getId());
em.persist(member); // 시퀀스 할당 시점
System.out.println("member.getId() = " + member.getId());

tx.commit(); // SQL 실행 시점
// 쿼리 생성 시점 출력
member.getId() = null
Hibernate: 
    call next value for MEMBER_SEQ // 시퀀스 증가 쿼리
member.getId() = 1
Hibernate: 
    /* insert hellojpa.Member
        */ insert 
        into
            Member
            (name, id) 
        values
            (?, ?)
11월 16, 2022 9:31:10 오후 org.hibernate.engine.jdbc.connections.internal.DriverManagerConnectionProviderImpl$PoolState stop
INFO: HHH10001008: Cleaning up connection pool [jdbc:h2:tcp://localhost/~/test]

Process finished with exit code 0

persist()를 실행할 때마다 데이터베이스에 시퀀스 값을 증가시킬 경우 성능이 느려질 것이다.

이러한 문제를 해결하기 위한 속성이 allocationSize이다.

 

allocationSize는 데이터베이스의 시퀀스 사이즈를 미리 확보하여 메모리에 적재한다.

이후에는 persist()가 여러 번 호출되어도 데이터베이스에 쿼리문을 실행하는 것이 아닌, 메모리에서 시퀀스 값을 증가시키면서 가져올 수 있다.

allocationSize는 성능 최적화를 위해 사용하는 속성이다.

 

TABLE 전략

테이블 전략은 키 생성 전용 테이블을 따로 만들어서 데이터베이스 시퀀스를 흉내 내는 전략이다.

모든 데이터베이스에 적용 가능하다는 장점이 있으나, 성능이 좋지 못하다.

@Entity
@TableGenerator(
        name = "MEMBER_SEQ_GENERATOR",
        table = "MY_SEQUENCES",
        pkColumnValue = "MEMBER_SEQ", allocationSize = 1)
public class Member {
            
    @Id
    @GeneratedValue(strategy = GenerationType.TABLE, generator = "MEMBER_SEQ_GENERATOR")
    private Long id;

    public Member(Long memberId) {
        this.memberId = memberId;
}

@TableGenerator 속성

  • name : 식별자 생성기의 이름 (필수 속성)
  • table : 키 생성 테이블의 이름 (기본값 : hibernate_sequences)
  • pkColumnName : 시퀀스 컬럼의 이름 (기본값 : sequence_name)
  • valueColumnNa : 시퀀스 값 컬럼명 (기본값: next_val)
  • pkColumnValue : 키로 사용할 값 이름 (기본값: 엔티티 이름)
  • initialValue : 초기 값, 마지막으로 생성된 값이 기준이다. (기본값 : 0)
  • allocationSize : 시퀀스 한 번 호출에 증가하는 수 (기본값 : 50)
  • catalog, schema : 데이터베이스 catalog와 schema 이름
  • uniqueConstraints : 유니크 제약 조건 지정

 

AUTO 전략

AUTO 전략은 @Id 필드에 @GeneratedValue(strategy = GenerationType.AUTO)를 지정하면서 JPA가 데이터베이스의 Dialect에 따라서 적절한 전략을 자동으로 선택하게 하는 방법이다.

 

참고

Dialect는 표준 SQL이 아닌 특정 데이터베이스에 특화된 고유한 기능을 의미한다.

만약, JPA가 지원하지 않는 데이터베이스의 기능을 사용할 경우 Dialect가 처리해준다.

 

권장하는 식별자 전략

기본 키 제약 조건

기본 키는 null 값을 가질 수 없어야 하며 유일한(유니크) 데이터이고 변하면 안 된다.

 

기본 키는 null 값을 가질 수 없으면서 유니크 값으로 제약하는 것은 쉬우나 먼 미래까지 변하지 않고 유지하는 것은 어렵다.

하지만, 이 조건을 모두 만족하는 자연 키는 찾기 어렵다. 따라서 대리 키(대체키)를 사용해야 한다.

 

기본 키 제약 조건을 만족하기 위한 권장하는 방법은 기본키를 Long형 + 대체키 + 키 생성 전략을 사용하는 것이다.

반응형