본문 바로가기
swift

스위프트 딕셔너리

by 승환파크 2024. 4. 8.

딕셔너리는 그 단어 자체로 알 수 있듯이 사전에서 고유 단어와 그 의미가 연결되어 있는 것처럼 Key 와 Value로 데이터를 저장하는 자료형이다. 인덱스 대신 고유 키를 사용하여 값을 얻는 것이다.

 

딕셔너리를 정의할 때는 배열과 마찬가지로 대괄호를 사용하며, 내부에 차례로 들어갈 데이터를 작성한다. 키와 값 사이는 콜론(:)으로 구분한다. 아래는 딕셔너리를 사용할 때의 주의점이다.

  • 하나의 키는 하나의 데이터에만 연결되어야 한다.
  • 하나의 딕셔너리에서 키는 중복될 수 없다. 중복해서 선언하면 아이템 추가가 아니라 수정이 이루어져 기존 키에 연결된 데이터는 제거된다.
  • 저장할 수 있는 데이터 타입에는 제한이 없지만, 하나의 딕셔너리에 저장하는 데이터 타입은 모두 일치해야 한다.
  • 딕셔너리의 아이템에는 순서가 없지만 키에는 내부적으로 순서가 있으므로 for ~ in 구문을 사용한 순회탐색이 가능하다.
  • 딕셔너리에서 사용할 수 있는 키의 타입은 거의 제한이 없으나 해시(Hash) 연산이 가능한 타입이어야 한다.

딕셔너리의 키값으로 사용할 수 있는 타입은 다양하다.

 

문자열은 물론 문자(Character), 정수, 실수 인스턴스도 가능하다. 딕셔너리가 아이템을 저장할 때는 입력된 키를 그대로 사용하는 것이 아니라 내부적으로 해시연산을 거친 값으로 변환한 다음 이를 정렬하여 사용한다. 이는 데이터의 빠른 검색을 위한 장치이지만, 키 타입은 해시 연산을 할 수 있는 타입으로 제한된다. 스위프트에서 해시 연산을 위해서는 Hashable 프로토콜이 구현되어 있어야 하는데, 문자열 타입은 Hashable 프로토콜이 구현되어 있으므로 대부분의 딕셔너리는 문자열을 키로 사용한다.

 

아래는 딕셔너리를 정의하는 예시이다.

var capital = ["KR" : "Seoul", "EN" : "London", "FR" : "Paris"]

 

이렇게 정의된 딕셔너리의 아이템 참조는 배열에서 인덱스를 참조하여 아이템을 참조하는 방식과 유사하다. 딕셔너리가 할당된 변수나 상수에 대괄호를 붙이고 그 안에 키를 넣으면 된다.

capital["KR"] // Seoul
capital["EN"] // London
capital["FR"] // Paris

 

딕셔너리 선언과 초기화

먼저 아무 초기값도 할당하지 않고 빈 딕셔너리를 선언하고 초기화 하는 구문 형식은 아래와 같다.

Dictionary <키의 타입, 값의 타입>()

 

딕셔너리를 선언할 때는 Dicitonary 구조체가 사용된다. 이 객체를 사용하여 선언할 때는 배열과 마찬가지로 < 와 > 사이에 키로 사용할 타입과 값으로 사용할 타입을 지정해야 한다. 이를 제네릭 이라고 부른다. 이 제네릭 안에 첫번째에는 키, 두번째에는 값으로 사용할 타입을 작성한다. 구문 마지막에는 초기화를 위한 연산자인 ()이 사용된다. 초기화 연산자를 거쳐야만 선언된 딕셔너리가 메모장에서 데이터를 저장할 공간을 할당 받을 수 있다.

 

이를 바탕으로 몇가지 딕셔너리를 선언해본다.

Dictionary<String, Int>()
Dictionary<String, String>()
Dictionary<String, AnyObjecty>()
DIctionary<Character, String>()

 

위의 딕셔너리 선언들을 간결한 형식으로 표현하면 다음과 같다.

[키로 사용할 타입 : 값으로 사용할 타입]()
[String : Int]()
[String : String]()
[String : AnyObject]()
[Character : String]()

 

두 가지 방법을 직접 써본 예시이다.

// 방법 1
var dict1 = Dictionary<String, String>()
// 방법 2
var dict2 = [String : String]()

// 선언과 초기화 분리 방법1
var dict3: Dictionary<String, String>
dict3 = Dictionary<String, String>()

// 선언과 초기화 분리 방법2
var dict4: [String : String]
dict4 = Dictionary()

// 선언과 초기화 분리 방법3
var dict5: [String : String]
dict5 = [String : String]()

// 선언과 초기화 분리 방법4
var dict6: [String : String]
dict6 = [:]

 

주의점은 배열에서와는 달리 타입 어노테이션으로 키와 값의 타입이 이미 선언되었다고 하여 초기화 구문에서 함부로 타입 작성을 생략해서는 안된다. 타입이 생략된 초기화 구문은 잘못 해석될 우려가 있기 때문이다.

 

위 예제에서 2번과 4번을 주의해야한다. 초기화 구문의 타입 지정이 생략되어 있는데 이는 사전에 타입 어노테이션을 통해 딕셔너리의 타입이 명시적으로 선언되어 있기 때문이다. 이외의 초기화 구문에서는 함부로 타입을 생략하면 안된다.

 

딕셔너리에 동적으로 아이템 추가하기

선언과 초기화까지 완료된 딕셔너리에는 동적으로 아이템을 추가할 수 있다.

var newCapital = [String : String]()
newCapital["JP"] = "Tokyo"

 

키와 값을 직접 대입하여 아이템을 추가할 수 있다. 딕셔너리 변수 뒤에 []를 붙이고, 괄호 안에 키로 사용할 String을 넣어주고 대입 연산자로 String 값을 넣어주면 된다.

 

딕셔너리에서도 배열처럼 아이템의 갯수가 딕셔너리의 크기를 결정한다. 정확히는 딕셔너리에 저장된 튜플의 갯수이다. 딕셔너리에 아이템이 있는지는 isEmpty 속성를 통해 확인할 수 있으며, 딕셔너리의 크기를 알려주는 count 값이 0일 때 isEmpty 속성의 값은 true로 설정된다.

if newCapital.isEmpty {
	print("딕셔너리가 비어있습니다.")
} else {
	print("딕셔너리의 크기는 현재 \(newCapital.count)입니다.")
}

// 결과값
// 딕셔너리의 크기는 현재 1입니다.

 

이번에는 메소드를 사용해서 동적으로 값을 할당해볼 것이다. 딕셔너리에 값을 할당하는데 사용되는 메소드는 updateValue(_:forKey:)이다. 만약 기존에 저장된 키가 있으면 연결된 값을 수정하는 역할을 하고, 새로운 키가 입력되면 아이템을 추가하는 역할을 수행한다. 특이한 점은 이 메소드를 사용하여 딕셔너리에 저장된 값을 수정하면 수정하기 이전의 값이 결과값에 반환된다는 점이다. 따라서 새로운 키와 값을 이 메소드에 사용하여 추가하면 기존에 저장되어 있던 값이 없으므로 nil을 반환한다.

<딕셔너리 객체>.updateValue(<저장할 데이터>, forKey:<데이터를 참조 및 저장하는데 사용할 Key>)
newCapital.updateValue("Seoul", forKey: "KR")
// "KR" : "Seoul" 데이터가 저장되고 nil이 리턴된다.
newCapital.updateValue("London", forKey: "EN")
// "EN" : "London" 데이터가 저장되고 nil이 리턴된다.
newCapital.updateValue("Sapporo", forKey: "JP")
// "JP" : "Sapporo" 데이터로 수정되고 "Tokyo"가 리턴된다.

 

첫 번째와 두 번째 경우는 이전에 사용된 적이 없는 키이므로 저장된 값도 없다. 따라서 키와 값이 새로 생성되고 이전에 저장된 값이 없으므로 nil이 반환된다. 하지만 세 번째의 경우 이미 사용되었던 키이고, Tokyo 라는 값을 가지고 있었으므로 Tokyo가 반환된다.

 

딕셔너리에 저장된 값을 제거할 때는 두 가지 방법이 있다. 하나는 키에 연결된 값에 직접 nil을 할당하는 방법이고 또 다른 하나는 명시적으로 removeValue(forKey:) 메소드를 이용하는 방식이다.

// 1. nil 할당
newCapital["KR"] = nil
// 2. 메소드 사용
newCatital.removeValue(forKey: "EN")

 

위 두가지 방법으로 딕셔너리 내 아이템을 제거할 수 있다. 두 번째 방식의 경우 실행된 결과로 삭제된 아이템의 값을 반환받는다.

if let removedValue = newCapital.removeValue(forKey: "JP"){
    print("삭제된 값은 \(removedValue)입니다.")
}else{
    print("아무것도 삭제되지 않았습니다.")
}

// 결과값
// 삭제된 값은 sappopo입니다.

 

물론 없는 키를 삭제하고자 할 때는 그 결과값도 당연히 없으므로 nil을 반환한다. 그런데 여기서 짚고 넘어가야 할 부분이 있다. 바로 배열의 인덱스와 딕셔너리의 키에 대한 접근 차이이다.

 

배열은 인덱스를 직접 참조하기 위해 참조할 인덱스가 이미 만들어져 있어야만 한다는 제약조건이 있다. 그렇지 않으면 잘못된 인덱스 참조에 의한 오류가 발생한다. 하지만 딕셔너리는 키 자체가 일련의 순서를 갖고 있지 않는다.(해시 연산에 의 한 결과값 역시 연속되는 값은 아니다.) 게다가 타입은 알 수 있을지언정 실제로 어떤 데이터가 키로 사용될지 미리 알 수 없으므로 기존에 사용된 적이 없던 새로운 키가 입력되면 이 키와 값을 저장하기 위한 튜플을 하나 만들어 저장하면 될 뿐이다. 새로운 인덱스 공간을 확보하고 크기를 늘릴 필요 없이 초기화 되어 있기만 하면 된다.

 

딕셔너리는 없는 키를 호출했을 가능성을 생각해야 한다. 이 경우를 처리해야 안전한 프로그래밍이 된다. 따라서 스위프트에서는 딕셔너리로부터 키를 호출해서 저장된 값을 불러올 때, 또는 업데이트 메소드를 실행한 결과를 반환할 때, 오류가 발생할 가능성을 염두에 둔 다음과 같은 특별한 형식으로 값을 반환한다.

Optional("Sapporo")

 

딕셔너리의 순회 탐색

딕셔너리에 저장된 아이템끼리는 순서가 없지만, 내부적으로 키를 바탕으로 해시 연산 값을 기준으로 정렬하므로 이 특성을 사용하면 순회 탐색을 할 수 있다. for ~ in 구문을 사용한 예제이다.

for i in newCapital {
	//키 - 값이 한 쌍에 담긴 i 상수를 튜플로 받는다
    let(key,value) = i
    print("현재 데이터는 \(key) : \(value)입니다.")
}

// 결과값
// 현재 데이터는 EN : London입니다.
// 현재 데이터는 JP : Sapporo입니다.
// 현재 데이터는 KR : Seoul입니다.

 

위 예제를 좀더 생략하여 작성하면 아래와 같다.

for (key, value) in newCapital {
	print("현재 데이터는 \(key) : \(value)입니다.")
}

// 결과값
// 현재 데이터는 EN : London입니다.
// 현재 데이터는 JP : Sapporo입니다.
// 현재 데이터는 KR : Seoul입니다.

 

이 방법이 훨씬 직관적이고 쉽다. 실제로 앱을 개발하면서 사용해야 할 코드는 이렇게 불필요한 과정을 제거한 코드가 될 것이다.

 

결과값을 보면 입력한 순서와 다르게 출력되는걸 볼 수 있다. 기본적으로 딕셔너리는 고유 키에 대한 해시 값을 기준으로 내부 정렬을 하기 때문에 우리가 생각한 결과와는 다르게 출력된다.

'swift' 카테고리의 다른 글

스위프트 함수  (0) 2024.04.09
스위프트 옵셔널  (0) 2024.04.08
스위프트 튜플  (0) 2024.04.08
스위프트 집합  (1) 2024.04.06
스위프트 배열  (0) 2024.04.06