1. 개요.

(이미지 출처)

REST(Representational State Transfer) API

  • REST Controller
    • 댓글 REST API를 위한 컨트롤러로서 서비스와 협업.
    • 클라이언트의 요청을 받아서 응답하고, 뷰(view)가 아닌 데이터를 반환.
  • Service
    • REST 컨트롤러리파지터리 사이에서 비즈니스 로직을 담당.
    • 즉, 처리 흐름을 담당하고 예외 상황이 발생하였을 때 @Transactional로 변경된 데이터를 롤백.
  • DTO(Data Transfer Object)
    • 사용자에게 보여 줄 댓글 정보를 담은 것.
    • 클라이언트서버간에 댓글 JSON 데이터를 전송.
  • Entity
    • DB 데이터를 담는 Java 객체로 엔터티를 기반으로 테이블이 생성됨.
    • 리파지터리가 DB에 있는 데이터를 조회하거나 전달할 때 사용.
  • Repository
    • 엔터티를 관리하는 인터페이스.
    • 데이터 CRUD 등의 기능을 제공함.
    • 서비스로부터 CRUD 등의 명령을 받아서 DB에 보내고 응답을 받음.

댓글 CRUD를 위한 REST API 주소 설계

  • JSON <======> REST API
methodURL
GET/articles/articleId/comments
POST/articles/articleId/comments
PATCH/comments/id
DELETE/comments/id

2. 컨트롤러와 서비스의 틀 만들기.

  • REST API를 구현하려면 Controller가 아닌 REST Controller를 만들어야 함.
@RestController
public class CommentApiController {
    @Autowired
    private CommentService commentService;
}
  • @RestController
    • REST 컨트롤러 선언.
  • @Autowired private CommentService commentService;
    • 서비스 객체 주입.
@Service
public class CommentService {
    @Autowired
    private ArticleRepository articleRepository;
    @Autowired
    private CommentRepository commentRepository;
}
  • @Service
    • 해당 클래스를 서비스로 선언.
  • @Autowired CommentRepository 뿐만 아니라 ArticleRepository까지 필요한 이유.
    • ArticleRepository가 있어야 댓글을 생성할 때 해당 게시글의 존재 여부를 파악할 수 있기 때문.

2-1. 댓글 조회.

  • GET : /articles/articleId/comments


2-1-1. DTO.

@AllArgsConstructor
@NoArgsConstructor
@Getter
@ToString
public class CommentDto {
    private Long id;
    private Long articleId;			// 댓글의 부모(article) id
    private String nickname;
    private String body;

    public static CommentDto createCommentDto(Comment comment) { 
        return new CommentDto(         
                  comment.getId()                
                , comment.getArticle().getId()    
                , comment.getNickname()          
                , comment.getBody()); 
    }
}
  • createCommentDto 메서드.
    • static, 즉, 정적 메서드로 선언되어 있음.
      • 객체 생성 없이 호출 가능한 메서드라는 뜻.
    • 이러한 방식으로 객체를 만드는 메서드를 생성 메서드 ≒ 정적 팩토리 메서드라 함.
  • return new CommentDto
    • 메서드의 반환값이 DTO가 되도록 CommentDto의 생성자를 호출.
      • comment.getId()
        • 매개변수로 받은 댓글 엔터티의 id.
      • comment.getArticle().getId()
        • 댓글 엔터티가 속한 부모 게시글의 id.
      • comment.getNickname()
        • 댓글 엔터티의 닉네임.
      • comment.getBody())
        • 댓글 엔터티의 body.

2-1-2. 컨트롤러.

// controller
    @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);
    }
  • {articleId}
    • URL에서 조회하는 게시글의 id를 변수화해서 유연한 코드.
  • @PathVariable Long articleId
    • URL에 있는 변수 articleId값을 매개변수와 매핑.
  • List<CommentDto> dtos = commentService.comments(articleId);
    • 서비스에 있는 comments메서드를 호출하고 argumentarticleId를 넘겨줌.
    • List<commentDto>타입의 dtos 변수에 저장함.
  • return
    • 상태 코드로 200, 본문(Body)에는 조회한 댓글의 목록인 DTOs를 실어 보냄.

2-1-3. 서비스.

    public List<CommentDto> comments(Long articleId) {
        List<Comment> comments = commentRepository.findByArticleId(articleId);
        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;
    }
  • 매개변수로 Long articleId값을 받음.
  • List<Comment> comments = commentRepository.findByArticleId(articleId);
    • 리파지토리findByArticleId메서드를 이용해서 articleId번에 있는 모든 댓글을 가져옴.
    • List<Comment> 타입의 comments 변수에 저장.
  • List<CommentDto> dtos = new ArrayList<CommentDto>();
    • 조회한 댓글 엔터티 목록DTO 목록으로 변환하기 위해서 CommentDto를 저장하는 빈 ArrayList를 만들고 List<CommentDto>타입의 dtos변수에 저장.
  • for 문
    • 비어있는 dtos에 DTO를 하나씩 저장하는 식.
      • comments(엔터티 목록)에서 엔터티를 하나씩 빼서 DTO로 변환 후 저장.
    • comments.size()
      • 가져온 엔터티의 개수만큼 반복함.
    • comments.get(i);
      • 엔터티 목록에서 엔터티를 하나씩 빼서 변수에 저장.
    • CommentDto.createCommentDto(c);
      • CommentDto의 createCommentDto(c)메서드를 호출해서 엔터티를 DTO로 변환한 결과를 저장.
    • dtos.add(dto);
      • dtos 리스트에 add메서드를 통해 dto를 저장.
  • return dtos;
    • dtos를 반환.

2-1-4. 서비스 코드 리팩토링.

  • for 문 -----> 스트림(stream).
    • 코드의 가독성 , 코드의 양
    public List<CommentDto> comments(Long articleId) {
        return commentRepository.findByArticleId(articleId)
                .stream().map(comment -> CommentDto.createCommentDto(comment))
                .collect(Collectors.toList());
    }
  • commentRepository.findByArticleId(articleId)
    • 리파지터리의 findByArticleId메서드를 호출해서 댓글 엔터티 리스트를 가져옴.
  • .stream()
    • 가져온 엔터티 리스트를 스트림으로 변환.
      • 스트림(stream)은 컬렉션이나 리스트에 저장된 요소들을 하나씩 참조하며 반복처리할 때 사용.
  • .map(comment -> CommentDto.createCommentDto(comment))
    • 스트림화 된 댓글 엔터티 리스트를 DTO로 변환.
    • 스트림에서 댓글 엔터티를 하나씩 빼서 DTO로 매핑.
      • 형식 : .map(a -> b) : 스트림의 각 요소(a)를 꺼내 b를 수행한 결과로 매핑
    • 각 엔터티(comment)를 스트림으로부터 입력받아 createCommentDto(comment)메서드를 호출해서 얻은 DTO를 다시 스트림에 매핑.
  • .collect(Collectors.toList());
    • 메서드의 반환타입을 맞추기 위한 collect() 메서드.
    • 스트림 데이터를 리스트로 반환 받을 수 있음.

2-2. 댓글 생성.

  • POST : /articles/articleId/comments


2-2-1. 컨트롤러

    @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);
    }
  • @PostMapping("/api/articles/{articleId}/comments")
    • POST로 보낸 요청을 받음.
    • 글 번호는 {articleID}로 변수 처리해서 유연하게.
  • (@PathVariable Long articleId, @RequestBody CommentDto dto)
    • @PathVariable로 요청 URL의 {articleID}을 받음.
    • @RequestBody는 HTTP 요청 본문에 실린 데이터(JSON, XML, YAML)Java객체로 변환해줌.
      • @RequestBody을 이용해서 dto로 생성할 댓글 정보를 받음.
  • CommentDto createdDto = commentService.create(articleId, dto);
    • 서비스에 있는 create(articleId, dto) 메서드 호출.
      • argument로 articleId, dto값을 넘겨줌.
    • 메서드의 반환값은 CommentDto.
  • return
    • 상태코드 200, 본문(body)에는 생성한 댓글 데이터인 createdDto를 실어서 보냄.

2-2-2. 서비스.

    @Transactional
    public CommentDto create(Long articleId, CommentDto dto) {
    	// 1번.
        Article article = articleRepository.findById(articleId).orElseThrow(() -> new IllegalArgumentException("댓글 생성 실패 " + "대상 게시글이 없음"));
        // 2번.
        Comment comment = Comment.createComment(dto, article);
        Comment created = commentRepository.save(comment);
        // 3번.
        return CommentDto.createCommentDto(created);        // Entity -> DTO 변환해서 리턴.
    }
  • create의 경우 데이터를 추가, 즉 DB의 데이터에 변동을 주기 때문에 @Transactional을 추가해서 실패했을 경우 롤백하도록.
  • 1번
    • 매개변수로 받은 articleId를 통해 DB에 데이터를 조회함.
    • 만약 게시글이 없을 경우 orElseThrow()메서드로 예외를 발생시킴.
      • orElseThrow()메서드는 Optional 객체(= null이 될수도 있는 객체)에 값이 존재하면 그 값을 반환하고, 존재 하지 않는다면 전달값으로 보낸 예외를 발생시키는 메서드.
      • 현재는 전달값으로 IllegalArgumentException 클래스를 사용하여 메서드가 잘못 됐거나, 전달값이 잘못 보냈음을 뜻함.
  • 2번
    • Comment의 생성 메서드(정적 팩토리 메서드) createComment(dto, article)를 호출해서 댓글 엔터티를 반환받음.
      • 댓글 DTO, 게시글 엔터티를 argument로 받아서 댓글 엔터티를 만듦.
  • 3번
    • created 엔터티를 DTO로 변환한 후 반환.
    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()
        );
    }
  • 예외 발생하는 경우.
    • DTO에 id가 존재하는 경우.
      • why? : 엔터티의 id는 DB가 자동으로 생성해주니깐.
    • DTO에서 가져온 부모 게시글의 id와 엔터티에서 가져온 부모 게시글의 id가 다를 경우.
      • 즉, JSON 데이터URL 요청 정보가 다르다는 뜻.
  • 예외 상황이 발생하지 않았다면 엔터티를 생성해서 반환.
    • new Comment() 생성자 호출.
    • 전달값으로 필요한 id, nickname, body는 dto에서 가져오고, 부모 게시글은 article 자체를 입력.

2-3. 댓글 수정.

  • PATCH : /comments/id
    닉네임, 내용 둘 다 수정.

닉네임만 수정.

내용만 수정


2-3-1. 컨트롤러.

    @PatchMapping("/api/comments/{id}")
    public ResponseEntity<CommentDto> update(@PathVariable Long id, @RequestBody CommentDto dto) {
    	// 1번.
        CommentDto updatedDto = commentService.update(id, dto);
        // 2번.
        return ResponseEntity.status(HttpStatus.OK).body(updatedDto);
    }
  • @PatchMapping("/api/comments/{id}")
    • 댓글 수정을 받음.
    • {id}는 수정 대상 댓글의 id
  • @PathVariable Long id, @RequestBody CommentDto dto
    • URL에 {id}변수를 @PathVariable을 이용해서 매개변수 id로 받아옴.
    • @RequestBody를 통해 JSON데이터를 dto로 받음.
  • 1번
    • 서비스에 update() 메서드를 호출하고 argument로 id, dto를 넘겨줌.
    • 메서드 반환타입은 CommentDto.
  • 2번
    • 상태 코드(200), 본문(body)에는 수정한 댓글 데이터(updatedDto)를 실어 보냄.

2-3-2. 서비스.

    @Transactional
    public CommentDto update(Long id, CommentDto dto) {
    	// 1번.
        Comment target = commentRepository.findById(id)
                            .orElseThrow(() -> new IllegalArgumentException("댓글 수정 실패 " + "대상 댓글이 없음."));
		// 2번.
        target.patch(dto);
        Comment updated = commentRepository.save(target);
        // 3번.
        return CommentDto.createCommentDto(updated);
    }
  • DB의 내용을 변경하는 기능이므로 실패할 경우 롤백하기 위해서 @Transactional을 사용.
  • 1번
    • 매개변수로 받은 id를 통해 데이터를 조회하고 값이 있다면 변수에 저장,
      없다면 orElseThrow()메서드로 예외 발생시킴.
  • 2번
    • Comment의 patch(CommentDto dto)메서드를 호출해서 기존 댓글 엔터티에 수정 요청 정보를 추가함.
  • 3번
    • 수정된 댓글 엔터티(updated)를 DTO로 변환해서 반환.
    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();
        }
    }
  • URL에 있는 id와 JSON 데이터의 id가 다른 경우에 예외 발생.
    • 즉, this 객체의 id와 dto의 id가 다른 경우.
  • 수정할 닉네임 데이터가 있다면 닉네임 수정, 수정할 내용 데이터가 있다면 내용 수정.

2-4. 댓글 삭제.

  • DELETE : /comments/id


2-4-1. 컨트롤러.

    @DeleteMapping("/api/comments/{id}")
    public ResponseEntity<CommentDto> delete(@PathVariable Long id) {
        CommentDto deleteDto = commentService.delete(id);
        // 1번.
        return ResponseEntity.status(HttpStatus.OK).body(deleteDto);
    }
  • @DeleteMapping("/api/comments/{id}")로 삭제 요청을 받음.
  • @PathVariable Long id
    • URL에 {id}변수를 받아오기 위해서.
  • 1번
    • 상태 코드(200), 본문(Body)에 삭제된 댓글 데이터(deleteDto)를 실어 보냄.

2-4-2. 서비스.

    @Transactional
    public CommentDto delete(Long id) {
    	// 1번
        Comment target = commentRepository.findById(id)
                            .orElseThrow(() -> new IllegalArgumentException("'댓글 삭제 실패. 대상이 없음."));
        commentRepository.delete(target);
        // 2번
        return CommentDto.createCommentDto(target);
    }
  • 데이터 삭제 요청, 즉 DB 데이터를 변경하는 기능이므로 @Transactional처리.
  • 1번
    • 매개변수로 받아온 id를 통해 데이터를 조회하고 값이 있으면 변수에 저장,
      값이 없다면 rElseThrow()메서드를 통해 예외 발생시킴.
  • 2번
    • target엔터티를 DTO로 변환해서 반환.

3. @JsonProperty

  • JSON 데이터키(key) 이름과 이를 받아 저장하는 DTO에 선언된 필드의 변수명이 다를 경우 DTO 필드 위에 @JsonProperty("키 이름")을 작성해줘야 됨.
    • 이렇게 해주면 해당 변수가 자동으로 매핑됨.
@AllArgsConstructor
@NoArgsConstructor
@Getter
@ToString
public class CommentDto {
    private Long id;
    
    @JsonProperty("article_id")
    private Long articleId;
    
    private String nickname;
    
    private String body;

		// 코드 생략.
}


"article_id"로 하니 정상적으로 통신이 됨.

  • 데이터 생성 요청 시 JSON 데이터의 articleId를 article_id로 썼음.

4. 스트림(stream)

  • 스트림은 Java의 컬렉션(Collection), 즉 리스트와 해시맵 등의 데이터 묶음을 요소별로 순차적으로 제어하는 데 좋음.
    • 스트림의 주요 특징.
      • 원본 데이터를 읽기만 하고 변경하지 않음.
      • 정렬된 결과를 컬렉션이나 배열에 담아 반환할 수 있음.
      • 내부 반복문으로, 반복문이 코드상에 노출되지 않음.

profile
Every cloud has a silver lining.

0개의 댓글