Framework/Spring Boot

[Framework] Spring Boot [REST API] 기본 구조

SeungyubLee 2022. 11. 30. 14:12

1. DTO + ApiController + Entity + Repository 구조 (Service 계층 없을 경우)

 

Client : 손님

RestController(Server) : 웨이터 & 셰프

Repository(Server) : 주방 보조

DataBase : 창고

 

src > main > java > com.example.firstproject > api의 ArticleApiController

package com.example.firstproject.api;

import com.example.firstproject.dto.ArticleDto;
import com.example.firstproject.entity.Article;
import com.example.firstproject.repository.ArticleRepository;
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;

@RestController
public class ArticleApiController {

    // Service 계층 없이 구현했을 경우

    @Autowired
    private ArticleRepository articleRepository;

    // GET
    @GetMapping("/api/article")
    public List<Article> readAll() {
        return articleRepository.findAll(); // 전체 조회 후 Entity 반환
    }

    // GET
    @GetMapping("/api/article/{id}")
    public Article read(@PathVariable Long id) {
        return articleRepository.findById(id).orElse(null); // 조건부 조회 후 Entity 반환
    }

    // POST
    @PostMapping("/api/article")
    public Article create(@RequestBody ArticleDto dto) { // REST API의 경우 JSON으로 데이터 넘길 때 RequestBody를 써야 한다.
        Article article = dto.toEntity(); // 입력받은 DTO를 Entity로
        return articleRepository.save(article); // Entity 저장
    }

    // PATCH 또는 PUT
    @PatchMapping("/api/article/{id}")
    public ResponseEntity<Article> update(@PathVariable Long id, @RequestBody ArticleDto dto) {
        // 1: 수정용 엔티티 생성
        Article article = dto.toEntity(); // 입력받은 DTO를 Entity로
        // 2: 대상 엔티티를 조회
        Article target = articleRepository.findById(id).orElse(null); // 조건부 조회 후 Entity 반환
        // 3: 잘못된 요청 처리(대상이 없거나, id가 다른 경우)
        if (target == null || id != target.getId()) {
            // 400, 잘못된 요청 응답
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(null);
        }
        // 4: 업데이트 및 정상 응답(200)
        target.patch(article); // 조건부 조회 Entity에 입력받은 값 적용
        Article updated = articleRepository.save(target); // Entity 저장
        return ResponseEntity.status(HttpStatus.OK).body(updated);
    }

    // DELETE
    @DeleteMapping("/api/article/{id}")
    public ResponseEntity<Article> delete(@PathVariable Long id) {
        // 1: 대상 찾기
        Article target = articleRepository.findById(id).orElse(null); // 조건부 조회 후 Entity 반환
        // 2: 잘못된 요청 처리
        if (target == null) {
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(null);
        }
        // 3: 대상 삭제
        articleRepository.delete(target); // Entity 삭제
        return ResponseEntity.status(HttpStatus.OK).build();
    }
}

2. DTO + ApiController + Service + Entity + Repository 구조

일반적으로 Service의 업무처리가 Transaction 단위로 진행되기 때문에 Service 계층을 두고 역할을 나누도록 한다.

 

Client : 손님

RestController(Server) : 웨이터

Service(Server) : 셰프

Repository(Server) : 주방 보조

DataBase : 창고

 

src > main > java > com.example.firstproject > api의 ArticleApiController

package com.example.firstproject.api;

import com.example.firstproject.dto.ArticleDto;
import com.example.firstproject.entity.Article;
import com.example.firstproject.service.ArticleService;
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;

@RestController
public class ArticleApiController {

    @Autowired // DI, 생성 객체를 가져와 연결
    private ArticleService articleService;

    @GetMapping("/api/article")
    public List<Article> readAll() { // 전체 조회
        return articleService.readAll();
    }

    @GetMapping("/api/article/{id}")
    public Article read(@PathVariable Long id) { // 조건부 조회
        return articleService.read(id);
    }

    @PostMapping("/api/article")
    public ResponseEntity<Article> create(@RequestBody ArticleDto dto) { // 추가 // REST API의 경우 JSON으로 데이터 넘길 때 RequestBody를 써야 한다.
        Article created = articleService.create(dto);
        return (created != null) ? ResponseEntity.status(HttpStatus.OK).body(created) : ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
    }

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

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

    @PostMapping("/api/transaction-test")
    public ResponseEntity<List<Article>> transactionTest(@RequestBody List<ArticleDto> dtos) { // Transaction 실패 -> Rollback
        List<Article> createdList = articleService.createArticles(dtos);
        return (createdList != null) ? ResponseEntity.status(HttpStatus.OK).body(createdList) : ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
    }
}

src > main > java > com.example.firstproject > service의 ArticleService

package com.example.firstproject.service;

import com.example.firstproject.dto.ArticleDto;
import com.example.firstproject.entity.Article;
import com.example.firstproject.repository.ArticleRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.stream.Collectors;

@Service // 서비스 선언! (서비스 객체를 Spring Boot에 생성)
public class ArticleService {

    @Autowired // DI(Dependency Injection)
    private ArticleRepository articleRepository;

    public List<Article> readAll() { // 전체 조회
        return articleRepository.findAll();
    }

    public Article read(Long id) { // 조건부 조회
        return articleRepository.findById(id).orElse(null);
    }

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

    public Article update(Long id, ArticleDto dto) { // 수정
        // 1: 수정용 엔티티 생성
        Article article = dto.toEntity(); // 입력받은 DTO를 Entity로
        // 2: 대상 엔티티를 조회
        Article target = articleRepository.findById(id).orElse(null); // 조건부 조회 후 Entity 반환
        // 3: 잘못된 요청 처리(대상이 없거나, id가 다른 경우)
        if (target == null || id != target.getId()) {
            // 400, 잘못된 요청 응답
            return null;
        }
        // 4: 업데이트 및 정상 응답(200)
        target.patch(article); // 조건부 조회 Entity에 입력받은 값 적용
        Article updated = articleRepository.save(target); // Entity 저장
        return updated;
    }

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

    @Transactional // 해당 메소드를 Transaction으로 묶는다. (해당 메소드 실행 중 실패 -> 이 메소드가 실행되기 전 상태로 Rollback)
    public List<Article> createArticles(List<ArticleDto> dtos) { // Transaction 테스트
        // 1: DTO 묶음을 Entity 묶음으로 변환
        List<Article> articleList = dtos.stream().map(dto -> dto.toEntity()).collect(Collectors.toList());
        // 2: Entity 묶음을 DB에 저장
        articleList.stream().forEach(article -> articleRepository.save(article));
        // 3: 강제 예외 발생
        articleRepository.findById(-1L).orElseThrow(
                () -> new IllegalArgumentException("결제 실패!")
        );
        // 4: 결과값 반환
        return articleList;
    }
}

src > main > java > com.example.firstproject > dto의 ArticleDto

package com.example.firstproject.dto;

import com.example.firstproject.entity.Article;
import lombok.AllArgsConstructor;
import lombok.ToString;

@AllArgsConstructor // Lombok 사용
@ToString // Lombok 사용
public class ArticleDto { // DTO 클래스

    private Long id;
    private String title;
    private String content;

//    public ArticleDto(Long id, String title, String content) { // @AllArgsConstructor와 같음
//        this.id = id;
//        this.title = title;
//        this.content = content;
//    }

//    @Override
//    public String toString() { // @ToString과 같음
//        return "ArticleDto{" +
//                "id=" + id +
//                ", title='" + title + '\'' +
//                ", content='" + content + '\'' +
//                '}';
//    }

    public Article toEntity() {
        return new Article(id, title, content);
    }
}

src > main > java > com.example.firstproject > entity의 Article

package com.example.firstproject.entity;

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

@Entity // DB가 해당 객체를 인식 가능! (해당 클래스로 테이블 생성)
@NoArgsConstructor // Lombok 사용
@AllArgsConstructor // Lombok 사용
@ToString // Lombok 사용
@Getter // Lombok 사용
public class Article {

    @Id // 대표값을 지정! PK (EX : 주민등록번호)
    @GeneratedValue(strategy = GenerationType.IDENTITY) // 1, 2, 3,  ... DB가 id를 자동 채번하도록 하는 어노테이션 // JPA 버전 문제로 자동 채번이 안될 수 있다.
    private Long id;

    @Column
    private String title;

    @Column
    private String content;

//    public Article() { // Entity 기본 생성자 필요 // @NoArgsConstructor와 같음
//
//    }

//    public Article(Long id, String title, String content) { // @AllArgsConstructor와 같음
//        this.id = id;
//        this.title = title;
//        this.content = content;
//    }

//    @Override
//    public String toString() { // @ToString과 같음
//        return "Article{" +
//                "id=" + id +
//                ", title='" + title + '\'' +
//                ", content='" + content + '\'' +
//                '}';
//    }

//    // @Getter와 같음
//    public Long getId() {
//        return id;
//    }
//
//    public String getTitle() {
//        return title;
//    }
//
//    public String getContent() {
//        return content;
//    }

    public void patch(Article article) {
        if (article.title != null) {
            this.title = article.title;
        }

        if (article.content != null) {
            this.content = article.content;
        }
    }
}

src > main > java > com.example.firstproject > repository의 ArticleRepository

package com.example.firstproject.repository;

import com.example.firstproject.entity.Article;
import org.springframework.data.repository.CrudRepository;
import java.util.ArrayList;

public interface ArticleRepository extends CrudRepository<Article, Long> { // JPA가 제공 <관리대상 Entity, PK 타입>

    @Override
    ArrayList<Article> findAll(); // List 형태로 받기 위해 Override 했음
}

 

Postman을 사용한 API 테스트 화면