도시가 돌아가는 이유는 적절한 추상화와 모듈화 때문이다. 또한 각 분야의 팀 때문이다.
소프트웨어 팀도 도시 처럼 구성한다. 그런데 막상 팀이 제작하는 시스템은 비슷한 수준으로 관심사를 분리하거나 추상화를 이뤄내지 못한다.
깨끗한 코드를 구현하면 낮은 추상화 수준에서 관심사를 분리하기 쉬워지는데, 이번 부분에서는 높은 추상화 수준, 즉 시스템 수준에서 깨끗함을 유지하는 방법을 살펴본다.
제작은 사용과 다르다.
시작은 관심사 분리로 시작한다. 불행히도 대다수 어플리케이션은 시작 단계라는 관심사를 분리하지 않는다.
초기화 지연, 계산 지연이라는 기법이 있다. 실제 필요할때까지 객체를 생성하지 않으므로 불필요한 부하가 걸리지 않는다. 또한 어떤 경우에도 null 포인터를 반환하지 않는다.
예를 들어 아래와 같은 메소드는 해당 기법에 속한다.
public Service getService(){
if(service == null){
service = new MyServiceImpl(...);
}
return service;
}
하지만 getService 메서드가 MyServiceImpl과 생성자 인수에 명시적으로 의존한다. (위에 생략됨)
런타임 로직에서 MyServiceImpl 객체를 사용하지 않더라도 의존성을 해결하지 않으면 컴파일이 되지 않는다.
테스트에도 문제가 있다. MyServiceImpl이 무거운 객체라면 단위 테스트에서 getService 메서드를 호출하기 전에 적절한 테스트 전용 객체(테스트 더블이나 목 객체)를 service에 할당해야 한다.
또한 일반 런타임 로직에다 객체 생성 로직을 섞어 놓은 탓에 (service가 null인 경로와 null이 아닌 경로 등) 모든 실행 경로도 테스트해야 한다.
책임이 둘이라는 말은 메서드가 작업을 두가지 이상 수행한다는 뜻이다. 즉 작게나마 단일책임 원칙을 깬다는 말이다.
무엇보다 MyServiceImpl이 모든 상황에 적합한 객체인지 모른다는 사실이 가장 큰 우려가 된다.
초기화 지연 기법을 한번 정도 사용한다면 별로 심각한 문제는 아니다. 하지만 많은 애플리케이션이 이런 기법을 수시로 사용하며 전반적인 설정 방식이 애플리케이션 곳곳에 흩어져 있다.
모듈성은 저조하고 대개 중복이 심각하다.
체계적이고 단단한 시스템을 만들고 싶다면 이런 손쉬운 기법으로 모듈성을 깨서는 절대 안된다. 객체를 생성하거나 의존성을 연결하는 부분에서도 마찬가지이다.
이러한 설정 논리는 일반 실행 논리와 분리해야 모듈성이 높아진다.
또한 주요 의존성을 해소하기 위한 전반적이며 일관적인 방식도 필요하다.
물론 때로는 객체가 생성되는 시점을 애플리케이션이 걸정할 필요도 있다. 예를 들어 추상 팩토리 패턴을 사용해 LineItem을 생성하도록 한다면 생성하는 시점은 애플리케이션이 결정하지만 LineItem을 생성하는 코드는 애플리케이션이 모른다.
이처럼 사용과 제작을 분리하는 강력한 메커니즘 중 하나가 스프링에서 사용하는 의존성 주입이다. (DI)
의존성 관리 맥락에서 객체는 의존성 자체를 인스턴스로 만드는 책임은 지지 않으나 대신에 이런 책임을 다른 전담 메커니즘에 넘겨야 한다.
그렇게 함으로써 제어를 역전한다.
초기 설정은 시스템 전체에서 필요하므로 대개 책임질 메커니즘으로 main 루틴이나 특수 컨테이너를 사용한다.
처음부터 올바르게 시스템을 만들 수 있다는 믿음은 미신이다.
오늘 주어진 사용자 및 환경, 스토리에 맞춰 시스템을 구현해야 한다. 내일은 또다시 새로운 스토리에 맞춰 시스템을 조정하고 확장하면 된다.
이것이 애자일 방식의 핵심이다. (+테스트 코드, 리팩토링)
하지만 시스템 수준에서는 어떨까? 사전계획은 필요하지 않을까? 단순한 아키텍처를 복잡한 아키텍처로 조금씩 키울수 있을까?
소프트웨어 시스템은 수명이 짧다는 본질로 인해 아키텍처의 점진적인 발전이 가능하다.
AOP와 같은 횡단 관심사를 떠올려 보자.
영속성과 같은 관심사는 애플리케이션의 자연스러운 객체 경계를 넘나드는 경향이 있다.
관점이라는 모듈 구성 개념은 특정 관심사를 지원하려면 시스템에서 특정 지점들이 동작하는 방식을 일관성 있게 바꿔야 한다고 명시한다.
영속성을 예로 들면, 프로그래머는 영속적으로 저장할 객체와 속성을 선언한후 영속성 책임을 영속성 프레임워크에 위임한다. 그러면 AOP 프레임 워크는 대상 코드에 영향을 미치지 않는 상태로 동작방식을 변경한다.
코드의 양과 크기는 프록시의 두가지 단점이다.
다행스럽게도 대부분의 프록시 코드는 판박이기 때문에 도구로 자동화가 가능하다.
POJO, 즉 순수 자바 관점을 구현하는 스프링 AOP, JBoss AOP등 과 같은 여러 자바 프레임워크는 내부적으로 프록시를 사용한다.
POJO는 도메인에 순수하게 초점을 맞추며 엔터프라이즈 프레임워크에, 그리고 다른 도메인에 의존하지 않는다.
따라서 테스트가 개념적으로 더 쉽고 간단하다. 상대적이 더 단순하기 때문에 사용자 스토리를 올바르게 구현하기 쉬우며 미래 스토리에 맞추어 코드를 보수하고 개선하기 편하다.
프로그래머, 개발자는 설정 파일이나 API를 사용해 필수적 어플리케이션의 기반 구조를 구현하고, 여기에는 영속성, 트랜젝션, 보안, 캐시, 장애조치 등과 같은 횡단 관심사들도 포함된다.
이때 프레임워크는 사용자가 모르게 프록시나 바이트코드 라이브러리를 사용해 이를 구현한다. 이런 선언들이 요청에 따라 주요 객체를 생성하고 서로 연결하는 등 DI 컨테이너의 구체적인 동작을 제어한다.
관점지향 프로그래밍과 AOP, 프록시를 추상적으로 알고 있다는 느낌이 들어 좀더 찾아봐야겠다는 생각이 들었다.
'컴퓨터' 카테고리의 다른 글
클린코드 - 클래스에 관하여 (0) | 2024.02.02 |
---|---|
클린 코드 - 단위테스트에 대하여 (0) | 2024.02.02 |
프록시 의미 (0) | 2021.11.16 |
docker entrypoint와 cmd 차이 (0) | 2021.11.16 |
ngix 및 apache 사용 이유 (0) | 2021.11.15 |