iOS/Swift

[Swift] Value type & Reference type

kimsangjunzzang 2025. 6. 10. 05:25

안녕하세요, 루피입니다.

 

오늘은 값과 참조에 관해 정리해보는 시간을 가져보려합니다. 가장 기본적인 내용이지만, 가장 중요한 개념이기에 이번 기회에 제대로 짚고 넘어가면 좋을거 같습니다.

 

바로 시작합니다.


값 타입 vs 참조 타입

값 타입은 데이터를 전달할 때 값을 복사 해서 전달합니다.

예를 들어, 구조체 인스턴스를 다른 변수에 할당하면, 전혀 별개의 인스턴스가 생성되어 원본과는 완전히 독립적으로 작동하게 됩니다.

참조 타입은 값을 전달하는 것이 아니라 그 값이 저장되어 있는 메모리 주소 를 전달합니다. 그래서 클래스 인스턴스를 다른 변수에 할당하더라도 실제로는 같은 객체를 가리키게 됩니다. 즉, 둘 중 하나에서 값을 바꾸면 나머지에도 영향을 끼치게 되는 것이죠.


Swift 의 타입

Swift에서 모든 타입은 크게 두 가지 범주로 나뉩니다. 바로 값 타입과 참조 타입인데요.

값 타입의 경우 구조체, 열거형, 튜플, 기본 타입들 (Int, String, Bool, Double 등)이 있습니다.

참조 타입의 경우 클래스, 클로저가 있습니다.


메모리 관점에서의 차이점

두 타입의 가장 큰 차이는 메모리에 저장되는 방식에 차이가 있다는 것입니다. 코드를 통해 확인해보겠습니다.

1. 값 타입의 메모리 구조

struct User {
    var name: String
    var age: Int
}

var user1 = User(name: "루피", age: 25)
var user2 = user1  // 값 복사 발생
user2.name = "나루토"

print(user1.name)  // "루피" (원본 유지)
print(user2.name)  // "나루토" (복사본 변경)

값 타입Stack 메모리에 실제 데이터가 저장되며, 할당 시 완전한 복사가 일어납니다.

2. 참조 타입의 메모리 구조

Struct를 Class로 변경하고 다른 내용은 다 동일하게 구성하겠습니다.

class UserClass {
    var name: String
    var age: Int

    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
}

var user1 = UserClass(name: "루피", age: 25)
var user2 = user1  // 참조 복사 발생
user2.name = "나루토"

print(user1.name)  // "나루토" (원본도 변경됨!)
print(user2.name)  // "나루토"

참조 타입Heap 메모리에 실제 데이터가 저장되고, Stack에는 Heap의 주소만 저장됩니다.


실제 동작 방식의 차이

위에서는 간단한 코드를 통해 확인해봤는데, 이제는 실제로 활용될법한 예시로 살펴보겠습니다.

1. 값 타입의 특징 (Call by Value)

struct Post {
    var title: String
    var text: String
    var numberOfLikes = 0

    init(title: String, text: String) {
        self.title = title
        self.text = text
    }
}

// 값 타입에서는 함수 매개변수가 기본적으로 상수입니다.
func like(_ post: Post) -> Post {
    var post = post  // 복사본 생성(상수는 변경이 불가능하기에 변수 복사본을 만듭니다.)
    post.numberOfLikes += 1
    return post
}

var myPost = Post(title: "Swift 공부", text: "값 타입과 참조 타입")
myPost = like(myPost)  // 반환값을 다시 할당해야 함

2. 참조 타입의 특징 (Call by Reference)

class PostClass {
    var title: String
    var text: String
    var numberOfLikes = 0

    init(title: String, text: String) {
        self.title = title
        self.text = text
    }
}

func like(_ post: PostClass) {
    post.numberOfLikes += 1  // 원본 객체 직접 수정
}

let myPost = PostClass(title: "Swift 공부", text: "값 타입과 참조 타입")
like(myPost)  // 원본이 바로 변경됨

3. 값 타입에서 Call by Reference 구현

Swift는 값 타입에서도 Call by Reference를 구현할 수 있는 inout 키워드를 제공합니다.

struct Counter {
    var count = 0
}

// inout을 사용한 Call by Reference
func increment(_ counter: inout Counter) {
    counter.count += 1  // 원본 직접 수정
}

// 일반적인 Call by Value
func incremented(_ counter: Counter) -> Counter {
    var counter = counter  // 복사본 생성
    counter.count += 1
    return counter
}

var myCounter = Counter()

// Call by Reference 방식
increment(&myCounter)  // & 기호로 참조 전달
print(myCounter.count)  // 1

// Call by Value 방식
myCounter = incremented(myCounter)
print(myCounter.count)  // 2

하지만, 이러한 방식은 선호되지 않습니다. 왜냐하면 그럴거면 그냥 Class를 사용하는게 더 좋다고 보기 때문입니다. 복잡하게 굳이 만들 필요가 없다는 것이죠.


let 키워드의 다른 동작

1. 값 타입에서의 let

struct Point {
    var x: Int
    var y: Int
}

let point = Point(x: 10, y: 20)
// point.x = 30  // 컴파일 오류! 값 자체가 상수

값 타입에서 let으로 선언하면 전체 데이터가 불변이 됩니다. 프로퍼티가 var로 선언되어 있어도, 인스턴스 자체가 상수이므로 어떤 프로퍼티도 변경할 수 없습니다.

2. 참조 타입에서의 let

class PointClass {
    var x: Int
    var y: Int

    init(x: Int, y: Int) {
        self.x = x
        self.y = y
    }
}

let point = PointClass(x: 10, y: 20)
point.x = 30  // 정상 작동! 참조는 고정, 내용은 변경 가능
// point = PointClass(x: 0, y: 0)  // 컴파일 오류! 참조 변경 불가

여기서 한 가지 의문이 생길 수 있습니다. 왜 참조 타입에서는 let으로 인스턴스를 생성 했는데 내용이 변경 가능할까요? 한번 잠깐 스스로에게 설명해보면 좋을 거 같습니다.

바로 참조 타입에서는 let이 참조(주소값)만 고정시키고, 참조가 기리키는 객체의 내용은 여전히 변경 가능하기 때문입니다.

struct Point {
    var x : Int
    var y : Int
}

let point = Point(x: 1, y: 2)
print(point) // Point(x: 1, y: 2)

값 타입은 Stack 메모리에 실제 데이터가 직접 저장되므로, let으로 선언하면 전체 데이터 블록이 불변이됩니다.

class Point {
    var x : Int
    var y : Int

    init(x: Int, y: Int) {
        self.x = x
        self.y = y
    }
}

let point = Point(x: 1, y: 2)
print(point) // __lldb_expr_55.Point

하지만 참조 타입은 Stack에 Heap 메모리의 주소값만 저장하므로, let은 주소값만 고정시키고 실제 객체의 내용은 변경할 수 있습니다.

 

즉 Stack 메모리에는 point : [주소 : 0x1234] ← 이 주소값만 상수로 저장되며

Heap 메모리에 저장된 0x1234 : [x: 1, y: 2] ← 이 내용은 변경이 가능하다는 것입니다.

 

그렇기에 참조 변경은 불가능합니다.

point = Point(x : 0, y: 0) // 컴파일 오류가 발생하게 됩니다.

언제 어떤 타입을 사용할까?

언제 어떤 타입을 사용할지 선택하기 위해서는 일단 Call by Value와 Call by Reference의 장.단점을 알아야겠죠?

1. Call by Value의 성능 특성

장점

  • Stack 메모리 사용으로 빠른 할당/해제가 가능합니다.
  • 메모리 단편화가 없습니다.
  • ARC 오버헤드가 없습니다.

단점

  • 큰 구조체의 경우 복사 cost가 증가합니다.

2. Call by Reference의 성능 특성

장점

  • 큰 객체도 참조만 복사하므로 효율적입니다.
  • 메모리 공유로 사용량 절약이 가능합니다.

단점

  • Heap 메모리 할당/ 해제 cost가 발생합니다.
  • ARC로 인한 오버헤드가 발생할 수 있습니다.

1. 값 타입을 사용해야 하는 경우

  • 데이터 모델링 : 좌표, 크기, 색상 등 단순한 데이터
  • 불변성이 중요한 경우 : 원본 데이터 보호가 필요할 때
  • 스레드 안전성 : 멀티스레딩 환경에서 안전한 코드
  • 성능이 중요한 경우 : Stack 메모리 사용으로 빠른 접근
struct Color {
    let red: Double
    let green: Double
    let blue: Double
}

struct Point {
    var x: Double
    var y: Double
}

struct UserProfile {
    let id: String
    var name: String
    var email: String
}

2. 참조 타입을 사용해야 하는 경우

  • 상태 공유 : 여러 곳에서 같은 객체를 참조해야 할 때
  • 상속이 필요한 경우 : 클래스 계층 구조가 필요할 때
  • 큰 데이터 구조: 복사 비용이 큰 경우
class DatabaseManager {
    static let shared = DatabaseManager()
    private var cache: [String: Any] = [:]

    func store(key: String, value: Any) {
        cache[key] = value
    }
}

class ViewController: UIViewController {
    // UIKit은 클래스 기반
}

실제 활용 사례

자 이제 기본적인 개념은 충분히 이해했다고 생각 됩니다. 그러면 실제 어떻게 활용되는지 살펴 보려합니다.

1. Copy-on-Write 최적화

Swift의 Array, Dictionary 등은 값 타입이지만, 내부적으로 Copy-on-Write를 사용합니다.

var array1 = [1, 2, 3, 4, 5]
var array2 = array1  // 아직 복사 안 됨 (최적화)

array2.append(6)  // 이 시점에서 실제 복사 발생

3. 함수형 vs 객체지향 스타일

// 함수형 스타일 (Call by Value)
struct FunctionalCalculator {
    func add(_ a: Int, _ b: Int) -> Int {
        return a + b
    }
     func multiply(_ a: Int, _ b: Int) -> Int {
        return a * b 
    }
}

// 객체지향 스타일 (Call by Reference)
class StatefulCalculator {
    private var result: Int = 0

    func add(_ value: Int) {
        result += value
    }

    func getResult() -> Int {
        return result
    }
}

let calculator = FunctionalCalculator()
let res1 = calculator.add(1,2) // 3
let res2 = calculator.multiply(2,3) // 6

let calculator2 = StatefulCalculator()
calculator2.add(5) 
calculator2.getResult() // 5
calculator2.add(5) 
calculator2.getResult() // 10

오늘도 화이팅입니다!