4장 엔티티 매핑

이주호·2024년 11월 23일

엔티티와 테이블을 어떻게 매핑하는지 설계에 해당하는 정적인 부분을 알아보자!
JPA를 사용할 때 가장 중요한 부분은 엔티티와 테이블을 정확히 매핑하는 일이다.


@Entity

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

@Entity(name = "엔티티 이름 지정")  // name을 지정하지 않으면 클래스 이름을 그대로 사용한다. ex) Member
public class Member {
//
...
}

@Entity 사용시 주의사항

  • 기본 생성자는 필수다.
    JPA가 엔티티 객체를 생성할 때 기본 생성자를 사용하므로 이 생성자는 반드시 있어야 한다.
  • final 클래스, enum, interface, inner 클래스에는 사용할 수 없다.
  • 저장할 필드에 final을 사용하면 안 된다.

@Table

엔티티와 매핑할 테이블
@Table은 엔티티와 매핑할 테이블을 지정한다. 생략하면 매핑한 엔티티 이름을 테이블 이름으로 사용한다.

import javax.persistence.*;

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

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

엔티티만 만들고 테이블은 자동 생성되도록 데이터베이스 스키마 자동 생성 기능을 알아보자.
JPA는 매핑정보와 데이터베이스 방언을 사용해서 데이터베이스 스키마를 자동으로 생성하는 기능을 지원한다.
해당 기능을 사용하려면 먼저 persistence.xml에 다음 속성을 추가해야 한다.

// 애플리케이션 실행 시점에 DB 테이블 자동 생성
<property name="hibernate.hbm2ddl.auto" value="create" />

// 콘솔에 실행되는 테이블 생성 DDL 출력
<property name="hibernate.show_sql" value="true" />

스키마 자동 생성 기능이 만든 DDL은 운영 환경에서 사용할 만큼 완벽하지는 않으므로 개발 환경에서 사용하거나 매핑을 어떻게 해야하는지 참고하는 정도로만 사용하는 것이 좋다.

hibernate.hbm2ddl.auto 속성

  • create : 기존 테이블을 삭제하고 새로 생성한다. (DROP + CREATE)
  • create-drop : create 속성에 추가로 애플리케이션을 종료할 때 생성한 DDL을 제거한다. (DROP+CREATE+DROP)
  • update : DB 테이블과 엔티티 매핑정보를 비교하여 변경 사항만 수정한다.
  • validate : DB 테이블과 엔티티 매핑정보를 비교해서 차이가 있으면 경고를 남기고 애플리케이션을 실행하지 않는다. 이 설정은 DDL을 수정하지 않는다.

(개발 초기에는 create 또는 update
초기화 상태로 자동화된 테스트를 진행하는 개발자 환경과 CI 서버는 create 또는 create-drop
테스트 서버는 update 또는 validate
스테이징과 운영 서버는 validate

이름 매핑 전략 변경하기

자바 언어는 관례상 카멜표기법을 주로 사용하고, DB는 관례상 언더스코어(_)를 주로 사용한다.

@Column(name = "role_type")
String roleType;

위와 같이 직접적으로 이름을 매핑해도 되지만 hibernate.ejb.naming_strategy 속성을 사용하면 이름 매핑 전략을 변경할 수 있다. 하이버네이트는 org.hibernate.cfg.ImprovedNamingStrategy 클래스를 제공한다. 이 클래스는 테이블 명이나 컬럼 명이 생략되면 자바의 카멜 표기법을 테이블의 언더스코어 표기법으로 매핑한다.

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

DDL 생성 기능

그 전에 앞서 설명 안 하고 넘겼던 몇 가지 어노테이션을 소개하겠다.
@Id : 기본 키 매핑
@Column : 필드와 컬럼 매핑
@Enumerated : 자바의 enum을 사용하기 위해 쓰는 어노테이션
@Temporal : 자바의 날짜 타입을 사용하기 위해 쓰는 어노테이션
@Lob : 데이터베이스의 VARCHAR 타입 대신에 CLOB, BLOB 타입을 매핑하기 위해 쓰는 어노테이션

@Entity
@Table(name="MEMBER2")
public class Member2 {

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

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

스키마 자동 생성하기를 통해 만들어지는 DDL에 nullable과 length와 같은 속성을 이용하여 제약조건을 추가할 수 있다. 위에 예제는 username 필드를 NAME 컬럼과 매핑하고 NOT NULL, 10자 제한을 건 모습이다.
실행되는 DDL은 아래와 같다.

create table MEMBER2 
	ID varchar(255) not null,
    NAME varchar(10) not null,
    ...
    primary key (ID)
)

유니크 제약조건

@Entity
@Table(name="MEMBER2", uniqueConstraints = {@UniqueConstraint(
    name = "NAME_AGE_UNIQUE",
    columnNames = {"NAME", "AGE"})})
public class Member2 {

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

    @Column(name = "name")
    private String username;

    private Integer age;
    ...
}

위와 같이 @UniqueConstraint 을 이용해서 유니크 제약조건을 만들 수 있다. 애플리케이션을 실행하면 스키마 자동생성 기능으로 다음과 같은 DDL이 실행된다.

ALTER TABLE MEMBER
	ADD CONSTRAINT NAME_AGE_UNIQUE UNIQUE (NAME, AGE)    

이런 기능들은 단지 DDL을 자동 생성할 때만 사용되고 JPA의 실행 로직에는 영향을 주지 않는다.
이 부분을 좀 더 설명하자면 해당 어노테이션들이 JPA가 엔티티를 관리하고 동작하는 방식(더티체킹, 동일성 보장 등)에는 영향을 미치지 않는다는 의미이다.
따라서 직접 DDL을 만들어 스키마 자동생성 기능을 사용하지 않는다면 사용할 이유가 없지만 해당 기능을 명시하면 개발자가 엔티티만 보고도 손쉽게 다양한 제약 조건을 파악할 수 있다는 장점이 있다.


기본 키(Primary Key) 매핑

지금까지 @Id 만을 사용하여 애플리케이션에서 기본 키를 직접 할당했다. 여기서는 데이터베이스에서 생성해주는 값을 기본 키로 사용하는 방법을 배우자.

JPA에서 기본 키를 할당하는 전략은 크게 아래 두 가지이다.

  • 직접 할당 : 애플리케이션에서 기본 키를 직접 할당한다.
  • 자동 생성 : 대리키 사용 방식
    IDENTITY : 기본 키 생성을 데이터베이스에 위임한다. (ex. MySQL)
    SEQUENCE : 데이터베이스 시퀀스를 사용해서 기본 키를 할당한다. (ex. 오라클)
    TABLE : 키 생성 테이블을 사용한다. 키 생성용 테이블을 만들어두고 마치 시퀀스처럼 사용하는 방법. 모든 데이터베이스에서 사용할 수 있다.

JPA에서 위처럼 자동 생성 전략이 다양한 이유는 데이터베이스 벤더마다 지원하는 방식이 다르기 때문이다.
기본 키를 직접 할당하려면 @Id 만 사용하면 되고 자동 생성 전략을 사용하려면 @GeneratedValue를 사용하면 된다.

// 자동 키 생성 전략을 사용하기 위해서는 persistence.xml에 아래 속성을 추가해야 한다.
<property name="hibernate.id.new_generator_mappings" value="true" />

기본 키 직접 할당 전략

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

@Id 적용 가능 자바 타입은 다음과 같다.

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

기본 키 직접 할당 전략은 em.persist()로 엔티티를 저장하기 전에 직접적으로 애플리케이션에서 기본 키를 직접 할당하는 방법이다.

Student st = new Student();
st.setId("id")	// 기본 키 직접 할당
em.persist(st);

기본 키로 객체를 사용할 수 있을까?

그런데 그러면 기본 키로 위와 같은 타입만 사용하면 객체를 기본 키로 사용할 수 없는거야?

위의 내용을 보다가 객체를 기본 키로 사용할 수는 없나 하는 궁금증이 생겼다.
조금 조사해보니 객체 자체를 기본 키로 사용할 수는 있지만 위 처럼 @Id를 사용하는 것으로는 안 된다고 한다. 객체 자체를 기본 키로 사용하는 경우 기본 키를 복합키로 설정해야 한다고 하며 @IdClass 나 @EmbeddedId 를 사용해야 한다고 한다. 아래는 해당내용을 좀 더 자세히 서술한 부분인데 7장 고급 매핑 - 7.3 복합 키와 식별 관계 매핑에서 다룰 것으로 보인다. 지금은 '이런 것도 있구나' 정도로 넘어가자.


1. 기본 키로 객체를 사용할 수 없는 이유

  • @Id는 기본 키를 지정하는 어노테이션이지만, 단순 값 타입(String, Long, Integer 등)이나 임베디드 타입만 기본 키로 직접 사용할 수 있습니다.
  • 객체 타입(Student)를 직접 기본 키로 설정하려면, 해당 객체가 복합 키로 간주되어야 하고, JPA에서 복합 키를 처리하기 위해 @EmbeddedId 또는 @IdClass를 사용해야 합니다.

2. 복합 키를 사용하는 방법

(1) @EmbeddedId 사용

@EmbeddedId를 사용하면 객체 자체를 기본 키로 사용할 수 있습니다.
이 방식에서는 기본 키 객체를 별도로 정의하고, 엔티티에서 이를 포함하도록 합니다.

예제:
@Embeddable
public class StudentId {
    private Long studentId;
    private String schoolId;

    // equals()와 hashCode() 반드시 구현
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        StudentId that = (StudentId) o;
        return Objects.equals(studentId, that.studentId) &&
               Objects.equals(schoolId, that.schoolId);
    }

    @Override
    public int hashCode() {
        return Objects.hash(studentId, schoolId);
    }

    // getter, setter
}
@Entity
public class Student {
    @EmbeddedId
    private StudentId id; // 객체를 기본 키로 사용

    private String name;

    // getter, setter
}

(2) @IdClass 사용

@IdClass복합 키를 정의하는 또 다른 방식입니다.
이 방식에서는 기본 키 클래스를 별도로 정의하고, 엔티티에서 해당 키의 구성 요소를 명시합니다.

예제:
// 복합 키 클래스
public class StudentId implements Serializable {
    private Long studentId;
    private String schoolId;

    // equals()와 hashCode() 반드시 구현
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        StudentId that = (StudentId) o;
        return Objects.equals(studentId, that.studentId) &&
               Objects.equals(schoolId, that.schoolId);
    }

    @Override
    public int hashCode() {
        return Objects.hash(studentId, schoolId);
    }

    // getter, setter
}
@Entity
@IdClass(StudentId.class)
public class Student {
    @Id
    private Long studentId;

    @Id
    private String schoolId;

    private String name;

    // getter, setter
}

3. 차이점 비교

특징@EmbeddedId@IdClass
구현 방식기본 키를 하나의 임베디드 객체로 관리기본 키를 개별 필드로 선언
클래스 선언 위치엔티티에서 기본 키로 사용할 객체를 필드로 선언엔티티 외부에 별도의 기본 키 클래스를 정의
코드 가독성하나의 필드로 관리 가능해 간결개별 필드를 명시해야 하므로 약간 복잡
equals/hashCode 구현 여부필수필수

4. 질문으로 돌아와서

코드:

@Id
@Column(name = "id")
private Student student;

위 코드는 JPA에서 에러가 발생합니다. 이유는:
1. @Id가 객체(Student) 타입에 직접 적용되었기 때문에, JPA는 이를 기본 키로 간주할 수 없습니다.
2. 기본 키로 객체를 사용하려면 @EmbeddedId 또는 @IdClass를 사용해야 합니다.


5. 결론

  • JPA에서 기본 키로 객체를 사용하려면 복합 키로 처리해야 하며, @EmbeddedId 또는 @IdClass를 사용해야 합니다.
  • 단순히 @Id를 객체 타입에 붙이는 것은 JPA 실행 시점에서 예외를 발생시킵니다.
  • 위의 방식 중 코드 가독성유지보수성을 고려해 적합한 방법을 선택하세요.
    일반적으로, @EmbeddedId 방식이 더 깔끔하고 객체 지향적인 설계를 선호할 때 사용됩니다.

IDENTITY 전략

기본 키 생성을 데이터베이스에 위임하는 전략.
MySQL, PostgreSQL, SQL Server, DB2에서 주로 사용하는 방법이라고 한다. MySQL의 AUTO_INCREMENT 기능은 많이 봤을 것이라고 생각든다.

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

IDENTITY 전략은 데이터베이스에 값을 저장하고 나서야 기본 키 값을 구할 수 있을 때 사용한다.
IDENTITY 전략을 사용하려면 @GeneratedValue의 strategy 속성 값을 GenerationType.IDENTITY로 지정하면 된다. 이 전략을 사용하면 JPA는 기본 키 값을 얻어오기 위해 데이터베이스를 추가로 조회한다.

@Entity
public class Board {

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

// IDENTITY 사용코드
  private static void logic(EntityManager em) {
        Board board = new Board();
        em.persist(board);
        System.out.println("board.getId() = " + board.getId());  //board.getId() = 1
    }

IDENTITY 전략 동작 원리 더 살펴보기

em.persist()를 호출하면 원래는 쓰기 지연 SQL 저장소에 insert 쿼리를 모아두고 트랜잭션을 커밋하면, 먼저 flush해서 DB와 통신하는 거였잖아? 여기서는 em.persist()를 호출하면 바로 DB와 통신하는 것 같은데?

1. IDENTITY 전략의 기본 동작

IDENTITY 전략은 데이터베이스가 기본 키 값을 생성하는 방식입니다. 보통 AUTO_INCREMENT 속성을 사용하는 데이터베이스에서 활용됩니다.
이 방식에서는 JPA가 기본 키 값을 데이터베이스에서 가져와야 엔티티 객체에 할당할 수 있습니다.


2. em.persist() 호출 시 동작

일반적인 em.persist() 흐름:

  • @GeneratedValue(strategy = GenerationType.IDENTITY)가 아닌 경우:

    • JPA는 엔티티를 영속성 컨텍스트에 저장하지만, 쓰기 지연 메커니즘에 따라 트랜잭션이 커밋될 때 DB에 INSERT 쿼리를 보냅니다.
    • 이때 ID 값은 JPA가 미리 생성(예: SEQUENCE 전략)하거나, DB의 INSERT 쿼리가 끝난 후 가져옵니다.
  • IDENTITY 전략인 경우:

    • em.persist() 호출 시, 즉시 INSERT 쿼리를 실행하고 데이터베이스와 통신하여 ID 값을 조회합니다.
    • 쓰기 지연이 아닌 즉시 플러시가 이루어지는 이유는, 기본 키 값이 엔티티에 바로 할당되어야 하기 때문입니다.

이유:

JPA는 엔티티의 식별자(기본 키)가 필요해야 다른 동작(연관 관계 설정, 영속성 컨텍스트 관리 등)을 수행할 수 있습니다.
IDENTITY 전략은 DB에서 기본 키 값을 생성하므로, 영속성 컨텍스트에 엔티티를 저장하기 전에 DB와 통신이 필수적입니다.


3. 언제 데이터베이스와 통신하나?

em.persist() 호출 시점:

  1. IDENTITY 전략을 사용할 때는 em.persist()를 호출하는 순간, JPA는 바로 DB에 INSERT 쿼리를 실행합니다.
    • 이 과정에서 데이터베이스에 행이 삽입되고, 기본 키 값이 생성됩니다.
    • 생성된 기본 키 값을 JPA가 가져와 엔티티의 식별자 필드에 할당합니다.

4. 정리: IDENTITY 전략의 특징

  • 쓰기 지연 SQL 저장소:

    • 일반적으로 JPA는 쓰기 지연을 통해 SQL 쿼리를 한 번에 실행하지만, IDENTITY 전략은 즉시 플러시를 수행합니다.
    • 즉, em.persist() 시점에 INSERT 쿼리가 실행되고, DB에서 생성된 ID 값을 가져옵니다.
  • 데이터베이스와의 통신 시점:

    • em.persist()를 호출하면 JPA는 바로 DB와 통신해 INSERT를 수행하고 ID 값을 가져옵니다.
    • 이후 getId()를 호출하면 이미 가져온 ID 값을 반환하므로 추가적인 DB 통신은 발생하지 않습니다.

5. 왜 IDENTITY 전략은 바로 플러시가 필요한가?

다른 전략(SEQUENCE, TABLE, AUTO)은 JPA가 직접 키를 관리하거나 미리 생성할 수 있지만,
IDENTITY 전략은 DB에서 기본 키를 생성하므로, DB와 통신 없이는 키 값을 얻을 수 없습니다.
이 때문에 em.persist() 단계에서 바로 플러시를 실행해 DB와 통신합니다.


6. 결론

  1. IDENTITY 전략을 사용하면 em.persist() 시점에 즉시 플러시가 이루어집니다.
    • INSERT 쿼리가 실행되고, DB에서 생성된 ID 값을 바로 조회합니다.
    • 쓰기 지연 저장소에 쌓아두지 않습니다.
  2. ID 값을 사용하는 시점에서 추가적인 DB 통신은 발생하지 않습니다.
    • 이미 ID 값을 영속성 컨텍스트에서 가져왔기 때문입니다.
  3. persist() 이후 아직 flush()되지 않은 다른 엔티티들은 여전히 쓰기 지연 저장소에 쌓여 있다가 트랜잭션 종료 시점에 처리됩니다.

SEQUENCE 전략

데이터베이스 시퀀스는 유일한 값을 순서대로 생성하는 특별한 데이터베이스 오브젝트다. SEQUENCE 전략은 이 시퀀스를 사용해서 기본 키를 생성한다. 이 전략은 시퀀스를 지원하는 오라클, PostgreSQL, 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"과 매핑한다.
@GeneratedValue의 속성의 strategy를 GenerationType.SEQUENCE로 지정한 후 generator 속성으로 방금 등록한 "BOARD_SEQ_GENERATOR" 시퀀스 생성기를 선택한다. 이 후에 id의 식별자 값은 BOARD_SEQ_GENERATOR 시퀀스 생성기가 할당한다.

SEQUENCE 전략 동작 원리

SEQUENCE 전략은 em.persit()를 호출하면 먼저 데이터베이스 시퀀스를 사용해서 식별자를 조회한다. 조회한 식별자를 엔티티에 할당한 후 엔티티를 영속성 컨텍스트에 저장한다. 이 후 트랜잭션을 커밋할 때 플러시가 실행되어 엔티티를 데이터베이스에 저장한다.


TABLE 전략

TABLE 전략은 키 생성 전용 테이블을 만들어 이름과 값으로 사용할 컬럼을 만들어 마치 시퀀스처럼 사용하는 전략이다. 모든 데이터베이스 적용할 수 있다.


// TABLE 전략 키 생성 DDL
create table MY_SEQUENCES (
	sequence_name varchar(255) not null,
    next_val bigint,
    primary key (sequence_name)
 )

sequence_name 컬럼을 시퀀스 이름으로 사용하고 next_val 컬럼을 시퀀스 값으로 사용한다. 컬럼 값은 변경할 수 있는데 위의 값이 기본 값이다.

// 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;
    ...
 }   

@TableGenerator를 사용해서 BOARD_SEQ_GENERATOR라는 이름의 테이블 키 생성기를 등록했다. 이 후 table 속성에 방금 생성한 키 생성용 테이블(MY_SEQUENCES)을 매핑했다. 필드에는 @GeneratedValue(strategy = GenerationType.TABLE)을 지저하고 generator 속성에 등록한 테이블 키 생성기를 입력한다. 이 후 id 식별자 값은 BOARD_SEQ_GENERATOR 테이블 키 생성기가 할당한다.
MY_SEQUENCES 테이블에는 @TableGenerator에서 pkColumnValue 속성으로 지정한 BOARD_SEQ 라는 이름으로 레코드가 생성된다.

sequence_namenext_value
BOARD_SEQ2

AUTO 전략

GenereationType.AUTO
선택한 데이터베이스 방언에 따라 자동으로 IDENTITY, SEQUENCE, TABLE을 사용하도록 설정하는 방법이다.
기본적으로 @GeneratedValue strategy 속성 값의 기본 값은 AUTO이므로 다음과 같이 사용해도 된다.

@Id @GeneratedValue  //@GeneratedValue(strategy = GenerationType.AUTO) 와 동일

데이터베이스를 수정해도 코드를 수정할 필요가 없다는 점이 장점이다. SEQUENE나 TABLE 전략을 사용되기 위해서는 시퀀스나 키 생성용 테이블을 미리 만들어 둬야 하는데, 만약 스키마 자동 생성 기능을 사용한다면 하이버네이트가 기본 값을 사용해서 적절한 시퀀스나 키 생성용 테이블을 만들어 줄 것이다.

권장하는 식별자 전략

데이터베이스의 기본 키는 다음과 같은 규칙을 반드시 지켜야 한다.

  • null 값을 허용해서는 안 된다.
  • 유일한 값이어야 한다.
  • 변해선 안 된다.

테이블의 기본 키 규칙은 크게 자연 키(natural key)대리 키(surrogate key)가 있다.
자연 키는 비즈니스적으로 의미있는 키로 주민등록 번호, 이메일 등이 있다.
대리 키는 비즈니스와 관련이 없는 임의의 키를 뜻하며 오라클 시퀀스나 auto_increment 등으로 생성하는 키를 의미한다. 대체키라고도 불린다.

비지니스는 언제든 바뀔 수 있으므로 대리 키를 권장한다.


필드와 컬럼 매핑

앞에서 간단하게만 설명하고 넘어갔던 필드-컬럼 매핑 어노테이션에 대해 설명하겠다. 모든 부분을 세세히 설명하는게 아니라 중요하다고 생각 드는 부분만 정리할 예정이다.

@Column

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

@Column을 생략하면 자바 기본 타입일 때 nullable 속성에 예외가 있다.

int data1;
data1 integer not null	// 생성된 DDL

Integer data2;
data2 integer	// 생성된 DDL

@Column
int data3;
data3 integer	// 생성된 DDL

위와 같이 @Column을 생략하면 자바의 기본 형은 null 허용이 안 되므로 not null이 붙는다. 기본형 타입의 @Column을 사용할거면 nullable속성을 false로 지정해야 함을 주의하자.

@Enumerated

  • 자바의 enum 타입을 매핑할 때 사용한다.

EnumType.ORDINAL : enum의 순서를 데이터베이스에 저장
장점 : 데이터 베이스의 enum에 생성한 순서에 맞게 0,1 로 들어가 저장되는 데이터 크기가 작다.
단점 : 이미 저장된 enum 순서를 바꿀 수 없다.

EnumType.STRING : enum의 이름을 데이터베이스에 저장
장점 : 저장된 enum의 순서를 바꾸거나 enum을 추가해도 안전하다.
단점 : 데이터베이스의 저장되는 데이터 크기가 ORDINAL에 비해 크다.

enum RoleType {
	ADMIN, MEMBER
}

//
@Enumerated(EnumType.STRING)
private RoleType roleType;

ORDINAL은 ADMIN은 0으로 MEMBER는 1로 저장하는 반면, STRING은 ADMIN, MEMBER 그대로 저장한다.
(enum이 추가되거나 순서가 바뀔 수도 있으므로 STRING을 권장)

@Temporal

  • 날짜 타입을 매핑할 때 사용한다.
    @Temporal(TemporalType.DATE)
    private Date date;	// 날짜
    
    @Temporal(TemporalType.TIME)
    private Date time;	// 시간
    
    @Temporal(TemporalType.TIMESTAMP)
    private Date timestamp;	//날짜와 시간

자바에는 Date가 있지만 데이터베이스에는 date, time, timestamp라는 세 가지 타입이 존재한다.
TemporalType을 설정하지 않으면 자바의 Date와 가장 유사한 timestamp가 지정된다. MySQL같은 경우에는 datetime을 이용하는데 데이터베이스 방언덕분에 코드를 수정하지 않아도 된다.

@Lob

  • 데이터베이스 BLOB, CLOB 타입과 매핑한다.

@Transient

  • 데이터베이스와 매핑하지 않을 필드에 사용한다.
    데이터베이스에 저장/조회를 하지 않기 때문에 객체에 임시로 어떤 값을 담고 싶을 때 사용한다.

@Access

  • JPA가 엔티티에 접근하는 방식을 설정한다.

필드 접근 : AccessType.FIELD로 지정하며 필드가 private이어도 접근할 수 있다. 필드에 @Id가 붙어있으면 @Access(AccessType.FIELD)로 지정한 것과 같아 생략할 수 있다.

프로퍼티 접근 : AccessType.PROPERTY로 지정하며 접근자(getter)를 사용한다. 다시 설명하면 getter 메서드를 이용해서 엔티티에 접근하는 것을 뜻한다.

아래는 해당 내용을 조금 더 찾아본 결과이다.


1. 필드와 프로퍼티의 차이

필드 기반 접근(Field Access)

  • 설명: 엔티티 클래스의 필드(멤버 변수)에 직접 @Id와 같은 JPA 어노테이션을 붙이는 방식.
  • 특징:
    • JPA는 필드에 직접 접근하여 데이터를 읽고 씁니다.
    • 필드에 붙은 어노테이션을 기준으로 매핑을 처리합니다.
    • 필드의 getter, setter는 무시됩니다.
예제:
@Entity
public class Member {
    @Id // 필드에 직접 어노테이션 부착
    private Long id;

    private String name;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

프로퍼티 기반 접근(Property Access)

  • 설명: 엔티티 클래스의 getter 메서드 또는 setter 메서드에 JPA 어노테이션을 붙이는 방식.
  • 특징:
    • JPA는 getter 메서드를 통해 데이터를 읽고, setter 메서드를 통해 데이터를 씁니다.
    • 어노테이션은 필드가 아니라 메서드에 적용됩니다.
    • 필드는 private이어도 상관없습니다.
예제:
@Entity
public class Member {
    private Long id;

    private String name;

    @Id // getter 메서드에 어노테이션 부착
    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

2. JPA에서 필드와 프로퍼티가 다른 이유

JPA의 동작 방식

  • JPA는 엔티티의 데이터에 접근할 때, 어노테이션이 어디에 붙었는지를 기준으로 필드 기반인지 프로퍼티 기반인지 결정합니다.
  • 필드 기반 접근:
    • 데이터에 직접 접근 (field.setAccessible(true)를 사용하여 private 필드도 접근 가능).
  • 프로퍼티 기반 접근:
    • 데이터에 getter/setter 메서드를 통해 접근.

3. 필드 vs. 프로퍼티 비교

구분필드(Field) 접근프로퍼티(Property) 접근
어노테이션 위치필드에 직접 붙임 (@Id, @Column)getter/setter 메서드에 붙임
데이터 접근 방식필드에 직접 접근getter/setter를 통해 접근
캡슐화필드에 직접 접근하므로 캡슐화 약함getter/setter 사용으로 캡슐화 유지
JPA의 기본 동작필드에 어노테이션이 있으면 필드 접근getter에 어노테이션이 있으면 프로퍼티 접근
장점간단하고 명료캡슐화가 유지됨
단점캡슐화가 깨질 수 있음코드가 길어질 수 있음

4. 실제 사용 시 주의점

  1. 필드와 프로퍼티를 혼용하지 말 것:
    • JPA는 엔티티 클래스에서 필드 기반 또는 프로퍼티 기반 중 하나만 선택해서 사용합니다.
    • 같은 클래스에서 필드와 프로퍼티 접근 방식을 혼용하면 예기치 않은 동작이 발생할 수 있습니다.
  2. 필드 기반 접근을 권장:
    • JPA 구현체는 필드 기반 접근을 더 효율적으로 처리할 수 있습니다.
    • getter/setter를 사용하지 않으므로 코드가 간결해집니다.

5. 결론

  • 필드(field): 클래스의 멤버 변수.
  • 프로퍼티(property): 클래스의 getter/setter 메서드.
  • JPA에서 @Id를 어디에 붙이는지에 따라 JPA의 데이터 접근 방식이 달라집니다:
    • 필드에 붙이면 필드 기반 접근.
    • getter에 붙이면 프로퍼티 기반 접근.
profile
코드 위에서 춤추고 싶어요

0개의 댓글