Skip to content

[REFACTOR] 도메인 설계 개선 #1035

Open
@jminkkk

Description

@jminkkk

📌 어떤 기능을 리팩터링 하나요?

도메인의 관련 검증 로직이 Service 계층에 과도하게 몰려있어요.
여러 PR에서 이 부분에 대해 논의가 나왔었는데요.
각 도메인이 자신에 대한 책임을 가지고 스스로 행동할 수 있도록 하기 위해 개선을 제안해요!

AS-IS

우리 서비스의 핵심 도메인인 템플릿에 대한 도메인 규칙 중 핵심인 내용들을 정리해봤어요.

  1. 하나의 템플릿은 여러 개의 소스코드를 가질 수 있다.
  2. 하나의 템플릿 안에서 소스 코드들은 고유한 순서를 가진다. (순서는 1부터 시작한다.)
  3. 하나의 템플릿은 소스 코드 중 대표 코드인 썸네일 코드를 가진다.

중요: 소스코드는 템플릿 없이 독립적으로 존재할 수 없다

etc...

image

현재 우리 서비스의 핵심 도메인인 "템플릿"에 대해 연관이 있는 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

refactor요구사항이 바뀌지 않은 변경사항

Type

No type

Projects

Status

Todo

Relationships

None yet

Development

No branches or pull requests

Issue actions