[Framework] Spring Boot [REST API] 기본 구조
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 테스트 화면