강의

멘토링

로드맵

Inflearn brand logo image

인프런 커뮤니티 질문&답변

dyl0115님의 프로필 이미지
dyl0115

작성한 질문수

스프링 MVC 2편 - 백엔드 웹 개발 활용 기술

ArgumentResolver가 Service레이어 클래스, DB접근에 접근하는 클래스에 의존하도록 설계해도 괜찮나요?

작성

·

123

0

문제 상황을 설명드리겠습니다.

page 정보를 받아서 게시글 리스트, 페이지 정보를 View로 전달하는 Controller 코드 입니다.

@GetMapping("/posts")
public String getPosts(@RequestParam("page") String page, Model model)
{
    // 사용자가 입력한 page문자열과 모든 post의 개수를 매개변수로 받아서 page 정보 생성
    // 사용자가 입력한 page값을 검증 후 올바른 Page 정보를 반환한다.
    // "@#$", "hello" 같은 이상한 page값이 들어오면 모두 1로 변경한다. 
    Page page = Page.validateAndCreate(page, postService.getPostsCount());

    // page 정보를 사용하여 해당 page에 속하는 post들을 반환한다.
    List<Post> posts = postService.getPostList(page);

    // view로 데이터를 전달한다.
    model.addAttribute("page", page);
    model.addAttribute("posts", posts);
}

처음에는 위 방식으로 코드를 작성했습니다.

그런데 page정보 뿐 아니라, 다른 sorting정보, searching정보 등 여러 매개변수들이 들어오다 보니, Controller 코드가 복잡해진다고 느꼈습니다. 간단한 예시 코드는 아래와 같습니다.

@GetMapping("/posts")
public String getPosts(@RequestParam("page") String page, @ModelAttribute("sort") Sort sort, @ModelAttribute("search") Search search, .... 등등 , Model model)
{
    // 여러가지 전처리 로직들
    Page page = Page.validateAndCreate(page, postService.getPostsCount());
    Sort sort = Sort.validateAndCreate(sort, ... )
    Search search = Search.validateAndCreate(search, ...) 
    ....등등

    // 위에서 생성한 정보들을 바탕으로 DB에 쿼리를 날려 posts 데이터를 받아온다.
    List<Post> posts = postService.getPostList(page, sort, search, ...등등);

    // view에 필요한 데이터를 전달한다.
    model.addAttribute("page", page);
    model.addAttribute("posts", posts);
    model.addAttribute("sort", sort); 
    ... 등등
}

예시 코드는 문제상황 설명을 위해서 깔끔하게 작성했으나,

실제로는 제 부족한 실력 탓에 지저분합니다.

 

저는 Controller 코드를 지저분하게 만드는 원인으로

  1. 여러가지 전처리 로직들 (ex. Page.validateAndCreate(...) 코드들)

  2. View에 필요한 데이터들을 전달하는 model.addAttribute(...) 코드들

때문이라고 생각했습니다.

 

위 문제에 대한 해결방안으로 저는 ArgumentResolver를 사용하는 것을 떠올렸습니다.

page에 대해서 @PageInfo라는 애노테이션을 정의하고, 이 애노테이션이 달린 매개변수에 대해서

1.여러가지 전처리 로직들을 수행해주고,

2.View에 자동으로 데이터들을 전달해주는,

ArgumentResolver를 만들어주었습니다.

Page에 대한 ArgumentResolver 코드는 다음과 같습니다.

@RequiredArgsConstructor
@Component
public class PageRequestArgumentResolver implements HandlerMethodArgumentResolver
{
    // 이 부분이 걱정입니다! ArgumentResolver가 특정 Service클래스에 의존해도 될까요?
    private final QuestionService questionService;

    @Override
    public boolean supportsParameter(MethodParameter parameter)
    {
        // @PageInfo를 가지고 있는 경우만 적용.
        boolean hasPageInfoAnnotation = parameter.hasParameterAnnotation(PageInfo.class);
        // 타겟변수가 Page 타입인 경우만 적용.
        boolean isPageClass = parameter.getParameterType().equals(Page.class);

        return hasPageInfoAnnotation && isPageClass;
    }

    @Override
    public Object resolveArgument(MethodParameter parameter,
                                  ModelAndViewContainer mavContainer,
                                  NativeWebRequest webRequest,
                                  WebDataBinderFactory binderFactory) throws Exception
    {
        // url에서 page문자열 정보를 추출.
        String pageString = webRequest.getParameter("page");
        
        // page문자열 정보를 검증 및 생성.
        Page page = Page.validateAndCreate(pageString, questionService.provideAllQuestionsCount());
    
        // mavContainer에 해당 정보를 넣어서 View로 자동전달.
        if (mavContainer != null) mavContainer.addAttribute("page", page);

        // 컨트롤러의 매개변수에 page정보를 바인딩.
        return page;
    }
}

 

위 처럼 page정보 뿐 아니라, 다른 자주쓰는 매개변수들에도 ArgumentResolver를 정의하고 적용하면,

아래와 같이 Controller코드가 굉장히 깔끔해진다고 느꼈습니다.

@GetMapping("/posts")
public String getPosts(@PageInfo Page page, @SortInfo Sort sort, @SearchInfo Search search, ...  등등, Model model)
{
    // 여러가지 전처리 로직들은 ArgumentResolver가 해주기 때문에 코드가 사라집니다.

    // 비즈니스 로직을 수행합니다.
    List<Post> posts = postService.getPostList(page, sort, search, ...등등);

    // View에 필요한 데이터도 ArgumentResolver의 mavContainer를 통해서 자동 전달됩니다.
    // 컨트롤러에서 관심이 있는 posts 정보만 전달합니다.
    model.addAttribute("posts", posts);
}

 

그런데 이게 정말 좋은 코드인지 판단이 잘 서질 않습니다.

그리고 ArgumentResolver가 다른 Service 클래스에 의존을 해도 되는지가 의문입니다.

AI에게 물어보니, 순환참조 문제가 발생할 수 있기 때문에 웬만해서는 하지 말라고하는데...

답변 1

1

안녕하세요. dyl0115님, 공식 서포터즈 David입니다.

그런데 page정보 뿐 아니라, 다른 sorting정보, searching정보 등 여러 매개변수들이 들어오다 보니, Controller 코드가 복잡해진다고 느꼈습니다.

public String getPosts(@RequestParam("page") String page, @ModelAttribute("sort") Sort sort, @ModelAttribute("search") Search search, .... 등등 , Model model)

...

위 처럼 page정보 뿐 아니라, 다른 자주쓰는 매개변수들에도 ArgumentResolver를 정의하고 적용하면, 아래와 같이 Controller코드가 굉장히 깔끔해진다고 느꼈습니다.

public String getPosts(@PageInfo Page page, @SortInfo Sort sort, @SearchInfo Search search, ... 등등, Model model)

앞쪽에서 언급한 문제와 해결한 뒤의 결과가 잘 연결되지 않는 것 같습니다.

제가 이해한 바로는 질문자 분은 '여러 매개변수들이 들어와서' 복잡하다고 느끼셨는데 ArgumentResolver를 사용하여 문제를 해결한 후에도 동일하게 여러 매개변수들이 존재합니다.

이 부분을 다시 고민해 보시면 좋을 것 같습니다:)

감사합니다.

dyl0115님의 프로필 이미지
dyl0115
질문자

David님 답변을 해주셔서 정말 감사합니다.
제가 문제상황 설명을 장황하고 이해하기 어렵게 한 것 같아요. 다시 설명하면 다음과 같습니다.

1. 처음 작성했던 Controller는 매개변수 검증 + 비즈니스 로직 수행 + 뷰로 데이터 전달 3가지의 역할을 수행한다.

@GetMapping("/posts")
public String getPosts(@RequestParam("page") String page, Model model)
{
    // 매개변수를 검증 후 올바른 값을 넣어줌.
    Page page = Page.validateAndCreate(page, postService.getPostsCount());

    // 비즈니스 로직 수행
    List<Post> posts = postService.getPostList(page);

    // 뷰로 데이터를 전달.
    model.addAttribute("page", page);
    model.addAttribute("posts", posts);
}

 

2.이런 Controller는 매개변수가 많아질 때 Controller가 비대해지고 가독성이 나빠지는 문제가 발생한다. (특히 매개변수 검증 + 뷰로 데이터를 전달. 이 2가지 때문에 지저분해진다.)

@GetMapping("/posts")
public String getPosts(@RequestParam("page") String page, @ModelAttribute("sort") Sort sort, @ModelAttribute("search") Search search, .... 등등 , Model model)
{
    // 매개변수 검증 후 올바른 값을 넣어줌. (매개변수가 많아지며 복잡)
    Page page = Page.validateAndCreate(page, postService.getPostsCount());
    Sort sort = Sort.validateAndCreate(sort, ... )
    Search search = Search.validateAndCreate(search, ...) 
    ...

    // 비즈니스 로직들 수행
    List<Post> posts = postService.getPostList(page, sort, search);
    ...

    // 뷰로 데이터를 전달. (매개변수가 많아지며 복잡해짐)
    model.addAttribute("page", page);
    model.addAttribute("posts", posts);
    model.addAttribute("sort", sort); 
    ...
}


3. 위 문제에 대한 해결방법으로 ArgumentResolver를 정의하여 위 2가지 역할(매개변수 검증 역할 + 뷰로 데이터를 전달 역할)을 Controller 대신 수행하도록 한다. Page말고 다른 매개변수에 대해서도 ArgumentResolver를 정의한다.

@RequiredArgsConstructor
@Component
public class PageRequestArgumentResolver implements HandlerMethodArgumentResolver
{
    // 매개변수를 검증하고 값을 넣어주는 과정에서 
    // ArgumentResolver가 DB에 접근하는 스프링빈에 의존하게 됨.
    // ArgumentResolver가 이렇게 의존해도 되는지 궁금합니다.
    private final PostService postService;

    @Override
    public boolean supportsParameter(MethodParameter parameter)
    {
        boolean hasPageInfoAnnotation = parameter.hasParameterAnnotation(PageInfo.class);
        boolean isPageClass = parameter.getParameterType().equals(Page.class);
        return hasPageInfoAnnotation && isPageClass;
    }

    @Override
    public Object resolveArgument(MethodParameter parameter,
                                  ModelAndViewContainer mavContainer,
                                  NativeWebRequest webRequest,
                                  WebDataBinderFactory binderFactory) throws Exception
    {
        // url에서 page문자열 정보를 추출.
        String page = webRequest.getParameter("page");
        
        // 매개변수 검증 역할을 대신 수행
        Page page = Page.validateAndCreate(page, postService.getPostsCount());
    
        // 뷰로 데이터 전달 역할을 대신 수행
        if (mavContainer != null) mavContainer.addAttribute("page", page);

        // 컨트롤러의 매개변수에 page정보를 바인딩.
        return page;
    }
}

 

4.ArgumentResolver를 정의하여 2가지 역할을 대신 수행하게 한 덕분에, 복잡했던 Controller 코드가 깔끔해졌다.

@GetMapping("/posts")
public String getPosts(@PageInfo Page page, @SortInfo Sort sort, @SearchInfo Search search, ...  등등, Model model)
{
    // 여러가지 검증을 ArgumentResolver가 해줘서 관련된 코드가 사라짐.

    // 비즈니스 로직 수행.
    List<Post> posts = postService.getPostList(page, sort, search, ...등등);

    // View에 필요한 page, sort, search 등도 ArgumentResolver가 대신 전달. 관련된 코드가 사라짐.

    // 컨트롤러에서 생성된 posts 정보만 뷰로 전달.
    model.addAttribute("posts", posts);
}

이런 상황입니다.


그리고 이런 상황에서 궁금한 것이
1. 위 3번 과정에서 ArgumentResolver가 외부 DB에 접근하는 스프링빈에 의존하는데 이렇게 해도 되는지?
2. 이런 해결방법이 좋은 해결 방법인지?
이 두 가지가 궁금합니다.

dyl0115님의 프로필 이미지
dyl0115

작성한 질문수

질문하기