서비스 클래스에서 검증 로직 분리해보기

찬근·2024년 9월 20일
0

이번 글에서는 InvitationService 클래스의 검증 로직을 별도의 InvitationValidator 클래스로 분리하는 과정을 살펴보겠습니다. 이를 통해 단일 책임 원칙(Single Responsibility Principle)을 준수하고, 코드의 가독성과 유지보수성을 향상시키는 방법을 알아보겠습니다.

배경

프로젝트를 진행하면서 InvitationService 클래스의 책임이 점점 커지고 있다는 것을 발견했습니다. 특히 초대 생성, 수락, 거절 등의 비즈니스 로직과 함께 다양한 검증 로직이 한 클래스 내에 존재하고 있었습니다. 이로 인해 코드의 복잡성이 증가하고, 단일 책임 원칙을 위반하는 문제가 발생했습니다.

1. 기존 코드

기존의 InvitationService 클래스는 다음과 같았습니다.

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class InvitationService {
    private final InvitationRepository invitationRepository;
    private final TeamMemberRepository teamMemberRepository;
    private final MemberRepository memberRepository;
    private final TeamRepository teamRepository;

    @Transactional
    public Long createInvitation(InvitationRequestDto dto, Long inviterId) {
        validateInviter(dto.getTeamId(), inviterId);
        Member invitee = findInvitee(dto.getInviteeLoginId());
        Member inviter = memberRepository.getReferenceById(inviterId);
        Team team = findTeam(dto.getTeamId());
        validateInviteeNotInTeam(dto.getTeamId(), invitee.getId());
        validateNoPendingInvitation(dto.getTeamId(), invitee.getId());
        Invitation invitation = Invitation.of(team, inviter, invitee);
        Invitation savedInvitation = invitationRepository.save(invitation);
        return savedInvitation.getId();
    }

    // 다른 메서드들...

    private void validateInviter(Long teamId, Long inviterId) {
        // 검증 로직...
    }

    private void validateInviteeNotInTeam(Long teamId, Long inviteeId) {
        // 검증 로직...
    }

    private void validateNoPendingInvitation(Long teamId, Long inviteeId) {
        // 검증 로직...
    }

    // 기타 검증 메서드들...
}

이 코드는 비즈니스 로직과 검증 로직이 혼재되어 있어, 클래스의 책임이 불분명해지는 문제가 있었습니다.

2. 문제 인식

InvitationService 클래스를 분석하면서 다음과 같은 문제점들을 발견했습니다.

  1. 단일 책임 원칙 위반: 서비스 클래스가 비즈니스 로직 처리와 검증을 동시에 담당하고 있었습니다.
  2. 코드 복잡성 증가: 검증 로직이 늘어날수록 서비스 클래스의 크기와 복잡도가 증가했습니다.

3. 해결 방안: InvitationValidator 도입

이러한 문제를 해결하기 위해 검증 로직을 별도의 InvitationValidator 클래스로 분리하기로 결정했습니다.

@Component
@RequiredArgsConstructor
public class InvitationValidator {
    private final TeamMemberRepository teamMemberRepository;
    private final InvitationRepository invitationRepository;

    public void validateInviter(Long teamId, Long inviterId) {
        boolean isValidMember = teamMemberRepository.existsByTeamIdAndMemberId(teamId, inviterId);
        if (!isValidMember) {
            throw new UnauthorizedException("초대 권한이 없습니다.");
        }
    }

    public void validateInviteeNotInTeam(Long teamId, Long inviteeId) {
        boolean isAlreadyMember = teamMemberRepository.existsByTeamIdAndMemberId(teamId, inviteeId);
        if (isAlreadyMember) {
            throw new IllegalStateException("이미 팀에 소속된 회원입니다.");
        }
    }

    public void validateNoPendingInvitation(Long teamId, Long inviteeId) {
        boolean hasPendingInvitation = invitationRepository.existsByTeamIdAndInviteeIdAndStatus(
                teamId, inviteeId, InvitationStatus.PENDING);
        if (hasPendingInvitation) {
            throw new IllegalStateException("이미 대기 중인 초대가 있습니다.");
        }
    }

    // 기타 검증 메서드들...
}

4. InvitationService 리팩토링

InvitationValidator를 도입한 후, InvitationService를 다음과 같이 리팩토링했습니다.

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class InvitationService {
    private final InvitationRepository invitationRepository;
    private final TeamMemberRepository teamMemberRepository;
    private final MemberRepository memberRepository;
    private final TeamRepository teamRepository;
    private final InvitationValidator validator;

    @Transactional
    public Long createInvitation(InvitationRequestDto dto, Long inviterId) {
        validator.validateInviter(dto.getTeamId(), inviterId);
        Member invitee = findInvitee(dto.getInviteeLoginId());
        Member inviter = memberRepository.getReferenceById(inviterId);
        Team team = findTeam(dto.getTeamId());
        validator.validateInviteeNotInTeam(dto.getTeamId(), invitee.getId());
        validator.validateNoPendingInvitation(dto.getTeamId(), invitee.getId());
        Invitation invitation = Invitation.of(team, inviter, invitee);
        Invitation savedInvitation = invitationRepository.save(invitation);
        return savedInvitation.getId();
    }

    // 다른 메서드들...
}

5. 개선 결과

이러한 리팩토링을 통해 다음과 같은 개선 효과를 얻었습니다.

  1. 단일 책임 원칙 준수: InvitationService는 비즈니스 로직에, InvitationValidator는 검증 로직에 집중할 수 있게 되었습니다.
  2. 코드 가독성 향상: 각 클래스의 역할이 명확해져 코드를 이해하기 쉬워졌습니다.
  3. 재사용성 증가: InvitationValidator의 검증 메서드들을 다른 서비스나 컴포넌트에서도 쉽게 사용할 수 있게 되었습니다.
  4. 유지보수성 개선: 검증 로직의 변경이 필요한 경우 InvitationValidator만 수정하면 되므로 유지보수가 용이해졌습니다.

결론

이번 리팩토링을 통해 단일 책임 원칙의 중요성과 적절한 책임 분리가 코드의 품질을 얼마나 향상시킬 수 있는지 다시 한 번 깨달았습니다. 또한, 이러한 작은 개선들이 모여 전체 시스템의 유지보수성과 확장성을 크게 향상시킬 수 있다는 것을 경험했습니다.

앞으로도 지속적으로 코드를 리뷰하고 개선하면서, 더 나은 설계와 구현 방식을 고민해 나가겠습니다.

profile
일관성 있는 개발자

0개의 댓글