개발새발 블로그

[스프링부트3 자바 백엔드 개발 입문] 12장 - 서비스 계층과 트랜잭션 본문

백/spring

[스프링부트3 자바 백엔드 개발 입문] 12장 - 서비스 계층과 트랜잭션

복지희 2024. 2. 9. 02:43

- 트랜잭션 : 모두 성공해야 하는 일련의 과정(순서)

- 롤백 : 트랜잭션이 실패가 될 경우 진행 초기상태로 다시 돌리는 것.

 

<ArticleService>

@Slf4j
@Service
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();
//API id값을 전달해줬는데 그게 기존에 있는 id일 경우 수정하면 안되므로 400 뜨도록
if(article.getId() != null)
return null;
return articleRepository.save(article);
}

public Article update(Long id, ArticleForm dto) {
Article article = dto.toEntity();
log.info("id: {}, article:{}", id, article.toString());
Article target = articleRepository.findById(id).orElse(null);

if(target == null || id != article.getId()){
log.info("잘못된 요청! id:{}, article:{}", id, article.toString());
return null; //응답은 컨트롤러가 하므로
}

target.patch(article); //필드중 일부만 수정하는 경우를 위해서.. patch 메서드 별도로 생성
Article updated = articleRepository.save(target);
return updated;
}

public Article delete(Long id) {
//1. 대상 찾기
Article target = articleRepository.findById(id).orElse(null);
//2. 잘못된 요청 처리하기
if(target == null){
return null;
}
//3. 대상 삭제하기
articleRepository.delete(target);
return target;
}
}

 

 

<ArticleApiController>

@Slf4j //로그를 찍을 수 있도록
@RestController
public class ArticleApiController {
// @Autowired //게시글 리파지토리
// private ArticleRepository articleRepository;
@Autowired //서비스
private ArticleService articleService;

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

//POST
@PostMapping("/api/articles")
public ResponseEntity<Article> create(@RequestBody ArticleForm dto){ //dto로 받아와서
Article created = articleService.create(dto); //엔티티로 변환
return (created != null) ?
ResponseEntity.status(HttpStatus.OK).body(created):
ResponseEntity.status(HttpStatus.BAD_REQUEST).body(null);
}

//PATCH
@PatchMapping("/api/articles/{id}")
public ResponseEntity<Object> 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);
}

//DELETE
@DeleteMapping("/api/articles/{id}")
public ResponseEntity<Article> delete(@PathVariable Long id){
Article target = articleService.delete(id);
return (target != null)?
ResponseEntity.status(HttpStatus.NO_CONTENT).body(null):
ResponseEntity.status(HttpStatus.BAD_REQUEST).body(null);
}
}

 

 

<트랜잭션 맛보기>

@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();
}

 

@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;
}

 

이렇게 list형태로 api를 보내면, articleService에서 만든 강제오류때문에, 서버 오류를 지칭하는 500오류가 뜬다.

 

근데 그냥 저장하면 에러는 뜨지만, 새로추가한 저 2개의 데이터는 계속 db에 저장되어있는 상태다.

이 문제를 해결하기 위해 @Transaction 어노테이션을 사용하고, 보통 서비스에서 사용한다.