대규모 트래픽 게시판 구축 시리즈 #13: 알림 서비스 구현과 통합 - AWS SNS 및 Slack

2024. 9. 7. 23:29·프레임워크/자바 스프링

이론

알림?

  • 알림은 Spring Boot 애플리케이션 내에서 이벤트, 경고 또는 에러와 같은 중요한 상황을 감지하고 이를 관리자,개발자, 유저에게 알리는 기능을 가리킵니다.
  • 알림은 어플리케이션의 신속한 대응과 문제 해결을 돕는 데 중요한 역할을 합니다.
    • 장애 감지와 대응
    • 서비스 가용성 유지
    • 성능 모니터링
    • 비용 절감
    • 사용자 경험 향상
    • 예방적 조치AWS SNS 연동
  • Amazon SImple Notification Service(Amazon SNS)은 AWS의 클라우드 기반 메시징 서비스입니다. Amazon SNS를 사용하면 애플리케이션, 서비스 또는 시스템 간에 다양한 종류의 메시지를 안전하게 전송하고 관리 할 수 있습니다.
  • 기능
    • 푸시알림
    • 다중 프로토콜(HTTP, HTTPS, SMS, email, SQS, Lambda 등 지원)
    • 이벤트 기반 아키텍처
    • 확장성과 신뢰성
    • 미리 알림 및 모니터링

AWS SNS 예제 코드

  • aws, accessKeyId, aws.secretKey, asw.sns.topicArn은 각각 AWS 계정의 액세스 키, 비밀 키, 및 SNS Topic의 ARN을 나타냅니다. 이러한 값들은 application.proerties 또는 application.yml에 설정되어야 합니다. 어플리케이션에서 위에서 만든 SnsService를 사용하여 메시지를 발행할 수 있습니다.
aws.accessKeyId=your-access-key-id
aws.secretKey=your=secret-key
aws.sns.topicArn=your-sns-topic-arn

@Service
public class SnsService{
    @value("${aws.accessKeyId}")
    private String awsAccessKey;

    @value("${aws.secretKey}")
    private String awsSecretKey;

    @value("${aws.sns.topicArn}")
    private String snsTopicArn;

    public void publishMessage(String message){
        // AWS SNS 클라이언트 생성 
        SnsClient snsClient = SnsClient.builder()
        .region(Region.US_EAST_1)
        .credentialsProvider(StaticCredentialsProvider.create(
                        AwsBasicCredentials.create(awsAccessKey, awsSecretKey)))
        .build();

    // SNS 메시지 발행 요청 생성 
    PublishRequest publishRequest = PublishRequest.builder()
        .topicArn(snsTopicArn)
        .message(message)
        .build();

    // SNS Message 발행
    PublishResponse publishResponse = snsClient
        .publish(publishRequest)

    }

}

SLACK 알림 연동

메신저 플랫폼인 Slack은 다양한 형태의 알림 및 통지 기능을 제공하며, 이를 통해 사용자들이
팀 간 소통, 협업, 작업 관리를 용이하게 할 수 있습니다. Slack은 주로 다음과 같은 목적으로 사용됩니다.

  • 메신저 알림
  • 이벤트 알림
  • 스케쥴 및 일정 알림
  • 앱 및 서비스 통합 알림
  • 사용자 정의 알림

SLACK 알림 예제 코드

slack.webhook.url은 Slack Incomming Webhooks URL을 나타냅니다.
이 값은 application.properties 또는 application.yml에 설정 되어야 합니다.
어플리케이션에서 SlackNotificationService를 사용하여 Slack으로 메시지를 전송 할 수 있습니다.

slack.webhook.url=https://hooks.slack.com/services/your/webhook/url

@Service
public class SlackNotificationService{

    @Value("${slack.webhook.url}")
    private String slackWebhookUrl;

    public void sendSlackNotification(String message){
        String payload = "{\"text\" : \"" + message + "\"}";
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION.JSON);

        HttpEntity<String> entity = new HttpEntity<>(payload, headers);

        RestTemplate restTemplate = new RestTemplate();
        restTemplate.postForObject(slackWebhookUrl, entity, String.class);

    }

}

실습

AWS SNS 연동

  • 사전 준비
    • AWS SNS 이용을 위한 서비스 구성과 설정이 필요합니다.
    • 첫째, ap-northeast-2.console.aws.amazon.com/sns/v3/home?region=ap-northeast-2#/dashboard URL로 접속합니다.
      이는 AWS SNS 서비스를 등록하고 이용하기 위함입니다.
      • 둘째, topic(주제)을 생성합니다.
        • 토픽 설정 방법
          • FIFO vs 표준 : 표준에 체크
          • 이름 지정 : 임의의 이름 지정
          • 주제 생성 버튼 클릭
      • ARN 값을 복사하여 project의 application.properties or application.yml 파일에 작성합니다.
      • IAM을 이용해 만든 사용자의 accessKey, secretKey도 필요합니다.

환경변수 설정

# aws sns  

sns.topic.arn=arn:aws:sns:ap-northeast-2:5335938367146:board-server  
aws.accessKey=accessKey  
aws.secretKey=secretKey  
aws.region=ap-northeast-2  
cloud.aws.region.static=ap-northeast-2  
cloud.aws.stack.auto=false
  • 추천 : application.yml 파일에서 aws.accessKey와 aws.secretKey는 민감한 정보이므로, 코드 베이스에 직접 작성하지 않고, 환경 변수나 AWS Secrets Manager와 같은 안전한 방법을 사용하여 관리하는 것이 좋습니다. 지금은 편의상 하드코딩으로 하겠습니다.
      sns.topic.arn=${SNS_TOPIC_ARN}
      aws.accessKey=${AWS_ACCESS_KEY}
      aws.secretKey=${AWS_SECRET_KEY}
      aws.region=${AWS_REGION}

의존성 라이브러리 설치

  • build.gradle 파일에 가서 아래 라이브러리와 의존성 라이브러리를 작성해줍니다. 그리고 intellij 에디터에서 우측 상단의 코끼리 버튼을 클릭하여 정상 설치가 되는지 확인합니다.

dependencies {  
    // 나머지 생략
    // aws sns    
    implementation 'software.amazon.awssdk:sns'  
    implementation platform('software.amazon.awssdk:bom:2.5.29')  

    compileOnly group: 'org.springframework.cloud', name:'spring-cloud-aws-messaging', version: '2.2.1.RELEASE'     compileOnly group: 'org.springframework.cloud', name:'spring-cloud-aws-autoconfigure', version: '2.2.1.RELEASE'  

}  

AWS 환경설정 클래스

package com.example.boardserver.config;  

import lombok.Getter;  
import org.springframework.beans.factory.annotation.Value;  
import org.springframework.context.annotation.Configuration;  

@Getter  
@Configuration  
public class AWSConfig {  

    @Value("${sns.topic.arn}")  
    private String topicArn;  

    @Value("${aws.accessKey}")  
    private String accessKey;  

    @Value("${aws.secretKey}")  
    private String awsSecretKey;  

    @Value("${aws.region}")  
    private String awsRegion;  

}

이 AwsConfig 클래스는 Spring Boot 애플리케이션에서 AWS 관련 설정값들을 관리하기 위한 설정(Configuration) 클래스입니다. 이 클래스는 애플리케이션이 실행될 때 application.properties 또는 application.yml 파일에서 정의된 설정값들을 읽어와서 사용하게 됩니다. 각각의 설정값은 AWS SNS(Simple Notification Service)와 관련이 있으며, 해당 값을 애플리케이션의 다른 클래스에서 쉽게 사용할 수 있도록 제공합니다.

클래스 설명

  1. @Configuration 어노테이션:
    • 이 어노테이션은 Spring에게 이 클래스가 하나 이상의 @Bean 정의를 포함하고 있으며, Spring IoC 컨테이너에서 빈(Bean)으로 등록해야 한다는 것을 나타냅니다.
    • 이 클래스 자체가 빈으로 등록되며, 다른 빈들이 이 클래스의 값을 주입받아 사용할 수 있습니다.
  2. @Getter 어노테이션:
    • 이 어노테이션은 Lombok 라이브러리에서 제공하는 기능으로, 클래스의 모든 필드에 대한 getter 메서드를 자동으로 생성해줍니다. 이를 통해 코드의 보일러플레이트(중복된 코드)를 줄일 수 있습니다.
  3. @Value 어노테이션:
    • @Value 어노테이션은 Spring에서 외부 설정값을 주입하기 위해 사용됩니다.
    • 이 클래스에서는 @Value 어노테이션을 사용하여 application.properties 또는 application.yml 파일에 정의된 값들을 클래스 필드에 주입합니다.
    • 예를 들어, @Value("${sns.topic.arn}")는 sns.topic.arn이라는 키의 값을 topicArn 필드에 주입합니다.

클래스 필드 설명

  • private String topicArn;:
    • sns.topic.arn 키에 해당하는 값을 저장합니다. 이는 SNS에서 생성한 주제(Topic)의 ARN(Amazon Resource Name)을 의미합니다. 이 ARN은 특정 SNS 주제를 식별하는 고유한 문자열입니다.
  • private String accessKey;:
    • sns.accessKey 키에 해당하는 값을 저장합니다. 이는 AWS에서 제공하는 액세스 키로, AWS 서비스에 접근하기 위한 자격 증명 중 하나입니다.
  • private String awsSecretKey;:
    • sns.awsSecretKey 키에 해당하는 값을 저장합니다. 이는 AWS 액세스 키와 짝을 이루는 비밀 키로, AWS 서비스에 접근하기 위한 자격 증명입니다. 이 값은 민감한 정보이므로 안전하게 관리되어야 합니다.
  • private String awsRegion;:
    • sns.region 키에 해당하는 값을 저장합니다. 이는 AWS 리소스가 위치한 리전을 나타냅니다. 예를 들어, ap-northeast-2는 서울 리전을 의미합니다.

SnsService & SnsServiceImpl 클래스

package com.example.boardserver.service;  

import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;  
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;  
import software.amazon.awssdk.services.sns.SnsClient;  

public interface SnsService {  
    AwsCredentialsProvider getAWSCredentials(String accessKeyId, String secretAccessKey);  
    SnsClient getSnsClient();  
    }
package com.example.boardserver.service.impl;  

import com.example.boardserver.config.AWSConfig;  
import com.example.boardserver.service.SnsService;  
import lombok.extern.slf4j.Slf4j;  
import org.springframework.stereotype.Service;  
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;  
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;  
import software.amazon.awssdk.regions.Region;  
import software.amazon.awssdk.services.sns.SnsClient;  

@Slf4j  
@Service  
public class SnsServiceImpl implements SnsService {  
    AWSConfig awsConfig;  

    public SnsServiceImpl(AWSConfig awsConfig){  
        this.awsConfig = awsConfig;  
    }  

    public AwsCredentialsProvider getAWSCredentials(String accessKeyId, String secretAccessKey) {  
        AwsBasicCredentials awsBasicCredentials = AwsBasicCredentials.create(accessKeyId, secretAccessKey);  
        return () -> awsBasicCredentials;  
    }  

    public SnsClient getSnsClient(){  
        return SnsClient.builder()  
                .credentialsProvider(  
                        getAWSCredentials(awsConfig.getAccessKey(), awsConfig.getAwsSecretKey()))  
                .region(Region.of(awsConfig.getAwsRegion()))  
                .build();  
    }  
}

위 클래스는 SnsService 인터페이스의 구현체로, AWS SNS(Simple Notification Service)를 사용하여 메시지를 전송하기 위한 클라이언트를 생성하는 기능을 제공합니다. SnsServiceImpl 클래스는 Spring Boot 애플리케이션에서 SnsService 인터페이스를 구현하며, AWS SDK를 사용해 SNS 클라이언트를 설정하고 구성합니다.

클래스 설명

  1. @Slf4j 어노테이션:
    • 이 어노테이션은 Lombok 라이브러리에서 제공하는 기능으로, 클래스에 로깅 기능을 추가합니다. 이 어노테이션을 사용하면 log라는 이름의 로거(Logger) 인스턴스가 자동으로 생성됩니다. 개발자는 log.info(), log.debug(), log.error() 등의 메서드를 사용해 로그를 기록할 수 있습니다.
  2. @Service 어노테이션:
    • 이 어노테이션은 Spring에게 이 클래스가 서비스 클래스임을 알리며, Spring IoC 컨테이너에 빈(Bean)으로 등록됩니다. 다른 빈에서 의존성 주입을 통해 이 서비스를 사용할 수 있습니다.
  3. 의존성 주입 (Constructor Injection):
    • AWSConfig 객체는 이 클래스의 생성자를 통해 주입됩니다. 이 방식은 의존성 주입 중 생성자 주입(Constructor Injection)을 사용하는 것으로, Spring에서 권장하는 방법입니다. 생성자 주입을 사용하면 불변성(immutability) 보장과 테스트 용이성을 높일 수 있습니다.
  4. getAWSCredentials 메서드:
    • 이 메서드는 AWS 서비스에 접근하기 위한 자격 증명(AwsCredentialsProvider)을 생성합니다.
    • AwsBasicCredentials.create(accessKeyId, secretAccessKey)를 사용하여 AwsBasicCredentials 객체를 생성하고, 이를 반환하는 AwsCredentialsProvider를 제공합니다.
    • 문제점: getAWSCredentials 메서드에서 자격 증명을 제공하는 방식이 비효율적입니다. 현재 방식은 자격 증명을 람다 표현식으로 반환하는데, 이를 직접 반환하는 것이 더 간단하고 명확합니다.
    public AwsCredentialsProvider getAWSCredentials(String accessKeyId, String secretAccessKey) {
        return StaticCredentialsProvider.create(AwsBasicCredentials.create(accessKeyId, secretAccessKey));
    }
  5. getSnsClient 메서드:
    • 이 메서드는 AWS SNS 클라이언트를 생성합니다.
    • SnsClient.builder()를 사용해 클라이언트를 구성하고, 자격 증명 공급자와 AWS 리전을 설정한 뒤 빌드하여 클라이언트를 반환합니다.
    • awsConfig.getAccessKey(), awsConfig.getAwsSecretKey(), awsConfig.getAwsRegion() 값을 사용하여 클라이언트의 자격 증명과 리전을 설정합니다.

수정해야 할 부분

  1. getAWSCredentials 메서드 개선:
    • 위에서 설명한 것처럼, 자격 증명 공급자를 반환하는 람다 대신 StaticCredentialsProvider.create()를 사용하여 더 간단하고 명확하게 수정할 수 있습니다.
    public AwsCredentialsProvider getAWSCredentials(String accessKeyId, String secretAccessKey) {
        return StaticCredentialsProvider.create(AwsBasicCredentials.create(accessKeyId, secretAccessKey));
    }
  2. 자격 증명 관리:
    • 자격 증명(액세스 키 및 시크릿 키)을 코드에서 직접 관리하는 것은 보안 측면에서 취약할 수 있습니다. 따라서 환경 변수, AWS Secrets Manager, 또는 AWS IAM 역할을 사용하는 것이 좋습니다. 이를 통해 자격 증명을 외부에서 관리하고, 코드 내에서 민감한 정보를 다루지 않도록 개선할 수 있습니다.

최종 클래스

package com.example.boardserver.service.impl;  

import com.example.boardserver.config.AWSConfig;  
import com.example.boardserver.service.SnsService;  
import lombok.extern.slf4j.Slf4j;  
import org.springframework.stereotype.Service;  
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;  
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;  
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.regions.Region;  
import software.amazon.awssdk.services.sns.SnsClient;  

@Slf4j  
@Service  
public class SnsServiceImpl implements SnsService {  
    private final AWSConfig awsConfig;  

    public SnsServiceImpl(AWSConfig awsConfig){  
        this.awsConfig = awsConfig;  
    }  

    public AwsCredentialsProvider getAWSCredentials(String accessKeyId, String secretAccessKey) {  
        return StaticCredentialsProvider.create(AwsBasicCredentials.create(accessKeyId, secretAccessKey));
    }  

    public SnsClient getSnsClient(){  
        return SnsClient.builder()  
                .credentialsProvider(  
                        getAWSCredentials(awsConfig.getAccessKey(), awsConfig.getAwsSecretKey()))  
                .region(Region.of(awsConfig.getAwsRegion()))  
                .build();  
    }  
}

이와 같이 수정된 코드가 더 간결하고, 효율적으로 동작할 것입니다. 자격 증명 관리에 대한 보안 문제도 별도의 관리 방법을 도입하여 해결하는 것이 좋습니다.

SnsController

package com.example.boardserver.controller;  

import com.example.boardserver.config.AWSConfig;  
import com.example.boardserver.service.SnsService;  
import lombok.extern.log4j.Log4j2;  
import org.springframework.http.HttpStatus;  
import org.springframework.http.ResponseEntity;  
import org.springframework.web.bind.annotation.PostMapping;  
import org.springframework.web.bind.annotation.RequestBody;  
import org.springframework.web.bind.annotation.RequestParam;  
import org.springframework.web.bind.annotation.RestController;  
import org.springframework.web.server.ResponseStatusException;  
import software.amazon.awssdk.services.sns.SnsClient;  
import software.amazon.awssdk.services.sns.model.*;  

import java.util.Map;  

@Log4j2  
@RestController  
public class SnsController {  
     private final AWSConfig awsConfig;  
     private final SnsService snsService;  

     public SnsController(AWSConfig awsConfig, SnsService snsService) {  
         this.awsConfig = awsConfig;  
         this.snsService = snsService;  
     }  

     @PostMapping("/create-topic")  
    public ResponseEntity<String> createTopic(@RequestParam(value="topic") String topic) {  
        final CreateTopicRequest createTopicRequest = CreateTopicRequest.builder()  
                .name(topic)  
                .build();  

        SnsClient snsClient = snsService.getSnsClient();  
        final CreateTopicResponse createTopicResponse = snsClient.createTopic(createTopicRequest);  

        if(!createTopicResponse.sdkHttpResponse().isSuccessful()) throw getResponseStatusException(createTopicResponse);  

         log.info("topic name = {}", createTopicResponse.topicArn());  
        snsClient.close();  
        return new ResponseEntity<>("TOPIC CREATED", HttpStatus.OK);  
     }  

     @PostMapping("/subscribe")  
     public ResponseEntity<String> subscribe(  
             @RequestParam(value="endpoint") String endpoint,  
             @RequestParam(value="topicArn") String topicArn  
         ){  
            final SubscribeRequest subscribeRequest = SubscribeRequest.builder()  
                    .protocol("https")  
                    .topicArn(topicArn)  
                    .endpoint(endpoint)  
                    .build();  
         SnsClient snsClient = snsService.getSnsClient();  
         final SubscribeResponse subscribeResponse = snsClient.subscribe(subscribeRequest);  
         if(!subscribeResponse.sdkHttpResponse().isSuccessful()) throw getResponseStatusException(subscribeResponse);  
         log.info("topicARN to subscribe = {}", subscribeResponse.subscriptionArn());  
         return new ResponseEntity<>("TOPIC SUBSCRIBED", HttpStatus.OK);  

     }  

    @PostMapping("/publish")  
    public ResponseEntity<String> publish(  
            @RequestBody Map<String, Object> message,  
            @RequestParam(value="topicArn") String topicArn  
    ){  
        final PublishRequest publishRequest = PublishRequest.builder()  
                .topicArn(topicArn)  
                .subject("HTTP ENDPOINT TEST MESSAGE")  
                .message(message.toString())  
                .build();  
        SnsClient snsClient = snsService.getSnsClient();  
        final PublishResponse publishResponse = snsClient.publish(publishRequest);  
        log.info("message : {}", publishResponse.sdkHttpResponse().statusCode());  
        snsClient.close();  
        return new ResponseEntity<>(publishResponse.messageId(), HttpStatus.OK);  
    }  

     private ResponseStatusException getResponseStatusException(SnsResponse snsResponse) {  
         return new ResponseStatusException(  
                 HttpStatus.INTERNAL_SERVER_ERROR, snsResponse.sdkHttpResponse().statusText().get()  
         );  
     }  
}

클래스 설명

SnsController 클래스는 AWS SNS(Simple Notification Service)를 사용하여 주제를 생성하고, 주제에 구독하고, 메시지를 게시할 수 있는 RESTful API를 제공하는 Spring Boot 컨트롤러입니다. 이 클래스는 세 가지 주요 기능을 제공합니다: 주제 생성, 주제에 대한 구독, 메시지 게시. 각 기능은 HTTP POST 요청을 통해 호출할 수 있습니다.

주요 구성 요소

  1. 의존성 주입 (Dependency Injection):
    • AWSConfig와 SnsService는 생성자 주입(Constructor Injection) 방식으로 주입됩니다. 이 클래스는 이들 의존성을 사용하여 AWS SNS와 관련된 작업을 수행합니다.
  2. @RestController 어노테이션:
    • 이 어노테이션은 이 클래스가 Spring의 RESTful 웹 서비스 컨트롤러임을 나타냅니다. 모든 메서드는 JSON 형식의 응답을 반환합니다.
  3. @Log4j2 어노테이션:
    • 이 어노테이션은 Log4j2 로깅 프레임워크를 사용하여 로그를 기록하는 데 사용됩니다. log.info(), log.error() 등의 메서드를 통해 로그 메시지를 기록할 수 있습니다.

주요 메서드

  1. createTopic 메서드:
    • 역할: 주어진 이름으로 SNS 주제를 생성합니다.
    • 세부사항:
      • CreateTopicRequest를 빌드하여 SNS 클라이언트를 통해 주제를 생성합니다.
      • 주제 생성이 성공하면 HTTP 상태 코드 200과 함께 "TOPIC CREATED"라는 메시지를 반환합니다.
      • 생성된 주제의 ARN을 로그로 기록합니다.
    • 예외 처리: 주제 생성이 실패할 경우, ResponseStatusException을 통해 500 내부 서버 오류를 반환합니다.
  2. subscribe 메서드:
    • 역할: 주어진 엔드포인트를 SNS 주제에 HTTPS 프로토콜을 사용하여 구독합니다.
    • 세부사항:
      • SubscribeRequest를 빌드하여 SNS 클라이언트를 통해 주제에 대한 구독을 수행합니다.
      • 구독이 성공하면 구독 완료 메시지를 반환합니다.
      • 구독된 주제의 ARN을 로그로 기록합니다.
    • 예외 처리: 구독이 실패할 경우, ResponseStatusException을 통해 500 내부 서버 오류를 반환합니다.
  3. publish 메서드:
    • 역할: 주어진 SNS 주제에 메시지를 게시합니다.
    • 세부사항:
      • PublishRequest를 빌드하여 SNS 클라이언트를 통해 메시지를 주제에 게시합니다.
      • 메시지 게시가 성공하면 메시지 ID를 반환합니다.
      • 게시된 메시지의 상태 코드를 로그로 기록합니다.
    • 예외 처리: 메시지 게시가 실패할 경우, ResponseStatusException을 통해 500 내부 서버 오류를 반환합니다.
  4. getResponseStatusException 메서드:
    • 역할: SNS 응답에서 HTTP 상태가 실패했을 때 예외를 발생시키는 유틸리티 메서드입니다.
    • 세부사항:
      • SNS 응답의 상태 메시지를 사용하여 ResponseStatusException을 생성합니다.
      • 이 메서드는 각 SNS 요청의 성공 여부를 검사하는 데 사용됩니다.

보완 및 개선 사항

  1. SNS 클라이언트 관리:
    • 각 메서드에서 SNS 클라이언트를 생성하고 snsClient.close()를 호출하고 있습니다. SNS 클라이언트는 스레드 안전하며 재사용 가능하므로, 클라이언트를 매번 생성하고 닫는 대신 빈으로 관리하여 애플리케이션 전체에서 재사용하는 것이 더 효율적입니다.
    • 이를 위해 Spring 빈으로 SNS 클라이언트를 관리하고, 컨트롤러에서는 주입받아 사용하는 방식으로 개선할 수 있습니다.
  2. 메서드 반환값의 일관성:
    • 메서드에서 반환하는 값이 일관성을 유지해야 합니다. 현재 publish 메서드는 ResponseEntity<String>을 반환하고 있어 다른 메서드와 일관성을 유지하고 있습니다.
  3. 예외 처리:
    • 예외 처리 로직이 각 메서드에 잘 적용되어 있지만, getResponseStatusException 메서드에서 snsResponse.sdkHttpResponse().statusText().get()이 사용되고 있습니다. 이 부분은 Optional에서 값을 꺼내는 것이므로 orElse를 사용하는 것이 더 안전합니다. 예를 들어:
    private ResponseStatusException getResponseStatusException(SnsResponse snsResponse) {
        return new ResponseStatusException(
                HttpStatus.INTERNAL_SERVER_ERROR, snsResponse.sdkHttpResponse().statusText().orElse("Error")
        );
    }
  4. 프로토콜 고정:
    • subscribe 메서드에서 protocol("https")로 고정되어 있습니다. 만약 다른 프로토콜(SMS, email 등)을 지원해야 한다면, 이 부분을 파라미터화하여 유연성을 제공할 수 있습니다.

최종 요약

SnsController 클래스는 AWS SNS 주제를 생성하고, 주제에 대한 구독을 관리하며, 메시지를 게시하는 RESTful API를 제공합니다. 이 클래스는 주어진 작업을 효과적으로 수행하지만, SNS 클라이언트 관리 방식과 예외 처리 로직의 일부를 개선함으로써 더 나은 성능과 안정성을 확보할 수 있습니다.

데모

postman을 이용하여 데모를 해보겠습니다.

방법

공통

  1. POSTMAN 실행.
  2. HTTP POST 메소드로 설정한다.

/create-topic 엔드포인트

  1. URL은 localhost:8080/create-topic?topic=notify1 로 한다.
  2. 요청을 하게되면, 정상적으로 200 OK로 나타나고 HTTP Response Body에는 응답이 나타난다.
  3. AWS Console 웹페이지의 SNS 서비스 대시보드로 이동하여 console(주제)로 이동한다. 그리고 notify1 이 생성되었는지 확인한다.
    1. 이외 board-server, notify2, notify3, test-notify등 여러 파라미터를 넘겨주어 정상적으로 요청과 응답이 이루어 지는지 확인한다.

/subscribe 엔드포인트

  1. URL은 localhost:8080/create-topic?topic=notify1 로 한다.
  2. 요청을 하게되면, 정상적으로 200 OK로 나타나고 HTTP Response Body에는 응답이 나타난다.
  3. AWS Console 웹페이지의 SNS 서비스 대시보드로 이동하여 console(주제)로 이동한다. 그리고 notify1 이 생성되었는지 확인한다.
    1. 이외 board-server, notify2, notify3, test-notify등 여러 파라미터를 넘겨주어 정상적으로 요청과 응답이 이루어 지는지 확인한다.

슬랙 알림 구현

라이브러리 설치

    // 생략
dependencies {

    // slack
    implementation 'com.slack.api:bolt:1.18.0'
    implementation 'com.slack.api:bolt-servlet:1.18.0'
    // implementation 'com.slack.api:bolt-jetty:1.18.0' 의존성 불일치 발생으로 주석 혹은 삭제 

    // 생략
}


위에 나열된 각 라이브러리는 Slack의 Bolt 프레임워크를 사용하는 Java 애플리케이션을 개발할 때 사용하는 주요 의존성들입니다. 각 라이브러리가 어떤 역할을 하는지 설명드리겠습니다.

1. bolt

  • Bolt for Java Core Library:
    • 이 라이브러리는 Slack 애플리케이션을 구축하기 위한 핵심 라이브러리입니다.
    • Bolt는 Slack에서 제공하는 고수준 API 프레임워크로, Slack의 이벤트, 인터랙티브 컴포넌트(버튼, 메뉴 등), Slash Commands, 메시지 구성 등을 처리하는 것을 쉽게 해줍니다.
    • 이 라이브러리를 사용하면 Slack 애플리케이션을 구축하는 데 필요한 많은 작업을 자동으로 처리할 수 있으며, 슬랙 API를 사용한 작업을 더 간결하고 효율적으로 수행할 수 있습니다.
    • 주로 Slack 앱에서 이벤트 핸들러와 미들웨어를 작성할 때 사용됩니다.

2. bolt-servlet

  • Bolt for Java Servlet Adapter:
    • 이 라이브러리는 Bolt for Java를 기존의 Servlet 기반 애플리케이션과 통합하기 위한 어댑터 역할을 합니다.
    • Spring Boot와 같은 Java 기반 웹 프레임워크에서 Bolt 애플리케이션을 쉽게 통합할 수 있도록 지원합니다.
    • 이를 통해, Slack 이벤트를 처리하기 위한 엔드포인트를 서블릿 기반의 애플리케이션 내에서 쉽게 정의할 수 있습니다.
    • Spring Boot와 같은 프레임워크에서 Slack 애플리케이션을 구축할 때 매우 유용합니다.

3. bolt-jetty

  • Bolt for Java Jetty Adapter:
    • 이 라이브러리는 Jetty 서버를 사용하여 Bolt 애플리케이션을 실행할 수 있도록 하는 어댑터입니다.
    • Jetty는 경량화된 Java 서블릿 컨테이너로, 내장형 웹 서버로 자주 사용됩니다.
    • Bolt 애플리케이션을 독립적으로 실행하거나, 서버리스 환경 또는 경량화된 서비스로 배포하려는 경우, Jetty와의 통합이 유용할 수 있습니다.
    • 이 라이브러리를 사용하면 Jetty 서버에서 Slack 앱을 쉽게 실행할 수 있습니다.

요약

  • bolt 라이브러리는 Slack 애플리케이션을 구축하는 핵심 라이브러리로, Slack 이벤트 및 인터랙션을 쉽게 처리할 수 있는 고수준 API를 제공합니다.
  • bolt-servlet 라이브러리는 Bolt 애플리케이션을 서블릿 기반의 Java 애플리케이션에 통합할 수 있도록 도와줍니다.
  • bolt-jetty 라이브러리는 Jetty 서버에서 Bolt 애플리케이션을 실행할 수 있도록 하는 어댑터 역할을 합니다.

구현 코드 톺아보기

package com.example.boardserver.service;  

import org.springframework.stereotype.Service;  

@Service  
public interface SlackService {  

    void sendSlackMessageByBot(String message, String channel);  
    void sendSlackMessageByWebhook(String message, String channel);  
}
package com.example.boardserver.service.impl;  

import com.example.boardserver.service.SlackService;  
import com.slack.api.Slack;  
import com.slack.api.methods.MethodsClient;  
import com.slack.api.methods.SlackApiException;  
import com.slack.api.methods.request.chat.ChatPostMessageRequest;  
import com.slack.api.methods.response.chat.ChatPostMessageResponse;  
import lombok.extern.slf4j.Slf4j;  
import org.springframework.beans.factory.annotation.Value;  
import org.springframework.stereotype.Service;  

import java.io.IOException;  

@Slf4j  
@Service  
public class SlackServiceImpl implements SlackService {  
    @Value("${slack.token}")  
    String slackToken;  

    @Value("${slack.webhook.url}")  
    String slackWebhookUrl;  

    @Override  
    public void sendSlackMessageByBot(String message, String channel) {  
        try {  
            MethodsClient methodsClient = Slack.getInstance().methods(slackToken);  

            ChatPostMessageRequest request = ChatPostMessageRequest.builder()  
                    .channel(channel)  
                    .text(message)  
                    .build();  
            ChatPostMessageResponse response = methodsClient.chatPostMessage(request);  

            if (!response.isOk()) {  
                log.error("Slack API Error: {}", response.getError());  
            } else {  
                log.info("Message sent to channel: {}", channel);  
            }  
        } catch (SlackApiException | IOException e) {  
            log.error("Error sending Slack message to channel {}: {}", channel, e.getMessage());  
        }  
    }  

    @Override  
    public void sendSlackMessageByWebhook(String message, String channel) {  
        try {  
            Slack slack = Slack.getInstance();  
            slack.send(slackWebhookUrl, message);  // Webhook URL을 통해 메시지 전송  
            log.info("Message sent via webhook to channel: {}", channel);  
        } catch (IOException e) {  
            log.error("Error sending message via webhook: {}", e.getMessage());  
        }  
    }  
}

1. SlackService 인터페이스 및 구현

  • SlackService 인터페이스는 메시지를 보낼 때 두 가지 방법, 즉 봇을 통한 메시지 전송과 웹훅을 통한 메시지 전송을 정의하고 있습니다.
  • SlackServiceImpl 구현체에서 이 인터페이스를 구현하여 Slack API를 통해 메시지를 전송하는 기능을 제공하고 있습니다.
  • 여기에서 큰 문제는 없지만, 다음 사항을 고려해볼 수 있습니다.

개선사항

  1. 채널 처리 로직 개선:
    • 채널 이름을 전달받아 #이 없으면 추가하는 로직은 컨트롤러에서 수행하고 있습니다. 이 로직을 서비스 레이어로 옮기는 것도 고려해볼 수 있습니다. 이렇게 하면 SlackService가 Slack 메시지 전송과 관련된 모든 처리를 담당하게 되므로, 책임의 분리가 더 명확해집니다.
  2. WebHook 메시지 전송 기능:
    • sendSlackMessageByWebhook 메서드에서 특정 채널을 사용하지 않고 있습니다. WebHook이 특정 채널에 바인딩되어 있다면 이 부분은 문제가 되지 않지만, 바인딩되지 않은 WebHook을 사용하는 경우 채널별로 WebHook URL을 관리해야 할 수 있습니다.

2. SnsController 클래스


@Log4j2  
@RestController  
@RequestMapping("/sns")  
@RequiredArgsConstructor  
public class SnsController {  

     private final SlackService slackService;  

    @GetMapping("/slack")  
    public void sendSlack(
        @RequestParam(value="message") String message, 
        @RequestParam(value="channel") String channel
    ) {  
        channel = (!channel.startsWith("#") ? "#".concat(channel) : channel;  
        slackService.sendSlackMessageByBot(message, channel);  
    }  

}
  • 이 클래스는 AWS SNS와 Slack 메시징 기능을 모두 제공하는 REST API 엔드포인트를 구현하고 있습니다.
  • 각 메서드에서 AWS SNS와 Slack API를 통해 작업을 수행하고, 결과를 로그에 기록하고 있습니다.

개선사항

  1. 필드 주입:
    • SnsController에서 @RequiredArgsConstructor를 사용하고 있으나, 생성자가 직접 정의되어 있습니다. 생성자를 정의할 필요가 없으므로, 직접 정의한 생성자를 제거하고 @RequiredArgsConstructor를 활용하는 것이 더 깔끔한 코드 작성 방법입니다.
    @Log4j2
    @RestController
    @RequestMapping("/sns")
    @RequiredArgsConstructor
    public class SnsController {
        private final SnsService snsService;
        private final SlackService slackService;
    
        // Remaining code...
    }
  2. 에러 핸들링 개선:
    • 에러 핸들링은 현재 적절히 이루어지고 있으나, HTTP 상태 코드와 메시지를 명확히 전달하고자 할 때, throw new ResponseStatusException(...) 구문 대신 @ExceptionHandler를 사용한 글로벌 예외 처리를 고려할 수 있습니다.
  3. API의 일관성:
    • @GetMapping("/slack") 메서드에서 채널에 메시지를 보낼 때 사용하는 GET 메서드는 데이터를 생성하는 동작을 수행하고 있습니다. 이는 REST API 설계 원칙에 위배됩니다. POST를 사용하는 것이 RESTful한 설계에 더 적합합니다.
    @PostMapping("/slack")
    public void sendSlack(@RequestParam(value="message") String message, @RequestParam(value="channel") String channel) {
        if (!channel.startsWith("#")) {
            channel = "#".concat(channel);
        }
        slackService.sendSlackMessageByBot(message, channel);
    }
  4. 로깅의 유용성:
    • 현재의 로깅은 적절하게 이루어지고 있지만, 특정한 상황에서는 더 많은 정보가 필요할 수 있습니다. 예를 들어, Slack API 호출이 실패할 때 어떤 메시지를 전송하려 했는지, 어떤 채널로 시도했는지 등의 정보도 포함되면 문제 해결이 더 용이할 수 있습니다.

최종 검토

코드는 전체적으로 잘 작성되었으며, Slack API와 AWS SNS를 사용하는 기능도 잘 구현되었습니다. 위에서 언급한 개선 사항들을 고려하여 코드를 좀 더 개선할 수 있습니다.

특히, 다음 사항을 고려하십시오:

  1. 컨트롤러에서 서비스 레이어로 책임을 이동하는 구조.
  2. RESTful API의 일관성 유지.
  3. 에러 핸들링을 더 구조적으로 개선하기.
  4. 로깅에 더 많은 정보를 포함하여 디버깅 용이성을 높이기.

이러한 개선사항을 통해 코드의 유지보수성과 확장성을 높일 수 있을 것입니다.

저작자표시 (새창열림)

'프레임워크 > 자바 스프링' 카테고리의 다른 글

접속자 대기열 시스템 #1: 시스템 설계와 Spring WebFlux, Redis  (3) 2024.09.09
대규모 트래픽 게시판 구축 시리즈 #14: 배포 자동화  (1) 2024.09.07
대규모 트래픽 게시판 구축 시리즈 #12: 성능 테스트  (5) 2024.09.07
대규모 트래픽 게시판 구축 시리즈 #11: 로깅, 예외처리  (0) 2024.09.07
대규모 트래픽 게시판 구축 시리즈 #10: 게시판 검색 API  (1) 2024.09.07
'프레임워크/자바 스프링' 카테고리의 다른 글
  • 접속자 대기열 시스템 #1: 시스템 설계와 Spring WebFlux, Redis
  • 대규모 트래픽 게시판 구축 시리즈 #14: 배포 자동화
  • 대규모 트래픽 게시판 구축 시리즈 #12: 성능 테스트
  • 대규모 트래픽 게시판 구축 시리즈 #11: 로깅, 예외처리
hyeseong-dev
hyeseong-dev
안녕하세요. 백엔드 개발자 이혜성입니다.
  • hyeseong-dev
    어제 오늘 그리고 내일
    hyeseong-dev
  • 전체
    오늘
    어제
    • 분류 전체보기 (286)
      • 여러가지 (107)
        • 알고리즘 & 자료구조 (72)
        • 오류 (4)
        • 이것저것 (29)
        • 일기 (1)
      • 프레임워크 (39)
        • 자바 스프링 (39)
        • React Native (0)
      • 프로그래밍 언어 (38)
        • 파이썬 (30)
        • 자바 (3)
        • 스프링부트 (5)
      • 컴퓨터 구조와 운영체제 (3)
      • DB (17)
        • SQL (0)
        • Redis (17)
      • 클라우드 컴퓨팅 (2)
        • 도커 (2)
        • AWS (0)
      • 스케쥴 (65)
        • 세미나 (0)
        • 수료 (0)
        • 스터디 (24)
        • 시험 (41)
      • 트러블슈팅 (1)
      • 자격증 (0)
        • 정보처리기사 (0)
      • 재태크 (5)
        • 암호화폐 (5)
        • 기타 (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    Spring WebFlux
    #개발자포트폴리오 #개발자이력서 #개발자취업 #개발자취준 #코딩테스트 #항해99 #취리코 #취업리부트코스
    Python
    OOP
    백준
    취업리부트
    mybatis
    그리디
    파이썬
    docker
    시험
    java
    celery
    Docker-compose
    spring
    WebFlux
    프로그래머스
    자바
    DP
    FastAPI
    RDS
    Spring Boot
    완전탐색
    reactor
    EC2
    AWS
    항해99
    SAA
    Redis
    ecs
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.0
hyeseong-dev
대규모 트래픽 게시판 구축 시리즈 #13: 알림 서비스 구현과 통합 - AWS SNS 및 Slack
상단으로

티스토리툴바