서비스 계층과 트랜잭션
오늘은 서비스 계층과 트랜잭션에 대해 알아보았다.
Service 계층이란 Controller와 Repository 사이에 위치한 계층으로서, 처리업무의 순서를 총괄하는 계층이다.
예를 들어 Controller가 식당의 웨이터라 생각하면 웨이터가 명령을 받아오면 쉐프인 Service가 명령을 동작하고 반환한다. Repository라는 매니져는 쉐프에게 필요한 재료를 가져다 주는 역할이다.
이렇게 Service 계층을 이용하면 역할이 확실하게 구분이 되어 코드를 유지 및 보수하기 편해진다.
하나의 클래스에 다 맡기는 것보다 중간에 계층을 나누어 분담하는 것이 MVC패턴의 모듈화(코드쪼개기)이다.
따라서 Controller에 있던 모든 코드들을 나누어서 역할 분담을 하게 하는 것이다. 만약 게시글을 추가하는 메서드를 만들고 싶다면, Service 계층에서 게시글을 추가하는 메서드를 만들고, Controller에서 Service 클래스의 메서드를 사용하는 방식이다.
RestController.java
@RestController
@Slf4j
public class ArticleApiController {
@Autowired // DI 외부에서 가져온다. (의존관계 주입)
private ArticleService articleService;
// GET
// 게시글 페이지 (메인 페이지)
@GetMapping("/api/articles")
public List<Article> index() {
return articleService.index();
}
// 게시글 상세 페이지
@GetMapping("/api/articles/{id}")
public Article index(@PathVariable Long id) {
return articleService.show(id);
}
// POST (게시글 추가)
@PostMapping("/api/articles")
public ResponseEntity<Article> create(@RequestBody ArticleForm dto) { // @RequestBody -> JSON 데이터 받기
Article created = articleService.save(dto);
return (created != null) ?
ResponseEntity.status(HttpStatus.OK).body(created):
ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
}
// PATCH (게시글 수정)
@PatchMapping("/api/articles/{id}")
public ResponseEntity<Article> edit(@PathVariable Long id, @RequestBody ArticleForm dto){ // Article을 담아서 ResponseEntity로 리턴 값을 보내야 한다. 그래야 응답 코드를 반환할 수 있다.
// ResponseEntity에 Article이 담겨서 JSON으로 반환이 된다.
Article updated = articleService.edit(id, dto);
return (updated != null) ?
ResponseEntity.status(HttpStatus.OK).body(updated):
ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
}
// DELETE
@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();
}
}
RestController 클래스를 보면, 대부분의 Mapping 메서드들의 반환값이 상태코드를 반환하거나, Service클래스의 메서드를 반환한다.
그렇다면 명령에 대한 동작을 하는 Service 계층을 보자.
service.java
@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 save(ArticleForm dto){
Article article = dto.toEntity();
if(article.getId() != null) { // id 값이 존재한다면 널값 반환(수정이 아니라 생성이기 때문에 기존 id 값의 데이터가 변경되면 안됨)
return null;
}
return articleRepository.save(article);
}
public Article edit(long id, ArticleForm dto){
// 1. 수정용 Entity 생성
Article article = dto.toEntity();
// 2. 대상 Entitu 찾기
Article target = articleRepository.findById(id).orElse(null);
// 3. 업데이트
target.patch(article);
Article updated = articleRepository.save(target);
// 4. 업데이트 값 반환
return updated;
}
// DELETE
@DeleteMapping("/api/articles/{id}")
public Article delete(Long id){
// 대상 Entity 조회
Article target = articleRepository.findById(id).orElse(null);
// 잘못된 요청 처리 (대상이 없는 경우)
if(target == null){
return null;
}
// 데이터 삭제 및 반환
articleRepository.delete(target);
return target;
}
}
@Service
각각의 동작들이 이 Service 클래스에 모여있는 것을 확인 할 수 있다.
이렇게 각각의 계층을 나누어서 코드를 만든다면 어디서 오류가 났는지도 편하게 확인할 수 있고, 고치기도 쉬워진다.
Transaction이란
Transaction은 수행되는 동작들이 순서대로 동작하다 실패하게되면 진행 초기 단계로 돌아가는 것이다. 순서대로 동작을 하다 예외처리가 나거나 실패하게 된다면, 다시 초기 단계로 돌아가는 것을 Rollback이라고 한다.
원자성은 트랜잭션이 DB에 모두 반영되거나, 전혀 반영되지 않거나를 뜻한다.
All or Nothing을 생각하면 된다.
일관성은 트랜잭션 작업 처리의 결과가 항상 일관되어야 한다를 뜻한다.
즉, 데이터 타입이 반환 후와 전이 항상 동일해야 한다.
독립성은 하나의 트랜잭션은 다른 트랜잭션에 끼어들 수 없고 마찬가지로 독립적임을 의미한다.
즉, 각각의 트랜잭션은 독립적이라 서로 간섭이 불가능하다.
지속성은 트랜잭션이 성공적으로 완료되면 영구적으로 결과에 반영되어야 함을 뜻한다.
보통 commit 이 된다면 지속성은 만족할 수 있다.
하나의 트랜잭션이 성공적으로 끝나서 데이터베이스가 일관성있는 상태에 있음을 의미한다.
트랜잭션의 원자성이 깨질 때, 즉 하나의 트랜잭션 처리가 비정상적으로 종료 되었을 때의 상태를 뜻한다.
Rollback 이 이뤄진다면 트랜잭션을 다시 실행하거나 부분적으로 변경된 결과를 취소할 수 있다.
@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( // id가 -1인 값을 찾을때
() -> new IllegalArgumentException("결재 실패!")
);
// 결과값 반환
return articleList;
}
이 트랜잭션 예제를 보면 talend tester를 통해 데이터 3개를 넣고 난 후 강제로 예외를 한다.
로그를 보게되면 3개의 데이터가 생성되었던 것을 확인 할 수 있다.
하지만 예외 값이 나와 DB를 확인 해보면 생성 되었던 3개의 데이터가 Rollback되어 없어져 있는 것을 확인 할 수 있다.
(ID 1,2,3 은 실행되면 초기에 저장되는 더미 데이터)
References (참고 자료)
https://www.inflearn.com/course/%EA%B0%9C%EB%85%90%EC%8B%A4%EC%8A%B5-%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8-
https://wonit.tistory.com/462