API 어플리케이션에서 로깅 처리를 할 때
기본적으로는 API에 대한 요청정보 - RequestBody-와 응답 정보 - ResponsBody - 는 출력하도록 설정합니다.

 

API의 스펙대로 요청 항목이 정확하게 들어왔는지,
응답 값이 정상적으로 반환되었는지를 확인하고 그 근거를 남기기 위해서 필요한 최소한의 정보라고 할 수 있습니다.

 

특히 외부에 제공하는 서비스의 성격을 띤 API 애플리케이션의 경우는
서비스의 장애 발생 시, 원인 파악과 해결을 위한 필수적인 정보가 됩니다.

 

물론 헬스체크 등, 비즈니스와 상관없는 로깅 자체가 불필요한 API의 경우에는
해당 API 자체가 로깅에서 제외되도록 처리하는 게 필요하겠죠.

 

그 방법에 대해서는 제가 이전에 올린 글을 참고하시면 됩니다.

https://yonguri.tistory.com/106?category=359077 

 

[스프링,스프링부트] 커스텀 어노테이션(Custom Annotation)을 활용한 AOP에서의 로깅처리

일반적으로 어플리케이션 서비스들이 공통으로 처리해야 할 기능들은 그 성격과 범위에 따라 인터셉터나 AOP를 사용합니다. 접근권한 로깅 예외처리(에러처리) 트랜잭션 어플리케이션에서의 공

yonguri.tistory.com

 

REST API의 경우라면
대부분의 응답유형이 JSON이기 때문에 로깅도 JSON 구조로 출력되도록 구현합니다.

 

Spring이 기본적으로 사용하는 JSON 라이브러리인 Jackson을 사용해서
요청과 응답을 처리하는 공통모듈 등에서 Serializaiton을 통해 JSON String으로 출력하도록 합니다.

 

그런데 요청/응답항목 중에도
꼭 로깅을 할 필요가 없는 항목들(정확하게는 항목의 값)이 있을 수 있습니다.

 

좀 더 구체적으로 얘기하자면

  • 값 전체를 보여주지 않아야 하는 경우(보안상의 이유 등)
  • 항목 값이 너무 커서 (이미지 파일의 Base64 인코딩 코드 등) 로깅 파일의 용량이 불필요하게 커진다거나 그로 인한 서버의 성능에 영향을 끼칠 때

해당 항목 값을 줄여서 보여주거나, 아예 보여주지 않거나 또는 마스킹 처리를 해야 할 필요성이 생길 수 있습니다.

 

이렇게 API단위가 아닌
응답 객체(Response DTO)의 특정항목에 대해서 필터링을 해야 할 때,
Jackson 라이브러리의 setAnnotationIntrospector기능을 이용해 간단히 구현할 수 있는 방법을 공유합니다.

 

 

구현 절차는 다음과 같습니다.

  1. Custom Annotaion 생성 (Masking 처리를 담당)
  2. AnnotationIntospector 정의
  3. AnnotationIntospector ObjectMapper에 적용

 

Custom Annotaion 생성

  • 마스킹 처리를 할 어노테이션 클래스를 생성합니다.
import com.fasterxml.jackson.annotation.JacksonAnnotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.ANNOTATION_TYPE, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotation
public @interface MaskLogging {
  String value() default "*****";
  String defaultValue() default "*****";
}

프로퍼티에서 적용할 어노테이션이기 때문에 Elementtype은 Field하나로만 정해주면 됩니다.

 

@Target({ElementType.ANNOTATION_TYPE, ElementType.FIELD})

 

이 어노테이션은 당연히 런타임 시에 실행되면 되기 때문에
@Retention(RetentionPolicy.RUNTIME)로 정의해 줍니다.

 

그리고 @JacksonAnnotation을 추가하여 이 어노테이션은

Jakson 라이브러리에서 사용되는 어노테이션임을 정의해줍니다.

 

이 어노테이션이 가지는 속성 값은 단순합니다. 
어떤 값으로 마스킹할지 그 값을 지정하는 value 속성만 필요합니다.

 

values는 지정하지 않으면 디폴트로 "*****" 값이 마스킹으로 적용됩니다.
추가적으로 value 속성 값을 생략할 수 있도록 default값을 정의했습니다.

어노테이션의 적용은 일반적인 어노테이션 사용법과 동일합니다.
아래와 같이 Masking이 필요한 프로퍼티에서 어노테이션을 붙여주면 됩니다.

 

DTO 클래스

public class ResponseBodyDto {
    private int id;
    private String name;
    private String addFileNm;
    private String addFileLoc;
    private Date creDt;
    ...
    ...
    @MaskLogging
    private String visitorImage;
    ...
    ...
}

 

이렇게 임의의 마스킹 값을 지정해서 사용할 수도 있습니다.

@MaskLogging(value = "*****")
private String visitorImage;

 

Jackson AnnotationIntospector 정의

public static class MaskPropertyAnnotationIntrospector extends NopAnnotationIntrospector {
    private static final long serialVersionUID = 1L;

    @Override
    public Object findSerializer(Annotated am) {
      MaskLogging annotation = am.getAnnotation(MaskLogging.class);
      if (annotation != null) {
        return MaskPropertySerializer.class;
      }
      return null;
    }
  }

  public static class MaskPropertySerializer extends StdSerializer<String> implements ContextualSerializer {
    private static final long serialVersionUID = 1L;

    private String maskValue;

    public MaskPropertySerializer() {
      super(String.class);
    }

    public MaskPropertySerializer(String maskValue) {
      super(String.class);
      this.maskValue = maskValue;
    }

    @Override
    public void serialize(String value, JsonGenerator gen, SerializerProvider provider) throws IOException {
      gen.writeString(maskValue);
    }

    @Override
    public JsonSerializer<?> createContextual(SerializerProvider serializerProvider, BeanProperty beanProperty) throws JsonMappingException {
      String maskValue = null;
      MaskLogging ann = null;
      if (beanProperty != null) {
        ann =beanProperty.getAnnotation(MaskLogging.class);
      }
      if (ann != null) {
        maskValue = ann.value();
      }
      return new MaskPropertySerializer(maskValue);
    }
  }

필요한 클래스는 2개입니다.
두 클래스 모두 정적 클래스로 생성하면 됩니다.

  • MaskPropertyAnnotationIntrospector
    • NopAnnotationIntrospector 인터페이스를 구현한 AnnotationIntrospector 구현클래스입니다.
    • 이 클래스는 findSerializer 메서드를 오버라이딩해서 마스킹어노테이션(MaskLogging 인터페이스)이 적용된 Serializer를 반환하게 됩니다.
    • 실직적으로 Jackson의 ObjectMapper에 적용될 AnnotationIntrospector 입니다.
  • MaskPropertySerializer
    • ContextualSerializer 인터페이스를 구현한 Serializer 클래스입니다. 
    • 이 클래스는 실제로 Json의 Serialize를 담당하는 클래스로,
      Serializer를 실행하는 시점에 MaskLogging 어노테이션이 적용된 프로퍼티에
      마스킹 값이 적용되도록 처리합니다. 

 

Jackson ObjectMapper에 AnnotationIntrospector 적용

최종적으로 이렇게 생성된 AnnotationIntrospector 클래스를,

사용하고 있는 Jackson의 ObjectMapper에 적용합니다.

 

아래의 예시는 Object타입의 매개변수를 받아 Json String으로 변환해주는 메서드입니다.

일반적으로 스프링 부트 기반에서는 ObjectMapper를 빈으로 등록시켜 놓고 필요시 주입받아 사용할 텐데,

속성을 정의하는 방식은 동일합니다.

 

적용 전 코드

ObjectMapper 속성 중 프로퍼티의 Visiblility에 대한 속성만 적용되어 있는 상태입니다.

public static String buildJsonStr(final Object src) {
  try {
    ObjectMapper mapper = Jackson2ObjectMapperBuilder.json().build();

    return mapper
        .setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE)
        .setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY)
        .writeValueAsString(src);
  } catch (Exception e) {
    mLog.error("stackTrace:", e);
  }
  return null;
}

 

적용 후 코드

AnnotationIntrospector 가 적용된 코드입니다.
AnnotationIntrospectorPair.pair 메서드를 통해서 MaskPropertyAnnotationIntrospector 객체를 얻어옵니다.

public static String buildJsonStr(final Object src) {
  try {
    ObjectMapper mapper = Jackson2ObjectMapperBuilder.json().build();
    AnnotationIntrospector sis = mapper.getSerializationConfig().getAnnotationIntrospector();
    AnnotationIntrospector ais = AnnotationIntrospectorPair.pair(sis, new MaskPropertyAnnotationIntrospector());

    return mapper
        .setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE)
        .setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY)
        .setAnnotationIntrospector(ais)
        .writeValueAsString(src);
  } catch (Exception e) {
    mLog.error("stackTrace:", e);
  }
  return null;
}

 

합쳐진 최종 코드입니다.

추가된 클래스는 별도의 클래스로 분리해서 생성해도 상관없습니다. 

public static String buildJsonStr(final Object src) {
    try {
      ObjectMapper mapper = new ObjectMapper();
      AnnotationIntrospector sis = mapper.getSerializationConfig().getAnnotationIntrospector();
      AnnotationIntrospector ais = AnnotationIntrospectorPair.pair(sis, new MaskPropertyAnnotationIntrospector());

      return mapper
          .setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE)
          .setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY)
          .setAnnotationIntrospector(ais)

          .writeValueAsString(src);
    } catch (Exception e) {
      mLog.error("stackTrace:", e);
    }
    return null;
  }

  public static class MaskPropertyAnnotationIntrospector extends NopAnnotationIntrospector {
    private static final long serialVersionUID = 1L;

    @Override
    public Object findSerializer(Annotated am) {
      MaskLogging annotation = am.getAnnotation(MaskLogging.class);
      if (annotation != null) {
        return MaskPropertySerializer.class;
      }
      return null;
    }
  }

  public static class MaskPropertySerializer extends StdSerializer<String> implements ContextualSerializer {
    private static final long serialVersionUID = 1L;

    private String maskValue;

    public MaskPropertySerializer() {
      super(String.class);
    }

    public MaskPropertySerializer(String maskValue) {
      super(String.class);
      this.maskValue = maskValue;
    }

    @Override
    public void serialize(String value, JsonGenerator gen, SerializerProvider provider) throws IOException {
      gen.writeString(maskValue);
    }

    @Override
    public JsonSerializer<?> createContextual(SerializerProvider serializerProvider, BeanProperty beanProperty) throws JsonMappingException {
      String maskValue = null;
      MaskLogging ann = null;
      if (beanProperty != null) {
        ann =beanProperty.getAnnotation(MaskLogging.class);
      }
      if (ann != null) {
        maskValue = ann.value();
      }
      return new MaskPropertySerializer(maskValue);
    }
  }

 

이제 해당 메서드를 호출해 JSON로그를 출력해보면 아래와 같이
MaskLogging 어노테이션이 붙은 프러퍼티가 마스킹 처리된 결과를 확인할 수 있습니다.

 

아래의 JSON 전문은 visitorImage 프로퍼티에 MaskLogging 어노테이션을 적용한 결과입니다.

 

원본 JSON

- 로그에서는 출력할 필요가 없는 이미지파일 Base64 Encoding 값이 출력되고 있습니다. 

...
...
{"id":"1","visitorImage":"iVBORw0KGgoAAAANSUhEUgAAAIAAAACAEAYAAAHkqY0eAAAACXBIWXMAAAsTAAALEwEAmpwYAAAGc2lUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDAgNzkuMTYwNDUxLCAyMDE3LzA1LzA2LTAxOjA4OjIxICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHAasdfasdfasdf6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdEV2dD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlRXZlbnQjIiB4bWxuczpwaG90b3Nob3A9Imh0dHA6Ly9ucy5hZG9iZS5jb20vcGhvdG9zaG9wLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgKE1hY2ludG9zaCkiIHhtcDpDcmVhdGVEYXRlPSIyMDE4LTA3LTE0VDE2OjQ5OjQyKzA5OjAwIiB4bXA6TWV0YWRhdGFEYXRlPSIyMDE4LTA3LTE0VDE2OjQ5OjQyKzA5OjAwIiB4bXA6TW9kaWZ5RGF0ZT0iMjAxOC0wNy0xNFQxNjo0OTo0MiswOTowMCIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo4MzM5YTFjMS1mMmYxLTQwOTgtOTdkYS05NDljNzJiOTljMjIiIHhtcE1NOkRvY3VtZW5...prxtxAJGw3bJNTckZPBSyQxxAJMS3iygkG0unVwS9FBZhUK2wyLQmglmkByAIKUZ6AJFgW3DTeXMhRzzm/H2HCsG2zxWL0a3/PvVJs8M1trFnDwYX+Y90Q49lqzrApFlz7vzTijiAVLJxIxpqcbFpTQSzyCeAIKQYcQCCkGLEAUSCbWMAggDEAQhCipFBwFTSsiVG1a+4Eukc19/z8pzSDR0r7ubH/2tQ9/WN1bbf+sr+6ueq312bfNz/d4grYIiQLeIAIsG24JEUmIOkkFbEAUQC/7y4GWjzkHtvA6VpyTPdLzk+kvT7/pD5/P4WyNzccPoLbsQBpApqWJ06YR0AhcCyH3yyFBUhZYsD+GylaQ2yRRxAJJg65ZgiAd30ezT490rM6JEUpk+H/PWNSTmKTSICRQTeYAUFSP1tLGTQkGLURf7Xu5DvLIKcOgUGWZGQI7Zqgvp7cRJSAwt4c6fQaNNmQE5R9WlbIBd+xAEIQoqRdQCCkGLEAQhCihEHIAgpRhyAIKQYcQCCkGL+Dzt5DO0YYENkAAAAAElFTkSuQmCC"}]} 
...
...

 

적용 후 Json 

- @MaskLogging어노테이션이 적용된 프로퍼티의 값이 마스킹 처리되어 출력됩니다.  

...
...
{"id":"1","visitorImage":"*****"}]} 
...
...

 

설명은 길었지만 프로퍼티에 마스킹을 적용하는 방법은 매우 간단하며,
속성 값을 잘 이용하면 단순한 마스킹 기능 외에도
좀 더 다양한 방법으로 응용해서 사용할 수 있을 것 같습니다.

  • 네이버 블러그 공유하기
  • 네이버 밴드에 공유하기
  • 페이스북 공유하기
  • 카카오스토리 공유하기