Study/Java

[Java] Optional의 개념 및 주요 메서드

jonghne 2024. 1. 23. 00:15

NullPointException

자바 애플리케이션에서 흔히 발생하는 오류로 NulPointerException(이하 NPE) 이 있다. 

 

NPE 에러가 발생하는 이유는 보통 객체 참조 값이 Null인 상태에서 메서드나 필드에 접근하려고 할 때 발생하게 되는데, 런타임에 발생하는 에러이기 때문에 조심해서 처리해야 한다.

String str = null;
int length = str.length(); // NullPointerException 에러 발생 !!
System.out.println("문자열 길이 : " + length);

NPE는 JDK 8 이전에는 Null 값을 메서드 내에서 예외 처리하거나 또는 Null값을 리턴한 다음 클라이언트 코드에서 Null 체크를 하는 식으로 처리해왔는데 이 방법들은 모두 몇가지 문제가 있었다.

 

1. Null값 예외 처리 시 문제점 

 

CallStack 탐색 비용이 발생한다.

- 예외 발생 시 JVM은 Exception Handler를 찾기 위한 CallStack 탐색하게 되는데 이 과정에서 클래스명, 메서드명, 코드 줄번호 등의 정보를 수집하게 되어서 Stack Depth가 깊어질수록 비용이 많이 발생하게 된다.

 

비정상적인 Thread 종료가 발생할 수 있다.

- JVM은 Call Stack에서 예외 처리를 할 수 있는 Handler를 발견하지 못한다면 Default Exception Handler에게 예외 객체를 전달하고, 이 handler는 예외 정보를 출력하면서 해당 Thread를 비정상적으로 종료시키게 된다.

 

 

2. Null값을 리턴하고 클라이언트 코드에서 관리할 때의 문제점

 

코드가 지저분해지고, 실수를 유발할 수 있다.

- Null이 발생할 수 있는 지 클라이언트 코드에서 판단하고 if(Obj != null) 와 같이 Null 체크를 해야하기 때문에 코드가 지저분해지고, 개발자가 실수로 Null 체크를 하지 않는다면 런타임 중 에러가 발생할 수 있다.

 

 

JDK 8 버전에서 이러한 문제를 해결하기 위해, Null이 될 수 있는 객체나 값을 Optional이라는 일종의 컨테이너 안으로 감싸서 안전하게 다룰 수 있게 도와주는 Optional API를 제공하기 시작했다.  

 

Java Optional<T>

Java 8 버전부터 Null을 안전하고 깔끔하게 처리할 수 있는 Optional 클래스가 추가되었다.

 

Optinal은 Null일 수 있는 객체나 값을 감싸주는 래퍼(Wrapper) 클래스로, Optional 안의 value 값은 제네릭 타입의 Null일 수도 있는 하나의 값이다.

https://www.linkedin.com/pulse/introduction-java-8-optional-aneshka-goyal/

 

Optional 클래스는 value 값을 다양한 메서드를 통해 접근하도록 하고, 이로 인해 NPE가 발생될 여지를 줄여준다.

 

주요 메서드로는 Optional 객체 생성 메서드, 비어있는지 확인하는 메서드, 값을 꺼내는 메서드, 중간 연산 메서드가 있다.

 

Optional 객체 생성

ofNullable()

Optional.ofNullable()은 Null을 허용하는 Optional 객체를 생성하는 메서드이다.

public class User {
    private int id;
    private String name;
    private Address address;
    
    public Optional<String> getAddress() {
    	return Optional.ofNullable(this.address);
    }   
}

of()

Optional.of() 는 Optional.ofNullable()와 다르게 Null을 허용하지 않는다는 특징이 있다.

만약 Null값을 인자로 넣게 된다면 NPE 에러가 발생하게 된다.

public class User {
    private int id;
    private String name;
    private Address address;
    
    public Optional<Address> getAddress() {
    	return Optional.of(this.address);
    }   
}
User user = new User();
user.setId(1);
user.setName("jh");

Optional<Address> addressOpt = user.getAddress(); // NPE 발생 !!

java.lang.NullPointerException
	at java.base/java.util.Objects.requireNonNull(Objects.java:208)
	at java.base/java.util.Optional.of(Optional.java:113)
    ...

 

Optional 값 가져오기 

get()

get() 메서드는 Optional에 있는 값을 바로 가져오는 메서드인데, 가져올 때 Null이 아닐 것이라고 가정하기 때문에 만약 Null인 경우 NPE가 발생하게 된다. 

 

그래서 직접 get()으로 Optional 값에 접근하는 것은 지양해야 한다.

Address address = addressOpt.get();

isPresent() , isEmpty()

isPresent() 메서드는 값이 Null이 아니면 true를 Null이면 false를 리턴하고, isEmpty()는 반대로 Null이면 true를 아니면 false를 리턴하는 메서드이다.

 

해당 메서드는 조건문과 get()메서드를 같이 사용하기 때문에, 값을 꺼내는 용도라면 orElseGet() 메서드가 권장되고 있다.

if(addressOpt.isPresent()) {
    Address address = addressOpt.get();
}

ifPresent(Consumer)

값이 Null이 아닌 경우에는 인자값으로 전달한 Consumer 함수형 인터페이스 타입의 함수를 실행하고, Null인 경우에는 무시하는 메서드이다. 

addressOpt.ifPresent((address)-> System.out.println(address.getAddressNo()));

orElse(T)

orElse(T) 메서드는 Optional에 값이 있으면 해당 값을 할당하고, 없다면 인자값으로 전달한 대체 값을 리턴하는 메서드이다. 

아래 예시에서 replacedAddress는 전달한 id가 1이고, 주소가 111-111인 Address를 참조하게 된다.

Address replacedAddress = addressOpt.orElse(new Address(1, "111-111"));

orElseGet(Supplier)

orElseGet() 메서드는 값이 있다면 할당하고, 없다면 Supplier 타입의 함수를 통해 생성된 객체를 리턴하게 된다.

Address replacedAddress = addressOpt.orElseGet(() -> new Address(1, "111"));

 

orElse()와 거의 비슷하지만 orElseGet() 메서드는 Optional 객체에 값이 있다면 인자값으로 전달한 함수는 무시하고, orElse() 메서드는 Optional 객체에 값이 있더라도 인자값의 함수가 실행된다는 차이가 있다. 

 

즉 아래와 같이 Optional 안에 실제 Address 객체가 존재할 때, replacedAddress 에는 Optional 객체 안의 값을 할당하지만 Address 객체 생성하는 함수 또한 같이 실행된다. (특별한 이유가 없다면 orElseGet을 사용하는게 좋다)

Optional<Address> addressOpt = Optional.ofNullable(new Address(2,"222-222"));
Address replacedAddress = addressOpt.orElse(new Address(1, "111-111"));

System.out.println(replacedAddress.getAddressId()); // 2를 출력한다

orElseThrow()

Optional에 값이 있으면 가져오고 없다면 에러를 던지는 메서드이다. 

 

인자값에 Supplier 함수로 예외를 지정하지 않는다면 NoSuchElementException 에러가 발생하고, 예외를 지정한 경우 해당 에러가 발생한다.

  • orElseThrow()
Address address = addressOpt.orElseThrow(); // 예외 발생 !!

java.util.NoSuchElementException: No value present
	at java.base/java.util.Optional.orElseThrow(Optional.java:377)
    ...
  • orElseThrow(Supplier)
Address address = addressOpt.orElseThrow(IllegalStateException::new); // 예외 발생 !!

java.lang.IllegalStateException
	at java.base/java.util.Optional.orElseThrow(Optional.java:403)
    ...

 

Optional 중간 연산

Optional에도 StreamAPI와 같이 중간 연산을 수행하는 메서드가 존재한다. 

filter(Predicate)

Optional에 들어있는 값을 Predicate 함수의 조건으로 걸러내는 기능을 한다. 

 

아래의 예시는 filter의 인자값으로 Predicate 타입의 람다식 함수를 전달해서, value값에 특정 문자열이 포함되어있지 않으면 빈 값을 반환하게 하는 코드이다. 

String value = "Java8 Optional";

Optional<String> optionalVal = Optional.ofNullable(value);

Optional<String> filterdA = optionalVal.filter(s -> s.contains("Java"));
System.out.println(filterdA.isPresent()); // true

Optional<String> filterdB = optionalVal.filter(s -> s.contains("Spring"));
System.out.println(filterdB.isPresent()); // false

map(Function)

map(Function) 메서드는 Optional 안에 들어있는 값을 Function 조건을 통해 새로운 값으로 변환하는 메서드이다.

Optional<String> optionalVal = Optional.ofNullable("Java8 Optional");

Optional<Integer> strLength = optionalVal.map((s) -> s.length()); // s의 길이를 담은 Optional 객체로 변환

flatMap(Function)

flatMap(Function)은 Optional의 값이 Optional<Optional<T>>와 같이 두겹으로 쌓여 있는 경우, Optional<T>로 차원을 줄여서 반환하는 메서드이다.

 

아래와 같이 User 타입의 객체가 들어가 있는 Optional 객체를 map()을 통해 Address로 변환하면, getAddress 메서드가 Optional 타입을 리턴하기 때문에 Optional<Optional<T>> 형태가 된다.

public class User {
    private int id;
    private String name;
    private Address address;
    
    public Optional<Address> getAddress() {
    	return Optional.of(this.address);
    }   
}
Optional<User> user = Optional.ofNullable(new User(1, "name", new Address(1,"address")));

Optional<Optional<Address>> address = user.map(User::getAddress);

 

이렇게 map 메서드의 리턴 타입 자체가 Optional인 경우, 아래와 같이 flatMap(Function) 메서드를 사용하면 Optional을 감싸지 않고 값을 리턴할 수 있다.

Optional<User> user = Optional.ofNullable(new User(1, "name", new Address(1,"address")));

Optional<Address> address = user.flatMap(User::getAddress);

 

Optional 사용 시 주의사항

1. Optional은 리턴 타입으로만 사용하자

Optional은 리턴 타입, 매개 변수 타입, Map의 키 값 등 여러 곳에서 사용할 수 있지만, Effective Java에서는 다음과 같은 이유로 리턴 타입으로만 사용하기를 권장하고 있다.

- 매개변수 타입으로 사용하는 경우 매개변수에 Null을 넘겨줄수 있어서 NPE 이 발생할 수 있다.
- Map의 키 값으로 사용하는 경우에는 Map 인터페이스는 Key가 Null일 수 없다는 특징을 깨뜨리게 된다. 
- 인스턴스의 필드 타입으로 사용하는 경우, 필드의 값이 있을 수도 없을수도 있다는 것은 클래스 설계가 잘못된 것이다.
- 등등.. 

2. Primitive Type 값인 경우 Optional 대신 OptionalInt와 같은 클래스 사용

Optional 클래스는 Primitive Type 값을 Boxing / Un-Boxing 할 때, Optional에서 꺼냈다가 다시 감싸는 과정이 추가되어서 성능상 좋지 않다. 

 

아래와 같은 Primitive Type을 위한 Optional 클래스를 사용하자 

3. 자체적으로 Null 체크를 할 수 있는 컨테이너 타입의 인스턴스는 Optional로 감싸지 말자

Collection, Map, Stream, Optional과 같은 클래스 / 인터페이스는 내부에 값이 비어있는지 파악할 수 있는 메서드가 제공되기 때문에 Optional을 사용하는 것이 불필요하고, 오히려 값을 두번 감싸는 행위가 되기 때문에 지양하는 것이 좋다.