"복잡성은 죽음이다. 개발자에게서 생기를 앗아가며, 제품을 계획하고 제작하고 테스트하기 어렵게 만든다."
- 레이 오지(Ray Ozzie, 마이크로소프트 최고 기술 책임자 (CTO))
제작 (construction)은 사용(use)과 아주 다르다.
시작 단계는 모든 애플리케이션이 풀어야 할 관심사(concern)다.
준비 과정 코드에 런타임 로직이 뒤섞인 예
public Service getService(){
if (service == null)
service = new MyServiceImpl(...); // 모든 상황에 적합한 기본값일까?
return service;
}
체계적이고 탄탄한 시스템을 만들고 싶다면 모듈성을 깨서는 절대로 안된다.
객체를 생성하거나 의존성을 연결할 때도 마찬가지
시스템 생성과 시스템 사용을 분리하는 방법 중 하나
생성과 관련한 코드는 모두 main이나 main을 호출하는 모듈로 옮기고 나머지 시스템은 모든 객체가 생성되고 모든 의존성이 연결되었다고 가정
의존성 주입(DI, Dependency Injection) : 제어의 역전(IoC, Inversion of Control) 기법을 의존성 관리에 적용한 매커니즘
IoC에서는 한 객체가 맡은 보조 책임을 새로운 객체에게 전적으로 떠넘김
JNDI 검색은 의존성 주입을 '부분적으로' 구현한 기능
// 객체는 디렉터리 서버에 이름을 제공하고 그 이름에 일치하는 서비스를 요청한다.
MyService myService = (MyService) (jndiContext.lookup("NameOfMyService"));
진정한 의존성 주입은 클래스가 의존성을 해결하려 시도하지 않는다. (클래스는 완전히 수동적)
스프링 프레임워크는 Java DI Container를 제공한다.
DI를 사용하더라도 초기화 지연 가능
단순한 상황에 적합
개별 객체나 클래스에서 메서드 호출을 감싸는 경우
JDK에서 제공하는 동적 프록시는 인터페이스만 지원 (클래스 proxy는 CGLIB, ASM, Javassist 등과 같은 바이트 코드 처리 라이브러리 필요)
JDK 제공 proxy 예제
import java.util.*
// 은행 추상화
public interface Bank {
Collection<Account> getAccounts();
void setAccounts(Collection<Account> accounts);
}
import java.util.*
// 추상화를 위한 POJO 구현
public interface 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);
}
}
}
import java.lang.reflect.*;
import java.util.*;
// 프록시 API가 필요한 "InvocationHandler"
public class BankProxyHandler implements InvocationHandler {
private Bank bank;
public BankProxyHandler (Bank bank) {
this.bank = bank;
}
// InvocationHandler에 정의된 메서드 (invoke)
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를 BankProxyHandler로 구현
Bank bank = (Bank) Proxy.newProxyInstance(
Bank.class.getClassLoader(),
new Class[] {Bank.class},
new BankProxyHandler(new BankImpl()));
프록시로 감쌀 인터페이스 Bank와 비즈니스 논리를 구현하는 POJO BankImpl을 정의
프록시 API에는 InvocationHandler를 넘겨줘야 한다.
단순한 예제(라고는 하는데..)이지만 코드가 상당히 많으며 제법 복잡하다.
코드의 '양', '크기'는 프록시의 두 가지 단점
Spring AOP, JBoss AOP 등과 같은 여러 자바 프레임워크는 내부적으로 프록시를 사용한다.
스프링은 비즈니스 논리를 POJO로 구현한다.
프로그래머는 설정 파일이나 API를 사용해 필수적인 Application 기반 구조를 구현한다.
ex) Spring 2.x의 app.xml
중첩 Decorator 객체 집합
의 가장 외곽과 통신한다. (필요하다면 트랜잭션, 캐싱 등에도 Decorator를 추가할 수 있다.)EJB3는 XML 설정 파일과 자바 5 애너테이션 기능을 사용해 횡단 관심사를 선언적으로 지원하는 스프링 모델을 따른다. (스프링 프레임워크는 EJB 버전 3를 완전히 뜯어고치는 계기를 제공)
aspect로(혹은 유사한 개념으로) 관심사를 분리하는 방식은 그 위력이 막강하다.
프로젝트를 시작할 때는 일반적인 범위, 목표, 일정은 물론이고 결과로 내놓을 시스템의 일반적인 구조도 생각해야 한다.
+ 변하는 환경에 대체해 진로를 변경할 능력도 유지해야 한다.
최선의 시스템 구조는 각기 POJO(또는 다른) 객체로 구현되는 모듈화된 관심사 영역(도메인)으로 구성
서로 다른 영역은 해당 영역 코드에 최소한의 영향을 미치는 관점이나 유사한 도구를 사용해 통합
aspect로 통합된 구조 역시 코드와 마찬가지로 테스트 주도 기법을 적용할 수 있음
도메인을 구현하기 위한 로직과 기반 환경(권한 설정, 보안 구성 등) 구현 로직과의 분리는 결국 도메인 로직을 구현하고 검증하는 데 집중할 수 있는 구조가 유지보수에 유용하다는 것으로 정리하였습니다.
aspect에 대해서는 추가적으로 정리가 필요하겠지만 비즈니스 로직이 특정 환경이나 설정에 의존적이거나 선행이 필요한 상태가 되지 않도록 유의해야겠습니다.