[Swift 디자인 패턴] Chain of Responsibility Pattern (책임 연쇄) - 디자인 패턴 공부 14
안녕하세요 Pingu입니다.🐧
지난 글에서는 Behavioral Patterhn(행동 패턴)에 대해 간단하게 알아봤는데요, 이번 글에서부터는 행동 패턴의 여러 가지 패턴을 하나씩 자세히 알아보도록 하겠습니다. 이번 글에서 알아볼 행동 패턴은 Chain Responsibility Pattern(책임 연쇄)입니다.
책임 연쇄 패턴이란?
Chain Responsibility Pattern은 Handler chain에 따라 요청을 전달 할전달할 수 있는 디자인 패턴입니다. 요청을 수신한 각 핸들러는 요청을 처리하거나 다음 핸들러로 전달할지 결정하게 됩니다.
즉 여러 개의 핸들러가 연결되어있고, 이렇게 연결된 핸들러들을 Chain이라고 합니다. 어떤 요청이 Chain에 들어왔을 때 이를 처리 할 수도 있고 다음 핸들러로 넘길 수도 있는 것이죠.
- Handler
- 클라이언트의 요청을 처리하기 위한 인터페이스를 정의합니다.
- 현재 핸들러에서 처리하지 않을것이라면 요청을 넘길 다음 핸들러를 구현합니다. (처리할 수 없는 요청인 경우 마지막 핸들러까지 가서도 처리되지 않을 수 있습니다. 마지막 핸들러는 다음 핸들러를 갖지 않을 것이므로 이는 선택적으로 만드시면 됩니다.)
- Concrete Handler
- 요청을 처리합니다.
- 다음 핸들러에 작업을 넘깁니다.
- Client
- Chain의 첫 번째 Concrete Handler 객체에 요청을 합니다. (이를 요청하기 위해 클라이언트가 첫 번째 Concrete Handler에 접근할 수 있어야 합니다.)
- 클라이언트의 요청은 Chain에 존재하는 모든 핸들러로 전송될 수 있습니다.
책임 연쇄 패턴은 언제 쓰나요?
유사한 이벤트를 처리하지만 이벤트의 타입, 속성, 기타 항목에 따라 달라지는 객체 그룹이 있을 때 책임 연쇄 패턴을 사용하면 좋습니다.
실생활에서 발견할 수 있는 책임 연쇄 패턴의 예로는 전화 고객 서비스가 있습니다. 예를 들어 어떤 가전제품 회사의 컴퓨터를 사용 중인데, 컴퓨터가 고장이 난 거예요. 그래서 그 회사의 고객 서비스에 전화를 합니다. 그 회사는 세탁기만 만드는 회사가 아니므로 고객에게 어떤 제품을 사용 중이냐고 고르라고 합니다. 고객은 컴퓨터가 고장 났다고 선택합니다. 그러자 고객 서비스에서는 컴퓨터에서도 자주 발생하는 문제를 나열한 뒤 그중에 있다면 골라달라고 합니다. 거기서 하나를 고르자 고객은 드디어 해당 내용을 담당하는 직원과 통화를 할 수 있게 되었습니다.
이렇게 어떤 일을 처리하기 위해 해당 업무 담당자를 찾는 방금의 예처럼, 클라이언트가 요청을 주면 핸들러 체인에 존재하는 객체들 중 해당 요청을 처리하는 객체가 나올 때까지 요청을 넘기는 방법이 책임 연쇄 패턴이라고 할 수 있습니다.
책임 연쇄 패턴의 결과
장점
- 요청 처리 순서를 제어할 수 있습니다.
- 결합도를 감소해줍니다. 체인에 존재하는 객체들은 요청을 처리할 것인지 넘길 것인지만 판단하면 되고, 체인의 다른 객체들이 뭘 하는지 알 필요가 없습니다. 물론 이건 클라이언트도 몰라도 됩니다.
- Single Responsibility Principle(단일 책임 원칙)을 지킬 수 있습니다. 작업을 수행하는 클래스, 작업을 호출하는 클래스를 분리할 수 있습니다.
- Open, Closed Principle(개방/폐쇄 원칙)을 지킬 수 있습니다. 기존 클라이언트의 코드를 바꾸지 않고 새로운 핸들러를 앱에 추가할 수 있습니다.
단점
- 처리되지 않는 요청이 있을 수 있지만, 요청할 때는 이걸 알 수 없습니다. 체인의 끝까지 가야 알 수 있기 때문이죠.
- 체인을 잘 못 만들 경우 사이클이 발생할 수 있습니다.
예제
그럼 이제 책임 연쇄 패턴을 Swift 코드로 구현해보겠습니다.
아까 예로 들었던 고객 서비스로 구현해볼게요.
일단 가장 먼저 Handler 프로토콜을 정의합니다.
// Handler
protocol CustomerServiceHandler: class {
var nextHandler: CustomerServiceHandler? { get set }
func setNext(handler: CustomerServiceHandler)
func handle(request: String) -> String
}
extension CustomerServiceHandler {
func setNext(handler: CustomerServiceHandler) {
if self.nextHandler == nil {
self.nextHandler = handler
} else {
self.nextHandler?.setNext(handler: handler)
}
}
}
그런 뒤 여러 가지 request에 따른 작업을 처리할 수 있는 Concrete Handler들을 만들어줍니다.
// Concrete Handler
class MainAppleServiceHandler: CustomerServiceHandler {
var nextHandler: CustomerServiceHandler?
func handle(request: String) -> String {
if request == "Apple" {
return "Apple 부서로 연결합니다."
} else {
if let response = nextHandler?.handle(request: request) {
return response
} else {
return "원하시는 서비스는 제공하지 않습니다."
}
}
}
}
// Concrete Handler
class MobileServiceHandler: CustomerServiceHandler {
var nextHandler: CustomerServiceHandler?
func handle(request: String) -> String {
if request == "Mobile" {
return "Mobile 부서로 연결합니다."
} else {
if let response = nextHandler?.handle(request: request) {
return response
} else {
return "원하시는 서비스는 제공하지 않습니다."
}
}
}
}
// Concrete Handler
class IPhoneServiceHandler: CustomerServiceHandler {
var nextHandler: CustomerServiceHandler?
func handle(request: String) -> String {
if request == "iPhone" {
return "iPhone 부서로 연결합니다."
} else {
if let response = nextHandler?.handle(request: request) {
return response
} else {
return "원하시는 서비스는 제공하지 않습니다."
}
}
}
}
이렇게 여러 개의 Handler 객체를 만들었다면 이젠 Chain 가지고 있을 Client를 만들어줘야 합니다.
// Client
class Client {
private var firstHandler: CustomerServiceHandler?
func request(request: String) -> String {
return self.firstHandler?.handle(request: request) ?? "firstHandler를 설정해주세요"
}
func addHandler(handler: CustomerServiceHandler) {
if let firstHandler = firstHandler {
firstHandler.setNext(handler: handler)
} else {
self.firstHandler = handler
}
}
}
위와 같이 Client는 Chain의 첫 번째 Handler만 가지고 있으면 됩니다.
이제 위의 코드를 간단하게 한 번 사용해보겠습니다.
위와 같이 결과가 나오게 됩니다.
근데 iPad 서비스도 추가하고 싶다면 어떻게 하면 될까요?
// Concrete Handler
class IPadeServiceHandler: CustomerServiceHandler {
var nextHandler: CustomerServiceHandler?
func handle(request: String) -> String {
if request == "iPad" {
return "iPad 부서로 연결합니다."
} else {
if let response = nextHandler?.handle(request: request) {
return response
} else {
return "원하시는 서비스는 제공하지 않습니다."
}
}
}
}
간단하게 iPad Handler 클래스를 만들어 준 뒤 Client에 추가해주면 됩니다.
공부를 하며 한 가지 들었던 의문은 하나의 핸들러 클래스가 여러 개의 next Handler를 가지면 책임 연쇄 패턴이 아닌 건지 였습니다. 지금은 Linked List의 느낌으로 요청을 처리하는데, 뭔가 Tree 느낌으로 요청을 처리할 수도 있지 않을까가 궁금했습니다. 이러한 구현은 Handler 프로토콜을 채택한 객체를 배열로 처리하면 가능할 거 같습니다.
또한 저는 Chain에 Handler를 추가할 때 항상 체인의 마지막 부분에 추가해줬는데, 이렇게 추가하는 게 맞는지도 의문이었습니다. 특정 Handler 객체에 setNext()로 다음 핸들러만 설정하도록 처음엔 구현했는데, 그렇게 하면 의도하지 않게 체인에 존재하는 핸들러를 잃어버릴 수도 있고 체인을 구성하는 게 상당히 귀찮아져서 항상 마지막 부분에 추가해줬습니다.
음.. 뭔가 직접 구현을 해보며 생각할 것이 많았던 패턴이었던 거 같습니다.
혹시라도 틀린 부분이 있다면 알려주시면 감사하겠습니다 ㅠ
전체 코드는 여기에서 볼 수 있습니다.
감사합니다.