다형성과 추상 타입
Chapter 3
Last updated
Chapter 3
Last updated
상속(Inheritance) 은 한 타입을 그대로 사용하면서 구현을 추가할 수 있도록 해주는 방법을 제공한다.
재정의(Overriding)을 통해, 하위 클래스는 필요에 따라 상위 클래스에 정의된 메서드를 새롭게 구현할 수 있다. 메서드를 재정의하면, 해당 메서드를 실행할 때 상위 타입의 메서드가 아닌 하위 타입에서 재정의한 메서드가 실행된다.
다형성(Polymorphism) 은 한 객체가 여러 가지(Poly) 모습(Morph)을 갖는다는 것을 의미한다. 여기서 모습이란 타입을 뜻하는데, 결과적으로 다형성이란 한 객체가 여러 타입을 가질 수 있다 는 것을 뜻한다.
그림과 같이, 중간에 위치한 객체는 타입A, 타입B, 타입C에 정의된 인터페이스의 구현을 제공하는데, 이 경우 다른 코드에서는 이 객체에게 타입A에 정의된 기능 실행을 요청하거나 타입B 또는 타입C에 정의된 기능 실행을 요청할 수 있다. 즉, 해당 객체를 타입A로도 사용할 수 있고, 타입B로도 사용할 수 있고, 타입C로도 사용할 수 있는 것이다.
자바와 같은 정적 타입 언어에서는 타입 상속 을 통해서 다형성을 구현한다.
타입 상속 은 크게 인터페이스 상속 과 구현 상속 으로 구분해 볼 수 있다. 인터페이스 상속 은 순전히 타입 정의만을 상속받는 것이다. 자바와 같이 클래스 다중 상속을 지원하지 않는 언어에서는 인터페이스를 이용해서 객체가 다형을 갖게 된다. 구현 상속 은 클래스 상속을 통해서 이루어진다. 이는 보통 상위 클래스에 정의된 기능을 재사용하기 위한 목적으로 사용된다. 또한, 재정의를 통해서 하위 타입은 상위 타입에 정의된 기능을 자신에 맞게 수정할 수 있다.
추상화(Abstraction) 는 데이터나 프로세스 등을 의미가 비슷한 개념이나 표현으로 정의하는 과정이다.
컴퓨터 분야에서 이런 추상화는 매우 광범위하게 사용되며, 타입도 이런 추상화의 대상이 된다. 추상화된 타입은 오퍼레이션의 시그니처만 정의할 뿐 실제 구현을 제공하지는 못한다. 따라서 의미만 전달할 뿐 상세 구현에 대해서는 알 수 없다.
추상 타입과 실제 구현 클래스는 상속을 통해서 연결된다. 즉, 구현 클래스가 추상 타입을 상속받는 방법으로 둘을 연결하는 것이며, 타입 상속을 통해 추상 타입과 구현 클래스 간에 연결 이 된다.
추상 타입에 대한 구현 클래스는 실제 구현을 제공한다는 의미에서 콘크리드(Concrete) 클래스 라고 부른다.
추상화는 공통된 개념을 도출해서 추상 타입을 정의해 주기도 하지만, 또한, 많은 책임을 가진 객체로 부터 책임을 분리 하는 촉매제가 되기도 한다. 결론적으로 추상화는 변경의 유연함을 증가시켜 준다.
재사용의 중요성으로 봤을때, 하위 수준의 상세 구현보다는 변하지 않는 상위 수준의 로직을 재사용할 수 있도록 설계하는 것이 더 중요하다.
추상화를 잘하기 위해서는 다양한 상황에서 코드를 작성하며 유연한 설계를 만들어 보는 경험을 해봐야 한다. 하지만 다양한 환경에서 많은 경험을 할 수 있는 것이 아니기 때문에, 변화될 부분을 미리 예측해서 추상화하는 것은 어렵다. 이를 위해, 경험하지 않은 분야라 하더라도 추상화할 수 있는 방법은 변화되는 부분을 추상화하는 것이다. 요구 사항이 바뀔 때 변화되는 부분은 이후에도 변경될 소지가 많기 때문에 이런 부분을 추상 타입으로 교체하면 향후 변경에도 유연하게 대처할 수 있다.
추상화가 되어 있지 않은 코드는 주로 동일 구조를 갖는 if-else 블록으로 드러난다.
추상화를 적용하게 되면, 추상 타입을 사용하는 코드에는 영향을 주지 않으면서, 추상 타입의 실제 구현(콘크리드 클래스)을 변경할 수 있는 유연함을 얻을 수 있다.
인터페이스에 대고 프로그래밍하기 (Program to Interface)
위는 객체 지향의 유명한 규칙 중 하나로써, 실제 구현을 제공하는 콘크리트 클래스를 사용해서 프로그래밍하지 말고, 기능을 정의한 인터페이스를 사용해서 프로그래밍하라는 뜻이다. 그러나, 인터페이스는 최초 설계에서 바로 도출되기 보다는 요구 사항의 변화와 함께 점진적으로 도출이 된다. 즉, 인터페이스는 새롭게 발견된 추상 개념을 통해서 도출되는 것이다.
추상 타입을 사용하면 기존 코드를 건드리지 않으면서 콘크리트 클래스를 교체할 수 있는 유연함을 얻을 수 있었는데, 주의할 점은 유연함을 얻는 과정에서 타입(추상 타입)이 증가하고 구조도 복잡해지기 때문에 모든 곳에서 인터페이스를 사용해서는 안된다는 것이다. 불필요하게 프로그램의 복잡도만 증가시킬 수 있기 때문에, 변화 가능성이 높은 경우에 한해서만 사용해야 한다.
결과적으로, 변화 가능성이 높은 콘크리트 클래스 대신 이를 추상화한 인터페이스를 사용하면 변경의 유연함이라는 효과를 얻을 수 있지만, 변경 가능성이 매우 희박한 클래스에 대해 인터페이스를 만든다면 오히려 프로그램의 구조만 복잡해지고 유연함의 효과는 누릴 수 없게 된다.
인터페이스를 작성할 때에는 그 인터페이스를 사용하는 코드 입장에서 작성해야 한다.
실제 콘크리트 클래스 대신에 진짜처럼 행동하는 객체를 Mock(가짜, 모의) 객체라고 부르는데, Mock 객체를 사용함으로써 실제 사용할 콘크리트 클래스의 구현 없이 테스트할 수 있다. Mock 객체를 만드는 방법은 다양하게 존재하지만, 사용할 대상을 인터페이스로 추상화하면 좀 더 쉽게 Mock 객체를 만들 수 있게 되며, 이는 사용할 코드의 완성을 기다릴 필요 없이 내가 만든 코드를 먼저 빠르게 테스트할 수 있다.
테스트 주도 개발(Test Driven Development)
TDD(테스트 주도 개발; Test Driven Development) 은 테스트 코드를 먼저 작성하고 실제 코드를 작성함로써, 테스트를 이용해서 실제 제품 코드를 만들어 가는 개발 기법이다.
TDD는 구현할 코드에 대한 테스트를 먼저 작성하고, 작성한 테스트를 통과하는 코드를 점진적으로 완성해 나가는 방식으로 개발을 진행한다. 테스트 주도 개발을 하다 보면 아직 완성하기 힘든 구현 때문에 테스트를 할 수 없는 경우가 발생하는데, 이런 경우 테스트를 할 수 없게 만드는 부분을 별도의 인터페이스로 분리하며 Mock 객체를 만드는 방식으로 테스트를 진행할 수 있다.
이 과정에서, 별도로 분리되는 인터페이스는 테스트 대상이 되는 클래스와 구분되는 책임을 갖게 되는 경우가 많으며, 이는 곧 새로운 책임을 갖는 객체를 도출하게 된다는 것을 의미한다.
객체 지향 설계가 객체마다 알맞은 책임을 할당하고 각 객체가 주고받는 메시지를 정의하는 과정임을 생각해 보면, 결국 TDD는 테스트를 강제함으로써 알맞은 책임을 가진 객체를 도출하도록 유도한다. 즉, 객체 지향 설계를 유도하는 좋은 개발 방식이 바로 TDD인 것이다.