5th Weekly Reflection

Metronon·2024년 12월 21일

REST-API

목록 보기
4/12
post-thumbnail

이번주 학습한 REST API 방식을 활용해서 기존의 프로젝트를 REST API 형식으로 변환하는 절차에 대해 개념정리를 하고자 한다.
변환하려는 대상은 점프 투 스프링부트 2-3장에서 구현하는 질의응답 게시판이다.

프로젝트를 REST API 방식으로 전환하기

우선 기존에 프로젝트는 Question, Answer, siteUser 이렇게 3개의 패키지로 구성되어있다.
REST API 아키텍쳐 스타일에 따라 하나씩 변경하는 절차를 정리해보면 다음과 같다.

1. 컨트롤러 변경

기존 스프링부트 기반 컨트롤러는 @Controller 어노테이션을 사용한다.
HTML 매핑주소를 리턴하고, 기존 입력값을 받아오기 위해 Model 클래스를 사용한다.

@Controller
public class QuestionController {
   @GetMapping("/question/detail/{id}")
   public String detail(Model model, @PathVariable("id") Integer id) {
       model.addAttribute("question", questionService.getQuestion(id));
       return "question_detail";
   }
}

REST API의 컨트롤러는 @RestController 어노테이션을 사용한다.
기존과 달리 HTTP 상태코드를 리턴한다.

// REST API 컨트롤러로 변경
@RestController  // @Controller + @ResponseBody
RequestMapping("/api")
public class QuestionApiController {
   @GetMapping("/question/{id}")
   public ResponseEntity<QuestionDto> getQuestion(@PathVariable("id") Integer id) {
       return ResponseEntity.ok(questionService.getQuestionDto(id));
   }
}

2. DTO (Data Transfer Object) 생성

기존 프로젝트에서는 Entity를 이용해 데이터 교환이 이루어졌지만 REST API에선 DTO가 대신 외부와의 데이터 교환을 담당한다.

@Getter
@Builder
public class QuestionDto {
   private Integer id;
   private String subject;
   private String content;
   private LocalDateTime createDate;
   
   // Entity -> DTO 변환 메서드
   public static QuestionDto from(Question question) {
       return QuestionDto.builder()
               .id(question.getId())
               .subject(question.getSubject())
               .content(question.getContent())
               .createDate(question.getCreateDate())
               .build();
   }
}

DTO를 사용하면서 얻는 이점은 다음과 같다.

  • 민감한 정보는 제외할 수 있다.
  • @JsonProperty를 사용해 응답 필드명을 변경할 수 있다.
  • 데이터의 표현 형식을 가공할 수 있다.
  • 필요한 데이터만 선택할 수 있다.
public class UserDto {
    // 1. 민감 정보 제외
    private String username;  // 포함
    // private String password;  // 제외
    
    // 2. 응답 필드명 변경
    @JsonProperty("user_email")
    private String email;
    
    // 3. 데이터 가공
    @JsonFormat(pattern = "yyyy-MM-dd")
    private LocalDateTime createDate;
    
    // 4. 필요한 데이터만 선택
    private List<String> roleNames;  // Role 엔티티 대신 이름만 전송
}

3. 전역적으로 사용되는 Response 클래스 생성

컨트롤러에서의 작업이 성공/실패되는 경우의 작업을 표준화해줄 필요가 있다.
모든 작업에서 전역적으로 HTTP 상태코드를 전달하기 위해서 Global한 Response를 만들어줄 필요가 있다.

@Getter
@AllArgsConstructor
public class ApiResponse<T> {
   private boolean success;    // 성공/실패 여부
   private T data;            // 실제 데이터 (질문, 답변 등)
   private String message;     // 응답 메시지
   private String error;       // 에러 메시지
    // 성공 응답 생성
   public static <T> ApiResponse<T> success(T data) {
       return new ApiResponse<>(true, data, "성공", null);
   }
    // 실패 응답 생성
   public static <T> ApiResponse<T> error(String errorMessage) {
       return new ApiResponse<>(false, null, null, errorMessage);
   }
}

4. 전역적으로 사용되는 ExceptionHandler 클래스 생성

API에서 발생하는 Exception 역시 GlobalExceptionHandler를 만들어 관리한다.
주로 ExceptionHandler를 통해 예외를 관리하고, 특수한 예외가 발생하는 경우 try-catch문으로 따로 관리하기도 한다.

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(ValidationException.class)
    public ResponseEntity<ErrorResponse> handleValidationException(ValidationException e) {
        ErrorResponse error = ErrorResponse.builder()
            .status(HttpStatus.BAD_REQUEST.value())
            .message(e.getMessage())
            .timestamp(LocalDateTime.now())
            .build();
        return ResponseEntity.badRequest().body(error);
    }

    @ExceptionHandler(DuplicateEmailException.class)
    public ResponseEntity<ErrorResponse> handleDuplicateEmailException(DuplicateEmailException e) {
        ErrorResponse error = ErrorResponse.builder()
            .status(HttpStatus.CONFLICT.value())
            .message("이미 존재하는 이메일입니다.")
            .timestamp(LocalDateTime.now())
            .build();
        return ResponseEntity.status(HttpStatus.CONFLICT).body(error);
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleAllException(Exception e) {
        ErrorResponse error = ErrorResponse.builder()
            .status(HttpStatus.INTERNAL_SERVER_ERROR.value())
            .message("서버 내부 오류가 발생했습니다.")
            .timestamp(LocalDateTime.now())
            .build();
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
    }
} 

5. API 문서화

API의 사용방법을 문서로 작성하는 작업을 해주어야 한다.

@Operation(summary = "사용자 조회", description = "ID로 사용자 정보를 조회합니다")
@ApiResponses({
    @ApiResponse(responseCode = "200", description = "조회 성공"),
    @ApiResponse(responseCode = "404", description = "사용자 없음")
})
@GetMapping("/users/{id}")
public ResponseEntity<UserDto> getUser(
    @Parameter(description = "사용자 ID") @PathVariable Long id
) {
    // 메서드 구현
}

6. 보안 설정 변경

기존 프로젝트는 세션 기반 인증방식을 사용하는 경우 CSRF(Cross-Site Request Forgery)를 사용하게 된다.
하지만 REST API는 상태를 저장하지 않고, JWT(JSON Web Token)와 같은 토큰 기반 인증을 사용하기 때문에 CSRF 보호를 사용하지 않는다.
각 요청마다 인증 토큰이 헤더에 포함되기 때문에 토큰이 없다면 요청이 거부되어 CSRF 공격으로부터 안전하다.

@Configuration
public class SecurityConfig {
   @Bean
   public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
       http
           .csrf().disable()  // REST API에서는 CSRF 보호가 필요없음
           .sessionManagement()
           .sessionCreationPolicy(SessionCreationPolicy.STATELESS)  // JWT 사용시
           .and()
           .authorizeRequests()
           .antMatchers("/api/**").authenticated()
           .and()
           .addFilterBefore(new JwtAuthenticationFilter(), 
               UsernamePasswordAuthenticationFilter.class);
   }
}
profile
비전공 개발 지망생의 벨로그입니다!

0개의 댓글