*책 내용과 다르게, 다음과 같은 환경에서 프로젝트 생성
- Windows11(윈도우 11) 환경
- 자바 JDK 17 버전 설치 https://yungenie.tistory.com/11
[Java] 차근차근 Java 설치하기 (JDK17, Window 11)
자바 개발 도구 설치 방법에 대해서 알아보겠습니다. Java17은 LTS(Long Term Support : 장기 지원) 릴리즈로 1년 후까지 기술 지원 및 버그를 개선한 서비스를 제공받을 수 있습니다. 업데이트 버전을 꾸
yungenie.tistory.com
- 스프링 부트 4.31.0 사용 - STS(Spring Tool Suite) 설치(Spring Tools for Eclipse - https://spring.io/tools)
=> https://priming.tistory.com/147 참고
[Windows] Spring Tool Suite 4(STS 4) 다운로드 및 설치
STS란?Spring Tool Suite(STS)는 스프링 프로젝트를 생성하고, 개발할 수 있게 해주는 도구입니다. STS 설치 과정에 대해 설명드리겠습니다. 설치 파일 다운로드STS 공식 사이트에서 설치 파일을 다운로
priming.tistory.com

- MySQL Community Server 8.0.42 설치 https://dev.mysql.com/downloads/mysql/
MySQL :: Download MySQL Community Server
Select Version: 9.3.0 Innovation 8.4.5 LTS 8.0.42 Select Operating System: Select Operating System… Microsoft Windows Ubuntu Linux Debian Linux SUSE Linux Enterprise Server Red Hat Enterprise Linux / Oracle Linux Fedora Linux - Generic Oracle Solaris mac
dev.mysql.com

- Gradle
**STS에서 Gradle 프로젝트 생성한 과정



*** 함께 보면 좋은 글
스프링 부트 핵심 가이드(장정우 지음) - 스프링 부트 개요
1. 스프링 프레임워크자바(Java) 기반 애플리케이션 프레임워크로, 엔터프라이즈급(기업 환경 대상 개발) 애플리케이션을 위한 다양한 기능 제공-> 오픈소스 경량급 애플리케이션 프레임워크로
keep-programming-study.tistory.com
스프링 부트 핵심 가이드(장정우 지음) - 개발에 앞서 알면 좋은 기초 지식
1. 서버 간 통신마이크로서비스 아키텍처에서 한 서버가 다른 서버에 통신을 요청하는 것을 의미-> 한 대는 서버/다른 한 대는 클라이언트가 됨 가장 많이 사용되는 방식은 HTTP/HTTPS 방식(TCP/IP, SOA
keep-programming-study.tistory.com
스프링부트 핵심 가이드(장정우 지음) - 스프링 시큐리티(Spring Security)와 JWT 1편
*책 내용과 다르게, 다음과 같은 환경에서 프로젝트 생성 Windows11(윈도우 11) 환경자바 JDK 17 버전 설치 https://yungenie.tistory.com/11 [Java] 차근차근 Java 설치하기 (JDK17, Window 11)자바 개발 도구 설치 방법
keep-programming-study.tistory.com
스프링부트 핵심 가이드(장정우 지음) - 스프링 시큐리티(Spring Security)와 JWT 2편
*** 함께 보면 좋은 글2026.04.10 - [스프링(Spring), 스프링부트(SpringBoot)/스프링부트(SpringBoot) 기초] - 스프링부트 핵심 가이드(장정우 지음) - 스프링 시큐리티(Spring Security)와 JWT 1편 스프링부트 핵심
keep-programming-study.tistory.com
8) 스프링 시큐리티 테스트
- '스프링 시큐리티(Spring Security)와 JWT' 1편과 2편에서 작성한 코드들을 바탕으로,
클라이언트 입장에서 스프링 시큐리티가 동작하는 상황에서 테스트 수행 - Swagger 활용하여 테스트
: WebSecurity를 사용하는 configure() 메서드에서 인증에 대한 예외 처리를 했으므로 정상 접속 가능
(1) 애플리케이션 가동 시(서버 시작 시), 스프링 시큐리티 관련 로그 살펴보기
- JwtTokenProvider 클래스는 @Component로 등록되어 있고, @PostConstruct로 init() 메서드가 정의되어 있음
- init() 메서드에서는 application.properties 파일에 정의된 secretKey의 값을 가져와 인코딩
-> 기본값인 "secretKey"를 그대로 사용하면 보안에 취약하므로, application.properties 파일에 꼭 다음과 같이 'https://randomkeygen.com/'등에서 생성한 키를 입력- 이때 application.properties의 변수명을 꼭 JwtTokenProvider와 대조해보자..
본인은 @Value("${springboot.jwt.secret}")를 @Value("${springbot.jwt.secret}")로 해서 오류가 났다.
- 이때 application.properties의 변수명을 꼭 JwtTokenProvider와 대조해보자..

- 아래는 JwtTokenProvider 관련 로그이다(실제 운영 환경이라면 비밀번호(security password)를 절대로 올리면 안된다!!)
2026-04-17 16:40:02.870 [main] INFO c.e.d.c.security.JwtTokenProvider - [init] JwtTokenProvider 내 secretKey 초기화 시작
2026-04-17 16:40:02.881 [main] INFO c.e.d.c.security.JwtTokenProvider - [init] JwtTokenProvider 내 secretKey 초기화 완료
2026-04-17 16:40:03.725 [main] WARN o.s.b.a.o.j.JpaBaseConfiguration$JpaWebConfiguration - spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning
2026-04-17 16:40:04.978 [main] INFO o.s.b.a.e.web.EndpointLinksResolver - Exposing 14 endpoints beneath base path '/actuator'
2026-04-17 16:40:05.106 [main] WARN o.s.b.a.s.s.UserDetailsServiceAutoConfiguration -
Using generated security password: 2376ca77-a5da-4db4-8cf9-f71ca896045f
This generated password is for development use only. Your security configuration must be updated before running your application in production.
- DefaultSecurityFilterChain 관련 로그:
Spring Boot 3.x + Spring Security 6.x부터는 로깅 정책이 바뀌어 디버그 레벨에서만 확인 가능
- 이 프로젝트에서는 application.properties에 logging.level.org.springframework.security=DEBUG를 추가하면 출력됨
-> Spring Security 로그를 DEBUG 레벨로 출력하는 것
- 이 프로젝트에서는 application.properties에 logging.level.org.springframework.security=DEBUG를 추가하면 출력됨
(2) Hibernate 오류 해결
- H2 데이터베이스에서 USER는 예약어(reserved keyword)
-> 따라서 references user라고 쓰면 SQL 파서가 식별자(identifier)가 아닌 예약어로 인식해서 문법 오류가 발생- Hibernate가 user_roles 테이블에 외래 키를 만들면서 user 테이블을 참조하려고 했는데, 예약어 충돌 때문에 실패
- 엔티티 클래스에서 @Table(name="users")로, 테이블 이름을 예약어가 아닌 이름으로 바꾸기
@Entity
@Table(name = "users") // user 대신 users
public class User implements UserDetails {
...
}
(3) JwtAuthenticationFilter에서, 특정 경로는 JWT 검증을 건너뛰도록 조건을 추가
- http://localhost:8080/swagger-ui/index.html에 접속했을 때, 아래와 같은 시큐리티 로그인 화면이 뜨지 않도록 만들어야 함

package com.example.demo.config.security;
import java.io.IOException;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
/**
* JWT 인증을 처리하는 커스텀 필터 클래스
*
* OncePerRequestFilter를 상속하여 모든 요청마다 한 번만 실행됨.
*
* 주요 역할:
* 1. HTTP 요청에서 JWT 토큰 추출 (resolveToken)
* 2. 토큰 유효성 검증 (validateToken)
* 3. 토큰이 유효하다면 Authentication 객체 생성 후 SecurityContext에 저장
* - SecurityContextHolder는 현재 요청의 인증 상태를 관리하는 Spring Security의 핵심 컨텍스트
* 4. 필터 체인을 계속 진행하여 이후 요청 처리 로직으로 전달
* ++ Swagger UI, OpenAPI 문서 등 공개 엔드포인트는 필터를 건너뛰도록 예외 처리 추가
*
* 즉, 이 필터는 클라이언트 요청이 들어올 때마다 JWT 기반 인증을 수행하고,
* 인증된 사용자 정보를 Spring Security 컨텍스트에 주입하는 역할을 담당
*/
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final Logger LOGGER = LoggerFactory.getLogger(JwtAuthenticationFilter.class);
private final JwtTokenProvider jwtTokenProvider;
public JwtAuthenticationFilter(JwtTokenProvider jwtTokenProvider) {
this.jwtTokenProvider = jwtTokenProvider;
}
// JWT 검증을 건너뛸 경로들
private static final List<String> EXCLUDE_URLS = List.of(
"/swagger-ui",
"/swagger-ui.html",
"/v3/api-docs",
"/api/public"
);
/**
* 실제 필터 로직 구현 메서드
*
* 동작 흐름:
* 1. 요청(servletRequest)에서 JWT 토큰 추출
* 2. 토큰 값 로그 출력
* 3. 토큰 유효성 검증 시작
* 4. 토큰이 존재하고 유효하다면:
* - JwtTokenProvider.getAuthentication(token) 호출 → Authentication 객체 생성
* - SecurityContextHolder.getContext().setAuthentication(authentication) → 인증 정보 저장
* - 로그 출력: 유효성 체크 완료
* 5. 필터 체인 계속 진행 (filterChain.doFilter)
*
* @param servletRequest 클라이언트 요청 객체
* @param servletResponse 클라이언트 응답 객체
* @param filterChain 필터 체인 객체
*/
@Override
protected void doFilterInternal(HttpServletRequest servletRequest,
HttpServletResponse servletResponse,
FilterChain filterChain) throws ServletException, IOException {
// Swagger, 공개 API는 JWT 검증 건너뛰기
String path = servletRequest.getRequestURI();
if (EXCLUDE_URLS.stream().anyMatch(path::startsWith)) {
filterChain.doFilter(servletRequest, servletResponse);
return;
}
// 요청에서 JWT 토큰 추출
String token = jwtTokenProvider.resolveToken(servletRequest);
LOGGER.info("[doFilterInternal] token 값 추출 완료. token: {}", token);
// 토큰 유효성 검증
LOGGER.info("[doFilterInternal] token 값 유효성 체크 시작");
if (token != null && jwtTokenProvider.validateToken(token)) {
// 토큰이 유효하면 Authentication 객체 생성 후 SecurityContext에 저장
Authentication authentication = jwtTokenProvider.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(authentication);
LOGGER.info("[doFilterInternal] token 값 유효성 체크 완료");
}
// 필터 체인 계속 진행
filterChain.doFilter(servletRequest, servletResponse);
}
}
(4) build.gradle에서 해당 코드를 지운 후, 파일 우클릭 -> Gradle -> Refresh Gradle Project 클릭
- springdoc-openapi-starter-webmvc-ui:2.5.0은 내부적으로 swagger-annotations 최신 버전을 기대하는데,
swagger-annotations:2.2.15가 충돌을 일으켜서 springdoc이 기대하는 메서드가 없는 구버전 클래스가 로딩되어 java.lang.NoSuchMethodError: 'java.lang.Class[] io.swagger.v3.oas.annotations.Parameter.validationGroups()' 오류 발생
implementation 'io.swagger.core.v3:swagger-annotations:2.2.20' // Swagger(OpenAPI)의 어노테이션들을 직접 사용할 수 있게 해주는 라이브러리
(4) 정상 동작 시나리오 기반 테스트 수행 - http://localhost:8080/swagger-ui/index.html에 접속하여 진행
- 절차: 회원가입 성공 -> 회원가입 성공 계정 정보를 기반으로 로그인 성공 -> 로그인 성공으로 토큰 발급 -> 상품 컨트롤러의 상품 등록 API 호출 -> API 호출 시 로그인 과정에서 받은 토큰을 헤더에 추가해서 전달 -> 상품 등록 완료
1. 회원가입
- http://localhost:8080/swagger-ui/index.html#/sign-controller/signUp 부분에서 'Try it out' 클릭 후,
각 칸에 다음과 같이 입력하고 'Execute' 클릭

- 정상적인 회원가입 완료 화면 출력

2. 로그인
- http://localhost:8080/swagger-ui/index.html#/sign-controller/signIn에서 'Try it out' 클릭 후,
각 칸에 다음과 같이 입력하고 'Execute' 클릭

- 정상적인 로그인 성공 화면 + 발급된 jwt 토큰 값 출력

3. 상품 등록 및 조회
- SwaggerConfig 필요
package com.example.demo.config;
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType;
import io.swagger.v3.oas.annotations.info.Info;
import io.swagger.v3.oas.annotations.security.SecurityScheme;
@OpenAPIDefinition(info = @Info(title = "Demo API", version = "v1"))
@SecurityScheme(
name = "X-AUTH-TOKEN", // Swagger UI에서 표시될 이름
type = SecuritySchemeType.APIKEY, // API Key 방식
in = io.swagger.v3.oas.annotations.enums.SecuritySchemeIn.HEADER, // 헤더에서 받음
paramName = "X-AUTH-TOKEN" // 실제 헤더 이름
)
public class SwaggerConfig {
}
- 이전에 만들었던 ProductController를 살짝 수정하고, Product와 관련 DTO들은 그대로 사용
// ProductController.java
package com.example.demo.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import com.example.demo.jpa.data.dto.ChangeProductNameDTO;
import com.example.demo.jpa.data.dto.ProductDTO;
import com.example.demo.jpa.data.dto.ProductResponseDTO;
import com.example.demo.service.ProductService;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
/**
* 상품 관련 HTTP 요청을 처리하는 REST 컨트롤러
* 클라이언트로부터 요청을 받아 Service 계층에 전달하고,
* 처리 결과를 ResponseEntity로 감싸서 응답
*/
@RestController
@RequestMapping("/product") // 모든 요청의 기본 경로 설정
public class ProductController {
private final ProductService productService;
@Autowired
public ProductController(ProductService productService) {
this.productService = productService;
}
/**
* 상품 조회 API
* GET /product?number=1
* @param number 조회할 상품의 고유 번호 (쿼리 파라미터)
* @return 상품 정보를 담은 응답 DTO
*/
@GetMapping
public ResponseEntity<ProductResponseDTO> getProduct(@RequestParam("number") Long number) {
ProductResponseDTO productResponseDTO = productService.getProduct(number);
return ResponseEntity.status(HttpStatus.OK).body(productResponseDTO);
}
/**
* 상품 생성 API
* POST /product
* @param productDTO 클라이언트가 전달한 상품 정보 (JSON Body)
* @return 저장된 상품 정보를 담은 응답 DTO
*/
@PostMapping
// jwt 인증 필요
@SecurityRequirement(name = "X-AUTH-TOKEN")
public ResponseEntity<ProductResponseDTO> createProduct(@RequestBody ProductDTO productDTO) {
ProductResponseDTO productResponseDTO = productService.saveProduct(productDTO);
return ResponseEntity.status(HttpStatus.OK).body(productResponseDTO);
}
/**
* 상품 이름 변경 API
* PUT /product
* @param changeProductNameDTO 상품 번호와 새로운 이름을 담은 DTO (JSON Body)
* @return 변경된 상품 정보를 담은 응답 DTO
* @throws Exception 상품이 존재하지 않거나 변경 실패 시 예외 발생
*/
@PutMapping
// jwt 인증 필요
@SecurityRequirement(name = "X-AUTH-TOKEN")
public ResponseEntity<ProductResponseDTO> changeProductName(
@RequestBody ChangeProductNameDTO changeProductNameDTO) throws Exception {
ProductResponseDTO productResponseDTO = productService.changeProductName(
changeProductNameDTO.getNumber(), changeProductNameDTO.getName());
return ResponseEntity.status(HttpStatus.OK).body(productResponseDTO);
}
/**
* 상품 삭제 API
* DELETE /product?number=1
* @param number 삭제할 상품의 고유 번호 (쿼리 파라미터)
* @return 삭제 성공 메시지
* @throws Exception 상품이 존재하지 않거나 삭제 실패 시 예외 발생
*/
@DeleteMapping
// jwt 인증 필요
@SecurityRequirement(name = "X-AUTH-TOKEN")
public ResponseEntity<String> deleteProduct(@RequestParam("number") Long number) throws Exception {
productService.deleteProduct(number);
return ResponseEntity.status(HttpStatus.OK).body("정상적으로 삭제되었습니다.");
}
}
- http://localhost:8080/swagger-ui/index.html 상단에서 'Authorize' 클릭
-> 클릭하면 X-AUTH-TOKEN 입력창이 뜨고,
거기에 '로그인 성공 후 발급받은 token 값'을 입력하면 이후 모든 API 호출에 자동으로 헤더가 붙음

- http://localhost:8080/swagger-ui/index.html#/product에서,
Body에 { "name":"공책", "price":"2000", "stock":"3500"}을 넣으면 정상적으로 상품 등록 성공 화면이 출력됨 - 상품 등록이 정상 완료됐다면, 상품 조회 API에서 상품 조회 가능
(5) 비정상 동작 시나리오 기반 테스트 수행 시
- token 값을 변조하여 상품 등록 API를 호출하면, 인증 예외 발생으로 { "msg": "인증이 실패하였습니다" }가 출력됨
- 사용자의 role이 ADMIN이 아닐 경우(USER 권한으로 회원가입 및 로그인 진행),
인가 예외 발생으로 /exception으로 리다이렉트된 후 예외 메시지가 응답으로 돌아옴
'스프링(Spring), 스프링부트(SpringBoot) > 스프링부트(SpringBoot) 기초' 카테고리의 다른 글
| 스프링부트 핵심 가이드(장정우 지음) - 스프링 시큐리티(Spring Security)와 JWT 2편 (1) | 2026.04.16 |
|---|---|
| 스프링부트 핵심 가이드(장정우 지음) - 스프링 시큐리티(Spring Security)와 JWT 1편 (1) | 2026.04.10 |
| 스프링부트 핵심 가이드(장정우 지음) - 스프링 시큐리티(Spring Security) 개요 (0) | 2026.03.26 |
| 스프링부트 핵심 가이드(장정우 지음) - 서버 간 통신 2: WebClient (0) | 2026.03.18 |
| 스프링부트 핵심 가이드(장정우 지음) - 서버 간 통신 1: RestTemplate (1) | 2026.03.03 |