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

구조체와 클래스의 기본 개념 (꼼꼼한 재은 씨의 Swift : 문법편)

by print_soo 2022. 3. 29.

[용어 정리]

1. 프로퍼티: 구조체나 클래스 내부에서 정의된 변수나 상수

2. 메소드: 구조체나 클래스 내부에서 정의된 함수

3. 멤버: 프로퍼티, 메소드를 합해서 칭하는 용어

[구조체와 클래스의 공통점]

1. 프로퍼티: 변수나 상수를 사용하여 값을 저장하는 프로퍼티를 정의할 수 있다.

2. 메소드: 함수를 사용하여 기능을 제공하는 메소드를 정의할 수 있다.

3. 서브스크립트: 속성 값에 접근할 수 있는 방법을 제공하는 서브스크립트를 정의할 수 있다.

4. 초기화 블럭: 객체를 원하는 초기 상태로 설정해주는 초기화 블럭을 정의할 수 있다.

5. 확장: 객체에 함수적 기능을 추가하는 확장 구문을 사용할 수 있다.

6. 프로토콜: 특정 형싱의 함수적 표준을 제공하기 위한 프로토콜을 구현할 수 있다.

 

[클래스에서만 할 수 있는 것]

1. 상속: 클래스의 특성을 다른 클래스에게 물려줄 수 있다.

2. 타입 캐스팅: 실행시 컴파일러가 클래스 인스턴스의 타입을 미리 파악하고 검사할 수 있다. 

3.  소멸화 구문: 인스턴스가 소멸되기 직전에 처리해야 할 구문을 미리 등록해 놓을 수 있다.

4. 참고에 의한 전달: 클랙스 인스턴스가 전달될 때에는 참조 형식으로 제공되며 이때 참조가 가능한 개수는 제약이 없다.

 

타입과 인스턴스 - 타입은 설계도,  인스턴스는 설계도에 따라 만든 실체

[구조체와 클래스의 정의 형식]

//-구조체-

struct 구조체_이름 {
	// 구조체 정의 내용이 들어갈 부분
}

//-클래스-

class 클래스_이름 {
	// 클래스 정의 내용이 들어갈 부분
}

 

[명명 규칙]

 

1. 첫 글자는 대문자로 시작 

struct Integer {...}
struct String {...}
struct Controller {...}

2. 두개의 단어 이상으로 이루어진 복합 단어일 때는 단어마다 대소문자를 번갈아서 표기

struct SingedInteger {...}
class ViewController {...}

3. 약어로 이루어진 부분은 모두 대문자로 표기

class NSNumber {...}
class UIView {...}

[프로퍼티와 메소드]

프로퍼티 (혹은 속성) : 구조체와 클래스 내부에서 정의된 변수나 상수

메소드 : 함수를 정의하여 특정 기능을 정의하는 것

// 프로퍼티와 메소드가 정의된 구조체와 클래스 예시

struct Resolution {
	var width = 0
	var height = 0

	func desc() -> String {
		return "Resolution 구조체"
	}

}

class VideoMode {
	var interlaced = false
	var frameRate = 0.0
	var name: String?

	func desc() -> String {
		return "VideoMode 클래스"
	}

}

[인스턴스]

우리가 정의한 구조체나 클래스는 단순한 객체의 정의일 뿐, 실제로 값을 저장하고 메소드를 호출하는 데에 필요한 메모리 공간을 할당받지 못했기 때문에 구조체나 클래스 자체를 그대로 사용해서 값을 저장하거나 메소드를 실행할 수 없다.

 

구조체나 클래스는 일종의 틀이기 때문에 우리는 직접 값을 담을 수 있는 그릇을 만들어야 값을 담을 수 있다. 

 

구조체나 클래스라는 틀로 만들어낸 그릇을 바로 인스턴스라고 한다.  정확히 말하면 인스턴스란 타입 설계도를 사용하여 메로리 공간을 할당받은 것을 말한다. 

 

<1. 인스턴스 생성>

 

그러면 이제  위의 Resolution 구조체와 VideoMode 클래스의 인스턴스를 생성하는 방식을 배워보자. 

let insRes = Resolution()
// Resolution의 인스턴스를 생성하고 상수에 할당

let insVMode = VideoMode()
// VideoMode의 인스턴스를 생성하고 상수에 할당

위의 코드를 설명하면 클래스 or 구조체 뒤에 ()를 넣어 객체를 초기화시키며 인스턴스를 생성시키고, 이 값을 상수에 할당한 것이다.

이렇게 인스턴스를 변수나 상수에 할당하면 이제 원하는 곳에서 사용할 수 있게 된다. 

 

<2. 프로퍼티 접근>

 

이제 이렇게 만든 인스턴스를 가지고 클래스나 구조체 내부에 있는 프로퍼티에 접근해보자.

 

프로퍼티는 위 예제에서 선언된 인스턴스를 통해서만 접근이 가능하다. 인스턴스가 생성되지 않은 상태에서는 프로퍼티도 존재하지 않는 것이나 마찬가지이다. 

 

프로퍼티에 접근하기 위해서는 Dot Symtax(점 구문)를 사용하여 접근해야한다.

let width = insRes.width

print("insRes 인스턴스의 width 값은 \(width)입니다.")
-----------------------------------------------
// 실행결과

// insRes 인스턴스의 width 값은 0입니다.

 

아래의 예제를 보면 좀더 단계적인 접근이 가능하다. 

 

class VideoMode {
	var interlaced = false
	var frameRate = 0.0
	var name: String?
    	var res = Resolution() 
    //인스턴스 생성

	func desc() -> String {
		return "VideoMode 클래스"
	}

}

let vMode = VideoMode()

print("vMode 인스턴스의 width 값은 \(vMode.res.width)입니다.")
---------------------------------------------------------
// 실행결과

// vMode 인스턴스의 width 값은 0입니다.

vMode -> resoluton -> width 순으로 단계적으로 접근했다.

 

Dot Symtax(점 구문)는 프로퍼티에 값을 대입할 때도 사용한다.

vMode.name = "Sample"
vMode.res.width = 1280

print("\(vMode.name!) 인스턴스의 width 값은 \(vMode.res.width)입니다.")
-----------------------------------------------------------------
// 실행결과

// Sample 인스턴스의 width 값은 1280입니다.

[초기화(구조체)]

Swift에서는 옵셔널 타입으로 선언되지 않은 모든 프로퍼티는 명시적으로 초기화해 주어야 한다.

초기화되지 않은 프로퍼티가 있을 경우 컴파일러는 이를 컴파일 오류로 처리한다. 

 

따라서, 클래스나 구조체의 모든 프로퍼티는 적어도 인스턴스가 생성되는 시점까지는 반드시 초기화 되어야한다.

 

여기서 명시적 초기화란 두 가지가 있다. 

1. 프로퍼티를 선언하면서 동시에 초기값을 지정하는 경우

2. 초기화 메소드 내에서 프로퍼티의 초기값을 지정하는 경우

 

구조체는 모든 프로퍼티의 값을 인자값으로 입력받아 초기화하는 기본 초기화 구문을 자동으로 제공한다. 

프로퍼티를 보통 멤버 변수라고 부르는 까닭에, 이 초기화 구문을 멤버와이즈 초기화 구문(Memberwise Initializer)라고 부른다. 

let defaultRes = Resolution(width: 1024, height: 768)
// width와 height를 매개변수로 하여 Resolution 인스턴스를 생성

print("width: \(defaultRes.width), height: \(defaultRes.height)")
------------------------------------------------------------------
// 실행결과

//width: 1024, height: 768

위의 구문은 Resolution 구조체에 있는 width, height를 초기화하기 위한 인자값을 입력 받아서 내부적으로 프로퍼티를 초기화한 것이다. 

 

따라서 width, height의 출력결과 각각 1024, 768이 나오게 된다.

Resolution 구조체를 정의할 때 width, height의 초기값은 0 이었다. 하지만 위의 구문을 통해서 프로퍼티가 각각 1024, 768로 초기화 되었다.

 

즉, 입력한 인자값이 프로퍼티의 초기값으로 설정된 결과이다. 

 

사실 위의 방법말고 우리가 인스턴스 생성할 때 사용했던 ()구문도 초기화를 한다. 하지만 이 구문을 사용하면 아무 인자 값도 입력받지 않기 때문에, 어떤 프로퍼티도 초기화하지 않는다. 따라서 이 형식의 초기화 구문을 사용할려면 객체의 모든 프로퍼티는 선언과 동시에 초기값이 지정되어 있어야 한다. 

 

Resolution() 기본 초기화 구문, 내부적으로 프로퍼티를 초기화X
Resolution(width: Int, height: Int) 모든 프로퍼티위 초기값을 입력받는 초기화 구문, 내부적으로 모든 프로퍼티를 초기화O

 

구조체에서는 멤버와이즈 초기화 구문이 존재하지만 클래스에서는 존재하지 않는다. 기본 초기화구문은 존재하지만 이마저도 모든 프로퍼티가 선언과 동시에 초기화 되어있을 때만 사용 가능하다.

 

만약 초기화되지 않은 프로퍼티가 있다면 기본 초기화 구문은 사용할 수 없으며,  이 때에는 직접 초기화 구문을 정의해서 내부에서 해당 프로퍼티를 초기화 해주어야 한다.

 

복잡해질 수 있으니 가급적 아래의 두가지 원칙을 지키며 프로퍼티와 초기화 구문을 사용하자.

1. 모든 프로퍼티는 정의할 때 초기값을 주던가, 아니면 옵셔널 타입으로 선언한다.
2. 인스턴스를 생성할 때에는 클래스명 뒤에 ()를 붙여준다.

 

인스턴스를 생성한다. == 인스턴스를 초기화한다.

 

 

위의 말이 성립되는 이유는 인스턴스를 생성하는 시점에서 프로퍼티의 값들이 모두 초기화되기 때문이다.

 


[값 전달 방식]

<1. 구조체 - 복사에 의한 전달>

 

구조체는 인스턴스를 생성한 후 이를 변수나 상수에 할당하거나 인자값으로 전달할 때 값을 복사하여 전달한다.

즉, 구조체 인스턴스를 변수에 대입하면 기존의 인스턴스가 그대로 대입되는 것이 아니라 이를 복사한 새로운 값이 대입되는 것이다.

이것을 바로 복사에 의한 전달 or 값 타입(Value Type)이라고 한다.

 

따라서 기존의 인스턴스와 변수의 인스턴스는 서로 독립적이다. 그렇기 때문에 기존의 인스턴스나 변수의 인스턴스에 무언가 변경이 발생하더라도 서로 영향을 미치지 않는다.

 

let hd = Resolution(width: 1920, height: 1080) 

var cinema = hd

위의 hd와 cinema 또한 서로 독립적이다. 그 이유는 hd를 cinema에 대입하는 시점에서 기존의 인스턴스의 복사본이 하나 더 만들어진 다음에 대입되기 때문이다. 따라서 hd와 cinema는 같은 width와 height 값을 갖고 있지만 값만 같을뿐 실제로는 별개인 인스턴스가 대입되어 있다. 

 

cinema.width = 2048

print("hd 인스턴스의 width 값은 \(hd.width)입니다.")
print("cinema 인스턴스의 width 값은 \(cinema.width)입니다.")
--------------------------------------------------------
//실행결과

//hd 인스턴스의 width 값은 1920입니다.
//cinema 인스턴스의 width 값은 2048입니다.

 

위의 예제처럼 hd인스턴스와 cinema인스턴스는 완전히 분리되어 있다.

 

<2. 클래스 - 참조에 의한 전달>

 

클래스는 구조체와 달리 메모리 주소 참조에 의한 전달 방식을 사용한다. 이를 참조 타입(Reference Type)이라고 한다.

참조타입은 변수나 상수에 할당될 때, 또는 함수의 인자값으로 전달될 때 값의 복사가 이루어지지 않는다.

 

여기서 말하는 참조란, 인스턴스가 저장된 메모리 주소 정보가 전달된다는 뜻이다. 

 

{복사 vs 참조 - 물건을 줄 때}

복사에 의한 전달 같은 물건을 하나 더 만들어서 준다.
참조에 의한 전달 물건의 위치를 알려준다. -> "xxx 보관함에 있으니깐 가져가."
let video = VideoMode()
video.name = "Original Video Instance"

print("video 인스턴스의 name 값은 \(video.name!)입니다.")
---------------------------------------------------------
//실행결과

//video 인스턴스의 name 값은 Original Video Instance입니다.

클래스를 초기화하여 인스턴스를 생성하고 video 상수에 할당하였다. 그리고 인스턴스의 name 프로퍼티 값을 변경해주었다. 

 

이제 이 인스턴스의 변수를 다른 상수에 할당해보자. 

let dvd = video
dvd.name = "DVD Video Instance"

print("video 인스턴스의 name 값은 \(video.name!)입니다.")
---------------------------------------------------
// 실행결과

// video 인스턴스의 name 값은 DVD Video Instance입니다.

예제를 보면 알 수 있듯 우리가 변경하지 않았던 video 상수의 프로퍼티에서도 값이 변경되었음을 알 수 있다.

 

다음 예제로 인스턴스 값을 함수의 인자갓으로 넣어 다시 수정해보자.

func changeName(v: VideoMode) {
    v.name = "Function Video Instance"
}

changeName(v: video)

print("video 인스턴스의 name 값은 \(video.name!)입니다.")
---------------------------------------------------
// 실행결과

// video 인스턴스의 name 값은 Function Video Instance입니다.

코드를 보면 우리가 보지 못한 것이 있는데  func changeName(v: VideoMode) 부분의 인자값 타입을 처음 보게되었다.

인자값 타입은 대부분 Int, String, Bool등이었지만 이렇게 클래스나 구조체를 이용해서 임의의 자료형을 만들어서 사용할 수 있다.

 

다시 돌아와서 코드를 보면 함수 내부에서 매개변수 v의 프로퍼티를 변경하는 것은 곧 video 인스턴스의 프로퍼티를 수정하는 것과 같다.

 

{클래스가 참조 타입이기 때문에 이해야하는 개념 2가지}

 

1. 메모리 문제

클래스는 구조체와 달리 여러곳에서 동시에 참조가 가능하기 때문에 메모리 해제 시점을 계산하여 메모리를 해제해야한다.

다른 곳에서 인스턴스를 참조하고 있는 상황에서 인스턴스를 해제해버리면 잘못된 메모리 참조로 인한 오류가 생긴다. 

 

이 오류를 해결하기 위해서는 참조하는 곳을 계속 검사하고 참조하는 곳들이 모두 제거되면 그 때 메모리를 해제해야한다. 

이러한 역할 담당하는 객체가 바로 ARC(Auto Reference Counter)의 약자로서 "현재 인스턴스를 참조하는 곳이 모두 몇 군데인지 자동으로 카운트해주는 객체"라고 할 수 있다.

 

2. 비교의 문제

클래스 인스턴스에서 단순한 값 비교는 불가능하다. 대신 두 대상이 같은 메모리 공간을 참조하는 인스턴스인지 아닌지를 비교해야한다.

따라서 클래스 인스턴스의 비교 연산자는 다음 연산자를 사용한다.

- 동일한 인스턴스인지 비교할 때 : ===
- 동일한 인스턴스가 아닌지 비교할 때 : !==

 

지금까지 알아본 결과 클래스는 참조 타입이고, 구조체는 복사 타입이다. 그러면 어떤 경우에는 클래스를 사용하고, 어떤 경우에는 구조체를 사용해야할까? 다음 조건에 하나 이상 해당하는 경우라면 구조체를 사용하는 것이 좋다.

 

1. 서로 연관된 몇 개의 기본 데이터 타입들을 캡슐화하여 묶는 것이 목적일 때 

 

2. 캡슐화된 데이터에 상속이 필요하지 않을 때 

 

3. 캡슐화된 데이터를 전달하거나 할당하는 과정에서 참조 방식보다는 값이 복사되는 것이 합리적일 때 

 

4. 캡슐화된 원본 데이터를 보존해야할 때