아무 생각 없이 사용하던 spring boot, 이 친구에 대해서 알고 싶어 정리를 해보았습니다.

이 글에서 Spring Boot의 등장 배경과 스프링의 핵심 개념 (IoC, DI, Bean, Container)에 대해서 살펴보겠습니다.

 


 

 

1. 자바의 탄생

스프링을 이해하기 전에, 그 근간이 되는 자바에 대해서 먼저 살펴보겠습니다.

 

1990년대, 당시 대부분의 언어는 운영체제에 종속되어 있었습니다. 예를 들어, Window용으로 작성한 프로그램은 Linux나 macOS에서 동작하지 않았습니다.

하지만 1995년, Sun Microsystems는 혁명적인 슬로건과 함께 새로운 프로그래밍 언어 Java를 세상에 선보였습니다.

 

"Write Once, Run AnyWhere"
한 번 작성하면 어디서나 실행된다

 

JVM 위에서 실행되는 자바는 운영체제에 관계없이 동일한 코드가 작동했습니다.

또한 C/C++의 복잡함을 줄이고, 객체지향 개념(OOP)을 쉽게 구현할 수 있는 언어로 유행을 타기 시작했습니다.

이 덕분에 자바는 개인용 애플리케이션에서 기업용 서버 애플리케이션까지 영역을 넓혔습니다.

특히 안정성과 확장성이 뛰어나 기업 환경에서 표준 플랫폼으로 자리 잡았습니다.


2. J2EE 시대의 도래

1999년, Sun은 J2EE(Java 2 Platform, Enterprise Edition)을 발표합니다. 

이건 기업용 웹 애플리케이션 개발 표준을 자바로 통합하려는 시도였습니다.

이 시도로 자바는 대규모 엔터프라이즈 시스템의 핵심 플랫폼이 되었습니다.

 

J2EE는 기업 환경의 복잡한 문제(트랜잭션, 보안, 분산 처리 등)를 표준화하려는 시도였습니다.

하지만 실제 개발 환경에서는 아래와 같은 불폄함이 생겼습니다.

XML 설정 지옥 단순한 기능 하나 추가에도 여러 설정 파일을 수정해야 함
EJB 의존성 문제 순수 자바 객체(POJO)가 아닌, EJB 컨테이너에 종속된 객체만 동작
테스트 불가능 컨테이너가 없으면 코드 실행조차 안 됨
생산성 저하 간단한 비즈니스 로직 하나를 구현해도 배포, 재시작, 설정 반복

3. Spring의 등장

이런 불편함을 해결하기 위해 Rod Johnson은 2002년, 단순한 객체(POJO)와 유연한 의존성 관리 메커니즘(DI)을 주장하며 스프링의 철학을 제안합니다.

 

스프링 철학 설명
POJO (Plain Old Java Object) 프레임워크에 종속되지 않는 순수 자바 객체
IoC / DI (제어 역전 / 의존성 주입) 객체의 생성과 연결을 외부 컨테이너가 담당
AOP (관점 지향 프로그래밍) 공통 기능(트랜잭션, 로깅 등)을 핵심 로직과 분리
테스트 용이성 컨테이너 없이 단위 테스트 가능

 

이 철학을 구현한 프레임워크가 바로 2003년에 등장한 Spring Framework 입니다.

Spring은 이후로도 계속 진화하며 개발자들의 생산성을 높였습니다.


4. Spring Boot의 등장

Spring이 충분히 도움이 되었지만 여전히 초기 설정이 많았습니다.

2014년, Spring팀은 새로운 철학을 바탕으로 Spring Boot를 공개했습니다.

Spring의 복잡한 설정을 자동화하여 누구나 쉽게 실행할 수 있도록 만든 것입니다.

 

"Convention over Configuration"
설정보다 관례

 

기존 Spring과 Spring Boot의 차이는 아래와 같습니다.

비교 항목 Spring Framework Spring Boot
목적 기업용 애플리케이션 핵심 기능 제공 Spring을 쉽고 빠르게 실행하기 위한 플랫폼
시작 방식 XML/자바 설정 + 외부 서버 배포 @SpringBootApplication + 내장 서버로 즉시 실행
서버 구성 외부 WAS 필요 (Tomcat, Jetty 등) 내장 서버 포함 (Tomcat, Jetty, Undertow)
설정 방식 수동 설정 (Bean 등록, 트랜잭션 관리 등) 자동 구성
의존성 관리 개별 라이브러리 버전 직접 관리 Starter 종속성으로 통합 관리
실행 명령 서버에 WAR 배포 java -jar 한 줄 실행
운영 기능 별도 구성 필요 Actuator로 Health check / 모니터링 내장
철학 유연성 중심 생산성과 간결성 중심

핵심 요약

  • 자바 - 플랫폼 독립성과 객체지향 철학으로 대규모 시스템의 기초가 됨
  • J2EE/EJB - 기업 시스템 표준을 제시했지만 복잡함으로 비판받음
  • Spring - 단순함, IoC/DI, POJO 철학으로 J2EE의 대안을 제시
  • Spring Boot - 스프링의 복잡한 설정을 자동화하고, 개발 경험을 혁신

 

로머트 C. 마틴의 Clean Architecture 책을 읽고 정리한 내용입니다.

 

12장. 컴포넌트


  • 배포 단위의 컴포넌트
    • 컴포넌트는 시스템의 구성 요소로 배포할 수 있는 가장 작은 단위
    • 잘 설계된 컴포넌트라면 반드시 독립적으로 배포 가능한, 따라서 독립적으로 개발 가능한 능력을 갖춰야 함

 

 

 

 

13장. 컴포넌트 응집도


  • 컴포넌트 응집도에 관련된 세 가지 원칙
    • REP: 재사용/릴리스 등가 원칙(Reuse/Release Equivalence Principle)
    • CCP: 공통 폐쇄 원칙(Common Closure Principle)
    • CRP: 공통 재사양 원칙(Common Reuse Principle)

 

[REP: 재사용/릴리스 등가 원칙]

  • 소개
    • 재사용 단위는 릴리스 단위와 같다.
  • 소프트웨어 설계와 아키텍처 관점에서의 원칙
    • 단일 컴포넌트는 응집성 높은 클래스와 모듈들로 구성되어야 함을 뜻함
    • 임의로 선택된 클래스와 모듈로 구성되어서는 안 됨
    • 컴포넌트를 구성하는 모든 모듈은 서로 공유하는 중요한 테마나 목적이 있어야 함
    • 하나의 컴포넌트로 묶인 클래스와 모듈은 반드시 함께 릴리스 할 수 있어야 한다.
  • 다른 원칙과의 관계
    • CCP와 CRP는 REP를 엄격하게, 하지만 제약을 가하는 측면에서 정의

 

[CCP: 공통 폐쇄 원칙]

  • 소개
    • 동일한 이유로 동일한 시점에 변경되는 클래스를 같은 컴포넌트로 묶어라. 서로 다른 시점에 다른 이유로 변경되는 클래스는 다른 컴포넌트로 분리하라.
  • 컴포넌트 관점의 SRP
    • SRP를 컴포넌트 관점에서 다시 쓴 것
    • 단일 컴포넌트는 변경의 이유가 여러 개 있어서는 안된다고 말함
  • 애플리케이션의 유지보수성
    • 대다수의 애플리케이션에서 유지보수성은 재사용성보다 훨씬 중요함
    • 코드가 반드시 변경되어야 한다면, 여러 컴포넌트에서 발생하기보다는, 차라리 변경 모두가 단일 컴포넌트에서 발생하는 편이 나음
  • CCP의 권장사항
    • CCP는 같은 이유로 변경될 가능성이 있는 클래스는 모두 한곳으로 묶을 것을 권함
    • 물리적 또는 개념적으로 강하게 결합되어 항상 함께 변경되는 클래스들은 하나의 컴포넌트에 속해야 한다.
    • 이를 통해 소프트웨어를 릴리스, 재검증, 배포하는 일과 관련된 작업량을 최소화 할 수 있음
  • OCP와 관련성
    • CCP의 폐쇄와 OCP의 폐쇄는 같은 뜻
    • 100% 완전한 폐쇄란 불가능하므로 전략적으로 폐쇄해야 함
    • CCP에서는 동일한 유형의 변경에 대해 닫혀 있는 클래스들을 하나의 컴포넌트로 묶음으로써 변경이 필요한 요구사항이 발생했을 때, 변경이 영향을 주는 컴포넌트들이 최소한으로 한정될 가능성이 확실히 높아짐
  • SRP와의 유사성
    • CCP는 컴포넌트 수준의 SRP
    • 두 원칙의 교훈
      • 동일한 시점에 동일한 이유로 변경되는 것들을 한데 묶어라. 서로 다른 시점에 다른 이유로 변경되는 것들은 서로 분리하라.

 

[CRP: 공통 재사용 원칙]

  • 소개
    • 컴포넌트 사용자들을 필요하지 않는 것에 의존하게 강요하지 마라
  • 클래스와 모듈을 어디서 위치시킬 것인가?
    • 클래스와 모듈을 어느 컴포넌트에 위치시킬지 결정할 때 도움이 되는 원칙
    • 같이 재사용되는 경향이 있는 클래스와 모듈들은 같은 컴포넌트에 포함해야 한다고 말함
  • 컴포넌트 내부의 수많은 의존성
    • 개별 클래스가 단독으로 재사용되는 경우는 거의 없음
    • 대체로 재사용 가능한 클래스는 재사용 모듈의 일부로써 해당 모듈의 다른 클래스와 상호작용하는 경우가 많음
    • 이런 클래스들이 동일한 컴포넌트에 포함되어야 한다고 말함
    • 이러한 컴포넌트 내부에서는 클래스들 사이에 수많은 의존성이 있으리라고 예상할 수 있음
  • 컴포넌트 의존 시 주의점
    • 의존하는 컴포넌트가 있다면 해당 컴포넌트의 모든 클래스에 대해 의존함을 확실히 인지해야 함
    • 한 컴포넌트에 속한 클래스들은 더 작게 그룹지을 수 없음
    • 그중 일부 클래스에만 의존하고 다른 클래스와는 독립적일 수 없음을 확실히 인지
  • 동일한 컴포넌트로 묶어서는 안되는 클래스
    • 강하게 결합되지 않은 클래스들을 동일한 컴포넌트에 위치시켜서는 안된다고 말함
  • ISP와의 관계
    • CRP는 ISP의 포괄적인 버전
    • 두 원칙의 요약
      • 필요하지 않은 것에 의존하지 말라.

 

[컴포넌트 응집도에 대한 균형 다이어그램]

  • 상충되는 세 원칙
    • ERP와 CCP
      • 포함 원칙
      • 컴포넌트를 더욱 크게 만듬
    • CRP
      • 배제 원칙
      • 컴포넌트를 더욱 작게 만듬
    • 이 원칙들이 균형을 이루는 방법을 찾아야 함
  • 균형 다이어그램
    • 프로젝트 초기에는 CCP가 REP보다 훨씬 중요
      • 개발 가능성이 재사용성보다 더욱 중요하기 때문
    • 프로젝트의 컴포넌트 구조는 시간과 성숙도에 따라 변함

 

[결론]

  • 시간이 흐름에 따라 프로젝트의 초점이 개발가능성에서 재사용성으로 바뀌고, 그에 따라 컴포넌트를 구성하는 방식도 진화함

로머트 C. 마틴의 Clean Architecture 책을 읽고 정리한 내용입니다.

 

7장. SRP: 단일 책임 원칙


  • SRP에 대한 오해
    • 부적절한 이름 때문에 SOLID 원칙 중에서 그 의미가 가장 잘 전달되지 못한 원칙
    • 모든 모듈이 단 하나의 일만 해야 한다는 의미로 받아들이기 쉬움
      • 해당 원칙은 따로 있음 - 함수는 반드시 하나의, 단 하나의 일만 해야 한다는 원칙
      • 이 원칙은 커다란 함수를 작은 함수들로 리팩터링하는 더 저수준에서 사용됨
      • 이 원칙은 SOLID 원칙이 아니며, SRP도 아님
    • 진정한 SRP의 의미: 단일 모듈은 변경의 이유가 하나, 오직 하나뿐이어야 한다.  
  • SRP의 다른 버전
    • 소프트웨어 시스템은 사용자와 이해관계자를 만족시키기 위해 변경됨
    • SRP가 말하는 변경의 이유란 바로 이들 사용자와 이해관계자를 가르킴
    • SRP의 다른 버전: 하나의 모듈은 하나의, 오직 하나의 사용자 또는 이해관계자에 대해서만 책임져야 한다.  
  • SRP의 최종 버전
    • 사용자나 이해관계자라는 단어는 두 명 이상일 경우 여기에 쓰기에는 올바르지 않음
    • 여기에서는 집단, 즉 해당 변경을 요청하는 한 명 이상의 사람들을 가르킴 - 이러한 집단은 액터
    • SRP의 최종버전: 하나의 모듈은 하나의, 오직 하나의 액터에 대해서만 책임져야 한다.
  • 모듈이란?
    • 가장 단순한 정의는 소스 파일
    • 코드를 소스 파일에 저장하지 않는 경우
      • 단순히 함수와 데이터 구조로 구성된 응집된 결합
  • 응집
    • 응집된(cohesive)이라는 단어가 SRP를 암시
    • 단일 액터를 책임지는 코드를 함께 묶어주는 힘이 바로 응집성(cohesion)
    • SRP를 이해하는 가장 좋은 방법은 이 원칙을 위반하는 징후들을 살펴보는 것

 

[징후 1: 우발적 중복]

  • 급여 애플리케이션의 Employee
    • 해당 클래스가 세 가지 메서드 calculatePay(), reportHours(), save()를 가짐
      • calculatePay(): 회계팀에서 기능을 정의, CFO 보고를 위해 사용
      • reportHours(): 인사팀에서 기능을 정의, COO 보고를 위해 사용
      • save(): DBA가 기능을 정의, CTO 보고를 위해 사용
    • 이들 세 가지 메서드가 서로 매우 다른 세명의 액터를 책임지기 때문에 SRP를 위반
    • 단일 클래스에 세 액터가 서로 결합되었고, 이로 인해 다른 팀에서 결정한 조치가 다른팀이 의존하는 무언가에 영향을 줄 수 있음
  • 문제의 원인
    • 서로 다른 액터가 의존하는 코드를 너무 가까이 배치했기 때문에 발생
    • SRP는 서로 다른 액터가 의존하는 코드를 서로 분리하라고 말함

 

[징후 2: 병합]

  • 문제의 발생
    • CTO팀에 속한 DBA는 테이블 스키마를 수정하고, COO팀에 속한 인사 담당자는 보고서 포맷을 변경하기로 결정
    • 서로 다른 팀의 개발자가 변경사항을 적용하면 병합이 발생
    • 발생한 병합은 CTO와 COO를 곤경에 빠뜨리고 CFO도 영향을 받게 됨
  • 문제의 원인
    • 많은 사람이 서로 다른 목적으로 동일한 소스 파일을 변경하는 경우에 해당
    • 이 문제를 벗어나는 방법은 서로 다른 액터를 뒷받침하는 코드를 서로 분리하는 것

 

[해결책]

  • 문제의 해결책
    • 다양한 해결책 모두가 메서드를 각기 다른 클래스로 이동시키는 방식
    • 가장 확실한 해결책은 데이터와 메서드를 분리하는 방식
      • 아무런 메서드가 없는 간단한 데이터 구조인 EmployeeData 클래스를 만들고 세 개의 클래스가 공유하도록 함
      • 각 클래스는 자신의 메서드에 반드시 필요한 소스 코드만을 포함
      • 세 클래스는 서로의 존재를 몰라야 함 - 우연한 중복을 피할 수 있음
  • 퍼사드(Facade) 패턴 
    • 이 해결책은 세 가지 클래스를 인스턴스화하고 추적해야하 한다는 단점이 존재
    • 이러한 난관에서 빠져나올때 흔히 쓰는 기법이 퍼사드 패턴
      • 코드가 거의 없는 EmployeeFacade를 생성, 세 클래스의 객체를 생성하고, 요청된 메서드를 가지는 객체로 위임하는 일을 책임짐
      • 가장 중요한 업무 규칙을 데이터와 가깝게 배치하는 방식도 존재
        • 가장 중요한 메서드는 기존의 Employee 클래스에 그대로 유지하되, Employee 클래스를 덜 중요한 나머지 메서드들에 대한 퍼사드로 사용할 수 있음

 

[결론]

  • SRP는 메서드와 클래스 수준의 원칙
  • 상위의 두 수준에서도 다른 형태로 다시 등장
    • 컴포넌트 수준에서는 공통 폐쇄 원칙(Common Closure Principle)
    • 아키텍처 수준에서는 아키텍처 경계(Architectural Boundary)의 생성을 책임지는 변경의 축(Axis of Change)

 

 

 

 

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


  • OCP의 의미
    • 소프트웨어 개체(artifact)는 확장에는 열려 있어야 하고, 변경에는 닫혀 있어야 한다.
    • 다시 말해 소프트웨어 개체의 행위는 확장할 수 있어야 하지만, 이때 개체를 변경해서는 안 된다.
    • 소프트웨어 아키텍처를 공부하는 가장 근본적인 이유

 

[사고 실험]

  • 재무제표 
    • 재무제표를 웹 페이지로 보여주는 시스템에서 동일한 정보를 보고서 형태로 변환해서 출력을 요청
    • 소프트웨어 아키텍처가 훌륭하다면 변경되는 코드의 양이 가능한 한 최소화. 이상적인 변경량은 0
  • 변경량을 최소화하는 방법
    • 서로 다른 목적으로 변경되는 요소를 적절하게 분리하고(SRP), 이들 요소 사이의 의존성을 체계화함으로써(DIP) 변경량을 최소화할 수 있음
  • 컴포넌트 계층구조의 조직화
    • 아키텍트는 기능이 어떻게, 왜, 언제 발생하는지에 따라서 기능을 분리하고, 분리한 기능을 컴포넌트의 계층구조로 조직화함
    • 이와 같이 조직화하면 저수준 컴포넌트에서 발생한 변경으로부터 고수준 컴포넌트를 보호할 수 있음

 

[결론]

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

 

 

 

 

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


 

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

 

[정사각형/직사각형 문제]

  • LSP를 위반하는 전형적인 문제
    • 만약 Rectangle의 하위 타입으로 Square가 존재하면 Square는 하위 타입으로 적합하지 않음
      • Rectangle의 높이와 너비는 서로 독립적으로 변경될 수 있는 반면, Square의 높이와 너비는 반드시 함께 변경되기 때문
    • 이런 형태의 LSP 위반을 막기 위한 유일한 방법은 Rectangle이 실제로는 Square인지를 검사하는 메커니즘을 User에 추가하는 것
      • 이렇게 하면 User의 행위가 사용하는 타입에 의존하게 되므로, 타입을 서로 치환할 수 없게 됨

 

[LSP와 아키텍처]

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

 

[결론]

  • LSP는 아키텍처 수준까지 확장할 수 있고, 반드시 확장해야만 함

 

 

 

 

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


  • ISP의 유래
    • 다수의 사용자가 한 클래스의 각 하위 메서드를 사용한다면 다른 메서드를 사용하지 않음에도 다른 메서드레 의존하게 됨
    • 이러한 문제는 인터페이스 단위로 분리해서 해결 가능

 

[ISP와 아키텍처]

  • ISP를 사용하는 동기
    • 일반적으로, 필요 이상으로 많은 걸 포함하는 모듈에 의존하는 것은 해로운 일
    • 소스 코드 의존성의 경우, 불필요한 재컴파일과 재배포를 강제하기 때문
    • 더 고수준인 아키텍처 수준에서도 마찬가지 상황이 발생

 

[결론]

  • 교훈
    • 불필요한 짐을 실은 무언가에 의존하면 예상치 못한 문제에 빠진다는 사실

 

 

 

 

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


  • 유연성이 극대화된 시스템
    • 소스 코드 의존성이 추상(abstraction)에 의존하며 구체(concretion)에는 의존하지 않는 시스템
    • 정적 타입 언어
      • use, import, include 구문은 오직 인터페이스나 추상 클래스 같은 추상적인 선언만을 참조해야 한다는 뜻
    • 동적 타입 언어
      • 소스 코드 의존 관계에서 구체 모듈은 참조해서는 안된다
  • 비현실적인 아이디어
    • 소프트웨어 시스템이라면 구체적인 많은 장치에 반드시 의존
    • 구체 클래스에 대한 소스 코드 의존성은 벗어날 수 없고, 벗어나서도 안 됨
    • 안정적인 클래스는 변경될 일이 거의 없으며, 있더라도 엄격하게 통제됨
    • 이러한 이유로 DIP를 논할 때 운영체제나 플랫폼 같이 안정성이 보장된 환경에 대해서는 무시하는 편
  • 의존을 피해야 하는 것
    • 변동성이 큰(volatile) 구체적인 요소
    • 이 구체적인 요소는 우리가 개발하는 중이라 자주 변경될 수 밖에 없는 모듈들

 

[안정된 추상화]

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

 

[팩토리]

  • 주의점
    • 변동성이 큰 구체적인 객체는 특별히 주의해서 생성해야 함
    • 사실상 모든 언어에서 객체를 생성하려면 해당 객체를 구체적으로 정의한 코드에 대해 소스 코드 의존성이 발생하기 때문
    • 자바 등 대다수의 객체 지향 언어에서 이처럼 바람직하지 못한 의존성을 처리할 때 추상 팩토리를 사용하곤 함
  • 아키텍처 경계
    • 추상 팩토리는 구체적인 것들과 추상적인 것들을 분리함
    • 소스 코드 의존성은 분리할 때 모두 한 방향, 즉 추상적인 쪽으로 향함
  • 두 가지 컴포넌트
    • 추상 컴포넌트
      • 애플리케이션의 모든 고수준 업무 규칙을 포함
    • 구체 컴포넌트
      • 업무 규칙을 다루기 위해 필요한 모든 세부사항을 포함

 

[구체 컴포넌트]

  • 피할 수 없는 DIP 위배
    • DIP 위배를 모두 없앨 수 없음
    • 하지만 DIP를 위배하는 클래스들은 적은 수의 구체 컴포넌트 내부로 모을 수 있고, 시스템의 나머지 부분과는 분리할 수 있음
    • 대표적인 예시로 main 함수

 

[결론]

  • 고수준의 아키텍처 원칙을 다루게 되면서 DIP는 몇 번이고 계속 등장
  • DIP는 아키텍처 다이어그램에서 가장 눈에 드러나는 원칙

로머트 C. 마틴의 Clean Architecture 책을 읽고 정리한 내용입니다.

 

3장. 패러다임 개요


  • 세 가지 패러다임
    • 구조적 프로그래밍(structured programming)
    • 객체지향 프로그래밍(object-oriented programming)
    • 함수형 프로그래밍(functional programming)

 

[구조적 프로그래밍]

  • 최초로 적용된 패러다임
  • 제어흐름의 직접전인 전환에 대해 규칙을 부과

 

[객체 지향 프로그래밍]

  • 제어흐름의 간접적인 전환에 대해 규칙을 부과

 

[함수형 프로그래밍]

  • 가장 먼저 만들어졌지만 최근에 들어서야 도입 시작
  • 할당문에 대해 규칙을 부과

 

[생각할 거리]

  • 각 패러다임은 프로그래머에게서 권한을 박탈
  • 새로운 권한을 부여하지 않음
  • 부정적인 의도를 가지는 일종의 추가적인 규칙을 부과
  • 무엇을 해야할지를 말하기보다는 무엇을 해서는 안 되는지를 말해줌
  • 세 가지 패러다임 각각은 goto문, 함수 포인터, 할당문을 앗아감
  • 프로그래밍 패러다임은 앞으로도 딱 세가지 밖에 없을 것

 

[결론]

  • 아키텍처 경계를 넘나들기 위한 메커니즘으로 다형성을 이용
  • 함수형 프로그래밍을 이용하여 데이터의 위치와 접근 방법에 대해 규칙을 부과
  • 모듈의 기반 알고리즘으로 구조적 프로그래밍을 사용

 

 

 

 

4. 구조적 프로그래밍


[증명]

  • 데이크스트라의 문제 해결
    • 증명이라는 수학적인 원리를 적용하여 문제를 해결하고자 함
      • 프로그래머는 입증된 구조를 이용하고, 이들 구조를 코드와 결합시키며, 코드가 올바르다는 사실을 스스로 증명하게 되는 방식
    • goto 문장이 모듈을 더 작은 단위로 재귀적으로 분해하는 과정에 방해가 되는 경우가 있다는 사실을 발견
    • goto문의 좋은 사용방식은 if/then/else와 do/while과 같은 분기와 반복이라는 단순한 제어구조에 해당한다는 사실을 발견
  • 구조적 프로그래밍의 탄생
    • 이러한 제어 구조는 순차실행과 결합했을 때 특별하다는 사실을 깨달음
    • 이 발견은 모듈을 증명 가능하게 하는 제어 구조가 모든 프로그램을 만들 수 있는 제어 구조의 최소 집합과 동일하다는 사실을 증명

 

[해로운 성명서]

  • goto문의 해로움
    • 데이크스트라는 세 가지 제어 구조에 대한 의견을 피력
    • 많은 논쟁이 있었으나 결국 goto 문장은 사라지게 됨
    • 현재 우리는 모두 구조적 프로그래머
      • 제어흐름을 제약 없이 직업 전활할 수 있는 선택권 자체를 언어에서 제공하지 않기 때문

 

[기능적 분해]

  • 모듈을 증명 가능한 더 작은 단위로 재귀적으로 분해할 수 있게 되었고, 기능적으로 분해할 수 있음을 뜻함
  • 프로그래머는 대규모 시스템을 모듈과 컴포넌트로 나눌 수 있음
  • 모듈과 컴포넌트는 입증할 수 있는 아주 작은 기능들로 세분화 할 수 있음

 

[테스트]

  • 테스트는 프로그램이 잘못되었음을 증명할 수 있지만, 프로그램이 맞다고 증명할 수는 없음
  • 소프트웨어 개발은 수학적인 시도가 아니라 과학과 같음. 올바르지 않음을 증명하는 데 실패함으로써 올바름을 보여주기 때문

 

[결론]

  • 반증 가능한 단위를 만들어 낼 수 있는 능력
    • 구조적 프로그래밍이 오늘날까지 가치 있는 이유
    • 또한 현대적 언어가 아무런 제약 없는 goto 문장은 지원하지 않는 이유
    • 뿐만 아니라 아키텍처 관점에서는 기능적 분해를 최고의 실천법 중 하나로 여기는 이유
  • 소프트웨어는 과학과 같고, 반증 가능성에 의해 주도됨
  • 모듈, 컴포넌트, 서비스가 쉽게 반증 가능하도록 만들기 위해 분주히 노력해야 함

 

 

 

 

객체 지향 프로그래밍


  • OO란 무엇인가?
    • 데이터와 함수의 조합
      • 그다지 만족스러운 대답은 아님
      • o.f()가 f(o)가 다르다는 의미를 내포하는데, 이미 훨씬 이전부터 데이터 구조를 함수에 전달했음
    • 실제 세계를 모델링하는 새로운 방법
      • 얼버무리는 수준
      • 의도조차도 불분명하며 그 정의가 모호함
    • 세 가지 주문
      • 캡슐화, 상속, 다형성
      • OO가 이 세 가지 개념을 적절하게 조합한 것이거나 OO 언어는 최소한 세 가지 요소를 반드시 지원해야 한다고 말함

 

[캡슐화?]

  • 언급하는 이유
    • 데이터와 함수를 쉽고 효과적으로 캡슐화하는 방법을 OO 언어가 제공하기 때문
    • 이를 통해 데이터와 함수가 응집력 있게 구성된 집단을 서로 구분 짓는 선을 그을 수 있음
    • 하지만 이러한 개념이 OO 에만 국한된 것은 아님
  • OO언어의 캡슐화
    • C는 헤더와 구현체를 분리하는 방식으로 완벽한 캡슐화를 제공했음
    • 하지만 자바와 C#은 이 방식을 버렸고, 캡슐화는 심하게 훼손되었음
    • 하지만 많은 OO 언어가 캡슐화를 거의 강제하지 않음
    • OO 프로그래밍은 프로그래머가 충분히 올바르게 행동함으로써 캡슐화된 데이터를 우회해서 사용하지 않을 거라는 믿음을 기반으로 함

 

[상속?]

  • OO의 상속
    • OO 언어가 더 나은 캡슐화를 제공하지는 못했지만, 상속만큼은 OO 언어가 확실히 제공
    • 하지만 상속이란 단순히 어떤 변수와 함수를 하나의 유효 범위로 묶어서 재정의하는 일
    • OO 언어가 고안되기 훨씬 이전에도 상속과 비슷한 기법이 사용되었으나 상속만큼 편리한 방식은 절대 아님
    • OO 언어가 완전히 새로운 개념을 만들지는 못했지만, 일을 상당히 편리한 방식으로 제공
  • 점수
    • 캡슐화에 대해서는 OO에 점수를 줄 수 없음
    • 상속에 대해서만 0.5점 점도를 부여
    • 그저 그런 점수지만 고려해야 할 속성이 하나 더 남음

 

[다형성?]

  • OO의 다형성
    • OO 언어가 있기 이전에도 다형성을 표현할 수 있었음
    • OO가 새롭게 만든 것은 전혀 없지만, 다형성을 좀 더 안전하고 더욱 편리하게 사용할 수 있게 해줌
    • OO 언어는 위험한 함수 포인터를 지켜야 하는 관례를 없애주며 실수 할 위험이 없음
    • 이러한 이유로 OO는 제어흐름을 간접적으로 전환하는 규칙을 부과한다고 결론지을 수 있음
  • 다형성이 가진 힘
    • 플러그인 아키텍처
      • 입출력 장치 독립성을 지원하기 위해 만들어짐
      • 등장 이후 거의 모든 운영체제에서 구현되었으나 함수를 가리키는 포인터를 사용하면 위험을 수반하기 때문에 직접 작성하는 프로그램에서는 이러한 개념을 확장하여 적용하지 않음
      • OO의 등장으로 언제 어디서든 적용할 수 있게 되었음
    • 의존성 역전
      • 전형적인 호출 트리
        • main 함수가 고수준 함수를 호출하고 고수준 함수는 중간 수준 함수를 호출, 중간 수준 함수는 저수준 함수를 호출
        • 소스 코드 의존성의 방향은 반드시 제어흐름을 따르게 됨
        • 즉, 제어흐름은 시스템의 행위에 따라 결정되며, 소스 코드 의존성은 제어흐름에 따라 결정됨
      • 다형성의 등장 이후
        • 인터페이스를 통해 소스 코드 의존성이 제어 흐름과는 반대가 되는데, 이를 의존성 역전이라고 함
        • 소프트웨어 아키텍트는 시스템의 소스 코드 의존성 전부에 대해 방향을 결정할 수 있는 절대적인 권한을 갖음
      • 배포 독립성과 개발 독립성
        • 업무 규칙이 데이터베이스와 UI에 의존하는 대신 의존성을 반대로 배치하여 데이터베이스와 UI가 업무 규칙에 의존하게 만들 수 있음
        • 결과적으로 업무 규칙, UI, 데이터베이스는 세 가지로 분리된 컴포넌트 또는 배포 가능한 단위로 컴파일할 수 있음
        • 배포 독립성
          • 특정 컴포넌트의 소스 코드가 변경되면, 해당 코드가 포함된 컴포넌트만 다시 배포하면 됨
        • 개발 독립성
          • 시스템의 모듈을 독립적으로 배포할 수 있게 되면, 서로 다른팀에서 각 모듈을 독립적으로 개발할 수 있음
    • 결론
      • 소프트웨어 아키텍트에게 OO란?
        • 다형성을 이용하여 전체 시스템의 모든 소스 코드 의존성에 대한 절대적인 제어 권한을 획득할 수 있는 능력
        • 플러그인 아키텍처를 구성할 수 있고, 이를 통해 고수준의 정책을 포함하는 모듈은 저수준의 세부사항을 포함하는 모듈에 대해 독립성을 보장할 수 있음
        • 저수준의 세부사항은 중요도가 낮은 플러그인 모듈로 만들 수 있고, 고수준의 정책을 포함하는 모듈과는 독립적으로 개발하고 배포할 수 있음

 

 

 

 

함수형 프로그래밍


[정수를 제곱하기]

  • 가변 변수는 프로그램 실행 중에 상태가 변할 수 있음
  • 함수형 프로그래밍은 가변 변수가 전혀 없음
  • 함수형 언어에서 변수는 변경되지 않음

 

[불변성과 아키텍처]

  • 변수의 가변성을 염려하는 이유
    • 경합 조건, 교착상태 조건, 동시 업데이트 문제가 모두 가변 변수로 인해 발생
    • 어떠한 변수도 갱신되지 않는다면 경합 조건이나 동시 업데이트 문제가 일어나지 않음
    • 락이 가변적이지 않다면 교착상태도 일어나지 않음
    • 불변성은 실현 가능하지만 일종의 타협을 해야 함

 

[가변성의 분리]

  • 주요한 타협
    • 애플리케이션 또는 애플리케이션 내부의 서비스를 가변 컴포넌트와 불변 컴포넌트로 분리하는 일
    • 불변 컴포넌트에서는 순수하게 함수형 방식으로 작업, 어떤 가변 변수도 사용하지 않음
    • 변수의 상태를 변경할 수 있는 (순수 함수형 컴포넌트가 아닌) 하나 이상의 다른 컴포넌트와 서로 통신
  • 애플리케이션의 구조화
    • 제대로 하기위해서는 변수를 변경하는 컴포넌트와 변경하지 않는 컴포넌트를 분리리해야 함
    • 이를 위해 가변 변수들을 보호하는 적절한 수단을 동원해 뒷받침해야 함

 

[이벤트 소싱]

  • 기본 발상
    • 상태가 아닌 트랜잭션을 저장하자는 전략
    • 상태가 필요해지면 단순히 상태의 시작점부터 모든 트랜잭션을 처리
    • 저장 공간과 처리 능력이 충분하면 애플리케이션이 완전한 불변성을 갖도록 만들 수 있음

 

[결론]

  • 요약
    • 구조적 프로그래밍은 제어흐름의 직접적인 전환에 부과되는 규율
    • 객체 지향 프로그래밍은 제어흐름의 간접적인 전환에 부과되는 규율
    • 함수형 프로그래밍은 변수 할당에 부과되는 규율
  • 소프트웨어의 핵심
    • 순차, 분기, 반복, 참조로 구성됨. 그 이상도 이하도 아님

로머트 C. 마틴의 Clean Architecture 책을 읽고 정리한 내용입니다.

 

1장. 설계와 아키텍처란?


  • 설계와 아키텍처의 정의
    • 아키텍처 : 저수준의 세부사항과는 분리된 고수준의 무언가
    • 설계 : 저수준의 구조 도는 결정사항 등을 의미
  • 설계와 아키텍처의 관계
    • 저수준의 설계와 고수준의 아키텍처는 모두 소프트웨어의 전체 설계의 구성요소
    • 단절없이 이어지며 이를 통해 대상 시스템의 구조를 정의
    • 개별로는 존재할 수 없고 이 둘을 구분 짓는 경계는 뚜렷하지 않음
    • 고수준에서 저수준으로 향하는 의사결정의 연속성만이 있음

 

[목표는?]

  • 소프트웨어 아키텍처의 목표
    • 필요한 시스템을 만들고 유지보수하는 데 투입되는 인력을 최소화하는 데 있다.
  • 좋은 설계란?
    • 설계 품질을 재는 척도는 고객의 요구를 만족시키는 데 드는 비용을 재는 척도
    • 이 비용이 낮을 뿐만 아니라 시스템의 수명이 다할 때까지 낮게 유지할 수 잇다면 좋은 설계라고 할 수 있음.
    • 새로운 기능을 출시할 때마다 비용이 증가한다면 나쁜 설계

 

[사례 연구]

  • 실제 데이터로 보는 사례
    • 주요 소프트웨어 출시 때마다 엔지니어링 직원 수가 꾸준히 증가
    • 하지만 동일한 기간의 생산성은 한 곳으로 수렴하는 것처럼 보임
    • 동일한 기간의 코드 라인당 비용도 엄청난 상승
    • 이러한 비용 곡선은 사업 모델의 수익을 엄청나게 고갈시키며, 회사의 성장을 멈추게 하거나 심지어는 완전히 망하게 만듬
    • 이는 엉망진창이 되어 가는 신호
  • 토끼와 거북이
    • 토끼는 타고난 빠르기를 과신한 나머지 경주를 심각하게 받아들이지 않아 낮잠을 자버리고, 거북이에게 패배
    • 토끼 개발자
      • 잠을 자는 것이 아니라 뼈 빠지게 일하지만 훌륭하고 깔끔하게 잘 설계된 코드가 중요하다는 사실을 알고 있는 뇌가 잠자고 있다.
      • 절대 수그러들지 않는 시장의 압박에 시장 출시가 먼저라며 코드 정리를 미룬다.
      • 토끼가 빠르기를 과신한 것처럼 생산성을 유지할 수 있다고 과신한다.
      • '지저분한 코드를 작성하면 빠르게 갈 수 있고 장기적으로 볼 때만 생산성이 낮아진다.라는 거짓말에 속는다.
  • 소프트웨어의 단순한 진리
    • 빨리 가는 유일한 방법은 제대로 가는 것이다.
    • 자신을 과신한다면 재설계하더라도 원래의 프로젝트와 똑같이 엉망으로 내몰린다.

 

[결론]

  • 개발 조직의 최고의 선택지
    • 조직에 스며든 과신을 인지하여 방지
    • 소프트웨어 아키텍처의 품질을 심각하게 고민하기 시작
      • 좋은 소프트웨어 아키텍처가 무엇인지 이해해야 함
      • 비용은 최소화하고 생산성은 최대화할 수 있는 설계와 아키텍처를 가진 시스템을 만들려면 시스템 아키텍처가 지닌 속성을 알고 있어야 함

 

 

 

 

2장. 두 가지 가치에 대한 이야기


  • 이해관계자의 두 가지 가치
    • 소프트웨어 시스템은 행위(behavior)와 구조(structure)라는 두 가지 가치를 이해관계자에게 제공
    • 소프트웨어 개발자는 두 가치 모두 반드시 높게 유지해야 하는 책임을 짐
    • 하지만 한 가치 가치에만 집중하고 나머지 가치는 배제하곤 함. 결국에는 시스템이 쓸모없게 되버림

 

[행위]

  • 프로그래머를 고용하는 이유
    • 이해관계자를 위해 기계가 수익을 창출하거나 비용을 절약하도록 만들기 위함
    • 이해관계자가 기능 명세서나 요구사항 문서를 구체화할 수 있도록 도움
    • 이해관계자의 기계가 이러한 요구사항을 만족하도록 코드를 작성
    • 만약 기계가 요구사항을 위반한다면, 디버거를 열고 문제를 고침
    • 많은 프로그래머가 이러한 활동이 전부라고 생각하지만 그들은 틀렸음.

 

[아키텍처]

  • 소프트웨어란?
    • 부드러운(soft)와 제품(ware)이라는 단어의 합성어
  • 소프트웨어의 본연의 목적
    • 반드시 부드러워야 하며 변경하기 쉬워야 한다.
    • 변경사항을 적용하는 데 드는 어려움은 변경되는 범위(scope)에 비례해야 하며, 변경사항의 형태(shape)와는 관련이 없어야 한다.
  • 소프트웨어 개발 비용 증가의 주된 요인
    • 변경사항의 범위와 형태의 차이에 있음
    • 개발 비용은 요청된 변경사항의 크기에 비례
    • 시스템의 형태와 요구사항의 형태가 서로 맞지 않으면 새로운 요청사항이 발생할 때마다 조금 더 힘들어짐
  • 문제의 원인
    • 시스템의 아키텍처가 문제
    • 아키텍처가 특정 형태를 다른 형태보다 선호하면 할수록, 새로운 기능을 이 구조에 맞추는 게 더 힘들어 짐
    • 따라서 아키텍처는 형태에 독립적이어야 하고, 그럴수록 더 실용적

 

[더 높은 가치]

  • 동작하는 프로그램 vs 변경이 쉬운 프로그램
    • 업무 관리자에게 묻는다면 동작하는 것이 더 중요하다고 얘기하며, 개발자는 대체로 동조하는 태도를 취함
    • 아래와 같은 양 극단의 사례를 검토하는 방식으로 반박가능
      • 완벽하게 동작하지만 수정이 불가능하다면 프로그램의 요구사항 변경 시 동작하지 않게 되고 결국 프로그램이 돌아가도록 만들 수 없게 된다.
      • 동작은 하지 않지만 변경이 쉬운 프로그램을 준다면 프로그램이 돌아가도록 만들 수 있고, 변경사항이 발생하더라도 여전히 동작하도록 유지보수할 수 있다.
    • 변경이 불가능한 프로그램은 존재하지 않지만 현실적으로 분가능한 시스템은 존재
      • 변경에 드는 비용이 변경으로 창출되는 수익을 초과하는 경우

 

[아이젠하워 매트릭스]

  • 드와이트 D. 아이젠하워가 고안한 매트릭스
    • 내겐 두 가지 유형의 문제가 있습니다. 하나는 긴급하며, 다른 하나는 중요합니다. 긴급한 문제는 중요하지 않으며, 중요한 문제는 절대 긴급하지 않습니다.
    • 중요함
      긴급함
      중요함
      긴급하지 않음
      중요하지 않음
      긴급함
      중요하지 않음
      긴급하지 않음
  • 소프트웨어의 매트릭스
    • 소프트웨어의 첫 번째 가치인 행위는 긴급하지만 매번 높은 중요도를 가지는 것은 아님
    • 소프트웨어의 두 번째 가치인 아키텍처는 중요하지만 즉각적인 긴급성을 필요로 하는 경우는 절대 없음
    • 우선순위는 다음과 같이 매길 수 있음
      • 1. 긴급하고 중요한
      • 2. 긴급하지는 않지만 중요한
      • 3. 긴급하지만 중요하지 않은
      • 4. 긴급하지도 중요하지도 않은
    • 아키텍처는 1,2번에, 행위는 1,3번에 위치함
  • 업무 관리자와 개발자의 흔한 실수
    • 긴급하지만 중요하지 않은 항목을 첫 번째로 격상시켜 버림
    • 긴급하지만 중요하지 않은 기능과 진짜로 긴급하면서 중요한 기능을 구분하지 못함
    • 업무 관리자는 보통 아키텍처의 중요성을 평가할 만한 능력을 겸비하지 못하지때문에 이것을 해결하기 위해 소프트웨어 개발팀은 마땅히 책임져야함

 

[아키텍처를 위해 투쟁하라] 

  • 가치를 위한 투쟁
    • 개발팀은 회사에서 가장 중요하다고 스스로 믿는 가치를 위해 투쟁
    • 관리팀도 자신만의 가치를 위해 투쟁하며, 마케팅팀, 영업팀, 운영팀 또한 마찬가지
    • 효율적인 소프트웨어 개발팀은 이러한 투쟁에서 정면으로 맞서 싸움
    • 소프트웨어를 안전하게 보호해야 할 책임이 있는 소프트웨어 개발자도 이해관계자
  • 소프트웨어 아키텍트의 역할
    • 시스템이 제공하는 특성이나 기능보다는 시스템의 구조에 더 중점을 둠
    • 이러한 특성과 기능을 개발하기 쉽고, 간편하게 수정할 수 있으며, 확장하기 쉬운 아키텍처를 만들어야 함
  • 아키텍처가 후순위가 된다면...
    • 시스템을 개발하는 비용이 더 많이 들고, 일부 또는 전체 시스템에 변경을 가하는 일이 현실적으로 불가능해짐
    • 이는 소프트웨어 개발팀이 스스로 옳다고 믿는 가치를 위해 충분히 투쟁하지 않았다는 뜻

+ Recent posts