한 동안 프로젝트 진행하고 있었다. API 구현 내용과 과정을 여기에 서술해 볼까 한다.
약 한달? 전에 미리 회의를 통해서 왼쪽과 같이 미리 스펙 등을 정해 놓았다. 저기 에 명시되어 있지 않지만. 구현하려면 저기에 엮인 모든 부분들을 날려 주어야 하는데
1. 멤버 컨탠트 좋아요 부분
2. 멤버 컨탠트 댓글 부분
3. 멤버 컨탠트 댓글 좋아요 부분
cascade 가 걸려있지 않아서 일일이 날려 주어야 한다. 왜? 최초 설계 할 때 매니 투 원을 이용해서 설계를 시작했고, 추후 필요한 부분이 생길 시 양방향으로 연관관계를 설정하려고 했기에 cascade 가 없다.
원투 매니 부분은 디비상에 영향을 주지 않으니 추후 리팩터링 과정에서 추가할지 말지 결정할 생각이다.
바로 가보자
1. Git pull부터 해서 떙겨오자 커밋된 내용들을
동료 분 이 그룹 컨 탠트 댓글 쪽 작업을 하셨네요 ㅎㅎㅎ 얼른 브런치 만들고 pr 날립시다
현재 스테이터스를 찍어보자
git status
On branch guiwoo
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: src/main/resources/application-dev.properties
no changes added to commit (use "git add" and/or "git commit -a")
프로퍼티 쪽에 aws 코드들이 있어 따로 커밋 할부분은 아니다.
자 먼저 컨트롤러 테스트 코드부터 작성해보자.
@Nested
@DisplayName("멤버 피드 삭제 API 테스트")
class delete{
@Test
@DisplayName("멤버 피드 삭제 성공")
public void successUpdate() throws Exception{
//given
doNothing().when(memberContentService).contentUpdate(any(),any());
//when
mockMvc.perform(delete("/api/v1/member/content/3")
.contentType(MediaType.APPLICATION_JSON)
)
.andExpect(status().isOk())
.andExpect(jsonPath("$.success").value("T"))
.andDo(print());
}
}
음 바디가 따로 필요 없다 content 부분 넣어줄 필요 없고, 이 포스트 쓰기 전 프로젝트 개발을 하면서 저 Nested 어노테이션이 효자노릇을 톡톡히 한다. 테스트를 돌리면 저 네스티드 분기 별로 나눠서 결과를 보여준다
이렇게 구분 선을 지어준다. 피드 삭제 부분은 컨트롤러가 없어서 에러가 났으니 얼른 작성해주러 가자.
@DeleteMapping("{memberContentId}")
public ResponseEntity<?> deleteContent(
@PathVariable("memberContentId") Long contentId
){
//서비스 코드 작성
return new ResponseEntity<>(
CommonResponse.ok(),
HttpStatus.OK
);
}
이러고 테스트 돌리면 통과한다. 실패하는 경우를 스펙에서 보자.
1. 로그인 안 한 유저가 악의적으로 api 호출 떄리는경우
2. 글 작성 유저와 로그인 한 유저가 일치하지 않는 경우
3. 글 이 이미 삭제된 경우
음 이렇게 3가지 경우가 되는데 이건 컨트롤러 쪽이 아닌 서비스 쪽에서 핸들링을 해야 할부분인 거 같다. 이런 실패 케이스는 서비스 코드 쪽에서 테스트 하자.
물론 네스트를 이용해서 둘러싸주자
@Nested
@DisplayName("멤버 피드 삭제 테스트")
class TestDeleteContent{
Long contentId = 169L;
ContentUpdate.Request req = ContentUpdate.Request.builder()
.title("Holy")
.content("Moly")
.build();
@Test
@DisplayName("멤버 피드 삭제 실패 [로그인 유저 미 일치]")
public void doseNotMatchUser(){
//given
given(commonRequestContext.getMemberEmail()).willReturn("True-Lover");
//when
MemberException exception = assertThrows(MemberException.class,
()->memberContentService.contentUpdate(contentId,req));
//then
assertEquals(MemberErrorCode.MEMBER_EMAIL_ERROR,exception.getErrorCode());
}
}
지난번에 작성한 멤버 발리드 함수를 이 딜리트 함수에도 써주자
어 생각해보니깐 피드가 존재하지 않는 경우도 있지 않겠는가? 테스트 코드 2개 작성해주고 오자
@Test
@DisplayName("멤버 피드 삭제 실패 [피드 작성자가 아닌 경우]")
public void doNotHaveAuthorization() throws Exception{
//given
given(commonRequestContext.getMemberEmail()).willReturn("True-Lover");
doReturn(Optional.of(Member.builder().email("True-Lover").build()))
.when(memberRepository).findByEmail(any());
doReturn(Optional.of(MemberContent.builder().build()))
.when(memberContentRepository).findById(any());
//when
MemberException exception = assertThrows(MemberException.class,
()->memberContentService.contentDelete(contentId));
//then
assertEquals(MemberErrorCode.MEMBER_CONTENT_AUTHORIZATION
,exception.getErrorCode());
}
@Test
@DisplayName("멤버 피드 삭제 실패 [피드가 삭제된 게시글 인 경우]")
public void feedTerminatedBefore() throws Exception{
Member m = Member.builder().id(2L).build();
//given
doReturn(Optional.of(m))
.when(memberRepository).findByEmail(any());
doReturn(Optional.of(MemberContent.builder().member(m)
.deletedYn(true).build()))
.when(memberContentRepository).findById(any());
//when
MemberException exception = assertThrows(MemberException.class,
()->memberContentService.contentDelete(contentId));
//then
assertEquals(MemberErrorCode.MEMBER_CONTENT_DELETED
,exception.getErrorCode());
}
테스트 코드를 돌려보자. 샷샷샷 , 매우 만족스럽다. 사실 수정 코드와 겹치는 부분이 많아서 긁어 왔다.
이제 성공 테스트를 해 야 한다. 이알디를 보고 오자
1. 피드 좋아요를 날려준다.
2. 피드 코멘트 좋아요 를 날려준다.
3. 피드 코멘트를 날려준다.
4. 피드 컨 탠트 내용을 지우며 업데이트 해주자.
(추후 코멘트 쪽 작업 하닥 원투 매니 걸고 커스 케이드 걸리면 리팩터링 하자.)
테스트 코드 먼저 작성하고 테스트 가 통과되도록 해보자.
verify(memberContentLikeRepository,times(1))
.deleteAllByMemberContent(any());
좋아요 날려주러 가보자.
// 레포지토리 쪽에 작성해주고
void deleteAllByMemberContent(Long memberContent);
// 서비스 쪽에 서 사용해주자
//피드 좋아요 날려
memberContentLikeRepository.
deleteAllByMemberContent(contentId);
음 좋아 통과된다.
피드 코멘트 좋아요를 날려 주자 레포지토리에 작성하는데 문득 피드 안에는 다양한 댓글들이 있을 텐데 그 댓글 들을 모조리 가져와서 쿼리를 날려야 한다 이게 왜 문제가 될까? 우리 쏜이 글 쓰고 지운다고 생각해보자 수많은 댓글과 좋아요 과 삽시간에 달릴 거다. 댓글이 100만 개 가 됐다고 쳐보자. 지워달라는 api 요청이 와서 우리 코드는 실행을 하는데 게시글 하나를 지우기 위해서 댓글당 좋아요를 지우기 위해 쿼리 100만 번을 날린다 ㅋㅋㅋㅋ 1방 쿼리로 변경해주자.
void deleteAllByMemberContentCommentIn(List<Long> commentsId);
in을 활용해서 날려주자.
@Override
public void contentDelete(Long contentId) {
Member member = validCheckLoggedInUser();
MemberContent content = getContent(contentId);
validEditCheck(member,content);
//댓글 아이디 어떻게 가져올래 ?
List<MemberContentComment> cList =
memberContentCommentRepository.findAllByMemberContent(content);
//피드 좋아요 날려
memberContentLikeRepository.
deleteAllByMemberContent(content);
//댓글 좋아요들 날려
memberContentCommentLikeRepository.
deleteAllByMemberContentCommentIn(cList);
}
최종적으로 완성된 테스트 코드이다.
@Test
@DisplayName("맴버 피드 삭제 성공")
public void feedTerminate() throws Exception{
//given
MemberContent build = MemberContent.builder().member(m)
.deletedYn(false).build();
MemberContent spy = spy(build);
doReturn(Optional.of(m))
.when(memberRepository).findByEmail(any());
doReturn(Optional.of(spy))
.when(memberContentRepository).findById(any());
doReturn(new ArrayList<>())
.when(memberContentCommentRepository).findAllByMemberContent(any());
//when
memberContentService.contentDelete(contentId);
verify(memberContentLikeRepository,times(1))
.deleteAllByMemberContent(any());
verify(memberContentCommentLikeRepository,times(1))
.deleteAllByMemberContentCommentIn(any());
verify(memberContentCommentRepository,times(1))
.deleteAllByMemberContent(any());
verify(spy,times(1)).terminate();
}
스파이를 한 이유는 코드가 마지막까지 닿아서 실행되는지 확인을 하고 싶어서 넣었다. 서비스 코드 쪽을 봐보자
@Override
public void contentDelete(Long contentId) {
Member member = validCheckLoggedInUser();
MemberContent content = getContent(contentId);
validEditCheck(member,content);
//댓글 아이디 어떻게 가져올래 ?
List<MemberContentComment> cList =
memberContentCommentRepository.findAllByMemberContent(content);
//피드 좋아요 날려
memberContentLikeRepository.
deleteAllByMemberContent(content);
//댓글 좋아요들 날려
memberContentCommentLikeRepository.
deleteAllByMemberContentCommentIn(cList);
//댓글 날려
memberContentCommentRepository.
deleteAllByMemberContent(content);
//컨탠트 딜리트 yn true 로 업데이트
content.terminate();
}
멤버 아이디 가져오는 쿼리 1, 컨 탠트 가져오는 쿼리 1, 코멘트 리스트 가져오는 쿼리 1,
지우는 쿼리 총 3개 , 마지막 업데이트 쿼리 1 개 총 7개의 쿼리가 날아가야 한다.
서버를 띄워서 테스트를 해보자. 로그인을 해주고 딜리트 api로 날려주자
(개인적으로 포스트맨 보다 인썸니아 좋아한다. ㅎㅎ, 더 예쁘서 좋아한다 더 이유가 필요한가 ㅎㅎㅎ)
2022-09-25 19:18:37.264 DEBUG 6000 --- [nio-8080-exec-3] org.hibernate.SQL :
update
member_content
set
updated_at=?,
content=?,
deleted_at=?,
deleted_yn=?,
member_id=?,
notice_yn=?,
sort_value=?,
title=?
where
member_content_id=?
2022-09-25 19:18:37.282 DEBUG 6000 --- [nio-8080-exec-3] org.hibernate.SQL :
delete
from
member_content_like
where
member_content_like_id=?
2022-09-25 19:18:37.283 DEBUG 6000 --- [nio-8080-exec-3] org.hibernate.SQL :
delete
from
member_content_like
where
member_content_like_id=?
2022-09-25 19:18:37.287 DEBUG 6000 --- [nio-8080-exec-3] org.hibernate.SQL :
delete
from
member_content_comment_like
where
member_content_comment_like_id=?
2022-09-25 19:18:37.287 DEBUG 6000 --- [nio-8080-exec-3] org.hibernate.SQL :
delete
from
member_content_comment_like
where
member_content_comment_like_id=?
2022-09-25 19:18:37.289 DEBUG 6000 --- [nio-8080-exec-3] org.hibernate.SQL :
delete
from
member_content_comment
where
member_content_comment_id=?
2022-09-25 19:18:37.289 DEBUG 6000 --- [nio-8080-exec-3] org.hibernate.SQL :
delete
from
member_content_comment
where
member_content_comment_id=?
음 뭔가 이상하다. 위로 스크롤 해서 보니 심지어 셀렉트 해서 들고와서 일일이 단건삭제로 넘기는거 아닌가 ? 아 ..... 직접 쿼리를 써줘야겠다..
쿼리
@Modifying
@Query("delete from MemberContentLike mcl where mcl.memberContent = :contentId")
void deleteAllByMemberContent(@Param("contentId") MemberContent memberContent);
@Modifying
@Query("delete from MemberContentComment mcc where mcc.memberContent = :memberContent")
void deleteAllByMemberContent(@Param("memberContent") MemberContent memberContent);
@Modifying
@Query("delete from MemberContentCommentLike mccl where mccl.memberContentComment in :comments")
void deleteAllByMemberContentCommentIn(
@Param("comments")
List<MemberContentComment> memberContentComment);
다시 테스트 해보자.
크 아름답다 정확히 딜리트가 3번 나가고 1번 업데이트 쿼리가 날아간다 ㅎㅎㅎ 만족스럽다.
고민이 든다 만약에 그냥 지우는 게 아니라 그냥 업데이트 쿼리를 이용해 셀렉트 해서 퍼올릴 때 필터링을 해주면 어떨까 비용적인 측면에서 차이가 있을까라고 고민해본다. 검색을 조금 해봤지만. 정책에 따라 가는 것 같다. 지워야 하면 무조건 지우고 그게 아니라면 업데이트시켜서 쿼리 퍼올릴 때 필터 하는 것도 한 방법이 될 수 있을 것 같다.
솔직히 셀렉트 쪽에서는 쿼리 디에스엘을 이용해서 한 번에 퍼올리기가 가능하기에 7번이라는 숫자가 많다고 느껴질 수도 있다. 이런 인서트 업데이트, 딜리트 쪽은 쿼리의 정확성이 보다 중요하다고 생각하기 때문에 7번이라는 쿼리 숫자가 크게 많다고 느껴지지 않는다.
기분 좋게 api 한 부분을 구현했다. 다음에도 시간이 되면 추가적으로 글을 작성할 예정이다.
긴 글 읽어주셔서 감사합니다.
'사이드 프로젝트 > 워크듀오-개발일지' 카테고리의 다른 글
멤버 피드 댓글 삭제 API (1) | 2022.09.28 |
---|---|
멤버 피드 댓글 업데이트 API (0) | 2022.09.28 |
멤버 피드 댓글 조회 API (1) | 2022.09.28 |
멤버 피드 댓글 작성 API (0) | 2022.09.28 |
멤버 피드 좋아요 API 구현 (0) | 2022.09.27 |