04. 엔티티 매핑

zwundzwzig·2023년 2월 5일
0
post-thumbnail

JPA를 사용하는 데 가장 중요한 일은 엔티티와 테이블을 정확히 매핑하는 일이기 때문에 매핑 어노테이션 숙지는 필수이다. 이번 장에선 네 가지 대표 매핑 방식 중 세가지에 대해 정리하자.

객체와 테이블 매핑

@Entity

테이블과 매핑할 클래스 앞에 붙이며 JPA가 관리한다.

name 속성이 있는데 JPA에서 사용할 이름을 지정한다. 기본값은 클래스 이름이나, 다른 패키지에 동명의 엔티티가 있다면 충돌을 방지하기 위해 사용해야 한다.

주의사항으로는

  • 파라미터가 없는 public, protected가 붙는 기본 생성자는 필수이다.
  • final 클래스, enum, interface, inner 클래스에 사용할 수 없다.
  • 저장할 필드엔 final이 붙을 수 없다.

@Table

엔티티와 매핑하 테이블을 지정하기 위해 사용한다.

name 속성의 기본값은 매핑한 엔티티 이름을 테이블 이름으로 사용한다.
schema, catalog 속성은 각각의 기능이 있는 DB에서 해당 조건을 매핑한다.
uniqueConstraints 속성은 유니크 제약 조건을 만든다. 스키마 자동 생성 기능으로 DDL을 만들 때만 사용한다.

다양한 매핑 사용

자바의 다양한 타입을 활용해 매핑할 수 있다.

enum

public enum RoleType {
	ADMIN, USER
}

@Enumerated(EnumType.STRING)
private RoleType roleType;

이런 식으로 회원 타입을 구분할 수도 있겠다.

Date

@Temporal(TemporalType.TIMESTAMP)
private Date createdDate;

자바의 날짜 타입은 @Temporal 어노테이션 활용해 매핑한다.

@Lob

@Lob
private String description;

길이 제한이 없는 필드에 사용하며 해당 타입을 매핑할 수 있다.

DB schema 사용

지금까지 테이블을 먼저 생성하고 엔티티를 그 다음에 만들었지만, JPA는 스키마 자동 생성을 활용하면 우린 엔티티만 만들어도 테이블을 자동으로 만들 수 있다.

// application.properties
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.hibernate.ddl-auto=create-drop
spring.datasource.initialization-mode=always
spring.jpa.defer-datasource-initialization=true
  • spring.jpa.show_sql=true : Runtime 콘솔에 DDL을 나타낸다.
  • spring.jpa.properties.hibernate.format_sql=true : Runtime 콘솔 화면에 나타나는 SQL 쿼리문을 pretty하게 나타낸다.
  • spring.jpa.hibernate.ddl-auto=create-drop : @Entity 컴포넌트를 스캔하여, 서버 실행 시 Table "자동 생성" 및 서버 종료 시 Table "자동 삭제"한다.
  • spring.datasource.initialization-mode=always : "src/main/resources/data.sql"에 Import 데이터를 작성하면, 서버 실행 시 자동으로 실행된다.

JPA는 매핑 정보와 DB 방언을 사용해 스키마를 생성한다. 그리고 런타임 시점에 테이블을 자동으로 생성해 개발자가 직접 테이블을 짜는 수고를 덜어준다.

하지만, DDL은 운영 환경에서 사용할 만큼 완벽하지 않기 때문에 참고용으로 사용하자. 특히 운영 서버에서 create, create-drop 옵션은 DLL을 수정하기 때문에 오직 개발 단계에서 사용하자.

DDL 생성 기능

만약 회원 이름은 필수로 입력되야 하고 10자를 초과하면 안 된다는 조건이 추가됐다고 가정하고, 스키마 자동 생성하기를 통해 만들어지는 DDL에 제약 조건을 추가해보자.

@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 name;
}
  • @Column 매핑 정보의 nullable 속성 값을 false로 지정하면 자동 생성되는 DDL에 not null 조건을 추가할 수 있다.
  • length 값으로 DDL 문자 크기를 지정할 수 있다.
  • @UniqueConstraint 어노테이션 값으로 유니크 값을 지정할 수 있다.

이러한 기능들은 DDL을 자동 생성할 때만 사용되고 JPA의 실행 로직엔 영향을 주지 않는다. 그럼에도 개발자가 엔티티만 보고도 다양한 제약 조건을 파악하도록 돕는다.

기본 키 매핑

지금까지 어노테이션만 사용해 회원의 기본키를 애플리케이션에서 직접 할당했다. 그런데 DB에서 생성해 주는 값을 사용하려면 어떻게 매핑해야 할까?

JPA는 다양한 기본 키 생성 전략을 갖고 있다. 다양한 이유는 자동 생성 전략을 취할 때 DB 벤더마다 다른 지원 방식 때문이다.

기본 키 직접 할당 전략

em.persist()로 엔티티를 저장하기 전에 @Id, @Column(name = "id")로 직접 매핑해 값을 할당하는 전략이다.

만약 식별작 값이 없으면 예외가 발생한다.

IDENTITY 전략

기본 키 생성을 DB에 위임하는 전략이다. 주로 MySQL, PostgreSQL, DB2 등에서 사용된다.

개발자가 엔티티에 직접 식별자를 할당하는 게 아니라 식별자가 생성되는 경우 @GeneratedValue 어노테이션을 활용하자.

strategy 값을 GeneratedType.IDENTITY로 맞춰주면 자동으로 식별자값이 DB의 규칙을 따라 올라간다.

이 전략을 사용하면 JPA는 DB에 데이터를 INSERT한 후에 기본키 값을 DB에 추가로 조회한다. persist()로 엔티티를 저장한 직후 할당 값을 조회할 수 있다. JDBC3에 추가된 Statement.getGeneratedKeys()를 사용하면 저장과 동시에 기본 키값을 얻을 수 있다. 하이버네이트는 이를 통해 DB와 한 번만 통신한다.

주의할 점은 엔티티가 영속 상태가 되려면 식별자가 필요한데, 이 전략은 저장 후 식별자를 구별할 수 있으므로 트랜젝션을 지원하는 쓰기 지연이 동작하지 않는다.

SEQUENCE 전략

유일한 값을 순서대로 생성하는 데이터베이스 오브젝트인 DB 시퀀스를 활용해 기본키를 생성하는 전략이다. Oracle, PostgreSQL, DB2, H2에서 사용 가능하다.

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

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

우선 사용할 DB 시퀀스를 매핑해야 한다.

@Entity
@SequenceGenerator(
	name = "BOARD_SEQ_GENERATOR",
    sequenceName = "BOARD_SEQ", // 매핑할 DB 시퀀스 이름
    initialValue = 1,
    allocationSize = 1
)
public class Board {
	@Id
    @GeneratedValue(
    	strategy = GenerationType.SEQUENCE,
        generator = "BOARD_SEQ_GENERATOR"
    )
    private Long id;
    ...
}

@SequenceGenerator를 통해 BOARD_SEQ_GENERATOR라는 시퀀스 생성기를 등록했고, 이름으로 BOARD_SEQ를 지정했다. 이제 JPA는 이 시퀀스 생성기를 실제 DB의 BOARD_SEQ 시퀀스와 매핑한다.

다음으로 키 생성 전략을 GenerationType.SEQUENCE로 설정하고 generator = "BOARD_SEQ_GENERATOR"로 방금 등록한 시퀀스 생성기를 선택했다. 이제 id 식별자 값은 BOARD_SEQ_GENERATOR 시퀀스 생성기가 할당한다.

private static void logic(EntityManager em) {
	Board board = new Board();
    em.persist(board);
    sysout("board id is " + board.getId());
}

위 코드는 IDENTITY 전략과 같지만 내부 동작 방식은 다르다.

SEQUENCE 전략은 em.persist()를 호출할 때 먼저 DB 시퀀스를 사용해 식별자를 조회하고 해당 식별자를 엔티티에 할당한 후 엔티티를 영속성 컨텍스트에 저장한다. 이후 트랜젝션을 커밋해 플러시가 일어나면 엔티티를 DB에 저장한다.

엔티티를 DB에 저장한 후 식별자를 조회해 식별자를 할당하는 IDENTITY 전략과의 차이이다.

@SequenceGenerator

속성기능기본값
name식별자 생성기 이름필수
sequenceNameDB에 등록된 시퀀스 이름hibernate_sequence
initialValueDDL 생성시만 사용. 시퀀스 DDL을 생성할 때 처음 시작하는 수 지정.1
allocationSize시퀀스 한 번 호출에 증가하는 수(성능 최적화에 사용됨)50
catalog, schemaDB catalog, schema 이름

위 기본값은 하이버네이트 기준이다. 매핑할 DDL은 다음과 같다. 참고로, @SequenceGenerator는 @GeneratedValue 옆에서 사용해도 된다.

CREATE SEQUENCE [sequenceName] 
START WITH [initialValue]
INCREMENT BY [allocationSize]

allocationSize의 기본값이 50인 것에 주목하자. 시퀀스 전략은 DB 시퀀스를 통해 식별자를 조회하는 추가 작업이 필요한데, 이런 횟수를 줄이기 위해 allocationSize이 필요하다.
즉, allocationSize의 값만큼 한 번에 시퀀스 값을 증가시키고 메모리에 할당한다.
이러한 최적화 방법은 시퀀스 값을 선점하므로 여러 JVM이 동시에 동작해도 기본 키 값이 충돌하지 않는 장점이 있다.
반면, DB에 접근할 때 한번에 많은 값을 넣게 되는데 DB나 개발자에게 부담스러울 수 있다.
hibernate.id.new_generator_mappings=true여야 최적화 방법이 적용된다.

TABLE 전략

키 생성 전용 테이블을 하나 만들고 여기에 이름과 값으로 사용할 컬럼을 만들어 DB 시퀀스를 흉내내는 전략이다. 이 전략은 테이블을 사용함으로 모든 DB에 적용할 수 있다.

우선 키 생성 용도로 사용할 테이블을 하나 만들자.

// ddl
CREATE TABLE MY_SEQUENCES (
	sequence_name VARCHAR(255) NOT NULL PRIMARY KEY, // 시퀀스 이름
    next_val BIGINT // 시퀀스 값
)
@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;
    ...
}

@TableGenerator를 통해 BOARD_SEQ_GENERATOR라는 시퀀스 생성기를 등록했고, MY_SEQUENCES 테이블을 키 생성용 테이블로 매핑했다.

다음으로 키 생성 전략을 GenerationType.TABLE로 설정하고 @GeneratedValue.generator에 방금 등록한 시퀀스 생성기를 지정했다. 이제 id 식별자 값은 BOARD_SEQ_GENERATOR 시퀀스 생성기가 할당한다.

private static void logic(EntityManager em) {
	Board board = new Board();
    em.persist(board);
    sysout("board id is " + board.getId());
}

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

@TableGenerator

속성기능기본값
name식별자 생성기 이름필수
table키생성 테이블명hibernate_sequences
pkColumnName시퀀스 컬럼 이름sequence_name
valueColumnName시퀀스 값 컬럼 이름next_val
pkColumnValue키로 사용할 값 이름엔티티 이름
initialValue초기값. 마지막으로 생성된 값 기준.0
allocationSize시퀀스 한 번 호출에 증가하는 수(성능 최적화에 사용됨)50
catalog, schemaDB catalog, schema 이름
uniqueConstraints(DDL)유니크 제약 조건을 지정할 수 있다.

위 기본값은 하이버네이트 기준이다. JPA 표준 명세서엔 table, pkColumnName, valueColumnName의 기본값을 JPA 구현체가 정의하도록 했다.

매핑할 DDL테이블 명 {table}
{pkColumnName}{valueColumnName}
{pkColumnValue}{initialValue}

AUTO 전략

@GeneratedValue.strategy의 기본값이다.

선택한 DB 방언에 따라 위 세 전략 중 하나를 자동으로 선택한다.

DB에 따라 유동적이기 때문에 DB를 바꿔도 코드를 수정할 필요가 없다. 대신 시퀀스나 테이블 전략이 선택되면 시퀀스나 테이블 생성용 테이블을 미리 만들어야 한다. 만약 스키마 자동 생성 기능을 사용하면 하이버네이트가 기본값을 사용해 만들어 줄 것이다.

권장하는 식별자 선택 전략

기본 키는 다음 3가지 조건을 만족해야 한다.
1. null 값은 허용되지 않는다.
2. 유일해야 한다.
3. 변해선 안 된다.
테이블의 기본키를 선택하는 전략은 크게 2가지가 있고, JPA는 대리키를 권장한다.
자연키 : 비즈니스에 의미가 있는 키 - ex) 주민번호, 이메일, 전화번호
대리키(대체 키) : 비즈니스와 관련 없는 키 - ex) 오라클 시퀀스, 키생성 테이블 사용

필드와 컬럼 매핑

@Column

객체 필드를 테이블 컬럼에 매핑한다.

속성기능기본값
name매핑할 테이블의 컬럼객체의 필드 이름
nullable(DDL)null 값의 허용 여부. false면 DDL 생성 시 not null 제약조건 붙음.true
unique(DDL)@Table의 uniqueContraints와 같지만 한 컬럼에 간단한 유니크 조건 걸때 사용.
columnDefinition(DDL)DB 컬럼 정보적절한 컬럼 타입
length(DDL)문자 길이 제약 조건. String 타입만 가능255
precision, scale(DDL)BigDecimal 타입에서 전체 자릿수, 소수점 자릿수로 판별.19, 2

int와 같은 자바 기본 타입에 @Column을 사용하려면 nullable=false로 지정하는 게 안전하다.

🧷 참조 교재

  • 김영한, 『자바 ORM 표준 JPA 프로그래밍』 에이콘(2015)
profile
개발이란?

0개의 댓글