Type | 이름 | 역할 | 비고 |
---|---|---|---|
enum | StringLiteral | 프로그램 내 상수 문자열들 | 나중에 .strings로 바꿔보자 |
enum | IOManager, PrintType | 프로그램 내 입력, 출력 담당 | |
enum | IOError | 프로그램 입출력 Error 정리 | |
final class | ContactManager | 사용자에게 받은 입력을 1차 검증함 - 주어진 메뉴만 입력했는지 - 연락처 정보 중 '/' 형식 지켰는지 - 검색하려는 이름 형식 지켰는지 Phonebook, userInfo와 소통해 연락처 정보를 관리, 검색 |
|
final class | Phonebook | 연락처를 관리하는 전화번호부 CM의 요청에 따라 연락처 추가, 설명 반환 |
|
struct | UserInfo | 유저 정보를 담은 구조체 init시 입력 받은 연락처 정보의 이름, 나이, 연락처를 검증함 |
-
/
입력 검증 방법
: ContactManager.swift 의 parse() 를 통해 ‘/‘을 검증하였습니다. 저희는 기존 기준을 더 명확히 하기 위해 추가적인 제한을 더 추가했습니다.- “이름 / 나이 / 전화번호” : / 기준으로 앞,뒤 space
- “이름/나이/전화번호” : / 기준으로 앞,뒤 모두 space X
- 이 두가지의 경우에만 ‘/‘ 검증을 통과하도록 했습니다.
- 예를들어 “이름/ 나이/ 전화번호” 이런 경우, 통과하지 못하도록 구현했습니다.
-
/
검증 정규식
let inputPattern = #"^.+\b(?<sep>( \/ )|(\/))(\b[^\s]+\b)\k<sep>(\b[^\s]+)$"#
- 무한루프를 반복문(while true)에 비해 재귀함수로 구현하는 것의 이점?
-
재귀함수
- 함수 안에 자기 자신을 호출하는 함수와 종료를 위한 조건이 존재한다.
- 조건에 수렴하지 않으면 무한한 함수 호출을 일으키기 때문에 스택 오버플로우를 일으킬 수 있다.
- 반복문보다 실행 속도가 느리다.
-
반복문
- 특정 조건에 도달하기 전까지 일련의 명령을 반복적으로 실행한다.
- 반복문은 무한 루프에 빠질 경우, 스택 메모리가 아닌 CPU 사이클을 반복적으로 사용한다.
- 재귀함수보다 실행 속도가 빠르다.
-
그럼에도 불구하고 재귀함수를 쓰는 이유는?
- 알고리즘 자체가 재귀함수에 더 자연스러운 경우
- 가독성이 더 좋아진다.
- 변수 사용을 줄인다.
→ 반복문보다 재귀함수를 사용했을 때, 변수의 수를 줄일 수 있다. 변수의 수를 줄이면 프로그램은 변경 가능한 상태(mutable state)를 제거하고 오류가 발생할 확률을 줄일 수 있다.
- 연락처 전체 조회
- 출력은 이름순으로
- 연락처 검색
- 이름으로 검색
Phonebook Class 생성
// Phonebook.swift
final class Phonebook {
private var contacts: [String:Set<UserInfo>]
init(contacts: [String:Set<UserInfo>]) {
self.contacts = contacts
}
func add(contact: UserInfo) -> Bool {
let (inserted, _) = contacts[contact.name, default: Set<UserInfo>()].insert(contact)
return inserted
}
}
Phonebook: class vs struct
- Phonebook의 contacts 속성의 타입 때문에 이를 class로 구현했다. contacts는 Key/Value로 String과 Set을 사용한다. String과 Set은 struct지만, 가변 길이 데이터이기 때문에 Heap에 저장되게 된다. Understanding Swift Performance를 참고한 결과,
- Struct containing many reference가 Class에 비해 reference counting 더 높고
- 비록 Phonebook의 property는 contacts 1개지만,
- contacts가 dictionary이고, key, value가 String, Set으로 모두 Heap에 저장되기 때문에
- reference counting이 Struct로 만들었을 때 더 높을 것이라고 판단했다
- 따라서 Phonebook은 struct보다 class에 저장하는 것이 더 맞다고 생각했다!
- 개념적으로 배운 것을 실제 코드에 적용하기에 아직 확신이 부족하여,
- ❓우리가 잘 이해한게 맞고, 실제로 그렇게 동작하는 것인지 더 공부해서 알아봐야겠다😊
Dictionary를 선택한 이유
유저가 추가한 연락처를 ContactManager 객체가 어떻게 저장할지, 연락처 관리를 위한 데이터 구조를 고민했다.
- Key값을
userInfo.name: String
, Value값이Set<UserInfo>
인 contacts Dictionary를 생성했다
/// key: userInfo.name
// 중복 고려 전
let contacts: [String: Array<UserInfo>]
// 중복 고려 후
let contacts: [String: Set<UserInfo>]
처음에는 유저의 이름이 key이고 value가 UserInfo의 Collections Type인 Dictionary를 생각했다 Collections Type 중 Dictionary를 선택한 이유는, 프로그램의 기능 중 검색 기능이 이름으로 검색하기 때문
- 평균 시간 복잡도
Collection Type 검색 by 이름 추가 조회 Dictionary_Array O(1) O(1) O(NlogN) Dictionary_Set O(1) O(1) O(NlogN)
처음에는 중복을 고려하지 못해서 Dictionary_Array로 가닥을 잡고 구현했다
그런데 이름, 나이, 연락처가 같은 중복 연락처인 경우에도 여과없이 Array에 추가되는 점이 바람직하지 않다고 생각했고 이를 해결하기 위해 Dictionary_Set으로 데이터 구조를 수정했다
Dictionary 구현하며 새로 배운 Swift 문법은 subscript의 default parameter 이다
key의 존재 여부를 따로 체크하지 않아도 괜찮아서 편리했다! 그런데 위험하지는 않은지, 실제 디바이스의 메모리단에서 어떻게 동작하는지는 등 아직 Dictionary, Array의 실제 구현과 컴퓨터 구조에 익숙하지 않아서 설명할 수 없는 점이 아쉬웠다. 조금 더 공부하고 고민해봐야겠다🙃
var d = ["a": []]
d["a"]?.append("haha")
print(d) // ["a": ["haha"]]
d["b"]?.append("haha") // ["a": ["haha"]]
d["b", default: []].append("bb")
print(d) // ["a": ["haha"], "b": ["bb"]]
d["b", default: []].append("bbc")
print(d) // ["a": ["haha"], "b": ["bb", "bbc"]]
- Set의 특성을 이용해 Value값에 연락처가 중복 저장되지 않도록 구현
- 연락처 중 이름이 동일한 연락처는 존재할 수 있지만, 이름-나이-번호 가 모두 동일한 연락처는 중복으로 생각하고
"동일한 연락처가 존재합니다. 다른 연락처를 입력해주세요."
를 출력
- Array에서 Set으로 구현을 변경하면서,
- UserInfo를 Hashable 프로토콜을 채택했다
static let ==
연산과hash(into:)
함수를 구현하고 hasher.combine()에 name, age, phone을 넣었다
- UserInfo를
phonebook.contacts
에 추가할 때,- append() 대신
insert()
method를 사용했다 subscript(_:default:)
는 Set에서도 구현되어 있었다👍
- append() 대신
phonebook.add(contact:)
가 반환하는 Bool을 이용해서 ContactManager는 연락처가 성공적으로 추가되었는지 체크한다
- UserInfo를 Hashable 프로토콜을 채택했다