본문 바로가기
Java-Spring

Java 8에 추가된 Optional에 대해 알아보기

by 감사쟁이야 2024. 11. 10.

Optional은 null처리를 하기 위한 도구가 맞는가?

Optional은 Java 8에서 등장한 null 처리를 보다 안전하게 하기 위한 도구입니다.

 

하지만 “null을 없애기 위해” 무분별하게 사용하는 경우,

오히려 코드의 복잡성을 높이고 성능을 저하시킬 수 있습니다.

 

Optional을 어떻게 올바르게 사용할 수 있을지 함께 알아봅시다. 😌

 

Optional은 'null을 없애는 도구가 아니다.

많은 사람들이 Optional을 “null을 없애기 위한 도구”로 착각하지만,

정확히 말하면 “null을 안전하게 다루기 위한 래퍼(wrapper)”입니다.

 

즉, Optional의 목적은 null을 완전히 없애는 게 목적이 아니라,

“값이 없을 수도 있다”는 상황을 명시적이고 안전하게 표현하기 위한 포장지입니다.

 

공식 문서 Optional의 설명은 다음과 같습니다.

“A container object which may or may not contain a non-null value.

“Optional is primarily intended for use as a method return type where there is a clear need to represent ‘no result,’ and where using null is likely to cause errors. A variable whose type is Optional should never itself be null; it should always point to an Optional instance.”

 

공식 문서에 따르면,

Optional은 “값이 있을 수도 없을 수도 있는 컨테이너 객체”이며,

무엇보다도 메서드의 리턴 타입에서 ‘결과 없음(no result)’을 표현하기 위해 설계되었습니다.

 

또한 Optional 자체가 null이 되어서는 안 되며,

항상 Optional.empty()나 Optional.of(...) 같은 실제 Optional 인스턴스를 가리켜야 합니다.

Optional<User> user = findUserById(id);

 

위 코드에서 findUserById()가 유저를 찾지 못해도,

null 대신 Optional.empty()를 반환할 수 있습니다.

 

이 덕분에 if (user != null) 같은 null 체크를 반복하지 않아도 되고,

orElse()나 ifPresent() 같은 메서드를 통해

보다 의도적이고 안전하게 값의 존재 여부를 다룰 수 있습니다.

 

하지만 Optional이 null을 완전히 없애주는 것은 아닙니다.

Optional 내부적으로는 여전히

“값이 있을 수도, 없을 수도 있는 상태”를 표현합니다.

즉, Optional은 null을 숨긴 것이 아니라,

‘값이 없을 수 있음’을 명시적으로 드러내는 방법을 제공하는 것 입니다.

 

orElse() vs orElseGet() 차이

많은 개발자가 헷갈리는 부분인데요.

두 메서드는 Optional이 비어 있을 때 기본값을 반환하는데,

호출 시점이 다릅니다.

  메세지 실행 시점 값 생성
orElse(value) 항상 실행 Optional이 비어있든 아니든, value를 무조건 생성
orElseGet(() -> value) 필요할 때만 실행 Optional이 비어있을 때만 Supplier를 통해 값 생성

 

public User getUser() {
    return findUser().orElse(createDefaultUser());
}

위 코드는 findUser()가 값을 가지고 있어도

createDefaultUser()가 항상 호출됩니다. (즉, 불필요한 객체 생성 발생)

 

반면 다음은 효율적이다.

public User getUser() {
    return findUser().orElseGet(() -> createDefaultUser());
}

Optional이 비어있을 때만 createDefaultUser()가 호출됩니다.

이는 공식 문서에서도 확인할 수 있습니다.

 

Optional 사용 시 주의할 점

✔️ 필드나 파라미터 타입으로 사용하지 말 것

public class User {
    private Optional<String> name; // 권장하지 않음
}

이런 코드는 Optional의 본래 의도를 벗어납니다.

Optional은 “리턴값에서의 null 처리 도구”이지,

클래스의 속성이나 메서드 파라미터 타입으로 설계된 게 아닙니다.

 

이유는 단일 책임 원칙(SRP, Single Responsibility Principle)과 관련되는데요.

  • Optional은 “값이 존재할 수도, 없을 수도 있음”을 표현하는 책임을 가집니다.
  • 파라미터나 필드에서 이 책임을 가지게 하면, 호출하는 쪽에서 또 Optional을 풀고 검사해야 하는 책임이 생기므로 책임이 중첩됩니다.
  • 결과적으로 코드 가독성과 응집도가 떨어집니다.

 

✔️ Optional은 직렬화(Serialization)에 적합하지 않음

공식 문서에서는 직접적으로 직렬화 제한이 명시되어 있진 않지만,

Optional은 Serializable을 구현하지 않았습니다.

따라서 Optional 타입을 엔티티나 DTO 필드로 사용하면,

직렬화/역직렬화 시 NotSerializableException이 발생할 수 있습니다.

특히 JPA 엔티티나 JSON 변환 대상에 Optional을 넣는 것은 권장되지 않습니다.

 

✔️ 컬렉션 내부 값 표현에 사용하지 말 것

List<Optional<User>> users

Optional은 값이 있을 수도, 없을 수도 있다를 표현하고 List도 값이 없을 수도 있다를 표현할 수 있는 구조입니다.

즉, 둘다 없음을 표현할 수 있는 도구인데, 이 둘을 겹쳐 쓰면 같은 일을 두 번 하는 꼴이 됩니다.

대신 다음처럼 단순히 빈 컬렉션으로 표현하면 됩니다.

List<User> users = Collections.emptyList()

 

✔️ 단순 null 체크 대체용으로 남용하지 말 것

Optional은 “모든 null을 없애기 위한 만능 도구”가 아닙니다.

공식 문서에서 앞서 말했듯이 Optional의 목적을 이렇게 명시하고 있습니다.

"Optional is primarily intended for use as a method return type where there is a clear need to represent ‘no result."

 

즉, "값이 없을 수도 있다."는 상황을 타입 수준에서 드러내기 위한 도구입니다.

예를 들어, 다음과 같은 사용은 불필요합니다.

Optional<String> name = Optional.ofNullable(getName()); if (name.isPresent()) { ... }

이 코드는 언뜻 “Optional로 감싸니까 더 안전하겠지?”라고 생각하기 쉽지만,

실제로는 Optional을 쓸 이유가 전혀 없습니다.

 

이미 getName()이 null을 반환할 수 있다면,

아래와 같이 쓰는 것이 직관적입니다.

String name = getName();
if (name != null) {
// ...
}

이 경우엔 Optional을 만들고 다시 isPresent()로 까보는 과정이

오히려 불필요한 코드와 성능 낭비를 만들 뿐입니다.

 

그렇다면 언제 Optional이 유용할까요?

반대로, 아래처럼 리턴 타입으로 사용될 때는 Optional이 빛을 발합니다.

public Optional<User> findUserById(Long id) {
    // 유저가 없을 수도 있는 상황을 명시적으로 표현
}

이렇게 하면 메서드를 사용하는 쪽에서는

“이 결과가 없을 수도 있구나”를 컴파일 시점부터 인지할 수 있고,

orElse(), orElseGet() 등으로 안전하게 처리할 수 있습니다.

 

즉, Optional은 null을 대체하려는 게 아니라,

“값이 없을 수도 있다”는 가능성을 코드에 의도적으로 드러내는 표현 도구입니다.

단순히 null 체크를 깔끔하게 바꾸는 용도가 아닙니다.

 

Optional과 Stream 조합 시 flatMap() 활용

Optional은 Stream API와 결합했을 때도 자연스럽게 사용할 수 있습니다.

Optional 내부의 값을 꺼낼 때 flatMap을 활용하면

복잡한 중첩 구조를 깔끔하게 처리할 수 있습니다.

List<String> result = users.stream() .map(User::getProfile) .flatMap(Optional::stream) .collect(Collectors.toList());

Optional::stream을 이용하면, 값이 존재할 때만 스트림으로 변환되어

null이나 빈 값 처리를 따로 하지 않아도 됩니다.

 

마무리

Optional은 ‘null을 없애기 위한 만능키’가 아닙니다.

“값이 있을 수도 없을 수도 있다”는 상황을 명확히 드러내는 표현 도구입니다.

 

적재적소에 쓰면 NPE 없는 깔끔한 코드를 만들 수 있지만,

남용하면 오히려 복잡도만 높아집니다.

 

“null을 없애기 위한 도구”가 아니라,

“null을 안전하게 다루는 도구”라는 인식하는 것이 중요합니다.

  • 파라미터, 필드에서는 사용 지양하고 리턴 타입으로만 사용하기
  • orElseGet()과 orElse() 구분해서 사용하기
  • 또한, 직렬화 객체나 컬렉션 내부에서는 피하기
  • Optional은 null을 완전히 없애주는 것이 아니라, “없을 수 있는 값”을 표현하는 것임을 잊지 않기

 

참고