본문 바로가기

프로그래밍/Swift

Swift가 안전한 언어인 이유?

반응형

Swift에 대한 문서들을 보면 Safe라는 단어가 참 많다. 그만큼 Swift는 '안전'을 강조하는데 도대체 그 안전은 무엇을 의미하는 것일까?

 

프로그래밍 언어에 있어서 안전은 단연 개발자에게 적용되는 말이다. 개발자가 소프트웨어를 이미 개발하고 배포까지 된 상황에서 예상치 못한 치명적인 오류가 발생하는 것 만큼 위험한 상황은 없다. 따라서 안전한 언어는, 개발자가 코딩을 하면서 오류를 즉각적으로 발견함으로써 문제를 미연에 방지할 수 있도록 해준다. 

 

그럼 Swift는 어떤 방법들로 안전을 보장한다는 것일까?

 

 


 

변수는 사용 전 항상 초기화된다.

초기화, 즉 변수가 초기값을 가지게 되면 값이 없을때 생기는 문제들을 예방할 수 있다. 아래의 간단한 클래스를 통해 확인해보자.

class Student {
    let name: String
}

'name' 프로퍼를 가진 'Student' 클래스를 만들었다. 하지만 'Class 'Student' has no initializers' 라는 에러가 발생한다는 사실을 알 수 있다. 즉, 'name' 프로퍼티가 초기화나 기본값이 필요하다는 것을 바로 알려주어 그것이 항상 초기화 되도록 유도하는 것이다.

 

오류는 아래와 같이 해결할 수 있다.

class Student {
    let name: String
    
    init(name: String) {
        self.name = name
    }
}

let student = Student(name: "Jack")
print(student.name) // "Jack"

 

이렇게 초기화를 한 후 자유롭게 'Student' 클래스를 사용할 수 있다.

 

한가지 방법이 더 있다. 'name'에 직접 스트링 값을 할당해 주는 방법이다.

class Student {
    let name: String = ""
}

다만 이렇게 'name' 프로퍼티를 상수(let)로 선언 한다면 추후 다른 값으로 변경할 수가 없기 때문에 변수(var)로 선언 해주어야 한다. 상황에 따라 적절한 방법으로 사용해 주면 되겠다.

 

따라서 이렇게 항상 변수의 초기화를 요구하여 더욱 안전한 코딩을 할 수 있도록 도와준다.

 

 

배열 인덱스에서 out-of-bounds(범위를 벗어난) 오류를 확인한다.

배열에서 벗어난 인덱스가 주어질 경우 Crash와 함께 인덱스 범위를 벗어났다 것을 확인할 수 있다.

let studentArray = ["jack", "john", "jane"]
let student = studentArray[10]

학생 배열에 "jack", "john", "jane" 세 값이 할당되었고 11번째 학생을 찾기위해 인덱스 [10]이 주어지면 충돌이 난다.

이를통해 미리 인덱스 범위의 불일치를 확인할 수 있고, 빈 배열에 대한 메모리 낭비를 줄일 수 있다.

 

 

 

Integer에서 오버플로우를 확인한다.

Int 값의 범위는 아래와 같다.

  • Int8: -128 ~ 127
  • Int16: -32,768 ~ 32,767
  • Int32: -2,147,483,648 ~ 2,147,483,647
  • Int64: -9,223,372,036,854,775,808 ~ 9,223,372,036,854,775,807
  • UInt8: 0 ~ 255
  • UInt16: 0 ~ 65,535
  • UInt32: 0 ~ 4,294,967,295
  • UInt64: 0 ~ 18,446,744,073,709,551,615

만약 위의 범위를 넘어가면 오버플로우가 발생하지만, Swift에서는 기본적으로 crash를 발생시켜 오버플로우를 방지한다.

var unsignedInteger8Min = UInt8.min
print(unsignedInteger8Min) // 0

unsignedInteger8Min -= 1 // crash

UInt8에서 가장 작은 값은 '0'이고 그보다 작은 수를 가질 수 없다. 따라서 '-1'을 시도시 오버플로우 대신 충돌이 발생한다.

 

unsignedInteger8Min &-= 1
print(unsignedInteger8Min) // 255

만약 충돌대신 오버플로우를 그대로 발생시키고 싶다면 '&'기호를 연산기호 앞에 붙여주면 된다. 0에서 -1을 시도하였으나 오버플로우가 발생하여 255를 반환하게 된다.

 

 

 

옵셔널이 nil 값을 명시적으로 처리되도록 보장한다.

옵셔널이란?

 

  1. nil이 될수도, 값을 가질 수도 있다.

  2. 안전하게 사용하기 위해 언래핑이 필요하다.

 

옵셔널은 값이 있을 수도, 값이 없을 수도 있는 타입을 말한다. 만약 변수에 값이 없으면 nil을 반환하고 값이 있으면 해당 자료형에 대한 값을 반환하며, 2가지 경우에 대한 상황을 ? 기호를 통해 명시적으로 처리할 수 있다.

var optinalString: String?
print(optinalString) // nil 반환

optinalString = "String Value"
print(optinalString) // "String Value" 반환

 

optionalString을 사용하기 위해서는 언래핑을 해주어야 한다. 여러 방법 중 강제로 언래핑을 하려면 ! 를 사용해준다. 하지만 만약 값이 없는 옵셔널을 강제로 언래핑을 시도하게 되면 충돌이 발생한다.

var optinalString: String?
let forcedUnwrappedString = optinalString! // crash 발생

 

따라서 안전하게 언래핑을 위해서 아래 '옵셔널 바인딩'과 같은 방법을 사용하면 충돌을 회피할 수 있다.

var optinalString: String?
let forcedUnwrappedString = optinalString! // crash 발생
if let unwrappedString = optinalString { // 옵셔널 바인딩
    // 실행 안됨
    print(unwrappedString)
}

optinalString = "String Value"
if let unwrappedString = optinalString { // 옵셔널 바인딩
    print(unwrappedString) // "String Value" 반환
}

 

 

 

 

메모리는 자동으로 관리된다.

Swift는 ARC(Automatic Reference Counting)에 의해 클래스 인스턴스의 레퍼런스를 카운팅 하는 방식으로 메모리가 자동으로 관리된다.

 

기존 자바 등에 사용되던 GC(Garbage Collection)가 메모리를 관리하기 위해 런타임에서 감시하며 오버헤드를 만들어 내는 반면, ARC는 빌드시 컴파일러가 메모리 릴리즈가 필요한 곳에 자동으로 처리해주는 원리로 작동하기 때문에, 결론적으로 수동으로 해야할 메모리 처리를 대신 해주는 정도의 역할을 하는 셈이다.

 

ARC는 새로운 클래스 인스턴스가 생성 될때 레퍼런스 카운팅을 증가시키고, 해제 될때 감소시키는 방식으로 작동한다. 모든 인스턴스의 레퍼런스가 해제되면 메모리가 비워진다.

 

var instanceOfPerson1: Person? = Person()
// 참조 카운트 1
var instanceOfPerson2: Person? = instanceOfPerson1
// 참조 카운트 2
var instanceOfPerson3: Person? = instanceOfPerson1
// 참조 카운트 3

Person이라는 클래스의 인스턴스 3개를 만들어서 참조 카운트 3이 되었다.

 

var instanceOfPerson1 = nil
// 참조 카운트 2
var instanceOfPerson2 = nil
// 참조 카운트 1

Person 인스턴스 중 1번과 2번의 참조를 해제시켜도 메모리는 비워지지 않는다.

 

var instanceOfPerson3 = nil
// 참조 카운트 0
// Person 클래스 인스턴스에 대한 메모리 정리됨

마지막 인스턴스까지 해제 시켜야 결국 메모리가 비워진다. 컴파일러가 수동으로 해야 할 작업을 인간의 실수 없이 처리해주므로 훨씬 안전하게 관리된다고 볼 수 있겠다.

 

 

 

 

오류 처리를 통해 예상치 못한 장애를 제어할 수 있다.

스위프트는 4가지 방법으로 오류를 처리한다.

 

  1. 함수를 호출할 때 오류를 전파

func canThrowErrors() throws -> String

func cannotThrowErrors() -> String

  2. do-catch 문을 사용하여 오류를 처리

do {
    try {expression}
    {statements}
} catch {
    {statements}
}

  3. 옵셔널로 오류를 처리

func someThrowingFunction() throws -> Int {
    // ...
}

let x = try? someThrowingFunction()

let y: Int?
do {
    y = try someThrowingFunction()
} catch {
    y = nil
}

  4. 오류가 발생하지 않는다고 처리

let photo = try! loadImage(atPath: "./Resources/John Appleseed.jpg")

 

자세한 내용은 아래 문서를 통해 확인할 수 있다.

docs.swift.org/swift-book/LanguageGuide/ErrorHandling.html

 

 

 

 

 

* 참고문서

Apple Inc. “The Swift Programming Language (Swift 5.3)”

swift.org

반응형