[번역글] Java 8(jdk 1.8) Optional 객체 사용 가이드

자바프로그래머들이 마주하는 가장 흔한 Exception은 NullpointerException 에러입니다. 이 Exception은 JVM에 의해
발생되는 RuntimeException 중 하나입니다.

여러분들도 아시다시피 NullpointerException은 애플리케이션은 Object가 필요한데 이 값이 null 값일 때 발생하는 에러입니다. Null 값은 자바프로그래머들이 대부분 쉽게 간과할 만한 케이스입니다.

Null 값은 비즈니스 로직에서 NullpointerException이 발생하지 않도록 애플리케이션 내부에서 처리되어야 합니다. 
이것은 불필요한 null 체크 코드를 만들어내게 됩니다.

이러한 뻔한 코드들을 처리하기 위해서, Java 8에서 Optional 타입이 도입되었습니다.

Oracle에 따르면 "Java 8 Optional"은 값이 없거나 null인 값의 컨테이너 유형으로 작동합니다. Optional 클래스는 java.util 패키지에 있는 final 클래스입니다. 즉, 확장할 수 없는 클래스입니다.


Optional이 없으면 발생할 문제들

자바 8 Optional이 없을 때 발생하는 문제점들을 체크해보겠습니다.

다음의 메소드가 존재한다고 했을 때, 이 메소드는 개별 아이디로 고용자의 상세정보를 데이터베이스에서 검색합니다.

Employee findEmployee(String id) { 
 ... 
}; 


다음은 id가 데이터베이스에 존재하지 않는 경우입니다. 메소드는 null을 리턴할 것입니다.

Employee employee = findEmployee("1234"); 
System.out.println("Employee's Name = " + employee.getName()); 

 

위의 개발자가 null 체크를 하지 않았기 때문에 코드는 NullPointerException을 낼 것입니다.


Optional 객체가 제공하는 NullPointerException 대응 솔루션

이제 Java 8 Optional이 어떻게 위 문제를 해결하고 NullPointerException을 제거하는 데 도움이 되는지 살펴보겠습니다.

 

위 코드에 필요한 수정 사항은 다음과 같습니다.

Optional < Employee > findEmployee(String id) { 
 ... 
}; 


위의 코드로써 매개변수 id에 해당하는 Employee가 없을 수 있는 가능성을 포함한 Optional 객체를 반환해주고 있
습니다. 이제 개발자에게 명시적으로 이러한 사실을 전달해주어야합니다.

Optional < Employee > optional = findEmployee("1234"); 
optional.ifPresent(employee -> { 
 System.out.println("Employee name is " + employee.getName()); 
}); 

 

처음에 Optional 객체를 생성하고 Optional 안쪽에 있는 객체에 다가가기 위해 다양한 유틸리티 메소드를 활용할 수 있습니다. ifPresent() 메소드는 Employee 객체가 존재하는 경우에만 기재된 람다 표현식을 실행합니다. 그렇지 않으면 실행하지 않습니다.


Optional 객체를 사용함으로써의 장점

아래의 리스트는 자바 8 Optional을 사용함으로써 가질 수 있는 장점 리스트입니다.
 - 런타임상에서의 NullPointerException을 막을 수 있다.
 - 직접 객체의 Null 값 체크가 요구되지 않는다.
 - 뻔할 뻔자의 코드가 쓰일 필요가 없다.(대표적으로 if로 null 체크하는 것)
 - 클린하고 아름다운 코드로 API를 개발할 수 있다.


Optional 객체 생성하기

- Optional 객체를 생성하는 각기 다른 방법을 살펴보도록 하겠습니다.

1. 빈 Optional 객체 생성
- 아래의 코드는 빈 Optional 객체를 생성하는 방법을 보여줍니다. 단지 값의 부재만을 의미합니다.

Optional < Employee > employee = Optional.empty(); 


2. 값이 들어있는 Optional 객체 생성
- 아래의 코드는 값이 들어있는 Optional 객체를 생성하는 방법을 보여줍니다.

Employee employee = new Employee("1234", "TechBlogStation"); 
Optional < Employee > optional = Optional.of(employee); 

 

여기서  Optional.of()의 인자로 null 값이 들어오게 된다면 NullPointerException을 발생시키고 Optional 객체를
생성할 수 없습니다.

3. Null 과 nonNull 모두 될수 있는 Optional 객체

Optional < Employee > optional = Optional.ofNullable(employee); 

 

Non-Null 인 값 무언가가 Optional.ofNullable()의 인자로 들어오게 된다면, 특정 값이 들어있는 Optional 객체를
리턴하게 됩니다. 반대로 Null 값이 들어오게 된다면 빈 Optional 객체로 반환됩니다.


Optional 객체의 값의 존재여부 확인하기

- 이제 Optional 객체의 값이 존재하는지 확인하는 여러가지 방법을 살펴보겠습니다.

 

1. isPresent() 메소드
 - Optional 객체의 인스턴스가 값을 가지고 있다면 true를 반환합니다. 반대면 false를 반환합니다.

// Suppose the non null value is present in Optional    
if (optional.isPresent()) { 
	System.out.println("Value - " + optional.get());  
} 
// Suppose the null value is present in Optional         
else {     
	System.out.println("Optional is empty");  
} 


2. ifPresent() 메소드
 - 인자로 Consumer 함수를 전달해야합니다. Consumer 함수는 Optional 객체에 값이 있는 경우에만 동작합니다.
Consumer 함수는 람다형식으로 작성하였습니다.

optional.ifPresent(value -> {    
  System.out.println("Value present - " + value);  
}); 

get() 메소드를 활용한 값 검색.

get() 메소드는 Optional 객체 안에 있는 객체를 반환하기 위해서 사용됩니다. 값이 없는 경우엔 NoSuchElementException을 날려버립니다.

Employee employee = optional.get() 

 

값이 없는 경우에는 Exception을 날려버리기 때문에 get() 메소드를 사용하기 전 반드시 값이 존재하는지 체크하는 부분이 필요합니다.


orElse() 메소드를 활용한 Optional 객체의 기본값(대체값) 반환.

orElse() 메소드는 default 값을 반환받을 때 활용할 수 있습니다. 예컨데, Optional 객체안의 값이 비어있는 경우 최소한의 데이터만 가진 default 값을 리턴해야 하는 경우 사용합니다.

// Below will return "Unknown Employee" if employee is null  
User finalEmployee = (employee != null) ? employee : new Employee("0", "Unknown Employee"); 

예컨데, 위의 로직을 Optional 객체를 활용하여 수정해보겠습니다.

// Below will return "Unknown Employee" if employee is null  
User finalEmployee = optional.orElse(new Employee("0", "Unknown Employee")); 

orElseGet() 메소드를 활용한 기본값 반환.

- 위에서 봤듯이, orElse() 메소드는 Optional 객체의 값이 빈 경우 직접적으로 기본값을 가져오지만, orElseGet() 메소드는 Supplier 함수를 인자로 받아 Supplier 함수를 실행하게 됩니다. Supplier 함수가 반환하는 값이 기본값이 됩니다.

User finalEmployee = optional.orElseGet(() -> { 
 return new Employee("0", "Unknown Employee"); 
}); 

Optional 객체 안에 값이 없으면 예외발생

orElseThrow() 메소드는 Optional 객체가 비어있는 경우 예외를 던지는 메소드입니다. 예컨데, 특정 request의 파라미터로 id를 넘겼는데 해당 데이터가 없는 경우 이 메소드를 활용하여 ResourceNotFound() 등과 같은 사용자 Exception클래스를 직접 정의하여 처리할 수 있습니다.

@GetMapping("/employees/{id}") 
public User getEmployee(@PathVariable("id") String id) { 
	//employeeRepository.findById(id) 는 id에 해당하는 Optional 객체를 반환 
 	return employeeRepository.findById(id)
           .orElseThrow(() -> new ResourceNotFoundException("Employee not found with id " + id);); 
}

 

filter() 메소드를 활용한 값 필터

Optional 객체가 있을 때, Employee의 특정 gender 변수의 속성을 확인하고 실행해야 한다면 다음과 같습니다.

if(employee != null && employee.getGender().equalsIgnoreCase("MALE")) {     
// calling the function  
} 

 

위의 코드를 아래와 같이 filter() 메소드를 활용하여 수정해보겠습니다.

optional.filter(user -> employee.getGender().equalsIgnoreCase("MALE")) 
   .ifPresent(() -> { /* Your function */ }) 

 

filter() 메소드는 predicate를 인자로 전달합니다. Optional에 null이 아닌 값이 있고 제공된 조건(predicate)과 일치하는 경우 이 메소드는 해당 값을 포함하는 Optional 객체를 반환합니다. 반대의 경우 빈 Optional 객체가 반환됩니다.

 

map()을 사용하여 값 추출 및 변환

직원의 주소를 추출하고 지정된 조건에 따라 해당 위치를 인쇄하려는 시나리오가 있다고 가정합니다.
아래의 예제를 따라와보세요.

Employee 클래스에는 getAddress() 라는 메소드를 가집니다.

class Employee {
	private Address address;

	public Address getAddress() { 
		return this.address; 
	}
}


아래의 코드는 전형적인 접근방법입니다.

if (employee != null) { 
	Address address = employee.getAddress(); 
	if (address != null && address.getCountry().equalsIgnoreCase("USA")) { 
		System.out.println("Employee belongs to USA"); 
	} 
} 


아래의 코드는 Optional 객체의 map() 메소드를 이용한 코드입니다.

userOptional.map(Employee::getAddress)
            .filter(address -> address.getCountry().equalsIgnoreCase("USA"))
            .ifPresent(() -> {System.out.println("Employee belongs to USA");}
); 

 

위의 코드는 이전 코드보다 읽기 쉽고 비교적 간결합니다. 보다 효율적이기도 합니다. 자세히 살펴보시죠

 


 - map() 메소드를 활용해서 Employee 클래스의 getAddress 메소드를 실행하여 Address Optional 클래스를 추출합니다.

addressOptional = employeeOptional.map(Employee::getAddress) 

 - 주소지가 USA인 객체인 경우를 필터링합니다. 

Optional usaAddressOptional = addressOptional.filter(address -> address.getCountry().equalsIgnoreCase("USA"));  

 

 - ifPresent() 메소드를 이용하여 존재하면 출력합니다. 

usaAddressOptional.ifPresent(() -> { System.out.println("Employee belongs to USA");});  


위의 코드에서 map() 메소드가 빈 Optional 객체를 리턴하는 경우가 있습니다. 
1. Employee 인스턴스를 employeeOptional 변수가 참조하지 않고 있는 경우 
2. Employee 인스턴스를 참조하고 있지만 getAddress() 메소드에서. null을 리턴하는 경우. 

위의 경우가 아니라면, 직원의 주소를 포함한 Optional 객체를 리턴하게 될것입니다. 


flatMap()을 활용한 Optional 

.map()과  .flatMap()을 비교한 소스입니다. 

class Employee {  
    public Address address;  
    Address getAddress(){  
        return this.address;  
    }  
    Employee(Address address){  
        this.address = address;  
    }  
}  
class Address {  
    public String country;  
    Address(String country){  
        this.country = country;  
    }  
    String getCountry(){  
        return this.country;  
    }  
}  


두개의 클래스가 다음과 같이 존재할 때!! 

public static void main(String[] args) {  
    Address addressObj = new Address("USA");  
    Optional userOptional = Optional.of(new Employee(addressObj));  

    //비교 1 (map)  
    userOptional.map(user -> {  
        return user.getAddress();  
    }).filter(address -> address.getCountry().equalsIgnoreCase("USA"))  
      .ifPresent(address -> {System.out.println("Employee belongs to USA");});  

    //비교 2 (flatMap)  
    userOptional.flatMap(user -> {  
        return Optional.ofNullable(user.getAddress());  
    }).filter(address -> address.getCountry().equalsIgnoreCase("USA"))  
      .ifPresent(address -> {System.out.println("Employee belongs to USA");});  
}

 

map() 메소드는 중간에 그냥 객체를 리턴해야하고 flatMap() 메소드는 Optional을 씌운 객체로 리턴해야합니다. 



Optional 클래스에 관한 더욱 실용적인 자료는 이 글을 참조해보시기 바랍니다. 간단한 설명의 영문과 용례가 정리되어 
있으니 참조해보시기 바랍니다. 
https://dzone.com/articles/using-optional-correctly-is-not-optional

 

26 Reasons Why Using Optional Correctly Is Not Optional - DZone Java

We take a look at the top 25 most important concepts to keep in mind when working with Optionals in Java, focusing on null variables and more.

dzone.com

 

댓글

Designed by JB FACTORY