본문 바로가기

스위프트

[Swift] Protocol에 대한 이해 및 정리

프로토콜

특정 객체 혹은 기능을 추상화하는 자료구조

그렇다면 프로토콜을 이해하는 것의 시발점은

추상화가 무엇인지 이해하는 것이 되겠다.

 

추상화

추상화는 특정 객체나 기능을 구현하기 위해 필요한

프로퍼티나 메서드의 집합을 의미한다고 개인적으로 정의하고 싶다.

그럼 여기서 의문이 들 수 있다.

특정 객체나 기능을 바로 실체화하여 사용하지 않고,

왜 굳이 추상화를 하여 한 번더 고민해야하는 걸까.

 

컴퓨터 과학용어중에 캡슐화라고 있다.

캡슐화는 특정 객체나 기능을 구현하는 데

매우 복잡한 로직을 외부로부터 숨기고

사용자는 오롯이 객체나 기능에서 노출된 API만 사용하여

비즈니스 목적에 도달할 수 있는 것이다.

 

추상화는 이 캡슐화를 진행하는 데 있어 매우 도움이 되고

특히, 위에서 말한 바로 실체화로는 이룰 수 없는

재사용성을 도와준다고 생각한다.

 

정리하면

추상화는 특정 기능을 캡슐링하고 재사용할 수 있는 방법이다.

 

그래서 프로토콜이 뭔데?

프로토콜은 추상화를 위한 프로퍼티와 메서드의 집합이다.

하지만, 실체화를 하지 않기 때문에

특정 프로퍼티를 소유하고 있지 않는 인터페이스(껍데기)다.

 

하나의 아주 간단한 예를 들어보자.

현재 미디어를 표현하는 확장자는 종류가 매우 많다.

이미지/비디오/GIF 나 애플에서 제공하는 LivePhoto등이 있다.

 

근데, 기획측에서 요청이 하나 들어왔다.

"미디어들의 대표이미지를 모아서 유저들에게 보여주는 기능을 구현해주세요."

 

여기서 만약 추상화를 위한 도구인 프로토콜이 없다면 어떻게 될까?

간단하게 코드로 보자.

 

// Image의 정보를 담는 객체.
final class ImageInformation {
	private let image: UIImage
    
    // MARK: - Init
    
    init(_ image: UIImage) {
    	self.image = image
    }
    
    // MARK: - API Methods
    
    func extractThumbnailImage() -> UIImage {
    	// 해당 객체의 프로퍼티 혹은 메서드를 통해 대표 이미지를 얻는 비즈니스 로직
        ...
    }
}

// Video의 정보를 담는 객체.
final class VideoInformation {
	private let asset: AVAsset
    
    // MARK: - Init
    
    init(_ asset: AVAsset) {
    	self.asset = asset
    }
    
    // MARK: - API Methods
    
    func extractThumbnailImage() -> UIImage {
    	// 해당 객체의 프로퍼티 혹은 메서드를 통해 대표 이미지를 얻는 비즈니스 로직
        ...
    }
}

// GIF의 정보를 담는 객체.
final class GIFInformation {
	private let sequenceImages: SequenceImages
    
    // MARK: - Init
    
    init(_ sequenceImages: SequenceImages) {
    	self.sequenceImages = sequenceImages
    }
    
    // MARK: - API Methods
    
    func extractThumbnailImage() -> UIImage {
    	// 해당 객체의 프로퍼티 혹은 메서드를 통해 대표 이미지를 얻는 비즈니스 로직
        ...
    }
}

// 위의 미디어들을 전부 가지고 있는 객체
final class MediaContainer {
	private var mediaArray = [Any]()
    
    // MARK: - API Methods
    
    func addNewMedia(_ media: Any) {
    	mediaArray.append(media)
    }
    
    func extractAllThumbnailImagesFromMedias() -> [UIImage] {
    	mediaArray.compactMap { media -> UIImage? in
        	if let imageInformation = media as? ImageInformation {
            	return imageInformation.extractThumbnailImage()
            else if let videoInformation = media as? VideoInformation {
            	return videoInformation.extractThumbnailImage()
            else if let gifInformation = media as? GIFInformation {
            	return gifInformation.extractThumbnailImage()
            }
            else {
            	return nil
            }
        }
    }
}

맨 아래 객체에 구현된 객체의 API인 extractAllThumbnailImagesFromMedias를 보자.

추상화를 하지 않았기에, 우린 모두 실체화된 객체를 알아야지만 API를 사용할 수 있고,

결국 저런 극악무도한(?) 코드를 작성해야 우린 API를 쓸 수 있다.

 

자 프로토콜은 추상화를 하는 객체이다.

우리가 원하는 기능이 뭔가?

특정 미디어에서 대표 이미지를 뽑아내는 거다.

그럼 이 기능을 추상화하는 것은 단순하다.

위 실체화 된 객체들이 모두 가지고 있어야만 하는 메서드를

정의만 해주면 되는 것이다.

아래처럼 될 것이다.

 

// 특정 대표 이미지를 뽑아내는 기능을 추상화한 프로토콜
protocol ThumbnailImageExtractable {
	func extractThumbnailImage() -> UIImage
}

프로토콜은 추상화다.

추상화는 실객체의 기능을 추상화하기 때문에

어떤 실객체가 추상화의 기능을 가진다는 것은

구현을 강제해야한다는 의미이다.

 

위 예제를 위 추상화 객체인 프로토콜을 통해 한 번 어떻게 바뀌는지 보자.

 

// Image의 정보를 담는 객체.
final class ImageInformation: ThumbnailImageExtractable {
	private let image: UIImage
    
    // MARK: - Init
    
    init(_ image: UIImage) {
    	self.image = image
    }
    
    // MARK: - ThumbnailImageExtractable Methods
    
    func extractThumbnailImage() -> UIImage {
    	// 해당 객체의 프로퍼티 혹은 메서드를 통해 대표 이미지를 얻는 비즈니스 로직
        ...
    }
}

// Video의 정보를 담는 객체.
final class VideoInformation: ThumbnailImageExtractable {
	private let asset: AVAsset
    
    // MARK: - Init
    
    init(_ asset: AVAsset) {
    	self.asset = asset
    }
    
    // MARK: - ThumbnailImageExtractable Methods
    
    func extractThumbnailImage() -> UIImage {
    	// 해당 객체의 프로퍼티 혹은 메서드를 통해 대표 이미지를 얻는 비즈니스 로직
        ...
    }
}

// GIF의 정보를 담는 객체.
final class GIFInformation: ThumbnailImageExtractable {
	private let sequenceImages: SequenceImages
    
    // MARK: - Init
    
    init(_ sequenceImages: SequenceImages) {
    	self.sequenceImages = sequenceImages
    }
    
    // MARK: - ThumbnailImageExtractable Methods
    
    func extractThumbnailImage() -> UIImage {
    	// 해당 객체의 프로퍼티 혹은 메서드를 통해 대표 이미지를 얻는 비즈니스 로직
        ...
    }
}

// 위의 미디어들을 전부 가지고 있는 객체
final class MediaContainer {
	private var mediaArray = [Any]()
    
    // MARK: - API Methods
    
    func addNewMedia(_ media: Any) {
    	mediaArray.append(media)
    }
    
    func extractAllThumbnailImagesFromMedias() -> [UIImage] {
    	mediaArray.compactMap { media -> UIImage? in
        	guard let thumbnailExtractable = media as? ThumbnailImageExtractable else {
            	return nil
            }
            
            return thumbnailExtractable.extractThumbnailImage()
        }
    }
}

 

어떤가 더욱 깔끔하지 않는가?

이젠 추상화한 프로토콜을 만들어놨으니

미디어가 매번 추가되더라도 실체화된 객체에

프로토콜을 준수만 시키면 위 미디어 배열을 가지는 객체의

코드는 변경하지 않아도 자동으로 돌아갈 것이다.

 

이런 재사용성, 캡슐화의 위력을 위해서라도

프로토콜을 정확이 이해해야 한다.

 

이쯤되면 프로토콜을 이해하였다고 생각한다.

하지만, Swift에서 프로토콜을 사용하다보면 생각보다 많은 제약이 있다.

보통 이런 제약은 추상화를 하려는 범위보다 실체화를 하려고 할 때

문제가 발생한다.

 

그러므로,

다양한 예제 케이스를 통해

프로토콜은 여기까지 사용할 수 있고,

어떤 부분은 추상화가 불가능하고 실체화를 시켜야만 된다고 인지하는 것이

개발을 하는 데 소요시간을 단축시킬 수 있는 방법 중 하나가 될 것이다.

다음 장에서 설명한다.