당신은 온라인 게임을 운영하고 있습니다. 같은 시간대에 게임을 이용하는 사람이m명 늘어날 때마다 서버 1대가 추가로 필요합니다. 어느 시간대의 이용자가m명 미만이라면, 서버 증설이 필요하지 않습니다. 어느 시간대의 이용자가n x m명 이상(n + 1) x m명 미만이라면 최소n대의 증설된 서버가 운영 중이어야 합니다. 한 번 증설한 서버는k시간 동안 운영하고 그 이후에는 반납합니다. 예를 들어,k= 5 일 때 10시에 증설한 서버는 10 ~ 15시에만 운영됩니다.
하루 동안 모든 게임 이용자가 게임을 하기 위해 서버를 최소 몇 번 증설해야 하는지 알고 싶습니다. 같은 시간대에 서버를 x대 증설했다면 해당 시간대의 증설 횟수는 x회입니다.
다음은m= 3,k= 5 일 때의 시간대별 증설된 서버의 수와 증설 횟수 예시입니다.
시각
게임 이용자의 수
증설된 서버의 수
증설된 서버의 수증설 횟수
0 ~ 1
0
0
0
1 ~ 2
2
0
0
2 ~ 3
3
1
1
3 ~ 4
3
1
0
4 ~ 5
1
1
0
5 ~ 6
2
1
0
6 ~ 7
0
1
0
7 ~ 8
0
0
0
8 ~ 9
0
0
0
9 ~ 10
0
0
0
10 ~ 11
4
1
1
11 ~ 12
2
1
0
12 ~ 13
0
1
0
13 ~ 14
6
2
1
14 ~ 15
0
2
0
15 ~ 16
4
1
0
16 ~ 17
2
1
0
17 ~ 18
13
4
3
18 ~ 19
3
3
0
19 ~ 20
5
3
0
20 ~ 21
10
3
0
21 ~ 22
0
3
0
22 ~ 23
1
0
0
23 ~ 24
5
1
1
모든 게임 이용자를 감당하기 위해 최소 7번 서버를 증설해야 하며, 이보다 적은 수의 서버 증설로는 모든 게임 이용자를 감당할 수 없습니다.
0시에서 23시까지의 시간대별 게임 이용자의 수를 나타내는 1차원 정수 배열players, 서버 한 대로 감당할 수 있는 최대 이용자의 수를 나타내는 정수m, 서버 한 대가 운영 가능한 시간을 나타내는 정수k가 주어집니다. 이때, 모든 게임 이용자를 감당하기 위한 최소 서버 증설 횟수를 return 하도록 solution을 완성해 주세요.
제한사항
players의 길이 = 24
0 ≤players의 원소 ≤ 1,000
players[i]는i시 ~i+1시 사이의 게임 이용자의 수를 나타냅니다.
1 ≤m≤ 1,000
1 ≤k≤ 24
내가 푼 풀이
주어진 players배열과 크기가 같은 servers 배열을 만들었습니다.
players배열을 반복문으로 순회하면서 각 시간마다 서버 증설이 필요한지 조건을 달았습니다.
증설이 필요하다면 그리디 기법으로 현재 시간의 인원을 수용할 만큼만의 서버를 증설하고 k시간까지 최대 이용자 수를 증가시켰습니다.
증설이 필요할 때 서버를 증설하고 증설된 수를 count 변수에 담아서 리턴했습니다.
import Foundation
func solution(_ players:[Int], _ m:Int, _ k:Int) -> Int {
var servers = Array(repeating: 0, count: players.count)
var count = 0
for i in 0..<players.count {
let allowPlayerCount = (servers[i] * m) + m
if players[i] >= allowPlayerCount {
let needServerCount = abs(servers[i] - (players[i] / m))
count += needServerCount
for j in i..<i+k {
if j >= players.count { continue }
servers[j] += needServerCount
}
}
}
return count
}
프로그래밍에서 타입은 값이 가질 수 있는 형태와 그 값에 대해 수행할 수 있는 연산을 정의하는 개념입니다.
더 나누면 정적타입, 동적타입이 존재하지만 이 글에서는 다루지 않습니다
이 글에서는 Swift에서 존재하는 타입들을 살펴볼 것 입니다.
더 자세한 내용은 Swift.org에 있습니다
Swift의 타입
Swift의 타입은 두가지 종류가 있습니다.
명명된 타입 (named type): 특정 이름을 부여할 수 있는 타입으로 클래스, 구조체, 열거형, 프로토콜, 배열, 딕셔너리, 옵셔널 모두 해당됩니다. 필요에따라 확장할 수 있습니다.
복합 타입 (compound type): 정의된 이름이 없는 타입으로 함수와 튜플이 있습니다. 복합 타입은 말 그대로 다른 명명된 타입과 다른 복합 타입을 합칠 수 있습니다.
Swift에서 정의된 타입은 아래와 같이 있습니다.
Grammar of a type
type → function-type (함수)
type → array-type (배열)
type → dictionary-type (딕셔너리)
type → type-identifier (타입 식별자)
type → tuple-type (튜플)
type → optional-type (옵셔널 타입)
type → implicitly-unwrapped-optional-type (묵시적 언래핑 옵셔널)
type → protocol-composition-type (프로토콜 조합 타입)
type → opaque-type (불투명 타입) (some 키워드를 붙이는 것)
type → boxed-protocol-type (existential type) (프로토콜 타입으로 선언한 것)
type → metatype-type (타입의 타입) (.self .Type) type → any-type
type → self-type
type → ( type )
타입 식별자 (Type Identifier)
타입 식별자는 타입 별칭(type alias)를 나타냅니다.
대부분은 식별자와 명명된 타입이 같아서 직접 참조합니다 (Int타입은 Int에 참조, Dictionary<String,Int>타입은 Dictionary<string,int></string,int>에 참조)
명명된 타입과 식별자가 같지 않은 두가지 경우가 있습니다.
첫번째로는 복합타입의 타입별칭입니다.
typealias Point = (Int, Int)
let origin: Point = (0, 0)
두번째로는 다른 모듈에 선언되거나 다른 중첩된 명명 타입입니다.
var someValue: ExampleModule.MyType
튜플 타입 (Tuple Type)
튜플 타입은 소괄호로 묶인 콤마로 구분되는 타입의 리스트입니다.
타입의 요소에 이름을 정할 수 있고, 이름과 타입을 구별하기 위해 세미콜론(:)을 사용합니다.
func swap(_ num1: inout Int,_ num2: inout Int) {
let temp: Int = num1
num1 = num2
num2 = temp
}
let swap1 = swap
var numA = 10
var numB = 15
swap1(&numA, &numB)
print(numA, numB)
// 출력
// 15 10
func printRange(range: Int...) {
for i in range {
print(i)
}
}
let printRange1 = printRange
printRange1(1, 2, 3)
// 출력
// 1
// 2
// 3
함수 타입은 위에 서술했듯이 파라미터 타입과 리턴 타입이 같으면 다른 같은 타입의 함수로도 변경할 수 있습니다.
someFunction을 변수 f에 할당하게 되면 f의 타입은 위 사진과 같이 (Int, Int) -> ()으로 설정됩니다.
f의 타입처럼 파라미터 타입은 (Int, Int), 리턴 타입은 Void인 함수라면 f에 다른 함수로 초기화 할 수 있습니다.
func someFunction(left: Int, right: Int) {}
func anotherFunction(left: Int, right: Int) {}
func functionWithDifferentLabels(top: Int, bottom: Int) {}
var f = someFunction
f = anotherFunction
f = functionWithDifferentLabels
위 세가지 함수는 인자의 이름이 달라도 파라미터 타입과 리턴 타입이 같기 때문에 f로 선언된 변수가 세 가지 함수로 타입을 지정 및 변경하여도 에러가 발생하지 않습니다. (모두 같은 타입이기 때문에)
다른 함수의 종류로 에러를 반환할 수 있는 throw 함수, async 함수 역시 타입으로 지정할 수 있습니다.
위 사진처럼 자동 완성으로 볼 때 해당 변수의 타입들은 throws, async 함수타입으로 지정된 것을 볼 수 있습니다.
배열 타입 Array Type
이미 많이 사용해봤던 배열 타입입니다.
[T], 혹은 Array<T> 형식으로 선언합니다.
let someArray1: Array<String> = ["Alex", "Brian", "Dave"]
let someArray2: [String] = ["Alex", "Brian", "Dave"]
let someArray3: Array<Array<String>> = [["Alex", "Brian"], ["Dave"]]
let someArray4: [[String]] = [["Alex", "Brian"], ["Dave"]]
딕셔너리 타입 Dictionary Type
Dictionary를 선언하는 방식은 아래와 같습니다!
let someDictionary1: [String: Int] = ["Alex": 31, "Paul": 39]
let someDictionary2: Dictionary<String, Int> = ["Alex": 31, "Paul": 39]
옵셔널 타입 Optional Type
Swift에서는 Optional<Wrapped>에 대해 접미사 ? 구문을 정의합니다.
옵셔널 타입을 다음과 같이 설정할 수 있습니다.
// enum Optional<Wrapped>
var optionalInteger1: Int?
var optionalInteger2: Optional<Int>
암시적 언래핑 옵셔널 타입 Implicitly Unwrapped Optional Type
암시적 언래핑 옵셔널 타입은 스토리보드에 있는 UI를 코드영역으로 드래그하면 자동으로 생성되는 모습을 볼 수 있었습니다.
@IBOutlet var button: UIButton!
이는 해당 인스턴스가 옵셔널 타입이지만, 강제 언래핑 동작을 자동으로 추가하기 위해 타입 뒤에 접미사 ! 를 추가하여 선언합니다.
button이 nil이라면 런타임 에러를 발생하고, nil이 아닌 값이 존재한다면 따로 옵셔널 바인딩 없이 접근할 수 있습니다.
프로토콜 혼합 타입 Protocol Composition Type
프로토콜 혼합 타입은 말 그대로 다중 프로토콜이 혼합된 하나의 타입으로 나타내게 되고 타입은 아래와 같이 나타냅니다.
<#Protocol 1#> & <#Protocol 2#>
protocol Nameable {
var name: String { get }
}
protocol Ageable {
var age: Int { get }
}
typealias Person = Nameable & Ageable
class Jenikeju: Person {
var name: String
var age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
}
혼합된 프로토콜은 protocol1 & protocol2 로 나타낼 수도 있지만, 이를 typealias를 통해 새로운 타입으로 별칭하고 사용할 수 있습니다.
혼합된 프로토콜이여도 하위클래스에 상위클래스를 상속하고 다른 프로토콜을 사용할 때, 상위클래스와 프로토콜을 혼합할 수 있습니다.
protocol Nameable {
var name: String { get }
}
protocol Ageable {
var age: Int { get }
}
class Animal {
func feed() { }
}
// class & protocol1 & protocol2
typealias CatCompositionType = Animal & Nameable & Ageable
class Cat: CatCompositionType {
var name: String
var age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
}
let cat = Cat(name: "meow", age: 5)
cat.feed()
클래스는 하나의 클래스만 상속받을 수 있기 때문에 최대 하나의 클래스만 혼합할 수 있습니다.
또한 혼합 과정에서 중복된 프로토콜은 자동으로 무시됩니다.
// 중복된 프로토콜 혼합
// Ageable & Nameable & Ageable & Ageable = Ageable & Nameable
typealias ManyCompositionType = Ageable & Nameable & Ageable & Ageable
class SomeClass: ManyCompositionType {
var age: Int
var name: String
}
불투명한 타입 Opaque Type
불투명한 타입은 기본 타입 지정없이 프로토콜 또는 프로토콜 구성을 준수하는 타입을 정의합니다.
some <#constraint#>
constraint는 클래스, 프로토콜, 프로토콜 구성 타입, Any 입니다.
불투명한 타입을 이해하기위해 some 키워드를 먼저 이해해봅시다.
some
불투명한 타입에서 불투명한이란 의미는 어떤 타입인지 정확히 명시하지 않지만, 컴파일러는 내부적으로 알고 있다는 의미로 생각할 수 있습니다.
A도둑과 B도둑이 팀을 이루어 모든 물건을 훔치려고 합니다. 단, 각 도둑이 물건을 훔칠 때 남기는 흔적이 누적되면 경찰에 붙잡히기 때문에, 두 도둑 중 누구도 경찰에 붙잡히지 않도록 흔적을 최소화해야 합니다.
물건을 훔칠 때 조건은 아래와 같습니다.
물건 i를 훔칠 때,
A도둑이 훔치면info[i][0]개의 A에 대한 흔적을 남깁니다.
B도둑이 훔치면info[i][1]개의 B에 대한 흔적을 남깁니다.
각 물건에 대해 A도둑과 B도둑이 남기는 흔적의 개수는 1 이상 3 이하입니다.
경찰에 붙잡히는 조건은 아래와 같습니다.
A도둑은 자신이 남긴 흔적의 누적 개수가n개 이상이면 경찰에 붙잡힙니다.
B도둑은 자신이 남긴 흔적의 누적 개수가m개 이상이면 경찰에 붙잡힙니다.
각 물건을 훔칠 때 생기는 흔적에 대한 정보를 담은 2차원 정수 배열info, A도둑이 경찰에 붙잡히는 최소 흔적 개수를 나타내는 정수n, B도둑이 경찰에 붙잡히는 최소 흔적 개수를 나타내는 정수m이 매개변수로 주어집니다. 두 도둑 모두 경찰에 붙잡히지 않도록 모든 물건을 훔쳤을 때,A도둑이 남긴 흔적의 누적 개수의 최솟값을 return 하도록 solution 함수를 완성해 주세요. 만약 어떠한 방법으로도 두 도둑 모두 경찰에 붙잡히지 않게 할 수 없다면 -1을 return해 주세요.
제한사항
1 ≤info의 길이 ≤ 40
info[i]는 물건i를 훔칠 때 생기는 흔적의 개수를 나타내며, [A에 대한 흔적 개수,B에 대한 흔적 개수]의 형태입니다.
1 ≤흔적 개수≤ 3
1 ≤n≤ 120
1 ≤m≤ 120
테스트 케이스 구성 안내
아래는 테스트 케이스 구성을 나타냅니다. 각 그룹 내의 테스트 케이스를 모두 통과하면 해당 그룹에 할당된 점수를 획득할 수 있습니다.
그룹총점테스트 케이스 그룹 설명
#1
15%
info[i][1]= 1
#2
40%
info의 길이 ≤ 20
#3
45%
추가 제한 사항 없음
입출력 예infonmresult
[[1, 2], [2, 3], [2, 1]]
4
4
2
[[1, 2], [2, 3], [2, 1]]
1
7
0
[[3, 3], [3, 3]]
7
1
6
[[3, 3], [3, 3]]
6
1
-1
내가 푼 풀이
첫번째 방법 - DFS
한개의 물건이 주어졌을 때, 두가지 경우의 수가 있습니다.
1. A가 훔치기
2. B가 훔치기
또한 경찰에게 걸리지 않게 모든 물건을 훔쳤기에 물건을 훔치지 않는 옵션은 없습니다.
결국 A가 최소로 도둑질을 하면 흔적도 자연스럽게 최솟값을 가질 것이라 생각하고, B에게 먼저 조건을 걸어주었고, B가 최대한 훔치도록 구현하였습니다.
import Foundation
func solution(_ info:[[Int]], _ n:Int, _ m:Int) -> Int {
var min = Int.max
var informations = info
func dfs(count: [Int], currentIndex: Int) {
if currentIndex >= info.count {
min >= count[0] ? min = count[0] : nil
return
}
let theifACount = [count[0] + info[currentIndex][0], count[1]]
let theifBCount = [count[0], count[1] + info[currentIndex][1]]
if theifACount[0] < n && theifACount[0] < min {
dfs(count: [count[0] + info[currentIndex][0], count[1]],
currentIndex: currentIndex + 1)
}
if theifBCount[1] < m {
dfs(count: [count[0], count[1] + info[currentIndex][1]],
currentIndex: currentIndex + 1)
}
}
dfs(count: [0, 0], currentIndex: 0)
return min == Int.max ? -1 : min
}
결과는 시간초과가 발생하였습니다.
이유를 확인해보면, 만약 m,n이 매우 넉넉하다면, 큰 수라면 bfs의 실행은 $2^{40}$ 번 실행되어 시간초과가 발생하였습니다.
그러면 시간만 줄여서 될까 해서 테스트케이스를 몇 개 추가했더니, 해당 케이스도 실패하는걸 볼 수 있었습니다.
info = [[1, 2], [3, 1], [3, 2]]
n = 5
m = 3
// 결과는 4가 나와야 하지만 1이 나왔습니다.
이를 통해 매번 탐색할 때 최적해를 갱신해야 할 필요가 있구나 싶었습니다.
두번째 방법 - DP
이전까지의 최적해를 저장하여 그다음 최적해를 구하기 위해 DP를 사용했습니다.
info배열을 돌면서 한번으로 최적해가 구해질 수 있을까? 해서 작성해보았는데 위 테스트 케이스도 통과하지 못했습니다.
B도둑이 알뜰살뜰하게 m을 넘지 않으면서 m과 가까운 수만큼 흔적을 남기는 방법이 뭐가 있을까 하다가 배낭문제가 생각났습니다.
(도둑이 배낭에 보석을 담을 때 정해진 배낭무게, 보석무게와 가치에 맞춰서 가장 가치있는 보석만 골라담는 문제)
B도둑이 최대한 흔적을 많이 남길 수 있게 배낭문제처럼 m값을 갱신하고 A흔적의 최솟값을 구하려고 합니다.
이를 1번 입력을 예시로 2차원배열로 나타내보겠습니다.
행은 m, B가 남길 수 있는 흔적 입니다. 열은 index로 info의 물건을 접근하기위한 인덱스입니다.
조건을 다시 살펴보자면,
1. 모든 물건을 훔쳤다.(물건을 훔치지 않고 건너뛰는건 불가능)
2. A는 n, B는 m 보다 같거나 클 수 없다. 만약 같거나 크다면 훔칠 수 없다고 판단한다.
위 조건을 바탕으로 배낭문제 처럼 2차원 배열을 채워가며 A의 최솟값을 구하겠습니다.
dp[i][j] = 0 은 물건을 하나도 훔치지 않을 때 값입니다.
행은 무게를 뜻하고, 열은 물건의 순서를 뜻하고, 해당 위치의 원소는 A의 흔적입니다.
첫번째는 아무것도 훔치지 않았기때문에 dp[0][1...3] = 0 입니다.
이제 첫번째 물건을 훔치겠습니다. 물건의 정보는 [1, 2] 입니다.
이때, 두가지 경우의 수가 있습니다. (1. A가 훔치기 2. B가 훔치기)
해당 DP인덱스의 원소는 A의 흔적을 의미하기에 A가 훔친다면 이전 값에서 A의 흔적을 더한값이 될 것입니다.
B가 훔친다면, (현재열 + 2) 인덱스에 이전 값을 넣게 됩니다.
이렇게 이전의 저장된 값을 바탕으로 최적해를 구하기위해 갱신하면 다음과 같습니다.
그 다음 두번째 물건을 훔치겠습니다. 물건의 정보는 [2,3] 입니다.
두번째 물건도 두가지 경우가 있습니다.
dp[2][0]에는 3이 들어가고 이 의미는 두번째 물건까지 A가 훔치는 경우입니다.
두번째 물건을 B가 훔치는 경우는 (현재 열 + 3) 인덱스에 넣게 되는데, 이 때 첫번째 물건을 넣은 값과 비교를 해야합니다.
import Foundation
func solution(_ info:[[Int]], _ n:Int, _ m:Int) -> Int {
let INF = 100000000
// DP
var dp = Array(repeating: Array(repeating: INF, count: m),
count: info.count+1)
for i in 0..<m {
dp[0][i] = 0
}
for i in 1...info.count {
// 현재 물건에 대한 A의 흔적과 B의 흔적
let aThief = info[i-1][0]
let bThief = info[i-1][1]
for j in 0..<m {
// A가 훔친 경우 값 갱신
dp[i][j] = min(dp[i][j], dp[i-1][j] + aThief)
// B가 훔친 경우 값 갱신
if j + bThief < m {
dp[i][j + bThief] = min(dp[i][j + bThief], dp[i-1][j])
}
}
}
// 마지막 물건과 B의 흔적이 m보다 크거나 같지 않게 훔쳤을 때의 최소값 리턴
return dp[info.count][m-1] >= n ? -1 : dp[info.count][m-1]
}
ARC는 Automatic Reference Counting으로 참조타입의 참조 횟수를 자동으로 카운팅 되며 참조 수가 0이 되면 메모리에서 자동으로 해제하여 메모리 사용량을 관리합니다.
자동으로 참조횟수를 트래킹 하지만, 강한 참조 사이클 같은 경우에는 각 객체가 소멸해도 강한 참조가 남아있어 메모리에 계속 유지되어 버리는 메모리 누수 현상이 발생하곤 합니다.
그래서 메모리 누수 방지를 위해 weak, unowned 키워드를 종종 사용하곤 했습니다.
강한 참조 사이클 예시
// 서로 참조하는 클래스 두개 생성
class Person {
let name: String
init(name: String) { self.name = name }
var apartment: Apartment?
deinit { print("\(name) is being deinitialized") }
}
class Apartment {
let unit: String
init(unit: String) { self.unit = unit }
var tenant: Person?
deinit { print("Apartment \(unit) is being deinitialized") }
}
// 인스턴스 생성 (Person의 강한참조: 1, Apartment의 강한참조: 1)
var person: Person? = Person(name: "john")
var apartment: Apartment? = Apartment(unit: "")
// person.apartment는 Apartment 클래스를 강한참조
// apartment.tenant는 Person 클래스를 강한참조
// (Person의 강한참조: 2, Apartment의 강한참조: 2)
person?.apartment = apartment
apartment?.tenant = person
// person과 apartment 소멸하여 강한참조가 1 줄어들지만, 서로 참조하는 카운팅은 남아있음
// 메모리에 계속 유지되는 Person클래스와 Apartment클래스는 어떤 방법으로도 접근할 수 없음
// (Person의 강한참조: 1, Apartment의 강한참조: 1)
person = nil
apartment = nil
weak? unowned?
강한참조 사이클 발생을 방지하기 위해 두 가지 키워드 중 하나를 선택하여 사용할 수 있습니다.
결과적으로 메모리 누수를 피하기 위해 사용되지만 두 키워드의 차이점은 있습니다.
weak: 옵셔널 타입으로 명시하며, 참조하는 객체가 소멸되면, 해당 값은 nil이 됩니다.
// 서로 참조하는 클래스 두개 생성
class Person {
let name: String
init(name: String) { self.name = name }
var apartment: Apartment?
deinit { print("\(name) is being deinitialized") }
}
class Apartment {
let unit: String
init(unit: String) { self.unit = unit }
weak var tenant: Person?
deinit { print("Apartment \(unit) is being deinitialized") }
}
// 인스턴스 생성 (Person의 강한참조: 1, Apartment의 강한참조: 1)
var person: Person? = Person(name: "john")
var apartment: Apartment? = Apartment(unit: "")
// person.apartment는 Apartment 클래스를 강한참조
// apartment.tenant는 Person 클래스를 약한참조
// (Person의 강한참조: 1, Apartment의 강한참조: 2)
person?.apartment = apartment
apartment?.tenant = person
// person객체가 소멸되면 apartment.tenant가 약한참조로 nil이 된다.
// (Person의 강한참조: 0, Apartment의 강한참조: 0)
person = nil
apartment = nil
unowned: 옵셔널 형식이 아니며, 참조하는 클래스보다 수명이 같거나 긴 경우에만 사용합니다. Swift5 업데이트 이후로 unowned도 옵셔널 형식을 가질 수 있지만, 참조하는 클래스가 nil이고 접근할 때 앱 크래시가 여전히 발생합니다.
앱크래시를 방지하기 위해 보통 절대로 메모리 해제하지 않는 객체를 참조할 때 사용합니다.
// 서로 참조하는 클래스 두개 생성
class Customer {
let name: String
var card: CreditCard?
init(name: String) {
self.name = name
}
deinit { print("\(name) is being deinitialized") }
}
class CreditCard {
let number: UInt64
unowned let customer: Customer
init(number: UInt64, customer: Customer) {
self.number = number
self.customer = customer
}
deinit { print("Card #\(number) is being deinitialized") }
}
// 인스턴스 생성
var john: Customer?
// (Customer 강한참조: 1, CreditCard 미소유 참조)
// CreditCard의 수명은 Customer과 같기때문에 미소유 참조를 하였다.
// 만약 CreditCard 인스턴스를 생성하고, Customer가 nil일때 CreditCard.customer을 접근하면 앱 크래시 오류가 발생한다
john = Customer(name: "John Appleseed")
john!.card = CreditCard(number: 1234_5678_9012_3456, customer: john!)
서로 참조하는 객체의 수명이나 기획의도에 따라 키워드를 선택할 수 있다.
모든 서로 참조는 weak 키워드와 함께 쓰면 되지않을까?
unowned는 치명적인 오류를 발생하기 때문에 사용하지 않고 모든 서로참조를 weak 키워드와 함께 사용하면 되는 거 아닌가? 생각이 들었다. 기본적으로 weak는 옵셔널 형식의 약한 참조이다. 이에 따라 접근하기 위해 옵셔널 체이닝을 통해 접근하며, 언래핑이 필요하다.
그렇기 때문에 사용함에 있어서 값이 nil이 아님을 항상 증명해야 하고, 참조하는 객체가 메모리에서 내려가면 nil이 되므로 사이드 이펙트 발생 가능성 또한 존재하게 된다.
객체가 nil이 되지 않는 경우에는 weak보다 unowned 키워드가 더 적절하다.
둘 다 쓸 수 있는 상황에선 단순 취향 차이로 사용될 수 있을까?
두 개의 키워드는 사용목적은 같지만 성능면에서 차이가 드러납니다.
그전에 직접 앱을 돌려보면서 reference count를 알아봅시다.
class Person {
let name: String
init(name: String) { self.name = name }
var apartment: Apartment?
deinit { print("\(name) is being deinitialized") }
}
class Apartment {
let unit: String
init(unit: String) { self.unit = unit }
var tenant: Person?
deinit { print("Apartment \(unit) is being deinitialized") }
}
먼저 두 개의 서로참조 클래스를 만들었습니다.
이를 lldb의 refCount를 확인하는 명령어를 통해 참조 카운트가 어떻게 진행되는지 알아보겠습니다.
명령어는 아래와 같습니다.
language swift refcount (클래스 변수명)
첫 번째로는 인스턴스 생성만 구현된 경우입니다.
var person: Person? = Person(name: "john")
var apartment: Apartment? = Apartment(unit: "unit")
중단을 통해 참조카운팅을 확인해 보니 세 가지 유형의 참조가 카운팅되는걸 볼 수 있었습니다.
ARC를 공부했을 때 Strong Count만 세면 되는 줄 알았는데 실제로는 세가지 유형이 존재하는 걸 알 수 있었습니다.
근데 왜 인스턴스만 생성하고 서로 참조하지 않고 있는데 strong count = 3이 될까요?
-> lldb에서 해당 참조카운팅을 확인하기 위해 참조하고 일시적으로 메모리에 남겨두기 때문에 strong count = 3이 됩니다. 실제로는 그렇지 않습니다.
두 번째로는 서로 참조를 구현한 경우입니다.
person?.apartment = apartment
apartment?.tenant = person
서로 참조하니 strong 카운트가 1 증가했습니다.
바로 person변수와 aparment에 nil을 주입시키겠습니다.
세 번째로 nil을 할당한 경우
person = nil
apartment = nil
변수에 nil을 대입하니 lldb에서 더 이상 접근할 수 없었습니다. 대신 Debug Memory Graph를 통해 메모리가 누수되었다는 것을 확인할 수 있습니다.
위 카운팅처럼 weak, unowned도 모두 카운팅이 되고 있었습니다.
- strong: 객체에 대한 강한 참조의 개수를 셉니다. 강한 참조의 카운트가 0이 되면 deinit이 호출됩니다. 이 시점에서 unowned 접근은 앱크래시, weak 접근은 nil을 리턴합니다.
- unowned: unowned의 참조 개수를 셉니다. 강한 참조가 존재하면 +1이 추가됩니다. deinit이 완료되면 추가된 1이 차감됩니다. unowned가 0이 되면 최종적으로 메모리에서 해제합니다.
- weak: weak의 참조 개수를 셉니다. unowned 참조가 존재하면 +1이 추가됩니다. 객체가 메모리 할당 해제가 되면 추가된 1이 차감됩니다.
Side Table
추가적인 정보가 여기서 나오는데, Side Table은 Swift4.0에서부터 도입되었습니다.
객체 인스턴스가 생성됐을 때, ARC 카운팅과 함께 Side Table이 생성됩니다.
이는 객체의 간단한 정보들을 담고 있습니다. (참조 주소와 같은)
Side Table은 weak/unowned참조가 존재할 때 생성되고, 최종적으로 weak카운트가 0이 될 때 소멸됩니다.
weak 접근은 사이드 테이블을 통해 객체 참조 주소에 접근하게 됩니다. 이는 간접접근이라 볼 수 있습니다.
unowned은 별도로 사이드테이블을 통한 것이 아닌 직접 접근하게 됩니다.
weak와 unowned의 성능차이는 눈에 띌 정도로 나타나진 않지만, weak를 통한 접근은 추가적인 비용이 발생하기 때문에, 굳이 비교하자면 weak보다 unowned가 더 빠르다고 볼 수 있습니다.
메모리 누수 방지를 위해 strong 참조만 신경 썼지만, 더 많은 카운팅이 존재했고, weak와 unowned의 차이를 알 수 있었습니다!
클로저가 아직 익숙하지 않아서 함수에서 사용된 후행클로저를 좀 자세히 알고 싶어서 작성합니다.
후행 클로저
함수의 마지막 인수에 클로저 표현식을 전달해야 하고 클로저 표현식이 긴 경우 후행 클로저로 작성하면 가독성이 더 좋아집니다.
func someFunctionThatTakesAClosure(closure: () -> Void) {
// function body goes here
}
// Here's how you call this function without using a trailing closure:
someFunctionThatTakesAClosure(closure: {
// closure's body goes here
})
// Here's how you call this function with a trailing closure instead:
someFunctionThatTakesAClosure() {
// trailing closure's body goes here
}
자주 사용했던 고차함수 map도 후행클로저로 더 가독성 있게 표현할 수 있습니다.
let num = arr.map{ $0.description }
이는 파라미터로 인자를 받아서 클로저에 실행할 수도 있고, 받지않고 실행할 수 있습니다.
// 인자 없이 후행 클로저
func calculate(_ closure: (Int, Int) -> String ) {
print(closure(10, 20))
}
// 인자를 전달하는 후행클로저
func calculate2(_ num1: Int,_ num2: Int,_ closure: (Int, Int) -> String) {
let result = closure(num1, num2)
}
// 후행 클로저의 구현부는 함수의 호출부에서 구성
calculate { num1, num2 in
return "\(num1 + num2)"
}
calculate2(10, 20) { num1, num2 in
return "\(num1 + num2)"
}
어떠한 객체나 UI같은 하나의 상태를 타입으로 나타낼 때 열거형(enum)을 자주 사용했었습니다.
enum State {
case run
case walk
case sleep
case eat
case seat
}
// 배열로 사용한다면..?
var state = ["run", "walk", "sleep", "eat", "seat"]
배열로 사용하면 인덱스 범위를 넘어가거나 올바른 문자열을 받지 못하는 사이드 이펙트를 경험하기도 했습니다.
이를 열거형으로 사용하면 정해진 범위의 타입으로 지정할 수 있어서 코드 가독성과 사이드이펙트를 줄일 수 있었습니다.
오늘 알아볼 것은 이런 단순하게 상태에 대한 정의를 넘어서 더 다양한 방식들을 사용해보려고 합니다.
연관값
열거형의 case는 각 상태와 연관된 값을 추가로 설정할 수 있습니다. 이를 통해서 해당 case를 더 구체화 할 수 있습니다.
enum NetworkResult {
case success
case serverError(code: Int)
}
var someEnum: NetworkResult = .serverError(code: 119)
print(someEnum)
// someEnum1(code: 119)
위 예시와 같이 서버에러의 경우 에러와 연관된 에러코드와 함께 지정되어 해당 케이스의 값을 더 구체적으로 설정할 수 있습니다.
CaseIterable protocol
열거형에 CaseIterable을 채택하여 모든 케이스를 배열에 담아 리턴할 수 있습니다.
enum Configuration: CaseIterable {
case setting
case ui
case network
case logic
}
for element in Configuration.allCases {
print(element)
}
// setting
// ui
// network
// logic
print(Configuration.allCases.count)
// 4
CaseIterable프로토콜의 allCases는 static 변수라 인스턴스가 아닌 타입으로 접근해야합니다.
public protocol CaseIterable {
/// A type that can represent a collection of all values of this type.
associatedtype AllCases : Collection = [Self] where Self == Self.AllCases.Element
/// A collection of all values of this type.
static var allCases: Self.AllCases { get }
}
생성자
열거형은 구조체와 같이 값타입 형식입니다. 그래서 생성자를 구현하지 않아도 생성할 수 있지만, 구조체처럼 생성자를 구현할 수 있습니다.
enum GameState: Int, CaseIterable {
case play = 1
case record
case exit
case none
init(_ index: Int) {
self = GameState.allCases[index]
}
}
var baseballGameState1: GameState = .play
var baseballGameState2 = GameState(rawValue: 1)
var baseballGameState3 = GameState(3)
print(GameState.allCases)
print(baseballGameState3)
// 4
// none
GameState(3) 인덱스를 받아서 생성할 수 있습니다.
이렇게 생성자를 사용한다면 코드를 더 간결하게 사용 할 수 있을것이라 생각합니다.
프로퍼티와 메서드
열거형 안에 프로퍼티와 메서드를 구현할 수 있습니다. 하지만 get-only 프로퍼티만 구현할 수 있습니다.
enum SomeEnum2 {
case some1
case some2
var description: String {
switch self {
case .some1:
return "some1"
case .some2:
return "some2"
}
}
func printState(state: SomeEnum2) {
switch self {
case .some1:
print("some1")
case .some2:
print("some2")
}
}
}
var someEnum2: SomeEnum2 = .some1
print(someEnum2.description)
// some1
static
열거형 안에 static 키워드를 추가하여 타입메서드와 타입 프로퍼티를 생성할 수 있습니다.
enum SomeEnumWithStaticMethod {
case some1
case some2
static var count: Int = 0
static func printState(state: SomeEnumWithStaticMethod) {
switch state {
case .some1:
print("some1")
case .some2:
print("some2")
}
}
}
print(SomeEnumWithStaticMethod.count)
SomeEnumWithStaticMethod.count = 10
SomeEnumWithStaticMethod.printState(state: .some1)
SomeEnumWithStaticMethod.printState(state: .some2)
print(SomeEnumWithStaticMethod.count)
// 0
// some1
// some2
// 10
기존의 프로퍼티는 get-only 프로퍼티만 생성할 수 있지만, static 키워드로 타입프로퍼티를 선언하면 타입에 접근하기 때문에 get-set property로 선언할 수 있습니다. 해당 타입 메서드와 프로퍼티는 값에 접근할 때 타입에 접근하여 사용해야합니다.
enum Toggle {
case on
case off
mutating func toggle() {
switch self {
case .on:
self = .off
case .off:
self = .on
}
}
}
var buttonState: Toggle = .off
buttonState.toggle()
If case, guard case
열거형 안의 case에 접근할 때 switch구문을 통해 접근했었습니다.
다만 switch구문을 사용하면 모든 케이스에 대한 동작을 구현해야 하기 때문에 코드가 길어질 수 밖에없습니다.
여러 케이스중 하나에만 접근하고 싶다면 switch 대신 If case, guard case를 사용하면 더 간결하게 열거형에 접근할 수 있습니다.
if case .on = buttonState {
print("On")
}
if case .on = buttonState {
print("ON")
}
On
ON
guard case .on = buttonState else {
return
}
프로토콜 채택
열거형에 프로토콜을 채택할 수 있습니다.
protocol GameStateProtocol {
static var gameStateTitle: String { get }
func printGameState()
mutating func changeGameState(state: BaseballGameState)
}
enum BaseballGameState: String, GameStateProtocol {
static var gameStateTitle: String = "Baseball"
case play = "play"
case pause = "pause"
case exit = "exit"
func printGameState() {
print(BaseballGameState.gameStateTitle)
}
mutating func changeGameState(state: BaseballGameState) {
self = state
}
}
var state = BaseballGameState.pause
state.printGameState()
print(BaseballGameState.gameStateTitle)
state.changeGameState(state: .play)
print(state)
// Baseball
// Baseball
// play