JPA와 REST API로 댓글 기능 만들기
✒️ 2025-06-02 16:31 내용 수정
스프링부트3 자바 백엔드 개발입문 내용 참고 및 정리
- 게시판 만들기에서는 MyBatis를 사용하여 게시판 기능을 만들었다.
- 이번에는 JPA(Java Persistence API)와 Spring boot로 REST API 구현(JPA)를 통해 간단하게 만든 게시판에 댓글 기능을 추가하였다.
댓글과 게시글의 관계
- 댓글과 게시글의 관계는 게시글 1개에 여러 개의 댓글이 달릴 수 있으므로 일대다(1:n), 다대일(n:1) 관계이다.
- 즉 댓글은 DB의 테이블 상에서 게시글의 id를 foreign key로 가짐으로써 두
Entity사이의 관계를 설정할 수 있다.- 제약조건 참고.
1. 댓글 엔티티와 리포지터리
com.example.package_name.entity패키지에Comment클래스를 생성한다.
Comment클래스에Entity설정과 Field 설정을 진행한다.@EntityAnnotation을 추가하고, Lombok의@DataAnnotation으로 Getter, Setter, ToString 추가를,@AllargsConstructor와@NoArgsConstructor도 추가로 생성자들도 추가해준다.id는@Id로 기본키 지정을 해주며, 자동으로 번호가 증가하도록@GeneratedValue(strategy = GenerationType.IDENTITY)를 설정한다.- 댓글 입장에서 게시글은 다대일 관계이므로,
Article과의 관계 설정은@ManyToOneAnnotation으로 명시하며,@JoinColumn으로 외래키를article_id로 설정한다. - 나머지 필드에는
@ColumnAnnotation으로 지정한다.
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; // 댓글 본문
}
- 댓글
Entity가 DB에 잘 생성될지 확인하기 위해data.sql에 더미 데이터를 추가한다.- Test와 Test 코드 1에서 더미 데이터 설정을 진행했다.
-- 게시글 더미데이터
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는 안해봤어?');
PackageNameApplication을 실행한 다음 브라우저에http://localhost:port/h2-console을 입력해 DB에 데이터가 잘 들어갔는지 확인한다.
http://localhost:port/article로 접속해도 더미 데이터 게시글이 잘 추가된 것을 확인할 수 있다.- 아직 사이트에서 댓글은 Controller에서 Model을 통해 View로 넘기지 않아 확인할 수 없다.
- 이번엔
CommentEntity를 관리할CommentRepository를repository패키지에 생성한다.Repository이므로 interface로 생성한다.@RepositoryAnnotation으로Repository임을 명시해주고, CRUD와 다양한 기능 사용을 위해JpaRepository를 상속받는다.
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> {
}
- 댓글 조회 시 특정 게시글의 모든 댓글 조회 기능과 특정 닉네임의 모든 댓글 조회를 위해
Repository에 Native Query Method를 작성한다.- 직접 작성한 SQL query를 메소드로 실행할 수 있는 메소드다.
- JPQL(Java Persistence Query Language) 라는 객체 지향 query 언어로 query 처리를 지원한다.
nativeQuery = true사용 시 기존 SQL문을 사용할 수 있다.
@QueryAnnotation을 사용하거나orm.xml파일로 사용할 수 있다.- 교재 실습에서 특정 게시글의 댓글 조회는
@QueryAnnotation으로, 특정 닉네임의 모든 댓글 조회는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);
}
- 이제 기능 Test를 위한 Test 코드를 작성한다. 먼저
CommentRepository에서 마우스 우클릭을 눌러 Generate -> Test를 선택하고 모든 메소드를 선택해 Test용 클래스를 생성한다.- 이번엔 테스트 클래스를 JPA와 연동해야 하기 때문에
@SpringBootTestAnnotation 대신@DataJpaTestAnnotation을 사용한다.
- 이번엔 테스트 클래스를 JPA와 연동해야 하기 때문에
- 테스트에 이름을 붙이기 위해
@DisplayNameAnnotation을 사용하여 메소드명을 따로 수정하지 않고 이름을 부여할 수 있다.
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의 모든 댓글 출력");
}
}
}
2. 댓글 REST API
- 댓글 CRUD를 위한 REST API를 만들기 위해 필요한 클래스를 간략하게 정리했다.
| 클래스 및 인터페이스 | 설명 |
|---|---|
CommentApiController |
댓글 관련 REST Controller |
CommentService |
Service(비즈니스 로직 담당) |
CommentRepository |
Service에서 DB와 상호 작용할 댓글 Repository |
ArticleRepository |
Service에서 DB와 상호 작용할 게시글 Repository |
CommentDto |
클라이언트로 보낼 데이터를 담는 DTO |
Comment |
DB에서 테이블과 연결되는 Entity |
- 먼저 REST Controller인
CommentApiController를com.example.package_name.api패키지에 생성한다.@RestControllerAnnotation으로 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;
}
com.example.package_name.service패키지에CommentService클래스를 생성한다.@ServiceAnnotation으로 Service 역할을 명시해준다.CommentRepositoryinterface와ArticleRepositoryinterface를 자동 주입해준다.
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;
}
com.example.package_name.DTO패키지에CommentDto클래스를 생성한다.- DTO는 DB로부터 가져온 데이터를 담는 역할이기에 Field는
Entity와 동일하게 설정한다. - JPA와 REST API로 댓글 기능 만들기#댓글 엔티티와 리포지터리의 댓글
Entity참고.
- DTO는 DB로부터 가져온 데이터를 담는 역할이기에 Field는
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 요청
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);
}
}
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;
}
}
- for문 대신 stream을 사용하여 처리할 수 있다.
- 스트림 API#유의 사항을 참고하여 상황에 따라 for문을 사용할지 stream을 사용할지 선택할 수 있다.
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
}
}
CommentEntity를CommentDto로 변환시키기 위해CommentDto에Entity->DTO메소드를 추가한다.CommentDto객체 생성 없이 메소드를 호출하기 위해 정적 메소드로 만든다.- 메서드(Methods)#1. 클래스 메소드(static method) 참고.
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()
);
}
}
- 확인을 위해 Talend API Tester에서
http://localhost:port/api/articles/번호/comments를 입력해 응답을 확인한다.- 게시글 번호는 더미 데이터 생성 시 댓글이 있던 게시글의 번호를 입력한다.
2-2. 댓글 REST API - POST 요청
- 이번엔 댓글 생성 요청을 받기 위해
@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);
}
}
CommentService에 댓글 추가 메소드를 추가한다.- DB에 내용을 저장하는 메소드이므로 저장 실패 시 진행 내용을 롤백해야 하므로
@TransactionalAnnotation을 메소드에 추가해 Transaction 처리를 해준다. - 댓글을 추가하기 앞서 매개변수로 받은
articleId를 가지는 게시글이 있는지 먼저 조회하고, 게시글이 없다면.orElseThrow()메소드를 사용해 예외 처리를 진행한다. DTO와Entity의 특성상 요청으로 받은 데이터는DTO, DB에 저장할 데이터는Entity이다.- 따라서 요청
DTO를Entity로 변환하여 DB에 저장하고, 저장한 결과를 클라이언트에 응답 보낼 때는 다시Entity->DTO를 수행한 후 보내야 한다.
- DB에 내용을 저장하는 메소드이므로 저장 실패 시 진행 내용을 롤백해야 하므로
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);
}
}
CommentDto->Comment로 변환할 정적 메소드createComment()를CommentEntity에 추가한다.
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()
);
}
}
- API 확인을 위해 Talend API Test에서
http://localhost:port/api/articles/번호/comments로 POST 요청을 전송한다.
- 만약 id가
null이 아니면 내부 서버에서 예외 처리를 진행하여 응답 코드가500으로 뜬다.
- 위 댓글은 Transaction 처리로 인해 롤백되면서 DB에도 추가되지 않은 것을 확인할 수 있다.
- 만약 JSON 데이터의 key 이름과 DTO에 선언된 Field의 변수명이 다를 경우
@JsonProperty("keyname")Annotation을 사용하여 자동으로 해당 key가 변수와 매핑되도록 설정할 수 있다.- 하지만
articleId라는 key로 데이터를 전송하고자 한다면 추가해선 안된다. - REST API fetch 시 값이 null로 넘어온 경우 - key 일치 문제 참고.
- 하지만
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 요청
- 이번엔 댓글 수정 요청을 받기 위해
@PatchMapping()을 추가한다.- GET, POST 요청때와 달리 PATCH 요청에선 수정 대상 댓글의
id를 받아야 하므로 주의한다.
- GET, POST 요청때와 달리 PATCH 요청에선 수정 대상 댓글의
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);
}
}
CommentService에 댓글 수정 메소드를 추가한다.- POST 요청과 마찬가지로 DB에 수정을 행하는 동작이므로 Transaction 처리를 위해
@TransactionalAnnotation을 추가 해 동작 실패시 롤백을 수행하도록 설정한다. - 해당
id를 가지는 댓글을 먼저 조회하고, 해당 댓글이 없을 때 예외 처리를.orElseThrow()로 처리한다.
- POST 요청과 마찬가지로 DB에 수정을 행하는 동작이므로 Transaction 처리를 위해
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);
}
}
CommentEntity에CommentDto로부터 받은 내용으로 수정하는 메소드를 추가한다.- 수정 요청
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();
}
}
}
- API 확인을 위해 Talend API Test에서
http://localhost:port/api/comments/번호로 PATCH 요청을 전송한다.- 먼저 예외 처리를 확인하기 위해 현재 DB에 없는
id로 PATCH 요청을 보냈을 땐 대상 댓글이 없다는500응답이 발생한다.
- 먼저 예외 처리를 확인하기 위해 현재 DB에 없는
- 이번엔 요청
id와 body의id가 다른 경우를 PATCH 요청 보냈을 때에 두id가 다르다는500응답이 돌아온다.
id가 서로 일치하게 수정한 뒤 요청을 보내면 정상적으로 응답이 온다.
- 다만 현재의 수정 코드는 해당
id의 댓글과 부모 게시글의id일치 여부까진 확인하지 않아articleId가 일치하지 않는 경우에도 댓글의id가 같으면 응답이200으로 온다.
2-4. 댓글 REST API - DELETE 요청
- 이번엔 댓글 삭제 요청을 받기 위해
@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);
}
}
CommentService에 댓글 수정 메소드를 추가한다.- POST 요청과 마찬가지로 DB에 수정을 행하는 동작이므로 Transaction 처리를 위해
@TransactionalAnnotation을 추가 해 동작 실패시 롤백을 수행하도록 설정한다. - 해당
id를 가지는 댓글을 먼저 조회하고, 해당 댓글이 없을 때 예외 처리를.orElseThrow()로 처리한다.
- POST 요청과 마찬가지로 DB에 수정을 행하는 동작이므로 Transaction 처리를 위해
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);
}
}
- API 확인을 위해 Talend API Test에서
http://localhost:port/api/comments/번호로 DELETE 요청을 전송한다.
- 댓글이 지워졌는지 확인하기 위해 위 댓글의 게시글인 6번 게시글의 모든 댓글을 조회해보면 해당 댓글이 없는 것을 확인할 수 있다.
- 만약 DB에 존재하지 않는
id의 댓글 삭제 요청을 보내면 해당 댓글이 존재하지 않는다는500응답이 온다.