[Kotlin] 제네릭(Generic)에 대해

Janzizu·2023년 9월 4일
0
post-thumbnail

제네릭이란 무엇일까?

제네릭은 클래스 내부에서 사용할 자료형을 나중에 인스턴스를 생성할 때 확정한다.
제네릭을 사용하게 되면 객체 자체의 자료형을 컴파일때 체크하기 때문에 안정성을 높일 수 있다.

장점?

의도하지 않은 자료형의 객체를 지정하는 것을 막고, 객체를 사용할 때 원래의 자료형에서 다른 자료형으로 형 변환시 발생할 수 있는 오류를 줄여준다!

기본적인 제네릭

제네릭을 사용할 때는 <> 사이에 형식 매개변수를 넣어 선언하며 형식 매개변수는 자료형을 대표하는 용어로,T와 같이 대문자로 사용한다.

아주 간단한 예제를 살펴보자.

Class Box<T>(t: T) {
	var name = t
}

fun main() {
	val box1: Box<Int> = Box<Int>(1)
    val box2: Box<String> = Box<String>("test")
}

Box에서 T가 바로 형식 매개변수 이름이다.
무조건 강제적으로 T만 사용해야하는 것은 아니며, 단지 일종의 규칙일 뿐이다.

제네릭에서 사용하는 형식 매개변수 이름을 알아보자!

  • E : 요소(elevemnt)
  • K : 키(key)
  • N : 숫자(number)
  • T : 형식(Type)
  • V : 값(value)
  • S, U, V : 두 번째, 세 번째, 네 번째형식..

그럼 제네릭 클래스는 뭘까?

제네릭 클래스는 형식 매개변수를 1개 이상 받는 클래스이다.
위 예제에서 봤던 Box가 제네릭 클래스라고 할 수 있다 :)

그럼 자료형 변환에 대해 다음 예제를 알아보자!

  open class Parent
  class Child: Parent()
  class Cup<T>
  
  fun main() {
  	val obj1: Parent = Child() 
  	val opb2: Child = Parent() //자료형 불일치
  
  	val obj3: Cup<Parent> = Cup<Child>() //오류!
  	val obj4: Cup<Child> = Cup<Parent>() //오류!
  
  	val obj5 = Cup<Child>()
  	val obj6: Cup<Child> = obj5
  }

위 코드를 살펴보면 Parent는 상위클래스이고 하위클래스로 Child가 있다.
obj1은 Child형식으로 형변환이 될 수 있지만, obj2처럼 형변환을 할 수 없다. (부모-자식 뒤바뀜)

obj3,obj4를 보면 형식 매개변수인 T에 상위,하위클래스를 지정하더라도 서로 관련이 없는 형식이기 때문에 형식이 일치하지 않아 에러가 발생한다.
(3에서 Parent형을 상속받았는데 Child형을 넣어줌)

위처럼 상.하위 클래스를 형식 매개변수에 지정해 서로의 관계에 따라 형 변환이 가능하게 하려면 제네릭의 가변성을 주어야 하고 이 때 inout을 쓰게 된다.

In과 out

in과out에 대해 알아보기에 앞서 우선 가변성이라는 의미를 알아보자.

아 뭔가 변할 수 있다는 건가...
그럼 여기서 말하는 가변성은 무엇일까?
형식 매개변수가 클래스 계층에 영향을 주는 것.

제네릭에서는 클래스 간에 상위 하위 개념이 없어 서로 무관하며, 이 때 상위/하위에 따른 형식을 주려면 가변성의 3가지 특징을 이해해야 한다.

1) 공변성(Covariance) = T가 T의 하위 자료형이면, C<T>는 C의 하위 자료형이다.
2) 반공변성(Contravariance) = T가 T의 하위 자료형이면, C는 C<T>의 하위 자료형이다.
3) 무변성(Invariance) = C와 C<T>는 아무 관계가 없다.

예를 들어보면,

왼쪽부터 하나하나 살펴보겠다.
Int클래스는 Number클래스의 하위 클래스이다.

class Box<out T>로 정의하면 Box<Int>Box<Number>의 하위 자료형이 된다.
이것을 공변성이라고 한다.
<out T> 키워드는 자바에서 <? extends T>와 같은 문법으로 T가 Number이기 때문에 그 하위타입인 제네릭 Int 타입이 컴파일 될 수 있도록 허용한다.
즉 자식(하위타입)을 받을 수 있게 허용한다.

반대로 in을 사용해 class Box<in T>로 정의하면 Box<Number>Box<Int>의 하위 자료형이 되며 이것을 반공변성이라고 한다.
자바에서는 <? super T>와 같은 의미이며, 상위타입도 전달받을 수 있게 되는 것이다.

좀 더 자세히 알아보도록 하겠다.

무변성

위에서 알아보았던 형식 매개변수에 in이나 out등으로 공변 혹은 반공변성을 따로 지정하지 않으면 무변성으로 제네릭 클래스가 선언된다.

class Box<T>(val size: Int)

fun main() {
	val anys: Box<Any> = Box<Int>(10)
	val nothings: Box<Int> = Box<Nothing>(20)
}

위 코드는 Any, Int, Nothing간의 상하관계를 잘 따졌음에도 불구하고 Box가 무변성이기 때문에 자료형 불일치 오류를 발생시킨다.

공변성

형식 매개변수의 상하 자료형 관계가 성립하고, 그 관계가 그대로 인스턴스 자료형 관계로 이루어지는 경우.
예를들어 Int가 Any의 하위 자료형일 때 형식 매개변수 T에 대해 공변적이라고 하며, 이 때는 out키워드를 사용한다.

class Box<out T>(val size: Int)

fun main() {
	val anys: Box<Any> = Box<Int>(10)
	val nothings: Box<Nothing> = Box<Int>(20) //자료형 에러
}

out키워드에 의해 형식 매개변수가 공변적으로 선언되어 상하 자료형 관계가 성립되었다.
Any의 하위클래스인 Int는 공변성을 가지므로 Box<Any>Box<Int>자료형을 할당할 수 있게 된 것이다!

반공변성

위 예제의 out을 in으로 바꾸면 어떻게 될까?
이 때 자료형의 상하관계가 반대로 되며 인스턴스 자료형이 상위의 자료형이 된다.

class Box<in T>(val size: Int)

fun main() {
	val anys: Box<Any> = Box<Int>(10) //  자료형 에러
	val nothins: Box<Nothing> = Box<Int>(20)
}

in 키워드에 의해 상하 관계가 반대로 되며 Box<Int>자료형의 상위 자료형이 Box<Nothing>이 되므로 객체를 생성할 수 있게 된다!

자, 그럼 마지막으로 하나만 더 알아보도록 하자!

out은 (read-only),
in은 (write-only)

공변성을 가지게 되면 값에 대해 read만 가능하고 write이 불가능해진다.
이유가 무엇일까?

fun test(box: Box<out Animal>) {
	box.item = Animal() //오류!!!
	val obj: Animal = box.item // item get!!
}

위 코드로 살펴보자.
컴파일러는 Animal이 부모인것으로 인지하고 있다.
그렇다는 것은 read할 때에는 Animal, Cat, Dog, Duck 등 중 하나인 것이라고 생각하며,
이것들을 전부 포함해주는 Animal로 할당하므로 문제가 발생하지 않는다.
하지만 write의 경우를 생각해보면,

val dog: Box<Animal> = Box<Dog>()
val cat: Box<Animal> = Box<Cat>()

out을 보면 Animal로 지정되어 있으므로 Dog, Cat등이 다 올 수 있으며 따라서
실제 Type을 모르기 때문에 write을 하면 문제가 발생하게 된다.

그럼 반공변성에 대해서는 어떨까?
반공변성을 가지게 되면 값에 대해 write이 되고 read가 불가능해진다.

fun test(box: Box<in Animal>) {
	box.item = Animal()
}

Box가 Animal의 하위타입도 포함하여 다양한 타입을 받아들일 수 있음을 의미한다.
따라서 box.item = Animal() 과 같이 Animal객체를 box.item에 할당할 수 있게된다.
하지만 read의 경우는 어떻게 될까?

val animal: Animal = box.item

위의 코드는 에러가 발생한다.
왜냐하면 실제 animal에는 어떤 타입의 객체가 들어있는지 모를 수 있기 떄문에 컴파일러가 값을 안전하게 읽을 수 없기 때문이다.

0개의 댓글