2024. 12. 31. 13:39ㆍSwift, iOS Foundation/Swift 문법 총정리
1. Swift의 OOP (Object-Oriented Programming)와 POP (Protocol-Oritented Programming)
Apple은 과거의 Swift를 프로토콜 지향 언어 (Protocol-Oriented Language)라고 소개한 적이 있습니다.
객체 지향 프로그래밍 (OOP: Object-Oriented Programming) 패러다임을 따르는 객체 지향 언어라는 말은 들어본 적이 있어도,
프로토콜 지향 프로그래밍 (POP: Protocol-Oriented Programming)을 따르는 프로토콜 지향 언어라는 말은 다소 생소할 것 같은데요.
처음 이 내용을 들었을 때 저는 아래와 같은 질문들이 머릿속에 떠올랐습니다.
- 그럼 프로토콜을 지향하는 언어 Swift는 객체 지향 프로그래밍 패러다임을 채택하지 않는 건가?
- OOP (Object-Oriented Programming)와 POP (Protocol-Oriented Programming)는 서로 별개의 개념인 건가?
- Swift가 프로토콜을 지향한다는 것은 어떤 점에서 그렇게 말하는 거지? 그냥 프로토콜을 쓰면 프로토콜 지향 프로그래밍인가?
- 나는 평소에 프로토콜 지향 프로그래밍 (POP) 패러다임을 잘 따르면서 개발하고 있던 건가?
이 질문에 대한 해답들을 이번 글에서 저와 함께 차근차근 알아가 보도록 하겠습니다.
일단 Swift 관점에서 객체 지향 (Object-Oriented)과 프로토콜 지향 (Protocol-Oriented) 개념을 정복하고 갈 필요가 있겠네요!
☑️ 일단 Swift는 객체 지향 프로그래밍 (OOP) 패러다임을 따르는 객체 지향 언어 (Object-Oriented Language)입니다.
예전 구조체와 클래스를 설명하는 글에서도 언급했는데, 객체 지향 프로그래밍 언어의 대표적인 특징으로 4가지 - 상속, 추상화, 캡슐화, 다형성 등이 있습니다.
묻지도 따지지도 않고 일단, Swift가 객체 지향 언어인 이유는 이 4가지 OOP의 특징을 따르고 있기 때문이죠!
- 상속과 추상화 -> 공통되는 기능과 특징을 뽑아 클래스를 만들고, 상속을 통해 추가로 필요한 기능을 확장할 수 있습니다!
- 캡슐화 -> 만들어진 클래스 자체 혹은 내부 프로퍼티, 메서드 등에 대해 접근 제어(private, fileprivate, internal, public, open) 연산자를 활용해 접근 범위를 직접 지정할 수 있었습니다!
- 다형성 -> 부모 클래스 타입으로 자식 클래스 객체를 다룰 수 있고, 오버라이딩을 통해 런타임 동적 바인딩을 지원했습니다!
그런데, 앞에서 말한 것처럼 Swift는 프로토콜 지향 언어 (Protocol-Oriented Programming)이기도 합니다.
🧐 첫 번째 질문에 대한 해답!
"그럼 프로토콜을 지향하는 언어 Swift는 객체 지향 프로그래밍 패러다임을 채택하지 않는 건가?"
: Swift는 어느 특정한 프로그래밍 패러다임만을 채택하는 것이 아니라, 여러 개의 패러다임을 채택하고 있는 다중 패러다임 프로그래밍 언어 (Multi-Paradigm Programming Language)입니다.
Swift뿐만 아니라 많은 최신 언어들도 이와 같은 여러 패러다임 원칙을 동시에 채택하여, 각 부분의 장점만을 모아 구현하고 있죠!
그래서 Swift는 객체 지향 언어이면서, 동시에 프로토콜 지향 언어이기도 한 다중 패러다임 언어인 것입니다. (함수형 프로그래밍 언어 특징도 포함한다고 하는데, 이번 글에서는 OOP와 POP만 집중적으로 알아보도록 할게요!)
그럼 Swift는 객체 지향 패러다임을 채택하면서, 왜 (Why?) 이와 동시에 프로토콜 지향 패러다임을 채택하게 된 것일까요?
이는 객체 지향 프로그래밍과 별개로 패러다임을 구분하기보다, 객체 지향 프로그래밍의 연장선에서 이어지는 프로토콜 지향 프로그래밍의 관점에서 이해하는 것이 더 좋을 것 같습니다!
기존 객체 지향 프로그래밍에서는 아래와 같은 한계점들이 있었고, 이 방법을 개선하기 위해 등장한 개념이 프로토콜이기 때문이죠.
- 클래스의 참조 타입 (Reference Type) 특징에서 오는 한계
- OOP의 기본 뼈대가 되는 클래스는 Reference Type에 해당합니다. (Struct는 Value Type이지만, 상속이 불가능했죠!)
- 참조 타입은 메모리 주소를 직접 공유하는 형태이기 때문에, 클래스 간 데이터 공유/수정이 많아진 현대 프로그래밍에서 이와 관련한 각종 문제 (Data-race, Memory Leak 등)들이 많아지고 있었습니다.
- 다중 상속의 불가
- 기본적으로 상속은 "단일 상속"만 가능하도록 설계되어 있습니다. -> 만약 두 개 이상의 클래스로 조합이 필요한 경우에는, 기존 OOP 상속으로는 어쩔 수 없는 문제가 있었던 것이죠.
- 의존성의 복잡성 문제
- 이러다 보니, 하나의 클래스가 이곳저곳에서 부모 클래스 역할을 하거나 / 혹은 상속에 상속에 상속을 타고 거쳐가는 상황이 많아지다 보니 처음 OOP가 도입된 목적과는 다르게 다시 의존성이 많아지고,,, 재사용성은 낮아지게 됩니다.
- 특히 기존 클래스 상속 과정에서는 부모 클래스에서 수정이 발생하면, 자식 클래스들도 모두 동시에 수정이 필요한 상황인 거죠.
🧐 두 번째 질문에 대한 해답!
"OOP (Object-Oriented Programming)와 POP (Protocol-Oriented Programming)는 서로 별개의 개념인 건가?"
: 공식적으로 이렇게 설명하지는 않지만, OOP의 단점을 보완하고자 생긴 패러다임이 POP라고 이해할 수 있습니다.
즉, 이 둘은 별개의 개념이 아니라 소프트웨어의 재사용성과 유지보수성을 높인다는 목표 아래, 계속 이어지고 있는 프로그래밍 패러다임의 흐름을 따른다고 받아들이는 것이 맞을 것 같네요!
2. Swift 프로토콜 (Protocol) 기본 문법 배워보기
오케이.
프로토콜 지향 프로그래밍이 객체 지향 프로그래밍이랑 어떤 관계를 갖고 있는지는 어느 정도 이해했습니다.
그럼 이제 프로토콜이 뭔지를 알아봐야겠죠?
*해당 부분은 이미 기초 문법을 알고 계신 분들은 빠르게 넘어가면 되고 / 기본 문법을 모르시는 분들을 위해서 필요한 부분만 간략하게 설명을 따라가시면 됩니다.
**더 자세한 프로토콜의 사용 상황이 궁금하면, 아래 Delegate Pattern 글을 참고하셔도 좋습니다!
☑️ 프로토콜 (Protocol)은 메서드, 프로퍼티 등의 특정 작업이나 기능을 위해 필요한 요구사항 (Requirements)의 청사진을 의미합니다.
protocol SomeProtocol {
// protocol definition goes here
}
☑️ 프로토콜 (Protocol)은 클래스, 구조체, 열거형에 채택될 수 있습니다. (한 타입당 채택할 수 있는 프로토콜 개수 제한은 없습니다!)
이때, 해당 타입은 채택한 프로토콜의 요구사항 (Requirements)을 준수 (Conform)하는 코드를 작성해야 합니다.
struct SomeStructure: FirstProtocol, AnotherProtocol {
// structure definition goes here
}
class SomeClass: SomeSuperclass, FirstProtocol, AnotherProtocol {
// class definition goes here
}
☑️ 프로토콜의 요구사항 (Requirements)은 프로퍼티 / 메서드 / 초기화 구문 등에 정의할 수 있습니다.
- 프로퍼티 : 변수 프로퍼티 (var)와 타입 프로퍼티 (static)을 선언할 수 있으며, (let은 불가!) 각 프로퍼티의 gettable/settable 성질도 함께 작성해야 합니다.
- 메서드 : 인스턴스 메서드와 타입 메서드를 선언할 수 있으며, 메서드의 중괄호와 본문은 작성할 수 없습니다.
*특별히 구조체/열거형 타입 내의 값을 수정하는 경우는 mutating이라는 키워드를 붙여 Value Type 인스턴스의 변경을 수행합니다. - 초기화 구문 : 모든 프로토콜을 채택한 타입에서 초기화 구문을 요구하고자 할 때, 본문 없이 작성할 수 있습니다.
protocol SomeProtocol {
/// Properties
var someProperty: Int { get }
static var someTypeProperty: Int { get set }
/// Methods
func someMethod() -> Double
static func someTypeMethod()
mutating func someMutatingMethod()
// init
init(someParameter: Int)
}
☑️ 프로토콜에 기본 구현 (default implementation)을 지정하고 싶은 경우 프로토콜 익스텐션을 활용할 수 있습니다.
extension SomeProtocol {
var someProperty: Int { return 100 }
func someMethod() -> Double { return 100.0 }
}
☑️ 프로토콜을 채택할 수 있는 특정한 조건을 주고 싶은 경우에는 프로토콜 익스텐션에서 where절을 사용해 제약사항을 추가할 수 있습니다.
protocol Identifiable {
var id: String { get }
}
extension Array where Element: Identifiable {
func printIDs() {
for item in self {
print(item.id)
}
}
}
이 정도 개념만 알면 여러분들은 프로토콜 지향 프로그래밍을 위한 기본 개념은 준비되어 있는 상태라고 볼 수 있습니다!
뭔가 너무 간단해서 찝찝하다.. 하시는 분들은 링크로 첨부한 Swift 공식 문서에서 Protocols 부분을 정독하고 오시길 권장해 드릴게요 ^__^
특히! 프로토콜은 제네릭이랑 함께 사용했을 때 많은 시너지를 냅니다!
그러려면 제네릭에 대한 개념을 확실하게 알아야겠죠??
이 부분은 예전에 <제네릭 (Generic) 완전 정복하기> 라는 주제로 글을 올린 적이 있으니 아래 링크에 들어가서 보고 오시면 좋을 것 같습니다☺️
3. 프로토콜을 사용하면 다 POP (Protocol-Oriented Programming)인가?
자 지금까지 설명했던 프로토콜 (Protocol)을 이제 코드에서 잘 사용하기만 하면, 여러분들은 POP (Protocol-Oriented Programming)를 잘 지키며 코드를 짤 수 있는 iOS 개발자가 된 것입니다! 엥 (???)
그래서 앞으로는 클래스 대신 프로토콜을 쓰라는 건가요... 무작정 프로토콜 만들고 클래스, 구조체, 열거형에서 채택하라는 말인가요...
프로토콜을 사용하는 것이 프로토콜 지향 프로그래밍이지만,
프로토콜을 어느 상황에서 어떻게 사용하는 것이 적합한지도 함께 알아야 진정한 POP를 이해했다고 할 수 있을 겁니다!
지금부터는 WWDC22의 <Embrace Swift generics> <Design protocol interface in Swift> 두 세션에서 자세하게 설명하고 있는, 프로토콜의 사용 상황에 대해 간략하게만 소개해보도록 할게요!
"농장 (Farm)에서 소 (Cow)에게 먹이를 먹는 상황 (feed)"을 구현한다고 해봅시다.
이 상황을 구현하기 위해서 농장 구조체(Struct)의 코드에는 아래와 같은 과정들이 순서대로 포함되어 있어야 한다고 해요!
- 알팔파 (Alfalfa, 건초의 원재료가 되는 식물)라는 식물을 먼저 길러야 합니다. -> grow
- 알팔파 (Alfalfa)가 길러지면 이를 수확해서 건초 (Hay)를 만들 수 있습니다. -> harvest
- 소 (Cow)는 건초 (Hay)를 먹습니다. -> eat
struct Cow {
func eat(_ food: Hay) { print("소가 건초를 먹습니다.") }
}
struct Hay {
static func grow() -> Alfalfa { return Alfalfa() }
}
struct Alfalfa {
func harvest() -> Hay { return Hay() }
}
struct Farm {
func feed(_ animal: Cow) {
let alfalfa = Hay.grow()
let hay = alfalfa.harvest()
animal.eat(hay) // "소가 건초를 먹습니다."
}
}
그런데 만약 농장이 커져서 당근을 먹고 자라는 말 (Horse), 곡물을 먹고 자라는 닭 (Chicken) 등을 함께 키우게 되었다면?
식물을 키우고 -> 키운 식물을 수확해서 -> 해당하는 동물에게 맞는 먹이를 주는 상황은 같지만,
키우는 식물과 / 먹이와 / 동물의 내용이 달라졌기 때문에 비슷한 틀을 갖고 있는 함수 (feed)를 각각 3개나 만들어야 하는 비효율적인 상황이 발생하게 될 겁니다.
지금은 동물이 3종류라 망정이지, 만약 농장이 더욱 커져서 동물이 10종류..100종류.. 늘어난다면...? 그럴 때마다 먹이를 주기 위한 함수 각각을 만드는 것은 너무 비효율적일 거에요!
☑️ Apple은 이렇게 함수의 오버로드를 활용해 다형성을 구현한 방식을 ad-hoc polymorphism이라고 정의합니다!
struct Farm {
func feed(_ animal: Cow) {
let alfalfa = Hay.grow()
let hay = alfalfa.harvest()
animal.eat(hay)
}
func feed(_ animal: Horse) {
let root = Carrot.grow()
let carrot = root.harvest()
animal.eat(carrot)
}
func feed(_ animal: Chicken) {
let wheat = Grain.grow()
let grain = wheat.harvest()
animal.eat(grain)
}
}
farm.feed(cow) // 소가 건초를 먹습니다.
farm.feed(horse) // 말이 당근을 먹습니다.
farm.feed(chicken) // 닭이 곡물을 먹습니다.
이럴 때 바로 필요한 것이 바로 객체지향 프로그래밍의 추상화 (Abstraction)를 활용하는 다형성 (Polymorphism) 원칙입니다!
위에서 말했던 것처럼 꼭 프로토콜을 사용하지 않고 OOP로도 이 문제를 해결할 수 있을 거에요.
Animal이라는 공통의 클래스를 만들고 / 동물들은 공통 클래스를 상속받아, 함수를 오버라이드 하는 방식으로 코드를 짤 수 있습니다.
이때는 Any라는 키워드 혹은 클래스의 타입을 받는 타입 파라미터 등으로 동물이 먹는 먹이 타입의 다양함을 대응할 수 있습니다.
☑️ Apple은 이와 같이 서브 타입 (이 예시에서는 Any나 타입 파라미터 등)을 활용해 다형성을 구현한 방식을 subtype polymorphism이라고 정의합니다!
// MARK: - Using Any with OOP
class Animal {
func eat(_ food: Any) { fatalError("Subclass must implement 'eat'") }
}
class Cow: Animal {
override func eat(_ food: Any) {
guard let food = food as? Hay else { fatalError("Invalid food") }
print("소가 건초를 먹습니다.")
}
}
// MARK: - Using Type Parameter with OOP
class Animal<Food> {
func eat(_ food: Food) { fatalError("Subclass must implement 'eat'") }
}
class Chicken: Animal<Grain> {
override func eat(_ food: Grain) { print("닭이 곡물을 먹습니다.") }
}
하지만 이 코드도 문제가 있어요.
일단 맨 처음에 말했던 것처럼 상속은 클래스에서만 가능하기 때문에, 모든 코드가 클래스로만 구성되어 있습니다. (Enum이나 Struct에서는 이 방식을 쓸 수 없는 거죠..)
또한, 한 클래스에서 eat라는 메서드만 다형성이 필요한다는 보장도 없잖아요!
만약 다른 메서드에서도 필요한 Subtype가 있다면, 그럴 때마다 클래스의 타입 파라미터는 계속 길어질 거고 - 코드는 더욱 이해하기 어려워질 거에요.
즉, 프로토콜과 제네릭을 함께 활용하는 이상적인 다형성 구현 방법이 필요하게 된 것입니다.
☑️ Apple은 제네릭과 프로토콜을 활용해 다형성을 구현한 방식을 Parametric polymorphism이라고 정의합니다!
소, 말, 닭 등을 아우를 수 있는 공통의 인터페이스인 Animal 프로토콜을 만들어줄 수 있을 거에요.
이 Animal 프로토콜을 채택할 구현부에서는 먹이를 먹는 함수 eat을 구현하라고 명시해주고 추가로, 그 먹이는 공통의 타입인 Feed로 받도록 지정해주는 것입니다.
protocol Animal {
associatedtype Feed: AnimalFeed
func eat(_ food: Feed)
}
struct Cow: Animal {
func eat(_ food: Hay) { print("소가 건초를 먹습니다.") }
}
struct Horse: Animal {
func eat(_ food: Carrot) { print("말이 당근을 먹습니다.") }
}
struct Chicken: Animal {
func eat(_ food: Grain) { print("닭이 곡물을 먹습니다.")}
}
더 구체적으로 프로토콜의 인터페이스화와 관련된 글은 내용이 너무 길어져서 별도의 글로 넘기도록 할게요 ^__^
커밍쑨!
🧐 세 번째 질문에 대한 해답!
"Swift가 프로토콜을 지향한다는 것은 어떤 점에서 그렇게 말하는 거지? 그냥 프로토콜을 쓰면 프로토콜 지향 프로그래밍인가?"
: 프로토콜 (Protocol)을 추상화 도구 (Abstraction Tool)로써 활용하라는 의미입니다.
추상화 도구라는 말을 한 단계 들어가서 이해하면,
준수하는 타입의 기능을 정의하고 (Describes the Functionality of Conforming Types) / 아이디어와 구체적인 구현부를 분리 (Separates Ideas from the Implementation Details)하여 인터페이스를 정의하는 역할로 프로토콜이 활용된다는 의미겠네요!
4. 정리! 그래서 POP (Protocol-Oriented Programming)를 잘 지키며 개발하기 위해서는?
결국은 돌고 돌아 SOLID 원칙과 디자인 패턴으로 돌아오게 되는 것 같네요.
예전 글에서 저는 다음과 같이 글을 쓴 적이 있었습니다.
"Swift 특성이 객체 지향 프로그래밍(OOP) 언어인 것도, 함수형 프로그래밍 언어인 것도, 프로토콜 지향 언어인 것도.
모두 결국은 좋은 아키텍처를 만들기 위해, 다시 말해, 소프트웨어라는 이름에 걸맞은 가치를 지니기 위해, 더 쉽게 말해, 변경하기 쉽고, 이해하기 쉽도록 하고자 지니게 된 특성이자 기능이라고 받아들이면 되겠다."
위 말에 대한 정답은 없다고 생각해요.
OOP던, POP던, Design Pattern이던, Clean Architectrue던,
결국은 변경 사항이 발생했을 때 기존 코드에 대한 영향(리소스가)이 최소화되고, 확장에 있어 자유로울 수 있도록 하기 위해 개발자로서 어떠한 노력이라도 기울이는 것이 중요한 거죠.
이에 대해 POP (Protocol-Oriented Programming)도 이 과정의 정답을 찾기 위한 하나의 노력인 것이고요!
다만, Swift에서 이 노력을 위해 가장 범용적이고 / 유용하게 사용되는 것이 프로토콜 (Protocol)이라는 점은 분명한 것 같습니다. 디자인 패턴이던 SOLID던 그동안의 제 설명에도 프로토콜의 활용은 정말 중요하게 묘사했거덩요...
🧐 마지막 질문에 대한 해답!
"나는 평소에 프로토콜 지향 프로그래밍 (POP) 패러다임을 잘 따르면서 개발하고 있던 건가?"
: Swift로 프로그램을 개발하는 데 있어, 구조적 노력은 뒤로한 채 단순히 기능 구현만 우선하지 않았는지 돌이켜서 생각해 보길 바랍니다.
변경하기 쉽고, 이해하기 쉽고, 확장에 자유로운 코드를 짜기 위해서 프로토콜 (Protocol)의 사용을 고려했거나, 사용한 적이 있다면 여러분들은 Software Developer의 책임을 다하기 위해 노력한 것일 거예요!
'Swift, iOS Foundation > Swift 문법 총정리' 카테고리의 다른 글
[Swift] KVO (Key-Value Observing) 완전 정복하기 (feat. WKWebView progressBar) (0) | 2024.10.01 |
---|---|
[Swift] 자네 열거형(enum)을 CaseIterable로 사용해본 적이 있는가? (0) | 2024.08.05 |
[Swift] [weak self] 이젠 제대로 알고 사용하자! (feat. ARC 2탄) (0) | 2024.07.08 |
[Swift] ARC (Automatic Reference Counting) 완전 정복하기 (0) | 2024.07.06 |
[Swift] 제네릭 (Generic) 완전 정복하기 (0) | 2024.07.03 |