튜플 (Tuple): 데이터의 집합

튜플(Tuple)은 Swift에 존재하는 두 가지 복합 타입 (compound type) 중 하나입니다.

복합 타입에는 함수 타입(Function type)과 튜플 타입(Tuple type)의 두 가지가 있습니다.

출처: Swift 공식 문서 Types

 

특징

튜플의 특징은 다음과 같습니다.

    여러 값을 동시에 표현 및 저장할 수 있다. (데이터의 집합)

    예를 들어 x, y 좌표를 나타내는 타입을 만들고 싶다고 합니다. Coordinate라는 이름의 struct를 만들 수도 있지만(이에 대한 내용은 생략합니다), 그보다 더 간단하게 튜플을 사용해서 x, y 좌표를 나타낼 수 있습니다.

    아래는 좌표 x, y를 튜플 타입인 (x: Int, y: Int)로 나타낸 예제입니다.

    var coordinate = (x: 10, y: 12)   // coordinate는  (x: Int, y: Int) 타입의 변수입니다.
    coordinate = (x: 4, y: 42)        // OK: 이름이 매치됩니다.
    coordinate = (9, 99)              // OK: 이름을 적지 않으면 자동으로 유추(inferred)됩니다.
    coordinate = (left: 5, right: 5)  // 오류: 이름이 매치되지 않습니다.
    
    coordinate = (x: 100, y: 200)
    coordinate.x // 100
    coordinate.y // 200
    
    var anotherCoordinate = (300, 400)
    anotherCoordinate.0 // 300
    anotherCoordinate.1 // 400
    
    var circle = (radius: 10, coord: (x: 30, y: 50))
    var.radius // 10
    var.coord = (x: 30, y: 50)
    var.coord.x // 30

     

    위 코드를 통해 튜플의 속성을 알아보면

    • 튜플에는 요소의 특성을 나타내는 이름이 붙을 수 있습니다.
      • 고유의 이름을 통해 x, y의 값을 별도로 관리할 수 있습니다. 마치 구조체, 클래스 등에서 사용하던 것처럼 이름을 붙여 사용하면 됩니다..
      • 이름을 붙여도 나중에 사용할 때 자동 유추(inferred)되어 생략할 수 있습니다. 이 점이 다른 타입 (구조체, 클래스, 함수 등)과 구분되는 특징입니다.
      • 단, 이름이 있는 튜플은 이름을 붙여 사용할거라면 반드시 정확하게 일치하는 이름을 사용해야 합니다.
    • 처음부터 이름이 없는 튜플을 만드는 것도 가능합니다.
      • var coordinate = (10, 12)로 만들어 사용할 수도 있습니다.
      • 이 때의 타입은 (Int, Int) 이 됩니다.
      • 이름이 없는 튜플에서 요소를 사용하는 방법은 숫자 0, 1, 2 ... 등을 붙입니다.
    • 튜플에는 Swift에서 사용할 수 있는 거의 대부분의 타입을 추가할 수 있습니다.
    • 또한 튜플 안에 튜플을 겹중으로 삽입할 수 있습니다.

     

    장단점

    구조체를 만드는 것보다 튜플을 통해 만들었을 때의 장단점에 대해 알아보겠습니다.

    예를 들어 동물이라는 구조체를 만든다고 하면

    struct 동물 {
      var 이름: String
      var 종: String
      var 무게: Int
    }
    
    var 니네집강아지 = 동물(이름: "뽀삐", 종: "요크셔테리어", 무게: 30)
    print("니네집강아지 이름은 \(니네집강아지.이름)이며, 종은 \(니네집강아지.종) 입니다. 무게는 \(니네집강아지.무게)kg에요.")

     

    이것을 튜플을 통해 다음과 같이 간편하게 구현할 수 있습니다.

    var 니네집강아지 = (이름: "뽀삐", 종: "요크셔테리어", 무게: 30)
    // 또는
    var 니네집강아지 = ("뽀삐", "요크셔테리어", 30)
    
    print("니네집강아지 이름은 \(니네집강아지.이름)이며, 종은 \(니네집강아지.종) 입니다. 무게는 \(니네집강아지.무게)kg에요.")
    // 또는
    print("니네집강아지 이름은 \(니네집강아지.0)이며, 종은 \(니네집강아지.1) 입니다. 무게는 \(니네집강아지.2)kg에요.")
    • 장점
      • 보다 간단한 문법과 과정을 통해 자료들을 관리할 수 있습니다.
      • 사용 시 이름 유추 기능을 통해 요소의 고유 이름들을 생략하여 사용할 수 있어 간편합니다.
    • 단점
      • 타입의 일관성이 저해되며 체계적인 관리가 어렵습니다.
        • 이 점은 typealias로 어느 정도 해결할 수 있습니다.
      • 다른 섹션에서 자세히 설명하겠지만, 튜플은 구조체, 클래스 등과 달리 어떠한 프로토콜도 준수(conform)할 수 없습니다.
        • 따라서 어떤 프로토콜에 대한 준수가 요구되는 사용처에서 튜플을 사용할 수 없습니다. (이 부분은 Swift 6에서 새로운 기능이 예정되어 있습니다.)

     

    튜플 패턴을 이용한 분해

    튜플 패턴을 이용한 분해(destruction) 이란 튜플을 개별 변수로 분리할 수 있는 기능입니다.

    위의 예제에서 니네집강아지 라는 튜플 안의 값을 묶어서 사용하기 보다 개별 변수로 분리해서 사용하고 싶다고 할 때

    let name = 니네집강아지.이름
    let species = 니네집강아지.종
    let weight = 니네집강아지.무게

    이렇게 할 수도 있지만, 이것을 튜플 패턴을 이용하면 라인 수를 줄이면서도 더 간편하게 개별 변수로 나눌 수 있습니다.

    let (name, species, weight) = 니네집강아지
    // 이 코드는 바로 위의 예제와 동일하게 작동합니다.
    
    print(name) // 뽀삐
    print(weight * 2) // 60
    

    변수 니네집강아지(이름: "뽀삐", 종: "요크셔테리어", 무게: 30) 의 데이터를 가지고 있는데, 이것을 이름을 생략하고 변수 선언 부분에서 괄호로 묶으면 ("뽀삐", "요크셔테리어", 30) 이 괄호 안에 선언된 순서대로 (name, species, weight) 자동으로 배정 되는것입니다.

    앞의 예제 circle처럼 튜플 안에 튜플이 있는 타입도 다음과 같이 분해할 수 있습니다.

    var circle = (radius: 10, coord: (x: 30, y: 50))
    let (radius, (x, y)) = circle
    
    print(x) // 30
    print(y) // 50

     

    튜플 패턴을 응용하면 일반적인 변수 선언도 간편하게 할 수 있습니다. 예를 들어, 자료를 인터넷에서 가져오는데 타입이 협의가 안되어 [Int] 배열 형태로 [x, y, width, height] 이렇게 들어온 상황입니다.

    이것을 튜플 패턴을 사용하면 여러 라인의 코드를 한 줄로 줄일 수 있습니다.

    let array = [3, 6, 200, 100]
    let x = array[0]
    let y = array[1]
    let width = array[2]
    let height = array[3]
    
    // 대신에 튜플 패턴을 사용
    let (x, y, width, height) = (array[0], array[1], array[2], array[3])
    • 튜플을 즉석에서 만든 다음, 바로 튜플 패턴을 통해 개별 변수로 배정합니다.

     

    [심화] 배열의 withUnsafeBytes 기능을 이용하여 배열을 튜플로 변환
    var polePairs: [(l: Int, r: Int)] = []
    polePairs.append([50, 60].withUnsafeBytes { $0.bindMemory(to: (l: Int, r: Int).self)[0] })
    polePairs.first! // (l: 50, r: 60)

     

    Swift 버전 6에서 튜플의 변경사항

    이렇게 다양한 기능이 있음에도 불구하고 실제로 튜플은 거의 사용되지 않고 있습니다. 다양한 이유가 있지만 튜플의 치명적인 단점 중 하나는 튜플은 다른 프로토콜을 준수(conform) 할 수 있는 기능이 없다는 점입니다.

    예를 들어 지도 등에서 두 지점 간 최소 거리를 찾아야 할 때 사용하는 Dijkstra Algorithm은 우선순위 큐라는 자료구조가 필요로 합니다. 그런데 Swift에서 우선순위 큐는 기본적으로 제공되지 않습니다.

    따라서 누군가가 우선순위 큐 구조체 PriorityQueue<T: Comparable>를 아래와 같이 만들었고 그것을 사용하고 싶다고 가정합니다.

    struct PriorityQueue<T: Comparable> { ... }
    
    // 최소 거리를 찾기 위한 기본 정보
    var priorityQueue = PriorityQueue<(weight: Int, value: Int)>
    // 는 사용이 불가능: 해당 튜플이 Comparable을 준수하게 할 수 있는 방법이 없기 때문에
    
    // 어쩔 수 없이 Comparable을 준수한 구조체 Node를 만들어서 사용
    struct Node: Comparable {
      static func < (lhs: Node, rhs: Node) -> Bool {
         lhs.weight < rhs.weight
      }
            
      var value: Int
      var weight: Int
    }
        
    
    • PriorityQueue는 두 값을 비교해서 더 작은(혹은 더 큰) 값에 더 우선순위가 올라가도록 자료를 재정렬하는 기능이 있습니다.
    • 따라서 우선순위큐에서 사용할 제너릭 타입은 Comparable이 필수적으로 준수되어야 합니다.
    • 하지만 튜플은 Comparable이 적용되지도 않고, 적용되도록 할 수 있는 기능도 없기 때문에 <T: Comparable> 이 아니며 될 수도 없습니다.
    • 결국 새로운 구조체를 만들어서 사용해야 하기 때문에 튜플의 이점이 사라지게 됩니다.

     

    이러한 사례로 많은 구조체, 클래스, 함수 등에서 요구하는 프로토콜 준수사항인 Equatable, Hashable, Comparable 등이 있지만 튜플은 이 중 단 하나도 지원하지 않습니다. 이러한 점 때문에 튜플은 거의 사용되지 않고 있던 것입니다.

    하지만 곧 출시될 Swift 6에서 (작성 시점 현재 Xcode에서 사용되는 버전은 Swift 5.9 입니다.) Equatable, Hashable, Comparable 을 지원한다는 소문이 있습니다.

    Swift 6.0에서 튜플은 Equatable, Comparable 및 Hashable을 준수합니다.

    튜플에는 프로토콜을 준수하는 기능이 부족했기 때문에 많은 개발자는 프로토콜을 준수할 수 있는 구조체를 선호하여 튜플 사용을 중단했습니다. 이제 튜플의 요소가 해당 프로토콜을 준수하는 경우 튜플은 Equatable, ComparableHashable을 준수합니다.

    출처: Swift 6 New Features

    // Equatable
    (1, 2, 3) == (1, 2, 3) // true
    // Labels are not taken into account to check for equality.
    (x: 0, y: 0) == (0, 0) // true
    
    // Comparable
    let origin = (x: 0, y: 0)
    let randomPoint = (x: Int.random(in: 1 ... 10), y: Int.random(in: 1 ... 10))
    (x: 0, y: 0) < (1, 0) // true
    // Labels are not taken into account for comparision.
    (x: 0, y: 0) < (1, 0) // true
    
    // Hashable
    let points = [(x: 0, y: 0), (x: 1, y: 2), (x: 0, y: 0)]
    let uniquePoints = Set(points)
    print (uniquePoints) // [(x: 0, y: 0), (x: 1, y: 2)]
    // Labels are not taken into account to check for hash value.
    (x: 0, y: 0).hashValue == (0, 0).hashValue // true

    구체적인 스펙이나 정확한 내용은 아직 출시 전이므로 알 수 없지만, Swift 6이 활성화되는 시기에는 치명적인 단점이 극복되어 튜플의 사용처가 늘지 않을까 싶습니다.

    문의 | 코멘트 또는 yoonbumtae@gmail.com


    카테고리: Swift


    0개의 댓글

    답글 남기기

    Avatar placeholder

    이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다