inflearn logo
강의

강의

N
챌린지

챌린지

멘토링

멘토링

N
클립

클립

로드맵

로드맵

지식공유

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

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

144

dyl0115

작성한 질문수 1

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에게 물어보니, 순환참조 문제가 발생할 수 있기 때문에 웬만해서는 하지 말라고하는데...

spring mvc

답변 1

1

David

안녕하세요. 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를 사용하여 문제를 해결한 후에도 동일하게 여러 매개변수들이 존재합니다.

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

감사합니다.

0

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. 이런 해결방법이 좋은 해결 방법인지?
이 두 가지가 궁금합니다.

이미지 업로드와 db 트랜잭션 묶는법

0

41

1

Could not resolve org.springframework.boot:spring-boot-starter-validation:2.4.4

0

50

2

MessageSourceTest 코드

0

46

1

인터셉터 에러 설정

0

48

1

resolveArgument()메서드 질문

0

55

1

43강 검증1 에서 실패 로직 관련 질문있습니다.

0

53

2

타임리프 3.X 버전 rendering, serializer 에러 해결 방법

2

132

3

스프링 빈에 등록이 안되는거 같은데 어떻게 하면 좋을까요?ㅠㅠ

0

87

3

pdf 오타 문의

0

56

1

ItemUpdateForm 검증 관련 질문입니다.

0

47

1

22page 링크 주소 변경

0

58

2

특정 데이터와 파일을 함께 저장 시, 테이블 구조 질문

0

52

1

섹션3번 수업에 대한 질문입니다.

0

79

2

@Autowired 보다 더 좋은 방법이 어떤 걸까요?

0

84

2

타입컨버터 가 람다랑 비슷해 보이는데 저의 생각이 맞는지?.

0

64

1

자바스크립트 인라인에서 객체 직렬화 시 오류가 납니다

0

141

3

스프링부트 - 오류페이지2 에서 500.html 에서 쓰인 객체 질문

0

63

1

톰캣 에러 페이지가 안보입니다.

0

100

2

apiEceptionController에서 센드 에러 호출하면 안되는지?

0

80

1

세션 타임아웃시 쿠키 삭제 방법이 없나요?

0

118

2

ApiExceptionController 질문드립니다.

0

62

1

셀렉박스 챕터에서 option value에 ==배송 방식 선택== 이것을 넣은 이유가 궁금함, 이렇게 구상해도 되는지?

0

64

1

MemberRepository 필드의 fianl 선언 유무

0

84

2

혹시 index.html 에서는 fragment 사용이 안되는건가요

0

57

1