SeatValidator
모듈러 모놀리식을 처음 적용하면서 가장 헷갈렸던 것은 인터페이스를 정의하는 위치, 구현하는 위치, 사용하는 위치였다.
reservation 모듈 ─ 정의: SeatValidator, MemberValidator (reservation.port)
└ 사용: ReservationService — 인터페이스만 호출
cafe 모듈 ─ 구현: CafeSeatValidatorImpl (cafe.service)
member 모듈 ─ 구현: MemberValidatorImpl (member.service)
- 정의와 사용은 같은 모듈에 있다. 필요한 것을 선언한 쪽이 그것을 쓴다.
- 구현만 다른 모듈에 있다. cafe와 member는 자기 도메인 데이터를 써서 답만 해준다.
- 구현체가 인터페이스보다 하위에 있으니 의존도 아래로 흐를 것 같지만, 실제로는 구현체(아래)에서 인터페이스(위)로 흐른다.
package com.studyhub.reservation.port; // reservation이 필요한 것을 스스로 선언
public interface SeatValidator {
SeatValidationResult validate(Long cafeId, Long seatId);
}
package com.studyhub.cafe.service; // cafe가 reservation의 요구사항을 구현
...
@Component
@RequiredArgsConstructor
public class SeatValidatorImpl implements SeatValidator {
private final SeatRepository seatRepository;
@Override
public SeatValidationResult validate(Long cafeId, Long seatId) {
Seat seat = seatRepository.findById(seatId).orElse(null);
if (seat == null) {
return SeatValidationResult.NOT_FOUND;
}
if (!seat.getCafe().getId().equals(cafeId)) {
return SeatValidationResult.CAFE_MISMATCH;
}
if (seat.getStatus() == SeatStatus.DISABLED) {
return SeatValidationResult.DISABLED;
}
return SeatValidationResult.VALID;
}
}
reservation이 Validator를 정의하는 이유
- 존재하지 않는 좌석에 예약이 생성되면 데이터 무결성이 깨지므로 검증이 필요하다.
- 그런데 모듈 경계 원칙상 reservation은 SeatRepository도 MemberRepository도 직접 참조할 수 없다.
- 검증은 필요하지만 구현은 직접 할 수 없다는 모순을 인터페이스로 질문만 던지고 답은 해당 도메인이 하도록 하였다.
구현체가 SeatRepository를 참조해도 되는 이유
- 처음에는
CafeSeatValidatorImpl이SeatRepository를 참조하는 것이 모듈 간 참조가 아닌지 헷갈렸다. SeatRepository는 cafe 모듈 소속이므로 같은 모듈이다. 모듈 소속은 패키지 최상위 이름으로 판단한다.
연관관계 매핑 가능 여부가 모듈 판단 기준이 아닌 이유
- Seat와 Cafe가
@ManyToOne을 맺을 수 있는 것은 애초에 두 엔티티를 같은 모듈로 설계했기 때문이다. - "연관관계가 있어서 같은 모듈"이 아니라 "같은 모듈로 정했기 때문에 연관관계가 허용"
MemberValidator
package com.studyhub.cafe.port;
public interface OwnerValidator {
OwnerValidationResult validate(Long memberId);
}
package com.studyhub.reservation.port;
public interface MemberValidator {
MemberValidationResult validate(Long memberId);
}
MemberValidator 인터페이스를 따로 만든 이유
- member 모듈은 이미
OwnerValidator를 구현하고 있어서 회원 조회 로직이 중복되는 것처럼 보였다. - 하지만 두 인터페이스가 던지는 질문이 다르다.
OwnerValidator는 "이 회원이 이 카페의 사장인가",MemberValidator는 "이 회원이 존재하고 활성상태인가"이다. - 하나로 합치는 경우 한쪽 요구사항 변경 시 다른 쪽 구현도 영향을 받는다.
실제 중복이 생기는 지점
@Component
@RequiredArgsConstructor
public class OwnerValidatorImpl implements OwnerValidator {
private final MemberService memberService;
@Override
public OwnerValidationResult validate(Long memberId) {
Member member = memberService.findById(memberId).orElse(null);
if (member == null) {
return OwnerValidationResult.NOT_FOUND;
}
if (member.getStatus() == MemberStatus.WITHDRAWN) {
return OwnerValidationResult.WITHDRAWN;
}
if (member.getRole() != Role.OWNER) {
return OwnerValidationResult.NOT_OWNER_ROLE;
}
return OwnerValidationResult.VALID;
}
}
Component
@RequiredArgsConstructor
public class MemberValidatorImpl implements MemberValidator {
private final MemberRepository memberRepository;
@Override
public MemberValidationResult validate(Long memberId) {
Member member = memberRepository.findById(memberId).orElse(null);
if (member == null) {
return MemberValidationResult.NOT_FOUND;
}
if (member.getStatus() == MemberStatus.WITHDRAWN) {
return MemberValidationResult.WITHDRAWN;
}
return MemberValidationResult.VALID;
}
}
- 두 인터페이스를 구현한
OwnerValidatorImpl과MemberValidatorImpl은 회원 조회와 탈퇴 확인이 겹친다. - 두 구현체 모두 member 모듈 소속이므로 이 중복은 모듈 내부에서 공용 컴포넌트로 뽑아내면 된다.(추후 리팩토링 예정)
구현체 네이밍을 Impl로 통일한 이유
MemberOwnerValidatorImpl ← 모듈명 + Impl
CafeSeatValidatorImpl ← 모듈명 + Impl
MemberValidatorImpl ← Impl
- 처음에는 구현체가 서로 다른 네이밍 규칙을 사용했다.
- 모듈명 + Impl로 통일하면
MemberValidator의 구현체가MemberMemberValidatorImpl이 된다. - 패키지 경로에 이미 모듈명이 드러나므로 클래스명에 다시 넣을 필요가 없다고 판단하였다.
CafeInfo
CafeInfo를 reservation.port에 둔 이유
package com.studyhub.reservation.port;
...
@Getter
@RequiredArgsConstructor
public class CafeInfo {
private final String cafeName;
private final String seatNumber;
public static CafeInfo of(String cafeName, String seatNumber) {
return new CafeInfo(cafeName, seatNumber);
}
}
- cafe 모듈에 두면 reservation이 cafe 클래스를 import하게 되어 모듈 경계가 깨진다.
단점
- Reservation이 Cafe를 연관관계로 맺고 있었다면
join fetch한 번으로 N+1문제를 해결할 수 있다. - ID만 참조하므로 포트로 따로 조회해야 하고, 그 포트를 단건으로 설계하면 반복 호출이 된다.
- 모듈 경계를 지키는 대신 조인으로 해결되는 것을 애플리케이션에서 조합해야 한다는 단점이 존재한다.
'개발진행목록 > 스터디카페 통합 조회 플랫폼' 카테고리의 다른 글
| [studyhub] 시간대 예약 기능 구현 (0) | 2026.08.27 |
|---|---|
| [studyhub] 카페 등록 구현 (0) | 2026.08.18 |
| [studyhub] Member 설계와 로그인, 토큰 재발급 구현 (0) | 2026.08.13 |
| [studyhub] Custom Error Code 구현 (0) | 2026.07.22 |
| [studyhub] JWT 기반 인증/인가 구현 (0) | 2026.07.15 |