Professional Documents
Culture Documents
프로토콜지향 프로그래밍
182
앨런 케이(Alan Kay)가 이런 말을 했다고 합니다. "내가 객체지향 프로그래밍을 고안해 냈
을 때, C++을 염두에 둔것은 아니었다.", "Java는 MS-DOS 이후로 컴퓨팅에 닥친 가장 큰
재앙이다."
네델란드 학회와 튜링상 수상자인 에드거 딕즈트라(Edsgar Dijkstra)는 객체지향 프로그래
밍에 대해서 매우 비판적이었다고 합니다.
(다음의 인용문은 지어낸 말이 확실함에도 불구하고 자주 인용되어 회자되어 왔습니다. "객
체지향 프로그래밍은 캘리포니아에서 생겨난 것 중에서 예외적으로 나쁜 아이디어이다.")
물론 프로그래머들이 "내가 사용하는 이 언어는 대단하고 네가 사용하는 그 언어는 쓰레기
야"라고 주장하는 사례를 찾아 보는 것이 어려운 것은 아닙니다. 더 많은 관심이 가고 믿을
만한 설명은 아마도 객체지향 프로그래밍을 실제로 하고 있는 (Gang of Four라 불리는 디
자인 패턴(Design Patterns)의 저자들과 같은) 사람들로 부터 나온 것일 겁니다. 이들을 포
함해서 다수의 사람들이 상속의 오남용에 대해서 경고하고 있습니다. 이들이 제시하는 원
칙들 중의 하나가 "favor composition over inheritance"입니다. 의역하면 정말로 상속이
필요한 경우가 아니라면 상속 대신에 컴포지션 패턴을 사용하라는 것입니다. 컴포지션은
상속 관계 없이 클래스들을 혼합하여(composition) 사용하는 것을 말합니다. 따라서 어떤 클래
스가 다른 클래스에 정의되어 있는 메서드를 사용해야할 때, 그 클래스의 인스턴스를 생성
하여 (또는 대상 클래스의 메서드를 직접 호출하거나) 메서드를 실행하여 나온 결과값을 이
용하라는 것입니다. 클래스의 프로퍼티를 다른 클래스의 인스턴스로 정의하는 것은 이러한
활용에서 대상 클래스의 메서드를 쉽고 빠르게 활용할 수 있도록 합니다. 상속과 콤포지션
모두 (유지보수의 악몽을 낳는) 메서드가 불필요하게 중복되는 것을 피하기 위해서 입니다.
도대체 상속의 어떤 점이 잘못되었다는 것일까요? 일반적으로 세 가지 비판이 주를 이룹니
다.
• 상속은 현실의 사물을 표현하기에 별로 좋은 방법이 아닙니다. 시간이 지남에 따라
유연성이 떨어집니다.
• 일상적으로 구현되는 상속은 안전하지 않거나 비효율적인 데이터 공유를 자동적으
로 내포하게 됩니다.
• 상속은 서로 다른 객체간의 강한 커플링(tight coupling)을 피해야한다는 패러다임에 너무
강압적입니다.
183
그리고 클래스와 상속은 실제 세상을 표현하는 좋은 방법이라고 여겨졌습니다. 왜냐하면
범주와 분류의 시스템(생물학에서의 분류학과 도서관에서의 책 분류 방법)에서의 확실한
성공 사례를 보여 주었기 때문입니다.
당연히 생물학 시스템도 예외가 있습니다. 새이지만 날지 못하는 펭귄이나. 포유류이지만
날수 있는 박쥐, 포유류이지만 바다에서 살고 있는 고래. 이런 예외 경우가 성가시긴해도,
소프트웨어 클래스의 상속과 오버라이딩을 통해서 소프트웨어적으로 처리할 수 있다고 생
각하였습니다.
소프트웨어의 발전은 천천히 진행되는 반면 변화는 요구사항은 매우 빠르게 변화하고 있습
니다. 상속에 관련된 문제들 중의 한 가지는 상속 자체의 유연성이 떨어진다는 것입니다.
(크레이 셔키(Clay Shirky)의 논문 “Ontology is Overrated”에서 인용한) 종교에 관한
Dewey Decimal 카테고리 시스템(도서관에서 책을 분야별 번호룰 분류하는 시스템)을 살
펴보면 카테고리 변경이 일어났을 때 미치는 영향에대해 잘 알 수 있습니다.
200: Religion
184
DD: Germany
DE: Mediterranean
DF: Greece
DG: Italy
DH: Low Countries
DJ: Netherlands
DS: Asia
DT: Africa
DU: Oceania
DX: Gypsies
185
자동적으로 공유되는 데이터
계층적 상속관계의 또 다른 문제점은 어떤 클래스와 그 클래스의 상위 클래스들 모두가 힙
(heap) 메모리에 데이터로 저장되어 있는 하나의 복사본에 대한 레퍼런스를 가진다는 것입니
다. 각각의 클래스에 포함되어 있는 메서드들은 근본적으로는 동시에 이 데이터에 접근할
수 있게 되어 동시 접근에 따른 전형적인 힘든 상황들을 만들어 냅니다. 락(lock)과 같은 메커
니즘을 통해 이런 문제를 방지할 수 있지만 안전하고 신뢰할 만한 수준으로 구현하는 것은
복잡하고 어려운 일입니다. 프로세싱 오버 헤드를 증가시키고 버그를 만들기도 합니다.
상속의 심한 간섭
상위 클래스가 하위클래스에 너무 심하게 간섭한다고 불평하는 것은 모순적인 면이 있습니
다. 왜냐하면, 객체지향 프로그래밍의 전도사들이 인캡슐레이션과 보안에 거의 과대망상적
으로 추종하도록 주장하였고 많은 언어(최근의 언어들에서 다소 완화되고 있다고 하더라
도)에 이러한 점들이 포함되었습니다. 그리고는 인캡슐레이션과 상속에 대한 고집이 객체
지향 프로그래밍의 근본 원칙인 것처럼 되어버렸습니다.
– Joe Armstrong
2.프로토콜의 장점
프로토콜 지향 프로그램의 접근 방식은 많은 상황에서, 프로토콜을 구조체와 열거형과 함
께 사용하면 클래스를 사용함으로써 발생하는 부작용 없이 클래스가 할 수 있는 일들을 해
낼 수 있다는 생각을 기반으로 합니다.
스위프트 설계의 일부분으로 구조체와 열거형은 클래스가 하던 대부분의 일을 할 수 있게
개발되었습니다.
186
이 점은 구조체에서 가장 확실하게 볼 수 있는데 클래스와 아주 흡사하지만 결정적인 두 가
지 차이점이 있습니다. 구조체는 상속(과 상속에 따르는 부담)이 없으며 밸류타입(Value
Type)입니다. 구조체의 새 인스턴스는 데이터를 참조하는 대신에 복사합니다. 따라서 자동적
으로 공유되는 데이터의 문제가 발생하지 않습니다.
구조체는 클래스처럼 정보를 인캡슐레이션 할 수 있고 프로퍼티와 메서드와 같은 동작이
가능합니다. 클래스처럼 인스턴스를 만들 수 있습니다. 코드에 대한 접근제어 메커니즘을
제공합니다. 구조체 내의 코드에서 네임 충돌을 피할 수 있도록 네임스페이스를 제공합니
다. 추상화를 위한 근간을 제공하며 클래스처럼 익스텐션이 가능합니다.
열거형은 아마도 구조체 만큼은 아닐지 몰라도, 비슷한 능력을 제공합니다. 열거형도 구조
체와 동일하게 메서드와 인스턴스를 생성하는 능력이 있고 저장 프로퍼티와 비슷한 기능을
하는 연관 값(associated values)를 제공합니다. 열거형도 밸류타입이어서 레퍼런스타입이 가진
자동 공유 문제가 없습니다.
메서드와 프로퍼티의 구현을 정의할 수 있는 프로토콜 익스텐션은 다중 상속도 가능합니
다. 프로토콜은 클레이 셔키“Clay Shirky”가 말한 책장도 없고“no shelf”, 카테고리도 없는“no category”
처럼 동작합니다. 클래스 상속보다 프로토콜을 사용하는 것의 차이 점은 레스토랑에 가서
정해진 세트메뉴를 주문하는 것과 하나씩 원하는 것을 골라서 주문하는 것의 차이에 비유
할 수 있습니다.
187
A Basic Protocol Extension
아래와 같은 프로토콜을 정의했다고 가정해 봅시다.
protocol TypicalSquirrel {
var nameOfIndividual: String { get }
var nameOfSpecies: String { get }
var skinHasFur: Bool { get }
var breathesAir: Bool { get }
var whyYouShouldNotPetThem: String { get }
func sayWhetherICanFly()
}
func sayWhetherICanFly() {
print("I am a typical squirrel and I can walk and climb trees but no fly")
}
}
188
발생할 수 있다는 문제의식에서 부터 시작된 접근 방식입니다. 여기에 포유류에 관한 사항
들이 있는가요? 신뢰할 수 있고 구체적으로 의도를 가지고 구현된 작은 컴포넌트를 사용하
는 것이 더 안전합니다.
이제 다람쥐 인스턴스를 생성하기위한 Squirrel 구조체를 정의해 보겠습니다.
TypicalSquirrel 프로토콜을 적용하여 보통의 다람쥐가 가진 속성과 동작을 가져올 수 있
습니다.
struct Squirrel: TypicalSquirrel {
var nameOfIndividual: String = ""
var nameOfSpecies: String = ""
}
pete.sayWhetherICanFly() // 출력: I am a typical squirrel and I can walk and climb trees
but no fly
날다람쥐에 대한 처리
여기까지는 문제가 없었는데, 이제 높은 산을 올라 가다가 다른 종류의 “북부 날다람쥐”를
발견하였습니다.
이 종류는 정말 희귀한 종입니다. 아마 이 부근에서 발견할 수 있는 유일한 날다람쥐 종류
일 것입니다. 따라서 이 다람쥐를 위해서 프로토콜을 만드는 것은 적당하지 않으므로 그냥
FryingSquirrel 구조체를 만들도록 하겠습니다. 역시 TypicalSquirrel 프로토콜을 사용
하면서 몇 가지 것들을 오버라이드할 것입니다. whyYouShouldNotPetThem에 프로토콜 익
189
스텐션에서 정의 값과 다른 값을 적용하고 sayWhetherICanFly 메서드를 오버라이드하여
새롭게 구현하도록 하겠습니다.
struct FlyingSquirrel:TypicalSquirrel {
var nameOfIndividual: String = ""
var nameOfSpecies: String = ""
var whyYouShouldNotPetThem: String = "They are not friendly and have sharp teeth"
func sayWhetherICanFly() {
print("I am flying squirrel and I can walk and climb trees and also fly, or at
least glide")
}
}
print(george.whyYouShouldNotPetThem) // 출력: They are not friendly and have sharp teeth
george.sayWhetherICanFly() // 출력: I am flying squirrel and I can walk and climb trees
and also fly, or at least glide
조건부 프로토콜
여러분이 캘리포니아 몬트레이에서 일하고 있는 생물학자라고 가정해 보겠습니다. 여러분
은 대부분의 시간을 바닷가 해변이나 보트위에서 보냅니다. 대부분의 장소에서 사람들이
볼 수 있는 포유류는 대부분 땅위에 살고 있습니다. 하지만 여기서는 반대의 상황입니다.
190
여기서 사람들 근처에서 볼수 있는 포유류는 대부분 바다에 살고 있습니다. 이것이 바로
"포유류"와 같은 매우 큰 크기의 복잡한 클래스를 사용하고 그 것을 상속받는 것에 대한 문
제점의 예시입니다.
따라서 우리는 대신 MarineMammal 프로토콜을 사용하겠습니다. 어떤 (바다사자나 물개같
은) 해양 포유류는 땅위를 걸을 수 있으므로 두번째 프로토콜인 CanWalkOnLand을 정의해
야 합니다.
몇 가지 동작을 정의하기 위해서 프로토콜 익스텐션을 사용할 것인데, 다만 이 동작들은 그
구조체가 두 프로토콜에 대응하는지 여부에 따라 달라집니다.
먼저 marineMammal 프로토콜을 정의하겠습니다.
protocol MarineMammal {
var nameOfIndividual: String { get }
var nameOfSpecies: String { get }
var skinHasFur: Bool { get }
var breathesAir: Bool { get }
}
191
print ("I can walk on land.")
}
}
cynthia.iCanWalkOnLand() // 컴파일러 에러
192
Mammal의 경우에는 이 메서드는 아래와 같고
func sayCanIWalkOnLand() {
print("I am a Mammal and I CAN walk on land")
}
func sayCanIWalkOnLand() {
print("I am a Marine Mammal and I CANNOT walk on land")
}
protocol MarineMammal {
func sayCanIWalkOnLand()
}
extension Mammal {
func sayCanIWalkOnLand() {
print("I am mammal and I CAN walk on land")
}
}
extension MarineMammal {
func sayCanIWalkOnLand() {
print("I am a Marine Mammal and I CANNOT walk on land")
}
}
193
스위프트 표준 라이브러리의 익스텐션
가장 자주 프로토콜 익스텐션이 사용되는 곳이 스위프트의 표준 라이브러리나 써드파티
(Third-party) 라이브러리의 메서드를 제공하기 위해서입니다.
하나의 타입뿐만 아니라 여러개의 타입에 기능을 추가하는 것이 한 번에 가능해졌습니다.
예를 들어 CollectionType 타입의 프로토콜에 익스텐션을 사용하면 추가된 기능은 배열과
딕셔너리와 집합 모두에 (이 데이터 타입들은 CollectionType 프로토콜을 따르고 있으므
로)에 적용됩니다.
프로토콜의 셀프 요구사항
프로토콜과 구조체가 클래스에 비해 더 우월한 경우가 있다는 아브라함의 주장에서 보여준
예제를 보면, 두 개의 값이 어떤 순서로 정렬되어야 하는지 결정하는 함수의 예가 있었습니
다. 배열을 정렬하거나 배열에서 바이너리 검색을 하는 데에 보통 이런 종류의 함수가 사용
됩니다. 이런 정렬을 하는 일반적인 솔루션을 만드는 것은 어려운 문제입니다. 왜냐하면 어
떤 타입의 값을 정렬하는 적절한 방법이 한 가지 이상의 방법이 있기 때문입니다. (정수
5는 정수 4보다 항상 큽니다. 하지만 문자열 “05”와 “4”의 경우에는 어떤 순서로 정렬해야
하는가?라는 문제가 남습니다.)
아브라함이 선택한 솔루션은 프로톨과 구조체를 사용하는 것이다.
protocol Ordered {
func precedes(other: Self) -> Bool
}
194
스위프트에서 소문자 self는 현재 인스턴스 자신에 대한 레퍼런스이며, 첫 글자가 대문자
인 Self는 인스턴스를 생성하는 타입에 대한 레퍼런스입니다. where절에 사용되어 단순히
셀프 요구사항(Self requirement)를 설정하거나 현재 타입을 뜻할 수 있습니다.
195