1. 들어가며:
객체지향 프로그래밍에서 객체를 생성하는 것은 가장 기본적인 행위이지만, 동시에 가장 조심스러운 행위이기도 하다. 클래스 내부에서 new 키워드를 사용하여 직접 객체를 생성하는 순간, 해당 클래스는 생성될 객체의 구체적인 타입과 강하게 결합(Coupling)되기 때문이다. 팩토리 패턴은 이러한 '객체 생성의 책임'을 별도의 클래스나 메서드로 분리하여 시스템의 유연성을 확보하는 패턴인 것이다.
1.1. 문제 상황: 복잡한 생성 로직의 파편화
만약 컨트롤러나 서비스 로직 곳곳에서 조건문을 통해 직접 객체를 생성한다면 어떻게 될까?
- 구체적인 클래스 이름이 코드 전반에 노출되어 수정 시 변경 범위가 넓어진다.
- 객체 생성 로직이 복잡해질 경우(예: 의존성 설정, 초기화 작업 등), 중복 코드가 발생한다.
- 생성 로직이 비즈니스 로직과 뒤섞여 가독성을 해친다.
팩토리 패턴은 이러한 혼란을 잠재우고 "객체 생성은 공장에서 담당하고, 클라이언트는 완성된 제품만 받아서 사용한다"는 철학을 실천한다.
2. 팩토리 패턴의 구조
팩토리 패턴은 크게 '팩토리 메서드 패턴'과 '추상 팩토리 패턴'으로 나뉘지만, 실무에서는 이를 포괄하여 객체 생성을 캡슐화하는 모든 기법을 팩토리라 부르기도 한다.
2.1. 구성 요소
- 제품 인터페이스 (Product): 생성될 객체들의 공통 인터페이스이다.
- 구체적인 제품 (Concrete Product): 인터페이스를 실제로 구현한 클래스들이다.
- 팩토리 (Factory): 객체 생성 로직을 가지고 있으며, 클라이언트에게 적절한 제품 객체를 반환한다.
3. 스프링 부트로 구현하는 팩토리 패턴
실무에서 가장 많이 쓰이는 '소셜 로그인 프로바이더 선택' 예시를 통해 팩토리 패턴의 위력을 확인해본다.
3.1. 제품 인터페이스와 구현체
public interface OAuthProvider {
String getProviderName();
void authenticate();
}
@Component
public class GoogleProvider implements OAuthProvider {
@Override public String getProviderName() { return "google"; }
@Override public void authenticate() { System.out.println("구글 로그인 시도"); }
}
@Component
public class KakaoProvider implements OAuthProvider {
@Override public String getProviderName() { return "kakao"; }
@Override public void authenticate() { System.out.println("카카오 로그인 시도"); }
}
3.2. 팩토리 클래스 구현
스프링의 빈 관리 기능을 활용하면 팩토리를 매우 강력하게 구축할 수 있다.
@Component
@RequiredArgsConstructor
public class OAuthProviderFactory {
// 모든 OAuthProvider 구현체를 주입받음
private final List<OAuthProvider> providers;
public OAuthProvider getProvider(String name) {
return providers.stream()
.filter(p -> p.getProviderName().equalsIgnoreCase(name))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("지원하지 않는 로그인 방식입니다."));
}
}
3.3. 컨텍스트(Service)에서의 사용
서비스 로직은 이제 구체적인 클래스(Google, Kakao)를 알 필요가 없다. 팩토리에게 이름만 던져주면 된다.
@Service
@RequiredArgsConstructor
public class LoginService {
private final OAuthProviderFactory providerFactory;
public void login(String providerName) {
OAuthProvider provider = providerFactory.getProvider(providerName);
provider.authenticate();
}
}
4. 스프링 컨테이너와 팩토리 패턴
사실 스프링 부트를 사용하고 있는 시점에서 우리는 이미 거대한 팩토리 패턴의 수혜를 입고 있는 것이다.
4.1. BeanFactory와 ApplicationContext
스프링의 핵심인 BeanFactory는 이름 그대로 빈(Bean)을 생성하고 관리하는 '빈 공장'이다. 우리가 @Component, @Service 등을 선언하면 스프링은 내부적으로 이 팩토리 패턴을 사용하여 객체를 생성하고, 필요한 곳에 주입해준다.
4.2. 정적 팩토리 메서드 (Static Factory Method)
스프링과 무관하게 자바 객체 설계 시에도 new 대신 of(), from()과 같은 정적 메서드를 통해 객체를 생성하는 기법을 권장한다. 이는 생성자 이름을 가질 수 있게 하여 가독성을 높이고, 객체 생성의 세부 사항을 캡슐화하는 팩토리 패턴의 변형인 것이다.
5. 실무적 이점과 주의사항
5.1. 장점
- 결합도 낮춤: 클라이언트는 인터페이스만 알면 되므로 구체 클래스의 변경에 자유롭다.
- 생성 로직의 집중화: 객체 생성 방식이 변경되어도 팩토리 클래스 한 곳만 수정하면 된다.
- 객체 재사용: 팩토리 내부에서 싱글톤 객체를 반환하거나 캐싱을 적용하여 성능을 최적화할 수 있다.
5.2. 주의사항
- 클래스 수 증가: 단순한 객체 생성에도 팩토리를 도입하면 관리 포인트가 늘어난다.
- 오버 엔지니어링: new로 생성해도 충분히 단순하고 변경 가능성이 희박한 경우라면 굳이 팩토리를 만들 필요는 없는 것이다.
6. 요약
팩토리 패턴은 "객체 생성이라는 지저분한 작업(Dirty Work)을 전담 팀(Factory)에 맡기는 것"이다. 이를 통해 비즈니스 로직은 순수하게 자신의 역할에만 집중할 수 있게 된다. 스프링 부트 환경에서는 빈 주입과 결합하여 더욱 강력한 기능을 발휘하며, 이는 시스템의 확장성을 결정짓는 핵심 요소가 된다.
'Java > Design Pattern' 카테고리의 다른 글
| [8] 디자인패턴: 퍼사드 패턴 (Facade) (0) | 2025.12.29 |
|---|---|
| [7] 디자인패턴: 어댑터 패턴 (Adapter) (0) | 2025.12.29 |
| [5] 디자인패턴: 데코레이터 패턴 (Decorator) (0) | 2025.12.29 |
| [4] 디자인패턴: 프록시 패턴 (Proxy) (0) | 2025.12.29 |
| [3] 디자인패턴: 템플릿 패턴 (Template Method/Callback) (0) | 2025.12.29 |
