<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>카샤의 만개시기</title>
    <link>https://skasha.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Sun, 19 Jul 2026 20:37:40 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>SKaSha</managingEditor>
    <image>
      <title>카샤의 만개시기</title>
      <url>https://tistory1.daumcdn.net/tistory/3028403/attach/168c0cc603124cc3b7f5e21f776b9936</url>
      <link>https://skasha.tistory.com</link>
    </image>
    <item>
      <title>Go언어, 맵(Map)의 성능과 메모리 함정</title>
      <link>https://skasha.tistory.com/126</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 실수 패턴 1: 비효율적인 맵 초기화로 인한 성능 저하  &lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대부분의 Go 개발자는 맵을 사용할 때 다음과 같이 초기화합니다.&lt;/p&gt;
&lt;pre class=&quot;go&quot;&gt;&lt;code&gt;m := make(map[string]int) // 크기 힌트 없음&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드는 당장은 문제가 없어 보이지만, 맵에 &lt;b&gt;대량의 데이터&lt;/b&gt;를 채워 넣을 때 성능 문제를 일으킬 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;맵의 내부 구조: 동적 확장과 Rehash&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Go 맵은 내부적으로 &lt;b&gt;해시 테이블(Hash Table)&lt;/b&gt;로 구현되어 있으며, 데이터를 저장하기 위해 &lt;b&gt;버킷(Bucket)&lt;/b&gt;이라는 메모리 공간을 사용합니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;초기 상태:&lt;/b&gt; 크기 힌트 없이 맵을 생성하면, 런타임은 &lt;b&gt;최소한의 버킷&lt;/b&gt;만 할당하여 시작합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;항목 추가 및 로드 팩터 초과:&lt;/b&gt; 키-값 쌍이 계속 추가되어 맵의 &lt;b&gt;로드 팩터(Load Factor)&lt;/b&gt;, 즉 (총 항목수 / 총 버킷수)가 약 &lt;b&gt;6.5&lt;/b&gt;를 초과하게 됩니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Rehashing (재배치):&lt;/b&gt; 맵이 이 임계값을 초과하면, 성능 유지를 위해 런타임은 &lt;b&gt;버킷 배열의 크기를 두 배로 늘리고&lt;/b&gt; (예: 8개 -&amp;gt; 16개), &lt;b&gt;기존 데이터를 새 버킷 배열로 모두 복사하고 재배치&lt;/b&gt;하는 무거운 작업, 즉 &lt;b&gt;Rehashing&lt;/b&gt;을 수행합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 &lt;b&gt;잦은 Rehashing&lt;/b&gt;은 메모리 할당, 데이터 복사, 해시값 재확인 등의 비용이 많이 드는 작업이므로, 대용량 데이터를 처리할 때 프로그램 전체의 속도를 눈에 띄게 늦춥니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  솔루션: 크기 힌트 제공하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;맵에 들어갈 항목의 대략적인 개수(N)를 알고 있다면, &lt;code&gt;make&lt;/code&gt; 함수에 이 크기를 힌트로 제공하여 &lt;b&gt;초기 버킷 크기를 미리 확보&lt;/b&gt;해야 합니다.&lt;/p&gt;
&lt;pre class=&quot;go&quot;&gt;&lt;code&gt;const expectedCount = 10000

// 효율적인 초기화: 미리 공간을 확보하라는 힌트 제공
m := make(map[string]int, expectedCount) &lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면, 런타임은 10000개의 항목을 수용할 수 있는 충분한 초기 버킷을 할당하므로, 데이터 추가 시 발생하는 &lt;b&gt;잦은 Rehashing 작업을 대폭 줄여&lt;/b&gt; 성능을 극대화할 수 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 실수 패턴 2: 맵과 관련된 논리적 메모리 누수 (Logical Memory Leak)  &lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 실수는 Go의 &lt;b&gt;가비지 컬렉터(GC)&lt;/b&gt;가 맵 메모리를 해제하는 방식에 대한 오해에서 비롯됩니다. 전통적인 메모리 누수(참조를 잃어 GC가 접근할 수 없는 메모리)는 아니지만, &lt;b&gt;메모리가 불필요하게 점유&lt;/b&gt;되는 현상입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;맵 메모리 축소의 부재&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Go 맵의 핵심적인 특징은 다음과 같습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;맵의 버킷은 한번 할당되면, 항목이 모두 삭제되어도 내부적으로 크기가 줄어들지 않는다.&quot;&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;데이터 추가 시:&lt;/b&gt; 맵이 커지면서 내부 버킷 배열의 크기가 늘어납니다 (위에서 설명한 Rehashing을 통해). 이 버킷들은 &lt;b&gt;힙 메모리&lt;/b&gt;에 할당됩니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;데이터 삭제 시:&lt;/b&gt; &lt;code&gt;delete(m, key)&lt;/code&gt;를 사용하여 맵의 모든 항목을 삭제해도, &lt;b&gt;버킷 배열 자체&lt;/b&gt;는 그대로 남아있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;GC의 역할:&lt;/b&gt; GC는 &lt;b&gt;참조가 없는 객체&lt;/b&gt;를 회수합니다. 맵 객체 자체는 여전히 존재하고 참조가 유효하므로, 맵이 사용했던 &lt;b&gt;큰 버킷 메모리&lt;/b&gt;는 GC의 대상이 되지 않고 &lt;b&gt;메모리상에 고착&lt;/b&gt;됩니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;문제 상황 시뮬레이션&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스 트래픽이 급증하여 맵에 수백만 개의 캐시 데이터가 일시적으로 저장되었다고 가정해 봅시다. 트래픽이 감소하고 모든 캐시를 삭제해도, 프로세스의 &lt;b&gt;메모리 사용량은 거의 줄어들지 않는&lt;/b&gt; 상황이 발생합니다. 이는 이전 트래픽 최고점에서 할당했던 거대한 버킷 공간이 그대로 유지되기 때문입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  솔루션: 주기적인 맵 재빌드 (Rebuilding)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;불필요하게 커진 맵의 메모리를 회수하는 가장 확실한 방법은 &lt;b&gt;새로운 맵을 생성하고 참조를 옮기는 것&lt;/b&gt;입니다.&lt;/p&gt;
&lt;pre class=&quot;go&quot;&gt;&lt;code&gt;func clearAndRebuildMap(oldMap map[string]string) map[string]string {
    // 1. 비어있는 새 맵을 생성합니다.
    newMap := make(map[string]string)

    // 2. 만약 일부 데이터만 유지해야 한다면, 새 맵으로 복사합니다.
    // for k, v := range oldMap {
    //     if shouldKeep(k) {
    //         newMap[k] = v
    //     }
    // }

    // 3. 기존 맵(oldMap)은 참조를 잃고 GC 대상이 됩니다.
    // 함수가 종료되면 oldMap은 해제되어 점유했던 메모리가 회수됩니다.
    return newMap
}

// 사용 예시:
largeMap = clearAndRebuildMap(largeMap)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주기적인 재빌드를 통해 맵이 필요로 하는 메모리만 사용하도록 관리해야 합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Go 맵 내부 작동 심층 분석: 버킷과 해시 비트  &lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;맵의 효율성과 메모리 관리를 완전히 이해하려면 &lt;b&gt;버킷이 어떻게 선택&lt;/b&gt;되는지 알아야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1. 버킷 개수 결정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;버킷 개수는 항상 &lt;b&gt;2의 거듭제곱&lt;/b&gt;(2^B) 형태를 유지합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;초기화:&lt;/b&gt; 맵 생성 시, 힌트(N)를 기반으로 N을 수용할 수 있는 최소한의 2^B를 찾아 할당합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;확장:&lt;/b&gt; 로드 팩터 6.5를 초과하면, B가 1 증가하여 버킷 개수가 &lt;b&gt;2배&lt;/b&gt;가 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.2. 버킷 인덱스 결정 알고리즘 (B-Bit Masking)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 키의 데이터가 특정 버킷에 저장될지는 키의 &lt;b&gt;해시값&lt;/b&gt;을 이용해 &lt;b&gt;비트 연산&lt;/b&gt;으로 매우 빠르게 결정됩니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;전체 해시값:&lt;/b&gt; 키 K에 대해 64비트 해시값 Hash(K)를 계산합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;마스크 생성:&lt;/b&gt; 현재 버킷 개수 2^B에 해당하는 &lt;b&gt;마스크 M&lt;/b&gt;을 만듭니다.&lt;br /&gt;M = 2^B - 1&lt;br /&gt;(예: B=4이면 M=15, 즉 이진수로 &lt;code&gt;...00001111&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;인덱스 추출:&lt;/b&gt; 해시값과 마스크를 &lt;b&gt;비트 AND(&amp;amp;) 연산&lt;/b&gt;합니다.&lt;br /&gt;버킷 인덱스 = Hash(K) &amp;amp; M&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;결론:&lt;/b&gt; &lt;b&gt;해시값의 최하위 B 비트&lt;/b&gt;가 바로 해당 키가 저장될 &lt;b&gt;버킷의 인덱스&lt;/b&gt;가 됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.3. 해시 충돌과 오버플로&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여러 키의 해시값이 &lt;b&gt;최하위 B 비트가 같다면&lt;/b&gt;, 이들은 모두 &lt;b&gt;같은 버킷 인덱스&lt;/b&gt;로 매핑됩니다. (이것이 &lt;b&gt;충돌&lt;/b&gt;입니다.)&lt;/li&gt;
&lt;li&gt;하나의 버킷은 기본적으로 &lt;b&gt;8개의 슬롯&lt;/b&gt;을 가집니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;오버플로:&lt;/b&gt; 8개의 슬롯이 모두 채워지면, 런타임은 &lt;b&gt;별도의 오버플로 버킷&lt;/b&gt;을 할당하여 기존 버킷에 &lt;b&gt;연결 리스트(체인)&lt;/b&gt; 형태로 붙입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;요약 및 핵심 정리 ✨&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;문제 원인&lt;/td&gt;
&lt;td&gt;해결책&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;맵 크기를 미리 지정하지 않아 잦은 &lt;b&gt;Rehashing&lt;/b&gt; 발생 =&amp;gt; &lt;b&gt;성능 저하&lt;/b&gt;.&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;make(map[K]V, 예상크기)&lt;/code&gt;로 &lt;b&gt;크기 힌트&lt;/b&gt;를 제공하여 초기 버킷 수를 확보.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;항목 삭제 후에도 &lt;b&gt;버킷 배열 메모리가 회수되지 않음&lt;/b&gt; =&amp;gt; &lt;b&gt;메모리 사용량 고착&lt;/b&gt;.&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;일정 기간 후 &lt;b&gt;새 맵을 생성&lt;/b&gt;하여 기존 맵을 &lt;b&gt;강제로 GC 대상&lt;/b&gt;으로 만듦.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;</description>
      <category>Go언어</category>
      <category>B-Bit Masking</category>
      <category>Bucket</category>
      <category>go언어</category>
      <category>Map</category>
      <category>버킷</category>
      <category>버킷 인덱스 결정 알고리즘</category>
      <author>SKaSha</author>
      <guid isPermaLink="true">https://skasha.tistory.com/126</guid>
      <comments>https://skasha.tistory.com/126#entry126comment</comments>
      <pubDate>Wed, 29 Oct 2025 12:35:38 +0900</pubDate>
    </item>
    <item>
      <title>ConcurrentHashMap</title>
      <link>https://skasha.tistory.com/125</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. ConcurrentHashMap이란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ConcurrentHashMap은 멀티스레드 환경에서 동시에 여러 스레드가 안전하고 빠르게 데이터를 읽고 쓸 수 있도록 설계된 해시맵입니다.&lt;br /&gt;기존의 HashMap은 스레드에 안전하지 않아 동시 접근 시 데이터 손상이나 무한 루프 같은 심각한 문제가 발생하지만, ConcurrentHashMap은 이를 해결했습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. ConcurrentHashMap 동작 원리&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-1. 세분화된 락 (Lock Striping)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바 7 이전에는 ConcurrentHashMap이 내부적으로 데이터를 여러 &lt;b&gt;세그먼트(segment)&lt;/b&gt;로 나누고, 각 세그먼트별로 락을 걸어 동시 접근을 허용했습니다.&lt;br /&gt;이 덕분에 전체 맵이 아닌 해당 세그먼트에만 락이 걸려, 동시성 성능이 크게 향상됐죠.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-2. 자바 8 이후: 락 대신 CAS + 버킷별 동기화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바 8부터는 내부 구조가 크게 바뀌어, &lt;b&gt;CAS(Compare-And-Swap)&lt;/b&gt;라는 하드웨어 원자 연산과 버킷 단위의 락만 사용합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CAS는 락(lock)을 쓰지 않는(non-blocking), 원자적(atomic) 연산 방식의 하나입니다.&lt;/li&gt;
&lt;li&gt;CAS는 CPU 명령어 수준에서 &amp;ldquo;현재 값이 예상값(oldValue)과 같으면 새 값(newValue)으로 바꿔라&amp;rdquo;를 한 번에 처리하는 원자적 연산입니다. (낙관적 동시성)&lt;/li&gt;
&lt;li&gt;만약 예상값과 메모리에 있는 실제 값이 다르면, 교체를 하지 않고 연산은 실패로 돌아가며 보통 다시 시도(retry)합니다.&lt;/li&gt;
&lt;li&gt;하드웨어나 OS/CPU 명령 수준에서 지원하는 경우가 많고, Java는 Unsafe나 JVM 내부 + Atomic* 클래스들을 통해 CAS를 제공합니다.&lt;/li&gt;
&lt;li&gt;이 덕분에 락 없이도 값을 안전하게 갱신할 수 있어, 락 경쟁을 줄이고 성능을 극대화했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2-2-1.&lt;span&gt; ConcurrentHashMap이 락&amp;nbsp;없이&amp;nbsp;원자적으로&amp;nbsp;동작하는&amp;nbsp;원리&lt;/span&gt;&lt;/h4&gt;
&lt;p data-end=&quot;145&quot; data-start=&quot;120&quot; data-ke-size=&quot;size16&quot;&gt;1. CAS의 하드웨어 수준 원자적 연산&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;498&quot; data-start=&quot;147&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;260&quot; data-start=&quot;147&quot;&gt;&lt;b&gt;CPU 명령어(CPU atomic instruction)&lt;/b&gt;&lt;br /&gt;CAS는 CPU가 직접 지원하는 &lt;b&gt;원자적 명령어&lt;/b&gt;입니다. 예를 들어 x86 계열의 CMPXCHG 명령어가 대표적이죠.&lt;/li&gt;
&lt;li data-end=&quot;379&quot; data-start=&quot;261&quot;&gt;&lt;b&gt;동작 원리&lt;/b&gt;&lt;br /&gt;CPU는 메모리 특정 위치에 대해 &quot;현재 값이 예상값이면, 새 값으로 교체&quot;를 한 사이클 내에 수행합니다. 이 사이에 다른 스레드가 개입할 수 없으니, 완전한 원자성을 보장합니다.&lt;/li&gt;
&lt;li data-end=&quot;498&quot; data-start=&quot;380&quot;&gt;&lt;b&gt;특징&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;498&quot; data-start=&quot;393&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;426&quot; data-start=&quot;393&quot;&gt;하드웨어가 직접 지원하므로 매우 빠르고 신뢰성이 높음&lt;/li&gt;
&lt;li data-end=&quot;461&quot; data-start=&quot;429&quot;&gt;JVM이나 OS 커널에서 별도의 락을 걸 필요 없음&lt;/li&gt;
&lt;li data-end=&quot;498&quot; data-start=&quot;464&quot;&gt;멀티코어 환경에서도 메모리 동기화를 하드웨어 수준에서 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;530&quot; data-start=&quot;505&quot; data-ke-size=&quot;size16&quot;&gt;2. Java에서 CAS를 제공하는 방식&lt;/p&gt;
&lt;p data-end=&quot;585&quot; data-start=&quot;532&quot; data-ke-size=&quot;size16&quot;&gt;자바는 직접 하드웨어 명령어를 호출하지 않고도 CAS를 쓸 수 있게 여러 추상화 계층을 둡니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1127&quot; data-start=&quot;587&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;804&quot; data-start=&quot;587&quot;&gt;&lt;b&gt;sun.misc.Unsafe 클래스&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;804&quot; data-start=&quot;619&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;654&quot; data-start=&quot;619&quot;&gt;JVM 내부에 존재하는 비공개(low-level) API&lt;/li&gt;
&lt;li data-end=&quot;698&quot; data-start=&quot;657&quot;&gt;하드웨어 CAS 명령어를 JNI나 JVM 내에서 호출하도록 연결해줌&lt;/li&gt;
&lt;li data-end=&quot;737&quot; data-start=&quot;701&quot;&gt;직접 메모리 접근과 CAS 같은 원자적 연산을 가능하게 함&lt;/li&gt;
&lt;li data-end=&quot;804&quot; data-start=&quot;740&quot;&gt;Java 9부터는 공식 API가 아닌 jdk.internal.misc.Unsafe로 이전되고 사용이 제한적임&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;999&quot; data-start=&quot;806&quot;&gt;&lt;b&gt;java.util.concurrent.atomic 패키지 (AtomicInteger, AtomicReference 등)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;999&quot; data-start=&quot;885&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;922&quot; data-start=&quot;885&quot;&gt;Unsafe API를 감싸 사용하기 쉽게 만든 고수준 클래스&lt;/li&gt;
&lt;li data-end=&quot;963&quot; data-start=&quot;925&quot;&gt;내부에서 Unsafe의 CAS 기능을 호출해 원자적 연산 수행&lt;/li&gt;
&lt;li data-end=&quot;999&quot; data-start=&quot;966&quot;&gt;멀티스레드 환경에서 락 없이 안전한 동시성 제어 지원&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;1127&quot; data-start=&quot;1001&quot;&gt;&lt;b&gt;JVM 내부 역할&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1127&quot; data-start=&quot;1021&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1078&quot; data-start=&quot;1021&quot;&gt;JVM은 Unsafe를 통해 하드웨어 CAS 명령어와 자바 객체 메모리를 연결하는 역할 수행&lt;/li&gt;
&lt;li data-end=&quot;1127&quot; data-start=&quot;1081&quot;&gt;JIT(Just-In-Time) 컴파일러가 CAS 연산을 CPU 명령어로 최적화&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;1170&quot; data-start=&quot;1134&quot; data-ke-size=&quot;size16&quot;&gt;3. ConcurrentHashMap이 사용하는 CAS 방식&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1561&quot; data-start=&quot;1172&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1258&quot; data-start=&quot;1172&quot;&gt;&lt;b&gt;ConcurrentHashMap&lt;/b&gt;은 자바 8 이상에서 내부적으로 Unsafe와 Atomic* 클래스를 조합해 CAS 연산을 수행합니다.&lt;/li&gt;
&lt;li data-end=&quot;1434&quot; data-start=&quot;1259&quot;&gt;구체적으로는:
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1434&quot; data-start=&quot;1273&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1377&quot; data-start=&quot;1273&quot;&gt;내부 구조인 &lt;b&gt;버킷 배열 요소&lt;/b&gt;나 &lt;b&gt;노드의 값 참조&lt;/b&gt;에 대해 Unsafe의 CAS 메서드 (compareAndSwapObject 등)를 호출해 원자적 갱신을 처리합니다.&lt;/li&gt;
&lt;li data-end=&quot;1434&quot; data-start=&quot;1380&quot;&gt;별도의 락 없이 값 업데이트를 시도하고, 실패 시 재시도하는 방식으로 동시성을 보장합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;1519&quot; data-start=&quot;1435&quot;&gt;즉, &lt;b&gt;하드웨어 수준의 원자적 명령어를 JVM이 Unsafe를 통해 호출하고, ConcurrentHashMap이 이를 활용하는 구조&lt;/b&gt;입니다.&lt;/li&gt;
&lt;li data-end=&quot;1561&quot; data-start=&quot;1520&quot;&gt;이 덕분에 락을 최소화하면서도 스레드 안전한 동시성 구현이 가능합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2-2-2. CAS 사용 장단점 및 문제점&lt;/h4&gt;
&lt;p data-end=&quot;990&quot; data-start=&quot;983&quot; data-ke-size=&quot;size18&quot;&gt;장점&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1148&quot; data-start=&quot;992&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1056&quot; data-start=&quot;992&quot;&gt;락을 안 쓰므로, &lt;b&gt;데드락(lock deadlock)&lt;/b&gt; 같은 위험이 없고, 락 대기(wait) 비용도 없음.&lt;/li&gt;
&lt;li data-end=&quot;1084&quot; data-start=&quot;1057&quot;&gt;경쟁(충돌)이 적을 경우 매우 빠르게 동작함.&lt;/li&gt;
&lt;li data-end=&quot;1148&quot; data-start=&quot;1085&quot;&gt;주로 락-프리(lock-free) 혹은 낙관적 동시성(optimistic concurrency) 방식에 쓰임.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;1163&quot; data-start=&quot;1150&quot; data-ke-size=&quot;size18&quot;&gt;단점 / 주의점&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1548&quot; data-start=&quot;1165&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1256&quot; data-start=&quot;1165&quot;&gt;&lt;b&gt;재시도 비용&lt;/b&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;CAS가 실패하면(즉, 예상값과 실제값이 다르면), 보통 루프를 돌면서 재시도를 해야 함 &amp;rarr; 바쁜 대기(spin)가 반복될 수 있음.&lt;/li&gt;
&lt;li data-end=&quot;1450&quot; data-start=&quot;1258&quot;&gt;&lt;b&gt;ABA 문제&lt;/b&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;예: 어떤 위치의 값이 A &amp;rarr; B &amp;rarr; A 로 바뀌었다면, 처음 A와 최종 A는 같아 보이므로 CAS가 &amp;ldquo;아직도 A였구나&amp;rdquo; 하고 허락해 버릴 수 있음. A &amp;rarr; B &amp;rarr; A 과정을 무시한다는 뜻.&lt;br /&gt;&amp;nbsp;&amp;nbsp;이를 막기 위해 버전 태그(version tag)나 포인터 + 버전 정보를 같이 갖는 방식 등이 사용되기도 함.&lt;/li&gt;
&lt;li data-end=&quot;1548&quot; data-start=&quot;1452&quot;&gt;&lt;b&gt;복잡한 연산에는 부적합&lt;/b&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;여러 필드를 동시에 바꿔야 한다거나 복잡한 구조 변경이 필요하면, CAS만으로 처리하기 어렵고 락이나 다른 동기화 기법이 필요해짐.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 충돌이 많아지는 버킷: 연결 리스트에서 트리 구조로 변화&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 해시맵은 같은 해시값을 가진 키들이 &lt;b&gt;버킷&lt;/b&gt; 안에서 연결 리스트 형태로 저장됩니다.&lt;br /&gt;하지만 충돌이 많이 발생해 연결 리스트가 길어지면, 조회 성능이 선형적으로 느려집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ConcurrentHashMap은 &lt;b&gt;연결 리스트가 일정 길이(기본 8개) 이상이 되면 이를 빨간-검정 트리(Red-Black Tree) 구조로 바꿔&lt;/b&gt; 조회 성능을 로그 시간(log n)으로 최적화합니다.&lt;br /&gt;이 변화는 자바 8부터 적용된 최적화입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. CAS가 락 없이 원자적으로 동작하는 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CAS 연산은 CPU의 특수 명령어(CMPXCHG)를 사용해, 메모리의 특정 위치 값을 &lt;b&gt;예상값과 비교 후 교체하는 작업을 하나의 원자적 명령어로 수행&lt;/b&gt;합니다.&lt;br /&gt;이 과정은 하드웨어 레벨에서 처리되므로, 별도의 락 없이도 안전하게 값을 업데이트할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 여러 스레드가 동시에 같은 키에 put을 시도해도, CAS 연산이 성공할 때까지 재시도하는 방식으로 안전성을 확보하는 것이죠.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. ConcurrentHashMap에서 Lock과 Get 요청은 어떻게 공존하나?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;put 작업 중에 락이 걸린 상태라도, &lt;b&gt;다른 스레드의 get 요청은 락에 방해받지 않고 바로 처리&lt;/b&gt;됩니다.&lt;br /&gt;이는 ConcurrentHashMap이 내부적으로 락을 걸더라도, 읽기 작업은 락 없이도 일관성 있게 값을 조회할 수 있도록 설계되었기 때문입니다.&lt;br /&gt;그래서 쓰기 락이 있어도 읽기 성능은 거의 저하되지 않습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. ConcurrentHashMap 주요 특징 정리&lt;/h2&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 151px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;&lt;b&gt;항목&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;&lt;b&gt;설명&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;동시성 제어 방식&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;CAS + 버킷 단위 락 (자바 8 이상)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;내부 데이터 구조&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;배열 + 연결 리스트 + 트리(Red-Black Tree)로 충돌 대응&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;읽기 작업 성능&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;락 없이 처리, 매우 빠름&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;쓰기 작업 성능&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;버킷 단위 락 사용, 락 범위가 좁아 병목 최소화&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;충돌 처리&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;일정 개수 이상 충돌 시 연결 리스트 &amp;rarr; 트리 구조로 변경&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;주요 메서드&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;put(), get(), remove(), computeIfAbsent()&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;활용 예&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;웹 세션 관리, 캐시 구현, 멀티스레드 데이터 공유&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Java/POJO</category>
      <category>ConcurrentHashMap</category>
      <author>SKaSha</author>
      <guid isPermaLink="true">https://skasha.tistory.com/125</guid>
      <comments>https://skasha.tistory.com/125#entry125comment</comments>
      <pubDate>Mon, 29 Sep 2025 00:43:57 +0900</pubDate>
    </item>
    <item>
      <title>HashMap / Hashtable / ConcurrentHashMap: null 허용 규칙</title>
      <link>https://skasha.tistory.com/124</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Java에서 &lt;span&gt;Map&lt;/span&gt;을 쓸 때 &lt;span&gt;&lt;b&gt;키/값에서 null을 허용하는지 여부&lt;/b&gt;&lt;/span&gt;에 대해서 알아보고자 합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;한눈에 보기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 114px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;b&gt;컬렉션&lt;/b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;&lt;span&gt;null&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;키&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;&lt;span&gt;null&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;값&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;b&gt;메모&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 38px;&quot;&gt;
&lt;td style=&quot;height: 38px;&quot;&gt;&lt;span&gt;&lt;b&gt;HashMap&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 38px;&quot;&gt;&lt;span&gt;✅ (정확히 1개)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 38px;&quot;&gt;&lt;span&gt;✅ (여러 개 가능)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 38px;&quot;&gt;&lt;span&gt;&lt;span&gt;get(k)&lt;/span&gt;가 &lt;span&gt;null&lt;/span&gt;이면 &amp;ldquo;키 없음&amp;rdquo; 또는 &amp;ldquo;값이 &lt;span&gt;null&lt;/span&gt;&amp;rdquo;일 수 있음 &amp;rarr; &lt;span&gt;containsKey(k)&lt;/span&gt;로 구분 필요&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;span&gt;&lt;b&gt;Hashtable&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;span&gt;❌&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;span&gt;❌&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;span&gt;put/get&lt;span&gt;에 &lt;/span&gt;null&lt;span&gt; 사용 시 &lt;/span&gt;NullPointerException&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 38px;&quot;&gt;
&lt;td style=&quot;height: 38px;&quot;&gt;&lt;span&gt;&lt;b&gt;ConcurrentHashMap&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 38px;&quot;&gt;&lt;span&gt;❌&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 38px;&quot;&gt;&lt;span&gt;❌&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 38px;&quot;&gt;&lt;span&gt;동시성&amp;middot;원자적 연산 일관성을 위해 &lt;span&gt;null&lt;/span&gt; 금지 (&lt;span&gt;compute/merge&lt;/span&gt;의 &lt;span&gt;null&lt;/span&gt; 반환은 &lt;i&gt;제거/미삽입 신호&lt;/i&gt;)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;왜 규칙이 다를까?&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1) HashMap &amp;mdash; 편의성 + 현대적 API&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;containsKey&lt;/span&gt;가 있어 &lt;span&gt;get&lt;/span&gt; 결과가 &lt;span&gt;null&lt;/span&gt;이어도 &lt;span&gt;&lt;b&gt;존재 여부&lt;/b&gt;&lt;/span&gt;를 구분 가능&lt;/li&gt;
&lt;li&gt;&lt;span&gt;그래서 &lt;/span&gt;&lt;b&gt;키 1개 null + 여러 null 값&lt;/b&gt;&lt;span&gt;을 허용&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2) Hashtable &amp;mdash; 역사적 설계(모호성 회피)&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;초창기(레거시) 설계에서 &lt;span&gt;get&lt;/span&gt;의 &lt;span&gt;null&lt;/span&gt;은 &amp;ldquo;없음&amp;rdquo; 의미로 사용&lt;/li&gt;
&lt;li&gt;값 자체에 &lt;span&gt;null&lt;/span&gt;을 허용하면 &lt;span&gt;&lt;b&gt;없음 vs null 값&lt;/b&gt;&lt;/span&gt;이 구분 불가 &amp;rarr; &lt;span&gt;&lt;b&gt;아예 금지&lt;/b&gt;&lt;/span&gt;로 일관성 유지&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3) ConcurrentHashMap &amp;mdash; 동시성&amp;middot;원자성 보장&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;락 없는 읽기에서 &lt;span&gt;null&lt;/span&gt;은 &amp;ldquo;부재/제거/미초기화&amp;rdquo;를 나타내는 &lt;span&gt;&lt;b&gt;특수 상태값&lt;/b&gt;&lt;/span&gt;과 충돌&lt;/li&gt;
&lt;li&gt;putIfAbsent&lt;span&gt;, &lt;/span&gt;computeIfAbsent&lt;span&gt;, &lt;/span&gt;merge&lt;span&gt; 같은 &lt;/span&gt;&lt;span&gt;&lt;b&gt;원자 연산의 의미&lt;/b&gt;&lt;/span&gt;&lt;span&gt;를 명확히 하기 위해 &lt;/span&gt;&lt;span&gt;&lt;b&gt;키/값 모두 null 금지&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;실무 가이드 &amp;amp; 모범 사례&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;HashMap&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;get(k)&lt;span&gt;가 &lt;/span&gt;null&lt;span&gt;일 때는 반드시 &lt;/span&gt;containsKey(k)&lt;span&gt;로 존재 확인&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;예: 캐시에서 &amp;ldquo;미계산&amp;rdquo;과 &amp;ldquo;계산했는데 &lt;span&gt;null&lt;/span&gt;&amp;rdquo;을 구분할 때&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ConcurrentHashMap&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;계산해서 채우는 캐시&lt;/b&gt;&lt;/span&gt;&lt;span&gt;라면 &lt;/span&gt;computeIfAbsent&lt;span&gt; 사용&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;compute/merge&lt;/span&gt;&lt;span&gt; 류에서 &lt;/span&gt;&lt;b&gt;리매핑 함수가 null을 반환&lt;/b&gt;&lt;span&gt;하면:&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;compute&lt;/span&gt;: 해당 매핑 &lt;span&gt;&lt;b&gt;제거&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;merge&lt;/span&gt;: 결합 결과가 &lt;span&gt;null&lt;/span&gt;이면 기존 매핑 &lt;span&gt;&lt;b&gt;제거&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span&gt;computeIfAbsent&lt;/span&gt;&lt;span&gt;에서 &lt;/span&gt;&lt;b&gt;매핑 함수가 null 반환&lt;/b&gt;&lt;span&gt;: &lt;/span&gt;&lt;b&gt;아무 것도 저장하지 않고 null 반환&lt;/b&gt;&lt;span&gt;(예외 아님)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;스레드 안전&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;동시 접근이 필요하면 &lt;span&gt;Hashtable&lt;/span&gt; 대신 &lt;span&gt;&lt;b&gt;ConcurrentHashMap 권장&lt;/b&gt;&lt;/span&gt;(성능&amp;middot;API 일관성 우수)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Q1. 왜 HashMap은 null 키를 &amp;ldquo;1개만&amp;rdquo; 허용하나요?&lt;br /&gt;&lt;/b&gt;A. 해시 버킷 인덱싱이 불가능한 &lt;span&gt;null&lt;/span&gt; 키를 &lt;span&gt;&lt;b&gt;특수 처리(고정 슬롯)&lt;/b&gt;&lt;/span&gt; 하기 때문입니다. 자연스럽게 &amp;ldquo;하나만&amp;rdquo; 저장됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Q2. ConcurrentHashMap에서 값으로 Optional&amp;lt;T&amp;gt;를 쓰면 어때요?&lt;br /&gt;&lt;/b&gt;A. 좋은 패턴입니다. &lt;span&gt;null&lt;/span&gt; 금지 정책과도 맞고, &amp;ldquo;부재/값 없음&amp;rdquo;을 명확히 표현할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Q3. Hashtable을 계속 써도 되나요?&lt;br /&gt;&lt;/b&gt;A. 새로운 코드에서는 비권장입니다. 동시성이 필요하면 &lt;span&gt;ConcurrentHashMap&lt;/span&gt;, 불필요하면 &lt;span&gt;HashMap&lt;/span&gt;을 사용하세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Java/POJO</category>
      <category>ConcurrentHashMap</category>
      <category>HashMap</category>
      <category>Hashtable</category>
      <author>SKaSha</author>
      <guid isPermaLink="true">https://skasha.tistory.com/124</guid>
      <comments>https://skasha.tistory.com/124#entry124comment</comments>
      <pubDate>Mon, 29 Sep 2025 00:30:41 +0900</pubDate>
    </item>
    <item>
      <title>SRV 레코드</title>
      <link>https://skasha.tistory.com/123</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;SRV 레코드는 &lt;b&gt;서비스 레코드(Service Record)&lt;/b&gt; 의 줄임말로, DNS(Domain Name System)에서 특정 서비스를 제공하는 서버의 위치를 정의하는 데 사용됩니다. 일반적인 A 레코드와 달리 SRV 레코드는 &lt;b&gt;서비스 이름&lt;/b&gt;, &lt;b&gt;프로토콜&lt;/b&gt;, &lt;b&gt;포트&lt;/b&gt; 및 &lt;b&gt;우선순위&lt;/b&gt; 등을 포함하는 구조를 가지고 있어, 특정 서비스의 위치를 동적으로 찾을 수 있게 해줍니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SRV 레코드의 구조&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SRV 레코드는 DNS에서 특정 서비스가 제공되는 서버의 호스트 이름과 포트 번호를 나타냅니다. SRV 레코드는 다음과 같은 형식을 가집니다:&lt;/p&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;_서비스명._프로토콜.도메인. TTL 클래스 SRV 우선순위 가중치 포트 대상
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 각 항목의 의미:&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;_서비스명&lt;/b&gt;: 제공하는 서비스의 이름입니다. 예를 들어, MongoDB, SIP, XMPP, HTTP 등.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;_프로토콜&lt;/b&gt;: 사용하는 프로토콜입니다. _tcp 또는 _udp가 일반적입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;도메인&lt;/b&gt;: 해당 서비스를 제공하는 도메인 이름입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;TTL&lt;/b&gt;: 이 레코드의 Time To Live(유효 시간)입니다. DNS 캐시에서 얼마 동안 이 정보를 유지할지를 나타냅니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;클래스&lt;/b&gt;: 대부분의 경우 IN으로 설정됩니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;우선순위(Priority)&lt;/b&gt;: 여러 서버가 있을 경우, 클라이언트가 먼저 연결하려는 서버를 결정할 때 사용하는 값입니다. 낮을수록 우선순위가 높습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;가중치(Weight)&lt;/b&gt;: 우선순위가 같은 서버들 간에 부하 분산을 위해 사용하는 값입니다. 높은 값일수록 더 많이 선택됩니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;포트(Port)&lt;/b&gt;: 해당 서비스가 제공되는 포트 번호입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;대상(Host)&lt;/b&gt;: 서비스를 제공하는 서버의 호스트 이름입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;예시:&lt;/h4&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;_mongodb._tcp.example.com. 3600 IN SRV 10 5 27017 mongo1.example.com.
_mongodb._tcp.example.com. 3600 IN SRV 10 5 27017 mongo2.example.com.
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;_mongodb._tcp.example.com.&lt;/b&gt;: MongoDB 서비스가 TCP 프로토콜을 사용하는 example.com 도메인에 있다는 뜻입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;우선순위(10)&lt;/b&gt;: 서버들이 여러 대 있을 때, 우선순위가 낮은 서버(값이 작은)가 먼저 선택됩니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;가중치(5)&lt;/b&gt;: 동일한 우선순위를 가진 여러 서버들 간에 연결 분배를 위한 가중치입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;포트(27017)&lt;/b&gt;: MongoDB의 기본 포트입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;대상(mongo1.example.com)&lt;/b&gt;: 실제 서비스가 제공되는 호스트입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SRV 레코드를 사용하는 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SRV 레코드는 주로 &lt;b&gt;서비스 디스커버리&lt;/b&gt;(Service Discovery) 용도로 사용됩니다. 클라이언트는 SRV 레코드를 조회하여 서버의 위치나 포트를 동적으로 찾아 연결할 수 있습니다. 예를 들어, &lt;b&gt;MongoDB&lt;/b&gt;와 같은 분산 시스템에서는 여러 서버가 있을 수 있기 때문에, 클라이언트가 직접 서버를 지정하는 대신 SRV 레코드를 통해 적절한 서버에 자동으로 연결할 수 있도록 합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;분산 시스템&lt;/b&gt;: 여러 대의 서버를 사용하는 시스템에서, 클라이언트는 DNS를 통해 여러 서버를 탐색하여 연결합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;부하 분산&lt;/b&gt;: 여러 서버에 연결 시, 가중치를 사용하여 트래픽을 분산시킬 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;서비스 변화&lt;/b&gt;: 서버가 변경되거나 추가되었을 때, 클라이언트는 DNS에서 자동으로 변경 사항을 반영합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Foundation/Network</category>
      <category>SRV 레코드</category>
      <author>SKaSha</author>
      <guid isPermaLink="true">https://skasha.tistory.com/123</guid>
      <comments>https://skasha.tistory.com/123#entry123comment</comments>
      <pubDate>Thu, 25 Sep 2025 21:37:24 +0900</pubDate>
    </item>
    <item>
      <title>Go의 고루틴, Kotlin의 코루틴, 그리고 Java 21의 가상 스레드</title>
      <link>https://skasha.tistory.com/122</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. Kotlin 코루틴, Go 고루틴, Java 21의 가상 스레드 요약&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Kotlin 코루틴&lt;/b&gt;: Kotlin은 &lt;b&gt;JVM 기반 언어&lt;/b&gt;로, 비동기 프로그래밍을 처리하는 데 매우 유용한 &lt;b&gt;코루틴&lt;/b&gt;을 도입했습니다. 코루틴은 &lt;b&gt;비동기 처리&lt;/b&gt;를 간결하고 효율적으로 만들어주는 기능으로, 스레드를 차단하지 않고 여러 작업을 동시에 처리할 수 있게 도와줍니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Go 고루틴&lt;/b&gt;: Go는 &lt;b&gt;동시성&lt;/b&gt;을 매우 효율적으로 처리할 수 있는 &lt;b&gt;고루틴&lt;/b&gt;을 제공합니다. 고루틴은 &lt;b&gt;경량 스레드&lt;/b&gt;로, 운영 체제의 스레드를 직접 사용하지 않고, &lt;b&gt;Go 런타임&lt;/b&gt;에서 관리하는 방식으로 동시성 처리를 최적화합니다. Go의 고루틴은 사용하기 간단하며, 동시성 작업을 자동으로 관리해줍니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Java 21 가상 스레드&lt;/b&gt;: Java 21은 &lt;b&gt;가상 스레드&lt;/b&gt;라는 새로운 기능을 도입하여, &lt;b&gt;JVM&lt;/b&gt; 환경에서 &lt;b&gt;경량 스레드&lt;/b&gt;를 사용한 동시성 처리를 가능하게 합니다. 가상 스레드는 &lt;b&gt;OS 스레드를 사용하지 않고&lt;/b&gt; JVM 내에서 효율적으로 관리되며, 많은 수의 동시 작업을 처리할 수 있도록 도와줍니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 세 가지 기술은 모두 &lt;b&gt;경량 스레드&lt;/b&gt;를 사용하여 &lt;b&gt;효율적인 동시성 처리&lt;/b&gt;를 구현하지만, 그 구현 방식은 조금씩 다릅니다.&lt;br /&gt;이 글에서는 각 기술의 특징을 비교하고, 각각이 선택된 이유와 장단점에 대해 살펴보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. Kotlin 코루틴&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.1 &lt;b&gt;코루틴이란?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;코루틴(Coroutine)&lt;/b&gt; 은 비동기 프로그래밍을 효율적으로 처리하기 위한 기법으로, Kotlin에서 제공하는 강력한 기능 중 하나입니다.&lt;br /&gt;코루틴은 스레드와 비동기 작업을 처리할 때 발생할 수 있는 불필요한 자원 낭비를 최소화하면서도, 여러 작업을 동시에 처리할 수 있게 해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코루틴의 가장 큰 특징은, &lt;b&gt;비동기 함수&lt;/b&gt;가 &lt;code&gt;suspend&lt;/code&gt; 키워드를 사용하여 정의된다는 점입니다.&lt;br /&gt;이를 통해 특정 작업을 중단(suspend)하고, 다른 작업을 다시 실행(resume) 할 수 있어, 코드가 블로킹되지 않으며 효율적인 동시성 처리가 가능합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.2 &lt;b&gt;suspend 함수와 비동기 흐름 제어&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kotlin의 코루틴은 &lt;code&gt;suspend&lt;/code&gt; 함수와 함께 사용되며, 이를 통해 &lt;b&gt;비동기 작업&lt;/b&gt;을 처리합니다. suspend 키워드는 &lt;b&gt;비동기 함수&lt;/b&gt;에서 작업을 중단하고, 해당 작업이 끝날 때까지 다른 작업을 처리하도록 허용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 다음과 같은 비동기 함수가 있다고 가정할 수 있습니다:&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;suspend fun fetchDataFromNetwork(): String {  
    delay(1000) // 1초 기다림 (네트워크 요청)  
    return &quot;데이터 수신 완료&quot;  
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드에서 fetchDataFromNetwork 함수는 1초 동안 대기하는 작업을 수행하는데, 이때 스레드를 차단하지 않고 다른 작업을 실행할 수 있습니다. suspend 키워드는 작업을 중단하고 그 흐름을 제어하는 역할을 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kotlin의 코루틴은 비동기 흐름을 직관적이고 가독성 좋게 만들 수 있게 도와줍니다.&lt;br /&gt;이를 통해 콜백 지옥이나 복잡한 상태 관리 없이도 비동기 작업을 효율적으로 처리할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.3 &lt;b&gt;Dispatchers와 withContext 활용&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kotlin 코루틴은 &lt;code&gt;Dispatchers&lt;/code&gt;를 사용하여 코루틴이 실행될 스레드를 지정할 수 있습니다.&lt;br /&gt;예를 들어, &lt;b&gt;UI 스레드&lt;/b&gt;에서 작업을 해야 할 경우나, &lt;b&gt;백그라운드 스레드&lt;/b&gt;에서 비동기 작업을 처리할 경우, Dispatchers를 통해 이를 명시적으로 설정할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;launch(Dispatchers.IO) {
    // 네트워크 요청 등을 백그라운드 스레드에서 처리
    val data = fetchDataFromNetwork()
    withContext(Dispatchers.Main) {
        // UI 스레드에서 결과를 처리
        updateUI(data)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 예시에서 &lt;code&gt;launch(Dispatchers.IO)&lt;/code&gt;는 백그라운드 스레드에서 네트워크 요청을 처리하고, &lt;code&gt;withContext(Dispatchers.Main)&lt;/code&gt;는 UI 스레드에서 결과를 업데이트하도록 합니다.&lt;br /&gt;withContext를 사용하면 스레드를 전환하여 작업을 처리할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.4 &lt;b&gt;Kotlin 코루틴의 장점과 단점&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;장점&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;간결하고 직관적인 코드&lt;/b&gt;: 코루틴을 사용하면 &lt;b&gt;비동기 코드&lt;/b&gt;가 &lt;b&gt;동기 코드&lt;/b&gt;처럼 읽히게 되어, 코드의 가독성이 높아집니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;자원 낭비 최소화&lt;/b&gt;: 코루틴은 &lt;b&gt;스레드를 차단하지 않으며&lt;/b&gt;, 필요할 때만 &lt;b&gt;대기(suspend)&lt;/b&gt; 상태로 들어가므로, 자원 낭비를 최소화할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;코드 흐름 제어&lt;/b&gt;: suspend와 withContext 등을 사용하여 &lt;b&gt;비동기 흐름&lt;/b&gt;을 명시적으로 제어할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;단점&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;복잡성&lt;/b&gt;: &lt;b&gt;명시적인 관리&lt;/b&gt;가 필요하기 때문에, 코루틴을 제대로 활용하려면 &lt;b&gt;suspend 함수와 Dispatcher&lt;/b&gt; 등을 잘 이해하고 있어야 합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;JVM 한계&lt;/b&gt;: 코루틴은 &lt;b&gt;JVM&lt;/b&gt;에서 동작하기 때문에, JVM의 &lt;b&gt;스레드 관리&lt;/b&gt;에 의존합니다. 즉, &lt;b&gt;스레드&lt;/b&gt; 수가 많아질수록 JVM의 오버헤드가 발생할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 &lt;b&gt;Kotlin 코루틴&lt;/b&gt;은 비동기 작업을 효율적으로 처리하는 강력한 도구이며, 특히 스레드 차단 없이 비동기 흐름을 제어하는 데 유리합니다.&lt;br /&gt;하지만 명시적인 관리가 필요하고, JVM 환경에서 실행된다는 점은 고려해야 할 요소입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3. Go 고루틴&lt;/b&gt;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1 &lt;b&gt;고루틴이란?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Go&lt;/b&gt;는 동시성을 처리하는 데 있어 매우 효율적인 방법을 제공하는 언어입니다. Go의 &lt;b&gt;고루틴(Goroutines)&lt;/b&gt; 은 경량 스레드로, 프로그램 내에서 많은 동시성 작업을 쉽게 처리할 수 있도록 도와줍니다. 고루틴은 Go 런타임에서 관리되며, 시스템 스레드를 직접 사용하지 않고, 런타임에서 관리하는 경량 스레드로 작업을 실행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Go에서 고루틴을 사용하는 방법은 매우 간단합니다. &lt;code&gt;go&lt;/code&gt; 키워드만 사용하여 새로운 고루틴을 생성할 수 있습니다. 예를 들어, 네트워크 요청을 처리하는 고루틴을 생성하려면 다음과 같이 작성할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;go fetchDataFromNetwork()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼 고루틴은 새로운 스레드를 생성하는 것보다 훨씬 가볍고 빠르며, 운영 체제의 스레드를 직접 생성하지 않기 때문에 자원 소모가 적습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.2 &lt;b&gt;Go의 동시성 모델: 고루틴과 채널&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Go의 핵심 동시성 모델은 고루틴(Goroutines)과 채널(Channels)을 중심으로 구성됩니다.&lt;br /&gt;고루틴은 실제 동작하는 경량 스레드이고, 채널은 고루틴 간에 데이터를 주고받는 통신 수단입니다.&lt;br /&gt;고루틴을 사용하여 병렬 작업을 수행하고, 채널을 통해 고루틴 간의 데이터 전달을 손쉽게 할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;고루틴은 M:N 모델을 사용하여 운영 체제의 스레드를 적절하게 관리합니다.&lt;br /&gt;즉, Go 런타임은 적은 수의 운영 체제 스레드를 이용해 여러 개의 고루틴을 효율적으로 실행할 수 있게 합니다.&lt;br /&gt;이 방식은 고루틴이 수천, 수백만 개의 작업을 처리하는 데에도 매우 유리합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.3 &lt;b&gt;고루틴의 스케줄링과 관리&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Go에서 고루틴은 &lt;b&gt;자동으로 스케줄링&lt;/b&gt;됩니다.&lt;br /&gt;개발자는 고루틴을 생성하는 &lt;code&gt;go&lt;/code&gt; 키워드를 사용하면, Go 런타임이 스케줄링과 관리를 담당합니다.&lt;br /&gt;이 덕분에 개발자는 직접 스레드를 관리하거나, 복잡한 스케줄링을 신경 쓸 필요가 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Go의 고루틴은 &lt;b&gt;논리적 스레드(logical threads)&lt;/b&gt; 라고 할 수 있으며, 운영 체제의 &lt;b&gt;물리적 스레드(physical threads)&lt;/b&gt; 와는 다릅니다.&lt;br /&gt;Go 런타임은 이러한 고루틴들을 효율적으로 관리하며, 운영 체제의 스레드 풀에 고루틴을 분배하여 실행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Go는 &lt;b&gt;M:N 모델&lt;/b&gt;을 채택하여, N개의 운영 체제 스레드로 M개의 고루틴을 실행할 수 있습니다.&lt;br /&gt;이는 적은 자원으로 많은 동시성 작업을 처리할 수 있게 하여 수천, 수백만 개의 고루틴을 효율적으로 관리할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.4 &lt;b&gt;Go 고루틴의 장점과 단점&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;장점&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;간단한 동시성 처리&lt;/b&gt;: &lt;code&gt;go&lt;/code&gt; 키워드를 사용하여 매우 간단하게 고루틴을 생성할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;자동 스케줄링&lt;/b&gt;: Go 런타임이 고루틴을 자동으로 관리하고 스케줄링하므로, 개발자는 스레드 관리에 신경 쓸 필요가 없습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;경량화된 스레드&lt;/b&gt;: 고루틴은 시스템 스레드보다 훨씬 적은 자원을 소모하므로, 대규모 동시성 처리가 필요할 때 매우 유리합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;고루틴과 채널을 통한 동시성 제어&lt;/b&gt;: Go의 채널을 사용하면 고루틴 간의 안전하고 효율적인 데이터 전달이 가능합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;단점&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;병렬 처리에 한계&lt;/b&gt;: Go의 고루틴은 다중 CPU 코어를 효과적으로 활용할 수 있지만, 병렬 처리에 있어서 스레드 수 제한이 있을 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;런타임 의존성&lt;/b&gt;: 고루틴의 스케줄링과 관리가 Go 런타임에 의존하므로, 런타임의 동작에 따라 성능이 달라질 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;동시성의 복잡성&lt;/b&gt;: Go의 동시성 모델은 간단하지만, 고루틴이 많아지면 데이터 경쟁(race condition)과 같은 동기화 문제가 발생할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;4. Java 21의 가상 스레드&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.1 &lt;b&gt;Java 21의 가상 스레드 소개&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java 21은 &lt;b&gt;가상 스레드(Virtual Threads)&lt;/b&gt; 라는 기능을 도입하여, 동시성 처리의 효율성을 획기적으로 개선했습니다.&lt;br /&gt;기존 Java에서 스레드는 운영 체제의 스레드를 직접 사용하면서 시스템 자원을 차지하고, 많은 수의 스레드를 처리하는 데 한계가 있었습니다.&lt;br /&gt;하지만 가상 스레드는 이러한 문제를 해결하면서, 경량 스레드 모델을 도입하여 더 적은 자원으로 더 많은 동시성 작업을 처리할 수 있게 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가상 스레드는 기존 &lt;code&gt;Thread&lt;/code&gt; 객체와 호환되며, JVM 내에서 관리됩니다.&lt;br /&gt;운영 체제의 스레드 대신 JVM이 직접 관리하는 가상 스레드를 사용함으로써, 수천 개의 스레드를 실행할 때 발생할 수 있는 &lt;b&gt;메모리 소비&lt;/b&gt;와 &lt;b&gt;오버헤드&lt;/b&gt;를 크게 줄일 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.2 &lt;b&gt;가상 스레드와 기존 스레드 모델의 차이&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 Java의 스레드(Thread)는 운영 체제의 스레드로, 각 스레드는 운영 체제 자원을 차지하며 시스템 수준에서 관리됩니다.&lt;br /&gt;이런 방식은 동시성이 중요한 애플리케이션에서 &lt;b&gt;스레드 수가 많아지면 시스템 자원의 낭비&lt;/b&gt;를 초래할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면, Java 21에서 도입된 가상 스레드는 JVM 내에서 관리됩니다.&lt;br /&gt;즉, 운영 체제의 스레드를 직접 사용하지 않고, JVM이 동적으로 스케줄링하여 가상 스레드를 처리합니다.&lt;br /&gt;이 방식은 M:N 스케줄링 모델을 채택하여, 적은 수의 물리적 스레드로 수많은 가상 스레드를 처리할 수 있게 됩니다.&lt;br /&gt;이로 인해 많은 동시성 작업을 효율적으로 처리할 수 있으며, 기존의 Java 스레드 모델보다 훨씬 적은 자원을 소모합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.3 &lt;b&gt;가상 스레드의 스케줄링 및 동시성 처리&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java 21의 가상 스레드는 JVM에서 자동으로 관리되며, 개발자는 이를 직접 스케줄링하거나 관리할 필요가 없습니다.&lt;br /&gt;가상 스레드는 스레드 풀에서 실행되며, JVM은 이를 효율적으로 분배하여 동시성 작업을 처리합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 Java에서 동시성 처리를 위해 스레드 풀이나 &lt;code&gt;ExecutorService&lt;/code&gt;를 사용했다면, 가상 스레드를 사용할 경우에도 동일한 방식으로 &lt;code&gt;ExecutorService&lt;/code&gt;를 이용할 수 있습니다.&lt;br /&gt;가상 스레드는 JVM 내에서 관리되므로 운영 체제 스레드의 한계를 뛰어넘어 수천, 수백만 개의 동시성 작업을 효율적으로 처리할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.4 &lt;b&gt;Java 21 가상 스레드의 장점과 단점&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;장점&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;효율적인 동시성 처리&lt;/b&gt;: 가상 스레드는 운영체제 스레드를 사용하지 않기 때문에, 자원 낭비가 적고, 많은 동시 작업을 처리할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;기존 API와의 호환성&lt;/b&gt;: 가상 스레드는 기존의 &lt;code&gt;Thread&lt;/code&gt;와 &lt;code&gt;ExecutorService&lt;/code&gt;와 호환되므로, 기존 코드를 수정하지 않고도 경량 스레드를 도입할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;자동 스케줄링&lt;/b&gt;: 가상 스레드는 JVM이 자동으로 관리하므로, 개발자는 스레드 관리에 대한 부담 없이 동시성 작업을 처리할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;높은 확장성&lt;/b&gt;: 기존의 운영 체제 스레드 모델에 비해 수천 개 이상의 동시성 작업을 효율적으로 처리할 수 있어, 대규모 애플리케이션에 적합합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;단점&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;JVM 종속성&lt;/b&gt;: 가상 스레드는 JVM 내에서 관리되므로, JVM에 종속됩니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;운영 체제 스레드와의 차이&lt;/b&gt;: 가상 스레드는 운영 체제 스레드와 다르게 동작하므로, 저수준의 시스템 제어가 필요한 작업에선 적합하지 않을 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java 21의 &lt;b&gt;가상 스레드&lt;/b&gt;는 기존의 스레드 모델에 비해 효율적이고 가벼운 동시성 처리를 가능하게 하며, 수천 개 이상의 동시성 작업을 처리할 수 있는 높은 확장성을 제공합니다.&lt;br /&gt;이는 특히 웹 서버, 대규모 데이터 처리, 병렬 연산 등이 중요한 애플리케이션에서 큰 장점으로 작용할 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;5. Kotlin 코루틴, Go 고루틴, Java 21의 가상 스레드 비교&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.1 &lt;b&gt;공통&amp;nbsp;개념&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kotlin 코루틴, Go 고루틴, Java 21 가상 스레드 모두&amp;nbsp;&lt;b&gt;경량 스레드 &lt;/b&gt;방식으로 동작한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.2 &lt;b&gt;동시성 처리 방식&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Kotlin 코루틴&lt;/b&gt;: Kotlin의 코루틴은 &lt;b&gt;명시적 관리&lt;/b&gt;가 필요하며, &lt;b&gt;&lt;code&gt;suspend&lt;/code&gt; 함수&lt;/b&gt;와 &lt;code&gt;Dispatchers&lt;/code&gt;를 통해 스레드를 제어합니다. 코루틴은 비동기 작업을 처리하면서 &lt;b&gt;블로킹 없이&lt;/b&gt; 실행되지만, &lt;b&gt;스케줄링을 명시적으로 정의&lt;/b&gt;해야 하므로 개발자가 &lt;b&gt;직접 관리&lt;/b&gt;해야 합니다. 특히 &lt;code&gt;withContext&lt;/code&gt;와 같은 기능을 통해 특정 스레드에서 작업을 전환할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Go 고루틴&lt;/b&gt;: &lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;고루틴은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;자동으로 스케줄링&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;되며, 운영 체제의 스레드를 직접 사용하지 않고,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;Go 런타임이&lt;span&gt;&amp;nbsp;&lt;/span&gt;고루틴을 관리&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;합니다. 고루틴은 간단한 동시성 처리를 위해 설계되었으며,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;자동화된 스케줄링&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;덕분에 개발자는 스레드 관리에 대해 신경 쓸 필요가 없습니다&lt;/span&gt;. Go는 채널(Channels)을 통해 고루틴 간의 통신을 관리하고, 동기화와 데이터 전달을 손쉽게 처리할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Java 21 가상 스레드&lt;/b&gt;: Java의 가상 스레드는 &lt;b&gt;JVM에서 자동으로 스케줄링&lt;/b&gt;됩니다. 기존의 &lt;code&gt;Thread&lt;/code&gt;나 &lt;code&gt;ExecutorService&lt;/code&gt;와 동일한 방식으로 가상 스레드를 활용할 수 있으며, &lt;b&gt;스레드 풀&lt;/b&gt;에서 실행되는 방식으로 동작합니다. Java 가상 스레드는 운영 체제의 스레드를 사용하지 않기 때문에,&lt;b&gt; 동시성 작업을 효율적으로 처리&lt;/b&gt;할 수 있습니다. 개발자는 &lt;b&gt;스레드 관리&lt;/b&gt;에 대해 신경 쓸 필요 없이, 기존의 스레드 기반 API를 그대로 사용할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.3 &lt;b&gt;장점과 단점 비교&lt;/b&gt;&lt;/h3&gt;
&lt;table data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;&lt;b&gt;Kotlin 코루틴&lt;/b&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;&lt;b&gt;Go 고루틴&lt;/b&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;&lt;b&gt;Java 21 가상 스레드&lt;/b&gt;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;개발 용이성&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;명시적 관리&lt;/b&gt; 필요 (비동기 흐름 제어가 가능)&lt;/td&gt;
&lt;td&gt;&lt;b&gt;자동 스케줄링&lt;/b&gt; (간단한 동시성 처리)&lt;/td&gt;
&lt;td&gt;&lt;b&gt;기존 Thread와 호환&lt;/b&gt; (기존 코드 수정 없이 사용 가능)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;동시성 처리&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;스케줄러 제어&lt;/b&gt; 가능, 스레드 전환 가능&lt;/td&gt;
&lt;td&gt;&lt;b&gt;자동 스케줄링&lt;/b&gt; 및 &lt;b&gt;간단한 고루틴 관리&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;자동 스케줄링&lt;/b&gt; 및 &lt;b&gt;많은 동시 작업 처리 가능&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;자원 관리&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;스레드 차단 없음&lt;/b&gt; (경량 스레드로 자원 낭비 최소화)&lt;/td&gt;
&lt;td&gt;&lt;b&gt;경량 스레드&lt;/b&gt;로 &lt;b&gt;적은 자원&lt;/b&gt; 사용&lt;/td&gt;
&lt;td&gt;&lt;b&gt;JVM 내 관리&lt;/b&gt; (운영 체제 스레드 사용하지 않음)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;확장성&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;JVM에 의존&lt;/b&gt; (스레드 수가 많으면 JVM 오버헤드 발생 가능)&lt;/td&gt;
&lt;td&gt;&lt;b&gt;수천 개 고루틴&lt;/b&gt;을 효율적으로 처리&lt;/td&gt;
&lt;td&gt;&lt;b&gt;수천 개 가상 스레드&lt;/b&gt; 처리 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;호환성&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;JVM 기반&lt;/b&gt; (Java 및 Kotlin 프로젝트와 호환 가능)&lt;/td&gt;
&lt;td&gt;&lt;b&gt;Go 런타임&lt;/b&gt;에 의존 (Go 생태계에서만 사용 가능)&lt;/td&gt;
&lt;td&gt;&lt;b&gt;기존 Thread와 호환&lt;/b&gt; (기존 Java 코드와 호환 가능)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;복잡성&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;명시적 관리&lt;/b&gt;가 필요하여 상대적으로 복잡함&lt;/td&gt;
&lt;td&gt;&lt;b&gt;자동 관리&lt;/b&gt;되어 개발자가 신경 쓸 필요 없음&lt;/td&gt;
&lt;td&gt;&lt;b&gt;자동 관리&lt;/b&gt;되지만, JVM에 의존성 있음&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.4 &lt;b&gt;어떤 기술을 선택해야 할까?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Kotlin 코루틴&lt;/b&gt;:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;복잡한 비동기 로직&lt;/b&gt;이 필요한 경우, 즉 &lt;b&gt;비동기 흐름&lt;/b&gt;을 &lt;b&gt;동기적으로 작성&lt;/b&gt;하려는 경우에 유리합니다. 특히 Kotlin을 사용하는 프로젝트에서 &lt;b&gt;&lt;code&gt;suspend&lt;/code&gt; 함수&lt;/b&gt;와 &lt;b&gt;코루틴 스코프&lt;/b&gt;를 잘 활용하면, 코드 가독성과 유지보수성이 뛰어난 동시성 처리를 할 수 있습니다. 하지만 &lt;b&gt;명시적인 관리&lt;/b&gt;가 필요하기 때문에, &lt;b&gt;비동기 처리&lt;/b&gt;에 대한 깊은 이해가 필요합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Go 고루틴&lt;/b&gt;:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;단순하고 효율적인 동시성 처리&lt;/b&gt;가 필요한 경우에 적합합니다. Go는 &lt;b&gt;고루틴과 채널&lt;/b&gt;을 통해 &lt;b&gt;동시성 모델을 간단하고 직관적으로 관리&lt;/b&gt;할 수 있습니다. &lt;b&gt;자동 스케줄링&lt;/b&gt; 덕분에 개발자가 스레드를 관리할 필요가 없고, &lt;b&gt;Go 런타임&lt;/b&gt;이 이를 효율적으로 처리하므로 &lt;b&gt;쉽게 동시성 처리&lt;/b&gt;를 구현할 수 있습니다. 다만, &lt;b&gt;Go 언어&lt;/b&gt;를 사용할 때만 적합하므로, 다른 언어와의 호환성에 제약이 있을 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Java 21 가상 스레드&lt;/b&gt;:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Java&lt;/b&gt; 환경에서 &lt;b&gt;많은 동시성 작업&lt;/b&gt;을 &lt;b&gt;효율적으로 처리&lt;/b&gt;하려는 경우에 적합합니다. 기존 &lt;code&gt;Thread&lt;/code&gt;와 &lt;code&gt;ExecutorService&lt;/code&gt;와의 호환성 덕분에 기존의 Java 코드에 큰 수정 없이 &lt;b&gt;가상 스레드&lt;/b&gt;를 도입할 수 있습니다. &lt;b&gt;자동 스케줄링&lt;/b&gt; 기능 덕분에 &lt;b&gt;수천 개의 동시성 작업&lt;/b&gt;도 처리할 수 있지만, &lt;b&gt;JVM 의존성&lt;/b&gt;이 있으므로 &lt;b&gt;JVM 성능&lt;/b&gt;에 영향을 받을 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.5 &lt;b&gt;결론&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Kotlin 코루틴&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;비동기 로직&lt;/b&gt;을 &lt;b&gt;동기식&lt;/b&gt;으로 작성하고 싶거나, &lt;b&gt;UI 스레드&lt;/b&gt;와 분리된 &lt;b&gt;백그라운드 작업&lt;/b&gt;을 처리해야 할 때 유용합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Android 애플리케이션&lt;/b&gt;에서 &lt;b&gt;네트워크 요청&lt;/b&gt;이나 &lt;b&gt;I/O 작업&lt;/b&gt;을 비동기적으로 처리할 때 적합합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Go 고루틴&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;간단한 동시성 처리&lt;/b&gt;가 필요한 &lt;b&gt;네트워크 서버&lt;/b&gt;나 &lt;b&gt;병렬 처리&lt;/b&gt;가 중요한 시스템에서 적합합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;수천 개의 요청을 처리&lt;/b&gt;해야 하는 웹 애플리케이션에서 &lt;b&gt;성능 최적화&lt;/b&gt;를 위해 사용됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Java 21 가상 스레드&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;기존 Java 코드&lt;/b&gt;와의 &lt;b&gt;호환성&lt;/b&gt;이 중요한 경우, &lt;b&gt;대규모 동시성 처리&lt;/b&gt;가 필요한 웹 서버나 &lt;b&gt;데이터베이스 쿼리 처리&lt;/b&gt;에 적합합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;기존 Thread&lt;/b&gt; 기반 시스템을 &lt;b&gt;최적화&lt;/b&gt;하고 싶을 때 선택할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세 가지 기술은 모두 동시성 처리를 효율적으로 구현할 수 있는 강력한 도구들입니다.&lt;br /&gt;각 기술을 선택할 때는 사용하는 언어, 기존 코드와의 호환성, 그리고 프로젝트의 요구사항에 맞춰 적절한 기술을 선택하는 것이 중요합니다.&lt;/p&gt;</description>
      <category>Go언어</category>
      <category>Coroutine</category>
      <category>go</category>
      <category>go언어</category>
      <category>java21</category>
      <category>jdk21</category>
      <category>Kotlin</category>
      <category>virtual thread</category>
      <category>가상스레드</category>
      <category>고루틴</category>
      <category>코루틴</category>
      <author>SKaSha</author>
      <guid isPermaLink="true">https://skasha.tistory.com/122</guid>
      <comments>https://skasha.tistory.com/122#entry122comment</comments>
      <pubDate>Mon, 4 Aug 2025 00:24:45 +0900</pubDate>
    </item>
    <item>
      <title>Go언어, 동시성 철학: &amp;ldquo;공유 메모리가 아닌, 통신으로 메모리를 공유하라&amp;rdquo;</title>
      <link>https://skasha.tistory.com/121</link>
      <description>&lt;p&gt;Go 언어에는 독특한 동시성 철학이 존재한다.&lt;br&gt;바로 &lt;strong&gt;&amp;quot;Don&amp;#39;t communicate by sharing memory; instead, share memory by communicating.&amp;quot;&lt;/strong&gt;&lt;br&gt;이는 단순한 코드 스타일 가이드가 아니라, Go가 동시성 문제를 어떻게 바라보는지를 명확히 보여주는 핵심 원칙이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;공유 메모리 중심의 전통적인 방식&lt;/h2&gt;
&lt;p&gt;다른 언어(C, Java 등)에서는 여러 스레드가 같은 변수나 객체에 접근하면서 동기화를 위한 수단으로 락(Mutex), 세마포어 등을 사용한다.&lt;br&gt;이러한 방식은 메모리를 중심으로 동시성을 처리한다. 예를 들면 아래와 같다:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;var count int
var mu sync.Mutex

func increment() {
    mu.Lock()
    count++
    mu.Unlock()
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 방식은 단순해 보일 수 있지만, 스레드가 늘어날수록 코드 복잡성이 증가하고, 데드락이나 레이스 컨디션과 같은 동시성 오류 가능성도 함께 높아진다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Go의 철학: 채널을 통한 명시적 통신&lt;/h2&gt;
&lt;p&gt;Go는 이러한 전통적인 접근 대신, &lt;strong&gt;채널(channel)&lt;/strong&gt; 을 통해 고루틴 간의 명시적인 통신을 권장한다.&lt;br&gt;핵심은 &lt;strong&gt;데이터는 공유하지 않고, 메시지를 통해 주고받는다&lt;/strong&gt;는 점이다.&lt;/p&gt;
&lt;p&gt;예시는 다음과 같다:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func counter(ch chan int) {
    count := 0
    for {
        ch &amp;lt;- count
        count++
    }
}

func main() {
    ch := make(chan int)
    go counter(ch)

    fmt.Println(&amp;lt;-ch) // 0
    fmt.Println(&amp;lt;-ch) // 1
    fmt.Println(&amp;lt;-ch) // 2
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 코드에서는 &lt;code&gt;count&lt;/code&gt; 변수에 여러 고루틴이 직접 접근하지 않는다.&lt;br&gt;&lt;code&gt;counter&lt;/code&gt; 함수 내부에서만 &lt;code&gt;count&lt;/code&gt;가 관리되며, 외부 고루틴은 오직 채널을 통해서만 값을 받아볼 수 있다.&lt;br&gt;이로 인해 &lt;strong&gt;락 없이도 안전한 동시성 처리가 가능&lt;/strong&gt;해진다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;왜 이 방식이 중요한가?&lt;/h2&gt;
&lt;p&gt;Go의 채널 기반 동시성은 단순히 문법상의 편의가 아니다.&lt;br&gt;이는 &lt;strong&gt;안전성과 명확성을 최우선으로 하는 설계 철학&lt;/strong&gt;이다.&lt;br&gt;메모리를 공유하려면 반드시 &lt;strong&gt;의도를 갖고 명시적으로 통신을 해야 하며&lt;/strong&gt;, 이는 코드의 안정성과 가독성, 유지보수성에 큰 영향을 미친다.&lt;/p&gt;
&lt;p&gt;또한 이러한 설계는 CSP(Communicating Sequential Processes)라는 이론적 기반 위에 놓여 있다.&lt;br&gt;Go는 이 개념을 실질적인 프로그래밍 모델로 끌어내렸고, 실제로도 복잡한 동시성 문제를 효과적으로 다룰 수 있게 한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;정리&lt;/h2&gt;
&lt;p&gt;Go 언어가 권장하는 동시성 방식은 다음과 같은 점에서 유의미하다:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;명시적 통신&lt;/strong&gt;을 통해 동시성을 관리하므로, 코드의 의도가 명확하다.&lt;/li&gt;
&lt;li&gt;락과 같은 저수준 동기화 기법보다 &lt;strong&gt;추론하기 쉽고 오류 가능성이 낮다.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;복잡한 공유 메모리 모델보다 훨씬 &lt;strong&gt;단순하고 직관적인 동시성 모델&lt;/strong&gt;을 제공한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;Go에서 동시성을 구현할 때는 항상 이 문장을 떠올리는 것이 좋다:&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;&lt;strong&gt;“공유 메모리로 통신하지 말고, 통신으로 메모리를 공유하라.”&lt;/strong&gt;&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;이 철학을 이해하고 따르는 것이 Go스러운 코드를 작성하는 출발점이 될 수 있다.&lt;/p&gt;</description>
      <category>Go언어</category>
      <category>go언어</category>
      <category>동시성 철학</category>
      <author>SKaSha</author>
      <guid isPermaLink="true">https://skasha.tistory.com/121</guid>
      <comments>https://skasha.tistory.com/121#entry121comment</comments>
      <pubDate>Sun, 3 Aug 2025 01:29:53 +0900</pubDate>
    </item>
    <item>
      <title>Go언어, 코루틴, 채널, Context 알아보기</title>
      <link>https://skasha.tistory.com/120</link>
      <description>&lt;p&gt;Go의 동시성을 책임지는 핵심 요소들, &lt;strong&gt;고루틴(Goroutine)&lt;/strong&gt;, &lt;strong&gt;채널(Channel)&lt;/strong&gt;, &lt;strong&gt;select 문&lt;/strong&gt;, 그리고 &lt;strong&gt;Context&lt;/strong&gt;까지 정리해보도록 하겠습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;1. 고루틴(Goroutine)&lt;/h3&gt;
&lt;p&gt;Go 언어의 동시성 프로그래밍은 바로 &lt;strong&gt;고루틴&lt;/strong&gt;에서 출발합니다.&lt;br&gt;흔히 아는 &amp;#39;스레드&amp;#39;와 비슷하지만, 고루틴은 훨씬 더 가볍고 Go 런타임에 의해 관리되는 특별한 존재입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;진짜 가벼워요!&lt;/strong&gt; 일반적인 운영체제(OS) 스레드가 몇 MB의 메모리를 사용하는 반면, 고루틴은 고작 몇 KB로 시작합니다. 필요에 따라 메모리 크기가 유연하게 조절되기 때문에, 수십만 개의 고루틴을 동시에 띄워도 시스템에 큰 부담을 주지 않아요.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;쉬운 생성:&lt;/strong&gt; 고루틴을 만드는 건 정말 간단합니다. 함수 호출 앞에 &lt;code&gt;go&lt;/code&gt; 키워드만 붙이면 끝이에요!&lt;/li&gt;
&lt;/ul&gt;
&lt;!-- end list --&gt;

&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;package main

import (
    &amp;quot;fmt&amp;quot;
    &amp;quot;time&amp;quot;
)

func say(word string) {
    for i := 0; i &amp;lt; 3; i++ {
        time.Sleep(100 * time.Millisecond) // 잠시 대기
        fmt.Println(word)
    }
}

func main() {
    fmt.Println(&amp;quot;메인 함수 시작&amp;quot;)
    go say(&amp;quot;안녕하세요&amp;quot;) // &amp;#39;안녕하세요&amp;#39;를 출력하는 고루틴 시작!
    go say(&amp;quot;Go Lang&amp;quot;)   // &amp;#39;Go Lang&amp;#39;을 출력하는 또 다른 고루틴 시작!

    time.Sleep(1 * time.Second) // 고루틴들이 작업할 시간을 벌어줌
    fmt.Println(&amp;quot;메인 함수 종료&amp;quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 코드를 실행하면 &lt;code&gt;안녕하세요&lt;/code&gt;와 &lt;code&gt;Go Lang&lt;/code&gt;이 뒤죽박죽 섞여서 출력될 거예요.&lt;br&gt;이게 바로 두 개의 고루틴이 동시에 실행되고 있다는 증거입니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;잠깐, 동시성(Concurrency)과 병렬성(Parallelism)은 달라요!&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;동시성&lt;/strong&gt;은 여러 작업을 &lt;strong&gt;동시에 처리하는 것처럼 보이게&lt;/strong&gt; 만드는 능력입니다. CPU 코어가 하나라도 여러 작업을 빠르게 번갈아 가며 처리하는 거죠. 마치 한 사람이 여러 개의 공을 저글링하는 것과 같아요.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;병렬성&lt;/strong&gt;은 여러 작업을 &lt;strong&gt;진짜로 동시에 실행하는&lt;/strong&gt; 능력입니다. 멀티코어 CPU 환경에서 각 코어가 다른 작업을 실제로 처리하는 것을 의미합니다. 여러 사람이 각자 하나의 공을 저글링하는 것과 비슷하죠.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Go의 고루틴은 주로 &lt;strong&gt;동시성&lt;/strong&gt;을 위한 도구지만, Go 런타임이 알아서 사용 가능한 CPU 코어에 고루틴을 분배해서 &lt;strong&gt;병렬성&lt;/strong&gt;까지도 활용할 수 있도록 해줍니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;2. 고루틴 간의 소통: 채널(Channel)&lt;/h3&gt;
&lt;p&gt;여러 고루틴이 각자 자기 일을 한다면, 서로에게 정보를 주고받아야 할 때가 생기겠죠? 이때 사용하는 것이 바로 &lt;strong&gt;채널(Channel)&lt;/strong&gt; 입니다.&lt;br&gt;Go 언어는 &amp;quot;메모리를 공유하며 통신하지 말고, 통신하며 메모리를 공유하라(Don&amp;#39;t communicate by sharing memory; instead, share memory by communicating.)&amp;quot;라는 철학을 가지고 있어요.&lt;br&gt;즉, 복잡한 락(Lock)이나 뮤텍스 없이 채널을 통해 메시지를 주고받는 방식으로 고루틴 간의 데이터를 안전하게 공유하는 것을 권장합니다.&lt;/p&gt;
&lt;p&gt;채널은 고루틴 간의 데이터를 주고받는 &amp;#39;파이프&amp;#39;라고 생각하면 됩니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;package main

import (
    &amp;quot;fmt&amp;quot;
    &amp;quot;time&amp;quot;
)

func sendData(ch chan int, data int) {
    fmt.Printf(&amp;quot;데이터 %d 전송 시작...\n&amp;quot;, data)
    time.Sleep(500 * time.Millisecond) // 전송 시간 시뮬레이션
    ch &amp;lt;- data // 채널로 데이터 전송
    fmt.Printf(&amp;quot;데이터 %d 전송 완료!\n&amp;quot;, data)
}

func main() {
    dataChannel := make(chan int) // int 타입 데이터를 주고받을 채널 생성

    go sendData(dataChannel, 100) // 고루틴으로 데이터 전송 시작

    fmt.Println(&amp;quot;메인 함수: 데이터 수신 대기 중...&amp;quot;)
    receivedData := &amp;lt;-dataChannel // 채널에서 데이터 수신 (데이터가 올 때까지 대기)
    fmt.Printf(&amp;quot;메인 함수: 데이터 %d 수신 완료!\n&amp;quot;, receivedData)

    fmt.Println(&amp;quot;프로그램 종료&amp;quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 예시에서는 &lt;code&gt;sendData&lt;/code&gt; 고루틴이 데이터를 채널로 보내면, &lt;code&gt;main&lt;/code&gt; 함수가 그 데이터를 받을 때까지 기다립니다.&lt;br&gt;채널이 없다면 &lt;code&gt;main&lt;/code&gt; 함수는 &lt;code&gt;sendData&lt;/code&gt; 고루틴의 작업이 끝났는지 알 수 없겠죠?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;비버퍼링 vs. 버퍼링 채널:&lt;/strong&gt;&lt;br&gt;위 예시처럼 &lt;code&gt;make(chan int)&lt;/code&gt;는 &lt;strong&gt;비버퍼링 채널&lt;/strong&gt;입니다.&lt;br&gt;데이터를 보내는 쪽과 받는 쪽이 동시에 준비되어야만 통신이 이루어집니다.&lt;br&gt;만약 한쪽이 준비되지 않았다면, 그 작업은 블로킹(Blocking)되어 기다리게 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;make(chan int, 3)&lt;/code&gt;처럼 버퍼 크기를 지정하면 &lt;strong&gt;버퍼링된 채널&lt;/strong&gt;이 됩니다.&lt;br&gt;이 채널은 지정된 개수만큼의 데이터를 버퍼에 저장할 수 있어서, 버퍼가 가득 차기 전까지는 보내는 쪽이 블로킹되지 않습니다.&lt;br&gt;받을 때도 버퍼에 데이터가 있다면 즉시 받을 수 있습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;3. 고루틴 스케줄링: 자발적 양보와 선점&lt;/h3&gt;
&lt;p&gt;고루틴이 왜 그렇게 가볍고 효율적으로 동작할까요? 바로 Go 런타임의 스케줄링 덕분입니다. &amp;quot;고루틴은 특정 지점에서 자발적으로 제어권을 양보하거나, 런타임에 의해 선점될 수 있습니다&amp;quot;라는 문장에 그 비밀이 숨어있어요.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;자발적 양보(Yield)&lt;/strong&gt; : 고루틴이 스스로 CPU 사용 권한을 다른 고루틴에게 넘겨주는 경우입니다. 주로 I/O 작업(네트워크 통신, 파일 읽기/쓰기 등)이나 채널에서 데이터를 기다릴 때 발생해요. 고루틴이 &amp;quot;잠깐 쉬어야 하니, 다른 고루틴이 일하게 해주세요!&amp;quot;라고 말하는 것과 같죠. 이렇게 하면 CPU 자원을 낭비하지 않고 효율적으로 사용할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;선점(Preemption)&lt;/strong&gt; : 고루틴이 너무 오랫동안 CPU를 독점할 때, Go 런타임 스케줄러가 강제로 CPU 사용 권한을 빼앗아 다른 고루틴에게 넘겨주는 경우입니다. Go 1.14 버전부터는 CPU를 많이 사용하는 고루틴도 강제로 멈춰 세울 수 있는 비협력적 선점(Non-cooperative Preemption)이 도입되어, 모든 고루틴에게 공평하게 CPU 시간을 분배하고 시스템의 응답성을 높일 수 있게 되었습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 두 가지 메커니즘 덕분에 개발자는 복잡한 스레드 스케줄링에 신경 쓸 필요 없이 &lt;code&gt;go&lt;/code&gt; 키워드 하나로 동시성을 구현하고, Go 런타임이 알아서 최적의 성능을 끌어내줍니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;4. 여러 채널을 동시에 기다리기: &lt;code&gt;select&lt;/code&gt; 문&lt;/h3&gt;
&lt;p&gt;이제 여러 고루틴이 동시에 작동하고, 서로 채널로 통신하는 상황을 생각해봅시다. 이때 여러 채널 중에서 &lt;strong&gt;&amp;quot;누가 먼저 데이터를 보냈지?&amp;quot;&lt;/strong&gt; 또는 &lt;strong&gt;&amp;quot;이 작업이 타임아웃 되지는 않았나?&amp;quot;&lt;/strong&gt; 등을 동시에 확인하고 싶을 때가 있죠. 이때 사용하는 것이 바로 &lt;strong&gt;&lt;code&gt;select&lt;/code&gt; 문&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;select&lt;/code&gt; 문은 여러 &lt;code&gt;case&lt;/code&gt; 절에 채널 연산을 넣어두고, 이 중에서 &lt;strong&gt;가장 먼저 준비된(데이터를 보내거나 받을 수 있는) &lt;code&gt;case&lt;/code&gt; 절을 실행&lt;/strong&gt;합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;package main

import (
    &amp;quot;fmt&amp;quot;
    &amp;quot;time&amp;quot;
)
func main() {
    ch1 := make(chan string)
    ch2 := make(chan string)

    go func() {
        time.Sleep(1 * time.Second)
        ch1 &amp;lt;- &amp;quot;메시지 from 채널1&amp;quot;
    }()

    go func() {
        time.Sleep(500 * time.Millisecond) // 채널1보다 먼저 도착
        ch2 &amp;lt;- &amp;quot;메시지 from 채널2&amp;quot;
    }()

    select {
    case msg1 := &amp;lt;-ch1:
        fmt.Println(&amp;quot;수신:&amp;quot;, msg1)
    case msg2 := &amp;lt;-ch2:
        fmt.Println(&amp;quot;수신:&amp;quot;, msg2)
    case &amp;lt;-time.After(2 * time.Second): // 2초 타임아웃 설정
        fmt.Println(&amp;quot;2초 안에 아무것도 오지 않았습니다. 타임아웃!&amp;quot;)
    }

    fmt.Println(&amp;quot;Select 예제 종료&amp;quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 코드를 실행하면 아마 &amp;quot;수신: 메시지 from 채널2&amp;quot;가 먼저 출력될 거예요. &lt;code&gt;select&lt;/code&gt; 문은 채널1과 채널2, 그리고 2초 타임아웃 중 가장 먼저 준비된 것을 실행합니다.&lt;/p&gt;
&lt;h4&gt;select`는 CPU를 낭비하지 않아요!&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;select&lt;/code&gt; 문이 계속 채널을 &amp;#39;체크&amp;#39;하는 것처럼 보이지만, 사실 &lt;code&gt;select&lt;/code&gt; 문은 &lt;code&gt;default&lt;/code&gt; 절이 없는 한, 준비된 채널 작업이 나타날 때까지 해당 고루틴을 블로킹(CPU를 사용하지 않고 대기) 상태로 만듭니다.&lt;br&gt;즉, &amp;#39;바쁜 대기&amp;#39;가 아니라 잠자다가 이벤트가 발생하면 깨어나는 스마트한 방식이죠.&lt;br&gt;&lt;code&gt;default&lt;/code&gt; 절이 있다면 즉시 실행하고 빠져나오기 때문에, 무한 루프에서 &lt;code&gt;default&lt;/code&gt; 절을 사용할 때는 &lt;code&gt;time.Sleep()&lt;/code&gt; 등으로 적절한 대기 시간을 주어 CPU를 과도하게 사용하지 않도록 주의해야 합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;5. 복잡한 비동기 작업 제어: &lt;code&gt;context&lt;/code&gt; 패키지&lt;/h3&gt;
&lt;p&gt;이제 고루틴과 채널로 많은 작업을 동시에 처리할 수 있게 되었지만, 실제 애플리케이션에서는 하나의 요청을 처리하기 위해 여러 고루틴이 계층적으로 동작하고, 이들을 중간에 취소하거나 특정 시간 내에 완료해야 하는 복잡한 상황이 발생합니다.&lt;br&gt;이때 사용하는 것이 바로 Go의 표준 라이브러리인 &lt;strong&gt;&lt;code&gt;context&lt;/code&gt; 패키지&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;context&lt;/code&gt;는 주로 다음과 같은 목적을 위해 사용됩니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;취소 신호 전파&lt;/strong&gt;: 부모 고루틴에서 자식 고루틴들에게 &amp;quot;야, 이제 그만 작업해!&amp;quot;라는 취소 신호를 전달합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;타임아웃/마감 시간 설정&lt;/strong&gt;: 특정 시간(타임아웃)이 지나거나 특정 시점(마감 시간)에 도달하면 자동으로 취소 신호를 보냅니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;요청 범위 데이터 전달&lt;/strong&gt;: HTTP 요청 ID와 같이 요청과 관련된 데이터를 여러 고루틴에 걸쳐 안전하게 전달합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;context&lt;/code&gt;는 &lt;code&gt;context.Background()&lt;/code&gt;나 &lt;code&gt;context.TODO()&lt;/code&gt;로 시작해서 &lt;code&gt;WithCancel&lt;/code&gt;, &lt;code&gt;WithTimeout&lt;/code&gt;, &lt;code&gt;WithDeadline&lt;/code&gt;, &lt;code&gt;WithValue&lt;/code&gt; 같은 함수들로 새로운 컨텍스트를 파생시켜 사용합니다. 중요한 건, 부모 컨텍스트가 취소되면 모든 자식 컨텍스트도 자동으로 취소된다는 점입니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;package main

import (
    &amp;quot;context&amp;quot; // context 패키지 import
    &amp;quot;fmt&amp;quot;
    &amp;quot;time&amp;quot;
)

func fetchUserData(ctx context.Context, userID int) {
    fmt.Printf(&amp;quot;유저 %d 정보 가져오기 시작...\n&amp;quot;, userID)
    select {
    case &amp;lt;-time.After(3 * time.Second): // 3초 걸리는 작업 시뮬레이션
        fmt.Printf(&amp;quot;유저 %d 정보 가져오기 완료!\n&amp;quot;, userID)
    case &amp;lt;-ctx.Done(): // 컨텍스트 취소 신호가 오면
        fmt.Printf(&amp;quot;유저 %d 정보 가져오기 취소됨! 사유: %v\n&amp;quot;, userID, ctx.Err())
    }
}

func main() {
    fmt.Println(&amp;quot;Context 예제 시작&amp;quot;)

    // 5초 타임아웃 컨텍스트 생성
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel() // 중요! 컨텍스트 사용 후 반드시 cancel 함수 호출하여 리소스 해제

    go fetchUserData(ctx, 123) // 유저 정보 가져오는 고루틴 시작

    // 메인 고루틴은 잠시 다른 일을 하거나 기다림
    time.Sleep(4 * time.Second) // 4초 대기 (fetchUserData는 아직 진행 중)
    fmt.Println(&amp;quot;메인 함수: 다른 작업 수행 중...&amp;quot;)

    // ctx의 타임아웃(5초)이 지나면 fetchUserData 고루틴이 자동으로 취소될 것임.
    time.Sleep(2 * time.Second) // 총 6초 대기 (타임아웃 발생 확인)
    fmt.Println(&amp;quot;Context 예제 종료&amp;quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 예시에서 &lt;code&gt;fetchUserData&lt;/code&gt; 고루틴은 3초가 걸리도록 시뮬레이션했습니다. 하지만 &lt;code&gt;main&lt;/code&gt; 함수에서 생성한 &lt;code&gt;ctx&lt;/code&gt;는 5초 타임아웃을 가지고 있죠. 만약 &lt;code&gt;fetchUserData&lt;/code&gt;가 5초를 넘겼다면, &lt;code&gt;ctx.Done()&lt;/code&gt; 채널이 닫히고 &lt;code&gt;fetchUserData&lt;/code&gt;는 작업을 취소하게 됩니다. 이처럼 &lt;code&gt;context&lt;/code&gt;는 고루틴 간의 계층적인 취소 및 타임아웃 관리를 매우 효과적으로 도와줍니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;마치며...&lt;/h3&gt;
&lt;p&gt;Go 언어의 동시성은 &lt;code&gt;고루틴&lt;/code&gt;이라는 가볍고 효율적인 실행 단위, &lt;code&gt;채널&lt;/code&gt;이라는 안전한 통신 메커니즘, 그리고 &lt;code&gt;select&lt;/code&gt;와 &lt;code&gt;context&lt;/code&gt;라는 강력한 제어 도구들의 조합으로 이루어져 있습니다. 이 모든 것들이 유기적으로 연결되어, 우리가 복잡한 분산 시스템이나 고성능 서버를 Go로 쉽고 견고하게 만들 수 있도록 해줍니다.&lt;/p&gt;</description>
      <category>Go언어</category>
      <category>channel</category>
      <category>conTeXt</category>
      <category>goroutine</category>
      <category>go언어</category>
      <category>select</category>
      <category>고루틴</category>
      <category>고루틴 스케줄링</category>
      <category>채널</category>
      <author>SKaSha</author>
      <guid isPermaLink="true">https://skasha.tistory.com/120</guid>
      <comments>https://skasha.tistory.com/120#entry120comment</comments>
      <pubDate>Sun, 3 Aug 2025 01:20:06 +0900</pubDate>
    </item>
    <item>
      <title>Go언어, 인터페이스</title>
      <link>https://skasha.tistory.com/119</link>
      <description>&lt;h2&gt;Go 언어 &lt;code&gt;인터페이스&lt;/code&gt;&lt;/h2&gt;
&lt;h3&gt;인터페이스란?&lt;/h3&gt;
&lt;p&gt;Go 언어에서 &lt;strong&gt;인터페이스는 메서드 시그니처(Method Signature)의 집합&lt;/strong&gt;입니다. 쉽게 말해, **&amp;quot;어떤 동작을 할 수 있다&amp;quot;**는 약속(규약)을 정의하는 타입이에요. 인터페이스 자체는 데이터를 가지지 않고, 오직 메서드의 이름, 매개변수 타입, 반환 타입만을 정의합니다.&lt;/p&gt;
&lt;p&gt;가장 중요한 특징은 Go의 인터페이스는 &lt;strong&gt;암시적으로(Implicitly) 구현된다&lt;/strong&gt;는 점입니다. 다른 언어처럼 &lt;code&gt;implements&lt;/code&gt;나 &lt;code&gt;extends&lt;/code&gt; 키워드를 사용해서 &amp;quot;내가 이 인터페이스를 구현할 것이다!&amp;quot;라고 명시적으로 선언할 필요가 없어요. 어떤 타입이 인터페이스가 정의한 모든 메서드를 가지고 있으면, 그 타입은 자동으로 해당 인터페이스를 구현한 것으로 간주됩니다.&lt;/p&gt;
&lt;p&gt;이것이 Go 인터페이스의 핵심이자 강력함의 원천입니다. 흔히 **덕 타이핑(Duck Typing)**이라고 불리는 개념과 밀접하게 연관되어 있습니다.&lt;/p&gt;
&lt;h4&gt;덕 타이핑(Duck Typing)이란?&lt;/h4&gt;
&lt;p&gt;&amp;quot;만약 어떤 것이 오리처럼 걷고, 오리처럼 꽥꽥거린다면, 그것은 오리일 것이다(If it walks like a duck and quacks like a duck, it must be a duck).&amp;quot;라는 유명한 말에서 유래했습니다. 프로그래밍에서 덕 타이핑은 &lt;strong&gt;객체의 실제 타입보다는 객체가 어떤 메서드들을 가지고 있는가(어떤 행동을 할 수 있는가)에 따라 타입을 판단하는 방식&lt;/strong&gt;을 의미합니다.&lt;/p&gt;
&lt;p&gt;Go의 인터페이스가 바로 이 덕 타이핑을 정적으로 구현한 형태입니다. 컴파일러가 어떤 타입이 인터페이스를 만족하는지 &amp;quot;행동&amp;quot;만 보고 판단하는 거죠. 명시적인 선언 없이도 덕 타이핑 원리에 따라 유연하게 코드를 작성할 수 있게 해줍니다.&lt;/p&gt;
&lt;h4&gt;인터페이스 선언 문법&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;type 인터페이스이름 interface {
    메서드1(매개변수1 타입1) 반환타입1
    메서드2(매개변수2 타입2, 매개변수3 타입3) 반환타입2
    // ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;예시를 볼까요?&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;package main

import &amp;quot;fmt&amp;quot;

// Speaker 인터페이스 정의: Speak() 메서드를 가짐
type Speaker interface {
    Speak() // 매개변수 없고, 반환값 없는 메서드
}

// Animal 구조체 정의
type Animal struct {
    Name string
}

// Animal 타입에 Speak 메서드 연결
func (a Animal) Speak() {
    fmt.Printf(&amp;quot;%s이(가) 소리냅니다.\n&amp;quot;, a.Name)
}

// Dog 구조체 정의
type Dog struct {
    Name string
}

// Dog 타입에 Speak 메서드 연결
func (d Dog) Speak() {
    fmt.Printf(&amp;quot;%s이(가) 멍멍!\n&amp;quot;, d.Name)
}

// Person 구조체 정의
type Person struct {
    Name string
}

// Person 타입에 Speak 메서드 연결
func (p Person) Speak() {
    fmt.Printf(&amp;quot;%s이(가) 말합니다!\n&amp;quot;, p.Name)
}

// saySomething 함수는 Speaker 인터페이스 타입의 인자를 받습니다.
// 이 함수는 Speaker 인터페이스를 구현하는 어떤 타입의 값도 받을 수 있습니다.
func saySomething(s Speaker) {
    s.Speak() // 인터페이스 타입으로 Speak 메서드 호출
}

func main() {
    animal := Animal{Name: &amp;quot;새&amp;quot;}
    dog := Dog{Name: &amp;quot;바둑이&amp;quot;}
    person := Person{Name: &amp;quot;철수&amp;quot;}

    saySomething(animal) // 출력: 새이(가) 소리냅니다.
    saySomething(dog)    // 출력: 바둑이이(가) 멍멍!
    saySomething(person) // 출력: 철수이(가) 말합니다!

    // 인터페이스 변수에 다양한 타입의 값 할당
    var speaker Speaker
    speaker = dog
    speaker.Speak() // 출력: 바둑이이(가) 멍멍!

    speaker = person
    speaker.Speak() // 출력: 철수이(가) 말합니다!
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위 예시에서 &lt;code&gt;Animal&lt;/code&gt;, &lt;code&gt;Dog&lt;/code&gt;, &lt;code&gt;Person&lt;/code&gt; 구조체는 &lt;code&gt;Speaker&lt;/code&gt; 인터페이스를 &lt;strong&gt;명시적으로 구현한다고 선언하지 않았습니다.&lt;/strong&gt; 하지만 이 모든 타입은 &lt;code&gt;Speaker&lt;/code&gt; 인터페이스가 요구하는 &lt;code&gt;Speak()&lt;/code&gt; 메서드를 가지고 있죠. 그래서 Go 컴파일러는 이 타입들이 자동으로 &lt;code&gt;Speaker&lt;/code&gt; 인터페이스를 구현했다고 인식합니다. 바로 이것이 &lt;strong&gt;덕 타이핑&lt;/strong&gt;의 원리가 Go의 인터페이스에 적용된 모습입니다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;saySomething&lt;/code&gt; 함수는 &lt;code&gt;Speaker&lt;/code&gt; 타입의 인자를 받습니다. 덕분에 우리는 &lt;code&gt;Animal&lt;/code&gt;, &lt;code&gt;Dog&lt;/code&gt;, &lt;code&gt;Person&lt;/code&gt; 중 어떤 타입의 인스턴스라도 &lt;code&gt;saySomething&lt;/code&gt; 함수에 전달할 수 있습니다. 이것이 바로 Go가 **다형성(Polymorphism)**을 구현하는 방식입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;인터페이스의 핵심 특징과 동작 원리&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;암시적 구현 (Implicit Implementation) &amp;amp; 덕 타이핑:&lt;/strong&gt; 가장 중요하고 강력한 특징입니다. 특정 인터페이스의 모든 메서드를 구현하면, 해당 타입은 자동으로 그 인터페이스를 만족합니다. 이는 코드의 결합도를 낮추고 유연성을 높입니다. 컴파일 타임에 덕 타이핑 원리에 따라 타입 적합성을 검사합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;타입으로서의 인터페이스:&lt;/strong&gt; 인터페이스는 Go에서 일반적인 타입처럼 사용될 수 있습니다. 변수의 타입으로 선언될 수 있고, 함수의 매개변수나 반환 타입으로 사용될 수 있습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;인터페이스 값의 내부 구조:&lt;/strong&gt; Go의 인터페이스 변수는 실제로 두 가지 정보를 저장합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Concrete Type (구체 타입):&lt;/strong&gt; 인터페이스 변수에 할당된 실제 값의 타입 정보.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Value (값):&lt;/strong&gt; 인터페이스 변수에 할당된 실제 값 자체 (또는 포인터).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 두 가지 정보 덕분에 인터페이스 변수는 다양한 타입의 값을 담을 수 있으면서도, 그 값의 실제 타입이 무엇인지 알고 해당 타입의 메서드를 호출할 수 있습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;nil 인터페이스 값:&lt;/strong&gt; 인터페이스 변수는 &lt;code&gt;nil&lt;/code&gt; 값을 가질 수 있습니다. 인터페이스 변수가 &lt;code&gt;nil&lt;/code&gt;인 경우는 구체 타입과 값이 모두 &lt;code&gt;nil&lt;/code&gt;인 경우입니다. 이때 메서드를 호출하면 런타임 패닉이 발생할 수 있으니 주의해야 합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;var s Speaker // s는 nil 인터페이스 (구체 타입과 값 모두 nil)
fmt.Println(s == nil) // true
// s.Speak() // 런타임 에러! nil 인터페이스에 대한 메서드 호출&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;빈 인터페이스 (&lt;code&gt;interface{}&lt;/code&gt;):&lt;/strong&gt; 아무런 메서드도 정의하지 않은 인터페이스입니다. Go의 모든 타입은 아무 메서드도 요구하지 않는 빈 인터페이스를 만족하므로, &lt;code&gt;interface{}&lt;/code&gt; 타입은 어떤 타입의 값이라도 담을 수 있습니다. 이는 제네릭 프로그래밍이나 다양한 타입의 데이터를 다룰 때 유용하게 사용됩니다 (하지만 타입 단언이 필요합니다).&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;var any interface{} // 어떤 타입의 값이라도 담을 수 있음
any = 10
fmt.Println(any) // 10
any = &amp;quot;Hello, Go!&amp;quot;
fmt.Println(any) // Hello, Go!&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h3&gt;인터페이스를 사용하는 이유 (장점)&lt;/h3&gt;
&lt;p&gt;Go 언어에서 인터페이스는 강력한 설계 도구이자 여러 이점을 제공합니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;다형성 (Polymorphism):&lt;/strong&gt; 가장 큰 장점입니다. 함수나 메서드가 특정 인터페이스를 인자로 받도록 설계하면, 해당 인터페이스를 구현하는 어떤 타입의 객체라도 전달할 수 있습니다. 이는 코드 재사용성을 높이고 유연한 설계를 가능하게 합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;예를 들어, &lt;code&gt;io.Reader&lt;/code&gt; 인터페이스를 사용하는 함수는 파일, 네트워크 연결, 메모리 버퍼 등 &lt;code&gt;Read&lt;/code&gt; 메서드를 구현하는 모든 소스에서 데이터를 읽을 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;느슨한 결합 (Loose Coupling):&lt;/strong&gt; 인터페이스는 코드 간의 결합도를 낮춥니다. 코드를 작성할 때 특정 구체 타입에 직접 의존하는 대신, 인터페이스에 의존하게 됩니다. 이는 모듈 간의 의존성을 줄여서 한 부분의 변경이 다른 부분에 미치는 영향을 최소화합니다. 덕분에 테스트하기 쉽고, 유지보수하기 용이하며, 새로운 기능을 추가하기에도 편리합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;확장성 (Extensibility):&lt;/strong&gt; 새로운 타입이 기존 인터페이스를 구현하기만 하면, 기존 코드에 어떤 변경도 없이 새로운 타입의 기능을 추가할 수 있습니다. 시스템의 확장이 매우 용이해집니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;테스트 용이성 (Testability):&lt;/strong&gt; 실제 구현체 대신 인터페이스를 만족하는 &amp;#39;모의(Mock)&amp;#39; 객체를 만들어 쉽게 테스트할 수 있습니다. 데이터베이스나 외부 API 의존성을 가진 코드를 테스트할 때 특히 유용합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;임베딩과의 시너지:&lt;/strong&gt; 구조체에 인터페이스를 임베딩할 수 있습니다. 이는 해당 인터페이스의 메서드 집합을 구조체가 &amp;#39;구현&amp;#39;해야 함을 의미하며, 이를 통해 코드의 가독성과 설계의 명확성을 높일 수 있습니다. (사실 인터페이스를 직접 임베딩하는 경우는 드물고, 보통 해당 인터페이스를 구현하는 구체 타입을 임베딩합니다.)&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h3&gt;인터페이스 사용 시 고려할 점&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;인터페이스는 작게, 명확하게 (Small and Focused):&lt;/strong&gt; Go의 철학은 인터페이스를 매우 작게, 즉 1~2개의 메서드만 가지도록 정의하는 것입니다. 예를 들어, &lt;code&gt;io.Reader&lt;/code&gt;는 &lt;code&gt;Read&lt;/code&gt; 메서드 하나만, &lt;code&gt;io.Writer&lt;/code&gt;는 &lt;code&gt;Write&lt;/code&gt; 메서드 하나만 가집니다. 이렇게 작게 만들면 더 많은 타입이 그 인터페이스를 구현하기 쉬워지고, 코드의 재사용성이 극대화됩니다. &amp;quot;큰 인터페이스는 필요 없다&amp;quot;라는 격언도 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;인터페이스는 타입을 정의하는 사람이 아닌, 타입을 사용하는 사람이 정의한다:&lt;/strong&gt; 이 역시 Go의 중요한 철학 중 하나입니다. 라이브러리를 만들 때, 모든 기능을 포괄하는 거대한 인터페이스를 미리 정의하기보다는, 라이브러리를 사용하는 쪽에서 필요한 동작만을 정의하는 작은 인터페이스를 만들고, 라이브러리의 타입이 이를 만족하도록 하는 것이 좋습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;성능 오버헤드:&lt;/strong&gt; 인터페이스를 통한 메서드 호출은 구체 타입에 대한 직접 호출보다 약간의 런타임 오버헤드가 발생할 수 있습니다 (내부적으로 타입과 값을 확인하는 과정이 필요하기 때문). 하지만 대부분의 애플리케이션에서는 무시할 만한 수준이며, 인터페이스가 제공하는 설계적 이점이 훨씬 큽니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3&gt;결론&lt;/h3&gt;
&lt;p&gt;Go 언어의 &lt;strong&gt;인터페이스&lt;/strong&gt;는 추상화를 통해 코드의 유연성, 확장성, 재사용성을 극대화하는 핵심 기능입니다. &lt;strong&gt;덕 타이핑 원리에 기반한 암시적인 구현 방식&lt;/strong&gt;과 작고 명확하게 정의될 때 가장 큰 힘을 발휘한다는 점을 이해하는 것이 중요합니다.&lt;/p&gt;
&lt;p&gt;인터페이스를 효과적으로 활용하면 복잡한 시스템을 더 관리하기 쉬운 작은 단위로 분리하고, 미래의 변화에 유연하게 대응할 수 있는 Go 애플리케이션을 만들 수 있을 겁니다.&lt;/p&gt;</description>
      <category>Go언어</category>
      <category>go언어</category>
      <category>덕 타이핑</category>
      <category>인터페이스</category>
      <author>SKaSha</author>
      <guid isPermaLink="true">https://skasha.tistory.com/119</guid>
      <comments>https://skasha.tistory.com/119#entry119comment</comments>
      <pubDate>Sat, 2 Aug 2025 17:59:33 +0900</pubDate>
    </item>
    <item>
      <title>Go언어, 메서드와 함수</title>
      <link>https://skasha.tistory.com/118</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;Go 언어의 &lt;code&gt;메서드&lt;/code&gt; 깊이 파고들기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 함수 (Function): 독립적인 동작 단위&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Go 언어의 함수는 다른 프로그래밍 언어의 함수와 크게 다르지 않습니다. 특정 작업을 수행하고, 선택적으로 인자를 받아들이며, 값을 반환할 수 있는 독립적인 코드 블록입니다.&lt;/p&gt;
&lt;pre class=&quot;go&quot;&gt;&lt;code&gt;package main

import &quot;fmt&quot;

// greeting 함수는 문자열 인자를 받아 메시지를 출력합니다.
func greeting(name string) {
    fmt.Printf(&quot;안녕하세요, %s님!\n&quot;, name)
}

// add 함수는 두 개의 정수 인자를 받아 합을 반환합니다.
func add(a, b int) int {
    return a + b
}

func main() {
    greeting(&quot;김철수&quot;) // 함수 호출
    result := add(10, 20) // 함수 호출
    fmt.Println(&quot;합계:&quot;, result) // 출력: 합계: 30
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;독립성:&lt;/b&gt; 함수는 어떤 특정 데이터 타입에 묶여 있지 않습니다. 전역적으로 선언되거나 다른 함수 내부에 중첩될 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;호출 방식:&lt;/b&gt; &lt;code&gt;함수이름(인자)&lt;/code&gt; 형태로 직접 호출합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 메서드 (Method): 특정 타입에 속하는 동작&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;메서드&lt;/b&gt;는 &lt;b&gt;특정 타입에 바인딩(연결)된 함수&lt;/b&gt;입니다. 즉, 어떤 데이터(리시버)에 대해 수행되는 동작을 정의할 때 사용합니다. Go 언어에는 클래스 개념이 없기 때문에, 메서드는 객체 지향 프로그래밍의 '메서드'와 유사한 역할을 수행하며, '타입'을 기반으로 동작을 확장하는 강력한 수단이 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;중요한 점은, Go 언어에서는 구조체(struct) 내부에 직접 함수를 선언할 수 없다는 것입니다.&lt;/b&gt; 다른 객체 지향 언어의 클래스처럼 구조체 안에 메서드를 포함시키는 것이 아니라, 별도로 선언하되 특정 구조체(또는 다른 사용자 정의 타입)와 연결하는 방식을 사용합니다. 이때 사용되는 것이 바로 **리시버(Receiver)**입니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;메서드 선언 문법&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메서드는 함수 선언과 비슷하지만, 함수 이름 앞에 &lt;b&gt;리시버&lt;/b&gt;를 명시해야 합니다.&lt;/p&gt;
&lt;pre class=&quot;go&quot;&gt;&lt;code&gt;func (리시버변수 리시버타입) 메서드이름(매개변수) 반환타입 {
    // 메서드 본문
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;리시버 변수:&lt;/b&gt; 메서드 내부에서 리시버 타입의 인스턴스에 접근할 때 사용되는 변수 이름입니다. 관례적으로 타입 이름의 첫 글자를 소문자로 사용합니다 (예: &lt;code&gt;u&lt;/code&gt; for &lt;code&gt;User&lt;/code&gt;, &lt;code&gt;p&lt;/code&gt; for &lt;code&gt;Point&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;리시버 타입:&lt;/b&gt; 메서드가 바인딩될 특정 타입입니다. &lt;b&gt;구조체(struct)&lt;/b&gt; 타입이 가장 일반적이지만, **모든 사용자 정의 타입(정의된 타입)**에 메서드를 연결할 수 있습니다. (예: &lt;code&gt;type MyInt int&lt;/code&gt;와 같이 기본 타입을 새로 정의한 타입에도 가능)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;메서드 예시: 구조체와 함께 사용하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 흔한 메서드 사용 예시는 구조체와 결합하는 경우입니다.&lt;/p&gt;
&lt;pre class=&quot;go&quot;&gt;&lt;code&gt;package main

import &quot;fmt&quot;

// Person이라는 구조체 타입 정의
type Person struct {
    Name string
    Age  int
}

// Person 타입에 Greet 메서드 연결 (리시버는 값 타입)
// 이 메서드는 Person 구조체 인스턴스의 데이터를 읽어와서 사용합니다.
func (p Person) Greet() {
    fmt.Printf(&quot;안녕하세요, 제 이름은 %s이고, %d살입니다.\n&quot;, p.Name, p.Age)
}

// Person 타입에 Birthday 메서드 연결 (리시버는 포인터 타입)
// 이 메서드는 Person 구조체 인스턴스의 데이터를 변경합니다.
func (p *Person) Birthday() {
    p.Age++ // p가 포인터이므로 원본 Person의 Age를 직접 변경
    fmt.Printf(&quot;%s의 새 나이는 %d살입니다!\n&quot;, p.Name, p.Age)
}

func main() {
    // Person 구조체 인스턴스 생성
    person1 := Person{Name: &quot;김영희&quot;, Age: 25}

    // 메서드 호출: 리시버.메서드이름()
    person1.Greet() // 출력: 안녕하세요, 제 이름은 김영희이고, 25살입니다.

    // Birthday 메서드 호출
    person1.Birthday() // 출력: 김영희의 새 나이는 26살입니다!
    person1.Greet()    // 출력: 안녕하세요, 제 이름은 김영희이고, 26살입니다.
    // Birthday 메서드가 p *Person (포인터 리시버)로 선언되었기 때문에 person1의 Age가 실제로 변경됨

    // 포인터 리시버 vs 값 리시버의 중요성
    person2 := Person{Name: &quot;박철수&quot;, Age: 30}
    person2.Greet() // 안녕하세요, 제 이름은 박철수이고, 30살입니다.

    // Birthday 메서드 호출 전후 Age 값 확인
    fmt.Println(&quot;Birthday 메서드 호출 전:&quot;, person2.Age) // 30
    person2.Birthday() // 박철수의 새 나이는 31살입니다!
    fmt.Println(&quot;Birthday 메서드 호출 후:&quot;, person2.Age) // 31
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;포인터 리시버 vs. 값 리시버 (매우 중요!)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메서드를 선언할 때 리시버를 값 타입(&lt;code&gt;(p Person)&lt;/code&gt;)으로 할 것인지, 포인터 타입(&lt;code&gt;(p *Person)&lt;/code&gt;)으로 할 것인지 결정해야 합니다. 이는 Go 언어에서 매우 중요한 개념입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;값 리시버 (&lt;code&gt;(p Person)&lt;/code&gt;):&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메서드가 호출될 때, 리시버 인스턴스의 &lt;b&gt;복사본&lt;/b&gt;이 메서드에 전달됩니다.&lt;/li&gt;
&lt;li&gt;메서드 내에서 리시버의 필드를 변경하더라도, &lt;b&gt;원본 인스턴스에는 아무런 영향을 주지 않습니다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;주로 리시버의 값을 변경하지 않는 '읽기 전용' 작업을 수행할 때 사용됩니다.&lt;/li&gt;
&lt;li&gt;작은 구조체나 기본 타입의 경우 오버헤드가 적지만, 큰 구조체의 경우 복사 비용이 발생할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;포인터 리시버 (&lt;code&gt;(p *Person)&lt;/code&gt;):&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메서드가 호출될 때, 리시버 인스턴스의 **메모리 주소(포인터)**가 메서드에 전달됩니다.&lt;/li&gt;
&lt;li&gt;메서드 내에서 리시버의 필드를 변경하면, &lt;b&gt;원본 인스턴스의 데이터가 실제로 변경됩니다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;리시버의 상태를 변경해야 하는 '쓰기' 작업을 수행할 때 반드시 사용해야 합니다.&lt;/li&gt;
&lt;li&gt;복사 비용이 발생하지 않으므로 효율적입니다. Go에서는 대부분의 경우 포인터 리시버를 선호합니다. &lt;code&gt;nil&lt;/code&gt; 포인터일 수도 있으므로, 메서드 내에서 &lt;code&gt;nil&lt;/code&gt; 체크가 필요할 수도 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;메서드는 모든 사용자 정의 타입에 연결 가능&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메서드 기능은 구조체에만 국한되지 않습니다.&lt;br /&gt;&lt;code&gt;type MyInt int&lt;/code&gt;와 같이 기본 타입을 기반으로 새로운 타입을 정의한 경우에도 메서드를 연결할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;go&quot;&gt;&lt;code&gt;package main

import &quot;fmt&quot;

type Celsius float64 // float64 타입을 기반으로 Celsius라는 새 타입 정의

// Celsius 타입에 ToFahrenheit 메서드 연결
func (c Celsius) ToFahrenheit() float64 {
    return (c * 9 / 5) + 32
}

func main() {
    tempC := Celsius(25.0)
    tempF := tempC.ToFahrenheit()
    fmt.Printf(&quot;섭씨 %.2f도는 화씨 %.2f도입니다.\n&quot;, tempC, tempF)
    // 출력: 섭씨 25.00도는 화씨 77.00도입니다.
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것은 기존 타입에 새로운 동작을 추가하는 강력한 방법이며, 인터페이스와 결합될 때 더욱 빛을 발합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;함수와 메서드의 주요 차이점 비교&lt;/h3&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th align=&quot;left&quot;&gt;특징&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;함수 (Function)&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;메서드 (Method)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;b&gt;선언 방식&lt;/b&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;func 이름(인자) 반환타입 { ... }&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;func (리시버) 이름(인자) 반환타입 { ... }&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;b&gt;바인딩&lt;/b&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;특정 타입에 바인딩되지 않음 (독립적)&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;b&gt;특정 타입(리시버)에 바인딩됨&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;b&gt;선언 위치&lt;/b&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;어디서든 선언 가능&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;b&gt;구조체 내부에 선언 불가능&lt;/b&gt;, 구조체 외부에서 리시버를 통해 연결&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;b&gt;호출 방식&lt;/b&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;이름(인자)&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;리시버인스턴스.이름(인자)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;b&gt;목적&lt;/b&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;독립적인 작업 수행&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;특정 타입의 데이터에 대한 동작 수행&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;b&gt;OOP 유사성&lt;/b&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;일반적인 절차적 프로그래밍의 함수&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;객체 지향 프로그래밍의 '메서드' 역할과 유사&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;b&gt;캡슐화&lt;/b&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;직접적인 캡슐화 기능 없음&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;리시버를 통해 데이터와 동작을 함께 묶는 효과 제공&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;왜 Go는 '메서드'를 이렇게 설계했을까? (추가적인 통찰)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Go 언어는 클래스나 상속과 같은 전통적인 객체 지향 개념을 직접적으로 제공하지 않습니다.&lt;br /&gt;대신, **임베딩(Embedding)**과 &lt;b&gt;인터페이스(Interface)&lt;/b&gt;, 그리고 이 &lt;b&gt;메서드&lt;/b&gt; 기능을 통해 유연하고 확장 가능한 소프트웨어를 만들 수 있도록 설계되었습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;다형성 구현:&lt;/b&gt; 메서드는 인터페이스와 결합하여 Go의 다형성(Polymorphism)을 구현하는 핵심 요소입니다. 어떤 타입이 특정 메서드를 구현하면, 해당 타입은 그 인터페이스를 만족하게 됩니다. 이는 유연한 시스템 설계로 이어집니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;컴포지션 선호:&lt;/b&gt; Go는 상속보다 컴포지션(Composition)을 선호합니다. 메서드는 특정 타입에 동작을 연결함으로써, 작은 단위의 기능들을 조합하여 더 큰 기능을 만드는 컴포지션 접근 방식을 자연스럽게 지원합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;캡슐화 (Kind of):&lt;/b&gt; 리시버를 통해 메서드 내부에서 해당 타입의 데이터에 접근할 수 있게 함으로써, 해당 데이터와 관련된 동작들을 논리적으로 묶어 캡슐화와 유사한 효과를 얻을 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;결론&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Go 언어의 &lt;b&gt;함수&lt;/b&gt;는 독립적인 작업을 수행하는 코드 블록인 반면, &lt;b&gt;메서드&lt;/b&gt;는 특정 타입에 속하는 동작을 정의하여 해당 타입의 인스턴스에 대한 작업을 수행하도록 합니다. 특히 &lt;b&gt;구조체 내부에 함수를 직접 선언할 수 없으므로, 구조체의 동작을 정의하기 위해 메서드를 활용한다는 점&lt;/b&gt;이 중요합니다. 또한 포인터 리시버와 값 리시버의 차이를 명확히 이해하고 적절히 사용하는 것이 Go 언어의 효율성과 안전성을 높이는 데 핵심입니다.&lt;/p&gt;</description>
      <category>Go언어</category>
      <category>go언어</category>
      <category>값 리시버</category>
      <category>메서드</category>
      <category>포인터 리시버</category>
      <category>함수</category>
      <author>SKaSha</author>
      <guid isPermaLink="true">https://skasha.tistory.com/118</guid>
      <comments>https://skasha.tistory.com/118#entry118comment</comments>
      <pubDate>Sat, 2 Aug 2025 17:46:08 +0900</pubDate>
    </item>
    <item>
      <title>Go언어, 동적 배열 `슬라이스`</title>
      <link>https://skasha.tistory.com/117</link>
      <description>&lt;h2&gt;배열과 슬라이스, 뭐가 다를까?&lt;/h2&gt;
&lt;h3&gt;1. 배열 (Array): 고정된 약속&lt;/h3&gt;
&lt;p&gt;Go 언어에서 &lt;strong&gt;배열&lt;/strong&gt;은 한 번 선언되면 &lt;strong&gt;크기가 절대 변하지 않는&lt;/strong&gt; 데이터 묶음입니다. 즉, 배열을 만들 때 몇 개의 요소를 담을지 미리 정하고, 그 개수는 프로그램이 끝날 때까지 바뀌지 않아요.&lt;/p&gt;
&lt;p&gt;예를 들어, &lt;code&gt;var scores [5]int&lt;/code&gt;라고 선언하면, &lt;code&gt;scores&lt;/code&gt;라는 배열은 무조건 &lt;code&gt;int&lt;/code&gt; 타입의 숫자 5개만 담을 수 있습니다. 6번째 숫자를 넣고 싶어도 공간이 없어서 불가능하죠.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;package main

import &amp;quot;fmt&amp;quot;

func main() {
    var numbers [3]int // int 타입 3개를 담을 수 있는 배열 선언
    numbers[0] = 10
    numbers[1] = 20
    numbers[2] = 30
    // numbers[3] = 40 // Error! 배열의 크기를 벗어남

    fmt.Println(&amp;quot;배열:&amp;quot;, numbers) // 배열: [10 20 30]
    fmt.Printf(&amp;quot;배열의 타입: %T\n&amp;quot;, numbers) // 배열의 타입: [3]int
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위 코드에서 보듯이, &lt;code&gt;[3]int&lt;/code&gt;는 &lt;code&gt;[4]int&lt;/code&gt;와는 아예 다른 타입으로 간주됩니다. 크기가 곧 타입의 일부가 되는 거죠. 이런 특성 때문에 배열은 특정 크기가 고정되어야 하는 상황(예: 이미지의 RGB 값 묶음, 암호화 블록 등)이 아니면 일반적인 프로그래밍에서는 자주 사용되지 않습니다.&lt;/p&gt;
&lt;h3&gt;2. 슬라이스 (Slice): 유연한 참조&lt;/h3&gt;
&lt;p&gt;반면에 &lt;strong&gt;슬라이스&lt;/strong&gt;는 &lt;strong&gt;동적으로 크기가 변할 수 있는&lt;/strong&gt; 데이터 묶음입니다. 이름만 들으면 &amp;quot;동적 배열&amp;quot; 같지만, 사실 슬라이스는 &lt;strong&gt;배열의 일부를 참조하는(가리키는) 개념&lt;/strong&gt;입니다. 내부적으로는 배열을 사용하지만, 우리는 슬라이스를 통해 더 유연하게 데이터를 다룰 수 있죠.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;package main

import &amp;quot;fmt&amp;quot;

func main() {
    // 슬라이스 리터럴로 선언 및 초기화
    mySlice := []string{&amp;quot;apple&amp;quot;, &amp;quot;banana&amp;quot;, &amp;quot;cherry&amp;quot;}
    fmt.Println(&amp;quot;슬라이스:&amp;quot;, mySlice) // 슬라이스: [apple banana cherry]
    fmt.Printf(&amp;quot;슬라이스의 타입: %T\n&amp;quot;, mySlice) // 슬라이스의 타입: []string

    // 요소 추가 (append 함수 사용)
    mySlice = append(mySlice, &amp;quot;date&amp;quot;)
    fmt.Println(&amp;quot;추가 후 슬라이스:&amp;quot;, mySlice) // 추가 후 슬라이스: [apple banana cherry date]
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;보시다시피, 슬라이스는 선언할 때 길이를 명시하지 않고 &lt;code&gt;[]타입&lt;/code&gt; 형태로 선언합니다. 그리고 &lt;code&gt;append()&lt;/code&gt; 함수를 사용해 얼마든지 요소를 추가할 수 있어요. 이것이 배열과의 결정적인 차이점이죠!&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;슬라이스를 더 깊이 파헤치기: 길이, 용량, 그리고 &lt;code&gt;make()&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;슬라이스를 제대로 이해하려면 **길이(Length)**와 **용량(Capacity)**이라는 두 가지 중요한 개념을 알아야 합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;길이 (Length):&lt;/strong&gt; 현재 슬라이스가 가지고 있는 요소의 개수입니다. &lt;code&gt;len()&lt;/code&gt; 함수로 확인할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;용량 (Capacity):&lt;/strong&gt; 슬라이스가 참조하고 있는 **기저 배열(underlying array)**이 최대로 수용할 수 있는 요소의 개수입니다. &lt;code&gt;cap()&lt;/code&gt; 함수로 확인할 수 있습니다. 즉, 슬라이스가 현재 가지고 있는 요소 외에 더 추가할 수 있는 공간의 총량이죠.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;make()&lt;/code&gt; 함수로 슬라이스 생성하기&lt;/h3&gt;
&lt;p&gt;슬라이스는 주로 &lt;code&gt;make()&lt;/code&gt; 함수를 사용해서 생성합니다. &lt;code&gt;make()&lt;/code&gt;는 슬라이스의 초기 &lt;strong&gt;길이&lt;/strong&gt;와 &lt;strong&gt;용량&lt;/strong&gt;을 지정할 수 있게 해줍니다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;make([]Type, length, capacity)&lt;/code&gt; 형태로 사용하며, &lt;code&gt;capacity&lt;/code&gt;는 생략 가능합니다. 생략하면 &lt;code&gt;length&lt;/code&gt;와 동일하게 설정됩니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;package main

import &amp;quot;fmt&amp;quot;

func main() {
    // 길이 5, 용량 5인 슬라이스 생성 (초기값은 int의 제로 값인 0)
    s1 := make([]int, 5)
    fmt.Printf(&amp;quot;s1: %v, 길이: %d, 용량: %d\n&amp;quot;, s1, len(s1), cap(s1))
    // s1: [0 0 0 0 0], 길이: 5, 용량: 5

    // 길이 0, 용량 10인 슬라이스 생성
    s2 := make([]string, 0, 10)
    fmt.Printf(&amp;quot;s2: %v, 길이: %d, 용량: %d\n&amp;quot;, s2, len(s2), cap(s2))
    // s2: [], 길이: 0, 용량: 10

    // s2에 요소 추가
    s2 = append(s2, &amp;quot;Go&amp;quot;, &amp;quot;Lang&amp;quot;, &amp;quot;Programming&amp;quot;)
    fmt.Printf(&amp;quot;s2 (append 후): %v, 길이: %d, 용량: %d\n&amp;quot;, s2, len(s2), cap(s2))
    // s2 (append 후): [Go Lang Programming], 길이: 3, 용량: 10
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;s2&lt;/code&gt;의 예시를 보면, 처음에는 길이가 0이었지만 용량이 10이었기 때문에 &lt;code&gt;append&lt;/code&gt;를 통해 3개의 요소를 추가해도 용량 범위 내에서 길이만 늘어났음을 알 수 있습니다.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;append()&lt;/code&gt;와 용량 확장&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;append()&lt;/code&gt; 함수로 슬라이스에 요소를 추가할 때, 현재 용량이 부족하면 Go 런타임은 자동으로 더 큰 새 &lt;strong&gt;기저 배열&lt;/strong&gt;을 만들고 기존 요소를 새 배열로 복사한 다음, 새 요소를 추가합니다. 그리고 슬라이스는 이 새로운 기저 배열을 참조하게 됩니다.&lt;/p&gt;
&lt;p&gt;이 과정에서 일반적으로 기존 용량의 2배로 용량을 확장하는 전략을 사용하므로, 잦은 &lt;code&gt;append()&lt;/code&gt;가 발생하더라도 성능 저하를 최소화할 수 있습니다. 하지만 매우 많은 요소를 한 번에 추가해야 할 때는 미리 충분한 용량을 확보해두는 것이 좋습니다.&lt;/p&gt;
&lt;h3&gt;슬라이싱(Slicing): 슬라이스에서 슬라이스 만들기&lt;/h3&gt;
&lt;p&gt;이미 존재하는 배열이나 슬라이스의 일부분을 잘라내어 새로운 슬라이스를 만들 수 있는데, 이를 &lt;strong&gt;슬라이싱&lt;/strong&gt;이라고 합니다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;[low:high]&lt;/code&gt; 또는 &lt;code&gt;[low:high:capacity]&lt;/code&gt; 형태로 사용합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;low&lt;/code&gt;: 시작 인덱스 (포함)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;high&lt;/code&gt;: 끝 인덱스 (포함하지 않음)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;capacity&lt;/code&gt; (선택 사항): 생성될 슬라이스의 용량을 지정&lt;/li&gt;
&lt;/ul&gt;
&lt;!-- end list --&gt;

&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;package main

import &amp;quot;fmt&amp;quot;

func main() {
    // 기존 배열에서 슬라이스 만들기
    array := [6]int{10, 20, 30, 40, 50, 60}
    s := array[1:4] // 인덱스 1부터 4 전까지 (20, 30, 40)
    fmt.Printf(&amp;quot;s: %v, 길이: %d, 용량: %d\n&amp;quot;, s, len(s), cap(s))
    // s: [20 30 40], 길이: 3, 용량: 5 (array[1]부터 array의 끝까지의 공간)

    // 기존 슬라이스에서 슬라이스 만들기
    original := []string{&amp;quot;A&amp;quot;, &amp;quot;B&amp;quot;, &amp;quot;C&amp;quot;, &amp;quot;D&amp;quot;, &amp;quot;E&amp;quot;}
    sub := original[2:5] // 인덱스 2부터 5 전까지 (C, D, E)
    fmt.Printf(&amp;quot;sub: %v, 길이: %d, 용량: %d\n&amp;quot;, sub, len(sub), cap(sub))
    // sub: [C D E], 길이: 3, 용량: 3 (original[2]부터 original의 끝까지의 공간)

    // 원본 변경 시 슬라이스 영향
    original[2] = &amp;quot;X&amp;quot;
    fmt.Println(&amp;quot;original 변경 후 sub:&amp;quot;, sub) // original 변경 후 sub: [X D E]
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;마지막 예시를 보면 &lt;code&gt;original&lt;/code&gt; 슬라이스의 요소를 변경하니 &lt;code&gt;sub&lt;/code&gt; 슬라이스도 함께 변경되는 것을 볼 수 있습니다. 이는 슬라이스가 &lt;strong&gt;참조 타입&lt;/strong&gt;이기 때문에 원본 배열/슬라이스의 데이터를 공유하기 때문입니다. 이 점을 꼭 기억해야 예상치 못한 버그를 줄일 수 있습니다!&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;그래서 언제 무엇을 써야 할까?&lt;/h2&gt;
&lt;p&gt;Go 언어에서 데이터를 묶어 다룰 때는 &lt;strong&gt;대부분의 경우 슬라이스를 사용&lt;/strong&gt;하세요.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;슬라이스:&lt;/strong&gt; 크기가 가변적이어서 요소를 추가하거나 삭제하는 등 유연한 작업이 필요할 때. 네트워크 통신으로 받은 데이터, 사용자 입력 목록 등 크기를 예측하기 어려운 경우에 최적입니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;배열:&lt;/strong&gt; 크기가 &lt;strong&gt;절대 변하지 않고 고정되어야 하는&lt;/strong&gt; 매우 드문 경우에만 고려하세요. (예: 행렬 연산, 특정 하드웨어 인터페이스 등)&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Go언어</category>
      <category>go언어</category>
      <category>slice</category>
      <category>slicing</category>
      <category>배열</category>
      <category>슬라이스</category>
      <category>슬라이싱</category>
      <author>SKaSha</author>
      <guid isPermaLink="true">https://skasha.tistory.com/117</guid>
      <comments>https://skasha.tistory.com/117#entry117comment</comments>
      <pubDate>Sat, 2 Aug 2025 17:26:34 +0900</pubDate>
    </item>
  </channel>
</rss>