본문 바로가기
Swift/TOPIC

Swift | final 키워드에 대한 고찰(feat. Method Dispatch)

by UDDT 2025. 11. 12.

final 키워드는 어디에 쓸까?

    Swift에서 final은 크게 2가지(클래스/메서드) 경우에 사용합니다

// 일반적인 class
class BaseViewController: UIViewController {
	func configureUI() {
    	print("Base configure")
    }
}

class ProfileViewController: BaseViewController {
	override func configureUI() {
    	print("Profile configure")
    }
}

let vc: BaseViewController = ProfileViewController()
vc.configureUI()

 

// class에서의 final 키워드
final class HomeViewController: UIViewController {
	func configureUI() {
    	print("Home Configure")
    }
}

let homeVC = HomeViewController()
homeVC.configureUI()

// 상속 가능 클래스 + final 메서드
class BaseViewController: UIViewController {
	final func configureNavigationBar() {
    	// 공통 네비게이션 설정
    }
    
    func configureUI() {
    	// 서브클래스에서 커스터마이징
    }
}

 

    위와 같은 형태에서 final 키워드를 붙이면 어떻게 될까요?

final이 뭔데?

    final 키워드의 역할은 단순합니다

   이 타입은 더 이상 상속할 수 없다 또는 이 메서드는 더 이상 override 할 수 없다는 선언입니다

   즉, final class는 상속 자체를 막는 것이고

   final func은 override 를 막는 것입니다

final 키워드가 없는 세상

    final이 없는 예제로 돌아와보겠습니다

// 일반적인 class
class BaseViewController: UIViewController {
	func configureUI() {
    	print("Base configure")
    }
}

class ProfileViewController: BaseViewController {
	override func configureUI() {
    	print("Profile configure")
    }
}

let vc: BaseViewController = ProfileViewController()
vc.configureUI()

 

    ProfileViewController가 configureUI() 메서드를 실행합니다

  그러면 여기서 컴파일러는 다음과 같은 고민(연산)을 하게 됩니다

  "vc의 타입은 BaseViewController인데, 얘는 실제로 ProfileViewController일까 아니면 BaseViewController일까?

    그러면 configureUI()는 정확히 어떤 메서드를 말하는거지?"

 

  이때 선택하는 전략이 method dispatch입니다

Method Dispatch

    Swift에서 사용되는 대표적인 메서드 디스패치 방식은 

    정적 디스패치(Static Dispatch)동적 디스패치(Dynamic Dispatch)가 있습니다

 

    정적 디스패치어떤 함수를 부를지 컴파일 타임에 결정되는 방식입니다

   보통 값 타입이나 Enum의 메서드가 정적으로 동작합니다

 

    Swift의 struct는 상속이 없고, 항상 값 타입입니다

    상속이 없다는 것은 override 개념도 없다는 것이기에 어떤 구현을 쓸지 시스템이 고민하는 시간이 적어집니다

    그렇기 때문에 대부분의 메서드가 정적 디스패치로 처리됩니다

 

    동적 디스패치클래스 상속/override를 지원하기 위해

   메서드들을 테이블(V-Table)에 모아두고, 런타임에 테이블을 통해 찾아가는 방식입니다

 

    Swift의 class는 상속과 override를 지원합니다

    따라서 이때 V-Table 기반의 테이블 디스패치를 통해 연산을 하게 됩니다

 

    이러한 연산 작용 때문에

   정적 디스패치는 컴파일 타임에, 동적 디스패치는 런타임에 함수의 호출이 결정됩니다

 Static Dispatch

    정적 디스패치는 다음과 같은 특성을 가지고 있습니다

    - 함수 포인터를 직접 호출

    - 인라인 최적화 가능(함수가 단순하기 때문에 바로 펼쳐서 빠르게 최적화)

    - 호출 오버헤드가 적음

    함수 포인터를 직접 호출하기 때문에, V-Table의 조회 없이 바로 실행해야할 함수를 파악할 수 있습니다

 

     정적 디스패치로 작동하는 대상들의 예시는 다음과 같습니다

     - struct, enum의 메서드

     - final class / final func

     - private / fileprivate 메서드(외부에서 override가 불가능)

Dynamic Dispatch

    동적 디스패치V-Table(Virtual Method Table)을 사용합니다

   각 클래스는 클래스가 가진 메서드들에 대한 함수 포인터 배열을 가지고 있습니다

   객체에는 각 객체가 어떤 클래스의 인스턴스인지를 가르키는 메타데이터 포인터가 있습니다

   메서드를 호출하게 되면,

   객체가 클래스 메타데이터로 이동하게 되고 그 안에 있는 V-Table 포인터를 가져옵니다

   이후 메서드 인덱스에 해당하는 함수 포인터를 꺼내 호출합니다 

 

    동적 디스패치로 작동하는 대상들의 예시는 다음과 같습니다

    - 클래스 상속 -> V-Table Lookup

    - 프로토콜 호출 -> Witness Table Lookup (프로토콜 타입으로 메서드를 호출할 때 PWT 조회)

// 일반적인 class 예제
class BaseViewController: UIViewController {
	func configureUI() {
    	print("Base configure")
    }
}

class ProfileViewController: BaseViewController {
	override func configureUI() {
    	print("Profile configure")
    }
}

let vc: BaseViewController = ProfileViewController()
vc.configureUI()

 

 

    vc.configureUI()라는 코드가 실행될 때 

   내부적으로는 vc의 실제 타입을 확인합니다("아 ~ ProfileViewController의 인스턴스구나")

   ProfileViewController의 메타데이터를 통해 V-Table을 찾고

   그 V-Table 안의 configureUI() 슬롯을 따라가서

   ProfileViewController.configureUI() 구현을 호출합니다

 성능적인 이점이 있을까?

    final 키워드가 붙으면 컴파일러는 

    이 메서드가 override될 일이 없다는 것을 확신할 수 있습니다

    따라서 V-Table lookup 없이 곧바로 함수를 호출하도록(devirtualization) 혹은 인라인 최적화를 할 수 있습니다

   컴파일러가 어떤 메서드를 동일하게 사용하는 것을 확신하면,

   함수 호출 자체를 없애고, 그 자리에 함수 본문을 그대로 붙여버립니다

// 인라인 최적화 예시
final class ProfileViewController: UIViewController {
	func logViewAppear() {
    	print("Profile appeared")
    }
}

let vc = ProfileViewController()
vc.logViewAppear()

// 인라인 최적화 시(개념적으로 적용)
print("Profile appeared")     // 컴파일러가 고민 없이 함수를 바로 써버림

 

    따라서 작은 유틸성 클래스나 상속할 의도가 전혀 없는 뷰 모델이나 매니저 객체들은

   설계 단계에서부터 final 키워드를 붙여주면 설계 의도가 명확해지고, 성능 최적화에 유리하게 작용할 수 있습니다

 정리

    Swift에서 final은 단순히 상속을 막는 키워드라기보다는,

   컴파일러 입장에서 동적 디스패치를 정적 디스패치로 바꿀 수 있게 해주는 힌트가 됩니다

 

    상속할 의도가 없는 타입이나 override를 허용하지 않는 메서드라면

   습관적으로 final 키워드를 붙여주는 것만으로도

   코드의 의도가 명확해지고, 성능 최적화를 고려하는 개발자가 될 수 있습니다

최근댓글

최근글

skin by © 2024 ttuttak