meaningful_life
meaningful life
meaningful_life
전체 방문자
오늘
어제
  • 분류 전체보기 (28)
    • Programming (28)
      • Backend (25)
      • Machine Learning (1)
      • Infrastructure (2)

블로그 메뉴

  • 홈

공지사항

인기 글

태그

  • Spring
  • 자바
  • kubectl
  • error
  • 자바의신
  • ufw
  • Kubernetes
  • docker
  • 쿠버네티스
  • install
  • git
  • Database
  • 백준
  • 머신러닝
  • java
  • flask
  • ubuntu
  • linux
  • python
  • stringbuilder

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
meaningful_life

meaningful life

협업을 위한 snake_case -> camelCase 변환 방법 @JsonNaming, @JsonProperty
Programming/Backend

협업을 위한 snake_case -> camelCase 변환 방법 @JsonNaming, @JsonProperty

2021. 11. 22. 00:34
728x90

협업 도중 발생한 문제로 시도한 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

흐름을 살펴보자면 다음과 같다.

  1. request의 body를 보면 테스트 코드에서 product_num, product_name, product_price로 스네이크 케이스 형태의 데이터를 보냈다.
  2. databind 시 productNum, productName, productPrice로 변환했고 Console 창에 출력된 결과값을 보면 request와 response의 key 값이 모두 카멜 케이스로 변환된 상태인 것을 확인 할 수 있다.
  3. 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;
    }

이상입니다.

도움이 되었길 바랍니다.

728x90

'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
    'Programming/Backend' 카테고리의 다른 글
    • MariaDB + SQLAlchemy + Flask에서 database 연결 끊김 현상
    • Spring Request 데이터를 List 형태로 받으면 @Valid 체크가 안되는 현상 해결 방법
    • 객체 지향, 오브젝트와 의존 관계의 이해
    • API 명세서 뜯어보기 - StringTokenizer Class
    meaningful_life
    meaningful_life
    하루하루가 의미 있고 가치 있는 삶이 될 수 있길. 그리고 나의 추억과 각종 지식의 기록과 이를 공유함으로써 다른 사람에게도 도움이 되길...

    티스토리툴바