MapStruct를 사용하는 이유
Controller, Service, Repository 등 레이어 간 데이터를 주고받을 때나 비즈니스 로직에서, 하나의 객체를 타입이 다른 객체로 형(Type) 변환하거나 여러 객체를 다른 객체로 합치는 일은 매우 빈번한여 결국 생산성을 떨어뜨리고, 비즈니스 로직에 섞이게 되면 코드가 복잡해집니다.

 

이 API에는 두 Java Bean 간에 자동으로 매핑되는 기능이 포함되어 있습니다.

MapStruct를 사용하여 인터페이스만 생성하면 라이브러리는 컴파일 시간 동안 구체적인 구현을 자동으로 생성합니다.

엔티티와 클라이언트 측으로 나가는 DTO 간에 변환이 발생하는 경우가 많은데 이때 수동으로 빈 mapper를 만드는 것은 시간이 많이 걸리고, 이 부분을 MapStruct가 해결합니다.

 

MapStruct 라이브러리는 자동으로 빈 매퍼 클래스를 생성합니다. MapStruct라이브러리를 추가하고, @Mapper 어노테이션이 선언된 interface를 선언합니다. 구현 클래스를 생성하지 않았도 MapStruct가 가 자동생성해줍니다. mvn clean install을 실행하여 MapStruct 처리를 trigger할 수 있고, 그러면 /target/generated-sources/annotations/ 아래에 구현 클래스가 생성됩니다.

 

Lombok과 함께 사용하는 경우

Lombok과 MapStruct를 함께 사용하면 런타임시 에러가 발생하는 것을 확인할 수 있었습니다.

Project Lombok은 (무엇보다도) 컴파일된 빈 클래스의 AST(추상 구문 트리)에 getter와 setter를 추가하는 주석 처리기인데
AST 수정은 Java 주석 처리 API에서 예측할 수 없으므로 Lombok과 MapStruct 내에서 둘 다 함께 작동하도록 하려면 별도 작업을 해줘야합니다. 기본적으로 MapStruct는 Lombok이 enhanced-bean에 대한 매퍼 클래스를 생성하기 전에 Lombok이 모든 수정을 완료할 때까지 기다립니다. Lombok 1.18.16 이상을 사용하는 경우 Lombok과 MapStruct가 함께 작동하도록 하기 위해 lombok-mapstruct-binding을 추가해야 합니다. MapStruct 또는 Lombok의 이전 버전을 사용하는 경우 솔루션은 Lombok에서 수정할 JavaBeans와 MapStruct에서 처리할 매퍼 인터페이스를 프로젝트의 두 개의 개별 모듈에 넣는 것입니다. 그런 다음 Lombok은 첫 번째 모듈의 컴파일에서 실행되어 두 번째 모듈을 컴파일하는 동안 MapStruct가 실행될 때 빈 클래스가 완료되도록 합니다.

 

작업내용 
mapstruct 라이브러리와 함께 lombok-mapstruct-binding을 추가하고,

아래와 같이 플러그인을 추가해주니 정상 작동 되는 것을 확인할 수 있었습니다.

    <properties>
        <org.mapstruct.version>1.4.2.Final</org.mapstruct.version>
        <org.projectlombok>1.18.20</org.projectlombok>
        <lombok-mapstruct-binding>0.2.0</lombok-mapstruct-binding>
    </properties>
        <dependency>
            <groupId>org.mapstruct</groupId>
            <artifactId>mapstruct</artifactId>
            <version>${org.mapstruct.version}</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok-mapstruct-binding</artifactId>
            <version>${lombok-mapstruct-binding}</version>
        </dependency>
 <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>${java.version}</source> <!-- depending on your project -->
                    <target>${java.version}</target> <!-- depending on your project -->
                    <annotationProcessorPaths>
                        <path>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                            <version>${org.projectlombok}</version>
                        </path>
                        <!-- This is needed when using Lombok 1.18.16 and above -->
                        <path>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok-mapstruct-binding</artifactId>
                            <version>${lombok-mapstruct-binding}</version>
                        </path>
                        <path>
                            <groupId>org.mapstruct</groupId>
                            <artifactId>mapstruct-processor</artifactId>
                            <version>${org.mapstruct.version}</version>
                        </path>
                        <!-- other annotation processors -->
                    </annotationProcessorPaths>
                </configuration>
            </plugin>

 

 

@Mapper(componentModel = "spring")
public interface TestMapper {
    Test toTestEntity(TestForm testForm);
}

 

 

참고 

https://mapstruct.org/faq/

 

Frequently Asked Questions (FAQ) – MapStruct

The strategies were developed over time and hence the naming / behavior deserves attention in future versions of MapStruct to getter better allignment. This would introduce backward incompatibillties, so we cannot not do this in the 1.x versions of MapStru

mapstruct.org

https://medium.com/uphill-engineering-design/deep-dive-into-mapstruct-spring-7ddd8dac3d6d

 

Deep dive into Mapstruct @ Spring

How to take advantage of Mapstruct to greatly reduce the amount of boilerplate code that would regularly be written by hand.

medium.com

 

 

현재 개발중인 애플리케이션은 Multi Datasource를 사용중이다. 

Header로 전달받은 값에 따라 연결할 데이터베이스를 결정하고, 경우에 따라선 한 API에서 2개 이상의 데이터베이스에 접근해야 한다. JPA를 사용중이고, 몇 십개의 엔티티들은 대부분 다 관계성을 띄고 있어 FetchType을 기본적으로 Lazy Association으로 가져간다.

 

OSIV (Open Session In View)

OSIV는 개발중에 처음 알게된 용어이다. 엔티티들의 수많은 관계속에서 딱 원하는 Response 데이터 형태를 만들어내기란 여간 힘든게 아니었다. 삽질의 시간속에서 허우적 댈 때쯤 Service Layer에서 CRUD중 C/U/D의 경우에는 @Transactional로 선언된 메소드들이 있었다. 그런데 @Transactional 메소들를 벗어난 이후의 동작이 예상대로 돌아가지 않는 것이다. 특히 데이터베이스 각각에 Update를 해야되는 경우였는데 (그 외에도 몇개의 문제상황이 있었다) A데이터베이스에서 쿼리를 하고, B데이터베이스에다 작업을 하기 위해 MultiDataSource를 사용하기 위해 이용되는 DatabaseContextHolder에 B데이터베이스를 재할당하였다. 당시 나는 당연히 B로 연결이 될 것이라 생각했는데 이게 웬걸 B 데이터베이스로 연결되지 않고 처음에 맺었던 A 데이터베이스에다가 모든 작업을 하고 있던게 아닌가. A에 하는 작업과 B에 하는 작업을 메소드 분리하고, 각각의 메소드에 @Transactional을 걸어줘봤는데 실패, propagation을 requires_new 로도 해보고 여러가지를 시도해봤는데도 실패. 그렇게 삽질을 하고 나서야 알게된게 OSIV였다.

 

일단 @Transactionl에 대해 알아보고, OSIV를 알아보도록 하자

 

  • @Transactional
    • 스프링에서는 간단하게 어노테이션 방식으로 @Transactional을 메소드, 클래스, 인터페이스 위에 추가하여 사용하는 방식이 일반적이다. 이 방식을 선언적 트랜잭션이라 부르며, 적용된 범위에서는 트랜잭션 기능이 포함된 프록시 객체가 생성되어 자동으로 commit 혹은 rollback을 진행해준다.
    • Spring이 제공하는 어노테이션으로 @Transactional을 메서드 또는 클래스에 명시하게 되면 특정 메서드 또는 클래스가 제공하는 모든 메서드에 대해 내부적으로 AOP를 통해 트랜잭션 처리코드가 전 후 로 수행된다.
      • AOP는 일반적으로 두가지 방식이 있는데 그 중에 하나인 Dynamic Proxy로 예를 들어보면
        • @Transactional가 걸려있는 메소드 a() 가 있으면 Proxy는 런타임 시점에 해당 a()메소드를 호출하는 코드를 포함, 트랜잭션 처리에 필요한 코드를 전후로 감싸 트랜잭션 처리를 다이나믹 프록시 객체에 대신 위임한다. 
    • 하지만, @Transactional이 걸려있는 메소드가 종료됟더라도 OSIV에 의해 트랜잭션이 끝나도 영속성 컨텍스트는 유지된다.
  • OSIV와 영속성컨텍스트
    • OSIV(Open Session In View)는 영속성 컨텍스트를 뷰까지 열어두는 기능이다. 영속성 컨텍스트가 유지되면 엔티티도 영속 상태로 유지된다. 뷰까지 영속성 컨텍스트가 살아있다면 뷰에서도 지연 로딩을 사용할 수가 있다.
    • OSIV 전략은 트랜잭션 시작처럼 최초 데이터베이스 커넥션 시작 시점부터 API 응답이 끝날 때 까지 영속성 컨텍스트와 데이터베이스 커넥션을 유지한다. 그래서 View Template이나 API 컨트롤러에서 지연 로딩이 가능하다.
    • 스프링에서는 OSIV가 기본값(true)으로 설정되어있다. 기본적으로는 트랜잭션을 시작할 때 영속성 컨텍스트가 DB 커넥션을 가져온다. 커넥션을 획득한 후에는 API 응답이 끝날때까지 유지한다. 트랜잭션이 끝나도 지연 로딩으로 프록시 객체를 초기화할 상황이 생기기 때문이다. 따라서 영속성 컨텍스트가 DB 커넥션을 계속 물고 있어야 한다.
    • JPA의 영속성 컨텍스트는 결국 DB를 1:1로 쓰면서 동작한다.
  • OSIV의 문제점?
    • 오랫동안 DB 커넥션을 물고 있기 때문에 실시간 트래픽이 중요한 애플리케이션에서는 커넥션이 모자랄 수 있다. 이는 결국 장애로 이어진다.
    • Multi Datasource를 사용하는 경우에는 처음 맺은 DB 커넥션을 계속 가져가기 때문에 DB Connection을 바꾸는 데에 있어 문제가 생길 수 있다.
  • OSIV = false
    • OSIV를 끄면 트랜잭션을 종료할 때 영속성 컨텍스트를 닫고, 데이터베이스 커넥션도 반환한다. 따라서 커넥션 리소스를 낭비하지 않는다. 하지만 모든 지연로딩을 트랜잭션 안에서 처리해야 한다. 따라서 지금까지 작성한 많은 지연 로딩 코드를 트랜잭션 안으로 넣어야 하는 단점이 있다. 그리고 view template에서 지연로딩이 동작하지 않는다.
    • CRUD 중에서 CUD의 경우에는 기본적으로 서비스레이어에 @Transactional을 설정하였고, Controller에는 비즈니스 로직이 없기 때문에 문제가 없을 수 있지만 R의 경우는 서비스레이어에서 Lazy Loading을 이용해 엔티티를 조작하려고 할 때 Exception이 발생하기 때문에 이 경우에는 @Transational과 read-only값을 true로 설정함으로써 해결이 가능하다.
  • OSIV=false & MultiDatasource
    • OSIV를 true로 하게 되면 최초 연결된 세션을 재사용하여 DetermineRoutingDatasource 클래스의 determineCurrentLookupKey 메소드가 동작하지 않게 되고, 결과적으로 DatabaseContextHolder로 재설정한 데이터베이스로 연결이 되지 않는다. 따라서 MultiDatasource를 사용하는 애플리케이션의 경우엔 OSIV를 false로 가져가는 것이 맞다. 

 

Datasoure를 변경하려고 하면 최초 연결된 세션을 재사용하기 때문에  DetermineRoutingDatasource 클래스의determineCurrentLookupKey 메소드가 동작하지 않게 되고, 결과적으로 DatabaseContextHolder로 재설정한 데이터베이스로 연결이 되지 않았다. 이 문제는 OSIV 를 false로 둠으로써 해결할 수 있었다. 

OSIV는 Spring에서 기본적으로 true로 가져가고 있지만, 애플리케이션의 복잡도와 성능을 고려해보고 이 값을 false로 변경해보는 것을 고려해보아야 한다.

 

 

참고

https://dodeon.gitbook.io/study/kimyounghan-spring-boot-and-jpa-optimization/04-osiv

https://ykh6242.tistory.com/102

https://incheol-jung.gitbook.io/docs/q-and-a/spring/osiv

 

  • Comparable과 Comparator 인터페이스의 차이는?
    • Comparable 인터페이스는 자연스러운 순서로 정렬할 때 사용하고. Comparator는 원하는 대로 정렬 순서를 지정한다.
    • Array와 Collection 클래스는 몇 가지 오버로딩된 정렬 메서드가 있다. 
      • 배열을 받는 메서드
      • 배열과 Comparator 객체를 받는 메서드 
    • 지정한 순서대로 정렬하고 싶으면 sort 메서드에서 사용할 Comparator 인터페이스를 구현한 후 이를 제공해야 한다.
      • Comparator를 인터페이스를 구현한 ReverseNumericalOrder 클래스가 있다고 치자.
        • 이 클래스는 compare 메소드를 오버라이드 한 것
      • 이것을 Collections.sort(number, new ReverseNumbericalOrder()); 이렇게 전달하면 원하는 순서대로 정렬이 되는 것을 볼 수 있다. 

 

 

  • 버블 정렬 알고리즘
    • 순환할 때 마다 하나의 원소만 변경한다. 
    • 최선의 경우는 이미 정렬되어 잇을 때
    • 최악의 경우는 역순으로 정렬되어 있을 떄 
    • O(n^2)
  • 삽입 정렬 알고리즘 
    • 정렬된 원소들을 새로운 리스트에 담아서 반환 (버블 정렬과 다른 점)
    • LinkedList를 사용하며 반환할 리스트가 구성되면 즉시 반환할 수 있다. 
    • 최악의 경우는 이미 정렬되어 있을 때고, 이때는 O(n^2)
    • 최선의 경우는 역순으로 정렬되어 있을 때고, 이때는 O(n)
  • 버블vs삽입
    • 삽입 정렬 알고리즘은 새로운 리스트를 반환해야 하므로 정렬하려는 리스트의 두배 정도 되는 저장 공간이 필요하다. 
    • 버블 정렬은 스왑할 때 사용하는 임시 변수용으로 사용할 여분의 메모리 슬롯 하나만 더 있으면 된다.
    • 따라서, (나의 의견)
      • 어느 정도 정렬된 아주 큰 배열은 버블정렬
      • 전혀 정렬되지 않았고 그다지 크지 않은 배열의 경우에는 삽입정렬
  • 퀵 정렬 알고리즘
    • 재귀적인 알고리즘으로 버블과 삽입 보다 훨씬 더 효율적이고 성능이 ㅇ좋음
    • 원소들을 두 개의 리스트로 분리하는 시간 복잡도는 O(n)이고, 각각의 재귀 호출은 각 리스트 숫자의 절반만큼의 횟수만 발생
    • 복잡도는 O(n logn)
    • 최악의 경우는 pivot (get(0)으로 무작위로 선택한 기준)을 선태갛는 기준에 있는데 리스트가 역순으로 정렬되어 있을 때
  • 병합 정렬 알고리즘
    • 분할정복 알고리즘
    • 리스트를 두 개로 나누고 각 하위 리스트를 정렬한 후 각각 하나로 합친다
    • 병합 정렬의 성능은 O(n logn)이고 각각의 병합시간은 O(n)이며 각 재귀 호출은 주어진 리스트 숫자의 절반 만큼만 발생

$ sudo wget -O /etc/yum.repos.d/jenkins.repo https://pkg.jenkins.io/redhat-stable/jenkins.repo
$ sudo rpm --import https://pkg.jenkins.io/redhat-stable/jenkins.io.key

이전에 Jenkins에서 키를 가져온 경우 이미 키가 있기 때문에 rpm --import가 실패합니다. 이땐 무시하고 진행하시기 바랍니다.

$ yum install epel-release # repository that provides 'daemonize'

$ yum install epel-release에서 에러가 나서 아래의 명령어로 대체

$ sudo amazon-linux-extras install epel

그리고, Jenkins 설치를 위해 자바를 설치해줍니다.

$ sudo yum install java-11-openjdk-devel
$ sudo yum install jenkins

 

 

 

jenkins를 시작하고, 잘 돌아가는지 확인합니다.

$ sudo systemctl start jenkins
$ sudo systemctl status jenkins.service

 

 

기본 포트가 8080이기 때문에 원하는 포트로 변경을 합니다.

$ sudo vim /etc/sysconfig/jenkins

JENKINS_PORT 부분을 맘에 드는 포트로 변경하고, 변경된 부분을 적용시키기 위해 서비스 재시작하면 끝!

 

$ sudo systemctl restart jenkins

 


출처: https://get.jenkins.io/redhat-stable/
출처: https://oingdaddy.tistory.com/354 

Spring Boot Application을 AWS Lambda에 올리려면 문제가 있음.

첫 번째 문제는 Spring Boot 애플리케이션을 이벤트 기반으로 만들어야 한다는 것.

AWS Lambda 함수가 호출되면 요청 객체를 핸들러로 보내는데

Spring Boot는 이 요청을 처리할 수 없으므로 앱을 변경해야 한다는 문제가..

 

이 문제를 해결하기 위해 AWS는 개발자가 사용자 지정 핸들러를 생성할 수 있는 라이브러리를 게시했다.

 


 

aws-serverless-java-container 라이브러리를 사용하여

AWS Lambda에서 Spring Boot 2 애플리케이션을 실행할 수 있습니다.

Lambda 핸들러 내의 라이브러리를 사용하여 Spring Boot 애플리케이션 및 프록시 이벤트를 로드합니다.

 

리포지토리에는 샘플 Spring Boot 애플리케이션이 포함되어 있습니다.

Serverless Java Container는 Spring Boot 버전 2.2.x, 2.3.x, 2.4.x 및 2.5.x에 대해 테스트 되었고,

Serverless Java Container 버전 1.4부터 WebFlux 애플리케이션도 지원합니다.

 

Maven archetype

 

아래 명령어를 통해 project를 생성해줍니다.

mvn archetype:generate -DgroupId=my.service -DartifactId=my-service -Dversion=1.0-SNAPSHOT \
       -DarchetypeGroupId=com.amazonaws.serverless.archetypes \
       -DarchetypeArtifactId=aws-serverless-springboot2-archetype \
       -DarchetypeVersion=1.6

 

Manual setup / Converting existing projects

 

<dependency>
    <groupId>com.amazonaws.serverless</groupId>
    <artifactId>aws-serverless-java-container-springboot2</artifactId>
    <version>1.6</version>
</dependency>

 

그러면 aws-serverless-java-container-core 및 aws-lambda-java-core 라이브러리도 자동으로 가져옵니다.
Spring을 사용한 종속성 주입은 함수의 cold start time에 상당한 영향을 미칠 수 있습니다.
이 문제를 해결하기 위해 spring-context-indexer 포함하여 컴파일 타임에 후보 구성 요소 목록을 생성할 수 있습니다.

애플리케이션 패키지에서 Lambda의 RequestStreamHandler 인터페이스를 구현하는 새 클래스를 선언합니다. 프록시 통합으로 API Gateway를 구성한 경우 기본 제공 POJO AwsProxyRequest 및 AwsProxyResponse를 사용할 수 있습니다.

 

다음 단계는 컨테이너 핸들러 객체를 선언하는 것입니다.

라이브러리는 AWS 프록시 이벤트에 대해 SpringBootLambdaContainerHandler 객체를 구성하는 유틸리티 정적 메서드를 노출합니다. 메서드는 Spring Boot의 @SpringBootApplication으로 주석이 달린 클래스를 수신합니다. 개체는 클래스 속성으로 선언되어야 하며 정적이어야 합니다. 이렇게 하면 Lambda가 후속 요청에 인스턴스를 재사용합니다.

 

클래스의 handleRequest 메소드는 이전 단계에서 선언한 핸들러 객체를 사용하여 Spring 애플리케이션에 요청을 보낼 수 있습니다.

 

public class StreamLambdaHandler implements RequestStreamHandler {
    private static SpringBootLambdaContainerHandler<AwsProxyRequest, AwsProxyResponse> handler;
    static {
        try {
            handler = SpringBootLambdaContainerHandler.getAwsProxyHandler(Application.class);
            // If you are using HTTP APIs with the version 2.0 of the proxy model, use the getHttpApiV2ProxyHandler
            // method: handler = SpringBootLambdaContainerHandler.getHttpApiV2ProxyHandler(Application.class);
        } catch (ContainerInitializationException e) {
            // if we fail here. We re-throw the exception to force another cold start
            e.printStackTrace();
            throw new RuntimeException("Could not initialize Spring Boot application", e);
        }
    }

    @Override
    public void handleRequest(InputStream inputStream, OutputStream outputStream, Context context)
            throws IOException {
        handler.proxyStream(inputStream, outputStream, context);
    }
}

위 코드는 mvn으로 생성한 프로젝트에 가보면 자동으로 생성되있는 것을 확인할 수 있다.

 

기본적으로 Spring Boot 프로젝트에는 spring-boot-maven-plugin 및 임베디드 Tomcat 애플리케이션 서버가 포함됩니다. AWS Lambda용 Spring Boot 애플리케이션을 패키징하려면 Spring Boot maven 플러그인이 필요하지 않으며 내장된 Tomcat을 제외하도록 shade 플러그인을 구성할 수 있습니다. serverless-java-container 라이브러리가 대신합니다.

 
<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-shade-plugin</artifactId>
            <version>2.3</version>
            <configuration>
                <createDependencyReducedPom>false</createDependencyReducedPom>
            </configuration>
            <executions>
                <execution>
                    <phase>package</phase>
                    <goals>
                        <goal>shade</goal>
                    </goals>
                    <configuration>
                        <artifactSet>
                            <excludes>
                                <exclude>org.apache.tomcat.embed:*</exclude>
                            </excludes>
                        </artifactSet>
                    </configuration>
                </execution>
            </executions>
        </plugin>
    </plugins>

 

Asynchronous initialization

 

Spring Boot 2 애플리케이션은 시작 속도가 느릴 수 있습니다.

위의 예에서 정적 블록 또는 RequestStreamHandler 클래스의 생성자를 사용하여 초기화 단계 동안 AWS Lambda에서 사용 가능한 더 높은 CPU를 활용하도록 프레임워크를 초기화하는 것이 좋습니다. 그러나 AWS Lambda는 초기화 단계를 10초로 제한합니다. 애플리케이션을 시작하는 데 10초 이상 걸리는 경우 AWS Lambda는 샌드박스가 중단된 것으로 가정하고 새 샌드박스 시작을 시도합니다. 초기화에 사용 가능한 10초를 최대한 활용하고 적시에 Lambda 런타임으로 제어를 반환하기 위해 비동기 초기화를 지원합니다.

public class StreamLambdaHandler implements RequestStreamHandler {
    private SpringBootLambdaContainerHandler<AwsProxyRequest, AwsProxyResponse> handler;
    
    public StreamLambdaHandler() throws ContainerInitializationException {
        long startTime = Instant.now().toEpochMilli();
        handler = new SpringBootProxyHandlerBuilder()
                .defaultProxy()
                .asyncInit(startTime)
                .springBootApplication(SlowApplication.class)
                .buildAndInitialize();
    }

    @Override
    public void handleRequest(InputStream inputStream, OutputStream outputStream, Context context)
            throws IOException {
        handler.proxyStream(inputStream, outputStream, context);
    }
}

비동기식 이니셜라이저는 타임스탬프를 사용하여 AWS Lambda가 핸들러를 생성한 후 이미 경과한 시간을 추정합니다. 이러한 이유로 생성자에서 가능한 한 빨리 이 측정을 수행하는 것이 중요합니다. 

 

비동기식 이니셜라이저는 별도의 스레드에서 Spring Boot 애플리케이션 컨텍스트를 시작하고 10초 제한 시간이 만료되기 직전에 AWS Lambda에 제어를 반환합니다. 분명히, 애플리케이션을 시작하는 데 10초 미만이 소요되는 경우 AWS Lambda에 제어 기능을 즉시 반환합니다. Lambda가 첫 번째 이벤트를 보낼 때 Serverless Java Container는 처리를 위해 이벤트를 보내기 전에 비동기 이니셜라이저가 작업을 완료할 때까지 기다립니다. 기본적으로 Serverless Java Container는 추가로 10초 동안 대기합니다. ContainerConfig 객체의 setInitializationTimeout 메소드를 사용하여 이를 변경할 수 있습니다.

 

-

내가 개발할 프로젝트의 경우 초기화가 오래 걸리지 않을 것으로 예상, Async 로 진행하는 코드는 생략하기로..

 

 

 

출처

https://github.com/awslabs/aws-serverless-java-container/wiki/Quick-start---Spring-Boot2

 

 

+ Recent posts