2026-07-09

사진: Unsplash 의 Luca Bravo
개요
Java 8부터 도입된 Optional은 NullPointerException을 방지하고 null 처리를 명시적으로 표현하기 위한 컨테이너 클래스입니다.
그러나 실무에서 잘못 사용하면 오히려 코드가 더 복잡해지거나, null 체크 수준에 머무는 경우가 많습니다. 이번 글에서는 Optional의 올바른 사용 패턴과 피해야 할 안티패턴을 정리합니다.
Optional 기본 생성 방법
// 값이 있을 때
Optional<String> opt1 = Optional.of("hello");
// null일 수도 있을 때
Optional<String> opt2 = Optional.ofNullable(getValue());
// 빈 Optional
Optional<String> opt3 = Optional.empty();
Optional.of(null) 은 즉시 NullPointerException을 던집니다. null 가능성이 있다면 반드시 ofNullable을 사용하세요.
❌ 흔한 안티패턴
1. isPresent() + get() 조합 — if-null 체크와 다를 게 없음
// ❌ 안티패턴: Optional을 쓰는 의미가 없음
Optional<User> userOpt = userRepository.findById(id);
if (userOpt.isPresent()) {
User user = userOpt.get();
System.out.println(user.getName());
}
2. Optional을 메서드 파라미터로 사용
// ❌ 안티패턴: 파라미터에 Optional 쓰지 말 것
public void process(Optional<String> name) { ... }
Optional은 반환 타입에만 사용하는 것이 원칙입니다. 파라미터로 넘기면 호출자가 Optional.of()로 감싸야 하는 번거로움이 생기고 가독성이 떨어집니다.
3. Optional을 필드로 선언
// ❌ 안티패턴: 직렬화 불가, 설계 의도와 맞지 않음
public class User {
private Optional<String> nickname;
}
✅ 올바른 사용 패턴
1. orElse() / orElseGet() 으로 기본값 처리
// orElse: 항상 기본값 객체를 생성함
String name = userOpt.orElse("익명");
// orElseGet: 람다로 지연 평가 → 생성 비용이 있을 때 유리
String name = userOpt.orElseGet(() -> generateDefaultName());
구분 orElse() orElseGet()
| 기본값 평가 시점 | Optional 상태와 무관하게 항상 실행 | Optional이 비어있을 때만 실행 |
| 사용 권장 | 단순 상수, 리터럴 | 메서드 호출, 객체 생성 등 비용 있는 경우 |
2. map() / flatMap() 으로 체이닝
// ✅ null 체크 없이 안전하게 변환
Optional<String> city = userRepository.findById(id)
.map(User::getAddress)
.map(Address::getCity);
3. orElseThrow() 로 예외 처리
// ✅ 값이 없으면 명시적 예외
User user = userRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("유저를 찾을 수 없습니다. id=" + id));
4. ifPresent() / ifPresentOrElse() 로 사이드 이펙트 처리
// 값이 있을 때만 실행
userOpt.ifPresent(user -> log.info("로그인: {}", user.getName()));
// Java 9+: 있을 때 / 없을 때 분기
userOpt.ifPresentOrElse(
user -> log.info("유저 존재: {}", user.getName()),
() -> log.warn("유저 없음")
);
filter() 활용 — 조건부 처리
// 특정 조건을 만족하는 경우에만 처리
Optional<User> activeUser = userRepository.findById(id)
.filter(user -> user.isActive());
activeUser.ifPresent(u -> sendWelcomeEmail(u));
Stream과 연계 (Java 9+)
// Optional → Stream 변환: stream()
List<String> names = userIds.stream()
.map(userRepository::findById) // Stream<Optional<User>>
.flatMap(Optional::stream) // Stream<User> (비어있는 Optional 제거)
.map(User::getName)
.collect(Collectors.toList());
Java 9 이전에는 filter(Optional::isPresent).map(Optional::get) 패턴을 써야 했지만, Optional::stream으로 훨씬 간결하게 처리할 수 있습니다.
정리
상황 권장 방법
| 기본값이 필요할 때 | orElse() / orElseGet() |
| 예외를 던져야 할 때 | orElseThrow() |
| 값을 변환할 때 | map() / flatMap() |
| 사이드 이펙트 처리 | ifPresent() / ifPresentOrElse() |
| 조건 필터링 | filter() |
| 절대 쓰면 안 되는 곳 | 메서드 파라미터, 클래스 필드, 컬렉션 원소 |
결론
Optional은 null 대신 "값이 없을 수도 있다"는 의도를 타입으로 표현하는 도구입니다. isPresent() + get() 조합은 지양하고, map(), orElseGet(), orElseThrow() 중심의 함수형 스타일로 작성하면 코드가 훨씬 명확해집니다.
메인 이미지 출처 : 사진: Unsplash의 Luca Bravo
태그: Java, Optional, Java8, NullPointerException, 자바 카테고리: Language/Java
