Files

5.3 KiB

Optional 사용 기준

목적

Optional은 값의 부재를 메서드 반환 타입에서 명시적으로 표현할 때 사용한다.
null을 감추기 위한 만능 래퍼로 쓰지 않는다.

공식 의미

  • Optional은 값이 있을 수도 없을 수도 있는 non-null 컨테이너다.
  • Optional주로 메서드 반환 타입으로 사용하도록 의도되었다.
  • Optional 변수 자체는 null이면 안 된다.
  • Optional은 value-based class이므로 동기화나 == 같은 identity-sensitive 사용을 하지 않는다.

기본 규칙

1. 기본값은 반환 타입으로만 사용

다음 경우에만 Optional<T>를 우선 검토한다.

  • 조회 결과가 없을 수 있는 단건 반환
  • 파싱/탐색/검색 결과가 없을 수 있는 반환
  • 호출자가 "없음"을 명시적으로 처리해야 하는 API

예:

  • findById
  • findUserByEmail
  • parseXxx
  • resolveCurrentTenant

2. 필드 타입으로는 기본 금지

Optional을 엔티티 필드, DTO 필드, 설정 필드, 상태 저장 필드로 두지 않는다.

이유:

  • 공식적으로 Optional은 주로 반환 타입 용도다.
  • 상태 저장 필드에서는 nullable field, value object, 별도 타입, 또는 명시적 상태 모델이 더 적합하다.
  • nullable Optional, empty Optional, present Optional의 혼합은 의미를 더 복잡하게 만든다.

3. 파라미터 타입으로는 기본 금지

메서드/생성자 파라미터에 Optional<T>를 직접 받지 않는다.

대신 아래를 우선 검토한다.

  • 메서드 오버로드
  • 별도 명시적 파라미터 타입
  • nullable + 명확한 nullability annotation
  • 요청 객체/커맨드 객체로 캡슐화

예외:

  • 외부 라이브러리 API와의 일관성을 맞춰야 하는 경우
  • 함수형 조합 API에서 정말 표현력이 좋아지는 경우

4. 절대 금지: Optional을 null로 사용

Optional<T> 타입 변수/반환값/파라미터에 null을 쓰지 않는다.

금지 예:

return null; // Optional<T> 반환 메서드
Optional<User> user = null;
someMethod(null); // Optional<User> 파라미터

없음은 반드시 Optional.empty()로 표현한다.

5. get()은 기본 금지

get()은 값이 없으면 예외를 던진다.
공식 문서도 preferred alternative로 orElseThrow()를 제시한다.

기본 규칙:

  • get()은 테스트 코드나 명백한 증명 직후의 제한된 상황이 아니면 사용하지 않는다.
  • 업무 코드에서는 map, flatMap, filter, ifPresent, orElse, orElseGet, orElseThrow를 사용한다.

6. isPresent() + get() 조합은 기본 금지

다음 패턴은 기본 금지한다.

if (opt.isPresent()) {
    return opt.get();
}

대신 아래를 우선 검토한다.

  • map
  • ifPresent
  • orElse
  • orElseGet
  • orElseThrow

7. 기본값 계산 비용이 있으면 orElseGet

단순 상수/기존 값이면 orElse를 검토한다.
계산/조회/생성 비용이 있거나 side-effect 가능성이 있으면 orElseGet을 사용한다.

기본 원칙:

  • expensive default는 orElseGet
  • 단순 literal/default object는 orElse 검토

8. nested Optional 금지

Optional<Optional<T>> 형태를 만들지 않는다.

  • mapper가 다시 Optional을 반환하면 map 대신 flatMap을 사용한다.
  • 계층 탐색은 map(...).flatMap(...) 또는 flatMap(...) 조합으로 평탄화한다.

9. map은 값 변환, ifPresent는 최종 부작용

  • 변환/계산/연결은 map, flatMap, filter를 사용한다.
  • 최종 경계 side-effect는 ifPresent, ifPresentOrElse를 사용한다.

반환값을 쓰지 않을 때 map을 사용하지 않는다.

10. 컬렉션/스트림은 Optional보다 빈 표현 우선

컬렉션/배열/스트림 결과는 Optional<List<T>>보다 빈 컬렉션/빈 스트림을 우선한다.

기본 규칙:

  • 단건 부재 -> Optional<T>
  • 다건 부재 -> empty collection / empty stream

11. Optional은 직렬화/DTO 경계 기본 금지

Request/Response DTO, JPA Entity, 설정 프로퍼티 객체에 Optional 필드를 두지 않는다.

이유:

  • 경계 타입은 직렬화/바인딩/스키마 명확성이 우선이다.
  • Optional은 표현 모델보다 API 결과 표현 쪽에 더 적합하다.

12. primitive optional은 정말 필요할 때만

OptionalInt, OptionalLong, OptionalDouble은 primitive 부재 표현이 정말 중요한 경우에만 사용한다.
일반 업무 도메인에서는 보통 Optional<T> 또는 명시적 값 객체가 더 읽기 쉽다.

Spring / Repository 기준

  • 단건 조회 결과 없음은 Optional<T>를 우선 검토한다.
  • 컬렉션 반환은 null 대신 empty collection을 반환한다.
  • repository package의 nullability는 @NonNullApi, @NonNull, @Nullable 같은 도구와 함께 설계한다.

프로젝트 기준 요약

  • Optional은 기본적으로 반환 타입에만 사용한다.
  • 필드/파라미터는 기본 금지한다.
  • Optional 자체를 null로 두지 않는다.
  • get()isPresent() + get()은 기본 금지한다.
  • 값 변환은 map, flatMap, filter를 사용한다.
  • 최종 부작용은 ifPresent, ifPresentOrElse를 사용한다.
  • 다건 결과는 Optional<List<T>> 대신 empty collection을 사용한다.