티스토리 뷰

클래스를 이용한 타입 계층 구현

타입은 객체의 퍼블릭 인터페이스를 가리킨다. 클래스는 객체의 타입과 구현을 동시에 정의하는 것과 같다고 볼 수 있다. 이러한 점이 객체지향 언어에서 클래스를 사용자 정의 타입(user-defined data type)이라고 부르는 이유다. 

public class Phone {
    private Money amount;
    private Duration seconds;
    private List<Call> calls = new ArrayList<>();
    
    public Phone(Money amount, Duration seconds) {
    	this.amount = amount;
        this.seconds = seconds;
    }
    
    public Money calculateFee() {
    	// 생략
    }
}

Phone 인스턴스는 calculateFee 메시지를 수신할 수 있는 퍼블릭 메서드를 구현한다. 또한 Phone의 퍼블릭 인터페이스를 구성한다. Phone 클래스는 Phone 타입을 선언함과 동시에 객체 구현을 정의하고 있다.

인터페이스를 이용한 타입 계층 구현

A타입이 B타입인 동시에 C타입이라고 가정해보자. 이 경우 A타입은 B, C클래스를 동시에 상속받아야 한다. 하지만 대부분의 언어는 다중 상속을 지원하지 않는다. 뿐만 아니라 클래스들을 상속 관계로 연결하면 자식 클래스가 부모 클래스의 구현에 강하게 결합될 가능성이 있다. 이러한 문제를 피하고 다중 상속이라는 제약도 해결하기 위해 클래스가 아닌 인터페이스를 사용할 수 있다. 

public class Player implements Collidable {
    @Override
    public String getName() { ... }
    
    @Override
    public boolean collideWith(Collidable other) { ... }
    
    // 이하 생략
}
public class Monster implements Collidable {
    @Override
    public String getName() { ... }
    
    @Override
    public boolean collideWith(Collidable other) { ... }
    
    // 이하 생략
}

위 두 클래스는 서로 다르지만, 두 클래스의 인스턴스들은 Collidable 인터페이스를 구현하고 있기에 동일한 메시지에 응답할 수 있다. 따라서 서로 다른 클래스에 의해 구현됐지만 타입은 동일하다.

public class Explosion implements Displayable, Effect {
    @Override
    public String getName() { ... }
    
    @Override
    public Point getPosition() { ... }
    
    // 이하 생략
}

위 클래스는 Displayable, Effect 인터페이스 두 가지를 동시에 구현한다. 따라서 Explosion의 인스턴스는 Displayable 타입인 동시에 Effect 타입이기도 하다. 정리하면 아래와 같다.

 

  • 여러 클래스가 동일한 타입을 구현할 수 있다.
  • 하나의 클래스가 여러 타입을 구현할 수 있다.

클래스와 타입 간의 차이를 이해해야한다. 객체의 클래스는 객체의 구현을 정의한다. 객체의 내부 상태와 오퍼레이션 구현 방법을 정의하는 것이다. 반면에, 객체의 타입은 인터페이스만을 정의한다. 객체가 반응할 수 있는 오퍼레이션의 집합을 정의하는 것이다. 객체가 외부에 제공하는 행동, 즉 타입을 중심으로 객체들의 계층을 설계하는 것이 중요하다.

추상 클래스를 이용한 타입 계층 구현

타입 계층을 구현하는 또 다른 방법으로 추상 클래스가 있다. 이 방법이 구체 클래스로 타입을 정의하는 것과는 두 가지 차이가 있다.

 

  • 의존하는 대상의 추상화 정도
  • 상속을 사용하는 의도

첫째로, 자식 클래스는 부모 클래스의 내부 구현이 아닌 추상 메서드의 시그니처에만 의존한다. 따라서 자식 클래스는 부모 클래스의 메서드 내부 구현 어떤 식으로 되어 있는지는 알 필요가 없다. 의존 대상의 추상화 정도가 달라진 것이다.

둘째로, 추상 클래스를 사용한다는 것은 상속을 염두에 둔다는 것이다. 추상 클래스의 목적은 자식 클래스를 추가하는 것이다. 추상 메서드를 제공함으로써 상속 계층을 쉽게 확장 가능하게 하고 결합도로 인한 부작용을 방지하는 것이다.

 

추상 클래스와 인터페이스 결합하기

인터페이스만으로 중복 코드를 제거하기는 어려운데, 이러한 문제를 해결하는 방법으로 인터페이스와 추상클래스를 함께 사용할 수가 있다. 이러한 방식을 골격 구현 추상 클래스(skeletal implementation abstract class)라고 한다. 

 

  • 다양한 구현 방법이 필요한 경우 새로운 추상 클래스를 추가해 쉽게 해결 가능
  • 이미 부모 클래스가 존재하지만, 인터페이스를 추가함으로써 새로운 타입으로 쉽게 확장 가능

 

덕 타이핑 사용하기

주로 동적 타입 언어에서 사용하는 방법으로 덕 테스트를 프로그래밍에 적용한 것이다. 덕테스트는 어떤 대상의 행동이 오리와 같다면 그것을 오리 타입으로 취급해도 무방하다는 것이다. 

 

어떤 새가 오리처럼 걷고, 오리처럼 헤엄치며 오리처럼 꽥꽥 소리를 낸다면 나는 이 새를 오리라고 부를 것이다.

 

정적 타입 언어에서는 퍼블릭 인터페이스만으로 타입을 추측할 수 없고 모든 요소의 타입이 명시적으로 기술돼 있어야 한다. 반면에 동적 타입 언어에서는 명시적으로 동일한 클래스를 상속받거나 동일한 인터페이스를 구현하지 않더라도 시그니처가 동일한 메서드를 가진 클래스는 같은 타입으로 취급할 수 있다.

 

C#에서는 dynamic 키워드를 통해 덕타이핑을 흉내 낼 수는 있다. 그러나 이 경우 컴파일 시점에 발견할 수 있는 오류를 실행 시점으로 미루게 되기 때문에 설계의 유연성을 얻는 대신 코드 안전성을 해칠 수 있다.

댓글
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday