[Java8] Chapter 4-1. Optional (1)

 

✍️ Optional의 등장 배경

Optional, 자바8 에서 추가된 새로운 인터페이스

비어있을 수 있고 무언가를 담고 있을 수 있는 컨테이너 인스턴스의 타입

 

Optional의 등장 배경을 먼저 살펴보자.

아래 코드가 무사히 실행이 될까? 

public static void main(String[] args) {
   OnlineClass spring_boot = new OnlineClass(1, "spring boot", true);
   Duration studyDruation = spring_boot.getProgress().getStudyDuration();
   System.out.println(studyDruation);
}
더보기
public class OnlineClass {
	private Integer id;
	private String title;
	private boolean closed;
	private Progress progress;

	public OnlineClass(Integer id, String title, boolean closed) {
		this.id = id;
		this.title = title;
		this.closed = closed;
	}

	public Integer getId() {
		return id;
	}

	public void setId(Integer id) {
		this.id = id;
	}

	public String getTitle() {
		return title;
	}

	public void setTitle(String title) {
		this.title = title;
	}

	public boolean isClosed() {
		return closed;
	}

	public void setClosed(boolean closed) {
		this.closed = closed;
	}

	public Progress getProgress() {
		return progress;
	}

	public void setProgress(Progress progress) {
		this.progress = progress;
	}
}
public class Progress {
	private Duration studyDuration;
	private boolean finished;

	public Duration getStudyDuration() {
		return studyDuration;
	}

	public void setStudyDuration(Duration studyDuration) {
		this.studyDuration = studyDuration;
	}

	public boolean isFinished() {
		return finished;
	}

	public void setFinished(boolean finished) {
		this.finished = finished;
	}
}

 

정답은 "NO"이다. 레퍼런스 타입의 기본값은 null로 getProgress가 기본 값인 null을 반환하기 때문이다.

Exception in thread "main" java.lang.NullPointerException

 

자바8 이전에 null 체크를 다음과 같은 코드로 작성해왔다.

public static void main(String[] args) {
	OnlineClass spring_boot = new OnlineClass(1, "spring boot", true);
	Progress progress = spring_boot.getProgress();

	if (progress != null) {
		System.out.println(progress.getStudyDuration());
	}
}

 

문제는 이러한 코딩 스타일이 에러를 유발하기 너무나 쉬운 형태라는 것이다.

첫째로, 코드를 작성하는 건 사람이기에 null 체크를 잊어버릴 수 있고 우리는 종종 NullPointerException을 마주하게 된다.

둘째로, 메서드가 null을 반환하는 것 자체가 문제다. getProgress 메서드는 Progress를 반환할 뿐 null을 반환하는 것은 메서드의 목적에 어긋난다. 

 

결국, 메서드에서 작업 중 특별한 상황에서 값을 제대로 반환하지 못하는 경우 두 가지 선택지가 있는데

어쩔 수 없이 null을 반환하거나 예외를 던지면 된다. 

 

null을 반환하는 경우 메서드 자체에선 발생하는 비용은 없다. 다만 null을 반환하므로 메서드를 사용하는 외부(클라이언트)에서 메서드의 호출과 동시에 null 체크 코드가 동반돼야 한다.

 

메서드 내부에서 예외를 던지면 메서드를 사용하는 클라이언트 쪽에선 null로 인해 발생하는 고통이 줄겠지만... 에러가 발생하면 Java는 stack trace를 찍는다. 쉽게 말하면 에러가 발생하기 전까지의 어떠한 call stack을 거쳐 에러가 발생했는지 스냅샷을 찍는다. 문제는 stack trace는 자원을 소모하는 행위이므로 필요한 경우에만 사용해야지 로직을 처리할 때 예외를 던지는 것은 그리 좋은 코딩 습관이 아니다.

public Progress getProgress() {
	if(this.progress == null) {
		throw new RuntimeException("error...");
	}
	
	return progress;
}

 

자바8부턴 메서드가 null을 반환해야 하는 상황이 생기면 Optional을 반환한다. Optional은 클라이언트에게 명시적으로 null 값일 수도 있다는 걸 알려주고 null 값인 경우 이에 대한 처리를 유도한다.

 

🍊 Optional 적용

단순 Progress를 반환하는 것이 아닌, Optional로 감싼 뒤 반환한다.

public Optional<Progress> getProgress() {
	return Optional.ofNullable(this.progress);
}

 

Optional.ofNullable은 Optinal로 감싸는 값이 null 일 수 있고 아닐 수 있을 때 사용한다.

 

public Optional<Progress> getProgress() {
	return Optional.of(this.progress);	
}

Optional.of, 단순 of를 사용하면 null값을 인자로 넘길 시 NullPointerException이 발생한다.

 

Optional은 반환값으로만 사용하자

Optinal을 사용하는 것엔 제한이 없다. 메서드의 매개 변수 타입, Map의 Key 타입, 인스턴스 필드 타입 등.. 하지만 가능하면 Optional은 반환값으로만 사용하길 권장한다.

 

1. 메서드의 매개 변수 타입으로 Optional이 올 때 왜 문제가 되는지에 대해서 간단히 살펴보면,

Optional을 사용하더라도 메서드를 호출할 때 null을 전달할 수 있기 때문이다. 결국, Optional을 매개 변수로 사용하면 Optional 자체가 null 인지도 체크해야 하고 Optional이 비어있는지도 체크해야 하는 근본적인 문제로 다시 돌아간다. 

public void setProgress(Optional<Progress> progress) {
    if (progress != null) {
        progress.ifPresent(p -> this.progress = p);
    }
}
public static void main(String[] args) {
    OnlineClass spring_boot = new OnlineClass(1, "spring boot", true);
    spring_boot.setProgress(null);
}

반대로 Optional 매개변수에 null을 전달하면 문제가 되는 것처럼 Optional을 반환하는 메서드도 절대 null을 반환하면 안 된다. 만약 반환할게 없다면 null이 아닌 Optional.empty()를 반환하자.

public Optional<Progress> getProgress() {
    return Optional.empty();
}

 

2. Map의 Key 값으로 절대 Optional을 사용하면 안 된다. Map의 가장 큰 특징 중 하나는 Key는 null이 될 수 없다는 것이다. 만약 Optional을 Map에 사용하면 Map의 본질을 흐리게 되는 것이다.

 

3. 인스턴스의 필드로 Optional을 사용하면 해당 필드가 있을 수도 있고 없을 수도 있다는 것인데 Optional을 필드에 사용할 정도면 설계의 문제로 Super, Sub class로 구조를 나누는 것이 바람직하다. 

 

컨테이너 타입을 Optional로 감싸지 않기

컨테이너 타입(Collection, Map, Array)을 Optional로 감싸는 것은 큰 의미가 없다. 컨테이너 타입은 그 자체로도 이미 빈 컨테이너를 반환할 수 있는 타입들이다. 따라서, Optional.empty()를 반환하듯 빈 컨테이너를 반환하도록 하자.

List<Object> obs = new ArrayList<>();
return obs == null ? Collections.emptyList() : obs;

 

 

본 내용은 백기선님의 자바8 강의 내용입니다.

 

더 자바, Java 8 - 인프런 | 강의

자바 8에 추가된 기능들은 자바가 제공하는 API는 물론이고 스프링 같은 제 3의 라이브러리 및 프레임워크에서도 널리 사용되고 있습니다. 이 시대의 자바 개발자라면 반드시 알아야 합니다. 이

www.inflearn.com