12월 프로젝트(https://velog.io/@dvmflstm/12%EC%9B%94-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-ticket-reservation) 에서는 dynamoDB를 사용해보기로 결정했고, 그에 따라 kotlin 및 aws sdk를 활용해 dynamodb를 조작하고 조회하는 방법을 실습해 보았다. dynamoDB에 대한 이론적인 내용은 AWS docs를 많이 참고하여 학습했다.
kotlin(또는 java) 환경에서 dynamoDB와 소통하는 방법은 크게 2가지이다.
dbMapper를 사용하지 않는 경우, raw하게 원하는 동작에 대응하는 request 객체를 만들어서 DynamoDBClient로 request를 날려주면 된다. 특정 table로부터 원하는 item을 가져오는 getItemRequest에 대한 예시는 다음과 같다. (해당 테이블은 생성되어있음)
GetItem.kt
//getitem : 지정해준 table과 key에 해당하는 item을 가져옴
fun main() {
val tableName = "test_table"
val name = "geunyoung"
val key = HashMap<String, AttributeValue>()
key["Name"] = AttributeValue(name)
//request 객체 생성
val request = GetItemRequest()
.withKey(key)
.withTableName(tableName)
//dynamoDB client 생성
val ddb: AmazonDynamoDB = AmazonDynamoDBClientBuilder.standard()
.withRegion(Regions.AP_NORTHEAST_2)
.build()
try {
// getItemRequest 전송
val item = ddb.getItem(request).item
if (item != null) {
val keys: Set<String> = item.keys
for (key in keys) {
println("$key, ${item[key].toString()}")
}
} else {
println("No item found with the key $name")
}
} catch (e: AmazonServiceException) {
println(e.errorMessage)
}
}
dynamoDB는 serverless한 cloud storage이기 때문에 별다른 endpoint도 없고 특별한 db connector 설정 따위도 필요없다. 그냥 client를 하나 만들어 적절한 request를 던져주면 끝!
위의 방식은 db 스키마가 비교적 단순할 때는 썩 괜찮아 보이지만, db 스키마가 복잡해지고 날릴 request의 요구사항이 복잡해질 경우에는 Request 객체를 생성하는 작업이 꽤 번거로워질 것이라고 예상할 수 있다. 이런 경우에는 내가 원하는 Request 객체를 손쉽게 만들어주고, db item을 kotlin 객체로 변환도 쉽게 할 수 있도록 해주는 dbMapper를 사용할 수 있다.
val ddb: AmazonDynamoDB = AmazonDynamoDBClientBuilder.standard()
.withRegion(Regions.AP_NORTHEAST_2)
.build()
val ddbMapper = DynamoDBMapper(ddb)
위와 같은 방식으로 손쉽게 DBMapper 인스턴스를 생성할 수 있다. 그리고 이 dbMapper는 @DynamoDBTable annotation의 도움을 받는 table 객체와 함께 사용하면 훨씬 다채롭게 활용이 가능하다. 아래와 같은 예시 table 객체를 생성해 주었다.
Event.kt
@DynamoDBTable(tableName = "event")
data class Event(
@DynamoDBHashKey(attributeName = "event_id")
@DynamoDBTyped(DynamoDBMapperFieldModel.DynamoDBAttributeType.S)
var event_id: String,
@DynamoDBRangeKey(attributeName = "event_name")
@DynamoDBTyped(DynamoDBMapperFieldModel.DynamoDBAttributeType.S)
var event_name: String,
@DynamoDBAttribute(attributeName = "last_login_datetime")
@DynamoDBTyped(DynamoDBMapperFieldModel.DynamoDBAttributeType.S)
var lastLoginDatetime: LocalDateTime? = null,
@DynamoDBAttribute(attributeName = "last_page_view_datetime")
@DynamoDBTyped(DynamoDBMapperFieldModel.DynamoDBAttributeType.S)
var lastPageViewDatetime: LocalDateTime? = null,
@DynamoDBAttribute(attributeName = "last_reservation_datetime")
@DynamoDBTyped(DynamoDBMapperFieldModel.DynamoDBAttributeType.S)
var lastReservationDatetime: LocalDateTime? = null
) {
constructor() : this(event_id = "", event_name = "")
}
event_id 필드를 Hash Key, event_name 필드를 Range Key로 설정하도록 annotation을 달아줄 수 있다. 이를 가지고 실제 db table을 생성하는 코드는 아래와 같다.
fun createEventTable(ddb: AmazonDynamoDB, ddbMapper: DynamoDBMapper) {
val request = ddbMapper.generateCreateTableRequest(Event::class.java)
.withProvisionedThroughput(ProvisionedThroughput(10, 10))
try {
ddb.createTable(request)
println("create event table success")
} catch (e: AmazonServiceException) {
println(e.errorMessage)
}
}
이전에는 hash key 및 range key 혹은 LSI/GSI에 대한 정보 등을 담아 수동으로 createTableRequest를 생성해 주어야 했는데, dbMapper를 통해 db Table 객체에 대응하는 createTableRequest를 자동으로 생성할 수 있어졌다! 훨씬 코드 가독성도 높아지고, 작성하기에도 편해진 것 같다. 위 코드를 실행한 결과 테이블이 잘 생성되었음을 AWS console에서 확인할 수 있다.
partition key와 sort key, RCU/WCU 등이 원하는 대로 잘 설정되어있다.
다음은 여기에 데이터를 넣고 partition key를 이용해 scan을 해볼 것이다.
item을 추가하는 것도 dbMapper의 save 함수를 이용하면 손쉽게 가능하다.
fun putItems(ddb: AmazonDynamoDB, ddbMapper: DynamoDBMapper, eventId: String) {
val event1 = Event(event_id = eventId, event_name = "START")
val event2 = Event(event_id = eventId, event_name = "RESERVATION")
val event3 = Event(event_id = eventId, event_name = "LOGIN")
val event4 = Event(event_id = eventId, event_name = "PAGE_VIEW")
val event5 = Event(event_id = eventId, event_name = "LOGOUT")
try {
ddbMapper.save(event1)
ddbMapper.save(event2)
ddbMapper.save(event3)
ddbMapper.save(event4)
ddbMapper.save(event5)
println("put items success")
} catch (e: AmazonServiceException) {
println(e.errorMessage)
}
}
save의 parameter로 들어가는 event 객체에는 table에 대한 정보부터 partition key / sort key 등 item을 추가하는 데 필요한 모든 정보들이 들어있으므로 매우 간단히 save operation을 실행할 수 있다.
이제 여러 데이터가 담겨 있는 이 테이블을 partition key를 이용해 scan해볼 것이다. partition key를 통한 search는 hash를 통해 이루어지므로 해당 작업은 O(1) 시간에 수행될 것으로 기대할 수 있다.
//partition key를 이용한 search
fun scanByPartition(ddb: AmazonDynamoDB, ddbMapper: DynamoDBMapper, eventId: String) {
val eav = HashMap<String, AttributeValue>()
eav[":eventId"] = AttributeValue(eventId)
val scanExpression = DynamoDBScanExpression()
.withFilterExpression("event_id = :eventId")
.withExpressionAttributeValues(eav)
val scanList = ddbMapper.scan(Event::class.java, scanExpression)
for(item in scanList) {
println("${item.event_id} , ${item.event_name}")
}
}
partition key를 통한 scan option을 주기 위해 filter expression을 사용하였다. 이 함수의 수행 결과는 (뻔하지만) 아래와 같다.
3 , LOGIN
3 , LOGOUT
3 , PAGE_VIEW
3 , RESERVATION
3 , START
Process finished with exit code 0