도서

[도서] 클린아키텍처 Clean Architecture 정리 5~8장

Parkour 2022. 4. 14. 23:08

5장 객체지향프로그래밍(Oject-Oriented, OO)

  • 객체지향프로그래밍을 정의하는 여러가지 말들이 있지만 대체로 모호하다.
  • 그 중 자주 언급되는 캡슐화, 상속, 다형성 세 가지 개념 알아보자.

 

캡슐화

  • OO언어는 private 멤버 데이터와 public 멤버 함수를 구분지음으로써 각각 응집할 수 있는 방법을 제공한다.
  • 구분선 밖의 데이터는 외부에서 알 수 없다(은닉). 일부 함수 만이 노출된다.!
  • 이런 개념은 C언어에서 완벽하게 제공한다. 데이터 구조와 함수를 헤더파일(.h)에 선언하고 구현파일(.c)에 정의하기때문에.
  • 오히려 C++의 형태로 OO가 등장했을때 완벽한 캡슐화가 깨짐.
  • C++의 컴파일러는 클래스 인스턴스 크기를 알아야해서 헤더파일에 클래스의 멤버변수를 선언해야만했음
  • 이때문에 헤더파일을 사용하는 외부에서 멤버변수를 알 수 있게됨
  • 자바와 C#으로 오면서 헤더/구현 파일을 나누는 방식을 버리면서 캡슐화는 더욱 훼손됨 → 이를 보완하기위해 public, private, protected 등의 키워드 도입
  • 결론 - OO는 기존의 강력한 캡슐화를 약화시켰다.

 

상속

  • 간단히 말해서 어떤 변수와 함수를 하나의 유효범위로 묶어서 재정의 하는것
  • C언어도 불편하지만 상속을 흉내낼수는있다. → 특정 데이터구조에 대응(변수선언 순서 일치)하는 상위 집합의 데이터구조를 생성한다면 이 구조는 특정 데이터구조처럼 쓰일 수 있다.
  • 이 과정에서 Upcasting(상위집합 데이터 구조 → 특정 데이터구조)이 이뤄지는데 OO에서는 이런 Upcasting을 명시할 필요없이 암묵적으로 이뤄짐. 상위집합의 데이터 구조가 특정 데이터구조를 상속한다고 볼 수 있음
  • 결론 - OO는 편리한 상속을 제공한다.

 

다형성

  • OO이전에도 C를 통해 다형성을 표현할 수는 있었다. → 하나의 객체가 다양한 형태로 취급되게 할 수 있었다.
    • A 데이터 구조에 함수를 가리키는 포인터가 있다. 이때 B 데이터 구조에서 A데이터 구조를 C라고 선언하고 C를 통해서 A 데이터 구조의 포인터가 가리키는 함수를 호출할 수 있다.
    • A 데이터 구조는 B 데이터 구조안에서 C라는 형태로 취급된다.
  • 함수의 포인터를 직접 컨트롤 하는 행위에는 지켜야하는 관례가 있다.
    • 포인터는 초기화해야한다.
    • 포인터를 통해 모든 함수를 호출해야한다.
  • 결론 - OO는 이런 관례를 없애줘서 오류와 버그없이 다형성을 이용할 수 있게 해줌

 

다형성이 가진 힘

  • 다형성을 이용하면 새로운 장치가 생길때마다 프로그래밍을 해주지않아도된다.
    • 기존에 복사프로그램 - 운영체제 표준함수(open,close...)일때는 새로운 입출력 장치에 맞는 복사프로그램을 새로 프로그래밍해줬어야함.새로운 입출력장치가 생기더라도 입출력드라이버만 설치해주면 복사프로그램은 얼마든지 작동함
    • 다형성을 이용해서 운영체제 표준함수를 참조하는 한 가지 형태로 입출력드라이버를 구현하고 이 드라이버를 쓰면 복사프로그램 - 입출력드라이버 - 운영체제 표준함수의 구조가됨
    • 입출력드라이버는 복사프로그램의 플러그인형태이며 복사프로그램은 장치독립적이다.
  • 결론 - OO는 포인터를 사용하면서 수반되는 위험을 제거함으로써 다형성을 이용한 플러그인이라는 개념을 아키텍처, 프로그램까지 적용할 수 있도록 해줌

 

의존성 역전

  • 기존에는 고수준의 함수가 저수준의 함수를 호출함. 동시에 고수준의 함수는 저수준의 함수를 의존함 → 즉 프로그램 제어흐름과 의존성의 방향은 동일하다. (의존성 방향 고수준함수 → 저수준함수)
  • OO가 다형성을 안전하고 편리하게 사용할 수 있게 함으로써 제어흐름과 의존성의 방향을 프로그래머가 설정할 수 있게됨
    • 고수준의 함수와 저수준의 함수 사이에 인터페이스를 두고 저수준의 함수가 인터페이스를 의존하도록 한다. (의존성 방향 고수준함수 → 인터페이스 ← 저수준함수)
      1. 저수준의 함수는 인터페이스에 의존하기때문에 인터페이스의 함수를 구현한다.
      2. 고수준의 함수는 인터페이스를 통해 저수준의 함수를 호출한다.
      3. 고수준의 함수는 인터페이스의 함수를 호출했지만 인터페이스의 함수를 구현한 저수준의 함수를 호출한 꼴이 된다. (다형성 덕분에 저수준 함수는 인터페이스의 한 가지 형태로 취급,대입될 수 있다)
    • 원래는 고수준의 함수가 저수준의 함수를 의존했지만 인터페이스를 둠으로써 저수준의 함수가 인터페이스를 의존하게되고 의존성 방향은 제어흐름과 반대방향이된다.
  • 의존성역전을 이용하면 UI나 데이터베이스(저수준 세부사항)이 중요한 업무 규칙에(고수준 정책) 의존할 수있도록 할 수 있다. 이는 제어흐름과는 반대이다.(의존성방향 UI → 업무규칙 ← 데이터베이스)
  • 즉, UI와 데이터베이스는 업무규칙의 플러그인이된다.(서로서로 분리할 수있다) → UI와 데이터베이스의 변경사항이 업무규칙에 영향을 미칠 수 없다.
  • 각각의 컴포넌트들이 개별적이기때문에 독립적으로 배포할 수있다. → 배포 독립성
  • 배포 독립성 덕분에 개발자들은 따로 개발할 수 있다 → 개발 독립성
  • 결론 - OO는 다형성을 이용해서 소스코드의 의존성에 대한 절대적인 권한을 부여함. 덕분에 고수준 정책과 저수준 세부사항의 독립성을 보장해준다. 또한 독립적인 개발과 배포를 가능하게 해준다.

 

6장 함수형 프로그래밍

  • 함수형 프로그램의 기본 개념은 람다 계산법의 영향을 받음
  • 람다 계산법의 핵심 개념은 함수의 추상화
    • 함수는 이름을 가질 필요 없다.
    • 함수의 입력 변수 또한 이름을 가질 필요 없다.

 

정수를 제곱하기

  • 함수형언어 *클로저를 사용한 예시 함수를 통해 함수가 실행될때 대입한 변수 x는 실행이 끝날때까지 변하지 않는다는걸 알 수있다. 이는 실행 중 상태가 변하는 자바의 가변 변수와 대조되는 경우이다.
    • 클로저 - 원래 (외부)함수 내에서 사용하는 특정 변수는 함수의 실행이 끝나고나면 접근할 수가 없음. 하지만 (외부)함수 내에서 특정 변수를 사용하는 (내부)클로저 함수를 반환하게하면 (내부)클로저 함수가 선언될때 (외부)함수의 환경이 같이 묶이게된다. → 이렇게되면 (외부)함수를 호출했을때 (내부)함수가 호출되면서 (외부)함수의 특정 변수에 접근할 수있게된다.

 

불변성과 아키텍처

  • 우리가 만드는 동시성 어플리케이션의 경합, 교착, 동시 업데이트등의 문제는 가변변수때문이다.
  • 저장 공간이 무한하지 않기 때문에 가변변수를 사용할 수밖에 없다.

 

가변성의 분리

  • 불변 컴포넌트에서 가변 컴포넌트의 변수를 처리하는 동안 락을 거는 트랙잭션 메모리 같은 기법들이 있지만 복잡한 프로그램에서 완벽하게 동작하길 기대하기는 어렵다.
  • 경합, 교착, 동시업데이트등의 문제를 예방하기 위해 프로그래머는 가능한 불변컴포넌트에서 많은 처리를 해야한다. 또한 가변컴포넌트에서 가능한 코드를 빼내야한다.

 

이벤트 소싱

  • 데이터의 상태를 저장하지 않고 데이터 처리 기록 자체를 저장한다는 발상
  • 상태를 알고싶으면 처음부터 지금까지의 처리기록을 계산하면 됨
  • 기술의 발달로 가용 리소스가 늘어나면서 가능해짐 → 완전한 함수형 프로그래밍이 가능해짐

 

결론

  • 구조적 프로그래밍, 객치제향 프로그래밍, 함수형 프로그래밍 세 패러다임은 프로그래머에게 지켜야하는 규율을 부과함
    • 구조적 프로그래밍 - 모듈, 컴포넌트를 테스트를 통해 반증할 수 있는 단위로 만들어라.
    • 객체지향 프로그래밍 - 고수준의 정책과 저수준의 세부사항들을 분리해서 독립적인 개발, 배포가 가능하도록해라.
    • 함수형 프로그래밍 - 동시성 프로그래밍의(경합,교착 등) 문제를 피하기위해 불변컴포넌트에서 가능한 많은 처리를해라.
  • 지난 반세기동안 세 패러다임을 제외하고는 앨런튜링이 최초의 코드를 작성할때와 크게 다를 것이 없다. → 모든 프로그램은 순차, 분기, 반복, 참조로 이루어진다.

 

7장 SRP: 단일 책임 원칙

  • 결론 - 하나의 응집된 코드(모듈)은 하나의 액터(기능 요구집단)의 요청사항을 위해서 동작해야한다.
    • 우발적 중복: 하나의 모듈이 여러 집단에서 쓰일때 한 집단에서 변경 요청사항이 들어와서 모듈을 수정하면 다른 집단에 영향을 끼치게된다.
    • 병합: 각 집단에서 서로 모듈을 수정했을때 변경사항이 충돌하게된다.
  • 단일 책임 원칙을 위반하지 않기 위해서는 데이터와 메서드를 분리해야한다. → 각 집단은 데이터 클래스만 공유하고 메서드는 각자 의 클래스에 정의해서 쓰면된다.
  • 대신 프로그래머가 분리된 클래스들을 관리해야하는 불편함이 발생한다.
  • 다수의 클래스를 하나로 묶는 퍼사드(건물외관)패턴을 이용해서 불편을 해결할 수 있다.
  • 퍼사드 클래스에 분리된 클래스들을 객체로 생성하고 요청 메소드를 전달만해줌.
  • SRP는 클래스와 메서드 수준에서 지켜야할 원칙이지만 컴포넌트 수준과 아키텍처 수준에서 다시 나올예정

 

8장 OCP: 개방-폐쇄 원칙

  • 소프트웨어 개체의 행위는 확장 할 수 있다. 하지만 이때 개체를 변경해서는 안된다.
  • OCP를 지키지 않으면 요구집단의 변경 요청사항을 반영하기위해서 많은 코드를 수정하게된다.

 

사고실험

  • OCP를 잘 적용했다면 새로운 기능을 추가할때 기존의 코드를 변경을 최소화 할 수 있다.
    • 각 요소들에 SRP를 적용하고 의존성을 체계화함으로써 OCP를 적용할 수 있음
      • SRP를 적용한다 → 각각 다른 행위에 대한 책임을 다른 클래스로 분리해서 관리한다.
      • 의존성을 체계화한다 → 모든 의존성이 가장 중요한 업무규칙을 향하도록 한다.

 

방향성 제어

  • 인터페이스를 이용해서 의존성 역전을 하면 저수준에서 고수준으로 의존성 방향성을 제어할 수 있다.

 

정보 은닉

  • 인터페이스는 의존성 역전 뿐만아니라 정보를 은닉하기위해서도 쓰임
  • A클래스가 B클래스를 의존하고 B클래스가 C클래스를 의존할때 A클래스는 C클래스의 모든걸 알 수 있음 → 추이종속성(A → B → C ⇒ A → C)
  • B클래스와 C클래스 사이에 인터페이스 <I>를 두어 C클래스의 일부 기능에만 의존하도록 할 수 있음 → 사용하지 않는 요소에는 절대 의존해서는 안되기때문에(A → B → <I> ←C)

결론 - 저수준에서의 변경사항으로부터 고수준의 요소를 보호할 수 있기도하고 (방향성 제어) 고수준에서의 변경사항으로부터 저수준의 요소를 보호할 수도 있음(정보 은닉)

 

결론

  • OCP는 시스템 확장은 쉽게, 동시에 확장으로 인한 변경은 적게하는것을 목표로한다.
  • 그러기 위해선 컴포넌트들을 책임에 따라 분리하고 저수준의 변경사항으로부터 고수준을 보호할 수 있는 의존성 체계를 갖추어야함