[자바] 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