플레이데이터 풀스택 백엔드 9기 16주차 주간회고 및 학습기록 (열여섯번째 기록)
이번 주에는 Jenkins 공부와 함께 4차 프로젝트 백을 구현했다. 이번 주에 공부하면서 어려웠던 것과 프로젝트 백단을 구현하며 복습한 부분을 정리해보고자 한다.
1. Spring Security 기반 JWT 인증 흐름 및 구성요소 설명
4차 프로젝트에서 로그인, 회원가입 등 user-service를 맡았다. Jwt 방식으로 인증 로직을 구현하기로 했기 때문에 필요한 클래스들의 역할과 기능을 정리했다.
[요청]
|
|---▶ [JwtAuthenticationFilter]
| └─ HTTP 요청 헤더에서 JWT 추출
| └─ JwtTokenProvider로 토큰 검증 & 정보 추출
| └─ 사용자 인증 정보를 SecurityContext 에 등록
|
▼
[Spring Security]
|
|---▶ [SecurityConfig]
| └─ JWT 필터 등록
| └─ 경로별 인증/인가 정책 설정
| └─ CustomAuthenticationEntryPoint 설정 (401 응답 커스터마이징)
|
▼
[JwtTokenProvider]
└─ JWT 발급, 서명, 파싱, 만료 검증
└─ 사용자 ID, 권한, 기타 Claim 삽입
|
▲
└─ [JwtConfig]
└─ application.yml의 JWT 설정을 주입받는 클래스
└─ (secret, expiration, header, prefix 등)
|
▼
[CustomAuthenticationEntryPoint]
└─ 인증 실패 시 401 응답을 커스터마이징
|
▼
[GlobalExceptionHandler]
└─ 예외를 @ControllerAdvice로 전역 처리
└─ (토큰 만료, 파싱 에러 등 공통 예외 처리)
|
▼
[CorsConfig]
└─ 프론트엔드에서 백엔드 접근 허용 설정
|
▼
[WebMvcConfig]
└─ 정적 자원 핸들링, 인터셉터 설정 등 (MVC 관련 커스터마이징)
JwtAuthenticationFilter (JWT 인증 로직의 시작점)
- HTTP 요청 헤더에서 JWT 추출 : Authorization 헤더에서 Bearer와 같은 prefix를 제거한 후 jwt를 가져옴
- JwtTokenProvider를 통해 토큰 검증
- 토큰에서 사용자 정보 추출 (userId, role 등이 들어가며, password 같은 개인정보는 들어가지 않게 함)
- 인증 성공 시 : UsernamePasswordAuthenticationToken을 생성 >> SecurityContextHolder에 등록하여 Spring Security가 인식하게 함
SecurityConfig (Spring Security의 핵심 설정 파일)
- JwrAuthenticationFilter 등록 : 요청마다 JWT 인증이 수행되도록 설정
- 경로별 인증/인가 정책 설정 : /auth/**는 permitAll(), /admin/**은 ROLE_ADMIN 필요 등
- CustomAuthenticationEntryPoint 등록 : 인증 실패 시의 응답 커스터마이
JwtTokenProvider (JWT 생성 및 검증 핵심 유틸리티 클래스)
- Jwt 생성 : 로그인 시 사용자 정보를 담은 jwt 발급
- Jwt 검증 : 요청 시 JWT의 서명, 만료 여부, 형식 등 검증
- Claims 추출 : userId, Role 등 custom claim 추출
JwtConfig (JWT 관련 설정을 외부 파일에서 관리)
- application.yml(application.properties)에 정의된 jwt 설정들을 주입받음
CustomAuthenticationEntryPoint (인증 실패 처리 커스터마이징)
- 인증이 되지 않은 상태에서 보호된 리소스에 접근했을 때 처리 담당
GlobalExceptionHandler (전역 예외 처리)
- 토큰 만료, 인증 실패 등의 공통 예외를 한곳에서 처리
2. JWT 서명에 사용할 비밀키(Secret Key)를 VM options로 관리하기
JWT 도입 초기에 실행하는 1회용 클래스로, 여기서 만들어진 Secret Key를 설정에 넣었다.
보안상 조심해야 하므로 이 클래스를 git에 남겨두거나, 실행결과를 push하면 안된다.
package olive.oliveyoung.config.jwt;
import java.util.Base64;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
/* jwt 비밀키를 생성하기 위한 일회용 클래스 */
public class JwtSecretGenerator {
public static void main(String[] args) {
try {
// HMAC-SHA256 키 생성
KeyGenerator keyGen = KeyGenerator.getInstance("HmacSha256");
keyGen.init(256); // 256비트 키
SecretKey secretKey = keyGen.generateKey();
// Base64 인코딩된 키 문자열 출력
String base64Key = Base64.getEncoder().encodeToString(secretKey.getEncoded());
System.out.println("Generated HMAC Secret Key (Base64):");
System.out.println(base64Key);
} catch (Exception e) {
System.err.println("Error generating secret key: " + e.getMessage());
}
}
}
- KeyGenerator.getInstance("HmacSha256") : HMAC-SHA256 알고리즘을 기반으로 하는 비밀키 생성기 생성
- keyGen.init(256) : 키 길이를 256 비트로 설정
- keyGen.generateKey() : 실제 SecretKey 객체 생성
- Base64.getEncoder().encodeToString(secretKey.getEncoded()) : 생성된 키를 Base64 문자열로 인코딩

이렇게 만들어진 비밀키는 아래와 같은 형식으로 인텔리제이 VM options에 넣어 (edit configurations > modify options) 시스템 속성을 설정하였고, 팀원들의 인텔리제이에도 같은 키를 넣어 jwt 인증 방식을 구현할 수 있도록 하였다.
-DJWT_SECRET_KEY=**************************
application.yml 파일에는 다음과 같이 입력해주었다.
jwt:
secret: ${JWT_SECRET_KEY}
expiration: 3600000 # 1시간 (밀리초)
refresh-expiration: 1209600000 # 14일 (밀리초)
header: Authorization
prefix: Bearer
인텔리제이를 안 쓴다면?
1. 터미널, CLI에서 환경변수 설정 후 실행
# 리눅스
export JWT_SECRET=your-secret-key
java -jar your-app.jar
# 윈도우 CMD
set JWT_SECRET=your-secret-key
java -jar your-app.jar
# 윈도우 PowerShell
$env:JWT_SECRET="your-secret-key"
java -jar your-app.jar
2. AWS EC2, Heroku, Github Actions 등 배포 플랫폼
이건 각 플랫폼에 따라 환경변수 설정 방식이 다르다. 최종프로젝트에서는 지금처럼 시스템 속성 설정이 아닌, 환경변수를 설정하거나 배포 플랫폼을 이용할 예정이다.
3. ArgoCD와 GitHub Actions를 활용한 컨테이너 배포 자동화

4. UserSignUpRequestDto의 createdAt 필드 설정
회원가입 시간을 나타내는 createAt 필드는 회원가입 요청 Dto에 따로 두는 게 좋지 않다고 한다. 악의적인 사용자가 가입시간을 조작해서 서버로 보낼 수 있기 때문이라고 한다.
먼저, @EnableJpaAuditing 어노테이션을 추가하여 프로젝트 전체의 Auditing 기능을 활성화한다.

이후 공통 시간 엔티티를 생성하기 위해 BaseEntity를 작성한다. updatedAt도 추가할 수 있으나, 일단 createdAt만 두었다.
@createdDate는 JPA Auditing 기능 중 하나로, 엔티티가 처음 저장될 때의 시간 (생성일)을 자동으로 기록한다.

이후 User 엔티티에서는 이를 상속받도록 한다.

이렇게 하면 사용자는 아이디, 이메일, 비밀번호, 이름 등만 UserSignUpRequestDto에 담아서 보내게 된다.
UserService에서는 이 Dto를 받아 User 엔티티를 생성하게 되는데, userRepository에서 save가 호출되는 순간 JPA Auditing 기능이 작동하여 createAt 필드에 현재 서버 시간이 자동으로 기록된다. 이처럼 회원가입 시간을 백엔드에서 직접 기록하여 클라이언트 측에서 생성 시간을 조작하는 것을 방지할 수 있다.
이번 주면 플레이데이터에서의 진도가 모두 끝난다. 이번 주를 잘 보내야한다. 4차 프로젝트를 제때에 끝내고 최종 프로젝트에 바로 들어갈 수 있어야 한다. 앞으로 부트캠프에서의 8시간 동안은 죽어라 프로젝트 개발에 매진하고, 집에서의 6시간은 데이터 공부에 몰두하는 시간이 될 것이다.
'PLAYDATA 주간회고' 카테고리의 다른 글
| 플레이데이터 풀스택 백엔드 9기 7월 3주차 회고 (2) | 2025.07.28 |
|---|---|
| 플레이데이터 풀스택 백엔드 9기 7월 2주차 회고 (1) | 2025.07.07 |
| 플레이데이터 풀스택 백엔드 9기 6월 4주차 회고 (1) | 2025.07.01 |
| 플레이데이터 풀스택 백엔드 9기 6월 3주차 회고 (0) | 2025.06.23 |
| 플레이데이터 풀스택 백엔드 9기 6월 2주차 회고 (2) | 2025.06.17 |