모종닷컴

[클린 아키텍처] 5장 ~ 6장 본문

독서나 해볼까

[클린 아키텍처] 5장 ~ 6장

모종 2022. 12. 28. 22:50
반응형

클린 아키텍처 - 소프트웨어  구조와 설계의 원칙 (로버트 C. 마틴)

5장 : 객체지향 프로그래밍

Object-Oriented = 객체지향

객체지향(Object-Oriented, OO)이란 무엇인가요?

  • 객체지향은 데이터와 함수의 조합 ? 객체지향이라는 패러다임이 나오기 훨씬 이전부터 프로그래머는 데이터 구조를 함수에 전달해 왔다. 때문에 객체지향이 데이터와 함수의 조합이라는 말은 성립하지 않는다.
  • 실제 세계를 모델링하는 새로운 방법 ? 객체지향을 사용하면 소프트웨어를 좀 더 쉽게 이해할 수 있다는 데 있는 듯하다. 하지만 의도가 불분명하고 정의가 모호하다.
  • 객체지향의 본질 ? 객체지향의 본질을 설명하기 위해 캡슐화, 상속, 다형을 말한다. 객체지향이 이 세 가지 개념을 적절한 조합한 것이거나, 또는 객체지향 언어는 최소한 이 세 가지 요소를 반드시 지원해야 한다고 말한다. 그렇다면 이 3가지 특성에 대한 개념을 알아보자

캡슐화

객체 지향을 정의하는 요소 중 하나로 캡슐화로 언급하는 이유는 데이터와 함수를 쉽고 효과적으로 캡슐화하는 방법을 객체지향언어에서 제공하기 때문이다.

하지만 이러한 캡슐화라는 개념은 객체지향에만 국한된 것이 아니다. 오히려 C 언어에서 더 완벽한 캡슐화가 가능하다. C 언어의 헤더 파일을 이용하면 사용하는 측에서 헤더 파일에 정의된 함수들을 호출할 수는 있지만 데이터 구조와 함수가 어떻게 구현되었는지에 대해서는 조금도 알지 못하기 때문에 이것이 바로 완벽한 캡슐화이다. 완벽한 캡슐화의 예시로 책에서는 아래와 같은 예시를 들었다.

예시

// point.h 파일
struct Point;

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

struct Pont {
  double x,y;
};

// main.c 파일
#include point.h

현재의 객체지향 언어에서는 public, private, protected 키워드를 도입함으로써 불완전한 캡슐화를 어느 정도 보완하기는 했다. 하지만 이는 컴파일러가 클래스의 멤버 변수를 볼 수 있어야 했기 때문에 조치한 임시방편이다. 객체지향 컴파일러는 클래스의 인스턴스 크기를 알아야 하는데 이러한 이유로 클래스의 멤버 변수를 클래스파일에 선언했다. 이 때문에 객체지향이 강력한 캡슐화에 의존한다는 정의는 받아들이기 힘들다. 오히려 C 언어에서 누렸던 완벽한 캡슐화를 약화시켜 온 것이라고 책에서는 주장한다.

상속

“객체 지향 언어가 더 나은 캡슐화를 제공하지는 못했지만, 상속만큼은 객체 지향 언어가 확실히 제공했다.” 틀린 말은 아니다. 하지만 상속이란 단순히 어떤 변수와 함수를 하나의 유효 범위로 묶어서 재정의하는 일에 불과하며 객체지향언어 이전에도 C 프로그래머는 언어의 도움 없이 이 상속을 구현할 수 있었다.

예시

// point.h 파일
struct Point;
double distance (struct Point* p1, struct Point *p2);

// point.c 파일
#include "point.h"
#include <math.h>

struct Pont {
  double x,y;
};

double distance (struct Point* p1, struct Point *p2) {
  double dx = p1->x - p2->x;
  double dy = p1->y - p2->y;
  return sqrt(dx*dx+dy*dy);
}

// namedPoint.h
struct NamedPoint;

// namedPoint.c
#include "namedPoint.h"

struct NamedPoint;
struct NaemdPoint* makeNamedPoint(double x, double y, char* name);

struct NamedPoint {
  double x,y;
  char* name;
}

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

int main() {
  struct NamedPoint* origin = makeNamedPoint(0.0, 0.0, "origin");
  struct NamedPoint* upperRight = makeNamedPoint(1.0, 1.0, "origin");
  printf("distance=%f\\n", distance((struct Point*) origin, (struct Point*) upperRight));
}

main 로직을 보면 NamedPoint 데이터 구조가 마치 Point 데이터 구조로부터 파생된 구조인 것처럼 동작한다는 사실을 볼 수 있다. 이는 NamedPoint에 선언된 두 변수의 순서가 Point와 동일하기 때문이다.

따라서 객체 지향 언어가 고안되기 이전에도 상속과 비슷한 기법이 사용되어 왔다. 하지만 상속을 흉내 내는 요령은 있었지만, 사실상 현재 객체 지향 언어가 제공하는 상속만큼 편리한 방식은 아니었다.

객체 지향 언어의 다중 상속 및 업캐스팅까지 고려한다면 객체 지향 언어가 상속이라는 새로운 개념을 만든 것은 아니나 편리한 방식으로 상속을 제공했다고 볼 수 있다.

다형성

#include <stdio.h>

void copy() {
  int c;
  while ((c=getchar()) != EOF)
    putchar(c);
}

getchar()는 STDIN에서 문자를 읽고, putchar()는 STDOUT으로 문자를 쓴다. STDIN, STDOUT의 경우 자바 형식의 인터페이스로, 자바에서는 각 장치별로 구현체가 있다. C는 이러한 인터페이스는 없다. 그렇다면 C는 함수를 호출할 때 어떤 방식으로 문자를 읽는 장치 드라이버를 호출할까?

유닉스 운영체제의 경우 모든 입출력 장치 드라이버가 다섯 가지 표준 함수(open, close, read, write, seek)를 제공할 것을 요구한다. File 데이터 구조는 이 함수들을 가리키는 포인터들을 포함한다.

struct FILE {
  void (*open)(char* name, int mode);
  void (*close)();
  int (*read)();
  void (*write)(char);
  void (*seek)(long index, int mode);
};

콘솔용 입출력 드라이버에서는 이들 함수를 정의하여 FILE 데이터 구조를 함수에 대한 주소와 함께 로드한다. 이렇게 로드된 코드는 아래와 같은 방식으로 사용할 수 있다.

extern struct FILE* STDIN;

int getchar() {
  return STDIN -> read();
}

이처럼 함수를 가리키는 포인터를 응용한 기법이 모든 객체 지향이 지닌 다형성의 근간이 된다. 고로 객체 지향에서 새롭게 만든 것이 아니며 다형성을 좀 더 안전하고 편리하게 사용할 수 있게 해 준 것이다.

함수에 대한 포인터를 직접 사용하여 다형적 행위를 만드는 이 방식에는 문제가 있는데, 함수 포인터는 위험하다는 사실이다. 이러한 기법은 프로그래머가 특정 관례를 수동으로 따라야 하는 방식이다. 즉, 포인터를 초기화는 관계, 포인터를 통해서만 모든 함수를 호출하는 관례 등을 기억해야 한다. 만약 이러한 관례를 지키는 것을 망각한다면 버그가 발생하고, 이렇게 발생된 버그는 없애기가 아주 힘들다.

객체 지향 언어는 이러한 관례를 없애주며, 따라서 실수할 위험이 없다.

다형성이 가진 힘.

새로운 입출력 장치가 생긴다면 프로그램에는 어떤 변화가 생길까? 다형성에 의해 아무런 변경도 필요치 않다. 프로그램 소스 코드는 입출력 드라이버의 소스 코드에 의존하지 않기 때문에 다시 컴파일할 필요도 없다.

입출력 드라이버가 프로그램의 플러그인이라고 볼 수 있다. 이처럼 입출력 장치 독립성을 지원하기 위해 플러그인 아키텍처가 만들어졌고, 등장 이후 거의 모든 운영체제에서 구현되었다. 그럼에도 함수를 가리키는 포인터를 사용하면 위험을 수반하기 때문에 이러한 개념을 확장하여 적용하지 않았는데, 객체 지향의 등장으로 언제 어디서든 플러그인 아키텍처를 적용할 수 있게 되었다.

의존성 역전

다형성을 안전하고 편리하게 적용할 수 있는 메커니즘이 등장하기 전 소프트웨어는 어떤 모습이었을까?

전형적인 호출 트리의 경우 main 함수가 고수준 함수를 호출하고, 고수준 함수는 다시 중간 수준 함수를, 중간 수준 함수는 다시 저수준 함수를 호출하는 구조이다. 이러한 호출 트리에서 소스 코드 의존성의 방향은 반드시 제어흐름을 따르게 된다.

이러한 제약 조건으로 인해 제어흐름은 시스템의 행위에 따라 결정되며, 소스 코드 의존성은 제어 흐름에 따라 결정되는데 이러한 부분에 다형성이 적용되면 의존성을 역전시킬 수 있다.

ML과 I 인터페이스 사이의 소스 코드 의존성이 제어흐름과 반대가 된다. 이렇게 인터페이스를 중간에 추가하게 되면 소스 코드 의존성은 방향을 역전시킬 수 있다.

결론

객체 지향이 무엇인가? 이 질문에 답하기 위해 수많은 의견과 답변이 있었다. 하지만 소프트웨어 아키텍트 관점에서 정답은 명백하다. 객체 지향이란 다형성을 이용하여 시스템의 모든 소스 코드 의존성에 대한 절대적인 제어 권한을 획득할 수 있는 능력이다. 객체 지향을 사용하면 플러그인 아키텍처를 구성할 수 있고, 고수준의 정책을 포함하는 모듈은 저수준의 세부사항을 포함하는 모듈에 대해 독립성을 보장할 수 있다. 저수준의 모듈은 고수준의 정책을 포함하는 모듈과는 독립적으로 개발하고 배포할 수 있다.

6장: 함수형 프로그래밍

불변성과 아키텍처

프로그래밍에서 일어날 수 있는 경합 조건(=레이스 컨디션), 교착 상태(deadlock), 동시 업데이트(concurrent update) 문제는 모두 가변 변수로 인해 발생한다.

이러한 가변 변수가 만약 갱신되지 않는다는 전제가 생긴다면 위와 같은 문제가 일어나지 않는다.

가변성의 분리

불변성과 관련하여 가장 주요한 타협 중 하나는 애플리케이션, 또는 애플리케이션 내부의 서비스를 가변 컴포넌트와 불변 컴포넌트로 분리하는 일이다. 불변 컴포넌트는 순수하게 함수형 방식으로만 작업이 처리되며, 어떤 가변 변수도 사용되지 않는다. 불변 컴포넌트는 변수의 상태를 변경할 수 있는 다른 컴포넌트와 서로 통신한다.

상태 변경은 컴포넌트를 동시성 문제에 노출하기에 트랜잭션 메모리(transactional memory)와 같은 기법을 사용하여 동시 업데이트와 경합 조건 문제로부터 가변 변수를 보호한다.

이벤트 소싱

고객의 계좌 잔고를 관리하는 은행 애플리케이션을 생각해 보자. 입금 트랜잭션과 출금 트랜잭션이 실행되면 잔고를 변경해야 한다.

이제 계좌 잔고를 변경하는 대신 트랜잭션 자체를 저장한다고 상상해 보자. 잔고 조회를 요청할 때마다 계좌 개설 시점부터 발생한 모든 트랜잭션을 단순히 더한다. 이 전략에서는 가변 변수가 하나도 필요 없다.

하지만 이러한 접근법에는 한계가 있다. 시간이 지날수록 트랜잭션 수는 끝없이 증가하고, 잔고 계산에 필요한 컴퓨팅 자원은 걷잡을 수 없이 커진다. 따라서 이러한 전략을 취하기 위해서는 무한한 저장 공간과 무한한 처리 능력이 필요하다.

하지만 이 전략이 영원히 동작하도록 만들 필요는 없다. 애플리케이션의 수명주기 동안만 문제없이 동작할 정도의 저장 공간과 처리 능력만 있으면 된다. 그리고 “이벤트 소싱”에 깔려 있는 기본 발상이 이러하다.

이벤트 소싱은 상태가 아닌 트랜잭션을 저장하자는 전략이다. 상태가 필요해지면 단순히 상태의 시작점부터 모든 트랜잭션을 처리한다. 물론 매일 자정에 상태를 계산한 후 저장한다면 자정 이후의 트랜잭션만을 처리해도 된다.

애플리케이션의 수명주기 동안 사용할 공간 또한 현시점에서는 충분하다고 느낄 정도로 크기에 문제가 되지 않는다. 데이터 저장소에서 변경과 삭제가 발생하지 않으므로 동시 업데이트 문제 또한 일어나지 않는다.

그리고 현재의 소스 코드 버전 관리 시스템이 바로 이러한 방식으로 동작하고 있다.

결론

  • 구조적 프로그래밍은 제어흐름의 직접적인 전환에 부과되는 규율
  • 객체 지향 프로그래밍은 제어흐름의 간접적인 전환에 부과되는 규율
  • 함수형 프로그래밍은 변수 할당에 부과되는 규율

위의 3가지의 패러다임은 우리의 권한이나 능력에 무언가를 보태지 않는다. 우리가 코드를 작성하는 방식의 형태를 한정시켜 해서는 안되는 것을 제약한다.

반응형

'독서나 해볼까' 카테고리의 다른 글

카프카 책 추천  (0) 2023.10.14
[클린 아키텍처] 7장 ~ 11장  (2) 2023.01.08
[클린 아키텍처] 3장 ~ 4장  (0) 2022.12.17
[클린 아키텍처] 챕터 1  (0) 2022.12.11