26. intercepter 활용(인증검사 공통 처리)

 

목차

    1. intercepter란 뭘까?

    인터셉터는 Spring MVC의 핵심 기능 중 하나로, 웹 애플리케이션에서 공통적인 처리를 재사용할 수 있게 해주는 강력한 도구이다.

    인터셉터(Interceptor)는 들어오는 요청과 나가는 응답을 가로채어 특정 로직을 수행할 수 있게 해주는 매커니즘을 제공한다. 이는 AOP(Aspect-Oriented Programming)의 일종으로 볼 수 있으며, 컨트롤러(Controller)로 요청이 도달하기 전, 후 또는 완료된 후에 추가적인 처리를 하기 위해 사용된다.

    대표적인 활용 사례

    1. 인증 및 권한 부여: 사용자의 인증 정보를 검사하여 요청이 유효한 사용자로부터 온 것인지 확인하고, 특정 자원에 대한 접근 권한을 확인한다.
    2. 로깅 및 감사: 요청의 처리 과정에 대한 로깅을 수행하거나 감사 로그를 생성하여 시스템의 보안과 무결성을 유지하는 데 도움을 준다.
    3. 성능 모니터링: 요청 처리 시간을 측정하고 성능 문제를 식별하기 위한 메트릭을 수집한다.
    4. 공통적인 응답 데이터 추가: 모든 응답에 공통적으로 포함되어야 하는 헤더나 데이터를 추가한다.

    인터셉터 구현 방법

    1. 동작 시키고자 하는 인터셉터 기능을 클래스로 만들어 준다.
      단, 만들고 자 하는 해당 클래스에 HandlerInterceptor 인터페이스를 구현하거나
      HandlerInterceptorAdapter 클래스를 상속받아야 한다.
    2. 내가 만든 인터셉터를 Spring Boot 애플리케이션에 등록을 해주어야 동작 한다.
      등록시에는 WebMvcConfigurer 인터페이스를 구현하는 설정 클래스에서 addInterceptors 메서드를 오버라이드하여 인터셉터를 등록한다.

    당연히 필요하다면 인터셉터를 구현한 사용자 정의 클래스를 여러개 정의해서 프로젝트에 활용 할 수 있다.

    2. 인터셉터 구현 클래스 만드는 방법과 인터셉트를 등록 처리

    package com.tenco.bank.handler;
    
    import org.springframework.http.HttpStatus;
    import org.springframework.stereotype.Component;
    import org.springframework.web.servlet.HandlerInterceptor;
    import org.springframework.web.servlet.ModelAndView;
    
    import com.tenco.bank.handler.exception.UnAuthorizedException;
    import com.tenco.bank.repository.model.User;
    import com.tenco.bank.utils.Define;
    
    import jakarta.servlet.http.HttpServletRequest;
    import jakarta.servlet.http.HttpServletResponse;
    
    @Component // IoC 대상 (싱글톤 패턴)
    public class AuthInterceptor implements HandlerInterceptor {
    	
    	// preHandle 동작 흐름 (단 / 스프링부트 설정파일, 설정 클래스에 등록이 되어야 함 : 특정 URL)
    	// 컨트롤러 들어 오기 전에 동작 하는 녀석
    	// true --> 컨트롤러 안으로 들여 보낸다.
    	// false --> 컨트롤러 안으로 못 들어감
    	@Override
    	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    		User principal = (User) request.getSession().getAttribute(Define.PRINCIPAL);
    		if (principal == null) {
    			throw new UnAuthorizedException(Define.NOT_AN_AUTHENTICATED_USER, HttpStatus.UNAUTHORIZED);
    		}
    		return true;
    	}
    	
    	// postHandle 
    	// 뷰가 렌더링 되기 바로전에 콜백 되는 메서드
    	@Override
    	public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
    		// TODO Auto-generated method stub
    		HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
    	}
    	
    	// afterCompletion
    	// 요청 처리가 완료된 후, 즉 뷰가 완전 렌더링이 된 후에 호출 된다.
    	@Override
    	public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
    		// TODO Auto-generated method stub
    		HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
    	}
    	
    }
    • 컨트롤러 호출 전 : preHandle
    • 컨트롤러 호출 후 : postHandle
    • 요청 완료 이후 : afterCompletion, 뷰가 렌더링 된 이후에 호출된다.
    config/WebMvcConfig.java 파일 생성
    인터셉터 등록 하기
    package com.tenco.bank.config;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
    
    import com.tenco.bank.handler.AuthInterceptor;
    
    import lombok.RequiredArgsConstructor;
    
    // @Component // 하나의 클래스를 IoC 하고 싶다면 사용
    @Configuration
    @RequiredArgsConstructor
    public class WebMvcConfig implements WebMvcConfigurer {
    	
    	@Autowired
    	private final AuthInterceptor authInterceptor;
    	
    	// 우리가 만들어 놓은 AuthInterceptor를 등록해야 함.
    	@Override
    	public void addInterceptors(InterceptorRegistry registry) {
    		registry.addInterceptor(authInterceptor)
    			.addPathPatterns("/account/**")
    			.addPathPatterns("/auth/**");
    	}
    	
    }
    AccountController 인증 검사 제거 및 테스트
    package com.tenco.bank.controller;
    
    // import 생략
    
    @Controller
    @RequestMapping("/account")
    @RequiredArgsConstructor
    public class AccountController {
    
    	// 계좌 생성 화면 요청 - DI 처리
    	@Autowired
    	private final AccountService accountService;
    
    	/**
    	 * 계좌 생성 페이지 요청
    	 */
    	@GetMapping("/save")
    	public String savePage() {
    		return "account/save";
    	}
    
    	/**
    	 * 계좌 생성 기능 요청
    	 * 
    	 * @return : /account/list
    	 */
    	@PostMapping("/save")
    	public String saveProc(SaveDTO dto, @SessionAttribute(Define.PRINCIPAL) User principal) {
    		if (dto.getNumber() == null || dto.getNumber().trim().isEmpty()) {
    			throw new DataDeliveryException(Define.ENTER_YOUR_ACCOUNT_NUMBER, HttpStatus.BAD_REQUEST);
    		}
    		if (dto.getPassword() == null || dto.getPassword().trim().isEmpty()) {
    			throw new DataDeliveryException(Define.ENTER_YOUR_PASSWORD, HttpStatus.BAD_REQUEST);
    		}
    		if (dto.getBalance() == null || dto.getBalance() <= 0) {
    			throw new DataDeliveryException(Define.ENTER_YOUR_BALANCE, HttpStatus.BAD_REQUEST);
    		}
    		accountService.createAccount(dto, principal.getId());
    		return "redirect:/account/list";
    	}
    
    	/**
    	 * 계좌 목록 화면 요청
    	 * 
    	 * @return list.jsp
    	 */
    	@GetMapping({ "/list", "/" })
    	public String listPage(Model model, @SessionAttribute(Define.PRINCIPAL) User principal) {
    		List<Account> accountList = accountService.readAccountListByUserId(principal.getId());
    		if (accountList.isEmpty()) {
    			model.addAttribute("accountList", null);
    		} else {
    			model.addAttribute("accountList", accountList);
    		}
    		return "account/list";
    	}
    
    	/**
    	 * 출금 페이지 요청
    	 * 
    	 * @return withdrawal.jsp
    	 */
    	@GetMapping("/withdrawal")
    	public String withdrawalPage() {
    		return "account/withdrawal";
    	}
    
    	/**
    	 * 출금 기능 요청
    	 * 
    	 * @param dto
    	 * @return
    	 */
    	@PostMapping("/withdrawal")
    	public String withdrawalProc(WithdrawalDTO dto, @SessionAttribute(Define.PRINCIPAL) User principal) {
    		// 유효성 검사 (자바 코드를 개발) --> 스프링 부트 @Valid 라이브러리가 존재
    		if (dto.getAmount() == null) {
    			throw new DataDeliveryException(Define.ENTER_YOUR_BALANCE, HttpStatus.BAD_REQUEST);
    		}
    		if (dto.getAmount().longValue() <= 0) {
    			throw new DataDeliveryException(Define.W_BALANCE_VALUE, HttpStatus.BAD_REQUEST);
    		}
    		if (dto.getWAccountNumber() == null || dto.getWAccountNumber().trim().isEmpty()) {
    			throw new DataDeliveryException(Define.ENTER_YOUR_ACCOUNT_NUMBER, HttpStatus.BAD_REQUEST);
    		}
    		if (dto.getWAccountPassword() == null || dto.getWAccountPassword().trim().isEmpty()) {
    			throw new DataDeliveryException(Define.ENTER_YOUR_PASSWORD, HttpStatus.BAD_REQUEST);
    		}
    		accountService.updateAccountWithdraw(dto, principal.getId());
    		return "redirect:/account/list";
    	}
    
    	/**
    	 * 입금 페이지 요청
    	 * 
    	 * @return deposit.jsp
    	 */
    	@GetMapping("/deposit")
    	public String depositPage() {
    		return "account/deposit";
    	}
    
    	/**
    	 * 입금 기능 요청
    	 * 
    	 * @return
    	 */
    	@PostMapping("/deposit")
    	public String depositProc(DepositDTO dto, @SessionAttribute(Define.PRINCIPAL) User principal) {
    		if (dto.getAmount() == null) {
    			throw new DataDeliveryException(Define.ENTER_YOUR_BALANCE, HttpStatus.BAD_REQUEST);
    		}
    		if (dto.getAmount().longValue() <= 0) {
    			throw new DataDeliveryException(Define.D_BALANCE_VALUE, HttpStatus.BAD_REQUEST);
    		}
    		if (dto.getDAccountNumber() == null || dto.getDAccountNumber().trim().isEmpty()) {
    			throw new DataDeliveryException(Define.ENTER_YOUR_ACCOUNT_NUMBER, HttpStatus.BAD_REQUEST);
    		}
    		accountService.updateAccountDeposit(dto, principal.getId());
    		return "redirect:/account/list";
    	}
    
    	// 이체 페이지 요청
    	@GetMapping("/transfer")
    	public String transferPage() {
    		return "account/transfer";
    	}
    
    	// 이체 기능 처리 요청
    	@PostMapping("/transfer")
    	public String transferProc(TransferDTO dto, @SessionAttribute(Define.PRINCIPAL) User principal) {
    		// 유효성 검사 (자바 코드를 개발) --> 스프링 부트 @Valid 라이브러리가 존재
    		if (dto.getAmount() == null) {
    			throw new DataDeliveryException(Define.ENTER_YOUR_BALANCE, HttpStatus.BAD_REQUEST);
    		}
    		if (dto.getAmount().longValue() <= 0) {
    			throw new DataDeliveryException(Define.W_BALANCE_VALUE, HttpStatus.BAD_REQUEST);
    		}
    		if (dto.getWAccountNumber() == null || dto.getWAccountNumber().trim().isEmpty()) {
    			throw new DataDeliveryException(Define.ENTER_YOUR_ACCOUNT_NUMBER, HttpStatus.BAD_REQUEST);
    		}
    		if (dto.getPassword() == null || dto.getPassword().trim().isEmpty()) {
    			throw new DataDeliveryException(Define.ENTER_YOUR_PASSWORD, HttpStatus.BAD_REQUEST);
    		}
    		if (dto.getDAccountNumber() == null || dto.getDAccountNumber().trim().isEmpty()) {
    			throw new DataDeliveryException(Define.ENTER_YOUR_ACCOUNT_NUMBER, HttpStatus.BAD_REQUEST);
    		}
    		accountService.updateAccountTransfer(dto, principal.getId());
    		return "redirect:/account/list";
    	}
    
    	/**
    	 * 계좌 상세 보기 페이지 주소 설계 : localhost:8080/account/detail/1?type=all, deposit, withdrawal
    	 * 
    	 * @return
    	 */
    	@GetMapping("/detail/{accountId}")
    	public String detail(@PathVariable(name = "accountId") Integer accountId, //
    			@RequestParam(required = false, name = "type") String type, @RequestParam(defaultValue = "1", name = "page") int page, Model model) {
    		// 유효성 검사
    		List<String> validTypes = Arrays.asList("all", "deposit", "withdrawal");
    		if (!validTypes.contains(type)) {
    			throw new DataDeliveryException("유효하지 않은 접근 입니다.", HttpStatus.BAD_REQUEST);
    		}
    		Account account = accountService.readAccountById(accountId);
    		int pageSize = 2; // 한페이지에 2개
    		int offset = (page - 1) * pageSize;
    		int totalHistories = accountService.countHistoryByAccountIdAndType(type, accountId);
    		int totalPage = (int) Math.ceil((double) totalHistories / pageSize);
    		int pageBlock = 5;
    		int tenCount = (int) Math.ceil(((double) page / pageBlock) - 1) * pageBlock;
    		int startPage = tenCount + 1;
    		int endPage = (tenCount + 5) > totalPage ? totalPage : (tenCount + pageBlock);
    		List<HistoryAccount> historyList = accountService.readHistoryByAccountId(type, accountId, offset, pageSize);
    		model.addAttribute("type", type);
    		model.addAttribute("currentPage", page);
    		model.addAttribute("startPage", startPage);
    		model.addAttribute("endPage", endPage);
    		model.addAttribute("totalPage", totalPage);
    		model.addAttribute("account", account);
    		model.addAttribute("historyList", historyList);
    		return "account/detail";
    	}
    }