반응형

JVM Memory는 스택과 힙 영역으로 구분된다. 이들은 어떻게 다르고 멀티 스레드에서는 어떻게 동작하는걸까?

Context

코틀린 프로그래밍에서 클래스의 인스턴스를 생성할 때 비용이 발생한다. 인스턴스를 생성하고 더 이상 사용하지 않을 경우 가비지 콜렉션 과정을 통해 메모리에서 해제하는 과정 또한 비용이 발생한다.따라서 인스턴스를 매번 생성할 필요가 없는 경우 매번 인스턴스를 생성하지 않는 것이 성능 측면에서 더 유리하다.

이 때문에 개발자는 요청마다 매번 인스턴스를 생성해야 하는지, 생성하지 않고 이미 생성된 인스턴스를 재사용할 것인지를 판단해야 한다. 이에 대한 기준은 인스턴스가 상태 값을 유지해야 하는지에 따라 구분된다.

송금 관리 시스템

예를 들어 은행 송금 시스템의 경우엔 송금을 관리하는 하나의 인스턴스를 만들어 놓고 송금 금액, 보내는 사람, 받는 사람 등의 정보만 입력 받아 처리해주면 될 것이다. (썩 좋은 예시는 아니지만 넘어가자)

data class Customer(
    val name: String,
    val accountNumber: String
)

data class Transfer(
    val customer: Customer,
    val quantity: Long
)

object TransferManager {
    lateinit var transfer: Transfer
    lateinit var transferTarget: Customer

    fun transfer() {
        println("Quantity: ${transfer.quantity}, ${transfer.customer} -> $transferTarget")
    }
}

송금 정보(금액, 보내는 사람)와 받는 사람을 필드로 갖는 TransferManager를 한 개 만들었다.

JVM은 코드를 실행하기 위해 메모리를 스택과 힙 영역으로 나눠서 관리한다. 스택 영역은 각 메소드가 실행될 때 메소드의 인자, 로컬 변수 등을 관리하는 메모리 영역으로 각 스레드마다 서로 다른 스택 영역을 가진다. 힙 영역은 클래스의 인스턴스 상태 데이터를 관리하는 영역이다. 힙 영역은 스레드가 서로 공유할 수 있다.

위 예시에서 TransferManager의 transfer와 transferTarget 필드는 JVM Heap에 적재되어 모든 스레드에서 공동으로 접근할 수 있게 된다.

발생할 수 있는 문제

현재 TransferManager는 애플리케이션 상에 단 한개의 인스턴스만 존재한다. 그리고 transfer와 transferTarget이 Heap영역에 적재되어 여러 스레드에서 동일한 transfer, transferTarget을 바라보고 있게 된다.

fun main() {
    // 첫 번째 송금 요청: 송금이 지연 됨
    thread {
        val transferCustomer = Customer(
            name = "Aaron Jin",
            accountNumber = "0000-0000-0000"
        )

        val transferTarget = Customer(
            name = "Martin",
            accountNumber = "1111-1111-1111"
        )

        TransferManager.transfer = Transfer(transferCustomer, 50000)
        TransferManager.transferTarget = transferTarget

        Thread.sleep(1000)

        println("첫 번째 송금 요청")
        TransferManager.transfer()
    }

    // 두 번째 송금 요청: 송금 요청이 바로 처리 됨
    thread {
        val transferCustomer = Customer(
            name = "Wanda Morton",
            accountNumber = "2222-2222-2222"
        )

        val transferTarget = Customer(
            name = "Philip Adams",
            accountNumber = "3333-3333-3333"
        )

        TransferManager.transfer = Transfer(transferCustomer, 1000000)
        TransferManager.transferTarget = transferTarget

        println("두 번째 송금 요청")
        TransferManager.transfer()
    }
}

두 개의 스레드 안에서 각각 송금 요청을 하고 있는 케이스다. (단, 첫 번째 요청은 1초가 소요되게끔 코드를 작성했다.)

  • 첫 번째 요청: Aaron Jin -> Martin (50,000원)
  • 두 번째 요청: Wanda Morton -> Philip Adams (1,000,000원)

이 코드를 실행한다면 어렵지 않게 두 번째 요청이 먼저 처리되고 약 1초 뒤 첫 번째 요청이 처리될 것이라는 것을 추측해볼 수 있다. 그리고 여기서 문제가 발생한다.

<실행 결과>
두 번째 송금 요청
Quantity: 1000000, Customer(name=Wanda Morton, accountNumber=2222-2222-2222) -> Customer(name=Philip Adams, accountNumber=3333-3333-3333)
첫 번째 송금 요청
Quantity: 1000000, Customer(name=Wanda Morton, accountNumber=2222-2222-2222) -> Customer(name=Philip Adams, accountNumber=3333-3333-3333)

첫 번째 송금 요청이 Wanda Morton -> Philip Adams (1,000,000원)으로 처리되었다.

이는 두 요청에서 사용되는 transfer, transferTarget이 힙 영역에 적재되어있기 때문이다. 이러한 문제를 발생시키지 않기 위해선 이러한 데이터를 스택 영역에 적재해야만 한다.

해결

package com.example.blog

import kotlin.concurrent.thread

data class Customer(
    val name: String,
    val accountNumber: String
)

data class Transfer(
    val customer: Customer,
    val quantity: Long
)

object TransferManager {
    fun transfer(transfer: Transfer, transferTarget: Customer) {
        println("Quantity: ${transfer.quantity}, ${transfer.customer} -> $transferTarget")
    }
}

fun main() {
    // 첫 번째 송금 요청: 송금이 지연 됨
    thread {
        val transferCustomer = Customer(
            name = "Aaron Jin",
            accountNumber = "0000-0000-0000"
        )

        val transferTarget = Customer(
            name = "Martin",
            accountNumber = "1111-1111-1111"
        )

        Thread.sleep(1000)

        println("첫 번째 송금 요청")
        TransferManager.transfer(
            transfer = Transfer(transferCustomer, 50000),
            transferTarget = transferTarget
        )
    }

    // 두 번째 송금 요청: 송금 요청이 바로 처리 됨
    thread {
        val transferCustomer = Customer(
            name = "Wanda Morton",
            accountNumber = "2222-2222-2222"
        )

        val transferTarget = Customer(
            name = "Philip Adams",
            accountNumber = "3333-3333-3333"
        )

        println("두 번째 송금 요청")
        TransferManager.transfer(
            transfer = Transfer(transferCustomer, 1000000),
            transferTarget = transferTarget
        )
    }
}
<실행 결과>
두 번째 송금 요청
Quantity: 1000000, Customer(name=Wanda Morton, accountNumber=2222-2222-2222) -> Customer(name=Philip Adams, accountNumber=3333-3333-3333)
첫 번째 송금 요청
Quantity: 50000, Customer(name=Aaron Jin, accountNumber=0000-0000-0000) -> Customer(name=Martin, accountNumber=1111-1111-1111)

 

반응형

'Kotlin&Java' 카테고리의 다른 글

스택과 힙 메모리, 그리고 멀티 스레드  (0) 2021.08.22
잘못된 내용이 있다면, 다른분들에게 전달되지 않게끔 댓글로 지적 부탁드립니다!

+ Recent posts