본문 바로가기

Server Programming/Spring Boot Backend Programming

6장-1. Ajax와 JSON을 이용한 REST 서비스 구현 (+ swagger, BindException, BindingResult, @RestControllerAdvice)

반응형

순수한 데이터만 제공하는 서버 사이드 프로그래밍

 

사용 기술

  • REST
  • Ajax
  • JSON
  • Swagger

 

  • RESTful 웹 서비스 (Representational State Transfer)
    • 특정한 URL과 HTTP 메소드를 결합해 '특정한 자원에 특정한 작업'을 지정하는 방식
    • 기존 방식 : URL이 '행위나 작업'을 의미하고, GET/POST가 '데이터를 전송하는 위치'를 의미
    • Ajax를 이용해 브라우저의 주소 이동없이도 서버와 데이터를 교환하므로,
      REST 방식 : URL이 '원하는 대상'을 의미, PUT/DELETE가 '행위나 작업'을 의미한다
    • 쿼리 스트링을 이용하지 않고, 직접 주소의 일부로 사용해 
      하나의 자원을 하나의 주소로 유일무이 표현함으로써, 하나의 URL이 하나의 자원을 식별하는 고유값이 된다.
    • URI이라는 자원을 이용해 '원하는 행위/작업'을 나타내는 GET/POST/PUT/DELETE를 수행하는 방식
    • 비동기로 서버와 상호작용하기 때문에 언제, 어디서 에러가 발생하는지 파악하기 위해 단계별로 검증 작업을 수행해야한다.
      : @Valid, @RestControllerAdvice
  • URL (Uniform Resource Locator) : URI의 하위 개념으로 원하는 정보의 위치를 가리키는 값
  • URI (Uniform Resource Identifier) : 좀더 구체적인 의미의 정보의 주소를 나타내는 자원식별자로, 기본키와 유사한 의미의 값

  • RESTful의 메서드
    • GET : 조회
    • POST : 등록
    • PUT : 수정
    • DELETE : 삭제
  • Ajax
    •  자바스크립트과 XML을 이용한 비동기 처리
    • 모든 작업이 브라우저 내부에서 이루어지는 방식으로, 브라우저 화면의 변화 없이 서버와 통신할 수 있다.
    • 자동 완성, 지도 서비스등에서 사용
    • 최근에는 XML보다는 JSON을 이용하는 방식을 선호한다.

    • JSON이 각광받는 이유는 순수한 데이터만 전송하기 문에 클라이언트 구현이 웹/앱에 관계없이 데이터의 재사용이 가능하다.
    • 또한, 대규모 웹 애플리케이션 개발을 지원하는 라이브러리나 프레임워크가 등장하면서 클라이언트와 서버의 역할 분배가 대두됨
  • JSON 문자열 - DTO <-> JSON (객체를 JSON으로 표현, JSON을 객체로 표현)
    • '문자열'은 종속된 데이터가 아니라는 장점이 있지만, 복잡한 구조의 데이터를 전송할 경우 문제가 발생
    • 따라서 등장한 개념이 XML과 JSON이라는 문자열 포맷
    • 자바스크립트가 키와 값 형태로 객체를 표현하는 방식의 문법을 지원하는 문자열로 데이터를 표현한다.
    • 클라이언트에서 어떤 기술을 이용하든 공통적으로 인식할 수 있다.

구현 순서

  • Swagger 연동
  • RestController 작성
  • 검증작업 수행

Swagger 연동

 

1. Swagger 의존성 추가

//swagger
implementation 'io.springfox:springfox-boot-starter:3.0.0'
implementation 'io.springfox:springfox-swagger-ui:3.0.0'

 

2. Swagger를 위한 SwaggerConfig 클래스 작성

package org.zerock.b01.config;

import lombok.Builder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.RequestHandler;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;

@Configuration
public class SwaggerConfig {
    @Bean
    public Docket api(){
        return new Docket(DocumentationType.OAS_30)
                .useDefaultResponseMessages(false)
                .select()
                .apis(RequestHandlerSelectors.basePackage("org.zerock.b01.controller"))
                .paths(PathSelectors.any())
                .build()
                .apiInfo(apiInfo());
    }

    private ApiInfo apiInfo(){
        return new ApiInfoBuilder()
                .title("Boot 01 Proejct Swagger")
                .build();
    }
}

 

3. Spring Web 관련 설정을 위한 CustomServletConfig 클래스 작성

package org.zerock.b01.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;

@Configuration
@EnableWebMvc
public class CustomServletConfig {
}

 

4. 'http://localhost:8080/swagger-ui/index.html' 호출 확인

 

5. 정적 파일 경로 설정

-Swagger UI 적용시 정적 파일의 경로가 변경되는 문제

-CustomServletConfig에서 Web-MvcConfigurer 인터페이스를 구현해 addResourceHandler 오버라이딩

@Configuration
@EnableWebMvc
public class CustomServletConfig implements WebMvcConfigurer {
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry){
        registry.addResourceHandler("/js/**")
                .addResourceLocations("classpath:/static/js/");
        registry.addResourceHandler("/fonts/**")
                .addResourceLocations("classpath:/static/fonts/");
        registry.addResourceHandler("/css/**")
                .addResourceLocations("classpath:/static/css/");
        registry.addResourceHandler("/assets/**")
                .addResourceLocations("classpath:/static/assets/");
    }
}

REST 방식의 댓글 처리

HTTP 요청/응답 방식이 아닌 REST 방식을 사용하면 새로고침 없이 댓글 처리가 가능하다

 

REST 방식의 댓글처리 단계

  1. URL의 설계와 데이터 포맷 결정
  2. 컨트롤러의 JSON/XML 처리
  3. 동작 확인
  4. 자바스크립트를 통한 화면 처리

 

URL의 설계와 DTO 설계

XML이나 JSON 형태의 문자열을 전송해 컨트롤러에서 처리하는 방식

DTO형식에 맞는 JSON 전송->  -> 스프링에서 데이터 처리 ->  DTO로 처리

Method 역할 반환
POST /replies 게시물에 댓글 추가 생성된 댓글 번호: {'rno' :11}
GET /replies/list:bno 게시물의 댓글 목록
'?' + '페이지 번호'로 댓글 페이징 처리
JSON으로 PageResponseDTO 처리
/replies/:rno 특정 댓글 조회 댓글 객체를 JSON으로 변환한 문자열
PUT /replires/:rno 특정 댓글 수정 수정된 댓글 번호: {'rno' :11}
DELETE /replires/:rno 특정 댓글 삭제 삭제된 댓글 번호: {'rno' :11}

 

컨트롤러의 JSON/XML 처리

1. JSON 데이터를 받아 객체로 변환하는 RestController 작성

(1) ReplyController 작성

-@RestController : Ajax와 JSON을 이용해 비동기처리를 수행하는 어노테이션으로 메소드의 리턴 값이 JSON/XML으로 처리된다.

-@RequestMapping : JSON 데이터를 이용해 DTO로 변환하는 어노테이션

@RestController
@Log4j2
//JSON -> DTO로 변환하는 어노테이션
@RequestMapping("/replies")
public class ReplyController {

}

 

(2) ReplyController에 댓글 작성 메서드 register() 추가

-@ApiOpertaion : Swagger UI에서 출력하는 어노테이션으로 value 속성과, notes 속성이 있다.

-consume : REST에서의 @PostMapping 어노테이션을 이용해 데이터를 전송할 때 어떤 데이터를 사용하는지 명시하는 속성

-@RequestBody : 입력받은 JSON 문자열을 DTO로 변환하는 어노테이션

-ResponseEntity : 리턴값으로 JSON을 사용하는데, HTTP 상태 코드를 함께 전송하는 어노테이션

 

//Swagger UI에서 출력하는 메시지
@ApiOperation(value = "Replies POST", notes = "POST 방식으로 댓글등록")
//consumes : 메서드를 받아서 사용하는 데이터가 어떤 데이터인지 명시하는 속성
@PostMapping(value = "/", consumes = MediaType.APPLICATION_JSON_VALUE)
//댓글 등록 메서드로, ReplyDTO를 전달받아 키,값 객체인 ResponseEntity로 반환한다.
public ResponseEntity<Map<String, Long>> register(@RequestBody ReplyDTO replyDTO){

    log.info(replyDTO);
    Map<String,Long> resultMap=Map.of("rno", 111L);

    return ResponseEntity.ok(resultMap);
}

 

2. 검증 작업 수행

-@Valid : 해당 객체를 검증하는 어노테이션

-@RestControllerAdvice : 검증 과정에서 문제가 발생했을 때 처리하기위한 어노테이션으로, 예외처리를 수행한다.

 

(1) ReplyDTO에 검증 관련된 어노테이션을 추가

javax.validation.constraints 어노테이션

  • @NotNull, @Null, @NotEmpty, @NotBlank
  • @Size(min=, max=), @Pattern(regex=)
  • @Max(num), Min(num)
  • @Future @Past
  • @Positive @PositiveOrZero @Negative @NegativeOrZero
package org.zerock.b01.dto;

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

import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.time.LocalDateTime;

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ReplyDTO {
    
    private Long rno;
    //특정 게시글의 번호를 지정하기 위한 멤버변수
    @NotNull
    private Long bno;
    
    @NotEmpty
    private String replyText;
    
    @NotEmpty
    private String replyer;
    
    private LocalDateTime regDate, modDate;
}

 

(2) 검증을 수행했을 때 에러처리하는 CustomRestAdvice클래스 작성

메서드 명 handleBindException handleFKException handleSuchException
예외처리 BindException DataIntegrityViolationExcetpion NoSuchElementException
파라미터 BindException e Exception e Exception e
리턴타입 ResponseEntity
<Map<String, String>>
ResponseEntity
<Map<String, String>>
ResponseEntity
<Map<String, String>>
특징 JSON과 DTO 불일치 참조하는 FK값이 존재하지 않음 원하는 요소가 존재하지 않음
메시지 에러 발생 시간, 에러 메시지 에러 발생 시간, 에러 메시지 에러 발생 시간, 에러 메시지
리턴값 ResponseEntity.badRequest()
.body(errorMap);
ResponseEntity.badRequest()
.body(errorMap);
ResponseEntity.badRequest()
.body(errorMap);

- CustomRestAdvice에 BindException 예외처리를 위해 handleBindException() 메서드 생성

import org.springframework.validation.BindException;

-handleBindException() : 검증 과정에서 문제 발생해서 BindingResult에 에러가 존재하면 JSON 메시지와 400에러 전송하는 메서드

-@RestControllerAdvice : 검증 과정의 예외처리를 수행하는 클래스를 나타내는 어노테이션

@RestControllerAdvice
@Log4j2
public class CustomRestAdvice {

    @ExceptionHandler(BindException.class)
    @ResponseStatus(HttpStatus.EXPECTATION_FAILED)
    public ResponseEntity<Map<String, String>> handleBindException(BindException e) {

        log.error(e);

        Map<String, String> errorMap = new HashMap<>();

        if(e.hasErrors()){

            BindingResult bindingResult = e.getBindingResult();

            bindingResult.getFieldErrors().forEach(fieldError -> {
                errorMap.put(fieldError.getField(), fieldError.getCode());
            });
        }

        return ResponseEntity.badRequest().body(errorMap);
    }

 

동작 확인

RestController의 register()를 수정해 동작 여부를 확인

  • @Valid 사용해 ReplyDTO 수집
  • BingResult를 파라미터로 추가하고 예외처리를 위해 BindException 추가
  • 메소드 선언부에 예외처리를 위해 BindException 추가
  • 메소드 리턴값의 문제 발생시 @RestControllerAdvice가 처리하므로, 해당 컨트롤러에서는 항상 정상처리된 값만 리턴

 

변경 전, register()

//Swagger UI에서 출력하는 메시지
@ApiOperation(value = "Replies POST", notes = "POST 방식으로 댓글등록")
//consumes : 메서드를 받아서 사용하는 데이터가 어떤 데이터인지 명시하는 속성
@PostMapping(value = "/", consumes = MediaType.APPLICATION_JSON_VALUE)
//댓글 등록 메서드로, ReplyDTO를 전달받아 키,값 객체인 ResponseEntity로 반환한다.
public ResponseEntity<Map<String, Long>> register(@RequestBody ReplyDTO replyDTO){

    log.info(replyDTO);
    Map<String,Long> resultMap=Map.of("rno", 111L);

    return ResponseEntity.ok(resultMap);
}

 

변경 후, register()

//Swagger UI에서 출력하는 메시지
@ApiOperation(value = "Replies POST", notes = "POST 방식으로 댓글등록")
//consumes : 메서드를 받아서 사용하는 데이터가 어떤 데이터인지 명시하는 속성
@PostMapping(value = "/", consumes = MediaType.APPLICATION_JSON_VALUE)
//댓글 등록 메서드로, ReplyDTO를 전달받아 키,값 객체인 ResponseEntity로 반환한다.
public Map<String, Long> register(@Valid @RequestBody ReplyDTO replyDTO, BindingResult bindingResult)
throws BindException {

    log.info(replyDTO);
    if(bindingResult.hasErrors()){
        throw new BindException(bindingResult);
    }
    Map<String,Long> resultMap=new HashMap<>();
    resultMap.put("rno", 111L);

    return resultMap;
}

-> Swagger UI를 이용하면 DTO 형식에 맞는 JSON 양식을 보고 테스트를 수행할 수 있다.

반응형