본문 바로가기
프로그래밍/디자인 패턴

[spring + 객체 지향 원칙] Spring에서의 의존성 역전의 원칙(Dependency Inversion Principle)

by 사바라다 2021. 1. 10.

안녕하세요. 오랜만에 포스팅을 올리게 되네요. 새해가 되서 책한권 읽어야지 했는데... 생각보다 오래 걸렸습니다. 그 책을 다 읽고 이제 포스팅을 하려고 합니다. 아쉽지만 책과 관련된 내용은 아닙니다. 책을 읽다가 이거 포스팅하면 좋겠다 싶었던 부분이 있었거든요.

오늘 여러분들께 알려드릴 것은 객체 지향 5가지 원칙 중 하나인 의존성 역전의 원칙(Dependency Inversion Principle)입니다. 사실 객체 지향의 5가지 원칙에 대해서는 2년정도 전에 한번 일괄적으로 포스팅을 했었습니다. 해당 원칙에 대해서 생소하신 분들은 객체지향 설계의 5가지 원칙 S.O.L.I.D 포스터를 참고해주시면 도움이 될 것입니다.

오늘은 java Spring을 사용할 때의 예를 들어 DIP에 대해서 좀 더 이해하기 쉽도록 이야기 해보려고합니다.

레이어드 아키텍처 [Layered Architecture]

시스템의 코드 아키텍처를 구성할 때 일반적으로 레이어드 아키텍처를 많이 사용합니다. 레이어드 아키텍처란 시스템의 구성을 표현 계층(Presentation) - 응용 계층(Application) - 도메인 계층(Domain) - 인프라 계층(Infra) 등 4개 이상의 계층으로 분할하며 각각 각 계층은 바로 아래의 계층 또는 더 낮은 계층에게 의존성을 가지게됩니다. 그리고 또한 Spring에서는 포현 계층은 Controller, 응용 계층은 Service에 매핑됩니다. 이미지로 나타내면 아래와 같습니다.

레이어드 아키텍처의 자세한 설명은 추후에 또 다른 포스팅으로 찾아뵙도록 하겠습니다. :)

계층간의 의존성

레이어드 아키텍처를 말씀드리며 계층간의 의존성은 위에서 아래로 흐른다고 말씀드렸습니다. 즉, Spring에서 표현 계층을 대표하는 Controller는 응용 계층인 Service의 로직에 의존하게되며 Service는 도메인 계층 또는 인프라 계층에 의존하게 되는 것입니다. 의존 한다는 것은 의존 하는 대상의 로직이나 파라미터 등의 변경이 있을 때 수정이 있을 수 있다는 의미입니다.

문제점

이렇게 의존성을 가지고 있을 때 문제점은 무엇일까요 ? 외부 서비스와의 동기화를 하는 예제를 가지고 이야기해보도록 하겠습니다. 이 로직은 응용 계층인 Service와 외부와 통신하는 인프라 계층을 가지고 있습니다. Sync 로직은 아래와 같습니다.

응용계층의 코드는 아래와 같습니다.

@Service
@RequiredArgsConstructor
public class SyncService {

    private final GameRepositoryV1 gameRepositoryV1;

    private final ThirdPartyClientService thirdPartyClient;

    public void syncDetail(Long thirdPartyId) {
      String gameDetail = thirdPartyClient.getGameDetail(thirdPartyId); // 외부에서 데이터 가져오기
      Game game = buildGame(gameDetail); // 가져온 데이터를 object로 매핑
      gameRepositoryV1.save(game); // DB에 저장
    }

    private Game buildGame(String gameDetail) {
        // mapping 후 Game 반환
    }

}

인프라 계층의 코드는 아래와 같습니다.

@Component
public class ThirdPartyClientService {

  public String getGameDetail(Long thirdPartyGameId) {
    // thirdParty에 직접 호출하여 데이터를 가져온 후 반환
  }
}

위 코드는 정상으로 동작하며 테스트도 잘 됩니다. 하지만 확장성 측면에서 본다면 문제가 있습니다. 바로 응용서비스인 SyncService가 ThirdParyClientService의 변화에 대해서 동시에 수정이 일어나야한다는 것입니다.

예를 들어 ThirdPartyClientService의 getGameDetail의 반환값이 변경된다고 하겠습니다. 그렇다면 Game DB에 넣기위한 변환이 필요합니다. 현재 변환을 하는 코드는 SyncService에 있는 buildGame 메서드입니다. 이쪽에 수정이 일어날 수 밖에 없게 되는것입니다.

다른 예로는 sync를 진행하는 외부 시스템이 추가되었다고 하겠습니다. 그렇다면 새로운 ClientService를 만드는 것은 물론 SyncService에는 새로운 buildGame 메서드를 새로 생성할 필요가 있습니다. 동기화 되는 시ㅅ템이 늘어나면 늘어날 수록 SyncService의 코드의 길이는 점점 길어지게 되는 결과가 될 것입니다.

DIP(Dependency Inversion Principle) 적용

예전 포스팅에서 저는 의존성 역전의 원칙(Dependency Inversion Principle) 이란 어떤 Class를 참조해야하는 상황이 생긴다면 그 Class를 직접 참조하는 것이 아니라 적절한 인터페이스를 만들고 해당 인터페이스를 상위 클레스를 기준으로 참조되도록 구현하는 것이라고 말씀드렸습니다.

아래는 기존 코드의 참조 관계입니다.

그리고 아래 참조 관계는 확장성을 고려한 참조관계가 됩니다.

이렇게 코드 관계를 구성했을 때 좋은점은 위에서 언급한 문제점의 경우라도 SyncService의 변경이 일어나지 않는다는 것입니다. 코드로 확인해보도록 하겠습니다.

아래는 중계해주는 인터페이스 코드입니다.

public interface SyncHandler<T, E> {
  Game build(T returnType, E thirdPartyId);
}

 

아래는 인프라 계층의 코드입니다.  

@Component
public class ThirdPartyClientService implements SyncHandler<String, Long> {

  public String getGameDetail(Long nintendoGameId) {
    // 직접 호출 후 결과 값 반환
  }


  @Override
  public Game build(String returnType, Long thirdPartyId) {
    String gameDetail = getGameDetail(thirdPartyId);
    // mapping 후 Game 반환
  }
}

아래는 응용 계층의 코드입니다.  

@Service
@RequiredArgsConstructor
public class SyncService {

    private final GameRepositoryV1 gameRepositoryV1;

    private final SyncHandler<String, Long> thirdPartyClientService;

    public void syncDetail(Long thirdPartyId) {
      Game game = thirdPartyClientService.build(gameDetail); 
      gameRepositoryV1.save(game); // DB에 저장
    }
}

이렇게 코드를 구성하게되면 결과값의 타입이 변경됬을경우는 인터페이스의 선언부와 ThirdPartyClientService에서만 수정을 하면 되며 추가적인 ThirdParty가 생겼을 때도 SyncService 코드의 변화는 크지 않다는 걸 눈으로 확인하실 수 있습니다.

그리고 DIP의 설계원칙의 중요한 주의하실 점이 있습니다. 바로 둘을 이어주는 인터페이스를 상위 Class의 입장에서 구성해야한다는 것입니다. 여기서는 SyncHadler를 SyncService의 입장에서 구성하였습니다. SyncHandler가 응용계층에 존재한다는 것이 중요한 부분입니다.

마무리

오늘은 이렇게 Spring에서 레이어드 아키텍처의 기본적인 부분과 DIP가 필요하게되는 상황, 그리고 사용하는 방버베 대해서 알아보는 시간을 가졌습니다.

여러분들께 도움이 됬으면 합니다.

감사합니다.

댓글