Spring Boot를 이용한 웹 애플리케이션 개발 시 보안과 유지보수성을 높이기 위한 심화 개념들을 정리한다. CORS 정책 대응부터 전역 예외 처리, 그리고 인터셉터를 이용한 인증 권한 관리까지의 내용을 포함한다.
CORS는 추가 HTTP 헤더를 사용하여, 한 출처(Origin)에서 실행 중인 웹 애플리케이션이 다른 출처의 선택한 자원에 접근할 수 있는 권한을 부여하도록 브라우저에 알려주는 체제다.
동일 출처(Same-Origin) 여부는 다음 세 가지 요소의 조합으로 결정된다.
Protocol (http, https)
Hostname (localhost, naver.com 등)
Port (8080, 5173 등)
이 중 하나라도 다르면 Cross-Origin으로 간주되며, 브라우저의 기본 보안 정책인 SOP(Same-Origin Policy)에 의해 리소스 요청이 차단된다.
개발용: Chrome 확장 프로그램('Allow CORS')을 사용하여 브라우저 수준에서 강제로 허용한다.
서버 설정: @CrossOrigin 어노테이션을 사용하여 허용할 Origin을 명시한다.
컨트롤러나 메소드 단위로 지정 가능하며, @Configuration 클래스에서 전역 설정도 가능하다.
Spring Boot는 기본적으로 'WhiteLabel Error Page'를 제공하지만, 이는 서버의 내부 구조나 코드 위치를 노출할 위험이 있어 별도의 처리가 필수적이다.
@ControllerAdvice를 사용하여 전역에서 발생하는 예외를 가로채고 특정 에러 페이지로 유도한다.
@ControllerAdvice // 에러 처리를 전역으로 설정
public class GlobalExceptionHandler {
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
@ExceptionHandler(RuntimeException.class) // 모든 RuntimeException 처리
public ModelAndView handleRuntimeException(final RuntimeException e) {
log.error("handleRuntimeException : {}", e.getMessage());
ModelAndView mnv = new ModelAndView();
if (e instanceof org.springframework.validation.BindException) {
mnv.addObject("errmsg", "파라미터가 잘 전달되었는지 확인하세요.");
} else if (e instanceof NoHandlerFoundException) {
mnv.addObject("errmsg", "페이지를 찾을 수 없습니다.");
} else {
mnv.addObject("errmsg", "알 수 없는 오류가 발생하였습니다.");
}
mnv.setViewName("/error/commonerr"); // 에러 전용 뷰 경로
return mnv;
}
}
@RestControllerAdvice를 사용하면 객체를 반환할 시 자동으로 JSON으로 응답한다.
// 사용자 정의 예외 클래스
public class HelloException extends RuntimeException {
private String code;
public HelloException(String code, String msg) {
super(msg);
this.code = code;
}
public String getCode() { return code; }
}
@RestControllerAdvice
public class GlobalExceptionHandler2 {
@ExceptionHandler(HelloException.class)
public ErrorResponse handleHelloException(final HelloException e) {
// JSON 형태로 에러 코드와 메시지 반환
return new ErrorResponse(e.getCode(), e.getMessage());
}
}
HTTP는 Stateless 프로토콜이므로 사용자의 상태를 유지하기 위해 별도의 기술이 필요하다.
| 방식 | 특징 |
|---|---|
| Cookie | 브라우저에 저장되는 텍스트 파일. 요청 시 자동으로 서버에 전송됨. |
| Session | 서버 메모리에 상태를 저장. 보안성이 좋으나 서버 부하가 발생할 수 있음. |
| JWT | JSON 포맷의 토큰. 클라이언트가 헤더에 담아 전송하며, 서버는 상태를 저장하지 않음(Stateless). |
@PostMapping("/login")
public String login(String userid, String userpwd, Model model, HttpServletResponse response) {
MemberDto dto = service.login(userid, userpwd);
if (dto != null) {
Cookie cookie = new Cookie("ssafy_id", dto.getUserid());
cookie.setMaxAge(60*60*24); // 24시간 유지
cookie.setPath("/");
response.addCookie(cookie); // 브라우저에 쿠키 전달
} else {
model.addAttribute("msg", "로그인 실패");
}
return "redirect:/";
}
특정 요청을 처리하기 전후에 로직을 가로채어 실행한다. 로그인 여부 확인과 같은 공통 관심 사항(Cross-cutting Concerns)을 처리하기에 적합하다.
@Component
public class ConfirmInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
Cookie[] cookies = request.getCookies();
boolean isLogin = false;
if (cookies != null) {
for (Cookie cookie : cookies) {
if ("ssafy_id".equals(cookie.getName())) {
isLogin = true;
break;
}
}
}
if (!isLogin) {
response.sendRedirect("/"); // 로그인 안 되어있으면 홈으로
return false; // 요청 진행 중단
}
return true; // 요청 진행 허용
}
}
@Configuration
public class WebConfiguration implements WebMvcConfigurer {
@Autowired
private ConfirmInterceptor intercepter;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(intercepter)
.addPathPatterns("/write", "/update/**", "/delete/**"); // 가로챌 경로 지정
}
}
프론트엔드와 백엔드 간의 원활한 협업을 위해 REST API를 자동 문서화해주는 도구다. 의존성을 추가하고 설정 클래스를 작성하면 /swagger-ui/index.html 경로를 통해 API 명세서 확인 및 직접 테스트가 가능하다.