135 lines
4.7 KiB
Markdown
135 lines
4.7 KiB
Markdown
# null 처리 기준
|
|
|
|
## 목적
|
|
|
|
`null`은 “값이 없음”을 표현하는 기본 도구가 아니라, **명시적으로 허용된 경계에서만 제한적으로 다루는 값**으로 취급한다.
|
|
|
|
기본값은:
|
|
- non-null
|
|
- 명시적으로 nullable인 경우만 `null` 허용
|
|
- 내부 로직에서는 가능한 빨리 null을 제거하고 더 명확한 표현으로 바꾼다
|
|
|
|
## 공식 의미
|
|
|
|
- Java는 null-safety를 타입 시스템으로 직접 표현하지 못한다.
|
|
- 따라서 null 허용 여부는 API 계약, annotation, 검증 코드로 명시해야 한다.
|
|
- `Objects.requireNonNull(...)`은 메서드/생성자 파라미터 검증 용도로 우선 사용한다.
|
|
- Spring 계열에서는 package/type 수준의 nullability 기본값과 `@Nullable` 명시를 통해 API 계약을 드러내는 방식을 권장한다.
|
|
|
|
## 기본 규칙
|
|
|
|
### 1. 기본값은 non-null
|
|
명시적으로 nullable이라고 선언되지 않은 값은 non-null로 간주한다.
|
|
|
|
프로젝트 기본 태도:
|
|
- 파라미터: 기본 non-null
|
|
- 반환값: 기본 non-null
|
|
- 필드: 기본 non-null
|
|
- nullable은 예외적 상황만 명시
|
|
|
|
### 2. nullable 여부는 계약으로 드러낸다
|
|
다음 중 하나로 null 허용 여부를 명시한다.
|
|
|
|
- nullability annotation
|
|
- Optional 반환
|
|
- 빈 컬렉션/빈 문자열이 아닌 명시적 상태 타입
|
|
- API/문서/Javadoc 계약
|
|
|
|
“읽어보면 알 수 있음” 상태를 금지한다.
|
|
|
|
### 3. 경계에서만 null을 받는다
|
|
다음 경계에서는 null이 들어올 수 있다고 가정하고 방어한다.
|
|
|
|
- 외부 요청 입력
|
|
- DB/JPA 결과
|
|
- 외부 API 응답
|
|
- 설정값/환경변수
|
|
- legacy library API
|
|
|
|
하지만 경계를 지나 내부 로직으로 들어오면:
|
|
- 즉시 검증하거나
|
|
- Optional/value object/default object/명시적 상태로 변환한다
|
|
|
|
### 4. 내부 로직에서 null 전파 금지
|
|
application/domain/business 로직에서는 nullable 값을 계속 흘려보내지 않는다.
|
|
|
|
금지:
|
|
- 여러 계층을 거치며 nullable String/DTO field를 계속 전달
|
|
- null 여부를 business 의미처럼 암묵적으로 해석
|
|
- “일단 null로 두고 나중에 확인” 패턴
|
|
|
|
### 5. 파라미터 검증은 가능한 한 즉시
|
|
public/protected/package boundary 또는 생성자에서는 필요한 경우 초기에 검증한다.
|
|
|
|
기본 방식:
|
|
- `Objects.requireNonNull(...)`
|
|
- 명시적 argument validation
|
|
- request binding / validation annotation
|
|
- value object 생성 시 검증
|
|
|
|
### 6. Optional과 null을 섞지 않는다
|
|
- `Optional` 자체를 null로 두지 않는다
|
|
- Optional을 반환하면 null 반환 금지
|
|
- nullable field를 억지로 Optional field로 바꾸지 않는다
|
|
|
|
### 7. 컬렉션/배열/스트림은 null 대신 빈 값 우선
|
|
다건 결과는 가능한 한 null 대신 다음을 사용한다.
|
|
|
|
- empty list
|
|
- empty set
|
|
- empty map
|
|
- empty stream
|
|
- empty array
|
|
|
|
`null` 컬렉션은 기본 금지다.
|
|
|
|
### 8. DTO/직렬화 경계는 nullable을 명시적으로 관리
|
|
Request/Response DTO에서는 nullable field가 필요할 수 있다.
|
|
이 경우:
|
|
- nullable 여부를 명시하고
|
|
- 내부 로직 진입 전에 변환/검증한다.
|
|
|
|
DTO의 nullable 상태를 domain/application 전체로 전파하지 않는다.
|
|
|
|
### 9. Entity와 Domain은 구분해서 본다
|
|
- JPA entity는 DB nullable 제약을 반영할 수 있다
|
|
- domain model은 비즈니스 invariant 기준으로 더 엄격해야 한다
|
|
|
|
즉:
|
|
- DB가 nullable이어도 domain은 non-null일 수 있다
|
|
- persistence mapper에서 변환/검증 책임을 진다
|
|
|
|
### 10. null-check는 가능한 한 한 곳에서 끝낸다
|
|
같은 값에 대해 여러 레이어에서 반복 null-check 하지 않는다.
|
|
|
|
기본 방향:
|
|
- 경계에서 검증
|
|
- 값 객체로 승격
|
|
- 이후는 non-null 가정
|
|
|
|
### 11. null을 business state로 쓰지 않는다
|
|
다음 패턴을 금지한다.
|
|
|
|
- `status == null`이면 임시 상태
|
|
- `provider == null`이면 로컬 로그인
|
|
- `deletedAt == null` 같은 인프라 관례를 business 의미로 직접 사용
|
|
|
|
business state는 enum, value object, explicit flag, 상태 타입으로 표현한다.
|
|
|
|
### 12. annotation 기반 nullability는 일관되게 사용
|
|
Spring/JSpecify 스타일을 도입하면:
|
|
- package/type default를 먼저 정하고
|
|
- 예외만 `@Nullable`로 표시한다
|
|
- 무의미하게 nullable/non-null annotation을 섞지 않는다
|
|
|
|
## 프로젝트 기준 요약
|
|
|
|
- 기본은 non-null
|
|
- nullable은 계약으로 명시
|
|
- 경계에서 null을 받고 내부에서 제거
|
|
- public/constructor 경계는 `requireNonNull` 등으로 빠르게 검증
|
|
- 컬렉션/스트림은 null 대신 empty
|
|
- Optional과 null 혼용 금지
|
|
- DTO/JPA nullable을 domain/application에 그대로 전파 금지
|
|
- null을 business meaning으로 사용 금지
|