전문가들은 초보자들처럼 모든 문제를 처음 기초 단계에서부터 해결하려고 하지 않습니다. 대신, 전에 사용했던 해결책을 다시 사용해 봅니다. 그리고 좋은 방법을 찾아 냈다면 그 방법을 반복해서 계속 사용하게 됩니다. 이런 경험을 통해 전문가가 만들어지고, 결국에는 많은 객체지향 시스템에서 클래스 패턴이나 객체들 간의 상호 작용 방식이 반복됨을 알게 됩니다.
이런 반복된 패턴들은 특정 설계의 문제점들을 해결해 주고, 좀더 유연하고, 근사하며, 재사용 가능한 객체지향 소프트웨어를 만들어 줍니다.
일반적으로 하나의 패턴에는 다음의 네 가지 요소가 반드시 들어 있습니다.
패턴 이름 : 한두 단어로 설계 문제와 해법을 서술합니다.패턴에 이름을 부여하는 것은 설계 어휘를 늘리는 일이며, 높은 수준의 추상화된 설계를 할 수 있도록 해 줍니다. 패턴의 이름을 정의해 두면 문서에서 이 이름을 사용하여 설계의 의도를 표현할 수 있게 됩니다. 또 이렇게 이름을 갖게 되면 설계에 대한 생각을 더욱 쉽게 할 수 있고, 개발자들 간의 의사소통이 원할해집니다.
문제 : 언제 패턴을 사용하는가를 서술하며 해결할 문제와 그 배경을 설명합니다. 즉, "어떤 알고리즘을 객체로 만들까"와 같은 설계의 세밀한 문제를 설명할 수 있습니다. 때론 유연성 없는 설계가 될 징조를 보이는 클래스나 객체의 구조를 제시합니다. 문제를 제시함으로써 패턴을 적용하는 것이 의미 있는 사례들을 적용하기도 합니다.
해결 : 설계를 구성하는 요소들과 그 요소들 간의 관계, 책임 그리고 협력 관계를 서술합니다. 그렇다고 해법이 어떤 구체적인 설계나 구현을 설명하지는 않습니다. 왜냐하면 패턴은 다양한 경우에 적용할 수 있는 템플릿이기 때문이죠. 구체적인 부분 대신, 디자인 패턴은 문제에 대한 추상적인 설명을 제공하고 문제를 해결하기 위해서 클래스나 객체들의 나열 방법을 제공합니다.
결과 : 디자인 패턴을 적용해서 얻는 결과와 장단점을 서술합니다. 어떤 설계를 결정할 때 그 설계의 결과를 고려하지 않기가 쉬운데, 어떻게 보면, 선택하는 과정에서 또는 비용과 효과를 측정하는 과정에서 설계의 결과는 가장 중요한 부분입니다. 소프트웨어에서 결과란 가끔 시간이나 공간 사이의 균형일 수도 있습니다. 즉, 시간을 중요한 요소로 볼 것인지 아니면 저장 공간의 효율을 중요한 요소로 볼 것인지에 따라 다른 설계 방법을 선택해야 한다는 것입니다. 또한 언어에 따라서도 차이가 있습니다. 재사용은 객체지향 설계의 주요 요소이므로, 패턴의 결과는 시스템의 유연성, 확장성, 이식성 등에 커다란 영향을 줍니다.
객체지향 프로그램은 객체(object)로 만듭니다. 객체는 데이터와 이 데이터에 연산을 가하는 프로시저를 함께 묶은 단위입니다. 프로시저를 일반적으로 메서드(method) 또는 연산(operation)이라고 합니다. 객체는 요청(request) 또는 메시지(message)를 사용자에게 받으면 연산을 수행합니다.
요청은 객체가 연산을 실행하게 하는 유일한 방법이고, 연산은 객체의 내부 데이터의 상태를 변경하는 유일한 방법입니다. 이러한 접근의 제약 사항으로 객체의 내부 상태는 캡슐화(encapsulate)된다고 말합니다. 객체 외부에서는 객체의 내부 데이터에 직접 접근할 수 없고, 객체의 내부 데이터 표현 방법(데이터 타입 등)을 알 수 없습니다.
객체지향 설계의 가장 어려운 부분은 시스템을 구성할 객체의 분할을 결정하는 것입니다. 여러 요인을 고려해야 하기 때문에 매우 어려운 작업입니다. 고려해야 할 요인에는 캡슐화, 크기 정하기, 종속성, 유연성, 성능, 진화, 재사용성 등이 있습니다.
객체가 선언하는 모든 연산은 연산의 이름, 매개변수로 받아들이는 객체들, 연산 반환 값을 명세합니다. 이를 연산의 시그니처(signature)라고 합니다. 인터페이스는 객체가 정의하는 연산의 모든 시그니처들을 일컫는 말로 객체의 인터페이스는 객체가 받아서 처리할 수 있는 연산의 집합입니다.
타입(type)은 특정 인터페이스를 나타낼 때 사용하는 이릅입니다. 객체가 "Window" 타입을 갖는다는 것은 "Window" 인터페이스에 정의한 연산들을 모두 처리할 수 있다는 것을 의미합니다. 객체는 여러 타입을 가질 수 있고, 서로 다른 객체가 하나의 타입을 공유할 수도 있습니다.
인터페이스가 다른 인터페이스를 부분집합으로 포함할 때도 있는데, 다른 인터페이스를 포함하는 인터페이스를 서브타입(subtype)이라고 하고, 다른 인터페이스가 포함하는 인터페이스를 슈퍼타입(supertype)이라 합니다. 서브타입은 슈퍼타입의 인터페이스를 상속한다고 이야기합니다.
인터페이스 개념은 객제지향 시스템에서 가장 기본적인 것입니다. 객체는 인터페이스로 자신을 드러냅니다. 외부에서 객체를 알 수 있는 방법은 인터페이스 밖에 없기 때문에 인터페이스를 통해서만 처리를 요청할 수 있습니다. 객체의 인터페이스는 구현에 대해서는 전혀 알려주지 않습니다. 그러므로 서로 다른 객체는 인터페이스에 정의한 요청의 구현 방법을 자유롭게 선택할 수 있습니다. 즉, 동일한 인터페이스를 갖는 두 객체가 완전히 다른 구현을 가질 수 있다는 것입니다.
객체에 요청이 전달되면, 요청과 이를 받는 객체에 따라서 수행되는 처리방식이 달라집니다. 동일한 요청이라도 처리하는 객체들이 다른 객체라면, 이 요청에 대한 구현을 어떻게 했는가에 따라서 다른 결과가 나올 수 있습니다. 어떤 요청과 그 요청을 처리할 객체를 프로그램 실행 중, 즉 런타임에 연결 짓는 것을 동적 바인딩(dynamic binding)이라고 합니다.
동적 바인딩은 요청이 어떻게 구현되어 어떤 결과를 만들어 낼지를 런타임에 결정할 수 있음을 의미합니다. 즉, 프로그램을 작성할 때 객체가 어떤 특정 인터페이스를 갖도록 작성하며, 이 객체는 요청을 처리할 정확한 인터페이스를 갖고 있습니다. 또한 동적 바인딩은 프로그램이 기대하는 객체를 동일한 인터페이스를 갖는 다른 객체로 대체할 수 있게 해 줍니다. 이런 대체성을 우리는 다형성(polymorphism)이라고 하는데, 이는 객체지향 시스템의 핵심 개념입니다.** 다형성은 사용자의 정의를 단순화하고 객체 간의 결합도를 없애며, 프로그램 실행 중에는 서로 간의 관련성을 다양화할 수 있게 해 줍니다. 다시 말해, 사용자는 어떤 특정 인터페이스를 제공하는 객체에게 요청을 보낸 것으로 프로그래밍하지만, 런타임에 그 객체를 동일한 인터페이스를 제공하는 다른 객체로 대체할 수 있습니다. 이로써 런타임에 대체한 객체와 새로운 관련성이 수립되는 것입니다.
어떤 객체의 구현은 클래스(class)에서 정의(define)합니다. 클래스는 객체의 내부 데이터와 표현 방법을 명세하고, 그 객체가 수행할 연산을 정의합니다.
OMT 기반의 표기법에서는 클래스를 표현하는 사각형에 진한 글자체로 클래스의 이름을 표현합니다. 연산의 이름은 클래스 이름 아래 줄에 나열하고, 클래스가 정의하는 데이터는 연산 아래 줄에 표시합니다. 클래스 이름과 연산 이름, 연산과 데이터를 구분하는 선을 긋습니다.
객체는 클래스를 인스턴스로 만듦으로써 생성됩니다. 즉 객체는 클래스의 인스턴스라고 할 수 있습니다. 클래스의 인스턴스화 과정은 객체의 내부 데이터[인스턴스 변수(instance variable)]에 대한 공간을 할당하고, 이 데이터들을 연산과 관련짓는 것입니다. 클래스의 인스턴스화 과정을 통해 객체의 인스턴스를 얻게 됩니다.
클래스와 타입 사이의 차이는 꼭 이해해 두어야 합니다.
객체의 클래스는 그 객체가 어떻게 구현되느냐를 정의합니다. 클래스는 객체의 내부 상태와 그 객체의 연산에 대한 구현 방법을 정의합니다. 반면, 객체의 타입은 그 객체의 인터페이스, 즉 그 객체가 응답할 수 있는 요청의 집합을 정의합니다.
하나의 객체가 여러 타입을 가질 수 있고 서로 다른 클래스의 객체들이 동일한 타입을 가질 수 있습니다. 즉, 객체의 구현은 다를지라도 인터페이스는 같을 수 있다는 의미입니다.
물론, 클래스와 타입 간에는 밀접한 관련이 있습니다. 클래스도 객체가 수행할 수 있는 연산을 정의하므로, 객체의 타입을 정의하는 것이기도 합니다. 그래서 어떤 객체가 어떤 클래스의 인스턴스라고 말할 때, 그 객체는 그 클래스가 정의한 인터페이스를 지원한다는 뜻이 숨어있다고 보면 됩니다.
클래스 상속은 객체의 구현을 정의할 때 이미 정의된 객체의 구현을 바탕으로 합니다. 쉽게 말해, 코드와 내부 표현 구조를 공유하는 메커니즘입니다. 이에 반해, 인터페이스 상속(서브타이핑)은 어떤 객체가 다른 객체 대신에 사용될 수 있는 경우를 지정하는 메커니즘입니다. 동적 바인딩을 설명할 때 이야기했듯, 인터페이스 상속 관계가 있다면 프로그램에는 슈퍼타입으로 정의하지만 런타임에는 서브타입의 객체로 대체할 수 있습니다.