Post

기본 어노테이션

기본 어노테이션

1. @Entity


@Entity는 해당 클래스가 JPA가 관리하는 엔티티임을 선언하는 어노테이션이다. 이 어노테이션이 붙어야만 JPA가 이 클래스를 인식하고 DB 테이블과 매핑한다.

📌 반드시 지켜야 할 제약

기본 생성자가 필수다.

JPA는 DB에서 조회한 결과를 엔티티 객체로 변환할 때 내부적으로 리플렉션(Reflection)을 사용한다. 이 과정에서 기본 생성자(인자 없는 생성자)로 객체를 먼저 만들고, 그 다음에 필드에 값을 채워넣는다. 기본 생성자가 없으면 JPA가 객체를 생성할 수 없어 예외가 발생한다. public 또는 protected이어야 한다.

protected를 권장하는 이유는 외부에서 기본 생성자로 엔티티를 만드는 것을 막기 위해서다. 엔티티는 의미 있는 값을 가진 상태로 생성되어야 한다. Lombok의 @NoArgsConstructor(access = AccessLevel.PROTECTED)를 사용하면 편리하다.

1
2
3
4
5
6
7
8
9
10
11
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED) // protected 기본 생성자 자동 생성
public class Member {
    ...
    // 의미 있는 생성자는 별도로 작성
    public Member(String name, int age, String email) {
        this.name = name;
        this.age = age;
        this.email = email;
    }
}

final, enum, interface, inner class에는 @Entity를 붙일 수 없다.

Hibernate가 프록시 객체를 만들기 위해 엔티티를 상속하는데, final 클래스는 상속이 불가능하기 때문이다. 저장할 필드에도 final을 사용하면 안 된다.

💡 name 속성으로 엔티티 이름을 변경할 수 있다. @Entity(name = "Member"). 기본값은 클래스명이다. 엔티티 이름은 JPQL에서 사용되므로 혼동을 피하기 위해 기본값을 그대로 쓰는 것이 좋다.


2. @Table


@Table은 엔티티가 매핑될 테이블을 지정한다. 생략하면 클래스 이름이 테이블 이름으로 사용된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Entity
@Table(
    name = "member",
    schema = "public",
    uniqueConstraints = {
        @UniqueConstraint(
            name = "uk_member_email",
            columnNames = {"email"}
        ),
        @UniqueConstraint(
            name = "uk_member_name_age",
            columnNames = {"name", "age"}
        )
    },
    indexes = {
        @Index(name = "idx_member_name", columnList = "name"),
        @Index(name = "idx_member_created", columnList = "created_at DESC")
    }
)
public class Member extends BaseEntity { ... }

@Column(unique = true)로도 유니크 제약을 줄 수 있지만 이 방식은 제약조건 이름이 자동 생성되어 알아보기 어렵다. @TableuniqueConstraints로 이름을 명시하는 것이 DB 관리 측면에서 훨씬 좋다.


3. @Id와 @GeneratedValue


@Id는 PK 필드를 지정한다. @GeneratedValue는 PK 생성 전략을 설정한다.

📌 전략 4가지 비교

전략동작 방식쓰기 지연적합한 DB
IDENTITYDB의 auto_increment 사용. persist 시 즉시 INSERT불가MySQL, MariaDB
SEQUENCEDB 시퀀스 객체에서 PK 채번 후 INSERT가능Oracle, PostgreSQL
TABLEPK 전용 테이블로 채번. Lock 발생 위험가능모든 DB (비권장)
AUTODB 방언에 따라 자동 선택DB에 따라 다름기본값

IDENTITY 전략 — MySQL 환경에서 가장 많이 사용한다.

1
2
3
4
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "member_id")
private Long id;

💡 IDENTITY 전략은 DB가 INSERT 후 PK를 생성하므로, JPA가 PK를 알려면 INSERT가 실행되어야 한다. 이 때문에 em.persist() 시점에 즉시 INSERT SQL이 실행된다. 쓰기 지연이 적용되지 않는다.

SEQUENCE 전략 — PostgreSQL, Oracle 환경에서 사용한다. 배치 INSERT 시 성능 최적화에 유리하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
@Entity
@SequenceGenerator(
    name = "member_seq_generator",
    sequenceName = "member_seq",
    initialValue = 1,
    allocationSize = 50        // 한 번에 50개씩 채번 (성능 최적화)
)
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE,
                    generator = "member_seq_generator")
    private Long id;
}

allocationSize = 50이 중요한 성능 최적화 포인트다. 시퀀스를 1씩 증가시키면 엔티티를 저장할 때마다 DB에 시퀀스 조회 요청이 발생한다. allocationSize = 50으로 설정하면 한 번의 시퀀스 조회로 50개의 PK를 미리 확보해서 메모리에서 사용한다. DB 왕복을 50분의 1로 줄이는 효과가 있다.


4. @Column


@Column은 필드를 테이블 컬럼에 매핑하는 어노테이션이다. 생략하면 필드명이 컬럼명으로 사용된다.

1
2
3
4
5
6
7
8
9
10
11
12
@Column(
    name = "member_name",
    nullable = false,              // NOT NULL 제약. 기본값: true (nullable)
    length = 50,                   // VARCHAR 길이. 기본값: 255
    unique = false,                // 유니크 제약 (이름 지정 안 됨 → @Table 권장)
    insertable = true,             // INSERT SQL에 포함 여부
    updatable = true,              // UPDATE SQL에 포함 여부
    columnDefinition = "VARCHAR(50) DEFAULT '이름없음'",
    precision = 10,                // 숫자 전체 자릿수 (BigDecimal 등)
    scale = 2                      // 소수점 이하 자릿수
)
private String name;

insertable = false, updatable = false 조합은 읽기 전용 컬럼에 사용한다. 예를 들어 다른 테이블의 컬럼을 읽기만 하는 경우에 활용한다.


5. @Transient


@Transient가 붙은 필드는 DB 컬럼과 매핑되지 않는다. 엔티티에 임시로 값을 보관해야 할 때, 계산된 값을 갖는 필드가 필요할 때 사용한다.

1
2
3
4
5
@Transient
private String tempDisplayName; // DB 저장 안 함. 비즈니스 로직에서만 사용

@Transient
private boolean isSelected;     // 화면 표시용 임시 상태

💡 @Transient 필드는 영속성 컨텍스트 입장에서 아예 존재하지 않는 필드다. 더티 체킹 대상도 아니고, 스냅샷에도 포함되지 않는다.


6. 전체 적용 예시


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@Entity
@Table(
    name = "member",
    uniqueConstraints = {
        @UniqueConstraint(name = "uk_member_email", columnNames = {"email"})
    }
)
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member extends BaseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "member_id")
    private Long id;

    @Column(name = "member_name", nullable = false, length = 50)
    private String name;

    @Column(nullable = false)
    private int age;

    @Column(nullable = false, length = 100)
    private String email;

    @Transient
    private String displayLabel; // 화면 표시용, DB 저장 안 함

    public Member(String name, int age, String email) {
        this.name = name;
        this.age = age;
        this.email = email;
    }

    public void updateName(String name) { this.name = name; }
    public void updateAge(int age) { this.age = age; }
}

참고 자료

This post is licensed under CC BY 4.0 by the author.