1. 개요

AWS Lambda를 사용하면 쉽게 배포하고 확장할 수 있는 경량 애플리케이션을 생성할 수 있습니다. 성능상의 이유로 Spring Cloud Function 과 같은 프레임워크를 사용할 수 있지만 일반적으로 가능한 한 적은 프레임워크 코드를 사용합니다.

때로는 Lambda에서 관계형 데이터베이스에 액세스해야 합니다. 이것이 HibernateJPA 가 매우 유용할 수 있는 입니다. 그러나 Spring 없이 Lambda에 Hibernate를 어떻게 추가합니까?

이 사용방법(예제)에서는 Lambda 내에서 RDBMS를 사용할 때의 문제와 Hibernate가 언제 어떻게 유용할 수 있는지 살펴보겠습니다. 이 예제에서는 서버리스 애플리케이션 모델을 사용하여 데이터에 대한 REST 인터페이스를 빌드합니다.

Docker 와 AWS SAM CLI를 사용하여 로컬 머신에서 모든 것을 테스트하는 방법을 살펴보겠습니다 .

2. Lambda에서 RDBMS 및 최대 절전 모드를 사용하는 문제

콜드 스타트 ​​속도를 높이려면 Lambda 코드는 가능한 한 작아야 합니다. 또한 Lambda는 밀리초 내에 작업을 수행할 수 있어야 합니다. 그러나 관계형 데이터베이스를 사용하면 많은 프레임워크 코드가 포함될 수 있고 더 느리게 실행될 수 있습니다.

클라우드 네이티브 애플리케이션에서는 클라우드 네이티브 기술을 사용하여 설계하려고 합니다. Dynamo DB 와 같은 서버리스 데이터베이스 는 Lambda에 더 적합할 수 있습니다. 그러나 관계형 데이터베이스의 필요성은 우리 프로젝트의 다른 우선 순위에서 비롯될 수 있습니다.

2.1. Lambda에서 RDBMS 사용

Lambda는 짧은 시간 동안 실행된 다음 해당 컨테이너가 일시 중지됩니다. 컨테이너는 향후 호출을 위해 재사용하거나 더 이상 필요하지 않은 경우 AWS 런타임에서 폐기할 수 있습니다. 이것은 컨테이너가 요구 하는 모든 리소스가 단일 호출의 수명 내에 신중하게 관리되어야 함을 의미합니다 .

특히 열린 연결이 안전하게 폐기되지 않고 잠재적으로 열려 있을 수 있으므로 데이터베이스에 대한 기존 연결 풀링에 의존할 수 없습니다. 호출하는 동안 연결 풀을 사용할 수 있지만 매번 연결 풀을 만들어야 합니다 . 또한 기능이 종료되면 모든 연결을 종료하고 모든 리소스해제해야 합니다 .

즉, 데이터베이스와 함께 Lambda를 사용하면 연결 문제가 발생할 수 있습니다. Lambda의 갑작스러운 확장은 너무 많은 연결을 소비할 수 있습니다. Lambda가 연결을 즉시 해제할 수 있지만 우리는 여전히 다음 Lambda 호출을 위해 연결을 준비할 수 있는 데이터베이스에 의존합니다. 따라서 관계형 데이터베이스를 사용하는 모든 Lambda에서 최대 동시성 제한사용 하는 것이 좋습니다 .

일부 프로젝트에서 Lambda는 RDBMS에 연결하기 위한 최선의 선택이 아니며 연결 풀이 있는 기존 Spring Data 서비스(EC2 또는 ECS에서 실행)가 더 나은 솔루션일 수 있습니다.

2.2. 최대 절전 모드의 경우

Hibernate가 필요한지 판단하는 좋은 방법은 Hibernate 없이 어떤 종류의 코드를 작성해야 하는지 묻는 것입니다.

Hibernate를 사용하지 않으면 필드와 열 사이에 복잡한 조인이나 많은 상용구 매핑을 코딩해야 하는 경우 코딩 관점에서 Hibernate가 좋은 솔루션입니다. 우리 애플리케이션이 높은 부하를 경험하지 않거나 짧은 대기 시간이 필요하지 않다면 Hibernate의 오버헤드는 문제가 되지 않을 수 있습니다.

2.3. 최대 절전 모드는 중량급 기술입니다.

그러나 Lambda에서 Hibernate를 사용하는 비용도 고려해야 합니다.

Hibernate jar 파일의 크기는 7MB입니다. Hibernate는 시작 시 어노테이션을 검사하고 ORM 기능을 생성하는 데 시간이 걸립니다. 이것은 엄청나게 강력하지만 Lambda의 경우 과도할 수 있습니다. Lambda는 일반적으로 작은 작업을 수행하도록 작성되므로 Hibernate의 오버헤드는 이점을 누릴 가치가 없을 수 있습니다.

JDBC를 직접 사용하는 것이 더 쉬울 수 있습니다 . 또는 JDBI 와 같은 가벼운 ORM과 같은 프레임워크 는 너무 많은 오버헤드 없이 쿼리에 대한 우수한 추상화를 제공할 수 있습니다.

3. 적용 예

이 사용방법(예제)에서는 소량 운송 회사를 위한 추적 애플리케이션을 빌드합니다. 그들이 위탁품 을 만들기 위해 고객으로부터 큰 품목을 수집한다고 상상해 봅시다 . 그런 다음 해당 화물이 이동하는 곳마다 타임스탬프와 함께 체크인되어 고객이 모니터링할 수 있습니다. 각 화물에는 출발지목적지가 있으며 이에 대해 what3words.com 을 지리적 위치 서비스로 사용 합니다.

또한 연결 및 재시도가 불량한 모바일 장치를 사용하고 있다고 가정해 보겠습니다. 따라서 위탁이 생성된 후 그에 대한 나머지 정보는 랜덤의 순서로 도착할 수 있습니다. 이러한 복잡성은 각 위탁에 대해 두 개의 List(항목 및 체크인)을 필요로 하는 것과 함께 Hibernate를 사용해야 하는 좋은 이유입니다.

3.1. API 디자인

다음 메서드를 사용하여 REST API를 만듭니다.

  • POST /consignment – 새 화물을 만들고 ID를 반환하고 출처  와  목적지를 제공 합니다 . 다른 작업보다 먼저 수행해야 합니다.
  • POST /consignment/{id}/item – 화물에 품목을 추가합니다. 항상 List의 끝에 추가
  • POST /consignment/{id}/checkin위치 와 타임스탬프를 제공하여 도중에 어느 위치에서든 화물을 체크인합니다 . 항상 타임스탬프 순서대로 데이터베이스에 유지됩니다.
  • GET /consignment/{id} – 목적지에 도달했는지 여부를 포함하여 화물의 전체 이력을 가져옵니다.

3.2. 람다 설계

단일 Lambda 함수를 사용하여 이 REST API에 서버리스 애플리케이션 모델 을 제공하여 정의합니다. 즉, 단일 Lambda 핸들러 함수가 위의 모든 요청을 충족할 수 있어야 합니다.

AWS에 배포하는 오버헤드 없이 빠르고 쉽게 테스트할 수 있도록 개발 머신에서 모든 것을 테스트합니다.

4. 람다 생성

API를 충족하지만 아직 데이터 액세스 계층을 구현하지 않은 새로운 Lambda를 설정해 보겠습니다.

4.1. 전제 조건

먼저 Docker 가 아직 없으면 설치 해야 합니다 . 테스트 데이터베이스를 호스팅하는 데 필요하며 AWS SAM CLI에서 Lambda 런타임을 시뮬레이션하는 데 사용됩니다.

Docker가 있는지 테스트할 수 있습니다.

$ docker --version
Docker version 19.03.12, build 48a66213fe

다음으로 AWS SAM CLI설치하고 테스트해야 합니다.

$ sam --version
SAM CLI, version 1.1.0

이제 Lambda를 생성할 준비가 되었습니다.

4.2. SAM 템플릿 만들기

SAM CLI는 새로운 Lambda 함수를 생성하는 방법을 제공합니다.

$ sam init

그러면 새 프로젝트의 설정을 묻는 메시지가 표시됩니다. 다음 옵션을 선택하겠습니다.

1 - AWS Quick Start Templates
13 - Java 8
1 - maven
Project name - shipping-tracker
1 - Hello World Example: Maven

이러한 옵션 번호는 이후 버전의 SAM 도구에 따라 다를 수 있습니다.

이제 스텁 애플리케이션이 있는 Shipping-tracker 라는 새 디렉토리가 있어야 합니다  . template.yaml 파일 의 내용을 보면 간단한 REST API를 사용하여 HelloWorldFunction 이라는 함수를 찾을 수 있습니다 .

Events:
  HelloWorld:
    Type: Api 
    Properties:
      Path: /hello
      Method: get

기본적으로 이것은 /hello 에 대한 기본 GET 요청을 충족합니다 . sam사용 하여 빌드하고 테스트 하여 모든 것이 제대로 작동하는지 빠르게 테스트해야 합니다.

$ sam build
... lots of maven output
$ sam start-api

그런 다음 curl을 사용하여 hello world API를  테스트할 수 있습니다 .

$ curl localhost:3000/hello
{ "message": "hello world", "location": "192.168.1.1" }

그런 다음 CTRL+C 사용 하여 프로그램을 중단 하여 API 수신기를 실행하는  sam을 중지하겠습니다 .

이제 빈 Java 8 Lambda가 있으므로 API가 되도록 사용자 지정해야 합니다.

4.3. API 생성

API를 생성하려면 template.yaml 파일 Events 섹션에  자체 경로를 추가해야  합니다.

CreateConsignment:
  Type: Api 
  Properties:
    Path: /consignment
    Method: post
AddItem:
  Type: Api
  Properties:
    Path: /consignment/{id}/item
    Method: post
CheckIn:
  Type: Api
  Properties:
    Path: /consignment/{id}/checkin
    Method: post
ViewConsignment:
  Type: Api
  Properties:
    Path: /consignment/{id}
    Method: get

호출하는 함수의 이름을 HelloWorldFunction 에서  ShippingFunction으로 변경해 보겠습니다  .

Resources:
  ShippingFunction:
    Type: AWS::Serverless::Function 

다음으로 디렉토리 이름을 ShippingFunction 으로 바꾸고 Java 패키지를 helloworld 에서  com.baeldung.lambda.shipping으로 변경합니다 . , 새 위치를 가리키도록 template.yamlCodeUri 및  Handler 속성 을 업데이트해야 합니다 .

Properties:
  CodeUri: ShippingFunction
  Handler: com.baeldung.lambda.shipping.App::handleRequest

마지막으로 자체 구현을 위한 공간을 만들기 위해 핸들러의 본문을 교체해 보겠습니다.

public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent input, Context context) {
    Map<String, String> headers = new HashMap<>();
    headers.put("Content-Type", "application/json");
    headers.put("X-Custom-Header", "application/json");

    return new APIGatewayProxyResponseEvent()
      .withHeaders(headers)
      .withStatusCode(200)
      .withBody(input.getResource());
}

단위 테스트는 좋은 생각이지만 이 예제에서는 src/test 디렉토리를 삭제하여 제공된 단위 테스트도 삭제합니다 .

4.4. 빈 API 테스트

이제 우리는 주변을 옮기고 API와 기본 핸들러를 만들었습니다. 모든 것이 여전히 작동하는지 다시 확인하겠습니다.

$ sam build
... maven output
$ sam start-api

curl사용 하여 HTTP GET 요청을 테스트해 보겠습니다 .

$ curl localhost:3000/consignment/123
/consignment/{id}

POST에 curl -d사용할 수도 있습니다 .

$ curl -d '{"source":"data.orange.brings", "destination":"heave.wipes.clay"}' \
  -H 'Content-Type: application/json' \
  http://localhost:3000/consignment/
/consignment

보시다시피 두 요청 모두 성공적으로 종료됩니다. 스텁 코드는 다양한 서비스 방법에 대한 라우팅을 설정할 때 사용할 수 있는 리소스( 요청 경로)를 출력합니다 .

4.5. Lambda 내에서 엔드포인트 생성

4개의 엔드포인트를 처리하기 위해 단일 Lambda 함수를 사용하고 있습니다. 동일한 코드베이스에서 각 엔드포인트에 대해 서로 다른 핸들러 클래스를 생성하거나 각 엔드포인트에 대해 별도의 애플리케이션을 작성할 수 있었지만 관련 API를 함께 유지하면 단일 Lambda 플릿이 공통 코드로 서비스를 제공할 수 있으므로 다음을 더 잘 사용할 수 있습니다. 자원.

그러나 각 요청을 적절한 Java 기능으로 전달하기 위해 REST 컨트롤러와 동등한 것을 빌드해야 합니다. 따라서 스텁 ShippingService 클래스를 만들고 핸들러에서 이 클래스로 라우팅합니다.

public class ShippingService {
    public String createConsignment(Consignment consignment) {
        return UUID.randomUUID().toString();
    }

    public void addItem(String consignmentId, Item item) {
    }

    public void checkIn(String consignmentId, Checkin checkin) {
    }

    public Consignment view(String consignmentId) {
        return new Consignment();
    }
}

ConsignmentItem 및  Checkin 에 대한 빈 클래스도 생성 합니다. 이것들은 곧 우리의 모델이 될 것입니다.

이제 서비스가 있으므로 리소스사용 하여 적절한 서비스 메서드로 라우팅해 보겠습니다 . 요청을 서비스로 라우팅하기 위해 핸들러에 switch 문을 추가 합니다.

Object result = "OK";
ShippingService service = new ShippingService();

switch (input.getResource()) {
    case "/consignment":
        result = service.createConsignment(
          fromJson(input.getBody(), Consignment.class));
        break;
    case "/consignment/{id}":
        result = service.view(input.getPathParameters().get("id"));
        break;
    case "/consignment/{id}/item":
        service.addItem(input.getPathParameters().get("id"),
          fromJson(input.getBody(), Item.class));
        break;
    case "/consignment/{id}/checkin":
        service.checkIn(input.getPathParameters().get("id"),
          fromJson(input.getBody(), Checkin.class));
        break;
}

return new APIGatewayProxyResponseEvent()
  .withHeaders(headers)
  .withStatusCode(200)
  .withBody(toJson(result));

잭슨사용 하여 fromJson 및  toJson 기능 을 구현할  수 있습니다 .

4.6. 스텁 구현

지금까지 AWS Lambda를 생성하여 API를 지원하고, sam 및  curl을 사용하여 테스트하고 , 핸들러 내에서 기본 라우팅 기능을 구축 하는 방법을 배웠습니다 . 잘못된 입력에 대해 더 많은 오류 처리를 추가할 수 있습니다.

template.yaml 내의 매핑은 이미 AWS API Gateway가 API의 올바른 경로가 아닌 요청을 필터링할 것으로 예상 한다는 점에 유의해야 합니다 . 따라서 잘못된 경로에 대한 오류 처리가 덜 필요합니다.

이제 데이터베이스, 엔티티 모델 및 최대 절전 모드로 서비스를 구현할 시간입니다.

5. 데이터베이스 설정

이 예에서는 PostgreSQL을 RDBMS로 사용합니다. 모든 관계형 데이터베이스가 작동할 수 있습니다.

5.1. Docker에서 PostgreSQL 시작

먼저 PostgreSQL 도커 이미지를 가져옵니다.

$ docker pull postgres:latest
... docker output
Status: Downloaded newer image for postgres:latest
docker.io/library/postgres:latest

이제 이 데이터베이스를 실행할 도커 네트워크를 생성해 보겠습니다. 이 네트워크를 통해 Lambda는 데이터베이스 컨테이너와 통신할 수 있습니다.

$ docker network create shipping

다음으로 해당 네트워크 내에서 데이터베이스 컨테이너를 시작해야 합니다.

docker run --name postgres \
  --network shipping \
  -e POSTGRES_PASSWORD=password \
  -d postgres:latest

–name을 사용  하여 컨테이너에 postgres 라는 이름을 지정했습니다 . –network를 사용  하여 배송 도커 네트워크에 추가했습니다 . 서버의 비밀번호를 설정하기 위해 -e 스위치로 설정 한 환경 변수 POSTGRES_PASSWORD를 사용했습니다 .

또한 -d사용  하여 쉘을 묶지 않고 백그라운드에서 컨테이너를 실행했습니다. PostgreSQL은 몇 초 후에 시작됩니다.

5.2. 스키마 추가

테이블에 대한 새 스키마가 필요하므로 PostgreSQL 컨테이너 내부의 psql 클라이언트를 사용 하여 배송 스키마 를 추가해 보겠습니다 .

$ docker exec -it postgres psql -U postgres
psql (12.4 (Debian 12.4-1.pgdg100+1))
Type "help" for help.

postgres=#

이 셸 내에서 스키마를 만듭니다.

postgres=# create schema shipping;
CREATE SCHEMA

그런 다음 CTRL+D사용 하여 쉘을 종료합니다.

이제 PostgreSQL이 실행되고 Lambda에서 사용할 준비가 되었습니다.

6. 엔티티 모델과 DAO 추가하기

이제 데이터베이스가 있으므로 엔터티 모델과 DAO를 생성해 보겠습니다. 단일 연결만 사용하고 있지만 Hikari 연결 풀사용하여 단일 호출에서 데이터베이스에 대해 여러 연결을 실행해야 하는 Lambda에 대해 구성할 수 있는 방법을 살펴보겠습니다.

6.1. 프로젝트에 하이버네이트 추가하기

HibernateHikari Connection Pool 모두에 대한 의존성을 pom.xml추가할 것이다 . PostgreSQL JDBC 드라이버 도 추가합니다 .

<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-core</artifactId>
    <version>5.4.21.Final</version>
</dependency>
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-hikaricp</artifactId>
    <version>5.4.21.Final</version>
</dependency>
<dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
    <version>42.2.16</version>
</dependency>

6.2. 엔티티 모델

엔터티 개체를 구체화해 보겠습니다. 위탁은 항목과 체크인,뿐만 아니라 List이  소스 , 목적지 , 그것은 (그것의 최종 대상으로 확인 여부 즉,) 아직 도착 여부 :

@Entity(name = "consignment")
@Table(name = "consignment")
public class Consignment {
    private String id;
    private String source;
    private String destination;
    private boolean isDelivered;
    private List items = new ArrayList<>();
    private List checkins = new ArrayList<>();
    
    // getters and setters
}

클래스에 엔티티와 테이블 이름으로 어노테이션을 달았습니다. 우리는 getter와 setter도 제공할 것입니다. 열 이름으로 getter를 표시해 보겠습니다.

@Id
@Column(name = "consignment_id")
public String getId() {
    return id;
}

@Column(name = "source")
public String getSource() {
    return source;
}

@Column(name = "destination")
public String getDestination() {
    return destination;
}

@Column(name = "delivered", columnDefinition = "boolean")
public boolean isDelivered() {
    return isDelivered;
}

List의 경우 @ElementCollection 어노테이션을 사용하여 위탁 테이블 과 외래 키 관계가 있는 별도의 테이블에 정렬된 List을 만듭니다  .

@ElementCollection(fetch = EAGER)
@CollectionTable(name = "consignment_item", joinColumns = @JoinColumn(name = "consignment_id"))
@OrderColumn(name = "item_index")
public List getItems() {
    return items;
}

@ElementCollection(fetch = EAGER)
@CollectionTable(name = "consignment_checkin", joinColumns = @JoinColumn(name = "consignment_id"))
@OrderColumn(name = "checkin_index")
public List getCheckins() {
    return checkins;
}

여기에서 Hibernate가 자체적으로 비용을 지불하기 시작하여 컬렉션 관리 작업을 아주 쉽게 수행합니다.

항목 개체는 더 간단하다 :

@Embeddable
public class Item {
    private String location;
    private String description;
    private String timeStamp;

    @Column(name = "location")
    public String getLocation() {
        return location;
    }

    @Column(name = "description")
    public String getDescription() {
        return description;
    }

    @Column(name = "timestamp")
    public String getTimeStamp() {
        return timeStamp;
    }

    // ... setters omitted
}

상위 개체에서 List 정의의 일부가 될 수 있도록 @Embeddable표시됩니다 .

마찬가지로 Checkin을 정의합니다 .

@Embeddable
public class Checkin {
    private String timeStamp;
    private String location;

    @Column(name = "timestamp")
    public String getTimeStamp() {
        return timeStamp;
    }

    @Column(name = "location")
    public String getLocation() {
        return location;
    }

    // ... setters omitted
}

6.3. 배송 DAO 생성

우리  ShippingDao의 에 의존 클래스는 오픈 최대 절전 모드 전달되는 세션을 . 세션을 관리하려면 ShippingService 가 필요합니다 .

public void save(Session session, Consignment consignment) {
    Transaction transaction = session.beginTransaction();
    session.save(consignment);
    transaction.commit();
}

public Optional<Consignment> find(Session session, String id) {
    return Optional.ofNullable(session.get(Consignment.class, id));
}

우리는 이것을 나중에 ShippingService에 연결할 것입니다.

7. 하이버네이트 라이프사이클

지금까지 우리 엔터티 모델과 DAO는 비-람다 구현과 비슷합니다. 다음 과제는 Lambda의 수명 주기 내에서 Hibernate SessionFactory를 생성하는 것  입니다.

7.1. 데이터베이스는 어디에 있습니까?

Lambda에서 데이터베이스에 액세스하려면 구성해야 합니다. JDBC URL과 데이터베이스 자격 증명을 template.yaml 내의 환경 변수에 넣습니다 .

Environment: 
  Variables:
    DB_URL: jdbc:postgresql://postgres/postgres
    DB_USER: postgres
    DB_PASSWORD: password

이러한 환경 변수는 Java 런타임에 주입됩니다. 포스트 그레스 사용자가 우리의 부두 노동자 PostgreSQL의 컨테이너의 기본값입니다. 우리는 암호 할당 된 암호를 우리가 용기 일찍 시작했을 때.

DB_URL 내에는 서버 이름이 있습니다. //postgres 는 컨테이너에 부여한 이름이며 데이터베이스 이름 postgres  는 기본 데이터베이스입니다.

이 예에서 이러한 값을 하드 코딩하지만 SAM 템플릿을 사용하면 입력 및 매개변수 재정의 를 선언할 수 있습니다. 따라서 나중에 매개변수화할 수 있습니다.

7.2. 세션 팩토리 생성

구성할 Hibernate와 Hikari 연결 풀이 모두 있습니다. Hibernate에 설정을 제공하기 위해 Map에 추가합니다 .

Map<String, String> settings = new HashMap<>();
settings.put(URL, System.getenv("DB_URL"));
settings.put(DIALECT, "org.hibernate.dialect.PostgreSQLDialect");
settings.put(DEFAULT_SCHEMA, "shipping");
settings.put(DRIVER, "org.postgresql.Driver");
settings.put(USER, System.getenv("DB_USER"));
settings.put(PASS, System.getenv("DB_PASSWORD"));
settings.put("hibernate.hikari.connectionTimeout", "20000");
settings.put("hibernate.hikari.minimumIdle", "1");
settings.put("hibernate.hikari.maximumPoolSize", "2");
settings.put("hibernate.hikari.idleTimeout", "30000");
settings.put(HBM2DDL_AUTO, "create-only");
settings.put(HBM2DDL_DATABASE_ACTION, "create");

여기에서는 System.getenv사용 하여 환경에서 런타임 설정을 가져옵니다. 애플리케이션 이 데이터베이스 테이블을 생성 하도록 HBM2DDL_ 설정을 추가했습니다  . 그러나 데이터베이스 스키마가 생성된 후 이러한 행을 어노테이션 처리하거나 제거해야 하며 Lambda가 프로덕션에서 이 작업을 수행하도록 허용하지 않아야 합니다. 하지만 지금 테스트에 도움이 됩니다.

우리가 볼 수 있듯이 많은 설정에는 Hibernate AvailableSettings 클래스에 이미 정의된 상수가  있지만 Hikari 고유의 설정은 그렇지 않습니다.

이제 설정이 완료 되었으므로 SessionFactory 를 빌드해야 합니다 . 엔티티 클래스를 개별적으로 추가합니다.

StandardServiceRegistry registry = new StandardServiceRegistryBuilder()
  .applySettings(settings)
  .build();

return new MetadataSources(registry)
  .addAnnotatedClass(Consignment.class)
  .addAnnotatedClass(Item.class)
  .addAnnotatedClass(Checkin.class)
  .buildMetadata()
  .buildSessionFactory();

7.3. 리소스 관리

시작 시 Hibernate는 엔터티 객체에 대한 코드 생성을 수행합니다. 응용 프로그램이 이 작업을 두 번 이상 수행하도록 설계되지 않았으며 이를 수행하는 데 시간과 메모리를 사용합니다. 따라서 Lambda의 콜드 스타트 ​​시 이 작업을 한 번 수행하려고 합니다.

따라서 핸들러 객체가 Lambda 프레임워크에 의해 생성되므로 SessionFactory생성해야 합니다 . 핸들러 클래스의 이니셜라이저 List에서 이 작업을 수행할 수 있습니다.

private SessionFactory sessionFactory = createSessionFactory();

그러나 SessionFactory 에는 연결 풀이 있으므로 호출 간에 연결을 열어두고 데이터베이스 리소스를 묶을 위험이 있습니다.

그보다 더 나쁜 것은 AWS 런타임에 의해 폐기되는 경우 Lambda가 리소스를 종료하도록 허용하는 수명 주기 이벤트가 없다는 것입니다 . 따라서 이러한 방식으로 유지되는 연결이 제대로 해제되지 않을 가능성이 있습니다.

연결 풀 에 대한 SessionFactory파고 모든 연결을 명시적으로 닫음으로써 이 문제를 해결할 수 있습니다  .

private void flushConnectionPool() {
    ConnectionProvider connectionProvider = sessionFactory.getSessionFactoryOptions()
      .getServiceRegistry()
      .getService(ConnectionProvider.class);
    HikariDataSource hikariDataSource = connectionProvider.unwrap(HikariDataSource.class);
    hikariDataSource.getHikariPoolMXBean().softEvictConnections();
}

이것은 우리가 연결 을 해제할 수 있도록 softEvictConnections제공하는 Hikari 연결 풀을 지정했기 때문에 이 경우에 작동  합니다.

우리는주의해야한다  SessionFactory에 의  가까운 방법은 또한 가까운 연결, 그러나 또한 렌더링 할 것 SessionFactory를 사용할 수 있습니다.

7.4. 핸들러에 추가

이제 핸들러가 세션 팩토리를 사용하고 연결을 해제하는지 확인해야 합니다 . 이를 염두에 두고 대부분의 컨트롤러 기능을 routeRequest 라는 메서드로 추출 하고 finally 블록 의 리소스를 해제하도록 핸들러를 수정해 보겠습니다 .

try {
    ShippingService service = new ShippingService(sessionFactory, new ShippingDao());
    return routeRequest(input, service);
} finally {
    flushConnectionPool();
}

또한 SessionFactory 및  ShippingDao 가 속성으로 생성자를 통해 주입 되도록 Shipping Service 를  변경  했지만 아직 사용하지 않습니다.

7.5. 최대 절전 모드 테스트

이 시점에서 ShippingService 는 아무 것도 하지 않지만 Lambda를 호출하면 Hibernate가 시작되고 DDL을 생성해야 합니다.

이에 대한 설정을 어노테이션 처리하기 전에 생성하는 DDL을 다시 확인하겠습니다.

$ sam build
$ sam local start-api --docker-network shipping

이전과 같이 애플리케이션을 빌드하지만 이제 sam local 에 –docker-network 매개변수를  추가합니다 . 이렇게 하면 Lambda가 컨테이너 이름을 사용하여 데이터베이스 컨테이너에 연결할 수 있도록 데이터베이스와 동일한 네트워크 내에서 테스트 Lambda를 실행합니다 .

curl 을 사용하여 엔드포인트에 처음 도달할 때  테이블이 생성되어야 합니다.

$ curl localhost:3000/consignment/123
{"id":null,"source":null,"destination":null,"items":[],"checkins":[],"delivered":false}

스텁 코드는 여전히 빈 Consignment를 반환했습니다 . 그러나 이제 데이터베이스를 확인하여 테이블이 생성되었는지 확인하겠습니다.

$ docker exec -it postgres pg_dump -s -U postgres
... DDL output
CREATE TABLE shipping.consignment_item (
    consignment_id character varying(255) NOT NULL,
...

Hibernate 설정이 작동하는 것에 만족 하면 HBM2DDL_ 설정을 어노테이션 처리할 수 있습니다  .

8. 비즈니스 로직 완성

그 모든 유물은 확인하는 것입니다 ShippingService가 사용 ShippingDao는 비즈니스 로직을 구현합니다. 각 메소드는 try-with-resources 블록에 세션 팩토리를 생성하여  닫히도록 합니다.

8.1. 위탁 생성

새 화물이 배송되지 않았으며 새 ID를 받아야 합니다. 그런 다음 데이터베이스에 저장해야 합니다.

public String createConsignment(Consignment consignment) {
    try (Session session = sessionFactory.openSession()) {
        consignment.setDelivered(false);
        consignment.setId(UUID.randomUUID().toString());
        shippingDao.save(session, consignment);
        return consignment.getId();
    }
}

8.2. 위탁보기

위탁을 받으려면 데이터베이스에서 ID로 읽어야 합니다. REST API는 알 수 없는 요청에 대해 Not Found반환해야 하지만 이 예에서는 아무것도 발견되지 않으면 빈 화물만 반환합니다.

public Consignment view(String consignmentId) {
    try (Session session = sessionFactory.openSession()) {
        return shippingDao.find(session, consignmentId)
          .orElseGet(Consignment::new);
    }
}

8.3. 아이템 추가

항목은 받은 순서대로 항목 List에 들어갑니다.

public void addItem(String consignmentId, Item item) {
    try (Session session = sessionFactory.openSession()) {
        shippingDao.find(session, consignmentId)
          .ifPresent(consignment -> addItem(session, consignment, item));
    }
}

private void addItem(Session session, Consignment consignment, Item item) {
    consignment.getItems()
      .add(item);
    shippingDao.save(session, consignment);
}

이상적으로는 위탁물이 존재하지 않는 경우 더 나은 오류 처리가 가능하지만 이 예에서는 존재하지 않는 위탁물이 무시됩니다.

8.4. 체크인

체크인은 요청을 받은 때가 아니라 발생한 순서대로 정렬해야 합니다. 또한 품목이 최종 목적지에 도달하면 다음과 같이 배달됨으로 표시되어야 합니다.

public void checkIn(String consignmentId, Checkin checkin) {
    try (Session session = sessionFactory.openSession()) {
        shippingDao.find(session, consignmentId)
          .ifPresent(consignment -> checkIn(session, consignment, checkin));
    }
}

private void checkIn(Session session, Consignment consignment, Checkin checkin) {
    consignment.getCheckins().add(checkin);
    consignment.getCheckins().sort(Comparator.comparing(Checkin::getTimeStamp));
    if (checkin.getLocation().equals(consignment.getDestination())) {
        consignment.setDelivered(true);
    }
    shippingDao.save(session, consignment);
}

9. 앱 테스트

백악관에서 엠파이어 스테이트 빌딩으로 여행하는 패키지를 시뮬레이션해 보겠습니다.

에이전트가 여정을 생성합니다.

$ curl -d '{"source":"data.orange.brings", "destination":"heave.wipes.clay"}' \
  -H 'Content-Type: application/json' \
  http://localhost:3000/consignment/

"3dd0f0e4-fc4a-46b4-8dae-a57d47df5207"

이제 화물에 대한 ID 3dd0f0e4-fc4a-46b4-8dae-a57d47df5207 이 있습니다. 그런 다음, 누군가가 탁송을 위해 그림과 피아노의 두 가지 항목을 수집합니다.

$ curl -d '{"location":"data.orange.brings", "timeStamp":"20200101T120000", "description":"picture"}' \
  -H 'Content-Type: application/json' \
  http://localhost:3000/consignment/3dd0f0e4-fc4a-46b4-8dae-a57d47df5207/item
"OK"

$ curl -d '{"location":"data.orange.brings", "timeStamp":"20200101T120001", "description":"piano"}' \
  -H 'Content-Type: application/json' \
  http://localhost:3000/consignment/3dd0f0e4-fc4a-46b4-8dae-a57d47df5207/item
"OK"

잠시 후 체크인이 있습니다.

$ curl -d '{"location":"united.alarm.raves", "timeStamp":"20200101T173301"}' \
-H 'Content-Type: application/json' \
http://localhost:3000/consignment/3dd0f0e4-fc4a-46b4-8dae-a57d47df5207/checkin
"OK"

그리고 나중에 다시:

$ curl -d '{"location":"wink.sour.chasing", "timeStamp":"20200101T191202"}' \
-H 'Content-Type: application/json' \
http://localhost:3000/consignment/3dd0f0e4-fc4a-46b4-8dae-a57d47df5207/checkin
"OK"

이 시점에서 고객은 위탁 상태를 요청합니다.

$ curl http://localhost:3000/consignment/3dd0f0e4-fc4a-46b4-8dae-a57d47df5207
{
  "id":"3dd0f0e4-fc4a-46b4-8dae-a57d47df5207",
  "source":"data.orange.brings",
  "destination":"heave.wipes.clay",
  "items":[
    {"location":"data.orange.brings","description":"picture","timeStamp":"20200101T120000"},
    {"location":"data.orange.brings","description":"piano","timeStamp":"20200101T120001"}
  ],
  "checkins":[
    {"timeStamp":"20200101T173301","location":"united.alarm.raves"},
    {"timeStamp":"20200101T191202","location":"wink.sour.chasing"}
  ],
  "delivered":false
}%

그들은 진행 상황을 보고 있지만 아직 전달되지 않았습니다.

Deflection.famed.apple도달했다는 메시지가 20:12에 전송되어야  하지만 지연되고 목적지의 21:46부터 메시지가 먼저 도착합니다.

$ curl -d '{"location":"heave.wipes.clay", "timeStamp":"20200101T214622"}' \
-H 'Content-Type: application/json' \
http://localhost:3000/consignment/3dd0f0e4-fc4a-46b4-8dae-a57d47df5207/checkin
"OK"

이 시점에서 고객은 위탁 상태를 요청합니다.

$ curl http://localhost:3000/consignment/3dd0f0e4-fc4a-46b4-8dae-a57d47df5207
{
  "id":"3dd0f0e4-fc4a-46b4-8dae-a57d47df5207",
...
    {"timeStamp":"20200101T191202","location":"wink.sour.chasing"},
    {"timeStamp":"20200101T214622","location":"heave.wipes.clay"}
  ],
  "delivered":true
}

이제 배송되었습니다. 따라서 지연된 메시지가 전달되면:

$ curl -d '{"location":"deflection.famed.apple", "timeStamp":"20200101T201254"}' \
-H 'Content-Type: application/json' \
http://localhost:3000/consignment/3dd0f0e4-fc4a-46b4-8dae-a57d47df5207/checkin
"OK"

$ curl http://localhost:3000/consignment/3dd0f0e4-fc4a-46b4-8dae-a57d47df5207
{
"id":"3dd0f0e4-fc4a-46b4-8dae-a57d47df5207",
...
{"timeStamp":"20200101T191202","location":"wink.sour.chasing"},
{"timeStamp":"20200101T201254","location":"deflection.famed.apple"},
{"timeStamp":"20200101T214622","location":"heave.wipes.clay"}
],
"delivered":true
}

체크인은 타임라인의 올바른 위치에 배치됩니다.

10. 결론

이 기사에서는 AWS Lambda와 같은 경량 컨테이너에서 Hibernate와 같은 중량 프레임워크를 사용할 때의 문제에 대해 논의했습니다.

Lambda 및 REST API를 구축하고 Docker 및 AWS SAM CLI를 사용하여 로컬 머신에서 테스트하는 방법을 배웠습니다. 그런 다음 데이터베이스와 함께 사용할 Hibernate의 엔터티 모델을 구성했습니다. 우리는 또한 테이블을 초기화하기 위해 Hibernate를 사용했습니다.

마지막으로 Hibernate SessionFactory 를 애플리케이션에 통합 하여 Lambda가 종료되기 전에 닫도록 했습니다.

평소와 같이 이 기사의 예제 코드는 GitHub 에서 찾을 수 있습니다 .

Persistence footer banner