(Java/Kotlin) 팩토리 패턴(Factory Pattern) 완전 정리
개요
- 생성 패턴(Creational Pattern) 중 팩토리 패턴(Factory Pattern) 을 다룹니다.
- 팩토리 패턴은 객체 생성 로직을 호출부에서 분리 해, 클라이언트가 구체 클래스를 몰라도 객체를 얻을 수 있게 하는 패턴입니다.
- 팩토리 패턴에는 두 가지 대표 형태가 있습니다.
- 팩토리 메서드 패턴(Factory Method Pattern) — 서브클래스가 생성할 타입을 결정
- 추상 팩토리 패턴(Abstract Factory Pattern) — 연관된 객체군을 함께 생성
- 이 글에서는 다음을 설명합니다.
- 팩토리 패턴이 필요한 이유
- 심플 팩토리 → 팩토리 메서드 → 추상 팩토리 순서로 단계별 설명
- Kotlin companion object, sealed class 활용
- Android 실전 예제 (ViewModelFactory, Fragment, Retrofit)
1. 왜 팩토리 패턴이 필요한가
❌ 문제 — 생성 코드가 호출부에 분산
// 알림 타입마다 호출부에서 직접 생성
fun sendNotification(type: String, message: String) {
when (type) {
"PUSH" -> PushNotification(message).send()
"EMAIL" -> EmailNotification(message).send()
"SMS" -> SmsNotification(message).send()
// 새 타입 추가 시 이 when 블록을 수정해야 함 → OCP 위반
}
}
문제점:
- 새 알림 타입이 생길 때마다 호출부를 수정해야 합니다.
- 생성 로직이 여러 곳에 퍼지면 일관성을 유지하기 어렵습니다.
- 생성 과정이 복잡할 때 (의존성 주입, 초기화 순서 등) 코드가 비대해집니다.
✅ 팩토리로 생성 책임 분리
val notification = NotificationFactory.create(type, message)
notification.send()
// 호출부는 구체 클래스를 전혀 모름
2. 심플 팩토리 (Simple Factory)
디자인 패턴이라기보다 가장 기본적인 팩토리 함수 형태입니다.
interface Notification {
fun send()
}
class PushNotification(private val message: String) : Notification {
override fun send() = println("[FCM PUSH] $message")
}
class EmailNotification(private val message: String) : Notification {
override fun send() = println("[EMAIL] $message")
}
class SmsNotification(private val message: String) : Notification {
override fun send() = println("[SMS] $message")
}
// 심플 팩토리 — 정적 메서드로 생성 책임 집중
object NotificationFactory {
fun create(type: String, message: String): Notification = when (type) {
"PUSH" -> PushNotification(message)
"EMAIL" -> EmailNotification(message)
"SMS" -> SmsNotification(message)
else -> throw IllegalArgumentException("알 수 없는 알림 타입: $type")
}
}
// 사용
val notification = NotificationFactory.create("PUSH", "주문이 완료되었습니다")
notification.send() // [FCM PUSH] 주문이 완료되었습니다
한계: 새 타입 추가 시 NotificationFactory 내부를 수정해야 합니다 → OCP 위반.
3. 팩토리 메서드 패턴 (Factory Method Pattern)
객체 생성을 서브클래스에 위임해, 상위 클래스는 생성 인터페이스만 정의한다.
Creator (상위)
├── createProduct() : abstract ← 팩토리 메서드
└── someOperation() ← 팩토리 메서드 사용
ConcreteCreatorA (하위)
└── createProduct() : ProductA ← 구체 생성 담당
ConcreteCreatorB (하위)
└── createProduct() : ProductB
// 추상 Creator
abstract class NotificationSender {
// 팩토리 메서드 — 서브클래스가 생성 방법을 결정
abstract fun createNotification(message: String): Notification
// 템플릿 메서드 — 팩토리 메서드로 만든 객체를 사용
fun send(message: String) {
val notification = createNotification(message)
println("알림 전송 준비...")
notification.send()
println("전송 완료")
}
}
// 구체 Creator — 각자 다른 Notification을 생성
class PushNotificationSender : NotificationSender() {
override fun createNotification(message: String): Notification =
PushNotification(message)
}
class EmailNotificationSender(private val email: String) : NotificationSender() {
override fun createNotification(message: String): Notification =
EmailNotification("$email: $message")
}
class SmsNotificationSender(private val phone: String) : NotificationSender() {
override fun createNotification(message: String): Notification =
SmsNotification("$phone: $message")
}
// 사용 — 호출부는 NotificationSender 타입만 알면 됨
fun notifyUser(sender: NotificationSender, message: String) {
sender.send(message)
}
notifyUser(PushNotificationSender(), "배송이 시작되었습니다")
notifyUser(EmailNotificationSender("user@example.com"), "결제가 완료되었습니다")
// 새 타입 추가 — 기존 코드 수정 없이 새 Creator만 추가 ✅
class KakaoNotificationSender : NotificationSender() {
override fun createNotification(message: String): Notification =
KakaoNotification(message)
}
4. 추상 팩토리 패턴 (Abstract Factory Pattern)
연관된 객체군(제품군)을 함께 생성하는 인터페이스를 제공한다.
팩토리 메서드가 하나의 제품을 생성한다면, 추상 팩토리는 관련된 여러 제품을 한 세트로 생성합니다.
// 제품 인터페이스들
interface Button {
fun render()
fun onClick()
}
interface TextField {
fun render()
fun onTextChanged(text: String)
}
interface CheckBox {
fun render()
fun onCheckedChanged(isChecked: Boolean)
}
// 추상 팩토리 — 연관된 UI 컴포넌트를 한 세트로 생성
interface UIComponentFactory {
fun createButton(label: String): Button
fun createTextField(hint: String): TextField
fun createCheckBox(label: String): CheckBox
}
// Material Design 세트
class MaterialUIFactory : UIComponentFactory {
override fun createButton(label: String) = MaterialButton(label)
override fun createTextField(hint: String) = MaterialTextField(hint)
override fun createCheckBox(label: String) = MaterialCheckBox(label)
}
// Dark Theme 세트
class DarkThemeUIFactory : UIComponentFactory {
override fun createButton(label: String) = DarkButton(label)
override fun createTextField(hint: String) = DarkTextField(hint)
override fun createCheckBox(label: String) = DarkCheckBox(label)
}
// Compact 세트 (태블릿용)
class CompactUIFactory : UIComponentFactory {
override fun createButton(label: String) = CompactButton(label)
override fun createTextField(hint: String) = CompactTextField(hint)
override fun createCheckBox(label: String) = CompactCheckBox(label)
}
// 클라이언트 — 팩토리만 교체하면 전체 UI 세트가 바뀜
class LoginScreen(private val factory: UIComponentFactory) {
fun build() {
val emailField = factory.createTextField("이메일")
val passwordField = factory.createTextField("비밀번호")
val loginButton = factory.createButton("로그인")
val rememberMe = factory.createCheckBox("로그인 상태 유지")
emailField.render()
passwordField.render()
loginButton.render()
rememberMe.render()
}
}
// 테마 교체 — LoginScreen 수정 없음 ✅
val lightScreen = LoginScreen(MaterialUIFactory())
val darkScreen = LoginScreen(DarkThemeUIFactory())
5. Kotlin — companion object Factory
Kotlin에서는 companion object에 팩토리 메서드를 두는 것이 관용적입니다.
class User private constructor(
val id: Long,
val name: String,
val role: Role
) {
enum class Role { ADMIN, MEMBER, GUEST }
companion object {
// 의미 있는 이름의 팩토리 메서드
fun admin(id: Long, name: String) =
User(id, name, Role.ADMIN)
fun member(id: Long, name: String) =
User(id, name, Role.MEMBER)
fun guest() =
User(id = 0L, name = "게스트", role = Role.GUEST)
fun fromJson(json: String): User {
// JSON 파싱 후 생성
val parsed = parseJson(json)
return User(parsed.id, parsed.name, Role.valueOf(parsed.role))
}
}
}
// 사용 — 생성 의도가 명확
val admin = User.admin(1L, "관리자")
val member = User.member(2L, "홍길동")
val guest = User.guest()
6. Kotlin — sealed class + 팩토리
sealed class와 팩토리를 결합하면 타입 안전하게 객체를 생성할 수 있습니다.
sealed class PaymentMethod {
abstract fun process(amount: Int): PaymentResult
data class Card(val cardNumber: String, val expiry: String) : PaymentMethod() {
override fun process(amount: Int) = PaymentResult.Success("카드 결제: ${amount}원")
}
data class BankTransfer(val accountNumber: String) : PaymentMethod() {
override fun process(amount: Int) = PaymentResult.Success("계좌 이체: ${amount}원")
}
object KakaoPay : PaymentMethod() {
override fun process(amount: Int) = PaymentResult.Success("카카오페이: ${amount}원")
}
companion object {
// sealed class 기반 팩토리
fun from(type: String, params: Map<String, String>): PaymentMethod = when (type) {
"CARD" -> Card(
cardNumber = params["cardNumber"] ?: error("카드번호 필수"),
expiry = params["expiry"] ?: error("유효기간 필수")
)
"BANK" -> BankTransfer(
accountNumber = params["accountNumber"] ?: error("계좌번호 필수")
)
"KAKAO" -> KakaoPay
else -> error("지원하지 않는 결제 수단: $type")
}
}
}
// 사용
val payment = PaymentMethod.from("CARD", mapOf("cardNumber" to "1234-5678", "expiry" to "12/26"))
val result = payment.process(15000)
println(result) // PaymentResult.Success(message=카드 결제: 15000원)
7. Android 실전 예제 ① — ViewModelFactory
Android에서 ViewModel에 파라미터를 전달할 때 팩토리 패턴을 사용합니다.
// ViewModel
class ProductDetailViewModel(
private val productId: Long,
private val productRepository: ProductRepository
) : ViewModel() {
val product = MutableLiveData<Product>()
fun loadProduct() {
viewModelScope.launch {
productRepository.getProduct(productId)
.onSuccess { product.value = it }
}
}
// companion object에 Factory 정의
companion object {
fun factory(
productId: Long,
repository: ProductRepository
): ViewModelProvider.Factory = object : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
@Suppress("UNCHECKED_CAST")
return ProductDetailViewModel(productId, repository) as T
}
}
}
}
// Fragment에서 사용
class ProductDetailFragment : Fragment() {
private val productId: Long by lazy {
requireArguments().getLong("productId")
}
private val viewModel: ProductDetailViewModel by viewModels {
ProductDetailViewModel.factory(
productId = productId,
repository = ProductRepositoryImpl(RetrofitClient.api)
)
}
}
8. Android 실전 예제 ② — Fragment Factory
Fragment에도 팩토리 패턴으로 안전하게 인자를 전달합니다.
class ProductDetailFragment : Fragment(R.layout.fragment_product_detail) {
private val productId: Long by lazy {
requireArguments().getLong(ARG_PRODUCT_ID)
}
companion object {
private const val ARG_PRODUCT_ID = "productId"
// 팩토리 메서드 — newInstance 패턴
fun newInstance(productId: Long): ProductDetailFragment {
return ProductDetailFragment().apply {
arguments = Bundle().apply {
putLong(ARG_PRODUCT_ID, productId)
}
}
}
}
}
// 사용 — 생성자 직접 호출보다 안전
val fragment = ProductDetailFragment.newInstance(productId = 42L)
supportFragmentManager.beginTransaction()
.replace(R.id.container, fragment)
.commit()
9. Android 실전 예제 ③ — 환경별 Retrofit 팩토리
// 추상 팩토리 — 환경별 API 세트 생성
interface ApiFactory {
fun createProductApi(): ProductApi
fun createOrderApi(): OrderApi
fun createUserApi(): UserApi
}
class ProductionApiFactory : ApiFactory {
private val client = OkHttpClient.Builder()
.addInterceptor(AuthInterceptor())
.build()
private val retrofit = Retrofit.Builder()
.baseUrl("https://api.example.com/")
.client(client)
.addConverterFactory(GsonConverterFactory.create())
.build()
override fun createProductApi(): ProductApi = retrofit.create(ProductApi::class.java)
override fun createOrderApi(): OrderApi = retrofit.create(OrderApi::class.java)
override fun createUserApi(): UserApi = retrofit.create(UserApi::class.java)
}
class MockApiFactory : ApiFactory {
override fun createProductApi(): ProductApi = MockProductApi()
override fun createOrderApi(): OrderApi = MockOrderApi()
override fun createUserApi(): UserApi = MockUserApi()
}
// Application에서 환경에 따라 팩토리 선택
class App : Application() {
val apiFactory: ApiFactory by lazy {
if (BuildConfig.DEBUG) MockApiFactory()
else ProductionApiFactory()
}
}
// 사용 — 어떤 환경이든 동일한 코드
val productApi = (application as App).apiFactory.createProductApi()
10. 세 가지 팩토리 패턴 비교
| 항목 | 심플 팩토리 | 팩토리 메서드 | 추상 팩토리 |
|---|---|---|---|
| 목적 | 생성 로직 집중 | 생성을 서브클래스에 위임 | 연관 객체군 일괄 생성 |
| OCP 준수 | ❌ 타입 추가 시 수정 | ✅ 새 Creator 추가 | ✅ 새 Factory 추가 |
| 복잡도 | 낮음 | 보통 | 높음 |
| 사용 시점 | 단순 분기 생성 | 단일 제품 다형성 | 제품군 교체 필요 시 |
| Android 사례 | object Factory | ViewModelFactory | 환경별 API 세트 |
11. 빌더 패턴 vs 팩토리 패턴
| 항목 | 팩토리 패턴 | 빌더 패턴 |
|---|---|---|
| 관심사 | 어떤 타입 의 객체를 만들 것인가 | 어떻게 객체를 조립할 것인가 |
| 매개변수 | 적고 타입 선택이 핵심 | 많고 선택적 |
| 반환 타입 | 다양한 서브타입 중 하나 | 항상 동일 타입 |
| 생성 단계 | 단일 호출 | 여러 단계 조립 후 build() |
12. 정리
| 항목 | 내용 |
|---|---|
| 핵심 아이디어 | 객체 생성 로직을 호출부에서 분리 |
| 심플 팩토리 | 정적 메서드로 생성 집중, 간단하지만 OCP 취약 |
| 팩토리 메서드 | 생성을 서브클래스에 위임, 단일 제품 다형성 |
| 추상 팩토리 | 연관 객체군을 한 세트로 생성, 테마/환경 교체에 유용 |
| Kotlin 활용 | companion object, sealed class, object |
| Android 사례 | ViewModelFactory, Fragment.newInstance, 환경별 API |
- 팩토리 패턴은 “무엇을 만들지는 내가 결정하되, 어떻게 만드는지는 몰라도 된다” 는 것이 핵심입니다.
- 팩토리 메서드는 단일 제품의 생성 유연성이 필요할 때, 추상 팩토리는 제품군 전체를 교체해야 할 때 사용합니다.
참고
- Design Patterns — GoF (Gang of Four)
- Effective Java 3rd Edition — Joshua Bloch
- 빌더 패턴 포스팅 보기
- Android ViewModelProvider.Factory 공식 문서