1. 서비스, 트랜잭션.

  • 서비스(service)
    • 컨트롤러리파지터리 사이에 위치한 계층.
    • 서버의 핵심 기능(비즈니스 로직)을 처리하는 순서를 총괄함.

컨트롤러 - 서비스 - 리파지터리의 관계(출처)

  • 컨트롤러(클라이언트의 요청 받기) -> 서비스(리파지터리에게 클라이언트의 요청에 대한 데이터를 반환해 달라고 요청) -> 리파지터리 -> 서비스 -> 컨트롤러(클라이언트에 응답)
  • 트랜잭션(transaction)
    • 일반적으로 서비스 업무 처리는 트랜잭션 단위로 진행됨.
    • 나눌 수 없는 처리의 최소 단위. (흔히 원자성이라 함.)
    • 모두 성공해야 하는 일련의 과정을 뜻함.
    • 실패로 돌아갈 경우 진행 초기 단계로 돌리는 것을 롤백(rollback)이라 함.

2. 서비스 계층.

  • 서비스 계층을 추가해서 컨트롤러, 서비스, 리파지터리의 역할을 분업.
@Service
public class ArticleService {
    @Autowired
    private ArticleRepository articleRepository;    // ArticleRepository 객체 주입.
}
@RestController
@Slf4j
public class ArticleApiController {
    @Autowired
    private ArticleService articleService;      // @Service로 인해 에러표시 사라짐.
}
  • @Service
    • 해당 클래스를 서비스로 인식해 서비스 객체를 생성.
  • (@Autowired) private ArticleService articleService;
    • @Service를 사용함으로써 에러표시 사라짐.
    • 객체 주입. (DI)

2-1. 모든 article 데이터 조회.

// controller
	@GetMapping("/api/articles")
    public List<Article> index() {      // 모든 데이터 조회.
        return articleService.index();
    }
// service
    public List<Article> index() {
        return articleRepository.findAll();
    }
  • controller
    • 클라이언트의 요청을 받으면 service에 있는 index() 메서드 호출.
  • service
    • articleRepositoryfindAll() 메서드를 통해 DB에서 조회한 데이터들을 반환.

2-2. 단일 article 조회.

// 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);
    }
  • controller
    • URL에 있는 {id} 변수를 @PathVariable을 이용해 매개변수로 받아서, service에 있는 show() 메서드 호출.
  • service
    • 매개변수를 통해 받은 id를 이용해서 repositoryfindById()메서드 호출함.
      • 조회한 값이 있으면 해당 데이터를 반환.
      • 조회한 값이 없으면 null을 반환.

2-3. 새로운 article 생성.

// 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();
        if (article.getId() != null) {
            return null;
        }
        return articleRepository.save(article);
    }
  • controller
    • REST API에서 데이터를 생성할 때는 JSON 데이터를 받아와야 하므로, @RequestBody를 통해 요청 Body(본문)에 실어 보낸 데이터매개변수(dto)매핑시켜줌.
    • servicecreate() 메서드 호출.
      • service에서 반환된 값
        • != null 이면 HTTP 상태 코드(200), 응답 Body에 생성한 데이터를 담아서 반환.
        • = null 이면 HTTP 상태 코드(400), 응답 Body에 null을 담아서 반환
  • service
    • 매개변수를 통해서 받은 dtoEntity로 변환.
    • id값은 DB에서 auto_increment로 자동 생성되기 때문에 id이 있을 경우 null을 반환.
    • 문제가 없다면 repositorysave()메서드를 통해 article엔터티를 저장하고 반환.

정상 처리

비정상 처리


2-4. article 수정.

// 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();
    }
// service
    public Article update(Long id, ArticleForm dto) {
        Article article = dto.toEntity();
        Article target = articleRepository.findById(id).orElse(null);
        if (target == null || id != article.getId()) {
            return null;
        }
        target.patch(article);
        return articleRepository.save(target);
    }
  • controller
    • URL에 있는 {id} 변수를 @PathVariable을 이용해 매개변수 id에 매핑.
    • JSON 데이터를 받아와야 하므로, @RequestBody를 통해 요청 Body(본문)에 실어 보낸 데이터매개변수(dto)에 매핑.
    • service에 있는 update()메서드 호출.
      • 반환된 결과
        • != null이면 HTTP 상태 코드(200), 응답 Body에 수정된 데이터를 담아서 반환.
        • = null이면 HTTP 상태 코드(400), 응답 Body에 null을 담아서 반환.
  • service
    • 매개변수로 받은 dtoEntity로 변환.
    • 매개변수로 받은 idrepositoryfindById()메서드를 이용해서 값을 조회.
      • 기존 데이터가 있다면, 해당 데이터를 저장.
      • 기존 데이터가 없다면, null을 저장.
    • null이 저장됐거나, URL id와 요청Body에 실려온 id값이 다를 경우 null반환.
    • if문이 false여서 지나쳐왔다면 patch()메서드를 호출해서 수정된 값이 있는 필드만 값을 갱신하고, save메서드를 통해 저장한 뒤 반환.

URL 요청id와 요청Body의 id가 다를 경우

일부 데이터만 수정한 경우

모든 데이터를 수정


2-5. article 삭제.

// 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;
    }
  • controller
    • URL에 있는 {id} 변수를 @PathVariable을 이용해 매개변수로 받아서, service에 있는 delete() 메서드 호출.
      • service의 반환값.
        • != null : HTTP 상태 코드(204) 반환.
        • = null : HTTP 상태 코드(400) 반환.
  • service
    • 매개변수로 받은 idrepositoryfindById()를 이용해서 값을 조회.
      • 기존 데이터가 있을 경우, 해당 데이터를 저장.
      • 없을 경우 null을 저장.
    • null일 경우 null을 반환.
    • if문이 false일 경우, repositorydelete()메서드를 호출해서 삭제한 뒤 반환.

3. 트랜잭션.

  • 반드시 성공해야만 하는 일련의 과정.
    • 만약 실패했다면 원래 상태로 롤백됨.
// controller
    @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();
    }
// service
	@Transactional
    public List<Article> createArticles(List<ArticleForm> dtos) {
        List<Article> articleList = dtos.stream().map(dto -> dto.toEntity()).collect(Collectors.toList());
//        List<Article> articleList = new ArrayList<>();
//        for (int i = 0; i < dtos.size(); i++) {
//            ArticleForm dto = dtos.get(i);
//            Article entity = dto.toEntity();
//            articleList.add(entity);
//        }
        articleList.stream().forEach(article -> articleRepository.save(article));
//        for (int i = 0; i < articleList.size(); i++) {
//            Article article = articleList.get(i);
//            articleRepository.save(article)
//        }
        articleRepository.findById(-1L).orElseThrow(() -> new IllegalArgumentException("실패!"));     // 강제로 예외 발생.
        return articleList;
    }
  • Controller
    • @PostMapping("/api/transaction-test")
      • @PostMapping으로 /api/transaction-test요청 URL 받음.
        • 데이터 생성 요청.
    • public ResponseEntity<List<Article>> transactionTest(@RequestBody List<ArticleForm> dtos)
      • 데이터를 여러개 받으니 매개변수 타입이 List<>, 메서드 반환타입도 List<>로 해줌으로써 응답시 List형태로 보냄.
        • 또한 반환시 상태코드도 보내주기 위해서 ResponseEntity
        • 즉, ResponseEntity<List<Article>>이러한 형태.
      • REST API방식의 Post요청을 받으므로 @RequestBody어노테이션 추가.
        • Post 요청 시 Body에 실려있는 데이터를 메서드 매개변수로 매핑시킴.
  • Service
    • 주석처리된 부분을 좀 더 깔끔하게 가독성 좋게 처리.
    • @Transactional
      • 해당 어노테이션을 추가해줌으로써 createArticles 메서드는 하나의 트랜잭션으로 묶임.
        • 즉 메서드 실행 중 실패하면 롤백됨.
    • List<Article> articleList = dtos.stream().map(dto -> dto.toEntity()).collect(Collectors.toList());
      • dtos 묶음을 entity 묶음으로 변환.
        • dtos.stream()
          • dtosstream으로 변환시킴.
        • .map(dto -> dto.toEntity())
          • map스트림의 각 요소를 다른 값으로 변환하는 메서드.
          • dto 객체 하나하나를 dto.toEntity() 메서드를 호출하여 변환.
        • .collect(Collectors.toList())
          • stream에서 다시 리스트 형태로.
      • 스트림(stream)문법은 List와 같은 자료구조에 저장된 요소를 하나씩 순회하면서 처리하는 코드 패턴.
    • articleList.stream().forEach(article -> articleRepository.save(article));
      • articleList.stream()
        • ListStream으로 변환.
      • forEach(article -> articleRepository.save(article))
        • forEach는 스트림의 각 요소를 순회하는 메서드.
        • 람다 표현식 article -> articleRepository.save(article)을 사용하여 각 Article 객체articleRepository를 통해 저장하는 작업을 수행.
    • articleRepository.findById(-1L).orElseThrow(() -> new IllegalArgumentException("실패!"));
      • id값에 -1(없는 값)을 넣어서 강제로 예외 발생시킴.
profile
Every cloud has a silver lining.

0개의 댓글