안녕하세요. 개발자 Jindory입니다.
오늘은 JPA의 칼럼값에 값이 설정되지 않았을때 default value로 저장될 수 있도록 설정하는 방법에 대해서 작성해보고자 합니다.
# 글 작성 이유
사용자가 값을 입력하지 않더라도 default로 값이 저장되길 원했으나, 예상과 다르게 null로 저장되거나 예샹치 못한 값으로 저장되는 문제가 발생하여, default value 설정을 어떤 방법으로 해야하는지를 정리하기 위해 작성해 보았습니다.
# Entity 및 Repository 구성
Account Entity
- Account Entity에는 아래와 같은 항목들을 관리합니다.
- id(PK,String,Not null,30)
- password(String,Not null,50)
- displayName(String,20)
- age(int)
- privateYn(char)
- 특정값 없이도 Entity가 생성되도록 아래와 같은 생성자 메서드를 만들었습니다.
- public Account(String email,String pw,String displayName,int age) : privateYn 제외
- public Account(String email,String pw,String displayName) : age,privateYn 제외
- public Account(String email,String pw) : displayName,age,privateYn 제외
package jpabook.jpashop.domain;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import org.hibernate.annotations.ColumnDefault;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Getter
@Entity
@Table(name="ACCOUNT")
@NoArgsConstructor
public class Account {
@Id
@Column(nullable = false, length = 30)
private String email;
@Column(name = "PASSWORD", nullable = false, length = 50)
private String pw;
@Column(length = 20)
@ColumnDefault("'guest'")
private String displayName;
@Column(length = 3)
@ColumnDefault("'15'")
private int age;
@Column(length = 1)
@ColumnDefault("'N'")
private char privateYn;
public Account(String email,String pw,String displayName,int age,char privateYn) {
this.email = email;
this.pw = pw;
this.displayName = displayName;
this.age = age;
this.privateYn = privateYn;
}
public Account(String email,String pw,String displayName,int age) {
this.email = email;
this.pw = pw;
this.displayName = displayName;
this.age = age;
}
public Account(String email,String pw,String displayName) {
this.email = email;
this.pw = pw;
this.displayName = displayName;
}
public Account(String email,String pw) {
this.email = email;
this.pw = pw;
}
}
AccountRepository
JpaRepository를 상속받아서 기본 JPA기능을 사용할 수 있도록 만들었습니다.
package jpabook.jpashop.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import jpabook.jpashop.domain.Account;
public interface AccountRepository extends JpaRepository<Account,String> {
}
# Default value 설정 방법 확인을 위한 테스트 방법
먼저 column값을 default로 설정할 수 있는 방법을 아래와 같이 시도하며, 어떤 방법이 default value가 설정되는지 또 어떤 방법이 더 좋은지에 대해서 알아보겠습니다.
- ColumnDefault로 설정
- ColumnDefault로 설정 + @DynamicInsert,@DynamicUpdate
- ColumnDefault로 설정 + @PrePersiste
TestCase 작성
- Repository를 필드주입하고, Database에서 입력된 값들을 확인하기 위해 Rollback(false) 설정을 했습니다.
package jpabook.jpashop.repository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase.Replace;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.test.annotation.Rollback;
import org.springframework.transaction.annotation.Transactional;
import jpabook.jpashop.domain.Account;
@DataJpaTest
@Transactional
@AutoConfigureTestDatabase(replace = Replace.NONE)
public class AccountRepositoryUnitTest {
@Autowired
private AccountRepository accRepository;
@Test
@Rollback(false)
void createAccount(){
Account acc1 = new Account("temp1@google.com","123qwe","unknown",5);
Account acc2 = new Account("temp2@google.com","123qwe","unknown");
Account acc3 = new Account("temp3@google.com","123qwe");
accRepository.save(acc1);
accRepository.save(acc2);
accRepository.save(acc3);
}
}
1. ColumnDefault로 설정
ColumnDefault 어노테이션은 Table의 칼럼 생성시 default값이 설정되도록 하는 설정입니다.
String, int, char 자료형에 대해서 값이 매핑되지 않았을때 어떤 값들이 매핑되는지 알아보기 위해 아래의 3개의 값에 @ColumnDefault값을 각각 설정합니다.
@Column(length = 20)
@ColumnDefault("'guest'")
private String displayName;
@Column(length = 3)
@ColumnDefault("'15'")
private int age;
@Column(length = 1)
@ColumnDefault("'N'")
private char privateYn;
※ @ColumnDefault가 아닌 @Column에 columnDefinition으로도 Default값을 설정할 수 있습니다.
@Column(length = 20,columnDefinition =" varchar(255) default 'guest'")
private String displayName;
@Column(length = 3,columnDefinition ="integer default 15")
private int age;
@Column(length = 1,columnDefinition ="char(1) default 'N'")
private char privateYn;
@ColumnDefault를 설정하면 JPA에서 테이블을 생성할때 아래와 같이 default값이 설정됩니다.
create table account (
email varchar(255) not null,
age integer default '15',
display_name varchar(20) default 'guest',
private_yn char(1) default 'N',
password varchar(50) not null,
primary key (email)
)
그렇다면 데이터를 저장할때 각각의 값이 설정되지 않으면 default값으로 설정 될까요?
위에서 작성한 Test Code를 실행했을때 아래와 같은 쿼리가 생성됩니다.
TestCode에서 실행한 Save method에서는 생성하지 않은 displayName,age,privateYn이
null,0,' '으로 입력되는 것을 볼 수 있습니다.
실제 데이터베이스에서도 DISPLAY_YN,AGE,PRIVATE_YN값이 기본값으로 들어가는것을 알 수 있습니다....
과연 설정하지 않은 값들이 왜 들어가는 걸까요?
Java의 자료형에서 값이 할당되지 않았을때, 아래와 같은 기본값이 들어가는 것을 알 수 있습니다.
String : null
byte : 0
short : 0
int : 0
long : 0L
float : 0.0f
double : 0.0d
char : '\u0000'(십진수로 0)
boolean : false
그래서 생성자에서 할당하지 않은 값이 기본값으로 생성된것을 알 수 있습니다.
2. ColumnDefault로 설정 + @DynamicInsert,@DynamicUpdate
@DynamicInsert와 @DynamicUpdate는 해당값이 입력되지 않았을때(null), INSERT, UPDATE에서 null인 value를 제외하는 어노테이션입니다.
@Getter
@Entity
@DynamicInsert
@DynamicUpdate
@NoArgsConstructor
public class Account {
...중략...
@Column(length = 20)
@ColumnDefault("'guest'")
private String displayName;
@Column(length = 3)
@ColumnDefault("'15'")
private int age;
@Column(length = 1)
@ColumnDefault("'N'")
private char privateYn;
...중략...
}
@ColumnDefault 설정을 했기 때문에 테이블 생성시 default값이 설정되는것을 알 수 있습니다.
create table account (
email varchar(255) not null,
age integer default '15',
display_name varchar(20) default 'guest',
private_yn char(1) default 'N',
password varchar(50) not null,
primary key (email)
)
그렇다면 @DynamicInsert와 @DynamicUpdate의 설정으로 값이 매핑되지 않았을때 @ColumnDefault값으로 생성되었을까요?
위에 SQL문 생성된걸을 보면, privateYn과 age값은 제외되지 않았지만, displayName은 Insert문에 없는것을 알 수 있습니다.
Database에서 확인해 봤을때도, DISPLAY_NAME의 값은 default 값으로 생성된것을 알 수 있습니다.
왜 privateYn과 age값은 값을 입력하지 않았는데 insert문에 포함되고, displayName은 insert문에 없는것일까요?
@DynamicInsert
The @DynamicInsert annotation is used to specify that the INSERT SQL statement should be generated whenever an entity is to be persisted.
By default, Hibernate uses a cached INSERT statement that sets all table columns. When the entity is annotated with the @DynamicInsert annotation, the PreparedStatement is going to include only the non-null columns.
See the @CreationTimestamp mapping section for more info on how @DynamicInsert works.
@DynamicUpdate
The @DynamicUpdate annotation is used to specify that the UPDATE SQL statement should be generated whenever an entity is modified.
By default, Hibernate uses a cached UPDATE statement that sets all table columns. When the entity is annotated with the @DynamicUpdate annotation, the PreparedStatement is going to include only the columns whose values have been changed.
위 설명에 따르면 @DynamicInsert, @DynamicUpdate는 null이 아닌 값만 SQL문에 추가된다는 설명이며, privateYn(char), age(int)는 기본값이 null이 아니므로 Insert문에 그대로 추가됨을 알 수 있습니다.
자세한 설명은 아래의 url에서 찾아서 읽어보시면 도움이 될것 같습니다.
3. ColumnDefault로 설정 + @PrePersiste
@PrePersist는 Entity 객체가 저장되기 전에 특정 로직이 적용될 수 있도록 설정하는 어노테이션이며, persist 직전에 적용됩니다.
@Getter
@Entity
@Table(name="ACCOUNT")
@NoArgsConstructor
public class Account {
...중략..
@Column(length = 20)
@ColumnDefault("'guest'")
private String displayName;
@Column(length = 3)
@ColumnDefault("'15'")
private int age;
@Column(length = 1)
@ColumnDefault("'N'")
private char privateYn;
@PrePersist
public void prePersist() {
// displayName이 null이면 default값으로 매핑 되도록 설정
if(this.displayName == null) {
this.displayName = "guest";
}
else {
this.displayName = this.displayName;
}
// age이 0이면 default값으로 매핑 되도록 설정
if(this.age == 0) {
this.age = 15;
}
else {
this.age = this.age;
}
// privateYn이 '\u0000'이면 default값으로 매핑 되도록 설정
if(this.privateYn == '\u0000') {
this.privateYn = 'N';
}
else {
this.privateYn = this.privateYn;
}
}
...중략..
}
@ColumnDefault 설정을 했기 때문에 테이블 생성시 default값이 설정되는것을 알 수 있다.
create table account (
email varchar(255) not null,
age integer default '15',
display_name varchar(20) default 'guest',
private_yn char(1) default 'N',
password varchar(50) not null,
primary key (email)
)
그렇다면 @PrePersist의 설정으로 값이 매핑되지 않았을때 설정한 로직으로 생성되었을까요?
위와 같이 @PrePersist에서 설정한 값으로 값들이 매핑되어 Insert된것을 볼 수 있습니다!!!
Database에도 저희가 @PrePersist에 설정한 값으로 저장되어 있네요.
※ 하지만 @ColumnDefault값과 @PrePersist에서 설정한 값이 달라질때는 @PrePersist의 설정이 먼저 적용되어 SQL에 반영되고 @DynamicInsert나 @DynamicUpdate가 없으면 @CollumnDefault 어노테이션은 제대로 적용되지 않으므로, 개발자로 하여금 더 큰 혼란을 야기할 수 있습니다.
< @ColumnDefault에서 설정한 값과 @PrePersist에서 설정한 값을 다르게 했을경우 >
@Column(length = 20)
@ColumnDefault("'guest'")
private String displayName;
@Column(length = 3)
@ColumnDefault("'15'")
private int age;
@Column(length = 1)
@ColumnDefault("'N'")
private char privateYn;
@PrePersist
public void prePersist() {
// displayName이 null이면 default값으로 매핑 되도록 설정
if(this.displayName == null) {
this.displayName = "member";
}
else {
this.displayName = this.displayName;
}
// age이 0이면 default값으로 매핑 되도록 설정
if(this.age == 0) {
this.age = 999;
}
else {
this.age = this.age;
}
// privateYn이 '\u0000'이면 default값으로 매핑 되도록 설정
if(this.privateYn == '\u0000') {
this.privateYn = 'F';
}
else {
this.privateYn = this.privateYn;
}
}
Table을 생성시에는 @ColumnDefault에 따라서 default값이 설정되는것으로 보입니다.
create table account (
email varchar(255) not null,
age integer default '15',
display_name varchar(20) default 'guest',
private_yn char(1) default 'N',
password varchar(50) not null,
primary key (email)
)
하지만 TestCode가 실행된 SQL을 보면 @PrePersist에서 설정한 값들이 먼저 적용되어, Table에서 설정한 default값과 다른 값들로 설정되어 Entity가 생성됨을 알 수 있습니다.
Database를 확인해 봐도, @PrePersist에 설정한 로직이 반영되어 결과를 본 개발자가 큰 혼란을 가질 수 있습니다!!!
그래서 제가 생각했을때,
만일 @ColumnDefault를 설정한 자료형이 모두 String값이라고 한다면, @ColumnDefault + @DynamicInsert,@DynamicUpdate 조합으로 설정하여 코드를 간결하게 하는것도 좋다고 생각합니다.
만일 Default로 설정해야하는 값이 String이 아닌 int,char,boolean,long과 같이 기본값이 null이 아닌 값이 있거나, 특정한 값에 따라서 default값이 설정되어야 할 경우에는 @PrePersist만 설정하여, 영속성 상태가 되기 이전에 값을 셋팅하여 SQL이 생성되도록 설정하는것이 더 좋다고 생각합니다.
이렇게 JPA의 칼럼값에 값이 설정되지 않았을때 default value로 저장될 수 있도록 설정하는 방법에 대해서 알아봤습니다.
혹시라도 정정할 내용이나 추가적으로 필요하신 정보가 있다면 댓글 남겨주시면 감사하겠습니다.
오늘도 Jindory 블로그에 방문해주셔서 감사합니다.
[참고]
https://www.baeldung.com/jpa-entity-lifecycle-events
'개발 > JPA' 카테고리의 다른 글
[JPA] QueryDsl 동적으로 정렬 조건 적용하기(OrderSpecifier 클래스 구현) (0) | 2022.11.16 |
---|---|
[JPA] Eclipse에서 QueryDsl 설정하기 (0) | 2022.05.21 |
[JPA] JPA 설정 방법 (0) | 2022.05.01 |
[JPA] JPA 관련 application.properties 설정 (0) | 2022.04.27 |
[JPA] 객체지향 쿼리 (0) | 2022.02.22 |