복잡성은 죽음이다. 개발자에게서 생기를 앗아가며, 제품을 계획하고 제작하고 테스트하기 어렵게 만든다.
- Ray Ozzie. 마이크로소프트 최구 기술 책임자
시스템 제작과 시스템 사용을 분리하라
소프트웨어 시스템은
에플리케이션 객체를 제작하고 의존성을 서로 연결하는 준비 과정과
런타임 로직을 분리해야 한다.
아래는 Lazy Initialization, 초기화 지현 이라는 기법이다.
아래에서 getService() 는 애플리케이션이 실행되며 기능에 필요한 런타임 로직이다.
하지만 이 런타임 로직 안에서 new MyServiceImpl() 이라는 객체생성, 즉 준비 과정(시작 단계) 를 섞어 쓰고 있다.
준비 과정은 모든 애플리케이션이 풀어야할 관심사 이며, 관심사는 분리 해야 한다.
private Service service = null;
public Service getService() {
if (service == null) {
service = new MyServiceImpl(...);
}
return service;
}
인스턴스 변수의 초기화를 하지 않고 있다가, 필요한 시점에 초기화하여 사용한다.
따라서, 필요 시점까지 불필요한 메모리를 잡고있지 않아도 된다. 물론 애플리케이션이 시작하는 시간이 더 빨라질 것이다.
또, 명시적인 null 검사를 하고 있기 때문에, null 의 반환이 있을 수 없다.
하지만 만약 MyService 를 구현한 MyServiceImpl2 클래스가 있다고 가정해보자.
위 클래스의 변수 service에 어떤 서비스 구현체가 들어갈지는 getService 의 코드 또는 저기서 생략한 생성자에 의해 정해진다.
위 클래스는 의존성이 높다는 말이다.
테스트. 테스트는 가벼워야 하기 때문에
만약 MyServiceImpl 이 무거운 객체라면, 테스트를 실행하기 전 미리 적절한 테스트용 Mock Object 를 생성해야 한다.
또 위의 코드에서는 service 가 null 일때와 아닐때. 모두 테스트해야 한다. 책임이 둘인 것이다. (따로 메서드를 분리하면 되겠지만 .. 책에서는 그것이 핵심이 아닌 것 같다.)
가장 중요한 문제는 항상 MyServiceImpl() 라는 구현체를 쓰고 있는 코드인 것이다.
모든 상황에 service 에 이 구현체가 들어가는 것이 맞을까?
변경이 필요하다면?
이는 OCP 를 위반하는 상황으로 보인다.
Main 분리
이 글을 읽는 사람은 책이 있다고 가정한다. (이 글을 읽는 사람이 있을까..)
책의 그림이 의미하는 바는,
위에서 섞여있는 객체의 생성을 Main 으로 넘기자는 것이다.
그리고 런타임 로직은 Main 에서 생성되어 넘겨진 객체를 가지고 실행만 하면 되는 것이다.
에플리케이션은 main에서 어떤 객체가 어떻게 생성되는지 전혀 모른다.
팩토리
만약 객체 생성을 런티임 로직에서 해야 할 필요가 있다면,
추상 팩터리 패턴을 이용한다.
팩터리에서 객체를 만들어 제공하면, 에플리케이션이 객체를 생성하긴 하지만,
어떻게 생성하는지는 모르게 되며 의존성을 줄일 수 있다.
그림을 보면,
에플리케이션 단의 OrderProcessing 부분은 LineItemFactory 인터페이스를 이용한다.
하지만 구체적인 객체의 생성은 main 단의 LineItemFactory 를 구현한 클래스를 이용한다.
따라서 결과적으로는 객체 생성과 런타임 로직이 분리된다.
의존성 주입
의존성 주입은 제어의 역전 기법을 의존성 관리에 적용한 메커니즘.
제어의 역전에서는 한 객체가 맡은 보조 책임을 새로운 객체에게 전적으로 넘긴다.
새로운 객체는 넘겨받은 책임만 맡으므로 SRP 를 지키게 된다.
의존성 관리 맥락에서, 의존성 객체 자체를 만드는 역할은 객체가 지지 않는다.
만들어진 객체를 받아서 쓰기만 할 뿐이다.
의존성 객체를 만드는 역할은 보통 main 이나 특수 컨테이너를 이용한다.
MyService myService = (MyService) (jdniContext.lookup("NameOfMyService"));
호출하는 객체 jdniContext는 반환하는 객체의 유형을 제어하지 않는다.
lookup 의 매개변수가 무엇이냐에 따라 반환되는 객체가 다를 것이다.
클래스에서 의존성을 주입하는 방식은 생성자 주입, setter 주입 스프링에서는 필드 주입 (생성자 주입이 추천하는 방식)
이 있다.
확장
(사실 이 부분 부터는 이해를 잘 못하겠다.. 나름대로 이해한 대로 적어두고 다음에 읽었을 때 이런 생각을 가지고 읽었구나를 보기위해 기록하는 느낌으로 포스팅해본다).
소프트웨어 시스템은 물리적인 시스템과 다르다.
관심사를 적절히 분리해 관리하면 소프트웨어 아키텍쳐는 점진적으로 발전할 수 있다.
EJB (Enterprise Java Beans) 는 관심사를 적절히 분리하지 못했기에 유기적인 성장이 어려웠다.
(엔티티 빈 : 관계형 자료를 표현하는 객체, 메모리에 상주)
책의 EJB2 코드를 보면, 비즈니스 로직이 EJB2 에플리케이션 컨테이너에 강하게 결합된다.
클래스를 생성할 때, 컨테이너에서 파생해야 하고, 컨테이너가 요구하는 다양한 생명주기 메서드도 제공해야 한다.
비즈니스 로직과 컨테이너가 밀접하게 관계되어 있어서 문제가 생긴다.
컨테이너(객체를 생성하는 것으로 생각됨) 를 흉내내어 목 객체로 만들어 테스트를 하기는 어려운 일이다. (컨테이너가 크면 클수록 더)
또한 빈을 이용하기 위해 DTO 를 보통 정의하는데, DTO 는 메소드 없이 중복적인 데이터 구조이다.
횡단 관심사
EJB2 아키텍쳐는 일부 영역에서 관심사를 완벽히 분리한다.
트랜잭션, 보안, 영속적 동작등을 소스코드가 아닌 설정파일에 정의함으로써 횡단관심을 처리한다.
트랜잭션, 영속성 이런 기능들은 에플리케이션에 걸쳐 일관성 있는 동작을 한다.
이런 것을 횡단관심이라 한다.
AOP : 관점 지향 프로그래밍, 특정 관심사를 지원하려면 시스템에서 특정 지점들이 동작하는 방식을 일관성 있게 바꿔야 한다.
예를 들어 영속성에서는, 프로그래머가 영속적으로 저장할 객체와 속성을 정의하고,
영속성을 할 책임을 영속성 프레임워크에 위임한다.
그러면 AOP 프레임워크는 코드를 바꾸지 않고, 동작 방식을 변경한다.
자바 프록시
프록시 : 대리인, 대신 해주는 사람
자바에서 프록시란 객체, 클래스, 메서드등을 감싸서 요청을 가로채어 부가적인 기능을 한다.
예를 들어 Bank 라는 객체에서 getMoney() 메서드를 실행할 때, 이를 가로채어 DB 에 영속화를 한 후 메서드를 실행하거나 이런 식이다.
자바의 동적 프록시는 인터페이스만 지원한다. 클래스를 지원하는 프록시는 대표적으로 CGLIB 같은 것이 있다.
m.blog.naver.com/cncn6666/221784973026
InvocationHandler 라는 인터페이스를 구현하는데, 이것이 간단한 로직도 엄청 복잡하다.
깨끗한 코드를 작성하기 힘들다.
순수 자바 AOP 프레임워크
대부분의 위의 프록시 코드는 도구로 자동화할 수 있다.
스프링의 AOP, JBoss AOP 같은 것들이다. 이들은 내부적으로 프록시를 사용한다.
스프링의 AOP는 POJO (plain old java object) 를 사용하여 구현하였는데,
이는 순수하게 도메인에 초점을 맞추고, 엔터프라이즈나 다른 프레임워크에 의존하지 않는다.
스프링을 사용하게 되면, 빈을 등록하고 사용하는데 빈을 등록하는 과정에서 러시아 인형 형태로 프록시를 사용한다.
애플리케이션에서 DI 컨테이너에게 시스템 내 최상위 객체를 요청하는 코드는 아래와 같다.
XmlBeanFactory bf = new XmlBeanFactory(new ClassPathResource("app.xml", getClass()));
Bank bank = (Bank) bf.getBean("bank");
스프링 관련 자바 코드가 거의 필요없으므로 애플리케이션은 스프링과 독립적이다.
EJB2 에서의 강한 결합이 해결된 것이다.
m.blog.naver.com/cncn6666/221784973026
AspectJ
AspectJ 는 관심사를 분리하는 강력한 도구이다.
최선의 시스템 구조는 각기 POJO 객체로 구현되는 모듈화된 관심사영역(도메인) 으로 구성된다. 이렇게 서로 다른 영역은 해당 영역 코드에 최소한의 영향을 미치는 관점이나 유사한 도구를 사용해 통합한다. 이런 구조 역시 코드와 마찬가지로 테스트 주도 기법을 적용할 수 있다.
결론
시스템은 깨끗해야 한다.
POJO 를 이용하여 각 구현 관심사를 분리하여 추상화 단계에서 의도를 명확히 한다.
돌아가는 가장 단순한 수단을 사용하는 것이 중요하다.