-
Kotlin Multiplatform(KMP) 윈도우 환경에서 File Lock을 이용한 중복 실행 방지Java-Kotlin/Android 2025. 12. 17. 20:25반응형
Kotlin Multiplatform(KMP)으로 윈도우 데스크톱 애플리케이션을 개발하던 중, "프로그램이 이미 실행 중이라면 경고창을 띄우고 종료"시켜야 하는 요구사항이 생겼다.
안드로이드와 달리 데스크톱 환경에서는 사용자가 빌드되어 설치된 exe 파일을 더블 클릭할 때마다 새로운 프로세스가 생성된다. 이를 방지하고 Single Instance Application을 구현하는 과정과, 그 과정에서 겪었던 IOException 트러블 슈팅 경험을 공유한다.
1. 중복 실행 방지 로직에 대한 접근 방법: File Lock (파일 잠금)
윈도우에서 프로세스 간 통신(IPC)이나 Mutex를 사용할 수도 있지만, JVM 환경에서 가장 간편하고 확실한 방법은 파일 락(File Lock)을 사용하는 것이다.
원리는 간단하다.
- 앱이 시작될 때 특정 경로의 파일에 락(FileChannel.tryLock())을 건다.
- 락 획득에 성공하면 앱을 실행한다.
- 락 획득에 실패하면(이미 다른 프로세스가 락을 걸고 있다면) 중복 실행으로 간주하고 종료한다.
2. 트러블 슈팅: IOException과 디렉터리 생성
처음에는 단순히 앱 경로 아래에 temp폴더 아래에 락 파일을 생성하려고 했다.
초기 코드:
// 초기 코드 (문제 발생) val tempFolder = "temp" val file = File(tempFolder, "application.applock") if (!file.exists()) { file.createNewFile() // throws IOException. }하지만 위 초기 코드로 실행 시 java.io.IOException: 지정된 경로를 찾을 수 없습니다 에러가 발생하며 앱이 죽어버렸다. 권한 문제인가 싶어 관리자 권한으로 실행해 봤지만 증상은 동일했다.
원인: Java/Kotlin의 File.createNewFile() 메서드는 파일을 생성할 뿐, 그 파일이 위치할 부모 디렉터리까지 자동으로 만들어주지 않는다. 즉, .my_app이라는 폴더가 없는 상태에서 그 안에 파일을 만들라고 하니 에러가 난 것이다.
해결: 파일 생성 전, 반드시 mkdirs()를 통해 디렉터리의 존재 여부를 확인하고 생성해 주는 로직을 추가하여 해결했다.
해결 한 코드:
val tempFolder = "temp" val file = File(tempFolder, "application.applock") if(!File(tempFolder).exists()) { // 폴더 위치까지 존재하는지 체크 File(tempFolder).mkdirs() } if (!file.exists()) { file.createNewFile() }3. 최종 구현 코드
3-1. AppLock 유틸리티 (Lock 관리)
desktopMain 소스셋에 작성한다. 락 파일은 프로세스가 종료되면 OS에 의해 자동으로 해제되지만, 명시적으로 deleteOnExit()를 호출하여 파일 자체도 정리되도록 했다.
완성 코드: ApplicationLock.ktimport java.io.File import java.io.RandomAccessFile import java.nio.channels.FileChannel import java.nio.channels.FileLock object ApplicationLock { private const val APP_DIR_NAME = "temp" private const val LOCK_FILE_NAME = "application.applock" private var lockFile: RandomAccessFile? = null private var lock: FileLock? = null fun acquireLock(): Boolean { try { val lockFileDir = File(APP_DIR_NAME, LOCK_FILE_NAME) if(!File(APP_DIR_NAME).exists()) { // 폴더 위치까지 존재하는지 체크 File(APP_DIR_NAME).mkdirs() } if (!lockFileDir.exists()) { lockFileDir.createNewFile() } // 파일 락 시도 lockFile = RandomAccessFile(file, "rw") val channel = lockFile!!.channel lock = channel.tryLock() if (lock == null) { // 이미 실행 중이라 락 획득 실패 closeLock() return false } file.deleteOnExit() return true } catch (e: Exception) { e.printStackTrace() closeLock() return false } } private fun closeLock() { try { lock?.release() lockFile?.close() } catch (e: Exception) { /* 무시 */ } } }3-2. Main 진입점 (Swing 팝업 연동)
ComposeUI의 Window를 띄우기 전에 검사를 수행한다. 이미 실행 중이라면 무거운 Compose UI를 로딩하는 대신, 가벼운 Swing의 JOptionPane 을 사용하여 알림 팝업을 띄우고 프로세스를 종료한다.
완성 코드: main.ktimport androidx.compose.ui.window.Window import androidx.compose.ui.window.application import javax.swing.JOptionPane import kotlin.system.exitProcess fun main() { // 락 검사 if (!AppLock.acquireLock()) { JOptionPane.showMessageDialog( null, "프로그램이 이미 실행 중입니다.", "알림", JOptionPane.WARNING_MESSAGE ) exitProcess(0) // 강제 종료 } // 정상 실행 application { Window(onCloseRequest = ::exitApplication, title = "My App") { App() } } }4. 결론
Kotlin Multiplatform(KMP)에서 데스크톱 프로그램 개발 시 파일 시스템에 접근할 때는 항상 해당 경로의 폴더가 실제로 존재하는가? 를 먼저 체크해야 한다는 것을 다시 한번 상기했다. FileLock과 Swing의 JOptionPane의 조합은 별도의 복잡한 라이브러리 없이 JVM에 기본적으로 있는 기능만으로 깔끔하게 중복 실행 방지 로직을 구현할 수 있는 좋은 방법이다.
'Java-Kotlin > Android' 카테고리의 다른 글
Android DataStore 를 사용하여 파일에 간단한 정보 저장하기 (0) 2026.01.14 SCP-380CII 영수증 프린터 안드로이드 SDK 연동기 (0) 2025.12.31 Android에서 USB Serial 장치 연결 시 USB 권한 문제 해결기 (0) 2025.12.03