본문 바로가기
독서/클린 아키텍처

클린 아키텍처 3부. 설계 원칙

by yoon_seon 2024. 7. 19.

7장. SRP: 단일 책임 원칙


단일 모듈의 변경의 이유가 하나, 오직 하나뿐이어야 한다.

  • SOLID 원칙 중 의미가 가장 잘 전달되지 못한 원칙은 단일 책임 원칙으로, 모듈이 단 하나의 일만 해야 한다는 의미가 아니다.
  • SRP는 함수는 반드시 단 하나의 일만 해야 한다는 원칙이다.
  • 여기서 변경의 이유는 변경을 요청하는 한 명 이상의 사람들(액터)에 해당한다.
  • 즉, 하나의 모듈(소스 코드)은 하나의, 오직 하나의 액터에 대해서만 책임져야 한다.
  • 모듈은 단순히 함수와 데이터 구조로 구성된 응집된 집합이다. ‘응집된(cohesive)’ 이라는 단어가 SPR를 암시한다.
  • 단일 액터를 책임지는 코드를 함께 묶어주는 힘이 바로 응집성(cohesion)이다.

 

징후 1: 우발적 중복

  • 코드의 중복을 피하기 위해 각기 다른 액터들이 다른 목적을 가지고 사용하는 메서드들을 단일 클래스에 배치한다면, 액터가 서로 결합되어 버리게 되고 변경 시에 각 액터들은 변경사항을 모르게되는 문제가 발생한다.
  • 이러한 문제는 서로 다른 액터가 의존하는 코드를 너무 가까이 배치했기 때문에 발생한다.
  • SRP는 서로 다른 액터가 의존하는 코드를 서로 분리해야한다고 말한다.

 

징후 2: 병합

  • 소스 코드에 다양하고 많은 메서드를 포함하면 병합이 자주 발생하는데, 서로 다른 액터를 책임진다면 병합이 발생할 가능성은 확실히 더 높다.
  • 이 문제를 벗어나는 방법은 서로 다른 액터를 뒷받침하는 코드를 서로 분리하는 것이다.

 

해결책

  • 해결책은 다양한데, 결국 그 모두가 메서드를 각기 다른 클래스로 이동시키는 방식이다.
  • 새로운 클래스를 만들고 각 액터의 클래스가 공유하도록 한다. 각 클래스는 자신의 메서드에 반드시 필요한 소스 코드만을 포함한다.
  • 각 액터의 클래스는 서로의 존재를 모르게되고, 따라서 우연한 중복을 피할 수 있다.

 

결론

  • 단일 책임 원칙은 메서드와 클래스 수준의 원칙이다.
  • 상위 인 컴포넌트 수준에서는 공통 폐쇠 원칙(CCP)가 된다.
  • 더 상위 인 아키텍처 수준에서는 아키텍처 경계의 생성을 책임지는 변경의 축이된다.

 

 

8장. OCP: 개방-폐쇄 원칙


소프트웨어 개체는 확장에 열려 있어야 하고, 변경에는 닫혀 있어야 한다.

  • 즉, 소프트웨어 개체의 행위는 확장할 수 있어야 하지만, 이 때 개체를 변경해서는 안된다.
  • 만약 요구사항을 살짝 확장해야하는데 소프트웨어를 엄청나게 수정해야 한다면, 아키텍트는 엄청난 실패에 맞닥 뜨린 것이다.
  • OCP를 클래스와 모듈을 설계할 때 도움되는 원칙이라고 알고 있으나, 아키텍처 컴포넌트 수준에서 OCP는 훨씬 중요한 의미를 가진다.

 

사고 실험

  • 소프트웨어 아키텍처가 훌륭하다면 새로운 코드를 작성할 때 변경되는 코드의 양이 가능한 한 최소화 될 것이고, 이상적인 변경량은 0이다.
  • 이를 위해 서로 다른 목적으로 변경되는 요소를 적절하게 분리하고(SRP), 의존성을 체계화함으로써(DIP) 변경량을 최소화 할 수 있다.

 

결론

  • OCP의 목표는 시스템을 확장하기 쉬운 동시에 변경으로 인해 시스템이 너무 많은 영향을 받지 않도록 하는 데 있다.
  • 시스템을 컴포넌트 단위로 분리하고, 저수준 컴포넌트에서 발생한 변경으로부터 고수준 컴포넌트를 보호할 수 있는 형태의 의존성 계층구조가 만들어지도록 해야한다.

 

 

9장. LSP: 리스코프 치환 원칙


S 타입의 객체 o1 각각에 대응하는 T 타입 객체 o2가 있고, T 타입을 이용해서 정의한 모든 프로그램 P에서 o2의 자리에 o1을 치환하더라도 P의 행위가 변하지 않는다면, S는 T의 하위 타입이다

 

LSP와 아키텍처

  • 객체 지향이 등장한 초창기에 LSP는 상속을 사용하도록 가이드하는 방법 정도로 간주되었다.
  • 하지만 시간이 지나면서 인터페이스와 구현체에도 적용되는 더 광범위한 소프트웨어 설계 원칙으로 변모해왔다.
  • 아키텍처 관점에서 LSP를 이해하는 최선의 방법은 이 원칙을 어겼을 때 시스템 아키첵처에서 무슨 일이 일어나는지 관찰하는 것이다.

 

LSP 위배 사례

  • 아키텍트는 버그들로 부터 시스템을 격리해야 한다.
  • ex) 택시 파견 서비스를 통합하는 애플리케이션을 만드는 상황
    • A 택시 회사의 택시 파견 URI는 aaa.com/driver/Bob/pickupAddress/24/pickupTime/153/destination/ORD
    • 새로운 팀이 B 택시 회사 파견 URI를 일부 다르게 설계함 ex) destination → dest
    • REST 서비스들이 서로 호환되지 않음
    • 아키텍처에서는 if로 분기 처리가 아니라, 치환되지 않는 REST 서비스들의 인터페이스를 처리하는 매커니즘이 필요함

 

결론

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

 

 

10장. ISP: 인터페이스 분리 원칙


ISP와 언어

  • 정적 타입 언어
    • 사용자가 import, use, include와 같은 타입 선언문을 사용하도록 강제하는데, 소스코드에 포함된 선언문으로 인해 소스 코드 의존성이 발생한다.
    • 이로인해 재컴파일 또는 재배포가 강제되는 상황이 무조건 초래된다.
  • 동적 타입 언어
    • 동적 타입 언어에서는 소스 코드에 선언문이 존재하지 않고 런타임에 추론이 발생한다. 결국 재컴파일과 재배포가 필요없다.
    • 동적 타입이 정적 타입보다 유연하며 결합도가 낮은 이유가 바로 이 때문이다.

 

ISP와 아키텍처

  • 소스 코드 의존성을 강제하면 불필요한 재컴파일과 재배포를 강제한다.
  • 더 고수준인 아키텍처 수준에서도 마찬가지 상황이 발생한다.

 

  • S는 F에 의존하며 F는 S는 D에 의존한다.
  • D가 변경되면 F를 재배포 해야 할 수 있고, S까지 재 배포해야 할지도 모른다.
  • 더 심각한 문제는 D 내부의 기능중 F와 S에서 불필요한 기능에 문제가 발생해도 F와 S에 영향을 준다는 사실이다.

 

결론

  • 여기서 배울 수 있는 교훈은 불필요한 짐을 실은 무언가에 의존하면 예상치도 못한 문제에 빠진다는 사실이다.

 

 

11장. DIP: 의존성 역전 원칙


소스코드의 의존성이 추상(abstraction)에 의존하며 구체(concretion)에는 의존하지 않는다.

  • String 클래스
    • String 클래스는 변경될 일이 거의 없고, 변경되더라도 엄격하게 통제되기에 매우 안정적이다.
    • 프로그래머와 아키텍트는 String 클래스에서 변덕스러운 변경이 자주 변경하리라고 염려할 필요가 없다.
  • 이러한 이유로 DIP를 논할 때 운영체제나 플랫폼 같이 안정성이 보장된 환경에 대해서는 무시한다.
  • 우리가 의존하지 않도록 피하고자 하는 것은 변동성이 큰 구체적인 요소(개발 중이라 자주 변경될 수 밖에 없는 모듈들)이다.

 

안정된 추상화

  • 인터페이스에 변경이 생기면 구현체들도 함께 수정해야하지만, 구현체에 변경이 생기면 인터페이스는 대부분 변경될 필요가 없다.
  • 따라서 인터페이스는 구현체보다 변경성이 낮으며 뛰어난 소프트웨어 설계자와 아키텍트라면 변동성을 낮추기 노력한다.
  • 즉, 안정된 소프트웨어 아키텍처란 변동성이 큰 구현체에 의존하는 일을 지양하고, 안정화된 추상 인터페이스를 선호하는 아키텍처라는 뜻이다.
  • DIP에서 전달하려는 원칙은 다음과 같이 구체적인 코딩 실천법으로 요약할 수 있다.
    • 변동성이 큰 구체 클래스를 참조하지말라.
      • 대신 추상 인터페이스를 참조하라.
      • 이 규칙은 언어 타입과 관계없이 모두 적용된다.
      • 이 규칙은 객체 생성 방식을 강하게 제약하며, 일반적으로 추상 팩토리를 사용하도록 강제한다.
    • 변동성이 큰 구체 클래스로부터 파생하지 말라
      • 정적 타입 언어에서 상속은 소스 코드에 존재하는 모든 관계 중에서 가장 강력한 동시에 뻣뻣해서 변경하기 어렵다
      • 따라서 상속은 신중하게 사용해야 한다.
      • 동적 타입 언어는 문제가 덜 되지만, 의존성을 가진다는 사실에는 변함이 없다.
    • 구체 함수를 오버라이드 하지 말라.
      • 대체로 구체 함수는 소스 코드 의존성을 필요로 한다.
      • 따라서 구체 함수를 오버라이드 하면 이러한 의존성을 제거할 수 없게 되며, 실제로는 그 의존성을 상속하게 된다.
      • 이러한 의존성을 제거하려면, 차라리 추상 함수로 선언하고 구현체들에서 각자의 용도에 맞게 구현해야 한다.
    • 구체적이며 변동이 크다면 절대로 그 이름을 언급하지 말라
      • 사실 이 실천법은 DIP 원칙을 다른 방식으로 풀어쓴 것이다.

 

팩토리

  • 구체적인 코딩 실천법 규칙을 준수하려면 변동성이 큰 구체적인 객체는 특별히 주의해서 생성해야 한다.
  • 객체를 생성하려면 해당 객체를 구체적으로 정의한 코드에 대해 소스 코드 의존성이 발생하기 때문이다.
  • 자바 등 대다수의 객체 지향 언어에서 이처럼 바람직하지 못한 의존성을 처리할 때 추상 팩토리를 사용하곤 한다.

 

  • 위 그림에 대한 설명
    • Application은 Service 인터페이스를 통해 ConcreteImpl을 사용한다.
    • Application은 어떤 식으로든 ConcreteImpl의 인스턴스를 생성해야 한다.
    • ConcreteImpl에 대해서 소스 코드 의존성을 만들지 않으면서 이 목적을 이루기 위해 Application은 ServiceFactory 인터페이스의 makeSve 메서드를 호출한다.
    • makeSve 메서드는 ServiceFactoryImpl에서 구현되고, ConcreteImpl 인스턴스를 생성한 후 Service 타입으로 반환한다.
  • 곡선은 아키텍처 경계를 뜻하는데, 구체적인 것들로부터 추상적인 것들을 분리한다.
    • 소스코드 의존성은 해당 곡선과 교차할 때 모두 한방향, 즉 추상적인 쪽으로 향한다.
    • 추상 컴포넌트는 애플리케이션의 모든 고수준 업무 규칙을 포함한다.
    • 구체 컴포넌트는 업무 규칙을 다루기에 필요한 모든 세부사항을 포함한다.
  • 제어 흐름이 소스 코드 의존성과는 정반대이다.
    • 소스 코드 의존성은 제어 프름과는 반대 방향으로 역전된다.
    • 이러한 이유로 이 원칙을 의존성 역전(Dependency Inversion)이라고 부른다.

 

구체 컴포넌트

  • 구체 컴포넌트에는 구체적인 의존성이 하나 있고, DIP에 위배 된다.
  • 이는 일반적인 일로 DIP 위배를 모두 없앨 수는 없다.
  • 하지만 DIP를 위배하는 클래스들은 적은 수의 구체 컴포넌트 내부로 모아서 시스템의 나머지 부분과 분리할 수 있다.

 

결론

  • DIP는 아키텍처 다이어그램에서 가장 눈에 드러나는 원칙이 된다.
  • 위 그림에서 곡선은 아키텍처 경계가 된다.
  • 의존성은 이 곡선을 경계로 더 추상적인 쪽으로만 향하는데, 이 규칙은 ’의존성 규칙’이라 부를 것이다.

 


모든 내용은 [클린 아키텍처] 서적의 정리한 내용이며, 망나니 개발자님의 블로그에서 정리 방법을 참고했습니다.

 

클린 아키텍처: 소프트웨어 구조와 설계의 원칙 | 로버트 C. 마틴 - 교보문고

클린 아키텍처: 소프트웨어 구조와 설계의 원칙 | 살아있는 전설이 들려주는 실용적인 소프트웨어 아키텍처 원칙 소프트웨어 아키텍처의 보편 원칙을 적용하면 소프트웨어 수명 전반에서 개발

product.kyobobook.co.kr

 

[개발서적] 클린 아키텍처 3부 설계 원칙 - 내용 정리 및 요약

이번에는 로버트 C 마틴의 클린 아키텍처를 읽은 내용을 정리해보도록 하겠습니다. 개인적인 설명은 기울임으로 표시해두었으니, 읽으면서 참고하시면 될 것 같습니다. 0. 서론 [ 도입 ] SOLID는

mangkyu.tistory.com

댓글