오늘부터는 두 번째 카테고리인 Structural Patterns에 해당하는 패턴을 배워보고자 합니다.
Structural Patterns 첫 번째는 Adapter Patterns입니다.
Wrapper로도 알려져있습니다.
내용이 어렵지 않고, 알게 모르게 많이 자주 사용하고 있을 패턴이라고 생각됩니다.
1. 목적
Adapter는 호환되지 않는 인터페이스 객체가 다른 객체와 상호작용할 수 있도록 하는 패턴입니다.
출처: https://refactoring.guru/design-patterns/adapter
2. 문제 상황

XML 주식 데이터를 활용해서 Application을 제작합니다.
하지만 Analytics Library
는 JSON 형식의 데이터를 사용하기 때문에 있는 주식 데이터를 그대로 활용할 수 없습니다.
사실 Analytics Library
를 수정하여 XML 데이터를 사용하도록 할 수도 있습니다.
그렇지만 타사의 Library 코드에 접근하는 것은 보통 불가능하거나, 큰 수정 비용이 발생합니다.
또한 해당 Library가 또 다른 Library에 의존하고 있는 경우에도 수정이 쉽지 않습니다.
3. 해결책
이러한 경우 Adapter를 만들어 해결합니다.
Adapter는 어떤 객체의 인터페이스를 다른 객체가 이해할 수 있도록 변환하는 객체입니다.
그러기 위해 특정 객체를 Wrapping 하여 만들게 됩니다.

4. 구조
두 가지 방식의 구조가 존재합니다. 문제 상황의 Application을 예시로 구조를 살펴봅시다.
사용할 XML 데이터입니다.
let xmlData = """
<stocks>
<stock>
<symbol>AAPL</symbol>
<price>150.0</price>
<volume>1000</volume>
</stock>
<stock>
<symbol>GOOGL</symbol>
<price>2800.5</price>
<volume>500</volume>
</stock>
<stock>
<symbol>AMZN</symbol>
<price>3300.75</price>
<volume>300</volume>
</stock>
</stocks>
"""
Stock
클래스와 XML Parser
도 준비합니다.
내부 로직이 궁금하신 분들은 확인하시기 바랍니다.
Stock Class
// Stock 클래스
class Stock: Codable {
let symbol: String
let price: Double
let volume: Int
init(symbol: String, price: Double, volume: Int) {
self.symbol = symbol
self.price = price
self.volume = volume
}
}
XML Parser
final class StockXMLParser: NSObject, XMLParserDelegate {
// Singleton 패턴 설계
static private let shared: StockXMLParser = StockXMLParser()
static public func parse(data: String) -> [Stock] {
var parser: XMLParser? = XMLParser(data: data.data(using: .utf8) ?? .init() )
parser?.delegate = shared
shared.initData()
parser?.parse()
parser?.delegate = nil
parser = nil
return shared.getStocks()
}
private override init() {
}
private var elementName: String?
private var itemList: [Stock] = []
private var currentValue = (symbol: "", price: 0.0, volume: 0)
func parser(
_ parser: XMLParser,
didStartElement elementName: String,
namespaceURI: String?,
qualifiedName qName: String?,
attributes attributeDict: [String : String] = [:]
) {
if elementName == "stock" {
self.currentValue = (symbol: "", price: 0.0, volume: 0)
}
self.elementName = elementName
}
func parser(
_ parser: XMLParser,
didEndElement elementName: String,
namespaceURI: String?,
qualifiedName qName: String?) {
if elementName == "stock" {
let stock = Stock(
symbol: currentValue.symbol,
price: currentValue.price,
volume: currentValue.volume
)
itemList.append(stock)
self.currentValue = (symbol: "", price: 0.0, volume: 0)
}
}
func parser(_ parser: XMLParser, foundCharacters string: String) {
if string.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { return }
switch elementName {
case "symbol":
currentValue.symbol = string
case "price":
currentValue.price = Double(string) ?? 0.0
case "volume":
currentValue.volume = Int(string) ?? 0
default:
break
}
}
public func initData() {
self.elementName = nil
self.itemList = []
self.currentValue = (symbol: "", price: 0.0, volume: 0)
}
public func getStocks() -> [Stock] {
self.itemList
}
}
Object Adapter

상호작용 시키고자 하는 두 객체에 대한 Adapter를 설계하게 됩니다.
이때 Adapter는 기존 클래스의 인스턴스를 포함하게 됩니다.
// Client 인터페이스
protocol Client {
func analyticsXMLData(from xmlString: String) throws -> Double
}
// 적응 대상 클래스 (Service)
enum AnalyticsError: Error {
case INVAILD_JSON_STRING
case DECODE_ERROR
}
class Analytics {
func analyticsJSONData(from jsonString: String) throws -> Double {
guard let jsonData = jsonString.data(using: .utf8) else {
throw AnalyticsError.INVAILD_JSON_STRING
}
guard let stocks = try? JSONDecoder().decode([Stock].self, from: jsonData) else {
throw AnalyticsError.DECODE_ERROR
}
return stocks.reduce(0.0) { $0 + ($1.price * Double($1.volume)) }
}
}
// Service 메서드를 Client 인터페이스에 맞게 변환
enum AdapterError: Error {
case ENCODE_ERROR
case INVAILD_JSON_DATA
}
class ObjectAdapter: Client {
// Adapter가 Analytics(Service)를 포함하는 구조
private let analytics: Analytics
// Dependency Injection
init(analytics: Analytics) {
self.analytics = analytics
}
public func analyticsXMLData(from xmlString: String) throws -> Double {
let stocks = StockXMLParser.parse(data: xmlString)
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
guard let jsonData = try? encoder.encode(stocks) else {
throw AdapterError.ENCODE_ERROR
}
guard let jsonString = String(data: jsonData, encoding: .utf8) else {
throw AdapterError.INVAILD_JSON_DATA
}
return try analytics.analyticsJSONData(from: jsonString)
}
}
// Client 사용 예시
let analytics = Analytics()
let adapter = ObjectAdapter(analytics: analytics)
do {
let priceSum = try adapter.analyticsXMLData(from: xmlData)
print(priceSum)
} catch(let e) {
// Error
}
이처럼 Service가 Adapter에 포함되어 Client
에서는 Analytics
이 JSON으로 데이터를 처리한다는 사실을 모르더라도 Analytics의 기능을 사용할 수 있습니다.
Class Adapter

이 방식은 기본적으로 다중 상속을 이용합니다. Client의 기존 Class와 Service를 상속받습니다.
하지만 Swift는 다중 상속을 지원하지 않습니다. 따라서 Protocol 채택과 Class 상속으로 비슷하게 구현해 보도록 하겠습니다.
// Client 인터페이스
protocol Client {
func analyticsXMLData(from xmlString: String) throws -> Double
}
// 적응 대상 클래스 (Service)
enum AnalyticsError: Error {
case INVAILD_JSON_STRING
case DECODE_ERROR
}
class Analytics {
func analyticsJSONData(from jsonString: String) throws -> Double {
guard let jsonData = jsonString.data(using: .utf8) else {
throw AnalyticsError.INVAILD_JSON_STRING
}
guard let stocks = try? JSONDecoder().decode([Stock].self, from: jsonData) else {
throw AnalyticsError.DECODE_ERROR
}
return stocks.reduce(0.0) { $0 + ($1.price * Double($1.volume)) }
}
}
// Service 메서드를 Client 인터페이스에 맞게 변환
enum AdapterError: Error {
case ENCODE_ERROR
case INVAILD_JSON_DATA
}
// Analytics(Service) Class 상속과 Client Protocol 채택
class ClassAdapter: Analytics, Client {
func analyticsXMLData(from xmlString: String) throws -> Double {
let stocks = StockXMLParser.parse(data: xmlString)
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
guard let jsonData = try? encoder.encode(stocks) else {
throw AdapterError.ENCODE_ERROR
}
guard let jsonString = String(data: jsonData, encoding: .utf8) else {
throw AdapterError.INVAILD_JSON_DATA
}
// Analytics(Service)로부터 상속받은 Method 호출
return try analyticsJSONData(from: jsonString)
}
}
// Client 사용 예시
let adapter = ClassAdapter()
do {
let priceSum = try adapter.analyticsXMLData(from: xmlData)
print(priceSum)
} catch(let e) {
// Error
}
Object Adapter
와 Client
, Analytics
코드는 동일하지만 Adapter의 코드가 달라진 것을 볼 수 있습니다.
또한 Object Adapter
에서 발생했던 의존성 주입 또한 사라졌습니다.
무엇을 선택해야 하는가?
보통은 사용하는 언어의 특성을 따라갑니다.
Swift는 다중 상속을 지원하지 않기 때문에 주로 Object Adapter
방식이 사용됩니다.
또한 의존성 주입이 발생하기 때문에 Adapter, Service를 별도로 테스트할 수 있습니다.
5. 장단점
SOILD 원칙 중 SRP(Single Responsibility Principle, 단일 책임 원칙)과 OCP(Open/Closed Principle, 개방/폐쇄 원칙)이 준수됩니다.
하지만 여러 인터페이스(프로토콜)와 클래스(Adapter)가 생성될 수 있습니다.
때문에 복잡성이 너무 증가하지 않는 방향으로 설계할 필요가 있습니다.
6. 실제 사용 사례
iOS 앱 개발에서 사용될 수 있는 혹은 사용되고 있는 Adapter 패턴을 살펴봅시다.
1. UIViewRepresentable
Apple에서 제공하는 Adapter 패턴이라고 볼 수 있습니다.
UIKit의 UIView
를 SwiftUI의 View
로 사용하기 위해 UIViewRepresentable
을 사용합니다.
import SwiftUI
import UIKit
// UIKit UIView
class CustomView: UIView {
func updateUI() {
//Code
}
}
// Adapter: UIView를 SwiftUI의 View로 변환
struct CustomViewAdapter: UIViewRepresentable {
func makeUIView(context: Context) -> CustomView {
return CustomView()
}
func updateUIView(_ uiView: CustomView, context: Context) {
uiView.updateUI()
}
}
// SwiftUI에서 UIView 사용
struct ContentView: View {
var body: some View {
VStack {
Text("CustomView in SwiftUI")
CustomViewAdapter()
.frame(height: 300)
}
}
}
2. LoginAdapter
아래의 코드처럼 특정 인터페이스(Apple Login)를 Wrapping 해서 Adapter를 생성할 수 있습니다.
// 기존 로그인 시스템의 인터페이스
protocol LoginService {
func login()
}
// Apple 로그인 시스템 (서드파티 API)
class AppleLogin {
func performAppleLogin(completion: (Bool) -> Void) {
print("Logging in with Apple...")
completion(true)
}
}
// Adapter 클래스: AppleLogin을 기존 LoginService와 연결
class AppleAdapter: LoginService {
private let appleLogin = AppleLogin()
func login() {
appleLogin.performAppleLogin { success in
if success {
print("Apple login successful.")
} else {
print("Apple login failed.")
}
}
}
}
// 사용 예시
let loginService: LoginService = AppleAdapter()
loginService.login()
하지만 실제로 로그인 시스템을 개발하다 보면 다양한 서드파티 로그인 API를 마주하게 됩니다.
만약 AppleAdapter
가 아닌 LoginAdapter
에서 Naver, Kakao, Facebook 등 더 다양한 로그인 서비스들을 Wrapping 하고 있다면 어떨까요?
결론부터 얘기하자면,
가능합니다. 하지만 Adapter 패턴이 아닙니다.
하나의 클래스에서 여러 개의 로그인 API의 로직을 담당하는 경우, Facade 패턴으로 판단하는 것이 더 적절합니다.
Adapter 패턴은 일반적으로 하나의 인터페이스를 다른 인터페이스로 변환하여 기존 시스템에서 쉽게 사용할 수 있도록 하는데 더 초점이 맞춰져 있습니다.
Facade 패턴도 추후 알아보도록 합시다.
Conclusion
Adapter 패턴은 Structural Patterns의 첫 번째 키워드였습니다.
외부 라이브러리를 사용하는 경우 자주 사용되고 있는 패턴인 만큼 개념적으로 이해하고 있는 것이 중요하다고 생각합니다.
Reference
Adapter
/ Design Patterns / Structural Patterns Adapter Also known as: Wrapper Intent Adapter is a structural design pattern that allows objects with incompatible interfaces to collaborate. Problem Imagine that you’re creating a stock market monitoring app. The
refactoring.guru
'Design Pattern > GoF - Structural Patterns' 카테고리의 다른 글
[GoF Design Patterns] Flyweight (2) | 2024.11.28 |
---|---|
[GoF Design Patterns] Facade (0) | 2024.11.27 |
[GoF Design Patterns] Decorator (3) | 2024.11.13 |
[GoF Design Patterns] Composite (0) | 2024.11.10 |
[GoF Design Patterns] Bridge (4) | 2024.10.28 |