Description
📌 어떤 기능을 리팩터링 하나요?
도메인의 관련 검증 로직이 Service 계층에 과도하게 몰려있어요.
여러 PR에서 이 부분에 대해 논의가 나왔었는데요.
각 도메인이 자신에 대한 책임을 가지고 스스로 행동할 수 있도록 하기 위해 개선을 제안해요!
AS-IS
우리 서비스의 핵심 도메인인 템플릿에 대한 도메인 규칙 중 핵심인 내용들을 정리해봤어요.
- 하나의 템플릿은 여러 개의 소스코드를 가질 수 있다.
- 하나의 템플릿 안에서 소스 코드들은 고유한 순서를 가진다. (순서는 1부터 시작한다.)
- 하나의 템플릿은 소스 코드 중 대표 코드인 썸네일 코드를 가진다.
중요: 소스코드는 템플릿 없이 독립적으로 존재할 수 없다
etc...

현재 우리 서비스의 핵심 도메인인 "템플릿"에 대해 연관이 있는 Service Layer는 위와 같아요. (사실 더 복잡하지만 태그, 카테고리 같이 선택적인 부분은 일단 제외할게요.)
문제를 느낀 부분은 다음이에요.
public class TemplateApplicationService {
private final TemplateService templateService;
private final SourceCodeService sourceCodeService;
private final CategoryService categoryService;
private final TagService tagService;
private final ThumbnailService thumbnailService;
private final LikesService likesService;
// 템플릿 생성
@Transactional
public Long create(Member member, CreateTemplateRequest request) {
Category category = categoryService.fetchById(member, request.categoryId());
Template template = templateService.create(member, request, category);
tagService.createTags(template, request.tags());
sourceCodeService.createSourceCodes(template, request); // 소스코드 생성
SourceCode thumbnail = sourceCodeService.getByTemplateAndOrdinal(template, request.thumbnailOrdinal());
thumbnailService.createThumbnail(template, thumbnail);
return template.getId();
}
}
public class SourceCodeService {
private static final int MINIMUM_SOURCE_CODE_COUNT = 1;
private final SourceCodeRepository sourceCodeRepository;
// 호출한 소스코드 생성 로직
@Transactional
public void createSourceCodes(Template template, CreateTemplateRequest request) {
validateSourceCodeCount(request);
validateSourceCodesOrdinal(request);
sourceCodeRepository.saveAll(
request.sourceCodes().stream()
.map(createSourceCodeRequest -> createSourceCode(template, createSourceCodeRequest))
.toList()
);
}
private void validateSourceCodeCount(ValidatedSourceCodesCountRequest request) {
if(request.countSourceCodes() < MINIMUM_SOURCE_CODE_COUNT) {
throw new CodeZapException(ErrorCode.INVALID_REQUEST, "소스 코드는 최소 1개 입력해야 합니다.");
}
}
private void validateSourceCodesOrdinal(ValidatedOrdinalRequest request) {
List<Integer> indexes = request.extractOrdinal();
boolean isOrderValid = IntStream.range(0, indexes.size())
.allMatch(index -> indexes.get(index) == index + 1);
if(!isOrderValid) {
throw new CodeZapException(ErrorCode.INVALID_REQUEST, "소스 코드 순서가 잘못되었습니다.");
}
}
사실 다른 도메인이지만, "도메인의 집합에 대한 규칙을 어디에서 처리할 것인가"에 대해서는 우리 이미 많은 이야기가 나왔었어요.
고생했던 초롱의 PR
ValidationService에 대해 이야기도 해보고 일급 컬렉션 객체를 만들어보는 시도도 했어요.
카테고리와는 조금 다를 수 있지만 템플릿이라는 도메인에 집중을 해보면 사실 우리가 생각해보지 못했던 중요한 부분이 하나있어요.
앞에서 템플릿의 핵심 도메인 규칙을 통해 알 수 있듯 소스 코드는 템플릿이 없다면 살아 있을 수 없다는 것이에요.
다시 말해, 소스 코드의 생명 주기는 메인 도메인인 템플릿에 의존해요.
하지만, 우리 서비스에서는 Template과 SourceCode는 각각 독립적으로 관리되고 있어요.
따라서 핵심 도메인 규칙을 가지고 있어야 할 Template 엔티티를 봐도 SourceCode와의 관계를 파악할 수 없어요. (@onetomany 관계가 맺어져 있지만 Fake 객체 사용 때 남겨진, 현재는 전혀 사용되지 않는 레거시 코드에요.)
또한 Service에서 검증하는 도메인 규칙도 있고, DTO에서 검증하는 도메인 규칙도 있는 등 응집도가 매우 떨어져요.
정리하면 다음과 같은 문제예요.
- 코드 상에서 Template과 SourceCode 두 도메인 간의 관계가 명확하지 않음
- 도메인의 관련 검증 로직이 Service 계층에 과도하게 몰려있음
- 비즈니스 규칙들이 여러 서비스 클래스에 흩어져 있음
TO-BE
단순히 다른 엔티티이기 때문에 각자의 레포지토리를 가지고 처리하는 것이 아닌, 진짜 현실의 도메인 규칙을 코드로 옮겼으면 해요.
각 도메인 간의 책임과 권한을 명확하게 하여 관계를 개선해요. 그 과정에서 영속성 전이를 활용할 수 있어요.
특히, Template이 자신의 SourceCode들을 전적으로 관리하게 하면, 모든 SourceCode 관련 작업은 반드시 Template을 통해서만 수행되어요.
-> 템플릿 없이는 독립적으로 살아있을 수 없는 소스코드가 되고, 현실의 도메인 규칙과 코드 상의 도메인 규칙이 닮아져요.
코드 변경 예시는 다음과 같아요.
템플릿이 소스 코드를 관리하고, 소스코드 집합에서 발생되는 도메인 규칙을 내부에서 관리해요.
@Entity
public class Template extends SkipModifiedAtBaseTimeEntity {
private static final Long LIKES_COUNT_DEFAULT = 0L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// 생략
@OneToMany(mappedBy = "template", cascade = CascadeType.ALL)
private List<SourceCode> sourceCodes = new ArrayList<>();
public Template(Member member, String title, String description, Category category) {
this(member, title, description, category, Visibility.PUBLIC);
validateSourceCodeCount(sourceCodes);
validateSourceCodesOrdinal(sourceCodes);
}
private void validateSourceCodeCount(List<SourceCode> sourceCodes) {
if (sourceCodes.isEmpty()) {
throw new CodeZapException(ErrorCode.INVALID_REQUEST, "소스 코드는 최소 1개 입력해야 합니다.");
}
}
private void validateSourceCodesOrdinal(List<SourceCode> sourceCodes) {
List<Integer> indexes = extractOrdinal(sourceCodes);
boolean isOrderValid = IntStream.range(0, indexes.size())
.allMatch(index -> indexes.get(index) == index + 1);
if (!isOrderValid) {
throw new CodeZapException(ErrorCode.INVALID_REQUEST, "소스 코드 순서가 잘못되었습니다.");
}
}
private List<Integer> extractOrdinal(List<SourceCode> sourceCodes) {
return sourceCodes.stream()
.map(SourceCode::getOrdinal)
.sorted()
.toList();
}
기대 효과는 다음과 같아요.
- 도메인 규칙이 코드에 더 명확하게 표현됨
- Template과 SourceCode의 관계가 명확해짐 (코드만 봐도 "아, SourceCode는 Template에 속해있구나"가 명확히 보임)
- 비즈니스 규칙의 응집도 또한 올라가요 (SourceCode 관련 규칙들이 Template 안에 모여있어서 관리가 쉬워짐)
- 실수로 Template 없이 SourceCode를 만드는 것을 방지할 수 있음
⏳ 예상 소요 시간 (예상 해결 날짜)
7일 0시간 소요 (00/00 00:00)
🔍 참고할만한 자료(선택)
사실 이 부분은 DDD 라는 설계에서 도움을 얻었어요.
애그리게잇 하나에 리파지토리 하나
도메인 원정대
[NHN FORWARD 22] DDD 뭣이 중헌디? 🧐
DDD에 대한 부분을 찾아보았었어요. 용어가 낯설고 어려워요.
하지만 용어나 특정 설계론보다 중요한 건 도메인 간의 관계에 집중하는 게 목표에요.
저는 "애그리게잇이 우리의 "템플릿"이고 이를 중심으로 관련 도메인을 관리한다" 정도만 이해하면 된다고 이야기 싶어요.
저도 DDD 잘 모름ㅎ 어려워
Metadata
Metadata
Assignees
Labels
Type
Projects
Status