문제 상황

  • 내가 개발을 맡았던 좋아요 기능에서 내가 봐도 문제가 많은 코드가 있었다.
  • 부끄럽지만 이를 공개하자면..
/*================== User가 좋아요 한 Entity 리스트 반환 ==================*/

    public List<PortfolioLikeResDto> getUserLikedPortfolio(Users users) {
        List<UserLikes> likeList = likesRepositoryService.getLikesByUsersAndType(users, LikeEnum.PORTFOLIO);

        return likeList.stream()
            .map(userLikes -> portfolioRepositoryService.findPortfolioById(userLikes.getLikedId()))
            .filter(portfolio -> (Boolean.FALSE.equals(portfolio.getIsDeleted())))
            .map(likesMapper::entityToDto)
            .toList();
    }

    public List<PortfolioLikeResDto> getUserLikedItem(Users users) {
        List<UserLikes> likeList = likesRepositoryService.getLikesByUsersAndType(users, LikeEnum.ITEM);

        return likeList.stream()
            .map(userLikes -> itemsRepositoryService.findItemById(userLikes.getLikedId()))
            .filter(item -> (Boolean.FALSE.equals(item.getIsDeleted())))
            .map(likesMapper::entityToDto)
            .toList();
    }

    public List<PlannerLikeResDto> getUserLikedPlanner(Users users) {

        List<UserLikes> likeList = likesRepositoryService.getLikesByUsersAndType(users, LikeEnum.PLANNER);

        return likeList.stream()
            .map(userLikes -> plannersRepositoryService.findPlannerById(userLikes.getLikedId()))
            .map(likesMapper::entityToDto)
            .toList();
    }

    public List<CompanyLikeResDto> getUserLikedCompany(Users users) {

        List<UserLikes> likeList = likesRepositoryService.getLikesByUsersAndType(users, LikeEnum.COMPANY);

        return likeList.stream()
            .map(userLikes -> {
                Long companyId = userLikes.getLikedId();
                Companies company = companiesRepositoryService.findCompanyById(companyId);
                return likesMapper.entityToDto(company);
            })
            .toList();
    }
  • user가 좋아요한 리스트를 반환하는 함수들이다. 사실 각 함수들은 거의 동일한 내용임에도 불구하고 모든 함수가 각각 따로 작성되어 있었다.
  • 하지만 더 심각한 코드는 따로 있는데..
private void updateLikeCount(Long id, LikeEnum likeEnum, boolean isIncrement) {

        switch (likeEnum) {
            case PORTFOLIO -> updatePortfolioLikeCount(id, isIncrement);
            case ITEM -> updateItemLikeCount(id, isIncrement);
            case PLANNER -> updatePlannerLikeCount(id, isIncrement);
            case COMPANY -> updateCompanyLikeCount(id, isIncrement);
        }
    }
  • likecount를 업데이트 하는 함수를 무려 switch문을 사용해서 분기중이었다..

문제점 정리

  1. 코드 중복과 SRP 위배 (Single Responsibility Principle):
    • updateLikeCount 메소드에서는 LikeEnum에 따라 다양한 엔터티를 업데이트하는 역할을 하고 있다. 이는 메소드가 여러 엔터티의 로직을 갖고 있다는 의미이며, 객체지향 원칙 중 단일책임원칙에 위배된다.
  2. 유연성 부족과 확장 어려움:
    • 새로운 LikeEnum이 추가되거나, 기존의 엔터티 업데이트 로직이 변경되면 updateLikeCount 메소드의 수정를 수정하고.. switch문을 수정하고.. 유지보수에 정말 최악의 코드다.
    • 이것 역시 객체지향 원칙 중 개방폐쇄의 원칙에 위배된다.

이를 해결하는 것이 시급한 상황..

해결방법

  • 이를 해결하기 위해 떠올린 것은 인터페이스이다.인터페이스를 통해 해당 행위를 주입하는 방식
  • 우리 프로젝트에서는 각 엔티티에 맞게 동적으로 코드가 변경되어야 하기 때문에 인터페이스와 그에 맞는 구현체들을 만들어 동적으로 코드를 수정할 수 있도록 하였다.

1. 구현체 만들기

public interface LikesService {  
void increaseLikeCount(String lockName, Long id);  

void decreaseLikeCount(String lockName, Long id);  

<T> List<T> getUserLiked(Users users);  
}

2. Spring DI를 이용한 동적 구현체 선택

private final Map<String, LikesService> likeServiceMap;
  • spring DI에서 제공하는 기능 중 문자열 키를 통해 여러 LikesService 구현체를 관리하고, 이러한 방식으로 런타임 시에 동적으로 구현체를 선택하도록 하는 방법이 있다.
  • 이를 활용하여 동적으로 각 엔티티에 해당하는 구현체를 선택하여 실행할 수 있다.
  • 매우매우 편리한 방법!

이에 활용한 디자인패턴 : 전략패턴

  • 실행(런타임) 중에 알고리즘 전략을 선택하여 객체 동작을 실시간으로 바뀌도록 할 수 있게 하는 행위 디자인 패턴
  • 상태패턴과 유사하기 때문에 주의!
  • 추후에 좀 더 자세하게 디자인패턴에 대한 글을 써보겠다.

객체 지향 원칙을 지키도록 리팩토링에 성공!

부가적으로 리팩토링한 내용

  1. uri를 restful하게 변경
    1. 멘토님께서 지적해주신 내용이다. URL만 보더라도 어떤 역할을 하는지 알 수 있도록 변경해야한다고 하셨다.
      따라서 /api/v1/likes/{service}/api/v1/likes/by-user-id/{service}로 변경하였다.
  2. likecount 줄이는 것에 validation 추가
    1. likeCount가 0보다 작은 값이 될 때의 오류를 처리하는 함수를 추가하였다.
private void validateLikeCount() {
    if (likeCount < 1) {
        throw new IllegalArgumentException();
}

마치면서

switch문, case만 보면 마음이 답답..하였는데 마음 속의 짐을 지운 기분이다.
Spring DI를 활용하는 방법이나 serviceImpl 패턴을 적절한 곳에 사용한 점이 아주 뿌듯하다.
아직 완전하지는 않기 때문에 다른 코드들도 리팩토링해보고 싶다.

참조

https://github.com/SWM-Space-Odyssey/WeddingMate_BackEnd/pull/128

BELATED ARTICLES

more