open class 사용
open class Player(open val name: Name) {
// ...
open fun copy() = Player(name)
}
data class GamePlayer(
override val name: Name
): Player(name)
class CopyTest {
@Test
fun copyTest() {
val players: List<Player> = listOf(GamePlayer(Name.from("player1")), OtherPlayer(Name.from("player2")))
players.map { it.copy() } // player copy()
}
}
문제발생
컬렉션에서 방어적 복사를 해주기 위해 부모클래스의 copy()를 사용하고 싶어서 open 메소드로 copy()를 부모 클래스에 구현해 주었습니다.
또, 위 코드에서 GamePlayer는 data class
입니다.
그러면, 자식클래스인 GamePlayer에서 GamePlayer타입을 반환하는copy()
를 생성할까요?
당연히 자동으로 생성해주는 줄 알았습니다.
하지만 결과는 자동으로 생성해주지 않았습니다.
이유는 이미 Player 부모 클래스는 open
클래스이고, open 함수로 copy()
를 작성했었습니다.
그러면 Player 클래스를 상속한 자식 클래스는 open 함수이므로, 자식클래스(GamePlayer)에서는 Player의 copy()
를 그대로 물려받게 됩니다.
여기서, data class는 copy()
, hashCode()
, equals()
, toString()
를 자동생성해주지만, 똑같은 이름의 메소드가 이미 구현되어 있다면 코드를 생성해주지 않습니다.
즉, GamePlayer 자식 클래스에서는 data class로 구현되어있음에도 불구하고
컴파일러는 copy()
가 이미 구현된 코드라고 생각하고 자동으로 생성해주지 않습니다. (대신 Player의 copy()함수의 반환 타입이 Player이므로 Player를 반환합니다.)
그러면 GamePlayer의 인스턴스로 copy()
를 호출해도 아래 스크린샷처럼 부모클래스로 업캐스팅이 되어버리는 일이 발생합니다.
해결방법
그러면 어떻게 해야할까요?
GamePlayer 자식클래스에서 copy()
를 override 해주면 됩니다.
data class GamePlayer(
override val name: Name
): Player(name) {
override fun copy(): GamePlayer = GamePlayer(name)
}
이렇게 추가를 해주면 아래처럼 컴파일 에러가 사라집니다.
abstract class 사용
abstract class 또한 마찬가지입니다.
자식 클래스에서만 사용할 경우 data class를 활용할 수 있습니다. (위에 open class에서는 안 다루었지만 아래처럼 copy()를 부모클래스에서 정의가 없다면 data class를 활용할 수 있습니다.)
abstract class Player {
abstract val name: Name
}
data class GamePlayer(override val name: Name) : Player() {
// copy() 자동 생성
// ...
}
class GamePlayerTest {
@Test
fun `Player copy test`() {
val gamePlayer = GamePlayer(Name.from("player"))
val gamePlayerCopy: GamePlayer = gamePlayer.copy() // 사용 가능
}
}
하지만 부모클래스에서 사용하고 싶다면 어떻게 할까요?
마찬가지로 부모클래스에서 copy()를 정의해주어야 합니다.
abstract class Player {
abstract val name: Name
abstract fun copy(): Player
}
data class GamePlayer(override val name: Name) : Player() {
override fun copy(): Player = GamePlayer(name)
// ...
}
data class OtherPlayer(override val name: Name) : Player() {
override fun copy(): Player = OtherPlayer(name)
// ...
}
class GamePlayerTest {
@Test
fun `Player copy test`() {
val players: List<Player> = listOf(GamePlayer(Name.from("player1")), OtherPlayer(Name.from("player2")))
players.map { it.copy() } // player copy()
}
}
Player의 부모클래스에서 copy()
가 필요할 경우는 data class를 활용할 수 없으므로 위처럼 일일히 구현해 주어야 합니다.
Gson의 JsonElement도 이런식으로 copy()를 구현한 것을 확인할 수 있습니다.
정리
부모클래스에서 open class나 abstract class 해결방법이 비슷해 보입니다.
하지만 open, abstract 둘 중 어느것을 사용하는 것이 바람직할까요?
위에 결과에서 보았듯이 open 클래스에서는 에러를 알아차리지 못했듯이 abstract
를 사용해야 한다고 생각합니다.
왜냐하면 위에 코드에서는 간단한 예제기 때문에 상속이 한번밖에 안이루어져 있기 때문에 개발자는 구현해야 할 코드를 단번에 알 수 있지만, 자식클래스가 10개이고 계속 추가가 되는 구조라면 어떨까요? 구현을 해주지 않으면 런타임 시점에 에러가 발생할 것이고, 에러 추적도 어려울 것입니다.
open으로 만들어버리면 컴파일 시점에 명확히 알 수가 없습니다. (부모 클래스의 Player를 리턴하기 때문에 자식 클래스에서는 무조건 구현해주어야 함에도)
하지만, abstract
로 만들면 개발자 입장에서 새로운 자식 클래스를 만들 때 copy()를 구현해 주어야 하는 것을 컴파일 시점에 명확히 해주기 때문에 더 바람직한 방법일 것 같습니다.