Spring Boot 개발 중 학습이 필요한 내용을 정리하고,
트러블 슈팅 과정을 기록하는 포스팅입니다.
이번에 진행하는 프로젝트에서 AWS NosqlDB
인 DynamoDB
를 활용합니다.
AWS 콘솔 기반으로 편리하게 관리할 수 있으며, Java SDK
도 매우 잘 구현돼있고 관련 Document도 많기 때문에 편리하게 활용수 있습니다.
이번 이슈는 DynamoDB
를 활용한 간단한 Read
작업시 발생합니다.
아래 메소드는 DynamoDB
에서 TextMemoStateLatest
를 Read
하는 부분입니다.
DynamoDBMapper
를 활용하면 Entitiy
별로 자동 매핑돼서 객체가 생성되기 때문에 간단한 CRUD 작업
이 가능합니다.
public TextMemoStateLatest getLatestFromDynamo(String individualVideoId) {
TextMemoStateLatest textMemoStateLatest = dynamoDBMapper.load(TextMemoStateLatest.class, UUID.fromString(individualVideoId));
return textMemoStateLatest;
}
위의 getLatestFromDynamo Method
에서 활용되는 Entity Class
입니다.
해당 Entitiy
는 TextMemoState Entity
를 상속 받습니다.
@SuperBuilder
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@DynamoDBTable(tableName = "text_memo_state_latest")
public class TextMemoStateLatest extends TextMemoState{
@DynamoDBHashKey(attributeName = "individual_video_id")
private UUID individualVideoId;
public TextMemoStateLatest(String id, UUID individualVideoId, String stateJson, LocalTime videoTime, LocalDateTime createdAt) {
super(id, individualVideoId, stateJson, videoTime, createdAt);
this.individualVideoId = individualVideoId;
}
}
위의 TextMemoStateLatest Entity
의 부모 Entity
입니다.
일반적인 상속 관계라고 볼 수 있습니다.
@SuperBuilder
@Getter
@NoArgsConstructor()
@DynamoDBTable(tableName = "text_memo_state_history")
public class TextMemoStateHistory extends TextMemoState{
//.. 중략
// @Builder
public TextMemoStateHistory(String id, UUID individualVideoId, String stateJson, LocalTime videoTime, LocalDateTime createdAt) {
super(id, individualVideoId, stateJson, videoTime, createdAt);
this.id = id;
this.individualVideoId = individualVideoId;
}
}
위의 getLatestFromDynamo Method
에서 DynamoDBMapper
를 활용한 load
시 다음과 같은 문제가 발생합니다.
해당 문제는 DynamoDBMapper
가 DynamoDB
의 형식으로부터 객체를 매핑하여 생성할 때,
생성자 접근 레벨이 AccessLevel.PROTECTED
로 돼있으면 접근하지 못하기 때문에 발생하는 오류였습니다. 단순히 Entitiy Level
의 생성자 AccessLevel
을 바꿔주면 해결되는 간단한 문제였습니다.
java.lang.IllegalAccessException: class com.amazonaws.services.dynamodbv2.datamodeling.StandardBeanProperties$DeclaringReflect cannot access a member of class com.chicplay.mediaserver.domain.individual_video.domain.TextMemoStateHistory with modifiers "protected"
at java.base/jdk.internal.reflect.Reflection.newIllegalAccessException(Reflection.java:361) ~[na:na]
at java.base/jdk.internal.reflect.Reflection.ensureMemberAccess(Reflection.java:99) ~[na:na]
at java.base/java.lang.Class.newInstance(Class.java:579) ~[na:na]
at com.amazonaws.services.dynamodbv2.datamodeling.StandardBeanProperties$DeclaringReflect.newInstance(StandardBeanProperties.java:183) ~[aws-java-sdk-dynamodb-1.12.289.jar:na]
at com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapperTableModel.unconvert(DynamoDBMapperTableModel.java:261) ~[aws-java-sdk-dynamodb-1.12.289.jar:na]
at com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapper.privateMarshallIntoObject(DynamoDBMapper.java:472) ~[aws-java-sdk-dynamodb-1.12.289.jar:na]
at com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapper.marshallIntoObjects(DynamoDBMapper.java:500) ~[aws-java-sdk-dynamodb-1.12.289.jar:na]
at com.amazonaws.services.dynamodbv2.datamodeling.PaginatedQueryList.<init>(PaginatedQueryList.java:65) ~[aws-java-sdk-dynamodb-1.12.289.jar:na]
at com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapper.query(DynamoDBMapper.java:1619) ~[aws-java-sdk-dynamodb-1.12.289.jar:na]
생성자 AccessLevel
을 수정하고 나서도 아래와 같이 DynamoDB
에서 Read
를 해오지 못했습니다.
java.lang.NullPointerException: null
at com.amazonaws.services.dynamodbv2.datamodeling.StandardBeanProperties$MethodReflect.set(StandardBeanProperties.java:133) ~[aws-java-sdk-dynamodb-1.12.289.jar:na]
at com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapperFieldModel.set(DynamoDBMapperFieldModel.java:111) ~[aws-java-sdk-dynamodb-1.12.289.jar:na]
at com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapperFieldModel.unconvertAndSet(DynamoDBMapperFieldModel.java:164) ~[aws-java-sdk-dynamodb-1.12.289.jar:na]
at com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapperTableModel.unconvert(DynamoDBMapperTableModel.java:267) ~[aws-java-sdk-dynamodb-1.12.289.jar:na]
at com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapper.privateMarshallIntoObject(DynamoDBMapper.java:472) ~[aws-java-sdk-dynamodb-1.12.289.jar:na]
at com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapper.marshallIntoObjects(DynamoDBMapper.java:500) ~[aws-java-sdk-dynamodb-1.12.289.jar:na]
at com.amazonaws.services.dynamodbv2.datamodeling.PaginatedQueryList.<init>(PaginatedQueryList.java:65) ~[aws-java-sdk-dynamodb-1.12.289.jar:na]
at com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapper.query(DynamoDBMapper.java:1619) ~[aws-java-sdk-dynamodb-1.12.289.jar:na]
at com.amazonaws.services.dynamodbv2.datamodeling.AbstractDynamoDBMapper.query(AbstractDynamoDBMapper.java:285) ~[aws-java-sdk-dynamodb-1.12.289.jar:na]
at com.chicplay.mediaserver.domain.individual_video.dao.TextMemoStateDao.getHistoryListFromDynamo(TextMemoStateDao.java:212) ~[main/:na]
분명 AWS 공식 Documentation와 똑같이 구현했는데,, 왜!! 도대체 왜!! 안되는 것인지ㅠㅠ
오류 메세지도 단순히 NullPointerException
으로만 뜨고 제대로된 오류 메세지가 없었습니다.
분명 제대로 DB단에서 데이터를 받아오기는 하는데, 매핑하여 Object
화하는 부분에서 문제가 있을 것이라고 생각했습니다.
하지만, 현재 Entity
클래스가 상속 관계로도 얽혀 있고, 다양한 롬복의 어노테이션 또한 DynamoDB
뿐만 아니라 Redis
관련 코드들도 있어서 어떤 부분이 문제인지 정확하게 파악하기 힘들었습니다..
그 이후로
(1) Redis
관련 코드를 제외하여 Entity
를 분리해보고
(2) 상속 관계
를 없애보고,
(3) Entitiy
구조를 바꿔보고(해시키 변경, 멤버 변경)을 시도했지만,
여전히 똑같은 오류가 발생했습니다.🤮🤮🤮
그러다가 우아한 형제들 기술 블로그 DynamoDB 관련 포스팅을 보게 됐습니다..
솔루션은 간단합니다!!!
DynamoDB Entitiy
들은 Setter
가 반드시 필요합니다!!!
아래는 AWS 공식 Documentation의 일부입니다! Setter
을 구현을 해주는 것을 볼 수 있습니다.
또 아래는 우아한 형제들 기술 블로그 DynamoDB 관련 포스팅의 일부입니다! 롬복의 @Setter 어노테이션
을 활용하고..
손수 주석으로 //Setters are used in aws-dynamodb-sdk 라고 적혀있는 것을 볼 수 있습니다..!
Setter
을 달아 줍시다...ㅎㅎ Entity에 상속 관계가 있기 때문에,
부모, 자식 클래스 둘다 달아주면 됩니다.
@Getter
@Setter // used in com.amazonaws.services.dynamodbv2
@RedisHash(value = "text_memo_state")
@DynamoDBDocument
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@SuperBuilder
public class TextMemoState {
@Id
@Indexed
@DynamoDBIgnore
private String id;
// .. 중략
@SuperBuilder
@Getter
@Setter // used in com.amazonaws.services.dynamodbv2
@NoArgsConstructor()
@DynamoDBTable(tableName = "text_memo_state_latest")
public class TextMemoStateLatest extends TextMemoState{
@DynamoDBHashKey(attributeName = "individual_video_id")
private UUID individualVideoId;
public TextMemoStateLatest(String id, UUID individualVideoId, String stateJson, LocalTime videoTime, LocalDateTime createdAt) {
super(id, individualVideoId, stateJson, videoTime, createdAt);
this.individualVideoId = individualVideoId;
}
}
시간을 꽤나 걸렸습니다.. 여러가지로 얽혀있는 Entity이다 보니 다양한 방향으로 트러블 슈팅
을 시도했던 것 같습니다. 하지만 결국 해결은 @Setter 메소드를 붙이는 아주 간단한 작업이었습니다..
Entity를 구현할 때, Setter 메소드
를 구현하는 것은 지양
해야합니다. 객체의 일관성
을 유지하기 힘들기 때문입니다.
하지만, DynamoDB 공식 Docs에서도 Entity
구현 시 Setter 메소드
를 필수적으로 사용합니다. 이는 DynamoDB Java SDK에서 DynamoDB
를 편리하게 다루라고 제공하는 DynamoDBMapper
가 객체 매핑 시 Setter을 사용하기 때문입니다!
Setter 메소드를 구현해 놓지 않고 매핑 작업시, 자동으로 매핑 되지 않기 때문에 아예 적절한 객체가 생성되지 않고 그렇기 때문에 계속해서 Read 작업시 Null을 반환한 것 입니다.
조금 더 친절한 오류 메세지가 있었으면... 이라는 아주 작은 불평도 있었지만, 공식 Docs를 조금더 분석해보지 못한 잘못도 큰 것 같습니다ㅠㅠ
다음부터 트러블 슈팅 과정에서는 Best Practice
를 조금더 꼼꼼히 분석하고 적용시켜 봐야겠습니다! 그게 결국에는 시간을 더 아낄 수 있는 방법인 것 같습니다!