728x90
Adapter Pattern
소프트웨어 디자인 패턴 중 하나로, 기존의 클래스나 인터페이스를 변경하지 않고도 다른 인터페이스와 호환되도록 만드는 데 사용됩니다. 주로 서로 호환되지 않는 인터페이스를 연결하여 시스템 간 통합을 쉽게 만드는 데 활용됩니다.
쉽게 설명하자면 Adapter Pattern은 중간 통역사라고 생각하면 편합니다.
- 예시로 이해해보기:
- A는 한국사람이고 영어를 못하고, B는 외국사람이고 영어만 가능합니다.
이때 통역사가 필요한데, 통역사는 한국어를 영어로, 영어를 한국어로 바꿔서 전달해줍니다.
결국, A(클라이언트)는 B와 원할하게 대화할 수 있게됩니다. - A: Client (타겟 인터페이스를 원하는 사용자)
B: Adaptee (타겟과 다른 인터페이스를 가지고 있는 클래스, 호환되지 않는 기존 객체)
통역사: Adapter (두 인터페이스를 연결해주는 중간다리) - 기존에 있는 것(Adaptee)을 수정하지 않고, 새로운 요구사항(Target)에 맞춰주는 중간 역할을 합니다.
- A는 한국사람이고 영어를 못하고, B는 외국사람이고 영어만 가능합니다.
예시
- 예시를 확인을 해봅시다.
// Target: 우리가 필요로 하는 USB 충전 인터페이스
interface USBCharger {
fun chargePhone()
}
// Adaptee: 벽 콘센트(220V 전기)
class WallSocket {
fun provideElectricity() {
println("Providing 220V electricity")
}
}
// Adapter: 충전기(220V를 USB로 변환)
class ChargerAdapter(private val socket: WallSocket) : USBCharger {
override fun chargePhone() {
socket.provideElectricity() // 220V를 받아서
println("Converting 220V to USB and charging phone") // USB로 변환
}
}
// Client: 핸드폰(USB 충전만 필요)
fun main() {
val wallSocket = WallSocket()
val charger: USBCharger = ChargerAdapter(wallSocket)
charger.chargePhone() // 핸드폰 충전 성공!
}
1. Target: USBCharger 인터페이스
- 클라이언트(핸드폰)가 기대하는 기능입니다.
- chargePhone() 메서드를 제공하며, USB 충전을 위한 통일된 인터페이스를 정의합니다.
- 클라이언트는 이 인터페이스만 알면 되며, 내부 구현 방식은 신경 쓰지 않습니다.
// Target: 우리가 필요로 하는 USB 충전 인터페이스
interface USBCharger {
fun chargePhone()
}
2. Adaptee: WallSocket 클래스
- 기존에 이미 존재하는 클래스입니다.
- 벽 콘센트에서 220V 전기를 제공하는 provideElectricity() 메서드를 가지고 있습니다.
- 그러나 USBCharger 인터페이스와는 호환되지 않습니다.
// Adaptee: 벽 콘센트(220V 전기)
class WallSocket {
fun provideElectricity() {
println("Providing 220V electricity")
}
}
3. Adapter: ChargerAdapter 클래스
- Target(USBCharger)과 Adaptee(WallSocket) 사이에서 중간다리 역할을 합니다.
- USBCharger 인터페이스를 구현하여 클라이언트가 기대하는 인터페이스를 제공합니다.
- 내부적으로 WallSocket의 provideElectricity()를 호출하여 USB 충전이 가능하도록 변환 로직을 수행합니다.
// Adapter: 충전기(220V를 USB로 변환)
class ChargerAdapter(private val socket: WallSocket) : USBCharger {
override fun chargePhone() {
socket.provideElectricity() // 220V를 받아서
println("Converting 220V to USB and charging phone") // USB로 변환
}
}
4. Client: main 함수
- 클라이언트는 USBCharger 인터페이스를 통해 충전기를 사용합니다.
- 실제로는 WallSocket의 전력을 사용하고 있지만, 클라이언트는 이를 알 필요가 없습니다.
- ChargerAdapter가 이를 추상화하여 처리해줍니다.
// Client: 핸드폰(USB 충전만 필요)
fun main() {
val wallSocket = WallSocket() // 기존 WallSocket 생성
val charger: USBCharger = ChargerAdapter(wallSocket) // Adapter로 감싸기
charger.chargePhone() // 클라이언트는 USBCharger 인터페이스만 사용
}
Adapter pattern의 장단점
1. 장점
- 프로그램의 기본 비즈니스 로직에서 인터페이스 또는 데이터 변환 코드를 분리할 수 있기 때문에 단일 책임 원칙 (SRP: Single Responsibility Priciple)을 만족합니다.
- SRP란? - 객체지향의 5대 원칙의 SOLID의 5가지 원칙 중 하나로 '클래스(혹은 객체)는 반드시 하나의 책임만 가져야 한다' 라는 원칙으로 클래스의 역할을 분리함으로써 코드 복잡성을 줄이고 유지보수와 테스트를 용이하게 만듭니다.
- 기존 클래스 코드를 건들지 않고 클라이언트 인터페이스를 통해 어댑터와 작동하기 때문에 개방-폐쇄 원칙 (OCP: Open Close Priciple)를 만족합니다.
- OCP란? - SRP와 같이 SOLID의 5가지 원칙중 하나로 '확장에는 열려 있고, 수정에는 닫혀 있어야 한다' 라는 원칙입니다. 즉 새로운 기능을 추가할 때 기존 코드를 수정하지 않고 확정(상속, 구현, 전략 패턴 등)을 통해 기능을 추가해야 합니다. 이를 통해 코드 변경으로 인한 부작용을 줄이고, 유지 보수성을 향상시킵니다.
2. 단점
- 코드가 복잡성이 증가합니다. 새로운 인터페이스와 어댑터 클래스 세트를 도입해야 하기 때문에 코드의 복잡성이 증가합니다.
- 직접 서비스(Adaptee) 클래스를 변경하는 것이 간단할 수 있는 경우가 있기 때문에 신중히 선택하여야 합니다.
실제 예제
- 실제 프로젝트상에서 어떻게 쓰이는지 확인해봅시다.
로그인 예제
예로 Adapter 패턴을 사용하지 않고 여러 플랫폼에 대한 로그인을 구성한다고 가정을 해보면
아래와 같이 코드가 작성될 겁니다.
fun login(platform)
if(platform == kakao) {
val kakao: Kakao = new Kakao()
kakao.loginPage()
kakao.Auth()
kakao.redirect()
} else if(platform == naver) {
val naver: Naver = new Naver()
naver.loginPage()
naver.login()
} else {
...
}
platform에 맞는 객체를 생성해서 로그인하도록 하는 코드입니다.
위 예제는 platform이 2개이지만 google, facebook 등등이 더 추가되면 코드가 길어지고
플랫폼이 추가될때 else if문을 추가해줘야 하는 불편함이 생깁니다.
그럼 위 코드에 adapter pattern을 적용하여 변경해봅시다.
1. LoginAdapter 추상화
// 어댑터 인터페이스 선언
interface LoginAdapter {
fun goLoginPage(): LoginAdapter
fun requestLogin(id: String, pw: String): LoginAdapter
fun redirect(url: String): String
}
// 각각 어댑터 인터페이스를 구현
class KakaoLoginAdapterImpl: LoginAdapter {
private val kakaoLoginService = KakaoLoginService()
override fun goLoginService() {
kakaoLoginService.goLoginPage()
return this
}
override fun requestLogin(id: String, pw: String): LoginAdapter {
kakaoLoginService.requestLogin(id, pw)
return this
}
override fun redirect(url String) = kakaoService.redirect(url)
}
class NaverLoginAdapterImpl: LoginAdapter {
private val naverLoginService = NaverLoginService()
override fun goLoginService() {
naverLoginService.goLoginPage()
return this
}
override fun requestLogin(id: String, pw: String): LoginAdapter {
naverLoginService.requestLogin(id, pw)
return this
}
// 네이버 로그인은 redirect하지 않기 때문에 빈 메소드 정의
override fun redirect(url String): String = url
}
2. 추상화된 LoginAdapter를 사용하는 Client
class LoginService(private val loginAdapter: LoginAdapter) {
fun login(id: String, pw: String, rediredctUrl: String) = loginAdapter
.goLoginPage()
.requesetLogin(id, pw)
.redirect(redirectUrl)
}
val kakaoLoginService = LoginService(KakaoLoginAdapterImpl())
val naverLoginService = LoginService(naverLoginAdapterImpl())
kakaoLoginService.login("kakao_id", "kakao_pw", "kakao.com")
naverLoginService.login("naver_id", "naver_pw", "naver.com")
위와 같이 구현하게 되면 클라이언트는 어떤 로그인 서비스를 사용할지 스스로 판단하여 필요한 로직을 추가 구현할 필요 없이
로그인 어댑터 인터페이스만 사용하면 됩니다.
platform이 추가되어도 클라이언트 코드가 변경될 필요가 없습니다.
728x90
'디자인패턴' 카테고리의 다른 글
Stractegy Pattern - 전략 패턴 (1) | 2024.12.28 |
---|---|
Factory Method Pattern - 팩토리 메서드 패턴 (0) | 2024.12.28 |
State Pattern(상태 패턴) (0) | 2024.12.19 |
Abstract Factory Pattern(추상 팩토리) (2) | 2024.12.17 |
Builder Pattern(빌더 패턴) (0) | 2024.12.16 |