도서
[도서] 클린아키텍처 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가 다형성을 안전하고 편리하게 사용할 수 있게 함으로써 제어흐름과 의존성의 방향을 프로그래머가 설정할 수 있게됨
- 고수준의 함수와 저수준의 함수 사이에 인터페이스를 두고 저수준의 함수가 인터페이스를 의존하도록 한다. (의존성 방향 고수준함수 → 인터페이스 ← 저수준함수)
- 저수준의 함수는 인터페이스에 의존하기때문에 인터페이스의 함수를 구현한다.
- 고수준의 함수는 인터페이스를 통해 저수준의 함수를 호출한다.
- 고수준의 함수는 인터페이스의 함수를 호출했지만 인터페이스의 함수를 구현한 저수준의 함수를 호출한 꼴이 된다. (다형성 덕분에 저수준 함수는 인터페이스의 한 가지 형태로 취급,대입될 수 있다)
- 원래는 고수준의 함수가 저수준의 함수를 의존했지만 인터페이스를 둠으로써 저수준의 함수가 인터페이스를 의존하게되고 의존성 방향은 제어흐름과 반대방향이된다.
- 고수준의 함수와 저수준의 함수 사이에 인터페이스를 두고 저수준의 함수가 인터페이스를 의존하도록 한다. (의존성 방향 고수준함수 → 인터페이스 ← 저수준함수)
- 의존성역전을 이용하면 UI나 데이터베이스(저수준 세부사항)이 중요한 업무 규칙에(고수준 정책) 의존할 수있도록 할 수 있다. 이는 제어흐름과는 반대이다.(의존성방향 UI → 업무규칙 ← 데이터베이스)
- 즉, UI와 데이터베이스는 업무규칙의 플러그인이된다.(서로서로 분리할 수있다) → UI와 데이터베이스의 변경사항이 업무규칙에 영향을 미칠 수 없다.
- 각각의 컴포넌트들이 개별적이기때문에 독립적으로 배포할 수있다. → 배포 독립성
- 배포 독립성 덕분에 개발자들은 따로 개발할 수 있다 → 개발 독립성
- 결론 - OO는 다형성을 이용해서 소스코드의 의존성에 대한 절대적인 권한을 부여함. 덕분에 고수준 정책과 저수준 세부사항의 독립성을 보장해준다. 또한 독립적인 개발과 배포를 가능하게 해준다.
6장 함수형 프로그래밍
- 함수형 프로그램의 기본 개념은 람다 계산법의 영향을 받음
- 람다 계산법의 핵심 개념은 함수의 추상화
- 함수는 이름을 가질 필요 없다.
- 함수의 입력 변수 또한 이름을 가질 필요 없다.
정수를 제곱하기
- 함수형언어 *클로저를 사용한 예시 함수를 통해 함수가 실행될때 대입한 변수 x는 실행이 끝날때까지 변하지 않는다는걸 알 수있다. 이는 실행 중 상태가 변하는 자바의 가변 변수와 대조되는 경우이다.
- 클로저 - 원래 (외부)함수 내에서 사용하는 특정 변수는 함수의 실행이 끝나고나면 접근할 수가 없음. 하지만 (외부)함수 내에서 특정 변수를 사용하는 (내부)클로저 함수를 반환하게하면 (내부)클로저 함수가 선언될때 (외부)함수의 환경이 같이 묶이게된다. → 이렇게되면 (외부)함수를 호출했을때 (내부)함수가 호출되면서 (외부)함수의 특정 변수에 접근할 수있게된다.
불변성과 아키텍처
- 우리가 만드는 동시성 어플리케이션의 경합, 교착, 동시 업데이트등의 문제는 가변변수때문이다.
- 저장 공간이 무한하지 않기 때문에 가변변수를 사용할 수밖에 없다.
가변성의 분리
- 불변 컴포넌트에서 가변 컴포넌트의 변수를 처리하는 동안 락을 거는 트랙잭션 메모리 같은 기법들이 있지만 복잡한 프로그램에서 완벽하게 동작하길 기대하기는 어렵다.
- 경합, 교착, 동시업데이트등의 문제를 예방하기 위해 프로그래머는 가능한 불변컴포넌트에서 많은 처리를 해야한다. 또한 가변컴포넌트에서 가능한 코드를 빼내야한다.
이벤트 소싱
- 데이터의 상태를 저장하지 않고 데이터 처리 기록 자체를 저장한다는 발상
- 상태를 알고싶으면 처음부터 지금까지의 처리기록을 계산하면 됨
- 기술의 발달로 가용 리소스가 늘어나면서 가능해짐 → 완전한 함수형 프로그래밍이 가능해짐
결론
- 구조적 프로그래밍, 객치제향 프로그래밍, 함수형 프로그래밍 세 패러다임은 프로그래머에게 지켜야하는 규율을 부과함
- 구조적 프로그래밍 - 모듈, 컴포넌트를 테스트를 통해 반증할 수 있는 단위로 만들어라.
- 객체지향 프로그래밍 - 고수준의 정책과 저수준의 세부사항들을 분리해서 독립적인 개발, 배포가 가능하도록해라.
- 함수형 프로그래밍 - 동시성 프로그래밍의(경합,교착 등) 문제를 피하기위해 불변컴포넌트에서 가능한 많은 처리를해라.
- 지난 반세기동안 세 패러다임을 제외하고는 앨런튜링이 최초의 코드를 작성할때와 크게 다를 것이 없다. → 모든 프로그램은 순차, 분기, 반복, 참조로 이루어진다.
7장 SRP: 단일 책임 원칙
- 결론 - 하나의 응집된 코드(모듈)은 하나의 액터(기능 요구집단)의 요청사항을 위해서 동작해야한다.
- 우발적 중복: 하나의 모듈이 여러 집단에서 쓰일때 한 집단에서 변경 요청사항이 들어와서 모듈을 수정하면 다른 집단에 영향을 끼치게된다.
- 병합: 각 집단에서 서로 모듈을 수정했을때 변경사항이 충돌하게된다.
- 단일 책임 원칙을 위반하지 않기 위해서는 데이터와 메서드를 분리해야한다. → 각 집단은 데이터 클래스만 공유하고 메서드는 각자 의 클래스에 정의해서 쓰면된다.
- 대신 프로그래머가 분리된 클래스들을 관리해야하는 불편함이 발생한다.
- 다수의 클래스를 하나로 묶는 퍼사드(건물외관)패턴을 이용해서 불편을 해결할 수 있다.
- 퍼사드 클래스에 분리된 클래스들을 객체로 생성하고 요청 메소드를 전달만해줌.
- SRP는 클래스와 메서드 수준에서 지켜야할 원칙이지만 컴포넌트 수준과 아키텍처 수준에서 다시 나올예정
8장 OCP: 개방-폐쇄 원칙
- 소프트웨어 개체의 행위는 확장 할 수 있다. 하지만 이때 개체를 변경해서는 안된다.
- OCP를 지키지 않으면 요구집단의 변경 요청사항을 반영하기위해서 많은 코드를 수정하게된다.
사고실험
- OCP를 잘 적용했다면 새로운 기능을 추가할때 기존의 코드를 변경을 최소화 할 수 있다.
- 각 요소들에 SRP를 적용하고 의존성을 체계화함으로써 OCP를 적용할 수 있음
- SRP를 적용한다 → 각각 다른 행위에 대한 책임을 다른 클래스로 분리해서 관리한다.
- 의존성을 체계화한다 → 모든 의존성이 가장 중요한 업무규칙을 향하도록 한다.
- 각 요소들에 SRP를 적용하고 의존성을 체계화함으로써 OCP를 적용할 수 있음
방향성 제어
- 인터페이스를 이용해서 의존성 역전을 하면 저수준에서 고수준으로 의존성 방향성을 제어할 수 있다.
정보 은닉
- 인터페이스는 의존성 역전 뿐만아니라 정보를 은닉하기위해서도 쓰임
- A클래스가 B클래스를 의존하고 B클래스가 C클래스를 의존할때 A클래스는 C클래스의 모든걸 알 수 있음 → 추이종속성(A → B → C ⇒ A → C)
- B클래스와 C클래스 사이에 인터페이스 <I>를 두어 C클래스의 일부 기능에만 의존하도록 할 수 있음 → 사용하지 않는 요소에는 절대 의존해서는 안되기때문에(A → B → <I> ←C)
결론 - 저수준에서의 변경사항으로부터 고수준의 요소를 보호할 수 있기도하고 (방향성 제어) 고수준에서의 변경사항으로부터 저수준의 요소를 보호할 수도 있음(정보 은닉)
결론
- OCP는 시스템 확장은 쉽게, 동시에 확장으로 인한 변경은 적게하는것을 목표로한다.
- 그러기 위해선 컴포넌트들을 책임에 따라 분리하고 저수준의 변경사항으로부터 고수준을 보호할 수 있는 의존성 체계를 갖추어야함