본문 바로가기
대외활동/DRACONIST-백엔드

스스로 공부하는 스프링부트. 10단원 REST API~12단원

by 피스타0204 2025. 2. 14.

🤚지난시간 중요 개념 요약)

서버는 dto로 클라이언트의 정보를 받아오고, 이것을 entity로 바꾸어 repository를 통해 DB에 저장합니다.

🐔10단원. REST API와 JSON

1.REST API와 JSON의 등장배경

웹 서비스를 사용하는 클라이언트에는 웹 브라우저 외에도 스마트폰, 스마트 워치등 다양한 종류가 있습니다. 기기별로 적절한 뷰 페이지를 응답해야 하는데 이럴때 REST API(Representational State Transfer API)는 서버의 자원을 클라이언트에 구애받지 않고 사용할 수 있도록 모든 기기에서 통용될 수 있는 데이터를 반환하는 방식입니다.

 

우리가 지금까지 했던 실습에서는 서버가 클라이언트의 요청에 대한 응답으로 화면을 전송했죠. REST API는 화면(view)가 아닌 데이터(data)를 전송합니다. 이때 사용하는 응답 데이터는 JSON(JavaScript Object Notation)입니다. 과거에는 응답 데이터로 XML을 많이 사용했지만 최근에는 JSON으로 통일되고 있습니다.

 

 

해당 단원에는 연습용 REST API서버에 접속해 HTTP 요처을 보내는 실습이 있었는데 번거롭고 막 자세하게 알고 싶은 것은 아니라 넘겼습니다.

 

🐔11단원. HTTP와 REST 컨트롤러

클라이언트가 보내는 HTTP요청 메시지의 첫줄에는 시작 라인인 요청 라인(request line)이 있고, 그 아래에는 header와 body(본문)이 있습니다. 서버에서 응답하는 메시지도 동일한 모양입니다.

 

서버는 응답 메시지로 상태 코드(성공시 200, 데이터 생성 완료시 201, 요청정보 못찾으면 404, 서버오류 500)를 반환합니다.

 

예외의 종류

더보기
예외 코드예외 이름(서버에서 사용)Http 코드메시지
COMMON500 _INTERNAL_SERVER_ERROR 500 (INTERNAL_SERVER_ERROR) 서버 에러, 관리자에게 문의 바랍니다.
COMMON400 _BAD_REQUEST 400 (BAD_REQUEST) 잘못된 요청입니다.
COMMON401 _UNAUTHORIZED 401 (UNAUTHORIZED) 인증이 필요합니다.
COMMON403 _FORBIDDEN 403 (FORBIDDEN) 금지된 요청입니다.
RESOURCE4041 RESOURCE_NOT_FOUND 404 (NOT_FOUND) 잘못된 API 요청입니다. 반복적인 오류 발생 시 관리자에게 문의해주세요.
USER4001 _USER_NOT_FOUND 400 (BAD_REQUEST) 해당하는 사용자를 찾을 수 없습니다.
USER4002 PASSWORD_NOT_CORRECT 403 (FORBIDDEN) 비밀번호가 일치하지 않습니다.
USER4003 _USER_IS_EXISTS 403 (FORBIDDEN) 해당하는 사용자가 이미 존재합니다.
FRIEND4041 _FRIEND_NOT_FOUND 404 (NOT_FOUND) 친구 ID에 해당하는 회원이 없습니다.
FRIEND4091 _FRIEND_ALREADY_EXISTS 409 (CONFLICT) 이미 친구로 등록된 사용자입니다.
FRIEND4092 _SELF_FRIEND_REQUEST_NOT_ALLOWED 409 (CONFLICT) 자기 자신과는 친구를 맺고 끊을 수 없습니다.
FRIEND4093 _IS_NOT_FRIEND 409 (CONFLICT) 해당 회원과 친구가 아닙니다.
LETTER4001 _LETTER_CONTENT_MISSING 400 (BAD_REQUEST) 편지 내용을 입력해 주세요.
LETTER4031 _NOT_OWNER_OF_LETTER 403 (FORBIDDEN) 해당 편지의 주인이 아닙니다.
LETTER4041 _LETTER_NOT_FOUND 404 (NOT_FOUND) 해당 편지를 찾을 수 없습니다.
LETTER502 _OPENAI_RESPONSE_NOT_RECEIVED 502 (BAD_GATEWAY) OpenAI 답변이 없습니다.
ARTICLE4001 _ARTICLE_TITLE_MISSING 400 (BAD_REQUEST) 제목을 입력해 주세요.
ARTICLE4002 _ARTICLE_CONTENT_MISSING 400 (BAD_REQUEST) 내용을 입력해 주세요.
ARTICLE4031 _NOT_OWNER_OF_ARTICLE 403 (FORBIDDEN) 해당 추억(게시글)의 주인이 아닙니다.
ARTICLE4041 _ARTICLE_NOT_FOUND 404 (NOT_FOUND) 해당 추억을 찾을 수 없습니다.
COMMENT4041 _COMMENT_NOT_FOUND 404 (NOT_FOUND) 해당 댓글을 찾을 수 없습니다.

 

REST API의 응답 표준으로 사용하는 JSON은 키와 값의 쌍으로 구성된 속성으로 데이터를 표현합니다. JSON의 값으로 또 다른 JSON데이터나 배열을 넣을 수도 있습니다.

 

 

실습 💦

더보기

1. 도메인에 api패키지를 만들고 안에 FirstApiController.java를 만듭시다.

REST API를 구현할 때는 일반 컨트롤러 대신 @RestController를 사용합니다.

package com.example.demo.api;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class FirstApiController {
    @GetMapping("/api/hello")
    public String hello() {
        return "Hello World";
    }
}



2. http://localhost:8080/api/hello 에 접속해서 반환값을 확인해봅시다.

 

3. Talend API Tester에서도 확인해보겠습니다.

https://chromewebstore.google.com/detail/talend-api-tester-free-ed/aejoelaoggembcahagimdiliamlcdmfm?hl=en

 

Talend API Tester - Free Edition - Chrome Web Store

Visually interact with REST, SOAP and HTTP APIs.

chromewebstore.google.com

 

REST controller는 JSON이나 텍스트 같은 데이터를 반환하는 반면 일반 컨트롤러는 뷰 페이지를 body안에 담아 반환합니다.

 

자 이제 GET 을 REST API로 구현해볼까요?

 

실습 💦💦

더보기

1. 게시글 조회하기

package com.example.demo.api;

import com.example.demo.dto.ArticleForm;
import com.example.demo.entity.Article;
import com.example.demo.repository.ArticleRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
public class ArticleApiController {
    @Autowired
    private ArticleRepository articleRepository;

    //GET
    //모든 게시글 가져오기
    @GetMapping("/api/articles")
    public List<Article> index(){
        return articleRepository.findAll();
    }
    //일부 게시글 가져오기
    @GetMapping("/api/articles/{id}")
    public Article show(@PathVariable Long id){
        return articleRepository.findById(id).orElse(null);
    }
    //POST
    //PATCH
    //DELETE
}

 

2. 게시글 만들기

반환형이 Article이라는 entity인 create메서드를 정의하고 수정할 데이터를 dto매개변수로 받아옵니다. 이렇게 받아온 dto는 DB에서 활용할 수 있도록 엔티티로 변환해 article변수에 넣고 articleRepository를 통해 DB에 저장한 후 반환합니다.

 

restAPI에서는 데이터를 생성할 때 JSON데이터를 반아와야 하므로 단순히 매개변수로 dto를 쓴다고 받아올 수 없고 @RequestBody라는 어노테이션을 사용해야합니다.

    //POST
    @PostMapping("/api/articles")
    public Article create(@RequestBody ArticleForm dto){
        Article article = dto.toEntity();
        return articleRepository.save(article);
    }

 

http://localhost:8080/api/articles 로 확인합시다.

{
  "title": "AAAA",
  "content": "sddddd"
}

 

3. 게시글 수정하기 & 상태 코드 작성

//PATCH
@PatchMapping("/api/articles/{id}")
public ResponseEntity<Article> update(@PathVariable Long id, @RequestBody ArticleForm dto){
    // 1. DTO -> entity 변환한기
    Article article = dto.toEntity();
    log.info("id: {}, article: {}", id, article.toString());
    // 2. target 조회하기
    Article targer = articleRepository.findById(id).orElse(null);
    // 3. 잘못된 요청 처리하기
    if(targer == null || id!= article.getId()){
        //400 잘못된 request요청
        log.info("잘못된 요청! id:{}, article:{}", id, article.toString());
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(null);
    }
    // 4. 업테이트 및 정상 응답(200) 하기
    Article updated = articleRepository.save(article);
    return ResponseEntity.status(HttpStatus.OK).body(updated);
}

 

대상 엔티티가 없거나 수정 요청 id와 본문 id가 다를 경우 400오류를 반환합니다.

ResponseEntity는 HTTP 응답의 상태 코드, 헤더, 본문을 모두 제어할 수 있게 해 줍니다. 상태코드를 반환할 것이므로 반환형을 ResponseEntity<Article>로 작성하고, status에는 상태를, body(본문)에는 반환할 데이터를 적습니다. 400 오류가 났을 때는 반환할 데이터가 없으므로 null을 적어 반환합니다.

 

# HttpStatus는 HTTP 상태 코드를 정의하는 **열거형(Enum)** 값으로 다음과 같이 사용합니다.

HttpStatus.OK // 상태 코드 200을 의미
HttpStatus.BAD_REQUEST // 상태 코드 400을 의미

 

 확인하기

{
  "id": "1",
  "title": "AAAA",
  "content": "sddddd"
}

 

id와 content 만 보낼 경우 title 내용이 null이 되는 문제가 발생합니다.


일부 데이터만 수정하는 경우를 처리하는 patch메서드를 만들어 해결해보겠습니다.

Entity주로 데이터베이스에 저장되는 데이터를 나타내는 클래스입니다. 이 클래스는 데이터를 담는 속성(필드)과 데이터를 다룰 수 있는 메서드를 포함합니다.

entity/Article.java에 patch메서드를 추가해보겠습니다.

package com.example.demo.entity;

import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;

@AllArgsConstructor
@NoArgsConstructor
@ToString
@Entity
@Getter
public class Article {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Column
    private String title;
    @Column
    private String content;

    public void patch(Article article) {
        if(article.title != null){
            this.title = article.title;
        }
        if(article.content != null){
            this.content = article.content;
        }
    }
}
    //PATCH
    @PatchMapping("/api/articles/{id}")
    public ResponseEntity<Article> update(@PathVariable Long id, @RequestBody ArticleForm dto){
        // 1. DTO -> entity 변환한기
        Article article = dto.toEntity();
        log.info("id: {}, article: {}", id, article.toString());
        // 2. target 조회하기
        Article targer = articleRepository.findById(id).orElse(null);
        // 3. 잘못된 요청 처리하기
        if(targer == null || id!= article.getId()){
            //400 잘못된 request요청
            log.info("잘못된 요청! id:{}, article:{}", id, article.toString());
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(null);
        }
        // 4. 업테이트 및 정상 응답(200) 하기
        targer.patch(article);
        Article updated = articleRepository.save(targer);
        return ResponseEntity.status(HttpStatus.OK).body(updated);
    }

 

조작해주고 있던 entity에 patch메서드로 이렇게 수정할 내용이 있을 때만 기존 데이터에 새 데이터를 붙이도록 설정합니다.

 

4. 게시글 삭제하기 DELETE 구현하기

//DELETE
@DeleteMapping("/api/articles/{id}")
public ResponseEntity<Article> delete(@PathVariable Long id){
    //1. 대상 찾기
    Article target = articleRepository.findById(id).orElse(null);
    //2. 잘못된 요청 처리
    if(target == null){
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(null);
    }
    //3. 대상 삭제하기
    articleRepository.delete(target);
    return ResponseEntity.status(HttpStatus.OK).body(null);
}

 

    //3. 대상 삭제하기
    articleRepository.delete(target);
    return ResponseEntity.status(HttpStatus.OK).build();

 

ResponseEntity의 build메서드를 body(null) 대신 작성해도 됩니다. build메서드는 HTTP 응답의 body가 없는 ResponseEntity객체를 생성합니다. 따라서 결과가 같습니다.

 

# build() 메서드는 ResponseEntity 객체를 생성하는 메서드입니다. 응답 본문을 설정하지 않고, 상태 코드만 설정하여 ResponseEntity 객체를 반환합니다.

 

 

https://velog.io/@pomeranian91/%EC%9D%B8%ED%85%94%EB%A6%AC%EC%A0%9C%EC%9D%B4-%ED%95%9C%EA%B8%80-%EA%B9%A8%EC%A7%90-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0%EB%B0%A9%EB%B2%95%EC%9D%B8%EC%BD%94%EB%94%A9-%EC%84%A4%EC%A0%95

 

인텔리제이 한글 깨짐 문제 해결방법(인코딩 설정)

인텔리제이에서 WAS 구동시나 app 실행시 한글이 깨진다면 보통 인코딩 설정이 잘못 되어서입니다.대부분의 경우 UTF-8 encoding 을 사용할테니 다음 방법으로 encoding 을 정확히 설정해 주면 됩니다.

velog.io

 

🐔12단원. 서비스 계층과 트랜잭션

서비스(service)란 컨트롤러와 리포지터리 사이에 위치하는 계층으로 서버의 핵심 기능(비즈니스로직)을 처리하는 "순서"를 총괄합니다.

클라이언트가 요청을 보내면 컨트롤러가 이것을 받아 서비스에게 전달하고 서비스는 리포지터리를 통해 DB에서 정보를 가져옵시다.

 

트랜잭션(transaction)모두 성공해야 하는 일련의 과정으로 쪼갤 수 없는 업무 처리 최소 단위를 말합니다.

메소드1,2,3이 순서대로 실행되는 것이 하나의 트랜잭션이라면 메소드2가 실패하면 앞서 진행한 1을 취소하고 다시 1,2,3이렇게 진행됩니다. 이렇게 트랜잭션이 실패로 돌아갈 경우, 진행 초기 단계로 되돌리는 것을 롤백(rollback)이라고 합니다.

 

우리가 지금까지 실습한 Rest Controller는 요청과 응답을 처리하고(컨트롤러 역할), 동시에 리포지터리에 데이터를 가져오도록 명령(서비스 역할)합니다. 우리는 이제 이 1인 2역하는 컨트롤러를 분리하겠습니다.

 

CRUD 리팩터링 실습 💦

더보기

1. service/ArticleService를 만듭니다.

package com.example.demo.service;

import com.example.demo.repository.ArticleRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class ArticleService {
    @Autowired
    private ArticleRepository articleRepository;
}

 

2. controller 내용을 전부 주석 처리하고  Service내용을 받아옵니다.

package com.example.demo.api;

import com.example.demo.dto.ArticleForm;
import com.example.demo.entity.Article;
import com.example.demo.repository.ArticleRepository;
import com.example.demo.service.ArticleService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@Slf4j
@RestController
public class ArticleApiController {
    @Autowired
    private ArticleService articleService;

}

 

3. CRUD 리팩터링

1) READ

controller

//GET
//모든 게시글 가져오기
@GetMapping("/api/articles")
public List<Article> index(){
    return articleService.index();
}
//일부 게시글 가져오기
@GetMapping("/api/articles/{id}")
public Article show(@PathVariable Long id){
    return articleService.show(id);
}

 

service 

public List<Article> index() {
    return articleRepository.findAll();
}

public Article show(Long id) {
    return articleRepository.findById(id).orElse(null);
}

 

2) CREATE

controller

//POST
@PostMapping("/api/articles")
public ResponseEntity<Article> create(@RequestBody ArticleForm dto) {
    Article created = articleService.create(dto);
    return (created != null) ?
            ResponseEntity.status(HttpStatus.OK).body(created) :
            ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
}

 

service 

public Article create(ArticleForm dto) {
    Article article = dto.toEntity();
    if(article.getId() != null) {
        return null;
    }
    return articleRepository.save(article);
}

post요청은 기존 값을 수정하면 안되므로  저러한 조건문을 달아주었습니다. 

그래서 이미 존재하는 id를 수정하려고 하면 400오류가 발생합니다.

 

3)

controller

//PATCH
@PatchMapping("/api/articles/{id}")
public ResponseEntity<Article> update(@PathVariable Long id, @RequestBody ArticleForm dto){
    Article updated = articleService.update(id, dto);
    return (updated != null) ?
            ResponseEntity.status(HttpStatus.OK).body(updated ) :
            ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
}

 

service 

public Article update(Long id, ArticleForm dto) {
    // 1. DTO -> entity 변환한기
    Article article = dto.toEntity();
    log.info("id: {}, article: {}", id, article.toString());
    // 2. target 조회하기
    Article targer = articleRepository.findById(id).orElse(null);
    // 3. 잘못된 요청 처리하기
    if(targer == null || id!= article.getId()){
        //400 잘못된 request요청
        log.info("잘못된 요청! id:{}, article:{}", id, article.toString());
        return null;
    }
    // 4. 업테이트 및 정상 응답(200) 하기
    targer.patch(article);
    Article updated = articleRepository.save(targer);
    return updated;
}

 확인해봅시다.

{
  "id": 1,
  "title": "ddddff",
  "content": "ㄴffffㄹㅈㅈ"
}

 

4) DELETE

 controller

//DELETE
@DeleteMapping("/api/articles/{id}")
public ResponseEntity<Article> delete(@PathVariable Long id){
    Article deleted = articleService.delete(id);
    return (deleted != null) ?
            ResponseEntity.status(HttpStatus.OK).body(deleted ) :
            ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
}

service 

public Article delete(Long id) {
    //1. 대상 찾기
    Article target = articleRepository.findById(id).orElse(null);
    //2. 잘못된 요청 처리
    if(target == null){
        return null;
    }
    //3. 대상 삭제하기
    articleRepository.delete(target);
    return target;
}

 

 

트랜잭션 맛보기 실습 💦
넘겨도 되는 실습이에요

더보기

controller

@PostMapping("/api/transaction-test")
public ResponseEntity<List<Article>> transactionTest(@RequestBody List<ArticleForm> dtos) {
    // 서비스 호출
    List<Article> createdList = articleService.createArticles(dtos);

    // 생성 결과에 따라 응답 처리
    return (createdList != null)
            ? ResponseEntity.status(HttpStatus.OK).body(createdList)
            : ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
}

 

service

@Transactional
public List<Article> createArticles(List<ArticleForm> dtos) {
    // 1. dto 묶음을 엔티티 묶음으로 변환하기
    List<Article> articleList = dtos.stream()
            .map(dto -> dto.toEntity())
            .collect(Collectors.toList());
    // 2. 엔티티 묶음을 DB에 저장하기
    articleList.stream()
            .forEach(article -> articleRepository.save(article));
    // 3. 강제 예외 발생시키기
    articleRepository.findById(-1L)
            .orElseThrow(() -> new IllegalArgumentException("결제 실패!"));
    // 4. 결과 값 반환하기
    return articleList;
}

 

실험 해봅시다.

[
  {
    "id": 1,
    "title": "Spring Boot Transaction Test",
    "content": "이것은 트랜잭션 테스트입니다."
  },
  {
    "id": 2,
    "title": "Another Article",
    "content": "이것은 또 다른 게시글입니다."
  }
]

 

500 서버오류가 발생합니다.

 

#스트림 문법은 리스트와 같은 자료구조에 저장된 요소를 하나씩 순회하면서 처리하는 코드 패턴으로 for문 대신에 사용할 수 있는 아주 편리한 문법입니다.