• 카테고리

    질문 & 답변
  • 세부 분야

    프로그래밍 언어

  • 해결 여부

    해결됨

추상클래스의 공변 / 반공변, 추상 제네릭 일급컬랙션 리팩토링 에 대한 질문입니다.

23.09.23 20:24 작성 23.09.25 10:10 수정 조회수 416

2

믿고보는 태현님강의 다른강의 듣다가 사두고 이제서야 초반을 달리고 있습니다. 역시나 그간 알다가 까먹고 했던부분 확실하게 다지고 가는 느낌이 듭니다.

저는 공변/반공변 예제 중 [꺼내기 / 저장] 각각의 기능만 하는 두가지 일급컬랙션을 따라하다가, 추상클래스 AbstractCage 를 둔다면 어떨지 아래처럼 한번 구성했었습니다.

 

abstract class AbstractCage<T>(
    protected val things: MutableList<T>,
)
/** 초기화를 통해 아무거나 저장 후, 꺼낼수만 있는 케이지 */
private class ProduceCage<out T>(
    vararg things: T
) : AbstractCage<T>(mutableListOf(*things)) {
// ProduceCage T는 'out(공변)' 이지만 AbstractCage T는 무공변 선언되어 대입불가.

    fun getFirst(): T =
        this.things.first()

    fun getAll(): List<T> =
        this.things.toList()
}
/** 아무거나 저장 만 가능한 케이지 */
private class ConsumeCage<in T>(
    vararg things: T
) : AbstractCage<T>(mutableListOf(*things)) {
// ConsumeCage T는 'in(반공변)' 이지만 AbstractCage T는 무공변 선언되어 대입불가.

    fun put(vararg things: T) {
        this.things.addAll(things)
    }
}

문제는 상속받을때의 : AbstractCage<T>(..things) 선언부에 부모 T 타입이 무공변이라 자식 T 타입이 적절치 못하다고 애러가 나는데요,

생각해보니 양쪽의 하위타입에서 이도저도 아닌 T 타입을 강요하는데, 이런 방식이 좋은 접근방식인가? 하는 의구심도 들고, 현업에서나, 또는 올바른 접근방법이 궁금해 질문하게 되었습니다. AbstractCage 의 타입파라미터를 <in T1, out T2> 이렇게 두는것도 이상하구요,

이렇게 일급컬랙션 의 공통부분을 만들어야 될때 어떻게 접근해서 풀어내실지 의견이 궁금해서 남기게 되었습니다.

읽어주셔서 감사합니다.

답변 1

답변을 작성해보세요.

3

안녕하세요 Truestar님~~ 정말 좋은 질문 감사드립니다! 😊

맞아요~~ 이게 제네릭 클래스를 상속 받을때 타입 파라미터에 대한 "변성"도 동일하게 적용해줘야 하다보니

타입 파라미터 <T> 를 갖는 클래스를 상속받는 <in T> 클래스를 만들 수는 없죠!

 

결국 저희가 관심을 갖는 부분은 "일급 컬렉션의 공통 부분을 어떻게 만들까?"로 보이는데요!

개인적으로 중요하게 느껴지는 부분은 "어떤 것을 공통 부분"으로 간주해서 코드의 중복을 제거해볼 수 있을까 인 것 같습니다.

 

예를 들어, 적어주신 부분처럼 MutableList<T> 에 대한 선언을 중복 제거 하고 싶다면 추상 클래스 - 하위 클래스 구조를 사용해야 하고요! 일급 컬렉션의 로직을 중복 제거 하고 싶다면, 우리가 흔히 사용하는 filter 함수나 map 함수처럼 제네릭 확장 함수를 만들어 로직 중복 제거를 할 수 있을 것 같아요!

 

또한, 저라면 추상 클래스 - 하위 클래스 구조를 사용할 때, 선언에 대한 중복 제거를 하고 싶다면 <T> <in T> <out T> 를 각각 갖는 추상 클래스를 만들어 놓은 후, 내가 만들고 싶은 일급 컬렉션의 성격에 따라 적절한 추상 클래스를 만들어 놓을 것 같습니다!! 세 타입 파라미터는 T 라는 공통된 문자를 갖지만 변성의 차이로 인해 다른 타입 파라미터로 생각해야 하니까요! 👍

 

다만, 개인적인 경험으로는 여러 일급컬렉션을 사용하더라도 "선언부"에 대한 중복 제거를 했던 경험은 없고, "로직"에 대한 중복 제거를 했던 경험은 꽤 있는 것 같은데요!

그 이유로는, 특히 코틀린의 경우 "선언부"를 추상 클래스에 넣더라도, 하위 클래스에서 추상 클래스를 상속 받을 때 어차피 매개변수 전달을 해줘야 하다보니

abstract class AbstractCollection<T>(
  val list: List<T>, // 여기에 list가 있다면...
)


class MyCollection(
  list: List<ADto>, // 어차피 여기에 list를 한 번 써서
) : AbstractCollection<ADto>(list) // 상위 클래스에 넘겨줘야 합니다!

// 물론, list 대신 override val list를 사용할 수도 있긴해요!

"선언부"에 대해서는 중복이 제거된다는 느낌이 크게 들지는 않더라고요!

 

제 답변이 도움이 되었으면 좋겠습니다~!!! 😊

언제라도 궁금한 점 생기시면 바로 편하게~ 질문 남겨주세요! 감사합니다! 🥰🙇

Truestar님의 프로필

Truestar

질문자

2023.09.25

답변 감사드립니다. 이렇게 이해 하는게 맞을까요?

질문에 대해 미세조정을 해주셔서 제가 궁금했던 부분의 본질을 알 수 있었구요,

일급 컬렉션의 공통 부분을 어떻게 만들까❓
➡️
무엇을 "공통 요소"로 두고 중복제거 할 것인가❓

말씀해주신 "중복제거" 에 대해 요약해봤습니다.

 

💡중복제거 방법 3가지

  1. 중복필드 제거
    ➡️ 상속구조에서 추상체로 구현체 필드를 추출하여 공통화.

  2. 중복로직 제거
    ➡️ Kotlin 제네릭확장함수 를 통해 구현체 로직을 추출하여 공통화

  3. 💡중복 타입파라미터 제거💡
    ➡️ 타입파라미터는 본래 정적 성질을 갖으며,<T> <in T> <out T> 는 모두 다른타입이기 때문에 각각의 추상체를 두어, 추상체 하나 당 하나의 구현체를 만든다.

 

이어서 강사님 경험에 의한 가이드 요약은,

일급컬렉션 구성 시에 생기는 중복요소는 보통 "선언부" 보단 "로직" 중복이 많았다.

Kotlin 제네릭의 경우,

"선언부"를 추상 클래스에 넣더라도 상속할 때 어짜피 "매개변수 전달"은 필수적이니, 아래와 같이 구성해 볼 수 있다.

abstract class AbstractCollection<T>(
  val list: List<T>,
)

class MyCollection(
  list: List<ADto>, // 구체적인 타입은 구체에서 정의
) : AbstractCollection(list) //타입추론으로 추상체 T 타입 자동선언

또한, "선언부" 에 의한 중복제거는 효과가 미미하다.

나름 정리를 했는데, 고칠부분을 알려주시면 참고하겠습니다.

안녕하세요, Truestar님!! 정리해주신 내용에 감탄하며 2~3번을 읽어보았습니다 ㅎㅎㅎ

제가 말씀 드렸던 내용에 대한 완벽한 요약입니다!! 👍👍

 

매우 개인적인 의견을 첨언드려보면, "로직" 중복을 해결할 때 코틀린의

  • 람다를 파라미터로 넘기는 함수형 프로그래밍

  • 확장함수

  • 제네릭

을 응용하면 정말 일반화(?)된 나만의(?) 라이브러리를 만드는 재미가 있더라고요!

 

예를 들어, 제가 주어진 List에서 원소를 하나씩 확인하며, 특정 조건을 만족하는 경우 예외를 던지는 코드를 많이 작성했다면,

fun <T> Collection<T>.throwIfAny(throwable: Throwable, predicate: (T) -> Boolean): Collection<T> {
  for (item in this) {
    if (predicate(item)) {
      throw throwable
    }
  }
  return this // 체이닝을 하고 싶다면 Collection<T>를 반환할 수 있다.
}

위와 같은 함수를 작성해 Collection 처리 로직의 중복을 제거할 수 있었습니다.

 

감사합니다!! 🙇

Truestar님의 프로필

Truestar

질문자

2023.09.26

첨언과 응용예제도 주셨네요!! Kotlin 확장함수가 정말 Java 의 많은 문제를 해결하는 것 같습니다.
말씀대로 나중에 개인용 라이브러리 만들어 봐야겠네요..!

강사님 피드백 덕분에 안개가 거치는것 같네요. 정성스런 답변 다시 감사드려요^^