주어진 배열 과 값이 나오고 배열 내에서 해당 값을 지워서 리턴해주면 되는 간단한 문제이다.
다만 문제의 조건상 에 추가적인 메모리 가 없기 떄문에 배열 자체를 수정해서 리턴 해야한다.
class Solution {
public int removeElement(int[] nums, int val) {
int left =0;
for(int i=0;i<nums.length;i++){
if(nums[i] != val){
int tmp = nums[left];
nums[left++] = nums[i];
nums[i] = tmp;
}
}
return left;
}
}
left 값 하나 를 선정해서 투포인터 느낌으로 스왑 해가면서 풀었다. for loop 자동으로 증가하는 값을 right 로 생각하면 보다 쉽게 풀듯하다.
나름 난감했던 문제 였다. Stack 을 이용하지 않고 구현을 해보려고 노력하다 보니 그런거 같다.
class Solution {
public int[] plusOne(int[] digits) {
int len = digits.length-1;
StringBuilder answer = new StringBuilder();
int willAdd = 1;
for (int i = len; i >=0 ; i--) {
int next = digits[i] + willAdd;
answer.append(next%10);
if(next > 9) willAdd = 1;
else willAdd = 0;
}
if(willAdd == 1) answer.append(1);
int[] ans = new int[answer.length()];
for (int i = 0; i < ans.length; i++) {
ans[i] = answer.charAt(ans.length-1-i)-'0';
}
return ans;
}
}
1ms 가 나왔다 스택을 이용해서 속도적인 차이가 궁금해서 바로 구현해 봤다.
class Solution {
public int[] plusOne(int[] digits) {
Stack<Integer> stack = new Stack<>();
int len = digits.length-1;
int willAdd = 1;
for (int i = len; i >= 0; i--) {
int val = digits[i]+willAdd;
stack.push(val%10);
if(val > 9) willAdd = 1;
else willAdd = 0;
}
if(willAdd == 1) stack.push(1);
int[] answer = new int[stack.size()];
for (int i = 0; i < answer.length; i++) {
answer[i] = stack.pop();
}
return answer;
}
}
2ms 가 나온다 더 느리다 . 스택은 백터 를 상속받아서 구현 된 클래스다. 따라서 스레드 세이프 하다는 의미이다. 대부분의 함수에 syncronize 가 걸려있다.
동기화가 제거된 버전인 Deque 를 사용해보자 보통 queue , stack 에서 deque 를 사용하면 성능적인 측면에서 좋은 효과가 있다 왜 ?
스레드 세이프 하지 않으니깐. 1ms 가 나온다.
다른 사람의 풀이를 한번 확인해보자.
class Solution {
public int[] plusOne(int[] digits) {
int n = digits.length;
for(int i=n-1; i>=0; i--) {
if(digits[i] < 9) {
digits[i]++;
return digits;
}
digits[i] = 0;
}
int[] newNumber = new int [n+1];
newNumber[0] = 1;
return newNumber;
}
}
와.... 생각지도 못했던 부분이다. 물론 9 이하인경우 +1 해서 리턴 생각을 했지만 구현의 구체적인 방향성이 떠오르지 않아 위와같이 작성했는데.... 0ms 나온다. 저 9이하 리턴 부분이 최적화 가 완성된 부분이기떄문에 2배이상 빠른것 같다.
지난번 pr 에 이렇게 리뷰를 받았다, ㅎㅎ 생각해보니 멤버가 인자에 포함되어 있다면 본인 이 쓴 댓글에 만 좋아요와 좋아요 취소가 가능해지는 바보같은 실수를 해버렸지 뭔가 ㅎㅎㅎ ...
바로 피드백 반영해서 추가하고 머지 했다.
어제 오전 오후 는 서버 띄워서 완전 Api 테스트 하느라 시간을 보냈다. 자바버전 이 달라서 생기는오류, 각각 db ddl 이 달라서 생기는 오류 등 파일의 이전에 따른 querydsl 컴파일 미실시 등 다양한 에러를 경험하며 어제 동료분 에게 메세지 엄청 보냈던것 같다. ㅎㅎㅎ
저런 자잘한 오류들 을 해결하고 나니 시간이 훌쩍 가있지 않은가..
바로 일정 관련 부분 오늘은 한번에 작성할 생각이다.
이렇게 보면 도대체 리스트 와 달력 의 구분이 왜 필요한가 애초에 줄때 그냥 던져주면 안되는건가 ? 라고 생각할수도 있지만, 우리의 미리 만들어진 와이어프레임을 본다면
이렇게 빨간색으로 점쳐진 부분이 보이는가 ? 그부분 을 한달치 달력으로 표현을 하고자 한다. 그렇기에 이렇게 따로 api 구분을 해놨다.
바로 테스트 코드 부터 들어가 보자.
@Nested
@DisplayName("멤버 월 일정 API 테스트")
class CalendarMonthTest{
@Test
@DisplayName("멤버 월 일정 API 실패 [날짜 데이트 이상할시]")
public void fail() throws Exception{
//given
//when
mockMvc.perform(get("/api/v1/member/calendar?date=2022")
.contentType(MediaType.APPLICATION_JSON)
)
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.success").value("F"))
.andDo(print());
}
@Test
@DisplayName("멤버 월 일정 API 성공")
public void success() throws Exception{
//given
//when
mockMvc.perform(get("/api/v1/member/calendar?date=2022-09-29")
.contentType(MediaType.APPLICATION_JSON)
)
.andExpect(status().isOk())
.andDo(print());
;
//then
}
}
날짜 관련 부분 에 있어 잘못된 데이터 를 받을 때 테스트를 위한 총 2가지를 작성하였다.
이렇게 테스트 를 작성할때 webmvctest 를 작성해야할지 아니면 springboottest 를 할지 고민이 많을것이다.
이둘은 분명한 차이가 있으니 명확히 하고 지나가자
바로 스프링 공홈 으로 가보자.
The @SpringBootTest annotation tells Spring Boot to look for a main configuration class (one with @SpringBootApplication , for instance) and use that to start a Spring application context. You can run this test in your IDE or on the command line
스프링 부트 테스트 어노테이션 은 스프링 부트 의 기본구성 을 찾고 , 스프링 어플리케이션 컨텍스트 를 실행할때 사용한다고 한다 응 뭔소리인가 ? SpringApplication을 통해 테스트에 사용되는 ApplicationContext를 생성하여 작동한다 라고 나와있다 이말은 즉슨 스프링실행 및 구성 을 위해 필요한 빈들을 모조리 등록해서 테스트에 임한다고 생각하면 된다.
그렇다면 WebMvcTest 는 ?
If you want to focus only on the web layer and not start a complete ApplicationContext, consider using @WebMvcTest instead.
설명을 타고 내려가다 보면 이렇게 명시 되어 있다. 완저히 어플리케이션을 실행하지 않고 웹레이어 에 초점을 맞추고 싶다면, mvcTest 를 고려하라고 말이다.
@WebMvcTest auto-configures the Spring MVC infrastructure and limits scanned beans to @Controller, @ControllerAdvice, @JsonComponent, Converter, GenericConverter, Filter, HandlerInterceptor, WebMvcConfigurer, WebMvcRegistrations, and HandlerMethodArgumentResolver
이렇게 나와 있다. 컨트톨러,어드바이스,제이슨 컴포넌트 ,컴버터 드등 이 제한적으로 스캔되어 스프링 어플리 케이션을 구성한다고 나온다. 이말은 다시말해 스프링 부트 테스트 와달리 테스트 에 필요한 최소한의 빈 스캔만 한다고 생각하면 된다.
이에 필요한 빈들이 있다면 @MockBean 을 이용해 빈을 대체 해주어 컨트롤러 본연 만 테스트 를 하면 된다.
이렇게 보면 단점 과 장점이 한눈에 들어올 것이다.
webmvctest 는 빠르다 대신 모든 테스트가 통과한다고 해서, 실제 모든빈이 등록된 부트 의 어플리케이션 상에서 오류 를 야기할수있다.
springboottest 는 실제 모든 빈을 등록 해 실행 하기 때문에 실제 프로덕트 환경과 가장 유사한 테스트 를 제공하나 , 시간이 오래걸릴수 있다. 본인이 추가하고자 하는 테스트 코드를 작성해서 UnitTesting을 할지 e2e테스트 를 할지 정해서 하면 될것같다. 두개 모두 필요한 테스트 이다.
이런식의 로컬데이트 로 데이터를 받아오는 컨트롤러 뭐 특별할것 없다. 주석으로 예시를 적어 보다 직관적으로 만들어 주어야겠다.
과정을 쓰고싶었는데 어제 날라 가는 바람에 이렇게 결과물 밖에 보여줄수가 없다 .. ㅠ
유저 의 이메일을 가져오고 상태값을 비교하기 위해 저렇게 함수로 빼주었다. 다음 서비스 함수에서도 사용 될거 같아 저렇게 작성했다.
데이트 의 형태가 "2022-09-30" 이러한 형식으로 오기 때문에 , 월별로 데이터를 얻기위해 파싱을 진행했다. 추후 함수 로 빼던가 해서 보다 직관적인 형태로 만들어볼까 생각중이다.
@Nested
@DisplayName("멤버 일정 월 서비스 테스트")
class month{
@Test
@DisplayName("멤버 일정 월 서비스 실패 [로그인 유저 미일치]")
public void fail1() throws Exception{
//given
//when
MemberException exception = assertThrows(
MemberException.class,
()->memberCalendarService.getMonthCalendar(LocalDate.now()));
//then
assertEquals(MEMBER_EMAIL_ERROR,exception.getErrorCode());
}
@Test
@DisplayName("멤버 일정 월 서비스 실패 [정지된 회원]")
public void fail2() throws Exception{
Member m = Member.builder().memberStatus(MemberStatus.MEMBER_STATUS_STOP).build();
//given
given(memberRepository.findByEmail(any()))
.willReturn(Optional.of(m));
//when
MemberException exception = assertThrows(
MemberException.class,
()->memberCalendarService.getMonthCalendar(LocalDate.now()));
//then
assertEquals(MEMBER_STOP_ERROR,exception.getErrorCode());
}
@Test
@DisplayName("멤버 일정 월 서비스 실패 [탈퇴한 회원]")
public void fail3() throws Exception{
Member m = Member.builder().memberStatus(MemberStatus.MEMBER_STATUS_WITHDRAW).build();
//given
given(memberRepository.findByEmail(any()))
.willReturn(Optional.of(m));
//when
MemberException exception = assertThrows(
MemberException.class,
()->memberCalendarService.getMonthCalendar(LocalDate.now()));
//then
assertEquals(MEMBER_WITHDRAW_ERROR,exception.getErrorCode());
}
@Test
@DisplayName("멤버 일정 월 서비스 성공")
public void success() throws Exception{
Member m = Member.builder().build();
//given
given(memberRepository.findByEmail(any()))
.willReturn(Optional.of(m));
doReturn(List.of("2022-09-01","2022-09-30")).when(memberCalendarQueryRepository)
.searchMemberMonthDate(any(), any(),any());
//when
List<String> monthCalendar = memberCalendarService.getMonthCalendar(LocalDate.now());
//then
verify(memberCalendarQueryRepository,times(1))
.searchMemberMonthDate(any(),any(),any());
assertThat(monthCalendar.size()).isEqualTo(2);
}
}
1~3번 은 지난번 수 많은 테스트 의 반복으로 숙달 되었을 것이다. 성공 테스트를 보자.
memberCalendarQuery 를 이용해 dto 없이 바로 뿌려준다. 이유는 바로 데이터 별로 필터를 해서 들고 오는데 굳이 dto 가 필요한가 생각해서 그냥 저렇게 작성해주었다.
테스트는 한번의 실행됨 을 체크하고 반환값의 사이즈를 측정하였다.
이 몇줄 안되는 쿼리를 작성 하는라 고민을 조금 많이 했다. 날짜부터 시작해서 이것저것 테스트를 해본후에 이렇게 작성할수 있었다.
월별로 데이터 만 뿌려서 빨간점을 찍어주면 되기때문에 그룹바이 로 중복 처리를 해버렸다.
또한 string 타입을 groupby orderby 쪽에 작성을 바로 못하니 저렇게 stringpath 를 이용해 작성해 넘겨주자.
select
DATE_FORMAT(groupmeeti1_.meeting_start_date,
?) as col_0_0_
from
member_calendar membercale0_
inner join
group_meeting groupmeeti1_
on membercale0_.group_meeting_id=groupmeeti1_.group_meeting_id
where
membercale0_.member_id=?
and membercale0_.meeting_active_status=?
and (
groupmeeti1_.meeting_start_date between ? and ?
)
group by
DATE_FORMAT(groupmeeti1_.meeting_start_date,
?)
크 생각한 대로 쿼리가 날아간다 만족 스럽다.
테스트 도 만족 스럽게 통과 된다. 점심먹고 이어서 작성해보자.
자 다음 일정 목록을 위한 테스트 코드를 작성해보자.
노션 상에 일자를 받지 않아서 이렇게일자를 받는 방식으로 변경해 주었다.
통과 되기위해 컨트롤러 코드를 작성해보자.
@GetMapping("/list")
public ApiResult<?> getDayCalendar(
// date type example "2022-09-30"
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) @Valid LocalDate date
){
// memberService.getDayCalendar(date);
return ApiUtils.success(null);
}
바로 통과 된다 기분이 매우좋다 ㅎㅎ
서비스 테스트 코드를 작성하러 가보자.
위의 서비스와 동일한 테스트에 한가지를 더 추가 하였다 물론 프론트 에서 달력 을 클릭할때 일정이 있는경우만 클릭하게 만들수 있으나, 2번 체크해서 나쁠거 없다고 생각해서 위와같은 방어선을 하나 더 추가했다 일정이 없는경우 에러를 터트려 400을 리턴할 생각이다.
아직 서비스가 만들어 지지않아 저렇게 빨간 불이 들어온다. 지난번 만든 함수 get member 를 이용해서 1~3번 까지 의 모든 테스트 를 통과 해 주자.
4번 과 5번의 테스트 를 진행하기 전에 먼저 프론트에서 어떻게 데이터를 받아야 보다 편하게 다음 페이지로 넘겨줄수 있을까 에 대해 고민을 해보자.
1. 그룹미팅 아이디 가 포함되어 있어야 디테일을 확인할때 Api 를 쏴서 편하게 확인할수 있다.
2. 그룹 의 이름 정도는 표기 해 주는게 맞지 않겠는가 이름 도 넣어주자.
3. 시간 이 제일 중요하다 시간에 따라 표기 true flase 를 이용해 화면에 랜더링 해줄테니
@DeleteMapping("{memberContentId}/comment/{commentId}")
public ResponseEntity<?> deleteContentComment(
@PathVariable("memberContentId") Long memberContentId,
@PathVariable("commentId") Long commentId
){
// memberContentService.contentConmmentDeltet(memberContentId,commentId);
return new ResponseEntity<>(
CommonResponse.ok(),
HttpStatus.OK
);
}
@Modifying
@Query("delete from MemberContentComment mcc where mcc.id = :commentId")
void deleteById(@Param("commentId") Long commentId);
//Service
@Override
public void contentConmmentDeltet(Long memberContentId, Long commentId) {
Member m = validCheckLoggedInUser();
MemberContent mc = getContent(memberContentId);
MemberContentComment memberContentComment = getMemberContentComment(commentId, m, mc);
memberContentCommentRepository.deleteById(memberContentComment.getId());
}
테스트 는 통과되는데 음 커맨트 좋아요 가 있지 않겠는가 ? 그것도 날려줘야한다. 까먹고 있엇다.
memberContentCommentLikeRepository.deleteAllByMemberContentCommentIn(
new ArrayList<>(List.of(memberContentComment))
);
지난번 피드 만들때 생성한 함수를 이용하자 서비스 코드 쪽에 추가해주자 .
부트 를 띄워서 테스트 를 해보자.
멤버,피드,댓글 가져오는 쿼리 총 3개, 지우는 쿼리 2개
다시 생각해보니깐 우리 워크듀오는 지우지 않고 피드 를 바꿔준다. 업데이트 쿼리 1개로 바꿔주고 terminate 라는 함수를 구현해주자.
public void terminate(){
this.deletedAt = LocalDateTime.now();
this.deletedYn = true;
}
2022-09-28 22:01:32.021 DEBUG 76860 --- [nio-8080-exec-2] org.hibernate.SQL :
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=?
2022-09-28 22:01:32.023 INFO 76860 --- [nio-8080-exec-2] p6spy : #1664370092023 | took 1ms | statement | connection 9| url jdbc:mysql://localhost:3306/workduo?autoReconnect=true&allowPublicKeyRetrieval=true&rewriteBatchedStatements=true
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=?
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='client2@client.com';
2022-09-28 22:01:32.030 DEBUG 76860 --- [nio-8080-exec-2] org.hibernate.SQL :
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=?
2022-09-28 22:01:32.034 INFO 76860 --- [nio-8080-exec-2] p6spy : #1664370092034 | took 4ms | statement | connection 9| url jdbc:mysql://localhost:3306/workduo?autoReconnect=true&allowPublicKeyRetrieval=true&rewriteBatchedStatements=true
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=?
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=4;
2022-09-28 22:01:32.044 DEBUG 76860 --- [nio-8080-exec-2] org.hibernate.SQL :
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=?
and membercont0_.deleted_yn=?
2022-09-28 22:01:32.048 INFO 76860 --- [nio-8080-exec-2] p6spy : #1664370092048 | took 2ms | statement | connection 9| url jdbc:mysql://localhost:3306/workduo?autoReconnect=true&allowPublicKeyRetrieval=true&rewriteBatchedStatements=true
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=? and membercont0_.deleted_yn=?
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=10 and membercont0_.member_id=2 and membercont0_.member_content_id=4 and membercont0_.deleted_yn=false;
2022-09-28 22:01:32.056 DEBUG 76860 --- [nio-8080-exec-2] org.hibernate.SQL :
delete
from
member_content_comment_like
where
member_content_comment_id in (
?
)
2022-09-28 22:01:32.063 INFO 76860 --- [nio-8080-exec-2] p6spy : #1664370092063 | took 7ms | statement | connection 9| url jdbc:mysql://localhost:3306/workduo?autoReconnect=true&allowPublicKeyRetrieval=true&rewriteBatchedStatements=true
delete from member_content_comment_like where member_content_comment_id in (?)
delete from member_content_comment_like where member_content_comment_id in (10);
2022-09-28 22:01:32.087 DEBUG 76860 --- [nio-8080-exec-2] org.hibernate.SQL :
update
member_content_comment
set
updated_at=?,
content=?,
deleted_at=?,
deleted_yn=?,
member_id=?,
member_content_id=?
where
member_content_comment_id=?
크 만족 스럽게 나간다.
이 딜리트 yn 을 업데이트 함에 따라 커멘트 가져오는 함수를 변경하였다.
@Transactional(readOnly = true)
protected MemberContentComment getMemberContentComment(Long commentId, Member m, MemberContent mc) {
MemberContentComment memberContentComment = memberContentCommentRepository.
findByIdAndMemberAndMemberContentAndDeletedYn(commentId, m, mc,false)
.orElseThrow(() -> new MemberException(MEMBER_COMMENT_DOES_NOT_EXIST));
isCommentDeleted(memberContentComment);
return memberContentComment;
}
@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;
이번에는 그럼 한방에 들고 오면 코드의 양도 줄 것으로 예상되고 그렇게 어려운 쿼리가 아니니깐 한방에 들고 오자.
//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에서도 사용할 거 같아 따로 빼주었다 삭제 체크하는 함수도 따로 빼주고 매우 맘에 든다.
지난번에 이어 이번에는 댓글 조회 기능 을 구현할 생각이다. 페이징 이 들어가야 해서 querydsl 로 구현 해야한다.
jpql 로 한다면 ㅎㅎ.... string 잘라서 이어붙이고 있을 나 를 생각해보면 끔찍하다.
지난글 pr 을 머지 하고 조회 쪽을 하자 .
지난번 pr 브런치 삭제해주고, pull master 땡겨주고 시작해보자.
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")
select
membercont0_.member_content_comment_id as col_0_0_,
member1_.member_id as col_1_0_,
member1_.username as col_2_0_,
membercont0_.content as col_3_0_,
member1_.nickname as col_4_0_,
member1_.profile_img as col_5_0_,
(select
count(membercont3_.member_content_comment_like_id)
from
member_content_comment_like membercont3_
where
membercont3_.member_content_comment_id=membercont0_.member_content_comment_id) as col_6_0_,
membercont0_.created_at as col_7_0_
from
member_content_comment membercont0_
inner join
member member1_
on membercont0_.member_id=member1_.member_id
inner join
member_content membercont2_
on membercont0_.member_content_id=membercont2_.member_content_id
where
membercont0_.member_content_id=?
and membercont0_.deleted_yn=?
group by
membercont0_.member_content_comment_id
order by
col_6_0_ desc,
membercont0_.created_at desc limit ?
크 왕쿼리 한방으로 댓글로 예쁘게 가져온다. 쿼리는 총 2방, 컨탠트 찾는 쿼리, 이렇게 조인으로 보내서 퍼올리는 쿼리
사실 이렇게 쿼리로 나오기 까지 mysql 에서 쿼리 를 엄청 셀렉트해서 보냈다. 일 대 다의 관계 에서는 데이터가 다에 맞춰서 펌핑 된다.
즉 일 은 한개의 데이터 지만 다에 맞춰 그냥 전부 중복으로 매꿔 로우로 보내준다. 매우 좋지 않다.
그렇게 되면 페이징 이 매우어렵다 왜 ? 일 로 페이징 하고 싶지만 다 에 맞춰서 줘서 오기떄문이다.