윤개발

DIP 아키텍처 설계 본문

백엔드/Java

DIP 아키텍처 설계

DEV_SJ 2021. 8. 3. 11:16

아키텍처

아키텍처를 설계할 때 출현하는 전형적인 영역은 그림과 같은 4가지 영역 표현, 응용, 도메인, 인프라입니다.

- 표현 계층은 흔히 Controller로 불립니다. 어플리케이션의 최상단에서 유저 또는 웹 요청을 받고 응용계층에 로직을 위임하고 적절한 응답을 합니다.

- 응용 계층은 Service로 많이 불리는데 처리해야할 로직을 중재하는 역할을 합니다. 원래는 응용계층에서 대부분의 로직 처리가 이루어졌으나 도메인 중심 개발에서는 도메인에게 로직 수행을 위임합니다.

- 도메인 모델은 일종의 객체이며 핵심 로직을 수행합니다. 또한 도메인은 식별자 값을 가진 엔티티로써 데이터베이스에 매핑됩니다. 응용 계층에서 도메인에게 메시지를 전달(메서드를 호출)하고 도메인은 해당 메시지를 적절한 행동을 실행합니다.

- 인프라는 데이터베이스, 메일 sender 등 외부 시스템에 직접적으로 연관된 시스템입니다.

 

아키텍처를 보면 표현->응용->도메인->인프라 로 상위 계층이 하위계층에 의존하는 모습을 보이고 있습니다.

특히 인프라 영역에 응용과 도메인이 종속되는데 아래 문제점에서 코드로써 알아봅시다.


문제점

상품 도메인의 상황을 예시로 들어보겠습니다.  상품 도메인에서 할인 금액 계산 로직이 복잡해지면 객체지향적으로 로직을 구현하는 것 보다 룰엔진을 사용하는 것이 더 알맞을 때가 있습니다. 다음 코드는 CalcurateDiscountService(응용계층)가 Drools라는 룰엔진(인프라 계층)에 의존하는 상황입니다.

 

RuleEngine

public class DroolsRuleEngine {
    public Money evaluate(List<OrderLine> orderLines, Customer customer){
        // 세부 로직...
        return new Money();
    }
}

먼저 룰엔진입니다. 세부 로직은 알필요 없어 생략하였습니다.

evaluate 메소드는 상품리스트와 고객을 매개변수로 받아 로직을 계산하여 할인된 금액을 리턴하는 룰엔진 입니다.

 

CalculateDiscountService

@Service
public class CalculateDiscountService {
    private DroolsRuleEngine ruleEngine;

    public Money calculateDiscount(List<OrderLine> orderLines, String customerId){
        Customer customer = findById(customerId);
        return ruleEngine.evaluate(orderLines, customer);
    }

    // ...
}

다음으로는 DroolsRuleEngine을 가지고 있는 CalcurateDiscountService 입니다.

calculateDiscount 에서 ruleEngine을 직접적으로 참조하여 사용하고 있습니다. 따라서 현재 의존관계는 아래와 같습니다.

그림과 같은 의존 관계라면 DroolsRuleEngine에 변경이 발생한다면 CalculateDiscountService 또한 변경이 필수적으로 일어납니다. 따라서 고수준 모듈인 Service가 저수준 모듈인 RuleEngine에 의존 하고 있습니다. 

 

이렇게 되면 DroolsRuleEngine을 이용하지 않고는 CalculateDiscountService 만의 테스트가 어렵고, 다른 룰엔진으로의 변경이 일어난다면 기능확장에 어려움이 생기게 됩니다.

다음과 같은 문제를 DIP를 적용하여 해결해봅시다.


DIP 적용

DIP를 이용해 저수준 모듈이 고수준 모듈에 의존하도록 변경해 봅시다. 방법은 추상화한 인터페이스를 사용하는 것 입니다. CalculateDiscountService 입장에서는 룰 적용을 Drools 로 하였는지 다른 것으로 구현하였는지 중요하지 않습니다. 단지 '고객 정보와 구매 정보에 룰을 적용한 할인 금액을 구한다' 라는 것이 중요할 뿐입니다.

따라서 다음과 같이 추상화한 인터페이스를 구현합니다.

public interface RuleDiscounter {
    Money evaluate(List<OrderLine> orderLines, Customer  customer);
}

또한 Service에서는 RuleDiscounter를 참조하도록 변경합니다.

@Service
public class CalculateDiscountService {
    private RuleDiscounter ruleDiscounter;

    public Money calculateDiscount(List<OrderLine> orderLines, String customerId){
        Customer customer = findById(customerId);
        return ruleDiscounter.evaluate(orderLines, customer);
    }
}

추상화 인터페이스를 통해 다음과 같이 의존관계가 변경되었습니다.

이제 저수준 모듈이 고수준 모듈에 의존하게 되었습니다. 이를 DIP(Dependency Inversion Priciple, 의존관계 역전 원칙) 이라고 부릅니다.

 

DIP를 통해 앞선 2가지의 문제점이 해결되었습니다.

DroolsRuleEngine 없이는 테스트에 어려움이 있던 CacluateDiscountService는 RuleDiscount 인터페이스에 Mock 객체를 주입하여 테스트하면 되도록 변경되었습니다.

또한 기능확장에 어려움이 있던 부분은 RuleDiscount를 새로 구현하기만 하면 되도록 수정되었습니다.


DIP 적용 주의점

DIP를 잘못 생각하면 단순히 인터페이스와 구현 클래스를 분리하는 정도로 받아들일 수도 있습니다. 

DIP 핵심은 고수준 모듈이 저수준 모듈에 의존하지 않도록 하기 위함인데 그림과 같이 저수준 모듈에서 인터페이스를 추출하는 경우가 있습니다. 다음 그림은 잘못된 구조입니다.


정리

어플리케이션을 개발하거나 운영할 때 변경은 필수적으로 일어납니다. 이런 변경에 영향을 최소화 하기위해서는 적절한 의존관계를 설계해야하며 포스팅과 같은 DIP를 이용한 설계를 해보는 것도 좋습니다.


출처 및 참고

ddd start - 최범균 저

 

 

 

'백엔드 > Java' 카테고리의 다른 글

Java의 Equals와 Hashcode  (1) 2021.04.27
Java Garbage Collection 이란?  (0) 2020.11.12
Comments