Number와 boolean 은 최대한 Not Null로 선언하기
테이블 설계시 종종 받는 질문 중 하나가 Boolean과 Number 컬럼의 Not Null
유무이다.
비즈니스적으로 기본값이 있는 경우가 아니면 유연하게 하기 위해 nullable
로 선언하는 경우를 자주 본다.
테이블의 Boolean과 Number 타입 컬럼을 nullable
로 설정하면 여러 문제가 발생할 수 있어서 가급적 추천하지 않는다.
대표적으로 다음과 같은 문제가 있다.
1. 혼란스러운 의미
컬럼을 nullable
로 설정하면 기본값이 null
이 되므로, 데이터의 의미가 굉장히 혼란스럽게 된다.
예를 들어, boolean 컬럼이면 값이 true
, false
, null
세 가지 상태가 될 수 있다.
이는 '참', '거짓', '미확인' 의 세 가지 상태가 된다는 것이다.
(일부 상황에서는 유용할 수 있지만) 대부분의 경우 논리적 복잡성이 추가되는 일이며, 특히 코드에서 이를 처리해야 하는 경우 복잡성을 추가한다.
boolean 컬럼의 null
값은 false
와 어떤 의미 차이가 있을까?
Number 타입이라면 0
과 null
은 어떤 의미 차이가 있을까?
신규 입사한 개발자가 혼란을 겪지 않으려면 null
을 허용한 컬럼에 대해서는 false
(0
) 와 null
의 의미 구분을 위한 주석이 항상 필요하다.
2. SQL의 복잡도
NULL 값을 가진 컬럼은 쿼리를 복잡하게 만든다.
크게 2가지가 있다.
2-1. Null 제외
예를 들어, 다음과 같이 테이블의 row 데이터가 있다고 해보자.
price - 1000;
price - null;
price - 1000;
price - 0;
이 상태에서 평균값은 몇일까?
4개 row의 총합은 2,000이며 총 4개가 있으니 500이 예상 된다.
하지만 실제로 쿼리를 수행하면 어떻게 될까?
select AVG(price) from table;
예상했던 500이 아닌, 666.666 이 나온다.
2000이란 합을 3으로 나눴을 때의 결과이다.
PostgreSQL의 AVG()
함수는 평균을 계산할 때, Null 값은 자동으로 무시된다.
즉, NULL은 계산에 포함되지 않는다.
NULL 값을 포함하여 평균을 계산하려면, NULL 값을 특정 값으로 바꾸는 함수인 COALESCE()
를 사용해야 한다.
select AVG(COALESCE(price, 0)) from table;
이렇게 하면 원래 의도했던 결과를 받을 수 있다.
물론
null
을 걸러내고, 실제 채워진 값들에 한해서만 결과를 가져와야하는 경우도 있다.
많은 집계 함수에서 이 null 데이터의 포함/미포함에 대해 항상 고민해야하고, 그에 따른 추가적인 SQL 함수를 고려해야만 한다.
매번 COALESCE
를 통한 추가 SQL를 사용하거나, 잘못된 결과를 사용하거나 등의 위험을 항상 안고가야 할 정도로 null
값을 유지해야할 필요가 있는지 고민 해봐야 한다.
2-2. IS NULL
단순한 SQL 조회문을 만들때도 이에 대한 고려가 항상 포함된다.
보통 false
와 null
혹은 0
과 null
은 함께 조건에 사용될 때가 많다.
(둘다 초기값 혹은 부정의 의미로 사용되기 때문)
하지만 null
값을 조회 하기 위해서는 일반적인 비교 연산자 (=, <>, <, >, in()
, 등) 을 사용할 수 없으며 IS NULL
또는 IS NOT NULL
조건을 사용해야 한다.
그래서 false
와 is null
을 함께 조회하려면 다음과 같이 or
연산자 쿼리를 작성해야만 한다.
SELECT * FROM users WHERE is_active IS FALSE OR is_active IS NULL;
단순히 하나의 상태값을 조회하기 위해서도 쿼리가 복잡해질 수 밖에 없다.
3. 애플리케이션 코드 복잡성
컬럼에 null
을 허용하면, 이 컬럼을 사용하는 애플리케이션 코드에서 Null 체크를 항상 수행해야만 한다.
이는 코드의 복잡성을 증가시킨다.
결국 nullable
컬럼의 데이터를 애플리케이션에서 조회하면 숫자 연산에 대해 0
과 null
상태 모두에 대해 항상 조건을 걸거나 null
-> 0
으로 변경을 수행해야만 한다.
// as-is
const result = getPrice() ?? 0;
// to-be
const result = getPrice();
일부 언어들에서 지원하는 널 병합 연산자 (nullish coalescing operator - ??
) 이 있다면 그래도 짧은 코드로 null
값을 처리할 수 있지만, 이런 문법 지원이 없는 언어라면 장황한 코드를 작성할 수 밖에 없다.
val price = getPrice();
val result = price? price : 0;
API의 결과에서, 테이블의 조회결과에서 항상 null
을 고려해야한다면 그만큼 애플리케이션의 코드는 장황해지며, 복잡도가 높아진다.
결론
Number 와 Boolean 타입에서 0
과 null
의 차이가 명확하거나, false
와 null
의 차이가 명확할 경우에만 Nullable
로 선언하며 가능하면 항상 Not Null
로 기본값을 보장하는 것이 좋다.
만약 null
과 false
, 0
의 구분이 필요한 상황이라면 그게 정말 null로 구분 해야하는 것인지 고민해봐야 한다.
오히려 상태를 나타내는 Enum이 좀 더 적합한 구분 방법이 될 수 있다.
다음과 같이 시험 결과
테이블이 있고, 합격 여부에 대한 항목이 필요하다고 했을때 기존에는 다음과 같이 boolean
과 null
을 활용해서 나타냈다면,
// as-is
pass.isPassed // 합격 여부
- null : 합격 발표 전
- false : 합격 발표 - 불합격
- true: 합격 발표 - 합격
사실은 아래와 같이 Enum
상태를 두는게 더 적합할 수 있다는 것이다.
// to-be
pass.status
- READY: 합격 발표 전
- FAIL: 합격 발표 - 불합격
- PASS: 합격 발표 - 합격
이에 대해서는 객체 생성 단계에서 무조건 기본값을 할당하면 좋다.
class Pass {
...
private _status = PassStatus.READY;
...
constructor() {
...
}
}
물론 정말로 null과 0의 차이가 명확하게 구분이 필요한 상황이라면 이에 대해 정확하게 주석을 남겨야 하며, 이 주석의 범위는 테이블 컬럼 주석과 ORM 영역 모두에 해당한다.