반응형

개요

spring data mongodb 의존성 사용시에 객체를 Mongodb 컬렉션의 문서로 저장할 때 두가지 방법이 있다.

 

1. MongoRepository 인터페이스를 extends한 Repository를 선언해 save(S entity) 메소드를 사용하는 방법

2. MongoTemplate 객체의 save(T objectToSave, String collectionName) 메소드를 사용하는 방법

 

내가 필요했던 것은 2번째 방법에서 객체를 저장시에 필요없는 필드를 제거하는 방법이었어서

그것에 대해서 한번 시도해보도록 한다.

 


Gradle 사용시 필요한 의존성

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-mongodb'
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
}

 

필자의 Spring Boot 버전은 2.1.6 버전을 사용하였다.


MongoTemplate Bean 정의하기

내가 Custom MongoTemplate을 정의한 이유는 두가지 때문이었다.

 

1. 자바 클래스 인스턴스 변수명은 camel case, mongodb 문서의 필드는 snake case로 사용하기

2. 자동으로 컬렉션에 패키지명을 포함한 entity _class 필드 들어가는 설정 제외하기

 

  • Mongodb에 대한 Configuration 클래스에 적용
    @Bean(name = "customMongoTemplate")
    public MongoTemplate customMongoTemplate() {
        SimpleMongoDbFactory mongoDbFactory = new SimpleMongoDbFactory(new MongoClient(new MongoClientURI(uri)), database);
        DbRefResolver dbRefResolver = new DefaultDbRefResolver(mongoDbFactory);
        MongoMappingContext mongoMappingContext = new MongoMappingContext();
        mongoMappingContext.setFieldNamingStrategy(new SnakeCaseFieldNamingStrategy());
        MappingMongoConverter converter = new MappingMongoConverter(dbRefResolver, mongoMappingContext);
        converter.setTypeMapper(new DefaultMongoTypeMapper(null));
        return new MongoTemplate(mongoDbFactory, converter);
    }

Entity 클래스 정의

  • 사람의 인적사항 및 소개 메세지를 담은 Entity가 있다고 가정해보자
    ※ 여기서 @Id 어노테이션은 org.springframework.data.annotation.Id임을 명심하자.
       java.persistance.Id를 사용하면 save 메소드 사용시 중복 체크가 제대로 되지 않아
       중복 저장시 upsert가 아닌 duplicate key error를 내게 된다. 
@Getter
@Setter
@Builder
@Document("user")
public class User {

  @Id
  private String userId;
  private String name;
  private String job;
  private int age;
  private double height;
  private double weight;
  private String introduceMessage;

}

Entity 저장 테스트코드 작성하기

실제 서비스 코드에서는 직접 MongoTemplate 객체를 스프링 컨테이너에 담아 사용하지 않고

Mocking하여 테스트하는 것이 좋다.

그렇지만 지금은 실제로 적재되는 문서를 확인하기 위해 컨테이너에

Bean을 생성하고 객체를 주입받아 테스트하기로 하자.

 

@DataMongoTest, @Import(config.class) 어노테이션을 사용하여 전체 context를 올리지 않고

Mongodb configuration 관련 Bean만 올려 테스트할 수 있다.

 

  • 소스를 까본것은 아니지만 save(S Entity) 메소드는 Entity 객체 클래스의
    @Document 어노테이션 collection 필드의 value를 가지고 인식하는듯 하다.
  • 환경별로 올려서 테스트하고 싶은 경우 @ActiveProfiles 어노테이션으로 필요한 properties를 설정하자.
@RunWith(SpringRunner.class)
@DataMongoTest
@Import(CustomMongoConfig.class)
public class UserTest {

  @Qualifier("customMongoTemplate")
  @Autowired
  private MongoTemplate mongoTemplate;

  private User user;

  @Before
  public void setup(){
    user = User.builder()
      .userId("1")
      .name("홍길동")
      .job("developer")
      .age(20)
      .height(170.5)
      .weight(60.1)
      .introduceMessage("저의 이름은 홍길동입니다. 저는 주니어 개발자입니다.")
      .build();
  }

  @Test
  public void saveTest(){
    mongoTemplate.save(user);
    //mongoTemplate.save(user,"user"); 와 동일함.
  }

}

결과


Entity에 새로운 요구사항을 정의하기

  • Entity 클래스의 필드가 여러개 존재하지만 필요한 필드들만
    저장한다는 의미를 부여하기 위해 시나리오를 만들어보자
저희는 이 지원자 정보들을 사용해 서류 합격자들을 선별하고자 합니다.
나이는 25세 이하, 키는 165센치미터 이상, 몸무게는 80kg 미만으로 합/불 여부를 판단하겠습니다.
지원자 정보는 다음 내용만 필요합니다. 
지원자 id, 이름, 직업, 합격 여부, 자기 소개서 글자 수
  • 이 요구사항을 통해 기존 Entity를 수정해보자.

Entity 다시 정의하고 테스트하기

@Getter
@Setter
@Builder
@Document("user")
public class User {

  @Id
  private String userId;
  private String name;
  private String job;
  private int age;
  private double height;
  private double weight;
  private String introduceMessage;
  private int introduceMessageLength;
  private boolean isPass;
  
  public void setIntroduceMessageLength(){
    this.introduceMessageLength = Optional.ofNullable(this.introduceMessage).map(String::length).orElse(0);
  }
  
  public void isPass(){
    this.isPass = age <= 25 && height >= 165.0 && weight < 80;
  }
  
}
  @Test
  public void saveTest(){
    user.setIntroduceMessageLength();
    user.isPass();

    mongoTemplate.save(user);
  }

결과

  • 추가적으로 필요한 필드 저장은 잘 되었지만, 기존의 필드들을 제거하고 저장하는 방법이 필요하다.

Mongodb 컬렉션으로 저장시 필요없는 필드 제외하기

mongodb는 schemaless한 key-value 기반의 nosql이다.

그래서, 실제로 save 메소드 호출을 해보면 호출 직전에
각 필드들의 value를 null로 지정하면 value가 없으므로 컬렉션에 저장되는 필드에서 제외된다.


이러한 요구사항의 Entity 클래스가 단 한개라면 null로 필드를 제외하는 방법을 사용하겠지만 

여러개가 존재한다면 더 좋은 방법이 있어야만 할 것 같다.

 

그래서 필자는 java annotation을 이용해 컬렉션에서 제외할 필드를 선택 해보기로 하였다.

 

  • 어노테이션 선언하기
@Documented
@Inherited
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface MongoJsonIgnore {

}

 

  • 제외할 필드 어노테이션 삽입하기
@Getter
@Setter
@Builder
@Document("user")
public class User {

  @Id
  private String userId;
  private String name;
  private String job;
  @MongoJsonIgnore
  private Integer age;
  @MongoJsonIgnore
  private Double height;
  @MongoJsonIgnore
  private Double weight;
  @MongoJsonIgnore
  private String introduceMessage;
  private int introduceMessageLength;
  private boolean isPass;

  public void setIntroduceMessageLength(){
    this.introduceMessageLength = Optional.ofNullable(this.introduceMessage).map(String::length).orElse(0);
  }

  public void isPass(){
    this.isPass = age <= 25 && height >= 165.0 && weight < 80;
  }

  public void setExceptFields() {
    for(java.lang.reflect.Field field : this.getClass().class.getDeclaredFields()){
      List<Annotation> annotations = Arrays.asList(field.getAnnotations());
      if(annotations.stream().anyMatch(annotation -> annotation.annotationType() == MongoJsonIgnore.class)){
        //primitive type은 null 설정 불가
        if(field.getClass().isPrimitive()) {
          continue;
        }
        field.setAccessible(true);
        try {
          field.set(this, null);
        } catch (IllegalAccessException e) {
          e.printStackTrace();
        }
      }
    }
  }

}
  • 여기서 주목할것은 제외 대상 필드가 primitive type인 경우는 value를 null로 설정할 수 없기 때문에
    wrapper type class로 변경해주야 한다는 것이다.

 

  • 이제 테스트 코드에서 save() 메소드 호출이전 setExceptFields() 메소드를 호출해주면 된다.
  @Test
  public void saveTest(){
    user.setIntroduceMessageLength();
    user.isPass();
    user.setExceptFields();

    mongoTemplate.save(user);
  }

 

save 테스트 코드 호출 결과

필요없는 필드 제외 성공!

  • 사용시 setExceptFields()를 구현한 클래스를 상속받아 Entity 클래스를 만들어서 저장 전 시점에
    이 메소드를 호출하면 될 것 같다.

마치며

  • Spring data mongodb 라이브러리는 rest api 반환 규격인 ResponseEntity를 만들때와 같이 Jackson 라이브러리를사용하지 않고 별도의 convertor를 사용한다.
  • MappingMongoConverter 클래스를 사용하는 것 같은데, setExceptFields() 메소드를 일일이 호출하는것은 비효율적인것 같으니, 이 클래스를 필드 제외 메소드를 포함해 직접 구현하여 Bean 생성시 사용하면 될 것 같긴 하다.
  • 또한, 이 구현 사항은 primitive type 변수는 저장시 제외하지 못하는 것도 한계점인 것 같다.
반응형