Part 3 REST API와 테스트 코드 작성하기 (10-13장)

ttt-1-2·2025년 10월 3일

10장 - REST API와 JSON

10.1 REST API와 JSON

REST API

  • REST API (Representational State Transfer API)는 서버의 자원을 클라이언트에 구애받지 않고 사용할 수 있게하는 설계 방식이다
  • 다음과 같이 REST API의 동작을 이해할 수 있다:
[클라이언트들]
 PC / 모바일 / 태블릿 / IoT ...
        │
        │  HTTP 요청
        ▼
+------------------+
|   REST API 서버  |
+------------------+
        │  JSON 응답(뷰X, 데이터O) //서버는 화면이 아닌 데이터(JSON) 를 돌려준다.
        ▼
자원(RESOURCE): /articles
   ├─ GET    /articles          → 글 목록 조회
   ├─ GET    /articles/{id}     → 글 단건 조회
   ├─ POST   /articles          → 글 생성
   ├─ PATCH  /articles/{id}     → 글 부분 수정
   └─ DELETE /articles/{id}     → 글 삭제

JSON

  • JSON 데이터는 키(key)와 값(value)으로 구성된 정렬되지 않는 속성(properties)의 집합이다.
    • 키: 큰따옴표 (””)로 감싼다
    • 값: 문자열인 경우만 큰따옴표 (””)로 감싼다
{
	"name": "망고",
	"breeds": "골든리트리버",
	"age": 2
}

10.2 REST API 동작 살펴보기

연습용으로 Talend API Tester 이용해 HTTP 요청을 보내고 돌아온 응답을 확인하겠습니다. {JSON} Placeholder 사이트에서 자원들을 JSON 형식으로 받아 실습해 보겠습니다!

HTTP 상태 코드

HTTP 상태 코드는 클라이언트가 보낸 요청이 성공/실패했는지 알려주는 코드다. 코드는 100번대부터 500번대까지 5개 그룹으로 나눠 있다.

자세한 내용: MDN 웹 문서

1XX (정보)요청이 수신돼, 처리 중
2XX (성공)요청이 정상적으로 처리됨
3XX (리다이렉션)요청을 완료하려면 추가 행동이 필요함
4XX (클라이언트 요청 오류)클라이언트의 요청이 잘못됨
5XX (서버 응답 오류)서버 내부에 에러가 발생해 클라이언트의 요청에 답할 수 없음

GET 요청하고 응답받기

POST 요청하고 응답받기

PATCH 요청하고 응답받기

DELETE 요청하고 응답받기


11장 - HTTP와 REST 컨트롤러

Part 2에서 만든 firstproject를 이용해 데이터를 CRUD하기 위해 REST API를 구현해 보겠습니다!

  • REST API:
    • REST: HTTP URL로 서버의 자원을 명시하고, HTTP 메소드로 해당 자원에 대해 CRUD하는 것
    • API: 클라이언트가 서버의 자원을 요청할 수 있도록 서버에서 제공하는 인터페이스

REST API 구현

GET 구현하기

	// 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 구현하기

  • 주의점: @RequestBody 추가해야 본문(BODY)을 받아 올 수 있다!
  // POST
  @PostMapping("/api/articles")
  public Article create(@RequestBody ArticleForm dto) {
      Article article = dto.toEntity();
      return articleRepository.save(article);
  }

PATCH 구현하기

  • 주의점: 일부만 수정한 경우: patch 메소드를 만들어야 한다
	// PATCH
  @PatchMapping("/api/articles/{id}")
  public ResponseEntity<Article> update(@PathVariable Long id, @RequestBody ArticleForm dto) {
      // 1. DTO -> 엔티티 변환하기
      Article article = dto.toEntity();
      // 2. 타깃 조회하기
      Article target = articleRepository.findById(id).orElse(null);
      // 3. 잘못된 요청 처리하기
      if(target == null || id != article.getId()) {
          return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(null);
      }
      // 4. 업데이트 및 정상 응답(200)하기
      target.patch(article); // 기존 데이터에 새 데이터 붙이기
      Article updated = articleRepository.save(target); // 수정 내용 DB에 최종 저장
      return ResponseEntity.status(HttpStatus.OK).body(updated);
  }

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).build();
  }

12장 - 서비스 계층과 트렌잭션

12.1 개념

  • 서비스: 컨트롤러와 리포지토리 사이에서 비즈니스 규칙을 수행하고 흐름을 조정한다. 여러 리포지토리 호출을 묶어 트랜잭션 경계를 만든다
  • 트랜잭션: 여러 데이터 작업을 하나의 논리적 단위로 묶어 전부 성공하면 커밋하고 하나라도 실패하면 롤백한다

ex:

  • 웨이터 = 컨트롤러: 손님(클라이언트) 주문을 받아 전달한다
  • 주방장 = 서비스: 어떤 메뉴(비즈니스 로직)를 어떤 순서로 만들지 결정하고 전체를 지휘한다. 여러 하위 작업을 하나의 주문 단위(트랜잭션) 로 묶는다
  • 보조요리사 = 리포지토리: 주방장이 지시한 대로 재료 창고(DB) 에서 꺼내고 넣는 CRUD 를 수행한다
  • 트랜잭션 = 한 테이블의 코스 전체: 모든 요리가 완성되어야 서빙한다. 하나라도 문제가 나면 전체 취소(롤백) 하고 처음부터 다시 준비한다

12.2 서비스 계층 만들기

이전 11장까지 만든 코드를 요청해 보겠습니다!

수정한 부분:

  • service package 만들기
  • ArticleApiController 수정하기

ex:

  • 수정하기 전:
// controller

  // POST
  @PostMapping("/api/articles")
  public Article create(ArticleForm dto) {
      Article article = dto.toEntity();
      return articleRepository.save(article);
  }
  • 수정한 후:
// 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);
	}

12.3 트랜잭션 맛보기

  • ArticleApiController
  @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();
  }
  • ArticleService

  @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;

  }

@Transactional로 메서드 전체를 하나의 트랜잭션으로 묶어 DTO 목록을 엔티티로 변환해 저장한 뒤, 의도적으로 예외를 던져 롤백이 일어나는지 확인한다. 예외가 발생하면 이전에 저장된 작업이 모두 취소되어 DB에 반영되지 않는다!


13장 - 테스트 코드 작성하기

테스트 코드는 보통 3단계로 작성한다:

  1. 예상 데이터 작성하기
  2. 실제 데이터 획득하기
  3. 예상 데이터와 실제 데이터 비교해 검증하기

테스트 코드 작성하기

  • 메소드 오른쪽 누르고 Generate → Test 선택. JUnit5로 테스트를 만들겠습니다!

  • ArticleSeriveTest 생성 위치를 확인할 수 있다

  • 테스트 코드 작성하기:

@SpringBootTest
class ArticleServiceTest {
    @Autowired
    ArticleService articleService;

    @Test
    void index() {
        // 1. 예상 데이터
        Article a = new Article(1L, "가가가가", "1111");
        Article b = new Article(2L, "나나나나", "2222");
        Article c = new Article(3L, "다다다다", "3333");
        List<Article> expected = new ArrayList<Article>(Arrays.asList(a,b,c));
        // 2. 실제 데이터
        List<Article> articles = articleService.index();
        // 3. 비교 및 검증
        assertEquals(expected.toString(), articles.toString());
    }
    @Test
    void show_성공() {
        // 1. 예상 데이터
        Long id = 1L;
        Article expected = new Article(id, "가가가가", "1111");
        // 2. 실제 데이터
        Article article = articleService.show(id);
        // 3. 비교 및 검증
        assertEquals(expected.toString(), article.toString());
    }

    @Test
    void show_실패() {
        // 1. 예상 데이터
        Long id = -1L;
        Article expected = null;
        // 2. 실제 데이터
        Article article = articleService.show(id);
        // 3. 비교 및 검증
        assertEquals(expected, article);
    }
}

0개의 댓글