[DDD] : 응용 영역

앞선 포스팅에서는 도메인 영역과 관련해 살펴 보았습니다. 이 도메인이 제 기능을 하기 위해서는 사용자와 도메인을 연결해 주는 매개체가 필요하며 응용 영역표현 영역이 이 매개체 역할을 합니다.

 

이번 포스팅에서는 응용 영역에 대해 알아보겠습니다.

응용 서비스

응용 서비스는 

사용자(클라이언트)가 요청한 기능을 실행합니다.
  • 주요 역할
    • 도메인 객체를 사용해 사용자의 역할을 처리
    • 트랜잭션 처리

응용 서비스는 주로 도메인 객체 간의 흐름을 제어하기에 아래와 같이 단순한 형태를 가집니다.

일반적인 기능

public Result doSomeFunc(SomeReq req) {
	
	// 1. 리포지터리에서 애그리거트를 구한다.
	SomeAgg agg = someAggRepository.findById(req.getId());
	checkNull(agg);
	
	// 2. 애그리거트의 도메인 기능을 실행한다.
	add.doFunc(req.getSomeValue());
	
	// 3. 결과를 리턴한다.
	return createSuccessResult(agg);
}

새로운 애그리거트 생성 기능

public Result doSomeCreation(CreateSomeReq req) {
	// 1. 데이터 중복 등 데이터가 유효한지 검사한다.
	validate(req);
	
	// 2. 애그리거트를 생성한다.
	SomeAgg newAgg = createSome(req);
	
	// 3. 리포지터리에 애그리거트를 저장한다.
	someAggRepository.save(newAgg);
	
	// 4. 결과를 리턴한다.
	return createSuccessResult(newAgg);
}

주의할 점

만약, 응용 서비스가 복잡하다면, 응용 서비스에서 도메인 로직 일부를 구현하고 있을 가능성이 높습니다. 응용 서비스가 도메인 로직을 일부 구현하게 되면 코드 중복, 로직 분산 등 코드 품질에 안 좋은 영향을 줄 수 있습니다.

도메인 로직을 도메인 영역과 응용 영역에 분산해서 구현하면 다음과 같은 문제가 발생할 수 있습니다.

  1. 코드의 응집성이 떨어집니다.
    • 도메인 로직을 파악하기 위해 여러 영역을 분산해야 하기 때문입니다.
  2. 여러 응용 서비스에서 동일한 도메인 로직을 구현할 가능성이 높아집니다.

이 두 가지 문제는 결과적으로 코드의 변경을 어렵게 만드는 원인이 됩니다. 그만큼 소프트웨어의 가치가 떨어지게 되기 때문에, 소프트웨어의 가치를 높이기 위해서는

도메인 로직을 도메인 영역에 모아서 코드 중복을 줄이고 응집도를 높여야 합니다.

 

응용 서비스의 구현

응용 서비스를 구현할 때 몇 가지 고려할 사항과 트랜잭션과 같은 구현 기술의 연동에 대해 알아보겠습니다.

응용 서비스의 크기

응용 서비스를 구현할 때 응용 서비스의 크기에 대해 고려할 점이 몇 가지 있습니다. 용용 서비스는 보통 다음의 두 가지 방법 중 한 가지 방식으로 구현합니다. 회원 도메인을 예시로 들어보겠습니다.

  1. 한 응용 서비스 클래스에 회원 도메인의 모든 기능 구현하기
  2. 구분되는 기능별로 응용 서비스 클래스를 따로 구현하기

첫 번째 방식은 다음과 같은 모습을 가집니다.

public class MemberService {
	// 각 기능을 구현하는 데 필요한 리포지터리, 도메인 서비스 필드 추가
	private MemberRepository memberRepository;
	
	public void join(MemberJoinRequest joinRequest) { ... }
	public void changePassword(String memberId, String curPwd, String newPwd) { ... }
	public void initializePassword(String memberId) { ... }
	public void leave(String memberId) { ... }
	...
}
  • 한 도메인과 관련된 기능을 구현한 코드가 한 클래스에 모두 위치합니다.
  • 각 기능에서 동일 로직에 대한 코드 중복을 제거할 수 있다장점이 존재합니다.
    • 예를 들어, 회원이 존재하지 않으면 NoMemberException을 발생시키는데, 이 로직을 사용하는 메서드가 여러 곳이라면 `private` 메서드를 구현해 이를 호출하는 방식을 사용할 수 있습니다.
  • 단점으로는 한 서비스 클래스의 크기가 커진다는 점입니다.
    • 코드가 커짐 → 연관성이 적은 코드가 한 클래스에 위치할 가능성 커짐 → 관련 없는 코드가 뒤섞여 코드 이해에 방해

 

두 번째 방식은 한 응용 서비스 클래스에서 한 개 내지 2 ~ 3개의 기능을 구현합니다. 아래와 같이 암호 변경 기능만을 위한 응용 서비스 클래스를 별도로 구현하는 식입니다.

public class ChangePasswordService {
	private MemberRepository memberRepository;

	public void changePassword(String memberId, String curPwd, String newPwd) { 
		Member member = memberRepository.findById(memberId);
		if(member == null) throw new NoMemberException(memberId);
		member.changePassword(curPwd, newPwd);
	}
	...
}
  • 클래스 개수는 많아지지만 코드 품질을 일정 수준으로 유지하는 데 도움이 됩니다.
  • 각 클래스별로 필요한 의존 객체만 포함하므로 다른 기능을 구현한 코드에 영향을 받지 않게 됩니다.

이 방식은 각 기능마다 동일한 로직을 구현할 경우, 여러 클래스에서 중복된 코드를 구현할 가능성이 있습니다. 이 경우, 아래와 같이 별도의 `helper` 클래스에 로직을 구현해서 코드가 중복되는 것을 방지할 수 있습니다.

// 각 응용 서비스에서 공통되는 로직을 별도의 클래스로 구현
public final class MemberServiceHelper {
	public static Member findExistingMember(MemberRepository memberRepository, String memberId) {
		Member member = memberRepository.findById(memberId);
		if(member == null) throw new NoMemberException(memberId);
		return member;
	}
}

// 공통 로직을 제공하는 메서드를 응용 서비스에서 사용
import static com...MemberServiceHelper.*;

public class ChangePasswordService {
	private MemberRepository memberRepository;

	public void changePassword(String memberId, String curPwd, String newPwd) {
		Member member = findExistingMember(memberRepository, memberId);
		member.changePassword(curPwd, newPwd);
	}
	...
}

 

값 리턴

응용 서비스에서 표현 영역으로 값을 넘겨줄 때는

필요한 데이터만 리턴하는 것이 좋습니다.

 

예를 들어, 주문 생성 기능에서 응용 서비스는 표현 영역에 OrderNo(주문 번호)만 주는 것이 아닌 Order 즉, 애그리거트 자체를 리턴해 사용할 수도 있습니다.

하지만 이 경우, 도메인의 로직 실행을 응용 서비스와 표현 영역 두 곳에서 할 수 있게 됩니다. 결과적으로 기능 실행 로직을 응용 서비스와 표현 영역에 분산시켜 코드의 응집도를 낮추는 원인이 됩니다.

따라서, 

응용 서비스는 표현 영역에서 필요한 데이터만 리턴하는 것이 기능 실행 로직의 응집도를 높이는 확실한 방법입니다.

 

표현 영역에 의존하지 않기

응용 서비스의 파라미터 타입을 결정할 때,

표현 영역과 관련된 타입을 사용하면 안됩니다.
@PostMapping
public String submit(HttpServletRequest request) {
	try {
		changePasswordService.changePassword(request);
	} catch (NoMemberException e) {
		...
	}
}

예를 들어, 위와 같이 표현 영역에 해당하는 `HttpServletRequest`나 `HttpSession`을 응용 서비스에 파라미터로 전달하면 안됩니다. 다음과 같은 문제점이 있습니다.

  • 응용 서비스만 단독으로 테스트하기 어려워진다.
  • 표현 영역의 구현이 변경되면 응용 서비스의 구현도 함께 변경해야 한다.
  • 응용 서비스가 표현 영역의 역할을 대신하면 표현 영역의 응집도가 깨지게 된다.

이를 위해 서비스 메서드의 파라미터와 리턴 타입으로 표현 영역의 구현 기술을 사용하지 않아야 합니다.

 

트랜잭션 처리

트랜잭션을 관리하는 것은 응용 서비스의 중요한 역할 중 하나입니다.

스프링이 제공하는 트랜잭션 관리 기능을 이용하면 다음과 같이 쉽게 트랜잭션을 처리할 수 있습니다.

public class ChangePasswordService {
	
	@Transactional
	public void changePassword(ChangePasswordRequest req) {
		Member member = findExistingMember(req.getMemberId());
		member.changePassword(req.getCurPwd(), req.getNewPwd());
	}
	...
}
  • 스프링은 `@Transactional`이 적용된 메서드
  • `RuntimeException`을 발생시키면 트랜잭션을 롤백
  • 그렇지 않으면 커밋

이를 통해 트랜잭션 처리 코드를 간결하게 유지할 수 있습니다.

'DDD' 카테고리의 다른 글

[DDD] : 표현 영역  (0) 2025.03.11
[DDD] : 리포지터리와 모델 구현  (0) 2025.03.06
[DDD] : 아키텍처  (0) 2025.03.04
[DDD] : 도메인 모델  (0) 2025.02.27