CleanCode - Chapter 11. System
책/CleanCode

CleanCode - Chapter 11. System

728x90
반응형

서론 :  도시 만들기 

거대한 도시가 돌아가는 이유 : 각각의 전원, 교통 등이 모듈로 모듈화되어 관리 / 일정 수준의 추상화를 통해 큰 도시 전체에 대한 이해 없이 개인과 개인이 관리하는 구성요소를 통해 효율적으로 동작 

 

SW = 하나의 도시 - 하나의 도시의 모듈화만큼의 추상화를 이루지 못하는 것이 클린코드답지 못한 문제 

 

낮은 단계의 추상화를 통해 관심사(Concern)을 분리하고
높은 추상화 수준(시스템 수준)에서도 클린 코드를 유지해보자

 

# 시스템의 생성(제작)과 사용을 분리 

  public Service getService() {
      if (service == null)
          service = new MyServiceImpl(...); // Good enough default for most cases?
      return service;
  }
  • Lazy Initialization 의 일반적인 형태 
    • 이점 : 불필요한 초기화 코스트의 최적화 / null 반환 방지 등 
    • 단점 :
      • 시스템 전반적으로 MyServiceImpl 객체 사용여부에 관계 없이  의존성 추가 
      • 테스트 수행에서도 해당 객체가 무겁다면 테스트를 위해 Test Double / Mock Object를 service field에 대입 
        • 이는 runtime 로직에 침투하는 문제 -> 모든 가능한 경우의 수를 고려해야함 

 

Test Double : 테스트를 진행하기 어려운 경우, 이를 대신해 테스트를 진행할 수 있도록 만들어주는 객체 
Mock Object : Test Double 중 하나,  객체 지향 프로그래밍으로 개발한 프로그램을 테스트 할 경우 테스트를 수행할 모듈과 연결되는 외부의 다른 서비스나 모듈들을 실제 사용하는 모듈을 사용하지 않고 실제의 모듈을 "흉내"내는 "가짜" 모듈을 작성하여 테스트의 효용성을 높이는데 사용하는 객체

 

 

  • 이러한 생성/사용의 혼용은 모듈성 저해, 코드의 중복 증가 
  • 잘 정돈된 견고한 시스템을 만들기 위해서 전역적이고, 일관된 의존성 해결 방법을 통해 위와 같은 작은 편의 코드들의 저해를 가져오는 것을 막아야한다. 

 

 

생성 로직은 어플리케이션 시작이 아닌 메인에 

이를 통해 어플리케이션에서 사용할 모든 객체들이 main에서 잘 생성되었을 것이라 가정 후 전체적인 디자인, 시스템에 집중할 수 있다. 

 

Factory Pattern 

객체의 생성 시기를 직접 결정하기 위해서는 main에서 완성된 객체를 던지는 것이 아닌, factory 객체를 만들어 전달하자 

이 때, 자세한 구현을 숨기고자 한다면 Abstract Factoy Pattern (추상화팩토리패턴)을 사용하자 

LineItem의 생성 시점은 OrderProcessing은 모르고 LineItemFactory가 안다. 

Dependency Injection(의존성 주입) 

- 사용과 제작을 분리하는 대표적인 메커니즘 

DI :
- 어떤 객체가 사용하는 의존 객체를 직접 만들어 사용하는 것이 아닌, 외부에서 주입 받아 사용하는 방법 
- IOC(Inversion Of Control) 기법을 의존성 관리에 적용한 메커니즘 
  • 특정 객체가 내부에서 사용할 객체를 만드는 책임을 제거해주고, 오로지 넘겨받아 사용하는 책임만 지게 된다 (SRP O) 
class Car{
    Tire tire;
    
    public Car(){
        this.tire = new Tire();
    }
}

================================================

class Car{
    Tire tire;
    
    public Car(Tire tire){
        this.tire = tire;
    }
    
    public void setTire(Tire tire){
        this.tire = tire;
    }
}

 

확장 

SW 시스템은 확장하기 마련이고, 언제나 확장성을 생각해야한다. 

  • '처음부터 올바르게' 시스템을 만들 수 있다 ≠ 정답 
  • 우린 오늘의 주어진 사용자 스토리(유스케이스)에 맞추어 시스템 구현하면 OK ,
    내일은 또 하나의 새로운 스토리에 조정 및 확장 = Agile 
  • BUT 확장성을 생각하여 구현하면 참 좋다 
    • TDD, 리팩터링 등을 통해 꺠끗한 코드로 코드 수준에서 시스템을 조정하고 확장하기 쉽게 만들어 줄 것이다.
    • SW 시스템은 물리적 시스템과 다르기 때문에, 관심사를 적절히 분리하여 전체적인 아키텍쳐를 점진적으로 발전시킬 수 있다. 

횡단 (Cross-Cutting) 관심사

  • 원론적으로 모듈화되고 캡슐화된 방식으로 영속성 방식을 구상할 수 있지만 현실적으로는 영속성 방식을 구현한 코드는 온갖 객체로 흩어지게 된다. 
  • AOP에서 특정 관점 (Aspect)라는 모듈 구성 개념은 "특정 관심사를 지원하려면 특정 지점에서 동작하는 방식을 일관성있게 해주어야 한다."
    • ex ) 프로그래머 - 영속성으로 저장할 객체와 속성 선언 -> 영속성 책임은 이제 영속성 프레임워크에게 위임
      ->  AOP 프레임워크는 대상 코드에 영향을 미치지 않는 상태로 동작 방식 변경 

AOP - 흩어진 관심사 

@Before (이전) : 어드바이스 타겟 메소드가 호출되기 전에 어드바이스 기능을 수행
@After (이후) : 타겟 메소드의 결과에 관계없이(즉 성공, 예외 관계없이) 타겟 메소드가 완료 되면 어드바이스 기능을 수행
@AfterReturning (정상적 반환 이후)타겟 메소드가 성공적으로 결과값을 반환 후에 어드바이스 기능을 수행
@AfterThrowing (예외 발생 이후) : 타겟 메소드가 수행 중 예외를 던지게 되면 어드바이스 기능을 수행
@Around (메소드 실행 전후) : 어드바이스가 타겟 메소드를 감싸서 타겟 메소드 호출전과 후에 어드바이스 기능을 수행

 

자바 내부에서 Aspect Or Aspect 와 유사한 메커니즘 3가지 

#1. Java Proxy

  • 단순한 상황에 적합, 개별 객체 및 클래스에서 메서드 호출을 감싸는 경우 
  • JDK에서 제공하는 동적 프록시는 인터페이스만 지원(클래스 프록시 지원 - 외부 바이트 코드 처리 라이브러리) 
  • 프록시로 감쌀 Back Interface (ex-Bank) , 논리를 구현하는 POJO(Plain-Old Java Object) (ex-BankImpl) 정의 
  • 코드의 양, 크기 너무 크다 
  • 프록시는 시스템 단위로 실행 '지점'을 명시하는 메커니즘 제공 X 

<프록시 예제 - Back Application에서 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()) 
     );
POJO란,
객체 지향적인 원리에 충실하면서 환경과 기술에 종속되지 않고 필요에 따라 재활용될 수 있는 방식으로 설계된 오브젝트를 말한다.

#2. 순수 자바 AOP 프레임워크 

  • Spring AOP 등과 같은 여러 자바 프레임워크는 내부적으로 프록시 사용 
    • 대부분의 프록시 코드는 판박이라 도구로 자동화 가능 
  • Spring은 비즈니스 논리를 POJO로 구현 
  • POJO는 순수하게 도메인에 초점을 맞추어 다른 프레임워크에 의존하지 않고 테스트하기 쉽다. 

<ComponentScan - Bean 방식> 

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; }
 }

<Xml-Configuration Bean 방식> 

/* Code 3-2(Listing 11-4): Spring 2.X configuration file */

<beans>
    ...
    <bean id="appDataSource"
        class="org.apache.commons.dbcp.BasicDataSource"
        destroy-method="close"
        p:driverClassName="com.mysql.jdbc.Driver"
        p:url="jdbc:mysql://localhost:3306/mydb"
        p:username="me"/>
    
    <bean id="bankDataAccessObject"
        class="com.example.banking.persistence.BankDataAccessObject"
        p:dataSource-ref="appDataSource"/>
    
    <bean id="bank"
        class="com.example.banking.model.Bank"
        p:dataAccessObject-ref="bankDataAccessObject"/>
    ...
</beans>

 

 

#3. AspectJ 관점 

  • 관심사를 관점으로 분리하는 가장 강력한 도구 AspectJ 언어 
  • 언어 차원에서 관점을 모듈화 구성으로 지원하는 자바 언어 확장 
  • 다만 사용하기 위해서는 새로운 툴 , 언어 구조,  관습적 코드를 익혀야함 (annotation-form AspectJ로 노력 감소) 

 

 

시스템 아키텍쳐 테스트 주도 (Test Drive The System Architecture)

  • 코드 레벨에서부터 아키텍쳐와 분리된(decouple된) 프로그램 작성은 당신의 아키텍쳐를 test drive하기 쉽게 만들어 줌 
  • 처음에는 작고 간단한 구조에서 시작하지만 필요에 따라 새로운 기술을 추가해 정교한 아키텍쳐로 진화 가능
  • 또한 decouple된 코드는 user story, 규모 변화와 같은 변경사항에 더 빠르게 대처할 수 있다
    • BDUF(Big Design Up First)와 같은 방식은 변경이 생길 경우 기존의 구조를 버려야 한다는 심리적 저항, 아키텍쳐에 따른 디자인에 대한 고민 등 변화에 유연하지 못한 단점들 존재 
    • 초기 EJB와 같이 너무 많은 엔지니어링이 가미되어 많은 concern들을 묶어버리지 않으며 오히려 많은 부분들을 숨기는 것이 아름다운 구조를 가져올 것

- 이상적인 시스템 아키텍쳐는 각각 POJO로 만들어진 모듈화된 관심 분야 영역(modularized domains of concern)으로 이루어져야 한다.
- 다른 영역끼리는 Aspect의 개념을 사용해 최소한의 간섭으로 통합되어야 한다. 이러한 아키텍쳐는 코드와 마찬가지로 test-driven될 수 있다.

 

의사 결정의 최적화하라

  • 충분히 큰 시스템에서는(그것이 도시이건 소프트웨어이건) 한 사람이 모든 결정을 내릴 수는 없다.
  • 결정은 최대한 많은 정보가 모일 때까지 미루고 시기가 되었을 경우 해당 파트의 책임자(여기서는 사람이 아닌 모듈화된 컴포넌트를 뜻한다)에게 맡기는 것이 불필요한 고객 피드백과 고통을 덜어줄 것

모듈화된 관심 분야로 이루어진 POJO 시스템의 (변화에 대한)민첩함은 최신의 정보를 가지고  최적의 선택을 할 수 있게 도와준다.
결정에 필요한 복잡도 또한 경감된다.

 

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

많은 소프트웨어 팀들은 훨씬 가볍고 직관적인 디자인이 가능했음에도 불구하고 그저 표준이라는 이유만으로 EJB2 구조를 사용했다. 

표준에 심취해 "고객을 위한 가치 창출"이라는 목표를 잃어 버렸기 때문

표준은 아이디어와 컴포넌트의 재사용, 관련 전문가 채용, 좋은 아이디어의 캡슐화, 컴포넌트들의 연결을 쉽게 도와 준다.
하지만 종종 표준을 만드는 데에 드는 시간은 납품 기한을 맞추기 어렵게 만들고 최초에 제공하려던 기능과 동떨어지게 되기도 한다.

 

시스템에는 DSL(도메인 영역 언어)이 필요하다

  • 좋은 DSL은 도메인 영역의 개념과 실제 구현될 코드 사이의 "소통의 간극"을 줄여 도메인 영역을 코드 구현으로 번역하는 데에 오역을 줄여준다.
  • DSL을 효율적으로 사용하면 코드 덩어리와 디자인 패턴의 추상도를 높여 주며 그에 따라 코드의 의도를 적절한 추상화 레벨에서 표현할 수 있게 해준다.

DSL은 "모든 단계에서의 추상화"와 "모든 도메인의 POJO화"를 고차원적 규칙과 저차원적 디테일 전반에 걸쳐 도와 준다.

 

결론

코드뿐만이 아니라 시스템 또한 깨끗해야 한다.
침략적인(invasive) 아키텍쳐는 도메인 로직에 피해를 주고 신속성에도 영향을 준다.
도메인 로직이 모호해지면 버그는 숨기 쉬워지고 기능 구현은 어려워 진다.
신속성이 침해되면 생산성이 저해되고 TDD로 인한 이득 또한 얻을 수 없다.

의도는 모든 레벨의 추상화에서 명확해야 한다.
이는 각각의 concern들을 POJO로 작성된 코드와 aspect-like 메커니즘을 통해 구성할 때 비로소 실현될 수 있다.
당신이 시스템을 디자인하든 독자적인 모듈을 디자인하든, 동작하는 범위에서 가장 간단한 것을 사용하는 것을 잊어서는 안된다.
관심사를 통해 분리하여 (생성과 사용 등) loose-coupling 된 시스템을 갖추어 클린한 시스템을 만들자.
728x90
반응형

' > CleanCode' 카테고리의 다른 글

CleanCode - 17.냄새와 휴리스틱  (0) 2022.05.07
Clean Code - #6 객체와 자료 구조  (0) 2022.01.31