마음만 바쁜 사람

우테코에서 진행한 스프링 장바구니 미션 중 Interceptor와 ArgumentResolver에 대해 알게되었고 이를 바로 코드에 적용해 보려 했다.

 

장바구니(Cart)의 기능들은 우선

1. 호출하는 유저가 시스템에 등록된 사용자인지 확인

2. 해당 사용의 장바구니에 물건 추가/삭제

등의 과정을 거친다.

 

여기서 Interceptor를 적용하면 Controller에 진입하기 전에 인증 여부를 검증할 수 있다.

-> 컨트롤러 메서드들에서 기본적으로 진행했던 중복 로직을 통합 가능하고, 약간이지만 오버헤드를 줄일 수 있다.(컨트롤러 로직 실행 전에 예외처리 할 수 있으니까)

 

LogInInterceptor

public class LoginInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String header = request.getHeader("Authorization");
        if (header == null) {
            throw new AuthenticationException("접근 권한이 없습니다.");
        }

        return HandlerInterceptor.super.preHandle(request, response, handler);
    }

}

 

또한, ArgumentResolver를 사용하면 Request에서 사용자 정보를 추출하던 로직을 미리 수행할 수 있다. 

 

기존의 CartController

@RestController
@RequestMapping("/cart/item")
public class CartController {

    private final CartItemService cartItemService;
    private final MemberService memberService;

    public CartController(CartItemService cartItemService, MemberService memberService) {
        this.cartItemService = cartItemService;
        this.memberService = memberService;
    }

    @GetMapping
    @ResponseBody
    public ResponseEntity<List<ProductEntity>> getCartItem(Model model, HttpServletRequest request) {
        AuthInfo authInfo = basicAuthorizationExtractor.extract(request);

        int memberId = memberService.findMemberId(authInfo.getEmail(), authInfo.getPassword());
        List<ProductEntity> cartItems = cartItemService.getCartItems(memberId);
    }

    @PostMapping("/{productId}")
    public ResponseEntity<Void> insertCartItem(HttpServletRequest request, @PathVariable int productId) {
        AuthInfo authInfo = basicAuthorizationExtractor.extract(request);

        int memberId = memberService.findMemberId(authInfo.getEmail(), authInfo.getPassword());
        cartItemService.addCartItem(new CartItemEntity(memberId, productId));
    }
    
}

 

ArgumentResolver를 적용한 CartController

@RestController
@RequestMapping("/cart/item")
public class CartController {

    private final CartItemService cartItemService;
    private final MemberService memberService;

    public CartController(CartItemService cartItemService, MemberService memberService) {
        this.cartItemService = cartItemService;
        this.memberService = memberService;
    }

    @GetMapping
    @ResponseBody
    public ResponseEntity<List<ProductEntity>> getCartItem(Model model, @AuthenticationPrincipal AuthInfo authInfo) {
        int memberId = memberService.findMemberId(authInfo.getEmail(), authInfo.getPassword());
        List<ProductEntity> cartItems = cartItemService.getCartItems(memberId);

        model.addAttribute("cartItems", cartItems);
        return ResponseEntity.ok().body(cartItems);
    }

    @PostMapping
    public ResponseEntity<Void> insertCartItem(@RequestParam int productId, @AuthenticationPrincipal AuthInfo authInfo) {
        int memberId = memberService.findMemberId(authInfo.getEmail(), authInfo.getPassword());
        cartItemService.addCartItem(new CartItemEntity(memberId, productId));

        return ResponseEntity.ok().build();
    }

    @DeleteMapping
    public ResponseEntity<Void> deleteCartItem(@RequestParam int cartId) {
        cartItemService.deleteCartItem(cartId);
        return ResponseEntity.ok().build();
    }

}

 

 

초기의 ArgumentResolver

public class AuthenticationArgumentResolver implements HandlerMethodArgumentResolver {

    private final BasicAuthorizationExtractor basicAuthorizationExtractor = new BasicAuthorizationExtractor();

    public AuthenticationArgumentResolver(MemberDao memberDao) {
        this.memberDao = memberDao;
    }

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.withContainingClass(AuthInfo.class)
                .hasParameterAnnotation(AuthenticationPrincipal.class);
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        String header = webRequest.getHeader(HttpHeaders.AUTHORIZATION);
		return basicAuthorizationExtractor.extract(header);
    }
    
}

 

코드를 보면 각 컨트롤러의 메서드에서 진핼하던 basicAuthorizationExtractor.extract 메서드를 ArgumentResolver가 미리 처리한 후 회원 정보 객체로 전달해 주게 된다. 

하지만 여기서 살짝 아쉬운게 보였다.

int memberId = memberService.findMemberId(authInfo.getEmail(), authInfo.getPassword());

 

가져온 회원 정보(AuthInfo)에서 id값만을 가지고 추가 로직을 진행하기 때문에 해당 로직이 각 컨트롤러의 메서드마다 중복되는 상황이다.

 

이럴 바에 그냥 id만을 컨트롤러에 넘겨줄 수 있도록 ArgumentResolver에서 처리해 주면 어떨까? 하는 생각에 추가적으로 리팩터링을 진행했다.

 

 

1차 리팩터링 후의 ArgumentResolver

@Component
public class AuthenticationArgumentResolver implements HandlerMethodArgumentResolver {

    private final BasicAuthorizationExtractor basicAuthorizationExtractor = new BasicAuthorizationExtractor();

    private final MemberDao memberDao;

    public AuthenticationArgumentResolver(MemberDao memberDao) {
        this.memberDao = memberDao;
    }

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.withContainingClass(AuthInfo.class)
                .hasParameterAnnotation(AuthenticationPrincipal.class);
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        String header = webRequest.getHeader(HttpHeaders.AUTHORIZATION);
        AuthInfo authInfo = basicAuthorizationExtractor.extract(header);
        return memberDao.findMemberId(authInfo.getEmail(),authInfo.getPassword());
    }

}

 

 

1차 리팩터링 후의 컨트롤러

@RestController
@RequestMapping("/cart/item")
public class CartController {

    private final CartItemService cartItemService;
    private final MemberService memberService;

    public CartController(CartItemService cartItemService, MemberService memberService) {
        this.cartItemService = cartItemService;
        this.memberService = memberService;
    }

    @GetMapping
    @ResponseBody
    public ResponseEntity<List<ProductEntity>> getCartItem(Model model, @AuthenticationPrincipal int memberId) {
//        int memberId = memberService.findMemberId(authInfo.getEmail(), authInfo.getPassword());
        List<ProductEntity> cartItems = cartItemService.getCartItems(memberId);

        model.addAttribute("cartItems", cartItems);
        return ResponseEntity.ok().body(cartItems);
    }

    @PostMapping
    public ResponseEntity<Void> insertCartItem(@RequestParam int productId, @AuthenticationPrincipal int  memberId) {
//        int memberId = memberService.findMemberId(authInfo.getEmail(), authInfo.getPassword());
        cartItemService.addCartItem(new CartItemEntity(memberId, productId));

        return ResponseEntity.ok().build();
    }

    @DeleteMapping
    public ResponseEntity<Void> deleteCartItem(@RequestParam int cartId) {
        cartItemService.deleteCartItem(cartId);
        return ResponseEntity.ok().build();
    }

}

 

findMemberId를 ArgumentResolver에서 진행하기 때문에 어쩔 수 없이 서비스를 참조하게 되었다. 중복은 제거했지만 뭔가 마음에 들지는 않았다.

 

Request -> Contoller -> Service -> DAO 

의 순서로 진행되던 과정들이

Request -> Interceptor, ArgumentResolver -> Service(findMemberId) -> Contoller -> Service -> DAO 

의 흐름으로 변경되면서 계층형 아키텍쳐 구조를 무너뜨리는 것 아닐까 하는 생각이 들었다.

 

여기서 두 가지 생각을 하게 되었는데,

1. 이정도 중복은 감안하고 다시 원상복귀하자

2. 서비스는 뭔가 너무 무거우니까 그냥 DAO 메서드를 호출해보자

 

사실 아직 확실한 정답?은 모르겠지만 1번으로 돌아가면 더이상 얻는 것이 없다고 생각해 2번으로 시도해 보았다.

 

그래서 현재 Service와 Dao는 어떤 기능을 수행하고 있는가? 하고 봤더니 서비스는 Dao 메서드에서 반환 받는 값을 검증하는 용도로만 사용하고 있었다.

 

MemberService의 메서드들 중 하나

public int findMemberId(String email, String password) {
    if (!memberDao.isMemberExist(email, password)) {
        throw new IllegalArgumentException("해당하는 유저가 존재하지 않습니다.");
    }
    return memberDao.findMemberId(email, password);
}

 

그러면 ArgumentResolver에서 Dao 메서드 호출 후 검증 로직까지 담당하게 만들어야 하고, 이러면 굳이 서비스에서 검증을 중복으로 진행할 필요가 없게 되기 때문에 최종적으로 서비스를 없애도 된다는 결론에 도달했다.

 

 

최종 변경한 ArgumentResolver

@Component
public class AuthenticationArgumentResolver implements HandlerMethodArgumentResolver {

    private final BasicAuthorizationExtractor basicAuthorizationExtractor = new BasicAuthorizationExtractor();
    private final MemberDao memberDao;

    public AuthenticationArgumentResolver(MemberDao memberDao) {
        this.memberDao = memberDao;
    }

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.withContainingClass(AuthInfo.class)
                .hasParameterAnnotation(AuthenticationPrincipal.class);
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        String header = webRequest.getHeader(HttpHeaders.AUTHORIZATION);
        AuthInfo authInfo = basicAuthorizationExtractor.extract(header);
        return memberDao.findMemberId(authInfo.getEmail(),authInfo.getPassword());
    }

}

 

하지만 해당 코드로의 변경 과정의 기저에는 Interceptor와 ArgumentResolver가 Service나 Dao와 의존관계를 가지고 있어도 상관 없다. 는 조건이 있기 때문에 결국 Interceptor와 ArgumentResolver가 어느 레이어에 해당하는지, 어떤 구조로 동작하고 있는지에 대해 더 알아봐야 할 것 같다.

 

 

 

profile

마음만 바쁜 사람

@훌루훌루

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!