๐Ÿ›ต ๋ฐฐ๋‹ฌ ์„œ๋น„์Šค ๋ฆฌ๋ทฐ ๋„๋ฉ”์ธ ๊ตฌํ˜„๊ธฐ โ€” DDD ๊ตฌ์กฐ๋กœ ์ฒ˜์Œ๋ถ€ํ„ฐ ๋๊นŒ์ง€

ํ•œ์†Œ์—ฐยท2026๋…„ 3์›” 10์ผ

๋‚ด์ผ๋ฐฐ์›€์บ ํ”„

๋ชฉ๋ก ๋ณด๊ธฐ
8/21
post-thumbnail

๋“ค์–ด๊ฐ€๋ฉฐ

์˜ค๋Š˜์€ ๋ฐฐ๋‹ฌ ์„œ๋น„์Šค ํ”„๋กœ์ ํŠธ์—์„œ ๋ฆฌ๋ทฐ ๋„๋ฉ”์ธ์„ DDD(Domain-Driven Design) ๊ตฌ์กฐ๋กœ ๊ตฌํ˜„ํ–ˆ๋‹ค.
๋„๋ฉ”์ธ ์„ค๊ณ„๋ถ€ํ„ฐ ํ”„๋ ˆ์  ํ…Œ์ด์…˜ ๋ ˆ์ด์–ด๊นŒ์ง€, ํŒ€์› ์ฝ”๋“œ์™€ ์—ฐ๋™ํ•˜๋ฉด์„œ ๊ฒช์€ ๋ฌธ์ œ๋“ค๊ณผ ํ•ด๊ฒฐ ๊ณผ์ •์„ ์ •๋ฆฌํ•œ๋‹ค.


1. DDD ๋ ˆ์ด์–ด ๊ตฌํ˜„ ์ˆœ์„œ

DDD ๊ตฌ์กฐ์—์„œ๋Š” ๋‹ค์Œ ์ˆœ์„œ๋กœ ๊ตฌํ˜„ํ•˜๋Š” ๊ฒƒ์ด ์ข‹๋‹ค.

Domain โ†’ Application โ†’ Infrastructure โ†’ Presentation
  • Domain: ๋น„์ฆˆ๋‹ˆ์Šค ํ•ต์‹ฌ ๋กœ์ง (Entity, Value Object, Domain Service ์ธํ„ฐํŽ˜์ด์Šค)
  • Application: ์œ ์ฆˆ์ผ€์ด์Šค ์ฒ˜๋ฆฌ (Service, DTO)
  • Infrastructure: ์™ธ๋ถ€ ๊ธฐ์ˆ  ๊ตฌํ˜„์ฒด (Repository ๊ตฌํ˜„์ฒด, Domain Service ๊ตฌํ˜„์ฒด)
  • Presentation: ์š”์ฒญ/์‘๋‹ต ์ฒ˜๋ฆฌ (Controller, Request/Response DTO)

2. Domain ๋ ˆ์ด์–ด ์„ค๊ณ„

Review ์—”ํ‹ฐํ‹ฐ

@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);
        }
    }
}

Domain Service ์ธํ„ฐํŽ˜์ด์Šค

์ธํ”„๋ผ์— ์˜์กดํ•˜์ง€ ์•Š๋„๋ก ๋„๋ฉ”์ธ ์„œ๋น„์Šค๋ฅผ ์ธํ„ฐํŽ˜์ด์Šค๋กœ ์ •์˜ํ–ˆ๋‹ค.

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);
}

3. Infrastructure ๋ ˆ์ด์–ด โ€” ํŒ€์› ์ฝ”๋“œ ์—ฐ๋™

ํ•ต์‹ฌ ํฌ์ธํŠธ: ํƒ€์ž… ๋ถˆ์ผ์น˜ ๋ฌธ์ œ

ํŒ€์›์˜ 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์œผ๋กœ ๋ณ€๊ฒฝ
}

ReviewerCheckImpl

@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 ์‚ฌ์šฉ โ€” QReview ์ž๋™ ์ƒ์„ฑ

QueryDSL์„ ์‚ฌ์šฉํ•˜๋ฉด @Entity ํด๋ž˜์Šค๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ Qํด๋ž˜์Šค๊ฐ€ ์ž๋™ ์ƒ์„ฑ๋œ๋‹ค.

./gradlew compileJava

๋นŒ๋“œ ํ›„ build/generated/sources/annotationProcessor/java/main/ ๊ฒฝ๋กœ์— QReview.java๊ฐ€ ์ƒ๊ธด๋‹ค.


4. jsonb ํƒ€์ž… ๋งคํ•‘ ๋ฌธ์ œ

List<ReviewOrderItem>์„ PostgreSQL์˜ jsonb ํƒ€์ž…์œผ๋กœ ์ €์žฅํ•˜๋ ค๋ฉด JPA๊ฐ€ ์ง๋ ฌํ™” ๋ฐฉ๋ฒ•์„ ์•Œ์•„์•ผ ํ•œ๋‹ค.

AttributeConverter ์‚ฌ์šฉ

@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);
        }
    }
}

5. ์ด๋ฒคํŠธ ๋ฐœํ–‰ โ€” ํ‰์  ์—…๋ฐ์ดํŠธ

๋ฆฌ๋ทฐ ์ž‘์„ฑ/์ˆ˜์ •/์‚ญ์ œ ์‹œ ์ƒ์  ํ‰์ ์„ ์—…๋ฐ์ดํŠธํ•˜๊ธฐ ์œ„ํ•ด ๋„๋ฉ”์ธ ์ด๋ฒคํŠธ๋ฅผ ๋ฐœํ–‰ํ–ˆ๋‹ค.

private void triggerEvent(UUID storeId) {
    reviewRepository.flush(); // ํ‰์  ํ‰๊ท  ๊ณ„์‚ฐ ์ „ ๋ฆฌ๋ทฐ ๋จผ์ € ๋ฐ˜์˜

    Events.trigger(new ReviewScoreChangedEvent(
            storeId,
            calculator.getReviewCount(storeId),
            calculator.getAverageRating(storeId)
    ));
}

6. Git ๋ธŒ๋žœ์น˜ ์ „๋žต

๊ธฐ๋Šฅ๋ณ„๋กœ ๋ธŒ๋žœ์น˜๋ฅผ ๋ถ„๋ฆฌํ•ด์„œ ์ž‘์—…ํ–ˆ๋‹ค.

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

7. ์˜ค๋Š˜ ๊ฒช์€ ์ฃผ์š” ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ…

๋ฌธ์ œ์›์ธํ•ด๊ฒฐ
Could not autowire์ธํ„ฐํŽ˜์ด์Šค๋งŒ ์žˆ๊ณ  ๊ตฌํ˜„์ฒด ์—†์Œ@Component ๋ถ™์ธ Impl ํด๋ž˜์Šค ์ƒ์„ฑ
QReview ์—†์Œ๋นŒ๋“œ ์•ˆ ํ•จ./gradlew compileJava ์‹คํ–‰
ํƒ€์ž… ๋ถˆ์ผ์น˜ํŒ€์› ์ฝ”๋“œ์™€ ํƒ€์ž… ๋‹ค๋ฆ„Long์œผ๋กœ ํ†ต์ผ
switch ๋ฏธ์™„์„ฑcase ๋ˆ„๋ฝ๋ชจ๋“  enum case ์ถ”๊ฐ€
new ํ‚ค์›Œ๋“œ ๋ˆ„๋ฝ์˜คํƒ€() -> new CustomException(...)

๋งˆ์น˜๋ฉฐ

๋“œ๋””์–ด ์ฒ˜์Œ์œผ๋กœ DDD ๊ตฌ์กฐ๋ฅผ ์ ์šฉํ•ด์„œ ๋ฆฌ๋ทฐ ๋„๋ฉ”์ธ์„ ๊ตฌํ˜„ํ•ด๋ดค๋‹ค.
์ธํ„ฐํŽ˜์ด์Šค์™€ ๊ตฌํ˜„์ฒด๋ฅผ ๋ถ„๋ฆฌํ•˜๋Š” ๊ฒƒ, ๋„๋ฉ”์ธ ์•ˆ์— ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ์บก์Аํ™”ํ•˜๋Š” ๊ฒƒ์ด ํ•ต์‹ฌ์ด์—ˆ๋‹ค.
ํŒ€์› ์ฝ”๋“œ์™€ ์—ฐ๋™ํ•˜๋ฉด์„œ ํƒ€์ž… ๋ถˆ์ผ์น˜ ๊ฐ™์€ ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ๋ฌธ์ œ๋„ ๊ฒช์—ˆ์ง€๋งŒ, ๋•๋ถ„์— ์ฝ”๋“œ๋ฅผ ๋” ๊นŠ์ด ์ดํ•ดํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค.
์•„์ง๋„ ์™„๋ฒฝํžˆ ์ •๋ฆฌ๋˜์ง„ ์•Š์€ ๊ฒƒ ๊ฐ™์•„์„œ ๋‹ค์‹œ ํ•œ๋ฒˆ ์ฝ”๋“œ๋ณด๋ฉด์„œ ๋ณต์Šต ํ•  ์˜ˆ์ •์ด๋‹ค.

profile
์•ˆ ๋˜๋ฉด ๋  ๋•Œ๊นŒ์ง€

0๊ฐœ์˜ ๋Œ“๊ธ€