"오늘의 문제를, 내일의 기록으로 남깁니다."

막연한 이론보다, 구체적인 코드가 필요할 때. 직접 겪고 해결한 문제들을 기록합니다. 실무에서 부딪히는 진짜 이슈와, 내가 이해한 방식 그대로 정리한 가이드입니다.

웹개발/Java

Java Optional 제대로 쓰고 계신가요? 실무에서 자주 하는 실수와 올바른 사용법 총정리

자바를잡아 2025. 7. 20. 08:00
반응형

Optional 남용하다가 성능 터진 썰 (feat. Java 실무 경험)

실제 실무 프로젝트 중, 전자정부프레임워크 기반의 레거시 시스템을 개선하면서 Optional을 적극 도입한 적이 있었다. NullPointerException 방지 목적이었고, 나름 Functional 스타일도 도입하고 싶어서였다. 그런데 코드 리뷰 중 “Optional을 왜 필드로 선언했냐”는 지적을 받았고, 성능 측정 결과 오히려 처리 속도가 느려지고 메모리 낭비도 발생했다. Optional은 제대로 사용하면 확실히 좋지만, 잘못 쓰면 차라리 null보다 못한 결과를 낳는다.

이번 글에서는 Java 8 이후 도입된 Optional의 정확한 개념, 쓰면 안 되는 패턴, 그리고 실무에서 자주 쓰는 올바른 방식을 정리해보겠다.

Optional이란 무엇인가?

Optional<T>값이 있을 수도 있고 없을 수도 있는 객체를 표현하기 위한 컨테이너다. 즉, null 대신 Optional을 쓰면 컴파일 단계에서부터 ‘존재 여부’를 명확히 표현할 수 있어, NullPointerException을 줄이는 데 도움이 된다.

예:

Optional<String> name = Optional.of("홍길동");

System.out.println(name.get()); // 홍길동

값이 없을 수도 있다면 Optional.empty() 또는 Optional.ofNullable(value)을 사용한다.

Optional, 이렇게 쓰면 안 됩니다 (실무에서 자주 나오는 잘못된 예)

1. DTO나 Entity 필드에 Optional 사용

// ❌ 잘못된 예시
public class User {
    private Optional<String> name;
    private Optional<Integer> age;
}

이건 절대 하면 안 된다. Optional은 필드가 아니라 메서드 반환값에만 쓰는 게 권장된다. 필드에 Optional이 있으면 직렬화, JPA, Jackson과의 호환성 문제가 발생한다.

2. Optional.get()을 바로 호출

// ❌ 위험한 예시
Optional<User> user = Optional.empty();
User u = user.get(); // NoSuchElementException 발생

Optional을 쓰는 이유는 null 체크를 안전하게 하기 위함인데, get()을 바로 쓰면 의미가 없다.

Optional의 올바른 사용법: 실무 기준 정리

1. Optional은 반환값으로만 사용하자

public Optional<User> findUserById(String id) {
    return Optional.ofNullable(userRepository.findById(id));
}

값이 없을 수도 있는 메서드의 반환값으로 Optional을 사용하면, 호출 측에서 처리 흐름이 명확해진다.

2. 기본값 처리: orElse, orElseGet

User user = findUserById("hong").orElse(new User("게스트"));

혹은 지연 생성이 필요한 경우:

User user = findUserById("hong").orElseGet(() -> createGuestUser());

3. 조건부 실행: ifPresent / ifPresentOrElse

findUserById("hong")
    .ifPresent(user -> System.out.println("이름: " + user.getName()));

Java 9 이상에선 ifPresentOrElse도 사용 가능:

findUserById("hong")
    .ifPresentOrElse(
        user -> System.out.println("회원: " + user.getName()),
        () -> System.out.println("비회원")
    );

4. 조건 필터링: filter

Optional<User> adult = findUserById("hong")
    .filter(user -> user.getAge() >= 20);

filter는 조건이 true일 때만 값을 유지하고, false면 Optional.empty()가 된다.

5. 값 변환: map / flatMap

String userName = findUserById("hong")
    .map(User::getName)
    .orElse("이름 없음");

중첩된 Optional을 다룰 땐 flatMap을 활용:

Optional<Address> address = findUserById("hong")
    .flatMap(User::getAddress);

실무 예제: 로그인 사용자 정보 조회

Spring Security에서 현재 로그인한 사용자 정보를 가져올 때 Optional을 유용하게 사용할 수 있다.

public Optional<User> getCurrentUser() {
    Authentication auth = SecurityContextHolder.getContext().getAuthentication();

    if (auth == null || !(auth.getPrincipal() instanceof UserDetails)) {
        return Optional.empty();
    }

    UserDetails userDetails = (UserDetails) auth.getPrincipal();
    return Optional.ofNullable(userService.findByUsername(userDetails.getUsername()));
}

이렇게 하면 호출부는 다음처럼 처리 가능:

getCurrentUser().ifPresent(user -> {
    log.info("현재 로그인한 사용자: {}", user.getName());
});

Optional vs null 비교 정리

항목 Optional null
의도 표현 값의 유무를 명시 함축적 (명시적 아님)
실수 방지 컴파일 타임에서 에러 줄임 NullPointerException 발생 가능
성능 약간 느릴 수 있음 빠름
사용 위치 반환값에 한정 모든 곳

마무리: Optional, 잘 쓰면 득이고 잘못 쓰면 독

Optional은 분명 Java에 필요한 기능이다. 특히 값이 없을 수 있다는 걸 명확하게 표현하고, null-safe한 코드를 작성할 수 있게 해준다. 하지만 DTO나 Entity 필드에 넣는다거나, Optional.get()을 무심코 사용하는 경우엔 null보다 위험할 수 있다.

이 글이 Optional을 실무에서 어떻게 활용하고, 어디까지 쓰는 게 좋은지 판단하는 데 도움이 되었길 바란다. 다음 코드 작성부터는 Optional을 '기능'이 아닌 '약속'으로 써보자.

반응형