
안녕하세요!
이번엔 커뮤니티 앱에서 작성되는 간단한 댓글에 대한 데이터를 CRUD 테스트 해보려합니다.
"Comment"를 테이블 명으로 엔티티를 기반으로하여 댓글의 기본 속성인 댓글 내용, 좋아요 수, 작성 시간을 중심으로 구현하고 나중에는 기능을 추가하거나 더 고도화 시켜서 댓글에 답글을 작성하는 기능과 댓글에 미디어 데이터를 추가해서 생성하는 방법, 한 유저가 좋아요를 중복해서 사용하지 못하게 하는 방법 등을 다뤄보겠습니다.
우선 기본적인 댓글의 기능만 추가할거기에 유저 엔티티를 비롯한 다른 테이블은 추가하지 않았습니다.
@Data
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "comment")
public class Comment {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "user_id")
private Long userId;
@Column(name = "vote_id")
private Long voteId;
@Column(name = "content")
private String content;
@Column(name = "likes")
private int likes = 0;
@Column(name = "time")
private String time;
}
다음은 제가 작성한 Comment 엔티티 클래스입니다. 위의 코드에서 userId와 voteId를 널로 초기화하였습니다. 이는 커뮤니티에서 댓글이 작성될 때 사용자 ID와 투표 ID가 비어있다는 것을 의미합니다. 사용자와 투표와의 연관 관계는 댓글이 작성될 때에 추가하면 될겁니다.
그리고 전에 상품을 주제로한 간단한 CRUD 구현 포스팅에서 볼 수 있었던 생성자와 빈생성자, 게터 함수, 세터 함수들은 모두 @Data, @Getter, @Setter, @NoArgsConstructor, @AllArgsConstructor 어노테이션으로 대체 하였습니다.
이에 관한 설명은 Lombok 관련 포스팅에서 제대로 다뤄보도록 하겠습니다.
@PrePersist //자동으로 날짜 및 시간을 설정하는 어노테이션
protected void Createtime() {
time = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy.MM.dd HH:mm:ss")); //현재시간을 저장
}
위 엔티티 클래스에서 작성 현재 시간에 관련하여 위 코드를 추가로 작성해 주었습니다.
"@PrePersist" 어노테이션 설명도 다음 포스팅에서 함께 다루도록 하겠습니다.
위 코드에서 @PrePersist 어노테이션이 적용된 createTime 메서드는 엔티티가 저장되기 전에 자동으로 호출됩니다. 메서드 내부에서는 현재 시간을 생성하여 엔티티의 time 필드에 저장할 수 있도록 설정하였습니다.
"LocalDateTime.now()"는 현재 날짜와 시간을 가져오는 메서드입니다.
Java 8부터 제공되는 java.time 패키지의 클래스로 날짜 및 시간을 효과적으로 처리할 수 있는 기능이 있습니다.
"DateTimeFormatter.ofPattern("yyyy.MM.dd HH:mm:ss")"은 날짜 및 시간의 형식을 지정하는 패턴을 정의합니다. "yyyy.MM.dd HH:mm:ss" 패턴을 사용하여 년, 월, 일, 시간, 분, 초를 나타내도록 설정하였습니다.
package com.example.commentserver.service;
import com.example.commentserver.entity.Comment;
import com.example.commentserver.repository.CommentRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Optional;
@Service
public class CommentServiceImpl {
@Autowired
private CommentRepository commentRepository;
// 좋아요 추가 기능
public Comment likeComment(long id) {
try {
Comment comment = commentRepository.findById(id).orElse(null);
if (comment != null) {
comment.setLikes(comment.getLikes() + 1);
return commentRepository.save(comment);
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
// 좋아요 취소 기능
public Comment unlikeComment(long id) {
try {
Comment comment = commentRepository.findById(id).orElse(null);
if (comment != null && comment.getLikes() > 0) {
comment.setLikes(comment.getLikes() - 1);
return commentRepository.save(comment);
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
// 댓글 추가 기능
public Comment save(Comment comment) {
try {
return commentRepository.save(new Comment(comment.getContent(), comment.getTime()));
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
// 댓글 조회 기능
public Optional<Comment> findById(Long id) {
try {
return commentRepository.findById(id);
} catch (Exception e) {
e.printStackTrace();
}
return Optional.empty();
}
// 댓글 수정 기능
public Comment update(Long id, Comment comment) {
try {
Optional<Comment> commentData = commentRepository.findById(id);
if (commentData.isPresent()) {
Comment _comment = commentData.get();
_comment.setContent(comment.getContent());
commentRepository.save(_comment);
return _comment;
} else {
return null;
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
// 댓글 삭제 기능
public void delete(Long id) {
try {
commentRepository.deleteById(id);
} catch (Exception e) {
e.printStackTrace();
}
}
}
댓글 기능을 위한 서비스 클래스인 CommentServiceImpl을 클래스 명으로 설정하고 생성하였습니다. 이 서비스 클래스는 댓글의 CRUD 기능을 구현합니다. 추가로 좋아요 추가 및 취소 기능도 구현하였습니다.
이 서비스 클래스는 주어진 댓글 Id에 따라 댓글을 찾아서 해당 동작을 수행합니다. 댓글의 수정은 내용(content)만을 수정하는 간단한 형태로 구현 해보았습니다. 앞서 말했듯이 추후 포스팅으로 기능을 더 추가하거나 고도화 해보도록 하겠습니다.
각 기능의 예외 처리 및 반환값은 프로덕션 환경에서 빠른 오류 파악 및 대응을 위한 것입니다.
package com.example.commentserver.controller;
import com.example.commentserver.entity.Comment;
import com.example.commentserver.service.CommentServiceImpl;
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.Optional;
@CrossOrigin("*")
@RestController
@RequestMapping("/api")함
public class CommentController {
@Autowired
private CommentServiceImpl commentService;
// 좋아요 추가 기능
@PostMapping("/comments/like/{id}")
public ResponseEntity<Comment> likeComment(@PathVariable("id") long id) {
try {
Comment likedComment = commentService.likeComment(id);
if (likedComment != null) {
return ResponseEntity.ok(likedComment);
} else {
return ResponseEntity.notFound().build();
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
// 좋아요 취소 기능
@PostMapping("/comments/unlike/{id}")
public ResponseEntity<Comment> unlikeComment(@PathVariable("id") long id) {
try {
Comment unlikedComment = commentService.unlikeComment(id);
if (unlikedComment != null) {
return ResponseEntity.ok(unlikedComment);
} else {
return ResponseEntity.notFound().build();
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
// 댓글 추가 기능
@PostMapping("/comments")
public ResponseEntity<Comment> createComment(@RequestBody Comment comment) {
try {
ResponseEntity
.status(HttpStatus.CREATED)
.body(commentService.save(comment));
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
// 댓글 조회 기능
@GetMapping("/comments/{id}")
public ResponseEntity<Optional<Comment>> getCommentById(@PathVariable("id") long id) {
try {
return ResponseEntity.ok(commentService.findById(id));
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
// 댓글 수정 기능
@PutMapping("/comments/{id}")
public ResponseEntity<Comment> updateComment(
@PathVariable("id") long id,
@RequestBody Comment comment
) {
try {
ResponseEntity
.status(HttpStatus.CREATED)
.body(commentService.update(id, comment));
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
// 댓글 삭제 기능
@DeleteMapping("/comments/{id}")
public ResponseEntity<HttpStatus> deleteComment(@PathVariable("id") long id) {
try {
commentService.delete(id);
ResponseEntity.noContent();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
댓글과 관련된 기능을 처리하는 CommentController 클래스를 작성하였습니다. 이 컨트롤러 클래스는 앞 서비스 클래스에 맞도록 댓글의 생성, 조회, 수정, 삭제, 좋아요 추가, 좋아요 취소 기능을 작성해 주었습니다.
이제 작성한 코드가 제대로 작동하는지 확인해 보겠습니다.
Mysql Workbench에 연결하여 POSTMAN으로 댓글 데이터가 잘 작동하는지 확인 해보도록 하겠습니다.
우선 Mysql Workbench를 설치해 줍니다.

설치가 됐다면 좌측 상단 동그라미 표시를 클릭하여 스키마를 생성해줍니다.

스키마 이름을 설정해 줍니다.(저는 테스트로 하였습니다. 나중에 스프링 프로퍼터에 작성해주는 것이니 알맞게 설정해주면 되겠습니다.)

스키마 이름을 입력하고 Apply를 누르면 다음과 같은 화면이 나오면 스키마 생성이 된것입니다.

이제 스키마 목록 좌측 상단에 Refresh 버튼을 누르고 우리가 방금 입력하여 설정했던 스키마 이름이 나온다면 성공!
이제 스키마에 테이블 설정을 해줍시다.

우리가 생성한 스키마를 누르고 테이블에서 오른쪽 마우스를 클릭한 뒤 테이블 생성을 클릭하면 됩니다.

테이블 컬럼들을 이렇게 설정해주도록 합니다. 우리가 위 스프링 코드에서 설정한 컬럼들과 데이터타입이 일치해야합니다. (안그럼 예외나 오류가 발생합니다)
ID는 AI(Auto_increment)를 설정하므로 숫자를 부여하는 규칙은 보통 하나씩 증가하는 형태로 구성하여금 설정 해주었습니다. ex)1. 2. 3. ...

모두 설정을 마치고 Apply 버튼을 누르면 됩니다. 별다른 오류가 없이 좌측 스키마 목록에 우리가 만든 스키마에서 생성한 테이블이 뜬다면 성공!
이제 테이블이 제대로 생성이 되었는지 확인해 봅시다.

우리가 스프링에서 값을 준 그대로 생성이 잘 된 것 같습니다.
이제 스프링과 연결을 해봅시다.

스프링 build.gradle 에서 다음과 같이 의존성을 추가해줍니다.

그런다음 어플리케이션 프로퍼터에서 다음과 같이 입력해 줍니다.
위 test는 방금 말했던 우리가 설정한 스키마의 이름을 적어주면 됩니다.

이제 실행을 했을 때 아무런 경고나 오류, 예외 등이 실행창에 나오지않고 위와 같은 Started CommentServerApplication이 실행되었다면 스프링 파일을 실행시키는데 까진 성공한겁니다.
이제 POSTMAN으로 데이터가 잘 들어가는지 확인해 봅시다.

POSTMAN에서 요청을 POST 설정하고 url을 입력합니다. 여기서 이 url은 우리가 스프링 컨트롤러 클래스에서 설정한 url을 적으시면 됩니다.
Body탭을 선택하고 raw를 선택한 다음 JSON 형식으로 데이터를 보낼 준비를 합니다.
이번엔 간단한 댓글 데이터 구현해보기가 주제이니 여기서 userId와 voteId값은 아직 외래키를 연결하지 않았으므로 null값을 주도록 설정하였습니다.
위와같이 원하는 댓글내용(content)을 작성하고 send 요청을 보낸뒤 우측 하단과 같이 200 OK가 뜨고 스프링에서도 아래와 같이 별다른 오류가 없이 Completed initialization이 출력창에 나온다면 데이터가 성공적으로 들어가게된 것 입니다.

이제 우리가 입력한 데이터가 데이터베이스에 잘 들어갔는지 확인해 봅시다.

위 사진처럼 입력한 값들이 제대로 들어갔다면 성공입니다!
위 댓글 데이터 목록을 보시다시피 우리가 설정했던 Auto_increment의 ID값과 우리가 작성한 댓글의 내용(content), 좋아요의 초기값 0, 댓글을 작성한 현재시간이 정확히 데이터가 잘 전달이 된 것 같습니다.
나머지 수정과 삭제도 해볼까요??

수정과 삭제를 위해 댓글 데이터를 더 추가해 주었습니다.
여기서 2번 댓글에 대한 내용을 반갑습니다 에서 어서오세요로 바꿔보도록 하겠습니다.

요청을 PUT으로 설정해주고 마찬가지로 컨트롤러에서 설정한 url인 commments 뒤에 수정하고싶은 댓글의 ID를 입력합니다. 그러고 댓글의 내용을 원하는 내용으로 적어주고 send 요청을 보냅니다.
추가와 마찬가지로 POSTMAN과 스프링에 별다른 오류가 없이 200 OK 메세지와 스프링 실행창에 완료됐다는 메세지가 나온다면 수정에 성공했을 것입니다.
이제 다시 데이터베이스에서 확인해보겠습니다.

사진과 같이 2번 댓글의 내용이 반갑습니다에서 저희가 설정해 주었던 어서오세요로 수정된것이 보이실겁니다.
이제 댓글 데이터 삭제도 해보도록 하겠습니다.

삭제도 수정과 추가와 마찬가지로 스프링 컨트롤러에서 설정한 DELETE 요청으로 설정하시고 삭제를 원하는 댓글의 ID를 입력하시면 됩니다.
저는 3번 댓글을 지워보도록 하겠습니다.
위와같이 입력하시고 send 요청을 보낸 뒤 200 OK와 스프링 실행창에 완료됐다는 메세지가 뜬다면 성공!
이제 데이터베이스에서 확인해보도록 하겠습니다.

이렇게 댓글 데이터가 삭제완료 된것을 볼 수 있습니다.
이제 마지막으로 댓글의 좋아요를 테스트 해봅시다.

댓글의 좋아요는 우리가 컨트롤러에서 이런식으로 작성했던 것이 기억나실겁니다.
이 부분 그대로 POSTMAN url에 작성해 주시면 됩니다.

이렇게 POST 요청으로 send 요청을 보내 1번 댓글에 좋아요를 남겨보겠습니다.

이렇게 하단에 우리가 구현한 로직대로 좋아요가 1씩 증가하는 것을 볼 수 있습니다. 데이터베이스에서도 데이터가 잘 전달이 됐는지 확인해 볼까요??

좋아요가 잘 구현이 된 것 같습니다.
이제 좋아요를 더 추가하고 좋아요를 취소하는 기능을 테스트 해보겠습니다.

1번 댓글에 총 4번의 좋아요를 누르신걸 볼 수 있습니다.
이제 1번 댓글에 좋아요를 한 번만 취소해보도록 하겠습니다.

이전에 우리가 작성한 컨트롤러 코드의 url부분을 POSTMAN의 url입력란에 맞게 적어주시면 됩니다.

컨트롤러에 맞게 요청을 설정한 뒤 원하는 댓글의 ID를 입력하고 send 요청을 보내면??

위 댓글 조회값과 같이 좋아요가 4개에서 3개로 1개 줄어들었습니다. 따라서 좋아요 취소 기능도 정상적으로 실행이 되는걸 볼 수 있습니다.
데이터베이스에도 데이터가 잘 전달이 됐는지 확인 해볼까요?

성공적으로 실행이 잘 된 것 같습니다.
이렇게 이번에는 커뮤니티 댓글에 관련하여 간단한 CRUD와 좋아요 기능을 구현해 보았습니다. 다음 포스팅에서는 위 기능에 있어서 유저 테이블과 투표 테이블을 외래키로 연결하여 합친다음 한 유저에 대한 좋아요 중복 방지 기능과 댓글에 답글을 입력할 수 있는 대댓글 기능, 댓글에 미디어(사진, 동영상 등) 데이터를 추가하여 생성하는 기능 등을 추가하거나 고도화 시켜보는 시간을 가져보도록 하겠습니다.
감사합니다!
사진 출처 : 구글 이미지