[LXP] roadmap.sh 써보셨나요?

dev_yuni·2025년 11월 23일

회고

목록 보기
5/5
post-thumbnail

들어가며..

이전 회고에 이어 포텐업에서의 2개월 LXP 프로젝트를 새로운 팀원들과 시작하게 되었습니다.

이번 프로젝트에서는 처음으로 DDD(Domain-Driven Design)를 적극적으로 도입해 봤습니다.
그 과정에서 무엇을 배웠고, 무엇이 아쉬웠고, 다음에는 무엇을 바꾸고 싶은지를 정리했습니다.


회고를 시작하기 앞서 먼저 프로젝트를 소개드리겠습니다.

프로젝트 소개

Github 보러가기

협업 방식

Notion과 GitHub 등을 활용했지만, 핵심은 도구 자체가 아닌 "서로의 성장을 위한 기록과 리뷰 문화"였습니다.

  • Daily Scrum: 이미 한 일, 할 일, 그리고 장애 요소를 매일 공유했습니다.
  • Log & Reference: 논의 내용과 결정 사항을 Notion에 남기며, 모두가 볼 수 있는 레퍼런스로 만들었습니다.
  • Discussion: 학습 내용과 고민 지점을 Discussion에 공유하며, 인사이트를 확장할 수 있었습니다.
  • Spotless: 코드 포맷팅을 자동화하여 컨벤션 논쟁을 최소화하고, 도메인에 집중할 수 있었습니다.

구현 방식

기능 구현에 바로 들어가기 전, 이벤트 스토밍을 통해 도메인 이벤트를 도출하고 이를 기준으로 도메인을 나눴습니다.

이후 각 도메인을 적절히 분배하여 담당자를 정하고, DDD 스타일의 패키지 구조와 레이어를 적용해 구현을 진행했습니다.

가장 큰 차이점 : LMS vs LXP

구분LMS (이전 프로젝트)LXP (이번 프로젝트)
핵심관리자 중심 (CRUD)사용자 경험 중심 (Customizing)
목표Spring MVC 경험도메인 주도 설계 (DDD)
레퍼런스기존 강의 사이트roadmap.sh

단순히 화면을 그리는 것이 아니라, "사용자가 학습 경로를 스스로 커스터마이징한다"는 도메인적 특성을 살리기 위해 DDD를 선택했습니다.

DDD?

DDD는 “Domain Driven Design”의 약자로, 복잡한 도메인 모델을 설계하고 구현하기 위한 방법론이다. 도메인 전문가와 개발자가 협력해 도메인 모델을 정의하고, 이를 기반으로 소프트웨어를 설계하는 것을 목표로 한다.

처음에는 DDD가 MVC와 완전히 다른 “어떤 패턴”이라고 생각했습니다.
실제로 써보니, DDD는 특정 프레임워크나 패턴이라기보다 도메인 중심으로 사고하고 설계하는 방법론에 가까웠습니다.

그래서

  • DDD를 사용한다고 해서 MVC를 못 쓰는 것도 아니고,
  • MVC를 쓴다고 해서 DDD를 못 하는 것도 아니라는 걸 알게 됐습니다.

MVC 위에 DDD의 관점을 얹어서, “코드를 어떤 책임 단위로 나눌지 도메인을 어디까지 보호할지”를 고민해 보는 쪽에 더 가깝다고 느꼈습니다.


그렇다면 왜 DDD여야 했을까요 🤔?

왜 DDD인가?

이번 프로젝트에서 “왜 굳이 DDD여야 했는가?” 를 계속 질문하게 됐습니다.
단순히 유행하는 개념을 써보고 싶어서가 아니라,
LXP라는 도메인의 특성과 팀의 협업 방식, 그리고 개인적인 학습 목표가 맞물린 선택에 가까웠습니다.

정리하면,

  • LXP 도메인 자체가 복잡해서, 화면이 아니라 도메인부터 다시 설계할 필요가 있었습니다.
  • 같은 화면을 보면서도 서로 다른 언어를 쓰는 문제를 해결하고 싶었습니다.
  • 설계–구현–코드 리뷰 전체를 도메인 중심으로 엮어 보는 실험을 해보고 싶었습니다.
  • 말로만 듣던 DDD를 실제 프로젝트에 적용해 보며 직접 몸으로 이해해보고 싶었습니다.

1. LXP 도메인이 던진 질문

이번 프로젝트의 목표는 단순히 강좌와 강의를 CRUD로 관리하는 LMS가 아니라, 사용자 중심으로 학습 경험을 설계하는 LXP였습니다.

그래서,

  • “어떤 강의를 가지고 있느냐”보다
  • “사용자가 어떤 경로로, 어떤 맥락에서 학습하느냐”

가 더 중요해진다고 느꼈습니다.

이 의미를 생각하다 보니 이런 질문들을 던지게 됐습니다.

  • “이걸 화면 기준이 아니라, 도메인 모델 기준으로 다시 잡아보면 어떨까?”
  • “사용자, 로드맵, 토픽, 별점 같은 개념들을
    단순 테이블이 아니라 규칙과 책임을 가진 도메인으로 바라보면 어떨까?”

이 지점에서 “도메인부터 먼저 잡고 가는 설계 방식”인 DDD를 떠올리게 되었고,
단순히 MVC를 써보는 수준을 넘어 도메인 중심 설계를 직접 경험해 보고 싶다는 생각이 들었습니다.

💡 요약

LXP라는 도메인 자체가 “학습 경험”에 초점이 있다 보니,
자연스럽게 화면이 아닌 도메인 모델부터 설계하고 싶은 마음이 들었습니다.

2. 같은 화면, 다른 언어를 맞추기 위해

이벤트 스토밍은 실제로 이렇게 포스트잇을 잔뜩 붙여가며 진행했어요 🙌

roadmap.sh를 분석한 뒤 이벤트 스토밍을 진행하면서,
같은 화면을 보고도 팀원마다 쓰는 언어가 다르다는 걸 체감했습니다.

  • 한 팀원은 Skill·Role 같은 사이드 요소를 카테고리로 보았고,
  • 또 다른 사람은 각 로드맵의 루트(예: Backend)를 카테고리로 이해했습니다.
  • 로드맵에서 무엇을 Topic, 무엇을 SubTopic으로 볼지도 엇갈렸습니다.

이 경험을 통해 유비쿼터스 언어의 필요성을 느꼈습니다.
이벤트 스토밍에서 도출한 도메인 이벤트를 기반으로 용어를 정리하면서,
바운디드 컨텍스트와 어그리게이트를 나눌 때 DDD의 관점을 적극적으로 활용하게 되었습니다.

  • 바운디드 컨텍스트(Bounded Context) : “이 말은 여기 안에서만 이 의미로 쓴다”라고 경계를 그어놓은 도메인 구역.
  • 어그리게이트(Aggregate) : “여기까지는 한 번에 함께 일관성을 지켜야 하는 덩어리”를 정의한 것.

3. 도메인 중심으로 엮어보기

이전 프로젝트에서도 요구사항 정의와 ERD 설계는 했지만,

이번에는 여기에 더해

  • UML,
  • 유비쿼터스 언어 정의,
  • 바운디드 컨텍스트, 어그리게이트 설계

까지 포함해 설계 단계에 더 많은 시간을 투자했습니다.

동시에

  • 작은 PR 단위,
  • 커밋 메시지 컨벤션,
  • 상호 코드 리뷰 규칙

을 세우면서 단순 스타일이나 구현 디테일이 아니라,
“이 도메인 규칙을 이 위치에서 책임지는 게 맞는가?”라는 관점으로 이야기를 나눌 수 있었습니다.

💡 요약

설계–구현–코드 리뷰를 하나의 흐름으로 엮어 본 첫 실험이었고,
“도메인 관점에서 코드를 보는 연습”이 많이 되었습니다.

4. 말로만 듣던 DDD

강의와 글로만 접하던 DDD를 “알고 있다” 수준에 두기보다는, 실제 프로젝트에 적용해 보면서 어디까지 도움이 되고 어디서부터는 과한지 직접 느껴보고 싶었습니다.

이번 프로젝트에서는 DDD를 이론이 아니라 코드와 협업 경험으로 이해해보고 싶었고, 덕분에 도메인 중심 사고와 설계가 무엇인지 조금은 느낄 수 있었습니다.


이제부터는, 이렇게 선택한 DDD 관점 아래에서 실제 구현을 하며 부딪혔던 고민들을 정리해보려 합니다.

고민포인트

발표 이후 강사님께 아래와 같은 피드백을 들었습니다.

MVP 단계에서는 SubTopic이 Topic 하위에 포함되는 구조로 가도 괜찮지만,
서비스가 확장되면 강사라는 액터가 등장해 콘텐츠를 직접 업로드하게 될 수 있다.
그런 경우를 미리 염두에 뒀다면 SubTopic을 루트 어그리게이트로 설계하는 선택도 가능했을 것.

이 피드백을 듣고, 제가 맡았던 도메인에서의 고민 포인트와 연결점이 있지 않을까 생각하게 되었고,

“도메인을 어디까지 확장 가능성을 염두에 두고 설계할 것인가”를 다시 돌아보는 계기가 되었습니다.

1. 도메인 확장성을 어디까지 설계에 녹일 것인가

제가 맡았던 도메인은 Category(카테고리)Star(별점)이었습니다.

둘 다 Roadmap과 분리된 별도의 어그리게이트로 설계했습니다.

  • Category → 로드맵을 분류하는 카테고리 도메인
  • Star → 사용자가 로드맵에 남기는 평가 도메인

으로 보고, 각자 독립된 도메인 모델과 패키지로 가져갔습니다.

이미 어그리게이트로 분리한 상태였지만, 구현을 진행할수록
“어디까지 확장 가능성을 설계에 녹여 둘 것인가” 에 대한 고민이 깊어졌습니다.

1-1. Star 도메인: 이미 어그리게이트지만, 어떻게 확장할 것인가

상황

Star는 처음부터 Roadmap 내부의 필드가 아닌 독립 어그리게이트였습니다.

  • 하나의 Star는 userroadmap을 연결하는 평가이고,value는 1~5점 사이의 값입니다.
  • “한 유저가 한 타깃에 대해 한 번만 평가할 수 있다”는 불변식을 가지고 있습니다.

그래서 처음에는 아래처럼, Star 테이블을 떠올렸습니다.

// "한 유저가 한 로드맵에 한 번만 평가할 수 있다" 불변식을 가진 초기 설계
@Entity
@Table(
    name = "star_road_map",
    uniqueConstraints = {
        @UniqueConstraint(
            name = "uk_star_user_roadmap",
            columnNames = {"user_id", "road_map_id"}
        )
    }
)
public class StarRoadMap {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "value", nullable = false)
    private int value;        // 1~5점

    @Column(name = "user_id", nullable = false)
    private Long userId;

    @Column(name = "road_map_id", nullable = false)
    private Long roadMapId;

    private LocalDateTime createdAt;
    private LocalDateTime modifiedAt;

    // 생성자/검증 로직 등 생략
}

고민

이 코드만 보면 도메인 규칙이 꽤 명확하게 드러납니다.

  • 하나의 Star는 userId + roadMapId 조합으로 유일하다.
    → “한 유저는 한 로드맵을 한 번만 평가한다.”
  • Star는 Roadmap의 필드가 아니라, 별도 테이블/애그리게이트로 분리되어 있다.

하지만 구현을 진행할수록 이런 시나리오들이 떠올랐습니다.

  • “별점이 Roadmap에만 머무르지 않고 Topic / SubTopic에도 붙게 된다면?”
  • “나중에 강사, 강의, 강사 프로필에도 같은 방식의 평가를 쓰고 싶다면?”

이 지점에서 “독립된 애그리게이트로 분리했다는 사실만으로 충분한가?” 라는 질문이 생겼습니다.

Roadmap에서만 쓸 생각으로 road_map_id에 고정해 버리면,
나중에 다른 도메인에 재사용하려 할 때 모델 자체를 갈아엎어야 하는 구조가 되기 때문입니다.

지금 정리한 기준

  • 하나의 유저는 하나의 타깃에 대해 한 번만 평가할 수 있다.
    → (user_id + target_id) 유니크 불변식

  • Star는 Roadmap에만 붙는 개념이 아니라, Topic / SubTopic / Instructor 등
    여러 타깃에 붙을 수 있는 "범용 평가 도메인"이 될 수 있다.

  • 지금은 Roadmap에만 사용하더라도,
    나중을 생각하면 (target_type + target_id) 구조로 여지를 열어두는 편이 확장에 유리하다.

이 기준으로 옮기면?

// 여러 도메인에 붙을 수 있는 "평가 타깃" 값 객체
@Embeddable
public class StarTarget {

    @Enumerated(EnumType.STRING)
    @Column(name = "target_type", nullable = false, length = 50)
    private StarTargetType type; // ex) ROADMAP, TOPIC, SUB_TOPIC

    @Column(name = "target_id", nullable = false)
    private Long id;

    ...
}
@Entity
@Table(
    name = "stars",
    uniqueConstraints = {
        @UniqueConstraint(
            name = "uk_star_user_target",
            columnNames = {"user_id", "target_type", "target_id"}
        )
    }
)
public class Star {
   ...
}

지금 프로젝트에서는 여기까지 구현하진 않았지만,

  • 현재 코드(StarRoadMap) 가 보장하는 불변식은 무엇인지
  • 확장 가능성을 의식한 코드 스케치(Star + StarTarget) 는 어떤 차이가 있는지

를 나란히 두고 보면서

“우리는 이미 Star를 애그리게이트로 분리했지만, 도메인 관점에서도 ‘확장’을 염두에 둔 설계였는가?”

를 다시 한 번 질문해 볼 수 있었습니다.

1-2. Category 도메인: 지금은 단순하지만, 나중에 달라질 수 있는 도메인

상황

Category도 Roadmap과 분리된 독립 어그리게이트로 가져갔습니다.

  • Category → 로드맵을 묶어주는 “분류 도메인”
  • Roadmap → 특정 Category에 속함

이라는 단순한 관계로 출발했고, 이에 맞게 엔티티도 아주 단순한 형태입니다.

@Entity
@Table(name = "category")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class Category extends IdEntity {

    @Enumerated(EnumType.STRING)
    private CategoryType categoryType; // 예: ROLE, SKILL로 고정된 분류 타입

    @Column(nullable = false)
    private String name;               
}

이 코드만 놓고 보면 Category는

  • 고정된 CategoryType 값에 의해 구분되고,
  • 별도의 parent/owner 같은 개념이 없는,
  • 전역적으로 공유되는 Flat한 카테고리 목록

관리자가 미리 정의해 둔 카테고리 집합에서 로드맵이 하나를 선택해서 매핑된다는 설계에 잘 맞는 구현입니다.

고민

지금 단계(LXP MVP)에서는 “최소한의 Category 어그리게이트”로 적절한 선택이었습니다.

LXP의 특성상, 구현 이후 이런 생각들이 따라왔습니다.

  • 사용자가 직접 커스텀 카테고리를 만들 수 있게 된다면?
  • 카테고리가 단일 depth가 아니라 상위/하위 관계를 가진 트리 구조로 바뀐다면?

이미 Category를 어그리게이트로 분리해 두었기 때문에
“나중에 이 도메인의 라이프사이클이 이렇게까지 바뀔 수도 있겠다”를 더 고민해 볼 수 있었습니다.

지금 정리한 기준

카테고리 도메인을 정리하면서 스스로 세운 기준은 대략 이런 느낌이었습니다.

  • 지금은 "전역, 고정, 단일 depth" 카테고리로도 충분하다.
  • 하지만 LXP 특성상 "유저 커스텀"이나 "계층형 트리"로 확장될 가능성이 높다.
  • 그렇다면 Category는 "변화할 가능성이 큰 도메인"이라는 메모를 해두는 편이 좋다.

만약 트리 구조를 확장한다면?

@Entity
@Table(name = "category")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class Category extends IdEntity {

    @Enumerated(EnumType.STRING)
    private CategoryType categoryType;

    @Column(nullable = false)
    private String name;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "parent_id")
    private Category parent;        // 상위 카테고리

    @OneToMany(mappedBy = "parent")
    private List<Category> children = new ArrayList<>(); // 하위 카테고리 리스트

	...
}

그리고 유저 커스텀 카테고리까지 상상한다면,
전역 Category와는 별도 어그리게이트로 이렇게 분리하는 선택도 가능할 것 같습니다.

// 전역 Category는 그대로 두고, 사용자 전용 커스텀 카테고리는 별도 애그리게이트로 분리
@Entity
@Table(name = "user_category")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class UserCategory extends IdEntity {

    @Column(nullable = false)
    private Long userId;       // 카테고리를 만든 사용자

    @Column(nullable = false)
    private String name;       // 사용자가 지정한 커스텀 이름

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "base_category_id")
    private Category base;    
}

지금 프로젝트에서 여기까지 구현한 것은 아니지만,

  • 현재 코드(Category)는 “전역, Flat, 단일 depth 카테고리”라는 현재 요구사항에 잘 맞고,
  • 트리/커스텀 같은 요구사항이 붙는다면 어느 지점을 바꿔야 할지 코드만 봐도 상상할 수 있게 된다는 점에서,

“지금은 리스트처럼 보이는 도메인도, 나중에는 구조와 책임이 달라질 수 있다.”는 걸 더 민감하게 느끼게 해 준 도메인이었습니다.

그래서 다음에는 설계 단계에서부터

  • “이 어그리게이트는 나중에 depth나 owner가 붙을 가능성이 있는가?”
  • “그렇다면 지금은 최소 구현으로 두더라도, 어디까지를 이 도메인의 책임으로 볼 것인가?”

를 먼저 적어두고 시작해 보자는 기준을 갖게 되었습니다.


도메인 자체에 대한 고민과 함께, 이번 프로젝트에서는 협업 과정에서의 저 자신의 태도에 대해서도 돌아보게 되었습니다.

2. 질문, PR, 테스트의 타이밍을 어떻게 가져갈 것인가

프로젝트를 진행하면서 아래 네 가지가 계속 마음에 남았습니다.

  • 질문을 적극적으로 하지 못했던 부분
  • 정한 기간이 있었는데 PR 리뷰나 머지를 지키지 못한 부분
  • 너무 급하게 테스트를 찍어내다 보니 목데이터를 제대로 신경 쓰지 못한 부분
  • 설계에 충분히 참여하지 못한 채 개발을 시작해서 막막했던 순간들

이걸 Before / After로 나눠 보니 더 선명해졌습니다.

Before / After 정리

항목BeforeAfter
질문“조금만 더 고민해보고 그래도 안 되면 물어보자”설계 단계에서 이해 안 되는 부분은 바로 질문하고, 논의에 적극적으로 참여하기
PR한 번에 몰아서, 덩치 큰 PR작은 단위로 자주 올리고, 중간 진행 상황 공유용 도구로 PR을 인식
테스트“일단 통과하는 테스트” 위주, 목데이터는 대충도메인 시나리오를 설명하는 문서에 가깝게 보고, 실제 도메인을 대표하는 목데이터를 고민

그리고 중간중간 받았던 팀원분들의 이해도 체크와 조언들이 큰 도움이 되었습니다 🙇🏻‍♀️
지금 이 설계를 어떻게 이해하고 있는지와 같은 팀원들의 질문이 오히려 제 사고를 정리하게 만든 계기가 되었습니다.

다시 한 번 팀원분들에게 너무 감사드립니다 😊

학습 포인트

1. 도메인 이벤트와 모듈 간 통신

도메인별 모듈이 생기면서, 모듈 간 통신 방식도 함께 고민하게 되었습니다.

  • 같은 모듈 내부에서는
    presentation → application → domain → infra 방향의 의존을 유지하고,
  • 다른 모듈과는 직접 참조를 최소화하면서 어떻게 연결할지가 관건이었습니다.

제가 맡았던 Category/Star 도메인은 Roadmap 도메인의 상태 변화에 영향을 받는 쪽(consumer)에 더 가까웠습니다.

예를 들어,

  • 로드맵의 공개 상태가 변경될 때,
  • 로드맵이 더 이상 노출되지 않는 상태가 될 때,

기존이라면 그냥 Roadmap 서비스를 참조해 와서,

  • 로드맵 상태를 조회해서 Star/Category 쪽 상태를 맞추자.
  • 필요하면 로드맵 쪽 메서드를 직접 호출해서 정합성을 맞추자.

하지만 모듈별로 나뉜 구조와 DDD 관점을 함께 보면서, 한 번 더 이런 질문을 던지게 되었습니다.

  • Star/Category가 정말 Roadmap 서비스에 직접 의존해야 하는가?

  • 아니면 “로드맵이 생성되었다”, “로드맵이 비공개로 전환되었다” 같은
    로드맵 도메인 이벤트를 수신하는 쪽으로 두는 게 더 자연스러운가?

이번 프로젝트에서는 모든 흐름을 완전히 이벤트 기반으로 만들지는 못했지만,

실제로 Star 모듈이 다른 도메인의 이벤트를 구독해서 반응하는 구조는 일부 구현해 볼 수 있었습니다.

예를 들어, Star 모듈에서는 다음과 같이

  • 유저 계정 상태 변경 이벤트(UserAccountStatusUpdated)
  • 로드맵 도메인 이벤트(RoadMapEventOccurred)

를 구독해서 별점 정보를 정리합니다.

@Slf4j
@Component
@RequiredArgsConstructor
public class StarRoadMapEventHandler {

    private final StarRoadMapCommandService starRoadMapCommandService;
    private final RoadMapQueryService roadMapQueryService;

    // 유저 상태 변경 이벤트는 별도 정리 (예: BLOCKED → 별점 삭제)
    // ...

    /**
     * 로드맵 도메인에서 "삭제됨" 이벤트가 발생하면
     * → 그 로드맵에 달린 별점들을 정리한다.
     */
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void handlerRoadMapEventOccurred(RoadMapEventOccurred event) {
        if (event.eventType() == EventType.DELETED) {
            log.info("RoadMap deleted → delete stars, roadmapId={}", event.roadMapId());
            
			// 실제 코드에서는 삭제 가능 상태 체크를 위해 RoadMapView 를 한 번 더 확인
            RoadMapView roadMapView = roadMapQueryService.findById(event.roadMapId());
            starRoadMapCommandService.deleteAllStarByRoadMapId(roadMapView.id());
        }
    }
}

이 코드 덕분에 Star 모듈은

  • UserServiceRoadMapService 에 직접 의존하지 않고,
  • "유저가 BLOCKED 되었다", "로드맵이 삭제되었다" 라는 도메인 이벤트만 보고
  • 자기 어그리게이트(별점)를 어떻게 정리할지 스스로 결정할 수 있었습니다.

아직 전체를 이벤트 기반으로 재구성한 단계는 아니지만,
“무슨 일이 일어났는가(What happened?)”를 기준으로 모듈을 연결하는 연습을 해볼 수 있었다는 점이 의미 있었습니다.

좀 더 생각해본다면,

  1. 이론적으로 더 느슨한 구조
  • 이벤트 자체에 삭제 가능 여부 플래그 등 처리에 필요한 정보들을 최대한 담아두고, 이벤트를 받은 쪽에서 다른 모듈을 다시 조회하지 않아도 되도록 만드는 편이 더 느슨한 결합(Loosely Coupled)에 가깝습니다.
  1. 이번 구현에서의 타협
  • 이번 구현에서는 RoadMapQueryService 를 통해 한 번 더 로드맵을 조회한 뒤 삭제 가능 상태를 다시 확인하는 쪽을 선택했습니다.
  • 운영/안정성 관점에서 “정말 삭제해도 되는 상태인지”를 한 번 더 방어적으로 확인한 셈이라, 완전히 잘못된 선택이라고 보긴 어렵지만, 의존성이 생긴다는 점은 인지하고 있습니다.
  1. 앞으로의 리팩터링 아이디어
  • 리팩터링한다면, RoadMapDeletedRoadMapVisibilityChanged 처럼 의미가 더 분명한 이벤트로 분리하고, 그 안에 “삭제 가능한 상태인지”, “어떤 사유로 삭제/비공개가 되었는지” 등의 정보를 함께 담는 방향도 고려해 볼 수 있겠다고 느꼈습니다.
  • 이렇게 되면 Star/Category 쪽은 RoadMap 을 다시 조회하지 않고도 “지금 이 이벤트를 어떻게 처리해야 하는지”를 더 자율적으로 판단할 수 있을 것 같습니다.
👀 도메인 이벤트 흐름 보기

2. DTO vs Projection vs Read Model

이번 프로젝트를 하면서 DTO / Projection / Read Model의 역할을 한 번 정리하게 되었습니다.

📌 자세히 보기: DTO / Projection / Read Model 간단 정리

조회 요구사항이 다양하다 보니,

  • 어디까지를 단순 DTO로 처리할지
  • 어디서는 Projection으로 최적화할지
  • 어디서는 아예 Read Model을 따로 빼는 게 좋을지

를 고민하게 되었고, 그 과정에서 조회 전용 모델도 도메인 관점에서 설계할 수 있다는 관점을 조금이나마 갖게 되었습니다.

아쉬웠던 점

1) 기술적인 부분 (Category / Star 도메인 관점)

  • 별점 확장 시나리오(Topic/SubTopic, 다른 도메인 대상 평가)를 먼저 상상해 보며 스키마를 더 과감하게 일반화해 보는 실험을 못 해본 점이 살짝 아쉽습니다.
  • 테스트 코드에서 도메인 규칙과 시나리오를 더 분명히 드러냈다면, “이 도메인이 무엇을 보장해야 하는지”를 더 잘 설명할 수 있었을 것 같습니다.

2) 협업적인 부분

  • 질문을 적극적으로 하지 못했던 부분 때문에,
    이해하지 못한 상태로 개발을 시작해 막막함을 느끼는 순간이 있었습니다.

  • 정한 기간 안에 PR 리뷰와 머지를 지키지 못한 부분이
    팀 전체 흐름에 작게나마 영향을 주었다고 느꼈습니다.

  • 테스트를 급하게 작성하면서 목데이터를 신경 쓰지 못한 부분은
    “테스트를 신뢰할 수 있는가?”라는 관점에서 아쉬움으로 남았습니다.

  • 설계 단계에서 더 적극적으로 참여했다면,
    나중에 구현 단계에서 느꼈던 막막함을 줄일 수 있었을 것 같다는 생각도 들었습니다.

좋았던 점

  • LXP라는 도메인 위에서 처음으로 DDD를 적극적으로 적용해 보면서,
    도메인 중심으로 설계하고 코드를 바라보는 경험을 했습니다.

  • Notion, GitHub Discussion, 코드 리뷰를 통해
    서로가 성장할 수 있도록 기록하고 리뷰하는 문화를 함께 만들어 갔다는 점이 좋았습니다.

  • 바운디드 컨텍스트, 어그리게이트, 도메인 이벤트, 도메인 기반 패키지 구조,
    DTO/Projection/Read Model 등 책에서만 보던 개념들을 실제 코드에 녹여 보면서
    “아 이래서 이런 개념이 필요하구나”를 몸으로 이해하기 시작했습니다.


지난 목표 점검

이전 프로젝트 회고에서 다음과 같은 세 가지 목표를 적어둔 적이 있습니다.

  1. 코드 리뷰 시간 확보 및 규칙 정하기
  2. 논의 주제에서의 본질을 벗어나지 않기 (실험: 논의 시간 타임박스)
  3. 서로 성장할 수 있는 분위기를 만들기

이번 팀 프로젝트에서 이 세 가지 목표를 실제로 얼마나 실천해 봤는지 점검해보았습니다.

목표달성도회고
코드 리뷰 문화PR 단위 축소 및 리뷰 사이클 정착 성공. (규칙 구체화 필요)
논의 본질 유지🟡Notion 기록으로 논의 발산은 막았으나, 타임박싱은 미흡.
성장 분위기"비난"이 아닌 "제안"하는 리뷰 문화 정착.

📌 자세히 보기: 지난 목표 점검

다음 목표

이번 프로젝트를 계기로, 다음과 같은 목표를 갖게 되었습니다.

  • 어그리게이트 경계 잡는 연습 더 해보기
  • 조회 모델 설계 연습 (DTO / Projection / Read Model)
    • 단순 DTO를 넘어서, “이 화면을 위한 Read Model은 무엇인가?”를 먼저 고민하고 구성해 보기.
  • 이해가 막히면 설계 단계에서 질문하기
  • 작은 단위로 자주 PR 보내기
  • 도메인 시나리오를 설명하는 테스트를 꾸준히 쌓아가기

DDD를 한 번 써봤다고 해서, “이제 DDD를 안다”고 말할 수 있는 단계는 아니라고 생각합니다.

하지만 이번 경험을 통해,

DDD는 정답이 정해진 규칙이 아니라, 복잡성과 싸우기 위한 하나의 사고방식이다.

라는 문장에는 조금 더 자신 있게 동의하게 되었습니다.

긴 글 읽어주셔서 감사합니다 🙌


참고 자료

profile
꾸준히 성장하는 백엔드 개발자

0개의 댓글