-
static 키워드 정복하기Programming🧑💻 2025. 3. 25. 21:08
Swift에서 static 키워드는
"프로퍼티를 인스턴스가 아닌 타입 레벨로 올림으로써 타입 내에서 공유하는 속성,기능을 만들 수 있게 해 준다. "라고 이해하고 있었으며 주로 싱글톤 패턴이나 타입 메서드를 구현할 때 사용했습니다.
따라서 static 키워드를 "타입 레벨의 동작을 지원하기 위해 만들어진 녀석이구나" 정도로 이해하고 있었습니다.
근데 thread-safe 코드 작성에 대해 살펴보기에 앞서 thread-unsafe 한 경우가 무엇이 있는지에 대해 보고 있던 와중에
"정적 지역 변수는 함수 내에 있더라도 data 영역에 저장이 되며 함수의 반환 이후에도 함수의 상태를 기억한다.
즉 여러 쓰레드에서 접근이 가능한 공유 자원으로 취급되기 때문에 주의가 필요하다." 라는 구절을 읽었습니다.
그리고 예제 코드에 static 키워드가 붙어 있는 것을 보고 다음과 같은 의문들이 생겼습니다.
1. 함수 내의 지역 변수는 함수가 반환되어 스텍 프레임이 날라갈때 함께 없어지지 않나?
2. static이 타입 내에서 프로퍼티나 메서드를 정의할 때 쓰는 키워드인 줄 알았는데 저게 왜 함수 안에 변수를 선언할 때 붙나?
아무래도 C, Cpp, Java, Swift 등 다양한 언어에서 이 키워드가 사용되지만, 그 의미가 사용되는 맥락에 따라 미묘하게 달라져서 오는 불편함이라고 생각하여 좀 더 본질적인 의미에 대해서 생각해보았습니다.
static 키워드는 "특정한 맥락에 엮이지 않고 현재 선언한 곳에 고정된다"라는 느낌으로 받아들이면 될 것 같습니다.
1. static 변수는 함수 호출에 관계 없이 선언 시점 부터 메모리 내에서 고정된 위치를 유지합니다.
2. static 멤버는 개별 인스턴스와 관계 없이 선언된 타입 자체에 속합니다.
C/C++에서의 static 키워드
C와 C++에서 static 키워드는 세 가지 주요 맥락에서 사용됩니다.
1. 정적 지역 변수(Static Local Variables)
함수 내부에 선언된 지역 변수에 static을 붙이면, 이 변수는 함수가 종료되어도 메모리에 남아 있으며 그 값을 유지합니다.
void countCalls() { static int count = 0; // 초기화는 최초 호출 시 한 번만 수행됨 count++; printf("이 함수는 %d번 호출되었습니다.\n", count); }
위 함수를 여러 번 호출하면, count 값은 계속 증가합니다. 일반 지역 변수와 달리, static 지역 변수는
- 함수 호출이 끝나도 메모리에서 해제되지 않음
- 다음 함수 호출 시 이전 값을 기억함
- 초기화는 프로그램 실행 중 단 한 번만 일어남
2. 파일 범위 제한(File Scope Restriction)
전역 변수나 함수 앞에 static을 붙이면, 해당 식별자는 그 파일 내에서만 접근할 수 있게 됩니다.
// helper.c 파일 static int internalCounter = 0; // 이 파일 내에서만 접근 가능 static void helperFunction() { // 이 파일 내에서만 호출 가능 internalCounter++; }
이는 모듈화와 정보 은닉을 위한 중요한 메커니즘으로, 다른 파일에서 이 식별자를 실수로 접근하거나 수정하는 것을 방지합니다.
3. 클래스 멤버(C++)
C++에서 클래스의 멤버에 static을 붙이면, 해당 멤버는 클래스의 모든 인스턴스가 공유합니다.
class User { private: int id; std::string name; public: static int userCount; // 모든 User 객체가 공유하는 변수 User(std::string userName) { name = userName; id = ++userCount; // 새 사용자가 생성될 때마다 카운트 증가 } static void printTotalUsers() { // 정적 메서드 std::cout << "총 사용자 수: " << userCount << std::endl; } }; // 클래스 외부에서 정적 멤버 변수 초기화 int User::userCount = 0;
정적 클래스 멤버의 특징
- 모든 객체 인스턴스에 걸쳐 하나의 복사본만 존재
- 객체 생성 전에도 접근 가능(클래스 이름으로 직접 접근)
- 정적 멤버 함수는 정적 멤버 변수만 직접 접근 가능
Swift에서의 static 키워드
1. 타입 프로퍼티(Type Properties)
struct Temperature { static let boilingPointCelsius = 100.0 // 타입 프로퍼티 static let freezingPointCelsius = 0.0 // 타입 프로퍼티 var currentTemperature: Double func isBoiling() -> Bool { return currentTemperature >= Temperature.boilingPointCelsius } } // 사용 방법 print("물의 끓는점: \(Temperature.boilingPointCelsius)°C")
타입 프로퍼티는 구조체나 클래스의 인스턴스가 아닌 타입 자체에 속하며, 모든 인스턴스가 공유합니다.
2. 타입 메서드(Type Methods)
struct MathUtils { static func square(_ number: Int) -> Int { return number * number } static func isEven(_ number: Int) -> Bool { return number % 2 == 0 } } // 사용 방법 let squared = MathUtils.square(5) // 25 let isEven = MathUtils.isEven(4) // true
타입 메서드도 타입 자체에 속하며, 인스턴스 없이 타입 이름으로 직접 호출합니다.
3. static vs class 키워드
Swift의 클래스에서는 static 외에도 class 키워드를 사용할 수 있습니다:
class Animal { static let species = "동물" // 오버라이드 불가능 class var sound: String { // 오버라이드 가능 return "소리" } } class Dog: Animal { // static let species = "개" // 컴파일 오류! override class var sound: String { // 오버라이드 가능 return "멍멍" } }
핵심 차이점:
- static 멤버는 하위 클래스에서 오버라이드할 수 없음
- class 멤버는 하위 클래스에서 오버라이드할 수 있음
- class 키워드는 클래스에서만 사용 가능(구조체, 열거형에서는 사용 불가)
실제 활용 사례
1. 싱글톤 패턴 구현
class DatabaseManager { // 싱글톤 인스턴스 static let shared = DatabaseManager() // 외부에서 인스턴스화 방지 private init() { // 초기화 로직 } func query(_ sql: String) -> [Any] { // 데이터베이스 쿼리 실행 로직 return [] } } // 사용 방법 let users = DatabaseManager.shared.query("SELECT * FROM users")
싱글톤 패턴은 클래스의 인스턴스가 프로그램 내에서 단 하나만 존재하도록 보장합니다. static 키워드는 이 패턴의 핵심 요소입니다.
2. 캐싱 및 메모이제이션
int fibonacci(int n) { static int cache[100] = {0}; // 계산 결과를 저장할 정적 배열 if (n <= 1) return n; // 이미 계산된 값이면 재계산하지 않고 바로 반환 if (cache[n] != 0) return cache[n]; // 계산하고 캐시에 저장 cache[n] = fibonacci(n-1) + fibonacci(n-2); return cache[n]; }
정적 지역 변수를 사용하여 이전 계산 결과를 캐시함으로써 성능을 크게 향상시킬 수 있습니다.
3. 유틸리티 클래스
struct StringUtils { // 인스턴스화 방지를 위한 private 초기화 private init() {} static func reverse(_ string: String) -> String { return String(string.reversed()) } static func isPalindrome(_ string: String) -> Bool { let lowercased = string.lowercased().filter { $0.isLetter } return lowercased == String(lowercased.reversed()) } } // 사용 방법 let reversed = StringUtils.reverse("Hello") // "olleH" let isPalindrome = StringUtils.isPalindrome("radar") // true
유틸리티 클래스는 인스턴스화할 필요 없이 정적 메서드만 제공하는 클래스입니다.
4. 상수 및 설정 관리
struct AppConfig { static let apiBaseURL = "https://api.example.com" static let maxRetryCount = 3 static let timeoutInterval = 30.0 static var isDebugMode: Bool { #if DEBUG return true #else return false #endif } } // 사용 방법 let url = URL(string: AppConfig.apiBaseURL + "/users")!
애플리케이션 전체에서 사용되는 상수와 설정값을 static 프로퍼티로 관리하면 중앙 집중화된 설정 관리가 가능합니다.
주의할 점
static 키워드는 강력하지만, 적절히 사용하지 않으면 코드 품질에 부정적인 영향을 줄 수 있습니다.
- 전역 상태의 위험성: static 변수는 전역 상태를 만들어내며, 이는 테스트와 디버깅을 어렵게 만들 수 있습니다.
- 스레드 안전성: 여러 스레드가 static 변수에 동시에 접근할 경우, 데이터 경쟁 조건(race condition)이 발생할 수 있습니다.
- 의존성 주입 어려움: static 멤버에 대한 의존성은 모의 객체(mock)로 대체하기 어려워 테스트를 복잡하게 만듭니다.
- 메모리 사용: static 변수는 프로그램이 종료될 때까지 메모리를 점유합니다.
따라서 static은 그 용도가 명확할 때만 사용하는 것이 좋습니다.
'Programming🧑💻' 카테고리의 다른 글
타입은 왜 중요할까? (0) 2025.01.29 web application은 어떻게 작동하는가? (0) 2022.10.28 HTTP messages (0) 2022.10.13