JPA 란 무엇인가? -어노테이션-

JPA 관련 어노테이션

ex_1

어느덧 스터디의 두번째 시간이 되었습니다.
이번에도 JPA 관련 된 내용을 말씀드리려고 합니다.

JPA는 DB와 객체를 연결해 주는 기술이므로, 이둘의 관계를 매핑해주는 어노테이션을 지원해 주고 있습니다.
JPA를 잘 사용하기 위해서는 관련 어노테이션을 정확하게 알고 사용할 줄 알아야합니다.

이번 시간에는 JPA에서 쓰이는 어노테이션에 대해서 알려드리려고 합니다.

해당 내용 또한 김영한 님의 자바 ORM 표준 JPA 프로그래밍 - 기본편 동영상 및 강의자료에 도움을 많이 받았습니다.

1. @Table

@Table 어노테이션은 엔티티와 매핑할 테이블을 지정합니다. 바로 속성정리로 가보겠습니다.

속성 기능 기본값
name 매핑할 테이블 이름 엔티티 이름을 사용
catalog 데이터베이스 catalog 매핑  
schema 데이터베이스 schema 매핑  
uniqueConstraints(DDL) DDL 생성 시에 유니크 제약 조건 생성  
@Entity
@Table(name = "NC_USR_ACCOUNT", uniqueConstraints = @UniqueConstraint(columnNames = {"divisionId", "username"}))
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@TableComment(value = "회원 관리 > 계정 관리", base = true)
public class Account extends BaseEntity {

    @Enumerated(EnumType.STRING)
    @ColumnComment("구분")
    private AccountDivision division;

    @Column(nullable = false)
    @ColumnComment("SNS 고유 아이디")
    private String divisionId;

    @Column(nullable = false)
    @ColumnComment("고유 아이디")
    private String username;

//...

2. @Id

@Id 어노테이션을 이용해서 기본 키를 직접 할당 할 수 있지만, 직접 할당 대신에 데이터베이스가 생성해주는 값을
사용할 수도 있습니다.
예를 들어 오라클의 시퀀스 오브젝트나 MySQL의 AUTO_INCREMENT 기능을 사용하고 싶을 때는
어떻게 하면 될까요?

JPA에서 제공해주는 데이터베이스 기본 키 생성 전략을 아래와 같이 있습니다.

직접 할당 : 기본 키를 애플리케이션에서 직접 할당한다.
자동 생성 : 대리 키 사용 방식

IDENTITY : 기본 키 생성을 데이터베이스에 위임한다.
SEQUENCE : 데이터베이스 시퀀스를 사용해서 기본 키를 할당한다.
TABLE : 키 생성 테이블을 사용한다.

1. 직접 할당

기본 키를 직접 할당하려면 아래와 같이 @Id로 매핑하면 됩니다.

@Id
@Column(name = "id")
private Long id;
//...

@Id 어노테이션이 적용 가능한 자바 타입은 다음과 같습니다.

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

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

Board board = new Board();
board.setId("id1"); //기본 키 직접 할당
em.persist(board);

2. IDENTITY 전략

기본 키 생성을 데이터베이스에 위임하는 전략입니다. 주로 MySQL, PostgreSQL, SQL Server, DB2에서 사용합니다.
예를 들어 MySQL의 AUTO_INCREMENT 기능은 데이터베이스가 기본 키를 자동으로 생성해줍니다.

@Entity
public class Board {

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

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

3. SEQUENCE 전략

데이터베이스 시퀀스는 유일한 값을 순서대로 생성하는 특별한 데이터베이스 오브젝트입니다. SEQUENCE 전략은 이 시퀀스를 사용해서 기본 키를 생성합니다. 이 전략은 시퀀스를 지원하는 오라클, PostgreSQL, DB2, H2 데이터베이스에서 사용할 수 있습니다.

@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 속성 정리

속성 기능 기본값
name 식별자 생성기 이름 필수
sequenceName 데이터베이스에 등록되어 있는 시퀀스 이름 hibernate_sequence
initialValue DDL 생성 시에만 사용됨, 시퀀스 DDL을 생성할 때 처음 시작하는 수를 지정한다. 1
allocationSize 시퀀스 한 번 호출에 증가하는 수(성능 최적화에 사용 됨) 50
catalog, schema 데이터베이스 catalog, schema 이름  

@SequenceGenerator는 다음과 같이 @GeneratedValue 옆에 사용해도 됩니다.

@Entity
public class Board {

    @Id
    @GeneratedValue(...)
    @SequenceGenerator(...)
    private Long id;
    //...
}

SEQUENCE 전략은 데이터베이스 시퀀스를 통해 식별자를 조회하는 추가 작업이 필요합니다.
따라서 데이터베이스와 2번 통신합니다.

  1. 식별자를 구하려고 데이터베이스 시퀀스를 조회한다.
    SELECT BOARD_SEQ.NEXTVAL FROM DUAL
  2. 조회한 시퀀스를 기본 키 값으로 사용해 데이터베이스에 저장한다.
    INSERT INTO BOARD…

JPA는 시퀀스에 접근하는 횟수를 줄이기 위해 allocationSize를 사용합니다.
간단히 설명하자면 여기에 설정한 값만큼 한 번에 시퀀스 값을 증가시키고 나서 그만큼 메모리에 시퀀스 값을 할당합니다.

4. TABLE 전략

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

@Entity
@TableGenerator(
    name = "BOARD_SEQ_GENERATOR",
    table = "MY_SEQUENCES",
    pkColumnValue = "BOARD_SEQ", allocationSize = 1
)
public class Board {

    @Id
    @GenerateValue(strategy = GenerationType.TABLE,
                   generator = "BOARD_SEQ_GENERATOR")
    private Long id;
    //...
}

@TableGenerator 속성 정리

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

TABLE 전략은 시퀀스 대신에 테이블을 사용한다는 것만 제외하면 SEQUENCE 전략과 내부 동작방식이 같습니다.

5. AUTO 전략

데이터베이스의 종류도 많고 기본 키를 만드는 방법도 다양합니다. AUTO 전략은 데이터베이스에 따라서
IDENTITY, SEQUENCE, TABLE 전략 중 하나를 자동으로 선택합니다. 예를 들어 오라클을 선택하면 SEQUENCE를, MySQL을 선택하면 IDENTITY를 사용합니다.

@Entity
public class Board {

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

3. @Transient

해당 어노테이션이 사용되면 이 필드는 매핑하지 않습니다. 따라서 데이터베이스에 저장하지 않고 조회하지도 않습니다.
객체에 임시로 어떤 값을 보관하고 싶을 때 사용합니다.

@Transient
private Integer temp;
//...

4. @Access

JPA가 엔티티 데이터에 접근하는 방식을 지정합니다.

  1. 필드접근 : AccessType.FIELD로 지정합니다. 필드에 직접 접근합니다. 필드 접근 권한이 private이어도 접근 할 수 있습니다.
  2. 프로퍼티 접근 : AccessType.PROPERTY로 지정합니다. 접근자(Getter)를 사용합니다.

@Access를 설정하지 않으면 @Id의 위치를 기준으로 접근 방식이 설정됩니다.

@Entity
@Access(AccessType.FIELD)
public class Member {

    @Id
    private Long id;

    private String data1;
    private String data2;
    //...
}

@Id가 필드에 있으므로 Access(AccessType.FIELD)로 설정한 것과 같습니다. 따라서 @Access는 생략해도 됩니다.

@Entity
@Access(AccessType.PROPERTY)
public class Member {
    
    private Long id;

    private String data1;
    private String data2;

    @Id
    public Long getId() {
        return id;
    }

    @Column
    public String getData1() {
        return data1;
    }

    @Column
    public String getData2() {
        return data2;
    }
    //...
}

@Id가 프로퍼티에 있으므로 @Access(AccessType.PROPERTY)로 설정한 것과 같습니다. 따라서 @Access는 생략해도 됩니다.

@Entity
public class Member {
    
    @Id
    private Long id;

    @Transient
    private String firstName;

    @Transient
    private String lastName;

    @Access(AccessType.PROPERTY)
    public String getFullName() {
        return firstName + lastName;
    }
    //...
}

@Id가 필드에 있으므로 기본은 필드 접근 방식을 사용하고 getFullName()만 프로퍼티 접근 방식을 사용합니다.
따라서 회원 엔티티를 저장하면 회원 테이블의 FULLNAME 컬럼에 firstName + lastName의 결과가 저장됩니다.

5. @Inheritance

ex_2

해당 어노테이션은 상속관계 매핑을 하기 위한 어노테이션입니다.
실제 관계형 데이터베이스는 상속 관계가 존재 하지 않는데, 슈퍼타입-서브타입 관계라는 모델링 기법이 객체 상속과 유사합니다.
상속관계 매핑을 통해서 객체의 상속구조와 데이터베이스의 슈퍼타입-서브타입 관계를 매핑합니다.

1. 조인 전략

ex_3

엔티티 각각을 모두 테이블로 만들고 자식 테이블이 부모 테이블의 기본 키를 받아서 기본 키 + 외래 키로
사용하는 전략입니다. 주의할 점은 객체는 타입으로 구분 할 수 있지만 테이블은 타입의 개념이 없으므로
타입을 구분할 수 있는 컬럼을 추가해 주어야 합니다.

@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorCloumn(name = "DTYPE") 
// 부모 클래스에 구분 컬럼을 지정합니다. 이 컬럼으로 저장 된 자식 테이블을 구분 할 수 있습니다.
public abstract class Board {

    @Id
    @GeneratedValue
    @Column(name = "BOARD_ID")
    private Long id;

    private String subject;
    private String content;
    //...
}

@Entity
@DiscriminatorValue("N") // 엔티티를 저장할 때 구분 컬럼에 입력할 값을 지정할 수 있습니다.
public class Notice extends Board {

    private Boolean topFixFlag;
    //...
}

만약 자식 테이블의 기본 키 컬럼명을 변경하고 싶으면 아래와 같이 작성하면 됩니다.

@Entity
@DiscriminatorValue("F")
@PrimaryKeyJoinColumn(name = "FAQ_ID") //ID 컬럼명 재정의
public class Fag extends Board {

    private Long sort;
    //...
}

조인전략의 장단점은 아래와 같습니다.

장점

테이블이 정규화된다.
외래 키 참조 무결성 제약조건을 활용할 수 있다.
저장공간을 효율적으로 사용한다.

단점

조회할 때 조인이 많이 사용되므로 성능이 저하 될 수 있다.
조회 쿼리가 복잡하다.
데이터를 등록할 INSERT SQL을 두번 실행한다.

2. 단일 테이블 전략

ex_4

테이블 하나만을 사용합니다. 그리고 구분컬럼으로 어떤 자식 데이터가 저장되었는지 구분합니다.
조회할 때 조인을 사용하지 않으므로 일반적으로 가장 빠릅니다.
이 전략을 사용할 때 주의할 점은 자식 엔티티가 매핑한 컬럼은 모두 null을 허용해야 한다는 점 입니다.

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorCloumn(name = "DTYPE") 
// 부모 클래스에 구분 컬럼을 지정합니다. 이 컬럼으로 저장 된 자식 테이블을 구분 할 수 있습니다.
public abstract class Board {

    @Id
    @GeneratedValue
    @Column(name = "BOARD_ID")
    private Long id;

    private String subject;
    private String content;
    //...
}

@Entity
@DiscriminatorValue("N") // 엔티티를 저장할 때 구분 컬럼에 입력할 값을 지정할 수 있습니다.
public class Notice extends Board {

    private Boolean topFixFlag;
    //...
}

단일 테이블 전략의 장단점은 아래와 같습니다.

장점

조인이 필요 없으므로 일반적으로 조회 성능이 빠르다.
조회 쿼리가 단순하다.

단점

자식 엔티티가 매핑한 컬럼은 모두 NULL을 허용해야 한다.
단일 테이블에 모든 것을 저장하므로 테이블이 커질 수 있다. 그러므로 상황에 따라서는 조회 성능이 오히려 느려질 수 있다.

3. 구현 클래스마다 테이블 전략

ex_5

해당 전략은 자식 엔티티마다 테이블을 만듭니다. 그리고 자식 테이블 각각에 필요한 컬럼이 모두 있습니다.

@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class Board {

    @Id
    @GeneratedValue
    @Column(name = "BOARD_ID")
    private Long id;

    private String subject;
    private String content;
    //...
}

@Entity
public class Notice extends Board {

    private Boolean topFixFlag;
    //...
}

구현 클래스마다 테이블 전략의 장단점은 아래와 같습니다.

장점

서브 타입을 구분해서 처리할 때 효과적이다.
NOT NULL 제약조건을 사용할 수 있다.

단점

여러 자식 테이블을 함께 조회할 때 성능이 느리다. (SQL에 UNION을 사용해야 한다.)
자식 테이블을 통합해서 쿼리하기 어렵다.

6. @MappedSuperclass

공통 매핑 정보가 필요할 때 사용합니다.
테이블과 관계 없고, 단순히 엔티티가 공통으로 사용하는 매핑 정보를 모으는 역할입니다.
주로 등록일, 수정일, 등록자, 수정자와 같은 전체 엔티티에서 공통으로 적용하는 정보를 모을 때 사용합니다.

@Getter
@MappedSuperclass
public class BaseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @ColumnComment("일련 번호")
    protected Long id;
    //...

테이블과 매핑되지 않고 자식 클래스에 엔티티의 매핑 정보를 상속하기 위해 사용합니다.
@MappedSuperclass로 지정한 클래스는 엔티티가 아니므로 em.find()나 JPQL에서 사용할 수 없습니다.
이 클래스를 직접 생성해서 사용할 일은 거의 없으므로 추상 클래스로 만드는 것을 권장합니다.