본문 바로가기
320x100
320x100

여러분들은 아마 DTO에 값이 어떻게 채워져서 컨트롤러 메서드 내부의 인스턴스화된 오브젝트로 넘어오는지 궁금하지않나요?

제가 완벽히는 아니더라도 아는만큼 써놓겠습니다

오브젝트(객체)가 메모리에 올라와서 접근 가능하고, 처리 가능하게 되는 것을 인스턴스화된 객체, 인스턴스라고 합니다.

그럼 DTO는 언제 인스턴스화가 될까요?

기본적으로 Get 방식과 Post 방식은 스프링에서 처리 방식이 다릅니다

(이 밑부터는 반..말체 ㅎ)

1. Get Request + DTO

Get요청에서의 DTO들은 모두 다 QueryString으로 들어간다

QueryString이란 ?{Key}={Value}&[Key}={Value} 방식으로 들어간다는 것이다

어차피 스프링에서 DTO로 만들어도 포스트맨 등에서는 Query Params에 넣어서 던져야 하기 때문이다

//    @AllArgsConstructor//혹시라도 원시타입이 있을 경우 주의!!
//    @NoArgsConstructor(access = AccessLevel.PROTECTED)
//    @NoArgsConstructor
//    @RequiredArgsConstructor
//    @Getter
//    @Setter
public class TestDTO1 {

        private String name;

        //@NonNull
		//private final String address;
        private String address;

		//private Integer age;
        private int age;
}

Get 요청일때 DTO를 인스턴스화 할 수 있는 방법에는 어떤 것들이 있을까?

1. @AllArgsConstructor

  모든 파라미터를 받는 생성자를 만들어준다

내부적으로는 이렇게 동작하는 것이다

new TestDTO1("Kim", null, null);

자, 그렇기때문에 혹시라도 DTO필드값을 참조타입이 아니라 원시타입으로 한 경우는 에러가 날것이라고 예상을 할 수 있다

Integer age에서 int age로 바꿔보면?

이렇게 422 UNPROCESSABLE_ENTITY 예외가 발생한다

boxing, unboxing을 생각해야겠지만, 입력값의 검증을 위해서는 ObjectUtils혹은 StringUtils을 활용할 수 있는데, 이 방식을 사용하려면 DTO로 셋팅되는 값은 원시(int)형이 아닌 참조(Integer) 타입이어야 한다는 것이다

 

2. @NoArgsConstructor

  파라미터가 없는 기본 생성자를 만들어준다

롬복 어노테이션으로 인해

public TestDTO1() {}

이 기본 생성자만 생기게 되었는데, 각각 파라미터를 비우거나 채워줘도 값을 잘 받아오는 것을 볼 수 있었다

 

3. @RequiredArgsConstructor

  final이나 @NonNull인 필드 값만 파라미터로 받는 생성자를 만들어준다

하지만 실제 동작은 조금 다르다. final이나 @NonNull이 붙은 필드의 파라미터를 가진 생성자를 만들어주는것은 맞으나, @NonNull일때는 값이 비어진 생성자가 생성되지 않는다. @NonNull대신 final을 붙이면서 생성자를 만든다면 생략할 수 있게 된다.

address만 가진 생성자를 갖고 있는다고 가정해도...

public TestDTO1(String address) {
	this.address = address;
}

 

위에 @NoArgsConstructor처럼 address만 받는게 아니라, name이나 age를 받는 것을 확인 할 수 있었다

private field + @RequiredArgsConstructor
@NonNull + @RequiredArgsConstructor

자 이렇게 동작 방식이 다르다. 어떻게보면 생성자는 1개만 만들어지지만, @NonNull의 경우 Validation 역할까지 겸하고있는 것을 볼 수 있다(정확히는 아님)

 

4. @Getter

  get + 필드네임(CamelCase) 방식으로 get메서드들을 만들어준다

public String getName() {
        return name;
}

컨트롤러단에서 이름을 받아오니 null이 들어온다

 

5. @Setter

  set + 필드네임(CamelCase) 방식으로 set메서드들을 만들어준다

public void setName(String name) {
        this.name = name;
}

잘 작동한다. 추가로 Integer age에서 int age로 바꿨는데도 동작을 한다

 

결론: 생성자 혹은 Set 메서드로 DTO가 인스턴스화 된다. 생성자는 기본 생성자 하나만 있어도 된다.

의문점 : 왜 set메서드가 있어야 작동할까?

: Get 요청은 WebDataBinder 객체를 사용하는데, 기본적으로 값을 할당하는 방법이 Java Bean 방법이기 때문이다.

Java Bean 방법은 setter 메서드로 인해서 값을 할당하는 것을 말하죠?

 

추가의문점 : 그럼 무조건 setter가 있어야하는가?

 : 그것은 아니다. InitBinder의 전략을 바꿔주면 된다. 전체 컨트롤러에 적용될 수 있게

1. @ControllerAdvice 어노테이션이 붙은 컨트롤러에 이 코드를 추가하거나

2. 모든 컨트롤러를 특정 컨트롤러를 상속받는 방법으로 바꾸거나

3. 그냥 현재 컨트롤러에 적용하거나

하면 된다

@InitBinder
public void initBinder(WebDataBinder binder) {
    binder.initDirectFieldAccess();
}

테스트ㄱㄱ(Without Setter)

셋터는 주석했구..

Getter만으로도 인스턴스화가 가능해졌다!

+

참고로 InitBinder를 필드로 변경한 경우에는 리플렉션으로 필드에 직접 접근하는 것이라서 Getter도 필요가 없다 ㅋㅋ

 

추가의문점 : 그럼 생성자 없이 Getter만 두면 되지 않는가?

 : 그것은 아니다. 테스트를 할 때 생성자를 이용할 목적으로 기본생성자를 두는 것이다. 또한 추가로 기본 생성자를 쓸 때는 accessLevel을 올려서 사용하는 것이 좋다. DTO와 도메인 클래스를 분리해서 사용할 목적, 특정 요청에는 특정 값만을 변경해야 하기 때문이다

 

최종결론: 기본적으로 Get 요청은 2가지 방식으로 객체가 인스턴스화가 될 수 있다

1. 생성자

기본 생성자, 전체 생성자, 필수생성자 3개중에 최소 1개만 있어도 인스턴스가 만들어진다

Best는 접근레벨을 두어서 각 레이어별 분리 사용을 하고 테스트 용도로 만들면 좋다

2. Setter

WebDataBinder의 기본 전략이 Java Bean 방식을 따르기 때문에 Setter가 있어야만 값을 채워서 온다

하지만 @InitBinder를 JavaBean이 아닌 FieldAccess 방식으로 바꾼다면 그냥 값을 채워서 가져온다(생성자나 Getter없이도)

기본적으로 Entity를 Setter가 열린채로 반환하면 매우 위험하기때문에 Get 방식에서 @ParameterObject를 넘기는 경우에는 필드접근방식 + 데이터처리를 위한 Getter + 테스트와 레이어 목적별 분리를 위한 기본생성자(accessLevel) 를 사용하자

 

2. Post Request

오히려 Post 요청은 기본적으로 @Get + @RequestBody가 커플이라고 보면 실수할일은 없다

다만 왜 이렇게 처리가 되는지 알아야 할 필요가 있다

Spring 에서는 JSON 형변환을 담당하는 것이 Jackson2HttpMessageConverter 객체이다
즉, @RequestBody로 JSON 데이터가 넘어오면 이 JSON을 Java Object로의 변환을 Jackson2HttpMessageConverter가 담당해서 처리한다

또한 Converter에서는 ObjectMapper를 사용해서 Object로 전환해준다

이 과정에서는 직렬화, 역직렬화라는 개념이 사용된다

ObjectMapper는 테스트코드를 작성할때나, 개발할때 ModelMapper와 함께 자주 사용했을 것이다(Object to Map 등등)

즉, Post 요청에서는 Setter가 없이도 Jackson2 Http메시지 컨버터에 있는 ObjectMapper가 알아서 값을 채워주기때문에 Setter가 필요가 없다는 것이다.

매우 잘 작동한다

 

+

Q. 그럼 생성자로는 인스턴스화가 불가능할까?

null이 들어온다

A. 생성자랑 @RequestBody랑은 상관이 없다

 

Q. 그럼 필드값을 반환하는 get함수만 다 작성되어있다면 값을 받아올수있겠네?

A. 아니다. 꼭 get[필드명]-카멜케이스형식이나 get[필드명]-맨앞자리만소문자 이 2개 형식을 만족해야한다, boolean의 경우에는 is까지 가능

 

요약하자면 getter 또는 setter 메서드의 'get', 'set' 부분을 때어내고 앞의 첫글자를 소문자로 바꿔서 body에 들어온
key와 같을때 매핑한다고 한다.

 

과연 그런지 테스트해봐야되지않겠나~~~

//    @AllArgsConstructor
//    @NoArgsConstructor
//    @Getter
    @ToString
    public static class TestDTO2 {

        private String name;
        private String address;
        private String location;
        private Integer age;
        private String leveldescription;
        private Boolean bool1;
        private Boolean bool2;
        private Boolean bool3;

        public String printName() {
            return this.name;
        }

//        public String getName() {
        public String getname() {
            return this.name;
        }

        public String getAddress() {
            return this.address;
        }

        public String getLoCaTion() {
            return this.location;
        }

        public String leveldescription() {
            return this.leveldescription;
        }

        public Boolean isBool1() {
            return this.bool1;
        }

        public Boolean getBool2() {
            return this.bool2;
        }

        public Boolean isbool3() {
            return this.bool3;
        }

    }

의도한 오답들은 getLoCaTion 메서드, 빠진 age의 get메서드, get이라는 네이밍이 안붙은 leveldescription 메서드

의도한 정답들은 getName 혹은 getname 메서드, 올바른 getAddress 메서드,  Boolean을 테스트할 isBool(카멜케이스), isbool(그냥소문자), getBool(겟함수)

과연 의도한대로 들어오는지 디버깅을 해보았다

name, address, boolean들에는 값들이 다 정상적으로 들어왔다

getLoCaTion에는 null, get 메서드가 없는 age에도 null, get으로 시작하는 함수명이 아닌 leveldescription에도 null이 들어왔다

 

 

최종결론: Post 요청은 Get 메서드 + 첫글자만 소문자 or 대문자 이후로는 카멜케이스규칙을 지켜야지 객체가 인스턴스화 된다

(Boolean의 경우 isXXX은 가능)

댓글