JPA - 기본 키 매핑 전략(@Id)

2021. 4. 15. 13:46 Spring Data/Spring Data JPA

JPA에서 기본 키 매핑 전략에는 크게 4가지가 있다.

 

1)직접 할당 : 기본 키를 애플리케이션에서 직접 엔티티클래스의 @Id 필드에 set해준다.

2)자동 생성 : 대리 키 사용 방식

- IDENTITY : 기본 키 생성을 데이터베이스에 위임한다.(ex MySQL - AUTO INCREMENT...)

- SEQUENCE : 데이터베이스 시퀀스를 사용해서 기본 키를 할당한다.(ex Oracle sequence...)

- TABLE : 키 생성 테이블을 사용한다.(ex 시퀀스용 테이블을 생성해서 테이블의 기본키를 저장하고 관리한다.)

 

자동 생성 전략이 이렇게 다양한 이유는 데이터베이스 벤더마다 지원하는 방식이 다르기 때문이다. 위 중에서 IDENTITY와 SEQUENCE는 데이터베이스 벤더에 의존적이다. 하지만 TABLE 전략은 키 생성용 테이블을 하나 만들어두고 마치 시퀀스처럼 사용하는 방법이기에 벤더에 의존하지 않는다.(하지만 각각 장단점이 존재함)

 

*키 생성 전략을 사용하려면 persistence.xml 혹은 application.properties(이것은 구글링 해봐야 할 듯)에 hibernate.id.new_generator_mappings = true 설정을 해주어야한다.

 

IDENTITY 전략

IDENTITY는 기본 키 생성을 데이터베이스에 위임하는 전략이다. 주로 MySQL,PostgreSQL,SQL Server,DB2에서 사용한다

/*
 * 유니크키 설정 및 nullable,length 등의 속성은 모두 auto DDL을 사용했을 때만 유효한 설정이다.
 * 즉, 테이블을 직접 생성한다면 적용되지 않는다. 하지만 테이블과 객체간의 관계표현에 있어 해당 설정들을 해놓으면
 * 엔티티 클래스만 봐도 테이블의 구조가 파악되기에 가독성을 위해서라도 설정을 해놓는 것이 좋다.
 */
@Entity
@Table(name = "MEMBER"
        ,uniqueConstraints = {
        @UniqueConstraint(
                name = "NAME_AGE_UNIQUE",
                columnNames = {"NAME","AGE"} //uniqueConstraints는 auto DDL 속성을 사용할때만 유효한 설정이다.
        )
})
@Getter
@Setter
@ToString
public class Member {
    @Id
    @Column(name = "ID")
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private Long id;
 
    /*
     * not null
     * varchar2(10) -> 기본값 255;
     */
    @Column(name = "NAME",nullable=false,length=10)
    private String username;
 
    private Integer age;
 
    /*
     * EnumType의 기본값 설정은 정수이다.
     */
    @Enumerated(EnumType.STRING)
    @Column(name="ROLE_TYPE",nullable=false,length=20)
    private RoleType roleType;
 
    @Temporal(TemporalType.TIMESTAMP)
    private Date createdDate;
 
    @Temporal(TemporalType.TIMESTAMP)
    private Date lastModifiedDate;
 
    @Lob
    private String description;
}

 

IDENTITY 전략의 단점이라고   있는 특징이 있다. 그것은 데이터베이스에 값을 저장하고 나서야 기본  값을 구할  있다. 여기서 기본  

값을 구하는 것이 무슨 상관이냐? 라고 말할  있지만 영속성 컨텍스트에 1차캐시(엔티티의 영속상태화) 하기 위해서는 구분자로 기본키(@Id) 필드를 이용한다. , 영속성

컨텍스트에 캐싱을 하기위한 primary key 값을 가져오기 위하여 테이블을 추가로 조회하게 된다. 그래서 다른 전략과는 다른 행동중 하나가 persist()호출을

하자마자 지연쓰기를 하는 것이 아니라, primary key값을 가져오기 위하여 바로 flush 호출하게 된다.

 

 

SEQUENCE 전략

 

데이터베이스의 시퀀스 오브젝트를 이용하여 유일한 값을 순서대로 생성한다.  전략은 주로 Oracle,PostgreSQL,DB2,H2 등의 데이터베이스에서 사용할  있다.

/*
 * 유니크키 설정 및 nullable,length 등의 속성은 모두 auto DDL을 사용했을 때만 유효한 설정이다.
 * 즉, 테이블을 직접 생성한다면 적용되지 않는다. 하지만 테이블과 객체간의 관계표현에 있어 해당 설정들을 해놓으면
 * 엔티티 클래스만 봐도 테이블의 구조가 파악되기에 가독성을 위해서라도 설정을 해놓는 것이 좋다.
 */
/*
 * sequence table
 * CREATE TABLE MY_SEQUENCE(
 *    sequence_name varchar2(255) PRIMARY KEY,
 *    next_val number(22,0)
 * )
 */
@Entity
@SequenceGenerator(
        name="BOARD_SEQ_GENERATOR",
        sequenceName="BOARD_SEQ",
        initialValue=1,allocationsSize=1
)
@Table(name = "MEMBER"
        ,uniqueConstraints = {
        @UniqueConstraint(
                name = "NAME_AGE_UNIQUE",
                columnNames = {"NAME","AGE"} //uniqueConstraints는 auto DDL 속성을 사용할때만 유효한 설정이다.
        )
})
@Getter
@Setter
@ToString
public class Member {
    @Id
    @Column(name = "ID")
    @GeneratedValue(strategy=GenerationType.SEQUENCE
            ,generator="BOARD_SEQ_GENERATOR"
    )
    private Long id;
 
    /*
     * not null
     * varchar2(10) -> 기본값 255;
     */
    @Column(name = "NAME",nullable=false,length=10)
    private String username;
 
    private Integer age;
 
    /*
     * EnumType의 기본값 설정은 정수이다.
     */
    @Enumerated(EnumType.STRING)
    @Column(name="ROLE_TYPE",nullable=false,length=20)
    private RoleType roleType;
 
    @Temporal(TemporalType.TIMESTAMP)
    private Date createdDate;
 
    @Temporal(TemporalType.TIMESTAMP)
    private Date lastModifiedDate;
 
    @Lob
    private String description;
}

 

IDENTITY와는 다른 설정이 있다면 시퀀스생성기 어노테이션이다. 여기서 name속성은 실제 @Id필드에서 참조할 이름이라고 생각하면 되고, sequenceName 

실제 데이터베이스에 생성되는 시퀀스 오브젝트 이름이다. 그리고 시퀀스 초기값과 allocationSize라는 속성이 있다. 여기서 allocationSize 실제 데이터베이스에서

가져오는 시퀀스의 한번 호출에 증가하는 값의 크기이다. 이것은 성능 최적화와 관련된 속성이므로 마지막에 따로 설명한다.

SEQUENCE 전략은 em.persist() 호출할  먼저 데이터베이스 시퀀스를 사용해서 식별자를 조회한다.(실제 엔티티에 할당할 primary key) 그리고 조회한 식별자를

엔티티에 할당한 후에 엔티티를 영속성 컨텍스트에 저장한다. 이후 커밋이 일어나게 되면 실제 데이터베이스에 INSERT되게 된다.

 

TABLE 전략

 

TABLE 전략은  생성 전용 테이블을 하나 만들고 여기에 이름과 값으로 사용할 컬럼을 만들어 데이터베이스 시퀀스를 흉내내는 전략이다. 이것은 벤더에 의존적이지 않은 전략이다.

 전략을 사용할  auto DDL설정을 했다면 상관없지만 나중에 프로덕환경에서의 데이터베이스 설계에서  시퀀스 테이블에 생성이 선행되어야한다.

 

CREATE TABLE APP_SEQUENCE(

sequence_name varchar2(255) primary key,

next_val number(22,0)

)

 

/*
 * 유니크키 설정 및 nullable,length 등의 속성은 모두 auto DDL을 사용했을 때만 유효한 설정이다.
 * 즉, 테이블을 직접 생성한다면 적용되지 않는다. 하지만 테이블과 객체간의 관계표현에 있어 해당 설정들을 해놓으면
 * 엔티티 클래스만 봐도 테이블의 구조가 파악되기에 가독성을 위해서라도 설정을 해놓는 것이 좋다.
 */
/*
 * sequence table
 * CREATE TABLE MY_SEQUENCE(
 *    sequence_name varchar2(255) PRIMARY KEY,
 *    next_val number(22,0)
 * )
 */
@Entity
@Table(name = "MEMBER"
        ,uniqueConstraints = {
        @UniqueConstraint(
                name = "NAME_AGE_UNIQUE",
                columnNames = {"NAME","AGE"} //uniqueConstraints는 auto DDL 속성을 사용할때만 유효한 설정이다.
        )
})
public class Member {
    @Id
    @Column(name = "ID")
    @GeneratedValue(strategy=GenerationType.TABLE, generator = "MEMBER_SEQ_GENERATOR")
    @TableGenerator(
            name="MEMBER_SEQ_GENERATOR",
            table="MY_SEQUENCE", //시퀀스 생성용 테이블 이름
            pkColumnName="sequence_name", //MY_SEQUENCE 테이블에 생성할 필드이름(시퀀스네임)
            pkColumnValue="MEMBER_SEQ", //SEQ_NAME이라고 지은 칼럼명에 들어가는 값.(키로 사용할 값)
            allocationSize=50
    )
    private Long id;
 
    /*
     * varchar2(10) -> 기본값 255;
     */
    @Column(name = "NAME",nullable=false,length=10)
    private String username;
 
    private Integer age;
 
    /*
     * EnumType의 기본값 설정은 정수이다.
     */
    @Enumerated(EnumType.STRING)
    @Column(name="ROLE_TYPE",nullable=false,length=20)
    private RoleType roleType;
 
    @Temporal(TemporalType.TIMESTAMP)
    private Date createdDate;
 
    @Temporal(TemporalType.TIMESTAMP)
    private Date lastModifiedDate;
 
    @Lob
    private String description;
}

 

MEMBER_SEQ_GENERATOR라는 테이블  생성기를 등록하였다. 여기의 설정들은 모두 주석으로 설명을 걸어놓았다

 

allocationSize 이용한 성능 최적화?

 

영속성 컨텍스트에 엔티티를 저장하기 위해 식별자를 구하는 과정을 설명하면,

 

1)식별자를 구하려고 데이터베이스 시퀀스를 조회한다.

->select board_seq.nextval from dual

 

그렇다면 만약 increment by 1이라는 설정이라면? 엔티티를 하나하나 저장할 때마다 데이터베이스에 엑세스하여 시퀀스 값을 가져와야한다.(allocationSize=1일때)

 

여기에서 allocationSize 이용하여 성능 최적화를   있다. allocationSize값을 적절히 크기를 키워 설정한 값만큼  번에 시퀀스 값을 증가시키고 나서

그만큼 메모리에서 기억해 시퀀스  자체를 메모리에서 할당하는 것이다. 예를 들어 allocationSize 값이 50이면 시퀀스를 한번 가져올때 마다 한번에 50  증가된

값을 받아온다. 그말은 처음 시퀀스 (50) 받아오면 1~50까지는 메모리에서 엔티티에 식별자를 할당한다. 그리고 51 되면 시퀀스 (100) 한번더 가져와 

51~100까지를 메모리에서 다시 할당해준다. 이말은 쉽게 말하자면 allocationSize=1 때보다 시퀀스 값을 가져오기 위해 데이터베이스에 엑세스하는 횟수를 49번을

줄인 것이다. insert 성능이 크게 중요하지 않은 애플리케이션은 상관없지만  반대는  성능최적화 전략을 사용해보는 것도 좋은 방안일 듯하다. 하지만 제대로

사용하지 못하면 아무리 좋은 것도 독이 되니  설계해서 쓰는 것이 좋을  같다.

 

기타

사실은 전략중에 AUTO전략이라는 것도 있다. 이것은 데이터베이스 벤더에 따라 자동으로 위의 3가지방법중 하나를 선택해 준다. 장점은 데이터베이스 벤더가 바뀌어도

코드에 수정이 없다는 것이다. 하지만 중요한 것은 사상이다. JPA 엔티티 클래스만 봐도 테이블이 예상이 되어야하는데 AUTO 가독성이 많이 떨어지기에

크게 추천하지 않고, 위의 3가지 방법중 하나를 선택하여 명시적으로 설정해주는 것이 좋을  같다.(개인적인 생각)



출처: https://coding-start.tistory.com/71?category=781616 [코딩스타트]