스프링부트 ~23 | 댓글 CRUD를 위한 Controller & Service (+ 댓글 Rest API 완성하기)
Intro
지난 시간 : Comment Entity를 추가했다! 그리고 Repository까지 준비해뒀다.
이번 시간 : Comment CRUD를 위한 Controller와 Service를 추가해보자.
이로써 REST api가 완성된다 >v <
Article 기능 만들 때 다 했던 거다. 똑같은 과정을 거칠 것이다.
지금까지 배운 것을 상기하면서 다시 보자.
컨트롤러와 서비스 - DI
컨트롤러 DI
∨ REST API로 설계하고 있음을 기억하면서, 컨트롤러에 @RestController를 사용해주자.
∨ 컨트롤러는 CRUD 요청을 받고, 실제 CRUD 처리는 서비스에게 위임한다는 것을 상기하자.
@RestController
public class CommentApiController {
@Autowired
private CommentService commentService;
}
서비스 DI
서비스는 리포지토리에 접근해서 실제 데이터 처리를 담당한다.
댓글서비스에서는 댓글리포뿐만 아니라 게시글리포도 사용할 일이 있기 때문에 두개 다 땡겨와준다.
@Service
public class CommentService {
@Autowired
private CommentRepository commentRepository;
@Autowired
private ArticleRepository articleRepository;
}
컨트롤러 - CRUD 구현
∨ 댓글 목록 조회 / 댓글 생성 / 댓글 수정 / 댓글 삭제 기능을 추가해보자.
순서대로 @GetMapping / @PostMapping / @PatchMapping / @DeleteMapping으로 요청을 받아냈던 것을 기억하자.
∨ 반환타입도 배운대로 신경써서 정해주자.
∨ 잘못된 요청에 대한 처리 : 이전에는 컨트롤러에서 경우를 다뤄줬는데, 이번에는 서비스에서 예외를 발생시키도록 했다.
@GetMapping("/api/articles/{articleId}/comments")
public ResponseEntity<List<CommentDto>> comments(@PathVariable Long articleId) {
return null;
}
@PostMapping("/api/articles/{articleId}/comments")
public ResponseEntity<CommentDto> create(@PathVariable Long articleId,
@RequestBody CommentDto dto) {
return null;
}
@PatchMapping("/api/comments/{id}")
public ResponseEntity<CommentDto> update(@PathVariable Long id,
@RequestBody CommentDto dto) {
return null;
}
@DeleteMapping("/api/comments/{id}")
public ResponseEntity<CommentDto> delete(@PathVariable Long id) {
return null;
}
recall) @RequestBody
※ rest API에서 json으로 데이터를 던질 때 @RequestBody 어노테이션을 표시해주어야 받을 수 있다. (반면 폼데이터 dto는 저 어노테이션 없이 받아올 수 있었다)
※ @RequestBody : JSON 데이터 받기
ㄴ 요청의 body에서 데이터를 받아오란 의미
컨트롤러 역할 작성하기
대충 잡은 CRUD의 틀을 컨트롤러에서 완성해보자.
일단 컨트롤러에서 쓱쓱 작성하고 서비스에서 원하던 작업을 처리하도록 구현하도록 하자.
+ 부가적으로 필요한 형식이나 기능은 그때그때 만든다.
댓글 목록 조회
@GetMapping("/api/articles/{articleId}/comments")
public ResponseEntity<List<CommentDto>> comments(@PathVariable Long articleId) {
// 서비스에게 위임
List<CommentDto> dtos = commentService.comments(articleId);
// 결과 응답
return ResponseEntity.status(HttpStatus.OK).body(dtos);
}
조회 결과를 Comment가 아니라 CommentDto의 List로 받아오도록 정했다.
그래서 이때 CommentDto 형식을 새로 추가해주었다.
@AllArgsConstructor
@NoArgsConstructor
@Getter
@ToString
public class CommentDto {
private Long id;
@JsonProperty("article_id")
private Long articleId;
private String nickname;
private String body;
}
참고) DB의 컬럼명은 article_id이고 CommentDto의 필드명은 articleId이다.
이럴 경우 Dto를 받아올 때 매치가 되지 않는 문제가 있다.
이런 경우 위와 같이 @JsonProperty("article_id")를 통해 매핑가능하다.
댓글 생성
∨ 생성요청시 들어오는 Json 데이터는 CommentDto 타입으로 들어온다.
@PostMapping("/api/articles/{articleId}/comments")
public ResponseEntity<CommentDto> create(@PathVariable Long articleId,
@RequestBody CommentDto dto) {
// 서비스에게 위임
CommentDto createdDto = commentService.create(articleId, dto);
// 결과 응답
return ResponseEntity.status(HttpStatus.OK).body(createdDto);
}
댓글 수정
∨ 수정요청시 들어오는 Json 데이터는 CommentDto 타입으로 들어온다.
@PatchMapping("/api/comments/{id}")
public ResponseEntity<CommentDto> update(@PathVariable Long id,
@RequestBody CommentDto dto) {
// 서비스에게 위임
CommentDto updatedDto = commentService.update(id, dto);
// 결과 응답
return ResponseEntity.status(HttpStatus.OK).body(updatedDto);
}
댓글 삭제
@DeleteMapping("/api/comments/{id}")
public ResponseEntity<CommentDto> delete(@PathVariable Long id) {
// 서비스에게 위임
CommentDto deletedDto = commentService.delete(id);
// 결과 응답
return ResponseEntity.status(HttpStatus.OK).body(deletedDto);
}
이렇게 컨트롤러의 역할 명시는 다 끝났다.
이제 컨트롤러가 서비스에게 넘긴 저러한 기능들을 서비스에서 마저 구현해주면 된다.
서비스층 기능 구현하기
댓글 목록 조회
public List<CommentDto> comments(Long articleId) {
// 반환 : stream 문법을 사용하여 깔끔하게 반환 가능하다. for문을 이용해도 된다.
return commentRepository.findByArticleId(articleId)
.stream()
.map(comment -> CommentDto.createCommentDto(comment))
.collect(Collectors.toList());
}
참고) 위 stream 문법은 다음 for문과 동일한 기능을 한다. 참고해서 이해하자.
// 조회: 댓글 목록
List<Comment> comments = commentRepository.findByArticleId(articleId);
// 변환: 엔티티 -> DTO
List<CommentDto> dtos = new ArrayList<CommentDto>();
for (int i = 0; i < comments.size(); i++) {
Comment c = comments.get(i);
CommentDto dto = CommentDto.createCommentDto(c);
dtos.add(dto);
}
// 반환
return dtos;
참고) Entity를 Dto로 변환하기 위해 Dto에다가 static인 클래스메서드를 뒀다.
public static CommentDto createCommentDto(Comment comment) {
return new CommentDto(
comment.getId(),
comment.getArticle().getId(),
comment.getNickname(),
comment.getBody()
);
}
댓글 생성
댓글생성은 DB 변경을 수반하는 작업이기 때문에 @Transactional 처리를 잊지 말자.
@Transactional
public CommentDto create(Long articleId, CommentDto dto) {
// 게시글 조회 및 예외 발생
Article article = articleRepository.findById(articleId)
.orElseThrow(() -> new IllegalArgumentException("댓글 생성 실패: 대상 게시글이 없습니다"));
// 댓글 엔티티 생성
Comment comment = Comment.createComment(dto, article);
// 댓글 엔티티를 DB로 저장
Comment created = commentRepository.save(comment);
// DTO로 변경하여 반환
return CommentDto.createCommentDto(created);
}
주의) 이렇게 Comment Entity를 생성할 때 유효한 경우에 대해서만 생성을 해야 한다.
∴ 적절하게 예외 발생을 시켜주자.
public static Comment createComment(CommentDto dto, Article article) {
// 예외 발생
if (dto.getId() != null)
throw new IllegalArgumentException("댓글 생성 실패! 댓글의 id가 없어야 합니다.");
if (dto.getArticleId() != article.getId())
throw new IllegalArgumentException("댓글 생성 실패! 게시글의 id가 잘못되었습니다.");
// 엔티티 생성 및 반환
return new Comment(
dto.getId(),
article,
dto.getNickname(),
dto.getBody()
);
}
댓글 수정
∨ 역시 DB 변경을 요하는 수정작업이므로 트랜잭션 처리를 잊지 말자.
@Transactional
public CommentDto update(Long id, CommentDto dto) {
// 댓글 조회 및 예외 발생
Comment target = commentRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("댓글 수정 실패! 대상 댓글이 없습니다."));
// 댓글 수정
target.patch(dto);
// DB로 갱신
Comment updated = commentRepository.save(target);
// 댓글 엔티티를 DTO로 변환 및 반환
return CommentDto.createCommentDto(updated);
}
참고) Entity를 수정하는 인스턴스 메서드 patch()는 아래와 같다.
파라미터로 들어온 dto의 내용이 유효하다면, dto의 내용대로 적절히 patch시켜주면 된다.
public void patch(CommentDto dto) {
// 예외 발생
if (this.id != dto.getId())
throw new IllegalArgumentException("댓글 수정 실패! 잘못된 id가 입력되었습니다.");
// 객체를 갱신
if (dto.getNickname() != null)
this.nickname = dto.getNickname();
if (dto.getBody() != null)
this.body = dto.getBody();
}
댓글 삭제
∨ 역시 삭제는 DB를 건드리는 작업이니까 트랜잭션 처리를 잊지 말자.
@Transactional
public CommentDto delete(Long id) {
// 댓글 조회(및 예외 발생)
Comment target = commentRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("댓글 삭제 실패! 대상이 없습니다."));
// 댓글 삭제
commentRepository.delete(target);
// 삭제 댓글을 DTO로 반환
return CommentDto.createCommentDto(target);
}
와우~~!!
rest api 설계방식으로 crud 구현하기.. 이런거구나 한번 더 복습완뇨..
빨리 스스로도 해봐야게따 느낌굿