애플리케이션 로직과 도메인 로직이 각각 무엇인지에 대해 이해하고, 둘을 구분해 코드를 개선한 사례를 글로 풀어보겠습니다.
애플리케이션 로직? 도메인(비즈니스) 로직?
각각을 아래와 같이 짧게 정의해볼 수 있습니다. 여기서 도메인 로직의 정의는 조금 추상적으로 느껴집니다.
•
애플리케이션 로직: 도메인 로직이 의사결정을 할 수 있도록 입력을 제공하거나, 그 결과를 외부 서비스, DB, UI 등에 업데이트하는 역할을 맡음
•
도메인(비즈니스) 로직: 현실 세상의 문제에 대한 의사결정을 함
애플리케이션 로직만 있고 도메인 로직은 없는 시스템 vs 애플리케이션 로직만 있고 도메인 로직은 없는 시스템
이를 좀 더 쉽게 이해하기 위해 아래 예시에 대해 소거법을 이용해 차이를 구분해보겠습니다.
[예시] 예약 가능 시간 목록 조회 API V1
1.
해당 기간동안 예약된 시간들을 DB에서 가져온다 → 애플리케이션
2.
전체 시간들을 DB에서 가져온다 → 애플리케이션
3.
전체 시간에서 예약이 된 시간과 안 된 시간을 구분한다 → 도메인
4.
예약이 된 시간과 안 된 시간을 구분해 출력한다 → 애플리케이션
애플리케이션 로직만 있고 도메 로직인이 없는 시스템은 겉으론 동작을 하는 것처럼 보이지만 그 값이 사용자가 원한 해결책이 아닐 것입니다. 예시의 API V1에선 예약이 된 시간과 안된 시간이 제대로 구분되지 않고 출력될 것입니다.
반면, 도메인만 있고 애플리케이션 로직은 없는 시스템은 문제에 대한 의사결정은 가능하지만 이에 대한 입출력 로직이 부재해 실질적으론 문제를 해결하지 못하는 시스템이 될 것입니다. 예시의 API V1에선 예약이 된 시간과 안 된 시간을 구분하는 방법은 알지만 구분할 대상과 그 결과를 보여줄 방법이 부재하는 시스템이 될 것입니다.
다만 어려운 게 V2처럼 짤 수도 있는 것이기에, 동일한 문제에 대해서도 둘의 구분을 사람마다 상황마다 다르게 둘 수 있는 거 같습니다.
[예시] 예약 가능 시간 목록 조회 API V2
1.
전체 시간을 예약 여부를 구분해 DB에서 가져온다 → 애플리케이션
2.
예약이 된 시간과 안 된 시간을 구분해 출력한다 → 애플리케이션
애플리케이션 로직과 도메인 로직을 구분해 얻을 수 있는 장점
제가 생각하는 가장 큰 장점은 도메인 로직을 분리함으로써 문제 해결 과정을 더 잘 묘사할 수 있단 점이 있습니다. 예를 들어 첫 번째 코드를 두 번째 코드로 리팩토링함으로써 읽는 사람을 덜 혼란스럽게 할 수 있습니다.
•
리팩토링 전
// Service
public class ReservationTimeService {
public List<AvailableReservationTimeResponse> findAllAvailableReservationTime(LocalDate date, Long themeId) {
List<Long> bookedTimeIds = reservationRepository.findTimeIdByDateAndThemeId(date, themeId);
List<ReservationTime> reservationTimes = reservationTimeRepository.findAll();
return reservationTimes.stream()
.map(time -> toAvailableReservationTimeResponse(time, unavailableTimeIds))
.toList();
}
private AvailableReservationTimeResponse toAvailableReservationTimeResponse(
ReservationTime time, List<Long> bookedTimeIds) {
boolean alreadyBooked = isAlreadyBooked(time.getId(), bookedTimeIds);
return AvailableReservationTimeResponse.of(time, alreadyBooked);
}
private boolean isAlreadyBooked(Long targetTimeId, List<Long> bookedTimeIds) {
return unavailableTimeIds.contains(targetTimeId);
}
}
Java
복사
•
리팩토링 후
// Service
public class ReservationTimeService {
public List<AvailableReservationTimeResponse> findAllAvailableReservationTime(LocalDate date, Long themeId) {
List<Long> bookedTimeIds = reservationRepository.findTimeIdByDateAndThemeId(date, themeId);
List<ReservationTime> reservationTimes = reservationTimeRepository.findAll();
return reservationTimes.stream()
.map(time -> toAvailableReservationTimeResponse(time, bookedTimeIds))
.toList();
}
private AvailableReservationTimeResponse toAvailableReservationTimeResponse(
ReservationTime time, List<Long> bookedTimeIds) {
boolean alreadyBooked = time.isAlreadyBooked(bookedTimeIds);
return AvailableReservationTimeResponse.of(time, alreadyBooked);
}
}
// Domain
public class ReservationTime {
...
public boolean isAlreadyBooked(List<Long> bookedTimeIds) {
return bookedTimeIds.contains(this.id);
}
}
Java
복사
리팩토링 한 버전에선 애플리케이션 로직과 도메인 로직을 잘 구분해둠으로써 적절한 위치(레이어)에 배분해줬고, 문제 해결 과정을 좀 더 자연스럽게 묘사할 수 있었습니다. 리팩토링 전 코드에선 도메인 로직과 애플리케이션 로직이 혼용해 전부 서비스 레이어에 위치했지만, 리팩토링 후엔 도메인 로직과 애플리케이션 로직을 구분해 도메인 로직은 도메인 레이어로 옮겨주었습니다.
문제 해결 과정
1.
해당 기간동안 예약된 시간들을 DB에서 가져온다 → 애플리케이션
2.
전체 시간들을 DB에서 가져온다 → 애플리케이션
3.
전체 시간에서 예약이 된 시간과 안 된 시간을 구분한다 → 도메인
4.
예약이 된 시간과 안 된 시간을 구분해 출력한다 → 애플리케이션
별거 아니게 보일 수 있지만, 생각보다 많은 사람들이 도메인 로직과 애플리케이션 로직 구분 없이 수많은 로직들을 전부 서비스 레이어에 몰아넣는다 생각합니다. 앞으로 도메인 로직을 분리하는 데에 도움이 되길 기대하며 포스트로 작성해보았습니다.