디자인 패턴은 객체지향 설계를 할 때 겪게 되는 문제들을 다양한 방법으로 해결해 줄 수 있는 좋은 수단이다. 이 포스팅에서는 문제를 해결하기 위해 디자인 패턴을 어떻게 이용하는지에 대해서 알아보고자 한다.
적당한 객체 찾기
객체지향 프로그램(OOP)은 당연하겠지만 객체로 만들어진다. 객체는 데이터와 이 데이터를 처리하는 Procedure를 함께 묶은 단위이다.
Procedure는 흔히 우리가 메서드(Method) 또는 오퍼레이션(Operation)이라고 부르는 것들이다. 객체는 요청(Request) 받으면 오퍼레이션을 수행한다.
요청은 객체가 오퍼레이션을 실행하게 하는 유일한 방법이고, 오퍼레이션은 객체 내부의 데이터의 상태를 변경할 수 있는 유일한 방법이다. 이러한 접근의 제약 사항으로 객체의 내부 상태는 캡슐화 된다고 말한다. 객체 외부에서는 객체 내부의 데이터에 직접 접근할 수 없고, 객체 내부의 데이터 표현 방법(int, string등의 데이터 타입 등)을 알 수 없다.
객체지향 설계가 어려운 이유는 시스템을 구성할 객체를 어떻게 분할해야할지 결정하는 것이다. 캡슐화, 크기, 종속성, 유연성, 성능, 진화, 재사용성 등의 여러 요인을 고려해야 하기 때문에 매우 어려운 작업일 수 있다. 이 모두를 어떻게 고려하는가에 따라서 다른 방법으로 분할할 수 있다.
이 문제에 대해 객체지향 설계 방법론들은 서로 다른 접근법을 취하고 있지만, 어느 방법이 가장 좋은 방법이라고 할 수는 없다.
** Rumbaugh의 OMT(Object Modeling Technique), Booch의 ODD(Object Orient Design), Jacobson의 OOSE(Object Oriented Software Engineering) 등..
설계 단계의 객체들 대부분은 분석 모델에서부터 만들어 진 것이다. 그러나 객체지향 설계는 실세계와 대응 관계를 갖지 못할 때가 많다. 분석 모델의 객체는 실세계의 객체들이지만, 설계 모델의 객체는 배열, 리스트와 같은 구현에 가까운 클래스들도 존재한다. 실세계를 그대로 반영하는 모델링만을 강조한다면 현재의 실세계는 반영할 수 있지만 미래의 실세계는 반영할 수 없다. 설계 단계 동안 만들어 내야 하는 새로운 추상화는 설계의 유연성을 증진하기 위한 중요한 단계이다.
디자인 패턴은 모호한 추상화 개념을 찾아서 이를 객체로 만들어 준다. 처음 객체지향 프로그래밍을 하는 개발자들에게 프로세스나 알고리즘을 객체로 만드는 것은 어려운 일이다. 그러나 프로세스나 알고리즘을 객체로 만드는 것은 유연한 설계를 만드는 데 필수적이다.
객체의 크기에 대한 결정
객체는 크기와 종류, 갯수 등에서 매우 다양함을 보일 수 있다. 컴포넌트 하나 하나를 모두 객체로 표현할 수 있지만, 애플리케이션 전체를 하나의 객체로 만들 수도 있다. 적당한 객체의 규모는 어떻게 결정해야 할까?
디자인 패턴을 통해 이런 문제에 대한 답도 얻을 수 있다. Facade패턴은 서브 시스템을 어떻게 객체로 표현할 수 있는지 설명하고 있고, Flyweight 패턴은 규모는 작지만 개수는 많은 객체를 다르는 방법을 설명하고 있다. Abstract Factory패턴과, Builder 패턴은 다른 객체를 생성하는 책임만 갖는 객체를 만들어낸다.
** 이러한 디자인 패턴들을 잘 이해한다면, 객체의 크기, 종류, 규모를 정하는데 용이할 것이다.
객체 인터페이스 명세
객체가 선언하는 모든 오퍼레이션은 오퍼레이션의 이름, 파라미터로 받는 객체들, 오퍼레이션의 반환 값을 명세하며, 이를 오퍼레이션 시그니처(Signature)라고 한다. 인터페이스란 객체가 정의하는 오퍼레이션의 모든 시그니처들을 일컫는 말로, 객체의 인터페이스는 객체가 받아서 처리할 수 있는 오퍼레이션들의 집합니다. 객체 인터페이스에 정의된 시그니처와 일치하는 어떤 요청이 객체에 전달되면, 객체는 오퍼레이션을 수행함으로써 그 요청을 처리한다.
** 말이 어려워서 그렇지 java나 C#등의 언어에서 구현하는 Interface를 생각하면 조금 더 이해하기 용이 할 것이다. 아래의 예시 코드들을 보며 시그니쳐, 요청, 오퍼레이션 등이 어떤것인지 확인해보자.
public interface IControl
{
void Paint();
}
public interface ISurface
{
void Paint();
}
public class SampleClass : IControl, ISurface
{
void IControl.Paint()
{
System.Console.WriteLine("IControl.Paint");
}
void ISurface.Paint()
{
System.Console.WriteLine("ISurface.Paint");
}
}
SampleClass sample = new SampleClass();
IControl control = sample;
ISurface surface = sample;
// The following lines all call the same method.
//sample.Paint(); // Compiler error.
control.Paint(); // Calls IControl.Paint on SampleClass.
surface.Paint(); // Calls ISurface.Paint on SampleClass.
// Output:
// IControl.Paint
// ISurface.Paint
타입은 특정 인터페이스를 상징할 때 사용하는 용어이다. 객체가 Window 타입을 갖는 다는 것은 Window 인터페이스에 정의한 오퍼레이션들을 모두 처리할 수 있다는 것을 의미한다. 객체는 여러 타입을 가질 수 있고, 서로 다른 객체가 하나의 타입을 공유할 수도 있다.
객체의 인터페이스에 정의된 오퍼레이션들 중 일부는 A 타입이 정의하는 오퍼레이션이고 다른 일부는 B 타입이 정의한 오퍼레이션일 수 있다. 같은 타입의 두 객체는 인터페이스의 일부를 공유해야 한다. 인터페이스가 다른 인터페이스를 부분집합으로 포함하는 경우도 있는데, 다른 인터페이스를 포함하는 인터페이스를 서브타입(subtype)이라고 하고, 다른 인터페이스가 포함하는 인터페이스를 슈퍼타입(super type)이라고 한다. 서브타입은 슈퍼타입의 인터페이스를 상속한다고 이야기한다. 서브타입이 슈퍼타입을 상속하면, 서브타입은 수퍼타입에 정의된 오퍼레이션을 포함하게 된다.
** 인터페이스의 상속을 이야기 하는 것인듯. 아래 코드를 참고해보면 될 것 같다.
interface ILogger // 슈퍼타입
{
void WriteLog(string message);
}
interface IFormattableLogger:ILogger // 서브타입
{
void WriteLog(string format, params Object[] args);
}
class Logger : IFormattableLogger {
// 2개 인터페이스의 구현을 강제받음
public void WriteLog(string message)
{
Console.WriteLine("{0} {1}", DateTime.Now.ToLocalTime(), message);
}
public void WriteLog(string format, params Object[] args)
{
String message = String.Format(format, args);
Console.WriteLine("{0} {1}", DateTime.Now.ToLocalTime(), message);
}
}
class MainApp {
static void Main()
{
IFormattableLogger logger = new Logger();
logger.WriteLog("The world is not flat.");
logger.WriteLog("{0} + {1} = {2}", 1, 1, 2);
}
}
인터페이스의 개념은 객체지향 시스템에서 가장 기본적인 것이다. 객체는 인터페이스를 통해서 자신을 드러낸다. 외부에서 객체에 대해 알 수 있는 것은 인터페이스 밖에 없기 때문에 인터페이스를 통해서만 처리를 요청할 수 있다. 객체의 인터페이스는 구현에 대해서는 전혀 알려주지 않는다. 그러므로 서로 다른 객체는 인터페이스에 정의한 요청의 구현 방법을 자유롭게 선택할 수 있다. 이 의미는 동일한 인터페이스를 갖는 두 객체가 완전히 다른 구현을 가질 수 있다는 것이다.
그러므로 객체에 요청이 전달되면, 요청과 이를 받는객체에 따라서 수행되는 처리 방식이 달라진다. 동일한 요청이라도 처리하는 객체들이 다른 객체라면, 이 요청에 대한 구현을 어떻게 했는가에 따라서 다른 결과가 나올 수 있다. 요청과 요청을 처리할 객체를 런타임 시에 결정하는 기법을 동적 바인딩(dynamic binding)이라고 한다.
동적 바인딩은 요청이 어떻게 구현되어 어떤 결과를 만들어 낼지를 런타임에 결정할 수 있음을 의미한다. 결과적으로 프로그램을 작성할 때는 객체가 어떤 특정 인터페이스를 갖도록 작성한다는 뜻이다. 즉, 이 객체는 요청을 처리할 정확한 인터페이스를 갖고 있다. 또한 동적 바인딩은 프로그램이 기대하는 객체를 동일한 인터페이스를 갖는 다른 객체로 대체할 수도 있다. 이런 대체성을 다형성(Polymorphism)이라고 하는데, 이는 객체지향 시스템의 핵심 개념이다. 다형성은 클라이언트의 정의를 단순화하고 객체들 간의 결합도를 없애고, 런타임 시에는 서로 간의 관련성을 다양화 할 수 있게 해준다.
디자인 패턴은 인터페이스에 정의해야 하는 중요 요소가 무엇이고 어떤 종류의 데이터를 주고받아야 하는지 식별하여 인터페이스를 정의할 수 있도록 도와준다. (또는 인터페이스에 넣지 말아야 할 것을 알려주기도 한다).
디자인 패턴은 인터페이스들 간의 관련성도 정의한다. 특히 클래스들 간에 유사한 인터페이스를 정의하도록 하거나 클래스들의 인터페이스에 여러가지 제약을 정의하고 있다.
다음 포스팅에서는 객체 구현을 명세하는 방법과, 재사용과 관련 된 내용을 다뤄보도록 하겠다.
GoF의 디자인패턴 참고
'Development > 디자인패턴' 카테고리의 다른 글
디자인 패턴을 이용하는 방법 (3) (0) | 2022.04.19 |
---|---|
디자인 패턴을 이용하는 방법 (2) (0) | 2022.04.18 |
디자인패턴의 조직화, 관계도 (0) | 2022.04.13 |
디자인 패턴의 종류 (0) | 2022.04.13 |
디자인 패턴이란 무엇인가? (0) | 2022.04.12 |