라섹 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 어노테이션을 붙이면 메서드가 트랜잭션 단위로 관리된다.
참고: 아직 눈이 흐려서 오타가 좀 있을 수 있따!
'web +a' 카테고리의 다른 글
스프링부트 ~22 | 댓글 CRUD를 위한 Entity, Repository & Test (0) | 2022.07.26 |
---|---|
스프링부트 ~21 | 테스트코드 작성 관련 (0) | 2022.07.25 |
스프링부트 ~19 | REST API를 적용하여 CRUD 구현하기 (0) | 2022.07.21 |
스프링부트 ~18 | REST API와 JSON (0) | 2022.07.21 |
스프링부트 ~17 | SQL 쿼리 로깅 & CRUD 쿼리 실습 (0) | 2022.07.20 |