프로그래밍/Backend

나만의게시글만들기(6)

supernovaMK 2024. 8. 16. 19:08

지난번의 게시글 만들기에 이어서 댓글에 관해 코딩해보고자 한다.

 

게시글과 댓글은 다대일 관계를 가진다. 

댓글 관련 엔티티를 먼저 작성해보도록 하자면

`

package com.example.firstproject.entity;

import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;

@Entity
@Getter
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class Comment {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @ManyToOne
    @JoinColumn(name="article_id")
    private Article article;
    @Column
    private String nickname;
    @Column
    private String body;

}

 

@ManyToOne:다대일 관계를 설명해주는 어노테이션이다.

@JoinColumn(name="article_id"): 외래키를 참조하여 연결해주는 어노테이션으로 참조하는 테이블의 기본 키와 매핑된다.

Article 객체와의 참조가 이뤄지기 때문에 이 부분에 대해서 알아보았다.

 

  • Java 객체: 객체 지향적으로 관계를 설정합니다. Comment 객체는 Article 객체를 필드로 가지며, Article 객체를 통해 게시글과의 관계를 설정합니다.
  • 데이터베이스: 관계형 데이터베이스에서는 외래 키를 사용하여 엔티티 간의 관계를 설정합니다. 이 관계는 SQL의 JOIN 문 등을 통해 사용됩니다.

즉 데이터베이스와 자바간의 참조방식이 다르기 때문에 sql 구문에서는 게시글 기본키를 활용하여 데이터를 찾게 되고,

Java에서는 Article 객체를 넣어주면서 만들어주게 된다.

 

댓글 관련 레포지토리에 관한 코드도 작성해 보았다.

package com.example.firstproject.repository;

import com.example.firstproject.entity.Comment;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;

import java.util.List;

public interface CommentRepository extends JpaRepository<Comment,Long> {
    @Query(value = "SELECT * from Comment WHERE article_id=:articleId",nativeQuery = true)
    List<Comment> findByArticleId(Long articleId);
    List<Comment> findByArticleNickname(String nickname);

}

 

 

보이는 것 처럼 

 

1. 게시글 아이디를 활용하여 해당 댓글 리스트를 뽑아내는 메서드와

2.한 닉네임이 작성한 모든 댓글들을 뽑아 내는 메서드 

 

이 두가지를 구현해보고자 한다.

쿼리문을 메서드를 활용하여 사용할 수 있는데 그 방법이 네이티브 쿼리 메서드이다.

@Query 어노테이션과 xml 파일을 활용하여 만드는 두가지 방법이 있어 각각 해보자면

@Query(value = "SELECT * from Comment WHERE article_id=:articleId",nativeQuery = true)

 

어노테이션을 활용하여 메서드에서 직접적인 쿼리문을 활용할 수 있다. netiveQuery를 true값으로 해준다면 sql과 유사한 jpql사용 대신 sql문을 활용할 수 있다.

 

네이티브 쿼리 xml를 활용하여 메서드를 작성할 수 있는데 이 때 XML의 기본 경로와 파일 이름은 resource>META-INF >orm.xml 경로이다.

 

xml파일을 자동으로 찾아주어 이를 매핑해주게 된다.

 

 

이 작성한 두 코드가 잘 작동되는지 테스트 코드를 작성해보자면

 

package com.example.firstproject.repository;

import com.example.firstproject.entity.Article;
import com.example.firstproject.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 java.util.Arrays;
import java.util.List;

import static org.junit.jupiter.api.Assertions.*;
@DataJpaTest
class CommentRepositoryTest {
    @Autowired
    CommentRepository commentRepository;
    @Test
    @DisplayName("특정 게시글의 모든 댓글 조회")
    void findByArticleId() {

        {   //1.입력 데이터 준비
        Long articleid = 4L;
        //2.실제 데이터
        List<Comment> comments = commentRepository.findByArticleId(articleid);
        //3.예상 데이터
        Article article = new Article(4L, "당신의 인생 영화는?", "댓글 고");
        Comment a = new Comment(1L, article, "park", "쇼생크");
        Comment b = new Comment(2L, article, "kim", "보헤미안");
        List<Comment> expected = Arrays.asList(a, b);
        //4.비교 및 검증
        assertEquals(comments.toString(), expected.toString(), "4번 게시글 모두 출력!");
    }
        {   //1.입력 데이터 준비
            Long articleid = 1L;
            //2.실제 데이터
            List<Comment> comments = commentRepository.findByArticleId(articleid);
            //3.예상 데이터
            Article article = new Article(1L, "가가가가", "1111");
            List<Comment> expected = Arrays.asList();
            //4.비교 및 검증
            assertEquals(comments.toString(), expected.toString(), "1번 게시글  댓글 없음!");
        }

    }

    @Test
    @DisplayName("닉네임으로 검색")
    void findByArticleNickname() {
        //1.입력 데이터 준비
        String nickname="park";
        //2.실제 데이터
        List<Comment> comments=commentRepository.findByArticleNickname(nickname);

        //3.예상 데이터
        Comment a=new Comment(1L,new Article(4L,"당신의 인생 영화는?","댓글 고"),nickname,"쇼생크");
        Comment b=new Comment(3L,new Article(5L,"당신의 소울 푸드는","댓글 고"),nickname,"치킨");
        Comment c=new Comment(5L,new Article(6L,"당신의 취미는","댓글 고"),nickname,"배드민턴");
        List<Comment> expected= Arrays.asList(a,b,c);
        //4.비교 및 검증
        assertEquals(comments.toString(),expected.toString(),"park의 모든 댓글 출력");
    }
}

 

전에 확인할 경우에는 @SpringbootTest를 활용하여 테스트 코드를 작성하였는데,

이번에는 레퍼지토리에 관련된 테스트를 위해서 @DataJpaTest 어노테이션을 활용하여 엔티티등의 객체를 테스트 코드에서 사용 할 수 있다.

 

@DisplayName을 활용하여 테스트하는 메서드의 이름을 바꿀 수 있다.

 

 

@SpringBootTest vs @DataJpaTest

 

Gpt에게 물어본 답변이다. 결론은 레포지터리만 테스트 할 경우에는 애플리케이션 컨텍스트를 로드 하지 않는 @DataJpaTest를 사용하는 것이 좋다.

 

@SpringBootTest를 사용한 경우

  • 장점:
    • 전체 스프링 애플리케이션 컨텍스트를 로드하므로, 애플리케이션의 모든 구성 요소가 포함된 테스트를 작성할 수 있습니다. 이는 전체적인 통합 테스트를 수행할 때 유용합니다.
    • 복잡한 통합 시나리오나 여러 계층의 상호작용을 테스트할 수 있습니다.
  • 단점:
    • 전체 스프링 컨텍스트를 로드하므로 테스트가 상대적으로 느릴 수 있습니다. 특히 많은 애플리케이션 구성 요소가 로드되는 경우 성능에 영향을 미칠 수 있습니다.
    • 불필요하게 큰 컨텍스트가 로드되므로, 레포지토리만 테스트하는 경우에는 과도할 수 있습니다.

@DataJpaTest를 사용한 경우

  • 장점:
    • JPA와 관련된 부분만 테스트하므로, 데이터베이스와 레포지토리 계층의 동작을 집중적으로 테스트할 수 있습니다.
    • 임베디드 데이터베이스를 자동으로 설정하고, 트랜잭션 롤백을 통해 테스트가 독립적이며 데이터베이스 상태가 깨끗하게 유지됩니다.
    • 전체 스프링 컨텍스트를 로드하지 않기 때문에 테스트 실행이 빠릅니다.
  • 단점:
    • JPA와 관련된 부분만 테스트하므로, 더 복잡한 상호작용이나 통합 테스트를 다루기 어렵습니다.

 

본격적으로 댓글 관련된 서비스 계층과 컨트롤러를 만들어보려고 한다. 

 

먼저 댓글을 조회하는 과정을 만들어보자.

 

@RestController
public class CommentApiController {
    @Autowired
    CommentService commentService;
    //1.댓글 조회
    @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);

    }

 

ResponseEntity<List<CommentDto>> 로 반환값을 설정해주는 부분을 주의해야한다. 뷰 템플릿을 활용하여 만들려고 하기에 반환값을 Dto로 해주었다.

 

서비스 계층을 살펴보면

package com.example.firstproject.service;

import com.example.firstproject.dto.CommentDto;
import com.example.firstproject.entity.Comment;
import com.example.firstproject.repository.ArticleRepository;
import com.example.firstproject.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
public class CommentService {
    @Autowired
    private CommentRepository commentRepository;
    @Autowired
    private ArticleRepository articleRepository;


    public List<CommentDto> comments(Long articleId) {
        /*//1.댓글 조회
        List<Comment> comments =commentRepository.findByArticleId(articleId);
        //2.Dto로 변환
        List<CommentDto> commentDtos = new ArrayList<CommentDto>();
        for(int i=0;i<comments.size();i++){
            Comment comment=comments.get(i);
            CommentDto dto=CommentDto.createCommentDto(comment);
            commentDtos.add(dto);
        }*/

        return commentRepository.findByArticleId(articleId).stream()
                .map(CommentDto::createCommentDto)
                .collect(Collectors.toList());

    }
}

 

스트림 문법을 사용하여 엔티티를 dto로 변환해주고 이를 다시 리스트로 만들어 주어 반환해 주었다.

 

Dto코드도 계속해서 살펴보도록 하자.

 

package com.example.firstproject.dto;

import com.example.firstproject.entity.Article;
import com.example.firstproject.entity.Comment;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.springframework.web.bind.annotation.GetMapping;

@AllArgsConstructor
@NoArgsConstructor
@Getter
@ToString
public class CommentDto {
private Long id;
private Long articleId;
private String nickname;
private String body;

    public static CommentDto createCommentDto(Comment comment) {

        return new CommentDto(comment.getId(),comment.getArticle().getId(),comment.getNickname(),comment.getBody());
    }
}

 

엔티티를 dto로 바꾸어주는 메서드를 추가해주면 된다.

 

 

댓글을 생성하는 과정을 만들어보았다.

컨트롤러부터 확인해보자면

 

    //2.댓글 생성
    @PostMapping("/api/articles/{articleId}/comments")
    public ResponseEntity<CommentDto> create(@PathVariable Long articleId, @RequestBody CommentDto dto){
        CommentDto commentDto=commentService.create(articleId,dto);


        return ResponseEntity.status(HttpStatus.OK).body(commentDto);

    }

 

원래 return해주는 부분에서 삼항 연산자를 사용하여 예외처리를 해주었지만, 이제는 코드 내부에서 예외처리를 발생시키는 방식으로 바꾸어 주어 사용하도록 한다.

 

안에서 일어나는 과정들을 서비스코드에게 위임을 해주어 사용한다.

 

서비스 코드를 살펴보자.

    @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);
        return CommentDto.createCommentDto(created);
    }

 

DB와 관련된 작업은 오류가 생기면 다시 롤백 해주기 위해서 transactional 어노테이션을 주로 사용하는 것을 사용한다.

 

먼저 orElseThrow안에 IllegalArgumentException을 발생시켜 게시글 조회 및 예외 발생에 주의하게 된다.

 

Comment.createComment를 활용하여 dto정보와article을 활용하여 댓글 엔티티로 만들어주는 과정을 거치게 된다.

이때,  예외 처리를 한번 해주게 되는데, article의 id와 dto에서 보낸 id가 서로 일치하는지 확인하는 과정을 거치게 된다.

 

 if(dto.getId()!=null)
     throw new IllegalArgumentException("댓글 생성 실패, id가 없어야 합니다.");
if(dto.getArticleId()!=article.getId())
    throw new IllegalArgumentException("댓글 생성 실패,게시글 id가 잘못 되었습니다.");

 

두가지 과정에서 예외를 처리해주게 된다.

 

다음은 수정작업에서 살펴본다.

컨트롤러부터 살펴보자면, 

 

@PatchMapping("/api/commnets/{id}")
public ResponseEntity<CommentDto> update(@PathVariable Long id,@RequestBody CommentDto dto){
    CommentDto commentDto =commentService.update(id,dto);


    return ResponseEntity.status(HttpStatus.OK).body(commentDto);
}

 

서비스도 살펴보면

@Transactional
public CommentDto update(Long id, CommentDto dto) {
Comment target = commentRepository.findById(id).orElseThrow(()->new IllegalArgumentException("없는 댓글 입니다."));
target.patch(dto);
Comment updated=commentRepository.save(target);
return CommentDto.createCommentDto(updated);

}

 

orElseThrow를 사용하여 예외를 발생 시킬 수 있다.

 

이후 target을 patch작업을 거져서 이를 수정해주게 된다.

 

댓글 삭제는 상대적으로 간단하다.

 

//4.댓글 삭제
    @DeleteMapping("/api/comment/{id}")
    public ResponseEntity<CommentDto> delete(@PathVariable Long id){
        CommentDto commentDto =commentService.delete(id);

        return ResponseEntity.status(HttpStatus.NO_CONTENT).body(commentDto);

    }

 

@Transactional
public CommentDto delete(Long id) {
Comment comment =commentRepository.findById(id).orElseThrow(()->new IllegalArgumentException("없는 게시글입니다."));
commentRepository.delete(comment);

return CommentDto.createCommentDto(comment);
}