엔티티 매핑

윤용운·2022년 3월 29일
1

JPA_스터디

목록 보기
4/9
post-thumbnail

4장. 엔티티 매핑

JPA에서 지원하는 매핑 어노테이션은 크게 다음과 같다.

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

@Entity

JPA를 사용해서 테이블과 매핑할 클래스는 @Entity어노테이션을 필수로 붙여야 한다. 이는 JPA가 관리한다는 뜻으로, 엔티티라고 부른다.

속성기능기본값
name    JPA에서 사용할 엔티티 이름을 지정. 보통 기본값인 클래스 이름을 사용하며, 다른 패키지에 이름이 같은 엔티티가 있다면 이름을 지정해서 충돌하지 않도록 해야 한다.설정하지 않으면 클래스 이름을 그대로 사용한다.

주의사항은 다음과 같다.

  • 기본 생성자는 필수(파라미터가 없는 public 또는 protected 생성자)
  • final 클래스, enum, interface, inner 클래스에서는 사용 불가능
  • 저장할 필드에 final 사용 불가

JPA가 객체를 생성할 때는 기본 생성자를 사용하므로 이 생성자는 반드시 있어야 한다. 만약 다른 생성자가 있다면, 이때는 기본 생성자를 만들어야 한다.

public Member(){}

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

@Table

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

속성기능기본값
name매핑할 테이블 이름엔티티 이름을 사용한다
catalogcatalog 기능이 있는 데이터베이스에서 catalog를 매핑한다.
schemaschema 기능이 있는 데이터베이스에서 schema를 매핑한다.
uniqueConstraints(DDL)DDL 생성 시 유니크 제약조건을 만든다. 2개 이상의 복합 유니크 제약조건도 만들 수 있다. 해당 기능은 스키마 자동 생성 기능을 사용해서 DDL을 만들 때만 사용된다.

다양한 매핑 사용

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

    @Id
    @Column(name = "ID")
    private String id;
    @Column(name = "NAME")
    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;
    
    // Getter, Setter
    ...
}

public enum RoleType {
    ADMIN, USER
}
  • roleType : 자바의 enum을 사용해서 회원의 타입을 구분하였다. 일반 회원은 USER, 관리자는 ADMIN으로 구분하였다. enum문 사용 시, @Enumerated 어노테이션으로 매핑한다.
  • createdDate, lastModifiedDate : 날짜 타입은 @Temporal을 사용해서 매핑한다.
  • description : Lob(Large Object)는 일반적인 데이터베이스에 저장하는 길이인 255개 이상의 문자를 저장하고 싶을떄 저장하며, CLOB(문자기반 데이터), BLOB(binary 데이터) 타입을 매핑할 수 있다.

데이터베이스 스키마 자동 생성

  • JPA는 매핑정보와 데이터 방언을 사용해서 데이터베이스 스키마를 생성한다.
<property name="hibernate.hbm2ddl.auto" value="create" />
  • 해당 속성 추가 시, 애플리케이션 실행 시점에서 데이터베이스 테이블을 자동으로 생성한다.
<property name="hibernate.show_sql" value="true" />
  • hibernate.show_sql 속성을 true로 설정하면 콘솔에 실행되는 테이블 생성 DDL을 출력할 수 있다.

    roletype 은 VARCHAR, createDate, lastModifiedDate은 TIMESTAMP, description은 CLOB 타입으로 생성되었다.

  • 자동 생성되는 DDL은 데이터베이스 방언에 따라 달라진다.

  • 자동 생성된 DDL은 개발자가 직접 생성하는 수고를 덜 수 있지만, 운영 환경에서 사용할 만큼 완벽하지는 않으므로 개발환경에서 사용하거나 매핑을 어떻게 해야 하는지 참고하는 정도로만 사용하는 것이 좋다.

  • hibernate.hdm2ddl.auto 속성은 아래 표로 정리하였다.

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

개발 초기단계는 create 또는 update, 초기화 상태로 자동화된 테스트를 진행하는 개발자 환경과 CI 환경에서는 create 또는 create-drop, 테스트서버는 update 또는 validate, 스테이징과 운영 서버는 validate 또는 none으로 사용하는 것이 좋다.

JPA는 2.1부터 스키마 자동 생성 기능을 표준으로 지원한다. 하지만, update, validate 옵션은 지원하지 않는다.

데이터베이스는 관례상 네이밍에 언더스코어(_)를 사용하고, 자바는 카멜 표기법을 사용하는데, 이를 매핑하려면 @Column.name 속성을 명시적으로 사용해야 한다. 하지만 hibernate.ejb.naming_strategy 속성을 사용하면 이름 매핑 전략을 변경할 수 있다. 직접 변경도 가능하지만, 하이버네이트는 org.hibernate.cfg.ImprovedNamingStrategy 클래스를 제공하여 테이블명이나 컬럼명이 생략 시, 카멜 표기법을 언더스코어 표기법으로 매핑해준다.

DDL 생성 기능

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

  • nullable 속성은 NOT NULL제약조건, length 속성 값을 사용하면 DDL에 문자의 크기를 지정할 수 있다.
@Entity
@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;
    ...
}

  • @TableuniqueConstraints 속성은 유니크 제약조건을 만들어준다.

위의 기능들은 단지 DDL을 자동으로 생성할 때만 사용되고, JPA의 실행로직에는 영향을 주지 않는다. 따라서, 직접 DDL을 만든다면 사용할 이유가 없다. 하지만 해당 기능 사용시, 개발자가 엔티티만 보고도 손쉽게 다양한 제약조건을 파악할 수 있는 장점이 있다.

기본 키 매핑

@Entity
public class Member {

    @Id
    @Column(name = "ID")
    private String id;
    ...
}
  • 지금까지는 @Id어노테이션만 사용해서 회원의 기본 키를 직접 할당했지만, 데이터베이스가 생성해주는 값(MySQL의 AUTO_INCREMEMT)같은 기능들도 사용할 수 있다.
  • 데이터베이스마다 기본 키를 생성하는 방식이 서로 다르고, 이를 JPA에서는 다음과 같이 해결한다
    • 직접할당 : 기본 키를 애플리케이션에서 직접 할당
    • 자동생성 : 대리 키 사용방식
      - IDENTITY : 기본 키 생성을 데이터베이스에 위임
      - SEQUENCE : 데이터베이스 시퀀스를 사용해서 기본키를 할당
      - TABLE : 키 생성 테이블 사용.
  • 데이터베이스 벤더마다 지원하는 방식이 다르기 때문에, 자동 생성 전략도 다양하다. 기본 키를 직접 생성하려면 @Id만 사용하면 되고, 자동 생성 전략 사용시에는 @GeneratedValue 도 추가해주면 된다.

    키 생성 전략 사용시, persistence.xml에 hibernate.id.new_generator_mappings = true 속성을 반드시 추가해줘야 한다.

기본 키 직접 할당 전략

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

Board board = new Board();
board.setId("id1");	// 기본 키 직접 할당
em.persist(board);
  • 기본 키 직접 할당시에는 다음과 같이 @Id로 매핑 후, em.persist()로 엔티티를 저장하기 전에 직접 기본키를 할당하여 준다.
  • @id 적용 가능 타입은 다음과 같다.
    • 자바 기본형
    • 자바 래퍼(Wrapper)형
    • String
    • java.util.Date
    • java.sql.Date
    • java.math.BigDecimal
    • java.math.BigInteger

식별자 값 없이 저장시 예외가 발생하는데, 어떤 예외가 발생하는지는 JPA 표준에 정의되어있지 않다. 하이버네이트를 구현체로 사용하면 JPA 최상위 예외인 javax.persistence.PersistenceException 예외가 발생하는데, 내부에 org.hibernate.id.IdentifierGenerationException 예외를 포함하고 있다.

IDENTITY 전략

  • 기본 키 생성을 데이터베이스에 위임하는 전략으로, MySQL, PostgreSQL, SQL Server, DB2에서 사용한다.
CREATE TABLE BOARD (
	ID INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
    DATA VARCHAR(255)
);
INSERT INTO BOARD(DATA) VALUES('A');
INSERT INTO BOARD(DATA) VALUES('B');
  • 기본 키 컬럼인 IDAUTO_INCREMENT를 추가하면 데이터베이스가 순서대로 값을 채워준다.
IDDATA
1A
2B
  • IDENTITY 전략은 지금처럼 데이터베이스에 값을 저장하고 나서야 기본 키 값을 구할 수 있을 때 사용한다.
@Entity
public class Board {
	@Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    ...
}
  • 다음처럼 @GeneratedValue 어노테이션을 사용하고, strategy 속성 값을 GenerationType.IDENTITY 로 설정해주면 JPA는 기본 키 값을 얻어오기 위해 데이터베이스를 추가로 조회한다.

JDBC3에 추가된 Statement.getGeneratedKeys()를 사용하면 데이터를 저장하면서 동시에 생성된 기본 키 값도 얻어올 수 있다. 하이버테이트는 해당 메소드를 사용해서 데이터베이스와 한번만 통신한다.

엔티티가 영속 상태가 되려면 식별자가 반드시 필요한데, IDENTITY 전략은 엔티티를 데이터베이스에 저장해야 식별자를 구할 수 있으므로, em.persist()를 호출하는 즉시 INSERT SQL이 데이터베이스에 전달된다. 따라서, 해당 전략은 트랜잭션을 지원하는 쓰기 지연이 동작하지 않는다.

SEQUENCE 전략

  • 데이터베이스 시퀀스는 유일한 값을 순서대로 생성하는 특별한 데이터베이스 오브젝트다. SEQUENCE 전략은 이 시퀀스를 사용해서 기본키를 생성하는데, 시퀀스를 사용하는 오라클, PostgreSQL, DB2, H2 데이터베이스에서 사용할 수 있다.
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",
    initialValue = 1, allocationSize = 1)
public class Board {
	@Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, 
    				generator = "BOARD_SEQ_GENERATOR")
    private Long id;
    ...
}
  • @SequenceGenerator를 사용해서 시퀀스 생성기 BOARD_SEQ_GENERATOR를 등록 후 sequenceName 속성의 이름으로 BOARD_SEQ를 지정하면 JPA는 시퀀스 생성기를 실제 데이터베이스의 BOARD_SEQ 시퀀스와 매핑한다. 이후, 키 생성 전략을 GenerationType.SEQUENCE로 설정하고 generator = "BOARD_SEQ_GENERATOR"로 시퀀스 생성기를 선택하면 id 식별자 값은 BOARD_SEQ_GENERATOR 시퀀스 생성기가 할당한다.
  • IDENTITY 전략과는 내부 동작방식이 다른데, em.persist()를 호출 시 먼저 데이터베이스 시퀀스를 사용해서 식별자를 조회하고, 조회한 식별자를 엔티티에 할당한 후 엔티티를 영속성 컨텍스트에 저장한다. 이후 커밋, 혹은 플러시가 일어나면 데이터베이스에 저장하게 된다.
  • @SequenceGenerator 속성 정리
속성기능기본값
name식졀자 생성기 이름필수
sequenceNameDB에 등록되어있는 시퀀스 이름hibernate_sequence
initialValueDDL 생성할 떄 처음 시작하는 수를 지정(DDL 생성시에만 사용)1
allocationSize시퀀스 한번 호출에 증가하는 수(성는 최적화에 사용)50
catalog, schema데이터베이스 catalog, schema 이름
  • allocationSize 기본값이 50인 이유
    SEQUENCE 전략은 시퀀스를 통해 식별자를 조회하는 추가 작업이 필요한데, JPA에서는 시퀀스에 접근하는 횟수를 줄이기 위해 @SequenceGenerator.allocationSize를 사용한다. 시퀀스에 접근 시 50만큼 시퀀스를 증가시킨 후, 1~50까지는 메모리에서 식별자를 할당하고, 51이 되면 시퀀스에 접근하여 100으로 증가 시킨 후 51~100까지 메모리에서 식별자를 할당하는 방식이다.

TABLE 전략

  • 키 생성 전용 테이블을 하나 만들고 여기에 이름과 값으로 사용할 컬럼을 만들어 데이터베이스 시퀀스를 흉내내는 전략이다. 테이블을 사용하므로, 모든 데이터베이스에 적용할 수 있다.
CREATE TABLE MY_SEQUENCES (
	SEQUENCE_NAME VARCHAR(255) NOT NULL,
    NEXT_VAL BIGINT,
    PRIMARY KEY (SEQUENCE_NAME)
)
@Entity
@SequenceGenerator(
	name = "BOARD_SEQ_GENERATOR",
    talbe = "MY_SEQUENCES",
    pkColumnValue = "BOARD_SEQ", allocationSize = 1)
public class Board {
	@Id
    @GeneratedValue(strategy = GenerationType.TABLE, 
    				generator = "BOARD_SEQ_GENERATOR")
    private Long id;
    ...
}
  • @TableGenerator를 사용해서 테이블 키 생성기 등록 후, MY_SEQUENCES테이블을 키 생성용 테이블로 매핑했다. 그리고 TABLE전략을 사용하기 위해 GenerationType.TABLE을 선택해주고, BOARD_SEQ_GENERATOR 테이블 키 생성기를 지정해 주면 된다.
  • 다음과 같이 사용 시, MY_SEQUENCES 테이블에는 다음과 같이 컬럼이 추가가 된다.
SEQUENCE_NAMENEXT_VAL
BOARD_SEQ2
MEMBER_SEQ10
........
  • MY_SEQUENCES 테이블에 값이 없으면 JPA가 값을 INSERT하면서 초기화하므로 값을 미리 넣어둘 필요가 없다.
  • @TableGenerator 속성 정리
속성기능기본값
name식별자 생성기 이름필수
table키 생성 테이블명hibernate_sequences
pkColumnName시퀀스 컬럼명sequence_name
valueColumnName시퀀스 값 컬럼명next_val
pkColumnValue키로 사용할 값 이름엔티티 이름
initialValue키로 사용할 값 이름0
allocationSize시퀀스 한번 호출에 증가하는 수(최적화에 사용)50
catalog, schema데이터베이스 catalog, schema 이름
uniqueConstraints(DDL)유니크 제약 조건 지정
  • JPA 표준 명세에는 table, pkColumnName, valueColumnName의 기본값을 JPA 구현체가 정의하도록 하였다.
{pkColumnName}{valueColumnName}
{pkColumnValue}{initialValue}

AUTO 전략

GenerationType.AUTO는 선택한 데이터베이스 방언에 따라 여러 전략중 하나를 자동으로 선택한다. @GenerationValue.strategy의 기본값은 AUTO이므로, 다음과 같이 사용해도 결과는 같다

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

AUTO를 사용할 때, SEQUENCETABLE 전략이 사용된다면 시퀀스나 키 생성 테이블을 미리 만들어 두어야 한다. 스키마 자동 생성을 사용한다면, 하이버네이트가 기본값을 사용해서 적절한 시퀀스나 키 생성용 테이블을 만들어 줄 것이다.

기본 키 매핑 정리

  • 직접할당
    em.persist()를 호출하기 전 애플리케이션에서 직접 식별자 값을 할당해야 한다. 없으면 예외가 발생한다.
  • SEQUENCE
    데이터베이스 시퀀스에서 식별자 값을 획득 후 영속성 컨텍스트에 저장한다.
  • TABLE
    데이터베이스 시퀀스 생성용 테이블에서 식별자 값을 획득 후 영속성 컨텍스트에 저장한다.
  • IDENTITY
    데이터베이스에 엔티티를 저장해서 식별자 값을 획득 후 영속성 컨텍스트에 저장한다.(테이블에 데이터를 저장해야 식별자 값 획득 가능)

권장하는 식별자 선택 전략

  • 기본키의 조건
    • null은 X
    • 유일해야 한다
    • 변해선 안된다
  • 기본 키 선택 전략
    • 자연 키
      비즈니스에 의미가 있는 키 (ex : 주민번호, 이메일, 전화번호)
    • 대리 키
      비즈니스와 관련 없는 임의로 만들어진 키. 대체 키로도 불린다. (ex : 시퀀스, auto_increment, 키 생성 테이블 사용)
  • 자연 키보다는 대리 키를 권장하며, JPA는 모든 엔티티에 일관된 방식으로 대리 키 사용을 권장한다.

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

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

@Column

객체 필드를 테이블 컬럼에 매핑한다. 속성 중 name, nullable이 주로 사용된다. insertable, updatable속성은 데이터베이스에 저장되어 있는 정보를 읽기만 하고 실수로 변경하는것을 방지하고 싶을 때 사용한다.

속성기능기본값
name필드와 매핑할 테이블의 컬럼 이름객체의 필드 이름
insertable
(거의 사용하지 않음)
엔티티 저장 이 필드도 같이 저장한다. false로 설명하면 이 필드는 데이터베이스에 저장하지 않는다. false 옵션은 읽기 전용일 때 사용한다.true
updatalbe
(거의 사용하지 않음)
엔티티 수정 시 이 필드도 같이 수정한다. false로 설명하면 이 필드는 데이터베이스에 수정하지 않는다. false 옵션은 읽기 전용일 때 사용한다.true
table
(거의 사용하지 않음)
하나의 엔티티를 두개 이상의 테이블에 매핑할 때 사용한다.현재 클래스가 매핑된 테이블
nullable(DDL)null값의 허용 여부를 설정한다. false로 설정시 DDL 생성시에 NOT NULL 제약조건이 붙는다true
unique(DDL)@Table의 uniqueConstraints와 같지만, 한 컬럼에 간단히 유니크 제약조건을 걸 떄 사용한다. 두 컬럼 이상을 사용해서 유니크 제약조건을 사용하려면 클래스 레벨에서 @Table.uniqueConstraints를 사용해야 한다
columnDefinition(DDL)데이터베이스 컬럼 정보를 직접 줄 수 있다필드의 자바 타입과 방언 정보를 사용해서 적절한 컬럼 타입을 생성한다
length(DDL)문자 길이 제약조건. String 타입에만 사용한다255
precision, scale(DDL)BigDecimal, BigInteger타입에서 사용한다. precision은 소수점을 포함한 전체 자릿수를, scale은 소수의 자릿수이다. double, float 타입에는 적용되지 않는다. 아주 큰 숫자나 정밀한 소수를 다루어야 할 때만 사용한다.precision = 19, scale = 2

@Column을 생략 시 대부분 속성의 기본값이 적용되는데, 기본타입에는 null이 입력될 수 없으므로 nullable = false 조건을 자동으로 추가해준다. 하지만 @Column 사용 시에는 nullable = true가 기본값이므로 not null 제약조건을 추가해주지 않기 떄문에 기본타입에 @Column 사용시에는 nullable = false로 지정하는 것이 안전하다.

@Enumerated

속성기능기본값
value- EnumType.ORDINAL : enum 순서를 데이터베이스에 저장
- EnumType.STRING : enum 이름을 데이터베이스에 저장
EnumType.ORDINAL
  • EnumType.ORDINAL은 enum에 정의된 순서대로 데이터베이스에 저장된다.
    • 장점 : 데이터베이스에 저장되는 데이터 크기가 작다.
    • 단점 : 이미 저장된 enum의 순서를 변경할 수 없다.
  • EnumType.STRING은 enum에 정의된 문자로 데이터베이스에 저장된다.
    • 장점 : 저장된 enum의 순서가 바뀌거나 추가되도 안전하다
    • 단점 : ORDINAL에 비해서 저장된 데이터 크기가 크다

EnumType.STRING을 권장한다고 한다.

@Temporal

속성기능기본값
value- Temporal.DATE : 날짜, 데이터베이스 date 타입과 매핑 (ex : 2022-03-29)
- Temporal.TIME : 시간, 데이터베이스 time 타입과 매핑 (ex : 22:22:22)
- Temporal.TIMESTAMP : 날짜와 시간, 데이터베이스 timestamp 타입과 매핑 (ex : 2022-03-29 22:22:22)
필수로 지정해야 한다
  • 날짜 타입(java.util.Date, java.util.Calendar)을 사용할 때 사용한다.

@Lob

  • 데이터베이스 BLOB, CLOB 타입과 매핑한다.
    • BLOB : byte[], java.sql.BLOB
    • CLOB : String, char[], java.sql.CLOB

@Transient

  • 해당 필드는 매핑하지 않는다. 임시로 어떤 값을 보관하고 싶을 때 사용한다.

@Access

  • JPA가 엔티티 데이터에 접근하는 방식을 지정한다.
    • 필드 접근 : AccessType.FIELD로 지정. 필드 접근 권한이 private이어도 접근할 수 있다.
    • 프로퍼티 접근 : AccessType.PROPERTY로 지정. 접근자(Getter)를 사용한다.
  • @Access를 설정하지 않으면 @Id의 위치를 기준으로 접근 방식이 설정된다(필드, Getter)

Reference

  • 자바 ORM 표준 JPA 프로그래밍 (김영한)

0개의 댓글