ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • golang: 배열(Array)과 슬라이스(Slice)
    Back-End/Golang 2024. 5. 11. 13:10
    반응형

    array vs slice

    I. 정의 및 기본 개념

    배열(Array): 배열은 Go에서 고정된 크기의 연속적인 메모리 영역에 동일한 타입의 요소들을 순차적으로 저장하는 데이터 구조입니다. 배열의 길이는 선언 시에 정해지며, 이후에는 변경할 수 없습니다.

     

    배열 초기화 방법

    1. 기본 초기화: 배열을 선언하고, Go의 기본값으로 초기화됩니다. 숫자 타입의 배열은 0, 불리언 배열은 false, 포인터와 슬라이스, 맵, 채널은 nil로 초기화됩니다.

    var arr1 [3]int

     

    2. 리터럴을 사용한 초기화: 배열 선언과 동시에 특정 값으로 초기화합니다.

    arr2 := [3]int{10, 20, 30}

     

    3. 인덱스 지정 초기화: 특정 인덱스에 값을 할당합니다. 할당되지 않은 인덱스는 타입의 기본값으로 초기화됩니다.

    arr3 := [5]int{1: 10, 4: 20}  // [0, 10, 0, 0, 20]

     

    슬라이스 초기화 방법

    1. 리터럴을 사용한 초기화: 슬라이스를 선언하고 값으로 직접 초기화합니다.

    slice1 := []int{10, 20, 30}

     

    2. make 함수 사용: make 함수를 사용하여 슬라이스를 초기화할 수 있습니다. 이 방법을 사용하면 길이와 용량을 지정할 수 있습니다.

    slice2 := make([]int, 3)  // 길이가 3인 슬라이스, 모든 요소는 0으로 초기화됩니다.
    slice3 := make([]int, 3, 5)  // 길이가 3이고 용량이 5인 슬라이스

     

    3. 배열에서 슬라이스 생성: 기존 배열의 일부 또는 전체를 참조하여 슬라이스를 생성합니다.

    arr := [5]int{1, 2, 3, 4, 5}
    slice4 := arr[1:4]  // arr의 1번 인덱스부터 3번 인덱스까지 참조하는 슬라이스 [2, 3, 4]

     

     이러한 방법들을 사용하여 Go 언어에서 배열과 슬라이스를 다양한 상황에 맞춰 유연하게 초기화할 수 있습니다. 각 방법은 특정 사용 사례에 따라 선택될 수 있으며, 초기화 방법을 잘 이해하는 것이 중요합니다.

     

    슬라이스(Slice): 슬라이스는 배열을 추상화하여 더 동적으로 사용할 수 있게 해주는 데이터 구조입니다. 슬라이스는 길이와 용량을 가지며, 필요에 따라 이들이 자동으로 조절됩니다. 슬라이스는 내부적으로는 배열에 대한 참조를 가지고 있어, 슬라이스의 요소를 변경하면 참조하고 있는 배열의 해당 요소도 변경됩니다.

     

    II. 크기 변경 가능성

    Go 언어에서 배열의 크기는 고정되어 있기 때문에 한 번 선언되면 크기를 변경할 수 없습니다. 그러나 슬라이스는 동적으로 크기를 변경할 수 있는 매우 유연한 데이터 구조입니다. 슬라이스의 크기를 변경하는 방법과 실무에서 유용하게 사용할 수 있는 몇 가지 기술을 소개하겠습니다.

    슬라이스 크기 변경 ++

    배열: 배열의 크기는 변경할 수 없습니다. 만약 더 많은 요소를 저장해야 한다면 새로운 배열을 만들고 요소를 복사해야 합니다.

     

    슬라이스: 슬라이스는 append 함수를 사용하여 동적으로 크기를 변경할 수 있습니다. 슬라이스의 용량(capacity)이 현재의 길이(length)보다 큰 경우, 슬라이스에 더 많은 요소를 추가할 수 있습니다. 용량을 초과하는 경우, 자동으로 더 큰 용량의 새 배열을 할당받고 기존 요소들을 새 배열로 복사합니다.

     

    1. append 함수 사용: append 함수는 슬라이스에 하나 이상의 요소를 추가하고, 필요한 경우 자동으로 용량을 늘립니다. 이 함수는 수정된 슬라이스를 반환합니다.

    slice := []int{1, 2, 3}
    slice = append(slice, 4, 5)  // 슬라이스에 4와 5를 추가

    2. 슬라이스 재슬라이싱: 슬라이스의 일부를 재슬라이싱하여 크기를 조정할 수 있습니다. 이 방법은 기존 슬라이스의 용량을 초과할 수 없습니다.

    original := []int{1, 2, 3, 4, 5}
    sliced := original[1:3]  // [2, 3]

     

    슬라이스 크기 변경 --

    1. 슬라이싱을 이용한 크기 조정: 슬라이스의 크기를 줄이는 가장 기본적인 방법은 슬라이싱을 사용하여 슬라이스의 특정 부분만을 참조하는 새로운 슬라이스를 생성하는 것입니다. 이 방법은 기존 슬라이스의 길이를 직접적으로 조절하지는 않지만, 필요한 부분만을 추출하여 사용할 수 있습니다.

    s := []int{1, 2, 3, 4, 5}
    s = s[:3]  // 슬라이스의 크기를 줄임, 결과는 [1, 2, 3]

     

    2. 길이 재설정: len 함수를 사용하여 슬라이스의 현재 길이를 확인하고, 이를 변경하기 위해 슬라이싱을 사용할 수 있습니다. 이 방법은 슬라이스의 '끝'을 잘라내고 싶을 때 유용합니다.

    s := []int{1, 2, 3, 4, 5}
    if len(s) > 3 {
        s = s[:3]  // 슬라이스의 길이를 3으로 조정
    }

     

    3. 특정 요소 제거: 슬라이스에서 하나 이상의 특정 요소를 제거하고 싶을 때, append와 슬라이싱을 함께 사용하여 슬라이스의 중간 요소들을 제거할 수 있습니다.

    s := []int{1, 2, 3, 4, 5}
    i := 2  // 제거하고자 하는 요소의 인덱스
    
    // 인덱스 i의 요소를 제거
    s = append(s[:i], s[i+1:]...)
    // 결과: [1, 2, 4, 5]

     

    4. 용량 조절을 통한 최적화: 슬라이스의 길이를 줄이는 것과 별개로, 슬라이스의 용량(capacity)을 조절하려면 새로운 메모리 할당을 통해 이를 수행할 수 있습니다. 이 방법은 슬라이스의 용량을 실제 사용 중인 길이에 맞추어 메모리 사용을 최적화하고자 할 때 사용됩니다.

    s := []int{1, 2, 3, 4, 5}
    s = s[:3]  // 길이를 줄임
    s = append([]int(nil), s...)  // 새로운 슬라이스에 복사하여 용량 조정

     

    슬라이스 사용 팁

    1. 용량을 사전에 할당하기: make 함수를 사용하여 슬라이스의 초기 용량을 미리 할당함으로써 append 호출 시 발생하는 재할당을 최소화할 수 있습니다. 이는 특히 반복적인 append 작업이 필요할 때 성능을 향상시킬 수 있습니다.

    slice := make([]int, 0, 100)  // 길이는 0, 용량은 100
    for i := 0; i < 100; i++ {
        slice = append(slice, i)
    }

     

    2. 슬라이스 용량 확인하기: cap 함수를 사용하여 슬라이스의 용량을 확인할 수 있습니다. 이 정보는 용량을 초과하는 추가가 발생할 때 유용합니다.

    fmt.Println(cap(slice))  // 슬라이스의 현재 용량 출력

     

    3. 슬라이스 복사하기: 슬라이스의 데이터를 다른 슬라이스로 복사하려면 copy 함수를 사용합니다. 이 방법은 두 슬라이스의 데이터가 서로 독립적이어야 할 때 사용됩니다.

    src := []int{1, 2, 3}
    dst := make([]int, len(src))
    copy(dst, src)  // src의 내용을 dst로 복사

     

    4. 슬라이스에서 요소 제거하기: 슬라이스에서 특정 위치의 요소를 제거하려면 슬라이스를 재조합해야 합니다. Go에서는 슬라이스 병합을 위해 append 함수를 사용합니다.

    a := []int{1, 2, 3, 4, 5}
    i := 2  // 제거할 요소의 인덱스
    a = append(a[:i], a[i+1:]...)  // 인덱스 i의 요소를 제거

     이러한 기술들은 Go에서 슬라이스를 효율적으로 관리하고 성능을 최적화하는 데 도움이 됩니다. 데이터 구조를 효과적으로 활용하여 코드의 성능과 유지보수성을 높일 수 있습니다.

     

    III. 성능

    Go 언어에서 배열과 슬라이스의 성능 비교는 몇 가지 주요 요소에 따라 달라질 수 있습니다. 각각의 데이터 구조의 특성을 고려해보면, 성능에 영향을 미치는 주된 요소들을 다음과 같이 정리할 수 있습니다.

    1. 메모리 할당

    배열:

    • 배열은 선언 시에 지정된 크기에 따라 연속된 메모리 블록이 할당됩니다.
    • 메모리는 정적으로 할당되기 때문에 배열의 크기를 실행 시간에 변경할 수 없습니다.
    • 배열의 크기가 컴파일 시점에 결정되므로 메모리 관리 측면에서 예측 가능합니다.

    슬라이스:

    • 슬라이스는 동적으로 크기가 조정될 수 있는 래퍼로, 내부적으로 배열을 가리키는 포인터, 길이, 용량을 관리합니다.
    • 초기 할당 시에는 작은 메모리를 사용하지만, append 연산을 통해 요소가 추가될 때 용량이 초과되면 새로운 더 큰 배열로 자동으로 확장됩니다.
    • 이 과정에서 기존의 데이터를 새 배열로 복사하는 오버헤드가 발생할 수 있습니다.

    2. 접근 속도

    배열:

    • 메모리 상에서 연속된 블록에 데이터가 저장되므로 배열의 요소에 대한 접근은 매우 빠릅니다.
    • 인덱스를 통해 직접적으로 메모리 위치에 접근할 수 있어, 요소 접근 시간이 상수 시간(O(1))입니다.

    슬라이스:

    • 슬라이스 역시 내부적으로 배열을 사용하기 때문에 요소 접근 속도는 배열과 거의 동일합니다.
    • 그러나 슬라이스의 경우 길이와 용량을 관리하는 추가적인 메커니즘이 있어 이론적으로는 배열보다 미세하게 느릴 수 있습니다.

    3. 데이터 추가 및 제거

    배열:

    • 배열에 요소를 추가하거나 제거하는 것은 직접적으로 지원되지 않습니다. 새로운 크기의 배열을 생성하고 원하는 요소를 복사해야 합니다.
    • 이런 이유로 데이터의 추가나 제거가 빈번한 경우 배열 사용은 권장되지 않습니다.

    슬라이스:

    • 슬라이스는 append 함수를 사용하여 쉽게 요소를 추가할 수 있으며, 슬라이싱을 통해 요소를 효과적으로 제거할 수 있습니다.
    • 데이터 추가 시 내부 배열의 용량이 충분하지 않으면, 새로운 배열을 할당하고 기존 데이터를 복사하는 비용이 발생하므로 성능 저하를 일으킬 수 있습니다.

    4. 용도에 따른 성능 결정

    • 고정 크기 데이터: 고정된 수의 요소를 다루는 경우 배열이 메모리 사용과 성능 면에서 이점을 제공합니다.
    • 동적 데이터: 요소의 추가 및 제거가 자주 일어나는 경우 슬라이스가 훨씬 편리하며, 적절한 용량 계획을 통해 성능 저하를 최소화할 수 있습니다.

    실제 애플리케이션에서 이러한 차이를 고려하여 데이터 구조를 선택하는 것이 중요하며, 구현하려는 기능과 성능 요구사항에 따라 적절한 선택이 달라질 수 있습니다.

     

    IV. 메모리 할당 및 관리 방식

    배열과 슬라이스의 메모리 할당과 관리 방식은 Go 프로그래밍에서 중요한 차이점을 나타냅니다. 이는 특히 데이터의 초기화와 복사 과정에서 두드러지게 나타납니다.

    배열의 메모리 할당 및 복사

    배열 초기화: 배열을 초기화할 때, 고정된 크기의 메모리 블록이 할당됩니다. 배열의 크기는 선언 시에 명시되며, 이 크기는 변경될 수 없습니다. 예를 들어, var a [5]int는 5개의 정수를 저장할 수 있는 메모리 공간을 할당합니다.

    배열 복사: 배열을 다른 배열에 할당하면, 배열의 전체 내용이 복사됩니다. 이는 배열 간의 독립성을 보장하지만, 큰 배열을 다룰 때는 메모리 사용과 성능에 영향을 줄 수 있습니다. 예를 들어, b := a를 사용하면 배열 a의 모든 요소가 배열 b로 복사됩니다.

    슬라이스의 메모리 할당 및 복사

    슬라이스 초기화: 슬라이스는 동적 배열로 사용됩니다. 슬라이스는 내부적으로 포인터, 길이(length), 용량(capacity)를 가지고 있어, 필요에 따라 자동으로 크기 조정이 가능합니다. 슬라이스를 초기화할 때, make 함수를 사용하면 특정 길이와 용량을 가진 배열이 내부적으로 생성됩니다. 예를 들어, s := make([]int, 5, 10)는 길이가 5이고 용량이 10인 슬라이스를 생성하고, 이에 대한 배열을 내부적으로 할당합니다.

    슬라이스 복사: 슬라이스를 다른 슬라이스에 할당하면, 내부 배열에 대한 참조가 복사됩니다. 이는 슬라이스가 같은 배열을 가리키게 하므로, 한 슬라이스에서의 변경이 다른 슬라이스에도 영향을 미칩니다. 슬라이스의 복사본을 만들고 싶다면, copy 함수를 사용하여 데이터의 실제 복사본을 생성해야 합니다. 예를 들어, t := make([]int, len(s)); copy(t, s)는 s의 내용을 t로 독립적으로 복사합니다.

    성능 관점

    • 배열: 크기 변경이 불가능하므로 메모리 재할당이 필요 없습니다. 그러나 크기가 큰 배열의 복사는 비용이 많이 들 수 있습니다.
    • 슬라이스: 초기 메모리 할당이 더 유연하지만, append 등을 통해 용량을 초과하는 경우 새로운 배열로의 복사가 필요하며 이는 추가적인 비용을 발생시킵니다.

    이러한 차이는 데이터의 용도와 성능 요구사항에 따라 적절한 선택을 요구합니다. 크기가 고정되거나 배열의 전체 복사가 문제가 되지 않는 경우 배열을 사용하고, 데이터 크기가 변동될 가능성이 있거나 부분적 참조가 빈번한 경우에는 슬라이스를 사용하는 것이 바람직합니다.

     

    V. 사용 시나리오

     배열과 슬라이스의 선택은 Go 프로그래밍에서 중요한 결정 중 하나이며, 각각의 데이터 구조가 제공하는 특성에 따라 최적의 사용 시나리오가 있습니다. 보다 세부적이고 고급스러운 설명을 위해 각 데이터 구조의 사용 시나리오를 아래와 같이 자세히 설명하겠습니다.

    배열의 사용 시나리오

    1. 데이터 크기가 명확하고 변경되지 않을 때: 배열은 고정된 크기를 가지고 있기 때문에, 배열을 사용하려는 데이터의 개수가 변경될 가능성이 없을 때 적합합니다. 예를 들어, 주어진 일수의 온도 데이터나 고정된 숫자의 구성 요소를 가진 설정 값들을 저장할 때 배열을 사용할 수 있습니다.

    2. 성능이 중요한 상황에서의 빠른 요소 접근: 배열은 메모리 내에서 연속된 공간에 할당되므로, 어떤 인덱스의 요소에도 즉시 접근이 가능합니다. 이는 CPU 캐시 활용을 최적화하여 성능을 향상시킬 수 있습니다. 예를 들어, 실시간 시스템이나 고성능 컴퓨팅에서 매우 빈번하게 접근하는 데이터 구조로 사용됩니다.

    3. 메모리 사용 최적화: 메모리 할당을 컴파일 시점에 결정하므로, 런타임 중에 발생할 수 있는 메모리 할당 오버헤드가 없습니다. 이는 특히 임베디드 시스템이나 메모리 자원이 제한된 환경에서 중요할 수 있습니다.

    슬라이스의 사용 시나리오

    1. 동적 크기 조정이 필요할 때: 슬라이스는 append와 같은 함수를 사용하여 동적으로 요소를 추가하거나 제거할 수 있습니다. 데이터의 크기가 실행 중에 변경될 수 있는 경우, 예를 들어 사용자 입력에 따라 데이터가 추가되거나 삭제되는 인터랙티브 애플리케이션에서 슬라이스가 매우 유용합니다.

    2. 부분적인 데이터 처리: 슬라이스는 기존의 배열 또는 다른 슬라이스로부터 부분적으로 데이터를 참조할 수 있습니다. 이 특성은 데이터의 큰 집합에서 작은 부분집합을 효율적으로 처리할 때 유리하며, 데이터를 복사할 필요 없이 작업할 수 있습니다. 예를 들어, 로그 파일의 특정 부분을 분석하거나 이미지 데이터의 부분적 처리에 사용될 수 있습니다.

    3. 함수 간의 데이터 전달: Go에서 슬라이스는 참조 타입이기 때문에, 함수 간에 데이터를 전달할 때 복사본을 생성하지 않고 메모리 효율적인 방식으로 전달됩니다. 이는 특히 대량의 데이터를 다루는 애플리케이션에서 메모리 사용량을 줄이는 데 도움이 됩니다.

    4. 유연한 데이터 구조 조작: 슬라이스는 길이와 용량이 분리되어 있어, 데이터를 조작하는 과정에서 다양한 연산을 유연하게 수행할 수 있습니다. 예를 들어, append, copy, slice 등의 기능을 통해 데이터를 동적으로 재구성하고, 필요에 따라 메모리를 재할당하는 등의 작업을 효과적으로 수행할 수 있습니다.

    각각의 데이터 구조는 고유의 장점을 가지고 있으며, 애플리케이션의 요구 사항과 성능 목표에 따라 적절하게 선택되어야 합니다.

     

     

    반응형

    'Back-End > Golang' 카테고리의 다른 글

    golang: Tee 패턴이란?  (1) 2024.05.15
    golang: 파이프라인(pipeline)이란?  (0) 2024.05.15
    고루틴 누수(Goroutine Leak)란?  (0) 2024.05.10
    golang: 1급 시민(First-class citizen)이란?  (1) 2024.04.28
    golang: Go 언어의 장점  (1) 2024.04.21
Designed by Tistory.