
“URL은 자원, HTTP Method는 동사, 응답 JSON은 자원의 표현”
이 조합으로 CRUD를 표현하는 것이 REST 스타일.
/orders/1, /users/10{ "key": "value" },로 구분[], 객체 {} 중첩 가능{
"name": "홍길동",
"age": 18,
"address": "서울특별시"
}
이런 REST 스타일을 잘 지켜서 만든 API를 보통 “RESTful하다”라고 표현.
핵심 규칙
/users/create 보다는 POST /users가 좋음세부 규칙
/ 슬래시는 계층 관계 표현/users/1/orders/ 붙이지 않기/users (O), /users/ (지양)GET /orders/2 + Accept: application/json/users/{userId}/orders@RestController
@RequestMapping("/response")
public class ResponseRestController {}
@Controller + @ResponseBody 조합@GetMapping("/hello")
public String helloworld() {
return "hello world";
}
결과
Hello World!
@GetMapping("/random")
public int getRandomNumber() {
return (int) (Math.random() * 10) + 1;
}
@AllArgsConstructor
@Getter
public class Message {
private int httpStatusCode;
private String message;
}
@GetMapping("/message")
public Message getMessage() {
return new Message(200, "메세지를 응답합니다.");
}
객체의 결과는 아래와 같이 JSON 형태로 반환된다.
{
"httpStatusCode": 200,
"message": "메시지를 응답합니다."
}
@GetMapping("/list")
public List<String> getList() {
return List.of("사과", "바나나", "복숭아");
}
결과
[
"사과",
"바나나",
"복숭아"
]
@GetMapping("/listMessage")
public List<Message> getList() {
list.of(
new Message(200, "메시지를 응답합니다."),
new Message(201, "메시지1를 응답합니다."),
new Message(202, "메시지2를 응답합니다.")
);
}
결과
[
{
"httpStatusCode": 200,
"message": "메시지를 응답합니다."
},
{
"httpStatusCode": 201,
"message": "메시지1를 응답합니다."
},
{
"httpStatusCode": 202,
"message": "메시지2를 응답합니다."
}
]
@GetMapping("map")
public Map<Integer, String> getMap() {
List<Message> messageList = new ArrayList<>();
messageList.add(new Message(200, "정상 응답"));
messageList.add(new Message(404, "페이지를 찾을 수 없습니다"));
messageList.add(new Message(500, "개발자의 잘못입니다"));
return messageList.stream()
.collect(Collectors.toMap(Message::getHttpStatusCode, Message::getMessage));
}
@GetMapping(value="/image", produces=MediaType.IMAGE_PNG_VALUE)
public byte[] getImage() throws IOException {
return getClass()
.getResourceAsStream("/images/sample.PNG")
.readAllBytes();
}
produces로 content-type을 이미지로 지정해야 브라우저에서 이미지로 인식.@GetMapping("/entity")
public ResponseEntity<Message> getEntity() {
return ResponseEntity.ok(new Message(123, "hello world"));
}
GET 할 때 헤더 정보까지 가져오고 싶다면, 이런 방식(JSON넘기기/문자열 값)으론 안됨
상태 코드값, 본문을 넘기려면 ResponseEntity를 통해서 넘겨주어야 한다.
{
"httpStatusCode": 123,
"message": "hello world"
}
이런식으로 httpStatusCode 까지 불러올 수 있는 것.
공통 DTO들:
@AllArgsConstructor
@Getter
@Setter
public class UserDTO {
private int no;
private String id;
private String pwd;
private String name;
private Date enrollDate;
}
@AllArgsConstructor
@Getter
@Setter
public class ResponseMessage {
private int httpStatus;
private String message;
private Map<String, Object> results;
}
컨트롤러 기본 구조:
@RestController
@RequestMapping("/entity")
public class ResponseEntityTestController {
private List<UserDTO> users;
public ResponseEntityTestController() {
users = new ArrayList<>();
users.add(new UserDTO(1, "user01", "pass01", "홍길동", new Date()));
users.add(new UserDTO(2, "user02", "pass02", "유관순", new Date()));
users.add(new UserDTO(3, "user03", "pass03", "이순신", new Date()));
}
}
유저 목록 조회 상황이므로 GET HTTP method로 설정한다. ResponseEntity를 구성하는 응답 바디, 응답 헤더, 응답 상태 코드를 ResponseEntity 생성자를 통해 전달한다.
@GetMapping("/users")
public ResponseEntity<ResponseMessage> findAllUsers() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(new MediaType("application", "json", Charset.forName("UTF-8")));
Map<String, Object> responseMap = new HashMap<>();
responseMap.put("users", users);
ResponseMessage responseMessage =
new ResponseMessage(200, "조회 성공", responseMap);
return new ResponseEntity<>(responseMessage, headers, HttpStatus.OK);
}
유저 조회 상황이므로 GET HTTP method로 설정한다. @PathVariable 로 유저 번호를 받는다. ResponseEntity를 구성하는 응답 바디, 응답 헤더, 응답 상태 코드를 빌더 패턴 형태로 작성한다.
@GetMapping("/users/{userNo}")
public ResponseEntity<ResponseMessage> findUserByNo(@PathVariable int userNo) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(new MediaType("application", "json", Charset.forName("UTF-8")));
UserDTO foundUser = users.stream()
.filter(user -> user.getNo() == userNo)
.toList().get(0);
Map<String, Object> responseMap = new HashMap<>();
responseMap.put("user", foundUser);
return ResponseEntity
.ok()
.headers(headers)
.body(new ResponseMessage(200, "조회 성공", responseMap));
}
유저 등록 상황이므로 POST HTTP method를 사용한다.
@RequestBody 어노테이션으로 클라이언트 측에서 JSON 형태로 넘어온 데이터를 전달 받는다. Controller라 클라이언트랑 소통해야하니 RequestBody / ResponseBody를 통해 소통하는데, 등록하는 과정이니 Request 형식이고, RequestBody를 통해 원하는 값을 JSON형식으로 받아옴.
조회 시에는 200번 코드를 응답하지만 삽입 시에는 201번 코드를 응답한다. 201 번 코드는 요청이 성공적으로 처리 되었으며, 자원이 생성 되었음을 나타내는 성공 상태 응답 코드이다. 해당 자원에 대한 요청 url을 location으로 설정하여 응답한다.
@PostMapping("/users")
public ResponseEntity<?> registUser(@RequestBody UserDTO newUser) {
int lastUserNo = users.get(users.size() - 1).getNo();
newUser.setNo(lastUserNo + 1);
newUser.setEnrollDate(new Date());
users.add(newUser);
return ResponseEntity
.created(URI.create("/entity/users/" + newUser.getNo()))
.build(); // body 없이 201 Created 응답
}
@PutMapping("/users/{userNo}")
public ResponseEntity<?> modifyUser(
@PathVariable int userNo, @RequestBody UserDTO modifyInfo) {
UserDTO foundUser = users.stream()
.filter(user -> user.getNo() == userNo)
.toList().get(0);
foundUser.setId(modifyInfo.getId());
foundUser.setPwd(modifyInfo.getPwd());
foundUser.setName(modifyInfo.getName());
return ResponseEntity
.created(URI.create("/entity/users/" + userNo))
.build();
}
@DeleteMapping("/users/{userNo}")
public ResponseEntity<?> removeUser(@PathVariable int userNo) {
UserDTO foundUser = users.stream()
.filter(user -> user.getNo() == userNo)
.toList().get(0);
users.remove(foundUser);
return ResponseEntity
.noContent() // 204
.build();
}
주요 어노테이션
Requestparam 하나씩
modelattribute :폼데이터 가져오기
RequestBody : 문자열, 객체 modelattribute등으로 가져옴
예외 클래스:
public class UserNotFoundException extends RuntimeException{
public UserNotFoundException(String msg) {
super(msg);
}
}
에러 응답 DTO:
@AllArgsConstructor
@Getter
@Setter
public class ErrorResponse {
private String code;
private String description;
private String detail;
}
전역 예외 처리(@ControllerAdvice):
@RestControllerAdvice
public class ExceptionController {
@ExceptionHandler(UserNotFoundException.class)
public ResponseEntity<ErrorResponse> handleUserRegistException(
UserNotFoundException e) {
String code = "ERROR_CODE_00000";
String description = "회원 정보 조회 실패";
String detail = e.getMessage();
return new ResponseEntity<>(
new ErrorResponse(code, description, detail),
HttpStatus.NOT_FOUND
);
}
}
사용 예시 (항상 예외 발생):
@GetMapping("/users/{userNo}")
public ResponseEntity<?> findUserByNo() throws UserNotFoundException {
boolean check = true;
if (check) {
throw new UserNotFoundException("회원 정보를 찾을 수 없습니다.");
}
return ResponseEntity.ok().build();
}
{
"code": "ERROR_CODE_00000",
"description": "회원 정보 조회 실패",
"detail": "❌회원 정보를 찾을 수 없습니다."
}
의존성:
implementation 'org.springframework.boot:spring-boot-starter-validation'
DTO에 검증 어노테이션 추가:
@AllArgsConstructor
@Getter
@Setter
public class UserDTO {
private int no;
@NotNull(message = "아이디는 반드시 입력 되어야 합니다.")
@NotBlank(message = "아이디는 공백일 수 없습니다.")
private String id;
private String pwd;
@NotNull(message = "이름은 반드시 입력 되어야 합니다.")
@Size(min = 2, message = "이름은 2글자 이상이어야 합니다.")
private String name;
@Past
private Date enrollDate;
}
컨트롤러에서 검증 활성화:
@PostMapping("/users")
public ResponseEntity<?> registUser(@Validated @RequestBody UserDTO user) {
System.out.println(user);
return ResponseEntity
.created(URI.create("/valid/users/" + "userNo"))
.build();
}
검증 실패 시 발생하는 MethodArgumentNotValidException 처리:
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> methodValidException(
MethodArgumentNotValidException e) {
String code = "";
String description = "";
String detail = "";
if (e.getBindingResult().hasErrors()) {
detail = e.getBindingResult().getFieldError().getDefaultMessage();
String bindResultCode =
e.getBindingResult().getFieldError().getCode();
switch (bindResultCode) {
case "NotNull":
code = "ERROR_CODE_00001";
description = "필수 값이 누락되었습니다.";
break;
case "NotBlank":
code = "ERROR_CODE_00002";
description = "필수 값이 공백으로 처리되었습니다.";
break;
case "Size":
code = "ERROR_CODE_00003";
description = "알맞은 크기의 값이 입력되지 않았습니다.";
break;
}
}
ErrorResponse errorResponse =
new ErrorResponse(code, description, detail);
return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST);
}
컨트롤러 기본:
@RestController
@RequestMapping("/hateoas")
public class HateoasTestController {
private List<UserDTO> users;
public HateoasTestController() {
users = new ArrayList<>();
users.add(new UserDTO(1, "user01", "pass01", "홍길동", new Date()));
users.add(new UserDTO(2, "user02", "pass02", "유관순", new Date()));
users.add(new UserDTO(3, "user03", "pass03", "이순신", new Date()));
}
}
전체 조회 + 링크 추가:
@GetMapping("/users")
public ResponseEntity<ResponseMessage> findAllUsers() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(
new MediaType("application", "json", Charset.forName("UTF-8")));
List<EntityModel<UserDTO>> userWithRel =
users.stream().map(
user -> EntityModel.of(
user,
linkTo(
methodOn(HateoasTestController.class)
.findUserByNo(user.getNo())
).withSelfRel(), // 자신 링크
linkTo(
methodOn(HateoasTestController.class)
.findAllUsers()
).withRel("users") // 전체 목록 링크
)
).collect(Collectors.toList());
Map<String, Object> responseMap = new HashMap<>();
responseMap.put("users", userWithRel);
ResponseMessage responseMessage =
new ResponseMessage(200, "조회 성공", responseMap);
return new ResponseEntity<>(responseMessage, headers, HttpStatus.OK);
}
@GetMapping("/users/{userNo}")
public ResponseEntity<ResponseMessage> findUserByNo(@PathVariable int userNo) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(
new MediaType("application", "json", Charset.forName("UTF-8")));
UserDTO foundUser = users.stream()
.filter(user -> user.getNo() == userNo)
.toList().get(0);
Map<String, Object> responseMap = new HashMap<>();
responseMap.put("user", foundUser);
return ResponseEntity
.ok()
.headers(headers)
.body(new ResponseMessage(200, "조회 성공", responseMap));
}
응답 예시에서 각 user마다:
"links": [
{
"rel": "self",
"href": "http://localhost:8001/hateoas/users/1"
},
{
"rel": "users",
"href": "http://localhost:8001/hateoas/users"
}
]
처럼 링크 배열이 붙음.
의존성:
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0'
application.yml:
springdoc:
packages-to-scan: com.ohgiraffers.restapi.section05
default-consumes-media-type: application/json;charset=UTF-8
default-produces-media-type: application/json;charset=UTF-8
enable-hateoas: false
SwaggerConfig:
@Configuration
public class SwaggerConfig {
@Bean
public OpenAPI openAPI() {
return new OpenAPI().info(swaggerInfo());
}
private Info swaggerInfo() {
return new Info()
.title("Ohgiraffers API")
.description("SpringBoot Swagger 연동 테스트")
.version("1.0.0");
}
}
@Tag : API 그룹 이름/설명@Operation : 각 API 메소드의 요약, 설명@ApiResponses, @ApiResponse : 응답 코드 설명@Schema : DTO 필드 설명컨트롤러 예시:
@Tag(name = "Spring Boot Swagger 연동 (user)")
@RestController
@RequestMapping("/swagger")
public class SwaggerTestController {
private List<UserDTO> users;
public SwaggerTestController() {
users = new ArrayList<>();
users.add(new UserDTO(1, "user01", "pass01", "홍길동", new Date()));
users.add(new UserDTO(2, "user02", "pass02", "유관순", new Date()));
users.add(new UserDTO(3, "user03", "pass03", "이순신", new Date()));
}
@Operation(
summary = "전체 회원 조회",
description = "전체 회원 목록을 조회한다."
)
@GetMapping("/users")
public ResponseEntity<ResponseMessage> findAllUsers() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(
new MediaType("application", "json", StandardCharsets.UTF_8));
Map<String, Object> responseMap = new HashMap<>();
responseMap.put("users", users);
ResponseMessage responseMessage =
new ResponseMessage(200, "조회 성공", responseMap);
return new ResponseEntity<>(responseMessage, headers, HttpStatus.OK);
}
@Operation(
summary = "회원번호로 회원 조회",
description = "회원번호를 통해 해당하는 회원 정보를 조회한다."
)
@GetMapping("/users/{userNo}")
public ResponseEntity<ResponseMessage> findUserByNo(@PathVariable int userNo) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(
new MediaType("application", "json", StandardCharsets.UTF_8));
UserDTO foundUser = users.stream()
.filter(user -> user.getNo() == userNo)
.toList().get(0);
Map<String, Object> responseMap = new HashMap<>();
responseMap.put("user", foundUser);
return ResponseEntity
.ok()
.headers(headers)
.body(new ResponseMessage(200, "조회 성공", responseMap));
}
@Operation(summary = "신규 회원 등록")
@PostMapping("/users")
public ResponseEntity<?> registUser(@RequestBody UserDTO newUser) {
int lastUserNo = users.get(users.size() - 1).getNo();
newUser.setNo(lastUserNo + 1);
users.add(newUser);
return ResponseEntity
.created(URI.create("/entity/users/" + newUser.getNo()))
.build();
}
@Operation(summary = "회원정보 수정")
@PutMapping("/users/{userNo}")
public ResponseEntity<?> modifyUser(
@PathVariable int userNo, @RequestBody UserDTO modifyInfo) {
UserDTO foundUser = users.stream()
.filter(user -> user.getNo() == userNo)
.toList().get(0);
foundUser.setId(modifyInfo.getId());
foundUser.setPwd(modifyInfo.getPwd());
foundUser.setName(modifyInfo.getName());
return ResponseEntity
.created(URI.create("/entity/users/" + userNo))
.build();
}
@Operation(summary = "회원정보 삭제")
@ApiResponses({
@ApiResponse(responseCode = "204", description = "회원정보 삭제 성공"),
@ApiResponse(responseCode = "400", description = "잘못 입력된 파라미터")
})
@DeleteMapping("/users/{userNo}")
public ResponseEntity<?> removeUser(@PathVariable int userNo) {
UserDTO foundUser = users.stream()
.filter(user -> user.getNo() == userNo)
.toList().get(0);
users.remove(foundUser);
return ResponseEntity.noContent().build();
}
}
DTO에 @Schema:
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
@Schema(description = "회원정보 DTO")
public class UserDTO {
@Schema(description = "회원번호(PK)")
private int no;
@Schema(description = "회원 ID")
private String id;
@Schema(description = "회원 비밀번호")
private String pwd;
@Schema(description = "회원 성명")
private String name;
@Schema(description = "회원 등록일")
private Date enrollDate;
}
접속:
http://localhost:포트/swagger-ui/index.html에서 모든 API를 문서 + 테스트 UI로 확인 가능.