Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

패키지 구조 개선 및 리팩토링 예시 코드 작성 #527

Open
wants to merge 2 commits into
base: develop-backend
Choose a base branch
from

Conversation

ksk0605
Copy link
Contributor

@ksk0605 ksk0605 commented Sep 7, 2024

PR의 목적이 무엇인가요?

지속 성장 가능한 소프트웨어를 위한 구조 개선 예시 코드 작성

이슈 ID는 무엇인가요?

설명

Service란 무엇인가?

Indicates that an annotated class is a "Service", originally defined by Domain-Driven Design (Evans, 2003) as "an operation offered as an interface that stands alone in the model, with no encapsulated state."
May also indicate that a class is a "Business Service Facade" (in the Core J2EE patterns sense), or something similar. This annotation is a general-purpose stereotype and individual teams may narrow their semantics and use as appropriate.

Service는 본래 DDD(Domain-Driven Design) 이라는 개발 철학에서 제시한 개념이다. 그리고 서비스의 본질은 “비즈니스 서비스의 파사드” 라고 한다. 즉 도메인 로직들의 세부 구현이 드러나는 것이 아니라 그 자체로 비즈니스 흐름이 보여야 한다.

따라서 서비스 코드의 전체적인 형태는 다음과 같아야 한다.

Something doSomething(){
		Something something = somethingFinder();
		something.do();
		somethingWriter.write(something);
		return something;
}
  1. 비즈니스에 필요한 도메인들을 찾는다.
  2. 도메인에게 적절한 일을 시킨다.
  3. 변경사항들을 적용한다.
  4. 결과를 전달한다.

적용 예시

findMoimRole

	@Transactional(readOnly = true)
	public MoimRoleFindResponse findMoimRole(Long darakbangId, Long moimId, DarakbangMember darakbangMember) {
		Optional<Chamyo> chamyoOptional = chamyoRepository.findByMoimIdAndDarakbangMemberId(moimId,
			darakbangMember.getId());
		chamyoOptional.ifPresent(chamyo -> {
			if (chamyo.getMoim().isNotInDarakbang(darakbangId)) {
				throw new ChamyoException(HttpStatus.BAD_REQUEST, ChamyoErrorMessage.MOIM_NOT_FOUND);
			}
		});

		MoimRole moimRole = chamyoOptional.map(Chamyo::getMoimRole).orElse(MoimRole.NON_MOIMEE);

		return new MoimRoleFindResponse(moimRole.name());
	}

위 코드를 분석한다면 크게 다음과 같은 비즈니스 흐름을 가진다.

  1. 모임을 찾는다.
    1. 역할을 찾으려는 모임이 실제로 다락방에 존재하는지 확인한다.
  2. 모임에서 자신의 역할을 찾는다.
    1. 멤버가 모임에 참여하고 있는지 검증한다
  3. 역할을 반환한다.

위에서 볼드체로 표기된 부분이 서비스코드에서 드러나야하는 비즈니스 흐름이며 하위 항목들이 세부 구현이라 할 수 있다. 따라서 우리는 비즈니스로직은 business layer구현로직은 implement layer에 작성하도록 한다. 개선한 코드는 아래와 같다.

@Transactional(readOnly = true)
public MoimRoleFindResponse findMoimRole(Long darakbangId, Long moimId, DarakbangMember darakbangMember) {
	Moim moim = moimFinder.find(moimId, darakbangId);
	MoimRole moimRole = chamyoFinder.readMoimRole(moim, darakbangMember);

	return new MoimRoleFindResponse(moimRole.name());
}
public class MoimFinder {

	private final MoimRepository moimRepository;

	public Moim find(long moimId, long darakbangId) {
		return moimRepository.findByMoimIdAndDarakbangId(moimId, darakbangId)
			.orElseThrow();
	}
}
public class ChamyoFinder {

	private final ChamyoValidator chamyoValidator;
	private final ChamyoRepository chamyoRepository;

	public Chamyo read(Moim moim, DarakbangMember darakbangMember) {
		Chamyo chamyo = find(moim.getId(), darakbangMember).orElseThrow();
		chamyoValidator.validateInDarakbang(chamyo.getMoim(), darakbangMember.getDarakbang().getId()); // <- 응집도에 대한 고민 필요.
		return chamyo;
	}

	private Optional<Chamyo> find(long moimId, DarakbangMember darakbangMember) {
		return chamyoRepository.findByMoimIdAndDarakbangMemberId(moimId,
			darakbangMember.getId());
	}

	public MoimRole readMoimRole(Moim moim, DarakbangMember darakbangMember) {
		Optional<Chamyo> chamyo = find(moim.getId(), darakbangMember);
		if (chamyo.isPresent()) {
			return chamyo.get().getMoimRole();
		}
		return MoimRole.NON_MOIMEE;
	}
}

이때 주의해야하는 것은 ChamyoValidator 와 같이 협력하는 구현객체가 존재할 때 응집도에 대한 고민을 충분히 한 후에 배치하도록 한다. 가령 검증이 비즈니스 흐름에서 드러나야 할 정도로 중요한 로직이라면 Service 클래스에 배치하거나 모든 찾는 행위에 검증이 필요하다면 find 내부에 배치하는 등 충분한 고민 후에 적용한 후 문서로 팀원들에게 전달해야한다.

또 필요하다면 새로운 도메인 객체를 고려한다.

User user = userFinder.find(userId);
Product product = productFinder.find(productId);
List<Coupon> coupons = couponFinder.find(user);

// 복잡한 가격 계산로직
Coupon target;
for (Coupon coupon : coupons) {
		if (coupon.canDiscount(price)) {
			 target = coupon;
		{
}

int price = product.getPrice();
if (target != null) {
		price -= target.getDiscountAmount();
}
price -= user.getMilege();
return price;

가격을 계산하는 역할은 누구의 것인가?

  1. 유저가 계산한다.
  2. 쿠폰이 계산한다.
  3. 상품이 계산한다.

셋 모두 애매한 부분이 있다고 느껴진다면 아래와 같이 개선한다.

// 개선한 로직 
User user = userFinder.find(userId);
Product product = productFinder.find(productId);
List<Coupon> coupons = couponFinder.find(user);

// 1번 옵션: Manager 클래스를 만든다.
int price = priceManager(user, coupons, product);
return price;

위 경우 Manager는 사실 이름만 바꾸는 것으로 도메인으로 취급할 수 있다.

User user = userFinder.find(userId);
Product product = productFinder.find(productId);
List<Coupon> coupons = couponFinder.find(user);

// 2번 옵션: 새로운 도메인 클래스를 만든다. 
Cashier cashier = new Cashier();
price = cashier.calculate(user, coupons, product);

무엇이 더 자연스러운지는 케이스 별로 다를 수 있으므로 본인 판단하에 수정후 팀원들과 상의한다.

가이드 정리

  1. 기존 서비스 코드의 비즈니스 흐름을 분석한다.
  2. Finder, Writer, Validator 와 적절히 협업하여 비즈니스 흐름이 서비스 코드에 드러나도록 수정한다.
  3. 서비스는 가능한 얇게 유지하기 위해 풍부한 구현 객체와 도메인 객체를 만든다.

레퍼런스

  1. https://www.youtube.com/watch?v=D0cEayHkp2U
  2. https://www.youtube.com/watch?v=pimYIfXCUe8

질문 혹은 공유 사항 (Optional)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant