팩토리 메서드 패턴 (Factory Method Pattern)
 - 객체 생성을 위한 인터페이스를 정의하지만, 어떤 클래스의 인스턴스를 생성할지에 대한 결정은 서브클래스가 내리도록 한다.

 

개발을 하다보면 추상화를 위해 하나의 인터페이스 또는 추상 클래스가 여러 구현체를 갖는 경우가 자주 있다.

이때 특정 타입의 구현체를 찾아주어야 하는 팩토리 메서드 패턴(이하 팩토리 클래스라 한다.)를 만드는 것이 불가피한데, 이 팩토리 클래스를 유연하게 만드는 방법에 대해 설명한다.

 

 

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

}

이러한 구조는 구현체가 점점 늘어나면 유지보수하기가 어려워진다. 그로므로 보다 유연하게 대응할 수 있는 팩토리 클래스가 필요하다.

 


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


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

 

가장 먼저 LoginService에서 해당 로그인 타입일 경우 처리 여부를 결정하는 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. 팩토리 클래스 코드를 깔끔하게 유지할 수 있음

  • 기존의 팩토리 클래스는 구현체가 많으면 수 많은 if-else와 함께 코드가 복잡해진다. 하지만 위와 같은 방식은 코드를 깔끔하게 유지하여 유지보수에 용이하다.

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

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

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

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

+ Recent posts