팩토리 패턴이란?
팩토리 패턴은 객체의 생성 책임을 클라이언트 코드에서 분리하여, 구체적인 객체 생성 과정을 외부에서 처리하는 패턴입니다.
일반적으로 정적 팩토리 메소드를 사용하여 객체를 반환합니다.
팩토리 패턴 적용 전 코드
아래 코드는 웹 IDE 프로젝트의 일부 코드입니다. Docker와 연결해 언어별로 다른 동작을 합니다.
public interface LanguageExecutor {
void executeCode();
}
public class JavaExecutor implements LanguageExecutor{
@Override
public void executeCode() {
System.out.println("자바 코드 실행");
}
}
public class JavaScriptExecutor implements LanguageExecutor{
@Override
public void executeCode() {
System.out.println("자바스크립트 코드 실행");
}
}
public class PythonExecutor implements LanguageExecutor{
@Override
public void executeCode() {
System.out.println("파이썬 코드 실행");
}
}
public enum LanguageType {
JAVA,
PYTHON,
JAVASCRIPT
}
public class LanguageExecutorClient {
public void execute(LanguageType languageType) {
LanguageExecutor executor = switch (languageType) {
case JAVA -> new JavaExecutor();
case PYTHON -> new PythonExecutor();
case JAVASCRIPT -> new JavaScriptExecutor();
};
// 언어별로 실행 로직 수행
executor.executeCode();
// TODO : 추가 로직 수행
}
}
LanguageExecutorClient는 언어별로 서로 다른 실행 로직을 수행하는 역할을 합니다. 그러나 위 코드를 보면, 로직 수행뿐만 아니라 객체를 직접 생성하는 책임까지도 맡고 있습니다. 이는 책임이 분산되어 코드의 유지보수성을 떨어뜨릴 수 있으며, 새로운 언어를 추가핼 때마다 클라이언트의 코드 수정이 필요해집니다.
팩토리 패턴 적용 후 코드
public class LanguageExecutorFactory {
private LanguageExecutorFactory() {
throw new IllegalStateException("Utility class");
}
public static LanguageExecutor of(LanguageType languageType) {
return switch (languageType) {
case JAVA -> new JavaExecutor();
case PYTHON -> new PythonExecutor();
case JAVASCRIPT -> new JavaScriptExecutor();
};
}
}
public class LanguageExecutorClient {
public void execute(LanguageType languageType) {
LanguageExecutor executor = LanguageExecutorFactory.of(languageType);
// 언어별로 실행 로직 수행
executor.executeCode();
// TODO : 추가 로직 수행
}
}
LanguageExecutorFactory 클래스를 생성해 객체 생성의 책임을 분리했습니다. 이제 새로운 언어가 추가되더라도 클라이언트 코드를 수정할 필요가 없습니다. LanguageExecutorFactory가 언어별로 적절한 객체를 생성해주므로, 클라이언트는 실행 로직에만 집중할 수 있습니다.
팩토리 패턴의 장단점
장점
- 객체 생성의 책임 분리 : 객체 생성 로직을 별도의 클래스(팩토리)로 분리함으러써, 클라이언트는 객체 생성 방식에 대해 알 필요가 없습니다. 객체 생성 관련 변경사항이 있어도 클라이언트 코드는 수정되지 않습니다.
- 유연한 확장성 : 새로운 객체 타입이 추가되더라도, 클라이언트 코드를 변경할 필요 없이 팩토리 클래스만 수정하면 됩니다. 이는 코드의 유지보수성을 높이고 확장을 용이하게 합니다.
- 결합도 감소 : 팩토리 패턴은 클라이언트와 구체적인 객체 클래스 간의 결합도를 줄입니다. 클라이언트는 인터페이스나 추상 클래스에 의존하게 되므로, 객체 생성 방식이 바뀌더라도 클라이언트에 영향을 미치지 않습니다.
단점
- 복잡성 증가 : 객체 생성 과정을 단순하게 처리할 수 있는 경우에도 팩토리 패턴을 사용하면 코드가 복잡해질 수 있습니다. 단순한 클래스 인스턴스화보다 더 많은 코드가 필요하게 되며, 작은 프로젝트에는 오히려 불필요한 복잡성을 유발할 수 있습니다.
- 추가 클래스 : 팩토리 클래스 자체가 추가되기 때문에, 코드의 양이 늘어나고 관리할 클래스가 많아집니다. 이는 특히 간단한 객체 생성이 필요한 경우에는 오히려 비효율적일 수 있습니다.
Spring에서의 팩토리 패턴
이번에는 Spring에서는 어떤 식으로 사용할 수 있는지 보겠습니다.
@Component
public class PythonExecutor implements LanguageExecutor{
@Override
public void executeCode() {
System.out.println("파이썬 코드 실행");
// TODO : DB 저장 로직 추가
}
}
위와 같이 LanguageExecutor를 구현하는 클래스들에 DB에 저장하는 추가 로직이 생겨 Spring의 Bean으로 등록되어야 합니다.
LanguageExecutor의 구현 클래스들이 전부 Bean으로 등록되었기 때문에, 의존성을 주입받기 위해 Factory 클래스 또한 Bean으로 등록되어야 합니다.
@Component
@RequiredArgsConstructor
public class LanguageExecutorFactory {
private final JavaExecutor javaExecutor;
private final JavaScriptExecutor javaScriptExecutor;
private final PythonExecutor pythonExecutor;
public LanguageExecutor of(LanguageType languageType) {
return switch (languageType) {
case JAVA -> javaExecutor;
case PYTHON -> pythonExecutor;
case JAVASCRIPT -> javaScriptExecutor;
};
}
}
클라이언트 코드 역시 Bean으로 등록 후, Factory 클래스의 의존성을 주입 받아 깔끔하게 처리할 수 있습니다.
@Service
@RequiredArgsConstructor
public class LanguageExecutorClient {
private final LanguageExecutorFactory languageExecutorFactory;
public void execute(LanguageType languageType) {
LanguageExecutor executor = languageExecutorFactory.of(languageType);
// 언어별로 실행 로직 수행
executor.executeCode();
// TODO : 추가 로직 수행
}
}
아래와 같이 List 혹은 Map으로 선언하면, 해당 인터페이스를 구현하고 있는 모든 클래스(Bean)들을 주입받을 수 있습니다.
@Component
@RequiredArgsConstructor
public class LanguageExecutorFactory {
private final List<LanguageExecutor> languageExecutors;
private final Map<String, LanguageExecutor> languageExecutorMap;
public void of() {
// [test.JavaExecutor@35f639fa, test.JavaScriptExecutor@5aaaa446, test.PythonExecutor@6c6333cd]
System.out.println(languageExecutors);
// {javaExecutor=test.JavaExecutor@35f639fa, javaScriptExecutor=test.JavaScriptExecutor@5aaaa446, pythonExecutor=test.PythonExecutor@6c6333cd}
System.out.println(languageExecutorMap);
}
}