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 등)에 정의해줘야한다.