본문 바로가기

클린아키텍처

[클린 아키텍처] 2부 프로그래밍 패러다임

** 클린 아키텍처 소프트웨어 구조와 설계의 원칙 책을 읽고 작성한 글입니다.

 

2장 내용을 소개하기에 앞서 1장 내용을 간략하게 정리하자면,

 

설계와 아키텍처

우리는 흔히 "아키텍처"라고 하면 세부사항과는 분리된 고수준의 무언가를 생각하고,

"설계"라고 하면 저수준의 구조 혹은 세부사항을 생각합니다.

 

하지만 집을 설계한다고 했을 때 아키텍트가 도면을 설계하면서

고수준의 결정사항부터 시작하여 무수히 많은 저수준의 세부사항까지 알아야하고

이 모든 사항이 집 전체 설계의 구성요소가 되듯이,

소프트웨어 설계에서도 저수준의 세부사항과 고수준의 구조를 구분 짓는 경계는 뚜렷하지 않습니다.

 

그렇다면 "잘 만든 소프트웨어"란 무엇일까요?

 

결론부터 말하면 아래와 같습니다.

 

  1. 기능이 정상적으로 동작하는 것 
  2. 추가 구현 요청사항이나 수정이 있을 때 이를 반영하기 쉬운 것 (확장 가능성)

 

소프트웨어 개발에 있어서 기능이 정상적으로 동작하는 것은 너무나도 당연한 일이고,

개발자는 지속적으로 새로운 요청사항에 유연하게 대응하며 서비스를 확장해나가야 합니다.

 

처음에는 빠르게 서비스를 개발했지만,

이후 코드 한 줄을 추가하는데도 너무 많은 공수가 들기 시작한다면

개발자 입장에서는 무수히 많은 노력을 들이지만 생산성은 떨어지고

경영자 입장에서도 인건비가 많이 드는 비효율적인 상황이 됩니다.

 

많은 팀에서 당장 눈앞의 출시(행위)에 급급해

"나중에 고쳐야지" 하며 유지보수성(구조)를 뒷전으로 미루는 경우가 많으나

그 "나중"은 오지 않습니다.

 

이와 더불어 책에서는 TDD(Test-Driven Development)를 적용한 팀이 적용하지 않은 팀에 비해

훨씬 더 빠른 시간 내 개발을 완료했고, 시간이 갈수록 그 차이가 커진 사례를 들며 

좋은 구조가 장기적으로는 속도를 높일 수 있다는 것을 명확히 보여줍니다.

 

 

 

소프트웨어의 두가지 가치에 대한 이야기: 행위와 구조

소프트웨어의 첫번째 가치는 행위(Behavior) 입니다.

행위는 쉽게 이야기하면 이해관계자에게 돈을 벌어다주는 코드를 작성하는 것,

즉 고객이 요구한 기능을 구현하는 것을 말합니다.

 

두번째 가치는 구조(Architecture) 입니다.

이는 소프트웨어라는 단어와 관련이 있습니다. 소프트웨어의 "소프트"에서 유추할 수 있는 것처럼

행위를 쉽게 변경할 수 있어야 합니다.

 

그렇다면 우리가 더 큰 가치를 두어야 할 것은 행위 혹은 구조 중에서 무엇일까요? 

 

  1. 완벽 동작, 수정 불가
  2. 동작 불가, 수정 가능

 

위 두가지 선택지 중 선택을 해야할 때

업무관리자 혹은 급하게 기능 개발이 필요한 개발자는 1번을 선택하겠지만,

 

장기적으로 보았을 때 소프트웨어의 생명을 유지하는 근본적인 힘은

변경 용이성에 있기 때문에 2번이 더 나은 선택일 것입니다.

 

이와 함께 등장하는 것이 아이젠하워의 아래와 같은 매트릭스 입니다.

 

 

소프트웨어의 첫번째 가치인 행위는 긴급하지만 매번 높은 중요도를 가지지는 않습니다.

그리고 두번째 가치인 구조는 긴급하지 않지만 높은 중요도를 가집니다.

 

아이젠하워 매트릭스에 따르면 우선순위는 다음과 같습니다.

 

  1. 긴급하고 중요한 (행위 + 구조)
  2. 긴급하지는 않지만 중요한 (구조)
  3. 긴급하지만 중요하지 않은 (행위)
  4. 긴급하지도 중요하지도 않은

 

많은 개발팀에서 저지르는 실수가 바로 3번(긴급하지만 중요하지 않은 행위)을 1순위로 격상시키는 것입니다.

 

"당장 고객이 원하니까", "일단 출시가 급하니까"라는 이유로

구조를 희생하면 시간이 지날수록 소프트웨어는 망가지고,

그 책임은 오롯이 개발팀이 지게 됩니다.

 

따라서 개발자는 구조의 중요성을 다른 팀으로부터 지켜내고 설득해야 합니다.

이것이 바로 우리가 개발자로 고용된 이유입니다.

 

 

여기까지가 1장의 내용입니다.


이어서, 본론으로 들어가 2장의 내용을 소개해 보도록 하겠습니다.

 

소프트웨어 개발의 세 가지 패러다임

소프트웨어 개발에는 크게 세 가지 프로그래밍 패러다임이 있습니다.

흥미로운 점은 이 세 패러다임 모두 프로그래머에게서 특정 권한을 박탈한다는 것입니다.

 

1. 구조적 프로그래밍 (Structured Programming)

구조적 프로그래밍은 제어흐름의 직접적인 전환에 대해 규칙을 부과합니다.

 

쉽게 말해, goto문의 무분별한 사용을 제한하고

순차(sequence), 분기(selection), 반복(iteration)이라는 세 가지 제어 구조만을 사용하도록 합니다.

 

이러한 제한이 왜 중요할까요?

 

구조적 프로그래밍을 통해 우리는 실행 흐름이 예측 가능해졌고,

프로그램을 증명 가능한 작은 단위로 분해할 수 있게 되었기 때문입니다.

 

여기서 "증명"이라는 것은 수학적 증명보다는 과학적 방법에 가깝습니다.

 

  • 수학은 임을 증명합니다
  • 과학은 반례를 통해 거짓임을 증명합니다

 

소프트웨어 테스트도 마찬가지입니다.

 

테스트는 버그가 있음을 보여줄 뿐, 버그가 없음을 보여줄 수는 없습니다.

 

따라서 우리가 할 수 있는 최선은 코드를 테스터블하게 만들고,

충분한 테스트를 통해 반례를 찾으려 노력하는 것입니다.

 

반례를 찾지 못했다면, 그 모듈은 목표에 부합할 만큼 충분히 안정적이라고 볼 수 있습니다.

 

2. 객체 지향 프로그래밍 (Object-Oriented Programming)

객체 지향 프로그래밍은 제어흐름의 간접적인 전환에 대해 규칙을 부과합니다.

 

많은 사람들이 OOP를 "데이터와 함수의 조합" 또는 "실제 세계를 모델링하는 방법"이라고 설명하지만,

이는 OOP의 본질을 완전히 설명하지 못합니다.

 

OOP의 핵심은 캡슐화, 상속, 다형성입니다.

 

다만, 캡슐화는 OOP만의 특징이 아닙니다.

C 언어에서도 가능했으며, 오히려 C++의 등장으로 완벽한 캡슐화가 깨졌다고 볼 수 있습니다.

 

상속은 OOP 언어가 확실히 제공한 기능입니다.

OOP 이전에도 상속을 흉내낼 수는 있었지만, OOP만큼 편리하지는 않았고 다중 상속을 구현하기는 어려웠습니다.

 

가장 중요한 것은 다형성입니다.

 

다형성이 가진 힘

C 언어에서의 예시를 먼저 들어보겠습니다.

 

STDIN->read()

 

 

위와 같은 코드를 실행한다고 가정해봅시다.

 

위 코드를 실행하면 우리는 "콘솔용 입출력 드라이버"라고 명시하지 않았지만

런타임에 적절한 입출력 장치로 자동으로 연결됩니다.

 

이것이 가능한 이유는 C 런타임 라이브러리가 프로그램 시작 시

stdin을 현재 환경에 맞는 입출력 장치로 초기화해주기 때문입니다.

 

즉, 여러 종류의 입출력 장치(콘솔, 파일, 네트워크 소켓 등)가 구현되어 있고,

프로그램 실행 환경에 따라 필요한 장치가 자동으로 선택되어 같은 코드로 다양한 입력 소스를 처리할 수 있습니다.

 

이것이 다형성의 힘입니다.

 

OOP 언어가 다형성을 새롭게 만든 것은 아니지만,

C 언어에서 함수 포인터와 수많은 관례 코드를 사용해야 했던 것을 훨씬 편리하게 구현할 수 있게 해주었습니다.

 

 

실질적인 예를 들어보겠습니다.

 

특정 입출력 장치에서 잘 동작하는 복사 프로그램을 필기체 인식 장치나 음성 합성 장치에도 사용하려는 상황을 생각해봅시다.

 

나쁜 경우:

 

[인식 장치] ← [복사 프로그램]
[새로운 인식 장치] ← [새로운 복사 프로그램]  // 복사 프로그램을 새로 만들어야 함

 

 

좋은 경우:

 

[인식 장치] → [File 인터페이스]
[새로운 인식 장치] → [File 인터페이스]
[File 인터페이스] ← [복사 프로그램]  // 복사 프로그램은 변경 불필요!

 

 

복사 프로그램이 구체적인 인식 장치에 의존하지 않고 인터페이스에만 의존하기 때문에,

아무런 변경 없이 새로운 장치를 지원할 수 있습니다.

 

의존성 역전 (Dependency Inversion)

 

다형성의 진정한 힘은 의존성 역전에 있습니다.

 

일반적으로 제어흐름이 시스템의 행위에 따라 결정되고,

소스 코드 의존성은 제어흐름을 따라 결정됩니다.

 

하지만 다형성을 사용하면 제어흐름과 소스 코드 의존성이 같은 방향이 아니게 만들 수 있습니다.

 

예를 들어 아래의 그림을 보면

 

제어흐름은 HL1에서 ML1으로 향하지만,

소스 코드 의존성은 HL1이 Interface를 향합니다.

 

이것이 바로 의존성 '역전'입니다.

 

고수준 컴포넌트 레벨에서 보면,

 

[UI] → [Business Rules] ← [Database]

 

 

이렇게 하면 Business Rules가 UI나 Database의 변경으로부터 독립적으로 유지될 수 있습니다.

 

 

더 정확히 표현하면,

  • UI는 Business Rules가 정의한 인터페이스를 구현
  • Database는 Business Rules가 정의한 인터페이스를 구현
  • Business Rules는 구체적인 UI나 Database에 의존하지 않고, 자신이 정의한 인터페이스에만 의존

 

즉, UI나 Database가 변경되어도 Business Rules는 영향을 받지 않으며,

Business Rules의 변경은 인터페이스를 통해 제어할 수 있습니다.

 

핵심은 의존성의 방향이 항상 Business Rules를 향한다는 것입니다.

UI와 Database가 Business Rules에 의존하지, Business Rules가 UI나 Database에 의존하지 않습니다.

 

3. 함수형 프로그래밍 (Functional Programming)

함수형 프로그래밍은 할당문에 대해 규칙을 부과합니다. 핵심은 불변성(Immutability)입니다.

 

예를 들어, 클로저(Clojure)와 같은 함수형 언어에서는 가변 변수를 사용할 수 없습니다.

모든 데이터는 한 번 할당되면 변경할 수 없습니다.

 

실용적인 함수형 아키텍처에서는:

  • 불변 컴포넌트를 독립적으로 두고
  • 가변 컴포넌트를 최소화하며
  • 불변 컴포넌트가 가변 컴포넌트를 의존하는 구조를 만듭니다

가변 컴포넌트에서는 동시성 문제가 발생할 수 있는데,

이때 이벤트 소싱(Event Sourcing) 같은 기법을 사용합니다.

 

이벤트 소싱 (Event Sourcing)

이벤트 소싱은 상태를 직접 저장하는 대신 상태를 변경하는 트랜잭션 자체를 저장합니다.

 

예를 들어, 

 

일반적인 방식 (상태 저장):

 

계좌 잔액: 100,000원

 

 

입금이나 출금이 발생하면 잔액을 직접 수정합니다.

 

 

이벤트 소싱 방식 (트랜잭션 저장):

 

1. 계좌 개설: +100,000원
2. 출금: -20,000원
3. 입금: +50,000원
4. 출금: -10,000원

 

 

현재 잔액을 알고 싶다면, 모든 이벤트를 순서대로 실행하면 됩니다.

100,000 - 20,000 + 50,000 - 10,000 = 120,000원

 

 

컴퓨팅 자원(데이터 저장소)의 소모가 크다는 단점은 있지만, 아래와 같은 장점을 얻을 수 있습니다.

  • 모든 변경 이력이 보존 (감사 추적 가능)
  • 특정 시점의 상태로 되돌릴 수 있음
  • 데이터를 수정하지 않기 때문에 불변성을 유지 가능

즉, 저장 공간과 처리 능력이 충분하다면

애플리케이션은 완전한 불변성을 가지도록 만들 수 있고, 이를 통해 완전한 함수형을 만들 수 있습니다.

 

마치며

 

세 가지 프로그래밍 패러다임은 각각 우리에게서 특정 권한을 빼앗습니다

 

  • 구조적 프로그래밍: goto의 무분별한 사용
  • 객체 지향 프로그래밍: 함수 포인터의 직접 사용
  • 함수형 프로그래밍: 변수의 재할당

 

하지만 이러한 제약이야말로 우리가 더 나은 소프트웨어를 만들 수 있게 해주는 원칙입니다.

 

좋은 아키텍처는 단순히 기능을 구현하는 것을 넘어,

변화에 유연하게 대응할 수 있고, 테스트 가능하며, 각 부분이 독립적으로 발전할 수 있는 구조를 만드는 것입니다.

'클린아키텍처' 카테고리의 다른 글

[클린 아키텍처] 3부 SOLID 설계 원칙  (0) 2025.12.16