@ColumnDefault

JPA 의 구현체인 Hibernate 에서 제공하는 @ColumnDefault 어노테이션은 스키마를 생성할 때 default 값을 설정해주는 역할을 한다. 사용방법은 매우 간단한데 default 값을 지정해주고 싶은 컬럼에 Literal 값을 적어주면 된다.

@Getter  
@NoArgsConstructor(access = AccessLevel.PROTECTED)  
@Entity  
public class Member {  
  
    @Id  
    @GeneratedValue(strategy = GenerationType.IDENTITY)  
    private Long id;  
  
    @ColumnDefault("'GENERAL'")  
	@Column(nullable = false)  
	@Enumerated(EnumType.STRING)
    private UserType userType;  
  
    @Embedded  
    private Email email;  
  
    @Embedded  
    private Address address;  
  
    @Embedded  
    private Nickname nickname;  
  
    @Embedded  
    private Password password;  
  
    @Builder  
	private Member(Long id, UserType userType, Email email, Address address, Nickname nickname, Password password) {  
	    this.id = id;  
	    this.userType = userType;  
	    this.email = email;  
	    this.address = address;  
	    this.nickname = nickname;  
	    this.password = password;  
	}
}

이제 애플리케이션을 실행해주면 JPA 에서 생성해주는 DDL 쿼리에 default 'GENERAL' 이라는 값이 잘 들어가는것을 확인할 수 있다.

create table member (
        id bigint generated by default as identity,
        nickname varchar(24) not null,
        address varchar(255),
        email varchar(255),
        password varchar(255),
        user_type varchar(255) default 'GENERAL' check (user_type in ('GENERAL','KAKAO','GOOGLE')),
        primary key (id)
    )

@DynamicInsert

@ColumnDefault 를 사용할 때, 해당 컬럼에 @Column(nullable = false)과 같이 null 을 허용하지 않는 제약조건을 걸어주었을때 조심해야 한다. 아래와 같이 Member 엔티티를 만들때 UserType 정보를 주지 않고 영속화하면 DataIntegrityViolationException 가 터지게 된다.

@Test  
public void columnDefaultTest() {  
    // given  
    Member member = Member.builder()  
            .email(new Email("revi1337@naver.com"))  
            .nickname(new Nickname("nickname"))  
            .build();  
  
    // when & then  
    assertThatThrownBy(() -> memberRepository.save(member))  
            .isExactlyInstanceOf(DataIntegrityViolationException.class);  
}

JPA 에서 insert 쿼리를 통해 값을 영속화시킬 때, 기본적으로 insert 쿼리에 기본적으로 모든 column 을 명시하게 된다. 만약 영속성 객체에 모든 Column 값이 제대로 채워지지않으면 아래와 같이 insert 쿼리에서 해당 column 에 null 값이 들어가게 된다. 하지만 이것은 nullable = false 에 위배되기 때문에 오류가 터지게 되는 것이다.

insert into member(email, nickname, user_type) values ('revi1337@naver.com', '넥네임4', null); 

@ColumnDefault 는 다이렉트로 들어온 null 값을 디폴트 값으로 바꾸어주지 않는다.

@ColumnDefault 를 통해 DDL 에서 default 제약조건을 걸어주어도, insert 쿼리를 통해 null 이 직접적으로 들어오면 default 값으로 바꿔지지 않는다.

해결방법은 간단하다. 영속성 객체의 클래스단에 @DynamicInsert 를 명시해주면 된다.

@DynamicInsert  
@Getter  
@NoArgsConstructor(access = AccessLevel.PROTECTED)  
@Entity  
public class Member {  
  
    @Id  
    @GeneratedValue(strategy = GenerationType.IDENTITY)  
    private Long id;  
  
    @ColumnDefault("'GENERAL'")  
    @Column(nullable = false)  
    @Enumerated(EnumType.STRING)  
    private UserType userType;  
  
    @Embedded  
    private Email email;  
  
    @Embedded  
    private Address address;  
  
    @Embedded  
    private Nickname nickname;  
  
    @Embedded  
    private Password password;  
  
    @Builder  
    private Member(Long id, UserType userType, Email email, Address address, Nickname nickname, Password password) {  
        this.id = id;  
        this.userType = userType;  
        this.email = email;  
        this.address = address;  
        this.nickname = nickname;  
        this.password = password;  
    }  
  
    public void updatePassword(CharSequence encodedPassword) {  
        this.password = password.update(encodedPassword);  
    }  
}

영속성 객체의 클래스단에 @DynamicInsert 를 명시해주면, 영속성객체를 영속화할 때 채워지지않은 column 들은 무시하게 되고, 아래와 같은 쿼리가 나가게 된다. 해당 쿼리가 나가게 되면 @ColumnDefault 에서 걸어준 default 제약조건으로 해당 쿼리의 userType 은 GENERAL 이 된다.

insert into member(email, nickname) values ('revi1337@naver.com', '넥네임4')

테스트 코드는 아래와 같이 짜볼 수 있다. 코드 Hightlight 된 부분을 보면, EntityManger 로 영속성 컨텍스트를 한번 비워주고 있다. 잘 생각해보면 객체에서는 UserType 을 비웠기 때문에 Null 인 상태이다. 하지만 DB 에서는 default 제약조건으로 인해 GENERAL 인 상태이다. 결과적으로 객체와 DB 와 Sync 가 맞지 않는 문제가 발생한다. 이를 해결하기 위해 ID 로 멤버를 찾아오는 쿼리가 한번 더 나가게 된다. (불필요한 쿼리)

@Rollback(false)  
@Test  
public void columnDefaultTest() {  
    // given  
    Member member = Member.builder()  
            .email(new Email("revi1337@naver.com"))  
            .nickname(new Nickname("nickname"))  
            .build();  
  
    // when  
    memberRepository.save(member);  
  
    // then  
    entityManager.clear();  
    Member resultMember = memberRepository.findById(1L).get();  
    assertThat(resultMember.getUserType()).isEqualByComparingTo(UserType.GENERAL);  
}

이 Sync 가 맞지않는 해결방법으로는 @ColumnDafault 를 통해 default 값을 설정하되, 객체에도 default 값이 셋팅될 수 있도록 코드를 만들어주면 된다. 이렇게 만들어주면, 영속성컨텍트스틀 비우고 추가적인 불필요한 쿼리 필요없이 DB 와 객체의 Sync 를 맞출 수 있다.



@DynamicInsert  
@Getter  
@NoArgsConstructor(access = AccessLevel.PROTECTED)  
@Entity  
public class Member {  
  
    @Id  
    @GeneratedValue(strategy = GenerationType.IDENTITY)  
    private Long id;  
  
    @ColumnDefault("'GENERAL'")  
    @Column(nullable = false)  
    @Enumerated(EnumType.STRING)  
    private UserType userType;  
  
    @Embedded  
    private Email email;  
  
    @Embedded  
    private Address address;  
  
    @Embedded  
    private Nickname nickname;  
  
    @Embedded  
    private Password password;  
  
    @Builder  
    private Member(Long id, UserType userType, Email email, Address address, Nickname nickname, Password password) {  
        this.id = id;  
        this.userType = userType == null ? UserType.GENERAL : userType;  
        this.email = email;  
        this.address = address;  
        this.nickname = nickname;  
        this.password = password;  
    }  
}