본문 바로가기
이직&취업/알고리즘

Optional 클래스는 언제 사용해야 하나요?

by journeylabs 2025. 4. 7.
728x90
반응형

목차

    💥 null 때문에 멘탈 나가지 마세요! Java Optional 클래스 사용가이드

    혹시 이런 경험 있으신가요? 열심히 코딩했는데, 런타임에 NullPointerException이 빵! 하고 터져서 디버깅 지옥에 빠지는 상황... 😱 Java 개발자라면 누구나 겪는 흔한 일이죠. 하지만 이제 걱정 마세요! Optional이 여러분의 코드를 더욱 안전하고 깔끔하게 만들어줄 겁니다.

    자바 8에서 도입된 Optional 클래스는 NullPointerException(NPE)을 방지하고 코드의 안정성을 높이는 데 유용한 도구입니다

     

    1. Optional 클래스란? 주요 개념과 특징

    Optional은 Java 8부터 도입된 클래스로, 값이 있을 수도 있고 없을 수도 있는 컨테이너 객체입니다. 쉽게 말해, Optional은 null이 될 수 있는 값을 감싸서, null 처리를 더 명시적으로, 그리고 안전하게 할 수 있도록 도와주는 역할을 합니다.

     

    주요 개념

    • 컨테이너 객체 (Container Object): 다른 객체를 담는 객체를 의미합니다. Optional은 특정 타입의 값을 담는 컨테이너 역할을 합니다.
    • null-safety (널 안전성): null pointer exception이 발생할 가능성을 줄여주는 것을 의미합니다.
    • isPresent(): Optional 객체 안에 값이 존재하는지 확인하는 메서드입니다.
    • get(): Optional 객체 안에 있는 값을 가져오는 메서드입니다. (주의해서 사용해야 합니다!)
    • orElse(T other): Optional 객체에 값이 없으면 other 값을 반환하는 메서드입니다.
    • orElseGet(Supplier<? extends T> other): Optional 객체에 값이 없으면 Supplier를 통해 생성된 값을 반환하는 메서드입니다.
    • orElseThrow(Supplier<? extends X> exceptionSupplier): Optional 객체에 값이 없으면 지정된 예외를 발생시키는 메서드입니다.

    특징

    • NullPointerException 방지: Optional을 사용하면 null 체크를 명시적으로 할 수 있어 NullPointerException 발생 가능성을 줄여줍니다.
    • 가독성 향상: 값이 없을 수 있다는 것을 코드 레벨에서 명확하게 표현하여 코드의 의도를 더 잘 드러냅니다.
    • 메서드 체이닝 지원: Optional은 다양한 메서드를 제공하여 함수형 프로그래밍 스타일로 코드를 작성할 수 있도록 돕습니다.
    • 불필요한 null 체크 감소: Optional을 사용하면 null 체크 로직을 간결하게 만들 수 있습니다.

    2. Optional 예시와 코드 설명

    예시 1. Optional 객체 생성하기

    import java.util.Optional;
    
    public class OptionalExample {
    
        public static void main(String[] args) {
            // 1. 값이 있는 Optional 객체 생성
            Optional<String> name = Optional.of("홍길동");
            System.out.println("name: " + name); // name: Optional[홍길동]
    
            // 2. null 값을 허용하는 Optional 객체 생성 (null이 들어갈 수 있다면 ofNullable 사용)
            Optional<String> nullableName = Optional.ofNullable(null);
            System.out.println("nullableName: " + nullableName); // nullableName: Optional.empty
    
            // 3. 비어있는 Optional 객체 생성
            Optional<String> emptyName = Optional.empty();
            System.out.println("emptyName: " + emptyName); // emptyName: Optional.empty
        }
    }

     

    📌 코드 설명:

    • Optional.of(value): null이 아닌 확실한 값을 가진 Optional 객체를 생성합니다. 만약 value가 null이면 NullPointerException이 발생합니다.
    • Optional.ofNullable(value): value가 null일 수도 있는 경우에 사용합니다. value가 null이면 비어있는 Optional 객체를 생성합니다.
    • Optional.empty(): 비어있는 Optional 객체를 생성합니다.

    예시 2. Optional 값 가져오기 (안전하게!)

    import java.util.Optional;
    
    public class OptionalExample {
    
        public static void main(String[] args) {
            Optional<String> name = Optional.of("홍길동");
            Optional<String> emptyName = Optional.empty();
    
            // 1. isPresent() 메서드로 값 존재 여부 확인 후 get() 메서드 사용
            if (name.isPresent()) {
                System.out.println("name value: " + name.get()); // name value: 홍길동
            }
    
            // 2. orElse() 메서드로 값이 없을 경우 대체값 지정
            String nameOrElse = emptyName.orElse("이름 없음");
            System.out.println("nameOrElse: " + nameOrElse); // nameOrElse: 이름 없음
    
            // 3. orElseGet() 메서드로 값이 없을 경우 Supplier를 통해 대체값 생성
            String nameOrElseGet = emptyName.orElseGet(() -> "Anonymous");
            System.out.println("nameOrElseGet: " + nameOrElseGet); // nameOrElseGet: Anonymous
    
            // 4. orElseThrow() 메서드로 값이 없을 경우 예외 발생
            try {
                String nameOrElseThrow = emptyName.orElseThrow(() -> new IllegalArgumentException("이름이 없습니다."));
            } catch (IllegalArgumentException e) {
                System.out.println("예외 발생: " + e.getMessage()); // 예외 발생: 이름이 없습니다.
            }
        }
    }

     

    📌 코드 설명:

    • isPresent(): Optional 객체에 값이 존재하는지 확인합니다. 값이 존재하면 true, 없으면 false를 반환합니다. get() 메서드를 사용하기 전에 반드시 isPresent()로 확인해야 NullPointerException을 방지할 수 있습니다.
    • orElse(T other): Optional 객체에 값이 없으면 other 값을 반환합니다. other는 미리 준비된 기본값입니다.
    • orElseGet(Supplier<? extends T> other): Optional 객체에 값이 없으면 Supplier를 통해 생성된 값을 반환합니다. Supplier는 필요할 때만 값을 생성하므로, orElse()보다 효율적일 수 있습니다.
    • orElseThrow(Supplier<? extends X> exceptionSupplier): Optional 객체에 값이 없으면 지정된 예외를 발생시킵니다. 예외 메시지를 커스터마이징할 수 있어, 디버깅에 유용합니다.

    예시 3. Optional 값 필터링 및 변환

    import java.util.Optional;
    
    public class OptionalExample {
    
        public static void main(String[] args) {
            Optional<String> name = Optional.of("홍길동");
    
            // 1. filter() 메서드로 특정 조건에 맞는 값만 Optional 객체에 유지
            Optional<String> filteredName = name.filter(n -> n.startsWith("홍"));
            System.out.println("filteredName: " + filteredName); // filteredName: Optional[홍길동]
    
            Optional<String> filteredName2 = name.filter(n -> n.startsWith("김"));
            System.out.println("filteredName2: " + filteredName2); // filteredName2: Optional.empty
    
            // 2. map() 메서드로 Optional 객체 안의 값을 변환
            Optional<Integer> nameLength = name.map(String::length);
            System.out.println("nameLength: " + nameLength); // nameLength: Optional[3]
    
            // 3. flatMap() 메서드로 Optional 객체 안의 값을 Optional 객체로 변환 (중첩된 Optional 방지)
            Optional<Optional<String>> nestedOptional = Optional.of(Optional.of("이순신")); // 끔찍하다!
            Optional<String> flatOptional = nestedOptional.flatMap(o -> o); // 깔끔하게 해결!
            System.out.println("flatOptional: " + flatOptional); // flatOptional: Optional[이순신]
        }
    }

     

    📌 코드 설명:

    • filter(Predicate<? super T> predicate): Optional 객체 안의 값이 주어진 조건을 만족하면 해당 값을 그대로 유지하고, 그렇지 않으면 비어있는 Optional 객체를 반환합니다.
    • map(Function<? super T, ? extends U> mapper): Optional 객체 안의 값을 주어진 함수를 통해 변환하고, 변환된 값을 담은 새로운 Optional 객체를 반환합니다.
    • flatMap(Function<? super T, Optional<U>> mapper): map()과 유사하지만, 함수가 Optional 객체를 반환할 때 사용합니다. Optional 안에 Optional이 중첩되는 것을 방지해 줍니다.

    3. 실무 코딩에서 Optional 적용 예시

    Optional데이터베이스 조회, API 응답 처리, 설정 값 관리 등에서 실무적으로 유용합니다.

     

    예시 1. 사용자 조회

    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    import java.util.Optional;
    
    @Service
    public class UserService {
        @Autowired
        private UserRepository userRepository;
    
        public String getUserName(Long userId) {
            Optional<User> user = userRepository.findById(userId); // Optional로 반환
            return user.map(User::getName)                         // 이름 추출
                    .orElse("Guest");                              // 없으면 "Guest"
        }
    
        static class User {
            private Long id;
            private String name;
    
            public User(Long id, String name) {
                this.id = id;
                this.name = name;
            }
    
            public String getName() { return name; }
        }
    
        static class UserRepository {
            public Optional<User> findById(Long id) {
                // DB 모의 조회: id=1인 경우만 사용자 반환
                if (id.equals(1L)) {
                    return Optional.of(new User(1L, "Alice"));
                }
                return Optional.empty();
            }
        }
    
        public static void main(String[] args) {
            UserService service = new UserService();
            service.userRepository = new UserRepository();
            System.out.println(service.getUserName(1L)); // 출력: Alice
            System.out.println(service.getUserName(2L)); // 출력: Guest
        }
    }

     

    📌 주요 연산 분석:

    • userRepository.findById(userId): DB에서 사용자 조회, Optional<User> 반환.
      • JPA의 findById는 기본적으로 Optional을 사용.
    • .map(User::getName): User 객체에서 이름 추출.
      • 값이 있으면 이름 반환, 없으면 다음 단계로.
    • .orElse("Guest"): 사용자가 없으면 "Guest" 반환.

    📌 실무 활용:

    • Spring Data JPA와 통합, null 대신 Optional로 안전한 처리.
    • 컨트롤러에서 호출해 API 응답으로 활용 가능.

    예시 2. API 응답 처리

    @GetMapping("/users/{id}")
    public ResponseEntity<User> getUser(@PathVariable Long id) {
        Optional<User> user = userService.findUserById(id);
    
        return user.map(ResponseEntity::ok) // User가 있으면 200 OK
                   .orElse(ResponseEntity.notFound().build()); // 없으면 404 Not Found
    }
    • API 응답으로 데이터를 반환할 때, 데이터가 없을 경우 Optional을 사용하여 클라이언트에게 명확하게 정보를 전달할 수 있습니다.

    예시 3. 설정 파일 로드

    • 설정 파일에서 특정 값을 읽어올 때, 해당 값이 없을 경우 Optional을 사용하여 기본값을 설정하거나 예외를 발생시킬 수 있습니다.

    4. Optional의 주의사항: 장점과 단점

    장점

    • NPE 방지: null 직접 접근을 줄여 안정성 향상.
    • 명확한 의도: "값이 없을 수 있다"를 코드로 표현.
    • 간결한 코드: if-else 대신 체이닝으로 처리.
    • 함수형 호환: 스트림 API와 잘 어울림.

    단점

    • 과용 위험: 모든 변수에 Optional을 남발하면 코드 복잡성 증가.
    • 성능 오버헤드: 객체 생성으로 약간의 메모리/속도 비용 발생.
    • 제한된 용도: 단일 값만 처리, 복잡한 로직에는 부적합.
    • 학습 곡선: 초보자에게는 직관적이지 않을 수 있음.

    주의사항

    • 필수 값에는 사용 금지: 반드시 값이 있어야 하는 경우 null 체크가 더 적합.
    • 중첩 Optional 피하기: Optional<Optional<T>> 같은 구조는 혼란 초래.
    • 불필요한 래핑 자제: 단순 변수에 Optional을 과도하게 사용하지 말 것.

    5. 결론

    Optional은 null 처리를 더욱 안전하고 명확하게 만들어주는 강력한 도구입니다. 하지만 모든 상황에서 Optional을 사용하는 것이 정답은 아닙니다. Optional의 장단점을 잘 이해하고, 상황에 맞게 적절하게 사용한다면 코드 퀄리티를 크게 향상시킬 수 있습니다.

     

    이제 여러분도 Optional을 자유자재로 활용하여 NullPointerException 걱정 없는 깔끔한 코드를 작성해 보세요!

    728x90
    반응형