ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Spring Web immutable Parameter
    카테고리 없음 2023. 4. 23. 14:05
    오늘은 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를 불변으로 만들 필요는 없지만 사용할일이 있다면 참고하면 되겠다. 또 한 프로퍼티들을 변경해야 하는 일이 있다면 사용해도 괜찮을 듯 하다.

    댓글

Designed by Tistory.