ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 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)을 사용하는 것이다.

     

    원리는 간단하다.

    1. 앱이 시작될 때 특정 경로의 파일에 락(FileChannel.tryLock())을 건다.
    2. 락 획득에 성공하면 앱을 실행한다.
    3. 락 획득에 실패하면(이미 다른 프로세스가 락을 걸고 있다면) 중복 실행으로 간주하고 종료한다.

     

     

    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.kt
    import 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.kt
    import 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)에서 데스크톱 프로그램 개발 시 파일 시스템에 접근할 때는 항상 해당 경로의 폴더가 실제로 존재하는가? 를 먼저 체크해야 한다는 것을 다시 한번 상기했다. FileLockSwing의 JOptionPane의 조합은 별도의 복잡한 라이브러리 없이 JVM에 기본적으로 있는 기능만으로 깔끔하게 중복 실행 방지 로직을 구현할 수 있는 좋은 방법이다.

    댓글

Designed by Tistory.