[Spring] REST API

배창민·2025년 10월 30일
post-thumbnail

REST API

한 번에 보는 REST → JSON → 응답 타입 → ResponseEntity → Validation → HATEOAS → Swagger


1. REST API

1-1. REST란?

  • REpresentational State Transfer: HTTP 위에서 리소스 지향으로 상태를 주고받는 아키텍처 스타일.
  • URL로 자원(Resource)을 식별하고, HTTP Method로 행위(Verb)를 표현.

1-2. 핵심 개념

  • ROA(Resource Oriented Architecture): 리소스 중심 설계.
  • 표현(Representation): JSON/XML/TEXT 등으로 동일 리소스를 다양한 포맷으로 전달(요즘은 대부분 JSON).

1-3. HTTP Method ↔ CRUD

Method의미
GET조회(Read)
POST생성(Create)
PUT전체 수정(Update)
DELETE삭제(Delete)

1-4. 특징 (RESTful)

  • 무상태(Stateless), 캐시 가능(Cacheable), 자체 표현(Self-Descriptive), 계층화(Layered), 유니폼 인터페이스(Uniform Interface).

1-5. 설계 규칙(요약)

  • URI는 리소스 명사형으로, 행위는 메소드로.
  • 슬래시(/)는 계층, 마지막 슬래시 금지.
  • 소문자 사용, 하이픈(-) 권장, 언더스코어(_) 지양.
  • 확장자 금지(콘텐츠 협상: Accept 헤더).
  • 연관 리소스: /users/{id}/orders.

1-6. JSON 요약

왜 JSON?

  • XML보다 경량·개발자 친화적, 파싱 용이, 네트워크 전송 효율적.

기본 규칙

  • "key": value 쌍, 콤마(,) 구분.
  • key는 문자열, 값은 문자열/숫자/불리언/배열/객체.
{
  "name": "홍길동",
  "age": 18,
  "address": "서울특별시"
}

2. Spring @RestController 응답 타입 모음

@RestController
@RequestMapping("/response")
public class ResponseRestController {}

2-1. 문자열

@GetMapping("/hello")
public String hello() { return "hello world"; }

2-2. 기본형

@GetMapping("/random")
public int random() { return (int)(Math.random()*10)+1; }

2-3. 객체(Object → JSON)

@AllArgsConstructor @Getter
class Message { int httpStatusCode; String message; }

@GetMapping("/message")
public Message getMessage() { return new Message(200,"메세지"); }

2-4. List

@GetMapping("/list")
public List<String> getList(){ return List.of("사과","바나나","복숭아"); }

2-5. Map

@GetMapping("/map")
public Map<Integer,String> getMap(){
  return Map.of(200,"정상",404,"없음",500,"서버오류");
}

2-6. 이미지(byte[])

@GetMapping(value="/image", produces=MediaType.IMAGE_PNG_VALUE)
public byte[] image() throws IOException {
  return getClass().getResourceAsStream("/images/sample.PNG").readAllBytes();
}

2-7. ResponseEntity

@GetMapping("/entity")
public ResponseEntity<Message> entity(){
  return ResponseEntity.ok(new Message(123,"hello world"));
}

3. ResponseEntity 실전 패턴

공통 DTO

@AllArgsConstructor @Getter @Setter
class UserDTO { int no; String id; String pwd; String name; Date enrollDate; }

@AllArgsConstructor @Getter @Setter
class ResponseMessage { int httpStatus; String message; Map<String,Object> results; }

3-1. 목록 조회 (헤더+바디+상태)

@GetMapping("/users")
public ResponseEntity<ResponseMessage> findAll(){
  HttpHeaders headers = new HttpHeaders();
  headers.setContentType(new MediaType("application","json", StandardCharsets.UTF_8));
  Map<String,Object> body = Map.of("users", users);
  return new ResponseEntity<>(new ResponseMessage(200,"조회 성공", body), headers, HttpStatus.OK);
}

3-2. 단건 조회 (빌더)

@GetMapping("/users/{userNo}")
public ResponseEntity<ResponseMessage> findOne(@PathVariable int userNo){
  UserDTO user = users.stream().filter(u->u.getNo()==userNo).toList().get(0);
  return ResponseEntity.ok()
    .contentType(new MediaType("application","json", StandardCharsets.UTF_8))
    .body(new ResponseMessage(200,"조회 성공", Map.of("user", user)));
}

3-3. 등록 (201 + Location)

@PostMapping("/users")
public ResponseEntity<?> create(@RequestBody UserDTO newUser){
  newUser.setNo(users.getLast().getNo()+1);
  newUser.setEnrollDate(new Date());
  users.add(newUser);
  return ResponseEntity.created(URI.create("/entity/users/"+newUser.getNo())).build();
}

3-4. 수정 (PUT → 201 or 204)

@PutMapping("/users/{userNo}")
public ResponseEntity<?> modify(@PathVariable int userNo, @RequestBody UserDTO req){
  UserDTO user = users.stream().filter(u->u.getNo()==userNo).toList().get(0);
  user.setId(req.getId()); user.setPwd(req.getPwd()); user.setName(req.getName());
  return ResponseEntity.created(URI.create("/entity/users/"+userNo)).build();
}

3-5. 삭제 (204 No Content)

@DeleteMapping("/users/{userNo}")
public ResponseEntity<?> remove(@PathVariable int userNo){
  users.removeIf(u->u.getNo()==userNo);
  return ResponseEntity.noContent().build();
}

4. Validation & Exception Handling

4-1. 사용자 예외 + 전역 처리

class UserNotFoundException extends Exception {
  public UserNotFoundException(String msg){ super(msg); }
}
@AllArgsConstructor @Getter
class ErrorResponse { String code; String description; String detail; }

@ControllerAdvice
class ExceptionController {
  @ExceptionHandler(UserNotFoundException.class)
  public ResponseEntity<ErrorResponse> handle(UserNotFoundException e){
    return new ResponseEntity<>(
      new ErrorResponse("ERROR_CODE_00000","회원 정보 조회 실패", e.getMessage()),
      HttpStatus.NOT_FOUND);
  }
}

4-2. Bean Validation

의존성

implementation 'org.springframework.boot:spring-boot-starter-validation'

DTO에 제약

@AllArgsConstructor @Getter @Setter
class UserDTO {
  int no;
  @NotNull(message="아이디는 반드시 입력") @NotBlank(message="아이디는 공백 불가") String id;
  String pwd;
  @NotNull(message="이름 필수") @Size(min=2, message="이름 2글자 이상") String name;
  @Past Date enrollDate;
}

Controller에서 활성화

@PostMapping("/users")
public ResponseEntity<?> create(@Validated @RequestBody UserDTO user){ ... }

검증 예외 처리

@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValid(MethodArgumentNotValidException e){
  String code="", desc="", detail="";
  if(e.getBindingResult().hasErrors()){
    var fe = e.getBindingResult().getFieldError();
    detail = fe.getDefaultMessage();
    switch (fe.getCode()){
      case "NotNull" -> { code="ERROR_CODE_00001"; desc="필수 값 누락"; }
      case "NotBlank"-> { code="ERROR_CODE_00002"; desc="공백 입력"; }
      case "Size"    -> { code="ERROR_CODE_00003"; desc="크기 불일치"; }
    }
  }
  return new ResponseEntity<>(new ErrorResponse(code, desc, detail), HttpStatus.BAD_REQUEST);
}

5. HATEOAS

응답에 링크를 포함해 다음 행동을 스스로 탐색하도록 하는 하이퍼미디어 원칙.

의존성

implementation 'org.springframework.boot:spring-boot-starter-hateoas'

예시

@GetMapping("/users")
public ResponseEntity<ResponseMessage> findAll(){
  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")
    )
  ).toList();

  return ResponseEntity.ok(new ResponseMessage(200,"조회 성공", Map.of("users", userWithRel)));
}

6. Swagger(OpenAPI)

API 문서 자동화 + 브라우저에서 바로 테스트.

의존성

implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0'

설정

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

OpenAPI Bean

@Configuration
public class SwaggerConfig {
  @Bean
  public OpenAPI openAPI(){
    return new OpenAPI().info(new Info()
      .title("Ohgiraffers API")
      .description("SpringBoot Swagger 연동 테스트")
      .version("1.0.0"));
  }
}

컨트롤러 문서화 포인트

@Tag(name="Spring Boot Swagger 연동 (user)")
@Operation(summary="전체 회원 조회", description="전체 회원 목록을 조회한다.")
@GetMapping("/users") ...

접속: http://{host}:{port}/swagger-ui/index.html


핵심 정리

  • URI는 명사, 행위는 HTTP 메소드로 분리.
  • 상태코드 정확히 사용: 200/201/204/400/404/500 등.
  • ResponseEntity로 헤더/상태/바디를 명시적으로 제어.
  • DTO + Bean Validation으로 입력 검증.
  • 예외는 전역 처리(@ControllerAdvice) 로 일관성 있게.
  • 필요 시 HATEOAS로 탐색성 제공.
  • Swagger(OpenAPI) 로 문서·테스트 자동화.
profile
개발자 희망자

0개의 댓글