본문 바로가기
Programing

[Java] Optional Class

by 윾수 2025. 5. 8.

1. Optional<T> 이란 무엇인가??

✅ 등장 배경

Java는 오랫동안 null을 값의 부재를 나타내는 수단으로 사용했다. 하지만 null은 다음과 같은 문제점을 가지고 있다.

  • NullPointerException(NPE) 발생 위험
  • 반복적인 null 체크로 코드의 가독성이 떨어짐

그래서 Java 8버전 부터 Optional<T> 클래스가 도입됐다!

"값이 있을 수도 있고 없을 수도 있는 컨테이너" 라는 개념으로, 명시적으로 '비어있음'을 표현할 수 있도록 설계됨.

즉, Optional은 null을 직접 쓰는 대신, 값이 존재할 수도 있고 존재하지 않을 수도 있다는 걸 타입으로 표현하게 해주는 도구!

전통적인 null 사용 방식 Optional<T>
값이 존재하지 않음을 표현 하는 방식이 명확하지 않음 값이 존재함 / 존재하지 않음을 명시적으로 표현 가능
NullPointerException(NPE)의 주요 원인 Null-safe한 포장 클래스로 NPE 발생을 방지하고
값이 없을 경우에도 명시적으로 처리할 수 있음
반복적인 null 체크 방식으로 가독성이 떨어짐 향상된 가독성과 유지보수성 제공

✅ JDK 버전별 특징

버전 내용
Java 8 Optional이 처음 등장한 버전, 주요 기본 기능들이 도입
Java 9 Optional에 몇 가지 유틸리티성 메서드 추가됨 (ifPresentOrElse, or, stream)
Java 10+ Optional 자체는 변화가 없고, jdk의 발전으로 활용법이 더 유연해졌다 (현재 최신 LTS 21 까지도)

2. null 처리 방식의 한계와 Optional<T>에서의 처리 비교 예시

✅ NullPointerException 대처

null 사용

String name = null;
int length = name.length(); // NPE발생

위와 같은 코드에서는 name이 null일 때 명시적인 null 체크를 없이 바로 메서드 호출을 하고 있다.

이로인해 NullPointerException이 발생하고 이로인해 프로그램이 예기치 않게 종료될 수 있다.

 

Optional 사용

Optional<String> name = Optional.ofNullable(null);
int length = name.map(String::length).orElse(0);  // 값이 없으면 0을 반환

Optional을 사용하면 null체크를 명시적으로 하거나, 기본 값을 설정할 수 있기 때문에 NullPointerException을 예방할 수 있다.

✅ 코드의 가독성 및 유지보수성

null 사용

if(user!=null){
    if(user.getName()!=null){
    	System.out.println(user.getName().toUpperCase());
    }
}

위의 예시처럼 중첩된 null 체크를 하는 경우, 코드가 복잡해지고 가독성이 크게 떨어지며

여러 단계의 null 체크는 유지보수성에도 좋지 않다.

 

Optional 사용

Optional.ofNullable(user)
        .map(User::getName)
        .map(String::toUpperCase)
        .ifPresent(System.out::println);

중첩된 null 체크 없이 체이닝 방식을 통해 깔끔하고 가독성 좋은 코드를 작성할 수 있고 유지보수성도 크게 높아진다.

✅ null을 반환할 때 

null 사용

public String getUserName(User user){
    if(user==null){
    	return null;
    }
    return user.getName();
}

null을 반환할 경우, 호출한 측에서 이를 제대로 예외처리 하지 않으면 오류가 발생할 가능성이 있다.

특히 null을 반환하는 경우가 많으면 코드의 안정성이 떨어지고 null을 반환하는 의도를 제대로 파악하지 못할 수 있다.

 

Optional 사용

public static Optional<String> getUserName(User user){
    //값이 존재하지 않는다면 Optional.empty() 반환함
    return Optional.ofNullable(user).map(User::getName);
}

public static void main(String[] args){
    Optional<String> userName1 = getUserName(null);
    Optional<String> userName2 = getUserName(new User("윾수"));
	
	
    //1. Optional의 null 처리 유연함 -- 기본값이 복잡하거나 무거운 연산엔 orElseGet() 사용
    System.out.println(userName1.orElse("UnKnown")); // "UnKnown"
    System.out.println(userName2.orElse("UnKnown")); // "윾수"
    
    //2. 값이 있을 때만 출력
    userName1.ifPresent(name -> System.out.println("User name: " + name)); // 실행안됨
    userName2.ifPresent(name -> System.out.println("User name: " + name)); // "User name: 윾수"
    
    //3. 값이 있을 때와 없을 때를 구분 처리 --Java9이상
    userName2.ifPresentOrElse(
    	name -> System.out.println("User name" + name),
        () -> System.out.println("No user found");
    );
    
    //4. 값이 없으면 예외 발생
    try{
    	String name = userName1.orElseThrow(()-> new RuntimeException("User name not found"));
        System.out.println(name);
    }catch (Exception e){
    	System.out.println("예외발생!!!!!");
    }
}

위와같은 코드에서는 실제 값이 null이더라도 호출부에서 좀 더 유연하게 처리하는게 가능해진다.

요약

null 처리 방식에서 발생할 수 있는 대표적인 문제들은 NullPointerException을 비롯해 코드의 가독성 저하, 중복된 처리, 그리고 의도 불명확 문제들이다. 이러한 문제들을 해결하기 위해 Optional은 매우 유용한 도구로, 명확하고 안전하게 값을 처리할 수 있도록 도와준다.


3. 잘못된 Optional 사용법

✅ get() 메서드의 무분별한 사용

  • Optional의 get() 메서드는 값이 없을 때 NoSuchElementException을 발생 시킨다.
    , 값이 없는 Optional에서 get()을 호출하면 런타임 예외가 발생할 수 있다.
  • get()을 사용할 때는 반드시 값이 존재하는지(ifPresent())를 확인 해야 하는데,
    이방식은 결국 null 체크와 다를 바 없어서 도입 취지를 훼손한다.
  • 최대한 사용을 지양하고 orElse(), orElseGet(), ifPresent(), map() 등 다른 메서드를 활용

✅ 파라미터 / 필드에 Optional 사용 금기

  • Optional은 반환 타입으로 사용하는 것이 적합하며, 파라미터나 클래스 필드에 사용하는것은 권장되지 않는다.
  • 파라미터에 Optional을 사용하면 오히려 코드가 복잡해지고 가독성이 떨어진다.
  • 필드에 Optional을 사용하는 것도 마찬가지로, Serialize/DeSerialize, JPA 등과 호환성 문제를 야기할 수 있다.
  • Java 공식 언급으로도 "Optional을 필드나 파라미터로 절대 사용하지 말라"는 가이드가 있다.

✅ Collection과는 같이 사용 지양 하기

  • Optional은 "값이 있을 수도, 없을 수도 있다"는 의미인데, 값이 없다면 애초에 Collection에 추가하지 않으면 된다.
  • Collection에서 제네릭 타입으로 Optional을 사용한다면, 값을 꺼낼때마다 매번 Optional을 해제 해야해서
    코드가 간결하지 않고, 이로인해 실수를 발생할 수 있다.
  • 또한 매번 박싱/언박싱 작업을 하게 되므로 성능 측면에서도 좋지 않다.
  • 역시나 Java 공식 언급으로도 Optional은 리턴 타입 용도로만 사용하라고 명시 되어있다.

⛔ 어쩔수 없이 Collection과 함께 사용했을 때 대처

  • Java 8 : Stream+filter,map (가장 일반적)
  • Java 9+ : Optional.stream() 메서드를 이용해 flatMap으로 한번에 처리
List<Optional<String>> list = ...;

// Java 8
List<String> result = list.stream()
    .filter(Optional::isPresent)  // 값이 있는 Optional만 필터링
    .map(Optional::get)           // Optional에서 값 추출
    .collect(Collectors.toList());

// Java 9 이상
List<String> result = list.stream()
    .flatMap(Optional::stream)
    .collect(Collectors.toList());

4. Optional의 기본 사용법

✅ 메서드 정리

리턴 타입 메서드명 설명
Optional<T> static Optional.of(T value) null이 아닌 값을 감싸는 Optional 생성 null이면 NPE 발생
Optional<T> static Optional.ofNullable(T value) 값이 null이면 빈 Optional,
아니면 값 감싸서 리턴
Optional<T> static Optional.empty() 빈 Optional 객체 리턴
boolean isPresent() 값이 있으면 true, 없으면 false
boolean isEmpty() (java 11+)
값이 있으면 true, 없으면 false 
void ifPresent(Consumer) 값이 있으면 Consumer 실행, 없으면 아무것도 하지 않음
void ifPresentOrElse
(Consumer, Runnable)
(Java 9+)
값이 있으면 Consumer 실행 없으면 Runnable 실행
T get() 값이 있으면 반환, 없으면 NoSuchElementException
Optional<T> filter(Predicate) 값이 있고 predicate 만족하면 유지 아니면 빈 Optional 반환
Optional<U> map(Function) function 적용 후 결과값을 Optional로 감싸서 리턴
없으면 빈 Optional 리턴
Optional<U> flatMap(Fucntion) Optional을 리턴하는function 적용
결과값을 Optional 평탄화 작업후
단일 Optional<U> 리턴
없으면 빈 Optional 리턴
T orElse(T other) 값이 있으면 반환, 없으면 other 반환
T orElseGet(Supplier) 값이 있으면 반환, 없으면 supplier 실행 결과 반환
<X extends Throwable> T orElseThrow(Supplier-exception) 값이 있으면 반환, 없으면 supplier에서 예외 생성후 throw
Optional<T> or(Supplier) (Java9+)
값이 없으면 Supplier에서 새로운 Optional반환
Stream<T> stream() (Java9+)
값이 있으면 1개짜리 스트림, 없으면 빈스트림 반환
Collection과 사용은 지양해야 해서 사용 지양

 

✅ 간단 예제 코드

Optional 객체 생성 (of, ofNullable, empty)

String s1 = "hello";
Optional<String> op1 = Optional.of(s1);
System.out.println(op1); // Optional[hello]
Optional<String> op2 = Optional.ofNullable(s1);
System.out.println(op2); // Optional[hello]

String s2 = null;
try {
	Optional<String> op3 = Optional.of(s2);
}catch (Exception e) {
	System.out.println(e.getClass().getName()); //java.lang.NullPointerException
}
Optional<String> op4 = Optional.ofNullable(s2);
System.out.println(Optional.ofNullable(s2)); // Optional.empty

Optional<String> op5 = Optional.empty();
System.out.println(op5); // Optional.empty

값 존재 여부 확인 및 처리 (isPresent, isEmpty, ifPresent, ifPresentOrElse)

Optional<String> opt = Optional.of("hello");
boolean present = opt.isPresent(); // true
boolean empty = opt.isEmpty();     // false (Java 11+)

// 값이 있으면 실행,없으면 동작하지 않음
opt.ifPresent(val -> System.out.println(val)); // "hello" 
// (Java 9+) 값이 있으면 실행, 없으면 else 동작
opt.ifPresentOrElse(
        val -> System.out.println(val),        // "hello"
        () -> System.out.println("없음")        // 동작하지 않음
);

값 필터링 (filter)

Optional<String> opt = Optional.of("hello");

// 조건에 맞으면 값 유지, 아니면 빈 Optional
Optional<String> filtered = opt.filter(s -> s.startsWith("h")); // Optional[hello]
Optional<String> filtered2 = opt.filter(s -> s.startsWith("x")); // Optional.empty

값 추출 혹은 대체 (get, map, flatMap, orElse, orElseGet, orElseThrow, or)

Optional<String> opt = Optional.of("hello");
Optional<String> empty = Optional.empty();

// 값이 있으면 추출, 없으면 NoSuchElementException 발생
String get = opt.get(); // "hello"

// function 실행후 값이 있으면 Optional로 감싸서 리턴, 없으면 Optional.empty
Optional<String> map = opt.map(String::toUpperCase); // Optional["HELLO"]

Optional<Optional<String>> opt2 = Optional.of(opt);
// Optional을 리턴하는 function 실행후 값이 있으면 중첩된 Optional을 평탄화하고 리턴, 없으면 Optional.empty
Optional<String> flatMap = opt2.flatMap(optional -> optional); //Optional[hello]

//값이 있으면 추출, 없으면 other 리턴
String orElse = empty.orElse("Default"); // "Default"
//값이 있으면 추출, 없으면 supplier 실행결과 리턴
String orElseGet = empty.orElseGet(() -> "디폴트"); //"디폴트"
//값이 있으면 추출, 없으면 예외 던짐
String orElseThrow = opt.orElseThrow(RuntimeException::new); // "hello"
//(Java 9+) 값이 있으면 추출, 없으면 supplier실행해서 Optional 객체 리턴
Optional<String> or = empty.or(() -> Optional.of("옵셔널")); // Optional["옵셔널"]

기타 (equals, hashCode, toString)

import java.util.Objects;
import java.util.Optional;

public class Test {
    public static void main(String[] args) {
        Optional<Person> opt1 = Optional.of(new Person(20));
        Optional<Person> opt2 = Optional.of(new Person(20));

        //equals, hash는 제네릭타입 기준!
        boolean eq = opt1.equals(opt2);
        int hash1 = opt1.hashCode();
        int hash2 = opt2.hashCode();
        StringBuilder sb = new StringBuilder()
                .append("eq=")
                .append(eq)
                .append(", hash1=")
                .append(hash1)
                .append(", hash2=")
                .append(hash2)
                .append(", opt1=")
                .append(opt1)
                .append(", opt2=")
                .append(opt2);
        System.out.println(sb);
        //eq=true, hash1=20, hash2=20, opt1=Optional[Person@14], opt2=Optional[Person@14]
    }
}
class Person{
    int age;
    public Person(int age) {
        this.age = age;
    }
    @Override
    public boolean equals(Object o) {
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return age == person.age;
    }
    @Override
    public int hashCode() {
        return Objects.hashCode(age);
    }
}

 

 

 요약

파라미터, 필드로의 Optional 객체 사용과
Collection에서의 제네릭 타입으로의 사용은반드시 지양하자!
Optional은 리턴타입으로만 사용하면 충분!