현재 일하는 곳에서 개발 중인 앱에 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/

 

게시글 목록을 조회할 때 페이지 번호로 목록이 구분되는 걸 많이 본적이 있을 거다.

한 화면에 모든 글을 보여줄 수 없기 때문에 페이지로 나눠 게시글 목록을 보여주는 것인데 오늘은 이 페이징 처리에 관해 글을 써보려고 한다.

 

이 기능을 실제로 하나하나 구현할 시 상당히 번거로운데 Spring Data JPA에서는 Pageable을 이용해 손쉽게 페이징 처리를 할 수 있도록 도와준다. 이떄 단순하게 pagination만 해주는 게 아니라 보여지는 목록에 정렬이 필요한 경우 sorting도 같이 할 수 있다.

 

1. 페이징 처리할 레포지토리에 JPARepositoy를 상속한다.

@Repository
public interface postRepository extends JpaRepository<Post, Long> {
	List<Post> findAll(Pageable pageable);
}

2. 페이징 처리

pageable 객체를 만들어 repository의 메소드에 인자로 전달하면 된다.

Pageable pageable = PageRequest.of(현재 페이지 넘버, 조회할 페이지의 수, 정렬관련정보들);

2-1. 페이징 처리

Pageable pageable = PageRequest.of(page - 1, 10);
repo.메소드명(pageable);

페이지 번호는 0번 부터 시작해 1을 빼줘야한다.

 

2-2. 페이징 처리+정렬

Pageable pageable = PageRequest.of(0, 3, Sort.by("postId").descending());
repo.메소드명(pageable);

 

 

참고자료

 

https://www.baeldung.com/spring-data-jpa-pagination-sorting

https://velog.io/@jjun_meatlov/Spring-Data-JPA-%ED%8E%98%EC%9D%B4%EC%A7%95-%EC%B2%98%EB%A6%AC%ED%95%98%EA%B8%B0

https://devlog-wjdrbs96.tistory.com/414

http://devstory.ibksplatform.com/2020/03/spring-boot-jpa-pageable.html

 

EC2서버에서 스프링 부트 프로젝트를 돌리는데 계속 빌드를 하다 멈추는 일이 발생했다.

디스크 메모리 용량을 확인해보니 아직 한참 남아서 cpu 사용량을 확인해보니 RAM이 딸려서 발생하는 문제인 것 같았다.

알아보니 t2.micro의 램이 1GB밖에 안돼서 현재 서버에서 스프링 부트 빌드만 해도 터지는 것 같았다.

이전 스프링 부트 프로젝트를 할 때는 램이 모잘라서 터지진 않았는데

이번 스프링 부트 프로젝트는 사용하는 기술들이 많아 몸집이 상당히 커져서 계속 터지는 것 같다.

 

다행히 리눅스에서는 SWAP 메모리를 지정할 수 있어서 RAM이 부족할 때 HDD의 일정 공간을 마치 RAM처럼 사용할 수 있었다. 속도가 좀 느리긴 하지만 그래도 돌아가는게 어디인가

(실무에서는 좋은 해결 방법이 아닌 것 같은데 애초에 실무에서 쓰이는 서버라면 RAM이 1GB이지 않을 것 같다ㅎㅎ)

 

램이 2GB이하일 땐 보통 RAM 용량의 2배를 SWAP 메모리로 잡는데 ec2는 1GB로 SWAP 메모리는 2GB로 잡아서 설정할 예정이다. 이때 주의해야되는게 SWAP 메모리의 공간이 최소 32MB는 돼야한다고 한다.

 

free 명령어를 통해 메모리(램)과 SWAP 메모리를 확인해 볼 수 있다.

현재 이렇게 SWAP 메모리 할당이 안돼있는 상태이다.

이제 SWAP 메모리를 할당해보자.

sudo dd if=/dev/zero of=/swapfile bs=128M count=16   //2GB정도 공간 할당
sudo chmod 600 /swapfile    //swap 파일 권한 업데이트
sudo mkswap /swapfile    //linux swap 영역 설정
sudo swapon /swapfile    //swap space에 swap file 추가
sudo swapon -s    //위 명령어들 성공 확인

다음으로 /etc/fstab 파일을 편집하여 부팅 시 swap 파일을 활성화 해줘야 한다.

$ sudo vi /etc/fstab

제일 마지막에 아래 내용을 추가해준다.

/swapfile swap swap defaults 0 0

 

다시 free 명령어로 확인해보자.

 

참고사이트

https://aws.amazon.com/ko/premiumsupport/knowledge-center/ec2-memory-swap-file/
https://sundries-in-myidea.tistory.com/102

https://incomeplus.tistory.com/284?category=868671

ec2 서버에 프로젝트 코드를 정상적으로 옮기는 것 까지 성공했다면

이젠 해당 코드들을 빌드하고 무중단 서비스를 실행하고 중단해보자.

 

스프링 부트 프로젝트가 있는 이전 폴더로 이동합니다.

 

맨 처음 빌드시 permission denied가 뜨니 먼저 권한 설정부터 해줍니다.

chmod -R 777 프로젝트있는폴더명/

이후 프로젝트 폴더로 이동해줍니다.

 

스프링 부트 프로젝트 빌드 

./gradlew clean build

스프링 부트 무중단 서비스 시작

각자 맞는 실행파일 이름은 build/libs 폴더에 들어가서 확인할 수 있다.

nohup java -jar build/libs/{각자맞는이름}-0.0.1-SNAPSHOT.jar &

스프링 부트 무중단 서비스 종료

ps -ef | grep {각자맞는이름}-0.0.1-SNAPSHOT.jar
=>종료시킬 스프링 부트 pid 확인
sudo kill -9 확인한pid
=>무중단 스프링 부트 중지

Amazon RDS(Relational Database Service)란?

분산 관계형 데이터베이스 서버 인스턴스로서 관계형 데이터베이스의 설치, 관리, 업데이트를 알아서 해준다.

따라서, RDS를 통해 관계형 데이터베이스를 손쉽게 사용, 관리할 수 있고 스토리지 확장 같은 기능들을 활용할 수 있다.

프리티어 계정이라면 aws에서 직접 운용하는 Aurora는 사용할 수 없고 Microsoft SQL, Oracle, MySQL, Aurora, Maria DB 중에서 하나를 택해서 임대 받으면 된다.


1. RDS 데이터베이스 생성받기

검색창에서 RDS 검색 후 데이터베이스 클릭

필자는 이미 임대 받은 RDS가 있어서 저렇게 뜨지만 원래는 목록이 비어있다.

데이터베이스 생성 클릭

원하는 데이터베이스 종류 선택

Aurora는 프리티어 무료 계정으로 사용할 수 없다.

버전을 확인할 수도 있고 변경을 원할 시 변경도 가능하다.

무료로 사용할 예정이므로 프리 티어를 선택한다.

데이터베이스 이름, 마스터 이름, 비밀번호를 설정한다.

이후 워크벤치, 데이터그립등을 활용하여 데이터베이스에 접근할 때 필요한 정보이므로 신중하게 설정해야한다.

과금을 막기 위해 범용 SSD 스토리지 유형 선택, 자동 조정 활성화 체크를 해제한다.

외부에서도 데이터베이스에 접근할 수 있도록 퍼블릭 엑세스를 예로 바꿔준다.

aws의 모든 인스턴스들은 내부/외부 상호작용을 위해 vpc 설정을하는데 필자는 기본 vpc를 선택했다.

외부에서 DB에 접근할 때 인증 옵션을 선택해야한다.

현업이 아니고 사이드 프로젝트용으로 사용할 것이기 때문에 간단하게 암호 인증으로 선택했다.

이후 데이터 베이스 생성 버튼 클릭하면 데이터베이스 인스턴스가 생성되며 RDS 데이터베이스 탭에서 확인가능하다.

 

2. RDS 파라미터 그룹 변경

Time Zone, Character Set, Max Connection 파라미터 값을 변경해줘야한다.

RDS에 들어가서 파라미터 탭->파라미터 그룹 생성 클릭

그룹 이름과 설명을 적어준 뒤 생성 버튼 클릭

 

아래 사진들의 왼쪽 이름을 검색 후 오른쪽과 같이 설정 변경

time_zone

charcter_set

한글과 이모지 지원

max_connection

 

 

최종 변경 사항 저장 클릭

다시 RDS 데이터베이스 탭으로 이동하여 해당 데이터베이스를 클릭한 후 수정 클릭

추가 구성에서 방금 생성한 파라미터 그룹으로 설정 후 초기 설정이므로 즉시 적용 클릭

예시로 만들던 파라미터 그룹과 이름이 다른건 이전 파라미터 그룹을 설정한 것으로 신경안쓰셔도 됩니다.

 

3. 보안그룹 설정

RDS 데이터베이스 탭에서 원하는 인스턴스의 이름 클릭

연결&보안에서 vpc 보안 그룹 밑 이름 클릭

 

인바운드 규칙 탭에서 인바운드 규칙 편집 클릭

외부에서 접속하려는 ip 추가(로컬에서 접근시 네이버에서 내 ip 주소치면 ip주소 확인 가능)

이때  ip주소 뒤에 /32를 넣어줘야된다.

ec2 보안 그룹 ID를 복사해서 넣어줘도 된다. 

다 추가했으면 규칙 저장

 

이제 초기 설정은 다 끝났다. 

 

이후 원하는 프로그램을 써서 데이터베이스에 접근하면 된다.

필자는 주로 데이터그립을 사용하며 데이터그립을 활용해 연결하는 방법은 아래 게시물을 참고하면 된다.

https://cofls6581.tistory.com/60

 

Datagrip으로 MySQL 외부에서 접속하기

Datagrip이란? 외부 접속위한 DB 개발과 관리를 용이하게 해주는 GUI 개발 툴 Datagrip 다운 www.jetbrains.com/ko-kr/datagrip/download/#section=windows 학생 인증 후 무료 라이센스 pack 다운로드 new project..

cofls6581.tistory.com

 

참고 사이트

https://overcome-the-limits.tistory.com/294

https://bcp0109.tistory.com/357?category=1073811

https://8iggy.tistory.com/71

https://brunch.co.kr/@topasvga/696

+ Recent posts