[Swift] Static: 타입에 속한다는 것의 의미

팔랑이·2025년 12월 23일

iOS/Swift

목록 보기
80/81
post-thumbnail

서론

프로젝트를 진행하며 static let으로 싱글턴을 구현하고, static func으로 유틸리티 함수나 팩토리 메서드를 작성해왔다.

class NetworkManager {
    static let shared = NetworkManager()
}

struct DateFormatter {
    static func format(_ date: Date) -> String { ... }
}

단순히 "전역적으로 사용할 수 있게 해주는 키워드" 라고 생각했다.
인스턴스를 만들지 않고도 접근 가능하니까 편하다 정도로만 이해하고 넘어갔다.

그런데...

문제 상황

회원가입 API 요청 DTO를 작성하던 중, GPT가 다음과 같은 코드를 제시했다.

struct SignUpRequestDTO: Encodable {
    let name: String
    let birthDate: String
    let gender: String
    let nativeLanguages: [String]
    
    init(from entity: UserEntity) {
        self.name = entity.name
        self.birthDate = entity.birth
        self.gender = Self.toAPIFormat(entity.gender)
        self.nativeLanguages = entity.languages.map { language in
            Self.toAPIFormat(language)
        }
    }
    
    private static func toAPIFormat(_ gender: GenderEntity) -> String { ... }
    
    private static func toAPIFormat(_ language: LanguageEntity) -> String { ... }

코드를 보며 세 가지가 궁금했다.

  1. self가 아닌 Self?
  2. private static? private인데 왜 static을 쓰는 거지?
  3. 왜 일반 함수가 아니라 static 함수여야 하지?

기존에 알던 "전역 접근을 위한 키워드"라는 이해로는 설명이 안 됐다.
private이면 외부 접근이 안 되는데, 굳이 static을 붙이는 이유가 뭘까?


본론

Static의 정확한 의미

static은 단순히 전역화를 위한 키워드가 아니었다.

"인스턴스가 아닌 타입 자체에 속하게 만드는 키워드"

예시를 통해 살펴보자.

struct Student {
    // 타입에 속함 - Student 구조체가 가지는 정보
    static var schoolName = "안양외국어고등학교"
    static var totalStudents = 0
    
    static func getUniformColor() -> String {
        return "Mouse Gray"
    }
    
    static func calculateGPA(scores: [Int]) -> Double {
        guard !scores.isEmpty else { return 0.0 }
        let total = scores.reduce(0, +)
        return Double(total) / Double(scores.count)
    }
    
    static func isValidGrade(_ grade: Int) -> Bool {
        return grade >= 1 && grade <= 3
    }
    
    static func formatStudentID(_ year: Int, _ number: Int) -> String {
        return "\(year)\(String(format: "%03d", number))"
    }
    
    // 인스턴스에 속함
    var name: String
    var grade: Int
    var scores: [Int]
    
    func study() {
        print("\(name)이(가) 공부합니다")
    }
}

참고: 순수 함수 (Pure Function)

  • 같은 입력에 항상 같은 출력을 반환하고,
  • 외부 상태를 변경하지 않는 함수

사용 예시를 보면 차이가 명확해진다.

// Static 멤버 - 인스턴스 생성 없이 바로 접근
let uniform = Student.getUniformColor()  // "Mouse Gray"
let gpa = Student.calculateGPA(scores: [85, 90, 88])  // 87.67
let isValid = Student.isValidGrade(4)  // false
let studentID = Student.formatStudentID(2024, 15)  // "202400015"

// 인스턴스 멤버 - 반드시 인스턴스 생성 후 접근
var kim = Student(name: "김철수", grade: 2, scores: [85, 90, 88])
kim.study()  // "김철수이(가) 공부합니다"

Static으로 선언된 calculateGPA는 Student 타입 자체가 가진 능력이다.
특정 학생(인스턴스)과 무관하게, 점수 배열만 있으면 GPA를 계산할 수 있다.

반면 study()는 각 학생(인스턴스)의 name을 사용하기 때문에, 반드시 인스턴스가 있어야 한다.

self vs Self

이제 처음 코드에서 봤던 Self의 의미가 명확해진다.

init(from entity: UserEntity) {
    self.gender = Self.toAPIFormat(entity.gender)
    //            ↑ 대문자 Self = 타입 자체 (SignUpRequestDTO)
    
    self.gender = self.toAPIFormat(entity.gender)
    //            ↑ 소문자 self = 인스턴스 자신
}
  • self (소문자): 인스턴스 자기 자신
  • Self (대문자): 타입 자체

Self.toAPIFormat()은 "SignUpRequestDTO 타입이 제공하는 함수"를 호출한다는 의미다.

init에서 Static 함수만 호출 가능한 이유

다시 처음의 코드로 돌아가보자.
만약 toAPIFormat을 일반 함수로 선언하면 어떻게 될까?

init(from entity: UserEntity) {
    self.name = entity.name
    self.gender = self.toAPIFormat(entity.gender)
}

private func toAPIFormat(_ gender: GenderEntity) -> String {
    // 일반 함수는 self의 다른 프로퍼티를 쓸 수 있음
    // 예: print(self.name)  ← 가능
    switch gender {
    case .female: return "FEMALE"
    case .male: return "MALE"
    }
}

Swift의 초기화 규칙상, 모든 프로퍼티가 초기화되기 전에는 self를 사용할 수 없다.
일반 함수는 self다른 프로퍼티에 접근할 수 있기 때문에,
초기화가 완료되지 않은 시점에서 호출하면 안전하지 않기 때문
이다.

❗️ 반면 맨 위 코드와 같이 static 함수로 선언하면,
self의 다른 프로퍼티에 접근하지 않았다는 것이 보장되므로, 초기화 중이든 아니든 안전하게 호출할 수 있다.

유틸, 팩토리 메서드에서의 Static

평소 작성하던 유틸리티 함수들을 다시 보자.

// Static 없이
extension NSAttributedString {
    func styled(...) -> NSAttributedString { ... }

// 사용할 때마다 불필요한 임시 인스턴스 생성
let temp = NSAttributedString()
let styled = temp.styled("Hello", font: .systemFont(ofSize: 16))

// Static으로
extension NSAttributedString {
    static func styled(...) -> NSAttributedString { ... }

// 바로 사용 가능
label.attributedText = NSAttributedString.styled(
    "Hello",
    font: .pretendard(.body2),
    letterSpacingMinus2Percent: true
)

유틸 함수, 팩토리 메서드는 모두 "타입이 제공하는 기능"이지, "인스턴스가 제공하는 기능"이 아니다.

  • 특정 인스턴스와 무관한 기능
  • 입력만으로 출력을 만드는 순수 함수
  • 관련 기능을 묶는 네임스페이스 역할

단순히 전역적으로 사용하게 하기 위해서가 아니라,
인스턴스의 상태가 아예 필요 없는 타입 자체의 기능을 사용하는 것이기 때문에 static으로 선언한다.

이에 따라 인스턴스 초기화도 필요 없는 것이었다.


결론

static은 "전역 접근"을 위한 키워드가 아니라, "타입에 속한다" 는 것을 명시하는 키워드다.

  • Static let: 타입이 가진 공유 데이터 (싱글턴, 상수)
  • Static func: 타입이 가진 능력 (유틸리티, 팩토리)

참고로
Static let: 프로그램 실행 시 메모리에 할당되어 종료까지 유지
Static func: 호출 시에만 스택에 잠깐 생성되었다가 즉시 해제됨

그리고 static 함수는 인스턴스 상태와 독립적이기 때문에:
1. 순수 함수를 표현하기에 적합하고
2. 초기화 중에도 안전하게 호출할 수 있다

profile
정체되지 않는 성장

0개의 댓글