스프링 프레임워크의 주요 철학 중 하나로 가장 큰 테두리 중 하나는 DI(Dependency Injection) 이다.


Dependency Injection 에 대해 먼저 살펴보자면, 말그대로 "의존성 주입" 을 말하며, 스프링 프레임워크는 Framework 레벨에서 DI 를 제공해준다.


Spring 의 Container 들은 Bean 객체들을 관리하는 데 있어서 DI 를 이용하며 이를 통해 Life Cycle 을 용이하게 관리할 수 있으며 이 것이 스프링 프레임워크의 핵심적인 동작이라고 할 수 있다.


즉, 프레임워크 레벨의 관리를 통해 개발자는 객체들간의 의존성에 신경을 덜 쓰고 Coupling 을 줄일 수 있으며 높은 재사용성과 가독성있는 코드를 만들어낼 수 있다.


이를 제어의 역전(Inversion Of Control) 이라 하며, 이것이 스프링 프레임워크의 특징적인 개념인 IOC 이다.

(클래스 관리의 주체가 개발자가 아닌 프레임워크라는 뜻이다.)


그리고 결과적으로 이러한 개발 편리성은 높은 생산성을 이끌어낼 수 있는 스프링의 큰 장점이다.


- Dependency Injection 을 통해 얻을 수 있는 장점 -


 (1) Dependency Reduction : 객체 상호 간 의존성 관계를 줄여준다.


 (2) Reusable Structure : 코드의 재사용과 조합이 용이하다.


 (3) Readability : 코드들이 분리되다보니 가독성이 뛰어나진다.


 (4) Loose Coupling & Easy to change : 구조는 변화에 민감하지 않을 수 있다.


그 외에 테스트가 용이하고 다양한 패턴을 적용하는 데 유연하다는 점도 큰 장점이 될 수 있다.



Spring Framework 에서 개발자가 Dependency Injection 을 하는 데 몇가지 방법들이 있다.


 (1) Field Injection


 가장 흔히 볼 수 있는 Injection 방법으로 사용하기도 간편하고 코드도 읽기 쉽다.



public class Sample {
    
    @Autowired
    private Example example;
    
}


 많이 사용됨에도 불구하고 Field Injection 을 통한 의존성 주입은 권장되지 않는다.

 이유는 너무 추상적인 Injection 기법 때문이다. 의존성 주입이 쉽기 때문에 Dependency 관계가 복잡해질 우려가 있으며 이는 Framework 의 사용에 있어 다음과 같은 안티패턴적 측면을 갖는다.


 - Single Responsibility Principle Violation 

 : 너무나 쉬운 의존성의 주입은 하나의 클래스가 지나치게 많은 기능을 하게됨으로써 초기 설계의 목적성이자 "객체는 그에 맞는 동작만을 한다." 는 원칙에 위배되기 쉽다.

 위배된 경우 리팩토링의 비용은 크다.


 - Dependency Hiding

 : 추상화된 의존관계는 의존성을 검증하기 힘들게 만든다. 


 - DI Container Coupling

 : Field Injection 을 사용하면 해당 클래스를 곧바로 Instant 화시킬 수 없다. 이 부분 때문에 Constructor Injection 이 권장되는 이유이기도 하다.

 가령 Container 밖의 환경에서 해당 클래스의 객체를 참조할 때, Dependency 를 정의해두는 Reflection 을 사용하는 방법 외에는 참조할 수 있는 방법이 없다.

 DI Framework 는 Field Injection 된 클래스의 Instance 화에 대해서 Null Pointer Exception 을 만들어낼 것이다.


 - Immutability

 : Field Injection 된 객체는 final 을 선언할 수 없으므로 가변적(Mutable)이다. 객체는 변경될 수 있으며 이에 대한 대응에는 큰 비용이 든다.



 (2) Setter Injection


 선택적인 의존성을 주입할 경우 유용하며, Spring 3.x 대까지 가장 권장되던 방식이다.



public class Sample {
private Example example;

@Autowired
public void setExample(Example example) {
this.example = example;
}
}


 Field Injection 으로 인한 패턴적 위험성을 상당 부분 해소한다. Optional Injection 의 경우 권장되는 방식이다.

 @Required 어노테이션을 이용하면 의존성이 필요한 Setter 를 만들 수 있다.


 (3) Constructor Injection


 Spring 4.x 이상부터 권장되는 방식이다.



public class Sample {
    private final Example example;

    @Autowired
    public Sample(Example example) {
        this.example = example;
    }
}


 코드를 통해 알 수 있듯, final 선언이 가능하며 Immutability 에 대한 해소가 가능하며 의존성의 순환 참조(Circular Dependency) 에 대한 예방이 가능하다.

 순환 참조 시 위의 방법을 이용한 코드는 BeanCurrentlyCreationExeption 을 발생시킨다.

 역시 위에서 언급된 Container Coupling 문제도 해결이 되는데, 생성자를 통한 Injection 이므로 즉각적인 Instance 화 등에 대한 문제도 해결된다.



 많은 예제 코드들이 Field Injection 방식을 사용하고 있으나 Constructor Injection 의 사용이 권장된다.

추가로 참조한 바에 의하면 Spring Team 에서는 Setter 방식을 좀 더 권장하며 이유는 생성자가 지나치게 복잡해질 수 있기 때문이라고 한다.

패턴적 관점에서 이견이 있는 듯 하다.



Spring 4.3 이상부터는 생성자가 하나인 경우 @Autowired 를 사용하지 않아도 무방하다.



정리에 있어 다음 링크들을 참조하였다.


https://www.vojtechruzicka.com/field-dependency-injection-considered-harmful/


https://dzone.com/articles/dependency-injection-pitfalls


https://blog.outsider.ne.kr/753




Spring 의 Bean 들은 대게 Singleton 패턴으로 정의되며 Application Context 에서 관리되는 형태로 개발자는 사용하게 된다.


그렇게 때문에 상당수의 개발자들이 Spring 의 각 Bean 들은 Thread Safety 가 보장된 안전한 Bean Object 이며 Spring 은 해당 동시성 문제에서 자유롭다고 여기는 경향이 있다.


하지만 정확히 Spring Bean 들은 Thread 의 Safety 와 무관하다. 


Spring Container 는 Object 들 각각에 대한 Life cycle 을 갖고 있으며 Container 내에서 하나만 존재하도록 보장하지만 그것이 Thread-Safe 를 말하는 것은 결코 아니며, Spring framework 는 오히려 이 책임을 개발자에게 맡긴다.


만약 Non-Thread-Safe Object 가 Injection 된다면 해당 객체는 쓰레드에 안전하지 않으며 개발자가 직접 핸들링해주어야 한다.


그렇다면 Bean 에 대해 어떤 방식으로 Thread Safety 를 부여할 수 있을까?


(1) Builder Pattern 의 사용

  간단하면서도 Tricky 한 방식으로 Builder Pattern 을 사용하면 좋다.

 Builder 패턴을 이용하면 객체의 Setter 를 정의하지 않은 상태에서 생성자만으로 객체의 Mutation 을 관리할 수 있으며, Spring 의 Bean 들은 Container 에 의해 life cycle 관리가 위임된다.

 따라서 Builder Pattern 을 통해 Set 을 관리하면 간단하면서도 명확히 Thread Safe 를 구현할 수 있다.


(2) Stateless Bean

  Bean 을 상태값과 무관하게 동작할 수 있는 Bean 으로 설계하는 것이다. 

  Bean 이 특정 상태를 나타내는 변수를 계속 메모리에 들고 상주하는 형태가 아닌, 가장 단순한 형태의 도메인 모델로 사용이 추천된다.


(3) Lock the beans

  가장 최후의 수단으로 여겨야 하는 방법으로 Bean 에 대해 Thread Safe 하게 설계를 하는 것이다. Spring 은 Lower level Library 들을 통해 Bean 단위의 Lock 을 지원하고 있으며 이를 통해 병렬처리에 있어서 동시성 문제의 해결이 필요하다.


물론 실제로 Safe 하지 않은 상황을 고려해야할 경우는 많지 않지만, 각기 다른 Request 를 공통으로 분류해야한다거나, 내부에서 Internal Thread 를 구동하는 경우에는 반드시 신경써보자.





CQRS(Command and Query Responsibility Segregation) 란 .Net 기반으로 발전되고 있는 설계 방법론으로 명령과 쿼리의 역할을 구분하는 것이다. 


이는 데이터에 대한 조작 Create, Insert, Delete 와 데이터에 대한 조회 Select 를 구분하는 것에서 출발한다.


어플리케이션을 개발할 때, 컨텐츠를 위한 데이터 모델은 계속해서 복잡도가 올라가게 된다.




특히 주로 사용되는 위의 모델처럼 데이터 변경과 조회는 보통 하나의 데이터모델을 사용하게 되는데, 어플리케이션의 복잡도가 증가할 수록 각 API 기능의 책임이 어떤 데이터 모델에 있는지는 불분명해진다.


이는 설계에 있어서 초기 의도를 지워버리는 역할을 하며 많은 경우의 레거시 코드들이 이런 기반으로 생겨나게 된다.


CQRS 는 이러한 고민에서 출발하며, 데이터에 대한 조회(Query) 와 데이터에 대한 조작(Command) 을 분리함으로써 이 문제를 해결하고자 한다.


기본적으로 CQRS 를 적용하기 위해서는 Command 을 위한 도메인 모델과 Query 를 위한 도메인 모델을 분리한다.



분리된 각각의 도메인 모델을 DB에 적용하는 방안으로는 몇가지가 있다.


(1) Simple


 : 같은 Scheme 을 가진 DB를 사용하며, Command / Query 시에 데이터에 대한 Converting 을 거친 후 DB에 CRUD 에 대한 작업을 수행한다.

 이 경우에는 일반 어플리케이션과 같으며 도메인 모델만 분리한 상태로 개발이 쉽고 적용이 간단하다.



(2) Premium


 : Command 를 위한 DB와 Query 를 위한 DB를 분리하는 형식으로, 데이터의 정합성을 위한 RDB를 Command 용 DB로 분리하고 Query 가 간편한 NoSQL 을 Query 용 DB로 주로 사용한다.

 이렇게 동일한 데이터에 대해 다수의 저장소를 운용하는 방식을 Polyglot Storage 라 하며 이 경우 용도에 맞는 저장소를 골라서 좀 더 알맞게 사용이 가능하다.

 하지만 분리된 저장소 각각에 대한 데이터 동기화 이슈를 Broker 등을 이용해 처리해주어야 하는 점은 이슈가 되며 책임의 소재나 로깅 등에 있어 신뢰도 확보를 위한 작업이 필요하다.



(3) Event Sourcing


 : Event Sourcing 이란 Application 내의 설계를 컨텐츠 기반이 아닌 기능 기반으로 하면서 이러한 "이벤트(Event)" 자체를 DB에 저장하는 방식을 말한다.

 이렇게 함으로써 이벤트에서 사용하는 도메인 모델은 컨텐츠를 위한 DB에 Write 되고 Query 시에는 이벤트를 저장한 DB로부터 해당 컨텐츠를 바탕으로 데이터를 만들어서 가져온다.

 도메인 모델에 대한 Command 가 따로 저장되고, Query 를 위한 도메인 모델은 Event DB로부터 불러오는 방식 때문에

 Event Sourcing 의 Architecture를 적용함에 있어서 CQRS 는 필수적인 설계 방식이 된다.

 CQRS 를 적용하는 데 있어서도 가장 큰 시너지를 낼 수 있는 Architecture 의 하나이다.


<향후 Event Sourcing Architecture에 대해서는 추가로 정리한다.>


CQRS 가 실무에 적용되는 데 있어 아직은 국내외적으로 불확실성이 있는 듯 하지만, 주목해볼만한 패턴인 것은 틀림없다.


(참고자료 : https://docs.microsoft.com/en-us/previous-versions/msp-n-p/dn568103(v=pandp.10))


 마틴 파울러가 재창한 두개의 개념으로, 비즈니스 로직을 처리하는 두가지 패턴을 말한다.

이는 책임지는 쪽이 Domain Level이냐 Script Level이냐에 따라 구분된다. 가령 다음과 같은 예시가 있다고 가정해보자.


먼저 트랜잭션 스크립트란, 은행의 계좌 이체 서비스처럼 하나의 트랜잭션 안에서 필요한 모든 로직을 수행하는 패턴이다. 

그래서 이름도 Transaction Script 로 부른다. 구현이 매우 쉽고 단순하지만 구조가 복잡해질 수록 모듈화의 복잡도 역시 높아진다.

다만, 하나의 강건한 로직이 해당 모듈에서만 구현되어야할 경우 side effect 를 예방할 수 있고, 좀 더 효율적인 코드를 작성할 수 있다.


도메인 모델은 우리가 흔히 객체 지향의 예제로 많이 배우는 형태의 모델로, 각 객체에 객체가 수행해야 하는 업무를 분담시키는 것이다. 

주요 특징은 데이터와 프로세스가 같이 존재한다는 점이며, 객체간 관계를 맺을 수 있어, 제약하거나 로직의 단순화에 도움이 된다.


또한 주로 같이 사용되는 상속이나 다양한 패턴들이 객체를 중심으로 얽히면서 개발자가 선택할 수 있는 유연성이 높아지고 모듈화가 간단해진다. 유지보수가 편리하며 재사용성이 뛰어나다.


 하지만 객체들간의 설계가 수반되어야하기 때문에 모델 구축이 쉽지 않고, 객체간의 관계를 잘 풀어나가야 한다. 특히 객체간의 dependency 는 반대로 말하자면 제약사항이기 때문에 설계가 매우 중요하다.


도메인 모델과 트랜잭션 스크립트 패턴의 차이를 보기 위해 다음 예제를 확인해보자.



    Emp emp = loadEmp("나직원");
    Dept dept = loadDept("A부서");



이 때 이를 비교 하기 위한 Domain Model 방식과 Transaction Script 방식의 차이는 다음과 같다.



    //Transaction Script 방식
    if(emp.getDeptName() == dept.getDeptName())
        return true;

    //Domain Model 방식
    if(emp.isBelongTo(dept))
        return true;



즉, Function의 책임 소재를 어디에 두는가, 사용자가 작성하는 코드 로직에 두는가(Transaction Script 방식) 아니면 객체 자체의 모델링 자체에 두는가(Domain Model 방식)의 차이이다.


참조

http://javacan.tistory.com/entry/94

http://lorenzo-dee.blogspot.com/2014/06/quantifying-domain-model-vs-transaction-script.html




+ Recent posts