본문 바로가기
설계/객체지향설계

[객체지향설계] SOLID 설계 원칙 (5) - DIP (의존성 역전 원칙)

by J4J 2024. 3. 22.
300x250
반응형

안녕하세요. J4J입니다.

 

이번 포스팅은 solid 설계 원칙 마지막인 dip (의존성 역전 원칙)에 대해 적어보는 시간을 가져보려고 합니다.

 

 

 

이전 글

 

[객체지향설계] SOLID 설계 원칙 (1) - SRP (단일 책임 원칙)

[객체지향설계] SOLID 설계 원칙 (2) - OCP (개방 폐쇄 원칙)

[객체지향설계] SOLID 설계 원칙 (3) - LSP (리스코프 치환 원칙)

[객체지향설계] SOLID 설계 원칙 (4) - ISP (인터페이스 분리 원칙)

 

 

반응형

 

 

DIP (의존성 역전 원칙) 란?

 

solid 설계 원칙에서 dip가 의미하는 것은 고수준 모듈은 저수준 모듈에 의존하지 않고 추상화에 의존해야 된다는 것을 말합니다.

 

여기서 고수준 모듈과 저수준 모듈은 무엇이며, 추상화에 의존해야 된다는 것은 또 무엇인지를 알아봐야 합니다.

 

 

 

먼저 고수준 모듈이라 하는 것은 전체 서비스의 주요한 기능을 의미하며 기능 구현을 할 때 상위에 배치되는 모듈들을 말합니다.

 

저수준 모듈이라고 하는 것은 기능 처리를 위한 구체적인 비즈니스 로직이 담기는 것을 의미하며 구현을 할 때 하위에 배치되는 모듈들을 말합니다.

 

 

 

 

고수준 모듈과 저수준 모듈을 spring에서 예시를 하나 들어보겠습니다.

 

항상 이렇다고 말할 수는 없지만 일반적으로 고수준 모듈은 controller 저수준 모듈은 service라고도 비유해 볼 수 있습니다.

 

controller에는 API와 같은 서비스의 주요한 기능이 담겨 있는 상태로 상위에 배치되어 있고, service는 API 동작을 위한 비즈니스 로직을 구성하며 하위에 배치되어 있다고 얘기해 볼 수도 있습니다.

 

 

 

그러나 많은 분들이 spring을 이용하여 개발하면서 controller는 의존성 주입을 통해 service에 담겨있는 기능을 사용하기 때문에 service에 의존되어 있다고 생각할 수 있습니다.

 

그리고 controller가 service에 의존되어 있다는 것은 고수준 모듈이 저수준 모듈에 의존하고 있다는 뜻이기에 dip가 적용되어 있지 않은 구조라고도 판단할 수 있습니다.

 

 

 

 

하지만 대부분 dip를 지키며 개발하고 계실 거라고 생각합니다.

 

dip를 지키지 않고 있다는 것은 다음과 같은 구조를 통해 의존되고 있는 경우입니다.

 

DIP를 지키지 않은 구조

 

 

 

그러나 spring을 이용하여 구조를 잡으시는 많은 분들은 위와 같이 구조를 구성하지 않고 다음과 같이 구성을 하고 계실 겁니다.

 

DIP를 지키는 구조

 

 

 

 

controller는 service의 구현체를 의존하고 있는 것이 아니라 service 구현을 위해 추상화되어 있는 인터페이스를 의존하고 있습니다.

 

반대로 service의 구현체 또한 동일하게 service 구현을 위해 추상화되어 있는 인터페이스를 의존하고 있습니다.

 

즉, 이와 같은 구조를 추상화에 의존하고 있다라고 얘기할 수 있습니다.

 

 

 

고수준 모듈인 controller는 저수준 모듈인 service impl을 의존하지 않고 대신 추상화인 service interface에 의존하게 됩니다.

 

저수준 모듈인 service impl 또한 추상화인 service interface에 의존하게 됩니다.

 

결국 고수준 모듈은 저수준 모듈에 의존하지 않고 추상화에 의존해야 된다는 설계 원칙을 제시하는 dip가 모두 지켜지고 있다라고 얘기할 수 있습니다.

 

 

 

 

DIP (의존성 역전 원칙) 특징

 

그러면 dip를 이용하여 코드를 설계할 때 얻을 수 있는 이점은 무엇인지 궁금할 수 있습니다.

 

dip의 이점은 다음과 같이 얘기해 볼 수 있습니다.

 

  • 모듈 간의 결합도를 낮춤
  • 유연한 개발이 가능
  • 코드 재 사용성을 증가시킬 수 있음

 

 

 

 

위의 예시에 대해 다시 한번 얘기해 보겠습니다.

 

controller는 service impl을 바라보는 것이 아니라 service interface를 바라보게 됩니다.

 

즉, service impl 내부에서 변경점이 발생되더라도 controller에 영향을 주는 경우는 작아지게 될 겁니다.

 

그만큼 변경에 대한 영향도가 작아지기 때문에 유연한 개발이 가능해질 수 있습니다.

 

또한 service interface를 바라보는 또 다른 service impl 들을 생산할 수 있기 때문에 재 사용성도 증가될 수 있습니다.

 

 

 

사실 dip는 해당 예시 외에도 요즘 spring 아키텍처를 설계할 때 많이 활용하는 DDD에 대해 언급을 안 할 수 없습니다.

 

DDD의 전술적 전략을 수행할 때 도출될 수 있는 clean 아키텍처와 hexagonal 아키텍처 등은 모두 dip가 활용되는 아키텍처들이기에 그만큼 활용도가 높은 설계 방법이라고 말할 수 있습니다.

 

 

 

 

DIP (의존성 역전 원칙) 적용

 

dip는 결국 인터페이스를 적극적으로 활용하여 적용해야 되는 설계 방법 중 하나입니다.

 

그리고 oop의 특징 중에서도 얘기해 본다면 다형성과 추상화에 대해서도 얘기할 수 있습니다.

 

결국 dip를 어떻게 적용해야 되는지에 대해 간단하게 생각해 보면 의존 관계에 대한 정의를 할 때 추상화를 위한 인터페이스를 정의하고 고수준 모듈과 저수준 모듈들을 모두 인터페이스를 바라보도록 구조를 잡아주시면 됩니다.

 

 

 

하지만 이렇다고 해서 모든 경우에 대해 추상화를 하라는 뜻은 아닙니다.

 

예를 들어 유틸성 객체를 생성해야 되는 경우에 대해서는 의존 관계에 대한 고민을 할 필요가 없기 때문에 추상화를 억지로 할 필요는 없습니다.

 

즉, layer 간 의존 관계를 표현해야 되는 상황에서만 활용해 보면 될 것으로 보입니다.

 

 

 

 

예제 코드 (1) - Bad Case

 

위에서 말한 spring 예시를 코드화시켜보겠습니다.

 

먼저 bad case에 대해 작성하면 다음과 같이 구성해 볼 수 있습니다.

 

// controller
package com.jforj.soliddip.badcase;

import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@RequiredArgsConstructor
public class ControllerCase {
    private final ServiceCase serviceCase;

    @GetMapping("/names")
    public ResponseEntity<List<String>> getNames() {
        return ResponseEntity.ok(serviceCase.getNames());
    }
}


// service
package com.jforj.soliddip.badcase;

import org.springframework.stereotype.Service;

import java.util.Arrays;
import java.util.List;

@Service
public class ServiceCase {

    public List<String> getNames() {
        return Arrays.asList("Henry", "Scott", "Semi");
    }
}

 

 

 

 

코드를 살펴보면 고수준 모듈인 controller에서 저수준 모듈인 service를 주입받기 위해 의존 관계가 형성된 것을 볼 수 있습니다.

 

구현체를 직접적으로 바라보는 것은 dip 설계 방식에 위배되는 행위이기도 하며 service 내부 로직이 변경될 경우 controller에 영향을 줄 확률이 높아지게 됩니다.

 

 

 

 

예제 코드 (2) - Best Case

 

bad case의 코드에서 dip를 적용할 경우 어떻게 되는지 확인해 보겠습니다.

 

// controller
package com.jforj.soliddip.bestcase;

import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@RequiredArgsConstructor
public class ControllerCase {
    private final ServiceCaseInterface serviceCaseInterface;

    @GetMapping("/names")
    public ResponseEntity<List<String>> getNames() {
        return ResponseEntity.ok(serviceCaseInterface.getNames());
    }
}


// service interface
package com.jforj.soliddip.bestcase;

import java.util.List;

public interface ServiceCaseInterface {
    List<String> getNames();
}


// service impl
package com.jforj.soliddip.bestcase;

import org.springframework.stereotype.Service;

import java.util.Arrays;
import java.util.List;

@Service
public class ServiceCaseImpl implements ServiceCaseInterface {

    @Override
    public List<String> getNames() {
        return Arrays.asList("Henry", "Scott", "Semi");
    }
}

 

 

 

 

이전과는 달리 controller와 service impl에서 동일한 추상화 객체인 service interface를 의존하고 있는 것을 확인할 수 있습니다.

 

즉, service impl 내부 로직이 변경된다고 하더라도 controller가 영향을 줄 확률이 낮아지며 유연한 개발이 가능하도록 도와줍니다.

 

 

 

 

 

 

 

 

 

이상으로 solid 설계 원칙 마지막인 dip (의존성 역전 원칙)에 대해 간단하게 알아보는 시간이었습니다.

 

읽어주셔서 감사합니다.

 

 

 

728x90
반응형

댓글