티스토리 뷰

JAVA

상속은 언제 사용해야 할까?

perseverance 2025. 3. 26. 20:37

들어가며 

상속이란, 기존의 클래스를 재사용하여 새로운 클래스를 작성하는 것이다. 상속을 이용하면 부모 클래스가 가지고 있는 멤버변수와 메서드를 사용할 수 있어 코드 재사용을 할때 효과적이라고 한다. 그렇다면 상속은 언제 사용하는것이 좋을까? 

상속을 사용하는 이유 

상속을 왜 사용할까? 상속은 두가지 용도로 사용된다. 첫번째는 타입 계층을 구현하는 것이고 두번째는 코드 재사용이다. 

객체지향 프로그래밍에서 타입 계층을 구현한다는 의미는 무엇일까? 객체지향 프로그래밍에서 타입을 정의하는 것은 객체의 퍼블릭 인터페이스를 정의하는 것과 동일하다. 즉, 객체의 퍼블릭 인터페이스가 객체의 타입을 결정한다. 따라서 동일한 퍼블릭 인터페이스를 제공하는 객체들은 동일한 타입으로 분류된다. 

 

여기서 중요한 사실은 객체의 타입은 객체 내부의 멤버변수로 정해지는 것이 아닌 행동으로 정해진다는 것이다. 속성이 같아도 행동이 다르다면 다른 타입계층에 속한다.

 

상속을 사용하는 기준 

상속이 타입 계층을 구현하는 용도로 사용된다는 것을 알았다. 그렇다면 언제 상속을 사용해야 할까?

보통 자바를 알려주는 책을 보면 is-a관계에서 상속을 사용하라고 한다. 여기서 의문이 두가지가 드는데 첫번째는 is-a관계는 무엇인지, 두번째는 is-a관계에서는 무조건 상속을 사용하는것이 좋은건지이다. 

 

is-a관계란

is-a관계는 어떤 타입 S가 다른 타입 T의 일종이라면 당연히 타입 S는 타입 T다 (S is-a T) 라고 말할 수 있어야 한다. 백엔드는 직업이다 라고 표현할 수 있고 개발자는 직업이다 라고 표현할 수 있다. 즉, 백엔드, 개발자, 직업은 is-a 관계를 만족시킨다. 

 

하지만 is-a관계가 생각처럼 직관적이고 명쾌한 것이 아니다. is-a관계인줄 알았지만 특정 상황에서는 is-a관계가 아닐 수 있기 때문이다. 

블랙잭 게임의 예제로 한번 살펴보자. 

 

블랙잭 게임에서는 딜러와 플레이어가 존재한다. 딜러와 플레이어는 서로 게임에 참여하여 패가 21과 가까운 수를 가진 사람이 이긴다. 

게임 참여자는 게임이 시작되면 베팅을 해야한다.

 

여기서 알 수 있는 사실은 다음과 같다.

- 딜러는 게임 참여자이다. 

- 플레이어는 게임 참여자이다. 

- 게임 참여자는 게임 시작시 베팅을한다.

 

위 사실을 조합하면 다음과 같은 코드를 만들 수 있다. 

public class Participant {
	public void preparebetting() {
		// 베팅을한다. 
	}
}

public Dealer extends Participant {
	...
}
public Player extends Participant {
	...
}

 

위 코드는 사실 틀린 코드이다. 딜러는 분명 참여자는 맞지만 베팅은 하지 않는다. 베팅은 플레이어만 한다. 하지만 코드는 분명히 딜러는 참여자고 따라서 베팅을 할 수 있다고 주장하고 있다.

 

이 예는 어휘적인 정의가 아니라 기대되는 행동에 따라 타입 계층을 구성해야 한다는 사실을 잘보여준다. 어휘적으로 딜러는 참여자이지만 만약 참여자의 행동에 베팅을 해야한다라는 행동이 추가되는 순간 딜러는 참여자의 서브 타입이 될 수 없다. 만약 참여자의 행동에 베팅을 한다는 행동이 포함되지 않는다면 딜러는 참여자의 서브 타입이 될 수 있다. 

 

이것을 통해 알 수 있는 결론은 두 타입 사이에 행동이 호환될 경우에만 타입 계층으로 묶어야 한다는 것이다. 여기서 중요한 것은 행동의 호환 여부를 판단하는 기준은 클라이언트의 관점이라는 것이다. 클라이언트가 두 타입이 동일하게 행동할 것이라고 기대한다면 두 타입을 타입 계층으로 묶을 수 있다. 클라이언트가 두 타입이 동일하게 행동하지 않을 것이라고 기대한다면 두 타입을 타입 계층으로 묶어서는 안된다.

 

만약 클라이언트가 다음과 같이 코드를 작성한다고 하자

public void betParticipant(Participant participant) {
    //인자로 전달된 모든 participant는 베팅할 수 있어야한다. 
    participant.prepareBetting();
}

 

위 코드에서 클라이언트는 인자로 전달된 Participant가 베팅을 할 수 있다고 기대하고 있다. 하지만 Dealer가 인자로 들어오게 된다면 클라이언트의 기대를 저버리게 된다. 

 

그렇다면 다음과 같이 구현을 한다면 되지 않을까? 

public class Dealar extends Participant {

    @Override
    public void prepareBetting() {
    	throw new UnsupportedOperationException();
    }
}

 

하지만  이 경우에도 클라이언트의 관점에서 기대를 저버리는것은 동일하다. 클라이언트는 모든 참여자가 베팅을 할 수 있을거라 기대하지만 예외가 발생하기 때문이다. 따라서 이 방법 역시 클라이언트의 관점에서 Participant와 Dealer의 행동이 호환되지 않는다. 

 

또 다른 방법으로 instanceof를 활용해 인자로 전달된 Participant의 타입이 Dealer가 아닐 경우에만 prepareBetting 메서드를 호출하게 하는것이다.

public void betParticipant(Participant participant) {
    
    if (!(participant instanceof Dealer) {
    	participant.prepareBetting();
    }
}

 

하지만 이 방법 역시 문제가 있다. 만약 Dealer 뿐만 아니라 베팅을 할 수 없는 참여자가 생긴다면 어떻게 될까? 그렇다면 위 코드를 계속해서 유지보수해야할 것이다. 일반적으로 instanceof 처럼 객체의 타입을 확인하는 코드는 새로운 타입이 추가할 때마다 코드 수정을 요구하기 때문에 OCP 원칙을 위반한다. 

 

기존의 상속 계층을 그대로 유지한 채 클라이언트의 기대를 충족시킬 수 있는 방법을 찾기란 쉽지 않다. 문제를 해결할 수 있는 방법은 클라이언트의 기대에 맞게 상속 계층을 분리하는 것 뿐이다. 

 

betParticipant 메서드는 파라미터로 전달되는 모든 참여자가 베팅을 할 수 있다고 가정하기 때문에 betParticipant 메서드와 협력하는 모든 객체는 prepareBetting 메시지에 대해 올바르게 응답할 수 있어야 한다. 그러기 위해서 베팅을 할 수 있는 참여자와 베팅을 할 수 없는 참여자를 구분하여 상속 계층을 만들면 어떨까? 

 

public class Participant {
	...
}

public class BettingParticipant extends Participant {
	public void prepareBetting() {...}
}

public class Dealer extends Participant {
	...
}

 

위와 같이 베팅을 할 수 있는 참여자와 하지 못하는 참여자를 구분한다면 BetttingParticipant 타입을 이용해 베팅을 할 수 있는 참여자만 인자로 전달돼야 한다는 사실을 코드에 명시할 수 있다. 

public void betParticipant(BettingParticipant participant) {
    participant.prepareBetting();
}

 

 

요구사항 속에서 클라이언트가 기대하는 행동에 집중하자. 클래스의 이름 사이에 어떤 연관성이 있다는 사실은 아무런 의미도 없다. 두 클래스 사이에 행동이 호환되지 않는다면 타입 계층이 아니기 때문에 상속을 사용해서는 안 된다.

 

결론

상속은 단지 코드 재사용을 위해서 사용하면 안된다. 상속은 타입 계층을 구성하기 위해 사용해야 한다. 단순히 코드 재사용을 위해서 상속을 사용한다면 부모 클래스와 자식 클래스의 행동이 호환되지 않기 때문에 자식 클래스의 인스턴스가 부모 클래스의 인스턴스를 대체할 수 없다. 

 

상속을 타입 계층을 구성하기 위해 사용된다면 자식 클래스가 부모 클래스를 대신할 수 있기 때문에 부모 클래스가 사용되는 모든 문맥에 자식 클래스로 대체하더라도 시스템이 문제없이 동작할 수 있다. 

 

출처

조영호, 오브젝트』  434 ~ 453 pg

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/03   »
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31
글 보관함