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