~12.3 트랜잭션 맛보기(종이책 p.333~362)
서비스(service)란 컨트롤러와 리파지터리 사이에 위치하는 계층으로, 서버의 핵심 기능(비즈니스 로직)을 처리하는 순서를 총괄한다.
일반적으로 서비스 업무 처리는 트랜잭션 단위로 진행된다. 트랜잭션(transaction)이란 모두 성공해야 하는 일련의 과정을 뜻하며, 쪼갤 수 없는 업무 처리의 최소 단위이다.
트랜잭션이 실패로 돌아갈 경우 진행 초기 단계로 돌리는 것을 롤백(rollback)이라고 한다.
REST 컨트롤러(api/ArticleApiController.java) 코드를 연다. 이 컨트롤러는 리파지터리와 협업해 게시판의 데이터를 CRUD했는데, 여기에 서비스를 계층을 추가해서 컨트롤러, 서비스, 리파지터리의 역할로 분업한다.
먼저 ArticleApiController 내 모든 코드를 주석 처리한다. (전체 드래그 후 cmd + /)
객체 주입된 리파지터리를 서비스로 바꾼다. 맨 위에 두 줄을 블록 설정한 후 cmd + / 을 눌러 주석을 해제하고 다음과 같이 수정한다.
@Autowired
private ArticleService articleService; // 서비스 객체 주입
아직 ArticleService 클래스를 정의하지 않았으므로 기본 패키지(com.example.firstproject)에 service 라는 패키지를 만들고 그 안에 ArticleService 라는 클래스를 만든다.
자동 생성된 ArticleService 코드에 @Service 어노테이션을 붙인다. @Service 어노테이션은 해당 클래스를 서비스로 인식해 스프링 부트에 서비스 객체를 생성한다.
리파지터리와 협업할 수 있도록 articleRepository 필드를 추가하고 @Autowired를 통해 객체 주입한다.

이제 REST 컨트롤러로 돌아가서 주석을 하나씩 풀며 리팩터링 해본다.
index() 메서드의 주석을 푼다.
리파지터리를 통해서 데이터를 가져오던 것을 서비스를 통해서 가져온다.
return 문의 articleRepository.findAll()을 삭제하고 articleService.index() 메서드를 호출한다.
index() 메서드는 아직 만들어지지 않았으므로 빨간색으로 표시된 index() 메서드 위에 마우스를 올리고 Create method ‘index’ in ‘ArticleService’ 를 클릭해서 메서드를 만든다.
return articleRepository.findAll(); 문을 작성해 DB에서 조회한 결과를 반환한다.
Talend API Tester를 통해서 잘 동작하는 것을 확인할 수 있다.
create() 메서드의 주석을 푼다.
dto.toEntity()를 삭제하고 articleService.create()를 호출한다. 괄호 안에 dto를 넣어서 전달한다.
이름이 헷갈리므로 article에서 마우스 오른쪽 버튼을 누르고 Refactor → Rename을 클릭하고 created로 수정한다. 이렇게 하면 article이 사용된 모든 곳이 created로 바뀐다.
삼항 연산자를 사용하여 return 문에서 결과로 받은 created 가 null이 아니면 good 요청을 보내고 그렇지 않으면 bad 요청을 보내도록 한다.
POST 요청에 대한 응답으로 ResponseEntity에 응답을 실어 보내주기 위해 반환형을 ResponseEntity<Article>로 수정한다.
서비스에 create() 메서드를 추가한다.
기존의 있는 데이터로 생성 요청을 보내면 안되기 때문에 article 객체에 id가 존재한다면(null이 아니라면) null을 반환하는 코드를 추가한다.
public Article create(ArticleForm dto) {
Article article = dto.toEntity();
if (article.getId() != null) {
return null;
}
return articleRepository.save(article);
}

update() 메서드의 주석을 모두 해제한다.
그리고 모든 코드를 잘라내 메모장에 붙여 넣는다.
서비스의 update() 메서드를 호출해 updated 객체로 반환값을 받는다.

서비스 클래스에 update() 메서드를 만들고 메모장에 붙여 넣었던 코드를 그대로 가져온다.
잘못된 요청을 처리하는 부분에서 return값을 null로 바꾼다. 잘못된 요청은 컨트롤러가 처리할 것이기 때문이다.
마지막 업데이트 부분도 return값을 updated로만 바꾼다. 저장된 데이터만 반환하고 응답은 컨트롤러가 할 것이기 때문이다.
delete() 메서드의 주석을 모두 해제한다.
그리고 모든 코드를 잘라내 메모장에 붙여 넣는다.
ArticleService의 delete(id) 메서드를 호출하고, deleted 객체로 받아온다.

서비스 클래스에 delete() 메서드를 만들고 메모장에 붙여 넣었던 코드를 그대로 가져온다.
잘못된 요청을 처리하는 코드에서 return 값으로 null을 설정한다. 마찬가지로 응답은 컨트롤러가 하기 때문이다.
마지막으로 삭제한 대상을 컨트롤러에 보내주기 위해 return값으로 target을 반환한다.
트랜잭션은 반드시 성공해야만 하는 일련의 과정이다. 만약 성공하지 못하면 원래 상태로 복구, 다시 말해 롤백된다. 정말 그런지 확인해보기 위해 다음과 같은 시나리오로 테스트 해본다.
다음의 데이터를 http://localhost:8080/api/transaction-test로 POST 요청을 보낼것이다.
[
{
“title”: “시간 예약” ,
“content”: “11111”
},
{
“title”: “테이블 지정” ,
“content”: “22222”
},
{
“title”: “메뉴 선택” ,
“content”: “33333”
}
]
ResponseEntity<List<Article>>로 한다.여기까지 하고 서버를 재시작한 후 Talend API Tester에서 데이터3개로 POST 요청을 보내면 상태 코드 500을 응답하고 에러가 났다는 것을 의미한다.
하지만 데이터 생성에 실패하였는데, DB에는 3개의 데이터가 담겨있는 것을 확인할 수 있다.
따라서 실패했을 경우 데이터가 남지 않게 하기 위해 createArticles() 메서드 위에 @Transactional을 붙인다.
서버를 재시작하고 확인해보면 에러가 발생한 후 데이터가 DB에 저장되지 않는 것을 확인할 수 있다.

