MSA 공부

인증/인가, Spring Boot 서버 구축

5_hyun 2025. 7. 5. 19:58
반응형

드디어 첫 번째 프로젝트인 Spring Boot로 구현된 JWT 기반 인증 서버 프로젝트를 정리해 보겠다.

[프로젝트 GitHub 링크] https://github.com/5hyun/AuthServer-SpringBoot

 

GitHub - 5hyun/AuthServer-SpringBoot

Contribute to 5hyun/AuthServer-SpringBoot development by creating an account on GitHub.

github.com


📝 프로젝트 개요

  • 주요 기능: 사용자 회원가입 및 로그인, JWT(JSON Web Token)를 사용한 인증, Access/Refresh Token을 이용한 토큰 갱신, Redis를 활용한 로그아웃 및 토큰 관리
  • 핵심 기술: Spring Security를 통해 보안을 강화하고, JPA로 데이터를 관리하며, Redis로 서버의 상태를 효율적으로 다루는 구조

📂 폴더 및 파일별 상세 역할

📍 src/main/java/com/example/AuthServer

Java 소스 코드가 위치하는 메인 디렉토리로, 기능별로 패키지가 잘 분리되어 있다.

  • 🚀 AuthServerApplication.java
    • 역할: Spring Boot 애플리케이션의 시작점
    • 상세 설명: @SpringBootApplication 어노테이션이 핵심. main 메소드에서 SpringApplication.run()을 호출하여 내장 웹 서버(Tomcat)를 실행하고 애플리케이션을 구동한다. @OpenAPIDefinition은 Swagger API 문서의 기본 정보를 설정하는 역할을 한다.
  • ⚙️ config 패키지 애플리케이션의 주요 설정을 담당하는 클래스들이 모여있다.
    • SecurityConfig.java
      • 역할: Spring Security 관련 설정을 총괄
      • 상세 설명: @EnableWebSecurity로 Spring Security를 활성화하고, passwordEncoder()로 비밀번호를 암호화한다. securityFilterChain()에서는 HTTP 요청에 대한 보안 규칙(CSRF 비활성화, Stateless 세션 관리, URL별 접근 권한 설정)과 JwtAuthenticationFilter를 등록하여 JWT 토큰을 먼저 검사하도록 설정한다.
    • RedisConfig.java
      • 역할: Redis 데이터베이스 연결 및 사용 설정을 담당
      • 상세 설명: application.yml의 정보를 바탕으로 Redis 연결을 설정하고, RedisTemplate을 Bean으로 등록하여 서비스 로직에서 쉽게 Redis를 사용하도록 지원한다.
    • jwt 하위 패키지
      • JwtTokenProvider.java: JWT 생성, 검증, 정보 추출 등 JWT 관련 모든 핵심 로직을 담당하는 유틸리티 클래스이다.
      • JwtAuthenticationFilter.java: 클라이언트의 모든 요청에 대해 JWT 토큰을 검사하는 커스텀 필터. 요청 헤더에서 토큰을 추출해 유효성을 검증하고, 유효한 경우 SecurityContextHolder에 인증 정보를 저장한다.
  • 🎮 controller 패키지 애플리케이션의 API 엔드포인트를 정의하는 컨트롤러가 위치한다.
    • UserController.java
      • 역할: 사용자 관련 HTTP 요청을 받아 처리하는 API 엔드포인트를 정의한다.
      • 상세 설명: @RestController를 통해 모든 메소드가 JSON 데이터를 반환한다. 회원가입, 로그인, 로그아웃, 토큰 갱신 등의 API가 정의되어 있으며, 클라이언트의 요청을 받아 UserService로 비즈니스 로직 처리를 위임한다.
  • 📦 domain 패키지 데이터베이스 테이블과 매핑되는 JPA 엔티티(Entity) 클래스가 위치한다.
    • User.java
      • 역할: 데이터베이스의 users 테이블과 매핑되는 JPA 엔티티 클래스.
      • 상세 설명: @Entity 어노테이션으로 JPA 엔티티임을 나타낸다. 사용자의 정보를 필드로 가지며, Spring Security의 UserDetails 인터페이스를 구현하여 User 객체 자체를 Spring Security에서 인증 주체로 사용할 수 있게 한다.
  • 🚚 dto (Data Transfer Object) 패키지 계층 간 데이터 전송을 위해 사용되는 객체들을 정의한다.
    • SignUpRequest.java: 회원가입 요청 데이터를 담으며 유효성 검사 어노테이션이 포함되어 있다.
    • LoginRequest.java: 로그인 요청 데이터를 담는다.
    • TokenInfo.java: 로그인 성공 시 발급되는 Access/Refresh Token 정보를 담아 클라이언트에 전달한다.
    • ErrorResponse.java: 예외 발생 시 일관된 형식의 에러 메시지를 전달하기 위해 사용된다.
  • 🚨 exception 패키지 애플리케이션 전역의 예외를 처리하는 클래스가 위치한다.
    • GlobalExceptionHandler.java
      • 역할: 애플리케이션 전역에서 발생하는 예외를 중앙에서 처리한다.
      • 상세 설명: @RestControllerAdvice를 통해 모든 컨트롤러의 예외를 감지하고, @ExceptionHandler를 사용하여 특정 예외 타입별로 적절한 HTTP 상태 코드와 에러 메시지를 생성하여 응답한다.
  • 🗄️ repository 패키지 데이터베이스 작업을 처리하는 JPA 리포지터리 인터페이스가 위치한다.
    • UserRepository.java
      • 역할: User 엔티티에 대한 데이터베이스 작업을 처리하는 인터페이스.
      • 상세 설명: JpaRepository<User, Long>를 상속받아 기본적인 CRUD 메소드가 자동으로 생성된다. findByEmail(String email)처럼 정해진 규칙에 따라 메소드를 선언하면 Spring Data JPA가 쿼리를 자동으로 생성해준다.
  • 🧑‍🔧 service 패키지 애플리케이션의 핵심 비즈니스 로직을 구현한다.
    • UserService.java
      • 역할: 사용자 관련 핵심 비즈니스 로직을 처리한다.
      • 상세 설명: 회원가입(비밀번호 암호화, 중복 체크), 로그인(인증 및 토큰 발급), 로그아웃(토큰 블랙리스트 처리), 토큰 갱신 로직이 구현되어 있다.
    • CustomUserDetailsService.java
      • 역할: Spring Security의 UserDetailsService를 구현하여, 사용자 인증 시 필요한 정보를 DB에서 조회하는 역할을 한다.

📍 src/main/resources

  • application.yml
    • 역할: 애플리케이션의 주요 외부 설정을 담당한다.
    • 상세 설명: Redis 서버 주소, 로깅 레벨 등 애플리케이션 동작에 필요한 다양한 설정값들을 정의한다. (DB 정보, JWT secret key 등은 보통 별도 파일로 분리하여 관리한다.)

📍 기타 최상위 파일

  • docker-compose.yml: Docker를 사용하여 MySQL과 Redis를 컨테이너 환경으로 손쉽게 구성하고 실행하기 위한 설정 파일.
  • README.md: 프로젝트에 대한 전반적인 소개, 기술 스택 등을 담고 있는 프로젝트 설명서.
  • .gitignore: Git 버전 관리에서 추적하지 않을 파일 및 폴더 목록을 정의. 빌드 결과물이나 민감 정보가 원격 저장소에 올라가지 않도록 방지한다.

🌊 AuthServer-SpringBoot 프로젝트 흐름도

이 흐름도는 사용자가 로그인을 요청했을 때, 서버 내부에서 어떤 일들이 순서대로 일어나는지 보여줍니다.

🔐 사용자 로그인 (POST /api/users/login) 요청 흐름

1. 클라이언트 (Client) 사용자가 아이디(이메일)와 비밀번호를 입력하고 '로그인' 버튼을 클릭합니다. 클라이언트는 서버의 /api/users/login 엔드포인트로 POST 요청을 보냅니다. (Request Body에 LoginRequest DTO 형식으로 이메일, 비밀번호 포함)

 

2. DispatcherServlet & 필터(Filter) 체인 Spring의 DispatcherServlet이 가장 먼저 요청을 받습니다. SecurityConfig에 등록된 필터 체인을 통과합니다.

  • JwtAuthenticationFilter (config/jwt 패키지): /api/users/login 경로는 인증 없이 접근 가능(permitAll())하도록 SecurityConfig에 설정되어 있으므로, 이 단계에서는 특별한 토큰 검증 없이 다음 단계로 요청을 전달합니다.

3. 컨트롤러 (Controller)

  • UserController (controller 패키지) @PostMapping("/login") 어노테이션이 붙은 login() 메소드가 요청을 처리합니다. @RequestBody를 통해 받은 JSON 데이터를 LoginRequest DTO 객체로 변환합니다. 이 DTO 객체를 UserService의 login() 메소드로 전달하여 실제 비즈니스 로직 처리를 위임합니다.

4. 서비스 (Service)

  • UserService (service 패키지) login(email, password) 메소드가 호출됩니다.
    • 1단계: 인증용 객체 생성 전달받은 이메일과 비밀번호로 UsernamePasswordAuthenticationToken을 생성합니다. (아직 인증되지 않은 상태의 토큰)
    • 2단계: 실제 인증 위임 AuthenticationManager의 authenticate() 메소드에 위에서 생성한 토큰을 넘겨 인증을 시도합니다.

5. Spring Security 인증 과정

  • AuthenticationManager (SecurityConfig에서 Bean으로 등록) 인증 처리를 위해 등록된 AuthenticationProvider를 호출합니다. 여기서는 CustomUserDetailsService를 사용하도록 설정되어 있습니다. AuthenticationManager는 CustomUserDetailsService에게 사용자 정보를 찾아오라고 요청합니다.
  • CustomUserDetailsService (service 패키지) loadUserByUsername(email) 메소드가 실행됩니다. UserRepository를 사용하여 데이터베이스에서 해당 이메일을 가진 사용자를 조회합니다.
  • UserRepository (repository 패키지) findByEmail(email) 메소드를 실행하여 DB에서 User 정보를 조회합니다. 조회된 User 엔티티 객체를 CustomUserDetailsService에게 반환합니다. (사용자가 없으면 UsernameNotFoundException 발생)
  • CustomUserDetailsService → AuthenticationManager DB에서 조회한 User 객체(UserDetails 타입)를 AuthenticationManager에게 반환합니다. AuthenticationManager는 UserService로부터 받은 요청 비밀번호와 DB에서 조회한 User의 암호화된 비밀번호를 PasswordEncoder를 통해 비교하여 인증을 최종 완료합니다. (비밀번호가 틀리면 AuthenticationException 발생) 인증이 성공하면, 사용자 정보와 권한이 담긴 인증된 Authentication 객체를 생성하여 UserService에게 돌려줍니다.

6. 서비스 (Service) - 후처리

  • UserService (service 패키지)
    • 3단계: JWT 토큰 생성 AuthenticationManager로부터 성공적으로 인증된 Authentication 객체를 받습니다. 이 객체를 **JwtTokenProvider**에게 전달하여 Access TokenRefresh Token을 생성합니다.
    • 4단계: Refresh Token 저장 생성된 Refresh Token을 Redis에 저장합니다. (Key: RT:user@email.com, Value: refreshToken) RedisTemplate을 사용합니다.
    • 5단계: 최종 결과 반환 TokenInfo DTO에 두 토큰 정보를 담아 UserController에게 반환합니다.

7. 컨트롤러 (Controller) → 클라이언트 (Client)

  • UserController (controller 패키지) UserService로부터 받은 TokenInfo 객체를 ResponseEntity에 담아 JSON 형태로 클라이언트에게 응답합니다. (HTTP Status 200 OK)

8. 클라이언트 (Client) 서버로부터 받은 Access Token과 Refresh Token을 저장하고, 이후 API 요청 시 Authorization 헤더에 Access Token을 담아 보냅니다.


[개발 용어] 헷갈리는 API, 엔드포인트, URL, URI 완벽 정리 🗺️

1. API (Application Programming Interface) → 📜 레스토랑의 '메뉴판'과 '주문 시스템 규칙'

  • 의미: 프로그램(손님)이 다른 프로그램(레스토랑)의 기능과 데이터를 사용하기 위한 모든 규칙과 약속의 총집합입니다.
  • 비유: 레스토랑의 메뉴판과 같습니다. 메뉴판에는 주문 가능한 음식(기능) 목록, 음식 설명, 가격(필요한 데이터), 그리고 "주문은 키오스크에서 해주세요"와 같은 주문 규칙까지 모두 명시되어 있죠. 손님(클라이언트)은 이 **정해진 메뉴와 규칙(API)**에 따라서만 음식을 주문(요청)할 수 있습니다.

2. 엔드포인트 (Endpoint) → 👆 메뉴판의 '김치찌개' 항목과 '주문 창구'

  • 의미: API라는 메뉴판에 명시된 특정 기능을 실행할 수 있는 구체적인 접점 또는 창구입니다.
  • 비유: 메뉴판에서 '김치찌개(8,000원)' 항목을 손가락으로 가리키는 행위가 바로 엔드포인트와 연결되는 과정입니다. 이 메뉴 항목을 통해 우리는 '김치찌개'라는 특정 기능을 요청하게 되고, 이 요청은 주방의 '주문 접수 창구' 로 전달됩니다. 이 창구가 바로 기능을 실행하는 최종 지점이죠.
  • 코드 예시: POST /api/users/login 은 '로그인'이라는 특정 메뉴를 주문하는 명확한 창구(엔드포인트)입니다.

3. URL (Uniform Resource Locator) → 📍 레스토랑까지 가는 '상세 주소'

  • 의미: 엔드포인트(주문 창구)가 있는 레스토랑까지 찾아갈 수 있는, '위치'가 명시된 완전한 경로입니다.
  • 비유: "배달의민족"으로 음식을 주문한다고 생각해 보세요. "서울특별시 강남구 테헤란로 123, A 타워 1층 '개발자 맛집'" 이라는 정확한 주소가 있어야 배달 기사님이 음식을 픽업하러 갈 수 있습니다. 이처럼 프로토콜(https://), 도메인(A 타워), 상세 경로(1층...)가 모두 포함된 이 '상세 주소'가 바로 URL입니다.

4. URI (Uniform Resource Identifier) → 🆔 레스토랑의 '사업자등록번호'

  • 의미: 인터넷의 모든 자원을 고유하게 식별하는 모든 종류의 정보. 가장 포괄적인 개념입니다.
  • 비유: 레스토랑의 '사업자등록번호' 와 같습니다. 이 번호는 전국의 수많은 식당 중에서 이 식당 하나를 유일하게 식별해 주지만, 이 번호만 보고 식당 위치를 찾아갈 수는 없습니다.
  • 관계: 레스토랑의 '상세 주소(URL)'는 당연히 레스토랑을 식별하는 정보이므로 '사업자등록번호(URI)'의 역할을 겸할 수 있습니다. 따라서, 모든 URL은 URI에 포함됩니다. 하지만 위치 정보가 없는 순수한 식별 정보(예: urn:isbn:1234 같은 책 고유번호)는 URL이 될 수 없습니다.
반응형