[클린코드 완독스터디] TIL (2022.02.15)

yourjin·2022년 2월 27일
0

read.log

목록 보기
25/37
post-thumbnail

TIL (2022.02.15)

DAY 2

🔖 오늘 읽은 범위 : 11장, 시스템


😃 책에서 기억하고 싶은 내용을 써보세요.

“복잡성은 죽음이다. 개발자에게서 생기를 앗아가며, 제품을 계획하고 제작하고 테스트하기 어렵게 만든다.”
- 레이 오지(Ray Ozzie), 마이크로소프트 최고 기술 책임자(CTO)*

  • 도시를 세운다면?
    • 여러분이 도시를 세운다면? 온갖 세세한 사항을 혼자서 직접 관리할 수 있을까? 아마도 불가능하리라.
      • 그럼에도 불구하고 (일상적으로) 도시는 잘 돌아간다. 왜? 수도 관리팀, 전력 관리 팀, 교통 관리 팀, 치안 관리 팀, 건축물 관리 팀 등 각 분야를 관리하는 이 있기 때문이다.
      • 도시가 돌아가는 또 다른 이유는 적절한 추상화모듈화 때문이다. 그래서 큰 그림을 이해하지 못할지라도 개인과 개인이 관리하는 ‘구성요소’는 효율적으로 돌아간다.
    • 흔히 소프트웨어 팀도 도시처럼 구성한다. 그런데 막상 팀이 제작하는 시스템은 비슷한 수준으로 관심사를 분리하거나 추상화를 이뤄내지 못한다.
    • 이 장에서는 높은 추상화 수준, 즉 시스템 수준에서도 깨끗함을 유지하는 방법을 살펴본다.

      여기서 말하는 시스템 수준이란 ‘전체적인 구조/아키텍처’를 의미하는 것 같다. 처음에는 시스템이 어떤 의미인지 감이 잘 안왔는데, 후에 “깨끗한 코드는 코드 수준에서 시스템을 조장하고 확장하기 쉽게 만든다. (p.199)” 라는 말에서 그와 반대되는 개념이라는 생각이 들었다.

  • 시스템 제작과 시스템 사용을 분리하라
    • 우선 제작(construction)은 사용(use)과 아주 다르다는 사실을 명심한다.
    • 소프트웨어 시스템은 (애플리케이션 객체를 제작하고 의존성을 서로 ‘연결’하는) 준비 과정과 (준비 과정 이후에 이어지는) 런타임 로직을 분리해야 한다.
    • 시작 단계는 모든 애플리케이션이 풀어야 할 관심사(concern)다. (…) 관심사 분리는 우리 분야에서 가장 중요한 설계 기법 중 하나다.
    • 관심사를 분리하지 못한 예시
      public service getService() {
      	if (service = null)
      		service = new MyServiceimpl(...); // 모든 상황에 적합한 기본값일까?
      	return service;
      }
      • 이것이 초기화 지연(Lazy Initialization)혹은 계산 지연(Lazy Evaluation)이라는 기법이다.
      • 장점
        • 실제로 필요할 때까지 객체를 생성하지 않으므로 불필요한 부하가 걸리지 않는다. 따라서 애플리케이션을 시작하는 시간이 빨라진다.
        • 어떤 경우에도 null 포인터를 반환하지 않는다.
      • 단점
        • 하지만 getService 메서드가 MyServiceImpl과 (위에서는 생략한) 생성자 인수에 명시적으로 의존한다.
        • 테스트도 문제다. MyServiceImpl이 무거운 객체라면 단위 테스트에서 getService 메서드를 호출하기 전에 적절한 테스트 전용 객체(TEST DOUBLE이나 MOCK OBJECT)를 service 필드에 할당해야 한다.
        • 또한 일반 런타임 로직에다 객체 생성 로직을 섞어놓은 탓에 (service가 null인 경로와 null이 아닌 경로 등) 모든 실행 경로도 테스트해야 한다. → 단일 책임 원칙(Single Responsibility Principle, SRP)를 깬다!
        • 무엇보다 MyServiceImpl이 모든 상황에 적합한 객체인지 모른다는 사실이 가장 큰 우려다.
      • 초기화 지연 기법을 한 번 정도 사용한다면 별로 심각한 문제가 아니다. 하지만 많은 애플리케이션이 이처럼 좀스러운 설정 기법을 수시로 사용한다.
    • 체계적이고 탄탄한 시스템을 만들고 싶다면 흔히 쓰는 좀스럽고 손쉬운 기법으로 모듈성을 깨서는 절대로 안 된다.
    • 설정 논리는 일반 실행 논리와 분리해야 모듈성이 높아진다. 또한 주요 의존성을 해소하기 위한 방식, 즉 전반적이며 일관적인 방식도 필요하다.
    • Main 분리
      • 시스템 생성과 시스템 사용을 분리하는 한 가지 방법으로, 생성과 관련된 코드는 모두 main이나 main이 호출하는 모듈로 옮기고, 나머지 시스템은 모든 객체가 생성되었고 모든 의존성이 연결되었다고 가정한다.
      • main 함수에서 시스템에 필요한 객체를 생성한 후 이를 애플리케이션에 넘긴다. 애플리케이션은 그저 객체를 사용할 뿐이다.
      • 즉, 애플리케이션은 main이나 객체가 생성되는 과정을 전혀 모른다는 뜻이다.
    • 팩토리
      • 물론 때로는 객체가 생성되는 시점을 애플리케이션이 결정할 필요도 생긴다.
      • 예를 들어, 주문처리 시스템에서 애플리케이션은 LineItem 인스턴스를 생성해 Order에 추가한다. 이때는 ABSTRACT FACTORY 패턴을 사용한다. 그러면 LineItem을 생성하는 시점은 애플리케이션이 결정하지만 LineItem을 생성하는 코드는 애플리케이션이 모른다.
    • 의존성 주입
      • 사용과 제작을 분리하는 강력한 매커니즘 하나가 의존성 주입(Dependency Injection)이다.
      • 의존성 주입은 제어 역전(Inversion of Control, IoC)기법을 의존성 관리에 적용한 메커니즘이다.
      • 제어 역전에서는 한 객체가 맡은 보조 책임을 새로운 객체에게 전적으로 떠넘긴다. 새로운 객체는 넘겨받은 책임만 맡으므로 단일 책임 원칙(Single Responsibility Principle, SRP)을 지키게 된다.
      • 의존성 관리 맥락에서 객체는 의존성 자체를 인스턴스로 만드는 책임은 지지 않는다. 대신에 이런 책임을 다른 ‘전담’ 매커니즘에 넘겨야만 한다. 그렇게 함으로써 제어를 역전한다. 대게 ‘책임질’ 매커니즘으로 ‘main’ 루틴이나 특수 컨테이너를 사용한다.
        • DI 컨테이너는 (대개 요청이 들어올 때마다) 필요한 객체의 인스턴스를 만든 후 생성자 인수나 설정자 메서드를 사용해 의존성을 설정한다.
    • 그렇다면 초기화 지연으로 얻는 장점은 포기해야 하는 걸까?
      • 먼저 대다수 DI 컨테이너는 필요할 때까지는 객체를 생성하지 않고, 대부분은 계산 지연이나 비슷한 최적화에 쓸 수 있도록 팩토리를 호출하거나 프록시를 생성하는 방법을 제공한다.
  • 확장
    • ‘확장’ 공사로 꽉 막힌 도로에서 “왜 처음부터 넓게 만들지 않았지?”라고 자문한 적이 얼마나 많던가?I
    • 하지만 다른 방식으로는 확장이 일어나기 어렵다. 성장할지 모른다는 기대로 자그만 마을에 6차선을 뚫는데 들어가는 비용을 정당화할 수 있을까? 아니, 어느 조그만 마을이 6차선을 반길까?
    • ‘처음부터 올바르게’ 시스템을 만들 수 있다는 믿음은 미신이다. 대신에 우리는 오늘 주어진 사용자 스토리에 맞춰 시스템을 구현해야 한다. 내일은 새로운 스토리에 맞춰 시스템을 조정하고 확장하면 된다.
    • 소프트웨어 시스템은 물리적인 시스템과 다르다. 관심사를 적절히 분리해 관리한다면 소프트웨어 아키텍처는 점진적으로 발전할 수 있다.
    • 소프트웨어 시스템은 ‘수명이 짧다’는 본질로 인해 아키텍처의 점진적인 발전이 가능하다.
    • 횡단(cross-cutting) 관심사
      • 영속성과 같은 관심사애플리케이션의 자연스러운 객체 경계를 넘나드는 경향이 있다. 모든 객체가 전반적으로 동일한 방식을 이용하게 만들어야 한다.

        • 예를 들어, 특정 DBMS나 독자적인 파일을 사용하고, 테이블과 열은 같은 명명 관례를 따르며 트랜잭션 의미가 일관적이면 더욱 바람직하다.
      • 원론적으로는 모듈화되고 캡슐화된 방식으로 영속성 방식을 구상할 수 있다. 하지만 현실적으로는 영속성 방식을 구현한 코드가 온갖 객체로 흩어진다. 여기서 횡단 관심사라는 용어가 나온다.

        한마디로, 횡단 관심사란 애플리케이션의 객체 경계를 넘나드는, 모든 객체가 전반적으로 동일한 방식을 이용해야하는 공통의 관심사를 의미한다.

      • AOP에서 관점(aspect)이라는 모듈 구성 개념은 “특정 관심사를 지원하려면 시스템에서 특정 지점들이 동작하는 방식을 일관성 있게 바꿔야 한다”라고 명시한다.

🤔 오늘 읽은 소감은? 떠오르는 생각을 가볍게 적어보세요

  • 내용이 점점 자바와 스프링에 딥하게 다가가면서 용어부터 막히기 시작했다. 그러다 보니 이번 TIL은 거의 모든 문장을 정리한 것 같다. 다행히 현재 스프링을 사용하고 있기 때문에, 이미 알고 있는 부분들도 있었다. 하지만 깊게 들어가면 어려워져서 이 부분은 날을 잡고 다시 한번 읽어보면 좋을 것 같다. 스프링 개발자로서 한번쯤은 고민해봤고, 그래서 한번쯤은 짚고 넘어가야 할 부분이 바로 아키텍처(구조) 설계이기 때문이다.

🔎 궁금한 내용이 있거나, 잘 이해되지 않는 내용이 있다면 적어보세요.

  • 초기화 지연(Lazy Initialization)혹은 계산 지연(Lazy Evaluation)
    • 필드의 초기화 시점을 그 값이 처음 필요할 때까지 늦추는 기법
    • 클래스 혹은 인스턴스 생성 시의 초기화 비용은 줄지만, 지연 초기화하는 필드에 접근하는 비용은 커진다.
    • 해당 클래스의 인스턴스 중 그 필드를 사용하는 인스턴스의 비율이 낮은 반면,필드를 초기화하는 비용이 크다면 지연 초기화가 제 역할을 할 것이다.
    • 용도 및 효과
      • 지연 초기화는 최적화 용도로 쓰인다.
      • 클래스와 인스턴스 초기화 때 발생하는 위험한 순환 문제를 해결하는 효과도 있다.
    • 적용 대상
      • 정적 필드(static) : 지연 초기화 홀더 클래스 관용구(lazy initialization holder class)
             class Example {
                 private static class FieldHolder {
                     static final FieldType field = computeFieldValue();
                 }
             
                 private static FieldType getField() {
                     return FieldHolder.field;
                 }
             }
         
      • “클래스가 처음 쓰일 때 비로소 초기화 된다”는 특성을 이용한 관용구
      • getField가 처음 호출되는 순간 FieldHolder.field가 처음 읽히면서, 비로소 FieldHolder 클래스 초기화를 촉발한다.
      • 인스턴스 필드 : 이중 검사 관용구(double-check)
        class Example {
            private volatile FieldType field;
        
            private FieldType getField() {
                FieldType result = field; // 초기화 시 한 번만 읽도록 하기 위함
                if (result != null) {
                    return result;
                }
        
                synchronized (this) {
                    if (field == null) { // 두 번째 검사 (락 사용)
                        field = computeFieldValue();
                    }
                    return field;
                }
            }
        }
        • 이 방법은 초기화된 필드에 접근할 때의 동기화 비용을 없애준다.
        • 필드의 값을 두 번 검사하는 방식
          • 한 번은 동기화 없이 검사, 두 번째는 동기화 하여 검사
          • 두 번째 검사에서도 필드가 초기화 되지 않았을 때만 필드를 초기화 한다.
      • cf. 일반적인 초기화
        class Example {
            // final 한정자를 통한 인스턴스 필드 생성
            private final FieldType field = computeFieldValue();
        }
    • 참고 링크: Item 83 - 지연 초기화는 신중히 사용하라
  • 제어 역전(Inversion of Control, IoC)
    • 오브젝트 전반에 걸친 모든 제어권을 애플리케이션이 아닌, 프레임워크의 컨테이너에게 넘기는 개념을 말한다. (참고로 스프링 프레임워크 만의 개념이 아니다.)
    • IoC가 필요한 이유?
      • 객체지향 원칙을 잘 지키기 위해!
      • 객체를 관리해주는 프레임워크와 내가 구현 하고자 하는 부분으로 역할과 관심을 분리응집도를 높이고 결합도를 낮추며, 이에 따라 변경에 유연한 코드를 작성할 수 있는 구조가 된다.
    • DI(Dependency Injection, 의존관계 주입)

      스프링에선 IoC라는 용어만 가지고는 개념이 너무 추상적이라 그 핵심을 짚는 용어가 필요했는데, 이때 몇몇 사람들의 제안으로 만든 용어가 바로 DI인 것이다.

      • 외부로부터 메모리에 올라가 있는 인스턴스의 레퍼런스를 인터페이스 타입의 파라미터로 의존 관계를 설정하는 것 → 의존 관계에 있는 인스턴스를 넣어주는 것!
    • 참고 링크: IoC(Inversion of Control, 제어의 역전) / DI(Dependency Injection, 의존관계 주입)
  • 영속성 - “영구히 저장되는 그 어떤 것”
  • DongHyo 님이 공유해주신 GoF 디자인 패턴!

소감 3줄 요약

  • 시스템 제작과 시스템 사용을 분리하라.
  • 설정 논리는 일반 실행 논리와 분리해야 모듈성이 높아진다
  • 소프트웨어 시스템은 관심사를 적절히 분리해 관리한다면 소프트웨어 아키텍처는 점진적으로 발전할 수 있다.
profile
make it mine, make it yours

0개의 댓글