ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 스프링 bean 순환참조 에러
    프로젝트/법잘알 2024. 1. 28. 17:47

    like와 suggestion 엔티티가 존재하는데, 프론트엔드 개발자가 suggestion/list에 조회되는 각각의 정책 건의 글마다 로그인된 사용자가 좋아요를 눌렀는지, 안 눌렀는지 상태를 반환해달라고 요청을 했다.

     

     

    public List<SuggestionListResponse> findAllByUser(User user) {
        List<Suggestion> suggestionList = suggestionRepository.findAllByUser(user);
    
        List<Long> suggestionIds = suggestionList.stream()
                .map((suggestion -> suggestion.getId()))
                .collect(Collectors.toList());
    
        List<Like> likes = likeService.likeListBySuggestion(user, suggestionIds);
    
        List<Long> userLikeSuggestions = likes.stream().
                map(like -> like.getSuggestion().getId())
                .collect(Collectors.toList());
    
        List<SuggestionListResponse> suggestions = suggestionList.stream()
                .map((suggestion -> SuggestionListResponse.convert(suggestion, userLikeSuggestions)))
                .collect(Collectors.toList());
    
        return suggestions;
    }

    내가 선택한 방법은 기존의 suggestionList를 반환하는 SuggestionListReponse.convert의 파라미터에 사용자가 좋아요를 누른 게시글의 id를 비교하는 것이다.

     

    @Data
    @Builder
    public class SuggestionListResponse {
        private String title;
        private Category category;
        private String name;
        private LocalDateTime createdTime;
        private Integer likeCount;
        private Boolean likeStatus;
        private Long id;
    
        public static SuggestionListResponse convert(Suggestion suggestion, List<Long> userLikeSuggestion) {
            return SuggestionListResponse.builder()
                    .title(suggestion.getTitle())
                    .category(suggestion.getCategory())
                    .name(suggestion.getUser().getName())
                    .createdTime(suggestion.getCreatedDate())
                    .likeCount(suggestion.getLikeList() != null ? suggestion.getLikeList().size() : 0)
                    .likeStatus(userLikeSuggestion.contains(suggestion.getId()) ? true : false)
                    .id(suggestion.getId())
                    .build();
        }
    }

    삼항연산자를 사용해서 userLikeSuggestion의 리스트에 해당 suggestion의 id가 존재하면 true, 아니라면 false를 반환하도록 설계했다.

     

     

    하지만 실행을 시켜보니 스프링 빈의 순환참조 에러가 발생했다.

    위의 사진처럼 서로 원의 형태로 순환이 되어서 계속 호출되는 에러가 발생한 것이다.

     

     

     

    해결 - LikeService에서 SuggestionService가 아닌 repository 사용

    @Service
    @RequiredArgsConstructor
    @Transactional(readOnly = true)
    public class LikeService {
    
        private final LikeRepository likeRepository;
        private final SuggestionRepository suggestionRepository;
        // Service가 아니라 Repository 사용
        
        ... (생략)
        public Suggestion findById(Long suggestionId) {
            return suggestionRepository.findById(suggestionId).orElseThrow(() -> new SuggestionNotFoundException(suggestionId));
        }
    }

    순환 참조를 방지하기 위해 LikeService에서 SuggestionRepository를 주입받도록 했다.

     

     

    SuggestionService가 아니라 Repository 계층을 주입받는 것은 DDD(Domain-Driven Design)에 별로 좋지는 않다. 하지만 꼭 필요한 빈을 주입받는 과정이며, 위에서 보이는 findById() 메서드 하나에만 suggestionRepository를 사용했기 때문에 이렇게 수정을 하는 것이 좋다고 생각을 했다.

     

     

    @RequiredArgsConstructor가 스프링 빈을 자동으로 주입해주는데, LikeService와 SuggestionService가 서로를 private final로 선언해서 호출을 한다면, 서로를 계속 호출하게 되어 빈이 정상적으로 생성되지 않는다. 하지만 이러한 에러가 발생했을 때 그냥 해결만 된다고 넘어가면 좋은 설계 방식이 아니다.

     

    이번에도 DDD 원칙을 찾아보며 다른 도메인의 repository 계층을 주입받아서 사용해도 되는지에 대해 깊은 고민을 했고, findById()라는 간단한 메서드 하나를 호출할 때만 사용하는 것이기 때문에 사용해도 된다는 결론을 내렸다.

     

     

    에러가 발생하고 수정해야 하는 상황이 생겼을 때, 단순히 해결만 하는 것이 아니라 유지보수 관점에서도 생각해보고, 좋은 설계인지 충분히 고민을 한 다음에 하나를 바꾸더라도 잘 바꿔야 겠다는 생각이 들었다.

     

     

     

    프론트엔드 개발자의 요구대로 설계 완료

    @Service
    @RequiredArgsConstructor
    @Transactional(readOnly = true)
    public class SuggestionService {
    
        private final SuggestionRepository suggestionRepository;
        private final LikeService likeService;
    
        public List<SuggestionListResponse> findByCategory(User user) {
            Category category = Category.categoryConverter(user.getName());
            List<Suggestion> suggestionList = suggestionRepository.findAllByCategory(category);
    
            List<Long> userLikeSuggestions = suggestionUserLikes(suggestionList, user);
    
            List<SuggestionListResponse> suggestions = suggestionList.stream()
                    .map((suggestion -> SuggestionListResponse.convert(suggestion, userLikeSuggestions)))
                    .collect(Collectors.toList());
            return suggestions;
        }
    
        public List<Long> suggestionUserLikes(List<Suggestion> suggestionList, User user) {
            List<Long> suggestionIds = suggestionList.stream()
                    .map((suggestion -> suggestion.getId()))
                    .collect(Collectors.toList());
    
            List<Like> likes = likeService.likeListBySuggestion(user, suggestionIds);
    
            return likes.stream().
                    map(like -> like.getSuggestion().getId())
                    .collect(Collectors.toList());
        }
    }

    프론트엔드 개발자의 요구에 맞춰서 게시글 별로 사용자가 좋아요를 눌렀는지 알려주는 여부도 성공적으로 반환할 수 있게 되었다.

     

    (사실 각각의 게시글마다 쿼리를 날려야 하나? 라고 생각하고 설계에 대한 시간을 많이 썼는데 아래처럼 SuggestionIdIn이라는 Naming query를 통해서 stream으로 반환된 게시글에 해당한 쿼리를 날릴 수 있었다.)

    public interface LikeRepository extends JpaRepository<Like, Long> {
        List<Like> findByUserIdAndSuggestionIdIn(Long userId, List<Long> suggestionIds);
    }
Designed by Tistory.