본문 바로가기

삽질

RestAssured와 기본 생성자에 관련된 삽질

시작은 MemberResponse 클래스의 기본 생성자를 없애고 싶었다.

public class MemberResponse {
    private Long id;
    private String email;
    private String name;

    public MemberResponse() {
    }

    public MemberResponse(Long id, String email, String name) {
        this.id = id;
        this.email = email;
        this.name = name;
    }
      ...
}

MemberResponse 객체에는 기본 생성자가 굳이 없어도 애플리케이션이 동작할 때 문제가 없었기 때문이다.

쓸모없는 코드를 굳이 둘 필요는 없지 않은가?

하지만 문제가 하나 있었다. Acceptance Test를 할 때, RestAssured API를 사용하는 곳이다.

RestAssured를 사용해서 다음과 같이 테스트 할 때,

return given()
            ...
            .then()
            .assertThat()
            .statusCode(HttpStatus.OK.value())
            .extract().as(MemberResponse.class);

.extract().as(MemberResponse.class) 이 부분에서 기본 생성자가 없으면 mapping이 되질 않았다.

발생하는 에러는 다음과 같다.

com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `wooteco.subway.service.member.dto.MemberResponse` (no Creators, like default constructor, exist): cannot deserialize from Object value (no delegate- or property-based Creator)
 at ...

JSON을 파싱한 결과를 전달할 적절한 생성자를 찾지 못했을 때 발생하는 에러이다.

에러를 검색해서 해결 방법은 어렵지 않게 찾을 수 있었다.

jackson 라이브러리에서 기본 생성자가 아닌, 생성자로 파싱하는 방법 중 하나는
jackson에서 제공하는 @JsonCreator를 생성자에, @JsonProperty를 생성자 파라미터에 붙이는 것이다.

public class MemberResponse {
    private Long id;
    private String email;
    private String name;

    @JsonCreator
    public MemberResponse(@JsonProperty("id") Long id, @JsonProperty("email") String email,
        @JsonProperty("name") String name) {
        this.id = id;
        this.email = email;
        this.name = name;
    }
    ...
}

그리고 이렇게 @JsonProperty로 속성명을 일일이 지정해주지 않을 수 있는 방법이
ParameterNameModule 활용하는 방법이다.

ParameterNameModule을 활용하는 방법을 간단히 설명하면

  1. build.gradle 안의 컴파일에 -parameters 라는 옵션 추가
  2. ParameterNameModule 의존성 추가 implementation 'com.fasterxml.jackson.module:jackson-module-parameter-names:2.10.3'
  3. ObjectMapper 선언에서 registerModule() 메서드로 ParameterNamesModule 을 추가

하지만 SpringBoot에서는 이 모든걸 자동으로 해준다.

Spring Boot Gradle Plugin에서 Java 컴파일의 -parameters 옵션이 자동 추가되어 있고
spring-boot-starter-web → spring-boot-starter-json → jackson-module-parameter-names 로 의존관계가 연결된다.
디폴트로 등록되는 ObjectMapper bean에는 ParameterNamesModule 이 이미 추가되어 있다.

그럼 왜 RestAssured에서는 기본 생성자가 없으면 안되고 기본 생성자 없이하려면 @JsonProperty 등으로 직접 지정해줘야만 할까?
RestAssured도 같은 jackson 라이브러리를 사용하는데 말이다.

Delegate setting을 default인 IntelliJ IDEA에서

Run tests using을 Gradle로 바꿔주면 기본 생성자 없이도 .as가 잘 동작한다.

저 설정을 바꿔주면 Gradle에서 ParameterNamesModule이 등록된 ObjectMapper를 사용해서 변환하는 것 같다.

정리하자면 다음과 같다.

1. jackson 라이브러리를 사용할 때, 기본 생성자 나 에노테이션 없이
mapping할 수 있는 방법은 ParameterNamesModule을 추가해주는 것이다.

2. SpringBoot에서는 ParameterNamesModule을 자동으로 추가해준다.

3. RestAssured에서는 SpringBoot에서 자동으로 추가된 ParameterNamesModule이 등록된 ObjectMapper를 사용하려면 Delegate setting에서 Run tests using을 Gradle로 바꿔준다.

4. Delegate setting이 default인 경우 기본 생성자가 없이 사용하려면 @JsonCreator, @JsonProperty, @ConstructorProperties 과 같은 jackson 라이브러리에서 제공하는 기능을 사용해야 한다.

RestAssured에서 jackson 라이브러리에서 제공하는 기능을 사용하지 않고
기본 생성자를 없애는 더 좋은 방법은 찾지 못했다.

지식 수준이 얕아서 틀린 내용이 많을 수도 있는 글이다.
고수님께서 댓글로 지적해주거나 더 좋은 방법을 알려주면 참 좋을 것 같다.