
์ค๋์ ๋ฐฐ๋ฌ ์๋น์ค ํ๋ก์ ํธ์์ ๋ฆฌ๋ทฐ ๋๋ฉ์ธ์ DDD(Domain-Driven Design) ๊ตฌ์กฐ๋ก ๊ตฌํํ๋ค.
๋๋ฉ์ธ ์ค๊ณ๋ถํฐ ํ๋ ์ ํ
์ด์
๋ ์ด์ด๊น์ง, ํ์ ์ฝ๋์ ์ฐ๋ํ๋ฉด์ ๊ฒช์ ๋ฌธ์ ๋ค๊ณผ ํด๊ฒฐ ๊ณผ์ ์ ์ ๋ฆฌํ๋ค.
DDD ๊ตฌ์กฐ์์๋ ๋ค์ ์์๋ก ๊ตฌํํ๋ ๊ฒ์ด ์ข๋ค.
Domain โ Application โ Infrastructure โ Presentation
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Review extends BaseUserEntity {
@EmbeddedId
private ReviewId id;
@Embedded
private Reviewer reviewer; // ์์ฑ์ ์ ๋ณด
@Embedded
private ReviewOrderInfo info; // ์ฃผ๋ฌธ ์ ๋ณด
@Embedded
private ReviewContent content; // ๋ฆฌ๋ทฐ ๋ด์ฉ
}
@EmbeddedId๋ก ๋ณตํฉํค ๋์ ReviewId๋ฅผ ์๋ณ์๋ก ์ฌ์ฉํ๊ณ ,
๊ด๋ จ ์ ๋ณด๋ค์ @Embedded Value Object๋ก ๋ถ๋ฆฌํ๋ค.
๋ฆฌ๋ทฐ ์์ฑ/์์ /์ญ์ ์ ๊ถํ์ ์ฒดํฌํ๋ ๋ก์ง์ ๋๋ฉ์ธ ์์ ์บก์ํํ๋ค.
private void checkAuthority(UUID orderId, ReviewerCheck reviewerCheck, RoleCheck roleCheck) {
// ๊ด๋ฆฌ์๋ ํญ์ ํ์ฉ
if (roleCheck.hasRole(List.of("MASTER", "MANAGER"))) {
return;
}
// ์ฃผ๋ฌธ์/์์ฑ์ ํ์ธ
if (!reviewerCheck.check(id, orderId)) {
if (id == null) {
throw new CustomException(ErrorCode.INVALID_REVIEW_UNAUTHORIZED);
} else {
throw new CustomException(ErrorCode.INVALID_REVIEW_DETAIL_UNAUTHORIZED);
}
}
}
์ธํ๋ผ์ ์์กดํ์ง ์๋๋ก ๋๋ฉ์ธ ์๋น์ค๋ฅผ ์ธํฐํ์ด์ค๋ก ์ ์ํ๋ค.
public interface ReviewerCheck {
boolean check(ReviewId reviewId, UUID orderId);
}
public interface OrderInfoProvider {
ReviewOrderInfo getOrderInfo(UUID orderId);
}
public interface StoreRatingCalculator {
double getAverageRating(UUID storeId);
Long getReviewCount(UUID storeId);
}
ํ์์ Orderer ํด๋์ค์์ userId๊ฐ Long ํ์
์ด์๋ค.
public class Orderer {
private Long userId; // UUID๊ฐ ์๋ Long!
}
๋ฐ๋ผ์ Reviewer์ id๋ Long์ผ๋ก ๋ง์ถฐ์ผ ํ๋ค.
public class Reviewer {
@Column(name = "reviewer_id")
private Long id; // UUID โ Long์ผ๋ก ๋ณ๊ฒฝ
}
@Component
@RequiredArgsConstructor
public class ReviewerCheckImpl implements ReviewerCheck {
@Override
public boolean check(ReviewId reviewId, UUID orderId) {
Long currentUserId = userDetails.getId();
return reviewId == null
? isOrderer(orderId, currentUserId) // ์ ๋ฆฌ๋ทฐ: ์ฃผ๋ฌธ์ ํ์ธ
: isReviewer(reviewId, currentUserId); // ์์ : ์์ฑ์ ํ์ธ
}
}
QueryDSL์ ์ฌ์ฉํ๋ฉด @Entity ํด๋์ค๋ฅผ ๊ธฐ๋ฐ์ผ๋ก Qํด๋์ค๊ฐ ์๋ ์์ฑ๋๋ค.
./gradlew compileJava
๋น๋ ํ build/generated/sources/annotationProcessor/java/main/ ๊ฒฝ๋ก์ QReview.java๊ฐ ์๊ธด๋ค.
List<ReviewOrderItem>์ PostgreSQL์ jsonb ํ์
์ผ๋ก ์ ์ฅํ๋ ค๋ฉด JPA๊ฐ ์ง๋ ฌํ ๋ฐฉ๋ฒ์ ์์์ผ ํ๋ค.
@Converter
public class ReviewOrderItemsConverter
implements AttributeConverter<List<ReviewOrderItem>, String> {
private final ObjectMapper objectMapper = new ObjectMapper();
@Override
public String convertToDatabaseColumn(List<ReviewOrderItem> items) {
try {
return objectMapper.writeValueAsString(items);
} catch (Exception e) {
throw new RuntimeException("JSON ๋ณํ ์คํจ", e);
}
}
@Override
public List<ReviewOrderItem> convertToEntityAttribute(String json) {
try {
return objectMapper.readValue(json, new TypeReference<>() {});
} catch (Exception e) {
throw new RuntimeException("JSON ํ์ฑ ์คํจ", e);
}
}
}
๋ฆฌ๋ทฐ ์์ฑ/์์ /์ญ์ ์ ์์ ํ์ ์ ์ ๋ฐ์ดํธํ๊ธฐ ์ํด ๋๋ฉ์ธ ์ด๋ฒคํธ๋ฅผ ๋ฐํํ๋ค.
private void triggerEvent(UUID storeId) {
reviewRepository.flush(); // ํ์ ํ๊ท ๊ณ์ฐ ์ ๋ฆฌ๋ทฐ ๋จผ์ ๋ฐ์
Events.trigger(new ReviewScoreChangedEvent(
storeId,
calculator.getReviewCount(storeId),
calculator.getAverageRating(storeId)
));
}
๊ธฐ๋ฅ๋ณ๋ก ๋ธ๋์น๋ฅผ ๋ถ๋ฆฌํด์ ์์ ํ๋ค.
dev
โโโ feature/review-domain # ๋๋ฉ์ธ ๋ ์ด์ด
โโโ feature/review-application # ์ ํ๋ฆฌ์ผ์ด์
๋ ์ด์ด
โโโ feature/review-infrastructure # ์ธํ๋ผ ๋ ์ด์ด
โโโ feature/review-pressentation # ํ๋ ์ ํ
์ด์
๋ ์ด์ด
PR์ด merge ๋๊ธฐ ์ ์ ๋ค๋ฅธ ๋ธ๋์น ์ฝ๋๊ฐ ํ์ํ ๊ฒฝ์ฐ:
# ๋ค๋ฅธ ๋ธ๋์น ์ฝ๋ ๊ฐ์ ธ์ค๊ธฐ
git merge ๋ธ๋์น๋ช
# ๋๋ ํน์ ์ปค๋ฐ๋ง ๊ฐ์ ธ์ค๊ธฐ
git cherry-pick ์ปค๋ฐํด์
# ์ปค๋ฐ ์ทจ์ (๋ณ๊ฒฝ์ฌํญ์ ์ ์ง)
git reset HEAD~1
# ๋ณ๊ฒฝ์ฌํญ ์์ ์ ์ฅ
git stash
# ์ฌ๋ฐ๋ฅธ ๋ธ๋์น๋ก ์ด๋
git switch ์ฌ๋ฐ๋ฅธ๋ธ๋์น
# ๋ณ๊ฒฝ์ฌํญ ๊ฐ์ ธ์ค๊ธฐ
git stash pop
| ๋ฌธ์ | ์์ธ | ํด๊ฒฐ |
|---|---|---|
Could not autowire | ์ธํฐํ์ด์ค๋ง ์๊ณ ๊ตฌํ์ฒด ์์ | @Component ๋ถ์ธ Impl ํด๋์ค ์์ฑ |
QReview ์์ | ๋น๋ ์ ํจ | ./gradlew compileJava ์คํ |
| ํ์ ๋ถ์ผ์น | ํ์ ์ฝ๋์ ํ์ ๋ค๋ฆ | Long์ผ๋ก ํต์ผ |
switch ๋ฏธ์์ฑ | case ๋๋ฝ | ๋ชจ๋ enum case ์ถ๊ฐ |
new ํค์๋ ๋๋ฝ | ์คํ | () -> new CustomException(...) |
๋๋์ด ์ฒ์์ผ๋ก DDD ๊ตฌ์กฐ๋ฅผ ์ ์ฉํด์ ๋ฆฌ๋ทฐ ๋๋ฉ์ธ์ ๊ตฌํํด๋ดค๋ค.
์ธํฐํ์ด์ค์ ๊ตฌํ์ฒด๋ฅผ ๋ถ๋ฆฌํ๋ ๊ฒ, ๋๋ฉ์ธ ์์ ๋น์ฆ๋์ค ๋ก์ง์ ์บก์ํํ๋ ๊ฒ์ด ํต์ฌ์ด์๋ค.
ํ์ ์ฝ๋์ ์ฐ๋ํ๋ฉด์ ํ์
๋ถ์ผ์น ๊ฐ์ ์์์น ๋ชปํ ๋ฌธ์ ๋ ๊ฒช์์ง๋ง, ๋๋ถ์ ์ฝ๋๋ฅผ ๋ ๊น์ด ์ดํดํ ์ ์์๋ค.
์์ง๋ ์๋ฒฝํ ์ ๋ฆฌ๋์ง ์์ ๊ฒ ๊ฐ์์ ๋ค์ ํ๋ฒ ์ฝ๋๋ณด๋ฉด์ ๋ณต์ต ํ ์์ ์ด๋ค.