본문 바로가기

리뷰/책 리뷰

[IT/리뷰] 클린 아키텍처

728x90

클린 아키텍처: 소프트웨어 구조와 설계의 원칙 : 네이버 도서 (naver.com)

 

클린 아키텍처: 소프트웨어 구조와 설계의 원칙 : 네이버 도서

네이버 도서 상세정보를 제공합니다.

search.shopping.naver.com

내가 생각했던 것보다 더 큰 차원에서 얘기를 해줬다. 나는 하나의 시스템 안에서 모듈 간의 구조 혹은 하나의 모듈 안에서의 구조 정도로 생각했다. 그런데 여기서 말하는 아키텍처는 시스템 간의 아키텍처를 포함하는 큰 구조를 말했다.

범위만 다를 뿐, 소스 코드 단위에서의 구조와 시스템 단위에서의 구조에서 중요한 점은 동일하다고 생각하기 때문에 적절히 받아들였다. 그리고 내가 여태 잘못 생각했던 부분들에 대해서도 짚어 주었다.

 

예전에 아키텍트를 꿈꾸기도 했다. 지금은 내 실력이 아직 많이 부족하다는 것을 알고 현재 회사에서 맡은 일들을 하고 있다. 개발과 설계가 다르긴 하겠지만, 더 세세한 설계도 못하다면 전체적인 시스템 설계를 어떻게 할 수 있겠는가 싶다. 점차 회사 제품들에 대한 이해와 시야가 넓어지고 있고 시스템적인 아키텍처도 보이고 있다. 이렇게 점점 배워나가는 게 좋다고 생각하지만 넓은 관점을 가질 수 있도록 훨씬 노력해야 할 것 같다.

 

이 책은 그 준비를 할 수 있게끔 주의해야 할 점에 대해 말해준 것 같다. 중요한 점이 무엇인지, 중요하지 않은 점은 무엇인지, 시스템을 어떻게 개발하고 유지해야 하는지에 대해 배울 수 있었다.

 

이 출판사의 Program Programming Programmer 시리즈에서 <클린 코드>와 <테스트 주도 개발>(테스트 주도 개발 후기도 곧 올려야겠다.)에 이어 세 번째 책을 읽었다. 당분간 이 시리즈의 책들을 몇 권 더 읽어볼 생각이다. 책을 빌리기가 쉽지 않은데 회사에 요청 올려볼지.. 생각해 봐야겠다.

 

아래는 책의 정리 내용들이다. 생각보다 더 많이 적었는데, 핵심 내용은 반복되는 것 같아 적은 내용을 다시 줄였다. 그래도 내용이 많다. 정리하려고 적어놓은 것들이 너무 많은데, 그 내용들이 다 중요해보인다.

정리 내용 들어가기 전에 중요한 부분을 요약 정리하자면 아래와 같다.

  • 아키텍처는 운영과는 전혀 상관없다. 구조가 엉망이어도 동작하게 할 수 있다.
  • 컴포넌트 간 의존성 관계를 관리하라
  • 추상적인 것에 의존하라
  • 세부사항은 최대한 미루고, 핵심 업무를 위주로 구성하라
    • 세부사항은 생각보다 포괄적이다 (웹, DB, 프레임워크..)
  • 아키텍트와 애자일 프로세스 간의 충돌(YAGNI)이 있을 때, 부분적 경계를 활용하라
  • 테스트 용이성은 좋은 아키텍처의 필요충분조건이다.

엉클 밥(로버트 C. 마틴)의 자서전과 같은 책이다. 저자는 하드웨어는 더 작아지고 빨라지지만, 소프트웨어 구성은 조금도 바뀌지 않았다고 한다. 그렇기에 더 좋은 소프트웨어 아키텍처를 만드는 원칙은 시간이 지나더라도 보편적이고 변함이 없다.

 

설계와 아키텍처에 차이는 없다. 보통 아키텍처는 저수준 세부사항과 분리된 고수준의 무언가를 가리킬 때 사용하고, 설계는 저수준의 구조 또는 결정사항을 의미하는 경우가 많다. 하지만, 아키텍트가 실제로 하는 일에서는 이러한 구분이 무의미하다. 저수준의 세부사항과 고수준의 결정사항 모두 전체 설계의 구성요소일 뿐이다. 둘 사이에는 고수준에서 저수준으로 향하는 의사결정의 연속성만 있을 뿐 경계가 또렷하지는 않다.

 

소프트웨어 아키텍처의 목표는 필요한 시스템을 만들고 유지보수하는 데 투입되는 인력을 최소화하는 데 있다.

 

모든 소프트웨어 시스템은 이해관계자에게 서로 다른 두 가지를 제공한다.

  • 행위
  • 구조

소프트웨어 개발자들은 "행위"에만 집중해 소프트웨어 시스템이 쓸모없어지게 만들기도 한다.

"소프트웨어"는 이름에서 알 수 있듯이 부드러워야 한다. 변경이 쉬워야 함을 의미한다. 변경사항을 적용하는 데 드는 어려움은 변경되는 범위에 비례해야지, 변경사항의 형태와는 관련 없어야 한다. 새로운 요청사항은 변경사항을 적용하는 것보다 더 어렵다. 시스템의 형태와 요구사항의 형태가 다를 수 있기 때문이다.

 

아키텍처는 형태 독립적이어야 하고, 그럴수록 더 실용적이다.

 

'아이젠하워 매트릭스'에 비춰봤을 때, 행위는 긴급하지만 매번 높은 중요도를 지닌 것은 아니다. 아키텍처는 중요하지만 즉각적인 긴급성을 필요로 하는 경우는 절대 없다. 기능의 긴급성이 아닌 아키텍처의 중요성을 설득하는 일은 소프트웨어 개발팀이 마땅히 책임져야 한다.

긴급하면서 중요한 기능과 긴급하지만 중요하지 않은 기능은 구분해야 한다.

 

좋은 소프트웨어 시스템은 깔끔한 코드(클린 코드)로부터 시작한다. 좋은 벽돌을 사용하지 않으면 빌딩의 아키텍처가 좋고 나쁨은 큰 의미가 없다. 반대로 좋은 벽돌을 사용해도 아키텍처를 엉망으로 만들 수 있다. 이때 중요한 것이 SOLID 원칙이다.

SOLID 원칙은 함스와 데이터 구조를 클래스로 배치하는 방법, 그리고 이들 클래스를 서로 결합하는 방법을 설명한다. 여기서의 클래스는 단순히 함수와 데이터를 결합한 집합을 의미하며, OO(객체 지향)에만 적용되는 것은 아니다.

SOLID의 목적은 중간 수준(프로그래머가 원칙을 모듈 수준에서 작업 시 적용할 수 있다는 것을 의미)의 소프트웨어 구조가 아래와 같도록 만드는 데 있다.

  • 변경에 유연하게 만들기
  • 이해하기 쉽게 만들기
  • 많은 소프트웨어 시스템에 사용될 수 있는 컴포넌트의 기반으로 만들기

 

SOLID의 SRP(단일 책임 원칙)는 가장 의미가 잘못 전달된 원칙이라 볼 수 있다. SRP는 모듈이 단 하나의 일만 한다는 것이 아니다.

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

시스템은 사용자와 이해관계자를 만족시키기 위한 '변경의 이유'를 갖는다. 따라서, 다음과 같이 정의를 새로 할 수 있다.

"하나의 모듈은 하나의, 오직 하나의 액터에 대해서만 책임져야 한다."

'엑터'는 사용자 또는 이해관계자를 말한다. '모듈'은 단순 정의로는 소스 파일을 말하며, 단순히 함수와 데이터 구조로 구성된 응집된 집합을 의미한다. 여기서 '응집된'이란 단어가 SRP를 암시한다. 단일 액터에 책임지는 코드를 묶어주는 힘이 응집성(cohension)이다.

SRP를 이해하기 좋은 방법은 원칙 위반 징후를 살펴보는 것이다.

징후 1:
우발적 중복급여 애플리케이션에 Empolyee 클래스가 있다고 해보자. calculatePay() / reportHours(), save() 세 메서드가 있을 때, 이 Empolyee 틀래스는 세 명의 다른 액터를 책임지고 있다.
- calculatePay() → 회계팀에서 정의하며, CFO 보고를 위해 사용
- reportHours() → 인사팀에서 정의하며, COO 보고를 위해 사용
- save() → 데이터베이스 관리자(DBA)가 정의하며, COO 보고를 위해 사용
이 위반으로 인해 CFO 팀에서 결정한 조치가 COO 팀이 의존하는 무언가에 영향을 줄 수 있다.

징후 2:
소스파일에서 많은 메서드를 포함해 병합이 자주 발생한다. 이들 메서드가 서로 다른 액터를 책임진다면, 병합이 발생할 가능성이 더 높다. 소스코드 병합은 동시에 서로 다른 목적으로 코드가 수정되면 둘 다 의도치 않은 영향을 끼친다.

SRP 해결책은 다양한데, 모든 해결책은 공통점이 있다.

공통점 : 메서드를 각기 다른 클래스로 이동시키기

확실한 해결책음 데이터와 메서드를 분리하는 방식이다. 메서드가 없는 데이터 구조 클래스를 만들고, 세 클래스가 데이터 구조 클래스를 공유하도록 하면 된다. 각 클래스는 자신의 메서드에 필요한 소스코드만 포함한다. 세 클래스는 서로의 존재를 몰라야 한다. 이때 우연한 중복을 피할 수 있다.
개발자는 세 클래스를 인스턴스화하고 추적해야 한다는 단점이 있다. 이 단점은 퍼사드 패턴을 적용하면 해결된다. 퍼사드 패턴의 클래스에는 코드가 거의 없다. 세 클래스의 객체를 생성하고 요청된 메시지를 갖는 객체로 위임하는 일만 처리한다.

다른 해결책으로는 업무 규칙을 데이터와 가깝게 배치하는 방법이다. 중요한 메서드는 Empolyee 클래스에 그대로 유지하고, 덜 중요한 메서드는 그것들에 대한 퍼사드를 수행하도록 하는 것이다.

SRP는 메서드와 클래스 수준의 원칙이다. 상위 수준에서는 다른 형태가 있다. 컴포넌트 수준에서의 공통 폐쇄 원칙(Common Closure Principle), 아키텍처 수준에서의 아키텍처 경계(Architectural Boundary)의 생성을 책임지는 변경의 축(Axis of Change)이 그것이다.

 

OCP에 의하면, 소프트웨어 개체의 행위는 확장할 수 있어야 하지만, 산출물을 변경해서는 안 된다는 것을 의미한다.

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

OCP가 클래스와 모듈 설계에 도움이 되는 원칙이라 생각하겠지만, 아키텍처 컴포넌트 수준에서 더 중요한 의미를 갖는다.

요구사항 확장 시 소프트웨어에 엄청난 수정이 필요하다면 아키텍트는 설계를 실패한 것이다. 아키텍트는 기능이 어떻게, 왜, 언제 발생하는지에 따라 기능을 분리하고 컴포넌트 계층 구조로 조직화한다. 계층구조의 컴포넌트는 저수준 컴포넌트에서 발생한 변경으로부터 고수준 컴포넌트를 보호할 수 있다.

컴포넌트 간의 방향성을 유지하기 위해 인터페이스는 의존성 역전을 사용한다. 의존성 역전이 없다면 클래스 간 방향성이 뒤죽박죽 되어 컴포넌트 간 방향성이 동시에 양방향이 되어버린다.

 

LSP는 상속과 구현(인터페이스)이 대표적인 예다.

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

LSP를 위반하는 대표적인 예는 정사각형과 직사각형 문제다. 직사각형의 하위 클래스로 정사각형을 둘 수 없다는 것이다. 직사각형과 달리 정사각형의 높이와 너비는 반드시 함께 변경되어야 하는데, 너비나 높이를 바꿀 때 직사각형의 하위 클래스로 두기에는 무리가 있기 때문이다.

LSP 위반을 막기 위한 유일한 방법은 직사각형이 실제로 정사각형인지 확인하는 방법이다. 하지만, 이럴 경우 직사각형을 다루는 클래스에서 행위가 타입에 의존하게 된다는 것을 알게 되며, 결국 타입을 치환할 수 없다는 결론에 도달한다.

객체 지향이 등장한 초창기에는 LSP는 상속의 가이드 정도로 간주되었다. 하지만, 현재는 인터페이스와 구현체에도 적용되는 고아범위한 설계 원칙으로 변했다.

 

ISP는 오퍼레이션의 분리를 의미한다.

OPS에 op1, op2, op3가 있다고 해보자. User1은 OPS의 op1을 사용하고, op2와 op3를 사용하지 않는다고 하더라도, User1은 op2와 op3에도 의존성을 갖게 된다. op2의 소스코드가 변경되어도 User1도 다시 컴파일되어야 한다.

이 문제는 인터페이스 단위 분리를 통해 해결 가능하다. User1이 U1Ops라는 op1의 인터페이스를 참조하고, U1Ops는 op1을 OPS에서 구현하는 역할을 맡는다면, User1은 U1Ops와 op1에는 의존하지만, OPS에는 의존하지 않는다.

다만, 정적 타입 언어(import, use, include 같은 타입 선언문을 사용하는 언어)에서  발생하는 문제로, 동적 타입 언어에서는 런타임 추론이 발생해 소스코드 의존성이 없다.

 

DIP는 저수준 모듈이 고수준 모듈에 의존하는 것을 말한다.

"유연성이 극대화된 시스템이란 소스코드 의존성이 추상에 의존하며 구체(concretion)에는 의존하지 않는 시스템이다."

정적 언어 타입에서는 use, import, include 구문은 오직 인터페이스나 추상 클래스 같은 선언만을 참조해야 됨을 의미한다. 구체적 대상에는 절대 의존해서는 안 됨을 의미한다. 동적 언어 타입에서도 동일한 규칙이 적용되지만, 이들 언어에서는 구체 모듈이 무엇인지 정의하기 다소 어려운 부분이 있다.

사실상 이 규칙은 비현실적이다. 소프트웨어 시스템이라면 구체적인 많은 장치에 반드시 의존하기 때문이다. 예를 들어, 자바 String은 구체 클래스로, 이를 추상 클래스로 만들려는 시도는 현실성이 없다. 하지만 String은 매우 안정적이고 변경되는 일이 거의 없으며 있더라도 엄격히 통제된다. 따라서 DIP를 논할 때 OS나 플랫폼 같이 안정성이 보장된 환경은 무시하는 편이다.

실제로 뛰어난 소프트웨어 설계자와 아키텍트라면 인터페이스 변동성을 낮추려 한다. 인터페이스를 변경하지 않고도 구현체에 기능을 추가할 방법을 찾기 위해 노력한다.

변동성 큰 구체적 객체는 주의해 생성해야 한다. 대다수 객체 지향 언어에서 바람직하지 못한 의존성을 처리할 때 추상 팩토리를 사용하곤 한다. 추상 팩토리는 아키텍처 경계를 만들고, 구체적인 것들로부터 추상적인 것들을 분리한다. 소스 코드 의존성은 아키텍처 경계를 교차할 때 단방향, 즉 추상적인 쪽으로 향한다. (구현 의존성)

분리된 두 컴포넌트를 보면, 추상 컴포넌트는 고수준 업무 규칙을 포함한다. 구체 컴포넌트는 업무 규칙을 다루기 위한 세부사항을 포함한다.

제어 흐름은 소스코드 의존성과 정반대 방향으로 아키텍처 경계를 가로지른다. 다시 말해, 소스코드 의존성은 제어 흐름과는 반대 방향으로 역전된다. 이것이 의존성 역전이다.

 

코딩 실천법

  • 변동성 큰 구체 클래스 대신 추상 인터페이스를 참조하라. 일반적으로 객체 생성 방식은 강하게 제약하며 추상 팩토리 사용을 강제한다.
  • 변동성 큰 구체 클래스를 파생하지 마라. 이전 규칙의 따름 규칙이다.
  • 구체 함수를 오버라이드 하지 마라. 구체 함수는 소스코드 의존성을 필요로 하기 때문에, 구체 함수의 오버라이딩은 의존성 제거가 아닌 의존성 상속이 된다. 차라리 추상 함수로 선언하고 구현체에서 각자 용도에 맞게 구현하라.
  • 구체적이며 변동성이 크다면 절대로 언급하지 마라.

 

DIP 위반을 모두 없앨 수는 없다. 팩토리 구현 클래스에서 구체 컴포넌트를 생성하는 일과 같이 필수적인 것들이 있다. 하지만, DIP 위배 클래스들은 적은 수의 컴포넌트 내부로 모을 수 있고 시스템의 다른 부분과 분리 가능하다.

 

SOLID가 벽과 방에 벽돌을 배치하는 것이라면, 컴포넌트 원칙은 빌딩에 방을 배치하는 방법이다.

 

컴포넌트는 배포 단위다. 시스템 구성 요소로 배포 가능한 가장 작은 단위로, 자바의 경우 jar 파일이 여기에 해당한다. 닷넷에서는 DLL, 컴파일형 언어에서는 바이너리 파일의 결합체, 인터프리터형 언어는 소스 파일의 결합체다.

여러 컴포넌트를 서로 링크해 실행 가능한 단일 파일로 생성할 수 있다. .war과 같은 단일 아카이브 파일, .jar 이나 .dll 같이 동적 로드 가능한 플러그인 또는 .exe 파일이 그 예다.

잘 설계된 컴포넌트라면 반드시 독립적 배포가 가능한, 독립적 개발이 가능한 형태를 갖춰야 한다.

 

어떤 클래스를 어떤 컴포넌트로 포함시켜야 할지는 제대로 된 소프트웨어 엔지니어링 원칙의 도움을 받아야 한다. 컴포넌트 응집도와 관련된 세 원칙은 다음과 같다.

  • REP(Reuse/Release Equivalence Principle) : 재사용/릴리즈 등가 원칙
  • CCP(Common Closure Principle) : 공통 폐쇄 원칙
  • CRP(Common Reuse Principle) : 공통 재사용 원칙

 

REP는 객체 지향 모델의 오랜 약속 중 하나가 실현된 것으로 볼 수 있다.

"재사용 단위는 릴리즈 단위와 같다."

소프트웨어 컴포넌트가 릴리즈 절차를 통해 추적 관리되지 않거나, 릴리즈 번호가 부여되지 않는다면 해당 컴포넌트를 재사용하고 싶어도 할 수 없을 것이다. 릴리즈 번호가 없다면 재사용 컴포넌트 간 호환성을 보증할 수도 없다. 또, 새 릴리즈의 변경사항을 전달할 수도 없다.

이는 설계와 아키텍처 관점에서 단일 컴포넌트가 응집도가 높아야 함을 의미한다. 컴포넌트를 구성하는 모든 모듈은 서로 공유하는 중요한 테마나 목적이 있어야 한다.

하지만 REP는 단순히 "이치에 맞다" 정도로, 클래스와 모듈을 단일 컴포넌트로 묵는 방법에 대해 설명하지 못한다. 이를 나머지 두 원칙에서 보완하게 된다.

 

CCP는 SRP의 컴포넌트 관점과도 같다.

"동일한 이유로 동일한 시점에 변경되는 클래스를 같은 컴포넌트로 묶어라. 서로 다른 시점에 다른 이유로 변경되는 클래스는 다른 컴포넌트로 분리하라."

단일 컴포넌트의 변경 이유는 여러 개 있어서는 안 된다. 대다수 유지보수성은 재사용성보다 훨씬 중요하다. 변경이 여러 컴포넌트 도처에 분산되어 발생하기보다는 단일 컴포넌트에 발생하는 것이 좋다.

 

CRP는 클래스와 모듈을 어느 컴포넌트에 위치시킬지 결정할 때 도움 되는 원칙이다. 재사용되는 경향이 있는 클래스와 모듈들은 같은 컴포넌트에 포함되어야 한다.

"컴포넌트 사용자들을 필요하지 않은 것에 의존하게 강요하지 마라."

개별 클래스가 단독으로 재사용되는 경우는 거의 없다. 대체로 재사용 모듈을 사용한다. CRP는 이런 클래스들이 동일 컴포넌트에 있어야 함을 말하는데, 이런 컴포넌트 내부에서는 클래스 간 수많은 의존성이 있으리라 예상 가능하다.

어떤 컴포넌트가 다른 컴포넌트를 사용하면 두 컴포넌트 사이 의존성이 생겨난다. 어쩌면 사용되는 컴포넌트의 단 하나의 클래스만 필요로 한 경우도 있다. 그래도 의존성은 조금도 약해지지 않는다. 의존하는 컴포넌트가 있다면 해당 컴포넌트의 모든 클래스에 의존함을 확실히 인지해야 한다. 따라서, 한 컴포넌트에 속한 클래스들을 더 작게 그룹 지어야 한다.

ISP의 포괄적 버전으로, 둘 다 '필요하지 않은 것에 의존하지 마라'는 내용이다.

 

REP는 재사용성, CCP는 유지보수성, CRP는 불필요한 릴리지를 피하기 위해 사용되는 개념이다.

 

컴포넌트 간 관계를 설명하는 세 원칙은 다음과 같다.

  • ADP(Acyclic Dependencies Principle) : 의존성 비순환 원칙
  • SDP(Stable Dependencies Principle) : 안정된 의존성 원칙
  • SAP(Stable Abstractions Principle) : 안정된 추상화 원칙

 

ADP는 컴포넌트간 의존성의 순환이 없어야 함을 말한다.

"컴포넌트 의존성 그래프에 순환이 있어서는 안 된다."

많은 개발자가 동시에 동일한 소스 파일을 수정하는 환경에서 이 의존성 순환이 발생할 수 있다. 이를 해결하는 방안으로는 두 가지가 있다.

  1. 주 단위 빌드
  2. ADP

주 단위 빌드는 중간 규모 프로젝트에서 흔히 사용되는 방식이다. 첫 4일은 각자 서로를 신경 쓰지 않고 5일째에 통합하는 방식이다. 그래서 규모가 큰 프로젝트일수록 통합이 하루 만에 끝나는 게 불가능하다.

ADP는 개발 환경을 릴리즈 가능한 컴포넌트 단위로 분리해 해결하는 방법이다. 컴포넌트는 개별 개발자 또는 단일 개발 팀이 책임질 작업 단위가 된다. 개발자는 지속적으로 컴포넌트를 개발하며, 나머지 개발자는 릴리즈 버전을 사용하는 방식으로 진행된다.

ADP를 사용하면 어떤 팀도 다른 팀에 의해 좌우되지 않게 되며, 각 팀에서 새 컴포넌트의 도입 여부를 추후에 결정할 수 있다. 통합은 작고 점진적으로 이뤄진다. 이 방식을 사용하려면 컴포넌트 사이 의존성 구조를 반드시 정리해야 한다.

 

SDP는 안정된 의존성 원칙을 말한다.

"안정성의 방향으로 의존하라."

설계는 정적일 수 없다. 변경이 불가피하다. OCP를 준수하면 컴포넌트가 다른 유형의 변경에 영향을 받지 않으면서 특정 유형의 변경에만 민감하게 만들 수 있다. 즉, 컴포넌트 중 일부는 변동성을 지니도록 설계해야 한다.

변경이 쉽지 않은 컴포넌트가 변동이 예상되는 컴포넌트에 의존하게 만들어서는 안 된다. 변경이 쉽지 않도록 설계하더라도 누군가 의존성을 매달아 버리면 당신의 모듈도 변경이 어려워진다. SDP를 준수하면 변경하기 어려운 모듈이 변경하기 쉽게 만들어진 모듈에 의존하지 않도록 함을 말한다.

소프트웨어 컴포넌트를 변경하기 어렵게 만드는 것은 다른 컴포넌트가 해당 컴포넌트에 의존하게 만드는 것이다. 컴포넌트로 의존하게 만들고 의존성이 많아지면 상당히 안정적이라 볼 수 있지만, 사소한 변경이라도 의존하는 모든 컴포넌트를 만족하기 위한 변경에 상당한 노력이 들 것이다.

반대로 어떤 컴포넌트도 의존하지 않는 컴포넌트는 책임성이 없다. 동시에 다른 컴포넌트에 의존하지 않다면 변경이 발생할 수 있는 외부 요인이 많아진다.

SDP를 만족하기 위해 안정된 컴포넌트는 유연한 컴포넌트에 의존하면 안 된다. 이때 DIP를 도입해 안정된 컴포넌트는 인터페이스에 의존하도록 하면 SDP가 만족한다.

 

SAP는 안정된 추상화 원칙을 말한다.

"컴포넌트는 안정된 정도만큼만 추상화되어야 한다."

고수준 정책은 시스템에서 자주 변경되어서는 안 된다. 고수준 정책을 캡슐화하는 소프트웨어는 반드시 안정된 컴포넌트에 위치해야 한다. 그 정책을 포함한 소스코드는 수정이 어려워진다. 즉, 유연성을 잃는다. 안정된 상태에서도 변경에 충분히 대응할 수 있도록 유연하게 할 수 있을까? 정답은 OCP에서 찾을 수 있다.

SAP는 안정성과 추상화 정도 사이의 관계를 정의한다. 안정된 컴포넌트는 추상 컴포넌트여야 하며, 불안정한 컴포넌트는 구체 컴포넌트여야 하며, 내부 구체 코드를 쉽게 변경할 수 있음을 말한다. 즉, 이를 통해 안정성이 컴포넌트 확장을 방해해서는 안 된다는 것을 말한다.

 

SAP와 SDP를 결합하면 컴포넌트에 대한 DIP를 말하는 것과 같다. SDP는 의존성이 안정성의 방향으로 향해야 함을 말하고, SAP는 안정성이 추상화를 의미함을 말하기 때문이다.

 

소프트웨어 아키텍트는 프로그래머다. 코드에서 탈피해 고수준의 문제에 집중해야 한다는 거짓말에 속아 넘어가면 안 된다. 아키텍트는 코드와 동떨어져서는 안 된다. 최고의 프로그래머로 팀원의 생산성을 극대화할 수 있는 설계를 하도록 방향을 이끌어야 한다.

이런 일을 용이하게 만들기 위해서는 가능한 많은 선택지를 가능한 오래 남겨두어야 한다.

위 정의에 놀랄 수 있겠으나, 아키텍처의 목표는 시스템을 제대로 동작하도록 만드는 것이 아니다. 운영도 되어야 하지만, 형편없는 아키텍처를 갖추더라도 운영은 충분히 가능하다. 아키텍처의 주된 목적은 시스템의 생명주기를 지원하는 것이다. 궁극적인 목표는 시스템 수명과 관련된 비용(이해, 개발, 유지보수, 배포)을 최소화하고, 프로그래머의 생산성은 최대화하는 데 있다.

 

과정별 비용과 관련해 설명을 하지만, 배포 과정에서 의미 있던 것 같다.

초기 개발 단계에서 MSA를 사용하자고 결정할 수 있다. 이 접근법으로 컴포넌트 경계가 뚜렷해지고 인터페이스가 안정화되어 시스템 개발이 쉽다고 판단할 것이다. 하지만, 배포시기가 되면 위협적일 만큼 수많은 마이크로 서비스를 발견할 수도 있다. 그로 인해 마이크로 서비스를 연결하고 설정하고 작동 순서를 결정할 때 오동작이 발생할 수도 있다.

아키텍트가 배포 문제를 처음부터 고려했다면, 더 작은 서비스를 사용하고 컴포넌트와 프로세스 수준의 컴포넌트를 하이브리드 형태로 융합해 좀 더 통합된 도구로 상호 연결을 관리했을 것이다.

 

소프트웨어는 두 종류의 가치가 있다.

  • 행위적 가치
  • 구조적 가치

구조적 가치가 일반적으로 더 중요하다. 소프트웨어를 말 그대로 부드럽게 만드는 가치이기 때문이다. 소프트웨어를 부드럽게 유지하는 방법은 가능한 선택사항을 많이, 오래 열어두는 것이다. 선택사항이란 중요치 않은 세부사항을 말한다.

 

소프트웨어 시스템은 주요한 두 가지 구성요소로 분해 가능하다.

  • 정책
  • 세부 사항

정책은 업무 규칙과 절차를 구체화한 진정한 가치다. 세부 사항은 정책과 소통할 때 필요한 요소이지만, 정책의 행위에는 조금도 영향을 미쳐서는 안 된다. 예를 들어, 입출력 장치, 데이터베이스, 웹 시스템, 서버, 프레임 워크, 통신 프로토콜 등이 세부사항이다.

아키텍트는 정책을 핵심적 요소로 식별하고, 동시에 세부사항은 정책과 무관하게 만들 수 있는 시스템을 구축해야 한다. 세부사항에 대한 결정은 미루거나 연기할 수 있어야 한다. 선택사항을 더 오래 열어 둘 수 있다면 더 많은 실험도 가능하고 더 많은 것을 시도할 수 있다. 그리고 결정을 연기할 수 없는 순간이 왔을 때 실험과 시도 덕에 많은 정보를 얻은 상태일 것이다.

아래는 예시들이다.

- 개발 초기에는 데이터베이스 시스템을 선택할 필요가 없다. 고수준 정책은 데이터 베이스가 관계형, 분산형, 계층형, 플랫 파일 여부와 상관없어야 한다.
- 개발 초기에는 웹 서버를 선택할 필요가 없다. 고수준 정책은 웹을 통해 정책이 전달되는 사실을 알아서는 안 된다. 프로젝트 후반까지 어떤 종료의 웹 시스템을 사용할지를 결정하지 않아도 된다.
- 개발 초기에는 REST를 적용할 필요가 없다. 고수준 정책은 외부 세계의 인터페이스에 대해 독립적이어야 한다. 마이크로 서비스 프레임워크, SOA 프레임워크 모두 적용할 필요가 없다.
- 개발 초기에는 의존성 주입 프레임워크를 적용할 필요가 없다. 고수준 정책은 의존성 해석 방식을 신경 써서는 안 된다.

 

좋은 아키텍처는 다음을 지원해야 한다.

  • 시스템 유스케이스
  • 시스템 운영
  • 시스템 개발
  • 시스템 배포

유스케이스는 시스템의 의도를 지원해야 함을 말한다. 아키텍트의 최우선 관심사는 유스케이스이며, 아키텍처에도 유스케이스가 최우선이다. 아키텍처는 시스템 행위에 큰 영향을 주지 않는다. 행위에 대해 열어 둘 수 있는 선택사항은 거의 없다. 아키텍처가 행위를 지원하기 위해 할 수 있는 일 중 가장 중요한 사항은 행위를 명확히 하고 외부로 드러내 시스템이 지닌 의도를 아키텍처 수준에서 알아볼 수 있게 하는 것이다.

운영 지원 관점에서 아키텍처는 더 실질적이며 덜 피상적인 역할을 맡는다. 아키텍트는 시스템에 따라 운영 작업을 허용할  수 있는 여러 형태를 아키텍처로 구조화할 수 있어야 한다. 병렬로 처리할 것인지, 같은 주소 공간을 공유할 것인지, 모노리틱 프로그램을 할 것인지 등에 대해 열어둬야 한다. 선택사항이기 때문이다.

개발을 지원에 있어, 아키텍처는 핵심적인 역할을 맡는다. 콘웨이 법칙이 적용된다. '시스템을 설계하는 조직이라면 어디든지 그 조직의 의사소통 구조와 동일한 구조의 설계를 만들어 낼 것이다.' 이러한 아키텍처를 만들려면 잘 격리되어 독립적으로 개발 가능한 컴포넌트 단위로 시스템의 분할이 필요하다.

배포 용이성을 결정하는 데 아키텍처는 중요한 역할을 한다. '즉각적인 배포'가 목표다. 좋은 아키텍처는 수십 개의 작은 설정 스크립트나 속성 파일을 약간씩 수정하는 방식을 사용하지 않는다. 좋은 아키텍처는 꼭 필요한 디렉터리나 파일을 수작업으로 생성하게 내버려 두지 않는다. 빌드 후 즉각 배포되어야 한다.

 

아키텍트는 종종 중복으로 인한 함정에 빠진다. 중복은 일반적으로 나쁘다. 하지만, 거짓 중복(우발적 중복)이 있을 수 있다. 중복으로 보이는 두 영역의 코드가 각자의 경로로 발전한다면 이는 더 이상 중복이 아닌 것이다.

이런 케이스는 코드를 통합하지 않도록 주의해야 한다. 보통 유스케이스를 통합하고 싶을 때 확인한다.

마찬가지로 수평 계층 분리 시에도 데이터베이스의 레코드 구조가 특정 화면의 레코드와 동일해 보여 복사가 아니라 그대로 전달하게 되는 경우가 있다. 이런 중복은 우발적이기 때문에 통합에 주의하자.

 

프로젝트 초기에는 결합 분리 수준(소스 수준 / 배포 수준(라이브러리) / 서비스 수준) 중 어떤 것이 최선인지 알기 어렵다. 프로젝트가 성장해 가면서 최적의 수준도 달라질 수 있다.

현시점 가장 인기가 있어 보이는 건 서비스 수준에서의 분리다. 비용이 많이 들고 결합이 큰 단위에서 이뤄진다는 문제가 있다. 마이크로 서비스가 아무리 작다 하더라도 충분히 작은 단위에서 분리될 가능성은 거의 없다.

컴포넌트가 서비스화 될 가능성이 있다면 저자는 컴포넌트 결합을 분리하되 서비스가 되기 직전에 멈추는 방식을 선호한다. 최대한 컴포넌트 간 동일한 주소 공간에 남겨두는 방식을 사용한다.

 

아키텍처는 선을 긋는 기술이며, 선을 경계라고 부른다. 경계는 소프트웨어 요소를 서로 분리하고 경계 반대편에 있는 요소를 알지 못하도록 막는다. 선의 일부는 프로젝트 수명 중 아주 초기에, 심지어 코드 작성 전에 그어지며, 어떤 선은 매우 나중에 그어진다.

아키텍트의 목표는 시스템을 만들고 유지하는 데 드는 인적 자원을 최소화하는 것이다. 인적 자원의 효율을 떨어뜨리는 요인은 결합(coupling)이다. 특히 너무 일찍 내려진 결정에 따른 결합이다.

이른 결정은 시스템의 업무 요구사항, 즉 유스케이스와 관련이 없는 결정이다. 프레임워크, 데이터베이스, 웹 서버, 유틸리티 라이브러리, 의존성 주입에 대한 결정 등 세부사항을 가리킨다.

 

서비스 중심으로 구조화된 소프트웨어 시스템은 본질적으로 잘못된 것이 아니다. 다만 SOA를 약속하는 일련의 도구들은 너무 일찍 채택해 적용하면 개발 비용을 가중시킬 뿐이다.

 

선을 어떻게 긋고, 언제 그을까? 관련이 있는 것과 없는 것 사이에 선을 긋는다.

GUI는 업무 규칙과 관련 없기 때문에, GUI와 업무규칙 사이에는 선이 있어야 한다. 
데이터베이스는 GUI와 관련 없기 때문에, 데이터베이스와 GUI 사이에는 선이 있어야 한다.
데이터베이스는 업무 규칙과 관련 없기 때문에, 데이터베이스와 업무 규칙 사이에는 선이 있어야 한다.

데이터베이스는 업무 규칙과 떼어놓을 수 없는 관계가 아니다. 업무 규칙이 간접적으로 사용할 수 있는 도구가 데이터베이스다. 업무 규칙은 스키마, 쿼리 언어, 데이터베이스와 관련된 나머지 세부사항 어떠한 것도 알아서는 안 된다. 데이터베이스는 인터페이스를 이용해 데이터를 로드하고 저장할 수 있어야 한다.

DAO가 데이터베이스 인터페이스를 실제로 구현해, 데이터베이스를 조작하는 일을 맡아야 한다. 경계선은 인터페이스와 DAO 사이 상속 관계를 횡단하며 그어진다. 즉, 비즈니스 규칙과 관련된 곳에서는 DAO의 존재 사실을 알지 못한다.

 

플러그인 아키텍처를 생각해 볼 수 있다. 데이터베이스와 GUI 모두 덜 중요한 컴포넌트로, 더 중요한 컴포넌트에 의존한다. 이를 살펴보면 일종의 패턴이 만들어지는데, 서드파티 플러그인을 사용할 수 있게 한 것과 동일하다.

플러그인 아키텍처는 수많은 종류의 사용자 인터페이스를 플러그인 형태로 연결할 수 있게 한다. 웹 기반일 수도 있고, 클라이언트/서버 기반이거나, SOA나 콘솔 기반 또는 임의의 어떤 사용자 인터페이스 기술이라도 가능하다.

데이터베이스도 동일하다. 플러그인으로 다루기 때문에 미래에 필요하리라 생각되는 어떤 종류의 데이터베이스 기술로도 대체할 수 있다.

누군가 웹 페이지 포맷을 변경하거나 데이터베이스 스키마를 변경해도 업무 규칙이 깨지지 않기를 바랄 것이다. 마찬가지로 시스템 한 부분이 변경되어도 관련 없는 나머지 부분이 망가지길 원하지 않는다. 플러그인 아키텍처는 이러한 변경이 전파될 수 없는 방화벽을 생성해 준다. 경계에는 변경의 축(axis of change)이 있는 지점에 그어진다. 경계에 따라 컴포넌트들은 경계 반대편과 다른 속도로, 그리고 다른 이유로 변경된다. 단일 책임 원칙에 따라 경계를 그으면 되는 것이다.

경계를 그리기 위해 시스템은 컴포넌트 단위로 분할되어야 한다. 일부 컴포넌트는 핵심 업무 규칙에 해당하고, 나머지 컴포넌트는 플러그인 형태로 연결된다. 이는 의존성 역전 원칙과 안정된 추상화 원칙을 응용한 것이다

 

소프트웨어 시스템이란 정책을 기술한 것이다. 소프트웨어 아키텍처를 개발하는 기술에는 이러한 정책을 신중하게 분리하고, 정책이 변경되는 양상에 따라 정책을 재편성하는 일도 포함된다. 동일한 이유로 동일한 시점에 변경되는 정책은 동일한 수준에 위치해 동일한 컴포넌트에 속해야 한다.

흔히 아키텍처 개발은 재편성된 컴포넌트들을 비순환 방향 그래프로 구성하는 기술을 포함한다. 노드는 동일 수준의 정책을 포함하는 컴포넌트에 해당하며, 간선은 의존성을 표현한다. 의존성은 소스 코드, 컴파일 타임의 의존성을 말한다. 좋은 아키텍처는 의존성 방향이 컴포넌트 수준을 기반으로 연결되도록 만들어야 한다. 즉, 저수준 컴포넌트가 고수준 컴포넌트에 의존하도록 설계해야 한다.

암호화 프로그램을 만든다고 해보자. 고수준 함수인 encrypt() 안에서 저수준 하뭇인 readChar()과 같은 함수에 의존하면 안 된다. reader와 writer를 인터페이스로 만들어야 하며, 인터페이스를 Console Reader/Writer로 실제 구현해야 한다.

 

애플리케이션을 업무 규칙과 플러그인으로 구분하려면, 업무 규칙이 실제로 무엇인지 잘 이해해야 한다. 업무 규칙은 사업적 수익을 얻거나 비용을 줄일 규칙 또는 절차다. 엄밀히 '컴퓨터상으로 구현되었는지와 상관없이' 동일하다.

이러한 규칙은 핵심 업무 규칙(critical business rule)이라 부를 것이다. 핵심 업무 규칙은 데이터를 요구하는데, 이 데이터를 핵심 업무 데이터라 부를 것이다. 핵심 규칙과 핵심 데이터는 본질적으로 결합되어 있다. 객체를 만들기 좋은 후보이며, 이러한 유형의 객체를 엔티티라 부를 것이다.

엔티티는 핵심 업무 데이터를 기반으로 동작하는 일련의 조그만 핵심 업무 규칙을 구체화한다. 인터페이스는 핵심 업무 데이터를 기반으로 동작하는 핵심 업무 규칙을 구현한 함수들로 구성된다. 엔티티는 꼭 클래스, 객체지향 언어를 사용할 필요는 없다.

 

모든 업무 규칙은 엔티티처럼 순수하지 않다. 자동화된 시스템의 구성요소로 존재해야만 의미 있는 경우도 있다. 이는 유스케이스로 볼 수 있다.

유스케이스는 자동화된 시스템이 사용하는 방법을 설명한다. 사용자가 제공해야 하는 입력, 보여줄 출력, 출력을 생성하기 위한 처리 단계로 이뤄진다. 엔티티의 핵심 업무 규칙과 반대로 유스케이스는 애플리케이션 특화 업무 규칙을 설명한다.

유스케이스에서 중요한 점은 시스템이 사용자에게 어떻게 보이는지를 설명하지 않는다. 사용자와 엔티티 사이 상호작용을 규정할 뿐, 데이터가 들어오고 나가는 방식은 유스케이스와 무관하다. 유스케이스만 봐서는 애플리케이션이 웹을 통해 전달되는지, 콘솔 기반인지, 순수 서비스인지, 클라이언트인지 알 수 없다.

 

엔티티(고수준)는 유스케이스(저수준)에 대해 아무것도 모른다. 의존성 역전 원칙을 준수하는 예다. 유스케이스가 입출력에 더 가까이 있기 때문에 저수준이라 볼 수 있다. 유스케이스도 데이터를 주고받는 방식은 알지 못해야 한다. 코드가 html이거나 sql을 사용하는 지를 알 수 없어야 한다.

 

아키텍처를 프레임워크로부터 제공받아서는 절대 안 된다. 프레임워크는 아키텍처가 사용하는 도구일 뿐이다. 아키텍처의 목적은 유스케이스를 중심에 두고, 프레임워크/도구/환경에 전혀 구애받지 않고 유스케이스를 지원하는 구조를 기술하는 것이다. 좋은 소프트웨어 아키텍처는 프레임워크/데이터베이스/웹 서버/여타 개발 환경 문제 및 도구에 대한 결정을 미룰 수 있게 만든다.

 

아키텍처가 유스케이스를 최우선으로 한다면, 프레임워크와는 적당히 거리를 둔다면, 프레임워크를 전혀 준비하지 않더라도 필요한 유스케이스 전부에 대한 단위 테스트를 할 수 있게 된다. 테스트를 돌리는 데 웹 서버나 데이터베이스가 반드시 필요한 상황이 되어서는 안 된다.

 

지난 수십 년간 시스템 아키텍처와 관련된 여러 아이디어를 봤다. 육각형 아키텍처(Hexagonal Architecture), DCI(Data, Context and Interaction), BCE(Boundary-Control-Entity) 등 세부적인 면에서 차이가 있더라도 내용은 상당히 비슷하다.

우선 각 아키텍처의 목표는 모두 동일한데, 관심사의 분리(Separation of concerns)다. 소프트웨어 계층 분리로 관심사의 분리라는 목표를 달성할 수 있다.

또한, 각 아키텍처는 최소한의 업무 규칙을 위한 계층 하나와 사용자와 시스템 인터페이스를 위한 다른 계층 하나를 반드시 포함한다.

추가적으로 아래의 특징을 지닌다.

  • 프레임워크 독립성
  • 테스트 용이성
  • UI 독립성
  • 데이터베이스 독립성
  • 모든 외부 에이전시에 대한 독립성

 

경계를 횡단하는 데이터는 어떤 모습이어야 하는가? 일반적으로 간단한 데이터 구조로 이루어져야 한다. 기본적인 구조체나 간단한 데이터 전송 객체 등 원하는 대로 고를 수 있다. 다른 다양한 방법으로 전달될 수 있는데, 중요한 점은 격리되어 있는 간단한 데이터 구조가 경계를 가로질러 전달된다는 사실이다.

데이터 구조가 어떤 의존성을 가져 의존성 규칙을 위배하게 되면 안 된다. 데이터베이스 프레임워크는 쿼리에 대한 응답으로 사용하기 편리한 행 구조의 데이터 포맷을 사용한다. 이 행 구조가 경계를 넘어 내부로 전달되면 의존성 규칙이 깨지게 된다. 내부에서도 외부 원의 무언가를 알아야 하기 때문이다. 따라서, 경계를 가로질러 데이터를 전달할 때 데이터는 내부 원에서 사용하기 편리한 형태를 가져야 한다.

 

험블 객체 패턴은 아키텍처 경계를 식별하고 보호하는 데 도움을 준다. 험블 객체 패턴은 디자인 패턴으로, 테스트하기 어려운 행위와 테스트하기 쉬운 행위를 단위 테스트 작성자가 분리하기 쉽게 하는 방법으로 고안되었다.

아이디어는 단순한데, 행위들을 두 개의 모듈 또는 클래스로 나눈다. 이 중 하나가 험블이다. 기본적인 본질은 남기고 테스트하기 어려운 행위를 험블로 이동시키는 것이다.

험블 객체 패턴을 이용하면 두 부류의 행위를 분리해, 프레젠터와 뷰라는 서로 다른 클래스로 만들 수도 있다.

 

프레젠터와 뷰를 살펴보자. 뷰는 험블 객체로, 테스트하기 어렵다. 이 코드는 가능한 한 간단하게 유지된다. 뷰는 데이터를 GUI로 이동시킬 뿐, 직접 처리하지 않는다.

프레젠터는 반대로 테스트하기 쉬운 객체다. 데이터를 받아 화면에 표현할 수 있는 포맷으로 변환하는 일을 수행한다.

이렇게 프레젠터와 뷰 사이 경계가 생기게 되고, 이는 험블 객체 패턴으로 볼 수 있다. 테스트 용이성은 좋은 아키텍처가 지녀야 할 속성이다.

 

아키텍처 경계를 완벽하게 만드는 데는 비용이 많이 든다. 쌍방향 다형적 경계 인터페이스, 입력과 출력을 위한 데이터 구조를 만들어야 하며, 두 영역을 독립적으로 컴파일하고 배포할 수 있는 컴포넌트로 격리하는 데 필요한 모든 의존성을 관리해야 한다.

많은 경우 뛰어난 아키텍트라면 경계를 만드는 비용이 너무 크다고 판단하면서도 나중에 필요할 수 있기 때문에 이런 경계에 필요한 공간을 확보하기 원할 수 있다.

그러나, 애자일 커뮤니티에 속한 많은 사람은 이런 종류의 선행적 설계를 탐탁지 않게 여긴다. YAGNI(You Aren't Going to Need It) 원칙을 위배하기 때문이다.

그럼에도 아키텍트는 여전히 필요하다고 외칠 수 있다. 그럴 때 부분적 경계(partial boundary)를 구현해 볼 수 있다.

 

부분적 경계를 생성하는 방법 중 하나는 독립적으로 컴파일하고 배포할 수 있는 컴포넌트를 만들기 위한 작업은 모두 수행 후 단일 컴포넌트에 그대로 모아두는 것이다. 쌍방향 인터페이스, 입력과 출력 데이터 구조 모두 분리만 하되, 단일 컴포넌트로 컴파일해서 배포한다는 뜻이다. 완벽한 경계를 만들 때만큼의 코드량과 사전 설계가 필요하지만, 다수의 컴포넌트를 관리하는 작업은 하지 않아도 된다.

추후 완벽한 경계로 확장할 수 있게 전통적인 전략 패턴을 사용할 수 있다. 또는 훨씬 더 단순한 경계로 Facade 패턴을 이용한 경계를 만들 수도 있다.

 

모든 시스템은 최소 하나의 컴포넌트가 존재하며, 이는 메인 컴포넌트다. 나머지 컴포넌트를 생성, 조정, 관리하는 역할을 수행하는데, 궁극적인 세부사항이다. 시스템의 초기 진입시점으로, 모든 팩토리, 전략, 나머지 기반 설비를 생성 후 더 높은 수준을 담당하는 부분으로 제어권을 넘긴다. 의존성 주입도 메인 컴포넌트에서 이뤄져야 한다. 이후 의존성 주입 프레임워크를 사용하지 않고도 의존성을 분배할 수 있어야 한다. 메인은 가장 지저분한 컴포넌트다.

 

테스트는 시스템의 일부이며, 아키텍처에도 관여한다. 시스템의 나머지 요소가 아키텍처에 관여하는 것과 동등하다.

아키텍처 관점에서 모든 테스트는 동일하다. 단위 테스트, 통합 테스트, 인수 테스트 등 테스트는 태생적으로 의존성 규칙을 따른다. 항상 테스트 대상이 되는 코드를 향해 의존성이 있다. 테스트는 아키텍처상 가장 바깥 원으로 생각할 수 있다. 내부의 어떤 것도 테스트에 의존하지 않는다. 또, 테스트는 독립적으로 배포된다.

테스트를 고려하지 않은 설계에서는 테스트가 시스템 설계와 잘 통합되지 않는다. 이때 테스트는 깨지기 쉬워지고 시스템은 뻣뻣해져 변경이 어렵다.

 

소프트웨어는 닳지 않지만, 펌웨어와 하드웨어에 대한 의존성을 관리하지 않으면 안으로부터 파괴될 수 있다.

 

펌웨어로부터 소프트웨어가 오염되지 않도록 해야 한다. 펌웨어를 수없이 양산하는 일을 멈추고 코드 수명을 늘릴 수 있는 기회를 주어야 한다.

 

앱-티튜드 테스트(App-titude test)는 앱이 동작하도록 만드는 것에 대해 저자가 부르는 말이다. 잠재적 임베디드 소프트웨어는 왜 그렇게도 많이 펌웨어로 변하는가? 임베디드 코드가 동작하는 데 대부분의 노력을 하고, 오랫동안 유용하게 남도록 구조화하는 데는 신경 쓰지 않기 때문으로 보인다.

켄트 백의 소프트웨어를 구축하는 세 가지 활동에 대해 보자.

  • 먼저 동작하게 만들어라
  • 그리고 올바르게 만들어라
  • 그리고 빠르게 만들어라 (요구되는 성능을 만족시켜라)

임베디드에서는 두 번째 활동에 무관심해 보인다. 프레드 브룩스(Fred Brooks)가 말한 "버리기 위한 계획을 세우라"는 말과 켄트 백이 말하는 것은 같다.

클린 임베디드 아키텍처는 테스트하기 쉬운 임베디드 아키텍처다. 소스코드와 펌웨어가 섞이는 안티 패턴은 지양해야 한다. 또한, 하드웨어는 언제까지나 세부사항이기 때문에 하드웨어 추상화 계층(HAL)을 잘 이용해야 한다. 즉, HAL은 소프트웨어와 펌웨어 사이 경계로 볼 수 있다.

 

프레임워크는 인기가 상당하다. 그러나, 프레임워크 제작자는 당신과 당신의 문제를 알지 못한다. 당신의 문제가 프레임워크가 풀려는 문제가 겹칠수록 프레임워크는 유용할 것이다.

그렇다 하더라도 업무 객체 생성 시 프레임워크 기반 클래스를 파생하지 마라. 프락시를 만들어 업무 규칙에 플러그인 가능한 컴포넌트에 프락시를 위치시켜라. 프레임워크와는 강하게 결합되지 말고 적당히 거리를 유지해야 한다. 프레임워크는 세부사항이기 때문이다.

C++의 STL과 Java의 표준 라이브러리는 정말 필요한 프레임워크와의 결합이다. 여기서 거리를 둬야 하는 프레임워크는 애플리케이션 프레임워크를 말하는 것이다.

728x90