JPA와 REST API로 댓글 기능 만들기

✒️ 2025-06-02 16:31 내용 수정

스프링부트3 자바 백엔드 개발입문 내용 참고 및 정리



댓글과 게시글의 관계


1. 댓글 엔티티와 리포지터리

  1. com.example.package_name.entity 패키지에 Comment 클래스를 생성한다.

jpa_comment 1.png

  1. Comment 클래스에 Entity 설정과 Field 설정을 진행한다.
    • @Entity Annotation을 추가하고, Lombok의 @Data Annotation으로 Getter, Setter, ToString 추가를, @AllargsConstructor@NoArgsConstructor도 추가로 생성자들도 추가해준다.
    • id@Id로 기본키 지정을 해주며, 자동으로 번호가 증가하도록 @GeneratedValue(strategy = GenerationType.IDENTITY)를 설정한다.
    • 댓글 입장에서 게시글은 다대일 관계이므로, Article과의 관계 설정은 @ManyToOne Annotation으로 명시하며, @JoinColumn으로 외래키를 article_id로 설정한다.
    • 나머지 필드에는 @Column Annotation으로 지정한다.
package com.example.demo.entity;  
  
import jakarta.persistence.*;  
import lombok.Data;  
import lombok.NoArgsConstructor;  
  
@Entity // Entity임을 명시하는 Annotation
@Data // Lombok으로, Getter, Setter, ToString 포함  
@NoArgsConstructor  
@AllArgsConstructor
public class Comment {  
    @Id  
    @GeneratedValue(strategy = GenerationType.IDENTITY) // 자동으로 1부터 증가하도록 설정  
    private Long id; // 기본키  
  
    @ManyToOne // 댓글 입장에서 게시글은 다대일 관계  
    @JoinColumn(name = "article_id") // 외래키 생성  
    private Article article; // 부모 게시글  
  
    @Column  
    private String nickname; // 댓글 작성자  
  
    @Column  
    private String body; // 댓글 본문  
  
}
  1. 댓글 Entity가 DB에 잘 생성될지 확인하기 위해 data.sql에 더미 데이터를 추가한다.
-- 게시글 더미데이터  
INSERT INTO article(title, content) VALUES ('1번 데이터', 'Test number 1');  
INSERT INTO article(title, content) VALUES ('2번 데이터', '2nd Test');  
INSERT INTO article(title, content) VALUES ('3번 데이터', 'Test 3');  
-- 댓글을 달 게시글 추가  
INSERT INTO article(title, content) VALUES ('오늘 날씨', '바람 엄청 불어요');  
INSERT INTO article(title, content) VALUES ('저녁 메뉴 추천좀', '뭐 먹을까');  
INSERT INTO article(title, content) VALUES ('요즘 할 게임 없나', '어려운 게임 말고');  
-- 댓글 더미데이터  
INSERT INTO comment(article_id, nickname, body) VALUES (4, 'Jack', '방금 우산 날아감');  
INSERT INTO comment(article_id, nickname, body) VALUES (4, 'Lee', '강풍 주의보던데');  
INSERT INTO comment(article_id, nickname, body) VALUES (4, 'Kim', '태풍 오려나');  
INSERT INTO comment(article_id, nickname, body) VALUES (5, 'Jack', '피자 어때');  
INSERT INTO comment(article_id, nickname, body) VALUES (5, 'Park', '돼지고기 김치찌개');  
INSERT INTO comment(article_id, nickname, body) VALUES (6, 'John', '엘든링 하쉴');  
INSERT INTO comment(article_id, nickname, body) VALUES (6, 'Min', '어려운거 싫대잖아');  
INSERT INTO comment(article_id, nickname, body) VALUES (6, 'Park', '팰월드 업뎃 했는데 어때');  
INSERT INTO comment(article_id, nickname, body) VALUES (6, 'Kale', 'FPS는 안해봤어?');
  1. PackageNameApplication을 실행한 다음 브라우저에 http://localhost:port/h2-console을 입력해 DB에 데이터가 잘 들어갔는지 확인한다.

jpa_comment 3.png

jpa_comment 2.png

  1. 이번엔 Comment Entity를 관리할 CommentRepositoryrepository 패키지에 생성한다.

jpa_comment 4.png

package com.example.demo.repository;  
  
import com.example.demo.entity.Comment;  
import org.springframework.data.jpa.repository.JpaRepository;  
import org.springframework.stereotype.Repository;  
  
@Repository  
public interface CommentRepository extends JpaRepository<Comment, Long> {  
}
  1. 댓글 조회 시 특정 게시글의 모든 댓글 조회 기능과 특정 닉네임의 모든 댓글 조회를 위해 RepositoryNative Query Method를 작성한다.
    • 직접 작성한 SQL query를 메소드로 실행할 수 있는 메소드다.
    • JPQL(Java Persistence Query Language) 라는 객체 지향 query 언어로 query 처리를 지원한다.
      • nativeQuery = true 사용 시 기존 SQL문을 사용할 수 있다.
    • @Query Annotation을 사용하거나 orm.xml 파일로 사용할 수 있다.
    • 교재 실습에서 특정 게시글의 댓글 조회는 @Query Annotation으로, 특정 닉네임의 모든 댓글 조회는 orm.xml로 사용했으나, 실제 수행 결과 orm.xml에는 문제가 발생하여 둘 다 @Query를 사용했다.
package com.example.demo.repository;  
  
import com.example.demo.entity.Comment;  
import org.springframework.data.jpa.repository.JpaRepository;  
import org.springframework.data.jpa.repository.Query;  
import org.springframework.stereotype.Repository;  
  
import java.util.List;  
  
@Repository  
public interface CommentRepository extends JpaRepository<Comment, Long> {  
    // 특정 게시글의 모든 댓글 조회  
    @Query(value = "SELECT * FROM comment WHERE article_id = :article_id", nativeQuery = true)  
    public List<Comment> findByArticleId(Long article_id);  
  
    // 특정 닉네임의 모든 댓글 조회  
    @Query(value = "SELECT * FROM comment WHERE nickname = :nickname", nativeQuery = true)  
    public List<Comment> findByNickname(String nickname);  
}
  1. 이제 기능 Test를 위한 Test 코드를 작성한다. 먼저 CommentRepository에서 마우스 우클릭을 눌러 Generate -> Test를 선택하고 모든 메소드를 선택해 Test용 클래스를 생성한다.
    • 이번엔 테스트 클래스를 JPA와 연동해야 하기 때문에 @SpringBootTest Annotation 대신 @DataJpaTest Annotation을 사용한다.

jpa_comment 5.png

package com.example.demo.repository;  
  
import com.example.demo.entity.Article;  
import com.example.demo.entity.Comment;  
import org.junit.jupiter.api.DisplayName;  
import org.junit.jupiter.api.Test;  
import org.springframework.beans.factory.annotation.Autowired;  
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;  
import org.springframework.boot.test.context.SpringBootTest;  
  
import java.util.Arrays;  
import java.util.List;  
  
import static org.junit.jupiter.api.Assertions.*;  
  
@DataJpaTest  
class CommentRepositoryTest {  
  
    @Autowired  
    CommentRepository commentRepository;  
  
    @Test  
    @DisplayName("특정 게시글의 모든 댓글 조회")  
    void findByArticleId() {  
        // Case 1 : 6번 게시글의 모든 댓글 조회  
        {  
            // 1. 입력 데이터 준비  
            Long articleId = 6L;  
            // 2. 실제 데이터  
            List<Comment> comments = commentRepository.findByArticleId(articleId);  
            // 3. 예상 데이터  
            Article article = new Article(6L, "요즘 할 게임 없나", "어려운 게임 말고");  
            Comment first = new Comment(6L, article, "John", "엘든링 하쉴");  
            Comment second = new Comment(7L, article, "Min", "어려운거 싫대잖아");  
            Comment third = new Comment(8L, article, "Park", "팰월드 업뎃 했는데 어때");  
            Comment fourth = new Comment(9L, article, "Kale", "FPS는 안해봤어?");  
            List<Comment> expected = Arrays.asList(first, second, third, fourth);  
            // 4. 두 데이터 비교 후 검증  
            assertEquals(expected.toString(), comments.toString(), "6번 글의 모든 댓글 출력");  
        }  
  
        // Case 2 : 1번 게시글의 모든 댓글 조회  
        {  
            // 1. 입력 데이터 준비  
            Long articleId = 1L;  
            // 2. 실제 데이터  
            List<Comment> comments = commentRepository.findByArticleId(articleId);  
            // 3. 예상 데이터  
            Article article = new Article(6L, "1번 데이터", "Test number 1");  
            List<Comment> expected = Arrays.asList();  
            // 4. 두 데이터 비교 후 검증  
            assertEquals(expected.toString(), comments.toString(), "1번 글은 댓글이 없음");  
        }  
    }  
  
    @Test  
    @DisplayName("특정 닉네임의 모든 댓글 조회")  
    void findByNickname() {  
        // Case 1 : "Park"의 모든 댓글 조회  
        {  
            // 1. 입력 데이터 준비  
            String nickname = "Park";  
            // 2. 실제 데이터  
            List<Comment> comments = commentRepository.findByNickname(nickname);  
            // 3. 예상 데이터  
            Comment first = new Comment(5L,  
                    new Article(5L, "저녁 메뉴 추천좀", "뭐 먹을까"),  
                    nickname, "돼지고기 김치찌개");  
            Comment second = new Comment(8L,  
                    new Article(6L, "요즘 할 게임 없나", "어려운 게임 말고"),  
                    nickname, "팰월드 업뎃 했는데 어때");  
  
            List<Comment> expected = Arrays.asList(first, second);  
            // 4. 두 데이터 비교 후 검증  
            assertEquals(expected.toString(), comments.toString(), "Park의 모든 댓글 출력");  
        }  
    }  
}

jpa_comment 6.png


2. 댓글 REST API

클래스 및 인터페이스 설명
CommentApiController 댓글 관련 REST Controller
CommentService Service(비즈니스 로직 담당)
CommentRepository Service에서 DB와 상호 작용할 댓글 Repository
ArticleRepository Service에서 DB와 상호 작용할 게시글 Repository
CommentDto 클라이언트로 보낼 데이터를 담는 DTO
Comment DB에서 테이블과 연결되는 Entity
  1. 먼저 REST Controller인 CommentApiControllercom.example.package_name.api 패키지에 생성한다.
    • @RestController Annotation으로 REST Controller 선언을 해준다.
    • Service와 협업 해야 하므로 CommentService 객체를 자동 주입해준다.
      • 아직 CommentService 클래스를 생성하지 않아 오류가 뜨지만 이후에 바로 생성할 예정이므로 큰 문제가 되지 않는다.
package com.example.demo.api;  
  
import com.example.demo.service.CommentService;  
import org.springframework.beans.factory.annotation.Autowired;  
import org.springframework.web.bind.annotation.RequestMapping;  
import org.springframework.web.bind.annotation.RestController;  
  
@RestController  // REST Controller
@RequestMapping("/api")  // 요청 path 공통 설정
public class CommentApiController {  
    @Autowired  
    private CommentService commentService;  
      
}
  1. com.example.package_name.service 패키지에 CommentService 클래스를 생성한다.
    • @Service Annotation으로 Service 역할을 명시해준다.
    • CommentRepository interface와 ArticleRepository interface를 자동 주입해준다.
package com.example.demo.service;  
  
import com.example.demo.repository.ArticleRepository;  
import com.example.demo.repository.CommentRepository;  
import org.springframework.beans.factory.annotation.Autowired;  
import org.springframework.stereotype.Service;  
  
@Service  // Service 명시
public class CommentService {  
    @Autowired  
    private CommentRepository commentRepository;  
    @Autowired  
    private ArticleRepository articleRepository;  
}
  1. com.example.package_name.DTO 패키지에 CommentDto 클래스를 생성한다.
package com.example.demo.DTO;  
  
import lombok.AllArgsConstructor;  
import lombok.Data;  
import lombok.NoArgsConstructor;  
  
@Data  
@AllArgsConstructor  
@NoArgsConstructor  
public class CommentDto {  
    private Long id; // 댓글 id    
    private Long articleId; // 게시글 id    
    private String nickname; // 작성자  
    private String body; // 내용  
}

2-1. 댓글 REST API - GET 요청

  1. CommentApiController에서 조회 요청인 GET 요청을 위한 메소드를 추가한다.
    • 특정 게시글의 댓글 목록을 조회하기 위해 @GetMapping()에서 articleId를 path variable로 받고, @PathVariable Long articleId로 매개변수화 하여 사용한다.
    • Response에 데이터와 응답 코드를 함께 보내기 위해 ResponseEntity 클래스를 사용한다.
package com.example.demo.api;  
  
import com.example.demo.DTO.CommentDto;  
import com.example.demo.service.CommentService;  
import org.springframework.beans.factory.annotation.Autowired;  
import org.springframework.http.HttpStatus;  
import org.springframework.http.ResponseEntity;  
import org.springframework.web.bind.annotation.GetMapping;  
import org.springframework.web.bind.annotation.PathVariable;  
import org.springframework.web.bind.annotation.RequestMapping;  
import org.springframework.web.bind.annotation.RestController;  
  
import java.util.List;  
  
@RestController // REST Controller  
@RequestMapping("/api")  // 요청 path 공통 설정  
public class CommentApiController {  
    @Autowired  
    private CommentService commentService;  

	// GET
    @GetMapping("articles/{articleId}/comments")  // 특정 게시글의 댓글 조회
    public ResponseEntity<List<CommentDto>> comments(@PathVariable Long articleId) {  
        List<CommentDto> dtos = commentService.comments(articleId);  
        return ResponseEntity.status(HttpStatus.OK).body(dtos);  
    }  
}
  1. CommentService에서 특정 게시글 id를 전달받아 DB에서 해당 id를 articleId로 가지는 댓글을 조회하는 메소드를 작성한다.
package com.example.demo.service;  
  
import com.example.demo.DTO.CommentDto;  
import com.example.demo.entity.Comment;  
import com.example.demo.repository.ArticleRepository;  
import com.example.demo.repository.CommentRepository;  
import org.springframework.beans.factory.annotation.Autowired;  
import org.springframework.stereotype.Service;  
  
import java.util.ArrayList;  
import java.util.List;  
  
@Service // Service 명시  
public class CommentService {  
    @Autowired  
    private CommentRepository commentRepository;  
    @Autowired  
    private ArticleRepository articleRepository;  
  
    public List<CommentDto> comments(Long articleId) { // 특정 게시글의 댓글 조회  
        // DB에서 댓글 가져오기  
        List<Comment> comments = commentRepository.findByArticleId(articleId);  
        // 댓글 Entity를 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;  
    }  
}
package com.example.demo.service;  
  
import com.example.demo.DTO.CommentDto;  
import com.example.demo.entity.Comment;  
import com.example.demo.repository.ArticleRepository;  
import com.example.demo.repository.CommentRepository;  
import org.springframework.beans.factory.annotation.Autowired;  
import org.springframework.stereotype.Service;  
  
import java.util.ArrayList;  
import java.util.List;  
import java.util.stream.Collectors;  
  
@Service // Service 명시  
public class CommentService {  
    @Autowired  
    private CommentRepository commentRepository;  
    @Autowired  
    private ArticleRepository articleRepository;  
  
    public List<CommentDto> comments(Long articleId) { // 특정 게시글의 댓글 조회  
        // DB에서 댓글 가져오기  
        List<Comment> comments = commentRepository.findByArticleId(articleId);  
        // 댓글 Entity를 DTO로 변환  
 
        return commentRepository.findByArticleId(articleId)  // 데이터 조회
                .stream()  // stream 변환
                .map(c->CommentDto.createCommentDto(c))  // 각 요소를 DTO로 변환
                .collect(Collectors.toList());  // stream -> List
    }  
}
  1. Comment EntityCommentDto로 변환시키기 위해 CommentDtoEntity -> DTO 메소드를 추가한다.
package com.example.demo.DTO;  
  
import com.example.demo.entity.Comment;  
import lombok.AllArgsConstructor;  
import lombok.Data;  
import lombok.NoArgsConstructor;  
  
@Data  
@AllArgsConstructor  
@NoArgsConstructor  
public class CommentDto {  
    private Long id; // 댓글 id    
    private Long articleId; // 게시글 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()  
        );  
    }  
}
  1. 확인을 위해 Talend API Tester에서 http://localhost:port/api/articles/번호/comments를 입력해 응답을 확인한다.
    • 게시글 번호는 더미 데이터 생성 시 댓글이 있던 게시글의 번호를 입력한다.

jpa_comment_api 1.png


2-2. 댓글 REST API - POST 요청

  1. 이번엔 댓글 생성 요청을 받기 위해 @PostMapping()을 추가한다.
    • 댓글이 추가될 게시글의 번호를 받아야 하므로 articleId를 path variable로 받고, @PathVariable Long articleId로 매개변수화 하여 사용한다.
    • Response에 데이터와 응답 코드를 함께 보내기 위해 ResponseEntity 클래스를 사용한다.
    • 요청의 body에 포함된 CommentDto를 사용하기 위해 @RequestBody를 추가한다.
package com.example.demo.api;  
  
import com.example.demo.DTO.CommentDto;  
import com.example.demo.service.CommentService;  
import org.springframework.beans.factory.annotation.Autowired;  
import org.springframework.http.HttpStatus;  
import org.springframework.http.ResponseEntity;  
import org.springframework.web.bind.annotation.*;  
  
import java.util.List;  
  
@RestController // REST Controller  
@RequestMapping("/api")  // 요청 path 공통 설정  
public class CommentApiController {  
    @Autowired  
    private CommentService commentService;  

	// GET
    @GetMapping("articles/{articleId}/comments") // 특정 게시글의 댓글 조회  
    public ResponseEntity<List<CommentDto>> comments(@PathVariable Long articleId) {  
        List<CommentDto> dtos = commentService.comments(articleId);  
        return ResponseEntity.status(HttpStatus.OK).body(dtos);  
    }  

	// POST
    @PostMapping("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);  
    }  
}
  1. CommentService에 댓글 추가 메소드를 추가한다.
    • DB에 내용을 저장하는 메소드이므로 저장 실패 시 진행 내용을 롤백해야 하므로 @Transactional Annotation을 메소드에 추가해 Transaction 처리를 해준다.
    • 댓글을 추가하기 앞서 매개변수로 받은 articleId를 가지는 게시글이 있는지 먼저 조회하고, 게시글이 없다면 .orElseThrow() 메소드를 사용해 예외 처리를 진행한다.
    • DTOEntity의 특성상 요청으로 받은 데이터는 DTO, DB에 저장할 데이터는 Entity 이다.
    • 따라서 요청 DTOEntity로 변환하여 DB에 저장하고, 저장한 결과를 클라이언트에 응답 보낼 때는 다시 Entity -> DTO를 수행한 후 보내야 한다.
package com.example.demo.service;  
  
import com.example.demo.DTO.CommentDto;  
import com.example.demo.entity.Article;  
import com.example.demo.entity.Comment;  
import com.example.demo.repository.ArticleRepository;  
import com.example.demo.repository.CommentRepository;  
import jakarta.transaction.Transactional;  
import org.springframework.beans.factory.annotation.Autowired;  
import org.springframework.stereotype.Service;  
  
import java.util.ArrayList;  
import java.util.List;  
import java.util.stream.Collectors;  
  
@Service // Service 명시  
public class CommentService {  
    @Autowired  
    private CommentRepository commentRepository;  
    @Autowired  
    private ArticleRepository articleRepository;  
  
    public List<CommentDto> comments(Long articleId) { // 특정 게시글의 댓글 조회  
        // DB에서 댓글 가져오기  
        List<Comment> comments = commentRepository.findByArticleId(articleId);  
        // 댓글 Entity를 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);  
        }  
        // Stream 사용 시
//        commentRepository.findByArticleId(articleId) // 데이터 조회  
//                .stream() // stream 변환  
//                .map(c->CommentDto.createCommentDto(c)) // 각 요소를 DTO로 변환  
//                .collect(Collectors.toList()); // stream -> List  
        return dtos;  
    }  
  
    @Transactional // Transaction 설정, 추가 실패시 롤백  
    public CommentDto create(Long articleId, CommentDto dto) { // 특정 게시글에 댓글 추가  
        // 게시글 조회 및 예외 처리  
        Article article = articleRepository.findById(articleId)  
                .orElseThrow(()-> 
                new IllegalArgumentException("댓글 생성 실패! : 대상 게시글 없음"));  
        // 댓글 엔티티 생성 (DTO -> Entity)        
        Comment comment = Comment.createComment(dto, article);  // Comment에 정적 메소드 추가
        // DB에 엔티티 저장  
        Comment created = commentRepository.save(comment);  
        // 결과를 Entity -> DTO로 반환  
        return CommentDto.createCommentDto(created);  
    }  
}
  1. CommentDto -> Comment로 변환할 정적 메소드 createComment()Comment Entity에 추가한다.
package com.example.demo.entity;  
  
import com.example.demo.DTO.CommentDto;  
import jakarta.persistence.*;  
import lombok.AllArgsConstructor;  
import lombok.Data;  
import lombok.NoArgsConstructor;  
  
@Entity // Entity임을 명시하는 Annotation
@Data // Lombok으로, Getter, Setter, AllargsConstructor, ToString 포함  
@NoArgsConstructor  
@AllArgsConstructor  
public class Comment {  
    @Id  
    @GeneratedValue(strategy = GenerationType.IDENTITY) // 자동으로 1부터 증가하도록 설정  
    private Long id; // 기본키  
  
    @ManyToOne // 댓글 입장에서 게시글은 다대일 관계  
    @JoinColumn(name = "article_id") // 외래키 생성  
    private Article article; // 부모 게시글  
  
    @Column  
    private String nickname; // 댓글 작성자  
  
    @Column  
    private String body; // 댓글 본문  

	// CommentDto -> Comment Entity
    public static Comment createComment(CommentDto dto, Article article) {  
        if (dto.getId() != null) {  // 댓글 id는 자동생성되므로 없어야 함
            throw new IllegalArgumentException("댓글 생성 실패! : 댓글 id가 존재함");  
        }  
        if (dto.getArticleId() != article.getId()) {  // 게시글 id가 동일해야 함
            throw new IllegalArgumentException("댓글 생성 실패! : 게시글 id가 다름");  
        }  
  
        return new Comment(  
                dto.getId(),  
                article,  
                dto.getNickname(),  
                dto.getBody()  
        );  
    }  
}
  1. API 확인을 위해 Talend API Test에서 http://localhost:port/api/articles/번호/comments로 POST 요청을 전송한다.

jpa_comment_api 2.png

jpa_comment_api 3.png

jpa_comment_api 4.png

package com.example.demo.DTO;  
  
import com.example.demo.entity.Comment;  
import com.fasterxml.jackson.annotation.JsonProperty;  
import lombok.AllArgsConstructor;  
import lombok.Data;  
import lombok.NoArgsConstructor;  
  
@Data  
@AllArgsConstructor  
@NoArgsConstructor  
public class CommentDto {  
    private Long id; // 댓글 id   
     
    // @JsonProperty("article_id")  // JSON 데이터에서 key를 article_id로 사용할때만
    private Long articleId; // 게시글 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()  
        );  
    }  
}

2-3. 댓글 REST API - PATCH 요청

  1. 이번엔 댓글 수정 요청을 받기 위해 @PatchMapping()을 추가한다.
    • GET, POST 요청때와 달리 PATCH 요청에선 수정 대상 댓글의 id를 받아야 하므로 주의한다.
package com.example.demo.api;  
  
import com.example.demo.DTO.CommentDto;  
import com.example.demo.service.CommentService;  
import org.springframework.beans.factory.annotation.Autowired;  
import org.springframework.http.HttpStatus;  
import org.springframework.http.ResponseEntity;  
import org.springframework.web.bind.annotation.*;  
  
import java.util.List;  
  
@RestController // REST Controller  
@RequestMapping("/api")  // 요청 path 공통 설정  
public class CommentApiController {  
    @Autowired  
    private CommentService commentService;  
  
    // GET  
    @GetMapping("articles/{articleId}/comments") // 특정 게시글의 댓글 조회  
    public ResponseEntity<List<CommentDto>> comments(@PathVariable Long articleId) {  
        List<CommentDto> dtos = commentService.comments(articleId);  
        return ResponseEntity.status(HttpStatus.OK).body(dtos);  
    }  
  
    // POST  
    @PostMapping("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);  
    }  
  
    // PATCH  
    @PatchMapping("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);  
    }  
}
  1. CommentService에 댓글 수정 메소드를 추가한다.
    • POST 요청과 마찬가지로 DB에 수정을 행하는 동작이므로 Transaction 처리를 위해 @Transactional Annotation을 추가 해 동작 실패시 롤백을 수행하도록 설정한다.
    • 해당 id를 가지는 댓글을 먼저 조회하고, 해당 댓글이 없을 때 예외 처리를 .orElseThrow()로 처리한다.
package com.example.demo.service;  
  
import com.example.demo.DTO.CommentDto;  
import com.example.demo.entity.Article;  
import com.example.demo.entity.Comment;  
import com.example.demo.repository.ArticleRepository;  
import com.example.demo.repository.CommentRepository;  
import jakarta.transaction.Transactional;  
import org.springframework.beans.factory.annotation.Autowired;  
import org.springframework.stereotype.Service;  
  
import java.util.ArrayList;  
import java.util.List;  
import java.util.stream.Collectors;  
  
@Service // Service 명시  
public class CommentService {  
    @Autowired  
    private CommentRepository commentRepository;  
    @Autowired  
    private ArticleRepository articleRepository;  
  
    public List<CommentDto> comments(Long articleId) { // 특정 게시글의 댓글 조회  
        // DB에서 댓글 가져오기  
        List<Comment> comments = commentRepository.findByArticleId(articleId);  
        // 댓글 Entity를 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);  
        }  
//        commentRepository.findByArticleId(articleId) // 데이터 조회  
//                .stream() // stream 변환  
//                .map(c->CommentDto.createCommentDto(c)) // 각 요소를 DTO로 변환  
//                .collect(Collectors.toList()); // stream -> List  
        return dtos;  
    }  
  
    @Transactional // Transaction 설정, 추가 실패시 롤백  
    public CommentDto create(Long articleId, CommentDto dto) { // 특정 게시글에 댓글 추가  
        // 게시글 조회 및 예외 처리  
        Article article = articleRepository.findById(articleId)  
                .orElseThrow(()->  
                        new IllegalArgumentException("댓글 생성 실패! : 대상 게시글 없음"));  
        
        // 댓글 엔티티 생성 (DTO -> Entity)        
        Comment comment = Comment.createComment(dto, article);  
        // DB에 엔티티 저장  
        Comment created = commentRepository.save(comment);  
        // 결과를 Entity -> DTO로 반환  
        return CommentDto.createCommentDto(created);  
    }  
  
    @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);  
        // Entity -> DTO로 DTO를 반환  
        return CommentDto.createCommentDto(updated);  
    }  
}
  1. Comment EntityCommentDto로부터 받은 내용으로 수정하는 메소드를 추가한다.
    • 수정 요청 id와 대상 id가 다르면 예외 처리를 진행한다.
    • 수정 요청 데이터에 내용이 존재하면 요청 정보로 Entity의 내용을 수정한다.
package com.example.demo.entity;  
  
import com.example.demo.DTO.CommentDto;  
import jakarta.persistence.*;  
import lombok.AllArgsConstructor;  
import lombok.Data;  
import lombok.NoArgsConstructor;  
  
@Entity // Entity임을 명시하는 Annotation
@Data // Lombok으로, Getter, Setter, AllargsConstructor, ToString 포함  
@NoArgsConstructor  
@AllArgsConstructor  
public class Comment {  
    @Id  
    @GeneratedValue(strategy = GenerationType.IDENTITY) // 자동으로 1부터 증가하도록 설정  
    private Long id; // 기본키  
  
    @ManyToOne // 댓글 입장에서 게시글은 다대일 관계  
    @JoinColumn(name = "article_id") // 외래키 생성  
    private Article article; // 부모 게시글  
  
    @Column  
    private String nickname; // 댓글 작성자  
  
    @Column  
    private String body; // 댓글 본문  
  
    // CommentDto -> Comment Entity  
    public static Comment createComment(CommentDto dto, Article article) {  
        if (dto.getId() != null) { // 댓글 id는 자동생성되므로 없어야 함  
            throw new IllegalArgumentException("댓글 생성 실패! : 댓글 id가 존재함");  
        }  
        if (dto.getArticleId() != article.getId()) { // 게시글 id가 동일해야 함  
            throw new IllegalArgumentException("댓글 생성 실패! : 게시글 id가 다름");  
        }  
  
        return new Comment(  
                dto.getId(),  
                article,  
                dto.getNickname(),  
                dto.getBody()  
        );  
    }  
    
    // Entity 수정  
    public void patch(CommentDto dto) {  
        // 예외 처리  
        if (this.id != dto.getId()) { // id가 다른 경우  
            throw new IllegalArgumentException("댓글 수정 실패! : 잘못된 id");  
        }  
        // 수정 동작  
        if (dto.getNickname() != null) { // 수정할 닉네임 데이터가 있으면 사용  
            this.nickname = dto.getNickname();  
        }  
        if (dto.getBody() != null) { // 수정할 내용이 있으면 사용  
            this.body = dto.getBody();  
        }  
    }  
}
  1. API 확인을 위해 Talend API Test에서 http://localhost:port/api/comments/번호로 PATCH 요청을 전송한다.
    • 먼저 예외 처리를 확인하기 위해 현재 DB에 없는 id로 PATCH 요청을 보냈을 땐 대상 댓글이 없다는 500응답이 발생한다.

jpa_comment_api 5.png

jpa_comment_api 6.png

jpa_comment_api 7.png

jpa_comment_api 8.png


2-4. 댓글 REST API - DELETE 요청

  1. 이번엔 댓글 삭제 요청을 받기 위해 @DeleteMapping()을 추가한다.
    • 삭제 대상 댓글의 id를 받아야 하므로 주의한다.
package com.example.demo.api;  
  
import com.example.demo.DTO.CommentDto;  
import com.example.demo.service.CommentService;  
import org.springframework.beans.factory.annotation.Autowired;  
import org.springframework.http.HttpStatus;  
import org.springframework.http.ResponseEntity;  
import org.springframework.web.bind.annotation.*;  
  
import java.util.List;  
  
@RestController // REST Controller  
@RequestMapping("/api")  // 요청 path 공통 설정  
public class CommentApiController {  
    @Autowired  
    private CommentService commentService;  
  
    // GET  
    @GetMapping("articles/{articleId}/comments") // 특정 게시글의 댓글 조회  
    public ResponseEntity<List<CommentDto>> comments(@PathVariable Long articleId) {  
        List<CommentDto> dtos = commentService.comments(articleId);  
        return ResponseEntity.status(HttpStatus.OK).body(dtos);  
    }  
  
    // POST  
    @PostMapping("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);  
    }  
  
    // PATCH  
    @PatchMapping("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);  
    }  
    // DELETE  
    @DeleteMapping("comments/{id}") // 특정 댓글 삭제  
    public ResponseEntity<CommentDto> delete(@PathVariable Long id) {  
        CommentDto deletedDto = commentService.delete(id);  
        return ResponseEntity.status(HttpStatus.OK).body(deletedDto);  
    }  
}
  1. CommentService에 댓글 수정 메소드를 추가한다.
    • POST 요청과 마찬가지로 DB에 수정을 행하는 동작이므로 Transaction 처리를 위해 @Transactional Annotation을 추가 해 동작 실패시 롤백을 수행하도록 설정한다.
    • 해당 id를 가지는 댓글을 먼저 조회하고, 해당 댓글이 없을 때 예외 처리를 .orElseThrow()로 처리한다.
package com.example.demo.service;  
  
import com.example.demo.DTO.CommentDto;  
import com.example.demo.entity.Article;  
import com.example.demo.entity.Comment;  
import com.example.demo.repository.ArticleRepository;  
import com.example.demo.repository.CommentRepository;  
import jakarta.transaction.Transactional;  
import org.springframework.beans.factory.annotation.Autowired;  
import org.springframework.stereotype.Service;  
  
import java.util.ArrayList;  
import java.util.List;  
import java.util.stream.Collectors;  
  
@Service // Service 명시  
public class CommentService {  
    @Autowired  
    private CommentRepository commentRepository;  
    @Autowired  
    private ArticleRepository articleRepository;  
  
    public List<CommentDto> comments(Long articleId) { // 특정 게시글의 댓글 조회  
        // DB에서 댓글 가져오기  
        List<Comment> comments = commentRepository.findByArticleId(articleId);  
        // 댓글 Entity를 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);  
        }  
//        commentRepository.findByArticleId(articleId) // 데이터 조회  
//                .stream() // stream 변환  
//                .map(c->CommentDto.createCommentDto(c)) // 각 요소를 DTO로 변환  
//                .collect(Collectors.toList()); // stream -> List  
        return dtos;  
    }  
  
    @Transactional // Transaction 설정, 추가 실패시 롤백  
    public CommentDto create(Long articleId, CommentDto dto) { // 특정 게시글에 댓글 추가  
        // 게시글 조회 및 예외 처리  
        Article article = articleRepository.findById(articleId)  
                .orElseThrow(()->  
                        new IllegalArgumentException("댓글 생성 실패! : 대상 게시글 없음"));  
        
        // 댓글 엔티티 생성 (DTO -> Entity)        
        Comment comment = Comment.createComment(dto, article);  
        // DB에 엔티티 저장  
        Comment created = commentRepository.save(comment);  
        // 결과를 Entity -> DTO로 반환  
        return CommentDto.createCommentDto(created);  
    }  
  
    @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);  
        // Entity -> DTO로 DTO를 반환  
        return CommentDto.createCommentDto(updated);  
    } 
     
    @Transactional  
    public CommentDto delete(Long id) { // 특정 댓글 삭제  
        // 댓글 조회 및 에러 처리  
        Comment target = commentRepository.findById(id)  
                .orElseThrow(()->  
                        new IllegalArgumentException("댓글 삭제 실패! : 대상 댓글 없음"));  
        
        // 댓글 제거  
        commentRepository.delete(target); 
        // Entity -> DTO로 DTO를 반환  
        return CommentDto.createCommentDto(target);  
    }  
}
  1. API 확인을 위해 Talend API Test에서 http://localhost:port/api/comments/번호로 DELETE 요청을 전송한다.

jpa_comment_api 9.png

jpa_comment_api 10.png

jpa_comment_api 11.png