7. 서비스

개발 99·2025년 4월 23일

"서비스의 역할은 도메인 객체나 도메인 서비스라고 불리는 도메인에 일을 위임하는 공간이어야 한다"

  1. 도메인 객체를 불러온다.

  2. 도메인 객체나 도메인 서비스에 일을 위임한다.

  3. 도메인 객체의 변경 사항을 저장한다.

7.1 Manager

  • 컨트롤러 = 제어부

  • 리포지터리 = 저장소

  • 컴포넌드 = 구성 요소

  • 서비스 = 서비스?

스프링 서비스는 DDD에서 파생된 개념이다.

DDD는 '도메인 주도 설계'로서 도메인 중심에 놓고 소프트웨어를 설계하는 방법
도메인 엔티티 = 비즈니스 영역, 문제 해결 영역

그렇다면, 개발자는 도메인(ex. 은행 시스템)에 대해서 누구보다 잘 이해를 하고 있어야 한다.

도메인에 대한 정보는 도메인 전문가( 비즈니스 종사자 )와 소통을 통해서 정보를 획득할 수 있다.
이를 통해서 핵심 개념들을 추출하는데, 이 방식을 '이벤트 스토밍'이라고 한다.

도메인을 탐색하고, 탐색한 내용을 바탕으로 소프트웨어를 설계하는 것이 DDD이다.

서비스는 이런 도메인 객체가 처리하기 애매한 '연산'자체(활동, 행동)를 표현하기 위한 컴포넌트이다.

대부분의 비즈니스 로직이 객체지향을 따르는 도메인 객체에 존재해야 하며, 서비스는 이를 위한 장이다.

그러면 어떻게 객체지향 컨셉을 적용하나?

@Service
@RequiredArgsConstructor
public class ProductService{
	...
    
    public int calculatePrice(long userId, long productId){
    	
        ...
        
        // 최대 할인율을 찾는 로직
        ...
        
        // 적용 가능한 쿠폰이 있다면 적용하는 로직
        ...
        
        // 사용자의 마일리지도 반영하는 로직
        ...
    }
}

위 처럼 작성하면 트랜잭션 스크립트이다. 그냥 일을 쭉 나열한 것에 불과하다.

그래서 아래와 같이 수정.

@Service
@RequiredArgsConstructor
public class ProductService{
	...
    
    public int calculatePrice(long userId, long productId){
    	
		User user = ...
        Product product = ...
        List<Coupon> coupons = ...
        
        PriceManager priceManager = new PriceManager();
        return priceManager.calculate(user,product,coupons);
        
    }
}

여기서 Manager가 바로 서비스이다.( PriceManager = PriceService )
Manager는 접두어(Price)에 있는 모델을 관리하는 클래스를 의미한다.

서비스 컴포넌트는 다음과 같은 일을 처리한다.

  1. 저장소에서 데이터를 불러온다.

  2. 네트워크 호출 결과를 정리해서 객체에 넘겨준다.

  3. 저장소에 데이터를 저장한다.

이 로직 그 자체가 연산이 도메인 객체에 넣기 어렵기 때문에, 서비스(매니저)가 처리한다.

그래서 이 코드에선 스프링 컴포넌트에 의한 @Service와 PriceManager가 필요하다.
(서비스가 서비스를 실행하는 꼴이다.)

PriceManager는 비즈니스 업무 규칙을 갖고 있으므로 '도메인'에 가까운 로직이다.
@Service는 애플리케이션의 실행에 초점을 맞춰 있다.(비즈니스 로직은 아님)

  • @Service = 애플리케이션 서비스

  • Manager = 복잡한 비즈니스 로직을 처리하기 위한 도메인 서비스이다.

요약

  • 스프링의 서비스는 DDD에서 유래

  • DDD에서 도메인은 비즈니스 영역이며 문제 영역

  • DDD에서 서비스는 도메인 문제를 해결하기 위한 패턴 중 하나

  • 서비스는 객체가 처리하기에 애매한 연산 로직을 갖고 있는 컴포넌트이다.

  • 도메인 개발에 필요하지만 객체로 표현하기 애매한 로직을 처리하는 서비스 = 도메인 서비스

  • 애플리케이션 개발에 필요하지만 객체로 표현하기 애매한 로직 = 애플리케이션 서비스

애플리케이션 서비스는 도메인을 저장소에서 불러오고, 도메인 서비스를 실행하고, 도메인을 실행한다.
즉, 본격적인 도메인 로직 처리는 도메인 서비스가 담당을 한다.

서비스는 저장소에서 객체를 불러오고, 객체에 일을 시키고, 객체를 반환하는 정도의 일만 처리하는 공간에 불과하다.

스프링 서비스는 도메인과, 도메인 서비스의 파사드(정면)처럼 사용하는 공간이다.
(본 내용은 무조건 도메인 서비스가 처리)

7.2 서비스보다 도메인 모델

앞서 PriceManager보단 Cashier를 쓰는게 낫다.
(뭐가 됐든간에 결국 도메인이 비즈니스 로직을 처리하는 것은 변함없다.)

// 이전 코드
...

PriceManager priceManager = new PriceManager();
return priceManager.calculate(user,product,coupons);

// 아래는 수정
Cashier cashier = new Cashier();
return cashier.calculate(user,product,coupons);

클래스 이름은 생각보다 더 많은 것을 결정하므로, 네이밍이 중요하다.

도메인과 도메인 서비스를 구분 짓는 것은 행동으로 결정된다.

객체지향으로 보는 서비스
1. 서비스는 가능한 한 적게 만들고, 얇게 유지해야 한다.(로직 길이가 짧게) = 비즈니스 로직을 도메인 객체로 옮겨라.
2. 서비스보다 풍부한 도메인 모델을 만들어야 한다.

개발 우선순위
도메인 모델 > 도메인 서비스 > 애플리케이션 서비스 (테스트에 용이한 순서)
도메인 모델은 특정 인프라에 의존하지 않기 때문이다.

7.3 작은 기계

  • 서비스는 한번 생성하면 여러 번 사용하지만 그 자신은 바꿀 수 없다.(불변성)
    계산식 그 자체이므로 불변이다.( 같은 입력에 항상 같은 응답, 함수와 동일 )

  • 서비스는 작은 기계처럼 영원히 실행할 수 있다.

생성자 주입을 하는 이유

  1. 명시적으로 의존성 표현

  2. 테스트 용이

  3. 순환 의존성 방지

그러나 무엇보다도 서비스는 원래 불변이다.

(필드 주입, 수정자 주입은 불변이 아니다.)

7.4 조언

  1. 서비스의 멤버 변수는 모두 final

  2. 서비스에 세터가 존재한다면 삭제

  3. 서비스는 반드시 생성자 주입으로 바꾼다.

  4. 서비스의 비즈니스 로직을 도메인에 양보한다.

  5. 서비스를 얇게 유지하라.

profile
구구구구구!

0개의 댓글