로직이 복잡한 프로젝트인데, 서비스 레이어 간의 참조로 인해 순환 참조가 발생할 가능성이 높아졌습니다. 순환 참조는 클래스 간 의존 관계가 얽혀 복잡성을 더하고, 결과적으로 유지보수를 어렵게 만드는 문제가 있습니다. 그리고 순환참조 에러도 터지겠죠.
또한, 서비스 레이어에서 비즈니스 로직이 명확하게 드러나야하는데, 세부 구현 로직까지 함께 섞이면서 로직을 한눈에 파악하기 힘들어졌습니다.
이러다가 큰일나겠다 싶어서 팀원들과 고민을 나누던 중 제미니님의 지속 성장 가능한 소프트웨어를 만들어가는 방법 글을 추천받았습니다.
이 글을 읽고 파사드 패턴을 도입하면 제가 고민하던 부분이 해결될 것이라는 결론을 내렸습니다.
파사드패턴
파사드 패턴은 복잡한 서브시스템을 단순화하여 클라이언트가 서브시스템 내부의 복잡성을 알지 못해도 쉽게 사용할 수 있게 도와주는 패턴입니다.
이를 통해 서비스 간 참조를 줄이고, 비즈니스 로직과 구현로직을 분리할 수 있습니다.
[장점]
[단점]
코드에 적용
우선 가장 기본적인 회원가입부분 부터 적용해보겠습니다.
코치가 회원가입하는 경우에는 코치테이블, 코치 이미지 테이블, 코치 해쉬테크 테이블, 코치 지역 테이블 총 4개의 테이블에 저장해야합니다.
아래의 repository 레이어를 사용하여 구현한 코드는 가독성이 떨어지고, 로직을 한눈에 파악하기 어렵습니다.
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
private final CoachRepository coachRepository;
private final CoachImageRepository coachImageRepository;
private final CoachTagRepository coachTagRepository;
private final CoachRegionRepository coachRegionRepository;
@Transactional
public void coachSignup(CoachSignupRequest request) throws ServiceException {
// 유저 정보를 조회 후 코치로 롤 업데이트 한다. 그리고 코치 정보를 저장한다.
User user = userRepository.findByUid(request.getUid()).orElseThrow(() -> new ServiceException(ErrorCode.USER_NOT_FOUND));
user.updateRole(Role.COACH);
Coach coach = coachRepository.save(request.toEntity(user.getId()));
// 코치 이미지 정보를 저장한다.
coachImageRepository.save(CoachImage.create(coach.getId(), request.getProfileImgUrl(), CoachImageType.COACH_PROFILE));
request.getCertificateImgUrlList()
.forEach(url -> coachImageRepository.save(CoachImage.create(coach.getId(), url, CoachImageType.CERTIFICATE)));
request.getAwardsImgUrlList().forEach(url -> coachImageRepository.save(CoachImage.create(coach.getId(), url, CoachImageType.AWARDS)));
request.getLicenseImgUrlList().forEach(url -> coachImageRepository.save(CoachImage.create(coach.getId(), url, CoachImageType.LICENSE)));
// 코치 태그 정보를 저장한다.
request.getTagIdList().forEach(tagId -> {
coachTagRepository.save(
CoachTagCreateRequest.builder()
.tagId(tagId)
.coachId(coach.getId())
.build()
.toEntity());
});
// 코치 지역 정보를 저장한다.
String region1 = (request.getRegionList().size() > 0) ? request.getRegionList().get(0) : null;
String region2 = (request.getRegionList().size() > 1) ? request.getRegionList().get(1) : null;
coachRegionRepository.save(
CoachRegion.builder()
.coachId(coach.getId())
.region1(region1)
.region2(region2)
.build()
);
}
}
이 코드는 너무 복잡하므로, 레포지토리를 서비스 레이어로 감싸서 리팩토링해보겠습니다.
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
private final CoachRepository coachRepository;
private final CoachImageService coachImageService;
private final TagService tagService;
private final CoachRegionService coachRegionService;
@Transactional
public void coachSignup(CoachSignupRequest request) throws ServiceException {
User user = userRepository.findByUid(request.getUid()).orElseThrow(() -> new ServiceException(ErrorCode.USER_NOT_FOUND));
user.updateRole(Role.COACH);
Coach coach = coachRepository.save(request.toEntity(user.getId()));
coachImageService.save(request, coach.getId());
tagService.save(request.getTagIdList(), coach.getId());
coachRegionService.save(coach.getId(), request.getRegionList());
}
}
코드가 훨씬 깔끔해졌습니다. 하지만, 순환 참조 문제가 발생합니다.
파사드 패턴을 도입하여 리팩토링해보겠습니다. insert, update가 발생하는 서비스는 Appender 클래스로, read가 발생하는 서비스는 Finder 클래스로 구분했습니다. 이 로직에서는 읽기가 없으므로 Appender만 사용했습니다.
@Service
@RequiredArgsConstructor
public class UserService {
private final CoachAppender coachAppender;
private final TagAppender tagAppender;
private final CoachImageAppender coachImageAppender;
private final CoachRegionAppender coachRegionAppender;
@Transactional
public void coachSignup(CoachSignupRequest request) throws ServiceException {
Coach coach = coachAppender.signup(request);
coachImageAppender.save(request, coach.getId());
tagAppender.save(request.getTagIdList(), coach.getId());
coachRegionAppender.save(coach.getId(), request.getRegionList());
}
}
Appender Class는 @Component로 등록해서 사용합니다.
@Component
@RequiredArgsConstructor
public class CoachAppender {
private final UserRepository userRepository;
private final CoachRepository coachRepository;
public Coach signup(CoachSignupRequest request) throws ServiceException {
User user = userRepository.findByUid(request.getUid()).orElseThrow(() -> new ServiceException(ErrorCode.USER_NOT_FOUND));
user.updateRole(Role.COACH);
return coachRepository.save(request.toEntity(user.getId()));
}
}
결과
파사드 패턴으로 리팩토링하여 서비스 간 참조를 줄이고, 비즈니스 로직과 구현 로직을 분리했습니다.
이 패턴을 적용함으로써 코드의 가독성이 크게 향상된 것을 확인할 수 있습니다.