클린 코드(Clean Code) - 11장 시스템

Muzi·2023년 1월 18일
0

Clean Code

목록 보기
11/14

복잡성은 죽음이다. 개발자에게서 생기를 앗아가며, 제품을 계획하고 제작하고 테스트하기 어렵게 만든다.

Intro

도시를 건설하고 관리하는 데에는 한 사람 만으로는 충분하지 않다. 그래도 도시는 돌아간다. 그것은 도시라는 거대한 덩어리를 수도, 전원, 교통 등의 모듈로 모듈화하고 관리되기 때문이다.

소프트웨어 또한 비슷한 방식으로 구성되기는 하나 도시의 모듈화 만큼의 추상화를 이루지 못하는 경우가 많다.

클린 코드는 이 것을 낮은 단계의 추상화를 통해 이루는 것을 도와준다. 이 장에서는 시스템 수준에서도 코드를 깨끗하게 유지하는 방법을 보자

1. 시스템 제작과 시스템 사용을 분리하라

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

1) 관심사 분리

소프트웨어 상에서 구조를 패턴, 역할, 기능 등을 각각 맞게 섹션 별로 분리해서 작성하는 것

  • 장점
    • 한 영역이 다른 영역과 격리되기 때문에 다른 영역에 영향을 주지 않고도 변경할 수 있다
    • 전체적인 유지보수가 용이해진다
    • 코드 중복이 줄어들어 코드 재사용률 높아진다

초기화 지연 혹은 계산 지연 방식의 한계

public Service getService() {
    if (service == null)
        service = new MyServiceImpl(...); // 모든 상황에 적합한 기본값일까?
    return service;
}
  • 장점
    • 필요할 때까지 객체 생성 x = 불필요한 부하 x, 어플리케이션 시작 시간 빨라짐
    • null 포인터 반환 x
  • 단점
    • getService 메서드가 MyServiceImpl과 생성자 인수에 의존 => MyServiceImpl 객체를 사용하지 않더라도 의존성 해결없이는 컴파일이 안된다
    • MyServiceImpl이 무거운 객체라면 단위 테스트에서 getService 메서드를 호출하기 위해 호출하기 적절한 테스트 전용 객체(Test double이나 Mock object)를 service 필드에 할당해야 한다
    • 일반 런타임 로직에 객체 생성 로직을 섞어놓아서 service가 null인 경로와 null이 아닌 경로 모두 테스트 해야 한다. 즉 메서드의 책임이 둘이기 때문에 단일책임원칙(SRP)를 깬다.

체계적이고 탄탄한 시스템을 만들고 싶다면 설정 논리는 일반 실행 논리와 분리해야 모듈성이 높아진다. 또한 주요 의존성을 해소하기 위한 방식, 즉 전반적이며 일관적인 방식도 필요하다.

2) Main 분리

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

즉 어플리케이션은 객체가 생성되는 과정을 전혀 모른다.

  • Main 함수에서 시스템에 필요한 객체를 생성 후 이를 애플리케이션으로 넘긴다

3) 팩토리

물론 때로는 객체가 생성되는 시점을 어플리케이션이 결정해야할 필요도 생긴다

예를 들어, 주문 처리 시스템에서 어플리케이션은 LineItem 인스턴스를 생성해 Order에 넘긴다. 이때는 Abstract Factory 패턴을 사용한다.

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

  • 여기서도 마찬가지로 모든 의존성이 main에서 OrderProcessing 어플리케이션으로 향한다. 즉, OrderProcessing 어플리케이션은 LineItem이 생성되는 구체적인 방법은 모른다.
  • 그 방법은 LineItemFactoryImplementation이 안다. 그럼에도 OrderProcessing 어플리케이션은 LineItem 인스턴스가 생성되는 시점을 완벽하게 통제하며, 필요하다면 OrderProcessing 어플리케이션에서 사용하는 생성자 인수도 넘길 수 있다

4) 의존성 주입

사용과 제작을 분리하는 강력한 매커니즘 중 하나가 의존성 주입(Dependency Injection) 이다. 의존성 주입은 제어 역전 (Inversion of Control) 기법을 의존성 관리에 적용한 메커니즘이다.

  • 제어 역전에서는 한 객체가 맡은 보조 책임을 새로운 객체에게 전적으로 떠넘긴다. 새로운 객체는 넘겨받은 책임만 맡으므로 단일 책임 원칙 (SRP)을 지키게 된다.
  • 초기 설정은 시스템 전체에서 필요하므로 대개 '책임질' 메커니즘으로 'main' 루틴이나 특수 컨테이너를 사용한다.

2. 확장

'처음부터 올바르게' 시스템을 만들 수 있다는 믿음은 미신이다. 대신에 우리는 오늘 주어진 사용자 스토리에 맞춰 시스템을 구현해야 한다. 내일은 새로운 스토리에 맞춰 시스템을 조정하고 확장하면 된다.

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

횡단(cross-cutting) 관심사

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

3. 자바에서 사용하는 Aspect or Aspect와 유사한 메커니즘 3가지

1) 자바 프록시

  • 단순한 상황에 적합하다
  • 개별 객체나 클래스에서 메서드 호출을 감싸는 경우가 좋은 예
  • 하지만 JDK에서 제공하는 동적 프록시는 인터페이스만 지원한다 (클래스 프록시 지원을 원하면 외부 바이트 코드 처리 라이브러리 사용)
    import java.utils.*;
 
    // 은행 추상화 
    public interface Bank {
        Collection<Account> getAccounts();
        void setAccounts(Collection<Account> accounts);
    }
    // BackInpl.java
    import java.utils.*;
 
    // 추상화를 위한 POJO("Plain Old Java Object") 구현
    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.*;
 
    // 프록시 API가 필요한 InvocationHandler 
    public class BankProxyHandler implements InvocationHandler {
        private Bank bank;
 
        public BankHandler (Bank bank) {
            this.bank = bank;
        }
 
        // 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 {
                ...
            }
        }
 
        // 세부사항은 여기에 이어진다.
        protected Collection<Account> getAccountsFromDatabase() { ... }
        protected void setAccountsToDatabase(Collection<Account> accounts) { ... }
    }
 
    // 다른 곳에 위치하는 코드
    Bank bank = (Bank) Proxy.newProxyInstance(
        Bank.class.getClassLoader(),
        new Class[] { Bank.class },
        new BankProxyHandler(new BankImpl())
    );
  • 프록시로 감쌀 Back Interface, 논리를 구현하는 POJO 정의
  • 프록시는 시스템 단위로 실행 '지점'을 명시하는 메커니즘도 제공하지 않는다
  • 코드의 양과 크기가 크다는 것이 프록시의 두 가지 단점

2) 순수 자바 AOP 프레임워크

  • 순수 자바 관점을 구현하는 Spring AOP 등과 같은 여러 자바 프레임워크는 내부적으로 프록시를 사용
  • 스프링은 비즈니스 논리를 POJO로 구현
  • POJO는 순수하게 도메인에 초점을 맞추어 다른 프레임워크에 의존하지 않아 테스트하기 쉽고 간단
  • 모든 정보가 어노테이션에 속에 있으므로 코드 자체는 깔끔하고 깨끗 => 테스트 개선 보수 쉬워짐
import javax.persistence.*;
import java.util.ArrayList;
import java.util.Collection;
 
@Entity
@Table(name = "BANKS")
public class Bank implements java.io.Serializable {
    @Id @GeneratedValue(strategy=GenerationType.AUTO)
    private int id;
 
    @Embeddable
    public class Address {
        protected String streetAddr1;
        protected String streetAddr2;
        protected String city;
        protected String state;
        protected String zipCode;
    }
 
    @Embedded
    private Address address;
    @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER, mappedBy="bank")
    private Collection<Account> accounts = new ArrayList<Account>();
    public int getId() {
        return id;
    }
 
    public void setId(int id) {
        this.id = id;
    }
 
    public void addAccount(Account account) {
        account.setBank(this);
        accounts.add(account);
    }
 
    public Collection<Account> getAccounts() {
        return accounts;
    }
 
    public void setAccounts(Collection<Account> accounts) {
        this.accounts = accounts;
    }
}

3) AspectJ 관점

  • 관심사를 관점으로 분리하는 가장 강력한 도구 AspectJ 언어
  • 언어 차원에서 관점을 모듈화 구성으로 지원하는 자바 언어 확장
  • AspectJ Annotation ( @Aspect, @Before 등) - 쉽게 접근 가능하게 함.

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

도메인 특화 언어 (DSL)을 사용하면 고차원 정책에서 저차원 세부사항에 이르기까지 모든 추상화 수준과 모든 도메인을 POJO로 표현할 수 있다.

4. 결론

  • 깨끗하지 못한 시스템 아키텍처는 도메인 논리를 흐리며 기민성을 떨어뜨린다
  • 기민성이 떨어지면 생산성이 낮아져 TDD가 제공하는 장점이 사라진다.
  • 모든 추상화 단계에서 의도는 명확히 표현해야 한다. 그러려면 POJO를 작성하고 관점 혹은 관점과 유사한 메커니즘을 사용해 각 구현 관심사르 분리해야 한다
profile
좋아하는걸 열심히

0개의 댓글