CleanCode 11장 시스템

김희윤·2021년 3월 11일
0

cleancode

목록 보기
11/13
post-thumbnail

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

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

'제작'과 '사용'은 다르다.

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

 public Service getService() {
      if (service == null)
          service = new MyServiceImpl(...); // Good enough default for most cases?
      return service;
  }

위와 같은 코드를 '초기화 지연', '계산 지연' 기법이라 부른다.
장점도 존재한다.

  • 실제로 필요할 때까지 객체를 생성하지 않기 때문에 부하가 걸리지 않는다(애플리케이션 시작 시간 빨라짐).
  • 어떠한 경우에도 null 포인터를 반환하지 않는다.

하지만 이 메서드는 단점이 크다.

  • MyServiceImpl 객체에 의존성을 가지게 되었다.
    -> MyServiceImpl의 사용 여부와 관계 없이 무조건 이 의존성을 만족해야 하게 된 것이다.
  • servie가 null인 경로와 not null 인 경로를 모두 가진다.
    -> 작지만 책임이 둘이기 때문에 SRP(단일책임원칙)을 위반한다.
  • MyServiceImpl이 모든 상황에 적합한 객체인지 확신할 수 없다!

체계적이고 단단한 시스템을 만들고 싶다면 위와 같은 '손쉬운 기법'으로 모듈성을 깨서는 안된다. 또한 '설정 논리'와 '일반 실행 논리'를 분리해야 모듈성이 높아진다.

2.   Main 분리

위의 흐름을 보면

  • main 함수에서 시스템에 필요한 객체를 생성한 후 이를 애플리케이션에 넘긴다.
  • 애플리케이션은 그저 객체를 사용할 뿐이다.
  • 애플리케이션은 main이나 객체가 생성되는 과정을 전혀 모른다.

3.  팩토리

때로는 객체가 생성되는 시점을 애플리케이션이 결정할 필요도 있다.

  • main에서 OrderProcessing 애플리케이션으로 의존성이 향한다

즉 위와 같이 애플리케이션은 생성되는 방법을 모른다.

4.  의존성 주입(DI, Dependency Injection)

의존성 관리의 관점에서 "객체는 그 자신의 의존성들을 직접 생성하지 말고 다른 전담 매커니즘에 넘겨야한다."

MyService myService = (MyService)(jndiContext.lookup("NameOfMyService"));

위 코드는 호출하는 쪽에서 lookup 메서드가 무엇을 리턴하는지 모르면서 의존성을 해결할 수 있다.

진정한 의존성 주입은 한 발 더 나아가서, 클래스가 의존성을 해결하려하지 않는다.
-> 의존성을 주입하는 방법으로 setter 메서드나 생성자 인수를 제공한다.

5.   확장(스케일링)

처음부터 올바르게 시스템을 만들 수 없다. 그저 오늘은 주어진 스토리대로 시스템을 구현하고 내일은 새로운 스토리에 맞춰 확장하고 조정해야 한다.

-> 애자일 방식의 핵심!
(TDD, 리팩터링은 코드 수준에서 시스템을 조정하고 확정하기 쉽게 만든다.)

/* Code 2-1(Listing 11-1): An EJB2 local interface for a Bank EJB */

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;
}
/* Code 2-2(Listing 11-2): The corresponding EJB2 Entity Bean Implementation */

package com.example.banking;
import java.util.Collections;
import javax.ejb.*;

public abstract class Bank implements javax.ejb.EntityBean {
    // Business logic...
    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 container logic
    public abstract void setId(Integer id);
    public abstract Integer getId();
    public Integer ejbCreate(Integer id) { ... }
    public void ejbPostCreate(Integer id) { ... }
    
    // The rest had to be implemented but were usually empty:
    public void setEntityContext(EntityContext ctx) {}
    public void unsetEntityContext() {}
    public void ejbActivate() {}
    public void ejbPassivate() {}
    public void ejbLoad() {}
    public void ejbStore() {}
    public void ejbRemove() {}
}

위 코드와 같은 EJB2 객체 구조는 문제점이 있다.

  • 비즈니스 로직이 EJB2 컨테이너에 타이트하게 연결되어 있다.
    - Entity를 만들기 위해 컨테이너 타입을 subclass하고 필요한 lifecycle 메서드를 구현해야 함
  • 실제로 사용되지 않을 테스트 객체 작성을 위해 mock 객체를 만드는 데 노력이 많이든다.
  • OOP 또한 등한시 되고있다. 상속도 불가능하며, 쓸데없는 DTO(Data Transfer Object)를 작성한다.

6.  Cross-Cutting-Concerns

= 이론적으로는 독립된 형태로 구분할 수 있지만 실제로는 코드에 산재하기 쉬운 부분
(transaction, authorization, logging 등)

이 문제를 AOP(Aspect-Oriented-Programming)으로 해결하려한다.
아래는 aspect-like-mechanism 이다.

  1. 자바 프록시
  • 간단한 경우라면 자바 프록시가 적절한 솔루션이다.
    (개별 객체나 클래스에서 메서드 호출을 감싸는 경우 같은)
  • 하지만 JDK는 interface만 프록시를 지원하며 클래스에서 사용하려면 CGLIB, ASM, Javassist등 라이브러리가 필요하다.
  • 추가적으로 복잡한 코드가 생겨 클린코드를 작성할 때 걸림돌이 된다.
  1. 순수 자바 AOP 프레임워크
  • 위 단점들을 Spring, JBoss와 같은 순수 자바 AOP 프레임워크를 통해 해결할 수 있다.
  • 프레임워크들은 내부적으로 프록시를 사용하며, POJO는 순수하게 도메인에 초점을 맞춘다.
    - POJO는 프레임워크에 의존하지 않는다. 따라서 테스트가 개념적으로 쉽고 간단하다.
  • 쉽고 간단하기 때문에 사용자들이 구현하기가 더 쉬우며 보수하고 개선하기 편하다.
  1. AspectJ
  • AOP를 실현하기 위한 full-featured tool
  • 보통의 경우에는 Spring AOP와 JBoss AOP로도 충분하지만 AspectJ는 훨씬 강력한 수준을 지원
  • 하지만 생로운 툴, 언어 구조, 관습적인 코드를 익혀야 한다는 단점
profile
블록체인, IOT, 클라우드에 관심이 많은 개발자 지망생

0개의 댓글

관련 채용 정보