[Java] Optinal 대체 왜 쓰는데?

한솔·2025년 7월 18일

아무리 봐도 Optional이 이해가 안 됐고 와닿지가 않아서 하루종일 이것만 공부해 보기로 했다.

왜 이해를 못했냐면

*NPE를 예방하기 위해서라면, 결국 에러 발생되게 만드는 용도아닌가?란 생각이 들었고,
에러를 임의로 발생시키도록 하는 예외처리가 따로 있는데.
그리고 null이던 말던 왜 신경을 써야 하는지에 대해 불만이 있었다.


NPE(nullPointerException)
아무 객체도 가리키지 않는 참조에서 메서드나 필드에 접근하려 할 때 발생하는 자바 런타임 에러.
왜 발생하는데?=> 자바는 null이라는 특별 값에 대해 아무것도 없다라고 표시하는데, 
그 상태에서 length(), toUpperCase(), .charAt(0)같은 메서드를 호출하면 
자바가 어떻게 실행하지?하고 터뜨리는 에러가 NPE.

그러나 Java에서는 null을 그대로 냅두게 되면, 여러문제를 일으킬 수 있다.

Java에서 null 참조를 그대로 두면 어디서 NPE가 터질지 모르는 문제와 중첩된 if (x != null) 처리의 복잡함을 야기한다. Optional은 ‘값 없음’을 안전하게 캡슐화해서, NPE 없이 깔끔하게 널 체크 흐름을 작성할 수 있게 도와준다.

위 내용을 정리해보면

  • null이면 메서드 호출 때 NullPointerException이 발생된다.
  • 이걸 일일이 if(x!=null)으로 막으면 코드가 길어진다.
  • 그래서 Optional.ofNullable(str)해야 하며, 코드는 간결해진다.

주의사항

  • 절대 get()만 믿고 쓰지 말 것, 반드시 isPresent()나 orElse...계열로 안전하게 처리 할 것.
  • 필드나 파라미터 타입으로 Optional 남발은 피하고 주로 메서드 반환용으로 사용 할 것.
  • of() vs ofNullable() 처리를 명확히 인지 할 것.

위 내용에 대해 설명하고자 한다.


1) 절대 get() 만 믿고 쓰지 말 것 (isPresent()/ orElse.. 함께 사용 권장)

  • opt.get()은 Optional 내부에 값이 있을 때는 그 값을 꺼내주지만, 값이 없을 경우에는 호출하는 순간 NoSuchElementException이 터진다.
  • 그래서 안전하게 쓰려면 아래 코드와 같다
if(opt.isPresent()){
	String v=opt.get();
}
  • 혹은
// 값이 있으면 그대로, 없으면 기본값 반환
String v=opt.orElse("기본");
String v=opt.orElseGet(()->"기본");

요약하자면, get() 단독 사용은 예외 위험이 크니 항상 "값 있음" 여부를 확인하거나 isPresent() 아니면 기본값을 지정해주는 orElse/ orElseGet 방식을 쓰라는 말이다.

2) 필드나 파라미터 타입으로 Optional 남발은 피하고 주로 메서드 반환용으로 사용 할 것.

  • 필드(클래스 변수)나 메서드 파라미터에 Optional을 쓰면 오히려 코드가 복잡해지고 직렬화나 테스트 작성 시 번거로워 진다.
// 권장하지 않는 예시코드
public class User{
	private Optional<String> middleName;
	public void setMiddleNaem(Optional<String> m){...}
}
  • 대신 Optional은 "이 메서드는 값이 없을 수도 있을 수도 있다."를 표현하는 용도로 메서드 반환타입에만 사용하는 것이 일반적인 관례다.
//권장되는 예시 코드
public Optional<User> findUserByEmail(String email){...}

요약하자면, Optional을 필드, 파라미터로 쓰면 오히려 부작용이 많으니 메서드 리턴으로만 쓰고 *내부 구현이나 호출 시에는 일반 Null체크를 쓰는 걸 추천한다.

*내부 구현이나 호출 시에는 일반 Null체크를 쓰는 걸 추천, 
일반 null체크란? 참조형 변수가 null인지 if문으로 먼저 확인한 뒤에야 안전하게 사용하는 방식이다.
예를들면 필드처리, 메서드 파라미터, 호출.

즉, 여기서 내부 구현이란, 클래스 안에서 필드나 파라미터에 Optioanl을 쓰지않고
if(param != null)/ if(field != null)으로 단순히 널 여부만 체크하는 방식을 말한다.
호출 시에만 Optional API를 사용해 주면, 인터페이스 설계와 구현이 깔끔하게 분리된다.

잠시 필드처리, 메서드 파라미터, 호출에 대해 설명해보자면 아래와 같다.

일반 Null 체크 1)필드 처리 예시

  • 잘못된 예 (Optional 필드 남발)
public class User{
//Optional을 필드로 쓰면 직렬화·테스트·가독성이 모두 불편해짐
	private Optional<String> middleName;

	public User(Optional<String> middleName){
		this.middleName=middleName;
	}

	public Optional<String> getMiddleName(){
		return middleName;
	}
}
  • 권장 예(null 체크로 간단히)
public class User{
	private String middleName;

	public User(String middleName){
		this.middleName=middleName != null?middleName:"";
	}

	public String getMiddleName(){
		return middleName != null && !middleName.isEmpty()
		?middleName
		:"정보 없음";
	}
}

일반 Null 체크 2) 메서드 파라미터 예시

잘못된 예(Optional 파라미터)

public void updateName(Optional<String> newNameOpt){
	newNameOpt.ifPresent(n->this.name=n);
}

권장 예(null체크 파라미터)

public void updateName(String newName){
//호출 시 null인지 직접 검사
	if(newName !=null && !newName.isBlack()){
		this.name=newName;
	}//else: 그대로 두거나 기본 처리.
}

일반 Null 체크 3) 호출 시 예시

// Optional 반환 메서드
public Optional<User> findUserById(String id){...}

//호출할 때
Optional<User> userOpt=repo.findUserById(id);

// ->Optional API로 안전하게 처리
if(userOtp.isPresent()){
	User u=userOpt.get();
	//내부적으로는 null 체크 없이 바로 사용 가능
	System.out.println(u.getName());
}else{
	System.out.println("사용자를 찾을 수 없습니다.");
}

일반 NULL체크에 대한 설명은 여기서 마무리하고 본론으로 돌아가자.

3) of() vs ofNullable() 처리를 명확히 인지 할 것

  1. Optional.of(value)
  • value가 절대 null이 아님을 보장할 때만 사용한다.
  • 만약 value가 null이면 NPE발생!
  1. Optional.ofNullable(value)
  • value가 null일 수도 아닐 수도 있을 때 사용한다.
  • value가 null이면 빈 Optional(Optional.empty())이 생성되고
  • value가 null이 아니면, 값을 담은 Optional이 생성된다.

요약하자면, null이 아님이 확실할 때 of() 사용, null 가능성이 조금이라도 있으면 ofNullable() 사용해주면 된다.


따라서

  • NPE을 예방하기 위해서라면, 결국 에러 발생시키는 용도 아닌가?란 생각이 들었고, ⇒ Optional은 에러 대신 “값 없음”을 반환해 주며, NPE를 아예 없애준다.
  • 에러를 임의로 발생시키도록 하는 예외처리가 따로 있는데. ⇒ 예외처리는 비정상 상황 대응용, Optional은 정상 흐름에서 값 부재를 처리하는 API이다.
  • 그리고 null이던 말던 왜 신경을 써야 하는지에 대해 불만이 있었다. ⇒ null 방치는 예측 불가능한 NPE를 유발하므로, Optional로 사전에 “값 없음”을 명시해 안정성을 높여야 한다.

문제 풀다가 추가적으로, 이해할 필요가 있겠다란 코드가 보였다.

Optional<String> optEmpty = Optional.ofNullable(null);

// orElse는 기본값을 즉시 호출합니다.
String a = optEmpty.orElse(defaultValue());  
// "기본값 생성"이 즉시 출력되고 "DEF" 반환

// orElseGet은 기본값을 필요할 때만 호출합니다.
String b = optEmpty.orElseGet(() -> defaultValue());  
// "기본값 생성"이 "DEF" 반환 시점에 출력

예제코드

		// 1) of() – null이 절대 아닐 때만
        Optional<String> optA = Optional.of("A값");
        // get단독사용한 이유: null 아님이 100%이기 때문.
        System.out.println("optA: " + optA.get());  // safe, 값 있음

        // 2) ofNullable() – null 가능
        String maybeB = null;
        Optional<String> optB = Optional.ofNullable(maybeB);

        // 3) empty() – 빈 Optional
        Optional<String> optEmpty = Optional.empty();

        System.out.println("-----------");

        // 4) isPresent() + get() => null 가능성이 조금이라도 있기 때문에 
        //isPresent로 먼저 확인 후 get씀
        if (optA.isPresent()) {
            System.out.println("optA 존재: " + optA.get());
        }
        if (!optB.isPresent()) {
            System.out.println("optB 비어있음");
        }

        System.out.println("-----------");

        // 5) ifPresent() – 값 있을 때만 실행
        optA.ifPresent(v -> System.out.println("ifPresent optA: " + v));
        optB.ifPresent(v -> System.out.println("ifPresent optB: " + v));  // 실행 안 됨

        System.out.println("-----------");

        // 6) orElse() – 값 없으면 기본값
        System.out.println("optB orElse: " + optB.orElse("기본B"));

        // 7) orElseGet() – 값 없을 때만 supplier 실행
        System.out.println("optEmpty orElseGet: " +
            optEmpty.orElseGet(() -> {
                System.out.println("  supplier 실행!");
                return "Lazy기본";
            })
        );

        System.out.println("-----------");

        // 8) filter() + map()
        Optional<String> optNum = Optional.of("123");
        Optional<Integer> optLen = optNum
            .filter(s -> s.length() > 2)  // 길이 > 2만 통과
            //map(s -> s.length())와 map(String::length); 일치 함
            .map(String::length);         // 길이를 Integer로 매핑
        System.out.println("filter+map 결과: " + optLen.orElse(0));
// 출력결과
optA: A값
-----------
optA 존재: A값
optB 비어있음
-----------
ifPresent optA: A값
-----------
optB orElse: 기본B
  supplier 실행!
optEmpty orElseGet: Lazy기본
-----------
filter+map 결과: 3

옵셔널 관련 문제

// 문제 4-1: Optional 입력값 처리
// 1) Scanner로 사용자로부터 문자열 input을 한 줄 입력받으세요.
// 2) Optional.ofNullable(input)으로 Optional<String> opt을 생성하세요.
// 3) opt.ifPresentOrElse를 사용해
//    값이 있으면 "입력값: {값}"을, 없으면 "값이 없습니다."를 출력하세요.

import java.util.Optional;
import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        // 여기에 코드 작성하세요
       
        
        sc.close();
    }
}

// 문제 4-2: Optional.of() 예외 확인
// 1) Optional<String> optA = Optional.of("ABC"); 선언
// 2) try-catch 구문 안에서 Optional.of(null)을 호출해 보세요.
// 3) catch(NullPointerException e) 블록에서
//    "of(null) 호출 시 예외 발생!"을 출력하세요.

import java.util.Optional;

public class Main {
    public static void main(String[] args) {
        // 여기에 코드 작성하세요
    }
}

// 문제 4-3: orElse vs orElseGet 비교
// 1) Optional<String> optEmpty = Optional.ofNullable(null); 선언
// 2) String a = optEmpty.orElse(defaultValue());
// 3) String b = optEmpty.orElseGet(() -> defaultValue());
// 4) defaultValue() 메서드를 정의하고,
//    내부에서 "기본값 생성"을 출력한 뒤 "DEF"를 반환하세요.
// 5) a와 b를 차례로 출력해 보세요.

import java.util.Optional;

public class Main {
    public static void main(String[] args) {
        // 여기에 코드 작성하세요
    }

    // 여기에 defaultValue() 메서드 작성
}

//[TIP]
// orElse(): 객체에 값이 없을때 지정한 기본값을 즉시 반환
//여기서 defaultValue()메서드는 orElse()가 호출될 때 즉시 실행됨

//orElseGet() :  객체에 값이 없을 때 람다식 사용하여 기본값을 계산하는 방식
//defaultValue()는 기본값을 반환하는 메서드,값이 없을 때 대체할 기본값을 생성하는 메서드

//람다식을 사용하면, orElse()와는 달리 값이 없을 때만 defaultValue() 메서드를 실행하게 됩니다.
// 그렇기 때문에 값이 있을 경우에는 defaultValue() 메서드가 호출되지 않습니다.

// 문제 4-4: Optional 필터 및 매핑
// 1) Optional<String> emailOpt = Optional.ofNullable("user@example.com"); 선언
// 2) filter(e -> e.contains("@"))로 검증 후
// 3) map(e -> e.substring(e.indexOf("@") + 1))로 도메인만 추출하세요.
// 4) orElse("도메인 없음")으로 최종 결과를 출력하세요.

import java.util.Optional;

public class Main {
    public static void main(String[] args) {
        // 여기에 코드 작성하세요
    }
}

// 문제 4-5: 중첩 Optional 처리 (flatMap)
// 아래 Address, User 클래스를 정의하고
// 1) User user = new User("홍길동", null); 생성
// 2) Optional.ofNullable(user)
//       .flatMap(u -> Optional.ofNullable(u.addr))
//       .map(a -> a.city)
//       .orElse("주소 정보 없음")
//    를 이용해 결과를 출력하세요.

import java.util.Optional;

class Address {
    String city;
    Address(String city) { this.city = city; }
}

class User {
    String name;
    Address addr;
    User(String name, Address addr) {
        this.name = name;
        this.addr = addr;
    }
}

public class Main {
    public static void main(String[] args) {
        // 여기에 코드 작성하세요
    }
}

0개의 댓글