
Java Optional은 null을 없애는 도구로 소개되는 경우가 많습니다. 하지만 그렇게만 이해하면 Optional을 너무 많이 쓰거나, 반대로 꼭 필요한 곳에서도 피하게 됩니다.
Optional의 핵심은 null을 감추는 것이 아니라 “결과가 없을 수 있다”는 의도를 반환 타입에 드러내는 것입니다. 호출자는 값을 바로 꺼내 쓰기 전에 없는 경우를 처리해야 합니다.
이 글은 Java Optional을 언제 쓰고 언제 피해야 하는지, 그리고 `orElse`, `orElseGet`, `get`에서 자주 생기는 실수를 코드로 정리합니다.
Java Optional을 먼저 한 장으로 보기

Java Optional은 반환 타입에서 가장 자연스럽다
Oracle API 문서의 API note는 Optional이 주로 method return type으로 의도되었다고 설명합니다. 이유는 간단합니다. 메서드를 호출하는 사람이 결과가 없을 수 있음을 타입에서 바로 볼 수 있기 때문입니다.
public Optional<User> findByEmail(String email) {
User user = userRepository.find(email);
return Optional.ofNullable(user);
}이 메서드는 사용자가 없을 수 있다는 사실을 숨기지 않습니다. 호출자는 `isPresent`로 확인하거나, `orElseThrow`, `map`, `orElseGet` 같은 방식으로 없는 경우를 처리해야 합니다.
Optional은 null을 반환하지 않겠다는 약속이 아니라, 없는 결과를 호출자에게 명시적으로 처리하게 만드는 반환 타입입니다.
필드와 파라미터에 습관적으로 쓰면 어색해진다
Optional을 모든 곳에 쓰면 코드가 좋아질까요? 그렇지는 않습니다. 필드에 Optional을 넣으면 객체 상태가 불필요하게 감싸지고, 파라미터에 Optional을 받으면 호출자가 값을 감싸서 넘겨야 합니다.
// 대체로 어색한 형태
public void send(Optional<String> message) {
...
}
// 보통은 overload나 명확한 null 정책이 더 읽기 쉽다
public void send(String message) {
...
}물론 팀 컨벤션이나 특정 프레임워크 상황에 따라 예외는 있을 수 있습니다. 하지만 기본값은 반환 타입에서 먼저 쓰고, 필드와 파라미터는 더 신중히 보는 편이 좋습니다.
orElse와 orElseGet 차이를 모르면 비용이 새어 나간다
Optional에서 가장 자주 나오는 실수 중 하나는 `orElse`와 `orElseGet` 차이입니다. `orElse`에 넘긴 값은 Optional이 비어 있지 않아도 먼저 계산될 수 있습니다. `orElseGet`은 값이 없을 때 supplier를 실행합니다.
User user = findByEmail(email)
.orElse(createGuestUser()); // Optional에 값이 있어도 createGuestUser()가 먼저 실행될 수 있음
User user = findByEmail(email)
.orElseGet(() -> createGuestUser()); // 값이 없을 때만 실행기본값이 단순 문자열이나 상수라면 큰 차이가 없을 수 있습니다. 하지만 DB 조회, 파일 읽기, 객체 생성 비용이 큰 작업이라면 `orElseGet`을 먼저 고려해야 합니다.
get을 쓰면 Optional을 쓴 의미가 약해진다
`optional.get()`은 값이 있으면 값을 반환하지만, 없으면 `NoSuchElementException`을 던집니다. 그래서 값이 없을 가능성을 처리하려고 Optional을 썼는데, 다시 런타임 예외로 돌아가는 코드가 됩니다.
// 피하고 싶은 형태
User user = findByEmail(email).get();
// 의도가 더 분명한 형태
User user = findByEmail(email)
.orElseThrow(() -> new UserNotFoundException(email));없는 경우가 정말 예외라면 `orElseThrow`로 예외 의미를 드러내는 편이 낫습니다. 없는 경우가 자연스러운 흐름이라면 `orElse`, `orElseGet`, `map`으로 분기하는 편이 읽기 쉽습니다.
Optional을 잘 쓰는 코드 모양
String displayName = findByEmail(email)
.map(User::getProfile)
.map(Profile::getDisplayName)
.filter(name -> !name.isBlank())
.orElse("익명 사용자");이 코드는 중간 값이 없을 수 있음을 단계적으로 처리합니다. `if (user != null)`을 무조건 없애는 것이 목표가 아니라, 값이 있을 때만 다음 변환을 진행하는 흐름을 타입으로 표현합니다.
실무 체크리스트
- 메서드 결과가 없을 수 있고 호출자가 처리해야 하는가? Optional 반환을 고려한다
- 필드에 Optional을 저장하려는가? 객체 모델이 더 복잡해지지 않는지 다시 본다
- 파라미터로 Optional을 받으려는가? overload나 명확한 입력 정책이 더 낫지 않은지 본다
- 기본값 생성 비용이 있는가? orElse보다 orElseGet을 고려한다
- get을 쓰고 있는가? orElseThrow나 map/filter 흐름으로 바꿀 수 있는지 본다
정리
Java Optional은 null을 없애는 만능 도구가 아닙니다. 결과가 없을 수 있다는 의도를 반환 타입에 드러내고, 호출자가 그 상황을 처리하게 만드는 도구입니다.
Optional을 잘 쓰려면 “어디에 쓰느냐”가 중요합니다. 반환 타입에서는 의미가 크지만, 필드와 파라미터에서는 오히려 코드가 무거워질 수 있습니다. 그리고 `orElse`, `orElseGet`, `orElseThrow`의 차이를 알고 써야 Optional의 장점이 살아납니다.
함께 보면 좋은 글: 자바 Optional은 왜 자주 오해될까, 자바 stream과 for문 차이