구조 패턴

종류: Pattern

1. 구조 - Bridge

빌더패턴은 구현부에서 추상층을 분리해서 각자 독립적으로 변형할 수 있게 해주는 패턴이다!

Untitled

위의 예시처럼 구성하면 새로운 모양이나 색이 추가하면 기하급수적으로 많은 클래스를 추가해야함!!

Untitled

이런 식으로 모양과 색을 분리해준다면 각 속성을 독립적으로 수정 및 확장이 가능해짐!

구조

Untitled

  • abstraction
    • client가 사용하는 최상위 타입
    • implementation을 참조하고 일을 위임
  • implementation
    • abstraction의 기능을 구현하기 위해 인터페이스를 정의

Untitled

2. 구조 - Decorator

주어진 상황 및 용도에 따라 객체에 책임을 덧붙이는 패턴으로, 서브클래싱 대신에 기능확장을 쉽게 해줄 수 있다!

  • 마트로시카마냥 계속 자기자신을 감싼다라고 생각하면 됨!

문제상황

ex) SNS 알림 라이브러리를 만들었다.

Untitled

  • 상속으로 해결했지만 사람들은 SMS, Facebook를 같이 알림을 받고 싶다는 피드백이 왔다!

→ 이걸 또 상속으로 한다면… 계속해서 서브클래싱이 무한대로 발산해버린다 ㅠㅠ

Untitled

⇒ 그렇다면 상속이 아닌 집합을 쓰는 건 어떨까??

Untitled

class Uploader {
    private func setUp() {
        /*...*/
  }

  func upload() {
    setUp()
  }
}

class DecoratedUploader: Uploader {
  var uploader: Uploader? // 데코레이터가 내부 uploader를 가지기에 랩핑하게 됨!!

    init(_ uploader: Uploader? = nil) {
    self.uploader = uploader
  }

  override func upload() {
    super.upload()
    uploader?.upload()
  }
}

class TextUploader: DecoratedUploader {
    override func upload() {
        super.upload()
        print("uploading text")
    }
}

class ImageUploader: DecoratedUploader {
    override func upload() {
        super.upload()
        print("uploading image")
    }
}

class VideoUploader: DecoratedUploader {
    override func upload() {
        super.upload()
        print("uploading video")
    }
}

class FileUploader: DecoratedUploader {
    override func upload() {
        super.upload()
        print("uploading file")
    }
}

let textAndImageAndVideoUploader = TextUploader(ImageUploader(VideoUploader()))
let imageAndVideoUploader = ImageUploader(VideoUploader())
let everythingUplodaer = TextUploader(ImageUploader(VideoUploader(FileUploader())))

3. 구조 - Facade

퍼사드는 건물의 정면, 외관을 의미함. 즉, 어떤 소프트웨어의 커다란 코드에 대한 간략화된 인터페이스라고 보면 됨!

  • 퍼서드는 여러 인스턴스를 소유하여 사용해야하는 타입이 있는 경우, 간단한 인터페이스를 통해 각 인스턴스들이 일하게 할 때 유용!
protocol Facade {
    func work()
}

struct CPU {
    func work(with memory: Memory) { }
}

struct Memory {
    func input(from Devices: [Device]) { }
    func output(to Devices: [Device]) { }
}

class Device { ... }
class InputDevice: Device { ... }
class OutputDevice: Device { ... }
class Keyboard: InputDevice { ... }
class Monitor: OutputDevice { ... }
class TouchBar: Device { ... }

struct Computer: Facade {
    private let cpu = CPU()
    private let memory = Memory()
    private let keyboard = Keyboard()
    private let monitor = Monitor()
    private let touchBar = TouchBar()

    func work() {
        memory.input(from: [keyboard, touchBar])
        cpu.work(with: memory)
        memory.output(to: [monitor, touchBar])
    }
}

// My code
let computer = Computer()
computer.work()

4. 구조 - Flyweight

동일하거나 유사한 객체 사이에서 가능한 많은 데이터를 공유하게 해서 메모리 사용량을 최소화하는 패턴!

Untitled

  • flyweight: 공유되는 데이터에 대한 인터페이스
  • flyweightFactory: flyweigfht객체들을 가지며 관리

ex) 몬스터들이 대량으로 이동하면서 사라지고 다시 생성되는 경우

import Foundation
import CoreGraphics

// flyweight
protocol Monster {
    func createAtCurrentLocation(at location: CGPoint)
    func deleteAtCurrentLocation(at location: CGPoint)
    func recreateAtOtherLocation(at location: CGPoint)
    func attack()
}

// flyweight 구현체
class LowerLevelMonster: Monster {
    func createAtCurrentLocation(at: CGPoint) {
        // some code
    }

    func deleteAtCurrentLocation(at: CGPoint) {
        // some code
    }

    func recreateAtOtherLocation(at: CGPoint) {
        // some code
    }

    func attack() {
        // some code
    }
}

class MonsterClient {
    let monster: Monster
    var currentLocation: CGPoint
    init(monster: Monster, currentLocation: CGPoint) {
        self.monster = monster
        self.currentLocation = currentLocation
    }

    func createMonster(currentLocation: CGPoint) {
        monster.createAtCurrentLocation(at: currentLocation)
    }

    func recreateMonster(currentLocation: CGPoint, at location: CGPoint) {
        monster.deleteAtCurrentLocation(at: currentLocation)
        monster.recreateAtOtherLocation(at: location)
    }
}

// MARK: Factory: flyweight들을 관리하는 객체
class MonsterFactory {
    static let shared = MonsterFactory()
    private init() { }

    enum MonsterLevel {
        case lower
    }

    private var createdMonster = [MonsterLevel: Monster]()

    private func createMonster(_ level: MonsterLevel) -> Monster {
        switch level {
        case .lower:
            let lowerLevelMonster = LowerLevelMonster()
            createdMonster[level] = lowerLevelMonster

            return lowerLevelMonster
        }
    }

    func monster(level: MonsterLevel) -> Monster {
        if let monster = createdMonster[level] {
            return monster
        } else {
            let monster = createMonster(level)
            return monster
        }
    }
}

let lowerLevelMonster = MonsterFactory.shared.monster(level: .lower)
let lowerLevelMonsterClient = MonsterClient(monster: lowerLevelMonster, currentLocation: CGPoint(x: 10, y: 20))

lowerLevelMonsterClient.createMonster(currentLocation: CGPoint(x: 10, y: 20))
lowerLevelMonsterClient.recreateMonster(currentLocation: CGPoint(x: 10, y: 20), at: CGPoint(x: 100, y: 110))

  • 몬스터의 인스턴스를 가져올 때 새로운 인스턴스를 가져오는게 아니라 딕셔너리에 있는 값을 캐시해서 가져올 수 있게 됨!

5. 구조 - Proxy

프록시 대리라는 뜻으로 다른 누군가를 대신해서 그 역할을 수행하는 존재 자신이 할 수 있는 최선의 일을 한 후에 범위를 벗어나면 진짜 일을 하는 사람에게 요청하는 패턴

프록시의 종류

  • remote proxy
    • 요청을 처리하고 서비스 객체에 이를 전달하는 역할 담당
  • virtual proxy
    • 서비스 객체에 대한 정보를 캐싱하여 접근을 연기
  • protection proxy
    • 특정 작업을 요청한 객체가 해당 작업을 수행할 권한을 가지고 있는지 확인

구조

Untitled

  • Proxy: 대리 역할
  • RealSubject: 실제의 주체
  • Subject: 대리와 실제 역할을 동일시하기 위한 프로토콜

// subject
protocol YouTubeDownloadSubject {
   func downloadYoutubeVideos() async -> [String]
}

final class RealSubject: YouTubeDownloadSubject {
    func downloadYoutubeVideos() async -> [String] {
        //Todo: 유튜브 서버에서 비디오를 다운로드해오는 부분을 구현한다.
    }
}

final class Proxy: YouTubeDownloadSubject {
        //진짜 요청을 받아서 처리하는 개체, 정말로 사용할때만 초기화하기 위하여 lazy키워드 사용
    private lazy var realSubject = RealSubject()
        //캐싱구현
    private var videoCache = [String]()
        //클라이언트 권한 받음
    private var client: Clinet

    init(_ client: Clinet) {
        self.client = client
    }

    func downloadYoutubeVideos() async -> [String] {
        //클라이언트 권한에 따라 제어를 할 수도 있다.
        guard client.auth == .owner else {
            print("유튜브 비디오를 다운로드할 권한이 없습니다.")
            return []
        }

        //비디오 캐시가 비어있으면 실제 realSubject에 데이터를 요청함.
        if videoCache.isEmpty {
            videoCache = await realSubject.downloadYoutubeVideos()
            return videoCache
        } else {
            //비디오 캐시에 데이터 있으면 그거 리턴해줌.
            return videoCache
        }
    }
}

let client = Clinet(.owner)
let proxy = Proxy(client)

//프로토콜 타입을 받습니다.
func loadYouTubeVideo(_ service: YouTubeDownloadSubject) {
    service.downloadYoutubeVideos()
}

loadYouTubeVideo(proxy)

장점

  • realsubject가 아주 큰 인스턴스일 때 proxy를 이용해서 최대한 지연시킬 수 있음
  • proxy는 real이 준비되지 않거나 사용할 수 없는 경우에도 동작

단점

  • proxy를 도입해야하므로 코드가 복잡해짐

6. 구조 - Composite

객체들을 트리 구조로 구성하여 부분, 전체 계층을 표현하는 패턴 사용자가 단일 객체와 복합 객체 모두 동일하게 다루도록 한다

재귀적으로 최하단까지 실행하고 싶을 경우 유용하게 사용 가능

Untitled

  • component: 공통적인 기능을 하는 프로토콜
  • leaf, composite: 모두 component를 구현하지만, compoiste는 자식 component들을 추가적으로 가지고 있음
import Foundation

// Component
protocol Military {
    var unitName: String { get set }
    func attack()
}

// Leafs
struct AirForce: Military {
    var unitName: String
    func attack() {
        print("\\(unitName) 공격 🔫")
    }
}

struct Navy: Military {
    var unitName: String
    func attack() {
        print("\\(unitName) 공격 🔫")
    }
}

struct Army: Military {
    var unitName: String
    func attack() {
        print("\\(unitName) 공격 🔫")
    }
}

// Composite
struct MilitaryGroup: Military {
    var unitName: String
    var group: [Military]

    func attack() {
        print("-----\\(unitName) 예하에 있는 부대에 공격 명령을 하달한다.-----")
        group.forEach { unit in
            unit.attack()
        }
    }
}

let navy627 = Navy(unitName: "해군 627대대")
let navy625 = Navy(unitName: "해군 625대대")
let army653 = Army(unitName: "육군 653대대")
let army669 = Army(unitName: "육군 669대대")
let airForce257 = AirForce(unitName: "공군 257대대")
let airForce239 = AirForce(unitName: "공군 239대대")

let navy1 = MilitaryGroup(unitName: "해군 1사단", group: [navy627, navy625])
let army1 = MilitaryGroup(unitName: "육군 1사단", group: [army653, army669])
let airForce1 = MilitaryGroup(unitName: "공군 1사단", group: [airForce257, airForce239])

let thirdROKArmy = MilitaryGroup(unitName: "3군 사령부", group: [navy1, army1, airForce1])

thirdROKArmy.attack()

7. 구조 - Adapter

클래스의 인터페이스를 사용자가 원하는 다른 인터페이스로 변환하는 패턴

구조

Untitled

  • client: 기존 비즈니스 로직을 포함하는 클래스
  • target interface: 다른 클래스
  • adapter: adaptee를 wrapping해서 구현
  • adaptee: 호환되지 않은 클래스

Untitled

Untitled

**// MARK: - import 라이브러리의 인터페이스 (Adaptee)**
class AppleAuthorization {
    func presentAuth() {
        // 애플 로그인 내부구현
    }
}

class GoogleAuthorization {
    func tryLogin() {
        // 구글 로그인 내부구현
    }
}

**// Mark: - Target Interface**
protocol AuthorizationService {
    func login(completion: (Bool) -> ())
}

// MARK: - Adapter
class Authorization: AuthorizationService {
    func submitUserInfo(_ userInfo: Token, completion: (Bool) -> ()) {
        // 사용자 정보를 서버로 요청하는 동작
    }

    func login(completion: (Bool) -> ()) {
        // 로그인 동작
        let exampleInfo = Token(id: "사용자 아이디", password: "사용자 비밀번호")
        submitUserInfo(exampleInfo) { result in
            completion(result)
        }
    }
}

struct AppleAuthorizationAdapter: AuthorizationService {
    let adaptee = AppleAuthorization()

    func login(completion: (Bool) -> ()) {
        adaptee.presentAuth()
    }
}

struct GoogleAuthorizationAdapter: AuthorizationService {
    let adaptee = GoogleAuthorization()

    func login(completion: (Bool) -> ()) {
        adaptee.tryLogin()
    }
}

// Client
enum AuthorizationPlatform {
    case basic
    case apple
    case google
}

func presentAuthorization(_ platform: AuthorizationPlatform, completion: (Bool) -> ()) {
    switch platform {
    case .basic:
        let basicAuth = Authorization()
        basicAuth.login { result in
            // 로그인 성공/실패
        }
    case .apple:
        let appleAuth = AppleAuthorizationAdapter()
        appleAuth.login { result in
            // 로그인 성공/실패
        }
    case .google:
        let googleAuth = GoogleAuthorizationAdapter()
        googleAuth.login { result in
            // 로그인 성공/실패
        }
    }
}