우아한테크코스 블랙잭 미션 중 중복된 코드를 제거라하라
는 요구사항이 있었습니다. 그 이유는 블랙잭의 경우 딜러와 플레이어의 객체가 필요했는데 이 둘은 공통적으로 하는 행위를 가지고 있었기 때문입니다. 이를 하나로 묶기 위해 상속을 고려했습니다. 이번 미션을 진행하면서 제가 느낀 상속에 대해서 정리해 보는 시간을 가지려고 합니다.
자바에서 상속은 extends
키워드를 이용해서 구현할 수 있는 객체 지향 프로그래밍의 기본 개념 중에 하나로 기존 클래스의 메소드와 변수를 이용할 수 있는 새로운 클래스를 생성하는 것을 의미합니다. 이 때 기존의 클래스를 부모 클래스(상위 클래스)라고 부르며 새롭게 생긴 클래스를 자식 클래스(하위 클래스)라고 부릅니다.
상속의 큰 특징은 부모 클래스(상위 클래스)의 변화에 따라 자식 클래스(하위 클래스)가 큰 영항을 받는다는 것입니다. 부모 클래스의 메소드가 변하게 된다면 자식의 클래스 역시 결과가 변화하게 된다. 즉, 상속은 is-a
의 관계일 때 적용하는 것이 좋습니다.
이번 블래잭 미션을 적용해서 상속을 이용했을 때 얻을 수 있는 장점에 대해 알아보겠습니다.
블랙잭의 경우 Dealer와 Player는 공통된 행위를 가지고 있습니다.
…
과 같이 공통되는 기능을 가지게 됩니다. 이를 Dealer 클래스와 Player 클래스에 모두 구현하는 것은 코드 중복을 유발하게 됩니다.
public class Dealer {
public void hit(Card card) {
// 카드를 받는 기능
}
public boolean hasAceCard() {
// ace 카드를 가지고 있는지 검증하는 기능
}
public boolean isBlackJack() {
// 블랙잭을 확인하는 기능
}
}
public class Player {
public void hit(Card card) {
// 카드를 받는 기능
}
public boolean hasAceCard() {
// ace 카드를 가지고 있는지 검증하는 기능
}
public boolean isBlackJack() {
// 블랙잭을 확인하는 기능
}
}
상속을 이용하면 공통된 로직을 중복하지 않고 Dealer와 Player가 공통된 기능들을 가질 수 있습니다.
public class Participant {
public void hit(Card card) {
// 카드를 받는 기능
}
public boolean hasAceCard() {
// ace 카드를 가지고 있는지 검증하는 기능
}
public boolean isBlackJack() {
// 블랙잭을 확인하는 기능
}
}
public class Dealer extends Participant {
}
public class Player extends Participant {
}
상속을 이용하면 자동 타입 변환이 적용되어 다형성을 구현할 수 있습니다.
💡자동 타입 변환이란
컴파일러가 자동으로 타입을 변환하는 것을 의미합니다.
상속에서는 기본적으로 자식 클래스(하위 클래스)는 부모 클래스(상위 클래스)과 동일하게 취급이 됩니다.
ex) Participant dealer = new Dealer();
즉, 상위 클래스에서 Dealer와 Player 각각을 위한 메소드를 만들 필요 없이 하나의 메소드로 다형성을 적용할 수 있습니다.
public class BlackjackGame {
...
public int calculateDealerScore(Dealer dealer) {
// 딜러 점수 계산
}
public int calculatePlayerScore(Player player) {
// 플레이어 점수 계산
}
// 상속은 다형성 적용이 가능하기 때문에 위의 두 방법은 하나의 코드로 변경할 수 있습니다.
public int calculateScore(Participant participant) {
// 카드 점수 계산하기
}
}
위의 코드는 미션에서 직접 이용한 코드는 아니지만 상속의 다형성을 이용한 코드라고 볼 수 있습니다. Dealer와 Player 각각의 점수를 계산하는 메소드를 만들 필요 없이 상위 클래스(Participant)를 인자로 받아서 하나의 메소드로 처리할 수 있습니다. 이 부분 역시 중복되는 코드를 제거한다고 볼 수 있습니다.
지금까지 미션을 진행하면서 상속을 이용했을 때 느꼈던 장점에 대해 설명해보았습니다. 장점만 보면 중복되는 코드가 발생할 때 마다 상속을 고려할 만큼 좋은 장점을 가지고 있지만 치명적인 단점을 가지고 있습니다.
제가 상속을 이용하면서 느낀 점은 요구사항 변화에 쉽게 대응하는 것이 어려울 수 있겠다.
였습니다. 이와 같이 느낀 이유는 상위 클래스에 변화가 생겼을 때 이를 하위 클래스에서 그대로 적용이 된다면 상관이 없겠지만 하위 클래스에서는 기존에 방식을 유지해야 한다면 이는 오히려 유지보수를 하는데 더 많은 리소스가 필요하다고 생각됩니다. 즉, is-a
관계가 지속적으로 유지가 되지 않을 때 상속을 이용한다면 요구사항의 변화가 발생했을 때 더 큰 문제를 가져올 수 있다는 것을 느낄 수 있었습니다.
상속이 깊어지면 하위 클래스 생성이나 메소드 호출 시 오버헤드가 발생하지 않을까?
이 부분은 상속을 처음 학습하는 순간부터 고려했습니다. 상속은 하위 클래스를 호출하면 상위 클래스 역시 JVM의 메모리 영역중 하나인 heap 메모리에 같이 올라가게 됩니다. 만약에 상속이 계속 깊어지게 되고 그 때마다 메소드를 오버라이딩 해야한다면 클래스와 메소드 모두가 heap 메모리 영역에 올라가게 되는데 이는 오버헤드가 발생하게 될 테고 성능상에도 좋지 못할 것이라고 생각했습니다. 즉, 상속이 깊어지게 된다면 계속 상속을 유지하는 것이 좋은지에 대해 생각해볼 시점이라는 것을 느낄 수 있었습니다.
상속은 중복된 코드 제거와 객체지향 프로그래밍의 핵심인 다형성을 이용할 수 있다는 큰 장점을 가지고 있습니다. 하지만 상위 클래스와 하위 클래스가 강한 결합을 가지고 있어서 상위 클래스 변화에 하위 클래스도 영향을 받게 됩니다. 또한 상속이 깊어지는 상황에서는 중복 코드를 줄이는 것 이상으로 큰 단점을 가지고 올 수 있는데 이는 성능상의 문제점을 야기할 수 있습니다. 그렇기에 상속을 이용할 때에는 앞서 설명했던 문제점이 발생하지 않는지 고려해보고 적용하는 것이 좋다고 생각합니다.