제네릭 무한 연쇄 없애기 Youtube

문제 1

  • 한 클래스가 제네릭을 사용하기 시작하면, 이를 사용하는 클래스에서도 제네릭을 사용하게 된다.
  • 이로 인해 가독성이 떨어지고, 클래스의 역할을 알아보기가 어려워진다.

Ex

// AccessToken을 담을 DTO를 반환하는 API를 호출하는 Repository
val repository = Repository<Api<Dto<AccessToken>>>()


해결 1

  • 제네릭을 사용하는 인터페이스를 만든다.
  • 제네릭 인터페이스를 래핑하는 인터페이스를 만든다.

Ex

// 제네릭 인터페이스
private interface Repository<T: Token> {
    // Repository의 공통 사항
    suspend fun save(token: T)
    suspend fun load(): T?
}

// 제네릭 인터페이스를 래핑하는 인터페이스
// 역할이 명확하면서도 구현할 때는 제네릭 클래스처럼 타입이 정해진다 
abstract class AccessTokenRepository: Repository<AccessToken>


문제 2

  • 제네릭을 사용하지 않아 구현 클래스에 코드 중복이 많아진다.

Ex

class AccessTokenRepositoryImpl : AccessTokenRepository() {
    private var accessToken: AccessToken? = null

    override suspend fun save(accessToken: AccessToken) {
        this.accessToken = accessToken
    }

    override suspend fun load(): AccessToken? {
        return accessToken
    }
}

// AccessTokenRepositoryImpl과 구현 사항이 거의 똑같다
class RefreshTokenRepositoryImpl : RefreshTokenRepository() {
    private var refreshToken: RefreshToken? = null

    override suspend fun save(refreshToken: RefreshToken) {
        this.refreshToken = refreshToken
    }

    override suspend fun load(): RefreshToken? {
        return refreshToken
    }
}


해결 2

  • 상위 타입의 구현 클래스를 만들고, 구현 사항을 위임한다.

Ex

// 기존에 했던 것과 같은 방식으로
// 상위 타입의 제네릭 인터페이스를 래핑하는 인터페이스를 만든다.
abstract class SuperTokenRepository: Repository<Token>

// 이후 제네릭 인터페이스의 구현 클래스를 만든다
class SuperTokenRepositoryImpl: SuperTokenRepository() {
    private var token: Token? = null
    
    override suspend fun save(token: Token) {
        this.token = token
    }
    
    override suspend fun load(): Token? {
        return token
    }
}

// 기존의 구현 클래스에서는 상위 타입의 구현 클래스에게 구현을 위임한다
class AccessTokenRepositoryImpl : AccessTokenRepository() {
    private var delegate = SuperTokenRepositoryImpl()

    override suspend fun save(accessToken: AccessToken) {
        delegate.save(accessToken)
    }

    override suspend fun load(): AccessToken? {
        // delegate.load()는 Token? 타입을 반환하므로
        // AccessToken? 타입으로 변환해야 한다
        return delegate.load()?.let { reify(it) }
    }
}

// 런타임에 추상 타입인 Token을 구체 타입인
// AccessToken 또는 RefreshToken으로 변환한다
inline fun <reified T: Token> reify(token: Token): T {
    return token as T
}