[프로젝트4] 4. 셀프좋아요 불가하도록 Exception추가, 댓글 기능 추가 (서버)

rin·2020년 6월 25일
1
post-thumbnail

목표
1. 자신이 쓴 글은 좋아요 할 수 없도록 만든다.
2. 댓글 관련 도메인을 추가한다.
3. 댓글 기능을 수행하는 컨트롤러와 서비스를 개발한다.

자신이 쓴 글은 좋아요 할 수 없도록 하기

프론트는 만지지 않을 것이고, Exception을 추가해서 에러 메세지로 좋아요 불가한 사유를 보여줄 것이다.

따라서 Exception을 먼저 추가해주자.

🔎 GoodContentsHistoryExceptionType

CANNOT_LIKE_OWN_WRITING(3003, 200, "자신의 글에는 좋아요를 할 수 없습니다.");

🔎 BoardService
addGoodPoint 메소드에 본인글인지 판단하는 if 문을 추가해 위의 Exception을 호출하게한다.

    public GoodContentsHistoryEntity addGoodPoint(UserForm userForm, long boardId) {
        UserEntity user = Optional.of(userRepository.findByAccountId(userForm.getAccountId())).orElseThrow(() -> new FreeBoardException(UserExceptionType.NOT_FOUND_USER));
        BoardEntity target = Optional.of(boardRepository.findById(boardId).get()).orElseThrow(() -> new FreeBoardException(BoardExceptionType.NOT_FOUNT_CONTENTS));

        // 추가된 부분
        if (target.getWriter().equals(user)){
            throw new FreeBoardException(GoodContentsHistoryExceptionType.CANNOT_LIKE_OWN_WRITING);
        }

        goodContentsHistoryRepository.findByUserAndBoard(user, target).ifPresent(none -> {
            throw new FreeBoardException(GoodContentsHistoryExceptionType.HISTORY_ALREADY_EXISTS);
        });

        return goodContentsHistoryRepository.save(
                GoodContentsHistoryEntity.builder()
                        .board(target)
                        .user(user)
                        .build()
        );
    }

🔎 BoardApiControllerTest

    @Test
    @DisplayName("자신이 작성한 글에 좋아요 시도 시 예외가 발생한다.")
    void add_like_exception_test() throws Exception {
        BoardEntity newBoard = BoardEntity.builder().contents("contents").title("title").writer(testUser).build();
        boardRepository.save(newBoard);

        mvc.perform(post("/api/boards/" + newBoard.getId() + "/good")
                .session(mockHttpSession))
                .andExpect(result -> assertEquals(result.getResolvedException().getClass().getCanonicalName(), FreeBoardException.class.getCanonicalName()))
                .andExpect(result -> assertEquals(result.getResolvedException().getMessage(), GoodContentsHistoryExceptionType.CANNOT_LIKE_OWN_WRITING.getErrorMessage()))
                .andExpect(status().isOk());
    }

테스트 코드까지 잘 수행되면 웹에서 내가 쓴 글에 좋아요를 시도해보자

댓글 추가하기

설계

목적은 댓글과 좋아요를 이용해서 배치를 사용하는 것이기 때문에 대댓글같은 부가적인 기능은 추가하지 않을 것이다. 댓글을 작성하고, 수정하고, 삭제하는 기능을 만드는 것이 목표이다.

댓글과 관련해서 Comment 엔티티를 만들자. 관계는 다음과 같다.

  1. 게시글 : 댓글 = 1 : N → ManyToOne, BoardId
  2. 댓글 작성자 : 댓글 = 1 : 1 → OneToOne, UserId

🔎 CommentEntity
domain 패키지 하위에 Comment 패키지를 만들고 아래와 같이 엔티티 클래스를 만들어준다.

@Entity
@Getter
@Table("comment")
@NoArgsConstructor
public class CommentEntity extends BaseEntity {

    @ManyToOne
    @Column(name = "boardId")
    private BoardEntity board;

    @OneToOne
    @Column(name = "writerId")
    private UserEntity writer;

    private String contents;

    @Builder
    public CommentEntity (BoardEntity board, UserEntity writer, String contents){
        this.board = board;
        this.writer = writer;
        this.contents = contents;
    }

    public void update(CommentEntity newComment){
        this.contents = newComment.contents;
    }

}

🔎 CommentRepository
같은 댑스에 레파지토리도 추가해준다.

수행할 쿼리는

  1. pk로 하나의 댓글 가져오기
  2. boardId로 한 게시글에 달린 모든 댓글 페이징 처리해서 가져오기

이다.

따라서 다음처럼 작성했다.

@Repository
public interface CommentRepository extends JpaRepository<CommentEntity, Long> {
    Page<CommentEntity> findAllByBoard(BoardEntity board, Pageable pageable);
}

Service Layer

comment 패키지 하위에 CommentService 클래스를 추가한다.
서비스에는 get, save, update, delete 세 개의 메소드를 만들 것이다. save에서는 클라이언트에서 받은 form이 들어올 것이기 때문에 api 패키지 하위에 comment 패키지를 생성하고 다음처럼 CommentForm 클래스를 정의해주자.

🔎 CommentForm

@Getter
@NoArgsConstructor
public class CommentForm {
    
    private String contents;
    
    @Builder
    public CommentForm(String contents){
        this.contents = contents;
    }
    
    public CommentEntity convertCommentEntity(UserEntity writer, BoardEntity board){
        return CommentEntity.builder()
                .writer(writer)
                .contents(this.contents)
                .board(board)
                .build();
    }
    
}

엔티티를 반환해 줄 때는 Dto를 이용할 것이므로 같은 댑스에 CommentDto를 추가하였다.

🔎 CommentDto

@NoArgsConstructor
@Getter
public class CommentDto {
    
    private long id;
    private UserDto writer;
    private String contents;
    
    private CommentDto(CommentEntity comment){
        this.id = comment.getId();
        this.writer = UserDto.of(comment.getWriter());
        this.contents = comment.getContents();
    }
    
    public static CommentDto of(CommentEntity comment){
        return new CommentDto(comment);
    }
    
}

서비스는 간단한 CRUD 수준이므로 어렵지 않게 작성할 수 있을 것이다.
🔎 CommentService

@Service
@Transactional
public class CommentService {

    private UserRepository userRepository;
    private CommentRepository commentRepository;
    private BoardRepository boardRepository;

    @Autowired
    public CommentService(UserRepository userRepository, CommentRepository commentRepository, BoardRepository boardRepository) {
        this.userRepository = userRepository;
        this.commentRepository = commentRepository;
        this.boardRepository = boardRepository;
    }

    public CommentDto save(CommentForm commentForm, UserForm userForm, long boardId) {
        UserEntity writer = Optional.of(userRepository.findByAccountId(userForm.getAccountId())).orElseThrow(() -> new FreeBoardException(UserExceptionType.NOT_FOUND_USER));
        BoardEntity target = boardRepository.findById(boardId).orElseThrow(() -> new FreeBoardException(BoardExceptionType.NOT_FOUNT_CONTENTS));

        CommentEntity savedComment = commentRepository.save(commentForm.convertCommentEntity(writer, target));
        return CommentDto.of(savedComment);
    }

    public Page<CommentEntity> get(Pageable pageable, long boardId) {
        BoardEntity target = boardRepository.findById(boardId).orElseThrow(() -> new FreeBoardException(BoardExceptionType.NOT_FOUNT_CONTENTS));
        return commentRepository.findAllByBoard(target, pageable);
    }

    public CommentDto update(CommentForm commentForm, UserForm userForm, long id) {
        UserEntity user = Optional.of(userRepository.findByAccountId(userForm.getAccountId())).orElseThrow(() -> new FreeBoardException(UserExceptionType.NOT_FOUND_USER));
        //TODO : custom exception으로 변경
        CommentEntity target = commentRepository.findById(id).orElseThrow(() -> new RuntimeException());

        if (IsWriterEqualToUserLoggedIn.confirm(target.getWriter(), user) == false) {
            throw new FreeBoardException(BoardExceptionType.NO_QUALIFICATION_USER);
        }

        target.update(commentForm);
        return CommentDto.of(target);
    }

    public void delete(UserForm userForm, long id) {
        UserEntity user = Optional.of(userRepository.findByAccountId(userForm.getAccountId())).orElseThrow(() -> new FreeBoardException(UserExceptionType.NOT_FOUND_USER));
        //TODO : custom exception으로 변경
        CommentEntity target = commentRepository.findById(id).orElseThrow(() -> new RuntimeException());

        if (IsWriterEqualToUserLoggedIn.confirm(target.getWriter(), user) == false && HaveAdminRoles.confirm(user) == false) {
            throw new FreeBoardException(BoardExceptionType.NO_QUALIFICATION_USER);
        }
        commentRepository.delete(target);
    }


}

서비스 메소드가 잘 돌아가는지 확인하기 위해서 테스트 코드를 작성해보자 🙋🏻

🔎 CommentServiceIntegrationTest

@ExtendWith(SpringExtension.class)
@ContextConfiguration(locations = {"file:src/main/webapp/WEB-INF/applicationContext.xml"})
@Transactional
@Rollback(false)
class CommentServiceIntegrationTest {

    @Autowired
    private CommentService sut;

    @Autowired
    private CommentRepository commentRepository;

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private BoardRepository boardRepository;

    private UserEntity userEntity;
    private BoardEntity boardEntity;
    private UserForm userForm;
    private CommentForm newComment;

    @BeforeEach
    void init(){
        userEntity = userRepository.findAll().get(0);
        userForm = UserForm.builder().accountId(userEntity.getAccountId()).password(userEntity.getPassword()).build();
        boardEntity = boardRepository.findAll().get(0);

        newComment = CommentForm.builder().contents("무플방지위원회에서 나왔습니다. ^^*").build();
    }

    @Test
    void save_test(){
        CommentDto commentDto = sut.save(newComment, userForm, boardEntity.getId());
        CommentEntity savedComment = commentRepository.findById(commentDto.getId()).get();

        assertThat(savedComment.getId(), equalTo(commentDto.getId()));
    }

    @Test
    void update_test(){
        CommentEntity savedComment = getNewSavedCommentEntity();

        String updateContents = "수정 후 댓글 내용입니다~~";
        CommentForm commentForm = CommentForm.builder().contents(updateContents).build();

        CommentDto updatedComment = sut.update(commentForm, userForm, savedComment.getId());

        assertThat(updatedComment.getContents(), equalTo(updateContents));
    }

    private CommentEntity getNewSavedCommentEntity() {
        return commentRepository.save(newComment.convertCommentEntity(userEntity, boardEntity));
    }

    @Test
    void delete_test(){
        CommentEntity savedComment = getNewSavedCommentEntity();

        sut.delete(userForm, savedComment.getId());

        Optional<CommentEntity> deletedEntity = commentRepository.findById(savedComment.getId());

        assertThat(deletedEntity.isPresent(), equalTo(false));
    }

    @Test
    void get_test(){
        int pageSize = 5;
        int pageNumber = 1;

        Page<CommentEntity> commentEntityPage = sut.get(PageRequest.of(pageNumber, pageSize), boardEntity.getId());

        assertThat(commentEntityPage.getNumber(), equalTo(pageNumber));
        assertThat(commentEntityPage.getSize(), equalTo(pageSize));
    }

}

ApiController

이미 서비스를 만들어뒀기 때문에, 컨트롤러는 수월하게 만들 수 있을 것이다.

🔎 CommentApiController
api/comment 패키지 하위에 CommentApiController를 만들어 주자.

@RestController
@RequestMapping("/api/comments")
@RequiredArgsConstructor
public class CommentApiController {

    private final HttpSession httpSession;
    private final CommentService commentService;

    @GetMapping(params = {"boardId"})
    public ResponseEntity<PageDto<CommentDto>> get(@PageableDefault(page = 1, size = 5, sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable,
                                                   @RequestParam long boardId) {
        Page<CommentEntity> commentEntityPage = commentService.get(PageUtil.convertToZeroBasePage(pageable), boardId);
        List<CommentDto> commentEntities = commentEntityPage.getContent().stream().map(commentEntity -> CommentDto.of(commentEntity)).collect(Collectors.toList());

        return ResponseEntity.ok(PageDto.of(commentEntityPage, commentEntities));
    }

    @PostMapping(params = {"boardId"})
    public ResponseEntity<CommentDto> post(@RequestBody CommentForm commentForm, @RequestParam long boardId) {
        if (httpSession.getAttribute("USER") == null) {
            throw new FreeBoardException(UserExceptionType.LOGIN_INFORMATION_NOT_FOUND);
        }
        CommentDto savedComment = commentService.save(commentForm, (UserForm) httpSession.getAttribute("USER"), boardId);
        return ResponseEntity.ok(savedComment);
    }

    @PutMapping("/{id}")
    public ResponseEntity<CommentDto> put(@RequestBody CommentForm commentForm, @PathVariable long id) {
        if (httpSession.getAttribute("USER") == null) {
            throw new FreeBoardException(UserExceptionType.LOGIN_INFORMATION_NOT_FOUND);
        }
        CommentDto updatedComment = commentService.update(commentForm, (UserForm) httpSession.getAttribute("USER"), id);
        return ResponseEntity.ok(updatedComment);
    }

    @DeleteMapping("/{id}")
    public void delete(@PathVariable long id) {
        if (httpSession.getAttribute("USER") == null) {
            throw new FreeBoardException(UserExceptionType.LOGIN_INFORMATION_NOT_FOUND);
        }
        commentService.delete((UserForm) httpSession.getAttribute("USER"), id);
    }

}

🔎 CommentApiControllerTest
api를 검증하기 위한 mockMvc 테스트를 작성하였다.

우선 test root 하위에 utils 패키지를 만들고 TestUtil이라는 클래스에 랜덤하게 문자열을 가져오는 메소드를 static으로 정의하였다. (이전엔 계속 복붙복붙..👀💦해서 썼는데 영 귀찮아서 이렇게 쓰는게 났겠음)

public class TestUtil {

    public static String getRandomString(int length) {
        String id = "";
        for (int i = 0; i < length; i++) {
            double dValue = Math.random();
            if (i % 2 == 0) {
                id += (char) ((dValue * 26) + 65);   // 대문자
                continue;
            }
            id += (char) ((dValue * 26) + 97); // 소문자
        }
        return id;
    }

}

테스트 코드는 아래와 같다.

@ExtendWith(SpringExtension.class)
@ContextConfiguration(locations = {"file:src/main/webapp/WEB-INF/applicationContext.xml", "file:src/main/webapp/WEB-INF/dispatcher-servlet.xml"})
@Transactional
@WebAppConfiguration
class CommentApiControllerTest {

    @Autowired
    private WebApplicationContext webApplicationContext;

    @Autowired
    private BoardRepository boardRepository;

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private CommentRepository commentRepository;

    @Autowired
    private EntityManager entityManager;

    private MockMvc mvc;

    @Autowired
    private MockHttpSession mockHttpSession;

    private UserEntity testUser;
    private BoardEntity testBoard;

    private ObjectMapper objectMapper;


    @BeforeEach
    public void initMvc() {
        initSessionAndBoard();

        objectMapper = new ObjectMapper();

        mvc = MockMvcBuilders
                .webAppContextSetup(webApplicationContext)
                .addFilters(new CharacterEncodingFilter("UTF-8", true))
                .build();
    }

    private void initSessionAndBoard() {
        testUser = userRepository.findAll().get(0);
        UserForm userForm = UserForm.builder().accountId(testUser.getAccountId()).password(testUser.getPassword()).build();
        testBoard = boardRepository.findAllByWriterId(testUser.getId()).get(0);

        mockHttpSession.setAttribute("USER", userForm);
    }

    @Test
    void get_test() throws Exception {
        mvc.perform(get("/api/comments")
                .param("boardId", String.valueOf(testBoard.getId()))
                .session(mockHttpSession))
                .andExpect(status().isOk());
    }

    @Test
    void post_test() throws Exception {
        String TestContents = "댓글 달기 테스트~~ "+TestUtil.getRandomString(20);

        ObjectMapper objectMapper = new ObjectMapper();
        CommentForm commentForm = CommentForm.builder().contents(TestContents).build();

        MvcResult response = mvc.perform(post("/api/comments")
                .contentType(MediaType.APPLICATION_JSON_VALUE)
                .content(objectMapper.writeValueAsString(commentForm))
                .param("boardId", String.valueOf(testBoard.getId()))
                .session(mockHttpSession))
                .andExpect(status().isOk())
                .andReturn();

        String contents = response.getResponse().getContentAsString();
        CommentDto savedComment = objectMapper.readValue(contents, CommentDto.class);
        assertThat(savedComment.getContents(), equalTo(TestContents));
    }

    @Test
    void update_test() throws Exception {
        CommentEntity savedComment = createPrevEntity();

        String TestContents = "댓글 업데이트 테스트~~ "+TestUtil.getRandomString(20);

        CommentForm commentForm = CommentForm.builder().contents(TestContents).build();

        MvcResult response = mvc.perform(put("/api/comments/"+savedComment.getId())
                .contentType(MediaType.APPLICATION_JSON_VALUE)
                .content(objectMapper.writeValueAsString(commentForm))
                .session(mockHttpSession))
                .andExpect(status().isOk())
                .andReturn();

        String contents = response.getResponse().getContentAsString();
        CommentDto updatedComment = objectMapper.readValue(contents, CommentDto.class);
        assertThat(updatedComment.getContents(), equalTo(TestContents));
    }

    private CommentEntity createPrevEntity() {
        CommentForm prevComment = CommentForm.builder().contents("이전 값").build();
        CommentEntity savedComment = commentRepository.save(prevComment.convertCommentEntity(testUser, testBoard));
        entityManager.flush();
        return savedComment;
    }

    @Test
    void delete_test() throws Exception {
        CommentEntity savedComment = createPrevEntity();

        mvc.perform(delete("/api/comments/"+savedComment.getId())
                .session(mockHttpSession))
                .andExpect(status().isOk());

        Optional<CommentEntity> deletedComment = commentRepository.findById(savedComment.getId());

        assertThat(deletedComment.isPresent(), equalTo(false));
    }

}

이전에 작성한 mockMvc 테스트와는 약간 다르게 작성하긴했는데.. 추가된 것이라고 하면 andReturn()을 통해 반환값을 받아서 이를 assertThat()에 사용한 것이다.
직렬화되어서 넘어오는 데이터를 objectMapper를 이용해 객체에 매핑 시켜주었다. 이 때 한글 깨짐이 발생할 수 있는데 아래처럼 mockMvc를 생성할 때 EncodingFilter를 사용하면 해결할 수 있다.

모든 코드는 github에서 확인 할 수 있습니다.

profile
🌱 😈💻 🌱

0개의 댓글