[인프런] Spring Boot를 이용한 RESTful Web Services 개발 - User Service API 구현

June·2021년 5월 11일
0

User 도메인 클래스 생성

Step 7 - Creating User Bean and user Service

이전에 진행한 코드 패키지 구분을 해준다.

user 패키지 아래에 다음 것들을 추가해준다.

User

package com.example.restfulwebservice.user;

import java.util.Date;
import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class User {
    private Integer id;
    private String name;
    private Date joinDate;
}

int id가 아닌 Integer id를 사용하는 이유는 원시값 사용을 하지 않기 위해서다.
원시값 포장과 래퍼

UserDaoService

package com.example.restfulwebservice.user;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import org.springframework.stereotype.Service;

@Service
public class UserDaoService {

    private static List<User> users = new ArrayList<>();

    private static int userCount = 3;

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

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

    public User save(User user) {
        if (user.getId() == null) {
            user.setId(++userCount);
        }

        users.add(user);
        return user;
    }

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

비즈니스 로직 관련된 부분은 Service 클래스에 주로 추가한다. 데이터베이스와 관련된 코드를 다루기 위해서 Dao 클래스를 이용한다. Service로 사용할 것이기 때문에 Service 어노테이션을 추가했다.

사용자 목록 조회를 위한 API 구현 - GET HTTP Method

UserController

package com.example.restfulwebservice.user;

import java.util.List;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class UserController {
    private UserDaoService service;

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

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

    // GET /users/1 .. /users/10 -> 서버에는 String으로 전달됨
    @GetMapping("/users/{id}")
    public User retrieveUser(@PathVariable int id) {
        return service.findOne(id);
    }
}

생성자를 통한 방식으로 의존성을 주입하고 있다. Service는 @Service 컴포넌트를 붙여놔서 빈으로 등록되었기 때문에 가능하다.

retrieveUser 함수에서 인자로 int형을 받고 있는데 원래는 서버에서 String으로 받는데 int로 받겠다고 하면 전환해준다.

사용자 등록을 위한 API 구현 - POST HTTP Method

Step9 - Implementing POST Method to create User Resource

UserController

    @PostMapping("/users")
    public void createUser(@RequestBody User user) {
        User savedUser = service.save(user);
    }

post method, put method에서 form data가 아닌 json, xml같은 object 형태 데이터를 받으려면 @RequestBody를 선언해줘야 한다. 클라이언트에서 전달하는 데이터포맷을 일치하는 선언했던 필드에 매핑시켜 전달해준다.

HTTP Status Code 제어

지금까지는 GET이나 POST나 똑같이 응답코드 200이 왔다. 가능하면 응답코드가 구분되면 좋겠다.

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

        URI location = ServletUriComponentsBuilder.fromCurrentRequest()
            .path("/{id}")
            .buildAndExpand(savedUser.getId())
            .toUri();

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

이제 POSTMAN에서 POST를 하면 201 상태코드를 반환한다.

서버로부터 적당한 결과값에 따른 status code를 전달해주는 것이 좋은 코드이다.

생성된 아이디를 클라이언트가 알기 위해서는 서버에서 또 물어봐야하는데, POST method결과값으로 아이디를 주면 네트워크를 줄일 수 있다.

ServletUriComponentBuilder 참고글

좋은 REST API를 만드는데 매우 중요한듯하다!

HTTP Status Code 제어를 위한 Exception Handling

존재하지 않는 데이터를 조회하는데 status code가 200 ok이다. 이는 문제가 될 수 있다.

    // GET /users/1 .. /users/10 -> 서버에는 String으로 전달됨
    @GetMapping("/users/{id}")
    public User retrieveUser(@PathVariable int id) {
        return service.findOne(id);
    }

이 함수를 고쳐서 결과 값이 null이면 다르게 작동하게 해보자. 기존 코드를 나누기 위해 return 문에서 Ctrl + Alt + v를 눌러보자.

UserController

    // GET /users/1 .. /users/10 -> 서버에는 String으로 전달됨
    @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));
        }

        return user;
    }

UserNotFoundException

package com.example.restfulwebservice.user;

public class UserNotFoundException extends RuntimeException {

    public UserNotFoundException(String message) {
        super(message);
    }
}

extends throwable이 아닌 RuntimeException을 상속받았다.

이제 없는 데이터를 조회하려하면 500번대 코드를 내놓는다. 하지만 문제는 서버에 어떤 코드에서 문제가 발생했는지 다 보여주는 것은 적절하지 않다.

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

@ResponseStatus(HttpStatus.NOT_FOUND)
public class UserNotFoundException extends RuntimeException {

    public UserNotFoundException(String message) {
        super(message);
    }
}

앞으로 이 예외가 발생하면 500번대가 아닌 404번 에러를 보여준다.

Spring의 AOP를 이용한 Exception Handling

ExceptionResponse

package com.example.restfulwebservice.exception;

import java.util.Date;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class ExceptionResponse {
    private Date timestamp;
    private String message;
    private String details;
}

모든 컨트롤러에서 사용할 수 있는 일반화된 예외 클래스다.

ResponseEntityHandler라는 클래스를 상속받은 HandlerClass를 만들건데, 예외가 발생할 때 handler 예외 클래스가 발생하도록 해줄 것이다. 스프링에서 모든 비즈니스 로직에서 공통적으로 처리해줘야 할 것이 있다면 AOP를 사용해서 처리해줄 수 있다. 예외 처리 핸들러도 AOP 기능을 이용할 것이다.

CustomizedResponseEntityExceptionHandler

package com.example.restfulwebservice.exception;

import java.util.Date;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;

@RestController
@ControllerAdvice
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);
    }
}

ControllerAdvice라는 어노테이션을 추가하면 모든 컨트롤러가 실행될 때, 이 빈이 실행된다. 에러가 발생하면 Handler가 실행되는 것이다. 즉 모든 예외를 여기서 처리하는 것이다.

없는 데이터를 조회하면 이렇게 보여진다.

    @ExceptionHandler(UserNotFoundException.class)
    public final ResponseEntity<Object> handleUserNotFoundExceptions(Exception ex, WebRequest request) {
        ExceptionResponse exceptionResponse = new ExceptionResponse(new Date(), ex.getMessage(),
            request.getDescription(false));

        return new ResponseEntity(exceptionResponse, HttpStatus.NOT_FOUND);
    }

이 함수를 추가하면 UserNotFoundExceptoin에 대해서는 이 클래스가 처리한다.

UserDaoService

    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;
    }

사용자 삭제를 위한 함수를 추가해준다. 여기에서는 iterator를 이용해봤다.

UserController

    @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));
        }
    }

삭제를 하고 나서 200번대를 받는다.

0개의 댓글