gson에서 필드를 제외하는방법

서론

Spring Boot를 사용해서 개발을 하면 API의 응답/결과 값을 처리할때 기본적으로 Jackson 라이브러리를 이용해 JSON와 POJO간 변환 처리를 진행합니다. 하지만 gson을 이용해 변환을 처리하게 할 수 도 있습니다. Jackson도 물론 좋은 라이브러리지만 gson이 더 성능이 좋아서 그렇게 하시는분들도 있는거 같더라구요. JSON 라이브러리 벤치마크

회사에서도 현재 그렇게 사용하고 있습니다. 맨 처음 프로젝트를 설정하신 분의 의도와 히스토리는 정확히는 모르겠지만 아마도 성능의 문제로 하지 않았을까 싶습니다.

어쨌든! gson을 이용해서 API 결과값을 전달할때 항상 요청받는 사항들이 있습니다. “A 필드 안쓰는데 안내려주시면 안돼요?”, “B 필드 빼고 내려주세요” 라는 이야기인데요 사용하지도 않는 필드가 내려오는게 보기에 좋지 않을 수도 있고, 불필요하게 페이로드가 커져서 일 수도 있을거 같습니다. (사실 안물어봤어요… 명확한 이유가 있나 한번 물어봐야겠습니다)

그럼 gson을 이용해 직렬화/역직렬화 할때 필드를 제거하는 방법에 대해 알아볼까 합니다.

 

방법 0 - DTO와 Payload를 분리

특정 기술을 사용하는 방법은 아니고 단순하게 생각해서 요청으로 받을 DTO와 응답으로 돌려줄 DTO(payload라고 부르겠습니다)를 분리하여서 처리하는 방식을 선택할 수 있습니다.

payload에는 노출할 필드만 두고 DTO에는 모든 필드를 두고 처리한다면 요구사항 처럼 특정 필드만 노출이 안되게끔 처리가 가능할 수 있을거 같습니다.

DTO를 payload로 매핑할때는 ModelMapper를 이용해서 데이터를 매핑하면 좀더 수월하게 처리 할 수 있을거 같습니다.

 

방법 1 - transient 키워드 사용

gson의 github 이슈를 보면 종종 Expose는 있는데 왜 반대 되는건 없냐! 만들어달라! NotExpose나 Exclude를 만들어달라! 하는 이슈가 가끔씩 있습니다. java 키워드중 transient를 쓰라고 하는 답변이 있는데요.

transient 키워드를 필드앞에 적어두면 직렬화 되지 않는다고 합니다.
JVM에서 해당 키워드가 있는 필드는 기존의 값을 무시하고 변수타입의 기본값으로 대체된다 라고 하는데.(참고 마지막 링크에 해당 내용이 있습니다) 기본값도 값이 있는거 아닌가?(0은 null이 아닌것처럼) 라는 생각이 들었습니다.

스크린샷 2021-10-14 오전 1 05 02

필드 선언시 transient 키워드를 함께 사용해주면 응답값에서 해당 필드가 제외됩니다.

1
private transient String description;

** transient 키워드랑 static or final 키워드를 같이 사용할경우 직렬화가 되는 예제가 있습니다. java I/O스트림을 이용해서 직렬화/역직렬화를 수행시엔 직렬화가 됩니다.

gson사용시엔 gson에서 transient 키워드를 한번 더 걸러서 직렬화에서 제외합니다. 참고

참고

 

방법 2 - @Expose 사용

gson에서는 Expose라는 어노테이션을 제공해주는데 말 그대로 해당필드를 직렬화/역직렬화시 노출 시켜준다는 어노테이션 입니다.

하지만 기본적으로 해당 어노테이션이 없어도 모든 필드가 다 노출되는데, 어노테이션을 사용하려면 Gson을 처음 초기화할때 excludeFieldsWithoutExposeAnnotation() 메소드를 사용해 줘야합니다. 그래야 해당 어노테이션이 붙은 필드만 노출이되게 동작합니다.

그럼 위와 같은 내용으로 excludeFieldsWithoutExposeAnnotation()를 이용해 gson을 초기화 해주고, 노출시키고 싶지 않은 필드에만 @Expose를 안 넣으면 됩니다.

1
2
3
4
5
6
public class User {
@Expose
private String id; // 노출

private String password; // 비노출
}

위 방법대로면 gson에서 제공해주는 어노테이션으로 특정 필드가 노출되지 않게 처리할 수 있지만, 예를들어 필드가 100개인데 1개만 노출시키지 않겠다고 하면 99개의 필드에 @Expose를 붙여야하는 번거로움이 있습니다 -> 그래서 반대되는 기능을 만들어달라고 하는거 같습니다.

여기서 생각해봐야할게 그럼 왜 gson은 반대되는 기능을 하는 어노테이션을 안만들었을까 하는 건데요.

다른 분들의 의견을 참고하자면 @Expose를 사용하지 않고 처리할경우 필드가 추가되고, 해당 필드가 보안상 노출되지 않아야 하는 필드라면 개발자 따로 비노출 처리를 해야하고 실수로 그 처리를 하지 않게되면 보안상 이슈가 발생한다. 그래서 노출할 필드만 명시적으로 처리하는거 같다 라는 의견이 있었습니다.

 

방법 3 - ExclusionStrategy 사용

제외 전략을 개발자가 직접 구현해 gson 초기화시 설정을 할 수 있습니다.

구현 방법 순서는 다음과 같습니다.

  1. 비노출필드에 사용될 annotation 생성
  2. ExclusionStrategy 구현
  3. gson 초기화시 추가
1
2
3
4
5
// 1. 비노출필드에 사용될 annotation 생성
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Exclude {
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 2. ExclusionStrategy 구현
public class ExcludeStrategy implements ExclusionStrategy{

@Override
public boolean shouldSkipField(FieldAttributes f) {
// Exclude 어노테이션이 있는 필드는 true를 반환합니다.
Exclude annotation = f.getAnnotation(Exclude.class);
if (annotation != null) {
return true; // 필드 비노출
}
return false; // 필드 노출
}

@Override
public boolean shouldSkipClass(Class<?> clazz) {
return false;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 3. gson 초기화시 추가 -> setExclusionStrategies()
// Spring Boot에서 사용하는것처럼 GsonHttpMessageConverter 빈을 등록하고 Gson을 만들때 2번에서 만든 제외 전략을 추가합니다.
@Configuration
public class WebConfigure implements WebMvcConfigurer {

@Bean
public GsonHttpMessageConverter gsonHttpMessageConverter() {
Gson gson = new GsonBuilder()
.setExclusionStrategies(new ExcludeStrategy())
.create();

GsonHttpMessageConverter gsonHttpMessageConverter = new GsonHttpMessageConverter();
gsonHttpMessageConverter.setGson(gson);
return gsonHttpMessageConverter;
}
}

위와 같은 형태로 처리되면 API의 요청과 응답값을 처리할때 @Exclude annotation이 붙은 필드는 모두 비노출 처리됩니다.

테스트를 해봅시다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Getter
@AllArgsConstructor
public class TestDTO {

private final String a;

@Exclude
private final String b; // 이 필드만 비노출 합니다.
}

@PostMapping(value = "/test")
public TestDTO exclude(@RequestBody TestDTO testDTO) {
// 역직렬화 결과 출력
System.out.println("Deserialize-" + testDTO.getA());
System.out.println("Deserialize-" + testDTO.getB());

// 직렬화때 어떻게 처리되나 확인해봅니다.
return new TestDTO("Serialize-A","Serialize-B");
}

Postman을 이용해서 아래와 같이 요청을 날리면 응답은 필드 a만 옵니다. (필드 b는 비노출 처리됩니다.)
스크린샷 2021-10-15 오전 12 10 57

요청을 받는 컨트롤러에는 b필드가 null로 처리 됩니다.
스크린샷 2021-10-15 오전 12 11 34

@Exclude 가 붙은 필드 b만 요청(역직렬화)과 응답(직렬화)에서 제외되어서 처리됩니다.

 

방법 3-1

방법 3처럼 처리가 되면 직렬화와 역직렬화시 모두 필드가 비노출 처리가 되는데 직렬화 할때만 처리하거나 역직렬화 할때만 처리하거나 하고싶은 경우가 있을 수 있습니다.

setExclusionStrategies()는 직렬화/역직렬화에 모두 적용되는 전략을 설정하는거라면 아래 두 함수를 잘 이용하면 직렬화/역직렬화 각각 다른 전략을 사용해서 필드를 비노출 처리 할 수도 있습니다.

 

참고

공유하기