SQS에 대해서 공부한 내용을 블로그에 정리하는 목적으로 해당 게시글을 작성하게 되었다.
SQS를 적절하게 사용하면 어려운 문제에 대해서 아주 효과적으로 처리할 수 있는데 예를 들어서 선착순 티켓팅 시스템이나 알림 발송 시스템 등 순간적으로 많은 트래픽이 발생하는 작업에 대해서 유연하게 대처할 수 있다.

이번 게시글에서 SQS에 대한 간단한 설명과 설정 그리고 Spring boot와 연동하는 방법에 대해서 설명하겠다.

목차

1. SQS 란 (Amazon Simple Queue Service) ?
2. SQS를 사용하는 이유
3. SQS 유형
4. SQS 생성 및 설정 방법
5. SQS와 Spring Boot 연동 

6. 결론


1. SQS 란 (Amazon Simple Queue Service) ?

AWS SQS(Amazon Simple Queue Service)는 메시지를 송수신할 수 있는 완전 관리형 메시지 큐 서비스이다.
이를 통해 분산 시스템 간의 비동기 통신을 가능하게 하며, 메시지의 유실 없이 안정적인 데이터 전달을 보장한다.

2. SQS를 사용하는 이유

1) 비동기 처리 및 작업 분리

SQS는 비동기 처리를 가능하게 해준다. 시스템에서 시간이 오래 걸리는 작업이나 비동기적으로 처리해야 하는 작업이 있을 때, SQS를 사용하면 작업을 큐에 넣고 리시버에서 작업 수행하도록 설계할 수 있다. 
이런식으로 작업을 큐로 전송하는 서버와 작업을 처리하는 서버로 분리하면 처리량과 안정성을 높일 수 있다.

2) 시스템 간의 결합도 감소

마이크로서비스 아키텍처에서는 서비스 간의 결합도를 낮추는 것이 매우 중요하다. SQS는 서비스 간의 직접적인 통신을 피하고, 메시지를 큐를 통해 전달함으로써 서비스 간의 결합도를 낮출 수 있다. 이를 통해 각 서비스는 독립적으로 개발, 배포 및 확장할 수 있다.

3) 확장성과 유연성

SQS는 AWS의 관리형 서비스로, 메시지 큐의 크기와 처리량을 자동으로 확장할 수 있다. 대규모 메시지를 처리할 수 있으며 트래픽이 급증해도 시스템이 이를 처리할 수 있도록 자동으로 확장된다.

이는 수동으로 인프라를 관리할 필요 없고, 사용한 만큼만 비용을 소모할 수 있다.

4) 내결함성과 안정성

SQS는 메시지의 안정적이고 지속적인 전달을 보장한다. 메시지가 처리되지 못했을 경우 Dead-Letter Queue를 사용해 해당 메시지를 별도로 관리할 수 있다. 이는 시스템 오류나 장애가 발생하더라도 메시지를 안전하게 처리할 수 있도록 도와준다.

Dead-Letter Queue를 사용하면 별도의 큐를 관리해야 하므로 추가적인 비용이 발생할 수 있다. 하지만 중요한 작업이고 실패할 경우 안정적으로 재시도 작업을 원한다면 SQS는 아주 쉽게 해당 기능을 제공한다.

5) 보안

SQS는 AWS IAM을 사용해 액세스 제어를 제공하며, 전송 중인 데이터와 대기열에 저장된 데이터를 AWS KMS(Key Management Service)를 통해 암호화할 수 있다. 이를 통해 민감한 데이터를 안전하게 처리할 수 있다.

6) 저비용

SQS는 메시지의 수에 따라 비용이 청구되므로, 사용한 만큼만 비용을 지불하게 된다. 이는 예산을 효율적으로 관리할 수 있게 해주며, 특히 트래픽이 변동적인 애플리케이션에 유리하다.

3. SQS 유형

SQS는 크게 표준 대기열, FIFO 대기열로 나뉘어진다. 목적에 따라서 적절히 선택해야한다.

1) 표준 대기열

  • 무제한 처리량: 표준 대기열은 API 작업당 거의 무제한의 초당 트랜잭션(TPS)을 지원한다.
  • 최소한 한 번은 전달: 메시지가 최소한 한 번 전달되고, 가끔 2개 이상의 메시지 복사본이 전달될 수 있다.
  • 메시지 전송 순서 보장은 안됌: 가끔 메시지가 전송된 순서와 다르게 전달될 수 있다.

2) FIFO 대기열

  • 높은 처리량: 기본적으로 FIFO 대기열은 초당 최대 300개의 메시지(초당 300개의 전송, 수신 또는 삭제 작업)를 지원합한다. 작업당 최대 10개 메시지를 일괄 처리할 경우, FIFO 대기열은 초당 3000개의 메시지까지 지원할 수 있다. 
  • 정확히 한 번 처리: 메시지가 한 번 전달되고 소비자가 이를 처리 및 삭제할 때까지 유지된다. 중복 메시지는 대기열에 올라가지 않는다..
  • 선입선출 전달: 메시지가 전송되고 수신되는 순서가 엄격하게 지켜진다.

4. SQS 생성 및 설정 방법

1) SQS IAM 사용자 생성

사용자 세부 정보 지정

권한 설정 및 사용자 생성

SQS 전용 IAM 사용자를 생성했다면 액세스 키를 만들고 ARN을 복사해 두자.

2) Amazon SQS 대기열 생성

대기열 생성

  • 표준 대기열을 선택하고 큐 이름을 입력한다.

대기열 구성

  • 표시 제한 시간 (Visibility Timeout) 
    • 이 설정은 메시지가 중복 처리되는 것을 방지하기 위해 사용된다. 메시지가 소비자에게 전달되면 해당 메시지는 표시 제한 시간 동안 큐에서 "잠금(lock)" 상태가 된다. 이 기간 내에 소비자는 메시지를 처리하고 SQS에 처리가 완료되었음을 알려야 한다. 만약 표시 제한 시간이 지나도 처리가 완료되지 않으면, 해당 메시지의 잠금이 풀리며 다른 소비자가 메시지를 소비할 수 있다.
  • 전송 지연 (Delay Seconds)
    • 전송 지연은 메시지가 큐에 추가되자마자 소비자에게 전달되지 않고, 지정된 시간 동안 대기 상태가 되는 설정이다. 이 설정은 메시지의 즉각적인 처리가 필요하지 않거나, 특정 작업이 지연되어 실행되기를 원할 때 유용하다.
  • 메시지 수신 대기 시간 (Receive Message Wait Time)
    • 메시지 수신 대기 시간은 SQS가 폴링 방식으로 메시지를 검색할 때, 메시지가 큐에 없을 경우 대기하는 시간을 지정하는 설정이다. 이 설정을 통해 짧은 폴링 긴 폴링을 제어할 수 있다.  짧은 폴링은 메시지가 없을 때 즉시 응답하지만, 긴 폴링은 설정된 시간 동안 큐에 메시지가 들어올 때까지 대기한다. 긴 폴링을 사용하면 네트워크 트래픽과 비용을 줄일 수 있다.
    • 대기하는 주체는 폴링 요청을 하는 클라이언트(리시버)이며 메시지 수신 대기시간을 길게 설정하면 폴링 할 때 메시지가 없을 경우 바로 연결을 끊지 않고 설정한 시간동안 대기한다.
  • 메시지 보존 기간 (Message Retention Period)
    • 메시지 보존 기간은 큐에 들어온 메시지가 큐에서 삭제되지 않고 보관되는 최대 시간을 설정한다. 메시지가 이 기간 동안 처리되지 않으면, 자동으로 삭제된다.
    • 메시지 보존 기간을 길게 설정하면 메시지를 오랜 기간 동안 보관할 수 있지만, 이 경우 큐에 메시지가 쌓이면서 스토리지 비용이 증가할 수 있다. 반면, 보존 기간을 짧게 설정하면 메시지가 빠르게 삭제되므로 큐의 크기를 줄일 수 있지만, 메시지 손실 위험이 증가할 수 있다.

암호화

  • 메시지에 대한 암호화 기능을 제공한다.

SQS 큐 전송자와 큐 수신자에 대한 액세스 정책

  • 이전에 생성해둔 SQS IAM에 대한 ARN 링크를 복사해서 큐 전송자와 큐 수신자에 입력한다.
  • 큐 전송자와 수신자를 별도의 사용자로 나누고 싶다면 IAM 사용자를 추가로 생성해주면 된다.
  • IAM 사용자 외에도 역할에 SQS 관련 권한을 넣어주고 해당 역할에 대한 ARN 정보를 입력해도 된다. 

Dead-Letter Queue 정책 설정

  • 수신자에서 작업이 실패될 경우 Dead-Letter Queue로 실패 작업에 대한 메시지를 전송할 수 있다.
  •  해당 설정을 해주려면 위에서 대기열을 생성했던 것과 같이  Dead-Letter Queue 전용으로 생성해주어야 한다.
  • 그 다음 해당 대기열의 ARN을 복사해서 대기열 ARN에 입력해준다.

모든 설정이 완료되었으면 대기열을 생성해주자.

5. SQS와 Spring Boot 연동 

Spring Boot 버전에 따라서 연동하는 방법이 다를 수 있다. 이번 게시글에서는 jdk 17과 spring 3.x 으로 연동 테스트를 진행하겠다. Spring project를 생성하고 아래 코드를 참고해서 테스트해보자

build.gradle

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.3.2'
    id 'io.spring.dependency-management' version '1.1.6'
}

...

dependencies {
	...
    implementation platform("io.awspring.cloud:spring-cloud-aws-dependencies:3.0.1")
    implementation 'io.awspring.cloud:spring-cloud-aws-starter-sqs'
    ...
}

application.yml

cloud:
  aws:
    credentials:
      access-key: access-key
      secret-key: secret-key
    region:
      static: ap-northeast-2
    sqs:
      queue:
        name: my-test-queue
  • SQS IAM의 access-key와 secret-key를 입력하고 queue의 region과 name을 입력하자.

AwsSqsConfiguration

  • AWS SQS 관련 설정 클래스이다. 편의상 메시지 전송 설정과 메시지 수신 설정을 하나의 클래스에 정의하고 동일한 비동기 클라이언트를 사용하도록 설정한다 
import io.awspring.cloud.sqs.config.SqsMessageListenerContainerFactory;
import io.awspring.cloud.sqs.operations.SqsTemplate;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.sqs.SqsAsyncClient;

import java.time.Duration;

@Slf4j
@Configuration
public class AwsSqsConfiguration {

    private final String accessKey;

    private final String secretKey;

    private final String region;

    public AwsSqsConfiguration(
        @Value("${cloud.aws.credentials.access-key}") String accessKey,
        @Value("${cloud.aws.credentials.secret-key}") String secretKey,
        @Value("${cloud.aws.region.static}") String region
    ) {
        this.accessKey = accessKey;
        this.secretKey = secretKey;
        this.region = region;
    }

    /**
     * SQS 비동기 클라이언트
     *
     * @return
     */
    @Bean
    public SqsAsyncClient sqsAsyncClient() {
        return SqsAsyncClient.builder()
            .credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create(accessKey, secretKey)))
            .region(Region.of(region))
            .build();
    }
    
    /**
     * SQS 메시지 생성 템플릿
     *
     * @return
     */
    @Bean
    public SqsTemplate sqsTemplate() {
        return SqsTemplate.newTemplate(sqsAsyncClient());
    }    

    /**
     * SQS Listener 설정
     */
    @Bean
    public SqsMessageListenerContainerFactory<Object> defaultSqsListenerContainerFactory() {
        return SqsMessageListenerContainerFactory
            .builder()
            .configure(sqsContainerOptionsBuilder ->
                sqsContainerOptionsBuilder
                    .maxConcurrentMessages(10) // 컨테이너의 스레드 풀 크기
                    .maxMessagesPerPoll(10) // 한 번의 폴링 요청으로 수신할 수 있는 최대 메시지 수를 지정
                    .acknowledgementInterval(Duration.ofSeconds(5)) // AWS SQS 응답 간격
                    .acknowledgementThreshold(10) // AWS SQS 응답 최소 개수
            )
            .sqsAsyncClient(sqsAsyncClient())
            .build();
    }
}
  • 크게 SqsAsyncClient, SqsTemplete, SqsMessageListenerContainerFactory으로 구성된다.
  • SqsAsyncClient : AWS SQS 비동기 통신을 제공하며, access-key와 secret-key, region을 설정한 후 생성한다.
  • SqsTemplete : SQS로 메시지 생성 및 전송 템플릿 클래스이다.
  • SqsMessageListenerContainerFactory : SQS 메시지 수신 컨테이너 팩토리 클래스이다. 비동기로 수신하며 설정에 따라서 효율적으로 작업을 수행할 수 있다.
    • maxConcurrentMessages : 컨테이너의 스레드 풀을 설정한다. 기본값은 10이며 해당 설정에 따라서 병렬로 처리할 수 있는 스레드 수를 설정할 수 있다.
    • maxMessagesPerPoll : 한 번의 폴링(polling)으로 SQS 큐에서 가져올 수 있는 최대 메시지 수를 지정한다. 일종의 버퍼 역할을 한다고 보면된다.
    • acknowledgementInterval : 메시지를 처리한 후 즉시 SQS로 응답을 보내지 않고, 설정한 시간 만큼 기다렸다가 시간 내에 처리된 모든 메시지에 대해 한 번에 SQS로 응답을 보낸다.
    • acknowledgementThreshold : 설정한 개수의 메시지가 처리될 때마다 SQS로 응답을 보낸다. 설정한 개수 이하의 메시지가 처리된 경우에는 확인을 보내지 않고, 개수 충족 시 한 번에 SQS로 응답을 보낸다.
    • 일반적으로 acknowledgementInterval과 acknowledgementThreshold는 함께 사용되어, 시간과 메시지 수 기준 중 어느 한 가지 조건이 충족될 때 SQS로 응답을 보낼 수 있다.
  • 자세한 설정 정보를 확인하려면 아래 링크를 참조하면 된다.
https://docs.awspring.io/spring-cloud-aws/docs/3.0.0/reference/html/index.html#sqscontaineroptions-descriptions

NotificationSender

  • 알림 전송 관련 인터페이스 및 구현체를 정의한다. 
// 알림 메시지
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@Getter
public class Notification {
    private String message;
    private LocalDateTime createAt;

    public static Notification create(String message) {
        return new Notification(
            message,
            LocalDateTime.now()
        );
    }
}

// 알림 전송 결과
public record NotificationSendResult(
    String messageId,
    boolean success
) {

    public static NotificationSendResult success(String messageId) {
        return new NotificationSendResult(messageId, true);
    }

    public static NotificationSendResult failure() {
        return new NotificationSendResult(null, false);
    }
}

// 알림 전송 인터페이스
public interface NotificationSender {
    NotificationSendResult sendNotification(Notification notification);
}


// AWS SQS 알림 전송 구현체
@Slf4j
@RequiredArgsConstructor
@Component
public class AwsSqsNotificationSender implements NotificationSender {

    @Value("${cloud.aws.sqs.queue.name}")
    private String queueName;

    private final ObjectMapper objectMapper;

    private final SqsTemplate template;

    @Override
    public NotificationSendResult sendNotification(Notification notification) {
        try {
            String message = objectMapper.writeValueAsString(notification);

            SendResult<String> result = template.send(to -> to
                .queue(queueName)
                .payload(message));

            return NotificationSendResult.success(result.messageId().toString());
        } catch (Exception e) {
            log.error("send notification error : ", e);
            return NotificationSendResult.failure();
        }
    }
}

@RequiredArgsConstructor
@RequestMapping("/api/v1/send-notification")
@RestController
public class NotificationSendController {

    private final NotificationSender notificationSender;

    @PostMapping
    public ResponseEntity<NotificationSendResult> send(@RequestBody String message) {
        return ResponseEntity.ok(notificationSender.sendNotification(Notification.create(message)));
    }
}
  • AWS SQS 요청 및 응답 DTO를 생성하고 AWS SQS 구현체를 작성한 알림 전송 컨트롤러로 메시지를 전송해보자.

알림 메시지 전송 테스트

  • 알림 메시지 전송 엔트포인트로 요청을 보내고 SQS에 메시지가 수신되었는지 확인해보자 

  • AWS SQS 메시지 전송 및 수신 페이지로 이동한다

  • 메시지 수신을 확인해보면 사용가능한 메시지 2개를 확인할 수 있고, 메시지 폴링 버튼을 누르면 확인가능한 메시지 두건을 확인할 수 있다. 메시지의 ID를 보면 postman의 응답에 있는 messageId 와 동일한 것을 알 수 있다.
  • 폴링해서 메시지를 확인한다고 해도 메시지가 소모된 것은 아니며 SQS를 통해 메시지를 사용했고 삭제해달라는 요청을 보내야 비로서 메시지를 큐에서 제거한다

SQS 메시지 수신 Listener

@Slf4j
@Component
@RequiredArgsConstructor
public class AwsSqsListener {

    @SqsListener(value = "${cloud.aws.sqs.queue.name}")
    public void listen(String message) {
        log.info("notification : {}", message);
    }
}
  • @SqsListener(value = "${cloud.aws.sqs.queue.name}")을 어노테이션을 선언한 Bean에 대해서 SQS 메시지 수신을 수행한다.
  • SqsMessageListenerContainerFactory 설정에 따라서 동시에 처리할 수 있는 리시버(워커 또는 노드 라고도 한다)개수를 지정할 수 있다.
  • 서버 인스턴스 사양에 따라서 적절하게 설정 해주면 효율적인 SQS 메시지 처리가 가능하다
  • @SqsListener 어노테이션이 작동하지 않는 경우가 있는데 spring boot 3.0으로 변경되면서 연동하는 방식이 달라졌다고 한다. 라이브러리 버전을 잘 체크해보자
2024-08-14T17:22:59.582+09:00  INFO 38884 --- [ntContainer#0-1] i.s.s.i.sqs.receiver.AwsSqsListener      : notification : {"message":"{\r\n    \"message\" : \"반갑습니다\"\r\n}","createAt":"2024-08-14T17:09:33.4354856"}
2024-08-14T17:22:59.641+09:00  INFO 38884 --- [ntContainer#0-2] i.s.s.i.sqs.receiver.AwsSqsListener      : notification : {"message":"{\r\n    \"message\" : \"안녕하세요\"\r\n}","createAt":"2024-08-14T17:12:50.1727803"}
  • 위와 같이 메시지가 잘 수신된 것을 확인할 수 있다.
  • 별도의 설정을 하지 않으면 작업 종료 시 SQS에 응답하면서 메시지는 삭제된다.

6. 결론

 AWS SQS가 아니어도 Apache Kafka, Redis message broker, rebbitMQ 등을 활용해서 메시징 시스템을 구축할 수 있다. 전략적으로 현재 상황에 맞는 메시징 시스템을 선택하면 된다.

메시지 전송자와 메시지 수신자 (워커, 노드)를 별도의 서버로 구현하는 것이 좋으며 이러한 메시징 시스템을 적절하게 활용하는 것이 매우 중요하다

1. 스프링 부트란?

스프링 프레임워크 기반 프로젝트를 복잡한 설정없이 쉽고 빠르게 만들어주는 라이브러리입니다.
사용자가 일일이 모든 설정을 하지 않아도 자주 사용되는 기본설정을 알아서 해줍니다.

2. 왜 스프링 부트를 사용해야할까?

스프링 프레임워크를 사용하려면 많은 XML 설정 파일(web.xml, rootContext.xml, ServletContext.xml 등)들을 작성해야하고, 설정 방법을 모두 외우지 못했다면 기존에 사용했던 설정을 Copy&Paste하거나 개발자가 일일이 인터넷 검색을 통해서 설정해주어야 했습니다. 이는 곧 생산성과 비용 문제로 직결될 수 있습니다.

하지만 스프링 부트를 사용하면 복잡한 설정없이 쉽고 빠르게 스프링 프레임워크를 사용할 수 있습니다.

3. Spring Boot 장점

1) 라이브러리 관리의 자동화

 스프링 부트의 Starter 라이브러리를 등록해서 라이브러리의 의존성을 간단하게 관리할 수 있습니다.

2) 라이브러리 버전 자동 관리

기존에는 스프링 라이브러리의 버전을 하나하나 직접 입력해야 했지만, 스프링 부트는 pom.xml에 스프링 부트 버전을 입력하면 스프링 라이브러리 뿐만 아니라 서드 파티 라이브러리들도 호환되는 버전으로 알아서 다운해줍니다.
<!-- maven -->
<project>
...
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.4.0-SNAPSHOT</version>
        <relativePath/>
    </parent>
...

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <!-- 버전을 입력하지 않아도 된다. -->
        </dependency>
    </dependencies>

</project>

3) 설정의 자동화

스프링 부트는 @EnableAutoConfiguration 어노테이션을 선언해서 스프링에서 자주 사용 했던 설정들을 알아서 등록해줍니다.
이것을 스프링 부트에 마법이라고 불린다고 합니다.

4) 내장 Tomcat

스프링 부트는 Tomcat을 내장하고 있기 때문에 @SpringBootApplication어노테이션이 선언되어있는 클래스의 main() 메소드를  실행하는 것만으로 서버를 구동시킬 수 있습니다.  내장 톰캣을 사용하려면 특별한 설정없이 Web Starter 의존성만 추가해주면 됩니다.
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
</dependency>

 

5) 독립적으로 실행 가능한 JAR

웹 프로젝트라면 WAR 파일로 패키징해야하지만 스프링 부트는 내장 톰캣을 지원하기 때문에 JAR 파일로 패키징해서 웹 애플리케이션을 실행시킬 수 있습니다.

 


참조 :
https://kimyhcj.tistory.com/373 [기억과 기록]
elevatingcodingclub.tistory.com/25

1. HandlerInterceptor 란?

Spring Framework에서 지원하는 기능이며, URI 요청, 응답 시점을 가로채서 전/후 처리를 하는 역할을 합니다. Interceptor 시점에 Spring ContextBean에 접근할 수 있습니다. 

이와 비슷한 역할로 Filter와 AOP가 있습니다.

Filter는 Spring Framework와 무관하게 동작하며, Spring 자원을 이용할 수 없습니다. Filter는 보통 인코딩, XSS방어 등...의 용도로 이용됩니다. 

AOP는 주로 비즈니스 로직에서 실행됩니다. Logging, transaction 처리 등 중복 코드가 발생할 경우 중복을 줄이기 위해 사용되며, 메소드 처리 전후 지점에 자유롭게 설정이 가능합니다.  

2. Filter, Interceptor, AOP의 흐름

https://goddaehee.tistory.com/154

3. Interceptor 생성

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

public class MyInterceptor implements HandlerInterceptor {

    private Logger logger = LoggerFactory.getLogger(this.getClass());

    @Override
    public boolean preHandle(
            HttpServletRequest request,
            HttpServletResponse response,
            Object handler
    ) throws Exception {
        logger.info("[MYTEST] preHandle");
        return true;
    }

    @Override
    public void postHandle(
            HttpServletRequest request,
            HttpServletResponse response,
            Object handler,
            ModelAndView modelAndView
    ) throws Exception {
        logger.info("[MYTEST] postHandle");
    }

    @Override
    public void afterCompletion(
            HttpServletRequest request,
            HttpServletResponse response,
            Object object,
            Exception ex
    ) throws Exception {
        logger.info("[MYTEST] afterCompletion");
    }
}

1) PreHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
컨트롤러에 진입하기 전에 실행됩니다. 반환 값이 true일 경우 컨트롤러로 진입하고 false일 경우 진입하지 않습니다.
Object handler는 진입하려는 컨트롤러의 클래스 객체가 담겨있습니다.

2)PostHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)
컨트롤러 진입 후  View가 랜더링 되기 전에 수행됩니다.

3) afterComplete(HttpServletRequest request, HttpServletResponse response, Object object, Exception ex)
컨트롤러 진입 후 view가 랜더링 된 후에 실행되는 메소드입니다.

 4) afterConcurrentHandlingStarted(HttpServletRequest request, HttpServletResponse response, Object h)
비동기 요청 시 PostHandle과 afterCompletion이 수행되지 않고 afterConcurrentHandlingStarted가 수행됩니다. 

4. Interceptor 등록하기

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new MyInterceptor())
                .addPathPatterns("/*") // 해당 경로에 접근하기 전에 인터셉터가 가로챈다.
                .excludePathPatterns("/boards"); // 해당 경로는 인터셉터가 가로채지 않는다.
    }
}

WebMvcConfigurer를 구현해 Spring Boot에서 기본적으로 제공해주는 설정 중 interceptor부분을 커스텀합니다.

5. HomeController

@Controller
@RequestMapping("/")
public class HomeController {

    private Logger logger = LoggerFactory.getLogger(this.getClass());

    @GetMapping("/users")
    public String users() {
        logger.info("[MYTEST] users!!");
        return "생략..";
    }

    @GetMapping("/boards")
    public String boards() {
        logger.info("[MYTEST] boards!!");
        return "생략..";
    }
}

6. Test

GET localhost:8080/bamdule/users 요청

[MYTEST] preHandle
[MYTEST] users!!
[MYTEST] postHandle
[MYTEST] afterCompletion

GET localhost:8080/bamdule/boards 요청

[MYTEST] boards!!

참조 : https://goddaehee.tistory.com/154

 
IDE는 이클립스, intellij 등 아무거나 사용하셔도 됩니다.
보통은 IDE와 톰캣을 연동하여 테스트를 진행하지만 이번에는 별도의 tomcat을 이용해 테스트를 진행하려고 합니다.

Spring Boot-2.3.3.RELEASE
Maven-3.6.3
Tomcat 8.5
thymeleaf

1. 목표

java 8 설치
tomcat 8 설치
Spring Boot 프로젝트 생성
HomeController 생성
thymeleaf를 이용해서 hello 화면 뛰우기

 

2. java 8 설치

2020/04/07 - [IT/JAVA] - [Java] Windows 10 Open-JDK 8 다운로드 및 환경변수 설정

 

3. tomcat 8.5 설치

2020/09/07 - [IT/Apache, Tomcat] - [Tomcat] Windows 10에 Tomcat8.5 설치하기

 

4. maven 설치

2020/09/08 - [IT/Maven] - [Maven] Windows 10 maven 설치하기

 

5. Spring Boot 프로젝트 생성

https://start.spring.io/

Spring Boot initializr 사이트로 이동해서 아래 그림과 같이 선택 후 프로젝트를 GENERATE 해주세요. 

압축을 풀게되면 src 폴더와 pom.xml 및 여러가지 파일이 있는데, src와 pom.xml을 제외한 나머지 파일은 지워도 됩니다.

초기 프로젝트 구조

 

6.  pom.xml 수정

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.3.RELEASE</version>
        <relativePath/>
    </parent>
    <groupId>com</groupId>
    <artifactId>bamdule</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>war</packaging>
    <name>bamdule</name>
    <description>test project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
            <scope>provided</scope>
        </dependency>
        
        <!-- thymeleaf 라이브러리 추가 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>

    <build>
        <!-- war 이름 설정하기 -->
        <finalName>bamdule</finalName>
    
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

thymeleaf 라이브러리를 추가하고, war 이름을 설정해줍니다.

7. HomeController.java 생성 및 home.html 생성

1) HomeController.java

com.bamdule.controller package를 생성하고, 그 아래에 HomeController.java를 생성 후 아래와 같이 입력합니다.

package com.bamdule.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/")
public class HomeController {

    @GetMapping("/")
    public String homeView() {
        return "page/home";
    }

}

2) home.html

main / resources / templates / page / home.html 생성

<!DOCTYPE html>
<html>
    <head>
        <title>welcome</title>
        <meta charset="UTF-8">
    </head>
    <body>
        <div>hello</div>
    </body>
</html>

8. maven build

cmd를 실행시키고, 프로젝트 폴더로 이동합니다.

mvn install

빌드가 완료되면 프로젝트 폴더에 target 폴더가 생성됩니다.

빌드가 완료되었다면 bamdule.war를 설치해두었던 tomcat 폴더 안 webapps로 복사합니다.  

9.  tomcat 실행

cmd 실행 후, tomcat의 bin 폴더 경로로 이동한 다음 startup.bat을 실행시킨다.

실행이 완료되었다면 브라우저에 http://localhost:8080/bamdule/ 를 입력하면 다음과 같은 페이지가 노출된다.

Spring에서 JSR 303 어노테이션을 이용해 데이터 유효성검사를 진행할 수 있습니다.
보통 @NotBlank, @Size, @NotNull ...등 이미 만들어진 검증 어노테이션을 이용할 수 있지만, 자신의 목적에 맞는 검증 어노테이션을 커스텀하여 제작할 수 있습니다.

간단하게 password를 검증하는 어노테이션을 만들어보겠습니다.

1. Password Annotation 생성

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;
import kr.co.tlab.ppl.validator.impl.PasswordValidator;

@Documented
@Constraint(validatedBy = PasswordValidator.class)
@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Password {

    String message() default "";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

    public int min() default 0;

    public int max() default 2147483647;

    public boolean nullable() default false;
}

2. Password 검증 로직이 있는 ConstraintValidator 구현 

  • 비밀번호는 정해진 길이만큼 입력해야한다. (min ~ max)
  • 비밀번호는 nullable 속성을 통해 Null값을 받을 수 있다. (true/false)
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import kr.co.tlab.ppl.validator.Password;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils;

public class PasswordValidator implements ConstraintValidator<Password, String> {

    private int min;
    private int max;
    private boolean nullable;

    private Logger logger = LoggerFactory.getLogger(this.getClass());

    @Override
    public void initialize(Password passwordValidator) {
        //어노테이션 등록 시, 입력했던 Parameter를 초기화한다.
        min = passwordValidator.min();
        max = passwordValidator.max();
        nullable = passwordValidator.nullable();
    }

    @Override
    public boolean isValid(String password, ConstraintValidatorContext context) {
        //if password is not blank
        if (StringUtils.hasText(password)) {
            if (password.length() < min || password.length() > max) {
                addConstraintViolation(
                        context,
                        String.format("비밀번호는 %d자 ~ %d자 사이로 입력해주세요.", min, max)
                );
                return false;
            }
        } 
        else if (!nullable){
            addConstraintViolation(
                    context,
                    "비밀번호를 입력해주세요."
            );
            return false;
        }

        return true;
    }

    private void addConstraintViolation(ConstraintValidatorContext context, String msg) {
        //기본 메시지 비활성화
        context.disableDefaultConstraintViolation();
        //새로운 메시지 추가
        context.buildConstraintViolationWithTemplate(msg).addConstraintViolation();
    }

}

 

3. Member

public class Member{

    private Integer id;
    
    @NotBlank
    private String account;

    //Password 검증 어노테이션 등록
    @Password(min = 3, max = 10, nullable = true)
    private String password;

    @NotBlank
    private String name;

    private String email;

	//setter,getter 생략
}

 

4. ValidationTestController 

@Controller
@RequestMapping(value = "")
public class ValidationTestController {

    @PostMapping(value = "/member/save")
    @ResponseBody
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void saveMember(@Valid Member member) {
    	... save logic
    }
    
}

 

5. 비밀번호 "1" 입력 시 Response 결과

{
    "timestamp": 1594282566312,
    "status": 400,
    "error": "Bad Request",
    "errors": [{
            "codes": ["Password.member.password", "Password.password", "Password.java.lang.String", "Password"],
            "arguments": [{
                    "codes": ["member.password", "password"],
                    "arguments": null,
                    "defaultMessage": "password",
                    "code": "password"
                }, 10, 3, true],
            "defaultMessage": "비밀번호는 3자 ~ 10자 사이로 입력해주세요.",
            "objectName": "member",
            "field": "password",
            "rejectedValue": "1",
            "bindingFailure": false,
            "code": "Password"
        }],
    "message": "Validation failed for object='member'. Error count: 1",
    "path": "/member/save"
}

API 요청 시, 요청한 클라이언트의 정보를 확인할 수 있는 spring-mobile-starter 라이브러리 사용방법을 포스팅하겠습니다.

1. pom.xml

...
<dependencies>
	 <dependency>
            <groupId>org.springframework.mobile</groupId>
            <artifactId>spring-mobile-starter</artifactId>
            <version>2.0.0.M3</version>
            <type>pom</type>
        </dependency>
</dependencies>
...

<repositories>         
      <repository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>https://repo.spring.io/milestone</url>
      </repository>
</repositories>
...    

spring-mobile-starter 라이브러리를 추가하고, 해당 라이브러리가 저장 되어있는 repository 정보를 등록합니다.

2. TestController

@Controller
@RequestMapping(value = "/")
public class TestController {

    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    @GetMapping(value = "/deviceCheck")
    public String deviceCheck(
            Device device
    ) {

        if (device.isMobile()) {
            logger.info("[MYTEST] mobile user!");
        } else if (device.isTablet()) {
            logger.info("[MYTEST] tablet user!");
        } else {
            logger.info("[MYTEST] desktop user!");
        }

        logger.info("[MYTEST]Device : {} ", device);
        logger.info("[MYTEST]Device Platform : {}", device.getDevicePlatform());
        
        return "index";
    }
}

3. 결과 화면

devtools는 웹 서버 개발 시 유용한 기능들을 포함하고 있는 라이브러리 입니다.

화면 수정 시, 서버를 재구동 하지 않고 브라우저를 새로고침 함으로써 화면을 리로딩 시킬 수 있습니다.

1. pom.xml

...
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-devtools</artifactId>
</dependency>
...

spring-boot-devtools 라이브러리를 dependency합니다.

2. livereload enabled 및 Cache Disabled

aplication.yml
spring :    
    devtools :
        livereload :
            enabled : true
    thymeleaf:
        cache : false

devtools를 이용해서 livereload를 활성화 시키고, page cache를 비활성화 합니다. 

page cache option은 여러분이 사용하고 있는 페이지 템플릿에 따라 달라질 수 있습니다.


도움이 되셨다면 공감 한번씩 눌러주시면 감사하겠습니다.

1. Swagger란?

서버로 요청되는 URL 리스트를 HTML화면으로 문서화 및 테스트 할 수 있는 라이브러리입니다.

테스트는 Spring Boot 2.2.2 RELEASE에서 진행 되었습니다.


2. pom.xml

        ...
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.9.2</version> 
        </dependency>

        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.9.2</version>
        </dependency>      
        ...

3. SwaggerConfig.java 

import java.util.HashSet;
import java.util.Set;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

/**
 *
 * @author MW
 */
@Configuration
@EnableSwagger2
public class SwaggerConfig {

    @Bean
    public Docket api() {
        return new Docket(DocumentationType.SWAGGER_2)
                .consumes(getConsumeContentTypes())
                .produces(getProduceContentTypes())
                .apiInfo(getApiInfo())
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.example.bamdule.controller"))
                .paths(PathSelectors.ant("/member/**"))
                .build();
    }

    private Set<String> getConsumeContentTypes() {
        Set<String> consumes = new HashSet<>();
        consumes.add("application/json;charset=UTF-8");
        consumes.add("application/x-www-form-urlencoded");
        return consumes;
    }

    private Set<String> getProduceContentTypes() {
        Set<String> produces = new HashSet<>();
        produces.add("application/json;charset=UTF-8");
        return produces;
    }

    private ApiInfo getApiInfo() {
        return new ApiInfoBuilder()
                .title("API")
                .description("[Bamdule] API")
                .contact(new Contact("Bamdule Swagger", "https://bamdule.tistory.com/", "Bamdule5@gmail.com"))
                .version("1.0")
                .build();
    }
}
  • Swagger 설정을 정의한 코드입니다.
  • .consume()과 .produces()는 각각 Request Content-Type, Response Content-Type에 대한 설정입니다.(선택)
  • .apiInfo()는 Swagger API 문서에 대한 설명을 표기하는 메소드입니다. (선택)
  • .apis()는 Swagger API 문서로 만들기 원하는 basePackage 경로입니다. (필수)
  • .path()는 URL 경로를 지정하여 해당 URL에 해당하는 요청만 Swagger API 문서로 만듭니다.(필수)

4. Member.java

import io.swagger.annotations.ApiParam;

public class Member {

    @ApiParam(value = "member ID", required = true)
    private String id;

    @ApiParam(value = "member age", required = true)
    private int age;

    @ApiParam(value = "member email", required = true)
    private String email;
    
    //setter,getter 생략..
}
  • @ApiParam 어노테이션은 멤버 변수에 대한 설명 및 다양한 설정을 지원합니다.
    • value : 저장되야 할 값에 대한 설명을 명시합니다.
    • required : 필수 여부를 지정합니다.

5. MemberController.java

import com.example.bamdule.TO.Member;
import io.swagger.annotations.ApiOperation;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
@RequestMapping(value = "/member")
public class MemberController {

    @ApiOperation(value = "Member Save", notes = "사용자 저장")
    @GetMapping(value = "/save")
    @ResponseBody
    public ResponseEntity saveMember(Member member) {
        
        /*
            member Save ...
        */

        return ResponseEntity.ok(member);
    }
}
  • @ApiOperation : 메소드 설정 및 설명문을 기입합니다.
    • value : 메소드 설명
    • notes : 메소드 설명 2
  • SwaggerConfig.java 에서 해당 Controller를 Swagger API 문서로 지정합니다.
  • 브라우저를 통해 domain/contextPath/swagger-ui.html 로 이동하면 Swagger API 문서 페이지를 볼 수 있습니다.

6. Swagger API 문서 화면

Swagger API 문서 화면

  • member-controller를 클릭합니다.

 

paramter 입력

  • 화면에는 보이지 않지만 Try it out을 누른 후 Prameter를 입력해줍니다. 
  • 입력이 완료되었다면, Execute 버튼을 눌러주세요.

 

  • API 테스트에 대한 Code와 Detail한 응답 결과가 화면에 표시됩니다.

+ Recent posts