Read Book/JPA 프로그래밍

4장. 엔티티 매핑

nowwater 2023. 2. 20. 23:58
728x90

(1) @Entity, @Table

@Entity

JPA를 사용해서 테이블과 매핑할 클래스는 @Entity 어노테이션을 필수로 붙여야 한다.

@Entity 가 붙은 클래스는 JPA가 관리한다.

속성

1. name

  • JPA에서 사용할 엔티티 이름 지정. 보통 기본값인 클래스 이름을 사용한다.
  • 만약 다른 패지키에 이름이 같은 엔티티 클래스가 있다면 이름을 지정해서 충돌하지 않도록 해야 한다.

@Entity 적용 시 주의 사항

  • 기본 생성자는 필수이다.(파라미터가 없는 public 또는 protected 생성자)
  • final 클래스, enum, interface, inner 클래스에는 사용할 수 없다.
  • 저장할 필드에 final을 사용하면 안된다.
public Member() {}  // 기본 생성자

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

자바는 생성자가 하나도 없으면 파라미터가 없는 기본 생성자를 자동으로 만들지만, 생성자가 하나 이상 존재하면 기본 생성자를 자동으로 만들지 않는다.

이때는 기본 생성자를 직접 만들어야 한다.

왜냐면 JPA가 엔티티 객체를 생성할 때 기본 생성자를 사용하기 때문이다.

@Table

엔티티와 매핑할 테이블을 지정한다. 생략 시 매핑한 엔티티 이름을 테이블 이름으로 사용한다.

@Entity
@Table(name="MEMBER")
public class Member {
    ...
}

 


(2) 다양한 매핑 사용

import javax.persistence.*;
import java.util.Date;

@Entity
@Table(name="MEMBER")
public class Member {

    @Id
    @Column(name = "ID")
    private String id;

    @Column(name = "NAME", nullable = false, length = 10) //추가
    private String username;

    private Integer age;

    @Enumerated(EnumType.STRING)
    private RoleType roleType;

    @Temporal(TemporalType.TIMESTAMP)
    private Date createdDate;

    @Temporal(TemporalType.TIMESTAMP)
    private Date lastModifiedDate;

    @Lob
    private String description;

    @Transient
    private String temp;

    //Getter, Setter

    ...
}

public enum RoleType {
    ADMIN, USER
}

코드 설명

1. roleType : 자바의 enum을 사용해서 회원 타입을 구분. 자바의 enum을 사용하려면 @Enumerated 어노테이션으로 매핑.

2. createDate, lastModifiedDate : 자바의 날짜 타입은 @Temporal을 사용해서 매핑

3. description : 회원을 설명하는 필드는 길이 제한이 없다. 데이타베이스 VARCHAR 타입 대신에 CLOB 타입으로 저장. @Lob를 사용하면 CLOB, BLOB 타입을 매핑할 수 있다.

오라클 기준 LOB 설명

  • CLOB(Character Large OBject) : 텍스트 형태 파일 크기를 4GB까지 지원
  • BLOB(Binary Large OBject) : 이진파일(이미지 등) 형태의 파일 크기를 4GB까지 지원

 


(3) 데이터베이스 스키마 자동 생성

JPA는 데이터베이스 스키마를 자동으로 생성하는 기능을 지원 -> 클래스의 매핑 정보를 보면 어떤 테이블에 어떤 칼럼을 사용하는지 알 수 있다.

어플리케이션 실행 시점에 데이터베이스 테이블을 자동으로 생성한다.

<property name="hibernate.hbm2ddl.auto" value="create" />

콘솔에 실행되는 DDL을 출력한다.

<property name="hibernate.show_sql" value="true" />

출력 예제

Hibernate:
INFO: HHH000227: Running hbm2ddl schema export
    drop table MEMBER if exists
Hibernate:
    create table MEMBER (
        ID varchar(255) not null,
        age integer,
        createdDate timestamp,
        description clob,
        lastModifiedDate timestamp,
        roleType varchar(255),
        NAME varchar(10) not null,
        primary key (ID)
    )
Hibernate:
    alter table MEMBER
        add constraint NAME_AGE_UNIQUE  unique (NAME, age)
1월 11, 2016 1:22:35 오후 org.hibernate.tool.hbm2ddl.SchemaExport execute
INFO: HHH000230: Schema export complete
findMember=지한, age=20
Hibernate:
    /* insert jpabook.start.Member
        */ insert
        into
            MEMBER
            (age, createdDate, description, lastModifiedDate, roleType, NAME, ID)
        values
            (?, ?, ?, ?, ?, ?, ?)
Hibernate:
    /* update
        jpabook.start.Member */ update
            MEMBER
        set
            age=?,
            createdDate=?,
            description=?,
            lastModifiedDate=?,
            roleType=?,
            NAME=?
        where
            ID=?
Hibernate:
    /* select
        m
    from
        Member m */ select
            member0_.ID as ID1_0_,
            member0_.age as age2_0_,
            member0_.createdDate as createdD3_0_,
            member0_.description as descript4_0_,
            member0_.lastModifiedDate as lastModi5_0_,
            member0_.roleType as roleType6_0_,
            member0_.NAME as NAME7_0_
        from
            MEMBER member0_
members.size=1
Hibernate:
    /* delete jpabook.start.Member */ delete
        from
            MEMBER
        where
            ID=?
  • 자동 생성되는 DDL은 지정한 데이터베이스 방언에 따라 달라진다.
  • 스키마 자동 생성 기능이 만든 DDL은 운영환경에서 사용할 만큼 완벽하지 않다.
  • => 따라서 개발 환경에서 사용하거나 매핑 시 참고하는 용도로 사용한다.

스키마 자동 생성 옵션

옵션 설명
create 기존 테이블을 삭제하고 새로 생성. DROP + CREATE
create-drop create 속성에 추가로 애플리케이션 종료 시 생성한 DDL을 제거. DROP + CREATE + DROP
update 데이터베이스 테이블과 엔티티 매핑정보를 비교해서 변경 사항만 수정
validate 데이터베이스 테이블과 엔티티 매핑정보를 비교해서 차이가 있으면 경고를 남기고 애플리케이션을 실행하지 않는다. 이 설정은 DDL을 수정하지 않는다.
none 자동 생성 기능을 사용하지 않을 때 유효하지 않은 옵션값

이름 매핑 전략 변경

테이블 명이나 컬럼 명이 생략되면 자바의 카멜케이스 표기법언더스코어 표기법으로 매핑한다.

<property name="hibernate.ejb.naming_strategy" value="org.hibernate.cfg.ImprovedNamingStrategy" />

 


(4) DDL 생성 기능

DDL 생성 기능으로 제약조건을 추가할 수 있다.

@Entity
@Table(name="MEMBER")
public class Member {

    @Id
    @Column(name = "ID")
    private String id;

    @Column(name = "NAME", nullable = false, length = 10) //추가
    private String username;
    ...
}
// 생성된 DDL
create table MEMBER (
    ID varchar(255) not null,
    NAME varchar(10) not null,
    ...
    primary key(ID)
)
  • nullable = false : not null 제약조건 추가
  • length = 10 : 크기를 지정
// 유니크 제약조건
@Entity(name="Member")
@Table(name="MEMBER", uniqueConstraints = {@UniqueConstraint( // 추가
    name = "NAME_AGE_UNIQUE",
    columnNames = {"NAME", "AGE"} )})
public class Member {

    @Id
    @Column(name = "ID")
    private String id;

    @Column(name = "NAME", nullable = false, length = 10) //추가
    private String username;
    ...
}
// 생성된 DDL
ALTER TABLE MEMBER
    ADD CONSTRAINT NAME_AGE_UNIQUE UNIQUE (NAME, AGE)

이런 기능들은 단지 DDL을 자동으로 생성할 때만 사용되고 JPA 실행 로직에는 영향을 주지 않는다.

따라서 스키마 자동 생성 기능을 사용하지 않고 직접 DDL을 만든다면 사용할 이유가 없다.

이 기능을 사용하면 애플리케이션 개발자가 엔티티만 보고도 손쉽게 다양한 제약조건을 파악할 수 있는 장점이 있다.

 


(5) 기본 키 매핑

@Entity
public class Member {

    @Id
    @Column(name = "ID")
    private String id;

JPA가 제공하는 데이터베이스 기본 키 생성 전략

데이터베이스 벤더마다 기본 키 생성을 지원하는 방식이 다름

기본키 생성 전략 방식

1. 직접 할당 : 기본 키를 어플리케이션이 직접 할당

2. 자동 생성 : 대리 키 사용 방식

  • IDENTITY : 기본 키 생성을 데이터베이스에 위임
  • SEQUENCE : 데이터베이스 시퀀스를 사용해서 기본 키를 할당.
  • TABLE : 키 생성 테이블을 만들어서 시퀀스처럼 사용한다. -> 모든 데이터베이스에서 사용 가능

기본키 생성 방법

  • 기본 키를 직접 할당 : @Id 만 사용
  • 자동 생성 전략 사용 : @GeneratedValue 추가 및 키 생성 전략 선택.

키 생성 전략 사용을 위한 속성 추가

<property name="hibernate.id.new_generator_mappings" value="true" />

기본 키 직접 할당 전략

// 기본 키 직접 할당
@Id
@Column(name = "id")
private String id;


Board board = new Board();
board.setId("id1"); // 기본 키 직접 할당
em.persist(board);

@Id 적용 가능한 자바 타입

  • 자바 기본형
  • 자바 래퍼형
  • String
  • java.util.Date
  • java.sql.Date
  • java.math.BigDecimal
  • java.math.BigInteger

 

IDENTITY 전략

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

주로 MySQL, PostgreSQL, SQL Server, DB2, H2 에서 사용

식별자를 데이터베이스가 자동으로 생성해주는 전략

@GeneratedValue 어노테이션을 사용하고 식별자 생성 전략을 선택한다.

@Entity
public class Board {

  @Id
  @GeneratedValues(strategy = GenerationType.IDENTITY) // 식별자 생성 전략 - 기본 키 값을 얻어오기 위해 데이터베이스를 추가로 조회
  private Long id;
  ...
}
  • 데이터베이스에 값을 저장하고 나서 기본 키 값을 구할 수 있을 때 사용.
  • em.persist() 호출 시 INSERT SQL을 즉시 데이터베이스에 전달한다. -> 쓰기 지연이 동작하지 않는다
  • 식별자를 조회해서 엔티티의 식별자에 할당한다.
  • 엔티티에 식별자 값을 할당하려면 JPA는 추가로 데이터베이스를 조회해야 한다. 하지만 하이버네이트는 JDBC3에 추가된 Statement.getGeneratedKeys() 를 사용하여 데이터를 저장하는 동시에 생성된 기본 키 값도 얻어올 수 있다.

MySQL의 AUTO_INCREMENT 기능

CREATE TABLE BOARD {
    ID INT NOT NULL AUTO_INCREMENT PRIMARY KEY, // 기본 키 자동 생성
    DATA VARCHAR(255)
};

INSERT INTO BOARD(DATA) VALUES('A'); // ID : 1
INSERT INTO BOARD(DATA) VALUES('B'); // ID : 2

 

SEQUENCE 전략

유일한 값을 순서대로 생성하는 특별한 데이터베이스 오브젝트

주로 오라클, PostgreSQL, DB2, H2 데이터베이스에서 사용.

데이터베이스 시퀀스를 통해 식별자를 조회하는 추가 작업이 필요하다. 따라서 데이터베이스와 2번 통신한다.

  1. 식별자를 구하기 위해 데이터베이스 시퀀스를 조회
SELECT BOARD_SEQ.NEXTVAL FROM DUAL
  1. 조회한 시퀀스를 기본 키 값으로 사용해 데이터베이스에 저장
INSERT INTO BOARD...

JPA는 시퀀스에 접근하는 횟수를 줄이기 위해 @SequenceGnerator.allocationSize 를 사용한다.

여기에 설정한 값만큼 한 번에 시퀀스 값을 증가시키고 나서 그만큼 메모리에 시퀀스 값을 할당한다.

ex) allocationSize 값이 50이면 시퀀스를 한 번에 50 증가시킨 다음 1 ~ 50 까지는 메모리에서 식별자를 할당한다.
그리고 51이 되면 시퀀스 값을 100으로 증가시킨 다음 51 ~ 100 까지 메모리에서 식별자를 할당한다.

=> 이러한 최적화 방법은 시퀀스 값을 선점하므로 여러 JVM이 동시에 동작해도 기본 키 값이 충돌하지 않는 장점이 있다.

하지만 데이터베이스에 직접 접근해서 데이터를 등록할 때 시퀀스 값이 한 번에 많이 증가한다는 점을 염두에 둬야한다.

INSERT 성능이 중요하지 않으면 allocationSize 값을 1로 설정하면 된다.

시퀀스 관련 SQL

CREATE TABLE BOARD (
    ID BIGINT NOT NULL PRIMARY KEY,
    DATA VARCHAR(255)
)

//시퀀스 생성
CREATE SEQUENCE BOARD_SEQ START WITH 1 INCREMENT BY 1;

시퀀스 매핑 코드

@Entity
@SequenceGenerator(
    name = "BOARD_SEQ_GENERATOR",
    sequenceName = "BOARD_SEQ", // 데이터베이스의 BOARD_SEQ 시퀀스와 매핑
    initialValue = 1, // DDL 생성 시에만 사용, 처음 시작하는 수 지정. 기본값이 1
    allocationSize = 1 ) // 시퀀스 한 번 호출에 증가하는 수(성능 최적화에 사용). 기본값이 50
public class Board {

    @Id
    @GeneraedValue(strategy = GenerationType.SEQUNCE,
                    generator = "BOARD_SEQ_GENERATOR")
    private Long id;
}

시퀀스 사용 코드

private static void logic(EntityManager em) {
    Board board = new Board();
    em.persist(board);
    System.out.println("board.id = " + board.getId());
}
  1. em.persist() 를 호출하면 먼저 데이터베이스 시퀀스를 사용해 식별자를 조회한다.
  2. 조회한 식별자를 엔티티에 할당 후, 엔티티를 영속성 컨텍스트에 저장한다.
  3. 트랜잭션을 커밋
  4. 플러시 발생 -> 엔티티를 데이터베이스에 저장

IDNETITY 전략은 먼저 엔티티를 데이터베이스에 저장한 후 식별자를 조회해서 엔티티의 식별자에 할당

주의

  • SequenceGenerator.allocationSize 기본값이 50 이므로, 하나씩 증가시키려면 반드시 1로 설정해야 한다.

TABLE 전략

키 생성 전용 테이블을 하나 만들고 여기에 이름과 값으로 사용할 컬럼을 만들어 데이터베이스 시퀀스를 흉내내는 전략

테이블을 생성해 사용하므로 모든 데이터베이스에 적용할 수 있다.

시퀀스 대신에 테이블을 사용한다는 것만 제외하면 SEQUENCE 전략과 내부 동작방식이 같다.

TABLE 전략 키 생성 테이블

create table MY_SEQUENCES (
    sequence_name varchar(255) not null, // 시퀀스 이름
    next_val bigint, // 시퀀스 호출마다 증가하는 수
    primary key (sequence_name)
)

TABLE 전략 매핑 코드

@Entity
@TableGenerator(
    name = "BOARD_SEQ_GENERATOR", // 식별자 생성기 이름
    table = "MY_SEQUENCES", // 키 생성 테이블 명
    pkColumnValue = "BOARD_SEQ", allocationSize = 1)
   // 키로 사용할 값 이름          시퀀스 한 번 호출에 증가하는 수
public class Board {

    @Id
    @GeneratedValue(strategy = GenerationType.TABLE,
                generator = "BOARD_SEQ_GENERATOR")
    private Long id;
}

TABLE 전략 매핑 사용 코드

private static void logic(EntityManger em) {
    Board board = new Board();
    em.persist(board);
    System.out.println("board.id = " + board.getId());
}

MY_SEQUENCES 테이블에 값이 없으면 JPA가 값을 INSERT 하면서 초기화하므로 미리 넣어둘 필요는 없다.

값을 조회하면서 SELECT 쿼리 사용, 다음 값으로 증가시키기 위해 UPDATE 쿼리 사용

=> SEQUENCE 전략과 비교해서 데이터베이스와 한 번 더 통신하는 단점. -> 최적화하려면 @TableGenerator.allocationSize 를 사용

AUTO 전략

GenerationType.AUTO 는 선택한 데이터베이스 방언에 따라 IDENTITY, SEQUENCE, TABLE전략 중 하나를 자동으로 선택

ex) 오라클 - SEQUENCE, MySQL - IDENTITY

AUTO 전략 매핑 코드

@Entity
public class Board {

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

@GeneratedValue.strategy 의 기본값은 AUTO

장점

  • 데이터베이스를 변경해도 코드를 수정할 필요가 없다.
  • 키 생성 전략이 확정되지 않은 개발 초기 단계, 프로토타입 개발 시 편리하다.

SEQUENCE, TABLE 전략 시

스키마 자동 생성 기능을 사용하면 하이버네이트가 기본값을 사용해서 시퀀스나 키 생성용 테이블을 만들어 준다.

 

기본 키 매핑

영속성 컨텍스트는 엔티티를 식별자 값으로 구분하므로 엔티티를 영속 상태로 만들려면 식별자 값이 반드시 있어야 한다.

 

em.persist() 호출 직후 발생하는 일

1. 직접 할당

em.persist() 호출 전 애플리케이션에서 직접 식별자 값을 할당해야 한다. 만약 식별자 값이 없으면 예외 발생

 

2. SEQUENCE

데이터베이스 시퀀스에서 식별자 값을 획득한 후 영속성 컨텍스트에 저장.

 

3. TABLE

데이터베이스 시퀀스 생성용 테이블에서 식별자 값을 획득한 후 영속성 컨텍스트에 저장.

 

4. IDENTITY

데이터베이스에 엔티티를 저장해서 식별자 값을 획득한 후 영속성 컨텍스트에 저장

즉, 테이블에 데이터를 저장해야 식별자 값을 획득할 수 있다.

 

 

데이터베이스 기본 키의 조건

  1. null값은 허용하지 않는다.
  2. 유일해야 한다.
  3. 변해서는 안된다.

 

테이블의 기본 키 선택 전략 2가지

  1. 자연 키(natural key)
  • 비즈니스에 의미가 있는 키
  • ex) 주민등록번호, 이메일, 전화번호
  1. 대리 키(surrogate key)
  • 비즈니스와 관련 없는 임의로 만들어진 키. 대체 키로도 불린다.
  • ex) 오라클 시퀀스, auto_increment, 키생성 테이블 사용

 

자연 키보다는 대리 키를 권장한다. -> 현실과 비즈니스 규칙은 쉽게 변하기 때문에

대리 키는 비즈니스와 무관한 임의의 값이므로 요구사항이 변경되어도 기본 키가 변경되는 일은 드물다.

대리 키를 기본 키로 사용하되, 주민등록번호나 이메일처럼 자연 키의 후보가 되는 컬럼들은
필요에 따라 유니크 인덱스를 설정해서 사용하는 것을 권장

 

미래까지 충족하는 자연 키를 찾기 쉽지 않다.

JPA는 모든 엔티티에 일관된 방식으로 대리 키 사용을 권장

 


(6) 필드와 컬럼 매핑: 레퍼런스

분류 매핑 어노테이션 설명
필드와 컬럼 매핑 @Column 컬럼을 매핑한다.
  @Enumerated 자바의 enum 타입을 매핑한다.
  @Temporal 날짜 타입을 매핑한다.
  @Lob BLOB, CLOB 타입을 매핑한다.
  @Transient 특정 필드를 데이터베이스에 매핑하지 않는다.
기타 @Access JPA가 엔티티에 접근하는 방식을 지정

@Column

기본값 = @Column(nullable = true)

 

하지만 자바 기본 타입일 때는 null 값을 입력할 수 없다.

따라서 자바 기본 타입을 DDL 로 생성할 때는 @Column(nullable = false) 를 추가해주는 것이 안전하다.

JPA는 이런 상황을 고려해서 DDL 생성 기능 사용 시 기본 타입에는 not null 제약 조건을 추가한다. 반면 Integer 같은 객체 타입이면 null 이 입력될 수 있어서 not null 제약조건을 설정하지 않는다.

=> 자바 기본 타입에는 @Column 어노테이션 자체를 생략(자바 기본 타입의 기본값은 not null)하거나 @Column(nullable = false) 까지 적어줘야한다!

 

@Enumerated

자바의 enum 타입을 매핑할 때 사용

@Enumerated(value = ~~)

  • value = EnumType.ORDINAL : enum 순서를 데이터베이스에 저장 => 기본값
  • value = EnumType.STRING : enum 이름을 데이터베이스에 저장
// ex)
// enum 클래스
enum RoleType {
    ADMIN, USER
}

// enum 이름으로 매핑
@Enumerated(EnumType.STRING)
private RoleType roleType;
member.setRoleType(RoleType.ADMIN); // -> DB에 문자 ADMIN으로 저장

1. EnumType.ORDINAL

enum에 정의도니 순서대로 ADMIN은 0, USER는 1 값이 데이터베이스에 저장

  • 장점 : 데이터베이스에 저장되는 데이터 크기가 작다.
  • 단점 : 이미 저장된 enum의 순서를 변경할 수 없다. (중간에 새로운게 추가되면 뒤에꺼 다 1칸씩 밀려야함)

2. EnumType.STRING

enum 이름 그대로 ADMIN은 'ADMIN', USER는 'USER'라는 문자로 데이터베이스에 저장된다.

  • 장점 : 저장된 enum의 순서가 바뀌거나 enum이 추가되어도 안전하다.
  • 단점 : 데이터베이스에 저장되는 데이터 크기가 ORDINAL에 비해서 크다.

 

@Temporal

날짜 타입(java.util.Date, java.util.Calendar)을 매핑할 때 사용

생략 시 timestamp로 정의

@Temporal(value = ~~)

  • value = TemporalType.DATE : 날짜, 데이터베이스 date 타입과 매핑 (ex 2021-05-11)
  • value = TemporalType.TIME : 시간, 데이터베이스 time 타입과 매핑 (ex 12:54:23)
  • value = TemporalType.TIMESTAMP : 날짜와 시간, 데이터베이스 timestamp 타입과 매핑 (ex 2021-05-11 12:54:23)

자바 Date 타입에는 년월일 시분초,
데이터베이스에는 date(날짜), time(시간), timestamp(날짜와 시간) 세 가지 타입이 별도로 존재

@Temporal(TemporalType.DATE)
private Date date; // 날짜

@Temporal(TemporalType.TIME)
private Date time; // 시간

@Temporal(TemporalType.TIMESTAMP)
private Date timestamp; // 날짜와 시간

//==생성된 DDL==//
date date,
time time,
timestamp, timestamp,

 

@Transient

이 필드는 매핑하지 않는다. => 데이터베이스에 저장하지 않고 조회하지도 않는다.

객체에 임시로 어떤 값을 보관하고 싶을 때 사용.

@Transient
private Integer temp;

 


(7) 정리

1. 데이터베이스 스키마 자동 생성 기능 -> 엔티티 객체를 먼저 만들고 테이블은 자동으로 생성

2. JPA는 다양한 기본 키 매핑 전략을 지원

  1. 직접 할당
  2. 데이터베이스가 제공하는 기본 키를 사용하는 SEQUENCE, IDENTITY, TABLE 전략