웹개발/SpringBoot

보안과 권한 시스템 - 3. Controller

bergerac 2025. 6. 4. 09:11

Controller의 역할

프로젝트 구조

└── com.example.korea_sleepTech_springboot
    ├── controller
    │   ├── UserController.java
    │   ├── AuthController.java
    │   └── AdminController.java
    ├── service
    │   ├── UserService.java
    │   ├── AuthService.java
    │   └── AdminService.java
    └── dto
        └── user / auth / response / request ...

 

1. 웹 요청의 진입점(Entry Point)

Controller는 클라이언트가 전송한 HTTP 요청을 가장 먼저 받아들이는 **프론트 컨트롤러(FE Controller)**이다.

  • 어떤 경로(URL)로 어떤 동작을 할 것인지를 선언하고
  • 요청을 적절한 Service 계층에 위임하며
  • 응답을 포장해 클라이언트에게 다시 반환한다

2. Controller 분리 이유

단일 책임 원칙 (SRP: Single Responsibility Principle)

  • AuthController: 로그인/회원가입/이메일 인증 등 인증(Authentication), 인가(Authorization) 중심 기능
  • UserController: 로그인한 유저의 개인정보 조작 (조회, 수정, 탈퇴 등)
  • AdminController: 어드민 권한이 있는 사용자의 역할 승격/강등 기능

즉, 기능별로 책임을 명확히 나누어 설계하면 유지보수성과 확장성이 극대화된다.

 

3. Spring MVC 구조

Spring은 DispatcherServlet이 요청을 가로채고 적절한 Controller에 매핑한다.

[요청]
  ↓
DispatcherServlet
  ↓
HandlerMapping → UserController, AuthController 등
  ↓
Service → Repository
  ↓
응답 반환

4. REST API 설계 원칙

  • 리소스 단위로 URL을 설계하고
  • GET, POST, PUT, DELETE로 HTTP 메서드 활용
    • GET /api/v1/user/me: 내 정보 조회
    • PUT /api/v1/admin/promote: 관리자 승격
    • POST /api/v1/auth/signup: 회원 가입예시:

5. 보안 연동

  • 인증된 사용자 → @AuthenticationPrincipal
  • 관리자만 접근 → @PreAuthorize
  • 인증은 JWT, SecurityContext, FilterChain 등을 통해 동작하고, Controller는 이를 활용만 하면 됨

UserController: 인증된 사용자 전용 API

 

  • @AuthenticationPrincipal은 Spring Security에서 현재 인증된 사용자의 이메일을 주입
  • @Valid는 유효성 검사 → DTO에 @NotNull 등의 애너테이션이 있으면 자동 체크됨
  • ResponseEntity.ok()를 통해 HTTP 200 상태와 JSON 응답을 반환
  • Service 로직에만 집중하도록 컨트롤러에서는 흐름만 정의
@RestController
@RequestMapping(ApiMappingPattern.USER_API)  // /api/v1/user
@RequiredArgsConstructor
public class UserController {

    private final UserService userService;

    private static final String GET_USER_INFO = "/me";
    private static final String PUT_USER_INFO = "/me";
    private static final String DELETE_USER   = "/me";

    @GetMapping(GET_USER_INFO)
    public ResponseEntity<ResponseDto<GetUserResponseDto>> getUserInfo(
            @AuthenticationPrincipal String userEmail
    ) {
        ResponseDto<GetUserResponseDto> response = userService.getUserInfo(userEmail);
        return ResponseEntity.ok(response);
    }

    @PutMapping(PUT_USER_INFO)
    public ResponseEntity<ResponseDto<GetUserResponseDto>> updateUserInfo(
            @AuthenticationPrincipal String userEmail,
            @Valid @RequestBody UserUpdateRequestDto dto
    ) {
        ResponseDto<GetUserResponseDto> response = userService.updateUserInfo(userEmail, dto);
        return ResponseEntity.ok(response);
    }

    @DeleteMapping(DELETE_USER)
    public ResponseEntity<ResponseDto<Void>> deleteUser(
            @AuthenticationPrincipal String userEmail
    ) {
        userService.deleteUser(userEmail);
        return ResponseEntity.noContent().build();
    }
}

 

 

 

AuthController: 인증/가입 관련 공개 API

 

  • 인증 관련 요청은 대부분 POST 방식 사용: 민감 정보 (비밀번호 등)를 URL에 노출시키지 않기 위함
  • Spring WebFlux의 Mono<>를 사용하는 API도 있어 → 비동기 응답 처리
  • API 패턴이 명확해서 프론트엔드에서도 어떤 요청을 어디로 보내야 할지 혼동이 없음
@RestController
@RequestMapping(ApiMappingPattern.AUTH_API)  // /api/v1/auth
@RequiredArgsConstructor
public class AuthController {

    private final AuthService authService;
    private final MailService mailService;

    @PostMapping("/signup")
    public ResponseEntity<ResponseDto<UserSignUpResponseDto>> signup(@Valid @RequestBody UserSignUpRequestDto dto) {
        ResponseDto<UserSignUpResponseDto> response = authService.signup(dto);
        return ResponseEntity.status(HttpStatus.CREATED).body(response);
    }

    @PostMapping("/login")
    public ResponseEntity<ResponseDto<UserSignInResponseDto>> login(@Valid @RequestBody UserSignInRequestDto dto) {
        ResponseDto<UserSignInResponseDto> response = authService.login(dto);
        return ResponseEntity.ok(response);
    }

    @PostMapping("/send-email")
    public Mono<ResponseEntity<String>> sendEmail(@Valid @RequestBody SendMailRequestDto dto) {
        return mailService.sendSimpleMessage(dto.getEmail());
    }

    @GetMapping("/verify")
    public Mono<ResponseEntity<String>> verifyEmail(@RequestParam String token) {
        return mailService.verifyEmail(token);
    }

    @PostMapping("/reset-password")
    public Mono<ResponseEntity<String>> resetPassword(@Valid @RequestBody PasswordResetRequestDto dto) {
        return authService.resetPassword(dto);
    }
}

 

 

 

AdminController: 관리자 전용 API

  • @PreAuthorize("hasRole('ADMIN')"): Spring Security의 메서드 레벨 보안
    • 해당 어노테이션이 없으면 누구나 접근 가능함 → 보안상 치명적
  • /promote, /demote는 REST의 의미를 살려서 PUT 사용
@RestController
@RequiredArgsConstructor
@RequestMapping(ApiMappingPattern.ADMIN_API) // /api/v1/admin
public class AdminController {

    private final AdminService adminService;

    private static final String PUT_AUTHORITY_TO_ADMIN = "/promote";
    private static final String PUT_AUTHORITY_DEMOTE   = "/demote";

    @PreAuthorize("hasRole('ADMIN')")
    @PutMapping(PUT_AUTHORITY_TO_ADMIN)
    public ResponseEntity<ResponseDto<PromoteToAdminResponseDto>> promoteUserToAdmin(
            @RequestBody PutAuthorityRequestDto dto
    ) {
        ResponseDto<PromoteToAdminResponseDto> response = adminService.promoteUserToAdmin(dto);
        return ResponseEntity.ok(response);
    }

    @PreAuthorize("hasRole('ADMIN')")
    @PutMapping(PUT_AUTHORITY_DEMOTE)
    public ResponseEntity<ResponseDto<DemoteFromAdminResponseDto>> demoteUserFromAdmin(
            @RequestBody PutAuthorityRequestDto dto
    ) {
        ResponseDto<DemoteFromAdminResponseDto> response = adminService.demoteUserFromAdmin(dto);
        return ResponseEntity.ok(response);
    }
}