본문 바로가기

Spring boot/Bank App 만들기(deployment)

Bank App 만들기 ( deployment ) - intercepter 활용 ( 인증검사 공통 처리 )

intercepter란 뭘까?

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

 

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

 

 

 

 

대표적인 활용 사례

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

 

인터셉터 구현 방법

먼저 딱 2가지만 기억해 보자.

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

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

 

 

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

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;
import jakarta.servlet.http.HttpSession;

@Component 	// IoC 대상 (싱글톤 패턴)
public class AuthInterceptor implements HandlerInterceptor{

	// preHandle 동작 흐름 (단, 스프링부트 설정 파일, 설정 클래스에 등록이 되어야 한다 : 특정 URL)
	// 컨트롤러 들어 오기 전에 동작하는 녀석
	// true -> 컨트롤러 안으로 들여 보낸다.
	// false -> 컨트롤러 안으로 못 들어간다.
	@Override
	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
			throws Exception {
		
		HttpSession session =  request.getSession();
		User principal = (User)session.getAttribute(Define.PRINCIPAL);
		
		if(principal == null) {
			throw new UnAuthorizedException("로그인 먼저 해주세요.", HttpStatus.UNAUTHORIZED);
		}
		
		return true;
	}
	
	// postHandle
	// 뷰가 렌더링 되기 바로 전에 콜백 되는 메서드
	@Override
	public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
			ModelAndView modelAndView) throws Exception {
		HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
	}

	// afterCompletion
	// 요청 처리가 완료 된 후, 즉 뷰가 완전 렌더링이 된 후에 호출된다.
	@Override
	public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
			throws Exception {
		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;

@Configuration	// 하나의 클래스를 IOC 하고 싶다면 사용
@RequiredArgsConstructor
public class WebMvcConfig implements WebMvcConfigurer {
	
	@Autowired	// DI
	private final AuthInterceptor authInterceptor;
	
	// @RequiredArgsConstructor <- 생성자 대신 사용 가능
	
	// 우리가 만들어 놓은 AuthInterceptor 를 등록해야 한다.
	@Override
	public void addInterceptors(InterceptorRegistry registry) {
		registry.addInterceptor(authInterceptor).addPathPatterns("/account/**")
		.addPathPatterns("/auth/**");
	}

}

 

 

 

 

 

AccountController 인증 검사 제거 및 테스트
package com.tenco.bank.controller;

import java.util.Arrays;
import java.util.List;

import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.SessionAttribute;

import com.tenco.bank.dto.DepositDTO;
import com.tenco.bank.dto.SaveDTO;
import com.tenco.bank.dto.TransferDTO;
import com.tenco.bank.dto.WithdrawalDTO;
import com.tenco.bank.handler.exception.DataDeliveryException;
import com.tenco.bank.handler.exception.UnAuthorizedException;
import com.tenco.bank.repository.model.Account;
import com.tenco.bank.repository.model.HistoryAccount;
import com.tenco.bank.repository.model.User;
import com.tenco.bank.service.AccountService;
import com.tenco.bank.utils.Define;

import jakarta.servlet.http.HttpSession;

@Controller // IoC 대상(싱글톤으로 관리)
@RequestMapping("/account")
public class AccountController {

	// 계좌 생성 화면 요청 DI 처리
	private final HttpSession session;

	private final AccountService accountService;

	public AccountController(HttpSession session, AccountService accountService) {
		
		this.session = session;
		this.accountService = accountService;
		
	}

	/**
	 * 계좌 생성 페이지 요청 주소 설계 : http://localhost:8080/account/save
	 * 
	 * @return save.jsp
	 */
	@GetMapping("/save")
	public String savePage() {

		return "account/save";
		
	}

	/**
	 * 계좌 생성 기능 요청 주소 설계 : http://localhost:8080/account/save
	 * 
	 * @return 추후 계좌 목록 페이지 이동 처리
	 */
	@PostMapping("/save")
	public String saveProc(SaveDTO dto, @SessionAttribute(Define.PRINCIPAL) User principal) {
		// 1. form 데이터 추출 (파싱전략) SaveDTO

		// 3. 유효성 검사
		if (dto.getNumber() == null || dto.getNumber().isEmpty()) {
			throw new DataDeliveryException(Define.ENTER_YOUR_ACCOUNT_NUMBER, HttpStatus.BAD_REQUEST);
		}
		if (dto.getPassword() == null || dto.getPassword().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);
		}

		// 4. 서비스 호출
		accountService.createAccount(dto, principal.getId());

		return "redirect:/account/list";
		
	}

	/**
	 * 계좌 목록 화면 요청 주소 설계 : http://localhost:8080/account/list , ../
	 * 
	 * @return
	 */
	@GetMapping({ "/list", "/" })
	public String listPage(Model model) {

		// 1. 인증 검사
		User principal = (User) session.getAttribute(Define.PRINCIPAL);
		if (principal == null) {
			throw new UnAuthorizedException(Define.NOT_AN_AUTHENTICATED_USER, HttpStatus.UNAUTHORIZED);
		}

		// 2. 유효성 검사
		// 3. 서비스 호출
		List<Account> accountList = accountService.readAccountListByUserId(principal.getId());

		if (accountList.isEmpty()) {
			model.addAttribute("accountList", null);
		} else {
			model.addAttribute("accountList", accountList);
		}

		// JSP 데이터를 넣어주는 방법
		return "account/list";
		
	}

	/**
	 * 출금 페이지 요청
	 * 
	 * @return withdrawal.jsp
	 */
	@GetMapping("/withdrawal")
	public String withdrawalPage() {

		return "account/withdrawal";
		
	}

	@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.getWAccountPassword() == null) {
			throw new DataDeliveryException(Define.ENTER_YOUR_ACCOUNT_NUMBER, HttpStatus.BAD_REQUEST);
		}
		if(dto.getWAccountPassword() == null || dto.getWAccountPassword().isEmpty()) {
			throw new DataDeliveryException(Define.ENTER_YOUR_PASSWORD, HttpStatus.BAD_REQUEST);
		}
		
		accountService.updateAccountWithdraw(dto, principal.getId());
		
		return "redirect:/account/list";

	}

	/**
	 * 입금 페이지 요청
	 * @return
	 */
	@GetMapping("/deposit")
	public String depositPage() {

		return "account/deposit";
		
	}
	
	
	// 입금 처리 기능 만들기
	@PostMapping("/deposit")
	public String depositProc(DepositDTO dto, @SessionAttribute(Define.PRINCIPAL) User principal) {

		// 2. 유효성 검사
		if(dto.getAmount() == null) {
			throw new DataDeliveryException(Define.D_BALANCE_VALUE, HttpStatus.BAD_REQUEST);
		}
		
		if(dto.getAmount().longValue() <= 0) {
			throw new DataDeliveryException(Define.ENTER_YOUR_BALANCE, HttpStatus.BAD_REQUEST);
		}
		
		if(dto.getDAccountNumber() == null) {
			throw new DataDeliveryException(Define.ENTER_YOUR_ACCOUNT_NUMBER, HttpStatus.BAD_REQUEST);
		}
		
		if(dto.getDAccountNumber() != dto.getDAccountNumber()) {
			throw new DataDeliveryException(Define.NOT_ACCOUNT_OWNER, HttpStatus.BAD_REQUEST);
		}
		
		accountService.updateAccountDeposit(dto, principal.getId());
		
		return "redirect:/account/list";
		
	}
	
	
	/**
	 * 계좌 이체 화면 요청 
	 * @return transfer.jsp 
	 */
	@GetMapping("/transfer")
	public String transferPage() {

		return "account/transfer";
		
	}
	
     
	/**
	 * 계좌 이체 기능 구현 
	 * @param TransferDTO 
	 * @return redirect:/account/list
	 */
	@PostMapping("/transfer")
	public String transferProc(TransferDTO dto, @SessionAttribute(Define.PRINCIPAL) User principal) {

		// 2. 유효성 검사
		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.getWAccountNumber() == null || dto.getWAccountNumber().isEmpty()) {
			throw new DataDeliveryException("출금하실 계좌번호를 입력해주세요.", HttpStatus.BAD_REQUEST);
		}
		if (dto.getDAccountNumber() == null || dto.getDAccountNumber().isEmpty()) {
			throw new DataDeliveryException("이체하실 계좌번호를 입력해주세요.", HttpStatus.BAD_REQUEST);
		}
		if (dto.getPassword() == null || dto.getPassword().isEmpty()) {
			throw new DataDeliveryException(Define.ENTER_YOUR_PASSWORD, HttpStatus.BAD_REQUEST);
		}

		// 서비스 호출
		accountService.updateAccountTransfer(dto, principal.getId());

		return "redirect:/account/list";
		
	}
	
	/**
	 * 계좌 상세 보기 페이지
	 * 주소 설계 : http://localhost:8080/account/detail/${1}?type=all, deposit, withdraw
	 * @return detail.jsp
	 */
	@GetMapping("/detail/{accountId}")
	public String detail(@PathVariable (name = "accountId") Integer accountId, 
			@RequestParam (required = false, name = "type")String type, 
			@RequestParam (name = "page", defaultValue = "1") int page,
			@RequestParam (name = "size", defaultValue = "2") int size,
			Model model) {

		// 2. 유효성 검사
		List<String> vaildTypes = Arrays.asList("all", "deposit", "withdrawal");
		if(!vaildTypes.contains(type)) {
			throw new DataDeliveryException("유효하지 않은 접근입니다.", HttpStatus.BAD_REQUEST);
		}
		
		// 페이지 갯수를 계산하기 위해서 총 페이지 수를 계산해줘야한다.
		int totalRecords = accountService.countHistoryByAccountIdAndType(type, accountId);
		int totalPages = (int)Math.ceil((double)totalRecords / size);
		
		Account account = accountService.readAccountById(accountId);
		List<HistoryAccount> historyList = accountService.readHistoryByAccountId(type, accountId, page, size);
		
		model.addAttribute("account", account);
		model.addAttribute("historyList", historyList);
		
		model.addAttribute("currentPage", page);
		model.addAttribute("totalPages", totalPages);
		model.addAttribute("type", type);
		model.addAttribute("size", size);
		
		System.out.println("11111111111111111");
		return "account/detail";
		
	}

}
728x90