독서이야기/엘레강트 오브젝트 - 새로운 관점에서 바라본 객체지향

[엘레강트 오브젝트] 4.1 절대 NULL을 반환하지 마세요

사랑꾼이야 2023. 4. 9. 23:02
반응형

이 내용은 엘레강트 오브젝트 를 읽으면서 정리한 내용을 포함하고 있습니다.

  • 객체의 신뢰
  • 빠르게 실패하기 vs. 안전하게 실패하기
  • NULL의 대안
  • 정리

객체의 신뢰

객체가 NULL을 반환한다면, 해당 객체를 신뢰할 수 없게 된다.

  • 반환된 값이 객체인지부터 확인해야 하기 때문에, 객체에게 작업을 요청한 후 안심하고 결과에 의지할 수 없다.
String title = x.title();
if (title == null) {
    print("Cant't print; it's not a title.");
    return ;
}
print(title.length());
  • 객체라는 사상에는 우리가 신뢰하는 엔티티라는 개념이 담겨져 있다.
  • 객체는 자신만의 생명주기, 자신만의 행동, 자신만의 상태를 가지는 살아있는 유기체

신뢰라는 말에는 객체가 자신의 행동을 전적으로 책임지고 우리가 어떤 식으로든 간섭하지 않는다는 의미가 담겨져 있다.

if (title == null) {
    print("Can't print; it's not a title.");
    return ;
}
  • 반환값을 검사하는 방식은 애플리케이션에 대한 신뢰가 부족하다는 명백한 신호이다.

이러한 신뢰의 부족은 결과적으로 유지보수성의 심각한 손실로 이어진다, NULL을 반환하는 방식은 잘못됐으며 보기애도 좋지 않다.

빠르게 실패하기 vs. 안전하게 실패하기

안전하게 실패하기

  • 버그, 입출력 문제, 메모리 오버플로우 등이 발생한 상황에서도 소프트웨어가 계속 실행될 수 있도록 최대한 많은 노력을 기울일 것을 권장
  • NULL을 반환하는 방법도 일종의 생존 기법
  • 요청자의 잘못이지만, 안전하게 실패하기에서는 이 상황을 구조하기 위해 노력
  • IOException을 던지는 대신, 누군가 이 상황을 처리할 수 있도록 NULL을 반환

빠르게 실패하기

  • 정반대의 접근 방식
  • 일단 문제가 발생하면 곧바로 실행을 중단하고 최대한 빨리 예외 전달
  • 소프트웨어가 부서지기 쉽고 모든 단일 제어 지점에서 중단되도록 설계됐다면, 단위 테스트에서 실패 상황을 손쉽게 재현 가능
  • 상황을 구조하지 않는 대신, 가능하면 실패를 분명하게 전달

어떠한 방법이 더 나을까?

문제를 더 빨리 발견할수록 문제를 수정하는 시간 역시 빨라진다. 버그가 더 빨리 드러날수록 더 신속하게 대응할 수 있다. 그리고 모든 버그 수정은 제품을 좀 더 안정적이고 견고하게 만들어준다. 왜 그렇게 많은 Java 메서드가 예외를 던지지 않고 NULL을 반환하는 것일까? 아마 대부분의 개발자들이 안전하게 실패하기 를 선택한다.

NULL의 대안

public User user(String name) {
    if (/* 데이터베이스에서 발견하지 못했다면 */) {
        return null;
    }
    return /* 데이터베이스로부터 */
}
  • 실제 객체 대신 NULL을 반환하는 가장 흔한 경우
  • 객체가 존재하지 않는 상황을 예외로 보지 않기 때문에 예외를 던지고 싶지 않는 경우
  • 데이터베이스에서 사용자를 조회했지만 사용자가 존재하지 않는 경우

NULL을 대체할 수 있는 몇 가지 방법이 있다.

첫번째, 메서드를 두 개로 나누는 것

  • 객체의 존재를 확인하는 메서드
  • 객체를 반환하는 메서드
public boolean exists(String name) {
    if (/* 데이터베이스에서 발견하지 못했다면 */) {
        return false;
    }
    return true;
}
public User user(String name) {
    return /* 데이터베이스로부터 */
}
  • 이 방법은 데이터베이스에 요청을 두 번 전송하기 때문에 비효율적이라는 단점
  • 데이터베이스 안에 레코드가 존재하는지 확인한 후에 데이터베이스로부터 실제 객체를 읽어 와야 하기 때문

두번째, NULL을 반환하거나 예외를 던지는 대신 객체 컬렉션을 반환하는 것

public Collection<User> users(String name) {
        if (/* 데이터베이스에서 발견하지 못했다면 */) {
        return new ArrayList<>(0);
    }
    return Collections.singleton(/* 데이터베이스로부터 */);
}
  • 여기에서는 사용자를 발견하지 못한 경우 빈 컬렉션을 반환

자바8의 경우 java.util.Optional 이나 유사한 도구를 사용하는 방법도 있다.

세번째, 널 객체(null object) 디자인 패턴

  • 원하는 객체를 발견하지 못할 경우, 겉으로 보기에는 원래의 객체처럼 보이지만 실제로는 다르게 행동하는 객체를 반환한다.
  • 예를 들어, SqlUser을 발견하지 못한 경우 널 객체인 NullUser를 반환하기로 한다면 이 두 객체는 동일한 타입이어야 한다. 둘 다 동일한 User 인터페이스를 구현해야 한다.다음은 NullUser를 구현할 수 있는 한가지 방법이다.
class NullUser implements User {
    private final String label;
    NullUser(String name) {
        this.label = name;
    }
    @Override
    public String name() {
        return this.label;
    }
    @Override
    public void raise(Cash salary) {
        throw new IllegalStateException("제 봉급을 인상할 수 없습니다. 저는 스텁(stub)입니다.");
    }
}

정리

  • 절대로 NULL을 반환하지 마세요.
  • 찾지 못한 뭔가를 반환할 필요가 있다면 NULL 대신 예외를 던지거나, 컬렉션을 반환하거나, 널 객체를 반환하세요
반응형