Programming/Spring & Springboot

[REST] RESTful Service 구현방법 #2(SpringBoot와 RESTFUL Service)

오늘도출근하는다람쥐 2023. 10. 22. 22:10

RESTFul Service 구현방법 #1Permalink

RESTFul 서비스 SpringBoot를 통해서 구현하기 때문에 기본적인 어노테이션에 대해서는 학습 필요

1장. SpringBoot 어노테이션 간단 설명Permalink

@RestController : 일반적인 Controller와 달리 response를 자동으로 JSON 타입으로 반환

@RestController //자동으로 JSON 타입으로 반환
public class HelloWorldController {
}

일반 Controller를 사용할 경우 아래처럼 @ResponseBody를 붙여주면 @RestController와 동일한 결과 발생

@Controller
public class HelloWorldController{
    @GetMapping(path="/hello-world")
    @ResponseBody
    public String helloWorld(){

    }
}

@GetMapping : Get호출이 들어오는 것을 처리하는 어노테이션

    // GET(/hello-world)
    // 옛날 방식 : @RequestMapping(method=RequestMethod.GET, path="/hello-world")
    @GetMapping(path="/hello-world")
    public String helloWorld(){
        return "Hello World";
    }

객체 타입 반환 방법 : 반환 타입에 객체(Object) 넣어주고 return을 해당 객체로 반환

    @GetMapping(path="/hello-world-bean")
    public HelloWorldBean helloWorldBean(){
        return new HelloWorldBean("Hello World");
    }

    ================================================================
    @Data //Data만 해주면 모든 프로퍼티에 대한 세터, 게터, 투스트링 만들어진다
    @AllArgsConstructor //모든 아규먼트를 가진 생성자를 만든다
    @NoArgsConstructor //아규먼트가 없는 기본 생성자를 만든다
    public class HelloWorldBean {
        private String message;

    }
    ================================================================

@PathVariable : URI에 변수가 들어가는 부분

    @GetMapping(path="/hello-world-bean/path-variable/{name}") 
    //path-variable {name} 이름과 파라미터 name 일치. 만약 다를경우 value="name" 설정 필요
    public HelloWorldBean helloWorldBean(@PathVariable String name){
        return new HelloWorldBean(String.format("Hello World, %s", name));
    }
    
    /*
     TEST URL : http://localhost:8088/hello-world-bean/path-variable/icehotchoco
     => RESULT : "message": "Hello World, giyeonPak"
    */

2장. UserDaoService 구현Permalink

SpringBoot에서 사용할 Service 간단 구현

@Service
public class UserDaoService {
    //비즈니스 로직 = service
    private static List<User> users = new ArrayList<>();

    private static int usersCount = 3;

    static {
        users.add(new User(1, "Gildong", new Date()));
        users.add(new User(2, "Gildong", new Date()));
        users.add(new User(3, "Gildong", new Date()));
    }

    public List<User> findAll(){
        return users;
    }

    public User save(User user){
        if(user.getId() == null){
            user.setId(++usersCount);
        }
        users.add(user);
        return user;
    }

    public User findOne(int id){
        for(User user : users){
            if(user.getId() == id){
                return user;
            }
        }
        return null;
    }

    public User deleteById(int id){
        Iterator<User> iterator = users.iterator();

        while(iterator.hasNext()){
            User user = iterator.next();

            if(user.getId() == id){
                iterator.remove();
                return user;
            }
        }
        return null;
    }
}

3장. SpringBoot UserController 구현Permalink

생성자 주입 방식 : 위에서 만들었던 UserDaoService 주입

@RestController
public class UserController {
    private UserDaoService service;

    public UserController(UserDaoService service){
        this.service = service;
    }
}

RESTFul API 구현

@GetMapping(“/users”)

    @GetMapping("/users")
    public List<User> retriveAllUsers(){
        return service.findAll();
    }

@GetMapping(“/users/{id}”) - 기본방식

// GET /users/1 or /users/10 
// 기본적으로 String 타입이 들어오지만 (아래와 같이 int id)를 해주면 자동으로 int로 변환
    @GetMapping("/users/{id}")
    public User retrieveUser_temp(@PathVariable int id){
        return service.findOne(id);
    }

@GetMapping(“/users/{id}”) - 에러처리 추가

    @GetMapping("/users/{id}")
    public User retrieveUser(@PathVariable int id){
        User user = service.findOne(id);
        if(user == null){
            throw new UserNotFoundException(String.format("[ID[%s] not found]", id));
             //이렇게만 하면 500번 에러로 message에 첨부되서 넘어간다. 
        }
        return user;
    }

    
    ================================================================
    //새로운 예외클래스 생성
    @ResponseStatus(HttpStatus.NOT_FOUND) //400번대 에러 -> 사용자가 찾는 ID가 없다는 에러(클라이언트)
    public class UserNotFoundException extends RuntimeException {
        public UserNotFoundException(String message) {
            super(message);
        }
    }
    ================================================================

@PostMapping(“/users”) - 기본 방식

    @PostMapping("/users")
    public void createUser(@RequestBody User user){ 
        //Post, PUT Method에서 클라이언트에서 Form과 같은 데이터를 XML, JSON 
        //같은 데이터를 받기 위해서는 RequestBody 필요(자동으로 매핑)
        User savedUser = service.save(user);
    }

[주의사항] : 포스트맨으로 사용시 POST방식에 Body는 raw 선택 후 JSON 타입으로 전송

Content type ‘text/plain;charset=UTF-8’ not supported] 에러발생시 type을 text 아니라 json으로 전송해야 한다

@PostMapping(“/users”) - 에러처리 추가 + ResponseEntity 사용방법

    @PostMapping("/users")
    public ResponseEntity<User> createUser(@RequestBody User user){ 
        User savedUser = service.save(user);

        //이렇게 상태값을 전송해주는 방법이 REST API에서는 굉장히 중요하다
        URI location = ServletUriComponentsBuilder.fromCurrentRequest()
                .path("/{id}")
                .buildAndExpand(savedUser.getId())
                .toUri();

        return ResponseEntity.created(location).build();

        /**
         * Response Entity 사용이유
         * 이렇게 사용해주면 POSTMAN에서 요청후에 Header 정보에 클라이언트에서는 알 수 없었던 
         * 서버에서 생성한 id값을 넘겨주게 되는데
         * id 값을 받았으니, 클라이언트는 id값을 알기위해 새로 서버에 요청할 필요가 없어지니까
         * 네트워크 트래픽 감소와 효율성을 높일 수 있다
         */

        //안좋은 REST API 케이스 : 클라이언트의 모든 요청을 POST로 사용하면서 응답코드는 200번만 받는 경우. 
        //REST는 용도별로 나눠서 GET, POST, PUT, DELETE 사용하며 응답코드도 꼭 같이 리턴해주면서 상태값을 
        //확인 할 수 있게 해주자(200번 X , 201번 O)
    }

@DeleteMapping(“/users/{id}”) - 예외처리 추가

    @DeleteMapping("/users/{id}")
    public void deleteUser(@PathVariable int id){
        User user = service.deleteById(id);

        if(user == null){
            throw new UserNotFoundException(String.format("[ID[%s] not found]", id));
        }
    }

4장. 예외처리 구현Permalink

ExceptionResponse 객체 구현 : 앞으로 발생하는 예외는 이 객체에 데이터를 담아서 반환(에러 확인이 편함)

@Data
@AllArgsConstructor
@NoArgsConstructor
public class ExceptionResponse { //모든 예외처리는 이걸로 사용 + AOP 적용
    private Date timestamp;
    private String message;
    private String details;
}

Handler 구현 : ControllerAdvice 사용해서 모든 수행과정을 Handler를 통해서 예외처리 할 수 있게

@RestController //1. 웹서비스 사용
@ControllerAdvice //2. 모든 컨트롤러가 실행될때 자동으로 사용되게 되며 에러가 발생할 경우 Handler를 사용
public class CustomizedResponseEntityExceptionHandler extends ResponseEntityExceptionHandler {

    @ExceptionHandler(Exception.class) //모든 예외가 발생하면 아래 메소드 수행
    public final ResponseEntity<Object> handleAllExceptions(Exception ex, WebRequest request){
        ExceptionResponse exceptionResponse = new ExceptionResponse(new Date(), ex.getMessage(), request.getDescription(false));

        return new ResponseEntity(exceptionResponse, HttpStatus.INTERNAL_SERVER_ERROR);
         //INTERNAL_SERVER_ERROR = 500번 에러
         //(서버에서 발생할 수 있는 가장 일반적인 에러)
    }

    //에러 발생시 아래처럼 에러정보 표시(훨씬 간단하며 모든 에러처리 가능)
    /*
    {
        "timestamp": "2023-01-28T02:04:39.889+00:00",
        "message": "[ID[100] not found]",
        "details": "uri=/users/100"
    }
     */

    @ExceptionHandler(UserNotFoundException.class) //특정 에러가 발생하면 아래 메소드 수행
    public final ResponseEntity<Object> handleUserNotFoundException(Exception ex, WebRequest request){
        ExceptionResponse exceptionResponse = new ExceptionResponse(new Date(), ex.getMessage(), request.getDescription(false));

        return new ResponseEntity(exceptionResponse, HttpStatus.NOT_FOUND); //NOT_FOUND = 400번 에러(사용자가 찾는 ID가 없다는 에러)
    }
}

참고

  1. 인프런 - SpringBoot를 이용한 RESTful 개발