서비스와 트랜잭션의 개념
- 서비스(service) : 컨트롤러와 리파지터리 사이에 위치하는 계층으로 서버의 핵심 기능(비지니스 로직)을 처리하는 순서를 총괄
- 트랜잭션(transaction) : 모두 성공해야 하는 일련의 과정 , 쪼갤수 없는 업무 처리의 최소 단위
- 서비스 업무 처리는 트랜잭션 단위로 진행
- 만약 트랜잭션이 실패로 돌아갈 경우 진행 초기 단계로 돌리는 것을 롤백(rollback)라고 함
서비스 계층 만들기
- ArticleApiController 내에 모든 코드를 주석 처리
- 객체 주입된 리파지터리를 서비스로 변경후 밑에코드작성
@Autowired private ArticleService articleService; // 서비스 객체 주입
- service 라는 새 패키지 생성
- service 패키지 안에 ArticleService.java 파일 생성
- ArticleService.java 클래스위에 @Service 어노테이션 작성
- @Service은 해당 클래스를 서비스로 인식하여 스프링 부트에 서비스 객체를 생성
- ArticleService.java에 리파지터리와 협업할수 있도록 articleRepositoty 필드를 추가하고 @Autowired를 통해 객체주입
ArticleApiController.java 파일
public class ArticleApiController {
@Autowired
private ArticleService articleService; //서비스 객체 주입
==============================================================
ArticleService.java 파일
@Service // 서비스 객체 생성
public class ArticleService {
@Autowired
private ArticleRepository articleRepository; // 게시글 리파지터리 객체 주입
}
게시글 조회 요청 개선하기
- 모든 게시글 조회 요청 개선하기
- 아까 주석 처리한 부분에 index() 메서드의 주석 제거후 return 문의 원래 있던 코드를 제거후 articleService.index() 메서드를 호출 하도록 변경
- articleService에 index() 메서드를 생성 후 return문에 return articleRepository.findAll() 으로 작성회 DB에서 조회한 결과를 반환
ArticleApiController.java 파일
@GetMapping("/api/articles") //URL 요청 접수
public List<Article> index(){//index() 메서드 정의
return articleService.index();
}
==============================================================
ArticleService.java 파일
public List<Article> index() {
return articleRepository.findAll();
}
- 단일 게시글 조회 요청 개선하기
- 단일 게시글을 조회하는 show() 메서드의 주석 제거 후 return 문의 articleRepository.findById(id).orElse(null)을 삭제하고 articleService.show(id) 작성
- ArticleService에 show() 메서드 생성후 DB에서 id로 조회한 결과를 반환 하도록 return 문 작성
ArticleApiController.java 파일 @GetMapping("/api/articles/{id}") public Article show(@PathVariable Long id) { return articleService.show(id); } ============================================================== ArticleService.java 파일 public Article show(Long id) { return articleRepository.findById(id).orElse(null); }
게시글 생성 요청 개선하기
- 컨트롤러의 create()메서드의 주석 제거
- dto.toEntity() 를 삭제후 articleService.create()를 호출하고 POST 요청 메세지에 담긴 데이터도 전달해야 하므로 괄호안에 dto 넣음
- create() 메서드를 호출해 얻은 결과를 Article 타입의 created 에 넣음
- return 문의 결과 created가 null이 아니면 good 요청을 아니면 bad 요청을 보내도록 삼항 연산자 사용
- return 문의 조건이 참이면 ResponseEntity의 상태에 OK, 본문에는 created를 보내고 거짓이면 ResponseEntity의 상태에 BAD_REQUEST, 본문은 없으므로 그냥 빌드만 해서 보냄
- ResponseEntity에 응답을 실어서 보냈기 때문에 create() 메서드의 반환형 ResponseEntity<Article>로 수정
- 서비스에 create() 메서드 추가
- dto를 엔티티로 변환해 article 객체에 저장
- 리파지터리에 article을 DB에 저장하도록 함
- id를 데이터 생성할 때 굳이 넣을 필요가 없기 때문에 article 객체에 id가 존재한다면 null을 반환하는 코드 추가
ArticleApiController.java 파일 @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(); } ============================================================== ArticleService.java 파일 public Article create(ArticleForm dto) { Article article = dto.toEntity(); if (article.getId() != null) { return null; } return articleRepository.save(article); }
게시글 수정 요청 개선하기
- 컨트롤러의 update() 메서드의 주석을 제거
- 클라이언트의 요청을 받아 응답하는 코드 작성, 데이터를 수정하는 코드는 서비스에 위임
- 서비스의 update() 메서드를 호출하고 매개변수로 id와 dto를 전달하고 updated 객체로 반환값을 받아 저장
- updated에 내용이 있다면 ResponseEntity의 상태에는 OK, 본문에는 updated을 넣고 내용이 없다면 ResponseEntity의 상태에는 BAD_REQUEST, 본문에는 그냥 빌드만 보냄
- 서비스에 update() 메서드 작성
- 수정용 엔티티를 생성하고 로그를 찍는 코드 작성 ,ArticleService 클래스 위에 @Slf4j 어노테이션 추가
- 대상 엔티티를 찾는 코드 작성
- 잘못된 요청을 처리하고 응답하는 코드를 작성
- DB에 수정 데이트를 업데이트 한후 최정적으로 updated에 저장된 데이터를 컨트롤러에 반환
ArticleApiController.java 파일 @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(); } ============================================================== ArticleService.java 파일 public Article update(@PathVariable Long id , @RequestBody ArticleForm dto ) { // 1. DTO -> 엔티티 변환하기 Article article = dto.toEntity(); log.info("id: {}, article: {}", id, article.toString()); // 2. 타깃 조회하기 Article target = articleRepository.findById(id).orElse(null); // 3. 잘못된 요청 처리하기 if (target == null || id != article.getId()) { log.info("잘못된 요청! id: {}, article: {}", id, article.toString()); return null; // 응답은 컨트롤러가 하므로 여기서는 그냥 null 반환 } // 4. 업데이트하기 target.patch(article); Article updated = articleRepository.save(target); return updated; // 응답은 컨트롤러가 함므로 여기서는 수정 데이터만 반환 }
게시글 삭제 요청 개선하기
- 컨트롤러의 delete() 메서드의 주석 제거
- 게시글 삭제 요청이 오면 컨트롤러는 요청을 받아 결과를 응답하는 역할만 하도록 수정 실제 작업은 서비스에 위임
- 서비스에서 delete(id) 메서드를 호출하고 데이터를 deleted 객체로 받아옴
- deleted 에 내용이 있으면 삭제가 되었다는 뜻이므로 ResponseEntity의 상태에는 NO_CONTENT , 본문은 그냥 빌드만 해서 보내고 deleted 에 내용이 없으면 삭제하지 못했다는 뜻이므로 ResponseEntity의 상태에는 BAD_REQUEST , 본문에는 그냥 빌드만 보냄
- 서비스에 delete()메서드 생성
- 대상 엔티티를 찾는 코드 작성
- 잘못된 요청을 처리하는 코드 작성
- 삭제한 대상을 컨트롤러에 보내기 위해 target을 반환
ArticleApiController.java 파일 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(); } ============================================================== ArticleService.java 파일 public Article delete(Long id) { // 1. 대상 찾기 Article target = articleRepository.findById(id).orElse(null); // 2. 잘못된 요청 처리하기 if (target == null) { return null; //응답은 컨트롤러가 하므로 여기서는 null 반환 } // 3. 대상 삭제하기 articleRepository.delete(target); return target; // DB에서 삭제한 대상을 컨트롤러에 반환 }
트랜잭션 맛보기
- 컨트롤러가 데이터 3개의 생성 요청을 받아 결과를 응답하도록 transactionTest() 라는 메서드 추가
- @PostMapping으로 "/api/traansaction-test" URL 요청으로 데이터 생성을 요청받음
- transaactionTest()라는 메서드생성후 매개변수는 ArticleForm 데이터를 List로 묶은 dtos를 선언하고 REST API 방식으로 POST요청을 받고 있으므로 @RequestBody로 붙여주고 서버에서 응답할 때 데이터 생성 결과뿐만 아니라 상태 코드도 함께 보내므로 메서드의 반환형 ResponseEntity<List<Article>>로 설정
- 컨트롤러는 결과만 반환하기에 실제 작업하는 서비스 하도록 컨트롤러에 코드를 작성
- 서비스의 createArticles() 메서드를 호출 할때 매개변수로 받은 dtos도 함께 전달하고 반환값을 createdList라는 리스트에 저장
- createdList에 내용이 있으면 ResponseEntity의 상태에는 OK , 본문에는 createdList를 실어서 보내고 내용이 없으면 ResponseEntity의 상태에는 BAD_REQUEST , 본문에는 빌듬만 보냄
- 서비스에서 createdArticles() 메서드 작성
- dto 묶음(리스트)을 엔티티 묶음(리스트)으로 변환하기
- dtos를 스트림화 함
- map() 으로 dto가 하나하나 올 때마다 dto.toEntity()를 수행해 매핑
- 매핑된것을 리스트로 묶기
- 결과를 articleList에 저장
- 엔티티 묶음(리스트)을 DB에 저장하기
- articleList를 스트림화 함
- article이 하나씩 올 때마다 articleRepository을 통해 DB에 저장
- 강제로 에러를 발생시키기
- orElseThrow() 메서드로 IllegalArgumentException을 발생시켜 "결제 실패!"라는 메시지 남김
-
- orElseThrow() 메서드는 값이 존재함면 그 값을 반환하고 값이 존재하지 않으면 전달값으로 보낸 예외를 발생시킴
- IllegalArgumentException은 전달값이 없거나 유효하지 않는 경우를 뜻 함
-
- orElseThrow() 메서드로 IllegalArgumentException을 발생시켜 "결제 실패!"라는 메시지 남김
- 결과 값 반환하기
- 강제로 예외가 발생했지만 형식상 articleList을 반환
- dto 묶음(리스트)을 엔티티 묶음(리스트)으로 변환하기
- 서버의 재시작하고 Talend API Tester에서 POST 메서드, URL http://localhost:8887/api/transaction-test 하고 데이터를 입력하고 [Send] 버튼을 클릭하면 상태코드 500으로 응답
- IDE의 실행창으로 가면 "결제 실패!" 메시지 확인 가능 하지만 메시지를 띄우기 전에 insert문이 3번 수행됨
- http://localhost:8887/articles 페이지에 접속하면 데이터가 추가된 게 확인 가능 하지만 데이터 생성에 실패해서 나중에 추가한 데이터 3개가 남지않기를 원하기 때문에 트랜잭션을 선언
- 보통 트랜잭션은 서비스에서 관리하기 때문에 createArticles() 메서드 위에 @Transaction을 작성
- @Transaction 어노테이션은 동일한 이름의 어노테이션이 있기 때문에 org.springframework.transaction.annotation에서 임포트 하도록 선택
- 다시 서버를 재시작 하고 4번처럼 다시 POST 요청 함
- IDE의 실행창으로 가면 5번과 같은 결과를 확인 가능
- 6번과 같은 결과가 나오는것이 아닌 결제에 실패한 데이터는 롤백된것을 확인 가능
ArticleApiController.java 파일
@PostMapping("/api/transaction-test") // 여러 게시글 생성 요청 접수
public ResponseEntity<List<Article>> transactionTest(@RequestBody List<ArticleForm> dtos) {
// transactionTest() 메서드 정의
List<Article> createdList = articleService.createArticles(dtos); // 서비스 호출
return (createdList != null) ? ResponseEntity.status(HttpStatus.OK).body(createdList)
: ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
}
==============================================================
ArticleService.java 파일
@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. 강제 예외 발생시키기
// id가 -1인 데이터 찾고 찾는 데이터가 없으면 예외 발생
articleRepository.findById(-1L).orElseThrow(() -> new IllegalArgumentException("결제 실패!"));
// 4. 결과 값 반환하기
return articleList;
}
2023-12-12 14:06:31.321 ERROR 7456 --- [nio-8887-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.IllegalArgumentException: 결제 실패!] with root cause
java.lang.IllegalArgumentException: 결제 실패!
에러의 원인
'BE > 스프링 부트 3' 카테고리의 다른 글
14장 댓글 엔티티와 리파지터리 만들기 (1) | 2023.12.21 |
---|---|
13장 테스트 코드 작성하기 (1) | 2023.12.19 |
11장 REST API의 동작 이해하기 (0) | 2023.12.13 |
10장 REST API 와 JSON (0) | 2023.12.10 |
9장 CRUD와 SQL 쿼리 종합 (0) | 2023.12.06 |