11. 시스템

11. 시스템

Featured image

시스템 제작과 사용의 분리

관심사 분리는 가장 오래되고 중요한 설계 기법 중 하나다.

Main 분리

시스템 생성과 사용을 분리하는 한 가지 방법으로, 생성과 관련된 코드는 모두 main이나 main이 호출하는 모듈로 옮기고, 나머지 시스템은 모든 객체가 생성되었고 모든 의존성이 연결되었다고 가정한다.

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

팩토리

팩토리 패턴을 사용하여 객체가 생성되는 시점을 애플리케이션이 결정하게 구현할 수도 있다.

의존성 주입

사용과 제작을 분리하는 강력한 메커니즘 중 하나가 바로 의존성 주입(DI)이다.

DI는 IoC기법을 의존성 관리에 적용한 메커니즘이다.

DI를 통해 SRP를 지킬 수 있다.

DI 컨테이너는 객체가 필요해질때 까지 생성하지 않고 팩토리를 호출하거나 프록시를 생성하는 방법을 통해 DI를 사용하더라도 초기화 지연 방식을 활용한다.

확장

반복적이고 점진적인 애자일 방식의 핵심으로 TDD, 리팩터링을 통해 깨끗한 코드를 조성하면 코드 수정과 확장이 한결 쉬워진다.

자바 프록시


// 은행 추상화
public interface Bank {

    Collection<Account> getAccount();

    void setAccounts(Collection<Account> accounts);
}

// 추상화를 위한 POJO 구현
public class BankImpl implements Bank {

    private List<Account> accounts;

    @Override
    public Collection<Account> getAccount() {
        return accounts;
    }
    
    public void setAccounts(Collection<Account> accounts) {
        this.account = new ArrayList<Account>();
        for (Account account : accounts) {
            this.account.add(account);
        }
    }
}

// 프록시 API가 필요한 "InvocationHandler"
public class BankProxyHandler implements InvocationHandler {
    private Bank bank;
    
    public BankProxyHandler (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.getAccount());
            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())
);

프록시로 감쌀 interface Bank와 비즈니스 논리를 구현하는 POJO BankImpl을 정의했다.

프록시 API에는 InvocationHandler를 넘겨줘야 한다. 넘긴 InvocationHandler는 프록시에 호출되는 Bank 메서드를 구현하는데 사용된다.

BankProxyHandler는 자바 리플렉션 API를 사용해 제네릭스 메서드를 상응하는 BankImpl 메서드로 매핑한다.

단순한 예제임에도 코드의 양과 크기가 복잡한데 이는 프록시의 단점이다.

프록시를 사용하면 깨끗한 코드를 작성하기 어렵고 시스템 단위로 실행 지점을 명시하는 메커니즘도 제공하지 않는다.

순수 자바 AOP 프레임워크

POJO는 순수하게 도메인에 초점을 맞추기 때문에 테스트가 더 쉽고 유지보수하기 편하다.

실제 영속성, 트랜잭션, 보안, 캐시 등과 같은 처리를 위해 프레임워크에서는 사용자 모르게 프록시나 바이트코드 라이브러리를 활용해 이를 구현하고 있다.


@Entity
@Table(name = "BANKS")
public class Bank implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private int id;

    @Embeddable //  Bank의 데이터베이스 행에 '인라인으로 포함된' 객체
    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;
    }
}

모든 정보가 annotation 속에 있으므로 코드 자체는 깔끔하고 깨끗하다.

AspectJ 관점

관심사를 관점으로 분리하는 가장 강력한 도구는 AspectJ 언어다. 언어 차원에서 관점을 모듈화 구성으로 지원하는 자바 언어 확장이다.

하지만, 새 도구를 사용하고 새 언어 문법과 사용법을 익혀야 한다는 단점이 있다.

TDD 시스템 구축

코드 수준에서 아키텍처 관심사를 분리할 수 있다면, 진정한 TDD 구축이 가능해진다.

SW 아키텍처는 건축과 다르게 개발과정에도 추가와 변경이 유연하기 때문에 결과물을 재빨리 출시한 후, 기반 구조를 추가하며 조금씩 확장해나가도 괜찮다.

도메인 특화 언어

DSL(Domain-Specific Language)은 간단한 스크립트 언어나 표준 언어로 구현한 API를 가리킨다.

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

도메인 전문가가 사용하는 언어로 도메인 논리를 구현하면 도메인을 잘못 구현할 가능성이 줄어든다.

효과적으로 사용한다면 DSL은 추상화 수준을 코드 관용구나 디자인 패턴 이상으로 끌어올린다. 그래서 개발자가 적절한 추상화 수준에서 코드 의도를 표현할 수 있다.

결론

시스템이 깨끗해야 도메인 논리를 흐리지 않는다.

모든 추상화 단계에서 의도는 명확히 표현해야 한다.

시스템을 설계하든 개별 모듈을 설계하든, 실제로 돌아가는 가장 단순한 수단을 사용해야 한다.