byungil
3장. 패러다임 개요
구조적 프로그래밍
구조적 프로그래밍은 제어흐름의 직접적인 전환에 대해 규칙을 부과한다.
객체지향 프로그래밍
함수 포인터를 특정 규칙에 따라 사용하는 과정을 통해 필연적으로 다형성이 등장하게 된다.
객체 지향 프로그래밍은 제어흐름의 간접적인 전환에 대해 규칙을 부과한다.
함수형 프로그래밍
할당문에 대해 규칙을 부과하는 패러다임
생각할 거리
각 패러다임은 프로그래머에게서 권한을 박탈한다.
어느 패러다임도 새로운 권한을 부여하지 않고, 부정적인 의도를 갖는 일종의 추가적인 규칙을 부과한다.
패러다임은 무엇을 해서는 안 되는지를 말해준다.
4장. 구조적 프로그래밍
증명
다익스트라는 goto 문장이 모듈을 더 작은 단위로 재귀적으로 분해하는 과정에 방해가 되는 경우가 있음을 발견했다.
실제로 뵘과 야코피니는 다익스트라보다 2년 전에 프로그램이 순차, 분기, 반복이라는 3가지 구조만으로 표현할 수 있음을 증명함
기능적 분해
구조적 프로그래밍을 통해 모듈을 증명 가능한 더 작은 단위로 즉, 기능적으로 분해할 수 있게 되었음
거대한 문제 기술서를 받더라도 문제를 고수준의 기능들로 분해할 수 있음. 그리고 이들 각 기능은 다시 저수준의 함수들로 분해할 수 있고, 이러한 분해 과정을 끝없이 반복할 수 있음
게다가 이렇게 분해한 기능들은 구조적 프로그래밍의 제한된 제어 구조를 이용하여 표현할 수 있음
테스트
다익스트라는 “테스트는 버그가 있음을 보여줄 분, 버그가 없음을 보여줄 수는 없다"고 말한 적이 있음
다시 말해 프로그램이 잘못되었음은 증명 가능하지만, 프로그램이 맞다고는 증명할 수 없음
결론
소프트웨어 아키텍터는 모듈, 컴포넌트, 서비스가 쉽게 반증 가능하도록(테스트하기 쉽도록) 만들기 위해 분주히 노력해야 하며, 이를 위해 구조적 프로그래밍과 유사한 제한적인 규칙들을 받아들여 활용해야 함
5장. 객체 지향 프로그래밍
캡슐화
데이터와 함수가 응집력 있게 구성된 집단에서 데이터는 구분선 바깥에서 데이터는 은닉되고 일부 함수만이 외부에 노출된다.
OO가 아닌 언어에서도 충분히 가능하며, OO 언어에서 오히려 깨진다.
실제로 많은 OO 언어가 캡슐화를 강제하지 않고, 단지 프로그래머가 캡슐화된 데이터를 우회해서 사용하지 않을 거라는 믿음을 기반으로 함
상속
OO 언어가 더 나은 캡슐화를 제공하지는 못했지만, 상속만큼은 확실히 제공했다.
다형성
다형성 역시 함수를 가리키는 포인터를 응용한 것일 뿐이며 OO가 새롭게 만든 것은 없다.
다형성을 좀 더 안전하고 편리하게 사용할 수 있게 해줌
6장. 함수형 프로그래밍
정수를 제곱하기
일반적으로 자바와 같은 프로그램은 가변 변수를 사용하는데, 이는 프로그램 실행 중에 상태가 변할 수 있음을 의미함
하지만 함수형 프로그램에서는 가변 변수가 없음
변수가 한번 초기화되면 절대로 변하지 않는다는 것
예시: 리스트에서 짝수만 필터링하고 제곱한 후 출력하기
불변성과 아키텍처
아키텍트는 왜 변수의 가변성을 염려하는가?
경합 조건(race condition), 교착 상태, 동시 갱신 문제 모두 가변 변수로 인해 발생함
어떠한 변수도 갱신되지 않는다면 경합 조건이나 동시 업데이트 문제가 일어나지 않는다.
가변성의 분리
가변 컴포넌트와 불변 컴포넌트를 분리하는 것
불변 컴포넌트에서는 함수형 방식으로만 작업이 처리, 어떠한 가변 변수도 사용되지 않음
현명한 아키텍트라면 가능한 많은 처리를 불변 컴포넌트로 옮기고, 가변 컴포넌트에서는 가능한 많은 코드를 빼내야 함
7장. SRP: 단일 책임 원칙
하나의 모듈은 하나의, 오직 하나의 사용자 또는 이해관계자에 대해서만 책임져아 한다.
징후1: 우발적 중복
세 가지 메서드가 서로 매우 다른 세 명의 액터를 책임지기 때문이다.
징후2: 병합(Merge)
소스 파일에 다양하고 많은 메서드를 포함하면 병합이 발생하기 쉬운데, 서로 다른 액터를 책임진다면 그 가능성이 더욱 높음
이 문제를 벗어나기 위해 서로 다른 액터를 뒷받침하는 코드를 서로 분리해야 한다.
8장. OCP: 개방-폐쇄 원칙
확장에는 열려 있어야 하고, 변경에는 닫혀 있어야 한다.는 원칙임
행위를 확장할 수 있지만, 개체를 변경해서는 안 된다.
9장. LSP: 리스코프 치환 원칙
S 타입의 객체 o1 각각에 대응하는 T 타입 객체 o2가 있고, T 타입을 이용해서 정의한 모든 프로그램 P에서 o2의 자리에 o1을 치환하더라도 P의 행위가 변하지 않는다면 S는 T의 하위 타입이다.
상속 구조에서, 부모 클래스의 인스턴스를 자식 클래스의 인스턴스로 치환할 수 있어야 한다.
자식 클래스는 부모 클래스의 책임을 준수하며, 부모 클래스의 행동을 변경하지 않아야 한다.
LSP를 위반하면, 상속 클래스를 사용할 때 오동작, 예상 밖의 예외가 발생하거나, 이를 방지하기 위한 불필요한 타입 체크가 동반될 수 있다.
LSP 위배 사례
A 택시 회사의 택시 파견 URI는 aaa.com/driver/Bob/pickupAddress/24/pickupTime/153/destination/ORD
새로운 팀이 B 택시 회사 파견 URI를 일부 다르게 설계함 ex) destination -> dest
REST 서비스들이 서로 호환되지 않음
아키텍처에서는 if로 분기 처리가 아니라, 치환되지 않는 REST 서비스들의 인터페이스를 처리하는 매커니즘이 필요함
결론
LSP는 아키텍처 수준까지 확자알 수 있고, 반드시 확장해야만 한다.
치환 가능성을 조금이라도 위배하면 시스템 아키텍처가 오염되어 상당량의 별도 메커니즘을 추가해야 할 수 있기 때문임
10장. ISP: 인터페이스 분리 원칙
클라이언트는 자신이 사용하지 않는 인터페이스에 의존하면 안 된다.
인터페이스를 잘게 쪼개라.
ISP를 위반하면, 불필요한 의존성으로 인해 결합도가 높아지고, 특정 기능의 변경이 여러 클래스에 영향을 미칠 수 있다.
11장. DIP: 의존성 역전 원칙
의존성이 극대화된 시스템이란 소스 코드 의존성이 추상에 의존하며 구체에는 의존하지 않는 시스템임
상위 수준의 모듈은 하위 수준의 모듈에 의존해서는 안 된다. 둘 모두 추상화에 의존해야 한다.
고수준: 추상화 레벨이 높은 쪽
저수준: 추상화 레벨이 낮은 쪽 (구현체, 구체쪽에 가깝다.)
추상화 레벨이 높은 쪽 모듈이 구체쪽에 가까운 모듈에 직접적으로 의존해서는 안 된다.
둘 모두 추상화에 의존해야 한다.
의존성의 순방향: 고수준 모듈이 저수준 모듈을 참조하는 것 의존성의 역방향: 고수준, 저수준 모듈이 모두 추상화에 의존하는 것
저수준 모듈이 번경되어도, 고수준 모듈에는 영향이 가지 않는다.
안정된 추상화
인터페이스에 변경이 생기면 구현체들도 수정해야 하지만, 구현체들에 변경이 생기더라도 인터페이스는 대부분 변경될 필요가 없음
인터페이스는 구현체보다 변동성이 낮으며, 뛰어난 소프트웨어 설계자와 아키텍트라면 인터페이스의 변동성을 낮추려고 노력함
인터페이스를 변경하지 않고도 구현체에 기능을 추가할 수 있는 방법을 찾기 위해 노력하며, 이는 소프트웨어 설계의 기본임
즉, 안정된 아키텍처란 변동성이 큰 구현체에 의존하는 일은 지양하고, 안정된 추상 인터페이스를 선호하는 아키텍처라는 뜻임
변동성이 큰 구체 클래스를 참조하지 말라
대신 추상 인터페이스를 참조하라
이 규칙은 객체 생성 방식을 강하게 제약하며, 일반적으로 추상 팩토리를 사용하도록 강제함
변동성이 큰 구체 클래스로부터 파생하지 말라
따라서 상속은 아주 신중히 사용해야 한다.
구체 함수를 오버라이드하지 말라.
대체로 구체 함수는 소스 코드 의존성을 필요로 한다.
따라서 구체 함수를 오버라이드 하면 의존성을 제거할 수 없게 되며, 실제로는 그 의존성을 상속하게 된다.
이러한 의존성을 제거하려면, 차라리 추상 함수로 선언하고 구현체들에서 각자의 용도에 맞게 구현해야 한다.
구체적이며 변동성이 크다면 절대로 그 이름을 언급하지 말라.
Last updated