우당탕탕 우리네 개발생활

[클린코드] 11장 정리(시스템) 본문

tech

[클린코드] 11장 정리(시스템)

미스터카멜레온 2024. 6. 8. 22:45
<클린코드> 책을 통해 공부한 내용을 정리하기 위해 작성하였습니다. 제 개인적인 각색과 의견이 첨가되어 있어 실제 책의 내용과는 차이가 있을 수 있습니다.

11장. 시스템

- 시스템 제작과 시스템 사용을 분리하라. 소프트웨어 시스템은 (애플리케이션 객체를 제작하고 의존성을 서로 '연결'하는) 준비 과정과 (준비 과정 이후에 이어지는) 런타임 로직을 분리해야 한다.

 

public getService(): Service {
    if (service === null) {
        service = new MyServiceImpl(...); // 모든 상황에 적합한 기본값일까?
    }
    return service;
}

- 위 방법은 초기화 지연(Lazy Initialization) 혹은 계산 지연(Lazy Evaluation)이라는 기법이다. 장점은 아래와 같다.

  • 실제로 필요할 때까지 객체를 생성하지 않으므로 불필요한 부하가 걸리지 않는다.
  • 어떤 경우에도 null 포인터를 반환하지 않는다.

- getService 메서드가 MyServiceImpl과 (위에서는 생략한) 생성자 인수에 명시적으로 의존한다. 런타임 로직에서 MyServiceImpl 객체를 전혀 사용하지 않더라도 의존성을 해결하지 않으면 컴파일이 안 된다.

 

- 설정 논리는 일반 실행 논리와 분리해야 모듈성이 높아진다.

 

- Main 분리. (이해하기 쉽도록 main을 controller로 애플리케이션을 service로 대체했다.) 제어 흐름은 따라가기 쉽다. Controller 내 handler에서 시스템에 필요한 객체를 생성한 후 이를 service에 넘긴다. service는 그저 객체를 사용할 뿐이다. controller의 handler와 service 사이에 표시된 의존성 화살표의 방향에 주목한다. 모든 화살표가 controller 쪽에서 service 쪽을 향한다. 즉, service는 controller의 존재도 객체가 생성되는 과정도 전혀 모른다는 뜻이다. 단지 모든 객체가 적절히 생성되었다고 가정한다.

 

- ABSTRACT FACTORY 패턴. 위에서는 Controller에서 직접 객체를 생성한 후 인수를 통해 service에 객체를 넘겼다면 이제는 service 내에서 직접 객체를 컨트롤해본다. 이 때 service에 factory를 주입받아 factory를 통해 객체를 생성하게 되면 service는 객체의 생성로직을 모르면서 그저 원하는 비즈니스 단계에서 factory를 통해 객체를 받아 사용할 수 있다.

-> (내 생각) 즉, factory를 주입받아서 factory에게 생성 책임을 위임한 것이다.

 

- 의존성 주입. 사용과 제작을 분리하는 강력한 메커니즘 하나가 의존성 주입(Dependency Injection, DI)이다. 의존성 주입은 제어 역전(Inversion of Control, IoC)기법을 의존성 관리에 적용한 메커니즘이다. 제어 역전에서는 한 객체가 맡은 보조 책임을 새로운 객체에게 전적으로 떠넘긴다. 새로운 객체는 넘겨받은 책임만 맡으므로 단일 책임 원칙(SRP)을 지키게 된다. 의존성 관리 맥락에서는 객체는 의존성 자체를 인스턴스로 만드는 책임은 지지 않는다. 대신에 이런 책임을 다른 '전담' 메커니즘에 넘겨야만 한다. 그렇게 함으로써 제어를 역전한다.

-> (내 생각) 제어 역전의 핵심은 결국 생성 책임에 있는 듯 하다. Controller에서 Service를 생성자를 통해 생성하면 Service에 대한 제어권을 전부 갖게 된다. 하지만 Service에 대한 생성 책임을 IoC Container나 Factory등에 넘겨주게 되면 적어도 생성 제어권이 다른방향으로 역전되게 된다. 이렇게 제어 역전을 이해하는 것이 좋은 것 같다.

 

- 클래스는 의존성을 주입하는 방법으로 설정자(setter) 메서드나 생성자 인수를 (혹은 둘 다를) 제공한다. DI 컨테이너는 (대개 요청이 들어올 때마다) 필요한 객체의 인스턴스를 만든 후 생성자 인수나 설정자 메서드를 사용해 의존성을 설정한다. 실제로 생성되는 객체 유형은 설정 파일에서 지정하거나 특수 생성 모듈에서 코드로 명시한다.

-> (내 생각) Nestjs의 DI 컨테이너는 Initialize 과정에서 전부 인스턴스화 시켜두지 않나?

 

- '처음부터 올바르게' 시스템을 만들 수 있다는 믿음은 미신이다. 대신에 우리는 오늘 주어진 사용자 스토리에 맞춰 시스템을 구현해야 한다. 내일은 새로운 스토리에 맞춰 시스템을 조정하고 확정하면 된다. 이것이 반복적이고 점진적인 애자일 방식의 핵심이다. 테스트 주도 개발(Test-driven Development, TDD), 리팩터링, (TDD와 리팩터링으로 이어지는) 깨끗한 코드는 코드 수준에서 시스템을 조정하고 확장하기 쉽게 만든다.

 

- 인스턴스 지연/계산 지연은 최적화 기법 중 하나일 뿐이며, 섣부른 최적화일지도 모른다는 사실을 명심한다.

 

- 횡단(cross-cutting) 관심사. 영속성과 같은 관심사는 애플리케이션의 자연스러운 객체 경계를 넘나드는 경항이 있다. 모든 객체가 전반적으로 동일한 방식을 이용하게 만들어야 한다. 예를 들어, 특정 DBMS나 독자적인 파일을 사용하고, 테이블과 열은 같은 명명 관례를 따르며, 트랜잭션 의미가 일관적이면 더욱 바람직하다.

 

- 원론적으로는 모듈화되고 캡슐화된 방식으로 영속성 방식을 구상할 수 있다. 하지만 현실적으로는 영속성 방식을 구현한 코드가 온갖 객체로 흩어진다. 여기서 횡단 관심사라는 용어가 나온다. 영속성 프레임워크 또한 모듈화할 수 있다.

 

- POJO(Plain Old Java Object)는 순수하게 도메인에 초점을 맞춘다. POJO는 엔터프라이즈 프레임워크에 (그리고 다른 도메인에도) 의존하지 않는다. 따라서 테스트가 개념적으로 더 쉽고 간단하다. 상대적으로 단순하기 때문에 사용자 스토리를 올바로 구현하기 쉬우며 미래 스토리에 맞춰 코드를 보수하고 개선하기 편하다.

 

- (내 생각) 데코레이터 패턴은 러시아의 마트로시카 인형과 유사하다. 가장 작은 인형을 겹겹이 싸고 있는 수많은 인형들이 존재하고 그렇게 한 덩어리를 구성하게 된다. 객체 A와 객체 B가 있다고 하자. 객체 A는 데코레이터가 없는 POJO이고, 객체 B는 3개 정도의 데코레이터가 붙어 있는 POJO이다. 이렇게 될 경우 객체 A는 항상 객체 B와 직접 소통하고 있다고 생각하지만 때론 데코레이터 중 일부와 소통하고 있는 경우일 수도 있을 것이다. Core는 POJO이면서 데코레이터들이 겉을 둘러쌀 경우 의존성이 느슨해지는 효과로 인해 언제든 탈부착이 가능하다.

 

- 일부 상세한 엔티티 정보는 애너테이션에 포함되어 그대로 남아있지만, 모든 정보가 애너테이션 속에 있으므로 코드 자체는 깔끔하고 깨끗하다. 즉, 그만큼 코드를 테스트하고 개선하고 보수하기가 쉬워졌다.

-> (내 생각) 애너테이션과 데코레이터는 명칭만 다른가? 만들어진 히스토리가 어떻게 다른 지 확인이 필요하다

 

테스트 주도 시스템 아키텍처 구축. 애플리케이션 도메인 논리를 POJO로 작성할 수 있다면, 즉 코드 수준에서 아키텍처 관심사를 분리할 수 있다면, 진정한 테스트 주도 아키텍처 구축이 가능해진다. 그때그때 새로운 기술을 채택해 단순한 아키텍처를 복잡한 아키텍처로 키워갈 수도 있다. BDUF(Big Design Up Front)를 추구할 필요가 없다. 실제로 BDUF는 해롭기까지 하다. 처음에 쏟아 부은 노력을 버리지 않으려는 심리적 저항으로 인해, 그리고 처음 선택한 아키텍처가 향후 사고 방식에 미치는 영향으로 인해, 변경을 쉽사리 수용하지 못하는 탓이다.

 

- 설계가 최대한 분리되어 각 추상화 수준과 범위에서 코드가 적당히 단순화 되어야 한다.

 

- 최선의 시스템 구조는 각기 POJO (또는 다른) 객체로 구현되는 모듈화된 관심사 영역(도메인)으로 구성된다. 이렇게 서로 다른 영역은 해당 영역 코드에 최소한의 영향을 미치는 관점이나 유사한 도구를 사용해 통합한다. 이런 구조 역시 코드와 마찬가지로 테스트 주도 기법을 적용할 수 있다.

 

- 의사 결정을 최적화하라. 모듈을 나누고 관심사를 분리하면 지엽적인 관리와 결정이 가능해진다.

-> (내 생각) 멤버 단위로 데코레이터를 설정하는 등 지엽적 관리가 가능?

 

- 명백한 가치가 있을 때 표준을 현명하게 사용하라. 나는 업계에서 여러 형태로 아주 과장되게 포장된 표준에 집착하는 바람에 고객 가치가 뒷전으로 밀려난 사례를 많이 봤다. 표준을 사용하면 아이디어와 컴포넌트를 재사용하기 쉽고, 적절한 경험을 가진 사람을 구하기 쉬우며, 좋은 아이디어를 캡슐화하기 쉽고, 컴포넌트를 엮기 쉽다. 하지만 때로는 표준을 만드는 시간이 너무 오래 걸려 업계가 기다리지 못한다. 어떤 표준은 원래 표준을 제정한 목적을 잊어버리기도 한다.

 

- 결론. 시스템 역시 깨끗해야 한다. 깨끗하지 못한 아키텍처는 도메인 논리를 흐리며 기민성을 떨어뜨린다. 도메인 논리가 흐려지면 제품 품질이 떨어진다. 버그가 숨어들기 쉬워지고, 스토리를 구현하기 어려워지는 탓이다. 기민성이 떨어지면 생산성이 낮아져 TDD가 제공하는 장점이 사라진다. 모든 추상화 단계에서 의도는 명확히 표현해야 한다. 그러려면 POJO를 작성하고 관점 혹은 관점과 유사항 메커니즘을 사용해 각 구현 관심사를 분리해야 한다. 시스템을 설계하든 개별 모듈을 설계하든, 실제로 돌아가는 가장 단순한 수단을 사용해야 한다는 사실을 명심하자.