우리는 개발중 분명 complieTime에는 에러가 안났는데, 앱을 실행시켜보니 원하던 방향과 다르게 작동하는 버그를 많이 발견하곤 한다.
따라서, complieTime에 버그를 잡는다면 많은 시간과 노력이 준다는 것을 알고 있다.
그중 한 예시를 들어보고 어떻게 하면 Complie시에 버그를 잡아 낼 수 있는지 알아보자.
예시
개발중에 많은 단계에서 우리는 객체에 대해 unique한 Identifier(식별자)가 필요할 때가 있다.
예를 들면, 로그인을 할때 사용자를 식별한다던지, Cache를 할때 이미 Cache된 데이터인지 아닌지 등등..
주로 특정한 객체를 추적하고 그에 따른 값이나 결과를 가져오기 위해 사용이 된다.
문제는 이러한 Identifier를 가지는 타입의 객체가 여러개 일 수 있다는 것이다.
User라는 객체와 Game이라는 객체가 있다고 해보자.그리고 각 Type에 Identifier가 있다.
struct Identifier:Hashable {
let string: String
}
struct User {
let id: Identifier
let name: String
}
struct Game {
let id: Identifier
let name: String
}
이를 실제로 사용하려면 이런 모양이 될 것 이다.
User를 관리하는 UserManager, Game을 관리하는 GameManager가 Idenfitifer를 가지고 Id를 체크하는 모습이다.
struct UserMananger {
func checkID(id: Identifier) {
print("Check User\(id)!")
}
}
struct GameManager {
func checkID(id: Identifier) {
print("Check Game\(id)!")
}
}
let userManager = UserMananger()
let gameManager = GameManager()
let userA = User(id: Identifier(string: "Park") , name: "Seob")
let gameA = Game(id: Identifier(string: "King"), name: "dom")
userManager.checkID(id: userA.id)
gameManager.checkID(id: gameA.id)
identifier에 ExpressibleByStringLiteral를 추가해줌으로써 가독성을 조금더 높여보자.
extension Identifier: ExpressibleByStringLiteral {
init(stringLiteral value: StringLiteralType) {
string = value
}
}
이제 Id를 선언할때 이니셜라이저대신 String으로 Literal하게 만들 수 있게 되었다.
let userA = User(id: "Park" , name: "Seob")
let gameA = Game(id: "King", name: "dom")
문제점
여기서 문제점은 User나 Game이나 동일한 Identifier를 가지고 있다는 것이다.
만약, 실수로 GameManger에다가 UserId를 넣어도 에러가 발생하지 않으며 후에 문제를 알아차렸을 때에도 디버깅 하기 매우 까다롭다.
userManager.checkID(id: gameA.id)
gameManager.checkID(id: userA.id) //not Error in Compile Time
그렇다면 애초에 컴파일 타임에서 서로의 Identifier가 다른 타입이라는 것을 알아 차릴 방법이 없을까?
물론, GameIdentifer, UserIdentifier라는 구조체를 또 따로 생성을 할 수는 있으나 이는 매우 귀찮은 일이 될 것이다.
Generic을 이용한 타입 추론
Identifier에 Generic을 적용해 보자.
struct Identifier<Value>:Hashable {
let string: String
}
struct User {
let id: Identifier<User>
let name: String
}
struct Game {
let id: Identifier<Game>
let name: String
}
struct UserMananger {
func checkID(id: Identifier<User>) {
print("Check User\(id)!")
}
}
struct GameManager {
func checkID(id: Identifier<Game>) {
print("Check Game\(id)!")
}
}
여기서 주의 깊게 볼점은 Identifier의 제네릭이 사용 되는 곳이다.
막상 제네릭을 선언한 Identifier의 내부에서는 사용이 되지 않지만,
이를 구체화하는 곳 Game이나 User에서 자기 자신을 넣음으로써 서로 강하게 결합이 된다.
이제 컴파일러는 옳지 못한 id가 들어오면 에러를 내게 된다.
총 코드
struct Identifier<Value>:Hashable {
let string: String
}
extension Identifier: ExpressibleByStringLiteral {
init(stringLiteral value: StringLiteralType) {
string = value
}
}
struct User {
let id: Identifier<User>
let name: String
}
struct Game {
let id: Identifier<Game>
let name: String
}
struct UserMananger {
func checkID(id: Identifier<User>) {
print("Check User\(id)!")
}
}
struct GameManager {
func checkID(id: Identifier<Game>) {
print("Check User\(id)!")
}
}
let userManager = UserMananger()
let gameManager = GameManager()
let userA = User(id: "Park" , name: "Seob")
let gameA = Game(id: "King", name: "dom")
userManager.checkID(id: userA.id)
gameManager.checkID(id: gameA.id)
참고: https://www.swiftbysundell.com/articles/type-safe-identifiers-in-swift/
'Swift' 카테고리의 다른 글
Type Erasure in Swift (0) | 2022.05.23 |
---|---|
Data Testing in Swift (0) | 2022.05.12 |
Memory Leak Case When ReferenceType in Dictionary (0) | 2022.05.11 |
깊은 복사 & 얕은 복사 (Swift) (2) | 2022.05.08 |
MetaType_iOS (0) | 2022.05.05 |
댓글