오늘은 Web immutable Parameter Object에 대해서 알아보도록 하자.
요즘에는 immutable Object를 많이 사용하는 듯 하다. 아마도 가장 좋은점은 스레드 세이프하다는 장점이 있어야 일 것이다.
그래서 오늘 Spring web과 관련해서 immutable 한 Parameter에 대해서 알아보도록 하자.

요즘은 코틀린으로 Spring 개발을 많이 하고 있고 Spring 에서도 코틀린을 거의 완벽히 지원해주고 있다.
또한 java에서는 lombok도 많이 사용하고 있으니 괜찮다면 한번 살펴보는 것도 나쁘지 않다.

@ModelAttribute

Spring5 부터는 @ModelAttribute도 불변의 Object도 사용가능하다. 아마도 코틀린을 지원하면서 고려가 많이 된 것 같다.

@RestController
public class PersonController {

    @PostMapping("/")
    Person person(@ModelAttribute Person person) {
        return person;
    }
}


public class Person {
    private final String name;
    private final String email;

    public Person(String name, String email) {
        this.name = name;
        this.email = email;
    }

    public String getName() {
        return name;
    }

    public String getEmail() {
        return email;
    }

}

만약 위의 코드처럼 작성하고 해당 컨트롤러를 호출해보자.

http POST :8080 name==wonwoo email==wonwoo@test.com

만약 버전이 Spring5 이전버전이라면 아래와 같이 에러가 발생할 것이다.

java.lang.NoSuchMethodException: xxx.xxxxx.xxxxxxx.Person.<init>()
 ...
 ...

기본 생성자가 없다는 뜻으로 Spring5 이전버전에서는 무조건 default 생성자가 존재했어야 했다. 하지만 spring5 부터는 기본생성자 없이도 에러가 발생하지 않는다.
Spring5에서 테스트를 해보자.

http POST :8080 name==wonwoo email==wonwoo@test.com
HTTP/1.1 200
Content-Type: application/json;charset=UTF-8
Date: Sun, 27 Jan 2019 10:58:49 GMT
Transfer-Encoding: chunked

{
    "email": "wonwoo@test.com",
    "name": "wonwoo"
}

그럼 에러가 발생하지 않고 우리가 원하던 데이터가 출력이 된다. 아주 괜찮다. lombok을 대부분 많이, 잘 사용하니 lombok을 사용한다면 좀 더 코드가 간결해 질 수 있다.

@Value
public class Person {
    String name;
    String email;
}

lombok 의 @Value
lombok @Value가 하는 역할은 위의 링크를 참고 하자.

만약 해당 모델의 파라미터명과 전달하는 파라미터가 다르다면 어떻게 할까? 이것 역시 Spring 에서 지원해주고 있다.
@ConstructorProperties 어노테이션을 통해 해당 파라미터명을 변경할 수 있다. @ConstructorProperties 어노테이션은 Spring이 제공해주는 것 아니지만 지원은 해주는 java bean 스펙이다.

어쨌든 @ConstructorProperties 어노테이션을 이용해서 파라미터를 변경할 수 있으니 한번 해보자.

public class Person {
    private final String name;
    private final String email;

    @ConstructorProperties({"user_name", "user_email"})
    public Person(String name, String email) {
        this.name = name;
        this.email = email;
    }

    public String getName() {
        return name;
    }

    public String getEmail() {
        return email;
    }

}

위와 같이 생성자에 @ConstructorProperties 어노테이션을 작성하고 해당 컬럼의 명을 순서대로 작성해주면 된다.

http POST :8080 user_name==wonwoo user_email==wonwoo@test.com
HTTP/1.1 200
Content-Type: application/json;charset=UTF-8
Date: Sun, 27 Jan 2019 11:03:29 GMT
Transfer-Encoding: chunked

{
    "email": "wonwoo@test.com",
    "name": "wonwoo"
}

아주 간단하다.

만약 lombok을 사용한다면 아래와 같이 작성하면 된다.

@Value
@RequiredArgsConstructor(onConstructor_ = @ConstructorProperties({"user_name", "user_email"}))
//@RequiredArgsConstructor(onConstructor = @__(@ConstructorProperties({"user_name", "user_email"})))
class Person {
    String name;
    String email;
}

onConstructor 문법은 java 버전과 관련이 있다.

@RequestBody (Jackson)

jackson 기준이기 때문에 다른 라이브러리를 사용한다면 다를 수 있으니 참고하길 바란다.

@RequestBody 어노테이션 또 한 불변의 Object로 만들 수 있다. Spring 지원한다기 보다는 사용하는 해당 라이브러리가 지원하고 있으니 사용하는 라이브러리의 문서를 살펴보면 좋다.

일단 기본적으로는 아무 설정하지 않았다면 jackson의 경우에는 기본생성자가 있어야 한다. 하지만 여러 방법으로 기본생성자 없이 사용할 수 있으니 한번 살펴보도록 하자.

@JsonProperty

@JsonProperty 어노테이션으로 해당 필드를 지정해주면 된다.

public class Person {

    private final String name;
    private final String email;

    public Person(@JsonProperty("name") String name, @JsonProperty("email") String email) {
        this.name = name;
        this.email = email;
    }

    public String getName() {
        return name;
    }

    public String getEmail() {
        return email;
    }
}

위와 같이 작성한다면 기본생성자 없이도 deserialize가 가능하다. 만약 모델의 필드와 요청파라미터가 다르다면 @JsonProperty의 value를 변경하기만 하면 된다.

lombok을 사용한다면 다음과 같다.

@Value
class Person {
    String name;
    String email;

    public Person(@JsonProperty("name") String name, @JsonProperty("email") String email) {
        this.name = name;
        this.email = email;
    }
}

lombok일 경우에는 조금 귀찮다. 생성자 필드에는 어떻게 넣지..?

@ConstructorProperties

jackson 도 @ConstructorProperties 어노테이션을 지원하다. 아마도 jackson2.7 부터 지원한다고 했는데 그 이하버전은 테스트는 해보지 않았다.

public class Person {

    private final String name;
    private final String email;

    @ConstructorProperties({"name", "email"})
    public Person(String name, String email) {
        this.name = name;
        this.email = email;
    }

    public String getName() {
        return name;
    }

    public String getEmail() {
        return email;
    }
}

마찬가지로 해당 필드를 변경하고 싶다면 @ConstructorProperties의 속성을 순서대로 변경하면 된다.

lombok을 사용할 경우는 다음과 같다.

@Value
@RequiredArgsConstructor(onConstructor_ = @ConstructorProperties({"name", "email"}))
class Person {
    String name;
    String email;
}

jackson-module-parameter-names

jackson 해당 모듈을 사용해서도 가능하다. 하지만 ObjectMapper를 조금 설정해줘야 한다.

@Bean
public ObjectMapper objectMapper() {
    ObjectMapper objectMapper = new ObjectMapper();
    objectMapper.registerModule(new ParameterNamesModule());
    return objectMapper;
}

위와 같이 설정한 후에 사용하면 바로 사용할 수 있다. 하지만 위의 모듈은 java8부터 가능하다. 만약 그 이하 버전을 사용한다면 위의 모듈을 사용할 수 없다.

public class Person {

    private final String name;
    private final String email;

    public Person(String name, String email) {
        this.name = name;
        this.email = email;
    }

    public String getName() {
        return name;
    }

    public String getEmail() {
        return email;
    }
}

해당 모듈을 사용한다면 어노테이션을 사용하지 않아도 기본적으로 동작을 한다.

lombok을 사용할 경우는 다음과 같다.

@Value
class Person {
    String name;
    String email;
}

참고로 Spring boot 2.0 부터는 spring-boot-starter-json 에 jackson-module-parameter-names 모듈이 포함되어 있다. 그래서 spring-boot-starter-web을 디펜더시 받는 다면 기본적으로 spring-boot-starter-json가 포함 되어 있으니 jackson-module-parameter-names를 추가 하지 않아도 된다. 만약 그 이전 버전을 사용한다면 해당 모듈만 디펜더시만 받으면 자동설정이 동작한다.
또 한 jackson-module-parameter-names 과 @ConstructorProperties 어노테이션은 함께 동작하지 않는 것 같다. jackson-module-parameter-names 을 사용한다면 @JsonProperty 어노테이션을 사용해야 한다.

@Value
//@RequiredArgsConstructor(onConstructor_ = @ConstructorProperties({"user_name", "user_email"})) // not working
class Person {
    String name;
    String email;

    public Person(@JsonProperty("user_name") String name, @JsonProperty("user_email") String email) {
        this.name = name;
        this.email = email;
    }
}

위처럼 jackson-module-parameter-names 을 사용하는 경우엔 @JsonProperty 어노테이션을 사용해서 해당 필드를 변경해야 한다.

오늘은 이렇게 Spring Web immutable Parameter 에 대해서 알아봤다.
꼭 Object를 불변으로 만들 필요는 없지만 사용할일이 있다면 참고하면 되겠다. 또 한 프로퍼티들을 변경해야 하는 일이 있다면 사용해도 괜찮을 듯 하다.