JAVA Optional의 충격적인 사용법 - map을 이용한 체이닝

hksdpr·2022년 12월 10일
1

Optional 사용 계기

아마도 Optional을 처음 사용하게 된 것은 Spring Data JPA를 사용하면서 인 것 같다. findById를 통해 엔티티를 가져오는 경우 엔티티를 바로 반환하는 것이 아니라 Optional로 반환되기 때문에 Optional을 사용할 수 밖에 없었다. 그래서 보통은 엔티티를 조회한 후 존재하지 않는 엔티티를 조회했다면 orElseThrow를 사용하여 적당한 예외를 던지는 방식으로 사용하였다.

Optional은 굳이 왜 사용하지...

솔직히 굳이 Optional을 왜 사용해야 하는지 궁금할 때가 많았다. 그러다가 최근 다음과 같은 상황을 겪게 되었다. 어떤 객체의 멤버변수를 계속 조회해서 하위로 내려가야 하는 경우였다. 가령 특정 장치를 소유한 팀의 이름을 조회할 때 다음과 같이 객체 그래프를 탐색해야 한다고 가정해보자.

장치 -> 장치의 소유자 -> 소유자가 속한 팀 -> 팀의 이름

이런 경우 null일 수 있는 곳은 다음의 네 곳이나 있다.

  • 장치
  • 소유자
  • 팀 이름

어느 것 하나라도 null이라면 기본 값을 출력한다거나 특정 예외를 반환해야 한다면 확인해야 하는 부분이 네 곳이나 되는 것이다. 가령 장치로부터 팀의 이름을 구하고 null인 값이 존재하면 IllegalStateException 예외를 던지는 getTeamNameFromDevice 메소드는 다음과 같을 것이다.

public String getTeamNameFromDevice(Device device){
	if(device == null) throw new IllegalStateException();
    User owner = device.getOwner();
    if(owner == null) throw new IllegalStateException();
    Team team = owner.getTeam();
    if(team == null) throw new IllegalStateException();
    String teamName = team.getName();
    if(teamName == null) throw new IllegalStateException();
    return teamName;
}

Optional을 인자로 받는다고 해도 나는 다음과 같이 사용하고 있었다.

public String getTeamNameFromDevice(Optional<Device> optDevice){
	Device device = optDevice.orElseThrow(IllegalStateException::new);
	if(device == null) throw new IllegalStateException();
    User owner = device.getOwner();
    if(owner == null) throw new IllegalStateException();
    Team team = owner.getTeam();
    if(team == null) throw new IllegalStateException();
    String teamName = team.getName();
    if(teamName == null) throw new IllegalStateException();
    return teamName;
}

Optional객체를 인자로 전달받는다고 해도 사실상 처음에 orElseThrow를 사용한 것을 제외하면 달라진 부분이 없다.

Map 함수

그런데 Optional을 사용하면서 메소드 체이닝을 사용할 수 있다는 사실이 어렴풋이 기억났다. 처음 봤을 때는 Optional에서 무슨 체이닝을 사용한다는 건지 이해를 못하고 넘어갔는데 혹시나 내가 겪고 있는 문제를 해결해 줄 수 있지 않을까 하는 생각이 들었다. 그리고 map 함수가 있다는 사실을 알게 되었다. 그리고 getTeamNameFromDevice함수를 다음과 같이 바꾸어 보았다.

public String getTeamNameFromDevice(Optional<Device> optDevice){
	return optDevice
    	.map(Device::getOwner)
        .map(User::getTeam)
        .map(Team::getName)
        .orElseThrow(IllegalStateException::new);
}

복잡하게 if문을 사용하지 않고도 동일한 결과를 얻을 수 있었다. 팀 이름이 null이어도, 팀이 null이어도, 소유자가 null이어도, 장치가 null이어도 모두 IllegalStateException이 발생했다. 이제 Optional을 왜 쓰는지 조금이나마 이해하게 되었다. 문서를 찾아보니 다음과 같이 나와 있었다.

If a value is present, apply the provided mapping function to it, and if the result is non-null, return an Optional describing the result. Otherwise return an empty Optional.

값이 존재하면 제공된 매핑 함수를 적용하고 결과가 null이 아니면 결과에 대한 Optional을 반환한다고 한다. 그렇지 않은 경우, 그러니까 매핑 함수의 적용 결과가 null인 경우 빈 Optional을 반환한다고 한다. 그렇기에 메소드를 체이닝했을 때 중간에 null이 있어도 빈 Optional로 계속해서 이어 나갈 수 있었던 것이다.

아 이래서 쓰는 것이었구나

사람들이 이래서 Optional을 쓰라고 했다는 사실을 깨달으며 많은 충격을 받았다. NullPointerException으로부터 안전하다는 것은 알았지만 쓰기 불편해서 잘 쓰지 않게 되었는데 사용법을 제대로 알지 못하고 있었던 것이다. 이제부터는 적재적소에 잘 활용해야겠다.

2개의 댓글

comment-user-thumbnail
2023년 11월 19일

이걸 왜 지금에야 저도 알았을까요.. 감사합니다!

1개의 답글