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은 리턴타입으로만 사용하면 충분!
'Programing' 카테고리의 다른 글
[Java] 이미지/파일을 읽는 대표적인 두 가지 방법 (1) | 2025.05.14 |
---|---|
[Java] 함수형 인터페이스와 람다식 (0) | 2025.05.09 |
[Java] IO vs NIO (1) | 2025.04.30 |
[Java] Stream 한방 정리 (👆JDK_1.8) (0) | 2025.04.29 |
메모리 덤프(dump) 분석 [ jps, jmap, jhat ] (0) | 2022.05.16 |