인프런 커뮤니티 질문&답변

종운님의 프로필 이미지
종운

작성한 질문수

제미니의 개발실무 - 지속 성장 가능한 소프트웨어를 만들어가는 방법

Module & Outro

모듈에 대한 단방향 의존

해결된 질문

작성

·

418

·

수정됨

0

안녕하세요. 제미니님 이번에도 좋은 강의를 제공해주셔서 감사합니다.

모듈 분리에서 궁금한 내용이 있는데요.
제미니님이 제공해주신 PaymentAPI 와 DB 모듈을 별도로 했다고 했을 때 API 규격에 맞게 DB 모듈이 구현이 되어야 한다고 생각하고 있습니다. 즉, 해당 PaymentAPI 에서 제공하는 DB 접근에 대한 인터페이스를 DB 모듈이 구현하는 의존성 역전 원칙을 적용한 상황입니다.

하지만, 이 상황에서 단방향 모듈 참조를 하게 된다면 DB 모듈은 PaymentAPI 가 제공하는 인터페이스의 유무를 알 수가 없게 되는데요. 저는 위 문제에 대한 해결방법으로 두 가지가 떠오릅니다.

  1. 모듈 분리 시 API 모듈에 인터페이스를 만들고 DB 모듈 교체에 따른 새로운 구현체를 구현한다.(모듈 교체에 따라 이전 모듈에 대한 클래스 참조가 사라져 컴파일에러가 발생하게 되고 주석처리가 필요하다)

     

  2. 모듈 교체 시 이전 모듈에서 사용했던 인터페이스를 하위 모듈에서 똑같이 생성해주고 동일한 인터페이스를 참조하도록 하여 상위 모듈에는 변화를 주지 않는다.
    (변화가 최소화되지만 인터페이스가 많을 수록 구현도가 올라간다. 인터페이스를 동일하게 만들거라는 보장이 되어야 한다.)

1번 코드

paymentAPI {
    // implementation 'project:paymentDB'
    // implementation 'project:paymentDB2'

  interface CommandPort {
     fun save(command: PaymentCommand)
  }

  class PaymentDBImplV1 : CommandPort {
    
     override fun save(command: PaymentCommand) {
        paymentDB.saveV1(command); // paymentDB2 모듈 사용 시 주석 처리
     }
  }

  class PaymentDBImplV2 : CommandPort {
    
     override fun save(command: PaymentCommand) {
        paymentDB2.saveV2(command); // paymentDB1 모듈 사용 시 주석 처리
     }
  }
}

paymentDB {
  implementation 'A.DB'
}

paymentDB2 {
  implementation 'B.DB'
}

2번 코드

paymentAPI {
  // implementation 'project:paymentDB'
  // implementation 'project:paymentDB2'

  class PaymentAPILogic(val paymentDB: CommandPort){
    
     fun save(command: PaymentCommand) {
        paymentDB.save(command); // paymentDB2 모듈 사용 시 주석 처리
     }
  }
}

paymentDB {
  implementation 'A.DB'

  interface CommandPort {
     fun save(command: PaymentCommand)
  }

  class PaymentCommandImpl : CommandPort {
    override fun save(command: PaymentCommand) {
      DB.save(command);  
    }
  }
}

paymentDB2 {
  implementation 'B.DB'

  interface CommandPort {
     fun save(command: PaymentCommand)
  }

  class PaymentCommandImpl : CommandPort {
    override fun save(command: PaymentCommand) {
      DB.save(command);  
    }
  }
}

간단하게 코드를 작성하면 위와 같은 형태가 될 것 같습니다.

제미니님은 어떠한 방향으로 설계를 하시는지 혹은 제 질문에서 제가 잘 못 이해한 부분이 있어 이러한 방법으로 사고가 흘러가는지 말씀을 들어보고 싶습니다.

마지막으로 유튜브 및 인프런에서 귀한 지식과 귀한 시간을 제공해주셔서 항상 감사합니다! 

답변 2

2

제미니님의 프로필 이미지
제미니
지식공유자

제가 생각하는 구조는 1번 코드와 2번 코드 모두 아닙니다.

이 강의 설명 기준에서는 API 모듈이 자체적으로 디비 접근 의존성을 갖지 않고 DB 모듈을 사용하는게 핵심입니다.

그렇기에 인터페이스 없이 API 모듈이 DB 모듈을 의존하고 있는 형태가 전부 입니다.
(강의 중간에 인터페이스를 맞춘다는 얘기는 메소드 시그니처를 맞춘다는 의미로 이해하시면 됩니다.)

그래서 대략 이러한 구조라고 보시면 될 것 같습니다.

// Payments-API 모듈
//// 의존성
implementation(project(":storage:db-core"))

//// 코드
class PaymentAppender(val paymentRepository: PaymentRepository){
    fun append(user: User, payment: NewPayment): Payment {
        // ... 중략
    }
}


// db-core 모듈
//// 의존성
implementation(project("...:spring-boot-starter-data-jpa"))
/** 
 * 의존성이 변경된다면 
 * implementation(project("...:hyper-persistence"))
**/

//// 코드
class PaymentRepository(
    val paymentJpaRepository: PaymentJpaRepository
    /** 
     * 의존성이 변경된다면 
     * val paymentHyperRepository: PaymentHyperRepository
    **/    
){
    fun newPayment(paymentPersist: PaymentPersist): PaymentPersistResult {
        val saved = paymentJpaRepository.save(paymentCommand.toEntity())
        /** 
         * 의존성이 변경된다면 
         * val saved = paymentHyperRepository.insert(paymentCommand.toEntity())
        **/
        return saved.toResult()

    }
}

PaymentDB, PaymentDB2 가 별도 모듈이 있는게 아니라, DB-Core 안에서 변경을 대응하는 관점입니다.

--

소프트웨어는 단계적으로 진화해야 하기 때문에 이 강의에서는 초반 단계 정도의 기본적으로 생각해 볼거리를 중심으로 설명하고 있으므로 완전한 예제가 적절히 나오지 않습니다.

초반 단계 프로젝트 구조는 https://github.com/team-dodn/spring-boot-kotlin-template 를 참고하시면 됩니다.

 

실제 더 진화한 인터페이스(코드 상에서 쓰는 Interface를 의미)와 구현 방향성 관련해서는

직접적으로 설명 및 표현하고 있지 않지만, 좀 더 진화된 기준의 구조를 참고하시려면

아래 영상의 후반 부 모듈 구조에 대한 걸 참고하시면 좋을 것 같습니다.

https://youtu.be/RVO02Z1dLF8?si=FQdWoLdoBntwTfFS

 

또 이 아래 영상에서도 참고하실 부분이 있을 것 같습니다

https://youtu.be/p5ZMF2bpE6A?si=EE4JB5X2h6HS4F18

질문 남겨주셔서 감사합니다 :D

종운님의 프로필 이미지
종운
질문자

부족한 질문에도 좋은 답변을 해주셔서 감사합니다!

0

안녕하세요, 인프런 AI 인턴입니다.

님의 질문에 답변드리겠습니다. 제공해주신 정보를 바탕으로는 직접적으로 모듈의 단방향 의존에 대한 내용을 찾기 어려웠습니다. 그러나 모듈 분리와 의존성 관리에 관한 일반적인 설계 원칙을 바탕으로 논리적으로 답변을 드릴 수 있습니다.

모듈 분리와 의존성 역전 원칙을 적용하는 두 가지 방법에 대하여 언급하셨는데요, 각 방법의 장단점을 고려해 볼 필요가 있습니다.

1번 방법에서는 API 모듈에서 인터페이스를 정의하고, DB 모듈이 이를 구현함으로써 의존성을 역전시켜 주는 방식입니다. 이 경우, DB 모듈을 교체할 때마다 API 모듈의 구현체를 주석 처리하고 새로운 구현체를 사용해야하는 불편함이 있습니다. 그러나, 이 방식은 역할과 책임이 명확하게 분리되어 있어 시스템의 구조를 이해하기 쉽고, 기능 교체가 용이하다는 장점이 있습니다.

2번 방법에서는 API 모듈의 인터페이스를 하위 모듈에서 동일하게 생성하여 모듈 간의 결합도를 낮추는 방식입니다. 이 방식의 장점은 상위 모듈 변경 없이 하위 모듈만 교체하거나 수정할 수 있다는 점입니다. 그러나, 인터페이스가 많아질수록 중복 코드의 양이 증가하고, 인터페이스 동기화를 유지하는 데 더 많은 노력이 필요할 수 있습니다.

설계 방향 결정 시에는 유지보수성, 테스트 용이성, 결합도 및 응집도 같은 다양한 소프트웨어 엔지니어링 원칙을 고려해야 합니다. 프로젝트의 특성, 팀의 작업 방식, 기술 스택 등에 따라 가장 적합한 방법이 달라질 수 있습니다. 따라서, 구체적인 프로젝트 상황을 고려하여 가장 적절한 방법을 선택하는 것이 중요합니다.

님께서 제시한 두 방안 모두 각기 다른 상황에서 유용하게 적용될 수 있습니다. 결국, 프로젝트의 특성과 목표에 맞추어 가장 합리적인 설계 방향을 선택하는 것이 중요할 것 같습니다.

항상 유익한 질문 감사드립니다. 개발 과정에서 궁금한 점이 있으시면 언제든지 문의 주시기 바랍니다.

종운님의 프로필 이미지
종운

작성한 질문수

질문하기