본문으로 건너뛰기

Optional 또 다른분꺼 퍼옴

Java Optional( 자바 옵셔널 )

Java Optional 클래스는 Java 8에서 추가되었으며 자바의 고질적인 문제인 NullpointerException 문제를 해결할 수 있는 방법을 제공합니다.

import java.util.Optional;

of, ofNullable로 객체 감싸기

자바에서 제공하는 객체를 Optional 객체로 감싸기 위해서는 Optional 에서 제공하는 of 와 ofNullable 매서드를 사용합니다. 둘의 차이점은
of는 인자로서 null값을 받지 않는다는 것이고 ofNullable은 null값을 허용한다는 것입니다.

@Test
public void givenNonNull_whenCreatesNonNullable() {
String name = "saelobi";
Optional<String> opt = Optional.of(name);
assertEquals("Optional[saelobi]", opt.toString());
}

아래 코드를 보시면 null값을 of 메서드의 입력으로 받을 시 NullPointerException을 일으킵니다.

@Test(expected = NullPointerException.class)
public void givenNull_whenThrowsErrorOnCreate() {
String name = null;
Optional<String> opt = Optional.of(name);
}

ofNullable은 일반 객체뿐만 아니라 null값까지 입력으로 받을 수 있다는 것을 아래 코드로 확인해 볼 수 있습니다.

@Test
public void givenNonNull_whenCreatesNullable() {
String name = "saelobi";
Optional<String> opt = Optional.ofNullable(name);
assertEquals("Optional[saelobi]", opt.toString());
}
@Test
public void givenNull_whenCreatesNullable() {
String name = null;
Optional<String> opt = Optional.ofNullable(name);
assertEquals("Optional.empty", opt.toString());
}

isPresent 메서드로 현재 Optional이 보유한 값이 null인지 아닌지를 확인할 수 있습니다.

@Test
public void givenOptional_whenIsPresentWorks() {
Optional<String> opt = Optional.of("saelobi");
assertTrue(opt.isPresent());

opt = Optional.ofNullable(null);
assertFalse(opt.isPresent());
}

이 Optional 메서드를 이용하면 다음과 같은
if를 이용한 null값 체크를 대체할 수 있습니다. if를 이용한 null값 체크가 좋지 않은 이유는 크게 2가지가 있습니다.

  1. 코드가 길어짐에 따라 코드의 가독성이 점점 떨어지게 된다

  2. 각 변수마다 null값을 체크해야 되기 때문에 프로그래머의 실수를 유발할 가능성이 높아진다.

Optional 방식은 위의 문제를 해결하여 가독성 좋고 강건한 코드를 만드는 데 도움을 줍니다. 다음 예제를 보면 어떤 의미인지 바로 아실 수 있을겁니다.

if(name != null){
System.out.println(name.length);
}

위의 if 의 null 체크 방식을 다음과 같이 ifPresent로 간결하게 해결할 수 있습니다.

@Test
public void givenOptional_whenIfPresentWorks() {
Optional<String> opt = Optional.of("saelobi");
opt.ifPresent(name -> System.out.println(name.length()));
}

orElse, orElseGet으로 Optional 값 가져오기

if에서 null값이 아닌 경우의 처리를 else 키워드 이하의 코드로 해결하지만 Optional 에서는 orElse로 간단하게 해결할 수 있습니다.

@Test
public void whenOrElseWorks() {
String nullName = null;
String name = Optional.ofNullable(nullName).orElse("John");
assertEquals("John", name);
}

Optional에서는 값을 가져올 때 자주 사용되는 메서드 두 가지가 있습니다. orElseGet, orElse 이 두 가지 입니다. 이 메서드가 자주 사용되는 이유는
_null값 체크를 할 수 있음과 동시에 null값일 경우일 경우 간단한 코드로 처리할 수 있어 코드의 가독성이 좋아지고 코드 생산성이 올라간다는 장점 이 있어서입니다. 주의할 부분은 null값일 때 어떤 값을 쓸 것이냐를 처리하는 로직에 함수를 썻을 때입니다.
orElseGet은 Optional이 가지고 있는 값이 null일 경우에만 orElseGet에 주어진 함수를 실행합니다. 하지만 orElse는 null값 유무와 상관없이 사용하게 되어있습니다.
이 부분을 캐치하지 못해 성능 이슈가 발생할 수도 있으니 주의해서 써야할 것입니다.

public String getMyDefault() {
System.out.println("Getting Default Value");
return "Default Value";
}

@Test
public void whenOrElseGetAndOrElseOverLap() {
String text = null;

System.out.println("Using orElseGet:");
String defaultText =
Optional.ofNullable(text).orElseGet(this::getMyDefault);
assertEquals("Default Value", defaultText);

System.out.println("Using orElse:");
defaultText = Optional.ofNullable(text).orElse(getMyDefault());
assertEquals("Default Value", defaultText);
}

@Test
public void whenOrElseGetAndOrElseDiff() {
String text = "TEST";

System.out.println("Using orElseGet:");
String defaultText =
Optional.ofNullable(text).orElseGet(this::getMyDefault);
assertEquals("TEST", defaultText);

System.out.println("Using orElse:");
defaultText = Optional.ofNullable(text).orElse(getMyDefault());
assertEquals("TEST", defaultText);
}

또한 get과 orElseThrow를 이용해서 Optional의 값을 얻을 수 있습니다.
하지만 이런 방식은 기존의 null을 체크하는 방식과 다른게 없거니와 오히려 Optional을 써서 타이핑만 더 치는 안 좋은 방법이라고 생각합니다.
하루 빨리 deprecated 됬으면 하는 개인적인 소망입니다.

@Test(expected = IllegalArgumentException.class)
public void whenOrElseWorks1() {
String nullName = null;
String name = Optional.ofNullable(nullName).orElseThrow(IllegalArgumentException::new);
}

@Test
public void givenOptional_whenGetsValue() {
Optional<String> opt = Optional.of("saelobi");
String name = opt.get();

assertEquals("saelobi", name);
}

@Test(expected = NoSuchElementException.class)
public void givenOptionalWithNull_whenGetThrowsException() {
Optional<String> opt = Optional.ofNullable(null);
String name = opt.get();
}

다음은 Optional과 stream 메서드를 이용한 예제입니다. if문으로 처리할 로직을 Optional과 stream 메서드로 간결한 코드로 작성할 수 있거니와 null 체크도 간편하게 할 수 있다는 큰 장점이 있죠.

public class Modem {
private Double price;

public Double getPrice() {
return price;
}

public void setPrice(Double price) {
this.price = price;
}

public Modem(Double price) {
this.price = price;
}
}

public boolean priceIsInRange1(Modem modem) {
boolean isInRange = false;

if (modem != null && modem.getPrice() != null
&& (modem.getPrice() >= 10
&& modem.getPrice() <= 15)) {

isInRange = true;
}
return isInRange;
}

@Test
public void whenFiltersWithoutOptional() {
assertTrue(priceIsInRange1(new Modem(10.0)));
assertFalse(priceIsInRange1(new Modem(9.9)));
assertFalse(priceIsInRange1(new Modem(null)));
assertFalse(priceIsInRange1(new Modem(15.5)));
assertFalse(priceIsInRange1(null));
}

public boolean priceIsInRange2(Modem modem) {
return Optional.ofNullable(modem)
.map(Modem::getPrice)
.filter(p -> p >= 10)
.filter(p -> p <= 15)
.isPresent();
}

@Test
public void whenFiltersWithoutOptional2() {
assertTrue(priceIsInRange2(new Modem(10.0)));
assertFalse(priceIsInRange2(new Modem(9.9)));
assertFalse(priceIsInRange2(new Modem(null)));
assertFalse(priceIsInRange2(new Modem(15.5)));
assertFalse(priceIsInRange2(null));
}

다음은 Optional과 stream 메서드를 이용한 또다른 예제입니다.

@Test
public void givenOptional_whenMapWorks() {
List<String> companyNames = Arrays.asList(
"Samsung", "SK", "NAVER", "Daum");
Optional<List<String>> listOptional = Optional.of(companyNames);

int size = listOptional.map(List::size).orElse(0);
assertEquals(4, size);
}

@Test
public void givenOptional_whenMapWorks2() {
String name = "saelobi";
Optional<String> nameOptional = Optional.ofNullable(name);

int len = nameOptional.map(String::length).orElse(0);
assertEquals(7, len);
}

@Test
public void givenOptional_whenMapWorksWithFilter() {
String password = " password ";
Optional<String> passOpt = Optional.of(password);
boolean correctPassword = passOpt.filter(
pass -> pass.equals("password")).isPresent();
assertFalse(correctPassword);

correctPassword = passOpt
.map(String::trim)
.filter(pass -> pass.equals("password"))
.isPresent();
assertTrue(correctPassword);
}