swift 디자인 패턴 - 템플릿 메서드 패턴

swift 디자인 패턴 - 템플릿 메서드 패턴

 

학습목표

공통 코드의 재사용 방법을 부분적으로 이해하기

 

 

여러회사의 모터 지원하기

 

현대모터를 이용해서 엘리베이터를 제어하는 시스템이라면 HyundaiMotor 클래스에 move 메서드를 정의할 수 있다.

 

HyundaiMotor 클래스는 move 메서드를 실행할 때 안전을 위해 문(Door 클래스)이 닫혀 있는지 조사 할 필요가 있다.

 

따라서 HyundaiMotor 클래스에서 Door 클래스로의 연관 관계를 정의했다.

 

또한 엘리베이터가 이동 중이면 모터를 구동시킬 필요가 없다.

 

MotorStatus, DoorStatus, Direction은 Enumeration 으로 각각 모터의 상태 (정지 중, 이동 중) , 문의상태 (닫힘상태, 열림상태) , 이동 방향(위, 아래)를 나타낸다.

 

HyundaiMotor 클래스의 move 메서드는 우선 getMotorStatus 메서드를 호출해 모터의 상태를 조회한다.

 

모터가 이미 동작 중이면 move 메서드의 실행을 종료한다. 

 

그리고 Door 클래스의 getDoorStatus 메서드를 호출해 문의 상태를 조회한다.

 

문이 열려 있는 상태면 Door 클래스의 close 메서드를 호출해 문을 닫는다.

 

그리고 moveHyundaiMotor 메서드를 호출해서 모터를 구동시킨다. 

 

마지막으로 setMotorStatus를 호출해 모터의 상태를 MOVING으로 기록한다.

 

[코드]

import Foundation
import CoreLocation

enum DoorStatus: String{
    case CLOSED
    case OPENED
}

enum MotorStatus: String{
    case MOVING
    case STOPPED
}

enum Direction: String{
    case UP
    case DOWN
}

class Door{
    /// 문의 상태
    private var doorStatus: DoorStatus = .CLOSED
    

    func getDoorStatus() -> DoorStatus{
        return doorStatus
    }
    
    func close(){
        doorStatus = .CLOSED
    }
    
    func open(){
        doorStatus = .OPENED
    }
    
}


class HyundaiMotor{
    
    var door: Door?
    var motorStatus:MotorStatus = .STOPPED  //초기에는 멈춘 상태
    
    init(door: Door? = nil) {
        self.door = door
    }
    
    func moveHyundaiMotor(direction:Direction) {
        //현대모터를 구동시킴
        print("현대모터를 구동시킴")
    }
    
    func getMotorStatus() -> MotorStatus{
        return motorStatus
    }
    
    func setMotorStatus(motorStatus:MotorStatus){
        self.motorStatus = motorStatus
    }
    
    func move(direction:Direction){
        
        // 이미 이동중이면 아무 작업하지 않기
        let motorStatus = getMotorStatus()
        if motorStatus == .MOVING { return }
        
        // 만약 문이 열려 있으면 우선 문을 닫음
        let doorStatus = door?.getDoorStatus()
        if doorStatus == .OPENED { door?.close() }
        
        /// 모터를 주어진 방향으로 이동 시킴.
        moveHyundaiMotor(direction: direction)
        
        /// 모터 상태를 이동 중으로 변경함.
        setMotorStatus(motorStatus: .MOVING)
    }

}


let door = Door()
let hyundaiMotor = HyundaiMotor(door: door)
hyundaiMotor.move(direction: .UP)

 

문제점

  • HyundaiMotor 클래스는 현대 모터를 구동시킨다. 만약 다른 회사의 모터를 제어해야 한다면? 예를 들어 LG 모터를 구동시키려면 어떻게 해야 할까?

 

LG 모터를 구동하는 것은 Hyundai 모터를 구동하는 것과 완전히 동일하지는 않다.(비슷한 부분이 많다.)

그러므로 현대 모터를 구동시키는 HyundaiMotor 클래스를 복사한 후 LG 모터에 한정된 부분을 수정할 필요가 있다.

 

[코드2]

import Foundation
import CoreLocation



import Foundation
import CoreLocation

enum DoorStatus: String{
    case CLOSED
    case OPENED
}

enum MotorStatus: String{
    case MOVING
    case STOPPED
}

enum Direction: String{
    case UP
    case DOWN
}

class Door{
    /// 문의 상태
    private var doorStatus: DoorStatus = .CLOSED
    

    func getDoorStatus() -> DoorStatus{
        return doorStatus
    }
    
    func close(){
        doorStatus = .CLOSED
    }
    
    func open(){
        doorStatus = .OPENED
    }
}


class LGMotor{
    
    var door: Door?
    var motorStatus:MotorStatus = .STOPPED  //초기에는 멈춘 상태
    
    init(door: Door? = nil) {
        self.door = door
    }
    
    func moveLGMotor(direction:Direction) {
        //엘지모터를 구동시킴
        print("엘지모터를 구동시킴")
    }
    
    func getMotorStatus() -> MotorStatus{
        return motorStatus
    }
    
    func setMotorStatus(motorStatus:MotorStatus){
        self.motorStatus = motorStatus
    }
    
    func move(direction:Direction){
        
        // 이미 이동중이면 아무 작업하지 않기
        let motorStatus = getMotorStatus()
        if motorStatus == .MOVING { return }
        
        // 만약 문이 열려 있으면 우선 문을 닫음
        let doorStatus = door?.getDoorStatus()
        if doorStatus == .OPENED { door?.close() }
        
        /// 모터를 주어진 방향으로 이동 시킴.
        moveLGMotor(direction: direction)
        
        /// 모터 상태를 이동 중으로 변경함.
        setMotorStatus(motorStatus: .MOVING)
    }

}


let door = Door()
let lgMotor = LGMotor(door: door)
lgMotor.move(direction: .UP)

 

LGMotor 클래스와 HyundaiMotor 클래스를 비교해보면 여러개의 메서드가 동일하게 구현되어 있음을 알 수 있다.

 

즉, 2개의 클래스는 많은 중복 코드를 가진다.

 

일반적으로 코드 중복은 유지보수성을 악화 시키므로 바람직하지 않다.

 

 

[!] 2개 이상의 클래스가 유사한 기능을 제공하면서 중복된 코드가 있는 경우에는 상속을 이용해서 코드 중복 문제를 피할 수 있다.

 

예)

HyundaiMotor 클래스와 LGMotor 클래스에서 공통의 상위 클래스로 Motor 클래스를 정의하고 Motor 클래스에 HyundaiMotor 클래스와 LGMotor 클래스에서 중복되는 코드를 Motor 클래스로 이동할 수 있다.

 

 

[코드 3]

import Foundation
import CoreLocation

//MARK: 문상태
enum DoorStatus: String{
    case CLOSED
    case OPENED
}
//MARK: 모터상태
enum MotorStatus: String{
    case MOVING
    case STOPPED
}
//MARK: 방향
enum Direction: String{
    case UP
    case DOWN
}

//MARK: 문
class Door{
    /// 문의 상태
    private var doorStatus: DoorStatus = .CLOSED
    

    func getDoorStatus() -> DoorStatus{
        return doorStatus
    }
    
    func close(){
        doorStatus = .CLOSED
    }
    
    func open(){
        doorStatus = .OPENED
    }
}

/// 슈퍼 클래스
class Motor{
    var door: Door?
    var motorStatus: MotorStatus = .STOPPED
    
    init(door: Door? = nil) {
        self.door = door
    }
    
    func getMotorStatus() -> MotorStatus{
        return motorStatus
    }
    
    func setMotorStatus(motorStatus:MotorStatus){
        self.motorStatus = motorStatus
    }
    
}


class HyundaiMotor:Motor{
   
    override init(door: Door? = nil) {
        super.init(door: door)
    }
    
    private func moveHyundaiMotor(direction:Direction){
        //현대 모터를 구동시킴
        print("현대 모터를 구동시킴")
    }
    
    /// LG 모터와 다른 부분
    func move(direction:Direction){
        
        // 이미 이동중이면 아무 작업하지 않기
        let motorStatus = getMotorStatus()
        if motorStatus == .MOVING { return }
        
        // 만약 문이 열려 있으면 우선 문을 닫음
        let doorStatus = door?.getDoorStatus()
        if doorStatus == .OPENED { door?.close() }
        
        /// 모터를 주어진 방향으로 이동 시킴.
        /// 이 구문을 제외하면 move 메서드는 LGMotor 와 동일함
        moveHyundaiMotor(direction: direction)
        
        /// 모터 상태를 이동 중으로 변경함.
        setMotorStatus(motorStatus: .MOVING)
    }
    
}


class LgMotor:Motor{
   
    override init(door: Door? = nil) {
        super.init(door: door)
    }
    
    private func moveLgMotor(direction:Direction){
        //Lg 모터를 구동시킴
        print("Lg 모터를 구동시킴")
    }
    
    
    /// 현대모터와 다른 부분
    func move(direction:Direction){
        
        // 이미 이동중이면 아무 작업하지 않기
        let motorStatus = getMotorStatus()
        if motorStatus == .MOVING { return }
        
        // 만약 문이 열려 있으면 우선 문을 닫음
        let doorStatus = door?.getDoorStatus()
        if doorStatus == .OPENED { door?.close() }
        
        /// 모터를 주어진 방향으로 이동 시킴.
        moveLgMotor(direction: direction)
        
        /// 모터 상태를 이동 중으로 변경함.
        setMotorStatus(motorStatus: .MOVING)
    }
    
}

 

Motor 클래스를 HyundaiMotor 클래스와 LgMotor 클래스의 상위 클래스로 정의함으로써 원래 2개의 클래스에 있었던 Door 클래스와의 연관 관계, motorStatus 필드, getMotorStatus와 setMotorStatus 메서드의 중복을 피할 수 있었다.

 

그러나 LGMotor 클래스의 move 메서드와 HyundaiMotor 클래스의 move 메서드를 비교해보면 대부분이 비슷하다.

 

여전히 코드 중복의 문제가 존재한다.

 

해결책

HyundaiMotor 클래스와 LGMotor 클래스의 move 메서드처럼 완전히 중복되지는 않지만 부분적으로 중복되는 경우에도 상속을 활용해 코드 중복을 피할 수 있다.

 

즉, move 메서드에서 moveHyundaiMotor 와 moveLGMotor 메서드를 호출하는 구문을 제외하면 두 클래스의 move 메서드는 동일하다.

 

그리고 moveHyundaiMotor 메서드와 moveLGMotor 메서드는 모터의 제조사에 따라 구현 방법은 다르지만 모터 구동을 실제로 구현한다는 기능 면에서는 동일하다.

 

이러한 경우에는 move 메서드를 상위 Motor 클래스로 이동시키고 다른 구문, 즉 moveHyundaiMotor 와 moveLGMotor 메서드의 호출 부분을 하위 클래스에서 오버라이드하는 방식으로 move 메서드의 중복을 최소화 할 수 있다.

moveMotor 메서드를 오버라이드 하면서 현대 모터를 구동하는 코드를 작성하고, LGMotor 클래스의 moveMotor 메서드는 LG 모터를 구동하는 코드를 작성한다.

 

[코드 4]

import Foundation
import CoreLocation

//MARK: 문상태
enum DoorStatus: String{
    case CLOSED
    case OPENED
}
//MARK: 모터상태
enum MotorStatus: String{
    case MOVING
    case STOPPED
}
//MARK: 방향
enum Direction: String{
    case UP
    case DOWN
}

//MARK: 문
class Door{
    /// 문의 상태
    private var doorStatus: DoorStatus = .CLOSED
    

    func getDoorStatus() -> DoorStatus{
        return doorStatus
    }
    
    func close(){
        doorStatus = .CLOSED
    }
    
    func open(){
        doorStatus = .OPENED
    }
}

/// 슈퍼 클래스
class Motor{
    var door: Door?
    var motorStatus: MotorStatus = .STOPPED
    
    init(door: Door? = nil) {
        self.door = door
    }
    
    func getMotorStatus() -> MotorStatus{
        return motorStatus
    }
    
    func setMotorStatus(motorStatus:MotorStatus){
        self.motorStatus = motorStatus
    }
    
    /// LG 모터와 Hyundai 모터에서 오버라이드 해서 사용함.
    func moveMotor(){}
    
    //LGMotor와 HyundaiMotor의 move 메서드에서 공통되는 부분만을 가짐
    final func move(direction:Direction){
        
        // 이미 이동중이면 아무 작업하지 않기
        let motorStatus = getMotorStatus()
        if motorStatus == .MOVING { return }
        
        // 만약 문이 열려 있으면 우선 문을 닫음
        let doorStatus = door?.getDoorStatus()
        if doorStatus == .OPENED { door?.close() }
        
        //[!!] 이 메서드는 HyundaiMotor와 LGMotor에서 특수화(오버라이드)됨
        moveMotor()
        
        /// 모터 상태를 이동 중으로 변경함.
        setMotorStatus(motorStatus: .MOVING)
    }
    
}


class HyundaiMotor:Motor{
   
    override init(door: Door? = nil) {
        super.init(door: door)
    }

    override func moveMotor(){
        print("현대 모터를 구동시킴")
    }
    
}


class LgMotor:Motor{
   
    override init(door: Door? = nil) {
        super.init(door: door)
    }
    
    override func moveMotor(){
        print("Lg 모터를 구동시킴")
    }
    
}

 

 

템플릿 메서드 패턴

템플릿 메서드 패턴은 전체적으로는 동일하면서 부분적으로는 다른 구문으로 구성된 메서드의 코드 중복을 최소화할 때 유용하다. 다른 관점에서 보면 동일한 기능을 상위 클래스에서 정의하면서 확장/변화가 필요한 부분만 서브 클래스에서 구현할 수 있도록 한다.

 

위의 예제 처럼 Motor 클래스의 move 메서드는 HyundaiMotor 와 LGMotor에서 동일한 기능을 구현하면서 각 하위 클래스에서 구체적으로 정의할 필요가 있는 부분, 즉, moveMotor 메서드 부분만 각 하위 클래스에서 오버라이드 되도록 한다.

 

이러한 경우 Motor 클래스의 move 메서드를 템플릿 메서드라고 부르고, move 메서드에서 호출되면서 하위 클래스에서 오버라이드될 필요가 있는 moveMotor 메서드를 primitive 또는 hook 메서드라 부른다.

 

 

Key point 템플릿 메서드 패턴은 전체적인 알고리즘은 상위 클래스에서 구현하면서 다른 부분은 하위 클래스에서 구현할 수 있도록 하는 디자인 패턴이다. 

전체적인 알고리즘 코드를 재사용하는데 유용하다.

 

 

템플릿 메서드 패턴에서 나타나는 역할이 수행하는 작업은 아래와 같다.

 

AbstractClass : 템플릿 메서드를 정의하는 클래스. 하위 클래스에 공통 알고리즘을 정의하고 하위 클래스에서 구현될 기능을 primitive 메서드 또는 hook 메서드로 정의하는 클래스다.

 

ContreteClass : 물려받은 primitive 메서드나 hook 메서드를 구현하는 클래스. 상위 클래스에서 구현된 템플릿 메서드의 일반적인 알고리즘에서 하위 클래스에 적합하게 primitive 메서드나 hook 메서드를 오버라이드 하는 클래스다.

 

 

템플릿 메서드 패턴을 모터 예제에 적용한 경우

 

 

 

Motor 클래스는 AbstractClass 역할을 한다.

 

HyundaiMotor 클래스와 LGMotor 클래스는 각각 ConcreteClass 역할을 한다.

 

Motor 클래스의 move 메서드는 템플릿 메서드,  moveMotor 메서드는 primitive 메서드에 해당된다.

 

test4.zip
0.02MB

 

 

 

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