자바의 모든 객체는 null 일 수 있고 null을 참조할 경우 NullPointerException을 만나게 됩니다.

NullPointerException가 무엇인지 자바 기초 문법을 익힌 사람이라면 대부분 아는 내용이므로

이 글에서 자세하게 언급하지는 않겠습니다.

현업에서 종종 레거시 프로젝트에서 한 번쯤은 확인 가능한 코드가 있습니다.

어떤 조건들에 대해 무한히 체크하는 패턴을 볼 수 있습니다.

특히 비즈니스 로직이 복잡한 곳이 그런데요.

아래와 같이 a, b, c 객체의 null 체크 조건식과 그에 따른 비즈니스 로직이 추가되는 sudo 코드입니다.

체크해야 할 조건이 늘어날수록 코드는 복잡해지고 가독성도 떨어지게 됩니다.

이러한 반복되는 패턴 코드를 깊은 의심(deep doubt) 이라고도 합니다.

if (a != null) { // 객체 a의 null 체크 
	B b = a.getXxx();
    ...
    if ( b != null) { // 객체 b의 null 체크 
    	C c = b.getXxx();
        ...
        if ( c != null) { // 객체 c의 null 체크 
        	D d = c.getXxx();
            ...
        }
    }
}

 

자바 8 이후로 추가된 Optional 클래스를 통해 NullPointerException 등 null 참조로 인해 발생하는 문제들을

처리하는 코드를 가독성 있게 작성할 수 있습니다.

java.util.Optional<T> 클래스는 선택형 값을 캡슐화하는 클래스입니다.

예제를 통해서 기능들을 확인해보겠습니다.

 

Phone, Person, Insurance, Main 클래스를 각각 생성하겠습니다.

처음 예제와 같이 null 참조에 대해 조건문으로 처리한 예제이고

Insurance → Phone Person  중첩되어 있는 구조입니다.

public class Insurance {
	private String name;
	public String getName() { return name; }
}
public class Phone {
	private Insurance insurance;
	public Insurance getInsurance() { 
		return insurance;
	};
}
public class Person {
	private Phone phone;
	
	public Phone getPhone() {
		return phone;
	}
}
public class Main {
	public static void main(String[] args) {
		Person p = new Person();
		System.out.println(getPhoneInsuranceName(p));
	}
	
	public static String getPhoneInsuranceName(Person person) {
		if (person != null) {
			Phone phone = person.getPhone();
			if(phone != null) {
				Insurance insurance = phone.getInsurance();
				if(insurance != null) {
					return insurance.getName();
				}
			}
		}
		return "None";
	}
}

이전 문법으로 이렇게 처리는 가능합니다

여기서 조금 더 복잡해져서 if 문을 누락한다면? 런타임에서 null 참조가 있는 속성에 접근 전까지는 알 수가 없습니다.

Optional 클래스를 이용해 개선해보겠습니다.

import java.util.Optional;

public class Person {
	private Optional<Phone> phone;
	
	public Optional<Phone> getPhone() {
		return phone;
	}
}
import java.util.Optional;

public class Phone {

	private Optional<Insurance> insurance;

	public Optional<Insurance>  getInsurance() { 
		return insurance;
	}
}
public class Insurance {
	private String name;
	public String getName() { return name; }
}
import java.util.Optional;

public class Main {
	public static void main(String[] args) {
		Person person = new Person();
		Optional<Person> optPerson = Optional.of(person);
		System.out.println(getPhoneInsuranceName(optPerson));
	}
	
	public static String getPhoneInsuranceName(Optional<Person> person) {
		String name = person.flatMap(Person::getPhone)
						.flatMap(Phone::getInsurance)
						.map(Insurance::getName)
						.orElse("None");
		return name;
	}
}

Main의 테스트 코드를 실행하면 NullPointerException이 발생합니다.

기존 처리방식의 코드와 차이점은 Person 속성에 접근하여 null 참조하기 전에 NullPointerException이 발생하게 됩니다.

도메인에서 사용 주의점은 직렬화에서 문제가 발생하는데 이것은 자바 설계 상 Optional의 용도가

선택형 반환 값을 지원하지 않게 설계됐기 때문입니다. 필드 형식으로 사용하지 않는 것을 가정하므로 Serializable 인터페이스를 구현하지 않기 때문입니다.

 

Optional은 예제에서 사용한 of 메서드 포함해서 다음과 같은 메서드를 제공합니다.

 

empty 메서드

빈 Optional 객체를 얻을 수 있습니다.

Optional<Person> optPerson = Optional.empty();

 

of 메서드

null 아닌 값을 포함하는 Optional을 만들 수 있습니다.

person이 null 이면 즉시 NullPointerException 발생.

Person person = new Person();
Optional<Person> optPerson = Optional.of(person);

 

ofNullable 메서드

null 값을 저장할 수 있는 Optional을 만들 수 있습니다.

person이 null이면 빈 Optional 객체를 반환합니다.

Person person = new Person();
Optional<Person> optPerson = Optional.ofNullable(person);

 

get 메서드

Optional 랩핑 된 값이 있으면 반환하고 없으면 NoSuchElementException을 발생시킵니다.

따라서 반드시 Optional에 값이 있다고 가정할 수 있는 상황에서만 사용하는 것이 바람직합니다.

Person person = new Person();
Person tmp = Optional.get(person);

 

orElse 메서드

Optional 랩핑된 값이 있는 경우 해당 값을 반환하고 랩핑된 값이 없으면 인수(기본값)를 제공.

첫 번째, 두 번째 줄 주석을 각각 해제/주석 처리하면서 테스트해보면 어느 때나 randomName 메서드를 호출하는 것을

알 수 있습니다.

import java.util.Optional;
import java.util.Random;

public class Main {
	public static void main(String[] args) {
		//Optional<String> optStr = Optional.empty();
		Optional<String> optStr = Optional.of("ABC");
		System.out.println(optStr.orElse(randomName()));
	}
	
	public static String randomName() {
		System.out.println("randomName");
		return "T" + new Random().nextInt(); 
	}
 }

 

orElseGet 메서드

Optional 랩핑 된 값이 있는 경우 해당 값을 반환하고 랩핑된 값이 없으면 Supplier를 실행하고 호출 결과를 반환.

첫 번째, 두 번째 줄 주석을 각각 해제/주석 처리하면서 테스트해보면 null 일 경우에만 randomName() 메서드가 호출되는 것을 확인 가능합니다.

import java.util.Optional;
import java.util.Random;

public class Main {
	public static void main(String[] args) {
		Optional<String> optStr = Optional.empty();
		//Optional<String> optStr = Optional.of("ABC");
		System.out.println(optStr.orElseGet(() -> randomName()));
		//System.out.println(getPhoneInsuranceName(optPerson));
	}
	
	public static String randomName() {
		System.out.println("randomName");
		return "T" + new Random().nextInt(); 
	}
}

 

orElseThrow

Optional 랩핑 값이 존재하면 값을 반환하고, 값이 없으면 Supllier에서 생성한 예외 발생.

import java.util.Optional;
import java.util.Random;

public class Main {
	public static void main(String[] args) {
		Optional<String> optStr = Optional.empty();
		//Optional<String> optStr = Optional.of("ABC");
		try {
			System.out.println(optStr.orElseThrow(() -> new Exception("orElseThrow")));
		} catch(Exception e) {
			e.printStackTrace();
		}
		//System.out.println(getPhoneInsuranceName(optPerson));
	}
}

첫 번째, 두 번째 줄 주석을 각각 해제/주석 처리하면서 테스트해보면 null 일 경우에만 예외 발생.

예외 종류는 선택 가능합니다.

 

 

ifPresent

값이 존재하면 지정된 Consumer를 실행. 

값이 없으면 아무 수행하지 않음.

import java.util.Optional;
import java.util.Random;

public class Main {
	public static void main(String[] args) {
		Optional<String> optStr = Optional.empty();
		//Optional<String> optStr = Optional.of("ABC");
		optStr.ifPresent(t -> randomName());
	}
	
	public static String randomName() {
		System.out.println("randomName");
		return "T" + new Random().nextInt(); 
	}
}

 

ifPresentOrElse

값이 존재하면 지정된 Consumer를 실행.

null 이 빈 값일 경우 Runnable을 지정할 수 있다.

import java.util.Optional;
import java.util.Random;

public class Main {
	public static void main(String[] args) {
		//Optional<String> optStr = Optional.empty();
		Optional<String> optStr = Optional.of("ABC");
		optStr.ifPresentOrElse(t-> System.out.println(t), () -> System.out.println("empty"));
	}
}

 

isPresent

값이 존재하면 true를 반환하고, 값이 없으면 false를 반환.

public class Main {
	public static void main(String[] args) {
		//Optional<String> optStr = Optional.empty();
		Optional<String> optStr = Optional.of("ABC");
		if(optStr.isPresent()) {
			System.out.println("exist");
		} else {
			System.out.println("empty");
		}
	}
}

 

이외에 Stream에서 제공했던 것처럼 map, flatmap도 제공된다.

그리고 기본형 특화 클래스인 OptionalInt, OptionalLong, OptionalDouble 있으나 

map, flatmap, filter 등의 기능들을 지원하지 않는다고 합니다.

 

마치며

Optional이 많은 null 참조 관련 처리를 다 해주는 만능 해결사 노릇을 할 것이라 기대했지만

초기 설계부터가 만능 처리보다는 명시적으로 이것이 선택 값인지 아닌지를 나타내는 

역할에 초점을 맞춘 것으로 생각된다. (도메인 직렬화 문제, get의 경우 NoSuchException 처리 등 )

저 같은 경우 웹 Rest API 개발 시에 요청 변수나 반환 값에 현업에서 적용하여 사용해봤습니다.

적절하게 반환 값 같은 곳에 사용하면 좋은 효과를 거둘 수 있을 것 같습니다.

 

+ Recent posts