Search
Duplicate

11장 시스템

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

제작(construction)과 사용(use)는 다르다
소프트웨어 시스템은 (애플리케이션 객체를 제작하고 의존성을 서로 '연결'하는) 준비 과정과 (준비 과정 이후에 이어지는) 런타임 로직을 분리해야 한다.
10장 클래스와 이어지는 비슷한 내용이다. 먼저 아래 코드를 보자.
public Service getService() { if(service == null) service = new MyServiceImpl(...); return service; }
JavaScript
복사
위 코드는 초기화 지연(Lazy Initialization) 혹은 계산 지연(Lazy Evaluation)이라는 기법이다.
장점
필요할 때까지 객체를 생성하지 않아 과부하를 막는다.
애플리케이션 시작이 그만큼 빨라진다.
Null을 반환하지 않는다.
단점
MyServiceImpl과 생성자 인수에 명시적으로 의존한다.
실제로 MyServiceImpl객체를 사용하지 않더라도 의존성 해결이 안되면 컴파일이 안 된다.
테스트시 테스트 전용객체(Mock Object)를 할당해야 한다.
생성과 사용 로직이 섞여있어서 모든 실행 경로도 테스트 해야 한다.
책임이 여러개라는 말은 SRP(단일 책임 원칙)을 깬다.
MyServiceImpl이 모든 상황에 적합한 객체인지 알 수 없다.
결국, 위와같은 기법은 가볍게 한 번정도 사용할때는 상관없지만 사용빈도가 높아질수록 문제가 많아진다.
그렇기에 시스템의 생성과 사용 로직을 분리해야 한다.

Main 분리

생성과 관련한 코드는 모두 main이나 main이 호출하는 모듈로 옮기고, 나머지 시스템은 모든 객체가 생성 되었고 의존성이 연결되었다고 가정하는 방법.
그림11.1의 화살표 방향을 보면 모든 화살표가 main에서 애플리케이션을 가리킨다. 즉, 애플리케이션은 main이나 객체가 생성되는 과정을 모르고 생성 되었을 거라고 가정한다.
추상 팩토리 패턴을 사용해 애플리케이션이 아이템의 생성 시점을 결정할 수는 있지만 어떻게 생성하는지에 대해서는 몰라도 된다.

의존성 주입(DI: Dependency Injection)

제어 역전(Inversion of Control IoC)기법을 의존성 관리에 적용한 메커니즘
제어 역전: 한 객체가 맡은 보조 책임을 새로운 객체에 전적으로 떠넘긴다.
새로운 객체는 떠맡은 책임만 담당하기에 SRP를 지원한다.
의존성 자체를 인스턴스로 만들 책임은 지지 않고 이런 책임을 다른 '전담' 메커니즘에 전달한다.
이렇게 전달함으로써 제어를 역전한다.
초기 설정은 시스템 전체에서 필요하기에 대게 '책임질' 메커니즘으로 'main'이나 특수 컨테이너를 사용.
JNDI 검색은 의존성 주입을 '부분적으로' 구현한 기능이다.
MyService myService = (MyService)(jndiContext.lookup("NameOfMyService"));
JavaScript
복사
⇒ 호출하는 객체는 실제로 반환되는 객체의 유형을 제어하지 않는다. 대신 의존성을 능동적으로 해결한다.
더 나은 방법은 클래스가 의존성을 해결하지 않고 의존성을 주입하는 방법으로 설정자(setter)메소드나 생성자 인수를(혹은 둘 다) 제공한다.
필요한 객체의 인스턴스를 만든 후 생성자 인수나 설정자 메서드를 사용해 의존성을 설정한다.
실제로 생성되는 객체 유형은 설정 파일& 특수 생성 모듈에서 명시한다.

3. 확장

처음부터 너무 큰 확장성을 고려해서 설계 할 필요는 없다.
Ex: 작은 마을을 설계할 때 발전할 것을 고려해 6차선을 뚫거나 영화관을 짓는 것은 오버다.
오늘 주어진 스토리에 맞춰 시스템을 구현하라.
새로운 스토리는 새로운 스토리가 나올 때 맞춰 조정하고 확장하면 된다.
깨끗한 코드는 코드 수준에서 시스템을 조정하고 확장하기 쉽게 만든다.

관심사를 적절히 분리하지 못한 아키텍처 예제

EjB2아키텍처를 살펴보자.
Bank EJB용 EJB2 Inteface
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; }
Java
복사
⇒ Bank 주소, 은행이 소유하는 계좌가 열거되어 있다.
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() {} }
Java
복사
⇒ 비즈니스 논리는 컨테이너와 강결합이다. 클래스를 생성할 때 컨테이너에서 파생해야 하며 컨테이너가 요구하는 생명주기 메서드도 제공해야 한다.
⇒ 비즈니스 논리의 덩치가 매우 큰 컨테이너와 강결합 된 상태이기에 독자적인 단위테스트가 힘들다.
⇒ 프레임워크 밖에서 재사용하기가 거의불가능하다.
⇒ 상속조차 불가능하다.

횡단 관심사(cross-cutting)

EjB2는 이렇게 관심사가 잘 분리되지 않았지만 일부 영역에서는 또 제대로 분리된 부분이 있다.
원하는 트랜잭션, 보안, 일부 영속적인 동작은 소스 코드가 아닌 배치 기술자에서 정의한다.
관심사가 여러 객체에 흩어져있는 기능, 관심들을 횡단 관심사(cross-cutting-concerns)라 한다.
이러한 횡단관심사를 관점 지향 프로그래밍AOP(Aspect-Oriented-Programming) 방법론을 이용해 모듈성을 확보한다.
AOP에서 관점(aspect) 라는 모듈 구성 개념은 "특정 관심사를 지원하려면 시스템에서 특정 지점들이 동작하는 방식을 일관성 있게 바꿔야 한다" 라고 명시한다.
Ex: 프로그래머는 영속적으로 저장할 객체와 속성을 선언 후 영속성 책임을 영속성 프레임워크에 위임한다. 그러면 AOP 프레임워크는 대상 코드에 영향을 미치지 않는 상태로 동작 방식을 변경한다.

4. 자바에서 사용하는 관점(혹은 유사한) 메커니즘 세 가지

자바 프록시

단순한 상황에 적합하다.
개별 객체나 클래스에서 메서드 호출을 감싸는 경우.
JDK 에서 제공하는 동적 프록시는 인터페이스만 지원한다.
Bank에서 계좌 목록을 조회/설정하는 예제
import java.utils.*; // 은행 추상화 public interface Bank { Collection<Account> getAccounts(); void setAccounts(Collection<Account> accounts); } // BankImpl.java import java.utils.*; // 추상화를 위한 POJO("Plain Old Jaa 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()) );
Java
복사
⇒ Bank 프록시객체에서는 InvocationHandler를 구현하여 invoke를 오버라이딩한다.
⇒ 프록시에 호출되는 Bank 메소드를 구현하는데 사용하며 Reflection API를 사용해 제네릭스 메소드를 상응하는 BankImpl 메서드로 매핑한다.
⇒ 프록시 객체에서는 인터페이스를 통해 실 구현체로부터 모델과 로직을 구분했다.
⇒ 하지만, 단순한 예제에서도 코드가 많고 복잡하다. 즉, 프록시를 사용하면 깨끗한 코드 작성이 어렵다.
시스템 단위로 실행'지점'을 명시하는 메커니즘도 제공하지 않는다.

순수 자바 AOP 프레임워크

대부분의 프록시 코드는 비슷하기에 자동화가 가능하며 그러한 도구가 스프링 AOP, jBoss AOP등의 프레임워크가 있다.
Ex: 스프링은 비즈니스 논리를 순수하게 도메인에 초점을 맞춘 PJOJ로 구현하는데, POJO는 엔터프라이즈 프레임워크나 다른 도메인에 의존하지 않기에 테스트가 더 쉽고 간단하다. 그렇기에 자기 도메인에 집중할 수 있게해준다.
설정파일이나 API를 사용해 필수적인 애플리케이션 기반 구조를 구현한다.
스프링의 설정파일
<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>
Java
복사
위 설정파일을 도식화 한 게 그림11.3 이다.
각각의 빈들은 상자속의 상자 마치 러시아 인형 마뜨료쉬까 처럼 Bank 도메인 객체는 자료 접근 객체(Data Accessor Object, DAO)로 프록시 되었으며, 이 객체도 JDBC 자료 소스로 프록시 되어있다.
사용자(client)는 Bank의 메소드를 호출한다고 생각하지만 실제로는 Bank POJO의 기본 동작을 확장한 중첩 DECORATOR 객체 집합의 가장 외곽과 통신한다.
여기서 필요하다면 트랜잭션이나 캐싱 등에도 DECORATOR를 추가할 수도 있다.
위와같이 생성된 최상위 Bank 프록시 객체를 생성하는 방법은 아래와 같으며 사실상 애플리케이션은 스프링과 독립적이라고 볼 수 있다. 기존의 복잡하던 프록시 객체 생성방법을 사용하지 않아도 된다.
XmlBeanFactory bf = new XmlBeanFactory(new ClassPathResource("app.xml", getClass())); Bank bank = (Bank) bf.getBean("bank");
Java
복사
XML은 장황하고 읽기 어렵고, 설정 파일에 명시된 정책이 겉으로 노출되지 않지만 자동으로 생성되는 프록시나 관점 논리보다 단순하다. 그래서 스프링 프레임워크에서는 EjB 버전 3을 완전히 뜯어 고쳐서 XML설정 파일고 자바 5 애너테이션 기능을 사용해 횡단 관심사를 선언적으로 지원하는 모델을 따른다.
EjB3 Bank EJB 코드
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 // 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; } }
Java
복사
⇒ 훨씬 깨끗해졌다. 엔티티의 상세한 정보는 애너테이션 안에 기술되어 있기 때문에 코드는 깔끔하다.
⇒ 여기서 애너테이션에 있는 영속성 정보를 XML 배치 기술자로 옮기면 POJO만 남는다.

AspectJ 관점

관심사를 관점으로 분리하는 가장 강력한 도구
언어 차원에서 관점을 모듈화 구성으로 지원하는 자바 언어 확장이다.
새 도구를 사용하고 새 언어 문법과 사용법을 익혀야 한다는 단점이 있다.
AjpectJ 애너테이션 폼이 부담을 어느정도 완화 해 주기는 한다.

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

관점 혹은 유사 개념으로 관심사를 분리하는 방식은 매우 강력하여 애플리케이션 논리를 POJO로 작성할 수 있다면 즉, 코드 수준에서 아키텍처 관심사 분리가 가능하다면 진정한 테스트 주도 아키텍처 구축이 가능하다.
즉, 처음부터 크게 디자인하는 BDUF(Big Design Up Front)를 추구하지 않아도 된다.
다시 말해, 작지만 멋지게 분리된 아키텍처를 진행해 빠른 결과를 낸 후 기반 구조를 추가하여 확장을 할 수 있다는 의미이다.
정리하자면, 최선의 시스템 구조는 각기 POJO(또는 다른) 객체로 구현되는 모듈화 된 관심사 영역(도메인)으로 구성된다. 이렇게 서로 다른 영역은 해당 영역 코드에 최소한의 영향을 미치는 관점이나 유사한 도구를 사용해 통합한다. 이런 구조 역시 코드와 마찬가지로 테스트 주도 기법을 적용할 수 있다.

6. 의사 결정을 최적화하라.

책임은 가장 적합한 사람에게 맡기면 가장 좋다.
최대한 정보를 모아 최선의 결정을 하기 위해 가장한 마지막 순간까지 결정을 미루는게 좋다.
성급한 결정은 충분치 않은 지식과 자료로 내린 결정이다.

7. 명백한 가치가 있을 때 현명하게 사용하라.

표준이라는 의미로 충분히 필요가 없을때도 불필요하게 사용하는 것은 좋지 않다.
EjB2는 표준이라는 이유로 더 가볍고 간단한 설계만으로 충분한 프로젝트에서도 사용해서 무거워지는 경우가 있었다.
표준을 사용하면 재사용성과 지식이있는 기술자 구인이 쉽지만 표준이 너무 방대할 경우 표준이 나오는 시기가 너무 오래 걸려 업계가 기다리지 못해 무용지물이 될 수 있다.

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

최근 DSL(Domain Specific Language)이 조명받기 시작했다.
DSL은 간단한 스크립트 언어나 표준 언어로 구현한 API를 말한다.
좋은 DSL은 도메인 개념과 그 개념을 구현한 코드 사이에 존재하는 의사소통 간극을 줄여준다.
애자일 기법이 팀과 프로젝트 이해관계자 사이에 의사소통 간극을 줄여주는 것처럼..
도메인 특화 언어를 사용하면 고차원 정책에서 저차원 세부사항에 이르기까지 모든 추상화 수준과 모든 도메인을 POJO로 표현할 수 있다.

9. 결론

코드, 클래스와 마찬가지로 시스템도 깨끗해야 한다.
아키텍처가 깨끗하지 못하면 도메인 논리를 흐리며 기민성을 떨어트린다. 도메인 논리가 흐려지면 제품의 품질이 저하된다. 버그가 생길 위험이 높아진다.
모든 추상화 단계에서 의도는 명확히 표현해야 하는데 이를 위해 POJO를 작성해 관점 혹은 유사한 메커니즘을 사용해 구현 관심사를 분리해야 한다.
말하자면 시스템이던 클래스던 컴팩트하게 작성하여 사용해야 한다.