Optional과 함수형 인터페이스

도비·2023년 11월 28일
0

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);
    });

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

함수형 인터페이스와 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개의 댓글