# 기존 Article의 Rest API 구현에 Service 계층을 추가 한다.
요약
컨트롤러는 클라이언트로부터 요청을 받는것과 응답을 처리하는것에만 집중한다.
서비스는 자기가 맡은 업무의 일반적인 처리 흐름과 그 흐름의 실패했을 경우를 대비한 트랜잭션 관리를 하였다.
트랜잭션에 실패하면 롤백이 된다.
서비스 계층이란?
Controller와 Repository 사이에 위치한 계층으로 처리 업무의 순서를 총괄한다.
음식점을 예를 들어보면 웨이터에게 주문이 들어오면 이를 전달받은 쉐프가 요리를 총괄 한다. 이때 요리에 필요한 재료는 보조 요리사에게 가져오게 시킨다. 여기서 웨이터는 컨트롤러, 쉐프는 서비스가 되고 보조 요리사는 레파지토리가 된다.
클라이언트로부터 주문을 받은 컨트롤러는 주문 내용을 서비스에게 전달하고, 서비스는 이를 받아 정해진 레시피 순서에 따라서 요리를 한다. 요리에 필요한 재료는 레파지토리가 창고(DB)에서 가져와서 준비해 준다.
트랜잭션과 롤백
트랜잭션이란, 모두 성공되어야 하는 일련의 과정으로 볼 수 있다.
음식점 예약을 예로 들면 순서는 아래와 같다.
1. Time reservation
2. Table selection
3. Munu choice
4. Payment
5. Issuing a receipt
6. Reservation complete
위의 예약 순서에서 만약 3번의 Munu choice에서 과정이 멈췄다면 이전의 1번, 2번의 과정이 모두 취소 되어야 한다.
취소가 되지 않으면, 예약 기록이 남아 추후에 문제가 될 수도 있게 된다.
실패 시 진행 초기단계로 돌리는것을 Rollback(롤백) 이라고 한다.
기존 RestController의 경우는 1인 2역 즉 예를들어 웨이터와 쉐프의 역할을 동시헤 한다고 볼 수 있다.
클라이언트의 요청과 응답을 처리함과 동시에 Repository에게 데이터를 가져오도록 명령을 하고 있다.
일반적인 웹 서비스는 Controller와 Repository 사이에 Service를 두어 역할을 분업화 한다.
실제 코드를 통해 서비스 계층을 구현해 본다.
service 패키지 폴더를 만들고 ArticleService 클래스를 생성한다.
해당 클래스 상단에 @Service를 입력하여 서비스로 선언한다.
private ArticleRepository articleRepository; → 해당 클래스가 Repository와 협업 할 수 있게 필드를 추가한다.
다시 ArticleApiController로 이동하여 하나하나씩 주석을 풀면서 각각의 역할을 분업화 및 구현을 해본다.
목록 조회 (서비스에 동일한 메소드로 생성한다.) (GET 요청)
"/api/articles"를 통해서 요청이 들어왔을때, 기존에는 바로 Repository를 통해서 데이터를 가져 왔는데, 이제 여기서는 service를 통해서 가져오도록 하겠다. articleservice.index();라는 메소드로 가져오게 하겠다. service에 메소드를 추가하여 내용을 작성 한다.
//Controller 코드
@GetMapping("/api/articles")
public List<Article> index() {
return articleservice.index();
}
//Service 코드
public List<Article> index() {
return articleRepository.findAll();
}
단건 조회 (GET 요청)
articleservice.show(id); 를 통해서 값을 가져온다.
//Controller
@GetMapping("/api/articles/{id}")
public Article show(@PathVariable Long id) {
return articleservice.show(id);
}
//Service
public Article show(Long id) {
return articleRepository.findById(id).orElse(null);
}
Article 생성 (POST 요청)
//Controller
@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);
}
Article 수정 (PATCH 요청)
//Controller
@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. 수정용 엔티티를 생성
Article article = dto.toEntity();
log.info("id: {}", "article: {}", id, article.toString());
// 2. 대상 엔티티를 조회
Article target = articleRepository.findById(id).orElse(null);
// 3. 잘못된 요청 처리(대상이 없거나, id가 다른 경우)
if (target == null || id != article.getId()) {
// 400, 잘못된 요청 응답!
log.info("잘못된 요청! id: {}, article: {}", id, article.toString());
return null;
}
// 4. 업데이트 및 정상 응답(200)
target.patch(article);
Article updated = articleRepository.save(target);
return updated;
}
Article 삭제 (DELETE 요청)
//Controller
@DeleteMapping("/api/articles/{id}")
public ResponseEntity<Article> delete(@PathVariable Long id) {
Article deleted = articleservice.delete(id);
return (deleted != null) ?
ResponseEntity.status(HttpStatus.OK).build() :
ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
}
//Service
public Article delete(Long id) {
// 대상 찾기
Article target = articleRepository.findById(id).orElse(null);
// 잘못된 요청 처리
if (target == null) {
return null;
}
// 대상 삭제
articleRepository.delete(target);
return target;
}
Transaction 맛보기 코드
//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) {
// dto 묶음을 entity 묶음으로 변환
List<Article> articleList = dtos.stream()
.map(dto -> dto.toEntity())
.collect(Collectors.toList());
// entity 묶음을 DB로 저장
articleList.stream()
.forEach(article -> articleRepository.save(article));
// 강제 예외 발생
articleRepository.findById(-1L).orElseThrow(
() -> new IllegalArgumentException("결재 실패!")
);
// 결과값 반환
return articleList;
}
ArticleService의 부분 코드
dto를 entity로 변환 → Article article = dto.toEntity();
그리고 변환된 데이터를 저장 한다. → return articleRepository.save(article);
public Article create(ArticleForm dto) {
Article article = dto.toEntity();
if (article.getId() != null) {
return null;
}
return articleRepository.save(article);
}
트랜잭션 롤백
@Transactional 어노테이션이 없으면, 트랜잭션 중간에 오류가 발생해도 롤백되지 않는다.
@Transactional 어노테이션이 있으면, 트랜잭션 중간에 오류가 발생하면 해당 메소드를 실행 이전의 상태로 롤백이 된다. 예를들어 트랜잭션 처리중 3개의 데이터를 DB에 저장 후 오류가 발생하면 DB에 저장된 상태로 데이터가 남아 있게 된다. 하지만 @Transactional 어노테이션이 있다면, 실행 도중 오류가 발생해도 DB에 데이터가 남아있지 않고 롤백 된다.
@Transactional // 해당 메소드를 트랜잭션으로 묶는다!
public List<Article> createArticles(List<ArticleForm> dtos) {
// dto 묶음을 entity 묶음으로 변환
List<Article> articleList = dtos.stream()
.map(dto -> dto.toEntity())
.collect(Collectors.toList());
// entity 묶음을 DB로 저장
articleList.stream()
.forEach(article -> articleRepository.save(article));
// 강제 예외 발생
articleRepository.findById(-1L).orElseThrow(
() -> new IllegalArgumentException("결재 실패!")
);
// 결과값 반환
return articleList;
}
ArticleService의 전체 코드
@Slf4j
@Service // 서비스 선언! (서비스 객체를 스프링부트에 생성)
public class ArticleService {
@Autowired // DI
private ArticleRepository articleRepository;
public List<Article> index() {
return articleRepository.findAll();
}
public Article show(Long id) {
return articleRepository.findById(id).orElse(null);
}
public Article create(ArticleForm dto) {
Article article = dto.toEntity();
if (article.getId() != null) {
return null;
}
return articleRepository.save(article);
}
public Article update(Long id, ArticleForm dto) {
// 1. 수정용 엔티티를 생성
Article article = dto.toEntity();
log.info("id: {}", "article: {}", id, article.toString());
// 2. 대상 엔티티를 조회
Article target = articleRepository.findById(id).orElse(null);
// 3. 잘못된 요청 처리(대상이 없거나, id가 다른 경우)
if (target == null || id != article.getId()) {
// 400, 잘못된 요청 응답!
log.info("잘못된 요청! id: {}, article: {}", id, article.toString());
return null;
}
// 4. 업데이트 및 정상 응답(200)
target.patch(article);
Article updated = articleRepository.save(target);
return updated;
}
public Article delete(Long id) {
// 대상 찾기
Article target = articleRepository.findById(id).orElse(null);
// 잘못된 요청 처리
if (target == null) {
return null;
}
// 대상 삭제
articleRepository.delete(target);
return target;
}
@Transactional // 해당 메소드를 트랜잭션으로 묶는다!
public List<Article> createArticles(List<ArticleForm> dtos) {
// dto 묶음을 entity 묶음으로 변환
List<Article> articleList = dtos.stream()
.map(dto -> dto.toEntity())
.collect(Collectors.toList());
// entity 묶음을 DB로 저장
articleList.stream()
.forEach(article -> articleRepository.save(article));
// 강제 예외 발생
articleRepository.findById(-1L).orElseThrow(
() -> new IllegalArgumentException("결재 실패!")
);
// 결과값 반환
return articleList;
}
}
'⭐ SpringBoot > 𝄜 게시판 with SpringBoot' 카테고리의 다른 글
21. 댓글 CRUD를 위한 Entity와 Repository를 생성 (0) | 2022.04.06 |
---|---|
20. 테스트 TDD (0) | 2022.04.06 |
18. HTTP RestController (0) | 2022.04.04 |
17. RestAPI & JSON (0) | 2022.04.04 |
16. CRUD와 SQL 쿼리 (0) | 2022.04.04 |