여우발개발

4장. 엔티티 매핑 본문

기술 도서 리뷰/자바 ORM 표준 JPA 프로그래밍

4장. 엔티티 매핑

♬여우 2025. 3. 7. 13:56

4.1 @Entity

@Entity 를 선언한 클래스는 JPA가 entity로써 관리한다.

  • 파라미터가 없는 기본 생성자 필수 (public, protected 가능)
    • lombok @NoArgConstructor(access = AccessLevel.PROTECTED) 사용 추천
  • final class, enum, interface, inner class에는 사용 불가
  • 저장할 field가 final이면 안 된다.
속성 기능 기본값
name JPA에서 사용할 엔티티 이름을 지정한다. {class 이름}

4.2 @Table

속성 기능 기본값
name 매핑할 테이블 이름 {entity 이름}
catalog catalog 기능이 있는 DB에서 catalog 매핑  
schema schema기능이 있는 DB에서 schema매핑  
uniqueConstraints (DDL) DDL 생성 시 unique 제약조건 생성  
(스키마 자동 생성 기능을 사용해서 DDL 만들 때만 사용됨)    

4.3 다양한 매핑 사용

책에서 뒷 내용 설명을 위한 간단한 예시를 보여주고 있다.

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

JPA는 DB 스키마를 자동으로 생성하는 기능을 지원함

  • spring-data-jpa에서의 설정
spring:
  jpa:
    hibernate:
      ddl-auto: {옵션 값}
옵션 설명 추천 사용 시기
create 기존 테이블을 삭제하고 새로 생성 (DROP + CREATE) 개발 초기, 개발, CI
create-drop create + 어플리케이션 종료 시 생성한 DDL 제거 (DROP + CREATE + DROP) 개발, CI
update DB 테이블과 entity 매핑정보를 비교해서 변경 사항만 수정 개발 초기, 테스트
validate DB 테이블과 entity 매핑정보를 비교해서 차이가 있으면 경고하고 어플리케이션 실행시키지 않음 (DDL 수정 X) staging, prod
none invalid한 값을 주면 동작하지 않음 staging, prod
  • 이름 매핑 전략 변경하기
    • java는 관례상 camel case를 사용
    • DB는 관례상 snake case를 사용
    • naming_strategy 속성을 사용하면 이름 매핑 전략 변경 가능
더보기

💡 Hibernate5부터 org.hibernate.cfg.NamingStrategy는 사용되지 않음

  • hibernate.implicit_naming_strategy, hibernate.physical_naming_strategy 로 분리 대체
    • implicit_naming_strategy (암시적 명칭 전략) : naming이 지정되지 않은 경우 적용
    • physical_naming_strategy (물리적 명칭 전략) : naming이 지정되어도 적용
    • physical_naming_strategy 는 implicit_naming_strategy 가 적용된 이후에도 덮어씌움

 

  • spring-boot-jpa에서의 설정 방법
spring.jpa.hibernate.naming.strategy: {설정 값}

spring.jpa.hibernate.naming:
    implicit-strategy: {설정 값}
    physical-strategy: {설정 값}

4.5 DDL 생성 기능

스키마 자동 생성하기를 통해 만들어지는 DDL에 제약조건을 추가할 수 있다.

  • @Table의 uniqueConstraints
@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")
        private String username;

        private Integer age;
        ...
}
ALTER TABLE MEMBER
        ADD CONSTRAINT NAME_AGE_UNIQUE UNIQUE (NAME, AGE)
  • DDL 관련 annotaion은 DDL 생성 시에만 사용되고 JPA의 실행 로직에는 영향을 주지 않음
    • 직접 DDL을 만든다면 사용할 필요가 없음
    • 대신 개발자가 Entity class만 보고 손쉽게 제약조건을 파악할 수 있다

4.6 기본 키 매핑

기본 키를 직접 할당하는 대신 DB가 생성하는 방식으로 생성하려면?

DB마다 기본 키 생성 방식이 서로 다른 점을 유의해야 한다

더보기

💡 책에서는 hibernate.id.new_generator_mappings 옵션을 true로 설정해야 키 생성 전략의 성능 최적화가 가능하다고 되어있으나 Hibernate 4 기준이고 5 이상인 경우 기본 true로 설정되어 있다.

 

4.6.1 기본 키 직접 할당 전략

@Id
@Column
private long id;
  • 구현체에 따라 @Id 적용 가능 자바 타입이 달라질 수 있다.
    • 책에서는 7가지로 안내되어 있는데 요새는 UUID를 사용하는 케이스도 많아졌다고 함
Board board = new Board();
board.setId(1L);
em.persist(board);
  • 기본 키 직접 할당 전략
    • entity를 저장하기 전에 application에서 기본 키를 직접 할당하는 방법
    • 식별자 값 없이 저장하면 예외 발생 (Hibernete : IdentifierGenerationException → (래핑) → JPA : PersistenceException)

4.6.2 IDENTITY 전략

-- DDL
CREATE TABLE BOARD {
        ID INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
        DATA VARCHAR(255)
};

-- ID값이 없어도 DB가 자동으로 셋팅해 줌
INSERT INTO BOARD(DATA) VALUES('A');
  • 기본 키 생성을 DB에 위임하는 전략
  • MySQL, PostgreSQL, SQL Server, DB2에서 지원
  • DB에 값을 저장하고 나서야 기본 키 값을 구할 수 있을 때 사용
@Entity
public class Board {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private long id;
}
  • IDENTITY 전략은 DB에 INSERT한 후에 기본 키 값을 알 수 있다.
    • entity가 영속 상태가 되려면 식별자가 필요하다
      • 영속화시킬 때 바로 INSERT가 필요함
        • 쓰기 지연 불가능
      • entity에 식별자 값을 할당하려면 DB 조회가 필요
        • hibernete는 저장하면서 동시에 생성된 키 값을 얻어오는 Statement.getGeneratedKeys()를 사용해 DB와 한 번만 통신한다.

4.6.3 SEQUENCE 전략

-- DDL
CREATE TABLE BOARD {
        ID BIGINT NOT NULL PRIMARY KEY,
        DATA VARCHAR(255)
};

-- 시퀀스 생성
CREATE SEQUENCE **BOARD_SEQ** START WITH 1 INCREMENT BY 1;
INSERT INTO BOARD(DATA) VALUES('A');
  • 데이터베이스 시퀀스 (유일한 값을 순서대로 생성하는 오브젝트) 를 사용해서 기본 키 생성
  • Oracle, PostgreSQL, DB2, H2에서 지원
@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**")
        // 여기에 @SequenceGenerator 를 설정해도 됨
        private long id;
}

@SequenceGenerator

속성 기능 기본값
name Sequence Generator 이름 필수
sequenceName DB에 등록되어 있는 시퀀스 이름 hibernate_sequence
initialValue (DDL) 시퀀스 DDL을 생성할 때 처음 시작하는 수 1
allocationSize 시퀀스 한 번 호출에 증가하는 수 (성능 최적화에 사용) 50
catalog, schema DB catalog, schema 이름  
-- 생성되는 DDL
CREATE SEQUENCE {sequenceName}
start with {initialValue} increment by {allocationSize}
  • SEQUENCE 전략과 최적화
    • SEQUENCE 전략은 DB 시퀀스를 통해 만들어진 식별자를 추가로 조회해야 한다.
    SELECT BOARD_SEQ.NEXTVAL FROM DUAL
    INSERT INTO BOARD ...
    • 시퀀스에 접근하는 횟수를 줄이기 위해서 allocationSize 만큼 미리 시퀀스를 받아올 수 있음
    • INSERT의 성능이 중요하지 않으면 allocationSize를 1로 설정
    • hibernate.id.new_generator_mappings 이 false인 경우 설정방식이 달라지므로 성능 최적화를 하려면 true 로 설정 필요

4.6.4 TABLE 전략

CREATE TABLE **MY_SEQUENCES** {
        sequence_name varchar(255) not null,
        next_val bigint,
        primary key (sequence_name)
}
  • 키 생성 전용 테이블을 만들어 시퀀스를 흉내내는 전략
  • 테이블을 사용하기 때문에 모든 DB에 적용할 수 있다
@Entity
@TableGenerator(
        name = "**BOARD_SEQ_GENERATOR**",
        table = "**MY_SEQUENCES**",
        pkColumnValue = "BOARD_SEQ", alloacationSize = 1
)
public class Board {
        @Id
        @GeneratedValue(strategy = GenerationType.TABLE, generator = "**BOARD_SEQ_GENERATOR**")
        private long id;

        ...
}
  • TABLE 전략은 SEQUENCE 대신에 테이블을 사용한다는 것 외에 동작방식이 같다.
  • 스키마 자동 생성 옵션을 키면 JPA가 table도 생성함
  • row도 JPA 가 알아서 처리해줌

@TableGenerator

속성 기능 기본값
name 식별자 생성기 이름 필수
table 키 생성 테이블명 hibernate_sequences
pkColumnName 시퀀스 컬럼명 sequence_name
valueColumnName 시퀀스 값 컬럼명 next_val
pkColumnValue 키로 사용할 값 이름 {entity 이름}
initialValue 초기 값 0
allocationSize 시퀀스 한 번 호출에 증가하는 수 (성능 최적화에 사용) 50
catalog, schema DB catalog, schema 이름  
uniqueConstraints (DDL) 유니크 제약 조건 지정  
  • MY_SEQUENCES table 예시
{pkColumnName} {valueColumnName}
{pkColumnValue} {initialValue}
  • Table 전략은 조회를 위한 select, 값 증가를 위한 update가 필요
    • DB와 2번 통신
    • allocationSize를 사용해 성능 최적화가 가능

4.6.5 AUTO 전략

@Entity
public class Board {
        @Id
        @GeneratedValue(strategy = GenerationType.AUTO)
        private long id;
        ...
}
  • 선택한 DB의 방언에 따라 IDENTITY, SEQUENCE, TABLE 전략 중 자동으로 선택
  • @GeneratedValue.strategy 의 기본값 → AUTO
  • DB를 변경해도 코드를 수정할 필요가 없음

4.6.6 기본 키 매핑 정리

  • 직접 할당
    • em.persist()를 호출하기 전에 직접 식별자 값을 할당해야 함
    • 식별자 값이 없으면 예외 발생
  • SEQUENCE
    • DB sequence에서 식별자 값을 획득한 후 영속성 컨텍스트에 저장
    • 쓰기 지연 가능
  • TABLE
    • DB sequence 생성용 table에서 식별자 값을 획득한 후 영속성 컨텍스트에 저장
    • 쓰기 지연 가능
  • IDENTITY
    • DB에 entity를 저장하면서 식별자 값을 획득해 영속성 컨텍스트에 저장
    • 쓰기 지연 불가능

권장 식별자 선택 전략

  • Primary key의 필수 조건
    • not null
    • unique
    • Immutable (불변의)
  • 기본 키 선택 전략
    • 자연 키 (natural key)
      • business에 의미가 있는 키
      • ex. 주민등록번호, 이메일, 전화번호
    • 대리키 (surrogate key)
      • business와 관련 없는 임의로 만들어진 키
      • ex. UUID, sequence
    • 자연 키보다 대리 키 권장
      • 현실과 비즈니스 규칙은 생각보다 쉽게 변한다
      • 대리 키는 비즈니스와 무관하므로 요구사항이 변경되어도 기본 키가 변경되지 않을 수 있다
      • 자연 키는 필요에 따라 유니크 인덱스를 설정해 사용하는 것을 권장
더보기

💡 저장된 entity의 기본 키 값은 변경될 수 없다. 변경 시 JPA에서 정상 작동하지 않는다.

 

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

분류 매핑 어노테이션 설명
필드 - 컬럼 매핑 @ Column 컬럼 매핑
필드 - 컬럼 매핑 @Enumerated enum 타입 매핑
필드 - 컬럼 매핑 @Temporal 날짜 타입 배핑
필드 - 컬럼 매핑 @Lob BLOB, CLOB
필드 - 컬럼 매핑 @Transient DB 컬럼과 매핑하지 않을 필드 설정
기타 @Access JPA가 entity에 접근하는 방식 지정

4.7.1 @Column

  • 객체 필드를 테이블 컬럼에 매핑
속성 기능 기본값
name 필드와 매핑할 테이블의 이름 객체의 필드 이름
insertable entity 저장 시 컬럼에 필드 저장 여부  
읽기 전용 필드일 때 false로 설정 true  
updatable entity 수정 시 컬럼에 필드 수정  
읽기 전용 필드일 때 false로 설정 true  
table 하나의 entity를 두 개 이상의 table에 매핑할 때 사용  
지정한 필드를 다른 테이블에 매핑할 수 있다 현재 class가 매핑 된 table  
nullable (DDL) null 값 허용 여부 true
unique (DDL) 한 컬럼에 간단히 unique 제약조건을 걸 때 사용  
columnDefinition (DDL) DB 컬럼 정보 정의 {JPA가 field의 java type과 DB 방언 정보를 사용해 판단한 컬럼 타입}
length (DDL) 문자 길이 제약조건 (String 에서 사용) 255
precision (DDL) 소수점을 포함한 전체 자리수  
(BigDecimal, BigInteger 에서 사용) 19  
scale (DDL) 소수점 아래의 자리수  
(BigDecimal 에서 사용) 2  
더보기

💡 @Column 생략 시

대부분 @Column 속성의 기본값이 적용되나 nullable의 경우 예외가 있음

primitive type은 null 입력이 불가하므로 JPA는 not null로 DDL을 생성함

  • primitive type에 @Column을 명시할 시 nullable = false로 지정하는 것이 안전하다

 

4.7.2 @Enumerated

속성 기능 기본값
value EnumType.ORDINAL : enum 순서를 저장 EnumType.STRING : enum 이름을 저장 EnumType.ORDINAL
  • EnumType.ORDINAL
    • 장점 : DB에 저장되는 데이터 크기가 작다
    • 단점 : 이미 저장된 enum의 순서를 변경할 수 없다
  • EnumType.STRING (권장)
    • 장점 : 저장된 enum의 순서가 바뀌거나 enum이 추가되어도 안전하다
    • 단점 : DB에 저장되는 데이터 크기가 ORDINAL에 비해서 크다

4.7.3 @Temporal

java 8 이후로는 java.util.Date, java.util.Calendar 사용을 권장하지 않음

대신 java.time 패키지 (Instant, LocalDateTime, ZonedDateTime) 사용을 권장하고, JPA에서 @Temporal 없이 자동 매핑이 가능하다.

4.7.4 @Lob

DB의 BLOB (Binary Large OBject), CLOB (Character Large OBject) 타입과 매핑

  • CLOB : String, char[], java.sql.CLOB
  • BLOB : byte[], java.sql.BLOB

4.7.5 @Transient

선언된 필드를 DB 컬럼과 매핑하지 않음. DB에 값을 저장하지도, 조회하지도 않는다

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

더보기

❓ Entity class에 이 어노테이션을 사용하는 것이 맞는가?

  • Entity class는 본질적으로 domain 객체임
  • 비즈니스 로직이나 dto의 역할을 같이 수행하게 될 가능성이 있음

→ 관련 field를 dto로 빼거나, 서비스 계층에서 처리하는 것이 좋은 방식이나 . . .

→ 실무에서 실용적으로 사용하기 위해 사용될 수 있음

ex. DB에 저장된 필드들로 산출되는 값

  • totalPrice = price * quantity
  • fullName = firstName + lastName

 

4.7.6 @Access

JPA가 Entity에 접근하는 방식 지정

  • AccessType.FIELD (필드 접근)
    • 필드에 직접 접근
    • private여도 접근할 수 있다
  • AccessType.PROPERTY (프로퍼티 접근)
    • 접근자(getter)를 사용해 접근
  • @Access의 기본값
    • @Id의 위치를 기준으로 설정됨
    •  
// field에 @Id를 설정한 경우
@Id private String id;

===========================================

// property에 @Id를 설정한 경우
@Id public String getId() { return id; }

===========================================

// 같이 설정하는 경우
// 기본은 FIELD로 접근 
@Id 
private String id; 

@Transient 
private String firstName; 

@Transient 
private String lastName; 

// fullName 만 PROPERTY로 접근 
@Access(AccessType.PROPERTY) 
public String getFullName() { 
    return firstName + lastName; 
} 

// entity 접근 방식을 PROPERTY으로 설정한 경우 setter 필수 
public void setFullName(String fullName) { 
    this.firstName = ...; this.lastName = ...; 
}

 

 

더보기

💡 보통은 FIELD 방식을 사용함

PROPERTY 방식 사용 시,

  • 리플렉션 기반이 아니라 지연로딩이 되지 않고 프록시 초기화 문제가 발생할 수 있음
  • getter, setter 필수 → Immutable 객체 만들기가 어려움

 

'기술 도서 리뷰 > 자바 ORM 표준 JPA 프로그래밍' 카테고리의 다른 글

3장. 영속성 관리  (1) 2025.02.17
2장. JPA 시작  (2) 2025.02.07
1장. JPA 소개  (1) 2025.02.02