[Refactor/#14] 도메인 모듈 패키지 구조·경계 검증 방식 변경 및 infrastructure:db 설정 골격 - #15
Conversation
…경계는 ArchUnit으로 강제
|
|
| private String studentId; | ||
| private String name; | ||
|
|
||
| public static Member of(Long id, String studentId, String name) { |
There was a problem hiding this comment.
여기서 왜 생성자를 private으로 제한하고 of()로 생성하도록 했는지가 궁금합니다. 지금은 단순히 생성자를 그대로 호출하는 형태인 거 같은데 추후에 이 부분에 객체 생성에 대한 검증이나 로직이 추가적으로 들어가는 걸까요?
There was a problem hiding this comment.
생성자를 감추고 static 메서드로 객체를 생성하는 패턴이 정적 팩토리 메서드 패턴입니다. 말씀하신 것처럼 로직을 추가할 수도 있지만, 메서드의 이름으로 생성되는 경로를 구분할 수도 있습니다.
이 케이스 같은 경우에는 단순히 생성자를 만드는 것과 별 차이가 없지만 다른 곳에서 사용할 경우를 대비해 정적 팩토리 메서드 패턴으로 객체를 생성하도록 통일했습니다.
|
| @Getter | ||
| @EqualsAndHashCode |
There was a problem hiding this comment.
여기서 EqualsAndHashCode 사용한 건
record일 때는 Lombok 없이도 언어 차원에서 자동으로 만들어줬지만 일반 클래스로 바꾸면서 @Getter + @EqualsAndHashCode 조합 사용해서 record와 비슷하게 재현한 거 맞나요?
There was a problem hiding this comment.
값을 표현하는 vo의 형태로 두었다고 생각해주시면 될 거 같습니다.
equals()와 `hashCode()'를 왜 쓰는지에 대한 것도 한 번 찾아보시면 좋을 거 같아요~
| springBootStarterDataJpaTest = { module = "org.springframework.boot:spring-boot-starter-data-jpa-test" } | ||
|
|
||
| # Spring Modulith — verify() boundary checks only, bootstrap test scope | ||
| springModulithApi = { module = "org.springframework.modulith:spring-modulith-api" } |
There was a problem hiding this comment.
여기 이건 @ApplicationModule(type = OPEN) 어노테이션 컴파일 하기 위해 추가한걸로 이해했습니다!!
사소한 거긴 한데, SpringModulithApi 자체는 member, event, welfare, internal 4개 도메인 모듈의 build.gradle.kts에 compileOnly로 들어가있는데, 이거는 bootstrap test scope가 맞는지 궁금합니다! 만약 아니라면, 주석을 이 줄 밑으로 내리는 게 더 정확할 거라고 생각합니다..!
There was a problem hiding this comment.
원래는 bootstrap test에서만 verify() 사용을 위해서 추가했던 건데, 말씀하신대로 패키지를 오픈하기 위해서 @ApplicationModule을 사용하다보니 생각보다 많은 부분에서 사용하게 되었습니다.
사실 좀 더 고민을 해봐야겠지만 지금 spring modulith 자체를 걷어내는 방법도 고민 중에 있어서 이건 추후에 좀 더 고민해보겠습니다!
| @Column(name = "deleted_at") | ||
| private LocalDateTime deletedAt; | ||
| @Column(name = "is_deleted", nullable = false) | ||
| private boolean isDeleted = false; |
There was a problem hiding this comment.
deleted_at 대신 updatedAt 사용하여 중복저장 안하고, 삭제 여부 심플하게 변경하는 거 확인했습니다!
그리고 저장 크기 차이에서 인덱스 크기가 작아져 조회가 빨라진다는 장점이 있는 걸로 이해했는데 이부분 맞는지 확인 부탁드립니다!
There was a problem hiding this comment.
이건 말씀하신 부분과는 조금 많이 다릅니다...
MySQL은 유니크 인덱스에서 NULL을 서로 다른 값으로 취급합니다. 그래서 예를 들어 "삭제되지 않은 행 중에는 email 중복 불가"를 UNIQUE (email, deleted_at)으로 걸어도, 살아있는 행은 deleted_at이 전부 NULL이라 제약이 전혀 동작하지 않습니다.
대신 is_deleted는 NOT NULL이라 UNIQUE (email, is_deleted)가 의도대로 걸리게 됩니다.
관련 내용 한 번 더 찾아보시면 좋을 거 같아요~
[Feat/#13] Flyway 마이그레이션·JPA 엔티티 26개 추가
#️⃣연관된 이슈
🎯 해결하려는 문제가 무엇인가요?
엔티티 26개를 추가하는 #13에 앞서, 그 코드가 올라갈 도메인 모듈의 구조와 경계 검증 방식을 먼저 정한다. 아울러
infrastructure:db가 실제 MySQL에 붙을 수 있는 설정 골격을 만든다. 이 PR은 기존 member 코드에만 적용하며 새 테이블·엔티티는 포함하지 않는다.❓ 왜 해결해야 하나요?
internal)은 도메인·계층 단위로 패키지를 나누고 싶은 요구와 맞지 않았다.impl하나를 감추려고 공개 패키지마다@NamedInterface를 달아야 한다. 도메인이 늘수록 부담이 커진다.⭐ 어떻게 해결했나요?
{module}.domain.{도메인}.{domain|repository|service|service.impl}. 도메인 객체만 있는 도메인도 계층 패키지를 생략하지 않는다.@ApplicationModule(type = OPEN)으로 두고,service.impl차단은 ArchUnit 테스트DomainImplAccessTests가 담당한다. 규칙은 두 개 —..service.impl..은 같은 패키지 안에서만 참조 가능,..service.impl..에 public 클래스 금지.@Getter+@EqualsAndHashCode+ private 전체 생성자 +static of(...)). setter는 Lombok으로 만들지 않고 필요한 필드에만 수동으로 둔다.deleted_at DATETIME→is_deleted TINYINT(1)(BaseSoftDeleteEntity.isDeleted).application-infrastructure-db.yml(MySQL,ddl-auto: validate, Flyway),.env.example의DB_*,BaseCreatedTimeEntity, db 모듈의 도메인 모듈 의존.contextLoads를 제거했다. 남는 테스트는ModularityTests(모듈 간 순환·의존)와DomainImplAccessTests(impl 경계)다.🧩 이 PR의 한계 & 트레이드오프
verify()의 역할이 모듈 간 순환·의존 검사로 줄고, 모듈 내부 경계는 ArchUnit이 맡는다.contextLoads가 없어져 Entity 매핑 오류는 애플리케이션 기동(bootRun)에서만 드러난다.coding-style.md2-1절(도메인 객체 record)과flyway-migration.md(deleted_at예시)는 이 PR에서 고치지 않았다. 별도 docs 작업이 필요하다.⛓️ 기존 기능에 미치는 영향
Member가 record에서 클래스로 바뀌어 접근자가id()→getId(), 생성이new Member(...)→Member.of(...)로 바뀐다. 현재 호출처는MemberJpaEntity뿐이다.Member.studentNo→studentId로 이름을 바꿨다.MemberService등 member 타입의 패키지가 바뀐다. 현재 다른 모듈에서 참조하는 곳은infrastructure:db뿐이며 함께 수정했다.bootstrap/application.yaml이application-infrastructure-db.yml을 import하므로 기동 시DB_URL/DB_USERNAME/DB_PASSWORD환경변수가 필요하다.🔀 Edge Case & 실패 시나리오
service.impl에 public 클래스를 두거나 다른 패키지에서 참조하면DomainImplAccessTests가 실패한다. 실험으로 확인했다(임시 파일은 제거).package-info.java(OPEN)를 빠뜨리면 그 모듈의 하위 패키지 참조가verify()에서 실패한다.📋 검토한 대안과 선택 이유
@NamedInterface를 공개 계층 패키지마다 선언: 정확하지만 도메인당package-info.java3개가 늘고 하나만 빠져도verify()가 깨진다. 처음 이 방식으로 구현했다가 OPEN + ArchUnit으로 바꿨다.internal) 유지: 어노테이션이 필요 없지만 도메인·계층 단위 패키지를 포기해야 한다.ddl-auto: create-drop유지: Entity 매핑 검증에는 유용하지만 DB를 쓰는 테스트를 두지 않기로 한 방침과 어긋나 제거했다.💬 리뷰 포인트
[r]architecture.md4-3절 — 패키지 구조와 OPEN + ArchUnit 방식에 동의하는지[r]DomainImplAccessTests규칙 1이 같은 도메인의service패키지에서impl을 참조하는 것도 막는데, 이 강도가 적절한지[c]도메인 객체를 record 대신 클래스로 두는 결정(coding-style.md2-10·2-11절과의 정합)[c]is_deletedboolean으로의 변경