본문 바로가기
공부/코틀린코루틴:딥다이브(마르친 모스카와)

14장 - 공유 상태로 인한 문제

by 띵커베르 2024. 3. 1.
728x90
  • 위 예제에서는 방어적 복사로 toList 를 사용했습니다. downloaded 로 반환된 객체를 읽을 때와 변경 가능한 리스트에 원소를 추가할 때 발생할 수 있는 충돌을 피하기 위함입니다.users 를 읽기만 가능한 리스트(List<User>) 와 읽고 쓰기가 가능한 프로퍼티(var) 로 선언할 수도 있습니다.방어적 복사를 하지 않아도 되고 downloaded 함수를 보호할 필요도 없지만 컬렉션에 원소를 추가하는 작업의 효율이 떨어지게 됩니다.개인적으로 두번째 방법을 선호하지만, 실제 현업의 많은 프로젝트에서 변경 가능한 컬렉션을 사용하고 있으므로 첫 번째 방법을 예로 들었습니다.
    • users.toList()를 통해 생성된 복사본은 List<User> 타입입니다. 이 복사본은 기본적으로 변경할 수 없습니다. 즉, 복사본은 수정이 불가능합니다. 원본만 수정가능.
      이는 데이터의 불변성을 유지하고, 예기치 않은 변경으로부터 데이터를 보호하는 효과적인 방법입니다.

    • 그러나 이 방법은 매번 새로운 리스트를 생성하기 때문에 메모리 사용량이 증가하고, 리스트가 매우 크거나 자주 접근될 경우 성능 저하를 초래할 수 있습니다. 이를 개선하기 위한 한 가지 방법은, users 프로퍼티 자체를 읽기 전용 List<User>로 선언하고, 내부적으로는 변경 가능한 리스트를 유지하는 것입니다. 이 경우, 외부에서는 users를 직접 변경할 수 없으므로 복사본을 만들 필요가 없어집니다. 그러나 이 방식을 사용하면 리스트에 새로운 원소를 추가하는 등의 변경 작업이 더 복잡해지거나 효율이 떨어질 수 있습니다.
      실제 현업에서는 변경 가능한 컬렉션을 자주 사용합니다. 이는 코드의 유연성과 편의성 때문입니다. 하지만, 변경 가능성이 외부로 노출될 경우 데이터의 안정성이나 예측 가능성이 저하될 수 있습니다. 따라서, 방어적 복사 같은 기법을 사용하여 데이터를 보호하는 것이 일반적인 접근 방식입니다. 그럼에도 불구하고, 특정 상황이나 성능 요구 사항에 따라 다른 전략을 선택할 수 있습니다.
    @Test
    fun test1() {
        // 원본 MutableList 생성
        val originalUsers = mutableListOf(User("injin", "Jeong"))

        // 원본 리스트에서 복사본 생성
        val copiedUsers = originalUsers.toList()

        // 원본 리스트 변경
        originalUsers.add(User("injin2", "Jeong2"))
        // copiedUsers 는 읽기 전용이므로 추가할 수 없음
        // copiedUsers.add(User("injin3", "Jeong3")) // 에러 발생
        // copiedUsers.removeAt(0) // 에러 발생
        // copiedUsers 를 읽기전용으로만들고 add 하려면 아래와 같이 변경
         val copiedUsers2 = originalUsers.toList().toMutableList()
        copiedUsers2.add(User("injin3", "Jeong3"))

        // 복사본과 원본 리스트 출력
        println(copiedUsers)  // 복사본에는 injin 만 포함
        println(originalUsers) // 원본에는 injin, injin2 포함
    }

 

  • 자바에서 원자성 연산
    • 여러 단계를 거쳐야 할 작업을 중간에 방해받지 않고 한 번에 완료할 수 있는 연산을 의미
    • 동시성 문제를 방지하기 위해 사용된다.
  • 원자값: 원자성 연산을 지원하는 변수들을 말한다. 자바의 java.util.concurrrent 패키지에 원자성을 보장하는 클래스들이 포함되어 있다
    • ex: AtominInteger, AtomicLong, AtomicBoolean 등
  • 특징:
    • 스레드 안전하다
      • 원자성 연산을 수행하는 동안에는 해당 연산이 중간에 중단되거나 다른 스레드에 의해 방해받지 않습니다. 따라서 여러 스레드가 동시에 같은 변수를 수정하려고 해도, 각 연산은 서로 독립적으로 완료됩니다.
    • 비차단
      • 대부분의 원자성 연산은 락(lock)을 사용하지 않고 구현됩니다. 이는 락을 사용하는 대신, 하드웨어 수준에서의 CAS(Compare-And-Swap) 같은 연산을 활용하여 성능을 높입니다.
    • 성능
      • 원자성 연산은 일반적으로 락을 사용하는 동기화 메커니즘보다 빠릅니다. 특히 고성능이 요구되는 환경에서 많은 스레드가 동시에 같은 데이터에 접근해야 할 때 유용합니다.
  • 뮤텍스
    • 단 하나의 열쇠가 있는 방 이라고 생각할 수 있다.
    • 가장 중요한 기능은 lock 이다.
    • lock(), unlock() 을 직접 사용하는건 위험한데, 데드락을 일으킬 수 있다.
    • finally 를 사용해서 처리할 수 있겠지만 withLock 함수를 사용하면 이를 쉽게 처리할 수 있다.
    • synchronize 블록과 뮤텍스가 가지는 중요한 이점은 스레드를 블로킹하지는 대신 코루틴을 중단시킨다는 것이다.
    • 뮤텍스를 사용할 때 맞딱드리는 위험한 경우는 코루틴이 락을 두 번 통과할 수 없다는 것이다.
      • ex) withLock 안에 또다른 withLock 이라던지..
    • 두번째 문제는 코루틴이 중단되었을 때 뮤텍스를 풀 수 없다.
    • 싱글스레드로 제한된 디스패처를 사용하면 이런 문제는 발생하지 않는다.
      • ex) private val dispatcher = Dispatchers.IO.limitedParallelism(1)
  • 세마포어
    • mutex 와 비슷한 방식으로 동작하지만 둘 이상 접근할 수 있다.
    • 세마포어는 공유 상태로 인해 생기는 문제를 해결할 수는 없지만 동시요청을 처리하는 수를 제한할 때 사용할 수 있어 "처리율 제한 장치(rate limiter)를 구현할 때 도움이 된다.
      • ex) private val semaphore = Semaphore(10)
728x90

댓글