Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
jminkkk opened this issue Jan 16, 2025 · 1 comment
Open

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

jminkkk opened this issue Jan 16, 2025 · 1 comment
Assignees
Labels
refactor 요구사항이 바뀌지 않은 변경사항

Comments

@jminkkk
Copy link
Contributor

jminkkk commented Jan 16, 2025

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

도메인의 관련 검증 로직이 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 잘 모름ㅎ 어려워

@jminkkk jminkkk added the refactor 요구사항이 바뀌지 않은 변경사항 label Jan 16, 2025
@jminkkk jminkkk self-assigned this Jan 16, 2025
@jminkkk jminkkk added this to the 7차 스프린트 💭 milestone Jan 16, 2025
@zeus6768
Copy link
Contributor

지지합니다.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
refactor 요구사항이 바뀌지 않은 변경사항
Projects
Status: Todo
Development

No branches or pull requests

2 participants