이제보니 IntelliJ Edu 에 Atomi Kotlin 이란 것이 있었다.

내가 코틀린을 모르는 상태로 어찌저찌 docs를 보면서 문제를 풀고, 개념을 잡고 있었는데...

 

지금 보니 조금 멍청한 선택이었다.

 

애초에 기본 문법을 위한 코스가 따로 있었다.

 

Atomi Kotlin 으로 다시 시작하도록 하겠다.

 

-------------

 

Atomi Kotlin 으로 학습을 시작하기 전에, 일단 Kotlin docs를 보면서 의문을 가졌던

 

wildCard , PECS 등등에 대해 대충 정리하고 넘어가겠다.

 

일단 PECS

는 Produce Extends, Consumer Super 를 의미한다.

 

이 때 이 PECS 는 파라미터로 넘어가는 놈! 의 역할을 의미한다. (함수 안에서 이 파라미터가 어떤 역할인지를 생각 해보자.)

 

만약 파라미터로 넘어가는 "이 놈" 이 값을 뱉어내는 놈이다. -> 값을 생산하는 놈이다.

만약 파라미터로 넘어가는 "이 놈" 은 값을 뱉지는 않고, 되려 가져가는 놈이다. -> 값을 소비만 하는 놈이다.

 

사실 PECS는... 리스트(List)를 파라미터로 넘겼을 때 "리스트 관점에서" 생산자인지 소비자인지를 의미한다. 라고 설명된다.

이 리스트가 "나" 라고 생각 해보자. 내가 마트에 가서 물건을 골라 내 리스트에 막 넣는다. 이 때 나는 당연히 소비자이다.

내가 반대로 장바구니를 들고 가서 막 애들한테 나눠준다. 나는 "나눠주는, 생산자" 이다.

 

"나"는 이 함수 안에서, 생산자이다.

"나"는 이 함수 안에서, 소비자이다. 

 

일단, "잠깐 이 놈은 값을 생산도 하고 소비도 하는데?" 라고 할 때는 이 PECS 에서 일단 빼 놓고 얘기하자.

(상관은 없다. 하지만 이 것이 와일드카드랑 섞이기 시작하면 혼란이 찾아온다. 나처럼)

 

간단히 생각하자.

 

만약 값을 생산하는 놈은, 함수에 전달되서, "값을 뱉어내는 놈" 이다.

 

즉 T extends Animal 이라고 할 때,

T 는 반드시 Animal 을 상속하고 있는 놈이다. 따라서 Animal class에 있는 메서드와 필드를 기대할 수 있다.

즉 함수에서 이 T 타입의 파라미터를 사용할 때 아무런 문제가 없다.

 

반대로 이 T 에다가 값을 넣어보려 해보자. Animal 중 뭔지 알 수는 있나? 함수는 정확한 타입을 맞출 수 없다.

따라서 이 T 는 함수에 값을 제공하면 제공했지, 함수에서 값을 받아들일 놈은 아니란 거다. 

 

따라서 요놈은 "생성자" Producer이며, 파라미터로 넘겨질 때는 제네릭 사용시 부모 클래스를 extends 하면서 들어가야 한다.

 

 

반대로 값을 받아들이는 제네릭 T super Cat 이 함수의 파라미터로 넘어간다고 해보자.

요놈은 Cat 의 super 타입인 것이 "보장 돼 있다" 예를 들어 T 에 Animal 이 들어가면,

 

Animal 클래스에 Cat 인스턴스를 넣을 수 있나 없나? 당연히 넣을 수 있다.

 

반대로 이 T 를 함수에서 사용하려 할 때는 골치가 아파진다. 이 T 는 Cat의 super 타입이지만, 어떤 필드를 갖고 있고, 어떤 메서드를 갖고 있는지 알 수가 없다. 이 T 타입의 인스턴스를 사용 할 수 있을까?

 

따라서 이 T 타입의 인스턴스는, 함수에서 값을 가져가는(소비하는) 역할을 하지, 함수에 값을 제공하는 Producer 역할을 하는 것이 아니다.

 

따라서 Consumer 는 Super 다.

 

이런 식으로 PECS를 정리하고 가자...

 


여기서 WildCard ? 를 얹어 보자.

 

WildCard 는 말 그대로, 뭐든지 들어갈 수 있다. 라는 것을 표기하는 것이다. 이 것이 함수의 파라미터에 super, extends 와 같이 쓰일 때는 다른 작용을 한다.

 

첫 째로 WildCard로 넘어온 타입이 뭔지는 "당연히 알 수 없다." 

 

? extends Animal 이라고 해보자.

 

함수의 파라미터로 ? 타입의 인스턴스가 넘어갔다.

 

아니 잠깐만. 이게 T extends Animal 상황이랑 뭐가 다르지? 하는 의문이 들 것이다.

 

다르다.

 

함수에 파라미터로

 

Collection<? extends Animal>

 

요런 식으로 인스턴스를 받아들이면, 이 Collection 인스턴스에는 값을 꺼낼 순 있어도 값을 넣을 순 없다.

 

반대로 

 

Collection<? super Cat>

 

요런 식으로 인스턴스가 함수로 넘어가면, 이 인스턴스에는 값을 넣을 순 있어도, 값을 꺼낼 수는 없다.

 

List<? super Cat> 에다가 List<Animal> 인스턴스를 넣으면,

 

이 List<Animal> 타입의 인스턴스는 "소비자(왕) 이시다." 왕의 값을 감히 빼 볼 수 없다.

 

animalList.get(0) 요런식으로 값을 꺼내려고 하는 순간 바로 에러가 발생한다.

하지만 animalList.add(cat) 은 가능하다. 

 


 

이제 Kotlin 의 in 과 out 으로 넘어 가보자.

 

만약 자바에서 

 

void demo(Source<String> strs) {
  Source<Object> objects = strs;


  // ...
}

는 불가능 하다.

String s = "자바의 경우"

Object object = s 

 

왜냐면 Object는 String의 상위 클래스이기 때문이다.

 

하지만 

Source<Object>는 Sourc<String> 의 상위 타입이 아니다.

이것을 우린 "무공변성" 이라고 한다.

 

Object 는 String 의 상위 타입이니까?

Source<Object> 도 Sourcde<String> 의 상위 타입이겠지?

 

이게 허락되면 우리는 "공변성" 이 있다. (함께 변하는 관계) 라고 한다.

 

반대로 Object는 String의 상위 타입이야.

잉? 그런데 Source<String> 이 Source<Object> 의 상위 타입이야? 

이게 말이 안돼 보이는데 요런 상황이 찾아오면 이것을 "반공변성" (정 반대로 변하는 관계) 라고 한다.

 

 

위의 자바 코드를 가능하게 하려면 이렇게 해야한다.

 

 void demo(Source<? extends Object> strs) {
  Source<?> objects = strs;
  // ...
}

 

하지만 코틀린에서는 in, 과 out 을 사용해서 다음과 같이 사용할 수 있다.

 

interface Source<out T> {
    fun nextT(): T
}

fun demo(strs: Source<String>) {
    val objects: Source<Any> = strs // This is OK, since T is an out-parameter
    // ...
}

여기서 out 의 의미는 T 제네릭을 같는 이 클래스 타입 Source 는 T 타입을 "반환" 할 수는 있지만(out)// T 타입을 파라미터로 받아들일 수는 없다! 를 의미한다.

 

그럼 val objects: Source = strs // 이건 왜 가능한 것인감??

 

Source<Any> 클래스 안에 Source<String> 타입의 "인스턴스" 가 들어가 있다고 생각해보자.

그럼 반환하는 T 는 자동으로 Any 로 나와야 하는데, String 은 Any 하위이기 때문에 Any 타입의 반환에 String 형태의 인스턴스를 반환해도 아무런 문제도 없다.

 

따라서 위와 같은 "공변" 현상이 일어날 수 있는 것이다.

 

 

일단 여기까지만 정리하도록 하겠다. 저녁 약속이 있어 나가봐야... 더 자세한 정리는 다음 기회에... 

블로그 이미지

맛간망고소바

,