[Spring] if-else를 사용하지 않는 유연한 팩토리 클래스 구현하기
개발을 하다 보면 추상화를 위해 하나의 인터페이스 또는 추상 클래스가 여러 구현체를 갖는 경우가 자주 있습니다. 이때 특정 타입의 구현체를 찾아주어야 하는 팩토리 클래스를 만드는 것이 불가피한데, 이 팩토리 클래스를 유연하게 만드는 방법에 대해 알아보도록 하겠습니다.
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();
}
}
[ 유연한 팩토리 클래스의 장점 ]
이렇게 팩토리 클래스를 구현하면 여러 가지 장점을 얻을 수 있다.
- 팩토리 클래스 코드를 깔끔하게 유지할 수 있음
- 새로운 LoginService 구현체가 생겨도 팩토리 클래스를 수정할 필요가 없음
- 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;
}
}
자세한 코드는 깃허브 링크에서 참고할 수 있습니다. 혹시 더 좋은 방법이 있다면 댓글로 피드백 부탁드리겠습니다!
감사합니다:)