본문 바로가기

스위프트

[Swift/디자인패턴] Strategy 패턴

Strategy는 Behavioral 패턴 중에 하나이다.

Behavioral 패턴이 무엇인지 간략하게 알아보자.

 

Behavioral 패턴

하나의 객체가 너무 많은 기능을 한다면 읽어야하는 코드 범위가 너무 길어지고 이해가 복잡해진다.

Behavioral패턴은 이런 하나의 객체가 수행하기 힘들거나 이해가 복잡해지는 구조를 만들어낼 때

이 기능을 수행하기 위해 객체를 분리하고, 책임 분배를 하는 아이디어를 정립한 패턴이다.

단, 책임 분배를 하는 과정에서 최대한 객체간에 결합성을 줄여

재활용성, 테스트 가능성을 높인다면 더할나위 없다.

 

 

Strategy 패턴

Strategy 패턴을 한 줄로 요약하면

"런타임중에 특정 기능을 각각 다른 방식으로 구현한 객체들 중 선택해 사용하는 방법"

이라고 이해했다.

 

특정 기능을 해결하는 데 구현된 방식을 알고리즘이라고 하자.

하나의 기능을 구현하는 데는 다양한 방식이 존재할 수 있다.

 

예를 들어,

애플 갤러리 개발에서 사용되는 미디어인 이미지/비디오/라이브포토등은

로드하는 내부 방식은 다르지만 그걸 사용하는 측에선

필요한 미디어를 로드한다만 필요하다.

 

이걸 그림으로 나타내면 다음과 같다.

 

런타임 중에 Context는 비즈니스 로직에 따라 필요한 알고리즘을 가지고 있는

ConcreteStrategy를 생성하여 셋을 해주고,

추상화 Strategy객체에 있는 executeAlgorithm을 통해 실행시켜주면

실제로 실행되는 건 마지막으로 셋된 ConcreteStrategy의 메서드가 호출이 된다.

 

 

예제

위에서 예를 들었던 애플의 미디어 타입을 로드하는 예제를 만들어보자.

로드를 하는 데 필요한 인터페이스는 하나면 되고, 반환형을 enum을 통해

인터페이스는 일원화시켰지만 사용하는 쪽에서는 switch를 통해 사용할 수 있도록 하였다.

 

enum Media: CustomStringConvertible {
    case image(UIImage?)
    case video(AVAsset?)
    case livePhoto(PHLivePhoto?)

    var description: String {
        switch self {
        case .image(let image):
            return "\(image)를 들고있는 미디어입니다."
        case .video(let asset):
            return "\(asset)을 들고있는 미디어입니다."
        case .livePhoto(let livePhoto):
            return "\(livePhoto)를 들고있는 미디어입니다."
        }
    }
}

/// Strategy : 특정 알고리즘 및 책임에 대한 인터페이스를 가지는 프로토콜
protocol MediaLoadStrategy: AnyObject {
    func load(completion: @escaping (Media?) -> Void)
}

 

위처럼 미디어를 로드하는 메서드를 추상화시킨 MediaLoadStrategy를 구현한 객체를 만들면

실제로 미디어를 로드하는 하나의 방법의 객체화 된다.

위에 Media의 케이스들에 따라 각 미디어가 로드될 수 있는 로직을 지닌 콘크리트 Strategy를 구현했다.

 

/// Concrete Strategy : 추상화된 Strategy의 알고리즘을 실제로 구현하는 객체
class ImageLoadStrategy: MediaLoadStrategy {
    func load(completion: @escaping (Media?) -> Void) {
        // 이미지 로드 알고리즘 구현

        DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
            let image = UIImage()
            completion(.image(image))
        }
    }
}

class VideoLoadStrategy: MediaLoadStrategy {
    func load(completion: @escaping (Media?) -> Void) {
        // 비디오 로드 알고리즘 구현

        DispatchQueue.global().asyncAfter(deadline: .now() + 3) {
            guard let url = URL(string: "https:www.naver.com") else {
                completion(nil)
                return
            }

            let asset = AVAsset(url: url)
            completion(.video(asset))
        }
    }
}

class LivePhotoLoadStrategy: MediaLoadStrategy {
    func load(completion: @escaping (Media?) -> Void) {
        // 라이브 포토 로드 알고리즘 구현

        DispatchQueue.global().asyncAfter(deadline: .now() + 2) {
            guard let livePhoto = PHLivePhoto(coder: NSCoder()) else {
                completion(nil)
                return
            }

            completion(.livePhoto(livePhoto))
        }
    }
}

마지막 Context는 저런 미디어를 로드해서 사용하는 객체를 임의로 만들었다.

보통 저런 미디어를 로드하는 이유는 중간에 비즈니스 로직에 의한 가공도 추가될 수 있지만

결국 UIView의 어딘가 보여질 경우가 높기 때문에 임의의 웹 페이지 뷰 모델이라고 만들었다.

 

// Context: 특정 알고리즘을 사용하는 실제 객체
class WebPageViewModel {
    private var mediaLoadStrategy: MediaLoadStrategy?

    // MARK: - Internal Methods

    func setMediaLoadStrategy(_ strategy: MediaLoadStrategy) {
        mediaLoadStrategy = strategy
    }

    func loadMedia(completion: @escaping (Media?) -> Void) {
        mediaLoadStrategy?.load(completion: completion)
    }
}

위에 구현처럼, 런타임중에 전략을 바꿀 수 있도록 setter 메서드를 구현하였다.

그리고 실제 알고리즘을 사용할 수 있도록 internal 메서드를 구현하였다.

그래서 아래와 같이 구현하여 실행하면 결과가 런타임중에 바뀌는 것을 확인할 수 있다.

 

var contentType: ContentType = .image
let viewModel = WebPageViewModel()

func changeContentType(_ newContentType: ContentType) {
    contentType = newContentType

    switch contentType {
    case .image, .gif, .vr360:
        viewModel.setMediaLoadStrategy(ImageLoadStrategy())
    case .video, .shortFilm:
        viewModel.setMediaLoadStrategy(VideoLoadStrategy())
    case .livePhoto:
        viewModel.setMediaLoadStrategy(LivePhotoLoadStrategy())
    }

    viewModel.loadMedia { media in
        print(media ?? "아무런 미디어도 로드되지 않았습니다.")
    }
}

changeContentType(.gif)
DispatchQueue.global().asyncAfter(deadline: .now() + 3) {
    changeContentType(.video)
}


// 콘솔 결과
//Optional(<UIImage:0x600003648630 anonymous {0, 0} renderingMode=automatic>)를 들고있는 미디어입니다.
//Optional(<AVURLAsset: 0x600000472ea0, URL = https:www.naver.com>)을 들고있는 미디어입니다.

 

정리

연구해보면서 느낀 건

하나의 객체가 하위의 strategy의 라이프 싸이클과 전혀 관계 없이

계속 살아있는 상태에서 어떤 상태에 의해 다른 로직을 써야하는 케이스여야만 의미있어보인다.

ConcreteStrategy가 수십개인 케이스가 생길 수 있지만,

그 케이스가 중간에 바뀔일이 없다면 솔직히 처음부터 구현을 ConcreteStrategy에 해당하는 클래스를

생성하여 사용하면 덜 복잡하고 좋을 듯 싶다.