코드 리뷰에서 “이 클래스에 final 좀 붙여주세요” 같은 말, 한 번쯤 들어보셨을 거예요.
그냥 습관처럼 보이지만, 이유를 알고 나면 코드를 보는 눈이 달라집니다.
결론부터 말씀드리면, final은 “이 클래스는 상속으로 확장할 생각이 없다”는 선언이에요. 성능과 설계 안정성을 동시에 챙겨주는, 생각보다 묵직한 한 줄이죠.
오늘은 왜 Swift 클래스에 final을 붙이라고 하는지, 그 진짜 이유를 정리해볼게요.
final이 대체 뭔가요?
final은 상속과 오버라이드를 막는 키워드예요.
클래스 앞에 붙이면 다른 클래스가 그 클래스를 상속할 수 없어요.
메서드나 프로퍼티 앞에 붙이면 그 멤버만 오버라이드를 막을 수도 있고요.
final class Logger {
func log(_ msg: String) { print("[LOG] \(msg)") }
}
let logger = Logger()
logger.log("결제 완료") // 출력: [LOG] 결제 완료
여기서 Logger를 상속하려고 class FileLogger: Logger 라고 쓰면 컴파일 에러가 납니다.
“이 클래스는 여기서 끝이에요”라고 못을 박는 거죠.
final을 붙이면 왜 빨라질까?
첫 번째 이유는 성능이에요. 핵심은 디스패치(dispatch) 방식이에요.
상속 가능성이 열려 있으면 컴파일러는 어떤 메서드가 실제로 불릴지 런타임까지 몰라서, 매번 표를 뒤져 찾아야 합니다.
이걸 동적 디스패치라고 불러요. 클래스마다 있는 메서드 표(vtable)를 조회한 뒤 호출하죠.
반면 final이 붙으면 상황이 달라져요.
더 이상 자식 클래스가 끼어들 수 없으니 컴파일러는 “이 호출은 무조건 이 메서드”라고 확신할 수 있어요.
그래서 표를 뒤지는 과정 없이 곧장 호출합니다. 이게 정적 디스패치예요.
한 걸음 더 나아가면, 짧은 메서드는 아예 호출 지점에 코드를 펼쳐 넣는 인라이닝까지 가능해져요.
호출 한 번의 비용이 크진 않지만 반복문 안에서 수천 번 불리는 메서드라면 이 차이가 쌓입니다.
진짜 이유는 성능보다 ’설계’예요
사실 저는 성능보다 이쪽이 더 중요하다고 생각해요.
바로 취약한 기반 클래스 문제(fragile base class)를 막아준다는 점이에요.
상속을 열어두면, 내가 만든 부모 클래스의 내부 동작에 누군가 자유롭게 끼어들 수 있어요.
부모의 메서드 하나를 무심코 바꿨을 뿐인데, 그걸 오버라이드한 자식들이 줄줄이 깨지는 상황이 생기죠.
상속은 캡슐화를 가장 크게 깨뜨리는 관계예요. 자식은 부모의 속살까지 들여다보게 되니까요.
그래서 객체지향 설계 원칙에서도 이렇게 말해요.
상속을 위해 설계하고 문서화하라. 그럴 게 아니라면 상속을 금지하라. — 이펙티브 자바의 유명한 조언이죠.
final은 이 조언을 코드로 실천하는 도구예요.
“상속용으로 설계하지 않았으니 열어두지 않겠다”는 의도를 컴파일러가 강제해주는 거죠.
의도가 명확해지면, 나중에 코드를 읽는 동료도 헤맬 일이 줄어들어요.
그럼 무조건 붙여야 하나요?
여기서 중요한 이야기 하나. Swift에서 참조 타입이 꼭 클래스일 필요는 없어요.
상태를 값으로 다뤄도 된다면 struct가 더 나은 선택인 경우가 많아요. struct는 애초에 상속 자체가 없거든요.
즉 “final을 붙일까?“를 고민하기 전에, “이거 굳이 클래스여야 하나?“를 먼저 물어보는 게 순서예요.
그래도 클래스를 써야 한다면, 아래 기준으로 판단하시면 돼요.
| 상황 | 판단 |
|---|---|
| 상속을 염두에 두지 않은 클래스 | final을 붙인다 (기본값) |
| 성능이 민감한 반복 호출 코드 | final로 정적 디스패치 유도 |
| 명확한 확장 지점을 설계한 프레임워크 | final을 열어둔다 |
| UIViewController 등 상속 전제 API | 붙이지 않는다 |
정리하면 이렇게 기억하시면 편해요.
- 상속을 의도하지 않았다면 기본적으로 final을 붙인다
- 확장 지점은 열고 싶은 곳만 골라서 연다
- struct로 될 일이면 애초에 클래스를 피한다
면접에서는 이렇게 물어봅니다
Q. final 키워드를 붙이면 어떤 이점이 있나요?
상속과 오버라이드를 막아 설계 의도를 명확히 드러냅니다. 동시에 컴파일러가 동적 디스패치 대신 정적 디스패치를 쓸 수 있어 호출 비용이 줄고 인라이닝 최적화도 가능해집니다.
Q. 그럼 모든 클래스에 final을 붙이는 게 맞나요?
상속을 위해 설계하고 문서화한 클래스라면 열어둬야 합니다. 다만 그런 의도가 없는 클래스는 기본적으로 닫아두는 편이 안전하고 애초에 값 의미가 맞으면 struct를 먼저 고려하는 게 좋습니다.
final 한 줄은 단순한 최적화 팁이 아니라, “이 클래스를 어떻게 쓸 것인가”에 대한 답이에요.
오늘부터 클래스를 새로 만들 때 “이거 상속 열어둘 이유가 있나?“를 한 번씩만 물어보세요. 그 습관 하나로 코드가 훨씬 단단해질 거예요.
