코틀린 코루틴(15)
코루틴 무료 카지노 게임 (Channel)에서 Channel_ex01.kt를 살펴보면서 다음과 같은 채널의 특징을 이끌어 냈다.
- 송신 측에서 보낸 값이 수신되지 않으면 대기 상태가 된다. (send()가 receive()를 만나지 못하면 suspend 된다.)
- 수신 측에서는 값을 받지 않으면 대기 상태가 된다. (receive()가 send()를 만나지 못하면 suspend 된다.)
send()와 receive()가 중단되지 않으려면 한 쌍이 되어야 한다는 것이다. 하지만 이것은 선행되는 조건이 있다. 채널의 버퍼 크기가 0인 경우다. 버퍼 크기가 0인 채널을 랑데뷰(Rendezvous) 채널이라고 한다.
카지노 가입 쿠폰 크기가 0이기 때문에 send()와 receive()가 한 쌍이어야 하는 것이다. 카지노 가입 쿠폰가 없기 때문에 값을 전송하고 수신되지 않으면 다시 전송할 수가 없다. 그림을 보면 좀 더 명확하다.
send()가 data1이라는 값을 전송한다. send()는 해당 값이 수신되지 않은 상태에서 data2라는 값을 전송하려고 시도한다. 채널은 기존에 전송하려고 시도 중인 data1에 대한 데이터를 보관할 버퍼가 없기 때문에 send()의 요청을 처리할 수 없다. send()는 data1이 수신될 때까지 기다려야 한다.
receive()가 data1을 수신하면 채널은 다시 값을 전달할 수 있는 상태가 된다. 그러면 send()가 data2를 보내려고 하는 요청을 받아들일 수 있다.
만약 카지노 가입 쿠폰가 있다면 어떻게 동작할 수 있을까?
send()가 전송한 값을 receive()가 수신하지 않아도 카지노 가입 쿠폰에 보관할 수 있다. 따라서 카지노 가입 쿠폰가 가득 차기 전까지 send()는 대기 상태가 되지 않는다. 하지만 카지노 가입 쿠폰가 가득 차면 카지노 가입 쿠폰에 공간이 생기기 전까지 대기 상태가 된다.
실제 아래 코드를 통해서 확인할 수 있다.
실행 결과는 다음과 같다.
Try to send: 0
Sent: 0
Try to send: 1
Sent: 1
Try to send: 2
Sent: 2
Try to send: 3
Sent: 3
Try to send: 4
Sent: 4
Try to send: 5
Line 2: 채널을 생성할 때 버퍼 크기를 5로 지정한다.
Line 5~9: send()를 10회 시도한다.
Line 12~13: 반복문이 모두 실행되기 충분한 1초 동안 지연 후 sender 코루틴을 취소한다.
실행 결과를 보면 Sent는 총 5회 실행되었다. 하지만 Try to send는 총 6회 실행되었다. 마지막 send() 시도는 실제로 전송되지 않았고 대기 상태에 있는 것이다. 그림으로 보면 다음과 같다.
현재 버퍼의 상태는 가용할 수 있는 크기가 0이기 때문에 랑데뷰 채널에서 send()가 대기 상태에 빠진 것과 별반 다를 바 없는 것이다. 그렇다면 Sent: 5가 발생하고 Try to send: 6이 실행되려면 어떻게 해야 할까? 그렇다. receive()로 값을 하나 수신하면 된다.
실행 결과는 다음과 같다.
Try to send: 0
Sent: 0
Try to send: 1
Sent: 1
Try to send: 2
Sent: 2
Try to send: 3
Sent: 3
Try to send: 4
Sent: 4
Try to send: 5
Received: 0
Sent: 5
Try to send: 6
실행 결과를 보면 가장 먼저 전송되었던 0이 수신되었다. 따라서 버퍼에 빈 공간이 하나 생긴다. 그러면 대기 상태에 있던 send(5)가 다시 전송할 수 있는 상태가 된다. 그래서 Sent: 5가 실행된 것을 확인할 수 있다. 그 후 시도하는 send(6)은 다시 대기 상태가 된다. 그림으로 보면 다음과 같다.
앞서 살펴보았듯 채널을 생성할 때 버퍼 크기를 지정할 수 있다. 크기를 지정할 때 사용되는 파라미터는 capacity이다. 즉 Channel<Int(5)로 채널을 생성할 때 명명된 인자(Named Argument)로 선언하면 Channel<Int(capacity = 5)가 된다.
이러한 capacity 인자에 따라 미리 정의된 채널을 생성할 수 있다. 앞서 언급했던 랑데뷰 채널도 그중 하나다. 미리 정의된 채널 종류는 RENDEZVOUS, CONFLATED, UNLIMITED, BUFFERED이다. 이 채널들은 단순히 버퍼의 크기에 따른 구분이 아니다. Channel() 함수의 다른 파라미터인 onBufferOverflow에 따라 동작 방식의 차이가 있다. 그렇기 때문에 각 채널들이 어떤 특징을 가지는지 먼저 정의하기 전에 onBufferOverflow가 무엇인지 이해할 필요가 있다. 그 후에 각 채널이 어떤 특징을 가지는지 추론하여 정의를 내려보도록 한다.
onBufferOverflow는 채널의 카지노 가입 쿠폰가 가득 찼을 때 어떻게 처리할 것인지 결정할 수 있는 파라미터이다. 해당 파라미터의 타입은 BufferOverflow이다. BufferOverflow는 enum 클래스로 카지노 가입 쿠폰 오버플로우 시 동작 방식을 결정하는 상수들을 포함하고 있다. 상수는 세 가지이다.
SUSPEND: 버퍼 오버플로우 시 전송을 중단한다.
DROP_OLDEST: 버퍼 오버플로우 시 버퍼에 있는 값 중 가장 오래된 값을 삭제하고, 새로운 값을 추가한다. 중단하지 않는다.
DROP_LATEST: 버퍼 오버플로우 시 버퍼에 추가를 시도하는 가장 최신 값을 드롭한다. 즉, 새롭게 전송되는 값을 버퍼에 추가하지 않는다. 따라서 버퍼 내용이 변경되지 않는다. 중단하지 않는다.
하나씩 어떻게 동작하는지 코드와 그림을 통해서 살펴보자.
SUSPEND
Buffered_Channel_ex01.kt에 onBufferOverflow 파라미터만 설정해 준다.
실행 결과는 Buffered_Channel_ex01.kt와 똑같다. 왜 그럴까?
Try to send: 0
Sent: 0
Try to send: 1
Sent: 1
Try to send: 2
Sent: 2
Try to send: 3
Sent: 3
Try to send: 4
Sent: 4
Try to send: 5
Channel() 함수의 파라미터를 자세히 보면 onBufferOverflow는 기본값이 BufferOverflow.SUSPEND로 지정된 디폴트 파라미터다. 따라서 동일한 결과를 갖는다. 앞서 이야기했듯이 send()를 10회 시도하지만 5회에서 버퍼가 가득 차기 때문에 오버플로우 되는 6회에서 중단된다. 그래서 Sent: 5가 실행되지 않은 것이다.
DROP_OLDEST
DROP_OLDEST의 동작은 크게 두 가지로 나눌 수 있다.
1. 카지노 가입 쿠폰 오버플로우 시 카지노 가입 쿠폰에 있는 값 중 가장 오래된 값을 삭제한다.
2. 새로운 값을 추가한다.
다음 코드를 보자.
실행 결과는 다음과 같다.
Try to send: 0
Sent: 0
Try to send: 1
Sent: 1
Try to send: 2
Sent: 2
Try to send: 3
Sent: 3
Try to send: 4
Sent: 4
Try to send: 5
Sent: 5
Try to send: 6
Sent: 6
Try to send: 7
Sent: 7
Try to send: 8
Sent: 8
Try to send: 9
Sent: 9
Received: 5
Received: 6
Received: 7
Received: 8
Received: 9
버퍼 크기가 5이기 때문에 5회 반복까지는 버퍼에 값이 순서대로 추가된다. 그 후 6회 반복부터 오버플로우가 발생한다. 이때는 버퍼에 있는 값 중 가장 오래된 값, 즉 가장 먼저 들어온 값인 0을 제거한다. 0이 제거되면 가장 오래된 값은 1이 된다. 따라서 7회 반복에서는 1이 제거 대상이 된다. 이런 식으로 가장 오래된 값이 제거되고 새로운 값이 추가되기 때문에 중단이 없는 것이다. 이렇게 10회 반복되는 과정을 그림으로 나타내면 다음과 같다.
최종 결과 버퍼에는 [5,6,7,8,9]의 값이 들어있게 된다. (이 작업이 1초 이내에 수행된다는 것을 가정한다.) 따라서 수신되는 값은 5, 6, 7, 8, 9가 된다.
DROP_LATEST
DROP_LATEST는 버퍼 오버플로우 시 새로운 값을 버퍼에 추가하지 않는다.
다음 코드를 보자.
실행 결과는 다음과 같다.
Try to send: 0
Sent: 0
Try to send: 1
Sent: 1
Try to send: 2
Sent: 2
Try to send: 3
Sent: 3
Try to send: 4
Sent: 4
Try to send: 5
Sent: 5
Try to send: 6
Sent: 6
Try to send: 7
Sent: 7
Try to send: 8
Sent: 8
Try to send: 9
Sent: 9
Received: 0
Received: 1
Received: 2
Received: 3
Received: 4
카지노 가입 쿠폰 크기가 5이기 때문에 5회 반복까지는 카지노 가입 쿠폰에 값이 순서대로 추가된다. 그 후 6회 반복부터 오버플로우가 발생한다. 전송이 중단되는 것은 아니다. 다만 이때부터는 카지노 가입 쿠폰에 추가하려는 값들이 드롭된다. 따라서 카지노 가입 쿠폰에 새로운 값이 추가되지 않는다. 이렇게 10회 반복되는 과정을 그림으로 나타내면 다음과 같다.
최종 결과 버퍼에는 [0,1,2,3,4]의 값이 들어있게 된다. (이 작업이 1초 이내에 수행된다는 것을 가정한다.) 따라서 수신되는 값은 0, 1, 2, 3, 4가 된다.
이제 다시 미리 정의된 채널 유형을 살펴보자.
RENDEZVOUS
RENDEZVOUS 채널은 버퍼 크기가 0인 채널이다. 따라서 송신자와 수신자가 페어로 만나야 값이 전송된다. 그렇기 때문에 전송에 동기화가 필요할 때 사용된다.
RENDEZVOUS 채널을 생성하는 코드를 보면 onBufferOverflow가 기본값인 SUSPEND일 때는 onBufferOverflow에 대한 설정 없이 BufferedChannel(RENDEZVOUS, onUndeliveredElement)을 통해서 채널이 생성된다. 이유는 RENDEZVOUS 채널이 버퍼를 사용하지 않기 때문이다. 버퍼를 사용하지 않으니 버퍼 오버플로우가 발생할 수 없다. 그러므로 onBufferOverflow 파라미가 필요하지 않다. 코루틴 무료 카지노 게임사용했던 형태이다. SUSPEND가 아닐 때는 ConflatedBufferedChannel(1, onBufferOverflow, onUndeliveredElement)를 호출한다. 이때는 버퍼가존재하며 카지노 가입 쿠폰 크기는1이다. 따라서 capacity를 Channel.Factory.RENDEZVOUS로 설정했다고 하여 무조건 버퍼가 존재하지 않는 것이 아님에 유의해야 한다.
CONFLATED
CONFLATED 채널은 버퍼 크기가 1이며 버퍼에 최신 값만 유지하는 채널이다. 새로운 값이 기존 값을 덮어쓴다는 것이다. 이것은 크기가 1인 버퍼에서 오버플로우 시 카지노 가입 쿠폰에 있는 값 중 가장 오래된 값을 삭제한다는 것과 같은 의미다. 익숙한 표현이지 않은가? 바로 DROP_OLDEST의 동작과 같다. 그렇기 때문에 CONFLATED을 사용하지 않아도 직접 같은 채널을 만들 수 있다.
Channel<Int(capacity = Channel.Factory.CONFLATED, onBufferOverflow = BufferOverflow.SUSPEND)
Channel<Int(capacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
이 두 채널은 결국 동일한 동작을 하는 채널이다. CONFLATED 채널을 생성하는 코드를 자세히 보면 내부적으로 결국 위 코드 중 후자의 형태를 띠고 있는 것을 알 수 있다.
onBufferOverflow가 항상 DROP_OLDEST가 되어야 하기 때문에 기본 파라미터 값인 SUSPEND 외의 값을 사용하려고 하면 예외가 발생하도록 처리되어 있다.직관적인 API 구조는 아니다.결국 다음과 같이 쓰라는 말이다.
val channel = Channel<Int(capacity = Channel.Factory.CONFLATED)
UNLIMITED
UNLIMITED 채널은 말 그대로 버퍼 크기가 무제한인 채널이다. 사실 무제한이라는 개념은 불가능하다. 실제로 할당하는 capacity는 Int.MAX_VALUE이다. 또한 메모리가 허용하는 이상의 버퍼를 가질 수는 없을 것이다. 개념적으로 버퍼가 무제한이니 버퍼 오버플로우가 발생하지 않을 것이기 때문에 onBufferOverflow 파라미터가 필요하지 않다.
BUFFERED
BUFFERED 채널은 고정 크기를 가진 채널이다. 기본값은 64이다. (JVM에서 시스템 속성을 통해 재정의할 수 있다.)
BUFFERED 채널의 내부 코드를 보면 onBufferOverflow가 SUSPEND인 경우에는 크기가 기본값으로 설정된다. 이때는 버퍼 오버플로우 시 중단되기 때문에 별도로 onBufferOverflow를 설정할 필요가 없다.
여기까지는 괜찮다. 하지만 다음 부분은 개인적으로 다소 의아하다. SUSPEND가 아닌 경우에는 무조건 버퍼 크기를 1로 만든다. 이것은 API 사용자 입장에서는 내부 코드를 보지 않으면 알 수 없다. 이것이 올바른 API 설계 인가에 대해서 의문이 남는다.
BUFFERED 채널이 기본값으로 크기 64의 버퍼를 가지는 채널이라고 알고 있는 상태라면, 예를 들어 아래와 같이 채널을 생성하면서 기대할 수 있는 것은 무엇일까?
val channel = Channel<Int(capacity = Channel.Factory.BUFFERED, onBufferOverflow = BufferOverflow.DROP_OLDEST)
아마 이렇게 선언하는 것과 같다고 생각할 수 있지 않을까?
val channel = Channel<Int(capacity = 64, onBufferOverflow = BufferOverflow.DROP_OLDEST)
물론 API 문서에 설명이 되어 있기 때문에 문서를 읽고 사용한다면 오해의 여지가 없다. 하지만 여전히 개인적으로는 놀람 최소화 원칙에 위배된다고 생각한다.
요약하자.
- onBufferOverflow는 채널의 카지노 가입 쿠폰가 가득 찼을 때 어떻게 처리할 것인지 결정할 수 있는 파라미터이다.
- SUSPEND: 버퍼 오버플로우 시 전송을 중단한다.
- DROP_OLDEST: 버퍼 오버플로우 시 버퍼에 있는 값 중 가장 오래된 값을 삭제하고, 새로운 값을 추가한다. 중단하지 않는다.
- DROP_LATEST: 버퍼 오버플로우 시 버퍼에 추가를 시도하는 가장 최신 값을 드롭한다. 즉, 새롭게 전송되는 값을 버퍼에 추가하지 않는다. 따라서 버퍼 내용이 변경되지 않는다. 중단하지 않는다.
- RENDEZVOUS 채널은 버퍼 크기가 0인 채널이다. 따라서 송신자와 수신자가 페어로 만나야 값이 전송된다.
- CONFLATED 채널은 버퍼 크기가 1이며 버퍼에 최신 값만 유지하는 채널이다.
- UNLIMITED 채널은 말 그대로 버퍼 크기가 무제한인 채널이다.
- BUFFERED 채널은 기본값은 64의 고정 크기를 가진 채널이다.