문제 상황

회원가입 로직을 Optional을 활용하여 리팩토링하다 생긴 문제입니다.

다음과 같이 provider와 providerId를 통해 유저를 조회하여 해당하는 유저가 존재하지 않는다면 새로운 유저를 생성하도록 구현하였습니다.

return userRepository
    .findByProviderAndProviderId(provider, providerId)
    .orElse(createUser(oAuth2User, provider));

이때 중복된 provider, providerId로 인해 SQL integrity violation exception이 발생하였습니다.

디버깅을 해보니 findByProviderAndProviderId 메서드를 동해 유저를 조회하였을때,

유저가 존재하든 존재하지 않든 createUser메서드를 호출하여 생긴 문제였습니다.

 

이는 다음과 같이 orElse를 orElseGet으로 변경하여 해결하였습니다.

return userRepository
        .findByProviderAndProviderId(provider, providerId)
        .orElseGet(() -> createUser(oAuth2User, provider));

Optional 구현체

  public T orElse(T other) {
    return value != null ? value : other;
  }

  public T orElseGet(Supplier<? extends T> supplier) {
    return value != null ? value : supplier.get();
  }

orElse 메서드의 경우 값을 인자로 받는 반면 (Call by Value)

orElseGet의 경우 Supplier로 래핑된 값을 인자로 받는것을 확인할 수 있었습니다. (Call By Name)

 

이렇게 Supplier로 래핑되어있기때문에 createUser 메서드를 즉시 호출하지않고, 값이 존재하지 않을경우에만 값을 호출하게 됩니다.

이렇게 orElseGet을 잘 활용한다면, 불필요한 호출을 줄일 수 있습니다.

함수형 인터페이스 Supplier

Supplier 는 java8에 추가된 함수형 인터페이스로, "call by name" 을 통해 지연 연산(Lazy Evaluation)을 구현할 수 있습니다.

 

import java.util.function.Supplier;

public class Main {
    public static void main(String[] args) {
        // call by value
        String result = expensiveOperation(); 
        System.out.println(result); // expensive operation 수행
 
        // call by name (with Supplier)
        Supplier<String> resultSupplier = () -> expensiveOperation(); // expensive operation 수행 X
        System.out.println(resultSupplier.get()); // expensive operation 수행
    }

    public static String expensiveOperation() {
        // 오래 걸리는 연산
        return "Expensive Result";
    }
}

call by value 방식에서는, 함수 호출을 위해 expensiveOperation 메소드의 실행이 먼저 수행됩니다.
반면에 call by name 방식에서는, Supplier<String>를 사용하여 함수 호출이 실제로 필요한 시점(get() 메소드 호출 시점)까지 연산을 지연시킵니다.

+ Recent posts