머플러 어플리케이션을 기능 중 소비 내역을 등록할 때,
반복되는 소비를 자동으로 기록하게 해주는 반복 기록 기능이 있었다.
소비 등록 시 반복 체크를 해주면 소비 등록과 함께 루틴 기능이 같이 생성되는 방식이었다.
보기에는 간단히 체크 하나 하면 생성되는 기능으로 쉬워 보였는데..!
이 안에는 많은 고려 사항들이 들어있었다.
그래서 해당 기능을 개발하는데 예상 외로 꽤나 골치 아팠다 🤪
📝 루틴 기능 고려 사항
요일 반복
날짜 반복
현재 시점에서 과거의 소비 + 루틴 등록 시 현재 시점까지의 반복 기록을 자동으로 생성한다.
소비 등록에 종속되어 등록된다.
기본적으로 위와 같은 고려 사항들이 있었다.
여기서 내가 주목한 점은
루틴이 요일 반복과 날짜 반복 두 가지로 나뉘어져 있지만,
사실은 이 둘은루틴 조건을 만족하면 반복 소비를 생성한다.라는 같은 기능을 한다고 생각했다.
그래서 루틴 객체를 요일 루틴, 날짜 루틴으로 나누지 않고,
루틴 인터페이스를 사용하여 구현하면 좋겠다고 생각했다!
@RestController
@RequiredArgsConstructor
@RequestMapping("/routine")
public class RoutineController {
private final RoutineService routineService;
@PostMapping("/weekly")
public Response<Void> addWeeklyRoutine(@RequestBody AddWeeklyRoutineRequest request) { // TODO: @Valid 추가
routineService.addWeeklyRoutine(request);
routineService.addPastExpenses(request);
return Response.success();
}
@PostMapping("/monthly")
public Response<Void> addMonthlyRoutine(@RequestBody AddMonthlyRoutineRequest request) { // TODO: @Valid 추가
routineService.addMonthlyRoutine(request);
return Response.success();
}
}
public class RoutineService {
// 반복 소비 내역(요일) 추가
@Transactional
public Routine addWeeklyRoutine(AddWeeklyRoutineRequest request) {
Long memberId = 1L;
Member member = memberRepository.findById(memberId).orElseThrow(() -> new MemberException(ErrorCode.MEMBER_NOT_FOUND));
validateWeeklyRoutine(request);
Routine newRoutine = RoutineConverter.toWeeklyRoutine(request, member);
return routineRepository.save(newRoutine);
}
// 반복 소비 내역(날짜) 추가
@Transactional
public Routine addMonthlyRoutine(AddMonthlyRoutineRequest request) {
Long memberId = 1L;
Member member = memberRepository.findById(memberId).orElseThrow(() -> new MemberException(ErrorCode.MEMBER_NOT_FOUND));
validateMonthlyRoutine(request.getStartDate(), request.getDay());
Routine newMonthlyRoutine = RoutineConverter.toMonthlyRoutine(request, member);
return routineRepository.save(newMonthlyRoutine);
}
// 지난 소비 내역 등록 (요일 반복)
@Transactional
public void addPastExpenses(AddWeeklyRoutineRequest request) {
LocalDate currentDate = LocalDate.now();
Long memberId = 1L;
Member member = memberRepository.findById(memberId).orElseThrow(() -> new MemberException(ErrorCode.MEMBER_NOT_FOUND));
Long categoryId = 1L;
Category category = categoryRepository.findById(categoryId).orElseThrow(() -> new IllegalArgumentException("category"));
for (Integer dayOfWeek : request.getDayOfWeek()) {
LocalDate nextDate = request.getStartDate().with(TemporalAdjusters.nextOrSame(DayOfWeek.of(dayOfWeek)));
LocalDate effectiveEndDate = (request.getEndDate() == null || request.getEndDate().isAfter(currentDate))
? currentDate : request.getEndDate();
while (!nextDate.isAfter(effectiveEndDate)) {
if ((nextDate.isBefore(currentDate) || nextDate.equals(currentDate)) && !nextDate.equals(request.getStartDate())) {
Expense expense = Expense.builder()
.date(nextDate)
.title(request.getTitle())
.cost(request.getCost())
.memo(request.getMemo())
.member(member)
.category(category)
.build();
expenseRepository.save(expense);
}
nextDate = nextDate.plusWeeks(1);
}
}
}
}
기존 다른 개발자 분이 만드신 코드는 위와 같았다.
내가 수정 하고 싶었던 부분은
- 요일 or 날짜 루틴이 개별적으로 수행되고 있다는 점
- 루틴 생성 → 과거 루틴 소비 등록이 컨트롤러에서 따로 실행되어, 서로 다른 트랜잭션에서 수행되고 있다는 점
- 지난 소비 내역 등록 로직이 서비스 레이어 내에서 분리되어 존재하여 응집도가 떨어지는 점
이었다.
루틴은 소비 내역 생성이 성공적으로 되었을 때만 생성 가능하고,
지난 소비 내역 등록도 루틴 등록이 성공적으로 되었을 때만 수행되는 기능이다.
그러므로 위 기능들은 한 트랜잭선에서 수행되어,
중간에 실패 시 전체 로직이 Rollback되어야 하고, 전부 성공적으로 수행되었을 때 Commit이 되어야 한다.
그리고 두 가지 타입의 루틴 기능은 확장성과 유연성을 가지도록 만들어,
한 곳에서 구현하고 관리하여 사용하고 싶었다.
해당 부분들을 개선하기 위해 다음과 같은 개선 방향을 생각했다.
- RoutineService의 루틴 생성 로직이 RoutineController에서 호출되는 것이 아닌,
소비 등록 Service에서 호출되도록 만들어서 한 트랜잭션 내에서 수행되도록 한다.- 루틴 생성과 지난 소비 내역 등록이 한 트랜잭션 내에서 이루어지게 한다.
- RoutineUtils를 만들어 루틴과 관련된 로직들을 인터페이스로 수행하게 한다.
이렇게 함으로써 루틴 서비스 내의 코드를 깔끔하게 만들고,
루틴 관련 로직들을 분리하여 관리하고, 서비스 레이어에서 유연하게 사용하도록 만들고 싶었다.
다음 포스팅에 이어서 해당 리팩토링을 수행한 과정을 보여드리겠다.