📢 각 도메인 별 Repository 에 의존할 경우 도메인 규칙을 정해놓은 서비스를 이용하지 않아 유지보수성이 떨어지게 됩니다.
예를 들어 지갑(Wallet) 엔티티에 돈을 빼는 메서드가 있다고 합시다. Wallet에 돈을 빼면 땅바닥에 둘 수 없으니, 다른 Wallet (ex. Admin Wallet) 에 보관합니다. 위 로직은 Entity 내에 작성할 수 없는 코드입니다.
이를 간단한 예시로 설명 드리겠습니다.
문제 상황
초기 상태
WalletService 는 돈을 빼는 비즈니스 로직을 가지고 있습니다. 돈을 뺀 후에는 특정 규칙(여기서는 어드민 지갑에 추가) 이 적용됩니다.
public class WalletService {
private final WalletRepository walletRepository;
@Transational
public void 돈빼는메서드(Long walletId) {
Wallet wallet = findById(walletId);
int money = wallet.돈을뺀다(1000원);
// 규칙: 돈을 뺀 후 어드민 지갑에 추가
Wallet adminWallet = walletRepository.findById(1); // 어드민 아이디는 1로 고정
adminWallet.돈을추가한다(money);
}
public Wallet findById(Long walletId) {
return walletRepository.findById(walletId); // 예외 생략
}
}
기능 추가 및 문제 발생
근데 팀 내에 신입 개발자가 들어오고, 각종 기능이 추가되기 시작했습니다.
사람이 돈을 빼는 행동이 필요하다고 해서 Wallet 을 Human 이 다루게 되었습니다.
이것을 HumanService 에서 구현했지만, 신입은 규칙을 몰랐습니다.
public class HumanService {
private final HumanRepository humanRepository;
private final WalletRepository walletRepository;
@Transational
public void 사람이돈을빼는메서드(Long humanId) {
Human human = findById(humanId);
Wallet humanWallet = walletRepository(human.getWalletId());
// 규칙 누락: 돈을 뺀 후 어드민 지갑에 추가하지 않음
humanWallet.돈을뺀다(1000원);
}
public Human findById(Long humanId) {
return humanRepository.findById(humanId); // 예외 생략
}
}
결국 규칙이 누락되면서 전체적으로 도메인 규칙이 맞지 않게 되었습니다.
규칙 변경 시
다행히도 신입이 잘 규칙을 적용했습니다. 하지만 HumanService 에 규칙을 적용 시켰습니다.
Human human = findById(humanId);
Wallet humanWallet = walletRepository(human.getWalletId());
humanWallet.돈을뺀다(1000원);
Wallet adminWallet = walletRepository.findById(1); // 어드민 아이디는 1로 고정
adminWallet.돈을추가한다(money);
규칙이 다시 바뀝니다. 이제 어드민 지갑에 돈을 추가하지 않고 그냥 지워버립니다.
public class WalletService {
private final WalletRepository walletRepository;
@Transational
public void 돈빼는메서드(Long walletId) {
Wallet wallet = findById(walletId);
int money = wallet.돈을뺀다(1000원);
// 기존 규칙 삭제: 어드민 지갑에 돈을 추가하지 않음
// Wallet adminWallet = walletRepository.findById(1);
// adminWallet.돈을추가한다(money);
}
}
WalletService 는 수정이 되었지만, HumanService는 수정되지 않았습니다.
서비스가 운영된지 한참이 지나 HumanService 에 Wallet 의 규칙이 적용된지는 아무도 모릅니다.
결국 문제가 발생하게 됩니다.
그래서 Service끼리 의존해야하는 것입니다.
올바른 설계 예시
public class WalletService {
private final WalletRepository walletRepository;
@Transational
public void 돈빼는메서드(Long walletId) {
Wallet wallet = findById(walletId);
int money = wallet.돈을뺀다(1000원);
// 변경된 규칙 적용: 돈을 버림 (더 이상 어드민 지갑에 추가하지 않음)
}
public Wallet findById(Long walletId) {
return walletRepository.findById(walletId); // 예외 생략
}
}
public class HumanService {
private final HumanRepository humanRepository;
private final WalletService walletService;
@Transational
public void 사람이돈을빼는메서드(Long humanId) {
Human human = findById(humanId);
// HumanService는 WalletService를 호출하여 도메인 규칙을 따름
walletService.돈빼는메서드(human.getWalletId());
}
public Human findById(Long humanId) {
return humanRepository.findById(humanId); // 예외 생략
}
}
레포지토리는 데이터 접근만을 책임지고 비스니스 로직은 서비스 계층에서 관리해야합니다.
그러니까 어? 그럼 레포지토리 단에 규칙을 적용하면 되는 거 아닌가? 라고 생각하지마세요.
저도 오래 고민해봤는데 아닌 거 같아요.
서비스 레이어 테스트를 할 때 테스트의 범위가 모호해질 수 있습니다.
결국 유지 보수가 떨어집니다.
저도 예전에 정말 궁금해서 검색을 해봤는데
소규모 프로젝트에서는 된다는 말이 많습니다.
저는 다 귀찮아서 만들어내는 핑계라고 생각합니다.
👉 "소규모 프로젝트니까 된다" 는 핑계며, 오히려 작은 프로젝트일수록 더 짜임새있는 설계가 필요합니다.
순환참조랑 쓸데없이 많은 메서드를 가진 클래스를 의존 받는 건 어떻게 할 건데!!!!!!!!!!
관리 힘들면 서비스를 더 잘게 나누어 관리하세요
ReadService
SaveService 등
이것도 안 된다면, 설계가 잘못된게 아닌가를 의심해봐야 합니다.
그리고 메서드가 많다고 해서 스프링은 싱글톤 빈을 사용하니 메모리는 별 영향 없습니다.
서비스 나누는 건 적절히 트레이드 오프하는게 좋아보여요
'CS' 카테고리의 다른 글
RAG란 무엇일까? (0) | 2025.02.01 |
---|---|
@Transactional 은 어떻게 작동할까요? (0) | 2024.12.24 |