[자바] Technical Article 정리 - 자바 Optional
Raoul-Gabriel Urma의 Tired of Null Pointer Exceptions? Consider Using Java SE 8’s “Optional”! 기고 내용 정리입니다.
- Java SE 8의
Optional을 사용하여 코드를 보다 읽기 쉽게 만들고null포인터 예외로부터 보호할 수 있다. null참조란 값이 존재하지 않는 것을 나타내는 데 사용되므로 많은 문제의 원인이 된다.- Java SE 8에서 제공되는 새로운 클래스인
java.util.Optional클래스를 사용하여 이러한 문제들을 완화할 수 있다. null의 위험성- 의도치 않은
null포인터 예외를 예방하기 위해서 할 수 있는 일은 먼저 방어적으로null참조를 막기 위한 확인을 하는 것이다.
- 의도치 않은
- 다음 예는
null참조 확인을 위한 코드이다.String version = "UNKNOWN"; if(computer != null) { Soundcard soundcard = computer.getSoundcard(); if(soundcard != null) { USB usb = soundcard.getUSB(); if(usb != null) { version = usb.getVersion(); } } } - 그러나 이 경우 중첩된
null참조 확인 코드로 인해 코드가 매우 더러워지게 된다. - 불행히도
NullPointerException가 발생하지 않도록 만들기 위해서는 많은 상용구 코드가 필요하다. - 또한 이러한 확인 코드들이 비즈니스 로직을 방해한다는 것은 성가신 일이며 실제로 프로그램의 전체적인 가독성을 떨어뜨린다.
- 어떤 속성이
null이 되는 경우를 확인하는 것을 빼먹을 수 있기 때문에 이러한null확인 코드는 에러를 발생하기 쉽게 만든다. - 따라서
null을 사용하여 값이 존재하지 않는 것을 나타내는 것은 잘못된 접근이며 값의 존재 여부를 나타내기 위한 더 나은 방법이 필요하다.
Null에 대한 대안이 존재하는가
- Groovy와 같은 언어는 잠재적인
null참조를 안전하게 탐색하기 위해 safe navigation 연산자인?.를 제공하고 있으며 다음과 같이 동작한다.String version = computer?.getSoundcard()?.getUSB()?.getVersion();- 위 경우 변수
computer가null일 경우, 또는getSoundcard()가null을 반환할 경우, 또는getUSB()가null을 반환할 경우에 변수version에null이 할당된다. - 따라서
null을 확인하기 위해 복잡한 중첩 조건문을 작성하지 않아도 된다.
- 위 경우 변수
- 또한 Groovy는 Elvis 연산자인
?:를 제공하고 있으며 이는 기본값이 필요한 간단한 상황에서 사용된다.String version = computer?.getSoundcard()?.getUSB()?.getVersion() ?: "UNKNOWN";- safe navigation 연산자를 사용한 표현식이
null을 반환하면null대신 기본값인"UNKNOWN"이 반환되며 그렇지 않은 경우 사용 가능한 버전 값이 반환된다.
- safe navigation 연산자를 사용한 표현식이
- Haskell과 Scala와 같은 함수형 언어는 다른 방법을 사용한다.
- Haskell은
Maybe형을 제공하여 기본적으로 선택적인 값을 캡슐화한다.Maybe형의 값은 주어진 형의 값을 포함하거나 아무것도 포함하지 않을 수 있으며null참조라는 개념은 존재하지 않는다.
- Scala 또한
Option[T]라는 유사한 구조체를 제공하여 형이T인 값의 존재여부를 나타낼 수 있다.- 값이 존재하는지 그렇지 않는지
Option형에 있는 연산을 사용하여 명시적으로 확인해야 하므로null확인이라는 과정을 강제한다. - 타입 시스템에 의해 강제되므로 더 이상
null인지 확인하지 않아도 된다.
- 값이 존재하는지 그렇지 않는지
Optional
- Java SE 8에서는 Haskell과 Scala의 아이디어로부터 영감을 받아
java.util.Optional<T>이라는 새로운 클래스를 도입하였다. 이 클래스는 선택적인 값을 캡슐화한다. Optional을 값을 가지거나 가지지 않는 단일 값 컨테이너로 볼 수 있다.- 예컨대
Optional<Soundcard>형 객체는Soundcard형 객체를 포함할 수 있고, 아무것도 포함하지 않는 빈 객체가 될 수도 있다.
- 예컨대
Optional을 사용하여 다음과 같이 코드를 작성하여 새로운 모델을 만들 수 있다.public class Computer { private Optional<Soundcard> soundcard; public Optional<Soundcard> getSoundcard() { ... } } public class Soundcard { private Optional<USB> usb; public Optional<USB> getUSB() { ... } } public class USB { public String getVersion() { ... } }Computer형 객체는Soundcard형 객체를 포함할 수도 있고, 포함하지 않을 수도 있다. 즉,Soundcard는 선택적이다. 또한Soundcard는 선택적으로USB포트를 가지고 있을 수 있다.- 위의 새로운 모델은 주어진 값이 존재하지 않는 것이 허용된다는 것을 명확하게 반영한다.
- Guava와 같은 라이브러리에서도 유사한 개념이 적용 가능하다.
- 사용자는 결국
USB포트의 버전 번호를 얻고 싶은 것이다. - 간단히 말해서
Optional클래스는 값이 존재하거나 존재하지 않는 상황을 명시적으로 다루는 메소드를 포함하고 있기 때문에Optional<Soundcard>객체를 가지고 실제적으로 할 수 있는 일은 값이 존재하지 않은 상황에 대한 처리이다. - 즉,
null참조 확인과 비교하여Optional클래스가 제공하는 장점은 값이 존재하지 않은 상황을 생각해보게 만드는 것이다. - 결과적으로 의도치 않은
null포인터 예외를 예방할 수 있다. Optional클래스의 의도는 모든null참조를 대체하는 것이 아니라는 점 중요하다.- 대신, 그 목적은 메소드의 시그니처를 확인하는 것만으로 선택적인 값을 기대할 수 있는지 알 수 있도록 더 이해하기 쉬운 API를 설계하는 것이다.
- 이는 값이 존재하지 않는 경우를 처리하기 위해
Optional을 적극적으로 언래핑하도록 강요한다.
- 중첩된
null확인 코드 대신Optional을 사용해 다음과 같이 코드를 작성할 수 있다.String name = computer.flatMap(Computer::getSoundcard) .flatMap(Soundcard::getUSB) .map(USB::getVersion) .orElse("UNKNOWN");
Optional 객체 생성 방법
- 빈
Optional객체 생성Optional<Soundcard> sc = Optional.empty(); null이 아닌 값을 가지고 있는Optional객체 생성SoundCard soundcard = new Soundcard(); Optional<Soundcard> sc = Optional.of(soundcard);- 이 경우
soundcard가null이라면 즉시NullPointerException이 발생한다.Optional을 사용하지 않았을 경우에는soundcard의 속성에 접근하려고 할 때 발생할 것이다.
- 이 경우
null값을 가지고 있는Optional객체 생성Optional<Soundcard> sc = Optional.ofNullable(soundcard);- 위와 같이
ofNullable메소드를 사용하여null값을 가질 수 있는Optional객체를 생성할 수도 있다. - 이 경우
soundcard가null이라면 결과적으로Optional객체는 빈 객체가 된다.
- 위와 같이
값이 존재할 경우 어떤 일을 하기
Optional객체를 생성했다면 값의 존재유무를 명시적으로 다루기 위해 사용가능한 메소드들에 접근할 수 있다.- 다음과 같이
null인지 확인하는 코드를 잊지 않고 작성하는 것 대신ifPresent()메서드를 사용할 수 있다.SoundCard soundcard = ...; if(soundcard != null) { System.out.println(soundcard); }Optional<Soundcard> soundcard = ...; soundcard.ifPresent(System.out::println); - 더 이상 명시적으로
null확인을 하지 않아도 되며, 이는 타입 시스템에 의해 강제된다.- 만약
Optional객체가 비어있다면 아무런 내용도 출력되지 않게 된다.
- 만약
Optional객체에 값이 존재하는지를 알아내기 위해ifPresent()를 사용할 수도 있다.get()메소드는Optional객체에 값이 존재할 경우 그 값을 반환해주며 값이 존재하지 않는다면NoSuchElementException예외를 발생시킨다.- 다음과 같이
ifPresent()와get()메소드를 결합하여 예외를 막을 수 있다.if(soundcard.ifPresent()){ System.out.println(soundcard.get()); } - 그러나 이 방법은 중첩된
null확인 코드와는 크게 다르지 않으므로Optional을 사용하는 데 있어 권장된 것은 아니며 보다 이상적인 대안들이 존재한다.
기본값과 조치
- 전형적인 패턴은 코드 실행 결과가
null인지 결정할 때 기본값을 반환하는 것이다. - 이를 위해 일반적으로 다음과 같이 삼항(ternary) 연산자를 사용할 수 있다.
Soundcard soundcard = maybeSoundcard != null ? maybeSoundcard : new Soundcard("basic_sound_card"); Optional객체를 사용하고,Optional이 비어 있을 때 기본값을 제공하는orElse()메소드를 사용하면 다음과 같이 코드를 작성할 수 있다.Soundcard soundcard = maybeSoundcard.orElse(new Soundcard("default"));- 유사하게
Optional이 비어 있을 때 기본값을 제공하는 것 대신 예외를 발생시키는orElseThrow()메소드를 사용할 수도 있다.Soundcard soundcard = maybeSoundCard.orElseThrow(IllegalStateException::new);
filter 메서드를 사용하여 특정 값을 제외하기
- 객체를 대상으로 메소드를 호출하고 어떤 특성을 확인하는 것이 자주 필요하다.
USB객체가 특정 버전인지 확인이 필요하다면 이를 위한 안정적인 방법 중 하나는USB객체를 가르키는 참조가null인지 아닌지 확인한 다음getVersion()메소드를 호출하는 것이다.USB usb = ...; if(usb != null && "3.0".equals(usb.getVersion())) { System.out.println("ok"); }- 이 패턴은
Optional객체의filter()메소드를 사용하여 다음과 같이 다시 작성할 수 있다.Optional<USB> maybeUSB = ...; maybeUSB.filter(usb -> "3.0".equals(usb.getVersion()) .ifPresent(() -> System.out.println("ok")); filter()메소드는 술어(predicate; 불리언을 반환하는 함수)를 인자로 받는다.Optional객체에 값이 존재하고 값이 일치한다면 그 값을 반환하며, 그렇지 않다면 빈Optional객체를 반환한다.- 이러한 패턴은
Stream인터페이스에서 사용된filter()메소드와 유사하다.
map 메서드를 사용하여 값을 추출 및 변환시키기
- 또다른 흔한 패턴은 객체로부터 정보를 추출하는 것이다.
Soundcard객체에서USB객체를 추출하고 버전이 올바른지 확인하기 위한 전형적인 코드는 다음과 같다.if(soundcard != null) { USB usb = soundcard.getUSB(); if(usb != null && "3.0".equals(usb.getVersion()) { System.out.println("ok"); } }Optional객체(Soundcard객체)의map()메소드를 사용하면 “null값 확인과 값 추출” 코드를 다음과 같이 작성할 수 있다.Optional<USB> usb = maybeSoundcard.map(Soundcard::getUSB);- 이는 스트림과 함께 사용되는
map()메소드와 직접적으로 유사한데, 스트림에서는map()메소드에 함수를 전달할 수 있으며 그 함수는 스트림의 각각의 원소들에 대해 동작하지만 스트림이 비어있는 경우 아무런 일도 일어나지 않는다. Optional객체의map()메소드도 이와 정확히 동일하다.Optional객체 내부에 포함된 값은map()메소드의 인자로 전달된 함수에 의해 “변환되며”Optional객체가 비어있다면 아무런 일도 일어나지 않는다.- 결과적으로
map()메서드와filter()메서드를 결합하여 버전이 3.0이 아닌USB객체를 제외할 수 있다.maybeSoundcard.map(Soundcard::getUSB) .filter(usb -> "3.0".equals(usb.getVersion()) .ifPresent(() -> System.out.println("ok"));
flatMap 메서드를 사용하여 Optional 객체를 연쇄적으로 처리하기
Option을 사용하기 위해 리팩토링 할 수 있는 몇가지 패턴을 보았다.String version = computer.getSoundcard().getUSB().getVersion();- 위 코드가 수행하는 작업은 다른 객체에서 하나의 객체를 추출하는 것이며 이것이 바로
map()메서드의 목적이다. - 앞서
Computer에Optional<Soundcard>가 있고Soundcard에Optional<USB>가 있도록 모델을 변경했으므로 다음과 같이 작성할 수 있다.String version = computer.map(Computer::getSoundcard) .map(Soundcard::getUSB) .map(USB::getVersion) .orElse("UNKNOWN"); - 불행히도 이 코드는 컴파일 되지 않는다.
- 변수
computer는Optional<Computer>유형이므로map()메서드를 호출할 수 있다. - 하지만
getSoundcard()메서드는Optional<Soundcard>형 객체를 반환한다. 따라서map()메서드의 실행 결과는Optional<Optional<Soundcard>>형 객체가 된다. - 그 결과 바깥쪽
Optional객체가 다른Optional객체를 값으로 가지며 이Optional<Optional<Soundcard>>형 객체는getUSB()메서드를 지원하지 않기 때문에getUSB()메서드 호출은 유효하지 않다. - 스트림에서 사용한 패턴인
flatMap()메서드를 살펴보면, 스트림의flatMap()메서드는 함수를 인자로 전달받아서 다른 스트림을 반환한다.- 이 함수는 스트림의 각 요소에 적용되어 스트림의 스트림을 생성한다.
- 그러나
flatMap()메서드는 생성된 각각의 스트림을 그 스트림의 내용으로 교체하는 효과가 있다. - 다시 말해 함수에 의해 생성된 모든 개별 스트림들은 하나의 단일 스트림으로 통합되거나 “평평하게” 된다.
- 우리가 원하는 것도 이와 유사하며 중첩된 두 단계
Optional을 하나로 단조롭게 만드는 것이 목적이다. Optional또한flatMap()메서드를 지원하며 목적은Optional의 값에 변환 함수(transformation function)를 적용하고(맵 연산과 같이) 중첩된Optional을 단일Optional로 단조롭게 만드는 것이다.map(Computer::getSoundcard)메서드는 중첩된Optional객체(Optional<Optional<Soundcard>>)를 반환하는 반면,flatMap(Computer::getSoundcard)메소드는 단일Optional객체를 반환한다.- 이전 코드를 올바르게 만들기 위해서는
flatMap()메서드를 사용하여 다음과 같이 다시 작성해야 한다.String version = computer.flatMap(Computer::getSoundcard) .flatMap(Soundcard::getUSB) .map(USB::getVersion) .orElse("UNKNOWN"); - 첫
flapMap()메서드는Optional<Optional<Soundcard>>형 객체 대신Optional<Soundcard>형 객체를 반환하고, 두 번째flatMap()메서드도Optional<USB>형 객체를 반환하는 동일한 목적을 달성한다. getVersion()메서드는Optional객체가 아닌String객체를 반환하기 때문에 세 번째 호출은map()메소드이어야 한다.- 이로써 중첩된
null확인 코드 대신 더 읽기 쉬운 코드를 작성할 수 있게 되었으며, 또한null포인터 예외로부터 더 보호받을 수 있게 되었다.
결론
Optional의 목적은 모든null참조를 대체하는 것이 아니라 메서드의 시그니처를 확인하는 것만으로 선택적인 값을 기대할 수 있는지 알 수 있도록 더 이해하기 쉬운 API를 설계하는 것이다.- 또한 값이 존재하지 않는 경우를 처리하기 위해
Optional을 적극적으로 언래핑하도록 강요한다. - 결과적으로 의도하지 않은
null포인터 예외로부터 코드를 보호할 수 있다.
Comments