스프링(Spring), 스프링부트(SpringBoot)/스프링부트(SpringBoot) 기초

스프링부트 핵심 가이드(장정우 지음) - 스프링 시큐리티(Spring Security)와 JWT 2편

개발학생 2026. 4. 16. 17:37
반응형

*** 함께 보면 좋은 글

2026.04.10 - [스프링(Spring), 스프링부트(SpringBoot)/스프링부트(SpringBoot) 기초] - 스프링부트 핵심 가이드(장정우 지음) - 스프링 시큐리티(Spring Security)와 JWT 1편

 

스프링부트 핵심 가이드(장정우 지음) - 스프링 시큐리티(Spring Security)와 JWT 1편

*책 내용과 다르게, 다음과 같은 환경에서 프로젝트 생성 Windows11(윈도우 11) 환경자바 JDK 17 버전 설치 https://yungenie.tistory.com/11 [Java] 차근차근 Java 설치하기 (JDK17, Window 11)자바 개발 도구 설치 방법

keep-programming-study.tistory.com

 

2. 스프링 시큐리티와 JWT 적용

6) 커스텀 AccessDeniedHandler, AuthenticationEntryPoint 구현

(1) CustomAccessDeniedHandler

package com.example.demo.config.security;

import java.io.IOException;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

/**
 * Spring Security에서 접근 권한이 없는 요청(AccessDeniedException 발생 시)을 처리하는 핸들러 클래스
 *
 * 주요 역할:
 * 1. 사용자가 인증은 되었지만, 특정 리소스에 접근할 권한이 없을 때 실행됨
 *    - 예: ROLE_USER 권한만 있는 사용자가 ROLE_ADMIN 전용 API에 접근
 * 2. AccessDeniedHandler 인터페이스 구현 → handle() 메서드 오버라이드
 * 3. 로그 기록: 접근 거부 상황을 로깅
 * 4. response.sendRedirect("/지정된 경로/") 호출: 접근 거부 시 해당 경로로 리다이렉트
 *
 * 특징:
 * - @Component로 등록되어 Spring Security 설정에서 자동으로 사용 가능
 * - "인증은 되었지만 권한이 부족한 경우"에만 동작
 */
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
    
	// Logger 인스턴스 생성
	// - SLF4J LoggerFactory를 사용하여 현재 클래스(CustomAccessDeniedHandler)에 대한 로깅 객체를 생성
	// - 접근 거부 상황 발생 시 로그를 남겨서 추적 및 디버깅에 활용 가능
	private final Logger LOGGER = LoggerFactory.getLogger(CustomAccessDeniedHandler.class);

	@Override
	public void handle(
	        HttpServletRequest request, 
	        HttpServletResponse response, 
	        AccessDeniedException exception) throws IOException {
	    
	    // 접근 거부 상황 발생 시 로그 기록
	    // - 사용자가 인증은 되었지만 권한이 부족하여 요청이 차단된 경우 실행됨
	    LOGGER.info("[handle] 접근이 막혔을 때 경로 리다이렉트");
	    
	    // 접근 거부 시 지정된 경로로 리다이렉트
	    // - "/sign-api/exception" 경로로 이동시켜 사용자에게 권한 부족 안내 페이지를 제공할 수 있음
	    // - response.sendRedirect()는 클라이언트 브라우저가 해당 URL로 다시 요청하도록 지시하는 방식
	    response.sendRedirect("/sign-api/exception");
	}

}

(2) CustomAuthenticationEntryPoint

package com.example.demo.config.security;

import java.io.IOException;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import com.fasterxml.jackson.databind.ObjectMapper;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

/**
 * 인증 실패(401 Unauthorized) 상황을 처리하는 커스텀 EntryPoint 클래스
 *
 * 주요 역할:
 * - Spring Security에서 인증되지 않은 사용자가 보호된 리소스에 접근할 때 실행됨
 * - AuthenticationEntryPoint 인터페이스 구현 → commence() 메서드 오버라이드
 * - 로그 기록: 인증 실패 상황을 로깅
 * - 클라이언트에게 JSON 형태의 에러 응답 반환
 *
 * 특징:
 * - @Component로 등록되어 Spring Security 설정에서 자동으로 사용 가능
 * - "인증 자체가 안 된 경우"에만 동작
 */
@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
    
    // Logger 인스턴스 생성
    // - SLF4J LoggerFactory를 사용하여 현재 클래스(CustomAuthenticationEntryPoint)에 대한 로깅 객체를 생성
    // - 접근 거부 상황 발생 시 로그를 남겨서 추적 및 디버깅에 활용 가능
    private final Logger LOGGER = LoggerFactory.getLogger(CustomAuthenticationEntryPoint.class);

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response,
            AuthenticationException authException) throws IOException {
        
        // ObjectMapper 생성
        // - Java 객체를 JSON 문자열로 변환하기 위해 사용
        ObjectMapper objectMapper = new ObjectMapper();
        
        // 인증 실패 로그 기록
        // - 인증되지 않은 사용자가 보호된 리소스에 접근하려 할 때 실행됨
        LOGGER.info("[commence] 인증 실패로 response.sendError 발생");
        
        // 사용자에게 반환할 에러 응답 객체 생성
        // - EntryPointErrorResponse는 커스텀 응답 DTO로, 메시지를 담아 클라이언트에 전달
        EntryPointErrorResponse entryPointErrorResponse = new EntryPointErrorResponse();
        entryPointErrorResponse.setMsg("인증에 실패하였습니다.");
        
        // HTTP 응답 설정
        // - 상태 코드: 401 Unauthorized (인증 실패)
        // - Content-Type: application/json (JSON 응답)
        // - CharacterEncoding: UTF-8 (한글 깨짐 방지)
        
        // response.sendError(response.SC_UNAUTHORIZED)와 같은 형식으로 인증 실패 코드만 전달할수도 있음
        response.setStatus(401);
        response.setContentType("application/json");
        response.setCharacterEncoding("utf-8");
        
        // 응답 본문에 JSON 문자열 작성
        // - objectMapper.writeValueAsString(entryPointErrorResponse)로 DTO를 JSON으로 변환
        response.getWriter().write(objectMapper.writeValueAsString(entryPointErrorResponse));
    }
}

(3) EntryPointErrorResponse

package com.example.demo.config.security;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;

/**
 * 인증 실패 시 클라이언트에게 반환할 에러 응답 객체 (DTO)
 *
 * 주요 역할:
 * - CustomAuthenticationEntryPoint에서 사용됨
 * - 인증 실패(401 Unauthorized) 상황에서 JSON 응답으로 내려줄 데이터 구조 정의
 * - 단순히 메시지(msg) 필드 하나만 포함하여, 클라이언트가 쉽게 에러 내용을 확인할 수 있도록 함
 *
 * 특징:
 * - Lombok 어노테이션(@Data, @NoArgsConstructor, @AllArgsConstructor, @ToString)으로
 *   getter/setter, 생성자, toString 메서드를 자동 생성
 * - 직렬화 시 JSON 형태로 변환되어 클라이언트에 전달됨
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class EntryPointErrorResponse {

    /**
     * 에러 메시지를 담는 필드
     * - 인증 실패 시 "인증에 실패하였습니다." 등의 안내 문구를 저장
     * - ObjectMapper를 통해 JSON 변환 시 {"msg":"인증에 실패하였습니다."} 형태로 응답됨
     */
    private String msg;
}

7) 회원가입과 로그인 구현

  • User 객체 생성을 위한 회원가입 구현
  • 생성된 User 객체로 인증을 시도하는 로그인 구현
  • 회원가입과 로그인의 도메인은 Sign으로 통합해서 표현하며, 각각 Sign-up과 Sign-in으로 구분하여 기능 구현

(1) SignUpResultDto, SignInResultDto

package com.example.demo.jpa.data.dto;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;

/**
 * 회원가입 결과를 클라이언트에게 전달하기 위한 응답 DTO 클래스
 *
 * 주요 역할:
 * - 회원가입 처리 후 성공/실패 여부, 상태 코드, 메시지를 담아 반환
 * - Controller → Service → 클라이언트로 전달되는 데이터 구조 정의
 * - JSON 직렬화 시 {"success":true, "code":200, "msg":"회원가입 성공"} 형태로 응답 가능
 *
 * 특징:
 * - Lombok 어노테이션(@Data, @NoArgsConstructor, @AllArgsConstructor, @ToString) 사용
 *   → getter/setter, 기본 생성자, 전체 필드 생성자, toString 자동 생성
 * - 불필요한 보일러플레이트 코드 제거로 가독성 향상
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class SignUpResultDto {

    /**
     * 회원가입 성공 여부
     * - true: 회원가입 성공
     * - false: 회원가입 실패
     */
    private boolean success;

    /**
     * 결과 코드
     * - 예: 200 (성공), 400 (잘못된 요청), 500 (서버 오류) 등
     * - 클라이언트가 결과를 상태 코드 기반으로 처리할 수 있도록 제공
     */
    private int code;

    /**
     * 결과 메시지
     * - 회원가입 성공/실패에 대한 설명 문구
     * - 예: "회원가입 성공", "이미 존재하는 사용자입니다."
     */
    private String msg;
}
package com.example.demo.jpa.data.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;

/**
 * 로그인 결과를 클라이언트에게 전달하기 위한 응답 DTO 클래스
 *
 * 주요 역할:
 * - 로그인 처리 후 JWT 토큰을 포함한 결과를 반환
 * - 부모 클래스(SignUpResultDto)의 필드(success, code, msg)를 상속받아 기본 응답 정보 제공
 * - 추가적으로 token 필드를 포함하여 로그인 성공 시 발급된 JWT 토큰을 클라이언트에 전달
 *
 * 특징:
 * - Lombok 어노테이션(@Data, @NoArgsConstructor, @AllArgsConstructor, @ToString) 사용
 *   → getter/setter, 생성자, toString 자동 생성
 * - @Builder를 통해 빌더 패턴으로 객체 생성 가능
 * - 상속 구조 덕분에 회원가입/로그인 응답 DTO가 일관된 형태를 유지
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class SignInResultDto extends SignUpResultDto {
    
    /**
     * 로그인 성공 시 발급되는 JWT 토큰
     * - 클라이언트가 이후 요청 시 인증 헤더에 포함하여 사용
     */
    private String token;
    
    /**
     * 빌더 패턴을 활용한 생성자
     * - 부모 클래스(SignUpResultDto)의 생성자를 호출하여 success, code, msg 초기화
     * - 현재 클래스의 token 필드도 함께 초기화
     *
     * @param success 로그인 성공 여부
     * @param code    결과 코드 (예: 200 성공, 401 인증 실패 등)
     * @param msg     결과 메시지
     * @param token   JWT 토큰
     */
    @Builder
    public SignInResultDto(boolean success, int code, String msg, String token) {
        super(success, code, msg); // 부모 클래스 생성자 호출
        this.token = token;        // 현재 클래스 필드 초기화
    }
}

(2) SignService 인터페이스

package com.example.demo.service;

import com.example.demo.jpa.data.dto.SignInResultDto;
import com.example.demo.jpa.data.dto.SignUpResultDto;

/**
 * 회원가입 및 로그인 관련 기능을 정의하는 서비스 인터페이스
 *
 * 주요 역할:
 * - 회원가입(signUp)과 로그인(signIn) 기능을 추상화하여 구현 클래스에서 실제 로직을 작성하도록 강제
 * - Controller 계층에서 이 인터페이스를 호출하여 서비스 로직을 실행
 * - DTO(SignUpResultDto, SignInResultDto)를 반환하여 클라이언트에 결과 전달
 *
 * 특징:
 * - 인터페이스로 정의되어 있어 다양한 구현체를 만들 수 있음 (예: DB 기반, 외부 인증 서버 기반 등)
 * - 예외 처리나 세부 로직은 구현 클래스에서 담당
 */
public interface SignService {
    
    /**
     * 회원가입 기능
     *
     * @param id       사용자 ID
     * @param password 사용자 비밀번호
     * @param name     사용자 이름
     * @param role     사용자 권한 (예: ROLE_USER, ROLE_ADMIN)
     * @return SignUpResultDto 회원가입 결과 DTO
     *         - success: 성공 여부
     *         - code: 상태 코드
     *         - msg: 결과 메시지
     */
    SignUpResultDto signUp(String id, String password, String name, String role);
    
    /**
     * 로그인 기능
     *
     * @param id       사용자 ID
     * @param password 사용자 비밀번호
     * @return SignInResultDto 로그인 결과 DTO
     *         - success, code, msg: 기본 응답 정보
     *         - token: 로그인 성공 시 발급된 JWT 토큰
     * @throws RuntimeException 로그인 실패 시 예외 발생
     */
    SignInResultDto signIn(String id, String password) throws RuntimeException;
}

(3) PasswordEncoderConfiguration 클래스

package com.example.demo.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;

/**
 비밀번호를 안전하게 암호화하고 검증하기 위해 
 Spring Security에서 제공하는 PasswordEncoder를 Bean으로 등록
**/
@Configuration // 설정 클래스임을 명시
public class PasswordEncoderConfiguration {
    
    @Bean // PasswordEncoder를 Bean으로 등록 → 스프링 컨테이너에서 관리
    public PasswordEncoder passwordEncoder() {
        // DelegatingPasswordEncoder 생성 (기본은 BCrypt)
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }
}

(4) CommonResponse

package com.example.demo.config.security.common;

/**
 * 애플리케이션에서 공통적으로 사용하는 응답 상태 Enum
 *
 * 주요 역할:
 * - API 응답이나 서비스 로직에서 성공/실패 여부를 일관되게 표현하기 위해 사용
 * - 각 상태는 코드(int)와 메시지(String)를 함께 제공
 *   → 클라이언트가 응답을 처리할 때 코드와 메시지를 함께 활용 가능
 *
 * 특징:
 * - SUCCESS: 성공 상태 (code=0, msg="Success")
 * - FAIL: 실패 상태 (code=-1, msg="Fail")
 * - Enum으로 정의되어 있어 타입 안정성을 보장하고, 상수 값 관리가 용이함
 */
public enum CommonResponse {
    SUCCESS(0, "Success"), // 성공 응답
    FAIL(-1, "Fail");      // 실패 응답
    
    // 응답 코드 (성공/실패를 숫자로 표현)
    int code; 
    
    // 응답 메시지 (성공/실패를 문자열로 표현)
    String msg;
    
    /**
     * Enum 생성자
     * - 각 Enum 상수에 대응하는 code와 msg를 초기화
     */
    CommonResponse(int code, String msg) {
        this.code = code;
        this.msg = msg;
    }
    
    /**
     * 응답 코드 반환
     * @return code (예: 0, -1)
     */
    public int getCode() {
        return code;
    }
    
    /**
     * 응답 메시지 반환
     * @return msg (예: "Success", "Fail")
     */
    public String getMsg() {
        return msg;
    }
}

(5) SignServiceImpl (SignService 인터페이스를 구현)

package com.example.demo.service;

import java.util.Collections;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

import com.example.demo.config.security.JwtTokenProvider;
import com.example.demo.config.security.common.CommonResponse;
import com.example.demo.jpa.data.dto.SignInResultDto;
import com.example.demo.jpa.data.dto.SignUpResultDto;
import com.example.demo.jpa.data.entity.User;
import com.example.demo.jpa.data.repository.UserRepository;

/**
 * 회원가입과 로그인 기능을 실제로 구현한 서비스 클래스
 */
@Service // Spring의 Service 컴포넌트로 등록 → 비즈니스 로직 담당
public class SignServiceImpl implements SignService {
    
    // Logger 인스턴스 생성 → 서비스 동작 과정 로깅
    private final Logger LOGGER = LoggerFactory.getLogger(SignServiceImpl.class);
    
    // 의존성 주입 받을 Repository, TokenProvider, PasswordEncoder
    public UserRepository userRepository;
    public JwtTokenProvider jwtTokenProvider;
    public PasswordEncoder passwordEncoder;
    
    // 생성자 주입 → Spring이 자동으로 Bean을 주입해줌
    @Autowired
    public SignServiceImpl(UserRepository userRepository, JwtTokenProvider jwtTokenProvider, PasswordEncoder passwordEncoder) {
        this.userRepository = userRepository;         // DB 접근용 Repository
        this.jwtTokenProvider = jwtTokenProvider;     // JWT 토큰 생성/검증용 Provider
        this.passwordEncoder = passwordEncoder;       // 비밀번호 암호화/검증용 Encoder
    }
    
    @Override
    public SignUpResultDto signUp(String id, String password, String name, String role) {
        LOGGER.info("[getSignUpResult] 회원 가입 정보 전달"); // 로그 기록
        User user;
        
        // role이 admin이면 ROLE_ADMIN 부여, 아니면 ROLE_USER 부여
        if (role.equalsIgnoreCase("admin")) {
            user = User.builder()
                    .uid(id)                                   // 사용자 ID
                    .name(name)                                // 사용자 이름
                    .password(passwordEncoder.encode(password)) // 비밀번호 암호화 후 저장
                    .roles(Collections.singletonList("ROLE_ADMIN")) // 관리자 권한 부여
                    .build();
        } else {
            user = User.builder()
                    .uid(id)
                    .name(name)
                    .password(passwordEncoder.encode(password)) // 비밀번호 암호화 후 저장
                    .roles(Collections.singletonList("ROLE_USER")) // 일반 사용자 권한 부여
                    .build();
        }
        
        // DB에 사용자 저장
        User savedUser = userRepository.save(user);
        SignUpResultDto signUpResultDto = new SignUpResultDto();
        
        LOGGER.info("[getSignUpResult] userEntity 값이 들어왔는지 확인 후 결과값 주입");
        // 저장된 사용자 이름이 비어있지 않으면 성공 처리
        if(!savedUser.getName().isEmpty()) {
            LOGGER.info("[getSignUpResult] 정상 처리 완료");
            setSuccessResult(signUpResultDto); // 성공 응답 세팅
        } else {
            LOGGER.info("[getSignUpResult] 실패 처리 완료");
            setFailResult(signUpResultDto);    // 실패 응답 세팅
        }
        
        return signUpResultDto; // 결과 반환
    }

    @Override
    public SignInResultDto signIn(String id, String password) throws RuntimeException {
        LOGGER.info("[getSignInResult] signDataHandler로 회원 정보 요청");
        User user = userRepository.getByUid(id); // ID로 사용자 조회
        LOGGER.info("[getSignInResult] Id: {}", id);
        
        LOGGER.info("[getSignInResult] 패스워드 비교 수행");
        // 입력한 비밀번호와 DB에 저장된 암호화된 비밀번호 비교
        if(!passwordEncoder.matches(password, user.getPassword())) {
            throw new RuntimeException(); // 불일치 시 예외 발생
        }
        LOGGER.info("[getSignInResult] 패스워드 일치");
        
        LOGGER.info("[getSignInResult] SignInResultDto 객체 생성");
        // 로그인 성공 시 JWT 토큰 생성 후 DTO에 담음
        SignInResultDto signInResultDto = SignInResultDto.builder()
                .token(jwtTokenProvider.createToken(String.valueOf(user.getUid()), user.getRoles()))
                .build();
        
        LOGGER.info("[getSignInResult] SignInResultDto 객체에 값 주입");
        setSuccessResult(signInResultDto); // 성공 응답 세팅
        
        return signInResultDto; // 결과 반환
    }

    // 성공 응답 세팅 메서드
    private void setSuccessResult(SignUpResultDto result) {
        result.setSuccess(true);                          // 성공 여부 true
        result.setCode(CommonResponse.SUCCESS.getCode()); // 공통 응답 코드 SUCCESS
        result.setMsg(CommonResponse.SUCCESS.getMsg());   // 공통 응답 메시지 SUCCESS
    }
    
    // 실패 응답 세팅 메서드
    private void setFailResult(SignUpResultDto result) {
        result.setSuccess(false);                         // 성공 여부 false
        result.setCode(CommonResponse.FAIL.getCode());    // 공통 응답 코드 FAIL
        result.setMsg(CommonResponse.FAIL.getMsg());      // 공통 응답 메시지 FAIL
    }
}

(6) SignController

  • required=true는 선택 사항으로,
    이미 @PathVariable이나 @RequestParam에서 필수로 지정하면 자동 반영되므로 생략 가능
package com.example.demo.controller;

import java.util.HashMap;
import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.example.demo.jpa.data.dto.SignInResultDto;
import com.example.demo.jpa.data.dto.SignUpResultDto;
import com.example.demo.service.SignService;

import io.swagger.v3.oas.annotations.Parameter;

/**
 * 회원가입 및 로그인 관련 API를 제공하는 REST 컨트롤러
 *
 * 주요 역할:
 * - /sign-api 경로 하위에서 회원가입(sign-up), 로그인(sign-in) 요청 처리
 * - SignService를 호출하여 실제 비즈니스 로직 실행
 * - 예외 발생 시 @ExceptionHandler로 처리하여 JSON 응답 반환
 *
 * 특징:
 * - @RestController: REST API 응답(JSON)을 반환하는 컨트롤러
 * - @RequestMapping("/sign-api"): 모든 엔드포인트가 /sign-api 하위에 위치
 */
@RestController
@RequestMapping("/sign-api")
public class SignController {
	 // 로깅 객체 생성 → 요청/응답 과정 기록
    private final Logger LOGGER = LoggerFactory.getLogger(SignController.class);
    
    // 회원가입/로그인 서비스 의존성
    private final SignService signService;
    
    // 생성자 주입 → Spring이 SignService 구현체를 자동으로 주입
    @Autowired
    public SignController(SignService signService) {
        this.signService = signService;
    }
    
    /**
     * 로그인 API
     * - POST /sign-api/sign-in
     * - id, password를 받아 로그인 처리
     * - 성공 시 JWT 토큰을 포함한 SignInResultDto 반환
     */
    @PostMapping(value = "sign-in")
    public SignInResultDto signIn(
            // Swagger 문서화를 위한 @Parameter 사용
            @Parameter(required=true) @RequestParam String id, 
            @Parameter(required=true) @RequestParam String password)
            throws RuntimeException {
        
        LOGGER.info("[signIn] 로그인을 시도하고 있습니다. id: {}, pw: ****", id);
        SignInResultDto signInResultDto = signService.signIn(id, password);
        
        // 로그인 성공 시 로그 기록
        if (signInResultDto.getCode() == 0) {
            LOGGER.info("[signIn] 로그인 성공. id: {}, token: {}", id, signInResultDto.getToken());
        }
        return signInResultDto; 
    }
    
    /**
     * 회원가입 API
     * - POST /sign-api/sign-up
     * - id, password, name, role을 받아 회원가입 처리
     * - 성공/실패 여부를 SignUpResultDto로 반환
     */
    @PostMapping(value = "/sign-up")
    public SignUpResultDto signUp(
            @Parameter(required=true) @RequestParam String id,
            @Parameter(required=true) @RequestParam String password,
            @Parameter(required=true) @RequestParam String name,
            @Parameter(required=true) @RequestParam String role) {
        
        LOGGER.info("[signUp] 회원가입 수행. id: {}, password: ****, name: {}, role: {}", id, name, role);
        SignUpResultDto signUpResultDto = signService.signUp(id, password, name, role);
        
        LOGGER.info("[signUp] 회원가입 완료. id: {}", id);
        return signUpResultDto;
    }
    
    /**
     * 예외 발생 테스트 API
     * - GET /sign-api/exception
     * - 강제로 RuntimeException 발생시켜 예외 처리 흐름 확인
     */
    @GetMapping(value="/exception")
    public void exceptionText() throws RuntimeException {
        throw new RuntimeException("접근 권한이 없습니다.");
    }
    
    /**
     * RuntimeException 처리 핸들러
     * - 컨트롤러 내에서 발생한 RuntimeException을 잡아 JSON 응답 반환
     * - 상태 코드: 400 Bad Request
     * - 응답 본문: 에러 타입, 코드, 메시지 포함
     */
    @ExceptionHandler(value = RuntimeException.class)
    public ResponseEntity<Map<String, String>> ExceptionHandler(RuntimeException e) {
        HttpHeaders responseHeaders = new HttpHeaders();
        HttpStatus httpStatus = HttpStatus.BAD_REQUEST;
        
        LOGGER.error("ExceptionHandler 호출, {}, {}", e.getCause(), e.getMessage());
        
        // 에러 응답 데이터 구성
        Map<String, String> map = new HashMap<>();
        map.put("error type", httpStatus.getReasonPhrase()); // "Bad Request"
        map.put("code", "400");
        map.put("message", "에러 발생");
        
        // ResponseEntity로 JSON 응답 반환
        return new ResponseEntity<>(map, responseHeaders, httpStatus);
    }
}

 

 

*** 다음 편에서 계속됩니다 ***

반응형