우선 사용자 데이터를 나타내기 위해 User 도메인을 추가하겠습니다. 여기서 도메인주소가 아닌 전문분야에서 사용되어지는 전문 지식을 뜻합니다. 어떤 사용자 정보(데이터)를 저장할 지를 구조화합니다.
User 클래스를 생성하기 위해 package를 생성하고 User 클래스를 생성하고 아래와 같이 선언합니다. lombok을 통해 클래스가 가져야 할 생성자나 setter, getter 메서드 등을 정의합니다.
package com.example.restfulwebservice1.user;
import lombok.AllArgsConstructor;
import lombok.Data;
import java.util.Date;
@Data
@AllArgsConstructor
public class User {
private Integer id;
private String name;
private Date joinDate;
}
그리고 UserDaoService 클래스를 생성합니다. 추가한 User 도메인을 통해 해당 클래스에서 사용자 정보 CRUD등의 비즈니스 로직을 처리할 수 있으며 보통 @@Service라는 이름으로 명명합니다.
// .../restfulwebservice1/user/UserDaoService.java
package com.example.restfulwebservice1.user;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
public class UserDaoService {
// List<User> 가상의 메모리에 데이터 추가
private static List<User> users = new ArrayList<>();
private static int userCount = 3; //userCount를 초기화
// static으로 만들어진 변수이기 때문에 static을 사용
static {
// 매개변수는 id, name, joinDate
users.add(new User(1, "Coco", new Date()));
users.add(new User(2, "David", new Date()));
users.add(new User(3, "Cheese", new Date()));
}
// 사용자 전체 목록
public List<User> findAll() {
return users;
}
// 사용자 추가
public User save(User user) {
if (user.getId() == null) {
user.setId(++userCount); // ID값 오름차순으로 지정
}
users.add(user);
return user;
}
// 사용자 개별 데이터 전달, 고유 키값인 id를 매개변수로 전달
public User findOne(int id) {
for (User user : users) {
if (user.getId() == id) { // lombok을 사용했기 때문에 getId사용 가능
return user;
}
}
return null;
}
}
사용자의 요청을 추가할 수 있는 Controller클래스를 다루어보겠습니다. UserController클래스를 생성합니다.
이 때 개발자가 직접 인스턴스를 생성하는 것이 아닌 Spring framework에 의해 관리되도록 인스턴스를 선언하고 사용해야 합니다.
user라는 인스턴스를 생성하기 위해 private UserDaoService service = new ~;
처럼 new라는 키워드를 통해 관리하지 않습니다. 의존성 주입이라는 방법을 사용해야 합니다. Spring Container에 등록된 Bean을 사용하기 위해 Container에 등록된 Bean의 참조값을 받아와서 사용합니다.
의존성 주입
public class UserController {
private UserDaoService service;
public UserController(UserDaoService service) { // 의존성 주입
this.service = service; // 할당
}
그리고 service 용도로 사용하기 위해 UserDaoService
에 @Service
annotation을 붙입니다.
//userController
package com.example.restfulwebservice1.user;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController // RestController로 사용
public class UserController {
private UserDaoService service;
public UserController(UserDaoService service) { // 의존성 주입
this.service = service;
}
// 전체 사용자 조회
@GetMapping("/users")
public List<User> retrieveAllUsers() {
return service.findAll(); // UserDaoService_findAll()
}
// 사용자 한 명의 상세조회
@GetMapping("/users/{id}")
public User retrieveUser(@PathVariable int id) {
return service.findOne(id); // UserDaoService_findOne()
}
}
서버를 실행하면 bean으로서 userController가 등록되었고, 등록을 하면서 생성자 매개변수에다가 userDaoService값을 전달(정상적으로 의존성 주입 완료)한 것을 확인할 수 있습니다.
그리고 값이 정상적으로 잘 출력되고있음을 알 수 있습니다.
useDaoService에 사용자를 추가하기 위한 save 메서드를 추가하였습니다. controller에서 save하기 위한 메서드와 URI가 필요합니다.
// user/UserController.java
@PostMapping("/users")
public void createUser(@RequestBody User user) {
User savedUser = service.save(user);
}
POST 또는 PUT 메서드의 경우 즉 클라이언트로부터 폼데이터를 받기 위해서는 RequestBody
가 필요하며 annotation으로 달아줍니다. 그리고 service에 선언되어있는 save함수에 user라는 객체 추가합니다.
그리고 Dao에 save메서드에 전달하면 이 비즈니스 로직은 완료됩니다.
메서드 마다 목적이 다르니 응답 코드를 구분해야 합니다. 응답코드를 제어하기 위해 ServletUriComponentsBuilder
라는 클래스를 이용해 서버에서 반환하려하는 데이터 값을 ResponseEntity
에 담아서 전달하겠습니다.
사용자 추가라는 작업을 완료한 다름 어떤 URI을 가지고 추가 정보(상세 정보)를 확인할 수 있는지도 반환하려 합니다.
@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();
}
}
fromCurrentRequest()
는 현재 요청되어진 request 값을 사용한다는 의미입니다. 그리고 가변 변수로 받은 id값을 savedUser.getId()
를 통해 지정합니다. 마지막으로 이 모든 형태를 toUri()
를 통해 URI로 변형합니다.
더이상 createUser라는 메서드는 void라는 리턴 값이 아닌 ResponseEntity
라는 값을 가지고 도메인으로 지정해두었던 있는 User값을 반환합니다. 그 후에 build()
를 진행합니다.
POSTMAN을 통해 확인한 결과 Statys의 값은 201 Created임을 확인할 수 있습니다.그러나 서버에서 전달받은 respnse값이 없기 때문에, body에 아무 값이 없는 것을 확인할 수 있습니다. 그리고 5번의 유저로 생성된 것도 확인할 수 있습니다.
존재하지 않는 사용자를 요청할 때 등 예외를 다루어야 할 상황이 생깁니다. 예외 핸들링을 하지 않는다면 서버측에서는 아무런 문제가 없기 때문에 200OK를 전달할 것입니다. 이를 예외로 발생시켜 적절한 Status Code를 반환하도록 하겠습니다.
UserController
에서 return service.findOne(id);
라는 리턴을 볼 수 있습니다. 여기서 findOne 우측클릭>Selector>Introduce Variable을 클릭하면 변수명을 선택 수 코드가 분리가 되는 것을 확인할 수 있습니다.
//user/UserController.java
@GetMapping("/users/{id}")
public User retrieveUser(@PathVariable int id) {
return service.findOne(id);
User user = service.findOne(id);
if (user == null) {
throw new UserNotFoundException(String.format("ID[%s] not found", id));
}
return user;
}
만약 user가 null값이라면 UserNotFoundException
클래스를 실행하는데, 이를 위해서 해당 클래스를 생성합니다. 해당 클래스에 우측클릭을 해서 외부 클래스를 생성합니다.
//restfulwebservice1/user/UserNotFoundException.java
package com.example.restfulwebservice1.user;
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);
}
}
RuntimeException
을 상속받고, 생성자는 super(message)
를 통해 부모클래스쪽으로 전달받은 메시지를 반환합니다. 특히 @ResponseStatus(HttpStatus.NOT_FOUND)
annotation을 통해 500번대의 서버에러를 보여주는 것이 아닌 400번대(404번)의 클라이언트에서의 에러를 보여주도록 합니다. 그렇지 않으면 불필요한 오류를 보여줌으로서 보안상에 문제를 일으킬 수도 있습니다.
이번에는 이전 섹션에서 특정한 에러를 핸들링하는 것을 넘어 일반화된 예외클래스를 생성합니다. 예외 메세지, 시간, 상세 메세지 등 일반화된 객체를 선언하도록 하겠습니다.
Exception이라는 폴더에 ExceptionResponse.java
클래스를 생성합니다. 그리고 예외 발생 시간, 메세지, 정보를 넣어줍니다.
@Data // setter getter 메서드 생성
@AllArgsConstructor // 전체 모든 필드를 가지고 있는 생성자
@NoArgsConstructor // default 생성자
public class ExceptionResponse {
private Date timestamp; // 예외 발생 시간
private String message; // 예외 발생 메세지
private String details; // 예외 상세 정보
}
AOP를 통해 공통적으로 처리되어야 하는 로직, 처리해야 하는 로직을 작성합니다. exception 패키지에 CustomizedResponseEntityExceptionHandler
라는 클래스를 생성합니다. 그리고 ResponseEntityExceptionHandler
를 상속받습니다.
@RestController
@ControllerAdvice
public class CustomizedResponseEntityExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler(Exception.class) // Exception이 발생하면해당 메서드 실행
public final ResponseEntity<Object> handleAllExceptions(Exception ex, WebRequest request) {
ExceptionResponse exceptionResponse =
new ExceptionResponse(new Date(), ex.getMessage(), request.getDescription(false));
// "details": "uri=/users/10; client=127.0.0.1"
return new ResponseEntity(exceptionResponse, HttpStatus.INTERNAL_SERVER_ERROR);
}
ResponseEntity
는 이전에 사용자 객체를 추가했을 때 반환시켰던 형태의 값이며handleAllExceptions
로 메서드를 명명하였습니다. 그리고 Exception
, WebRequest
를 통해 에러 객체와 어디서 발생한지 request에 대한 정보를 파라미터로 받습니다.
그리고 exceptionResponse
라는 변수명과 Date로 언제 발생했는지, ex.getMessage()
로 어떤 메세지를 보내는지, request.getDescription(false)
로 request
가 어떤 설명을 가질 것인지 지정합니다.
그 후 500번 서버에러로ResponseEntity
에 인자로 해서 반환을 합니다.
마지막으로 @ExceptionHandler(Exception.class)
을 통해 ExceptionHandler로 사용될 수 있도록 annotaion을 지정합니다.
UserNotFoundException이 발생하게 되면 handleAllExceptions
가 아닌 해당 메서드가 발생하도록 합니다.
@ExceptionHandler(UserNotFoundException.class) // Exception이 발생하면해당 메서드 실행
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);
}
이번에는 DELETE라는 메서드를 이용해 삭제 API를 만들어보겠습니다.
삭제를 하기 위해서도 id값 하나를 가지고 있는 데이터를 가지고 삭제를 해야 합니다.
//user/UserDaoService.java
public User deleteById(int id) { // id에 의해 삭제라는 작업을 합니다.
Iterator<User> iterator = users.iterator();
while (iterator.hasNext()) {
User user = iterator.next();
if (user.getId() == id) {
iterator.remove();
return user;
}
}
return null;
}
Iterator
는 배열이나 리스트 데이터값을 순차적으로 사용하기 위한 열거형 타입의 데이터 값입니다. 그리고 while문으로 순차적으로 접근합니다. 만약 매개변수 id랑 동일한 데이터가 있다면 remove()
함수를 호출해서 삭제하고 user을 반환합니다. 그리고 이 값을 실행하지 못했다면 null을 값을 반환해 데이터를 찾지 못했다고 간주하였습니다.
아직 deleteById
는 사용되지 않았으므로 UserController에서 아래의 코드와 같이 선언합니다.
//user/UserController.java
@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));
}
} // 반환값이 void이기 때문에 추가적인 return은 하지 않아도 됨.