SpringMVC_RESTAPI (2)

이유준·2024년 6월 5일

Backend_School

목록 보기
4/6
post-thumbnail

1. JSON/XML 데이터 처리

JSON과 XML 처리는 웹 API 개발에서 중요한 부분이다. Spring Framework는 다양한 방법으로 JSON과 XML 데이터 형식을 지원하는데, 개발자는 이를 통해 RESTful 서비스를 구현할 때 클라이언트와 서버 간에 데이터를 쉽게 교환할 수 있다.

그럼 각각 JSON과 XML 데이터를 처리하는 방법을 알아보도록 하자.

1.1 JSON 데이터 처리

1.1.1 Jackson 라이브러리 :

  • Spring MVC는 Jackson 라이브러리를 사용하여 JSON 데이터를 자바 객체로 직렬화하거나 역 직렬화한다. 이는 @RestController나 @ResponseBody 어노테이션이 붙은 메서드에서 자동으로 처리된다.

  • Ex. 클라이언트가 JSON 형식의 데이터를 POST 요청으로 보내면, 해당 JSON 데이터는 자바 객체로 변환되어 메서드 파라미터로 전달된다. 반대로, 메서드가 자바 객체를 반환하면 이 객체는 JSON으로 자동 변환되어 클라이언트에게 응답된다.

    • 클라이언트 -> (POST) 데이터 (JSON) => 자바 객체 (변환) -> 메서드 파라미터
    • 메서드 -> 자바 객체 => 데이터 (JSON 변환) -> 클라이언트

1.1.2 @RequestBody와 @ResponseBody :

  • @RequestBody : HTTP 요청(request) 본문을 자바 객체로 매핑 가능.

  • @ResponseBody : 메서드에 사용하면 반환값을 HTTP 응답(response) 본문에 쓰고, 이 데이터는 JSON 형식으로 클라이언트에 전송된다.

@RequestBody와 @ResponseBody에 관한 내용은 SpringMVC_RESTAPI(1) 작성 글에서도 언급하였으니 필요하면 참고해도 좋을 것 같다.

1.2 XML 데이터 처리

1.2.1 JAXB 라이브러리 :

  • XML 데이터 처리를 위해 Spring은 JAXB(Java Architecture for XML Binding) 라이브러리를 사용한다.

  • JAXB를 통해 자바 객체와 XML 사이의 매핑이 가능하다.

@XmlRootElement, @XmlElement 등의 어노테이션을 사용하여 클래스와 필드를 XML 요소에 매핑할 수 있다.

1.2.2 Content Negotiation :

  • Spring MVC에서는 Accept 헤더를 기반으로 클라이언트가 요청한 데이터 형식(JSON or XML)에 따라 응답 형식을 결정할 수 있다. (이를 "콘텐츠 협상"이라고도 한다)

  • @RequestMapping의 produces 속성을 통해 특정 컨트롤러 메서드가 생성할 수 있는 응답 타입을 지정할 수 있다.

위의 내용을 이렇게 줄글로만 보면 이해가 어려우니 예제 코드를 보며 이해해 보도록 하자.

1.3 JSON/XML 데이터 처리 예제

예제 코드를 작성하기 전, build.gradle에 추가해야 하는 내용이 있다.

implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml'

위 내용을 build.gradle의 dependencies 부분에 추가해야 한다. 참고로, 위 문장은 Jackson 라이브러리의 XML 데이터 포맷을 지원하는 모듈을 Maven이나 Gradle 프로젝트에 추가하기 위한 의존성 선언 부분이다. 이 의존성을 추가하면 Jackson을 사용하여 XML 데이터를 파싱하고 생성할 수도 있다.

우선, controller를 작성하기 전에, 메서드에 필요한 domain Product를 생성해 준다.

@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class Product {
    @Id
    private Long id;
    private String name;
    private double price;
}
@RestController
public class ProductAsJsonOrXmlController {
    @GetMapping(value = "/productjson/{id}", produces = "application/json")
    public Product getProductAsJson(@PathVariable("id") Long id){
        return new Product(id, "Example Product", 9.99);
    }

    @GetMapping(value = "/productxml/{id}", produces = "application/xml")
    public Product getProductAsXml(@PathVariable("id") Long id){
        return new Product(id, "Example Product", 9.99);
    }
}

getProductAsJson(), getProductAsXml() 모두 데이터를 Read 하는 것이기 때문에 @GetMapping을 사용하였다.

  • value에는 URL 값으로, 매핑 조건인 "/productjson/{id}", "/productxml/{id}"을 넣어주었고,

  • produces는 HTTP 응답 헤더의 Content-Type을 "application/json", "application/xml"로 변환해 주었다.

localhost:8080/productjson/{id}와 localhost:8080/productxml/{id}를 주소창에 입력하면, 다음과 같이 각각 json/xml 형식의 데이터를 확인할 수 있다.

🤔@PostMapping 없이(Create 없이) 어떻게 id에 해당하는 값을 넣어줬을까?

평소에는 @GetMapping에 해당하는 메서드가 있으면, @PostMapping 메서드를 작성해서 Read 하기 전에 Create를 먼저 해줬다. 그래야 Read할 값이 존재하기 때문이다. 그렇다면 이번 예제에서는 Create에 해당하는 메서드 없이 어떻게 Read 했을 때 값이 있었을까?

답은 getProductAsJson()과 getProductAsXml() 메서드의 return 값에 있다. Product의 생성자를 lombok의 @AllArgsConstructor을 추가해 줬고, return 값을을 new Product(id, "Example Product", 9.99) 이런 식으로 했기 때문에, Read 할 때 수동으로 값을 넣어준(생성자로) 것이다.

이를 확인해 보기 위해 다음과 같이 생성자의 값들을 바꾸고, localhost:8080/productjson/2, localhost:8080/productxml/2에 접속해 보았다.

id = 2에 해당하는 데이터들을 확인할 수 있다.

참고로, 생성자의 값을 바꾸지 않고 주소창의 id 값을 바꾸어도 데이터가 읽어지긴 하나, 데이터의 값은 이전과 동일하다. 따라서 return에서 생성자로 값을 넣어주는 것은 좋아 보이지는 않는다.

2. ResponseEntity와 HTTP 상태 코드 관리

ResponseEntity에서는 강의 들으면서 질문들이 많았다. 강사님이 설명 안 하시고 넘어간 부분이 많으셨고, 빠르게 넘어간 부분이 많으니 유의 깊게 복습하고 넘어가도록 하자.

2.1 ResponseEntity

ResponseEntity란, HttpEntity를 상속받는, 결과 데이터와 HTTP 상태 코드를 직접 제어할 수 있는 클래스이다.

HttpEntity는 요청 혹은 응답에 해당하는 HttpHeader와 HttpBody를 포함하는 클래스이다. 이러한 HttpEntity 클래스를 상속받아 구현한 클래스가 RequestEntity, ResponseEntity인 것이다. 코드로 보면 다음과 같다.

public class HttpEntity<T> {
	private final HttpHeaders headers;

	@Nullable
	private final T body;
}
public class RequestEntity<T> extends HttpEntity<T>
public class ResponseEntity<T> extends HttpEntity<T>

ResponseEntity에는 사용자의 HttpRequest에 대한 응답 데이터가 포함된다. 또한, HTTP 아키텍처 형태에 맞게 Response를 보내주는 것에도 의미가 있다.

Error 코드와 같은 HTTP 상태 코드를 전송하고 싶은 데이터와 함께 전송할 수 있기 때문에, 조금 더 세밀한 제어가 필요한 경우에 사용한다고 한다.

ResponseEntity 클래스는 Spring MVC에서 HTTP 요청에 대항 응답을 구성할 때 사용되는데, HTTP 상태 코드(HttpStatus), 헤더(HttpHeaders), 응답 본문(HttpBody)을 포함하여 HTTP 응답을 전체적으로 제어할 수 있게 해준다. ResponseEntity의 생성자의 순서를 보면 <body, header, status> 순의 생성자가 만들어진다.

  • status 메서드 : 상태 코드 설정
  • body 메서드 : 응답 본문 설정
  • headers 메서드 : HTTP 헤더 추가

이를 통해 개발자는 RESTful API를 보다 세밀하게 조정할 수 있으며, 클라이언트에게 명확한 상태 정보와 데이터를 제공할 수 있다.

2.2 ResponseEntity는 왜 쓰는 걸까?

지금까지 배운 RestController의 return 값은 대부분 Object 타입이었는데, 객체를 return 하면 HTTP 응답을 제어할 수가 없다. 하지만, REST API를 만든다면 우리는 클라이언트와 서버 간의 통신에 필요한 정보를 제공해야 한다. 이럴 때 ResponseEntity를 사용한다면, 적절한 생태 코드와 응답 헤더, 응답 본문을 생성해서 클라이언트에게 보내줄 수 있는 것이다.

ResponseEntity를 사용하면, 클라이언트가 요청의 결과를 더 잘 이해하고 적절히 대응할 수 있도록 돕는다. API 사용자는 HTTP 상태 코드를 통해 요청이 성공했는지, 실패했는지, 그리고 어떤 조치를 취해야 하는지 쉽게 판단할 수 있다는 장점이 있다.

2.3 ResponseEntity 예제

ResponseEntity를 어떻게 쓰는지 알아보기 위해 MemoRestController class를 살펴보도록 하자.

@RestController
@RequestMapping("/api/memos")
public class MemoRestController {
    private final Map<Long, String> memos = new HashMap<>();
    private final AtomicLong counter = new AtomicLong();

    @PostMapping
    public ResponseEntity<Long> createMemo(@RequestBody String content) {
        Long id = counter.incrementAndGet();
        memos.put(id, content);
        return ResponseEntity.ok(id);
    }

    @GetMapping("/{id}")
    public ResponseEntity<String> getMemo(@PathVariable("id") Long id) {
        String memo = memos.get(id);
        if (memo == null) {
            return ResponseEntity.notFound().build();
        }
        return ResponseEntity.ok(memo);
    }

    @PutMapping("/{id}")
    public ResponseEntity<String> updateMemo(@PathVariable("id") Long id, @RequestBody String content) {
        if (!memos.containsKey(id)) {
            return ResponseEntity.notFound().build();
        }
        memos.put(id, content);
        return ResponseEntity.ok("Memo update successfully!!");
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<String> deleteMemo(@PathVariable("id") Long id) {
        String removeMemo = memos.remove(id);
        if(removeMemo == null){
            return ResponseEntity.notFound().build();
        }
        return ResponseEntity.ok("Memo delete successfylly!!");
    }
}

MemoRestController는 다음과 같이 구성되어 있다. 메모 관리에 필요한 기본적인 CRUD 작업을 수행하고, 실제 DB 대신 HashMap을 사용하여 데이터를 저장한다.

  • private final Map<Long, String> memos = new HashMap<>();

    • DB 대신에 HashMap을 사용하여 메모 데이터를 메모리에 저장하였다.
  • private final AtomicLong counter = new AtomicLong();

    • id 값을 자동으로 증가시키고 얻어오기 위해 AtomicLong을 사용하였다.

  • POST /api/memos

    • 새로운 메모를 생성한다. 메모 내용은 요청 본문에서 받아오고, 생성된 메모의 ID를 반환한다.
  • GET /api/memos/{id}

    • ID에 해당하는 메모를 조회한다. 메모가 존재하지 않을 경우 404 Not Found를 반환한다.
  • PUT /api/memos/{id}

    • ID에 해당하는 메모의 내용을 업데이트한다. 메모가 존재하지 않을 경우 404 Not Found를 반환한다.
  • DELETE /api/memos/{id}

    • ID에 해당하는 메모를 삭제한다. 메모가 존재하지 않을 경우 404 Not Found를 반환한다.

아래는 MemoRestController를 test 하기 위한 memo.http의 내용이다. 참고하도록 하자.

### POST
POST http://localhost:8080/api/memos
Content-Type: application/json

{
  "memo": "luready memo2"
}
### GET
GET http://localhost:8080/api/memos/1
Accept: application/json

### PUT
PUT http://localhost:8080/api/memos/1
Content-Type: application/json

{
  "content": "luready memo1"
}

### DELETE
DELETE http://localhost:8080/api/memos/1
Accept: application/json
  • POST, PUT의 경우는 Content-Type을 application/json으로, GET, DELETE의 경우는 Accept을 application/json으로 설정해 주었다.

예외 처리와 에러 핸들링

예외 처리와 에러 핸들링은 Spring MVC에서 중요한 부분을 차지하며, API가 안정적이고 사용자에게 유용한 피드백을 제공하는 데 필수적이기에, 살펴보고 넘어가 보자.

3. 예외 처리 예제

@RestControllerAdvice
public class RestControllerExceptionHandler {
    @ExceptionHandler(Exception.class)
    public ResponseEntity<String> handleException(Exception e){
        return ResponseEntity
                .status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body("An error occurred");
    }

}
@RestController
public class ExampleController {
    @GetMapping("/api/error")
    public String apierror(){
        throw new RuntimeException("API Error");
    }
}

ResponseEntity는 에러 핸들링에서도 유용하다. 커스텀 예외를 처리하고, 클라이언트에 적절한 HTTP 상태 코드와 에러 메시지를 반환할 수 있다. 본 예제에서는 status로 500 Internal Server Error을, body로는 "An error occurred"를 return 하였다.

ExampleController의 내용은 이해를 위해 넣어두었지만, 단순히 /api/error 페이지에서 error를 발생시키기 위한 코드이다.

기타

자주 사용되는 HTTP 상태 코드

상태 코드는 100번대부터 500번대까지 매우 다양하지만, 참고용으로 주로 사용되는 상태 코드를 정리해 보았다.

  • HttpStatus.OK: 200 OK
  • HttpStatus.CREATED: 201 Created
  • HttpStatus.NO_CONTENT: 204 No Content
  • HttpStatus.BAD_REQUEST: 400 Bad Request
  • HttpStatus.UNAUTHORIZED: 401 Unauthorized
  • HttpStatus.FORBIDDEN: 403 Forbidden
  • HttpStatus.NOT_FOUND: 404 Not Found
  • HttpStatus.INTERNAL_SERVER_ERROR: 500 Internal Server Error

ResponseEntity를 잘 쓰는 방법

예제에서 사용해 보지는 않았지만, ResponseEntity는 생성자 패턴을 통해서도 return 될 수 있다. Intellij에서 ResponseEntity.class를 살펴보면, 실제로 매우 다양한 생성자를 정의해두었다.

※참고로, ResponseEntity(body)만 return 하면 나머지 인자들에 null이 들어간다.

return new ResponseEntity(body, headers, HttpStatus.valueOf(200));

그럼에도 왜 우리는 빌더 패턴으로 작성하고 있었을까?

그 이유는 ResponseEntity.class에 ResponseEntity.ok()와 같이 정적 팩토리 메서드가 구현되어 있는데, 체이닝으로 연결한 빌더 패턴을 사용하는 것이 의미가 더 직관적이고 유지 보수에 좋기 때문이다.

status? header? body?

앞서 언급했듯, ResponseEntity는 HTTP status, header, body을 포함하여 HTTP 응답을 전체적으로 제어할 수 있게 해준다. 그렇다면 statsu, header, body HTTP 응답은 어떻게 보일까?

1. 응답 상태 코드 (status)

  • 응답 상태 코드(ex. 200)
  • 메시지

2. Header

  • 응답에 대한 부가적인 정보

3. Body (본문)

  • 서버에서 클라이언트로 전송되는 데이터

4. 파일 업로드 & 다운로드

FileController 예제

@RestController
public class FileController {
    @GetMapping("/download")
    public void downloadFile(HttpServletResponse response) {
        Path filePath = Paths.get("c:/temp/upload/cat.jpg");
        response.setContentType("image/jpeg");
        try{
            InputStream inputStream = Files.newInputStream(filePath);
            StreamUtils.copy(inputStream, response.getOutputStream());
            response.flushBuffer();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
    @PostMapping("/upload")
    public ResponseEntity<String> handlerFileUpload(
            @RequestParam("file") MultipartFile file,
            @RequestPart("info") UploadInfo uploadInfo) {
        String message = "";
        System.out.println(file.getOriginalFilename() + "===============");
        System.out.println(uploadInfo.getDescription() + "===============");
        System.out.println(uploadInfo.getTag() + "===============");
        try {
            InputStream inputStream = file.getInputStream();
            StreamUtils.copy(inputStream, new FileOutputStream("c:/temp/upload/" + file.getOriginalFilename()));

            message = "You successfully uploaded " + file.getOriginalFilename() + "!";
            return ResponseEntity.ok().body(message);
        } catch (IOException e) {
            message = "FAIL to upload " + file.getOriginalFilename() + "!";
            return ResponseEntity.badRequest().body(message);
        }
    }
}

FilController class의 GET 부분은 file을 upload 하기 위한 메서드이고, POST 부분은 file을 download 하기 위한 메서드이다.

4.1 File Upload

먼저, 파일을 업로드 받을 수 있는 REST API를 구현하기 위해 @RestController와 MultipartFile을 활용한다. MultipartFile 인터페이스는 Spring에서 파일 업로드를 처리할 때 사용된다.

  • InputStream inputStream = file.getInputSream();

    • 파일로부터 스트림을 읽어와서 inputStream에 저장한다.
  • StreamUtils.copy(inputStream, new FileOutputStream("c:/temp/upload/" + file.getOriginalFilename()));

    • StreamUtils : 스트림을 처리하기 위한 간단한 유틸리티 메서드. 입력 스트림에서 출력 스트림으로 데이터를 효율적으로 복사한다.
  • StreamUtils.copy(InputStream in, OutputStream out) : 주어진 InputStream의 내용을 주어진 OutputStream에 복사한다.

    • 우리는 파일로부터 읽어온 스트림을, new FileOutputStream을 통해 복사하였다.

    • FileOutPutStream(String fileName) : fileName의 파일을 쓰기 위한 객체를 생성한다.

  • 파일 업로드에 성공하면 ResponseEntity.ok().body(message)를 return, 실패하면 ResponseEntity.badRequest().body(message)를 return 한다.

4.2 File Download

위 코드에서 downloadFile 메서드는 HTTP 응답으로 파일을 직접 보내는데, 파일의 경로는 실제 필요에 따라 지정해야 한다. 우리는 "c:/temp/upload/cat.jpg" 경로를 지정해 주었다.

  • HttpServletResponse response

    • HttpServletResponse : HTTP 응답 정보(요청 처리 결과)를 제공하는 인터페이스

      • Servlet은 HttpServletResponse 객체에 Content-Type, 응답 코드, 응답 메세지 등을 담아서 전송한다.

        • Servlet : 동적 웹페이지 구현을 할 수 있도록 도와주는 자바 클래스의 일종
  • response.setContentType()

    • Content-Type을 image/jpeg로 설정해준다.
  • response.flushBuffer();

    • 버퍼에 저장되어 있는 내용을 클라이언트에 전송해준다.

위의 예제 코드를 다음의 file.http로 test하여 c:/temp/cat.jpg를 c:/temp/upload/cat.jpg로 업로드, 다운로드 하는 실습을 진행하였다.

🤔중간에 사용된 @RequestPart는 뭘까?

@RequestPart는 HTTP request body에 multipart/form-data가 포함되어 있는 경우에 사용하는 어노테이션이다. multipart/form-data 요청의 일부를 메서드 인수와 연결하는 데 사용된다. multipart 요청에서 "info" 라는 이름을 가진 파트를 추출하겠다는 것이다.

메소드 인수가 String 또는 multipartFile/Part가 아닌 경우

  • 요청 헤더의 Content-Type에 해당하는 HttpMessageonverter로 변환을 시도한다.

사용되는 곳

  • JSON, XML 등을 포함하는 복잡한 내용의 Part와 함께 사용된다.

오늘을 마치며


어제는 새롭게 배운 내용이 없다는 말을 남겼는데, 오늘은 그와 정반대였다. 허겁지겁 강의 내용을 따라가기 바빴고, 회고 시간에 다른 분들께 여쭤보니 다들 마찬가지라는 말에 나만 그런 것이 아니라는, 조금의 안심을 얻었다.

오랜 시간이 걸렸지만, 그래도 정리를 하니 오늘 배운 내용은 머리에 정리가 많이 된 기분이다. JSON과 XML 데이터 타입을 처리하는 방법을 익혔고, 가장 많은 ResponseEntity도 결과 데이터와 HTTP 상태 코드를 직접 처리하기 위해 사용한다는 것도 익혔다.

아까 회고 시간에 ResponseEntity의 return이 어떻게 이루어지는 지에 대한 의문을 제시했고, 내일 회고 시간에 이에 대해 얘기해보기로 했으니 오늘 익혀두고 얘기 나눠보면 좋을 것 같다.

0개의 댓글