├── .gitignore ├── 1_비동기_아키텍처.md ├── README.md ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── images ├── 1.png ├── 2.png ├── 3.png ├── sqs.png └── 메세징사례.png └── src ├── main ├── java │ └── com │ │ └── jojoldu │ │ └── aws │ │ └── springbootsqs │ │ ├── Application.java │ │ ├── controller │ │ ├── PointController.java │ │ └── SqsController.java │ │ ├── domain │ │ ├── Point.java │ │ └── PointRepository.java │ │ └── dto │ │ ├── PointDto.java │ │ └── SqsMessage.java └── resources │ ├── access-example-application.yml │ └── application.yml └── test ├── groovy └── com │ └── jojoldu │ └── aws │ └── springbootsqs │ └── controller │ └── PointControllerTest.groovy ├── java └── com │ └── jojoldu │ └── aws │ └── springbootsqs │ └── ApplicationTests.java └── resources └── application.yml /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | /build/ 3 | !gradle/wrapper/gradle-wrapper.jar 4 | /out 5 | /src/main/resources/access-application.yml 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | 15 | ### IntelliJ IDEA ### 16 | .idea 17 | *.iws 18 | *.iml 19 | *.ipr 20 | 21 | ### NetBeans ### 22 | nbproject/private/ 23 | build/ 24 | nbbuild/ 25 | dist/ 26 | nbdist/ 27 | .nb-gradle/ -------------------------------------------------------------------------------- /1_비동기_아키텍처.md: -------------------------------------------------------------------------------- 1 | # 비동기 아키텍처 2 | 3 | ![메세징사례](./images/메세징사례.png) 4 | 5 | ## SQS 6 | 7 | ![sqs](./images/sqs.png) 8 | 9 | * SQS는 [장애허용 시스템](https://ko.wikipedia.org/wiki/%EC%9E%A5%EC%95%A0_%ED%97%88%EC%9A%A9_%EC%8B%9C%EC%8A%A4%ED%85%9C) 구축의 기반이 된다. 10 | * 메세지 처리하던 sub이 죽어도 처리중이던 메세지는 2분 동안 큐에 남아있어, 그 사이에 다른 sub이 가져가서 처리합니다. 11 | * AWS 12 | * SQS는 **적어도 1번**은 메세지 전달을 보장한다. 13 | * 하나의 메세지가 2번 소비될수도 있다. 14 | * 즉, SQS를 쓴다면 메세지의 **멱등성**을 보장해야 합니다. 15 | * 멱등성이란, 동일한 메세지가 반복 처리되어도 **결과가 동일하게 유지**되는것을 얘기합니다. 16 | 17 | * SQS는 메세지의 순서를 보장하지 않는다. 18 | * 메세지가 **생성된 순서와 읽는 순서가 다를 수** 있습니다. 19 | * 20 | * -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spring Boot Cloud + AWS SQS 2 | 3 | Spring Boot Cloud 와 AWS SQS 연습용 프로젝트 4 | 5 | ## 0. Usage 6 | 7 | access-example-application.yml 파일명을 access-application.yml로 변경후 인증값 할당 8 | 9 | ## 1. Basic 10 | 11 | * Messages in Flight 12 | * 전달 대기 중이 아니며 이미 전달되었지만 더 이상 소비자가 처리하지 않은 메시지. 13 | * [why-do-sqs-messages-sometimes-remain-in-flight-on-queue](https://stackoverflow.com/questions/19792881/why-do-sqs-messages-sometimes-remain-in-flight-on-queue) 14 | 15 | ## 2. Dead Letter Queue 16 | 17 | Dead Letter Queue(줄여서 DLQ)는 **배달 못한 메세지 큐**로 불립니다. 18 | 19 | 처리하지 못한 SQS 메세지는 다른 작업자가 큐를 들여다볼때 다시 나타납니다. 20 | 이를 **Retry** (재시도) 라고 합니다. 21 | 하지만 어떤 이유(코드 버그, SQS 문제 등등)로 인해서 **모든 재시도가 실패**하면 메세지는 큐에 영원히 남게 되고 22 | 계속해서 재시도를 하게 됩니다. 23 | 이렇게 되면 **절대 처리되지 않는 메세지**가 되는 것이며, 자원의 낭비도 심합니다. 24 | 이를 방지하기 위해 DLQ (배달 못한 메세지큐)를 구성하게 됩니다. 25 | 26 | DLQ의 방식은 간단합니다. 27 | **메세지를 특정 횟수 이상 재시도하면 원래 큐에서 제거되고 DLQ로 전달**이 끝입니다. 28 | 29 | DLQ는 기존 큐와 한가지 차이가 있습니다. 30 | 31 | * 작업자들은 DLQ의 32 | 33 | 34 | ## Local Development -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext { 3 | springBootVersion = '2.0.0.RELEASE' 4 | } 5 | repositories { 6 | mavenCentral() 7 | } 8 | dependencies { 9 | classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") 10 | } 11 | } 12 | 13 | apply plugin: 'java' 14 | apply plugin: 'groovy' 15 | apply plugin: 'eclipse' 16 | apply plugin: 'org.springframework.boot' 17 | apply plugin: 'io.spring.dependency-management' 18 | 19 | group = 'com.jojoldu.aws' 20 | version = '0.0.1-SNAPSHOT' 21 | sourceCompatibility = 1.8 22 | 23 | repositories { 24 | mavenCentral() 25 | maven { url "https://repo.spring.io/milestone" } 26 | maven { url 'https://jitpack.io' } 27 | } 28 | 29 | task wrapper(type: Wrapper) { 30 | gradleVersion = '4.5.1' 31 | } 32 | 33 | ext { 34 | springCloudVersion = 'Finchley.M8' 35 | } 36 | 37 | dependencies { 38 | compile('org.springframework.boot:spring-boot-starter-actuator') 39 | compile('org.springframework.boot:spring-boot-starter-web') 40 | compile('org.springframework.boot:spring-boot-starter-data-jpa') 41 | compile('org.springframework.cloud:spring-cloud-starter-aws-messaging') 42 | 43 | compile('com.h2database:h2') 44 | 45 | // ElasticMQ (for Mocking SQS) 46 | compile group: 'org.elasticmq', name: 'elasticmq-core_2.12', version: '0.13.8' 47 | compile group: 'org.elasticmq', name: 'elasticmq-rest-sqs_2.12', version: '0.13.8' 48 | compile 'com.github.jojoldu.spring-boot-aws-mock:spring-boot-starter-mock-sqs:0.0.5' 49 | 50 | compileOnly('org.projectlombok:lombok') 51 | 52 | testCompile('org.springframework.boot:spring-boot-starter-test') 53 | testCompile('org.spockframework:spock-core:1.1-groovy-2.4') 54 | testCompile('org.spockframework:spock-spring:1.1-groovy-2.4') 55 | } 56 | 57 | dependencyManagement { 58 | imports { 59 | mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jojoldu/springboot-sqs-example/dbfce0518272b3a159ff73cdc761c487450fa74d/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Feb 06 12:27:20 CET 2018 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.5.1-bin.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn ( ) { 37 | echo "$*" 38 | } 39 | 40 | die ( ) { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save ( ) { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /images/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jojoldu/springboot-sqs-example/dbfce0518272b3a159ff73cdc761c487450fa74d/images/1.png -------------------------------------------------------------------------------- /images/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jojoldu/springboot-sqs-example/dbfce0518272b3a159ff73cdc761c487450fa74d/images/2.png -------------------------------------------------------------------------------- /images/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jojoldu/springboot-sqs-example/dbfce0518272b3a159ff73cdc761c487450fa74d/images/3.png -------------------------------------------------------------------------------- /images/sqs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jojoldu/springboot-sqs-example/dbfce0518272b3a159ff73cdc761c487450fa74d/images/sqs.png -------------------------------------------------------------------------------- /images/메세징사례.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jojoldu/springboot-sqs-example/dbfce0518272b3a159ff73cdc761c487450fa74d/images/메세징사례.png -------------------------------------------------------------------------------- /src/main/java/com/jojoldu/aws/springbootsqs/Application.java: -------------------------------------------------------------------------------- 1 | package com.jojoldu.aws.springbootsqs; 2 | 3 | import com.amazonaws.services.sqs.AmazonSQSAsync; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import org.springframework.boot.autoconfigure.SpringBootApplication; 6 | import org.springframework.boot.builder.SpringApplicationBuilder; 7 | import org.springframework.cloud.aws.messaging.core.QueueMessagingTemplate; 8 | import org.springframework.context.annotation.Bean; 9 | 10 | @SpringBootApplication 11 | public class Application { 12 | 13 | public static final String APPLICATION_LOCATIONS = "spring.config.location=" 14 | + "classpath:application.yml," 15 | + "classpath:access-application.yml,"; 16 | 17 | public static void main(String[] args) { 18 | new SpringApplicationBuilder(Application.class) 19 | .properties(APPLICATION_LOCATIONS) 20 | .run(args); 21 | } 22 | 23 | @Bean 24 | public QueueMessagingTemplate queueMessagingTemplate(AmazonSQSAsync amazonSqs) { 25 | return new QueueMessagingTemplate(amazonSqs); 26 | } 27 | 28 | @Bean 29 | public ObjectMapper objectMapper(){ 30 | return new ObjectMapper(); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/jojoldu/aws/springbootsqs/controller/PointController.java: -------------------------------------------------------------------------------- 1 | package com.jojoldu.aws.springbootsqs.controller; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.github.jojoldu.sqs.config.SqsQueueNames; 5 | import com.jojoldu.aws.springbootsqs.domain.PointRepository; 6 | import com.jojoldu.aws.springbootsqs.dto.PointDto; 7 | import lombok.AllArgsConstructor; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.springframework.cloud.aws.messaging.core.QueueMessagingTemplate; 10 | import org.springframework.cloud.aws.messaging.listener.annotation.SqsListener; 11 | import org.springframework.messaging.handler.annotation.Header; 12 | import org.springframework.web.bind.annotation.PostMapping; 13 | import org.springframework.web.bind.annotation.RequestBody; 14 | import org.springframework.web.bind.annotation.RestController; 15 | 16 | import java.io.IOException; 17 | 18 | /** 19 | * Created by jojoldu@gmail.com on 2018. 3. 15. 20 | * Blog : http://jojoldu.tistory.com 21 | * Github : https://github.com/jojoldu 22 | */ 23 | 24 | @Slf4j 25 | @AllArgsConstructor 26 | @RestController 27 | public class PointController { 28 | private QueueMessagingTemplate messagingTemplate; 29 | private SqsQueueNames sqsQueueNames; 30 | private PointRepository pointRepository; 31 | private ObjectMapper objectMapper; 32 | 33 | @PostMapping("/point") 34 | public String save(@RequestBody PointDto requestDto){ 35 | messagingTemplate.convertAndSend(sqsQueueNames.getQueue("point"), requestDto); 36 | return "success"; 37 | } 38 | 39 | @SqsListener(value = "${sqs.queueNames.point}") 40 | public void receive(String message, @Header("SenderId") String senderId) throws IOException { 41 | log.info("senderId: {}, message: {}", senderId, message); 42 | PointDto messageObject = objectMapper.readValue(message, PointDto.class); 43 | pointRepository.save(messageObject.toEntity()); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/com/jojoldu/aws/springbootsqs/controller/SqsController.java: -------------------------------------------------------------------------------- 1 | package com.jojoldu.aws.springbootsqs.controller; 2 | 3 | import com.github.jojoldu.sqs.config.SqsQueueNames; 4 | import com.jojoldu.aws.springbootsqs.dto.SqsMessage; 5 | import lombok.AllArgsConstructor; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.springframework.cloud.aws.messaging.core.QueueMessagingTemplate; 8 | import org.springframework.cloud.aws.messaging.listener.annotation.SqsListener; 9 | import org.springframework.messaging.handler.annotation.Header; 10 | import org.springframework.web.bind.annotation.GetMapping; 11 | import org.springframework.web.bind.annotation.RestController; 12 | 13 | import java.time.LocalDate; 14 | 15 | /** 16 | * Created by jojoldu@gmail.com on 2018. 3. 13. 17 | * Blog : http://jojoldu.tistory.com 18 | * Github : https://github.com/jojoldu 19 | */ 20 | 21 | @Slf4j 22 | @RestController 23 | @AllArgsConstructor 24 | public class SqsController { 25 | 26 | private QueueMessagingTemplate messagingTemplate; 27 | private SqsQueueNames sqsQueueNames; 28 | 29 | @GetMapping("/send") 30 | public String send(){ 31 | messagingTemplate.convertAndSend(sqsQueueNames.getQueue("basic"), new SqsMessage("test", LocalDate.now())); 32 | return "success"; 33 | } 34 | 35 | @SqsListener(value = "${sqs.queueNames.basic}") 36 | public void receive(String message, @Header("SenderId") String senderId) { 37 | log.info("senderId: {}, message: {}", senderId, message); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/jojoldu/aws/springbootsqs/domain/Point.java: -------------------------------------------------------------------------------- 1 | package com.jojoldu.aws.springbootsqs.domain; 2 | 3 | import lombok.Builder; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | 7 | import javax.persistence.Entity; 8 | import javax.persistence.GeneratedValue; 9 | import javax.persistence.Id; 10 | 11 | /** 12 | * Created by jojoldu@gmail.com on 2018. 3. 15. 13 | * Blog : http://jojoldu.tistory.com 14 | * Github : https://github.com/jojoldu 15 | */ 16 | 17 | @Getter 18 | @NoArgsConstructor 19 | @Entity 20 | public class Point { 21 | 22 | @Id 23 | @GeneratedValue 24 | private Long id; 25 | 26 | private Long userId; 27 | private Long point; 28 | private String description; 29 | 30 | @Builder 31 | public Point(Long userId, Long point, String description) { 32 | this.userId = userId; 33 | this.point = point; 34 | this.description = description; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/jojoldu/aws/springbootsqs/domain/PointRepository.java: -------------------------------------------------------------------------------- 1 | package com.jojoldu.aws.springbootsqs.domain; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | 5 | /** 6 | * Created by jojoldu@gmail.com on 2018. 3. 15. 7 | * Blog : http://jojoldu.tistory.com 8 | * Github : https://github.com/jojoldu 9 | */ 10 | 11 | public interface PointRepository extends JpaRepository{ 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/jojoldu/aws/springbootsqs/dto/PointDto.java: -------------------------------------------------------------------------------- 1 | package com.jojoldu.aws.springbootsqs.dto; 2 | 3 | import com.jojoldu.aws.springbootsqs.domain.Point; 4 | import lombok.Builder; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | import lombok.Setter; 8 | 9 | /** 10 | * Created by jojoldu@gmail.com on 2018. 3. 16. 11 | * Blog : http://jojoldu.tistory.com 12 | * Github : https://github.com/jojoldu 13 | */ 14 | 15 | @Getter 16 | @Setter 17 | @NoArgsConstructor 18 | public class PointDto { 19 | 20 | private Long userId; 21 | private Long savePoint; 22 | private String description; 23 | 24 | @Builder 25 | public PointDto(Long userId, Long savePoint, String description) { 26 | this.userId = userId; 27 | this.savePoint = savePoint; 28 | this.description = description; 29 | } 30 | 31 | public Point toEntity() { 32 | return Point.builder() 33 | .userId(userId) 34 | .point(savePoint) 35 | .description(description) 36 | .build(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/jojoldu/aws/springbootsqs/dto/SqsMessage.java: -------------------------------------------------------------------------------- 1 | package com.jojoldu.aws.springbootsqs.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | 6 | import java.time.LocalDate; 7 | 8 | /** 9 | * Created by jojoldu@gmail.com on 2018. 3. 12. 10 | * Blog : http://jojoldu.tistory.com 11 | * Github : https://github.com/jojoldu 12 | */ 13 | 14 | @Getter 15 | @AllArgsConstructor 16 | public class SqsMessage { 17 | private String name; 18 | private LocalDate executeDate; 19 | } 20 | -------------------------------------------------------------------------------- /src/main/resources/access-example-application.yml: -------------------------------------------------------------------------------- 1 | cloud: 2 | aws: 3 | credentials: 4 | accessKey: accessKey 5 | secretKey: secretKey 6 | region: 7 | static: 리전 코드 8 | 9 | sqs: 10 | queueNames: { # example 11 | "basic": "basic", 12 | "basicDlq": "basic-dlq", 13 | "point": "point", 14 | "pointDlq": "point-dlq" 15 | } 16 | -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | profiles: 3 | active: local 4 | jackson: 5 | serialization: 6 | WRITE_DATES_AS_TIMESTAMPS: false 7 | jpa: 8 | open-in-view: false -------------------------------------------------------------------------------- /src/test/groovy/com/jojoldu/aws/springbootsqs/controller/PointControllerTest.groovy: -------------------------------------------------------------------------------- 1 | package com.jojoldu.aws.springbootsqs.controller 2 | 3 | import com.jojoldu.aws.springbootsqs.domain.Point 4 | import com.jojoldu.aws.springbootsqs.domain.PointRepository 5 | import com.jojoldu.aws.springbootsqs.dto.PointDto 6 | import org.springframework.beans.factory.annotation.Autowired 7 | import org.springframework.boot.test.context.SpringBootTest 8 | import org.springframework.boot.test.web.client.TestRestTemplate 9 | import org.springframework.http.HttpStatus 10 | import org.springframework.http.ResponseEntity 11 | import spock.lang.Specification 12 | 13 | /** 14 | * Created by jojoldu@gmail.com on 2018. 3. 16. 15 | * Blog : http://jojoldu.tistory.com 16 | * Github : https://github.com/jojoldu 17 | */ 18 | 19 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 20 | class PointControllerTest extends Specification { 21 | 22 | @Autowired 23 | PointRepository pointRepository 24 | 25 | @Autowired 26 | TestRestTemplate restTemplate 27 | 28 | void cleanup() { 29 | pointRepository.deleteAllInBatch() 30 | } 31 | 32 | def "earn points through the queue."() { 33 | given: 34 | PointDto requestDto = PointDto.builder() 35 | .userId(10L) 36 | .savePoint(1000L) 37 | .description("buy laptop") 38 | .build() 39 | when: 40 | ResponseEntity response = restTemplate.postForEntity("/point", requestDto, String.class) 41 | Thread.sleep(500L) 42 | 43 | then: 44 | response.getStatusCode() == HttpStatus.OK 45 | List points = pointRepository.findAll() 46 | !points.isEmpty() 47 | points.get(0).getPoint() == 1000L 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/test/java/com/jojoldu/aws/springbootsqs/ApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.jojoldu.aws.springbootsqs; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | import org.springframework.test.context.junit4.SpringRunner; 7 | 8 | @RunWith(SpringRunner.class) 9 | @SpringBootTest 10 | public class ApplicationTests { 11 | 12 | @Test 13 | public void contextLoads() { 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/test/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | profiles: 3 | active: local 4 | cloud: 5 | aws: 6 | region: 7 | static: ap-northeast-2 8 | sqs: 9 | queueNames: { 10 | "basic": "basic", 11 | "basicDlq": "basic-dlq", 12 | "point": "point", 13 | "pointDlq": "point-dlq" 14 | } 15 | 16 | --- 17 | spring: 18 | profiles: local 19 | sqs: 20 | mock: 21 | enabled: true 22 | 23 | --- 24 | spring: 25 | profiles: beta 26 | --------------------------------------------------------------------------------