Skip to content
Woo Jin Jang edited this page Jun 8, 2025 · 8 revisions

📚 Optional이 왜 필요한지?

  • NPE(NullPointerException) 문제
    • 자바에서 null은 값이 없음을 표현하는 가장 기본적인 방법이다.
    • 하지만 null을 잘못 사용하거나 null 참조에 대한 메서드를 호출하면 NPE가 발생하여 프로그램이 예기치 않게 종료될 수 있다.
    • 특히 여러 메서드가 연쇄적으로 호출되어 null 체크가 누락되면 추적이 어렵고 디버깅 비용이 증가한다.
  • 가독성 저하
    • null 체크 로직이 누적되면 코드가 복잡해지고 가독성이 떨어진다.
  • 의도가 드러나지 않음
    • 메서드 시그니처만 보고 이 메서드가 null을 반환할 수도 있다는 사실을 명확히 파악하기 어렵다.
    • 호출하는 입장에서는 반드시 값이 존재할 것이라고 가정했다가 런타임에 null이 나와서 문제가 생길 수 있다.
  • Optional의 등장
    • 이러한 문제를 해결하고자 자바 8부터 Optional 클래스가 도입됐다.
    • Optional은 값이 있을수도 있고 없을수도 있음을 명시적으로 표현해주어 메서드의 계약이나 호출 의도를 좀 더 분명하게 드러낸다.
    • Optional을 사용하면 "빈 값"을 표현할 때 더 이상 null 자체를 넘겨주지 않고 Optional.empty()처럼 의도를 드러내는 객체를 사용할 수 있다.
    • null 체크 로직을 간결하게 만들고 특정 경우에 NPE가 발생할 수 있는 부분을 빌드 타임이나 IDE, 코드 리뷰에서 더 쉽게 파악할 수 있게 해준다.

📚 Optional 생성과 값 획득 방법

[Optional 값 생성]

  • Optional.of(T value)
  • Optional.ofNullable(T value)
  • Optional.empty()

[Optioanl 값 획득]

  • isPresent(), isEmpty()
  • get()
  • orElse(T other)
  • orElseGet(Supplier<? extends T> supplier)
  • orElseThrow()
  • or(Supplier<? extends Optional<? extends T>> supplier)
  • get()메서드는 Optional 사용 시 가능하면 피해야 한다. 왜냐하면 값이 없는 상태에서 get()을 호출하면 예외가 터지기 때문에 안전하게 사용하려면 isPresent()와 같은 사전 체크가 반드시 필요하다.
  • get()보다는 orElse(), orElseGet(), orElseThrow() 등의 메서드를 활용하면 좀 더 세련되고 안전하게 값을 처리할 수 있다.

📚 Optional 값 처리 방법

  • ifPresent()
    • 값이 존재하면 action 실행
    • 값이 없으면 아무것도 안 함
  • ifPresentOrElse()
    • 값이 존재하면 action 실행
    • 값이 없으면 emptyAction 실행
  • map()
    • 값이 있으면 mapper를 적용한 결과 반환
    • 값이 없으면 Optional.empty() 반환
  • flatMap()
    • map과 유사하나 Optional을 반환할 때 중첩되지 않고 평탄화해서 반환
  • filter()
    • 값이 있고 조건을 만족하면 그대로 반환
    • 조건 불만족이거나 값이 없다면 Optional.empty() 반환
  • stream()
    • 값이 있으면 단일 요소를 담은 Stream 반환
    • 값이 없으면 빈 스트림 반환

📚 즉시 평가와 지연 평가

  • 즉시 평가(eager evaluation)
    • 값(혹은 객체)을 바로 생성하거나 계산해 버리는 것
  • 지연 평가(lazy evaluation)
    • 값이 실제로 필요할 때까지 계산을 미루는 것

📚 orElse() vs orElseGet()

import java.util.Optional;
import java.util.Random;

public class OrElseGetMain {
	public static void main(String[] args) {

		Optional<Integer> optValue = Optional.of(100);
		Optional<Integer> optEmpty = Optional.empty();
		System.out.println("단순 계산");

		Integer i1 = optValue.orElse(10 + 20); // 10 + 20 계산 후 버림
		Integer i2 = optEmpty.orElse(10 + 20); // 10 + 20 계산 후 사용
		System.out.println("i1 = " + i1);
		System.out.println("i2 = " + i2);

		// 값이 있으면 그 값, 없으면 지정된 기본값 사용
		System.out.println("=== orElse ===");
		System.out.println("값이 있는 경우");
		Integer value1 = optValue.orElse(createData());
		System.out.println("value1 = " + value1);
		System.out.println("값이 없는 경우");
		Integer empty1 = optEmpty.orElse(createData());
		System.out.println("empty1 = " + empty1);

		// 값이 있으면 그 값, 없으면 지정된 람다 사용
		System.out.println("=== orElseGet ===");
		System.out.println("값이 있는 경우");
		Integer value2 = optValue.orElseGet(() -> createData());
		System.out.println("value2 = " + value2);
		System.out.println("값이 없는 경우");
		Integer empty2 = optEmpty.orElseGet(() -> createData());
		System.out.println("empty2 = " + empty2);
	}

	public static int createData() {
		System.out.println("데이터를 생성합니다...");
		try {
			Thread.sleep(3000);
		} catch (InterruptedException e) {
			throw new RuntimeException(e);
		}
		int createValue = new Random().nextInt(100);
		System.out.println("데이터 생성이 완료되었습니다. 생성 값: " + createValue);
		return createValue;
	}
}

실행 결과

단순 계산
i1 = 100
i2 = 30
=== orElse ===
값이 있는 경우
데이터를 생성합니다...
데이터 생성이 완료되었습니다. 생성 값: 39
value1 = 100
값이 없는 경우
데이터를 생성합니다...
데이터 생성이 완료되었습니다. 생성 값: 33
empty1 = 33
=== orElseGet ===
값이 있는 경우
value2 = 100
값이 없는 경우
데이터를 생성합니다...
데이터 생성이 완료되었습니다. 생성 값: 51
empty2 = 51
  • orElse(T other)는 빈 값이면 other를 반환하는데 이 때, other를 항상 미리 계산한다.
    • 따라서 other를 생성하는 비용이 큰 경우 실제로 값이 있을 때도 쓸데없이 생성 로직이 실행될 수 있다.
    • orElse()에 넘기는 표현식은 즉시 평가하므로 즉시 평가가 적용된다.
  • orElseGet(Supplier supplier)은 빈 값이면 supplier를 통해 값을 생성하기 때문에 값이 있을 경우에는 supplier가 호출되지 않는다.
    • 생성 비용이 높은 객체를 다룰 때는 orElseGet()이 더 효율적이다.
    • orElseGet()에 넘기는 표현식은 필요할 때만 평가하므로 지연 평가가 적용된다.

❗생성 비용이 높다는 말이 무슨 뜻?

  • 복잡한 초기화 작업이 필요한 경우
    • 객체 생성 시 내부적으로 많은 계산이나 리소스 준비가 필요할 경우
    • Ex. DB 커넥션 생성, 파일 읽기, 네트워크 연결
  • 메모리 사용량이 많은 경우
    • 큰 배열이나 컬렉션을 포함하고 있어 메모리를 많이 사용하는 객체
  • GC 부담이 커지는 경우
    • 객체를 자주 생성하고 버리면 GC가 자주 일어나 성능이 저하
  • 생성 과정에서 외부 시스템과 연동되는 경우
    • 객체 생성 시 외부 API를 호출하는 경우, 디스크 접근, 로깅

📚 실무에서의 Optional Best Practice

📖 Java

📖 Kotlin

📖 Coroutine

📖 Spring

📖 Spring Security

📖 Spring Batch

📖 Reactive Programming

📖 Database

📖 MySQL

📖 Redis

📖 JPA

📖 QueryDsl

📖 MSA

📖 Kafka

📖 Apache Flink

  • [Apache Flink - Apache Flink Architecture]
  • [Apache Flink - Stream Processing]
  • [Apache Flink - Data Stream API & Window]
  • [Apache Flink - State Management]

📖 HTTP

📖 AWS

📖 Docker

📖 Kubernetes

📖 CI/CD

📖 Nginx

📖 Monitoring🥈

  • [Monitoring - Log Concept]
  • [Monitoring - Log Level & Filter]
  • [Monitoring - Logback]
  • [Monitoring - Log Collection with ELK Stack]
  • [Monitoring - Log Monitoring with Kibana]
  • [Monitoring - Building a Monitoring System with Spring Boot Actuator]
  • [Monitoring - Server Monitoring with Prometheus and Grafana with Discord Alerts]

📖 Test

📖 Effective Java 3/E

📖 Kotlin Academy - Effective Kotlin

📖 Kotlin Academy - 핵심편

📖 스프링으로 시작하는 리액티브 프로그래밍

📖 가상 면접 사례로 배우는 대규모 시스템 설계 기초 1

📖 가상 면접 사례로 배우는 대규모 시스템 설계 기초 2

📖 Clean Code

📖 리팩토링 2판

📖 주니어 백엔드 개발자가 반드시 알아야 할 실무 지식

📖 GraphQL

Clone this wiki locally