공변성과 반공변성은 무엇인가?

Stephan BoyerWhat are covariance and contravariance?을 번역한 글이다.


공변성과 반공변성은 무엇인가?

서브타이핑은 프로그래밍 언어 이론에서 까다로운 주제다. 공변성과 반공변성은 오해하기 쉬운 주제이기 때문에 까다롭다. 이 글에서는 이 용어를 설명하려고 한다.

이 글에서는 다음과 같은 표기법을 사용한다.

  • A <: BAB의 서브타입이라는 뜻이다.
  • A -> B는 함수 타입으로 함수의 인자 타입은 A며 반환 타입은 B라는 의미다.

동기부여 질문

다음과 같은 세 타입이 있다고 가정하자.

Greyhound <: Dog <: Animal

GreyhoundDog의 서브타입이고 DogAnimal의 서브타입이다. 서브타입은 일반적으로 추이적 관계(transitive)를 갖는다. 그래서 GreyhoundAnimal의 서브타입이라 할 수 있다.

여기서 질문이다. 다음 중 Dog -> Dog의 서브타입이 될 수 있는 경우는 어느 것일까?

  1. Greyhound -> Greyhound
  2. Greyhound -> Animal
  3. Animal -> Animal
  4. Animal -> Greyhound

이 질문에 어떻게 답할 수 있을까? Dog -> Dog 함수를 인자로 받는 f 함수를 살펴보자. 반환 타입에 대해서는 크게 생각하지 않는다. 구체적으로 적어보면 다음과 같다. f : (Dog -> Dog) -> String.

이제 f를 다른 함수인 g와 함께 호출해보자. g에는 위에서 나열했던 각각의 함수를 넣어서 어떤 일이 나타나는지 관찰한다.

1. g : Greyhound -> Greyhound로 가정하면 f(g)는 타입 안전(type safe)한가?

아니다. 함수 f는 인자 g를 사용하면서 Dog의 다른 서브타입, 예를 들면 GermanShepherd를 사용해서 호출할 수도 있기 때문이다.

2. g : Greyhound -> Animal로 가정하면 f(g)는 타입 안전한가?

아니다. 1과 동일한 이유다.

3. g : Animal -> Animal로 가정하면 f(g)는 타입 안전한가?

아니다. f에서 인자 g를 호출하면서 개가 어떻게 짖는지 그 반환 값을 얻으려고 할 수 있다. 하지만 모든 Animal이 짖는 것은 아니다.

4. g : Animal -> Greyhound로 가정하면 f(g)는 타입 안전한가?

그렇다. 이 경우는 안전하다. f 함수는 인자인 g를 호출할 때 어떤 종류의 Dog든 사용할 수 있다. 모든 DogAnimal이기 때문이다. 또한 반환값은 Dog로 가정할 수 있는데 모든 GreyhoundDog이기 때문이다.

무슨 일이 일어나고 있나요?

결과적으로 다음 경우에 안전하다.

(Animal -> Greyhound) <: (Dog -> Dog)

반환 타입은 간단하다. GreyhoundDog의 서브타입이다. 하지만 인자 타입은 반대다. AnimalDog의 수퍼타입이다!

이 독특한 동작 방식을 적당한 용어를 사용해서 설명한다. 함수 타입에서 반환 타입은 공변적(covariant) 이고, 인자 타입은 반공변적(contravariant) 이다. 반환 타입의 공변성은 A <: B(T -> A) <: (T -> B)로 적용된다는 뜻이다. (A<:의 좌측에, B는 우측에 남아 있다.) 인자 타입의 반공변성은 A <: B(B -> T) <: (A -> T)로 적용된다는 의미다. (AB가 위치를 바꾸게 된다.)

재미있는 사실 타입스크립트에서는 인자 타입이 이변적(bivariant), 즉 공변성과 반공변성을 동시에 지닌다. 뭔가 말이 안되는거 같겠지만 말이다. (TypeScript 2.6부터 --strictFunctionTypes 또는 --strict를 사용하면 이 문제를 해결할 수 있다.) Eiffel은 인자 타입을 반공변적이 아닌 공변적으로 잘못 구현했다.

다른 타입은?

또 다른 질문이다. List<Dog>List<Animal>의 서브타입이 될 수 있을까?

답변하기 좀 미묘하다. 만약 목록이 불변이면 맞다고 답할 수 있다. 하지만 가변적이라면 당연히 안전하지 않다!

이유를 생각해보자. 나는 List<Animal>이 필요한데 List<Dog>를 넘겨줬다고 해보자. 나는 당연히 List<Animal>을 갖고 있다고 생각하고 Cat을 집어넣었다. 이제 List<Dog>Cat이 들어있게 된다! 타입 시스템은 이런 동작을 허용하지 않을 것이다.

불변 목록의 타입이라면 타입 파라미터가 공변적일 수 있지만 가변 목록의 타입은 반드시 공변적이지도, 반공변적이지도 않아야(invariant) 한다.

재미있는 사실 자바에서의 배열은 가변적이면서도 공변적이다. 물론 부적절하다.


추가로, 원문의 덧글 중에 시각적으로 잘 설명하는 자료가 있었다.

김용균

안녕하세요, 김용균입니다. 문제를 해결하기 위해 작고 단단한 코드를 작성하는 일을 합니다. 웹의 자유로운 접근성을 좋아합니다. 프로그래밍 언어, 소프트웨어 아키텍처, 커뮤니티에 관심이 많습니다.

이 글 공유하기

이 글이 유익했다면 주변에도 알려주세요!

페이스북으로 공유하기트위터로 공유하기링크드인으로 공유하기Email 보내기

주제별 목록

같은 주제의 다른 글을 읽어보고 싶다면 아래 링크를 확인하세요.

May 10, 2018

Stop and smell the roses 🌹

오늘이 마지막 출근이다. 1년 조금 넘는 기간을 다닌 이번 회사에서는 이전 다녔던 곳과는 확연히 다른 경험을 했다. 규모도 달랐고 프로세스도 갖춰져 있었다. 다른 부서와 함께 일하는 경험은 처음이었다. PM과 BA와 함께 일하고, 아키텍트에게 리뷰도…

April 12, 2018

C# 으로 배우는 적응형 코드

요즘 사무실에서 비는 시간이 좀 많이 있어서 책을 가져다두고 읽었다. 가볍게 읽으려고 읽었던 책을 가져가야지 했는데 지금 회사에서는 C#을 전혀 쓰지 않고 있으니 리마인드도 할 겸 읽게 되었다. 베타리딩을 포함해서 3번째 읽는데 그래도 또 배우는 게…