| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | |
| 7 | 8 | 9 | 10 | 11 | 12 | 13 |
| 14 | 15 | 16 | 17 | 18 | 19 | 20 |
| 21 | 22 | 23 | 24 | 25 | 26 | 27 |
| 28 | 29 | 30 |
- 프록시 패턴
- 추상클래스
- 자바
- Spring
- 이펙티브 자바
- 계산기
- 디자인 패턴
- spring boot
- 스케줄러
- 배치
- 백엔드
- 토스
- 로드밸런서
- GoF 23
- 트러블슈팅
- 김영한
- DB
- Til
- Effective Java
- 프로그래머스
- 템플릿 메서드 패턴
- 스프링 배치
- 빌더 패턴
- lv1
- Spring Batch
- 성능 개선
- 코드카타
- java
- redis
- 스프링
- Today
- Total
김코딩
추상 팩토리 패턴(Abstract Factory Pattern) 본문
디자인 패턴 시리즈의 세 번째 주제는 추상 팩토리 패턴(Abstract Factory Pattern)이다.
팩토리 메서드 패턴이 "하나의 객체 생성을 유연하게"였다면, 추상 팩토리 패턴은 "연관된 여러 객체들을 일관성 있게 생성하자"는 해답이다.
예를 들어, 배달 앱에서 음식 주문 시 결제, 알림, 영수증 발행이 함께 일어난다.
토스페이로 결제했다면 토스 알림과 토스 영수증이 함께 발행되어야 한다.
카카오페이 결제에 토스 알림이 가면 안 된다. 추상 팩토리 패턴은 이런 문제를 해결한다.
이번 글에서는 추상 팩토리 패턴이 무엇인지, 팩토리 메서드와 어떻게 다른지, 어떻게 구현하는지 알아보자.
추상 팩토리 패턴의 정의
추상 팩토리 패턴(Abstract Factory Pattern)은 연관되거나 의존적인 객체들의 조합을 만드는 인터페이스를 제공하는 패턴이다.
구체적인 클래스를 지정하지 않고, 관련성 있는 여러 객체를 일관성 있게 생성할 수 있다.
핵심 특징:
- 연관된 객체들의 군(Family)을 생성한다
- 구체적인 클래스를 지정하지 않고 일관성 있는 제품군을 만든다
- 추상 팩토리 인터페이스를 통해 제품군을 생성한다
팩토리 메서드 vs 추상 팩토리:
- 팩토리 메서드: 하나의 제품 생성에 집중
- 추상 팩토리: 여러 연관된 제품들을 함께 생성
왜 추상 팩토리가 필요할까?
배달 앱에서 결제 시스템을 개발한다고 가정해보자.
// 문제가 있는 코드
public class OrderService {
public void processOrder(String paymentType) {
if (paymentType.equals("TOSS")) {
PaymentGateway gateway = new TossPaymentGateway();
NotificationSender notification = new TossNotificationSender();
ReceiptGenerator receipt = new TossReceiptGenerator();
} else if (paymentType.equals("KAKAO")) {
PaymentGateway gateway = new KakaoPaymentGateway();
NotificationSender notification = new KakaoNotificationSender();
ReceiptGenerator receipt = new KakaoReceiptGenerator();
}
}
}
문제점:
- 토스 결제에 카카오 알림이 섞일 수 있음 (일관성 없음)
- 실수로 new TossPaymentGateway()와 new KakaoNotificationSender()를 함께 쓸 수 있음
- 새로운 결제 수단 추가 시 모든 관련 클래스를 수동으로 추가해야 함
- 코드가 특정 클래스에 강하게 결합됨
추상 팩토리 패턴은 "관련된 객체들을 세트로 생성"하여 이런 문제를 해결한다.
순수 Java로 구현하기
1단계: 제품 인터페이스 정의
결제와 관련된 각 컴포넌트의 인터페이스를 만든다.
// 결제 게이트웨이
public interface PaymentGateway {
boolean processPayment(int amount);
}
// 알림 발송
public interface NotificationSender {
void sendPaymentNotification(String message);
}
// 영수증 생성
public interface ReceiptGenerator {
String generateReceipt(int amount);
}
2단계: 구체적인 제품 구현 (토스페이 제품군)
public class TossPaymentGateway implements PaymentGateway {
@Override
public boolean processPayment(int amount) {
System.out.println("토스페이로 " + amount + "원 결제 처리");
return true;
}
}
public class TossNotificationSender implements NotificationSender {
@Override
public void sendPaymentNotification(String message) {
System.out.println("[토스 알림] " + message);
}
}
public class TossReceiptGenerator implements ReceiptGenerator {
@Override
public String generateReceipt(int amount) {
return "=== 토스페이 영수증 ===\n금액: " + amount + "원";
}
}
3단계: 구체적인 제품 구현 (카카오페이 제품군)
public class KakaoPaymentGateway implements PaymentGateway {
@Override
public boolean processPayment(int amount) {
System.out.println("카카오페이로 " + amount + "원 결제 처리");
return true;
}
}
public class KakaoNotificationSender implements NotificationSender {
@Override
public void sendPaymentNotification(String message) {
System.out.println("[카카오톡 메시지] " + message);
}
}
public class KakaoReceiptGenerator implements ReceiptGenerator {
@Override
public String generateReceipt(int amount) {
return "=== 카카오페이 영수증 ===\n결제금액: " + amount + "원";
}
}
4단계: 추상 팩토리 인터페이스
public interface PaymentFactory {
PaymentGateway createPaymentGateway();
NotificationSender createNotificationSender();
ReceiptGenerator createReceiptGenerator();
}
5단계: 구체적인 팩토리 구현
// 토스페이 전용 팩토리
public class TossPaymentFactory implements PaymentFactory {
@Override
public PaymentGateway createPaymentGateway() {
return new TossPaymentGateway();
}
@Override
public NotificationSender createNotificationSender() {
return new TossNotificationSender();
}
@Override
public ReceiptGenerator createReceiptGenerator() {
return new TossReceiptGenerator();
}
}
// 카카오페이 전용 팩토리
public class KakaoPaymentFactory implements PaymentFactory {
@Override
public PaymentGateway createPaymentGateway() {
return new KakaoPaymentGateway();
}
@Override
public NotificationSender createNotificationSender() {
return new KakaoNotificationSender();
}
@Override
public ReceiptGenerator createReceiptGenerator() {
return new KakaoReceiptGenerator();
}
}
6단계: 클라이언트 코드
public class OrderService {
private PaymentGateway paymentGateway;
private NotificationSender notificationSender;
private ReceiptGenerator receiptGenerator;
// 팩토리를 주입받음
public OrderService(PaymentFactory factory) {
this.paymentGateway = factory.createPaymentGateway();
this.notificationSender = factory.createNotificationSender();
this.receiptGenerator = factory.createReceiptGenerator();
}
public void processOrder(int amount) {
// 결제 처리
boolean success = paymentGateway.processPayment(amount);
if (success) {
// 알림 발송
notificationSender.sendPaymentNotification("결제가 완료되었습니다!");
// 영수증 생성
String receipt = receiptGenerator.generateReceipt(amount);
System.out.println(receipt);
}
}
}
// 사용 예시
public class Main {
public static void main(String[] args) {
// 사용자가 토스페이를 선택
PaymentFactory factory = new TossPaymentFactory();
// 토스페이 제품군으로 주문 서비스 생성
OrderService orderService = new OrderService(factory);
orderService.processOrder(15000);
System.out.println("\n--- 다른 사용자 ---\n");
// 다른 사용자가 카카오페이를 선택
PaymentFactory factory2 = new KakaoPaymentFactory();
OrderService orderService2 = new OrderService(factory2);
orderService2.processOrder(25000);
}
}

결과:
- 토스페이를 선택하면 모든 컴포넌트가 토스페이 스타일
- 카카오페이를 선택하면 모든 컴포넌트가 카카오페이 스타일
- 절대로 토스 결제에 카카오 알림이 섞이지 않음!
실전 예제: 클라우드 스토리지 시스템
더 실무적인 예시를 보자. AWS와 Google Cloud를 지원하는 파일 저장 시스템이다.
// 스토리지 관련 제품들
public interface FileUploader {
void upload(String fileName, byte[] data);
}
public interface FileDownloader {
byte[] download(String fileName);
}
public interface AccessManager {
void grantAccess(String fileName, String userId);
}
// AWS 제품군
public class S3FileUploader implements FileUploader {
@Override
public void upload(String fileName, byte[] data) {
System.out.println("S3에 " + fileName + " 업로드");
}
}
public class S3FileDownloader implements FileDownloader {
@Override
public byte[] download(String fileName) {
System.out.println("S3에서 " + fileName + " 다운로드");
return new byte[0];
}
}
public class IAMAccessManager implements AccessManager {
@Override
public void grantAccess(String fileName, String userId) {
System.out.println("IAM으로 " + userId + "에게 " + fileName + " 접근 권한 부여");
}
}
// Google Cloud 제품군
public class GCSFileUploader implements FileUploader {
@Override
public void upload(String fileName, byte[] data) {
System.out.println("Google Cloud Storage에 " + fileName + " 업로드");
}
}
public class GCSFileDownloader implements FileDownloader {
@Override
public byte[] download(String fileName) {
System.out.println("Google Cloud Storage에서 " + fileName + " 다운로드");
return new byte[0];
}
}
public class GCPAccessManager implements AccessManager {
@Override
public void grantAccess(String fileName, String userId) {
System.out.println("GCP IAM으로 " + userId + "에게 " + fileName + " 접근 권한 부여");
}
}
// 추상 팩토리
public interface CloudStorageFactory {
FileUploader createFileUploader();
FileDownloader createFileDownloader();
AccessManager createAccessManager();
}
// 구체적인 팩토리들
public class AWSFactory implements CloudStorageFactory {
@Override
public FileUploader createFileUploader() {
return new S3FileUploader();
}
@Override
public FileDownloader createFileDownloader() {
return new S3FileDownloader();
}
@Override
public AccessManager createAccessManager() {
return new IAMAccessManager();
}
}
public class GCPFactory implements CloudStorageFactory {
@Override
public FileUploader createFileUploader() {
return new GCSFileUploader();
}
@Override
public FileDownloader createFileDownloader() {
return new GCSFileDownloader();
}
@Override
public AccessManager createAccessManager() {
return new GCPAccessManager();
}
}
// 클라이언트
public class FileStorageService {
private FileUploader uploader;
private FileDownloader downloader;
private AccessManager accessManager;
public FileStorageService(CloudStorageFactory factory) {
this.uploader = factory.createFileUploader();
this.downloader = factory.createFileDownloader();
this.accessManager = factory.createAccessManager();
}
public void saveAndShare(String fileName, byte[] data, String userId) {
uploader.upload(fileName, data);
accessManager.grantAccess(fileName, userId);
System.out.println("파일 저장 및 공유 완료\n");
}
}
// 사용
public class Main {
public static void main(String[] args) {
// 설정에 따라 클라우드 선택
String cloudProvider = "AWS"; // 환경 변수나 설정 파일에서 읽어올 수 있음
CloudStorageFactory factory;
if (cloudProvider.equals("AWS")) {
factory = new AWSFactory();
} else {
factory = new GCPFactory();
}
FileStorageService service = new FileStorageService(factory);
service.saveAndShare("report.pdf", new byte[100], "user123");
}
}
장점:
- AWS에서 GCP로 마이그레이션해도 클라이언트 코드는 그대로
- S3 업로더와 GCP 접근 관리자가 섞일 일이 없음
- 새로운 클라우드(Azure 등) 추가도 쉬움
추상 팩토리 패턴의 장점
- 일관성 보장: 같은 제품군의 객체들만 생성됨
- 결합도 감소: 클라이언트가 구체 클래스를 몰라도 됨
- 제품군 교체 용이: 팩토리만 바꾸면 전체 제품군이 바뀜
- 단일 책임 원칙: 제품 생성 코드를 한 곳에 모음
- 개방-폐쇄 원칙: 새로운 제품군 추가가 쉬움
추상 팩토리 패턴의 단점
- 복잡도 증가: 클래스와 인터페이스가 많아짐
- 새로운 제품 추가 어려움: 모든 팩토리를 수정해야 함
- 예: 결제, 알림, 영수증에 환불 처리기를 추가하려면?
- 모든 팩토리 인터페이스와 구현체를 수정해야 함
- 코드량 증가: 간단한 경우 오버엔지니어링
Spring에서의 추상 팩토리
Spring에서도 추상 팩토리 패턴을 사용할 수 있다.
// 추상 팩토리 인터페이스
public interface PaymentFactory {
PaymentGateway createGateway();
NotificationSender createNotification();
ReceiptGenerator createReceipt();
}
// 구체적인 팩토리
@Component
@Profile("toss")
public class TossPaymentFactory implements PaymentFactory {
@Override
public PaymentGateway createGateway() {
return new TossPaymentGateway();
}
@Override
public NotificationSender createNotification() {
return new TossNotificationSender();
}
@Override
public ReceiptGenerator createReceipt() {
return new TossReceiptGenerator();
}
}
@Component
@Profile("kakao")
public class KakaoPaymentFactory implements PaymentFactory {
@Override
public PaymentGateway createGateway() {
return new KakaoPaymentGateway();
}
@Override
public NotificationSender createNotification() {
return new KakaoNotificationSender();
}
@Override
public ReceiptGenerator createReceipt() {
return new KakaoReceiptGenerator();
}
}
// 사용
@Service
public class OrderService {
private final PaymentFactory factory;
public OrderService(PaymentFactory factory) {
this.factory = factory;
}
public void processOrder(int amount) {
PaymentGateway gateway = factory.createGateway();
NotificationSender notification = factory.createNotification();
ReceiptGenerator receipt = factory.createReceipt();
// 일관된 제품군으로 결제 처리
}
}
@Profile 어노테이션으로 환경에 따라 다른 팩토리를 주입받을 수 있다.
실무에서 언제 사용할까?
사용하면 좋은 경우:
- 여러 관련된 객체들을 일관성 있게 생성해야 할 때
- 다양한 제품군이 존재하고, 런타임에 선택해야 할 때
- 결제 시스템, 클라우드 서비스 등 교체 가능한 외부 시스템 연동 시
- 멀티 테넌트 시스템에서 고객별로 다른 구현체를 사용할 때
사용하지 않아도 되는 경우:
- 관련된 객체가 1-2개뿐일 때 (팩토리 메서드면 충분)
- 제품군이 하나뿐이고 변경 가능성이 없을 때
- 간단한 객체 생성만 필요할 때
핵심 원칙:
"관련된 객체들을 세트로 생성하고, 그 조합이 여러 가지라면 추상 팩토리를 고려하라"
팩토리 메서드 vs 추상 팩토리 비교
| 구분 | 팩토리 메서드 | 추상 팩토리 |
| 목적 | 하나의 제품 생성 | 연관된 제품군 생성 |
| 구조 | 하나의 메서드 | 여러 개의 메서드 |
| 초점 | 상속 (서브클래스 결정) | 객체 조합 (관련 객체 생성) |
| 복잡도 | 낮음 | 높음 |
| 확장성 | 제품 추가 쉬움 | 제품군 추가 쉬움, 제품 추가 어려움 |
간단한 선택 기준:
- 하나의 객체만 생성? → 팩토리 메서드
- 여러 관련 객체를 함께 생성? → 추상 팩토리
결론
추상 팩토리 패턴은 연관된 객체들을 일관성 있게 생성하는 강력한 패턴이다.
기억해야 할 포인트:
- 관련 있는 객체들을 제품군으로 묶어서 생성한다
- 제품군 간의 일관성을 보장한다
- 새로운 제품군 추가는 쉽지만, 새로운 제품 추가는 어렵다
- 팩토리 메서드보다 복잡하므로 꼭 필요할 때만 사용한다
- Spring의 Profile 기능과 결합하면 강력하다
다음 글에서는 복잡한 객체를 단계별로 생성하는 빌더 패턴을 알아볼 것이다.
다음 글 예고: 빌더 패턴 - 복잡한 객체를 단계별로 만들어라
'디자인 패턴' 카테고리의 다른 글
| 프로토타입 패턴(Prototype Pattern) (0) | 2026.01.13 |
|---|---|
| 빌더 패턴(Builder Pattern) (0) | 2026.01.13 |
| 팩토리 메서드 패턴(Factory Method Pattern) (0) | 2026.01.13 |
| 싱글톤 패턴(Singleton Pattern) (0) | 2026.01.13 |
| 왜? 디자인 패턴을 공부해야할까? (0) | 2026.01.13 |