본문 바로가기
Side Project/Socket

List<String> 타입의 경우 Nullable 할 때 주의할 점.

by asdft 2024. 2. 5.

본격적으로 들어가기전, 아래의 개념을 숙지하자

null

"어떠한 값으로도 초기화 되지 않은 상태"

  • 아무것도 없는 것을 나타내는 자바의 Keyword
  • 참조형 타입의 기본값

null 은 참조형 타입의 기본값이다.(초기값)
null 은 참조형 타입에서만 사용할 수 있다. 기본형 타입의 변수에 할당하게 될 경우 컴파일 오류가 발생한다.

참조변수가 지역변수로 선언된 경우 선언과 동시에 초기화 되어야 하기때문에 , 따로 초기화를 해주지 않는다면 기본값인 null 값으로 초기화 한다.

null 과 "" 빈값의 차이해

null 은 선언만 되어있고 초기화가 되어있지 않은 상태이다. 그래서 힙 메모리상 데이터가 존재하지 않는다.
하지만 ""은 빈값이라는 데이터로 초기화가 되어있는 상태이다. 힙메모리상 빈값이 들어가 있을 것이고 스택 메모리에 빈값이 들어있는 메모리 주소가 들어 있을 것이다.

null 을 참조할 경우

우리는 객체를 참조할 때, 스택에 있는 지역변수(참조변수) 에 값으로 있는 메모리주소를 통해 객체를 참조한다.
하지만 참조변수인 지역변수에 null 값이 들어가있고 참조변수를 참조 하려고 할 경우 , 우리는 NullPointerException을 마주치게 된다.


개요

프로젝트를 진행하던 도중, RequstDto로 Null일 수도 아닐수도 있는(Nullable) 한 List<String> skillNames를 받는데, 

이 skillNames 안에 값이 있는지 없는지 boolean타입으로 반환하는 isEmptyTag( ) 메소드를 아래와 같이 내가 짜놓은게

눈에 띄었다. 

 

물론, skillNames가 Null이 불가능한 제약조건을 걸었다면 아래와 같은 코드가 아무런 문제가 없었다.

 

하지만, 내가 만들고 있는 API에서는 skillNames가 Nullable  해야 했기에,

Postman에서 API 동작 테스트시 null을 넣었을 때, NullPointerException이 터졌다.

 @Override
  @Transactional
  public Post createPost(PostSaveCommand postSaveInfo) {
    User user = findUser(postSaveInfo.userId());

    Post postToSave = savePost(postSaveInfo.toEntity(user));

    //태그가 있을 경우 실행
    if (!isEmptyTag(postSaveInfo)) {
      mapPostWithSkill(postToSave, postSaveInfo.skillNames());
    }
    return postToSave;
  }
private boolean isEmptyTag(PostSaveCommand postSaveInfo) {
    return postSaveInfo.skillNames().isEmpty();
  }

 

 

 

하지만, 아래 코드 Controller의 단위테스트 수행 시,skillNames가 null일때의 테스트는 성공했다. 

가만 생각해보니 당연한 결과 였다. 그 이유는 맨 밑에 가서 설명하겠다.

 

@ParameterizedTest(name = "skillNames가_{0}이면_201_응답을_한다")
  @NullAndEmptySource
  @WithMockUser(username = "1", authorities = "ROLE_USER")
  void PostSaveRequestDto_skillNames_is_valid_with_Null_and_Empty(List<String> skillNames)
      throws Exception {
    PostSaveRequestDto requestBody = new PostSaveRequestDto("title", "content", PostType.PROJECT,
        PostMeeting.ONLINE, skillNames);

    when(postSaveUseCase.createPost(any())).thenReturn(Post.builder().id(1L).build());

    mockMvc.perform(post("/posts")
            .header("Authorization", "Bearer access-token")
            .contentType(APPLICATION_JSON)
            .content(objectMapper.writeValueAsBytes(requestBody)))
        .andExpectAll(
            status().isCreated(),
            header().string("Location", containsString("posts/1"))
        );
  }


 

당연한 결과였다. skillNames에 null값을 넣고 이를 참조해서 isEmpty( )인지를 알려고 한게 잘못이었다.

 

그래서 이를 방지하기 위한 방법 2가지를 쓰려고 한다.

 

Case 1. testList != null && !testList.isEmpty()

 

Null이 아니고 isEmpty( )가 아니라는 두가지 조건을 and로 묶어서 boolean 타입으로 반환한다.

 

Case 2. ObjectUtils.isEmpty( )

 

ObjectUtils 클래스의 isEmpty( ) 를 호출하면 위 Null & isEmpty 소스를 하나의 메소드 호출로 해결할 수 있다.

ObjectUtils.isEmpty( ) 메소드 코드를 보자.

ObjectUtils.isEmpty()

위 코드에서 볼 수 있듯이, obj == null 일때랑 isEmpty( )일 경우 둘다 수행하는것을 확인 할 수 있다.

 

 

나는 개인적으로 Case 2의 방법이 더 짧고 간단해서 자주 사용할 거 같다.

그래서 위의 코드를 아래처럼 수정해 주니 API 기능이 정상 작동했다!

사소하지만, Nullable한 List<String> 타입의 데이터를 받을때, 주의해야 겠다.

 

private boolean isEmptyTag(PostSaveCommand postSaveInfo) {
    return ObjectUtils.isEmpty(postSaveInfo.skillNames());
  }

 


여담으로 Controller의 테스트가 성공한 이유에 대해서 말하자면,

NullPointerException이 뜨게 된 과정을 먼저 살펴봐야 한다.

 

1. 유저한테 PostSaveRequestDto를 통해 입력값을 받는다.

2. Dto에서 Null 값인 skillNames List를 가져와 수정 전 버젼인 초반에 작성했던 isEmpty( ) 메소드를 실행

3. 참조변수인 지역변수에 null 값이 들어가있고 참조변수를 참조 하려고 한 결과, NullPointerException 발생.

4. Exception이 발생해 Controller 단에서 postSaveUseCase.createPost이 실패하고, 그결과 build 또한 실패.

 

 

하지만 Controller의 단위테스트에서는 아래와 같이, Stubbing을 통해 postSaveUseCase.createPost( )를 수행하면,

무조건 Post 엔티티를 반환하도록 되어있다.

    when(postSaveUseCase.createPost(any())).thenReturn(Post.builder().id(1L).build());

 

따라서 build에 실패하지 않고 정상적으로 Exception 없이 PostSaveRequestDto의 skillNames에 Null이 들어가도 빌드에 성공 했던것이다.

 

 

고민해결!!!!!!!!!

 

 

 

[참고]

https://velog.io/@john7645/isEmpty-%EC%99%80-null

 

velog

 

velog.io

https://devfunny.tistory.com/360