[디자인 패턴에 뛰어들기] 구조 패턴 #1 Adapter

devapploper·2024년 1월 26일
post-thumbnail

문제 상황

  • 인터페이스가 불일치하는 두개 이상의 클래스가 존재
  • 사용하고자 하는 서비스 클래스 (레거시 혹은 서드 파티 라이브러리)
  • 서비스 클래스를 이용하면 혜택을 볼 수 있는 하나 이상의 클라이언트 클래스

패턴 설명

  • 일상 속 어댑터의 역할을 떠올려보면 이해하기가 수월하다.

대한민국은 220V 콘센트를 사용한다. 옆나라 일본은 110V을 사용한다. 일본에 220V 플러그를 가진 전자제품을 가져가도 110V 콘센트에 220V 플러그 접지가 맞지 않아 사용할 수 없다. 소위 말하는 돼지코를 꽂아 사용해야한다.

돼지코가 어댑터의 역할을 하고, 전자제품은 클라이언트 클래스, 그리고 콘센트는 서비스 클래스에 해당한다고 생각해보면 된다.

어댑터 패턴은 이처럼 인터페이스가 맞지 않는 서비스 클래스와 클라이언트 클래스가 있을 때, 서비스 클래스를 래핑해서 클라이언트 클래스가 이용할 수 있도록 하는 디자인 패턴이다.

이제 개발 관점에서 어댑터를 이해하기 위해 상황을 가정해보겠다.

사진을 찍어서 PNG 이미지로 보여주는 앱이 있다. 앱에 사진을 편집하는 기능을 추가하려고 서드 파티 라이브러리를 추가하고자 하는데 이 서드 파티는 JPG 이미지만 다룬다. 이미지의 확장자가 맞지 않아 사용이 불가능한 상황이다.

어떻게 하면 서드파티를 사용할 수 있을까?

몇가지 방법을 떠올려 볼 수 있다.

  1. 서드파티 라이브러리가 PNG 확장자를 받도록 구현을 추가하거나 기존 메서드를 변경한다.
  2. 앱의 기존 클래스에 JPG를 PNG로 변환하는 구현을 추가한다.

첫번째 방법은 서비스 클래스를 수정하는 방법인데, 상황에 따라 서비스 클래스의 수정이 막혀있을 수도 있고 (서드 파티 라이브러리인 경우), 추가된 변경 사항이 기존 동작을 망가뜨릴 수도 있다.

두번째 방법은 클라이언트 클래스 입장에서 비즈니스 로직과는 별개의 변환 로직을 추가하게 되는 것이고, 이는 단일책임원칙(single responsibility rule)에서 멀어지는 방향이다.

어댑터 클래스를 사용하면 위 두가지 경우에서의 단점을 모두 보완하면서 문제를 해결할 수 있다.

  1. 서드파티 라이브러리 클래스를 래핑한 어댑터 클래스를 생성하고 PNG 이미지를 JPG 이미지로 변환과 동시에 서비스 클래스의 메서드를 호출하는 메서드를 구현한다.
  2. 그리고 클라이언트가 해당 어댑터 메서드를 호출할 수 있는 인터페이스를 생성하고, 클라이언트가 이 인터페이스를 통해 메서드를 호출하도록 한다.

이렇게 하면 서드 파티를 수정하지 않음과 동시에 기존 클라이언트 코드에 단일 책임 원칙을 그대로 지키면서 서드 파티를 사용할 수 있다. 확장에는 열려 있고, 수정에는 닫혀있도록 하는 개방 폐쇄 원칙에도 부합하는 수정 방향이다.

장점

  • 단일 책임 원칙이 지켜진다
    • 앱의 비즈니스 로직으로부터 인터페이스나 데이터 변환 로직을 분리한 형태다.
  • 개방 폐쇄 원칙이 지켜진다
    • 기존 코드를 망가뜨리지 않으면서 어댑터 추가가 가능하다.

단점

  • 복잡도가 증가한다
    • 인터페이스와 클래스를 추가해야하므로 코드의 복잡도가 늘어난다. 따라서 상황에 따라 클라이언트 클래스 인터페이스에 맞도록 서비스 클래스를 수정하는 편이 더 간단할 수도 있다.

예시 코드

class RoundHole {
    var radius: CGFloat
    
    init(radius: CGFloat) {
        self.radius = radius
    }
  
    func isFitting(peg: RoundPeg) -> Bool {
        return self.radius >= peg.radius
    }
}

class RoundPeg {
    var radius: CGFloat
    
    init(radius: CGFloat) {
        self.radius = radius
    }
}

class SquarePeg {
    var width: CGFloat
    
    init(width: CGFloat) {
        self.width = width
    }
}

class SquarePegAdapter {
    private var peg: SquarePeg
    var radius: CGFloat { peg.width * sqrt(2) / 2 }
    
    init(peg: SquarePeg) {
        self.peg = peg
    }
}

// somewhere in client code
var roundHole = RoundHole(radius: 3)
var squarePeg1 = SquarePeg(width: 4)

roundHole.isFitting(peg: squarePeg1) // compiler error: type mismatch

var squarePegAdapter1 = SquarePegAdapter(peg: squarePeg) // radius: 4 * sqrt(2) / 2 ~= 2.82

var squarePeg2 = SquarePeg(width: 5)
var squarePegAdapter2 = SquarePegAdapter(peg: squarePeg) // radius: 5 * sqrt(2) / 2 ~= 3.54

roundHole.isFitting(peg: squarePegAdapter1) // true
roundHole.isFitting(peg: squarePegAdapter2) // false
profile
iOS, 알고리즘, 컴퓨터공학에 관련 포스트를 정리해봅니다

0개의 댓글