Post

Design Pattern: Abstract Factory

구체적인 클래스에 의존하지 않고 연관된 타입들의 그룹을 일관성 있게 생성하는 추상 팩토리 패턴을 다룹니다. 객체 간 결합도를 낮추고 OCP를 준수하는 안정적인 설계 방법을 분석합니다.

Design Pattern: Abstract Factory

객체 생성 결합도의 한계

많은 안드로이드 프로젝트에서 클라이언트 코드가 구체적인 클래스에 강하게 결합되어 유지보수가 힘든 경우가 발생합니다.

특정 테마의 UI를 그리기 위해 해당 테마의 인스턴스 컴포넌트를 직접 생성하는 코드를 살펴보겠습니다.

1
2
3
4
5
6
7
8
9
class LightScreen {
    val button = LightButton()
    val checkbox = LightCheckBox()

    fun render() {
        button.render()
        checkbox.render()
    }
}

이러한 객체 생성 방식은 다음의 문제점을 가집니다:

  1. 깨지기 쉬운 확장성: Dark 테마를 추가하려면 클라이언트 코드 내부의 모든 객체 생성 로직을 DarkButton, DarkCheckbox로 직접 수정하거나 조건문을 추가해야 합니다.
  2. OCP 위배: 새로운 제품군이 추가될 때마다 기존 클라이언트 코드가 수정되어야 합니다.
  3. 제품군 불일치 위험: 개발자의 실수로 LightButtonDarkCheckbox가 섞여 생성될 수 있는 상태 무결성 훼손 위험이 존재합니다.

Abstract Factory Pattern

추상 팩토리 패턴은 구체적인 클래스를 지정하지 않고도 관련 객체 또는 의존 객체의 제품군을 생성하기 위한 인터페이스를 제공하는 설계 패턴입니다.

쉬운 비유로 가구 매장을 생각해 볼 수 있습니다. “모던 스타일” 제품군을 선택하면 의자, 테이블, 소파가 모두 모던 디자인으로 맞춰서 생성됩니다. 클라이언트는 각 가구가 구체적으로 어떻게 만들어지는지 알 필요 없이, 선택한 제품군에서 서로 완벽하게 어울리는 세트를 보장받습니다.

구현 가이드

불필요한 구현체와 구현부를 생략하고, 팩토리 패턴의 핵심적인 방향과 구조에 집중한 예시 코드입니다.

Step 1: 인터페이스 정의

클라이언트가 의존해야 할 유일한 인터페이스입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/** 모든 버튼이 반드시 구현해야 하는 공통 인터페이스 */
interface Button {
    fun render(): String
    fun onClick(): String
}

/** 모든 체크박스가 반드시 구현해야 하는 공통 인터페이스 */
interface Checkbox {
    fun render(): String
    fun onToggle(checked: Boolean): String
}

// ============================================================
// 추상 팩토리 (Abstract Factory)
// 연관된 제품군을 생성하는 메서드들의 집합을 선언합니다.
// 구체적인 제품 클래스에 의존하지 않습니다.
// ============================================================

interface UiFactory {
    fun createButton(): Button
    fun createCheckbox(): Checkbox
}

Step 2: 구체적 제품 및 팩토리 구현

실제 각 타입의 인스턴스를 생성하는 세부 로직입니다. 클라이언트는 이 클래스들의 존재를 몰라야 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class LightButton : Button {
    override fun render(): String = "[Light Button] 흰 배경, 어두운 테두리로 렌더링"
    override fun onClick(): String = "[Light Button] 클릭 이벤트 발생 (Light 스타일)"
}

// 생략
class LightCheckbox : Checkbox { ... }
class DarkButton : Button { ... }
class DarkCheckbox : Checkbox { ... }

class LightThemeFactory : UiFactory {
    override fun createButton(): Button = LightButton()
    override fun createCheckbox(): Checkbox = LightCheckbox()
}

class DarkThemeFactory : UiFactory { ... }

Step 3: 클라이언트

클라이언트는 생성 로직을 팩토리에 위임하고, 오직 인터페이스하고만 상호작용합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
 * 어떤 팩토리가 주입되든 동일한 방식으로 동작하는 UI 렌더러.
 * 구체적인 제품 클래스에 전혀 의존하지 않는 것이 핵심입니다.
 */
class UiRenderer(private val factory: UiFactory) {

    // 팩토리로부터 위젯을 생성하되, 구체적 타입은 모릅니다.
    private val button: Button = factory.createButton()
    private val checkbox: Checkbox = factory.createCheckbox()

    fun renderAll(): List<String> = buildList {
        add(button.render())
        add(checkbox.render())
    }

    fun simulateInteraction(): List<String> = buildList {
        add(button.onClick())
        add(checkbox.onToggle(true))
        add(checkbox.onToggle(false))
    }
}

Pros & Cons

  1. 제품군 일관성: LightThemeFactory를 사용하는 한, 해당 뷰는 라이트 계열 위젯만 생성되도록 구조적으로 보장됩니다.
  2. OCP: 새로운 MaterialThemeFactory를 도입하더라도 클라이언트 코드는 수정할 필요가 없습니다.
  3. 확장의 경직성: 새로운 ‘종류의’ 컴포넌트가 추가되면 UiFactory인터페이스를 수정해야 하고, 이에 의존하는 모든 구체 팩토리 클래스에 연쇄적인 수정이 강제됩니다.

동작 확인

제대로 된 결과를 반환하는지 확인합니다.

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
@DisplayName("Abstract Factory Pattern")
class AbstractFactoryTest {

    @Nested
    @DisplayName("LightThemeFactory")
    inner class LightThemeFactoryTests {

        private val factory: UiFactory = LightThemeFactory()

        @Test
        fun light_factory_creates_light_button() {
            val button: Button = factory.createButton()
            val rendered = button.render()

            assertTrue(
                rendered.contains("Light Button"), 
                "Light 팩토리는 Light 버튼을 생성해야 합니다."
            )
        }

        // 생략

        @Test
        fun light_factory_creates_light_checkbox() { ... }

        ...
    }

    @Nested
    @DisplayName("DarkThemeFactory")
    inner class DarkThemeFactoryTests {

        private val factory: UiFactory = DarkThemeFactory()

        @Test
        fun dark_factory_creates_dark_button() {
            val button: Button = factory.createButton()
            val rendered = button.render()

            assertTrue(
                rendered.contains("Dark Button"), 
                "Dark 팩토리는 Dark 버튼을 생성해야 합니다."
            )
        }
        
        // 생략

        @Test
        fun dark_factory_creates_dark_checkbox() { ... }

        ...
    }

    @Nested
    @DisplayName("UiRenderer (클라이언트)")
    inner class UiRendererTests {

        @Test
        fun renderer_with_light_factory_renders_light_widgets() {
            val renderer = UiRenderer(LightThemeFactory())
            val results = renderer.renderAll()

            assertTrue(
                results.all { it.contains("Light") }, 
                "Light 팩토리 주입 시 모든 위젯은 Light 계열이어야 합니다."
            )
        }

        // 생략
        
        ... 
    }
}

결론

추상 팩토리 패턴의 본질은 단순히 객체를 생성하는 클래스를 분리하는 것이 아닙니다. 시스템에서 변하는 부분과 변하지 않는 부분을 완벽하게 디커플링하여, 유지보수 시 Ripple Effect를 최소화 하는 아키텍처를 구축하는 데 목적이 있습니다.

Learn More

앞으로 다양한 디자인 패턴을 다룰 예정이지만, 스스로 어떤 패턴이 있는지 찾아보며 자기만의 코드 스타일로 구현해 보세요. 디자인 패턴은 단순히 복사해서 붙여넣는 코드 템플릿이 아닙니다. 많은 예제들이 Factory, Manager 같은 전형적인 네이밍 컨벤션을 사용하지만, 표면적인 형태나 이름에 얽매일 필요는 없습니다.

가장 중요한 것은 이 패턴이 왜 탄생했는가?라는 본질을 보는 것입니다.

패턴의 목적을 명확히 이해했다면, GoF의 고전적인 패턴들을 맹목적으로 답습하기보다 Kotlin의 강력한 기능을 활용해 본인만의 코드로 재해석해 보세요.

사실 모든 개발자들은 이미 훌륭한 패턴들을 매일 사용하고 있습니다. 무심코 사용하던 라이브러리들의 내부 동작들을 확인하기 위해 구현체/구현부를 들여다본 적이 있다면, 그 안에 녹아있는 견고한 설계 철학을 이미 눈으로 확인한 것입니다. 디자인 패턴은 색다른 기술이 아닙니다. 수많은 엔지니어들이 치열하게 고민하며 반복적으로 사용해 온 최적의 해결책을 공통의 언어로 정립해 둔 것에 불과합니다. 알게 모르게 여러분이 작성한 로직 속에도 이미 훌륭한 패턴들이 스며들어 있을 것입니다.

마지막으로, 많은 사람들이 MVC, MVVM, MVI 등의 아키텍처 패턴을 디자인 패턴으로 혼용해서 부르곤 합니다.

하지만 저는 이 둘의 스케일(Scope)을 명확히 다르게 봐야 한다고 생각합니다. 엔지니어라면 거시적인 뼈대와 미시적인 전술을 엄격히 구분해야 합니다.

앞으로 학습을 진행하시면서, 이 두 개념이 어떻게 다르고 또 실제 시스템에서 어떻게 조화를 이루는지 스스로 탐구해 보시길 권합니다.

This post is licensed under CC BY 4.0 by the author.