본문 바로가기
문법관련/Swift

열거형 (꼼꼼한 재은 씨의 Swift : 문법편)

by print_soo 2022. 4. 11.

열거형이란 하나의 주제로 연관된 데이터들이 멤버로 구성되어 있는 자료형 객체를 말한다.

 

열거형에서 데이터들은 열거형 객체를 정의하는 시점에 함께 정의된다. 따라서, 데이터를 함부로 삭제하거나 변경할 수 없다. 삭제하거나 변경하려면 객체를 정의하는 구문을 직접 수정해야한다. 열거형의 데이터 멤버들은 정의 개념으로 작성되는 것이므로 타입으로 사용할 수 있을 뿐만 아니라 컴파일러가 미리 인지할 수 있다.

 

컴파일러가 미리 인지할 수 있고 없고의 차이는 런타임(Run - Time)오류컴파일 오류의 차이로 나타난다. 

즉, 열거형을 이용하여 데이터 타입을 정의하고 사용하면 오타나 실수가 발생했을 때 그 즉시 컴파일러가 오류를 찾아주므로 잘못된 점을 바로 확인할 수 있다.(컴파일 오류) 하지만 집단 자료형을 사용해서 데이터 타입을 사용하면 잘못 사용했더라도 실행된 다음에야 오류를 발견할 수 있다.(런타임 오류)

 

일반적으로 다음의 조건을 만족하는 경우 값을 직접 입력하거나 집단 데이터를 사용하는 것보다 열거형 객체를 정의해서 사용하는 것이 좋다.

1. 원치 않는 값이 잘못 입력되는 것을 막고 싶을 때
2. 입력받을 값을 미리 특정할 수 있을 때
3. 제한된 값 중에서만 선택할 수 있도록 강제하고 싶을 때

열거형 멤버로 정의할 수 있는 데이터 집합은 연속된 값들이 아닌 불연속된 값들의 집합이어야 하며 (이산집합) 공통 주체와 연관되는 값들로 이루어져야 한다. 또한 종류가 몇가지로 수렴되는 값이어야 한다. (아래의 예시 참고)

 

- 성별: 남자, 여자, 히즈라(제 3의 성)
- 국가: 한국, 일본, 미국, 인도, 호주, 캐나다, 프랑스
- 지역: 서울, 부산, 강원, 충남, 경북, 전남, 경남, 제주
- 직급: 사원, 대리, 과장, 차장, 부장, 사장
- 색상: 빨강, 노랑, 초록, 파랑
- 방향: 동, 서, 남, 북

 

위 값들의 공통점은 무한히 늘어나지 않고, 미리 특정할 수 있는 값들이라는 것이다. 또한 각 죽마다 공통된 주제로 연관되는 값들로 구성되어 있다. 

 

이렇게 공통주제에 연관된 몇 가지 종류의 값만 제한적으로 사용할 수 있도록 정의하는 것이 열거형이다.

 

[열거형의 정의]

Swift에서 열거형 객체를 정의할 때는 enum 키워드를 사용한다. 그 후 열거형으로 정의할 객체의 이름을 작성하며, 마지막으로 열거형에 대한 내용을 정의하기 위한 중괄호 블록이 추가된다. 이 중괄호 블록에는 데이터 멤버들이 case 키워드와 함께 정의된다. 

enum 열거형 이름 {

    case 멤버값 1
    case 멤버값 2
    case 멤버값 3
    case 멤버값 4
    case ...

}

 

열거형의 이름은 소문자로 작성하되 첫글자만 대문자로 작성해야한다. 만약 두 단어 이상으로 작성하여면 각 단어의 첫 글자를 대문자로 작성해야 한다. 

 

 

열거형 객체가 제공학 값들 즉, 데이터 멤버는 중괄호 내부에 소문자를 사용하여 작성되어야한다. 보통은 case 키워드와 하나의 멤버로 정의하지만 멤버가 많은 때는 다수의 멤버와 case 키워드 하나로 정의하기도 한다. (아래의 예시 참고)

enum Direction {
    case north
    case south
    case east, west
}

위의 예시처럼 case 키워드 하나에 east, west 멤버 모두 선언할 수 있다. 멤버의 수에 대한 제한은 없지만 가독성을 해칠 수 있기 때문에 적절하게 나누어서 작성해야한다.

 

예시에서 작성된 멤버들은 아래의 형식으로 사용할 수 있다. 

let N = Direction.north
let S = Direction.south
let E = Direction.east
let W = Direction.west

 


<열거형 객체의 사용>

 

열거형을 정의한다는 것은 새로운 타입의 데이터를 정의하는 것과 같아서 열거형의 멤버를 사용하여 변수나 상수에 대입하면 그 변수나 상수는 열거형 타입으로 선언된다. 

var directionToHead = Direction.west //열거형을 이용하여 변수를 정하기

 

directionToHead 변수는 Direction 객체에서 정의된 멤버중 하나인 west로 초기화 되었다. 그렇기 때문에 굳이 타입 어노테이션을 명시하지 않아도 이 변수는 Dirction 타입으로 정의 된다.  (아래는 굳이 타입 어노 테이션을 명시한 것)

var directionToHead: Direction = Direction.west

 

변수를 열거형 타입으로 정의하고 나면 변수에 대입할 수 있는 값으 열거형 타입에 정의된 다른 멤버 뿐이다. 따라서 변수 값을 변경할 때는 열거형 타입명을 생략하고 dot을 붙여서 값을 변경해야한다.

directionToHead = .east

 

위의 경우가 성립되는 이유는 directionToHead 변수가 Direction 타입으로 정의되었다는 것을 컴파일러가 미리 알고 있기 때문이다. 

 

하지만 컴파일러가 인지하기 전 선언할 때부터 열거형 타입명을 생략하고 변수를 선언한다면 컴파일러에 의해서 오류는 발생하기 때문에 아래와 같이 타입 어노테이션을 함께 작성해주어야한다.

// 잘못된 예시 (x)

var directionToHead = .west

// 올바른 예시 (O)

var directionToHead: Direction = .west

 

위의 내용을 요약해보자. 

1. 열거형 타입으로 정의된 변수에는 열거형 타입명을 생략하고 멤버값만 대입해도 오류가 발생하지 않는다.

2. 변수나 상수의 타입 어노테이션을 명시한 경우, 처음부터 타입명을 생략하고 멤버값만 대입해도 오류가 발생하지 않는다.

3. 타입 어노테이션 없이 변수나 상수를 초기화할 때 타입명은 생략할 수 없다. -> 오류 발생

<switch 구문과 열거형>

 

열거형 타입으로 정의된 변수는 switch 구문에서 열거형이 멤버와 비교하는 분기 구문을 사용할 수 있다. 

 

switch 비교대상 {
case 열거형.멤버1 :
    //실행구문
case 열거형.멤버2 :
    //실행구문
}​

 

앞에서 정의한 변수  directionToHead 변수는 Direction 타입으로 정의되어 있으므로 switch 구문 내에서도 Direction 객체의 멤버들을 case 블록에 나누어 비교해주면된다.

var directionToHead = Direction.west

switch directionToHead {
case Direction.north :
    print("북")
case Direction.south :
    print("남")
case Direction.east :
    print("동")
case Direction.west :
    print("서")
}

// --결과--
// 서​

 

위의 구문을 좀더 줄여보자. 위에서 배웠던 열거형 생략을 활용해보자.

var directionToHead = Direction.west

switch directionToHead {
case .north :
    print("북")
case .south :
    print("남")
case .east :
    print("동")
case .west :
    print("서")
}

 

switch 키워드 다음에 입력받는 변수를 통해서 비교 대상의 타입이 열거형인 것을 추론하기 때문에 위와 같이 생략이 가능하다.

(주의! 굳이 열거형 타입을 명시하지 않더라도 충분히 추론이 가능할 때에만 열거형 타입을 생략해야 한다.)

 

또한 열거형 타입을 사용할 때에는 default 구문을 생략할 수 있다. 

모든 상황에서 생략가능한 것이 아니라 열거형 타입에 정의된 모든 멤버를 case 구문에서 빠짐없이 비교했을 경우 default 구문을 생략할 수 있다.


[멤버와 값의 분리]

이제는 멤버와 실질적 값을 분리하여 열거형을 작성하는 법에 대해서 배워보자.

예시로는 HTTP 응답 코드를 사용해서 진행하겠다. 

enum HTTPCode: Int {
    case OK = 200
    case NOT_MODIFY = 304
    case INCORRECT_PAGE = 404
    case SERVER_ERROR = 500
}

위처럼 별도의 값을 대입할 때에는 주의할 점이 있다. 멤버에 값의 자료형을 열거형 타입의 선언뒤에 타입 어노테이션으로 표기해야 한다는 것이다.  

enum HTTPCode: Int { ...

이렇게 표기된 열거형은 멤버가 정수 형태의 값을 할당받는다는 것을 의미한다. 

멤버값은 일반적으로 문자열이므로, 문자열 형식의 멤버에 할당된 값을 읽을 때는 아래와 같이 rawValue라는 속성을 사용한다. 

HTTPCode.OK.rawValue
HTTPCode.NOT_MODIFY.rawValue
HTTPCode.INCORRECT_PAGE.rawValue
HTTPCode.SERVER_ERROR.rawValue

아래의 사진은 rawValue 사용여부에 따른 결과 비교 사진이다. 


<연관 값(Associated Values)>

 

연관 값을 정의하는 방식은 아래와 같다. 

enum ImageFormat {
    case JPEG
    case PNG(Bool)
    case GIF(Int, Bool)
}

var newImage = ImageFormat.PNG(true)
newImage = .GIF(256, false)

이미지 포맷을 정하는 열거형 ImageFormat은 3개의 멤버를 가지지만 PNG는 투명한 값을 가지는 PNG와 그렇지 않은 PNG로 나뉜다. GIF 역시 사용된 컬러의 수 애니메이션 여부에 따라서 나눌 수 있다.

 

이러한 특성을 모두 반영하여 멤버를 정의하면 필요한 멤버의 수가 많이 들어나게 된다. 

하지만 구분해야 할 값을 연관 값으로 처리하게 되면 세개의 멤버만으로 다양한 포맷을 처리할 수 있다. 

 


열거형은 클래스나 구조체처럼 내부에 연산 프로퍼티와 메소드를 정의할 수 있다. 

열거형은 클래스나 구조체처럼 인스턴스는 만들수 없지만. 열거형의 멤버를 인스턴스처럼 사용할 수 있다. 

enum HTTPCode: Int {
    case OK = 200
    case NOT_MODIFY = 304
    case INCORRECT_PAGE = 404
    case SERVER_ERROR = 500
    
    var value: String {
        return "HTTPCode의 번호는 \(self.rawValue) 입니다."
    }
    
    func getDescription() -> String {
        switch self {
        case .OK :
            return "응답에 성공했습니다."
        case .NOT_MODIFY :
            return "변경된 내역이 없습니다."
        case .INCORRECT_PAGE:
            return "존재하지 않는 페이지 입니다. "
        case .SERVER_ERROR:
            return "서버오류 입니다,"
        }
    }
    
    static func getName() -> String {
        return "HTTP"
    }
    
}

 

그렇다면 열거형 내부에 작성된 프로퍼티와 메소드는 어떻게 호출할까?

var response = HTTPCode.OK 
response = .NOT_MODIFY

response.value // "HTTPCode의 번호는 304 입니다."
response.getDescription() // "변경된 내역이 없습니다."

HTTPCode.getName() // "HTTP"
//타입 메소드이기 때문에 타입 자체 이름으로 호출가능