ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Aws + Spring] SNS(Simple Notification Service) + Spring Boot 에러 메일 전송 기능 구축하기
    Spring 2024. 1. 20. 16:28
    반응형

     

     

    소개

    이번 포스트에서는 Spring Boot와 AWS의 Simple Notification Service(SNS)를 결합하여 어플리케이션에서 발생한 에러를 실시간으로 감지하고 관리자에게 메일로 알림을 보내는 기능을 구축하는 방법에 대해 알아보겠습니다.

     

    AWS SNS(Simple Notification Service)란?

    AWS SNS는 Amazon Web Services(AWS)에서 제공하는 완전 관리형 메시징 서비스로, 분산된 시스템에서 효율적으로 메시지를 전송하고 받을 수 있는 서비스입니다. 다양한 플랫폼과 어플리케이션 간에 쉽게 메시지를 송수신할 수 있도록 설계되었습니다.

     

    주요 특징:

    1. 다양한 플랫폼 지원: SNS는 모바일 디바이스, 웹 어플리케이션, 이메일, HTTP 엔드포인트 등 다양한 플랫폼으로 메시지를 전송할 수 있습니다.
    2. 토픽 기반 아키텍처: SNS는 토픽(topic)이라는 개념을 사용하여 메시지를 관리합니다. 메시지를 특정 주제에 발행(publish)하면 해당 주제에 구독(subscribe)된 모든 엔드포인트에 메시지가 전송됩니다.
    3. 확장성 및 탄력성: SNS는 대규모 및 고가용성의 애플리케이션을 지원하도록 설계되어 있으며, 메시지 큐 및 다양한 프로토콜을 지원하여 애플리케이션 간의 상호 작용을 효율적으로 처리합니다.
    4. 메시지 필터링: 토픽을 구독하는 클라이언트는 필터 정책을 설정하여 특정 유형의 메시지만 수신할 수 있습니다. 이는 불필요한 트래픽을 줄이고 효율적으로 메시지를 관리하는 데 도움이 됩니다.
    5. 암호화 및 보안: SNS는 전송 중인 메시지를 암호화하여 데이터의 안전성을 보장하며, AWS Identity and Access Management(IAM)을 통해 안전한 액세스 제어를 제공합니다.

    사용 예시:

    1. 모바일 푸시 알림: SNS를 사용하여 iOS, Android 디바이스에 푸시 알림을 전송할 수 있습니다.
    2. 이메일 알림: SNS를 이용하여 이메일 알림을 구독자에게 전송할 수 있습니다.
    3. 자원 상태 알림: AWS 서비스에서 발생하는 이벤트를 통지받아 특정 자원의 상태를 모니터링하고자 할 때 활용할 수 있습니다.
    4. 마이크로서비스 아키텍처: 서로 다른 마이크로서비스 간에 이벤트 기반 아키텍처를 구성하여 효율적인 통신을 구현하는 데 사용할 수 있습니다.

    AWS SNS는 다양한 시나리오에서 유연하게 활용되며, 복잡한 메시징 요구사항을 간단하게 해결할 수 있도록 도와줍니다.

     

    이 중에서, 이메일 알림 방식을 사용하여 해당 기능을 구현할 예정입니다. 시작해보겠습니다.

     

     

    1. IAM 권한 추가

    먼저 IAM 계정에 SNS 권한을 추가 해줘야 합니다.
    IAM > 사용자 > [권한 추가] 클릭합니다.

     

     

     

    SNS를 검색하여 AmazonSNSFullAccess 권한을 추가합니다.

     

     

     

    2. 주제 생성

    이메일 방식을 사용할 것이기 때문에 표준을 선택하고, 주제 이름을 입력합니다.

     

     

     

    보안을 위해 전송 중 암호화를 선택하겠습니다. 값은 기본값을 사용하겠습니다.

     

     

     

     

    주제에 액세스 할 수 있는 정책을 정의해야 합니다. 저는 '지정된 AWS 계정만' 허용하는 정책을 선택 하겠습니다.

    AWS 계정 ID는 IAM > 보안 자격 증명 페이지에서 확인 가능합니다. (일련의 숫자 번호)
    확인한 AWS 계정 ID를 '게시자', '구독자' 입력칸에 입력합니다.

     

     

     

    나머지는 선택 하지 않고 기본값으로 생성하겠습니다.

    이로써 SNS 주제(토픽) 생성을 완료 했습니다. 다음으로 해당 토픽의 구독을 생성해보겠습니다.

     

     

    3. 구독 생성

     

    윗단계에서 생성한 주제 ARN을 선택합니다. 저희는 이메일 방식을 사용할 것이기 때문에 이메일 프로토콜을 선택하고,
    엔드포인트이메일을 받을 주소를 입력합니다. 선택 사항으로 구독 필터 정책을 사용하여 원하는 형식의 이메일만 수신할 수도 있습니다. 해당 포스팅에선 사용하지 않겠습니다.

     

     

     

    위에서 구독을 생성하고 나면, 입력한 이메일로 AWS Notifications 메일이 전송됩니다.

    Confirm subscription을 클릭하여 구독 인증을 해야 활성화가 정상적으로 이루어 집니다.

     

     

     

    SNS 주제를 확인했을때, 구독이 활성화 된 것을 확인할 수 있습니다.

     

     

    이로써 AWS SNS 토픽, 구독을 생성 완료했습니다.
    이제 Spring Boot에서 에러가 발생했을 때, 구독 계정으로 알림 메일을 전송하는 기능을 구현하겠습니다.


    실습환경

    • Intellij Ultimate
    • JAVA 17
    • Spring Boot 2.7.3
    • Maven
    • MySQL

     

    Maven Dependency 추가

    <dependency>
        <groupId>com.amazonaws</groupId>
        <artifactId>aws-java-sdk-sns</artifactId>
        <version>1.12.470</version>
    </dependency>

    pom.xml에 aws-java-sdk-sns dependency를 추가합니다.

     

     

    AWS Credentials 설정

    -- credentials 설정
    cloud:
      aws:
        credentials:
          access-key: {IAM 계정의 Access Key 입력}
          secret-access-key: {IAM 계정의 Secret Key 입력}
        region:
          static: ap-northeast-2
    -- sns topic arn 설정      
        sns-topic-arn: arn:aws:sns:ap-northeast-2:XXXXXXXXX:error-notification-topic

    application.yml에 aws credentialssns topic arn을  설정합니다.

     

     

     

    AwsConfig.class

    @Configuration
    @RequiredArgsConstructor
    public class AwsConfig {
    
        private final Environment environment;
    
        @Value("${cloud.aws.credentials.access-key}")
        private String accessKey;
    
        @Value("${cloud.aws.credentials.secret-access-key}")
        private String secretKey;
    
        @Value("${cloud.aws.region.static}")
        private String region;
        
        @Value("${cloud.aws.sns-topic-arn}")
        private String topicArn;
    
        @Bean
        public AwsComponent amazonSnsClient() {
        	BasicAWSCredentials awsCredentials = new BasicAWSCredentials(accessKey, secretKey);
            AWSStaticCredentialsProvider staticCredentialsProvider = new AWSStaticCredentialsProvider(awsCredentials);
    
            return AwsComponent
                    .builder()
                    .environment(environment)
                    .accessKey(accessKey)
                    .secretKey(secretKey)
                	.topicArn(topicArn)
                    .amazonSNS(
                            AmazonSNSClientBuilder
                                    .standard()
                                    .withRegion(region)
                                    .withCredentials(staticCredentialsProvider)
                                    .build()
                    )
                    .build();
        }
    }

    AmazonSNSClient 설정값을 초기화 하는 Class입니다.
    내용은 아래와 같습니다.

     

    1. @Value Annotation으로 yml에서 입력한 설정 값들을 불러와 injection 합니다.

    2. BasicAWSCredentials 클래스는 AWS 자격 증명을 나타내는 클래스입니다. 이 클래스를 사용하여 액세스 키와 비밀 키를 저장하고 AWS 서비스와 통신할 때 이 자격 증명을 사용할 수 있습니다.

    3. topicArn을 지정하여 AWS SNS Client를 구성하고 Bean으로 등록합니다.

     

     

     

    AwsComponent.class

    @Slf4j
    @Builder
    public class AwsComponent {
    
        private Environment environment;
        
        private AmazonSNS amazonSNS;
    
        private String accessKey;
    
        private String secretKey;
        
        private String topicArn;
    
        public void sendSnsMessage(Exception exception, String userId, String requestUri) {
            String allowedActiveProfile = Arrays.stream(environment.getActiveProfiles())
                    .filter(profile -> profile.equals("stage") || profile.equals("production"))
                    .findFirst()
                    .orElse(null);
    
            if (!StringUtils.hasText(allowedActiveProfile)) return;
    
            StringBuilder builder = new StringBuilder();
            builder.append("server : ").append(allowedActiveProfile).append("\n");
            builder.append("Error Occurrence Time : ").append(LocalDateTime.now()).append("\n");
            builder.append("Request URI : ").append(requestUri).append("\n");
            builder.append("UserId : ").append(userId).append("\n\n");
            builder.append(ExceptionUtils.getStackTrace(exception));
    
            PublishRequest request = new PublishRequest(topicArn, builder.toString());
            PublishResult result = amazonSNS.publish(request);
            log.info("Message sent. MessageId: {}", result.getMessageId());
        }
    }

     

    Aws Sns Message를 전송할 메서드를 해당 컴포넌트에 구현합니다. 본 포스팅은 로컬 환경에서 테스트를 진행할 예정이지만, 실제 상용 환경에서는 위 코드처럼 stage, production 환경에서만 메일을 수신할 수 있게끔 설정 하는 것이 좋습니다. 로컬에서 테스트를 진행시에는 "local"만 입력 해주시면 되겠습니다. 위 코드의 설명은 아래와 같습니다.

    먼저 Environment에서 active중인 Profiles를 가져온 뒤, allowedActiveProfile에 지정한 Profile인 경우에만 SNS에 Publish를 합니다. 그리고 메일의 본문을 설정해줘야 합니다. 에러가 발생했을때, 파악하기 쉽게 내용은 최대한 자세히 입력하는게 좋습니다.


    - server: 현재 에러가 발생한 서버 (stage or production)

    - time: 에러가 발생한 시각
    - request uri: 에러가 발생한 API Path

    - userId: API를 요청한 사용자 고유 ID (본 포스팅에선 세션 ID를 기반으로 사용했습니다. 프로젝트 환경에 맞춰서 설정하시면 됩니다.)
    - trace: Exception Stack Trace 내용

     

    SNS SDK의 PublishRequest에 topicArn과 StringBuilder의 내용을 초기화 합니다. 그리고 AmazonSNS 객체의 publish 메서드를 이용하여 request를 전송한 뒤 PublishResult 객체로 전송 된 message의 ID를 return 받습니다. 최종적으로 혹시 모를 상황에 대비하여 message ID를 log로 출력합니다.

     

     

     

    이렇게 Amazon SNS의 설정을 초기화 하고 Message를 Publish하는 컴포넌트 메서드도 구현을 했습니다.
    실제 테스트를 통해 결과를 확인 해보겠습니다.


    먼저, Exception이 발생 했을때 예외 처리를 해주는 ControllerAdvice 클래스를 생성한 뒤, Exception Handler를 지정해야 합니다. 본 포스팅에서는 테스트를 위해 간단하게 구성할 예정입니다.

     

    추가로, 공통 Global Exception Handler를 구성 하고 싶으신 분은 아래 링크를 참고 부탁 드립니다.
    https://developer-been.tistory.com/5

     

    Spring Boot 공통 Global Exception Handler

    🎈 Spring에서 전역적으로 Exception을 처리하는 방법을 포스팅한다. 예외를 처리하는 방법은 다양하게 있다. 1. 메서드 내 예외 상황을 예측하여 try-catch문 사용 2. 요구사항에 대한 예외 처리 (validat

    developer-been.tistory.com

     

     

     

    WebControllerAdvice.class

    @ControllerAdvice
    @Slf4j
    public class WebControllerAdvice {
    
        @Autowired
        AwsComponent awsComponent;
        
        @ResponseBody
        @ExceptionHandler(Exception.class)
        public ResponseObject<Void> handleException(Exception e, HttpServletRequest request) {
            String sessionUserId = getSessionUserId();
            log.error(ExceptionUtils.getStackTrace(e));
    
            if (!(e instanceof ClientAbortException)) {
                this.awsComponent.sendSnsMessage(e, sessionUserId, request.getRequestURI());
            }
    
            return ResponseObject.<Void>builder()
                    .code(HttpStatus.INTERNAL_SERVER_ERROR.value())
                    .message(HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase())
                    .build();
        }
        
        private String getSessionUserId() {
            Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
            if (authentication != null) {
                Object principal = authentication.getPrincipal();
                if (principal instanceof UserIdHolder userIdHolder) {
                    if (userIdHolder.toUserId() == null) return  null;
                    return userIdHolder.toUserId().getId();
                }
            }
            return null;
        }
    }

     

    WebControllerAdvice Class는 @ControllerAdvice 어노테이션을 통해 전역적인 예외 처리를 담당하는 클래스입니다.
    코드 설명은 아래와 같습니다.

     

    1. @ControllerAdvice: 이 어노테이션은 클래스가 전역적인 예외 처리를 수행하는 클래스임을 나타냅니다. 여러 컨트롤러에서 발생하는 예외를 한 곳에서 처리할 수 있도록 도와줍니다.
    2. awsComponent: 위에서 생성한 AwsComponent를 주입받는 필드입니다.
    3. @ResponseBody: 해당 메서드가 HTTP 응답의 몸체로 데이터를 직렬화하여 반환함을 나타냅니다.
    4. @ExceptionHandler(Exception.class): 테스트를 위해 Exception Class를 지정 했습니다. 해당 예외가 발생하면 이 메서드가 호출되어 예외를 처리하고, 그 결과를 응답으로 반환합니다. 원하는 종류의 Exception Class를 지정 하여 사용하면 됩니다.
    5. Return Class는 각 프로젝트에 맞춰서 선언 하시면 됩니다.

     

    if (!(e instanceof ClientAbortException)

    SNS를 통해 예외 정보를 전송하고 싶지 않은 Exception의 경우 위 조건문처럼 Exception Instance를 지정할 수 있습니다.
    참고용으로 추가 해놓은 부분이니 제거 하셔도 됩니다.

     

     

    private String getSessionUserId() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication != null) {
            Object principal = authentication.getPrincipal();
            if (principal instanceof UserIdHolder userIdHolder) {
                if (userIdHolder.toUserId() == null) return  null;
                return userIdHolder.toUserId().getId();
            }
        }
        return null;
    }

     

    Spring Security를 사용하는 경우, 파라미터로 HttpServletRequest 객체를 선언하면 위 코드를 이용하여
    API를 호출한 사용자의 session id를 가져올 수 있습니다. Principal에서 커스텀 사용자 아이디를 조회하기 위해서
    UserIdHolder라는 인터페이스를 사용하고 있습니다.

     

    최종적으로, Exception이 발생하면 위 handler method가 호출 되고 awsComponent의 sendSnsMessage
    메서드를 통해
    에러 정보를 Publish 하게 됩니다.

     

     

    TestRestController.Class

    @RestController
    @RequiredArgsConstructor
    @RequestMapping("/api/test")
    public class TestController {
    	
        @GetMapping
        public void throwsException(@RequestParam(value = "id") Long id) {
        	// insert logic
        }
    }

    테스트로 MethodArgumentTypeMismatchException를 발생시키기 위한 Controller를 추가 합니다.

     

     

     

     

    Talend API Tester로 해당 API의 id query parameter에 "undefined" 라는 String 값을 붙여서 호출 합니다.
    실제로 id parameter는 Long Type으로 선언 되어 있기 때문에
    String Value 가 들어오면 MethodArgumentTypeMismatchException이 발생합니다.


     

     


    호출 후, 에러 메일이 성공적으로 온 것을 확인 할 수 있습니다. 이처럼 에러가 발생했을 때, 예외 정보를 메일로 전송하게끔 구현 해놓는다면, 빠르게 에러를 파악하고 대응 할 수 있습니다. 메일을 이용한 방식뿐만 아니라 어떤 방법으로든 이러한 에러 알림 기능은 서비스를 운영함에 있어서 필수라고 생각 되는 부분입니다.

     

    감사합니다.

    반응형

    댓글

Designed by Tistory.