티스토리 뷰
문제가 뭐야?
프로젝트를 진행하면서 요청값과 응답값을 String 타입을 사용하였다.
json 라이브러리를 사용해서 문자열을 파싱하고 또 그안에서 필요한 값을 가져와 사용하고..
컨트롤러의 코드가 길어질뿐만아니라, 값을 가져오려다 오타가 발생하여 런타임에 에러가 발생하고 중복된 코드도 작성하게 되었다.
프로젝트가 커지기 전, 그때 미리 정의하고 수정했다면 아마 내가 12건 이상의 API를 수정할일은 없었을것이다.
어떻게 해결했어? 왜 그렇게 했어?
DTO 클래스를 정의하여 활용하기로했다.
사실 보통 프로젝트를 보면 DTO를 관례적으로 사용하는거같았지만, 또한 그렇게 진행하는 이유는 많지만.
나는 DTO 객체를 활용하고자했던 가장 큰 이유는 다음과같다.
1. 반복적인 데이터 파싱작업의 코드를 줄이고싶다.
2. 요청값 검증을 쉽게하고싶다.
3. 수정 범위가 요청/응닶값에 대해서만 이루어졌으면 좋겠다.
그건 어떻게 동작해?
DTO 객체를 정의는 했겠다. 어떻게 동작하는지는 알아야하지 않을까?
먼저, 습관적으로 작성했었던 @RequestBody부터 알아보자.
@RequestBody 는 다음과 같이 설명되어있다.
Annotation indicating a method parameter should be bound to the body of the web request. The body of the request is passed through an HttpMessageConverter to resolve the method argument depending on the content type of the request. Optionally, automatic validation can be applied by annotating the argument with @Valid.
요약해보면, request의 body는 HttpMessageConverter 통해 전달되고 request의 content type에 따라 그 형식에 맞게 데이터를 변환한다는것이다.
여기서 말하는 HttpMessageConverter 는 무엇일까?
doc 에 따르면 HTTP 요청과 응답을 변환해주는 전략 인터페이스라고 정의되어있다.
제공하는 함수들을 살펴보면 변환 대상 클래스와 미디어타입을 지원할 수 있는지와 메세지를 읽고 쓰는 기능을 제공한다.
그럼 JSON 타입의 메세지를 어떻게 객체에 할당할 수 있는것일까?
HttpMessageConverter의 구현체 중 하나인 MappingJackson2HttpMessageConverter 를 통해 진행된다.
Jackson 2.x 의 ObjectMapper 를 사용하여 JSON을 읽고 쓸 수 있는 클래스로, JavaBeans객체와 HashMap 데이터를 변환하거나 바인딩할 때 사용한다.
클래스를 직접 확인해보면, ObjectMapper 객체가 없고 구체적인 로직이 없다.
그렇다면 해당 클래스가 상속하고 있는 AbstractJackson2HttpMessageConverter 를 확인해보자.
ObjectMapper 객체가 여기있었다.
또한, 해당 클래스의 함수를 보면 ObjectMapper 객체를 통해 JSON과 관련된 로직을 처리하고 있는걸 확인할 수 있다.
여기까지 정리를 한번 해보자면 다음과 같다.
1. RequestBody로 들어온 요청은 각 요청의 content type에 맞게 HttpMessageConverter가 동작한다.
2. content type이 JSON이면 HttpMessageConverter를 구현한 MappingJackson2HttpMessageConverter가 동작한다.
3. MappingJackson2HttpMessageConverter의 부모 클래스인 AbstractJackson2HttpMessageConverter 클래스가 ObjectMapper 객체를 갖고있고, JSON 을 처리할 수 있는 read/write 함수가 구현되어있다.
그럼 Jackson의 ObjectMapper는 어떻게 JSON 필드를 Java 객체의 필드에 매핑을 할 수 있을까?
Jackson 홈페이지에는 다음과 같이 설명되어있다.
To read Java objects from JSON with Jackson properly, it is important to know how Jackson maps the fields of a JSON object to the fields of a Java object, so I will explain how Jackson does that.
By default Jackson maps the fields of a JSON object to fields in a Java object by matching the names of the JSON field to the getter and setter methods in the Java object. Jackson removes the "get" and "set" part of the names of the getter and setter methods, and converts the first character of the remaining name to lowercase.
For instance, the JSON field named brand matches the Java getter and setter methods called getBrand() and setBrand(). The JSON field named engineNumber would match the getter and setter named getEngineNumber() and setEngineNumber().
If you need to match JSON object fields to Java object fields in a different way, you need to either use a custom serializer and deserializer, or use some of the many Jackson Annotations.
Jackson은 JSON 필드를 Java 객체의 getter/setter 메서드와 매칭시켜서 JSON 필드와 Java 객체의 필드를 매핑한다고 한다. 이 때, Jackson 은 getter/setter 메서드에서 get, set 부분을 제거하고 첫 문자를 소문자로 변환한다.
즉, 필드명이 아닌 getter/setter 메서드를 보고 get/set 부분을 제거하여 필드를 확인한 다음 매핑한다는 의미이다.
그럼 여기서 몇가지만 테스트해보자.
1. 기본 생성자만 있는 경우; 매핑이 되지 않았다.
public class HelloRequest {
private String message;
public HelloRequest() {}
}
// 결과
HelloRequest{message='null'}
2. getter만 있는 경우; 매핑이 된다.
public class HelloRequest {
private String message;
public String getMessage() {
return message;
}
}
// 결과
HelloRequest{message='hello'}
3. setter만 있는 경우; 매핑이 된다.
public class HelloRequest {
private String message;
public void setMessage(String message) {
this.message = message;
}
}
// 결과
HelloRequest{message='hello'}
4. 1개의 매개변수가 있는 생성자; 에러가 발생한다. 기본생성자가 없다고 한다.
public class HelloRequest {
private String message;
public HelloRequest(String message) {
this.message = message;
}
}
// 결과
Resolved [org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Cannot construct instance of `com.example.web_test.request.HelloRequest` (although at least one Creator exists): cannot deserialize from Object value (no delegate- or property-based Creator)]
5. 1개의 매개변수가 있는 생성자 + getter; 에러가 발생한다. 기본생성자가 없다고 한다.
public class HelloRequest {
private String message;
public HelloRequest(String message) {
this.message = message;
}
public String getMessage() {
return message;
}
}
// 결과
Resolved [org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Cannot construct instance of `com.example.web_test.request.HelloRequest` (although at least one Creator exists): cannot deserialize from Object value (no delegate- or property-based Creator)]
6. n개의 매개변수가 있는 생성자; 매핑이 된다.
public class HelloRequest {
private String message;
private String sender;
public HelloRequest(String message, String sender) {
this.message = message;
this.sender = sender;
}
}
// 결과
HelloRequest{message='hello', sender='alice'}
이 테스트 결과로 알 수 있는 내용은 다음과 같다.
1. getter/setter만 있어도 매핑이된다.
Jackson에서 값을 주입할때 Reflection을 통해서 진행한다. 따라서 값의 변경 가능이 있는 setter대신 getter를 사용하는것이 안전하다.
2. 1개의 매개변수가 있는 생성자는 getter/setter 유무에 상관없이 에러가 발생한다.
-> Jackson 은 기본생성자를 통해 객체를 생성하는데, 매개변수가 있는 생성자가 있을 경우에는 객체를 생성할 수 없어서 에러가 발생한다.
3. n개의 매개변수가 있는 생성자는 getter/setter 유무에 상관없이 매핑이 된다.
-> Jackson의 `jackson-module-parameter-names` 라는 모듈로 인하여 매핑을 할 수 있는 것이다. getter/setter 유무의 상관없이 생성자의 매개변수 이름으로 JSON 필드를 매핑할 수 있다. 1개의 매개변수가 있는 생성자는 동작하지 않는다. -참고
그래서 어땠어?
혼자서 진행했던 일이라 힘들었던 기억이 난다.
각 API마다 요청값보다 응답값에 대한 필드가 많아도 너무 많았다.
명세서도 따로 없었을뿐더러, API를 직접 테스트해서 값에 대한 정의를 했어야했다.
더군다나.. 서비스가 운영중이었기에 잘못하면 안되기때문에 심적으로 부담이 많이되었고 수없이 확인을 했어야했다.
근데 수정하고 나니 그 이후로 API에 대한 요청/응답값이 변경될때마다 DTO 클래스 파일만 찾아서 수정하면 되었고 무엇보다 컨트롤러 코드의 길이가 많이 줄었고 응답값에 대한 json 객체를 생성하지 않아서 좋았다.
- Total
- Today
- Yesterday
- HashMap
- Spring
- 자동구성
- Security
- AutoConfiguration
- syncronized
- Sticky Session
- Load Balancer
- 고정 세션
- 오블완
- 추상클래스
- Red-Black Tree
- 인터페이스
- HashSet
- nosql
- 다중화
- nginx
- fail-safe
- @conditional
- Hash
- fail-fast
- JPA
- 로드 밸런서
- 인스턴스변수
- 티스토리챌린지
- spring boot
- object
- Caching
- 정적변수
- java
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |