네로개발일기

개발자 네로의 개발 일기, 자바를 좋아합니다 !

반응형

 이전 글 

https://frogand.tistory.com/209

 

[Java] Optional를 잘 사용하는 법 1 (NPE, if문으로 null 체크를 하지 말자)

Optional을 왜 사용하냐, 그럼 언제 사용해야할까, 에 대한 질문과 답을 적으려고 한다. 요점 - NPE을 방지하기 위해 사용하는 클래스. - null이 들어올 수 있는 값을 한 번 감싸는 Wrapper 클래스. NPE (Nul

frogand.tistory.com

Stream 처럼 사용하기

Optional을 제대로 사용하려면 최대 1개의 원소를 가지고 있는 특별한 Stream이라고 생각하는 것이 좋다. Optional 클래스와 Stream 클래스는 직접적인 구현, 상속과는 관련없지만 사용방식이 유사하다. Stream 클래스의 map(), flatMap(), filter() 와 같은 메서드가 Optional도 가지고 있기 때문에 Stream처럼 사용한다고 생각하면 될 것 같다.

 

map()

public String getCityOfMemberFromOrder(Order order) {
    if (order != null) {
        Member member = order.getMember();
        if (member != null) {
            Address address = member.getAddress();
            if (address != null) {
                String city = address.getCity();
                if (city != null) {
                    return city;
                }
            }
        }
    }

    return "Seoul"; // default
}

해당 코드를 Optional을 사용하여 변경하겠다.

public String getCityOfMemberFromOrder(Order order) {
    return Optional.ofNullable(order)
            .map(Order::getMember)
            .map(Member::getAddress)
            .map(Address::getCity)
            .orElse("Seoul");
}

이전 코드의 전통적인 NPE 방어 패턴에 비해 훨씬 간결하고 명확해진 코드를 볼 수 있다. 기존에 존재하던 조건문이 사라지고 Optional의 메서드 체이닝으로 대체되었다.

- ofNullable() 정적 팩토리 메서드를 활용하여 Order 객체를 Optional로 감쌌다. Order 객체가 null인 경우를 대비해 of() 메서드 대신에 ofNullable() 메서드를 사용했다.

- map() 메서드의 연쇄 호출을 통해 Optional 객체를 3번 변환하였다. 

Optional<Order> -> Optional<Member> -> Optional<Address> -> Optional<String>

- orElse() 메서드를 호출하여 기본값으로 사용할 값을 정해주었다.

 

filter()

null 체크로 시작하는 if 조건문 패턴이 많이 있다.

if (obj != null && obj.do() ...)

예를 들어, 주어진 시간(분) 내에 생성된 주문을 한 경우에만 해당 회원 정보를 구하는 메소드를 위 패턴으로 작성해보자.

public Member getMemberIfOrderWithin(Order order, int min) {
	if (order != null && order.getDate().getTime() > System.currentTimeMillis() - min * 1000) {
		return order.getMember();
	}
}

위 코드는 if 조건문에 null 체크와 비즈니스 로직이 같이 적혀있어서 가독성이 떨어진다.  

 

반면 filter() 메서드를 사용하면 if 조건문 없이 메서드 연쇄 호출만으로 가독성있는 코드를 작성할 수 있다. 또, 메서드의 리턴타입을 Optional로 사용함으로써 호출자에게 해당 메서드가 null을 담고있는 Optional로 반환할 수도 있다는 것을 명시적으로 알려주고 있다.

public Optional<Member> getMemberIfOrderWithin(Order order, int min) {
    return Optional.ofNullable(order)
            .filter(o -> o.gerDate().getTime() > System.currentTimeMillis() - min * 1000)
            .map(Order::getMember);
}

filter() 메서드는 넘어온 함수형 인자의 리턴 값이 false인 경우, Optional을 비우기 때문에 그 이후 메서드 호출이 의미가 없어진다.

 

Java8 이전 개발 코드를 Optional로 바꾸기

Java8 이전에 개발된 코드는 Optional이 없어 null-safe하지 않을 수 있다. JAVA 표준 API조차 하위 호환성을 보장하기 위해 기존 API에 Optional을 적용할 수 없었다.

 

메서드의 반환값이 존재하지 않을 때 전통적인 처리 패턴

이전 개발된 메서드들은 반환값이 존재하지 않을 경우 크게 2가지 패턴으로 처리하였다.

 

1. null 반환

Map 인터페이스의 get() 메서드는 주어진 인덱스에 해당하는 값이 없으면 null을 반환한다.

Map<Integer, String> cities = new HashMap<>();
cities.put(1, "Seoul");
cities.put(2, "Busan");
cities.put(3, "Daejeon");

보통은 null 체크 코드가 들어간다.

String city = cities.get(4); // returns null
int length = city == null ? 0 : city.length(); // null check
System.out.println("length: " + length);

get() 메서드의 반환값을 Optional로 감싸주면 null-safe하게 처리할 수 있다.

Optional<String> maybeCity = Optional.ofNullable(cities.get(4));
int length = maybeCity.map(String::length).orElse(0);
System.out.println("length: " + length);

map() 메서드로 결과를 얻어내고 orElse() 메서드로 디폴트 값을 설정해주었다.

 

2. 예외 발생

두번째 패턴은 예외를 던져버리는 경우이다. List 인터페이스의 get() 메서드는 주어진 인덱스에 해당하는 값이 없으면 ArrayIndexOutOfBoundsException 을 던진다.

List<String> cities = Arrays.asList("Seoul", "Busan", "Daejeon");

다음과 같이 try-catch 문을 사용하여 예외 처리를 해주고 예외 처리 이후에 null 체크도 진행해야 한다.

String city = null;
try {
    city = cities.get(3); // throws exception
} catch (ArrayIndexOutOfBoundsException e) {
    // 생략
}

int length = city == null ? 0 : city.length(); // null check
System.out.println("length: " + length);

이런 경우, 예외 처리부를 감싸서 정적 유틸리티 메서드로 분리해야 한다.

Optional 클래스의 정적 팩터리 메서드를 사용해서 정상처리와 예외 처리시에 반환할 Optional 객체를 각각 지정해주자.

public static <T> Optional<T> getAsOptional(List<T> list, int index) {
    try {
        return Optional.of(list.get(index));
    } catch (ArrayIndexOutOfBoundsException e) {
        return Optional.empty();
    }
}

Optional<String> maybeCity = getAsOptional(cities, 3); // Optional
int length = maybeCity.map(String::length).orElse(0); // null check
System.out.println("length: " + length);

 

ifPresent() 메서드 사용하기

ifPresent(Consumer<? super T> consumer

이 메서드는 특정 결과를 반환하는 대신에 Optional 객체가 감싸고 있는 값이 존재하는 경우에만 실행될 로직을 함수형 인자로 넘길 수 있다. 함수형 인자로 람다식이나 메서드 레퍼런스가 넘어올 수 있다.

Optional<String> maybeCity = getAsOptional(cities, 3); 
maybeCity.ifPresent(city -> {
	System.out.println("length: " + city.length());
});

 

728x90
반응형
blog image

Written by ner.o

개발자 네로의 개발 일기, 자바를 좋아합니다 !