Optional과 함수형 인터페이스

도비·2023년 11월 28일

Spring Boot

목록 보기
6/13
post-thumbnail

좋아요 로직에 대해 생각하다가 생성과 삭제 요청을 분리하지 않고 서버 안에서 처리하게 구현하고자 했다.

그래서 좋아요 관련 로직을 어떻게 처리해야할까 고민하던 중..
원래 처리하던 방식은 두 가지 후보가 있었는데,

첫 번째 방식은 아래처럼 if 문으로 분기처리하여, 존재하지 않는다면 생성하고, 아닐 경우 .get() 을 통해서 값을 가져오는 방식이다.

Optional<Heart> existingHeart = heartRepository.findByMemberIdAndProductId(memberId, productId);
if(existingHeart.isEmpty()) {
      Heart newHeart = createHeart(memberId, productId);
      return HeartPutResponse.of(newHeart);
        }
deleteHeart(existingHeart.get());
return HeartPutResponse.of(null);

첫 번째 방법에서 권장되지 않는 방식인 get을 사용하는 것이 마음에 들지 않았다.

그렇게 생각해낸 두 번째는 return 문에서 map 함수와 orElseGet을 통해서 처리하는 방식이다.

Optional<Heart> existingHeart = heartRepository.findByMemberIdAndProductId(memberId, productId);
return existingHeart
	.map(heart -> {
     deleteHeart(heart);
     return HeartPutResponse.of(null);
     })
     .orElseGet(() -> {
            Heart newHeart = createHeart(memberId, productId);
            return HeartPutResponse.of(newHeart);
    });

이 코드는 map과 orElseGet을 사용하여 분기문을 처리하고 있지는 않지만, 메서드 체이닝이 길어 가독성이 떨어진다는 단점이 있다.

함수형 인터페이스와 Optional 종단 처리 메서드에 대해 정리해보고 위의 코드를 개선해보고자 한다.

Optional

우선 Optional은 JDK 8 부터 고안된 null을 대신하기 위해 만들어진 데이터 타입 클래스이다. null이나 null이 아닌 값을 담을 수 있다.

보통 우리는 JPA에서 개발할 때 findBy 함수의 리턴 값으로 Optional 객체를 반환받는다. 그렇게 하므로써 NPE가 발생할 가능성을 줄일 수 있기 때문이다.

그러면 Optional이 null 일 경우, 조건부 처리를 하는 방법이 무엇일까?

  • ifPresent
    Optional 객체가 non-null 일 경우에 인자로 넘긴 함수를 실행하는 메서드이다. Optional 객체가 null이면 인자로 넘긴 함수는 실행되지 않는다. 내부 코드를 살펴보면
        public void ifPresent(Consumer<? super T> action) {
        if (value != null) {
            action.accept(value);
        }
    Consumer 인터페이스 매개변수로 전달받아 값이 존재하는 경우 인수로 넘겨준 Consumer를 실행한다. 값이 없는 경우 인수로 넘겨준 Consumer를 실행한다.
  • ifPresentOrElse
    자바 9에서 새롭게 추가된 메서드이다. ifPresent 메서드와 다르게 매개변수를 하나 더 받는다.
public void ifPresentOrElse(Consumer<? super T> action, Runnable emptyAction) {
    if (value != null) {
        action.accept(value);
    } else {
        emptyAction.run();
    }
}

첫 번째 매개변수는 Optional 객체에 값이 있을 때 실행되는 Consumer를 받고 Optional 객체가 비었을 때 실행되는 Runnable을 받는다.

  • OrElse
    orElse메서드를 사용하면 비어있는 Optional 객체에 기본값을 설정할 수 있다.
public T orElse(T other) {
    return value != null ? value : other;
}

Optional 객체에 값이 래핑되어 있다면 그 값을 반환하고, 그렇지 않은 경우에는 설정된 기본 값을 반환한다.

  • OrElseGet
    orElse와 유사하지만, orElseGet 은 인자로 Supplier를 받는다.
public T orElseGet(Supplier<? extends T> supplier) {
    return value != null ? value : supplier.get();
}

사용 방법

Heart heart = heartRepository.findByProductAndUser(product, user).OrElseGet(() -> Heart.create(product, user));

orElse 메서드와 orElseGet 메서드의 차이

orElseGet 메서드의 경우 Optional에 값이 없을 때만 실행되지만 orElse 메서드는 값의 존재 여부와 관계 없이 항상 실행된다.

  • OrElseThrow
    Optional 객체에 래핑된 값이 없을 경우 예외를 발생시킨다.
public <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier) throws X {
    if (value != null) {
        return value;
    } else {
        throw exceptionSupplier.get();
    }
}

Optional 메서드 인자로 주어지는 함수형 인터페이스에 대해 톺아보자.
Java에서 제공하는 함수형 인터페이스는 Supplier<T>,Consumer<T> Function<T,R> Predicate<T> 이 있다.

Optional에서 인자로 전달되는 두 인터페이스인 Supplier<T>,Consumer<T>만 알아보자.

  • Consumer<T>
    객체 T를 매개변수로 받아서 사용하며, 반환값은 없는 함수형 인터페이스이다.
    void accept(T t)를 추상메소드로 갖는다.
@FunctionalInterface
public interface Consumer<T> {

    void accept(T t);

    default Consumer<T> andThen(Consumer<? super T> after) {
        Objects.requireNonNull(after);
        return (T t) -> { accept(t); after.accept(t); };
    }
}

이 인터페이스가 사용되는 ifPresent 메서드를 통해 사용 예시를 살펴보자면,

existingHeart.ifPresent(this::deleteHeart);

deleteHeart 함수는 리턴 값이 없기 때문에 위와 같이 사용할 수 있다.

실제로 이 매개변수에 리턴 값을 넣어주면 에러가 발생한다.

  • Supplier<T>
    Supplier는 매개변수 없이 반환값 만을 갖는 함수형 인터페이스이다.
@FunctionallInterface
public interface Supplier<T>{
	T get();
}

이 인터페이스를 사용하는 orElseGet 을 통해 사용 예시를 살펴보자면,

existingHeart.map(heart -> {
            deleteHeart(heart);
            return HeartPutResponse.of(null);
        }).orElseGet(() -> {
            Heart newHeart = createHeart(memberId, productId);
            return HeartPutResponse.of(newHeart);
        })

이렇게 리턴 값을 지정해줄 수 있다.

좋아요 기능 개선

get을 사용하지 않고 존재할 경우, 삭제하고 비어있을 경우 생성하는 로직을 작성했다.

Optional<Heart> existingHeart = heartRepository.findByMemberIdAndProductId(memberId, productId);
if (existingHeart.isEmpty()) {
	Heart newHeart = createHeart(memberId, productId);
	return HeartPutResponse.0f(newHeart);
} else {
	existingHeart.ifPresent(this::deleteHeart);
	return HeartPutResponse.of(null);
}

Optional 객체값을 두 번 참조하는 단점이 있었기 때문에 코드를 조금 더 개선해 볼 방법에 대해 고민해보았다.
if (existingHeart.isEmpty()) 와 existingHeart.ifPresent(this::deleteHeart); 에서
두번 분기 처리되는 방식을 개선해볼 방향을 고민하던 중 기존에 작성했던 .map을 사용한 방식을 채택하기로 했다 😅

개선하진 않았지만 이번 기회에 Optional과 함수형 인터페이스에 대해 살펴보았다!

profile
하루에 한 걸음씩

0개의 댓글