IT 강좌(IT Lectures)/SpringBoot

15강. RESTful 웹 서비스

소울입니다 2024. 7. 28. 08:00
728x90
반응형

 

챕터 15: RESTful 웹 서비스

15.1 REST API 설계 원칙

15.1.1 RESTful 설계 원칙

REST(Representational State Transfer)는 2000년 Roy Fielding의 박사 논문에서 처음 소개된 아키텍처 스타일로, 웹의 장점을 최대한 활용하기 위한 설계 원칙을 제공합니다. RESTful 설계 원칙은 다음과 같습니다:

  • 무상태성(Statelessness): 서버는 클라이언트의 상태를 저장하지 않습니다.
  • 캐시 가능(Cacheable): 응답은 캐시 가능해야 합니다.
  • 통일된 인터페이스(Uniform Interface): 일관된 방식으로 자원에 접근해야 합니다.
  • 클라이언트-서버 구조(Client-Server): 클라이언트와 서버는 서로 독립적으로 동작합니다.
  • 계층형 시스템(Layered System): 시스템은 계층화될 수 있습니다.
  • 코드 온 디맨드(Code on Demand): 서버는 클라이언트에게 실행 가능한 코드를 전송할 수 있습니다.

15.1.2 URI 설계 가이드라인

RESTful URI 설계 가이드라인은 다음과 같습니다:

  • 명사 사용: 자원은 명사를 사용하여 나타냅니다. 예: /users, /orders
  • 계층적 구조: URI는 계층적 구조를 가집니다. 예: /users/{userId}/orders
  • 소문자 사용: URI는 소문자를 사용합니다. 예: /users
  • 하이픈 사용: 단어 구분에는 하이픈(-)을 사용합니다. 예: /user-profiles

관련된 Java 개념: Java Servlets, JAX-RS (Java API for RESTful Web Services)

15.2 Spring MVC를 이용한 RESTful 서비스 개발

15.2.1 컨트롤러 작성

Spring MVC를 사용하여 RESTful 서비스를 개발할 수 있습니다. 아래는 컨트롤러 작성 예제입니다:


package com.example.demo;

import org.springframework.web.bind.annotation.*;

import java.util.ArrayList;
import java.util.List;

@RestController
@RequestMapping("/users")
public class UserController {

    private List users = new ArrayList<>();

    // 모든 사용자 목록을 반환하는 메서드
    @GetMapping
    public List getAllUsers() {
        return users;
    }

    // 특정 사용자를 반환하는 메서드
    @GetMapping("/{id}")
    public User getUser(@PathVariable int id) {
        return users.stream()
                    .filter(user -> user.getId() == id)
                    .findFirst()
                    .orElse(null);
    }

    // 새로운 사용자를 생성하는 메서드
    @PostMapping
    public User createUser(@RequestBody User user) {
        users.add(user);
        return user;
    }

    // 사용자를 업데이트하는 메서드
    @PutMapping("/{id}")
    public User updateUser(@PathVariable int id, @RequestBody User userDetails) {
        User user = users.stream()
                         .filter(u -> u.getId() == id)
                         .findFirst()
                         .orElse(null);
        if (user != null) {
            user.setName(userDetails.getName());
            user.setEmail(userDetails.getEmail());
        }
        return user;
    }

    // 사용자를 삭제하는 메서드
    @DeleteMapping("/{id}")
    public void deleteUser(@PathVariable int id) {
        users.removeIf(user -> user.getId() == id);
    }
}
    

package com.example.demo;

public class User {
    private int id;
    private String name;
    private String email;

    // Getter와 Setter 메서드
    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }
}
    

위 예제는 사용자를 관리하는 RESTful 서비스를 제공합니다. 기본적인 CRUD(Create, Read, Update, Delete) 기능을 포함하고 있습니다.

15.2.2 요청과 응답 처리

Spring MVC는 요청과 응답을 쉽게 처리할 수 있는 기능을 제공합니다. @RequestBody를 사용하여 요청 본문을 객체로 변환하고, @ResponseBody를 사용하여 객체를 응답 본문으로 변환할 수 있습니다.


package com.example.demo;

import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api")
public class ApiController {

    // 요청 본문을 객체로 변환하여 처리하는 메서드
    @PostMapping("/data")
    public Data processData(@RequestBody Data data) {
        // 데이터 처리 로직
        data.setValue(data.getValue() * 2);
        return data;
    }
}

class Data {
    private int value;

    // Getter와 Setter 메서드
    public int getValue() {
        return value;
    }

    public void setValue(int value) {
        this.value = value;
    }
}
    

위 예제는 요청 본문을 객체로 변환하여 처리하고, 응답 본문으로 객체를 반환합니다.

15.3 데이터 직렬화와 역직렬화

15.3.1 JSON과 XML 직렬화

Spring Boot는 Jackson 라이브러리를 사용하여 JSON 직렬화와 역직렬화를 지원합니다. 또한, Jackson XML 확장을 통해 XML 직렬화와 역직렬화도 지원합니다.


package com.example.demo;

import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;

import java.io.IOException;

@RestController
@RequestMapping("/serialize")
public class SerializeController {

    // JSON 데이터를 직렬화하는 메서드
    @GetMapping(value = "/json", produces = MediaType.APPLICATION_JSON_VALUE)
    public Data getJsonData() {
        return new Data(100);
    }

    // XML 데이터를 직렬화하는 메서드
    @GetMapping(value = "/xml", produces = MediaType.APPLICATION_XML_VALUE)
    public Data getXmlData() {
        return new Data(200);
    }

    // JSON 데이터를 역직렬화하는 메서드
    @PostMapping(value = "/json", consumes = MediaType.APPLICATION_JSON_VALUE)
    public Data deserializeJsonData(@RequestBody Data data) {
        return data;
    }

    // XML 데이터를 역직렬화하는 메서드
    @PostMapping(value = "/xml", consumes = MediaType.APPLICATION_XML_VALUE)
    public Data deserializeXmlData(@RequestBody String xml) throws IOException {
        XmlMapper xmlMapper = new XmlMapper();
        return xmlMapper.readValue(xml, Data.class);
    }
}

class Data {
    private int value;

    public Data() {}

    public Data(int value) {
        this.value = value;
    }

    // Getter와 Setter 메서드
    public int getValue() {
        return value;
    }

    public void setValue(int value) {
        this.value = value;
    }
}
    

위 예제는 JSON과 XML 데이터를 직렬화하고 역직렬화하는 방법을 보여줍니다.

15.3.2 커스텀 직렬화 방법

Spring Boot는 커스텀 직렬화 방법을 제공하여 특정 요구사항에 맞게 데이터를 직렬화할 수 있습니다. 이를 위해 Jackson 라이브러리의 @JsonSerialize와 @JsonDeserialize 어노테이션을 사용할 수 있습니다.


package com.example.demo;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.io.IOException;

@RestController
@RequestMapping("/custom-serialize")
public class CustomSerializeController {

    // 커스텀 직렬화된 데이터를 반환하는 메서드
    @GetMapping
    public CustomData getCustomData() {
        return new CustomData("Example", 123);
    }
}

@JsonSerialize(using = CustomDataSerializer.class)
class CustomData {
    private String name;
    private int value;

    public CustomData(String name, int value) {
        this.name = name;
        this.value = value;
    }

    // Getter와 Setter 메서드
    public String getName() {
        return name;
    }

    public int getValue() {
        return value;
    }
}

class CustomDataSerializer extends JsonSerializer {

    @Override
    public void serialize(CustomData customData, JsonGenerator gen, SerializerProvider serializers) throws IOException {
        gen.writeStartObject();
        gen.writeStringField("custom_name", customData.getName());
        gen.writeNumberField("custom_value", customData.getValue());
        gen.writeEndObject();
    }
}
    

위 예제는 커스텀 직렬화 클래스를 작성하고 이를 적용하는 방법을 보여줍니다. CustomDataSerializer는 CustomData 객체를 특정 형식으로 직렬화합니다.

15.4 HATEOAS와 HAL 적용

15.4.1 HATEOAS 개념

HATEOAS(Hypermedia As The Engine Of Application State)는 RESTful API의 클라이언트가 서버와 상호작용하는 방법을 동적으로 결정할 수 있도록 하며, 서버가 제공하는 하이퍼미디어 링크를 통해 가능한 액션을 설명합니다.

역사적 배경:

  • HATEOAS는 2000년 Roy Fielding의 박사 논문에서 REST의 핵심 요소 중 하나로 소개되었습니다.
  • HATEOAS는 클라이언트와 서버의 느슨한 결합을 촉진하여, API의 유연성과 확장성을 높이는 데 기여합니다.

15.4.2 HAL 적용 방법

HAL(Hypertext Application Language)은 하이퍼미디어 링크를 포함하는 JSON 또는 XML 형식입니다. Spring HATEOAS 라이브러리를 사용하여 HAL을 적용할 수 있습니다.


package com.example.demo;

import org.springframework.hateoas.EntityModel;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.server.mvc.WebMvcLinkBuilder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.*;

@RestController
@RequestMapping("/hateoas")
public class HATEOASController {

    @GetMapping("/data")
    public EntityModel getData() {
        Data data = new Data(300);
        Link selfLink = linkTo(methodOn(HATEOASController.class).getData()).withSelfRel();
        return EntityModel.of(data, selfLink);
    }
}
    

위 예제는 Spring HATEOAS를 사용하여 HAL 형식의 응답을 생성하는 방법을 보여줍니다. EntityModel을 사용하여 데이터와 링크를 함께 반환합니다.

추가 예제 코드

예제 1: 페이징 및 정렬을 지원하는 RESTful 서비스

Spring Data를 사용하여 페이징 및 정렬을 지원하는 RESTful 서비스를 작성하는 예제입니다.


package com.example.demo;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.web.PageableDefault;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/paged-users")
public class PagedUserController {

    @Autowired
    private UserRepository userRepository;

    @GetMapping
    public Page getPagedUsers(@PageableDefault(size = 5, sort = "name", direction = Sort.Direction.ASC) Pageable pageable) {
        return userRepository.findAll(pageable);
    }
}
    

위 예제는 Spring Data의 페이징 및 정렬 기능을 사용하여 페이징된 사용자 목록을 반환하는 방법을 보여줍니다.

예제 2: 예외 처리를 위한 글로벌 예외 핸들러

글로벌 예외 처리를 위한 @ControllerAdvice를 사용하는 예제입니다.


package com.example.demo;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.servlet.ModelAndView;

@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(ResourceNotFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public ModelAndView handleResourceNotFoundException(ResourceNotFoundException ex) {
        ModelAndView modelAndView = new ModelAndView("error");
        modelAndView.addObject("message", ex.getMessage());
        return modelAndView;
    }
}
    

예제 3: 조건부 요청을 처리하는 RESTful 서비스

조건부 요청(예: If-Modified-Since 헤더)을 처리하는 RESTful 서비스를 작성하는 예제입니다.


package com.example.demo;

import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Date;

@RestController
@RequestMapping("/conditional")
public class ConditionalController {

    @GetMapping("/data")
    public ResponseEntity getData(@RequestHeader(value = HttpHeaders.IF_MODIFIED_SINCE, required = false) Date ifModifiedSince) {
        Instant lastModified = Instant.now().minus(1, ChronoUnit.DAYS);
        if (ifModifiedSince != null && lastModified.isBefore(ifModifiedSince.toInstant())) {
            return ResponseEntity.status(HttpStatus.NOT_MODIFIED).build();
        }
        Data data = new Data(400);
        return ResponseEntity.ok()
                             .lastModified(Date.from(lastModified).getTime())
                             .body(data);
    }
}
    

위 예제는 If-Modified-Since 헤더를 사용하여 조건부 요청을 처리하는 방법을 보여줍니다. 데이터가 수정되지 않은 경우 304 Not Modified 상태 코드를 반환합니다.

반응형