티스토리 뷰

Spring

[Spring] if-else를 사용하지 않는 유연한 팩토리 클래스 구현하기

망나니개발자 2022. 6. 2. 10:00
반응형

개발을 하다 보면 추상화를 위해 하나의 인터페이스 또는 추상 클래스가 여러 구현체를 갖는 경우가 자주 있습니다. 이때 특정 타입의 구현체를 찾아주어야 하는 팩토리 클래스를 만드는 것이 불가피한데, 이 팩토리 클래스를 유연하게 만드는 방법에 대해 알아보도록 하겠습니다.

 

 

 

 

1. if-else로 팩토리 클래스 구현하기


[ if-else로 팩토리 클래스 구현하기 ]

우리가 다양한 로그인 방법을 지원하기 위해 이를 LoginService라는 하나의 인터페이스를 만들고, 웹 로그인, 모바일 로그인, SNS 로그인과 같이 3가지 구현체를 두었다고 하자.

public interface LoginService {

    void login();

}

 

 

 

LoginController에서는 로그인 타입을 파라미터로 받아서, 타입에 맞는 올바른 LoginService 구현체를 호출해주어야 한다고 하자.

 

 

 

그러면 우리는 일반적으로 다음과 같이 팩토리 클래스를 만들고 if-else 로직을 구현한다.

@Component
@RequiredArgsConstructor
public class DeprecatedLoginFactory {

    private final MobileLogin mobileLogin;
    private final WebLogin webLogin;
    private final SnsLogin snsLogin;

    public LoginService find(final LoginType loginType) {
        if (loginType == LoginType.MOBILE) {
            return mobileLogin;
        } else if (loginType == LoginType.WEB) {
            return webLogin;
        } else if (loginType == LoginType.SNS) {
            return snsLogin;
        }

        throw new NoSuchElementException("Cannot find loginType: " + loginType);
    }

}

 

 

그리고 컨트롤러에서는 이러한 팩토리 클래에 의존해서 구현체를 찾아줄 것이다. 하지만 이러한 구조는 문제가 많다. 왜냐하면 LoginService의 새로운 구현체가 생겼을 때 해당 팩토리 클래스도 수정이 필요하기 때문이다. 예를 들어 게스트 로그인이 추가되었다면 위의 팩토리 클래스는 다음과 같이 수정이 필요할 것이다. 이러한 구조는 구현체가 점점 늘어나면 유지보수하기가 어려워진다. 그러므로 보다 유연하게 대응할 수 있는 팩토리 클래스가 필요하다.

@Component
@RequiredArgsConstructor
public class DeprecatedLoginFactory {

    private final MobileLogin mobileLogin;
    private final WebLogin webLogin;
    private final SnsLogin snsLogin;
    private final GuestLogin guestLogin;

    public LoginService find(final LoginType loginType) {
        if (loginType == LoginType.MOBILE) {
            return mobileLogin;
        } else if (loginType == LoginType.WEB) {
            return webLogin;
        } else if (loginType == LoginType.SNS) {
            return snsLogin;
        } else if (loginType == LoginType.GUEST) {
            return guestLogin;
        }

        throw new NoSuchElementException("Cannot find loginType: " + loginType);
    }

}

 

 

 

 

 

 

 

2. if-else를 사용하지 않는 유연한 팩토리 클래스 구현하기


[ if-else를 사용하지 않는 유연한 팩토리 클래스 구현하기 ]

가장 먼저 LoginSerivce에서 해당 로그인 타입일 경우 처리 여부를 결정하는 supports 메소드를 LoginService에 추가해주도록 하자. 해당 메소드는 LoginType을 파라미터로 받고, 처리 여부를 boolean으로 반환한다.

public interface LoginService {

    boolean supports(LoginType loginType);

    void login();

}

 

 

대표적으로 WebLogin일 경우에는 다음과 같이 supports 메소드를 구현할 수 있다. 여기서 해당 구현체가 package-private으로 선언되어 있는데, 이에 대해서는 아래에서 다시 살펴보도록 하자.

@Service
class WebLogin implements LoginService {

    @Override
    public boolean supports(final LoginType loginType) {
        return loginType == LoginType.WEB;
    }

    @Override
    public void login() {
        System.out.println("Web Login");
    }

}

 

 

그 다음 팩토리 클래스를 수정해주어야 한다. 스프링은 여러 구현체가 있을 때 이를 리스트로 받을 수 있다. 그러므로 다음과 같이 모든 LoginService 중에서 LoginType을 supports 하는 구현체를 찾도록 구현할 수 있다. 이러한 구조의 팩토리 클래스는 이전과 비교해 많은 장점을 지니는데, 자세히 살펴보도록 하자.

@Component
@RequiredArgsConstructor
public class LoginFactory {

    private final List<LoginService> loginServiceList;

    public LoginService find(final LoginType loginType) {
        return loginServiceList.stream()
                .filter(v -> v.supports(loginType))
                .findFirst()
                .orElseThrow();
    }

}

 

 

 

 

 

[ 유연한 팩토리 클래스의 장점 ]

이렇게 팩토리 클래스를 구현하면 여러 가지 장점을 얻을 수 있다.

  1. 팩토리 클래스 코드를 깔끔하게 유지할 수 있음
  2. 새로운 LoginService 구현체가 생겨도 팩토리 클래스를 수정할 필요가 없음
  3. LoginService 구현체를 package-private으로 선언할 수 있음

 

 

1. 팩토리 클래스 코드를 깔끔하게 유지할 수 있음

기존의 팩토리 클래스는 구현체가 많으면 수 많은 if-else와 함께 코드가 상당히 복잡해진다. 하지만 위와 같은 방식은 코드를 깔끔하게 유지하여 유지보수에 용이하다. 만약 잘못된 빈이 찾아지는 경우라면 팩토리 클래스의 if-else가 아니라 해당 빈의 supports만 보면 된다.

 

 

 

2. 새로운 LoginService 구현체가 생겨도 팩토리 클래스를 수정할 필요가 없음

또한 새로운 구현체가 생기면 새로운 빈을 주입 받고 if-else 문을 수정해주어야 한다. 하지만 위와 같은 방식은 새로운 구현체가 생겨도 별도의 수정이 필요하지 않다. 새로운 구현체를 구현만 해주면 팩토리 클래스를 바로 사용할 수 있다.

 

 

 

3. LoginService 구현체를 package-private으로 선언할 수 있음

마지막으로 LoginService의 구현체에 직접 의존하는 경우를 방지하도록 package-private으로 선언해줄 수 있다. 기존의 팩토리 클래스라면 팩토리 클래스에서 구현체를 의존해야 하므로 다른 패키지일 경우 반드시 public으로 클래스를 선언해주어야 했다. 하지만 위와 같은 방식이라면 구현체에 직접 의존하는 경우를 방지하도록 다음과 같이 package-private 선언이 가능하다. 또한 package-private으로 선언하면 IDE의 불필요한 추천도 필터링되는 장점도 있다.

 

 

 

 

[ 캐싱을 적용해 고도화하기 ]

위와 같은 팩토리 클래스는 항상 로그인 타입을 기준으로 반복하게 된다. 동일한 요청이 많을 때 이러한 과정을 계속 반복하면 비효율적이므로 캐싱을 적용해볼 수 있다. 이는 스프링 프레임워크에서 내부적으로 자주 사용하는 방법인데, 만약 메모리에 있다면 그대로 꺼내서 반환하고 없다면 전체 목록에서 구현체를 찾아 캐시에 넣고 반환하는 것이다.

@Component
@RequiredArgsConstructor
public class LoginFactory {

    private final List<LoginService> loginServiceList;
    private final Map<LoginType, LoginService> factoryCache;

    public LoginService find(LoginType loginType) {
        LoginService loginService = factoryCache.get(loginType);
        if (loginService != null) {
            return loginService;
        }

        loginService = loginServiceList.stream()
                .filter(v -> v.supports(loginType))
                .findFirst()
                .orElseThrow();

        factoryCache.put(loginType, loginService);

        return loginService;
    }

}

 

 

 

 

 

자세한 코드는 깃허브 링크에서 참고할 수 있습니다. 혹시 더 좋은 방법이 있다면 댓글로 피드백 부탁드리겠습니다!

감사합니다:)

 

 

반응형
댓글
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG more
«   2024/12   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
글 보관함