본문 바로가기
공부/프로그래밍

[springboot] Dynamodb, Local로 연결하여 연동테스트 및 기능살펴보기

by demonic_ 2020. 8. 22.
반응형

이번글은 평소 자주 보는 창천항로님 글을 많이 참조했습니다.

늘 그렇지만 좋은글 많이 올려주시는 창천항로님에게 감사의 말씀드립니다.

 

참조사이트:

https://jojoldu.tistory.com/484

 

[DynamoDB] Spring Data DynamoDB와 Embedded 개발 환경 구축하기

모든 코드는 Github에 있습니다. 이번 시간엔 로컬 개발 환경에서 DynamoDB를 Embedded로 활용하는 방법에 대해서 알아보겠습니다. 이미 도커를 적극적으로 테스트와 개발에 사용하고 계신 분들이라면

jojoldu.tistory.com


 

도커를 사용할 수도 있겠지만 도커없이도 DynamoDB Embedded를 통해 테스트가 가능하다. 만약 도커로 이미 DynamoDB 테스트 환경을 설정했다면 DynamoDB Embedded를 하지 않아도 된다.

 

DynamoDB 연동을 위해 다음 디펜던시를 추가한다.

repositories {
    mavenCentral()
    maven { url 'https://s3-us-west-2.amazonaws.com/dynamodb-local/release' } // for DynamoDBLocal Lib
}

dependencies {
...
    implementation 'org.springframework.cloud:spring-cloud-starter-aws'
    implementation 'io.github.boostchicken:spring-data-dynamodb:5.2.3'
    implementation 'com.amazonaws:DynamoDBLocal:1.11.119'
...
}

dependencyManagement {
    imports {
        mavenBom org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES
        mavenBom 'org.springframework.cloud:spring-cloud-dependencies:Hoxton.SR3'
    }
}

 

 

dependencyManagement 를 추가한 이유는, 이것이 없으면 build시 다음과 같은 에러를 보게 된다. 그러니 반드시 추가하자.

Execution failed for task ':compileQuerydsl'.
> Could not resolve all files for configuration ':querydsl'.
   > Could not find org.springframework.cloud:spring-cloud-starter-aws:.
     Required by:
         project :

Possible solution:
 - Declare repository providing the artifact, see the documentation at https://docs.gradle.org/current/userguide/declaring_repositories.html

 

창천항로 님의 글을 보면 DynamoDBLocal 버전을 1.11.119로 명시한 이유를 설명했는데, 그 이유를 호완성 때문이다.

 

AWS공식문서를 보면 2020.08.22 현재 1.12 버전까지 지원한다. 다만 Maven에 올라온 최신 버전은 1.13이기 때문에 버전을 지정할 필요가 있다. 여기서는 창천항로님의 글을 참조해 1.11 버전을 사용하기로 한다.

 

AWS 공식문서에 보면 jar로 다운받거나 Docker를 이용해 띄우는 방법이 설명되어 있으니 한번 꼭 보고 가면 좋겠다.

 

DynamoDB Local 관련 문서

https://docs.aws.amazon.com/ko_kr/amazondynamodb/latest/developerguide/DynamoDBLocal.DownloadingAndRunning.html

 

Deploying DynamoDB Locally on Your Computer - Amazon DynamoDB

yaml 스크립트를 사용하려면 AWS 액세스 키와 AWS 보안 키를 지정해야 하지만 DynamoDB Local에 액세스하기 위해 유효한 AWS 키가 아니어도 됩니다.

docs.aws.amazon.com

 

 

# DynamoDB Embedded 하기

임베디드 하기위한 Config 클래스를 생성한다.

(코드는 창천항로님 것을 그대로 가져왔다.)

Spring.profiles.active가 local일때만, 그리고 property의 설정중 embedded-dynamodb.use가 true인 경우만 실행하도록 한다.

@Slf4j
@Configuration
@Profile("local")
@ConditionalOnProperty(name = "embedded-dynamodb.use", havingValue = "true")
public class EmbeddedDynamoDbConfig {

    private DynamoDBProxyServer server;

    @PostConstruct
    public void start() {
        if(server != null) {
            return;
        }

        try {
            AwsDynamoDbLocalTestUtils.initSqLite();
            server = ServerRunner.createServerFromCommandLineArgs(new String[]{"-inMemory"});
            server.start();
            log.info("Start Embedded DynamoDB");
        } catch (ParseException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @PreDestroy
    public void stop() {
        if(server == null) {
            return;
        }

        try {
            log.info("Stop Embedded DynamoDB");
            server.stop();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

 

 

다만 띄울때 AwsDynamoDbLocalTestUtils 클래스가 없을텐데 이것을 직접 추가해줘야 한다. 창천항로님 블로그에 따르면 sqlite4java 라이브러리 때문이라 하는데, 메이븐에 추가하는 방법은 있으나 Intellij IDEA나 gradle에서 수행하면 이 라이브러리를 사용할 수 없어 실행이 되지 않는 문제가 발생한다 한다.(더 자세한 이야기는 창천항로님 블로그로 접속해서 확인)

 

파일 다운로드을 다운로드 받아 추가하거나 아래 파일코드를 복사해서 클래스를 생성하자.

AwsDynamoDbLocalTestUtils.java
0.01MB

파일 내 코드:

package kr.sample.demo.dynamodb.config;

import com.google.common.base.Splitter;
import com.google.common.collect.Lists;

import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.function.Supplier;

/**
 * Helper class for initializing AWS DynamoDB to run with sqlite4java for local testing.
 *
 * Copied from: https://github.com/redskap/aws-dynamodb-java-example-local-testing
 */
public class AwsDynamoDbLocalTestUtils {

    private static final String BASE_LIBRARY_NAME = "sqlite4java";

    /**
     * Static helper class.
     */
    private AwsDynamoDbLocalTestUtils() {
    }

    /**
     * Sets the sqlite4java library path system parameter if it is not set already.
     */
    public static void initSqLite() {
        initSqLite(() -> {
            final List<String> classPath = getClassPathList(System.getProperty("java.class.path"), File.pathSeparator);

            final String libPath = getLibPath(System.getProperty("os.name"), System.getProperty("java.runtime.name"),
                    System.getProperty("os.arch"), classPath);

            return libPath;
        });
    }

    /**
     * Sets the sqlite4java library path system parameter if it is not set already.
     *
     * @param libPathSupplier Calculates lib path for sqlite4java.
     */
    public static void initSqLite(Supplier<String> libPathSupplier) {
        if (System.getProperty("sqlite4java.library.path") == null) {
            System.setProperty("sqlite4java.library.path", libPathSupplier.get());
        }
    }

    /**
     * Calculates the possible Library Names for finding the proper sqlite4j native library and returns the directory with the most specific matching library.
     *
     * @param osName      The value of <code>"os.name"</code> system property (<code>System.getProperty("os.name")</code>).
     * @param runtimeName The value of <code>"java.runtime.name"</code> system property (<code>System.getProperty("java.runtime.name")</code>).
     * @param osArch      The value of <code>"os.arch"</code> system property (<code>System.getProperty("os.arch")</code>).
     * @param osArch      The classpath split into strings by path separator. Value of <code>"java.class.path"</code> system property
     *                    (<code>System.getProperty("os.arch")</code>) split by <code>File.pathSeparator</code>.
     * @return
     */
    public static String getLibPath(final String osName, final String runtimeName, final String osArch, final List<String> classPath) {
        final String os = getOs(osName, runtimeName);
        final List<String> libNames = getLibNames(os, getArch(os, osArch));

        for (final String libName : libNames) {
            for (final String classPathLib : classPath) {
                if (classPathLib.contains(libName)) {
                    return new File(classPathLib).getParent();
                }
            }
        }

        throw new IllegalStateException("SQLite library \"" + libNames + "\" is missing from classpath");
    }

    /**
     * Calculates the possible Library Names for finding the proper sqlite4java native library.
     *
     * Based on the internal calculation of the sqlite4java wrapper <a href="https://bitbucket
     * .org/almworks/sqlite4java/src/fa4bb0fe7319a5f1afe008284146ac83e027de60/java/com/almworks/sqlite4java/Internal
     * .java?at=master&fileviewer=file-view-default#Internal.java-160">Internal
     * class</a>.
     *
     * @param os   Operating System Name used by sqlite4java to get native library.
     * @param arch Operating System Architecture used by sqlite4java to get native library.
     * @return Possible Library Names used by sqlite4java to get native library.
     */
    public static List<String> getLibNames(final String os, final String arch) {
        List<String> result = new ArrayList<>();

        final String base = BASE_LIBRARY_NAME + "-" + os;

        result.add(base + "-" + arch);

        if (arch.equals("x86_64") || arch.equals("x64")) {
            result.add(base + "-amd64");
        } else if (arch.equals("x86")) {
            result.add(base + "-i386");
        } else if (arch.equals("i386")) {
            result.add(base + "-x86");
        } else if (arch.startsWith("arm") && arch.length() > 3) {
            if (arch.length() > 5 && arch.startsWith("armv") && Character.isDigit(arch.charAt(4))) {
                result.add(base + "-" + arch.substring(0, 5));
            }
            result.add(base + "-arm");
        }

        result.add(base);
        result.add(BASE_LIBRARY_NAME);

        return result;
    }

    /**
     * Calculates the Operating System Architecture for finding the proper sqlite4java native library.
     *
     * Based on the internal calculation of the sqlite4java wrapper <a href="https://bitbucket
     * .org/almworks/sqlite4java/src/fa4bb0fe7319a5f1afe008284146ac83e027de60/java/com/almworks/sqlite4java/Internal
     * .java?at=master&fileviewer=file-view-default#Internal.java-204">Internal
     * class</a>.
     *
     * @param osArch The value of <code>"os.arch"</code> system property (<code>System.getProperty("os.arch")</code>).
     * @param os     Operating System Name used by sqlite4java to get native library.
     * @return Operating System Architecture used by sqlite4java to get native library.
     */
    public static String getArch(final String os, final String osArch) {
        String result;

        if (osArch == null) {
            result = "x86";
        } else {
            final String loweCaseOsArch = osArch.toLowerCase(Locale.US);
            result = loweCaseOsArch;
            if ("win32".equals(os) && "amd64".equals(loweCaseOsArch)) {
                result = "x64";
            }
        }

        return result;
    }

    /**
     * Calculates the Operating System Name for finding the proper sqlite4java native library.
     *
     * Based on the internal calculation of the sqlite4java wrapper <a href="https://bitbucket
     * .org/almworks/sqlite4java/src/fa4bb0fe7319a5f1afe008284146ac83e027de60/java/com/almworks/sqlite4java/Internal
     * .java?at=master&fileviewer=file-view-default#Internal.java-219">Internal
     * class</a>.*
     *
     * @param osName      The value of <code>"os.name"</code> system property (<code>System.getProperty("os.name")</code>).
     * @param runtimeName The value of <code>"java.runtime.name"</code> system property (<code>System.getProperty("java.runtime.name")</code>).
     * @return Operating System Name used by sqlite4java to get native library.
     */
    public static String getOs(final String osName, final String runtimeName) {

        String result;
        if (osName == null) {
            result = "linux";
        } else {
            final String loweCaseOsName = osName.toLowerCase(Locale.US);
            if (loweCaseOsName.startsWith("mac") || loweCaseOsName.startsWith("darwin") || loweCaseOsName.startsWith("os x")) {
                result = "osx";
            } else if (loweCaseOsName.startsWith("windows")) {
                result = "win32";
            } else {
                if (runtimeName != null && runtimeName.toLowerCase(Locale.US).contains("android")) {
                    result = "android";
                } else {
                    result = "linux";
                }
            }
        }

        return result;
    }

    /**
     * Splits classpath string by path separator value.
     *
     * @param classPath     Value of <code>"java.class.path"</code> system property (<code>System.getProperty("os.arch")</code>).
     * @param pathSeparator Value of path separator (<code>File.pathSeparator</code>).
     * @return The list of each classpath elements.
     */
    public static List<String> getClassPathList(final String classPath, final String pathSeparator) {
        return Lists.newArrayList(Splitter.on(pathSeparator).split(classPath));
    }

}

 

이파일은 이곳을 방문하면 직접 다운로드 받을 수 있으니 참조하자.

https://github.com/redskap/aws-dynamodb-java-example-local-testing

 

redskap/aws-dynamodb-java-example-local-testing

Example Gradle Java project for using AWS DynamoDB for local testing. - redskap/aws-dynamodb-java-example-local-testing

github.com

 

다음은 DynamoDB를 사용하기 위해 Repository를 등록하는 작업이다.

import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.auth.InstanceProfileCredentialsProvider;
import com.amazonaws.client.builder.AwsClientBuilder;
import com.amazonaws.regions.Regions;
import com.amazonaws.services.dynamodbv2.AmazonDynamoDB;
import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClientBuilder;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapper;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapperConfig;
import lombok.extern.slf4j.Slf4j;
import org.socialsignin.spring.data.dynamodb.repository.config.EnableDynamoDBRepositories;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.context.annotation.Profile;

@Slf4j
@Configuration
@EnableDynamoDBRepositories(basePackages = {"[레파지토리가 위치할 패키지 경로"})
public class AwsDynamoDbConfig {

    @Bean
    @Primary
    public DynamoDBMapperConfig dynamoDBMapperConfig() {
        return DynamoDBMapperConfig.DEFAULT;
    }

    @Profile({"!local"}) // (2)
    @Bean
    @Primary
    public AmazonDynamoDB amazonDynamoDB() {
        log.info("Start AWS Amazon DynamoDB Client");
        return AmazonDynamoDBClientBuilder.standard()
                .withCredentials(InstanceProfileCredentialsProvider.getInstance()) // (3)
                .withRegion(Regions.AP_NORTHEAST_2)
                .build();
    }

    @Profile({"local"}) // (4)
    @Bean(name = "amazonDynamoDB")
    @Primary
    public AmazonDynamoDB localAmazonDynamoDB() {
        log.info("Start Local Amazon DynamoDB Client");
        BasicAWSCredentials basicAWSCredentials = new BasicAWSCredentials("testId", "testAccessKey");
        return AmazonDynamoDBClientBuilder.standard()
                .withCredentials(new AWSStaticCredentialsProvider(basicAWSCredentials)) // (5)
                .withEndpointConfiguration(new AwsClientBuilder.EndpointConfiguration("http://localhost:8000"
                        , Regions.AP_NORTHEAST_1.getName())) // (6)
                .build();
    }

    @Bean
    @Primary
    public DynamoDBMapper dynamoDBMapper(AmazonDynamoDB amazonDynamoDB, DynamoDBMapperConfig config) {
        return new DynamoDBMapper(amazonDynamoDB, config);
    }
}

 

Profile 설정에 따라 어떤것으로 인증할것인지 분기되어 있다.

인증방법에 따라 IAM Role 지정하여 올리거나 혹은 accessKey를 받아 인증받는 방법을 주로 택한다. 여기서는 Local로 Embedded 한 것이기에 accessKey를 임의로 입력해 인증받는 것으로 되어있다.

 

그리고 ithEndpointConfiguration를 설정하지 않으면 다음과 같은 에러를 볼 수 있다.

WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by com.fasterxml.jackson.databind.util.ClassUtil (file:/Users/dgpark/.gradle/caches/modules-2/files-2.1/com.fasterxml.jackson.core/jackson-databind/2.10.2/528de95f198afafbcfb0c09d2e43b6e0ea663ec/jackson-databind-2.10.2.jar) to field java.lang.Throwable.cause
WARNING: Please consider reporting this to the maintainers of com.fasterxml.jackson.databind.util.ClassUtil
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release

com.amazonaws.services.dynamodbv2.model.AmazonDynamoDBException: The security token included in the request is invalid. (Service: AmazonDynamoDBv2; Status Code: 400; Error Code: UnrecognizedClientException; Request ID: CQHH2AAGIIJESRJKJB9DANKSFBVV4KQNSO5AEMVJF66Q9ASUAAJG)
...

 

현재 로컬에 띄워둔 것이기 때문에 Endpoint를 명확히 지정해야 하는거 같다. DynamoDB Local은 기본설정 포트가 8000 이므로 localhost:8000 을 명시해주는게 좋다. 만약 포트번호가 틀리면 다음과 같은 에러가 발생한다.

com.amazonaws.SdkClientException: Unable to execute HTTP request: Connect to localhost:8080 [localhost/127.0.0.1, localhost/0:0:0:0:0:0:0:1] failed: Connection refused (Connection refused)
...

 

 

 

# 테스트: 접속 확인 및 테이블 생성

테스트 환경은 junit5 를 이용했다.

@ActiveProfiles를 local로 설정해야 Config에 설정한 것들을 제대로 작동시킬 수 있다.

그밖에 @TestPropertySource를 이용한 것은 Embedded를 시키기 위함이다.

@ExtendWith(SpringExtension.class)
@SpringBootTest
@TestPropertySource(properties = {"embedded-dynamodb.use=true"})
@ActiveProfiles("local")
public class DynamoDbExampleTest {

    @Autowired
    private AmazonDynamoDB dynamoDB;

    @Test
    @DisplayName("테이블 생성 테스트")
    void test_createTable() {
        //given
        String tableName = "Post";
        String hashKeyName = "user_id";

        //when
        CreateTableResult res = createTable(tableName, hashKeyName);

        TableDescription tableDesc = res.getTableDescription();
        assertThat(tableDesc.getTableName()).isEqualTo(tableName);
        assertThat(tableDesc.getKeySchema().toString()).isEqualTo("[{AttributeName: " + hashKeyName + ",KeyType: HASH}]");
        assertThat(tableDesc.getAttributeDefinitions().toString()).isEqualTo("[{AttributeName: " + hashKeyName + ",AttributeType: S}]");
        assertThat(tableDesc.getProvisionedThroughput().getReadCapacityUnits()).isEqualTo(1000L);
        assertThat(tableDesc.getProvisionedThroughput().getWriteCapacityUnits()).isEqualTo(1000L);
        assertThat(tableDesc.getTableStatus()).isEqualTo("ACTIVE");
        assertThat(tableDesc.getTableArn()).isEqualTo("arn:aws:dynamodb:ddblocal:000000000000:table/Post");
        assertThat(dynamoDB.listTables().getTableNames()).hasSizeGreaterThanOrEqualTo(1);
    }

    private CreateTableResult createTable(String tableName, String hashKeyName) {
        List<AttributeDefinition> attributeDefinitions = new ArrayList<>();
        attributeDefinitions.add(new AttributeDefinition(hashKeyName, ScalarAttributeType.S));

        List<KeySchemaElement> ks = new ArrayList<>();
        ks.add(new KeySchemaElement(hashKeyName, KeyType.HASH));

        ProvisionedThroughput provisionedThroughput = new ProvisionedThroughput(1000L, 1000L);

        CreateTableRequest request = new CreateTableRequest()
                .withTableName(tableName)
                .withAttributeDefinitions(attributeDefinitions)
                .withKeySchema(ks)
                .withProvisionedThroughput(provisionedThroughput);

        return dynamoDB.createTable(request);
    }
}

테스트를 수행하면 다음 로그가 뜨면서 DynamoDB 가 Local에서 생성된 것을 확인할 수 있다.

Initializing DynamoDB Local with the following configuration:
Port:	8000
InMemory:	true
DbPath:	null
SharedDb:	false
shouldDelayTransientStatuses:	false
CorsParams:	*

아울러 테스트가 성공하게 되면 다음과 같이 response body에 스크립트가 만들어져 리턴된다.

c.a.s.d.l.s.LocalDynamoDBServerHandler   : [Response] header name: x-amzn-RequestId : value: 3fcf1343-f06f-4155-8f2e-7081e2d98f2f
header name: x-amz-crc32 : value: 3789893850
header name: Date : value: Fri, 21 Aug 2020 00:08:37 GMT
header name: Content-Type : value: application/x-amz-json-1.0
status: 200
response body: {"TableDescription":{"AttributeDefinitions":[{"AttributeName":"user_id","AttributeType":"S"}],"TableName":"Post","KeySchema":[{"AttributeName":"user_id","KeyType":"HASH"}],"TableStatus":"ACTIVE","CreationDateTime":1597968517.799,"ProvisionedThroughput":{"LastIncreaseDateTime":0.000,"LastDecreaseDateTime":0.000,"NumberOfDecreasesToday":0,"ReadCapacityUnits":1000,"WriteCapacityUnits":1000},"TableSizeBytes":0,"ItemCount":0,"TableArn":"arn:aws:dynamodb:ddblocal:000000000000:table/Post"}}

테스트도 성공.

 

 

 

# Entity와 Repository 생성

일단 dynamoDB에 넣을 entity를 만든다

@Getter
@Setter
@NoArgsConstructor
@DynamoDBTable(tableName = "Post")
public class Post {

    @Id
    @DynamoDBHashKey
    private String postNo;

    @DynamoDBAttribute
    private String title;


    public Post(String postNo, String title) {
        this.postNo = postNo;
        this.title = title;
    }
}

 

@Setter 라든가 @NoArgsConstructor 는 반드시 넣어주어야 한다.

특히 @Setter는 RDBS의 JPA를 만들때는 전혀 필요가 없었는데 여기서 만드는 이유는, Repository를 이용해 조회하여 Entity에 담는 과정에서 다음의 에러가 발생하기 때문이다.

 

com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMappingException: Post[postNo]; could not unconvert attribute
	at com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapperTableModel.unconvert(DynamoDBMapperTableModel.java:271)
	at com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapper.privateMarshallIntoObject(DynamoDBMapper.java:471)
...

Caused by: com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMappingException: could not invoke null on class kr.sample.demo.dynamodb.repository.Post with value postNo of type class java.lang.String
...

Caused by: java.lang.NullPointerException
...

 

이번엔 Repository를 생성한다.

@EnableScan
public interface PostRepository extends CrudRepository<Post, String> {
}

이제 이것을 테스트할 코드를 작성한다

 

 

 

# 테스트: 데이터 삽입, 조회

이번에는 Spring Data DynamoDB가 잘 작동하는지 검증하려 한다.

테스트 환경은 위와 동일하게 하되, 테스트 시작 전에 테이블이 생성되어 있지 않다면 생성할 수 있도록 @BeforeEach 에 추가해준다.

@ExtendWith(SpringExtension.class)
@SpringBootTest
@TestPropertySource(properties = {"embedded-dynamodb.use=true"})
@ActiveProfiles("local")
class PostRepositoryTest {
    @Autowired
    private AmazonDynamoDB dynamoDB;

    @Autowired
    private DynamoDBMapper dynamoDbMapper;

    @Autowired
    private PostRepository postRepository;

    @BeforeEach
    void init() {
        CreateTableRequest createTableRequest = dynamoDbMapper.generateCreateTableRequest(Post.class)
                .withProvisionedThroughput(new ProvisionedThroughput(1L, 1L));

        TableUtils.createTableIfNotExists(dynamoDB, createTableRequest);
    }
...
}

 

Post를 저장하는 테스트를 수행해보자.

...
    @Test
    @DisplayName("Post 저장 테스트-1")
    void postSave1Test() {
        // given
        String postNo = "postNo";
        String title = "첫번째 글";

        // when
        postRepository.save(new Post(postNo, title));

        // then
        Iterable<Post> findPosts = postRepository.findAll();
        for (Post findPost : findPosts) {
            System.out.println("findPost.getPostNo() = " + findPost.getPostNo());
            System.out.println("findPost.getTitle() = " + findPost.getTitle());
        }
        Assertions.assertThat(postNo).isEqualTo(findPosts.iterator().next().getPostNo());
        Assertions.assertThat(title).isEqualTo(findPosts.iterator().next().getTitle());
    }
...

결과

response body: {"Items":[{"title":{"S":"첫번째 글"},"postNo":{"S":"postNo"}}],"Count":1,"ScannedCount":1}

findPost.getPostNo() = postNo
findPost.getTitle() = 첫번째 글

 

잘 저장된다.

 

이참에 DynamoDB의 기능을 좀더 알아보고자 다음 테스트를 추가로 했다.

 

 

- 똑같은 ID로 저장하면 추가가 될까 덮어쓸까?

다음 테스트를 작성 후 결과를 확인해봤다.

...
    @Test
    @DisplayName("Post 저장 테스트-(ID중복)")
    void postSave2Test() {
        // given
        String postNo = "postNo";
        String title = "첫번째 글";

        String title2= "두번째 글";

        // when
        postRepository.save(new Post(postNo, title));
        postRepository.save(new Post(postNo, title2));

        // then
        Iterable<Post> findPosts = postRepository.findAll();
        for (Post findPost : findPosts) {
            System.out.println("findPost.getPostNo() = " + findPost.getPostNo());
            System.out.println("findPost.getTitle() = " + findPost.getTitle());
        }

        Assertions.assertThat(title2).isEqualTo(findPosts.iterator().next().getTitle());
    }
...

결과

# 첫번째 저장
request body: {"TableName":"Post","Key":{"postNo":{"S":"postNo"}},"AttributeUpdates":{"title":{"Value":{"S":"첫번째 글"},"Action":"PUT"}},"ReturnValues":"ALL_NEW"}

...
# 두번째 저장
request body: {"TableName":"Post","Key":{"postNo":{"S":"postNo"}},"AttributeUpdates":{"title":{"Value":{"S":"두번째 글"},"Action":"PUT"}},"ReturnValues":"ALL_NEW"}

...
# 조회
response body: {"Items":[{"title":{"S":"두번째 글"},"postNo":{"S":"postNo"}}],"Count":1,"ScannedCount":1}

findPost.getPostNo() = postNo
findPost.getTitle() = 두번째 글

 

2번 저장시도를 하지만 최종적으로 저장된건 가장 마지막에 있는 것만 남아있었다. 덮어쓰기 했다.

 

- ID를 다르게 넣으면 어떻게 될까?

 

...
    @Test
    @DisplayName("Post 저장 테스트-(2개이상)")
    void postSave3Test() {
        // given
        String postNo = "postNo1";
        String title = "첫번째 글";

        String postNo2 = "postNo2";
        String title2= "두번째 글";

        // when
        postRepository.save(new Post(postNo, title));
        postRepository.save(new Post(postNo2, title2));

        // then
        Iterable<Post> findPosts = postRepository.findAll();
        for (Post findPost : findPosts) {
            System.out.println("findPost.getPostNo() = " + findPost.getPostNo());
            System.out.println("findPost.getTitle() = " + findPost.getTitle());
        }
    }
...

결과

request body: {"TableName":"Post","Key":{"postNo":{"S":"postNo1"}},"AttributeUpdates":{"title":{"Value":{"S":"첫번째 글"},"Action":"PUT"}},"ReturnValues":"ALL_NEW"}

...

request body: {"TableName":"Post","Key":{"postNo":{"S":"postNo2"}},"AttributeUpdates":{"title":{"Value":{"S":"두번째 글"},"Action":"PUT"}},"ReturnValues":"ALL_NEW"}

...

response body: {"Items":[{"title":{"S":"두번째 글"},"postNo":{"S":"postNo2"}},{"title":{"S":"첫번째 글"},"postNo":{"S":"postNo1"}}],"Count":2,"ScannedCount":2}

findPost.getPostNo() = postNo2
findPost.getTitle() = 두번째 글
findPost.getPostNo() = postNo1
findPost.getTitle() = 첫번째 글

예상한대로 2개모두 저장되어 있다.

 

 

 

# 테스트: ID를 자동생성하게 설정하고 테스트하기

이번에는 ID를 자동 생성하도록 Entity를 구성하려 한다. Post2 라는 엔터티를 다음과 같이 생성한다.

@Getter
@Setter
@NoArgsConstructor
@DynamoDBTable(tableName = "Post2")
public class Post2 {

    @DynamoDBAutoGeneratedKey
    @DynamoDBHashKey
    private String postNo;

    @DynamoDBAttribute
    private String title;

    public Post2(String title) {
        this.title = title;
    }
}

Repository를 생성한다.

import org.socialsignin.spring.data.dynamodb.repository.EnableScan;
import org.springframework.data.repository.CrudRepository;

@EnableScan
public interface Post2Repository extends CrudRepository<Post2, String> {
}

 

이제 이걸로 테스트를 작성해보자.

@ExtendWith(SpringExtension.class)
@SpringBootTest
@TestPropertySource(properties = {"embedded-dynamodb.use=true"})
@ActiveProfiles("local")
class Post2RepositoryTest {
    @Autowired
    private AmazonDynamoDB dynamoDB;

    @Autowired
    private DynamoDBMapper dynamoDbMapper;

    @Autowired
    private Post2Repository post2Repository;

    @BeforeEach
    void init() {
        CreateTableRequest createTableRequest = dynamoDbMapper.generateCreateTableRequest(Post2.class)
                .withProvisionedThroughput(new ProvisionedThroughput(1L, 1L));

        TableUtils.createTableIfNotExists(dynamoDB, createTableRequest);
    }

    @Test
    @DisplayName("자동ID증가")
    void autoIncrementIdTest() {
        // given
        String title = "(자동) 첫번째 글";
        String title2 = "(자동) 두번째 글";
        String title3 = "(자동) 세번째 글";

        // when
        post2Repository.save(new Post2(title));
        post2Repository.save(new Post2(title2));
        post2Repository.save(new Post2(title3));

        // then
        Iterable<Post2> findPost2s = post2Repository.findAll();
        for (Post2 findPost2 : findPost2s) {
            System.out.println("findPost2.getPostNo() = " + findPost2.getPostNo());
            System.out.println("findPost2.getTitle() = " + findPost2.getTitle());
        }
    }
}

 

수행해보면 ID를 해시키로 생성하고 각각 저장됨을 확인할 수 있었다.

...
response body: {"Items":[{"title":{"S":"(자동) 첫번째 글"},"postNo":{"S":"5eefd649-689a-45d8-a097-f100ec848283"}},{"title":{"S":"(자동) 두번째 글"},"postNo":{"S":"298b35ff-e225-4335-8353-95896803b8d4"}},{"title":{"S":"(자동) 세번째 글"},"postNo":{"S":"0e50c938-7fda-49e7-8085-30b080dc2b0a"}}],"Count":3,"ScannedCount":3}

findPost2.getPostNo() = 5eefd649-689a-45d8-a097-f100ec848283
findPost2.getTitle() = (자동) 첫번째 글
findPost2.getPostNo() = 298b35ff-e225-4335-8353-95896803b8d4
findPost2.getTitle() = (자동) 두번째 글
findPost2.getPostNo() = 0e50c938-7fda-49e7-8085-30b080dc2b0a
findPost2.getTitle() = (자동) 세번째 글

 

 

- 검색기능 추가하기

이번엔 title을 검색하는 기능을 추가해보려 한다.

Post2Repository를 다음과 같이 수정한다.

import org.socialsignin.spring.data.dynamodb.repository.EnableScan;
import org.springframework.data.repository.CrudRepository;

import java.util.List;

@EnableScan
public interface Post2Repository extends CrudRepository<Post2, String> {
    List<Post2> findByTitle(String title);
}

 

by이후에 컬럼명을 쓰면 자동으로 생성해 검색해준다.(규칙이 있음)

해당 규칙에 대한 자세한 설명은 다음 공식홈페이지를 참조하자.

https://docs.spring.io/spring-data/jpa/docs/1.10.1.RELEASE/reference/html/#jpa.sample-app.finders.strategies

 

Spring Data JPA - Reference Documentation

Example 11. Repository definitions using Domain Classes with mixed Annotations interface JpaPersonRepository extends Repository { … } interface MongoDBPersonRepository extends Repository { … } @Entity @Document public class Person { … } This example

docs.spring.io

 

테스트를 다음과 같이 작성한다.

...
    @Test
    @DisplayName("검색어 추가")
    void searchTitleTest() {
        // given
        // given
        String title = "(자동) 첫번째 글";
        String title2 = "(자동) 두번째 글";
        String title3 = "(자동) 세번째 글";

        // when
        post2Repository.save(new Post2(title));
        post2Repository.save(new Post2(title2));
        post2Repository.save(new Post2(title3));

        // then
        List<Post2> byTitle = post2Repository.findByTitle(title2);
        for (Post2 post2 : byTitle) {
            System.out.println("post2.getTitle() = " + post2.getTitle());
        }
        Assertions.assertThat(byTitle.size()).isEqualTo(1);
        Assertions.assertThat(byTitle.get(0).getTitle()).isEqualTo(title2);
    }
...

결과

// 검색을 요청하는 request
request body: {"TableName":"Post2","ScanFilter":{"title":{"AttributeValueList":[{"S":"(자동) 두번째 글"}],"ComparisonOperator":"EQ"}}}
...
// 검색 결과
response body: {"Items":[{"title":{"S":"(자동) 두번째 글"},"postNo":{"S":"24c13b2c-f185-4a19-a15e-0f10d66f351c"}}],"Count":1,"ScannedCount":3}

post2.getTitle() = (자동) 두번째 글

 

 

끝.

 

 

참조:

DynamoDB Local 연동[DynamoDB] Spring Data DynamoDB와 Embedded 개발 환경 구축하기

https://jojoldu.tistory.com/484

 

[DynamoDB] Spring Data DynamoDB와 Embedded 개발 환경 구축하기

모든 코드는 Github에 있습니다. 이번 시간엔 로컬 개발 환경에서 DynamoDB를 Embedded로 활용하는 방법에 대해서 알아보겠습니다. 이미 도커를 적극적으로 테스트와 개발에 사용하고 계신 분들이라면

jojoldu.tistory.com

Aws DynamoDB Local 문서

https://docs.aws.amazon.com/ko_kr/amazondynamodb/latest/developerguide/DynamoDBLocal.DownloadingAndRunning.html

 

Deploying DynamoDB Locally on Your Computer - Amazon DynamoDB

yaml 스크립트를 사용하려면 AWS 액세스 키와 AWS 보안 키를 지정해야 하지만 DynamoDB Local에 액세스하기 위해 유효한 AWS 키가 아니어도 됩니다.

docs.aws.amazon.com

 

JPQ Query 규칙

https://docs.spring.io/spring-data/jpa/docs/1.10.1.RELEASE/reference/html/#jpa.sample-app.finders.strategies

 

Spring Data JPA - Reference Documentation

Example 11. Repository definitions using Domain Classes with mixed Annotations interface JpaPersonRepository extends Repository { … } interface MongoDBPersonRepository extends Repository { … } @Entity @Document public class Person { … } This example

docs.spring.io

 

반응형

댓글