본문 바로가기
관리자

Programming-[Base]/OS-Unix, Linux, Ubuntu

macOS launchctl: firebase messaging background scheduling

728x90
반응형

 

 

1. 배경

앱을 운영하는데, 동적인 메시지를 firebase messaging으로 사용자들에게 scheduling하여 주기적으로 알람을 전송하고 싶었다. intellij에서 실행하는 java 코드라, intellij로 계속 서버를 띄워놔도 되겠지만 intellij를 이용해서 여러 프로젝트 작업들을 하는데 이 서버를 계속 띄워놓으면 방해가 될 것 같았다. 그리고 intellij process가 종료되면 messaging이 중단되는거라, 가용성이 걱정되어 백그라운드로 실행하는 법을 알아보게 되었다.

 

 

2. launchctl

macOS에서는 launchctl을 사용하여 plist 파일로 프로그램을 실행할 수 있다. .plist 파일을 ~/Library/LaunchAgents/ 디렉토리 내에 넣어두고 load하면 프로그램이 작동된다.

 

load로 실행하고, unload로 종료할 수 있다.

launchctl load ~/Library/LaunchAgents/org.example.your_project.plist
launchctl unload ~/Library/LaunchAgents/org.example.your_project.plist

 

 

실행 중인 프로그램은 launchctl list 명령어로 확인이 가능하다. 실행 중이라면 port 번호와 함께 정보가 뜨고, 없다면 아무런 내용이 뜨지 않는다.

launchctl list | grep org.example.your_project

 

 

자바로 실행 중이라면 아래 명령어를 통해 확인할 수도 있다.

ps aux | grep java

 

 

3. plist

plist 파일은 아래처럼 작성했다. 더 자세한 내용들은 검색해보면 알 수 있다. 중간중간 your_project 라는 이름으로 실제 프로젝트 이름을 대체했다.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  <dict>
    <key>Label</key>
    <string>org.example.your_project</string>

    <key>ProgramArguments</key>
    <array>
      <string>/usr/bin/java</string>
      <string>-jar</string>
      <string>{프로젝트 경로}/build/libs/your_project-1.0-SNAPSHOT-all.jar</string>
      <string>-Dlogback.configurationFile=/Users/user_name/your_project/logback.xml</string>
    </array>

    <key>RunAtLoad</key>
    <true/>

    <key>StandardOutPath</key>
    <string>/tmp/your_project_log.out</string>

    <key>StandardErrorPath</key>
    <string>/tmp/your_project_err.out</string>
  </dict>
</plist>

 

 

Label로 위 launchctl 명령어를 실행할 프로젝트 이름을 정한다. ProgramArguments에 java -jar {프로젝트 이름.jar}를 입력했다. log를 남기기 위해서 -Dlogback 설정을 추가했다.

 

StdOut, StdErr를 출력해서 저장해놓기 위해서 /tmp/... 위치에 .out 파일로 로그가 남도록 설정해주었다.

 

 

4. fat jar, shadowJar,  ClassLoader 상대 경로

 

프로젝트를 jar로 패키징하기 위해 아래처럼 build.gradle을 구성했다.

 

일반 jar 태스크는 enabled=false로 두어 실행이 안되게 했다. 프로젝트에 포함된 의존성 라이브러리들을 모두 포함하기 위해서 fat jar 형태로 패키징했다. fat jar에 사용되는 도구가 shadowJar이며, plugins에 추가하고 shadowJar 태스크를 추가했다. 프로그램의 진입점인 Main 클래스를 Main-Class 속성으로 지정해주었다.

plugins {
    id 'java'
    id 'com.github.johnrengelman.shadow' version '7.1.2'
}

group = 'org.example'
version = '1.0-SNAPSHOT'

repositories {
    mavenCentral()
    google()
}

dependencies {
    implementation 'com.google.firebase:firebase-admin:9.3.0'
    implementation 'org.slf4j:slf4j-api:2.0.16'
    implementation 'org.quartz-scheduler:quartz:2.3.2'
    implementation 'org.slf4j:slf4j-api:2.0.16'
    implementation 'ch.qos.logback:logback-classic:1.5.8'
    compileOnly 'org.projectlombok:lombok:1.18.34'
    annotationProcessor 'org.projectlombok:lombok:1.18.34'

    testImplementation 'ch.qos.logback:logback-classic:1.5.8'
    testImplementation 'org.slf4j:slf4j-reload4j:2.0.16'
    testImplementation platform('org.junit:junit-bom:5.9.1')
    testImplementation 'org.junit.jupiter:junit-jupiter'
}

shadowJar {
    manifest {
        attributes(
                'Main-Class': 'org.example.Main'
        )
    }
    mergeServiceFiles()
}

jar {
    enabled = false
}

 

 

이렇게 생성된 파일의 위치를 위 plist의 ProgramArguments에 지정해주어 프로그램을 실행하는 것이다.

 

 

상대 경로 지정하기

launchctl로 프로그램을 실행했는데, firebase messaging이 제대로 가지 않고 있었다. 로그를 통해 확인해보니, firebase를 initialize하는 부분에서 firebase용 .json 파일이 제대로 로딩이 되지 않고 있었다.

 

이는 intellij에서는 해당 파일을 바로 인식하지만, jar에서는 상대경로로 지정해주어야 정상적으로 인식하기 때문이였다. 그래서 아래와 같은 코드를,

 

public static void initialize() throws IOException {
    // 서비스 계정 JSON 파일 경로를 사용해 FirebaseOptions 생성

    FileInputStream serviceAccount =
            new FileInputStream("src/main/resources/{firebase 설정 파일 이름}.json");

 

 

아래처럼 classLoader가 상대 경로를 인식할 수 있도록 변경해주었다.

public static void initialize() throws IOException {
    // 서비스 계정 JSON 파일 경로를 사용해 FirebaseOptions 생성
    InputStream serviceAccount = Main.class.getClassLoader().getResourceAsStream("{firebase 설장 파일 이름}.json");
    if (serviceAccount == null) {
        throw new FileNotFoundException("Firebase config file not found in classpath");
    }

 

728x90
반응형