까다로운 Optional 느낌있게 사용하는 법

nswon·2022년 6월 19일
39

JAVA

목록 보기
1/2
post-thumbnail

Optional 다들 이렇게 쓸 걸 ?

Optional이 나온 이유부터 알아보자. 이유를 모르고 쓰는 사람들은 이렇게 말한다.

"null을 반환하면 그 무서운 NPE가 뜨기 때문에, Optional로 null을
가지는 변수, 또는 객체를 Optional로 묶어요. 그럼 NPE가 안떠요"

자바로 개발하면서 한 번이라도 NullPointerException을 겪은 사람은 손을 들어보자. NPE는 다양한 예외 중에서도 가장 악질로 유명한 예외이다.

그렇기에 NPE를 피하기 위해서 Optional을 사용하는 사람은 드물지 않을 것이다.
하지만 당연히 Optional이 나온 이유는 NPE를 피하기 위함이 아니다.

그럼 NPE가 얼마나 무섭길래 Optional을 쓰는 이유를 모르면서까지 Optional을 쓰며 피하는 것일까 ? 우선 NPE의 원인이 되는 null이 어떻게 탄생되었는지 알아보자.

null 창시자: 아.. 잘못 만들었다.

1965년 토니 호어라는 영국 컴퓨터과학자가 프로그래밍 언어를 설계하면서
처음으로 null 참조가 등장했다. 당시 그는 null 참조를 만든 이유로 이렇게 말했다.

"존재하지 않는 값을 표현할 수 있는 방법 중 가장 구현하기가 쉬운 방법이
null 참조였어요"

그리고 몇 년 후, 자바 개발자들은 컴파일에서는 조용히 잠복해있다가 런타임 때 펑펑 터지는 NPE 때문에 자바 개발자들의 멘탈도 펑펑 터질 수 밖에 없었다.

창시자인 호어는 null 참조와 NPE를 가리키며 "1조짜리 큰 실수"라고 표현했다.
아마 피해비용은 훨씬 클 것이다.

Java8 이전에는 어떻게 NPE를 줄여왔을까 ?

Optional이 나오기 전까지 어떻게 NPE를 줄여왔을까 ?
나는 하나 떠오르는 생각이 있었다.

설마 하나 하나 if문으로 ?

그렇다. 일단 NPE 발생 확률이 높은 코드로 예시를 들겠다.
아래와 같은 데이터 모델이 있다고 가정해보자.

/* 차 */
public class Car {
	private Person person;
	// getters & setters
}

/* 사람 */
public class Person {
	private String name;
	// getters & setters
}
/* 아래 코드는 차를 산 사람의 이름을 반환하는 메서드이다. */
public String getCarPersonName(Car car) {
	return car.getPerson().getName();
}

앞에서도 말했다시피 NPE 위험에 노출된 코드이다. 만약 차를 산 사람이 없으면 ?
car.getPerson()의 결과값이 null이다.

그러면 car.getPerson().getName()의 값도 null이 반환되므로 NPE가 발생된다.
그래서 Java8 이전 개발자들이 아래와 같은 코드로 NPE를 방어했다.

public String getCarPersonName(Car car) {
	if(car != null) {
    	Person person = car.getPerson();
        if(person != null) {
        	return person.getName();
        }
    }
    return "No Car";
}

복잡한 코드가 탄생해버렸다. 변수를 참조할 때마다 null이 들어가는지 안들어가는지 확인하기 때문에 코드가 지저분해졌다. 또한 null 참조 여부 코드를 반복하다보면 코드의 구조가 엉망이 되고 가독성도 떨어진다.

분명 내가 짠 코드의 목적은 차를 산 사람의 이름을 알려주는 코드였는데 어느새 null값을 체크하는 코드로 가득찼다. 그리고 null 창시자가 의도 했던 바와 다르게 null은 자바 개발자들에게 NPE 방어라는 끝나지 않는 숙제를 남겼다.

null 때문에 발생하는 문제

  • 에러의 근원 : NPE
  • 코드를 어지럽힘 : 가독성이 떨어짐
  • 자바 철학에 위배 : 자바는 개발자로부터 모든 포인터를 숨겼는데 유일하게 null 포인터만 남아있다.

그래서 Java8 때 나온 것이 바로 Optional 클래스이다.

Optional 정의와 기본 사용법

Optional는 “존재할 수도 있지만 안 할 수도 있는 객체”, 즉, “null이 될 수도 있는 객체”을 감싸고 있는 일종의 래퍼 클래스이다. 쉽게 말해서 직접 다루기에 위험하고 까다로운 null을 담을 수 있는 특수한 그릇이라고 보면 된다. 한번 사용해보자.

Optional 선언

전에 짰던 코드로 예를 들겠다.

public class Car {
	private Optional<Person> person;
}

차를 산 사람이 있을 수도 있고 없을 수 도 있으니까 Optional로 감싸주었다.
하지만 Optional을 필드에 사용하는 것은 좋지 않으며, 이 내용은 나중에 설명하겠다.

Optional 객체 만들기

Optional이 쉽게 생성하라고 만든 정적 팩토리 메서드들이다.

  • Optional.empty()

빈 Optional 객체를 얻을 수 있음

Optional<Car> optCar = Optional.empty();
  • Optional.of()

null이 아닌 값을 포함하는 Optional을 만들 수 있음
null이 들어가면 NPE이 발생한다.

Optional<Car> optCar = Optional.of(car);
  • Optional.ofNullable()

of와 다르게 null이면 그대로 반환해준다.

Optional<Car> optCar = Optional.ofNullable(car);

Optional 객체 접근하기

Optional 객체를 가져오기 위한 메서드들이다.

  • get()

이 메서드는 값을 가져오는 가장 간단한 메서드이면서 동시에 가장 안전하지 않은 메서드이다. Optional에 값이 반드시 있다고 가정할 수 있는 상황이 아니면 쓰지 않아야 한다. 만약 확신할 수 없다면 중첩된 null 확인 코드를 넣는 상황과 똑같다.

  • orElse()

Optional이 비어있으면 인자를 반환한다.

  • orElseGet()

간단하게 생각해서 orElse는 null이 아니어도 실행이 되지만, orElseGet은 null일 때만 실행된다. 그래서 orElse의 게으른 버전이라고 부르는 것이다.

  • orElseThrow()

비어있을 때 예외를 터트린다. 주로 get()과 같이 쓴다. 터트릴 예외의 종류를 정할 수 있다.

  • ifPresent()

값이 존재하면 인수로 넘겨준 동작을 실행한다. 값이 없으면 아무 일도 일어나지 않는다.

Optional을 왜 쓸까 ?

위의 사진을 보면, Java8 이전의 코딩스타일이 Java8 이후의 코딩스타일보다 더
명확하고 코드길이가 적게 표현되는데 왜 Optional을 사용합니까 ? 라는 질문이다.

여기까지 왔다면 답을 알 것이다. 위 질문을 한 사람은 Optional을 Optional답게 사용하지 못했기 때문이다. Optional을 Optional답게 사용하지 못하는게 무슨 말일까 ?

"Optional을 사용하는 이유는 고통스러운 null 처리를 직접하지 않고 Optional 클래스에 위임하기 위함이다" 라고 생각하면,
Java8 이전에 null을 체크하는 코드와 다를 바 없다.

오히려 Optional을 생성하고 그 객체를 접근해야하니까 코드가 늘어난다.
위 질문을 한 사람도 아마 이런 생각을 가지고 있었을 것이다.

Optional을 Optional답게 사용하는 개발자들은 Java8 이전 스타일을 다음과 같이 변경할 것이다.

Optional<Employee> employee = Optional.of(employeeService)
        .map(EmployeeService::getEmployee)   
        .map(Employee::getId)                
        .ifPresent(System.out::println);     

Java8 이후 스타일이 바뀌어야 된다는 생각이 들지 않는다면 이 글을 처음부터 다시 읽는 것을 추천한다.

안타깝게도 Optional 관련해서 개발자들이 제일 많이 하는 질문 중 하나가 “Optional 적용 후 어떻게 null 체크를 해야하나요?” 이다. 사실 이 질문에 대한 답변은 아래와 같다.

“null 체크를 하실 필요가 없으시니 하시면 안 됩니다.”

Optional에 대한 오해

그냥 Optional을 쓰면 되지, 왜 "Optional 답게 써라"라고 말하는 것일 까?

Optional을 만든 Brian Goetz는 스택오버플로우 에서 Optional을 만든 의도에 대해 다음과 같이 설명했다.

… it was not to be a general purpose Maybe type, as much as many people would have liked us to do so. Our intention was to provide a limited mechanism for library method return types where there needed to be a clear way to represent “no result” …
Optional은 많은 사람들이 우리(자바 언어 설계자)에게 기대했던 범용적인 Maybe 타입과는 다르다. 라이브러리 메서드가 반환할 결과값이 ‘없음’을 명백하게 표현할 필요가 있는 곳에서 제한적으로 사용할 수 있는 메커니즘을 제공하는 것이 Optional을 만든 의도였다.

요약해서 말하자면 Optional은 메서드의 반환값이 '없음'을 나타내는 것이 주 목적이며, 사람들의 기대와 다른 의도로 만들어졌다. 공식 API 문서 에서도 만든 의도가 무엇인지 설명되어있다.

API Note:Optional is primarily intended for use as a method return type where there is a clear need to represent “no result,” and where using null is likely to cause errors. A variable whose type is Optional should never itself be null; it should always point to an Optional instance.
메서드가 반환할 결과값이 ‘없음’을 명백하게 표현할 필요가 있고, null을 반환하면 에러를 유발할 가능성이 높은 상황에서 메서드의 반환 타입으로 Optional을 사용하자는 것이 Optional을 만든 주된 목적이다. Optional 타입의 변수의 값은 절대 null이어서는 안 되며, 항상 Optional 인스턴스를 가리켜야 한다.

Optional을 Optional답게

그럼 어떤 상황에서 Optional을 활용해야, 쓰지 말아야 창시자의 의도대로 잘 사용할까 ? 중요하다고 생각되는 몇 가지를 알아보자.

1. isPresent()-get() 대신 orElse(), orElseThrow()

//안좋음
Optional<Member> member = ...;
if (member.isPresent()) {
    return member.get();
} else {
    return null;
}

// 좋음
Optional<Member> member = ...;
return member.orElse(null);



// 안 좋음
Optional<Member> member = ...;
if (member.isPresent()) {
    return member.get();
} else {
    throw new NoSuchElementException();
}

// 좋음
Optional<Member> member = ...;
return member.orElseThrow(() -> new NoSuchElementException());

2. Stream처럼

Optional은 Stream처럼 map(), filter() 같은 메서드를 가지고 있다.
초반 부분에 나왔던 getCarPersonName메서드를 스트림API처럼 바꿔보겠다.

//Java8 이전에 null을 체크하는 코드
public String getCarPersonName(Car car) {
	if(car != null) {
    	Person person = car.getPerson();
        if(person != null) {
        	return person.getName();
        }
    }
    return "No Car";
}
//map을 이용해 리펙토링한 코드
public String getCarPersonName(Car car) {
	return Optional.ofNullable(car)
    		.map(Car::getPerson)
            .map(Person::getName)
            .orElse("No Car");
}

훨씬 간단해진 것을 볼 수 있다. 상세하게 코드를 설명하자면
1. ofNullable()을 사용한 이유는 객체를 Optional로 감싸줌과 동시에 객체가 null일 수 있기 때문
2. 총 두 번 Optional 타입을 바꿔줌. Optional<Car> -> Optional<Person>
3. orElse()로 null이 들어왔다면 default로 사용할 문장을 적어주었다.

3. Optional은 필드에서 사용 금지

Optional은 필드에 사용할 목적으로 만들어지지 않았다.

//안좋음
public class Car {
	private Optional<Person> person;
}

//좋음
public class Car {
	private Person person;
}

4. of(), ofNullable() 헷갈리지 말자

of()는 인자가 null이 아님이 확실할 때만 사용해야 하며, 인자가 null이면 NPE가 발생한다. ofNullable()은 인자가 null일 수도 있을 때만 사용해야 하며, 인자가 null이 아님이 확실하면 of()를 사용해야 한다.

//좋음
return Optional.ofNullable(user.getEmail()); //email이 선택사항이라고 가정

//안 좋음
return Optional.of(user.getEmail()); //만약 email이 null이면 NPE 발생



//좋음
return Optional.of(person.getName());  // 사람이름은 null일 수가 없다. 

//안좋음
return Optional.ofNullable(person.getName());

Optional을 Optional답게 - 실전예시

get()으로 객체를 불러올 때 주의해야 할 점은 Optional.get() 호출 전에 Optional 객체가 값을 가지고 있음을 확실히 알아야 한다. 위 코드에서 만약 빈 객체가 들어왔으면 NoSuchElementException이 터지기 때문에 다음과 같은 코드로 고쳐주어야 한다.

예외를 명시적으로 처리해주고 싶다면 람다를 이용해 작성해주면 된다.

마치며 - 정리

  • NPE 등장

  • Java8 이전에는 NPE를 방지하기 위해 하나하나 변수가 참조하는 것이 null인지 체크하는 if문을 작성했음

  • 하나하나 작성하기엔 유지보수가 어렵고, 터질 NPE는 터지기 때문에 골치아팠음

  • 메서드에 null을 반환하면 오류가 발생할 가능성이 매우 높은 경우에 '결과 없음'을 명확하게 드러내기 위해 메소드의 반환 타입으로 사용되도록 매우 제한적인 경우로 Optional을 설계

  • 하지만 사람들은 Optional을 만든 의도와 다르게 사용하기 시작함

  • 의도에 맞는 Optional사용은 좋은 코드가 되지만, 그렇지 않은 경우(Optional을 남발함)에는 오히려 더 큰 부작용이 생김
    NPE의 친구인 NoSuchElementException

    NSEE가 생기는 이유는 Optional로 받은 변수, 객체에 값이 있는지 판단하지 않고 접근하려고 했기 때문
    다른 말로 NPE를 피하려고 Optional로 감쌌는데 또 다른 예외가 발생되어 버림

  • 그래서 Optional을 올바르게 사용하자!

References

https://jang8584.tistory.com/263
https://www.daleseo.com/java8-optional-effective/
https://jang8584.tistory.com/263
https://www.latera.kr/blog/2019-07-02-effective-optional/

profile
부산소프트웨어마이스터고

3개의 댓글

comment-user-thumbnail
2022년 6월 21일

잘 읽었습니다 감사합니다

답글 달기
comment-user-thumbnail
2022년 6월 24일

좋은 글 감사합니다~!

답글 달기
comment-user-thumbnail
3일 전

감사합니다.

답글 달기