Back-end/Spring

[Spring] Optional 개념과 사용하는 이유 정리 (Spring Data JPA)

랑 이 2026. 6. 29. 21:10
반응형

Spring 백엔드 개발을 할 때 Optional이라는 객체를 자주 사용하게 되는데 Spring Data JPA를 사용할 때 findById() 메서드의 반환 타입이 Optional<T>으로 되어 있는 걸 볼 수 있는데 조회 결과가 없을 수 있는 상황(Null)을 명확하게 표현하기 위해서 사용된다

1. Optional이란?

Optional은 Null값을 포함할 수도 있고 포함하지 않을 수도 있는 컨테이너 객체입니다 

결과 없음을 명확하게 나타내야 할 필요가 있고 null 오류가 발생할 가능성이 높은 메서드 반환 타입으로 사용됩니다
 

JDK 26 Documentation - Home

The documentation for JDK 26 includes developer guides, API documentation, and release notes.

docs.oracle.com

다음과 같은 코드가 있다고 가정해보자

Optional<User> optionalUser;

Optional<User>는 User 객체를 직접 들고 있는 것이 아닌 User가 존재할 수도 있고 존재하지 않을 수도 있는 상태를 표현한다

 

일반적인 객체를 반환받는 경우

User user = userRepository.findById(userId); 
if (user == null) { 
	throw new IllegalArgumentException("사용자를 찾을 수 없습니다."); 
}

해당 코드는 조회 결과가 없을 때 null을 반환하는 경우가 발생하며 직접 예외처리를 해야 하며 null 체크를 하지 않은 경우 NullPointerException이 발생할 수 있다  

 

Optional은 이런 문제를 줄이고 값이 없을 수도 있다는 사실을 반환 타입에게 명확하게 드러내기 위해서 사용된다

2. Optional 내부 구조 살펴보기

Optional은 Java에서 제공하는 클래스로 내부적으로 값을 하나 감싸는 구조를 가진다

Optional<T> 내부 구조

 

Optional은 내부에 value라는 값을 가지고 있다

private final T value;

이 값이 존재하면 "값이 있는 Optional"이고, value가 null이면 "값이 없는 Optional"을 의미한다

Optional<User>는 내부적으로 다음과 같은 구조라고 볼 수 있다

Optional<User>
    └── value = User 객체

값이 없는 경우에는 다음과 같다

Optional<User>
    └── value = null

그리고 비어 있는 Optional은 내부적으로 EMPTY라는 객체를 사용한다

private static final Optional<?> EMPTY = new Optional<>(null);

Optional.empty()를 호출하면 새로운 객체를 계속 만드는 것이 아니라 비어 있는 Optional 객체를 반환하는 방식으로 동작한다

2-1. Optional 빈 객체 확인 주의점

비어 있는 Optional인지 확인할 때 다음처럼 비교하면 안 된다

optional == Optional.empty()

Optional 공식 문서에서도 Optional.empty()가 반드시 싱글톤이라는 보장은 없다고 설명한다

Optional.empty()는 현재 구현상 같은 객체를 반환하는 것처럼 보일 수 있지만 APi 문서상 반드시 같은 객체라는 보장이 없다는 의미

"== "비교는 객체의 값이 아니라 참조 주소를 비교하는데 Optional이 비어 있는지 확인할 때 위 코드처럼 비교하면 안 된다

 

Optional의 값 존재 여부는 객체 주소가 아니라 Optional이 제공하는 메서드로 확인해야 한다

if (optional.isEmpty()) {
    // 값이 없는 경우
}

또는 Java 8 기준에서는 다음과 같이 사용할 수 있다

if (!optional.isPresent()) {
    // 값이 없는 경우
}

2-2. orElseThrow() 내부 동작

Spring Data JPA에서 findById()를 사용할 때 자주 사용되는 Optional 메서드 orElseThrow()입니다

이 메서드는 Optional 내부에 값이 있으면 그 값을 반환하고 값이 없으면 지정한 예외를 발생시킨다 실제 내부 구조는 다음과 같다

orElseThrow 메서드 내부구조

  1. Optional 내부의 value가 null이 아니면 value를 반환한다
  2. value가 null이면 exceptionSupplier.get()을 실행한다
  3. get()으로 생성된 예외를 throw 한다

예를 들어 다음 코드가 있다고 해보자

User user = userRepository.findById(userId)
        .orElseThrow(() -> new UserNotFoundException());

조회 결과가 존재하면 Optional 내부의 value에 User 객체가 들어 있다

Optional<User>
    └── value = User 객체

이 경우 orElseThrow()는 value를 그대로 반환 또는 반대로 조회 결과가 없다면 Optional 내부의 value는 null이다

Optional<User>
    └── value = null

이 경우 orElseThrow()는 다음 코드를 실행한다

exceptionSupplier.get()

그리고 이 Supplier가 만든 예외를 발생시킨다

throw new UserNotFoundException();

 

그래서 Spring Service 계층에서는 다음과 같은 코드가 자주 사용된다

public UserResponse findUser(Long userId) {
    User user = userRepository.findById(userId)
            .orElseThrow(UserNotFoundException::new);

    return UserResponse.from(user);
}

여기서 UserNotFoundException::new는 예외 객체를 생성하는 메서드 참조이다

 

다음 코드와 같은 의미이다

.orElseThrow(() -> new UserNotFoundException())

orElseThrow()는 Optional 내부 값이 없을 때 실행할 예외 생성 로직을 전달받고 실제로 값이 없을 때만 그 예외를 생성해서 던진다

3. Spring에서 Optional을 사용하는 이유

Spring Data JPA의 findById()가 Optional <T>를 반환하는 이유는 조회 결과가 없을 수 있기 때문이다

Optional<User> findById(Long id);

그리고 Service 계층에서는 이 Optional을 그대로 Controller까지 넘기기보다는 필요한 도메인 객체를 꺼내거나 예외를 발생시키는 방식으로 처리하는 것이 일반적이다

User user = userRepository.findById(userId)
        .orElseThrow(UserNotFoundException::new);

이 코드의 장점은 다음과 같다

  • 조회 결과가 없을 수 있다는 사실이 Optional로 표현된다
  • 값이 없을 때의 처리가 orElseThrow()로 명확하게 드러난다
  • get()을 직접 사용하는 것보다 예외 의미가 분명하다
  • Service 계층에서 조회 실패 상황을 책임지고 처리할 수 있다

Optional은 내부적으로 value를 감싸는 클래스이고 Spring에서는 Repository 조회 결과가 없을 수 있는 상황을 표현하는 데 자주 사용된다 특히 findById().orElseThrow() 패턴은 조회 결과가 반드시 필요한 서비스 로직에서 가장 많이 사용되는 방식이다

반응형