[ODOP] 100일차 - ApplicationService 와 DomainService

[ODOP] 100일차 - ApplicationService 와 DomainService

많은 백엔드 개발자들은 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 들은 서로를 호출할 수 없게 된다.

먼저, 설명하기 위해 머릿속에서 아래의 과정을 그려보자

  1. UserService 와 ScheduleService 가 있다
  2. UserService 는 ScheduleService 를 사용하고 있다
  3. 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 받아서 코드를 복사 붙여넣기 하면 된다.

중복 코드가 양산되고 서비스의 경계가 허물어진다.

그럼 어떻게 이 문제를 해결할까?

내일 이에 대해 살펴보겠다.