12일차 - 서비스 계층과 트랜잭션

2024. 8. 13. 17:56코딩 자율학습 스프링 부트3 자바 백엔드 개발 입문 스터디

서비스와 트랜잭션의 개념

서비스

서비스란 컨트롤러와 레포지토리 사이에 위치하는 계층으로, 서버의 핵심 기능을 처리하는 순서를 총괄한다.

  1. 클라이언트가 요청을 보낸다.
  2. 컨트롤러에서 요청을 받아 서비스로 전달한다.
  3. 서비스에서 코드 흐름을 따라 처리를 진행할 때 필요한 데이터를 레포지토리에서 받는다.
  4. 레포지토리는 DB에서 필요한 데이터를 가져와 서비스로 반환한다.

트랜잭션

일반적으로 서비스 업무 처리는 트랜잭션 단위로 진행된다. 트랜잭션 이란 모두 성공해야 하는 일련의 과정으로, 쪼갤 수 없는 업무 처리의 최소 단위이다.

서비스 계층 만들기

클라이언트 요청,응답과 레포지토리에서 데이터를 가져오는 역할을 컨트롤러에서 다 진행했는데, 컨트롤러에서는 클라이언트의 요청, 응답 역할, 서비스에서는 레포지토리에서 데이터를 가져오는 역할로 분업하여야 한다.

@Service
public class ArticleService() {

	@Autowired
	ArticleRepository articleRepository;

}
  • service 클래스를 생성하여 @Service 를 추가한다. 해당 어노테이션은 클래스를 서비스로 인식해 스프링 부트에 서비스 객체를 생성한다.
  • 서비스에서 레포지토리를 통해 데이터를 받을 수 있게 객체를 주입한다.

모든, 단일 게시글 조회 요청

  • Service
    • 서비스가 조회 요청을 처리한다.
public List<Article> index() { 
	return articleRepository.findAll(); 
} 

public Article show(Long id) { 
	return articleRepository.findById(id).orElse(null); 
}
  • Controller
    • 컨트롤러에서는 요청이 들어오면 서비스로 전달한다.
@GetMapping("/api/articles") 
public List<Article> index() { return articleService.index(); } 

@GetMapping("/api/articles/{id}") 
public Article show(@PathVariable Long id) { return articleService.show(id); }

게시글 생성 요청

  • Service
    • id 는 데이터를 생성할 때 DB가 자동 생성 해주기 때문에, id 값이 들어가지 않도록 방지한다.
public Article create(ArticleForm dto) { 
    Article article = dto.toEntity(); 
    if (article.getId() != null) { 
    return null; 
    } 
    return articleRepository.save(article); 
}
  • 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 update(Long id, ArticleForm dto) { 
    Article article = dto.toEntity(); 
    Article target = articleRepository.findById(id).orElse(null); 
    if (target == null || id != article.getId()) { 
    return null; 
    } 
    target.patch(article); 
    Article updated = articleRepository.save(target); 
    return updated; 
}
  • 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 delete(Long id, ArticleForm dto) { 
    Article article = dto.toEntity(); 
    Article target = articleRepository.findById(id).orElse(null); 
    if (target == null) { 
    return null; 
    } 
    articleRepository.delete(target); 
    return target; 
}
  • Controller
@DeleteMapping("/api/articles/{id}") 
public ResponseEntity<Article> delete(@PathVariable Long id, @RequestBody ArticleForm dto) { 
    Article deleted = articleService.delete(id, dto); 
    return (deleted != null) ? ResponseEntity.status(HttpStatus.NO_CONTENT).build() : ResponseEntity.status(HttpStatus.BAD_REQUEST).build(); 
}

트랜잭션 맛보기

/api/transaction-test URL 로 요청받을 수 있는 컨트롤러 메서드를 추가한다. 데이터를 한꺼번에 생성 요청 한 후에 오류를 발생시키고, 롤백되는지 확인해본다.

@PostMapping("/api/transaction-test")
public ResponseEntity<List<Article>> transactionTest(@RequestBody List<AritlcleForm> dtos) {
		List<Aritlcles> createdList = articleService.createArticles(dtos);
		
		return (createdList != null) ?
				ResponseEntity.status(HttpStatus.OK).body(createdList) :
				ResponseEntity.status(HttpStatus.BAD_REQUEST).build() ;
}
public List<Article> createdArticles(List<Article> dtos) {

		List<Article> articleList = dtos.stream()
							.map(dto -> dto.toEntity)
							.collect(Collectors.toList());
							
		articleList.stream()
				.forEach(article -> articleRepository.save(article));
				
		articleRepository.findById(-1L)
				.orElseThrow(() -> new IllegalArgumentException("결제 실패"));
				
		return articleList;
		
}
  • Service에서 id가 -1인 데이터를 찾게 함으로써 오류를 발생시켰고, 이 상태에서 데이터 생성 요청을 보내면 오류가 발생한다.

  • 하지만 데이터 조회를 했을 때 DB 에 저장이 되어있는 상태이다.

  • 이렇게 오류가 발생했을 때 데이터가 저장되지 않고 데이터 생성 실패 이전 상황으로 되돌리기 위해서 트랜잭션을 선언하면 된다.
@Transactional
public List<Article> createdArticles(List<Article> dtos) {

		List<Article> articleList = dtos.stream()
							.map(dto -> dto.toEntity)
							.collect(Collectors.toList());
							
		articleList.stream()
				.forEach(article -> articleRepository.save(article));
				
		articleRepository.findById(-1L)
				.orElseThrow(() -> new IllegalArgumentException("결제 실패"));
				
		return articleList;
		
}