스프링부트 DDD를 배우며 깨달은 것들

한소연·2026년 3월 8일

내일배움캠프

목록 보기
6/21
post-thumbnail

스파르타 심화 과정에서 모놀리식 DDD 구조를 처음 접하며 배운 것들을 정리했습니다.


목차

  1. 3 레이어드 vs 모놀리식 DDD
  2. 유스케이스란?
  3. 트랜잭션과 롤백
  4. JPA란?
  5. 정적 팩토리 메서드
  6. 비즈니스 로직이란?
  7. 느슨한 결합과 이벤트 발행
  8. Value Object와 @Embedded
  9. 소프트 딜리트와 @SQLRestriction
  10. 인덱스란?

1. 3 레이어드 vs 모놀리식 DDD

처음 스프링부트를 배울 때는 3 레이어드 아키텍처로 배웠다.

3 레이어드          모놀리식 DDD
──────────────────────────────
Controller    →   Controller  (그대로)
Service       →   UseCase     (흐름만 담당)
              →   Domain      (핵심 규칙 담당)
Repository    →   Repository  (그대로)

결국 DDD는 3 레이어드에서 Service를 UseCase와 Domain으로 더 세분화한 것이다.
처음부터 DDD로 배우지 않고 3 레이어드로 전체 흐름을 익힌 뒤 DDD로 넘어오는 게 맞는 순서였다.


2. 유스케이스란?

"사용자가 시스템으로 하는 행동 하나"

라면 끓이기에 비유하면:

라면 끓이기 유스케이스:
  1. 물을 끓인다
  2. 스프를 넣는다
  3. 면을 넣는다
  4. 계란을 넣는다

코드로 보면:

@Service
@Transactional
public class ReviewUseCase {

    public void createReview(ReviewRequestDto request, Long userId) {
        // 1. 주문 조회
        // 2. 배달 완료 확인
        // 3. 주문자 일치 확인
        // 4. 리뷰 생성
        // 5. DB 저장
    }
}

순서가 곧 비즈니스 규칙이다. UseCase는 이 흐름을 조율하는 역할만 한다.


3. 트랜잭션과 롤백

"여러 DB 작업을 하나로 묶는 것" — 전부 성공하거나 전부 실패하거나

계좌이체 예시:
  1. A 계좌에서 10만원 차감  ✅
  2. B 계좌에   10만원 추가  💥 오류 발생!

트랜잭션 없으면 → A 돈만 사라짐 (데이터 망가짐)
트랜잭션 있으면 → 1번도 자동으로 취소 (롤백)
@Transactional  // 이 어노테이션 하나로 자동 롤백!
public void order(Long userId, Long itemId, int quantity) {
    orderRepository.save(order);   // 성공
    pointService.addPoint(userId); // 성공
    smsService.sendMessage(userId);// 💥 실패 → 위 두 개도 롤백!
}

파이썬 SQLAlchemy의 session.commit() / session.rollback() 을 자동으로 처리해주는 것이다.


4. JPA란?

SQL을 직접 쓰지 않고 DB를 다룰 수 있게 해주는 것

JPA          → 표준 명세 (설계도)
Hibernate    → JPA의 실제 구현체
Spring Data JPA → Hibernate를 더 편하게 쓰게 해주는 프레임워크
// JPA 없이
String sql = "SELECT * FROM users WHERE id = ?";
PreparedStatement ps = conn.prepareStatement(sql);
// ... 수십 줄

// JPA 있으면
userRepository.findById(id);  // 끝!

파이썬의 SQLAlchemy와 동일한 개념이다.


5. 정적 팩토리 메서드

new 대신 메서드로 객체를 만드는 패턴

// ❌ 일반 생성자 - 뭐가 뭔지 모름
Account account = new Account("김철수", 1000, false);

// ✅ 정적 팩토리 - 의도가 명확함
Account account = Account.createSavingsAccount("김철수", 1000);

static 이라서 객체 없이 클래스이름.메서드이름() 으로 바로 호출 가능하다.

public class User {
    protected User() {}  // 외부에서 new User() 못 하게 막음

    public static User create(String name, String email) {
        User user = new User();
        user.name = name;
        user.email = email;
        return user;
    }
}

// 사용할 때
User user = User.create("김철수", "kim@email.com");

파이썬의 @staticmethod 와 완전히 동일한 개념이다.


6. 비즈니스 로직이란?

"이 서비스만의 규칙과 계산"

배달앱 비즈니스 로직:
- 주문 금액이 12,000원 이상이면 배달비 무료
- 리뷰는 배달 완료 후에만 작성 가능
- 포인트는 결제금액의 1% 적립

비즈니스 로직이 아닌 것:

- 화면에 버튼 보여주기   → UI 로직
- DB에 저장하기         → 인프라 로직
- HTTP 요청 받기        → 네트워크 로직

DDD에서는 비즈니스 로직을 Service가 아닌 Domain Entity 안에 넣는 게 핵심이다.

// ✅ DDD 방식 - 규칙이 Entity 안에!
public class Review {
    public static Review create(String content, int rating) {
        if (content == null || content.isBlank()) {
            throw new BusinessException(ErrorCode.REVIEW_CONTENT_EMPTY);
        }
        if (rating < 1 || rating > 5) {
            throw new BusinessException(ErrorCode.INVALID_RATING);
        }
        // ...
    }
}

7. 느슨한 결합과 이벤트 발행

리뷰 작성 후 상점 평점을 업데이트할 때 두 가지 방법이 있다.

// ❌ 강한 결합 - 리뷰가 Store 내부를 직접 알아야 함
public void createReview(...) {
    reviewRepository.save(review);
    store.updateTotalRating(rating);  // 리뷰가 Store를 직접 건드림
}

// ✅ 느슨한 결합 - 이벤트만 던지고 끝
public void createReview(...) <{
    reviewRepository.save(review);
    eventPublisher.publish(new ReviewCreatedEvent(storeId, rating));
    // 리뷰는 Store가 어디있는지 전혀 모름!
}

// Store 쪽에서 알아서 처리
@EventListener
public void onReviewCreated(ReviewCreatedEvent event) {
    store.updateTotalRating(event.getRating());
}

유튜브 알림에 비유하면:

크리에이터가 영상 올림 (이벤트 발행)
     ↓
구독자한테 알림 울림 (이벤트 리스너)

크리에이터는 구독자가 누군지 몰라도 알림은 자동으로 울림!

8. Value Object와 @Embedded

DDD의 핵심 개념 중 하나. 관련 있는 값들을 의미있게 묶은 객체다.

// ❌ 다 때려넣기
public class Review {
    private Long id;
    private Long userId;
    private String nickname;
    private Long orderId;
    private Long storeId;
    private String content;
    private int rating;
}

// ✅ 관련된 것끼리 묶기
public class Review {
    @EmbeddedId
    private ReviewId id;
    @Embedded
    private Reviewer reviewer;      // 리뷰어 관련 묶음
    @Embedded
    private ReviewOrderInfo info;   // 주문 관련 묶음
    @Embedded
    private ReviewContent content;  // 내용 관련 묶음
}

@Embedded 를 쓰면 JPA가 자동으로 펼쳐서 하나의 테이블에 저장해준다.
코드에선 객체로 묶여있지만 DB에선 하나의 테이블!


9. 소프트 딜리트와 @SQLRestriction

소프트 딜리트란? DB에서 실제로 삭제하지 않고 삭제 시간만 기록하는 것

왜 쓰냐면:
- 삭제된 데이터도 나중에 필요할 수 있음
- 실수로 삭제해도 복구 가능
@Entity
@SQLRestriction("deleted_at IS NULL")  // Entity에 붙임
public class Review {
    private LocalDateTime deletedAt;
}

// 조회할 때 자동으로 조건 추가됨
reviewRepository.findAll();
// → SELECT * FROM reviews WHERE deleted_at IS NULL

10. 인덱스란?

DB에서 특정 컬럼을 빠르게 찾게 해주는 것

책의 목차와 같다.
목차 없으면 처음부터 끝까지 찾아야 하고, 목차 있으면 바로 페이지 찾아간다.

@Table(name="P_REVIEW", indexes = {
    @Index(
        name="idx_review_order_id",
        columnList = "order_id, deleted_at",
        unique = true
    )
})
  • columnList = "order_id, deleted_at" → 이 두 컬럼을 묶어서 인덱스 생성
  • unique = true → 이 조합이 유일해야 함 (하나의 주문에 리뷰 하나만!)
  • 소프트 딜리트와 연계 → 삭제(deleted_at에 값) 후 재작성 가능

마치며

3 레이어드로 전체 흐름 이해
       ↓
DDD로 역할을 더 명확하게 분리

기초가 있으면 DDD는 "Service를 더 정교하게 쪼갠 것" 으로 이해할 수 있다.
"왜?" 를 계속 물어보는 습관이 가장 중요한 것 같다. 이번 기회에 완벽하게 DDD구조를 파악 할 예정이다.

profile
안 되면 될 때까지

0개의 댓글