서비스와 도메인

PPakSSam·2022년 1월 30일
0
post-thumbnail

도메인간의 의존성을 서비스로 가져오지 말자


[LineService]

public LineResponse saveLine(LineRequest request) {
    validateDuplicateLineName(request.getName());

    Line line = lineRepository.save(new Line(request.getName(), request.getColor()));
    Section section = createSection(request, line);
    line.getSections().add(section);

    return LineResponse.of(line);
}

private Section createSection(LineRequest request, Line line) {
    Station upStation = findStationById(request.getUpStationId());
    Station downStation = findStationById(request.getDownStationId());

    return Section.builder()
            .line(line)
            .upStation(upStation)
            .downStation(downStation)
            .distance(request.getDistance())
            .build();
}

위의 코드는 새로운 노선(2호선)을 생성하는 코드이다.
노선(Line)을 생성하려면 구간(Section)이 필요하므로 어쩔 수 없이 Line과 Section은 의존성이 생길 수 밖에 없다.
그래서 나는 일단 서비스 레이어에서 Line 생성 -> Section 생성 -> Line에 Section추가의 과정을 넣었다. 그런데 다음과 같은 리뷰를 받았다.

섹션을 LineService에서 생성하여 line에 넘겨줄 경우 LineService는 Section에 의존하게 됩니다.
제거할 수 있는 의존을 최대한 제거하기 위해서는 Section을 생성하는 로직을 line 내부로 옮기는 것을 추천드립니다.
즉, LineService가 Section의 존재를 모르게 하면 LineService가 Section에 의존하지 않게 됩니다.
아래와 같은 형태가 될 수 있도록 리팩터링 해보세요 :)

정리를 하자면 노선과 구간은 의존성을 가질 수 밖에 없는데 즉 Line과 Section 도메인은 서로 의존성을 이미 가지는데 Service에서 마저 의존성을 가질 필요는 없다는 것이다.

그래서 위의 코드를 다음과 같이 바꾸었다.

[LineService]

public LineResponse saveLine(LineRequest request) {
    validateDuplicateLineName(request.getName());

    Station upStation = findStationById(request.getUpStationId());
    Station downStation = findStationById(request.getDownStationId());

    Line line = new Line(request.getName(), request.getColor());
    line.addSection(upStation, downStation, request.getDistance());
    lineRepository.save(line);

    return LineResponse.of(line);
}

[Line]

public void addSection(Station upStation, Station downStation, int distance) {
    Section section = Section.builder()
            .line(this)
            .upStation(upStation)
            .downStation(downStation)
            .distance(distance)
            .build();

    sections.add(section);
}

이렇게 함으로써 LineService는 오로지 Line에 대한 의존성만을 가지게 되었고 만약 Section의 로직이 바뀐다면 Line만 고치면 되는 것이니 유지보수 면에서 더 좋은 코드가 되었다.

비즈니스 로직은 도메인에서!


[LineService]

public void saveSection(SectionRequest request, Long lineId) {
    Line line = lineService.findById(lineId);
    validateSaveSection(new Sections(line.getSections()), request);

    Station upStation = findStationById(request.getUpStationId());
    Station downStation = findStationById(request.getDownStationId());

    line.addSection(upStation, downStation, request.getDistance());
}

private void validateSaveSection(Sections sections, SectionRequest request) {
    if(!sections.isDownStation(request.getUpStationId())) {
        throw new BadRequestException();
    }

    if(sections.isRegisteredStation(request.getDownStationId())) {
        throw new BadRequestException();
    }
}

위의 코드는 기존의 지하철 노선(2호선)에 새로운 구간(문래역 ~ 영등포구청역)을 추가하는 로직이다. 보면 알 수 있듯이 예외처리를 서비스 레이어에서 하고 있다.

그런데 이는 절차지향적 코드이다. 이를 객체지향적 코드로 바꾸려면 서비스 레이어에서는 도메인의 메소드를 호출하는 순서를 결정하고 비즈니스 로직은 도메인에서 처리하도록 해야한다.

무슨 말이냐면 LineService가 해야하는 일은 단순히 Line의 addSection 메소드를 호출하는 일 뿐이지 구간을 추가할 때 검증하는 비즈니스로직은 LineService가 관여할 일이 아니라는 것이다. 이러한 관점에서 코드를 짜보면 다음과 같이 변하게 된다.

[LineService]

public void addSection(Long lineId, SectionRequest request) {
    Line line = findLineById(lineId);
    Station upStation = findStationById(request.getUpStationId());
    Station downStation = findStationById(request.getDownStationId());

    line.addSection(upStation, downStation, request.getDistance());
}

[Line]

public void addSection(Station upStation, Station downStation, int distance) {
    Section section = Section.builder()
            .line(this)
            .upStation(upStation)
            .downStation(downStation)
            .distance(distance)
            .build();

    sections.add(section);
}

[Sections]

public void add(Section section) {
    if(sections.isEmpty()) {
        sections.add(section);
        return;
    }

    validateAddSection(section);
    sections.add(section);
}

private void validateAddSection(Section section) {
    if(!isDownStation(section.getUpStation())) {
        throw new BadRequestException();
    }

    if(isRegisteredStation(section.getDownStation())) {
        throw new BadRequestException();
    }
}

위의 코드를 보면 Service는 단순히 도메인의 메소드를 호출하는 순서만 보장하고 비즈니스 로직은 도메인에서 처리함을 알 수 있다. 이에 대한 장점 또한 유지보수적 관점에서 좋은 것인데, 만약 비즈니스 로직을 서비스에서 처리했을 때 구간(Section)에 대한 변경사항이 발생하면 서비스와 도메인 모두 바꿔야 하지만 비즈니스 로직을 도메인에서 처리한다면 도메인에서만 변경하면 되는 것이다.

도메인간의 의존성을 서비스로 가져오지 말자비즈니스 로직은 도메인에서!는 결국 의존성을 없애는 관점에서 일맥상통하는 이야기인 것이다.

참고

묻지 말고 시켜라

객체는 다른 객체의 상태를 묻지 말아야 한다. 객체가 다른 객체의 상태를 묻는다는 것은 메세지를 전송하기 이전에 객체가 가져야 하는 상태에 관해 너무 많이 고민하고 있다는 증거다. 고민을 연기하라단지 필요한 메세지를 전송하기만 하고 메세지를 받는 객체가 스스로 메세지의 처리 방법을 결정하게 하라.

절차적인 코드는 정보를 얻은 후에 결정한다. 객체지향 코드는 객체에게 그것을 하도록 시킨다.

묻지 말고 시켜라 원칙을 따르면

  • 밀접하게 연관된 정보와 행동을 함께 가지는 객체를 만들 수 있다.

  • 객체의 정보를 이용하는 행동을 객체의 외부가 아닌 내부에 위치시키기 때문에 자연스럽게 정보와 행동을 동일한 클래스 안에 두게 된다.

  • 보다 자연스럽게 정보 전문가에게 책임을 할당하게 되고 높은 응집도를 가진 클래스를 얻을 확률이 높아진다.

훌륭한 인터페이스를 수확하기 위해서는 객체가 어떻게 작업을 수행하는지를 노출해서는 안된다. 인터페이스는 객체가 어떻게 하는지가 아니라 무엇을 해야하는지를 서술해야 한다. 이것이 바로 캡슐화이다.

profile
성장에 대한 경험을 공유하고픈 자발적 경험주의자

0개의 댓글