서비스
- 컨트롤러와 리파지터리 사이에 위치하는 계층
- 서버의 핵심 기능(비즈니스 로직)을 처리하는 순서를 총괄
- 클라이언트가 요청을 보내면 컨트롤러가 이를 받아 서비스로 전달하고 서비스는 받은 요청을 순서에 따라 진행
- 처리에 필요한 데이터는 리파지터리가 DB에서 가져와 반환함
- 이전장들에서 컨트롤러가 하던 일을 서비스 + 컨트롤러로 역할을 나눈것 (복잡한 로직일 수록 컨트롤러만으로 하기 힘들어짐)
트랜잭션
- 모두 성공해야만 정상적으로 완료됨
- 쪼갤 수없는 업무 처리의 최소 단위
- 보통 서비스 단계에서 관리함
- 트랜잭션 과정 중 오류가 발생하면 모두 롤백시킴
롤백
- 트랜잭션 내부에서 실행에 실패하면 지금까지 수행한 것을 모두 폐기하고 진행 초기 단계로 되돌아가는 것
@Service
- 해당 어노테이션이 선언된 클래스는 서비스 클래스로 인식되어 서비스 객체를 생성
- 컨트롤러는 객체주입(@Autowired를 통해 객체를 가져와 연결)하는 방식으로 서비스 객체를 사용
@Transactional
- 해당 어노테이션이 선언된 메서드를 트랜잭션으로 묶음
- 클래스에 이 어노테이션을 선언하면 클래스의 모든 메서드별로 각각의 트랜잭션이 부여됨
- 처음부터 끝까지 완전하게 수행 or (중간에 오류가 발생할 경우) 전혀 실행X(롤백)
서비스 계층 만들기
- 기존에 컨트롤러에서 하던 역할을 서비스에서 수행하도록 코드 수정
- 서비스에서 수행한 결과를 바탕으로 반환
api 컨트롤러
@Slf4j
@RestController
public class ArticleApiController {
@Autowired
private ArticleService articleService;
@GetMapping("/api/articles")
public List<Article> index() {
return articleService.index();
}
@GetMapping("/api/articles/{id}")
public Article show(@PathVariable Long id) {
return articleService.show(id);
}
@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).body(null);
}
@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).body(null);
}
@DeleteMapping("/api/articles/{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).body(null);
}
}
Service 컨트롤러
@Service
@Slf4j
public class ArticleService {
@Autowired
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) {
//DTO -> 엔티티 변환
Article article = dto.toEntity();
log.info("id : {}, article: {}",id,article.toString());
//타깃 DB에서 조회하기
Article target = articleRepository.findById(id).orElse(null);
//잘못된 정보 처리
if(target == null || article.getId() != id){
log.info("잘못된 요청! id : {}, article: {}",id,article.toString());
return null;
}
//업데이트(수정) 및 정상응답하기
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;
}
}
트랜잭션 (Test)
- 여러 입력을 받도록 하는 메서드를 컨트롤러와 서비스에 추가
- 서비스의 메서드에 @Transaction어노테이션 추가
- 테스트를 위해 억지로 오류 발생
- 오류발생 전에 저장된 데이터가 롤백 되는지 확인
api 컨트롤러(트랜잭션)
@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).body(null);
}
Service 컨트롤러 (트랜잭션)
@Transactional
public List<Article> createArticles(List<ArticleForm> dtos) {
//dto 묶음을 엔티티 묶음으로 변환하기
List<Article> articleList = dtos.stream().map(dto -> dto.toEntity()).collect(Collectors.toList());
//엔티티 묶음을 DB에 저장하기
articleList.stream().forEach(article -> articleRepository.save(article));
//강제 예외 발생시키기
articleRepository.findById(-1L).orElseThrow(()-> new IllegalArgumentException("결제 실패!"));
//결과 값 반환하기
return articleList;
}