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

대표적인 활용 사례
- 인증 및 권한 부여 : 사용자의 인증 정보를 검사하여 요청이 유효한 사용자로부터 온 것인지 확인하고, 특정 자원에 대한 접근 권한을 확인한다.
- 로깅 및 감사 : 요청의 처리 과정에 대한 로깅을 수행하거나 감사 로그를 생성하여 시스템의 보안과 무결성을 유지하는 데 도움을 준다.
- 성능 모니터링 : 요청 처리 시간을 측정하고 성능 문제를 식별하기 위한 메트릭을 수집한다.
- 공통적인 응답 데이터 추가 : 모든 응답에 공통적으로 포함되어야 하는 헤더나 데이터를 추가한다.
인터셉터 구현 방법
먼저 딱 2가지만 기억하자.
- 동작 시키고자 하는 인터셉터 기능을 클래스로 만들어 준다. 단, 만들고 자 하는 해당 클래스에 HandlerInterceptor 인터페이스를 구현하거나 HandlerInterceptorAdapter 클래스를 상속받아야 한다.
- 내가 만든 인터셉터를 Spring Boot 애플리케이션에 등록을 해주어야 동작 한다. 등록시에는 WebMvcConfigurer 인터페이스를 구현하는 설정 클래스에서 addInterceptors 메서드를 오버라이드하여 인터셉터를 등록한다.
당연히 필요하다면 인터셉터를 구현한 사용자 정의 클래스를 여러개 정의해서 프로젝트에 활용 할 수 있다.
인터셉터 구현 클래스 만드는 방법과 인터셉트 등록 처리
package com.tenco.bank.handler;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@Component // IoC 대상 (싱글톤 패턴)
public class AuthInteceptor implements HandlerInterceptor {
// preHandle 동작 흐름 (단, 스프링부트 설정파일, 설정 클래스에 등록이 되어야 동작함)
// 컨트롤러가 들어오기 전에 이 메서드가 먼저 동작
// return 값 -- true : 컨트롤러 안으로 들여 보낸다. --false : 컨트롤러 안으로 들어갈 수 없다.
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
return HandlerInterceptor.super.preHandle(request, response, handler);
}
// 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);
}
// 요청 처리가 완료된 후. 즉, 뷰가 완전히 렌더링 된 후에 호출 된다.
@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.stereotype.Component;
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;
// WebMvcConfigurer : 약속 - 설정파일
@Configuration // 하나의 클래스를 IOC 하고 싶을때 사용
@RequiredArgsConstructor
public class WebWvcConfig 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.beans.factory.annotation.Autowired;
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; // 멤버변수 선언 시 final 사용하면 성능적으로 더 낫다.
// @Autowired
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) {
// 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", "/" }) // url 매핑을 두 개 설정 가능하다.
public String listPage(Model model, @SessionAttribute(Define.PRINCIPAL) User principal) { // TODO - 검색 기능 + 페이징 처리 추가 가능
// 2. 유효성 검사 - 추출할 데이터 없으므로 아직 필요 x
// 3. 서비스 호출
List<Account> accountList = accountService.readAccountListByUserId(principal.getId());
if (accountList.isEmpty()) { // 리스트는 있으나, 값이 비어있는 경우
model.addAttribute("accountList", null); // 값은 null
} else {
model.addAttribute("accountList", accountList);
}
// JSP에 데이터를 넣어주는 기술 - set/getAttribute
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) {
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 deposit.jsp
*/
@GetMapping("/deposit")
public String depositPage() {
return "account/deposit";
}
/**
* 입금 기능 요청
*
* @param dto
* @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.W_BALANCE_VALUE, HttpStatus.BAD_REQUEST);
}
if (dto.getDAccountNumber() == null) {
throw new DataDeliveryException(Define.ENTER_YOUR_ACCOUNT_NUMBER, HttpStatus.BAD_REQUEST);
}
accountService.updateAccountDeposit(dto, principal.getId());
return "redirect:/account/list";
}
/**
* 이체 페이지 요청
*
* @return transfer.jsp
*/
@GetMapping("/transfer")
public String transferPage() {
return "account/transfer";
}
/**
* 이체 기능 처리 요청
*
* @param dto
* @return
*/
@PostMapping("/transfer")
public String tranferProc(TransferDTO 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.W_BALANCE_VALUE, HttpStatus.BAD_REQUEST);
}
if (dto.getWAccountNumber() == null) {
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.getDAccountNumber() == null) {
throw new DataDeliveryException(Define.ENTER_YOUR_ACCOUNT_NUMBER, HttpStatus.BAD_REQUEST);
}
accountService.updateAccountTransfer(dto, principal.getId());
return "redirect:/account/list";
}
/**
* 계좌 상세 보기 페이지
* 주소설계 - http://localhost:8080/account/detail/1?type=all, deposit, withdraw
* @RequestParam - 쿼리 스트링으로 들어오는 주소를 받아오는 방법
* @return
*/
@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) {
// 유효성 검사
List<String> validTypes = Arrays.asList("all", "deposit", "withdrawal");
if(!validTypes.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);
return "account/detail";
}
}
UserController 생성자 수정 및 코드 추가
package com.tenco.bank.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import com.tenco.bank.dto.SignInDTO;
import com.tenco.bank.dto.SignUpDTO;
import com.tenco.bank.handler.exception.DataDeliveryException;
import com.tenco.bank.repository.model.User;
import com.tenco.bank.service.UserService;
import com.tenco.bank.utils.Define;
import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
@Controller
@RequestMapping("/user")
@RequiredArgsConstructor
public class UserController {
@Autowired
private final UserService userService;
private final HttpSession session;
/**
* 회원 가입 페이지 요청
* 주소 설계 : http://localhost:8080/user/sign-up
* @return signUp.jsp
*/
@GetMapping("/sign-up")
public String signUpPage() {
return "user/signUp";
}
/**
* 회원 가입 로직 처리 요청
* 주소 설계 : http://localhost:8080/user/sign-up
* @param dto
* @return
*/
@PostMapping("/sign-up")
public String signUpProc(SignUpDTO dto) {
if(dto.getUsername() == null || dto.getUsername().isEmpty()) {
throw new DataDeliveryException(Define.ENTER_YOUR_USERNAME, HttpStatus.BAD_REQUEST);
}
if(dto.getPassword() == null || dto.getPassword().isEmpty()) {
throw new DataDeliveryException(Define.ENTER_YOUR_PASSWORD, HttpStatus.BAD_REQUEST);
}
if(dto.getFullname() == null || dto.getFullname().isEmpty()) {
throw new DataDeliveryException(Define.ENTER_YOUR_FULLNAME, HttpStatus.BAD_REQUEST);
}
userService.createUser(dto);
return "redirect:/user/sign-in";
}
/**
* 로그인 화면 요청
* @return
*/
@GetMapping("/sign-in")
public String singInPage() {
return "user/signIn";
}
/**
* 로그인 요청 처리
* 주소설계 : http://localhost:8080/user/sign-in
* @return
*/
@PostMapping("/sign-in")
public String signProc(SignInDTO dto) {
if(dto.getUsername() == null || dto.getUsername().isEmpty()) {
throw new DataDeliveryException(Define.ENTER_YOUR_USERNAME, HttpStatus.BAD_REQUEST);
}
if(dto.getPassword() == null || dto.getPassword().isEmpty()) {
throw new DataDeliveryException(Define.ENTER_YOUR_PASSWORD, HttpStatus.BAD_REQUEST);
}
User principal = userService.readUser(dto);
session.setAttribute(Define.PRINCIPAL, principal);
return "redirect:/account/list";
}
/**
* 로그아웃 처리
* @return
*/
@GetMapping("/logout")
public String logout() {
session.invalidate();
return "redirect:/user/sign-in";
}
}
'Spring boot' 카테고리의 다른 글
스프링 부트 핵심 콘셉트 (1) | 2024.09.26 |
---|---|
Bank App 만들기 - 사용자 비밀번호 암호화 처리 (0) | 2024.08.16 |
Bank App 만들기 - 계좌 상세보기 페이징 처리 (0) | 2024.08.16 |
Bank App 만들기 - 간단한 유틸 클래스 만들기 (0) | 2024.08.16 |
Bank App 만들기 - 계좌 상세보기(기능, 동적쿼리 구현) (0) | 2024.08.16 |