[Swift] 열거형과 구조체, 클래스 이해하기

승민·2025년 4월 16일

Swift

목록 보기
6/10
post-thumbnail
  • Swift에서 열거형인 enum에 대해 알아보는 시간이에요.
  • 구조체와 클래스의 차이를 이론과 실습을 통해 차이점을 확인해볼 예정이에요.

열거형(enum)

열거형은 관련있는 데이터들이 멤버로 구성되어 있는 자료형 객체를 의미해요.
다음과 같은 상황에서 주로 사용해요.

  • 원치 않는 값이 잘못 입력되지 않도록 할 때
  • 입력 받을 값이 한정되어 있을 때
  • 특정 값 중 하나만 선택하게 할 때

열거형 활용 예시

다음과 같은 분류를 원할 때 사용할 수 있어요.

  • 색깔
    • 빨강, 녹색, 파랑
  • 성별
    • 남, 여
  • 방위
    • 동, 서, 남, 북

열거형 정의

열거형은 다음과 같은 방식으로 정의할 수 있어요. 코드를 통해서 확인해볼게요.

enum Compass {
    case North
    case South
    case East
    case West
}
//var x : Compass // Compass형 인스턴스 x
print(Compass.North) // North
var x = Compass.West
print(type(of:x)) // Compass
x = .East
print(x) // East

열거형 활용

열거형에 메서드를 정의해서 활용하는 것도 가능해요.

enum Week {
    case Mon,Tue,Wed,Thur,Fri,Sat,Sun
        func printWeek() { //메서드도 가능
        switch self {
        case .Mon, .Tue, .Wed, .Thur, .Fri:
            print("주중")
        case .Sat, .Sun:
            print("주말")
        }
    }
}
Week.Sun.printWeek()

열거형의 원시값(rawValue)

열거형의 rawValue를 지정할 수 있어요.
값이 없는 경우에는 초기값을 0으로 가져요.

중간에 다른 값으로 지정해준 경우에는 그 숫자의 다음 값을 가져요.

enum Color: Int { //원시값(rawValue) 지정
    case red
    case green = 2
    case blue
}

print(Color.red) //red
print(Color.blue)

print(Color.red.rawValue) //0
print(Color.blue.rawValue)

위 경우에는 중간 green의 값을 2로 지정해줬기 때문에 다음 값인 blue3으로 출력되는 것을 확인할 수 있어요.

String 형 값을 갖는 열거형의 rawValue

열거형의 자료형이 String으로 지정되어 있고, 원시값(rawValue)을 지정해주지 않은 경우 해당 case의 값이 이름으로 지정돼요.

enum Week: String {
    case Monday = "월"
    case Tuesday = "화"
    case Wednesday = "수"
    case Thursday = "목"
    case Friday = "금"
    case Saturday //값이 지정되지 않으면 case 이름이 할당됨
    case Sunday // = "Sunday"
}
print(Week.Monday) //Monday
print(Week.Monday.rawValue) //월
print(Week.Sunday)
print(Week.Sunday.rawValue)

연관값(associated value)을 갖는 enum

연관값(associated value)은 Swift 열거형(enum)의 각 케이스에 추가적인 데이터를 연결하는 기능이에요.
각 케이스는 Int, String, 튜플 등 서로 다른 타입의 값을 가질 수 있어요.

enum Date {
    case intDate(Int,Int,Int) //(int,Int,Int)형 연관값을 갖는 intDate
    case stringDate(String) //String형 연관값을 값는 stringDate
}
var todayDate = Date.intDate(2025,4,30)
todayDate = Date.stringDate("2025년 5월 20일")

switch todayDate {
    case .intDate(let year, let month, let day):
        print("\(year)\(month)\(day)일")
    case .stringDate(let date):
        print(date)
}

위 예제에서는 5번 줄에서 todayDate 의 값을 변경했기 때문에 2025년 5월 20일 이 출력되었어요.
만약, 해당 줄이 없었으면 2025년 4월 30일 이 출력돼요.

nonesome

  • none : Optionalnil인 상태로 값이 없음을 나타내요.
  • some : Optional이 실제 값을 가지는 상태로 연관값(associated value)으로 실제 데이터(Wrapped 타입)를 포함하는 것을 의미해요.
  • nonesome이 가지는 특징은 다음과 같아요.
    • none 은 데이터 부재, some 은 데이터 존재를 명시함
    • switch 나 패턴 매칭으로 nonesome 을 분기 처리
    • Optional 의 연산(map, flatMap, ?? 등)은 nonesome 기반

구조체(struct)

Swift에서 구조체(Structure)는 관련된 데이터와 기능을 하나의 단위로 묶는 타입이에요.
다음과 같은 특징을 가져요.

  • 구조체는 값 타입(Value Type)으로, 복사될 때 값이 복사돼요.
  • 프로퍼티(속성)와 메서드를 가질 수 있어요.
  • 초기화 메서드(Initializer)를 정의할 수 있어요.
  • 프로토콜을 채택할 수 있어요.

Memberwise Initializer

Swift에서 구조체는 기본적으로 Memberwise Initializer를 자동으로 제공해요.
Memberwise Initializer란 모든 구조체에 자동으로 생성된 멤버별 초기화가 되며, 이를 사용하여 새 구조체 인스턴스의 멤버 속성을 초기화할 수 있는 것을 의미해요.
그러나 직접 이니셜라이저를 정의하면 자동으로 생성되는 Memberwise Initializer는 사용할 수 없어요.

이해를 돕기 위해 코드를 통해서 다양하게 확인해볼게요.

struct Resolution {
    var width : Int
    var height : Int
}
let myComputer = Resolution(width:1920, height:1080)
print(myComputer.width)

위 코드를 보면 따로 init을 생성하지 않았지만, 코드가 정상적으로 실행된 것을 확인할 수 있어요.
이처럼 구조체의 경우 Memberwise Initializer를 통해 따로 Initializer를 생성하지 않아도 바로 사용할 수 있다는 특징이 있어요.

클래스 내에 구조체

구조체를 활용하면 다음과 같이 클래스 내에서 별도의 init 없이 정의할 수 있어요.

struct Resolution {
    var width = 1024
    var height = 768
}
class VideoMode {
    var resolution = Resolution()
    var frameRate = 0.0
}
let myVideo = VideoMode()
print(myVideo.resolution.width)

Resolution 구조체를 정의해서 VideoModeresolution의 값을 구조체로 초기화했어요.
이런식으로 초기화를 진행하면 문제없이 코드를 작성할 수 있다는 특징이 있어요.

구조체 vs. 클래스

이렇게만 본다면 구조체와 클래스는 공통점이 굉장히 많다는 것을 알 수 있어요.
두 기능의 쓰임이 비슷하다보니 사용 시에 헷갈릴 수 있어요.

각각 사용해야 하는 시점이나 기능이 어떤 점에서 다른지 알아볼게요.

공통점

  • Swift에서 클래스와 구조체는 모두 데이터와 기능을 하나의 단위로 묶는 역할을 해요.
  • 두 타입 모두 프로퍼티를 사용해 값을 저장할 수 있고, 메서드를 통해 기능을 제공해요.
    • 확장(Extension)을 통해 기능을 추가할 수 있고, 프로토콜을 채택해 특정 기능이나 인터페이스를 구현할 수 있어요.
  • 초기화 구문(Initializer)을 정의해 인스턴스 생성 시 초기값을 설정할 수 있어요.
    • 서브스크립트(Subscript)를 정의해 특정 값에 접근하는 문법을 제공할 수 있어요.
  • 클래스와 구조체 모두 타입 메서드와 타입 프로퍼티를 가질 수 있어요.

차이점

  • 값 타입 vs 참조 타입
    • 가장 중요한 차이점은 구조체는 값 타입(Value Type)인 반면, 클래스는 참조 타입(Reference Type)이에요.
    • 구조체 인스턴스를 변수나 상수에 할당하거나 함수에 전달할 때는 값이 복사되지만, 클래스 인스턴스는 참조가 전달돼요.
  • 따라서 여러 변수가 하나의 클래스 인스턴스를 참조할 수 있고, 한 변수를 통해 변경된 내용이 다른 변수에도 영향을 미쳐요.
  • 상속
    • 클래스는 다른 클래스를 상속할 수 있지만, 구조체는 상속을 지원하지 않아요.
    • 클래스는 부모 클래스의 속성과 메서드를 상속받고 재정의(override)할 수 있어요.
  • 소멸자(Deinitializer)
    • 클래스는 deinit이라는 소멸자를 가질 수 있어 인스턴스가 메모리에서 해제되기 전에 정리 작업을 수행할 수 있어요.
    • 구조체는 소멸자를 가질 수 없어요.
  • 참조 카운팅
    • 클래스 인스턴스는 자동 참조 카운팅(ARC)을 통해 메모리 관리가 이루어져요.
    • 인스턴스를 참조하는 변수나 상수가 없으면 메모리에서 해제돼요.
    • 구조체는 값 타입이므로 참조 카운팅을 사용하지 않아요.
  • Memberwise Initializer
    • 구조체는 모든 프로퍼티를 초기화하는 Memberwise Initializer를 자동으로 제공하지만, 클래스는 그렇지 않아요.
    • 클래스는 모든 저장 프로퍼티의 초기값을 설정하는 초기화 구문을 직접 작성해야 해요.
  • Identity Operators
    • 클래스 인스턴스는 참조 타입이므로 ===!== 연산자를 사용해 두 변수가 같은 인스턴스를 참조하는지 확인할 수 있어요.
    • 구조체는 값 타입이므로 이러한 연산자를 사용할 수 없어요.
  • 변경 가능성
    • 상수(let)로 선언된 구조체 인스턴스는 그 프로퍼티도 변경할 수 없지만, 상수로 선언된 클래스 인스턴스의 가변 프로퍼티는 변경할 수 있어요.
  • 성능
    • 일반적으로 구조체는 스택 메모리에 할당되고 클래스는 힙 메모리에 할당되므로, 구조체가 더 가볍고 빠를 수 있어요.

클래스와 구조체의 정의 방식

  • 구조체
    • 구조체의 값 타입은 복사할 때 새로운 데이터가 하나 더 생기는 방식이에요.
    • 값을 복사하기 때문에 복제된 내용을 수정해도 원본에 영향을 주지 않아요.
    • 코드를 통해서 확인해볼게요.
struct Human {
    var age : Int = 1
}
var kim = Human()
var lee = kim //값 타입
print(kim.age, lee.age)
lee.age = 20
print(kim.age, lee.age)
kim.age = 30
print(kim.age, lee.age)

  • 클래스
    • 클래스의 참조 타입은 복사할 때 주소를 복사해서 한 데이터의 reference가 2개 생기는 방식이에요.
    • 값을 복사하는 경우 값이 아닌 참조를 가져오기 때문에 복사된 내용을 수정하면 원본도 같이 바뀌어요.
    • 이해를 돕기 위해 코드를 통해서 확인해볼게요.
class Human {
    var age : Int = 1
}
var kim = Human()
var lee = kim //참조 타입
print(kim.age, lee.age)
lee.age = 20
print(kim.age, lee.age)
kim.age = 30
print(kim.age, lee.age)

구조체와 클래스가 쓰이는 시점

  • 클래스는 참조타입, 구조체는 값 타입
    • 구조체는 간단한 데이터 값들을 한데 묶어서 사용하는 경우
    • 전체 덩어리 크기가 작은 경우, 복사를 통해 전달해도 좋은 경우 구조체
    • 멀티 쓰레드 환경이라면 구조체가 더 안전
    • 구조체는 상속할 필요가 없는 경우
      • 너비, 높이를 표현하는 기하학적 모양을 처리할 경우
      • 시작값, 증분, 길이 등으로 순열을 표현할 경우
      • 3차원 좌표 시스템의 각 좌표

extension

  • 클래스, 구조체, 열거형, protocol에 새로운 기능을 추가
  • extension은 기존 클래스에 메서드, 생성자(initializer), 계산 프로퍼티 등의
    기능을 추가하기 위하여 사용
  • Swift built-in 클래스와 iOS 프레임워크에 내장된 클래스에 기능을 추가할 때
    extension을 이용하면 매우 효과적임
  • 클래스(구조체, 열거형, protocol)는 다음과 같은 형태로 extension

Swift에서 타입과 기능별 사용 시기

Class (클래스)

클래스는 다음과 같은 상황에서 주로 사용돼요.

  • 참조 타입이 필요할 때 (여러 곳에서 같은 인스턴스를 공유해야 할 때)
  • 상속이 필요한 경우
  • 인스턴스의 수명 주기 동안 상태를 관리해야 할 때
  • 소멸자(deinit)가 필요한 경우
  • 복잡한 데이터 모델이나 비즈니스 로직을 구현할 때
  • UIKit, AppKitObjective-C 기반 프레임워크와 상호작용할 때 (대부분 클래스 기반)

Struct (구조체)

구조체는 다음 상황에서 적합해요.

  • 값 타입이 필요할 때 (데이터 복사가 필요할 때)
  • 간단한 데이터 구조를 표현할 때
  • 상속이 필요 없을 때
  • 다른 타입에게 복사될 때 독립적인 사본이 필요할 때
  • 멀티스레딩 환경에서 데이터 안전성이 중요할 때
  • 작고 가벼운 값을 표현할 때

Enum (열거형)

열거형은 다음 경우에 사용돼요.

  • 제한된 선택지 중에서 하나를 표현할 때
  • 서로 배타적인 케이스들을 모델링할 때
  • 연관 값이나 원시값을 통해 추가 정보를 담을 필요가 있을 때
  • 상태 관리나 타입 안전성을 강화하고 싶을 때
  • 스위치 구문으로 모든 케이스를 처리해야 할 때

Protocol (프로토콜)

프로토콜은 다음과 같은 경우에 사용돼요.

  • 서로 다른 타입들이 공통된 기능을 구현해야 할 때
  • 타입 간의 계약을 정의할 때
  • 의존성 주입과 같은 디자인 패턴을 구현할 때
  • 클래스 상속 없이 코드 재사용성을 높이고 싶을 때
  • 다형성을 활용하고 싶을 때
  • 인터페이스와 구현을 분리하고 싶을 때

Extension (확장)

확장은 다음과 같은 경우에 사용돼요.

  • 기존 타입에 새로운 기능을 추가할 때
  • 프로토콜 구현을 추가할 때
  • 코드를 논리적으로 그룹화할 때
  • 본래 정의에 접근할 수 없는 타입에 기능을 추가할 때
  • 초기화 구문을 추가할 때
  • 제네릭 제약조건에 따라 특정 타입에만 적용되는 기능을 추가할 때

정리

  • 열거형인 enum에 대해 알아서 알아봤어요.
  • 구조체와 클래스의 공통점과 차이점을 정리하고 어떤 특징이 있는지 알아봤어요.
  • Swift에서 타입과 기능별 사용 시기를 정리해서 어떤 시기에 어떤 것이 사용되는 지 확인할 수 있었어요.

출처 : Smile Han - iOS 프로그래밍 기초

0개의 댓글