다수의 사용자가 OPS 클래스의 오퍼레이션을 사용한다.
User1 2 3은 오직 op1 2 3만을 사용한다고 가정하자.

그리고 OPS가 정적타입 언어로 작성된 클래스라고 해보자
이 경우 User1에서는 op2, op3을 전혀 사용하지 않음에도 User1의 소스코드는 두 메서드에 의존하게 된다.

이러한 의존성으로 OPS 클래스에서 op2의 소스코드가 변경되면 User1도 다시 컴파일 후 새로 배포해야 한다.
-> User1과 관련된 코드는 전혀 변경되지 않았더라도!

오퍼레이션을 인터페이스 단위로 분리하여 해결

ISP와 언어

앞의 예제는 언어 타입에 의존한다.
정적 타입 언어는 사용자가 import use include와 같은 타입 선언문을 사용한다.
이처럼 소스 코드에 포함된 선언문으로 인해 소스코드 의존성이 발생하고, 재컴파일해야 하는 상황이 무조건 온다.

파이썬/루비 같은 동적 타입 언어에서는 소스 코드에 이러한 선언문이 존재하지 않는다.
대신 런타임에 추론이 발생한다. 따라서 소스코드 의존성이 없고 재컴파일해야 하는 상황도 없다.

ISP와 아키텍처

ISP를 사용하는 근본적인 동기를 살펴보면, 다른 우려사항을 볼 수 있다.
일반적으로, 필요 이상으로 많은 걸 포함하는 모듈에 의존하는 것은 해롭다.
소스 코드 의존성의 경우 분명한데, 불필요한 재컴파일을 강제하기 때문이다.

저수준의 파일, 모듈 뿐만 아니라 고수준의 아키텍처 수준에서도 마찬가지다

System -> Framework -> Database

의 의존성을 가진다면, S는 D에 의존하게 된다.

D의 내부에서 사용하지 않는 기능이 변경되더라도, F를 재배포해야 할 수 있다.
F를 재배포하게 되면 S 역시 재배포 가능성이 존재한다.

하위 타입

여기에서 필요한 것은 다음과 같은 치환 원칙이다.
S 타입의 객체 o1 각각에 대응하는 T 타입 객체 o2가 있고,
T 타입을 이용해서 정의한 모든 프로그램 P에서 o2의 자리에 o1을 치환하더라도 P의 행위가 변하지 않는다면,
-> S는 T의 하위 타입이다.

상속을 사용하도록 가이드

License라는 클래스가 있다고 해보자
calcFee() 메서드를 가지며 Billing Application이 메서드를 호출한다.
License는 PersonalLicense와 BusinessLicense라는 두가지 '하위 타입' 을 갖는다.
두 하위 타입은 서로 다른 알고리즘을 이용해서 메서드를 오버라이드 한다.

이 설계는 LSP를 준수한다.
-> Billing App의 행위가 License 하위타입중 무엇을 사용하는 지에 전혀 의존하지 않기 때문이다.

정사각형/직사각형 문제

LSP를 위반하는 전형적인 문제

Square는 Rectangle의 하위 타입으로는 적합하지 않은데,
Rectangle의 높이와 너비는 서로 독립적으로 변경될 수 있는 반면,
Square의 높이와 너비는 반드시 함께 변경되기 때문이다.
User는 대화하고 있는 상대가 Rectangle이라고 생각하므로 혼동이 생길 수 있다.

Rectangle r = ...
r.setW(5);
r.setH(2);
assert(r.area() == 10);

...코드에서 Square를 생성한다면 assert문은 실패하게 된다.
-> W든 H든 변경될 때, 정사각형은 W와 H를 모두 바꾸기 때문
이런 형태의 LSP 위반을 막기 위한 유일한 방법은
if문과 같은 조건을 이용해서 Rectangle이 실제로는 Square인지를 검사하는 메커니즘을 User에 추가하는 것이다.
-> 하지만 이렇게 하면 Userdㅢ 행위가 사용하는 타입에 의존하게 되므로 타입을 서로 치환할 수 없게 된다.

LSP와 아키텍처

객체 지향이 등장한 초창기에는 LSP는 상속을 사용하도록 가이드되었다.
하지만 시간이 지나면서 LSP는 인터페이스와 구현체에도 적용되는 더 광범위하게 변했다.

여기서 말하는 인터페이스는 다양한 형태로 나타난다.
자바스러운 언어라면 인터페이스 하나와 이를 구현하는 여러개의 클래스로 구성
잘 정의된 인터페이스와 그 구현체끼리의 상호 치환 가능성에 기대는 사용자들이 존재하기 때문이다.

LSP 위배 사례

  1. 다양한 택시 파견 서비스를 통합하는 App을 만들고 있다고 가정하자.

  2. 고객은 어느 택시업체인지는 모르지만 자신의 상황에서의 적절한 택시를 찾는다.

  3. 고객이 이용할 택시를 결정하면, 시스템은 REST 서비스를 통해 선택된 택시를 고객 위치로 파견한다.

  4. 택시 파견 REST 서비스의 URI가 운전기사 DB에 저장되어 있다고 가정해보자

  5. 시스템이 고객에게 알맞은 기사흫 선택하면, 해당 기사의 레코드로부터 URI를 얻어 해당 기사를 고객 위치로 파견한다.

EX
택시기사 Bob의 파견 URI = purplecab.com/driver/Bob
시스템은 기사 URI에 정보를 붙힌다.
purplecab.com/driver/Bob/pickupAddress/24 Maple St./pickupTime/153/destination/ORD

이 예제에서 분명한 것은 파견 서비스를 만들 때, 다양한 택시업체에서 동일한 REST 인터페이스를 반드시 준수하도록 만들어야 한다는 사실이다.
서로 다른 택시업체가 pickupAddress/pickupTime/destination 을 동일하게 처리해야 한다.

이제 택시업체(kakaoTaxi)에서 프로그래머를 몇 명 고용했는데, 서비스 사양서를 신중히 읽지 않았다고 가정하자.
destination 필드를 dest로 축약했다.
그렇다면 파견 서비스에서는 kakaoTaxi만을 위해 모든 모듈에

if(driver.getDispatchUri().startsWith("kakaoTaxi")).....

를 추가해야 할 것이다.

결론

LSP는 아키텍처 수준까지 확장할 수 있고, 반드시 확장해야만 한다.
치환 가능성을 조금이라도 위배하면 시스템 아키텍처가 오염되어 별도의 코드/메커니즘이 필요할 수도 있다.

설계 원칙

좋은 SW 시스템은 깔끔한 코드로부터 시작한다.
좋은 벽돌을 사용하지 않으면 아키텍처가 좋고 나쁨은 중요치 않다.
반대로 좋은 벽돌을 사용하더라도 엉망으로 만들 수 있다.

-> 따라서 좋은 벽돌로 좋은 아키텍처를 정의하는 원칙이 필요한데...
그것이 SOLID이다.

SOLID 원칙은 함수와 데이터 구조를 클래스로 배치하는 방법 그리고 이들 클래스를 서로 결합하는 방법을 설명해준다.

SOLID의 목적

  • 변경에 유연하다
  • 이해하기 쉽다
  • 많은 SW 시스템에 사용할 수 있는 컴포넌트의 기반이 된다.

'중간 수준'이라 함은 프로그래머가 이들 원칙을 모듈 수준에서 작업할 때 적용할 수 있다는 뜻이다.

SRP

단일 책임 원칙
이름만 들으면 모든 모듈이 단 하나의 일만 해야 한다는 의미로 받아들이기 쉽다.
-> 함수는 하나의 일만 해야 한다 가 정확하다.

모듈 = "소스 파일"

원칙을 위반하는 징후

우발적 중복

급여 App의 Employee Class
해당 클래스는 calculatePay(), reportHours(), save() 메서드를 갖는다.

  • calculatePay() 는 회계팀에서 기능을 정의하며, DFO 보고를 위해 사용
  • reportHours() 는 인사팀에서 기능을 정의하고 사용하며, COO 보고를 위해 사용
  • save() 는 DBA가 기능을 정의하고, CTO 보고를 위해 사용

각각의 메서드를 갖고 있는 Employee라는 단일 클래스에 3종류의 actor가 결합되었다.

예를 들어, calculatePay()와 ㄱeportHours()가 초과근무를 제외한 업무 시간을 계산하는 알고리즘을 공유한다고 해보자
그리고 중복을 회피하기 위해 regularHours()라는 메서드를 넣었다고 가정하자

이제 CFO 팀에서 초과 근무를 제외한 업무 시간을 계산하는 방식을 수정하기로 했다.
반면 인사를 담당하는 COO 팀에서는 초과 근무를 제외한 업무 시간을 CFO 팀과는 다른 목적으로 사용하기에, 변경을 원치 않는다.

  1. 해당 업무를 받은 개발자는 calculatePay() 메서드가 regularHours()를 호출한다는 것을 발견!
  2. 하지만 reportHours() 메서드에서도 호출된다는 것을 발견하지 못함..

개발자는 업무를 테스트하고 변경했다.
CFO 팀에서는 기능을 검증하고 시스템은 배포된다.
하지만 COO 팀에서는 이러한 사실을 알지 못했고, reportHours() 메서드를 사용하고 나서야 잘못된 수치들을 확인한다.

-> 누가, 어떤 목적으로 사용하는지에 대한 기능을 제공을 단일로 책임? , 즉 Class가 단일 actor에 대한 책임을 진다?

병합

소스 파일에 다양하고 많은 메서드를 포함하면 병합이 자주 발생할 것이다.
이들 메서드가 서로 다른 actor를 책임진다면 병합이 발생할 가능성은 더 높다

DBA가 속한 CTO팀에서 DB의 Employee Table Schema를 수정하기로 했다.
동시에 COO 팀에서는 reportHours() 메서드의 보고서 포맷을 변경하기로 했다.

서로 다른 개발자가 Employee Class를 체크아웃 받을 후 적용할 것이다.
이러한 변경사항들은 분명히 충돌한다.

-> 이 문제를 벗어나기 위해서는 서로 다른 actor를 책임지는 코드를 서로 분리하는 것이다.

해결책

가장 확실한 해결책은 데이터와 메서드를 분리하는 방식이다.
즉, 아무런 메서드가 없는 간단한 데이터 구조인 EmployeeData Class를 만들어 세개의 Class가 공유한다.
각 클래스는 자신의 메서드에 반드시 필요한 소스 코드만을 포함한다.
각 클래스는 서로의 존재를 모른다.

하지만 개발자가 3가지 Class를 인스턴스화하고 추적해야 한다는 게 단점이다.
이러한 난관을 피하기 위해 퍼싸드 패턴을 사용한다.

EmployeeFacade에 코드는 거의 없다. 이 클래스는 3개의 클래스의 객체를 생성하고, 요청된 메서드를 가지는 객체로 위임하는 일을 책임진다.

결론

SRP는 메서드와 클래스 수준의 원칙이다. 하지만 이보다 상위 두 수준에서도 다른 형태로 다시 등장한다.
컴포넌트 수준에서는 공통 폐쇄 원칙
아키텍처 수준에서는 아키텍처 경계의 생성을 책임지는 변경의 축이 된다.

정수를 제곱하기

FP가 무엇인지 설명하기 위한 예제

//자바 언어
public class Squint{
    public static void main(String args[]){
        for(int i=0;i<25;i++)
            System.out.println(i*i);
    }
}

//LISP에서 파생한 클로저는 함수형 언어로, 클로저를 이용하면 같은 프로그램을 아래처럼 구현 가능
(println (take 25 (map (fn [x] (* x x)) (range))))

(println ; -> 출력한다
    (take 25 ; -> 처음부터 25까지
        (map (fn [x] (* x x)) ; -> 제곱을
            (range)))) ; -> 정수의

println, take, map, range 모두 함수다.

(fn [x] (* x x))는 익명 함수로, 곱셈 함수를 호출하면서 입력 인자를 두번 전달한다.

클로저 vs 자바

자바

  • 가변 변수를 사용
    • 가변 변수는 프로그램 실행 중에 상태가 변할 수 있다
      • 앞의 예제의 i는 가변 변수이다.
        클로저
  • 가변 변수가 없다
    • x와 같은 변수가 한번 초기화 되면 절대로 변하지 않는다.

불변성과 아키텍처

아키텍처를 고려할 때 이러한 내용이 왜 중요한가?

  • race Condition, DeadLock 조건, 동시성 문제가 모두 가변 변수로 인해 발생하기 때문이다.

만약 어떠한 변수도 갱신되지 않는다면 위의 문제들은 발생하지 않는다.
결국 우리가 Appdㅔ서 마주치는 모든 문제, 즉 다중 스레드와 프로세스를 사용하는 App에서의 모든 문제는 가변 변수가 없다면 발생하지 않는다.

가변성의 분리

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

이벤트 소싱

저장 공간과 처리 능력의 한계는 계속해서 사라지고 있다.
프로세서는 초당 수십억 개의 명령을 수행하고
RAM 용량은 엄청 커졌다.

입금과 출금 트랜잭션이 있다고 해보자
편히 알고 있는 방법은 계좌 잔고를 변경했지만,
발생하는 트랜잭션을 저장한다고 가정해보자 -> 가변 변수가 필요가 없다.
무한한 공간과 처리능력이 필요하여 비현실적이다.
-> 하지만 App의 수명주기 동안만 문제없이 동작할 정도의 리소스만 있으면 충분할 것이다.

이벤트 소싱
기본 발상이 바로 이것이다.
상태가 아닌 트랙잭션을 저장하자는 전략이다.
상태가 필요해지면 단순히 상태의 시작점부터 모든 트랙잭션을 처리한다.
-> 물론 중간 중간(매일 자정)마다 계산한 후에 그 뒤에 들어오는 트랜잭션만 처리해도 된다.

결론

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

+ Recent posts