ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Spring-Data-DynamoDB를 사용하여 SpringBoot와 AWS DynamoDB 연동하기
    SpringBoot 2024. 1. 28. 23:39

    들어가며

    현재 진행하고 있는 실시간 채팅방 관련 프로젝트에서

    채팅방에서 나눈 이전 대화들을 저장하여 사용자가 다시 채팅방에 접속하면

    이전 대화 내용들을 보여주는 기능을 추가적으로 구현해야 했다.

     

    이를 위해서는 채팅방 메시지들을 저장하고 조회, 삭제를 수행하게 될 데이터베이스를 무엇을 사용해야 할지 선택해야 했다.

     

    💁‍♂️ RDB...? 아니면 NoSQL?? 💁‍♂️

    기존 데이터베이스는 RDB인 MySQL을 사용하고 있었는데, 원래 사용하던 MySQL을 사용할지

    아니면 채팅방 메시지 관리를 위한 추가적인 NoSQL DB를 사용할지에 대한 고민이 깊어졌다.

     

    그러나 NoSQL이 기존의 RDB보다 더 빠른 읽기 쓰기 성능을 가지고 있기 때문에,
    각 채팅방 별, 채팅 내용을 저장하고 불러오기에는 NoSQL을 추가적으로 사용하는게 성능상 더 효과적이라고 판단하였다.

    대표적인 NoSQL로 AWS DynamoDB와 MongoDB가 존재하는데 각자의 장단점이 존재하였고,

    같이 개발하는 멤버와의 논의 끝에 AWS DynamoDB를 사용하게 되었다.

     

    가격 측면에서 두 데이터베이스를 비교해보자면

     

    MongoDB의 경우 Serverless와 Dedicated, Shared에 따라서 가격이 다르게 측정되는데,

    Shared Cluster의 경우 512MB storage, shared RAM, shared vCPUs가 무료고

    2GB storage는 한달에 9달러, 5GB는 한달에 25달러 정도 하였다.

    자세한 내용은 아래 링크에서 참고

     

    Pricing

    MongoDB Product Pricing

    www.mongodb.com

     

    반면 AWS DynamoDB의 경우

    무료 스토리지 25GB에, 매월 최대 2억 건의 읽기/쓰기 요청을 AWS 프리 티어로 제공하는데

    심지어 이건 1년의 프리 티어가 끝나도 계속 적용이 된다.... 허허

    (이게 바로 대기업?)

    물론 글로벌 보조 인덱스를 추가하고 설정하는데에 있어서는 추가 요금이 부가된다.

     

    프로젝트를 진행하는데 있어서 채팅 메시지에 대한 용량 걱정이 별로 없고, 단순 기능 구현이 목적이라면

    MongoDB를 사용해도 좋을 거 같다.

    Spring에서 공식으로 Spring Data MongoDB를 지원하고 있기 때문에, 설정하는데 있어서 훨씬 편리하다!

     

    Spring Data MongoDB

    Spring Data for MongoDB is part of the umbrella Spring Data providing integration with the MongoDB document database, offering a familiar and consistent Spring based programming model while retaining store specific features and capabilities. Features Mongo

    spring.io

     

     

     

    DynamoDBMapper vs Spring-Data-DynamoDB

    Spring-Data-DynamoDB를 통해 연동하는데에 있어서 먼저 살펴보고 넘어가야 할 점이 있다.

    바로 DynamoDBMapper와 Spring-Data-DynamoDB에 대한 내용이다.

    우선 DynamoDBMapper는 AWS에서 공식적으로 제공해주는 AWS JAVA SDK에 들어있는 클래스의 일종이다.

    DynamoDBMapper를 사용하고 싶다면 아래 AWS 공식 문서를 살펴보도록 하자!

     

    DynamoDBMapper CRUD 작업 - Amazon DynamoDB

    DynamoDBMapper CRUD 작업 다음 Java 코드 예제에서는 Id, Title, ISBN 및 Authors 속성을 가진 CatalogItem 클래스를 선언합니다. 이 예제에서는 주석을 사용하여 이러한 속성을 DynamoDB의 ProductCatalog 테이블로 매

    docs.aws.amazon.com

     

     

    그렇다면 Spring-Data-DynamoDB는....?

    AWS에서 제공하는 서비스가 아니다. 그리고 Spring Data에서 공식으로 제공하는 기술도 아니다.

    michaellavelle/spring-data-dynamodb -> derjust/spring-data-dynamodb -> boostchicken/spring-data-dynamodb 로 계속 fork 하며 이어진 개인이 진행하는 프로젝트이다.

    Spring은 Spring-data-JPA, Spring-data-mongoDB, Spring-data-cassandra 등 

    여러가지 Spring data 인터페이스를 제공하지만 이 DynamoDB에 대해서는 제공하지 않고 있다.

    그러므로 본인의 Spring Boot 버전이 Spring-Data-DynamoDB과 호환이 되는지 확인이 필요하다.

     

    GitHub - boostchicken/spring-data-dynamodb: This module deals with enhanced support for a data access layer built on AWS DynamoD

    This module deals with enhanced support for a data access layer built on AWS DynamoDB. - GitHub - boostchicken/spring-data-dynamodb: This module deals with enhanced support for a data access layer ...

    github.com

     

     

     

    프로젝트에 Spring-Data-DynamoDB 적용

    build.gradle

    AWS에 연결하는 Configuration 설정하고 Spring-Data-Dynamodb의 쿼리를 사용하기 위해서 아래 dependency를 가져온다.

     

    JAVA : 17

    SpringBoot version : 2.7.15

    // dynamodb
    implementation 'com.amazonaws:aws-java-sdk-s3:1.12.268'
    implementation 'io.github.boostchicken:spring-data-dynamodb:5.2.5'

     

    application.yml

    aws:
      dynamodb:
        accessKey: {AWS access-key 값}
        secretKey: {AWS secret-key 값}
        region: {AWS DynamoDB를 사용하는 리전명}

     

    AWS IAM 액세스 키 발급에 대한 내용은 아래 링크를 참고하자

     

    [AWS] AWS Access Key란? (엑세스 키 생성 방법)

    Access Key는 AWS 계정에 액세스하기 위한 인증 정보 중 하나입니다. Access Key ID와 Secret Access Key 두 부분으로 구성되어 있습니다. AWS 자원에 접근하거나, API를 호출할 때 사용됩니다. 보통 백엔드에서

    hyunki99.tistory.com

     

    DynamoDBConfig

    액세스키와 비밀키를 활용하여 로컬 코드에서 AWS의 DynamoDB 서비스로 접근할 수 있도록 Config 코드를 작성해야 한다.

    @Configuration
    @EnableDynamoDBRepositories(basePackages = {"com.dku.council.domain.chatmessage.repository"},
            includeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Repository.class))
    public class DynamoDBConfig {
    
        @Value("${aws.dynamodb.accessKey}")
        private String awsAccessKey;
    
        @Value("${aws.dynamodb.secretKey}")
        private String awsSecretKey;
    
        @Value("${aws.dynamodb.region}")
        private String awsRegion;
    
        public AWSCredentials amazonAWSCredentials() {
            return new BasicAWSCredentials(awsAccessKey, awsSecretKey);
        }
    
        public AWSCredentialsProvider amazonAWSCredentialsProvider() {
            return new AWSStaticCredentialsProvider(amazonAWSCredentials());
        }
    
        @Bean
        public AmazonDynamoDB amazonDynamoDB() {
            return AmazonDynamoDBClientBuilder.standard().withCredentials(amazonAWSCredentialsProvider())
                    .withRegion(awsRegion).build();
        }
    
        /**
         * Java DynamoDB SDK가 Java의 기본 Date 타입만 허용하므로
         * LocalDateTimeType과 Date를 상호 변환할 수 있는 컨버터 추가
         */
        public static class LocalDateTimeConverter implements DynamoDBTypeConverter<Date, LocalDateTime> {
            @Override
            public Date convert(LocalDateTime source) {
                ZoneId seoulZoneId = ZoneId.of("Asia/Seoul");
                return Date.from(source.atZone(seoulZoneId).toInstant());
            }
    
            @Override
            public LocalDateTime unconvert(Date source) {
                return source.toInstant().atZone(ZoneId.of("Asia/Seoul")).toLocalDateTime();
            }
        }
    
    }

     

     

     

    DynamoDB Entity 작성

    ChatRoomMessage

    @DynamoDBTable(tableName = "ChatRoomMessage")
    @Getter
    @Setter
    @NoArgsConstructor()
    public class ChatRoomMessage {
    
        @Id
        @Getter(AccessLevel.NONE)
        @Setter(AccessLevel.NONE)
        private ChatRoomMessageId chatRoomMessageId;
    
        @DynamoDBHashKey(attributeName = "roomId")
        public String getRoomId() {
            return chatRoomMessageId != null ? chatRoomMessageId.getRoomId() : null;
        }
    
        public void setRoomId(String roomId) {
            if (chatRoomMessageId == null) {
                chatRoomMessageId = new ChatRoomMessageId();
            }
            chatRoomMessageId.setRoomId(roomId);
        }
    
        @DynamoDBRangeKey(attributeName = "createdAt")
        @DynamoDBTypeConverted(converter = DynamoDBConfig.LocalDateTimeConverter.class)
        public LocalDateTime getCreatedAt() {
            return chatRoomMessageId != null ? chatRoomMessageId.getCreatedAt() : null;
        }
    
        @DynamoDBTypeConverted(converter = DynamoDBConfig.LocalDateTimeConverter.class)
        public void setCreatedAt(LocalDateTime createdAt) {
            if (chatRoomMessageId == null) {
                chatRoomMessageId = new ChatRoomMessageId();
            }
            chatRoomMessageId.setCreatedAt(createdAt);
        }
    
        @DynamoDBAttribute
        private String messageType;
    
        @DynamoDBAttribute
        private Long userId;
    
        @DynamoDBAttribute
        private String userNickname;
    
        @DynamoDBAttribute
        private String content;
    }

     

    ChatRoomMessageId

    @Getter
    @Setter
    public class ChatRoomMessageId implements Serializable {
        private static final long serialVersionUID = 1L;
    
        @DynamoDBHashKey
        private String roomId;
    
        @DynamoDBRangeKey
        @DynamoDBTypeConverted(converter = DynamoDBConfig.LocalDateTimeConverter.class)
        private LocalDateTime createdAt;
    }

     

    DynamoDB 테이블을 작성하는데 있어서 필요한 기본 개념들은

    아래 공식 문서를 참고해주세요!

     

    Amazon DynamoDB의 핵심 구성 요소 - Amazon DynamoDB

    Amazon DynamoDB의 핵심 구성 요소 DynamoDB에서 테이블, 항목 및 속성이 작업하는 핵심 구성 요소입니다. 테이블은 항목의 컬렉션이고 각 항목은 속성의 컬렉션입니다. DynamoDB는 기본 키를 사용하여 테

    docs.aws.amazon.com

     

    엔티티 작성 후, 여기서 스프링을 많이 써보신 분들이면 아래와 같은 의문이 들 수 있을 것 같다.

     

    💁‍♂️1. Entity 작성에 있어서 굳이 @Setter를 사용하는 이유는?? 💁‍♂️

    Entity를 구현하는 데 있어서 Setter 메소드를 구현하는 것은 객체의 일관성을 유지하기 위해서 지양해야 한다.

    하지만 위에 DynamoDBMapper로 CRUD 구현하는 링크에서도 보다시피,

    DynamoDB 공식 Docs에서도 Entity 구현 시 Setter 메소드를 필수적으로 사용한다.

    DynamoDB Java SDK에서 DynamoDB를 편리하게 다루라고 제공하는 DynamoDBMapper가 

    객체 매핑 시 Setter를 사용하기 때문이다.

    초반에 해당 내용을 모르고 @Builder로 평소처럼 Entity를 작성하다, 여러 오류를 마주치게 되어 며칠을 고생했다... ㅠ

     

    💁‍♂️2. ChatRoomMessageId를 별도로 생성해 준 이유는?? 💁‍♂️

    결론부터 말하자면, DynamoDB에서 사용하는 개념인 Composite Key (복합키)를 Entity에 적용했기 때문이다.

    DynamoDB에서 복합키는 Partition Key와 Range Key를 같이 사용하는 것을 의미한다.

    위와 같이 작성하지 않으면 Bean 등록 오류가 발생하니, 복합키를 사용한다면 반드시 위와 같은 방식으로 작성하자.

    작성은 아래 Spring-Data-DynamoDB 깃허브 내용을 보고 참고하였다.

     

    Use Hash Range keys

    This module deals with enhanced support for a data access layer built on AWS DynamoDB. - derjust/spring-data-dynamodb

    github.com

     

     

     

    AWS Console에서 DynamoDB 생성

    이제 AWS console에 로그인하여 DynamoDB 서비스에 들어가서

    스프링부트에서 작성한 엔티티 내용들을 기반으로 테이블을 생성해 주자.

     

     

    테이블 생성을 누르면 해당 테이블이 생성되고 원격으로 사용할 수 있게 되며,
    해당 작업은 AWS Console 뿐만 아니라 AWS CLI를 사용하여 수행할 수도 있다.

     

     

     

    DynamoDB Repository 작성

    ChatRoomMessageRepository

    @EnableScan
    @Repository
    public interface ChatRoomMessageRepository extends CrudRepository<ChatRoomMessage, ChatRoomMessageId> {
    
        List<ChatRoomMessage> findAllByRoomIdOrderByCreatedAtAsc(String roomId);
    
        void deleteAllByRoomId(String roomId);
    }

     

    Spring에서 제공하는 CrudRepository를 상속받아 인터페이스를 작성하면 메소드로 쿼리를 동작시킬 수 있다.

    DynamoDBMapper 사용하다가 이거 쓰면 너무 편안....

     

     

     

     

    DynamoDB Service 작성

    사실 Service 로직부터는 특별히 다른 부분은 없어서 각자의 기능에 맞게 코드를 작성해주면 된다.

    나의 경우 채팅방 메시지를 관리하기 위한 서비스 로직을 작성해야 하므로 아래와 같이 작성했다. 

    @Service
    @RequiredArgsConstructor
    @Slf4j
    public class ChatRoomMessageService {
    
        private final ZoneId seoulZoneId = ZoneId.of("Asia/Seoul");
    
        private final ChatRoomMessageRepository chatRoomMessageRepository;
    
        public void create(String roomId,
                           String messageType,
                           Long userId,
                           String userNickname,
                           String content) {
    
            ChatRoomMessage chatRoomMessage = new ChatRoomMessage();
            chatRoomMessage.setRoomId(roomId);
            chatRoomMessage.setMessageType(messageType);
            chatRoomMessage.setUserId(userId);
            chatRoomMessage.setUserNickname(userNickname);
            chatRoomMessage.setContent(content);
            chatRoomMessage.setCreatedAt(LocalDateTime.now().atZone(seoulZoneId).toLocalDateTime());
    
            chatRoomMessageRepository.save(chatRoomMessage);
        }
    
        public List<ChatRoomMessage> findAllChatRoomMessages(String roomId) {
            return chatRoomMessageRepository.findAllByRoomIdOrderByCreatedAtAsc(roomId);
        }
    
        public void deleteChatRoomMessages(String roomId) {
            chatRoomMessageRepository.deleteAllByRoomId(roomId);
        }
    }

     

     

     

     

    AWS Console을 통해 테이블에 저장된 내용 확인하기 

     

    컨트롤러 로직을 구성하고 API 요청을 보내면 아래와 같이 잘 들어오는걸 확인할 수 있다. 

     

     

    DynamoDB를 다루는데 있어서 정말 많은 오류를 직면했던 것 같다.

    자료도 별로 없고..

    뭔가 음... 뭐라 해야 할까... 스프링부트와 DynamoDB를 설정하는데 있어서

    다른 데이터베이스에 비해 옛날의 향기가 느껴진다고 해야하나... 

    물론 DynamoDB 자체는 정말 정말 편리하고 좋다.

     

     

     

    이제 채팅방 관련해서 구체적인 기능들을 적용하고

    DynamoDB 쪽 수정 사항이 있다면 수정해 나가야겠다...

     

     

     

    🌟DynamoDB 넌 이제 내꺼야!🌟

     

Designed by Tistory.