[Swift] Access Control: 깊이 있는 이해와 실전 활용법
Swift의 접근 제어(Access Control)는 코드의 가독성과 보안성을 높이는 핵심 기능입니다. 코드 구성 요소에 대한 접근을 의도적으로 제한함으로써 내부 구현 세부 사항을 숨기고 선호하는 인터페이스를 제공할 수 있습니다. 이 글에서는 Swift 접근 제어의 다양한 레벨과 실제 활용 방법, 그리고 개발 과정에서 마주치는 일반적인 상황들을 살펴보겠습니다.
Swift 접근 제어의 기본 원리
접근 제어는 특정 코드 조각이 다른 소스 파일이나 모듈에서 접근할 수 있는지를 제한하는 메커니즘입니다. 이를 통해 코드의 구현 세부 사항을 숨기고 사용자에게 명확한 인터페이스만 제공할 수 있습니다.
Swift에서는 6가지 접근 레벨을 제공합니다:
- open: 가장 덜 제한적인 접근 레벨로, 모듈 외부에서의 접근과 상속을 허용
- public: 모듈 외부에서 접근 가능하지만, 외부 모듈에서의 상속은 제한
- package: 같은 패키지 내에서만 접근 가능
- internal: 같은 모듈 내에서만 접근 가능 (기본 접근 레벨)
- fileprivate: 같은 소스 파일 내에서만 접근 가능
- private: 선언된 범위 내에서만 접근 가능 (가장 제한적)
이러한 접근 레벨은 소스 파일, 모듈, 패키지 개념을 기반으로 합니다.
public class PublicClass {
public var publicProperty = 0
internal var internalProperty = 0
fileprivate func filePrivateMethod() {}
private func privateMethod() {}
}
위 코드에서 PublicClass는 외부 모듈에서도 접근 가능하고, publicProperty도 마찬가지입니다. 그러나 internalProperty는 같은 모듈 내에서만, filePrivateMethod()는 같은 파일 내에서만, privateMethod()는 클래스 내에서만 접근 가능합니다.
접근 제어의 기본 원칙과 구체적 규칙
Swift 접근 제어에는 '더 낮은(더 제한적인) 접근 레벨의 엔티티를 이용해 더 높은 접근 레벨의 엔티티를 정의할 수 없다'는 핵심 원칙이 있습니다. 이 원칙은 몇 가지 중요한 규칙으로 이어집니다:
- 변수와 프로퍼티: 변수나 프로퍼티는 그 타입보다 더 높은 접근 레벨을 가질 수 없습니다.
private struct PrivateStruct { }
// 오류: 프로퍼티 타입이 private이므로 public으로 선언할 수 없음
public var publicInstance = PrivateStruct()
위 코드에서 오류가 발생하는 이유는 PrivateStruct가 private이기 때문에, 이 타입의 변수를 public으로 선언할 수 없기 때문입니다. 이는 타입이 접근할 수 없는 곳에서 변수가 사용될 수 있는 상황을 방지합니다.
- 함수: 함수는 그 매개변수 타입과 반환 타입보다 더 높은 접근 레벨을 가질 수 없습니다.
private struct PrivateStruct { }
// 오류: 반환 타입이 private이므로 internal(기본값) 함수로 선언할 수 없음
func returnPrivateStruct() -> PrivateStruct {
return PrivateStruct()
}
이 함수는 PrivateStruct를 반환하므로, 함수 자체도 private으로 선언되어야 합니다. 그렇지 않으면 함수는 접근 가능하지만 그 반환 타입은 접근 불가능한 모순이 발생합니다.
- 튜플과 함수 타입: 복합 타입의 접근 레벨은 구성하는 타입 중 가장 제한적인 접근 레벨을 따릅니다.
internal class SomeClass { }
private struct SomeStruct { }
// 튜플 타입은 가장 제한적인 타입인 SomeStruct(private)의 접근 레벨을 따름
let tuple: (SomeClass, SomeStruct) = (SomeClass(), SomeStruct())
// 이 튜플은 private 접근 레벨을 가짐
이 코드에서 튜플은 SomeClass(internal)와 SomeStruct(private)를 포함하므로, 튜플 전체의 접근 레벨은 더 제한적인 private이 됩니다.
클래스, 구조체 및 열거형의 접근 제어
클래스, 구조체, 열거형에 접근 제어를 적용할 때는 몇 가지 중요한 특성이 있습니다.
- 멤버의 기본 접근 레벨: 타입의 접근 레벨에 따라 멤버의 기본 접근 레벨이 결정됩니다.
public struct PublicStruct {
var implicitlyInternalProperty = 0 // 기본값: internal
public var explicitlyPublicProperty = 0
private var explicitlyPrivateProperty = 0
}
위 코드에서 PublicStruct는 public이므로 외부 모듈에서 접근 가능합니다. 그러나 implicitlyInternalProperty는 명시적인 접근 레벨이 없으므로 기본값인 internal을 가지며, 동일 모듈 내에서만 접근 가능합니다. 이는 공개 API를 명시적으로 선택하도록 하여 실수로 내부 작동 방식이 노출되는 것을 방지합니다.
- 중첩 타입의 접근 레벨: 중첩 타입은 포함하는 타입의 접근 레벨을 따르지만, public 타입 내의 중첩 타입은 기본적으로 internal입니다.
public struct OuterStruct {
struct NestedStruct {
// NestedStruct는 기본적으로 internal
}
public struct PublicNestedStruct {
// 명시적으로 public으로 선언된 중첩 타입
}
}
이 예제에서 NestedStruct는 명시적인 접근 제어 수준이 없으므로 기본적으로 internal입니다. OuterStruct가 public임에도 불구하고, 중첩 타입을 공개 API의 일부로 만들려면 명시적으로 public으로 선언해야 합니다.
서브클래싱과 멤버 오버라이딩
접근 제어는 상속과 상호작용할 때 몇 가지 중요한 규칙을 가집니다:
- 서브클래싱 제한: 서브클래스는 슈퍼클래스보다 높은 접근 레벨을 가질 수 없습니다.
internal class InternalClass { }
// 오류: 서브클래스는 슈퍼클래스보다 더 높은 접근 레벨을 가질 수 없음
public class PublicSubclass: InternalClass { }
이 코드는 오류를 발생시킵니다. InternalClass는 모듈 내부에서만 접근 가능하므로, 그 서브클래스인 PublicSubclass를 외부 모듈에서 접근 가능하게 만들 수 없습니다.
- 멤버 오버라이딩 시 접근성 확장: 서브클래스에서 상속받은 멤버를 오버라이드할 때 더 높은 접근 레벨을 부여할 수 있습니다.
public class SuperClass {
fileprivate func someMethod() { }
}
public class SubClass: SuperClass {
// 오버라이드 메서드는 더 높은 접근 레벨 가능
override internal func someMethod() {
super.someMethod()
}
}
이 예제에서 SuperClass의 someMethod()는 fileprivate이지만, SubClass에서 오버라이드할 때 internal로 접근 레벨을 높일 수 있습니다. 이는 더 넓은 컨텍스트에서 상속된 기능을 사용할 수 있게 해줍니다.
프로토콜 접근 제어의 특수성
프로토콜 접근 제어는 몇 가지 특별한 규칙을 가집니다:
- 프로토콜 요구사항: 프로토콜의 요구사항은 자동으로 프로토콜과 동일한 접근 레벨을 가집니다.
public protocol PublicProtocol {
func someMethod() // 자동으로 public
}
프로토콜이 public이므로 그 요구사항인 someMethod()도 자동으로 public이 됩니다. 이는 프로토콜의 모든 요구사항이 프로토콜을 채택하는 모든 타입에서 동일한 접근성을 가져야 함을 보장합니다.
- 프로토콜 상속: 새 프로토콜은 상속하는 프로토콜보다 높은 접근 레벨을 가질 수 없습니다.
internal protocol InternalProtocol { }
// 오류: 새 프로토콜은 상속하는 프로토콜보다 더 높은 접근 레벨을 가질 수 없음
public protocol PublicProtocol: InternalProtocol { }
이 코드는 오류를 발생시킵니다. InternalProtocol은 모듈 내부에서만 접근 가능하므로, 그것을 상속하는 PublicProtocol을 외부 모듈에서 접근 가능하게 만들 수 없습니다.
확장을 통한 접근 제어 수정
Swift에서는 확장(extension)을 사용하여 기존 타입에 새로운 기능을 추가할 수 있으며, 이 과정에서 접근 제어도 적용할 수 있습니다.
- 확장의 기본 접근 레벨: 확장에서 추가된 멤버는 확장된 타입의 접근 레벨을 기본값으로 가집니다.
public struct PublicStruct { }
extension PublicStruct {
func implicitlyInternalMethod() { } // 기본적으로 internal
private func explicitlyPrivateMethod() { }
}
이 예제에서 implicitlyInternalMethod()는 명시적인 접근 제어 수준이 없으므로 기본적으로 internal입니다. 이는 PublicStruct가 public임에도 불구하고, 확장에서 추가된 멤버는 명시적으로 접근 레벨을 지정하지 않으면 기본적으로 internal임을 보여줍니다.
- 명시적 접근 레벨 지정: 확장 자체에 접근 수준을 지정하면 확장 내의 모든 멤버가 해당 수준을 기본값으로 가집니다.
public struct PublicStruct { }
// 이 확장의 모든 멤버는 기본적으로 private
private extension PublicStruct {
func privateMethod() { } // private
}
이 코드에서 확장 자체가 private으로 선언되었으므로, 확장 내의 모든 멤버는 기본적으로 private입니다. 이는 확장에서 추가된 기능을 특정 파일이나 범위에 제한하고 싶을 때 유용합니다.
실전 예제: 라이브러리 설계 시 접근 제어 활용
실제 라이브러리 개발에서 접근 제어는 API 설계의 중요한 부분입니다. 다음은 간단한 네트워킹 라이브러리의 예시입니다:
// 공개 API인 NetworkManager
public class NetworkManager {
// 공개 초기화 메서드
public init(configuration: NetworkConfiguration) {
self.configuration = configuration
self.session = URLSession(configuration: .default)
}
// 공개 함수 - 라이브러리 사용자가 호출할 수 있음
public func fetchData(from url: URL, completion: @escaping (Result<Data, Error>) -> Void) {
let request = createRequest(url: url)
performRequest(request, completion: completion)
}
// 내부 구현 상세 - 라이브러리 내부에서만 사용
private func createRequest(url: URL) -> URLRequest {
var request = URLRequest(url: url)
request.timeoutInterval = configuration.timeout
return request
}
private func performRequest(_ request: URLRequest, completion: @escaping (Result<Data, Error>) -> Void) {
let task = session.dataTask(with: request) { data, response, error in
// 응답 처리 로직...
if let error = error {
completion(.failure(error))
return
}
guard let data = data else {
completion(.failure(NetworkError.noData))
return
}
completion(.success(data))
}
task.resume()
}
// 프라이빗 프로퍼티 - 내부 상태
private let configuration: NetworkConfiguration
private let session: URLSession
// 에러 정의
public enum NetworkError: Error {
case noData
case invalidResponse
}
}
// 공개 설정 객체
public struct NetworkConfiguration {
public var timeout: TimeInterval
public init(timeout: TimeInterval = 30.0) {
self.timeout = timeout
}
}
위 코드에서 접근 제어는 다음과 같이 활용됩니다:
- NetworkManager 클래스와 NetworkConfiguration 구조체는 public으로 선언되어 외부 모듈에서 접근할 수 있습니다.
- fetchData(from:completion:) 메서드는 public으로 선언되어 라이브러리 사용자가 호출할 수 있는 공개 API입니다.
- createRequest(url:)와 performRequest(_:completion:) 메서드는 private으로 선언되어 내부 구현 상세를 숨기고, 클래스 내부에서만 사용됩니다.
- 마찬가지로, configuration과 session 프로퍼티도 private으로 선언되어 내부 상태를 보호합니다.
이런 방식으로 접근 제어를 사용하면, 라이브러리 사용자는 명확하고 간결한 API만을 볼 수 있고, 복잡한 내부 구현 세부 사항은 숨겨집니다. 또한, 내부 구현이 변경되더라도 공개 API가 동일하게 유지된다면 호환성이 깨지지 않습니다.
게터와 세터의 접근 제어
Swift에서는 프로퍼티의 게터(getter)와 세터(setter)에 대해 서로 다른 접근 수준을 지정할 수 있습니다. 이는 읽기는 더 넓은 범위에서 허용하고, 쓰기는 제한하고 싶을 때 유용합니다.
struct TrackedString {
private(set) var numberOfEdits = 0
var value: String = "" {
didSet {
numberOfEdits += 1
}
}
}
위 코드에서 numberOfEdits 프로퍼티는 기본적으로 internal 접근 레벨을 가지므로 같은 모듈 내에서 읽을 수 있지만, private(set)으로 인해 세터는 private이므로 구조체 내부에서만 수정할 수 있습니다.
이를 사용하면 다음과 같이 동작합니다:
var tracked = TrackedString()
tracked.value = "Hello" // numberOfEdits는 1이 됨
print(tracked.numberOfEdits) // 1 출력 - 읽기는 가능
// tracked.numberOfEdits = 0 // 오류 - 세터는 private
이 코드에서 tracked.value를 변경하면 내부적으로 numberOfEdits가 증가하지만, 외부에서 직접 numberOfEdits를 변경하려고 하면 컴파일 오류가 발생합니다. 이는 값의 변경 추적과 같은 기능을 구현할 때 유용합니다.
단위 테스트와 접근 제어
Swift에서는 내부 구현 세부 사항을 테스트할 수 있도록 특별한 메커니즘을 제공합니다. @testable 속성을 사용하면 단위 테스트에서 internal 멤버에 접근할 수 있습니다.
// MyModule.swift
internal func calculateSomething() -> Int {
// 복잡한 계산...
return 42
}
// MyModuleTests.swift
import XCTest
@testable import MyModule
class MyModuleTests: XCTestCase {
func testCalculation() {
// @testable 덕분에 internal 함수에 접근 가능
XCTAssertEqual(calculateSomething(), 42)
}
}
이 예제에서 calculateSomething() 함수는 internal이므로 일반적으로는 모듈 외부에서 접근할 수 없습니다. 그러나 @testable import MyModule을 사용하면 테스트 코드에서 이 함수에 접근할 수 있습니다. 이는 내부 구현 세부 사항을 노출하지 않으면서도 철저히 테스트할 수 있게 해줍니다.
결론
Swift의 접근 제어는 코드 구성 요소의 가시성을 세밀하게 조절할 수 있는 강력한 도구입니다. 적절한 접근 레벨을 사용하면 API를 명확하게 정의하고, 구현 세부 사항을 숨기며, 코드 변경으로 인한 부작용을 최소화할 수 있습니다.
접근 제어를 효과적으로 활용하기 위한 몇 가지 핵심 원칙을 기억하세요:
- 가능한 한 가장 제한적인 접근 레벨을 선택하여 실수로 인한 외부 의존성을 방지하세요.
- 공개 API는 명시적으로 public으로 표시하여 의도를 명확히 하세요.
- 내부 구현 세부 사항은 private 또는 fileprivate으로 숨겨 캡슐화를 강화하세요.
- 모듈 내부에서만 사용되는 코드는 기본 접근 레벨인 internal을 사용하세요.
- 게터와 세터에 대한 다른 접근 레벨을 활용하여 필요한 경우 읽기 전용 프로퍼티를 만드세요.
이러한 원칙을 따르면 더 유지보수하기 쉽고, 이해하기 쉬우며, 오류에 강한 코드를 작성할 수 있습니다.