게시판 만들기(스프링 부트3, 나도코딩 스터디)/12장. 서비스 계층과 트랜잭션

12.1~12.3 서비스 계층과 트랜잭션(게시판 만들기 / 길벗 코딩 자율학습단)

coding232624 2023. 12. 2. 23:11

서비스

  • 컨트롤러와 리파지터리 사이에 위치하는 계층
  • 서버의 핵심 기능(비즈니스 로직)을 처리하는 순서를 총괄
  • 클라이언트가 요청을 보내면 컨트롤러가 이를 받아 서비스로 전달하고 서비스는 받은 요청을 순서에 따라 진행
  • 처리에 필요한 데이터는 리파지터리가 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;
}