오늘 피드 댓글 쪽 끝낼 생각으로 달려본다.

 

업데이트 관련 부분은 처음 포스팅한다. 업데이트 관련 http 메서드 중 patch와 put 이 있다. 이 차이를 몰라서 지난번에 한번 검색한 적 이 있는데 적어볼까 한다.

간단히 구글링 먼저 해보자 Difference between PUT and PATCH request 아래처럼 자세하게 설명이 나온다.

Put Patch
PUT is a method of modifying resource where the client sends data that updates the entire resource . PATCH is a method of modifying resources where the client sends partial data that is to be updated without modifying the entire data.

Put => 클라이언트 측에 서 보낸 데이터를 이용해 전체 자원을 업데이트 한다.

Patch => 업데이트되어야 할 부분의 데이터만 보내서 전체 자원을 수정하지 않고 업데이트한다.

 

심플하다 그런데 단지 이 이유만으로 http method를 이렇게 구분 지을 필요가 없다고 생각한다. 조금 더 자세하게 검색해보자.

검색하다 보면 이런 단어 가 자주 보인다 Idempotence  발음 도 힘들게 시리 멱등법칙 이라고 한다. 한글도 어렵다 ;;;

의미를 보자면 연산을 여러 번 적용하더라도 결과가 달라지지 않는 성질을 의미한다.

 

Put 은 멱등성을 지녀 여러 번 request를 보내더라도,한번의 request 를 보낸 결과와 같다. 오케이 인정한다
그렇다면 Patch는?

멱등성을 지니지 않는다 따라서 request를 N번 재시도하면 서버에 생성된 N개의 서로 다른 N개의 자원이 생긴다.라고 알려준다.
응? 

 

동일한 데이터를 이용해서 patch를 보내면 항상 같은 값을 리턴한다.

 

ex) Patch /user/1 => {email :"guiwoo@hotamil.com"} 

위 요청을 100만 번 쏘더라도 100만 번 같은 결과 값을 가진다. 검색해보자. 위키피디아 가  틀렸을 리가 없는데.. 

음 ㅋㅋㅋ 나와 같은 고민을 한 사람이 있다. (궁금하면 보러 가기)

 

요약해서 설명하자면 patch를 이용해서 user를 생성하는 operation 이 있는 api 가 있다고 가정해보자. 그렇게 되면 패치를 넣을 때마다. 유저의 개수는 증가해서 결과가 달라진다. 이래서 멱등성을 지니지 않는다고 한다.

이건 예제에 따라 엄청난 차이가 있다고 생각한다. 사용하는 방법에 따라 벽등이 될 수도 아닐 수도 있다는 의미 아닌가? 

 

구글 첫 번째 설명을 읽고 patch 가 항상 좋은 줄 알았는데 실버 불릿이 아니었던 거다 ㅎㅎ

 

긱포긱 에도 설명이 잘 나와 있는데 거기서 처음 보는 차이점을 발견했다. 
즉 전송되는 데이터가 전체 vs 일부분 이기 때문에 이에 따른 전송속도? 대역 폭의 차이에 대해서도 설명해주고 있다.

put 이 높고 patch는 전송해야 하는 일부분이기 때문에 대역폭? 이 낮다고 설명되어 있다.


그렇다면 전송해야 하는 데이터에 따라 다르 게 볼 수 있다. 만약에 patch를 이용해 데이터 전부를 보내면? 둘이 같은 대역폭 인 게 아닌가? 이게 차이점이 될 수 있나? 속도의 측에서 차이가 나나?


속도의 측면에서는 어느 게 빠르다고 딱히 단정할 수 없다고 한다 궁금해서 검색해 봤다 ㅎㅎ

 

그리고 애초에 put을 사용하지 patch 를 사용 하지 않는다 ㅎㅎ 바보다 ㅋㅋ 그렇다면 전송 데이터의 값에 따라 patch, put을 선택해서 사용하면 될 것 같다. 

 

서론이 엄청 길었다 기본적인 차이점만 알고 있었지 찾다 보니 시간이 많이 흘렀다. 바로 컨트롤러 테스트 구현해보자. 

이제 이 정도 했으면 테스트 통달했다. 바로 컨트롤러 작성해주러 가보자.

깔끔하게 통과한다. 음? 뭐하나 빼먹었다. ㅎㅎ 업데이트인데 코멘트를 받지 않는다 response body 받을 클래스 만들어주자

더보기
package com.workduo.member.content.dto;

import lombok.*;

import javax.validation.constraints.NotBlank;

public class ContentCommentUpdate {
    
    @Getter
    @Setter
    @AllArgsConstructor
    @NoArgsConstructor
    @Builder
    public static class Request{
        
        @NotBlank
        private String comment;
        
    }
    
}

이에 따른 바인딩 검증 추가를 테스트로 추가적으로 해주면 될듯하다. 지난번 이용한 notblank를 이용해서 작성하자.

최종 테스트 코드

@Nested
@DisplayName("멤버 피드 댓글 업데이트 API 테스트")
class contentCommentListUpdate{

    @Test
    @DisplayName("멤버 피드 댓글 업데이트 실패 [리퀘스트 검증 테스트]")
    public void NotBlankTest() throws Exception{
        ContentCommentUpdate.Request req =
                ContentCommentUpdate.Request.builder().comment("Test").build();

        var test1
                = validator.validate(req);
        assertThat(test1.size()).isEqualTo(0);

        req.setComment("");
        var test2 = validator.validate(req);
        assertThat(test2.size()).isEqualTo(1);

        req.setComment(" ");
        var test3 = validator.validate(req);
        assertThat(test3.size()).isEqualTo(1);

        req.setComment(null);
        var test4 = validator.validate(req);
        assertThat(test4.size()).isEqualTo(1);
    }

    @Test
    @DisplayName("멤버 피드 댓글 업데이트 성공")
    public void successCommentList() throws Exception{

        ContentCommentUpdate.Request req
                = ContentCommentUpdate.Request.builder().comment("test").build();

        mockMvc.perform(patch("/api/v1/member/content/3/comment/1")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(req))
                )
                .andExpect(status().isOk())
                .andExpect(
                        jsonPath("$.success")
                                .value("T")
                )
                .andDo(print());
    }
}

 

자 업데이트를 위한 서비스 코드를 작성해야 하는데 이번에는 예외처리를 빡빡하게 해야 하니 미리 정해보고 가자

 

1. 로그인 유저 미일치

2. 피드 가 존재하지 않는 경우

3. 피드 가 삭제된 경우

4. 로그인 한 유저가 작성한 댓글 이 아닌 경우

5. 댓글 이 존재하지 않는 경우

6. 댓글 이 삭제된 경우

 

이 정도 생각  이 나는데 추가적으로 뭐가  더 있을지 고민해보자 흠... 일단 발리 데이트 함수에 전부 때려 박고 추후에 업데이트 날려주자  

어우 길다 1 ~ 3번까지는 코드의 반복이기에  지난번 코드를 가져와서 사용하겠습니다.

 

더보기
@Test
@DisplayName("멤버 피드 댓글 업데이트 실패 [로그인 유저 미 일치]")
public void doesNotMatchLogInUser() throws Exception{
    //given
    given(commonRequestContext.getMemberEmail()).willReturn("True-Lover");
    //when
    MemberException exception = assertThrows(MemberException.class,
            ()->memberContentService.contentCommentUpdate(contentId,
                    commentId,req));
    //then
    assertEquals(MemberErrorCode.MEMBER_EMAIL_ERROR,exception.getErrorCode());
}

@Test
@DisplayName("멤버 피드 댓글 업데이트 실패 [피드 가 존재하지 않는 경우]")
public void doesNotExistFeed() throws Exception{
    //given
    doReturn(Optional.of(m))
            .when(memberRepository).findByEmail(any());
    //when
    MemberException exception = assertThrows(MemberException.class,
            ()->memberContentService.contentCommentUpdate(contentId,
                    commentId,req));
    //then
    assertEquals(MemberErrorCode.MEMBER_CONTENT_DOES_NOT_EXIST,exception.getErrorCode());
}

@Test
@DisplayName("멤버 피드 댓글 업데이트 실패 [피드가 삭제된 경우]")
public void hasDeletedFeed() throws Exception{
    //given
    doReturn(Optional.of(m))
            .when(memberRepository).findByEmail(any());
    doReturn(Optional.of(MemberContent.builder().deletedYn(true).build()))
            .when(memberContentRepository).findById(any());
    //when
    MemberException exception = assertThrows(MemberException.class,
            ()->memberContentService.contentCommentUpdate(contentId,
                    commentId,req));
    //then
    assertEquals(MemberErrorCode.MEMBER_CONTENT_DELETED,exception.getErrorCode());
}

이렇게1~3번 테스트 가 통과한다 서비스 코드는 단 2줄이면 클리어

 

4번 테스트 코드

생각을 해봤는데 댓글의 권한보다 존재 여부를 먼저 파악하는 게 우선인 거 같아서 순서를 바꿨다.

쿼리를 짜다가 문득 아니 댓글 아이디도 알고 코멘트도 알고, 멤버도 알고 있는데 굳이 자바에 들고 와서 해야 하는가? 딜레마에 빠졌다.

select * member_content_comment mcc where mcc.member_content_comment_id = 9;

select * from member_content_comment mcc
where mcc.member_content_comment_id = 9 and member_content_id = 4 and member_id = 2;

별반 차이가 없다 둘 다 인덱스 조회로 들고 온다. 그렇다면 어떤 걸 해야 할까? 


https://stackoverflow.com/questions/7976374/filter-data-in-sql-or-in-java 

 

Filter data in SQL or in Java?

What is the general guideline/rule for filtering data? I am accustomed to seeing filters in an SQL statement in the WHERE clause, although there are occasions that filters introduce complexity to t...

stackoverflow.com

여기에는 상황에 맞게 사용하라고 한다.  

이번에는 그럼 한방에 들고 오면 코드의 양도 줄 것으로 예상되고 그렇게 어려운 쿼리가 아니니깐 한방에 들고 오자. 

 

//REPOSITORY
Optional<MemberContentComment> findByIdAndMemberAndMemberContent(
            Long mcc,Member m, MemberContent mc);

//TEST
@Test
@DisplayName("멤버 피드 댓글 업데이트 실패 [댓글 이 존재하지 않는 경우]")
public void doesNotExistComment() throws Exception{
    //given
    doReturn(Optional.of(m))
            .when(memberRepository).findByEmail(any());
    doReturn(Optional.of(MemberContent.builder().build()))
            .when(memberContentRepository).findById(any());
    //when
    MemberException exception = assertThrows(MemberException.class,
            ()->memberContentService.contentCommentUpdate(contentId,
                    commentId,req));
    //then
    assertEquals(MemberErrorCode.MEMBER_COMMENT_DOES_NOT_EXIST,exception.getErrorCode());
}

//SERVICE
@Override
public void contentCommentUpdate(Long memberContentId, Long commentId, ContentCommentUpdate.Request req) {
    Member m = validCheckLoggedInUser();
    MemberContent mc = getContent(commentId);
    isContentDeleted(mc);

    MemberContentComment memberContentComment = memberContentCommentRepository.
            findByIdAndMemberAndMemberContent(commentId, m, mc)
            .orElseThrow(() -> new MemberException(MEMBER_COMMENT_DOES_NOT_EXIST));

}

테스트가 통과된다 굿

 

5번 테스트 코드

@Test
@DisplayName("멤버 피드 댓글 업데이트 실패 [댓글 이 삭제된 경우]")
public void hasDeletedComment() throws Exception{
    //given
    doReturn(Optional.of(m))
            .when(memberRepository).findByEmail(any());
    doReturn(Optional.of(mc)).when(memberContentRepository).findById(any());
    doReturn(Optional.of(MemberContentComment.builder()
            .deletedYn(true)
            .build()))
            .when(memberContentCommentRepository)
            .findByIdAndMemberAndMemberContent(commentId,m,mc);
    //when
    MemberException exception = assertThrows(MemberException.class,
            ()->memberContentService.contentCommentUpdate(contentId,
                    commentId,req));
    //then
    assertEquals(MemberErrorCode.MEMBER_COMMENT_DELETED,exception.getErrorCode());
}

//SERVICE
@Override
public void contentCommentUpdate(Long memberContentId, Long commentId, ContentCommentUpdate.Request req) {
    Member m = validCheckLoggedInUser();
    MemberContent mc = getContent(commentId);
    isContentDeleted(mc);

    MemberContentComment memberContentComment = memberContentCommentRepository.
            findByIdAndMemberAndMemberContent(commentId, m, mc)
            .orElseThrow(() -> new MemberException(MEMBER_COMMENT_DOES_NOT_EXIST));

    if(memberContentComment.isDeletedYn()){
        throw new MemberException(MEMBER_COMMENT_DELETED);
    }

}

작성하다 보니 권한이 없는 경우는 발생될 수가 없다.

토큰 인증받은 유저의 아이디와 피드 아이디 코멘트 아이디로 조회해온 댓글이
로그인한 유저와 맞지 않을 수 있는 방법이 있는가?  이 리퀘스트 가 진행되는 동안에는 발생할 수 없는 경우인 것 같지만

혹시 모르니 테스트 코드만 내버려두고 넘어가자.

 

6번 테스트 성공 케이스

@Test
@DisplayName("멤버 피드 댓글 업데이트 성공")
public void success() throws Exception{
    MemberContentComment build = MemberContentComment.builder().build();
    MemberContentComment spy = spy(build);
    //given
    doReturn(Optional.of(m))
            .when(memberRepository).findByEmail(any());
    doReturn(Optional.of(mc)).when(memberContentRepository).findById(any());
    doReturn(Optional.of(spy))
            .when(memberContentCommentRepository)
            .findByIdAndMemberAndMemberContent(commentId,m,mc);

    //when
    memberContentService.contentCommentUpdate
            (contentId,commentId,req);
    //then
    verify(spy,times(1)).updateComment(any());
 }
 
 //SERVICE
 @Override
public void contentCommentUpdate(Long memberContentId, Long commentId, ContentCommentUpdate.Request req) {
    Member m = validCheckLoggedInUser();
    MemberContent mc = getContent(commentId);
    isContentDeleted(mc);

    MemberContentComment memberContentComment = memberContentCommentRepository.
            findByIdAndMemberAndMemberContent(commentId, m, mc)
            .orElseThrow(() -> new MemberException(MEMBER_COMMENT_DOES_NOT_EXIST));

    if(memberContentComment.isDeletedYn()){
        throw new MemberException(MEMBER_COMMENT_DELETED);
    }
    memberContentComment.updateComment(req.getComment());
}

가볍게 통과된다 저 스파이 기법은 전에도 설명했다시피 해당 인스턴스의 함수가 실행되는지 확인해서 함수의 마지막까지 실행되는지 그 여부를 파악했다.

 

모든 테스트 가 통과된다.

서버를 띄우고 실행해보자. 쿼리는 총 멤버 찾는 쿼리, 컨탠트 찾는쿼리, 커맨트 찾는쿼리 3개 에 업데이트 쿼리 1방 총 4번이다.

에구.. 저기 memberContent 아이디 똑바로 안 넣어줘서 피드를 찾지 못하는 에러가 나왔다... ㅠ 

// 멤버 가져오는 쿼리 1방
select
    member0_.member_id as member_i1_12_,
    member0_.created_at as created_2_12_,
    member0_.updated_at as updated_3_12_,
    member0_.deleted_at as deleted_4_12_,
    member0_.email as email5_12_,
    member0_.member_status as member_s6_12_,
    member0_.nickname as nickname7_12_,
    member0_.password as password8_12_,
    member0_.phone_number as phone_nu9_12_,
    member0_.profile_img as profile10_12_,
    member0_.status as status11_12_,
    member0_.username as usernam12_12_ 
from
    member member0_ 
where
    member0_.email=?
                      
// Content 가져오는 쿼리 1방
select
    membercont0_.member_content_id as member_c1_15_0_,
    membercont0_.created_at as created_2_15_0_,
    membercont0_.updated_at as updated_3_15_0_,
    membercont0_.content as content4_15_0_,
    membercont0_.deleted_at as deleted_5_15_0_,
    membercont0_.deleted_yn as deleted_6_15_0_,
    membercont0_.member_id as member_10_15_0_,
    membercont0_.notice_yn as notice_y7_15_0_,
    membercont0_.sort_value as sort_val8_15_0_,
    membercont0_.title as title9_15_0_ 
from
    member_content membercont0_ 
where
    membercont0_.member_content_id=?
        
// Comment 가져오는 쿼리 1방

select
    membercont0_.member_content_comment_id as member_c1_16_,
    membercont0_.created_at as created_2_16_,
    membercont0_.updated_at as updated_3_16_,
    membercont0_.content as content4_16_,
    membercont0_.deleted_at as deleted_5_16_,
    membercont0_.deleted_yn as deleted_6_16_,
    membercont0_.member_id as member_i7_16_,
    membercont0_.member_content_id as member_c8_16_ 
from
    member_content_comment membercont0_ 
where
    membercont0_.member_content_comment_id=? 
    and membercont0_.member_id=? 
    and membercont0_.member_content_id=?
        
// Update 쿼리 한방

update
    member_content_comment 
set
    updated_at=?,
    content=?,
    deleted_at=?,
    deleted_yn=?,
    member_id=?,
    member_content_id=? 
where
    member_content_comment_id=?

코멘트에 where 절에 and로 묶여서 잘 날아간다. ㅎㅎ 기분 이 좋다

 

자 이제 서비스 부분의 코드를 리펙토링 하자.

ㅎㅎ 저기 코멘트 가져오는 코드는 다음 API에서도 사용할 거 같아 따로 빼주었다  삭제 체크하는 함수도 따로 빼주고 매우 맘에 든다.

 

긴 글 읽어주셔서 감사합니다.

+ Recent posts