FCM 이란

Firebase 클라우드 메시징(FCM)으로 배터리를 절약하면서 서버와 기기를 안정적으로 연결하고, iOS, Android, 웹에서 메시지와 알림을 무료로 주고받을 수 있습니다.

 

curl 발송

curl -X POST --header "Authorization: key=your key" --header "Content-Type: application/json" https://fcm.googleapis.com/fcm/send -d '{"to" : "your token",  "priority" : "high",  "notification" : {    "body" : "Background Message",    "title" : "BG Title"  },  "data" : {    "title" : "FG Title",    "message" : "Foreground Message"  }}'

 

Java 구현

1. gradle 라이브러리

// Google FCM
implementation 'com.google.auth:google-auth-library-oauth2-http:1.18.0'
implementation 'com.google.firebase:firebase-admin:9.2.0'

 

  •  Firebase에서 Legacy > HTTP v1으로 방식이 변경되면서 oauth2 인증 방식으로 구현을 하고 있습니다.

2. oauth2 인증 pem 발급

    1. https://firebase.google.com/products/cloud-messaging?hl=ko 에서 프로젝트 생성

    2. 앱등록

    3. 프로젝트 설정 > 서비스 계정 > 새 비공개키 발급

3. Java FirebaseApp 세팅

import java.io.FileInputStream;
import com.google.firebase.FirebaseOptions;
import com.google.firebase.FirebaseApp;


public class AppPush {

	public static void init() {
        Stirng projectId = "[Project Id]"; // 프로젝트 설정 > 일반 > 프로젝트ID
        String oauthPath = "service-oauth.json";
         // Firebase 앱 초기화
        FileInputStream serviceAndroidAccount = null;
        try {
            serviceAndroidAccount = new FileInputStream(oauthPath);
            FirebaseOptions options = new FirebaseOptions.Builder()
                    .setCredentials(GoogleCredentials.fromStream(serviceAndroidAccount))
                    .build();
            FirebaseApp.initializeApp(options);
        } catch (IOException e) {
            log.warn("=============== App Push Setting Failed ==============");
        }
    }
}

 

4. Java 발송

import com.google.firebase.messaging.*;

public class AppPush {

	public static void init() {
        ...
    }
    
    // 안드로이드 단건
    public boolean androidSendPush(String title, String body, String summary
            , String thumbnailURL, String clickAction,  String openAction, String receiveAction, long ttl,
            String token
    ) {

        try {
            Message message = Message.builder()
                    .setToken(token) // Phone Token
                    .setAndroidConfig(
                        AndroidConfig.builder()
                                .setTtl(ttl)
                                .setPriority(AndroidConfig.Priority.HIGH)
                                .putAllData(
                                        PushDataModel.builder()
                                                .title(title)
                                                .body(body)
                                                .summary(summary)
                                                .image(thumbnailURL)
                                                .click_action(clickAction)
                                                .open_action(openAction)
                                                .receive_action(receiveAction)
                                                .build()
                                                .toMap()
                                )
                                .setDirectBootOk(false)
                                .build()
                    )
                    .build();
            String responses = FirebaseMessaging.getInstance().send(message);
            return true;
        } catch (FirebaseMessagingException e) {
        	return false;
   	}
    }
    
    // IOS 단건
    public boolean iosSendPush(
            String title, String body, String summary
            , String thumbnailURL, String clickAction,  String openAction, String receiveAction,
            String token
    ) {
        try {
            Message message = Message.builder()
                    .setToken(token)
                    .putAllData(PushDataModel.builder()
                            .title(title)
                            .body(body)
                            .summary(summary)
                            .image(thumbnailURL)
                            .click_action(clickAction)
                            .open_action(openAction)
                            .receive_action(receiveAction)
                            .build()
                            .toMap()
                    )
                    .setApnsConfig(
                            ApnsConfig.builder()
                                    .setAps(
                                            com.google.firebase.messaging.Aps.builder()
                                                    .setAlert(
                                                            ApsAlert.builder()
                                                                    .setTitle(title)
                                                                    .setBody(body)
                                                                    .setSubtitle(summary)
                                                                    .build()
                                                    )
                                                    .build()
                                    )
                                    .build()
                    )
                    .build();
            String responses = FirebaseMessaging.getInstance().send(message);
            return true;
        } catch (FirebaseMessagingException e) {
                log.error("{}", e.getMessage(), e);
            return false;
        }
    }
	// 안드로이드 다건 
	public Map<String, Boolean> androidSendMulticastPush(
            String title, String body, String summary
            , String thumbnailURL, String clickAction,  String openAction, String receiveAction, long ttl,
            List<String> tokens
            ) {
        Map<String, Boolean> result = new HashMap<>();
        for (String token : tokens) {
            result.put(token, false);
        }

        try {
            MulticastMessage messages = MulticastMessage.builder()
                    .addAllTokens(tokens)
                    .setAndroidConfig(
                            AndroidConfig.builder()
                                    .setTtl(ttl)
                                    .setPriority(AndroidConfig.Priority.HIGH)
                                    .putAllData(
                                            PushDataModel.builder()
                                                    .title(title)
                                                    .body(body)
                                                    .summary(summary)
                                                    .image(thumbnailURL)
                                                    .click_action(clickAction)
                                                    .open_action(openAction)
                                                    .receive_action(receiveAction)
                                                    .build()
                                                    .toMap()
                                    )
                                    .setDirectBootOk(false)
                                    .build()
                    )
                            .build();
            BatchResponse responses = FirebaseMessaging.getInstance().sendEachForMulticast(messages);
            for (int i = 0; i < responses.getResponses().size(); i++) {
                result.put(tokens.get(i), responses.getResponses().get(i).isSuccessful());
            }
            return result;
        } catch (FirebaseMessagingException e) {
            log.error("{}", e.getMessage(), e);
            return result;
        }
    }

	// IOS 다건
    public Map<String, Boolean> iosSendMulticastPush(
            String title, String body, String summary
            , String thumbnailURL, String clickAction,  String openAction, String receiveAction,
            List<String> tokens
    ) {
        Map<String, Boolean> result = new HashMap<>();
        for (String token : tokens) {
            result.put(token, false);
        }

        try {
            MulticastMessage messages = MulticastMessage.builder()
                    .addAllTokens(tokens)
                    .putAllData(PushDataModel.builder()
                            .title(title)
                            .body(body)
                            .summary(summary)
                            .image(thumbnailURL)
                            .click_action(clickAction)
                            .open_action(openAction)
                            .receive_action(receiveAction)
                            .build()
                            .toMap()
                    )
                    .setApnsConfig(
                            ApnsConfig.builder()
                                    .setAps(
                                            com.google.firebase.messaging.Aps.builder()
                                                    .setAlert(
                                                            ApsAlert.builder()
                                                                    .setTitle(title)
                                                                    .setBody(body)
                                                                    .setSubtitle(summary)
                                                                    .build()
                                                    )
                                                    .build()
                                    )
                                    .build()
                    )
                    .build();
            BatchResponse responses = FirebaseMessaging.getInstance().sendEachForMulticast(messages);
            for (int i = 0; i < responses.getResponses().size(); i++) {
                result.put(tokens.get(i), responses.getResponses().get(i).isSuccessful());
            }
            return result;
        } catch (FirebaseMessagingException e) {
            log.error("{}", e.getMessage(), e);
            return result;
        }
    }
}

'Java > Spring' 카테고리의 다른 글

Java Mail  (0) 2023.08.02
[Spring] Rest API 통신 방법 ( RestTemplate vs FeignClient vs WebClient )  (0) 2023.05.05
[Spring] AOP  (0) 2023.03.23
[Spring] MapStruct 적용 방법  (0) 2023.03.21
[AWS] SQS Listener 구축 ( Java + Gradle + Spring )  (1) 2023.03.17

Java Mail 라이브러리 통신 원리

SMTP ( Simple Mail Transfer Protocol )

SMTP 프로토콜은 Simple Mail Transfer Protocol의 약어로 인터넷상에서 이메일을 전송하기 위해서 사용되는 통신 규약 중에 하나입니다. 그리고 이메일을 송수신하는 서버를 SMTP서버라고 합니다.

SMTP서버를 구축하기 위해서는 물리적인 서버(예를 들어 리눅스)를 구축하여 서버를 설치하고 네트워크 환경을 잡아줘야 하지만 네이버와 구글에서 계정에 대한 SMTP를 제공해 주기 때문에 SMTP를 구축하지 않는 방법

SMTP는 기본적으로 텍스트 기반 프로토콜로서, 암호화되지 않은 통신에는 포트 25를, 암호화된 통신(TLS/SSL)에는 포트 587이나 465를 사용합니다. 이는 연결을 초기화하고, 이메일 내용을 전송하며, 세션을 종료하는데 필요한 명령어와 응답들로 이루어져 있습니다.

 

구현

  1. 라이브러리
// https://mvnrepository.com/artifact/javax.mail/mail
implementation 'javax.mail:mail:1.4.7'

https://mvnrepository.com/artifact/javax.mail/mail

 

  2. 호출

// Session 선언

// Google TTL 방식 
Properties props = new Properties();
props.put("mail.smtp.host", "smtp.gmail.com");
props.put("mail.smtp.port", "587");
props.put("mail.smtp.auth", "true");
props.put("mail.smtp.starttls.enable", "true"); // STARTTLS 명령을 사용하여 암호화를 적용합니다.
props.put("mail.smtp.ssl.protocols", "TLSv1.2");

Session session = Session.getDefaultInstance(props
        , new Authenticator() {
            @Override
            protected PasswordAuthentication getPasswordAuthentication() {
                return new PasswordAuthentication(
                        "[이메일]",
                        "[비밀번호]"
                );
            }
        }
);

try {
	// 메일 선언
    MimeMessage message = new MimeMessage(this.session);
    // 발신자 이메일
    message.setFrom(new InternetAddress("[발신자 이메일]"));

    // 수신자 메일 주소
    // 이메일 메시지의 주요 수신자
    // to = List.of("[수신자이메일]");
    if (to != null && !to.isEmpty()) {
        for (String recipient : to) {
            message.addRecipient(Message.RecipientType.TO, new InternetAddress(recipient));
        }
    }
    // 이메일 메시지의 참조 수신자
    // cc = List.of("[참조자 이메일]");
    if (cc != null && !cc.isEmpty()) {
        for (String recipient : cc) {
            message.addRecipient(Message.RecipientType.CC, new InternetAddress(recipient));
        }
    }
    // 이메일 메시지의 비밀 참조 수신자
    // bcc = List.of("[비밀 참조 이메일]");
    if (bcc != null && !bcc.isEmpty()) {
        for (String recipient : bcc) {
            message.addRecipient(Message.RecipientType.BCC, new InternetAddress(recipient));
        }
    }

    // 제목
    message.setSubject("제목");

    // 첨부 파일
    // filePaths = List.of("첨부파일위치")
    if (filePaths != null && !filePaths.isEmpty()) {
        // 본문을 포함하는 MimeMultipart 객체 생성
        MimeMultipart multipart = new MimeMultipart();
        // 본문 추가
        BodyPart messageBodyPart = new MimeBodyPart();
        messageBodyPart.setText(text);
        multipart.addBodyPart(messageBodyPart);
        for (String filePath : filePaths) {
            messageBodyPart = new MimeBodyPart();
            DataSource source = new FileDataSource(filePath);
            messageBodyPart.setDataHandler(new DataHandler(source));
            messageBodyPart.setFileName(source.getName());
            multipart.addBodyPart(messageBodyPart);
        }
        // 메시지에 MimeMultipart 설정
        message.setContent(multipart);
    } else {
        // 메일 내용
        message.setText("메일본문");
    }

    // 전송
    Transport.send(message);
} catch (AddressException e) {
    log.error("Mail Sender AddressException :: ", e);
} catch (MessagingException e) {
    log.error("Mail Sender MessagingException :: ", e);
}

 

이슈사항

1. javax.mail.AuthenticationFailedException: 535-5.7.8 Username and Password not accepted. Learn more at
535 5.7.8 https://support.google.com/mail/?p=BadCredentials a15-20020a62e20f000000b0068664ace38asm8418364pfi.19 - gsmtp

원인 : 보안 연결 실패

  • 구글 

으로 설정하면 나오는 비밀번호를 위에 Session 연결용 비밀번호입니다.

아래와 같은 방식은 구글에서 보안 업그레이드로 인해 불가능합니다.

더보기

보안 수준이 낮은 앱 및 Google 계정

계정을 안전하게 보호하기 위해 2022년 5월 30일부터 ​​Google은 사용자 이름과 비밀번호만 사용하여 Google 계정에 로그인하도록 요청하는 서드 파티 앱 또는 기기의 사용을 더 이상 지원하지 않습니다.

중요: Google Workspace 또는 Google Cloud ID 고객에게는 이 기한이 적용되지 않습니다. 이러한 고객을 대상으로 한 시행일은 추후 Workspace 블로그에 공지될 예정입니다.

자세한 내용을 확인하려면 계속해서 아래 내용을 참고하세요.

 

Java에서 타 Rest API 통신

Java 진영에서 Rest API 통신을 위해 지원해 주는 라이브러리에 대해 소개합니다.

 

1. RestTemplate

스프링에서 제공하는 Http 통신을 하게하는 라이브러리입니다.

참고사항 : Spring 진영에서 더 이상 RestTemplate 업데이트 지원을 하지 않겠다고 선언 또한 비동기 방식을  지원을 하지 않아 AsyncRestTemplate이 존재하나 해당 부분은 Deprecated 되었고, 해당 대체제로 나온 것이 WebClient입니다.

특징

  1. Spring 3.0부터 지원하는 Spring의 HTTP 통신 템플릿
  2. HTTP 요청 후 JSON, XML, String과 같은 응답을 받을 수 있는 템플릿
  3. Blocking I/O 기반의 동기방식을 사용하는 템플릿
  4. RESTful 형식에 맞추어진 템플릿
  5. Header, Content-Tpye등을 설정하여 외부 API 호출
  6. Server to Server 통신에 사용

예제

@Configuration
public class RestTemplateConfig {
    @Bean
    public RestTemplate restTemplate(RestTemplateBuilder builder) {
        return builder
                .setConnectTimeout(Duration.ofSeconds(10)) // Connection Timeout 설정
                .setReadTimeout(Duration.ofSeconds(10)) // Read Timeout 설정
                .build();
    }
}
@Slf4j
@RestController
@RequiredArgsConstructor
public class RestTemplateController {

    private final RestTemplate restTemplate;
    private final static String BASE_URL = "http://localhost:8080";
    private final static String SAMPLE01 = "/api/sample01/";
 
    @GetMapping("/api/rest")
    public void sample() {
    	// Uri 생성 Rest API URI
    	URI uri = UriComponentsBuilder
            .fromUriString(BASE_URL)
            .path(SAMPLE01)
            .encode()
            .build()
            .toUri();
            
       // Get 방식 호출     
       String res = restTemplate.getForObject(uri, String.class);
       log.info("Elapsed : {} ", res);
       
       // 다양한 Rest API 호출 방식
       // 1. Header 정의
       Map postData = new HashMap();
       HttpHeaders headers = new HttpHeaders();
       List<MediaType> acceptableMediaTypes = new ArrayList<>();
       acceptableMediaTypes.add(MediaType.ALL);
       headers.setAccept(acceptableMediaTypes);
       headers.setContentType(MediaType.APPLICATION_JSON_UTF8);
       headers.setCacheControl(CacheControl.noCache());
       HttpEntity<String> entity = new HttpEntity<>(JsonHelper.toJson(postData), headers);
       // 2. Rest API 통신
       String res2 = restTemplate.exchange(uri, HttpMethod.GET, httpEntity, String.class);
       
       log.info("Elapsed : {} ", res2);
    }
 
}

 

참고

https://dejavuhyo.github.io/posts/spring-resttemplate/

 

2. FeignClients

Feign Client web service 클라이언트를 보다 쉽게 작성할 있도록 도와주며, interface 작성하고 annotation 붙여주면 세부적인 내용 없이 사용할 있기 때문에 코드 복잡도가 낮아집니다. Netflix 에서 만들어졌고spring-cloud-starter-openfeign으로으로 스프링 라이브러리에서 사용할 있습니다.

특징

  1. SpringMvc에서 제공되는 어노테이션을 그대로 사용할 수 있다. 
  2. RestTemplate 보다 간편하게 사용할 수 있으며 가독성이 좋다.
  3. Feign Client를 사용한 통합 테스트가 비교적 간편하다.
  4. 요청에 대한 커스텀이 간편하다.

예제

  1. 라이브러리 등록

implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'

  2. 선언

@EnableFeignClients // FeignClients사용 선언
@SpringBootApplication
public class FeignClientApplication {

	public static void main(String[] args) {
		SpringApplication.run(FeignClientApplication.class, args);
	}

}

  3. 통신 인터페이스 작성

@FeignClient(name = "sample-client", url = "http://localhost:8080")
public interface SampleClient {
    @GetMapping(path = "/api/sample01/{id}")
    String getSample01(@PathVariable Long id);
}

  4. 호출

@Slf4j
@RestController
@RequiredArgsConstructor
public class FeginClientsController {
    private final SampleClient sampleClient;
    
    @GetMapping("/api/fegin")
    public void sample() {
    	String res = sampleClient.getSample01(1L);
        log.info("Elapsed : {} ", res);
    }
    
}

3. WebClient

Spring WebFlux를 쓰게 되면 지원하게 되는 Rest API 통신 모듈로 기본적으로 WebFlux에 특징에 맞게 Non-Blocking 방식으로 통신을 지원하여 자원을 좀 더 효율적으로 쓸 수 있는 장점을 가지고 있습니다.

특징

  1. Spring 5.0부터 지원
  2. 싱글 스레드 방식
  3. Non-Blocking 방식, 동기/비동기 모두 지원
  4. Reactor 기반의 Functional API (Mono, Flux)

예제

   1. WebFlux로 Spring boot 만들기

implementation 'org.springframework.boot:spring-boot-starter-webflux'

  2. WebFlux 선언

@EnableWebFlux // WebFlux 선언
@SpringBootApplication
public class WebclientApplication {

	public static void main(String[] args) {
		SpringApplication.run(WebclientApplication.class, args);
	}

}

  3. WebClient Bean 등록

@Configuration
public class WebClientConfig {
    @Bean
    public WebClient webClient() {
        return WebClient.builder()
                .baseUrl("http://localhost:8080")
                .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
                .clientConnector(new ReactorClientHttpConnector())
                .build();
    }
}

  4. 호출

@Slf4j
@RestController
@RequiredArgsConstructor
public class WebClientController {

    private final WebClient webClient;
    private final static String SAMPLE01 = "/api/sample01/";
    
    @GetMapping("/api/webclient")
    public void sample01() {
    	Mono<String> res = webClient.get()
                    .uri(SAMPLE01 + finalI)
                    .retrieve()
                    .bodyToMono(String.class);
                   
        log.info("Elapsed : {} ", res.block());           
    }
    
 }

번외 interface 방식 예제

1 ~ 3번 예제까지 처리는 동일

 4. HttpServiceProxyFactory 선언

@Bean
public HttpServiceProxyFactory httpServiceProxyFactory(
        WebClient webClient
) {
    return HttpServiceProxyFactory
            .builder(WebClientAdapter.forClient(webClient))
            .build();
}

 5. Interface 선언

public interface SampleClient {

    @GetExchange("/api/sample01/{id}")
    Mono<String> sample01(@PathVariable Long id);
}

 6. 호출

@Slf4j
@RestController
@RequiredArgsConstructor
public class WebClientController {
    private final HttpServiceProxyFactory httpServiceProxyFactory;
 
    @GetMapping("/api/webclient/interface")
    public void sample01Interface() {
    	Mono<String> res = sampleClient.sample01(1L);
        log.info("Elapsed : {} ", res.block());     
    }
 
 }

총정리

  RestTemplate FeignClients WebClient
Non-Blocking 불가능 불가능 가능
비동기화 불가능 불가능 가능

성능 정리

상황 :

  • Rest API를 받는 서버는 Thread가 5개이며 1초에 딜레이가 걸리는 API입니다.
  • 보내는 쪽은 100개에 Thread를 만들어서 통신 요청
  • Elapsed : [호출카운트] [통신완료 시간] | [결과]

1. RestTemplate

건당 Blocking 방식이다 보니 1초씩 증가하여 최종적으로 보내는 각각의 Rest API는 9초 정도 걸리는 것으로 확인

 

2. FeignClients

위에 RestTemplate 보다 최종적으로 오는 API는 20초까지 걸리는 현상 발견???

3. WebClient

확실히 Non-Blocking방식이라서 좀 더 빨리 치고 오는 느낌도 있는 것 같습니다.

'Java > Spring' 카테고리의 다른 글

[FCM] Java FCM 구현  (0) 2023.11.09
Java Mail  (0) 2023.08.02
[Spring] AOP  (0) 2023.03.23
[Spring] MapStruct 적용 방법  (0) 2023.03.21
[AWS] SQS Listener 구축 ( Java + Gradle + Spring )  (1) 2023.03.17

Docker Hub에 Push

1. Docker Hub 사이트 접속

https://hub.docker.com/

 

Docker Hub Container Image Library | App Containerization

Deliver your business through Docker Hub Package and publish apps and plugins as containers in Docker Hub for easy download and deployment by millions of Docker users worldwide.

hub.docker.com

해당 사이트에 계정을 만들어주세요. 

2. Docker 설치

( 저 같은 경우는 Mac이라 해당 OS에 맞게 설치해 주시면 됩니다. )
https://docs.docker.com/desktop/install/mac-install/

 

Install Docker Desktop on Mac

 

docs.docker.com

3. 실행 예제 파일 생성 및 docker 등록

  • hello.js
var http = require('http');
var content = function(req, resp) {
	resp.end("Hello World!"+"\n");
	resp.writeHead(200);
}

var w = http.createServer(content);
w.listen(8000);
  • Dockerfile
FROM node:slim
EXPOSE 8000
COPY hello.js .
CMD node hello.js

위의 두 파일을 생성합니다. hello.js는 node로 실행하면 간단히 Hello World가 나오는 화면입니다.

Dokerfile은 도커를 실행할 파일입니다.
FROM : 설치할 파일 ( node:slim 같이 설치 )

EXPOSE : 포트
COPY : 어떤 파일 복사 ( 뒤에. 은 현재 폴더라는 의미 )

CMD : 실행 명령어

 파일 실행

docker build -t tmkube/hello .
# docker build -t [레포지토리]/[이미지]:[버전] .[위치]

4. Docker 이미지 생성 확인

1. Docker 명령어 확인

docker images

2. Docker 프로그램으로 확인

5. Run

docker run -d -p 8100:8000 tmkube/hello
# docker run -d -p [입력포트]:[도커에서 받을 포트] [레포지토리]/[이미지]

-d : 백그라운드 모드

-p : 포트
6. 사이트 확인

7. Hub push

1. docke login

docker login

2. push

docker push tmkube/hello
# docker push [레포지토리]/[이미지]

8. 확인


Issue

상황 : 권한 부족

원인 :

1. 로그인이 제대로 안되었을 때 -> 로그인은 정상

2. 주소가 이상할 때 -> 이게 문제였는데.

강의에서 들은 내용하고 똑같이 해도 문제가 되었습니다. 그런데 알고 보니 저기 tmkube라는 것이 강사님 도커 레퍼지토리였더군요....

 

해결 :

내 레포지토리로 새로 tag로 이미지를 만든다.

docker tag tmkube/hello [로그인ID]/hello

다시 Push

docker push [로그인ID]/hello

 

개념

AOP (Aspect Oriented Programming ) 관점 지향 프로그래밍 여러 기능 중에 공통되어 사용되는 곳을 관점을 통일시켜서 기능을 동작하게 하는 방식입니다.
내부 적으로는 JDK 동적 프록시 or CGLIB 프락시를 이용해서 구현되는 기능
( 개념만 보고는 이해가 안됩니다. 백문일타( 백번 듣는 것보다는 한번 쳐보는게 좋다 )입니다. 한번 쳐보면 바로 이해되세요. )

AS-IS Class 기능
TO-BE AOP적용

용어

포인트컷 ( Pointcut )

  • 대상 여부를 확인하는 필터 역할
  • 주로 AspectJ표현식을 사용해서 지정
  • NameMatchMethodPointcut : 메서드 이름을 기반으로 매핑
  • JdkRegexpMethodPointcut : JDK 정규 표현식을 기반으로 포인트컷을 매핑
  • TruePointcut : 항상 참을 반환
  • AnnotationMatchingPointcut : 애노테이션을 매칭한다.
  • AspectJExpressionPointcut : aspectJ표현식으로 매칭한다.

어드바이스 ( Advice )

  • 부가 기능 로직만 담당
  • 특정 조인 포인트에서 Aspect에 의해 취해지는 조치
  • Around(주변), Before(전) After(후) 와 같은 다 향한 종류의 어드바이스가 있음
  • Around : 프로시저에 전체적인 역할
  • Before : 타겟 실행 전에 실행하는 역할
  • AfterReturning : 타겟이 성공한 결과 후 실행하는 역할
  • AfterThrowing : 타겟이 실패한 결과 후 실행하는 역할
  • After : 타겟이 수행 완료 후 실행하는 역할

어드바이저 ( Advisor )

  • 어드바이스 + 포인트컷으로 구성

조인 포인트 ( Join Point )

  • 어드바이스( Advice ) 가 적용될 수 있는 위치, 메서드 실행, 생성자 호출, 필드 값 접근, static 메서드 접근 같은 프로그램 실 중 지점을 의미
  • 조인 포인트는 추상적인 개념. AOP를 적용 할 수 있는 모든 지점을 의미

위빙 ( Weaving )

  • 포인트컷으로 결정한 타겟의 조인 포인트에 어드바이스를 적용하는 것
  • 위빙을 통해 핵심 기능 코드에 영향을 주지 않고 부가 기능을 추가할 수 있음
  • AOP 적용을 위해 에스팩트를 객체에 연결하는 상태
    - 컴파일 타임
    - 로드 타임
    - 런타임

사용법

1. dependencies

compileOnly 'org.springframework.boot:spring-boot-starter-aop'

2. 예제 선언

@Aspect
@Component
public class ExampleAop {
	@Around("execution(* spring.example.io..*(..))") 
	public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
    	// 실행전작업
        try {
        	return joinPoint.proceed(); // AOP를 적용시킨 메서드 실행
        } finally {
          // 실행 완료 작업
        }
    }
}

@Aspect : AOP 선언

@Around : 어드바이스 선언 ( @Before , @After , @AfterReturing , @AfterThrowing 이 더 존재합니다. )
   내부 선언 부분은 Point cut 부분 선언

  • execution : AspectJ 표현식으로 패키지 주소 또는 이름을 설정해서 AOP를 적용할 대상을 선택합니다.
  • @annotation(어노테이션주소) : custom annotation 을 선언한 메서드들 대상으로 지정
@Aspect
@Component
public class ExampleAop {
	@Around("@annotation(io.example.aop.annotation.CustomAnnotation)") 
	public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
    	// 실행전작업
        try {
        	return joinPoint.proceed(); // AOP를 적용시킨 메서드 실행
        } finally {
          // 실행 완료 작업
        }
    }
}

joinPoint 객체 활용

  • joinPoint.getTarget() : 실제 호출 대상
  • joinPoint.getArgs() : 전달인자
  • joinPoint.getSignature() : 클라이언트가 호출한 메소드의 시그니처 (리턴타입, 이름, 매개변수) 정보가 저장된 Signature 객체를 리턴
  • joinPoint.proceed() : 실제 호출 대상 실행

문제 상황

1. 어떤 메서드가 동작 후 반환값을 바꾸고 싶은 상황

결론 : 반환값 자체는 바꿀 수 없습니다.

상황 : 예전에 마스킹처리를 위해 특정 조건이면 반환값들을 마스킹해서 나오게 하는 상황이 있었습니다. 이때, AOP를 이용해서 반환값 자체를 바꾸려고 하니 안된 경험이 있습니다.

 

도움을 받은 주소

김영한님 스프링 핵심 원리 - 고급 편

( https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B3%A0%EA%B8%89%ED%8E%B8 )

 

MapStruct 란?

DTO와 Entity의 변환을 쉽게 도와주는 라이브러리입니다.

 

사용법

기본 사용법

1. gradle에 dependencies 추가

build.gradle

dependencies {	
    // mapstruct 라이브러리입니다.
	implementation 'org.mapstruct:mapstruct:1.5.3.Final'
	annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.3.Final'
}

2. mapper Interface 추가

@Mapper(componentModel = "spring")
public interface ComputerMapper{
    ComputerDTO asDto(Computer computer);
    Computer asEntity(ComputerDTO computerDTO);
}
  • componentModel : Spring boot에서 사용할 때는 spring으로 하시면 됩니다.
  • asDto, asEntity 명칭은 다른 거 하셔도 무방합니다. toDTO , toEntity 뭐 이런 식으로 하셔도 무방합니다. 

3. 호출

private final ComputerMapper computerMapper;
...
ComputerDTO computerDTO = computerMapper.asDto(computer);
Computer computer = computerMapper.asEntity(computerDTO);
...
  • Computer
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@ToString
public class Computer {
    private String name;
    private String author;
    private String version;
    private ComputerAws computerAws;

}
  • ComputerDTO
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@ToString
public class ComputerDTO {
    private String name;
    private String author;
    private String version;

}

GenericMapper 구현

1. GenericMapper

public interface GenericMapper <D, E> {
    D asDto(E e);
    E asEntity(D d);
}

2. mapper Interface 추가

@Mapper(componentModel = "spring")
public interface ComputerMapper extends GenericMapper<ComputerDTO, Computer> {

}

3. 호출

private final ComputerMapper computerMapper;
...
ComputerDTO computerDTO = computerMapper.asDto(computer);
Computer computer = computerMapper.asEntity(computerDTO);
...

DTO안에 객체가 있는 경우

1. Entity와 DTO 안에 객체를 호출하는 방식

  • ComputerAwsWithComputerDTO
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class ComputerAwsWithComputerDTO extends ComputerDTO{
    private ComputerAwsDTO computerAws;
}

2. Mapper 구현

@Mapper(componentModel = "spring")
public interface ComputerMapper extends GenericMapper<ComputerDTO, Computer> {
    ComputerAwsWithComputerDTO computerToComputerAwsWithComputerDTO(Computer computer);
}

이런 식으로 구현이 가능합니다. 여기서 이름은 사실 중요하지 않습니다. 

 

확인 방법

1. clean > build

해당 위치에 파일이 생성됩니다.

 

옵션

  • @Mapper
  • componentModel : spring으로 줬기 때문에 생성되는 Impl은 스프링의 싱글톤 빈으로 관리된다(@Component 붙음)
@Mapper(componentModel = "spring")
  • unmappedTargetPolicy : Target의 필드가 매핑되지 않을 때 정책
@Mapper(componentModel = "spring"
        , unmappedTargetPolicy = ReportingPolicy.IGNORE)
  • unmappedSourcePolicy : Source의 필드가 Target에 매핑되지 않을 때 정책
@Mapper(componentModel = "spring"
	, unmappedTargetPolicy = ReportingPolicy.IGNORE
	, unmappedSourcePolicy = ReportingPolicy.IGNORE)
  • typeConversionPolicy : 타입 변환 시 유실이 발생할 수 있을 때 정책
@Mapper(componentModel = "spring"
	, unmappedTargetPolicy = ReportingPolicy.IGNORE
	, unmappedSourcePolicy = ReportingPolicy.IGNORE
	, typeConversionPolicy = ReportingPolicy.IGNORE)
  1. ReportingPolicy.IGNORE : 무시 정책
  2. ReportingPolicy.WARN : 경고 정책
  3. ReportingPolicy.ERROR :  에러 정책
  • @IterableMapping(qualifiedByName = "asDTO")

해당 리스트 작업시 어떤것을 가져다 쓸지 모를때 지정해주면 됩니다.

보통 밑에와 같은 예제에서는

public interface GenericMapper <D, E> {
    @Named(value = "asDto")
    D asDto(E e);

    @IterableMapping(qualifiedByName = "asDTO")
    List<D> asDTOList(List<E> entityList);
}

실행시 아래와 같은 오류가 나옵니다.

더보기

error: Qualifier error. No method found annotated with @Named#value: [ asDTO ]. See https://mapstruct.org/faq/#qualifier for more info. Occured at 'List asDTOList(List entityList)' in 'GenericMapper'.

이유는 asDto 와 asDTO 처럼 이름이 안 맞기때문에 나온겁니다. 즉 어떤걸로 매핑할건데 지정된 @Named 로 지정해서 부르는 방식입니다.

  • @Mapping
  • source : 들어가는 Param에 어떤 값을 할지  
  • target : 나오는값을 어떤 값을 할지
@Mapping(source = "computerAws", target = "computerAws")
ComputerAwsWithComputerDTO computerToComputerAwsWithComputerDTO(Computer computer);

// Source
public class Computer {
    private String name;
    private String author;
    private String version;
    private ComputerAws computerAws;
}

// target
public class ComputerAwsWithComputerDTO extends ComputerDTO{
    private ComputerAwsDTO computerAws;
}

주의사항

1. Lombok 사용 시 주의 사항

dependencies에서 lombok에 위치는 mapstruct 보다 위에 있어야 됩니다. 

그렇지 않으면 mapperImpl이 만들어질 때 Lombok 전에 만들어지기 때문에 오류가 발생합니다.

2. @Setter 보다는 @Builder or @AllArgsConstructor를 사용하여 생성자 패턴으로 만들어주는 게 좋습니다.

요즘은 Setter를 지양하는 추세이기 때문입니다. 

 

JitPack 이란

Java 오픈 라이브러리 무료로 생성시켜주며, 나만의 유틸성 라이브러리를 사용이 가능하게 해 줍니다.

사이트 : https://jitpack.io/

 

JitPack | Publish JVM and Android libraries

JitPack makes it easy to release your Java or Android library. Publish straight from GitHub or Bitbucket.

jitpack.io

 

사용법

Spring + Gradle7.* 로 예제가 작성되었습니다.

 

1. 일단 Git부터 가입하자 JitPack은 Github에 올라간 소스를 받을 수 있게 됩니다.
Git 가입법은 생략... ( https://github.com/ )

2. git으로 소스 생성 자신이 원하는 모양에 프로젝트를 생성합니다.

3. Gradle 설정

build.gradle

// 플러그인 설정
plugins {
    id 'java'
    id 'org.springframework.boot' version '2.7.8'
    id 'io.spring.dependency-management' version '1.0.15.RELEASE'
    id 'maven-publish'
}

// 만들 빌드 정보
group = 'io.com.open'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8

// config 설정으로 저는 compileOnly, runtimeClasspath만 설정
configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
    runtimeClasspath {
        extendsFrom
        implementation
    }
}

// repositories는 mavenCentral만 일단
repositories {
    mavenCentral()
}

// 예제는 validation 관련이며, 실제 사용 가능하게 꾸미고 싶어서 두가지만 의존성을 받았습니다.
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-validation'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

tasks.named('test') {
    useJUnitPlatform()
}

// bootJar는 생성 안하게 설정
tasks.named("bootJar") {
    enabled = false
}

// plan.jar에 뒤에 plan제거
tasks.named("jar") {
    archiveClassifier = ''
}

// 라이브러리에 의존성 주입받은것을 같이 빌드되게 하는 옵션
jar {
    manifest {
        attributes 'java-open-valid': 'io.com.open'
    }
    from {
        configurations.runtimeClasspath.collect {
            it.isDirectory() ? it : zipTree(it)
        }
    }
    duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}

// publishin 설정
publishing {
    publications {
        maven(MavenPublication) {
            groupId = group
            artifactId = rootProject.name
            version = version

            artifact("build/libs/$rootProject.name-$version" + ".jar") {
                extension 'jar'
            }
        }
    }
}

gradle 부분은 저도 그리 강한 편이 아니라 하면서 제가 느낀 부분에 주석을 달았습니다.

4. Jitpack 접속하여 연결

https://jitpack.io/

 

JitPack | Publish JVM and Android libraries

JitPack makes it easy to release your Java or Android library. Publish straight from GitHub or Bitbucket.

jitpack.io

Github 주소를 jitpack에 붙여두시면 됩니다.

5. 연결된 모습

저 같은 경우 Release로 만들어서 버전링을 했습니다. 하고 싶지 않으면 안 하셔도 되고요.

  • Status > Get it 버튼을 누르면 Jitpack에서 build파일을 자기들 스토리지에 올리는 행위를 합니다. 만약 여기서 실패가 나면 Log파일에 아이콘이 빨간 거 보이시죠 저런 모양이 떠요.ㅜㅜㅜ 성공하셨다면 일단 축하드려요.

6. Get it을 누르면 밑에 이동이 되는데 아래와 같이 적용하고자 하는 코드 부분에 추가해 주시면 됩니다.

7. 적용

내가 만든 라이브러리가 잘 들어온 게 확인이 됩니다.

 

이제 잘 사용만 하면 끝~~

 

작업 중 이슈 사항

1. JitPack 빌드 중 터지는 이슈

터지게 되면 Log부분을 클릭하여 들어가서 확인이 가능합니다.

원인은 빌드 파일을 제대로 못 찾는 이슈 plan.jar를 생성 안 하게 하려다가 둘 다 제대로 생성 안되게 하는 이슈로 발생

// bootJar는 생성 안하게 설정
tasks.named("bootJar") {
    enabled = false
}

// plan.jar에 뒤에 plan제거
tasks.named("jar") {
    archiveClassifier = ''
}

2. build jar가 라이브러리처럼 못 쓰게 말린 경우

아래와 같이 jar를 풀어봤을 때 BOO-INF로 말리면 잘못된 경우입니다. 위에 설명한 설정되로 하면 이렇게 말리지는 않습니다.

Tip. Git에 올렸다가 배포했다 확인하기 힘든 관계로 build를 하면 build/libs에 파일이 잘 생성되는지 확인 후 

풀어서 내부를 보고 싶으면

jar xvf [jar파일명]

터미널에서 입력하시면 됩니다.

 

마무리

요즘 MSA로 많이 가는 추세입니다. 이렇게 라이브러리로 만들어서 자신들에 서버들에 라이브러리만 가져다 쓰면 동작하게 할 수 있는 장점이 있는 것 같아요.

다만 여기서 고민해 봐야 되는 문제는 라이브러리 자체에 의존성을 어디까지 주어야 되냐에서 저는 최대한 필요한 것만 어쩔 수 없이 의존성을 가져다가 같이 말아서 라이브러리화를 해야 된다고 생각합니다. 개발에 속도를 향상하기 위한 Lombok이나 이런 거는 최대한 지양하고 꼭 필요한 라이브러리들만 가지고 만들어가는 게 라이브러리 적용 시 이슈를 줄이는 길이지 않나 생각합니다.

SQS Listener 구축

  1. 의존성 추가
implementation 'org.springframework.cloud:spring-cloud-starter-aws-messaging'

   2. SQS 주소 Properties 생성

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import lombok.Getter;

@Component
@Getter
@ConfigurationProperties("sqs")
public class AwsSqsNameProperties {
     private String sqsExample;
}

 

  • application.yml
sqs:
 sqsExample: [SQS 주소]
 
cloud:
 aws:
  region:
   static: 리전 정보(ex: ap-northeast-2)
  stack:
   auto: false
  credentials:
   access-key: IAM 에서 발급받은 엑세스키
   secret-key: IAM에서 발급받은 시크릿키

SQS 주소는 빨간 부분을 넣어주시면 됩니다.

  3. SQSConfig 작성

@EnableSqs
@Configuration
@EnableConfigurationProperties(value = {AwsSqsNameProperties.class})
@RequiredArgsConstructor
public class SQSConfig {


    public static final ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.json()
        .simpleDateFormat("yyyy-MM-dd HH:mm:ss")
        .timeZone("Asia/Seoul")
        .build();

    public static final MappingJackson2MessageConverter JSON_MESSAGE_CONVERTER;

    static {
        JSON_MESSAGE_CONVERTER = new MappingJackson2MessageConverter();
        JSON_MESSAGE_CONVERTER.setSerializedPayloadClass(String.class);
        JSON_MESSAGE_CONVERTER.setStrictContentTypeMatch(false);
        JSON_MESSAGE_CONVERTER.setObjectMapper(objectMapper);
    }

    // Resolver 연결 특정 객체로 변경할려고 할때 작성
    @Bean
    public QueueMessageHandlerFactory queueMessageHandlerFactory() {
        QueueMessageHandlerFactory factory = new QueueMessageHandlerFactory();
        factory.setArgumentResolvers(unmodifiableList(asList(
            new MessageResolver(
                new NotificationRequestConverter(JSON_MESSAGE_CONVERTER))
        )));
        return factory;
    }

    // SQS Listener 빈등록
    @Bean
    public SimpleMessageListenerContainerFactory simpleMessageListenerContainerFactory(
        AmazonSQSAsync amazonSQSAsync, AsyncTaskExecutor taskExecutor) {
        SimpleMessageListenerContainerFactory factory = new SimpleMessageListenerContainerFactory();
        factory.setAmazonSqs(amazonSQSAsync);
        factory.setMaxNumberOfMessages(10);
        factory.setAutoStartup(true);
        factory.setBackOffTime(1000L);
        factory.setTaskExecutor(taskExecutor);
        return factory;
    }


    // 리젼 정의
    @Bean
    public RegionProvider regionProvider() {
        return new StaticRegionProvider(Regions.AP_NORTHEAST_2.getName());
    }
}

   4. SqsListener 정의

@Slf4j
@Component
@RequiredArgsConstructor
public class SqsListener {

    @SqsListener(value = "${sqs.sqsExample}", deletionPolicy = SqsMessageDeletionPolicy.ON_SUCCESS)
    @Override
    public void receiveEvent(MessageDTO message) throws IOException {
       log.info("", message);
    }
    
}

@SqsListener 만 지정해 주면 해당 메서드에 Queue가 읽어 옵니다.

  • Resolver를 쓰지 않고 String으로 설정 시 String 값으로 넘어오는 것을 확인할 수 있습니다.
@Slf4j
@Component
@RequiredArgsConstructor
public class SqsListener {

    @SqsListener(value = "${sqs.sqsExample}", deletionPolicy = SqsMessageDeletionPolicy.ON_SUCCESS)
    @Override
    public void receiveEvent(String message) throws IOException {
       log.info("", message);
    }
    
}
  • deletionPolicy는 SQS Listener에 4가지 삭제 정책(SqsMessageDeletionPolicy)을 설정합니다.
  • ALWAYS : 리스너 메서드에 의한 메시지 처리 중 성공( 예외 발생 없음) 또는 실패 (예외 발생) 시 항상 메시지 삭제
  • NEVER : 메시지를 자동으로 삭제하지 않습니다. 수신 확인 ( Acknowledgment )로 명시적으로 삭제가 가능
  • NO_REDRIVE : Redrive pollcy(DeadLetterQueue)가 정의되지 않은 경우 메시지를 삭제
  • ON_SUCCESS : 리스터 메서드에 의해 성공적으로 실행되면 메시지를 삭제합니다. 

  5. Resolve 설정

import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver;
import org.springframework.messaging.converter.MessageConversionException;
import org.springframework.messaging.converter.MessageConverter;
import org.springframework.messaging.Message;

@Slf4j
@AllArgsConstructor
public class MessageResolver implements HandlerMethodArgumentResolver {
    private final MessageConverter messageConverter;

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return isAssignable(MessageDTO.class, parameter.getParameterType());
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, Message<?> message) throws Exception {
        Object convertMessage = messageConverter.fromMessage(message, MessageDTO.class);
        return ((NotificationRequestConverter.NotificationRequest) convertMessage).getMessage();
      
    }
}

위에 설정을 하면 보통 연결이 정상적으로 되는 것을 확인 가능하세요.

 

작업 중 오류 발생 상황

1. SQS 읽지 못했다는 오류 확인

WARN o.s.c.a.m.l.SimpleMessageListenerContainer@queueAttributes(329) - Ignoring queue with name '[SQS명]': The queue does not exist.; nested exception is com.amazonaws.services.sqs.model.QueueDoesNotExistException: The specified queue does not exist for this wsdl version. (Service: AmazonSQS; Status Code: 400; Error Code: AWS.SimpleQueueService.NonExistentQueue; Request ID: 86246416-3974-5cd5-93ff-389aa43357b9; Proxy: null)

해당 오류가 발생하는 원인

  • SQS 권한을 막아두었을 때 발생
  • SQS 주소가 이상이 있는지 확인

2. application.yml에 IAM 키가 Open 되는 이슈

보안상 yml에 보안키가 들어있게 되면 해킹에 위험이 있어서 해당 문제를 해결이 필요

1. AwsCredentialConfiguration 추가

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.core.env.Environment;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
import software.amazon.awssdk.auth.credentials.AwsCredentialsProviderChain;
import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
import software.amazon.awssdk.utils.StringUtils;

@Configuration
public class AwsCredentialConfiguration {

    @Bean
    public AwsCredentialsProvider awsCredentialsProviderOnLocal(Environment environment) {
        // 해당 방식으로 값을 불러옵니다.
        String accessKeyId = getEnviromentValue(environment, "AWS_ACCESS_KEY_ID", "AWS_ACCESS_KEY_ID");
        String secretAccessKey = getEnviromentValue(environment, "AWS_SECRET_ACCESS_KEY", "AWS_SECRET_ACCESS_KEY");
        // 해당 부분을 다듬어서 보내주기만 하면됩니다.
        return AwsCredentialsProviderChain.builder().credentialsProviders(() -> AwsBasicCredentials.create(accessKeyId, secretAccessKey)).build();
    }

}

2. SQSConfig에 AmazonSQSAsync 추가

@EnableSqs
@Configuration
@EnableConfigurationProperties(value = {AwsSqsNameProperties.class})
@RequiredArgsConstructor
public class SQSConfig {
   ...
   
 // AmazonSQSAsync 에 정보 추가
 @Primary
 @Bean
 public AmazonSQSAsync amazonSQSAws(AwsCredentialsProvider awsCredentialsProvider) {
  BasicAWSCredentials awsCreds = new BasicAWSCredentials(awsCredentialsProvider.resolveCredentials().accessKeyId(), awsCredentialsProvider.resolveCredentials().secretAccessKey());
  return AmazonSQSAsyncClientBuilder.standard()
            .withRegion(String.valueOf(Region.AP_NORTHEAST_2))
            .withCredentials(new AWSStaticCredentialsProvider(awsCreds))
            .build();
 }
   
   ...
}

  3. application.yml 제거

// 프로젝트 배포시 기본으로 CloudFormation 구성을 시작하기 때문에 설정한
// CloudFormation이 없으면 프로젝트 실행이 되지 않음. 해당 기능을 사용하지 않도록 false로 설정.
cloud:
 aws:
  stack:
   auto: false

해당 부분은 위에 이슈로 인해 두고 나머지 설정 부분은 SQSConfig에 추가합니다.

  4. 실행

jar 실행시 

AWS_ACCESS_KEY_ID=[Access key]; AWS_SECRET_ACCESS_KEY=[Secret key]

3. SQS를 읽어는 지나 실제 Listener까지 오지 않는 현상

  • 문제상황

SimpleMessageListenerContainer.java

첫 번째 블락까지 정상적으로 SQS가 전부 읽어집니다. 그러나 두 번째 블락에서 멈추는 현상 발생 그 후 일정 시간이 지나면 SQS가 닫히는 현상

  • 원인

AsynThreadPool을 이용하는 해당 프로젝트에서 따로 설정을 안 해두니 ThreadPool이 0으로 잡히면서 await() 부분에서 스레드가 풀려서 진행될 때까지 무한정 기다리는 현상

  • 해결 방법

AsynThreadPool 추가

@EnableSqs
@Configuration
@EnableConfigurationProperties(value = {AwsSqsNameProperties.class})
@RequiredArgsConstructor
public class SQSConfig {

  ...
  
 // ThreadPool 추가  
 @Bean(name = "sqsThreadPoolTaskScheduler")
 public Executor sqsThreadPoolTaskScheduler() {
    ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
    taskExecutor.setCorePoolSize(3);
    taskExecutor.setMaxPoolSize(10);
    taskExecutor.setQueueCapacity(10);
    taskExecutor.setThreadNamePrefix("sqsThreadPool-");
    taskExecutor.initialize();
    return taskExecutor;
 }
 
 @Bean
 public SimpleMessageListenerContainerFactory simpleMessageListenerContainerFactory(
        AmazonSQSAsync amazonSQSAsync,
        @Qualifier("sqsThreadPoolTaskScheduler") Executor taskExecutor) {
        SimpleMessageListenerContainerFactory factory = new SimpleMessageListenerContainerFactory();
        factory.setAmazonSqs(amazonSQSAsync);
        factory.setMaxNumberOfMessages(10);
        factory.setAutoStartup(true);
        factory.setBackOffTime(1000L);
        // 이 부분에 해당 taskExecutor 등록
        factory.setTaskExecutor((AsyncTaskExecutor) taskExecutor);
        return factory;
 }
  
  ...
  
}

'Java > Spring' 카테고리의 다른 글

[Spring] Rest API 통신 방법 ( RestTemplate vs FeignClient vs WebClient )  (0) 2023.05.05
[Spring] AOP  (0) 2023.03.23
[Spring] MapStruct 적용 방법  (0) 2023.03.21
[Java] Spring Validation Library  (1) 2023.03.12
[Java] Faker Library  (2) 2023.03.11

템플릿 메소드 패턴이란?

동일한 작업을 할 때 내부에 전체적인 구조를 바꾸지 않고 특정 템플릿 메소드로 지정된 부분만 바꾸어서 진행이 가능한 구조

 

예제

커피 머신기를 이용해서 아메리카노와 라때를 만드는 예제입니다.

AbstractCoffeeMachine을 보시면 run()을 열어두고 나머지 add(), compression(), brewing(), mix()를 추상 메소드로 만들어서 해당 부분만 정의를 하면 커피를 뽑는 자체에 행위는 유지하게 되는 패턴입니다.

  • AbstractCoffeeMachine
package com.code.test.design.template;

public abstract class AbstractCoffeeMachine {
    // 각 로직에서 실제 행해지는 부분들 정의
    protected abstract void add();
    protected abstract void compression();
    protected abstract void brewing();
    protected abstract void mix();

    // 실제 로직
    public void run() {
        // 1. Coffee 원두 추가
        add();
        // 2. Coffee 압축
        compression();
        // 3. Coffee 내리기
        brewing();
        // 4. Coffee 다른 물품과 섞기
        mix();
        // 5. 전달
        System.out.println("전달");
    }
}
  • AmericanoCoffeeMachine
package com.code.test.design.template;

public class AmericanoCoffeeMachine extends AbstractCoffeeMachine {
    @Override
    protected void add() {
        System.out.println("에티오피아 커피콩을 더 한다.");
    }

    @Override
    protected void compression() {
        System.out.println("커피를 세번 압축한다.");
    }

    @Override
    protected void brewing() {
        System.out.println("원샷으로 내린다");
    }

    @Override
    protected void mix() {
        System.out.println("물 한잔을 섞는다.");
    }
}

로직을 보면 run()에서는 정해진 부분들에 순서와 공통된 로직이 진행되고, 실제 구현을 해야되는 부분에서 정의를 해나가는 방식입니다.

그럼 결과적으로 공통된 로직과 규칙을 정해지고, 내부적으로 각 특징에 맞게 변경만 해도 되는 패턴

활용 사례

예를 들어 카카오톡, 페이스북, 네이버톡톡과 같은 외부 채팅과 연동하는 로직을 만든다고 하자.

전체적인 로직

1. 각 채널에 사용자 키값

2. 키값을 이용한 채팅방을 만들거나 찾는다.

3. 메시지를 보낸다.

위와 같이 3가지 로직은 똑같지만 각 작업마다 카카오톡, 페이스북, 네이버톡톡에 맞게 로직을 수행하는 작업이 필요하다.

이런 상황일 때 사용이 가능할 것 같습니다.

Validation 이란

스프링에서는 Validator 인터페이스를 지원하여 애플리케이션에서 사용하는 객체를 검증할 수 있는 기능을 제공한다. 이 Validator 인터페이스는 어떤 특정 계층에 사용하는 기능이 아닌 모든 계층에서 사용할 수 있다.

 

기존에 Controller나 Service단에서 넘어오는 Param들을 체크하기 위한 로직이 들어가면서 복잡성과 가독성이 떯어짐에 따라 나온 라이브러리 같습니다. 받는 DTO Param 안에 어노테이션으로 정의하여 해당 Param이 가져야 하는 규칙들을 DTO 안에 둘 수 있어서 이로 인해 단일 책임 원칙 (Single responsibility principle)을 해치지 않을 수 있는 장점이 있는 것 같습니다.

 

라이브러리 등록

Gladle

implementation 'org.springframework.boot:spring-boot-starter-validation'

https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-validation

 

예제

들어가기 전에 Validation은 처리하는 방법은 크게 두가지 방식이 있습니다.

1. annotaion을 이용한 지정된 Valid 처리

2. 커스텀 annotation을 생성하는 방식이 있습니다.

일단 들어가기 전에 위에 두가지 방식을 쓰기 전에 선언 부터할게요.

1. Controller 선언

아래와 같은 컨트롤러 단에 사용할 메서드에 사용할 DTO에 @Valid라고 선언을 해야만 해당 DTO에 정의된 Validation을 체크할 수 있습니다.

회원가입 컨트롤러 메서드에 Valid 선언

참고 

해당 import값입니다.

import javax.validation.*;

1-1. annotation을 이용한 지정된 Valid 처리

아래와 같이 처리하고자 하는 변수 위에 Validation에서 지원해 주는 @NotBlank , @NotNull 등 을 지정하고 반환 message들을 등록하여 사용하는 방식입니다. 

여러 가지 어노테이션을 중첩으로 사용하셔도 됩니다.

1-2. 커스텀 annotation을 생성

예제로는 전화번호 유효성 체크하는 예제로 만들어 보겠습니다.

1-2-1. 커스텀 annotaion 선언

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
@Retention(RUNTIME)
@Constraint(validatedBy = PhoneNumberValidator.class)
@Documented
public @interface PhoneNumber {
    String message() default "{javax.validation.constraints.PhoneNumber.message}";

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

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

    @Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
    @Retention(RUNTIME)
    @Documented
    @interface List{
        PhoneNumber[] value();
    }
}

위에서 선언된 message , groups, payload와 내부에 있는 List는 필수 값이에요. 꼭 작성해 주셔야 됩니다.

@Constraint에 해당 어노테이션이 체크해야 될 유효성 체크로직이 들어있는 클래스를 선업니다.

1-2-2. 유효성 체크 클래스 생성

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class PhoneNumberValidator implements ConstraintValidator<PhoneNumber, String> {

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if (phoneNumberValid(value)) {
            return true;
        }
        return false;
    }

    private static boolean phoneNumberValid(String value) {
        String phone = toPhoneNumber(value.replaceAll("-", ""));
        if (phone.matches("^\\d{3}-\\d{3,4}-\\d{4}$")) {
            return true;
        } else if (phone.matches("^\\d{2,3}-\\d{3,4}-\\d{4}$")) {
            return true;
        } else if (phone.matches("^\\d{4}-\\d{4}$")) {
            return true;
        } else {
            return false;
        }
    }

    private static String toPhoneNumber(String value) {
        if (value.startsWith("02")) {
            return value.replaceAll("([0-9]{2})([0-9]+)([0-9]{4})", "$1-$2-$3");
        } else if (value.length() == 8) {
            return value.replaceAll("([0-9]{4})([0-9]{4})", "$1-$2");
        } else {
            return value.replaceAll("([0-9]{3})([0-9]+)([0-9]{4})", "$1-$2-$3");
        }
    }
}

위에 ConstraintValidator를 상속받아서 작성해 주시면 됩니다. 여기서 isValid를 override를 해서 true를 내보내면 유효성에 문제가 없는 것이고, false는 문제 발생 시 내보내게 됩니다.

1-2-3. 커스텀 Annotaion을 선언

/**
 * 전화번호
 * */
@NotBlank(message = "phoneNumber cannot be empty or null.")
@PhoneNumber(message = "The format of the phone number message is strange.")
private String phoneNumber;

1-1에서 사용하는 방식과 같이 선언만 해서 사용하시면 됩니다.


참고로 혹시 커스텀 어노테이션에 사용자가 지정한 사이즈라 던가 특정값을 받고 싶다면 어노테이션에 추가해서 받을 수도 있습니다.

아래에 짧은 예제입니다.

- 어노테이션 선언

package io.com.open.javaopenvalid.support;

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
@Retention(RUNTIME)
@Constraint(validatedBy = PasswordValidator.class)
@Documented
public @interface Password {
    String message() default "{javax.validation.constraints.Password.message}";

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

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

    /**
     * 대문자 필수 포함 여부
     * */
    boolean capitalLetter() default true;

    /**
     * 소문자 필수 포함 여부
     * */
    boolean smallLetter() default true;

    /**
     * 특수 문자 필수 포함 여부
     * */
    boolean specialSymbol() default true;

    /**
     * 숫자 포함여부
     * */
    boolean number() default true;

    @Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
    @Retention(RUNTIME)
    @Documented
    @interface List{
        Password[] value();
    }
}

- 선언한 어노테이션에 값을 빼서 쓸 때

package io.com.open.javaopenvalid.support;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class PasswordValidator implements ConstraintValidator<Password, String> {

    private boolean capitalLetter = true;
    private boolean smallLetter = true;
    private boolean specialSymbol = true;
    private boolean number = true;

    private int mixedNum = 0;

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if (passwordCheck(value)) {
            return true;
        }
        return false;
    }

    @Override
    public void initialize(Password constraintAnnotation) {
        ConstraintValidator.super.initialize(constraintAnnotation);
        this.capitalLetter = constraintAnnotation.capitalLetter();
        mixedNum += this.capitalLetter ? 1 : 0;
        this.smallLetter = constraintAnnotation.smallLetter();
        mixedNum += this.smallLetter ? 1 : 0;
        this.specialSymbol = constraintAnnotation.specialSymbol();
        mixedNum += this.specialSymbol ? 1 : 0;
        this.number = constraintAnnotation.number();
        mixedNum += this.number ? 1 : 0;
    }
 
    ...
 }

initialize를 override를 받아서 뽑아 쓰시면 됩니다.

- 선언 부분 

예제는 대문자는 필수로 포함 안 해도 된다는 선언입니다.

@Password(
        message = "The format of the password message is strange.",
        capitalLetter = false
)
private String password;

Spring이 지원해 주는 기본 Validation 

참고. 기본적으로 message는 다 지원합니다. 유효성 체크 후 예외 안내 메시지 용도

  • 문자 체크 : 해당 String만 체크가 정상적으로 되며, 내부적으로는 null이 아니고 빈값이 아닌 경우 체크
@NotBlank(message = "name cannot be empty or null.")
  • Null 체크 : Null만 체크가 필요한 경우 사용
@NotNull(message = "age cannot be null.")
  • 숫자 검증
//음수 검증 0 포함
@NegativeOrZero(message = "age negative number including zero")
//음수 검증
@Negative(message = "age negative number including zero")
// 양수 검증 0 포함
@PositiveOrZero(message = "age positive number including zero")
// 양수 검증
@Positive(message = "age positive number not including zero")
  • 크기 체크 : 길이가 특정 값보다 크고 작은지 체크
@Size(
    min = 8, max = 16,
    message = "Password must be at least 8 digits"
)

       ⚬ min : 최소 사이즈

       ⚬ max : 최대 사이즈

  • 이메일 체크 : 이메일 패턴이 맞는지 체크
@Email(message = "The format of the email message is strange."
        , regexp = "^[a-zA-Z0-9+-\\_.]+@[a-zA-Z0-9-]+\\.[io]+$"
)

      ⚬ regexp : 체크할 정규표현식 등록

  • 날짜 검증
// 오늘 보다 이후에 들어온 날짜인지 체크
@Negative(message = "오늘 날짜보다 미래가 아닙니다.")
// 오늘포함 이후에 들어온 날짜인지 체크
@NegativeOrZero(message = "오늘 날짜보다 포함 미래가 아닙니다.")
// 오늘포함 이전에 들어온 날짜인지 체크
@PositiveOrZero(message = "오늘 날짜 포함 과거가 아닙니다.")
// 오늘보다 이전에 들어온 날짜인지 체크
@Past(message = "오늘 날짜보다 과거가 아닙니다.")

 

Junit 테스트 방법

@SpringBootTest
class ValidRequestTest {

    // 해당 부분들을 호출 
    private static ValidatorFactory factory;
    private static Validator validator;

    @BeforeAll
    public static void init() {
        factory = Validation.buildDefaultValidatorFactory();
        validator = factory.getValidator();
    }

    @Test
    @DisplayName("name 빈문자열 전송 시 에러 발생")
    void 빈문자열_유효성_실패_테스트() {
        // given
        ValidRequest validRequest = ValidRequest.of("홍길동", 11, "system@gmail.com", true, LocalDateTime.now().minusDays(1L), "01012341234", "asdfASDF1234!@$#");
        validRequest.setName("");

        // when
        Set<ConstraintViolation<ValidRequest>> violations = validator.validate(validRequest);

        // then
        assertThat(violations).isNotEmpty();
        violations.forEach(error -> {
            // 예외 상황들이 이 곳으로 들어옵니다.
            assertThat(error.getMessage()).isEqualTo("name cannot be empty or null.");
        });
    }
 
 	...
 }

+ Recent posts