Language/Java

[클린 코드] Ch.11 - 시스템

비소_ 2022. 8. 19.

클릭하시면 네이버 Book으로 연결됩니다!

해당 서적을 참고하여 개인 공부용으로 정리한 글입니다.


시스템 수준에서 깨끗함 유지

제작(construction)과 사용(use)는 다르다.

 

시스템은 준비과정과 런타임 로직을 분리해야 한다.

하지만 대다수 어플리케이션은 이를 분리하지 않는다.

다음처럼 런타임 로직과 뒤섞는다.

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

이것은 초기화 지연(lazy Intialization) 혹은 계산 지연(Lazy Evaluation)이라는 기법이다.

몇 가지 장점이 있긴 하다.

  1. 필요하기 전까지 객체를 생성하지 않으므로 부하가 걸리지 않아 시작하는 시간이 빨라진다.
  2. 어떤 경우라도 null을 반환하지 않는다.

하지만 위 메서드는 MyServiceImpl에 의존한다.

또한 런타임 로직에서 이 객체를 사용하지 않더라도 의존성을 해결하지 않으면 컴파일 되지 않는다.

 

테스트 수행에도, MyServiceImpl이 무거운 객체라면 테스트 전용 객체를 service 필드에 할당해야 하며,

일반 런타임 로직에 객체 생성 로직을 섞어놓은 탓에 모든 실행 경로도 테스트해야한다.

 

무엇보다 모든 상황에 적합한 객체인지 알 수 없다라는 점이 가장 큰 우려다.

 

따라서, 설정 논리와 일반 실행 논리를 분리해 모듈성을 높여야 하며,

전반적이고 일관적인 방식도 필요하다.

Main 분리

생성과 사용을 분리하는 방법 중 하나로 생성 관련 코드를 모두 main이나 main이 호출하는 모듈로 옮기는 것이다.

나머지 시스템은 모든 의존성이 연결되었다고 가정한다.

팩토리

객체 생성 시점을 어플리케이션이 결정해야 할 때는 추상 팩토리(Abstract Factory) 패턴을 사용한다.

팩토리 객체를 통해 인스턴스를 생성할 수 있다.

의존성 주입

객체는 자신의 의존성을 직접 생성하지 말고 다른 전담 메커니즘에 맡겨야 한다.

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

위 코드를 호출하는 쪽에서 실제로 lookup메서드가 무엇을 반환하는지에 대해 관여하지 얺으면서 의존성을 해결할 수 있다.


확장

처음부터 올바르게 시스템을 만들 수 있다는 믿음은 미신이다.

우리는 오늘 필요한 것만 만들면 된다.

내일은 내일에 맞춰 시스템을 조정하고 확장하면 된다.

 

소프트웨어 시스템은 물리적인 시스템과 달리 조금씩 커질 수 있다.

먼저 관심사 분리를 적절히 하지 못한 예시를 살펴보자.

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

위에서 열거하는 속성은 주소, 은행이 소유한 계좌다.

각 계좌는 Account EJB로 처리하며, 아래는 이를 구현한 Bank bean에 대한 구현 클래스다.

//사응하는 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() {}
}

위 코드와 같은 전형적인 EJB2 객체 구조는 아래와 같은 문제점을 가지고 있다.

  1. 비지니스 로직이 EJB2 컨테이너에 강하게 연결되어 있다.
    클래스를 생성할 때는 컨테이너에서 파생해야 하며 컨테이너가 요구하는 다양한 lifecycle 메서드를 구현해야 한다.
  2. 실제로 사용되지 않을 테스트 객체의 작성을 위해 mock 객체를 만드는 데에도 무의미한 노력이 많이 든다.
    EJB2 구조가 아닌 다른 구조에서 재사용할 수 없는 컴포넌트를 작성해야 한다.
  3. OOP 또한 등한시되고 있다. 상속도 불가능하며 쓸데없는 DTO(Data Transfer Object)를 작성하게 만든다.

횡단(Cross-Cutting) 관심사

횡단 관심사란?
이론적으로는 독립된 형태로 구분될 수 있지만 실제 코드에는 산재하기 쉬운 부분들을 뜻한다.(보안, 로그, 영속성 등)

EJB2는 횡단 관심사 분리는 잘 이행하고 있었다.

AOP를 통해 횡단 관심사의 모듈성을 되살리고 있다.

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


Aspect-Like 메커니즘

자바 프록시

자바 프록시는 단순한 상황에 적합하다. ex) 개별 객체나 클래스에서 메서드 호출을 감사는 경우

하지만, 프록시를 사용하면 깨끗한 코드를 작성하기 어렵다. (양과 크기가 늘어남)

또한, 시스템 단위로 실행 지점을 명시하는 메커니즘도 제공하지 않는다.

순수 자바 AOP 프레임워크

Spring AOP, JBoSS AOP 등 여러 자바 프레임워크는 내부적으로 프록시를 사용한다.

Spring은 비즈니스 논리를 POJO로 구현하고, POJO는 도메인에 초점을 맞춘다.

상대적으로 단순하기 때문에 코드를 보수하고 개선하기 편하다.

 

무엇보다 어플리케이션은 스프링 관련련 자바 코드가 거의 필요없으므로 사실상 독립적이다.

EJB2 시스템이 지녔던 강한 결합 문제가 모두 사라진다.

따라서, EJB3를 완전히 뜯어고치는 계기를 제공했다.

package com.example.banking.model;

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 // An object “inlined” in Bank’s DB row
    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 배치 기술자로 옮겨 순수 POJO만 남길수도 있다.

AspectJ

AsjpectJ는 관점을 분리하는 강력한 도구다.

최근 나온 AspectJ 어노테이션 폼은 새로운 도구와 언어라는 부담을 완화해준다.


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

도메인 논리를 POJO로 작성할 수 있다면, 진정한 테스트 주도 아키텍처 구축이 가능해진다.

BDUF(Big Design Up Front)를 추구할 필요가 없다. (BDUF : 앞으로 벌어질 모든 사항을 설계하는 기법)

소프트웨어는 극적인 변화가 경제적으로 가능하다.

 

이상적인 시스템 구조는 각기 POJO 객체로 구현되는 모듈화된 관심사 도메인으로 구성된다.

이렇게 서로 다른 영역은 해당 영역 코드에 최소한의 영향을 미치는 관점이나 유사한 도구를 사용해 통합된다.

이런 구조 역시 코드와 마찬가지로 테스트 주도 기법을 적용할 수 있다.


표준에 집착하지 마라

표준을 사용하면 여러 장점이 있지만, 때로는 표준을 만드는 시간이 너무 오래 걸려 업계가 기다리지 못한다.

아주 과장되게 포장된 표준에 집착하는 바람에 고객 가치가 뒷전으로 밀려난 사례가 있다.


DSL

좋은 DSL은 도메인 개념과 이를 구현한 코드 사이에 존재하는 의사소통 간극을 줄여준다.

또한, DSL은 고차원 정책에서 저차원 세부사항에 이르기까지 모든 추상화 수준과 모든 도메인을 POJO로 표현할 수 있다.

'Language > Java' 카테고리의 다른 글

[클린 코드] Ch.13 - 동시성  (0) 2022.08.23
[클린 코드] Ch.12 - 창발성  (0) 2022.08.22
[클린 코드] Ch.10 - 클래스  (0) 2022.08.18
[클린 코드] Ch. 9 - 단위 테스트  (0) 2022.08.16
[클린 코드] Ch.8 - 경계  (0) 2022.08.15

댓글