ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Design Pattern]디자인패턴 이해하기(Kotlin)
    DesignPattern/GoF 2020. 11. 26. 18:16


    Design Pattern Category


    1. Creational Pattern

      • Abstract Factory ✅
      • Builder ✅
      • Factory Method ✅
      • Object Pool
      • Prototype ✅
      • Singleton ✅

    2. Structural Pattern

      • Adapter ✅
      • Bridge ✅
      • Composite ✅
      • Decorator ✅
      • Facade
      • Flyweight
      • Private Class Data
      • Proxy

    3. Behavioral Pattern

      • Chain of responsibility
      • Command
      • Interpreter
      • Mediator
      • Memento
      • Null Object
      • Observer
      • State
      • Strategy ✅
      • Template Method ✅
      • Visitor

    기본 개념

    Abstract class vs Interface


    Abstract Class(추상 클래스)

    • abstract 프로퍼티, 메서드 가질 수 있음. 이는 상속받은 클래스가 반드시 구현해야 하는 약속.
    • 단, 그냥 메서드도 만들 수 있음. 이를 상속받은 클래스가 그대로 사용 가능. 즉, default한 기능 설정 가능.
    • protected 사용 가능
    • Constructor 사용 가능
    • 단, 인스턴스로는 못만든다(단독으로 실체화 못시킴!)
    • 단 하나만 상속 가능(클래스 상속의 특징)

    Interface(인터페이스)

    • 모든 구성이 다 abstract라고 보면 됨. 즉, 상속받은 클래스는 interface의 모든 것을 구현해야 함(override)
    • 얘도 프로퍼티, 메서드 가질 수 있음.
    • 상속받은 클래스들이 모두 같은 기능(동작이 완전 같은게 아니라, 행동이 같도록)을 하도록 보장하고 싶을때 사용
    • protected 사용 못함. 그 이유는 인터페이스 자체가 "외부로부터 공통되게 볼 수 있는 것"이기 때문.
    • Constructor 사용 못함
    • 단, 인스턴스로는 못만든다(단독으로 실체화 못시킴!)
    • 무한정 구현 가능

    Delegate(위임)이란


    어떤 일을 다른 객체에게 해달라고 떠넘기는 방식.

    똑같은 기능을 여러곳에서 사용하는데, 매번 같은 코드를 쓸 필요는 없다!

    대신 해주는 객체를 만들고, 그 기능을 해당 객체에게 위임하자.

    아래의 Delegation Pattern을 보면 이해가 잘 될것이다!

    Inheritance vs Composition


    • 상속은 "Is-a" 관계 vs 구성은 "has-a" 관계

    상속은 클래스를 상속받아, 기능을 확장함. 단, 문제점이 있는데

    • 상속은 단일 패키지에서 사용해야만 안전하다.
    • 하위 클래스는 상위 클래스에 많이 의존하게 된다.
    • 상위 클래스의 코드가 수정되면 하위 클래스의 코드도 수정되어야 하는 경우가 많다.(재정의 메소드)
    • 하위 클래스가 상위 클래스의 메서드를 재정의 할 때, 메서드의 parameter 타입이나 개수를 바꾸거나 등이 불가능하다!
    • 확장이라는 목표를 두고 상속을 사용하면 괜찮으나, 해당 설계가 아닌 상황에서 상속을 사용할 시 문제를 발생할 수 있다.
    • 상속은 캡슐화를 위반한다.
      • 하위 클래스가 상위 클래스에 구체적인 구현 내용을 의존하고 있기 때문이다.

    구성은 하위 클래스에서 상위 클래스를 상속받는게 아니라, 인스턴스를 생성해서 참조하는 방식이다.

    class Child {
        private val parent = Parent()  << 상속 대신 프로퍼티로써 갖는다.
    
        fun sum(x: Int, y: Int){  << 상속과는 다르게, 파라미터도 마음대로 바꿀 수 있다!
    				parent.sum(x, y)  << 부모의 메서드를 호출하여 사용.
    				println("Child sum!")
        }
    }

    하지만 위의 예시는 이해하긴 좋지만 적절치 못하다. "has-a" 관계로만 쓰는것이 좋다! (Child has a Parent는 아니니...)

    class Car {
        private val engine = Engine()  << Car has a engine 말이 된다.
    
        fun start(power: Int, seconds: float){  
    				engine.start(power)  
    	      for (i in 0..seconds){
    		      println("부릉부릉 $i 초")
    				}
        }
    }

    상위 클래스에 필요한 것을 참조해서 마음대로 갖다 쓴다.

    Dependency vs Composition


    • Dependency는 외부로부터 객체를 받음. 또한 자신 객체와 받은 객체 사이의 연관성은 딱히 없음.
    • Composition은 내부로부터 객체를 만듬. 또한 자신 객체와 생성한 객체 사이는 부모-자식 관계임. 포함관계.

    Design Pattern

    Strategy Pattern


    공통된 인터페이스 A를 갖는 객체들(E, F, G)을 B 클래스에서 사용할때, 각각을 따로 정의하여 사용하는게 아니라, 인터페이스 A만을 이용하여 들어오는 객체 종류(E, F, G)에 따라 동작하도록 하는 패턴

    우리가 인터페이스를 쓰는 일반적인 이유.

    예시

    • Weapon이라는 인터페이스를 구현한 Sword, Knife, Axe가 있다고 하자. 그리고 Character가 있다.
    • Character는 무기를 받을 때 각 무기 클래스가 아니라, Weapon이라는 인터페이스로 받는다(의존성 주입)
    • Weapon에는 attack()이라는 공통 메서드가 있으므로, 3가지 무기 아무거나 attack()을 사용할 수 있다.
    • 이제 Character는 3가지 무기에 대한 공격을 따로 구현하지 않아도 되며, Weapon이라는 인터페이스의 공통 메서드를 통해 어느 무기 종류나 공격할 수 있다!

    즉, 하나의 공통 인터페이스를 통해 그걸 구현한 여러 객체를 전략적으로 사용 가능함.

    Template Method Pattern


    어떤 일련의 과정(일정한 프로세스)이 있고, 그 순서 자체는 변경이 안될때 사용하는 패턴.

    과정은 그대로지만, 각 과정에 해당하는 메서드들을 사용자가 재정의하여 사용할 수 있음.

    즉, 각 과정을 유지보수하기 편함!

    예시

    • 게임 접속 과정은 (암호화 - 사용자 인증 - 권한 확인 - 연결)로 이루어져 있음
    • 추상 클래스 Helper를 만들어, connectGame()이라는 메서드에 위 연결 과정들을 순서대로 사용함(어느 게임이나 게임 접속 과정은 같으므로). 대신 각 과정은 추상 메서드로 만듬.
    • 이제 사용자가 임의의 게임 연결을 구현할 때, 해당 Helper를 상속하여 각 과정만 구현하면 됨(암호화 - 사용자 인증 - 권한 확인 - 연결 각 과정들)
    • 내가 connectGame()을 따로 구현하지 않더라도, 각 과정을 구현했다면 게임 연결이 바로 사용 가능하다.
    • 또한 연결 순서는 어느 게임이나 똑같아서 다른 게임에도 그대로 적용 가능하다!(물론 각 과정은 손 봐야 겠지만)

    Adapter Pattern


    특정 기능을 요구사항에 맞춰 사용할 수 있도록 하는 패턴.

    실제 어댑터는 110V를 220V로 바꿔주는 등의 기능을 한다. 똑같다.

    예시

    • Weapon 인터페이스가 있고, 나는 Sword를 잘 쓰고 있다.
    • 하지만 무기가 없어졌다.
    • Weapon이 아닌 Ball 클래스가 있다. 이는 Weapon 인터페이스를 구현하지 않은, 그냥 공이다(무기가 아닌것).
    • 하지만 무기로 급하게 써야한다!
    • 그때 WeaponAdapter를 만들어 공을 공격 가능하도록 만든다(던져서 맞춘다).

    즉, 서로 다른 인터페이스는 각자의 기능을 못쓰니까, 쓸 수 있도록 Adapter를 거치게 하여 사용 가능하도록 하는 패턴.

    (인터페이스가 달라도 쓸 수 있게 해준다는 뜻)

    Factory Method Pattern


    Template Method Pattern이랑 똑같음. 다만, 공장처럼 특정 객체를 만들어서 반환하는 경우의 패턴임.

    예시

    • HP포션과 MP포션 Item이 있다.
    • 이 포션을 만드는 추상 클래스(인터페이스 말고) ItemCreator를 선언한다.
    • ItemCreator에는 (아이템 정보 DB에서 가져옴 - 아이템 만든다고 DB에 정보 등록 - 아이템 객체 생성해서 반환)라는 일련의 과정인 create() 메서드 존재(템플릿 메서드랑 똑같음)
    • 그럼 HpPotionCreator와 MpPotionCreator 클래스는 추상 클래스 ItemCreator를 상속하여 각 과정(DB와 통신하여 객체 생성 반환)을 구현하면 됨. create()는 신경 쓸 필요가 없다.
    • Template Method와 같지만 공장처럼 객체를 만들어서 반환해주는 것이 차이점!

    즉, 특정 객체를 만들어 반환해야 하는 경우의 템플릿 메서드 패턴임.

    한 팩토리 당 한 종류(create 메서드가 1개)

    왜 Template Method랑 Factory Method는 인터페이스가 아니라 추상클래스로 구현하는가?

    → 인터페이스는 그냥 메서드를 작성할 수 없다(추상 메서드만 작성 가능). 따라서 일련의 고정적인 과정을 구현한 메서드를 만드려면 추상 클래스가 요구되는 것!

    Singleton Pattern


    계속 공통된 객체를 사용해야 할 경우(아무 변경 없이), 매번 인스턴스를 생성하면 메모리만 낭비함.

    이 경우 단 한번만 객체가 생성되도록 하는 패턴(해당 객체로 돌려쓰기)

    예시

    • 클래스 안에 자기 자신의 인스턴스를 갖도록 함(static한 클래스 멤버로 선언해야함)
    • getInstance()와 같은 메서드 호출 시, 자신의 인스턴스를 갖고 있나 확인하여 없다면 만들고 있다면 전에 있던거 반환

    즉, 메모리 낭비 막기 위해 사용하는 패턴.

    Kotlin에선 object class로 바로 생성 가능하다.

    Prototype Pattern


    인스턴스를 생성하는 비용이 매우 큰 객체가 있을 때, 새로 생성하지 않고 기존의 것을 복사(clone)하여 만드는 패턴.

    • 프로토타입 패턴을 적용하고 싶은 클래스에다가 clonable 인터페이스를 구현한다.
    • clone 메서드를 구현하여(clonable 인터페이스에 있는 메서드임) 복사한 객체를 반환하도록 함.

    주의할 점

    clone할 때 얕은 복사와 깊은 복사 둘 다 이루어짐. primitive한 타입들은 깊은 복사가 이루어지지만(값이 넘어가므로), heap에 올라가는 객체들은 얕은 복사가 이루어짐.

    즉, 생성 비용이 큰 경우 새로 생성하는 대신 다른 객체를 복사하는 패턴

    Builder Pattern


    많은 변수(멤버)를 가진 객체 생성을 대신해주는 패턴. 가독성이 높아진다.

    멤버가 오지게(?) 많으면 객체 생성 시 실수할 수도 있고 가독성도 떨어진다.

    ex) MyCharacter(nickname, age, class, sex, hometown, level, equipLevel, ...)

    Builder를 사용하면 MyBuilder.setCPU("i7").setRAM("DDR4 8g").build() 와 같이 사용하는 것.

    Factory Method Pattern과의 차이

    • 팩토리 패턴은 사용자가 그 과정을 알 필요가 없음. 그냥 create하면 객체 생성해서 줌. 생성자 wrapper라 보면 됨.
    • 빌더 패턴은 사용자가 그 과정을 커스텀 가능함. 각 멤버들을 setCPU()같은 메서드로 직접 조정하면서 객체를 생성함. 멤버 wrapper라 보면 됨.

    Abstract Factory Pattern


    공통된 주제객체들을 만드는 공장이 필요할 때, 그 공장에 대한 인터페이스가 바로 추상 팩토리 패턴.

    우리가 흔히 팩토리 패턴이라 일컫는건 사실 이 추상 팩토리 패턴을 말한다!(팩토리 메서드가 아니다!)

    예시

    • 인터페이스 GuiFactory는 Button, TextArea를 생성한다.
    • 운영체제 linux, window, mac 3개가 있다.
    • 각 운영체제마다 GUI를 구현하는건 다르다. (LinuxGuiFactory, WindowGuiFactory, MacGuiFactory)
    • 각 운영체제마다의 팩토리는 GuiFactory를 상속받아 구현한다.
    • 각 운영체제의 팩토리에 맞는 Button, TextArea 생성을 구현한다(LinuxButton, WindowButton 등...)

    하지만 이렇게 하면 위험하다. 사용자가 자기 운영체제에 맞게 Factory를 직접 선택해서 인스턴스를 생성해야하기 때문.

    따라서 사용할 때 다음과 같이 하는게 좋다.

    • FactoryInstance 클래스를 만들어 개발자가 현재 운영체제에 맞게 알맞은 Factory 인스턴스를 알아서 리턴하도록 만들어 주어야 함.

    Factory Method Pattern vs Abstract Factory Pattern

    • 객체 1개 생성 vs 객체 여러개 생성
    • 객체의 종류를 결정 vs 팩토리의 종류를 결정
    • 과정(Process) 중심 vs 종류(Platform) 중심
    • ConcreteProduct와 Client의 결합도를 낮춤 vs ConcreteFactory와 Client의 결합도를 낮춤 (이게 무슨 말이냐면, 사용자가 해당 객체에 큰 신경을 쓸 필요가 없다는 뜻이다.) (전자의 경우 Hp포션을 만들어달라 하면 Hp포션을 얻는 것) (후자의 경우 사용자가 Windows 운영체제일 때 Button이나 TextArea를 달라하면 WindowsButton과 WindowsTextArea를 줌)

    실제 Android에서는 ViewModelFactory로 많이 사용한다.

    ViewModel은 각 Activity 또는 Fragment마다 한 개씩 개별적으로 존재하는데, 이를 사용자가 신경쓰지 않고 ViewModelFactory를 통해 호출한 곳에 알맞은 ViewModel 객체를 리턴받는 형식으로 사용된다.

    Bridge Pattern


    구현과 추상을 분리하는 패턴. Strategy Pattern과 상당히 헷갈린다;;

    • Strategy Pattern: 구현부분이 자주 바뀔때 사용됨
    • Bridge Pattern: 계층간의 의존성을 낮추는데 사용됨

    전략 패턴은 client가 사용 시 자주 바뀌는 부분에 주로 사용되고,

    브릿지 패턴은 client의 플랫폼에 따라 바뀌는 부분에 주로 사용된다.

    예시

    • 무기가 검, 도끼, 활 3개가 있다고 하자.
    • 그럼 Weapon이란 인터페이스를 만들고, 각 무기는 attack()이란 메서드를 공통으로 갖는다.
    • 여기까지가 Strategy Pattern이다!
    • 하지만 무기에 속성이 추가되었다. 불과 얼음이 생겼다.
    • 그럼 Weapon 인터페이스를 구현한 FireWeapon, IceWeapon 클래스를 만들고, 각 클래스를 각각의 무기가 상속받아 구현한다(FireSword, IceSword, FireAxe ...)
    • 2 * 3의 6가지 클래스를 만들어야 한다.
    • 속성이 또 생긴다면..? 트리구조로 계속 생긴다!!
    • 따라서 속성을 담당하는 Elemental과, 무기를 담당하는 Weapon 두 가지 대분류를 만든다.
    • 그리고 Weapon 인터페이스에서 Elemental 객체를 속성으로 갖는다!(의존성 주입 생각하면 된다)
    • 그럼 각 무기에는 Elemental 프로퍼티가 생겼으며, 이는 사용자가 속성을 주입해줌에 따라 무기 속성이 계속 바뀐다!

    중간에 Weapon 인터페이스 프로퍼티로써 Elemental 객체를 갖는다고 했는데, 이는 Composition과 같은 것 아닌가요?

    → 틀리다. 맨 위의 비교 설명을 보면 알겠지만, Composition은 내부에서 객체를 생성한다. 하지만 여기선 Elemental 객체를 주입받음으로, Dependency가 맞는 것!

    Composite Pattern


    컴포짓 패턴은 클라이언트가 복합 객체나 단일 객체를 동일하게 취급하는 것을 목적으로 한다. 컨테이너와 내용물을 동일시 한다고 보면 된다. 즉, 상하관계가 있는 객체들을 동일하게 취급할 수 있도록 하는 패턴.

    보통 트리 구조로 작성되는 것이 일반적이며(leaf 노드와 branch), 집합-부분(Composite-Leaf)의 관계를 표현한다.

    Client는 둘을 나타내는 공통 인터페이스를 통해 상호작용 하면 된다!

    예시

    • 가장 대표적인 예시가 Directory - File 관계이다.
    • 리눅스 파일 시스템의 요소를 Component 인터페이스라 놓고, Directory를 Composite, 파일을 Leaf라고 생각하면 쉽다.
    • 사용자는 Component 인터페이스를 통해 폴더&파일과 상호작용한다. 폴더 따로 파일 따로 호출할 필요가 없다는 것!
    • 물론 인터페이스를 추상 클래스로 구현하여도 된다. 사용자 마음.

    아니 Strategy Pattern도 인터페이스 하나로 여러 클래스를 클라이언트가 공통되게 쓰는데, 무슨 차이가 있는거죠?

    → Strategy Pattern은 각 클래스들이 동일한 우위를 갖고 있다(평등하다?). 반면 Composite Pattern은 폴더와 파일 관계처럼 집합관계가 존재한다.

    Decorator Pattern


    데코레이터 패턴이란 주어진 상황 및 용도에 따라 어떤 객체에 책임을 덧붙이는 패턴으로, 기능 확장이 필요할 때 서브클래싱 대신 쓸 수 있는 유연한 대안이 될 수 있다.

    어떤 기능을 하는 A가 있다. 하지만 난 A에 덧붙여서 기능들을 추가해 나가고 싶다.

    그때 사용하는 것이 데코레이터 패턴.

    대표적으로, java의 List를 생각하면 쉽다. List에서 기능을 확장해 나간 ArrayList, MutableList 등등...

    예시

    • 무기에 인챈트 하는 예시는 적절치 못한게, 인챈트 속성이 큰 집합을 이루기 때문에(속성) 매번 방대한 decorate을 해 줄수가 없다! 인챈트는 Bridge Pattern이 더 어울린다.
    • 따라서 슬라임이 진화하는 것으로 예를 들자.
    • Slime 인터페이스가 있고, 가장 기본인 BasicSlime 클래스가 있다.
    • BasicSlime 클래스는 attackPower가 10이다.
    • 우린 PoisonSlime 클래스를 만들고 싶다.
    • 그렇다고 PoisonSlime에 BasicSlime이나 Slime을 상속받는게 아니라, PoisonSlime에 BasicSlime을 의존성 주입해준다.
    • 그리하면 PoisonSlime은 BasicSlime의 기존 공격력 10에다가 독 추가(+30)로 총 공격력이 40이 된다!

    그냥 상속받으면 될걸 굳이 의존성 주입을 받아야 하나?

    → 물론 상속을 받아도 똑같이 구현할 수 있다. 하지만 슬라임은 계열이 굉장히 많다. 기본 슬라임이 포이즌 슬라임이 되면서 기본 공격에 독 공격이 추가됐다. 하지만 만약 불 슬라임이 포이즌 슬라임으로 진화한거라면? 불 공격에 독 공격이 추가되는거다. 그럼 불 슬라임을 상속한 포이즌 슬라임을 또 만들것인가?!

    → 그래서 의존성을 주입받아 원래 있는 부모 슬라임 기능에 독립적으로 추가 기능을 넣는것!

    Bridge Pattern과의 차이

    • Bridge Pattern은 큰 집합들을 서로 다리로 이어주는 느낌이다(Weapon과 Elemental).
    • 앞선 Bridge Pattern에 쓰인 속성 무기 예제를 Decorator로 구현한다고 해보자.
    • FireSword는 Sword를 주입받아 불 속성이 추가될 것이다.
    • IceSword도 Sword를 주입받아 얼음 속성이 추가될 것이다.
    • FireSword가 IceSword를 주입받아(같은 Sword를 상속받은 클래스) 얼음+불 속성이 추가 되었다. 하지만 이름이 FireSword이다(??)
    • Decorator Pattern은 단순히 기능의 '확장'임. 즉, 확장하는 기능이 어떤 집합을 이루게 되는건 Bridge Pattern이 맞는거고, 독립적인 기능을 가진 확장이면(즉 조금 손봐야 되는것) Decorator Pattern이 맞는것이다.

    Delegation Pattern


    구현해야할 추상메서드 등의 구현을 다른 객체로 위임하는 방식.

    예시

    • Sword는 Weapon 공통 인터페이스를 가지며 attack 메서드를 구현한다.
    • 하지만 광석류 무기들은 strongAttack이란 메서드가 추가로 있다고 해보자.
    • 기존에 IronSword를 사용하고 있다 하자.
    • 갑자기 BronzeSword를 구현해야 한다.
    • 이 때 단순히 Weapon을 구현하고 strongAttack 메서드를 추가할 수 있다. 또는 광석류 무기를 인터페이스나 추상클래스로 집합을 만들 수도 있다.
    • 하지만 광석류 무기가 단 2개라면?!
    • 그 때, BronzeSword 클래스 내에서 IronSword 객체를 만들어 strongAttack 메서드를 위임받아 사용한다!
    class BronzeSword : Weapon {
    	override fun attack(){
    		println("Bronze attack!")
    	}
    
    	fun strongAttack(){
    		println("Strong attack!")  << 원래라면 메서드 새로 구현해야 해서 귀찮다.
    		... 
    		굉장히 많은 코드들
    		...
    	}
    }
    
    // -------------------------------------
    
    class BronzeSword : Weapon {
    	private val ironSword: IronSword = IronSword()  << 위임 객체 생성
    
    	override fun attack(){
    		println("Bronze attack!")
    	}
    
    	fun strongAttack(){
    		this.ironSword.strongAttack()  << 위임 받아 boilerplate code 삭제!
    	}
    }

    아니 Composition과 Delegation 차이점이 없어 보이는데요?

    Composition은 해당 객체와 포함관계를 갖는다. 즉, 부모-자식같은 관계가 있다. "has-a" 관계가 있어야 한다!

    → 하지만 Delegation은 관계가 존재하지 않아도 된다. 단지, 해당 객체의 특정 기능을 빌려 쓸때 사용하는 것! (BronzeSword has a IronSword 말이 안된다!)

    +추가로, Interface Delegation

    • Decorator Pattern의 구현을 줄이기 위해 제공됨
    • Kotlin에서 by키워드로 사용

    아래 코드를 보면 바로 이해가 될 것이다.

    interface Weapon {
    	fun attack()
    }
    
    class IronSword : Weapon{
    	override fun attack(){
    		println("Attack!!")
    	}
    }
    
    class BronzeSword : Weapon by IronSword(){
    	// 아무 코드도 없음.
    }
    
    // ----------------------
    fun main(){
    	val myWeapon = BronzeSword()
    	myWeapon.attack()
    }
    >> "Attack!!"
    • 즉, Weapon이라는 공통 인터페이스때문에 구현해야 하는 사항들을 다른 객체에게 구현을 위임하는 것이다.
    • 위를 보면, BronzeSword의 attack 메서드를 IronSword 객체를 통해 대신 구현했다.

    (Notion에서 그대로 옮겼는데, 코드블럭 부분이 이쁘지가 않다ㅠㅠ...!)


    댓글

Designed by Tistory.