IF문 리팩토링(전략패턴과 템플릿 메서드)

쩡log·2024년 9월 19일
0

특정 이벤트는 QR코드로 출석하는 시스템을 개발하면서, 타입을 구분하는 switch문을 사용했다. 처음에는 간단하게 사용했지만 타입이 늘어날수록 코드는 유지보수와 멀어지고 점점 길어져만 갔다.

리팩토링한 과정을 기록해보려고한다.

AS-IS

@Service
@RequiredArgsConstructor
@Slf4j
public class QrAttendanceServiceImpl implements QrAttendanceService {

    private final SubmissionsClient submissionsClient;
    private final CourseClient courseClient;
    private final AssignmentGroupClient assignmentGroupClient;
    private final QrAttendanceRepository qrAttendanceRepository;

    @Transactional
    @Override
     switch (qrAttendanceCreateDto.getQrType()) {
            case FESTIVAL:
                // 1-1. DB에 있는지 조회
                Optional<QrAttendance> qrAttendance = qrAttendanceRepository.findAllByLoginIdAndQrTypeAndBoothNumber(
                    qrAttendanceCreateDto.getLoginId(),
                    qrAttendanceCreateDto.getQrType().getNumber(), 
                    qrAttendanceCreateDto.getBoothNumber()
                );

                // 1-2. 있다면 에러 발생, 없으면 저장
                checkIsPresent(qrAttendance, qrAttendanceCreateDto);

                // 1-3. 부스 번호가 3개 이상이면 과제 제출
                List<QrAttendance> qrAttendanceList = qrAttendanceRepository.findAllByLoginIdAndQrType(
                    qrAttendanceCreateDto.getLoginId(), 
                    qrAttendanceCreateDto.getQrType().getNumber()
                );
                if (qrAttendanceList.size() >= 3) {
                    return submitQrAttendance(lmsUserId, "[1주차] 축제 부스 방문");
                }
                break;

            case MEETING:
                // 2-1. DB에 있는지 조회
                qrAttendance = qrAttendanceRepository.findByLoginIdAndQrType(
                    qrAttendanceCreateDto.getLoginId(),
                    qrAttendanceCreateDto.getQrType().getNumber()
                );

                // 2-2. 있다면 에러 발생, 없으면 저장
                checkIsPresent(qrAttendance, qrAttendanceCreateDto);

                // 2-3. 과제 제출
                return submitQrAttendance(lmsUserId, "[2주차] 간담회 참석");

            case CAREER_1:
                return saveCareerQrAttendance(qrAttendanceCreateDto, lmsUserId, "[4주차 1차시EXPO 부스 방문(야외)");

            case CAREER_2:
                return saveCareerQrAttendance(qrAttendanceCreateDto, lmsUserId, "[4주차 2차시] 커리어 페스티발 행정부스 방문하기(체육관)");

            case CAREER_3:
                return saveCareerQrAttendance(qrAttendanceCreateDto, lmsUserId, "[4주차 3차시] 커리어 페스티발 이벤트, 학과부스 방문하기(체육관)");

            default:
                throw new BusinessException(ErrorCode.INVALID_QR_TYPE);
        }
        
 	private QrSuccessResponse saveCareerQrAttendance(QrAttendanceCreateDto qrAttendanceCreateDto, Long lmsUserId, String eventName) {
        Optional<QrAttendance> qrAttendance = qrAttendanceRepository.findByLoginIdAndQrType(qrAttendanceCreateDto.getLoginId(),
            qrAttendanceCreateDto.getQrType().getNumber());
        checkIsPresent(qrAttendance, qrAttendanceCreateDto);
        return submitQrAttendance(lmsUserId, eventName);
    }

    private void checkIsPresent(Optional<QrAttendance> qrAttendance, QrAttendanceCreateDto qrAttendanceCreateDto) {
        if (qrAttendance.isPresent()) {
            throw new BusinessException(ErrorCode.ALREADY_EXIST_QR_ATTENDANCE);
        } else {
            qrAttendanceRepository.save(QrAttendance.of(qrAttendanceCreateDto));
        }
    }

	// 외부 API 호출
    private QrSuccessResponse submitQrAttendance(Long lmsUserId, String eventName) {
        List<QrCourseFeignDto> courseList = courseClient.getQrCourseList(lmsUserId).stream()
            .filter(course -> course.getWorkflowState().equals("available") || course.getWorkflowState().equals("published"))
            .toList();

        Optional<QrAssignmentFeignResponse> qrAssignmentOptional = courseList.stream()
            .map(course -> assignmentGroupClient.findQrAssignment(lmsUserId, course.getId()))
            .flatMap(List::stream)
            .filter(assignment -> assignment.getName().trim().equals(eventName.trim()))
            .filter(assignment -> assignment.getSubmissionTypes().contains("on_paper"))
            .filter(assignment -> DateTimeUtil.addHoursToLocalDateTime(assignment.getDueAt()).isAfter(LocalDateTime.now()))
            .findFirst();

        QrAssignmentFeignResponse qrAssignment = qrAssignmentOptional.orElseThrow(
            () -> new BusinessException(ErrorCode.NOT_FOUND_QR_ASSIGNMENT));

        submissionsClient.updateAssignmentComplete(
            qrAssignment.getCourseId(),
            qrAssignment.getId(),
            lmsUserId,
            "complete"
        );

        return new QrSuccessResponse("success", "출석이 완료되었습니다.");
    }

}

QrType 이넘클래스

@AllArgsConstructor
@NoArgsConstructor
@Getter
public enum QrType {

    FESTIVAL,
    MEETING("간담회", 1),
    CAREER_1("커리어 페스티발 1차시", 2),
    CAREER_2("커리어 페스티발 2차시", 3),
    CAREER_3("커리어 페스티발 3차시", 4);


    private String name;
    private Integer number;
    
  }
    

기존 코드의 문제점은 새로운 요구사항이 추가되거나, 새로운 타입이 생길때 마다 계속해서 수정해야 한다는 점이다. 확장성이 유연하지 않으며 코드가 중복되고 가독성도 떨어진다. 이 레거시코드를 전략패턴과 탬플릿 메소드 패턴으로 리팩토링 해보려고 한다.


리팩토링

  • 탬플릿 메소드 패턴
    • 로직의 기본적인 처리 흐름이 동일하지만, 세부적인 로직이 타입에 따라 달라지는 경우가 있다.
    • 이때, 패턴을 통해 공통적인 처리 흐름은 부모 클래스에서 관리하고, 타입에 따라 다른 동작은 자식 클래스에서 오버라이딩 할 수 있도록 해준다.
    • 이를 통해 중복코드가 줄어들고, 변경이 필요한 부분만 자식 클래스에서 수정하면 되므로 확장성이 높아진다.
  • 전략패턴
    • 타입에 따라 다른 로직이 필요할때, ApplicationContext를 통해 적절한 구현체를 동적으로 주입받아 처리할 수 있다.

    • 이때 조건문(switch, if문 등)의 남용을 피할 수 있다.

    • 타입이 추가되더라도 코드의 변경없이 새로운 전략을 추가해 확장할 수 있다.

    • 이는 개방-폐쇄원칙(OCP)을 지킬 수 있으며 유연성이 높아진다.


이런 장점들로 탬플릿 메소드 패턴과 전략패턴을 활용해서 아래의 순서대로 리팩토링을 해봤다.

  1. 출석 과제명을 이넘으로 관리
  2. 템플릿 메소드 패턴으로 부모 클래스에서 호출되는 단계 메소드를 정의하고, 일부 메소드는 자식 클래스에서 구현
  3. 전략패턴으로 qrType에 따른 로직을 ApplicationContext를 통해서 동적으로 빈을 주입

출석 과제명을 이넘으로 관리

출석처리 할때 필요한 과제를 이넘으로 관리하게끔 리팩토링했다.

@AllArgsConstructor
@NoArgsConstructor
@Getter
public enum QrType {

    FESTIVAL(QrAttendanceFestival.class, "[10주차 1,2,3차시] 대학 축제 부스 방문", 0),
    MEETING(QrAttendanceMeeting.class, "[15주차 1차시] 간담회 참석", 1),
    CAREER_1(QrAttendanceCareer.class, "[4주차 1차시] KBU 건강복지 EXPO 부스 방문(야외)", 2),
    CAREER_2(QrAttendanceCareer.class,"[4주차 2차시] 커리어 페스티발 행정부스 방문하기(체육관)", 3),
    CAREER_3(QrAttendanceCareer.class,"[4주차 3차시] 커리어 페스티발 이벤트, 학과부스 방문하기(체육관)", 4);

    private Class<? extends QrAttendanceSaver> bean;
    private String name;
    private Integer number;

    
 }

템플릿 메소드 패턴 적용

QrAttendanceSaver는 부모 클래스로, 전체 로직의 탬플릿을 만들었다.
getQrAttendance 메소드를 호출하여 QR 출석 정보를 가져온다. 이 메소드는 추상 메소드로, 서브 클래스에서 구현해야 한다.
출석 정보가 이미 존재하는지 확인하고(checkIsPresent), 이미 존재하면 예외를 던진다.
추가적인 검증(checkOverItems)을 수행합니다.

@Service
@RequiredArgsConstructor
public abstract class QrAttendanceSaver {

    private final SubmissionsClient submissionsClient;
    private final CourseClient courseClient;
    private final AssignmentGroupClient assignmentGroupClient;
    private final QrAttendanceRepository qrAttendanceRepository;

    @Transactional
    public QrSuccessResponse saveStudentQrAttendance(QrAttendanceCreateDto qrAttendanceCreateDto, Long lmsUserId) {
        Optional<QrAttendance> qrAttendance = getQrAttendance(qrAttendanceCreateDto);
        checkIsPresent(qrAttendance, qrAttendanceCreateDto);
        checkOverItems(qrAttendanceCreateDto);
        return submitQrAttendance(lmsUserId, qrAttendanceCreateDto.getQrType().getName());
    }

    public abstract Optional<QrAttendance> getQrAttendance(QrAttendanceCreateDto qrAttendanceCreateDto);

    public void checkOverItems(QrAttendanceCreateDto qrAttendanceCreateDto) throws BusinessException {

    }

    private void checkIsPresent(Optional<QrAttendance> qrAttendance, QrAttendanceCreateDto qrAttendanceCreateDto) {
        if (qrAttendance.isPresent()) {
            throw new BusinessException(ErrorCode.ALREADY_EXIST_QR_ATTENDANCE);
        } else {
            qrAttendanceRepository.save(QrAttendance.of(qrAttendanceCreateDto));
        }
    }

    private QrSuccessResponse submitQrAttendance(Long lmsUserId, String eventName) {
        List<QrCourseFeignDto> courseList = courseClient.getQrCourseList(lmsUserId).stream()
            .filter(course -> course.getWorkflowState().equals("available") || course.getWorkflowState().equals("published"))
            .toList();

        Optional<QrAssignmentFeignResponse> qrAssignmentOptional = courseList.stream()
            .map(course -> assignmentGroupClient.findQrAssignment(lmsUserId, course.getId()))
            .flatMap(List::stream)
            .filter(assignment -> assignment.getName().trim().equals(eventName.trim()))
            .filter(assignment -> assignment.getSubmissionTypes().contains("on_paper"))
            .filter(assignment -> DateTimeUtil.addHoursToLocalDateTime(assignment.getDueAt()).isAfter(LocalDateTime.now()))
            .findFirst();

        QrAssignmentFeignResponse qrAssignment = qrAssignmentOptional.orElseThrow(
            () -> new BusinessException(ErrorCode.NOT_FOUND_QR_ASSIGNMENT));

        submissionsClient.updateAssignmentComplete(
            qrAssignment.getCourseId(),
            qrAssignment.getId(),
            lmsUserId,
            "complete"
        );

        return new QrSuccessResponse("success", "출석이 모두 완료되었습니다.");
    }

}

전략 패턴 적용

탬플릿 메소드 자식 클래스로 전략 패턴을 구현했다. QrAttendanceSaver를 상속받는다.
총 career, meeting, festival 세가지의 전략인데, career경우 과제명은 다르지만 로직은 같기 때문에 하나의 QrAttendanceCareer 클래스로 구현했다.

@Service
public class QrAttendanceCareer extends QrAttendanceSaver {

    private final QrAttendanceRepository qrAttendanceRepository;

    public QrAttendanceCareer(SubmissionsClient submissionsClient, CourseClient courseClient,
        AssignmentGroupClient assignmentGroupClient,
        QrAttendanceRepository qrAttendanceRepository) {
        super(submissionsClient, courseClient, assignmentGroupClient, qrAttendanceRepository);
        this.qrAttendanceRepository = qrAttendanceRepository;
    }

    @Override
    public Optional<QrAttendance> getQrAttendance(QrAttendanceCreateDto qrAttendanceCreateDto) {
        return qrAttendanceRepository.findByLoginIdAndQrType(qrAttendanceCreateDto.getLoginId(),
            qrAttendanceCreateDto.getQrType().getNumber());
    }

}
@Service
public class QrAttendanceFestival extends QrAttendanceSaver {
	
    private final QrAttendanceRepository qrAttendanceRepository;

    public QrAttendanceFestival(SubmissionsClient submissionsClient, CourseClient courseClient,
        AssignmentGroupClient assignmentGroupClient,
        QrAttendanceRepository qrAttendanceRepository) {
        super(submissionsClient, courseClient, assignmentGroupClient, qrAttendanceRepository);
        this.qrAttendanceRepository = qrAttendanceRepository;
    }

    @Override
    public Optional<QrAttendance> getQrAttendance(QrAttendanceCreateDto qrAttendanceCreateDto) {
        return qrAttendanceRepository.findAllByLoginIdAndQrTypeAndBoothNumber(
            qrAttendanceCreateDto.getLoginId(),
            qrAttendanceCreateDto.getQrType().getNumber(), qrAttendanceCreateDto.getBoothNumber());

    }

    public void checkOverItems(QrAttendanceCreateDto qrAttendanceCreateDto) throws BusinessException {
        List<QrAttendance> qrAttendanceList = qrAttendanceRepository.findAllByLoginIdAndQrType(qrAttendanceCreateDto.getLoginId(),
            qrAttendanceCreateDto.getQrType().getNumber());
        if (qrAttendanceList.size() > 3) {
            throw new BusinessException(ErrorCode.ALREADY_EXIST_QR_ATTENDANCE);
        }
    }

}
@Service
public class QrAttendanceMeeting extends QrAttendanceSaver {

    private final QrAttendanceRepository qrAttendanceRepository;

    public QrAttendanceMeeting(SubmissionsClient submissionsClient, CourseClient courseClient,
        AssignmentGroupClient assignmentGroupClient,
        QrAttendanceRepository qrAttendanceRepository) {
        super(submissionsClient, courseClient, assignmentGroupClient, qrAttendanceRepository);
        this.qrAttendanceRepository = qrAttendanceRepository;
    }

    @Override
    public Optional<QrAttendance> getQrAttendance(QrAttendanceCreateDto qrAttendanceCreateDto) {
        return qrAttendanceRepository.findByLoginIdAndQrType(qrAttendanceCreateDto.getLoginId(),
            qrAttendanceCreateDto.getQrType().getNumber());
    }

}

서비스레이어 코드

전략 패턴을 활용하여 qrType에 따라 다른 QrAttendanceSaver 빈을 사용한다. 즉, 여러 QR 처리 전략 중 하나를 동적으로 선택해 실행한다.

@Service
@RequiredArgsConstructor
@Slf4j
public class QrAttendanceService {

    private final ApplicationContext applicationContext;

    @Override
    public QrSuccessResponse saveStudentQrAttendance(QrAttendanceCreateDto qrAttendanceCreateDto, Long lmsUserId) {
        QrAttendanceSaver saver = applicationContext.getBean(qrAttendanceCreateDto.getQrType().getBean());
        return saver.saveStudentQrAttendance(qrAttendanceCreateDto, lmsUserId);
    }

}

결론

이렇게 리팩토링을 통해서 코드의 유지보수성과 확장성을 높여봤다. 하지만 클래스가 많아질 때 복잡해질 수 있다는 단점을 고려해서, 적절한 수준의 추상화와 분리를 통해 복잡도를 관리하는 것도 중요할것 같다는 생각이 들었다.

0개의 댓글