포스트

스프링 부트3 제 12장

서비스? 롤백? 문제가 생기면 그 전으로 되돌린다고요?

스프링 부트3 제 12장

서비스(Service)

컨트롤러와 리파지터리 사이에 위치하는 계층으로, 서버의 핵심 기능(비즈니스 로직)을 처리하는 순서를 총괄한다. 대략적인 흐름은 아래와 같다.

웹 서버 흐름

서비스 계층 만들기

ArticleApiController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// ... 생략
    // 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) { // 게시글 쓰기
        // RequestBody는 본문에 실어 보내는 데이터를 메서드의 매개변수로 받아올 수 있게 하는 어노테이션
        Article created = articleService.create(dto);
        
        // 삼항 연산자(게시글 생성에 성공하면 정상, 실패하면 오류 응답
        return (created != null) ?
                ResponseEntity.status(HttpStatus.OK).body(created) :
                ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
    }

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

    // DELETE
    @DeleteMapping("/api/articles/{id}")
    public ResponseEntity<Article> delete(@PathVariable Long id) { // 단일 게시글 삭제
       Article deleted = articleService.delete(id);
        return (deleted != null) ?
                ResponseEntity.status(HttpStatus.NO_CONTENT).body(deleted) :
                ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
    }

기존의 articleRepository가 하던 일을 ArticleService가 담당하도록 분업했다.

ArticleService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// ... 생략
@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();
        if (article.getId() != null) { // article 객체에 id가 존재한다면 null 반환(기존 게시글 수정을 막기 위해)
            return null; // id는 DB가 생성하기 때문에 필요 없음
        }
        return articleRepository.save(article);
    }

    public Article update(Long id, ArticleForm dto) { // 게시글 수정
        // 1. 수정용 엔티티 생성
        Article article = dto.toEntity();
        log.info("id: {}, article: {}", id, article.toString());

        // 2. DB에 대상 엔티티가 있는지 확인
        Article target = articleRepository.findById(id).orElse(null);

        // 3. 잘못된 요청 처리
        if (target == null || id != article.getId()) { // DB에 데이터가 없거나 URL의 id와 게시글의 id가 맞지 않는 경우
            log.info("Error! id: {}, article: {}", id, article.toString());
            return null;
        }

        // 4. 있으면 업데이트하고 정상 응답 보내기
        target.patch(article);
        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;
    }
}

ctrl + / 를 누르면 선택한 라인이 모두 주석 처리된다.

트랜잭션(Transaction)

모두 성공해야 하는 일련의 과정을 뜻한다. 쪼갤 수 없는 업무의 최소 단위이기도 하다.

트랜잭션 수행 과정 중 실패할 경우, 진행 초기 단계로 되돌리는 것을 롤백(rollback)이라고 한다.

트랜잭션

트랜잭션 맛보기 코드

ArticleApiController의 transactionTest() 메서드

1
2
3
4
5
6
7
@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();
    }

ArticleService의 createArticles() 메서드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@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. 강제로 예외 발생시킴
        articleRepository.findById(-1L)
                .orElseThrow(() -> new IllegalArgumentException("결제 실패"));
        
        // 4. 결과 값 반환
        return articleList;
    }

@Transactional 어노테이션을 통해 메서드를 하나의 트랜잭션으로 만들 수 있다.

어노테이션이 없을 경우, 각 과정은 따로 수행되어 3번 과정에서 오류가 발생했음에도 DB에 데이터가 들어간 것을 볼 수 있다.
어노테이션이 있을 경우, 각 과정은 하나의 트랜잭션으로 취급되어 3번에서 오류가 발생하면 DB에 들어갔던 데이터가 다시 롤백된다.

스트림 문법은 배열 같은 자료구조를 순회하며 처리하는 코드 패턴이다.
for() 문 같은 반복문을 더 짧게 줄여 쓸 수 있다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.