Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[멀티 모듈 연동] 킬리안(박명규) 제출합니다 🚀 #104

Open
wants to merge 108 commits into
base: audrb96
Choose a base branch
from

Conversation

audrb96
Copy link

@audrb96 audrb96 commented Jun 27, 2024

브라운 안녕하세요!
미션 리뷰를 받기 전에, 실무를 하면서 멀티 모듈에 대한 고민을 많이 했습니다.
실무에 멀티 모듈 형식의 프로젝트를 진행하기 전에 미션에서 적용해보면서 공부하고 싶어서 적용하게 되었습니다.
미션 외의 리뷰이지만 브라운의 생각과 의견을 들어보고 싶습니다.  😄
구조 자체를 변경하다 보니 코드 수정이 매우 많습니다... 😅

모듈은 아래와 같이 되어있습니다.

  • 애플리케이션 모듈은 application:api 하위에 위치합니다.
  • DB 모듈은 core:core-db 하위에 위치합니다.
  • 도메인 모듈은 core:core-domain 하위에 위치합니다.

고민 했던 점은 아래와 같습니다.

  • DB의 변경점이 생기더라도 애플리케이션 모듈에는 변경사항이 없어야 한다.
    • 이 부분을 고민하다가 도메인 모듈에 도메인 레포지토리를 추상화 하고 DB 모듈에서 구현하도록 했습니다.
  • 프레젠테이션 레이어와 서비스 레이어 사이에도 추상화를 해야할까?
    • 제 생각에는 프레젠테이션 레이어는 변경될 수 있어도 서비스 레이어가 변경되는 일은 없다.
    • 프레젠테이션 레이어가 변경되더라도 서비스 레이어는 하위 레이어이기 때문에 어차피 변경을 하지 않아도 된다.
    • 고로 서비스 레이어와 프레젠테이션 레이어 사이는 추상화 할 필요가 없다.
  • core의 의미가 무엇일까?
    • 제가 생각하기에는 core는 공통적으로 다른 모듈에서 가져다 쓸 수 있는 모듈로 이해를 했습니다.
    • 그럼 db 접근 로직은 core에 있는게 맞을까?
      • domain과 db 접근 로직은 같은 도메인의 다른 어플리케이션이 추가되었을 때 하위 모듈로 쓸 수 있다고 판단했습니다.

kilian and others added 30 commits June 6, 2024 17:46
- "/" 요청 시 "/admin/index.html" 페이지 응답
- "/admin/reservation" 요청 시 "/admin/reservation-legacy.html" 페이지 응답
- reservation query controller 생성
- reservation service 생성
- response dto 생성
- reservation 도메인 객체 생성
- 패키지 구조 변경
- InMemory Reservation Repository 구현
- 예약 생성 API 개발
- id가 존재하는지 확인 후 save 방식 결정
- generatedKeyHolder.getKey()가 insert가 아닌 update가 수행되는경우 null을 리턴하게 되는 문제 수정
…agement/step1-2

# Conflicts:
#	src/main/java/roomescape/repository/ReservationRepository.java
kilian and others added 27 commits June 18, 2024 14:35
…agement/step2-2

# Conflicts:
#	src/main/java/roomescape/application/api/ReservationCommandApi.java
#	src/main/java/roomescape/application/api/ReservationQueryApi.java
#	src/main/java/roomescape/application/service/ReservationService.java
#	src/main/java/roomescape/repository/ReservationRepository.java
#	src/main/java/roomescape/repository/memory/InMemoryReservationRepository.java
#	src/main/java/roomescape/repository/mysql/MySQLJdbcReservationRepository.java
#	src/main/resources/db/data/init.sql
#	src/main/resources/db/schema/schema.sql
#	src/test/java/roomescape/repository/mysql/MySQLJdbcReservationRepositoryTest.java
…agement/step2-2

# Conflicts:
#	application/api/roomescape-reservation-api/src/main/java/roomescape/application/presentation/api/ReservationTimeCommandApi.java
#	application/api/roomescape-reservation-api/src/main/java/roomescape/application/presentation/api/ReservationTimeQueryApi.java
#	application/api/roomescape-reservation-api/src/main/java/roomescape/application/presentation/api/ThemeCommandApi.java
#	application/api/roomescape-reservation-api/src/main/java/roomescape/application/presentation/api/ThemeQueryApi.java
#	application/api/roomescape-reservation-api/src/main/java/roomescape/application/presentation/api/dto/request/CreateReservationTimeRequest.java
#	application/api/roomescape-reservation-api/src/main/java/roomescape/application/presentation/api/dto/request/CreateThemeRequest.java
#	application/api/roomescape-reservation-api/src/main/java/roomescape/application/presentation/api/dto/response/CreateReservationResponse.java
#	application/api/roomescape-reservation-api/src/main/java/roomescape/application/presentation/api/dto/response/CreateReservationTimeResponse.java
#	application/api/roomescape-reservation-api/src/main/java/roomescape/application/presentation/api/dto/response/CreateThemeResponse.java
#	application/api/roomescape-reservation-api/src/main/java/roomescape/application/presentation/api/dto/response/FindAllReservationTimesResponse.java
#	application/api/roomescape-reservation-api/src/main/java/roomescape/application/presentation/api/dto/response/FindAllReservationsResponse.java
#	application/api/roomescape-reservation-api/src/main/java/roomescape/application/presentation/api/dto/response/FindAllThemesResponse.java
#	application/api/roomescape-reservation-api/src/main/java/roomescape/application/service/ReservationCommandService.java
#	application/api/roomescape-reservation-api/src/main/java/roomescape/application/service/ReservationQueryService.java
#	application/api/roomescape-reservation-api/src/main/java/roomescape/application/service/ReservationTimeService.java
#	application/api/roomescape-reservation-api/src/main/java/roomescape/application/service/ThemeService.java
#	application/api/roomescape-reservation-api/src/main/java/roomescape/application/service/command/CreateReservationTimeCommand.java
#	application/api/roomescape-reservation-api/src/main/java/roomescape/application/service/command/CreateThemeCommand.java
#	application/api/roomescape-reservation-api/src/main/java/roomescape/application/service/command/DeleteReservationTimeCommand.java
#	application/api/roomescape-reservation-api/src/main/java/roomescape/application/service/command/DeleteThemeCommand.java
#	application/api/roomescape-reservation-api/src/main/java/roomescape/application/service/component/creator/ReservationCreator.java
#	application/api/roomescape-reservation-api/src/main/java/roomescape/application/service/component/reader/ReservationTimeReader.java
#	application/api/roomescape-reservation-api/src/main/java/roomescape/application/service/component/reader/ThemeReader.java
#	application/api/roomescape-reservation-api/src/main/java/roomescape/application/service/component/validator/CreateReservationTimeValidator.java
#	application/api/roomescape-reservation-api/src/main/java/roomescape/application/service/component/validator/CreateReservationValidator.java
#	application/api/roomescape-reservation-api/src/main/java/roomescape/application/service/component/validator/DeleteReservationTimeValidator.java
#	application/api/roomescape-reservation-api/src/main/resources/static/js/reservation-new.js
#	application/api/roomescape-reservation-api/src/main/resources/static/js/theme.js
#	application/api/roomescape-reservation-api/src/test/java/roomescape/presentation/api/ReservationCommandApiTest.java
#	application/api/roomescape-reservation-api/src/test/java/roomescape/presentation/api/ReservationTimeCommandApiTest.java
#	application/api/roomescape-reservation-api/src/test/java/roomescape/presentation/api/ReservationTimeQueryApiTest.java
#	application/api/roomescape-reservation-api/src/test/java/roomescape/presentation/api/ThemeCommandApiTest.java
#	application/api/roomescape-reservation-api/src/test/java/roomescape/presentation/api/ThemeQueryApiTest.java
#	application/api/roomescape-reservation-api/src/test/java/roomescape/presentation/api/config/MockMvcCharacterEncodingConfig.java
#	application/api/roomescape-reservation-api/src/test/java/roomescape/presentation/controller/AdminPageControllerTest.java
#	build.gradle
#	core/core-db/roomescape-mysql-jdbc-db/src/test/java/roomescape/repository/mysql/MySQLJdbcReservationRepositoryTest.java
#	core/core-db/roomescape-mysql-jdbc-db/src/test/java/roomescape/repository/mysql/MySQLJdbcReservationTimeRepositoryTest.java
#	core/core-db/roomescape-mysql-jdbc-db/src/test/java/roomescape/repository/mysql/MySQLJdbcThemeRepositoryTest.java
#	core/core-domain/roomescape-reservation-domain/src/main/java/roomescape/domain/reservation/ReservationView.java
#	core/core-domain/roomescape-reservation-domain/src/main/java/roomescape/domain/reservation/ReservationViews.java
#	core/core-domain/roomescape-reservation-domain/src/main/java/roomescape/domain/reservation/service/ClockHolder.java
#	core/core-domain/roomescape-reservation-domain/src/main/java/roomescape/domain/reservation/service/CreateReservationValidator.java
#	core/core-domain/roomescape-reservation-domain/src/main/java/roomescape/domain/reservation/service/SystemClockHolder.java
#	core/core-domain/roomescape-reservation-domain/src/main/java/roomescape/domain/reservation/vo/ReservationDate.java
#	core/core-domain/roomescape-reservation-domain/src/main/java/roomescape/domain/reservation/vo/ReservationId.java
#	core/core-domain/roomescape-reservation-domain/src/main/java/roomescape/util/ObjectUtils.java
#	core/core-domain/roomescape-reservation-domain/src/main/java/roomescape/util/validator/ObjectValidator.java
#	src/main/java/roomescape/application/api/ReservationCommandApi.java
#	src/main/java/roomescape/application/api/ReservationQueryApi.java
#	src/main/java/roomescape/application/api/dto/request/CreateReservationRequest.java
#	src/main/java/roomescape/application/mapper/ReservationEntityMapper.java
#	src/main/java/roomescape/application/mapper/ReservationMapper.java
#	src/main/java/roomescape/application/service/command/CreateReservationCommand.java
#	src/main/java/roomescape/domain/reservation/Reservation.java
#	src/main/java/roomescape/domain/reservation/vo/ReservationDateTime.java
#	src/main/java/roomescape/repository/ReservationRepository.java
#	src/main/java/roomescape/repository/entity/ReservationEntity.java
#	src/main/java/roomescape/repository/memory/InMemoryReservationRepository.java
#	src/main/java/roomescape/repository/mysql/MySQLJdbcReservationRepository.java
#	src/main/resources/db/data/init.sql
#	src/main/resources/db/schema/schema.sql
#	src/main/resources/static/js/time.js
#	src/main/resources/templates/admin/index.html
#	src/main/resources/templates/admin/reservation-legacy.html
#	src/main/resources/templates/admin/reservation.html
#	src/main/resources/templates/admin/time.html
#	src/test/java/roomescape/MissionStepTest.java
#	src/test/java/roomescape/application/api/ReservationQueryApiTest.java
#	src/test/java/roomescape/application/controller/AdminPageControllerTest.java
#	src/test/java/roomescape/controller/AdminPageControllerTest.java
#	src/test/java/roomescape/domain/reservation/vo/ReservationDateTimeTest.java
#	src/test/java/roomescape/repository/momory/InMemoryReservationRepositoryTest.java
Copy link
Contributor

@boorownie boorownie left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

안녕하세요 명규님!
제가 멀티모듈에 대한 경험이 없다보니 이 부분에서 큰 도움이 될 진 모르겠지만

코드 보시고 왜 이렇게 했는지 궁금한 부분이 있으면 말씀해주세요!
브라운과 의견을 나누고 싶습니다.

이 코멘트를 기반해서 궁금한 부분을 몇가지 적어보겠습니다.

  1. 모듈을 나누는 기준은 어떻게 설정해두셨는지?
  • 우선 멀티모듈로 설계를 하는 상황이 어떤 상황을 염두해두신건지 궁금합니다.
  • 사실 이정도 규모의 프로젝트에서는 모듈로 나누는게 오히려 구조적으로 유지보수를 힘들게 만드는 요인이 될 수 있습니다.
  • 어떤 이점을 고려해서 설계를 하셨고, 그럼 그 이점을 얻기 위해 어떤 기준으로 모듈을 구분하셨는지 궁금합니다.
  1. 도메인 모듈에는 도메인 로직이 하나도 없는데 의도하신건지 궁금합니다.
  • 도메인과 관련된 로직(생성을 포함한 기타 밸리데이션 등)이 도메인 모듈이 아닌 애플리케이션 모듈에 존재하는데 혹시 어떤 이유인지 궁금합니다.
  • 그리고 로직이 없다면 도메인의 존재의 의도가 궁금합니다.

@audrb96
Copy link
Author

audrb96 commented Jun 29, 2024

@boorownie
안녕하세요! 브라운!

일단 제가 코드를 작성할 때,
"한 달 교육기간 동안만 사용하고 방치할 코드를 작성할 꺼야" 가 아닌
"회사에서 오랫동안 유지 보수할 코드를 작성해야하는데 이번 기회에 학습 삼아 경험해 볼까?" 의 기준으로 작성했다는 것을 고려해주셨으면 합니다.

모듈을 나누는 기준은 어떻게 설정해두셨는지?

모듈을 나누는 기준은 아래와 같습니다.

  • 외부(ex. client 요청, persistence)와 내부(비즈니스)를 나누는 기준
  • 역할의 분리(ex. auth, logging)를 기준

우선 멀티모듈로 설계를 하는 상황이 어떤 상황을 염두해두신건지 궁금합니다.

일단 회사 코드를 경험하면서 계층간의 구분없이 레이어간 의존성이 양방향으로 되어있거나 순환되게 설계되어 코드에 수정사항이 있을 때
많은 레이어의 코드를 고려하고 수정해야하는 부분이 있었습니다.
예를 들면 이런 상황이 있을 수 있습니다.

  • persistence 레이어에서 비즈니스레이어의 코드를 호출, persistence > 비즈니스 의존
  • 비즈니스레이어에서 persistence 레이어의 코드를 호출, 비즈니스 > persistence

위와 같은 상황에서 어느 한 레이어의 코드 수정이 있다고 한다면 그 영향은 양방향으로 퍼져갈 것입니다.
이런 양방향 의존이 비정상적인 것은 맞습니다. 하지만 이런 비정상적인 의존을 가진 코드를 모든 개발자 평생 작성안할 것이다? 라는 보장이 없었습니다.
하지만 멀티 모듈에서는 의존성을 상위 레이어가 하위 레이어만 의존할 수 있도록 단방향으로만 컴파일 수준에서 강제할 수 있습니다.

그리고 모듈간의 독립적이라는 장점을 통해 의존성 관리를 작게 유지할 수 있습니다.
하나의 큰 서비스에서 의존성 수정은 많은 영향을 끼칠 수 있습니다.
예를 들어, 모듈로 분리가 되어있지 않은 한 서비스가
testImplementation 'io.rest-assured:rest-assured:5.3.1' 이와 같은 의존성이 있습니다.
여기서 만약 인수 테스트를 위해 사용했던 이 의존성이 버전이 수정되야 한다던가 어떤 개발자가 의존성을 정리하면서
안쓰고 있던 의존성 같은데? 라고 생각하고 지웠을 때 개발자가 고려해 봐야 하는 코드의 범위가 어느정도 일까요?
사실상 모든 코드를 다 살펴봐야합니다.

하지만 모듈 별로 필요한 의존성만 작게 나누어서 관리한다면 그 범위는 의존성을 가지고 있는 모듈만으로 한정지어지게 될 것입니다.

사실 이정도 규모의 프로젝트에서는 모듈로 나누는게 오히려 구조적으로 유지보수를 힘들게 만드는 요인이 될 수 있습니다.

맞습니다. 작은 규모의 프로젝트에서는 유지 보수를 힘들게 만드는 요인이 될 수 있습니다.
일단 제가 코드를 작성한 기준은 위와 같이 "회사에서 오랫동안 유지 보수할 코드를 작성해야하는데 이번 기회에 학습 삼아 경험해 볼까?"
였습니다.
그러면 일단 모듈로 나누지 말고 작성하다가 규모가 커지면 수정하면 되는거아니야? 라고 봤을 때 단순하게 코드 수준에서는 가능합니다.
하지만 아키텍처 수준에서의 변경은 규모가 커졌을 때 수정하는 리소스가 엄청나게 소모됩니다. 심지어는 아예 아키텍처를 변경할 수도 없는 수준이 되기도 합니다.
그런 의미에서 규모가 작더라도 아키텍처를 잡고 시작하는 편이 더 낫다고 판단되었습니다.

이게 실제 회사 프로젝트라도 초반에는 작은 프로젝트니까 아키텍처 고려하지 말고 그냥 단순하게 만들자. 라고 시작하고 개발 완료했다가.
나중에 프로젝트의 사업성이 보장되어서 이 프로젝트를 확장하거나 오랫동안 유지보수 해야된다 라고 했을 때도 아키텍처를 변경하는 리소스가 엄청날 것이라고 생각이 듭니다.

도메인 모듈에는 도메인 로직이 하나도 없는데 의도하신건지 궁금합니다.

의도했다기 보다는 아직 복잡한 도메인 로직이 없었습니다.
예약을 생성할 때 DB에 데이터가 있는지 확인하는 수준의 비즈니스만 있었습니다.
이 비즈니스 로직은 사실 완전한 도메인이 생성된 이후의 비즈니스 로직이 아니라,
완전한 도메인을 생성하기 위해 외부 모듈(persistence)에 의존할 수 밖에 없는 검증을 위한 비즈니스 로직이였습니다.
당연히 reservation을 생성할 때 외부의 의존이 필요없는 비즈니스 로직, 예를 들면 "예약 시간이 미래여야 한다."와 같은 로직은 도메인로직을 통해 작성했습니다.

일단 도메인에서 말하는 엔티티와 DB 에서 말하는 엔티티(JPA 엔티티는 이쪽에 가깝다고 생각합니다) 는 다르다고 생각합니다.
DB의 데이터와 무조건 똑같이 도메인이 형성될 수도 없을 뿐더러 사용하려는 목적이 다르다고 생각합니다.

도메인 엔티티는 비즈니스 로직의 핵심을 담고 있는 엔티티로 실제 로직에서 객체간의 상호작용을 통해 우리가 해결하고자 하는 문제를 해결하는 행동 중심의 엔티티입니다.
DB 엔티티는 데이터 베이스의 형상을 저장하고 구분 짓는 엔티티입니다.

두 엔티티를 같이 썼을 때 나올 수 있는 문제는 다음과 같습니다.

  • DB에 의존적인 엔티티 객체에서 모든 비즈니스 로직을 처리했을 때 DB의 변경 (ex. MySQL > MongoDB)으로 인해 그 영향이 비즈니스 수준까지 번지게 됩니다.
  • 개발자가 도메인을 통한 행동 위주의 사고 방식에서 DB가 어떤 값을 가지고 있느냐의 데이터 위주의 사고 방식으로 개발하게 됩니다.

거대한 서비스에서 DB 변경은 아예 고려안해도 되지 않나요?? DB 변경은 아예 불가능한 서비스일 수 있는데?

  • 거대한 서비스는 처음 부터 거대한 서비스 였을까요? 거대해질 수 밖에 없이 처음부터 설계된 건 아닐까요?

많은 서비스가 모놀리식에서 MSA로 바뀌어 가고있는 상황에서 DB 변경과 같은 일은 충분히 일어날 수 있다고 생각이 듭니다.

그럼 여기서 DB Enitity의 역할은 무엇일까요?
저는 "DB의 형상을 가지고 DB에 데이터를 전달하는 객체" 즉 DB 전용 DTO와 다를 것 없이 없다고 판단했습니다.

비즈니스 로직이 작을때는 DB에 접근하는 엔티티와 도메인에 접근하는 엔티티를 같은 것으로 사용하면 되지 않나요? 일 수 있습니다.
그럼 저는 이렇게 생각했습니다. 그럼 처음 부터 dto와 DB 엔티티는 왜 나누었을까요? 컨트롤러와 서비스는 왜 나누었을까요?
서비스 규모가 작다면 컨트롤러 안에 모든 비즈니스로직을 채워도 동작하는데는 아무 문제 없을 텐데요.

현재 도메인에 엄청나게 많은 로직이 있지는 않지만 이렇게 나누어 놓음으로써 핵심 비즈니스 로직이 추가 되었을 때 그 의미가 있다고 생각이 듭니다.
이렇게 설계 함으로써 코드를 보실 때 어플리케이션 모듈에서 DB Entity 객체와 전혀 관련이 없이 도메인 계층의 객체만 사용하고 있음을 확인 하실 수 있습니다.
그 의미는 DB 모듈이 JDBC가 아닌 JPA로 변경하는 앞으로의 미션에도 어플리케이션 코드의 수정없이 갈아 끼울 수 있게 된다고 볼 수 있습니다.
심지어는 앞으로의 미션에 MySQL이 아닌 NoSQL로 변경되었습니다~! 라는 미션이 추가된다고 하더라도 말입니다.

여기까지가 제 생각이였습니다! 너무 든든하게 국밥같은 느낌으로 작성했습니다...! 😅
브라운의 추가적인 의견이 있다면 더 들려주시면 감사하겠습니다! 🚀🚀🚀

@boorownie
Copy link
Contributor

안녕하세요 명규님! 든든한 국밥같은 말씀 잘 들었습니다!
앞서 드린 질문의 의도는 명규님의 설계의 의도를 파악하기 위함이고,
답변 주신 부분들을 통해 궁금증이 해소가 되었습니다.

모듈 분리의 의도를 잘 이해했습니다. 설계라는것도 두 관점으로 접근할 수 있는게, 하나는 점진적으로 필요할 때 마다 설계를 개선해나가는 방법이 있고, 또 다른 하나는 예측이 되는 수준까지는 그 수준에 도달하기 전에 미리 구축해 놓는 방법이 있다고 생각합니다.

물론 심적으로는 전자를 조금 더 선호하나, 제가 경험했던 실무 환경에서는 가용 리소스나 유지보수, 그리고 참여하는 팀원들의 선호도를 고려했을 때 후자를 선택해서 진행하는 경우가 많았습니다. 특히 경험이 많은 시니어의 탄탄한 설계는 생산 효율성을 극대화 하여 업무에 많은 도움을 주었습니다.
그런 면에서 회사에서 오랫동안 유지 보수할 코드를 작성해야하는데 이번 기회에 학습 삼아 경험해 볼까?라는 생각과 그럼 처음 부터 dto와 DB 엔티티는 왜 나누었을까요? 컨트롤러와 서비스는 왜 나누었을까요?라는 생각에 크게 공감을 했습니다.

한가지 아쉬운 점이 있다면 이번 미션이 모듈 설계나 멀티모듈을 경험하기 위해 설계된 미션이 아니다보니 실질적으로 이렇게 모듈 설계를 했을 때 얻어갈 수 있는 경험이 제한적이라는 점 입니다.
예를 들면, 현재 모듈 분리를 계층별로 진행해주셨는데요, 만약 도메인 복잡도가 높아질 경우 도메인별로 모듈을 분리해주는 것도 고려할 수 있을 것 입니다. 하지만 요구사항의 복잡도가 그 정도는 아니다보니 아쉬울 수 있을 것 같아요. DDD 세레나데 강의에서는 더 복잡한 요구사항 속에서 여러가지 시도를 해보실 수 있으니 참고해주세요.

그리고 앞서 드린 질문은 향후 미션 수행과 리뷰어와의 소통에 큰 영향을 줄 수 있다고 생각합니다.
저도 물론 멀티모듈에 대해 경험이 많지 않지만 다른 미션의 리뷰어 또한 비슷할 경우 이 부분에 대해서 드릴 코멘트가 많지 않을 수 있습니다.
지금 처럼 리뷰어와 소통하실 때 리뷰를 받고싶은 부분을 구체적으로 명시해주신다면 제한된 미션과 환경에서 그나마 조금이라도 얻고자 하는 리뷰를 받는데 도움이 될 것 같습니다!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants