[ODOP] 100일차 - ApplicationService 와 DomainService
![[ODOP] 100일차 - ApplicationService 와 DomainService](/content/images/size/w1200/2023/09/odop-4.png)
많은 백엔드 개발자들은 DomainService 와 ApplicationService 를 구분하지 않고 한 클래스에 몰아넣어 사용한다
이렇게 되면 여러 문제가 발생하게 되는데, 문제를 짚어보기 전에 먼저 DomainService 가 무엇인지, ApplicationService 가 무엇인지 알아보자
도메인 서비스와 애플리케이션 서비스
- DomainService: 도메인을 풀어나가는 서비스
- ApplicationService: 애플리케이션의 비즈니스를 풀어나가는 서비스
다음의 코드를 보자
@Service
@RequiredArgsConstructor
public class DomainService {
private final DomainRepository domainRepository;
public Domain get(Long id) throws NotFoundException {
return domainRepository.findOne(id) .orElseThrow(NotFoundException::new);
}
public Domain create(String name) {
Domain persistTarget = new Domain(name);
if (domainRepository.isExist(persistTarget)) {
// 이름이 중복되어 가입 불가
throw new DuplicateKeyException("Duplicated name");
}
return domainRepository.save(persistTarget);
}
}
도메인 서비스일까 애플리케이션 서비스일까?
정답은 둘 다 될 수 있다
이다.
둘의 특성을 모두 가지고 있기 때문이다. 도메인을 풀어나가고 있음과 동시에 비즈니스를 구현하고 있을 수 있다.
따라서 도메인 서비스와 애플리케이션 서비스의 차이를 이 코드에서는 보기 힘들다.
그럼 아래의 코드는 어떨까?
@Service
@RequiredArgsConstructor
public class DomainService {
private final DomainRepository domainRepository;
private final UserRepository userRepository;
private final ScheduleService scheduleService;
public Domain get(Long id) throws NotFoundException {
return domainRepository.findOne(id)
.orElseThrow(NotFoundException::new);
}
public Domain create(String domainName, Long userId) {
Domain persistTarget = new Domain(domainName);
if (userRepository.findById(userId).isPresent()) {
// 유저가 존재하는 경우 특정 비즈니스
Schedule schedules = scheduleService.getSchedules(2L, YearMonth.now());
// 스케쥴에 등록하는 비즈니스
}
if (domainRepository.isExist(persistTarget)) {
// 이름이 중복되어 가입 불가
throw new DuplicateKeyException("Duplicated name");
}
return domainRepository.save(persistTarget);
}
}
도메인 서비스일까 애플리케이션 서비스일까?
정답은 애플리케이션 서비스이다
위 서비스를 도메인 서비스라고 부를 수 없는 이유는 다음과 같다
- DomainService 와 전혀 연관성이 없는 DomainService 에서 UserRepository 를 가지고 User 도메인을 다루고 있다.
- ScheduleService 는 다른 도메인 서비스이나, 해당 도메인 서비스에서 합쳐서 특정한 비즈니스를 구현하고있다
한가지의 도메인을 다뤄야 하지만 여러가지의 도메인을 다룸으로서 강한 의존성이 생겼다
이게 왜 문제인가?
위와 같이 애플리케이션 서비스를 무분별하게 만들어 사용하면 여러 문제가 발생하게 되는데, 하나 하나 알아보자
만능 서비스
위와 같이 Repository, Service 를 무분별하게 아무곳에서나 호출이 가능하다.
그럼 결국 관련 있는 비즈니스는 모두 하나의 서비스로 모이게 되며 연관되는 모든 비즈니스를 서비스 하나로 처리할 수 있게 된다.
말 그대로 만능 서비스가 탄생하는 것이다.
충격적인 것은 딱 하나의 클래스만 그러는 것이 아니라 대부분의 서비스 클래스가 같은 형태로 만능 서비스가 될 것이라는 부분이다.
순환 참조
Service 들은 서로를 호출할 수 없게 된다.
먼저, 설명하기 위해 머릿속에서 아래의 과정을 그려보자
- UserService 와 ScheduleService 가 있다
- UserService 는 ScheduleService 를 사용하고 있다
- ScheduleService 는 새로운 기능 개발을 위해 UserService 를 사용하고자 한다.
그럼 이 때 ScheduleService 는 UserService 를 DI 받을 수 있는가?
받지 못한다. 순환참조가 일어나기 때문이다.
그렇다면 어떻게 해결할까?
끊임없이 상승하는 코드의 복잡도
좋지는 않은 해결 방법이지만 많은 곳에서 사용하는 해결 방법은 이렇다.
한 단계 낮은 레이어인 Repository 를 ScheduleService 에서 받아 사용한다.
UserService 에서 구현해서 사용해야 할 기능을 ScheduleService 에서 사용하려니 순환참조가 발생한다.
그럼 간단하게 UserService 에서 사용해야 할 UserRepository 를 ScheduleService 에서 DI 받아서 User 도메인 코드를 작성한다.
간단히 해결되었다.
... 정말 해결되었다고 생각하는 사람이 없길 바란다.
마땅히 UserService 에 있어야 할 기능이 ScheduleService 로 갔고, 이는 코드의 복잡성을 증대시킨다
어디 사용하는 서비스가 하나 뿐이겠는가? 처음엔 3개, 4개. 어느새 10개가 넘어버린 의존성을 허용 할 것이다.
중복 코드의 양산
그럼 새로운 가정을 추가해보자
TearService 라는 새로운 기능을 추가했다
TearService 가 ScheduleService 에 구현된 User 도메인 기능을 사용하려면?
ScheduleService 를 DI 받거나 UserRepository 를 똑같이 DI 받아서 코드를 복사 붙여넣기 하면 된다.
중복 코드가 양산되고 서비스의 경계가 허물어진다.
그럼 어떻게 이 문제를 해결할까?
내일 이에 대해 살펴보겠다.