특정 이벤트는 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)을 지킬 수 있으며 유연성이 높아진다.
이런 장점들로 탬플릿 메소드 패턴과 전략패턴을 활용해서 아래의 순서대로 리팩토링을 해봤다.
출석처리 할때 필요한 과제를 이넘으로 관리하게끔 리팩토링했다.
@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);
}
}
결론
이렇게 리팩토링을 통해서 코드의 유지보수성과 확장성을 높여봤다. 하지만 클래스가 많아질 때 복잡해질 수 있다는 단점을 고려해서, 적절한 수준의 추상화와 분리를 통해 복잡도를 관리하는 것도 중요할것 같다는 생각이 들었다.