[Spring boot] DynamoDB Read시 객체 매핑 이슈

Byuk_mm·2022년 9월 4일
0

Spring Boot Development

목록 보기
5/13
post-thumbnail

Spring Boot 개발 중 학습이 필요한 내용을 정리하고,
트러블 슈팅 과정을 기록하는 포스팅입니다.




✅ Background

이번에 진행하는 프로젝트에서 AWS NosqlDBDynamoDB를 활용합니다.

AWS 콘솔 기반으로 편리하게 관리할 수 있으며, Java SDK도 매우 잘 구현돼있고 관련 Document도 많기 때문에 편리하게 활용수 있습니다.

이번 이슈는 DynamoDB를 활용한 간단한 Read 작업시 발생합니다.


📌 GetLatestFromDynamo Method

아래 메소드는 DynamoDB에서 TextMemoStateLatestRead하는 부분입니다.

DynamoDBMapper를 활용하면 Entitiy 별로 자동 매핑돼서 객체가 생성되기 때문에 간단한 CRUD 작업이 가능합니다.

public TextMemoStateLatest getLatestFromDynamo(String individualVideoId) {

    TextMemoStateLatest textMemoStateLatest = dynamoDBMapper.load(TextMemoStateLatest.class, UUID.fromString(individualVideoId));

    return textMemoStateLatest;
}

📌 TextMemoStateLatest Entity

위의 getLatestFromDynamo Method에서 활용되는 Entity Class입니다.
해당 EntitiyTextMemoState 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;
    }
}

📌 TextMemoStateHistory Entity

위의 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;
    }
}



✅ Problem


📌 @NoargsConstructor(AccessLevel.PROTECTED) 사용 문제

위의 getLatestFromDynamo Method에서 DynamoDBMapper를 활용한 load시 다음과 같은 문제가 발생합니다.

해당 문제는 DynamoDBMapperDynamoDB의 형식으로부터 객체를 매핑하여 생성할 때,
생성자 접근 레벨이 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]


📌 Read가 안되는 이슈 (뭐가... 문제야..??)

생성자 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 관련 포스팅을 보게 됐습니다..




✅ Solution

솔루션은 간단합니다!!!
DynamoDB Entitiy들은 Setter가 반드시 필요합니다!!!

아래는 AWS 공식 Documentation의 일부입니다! Setter을 구현을 해주는 것을 볼 수 있습니다.


또 아래는 우아한 형제들 기술 블로그 DynamoDB 관련 포스팅의 일부입니다! 롬복의 @Setter 어노테이션을 활용하고..
손수 주석으로 //Setters are used in aws-dynamodb-sdk 라고 적혀있는 것을 볼 수 있습니다..!

Setter을 달아 줍시다...ㅎㅎ Entity에 상속 관계가 있기 때문에,
부모, 자식 클래스 둘다 달아주면 됩니다.

📌 TextMemoStateLatest 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;
    
    // .. 중략

📌 TextMemoStateHistory Entity

@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를 조금더 꼼꼼히 분석하고 적용시켜 봐야겠습니다! 그게 결국에는 시간을 더 아낄 수 있는 방법인 것 같습니다!

profile
어디야 벽벽 / 블로그 이전 -> byuk.dev

0개의 댓글