프로젝트를 진행하면서 요구사항이 복잡해지면 다음과 같은 서비스 Bean을 빈번하게 만드는 문제를 겪었습니다.
@Slf4j
@Service
@RequiredArgsConstructor
public class ExpenditureService {
private final ExpenditureRepository expenditureRepository;
private final UserRepository userRepository;
private final ExpenditureCategoryRepository expenditureCategoryRepository;
private final UserBudgetRepository userBudgetRepository;
private final BudgetConsultingService budgetConsultingService;
private final ApplicationEventPublisher applicationEventPublisher;
private final RedissonLockContext redissonLockContext;
private final TransactionService transactionService;
....
}
이러한 이유로 프로젝트에서 리팩토링을 통해 서비스 Bean들 간의 의존성을 개선시키고 싶었습니다.
평소 함수형 프로그래밍에 관심이 많던 와중에 이번 KakaoTechMeetup 2023 레퍼런스를 참고하여 고차함수와 Handler 컴포넌트를 활용하여 리팩토링한 경험을 풀어보고자 합니다.
@Service
@RequiredArgsConstructor
public class ExpenditureService {
private final ExpenditureRepository expenditureRepository;
private final UserRepository userRepository;
private final ExpenditureCategoryRepository expenditureCategoryRepository;
private final UserBudgetRepository userBudgetRepository;
private final BudgetConsultingService budgetConsultingService;
private final ApplicationEventPublisher applicationEventPublisher;
private final RedissonLockContext redissonLockContext;
private final TransactionService transactionService;
public ExpenditureByTodayRecommendationResponse getExpenditureRecommendationByToday(String username) {
..............
List<UserBudgetCategoryAndAvailableExpenditure> availableUserBudgetByCategoryByToday = userBudgetRepository.getAvailableUserBudgetByCategoryByToday(user);
Long realAvailableExpenditure = availableUserBudgetByCategoryByToday.stream()
.mapToLong(UserBudgetCategoryAndAvailableExpenditure::availableExpenditure).sum();
// 예산 컨설팅 서비스에서 실제 예산 대비 지출액으로 구체적인 분석 담당
// UserBudgetConsultingService를 의존하고 있는 포인트1!
String message = budgetConsultingService.analyzeBudgetStatus(realAvailableExpenditure);
// 지속적인 소비 습관을 생성하기 위한 서비스이므로 예산을 초과하더라도 적정한 금액을 추천
List<UserBudgetCategoryAndAvailableExpenditureRecommendation> res =
availableUserBudgetByCategoryByToday.stream()
.map(i -> {
if (i.availableExpenditure() < 0) {
// UserBudgetConsultingService를 의존하고 있는 포인트2!
Long minimumAvailableExpenditure = budgetConsultingService.getMinimumAvailableExpenditure(i);
return UserBudgetCategoryAndAvailableExpenditureRecommendation.toMinimumRecommendation(i, minimumAvailableExpenditure);
}
return UserBudgetCategoryAndAvailableExpenditureRecommendation.toRecommendation(i);
})
.toList();
...........
}
.......
@Component
public class ExpenditureServiceHandler {
private final BudgetConsultingService budgetConsultingService;
private final ExpenditureService expenditureService;
private final RedissonLockContext redissonLockContext;
public ExpenditureServiceHandler(final BudgetConsultingService budgetConsultingService,
final ExpenditureService expenditureService,
final RedissonLockContext redissonLockContext) {
this.budgetConsultingService = budgetConsultingService;
this.expenditureService = expenditureService;
this.redissonLockContext = redissonLockContext;
}
public ExpenditureByTodayRecommendationResponse getExpenditureRecommendationByToday(String username) {
return expenditureService.getExpenditureRecommendationByToday(
username,
// 함수를 파라미터로 넘긴다.
budgetConsultingService::analyzeBudgetStatus,
// 함수를 파라미터로 넘긴다.
budgetConsultingService::getMinimumAvailableExpenditure
);
}
}
public ExpenditureByTodayRecommendationResponse getExpenditureRecommendationByToday(String username,
Function<Long, String> analyzeBudgetStatus,
Function<String, Long> getMinimumAvailableExpenditure) {
.......
List<UserBudgetCategoryAndAvailableExpenditure> availableUserBudgetByCategoryByToday = userBudgetRepository.getAvailableUserBudgetByCategoryByToday(user);
Long realAvailableExpenditure = availableUserBudgetByCategoryByToday.stream()
.mapToLong(UserBudgetCategoryAndAvailableExpenditure::availableExpenditure).sum();
// 예산 컨설팅 서비스에서 실제 예산 대비 지출액으로 구체적인 분석 담당
// UserBudgetConsultingService를 의존하고 있는 포인트1! - 함수 apply
String message = analyzeBudgetStatus.apply(realAvailableExpenditure);
// 지속적인 소비 습관을 생성하기 위한 서비스이므로 예산을 초과하더라도 적정한 금액을 추천
List<UserBudgetCategoryAndAvailableExpenditureRecommendation> res =
availableUserBudgetByCategoryByToday.stream()
.map(i -> {
if (i.availableExpenditure() < 0) {
// UserBudgetConsultingService를 의존하고 있는 포인트2! - 함수 apply
Long minimumAvailableExpenditure = getMinimumAvailableExpenditure.apply(i.name());
return UserBudgetCategoryAndAvailableExpenditureRecommendation.toMinimumRecommendation(i, minimumAvailableExpenditure);
}
return UserBudgetCategoryAndAvailableExpenditureRecommendation.toRecommendation(i);
})
.toList();
...................
}
@Service
@RequiredArgsConstructor
public class ExpenditureService {
private final ExpenditureRepository expenditureRepository;
private final UserRepository userRepository;
private final ExpenditureCategoryRepository expenditureCategoryRepository;
private final UserBudgetRepository userBudgetRepository;
private final BudgetConsultingService budgetConsultingService;
private final ApplicationEventPublisher applicationEventPublisher;
private final RedissonLockContext redissonLockContext;
private final TransactionService transactionService;
public String createExpenditure(String username, Long categoryId, ExpenditureRequest request) {
redissonLockContext.executeLock(username, () ->
// 락을 점유한 스레드만 트랜잭션 적용
transactionService.executeAsTransactional(() -> {
User user = userRepository.findUserByUsername(username)
.orElseThrow(() -> new ApiException(CustomErrorCode.USER_NOT_FOUND_DB));
ExpenditureCategory category = expenditureCategoryRepository.findById(categoryId)
.orElseThrow(() -> new ApiException(CustomErrorCode.CATEGORY_NOT_FOUND_DB));
Expenditure expenditure = new Expenditure(user, category, request);
expenditureRepository.save(expenditure);
return null;
}));
return "created";
}
.........
public class ExpenditureServiceHandler {
private final BudgetConsultingService budgetConsultingService;
private final ExpenditureService expenditureService;
private final RedissonLockContext redissonLockContext;
public ExpenditureServiceHandler(final BudgetConsultingService budgetConsultingService,
final ExpenditureService expenditureService,
final RedissonLockContext redissonLockContext) {
this.budgetConsultingService = budgetConsultingService;
this.expenditureService = expenditureService;
this.redissonLockContext = redissonLockContext;
}
public String createExpenditure(String username, Long categoryId, ExpenditureRequest request) {
redissonLockContext.executeLock(username,
() -> expenditureService.createExpenditure(username, categoryId, request));
return "created";
}
...........
Handler 활용으로 락은 Handler에서 잡아주고 @Transactional을 ExpenditureService에서 적용해줌으로써 TransactionService가 필요가 없어지게 되었습니다.
@Transactional
public void createExpenditure(String username, Long categoryId, ExpenditureRequest request) {
User user = userRepository.findUserByUsername(username)
.orElseThrow(() -> new ApiException(CustomErrorCode.USER_NOT_FOUND_DB));
ExpenditureCategory category = expenditureCategoryRepository.findById(categoryId)
.orElseThrow(() -> new ApiException(CustomErrorCode.CATEGORY_NOT_FOUND_DB));
Expenditure expenditure = new Expenditure(user, category, request);
expenditureRepository.save(expenditure);
}
