들어가며
public String getBookTitle(Long id) {
Book book = bookRepository.findById(id);
return book.getTitle(); // book이 null이면?
}
Spring Data JPA의 findById()는 Optional<T>를 반환합니다. 위 코드처럼 직접 엔티티를 반환받으려 하면 컴파일 에러가 나죠. 이 글에서는 왜 JPA가 Optional을 선택했는지, 그리고 이를 어떻게 활용해야 하는지 정리합니다.
Optional이란?
Optional은 "값이 있을 수도 있고 없을 수도 있다"를 명시적으로 표현하는 컨테이너입니다. 맞습니다. 슈뢰딩거의 고양이입니다. 컴파일러는 값의 존재 여부를 알 수 없으니, 런타임에 조치하라고 강제하는 셈입니다.
// null을 직접 다루는 방식
Book book = findBook(id);
if (book != null) {
return book.getTitle();
}
return "Unknown";
// Optional 방식
return findBook(id)
.map(Book::getTitle)
.orElse("Unknown");
또 코드를 읽는 사람에게 메서드 시그니처만으로 "이 값은 없을 수 있다"는 걸 알릴 수 있습니다. null 체크를 빼먹는 실수를 컴파일러가 잡아주지는 않지만, 코드 자체가 인지를 전달합니다.
// null 반환 가능성이 숨겨져 있음
Book findByIsbn(String isbn);
// null 가능성이 명시됨
Optional<Book> findByIsbn(String isbn);
Spring Data JPA에서의 활용
기본 패턴
@Service
@RequiredArgsConstructor
public class BookService {
private final BookRepository bookRepository;
public Book findBook(Long id) {
return bookRepository.findById(id)
.orElseThrow(() -> new BookNotFoundException(id));
}
public BookResponse findBookResponse(Long id) {
return bookRepository.findById(id)
.map(this::toResponse)
.orElseThrow(() -> new BookNotFoundException(id));
}
}
orElseThrow()를 통해 null 일 경우에 대한 조치를 지시합니다.
orElse vs orElseGet
이 둘의 차이를 모르면 의도치 않은 성능 문제가 발생합니다.
// orElse: 값이 있어도 항상 실행됨
Book book = bookOpt.orElse(createDefaultBook());
// orElseGet: 값이 없을 때만 실행됨
Book book = bookOpt.orElseGet(() -> createDefaultBook());
orElse()는 인자를 항상 평가합니다. 대체값 생성에 비용이 든다면(DB 조회, 객체 생성 등) orElseGet()을 사용해야 합니다.
map과 filter
연관 엔티티를 안전하게 탐색하거나 조건을 걸 때 유용합니다.
// 저자 이름 안전하게 가져오기
Optional<String> authorName = bookRepository.findById(id)
.map(Book::getAuthor)
.map(Author::getName);
// 재고가 있는 책만 반환
public Optional<Book> findAvailableBook(Long id) {
return bookRepository.findById(id)
.filter(book -> book.getStock() > 0);
}
주의사항
필드로 사용하지 않기
Optional은 반환 타입으로 설계되었습니다. JPA 엔티티 필드로는 사용할 수 없습니다.
// ❌ 잘못된 사용
@Entity
public class Book {
private Optional<Author> author;
}
// ✅ 올바른 사용
@Entity
public class Book {
@ManyToOne
private Author author;
public Optional<Author> getAuthorOptional() {
return Optional.ofNullable(author);
}
}
컬렉션에는 사용하지 않기
빈 컬렉션을 반환하는 것이 더 좋은 설계입니다.
// 불필요
Optional<List<Book>> findByAuthor(String author);
// 빈 리스트 반환
List<Book> findByAuthor(String author);
이 경우에는 리스트를 다루는 일관된 설계 원칙이 필요합니다. 일반적으로 JPA에서 반환시에는 null 리스트를 반환하지 않습니다. 따라서 이를 사용하는 비즈니스 로직에서도 일관성을 유지해야 합니다.
isPresent() + get() 안티패턴
Optional의 이점을 무시하는 코드입니다.
// null 체크와 다를 바 없음
if (bookOpt.isPresent()) {
return bookOpt.get().getTitle();
}
return "Unknown";
// 직관적 사용법
return bookOpt.map(Book::getTitle).orElse("Unknown");
of() vs ofNullable()
Optional.of(book); // book이 null이면 NPE
Optional.ofNullable(book); // book이 null이면 Optional.empty()
값이 확실히 null이 아닐 때만 of()를 사용합니다.
마치며
Optional은 개발자에게 아주 큰 지표가 되지만 만능은 아닙니다. 핵심은 "값이 없을 수 있다"를 명시적으로 표현하는 것입니다. 그렇기 때문에 Optional을 사용하는 개발자의 일관된 철학이 필요합니다.
- Repository 조회 결과는 Optional로 반환
- 서비스 계층에서 orElseThrow()로 예외 처리
- 필드나 파라미터에는 사용 금지
- 컬렉션은 빈 리스트로 반환
- isPresent() + get() 대신 map(), orElse() 활용