해결된 질문
작성
·
417
·
수정됨
0
안녕하세요. 제미니님 이번에도 좋은 강의를 제공해주셔서 감사합니다.
모듈 분리에서 궁금한 내용이 있는데요.
제미니님이 제공해주신 PaymentAPI 와 DB 모듈을 별도로 했다고 했을 때 API 규격에 맞게 DB 모듈이 구현이 되어야 한다고 생각하고 있습니다. 즉, 해당 PaymentAPI 에서 제공하는 DB 접근에 대한 인터페이스를 DB 모듈이 구현하는 의존성 역전 원칙을 적용한 상황입니다.
하지만, 이 상황에서 단방향 모듈 참조를 하게 된다면 DB 모듈은 PaymentAPI 가 제공하는 인터페이스의 유무를 알 수가 없게 되는데요. 저는 위 문제에 대한 해결방법으로 두 가지가 떠오릅니다.
모듈 분리 시 API 모듈에 인터페이스를 만들고 DB 모듈 교체에 따른 새로운 구현체를 구현한다.(모듈 교체에 따라 이전 모듈에 대한 클래스 참조가 사라져 컴파일에러가 발생하게 되고 주석처리가 필요하다)
모듈 교체 시 이전 모듈에서 사용했던 인터페이스를 하위 모듈에서 똑같이 생성해주고 동일한 인터페이스를 참조하도록 하여 상위 모듈에는 변화를 주지 않는다.
(변화가 최소화되지만 인터페이스가 많을 수록 구현도가 올라간다. 인터페이스를 동일하게 만들거라는 보장이 되어야 한다.)
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'
}
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 모듈의 인터페이스를 하위 모듈에서 동일하게 생성하여 모듈 간의 결합도를 낮추는 방식입니다. 이 방식의 장점은 상위 모듈 변경 없이 하위 모듈만 교체하거나 수정할 수 있다는 점입니다. 그러나, 인터페이스가 많아질수록 중복 코드의 양이 증가하고, 인터페이스 동기화를 유지하는 데 더 많은 노력이 필요할 수 있습니다.
설계 방향 결정 시에는 유지보수성, 테스트 용이성, 결합도 및 응집도 같은 다양한 소프트웨어 엔지니어링 원칙을 고려해야 합니다. 프로젝트의 특성, 팀의 작업 방식, 기술 스택 등에 따라 가장 적합한 방법이 달라질 수 있습니다. 따라서, 구체적인 프로젝트 상황을 고려하여 가장 적절한 방법을 선택하는 것이 중요합니다.
님께서 제시한 두 방안 모두 각기 다른 상황에서 유용하게 적용될 수 있습니다. 결국, 프로젝트의 특성과 목표에 맞추어 가장 합리적인 설계 방향을 선택하는 것이 중요할 것 같습니다.
항상 유익한 질문 감사드립니다. 개발 과정에서 궁금한 점이 있으시면 언제든지 문의 주시기 바랍니다.
부족한 질문에도 좋은 답변을 해주셔서 감사합니다! ☺