본문 바로가기
아이폰 개발/디자인 패턴

swift 디자인 패턴 - 데코레이터 패턴

by 인생여희 2022. 11. 8.

swift 디자인 패턴 - 데코레이터 패턴

 

목표 : 독립적인 추가 기능의 조합 방법 이해하기.

데커레이터 패턴을 통한 기능의 조합 방법 이해하기.

 

도로 표시 방법 조합하기

 

자동차의 네비게이션에서 도로를 표시해 주는 기능이 있다.

 

기본적으로 도로를 표시해 주는 기능이 있고, 추가적으로 차선을 표시해주는 기능이 있다. 

 

RoadDisplay 클래스 : 기본 도로 표시 기능 담당

RoadDisplayWithLane 클래스 : 기본 도로 표시에 추가적으로 차선을 표시하는 기능 담당

 

이때 RoadDisplayWithLane 클래스는 RoadDisplay 클래스의 하위 클래스로 설계를 한다.

[코드 1]

import Foundation
import CoreLocation


/// 기본 도로 표시 클래스
class RoadDisplay{
    func draw(){
        print("기본 도로 표시")
    }
}


/// 기본도로 표시 + 차선 표시 클래스
class RoadDisplayWithLane: RoadDisplay{
    
    override func draw() {
        // 상위 클래스, 즉 RoadDisplay 클래스의 draw 메서드를 호출해서 기본 도로를 표시
        super.draw()
        
        // 추가적으로 차선을 표시
        drawLane()
    }
    
    private func drawLane(){
        print("차선 표시")
    }
    
}

let road = RoadDisplay()
road.draw() // 기본 도로만 표시
/*
 [출력]
 기본 도로 표시
 */


print("")

let roadWithLane = RoadDisplayWithLane()
roadWithLane.draw() // 기본 도로 + 차선 표시

/*
 [출력]
 기본 도로 표시
 차선 표시
 */

 

RoadDisplay 클래스에는 기본 도로 표시 기능을 실행하기 위한 draw 메서드를 구현한다.

 

RoadDisplayWithLane 클래스에는 기본 도로 표시뿐만 아니라 차선을 표시하려고 상속 받은 draw 메서드를 오버라이드 한다.

 

RoadDisplayWithLane 클래스에서 기본 도로 표시 기능은 상위 클래스, 즉, RoadDisplay 클래스의 draw 메서드를 호출 함으로써 구현하고 , 차선을 표시하는 추가 기능은 drawLane 메서드를 호출 함으로써 구현한다.

 

 

문제점

1.또 다른 도로 표시 기능을 추가로 구현하고 싶다면 어떻게 해야 하는가?

예) 기본 도로 표시에 교통량을 표시하고 싶다면?

 

 

2.여러가지 추가 기능을 조합해 제공하고 싶다면 어떻게 해야 하는가?

예) 기본 도로 표시에 차선 표시 기능과 교통량 표시 기능을 함께 제공하고 싶다면?

 

 


 

 [시나리오 1] 또 다른 도로 표시 기능을 추가로 구현하는 경우

 

RoadDisplay 클래스를 상속 받아서 RoadDisplayWithLane 클래스를 정의해서 도로에 차선을 표시한 것과 동일한 방식으로 교통량을 도로에 표시하는 클래스를 새로 정의 할 수 있다.

 

예) RoadDisplayWithTraffic 클래스는 도로에 교통량을 추가로 표시하는 클래스로 RoadDisplay 클래스를 상속 받아 정의 할 수 있다.

 

 

RoadDisplayWithTraffic 클래스의 draw 메서드는 기본 도로 표시는 물론이고 교통량도 표시해야 한다.

 

그러므로 RoadDisplay 클래스에서 상속받은 draw 메서드를 오버라이드 할 필요가 있다.

 

이때 draw 메서드는 기본 도로 표시는 RoadDisplay 클래스의 draw 메서드를 호출하고, 교통량 표시는 drawTraffic 메서드를 호출 함으로써 구현한다.

 

[코드2]

import Foundation
import CoreLocation


/// 기본 도로 표시 클래스
class RoadDisplay{
    func draw(){
        print("기본 도로 표시")
    }
}


/// 기본도로 표시 + 교통량 표시 클래스
class RoadDisplayWithTraffic: RoadDisplay{
    
    override func draw() {
        // 상위 클래스, 즉 RoadDisplay 클래스의 draw 메서드를 호출해서 기본 도로를 표시
        super.draw()
        
        // 추가적으로 교통량을 표시
        drawTraffic()
    }
    
    private func drawTraffic(){
        print("교통량 표시")
    }
    
}

let road = RoadDisplay()
road.draw() // 기본 도로만 표시
/*
 [출력]
 기본 도로 표시
 */


print("")

let roadWithLane = RoadDisplayWithTraffic()
roadWithLane.draw() // 기본 도로 + 교통량 표시

/*
 [출력]
 기본 도로 표시
 교통량 표시
 */

 

 

✅ [시나리오 2] 여러가지 다양한 추가 기능을 조합해야 하는 경우

RoadDisplay 클래스의 하위 클래스로 도로표시에 차선표시, 교통량 표시 등을 추가하는 것은 적절한 설계 방법이 될 수 있다.

 

그러나 다양한 기능의 조합을 고려해야 하는 경우 상속을 통한 기능의 확장은 각 기능별로 클래스를 추가해야 한다는 단점이 있다.

 

예를 들어 도로 표시를 하는 기본 기능에 추가로 제공할 수 있는 기능으로 차선 표시, 교통량표시, 교차로 표시를 한다고 가정할 때 아래와 같은 총 8가지의 조합이 가능하다.

 

 

위와 같이 상속을 통해서 조합의 각 경우를 설계한다면 각 조합별로 하위 클래스를 구현해야 한다.

 

 

✅ 해결책

위에서 상속을 이용한 기능 추가 방법을 이야기 했다.

 

그런데 문제는 이 방법은 추가되는 기능의 조합별로 하위 클래스를 구현해야 하는 문제가 있다.

 

이렇게 조합 수가 늘어나는 문제를 해결할 수 있는 설계를 하려면 각 추가 기능별로 개별적인 클래스를 설계하고 기능을 조합할 때 각 클래스의 객체 조합을 이용하면 된다.

 

아래 그림은 기본 기능인 RoadDisplay 클래스에 차선을 표시하는 기능을 추가하기 위한 LaneDecorator 클래스와 교통량을 표시하는 기능을 추가하기 위한 TrafficDecorator 클래스를 이용한 설계 그림이다.

 

 

기본 기능만 이용할 때는 RoadDisplay 클래스의 객체를 생성하면 충분하다. 

 

하지만 차선을 표시하는 기능이 추가적으로 필요하다면 LaneDecorator 클래스의 객체가 필요하다.

 

이때 LaneDecorator 에서는 차선 표시 기능만 직접 제공하고 도로 표시 기능은 RoadDisplay 클래스의 draw 메서드를 호출하는 방식으로 구현한다.

 

LaneDecorator 클래스는 RoadDisplay 객체에 대한 참조가 필요한데, 이는 LaneDecorator 클래스의 상위 클래스인 DisplayDecorator 클래스에서 Display 클래스로의 컴포지션 관계를 통해 표현되고 있다.

 

[코드3]

import Foundation
import CoreLocation


protocol Display{
    func draw()
}



/// 기본 도로 표시 클래스
class RoadDisplay : Display{
    func draw(){
        print("기본 도로 표시")
    }
}


/// 다양한 추가 기능에 대한 공통 클래스
class DisplayDecorator : Display{
    
    private var decoratedDisplay:Display?
    
    init(decoratedDisplay: Display? = nil) {
        self.decoratedDisplay = decoratedDisplay
    }
    
    func draw() {
        decoratedDisplay?.draw()
    }
}


//MARK: 차선표시
/// 차선 표시를 추가하는 클래스
class LaneDecorator: DisplayDecorator{
    
    //기존 표시 클래스 설정
    override init(decoratedDisplay: Display? = nil) {
        super.init(decoratedDisplay: decoratedDisplay)
    }
    
    
    override func draw() {
        // 설정된 기존 표시 기능을 수행
        super.draw()
        
        // 추가적으로 차선을 표시
        drawLane()
    }
    
    /// 추가적으로 차선 표시
    private func drawLane(){
        print("\t차선 표시")
    }
}



//MARK: 교통량 표시
/// 교통량 표시를 추가하는 클래스
class TrafficDecorator: DisplayDecorator{
    
    
    override init(decoratedDisplay: Display? = nil) {
        //기존 표시 클래스의 설정
        super.init(decoratedDisplay: decoratedDisplay)
    }
    
    
    override func draw() {
        // 설정된 기존 표시 기능을 수행
        super.draw()
        
        // 추가적으로 교통량을 표시
        drawTraffic()
    }
    
    /// 추가적으로 교통량 표시
    private func drawTraffic(){
        print("\t교통량 표시")
    }
    
}

let road = RoadDisplay()
road.draw() // 기본 도로만 표시
/*
 [출력]
 기본 도로 표시
 */


print("")

// 기본 도로 표시 + 차선 표시
let roadWithLane = LaneDecorator(decoratedDisplay: RoadDisplay())
roadWithLane.draw()
/*
 [출력]
 기본 도로 표시
     차선 표시
 */

print("")

// 기본 도로 표시 + 교통량 표시
let roadWithTraffic = TrafficDecorator(decoratedDisplay: RoadDisplay())
roadWithTraffic.draw()

/*
 [출력]
 기본 도로 표시
     교통량 표시
 */




print("")


/// 가장 먼저 생성된 RoadDisplay 객체의 draw 메서드가 실행된고,
/// 첫번째 추가 기능인 LaneDecorator 클래스의 drawLane 메서드가 실행되고,
/// 두번째 추가 기능은 TrafficDecorator 클래스의 drawTraffic 메서드가 실행된다.
let roadWithLaneAndTraffic = TrafficDecorator(decoratedDisplay: LaneDecorator(decoratedDisplay: RoadDisplay()))
roadWithLaneAndTraffic.draw()


/*
 [출력]
 기본 도로 표시
     차선 표시
     교통량 표시
 */

 

주목할 점은 road, roadWithLane, roadWithTraffic 객체의 접근이 모두 Display 클래스를 통해 이루어 진다는 것이다.

 

즉, 기본 도로 표시 (road) , 기본 도로 표시에 차선 표시 추가(roadWithLane), 기본 도로 표시에 교통량 표시 추가 (roadWithTraffic)에 관계 없이 Client 클래스는 동일한 Display 클래스만을 통해 일관성 있는 방식으로 도로 정보를 표시할 수 있다.

 

roadWithLane 객체에 draw 메서드가 호출되면 먼저 RoadDisplay 객체의 draw 메서드를 호출한다. 

 

그리고 LaneDecorator 객체의 drawLane 메서드를 호출한다.

 

 

roadWithLane 객체의 draw 메서드는 먼저 RoadDisplay 클래스의 draw 메서드를 호출해야 한다.

 

이는 LaneDecorator 클래서의 상위 클래스인 DisplayDecorator 클래서의 draw 메서드를 호출해 DisplayDecorator 클래스를 decoratedDisplay 객체를 호출함으로써 실행된다.

 

그런 다음 LaneDecorator 클래스의 drawLane 메서드를 호출하면 추가 기능인 차선 표시를 제공할 수 있다.

 

이런 방식의 설계를 이용하면 추가 기능 조합별로 별도의 클래스를 구현하는 대신 각 추가 기능에 해당하는 클래스의 객체를 조합해 추가 기능의 조합을 구현할 수 가 있다.

 

 

✅ 데커레이터 패턴 정리

데커레이터 패턴은 기본 기능에 추가할 수 있는 기능의 종류가 많은 경우에 각 추가 기능을 Decorator 클래스로 정의한 후 필요한 Decorator 객체를 조합함으로써 추가 기능의 조합을 설계하는 방식이다.

 

예를 들어 기본 도로 표시 기능에 차선 표시, 교통량 표시, 교차로 표시, 단속 카메라 표시의 4가지 추가 기능이 있을 때 추가 기능의 모든 조합은 15가지가 된다.

 

데커레이터 패턴을 사용하면 개별 추가 기능에 해당하는 Decorator 클래스 4개만 구현하고 개별 추가 기능을 객체의 형태로 조합함으로써 추가 기능의 조합을 구현할 수 있다.

 

또한 프로그램을 실행하는 중에도 Decorator 객체의 조합이 가능하므로 필요한 추가 기능의 조합을 동적으로 생성하는 것도 가능하다.

 

✅ 실행결과

 

keyPoint 데커레이터 패턴은 기본 기능에 추가할 수 있는 많은 종류의 부가 기능에서 파생되는 다양한 조합을 동적으로 구현할 수 있는 패턴이다.

 

 

Component: 기본 기능을 뜻하는 ConcreteComponent와 추가 기능을 뜻하는 Decorator의 공통 기능을 정의 한다.

즉, 클라이언트는 Component를 통해 실제 객체를 사용한다.

 

ConcreteComponent : 기본 기능을 구현하는 클래스다.

 

Decorator: 많은 수가 존재하는 구체적인 Decorator의 공통 기능을 제공한다.

 

ConcrereDecoratorA, ConcrereDecoratorB ,ConcrereDecoratorC : Decorator의 하위 클래스로 기본 기능에 추가 되는 개별적인 기능을 뜻한다.

 

 

 

ConcreteComponet로 c가 정의되어 있고, ConcreteDecoratorA의 객체 a와 이에 대한 데커레이터로 ConcreteDecoratorB의 객체 b가 있다고 가정한다. 

 

즉, 다음과 같이 객체가 생성된 경우에 해당한다.

 

 

 

 

Client가 객체 b의 operation 메서드를 호출하면 객체 b가 가리키는 Component, 즉 ConcreteDecoratorA 객체 a 의 operation 메서드를 호출한다.

 

객체 a 역시 자신이 가리키는 Component, 즉, ConcreteComponent 객체 c의 operation 메서드를 호추한 후 자신의 addedBehavior 메서드를 호출한다.

 

객체 b 역시 객체 a의 operation 메서드를 호출한 후 자신의 addedBehavior 메서드를 호출 한다.

 

이와 같은 순서는 addedBehavior 메서드가 자신의 Component 동작 후에 호출되는 경우를 보여준다.

 

데커레이터 패턴에서는 자신의 addedBehavior 메서드를 먼저 호출한 후 Component의 operation 메서드를 호출하는 방식으로도 구현할 수 있다. 

 

test3.zip
0.02MB

 

 

 

참고 : JAVA 객체지향 디자인 패턴