좋은 아키텍처를 만드는 일은 OO 설계 원칙을 이해하고 응용하는 데서 출발한다.

그렇다면 OO란 무엇인가?
-> 데이터와 함수의 조합
-> 만족스럽진 않다. o.f()와 f(o)가 다르다는 의미를 내포하기 때문
-> 실제 세계를 모델링하는 새로운 방법
-> 얼버무리는 수준
-> 현실 세계와 의미적으로 가깝기 때문에, 사용하면 SW를 쉽게 이해할 수 있다고 한다.
-> 하지만 정의가 너무 모호하다

캡슐화

OO를 정의하는 요소 중 하나로 데이터와 함수를 쉽고 효과적으로 캡슐화하는 방법을 OO 언어가 제공하기 때문이다.
이를 통해 데이터와 함수가 응집력 있게 구성되고 서로 구분 지을 수 있다.
구분선 바깥(외부)에서 데이터는 은닉되고, 일부 함수만이 외부에 노출된다.
실제 OO 언어에서는 각 클래스의 private/public 으로 표현된다.

EX
//point.h
struct Point;
struct Point* makePoint(double x, double y);
double distance (struct Point *p1, struct Point *p2);

//point.c
#include "point.h"
...

struct Point{
double x,y;
};

struct Point* makepoint(...){...}
double distance(...){...}

point.h를 사용하는 측에서 struct Point의 멤버에 접근할 방법이 전혀 없다.
사용자는 makePoint() 와 distance()를 호출할 수 있지만, Point 구조체의 데이터 구조와 함수가 어떻게 구현되었는지는 모른다.
이것은 완벽한 캡슐화이며, 위와 같은 OO가 아닌 언어에서도 가능하다

C언어 개발자들은 이러한 방식을 활용했다.
데이터 구조와 함수를 헤더 파일에 선언하고, 구현 파일에서 구현한다.
그리고 사용자는 구현 파일에 작성된 항목에 대해서는 어떻게도 접근할 수 없었다.

이후에 C++이 등장했고, C언어가 제공하던 완전한 캡슐화가 깨졌다.
C++ 컴파일러는 기술적인 이유로 클래스의 멤버 변수를 해당 클래스의 헤더파일에 선언할 것을 요구했다.
-> 기술적인 이유 : 클래스의 인스턴스 크기를 알 수 있어야 한다.

EX
//point.h
class Point{
public:
Point(double x, double y);
double distance(const Point& p) const;
private:
double x;
double y;
}

//point.cc
...... 구현 .......

이제 point.h 헤더 파일을 사용하는 측에서는 멤버 변수인 x, y를 알 수 있다.
물론 접근하는 것은 private으로 막겠지만, 사용자는 멤버 변수가 존재한다는 사실을 알게 된다.
예를 들어, 멤버 변수의 이름이 바뀌면 point.cc 파일은 다시 컴파일 해야 한다 -> 캡슐화가 깨졌다.

언어에 public, private, protected 를 도입하여 불완전한 캡슐화를 보완하기는 했다.
하지만 이는 컴파일러가 헤더 파일에서 멤버 변수를 볼 수 있어야 했기에 임시 방편일 뿐...

자바와 C#은 헤더와 구현체를 분리하는 방식을 보렸다. 이로써 캡슐화는 더욱 더 훼손되었다.
클래스 선언과 정의를 구분하는게 불가능하다.

-> 이 때문에 OO가 강력한 캡슐화에 의존한다는 정의는 받아들이기 어렵다.
-> 실제로 많은 OO 언어가 캡슐화를 강제하지 않는다.

상속

OO 언어가 더 나은 캡슐화를 제공하지 못했지만, 상속만큼은 OO 언어가 제공했다.
상속이란?
-> 어떤 변수와 함수를 하나의 유효 범위로 묶어서 재정의하는 일에 불과하다
사실상 OO 언어가 있기 전에도 프로그래머는 직접 이러한 방식을 구현할 수 있었다.

다형성

OO 언어가 있기 전에 다형성을 표현할 수 있는 언어가 있었다.
하지만 함수 포인터를 사용하기 때문에, 위험하다.
OO는 다형성을 좀 더 안전하고 편리하게 사용할 수 있게 해준다.

다형성이 가진 힘

그렇다면 다형성은 뭐가 좋은 것일까?
새로운 입출력 장치가 생긴다면 프로그램에는 어떤 변화가 생기는가?
새로운 장비에서도 복사 프로그램이 동작하려면 수정해야 하는가? -> 변경이 필요치 않다.
왜냐하면 복사 프로그램의 소스 코드는 입출력 드라이버의 소스코드에 의존하지 않기 때문이다.
-> 장치에 의존적인 수많은 프로그램을 만들고 나서, 하나의 프로그램이 다른 장치에서도 같은 동작을 할 수 있도록 만드는 것을 바래왔다.

의존성 역전

다형성을 안전하고 편리하게 적용할 수 있는 메커니즘이 등장하기 전 SW는 어떤 모습이었을까?
전형적으로 main 함수가 고수준 함수를 호출하고
고수준 함수는 다시 중간 수준 함수
중간 수준 함수는 다시 저수준 함수를 호출한다.
이러한 호출 트리에서 소스코드 의존성 방향은 반드시 제어흐름을 따르게 된다

main 함수가 고수준 함수를 호출하기 위해서는 고수준 함수가 포함된 모듈의 이름을 지정해야만 한다.
C는 #include
Java는 import 이다.

실제로 모든 호출 함수는 피호출 함수가 포함된 모듈의 이름을 명시적으로 지정해야 한다.

하지만 다형성이 끼어들면 무언가 특별한 일이 일어난다.

HL1 모듈은 ML1 모듈의 F() 함수를 호출한다.
소스 코드에서는 HL1 모듈은 인터페이스를 통해 F() 함수를 호출한다.
이 인터페이스는 런타임에는 존재하지 않는다.
HL1은 단순히 ML1 모듈의 함수 F()를 호출할 뿐이다.

하지만 ML1과 I 인터페이스 사이의 소스 코드 의존성(상속 관계)이 제어흐름과는 반대인 점을 주목하자.
이를 의존성 역전(DI)라고 부른다.

OO 언어가 다형성을 안전하고 편리하게 제공한다는 사실은 소스 코드 의존성을 어디에서든 역전시킬 수 있다는 뜻이기도 하다.

소스 코드의 의존성은 소스 코드 사이에 인터페이스를 추가함으로써 방향을 역전시킬 수 있다.

이러한 접근법을 사용한다면, OO 언어로 개발된 시스템을 다루는 SW 아키텍트는 소스 코드 의존성 전부에 대해 방향을 결정할 수 있는 권한을 갖는다.
즉, 소스코드 의존성이 제어흐름의 방향과 일치하지 않아도 된다.
호출하는 모듈이든 호출되는 모듈이든 관계없이 소스 코드 의존성을 원하는 방향으로 설정할 수 있다.

그렇다면 이 힘으로 무엇을 할 수 있는가?
예를 들어) 업무 규칙이 DB와 UI에 의존하는 대신에, 소스코드 의존성을 반대로 배치하여 DB와 UI가 업무 규칙에 의존하게 만들 수 있다.

즉, UI와 DB가 업뮤 규칙의 플로그인이 된다는 뜻이다. 다시 말해 업무 규칙의 소스 코드에서는 UI나 DB를 호출하지 않는다.
따라서 업무 규칙을 UI와 DB와는 독립적으로 배포할 수 있다.
UI나 DB에서 발생한 변경사항은 업무 규칙에 일절 영향을 미치지 않는다.
이 컴포넌트들은 개별적이며 독립적으로 배포 가능하다.

결론

OO란 무엇인가?
아키텍처 관점에서는 OO란 다형성을 이용하여 전체 시스템의 모든 소스 코드 의존성에 대한 절대적인 제어 권한을 획득할 수 있는 능력이다
각각의 세부사항을 포함하는 모듈들을 독립적으로 개발하고 배포할 수 있다.

+ Recent posts