Skip to content

객체 지향 디자인

개발 과정

계획 → 분석 → 설계 → 구현 → 테스트

설계 단계의 객체가 분석 모델에서부터 만들어진 경우도 있지만, 객체지향 설계 모델이 항상 실세계와 대응 관계를 갖는 것은 아니다. 분석 모델의 객체는 실세계 객체이며, 설계 모델의 객체는 구현 수준에 가까운 객체(리스트, 배열 등)이기 때문이다.

디자인 패턴은 이러한 추상적인 개념과 이것을 구체화한 객체를 잡아낼 수 있게 도와주며, 유연한 설계를 돕는다.

객체 설정

객체지향 프로그래밍(OOP)에서 객체는 데이터프로시저로 이루어진 것이며, 이것들이 모여 객체지향 프로그램을 구성한다.

프로시저는 메소드(Method) 또는 연산(Operation)이라고도 한다.

객체는 요청(Request) 또는 메시지(Message)를 사용자에게서 받아 연산을 수행한다.

  • 요청: 객체가 연산을 실행하게 하는 유일한 방법
  • 연산: 객체의 내부 데이터 상태를 변경하는 유일한 방법

이러한 접근의 제약 사항으로 객체의 내부 데이터는 캡슐화된다. 이것의 의미는 객체 외부에서 객체 내부 데이터에 직접 접근이 불가하며, 객체의 내부 데이터 타입을 알 수 없다는 것이다.

시스템을 구성할 객체의 분할을 결정하는 것은 여러 요인들을 고려해야 하므로 매우 어려운 작업이다. 고려해야 할 요인에는 캡슐화, 크기 결정, 종속성, 유연성, 성능, 진화, 재사용성 등이 있다.

객체 구현 명세

OMT(Object-Modeling Technique)란 소프트웨어 구성 요소를 그래픽 표기로 모델링하는 기법을 말한다.

Object 모델링 → Dynamic 모델링 → Functioanl 모델링
  • Object 모델링: 객체 다이어그램으로 표시하며 객체 사이의 관계를 정하는 모델링
  • Dynamic 모델링: 상태 다이어그램(상태도)를 이용하여 제어 흐름을 모델링
  • Functional 모델링: 자료 흐름도(DFD)를 이용하여 다수의 프로세스 사이의 자료 흐름을 중심으로 처리 과정을 표현한 모델링

클래스

class

객체의 구현은 클래스(Class)를 이용하여 객체의 내부 데이터와 연산을 정의한다.

insta045

객체는 클래스가 인스턴스화 됨으로 생성된다. 따라서 해당 관계는 위와 같이 표현할 수 있다.

서브 클래스

subcl009

하나의 클래스는 다른 클래스로 상속되기도 하는데 이러한 경우 상속하는 클래스는 부모 클래스(Parent Class or Super Class), 상속되는 클래스는 서브 클래스(Sub Class or Child Class)라고 한다.

상속은 부모 클래스의 구성을 모두 갖는 새로운 클래스를 만드는 것이다.

추상 클래스

absclass

추상(Abstract Class)는 모든 서브클래스 사이의 공통되는 인터페이스를 정의하는 것이다. 이러한 클래스는 상속을 통해 만들어진 구체클래스(Concrete Class)인 서브클래스에서 구체적인 명세가 이루어진다. 일반적인 클래스와는 다르게 추상 클래스는 단지 정의만 하고 구현은 하지 않는다.

보통 추상 클래스는 이텔릭체로 표기한다.

믹스인 클래스

mixin

믹스인 클래스(Mixin Class)는 서브클래스에게 인터페이스를 제공하려는 목적을 가지는 클래스이다. 인스턴스로 만들 의도가 없다는 점에서 추상클래스와 비슷하나, 다수의 클래스가 서브클래스에게 인터페이스를 상속하기 위한 것이라는 점에서 큰 차이가 있다.

다중 상속과의 차이점 `다중상속`은 `여러 인터페이스`의 기능을 상속한 하나의 클래스를 만드는 것이다. 반면 `믹스인`은 위에서 설명했듯이 `여러 클래스`의 기능을 상속하여 하나의 클래스를 만든다. `인터페이스`는 선언만을 할 뿐 구현이 전혀 이루어져 있지 않다면, `클래스`는 구현까지 모두 이루어져 있다. 따라서 클래스는 다중 상속할 경우 동일한 이름의 메소드의 구현이 다른 부분에서 충돌이 일어날 수 있다. 믹스인은 이러한 충돌을 해결해서 클래스를 다중상속할 수 있도록 하는 것이다.

인터페이스 명세

객체는 인터페이스로 자신을 드러내므로 외부에서 객체를 알 수 있는 방법은 인터페이스 밖에 없다. 따라서 인터페이스를 통해서만 처리 요청이 가능하며 구현에 대해서는 전혀 알 수 없다.

  • 시그니처(Signature): 객체가 선언하는 연산으로 연산명, 매개변수, 리턴 값을 명세
  • 인터페이스(Interface): 객체가 정의하는 모든 시그니처로 객체가 처리할 수 있는 연산의 집합
  • 타입(Type): 특정 인터페이스
  • 서브 타입(Sub-Type): 다른 인터페이스를 포함하는 인터페이스
  • 슈퍼 타입(Super-Type): 다른 인터페이스가 포함하는 인터페이스

서로 다른 객체라 할 지라도 동일한 타입이라 한다면 해당 인터페이스에서 지원하는 연산을 동일한 방식으로 요청이 가능하지만, 그 내부 구현에 따라 다른 결과를 전달 받을 수 있다.

  • 동적 바인딩: 위 원리를 활용하여 런타임에 사용자 요청을 처리하는 객체를 결정하는 것
  • 다형성: 동일한 인터페이스를 갖는 다른 객체로 대체할 수 있는 성질로, 객체지향 시스템의 핵심 개념

디자인 패턴을 이용하면 인터페이스에 정의해야 하는 요소가 무엇이고 어떤 데이터를 주고 받아야 하는지 식별할 수 있게 도와준다. 메멘토 패턴은 객체 내부 상태를 어떻게 저장하고 캡슐화해야 하는지를 정의함으로 나중에 그 상태를 복구할 수 있는 방법을 알려준다.

디자인 패턴은 또한 인터페이스 간의 관련성도 정의한다. 유사한 인터페이스를 정의하거나 인터페이스에 여러 제약을 정의한다. 데코레이터 패턴프록시 패턴은 대상 객체와 장식되고 중재하는 객체가 동일한 인터페이스를 갖도록 한다. 또한 방문자 패턴에서 방문자 인터페이스는 방문자 객체가 방문하는 개체의 인터페이스를 반영한다.

클래스 상속 vs 인터페이스 상속

  • 클래스: 객체가 어떻게 구현되는지르 정의, 객체의 내부 상태와 연산에 대한 구현 방법을 정의
  • 인터페이스(타입): 객체가 응답할 수 있는 요청의 집합을 정의

하나의 객체는 여러 타입을 가질 수 있으며, 서로 다른 클래스의 객체들이 동일한 타입을 가질 수도 있다. 즉, 객체의 구현은 다르더라도 인터페이스는 같을 수 있다는 의미다.

  • 클래스 상속: 개체의 구현을 정의할 때 이미 정의된 객체를 기반으로 하는 것.
  • 인터페이스 상속(서브타이핑): 어떤 객체가 다른 객체 대신에 사용(동적 바인딩)될 수 있는 경우 사용.

C++에서 순수한 인터페이스 상속은 순수 가상 함수를 정의한 추상 클래스를 public으로 상속한다. 반면 클래스 상속은 private으로 상속하게 되는데 이는 부모 클래스의 구현을 캡슐화하기 위함이다. 이로 인해 서브클래스의 사용자에게는 부모클래스에 정의된 연산이 공개되지 않게 된다.

책임 연쇄 패턴에 나오는 객체들은 반드시 동일한 타입을 가져야 하지만, 이들이 구현을 공유하지는 않는다. 복합체 패턴에서 Component 클래스는 공통의 인터페이스를 정의하고 Composite 클래스는 공통의 구현을 정의한다. 커맨드, 옵저버, 상태, 전략 패턴은 순수 인터페이스인 추상 클래스를 써서 구현하는 경우가 많다.

인터페이스 프로그래밍

클래스 상속은 부모 클래스에 정의된 구현을 재사용해서 빠르게 새로운 기능을 구현하는 것이 가장 큰 목적이다. 즉, 상속을 이용하면 중복되는 코드를 줄여준다는 장점이 있다. 다만 상속은 이런 단순한 작업뿐 아니라 더 복잡한 일도 이루어낼 수 있다.

동일한 인터페이스를 갖는 객체들을 공통된 추상 클래스를 상속하도록 하여 인터페이스를 공유할 수 있다. 이렇게 만들어진 인터페이스를 공유하는 객체군들은 모두 부모 클래스에 정의된 요청을 처리할 수 있게 된다. 앞에서도 언급했지만 이러한 원리를 이용하면 프로그램이 동작중인 런타임 중에 사용자 요청을 처리할 객체를 동적으로 결정할 수 있다. 이는 객체 지향에서 중요한 개념인 다형성이라는 성질이다.

추상 팩토리, 빌더, 팩토리 메서드, 프로토타입, 복합체 패턴에서는 구체 클래스에서 인스턴스를 생성하도록 한다. 객체 생성 과정을 추상화 함으로써 인스턴스화할 때 인터페이스와 구현을 연결하는 방법을 제공한다.

재사용 가능한 소프트웨어 개발

객체 합성

객체 합성은 클래스 상속에 대한 대안으로 다른 객체들을 여러개 붙여서 새로운 기능 혹은 객체를 구성하는 것이다. 객체를 합성하기 위해서는 합성하는 객체의 인터페이스를 명확하게 정의해야만 한다. 이런 방법의 재사용을 블랙박스 재사용(black-box reuse)라고 한다. 객체의 내부가 공개되지 않고 인터페이스를 통해서 재사용되기 때문이다.

클래스 상속은 부모 클래스의 내부가 서브클래스에 공개되므로 화이트박스 재사용(white-box reuse)라 한다.

public class Computer {
    private CPU cpu;
    private Memory memory;
    private MainBoard mainBoard;
    private PowerSupply powerSupply:

    public Computer() {
        cpu = new CPU();
        memory = new Memory();
        mainBoard = new MainBoard();
        powerSupply = new PowerSupply():
    }

    public void run() {
        powerSupply.on();
        mainboard.run();
        cpu.run();
        memory.run();
    }
}

위 예제는 컴퓨터를 구성하는 부품 객체들을 합성하여 컴퓨터 클래스를 정의한 것이다. 객체 합성을 사용하면 각 클래스를 한 가지 작업에 특화시켜 응집도가 높고 결합도가 낮은 소프트웨어를 만들 수 있다는 점에서 클래스 상속보다 좋은 방법이다.

객체 위임

위임(delegation)은 객체 합성 원리를 이용하여 특정 객체의 필요한 기능만을 가져오는 것이다. 상속은 필요없는 부분도 다 물려받아야 된다는 점과 대조된다.

import java.util.ArrayList;

public class MyStack {
  private ArrayList<String> list = new ArrayList<>();

  public boolean isEmpty() {
    return list.isEmpty();
  }
  public int getSize() {
    return list.size();
  }
  public String peek() {
    return list.get(getSize() - 1);
  }
  public String pop() {
    String text = list.get(getSize() - 1);
    list.remove(getSize() - 1);
    return text;
  }
  public void push(String text) {
    list.add(text);
  }
}

위 예제는 기존에 존재하는 ArrayList를 활용하여 MyStack 클래스를 생성하는 것이다. 해당 예제를 보면 알 수 있듯이 객체 위임은 부모 클래스가 제공하는 기능을 가져오기만 하는 것이 아니라 재정의해서 사용할 수 있다는 특징을 가진다.

매개변수 타입

JAVA에서 제네릭(generic)이라 하며 c++에서는 템플릿(template)이라 하는 매개변수 타입을 활용하면 프로그램을 보다 동적으로 짤 수 있게 된다. 타입을 컴파일 전에 정의하지 않고 런타임 중에 정의하는 방법이다.

class ArrayList<E> implements List<E> {
    ...
}

private ArrayList<String> stringList = new ArrayList<>();
private ArrayList<Integer> integerList = new ArrayList<>();

제네릭 형식의 ArrayList가 정의한 것을 활용하여 문자열 리스트와 정수형 리스트를 생성하는 예제이다. 제네릭을 활용하지 않았다면 각각의 리스트를 만들기 위한 클래스가 필요했을 것이다.

변화에 대비한 설계

재사용을 최대화하기 위해서는 새로운 요구사항과 기존 요구사항에 발생한 변경을 예측하여 앞으로의 시스템 설계가 진화할 수 있도록 해야한다.

  • 특정 클래스에서 객체 생성
  • 문제점: 특정 인터페이스가 아닌 특정 구현에 종속
  • 해결방법: 추상 팩토리, 팩토리 메서드, 프로토타입
  • 특정 연산에 대한 의존성
  • 문제점: 요청을 만족하는 한 가지 방법에만 종속
  • 해결방법: 책임 연쇄, 커맨드
  • 하드웨어와 소프트웨어 플랫폼에 대한 의존성
  • 문제점: 플랫폼 이식에 어려움
  • 해결방법: 추상 팩토리, 브릿지
  • 객체의 표현이나 구현에 대한 의존성
  • 문제점: A가 B의 구현을 알고 있다면 B를 변경할 때, A도 변경해야 함
  • 해결방법: 추상 팩토리, 브릿지, 메멘토, 프록시
  • 알고리즘 의존성
  • 문제점: 알고리즘을 변경하기 어려움
  • 해결방법: 빌더, 이터레이터, 전략, 템플릿 메서드, 방문자
  • 높은 결합도
  • 문제점: 클래스를 독립적으로 사용할 수 없음
  • 해결방법: 추상 팩토리, 브릿지, 책임 연쇄, 커맨드, 중재자, 옵저버
  • 서브클래싱을 통한 기능 확장
  • 문제점: 새로운 클래스마다 객체를 재정의해야 함
  • 해결방법: 브릿지, 책임 연쇄, 테커레이터, 옵저버, 전략
  • 클래스 변경이 편하지 못한 점
  • 문제점: 변경사항에 서브클래스 다수를 수정해야 하는 불편함
  • 해결방법: 어댑터, 테커레이터, 방문자

디자인 패턴 고르기

  • 패턴의 문제 해결방법 파악
  • 패턴의 의도 파악
  • 패턴들 간의 관련성을 파악
  • 비슷한 목적의 패턴들을 모아서 공부
  • 재설계의 원인을 파악
  • 설계에서 가변성을 가져야 하는 부분이 무엇인지 파악

참고문헌


Last update : 29 octobre 2023
Created : 1 novembre 2020