(Java/Kotlin) 팩토리 패턴(Factory Pattern) 완전 정리

개요


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

참고



Related Posts