인증 파트를 맡고 싶은 이유는 이전 글 2026.05.15 - [BackEnd/프로그래머스 데브코스] - MSA 환경에서의 DB 모델링 참고
MSA 환경에서의 DB 모델링
이전 글 2026.05.14 - [BackEnd/프로그래머스 데브코스] - 세미 프로젝트 시작 참고 세미 프로젝트 시작약 2주의 온라인 강의를 진행한 후에 자유롭게 PR을 올려 자기 어필을 통해 팀을 구성하게 했고
memo4667.tistory.com
내가 기존에 했던 프로젝트에서는 인증/인가 파트가 가장 난이도 어려운 도메인에 속했다.
물론 인증/인가 로직 자체가 서비스마다 크게 다르지 않아서 공부를 제대로 하고 개발 경험이 있는 사람에게는 어려운 것이 아니지만
Spring Security가 생소한 사람에게는 매우 어려울 수 있고 주니어 개발자 중에서도 잘 모르는 사람이 꽤 있다고 알고 있다.
나는 개발에 들어가기에 앞서 공부를 충분히 하고 개발을 시작하는 방법을 추구한다.
AI가 너무 잘 되어있는 시대이긴 하지만 적어도 내가 맡은 파트에 있어서 개념을 꼼꼼하게 공부하고 명확한 설계 의도를 갖고
AI를 활용하는 것과 아닌 것의 차이는 어마무시하다고 생각한다.
이 과정을 스킵하고 AI에 의존해서 개발하는 것은 취준생에게 독이 되는 행위라고 생각한다.
그러나 우리에게 주어진 세미프로젝트 기간은 약 2주정도 밖에 되지 않았기 때문에 충분히 공부하고 들어가기엔
계획에 차질이 생길 수 있고 팀 프로젝트이기 때문에 최대한 빠르게 개념 공부를 마치고 개발을 시작하기로 결정했다.
[초기 설계]
MSA 프로젝트임에도 API Gateway는 세미 프로젝트 이후 최종 프로젝트에서 도입하기로 했다.
이유는 팀 회의에서 API Gateway 까지 도입해서 적용하기엔 시간이 부족하다고 판단했기 때문이다.
(내 개인 의견은 아니었다.)
당시에는 내가 인증 파트를 구현해 본적이 없었기에 이견 없이 동의했지만 최종 프로젝트까지 마친 지금 시점에서
생각해보면 팀원들의 판단이 옳았던 것 같다.
API Gateway 대신 common 모듈을 만들어서 이 모듈에서 인증 처리를 하고
각 서비스 모듈에서 서비스 특성에 맞게 인가 처리를 하기로 했다.
API Gateway를 사용하진 않지만 중앙 집중형 인증 방식을 가져가기 위함이었다.
(+ Redis 또한 최종 프로젝트에서 사용하기로 했다. 이유는 세미에서 Gateway를 도입하지 않은 이유와 같다.)
[JWT란 무엇일까?]
내가 개발을 시작하기 전에 공부한 내용들에 대해 정리해보려고 한다.
책도 있고 강의 자료도 갖고 있었지만 최대한 빠르게 개념 공부를 마치고 개발을 시작하기 위해 AI와 대화하는 방식을 선택했다.
AI는 ChatGPT, Gemini, Claude를 사용했다.
0. JWT(JSON Web Token)는 JSON 형식으로 정보를 담아 전달하는 토큰이다.
- 형태
xxxxx.yyyyy.zzzzz
점(.)으로 구분된 세 부분으로 구성된다.
Header: 토큰 타입(JWT)과 서명 알고리즘 정보
{
"alg": "HS256",
"typ": "JWT"
}
Payload: 실제 담을 데이터 (Claims)
{
"userId": "abc123",
"iat": 1716000000,
"exp": 1716003600
}
- Claims란 토큰 안에 담기는 데이터다.
사용자 식별 정보, 발급 시각(iat), 만료 시각(exp) 등을 담는다.
- Signature: Header + Payload를 서버의 secret key로 서명한 값
서명이 핵심이다.
클라이언트가 토큰을 변조하면 서명 검증에서 실패하기 때문에 서버는 변조 여부를 감지할 수 있다.
중요한 점은 Payload는 암호화된 게 아니라 Base64로 인코딩된 것이다.
누구나 디코딩해서 내용을 볼 수 있기 때문에 비밀번호 같은 민감한 정보는 절대 담으면 안 된다.
1. 인증(Authenntication) VS 인가(Authorization)
- 인증: “너 누구야?” → 로그인
- 인가: “이거 해도 돼?” → 권한 체크
2. JWT기반 인증 흐름(Redis 없이)
- stateless 구조란 서버가 상태를 기억하지 않는 것이다.
즉, 요청 하나하나가 독립적이며 이전 요청의 정보를 서버가 저장하지 않는다. - stateless(JWT 방식)는
"로그인 → 토큰 발급 → 이후 요청마다 토큰 포함 → 서버는 토큰만 보고 판단"
흐름으로 가기 때문에 서버는 기억하지 않는다. - stateless에서는 '토큰 만료 = 로그인 만료' 이기 때문에 서버가 연장해주지 않는다.
그래서 생긴 개념이 'refresh token' 이다.
3. Spring Security의 본질
Spring security는 사실 하나다 -> "Filter Chain"
동작 구조
"요청 -> Security Filter Chain -> Controller"
여기서 우리가 하는 모든 작업은
- 필터 추가
- 인증 객체 생성
- 권한 검사
4. 핵심 구성요소
4-1. SecurityFilterChain
Spring Security의 시작점
@Bean
public SecurityFilterChain filterChain(HttpSecurity http)
여기서 하는 것
- 어떤 URL 허용할지
- 어떤 필터 넣을지
- 세션 사용할지 말지
FilterChain
- 인증 처리(JWT 검증)
- 일부 인가 설정
→ 모든 요청에서 실행된다
4-2. Authentication(인증 객체)
이 안에는
- 사용자 정보
- 권한
이 담긴다.
Authentication
- principal(유저 정보)
- authorities(권한)
- authenticated(인증 여부)
4-3. SecurityContext
SecurityContextHolder.getContext().getAuthentication()
→ 현재 로그인된 사용자 정보를 꺼낼 수 있는 장소다.
Authentication → "인증 정보 그 자체" (주민등록증)
SecurityContext → "인증 정보를 담는 그릇" (지갑)
왜 이렇게 나눠놨을까?
Spring은 요청 하나를 Thread 하나에서 처리한다.
SecurityContextHolder는 내부적으로 ThreadLocal을 사용해서 요청별로 인증 정보를 독립적으로 관리한다.
덕분에 여러 사용자의 요청이 동시에 들어와도 서로의 인증 정보가 섞이지 않는다.
4.4 UserDetails / UserDetailsService
Spring Security가 사용자 정보를 다루기 위한 인터페이스다.
- UserDetails: Spring Security가 인식하는 사용자 정보 모델
- UserDetailsService: DB에서 사용자를 조회하는 역할
직접 구현할 때는 우리가 만든 User 엔티티를 UserDetails로 감싸는
CustomUserDetails 클래스를 만들고,
loadUserByUsername()에서 DB를 조회해서 그 객체를 반환하도록 구현한다.
UserDetailsService.loadUserByUsername()
→ DB에서 사용자 조회하는 역할
4.5 PasswordEncoder
비밀번호를 안전하게 저장하기 위한 컴포넌트다.
- BCryptPasswordEncoder
BCrypt는 단방향 해시 알고리즘으로 한 번 암호화하면 복호화가 불가능하다.
로그인 시에는 passwordEncoder.matches()로 입력값과 저장된 해시값을 비교한다.
5. JWT 기반으로 바뀌면 뭐가 달라지냐
기존 세션 방식:
서버가 로그인 상태를 기억 → 요청마다 세션 조회
JWT 방식:
서버는 기억 안 함 → 요청마다 토큰 검증으로 대체
그래서 JWT 방식에서는 Spring Security가 자동으로 해주던 것들을 직접 구현해야 한다.
핵심 3가지
1. JWT 생성: 로그인 시 Access Token, Refresh Token 발급
2. JWT 검증 필터: 요청마다 실행, 토큰 유효성 확인
3. 인증 객체 생성: 토큰 → Authentication 변환 → SecurityContext 저장
6. JWT 필터 구조 (핵심)
요청
→ JWT 필터
→ 토큰 꺼냄
→ 유효성 검사
→ 사용자 정보 추출
→ Authentication 생성
→ SecurityContext 저장
→ Controller
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
// 1. 헤더에서 토큰 꺼내기 (Authorization: Bearer xxx)
String token = resolveToken(request);
// 2. 토큰 없으면 그냥 통과 (인증 불필요한 요청 고려)
if (token == null) {
filterChain.doFilter(request, response);
return;
}
// 3. 토큰 검증
if (jwtProvider.validateToken(token)) {
// 4. 사용자 정보 추출
String username = jwtProvider.getUsername(token);
// 5. DB 조회
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
// 6. Authentication 생성
Authentication authentication =
new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities()
);
// 7. SecurityContext에 저장 → 이제부터 로그인된 상태
SecurityContextHolder.getContext().setAuthentication(authentication);
}
// 8. 다음 필터로 이동 (빠뜨리면 요청이 여기서 끊긴다)
filterChain.doFilter(request, response);
}
}
여기서 반드시 이해해야 할 것 3가지
1. Authentication은 우리가 직접 만든다
JWT에서는 Spring이 자동으로 생성해주지 않는다.
2. SecurityContext에 저장해야 인증된 상태다
저장하지 않으면 Controller는 인증되지 않은 요청으로 본다.
3. filterChain.doFilter()를 반드시 호출해야 한다
빠뜨리면 요청 처리가 여기서 끊긴다.
이 필터는 아래와 같이 등록한다.
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
7. Redis 없이 갈 때 - 로그아웃 처리 문제
JWT stateless 구조의 가장 큰 약점이다.
발급된 토큰은 만료 전까지 서버가 강제로 무효화할 수 없다.
서버가 상태를 기억하지 않기 때문이다.
해결 방법
1. Access Token 유효시간을 짧게 (30분 ~ 1시간)
2. Refresh Token은 DB에 저장하고 로그아웃 시 null 처리
로그아웃을 하면 DB의 Refresh Token을 지운다.
만료된 Access Token으로 재발급을 시도할 때 DB에 Refresh Token이 없으면 차단한다.
이렇게 하면 완벽하진 않지만 (Access Token이 남아있는 짧은 시간은 여전히 유효)
실용적인 수준에서 로그아웃을 처리할 수 있다.
이렇게 개념 공부를 마치고 실제 구현에 들어갔다.
다음 글에서는 실제 구현 과정에서 초기에 공부한 내용과 달라진 부분들,
특히 UserDetails를 사용하지 않은 이유와 common 모듈 설계 결정을 다루겠다.
'BackEnd > 프로그래머스 데브코스' 카테고리의 다른 글
| 컨벤션 설정 (2) | 2026.05.19 |
|---|---|
| MSA 환경에서의 DB 모델링 (0) | 2026.05.15 |
| 세미 프로젝트 시작 (0) | 2026.05.14 |
| 프로그래머스 데브코스 단기 심화 시작 (0) | 2026.05.08 |