스프링부트

spring data MongoDB - 필드 변환하기 (Converter)

알쓸개잡 2024. 2. 4.

이번 포스팅에서는 MongoDB 에서 특정 필드 타입에 대해서 값을 변환하거나 지정된 필드에 대해서만 값을 변환하는 방법과 함께 key 필드에 도트 문자('.')가 포함된 경우 에러가 발생하지 않도록 하는 방법에 대해서 소개하고자 한다.

 

Document key 필드에 도트 문자가 포함된 경우 처리

MongoDB의 Document key 필드에 도트 문자가 포함된 경우 다음과 같은 에러를 만날 수 있다.

Map key {필드명} contains dots but no replacement was configured! Make sure map keys don't contain dots in the first place or configure an appropriate replacement!

위 에러를 피하는 방법은 key에 도트 문자가 포함된 경우 해당 도트 문자를 특정 문자열로 치환하여 저장하고, 치환된 문자열을 다시 도트 문자로 읽도록 하는 것이다.

 

MappingMongoConverter 빈에 도트 치환 문자열 등록

MappingMongoConverter 빈을 생성하여 해당 빈에 도트 문자를 치환할 문자열을 등록하면 된다.

@Bean
public MappingMongoConverter mongoConverter( MongoDatabaseFactory mongoDbFactory, MongoMappingContext mongoMappingContext ) {
    DbRefResolver dbRefResolver = new DefaultDbRefResolver( mongoDbFactory );
    MappingMongoConverter mongoConverter = new MappingMongoConverter( dbRefResolver, mongoMappingContext );
    mongoConverter.setMapKeyDotReplacement( "#DOT#" );
    return mongoConverter;
}

setMapKeyDotReplacement 메서드를 통해서 필드 키에 포함된 도트 문자를 변환할 문자열을 등록할 수 있다.

위와 같이 등록하면 Document에 저장 시 필드 key에 포함된 도트 문자는 #DOT# 으로 치환되어 저장되며 데이터를 읽을 때는 #DOT# 문자열이 도트 문자로 치환된다.

AnimalDocument animalDocument = AnimalDocument.builder()
    .animalId( UUID.randomUUID() )
    .animalNation( "korea" )
    .animalName( "happy" )
    .animalAge( 10 )
    .animalGender( Gender.MALE )
    .mapData( Map.ofEntries(
            Map.entry( "attribute.1", "성격이 좋음" ),
            Map.entry( "attribute.2", "털이 잘빠짐")) )
    .build();

위와 같은 AnimalDocument를 저장시에 다음과 같이 저장된다.

{
  "_id": {
    "$binary": {
      "base64": "cu/P9jGQR+igoMd5hvW2ZQ==",
      "subType": "04"
    }
  },
  "animalName": "happy",
  "animalAge": 10,
  "animalGender": "남성",
  "animalNation": "korea",
  "mapData": {
    "attribute#DOT#1": "성격이 좋음",
    "attribute#DOT#2": "털이 잘빠짐"
  },
  "_class": "com.example.springmongodb.documents.AnimalDocument"
}

 

Type Based Converter

지정된 특정 타입의 필드에 대해서 Converter를 등록하여 사용할 수 있다.

샘플 코드를 통해서 동작을 알아보자.

샘플코드

@Getter
public enum Gender {
    NONE("성별 없음"),
    MALE("남성"),
    FEMALE("여성"),
    ;

    final String kr;
    Gender( String kr ) {
        this.kr = kr;
    }

    public static Optional<Gender> genderByKr( String kr) {
        return Arrays.stream( Gender.values() )
                .filter( gender -> gender.getKr().equals( kr ) )
                .findFirst();
    }
}
/**
 * TypeConverter 는 org.springframework.core.convert.converter.Converter 인스턴스를 구현한다.
 * mongodb 에서 데이터를 읽을 때 String 타입의 데이터를 Gender 타입의 데이터로 변환한다.
 */
@ReadingConverter
public class ReadGenderTypeConverter implements Converter<String, Gender> {

    @Override
    public Gender convert( String gender ) {
        Optional<Gender> optionalGender = Gender.genderByKr( gender );
        return optionalGender.orElse( Gender.NONE );
    }
}

@ReadingConverter 어노테이션을 통해서 Document를 읽을 때 동작하는 Converter임을 나타낸다. 

성별에 대한 한글 문구를 그에 맞는 Gender 객체로 변환한다.

/**
 * TypeConverter 는 org.springframework.core.convert.converter.Converter 인스턴스를 구현한다.
 * mongodb 에서 데이터를 쓸 때 Gender 타입의 데이터를 String 타입의 데이터로 변환한다.
 */
@WritingConverter
public class WriteGenderTypeConverter implements Converter<Gender, String> {
    @Override
    public String convert( Gender source ) {
        return source.getKr();
    }
}

@WritingConverter 어노테이션을 통해서 Document로 저장할 때 동작하는 Converter임을 나타낸다.

Gender 타입에 대해서 각 성별의 한글 문구로 변환해서 저장하도록 한다.

 

Configuration

@Bean
public MappingMongoConverter mongoConverter( MongoDatabaseFactory mongoDbFactory, MongoMappingContext mongoMappingContext ) {
    DbRefResolver dbRefResolver = new DefaultDbRefResolver( mongoDbFactory );
    MappingMongoConverter mongoConverter = new MappingMongoConverter( dbRefResolver, mongoMappingContext );
    mongoConverter.setMapKeyDotReplacement( "#DOT#" );
    mongoConverter.setCustomConversions( customConversions() );
    return mongoConverter;
}

...

@Bean
public MongoCustomConversions customConversions() {
    return MongoCustomConversions.create(
        adapter -> {
            adapter.registerConverter( new ReadGenderTypeConverter() );
            adapter.registerConverter( new WriteGenderTypeConverter() );
        }
    );
}

ReadGenderTypeConverter, WriteGenderTypeConverter를 MongoCustomConversions 인스턴스에 등록하고 MongoCustomConversions 인스턴스를 MappingMongoConverter 빈에 세팅한다.

 

저장된 Document는 다음과 같이 저장될 것이다.

{
  "_id": {
    "$binary": {
      "base64": "wTiw+MfgQ5GDrAkyJo1NbQ==",
      "subType": "04"
    }
  },
  "personName": "test-name",
  "personAge": 100,
  "personGender": "성별 없음",
  "personNation": "Korea",
  "_class": "com.example.springmongodb.documents.PersonDocument"
}
{
  "_id": {
    "$binary": {
      "base64": "lynEylkQRV2bNKRLQHGOcw==",
      "subType": "04"
    }
  },
  "personName": "test-name2",
  "personAge": 50,
  "personGender": "남성",
  "personNation": "Korea",
  "_class": "com.example.springmongodb.documents.PersonDocument"
}

 

 

Typed Based Converter의 경우에는 모든 지정된 타입에 대해서 변환을 수행한다.

예를 들어 PersonDocument, AnimalDocument에 모두 Gender 타입의 필드가 정의되어 있는 경우 두 개의 Document 모두에서 Gender 타입 필드에 대해서 변환이 수행된다. 만일 PersonDocument에 대해서만 Gender 타입 필드에 대해서 변환을 수행하고자 한다면 Property Converter를 사용한다.


Property Converter

Type Based Converter와 달리 특정 Document에 있는 타입 혹은 필드에 대해서만 변환을 수행하고자 할 때는 Property Converter를 사용한다.

Property Converter를 등록하는 방법은 다음과 같다.

  • @ValueConverter 어노테이션을 이용한 등록
  • PropertyValueConverterRegistrar를 이용한 등록

PropertyConverter는 MongoValueConverter를 구현체로 하는 클래스이다.

다음 PropertyConverter는 지정된 필드에 대해서 base64로 인코딩하여 데이터를 저장하고 base64 디코딩하여 데이터를 읽어오도록 한다.

public class CommentPropertyConverter implements MongoValueConverter<String, String> {

    @Override
    public String read( String value, MongoConversionContext context ) {
        return new String(Base64.decodeBase64( value ));
    }

    @Override
    public String write( String value, MongoConversionContext context ) {
        return new String(Base64.encodeBase64( value.getBytes(), false ));
    }
}

 

@ValueConverter를 이용한 Converter 등록

@ValueConverter를 이용한 등록은 구현 Converter를 변환하고자 하는 엔티티의 필드에 @ValueConverter 어노테이션을 이용하여 지정해 주기만 하면 된다.

 

@Document(collection = "person")
@Getter
@ToString
@CompoundIndexes(value = {
        @CompoundIndex(name = "GenderNationIdx", def = "{'personGender': 1, 'personNation': -1}" )
})
public class PersonDocument {
    @Id
    private final UUID personId;
    //spring.data.mongodb.auto-index-creation 설정이 true인 경우 @Indexed로 지정된 컬럼에 index가 생성된다.
    @Indexed
    private final String personName;
    private final Integer personAge;
    @Setter private Gender personGender;
    @Setter private String personNation;
    //comment 필드는 CommentPropertyConverter에 의해서 변환된다.
    @ValueConverter( CommentPropertyConverter.class )
    private final String comment;


    @Builder
    public PersonDocument( UUID personId,
                           String personName,
                           Integer personAge,
                           Gender personGender,
                           String personNation,
                           String comment) {
        this.personId = personId;
        this.personName = personName;
        this.personAge = personAge;
        this.personGender = personGender;
        this.personNation = personNation;
        this.comment = comment;
    }
}

@ValueConverter(CommentPropertyConverter.class) 어노테이션을 변환하고자 하는 필드에 지정하여 적용한다.

저장되는 Document는 다음과 같이 comment 필드에 base64 인코딩 된 데이터가 저장된다.

{
  "_id": {
    "$binary": {
      "base64": "dtpgrssIQ6aYMITucWHCjA==",
      "subType": "04"
    }
  },
  "personName": "test-name2",
  "personAge": 50,
  "personGender": "남성",
  "personNation": "Korea",
  "comment": "dGVzdC1uYW1lMiDsvZTrqZjtirg=",
  "_class": "com.example.springmongodb.documents.PersonDocument"
}

 

PropertyValueConverterRegistrar을 통한 Property Converter 등록

PropertyValueConverterRegistrar를 사용하여 엔티티 모델 내의 프로퍼티에 대한 PropertyValueConverter 인스턴스를 등록한다. @ValueConverter를 이용하여 Converter를 등록하는 방식과의 차이점은 엔티티 모델 외부에서 완전히 이루어진다는 점이다.

이러한 접근 방식은 엔티티 모델에 @ValueConverter 어노테이션을 추가할 수 없거나 추가하고 싶지 않은 경우에 유용하다.

 

Configuration 클래스에서 PropertyValueConverterRegistrar을 통해서 등록할 수 있다.

@Bean
public MappingMongoConverter mongoConverter( MongoDatabaseFactory mongoDbFactory, MongoMappingContext mongoMappingContext ) {
    DbRefResolver dbRefResolver = new DefaultDbRefResolver( mongoDbFactory );
    MappingMongoConverter mongoConverter = new MappingMongoConverter( dbRefResolver, mongoMappingContext );
    mongoConverter.setMapKeyDotReplacement( "#DOT#" );
    mongoConverter.setCustomConversions( customConversions() );
    return mongoConverter;
}


@Bean
public MongoCustomConversions customConversions() {
    return MongoCustomConversions.create(
        adapter -> {
            //property converter 등록
            //PersonDocument 클래스의 comment 필드에 대해서 CommentPropertyConverter가 적용되도록 한다.
            adapter.configurePropertyConversions(
                registrar -> registrar.registerConverter(
                    PersonDocument.class,
                    "comment",
                    new CommentPropertyConverter() ) );
            //type converter 등록
            //Gender 타입에 대해서 ReadGenderTypeConverter, WriteGenderTypeConverter가 적용된다.
            adapter.registerConverter( new ReadGenderTypeConverter() );
            adapter.registerConverter( new WriteGenderTypeConverter() );
        }
    );
}
PropertyConverter를 등록할 때 프로퍼티를 하위 문서로 구분하기 위한 점 표기법 (예: registerConverter(Person.class, "address.street",...) 은 지원하지 않는다.
public class PersonDocument {
    @Id
    private final UUID personId;
    //spring.data.mongodb.auto-index-creation 설정이 true인 경우 @Indexed로 지정된 컬럼에 index가 생성된다.
    @Indexed
    private final String personName;
    private final Integer personAge;
    @Setter private Gender personGender;
    @Setter private String personNation;
    private final String comment;
    ...
}

comment 필드에 지정되었던 @ValueConverter 어노테이션을 제거한다.

 

저장되는 Document는 다음과 같이 comment 필드에 base64 인코딩 된 데이터가 저장된다.

 

{
  "_id": {
    "$binary": {
      "base64": "1P/ggdhmQTuRRuZM+4rdiw==",
      "subType": "04"
    }
  },
  "personName": "test-name2",
  "personAge": 50,
  "personGender": "남성",
  "personNation": "Korea",
  "comment": "dGVzdC1uYW1lMiDsvZTrqZjtirg=",
  "_class": "com.example.springmongodb.documents.PersonDocument"
}

 

지금까지 Spring Data MongoDB에서 타입 기반 데이터 변환, 특정 필드에 대해서 데이터를 변환하는 방법을 알아보았다.

더불어 필드의 key에 도트 문자가 포함된 경우에 에러가 발생하지 않도록 처리하는 방법에 대해서도 알아보았다.

 


샘플코드 gitlab 링크


참고링크

https://docs.spring.io/spring-data/data-mongodb/reference/mongodb/mapping/custom-conversions.html

https://docs.spring.io/spring-data/data-mongodb/reference/mongodb/mapping/property-converters.html

댓글

💲 추천 글