클린코드 11장 시스템

kimjunkyung·2021년 8월 5일
1

클린코드

목록 보기
12/15
post-thumbnail

노션에서 정리한 내용을 벨로그로 옮겼기 때문에 노션으로 보면 조금 더 보기 더 편합니다🤗

이동하기 → junnkk's Notion


도시를 세운다면

도시 - 추상화모듈화 덕분에 큰 그림을 이해하지 못할지라도 개인이 관리하는 구성요소는 효율적으로 돌아감

⇒ 시스템 수준에서도 코드를 깨끗하게 유지하는 방법


### 시스템 제작과 시스템 사용을 분리하라
  • 제작 ≠ 사용
    소프트웨어 시스템은 준비 과정과 런타임 로직을 분리해야 한다

  • 시작 단계는 모든 어플리케이션이 풀어야 할 관심사이다.

    • 관심사 분리 (Seperation of concerns)

      : 소프트웨어 개발에서 가장 기본적인 원칙 중 하나로 소프트웨어 상에서 구조를 패턴, 역할, 기능 등을 각각 맞게 섹션 별로 분리해서 작성하는 것

      • 장점
      • 구현 종류
      • 실제 사례
      • 관심사의 분리
      • 관심사 분리에 사용되는 전략

      참고1

      참고2

    public Service getService() {
    	if(service == null)
    		service = new MyServiceImpl(...);
    	return service;
    }
  • 초기화 지연

    장점

    1. 필요할 때까지 객체 생성 X ∴ 부하 없음. 빨라짐.

    2. null 포인터 반환 X

      단점

    3. getService 메서드가 MyServiceImpl과 생성자 인수에 의존함.

      → 런타임 로직에서 MyServiceImpl 객체를 사용하지 않더라도 의존성 해결 없이는 컴파일이 안됨.

      1. 테스트 시 MyServiceImpldl이 무거운 객체라면 단일 책임 원칙 위배.
      2. 모든 상황에서 MyServiceImpl가 적합한지 모름
      3. 모듈성 ↓, 중복 多

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

    사용과 제작을 분리하는 3가지 방법 ⇒ main 분리, 팩토리, 의존성 주입

  • Main 분리

    : 생성과 관련된 모든 코드는 main이나 main이 호출하는 모듈로 옮기고 나머지 시스템은 모든 객체가 생성되었고 모든 의존성이 연결되었다고 가정.

    ⇒ 즉 어플리케이션은 객체가 생성되는 과정을 전혀 모른다. (적절히 생성되었다고 가정)

    → main 함수에서 시스템에 필요한 객체를 생성 후 이를 애플리케이션에 넘김.

    모든 의존성이 main → 애플리케이션

  • 팩토리

    객체 생성 시점을 애플리케이션이 결정할 필요가 있을 때 Abstract Factory 패턴을 사용하여 생성 코드를 감춘다

    ⇒ OrderingProcessing 애플리케이션은 객체 LineItem이 생성되는 구체적인 방법을 모른다

    • Abstract Factory 추상 팩토리

      : 구체적인 클래스에 의존하지 않고 서로 연관되거나 의존적인 객체들의 조합을 만드는 인터페이스를 제공하는 패턴

      • 관련성 있는 객체를 일관성 있는 방식으로 생성하는 경우에 유용

      • 생성 패턴의 하나

      • 수행하는 작업

        • AbstractFactory : 실제 팩토리 클래스의 공통 인터페이스

        • ConcreteFactory : 구체적인 팩토리 클래스로 AbstractFactory 클래스의 추상 메서드를 오버라이드함으로써 구체적인 제품을 생성한다.

        • AbstractProduct : 제품의 공통 인터페이스

        • ConcreteProduct : 구체적인 팩토리 클래스에서 생성되는 구체적인 제품

        참고1
        참고2

→ 모든 의존성이 main → orderProcessing 애플리케이션
  • 의존성 주입

    : 제어 역전 기법을 의존성 관리에 적용한 메커니즘

    • 제어 역전 기법

      : 한 객체가 맡은 보조 책임을 새로운 객체에게 전적으로 떠넘긴다.

      → 새로운 객체는 넘겨받은 책임만 맡으므로 SRP 만족시킴

    • 의존성 관리의 관점에서 객체는 의존성 자체를 인스턴스로 만드는 책임을 지지 않는다.(자신의 의존성을 직접 생성하지 않는다.) 대신 다른 전담 메커니즘에게 제어를 역전한다.

      MyService myService = (MyService)(jndiContext.lookup(“NameOfMyService”));

      → 코드를 호출하는 쪽에서는 실제로 lookup 메서드가 어떤 구현체를 리턴하는지 관여하지 않으면서 의존성을 해결할 수 있음.

    • 진정한 의존성 주입

      → 클래스가 의존성을 해결 X. 대신에 의존성을 주입하는 방법으로 설정자 메서드나 생성자 인수를 (혹은 둘 다) 제공

      • 배터리로 예시

        - 배터리의 일체형인 경우에는 생성자에서만 의존성을 주입해주는 상황이라 배터리가 떨어지게 된다면 다른 배터리로 교체하지 못하고 새로운 것으로 바꿔야 하기 때문에 유연하지 못한 방식.

        - setter, 생성자를 이용해서 외부에서 주입해주는 상황은 외부에서 배터리를 교체해줄 수 있기 때문에 일체형보다 유연한 상황.

    - DI(의존성 주입) 컨테이너는 필요한 객체의 인스턴스를 만든 후 생성자 인수나 설정자 메서드를 사용해 의존성 설정.

- 스프린 프레임워크는 자바 DI 컨테이너 제공

    → 객체 사이 의존성은 XML 파일에 정의

    → 자바 코드에서는 이름으로 특정한 객체를 요청

확장

TDD(테스트 주도 개발)과 리펙터링으로 얻어지는 깨끗한 코드는 코드 수준에서 시스템을 조정하고 확장하기 쉽게 만든다.

관심사를 적절히 분리해 관리한다면 소프트웨어 아키텍처는 발전할 수 있다.

  • 관심사를 적절히 분리하지 못하는 아키텍처 예

    → EJB1, EJB2

    • 예시)

      entity bean이란 관계 데이터(DB 테이블의 행)의 메모리상의 표현

      [11-1] Bank EJB용 EJB2 지역 인터페이스

      package com.example.banking;
      import java.util.Collections;
      import javax.ejb.*;
      
      public interface BankLocal extends java.ejb.EJBLocalObject {
          String getStreetAddr1() throws EJBException;
          String getStreetAddr2() throws EJBException;
          String getCity() throws EJBException;
          String getState() throws EJBException;
          String getZipCode() throws EJBException;
          void setStreetAddr1(String street1) throws EJBException;
          void setStreetAddr2(String street2) throws EJBException;
          void setCity(String city) throws EJBException;
          void setState(String state) throws EJBException;
          void setZipCode(String zip) throws EJBException;
          Collection getAccounts() throws EJBException;
          void setAccounts(Collection accounts) throws EJBException;
          void addAccount(AccountDTO accountDTO) throws EJBException;
      }

      [11-2] 상응하는 EJB2 엔티티 빈 구현

      package com.example.banking;
      import java.util.Collections;
      import javax.ejb.*;
      
      public abstract class Bank implements javax.ejb.EntityBean {
          // 비즈니스 논리...
          public abstract String getStreetAddr1();
          public abstract String getStreetAddr2();
          public abstract String getCity();
          public abstract String getState();
          public abstract String getZipCode();
          public abstract void setStreetAddr1(String street1);
          public abstract void setStreetAddr2(String street2);
          public abstract void setCity(String city);
          public abstract void setState(String state);
          public abstract void setZipCode(String zip);
          public abstract Collection getAccounts();
          public abstract void setAccounts(Collection accounts);
          
          public void addAccount(AccountDTO accountDTO) {
              InitialContext context = new InitialContext();
              AccountHomeLocal accountHome = context.lookup("AccountHomeLocal");
              AccountLocal account = accountHome.create(accountDTO);
              Collection accounts = getAccounts();
              accounts.add(account);
          }
          
          // EJB 컨테이너 논리
          public abstract void setId(Integer id);
          public abstract Integer getId();
          public Integer ejbCreate(Integer id) { ... }
          public void ejbPostCreate(Integer id) { ... }
          
          // 나머지도 구현해야 하지만 일반적으로 비어있다.
          public void setEntityContext(EntityContext ctx) {}
          public void unsetEntityContext() {}
          public void ejbActivate() {}
          public void ejbPassivate() {}
          public void ejbLoad() {}
          public void ejbStore() {}
          public void ejbRemove() {}
      }
    • 단점

      1. 비지니스 로직이 EJB2 컨테이너와 강하게 결합되어 있어, 클래스 생성 시 컨테이너에서 파생해야 하며 컨테이너의 다양한 생명주기 메서드를 제공해야 한다.
      2. 비지니스 로직이 덩치 큰 컨테이너와 밀접하게 결합되어 독자적인 단위 테스트가 어렵다.
      3. 객체 지향 프로그래밍의 개념이 무너진다. 빈은 다른 빈을 상속 받지 못한다. 일반적으로 EJB2 빈은 DTO(Data Transfer Object)를 정의한다. DTO는 메서드가 없는 사실상의 구조체이므로 동일한 정보를 저장하는 자료 유형이 두 개가 된다. 그래서 객체 간의 자료를 복사하는 반복적인 코드가 필요하다.
  • 횡단(cross-cutting) 관심사

    : (영속성과 같이) 다른 관심사에 영향을 미치는 aspect.

    ⇒ AOP(관점 지향 프로그래밍) 예견

    : 횡단 관심사항의 기능을 모듈화하여 중복을 최소화하면서, 핵심 관심사항에 집중하도록 하는 프로그래밍 기법

    • 모듈 구성 개념인 '관점' → 특정 관심사를 지원하려면 시스템에서 특정 지점들이 동작하는 방식을 일관성 있게 바궈야 한다


      자바에서 사용하는 관점 or 관점과 유사한 메커니즘 3가지

    1. 자바 프록시
    2. 순수 자바 AOP 프레임워크
    3. AspectJ 관점


자바 프록시

자바 프록시

  • 개별 객체나 클래스에서 메서드 호출을 감싸는 경우와 같은 단순한 상황에 적합.

  • 예시) p203~205 [11-3]

    // Bank.java (suppressing package names...)
    import java.utils.*;
    
    // The abstraction of a bank.
    public interface Bank {
        Collection<Account> getAccounts();
        void setAccounts(Collection<Account> accounts);
    }
    // BankImpl.java
    import java.utils.*;
    
    // The “Plain Old Java Object” (POJO) implementing the abstraction.
    public class BankImpl implements Bank {
        private List<Account> accounts;
    
        public Collection<Account> getAccounts() {
            return accounts;
        }
        
        public void setAccounts(Collection<Account> accounts) {
            this.accounts = new ArrayList<Account>();
            for (Account account: accounts) {
                this.accounts.add(account);
            }
        }
    }
    // BankProxyHandler.java
    import java.lang.reflect.*;
    import java.util.*;
    
    // “InvocationHandler” required by the proxy API.
    public class BankProxyHandler implements InvocationHandler {
        private Bank bank;
        
        public BankHandler (Bank bank) {
            this.bank = bank;
        }
        
        // Method defined in InvocationHandler
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            String methodName = method.getName();
            if (methodName.equals("getAccounts")) {
                bank.setAccounts(getAccountsFromDatabase());
                
                return bank.getAccounts();
            } else if (methodName.equals("setAccounts")) {
                bank.setAccounts((Collection<Account>) args[0]);
                setAccountsToDatabase(bank.getAccounts());
                
                return null;
            } else {
                ...
            }
        }
        
        // Lots of details here:
        protected Collection<Account> getAccountsFromDatabase() { ... }
        protected void setAccountsToDatabase(Collection<Account> accounts) { ... }
    }
    // Somewhere else...
    Bank bank = (Bank) Proxy.newProxyInstance(
        Bank.class.getClassLoader(),
        new Class[] { Bank.class },
        new BankProxyHandler(new BankImpl())
    );
    1. 프록시로 감쌀 인터페이스 Bank 작성

    2. 비즈니스 논리를 구현하는 POJO(Plain Old Java Object) BankImpl 정의

      • POJO
    3. InvocationHandler를 구현하는 BankProxyHandler를 작성

  • 단점

    → 코드의 양과 크기가 상당해져 깨끗한 코드를 쓰기 어렵다.

    → 프록시는 시스템 단위로 실행 '지점'을 명시하는 메커니즘은 제공 X


❓순수 자바 AOP 프레임워크

  • 대부분의 프록시 코드는 자동화가 가능. 여러 자바 프레임워크는 내부적으로 프록시 사용

  • 스프링은 비즈니스 논리를 POJO로 구현

    → POJO는 도메인에 초점을 맞춤. 엔터프라이즈 프레임워크에 의존 X

    ∴ 테스트가 개념적으로 더 쉽고 간단

  • 프로그래머는 설정 파일이나 API를 사용하여 필수적인 애플리케이션 기반 구조 표현

→ 클라이언트에서 bank의 getAccount를 호출한다고 믿지만 실제로는 Bank POJO의 기본 동작을 확장한 중첩 DECORATOR 객체 집합의 가장 외곽과 통신


AspectJ 관점

  • AspectJ 언어

    : 언어 차원에서 관점을 모듈화 구성으로 지원하는 자바 언어 확장

    • 관심사를 관점으로 분리하는 가장 강력한 도구 집합 제공

      but 새 도구를 사용하고 새 언어 문법과 사용법을 익혀야 함.

    • AspectJ Annotation ( @Aspect, @Before 등) - 쉽게 접근 가능하게 함.

⇒ 자세한 내용은 따로 공부


테스트 주도 시스템 아키텍처 구축

  • 애플리케이션 도메인 논리를 POJO로 작성한다면, 즉 코드 수준에서 아키텍쳐와 분리가 가능하다면 진정한 테스트 주도 아키텍처 구축이 가능하다
  • 필요에 따라 새로운 기술을 채택해 단순한 아키텍처를 복잡한 아키텍쳐로 키울 수 있다.
  • 소프트웨어 구조가 관점을 효과적으로 분리한다면 확장이 가능하다.

의사 결정을 최적화하라

큰 시스템에서는 한 사람이 모든 결정을 내릴 수는 없다.

따라서 결정은 최대한 많은 정보가 모일 때까지 미루고 시기가 되었을 경우 해당 파트의 책임자(여기서는 모듈화된 컴포넌트)에게 맡기는 것이 불필요한 고객 피드백과 고통을 덜어줄 것이다.


명백한 가치가 있을 때 표준을 현명하게 사용하라

표준을 사용하면 아이디어와 컴포넌트를 재사용하기 쉽고, 적절한 경험을 가지는 사람을 구하기 쉽고, 좋은 아이디어를 캡슐화하기 쉽고, 컴포넌트를 엮기 쉽지만 때로는 표준이 불필요함에도 사용하는 경우가 있다

⇒ 표준은 확실한 이득을 가져올 경우 사용하라


시스템은 도메인 특화 언어가 필요하다

  • DSL : 간단한 스크립트 언어나 표준 언어로 구현한 API.
  • 좋은 DSL → 도메인 개념과 그 개념을 구현한 코드 사이에 존재하는 의사소통 간극 ↓
  • DSL을 사용하면 모든 추상화 수준과 모든 도메인을 POJO로 표현 가능

결론

  • 깨끗하지 못한 아키텍처는 도메인 논리 흐리며 기민성을 떨어뜨린다. 기민성이 떨어지면 생산성이 낮아져 TDD가 제공하는 장점이 사라진다.

  • 모든 추상화 단계에서 의도는 명확해야 함.

    • POJO 작성

    • 관점 혹은 관점과 유사한 메커니즘을 사용해 각 구현 관심사를 분리



12장 창발성

profile
#Backend #Developer

0개의 댓글