🎁[속보] 인프런 내 깜짝 선물 출현 중🎁

스프링 핵심 원리 - 기본편 / 객체 지향 설계 / DI와 정적, 동적 의존 관계

  • 스프링 핵심 원리의 기본편을 수강중입니다. 이론에 해당되는 내용 중 일부를 재해석해서 정리해보았습니다.

  • 용어 정리

    • 의존 관계

      • 이 글에서는 일단 "A가 B에 의존한다"를 "B가 변경될 경우 A가 변경되어야 한다"라고 정의하겠습니다.

      • 예를 들어 어떤 프로젝트 내에서 데이터 저장소를 ARepository에서 BRepository로 변경할 때 CService의 코드 내에 해당 부분을 변경해야 할 경우, "CService 클래스 구현 코드는 ARepository 클래스에 의존한다"고 표현하겠습니다.

         

    • 정적 / 동적

      • 이 글에서만 관심 있는 영역의 코드가 컴파일되기 전에 이미 고정된 것을 정적인 것으로 정의하고, 코드 상으로는 고정되어 있지 않고, 컴파일 후 실행될 때에야 정해지는 것을 동적인 것으로 정의하겠습니다.

      • 다른 글들을 보았을 때 맥락에 따라 "컴파일" 대신 다른 단어가 들어오는 경우도 있는 것 같습니다.

    • OCP (개방-폐쇄 원칙)

      • "코드는 기능 확장에는 열려 있으면서, 수정에는 닫혀 있어야 한다."는 원칙입니다.

      • 다시 말해 코드 자체를 수정하지 않고도 기능을 확장할 수 있어야 한다는 원칙입니다.

         

    • SRP (단일 책임 원칙)

      • 모듈은 한 가지 책임만 지녀야 한다는 원칙입니다.

        • 여기서 책임이란 모듈의 변화에 대한 이유입니다.

      • 이 글의 맥락에서는 지엽적인 내용이지만, 솔직히 말씀드리면 위 정의를 보긴 했지만 적용이 가능할 정도로 명확히 알지 못해 다음 자료들을 참고하였습니다. 제가 일단 이해한 내용을 공유드리겠습니다.

  • DI (의존성 주입)

    • 해결하고자 하는 문제

      • 어떤 기능을 실행 시에 다양한 구현체들을 사용하는 방식으로 확장하고 싶습니다.

        • 예시: 데이터 저장을 위한 Repository를 운영 시에는 DatabaseRepository를 사용해서, 개발 시에는 InMemoryRepository를 사용하여 실행하고 싶습니다.

      • 그러나 Repository 구현체를 변경하려고 보니 Repository를 사용하는 모든 곳에서 구현체를 수정해야 하는 문제가 발생했습니다.

        • 예시

           

          class Service {
              private final Repository repository = new InMemoryRepository(); // 코드 수정 필요
              // Repository 사용 로직
              ...
          }
    • DI를 사용한 해결 방안

      • Repository를 사용하는 클래스들이 DatabaseRepository와 같은 구체적인 구현체들을 사용하지 않게 지워버린 후 Repository 인스턴스를 외부에서 주입받도록 합니다. 여기서는 생성자를 통해 주입받았습니다.

        • 예시

           

          class Service {
              private final Repository repository;
              public Service(Repository repository) {
                  this.repository = repository;
              }
              // Repository 사용 로직
              ...
          }
      • Config 클래스를 따로 두어 인스턴스를 생성하는 코드들을 모두 이 클래스에 둡니다. 인스턴스가 필요한 경우 이 클래스에서 사용하면 됩니다.

         

        • 예시

          class Config {
              repository() {
                  return new InMemoryRepository();
              }
              service() {
                  return new Service(repository());
              }
          }
    • OCP 관점에서의 해석

      • 원래 문제에서는 실행 시점에 다른 구현체를 사용하는 기능 확장을 하고 싶었지만, 이를 위해 구현체를 사용하는 코드를 변경해야 했습니다. 따라서 OCP를 위반하는 설계였습니다.

         

      • 이 때 OCP의 각 부분을 이 맥락에서 다음과 같이 해석할 수 있습니다.

        • 기능 확장의 대상은 실행 시점에 실행되는 코드로, 이는 Repository 클래스의 인스턴스입니다.

        • 반면 소스 코드 수정과 관련된 부분은 컴파일 시점에 결정되는 Service 클래스의 코드 자체입니다.

        • 따라서 OCP를 만족하기 위해서는 "Service 인스턴스의 실행 시점에는 서로 다른 Repository 구현체 인스턴스를 사용하되, 컴파일 시점에 같은 Service

          클래스 코드를 사용하고 싶다"는 문제를 해결해야 합니다.

      • 다시 말하면 각 Service 인스턴스는 서로 다른 Repository 구현체 인스턴스에 의존하게 하되, Service 클래스는 구체적인 Repository 구현체 클래스에 의존하지 않도록 해야 합니다.

      • 그래서 DI를 사용해서 Service와 Repository 간의 동적인 의존관계와 정적인 의존관계를 서로 다르게 만듦으로써 OCP를 만족하는 설계를 구성하였다고 해석할 수 있습니다.

         

    • SRP 관점에서의 해석

      • 기존의 Service 코드에서는 Repository의 인스턴스를 생성하는 책임과 Repository를 사용하는 책임이 같이 있었습니다. 따라서 SRP를 위반하는 설계였습니다.

        • 이 때 Repository를 사용하는 로직은 Repository 인터페이스만 알고 구현체 클래스를 몰라도 문제가 없습니다.

        • 반면 Repository 인스턴스를 생성하려면 구체적인 Repository 구현체 클래스의 정보를 알아야 합니다.

        • 여기서 Repository 구현체를 변경하면, 원래는 구현체를 몰라도 되는 Repository 사용 로직이 인스턴스 생성 로직과 같은 클래스 안에 담겨 있어 수정의 대상이 되었습니다.

      • DI를 사용하여 수정된 설계에서는 객체들의 인스턴스를 생성하는 책임을 Config 클래스에 몰아줌으로써 Repository 구현체를 변경할 때에도 Service 코드는 영향을 받지 않고 Config 클래스만 수정하면 되도록 바뀌었습니다.

댓글을 작성해보세요.


채널톡 아이콘