협업 도중 발생한 문제로 시도한 JsonNaming과 JsonProperty
문제가 발생한 이유는 다음과 같다.
Front-End에서는 이미 많은 부분을 snake_case를 사용하고 있었기에 변경소요가 너무 커서 계속해서 snake_case를 사용하기 원했다.
Back-End 개발자인 나는 Java 작성 규칙에 따라 camelCase로 작성하길 원했다.
그렇기 때문에 Front-End에서는 snake_case로 데이터를 보냈고, snake_case로 데이터를 받았다.
두 가지의 요구사항을 만족하기 위해 어떻게 해결할까? 생각을 하며 여러가지 방법을 시도해 보았는데, 결과물은 같지만 불필요한 코드가 많이 발생하게 되는 과정이 많이 생겼다.
이러한 문제해결 도중 @JsonNaming과 @JsonProperty를 알게 되었다.
결론적으로 아주~ 깔끔해지고 간편해졌다.
본인이 코드를 작성하는 것도 좋지만 이미 만들어져 있는 라이브러리를 잘 이용하는 것도 개발자의 능력이라 생각한다.
이제 위 기능을 알아보자
이 기능은 언제 사용하나...?
자바에서는 Json을 직접 타이핑해서 작성하기는 매우 불편한데...
System.out.println("{\\"product_num\\"=1, \\"product_name\\"=닭가슴살}");
결과 : {"product_num"=1, "product_name"=닭가슴살}
심신 건강에 매우 해로워 여기까지만 작성해 보겠다...
이를 편하게 JSON 형식인 key와 value 형태로 편하게 사용이 가능한 jackson 라이브러리가 존재한다.
그 안에서 key 값의 형태를 snake_case, camel_case 등으로 본인이 사용하지 않는 방식으로 데이터를 받아야 할 때 JsonNaming과 JsonProperty를 사용해 내가 사용하는 규칙으로 자동 변환이 되도록 해주는 유용한 기능이 존재한다.
ex) product_num → productNum
ex) productName → product_name
주로 언제 사용하겠는가?
다른 팀 또는 다른 회사와의 협업을 할 경우 참 다양한 규칙을 사용할 것인데,
이럴 때 나에게 맞는 형식으로 변환하는 식으로 유용하게 사용될 것이라 생각한다.
JsonNaming VS JsonProperty
JsonNaming과 JsonProperty 차이는 무엇일까?
- JsonNaming : class단위
- JsonProperty : 하나의 변수 단위
위와 같이 생각하면 편하겠다.
테스트를 위해 다음과 같이 작성해봤다.
- 데이터를 받을 Controller
- Data Bind 할 DTO
- 테스트에 사용할 Test Code
Controller
package com.example.demo.jackson_databind;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;
@RestController
@RequestMapping("/example/jackson/databind")
public class JacksonDatabindController {
@PostMapping("/json-naming")
public JacksonDatabindDTO.JsonNamingResponse jsonNamingExample(
@RequestBody @Valid JacksonDatabindDTO.JsonNamingRequest request){
JacksonDatabindDTO.JsonNamingResponse response = new JacksonDatabindDTO.JsonNamingResponse();
response.setProductNum(request.getProductNum());
response.setProductName(request.getProductName());
response.setProductPrice(request.getProductPrice());
System.out.println("----Json Naming Example----");
System.out.println("----request----");
System.out.println(request.toString());
System.out.println("----response----");
System.out.println(response.toString());
return response;
}
@PostMapping("/json-property")
public JacksonDatabindDTO.JsonPropertyResponse jsonPropertyExample(
@RequestBody @Valid JacksonDatabindDTO.JsonPropertyRequest request){
JacksonDatabindDTO.JsonPropertyResponse response = new JacksonDatabindDTO.JsonPropertyResponse();
response.setProductName(request.getProductName());
response.setProductPrice(request.getProductPrice());
response.setProductNum(request.getProductNum());
System.out.println("----Json Property Example----");
System.out.println("----request----");
System.out.println(request.toString());
System.out.println("----response----");
System.out.println(response.toString());
return response;
}
}
DTO
package com.example.demo.jackson_databind;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import lombok.Data;
public class JacksonDatabindDTO {
@Data
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public static class JsonNamingRequest{
private int productNum;
private String productName;
private int productPrice;
}
@Data
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public static class JsonNamingResponse{
private int productNum;
private String productName;
private int productPrice;
}
@Data
public static class JsonPropertyRequest {
@JsonProperty("product_num")
private int productNum;
@JsonProperty("product_name")
private String productName;
@JsonProperty("product_price")
private int productPrice;
}
@Data
public static class JsonPropertyResponse {
@JsonProperty("product_num")
private int productNum;
@JsonProperty("product_name")
private String productName;
@JsonProperty("product_price")
private int productPrice;
}
}
Test Code
package com.example.demo.jackson_databind;
import org.json.JSONObject;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import static org.junit.jupiter.api.Assertions.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@SpringBootTest
@AutoConfigureMockMvc
class JacksonDatabindControllerTest {
@Autowired
private MockMvc mockMvc;
private JSONObject request;
@BeforeEach
public void jsonSetting() throws Exception {
request = new JSONObject();
request.put("product_num",1);
request.put("product_name","닭가슴살");
request.put("product_price",35000);
}
@Test
@DisplayName("JSON_Naming 스네이크 케이스 -> 카멜 케이스 자동 변환")
public void jsonNamingExample() throws Exception {
mockMvc.perform(
post("/example/jackson/databind/json-naming")
.content(request.toString())
.contentType(MediaType.APPLICATION_JSON)
)
.andDo(print())
.andExpect(status().isOk())
.andExpect(content().json(request.toString()));
}
@Test
@DisplayName("JSON_Property 스네이크 케이스 -> 카멜 케이스 자동 변환")
public void jsonPropertyExample() throws Exception {
mockMvc.perform(
post("/example/jackson/databind/json-property")
.content(request.toString())
.contentType(MediaType.APPLICATION_JSON)
)
.andDo(print())
.andExpect(status().isOk())
.andExpect(content().json(request.toString()));
}
}
테스트 결과
테스트 결과는 동일하다
JsonNaming Request
JsonNaming Console
JsonNaming Response
JsonProperty Request
JsonProperty Console
JsonProperty Response
흐름을 살펴보자면 다음과 같다.
- request의 body를 보면 테스트 코드에서 product_num, product_name, product_price로 스네이크 케이스 형태의 데이터를 보냈다.
- databind 시 productNum, productName, productPrice로 변환했고 Console 창에 출력된 결과값을 보면 request와 response의 key 값이 모두 카멜 케이스로 변환된 상태인 것을 확인 할 수 있다.
- response의 body를 보면 product_num, product_name, product_price로 스네이크 케이스 형태로 데이터를 반환한 것을 확인할 수 있다.
두 가지의 사용법이 다른 점은 두 가지이다. 간단하다.
- Class 단위로 어노테이션을 한번 작성해 변환하느냐?
- 변수 메소드 단위로 하나하나 작성해 변환하느냐?
클래스 단위로 작성해서 변환하는 것이 가장 편하고 불필요한 코드가 발생하지 않겠지만 경험상 무조건 key 값이 product_num → productNum으로 넘어오지는 않더라
예를 들어 Legacy Code를 보면 product_num이 아닌 prod_num 과 같이 축약 형태로 작성된 코드가 많기에 JsonNaming과 JsonProperty를 짬뽕해서 사용해야 하는 경우도 있었다.
즉 자신에게 가장 필요한 방식을 잘 이용하는 것이 중요하다.
추가적인 정리
@JsonNaming
- 외부에서 넘어온 값이 product_num일 경우 productNum으로 자동 변환하여 databind 해준다.
@Data
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public static class JsonNamingRequest{
private int productNum;
private String productName;
private int productPrice;
}
@JsonProperty
- 외부에서 넘어온 값이 product_num 일 경우 productNum으로 databind 된다.
- 한 가지 추가적인 팁은 product_num → productNum이 아닌 prodNum이어도 상관 없다. JsonProperty는 외부에서 넘어온 Key값이 무엇이냐에 따라서 databind를 어떤 변수명에 할 것이냐가 정해질 수 있다.
@Data
public static class JsonPropertyRequest {
@JsonProperty("product_num")
private int productNum;
@JsonProperty("product_name")
private String productName;
@JsonProperty("product_price")
private int productPrice;
}
이상입니다.
도움이 되었길 바랍니다.
'Programming > Backend' 카테고리의 다른 글
MariaDB + SQLAlchemy + Flask에서 database 연결 끊김 현상 (0) | 2022.03.21 |
---|---|
Spring Request 데이터를 List 형태로 받으면 @Valid 체크가 안되는 현상 해결 방법 (1) | 2021.12.04 |
객체 지향, 오브젝트와 의존 관계의 이해 (0) | 2021.10.09 |
API 명세서 뜯어보기 - StringTokenizer Class (0) | 2021.10.05 |
Mock && Mockito : 가짜 객체 (0) | 2021.10.03 |