이번 글에서는 InvitationService 클래스의 검증 로직을 별도의 InvitationValidator 클래스로 분리하는 과정을 살펴보겠습니다. 이를 통해 단일 책임 원칙(Single Responsibility Principle)을 준수하고, 코드의 가독성과 유지보수성을 향상시키는 방법을 알아보겠습니다.
프로젝트를 진행하면서 InvitationService 클래스의 책임이 점점 커지고 있다는 것을 발견했습니다. 특히 초대 생성, 수락, 거절 등의 비즈니스 로직과 함께 다양한 검증 로직이 한 클래스 내에 존재하고 있었습니다. 이로 인해 코드의 복잡성이 증가하고, 단일 책임 원칙을 위반하는 문제가 발생했습니다.
기존의 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) {
// 검증 로직...
}
// 기타 검증 메서드들...
}
이 코드는 비즈니스 로직과 검증 로직이 혼재되어 있어, 클래스의 책임이 불분명해지는 문제가 있었습니다.
InvitationService 클래스를 분석하면서 다음과 같은 문제점들을 발견했습니다.
이러한 문제를 해결하기 위해 검증 로직을 별도의 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("이미 대기 중인 초대가 있습니다.");
}
}
// 기타 검증 메서드들...
}
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();
}
// 다른 메서드들...
}
이러한 리팩토링을 통해 다음과 같은 개선 효과를 얻었습니다.
이번 리팩토링을 통해 단일 책임 원칙의 중요성과 적절한 책임 분리가 코드의 품질을 얼마나 향상시킬 수 있는지 다시 한 번 깨달았습니다. 또한, 이러한 작은 개선들이 모여 전체 시스템의 유지보수성과 확장성을 크게 향상시킬 수 있다는 것을 경험했습니다.
앞으로도 지속적으로 코드를 리뷰하고 개선하면서, 더 나은 설계와 구현 방식을 고민해 나가겠습니다.