DynamoDB

DynamoDB를 이용한 백엔드 구축하기

학습내용


1. Amazon DynamoDB란?

1.1 DynamoDB 핵심 구성 요소

다음은 기본 DynamoDB 구성 요소입니다.

1.2 기본 키


2. Java를 이용한 Amazon DynamoDB 실습

2.1 Java와 DynamoDB를 사용하는 AWS Lambda 함수 구현하기

2.1.1 DynamoDB 테이블 생성

2.1.2 Lambda 함수 생성

  1. AAWS Serverless App 프로젝트를 Jetbrains 용 AWS Toolkit을 이용하여 생성한다.

    • Project name: DynamoDBLambdaJavaProject
    • Runtime: java11
    • SDK:11버전의 SDK를 선택하거나 없으면 다운로드한 후 선택
    • [주의] Runtime과 SDK를 17로 선택하게 되면, DynamoDB 사용시 Exception이 발생하므로, 반드시 16이하의 버전을 사용해야함
  2. build.gradle 파일을 열고 다음 의존성을 추가하고, 변경사항을 반영합니다.

    dependencies {
         ...
         implementation platform('com.amazonaws:aws-java-sdk-bom:1.12.529')
         implementation 'com.amazonaws:aws-java-sdk-dynamodb'
         ...
    }
    
  3. src/main/java/helloworld/App.java 파일을 다음 코드로 바꿉니다.

    package helloworld;
       
    import com.amazonaws.services.dynamodbv2.AmazonDynamoDB;
    import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClientBuilder;
    import com.amazonaws.services.dynamodbv2.document.DynamoDB;
    import com.amazonaws.services.dynamodbv2.document.Item;
    import com.amazonaws.services.dynamodbv2.document.PutItemOutcome;
    import com.amazonaws.services.dynamodbv2.document.spec.PutItemSpec;
    import com.amazonaws.services.dynamodbv2.model.ConditionalCheckFailedException;
    import com.amazonaws.services.lambda.runtime.Context;
    import com.amazonaws.services.lambda.runtime.RequestHandler;
       
    public class App implements RequestHandler<Person, String> {
        private DynamoDB dynamoDb;
        private String TABLE_NAME = "People";
        private String REGION = "ap-northeast-2";
       
        @Override
        public String handleRequest(Person input, Context context) {
            this.initDynamoDbClient();
       
            putData(input);
            return "Saved Successfully!!";
        }
       
        private void initDynamoDbClient() {
             AmazonDynamoDB client = AmazonDynamoDBClientBuilder.standard()
                     .withRegion(REGION).build();
             this.dynamoDb = new DynamoDB(client);
        }
       
        private PutItemOutcome putData(Person person)
                  throws ConditionalCheckFailedException {
                    return this.dynamoDb.getTable(TABLE_NAME)
                      .putItem(
                        new PutItemSpec().withItem(new Item()
                                .withPrimaryKey("id",person.id)
                                .withString("firstName", person.firstName)
                                .withString("lastName", person.lastName)));
                }
    }
       
    class Person {
        public String firstName;
        public String lastName;
        public int id;
    }
    
  4. src/test/java/helloworld/AppTest.java 파일의 코드를 주석처리하여 컴파일 오류를 제거시킨다.

  5. 로컬에서 실행

    • [필수] Docker 프로세스가 실행된 상태이어야 함

    • IntelliJ IDEA IDE의 화면 상단 타이틀 바에서 “[Local] HelloWorldFunction” 옆의 연두색 실행 버튼 (삼각형)을 클릭

    • [Edit Configuration] 다이얼로그 화면에서 Text – Event Templates – 부분의 드롭다운 메뉴 중에서 API Gateway AWS Proxy를 선택

    • 데이터 창에 다음 JSON 문자열을 입력하고 Run 클릭

      {
        "id": 1,
        "firstName": "Kwawnoo",
        "lastName": "Lee"
      }
      
  6. 결과를 확인하기 위해서는, DynamoDB 콘솔로 이동합니다.

    • 콘솔 왼쪽의 탐색 창에서 [테이블]를 선택합니다.
    • 오른쪽 창에서 People 테이블을 선택합니다.
    • 표 항목 탐색 을 클릭하여 새로운 항목이 추가되었는지 확인합니다.

3. DynamoDB와 AWS Lambda를 이용한 IoT Data 저장 백엔드 구축 실습

사전준비

단계1: DynamoDB 테이블

단계2: Lambda 함수 정의

  1. AAWS Serverless App 프로젝트를 Jetbrains 용 AWS Toolkit을 이용하여 생성한다.

    • Project name: RecordingDeviceDataLambda
    • Rumtime:java11
    • SDK: 11버전의 SDK를 선택하거나 없으면 다운로드한 후 선택
    • [주의] Runtime과 SDK를 17로 선택하게 되면, DynamoDB 사용시 Exception이 발생하므로, 반드시 16이하의 버전을 사용해야함
  2. build.gradle 파일을 열고 다음 의존성을 추가하고, 변경사항을 반영합니다.

    dependencies {
         ...
         implementation platform('com.amazonaws:aws-java-sdk-bom:1.12.529')
         implementation 'com.amazonaws:aws-java-sdk-dynamodb'
         ...
    }
    
  3. src/main/java/helloworld/App.java 파일을 다음 코드로 바꿉니다.

    package helloworld;
       
    import java.text.SimpleDateFormat;
    import java.util.TimeZone;
       
    import com.amazonaws.services.dynamodbv2.AmazonDynamoDB;
    import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClientBuilder;
    import com.amazonaws.services.dynamodbv2.document.DynamoDB;
    import com.amazonaws.services.dynamodbv2.document.Item;
    import com.amazonaws.services.dynamodbv2.document.PutItemOutcome;
    import com.amazonaws.services.dynamodbv2.document.spec.PutItemSpec;
    import com.amazonaws.services.dynamodbv2.model.ConditionalCheckFailedException;
    import com.amazonaws.services.lambda.runtime.Context;
    import com.amazonaws.services.lambda.runtime.RequestHandler;
       
     public class App implements RequestHandler<Thing, String> {
       
         private DynamoDB dynamoDb;
         private String DYNAMODB_TABLE_NAME = "DeviceData";
       
         @Override
         public String handleRequest(Thing input, Context context) {
             this.initDynamoDbClient();
       
             persistData(input);
             return "Success in storing to DB!";
         }
       
         private PutItemOutcome persistData(Thing thing) throws ConditionalCheckFailedException {
       
             // Epoch Conversion Code: https://www.epochconverter.com/
             SimpleDateFormat sdf = new SimpleDateFormat ( "yyyy-MM-dd HH:mm:ss");
             sdf.setTimeZone(TimeZone.getTimeZone("Asia/Seoul"));
             String timeString = sdf.format(new java.util.Date (thing.timestamp*1000));
       
             return this.dynamoDb.getTable(DYNAMODB_TABLE_NAME)
                     .putItem(new PutItemSpec().withItem(new Item().withPrimaryKey("time", thing.timestamp)
                             .withString("temperature", thing.state.reported.temperature)
                             .withString("LED", thing.state.reported.LED)
                             .withString("timestamp",timeString)));
         }
       
         private void initDynamoDbClient() {
             AmazonDynamoDB client = AmazonDynamoDBClientBuilder.standard().
                                     withRegion("ap-northeast-2").build();
       
             this.dynamoDb = new DynamoDB(client);
         }
       
     }
       
     class Thing {
       
         public State state = new State();
         public long timestamp;
       
         public class State {
             public Tag reported = new Tag();
             public Tag desired = new Tag();
       
             public class Tag {
                 public String temperature;
                 public String LED;
             }
         }
       
     }
    
  1. src/test/java/helloworld/AppTest.java 파일의 코드를 주석처리하여 컴파일 오류를 제거시킨다.

단계3: Lambda 함수의 로컬 테스트

작성된 Lambda함수가 정상적으로 동작하는 지를 테스트해 보기 위해서 다음 절차를 수행합니다.

단계4: Lambda 함수의 배포

단계5: Lambda 함수의 원격 테스트

단계6: IoT 규칙 설정

  1. AWS IoT 콘솔로 이동하여 탐색 창에서 메시지 라우팅 > 규칙을 선택합니다.

  2. 규칙생성을 선택하여 AWS IoT 규칙을 생성합니다.

  3. 규칙속성 페이지에서 규칙의 이름(RecodingDeviceInfoRule)을 입력하고, 다음을 선택합니다.

  4. SQL 문 구성 페이지에서 SQL 문 란에 다음 쿼리를 입력합니다.

    SELECT * FROM '$aws/things/MyMKRWiFi1010/shadow/update/accepted'
    
  5. 규칙작업의 드롭다운 메뉴(작업 선택)에서 Lambda을 선택한 다음, Lambda 함수로 * RecordingDeviceDataLambda-HelloWorldFunction-XXXX*를 선택하고, 다음을 선택합니다.

  6. 검토 및 생성 페이지에서 단계별 내용을 검토 후에 생성을 선택합니다.

  7. 작업 추가를 선택합니다.

단계7: 아두이노 연결 테스트

  1. AWS_IoT_DHT11을 다운로드하여 Arduino IDE에서 실행한다.
  2. arduino_secrets.h에서 다음 항목을 사용 환경에 맞도록 수정후, 빌드/업로드 한다.
    • SECRET_SSID: 무선랜 아이디
    • SECRET_PASS: 무선랜 패스워드
    • SECRET_BROKER: AWS IoT broker 엔드포인트
    • SECRET_CERTIFICATE: 디바이스 인증서
  3. 아두이노 IDE의 시리얼 모니터를 열고, 접속이 제대로 이루어지는 지 확인해 본다.
  4. DHT-11 센서의 측정값이 26도를 초과하는 경우에 이메일이 수신되는 지 확인한다.
  5. DynamoDB 콘솔로 이동합니다.
    • 콘솔 왼쪽의 탐색 창에서 [테이블]를 선택합니다.
    • 오른쪽 창에서 DeviceData 테이블을 선택합니다.
    • 항목 탭에서 새로운 DeviceData 항목이 추가되었는지 확인합니다.

4. DynamoDB와 AWS Lambda를 이용한 IoT Data 저장 백엔드 구축 실습 (수정)

이번 실습에서는 3절의 실습 내용을 다음 관점에서 개선해 본다.

  1. 데이터를 저장할 테이블을 파티션 키와 정렬 키로 구성하게 함으로써, 데이블에 저장된 항목을 정렬 키를 기준으로 정렬 시킬 수 있게 한다.

    • [주의] 파티션 키로만 구성된 테이블은 항목의 검색결과를 항상 정렬된 형태로 얻을 수는 없다. 따라서 특정한 키 값을 바탕으로 항목을 정렬시키기 위해서는 테이블을 파티션 키와 정렬 키로 구성된 복합키로 구성해야 한다.
  2. 디바이스로부터 수신된 모든 정보를 테이블에 저장하지 않고, 이전에 수신된 정보와 차이가 나는 경우에만 테이블에 저장하도록 수정한다.

    • 디바이스에서 측정된 디바이스 상태 값을 주기적으로 AWS IoT로 전송하는데, 이 상태 값의 변화가 매우 적은 경우에는 테이블에 중복된 데이터가 많이 저장된다. 따라서 이를 개선하기 위해서는 이전 값과 차이가 나는 정보만 저장하도록 하는 것이 좀더 효율적이다.

    • 이전 값과 차이가 나는 정보를 어떻게 알아낼 수 있는가?

      • 새로운 규칙을 만들고, 규칙 쿼리를 ‘$aws/things/MyMKRWiFi1010/shadow/update/documents’ 로 정의하여, 이 때 수신된 메시지로부터 DB에 저장할 지 말지를 결정하는 Lambda 함수를 연결하면 됩니다.

        • /update/documents 토픽에 대한 응답 메시지는 2개의 기본 노드(previouscurrent)를 포함합니다. previous 노드에는 업데이트가 수행되기 전의 전체 섀도우 문서의 내용이 포함되고, current에는 업데이트가 성공적으로 적용된 후의 전체 섀도우 문서가 포함됩니다.

        • previous 노드와 current 노드의 차이를 비교하여 차이가 있는 경우에만 DB에 데이터를 저장하면 됩니다.

          • /update/documents 토픽에 대한 응답 메시지 예시

            {
              "previous": {
                "state": {
                  "reported": {
                    "temperature": "24.50",
                    "LED": "OFF"
                  }
                },
                "metadata": {
                  "reported": {
                    "temperature": {
                      "timestamp": 1575167548
                    },
                    "LED": {
                      "timestamp": 1575167548
                    }
                  }
                },
                "version": 4447
              },
              "current": {
                "state": {
                  "reported": {
                    "temperature": "24.40",
                    "LED": "OFF"
                  }
                },
                "metadata": {
                  "reported": {
                    "temperature": {
                      "timestamp": 1575178081
                    },
                    "LED": {
                      "timestamp": 1575178081
                    }
                  }
                },
                "version": 4448
              },
              "timestamp": 1575178081
            }
            

단계1: DynamoDB 테이블


단계2: Lambda 함수 정의

  1. AAWS Serverless App 프로젝트를 Jetbrains 용 AWS Toolkit을 이용하여 생성한다.

    • Project name: LoggingDataLambda
    • Rumtime:java11
    • SDK: 11버전의 SDK를 선택하거나 없으면 다운로드한 후 선택 - [주의] Runtime과 SDK를 17로 선택하게 되면, DynamoDB 사용시 Exception이 발생하므로, 반드시 16이하의 버전을 사용해야함
  2. build.gradle 파일을 열고 다음 의존성을 추가하고, 변경사항을 반영합니다.

    dependencies {
         ...
         implementation platform('com.amazonaws:aws-java-sdk-bom:1.12.529')
         implementation 'com.amazonaws:aws-java-sdk-dynamodb'
         ...
    }
    
  3. src/main/java/helloworld/App.java 파일을 다음 코드로 바꿉니다.

    package helloworld;
    import java.text.SimpleDateFormat;
    import java.util.TimeZone;
       
    import com.amazonaws.services.dynamodbv2.AmazonDynamoDB;
    import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClientBuilder;
    import com.amazonaws.services.dynamodbv2.document.DynamoDB;
    import com.amazonaws.services.dynamodbv2.document.Item;
    import com.amazonaws.services.dynamodbv2.document.spec.PutItemSpec;
    import com.amazonaws.services.dynamodbv2.model.ConditionalCheckFailedException;
    import com.amazonaws.services.lambda.runtime.Context;
    import com.amazonaws.services.lambda.runtime.RequestHandler;
       
     public class App implements RequestHandler<Document, String> {
       
         private DynamoDB dynamoDb;
         private String DYNAMODB_TABLE_NAME = "Logging";
       
         @Override
         public String handleRequest(Document input, Context context) {
             this.initDynamoDbClient();
             context.getLogger().log("Input: " + input);
       
             //return null;
             return persistData(input);
         }
       
         private String persistData(Document document) throws ConditionalCheckFailedException {
       
             // Epoch Conversion Code: https://www.epochconverter.com/
             SimpleDateFormat sdf = new SimpleDateFormat ( "yyyy-MM-dd HH:mm:ss");
             sdf.setTimeZone(TimeZone.getTimeZone("Asia/Seoul"));
             String timeString = sdf.format(new java.util.Date (document.timestamp*1000));
       
             // temperature와 LED 값이 이전상태와 동일한 경우 테이블에 저장하지 않고 종료
             if (document.current.state.reported.temperature.equals(
                 document.previous.state.reported.temperature) &&
                     document.current.state.reported.LED.equals(
                     document.previous.state.reported.LED)) {
                     return null;
             }
       
             return this.dynamoDb.getTable(DYNAMODB_TABLE_NAME)
                     .putItem(new PutItemSpec().withItem(new Item()
                             .withPrimaryKey("deviceId", document.device)
                             .withLong("time", document.timestamp)
                             .withString("temperature", document.current.state.reported.temperature)
                             .withString("LED", document.current.state.reported.LED)
                             .withString("timestamp",timeString)))
                     .toString();
         }
       
         private void initDynamoDbClient() {
             AmazonDynamoDB client = AmazonDynamoDBClientBuilder.standard().
                                     withRegion("ap-northeast-2").build();
       
             this.dynamoDb = new DynamoDB(client);
         }
       
     }
       
     /**
            * AWS IoT은(는) 섀도우 업데이트가 성공적으로 완료될 때마다 /update/documents 주제에 다음 상태문서를 게시합니다
            * JSON 형식의 상태문서는 2개의 기본 노드를 포함합니다. previous 및 current.
            * previous 노드에는 업데이트가 수행되기 전의 전체 섀도우 문서의 내용이 포함되고,
            * current에는 업데이트가 성공적으로 적용된 후의 전체 섀도우 문서가 포함됩니다.
            * 섀도우가 처음 업데이트(생성)되면 previous 노드에는 null이 포함됩니다.
            * timestamp는 상태문서가 생성된 시간 정보이고,
            * device는 상태문서에 포함된 값은 아니고, Iot규칙을 통해서 Lambda함수로 전달된 값이다.
            * 이 값을 해당 규칙과 관련된 사물이름을 나타낸다.
      **/
       
      class Document {
       public Thing previous;         
       public Thing current;
       public long timestamp;
       public String device;        // AWS IoT에 등록된 사물 이름
      }
       
      class Thing {
       public State state = new State();
       public long timestamp;
       public String clientToken;
       
       public class State {
       
           public Tag reported = new Tag();
           public Tag desired = new Tag();
       
           public class Tag {
               public String temperature;
               public String LED;
           }
       
       }
      }
    
  4. src/test/java/helloworld/AppTest.java 파일의 코드를 주석처리하여 컴파일 오류를 제거시킨다.

단계3: Lambda 함수의 로컬 테스트

작성된 Lambda함수가 정상적으로 동작하는 지를 테스트해 보기 위해서 다음 절차를 수행합니다.

단계4: Lambda 함수의 배포

단계5: Lambda 함수의 원격 테스트


단계6: IoT 규칙 설정

  1. AWS IoT 콘솔로 이동하여 탐색 창에서 메시지 라우팅 > 규칙을 선택합니다.

  2. 규칙생성을 선택하여 AWS IoT 규칙을 생성합니다.

  3. 규칙속성 페이지에서 규칙의 이름(LoggingRule)을 입력하고, 다음을 선택합니다.

  4. SQL 문 구성 페이지에서 SQL 문 란에 다음 쿼리를 입력합니다.

    SELECT *, 'MyMKRWiFi1010' as device FROM '$aws/things/MyMKRWiFi1010/shadow/update/documents'
    
    • ‘$aws/things/MyMKRWiFi1010/shadow/update/documents’ 주제로 전송된 메시지가 규칙을 트리거하고, 이 때 전달된 메시지의 모든 항목과 함께 항목 값이 MyMKRWiFi1010인 device 항목을 규칙에 설정된 작업의 입력값으로 전달한다.
  5. 규칙작업의 드롭다운 메뉴(작업 선택)에서 Lambda을 선택한 다음, Lambda 함수로 LoggingDataLambda-HelloWorldFunction-XXXX를 선택하고, 다음을 선택합니다.

  6. 검토 및 생성 페이지에서 단계별 내용을 검토 후에 생성을 선택합니다.

  7. 작업 추가를 선택합니다.

  8. LoggingRule 규칙이 목록에 표시되는 것을 확인한다.


단계7: 테스트

  1. AWS_IoT_DHT11을 다운로드하여 Arduino IDE에서 실행한다.
  2. arduino_secrets.h에서 다음 항목을 사용 환경에 맞도록 수정후, 빌드/업로드 한다.
    • SECRET_SSID: 무선랜 아이디
    • SECRET_PASS: 무선랜 패스워드
    • SECRET_BROKER: AWS IoT broker 엔드포인트
    • SECRET_CERTIFICATE: 디바이스 인증서
  3. 아두이노 IDE의 시리얼 모니터를 열고, 접속이 제대로 이루어지는 지 확인해 본다.
  4. DHT-11 센서의 측정값이 26도를 초과하는 경우에 이메일이 수신되는 지 확인한다.
  5. DynamoDB 콘솔로 이동합니다.
    • 콘솔 왼쪽의 탐색 창에서 [테이블]를 선택합니다.
    • 오른쪽 창에서 Logging 테이블을 선택합니다.
    • 항목 탭에서 새로운 항목이 추가되었는지 확인합니다.