본문 바로가기

web +a

스프링부트 ~20 | 서비스 계층 추가 & 트랜잭션 처리

라섹 4일챠다~!!

넘 심심해서 눈에 무리 안가게 살포시 공부해봤다.

 

Intro

지난시간 : Article을 CRUD하는 기능을 구현하는데, 특별히 REST api 구현 방식으로 구현했다. 요즘 백엔드 개발은 이러한 REST api 기반으로 개발한다

(∴일반 @Controller가 아닌 @RestController를 사용한다)

그러니까 잘 기억하며 공부하도록 하쟛~

 


오늘 해볼 것 & 사전지식

server에 서비스계층을 새로 추가해 볼 것이다.

 

∨ 서비스 계층

: 컨트롤러와 리포지토리 사이에 있는 계층으로, 처리 업무의 순서를 총괄한다.

: 일반적인 웹 서비스는 컨트롤러와 리포지토리 사이에 서비스 계층을 두어 역할을 나눈다. 

 

: 지금까지 실습한 건 하나의 컨트롤러 내에서 클라이언트 요청을 받고, 리포지토리에게 뭐 가져오라고 지시하는 이중 역할을 혼합해 수행중이었다. 이번에는 컨트롤러가 클라이언트의 요청만 받아내는 역할만 담당하도록 분리해줄 것이다.

 

 

 

∨ 컨트롤러 / 서비스 / 리포지토리 역할은 다음과 같은 비유로 이해가능하다.

음식점 : 손님이 주문하면 => 웨이터가 주문받고 => 셰프가 요리하고 => 재료같은건 보조요리사가 가져옴

웹서비스 : 클라이언트 요청 => 컨트롤러가 받고 => Service 계층의 처리 => Repository의 재료준비

 

 

 

∨ 트랜잭션 개념

서비스의 업무 처리는 트랜잭션 단위로 이루어진다.

어 이 트랜잭션은 예전에 기초공부해둘 때 만났다 : https://cherryjubilee.tistory.com/5

 

트랜잭션은 모두 성공되어야 하는 일련의 과정 & 실패 시 롤백이 이루어져야 한다.

트랜잭션 롤백의 책임도 서비스 계층에 있다.

서비스 계층의 메서드에 @Transactional을 붙이면 해당 메서드가 트랜잭션으로 관리된다.

 

.

.

 

∨ 서버를 어떻게 [ 컨트롤러 | 서비스 | 리포지토리] 로 분리할까?

 

- 분리 전 컨트롤러

: 컨트롤러에 Repository가 DI된 상황이었다.

: 즉 컨트롤러가 클라이언트 요청만을 받는 게 아니고, Repository와 직접 컨텍까지 하는 상황이었다.

 

- 이렇게 수정하자

: 컨트롤러는 Service에만 의존하도록 하자!

: Service는 Repository에만 의존하도록 하자!

더보기
@RestController
public
class ArticleApiController {
    @Autowired
    private ArticleService articleService;
}

 

@Service
public class ArticleService {
    @Autowired
    private ArticleRepository articleRepository;
}

@Service 어노테이션 : 스프링 부트에서 제공하는 어노테이션이다. 컨테이너에 Service 객체로 등록된다.

 

이제 이전과 다르게 컨트롤러와 Repository 사이에 Service 계층이 생겼다.

그럼 이전에 구현한 CRUD 기능을 어떻게 리팩터링 해야 하는지 공부해보자.

 


리팩터링 시작!! 

 

● 목록 조회 (: GET 요청 처리)

이전에는 Repository에서 데이터를 직접 가져왔다.

더보기
모든 목록을 조회

@GetMapping("/api/articles")
public List<Article> index() {
    return articleRepository.findAll();
}

 

단일 Entity를 조회

@GetMapping("/api/articles/{id}")
public Article index(@PathVariable Long id) {
    return articleRepository.findById(id).orElse(null);
}

서비스 계층을 추가한 지금은

articleService의 index() 메서드에게 조회 기능을 넘기자. 

그냥.. Service는 Repository를 가지고 있으니까.. 간단히 역할만 넘겨주면 된다.

 

ⓐ 전체 조회 (목록 조회)

- Controller - 
@GetMapping("/api/articles")
public List<Article> index() {
    return articleService.index();
}
- Service - 
public List<Article> index() {
    return articleRepository.findAll();
}

 

ⓑ 단건 조회

- Controller -
@GetMapping("/api/articles/{id}")
public Article show(@PathVariable Long id) {
    return articleService.show(id);
}
- Service -
public Article show(Long id) {
    return articleRepository.findById(id).orElse(null);
}

 

 

 


● 글 생성 (: POST 요청 처리)

이전에는 컨트롤러가 post 요청을 받으면 Repository에게 직접 생성을 부탁했다.

@PostMapping("/api/articles")
public Article create(@RequestBody ArticleDto dto) {
	Article article = dto.toEntity();
    return articleRepository.save(article);
}

 

리팩터링하쟛 : 서비스 계층으로 분리한 지금은.. ↓

컨트롤러가 받은 dto를 서비스에게 넘겨주면서 create()를 수행하도록 역할을 넘긴다.

- 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 create(ArticleForm dto) {
    Article article = dto.toEntity();
    // id값이 들어올 필요가 없다. DB가 자동으로 부여하도록 해뒀기 때문이다.
    // 만약 id값이 들어왔다면 생성이 아니라 수정의 의도가 있으므로 null을 반환하도록 하자.
    if (article.getId() != null) {
        return null;
    }
    return articleRepository.save(article);
}

 

 


● 글 수정 (: PATCH 요청 처리) 

이전에는 컨트롤러가 Patch 기능을 다 처리하느라고 코드가 지저분했다.

 

여태까지 서비스에 역할을 분리해온 맥락으로 슉 수정해주자

- 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();
}
- Servoce -
public Article update(Long id, ArticleForm dto) {
    // 1: DTO -> 엔티티
    Article article = dto.toEntity();
    log.info("id: {}, article: {}", id, article.toString());
    // 2: 타겟 조회
    Article target = articleRepository.findById(id).orElse(null);
    // 3: 잘못된 요청 처리
        if (target == null || id != article.getId()) {
        // 400, 잘못된 요청 응답!
        log.info("잘못된 요청! id: {}, article: {}", id, article.toString());
        return null;
    }
    // 4: 업데이트
    target.patch(article);
    Article updated = articleRepository.save(target);
    return updated;
}

 

∨ 체크체크포인투

: 응답상태 처리 즉 ResponeEntity.status(~~) 이런거는 이제 컨트롤러가 하면 되는 거지, 서비스 계층에서 로직 수행하면서 응답처리까지 할 필요는 없어졌다. 그냥 정상적으로 Entity가 patch되었으면 그 Entity만 반환해주면 된다. 

 

 


● 글 삭제 (: DELETE 요청 처리)

이번에도.. 이전 Controler에서 다 처리하던 서비스 로직을.. Service 계층에게 위임해주자.

수정 후 Controller와 Service ↓

- Controller -
@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).build();
}
- Service -
public Article delete(Long id) {
    // 대상 찾기
    Article target = articleRepository.findById(id).orElse(null);
    // 잘못된 요청 처리
    if (target == null) {
        return null;
    }
    // 대상 삭제
    articleRepository.delete(target);
    return target;
}

 


 

트랜잭션 처리하기

 

상황: 서비스 계층에서 Article을 한번에 여러개 생성해내는 트랜잭션을 수행해야 한다. 

하지만 DB에 휘리리릭 저장하는 도중 에러를 만나면 롤백이 일어나야 한다.

해결 방법: 해당 메서드에 @Transactional 어노테이션을 붙이면 메서드가 트랜잭션 단위로 관리된다.

 


 

참고: 아직 눈이 흐려서 오타가 좀 있을 수 있따!

 

반응형
다른 블로그