진행 중인 프로젝트 서버가 내일 더 큰 서버로 확장 이전되며 S3 관련 설정을 바꿔주는 작업이 필요해졌다!

내일 관련 회의에 참여해 작업을 진행하기 전에 미리 S3에 관해 정리를 해가면 좋을 것 같아 공부할 겸 포스팅을 하게됐다.

 

Amazon Simple Storage Service(Amazon S3)란?

AWS에서 제공하는 파일 서버의 역할을 하는 객체 스토리지 서비스

일반적인 파일 서버는 트래픽이 증가함에 따라 장비를 증설하는 작업이 필요한데 S3가 이를 대행해줘 트래픽에 따른 시스템적인 문제를 해결해준다.

Amazon Simple Storage Service(Amazon S3) 특징

-원하는 양의 데이터를 저장, 검색, 삭제 가능

-내구성과 확장성이 뛰어나며 사용한 스토리지 용량만큼 요금이 청구

-저장할 수 있는 파일 수의 제한 x

-최소 1바이트에서 최대 5TB의 데이터를 저장하고 서비스 제공 가능

-파일에 인증을 붙여서 무단으로 엑세스 하지 못하도록 가능

-정보의 중요도에 따라서 보호 수준을 차등화 가능

-버킷(bucket)과 키(key)로 구성

Amazon Simple Storage Service(Amazon S3) 사용 용어

객체

저장된 데이터(파일) 하나 하나를 객체라고 명명

버킷

연관된 객체들을 그룹핑한 최상위 디렉토리

버킷 단위로 지역 지정 가능

버킷에 포함된 모든 객체에 대해 일괄적으로 인증 및 접속 제한 가능

버킷 내 객체의 고유한 식별자

버킷 내 모든 객체는 고유한 키를 가짐

버전 관리

저장된 객체들의 변화를 저장

RSS(Reduced Redundancy Storage)

객체에 비해 데이터가 손실될 확률이 높은 형태의 저장 방식

대신 가격 저렴

복원이 가능한 데이터를 저장하는데 적합

 

Amazon Simple Storage Service(Amazon S3) 간략한 사용 흐름

AWS S3 페이지에서 버킷 생성

버킷 생성시 원하는 지역 설정

버킷 퍼블릭 액세스 설정

버킷에 객체(파일) 업로드

업로드시 권한 관련 설정 진행

업로드 후 다운로드 가능

ec2에서 s3 접근 위해서는 IAM 역할 설정 필요

 

참고 사이트

https://aws.amazon.com/ko/s3/

https://seoyeonhwng.medium.com/aws-s3%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80-b0da502b0504

https://dev.classmethod.jp/articles/for-beginner-s3-explanation/

https://vlee.kr/4765

 

로컬 커맨드 창에서 스프링 프로젝트 빌드 시 발생한 오류다.

현재 진행 중인 프로젝트의 jdk가 8버전인데 자바 버전과 롬복 버전이 호환되지 않아 발생하는 문제라고한다.

이전에는 빌드가 잘 됐었는데 이틀 전에 깃 공부를 하면서 로컬 커맨드 창에서 새로 프로그램을 깔고 업데이트 작업을 좀 했는데 그때 롬복 버전이 업그레이드 되면서 발생한 문제 같다.

해결 방법은 아래와 같다.

기존 build.gradle

dependencies{
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
}

수정한 build.gradle

dependencies{
    compileOnly 'org.projectlombok:lombok:1.18.20'
    annotationProcessor 'org.projectlombok:lombok:1.18.20'
}

버전을 직접 명시해줬다.

 

참고 사이트

https://jin2rang.tistory.com/entry/javalangIllegalAccessError-class-lombokjavacaptLombokProcessor-in-unnamed-module-0x2fbb01ba-cannot-access-class-comsuntoolsjavacprocessingJavacProcessingEnvironment-in-module-jdkcompiler

 

개발 환경

스프링 부트  2.4.0 + ubuntu 20.04+ nginx1.18.0



진행 중인 프로젝트에서 기존 로그인 로직 수정 요청이 들어오며 추가로 다날 SMS 본인인증 처리가 필요해졌다.

서브 도메인을 연결하여 SMS 인증 페이지를 띄워 웹뷰를 통해 클라이언트 분들이 사용할 수 있도록 해야했고 보안을 위해 access 토큰을 서버사이드에서 받아 클라이언트로 보낼 수 있도록 관련 API를 개발했다.

다날 아임포트사에서 코드 예시와 함께 가이드 라인을 제공해주고 있는데 다 node.js다..깃헙 레포를 통해 java 사용자들도 사용할 수 있도록 maven 플러그인을 제공해주고 있으니 참고하면 좋을 것 같다. 필자는 gradle을 사용하고 있기도하고 제공해주는 모듈에서 안쓰는 기능들이 많아 간단하게 코드로 직접 만들어서 개발했다.
https://github.com/iamport/iamport-rest-client-java

 

GitHub - iamport/iamport-rest-client-java: JAVA사용자를 위한 아임포트 REST API 연동 모듈입니다

JAVA사용자를 위한 아임포트 REST API 연동 모듈입니다. Contribute to iamport/iamport-rest-client-java development by creating an account on GitHub.

github.com

 

<서브 도메인으로 본인인증 페이지 띄우기>

다날 SMS 본인 인증 페이지를 띄울 수 있는 html 파일의 코드는 아래와 같다.

<!DOCTYPE html>
<html>
<head>
<!-- jQuery -->
<script type="text/javascript" src="https://code.jquery.com/jquery-1.12.4.min.js" ></script>
<!-- iamport.payment.js -->
<script type="text/javascript" src="https://cdn.iamport.kr/js/iamport.payment-1.1.4.js"></script>
</head>

<body>
<script type="text/javascript">
IMP.init('가맹점식별코드를입력해주세요.');
IMP.certification({
merchant_uid : 'merchant_' + new Date().getTime() //본인인증과 연관된 가맹점 내부 주문번호가 있다면 넘겨주세요
}, function(rsp) {
if ( rsp.success ) {
    // 인증성공
    console.log(rsp.imp_uid);
    console.log(rsp.merchant_uid);

$.ajax({
    type : 'POST',
    url : '/certifications/confirm',
    dataType : 'json',
    data : {
    imp_uid : rsp.imp_uid
    }
    }).done(function(rsp) {
    // 이후 Business Logic 처리하시면 됩니다.
    });

} else {
    // 인증취소 또는 인증실패
    var msg = '인증에 실패하였습니다.';
    msg += '에러내용 : ' + rsp.error_msg;

    alert(msg);
    }
});

</script>
</body>
</html>

가맹점 식별 코드를 적어 해당 html 파일을 만들었다면 운영 중인 서버의 원하는 위치에 파일을 위치시킨다.


다음으로 사용 중인 도메인에 서브 도메인을 하나 새로 파자!

필자가 참여 중인 프로젝트는 가비아라는 도메인 판매 사이트를 이용하고 있어 해당 사이트에서 서브 도메인을 추가해줬다.

각자의 상황에 맞는 도메인 판매 사이트에서 서브 도메인 관련 설정을 맞췄다면 ec2 nginx의 설정을 변경해 서브 도메인을 연결해줘야한다.

위 내용은 이전 포스팅에서 다룬 적 있어 해당 포스팅 링크로 대체!

https://cofls6581.tistory.com/64

 

[서브 도메인] 서브도메인 적용 및 프로젝트 폴더 분리 (가비아)

개발용 서버와 배포용 서버를 분리하기 위해 서브 도메인을 적용해보자. 개발용 주소는 dev.도메인 주소로 설정하고 배포용 주소는 prod.도메인 주소를 사용하겠다. 이름은 취사 선택의 문제로 원

cofls6581.tistory.com

root에는 위에서 만든 html 파일이 존재하는 파일 경로를 써주고 index에는 html 파일의 이름을 적어주면 된다.

이후 연결한 서브 도메인으로 접속하면 본인 인증 페이지가 잘 뜸을 확인할 수 있다.

만약, 이때 제대로 뜨지 않는다면 포트 번호와 서버의 인바운드 규칙 등을 확인해보자.


<아임포트 access 토큰 전달 API 구축>

컨트롤러단

    
    private final UserService userService;
    
    @ApiOperation(value = "아임포트 access token 발급 API") //스웨거 사용안할 시 생략 가능
    @GetMapping("/iamports/accessToken")
    public BaseResponse<String> getIamportAccessToken() {
        return new BaseResponse<>(userService.getIamportAccessToken());
    }

 

서비스단

    @Transactional
    public String getIamportAccessToken () {
        RestTemplate restTemplate = new RestTemplate();
        String requestUrl = "https://api.iamport.kr/users/getToken";

        Map<String, String> iamportKey = new HashMap();
        iamportKey.put("imp_key", imp_key); //발급받은 REST API key 값을 넣어주세요
        iamportKey.put("imp_secret",imp_secret); //발급받은 REST API secret 값을 넣어주세요.

        ResponseEntity<Object> responseData = restTemplate.postForEntity(requestUrl, iamportKey, Object.class);
        LinkedHashMap responseBody = (LinkedHashMap) responseData.getBody();
        LinkedHashMap responseBodyProps = (LinkedHashMap) responseBody.get("response");
        String accessToken = (String) responseBodyProps.get("access_token");

        return accessToken;
    }

필자는 키 값들을 노출시키지 않기 위해 다른 파일에 적은 후 git ignore에 해당 파일을 추가하여 개발했다.

 

참고 사이트

https://docs.iamport.kr/tech/mobile-authentication

https://blog.naver.com/iamport/221004352427

https://cordingmonster.tistory.com/76

@Scheduled  어노테이션

주기적으로 해야되는 작업을 쉽게 적용할 수 있도록 도와주는 어노테이션

스프링 3.1 이상부터 지원 

 

@Scheduled  사용 준비

@SpringBootApplication 어노테이션이 있는 어플리케이션 자바 파일에 @EnableScheduling 어노테이션 추가

@SpringBootApplication
@EnableScheduling
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

@Scheduled  사용 

1. cron 

cron 표현식을 지원

초 분 시 일 월 주(년) 순으로 표기 

@Scheduled(cron = "0 0 9 * * *") //매일 9시마다
public void TestScheduler(){
        System.out.println("테스트");
}
0 ~ 59
0 ~ 59
0 ~ 23
1 ~ 31
1 ~ 12 
요일 1 ~ 7 (1 => 일요일, 7=> 토요일) 
년도 1970 ~ 2099

* : 모든 값

? : 특정 값 없음

- : 범위 지정에 사용

, : 여러 값 지정 구분에 사용

/ : 초기값과 증가치 설정에 사용

L : 지정할 수 있는 범위의 마지막 값

W : 월~금요일 또는 가장 가까운 월/금요일

# : 몇 번째 무슨 요일 2#1 => 첫 번째 월요일

 

사용 예시

 "0 0 02 * * ?" : 매일 새벽 두시

 "0 0 12 * * ?" : 매일 정오

 "0 15 10 ? * *" : 모든 요일, 매월, 아무 날이나 10:15:00 

 "0 15 10 * * ? *" : 모든 연도, 아무 요일, 매월, 매일 10:15 

 "0 0/5 14,18 * * ?" : 아무 요일, 매월, 매일, 14시, 18시 매 5분마다 0초 

 "0 0-5 14 * * ?" : 아무 요일, 매월, 매일, 14:00 부터 매 14:05까지 매 분 0초 

 "0 10,30 14 ? 6 WED" : 6월의 매 수요일, 아무 날짜나 14:10:00, 14:30:00 

 "0 15 10 L * ?" : 아무 요일, 매월 마지막 날 10:15:00 

 "0 15 10 ? * 7L" : 매월 마지막 토요일 아무 날이나 10:15:00 

 "0 15 08 ? * 6L 2002-2005" : 2002년부터 2005년까지 매월 마지막 금요일 아무 날이나 0815:00 

 "0 15 08 ? * 6#3" : 매월 3번째 금요일 아무 날이나 08:15:00

 

2. fixedDelay 

해당 메소드가 종료된 시점부터 다음 메소드 실행 시점까지의 주기

시간 단위는 밀리세컨드

@Scheduled(fixedDelay=1000)
public void TestScheduler(){
        System.out.println("테스트");
}

3. fixedRate

해당 메소드가 시작된 시점부터 다음 메소드 실행 시점까지의 주기

시간 단위는 밀리세컨드

@Scheduled(fixedRate=1000)
public void TestScheduler(){
        System.out.println("테스트");
}

 

 

참고 사이트

https://rooted.tistory.com/12

https://toma0912.tistory.com/17

https://jeong-pro.tistory.com/186

https://java119.tistory.com/34
https://javafactory.tistory.com/1386

현재 일하는 곳에서 개발 중인 앱에 firebase를 통해 푸시 알림을 보내고 sms를 전송하는 부분이 있다. 관련된 부분을 수정을 해야하는 상황이라 fcm이 뭐고 어떤 방식으로 돌아가는지 알아볼 겸 글을 작성하게 됐다. 

FCM이란?

구글에서 무료로 메시지를 안정적으로 전송할 수 있는 교차 플랫폼 메시징 솔루션

앱 서버에서 FCM 서버로 메시지 요청이 가고 이를 받은 FCM 서버가 클라이언트에게 메시지를 보낸다.

쉽게 말해 서비스 회사의 서버와 클라이언트 사이에 FCM 서버가 존재하는 거다.

 

그래 메시지 보내주는 거 알겠어,,근데 왜 이렇게 메시지를 보내는데? 뭐가 다른걸까

 

사용자 김씨에게 어떠한 메세지를 전달해야한다고 가정해보자.

김씨에게 메세지를 어떻게 전달할 수 있을까

서비스 회사의 서버에서 김씨에게 메세지를 보내면 된다.

문제는 김씨가 실시간으로 서버로 부터 메세지를 받으려면 서버에 계속 접속해 있어야 한다.

이러한 방식을 사용하면 배터리도 빨리 딣고 네트워크 사용에도 문제가 발생한다.

이때 FCM을 사용하면 문제를 해결할 수 있다.

중간에 FCM 서버가 끼므로 서비스 회사의 서버에 항상 접속해 있지 않아도 되기 때문이다.

서비스 프로그램이 실행 중이 아니더라도 리스너를 통해 메시지를 수신받을 수 있다.

 

FCM을 통한 메시지 전송 흐름(FE+BE)

1. 클라이언트에서 FCM 서버로부터 FCM 토큰을 요청하고 획득한다.

2. 클라이언트에서 서버한테 해당 토큰을 전달하고 서버는 전달 받은 토큰을 디비에 저장한다.

3. 서버가 전달 받은 토큰을 이용해 FCM 서버로 메시지 전송 요청을 보낸다.

4. 요청을 받은 FCM 서버가 사용자에게 메시지를 전송한다.

5. 사용자가 사용 중인 서비스에서 리스너를 통해 메시지를 받는다.

 

FCM 토큰 만료 케이스

FCM 토큰은 만료되지 않으나, 다음과 같은 특정 케이스들에선 변경된다.

1. 앱 instance ID 삭제

2. 앱 삭제 혹은 재설치

3. 앱 사용자가 앱 데이터 삭제


FCM 라이브러리 추가(BE, build.gradle)

dependencies {
    ...
    
    implementation 'com.google.firebase:firebase-messaging:21.1.0'
}

 

FCM 메시지 종류 2가지

1. notification message

title과 body로 구성

fcm SDK에서 자동으로 처리

{
  "message":{
    "token":"bk3RNwTe3H0:CI2k_HHwgIpoDKCIZvvDMExUdFQ3P1...",
    "notification":{
      "title":"Portugal vs. Denmark",
      "body":"great match!"
    }
  }
}

2. data message

key-value 쌍으로 구성

클라이언트 앱에서 처리

{
  "message":{
    "token":"bk3RNwTe3H0:CI2k_HHwgIpoDKCIZvvDMExUdFQ3P1...",
    "data":{
      "Nick" : "Mario",
      "body" : "great match!",
      "Room" : "PortugalVSDenmark"
    }
  }
}

 

FCM 서버 메시지 요청 방법 4가지

1. Firebase Admin SDK 이용(원시 프로토콜)

Node.js자바PythonC#Go 지원

2. HTTP V1 API 이용

가장 최신 프로토콜, firebase admin sdk는 이 프로토콜을 기반으로

대부분의 사례에 이 API 사용 추천

3. 기존 HTTP API 이용

4. XMPP 서버

전송하는 각 메시지를 고유하게 구별하기 위해 서버에서 메시지 ID를 생성할 수 있어야 함

 

 

 

 

 

 

참고 사이트

https://firebase.google.com/docs/cloud-messaging/

https://developer88.tistory.com/159

https://team-platform.tistory.com/23

https://medium.com/@vdongbin/firebase%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-push-notification-5c8a83932472

https://maejing.tistory.com/entry/Android-FCM%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%B4-Push-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0

 

직렬화

자바 객체를 전송 가능한 상태로

 

직렬화 어노테이션들

@JsonAnyGetter

map 필드를 다룰 때 편리

map 필드가 한 번 감싸져서 나옴

public class Member {
    public String name;
    private Map<String, String> properties;

    @JsonAnyGetter
    public Map<String, String> getProperties() {
        return properties;
    }
}

@JsonGetter

@JsonProperty 어노테이션의 대안

메소드의 이름을 getter 메소드로 표현

public class Member {
    public int studentCode;
    private String name;

    @JsonGetter("name")
    public String getTheName() {
        return name;
    }
}

@JsonPropertyOrder

직렬화 하는 속성의 순서를 정함

@JsonPropertyOrder({ "name", "studentCode" })
public class Member {
    public int studentCode;
    public String name;
}

@JsonRawValue

Jackson이 속성을 그대로 직렬화

public class Member {
    public String name;

    @JsonRawValue
    public String deep;
}

output 예시

//적용 후
{
    "name":"My bean",
    "deep":{
        "attr":false
    }
}
//적용 전
{
  "name": "yun",
  "deep": "{\n  \"attr\":false\n}"
}

@JsonValue

@JsonValue 해당 멤버 필드 이름을 통해 직렬화

public enum Member {
    TYPE1(1, "Type A"), TYPE2(2, "Type 2");
    
    private Integer studentCode;
    private String name;

    @JsonValue
    public String getName() {
        return name;
    }
}

@JsonRootName

root wrapper의 이름을 설정할 수 있음

@JsonRootName(value = "user")
public class Member {
    public int studentCode;
    public String name;
}

아웃풋 예시

{
    "user":{
        "studentCode":1,
        "name":"John"
    }
}

 

 

참고 사이트 

https://www.baeldung.com/jackson-annotations

https://pjh3749.tistory.com/281

https://cheese10yun.github.io/jackson-annotation/

 

+ Recent posts