• 카테고리

    질문 & 답변
  • 세부 분야

    백엔드

  • 해결 여부

    해결됨

컨트롤러 테스트시 size 없이 전달할때 npe

22.08.03 14:29 작성 조회수 482

3

안녕하세요 호돌맨님!
 
이번 영상을 다 실습하고 컨트롤러 테스트시에 size값을 주지 않는 경우를 테스트 해봤더니 npe가 발생하더라구요
 
 
 
 
 
오류는 PostSearch 클래스에 @NoArgsConstructor @AllArgsConstructor 두 어노테이션을 붙여서 해결했는데요
 
정확한 오류의 원인이 무엇인지 궁금해서 질문드립니다
 
서비스 테스트에서는 @Builder.Default 어노테이션을 사용한 초기값 설정이 잘 됐습니다
 
컨트롤러 테스트에서만 이 오류가 발생하는 원인이 궁금합니다
 
서비스 테스트에서는 postSearch 객체 생성 후 postService.getList() 메서드에 전달했는데요
빌더로 객체 생성시 클래스내에 생성자가 없었으니 롬복이 @Builder 어노테이션이 붙은 클래스에 암묵적으로 @AllArgsConstructor를 만들고 필드 값을 주입하는 시점에 초기화가 된건가요? 이유가 궁금합니다!

답변 3

·

답변을 작성해보세요.

4

안녕하세요 호돌맨님!!

장문의 답변 감사드립니다 ㅠㅠ

처음엔 기본 생성자만 생성하려고 @NoArgsConstructor 를 사용했었는데요

기본 생성자가 있으니 롬복이 자동으로 모든 필드를 받는 생성자를 만들지 않고 빌더는 모든 필드를 받는 생성자가 필요하다고 컴파일 에러가 나더라구요..

그래서 모든 필드를 받는 생성자를 만들어줘야겠다 싶어서 간편하게 @AllArgsConstructor를 사용했는데 안티패턴이었군요 ㅎㅎ..;;

호돌맨님의 정성스러운 답변을 보고도 제 지식이 부족해 아직 충분히 이해가 가지 않아 다시 질문드립니다 ㅠㅠ

두번째 질문에서 @Builder.Default 에 관해서 여쭤본 계기는 아래와 같습니다

처음 질문 후 여러 가지 시도해봤었는데요

위와 같이 빌더를 제외한 나머지 기본 생성자, 전체필드를 받는 생성자,겟터,셋터를 모두 직접 만들어서 테스트 케이스를 돌려보니 똑같이 NPE가 나오더라구요

(기본생성자는 컨트롤러에서 ModelAttribute 어노테이션이 프로퍼티로 접근할때 필요하다고 생각하여 추가했었습니다.. setter도 타지 않는것을 확인했습니다)

이 방법을 시도 후 전 @NoArgsConsturctor, @AllArgsConstructor 두 어노테이션에서 만들어주는 생성자를 직접 만들었는데도 왜 에러가 나는지 궁금하더라구요

그래서 컴파일된 클래스파일을 열어봤습니다

1.직접 생성자를 생성, 테스트 후 디컴파일된 소스입니다 

 

2.두 어노테이션을 사용해 테스트 후 디컴파일한 소스입니다

위와 같이 생성자에서 $default$size() 라는 메서드를 호출 해 기본값을 셋팅하는걸 발견해서 @Builder.Default 에 대해 질문을 드렸습니다 ㅠㅠ

저의 경우 호돌맨님이 말씀하신 것 처럼 기본생성자를 생성해서 테스트 케이스를 돌려봐도 똑같이 NPE가 발생해서 의문이 생겼었는데.. 아마 제 테스트에서 잘못된 부분이 있었나봅니다.. ㅠㅠ 알려주시면 또 테스트 해보겠습니다!

늦은 시간에 정성스러운 답변 감사합니다!

롬복 @Builder.Default를 넣으면 컴파일시에 기본 값 page=1, size=10이 제거 되네요. ㅠㅠ;

- 소스

@Builder
public class PostSearch {

private static final int MAX_SIZE = 2000;

@Builder.Default
private Integer page = 1;

@Builder.Default
private Integer size = 10;

public long getOffset() {
return (long) (max(1, page) - 1) * min(size, MAX_SIZE);
}

public PostSearch() {
System.out.println("good");
}

public PostSearch(Integer page, Integer size) {
this.page = page;
this.size = size;
}

public Integer getPage() {
return page;
}

public Integer getSize() {
return size;
}

public void setPage(Integer page) {
this.page = page;
}

public void setSize(Integer size) {
this.size = size;
}
}

 

- @Builder.Default 제거 후

public class PostSearch {
private static final int MAX_SIZE = 2000;
private Integer page = 1;
private Integer size = 10;

public long getOffset() {
return (long)(Math.max(1, this.page) - 1) * (long)Math.min(this.size, 2000);
}

public PostSearch() {
System.out.println("good");
}

public PostSearch(Integer page, Integer size) {
this.page = page;
this.size = size;
}

public Integer getPage() {
return this.page;
}

public Integer getSize() {
return this.size;
}

public void setPage(Integer page) {
this.page = page;
}

public void setSize(Integer size) {
this.size = size;
}

public static PostSearch.PostSearchBuilder builder() {
return new PostSearch.PostSearchBuilder();
}
}

 

- @Builder.Default 추가

public class PostSearch {
private static final int MAX_SIZE = 2000;
private Integer page = 1;
private Integer size = 10;

public long getOffset() {
return (long)(Math.max(1, this.page) - 1) * (long)Math.min(this.size, 2000);
}

public PostSearch() {
System.out.println("good");
}

public PostSearch(Integer page, Integer size) {
this.page = page;
this.size = size;
}

public Integer getPage() {
return this.page;
}

public Integer getSize() {
return this.size;
}

public void setPage(Integer page) {
this.page = page;
}

public void setSize(Integer size) {
this.size = size;
}

public static PostSearch.PostSearchBuilder builder() {
return new PostSearch.PostSearchBuilder();
}
}

그런데 @NoArgsConstructor, @AllArgsConstructor를 달면 기본 생성자에 default 값을 넣어줘서 NPE가 발생하지 않네요. 저도 처음알았습니다.

정리를해보면

@Builder
@AllArgsConstructor
@NoArgsConsturctor 를 달고 컴파일 된 값을 확인하면

 public PostSearch() {

        this.page = $default$page();

        this.size = $default$size();

    }

위와 같이 기본 생성자에 defaultValue를 넣어줍니다.

 

그런데

@AllArgsConstructor

@NoArgsConsturctor 를 제거하고


public class PostSearch {

    public PostSearch() {

    }

    public PostSearch(Integer page, Integer size) {

        this.page = page;

        this.size = size;

    }

위와같이 직접 생성자를 생성하면 defaultValue가 들어가지 않습니다.

3

안녕하세요 호돌맨님!!

답변을 몇개씩이나.. 자세하게 달아주셨는데 이제 봤습니다 ㅠㅠ ..

답변 달아주신 내용과 github 링크를 보니 궁금했던 부분이 좀 해소가 됐습니다!

이걸 해결하려고 이것 저것 찾아보고 호돌맨님의 의견을 들으면서 관련 키워드로 검색도 해보고 했더니 부족한 것도 많이 느끼고 공부가 많이 됐어요.. 감사드립니다 ㅎㅎ

이제 남은 인강도 마저 달려야겠어요 ㅎㅎ

항상 자세한 답변 감사드립니다!!!

1

안녕하세요. 호돌맨입니다.
질문을 남겨주셔서 감사합니다.

비슷한 질문이 올라왔어서 링크를 드립니다. https://www.inflearn.com/questions/599184
어떻게 하면 좋을지 한 번 연구해보시고 댓글 달아주세용.
잘 모르시겠으면 제가 알려드리겠습니다.

안녕하세요 호돌맨님 !

저는 ModelAttribute에서 값을 주입하지 못해 npe가 발생한다고 생각해 본문처럼 PostSearch 클래스에

@NoArgsConstructor

@AllArgsConstructor

두 어노테이션을 붙여서 해결했었는데요

이 방법이 아닌걸까요? ㅠㅠ

컴파일된 PostSearch를 열어보니까 기본생성자에서 기본값 셋팅을 하고 있더라구요

기본 생성자에서 왜 @Builder.Default 이 지정된 기본값으로 셋팅 하는지 잘 모르겠습니다 ㅎㅎ;;

(@Builder.Default는 헷갈리기만 하고 이 설명에서는 의미가 없어서 제외합니다.)

1. page값이 null로 들어가는 이유

그림 1-1

 

그림 1-2

그림 1-1처럼 @Builder를 작성하면 모든값-생성자(=@AllArgsconstructor)가 만들어집니다. 그리고 get parameter는 해당 생성자를 통해 값이 주입됩니다. 그런데 page값을 보내지 않으면 그림 1-2처럼 null이 할당되고 에러가 발생하겠죠. 그러므로 page 기본 값 1로 지정 했어도 null로 바뀌게 됩니다.

 

자, 이번에는 문제의 @NoArgsConstructor와 @AllArgsConstructor를 같이 입력했습니다. 사실 @AllArgsConsturctor는 빌더를 통해 만들어지죠? 의미가 없습니다. 이번 설명에는 @NoArgsConsturctor가 붙은게 중요합니다. 이렇게 되면 @Setter(public void setPage(..))를 통해 값이 주입됩니다. 어라?! 그런데 우리는 page값을 넘기지 않았습니다. setPage() 가 호출되지 않습니다. 기본 값으로 넣어둔 1으로 계속 남게됩니다.
그러므로 @NoArgsConstructor와 @AllArgsConstructor를 같이 써서 해결 됐다라고 할 수 없습니다. @NoArgsConstructor 덕분에 PostSearch의 인스턴스가 생성 되었지만 setPage를 통해 값이 주입되지 않았기 때문에 테스트가 성공한겁니다. (setter, 생성자 주입 우선순위 검색!)

그렇다면 어떻게 코드를 고치면 좋을까요? 우선 @AllArgsConstructor는 안티 패턴으로 사용하지 않는것이 좋습니다.

1. 그냥 @NoArgsConsturctor를 사용는 것도 방법입니다. (사실 개인적으로는 @NoArgsConsturctor도 선호하지는 않음)

2. 생성자에서 null 체크를 하는것도 방법입니다.

3. page, size의 타입 Integer를 int로 바꿔보는것도 방법도 있습니다. 그리고 나서 page를 빼고 보내면 어떤 일이 발생할까요?