🚩 1. 들어가며...
Spring Data JPA를 사용하며 개인 프로젝트를 진행하고 있던 중 유저 계정의 활성화 여부를 별도로 값을 주지 않아도 기본값(true = '1')이 삽입되도록 데이터베이스에서 DEFAULT 값을 세팅해주었다. 이러한 과정에서 발생한 문제와 해결방법과 함께 포스팅한다.
🚩 2. 문제 상황 - 기본형 boolean과 데이터베이스 기본값
데이터 베이스의 DDL에서 'is_enabled' 컬럼을 tinyint NOT NULL DEFAULT '1'로 설정했다. 설정의 목적은 해당 엔티티의 필드에 값이 명시적으로 초기화 되지 않고, 쿼리에 포함되지 않는다면(null이 할당되는 것과는 다르다.) 데이터베이스가 자동으로 true = '1'을 기본값으로 사용할 수 있도록 하는 것이다. 하지만, 실제로 데이터베이스에서는 false = '0'이 저장되는 문제가 발생했다. 아래는 문제가 되는 DDL과 Entity 클래스다.
CREATE TABLE `user` (
...
`is_enabled` tinyint NOT NULL DEFAULT '1',
...
)
@Entity
public class User {
...
@Column(nullable = false)
private boolean isEnabled;
// 빌더패턴 생성자
@Builder
public User(Long no, String id, String name, String email, String password, String tel, LocalDateTime createdAt, LocalDateTime updatedAt,
boolean isEnabled, Role role, LocalDate birthdate, LoginInfo loginInfo) {
this.no = no;
this.id = id;
this.name = name;
this.email = email;
this.password = password;
this.tel = tel;
this.createdAt = createdAt;
this.updatedAt = updatedAt;
this.isEnabled = isEnabled;
this.role = role;
this.birthdate = birthdate;
this.loginInfo = loginInfo;
}
}
이렇게 문제가 발생했던 이유는 바로 자바 기본형(int, double, char, boolean 등)은 필드에 값이 명시적으로 초기화되지 않으면 각 타입에 맞는 기본값을 할당한다. 이때 'isEnabled' 필드에 값을 명시적으로 전해주지 않는다면 이 필드는 boolean의 기본값인 false = '0'이 초기화된다. 이렇게 필드의 값이 false로이미 존재하기 때문에 데이터베이스에서 설정한 기본값 '1'대신 '0'이 저장되었던 것이다.
쿼리를 보면 'isEnabled' 필드에 값을 주지 않았는데도 기본타입의 기본값으로 인해 'is_enabled' 컬럼이 추가되어있는 것이 보인다.
🚩 3. 해결방법 - @DynamicInsert와 Boolean타입 사용
이를 해결 할 방법은 '@DynamicInsert' 와 Boolean래퍼 클래스를 활용하는 것이다.
📍 @DynamicInsert란?
- @DynamicInsert를 엔티티 클래스에 적용하면 INSERT쿼리를 생성할 때 필드가 'null' 인 필드를 자동으로 제외한다.
📍 Boolean란?
- 기본형 boolean의 래퍼 클래스로서 null을 가질 수 있는 참조형 객체이다.
@Entity
@DynamicInsert
public class User {
...
@Column
private Boolean isEnabled; // 'null' 이 허용
// 빌더패턴 생성자
@Builder
public User(Long no, String id, String name, String email, String password, String tel, LocalDateTime createdAt, LocalDateTime updatedAt,
Boolean isEnabled, Role role, LocalDate birthdate, LoginInfo loginInfo) {
this.no = no;
this.id = id;
this.name = name;
this.email = email;
this.password = password;
this.tel = tel;
this.createdAt = createdAt;
this.updatedAt = updatedAt;
this.isEnabled = isEnabled;
this.role = role;
this.birthdate = birthdate;
this.loginInfo = loginInfo;
}
}
이러한 특성을 활용하여 'isEnabled' 명시적으로 필드를 초기화하지 않는다면 null을 가질 수 있게 되었고, @DynamicInsert 어노테이션으로 인해 INSERT쿼리를 생성할 때 'null'인 해당 필드는 쿼리에서 제외되어 'is_enabled' 컬럼은 데이터베이스가 설정한 기본값 '1'이 적용되었다.
위의 쿼리를 보면 'isEnabled' 필드가 null이기 때문에 쿼리에서 'is_enabled' 컬럼은 제외되어 INSERT되고 있다.
🚩 4. 쿼리에서 'null' 값을 갖는 것과 아예 제외되는 것의 차이
지금까지 @DynamicInsert 어노테이션과 래퍼 클래스 그리고 데이터베이스의 DDL의 'DEFAULT' 키워드를 사용할 때 발생했던 문제점과 해결방법에 대해서 이야기했다. 그리고 나는 이 과정에서 'DEFAULT' 키워드가 실제로 어떻게 동작하는 지에 대한 오해가 있었다는 것을 알게되었다.
📌 4-1. MyBatis 사용 시의 오해
- 나는 지금껏 MyBatis를 사용하여 데이터베이스에 접근해 왔으며, 이 경우 'DEFAULT' 값을 가진 컬럼을 SQL 쿼리에서 명시적으로 제외시켜서 해당 값을 활용할 수 있었다. 이때 나는 컬럼이 아예 제외되는 것 == null 값을 갖는 것이라고 생각했다.
- 또한 데이터베이스의 'DEFAULT' 키워드가 특정 컬럼이 'null' 값을 가질 때 자동으로 할당되는 값 설정하는 것이라 오해한 것이다.
📌 4-2. JPA에서의 실제 동작
- JPA와 같은 ORM을 사용하면서, 필드가 'null'인 경우와 필드가 쿼리에서 완전히 제외되는 경우에 차이가 있다는 것을 알게 되었다.
- 'null' 값으로 설정된 필드는 쿼리에 포함되며, 데이터베이스에 nullable인 컬럼이라면 'null' 이 그대로 저장된다. 이때 'null' 또한 값을 입력한 것으로 간주하고 데이터베이스의 'DEFAULT' 값이 적용되지 않는다.
- 'DEFAULT' 키워드만 있을 경우 'null'을 입력할 수 있기 때문에 보통 NOT NULL과 함께 사용한다.
- 반면 '@DynamicInsert' 어노테이션을 사용하면, 필드가 'null' 일 때 해당 필드를 INSERT 쿼리에서 아예 제외시킬 수 있으며, 이 때만 데이터베이스의 'DEFAULT' 제약 조건의 값이 적용된다.
🚩 5. 마치며...
MyBatis와 같이 개발자가 직접 쿼리를 작성하여 개발할 때는 명시적으로 컬럼을 쿼리에서 제외시켜 'DEFAULT' 값을 활용할 수 있었던 반면 JPA는 이런 동작을 '@DynamicInsert' 어노테이션을 통해서 데이터베이스의 기본값 조건을 활용할 수 있었다. 이 과정을 통해서 컬럼이 null 값을 갖는 것과 아예 제외되는 것과는 다르다는 것을 인식했고 'DEFAULT' 제약 조건이 어떻게 작동하는 지에 대해서 이해하게 되었다. 또한 JPA는 쿼리를 자동으로 생성해주는 장점이 있지만 실제로 쿼리를 시각적으로 파악할 수 없기 때문에 어노테이션과 같은 추가적인 활용 방법과 동작 방식을 정확히 이해하고 있어야 하는 것이 중요하다고 생각했다. 즉 도구를 잘 쓰기 위해서는 사용법을 잘 알고 있어야 한다.