이름에서 유추할 수 있듯이, 데코레이터 패턴은 특정 객체에 추가적인 기능을 덧붙여서 꾸며주는 기능입니다.
데코레이터 패턴을 사용하면 유연하면서 깔끔한 구조로 하나의 추상적인 동작에 대한 구체 기능들을 조합할 수 있습니다.
저는 디자인 패턴들의 정의를 볼 때 다이어그램과 글로 된 정의들을 보면 이해가 잘 안 가는 경우가 많았습니다.
따라서 정의를 보기전에 먼저 예제를 통해서 데코레이터 패턴이 무엇이고 어떤 특징을 가지고 있는지에 대해 알아보겠습니다.
단순 구현하기
특정 기능을 수행하던 중 에러가 발생했을 때 에러를 기록하는 ErrorLogger 객체가 있습니다.
이 객체는 에러가 발생하면 에러를 저장하여 나중에 꺼내서 확인해 볼 수 있도록 하는 역할을 하고 있습니다.
class ErrorLogger {
func logError(_ error: Error) {
// 에러를 로컬 스토리지에 로깅합니다.
logToFile(error)
}
}
이 상황에서 배포된 버전이 아닌 개발 환경일 경우에는 디버깅을 위해 에러를 콘솔에 출력하는 기능을 추가하고자 합니다.
우선 단순하게 기존 객체에 해당 기능을 추가해 보겠습니다.
class ErrorLogger {
func logError(_ error: Error) {
// 에러를 로컬 스토리지에 로깅합니다.
logToFile(error)
#if DEBUG
// 에러 정보를 콘솔에 출력합니다.
logToConsole(error)
#endif
}
}
벌써부터 handleError 메서드가 복잡해지기 시작합니다.
여기서 특정 조건에서는 에러를 서버에 저장하게 기능이 추가되는 등과 같이,
점점 handleError로 해야 하는 기능과 조건이 늘어난다면 어떻게 해야 할까요?
인터페이스 분리해 보기
먼저, 인터페이스와 구현체를 분리해서 구현체를 특정 조건에 맞게 동적으로 변경할 수 있도록 해보겠습니다.
protocol ErrorLogger {
func logError(_ error: Error)
}
class LocalStorageErrorLogger: ErrorLogger {
func logError(_ error: Error) {
logToFile(error)
}
}
class LocalStorageAndConsoleErrorLogger: ErrorLogger {
func logError(_ error: Error) {
logToFile(error)
logToConsole(error)
}
}
// 호출부 코드
#if DEBUG
logger = LocalStorageAndConsoleErrorLogger()
#else
logger = LocalStorageErrorLogger()
#endif
코드가 분리된 것 같아 보이지만, 여전히 logToFile 로직이 중복되고 있고,
새로운 로직이 추가된다면 다음과 같이 중복된 로직이 또 반복될 수 있습니다.
class ServerAndStorageErrorLogger: ErrorLogger {
func logError(_ error: Error) {
logToFile(error)
logToServer(error)
}
}
class ServerAndStoragetAndConsoleErrorLogger: ErrorLogger {
func logError(_ error: Error) { ... 이하 생략 }
}
이렇게 로직이 추가될 때마다 로직이 중복되고 또 조합마다 추가될 클래스가 늘어난다면 관리가 점점 힘들어집니다.
이럴 때 Decorator Pattern을 사용하면 깔끔하게 구조를 풀어낼 수 있습니다.
Decorator Pattern 사용하기
위의 상황을 데코레이터로 풀어내면 다음과 같습니다.
class LocalStorageErrorLogger: ErrorLogger {
func logError(_ error: Error) {
logToFile(error)
}
}
먼저, ErrorLogger 인터페이스와 베이스 구현체가 되는 LocalStorageErrorLogger는 기존과 같습니다.
여기서 조건에 따라 추가될 수 있는 기능들을 데코레이터로 구현해 주겠습니다.
class ConsoleErrorLogger: ErrorLogger {
// 데코레이터는 다음과 같이 동일한 인터페이스 객체를 받아 그 객체의 기능을 확장합니다.
let decoratee: ErrorLogger
func logError(_ error: Error) {
self.decoratee.logError(error)
logToConsole(error)
}
}
class ServerErrorLogger: ErrorLogger {
let decoratee: ErrorLogger
func logError(_ error: Error) {
self.decoratee.logError(error)
logToServer(error)
}
}
ConsoleErrorLogger와 ServerErrorLogger가 데코레이터로 구현되었습니다.
데코레이터는 꾸며줄 객체를 추상 타입으로 받아서 그 객체의 기능에 자신의 기능을 추가해 주는 객체입니다.
꾸며줄 객체를 추상 타입으로 받기 때문에 해당 인터페이스만 상속하고 있다면 어떤 객체든 확장할 수 있고,
심지어 데코레이터 자신도 해당 인터페이스를 상속하고 있기 때문에 데코레이터끼리도 확장이 가능합니다.
데코레이터를 사용하는 호출부 코드도 보겠습니다.
let base: ErrorLogger = LocalStorageErrorLogger()
if isServerAvailable {
if isDebug {
logger = ServerErrorLogger(ConsoleErrorLogger(base)))
} else {
logger = ServerErrorLogger(base)
}
} else {
if isDebug {
logger = ConsoleErrorLogger(base)
} else {
logger = base
}
}
이제 복잡한 조건들에 맞게 ErrorLogger들을 조합해 주면 됩니다.
데코레이터에서 ErrorLogger 인터페이스의 구현체를 동적으로 주입받아 기능을 추가해 주기 때문에,
인터페이스 분리해 보기 에서 나타났던 조합마다 클래스를 정의해줘야 하고, 로직이 중복되는 문제를 벗어날 수 있습니다.
정리
앞서 예제로 보신 것처럼 데코레이터 패턴은 동적이고 분리된 구조로 여러 기능을 덧붙일 수 있습니다.
이제는 데코레이터 패턴의 정의 다이어그램이 이해가 되실 것입니다.
다시 한번 설명하자면
데코레이터 패턴의 구조는 특정 인터페이스를 꾸며주기 위해 인터페이스 객체를 받아서 기능을 확장하고,
자기 자신도 해당 인터페이스의 구현체가 되어 외부(호출부)에서는 변화 없이 기능 확장을 해줄 수 있는 방식입니다.
꼭 정확히 이 구조를 따를 필요는 없지만 이런 형태를 익혀 두신다면 나중에 복잡한 구조를 유연하게 풀어내도록 설계하거나
Decorator나 Composite 패턴을 사용한 다른 코드를 보실 때 도움이 될 것입니다.
'Tech' 카테고리의 다른 글
Swift Reflection/Mirror (0) | 2023.11.12 |
---|---|
Swift Macro 정리 (0) | 2023.10.29 |
iOS 개발자가 코틀린 코드를 볼 때 필요한 몇 가지 문법 (0) | 2023.09.19 |
Swift Dependencies (0) | 2023.08.26 |
StoreKit 기초 (0) | 2023.08.26 |