Spring Framework

Spring 에서 제공하는 MongoDB Annotation

흥부가귀막혀 2023. 4. 5. 12:17

개요

  • Application 에서 정의한 Model 객체가 어떻게 MongoDB 로 변환이 되어 들어가고, 조회시에는 어떻게 Model 객체로 맵핑이 되는지에 대한 궁금증에서 시작.
  • MappingMongoConverter 에서 이러한 기능을 담당하는거로 확인했지만, 이번 시간에는 이부분에 대해 깊게 파진 않고 이러한 Mapping 을 도와주는 Annotation 에 대해 알아보려고 한다.

@Document

  • Spring Data Mongodb 에서만 사용할 수 있는 Annotation
  • 어노테이션이 정의된 Class 를 사용할 MongoDB Collection 을 지정할 수 있다.
/**
 * 나는 base collection 에 들어갈거다!
 */
@Document("base")
public class BaseDocument {
    @Id
    private String documentId;
}

@Id

  • ObjectId 에 해당하는 필드를 지정할 경우 선언할 수 있다.
  • 해당 어노테이션이 없을 경우 필드 네이밍이 id 라고 선언한 필드를 ObjectId 로 인지한다.
  • id 이름으로 선언된 필드와 @Id 어노테이션이 지정된 필드가 있을경우 어노테이션을 선언한 필드를 ObjectId 필드로 인지한다.
  • @PersistenceConstructor 로 지정한 생성자에 id 값을 주입 안하더라도 MappingMongoConverter 에서 field injection 으로 값을 넣어준다.(이건 setter method 가 없어도 동일하다.)
  • 해당 어노테이션은 MongoDB 전용은 아니고 Spring Data 하위 프로젝트에서 모두 사용할 수 있다.

@Id annotation 을 사용할 경우

public class BaseDocument {
    @Id
    private String documentId;
}

@Id annotation 을 사용하지 않을 경우

public class BaseDocument {
    private String id;
}

setter method 가 없어도 주입된다.

public class BaseDocument {
    @Id
    private String id;

        public String getId() {
           return id;
        }
}

@PersistenceConstructor 를 사용할 때 굳이 id 값을 주입받지 않아도 된다.

public class BaseDocument {
    @Id
    private String id;
        private String name;

        @PersistenceConstructor
    BaseDocument(String name) {
        this.name = name;
    }
}

@PersistenceConstructor

  • MongoDB 에서 가져온 데이터(DBObject)를 Model Class 로 전환할 때 사용할 생성자를 지정한다.
  • 해당 어노테이션이 없을 경우 기본 생성자를 찾아서 사용한다.
  • 해당 어노테이션을 지정하는 생성자는 public 이 아니어도 된다. protected, private, package private 로 정의된 생성자 모두 사용 가능하다. 이게 가능한 이유는 역시 Java Reflection 때문.
  • 생성자에 정의된 각 argument 의 key 값이 db 필드의 key 값과 일치하면 주입해준다.
  • 해당 어노테이션은 MongoDB 전용은 아니고 Spring Data 하위 프로젝트에서 모두 사용할 수 있다.
public class BaseDocument {
    private String service;
    private Integer version;
    private String name;

    public BaseDocument(String service, Integer version) {
        this.service = service;
        this.version = version;
    }

    @PersistenceConstructor
        // 어노테이션을 지정했기 때문에 위에 선언한 생성자를 사용하지 않는다는데 내 맥북과 마우스 전부를 건다.
        // private 여도 이 생성자를 쓴다는거에 내 맥북과 마우스 전부를 건다.
    private BaseDocument(String service, Integer version, String name) {
        this.service = service;
        this.version = version;
        this.name = name;
    }
}

@CreatedDate, @LastModifiedDate

  • Document 의 생성시간, 변경 시간을 정의한 field 에 선언하면 알아서 날짜값을 만들어준다.
  • 단, 해당 어노테이션이 의도한대로 동작하기 위해서는 @EnableMongoAuditing, @EnableReactiveMongoAuditing 가 선언되야한다.
  • 단, save 를 통해 한번 저장된 data 를 또 save 하게 될 경우 의도한대로 동작하지 않는다. 한번 save 된 데이터일 경우 findAndModify 를 쓰거나 @Version Annotation 을 활용해야한다.
  • 해당 어노테이션은 MongoDB 전용은 아니고 Spring Data 하위 프로젝트에서 모두 사용할 수 있다.

@EnableMongoAuditing 혹은 @EnableReactiveMongoAuditing 을 선언하고

@SpringBootApplication
@EnableReactiveMongoAuditing
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

다음과 같이 class 를 선언하고

public class BaseDocument {
    @Id
    private String id;
    @CreatedDate
    private Date createTime;
    @LastModifiedDate
    private Date updateTime;

    public BaseDocument() {
    }

    @Override
    public String toString() {
        return ToStringBuilder.reflectionToString(this, ToStringStyle.NO_CLASS_NAME_STYLE);
    }
}

이렇게 코드를 짜서 보면

BaseDocument saveDocument = template.save(new BaseDocument());
System.out.println(saveDocument);

console 에 이렇게 찍힌다.

[mongod output] [id=63bfca54eb3c3c148edb42a9,createTime=Thu Jan 12 17:52:36 KST 2023,updateTime=Thu Jan 12 17:52:36 KST 2023]

@CreatedBy, @LastModifiedBy

  • Document 를 생성 및 변경에 대한 사용자 관련 field 에 해당 어노테이션을 선언하면 알아서 해당 값을 주입해준다.
  • 단, 해당 어노테이션이 의도한대로 동작하기 위해서는 @EnableMongoAuditing, @EnableReactiveMongoAuditing 가 선언되야한다.
  • 또한 생성할 사용자 정보를 전달해주기 위해 AuditorAware 혹은 ReactiveAuditorAware 를 정의해줘야한다.
  • 해당 어노테이션은 MongoDB 전용은 아니고 Spring Data 하위 프로젝트에서 모두 사용할 수 있다.

@EnableMongoAuditing 혹은 @EnableReactiveMongoAuditing 을 선언하고

@SpringBootApplication
@EnableReactiveMongoAuditing
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

AuditorAware 혹은 ReactiveAuditorAware 를 정의하고

@Component
public class UserAudtiting implements AuditorAware<String>{

    @Override
    public Optional<String> getCurrentAuditor() {
        return Optional.of("me");
    }

}

다음과 같이 class 를 선언하고

public class BaseDocument {
    @Id
    private String id;
    @CreatedBy
    private String createUser;
    @LastModifiedBy
    private Date updateUser;

    public BaseDocument() {
    }

    @Override
    public String toString() {
        return ToStringBuilder.reflectionToString(this, ToStringStyle.NO_CLASS_NAME_STYLE);
    }
}

이렇게 코드를 짜서 보면

BaseDocument saveDocument = template.save(new BaseDocument());
System.out.println(saveDocument);

console 에 이렇게 찍힌다.

[mongod output] [id=63bfca54eb3c3c148edb42a9,createUser=me,updateUser=me]

@Transient

  • DB 에 기록하지 않을 field 가 있을 경우 해당 어노테이션을 선언한다.
  • Spring Data Mongodb 는 기본적으로 field 에 정의된 데이터만 DB 에 기록하기 때문에 getter method 로 선언한 내용은 해당 어노테이션을 붙이지 않아도 된다.(어차피 DB 에 저장하지 않을것이기 때문)
  • 해당 어노테이션은 MongoDB 전용은 아니고 Spring Data 하위 프로젝트에서 모두 사용할 수 있다.

@Version

  • Document 의 version 값에 해당하는 field 가 있을 경우 해당 어노테이션을 선언하면 save 할 때 자동으로 version 값을 올려준다.
  • 해당 어노테이션을 둘 경우 save query 에 기존 version 값이 조건으로 추가되어 동시에 save 할 경우 벌어질 동시성 이슈를 막을 수 있다.(다만, 이건 로직적으로 잘 파악하고 사용해야할듯 하다.)

다음과 같이 class 를 선언하고

public class BaseDocument {
    @Id
    private String id;
    @Version
    private long version;

    public BaseDocument() {
    }

    @Override
    public String toString() {
        return ToStringBuilder.reflectionToString(this, ToStringStyle.NO_CLASS_NAME_STYLE);
    }
}

이렇게 코드를 짜서 보면

BaseDocument saveDocument = template.save(new BaseDocument());
System.out.println(saveDocument);
BaseDocument secondSaveDocument = template.save(saveDocument);
System.out.println(secondSaveDocument);

console 에 이렇게 찍힌다.

[mongod output] [id=63bfca54eb3c3c148edb42a9,version=1]
[mongod output] [id=63bfca54eb3c3c148edb42a9,version=2]

@Field

  • Spring Data Mongodb 에서만 사용할 수 있는 Annotation
  • DB 에 정의된 field 이름과 model 에 정의된 field 이름이 다를 경우 사용할 수 있다.
  • MongoDB 에서 Model Class 로 Mapping 할 때 Setter method 를 사용하고자 할때도 사용할 수 있다. 단, 이럴 경우 @AccessType(AccessType.Type.PROPERTY) 를 같이 선언해줘야 한다.

DB 에는 createTime, Model Class 에는 createDate 라고 정의했을 경우

public class BaseDocument {
    @Id
    private String id;
    @Field("createTime")
    private Date createDate;

    public BaseDocument() {
    }

    @Override
    public String toString() {
        return ToStringBuilder.reflectionToString(this, ToStringStyle.NO_CLASS_NAME_STYLE);
    }
}

setter 함수로 데이터 injection 을 할 경우

public class BaseDocument {
    @Id
    private String id;
    private NameCard nameCard = new NameCard();

    public BaseDocument() {
    }

        @AccessType(AccessType.Type.PROPERTY)
        @Field("name")
        public setPhone(String name) {
            nameCard.setName(name);
        }

        @AccessType(AccessType.Type.PROPERTY)
        @Field("phoneNumber")
        public setPhoneNumber(String phoneNumber) {
            nameCard.setPhoneNumber(phoneNumber);
        }

    @Override
    public String toString() {
        return ToStringBuilder.reflectionToString(this, ToStringStyle.NO_CLASS_NAME_STYLE);
    }
}

@TypeAlias

  • MongoDB 의 Collection 에는 다양한 유형의 Model Class 를 Document 로 포함할 수 있는데, 이렇게 저장된 객체를 조회할 때는 입력당시 사용한 Model Class 에 올바르게 Mapping 하는 것이 중요하다. 따라서 Document 를 저장할 때 함께 타입 정보를 저장하는 매커니즘을 사용하며, 이러한 역할에 도움을 주는 것이 바로 _class 필드다.
  • 하지만 이 기능의 치명적인 단점은, _class 로 기록된 타입값이 추후 변경되면 동일한 모델이더라도 올바르게 맵핑할 수 없다는 단점이 존재한다.(예를 들어 package 명이 변경되거나 Model Class 이름을 변경했을 경우)
  • 그래서 이런 불상사를 막기 위해 _class 에 기록할 특정 네이밍을 정하고자 한다면, @TypeAlias 를 사용하면 된다.

❓ @TypeAlias 가 동작하는 케이스는?

  • MongoTemplate 혹은 ReactiveMongoTemplate 에서 쿼리를 요청할 때 파라미터로 전달하는 Entity Class 가 Abstract class 혹은 Interface 일 경우 동작한다.
  • 만약 구현체 Class 를 Entity Class 로 넘겨줄 경우 _class 값을 통한 Model Class 맵핑은 무시되고 Entity Class 로 전달한 구현체 Class 에 맞춰 데이터가 맵핑된다.

❓ _class 가 Package 경로로 주입된 데이터의 모델에 @TypeAlias 를 넣으면 어떻게 될까?

  • 이미 package 경로로 _class 가 입력된 데이터에 대한 Model Class 에 @TypeAlias 로 다른 네이밍을 정의했을 경우에도 조회시 정상동작한다.
  • 즉, Spring Data 에서는 _class 값이 Model Class 의 Package 경로와 일치하거나 @TypeAlias 의 정의된 네이밍과 일치하면 정상적으로 맵핑시키는 구조로 보인다.

❓ 중간에 @TypeAlias 를 빼면 어떻게 될까?

  • _class 가 이미 @TypeAlias 에 지정된 값으로 입력되었다면 이후 @TypeAlias 를 제거하거나 네이밍을 변경할 경우 정상적으로 맵핑을 못시킨다.
  • 단, 이는 특정 field 가 Object 형태이고 해당 Object 내부에 _class 가 정의되었을경우 정상적으로 맵핑을 못시키지만, root 경로에 있는 _class 는 실제 Model Class 와 달라도 정상동작한다. 그 이유는 MongoTemplate 에서 findById 등으로 조회시 class 값을 넘겨주는데 이걸로 맵핑을 하기 때문에 root 경로에 있는 _class 가 달라도 정상적으로 맵핑이 가능하다.

❓ 특정 필드의 데이터를 Map 혹은 Object 로 조회하면 어떻게 될까?

  • 만약 @TypeAlias 로 선언된 class 가 있거나, _class 와 동일한 경로에 Model Class 가 존재할 경우 해당 class 로 맵핑시켜준다.

⚠️ @TypeAlias 사용시 주의사항

  • Spring 에서 Model Mapping 에 사용하는 MappingMongoConverter 에 @TypeAlias 어노테이션이 정의된 네임에 맵핑할 class 정보가 사전에 미리 셋팅 되어있어야 한다.
  • 정상적인 Spring Application 이라면 @EnableMongoRepositories 혹은 @EnableReactiveMongoRepositories 가 선언되어있을 경우 이를 scan 하고 자동으로 생성된 MappingMongoConverter 에 주입해주기 때문에 이슈가 없다.
  • 하지만 직접 MongoTemplate 혹은 MappingMongoConverter 를 생성해서 사용하게 될 경우 위의 scan 작업에 대한 결과가 주입되지 못하기 때문에 해당 class 에 대해 save query 를 수행해야지만 @TypeAlias 네임과 class 정보가 맵핑된다. 그전까지는 정상적으로 맵핑이 되지 않는다.

이렇게 TypeAlias 를 정의하고

/**
 * 나는 base collection 에 들어갈거다!
 */
@Document("base")
@TypeAlias("base")
public class BaseDocument {
    @Id
    private String id;
}

save 를 해보면?

{
  "_id":{
           "$oid":"63c001f341eb836dfd411888"
        },
  "_class":"base"
}

@DBRef

  • Spring Data Mongodb 에서만 사용할 수 있는 Annotation
  • MongoDB 에서 제공하는 DBRefs 를 지원하도록 만들어진 어노테이션
  • DBRefs 로 정의된 Data 일 경우 자동으로 해당 Collection 에 맞게 데이터를 조회하여 model 에 맵핑해준다.
  • 어노테이션의 db 속성값을 통해 다른 Database 에 들어간 데이터도 맵핑이 가능합니다.
  • lazy 속성값을 통해 field 에 접근하기 전까지 data 조회를 지연시킬 수 있습니다.(기본값은 false 라서 처음 mapping 과정에서 모든 객체를 로드합니다.)
  • 해당 어노테이션은 DBObject 에서 Model 객체로 Mapping 해주는 기능에만 사용하기 때문에 아쉽게도 save 시에는 자동으로 생성해주진 않는다.
  • 자세한 예제는 baeldung 참고

@EnableMongoAuditing, @EnableReactiveMongoAuditing

  • Spring Data Mongodb 에서만 사용할 수 있는 Annotation
  • Spring Data 에서 제공하는 Auditing 기능을 MongoDB 에 맞게 제공할 수 있도록 지원하는 Annotation
  • @CreatedBy, @LastModifiedBy, @CreatedDate, @LastModifiedDate 어노테이션을 사용할 경우 해당 annotation 을 특정 class(Configuration 혹은 Application Class 등)에 정의해줘야한다.