본문 바로가기
language/java

Optional 심화

by 죄니안죄니 2026. 6. 17.
반응형

이번 글은 Java 8 이후 가장 많이 오해되는 클래스이다.

많은 사람들이 Optional을

"null 대신 쓰는 클래스"

정도로만 알고 있다.

그래서

 
class User {

    Optional<String> name;

}
 

처럼 필드에도 쓰고,

 
void save(Optional<User> user)
 

처럼 파라미터에도 쓰기도 한다.

하지만 이것은 Optional을 만든 목적과 맞지 않는 사용법이다.

이번 글에서는 Optional의 탄생 이유부터 내부 동작, 실무에서의 올바른 사용법까지 깊게 알아보자.


Optional 심화

Optional은 null을 없애기 위한 클래스가 아니라, "값이 없을 수도 있음"을 반환 타입으로 명확하게 표현하기 위한 컨테이너(Container)이다.


Optional이 등장한 이유

Java에서 가장 흔한 예외는 무엇일까?

아마도

NullPointerException
 

일 것이다.


다음 코드를 보자.

 
User user = repository.find(id);

System.out.println(user.getName());
 

문제는

 
repository.find(id)
 

User

또는

null
 

을 반환할 수 있다는 것이다.


메서드 선언만 보면

 
User find(Long id);
 

이다.


여기서는

반드시 User를 반환하는가?

없으면 null인가?

예외를 던지는가?
 

를 알 수 없다.


Optional을 사용하면

 
Optional<User> find(Long id);
 

메서드 시그니처만 봐도

User가 있을 수도 있고

없을 수도 있다.
 

라는 의도가 명확하게 드러난다.


Optional은 Box이다.

 
Optional<String> name =
        Optional.of("Kim");
 

메모리 관점에서는

Optional

────────────

value

↓

"Kim"

────────────
 

처럼 값을 감싸고 있는 하나의 객체이다.


내부 구조는 매우 단순하다.

실제로는 거의 이런 형태이다.

 
public final class Optional<T> {

    private final T value;

}
 

즉,

Optional

↓

value 하나 보관
 

하는 Wrapper 객체이다.


Optional 생성 방법

Optional.of()

 
Optional<String> name =
        Optional.of("Kim");
 

반드시 값이 있어야 한다.


 
Optional.of(null);
 

결과

NullPointerException
 

Optional.ofNullable()

 
Optional<String> name =
        Optional.ofNullable(userName);
 

null

↓

Optional.empty()

---------

값 존재

↓

Optional(value)
 

가장 많이 사용하는 방식이다.


Optional.empty()

 
Optional<String> name =
        Optional.empty();
 

값 없음
 

을 의미한다.


값 꺼내기

많은 사람들이

 
optional.get();
 

을 사용한다.


 
Optional<User> user =
        Optional.empty();

user.get();
 

결과

NoSuchElementException
 

즉,

Optional.get()

↓

무조건 꺼냄

↓

없으면 예외
 

이다.


실무에서는 거의 권장하지 않는다.


orElse()

 
User user =
        optional.orElse(new User());
 

값 있음

↓

기존 값

---------

값 없음

↓

new User()
 

겉보기에는 문제없어 보인다.


하지만 중요한 함정

 
User user =
        optional.orElse(createUser());
 

여기서

 
createUser();
 

Optional에 값이 있어도 실행된다.


즉,

Optional 존재

↓

createUser() 실행

↓

버림
 

이다.


orElseGet()

 
User user =
        optional.orElseGet(() -> createUser());
 

실행 흐름

Optional 존재

↓

createUser() 실행 안 함

---------

Optional 없음

↓

createUser() 실행
 

실무에서는

비용이 큰 객체 생성

DB 조회

외부 API 호출
 

이라면

반드시

 
orElseGet()
 

을 사용하는 것이 좋다.


map()

Optional의 가장 큰 장점이다.


기존 코드

 
if (user != null) {

    return user.getName();

}

return null;
 

Optional

 
Optional<String> name =

    user.map(User::getName);
 

흐름

Optional<User>

↓

User 존재

↓

getName()

↓

Optional<String>

---------

없음

↓

Optional.empty()
 

map이 계속 연결되는 이유

 
Optional.of(user)

        .map(User::getAddress)

        .map(Address::getCity)

        .map(City::getName);
 

흐름

User

↓

Address

↓

City

↓

Name
 

중간에 하나라도

Optional.empty()
 

가 되면

그 이후 map은 실행되지 않는다.


filter()

 
Optional<User> user =

    optional.filter(User::isActive);
 

흐름

User 존재

↓

isActive()

↓

true

↓

유지

---------

false

↓

Optional.empty()
 

flatMap()

많이 헷갈리는 부분이다.


다음 메서드가 있다고 하자.

 
Optional<Address> getAddress();
 

map

 
Optional<Optional<Address>>
 

가 된다.


Optional이 한 번 더 감싸진다.


flatMap

 
Optional<Address>
 

한 번만 감싼다.


즉,

map

↓

Optional<Optional<T>>

---------

flatMap

↓

Optional<T>
 

이다.


Optional을 필드에 사용하지 않는 이유

좋지 않은 코드

 
class User {

    Optional<String> name;

}
 

이제

메모리 구조는

User

↓

Optional

↓

String
 

이다.


원래

User

↓

String
 

이면 충분한데

객체가 하나 더 생긴다.


또한

직렬화, JPA, Jackson 등과 함께 사용할 때도 불필요한 복잡성이 증가한다.


Optional을 파라미터에 사용하지 않는 이유

좋지 않은 코드

 
void save(Optional<User> user)
 

호출하는 사람은

 
save(Optional.of(user));

save(Optional.empty());
 

를 직접 만들어야 한다.


그냥

 
void save(User user)
 

또는

메서드를 분리하는 것이 더 명확하다.


Optional을 컬렉션에 사용할까?

 
List<Optional<User>>
 

보다는

 
List<User>
 

를 사용한다.


없으면

애초에 리스트에 넣지 않는 것이 더 자연스럽다.


실무에서 가장 많이 사용하는 곳

Repository

 
Optional<User>

findById(Long id);
 

Service

 
return repository.findById(id)

        .map(UserDto::from);
 

Controller

 
return repository.findById(id)

        .orElseThrow(NotFoundException::new);
 

Optional 남용 사례

 
Optional<User>

↓

Optional<Address>

↓

Optional<City>

↓

Optional<String>
 

너무 많은 Optional 체인은

오히려 가독성을 떨어뜨린다.


면접 포인트

Optional이 만들어진 이유는?

null을 제거하기 위한 것이 아니라, "값이 없을 수도 있음"을 반환 타입으로 명확하게 표현하기 위해 만들어졌다.


Optional.get()을 권장하지 않는 이유는?

값이 없으면 NoSuchElementException이 발생하므로 orElse(), orElseGet(), orElseThrow() 등을 사용하는 것이 더 안전하다.


orElse()와 orElseGet() 차이

 
orElse(create())
 

→ create()가 항상 실행된다.

 
orElseGet(() -> create())
 

→ 값이 없을 때만 실행된다.


Optional을 어디에 사용하는 것이 가장 좋은가?

메서드의 반환 타입(Return Type)


Optional을 필드, 파라미터, 컬렉션에 권장하지 않는 이유는?

불필요한 객체 생성, 복잡성 증가, 프레임워크(JPA/Jackson 등)와의 호환성 문제 때문이다.


다음 글

다음은 Stream 개선사항(Java 9~21) 이다.

여기서는

  • takeWhile()
  • dropWhile()
  • ofNullable()
  • iterate() 개선
  • toList()와 Collectors.toList()의 차이
  • mapMulti()
  • 병렬 Stream의 최신 변화

까지 Java 9부터 Java 21까지 실무에서 유용한 변경점을 정리하겠다.

반응형

'language > java' 카테고리의 다른 글

Virtual Thread(Java 21)  (0) 2026.06.17
Stream 개선사항(Java 9~21)  (0) 2026.06.17
sealed class(Java 17)  (0) 2026.06.17
record(Java 16)  (0) 2026.06.17
Pattern Matching for switch(Java 21)  (0) 2026.06.17

댓글