[오브젝트] 유연한 설계

개방-폐쇄 원칙

OCP(Open-Closed Principle)의 중요한 키워드는 확장수정이다.

  • 확장에 대해 열려 있다 : 어플리케이션의 요구사항이 변경될 때 이 변경에 맞게 새로운 동작을 추가해서 어플리케이션의 기능을 확장할 수 있다.
  • 수정에 대해 닫혀 있다 : 기존의 코드를 수정하지 않고도 어플리케이션의 동작을 추가하거나 변경할 수 있다.

이런 OCP원칙은 런타임 의존성과 컴파일타임 의존성에 관한 이야기이다. 런타임 의존성은 실행시점의 협력관계를, 컴파일타임 의존성은 코드에서 나타나는 협력관계를 의미한다. 그리고 이 둘은 서로 다른 구조를 가지게 된다.

OCP의 핵심은 추상화에 의존하는 것이다. 추상화란 핵심적인 부분만 남기고 불필요한 부분은 생략함으로써 복잡함을 극복하는 방법이다. 이를 통해 생략된 문맥에 적절한 내용을 채움으로 각 문맥에 적합한 기능을 부여할 수 있다. OCP의 관점에서 생략되지 않고 남겨지는 부분은 다양한 상황에서의 공통점을 반영한 추상화의 결과물이다. 공통적인 부분은 문맥이 바뀌더라도 변하지 않아야 한다. 즉, 수정할 필요가 없어야 한다. 또한 주의해야할 점은 추상화를 했다고 해서 모든 수정에 대해 설계과 폐쇄되는 점은 아니란 것이다. 변경에 의한 파급효과를 최대한 피하기 위해서는 변하는 것과 변하지 않는것이 무엇인지를 이해하고 이를 추상화의 목적으로 삼아야 한다.

생성 사용 분리

결합도가 높을수록 OCP를 따르는 구조를 설계하기 어려워진다. 알아야하는 지식이 많을수록 결합도는 높아진다. 특히 객체 생성에 대한 지식은 높은 결합도를 초래할 수 있다. 유연하고 재사용 가능한 설계를 원한다면 객체를 생성하는 책임과 객체를 사용하는 책임을 분리해야 한다.

사용과 생성을 분리하는데 사용되는 보편적인 방법은 객체를 생성할 책임을 클라이언트로 옮기는 것이다. 이를 통해 특정 컨택스트에 종속되지 않고 추상화된 객체의 인스턴스를 사용하는데 주력하는 코드를 만들 수 있다.

하지만 위와 같은 전제는 클라이언트는 특정 컨텍스트에 묶여도 된다는 전제가 있기 때문에 가능하다. 만약 클라이언트 또한 특정 컨텍스트에 묶이지 않아야 한다면 어떨까? 이럴때는 추상 팩토리 패턴이 해답이 될 수 있다. 이런 팩토리 패턴은 도메인 모델에 속하지 않고 순수한 기술적 결정을 위해 만들어진 가공의 객체이다.

크레이그 라만은 시스템을 객체로 분해하는데 두 가지 방식이 존재한다 말한다. 하나는 표현적 분해이다. 표현적 분해는 도메인에 존재하는 사물 또는 개념을 표현하는 객체들을 이용해 시스템을 분해하는 것 이다. 도메인 모델에 담긴 개념과 관계를 따라 소프트웨어와 도메인 사이의 표현적 차이를 최소화하는 것을 목적으로 한다. 또 다른 하나는 행위적 분해이다. 종종 도메인 개념을 표현하는 객체에 책임을 할당하는 것으로는 부족한 경우가 발생한다. 도메인 모델은 설계를 위한 중요한 출발점이지만 단순히 출발점이라는 사실을 기억해야 한다. 그렇기 때문에 실제 동작하는 어플리케이션을 위해 데이터베이스와 같은 도메인 개념을 초월하는 기계적 개념들을 필요로 하기도 한다. 모든 책임을 도메인 객체에 할당하면 낮은 응집도, 높은 결합도, 재사용성 저하와 같은 문제점에 봉착하게 될 가능성이 높아진다. 이 경우 도메인 개념을 표현한 객체가 아닌 설계자가 편의를 위해 임의로 만들어낸 가공의 객체에게 책임을 할당해서 문제를 해결해야 한다.

의존성 주입

사용하는 객체가 아닌 외부의 독립적인 객체가 인스턴스를 생성한 후 이를 전달해서 의존성을 해결하는 방법을 의존성 주입이라 한다. 의존성을 주입하는 방법에는 크게 3가지가 있다.

  • 생성자 주입
  • setter 주입
  • 메서드 주입

세 방법은 모두 앞에서 살펴보았고 이 외에도 의존성을 주입하는 다양한 방법이 존재한다. 그 중 하나가 Service Locator 패턴이다. 다음 코드는 Service Locator 패턴의 한 예이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class ServiceLocator {
private static ServiceLocator instance = new ServiceLocator();
private Student student;

public static Student student() {
return instance.student;
}

public static void provide(Student student) {
instance.student = student;
}

private ServiceLocator() {
}
}

public class Teacher {
private Student student;

public Teacher() {
this.student = ServiceLocator.student();
}
}
1
2
ServiceLocator.provide(new Junior( ... ));
Teacher teacher = new Teacher();

이 패턴을 보면 의존성을 해결할 수 있는 간단한 방법처럼 보인다. 하지만 이 패턴의 가장 큰 단점은 의존성을 감춘다는 점이다. 또한 디버깅도 어렵게 만든다. 숨겨진 의존성은 명시적인 의존성보다 나쁘다.

의존성 역전 원칙

객체 사이의 협력이 존재할 때 그 협력의 본질을 담고 있는 것은 상위 수준의 정책이다. 즉, 어떤 협력에서 중요한 정책이나 의사결정, 비즈니스의 본질을 담고 있는 것은 상위 수준의 클래스이다. 이런 상위 수준의 클래스가 하위 수준의 클래스에 의존한다면 하위 수준의 변경에 의해 상위 수준 클래스가 영향을 받게 될 것이다. Teacher가 하위 수준의 Junior를 변경한다고 해서 영향을 받아서는 안된다. 상위 수준인 Teacher의 변경으로 인해 하위 수준인 Junior가 영향을 받아야 한다. 이 경우에도 해답은 추상화이다.

상위 수준 클래스가 하위 수준 클래스에 의존하는 경우

추상화를 통해 문제를 해결한 경우

이를 통해 알 수 있는 내용은 다음과 같다.

  1. 상위 수준의 모듈은 하위 수준의 모듈에 의존해선 안된다. 둘 다 추상화에 의존해야 한다.
  2. 추상화는 구체적인 상항에 의존해서는 안된다. 구체적인 사항은 추상화에 의존해야 한다.

이를 의존성 역전 원칙이라 부른다. 역전이란 단어가 붙은 의미는 의존성 역전 원칙을 따르는 설계는 의존성의 방향이 전통적인 절차형 프로그래밍과는 반대 방향으로 나타나기 때문이다.

역전은 의존성의 방향뿐만 아니라 인터페이스의 소유권에도 적용된다. 객체지향 프로그래밍 언어에서 어떤 구성요소의 소유권을 결정하는 것은 모듈이다. 자바는 패키지, C++ C#은 네임스페이스를 사용해 모듈을 구성한다.

인터페이스가 서버 모듈 쪽에 위치하는 모듈 구조

위 그림에서 구체 클래스인 Teacher, Junior, Senior 모두 추상 클래스인 Student에 의존한다. 따라서 OCP를 준수하고 의존성 역전 원칙도 따르고 있다고 볼 수 있다. 하지만 Teacher를 다양한 컨텍스트에서 재사용하기 위해서는 불필요한 클래스들이 Teacher와 함께 배포되어야 한다. Teacher가 Student에 의존하고 있다는 사실에 주목해야 한다. Teacher를 정상적으로 컴파일하기 위해서는 Studnet 클래스가 필요하다. 코드 컴파일에 성공하기 위해 함께 존재해야 하는 코드를 정의하는 것이 컴파일타임 의존성이다. 문제는 Student의 패키지 내에 Senior와 Junior가 함께 있다는 점이다. 의존성 정의에 따라 Teacher는 Student를 수정하지 않을 경우에는 영향을 받지 않아야 한다. 이는 코드 수정 측면에서는 사실이지만 컴파일 측면에서는 사실이 아니다. Student가 포함된 패키지 내의 어떤 클래스가 수정되더라도 패키지 전체가 재배포 되어야 한다. 이로 인해 이 패키지에 의존하는 Teacher는 재컴파일 되어야 한다. 이와 같은 맥락으로 이런 상황은 어플리케이션 전체로 번진다. 이는 빌드 시간을 상승시킨다. 이는 다음과 같이 해결할 수 있다.

인터페이스의 소유권을 역전시킨 객체지향적인 모듈 구조

추상화를 별도의 독립적인 패키지가 아니라 클라이언트가 속한 패키지에 포함시켜야 한다. 그리고 함께 재사용될 필요가 없는 클래스들은 별도의 독립적인 패키지에 모아야 한다. 이를 Separated Interface 패턴이라 한다. 의존성 역전 원칙에 따라 상위 수준의 협력 흐름을 재사용하기 위해서는 추상화가 제공하는 인터페이스의 소유권 역시 역전시켜야 한다.

Author: Song Hayoung
Link: https://songhayoung.github.io/2021/01/16/Object/object9/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.