파사드패턴으로 클-린한 코드 만들기

쩡log·2024년 9월 11일
0

로직이 복잡한 프로젝트인데, 서비스 레이어 간의 참조로 인해 순환 참조가 발생할 가능성이 높아졌습니다. 순환 참조는 클래스 간 의존 관계가 얽혀 복잡성을 더하고, 결과적으로 유지보수를 어렵게 만드는 문제가 있습니다. 그리고 순환참조 에러도 터지겠죠.
또한, 서비스 레이어에서 비즈니스 로직이 명확하게 드러나야하는데, 세부 구현 로직까지 함께 섞이면서 로직을 한눈에 파악하기 힘들어졌습니다.

이러다가 큰일나겠다 싶어서 팀원들과 고민을 나누던 중 제미니님의 지속 성장 가능한 소프트웨어를 만들어가는 방법 글을 추천받았습니다.
이 글을 읽고 파사드 패턴을 도입하면 제가 고민하던 부분이 해결될 것이라는 결론을 내렸습니다.


파사드패턴

파사드 패턴은 복잡한 서브시스템을 단순화하여 클라이언트가 서브시스템 내부의 복잡성을 알지 못해도 쉽게 사용할 수 있게 도와주는 패턴입니다.
이를 통해 서비스 간 참조를 줄이고, 비즈니스 로직과 구현로직을 분리할 수 있습니다.


[장점]

  • 의존성 감소
    여러 서비스 레이어에서 서로 직접적으로 참조하지 않고, 파사드를 통해 간접적으로 의존성을 관리하므로 순환 참조 문제를 피할 수 있습니다.
  • 코드 가독성 및 유지보수성 향상
    비즈니스 로직을 파사드 클래스에 모아놓고, 세부 구현 로직은 각 책임에 맞는 클래스에 위임함으로써 비즈니스 흐름이 더 명확해집니다. 이로 인해 코드의 가독성이 향상되고, 각 역할이 명확히 분리되므로 유지보수도 쉬워집니다.
  • 클라이언트 코드의 단순화
    복잡한 로직을 파사드에 감싸 처리하므로, 클라이언트 코드에서 하위 서비스들의 복잡한 호출을 신경쓰지 않아도 됩니다.

[단점]

  • 추가적인 레이어 도입
    파사드패턴을 적용하면 새로운 레이어가 생기기 때문에 코드의 전체 구조가 박잡해질 수 있습니다. 잘못 설계되면 오히려 과도한 추상화로 인해 가독성이 떨어질 수 있습니다.
  • 확장성 제한
    파사드에서 비즈니스 로직의 흐름을 단일 진입점으로 처리하다 보면, 특정 기능을 확장하거나 변경할 때 유연성이 떨어질 수 있습니다.

장점과 단점을 고민해봤지만, 저의 상황에는 장점이 더 커서 적용했습니다.

코드에 적용

우선 가장 기본적인 회원가입부분 부터 적용해보겠습니다.
코치가 회원가입하는 경우에는 코치테이블, 코치 이미지 테이블, 코치 해쉬테크 테이블, 코치 지역 테이블 총 4개의 테이블에 저장해야합니다.

아래의 repository 레이어를 사용하여 구현한 코드는 가독성이 떨어지고, 로직을 한눈에 파악하기 어렵습니다.

AS-IS

@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()));
    }
    
 }

결과

파사드 패턴으로 리팩토링하여 서비스 간 참조를 줄이고, 비즈니스 로직과 구현 로직을 분리했습니다.
이 패턴을 적용함으로써 코드의 가독성이 크게 향상된 것을 확인할 수 있습니다.

0개의 댓글