새로운 파트 인 댓글 부분을 작성할 생각이다. 

 

개발의 순서 에는 사람마다 의 취향 차이가 있다. 개인적으로 등록부터 하는 것을 좋아한다.

이렇게 해야 직접 테스트 해보면서 mysql에 쿼리도 날려보고 하면서 테스트하기 좋아서 이렇게 하는 걸 선호한다.

사실 제일 구현하기 간편하다. ㅎㅎㅎ 조회 가 페이징 과 querydsl 써서 해야 해서 하기 편한 등록 먼저 하고 조회를 하기로 했다 ㅎㅎ..


지난번 좋아요 와 싫어요 PR 이후로 컨탠트 쪽은 마무리되었다. 이번에는 댓글 파트 쪽을 다뤄 볼까 한다. 

Pull로 떙겨오고, 동료 분은 Group Meeting 등록 쪽을 하셨다. 아무래도 시간을 다루고 서비스 핵심 로직이기에 코드가 쫌 많다. ㅎㅎ 

 

 git checkout guiwoo
M       src/main/resources/application-dev.properties
Switched to branch 'guiwoo'

 

 

지난번과 동일하게 바로 컨트롤러 테스트 코드 먼저 작성하러 가자.

@Test
@DisplayName("멤버 피드 댓글 작성 성공")
public void contentCommentCreate() throws Exception{
    ContentCommentCreate.Request req
            = ContentCommentCreate.Request.builder().comment("test").build();

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

댓글 등록이다 보니 댓글의 정보를 가져올 리퀘스트 바디가 필요하다.

erd의 댓글 필드를 확인해보고 오자. 음? content  만 받아오면 해결된다. 해결해줄 클래스 작성해 주러 가자.

public class ContentCommentCreate {

    @Getter
    @Setter
    @AllArgsConstructor
    @NoArgsConstructor
    @Builder
    public static class Request{
        @NotNull(message = "내용은 필수 입력 사항입니다.")
        private String comment;
    }
}


요렇게 이너 클래스로 리퀘스트 지정해주자. 이번에 동료분 께서 알려주신 @NotBlank 가 가장 인텐시브 하게 체크를 한다고 한다. 

이참에 정리해보자 @NotEmpty와 @NotNull을 @NotBlank 프로젝트 진행하면서 이것저것 사용했던 거 같은데 이런 인텐시브 체킹

관련해서는 상관없이 그냥 직관적인 이름에 따라 사용해왔다.

 

바로 테스트해보자 궁금하니깐 @NotBlank 타고 올라가 보자 

The annotated element must not be null and must contain at least one non-whitespace character. Accepts CharSequence.

오 null 값이 아니고 적어도 하나 이상의 공백이 아닌 문자를 포함하고 있어야 한다고 한다. @NotNull 도 확인해보자. 

The annotated element must not be null. Accepts any type.

눌 만 아니면 어느 타입이든 받아들인다고 심플하게 적혀있다. 

테스트를 해보자.

/** 테스트 시작점 위쪽에 걸어 놓은 부분
private static Validator validator;

@BeforeAll
public static void init() {
    factory = Validation.buildDefaultValidatorFactory();
    validator = factory.getValidator();
}
*/
@Test
public void NotBlankTest() throws Exception{
    ContentCommentCreate.Request req =
            ContentCommentCreate.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);
}

와 이 테스트가 통과된다. 공백 인 배분을 모조리 체크해준다.  서비스 구현이 마무리되는 대로 NotNull 과 NotEmpty 를 섞어서 사용했는데 모두 NotBlank 로 통일해 주어야 겠다.

이제 진짜 컨트롤러 테스트 하기 위한 코드를 작성해주자.

더보기
    @PostMapping("{memberContentId}/comment")
    public ResponseEntity<?> contentComment(
            @PathVariable("memberContentId") Long contentId,
            @RequestBody @Valid ContentCommentCreate.Request req,
            BindingResult bindingResult){
        
        if(bindingResult.hasErrors()){
        throw new CustomMethodArgumentNotValidException(bindingResult);
        }
        
//        memberContentService.contentCommentCreate(req);
        return new ResponseEntity<>(
                CommonResponse.ok(),
                HttpStatus.OK
        );
    }

바디의 값과, 성공 사례 총 2가지 의 경우를 테스트해주었다. 이제 서비스로 넘어가자.

 

서비스 테스트 코드

더보기
@Nested
@DisplayName("멤버 피드 댓글 등록 테스트")
class MemberFeedContent{
    Long contentId = 169L;

    @Test
    @DisplayName("멤버 피드 댓글 등록 실패 [로그인 유저 미 일치]")
    public void doesNotMatchUser() throws Exception {
        //given
        //when
        //then
    }

    @Test
    @DisplayName("멤버 피드 댓글 등록 실패 [피드 가 존재하지 않는 경우]")
    public void doesNotExistFeed() throws Exception{
        //given
        //when
        //then
    }

    @Test
    @DisplayName("멤버 피드 댓글 등록 실패 [피드가 삭제된 경우]")
    public void feedDeleted() throws Exception{
        //given
        //when
        //then
    }

    @Test
    @DisplayName("멤버 피드 댓글 등록 성공")
    public void contentCommentCreateSuccess() throws Exception{
        //given
        //when
        //then
    }
}

1번 ~ 3번까지는 지난번 테스트 와 동일하니 코드만 첨부한다. 

1번 로그인 유저 미 일치 

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

// 서비스
@Override
public void contentCommentCreate(ContentCommentCreate.Request req, Long contentId) {
    Member m = validCheckLoggedInUser();
}

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

더보기
@Test
@DisplayName("멤버 피드 댓글 등록 실패 [피드 가 존재하지 않는 경우]")
public void doesNotExistFeed() throws Exception{
    doReturn(Optional.of(Member.builder().build()))
            .when(memberRepository).findByEmail(any());
    //when
    MemberException exception = assertThrows(MemberException.class,
            ()->memberContentService.contentCommentCreate(req,contentId));
    //then
    assertEquals(MemberErrorCode.MEMBER_CONTENT_DOES_NOT_EXIST
            ,exception.getErrorCode());
}

// servcie
@Override
public void contentCommentCreate(ContentCommentCreate.Request req, Long contentId) {
    Member m = validCheckLoggedInUser();
    MemberContent mc = getContent(contentId);
}

3번 피드 가 삭제된 경우

더보기
@Test
@DisplayName("멤버 피드 댓글 등록 실패 [피드가 삭제된 경우]")
public void feedDeleted() throws Exception{
    //given
    doReturn(Optional.of(Member.builder().build()))
            .when(memberRepository).findByEmail(any());
    doReturn(Optional.of(MemberContent.builder().deletedYn(true).build()))
            .when(memberContentRepository).findById(any());
    //when
    MemberException exception = assertThrows(MemberException.class,
            ()-> memberContentService.contentLikeCancel(12L) );
    //then
    assertEquals(MemberErrorCode.MEMBER_CONTENT_DELETED,
            exception.getErrorCode());
}

// servcie

@Override
public void contentCommentCreate(ContentCommentCreate.Request req, Long contentId) {
    Member m = validCheckLoggedInUser();
    MemberContent mc = getContent(contentId);

    isContentDeleted(mc);
}

private void isContentDeleted(MemberContent mc){
    if(mc.isDeletedYn()){
        throw new MemberException(MEMBER_CONTENT_DELETED);
    }
}

1~3번 모두 통과 완료! 이제 성공 케이스만 테스트하면 된다 바로 가보자.

지난번에 말한 대로 저장할 값을 캡처 해와 벨류 확인을 하는 작업을 추가하고

제일 좋아하는 verify times를 이용해 함수의 실행이 마지막까지 실행되는지 확인했다.

서비스 코드 작성하러 가보자.

더보기
    @Override
    public void contentCommentCreate(ContentCommentCreate.Request req, Long contentId) {
        Member m = validCheckLoggedInUser();
        MemberContent mc = getContent(contentId);

        isContentDeleted(mc);

        MemberContentComment mcc = MemberContentComment.builder()
                .member(m)
                .memberContent(mc)
                .content(req.getComment())
                .deletedYn(false)
                .build();

        memberContentCommentRepository.save(mcc);
    }

 

지난번  과제 테스트로 롬복 없이  계좌 시스템을 구현하는 경우가 있었는데 롬복의 소중함 매번 느낀다 ㅎㅎ

 

테스트는 모두 통과된다.

서버 올리고 테스트해보자

로그인해서 토큰 받아주고

헤더에 토큰 넣어주고 content-type 넣어주고

응?

게시글이 없네 🥲 4번으로 보내자 

예이 200 리스폰스 받았다. ㅎㅎ

// 멤버 찾아주고 
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
    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=?
// save 해주구
insert 
into
    member_content_comment
    (created_at, updated_at, content, deleted_at, deleted_yn, member_id, member_content_id) 
values
    (?, ?, ?, ?, ?, ?, ?)

원하는 쿼리 개수만큼 나갔다 의도한 대로 작성이 되었다.

 

마지막 확인을 위한 mysql_workBench 확인 좋다.

테스트를 하는데 이렇게 또 서버를 띄우고 이렇게 여러 번 확인해도 항상 실제 서버 배포하고 그러면 오류가 난다. 그러면 배포를 다시 하고 그래야 하는데 우리 같은 작은 구조의 서비스 면 금방 배포가 되겠지만 그게 아니라면? 끔찍하다 ㅎㅎ 이래서 대부분의 서비스는 배포용 서버와 테스트용 서버 두 개 운용을 한다고 들었는데 그래도 배포하고 이런 게 다 돈아 니겠는가? 간단히 핸드폰 게임만 해도 긴급 점검 이 빈번하게 일어나는 경우가 많다. 왜 그러겠는가 수많은 개발자들이 확인했는데도 확인하지 못한 에러가 존재하기 때문이다. 이런 전문가들도 놓치는 부분이 있는데 하물며 나라고 없겠는가?  내가 생각하는 예외가 전부라고 생각하지 말자.

이번 프로젝트 진행하면서 처음 몇 개의 api 구현하는 동안 와 완벽하다 테스트 도 다통과 하고 그랬는데 나중에 보니 다른 테이블 데이터를 넣어주지 않는 바보 같은 실수를 해서 디버그 찍으면서 왜 안 들어가지 테스트가 들어가는데 ? 서버를 띄우고 하면 왜 안들어가지 이러면서 많은 삽질을 했다. ㅎㅎㅎㅎ 

 

테스트 도 내가 작성하고 예외도 내가 생각하기 때문에, 테스트 가 통과 가 되는 것이다. 

열린 마음으로 에러를 받아들이자 스트레스 그만 받자 🧐

 

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

지난번에 이어 이번에는 피드 좋아요 부분을 작성할 예정이다. 

다양한 웹사이트에서 좋아요 토글을 테스트해본 결과, 큰 서비스 들은 대부분 좋아요 쿼리와 좋아요, 싫어요 쿼리 이렇게 두 개가 있어서 우리도 그렇게 분리하기로 했다.

노션은 추후 업데이트가 필요한 부분이다.

이 api 가 호출 되면 무엇부터 해주어야 할까?

1. like 부분의 엔티티를 생성해서 인서트 해주면 될 거 같다. 

음? 지난번보다 생각 보다 간단하다. 

예외처리를 생각해보자.

정책에 명시된 것부터 나열해보자.

1. 토큰과 로그인 유저가 일치하지 않는 경우,
2. 피드 가 존재하지 않는경우?,3. 피드가 이미 삭제된 경우 ? 

 

위 2  피드는 하나의 예외처리로 묶을 수 있을 거 같다. 

음 이미 좋아요 가 눌러진 경우라면? 좋아요 테이블 내에서 중복 여부도 체크해야 할 거 같다.

마땅히 생각나는 추가 예외적인 경우가 없는 것 같다.

우리 동료분은 그룹 내에서 좋아요 여부를 판별해야 해서 생각보다 예외처리 가 많다. 하 복 받았네 ㅋㅋ

 

바로 지난번 날려준 pr 머지해주고 컨트롤러 테스트 코드부터 작성해보자.

modified:   src/main/java/com/workduo/member/content/service/impl/MemberContentServiceImpl.java
modified:   src/main/resources/application-dev.properties
modified:   src/test/java/com/workduo/member/content/controller/MemberContentControllerTest.java

 

 

브런치 생성해주고 , 스테이터스 찍어보니 impl 이랑 Test 가 약간 수정이 되어있다. 단순한 줄 바꿈 정도이니 바로 테스트 코드 작성하러 가자.

지난번 테스트와 다를 바 없다. 네스티드를 이용해 새로운 api 테스트임을 명시해주고, 바로 컨트롤러 테스트를 위한 코드를 작성했다. 

컨트롤러 작성하러 가 보자.

 

컨트롤러 위에 저렇게 코멘트를  박아주면 타인이 읽을 때 보다 빠르게 파악하기 좋다.

물론 함수 명도 동일한 의미를 지녀야 해서 굳이 필요한가 싶지만 한국인이라 그런지 나는 한글이 가장 먼저 눈에 들어온다. ㅎㅎㅎ 

서비스 코드는 미리 이름을 정해 놓고, 주석처리했다. 안 하고 돌리면 컴파일 에러니 주석 처리를 해주자.

 

 

기분 좋게 컨트롤러 테스트가 통과된다 ㅎㅎ

예외 처리 부분은 서비스 쪽에서 테스트할 것이니 바로 서비스 테스트 코드를 작성하러 가 보자.

 

더보기
@Test
@DisplayName("멤버 피드 좋아요 실패 [로그인 유저 미 일치]")
public void doesNotMatchUser() throws Exception{
    //given
    //when
    //then
}

@Test
@DisplayName("멤버 피드 좋아요 실패 [피드가 삭제된 게시글 인 경우]")
public void feedDeleted() throws Exception{
    //given
    //when
    //then
}

@Test
@DisplayName("멤버 피드 좋아요 실패 [피드가 존재하지 않는 경우]")
public void feedDoesNotExist() throws Exception{
    //given
    //when
    //then
}

@Test
@DisplayName("멤버 피드 좋아요 실패 [이미 피드 를 좋아요 한 경우]")
public void feedLikedAlready() throws Exception{
    //given
    //when
    //then
}

@Test
@DisplayName("멤버 피드 좋아요 성공")
public void feedLikeSuccess() throws Exception{
    //given
    //when
    //then
}

총 5개 정도 테스트하면 될꺼같다 이 테스트 코드를 작성하면서 의문이 들었다.

이미 좋아요 를 한 경우를 예외처리 가 굳이 필요한가 이다.

Return으로 함수를 종료하고 200으로 리턴하는 건 어떤가?  왜냐하면 프런트에서는 좋아요  캐시 처리한다고 가정해보자.

그렇다면? 이미 화면에 업데이트하고 쿼리가 날아간다. 쿼리의 리턴 값 여부에 상관없이 미리 예상해서 업데이트를 해버린 것이다.

 

이렇게 되면  굳이 예외처리가 필요한가 부분에서 의문이 든다. 이 부분은 pr에 포함해서 날려야겠다.
일단 정책 이 저렇게 정해져 있으니 동료분 코드와 통일성 있게 예외처리를 진행 해주자.

 

1. 토큰 인증 메일과 로그인 유저 미일치

지난번 테스트 코드와 동일하다 
딱히 뭐 추가해줄 부분이 없다 서비스 코드 부분에도 이미 유저 발리 데이팅 이 프라이빗 함수로 구현되어 있으니 그냥 그거 가져다가 사용하면 된다.

@Override
public void contentLike(Long contentId) {
    Member m = validCheckLoggedInUser();
}

ㅎㅎ 특별할 것 없는 member의 이메일은 매번 jwt 인증을 태울 때마다 컨택스트 홀더에 저장해놓고 사용한다.

따라서 별다른 파라미터 없이 저렇게 멤버 검증이 가능해진다.

스무스하게 테스트가 바로 통과된다.

 

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

지난번 테스트 코드와 동일하다 

딱히 뭐 추가해줄 부분이 없다 서비스 코드 부분에도 이미 멤버 컨 탠트 가져오는  함수가  구현되어 있으니 그냥 그거 가져다가 사용하면 된다

이런 멤버나 컨 탠트 가져오는 부분은 나중에 join 쿼리로 가져오게 리팩터링을 해줘야겠다. 굳이 2개로 나눠서 보낼 필요가 없어 보인다.

 

    @Override
    public void contentLike(Long contentId) {
        Member m = validCheckLoggedInUser();
        MemberContent mc = getContent(contentId);

    }

딱히 뭐 유별난 게 없어 바로 다음 테스트 코드 작성하러 가자.

 

3. 피드 가 삭제된 경우

삭제된 경우 우리 워크 듀오 같은 경우는 피드 가 삭제되어도 테이블 상에서 지우지 않는다. 

 

유저의 복구 신청이 있다면?이라는 가정을 두고 테이블 상에서 지우지 않는다. 따라서 칼럼 하나를 따로 지정해주어 

삭제 여부를 판단한다.

 

deleteYn 저부 분만 트루가 되어서 들어간다면 정확하게 삭제된 피드 에러를 터트려야 한다.

    @Override
    public void contentLike(Long contentId) {
        Member m = validCheckLoggedInUser();
        MemberContent mc = getContent(contentId);
        if(mc.isDeletedYn()){
            throw new MemberException(MEMBER_CONTENT_DELETED);
        }
    }

ㅋㅋㅋㅋ 매우 심플하다. 코드를 작성하면서 함수를 작성한 나 매우 칭찬한다 ㅎㅎ

가볍게 3번째 테스트 케이스 또한 통과된다.

 

4. 이미 좋아요 가 된 경우

단순한 중복 여부의 체크를 위해 다양하게 쿼리를 짠다 단순히 구글 검색으로 duplicate check performance in sql로 검색만 하더라도 정말 다양한 방법으로 구현한다. find count exist 다양한 포스트를 읽다 보면 마무리 글에 강조하는 부분이 있다. exist 체크를 위해 count를 사용하지 말라는 글들이다. 궁금하면 한번 검색해서 읽어보는 것을 권장한다. 한글 영어 다양한 자료들이 있다.

 

레포지토리 함수 와, ErrorCode 작성이 필요하다 저 빨간불부터 지우자.

//Repository
boolean existsByMemberAndMemberContent(Member m,MemberContent mc);
//ErrorCode
MEMBER_CONTENT_LIKE_ALREADY(HttpStatus.BAD_REQUEST,"❌ 이미 좋아요 한 게시글 입니다."),
//Service
@Override
public void contentLike(Long contentId) {
    Member m = validCheckLoggedInUser();
    MemberContent mc = getContent(contentId);
    if(mc.isDeletedYn()){
        throw new MemberException(MEMBER_CONTENT_DELETED);
    }
    boolean exists = memberContentLikeRepository.existsByMemberAndMemberContent(m, mc);
    if(exists){
        throw new MemberException(MEMBER_CONTENT_LIKE_ALREADY);
    }
}

기분 좋게 테스트가 통과 가 된다  ㅎㅎ 자 대망의 성공 케이스를 작성하자.

 

5. 멤버 피드 좋아요 성공

음 좋아요는 마지막에 저장되는 레포지토리가 실행되는 부분만을 확인하고 싶다.

 

뭐 정확히 데이터가 들어가는지 확인하고 싶으면 캡처를 써서 메서드의 저장의 데이터가 올바르게 들어가는지 테스트 확인하는 것도

 

하나의 방법이 될 수 있으니 다음 테스트 세이브에서 활용해볼까 한다.

 

@Override
public void contentLike(Long contentId) {
    Member m = validCheckLoggedInUser();
    MemberContent mc = getContent(contentId);
    if(mc.isDeletedYn()){
        throw new MemberException(MEMBER_CONTENT_DELETED);
    }
    boolean exists = memberContentLikeRepository.existsByMemberAndMemberContent(m, mc);
    if(exists){
        throw new MemberException(MEMBER_CONTENT_LIKE_ALREADY);
    }
    memberContentLikeRepository.save(MemberContentLike.builder()
                    .member(m)
                    .memberContent(mc)
                    .build());
}

크 테스트 도 모두 통과한다.

자 서버를 띄워서 테스트해보기 전 총 나가야 하는 쿼리 수를 알아보자.

멤버를 가져오기 위한 쿼리 1방, 컨 탠트 확인 쿼리 1방, 컨탠트 라이크 쿼리 존재여부 1방, 컨탠트 라이크 세이브 1방 총 4개이다.

더보기
//멤버 확인
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
    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=?
// 라이크 존재여부 exsit 체크여서 limit 1 로나감
select
    membercont0_.member_content_like_id as col_0_0_ 
from
    member_content_like membercont0_ 
where
    membercont0_.member_id=? 
    and membercont0_.member_content_id=? limit ?
// 인서트
insert 
into
    member_content_like
    (member_id, member_content_id) 
values
    (?, ?)

서비스 부분의 코드가 살짝 더러우니 리팩터링을 진행하자.

조금 더 괜찮아진 것 같다. 멤버와 컨 탠트 그리고 검증 이후 저장 깔끔하다. 의심의 여지없이 저장하는 함수이다.


매우 만족스럽게 이번에도 구현 완료 , 내일 취소까지 마무리해서 pr 올려야겠다.

 

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

한 동안 프로젝트 진행하고 있었다. 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 한 부분을 구현했다. 다음에도 시간이 되면 추가적으로 글을 작성할 예정이다. 
긴 글 읽어주셔서 감사합니다.

싱글턴 패턴 이란 ?

Singleton is a creational design pattern that lets you ensure that a class has only one instance, while providing a global access point to this instance.

참조

코드 전체적으로 인스턴스 의 접근을 제공할때, 항상 하나의 인스턴스 만을 반환 해주는 것을 싱글턴 으로 정의한다.

 

왜 필요할까?

사실 코드를 작성해 오면서 문제가 되었던 부분이 없었다 이러한 디자인 패턴이 없어도, 그러나 나 자신도 모르게 썻던 기억이 있을 것이다. 예를 들어보자 피자를 좋아하니 피자를 구워보자.

우리 코드에는 오븐이 하나밖에 없다고 가정을 해보자. 그렇다면 우리 오븐에 쿠킹 을 하기 전 분명 전역변수를 이용하던 정적 필드 를 이용하던 어떻게든 현재 오븐상태 를 알아오고 그에 대한 검증이후에 도우 를 넣을것 아닌가 ? 비슷하다 이런 느낌을 좀더 우아하게 정의 내린것이 싱글턴 패턴이다. 필요한 이유에 대한 정의를 보자.

Ensure that a class has just a single instance. Why would anyone want to control how many instances a class has? The most common reason for this is to control access to some shared resource—for example, a database or a file.

1. 가장 일반적인 이유가 공유된 자원 의 접근을 제어하기 위해 사용된다고 한다  예 를든다면 데이터베이스 혹은 파일 에서

Provide a global access point to that instance. Remember those global variables that you (all right, me) used to store some essential objects? While they’re very handy, they’re also very unsafe since any code can potentially overwrite the contents of those variables and crash the app.

2. 매우 편리하게 전역 변수를 사용해서 공유자원 에 접근을 한다면, 어느 코드(어플리케이션 이 실행되는 데 필요한 모든 코드 들) 나 잠재적으로 이 전역변수를 덮어 사용할수 있어 앱에 문제를 야기한다고 한다.

오븐에 예로 들었던 사실중 하나가 사실은 앱에 문제를 야기할수 있다. 왜? 오븐 사용 전 에 검증 뿐만 아니라, 도우 를 만들때도, 토핑 을 얹을때도 무언가 잡다한 어떤한것을 할때도 모든 함수에 저 전역변수 에 접근하고 덮어서 사용할수 있다 라고 이해가 된다.

 

이런 잠재적인 이유를 알았으니 한번 적용해보자 도대체 어떻게 하면 될까 ? 제공 되는 예시를 내가좋아하는 피자 오븐으로 덮어서작성해보자.

코드보기

더보기
public class Testing {
    public static void main(String[] args) throws InterruptedException {
        System.out.println("피자도우 를 만들고 오븐에 넣어보자.");
        Oven singleton = Oven.getInstance(true);
        Oven anotherSingleton = Oven.getInstance(false);
        System.out.println(singleton.isUse);
        System.out.println(anotherSingleton.isUse);
    }
}

 

public class Oven {
    private static Oven instance;
    public boolean isUse;

    private Oven(boolean isUse) throws InterruptedException {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.isUse = isUse;
    }
    public static Oven getInstance(boolean isUse) throws InterruptedException {
        if(instance == null){
            instance = new Oven(isUse);
        }
        return instance;
    }
}

보이는가? 오븐을 두번 생성해서 넣어도 우리는 현재 오븐 의 상태 값을 받는다. 

자 그러면 문제가 될 상황인 서로다른 방에서 피자를 여러개 만들어서 오븐을 가져온다면 ?  어떻게 해야할까 ?  싱글턴에서는 그러면 어떻게 핸들링을 할까 ?

코드보기

더보기
package Singleton;

public class Testing {
    public static void main(String[] args) throws InterruptedException {
        Thread threadFoo = new Thread(new Hawaiian());
        Thread threadBar = new Thread(new Margherita());
        threadFoo.start();
        threadBar.start();
    }

    static class Hawaiian implements Runnable {
        @Override
        public void run() {
            Oven singleton = null;
            try {
                singleton = Oven.getInstance(true);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println(singleton.isUse);
        }
    }

    static class Margherita implements Runnable {
        @Override
        public void run() {
            Oven singleton = null;
            try {
                singleton = Oven.getInstance(false);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println(singleton.isUse);
        }
    }
}

홀리몰리 ? 원하던 결과 값이 아니다 공유된 오븐이 아닌 각자 새로운 오븐을 생성한다. 이렇게 된다면 어떻게해야 동일한 오븐을 타켓할수 있을까 ? volatile 과, 싱크로나이즈 하나 띡  인스턴스 생성하는 부분에 묶어주면 된다. 

Java volatile이란?

  • volatile keyword는 Java 변수를 Main Memory에 저장하겠다라는 것을 명시하는 것입니다.
  • 매번 변수의 값을 Read할 때마다 CPU cache에 저장된 값이 아닌 Main Memory에서 읽는 것입니다.
  • 또한 변수의 값을 Write할 때마다 Main Memory에 까지 작성하는 것입니다.

코드보기

더보기
package Singleton;

public class Oven {
    private static volatile Oven instance;
    public boolean isUse;

    private Oven(boolean isUse) throws InterruptedException {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.isUse = isUse;
    }
    public static Oven getInstance(boolean isUse) throws InterruptedException {
        synchronized(Oven.class) {
            if (instance == null) {
                instance = new Oven(isUse);
            }
            return instance;
        }
    }
}

똑같은 결과값을 반환하고 똑같은 오븐을 참조하기 시작했다. 그런데 굳이 이렇게 까지 해서 동기화를 시켜야하나 싶은 생각이 들기 시작한다. 이때 쉽게 작성하기 위해서 Jvm 을 이용하는 방법이 있다. 오븐 클래스 안에 이너클래스 로 스테틱을 생성하게 되면 Jvm 실행 시점에 클래스 로더에 올라가 jvm 이 나쁜 쓰레드로 부터 보호해준다. 바로 가보자.

코드보기

더보기
package Singleton;

public class Oven {
    public boolean isUse;
    private Oven(){}
    private static class OvenKeeper{
        private static final Oven instance = new Oven();;
    }
    public static Oven getInstance(boolean isUse){
        Oven o = OvenKeeper.instance;
        o.isUse = isUse;
        return o;
    }
}

스레드 가순서가 불안정해서 동일한 값을 매 실행마다 주진 않지만 각자 동일한 객체를 가르킨다. 오우 홀리 static 을 활용한 jvm 에 올리는 방법도 좋은거 같다. 

이 외에 눈에 띄는 방법중 하나인 enum 을 이용하는 방법이다. 

To overcome this situation with Reflection, Joshua Bloch suggests the use of Enum to implement Singleton design pattern as Java ensures that any enum value is instantiated only once in a Java program. Since Java Enum values are globally accessible, so is the singleton. The drawback is that the enum type is somewhat inflexible; for example, it does not allow lazy initialization.

이넘 또한 전역적으로 접근이 가능하고 한번만 인스턴스화 되기에 조슈아 씨는 이 방법을 추천한다고 합니다. 플랙시블 하지않아 지연 로딩을 허가하지 않는다는데 바로 가보자

코드보기

더보기
package Singleton;

public enum Oven {
    INSTANCE;
    public static boolean isUse;

    public static Oven getInstance(boolean change){
        isUse = change;
        return Oven.INSTANCE;
    }
}

와우 ... 제일 간단하면서도 위에 멀티스레드 케이스를 전부 통과한다 왜 ?

Enum은 private 생성자로 인스턴스 생성을 제어하며, 상수만 갖는 특별한 클래스이기 때문이다.

시각적으로 봐도 매우 간단하게 구현이 가능한 부분에서 보면 환상적 이다. 다만 왜 이러한 이넘 타입을 권하는지 알아보자.

일반적인 위에 구현된 싱글톤 패턴 에서는 직렬화 과정에서 싱글톤이 싱글톤이 아닌 매우 이상한 값들을 생성하고 부여한다.

따라서 일반적인 싱글톤에는 implements Serializable 추가해주고 모든 필드에 transient 를 추가해 직렬화 과정에 그대로 넘어가게 설정해 주어야 한다. readResolve() 메서드를 구현하여 현재 싱글톤의 인스턴스를 리턴해주는 이런 불필요한 과정을 거쳐야 한다.

 

Enum 에는 그런것이 전혀 필요하지 않다. 그냥 다 해준다. 또한 싱글톤 관련하여 검색하다 보면

Using Reflection to destroy Singleton Pattern 이라는 결과를 볼수 있는데 이는 싱글턴 패턴 을 파괴할수 있는 방법을 보여준다.

package com.journaldev.singleton;

import java.lang.reflect.Constructor;

public class ReflectionSingletonTest {

    public static void main(String[] args) {
        EagerInitializedSingleton instanceOne = EagerInitializedSingleton.getInstance();
        EagerInitializedSingleton instanceTwo = null;
        try {
            Constructor[] constructors = EagerInitializedSingleton.class.getDeclaredConstructors();
            for (Constructor constructor : constructors) {
                //Below code will destroy the singleton pattern
                constructor.setAccessible(true);
                instanceTwo = (EagerInitializedSingleton) constructor.newInstance();
                break;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println(instanceOne.hashCode());
        System.out.println(instanceTwo.hashCode());
    }

}

이 코드를 보면 해쉬코드 의 값이 각각 다르게 나온다. 어떻게 ? 리플렉션, setAccesible 이라는 함수를 이용하면 private 생성자 호출이 가능해진다. (반면 이 리플렉션은 하이버네이트 , 스플링에서 정말 많이 사용된다.어노테이션 에서 사용되어진다.)

그러나 enum 싱글턴에서는 이 모든 반례들을 보장해준다. 

 

만들기 제일 쉬운 Enum 이 최고의 방법이라니..

Enum 싱글턴 은 멀티쓰레드 상황, 직렬화, 리플렉션 을 이용한  싱글턴 부수기 모든 곳에서 방어가 되기 때문에 최고라고 생각한다. 무엇보다 사용하기 정말 쉽지 않은가 쉽고 직관적인게 최고다.

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


참조 사이트

디자인 패턴

 

Refactoring and Design Patterns

Hello, world! Refactoring.Guru makes it easy for you to discover everything you need to know about refactoring, design patterns, SOLID principles, and other smart programming topics. This site shows you the big picture, how all these subjects intersect, wo

refactoring.guru

싱글턴 패턴 파괴

 

Java Singleton Design Pattern Best Practices with Examples | DigitalOcean

 

www.digitalocean.com

Enum 구현

 

[JAVA] Enum을 이용해서 Singleton을 구현하자

Enum의 특징먼저 Enum의 성질에 대해 알아보자. 위의 글에서 Enum의 성질을 요약하자면, 1. 상수들만 모...

blog.naver.com

 

'자꾸 검색하는 내용' 카테고리의 다른 글

[Java] LinkedList 알아보기  (0) 2023.01.02
[Java] ArrayList 알아보기  (0) 2023.01.01
Sql Antipatterns 스키마 및 erd  (0) 2022.12.03
코드 시간측정  (0) 2022.07.25
AWS-EC2 서버 우분투에 MariaDB Open 하기  (0) 2022.06.29

나는 Bfs를 못한다. 따로 bfs의 문제의 풀이 흐름을 이해하기 위해 bfs101을 하려고 한다.

나중에 안 사실인데 title 순으로 정렬하다 보니 tree 문제만 풀었다 ㅎㅎㅎ... 

아름다운 코드들은 해당 문제 discuss 를 확인하시면 멋진 아이디어들이 다양하게 있으니 한번 찾아보자.

 

100Same Tree

풀러 가기

트리노드 두 개가 주어지고 두 개의 노드가 동일한 노드인지 를 체크하는 문제이다. 바로 루트 노드부터 bfs로 트리를 계층으로 타고 내려 갈 생각 하면서 비교를 할 생각을 하였다.

class Solution {
    public boolean isSameTree(TreeNode p, TreeNode q) {
       if(p == null && q == null) return true;
        if(p == null || q == null) return false;
        Deque<TreeNode> q1 = new ArrayDeque<>();
        Deque<TreeNode> q2 = new ArrayDeque<>();
        q1.add(p);
        q2.add(q);
        
        while(!q1.isEmpty() && !q2.isEmpty()){
            TreeNode curOne = q1.poll();
            TreeNode curTwo = q2.poll();
            
            if(curOne ==null && curTwo != null) return false;
            if(curOne != null && curTwo == null) return false;
            if(curOne.val != curTwo.val) return false;
            
            //left null check
            if(curOne.left != null){
                q1.add(curOne.left);
                if(curTwo.left == null){return false;}
            }
            if(curTwo.left != null){
                q2.add(curTwo.left);
                if(curOne.left == null){return false;}
            }
            //right null check
            if(curOne.right != null){
                q1.add(curOne.right);
                if(curTwo.right == null){return false;}
            }
            if(curTwo.right != null){
                q2.add(curTwo.right);
                if(curOne.right == null){return false;}
            }
        }
        return true;
    }
}

릿코드 답게 예상하지 못한 예외 케이스가 존재한다 ㅎㅎ, 풀이 의 과정은 2개의 2 덱을 선언해서 각각의 트리를 bfs로 순환해서 타 줄 것이다. 대신 그 중간에 null 값은 들어가지 못하게 예외처리를 분명하게 해주어야 한다. 이렇게 해도 통과되는 이유는 양쪽의 노드가 동일한 노드인지를 확인해주는 것이기 때문에 null 값은 체크 안 하고 넘겨주면 된다. if 처리가 복잡해서 그렇지 시간적 여유를 가지고 천천히 작성하면 한 번에 통과될 수준의 문제이다.

이 코드가 이해가 안 된다면 뒤에 문제들 은 전부 손볼 수가 없다. 확실히 이해하고 넘어가자. 

참고로 모든 트리 순회 문제는 dfs 풀이가 2000배 섹시하다. 내가 좋아하는 풀이법이니 첨부하자.
Dfs풀이

더보기
class Solution {
    public boolean isSameTree(TreeNode p, TreeNode q) {
        return isSameNode(p, q);
    }
    public boolean isSameNode(TreeNode p, TreeNode q){
        if(p == null && q == null){
            return true;
        }else if(p == null || q == null){
            return false;
        }
        if((p.val != q.val) || !isSameNode(p.left, q.left) || !isSameNode(p.right, q.right)){
            return false;
        }
        return true;
    }
}

101Symmetric Tree

풀러 가기

음? ㅋㅋㅋ 위의 문제가 정확히 일치하는 트리노드를 찾는 것이라면 이건 대칭되는 트리 노드를 찾는 것이다. 즉 위의 코드에서 q에 들어가 탐색할 대상이 deque 1 은 기존과 동일하게 그렇지만 deque 2는 기존과 반대로 넣어주면 해결되는 문제이다. 이해가 안 된다면 4층 레이어도 그려 본다면 느낌이 올 것이다. 풀이를 보자 위의 풀이와 다를게 크게 없다.

class Solution {
    public boolean isSymmetric(TreeNode root) {
        if(root.left == null && root.right == null) return true;
        if(root.left == null || root.right == null) return false;
        
        Deque<TreeNode> left = new ArrayDeque<>();
        Deque<TreeNode> right = new ArrayDeque<>();
        
        left.add(root.left);
        right.add(root.right);
        
        while(!left.isEmpty() && !right.isEmpty()){
            TreeNode leftPoll = left.poll();
            TreeNode rightPoll = right.poll();
            
            if(leftPoll.val != rightPoll.val) return false;
            //left check
            if(leftPoll.left != null && rightPoll.right == null) return false;
            if(leftPoll.left == null && rightPoll.right != null) return false;
            if(leftPoll.left != null && rightPoll.right != null){
                left.add(leftPoll.left);
                right.add(rightPoll.right);
            }
            //right check
            if(leftPoll.right != null && rightPoll.left == null) return false;
            if(leftPoll.right == null && rightPoll.left != null) return false;
            if(leftPoll.right != null && rightPoll.left != null){
                left.add(leftPoll.right);
                right.add(rightPoll.left);
            }
        }
        if(left.size() > 0 || right.size() > 0) return false;
        return true;
    }
}

처음 주어지는 노드의 null 체크를 진행한 후 bfs를 진행한다. 비교대상이 될 왼쪽의 왼쪽 자식, 오른쪽의 오른쪽 자식을 null 및 추가해주는 과정을 진행하고 반대로 왼쪽의 오른쪽 자식, 오른쪽의 왼쪽 자식을  비교해주면 된다. 

while의 과정을 각 덱의 사이즈 0 전까지만 실행한 이후에도 덱 안에 노드가 남아있다면 그건 비대칭 트리이다 한쪽이 더 길다는 소리이니깐..

Dfs풀이

더보기
public boolean isSymmetric(TreeNode root) {
    return root==null || isSymmetricHelp(root.left, root.right);
}

private boolean isSymmetricHelp(TreeNode left, TreeNode right){
    if(left==null || right==null)
        return left==right;
    if(left.val!=right.val)
        return false;
    return isSymmetricHelp(left.left, right.right) && isSymmetricHelp(left.right, right.left);
}

102Binary Tree Level Order Traversal

풀러 가기

오 계층별로 트리를 탐색하는 문제이다. 이 문제를 보자마자 백준의 토마토 완숙이랑 리코드 썩은 오렌지가 생각난다.

미디엄 난이도인데 위에 트리 발리데이션 체크하는 거보다 쉽게 풀었다. 각 층 별로 탐색한 후 list에 담아 준다고 생각하면 된다. 이건 말보다 코드를 보는 것이 2000 배 이해하기 쉽다.

class Solution {
    public List<List<Integer>> levelOrder(TreeNode root) {
        List<List<Integer>> result = new ArrayList<>();
        if(root == null) return result;
        Deque<TreeNode> q = new ArrayDeque<>();
        q.add(root);
        
        while(!q.isEmpty()){
            int n = q.size();
            List<Integer> attach = new ArrayList<>();
            
            for(int i=0;i<n;i++){
                TreeNode cur = q.poll();
                attach.add(cur.val);
                
                if(cur.left != null){
                    q.add(cur.left);
                }
                if(cur.right != null){
                    q.add(cur.right);
                }
            }
            result.add(attach);
        }
        return result;
    }
}

최초 정답을 배열 List를 생성하고 만약 주어지는 노드가 null이라면 그냥 비어있는 list를 반환해준다.

기본적인 틀은 위와 동일하게 현재 노드들을 담을 덱을 만들고 bfs이다

단 while 안에서는 현재 덱 안에 들어있는 사이즈만큼 돌게 된다면? 그것이 바로 현재 진행 중인 깊이이다. 따라서 이 깊이에 따라 새로운 리스트를 만들고 그 리스트를 정답에 추가해주면 되는 것이다.

Dfs풀이

더보기

 

public List<List<Integer>> levelOrder(TreeNode root) {
		List<List<Integer>> res = new ArrayList<>();
		if (root == null)
			return res;
		levelOrderHelper(res, root, 0);
		return res;
	}
	
	public void levelOrderHelper(List<List<Integer>> res, TreeNode root, int level) {
		if (root == null)
			return;
		List<Integer> curr;
		if (level >= res.size()) {
			curr = new ArrayList<>();
			curr.add(root.val);
			res.add(curr);
		} else {
			curr = res.get(level); 
			curr.add(root.val); 
			//res.add(curr); // No need to add the curr into the res, because the res.get(index) method does not remove the index element
		}
		levelOrderHelper(res, root.left, level + 1);
		levelOrderHelper(res, root.right, level + 1);
	}

 

103Binary Tree Zigzag Level Order Traversal

풀러 가기

응...? 이번에는 계층 간의 출력이긴 하지만 중간에 계속 방향을 바꾸어 주어야 한다. 홀수 층 일 때는 정방향 짝수층일 때는 역방향이니 홀수 짝수 층 구분할 불리언 변수로 주고 , 컬렉션 리버스를 이용해 정답에 계속 더해주자.

class Solution {
    public List<List<Integer>> zigzagLevelOrder(TreeNode root) {
        List<List<Integer>> result = new ArrayList<>();
        if(root == null) return result;
        
        Deque<TreeNode> q = new ArrayDeque<>();
        q.add(root);
        boolean isOdd = true;
        while(!q.isEmpty()){
            int n = q.size();
            List<Integer> attach = new ArrayList<>();
            for(int i=0;i<n;i++){
                TreeNode cur = q.poll();
                attach.add(cur.val);
                
                if(cur.left != null){
                    q.add(cur.left);
                }
                if(cur.right != null){
                    q.add(cur.right);
                }
                
            }
            
            if(isOdd){
                result.add(attach);
            }else{
                Collections.reverse(attach);
                result.add(attach);
            }
            isOdd = !isOdd;
        }
        return result;
    }
}

정확하게 위위 코드와 같고 중간에 컬렉션 리버스와 odd라는 불리언을 변경해준다.

Dfs풀이

더보기
 public List<List<Integer>> zigzagLevelOrder2(TreeNode root) {
     List<List<Integer>> ret = new ArrayList<>();
     dfs(root, 0, ret);
     return ret;
 }
 
 private void dfs(TreeNode node, int l, List<List<Integer>> ret) {
     if (node != null) {
         if (l == ret.size()) {
             List<Integer> level = new ArrayList<>();
             ret.add(level);
         }
         if (l % 2 == 1) {
            ret.get(l).add(0, node.val);  // insert at the beginning
         } else {
            ret.get(l).add(node.val);
         }
         dfs(node.left, l+1, ret);
         dfs(node.right, l+1, ret);
     }
 }

 

104Maximum Depth of Binary Tree

풀러 가기

노드의 최대 깊이를 찾아주는 문제이다. bfs를 돌려 q 안에 값이 없을 때 까지 돌린다면 최고 층을 손쉽게 찾을 수 있다.

class Solution {
    public int maxDepth(TreeNode root) {
        int result = 0;
        if(root == null) return result;
        Deque<TreeNode> q = new ArrayDeque<>();
        q.add(root);
        while(!q.isEmpty()){
            int n = q.size();
            for(int i=0;i<n;i++){
                TreeNode cur = q.poll();
                if(cur.left != null){
                    q.add(cur.left);
                }
                
                if(cur.right != null){
                    q.add(cur.right);
                }
            }
            result++;
        }
        return result;
    }
}

계층 별로 탐색하기 위해 사이즈만큼 폴을 진행하고 더해준다. 그러한 일련의 과정 중 깊이를 측정해 리턴해준다.

Dfs풀이

더보기
public int maxDepth(TreeNode root) {
        if(root==null){
            return 0;
        }
        return 1+Math.max(maxDepth(root.left),maxDepth(root.right));
    }

107Binary Tree Level Order Traversal II

풀러 가기

트리 레벨 순회인데 역순이다. 위에서 진행했던 트리 오더를 리벌스 해서 제출해주자.

class Solution {
    public List<List<Integer>> levelOrderBottom(TreeNode root) {
        List<List<Integer>> result = new ArrayList<>();
        if(root == null) return result;
        Deque<TreeNode> q = new ArrayDeque<>();
        q.add(root);
        while(!q.isEmpty()){
            int n = q.size();
            List<Integer> attach = new ArrayList<>();
            for(int i=0;i<n;i++){
                TreeNode cur = q.poll();
                attach.add(cur.val);
                if(cur.left != null) q.add(cur.left);
                if(cur.right != null) q.add(cur.right);
            }
            result.add(attach);
        }
        Collections.reverse(result);
        return result;
    }
}

이렇게 말고도 정답을 linkedList로 생성해서 result.add(0, attach); 이런 식으로 하는 사람도 있었다.  확실히 리버스 보다 링크드 리스트 앞으로 삽입 하느것이 빠르다 속도적인 측면에서의 차이는 없었으나 제출자 들 사이에서 의 속도 차이는 많이 유의미했다.

Dfs풀이

더보기
public class Solution {
        public List<List<Integer>> levelOrderBottom(TreeNode root) {
            List<List<Integer>> wrapList = new LinkedList<List<Integer>>();
            levelMaker(wrapList, root, 0);
            return wrapList;
        }
        
        public void levelMaker(List<List<Integer>> list, TreeNode root, int level) {
            if(root == null) return;
            if(level >= list.size()) {
                list.add(0, new LinkedList<Integer>());
            }
            levelMaker(list, root.left, level+1);
            levelMaker(list, root.right, level+1);
            list.get(list.size()-level-1).add(root.val);
        }
    }

 

111Minimum Depth of Binary Tree

풀러 가기

음? 이번에는 트리 깊이의 최솟값을 구하면 된다 bfs 돌다가 처음 자식들이 null 인 경우의 깊이를 반환해주면 된다.

class Solution {
    public int minDepth(TreeNode root) {
        if(root == null) return 0;
        int d = 1;
        Deque<TreeNode> q = new ArrayDeque<>();
        q.offer(root);
        while(!q.isEmpty()){
            int n = q.size();
            // for each level
            for(int i=0;i<n;i++){
                TreeNode node = q.poll();
                if(node.left == null && node.right == null){
                    return d;
                }
                if(node.left != null){
                    q.offer(node.left);
                }
                if(node.right != null){
                    q.offer(node.right);
                }
            }
            d++;
        }
        return d;
	}
}

 

 

특별할 것 없는 코드이다. 위에 설명한 대로 최초 만난 자식 없는 노드는 리프임으로 현재 깊이를 반환해준다.

이 문제는 dfs 보다는 bfs 가 더 괜찮다고 생각한다. 왼쪽에는 노드 한 개가 있지만 오른쪽은 노드가 500 층까지 있다고 생각하면 bfs의 경우 왼쪽 노드 한 개를 탐 새하고 리턴 하지만, dfs의 경우 반대쪾 500 개 모두를 탐색해야 한다. 그래도 dfs 코드는 섹시하니깐 첨부한다.

Dfs풀이

더보기
public class Solution {
    public int minDepth(TreeNode root) {
        if(root == null) return 0;
        int left = minDepth(root.left);
        int right = minDepth(root.right);
        return (left == 0 || right == 0) ? left + right + 1: Math.min(left,right) + 1;
       
    }
}

112. Path Sum

풀러 가기

이 문제는 트리 가 주어지고 타깃 값이 주어지면 현재 시작점부터 리프까지 그중에 타겟값이 존재하냐는 문제이다. 처음 - 값의 제한상을 보지 못해서 트리의 bfs를 진행함과 동시에 가중치를 넘겨주었다 이 가중치가 타깃보다 크다면 q에 추가를 하지를 않았는데 제한사항을 보니 - 값이 있기에 위의 최적화 부분을 제외해 주었다.

class Solution {
    public boolean hasPathSum(TreeNode root, int targetSum) {
        if(root == null) return false;
        Deque<TreeNode> q = new ArrayDeque<>();
        q.add(root);
        while(!q.isEmpty()){
            TreeNode cur = q.poll();
            if(cur.val == targetSum && cur.left ==null && cur.right ==null) return true;
            if(cur.left != null ){
                cur.left.val += cur.val;
                q.add(cur.left);
            }
            if(cur.right != null){
                cur.right.val += cur.val;
                q.add(cur.right);
            }
        }
        return false;
    }
}

val와 같은 값임과 동시에 리프 여야 하는 조건이 있기에 리턴 값을 위와 같이 지정해 주었다.

Dfs풀이

더보기
public boolean hasPathSum(TreeNode root, int sum) {
    // recursion method
    if (root == null) return false;
    if (root.left == null && root.right == null && root.val == sum) return true;
    return hasPathSum(root.left, sum - root.val) || hasPathSum(root.right, sum - root.val);
}

트리라고 써진 건 다 푼 줄 알았는데 밑에 문제가 더 있다... 101 2탄을 조만간 날 잡고 진행할 생각이다.. 

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

1965. Employees With Missing Information

이번 문제는 많이 새로웠다,  두 테이블 데이터 사이에 비어있는 아이디를 출력해야한다. 난해하다... 셀렉트 따로따로 하라그러면 하겠는데 두가지 문제의 2조건을 모두 충족할려면 셀렉트를 두번해야하지 않겠는가 ....머리가 터질듯이 아파왔다.. ㅠ
아래 관련토픽 에서 유니온 이라는 태그를 보고 검색했다.
'union'
두개이상 의 SELECT 결과를 합칠수 있습니다.  합친 결과에서 중복되는 행은 하나만 표시합니다.

단, 컬럼의 개수가 같아야하고, 각 컬럼의 데이터타입이 같아야합니다.

테이블 수십개도 유니온으로 가능한가 ? 가능하다.ㅋㅋㅋ 칼럼의 개수와 데이터 타입만 일치한다면 뭐 마음껏 써도 된다.

단 중복된 데이터를 원한다면 union all 이라는 구조를 이용해서 작성해 주어야 한다.

union all 에 대한 예제를 검색하다 재미난 문구를 발견했다.

만약 테이블 안에서 유니크 한 값을 반환한다면 union all 을 이용하는게 더 빠른 결과를 반환한다는 내용이다.
우리의 경우 각각 의 테이블에서 이름 과 급여를 놓친 부분을 반환을 하는데 있어 union all 이 될까 싶었지만 된다.
뭐 안되더라도 위에 문구 처럼 결과에 DISTINCT 를 넣어주면 되지 않은가 싶다.

select e.employee_id as employee_id from Employees e
left join Salaries s
on s.employee_id = e.employee_id
where s.employee_id is null
union all
select s.employee_id as employee_id from Employees e
right join Salaries s
on s.employee_id = e.employee_id
where e.employee_id is null
order by employee_id;

정말 단순하다.. select 두개 사이에 유니온 써주면 된다.. 호올리... 단순하게 설명하자면
left join 을 이용해 데이터를 한군데 전부 몰아주자. 여기서 join 으로 구사한다면 원하는 데이터를 얻지못한다. 양쪽 모두에 속해 있는 결과만 리턴받기 때문에.. left join 을 써주어야한다.

뭐 이것만 안다면 밑에 셀렉트 문도 크게 다를바 없다. 

추가적으로 union 이나 union all 이나 그렇게 큰 속도차이는 없었다. 각각 990ms 900ms 정도 만약 릿코드에 나온 데이터들 보다 큰 데이터를 다루게 된다면 이러한 조그마한 속도의 디테일 이 나중에 더 큰 나비효과 를 불러 일으킬수 있다고 생각한다. 기억해두자 union all 생각보다 많이 쓸것같다.

 

1795. Rearrange Products Table

보고나서 바로 유니온 떠올랐다....

각 가게 별로 찾아서 값을 넣어주면 되는게 아닌가 ?
왜냐하면 이미 가게의 종류가 칼럼안에 정의가 되어있기 때문에 이렇게 생각했다. 
위에서 유니온 테이블 2개이상 가능하다고 했으니 바로 해보자 

select product_id,"store1" as store, store1 as price
from products
where store1 is not null
union all
select product_id,"store2" as store, store2 as price
from products
where store2 is not null
union all
select product_id,"store3" as store, store3 as price
from products
where store3 is not null;

output 출력에 맞게 select 설정해주고 각 스토어 별로 눌이 아닌경우를 찾아서 뽑아주면 된다. ㅋㅋㅋ 위에서 하고 오니 뭔가좀더 쉽다. 

 

608. Tree Node

음... 트리를 알고리즘 에서 만 보던게 여기서 보니 새롭지만 문제 넘기고싶다. 전혀 읽고 싶지가 않다...

첫번쨰 미디움 난이도의 문제 이다. 

id 는 본인 자신의 숫자를 의미하고, p_id 는 자신보다 상위 노드의 트리 아이디를 가르킨다. 항상 트리의 형태가 가능한 숫자들만 주어진다고 한다. 

여기서 root 와 inner leaf 를 구분해서 작성해 주어야 한다. 트리가 나와서 그렇지 맨위 root 중간 inner leaf 꼬리 로 분류해서 작성해주면 별볼일 없는 문제다. 여기서 inner 의 체크가 중요한데 이거는 p_id 의 대상이 2개 라면 그 p_id 본인은 inner 가된다 위의 두경우를 제외 한다면 나머지는 leaf 때려주면 된다.셀렉트 3개 유니온 갈겨주자. 물론 각 노드들은 유니크한 값들이니 위에서 배운대로 union all 을하자.

 

약 4번 제출하고 통과한 코드이다. 일련의 과정을 적을 예정이니 답이 궁금하면 스크롤 내려서 확인바란다.

 제출 1번

# Write your MySQL query statement below
select id,"Root" as type from tree
where p_id is null
union all
select id,"Inner" as type from tree
where id in (select p_id from tree where p_id != 1)
union all
select id,"Leaf" as type from tree
where id not in (select p_id from tree where p_id is not null);

null 인 아이디 라면 ? 그게 root 이다 부모가 없으니깐(???), inner 의 경우는 ? 1 이 부모가 아닌 경우 를 서브쿼리로 뽑아 in 으로 확인후 inner 로 퍼올린다, 마지막 inner 와 동일 조건으로 not in 을 이용해 주었다.


실패했다... ㅋㅋㅋ 

16/19 에서 걸린다. {"headers": {"Tree": ["id", "p_id"]}, "rows": {"Tree": [[1,null]]}} 이경우 나는 leaft 1 이 다시 들어간다 왜 ?

id 에는 null 을 포함한 값들을 가지고 대조하는데 내가 퍼올린건 null 이 아닌 값들이 올라온다.  그래서 leaf 1 도 들어간다 and 연산으로 이어붙여주자.

# 수정부분
select id,"Leaf" as type from tree
where id not in (select p_id from tree where p_id is not null) and p_id is not null;

아씨 17/19 에서 걸린다 왜? root가 1이 아닌 경우가 있다..... 문제 어디에서도 1 이 항상 루트인 경우라는 말은 없었다.... 다시 고쳐주자

#수정부분
select id,"Inner" as type from tree
where id in (select p_id from tree where p_id is not null)

응 ? 이러고 제출하니 1번이 틀린다 왜 ? 1번 제출과 동일한 이유다 null 이아닌값을 퍼올리고 null 값을 들고 대조하니 당연히 중복이지..

#코드최종
select id,"Root" as type from tree
where p_id is null
union
select id,"Inner" as type from tree
where id in (select p_id from tree where p_id is not null) and p_id is not null
union
select id,"Leaf" as type from tree
where id not in (select p_id from tree where p_id is not null) and p_id is not null
order by id;

아 union all 을 이용해서 작성해도 통과된다. 위에설명한것과 같이 각 select 문은 유니크한 값들 만 리턴한다.

아유... 드디어 통과 됬다... 멀리도 돌아왔다.... 이건 디스커스 를 안볼수가 없다 보러가보자.

SELECT id,
CASE
    WHEN p_id is NULL THEN "Root"
    WHEN id NOT IN (SELECT p_id FROM Tree WHERE p_id IS NOT NULL) THEN "Leaf"
    ELSE "Inner" 
END AS type 
FROM Tree

하 씨 ? case 와 if 문을 사용할수 있는건 알고 있었다. 그런데 여기에 이렇게 활용할 생각을 1도 생각 못했다. 와.....

이사람은 뒤에 and is not null 을 또 안써도 되는이유는 for 문처럼 하나씩 들어오는데 root 의 경우 이미 위에서 걸러지기 떄문에.. 하 씨 와 ..... 멋진 코드다 내 union  코드보다 3000배 간결하고 가독성도 좋다..

오늘 은 할당량이 좀 많다. 4문제다 .. 마지막 문제 보고 가자.

 

문제는 진짜 간단하다. 2번째 높은 급여를 반환 하는 문제이다. 자바 Pq 만들어서 쓱삭 할텐데 아쉽다. 그런데 왼걸 정말 난해했다 구현하는데 있어 제출 횟수가 보이는가.... 고생 많이한 문제다..

 

어떻게 해야 두번째 높은 값을 뽑을 수 있는가 ? 느낌이 1도 안와서 스터디원(큐빈) 에게 물어 봤다. limit 과 offset 을 이용해서 짜르면 어떻냐는 제안을 받았고 바로 제출.

select 
	salary as "SecondHighestSalary"
from employee
order by salary desc
limit 1 offset 1;

음 ? null 처리 가 안되어있다. if 문으로 처리해주자.

select 
	if(salary = null ,null,salary) as "SecondHighestSalary"
#중략----

아니 ? null 값이 반영이 안된다.  null 이 되어서 아예 반영이 안되는듯 싶었다.

case 를 이용해보자..

select 
	case
        when salary is not null then salary
        else (null)
    end as SecondHighestSalary
# 중략 ----

하씨 ? null 이 반영이 안된다.  mysql 로컬 서버를 키고 이것저것 실험 을 했다. null 자체 스트링으로 반환을 하니 응 ? 모든 반환값들이 문자열로 변했다. cast 를 이용해 형변환을 salary 에 int 로 했지만 안된다. case 를 이용하면 오로지 하나의 데이터 타입 만을 반환하는것 같았다. 

select 
	case
        when count(*)<2 then null
        else cast(min(salary) as decimal)
    end as SecondHighestSalary
from (
    select distinct salary 
    from employee 
    order by salary 
    Desc Limit 2
) a

이건 통과된 코드이다. 내코드와의 차이 점이라면 from 에서 퍼올린 데이터 기준으로 셀렉트 값이 나간다. 어떤 차이가 있는것인가.. 

select 
	salary
from employee
order by salary desc
limit 1 offset 1;

이렇게 특별하게 지정이 된다면 빈값을 반환한다. 이게 문제다 ㅋㅋ ..... limit1 offset1 아무데이터도 없다면 아무것도 반환하지 않는다.. null 조차... 에초에 전제 자체가 잘못된것이다.

select case 
when count(Salary) > 1 then (select distinct Salary from Employee
    order by Salary DESC limit 1, 1) 
else NULL end
from Employee;

내가 구현하고자 했던 코드가 이런 코드 였는데 많이 돌아간것 같다.

 

max 를 이용한 간단한 풀이를 보자.

Select MAX(salary) as SecondHighestSalary 
from Employee 
where salary < (select MAX(salary) from Employee);

max 를 이용하면 null 처리도 간단하게 할수 있다. max 값이 없다면 null 을 리턴해주기 때문에 이런 방식의 코드가 가능하다.

max 를 두번이용해서 2번째 값을 찾아주는것이 참 생소했다. ㅎㅎ......

 

와 오늘 sql 은 풀이도 오래걸리고 블로그 작성도 오래걸렸다...

확실히 모르는 부분은 디스커스 를 보면서 내꺼화 시켜야 하는데 그게 참 어려운 부분이다.

+ Recent posts