본문 바로가기
Java/Spring Boot

Optional<T>

by curious week 2025. 3. 31.

1. Optional이란?

null을 직접 다루지 않고,
"값이 있을 수도, 없을 수도 있는 객체"를 감싸는 컨테이너 클래스

  • Optional<T>은 null-safe한 값을 표현하는 도구
  • 리턴 타입에 사용하되, 파라미터/필드에는 되도록 쓰지 말기
  • orElseThrow, map, filter, ifPresent 잘 쓰면 코드 깔끔해짐
Optional<String> name = Optional.of("Mars");
Optional<String> empty = Optional.empty();

2. 왜 쓰는가?

  • NullPointerException 방지
  • "null 체크"를 명시적으로 강제
  • 리턴값이 "있을 수도/없을 수도" 있는 경우 문서화 효과

예전 방식

User user = repository.findByEmail(email);
if (user != null) { ... }

Optional 방식

Optional<User> userOpt = repository.findByEmail(email);
userOpt.ifPresent(user -> { ... });

3. Optional 사용 방법 정리

of(T) 절대 null 아닌 값을 감쌀 때 사용 (null이면 NPE 발생)
ofNullable(T) null 가능성을 허용
empty() 값이 없는 Optional 생성
isPresent() / isEmpty() 값 존재 여부 확인
get() 내부 값 꺼내기 (⚠️ 값 없으면 예외 발생 → 비추)
orElse(default) 값 없을 때 기본값 제공
orElseGet(Supplier) 지연 실행으로 기본값 제공
orElseThrow() 값 없을 때 예외 던짐
map() / flatMap() 값이 있을 경우 변환
filter(Predicate) 조건 만족 여부로 Optional 유지/제거

4. 실무 예시들

리턴 타입에 사용 (가장 많이 씀)

public Optional<User> findByEmail(String email) {
    return userRepository.findByEmail(email);
}

호출부:

userService.findByEmail("mars@gmail.com")
    .ifPresent(user -> doSomething(user));

기본값 처리: orElse, orElseGet

String name = userOpt.map(User::getName).orElse("익명");

orElse는 항상 실행됨
orElseGet(() -> ...)은 값이 없을 때만 실행됨 → 성능상 유리


예외 처리

User user = userRepository.findById(id)
    .orElseThrow(() -> new NotFoundException("해당 유저 없음"));

매우 흔한 패턴으로 실무에서 if (user == null)을 대체


map / flatMap 활용

Optional<String> name = userOpt.map(User::getName);
Optional<String> city = userOpt
    .flatMap(user -> Optional.ofNullable(user.getAddress()))
    .map(Address::getCity);

리턴 타입이 null 가능할 때 ✅ Optional 적극 사용
파라미터에 Optional 사용 🔴 비추 (코드 가독성 ↓)
필드에 Optional 사용 🔴 비추 (JPA @Entity에서 절대 금지)
외부 API 응답 파싱 ✅ null-safe하게 사용 가능
내부에서 즉시 사용하는 임시 값 ❌ 굳이 감싸지 말 것

❗ 실무에서 주의할 점

  • get()은 거의 쓰지 마세요. (ifPresent, orElseThrow 사용 권장)
  • Optional<T>을 파라미터로 받는 건 지양 → 그냥 @Nullable 쓰는 게 더 낫다
  • JPA 엔티티 필드에 Optional 사용 ❌ → Hibernate가 못 읽음

 

예제 1: 기본 null 체크 → Optional

before (전통적인 null 검사)

User user = userRepository.findByEmail(email);
if (user != null) {
    return user.getName();
} else {
    return "알 수 없음";
}

after (Optional 리팩토링)

return userRepository.findByEmail(email)
        .map(User::getName)
        .orElse("알 수 없음");

null 체크도 없어지고, 가독성도 높아짐


예제 2: null이면 예외 던지기

before

User user = userRepository.findById(id);
if (user == null) {
    throw new NotFoundException("유저 없음");
}
return user;

after

User user = userRepository.findById(id)
        .orElseThrow(() -> new NotFoundException("유저 없음"));

orElseThrow()는 가장 실무에서 자주 쓰이는 Optional 방식 중 하나


예제 3: 중첩된 null 검사

before

if (user != null) {
    Address addr = user.getAddress();
    if (addr != null) {
        return addr.getCity();
    }
}
return "Unknown";

after

return Optional.ofNullable(user)
        .map(User::getAddress)
        .map(Address::getCity)
        .orElse("Unknown");

null 안전하게 중첩 탐색 가능 → NPE 걱정 끝!


예제 4: ifPresent로 조건부 실행

before

User user = userRepository.findByEmail(email);
if (user != null) {
    logger.info(user.getName());
}

after

userRepository.findByEmail(email)
        .ifPresent(user -> logger.info(user.getName()));

예제 5: 기본값 orElse vs orElseGet 차이

User user1 = userOpt.orElse(createDefaultUser());     // ❌ createDefaultUser()는 항상 실행됨
User user2 = userOpt.orElseGet(() -> createDefaultUser()); // ✅ 없을 때만 실행됨

orElse는 항상 실행되고, orElseGet은 조건부 실행됨
무거운 연산이거나, 디폴트 객체 생성할 때는 orElseGet 추천!


기본 null 체크 .map(...).orElse(...)
예외 던지기 .orElseThrow(...)
중첩 null 접근 .map(...).map(...).orElse(...)
조건부 실행 .ifPresent(...)
디폴트 객체 지연 생성 .orElseGet(...)

 

'Java > Spring Boot' 카테고리의 다른 글

Swagger와 API 명세  (1) 2025.03.31
Spring Boot Test 관련 (JUnit, Mockito, AssertJ,Spring Boot Test, Testcontainers )  (0) 2025.03.31
JPQL과 QueryDSL  (0) 2025.03.31
JPA  (3) 2025.03.31
OAuth 구현  (0) 2025.03.28