Facade로 답변 생성 로직의 의존성 분리하기
이전 글에서는 기존 코드를 리팩터링했습니다. 회원 조회, 질문 조회, 답변 저장은 메서드 하나가 모두 맡고 있었습니다. 이번에는 그 메서드를 나누고 호출하는 쪽에서는 과정을 간단하게 볼 수 있도록 정리했습니다.
Facade가 감추는 것
Facade는 여러 작업을 하나의 진입점으로 묶어 내부의 복잡한 과정을 감춥니다. 덕분에 호출자는 각 서비스를 어떻게 조합하는지 몰라도 필요한 작업을 요청할 수 있습니다.
서비스 조합을 별도로 두면 각 도메인의 로직과 조합하는 로직이 나뉘어서 따로 살펴보기 쉽습니다. 외부 변경이 호출자에게 직접 전해지는 범위도 줄일 수 있습니다. 그렇다고 Facade에 일을 계속 모으면 그 클래스가 다시 복잡해집니다. 클래스를 하나 더 만든다고 해서 독립성이나 테스트 용이성까지 보장되지는 않습니다.
답변 서비스에 모여 있던 작업
@Service
public class AnswerService {
private final AnswerRepository answerRepository;
private final MemberService memberService;
private final QuestionService questionService;
private final AnswerMapper answerMapper;
public AnswerService(AnswerRepository answerRepository, MemberService memberService,
QuestionService questionService, AnswerMapper answerMapper) {
this.answerRepository = answerRepository;
this.memberService = memberService;
this.questionService = questionService;
this.answerMapper = answerMapper;
}
@Transactional
public void create(AnswerRequest request, Long memberId) {
Member findedMember = memberService.findById(memberId);
Question findedQuestion = questionService.findById(request.questionId());
answerRepository.save(answerMapper.answerRequestToEntity(request, findedQuestion, findedMember));
}
}create() 하나 때문에 AnswerService가 MemberService와 QuestionService를 함께 참조하고 있었습니다. 그래서 두 조회와 엔티티 변환을 Facade로 옮겼습니다.
조회와 조합을 Facade로 옮깁니다
@Service
public class AnswerService {
private final AnswerRepository answerRepository;
private final AnswerFacade answerFacade;
public AnswerService(AnswerRepository answerRepository, AnswerFacade answerFacade) {
this.answerRepository = answerRepository;
this.answerFacade = answerFacade;
}
@Transactional
public void create(AnswerRequest request, Long memberId) {
Answer answer = answerFacade.create(request, memberId);
answerRepository.save(answer);
}
}public interface AnswerFacade {
Answer create(AnswerRequest request, Long memberId);
}@Component
public class AnswerFacadeImpl implements AnswerFacade {
private final MemberService memberService;
private final QuestionService questionService;
private final AnswerMapper answerMapper;
public AnswerFacadeImpl(MemberService memberService, QuestionService questionService,
AnswerMapper answerMapper) {
this.memberService = memberService;
this.questionService = questionService;
this.answerMapper = answerMapper;
}
@Override
@Transactional
public Answer create(AnswerRequest request, Long memberId) {
Member findedMember = memberService.findById(memberId);
Question findedQuestion = questionService.findById(request.questionId());
return answerMapper.answerRequestToEntity(request, findedQuestion, findedMember);
}
}이제 AnswerService는 AnswerRepository와 AnswerFacade에만 의존합니다. 다른 도메인의 조회와 AnswerMapper 호출은 AnswerFacadeImpl이 맡습니다. 실제 저장은 여전히 서비스에 남겨 두었습니다.
Facade의 create()에도 @Transactional을 붙였습니다. 기본 전파 방식인 REQUIRED는 기존 트랜잭션이 있으면 거기에 참여합니다. 당시에는 Facade를 다른 곳에서도 호출할 가능성을 고려해 붙였습니다.
남겨 둔 고민
외부 도메인을 조합하는 부분은 분리했습니다. 그런데 AnswerMapper의 위치는 아직 고민입니다. 서비스와 Facade 양쪽에서 사용해도 괜찮을까요?