본문 바로가기
아이폰 개발/Objective C

2장 클래스

by 인생여희 2020. 8. 23.

 

Objective C의 프로그래밍은 클래스를 설계하고 이것을 운영하는 것

 

 

1.클래스의 구성 요소

 

Objective C는 기존의 C 언어로 프리컴파일되어 실행되기 때문에 기본적인 실행구조는 C 언어와 동일하다. 이런 이유로 Objective C 프로그램은 main() 함수가 실행되면서 시작하고 이 함수가 종료되면 프로그램도 끝나게 된다. 함수 main()은 클래스[Class, 형식]에 메시지를 보내 도장 찍듯이 인스턴스[Instance, 예]를 만들어내며 이 인스턴스가 동작하면서 프로그램이 운영된다. 클래스는 인스턴스를 만들기 위한 일 종의 형틀이며 변수부분과 동작부분으로 구성된다.

 

변수는 멤버[member, 구성원]라 하며, 값을 기억하는 역할을 한다. 동작은 메소드[method, 방법/함수]라고 하며, 클래스 또는 인스턴스의 내부에서 동작하는 함수로 멤버변수의 값을 이용하여 지정된 동작을 수행한다. 가장 기본적인 메소드로는 멤버변수에 값을 설정(write, 쓰기)하는 setter와, 값을 참조하는 getter(read, 읽기)가 있다. 물론 C 언어에서 사용하던 함수와 전역/지역 변수도 그대로 사용할 수 있다.

 

 

Objective C는 크게 다음과 같이 세 부분으로 구성되어 있다.

 

 

 

 

 

 

Objective C는 가장 기본적으로 main() 함수를 포함하는 main.m 파일이 있어야 한다. 여기에 추가적으로 클래스의 선언과 연결을 위한 @interface [클래스 이름].h 파일과 메소드 정의를 위한 @implementation [클래스 이름].m 파일이 각각 필요하다. 클래스 이름을 ChildClass로 가정하면 다음과 같은 파일이 구성된다.

 

 

2.클래스 제작을 위한 파일의 구성과 연관 관계

상속이란? 설명되지 않은 ParentClass란 무엇인가? 상속[Inheritance, 인헤리턴스]이란 기존의 클래스에 원하는 기능을 추가하거나 기존 기능을 변경하고 싶을때 기존 클래스의 변수와 메소드를 상속 받아 정의 또는 재정의하여 클래스를 재사용하는 것을 뜻한다. 상속에서 기존의 클래스를 부모(parent, super) 클래스라고 하고 새로 만들어지는 클래스를 자식(child, sub) 클래스라고 한다. 이것은 객체지향 프로그램의 첫번째 장점이다. 왼쪽 페이지의 코드처럼 ChildClass는 ParentClass를 상속받고 있다. 여기서 또 하나 중요한 것은 ParentClass는 NSObject를 상속받고 있다는 점이 다. NSObject는 모든 클래스의 직접 또는 간접 부모이며 Objective C의 루트[root, 뿌리] 클래스의 역할을 한다.

 

 

다중상속 언어는 여러 개의 부모 클래스를 상속받아 강력한 클래스를 구성할 수 있는 장점이 있는 반면 다양한 모호성이 유발되기 때문에 설계 이후 활용이 까다롭다. 반면 단일상속 언어는 하나의 부모 클래스만 가질 수 있는 단순함을 장점으로 가지고 있지만 클래스를 설계하는데 많은 제한을 가지게 된다. 반면 활용은 쉽다는 장점이 있다.

 

3.첫 클래스의 설계 및 운영

 

*이름 규칙[naming rules, 네이밍 룰스]

① 클래스 : 대문자로 시작하는 명사형 → NSObject, Fraction

② 인스턴스/멤버변수 : 소문자로 시작하는 명사형 → a, b, aFraction, bFraction, num, den

③ 메소드/함수 : 소문자로 시작하는 동사형 → alloc, init, set, print, convertToFloat, add

 

*동적인 메모리 할당, 인스턴스 메모리 관리 기법 ARC

Objective C 프로그램의 클래스는 정적으로 메모리를 점유한다. 반면 클래스가 만들어내는 인스턴스[Instance, 예제/사례]는 프로그래머에 의해 동적으로 메모리를 점유한다. 동적 메모리 할당 기능은 확장성이 뛰어난 기법이지만 해결이 어려운 버그가 자주 유발된다.

 

 

4.메소드[method, 방법]의 문법과 호출 방법

 

getter는 멤버변수의 값을 반환하는 함수로 인수가 필요없고 다만 멤버변수의 형식을 그대로 함수의 반환형식으로 사용한다. 즉, int 형식 변수 n의 값을 반환하는 getter는 int 형을 반환한다. 반대로 setter는 멤 버변수의 값을 설정하는 함수로 하나의 인수를 사용하고 인수의 형식은 멤버변수의 형식을 그대로 사용한 다. setter 함수는 반환값이 없으므로 형식은 언제나 void가 된다. 즉, int 형식 변수 n의 값을 설정하는 setter는 int형 인수 하나를 사용한다.

 

setter/getter의 이름 규칙

Objective C 2.0은 getter/setter 메소드(method)를 이름규칙에 따라 자동 생성하는 기능이 추가 되었다. setter의 이름은 set[멤버변수이름]으로 만들어지며, getter의 이름은 단순히 [멤버변수이름]으로 만들어진다. setter의 경우 멤버변수이름 첫철자가 카멜표기법에 따라 대문자로 전환된다. 멤버 변수 num의 getter/setter 메소드는 다음과 같이 생성된다.

 

•getter → num

•setter → setNum

 

 

전통적으로 Objective C의 메소드는 포인터 주소인 id 형식을 반환하고 이 값은 바로 현재 인스턴스의 포인터 주소인 self를 뜻한다. 이렇게 자기 주소를 반환하는 메소드의 기본 반환 방식은 다음과 같은 중첩된 메시지 전송을 가능하게 해준다.

 

 

*메소드 반환 형식을 생략할 경우

메소드를 선언/정의할 때 반환형을 생략하면 프리컴파일러는 자동으로 id 형식 포인터를 반환하도록 지정한다. 그러므로 void 형식이나 다른 형식을 반환하는 메소드를 설계/사용할 때는 주의가 필요하다. 다음의 두 가지 메소드 선언은 실제로 동일하게 해석된다. 추가로 인수 형식도 id일 경우 생략이 가능하다.

 

-initWithFraction:f;  =   -(id)initWithFraction:(id)f;

 

메소드의 호출은 실제 상당히 복잡한 처리를 통해 이루어진다. ANSI C에서는 단순히 실행용 함수 코드의 포인터와 매개변수의 명세만으로 관리가 가능하였으나 Objective C에서는 다형성을 지원하므로 복잡하게 처리된다. 

 

다형성[polymorphism, 폴리모르피즘]이란, 동일한 메소드 실행 메시지를 받더라도 각 객체가 속한 클래스에 따라 실행시간에 수행될 메소드를 결정한다는 것을 뜻한다. 객체지향 프로그램은 상속이라는 개념을 통해 코드의 재사용성과 확장성을 높였으며, 다형성을 통해 실행시간에 유연하게 동작하도록 한다. 

 

 

메소드에 관련된 몇 가지 용어

 

Fraction 예제

 

Fraction.h

Fraction.m

main.m

결과

더보기

1/4

+

1/2

---

3/4

Program ended with exit code: 0

 

 

5.변수의 종류

 

Objective C의 변수규칙은 다음과 같은 ANSI C의 변수규칙을 완전히 포함하고 있다. C의 변수규칙은 비교적 간단한데 특정 모듈의 외부 바탕(블럭 외부)에서 선언한 전역변수는 그 모듈 전체에서 접근 가능하며 프로그램이 운영되는 도중 사라지지 않는 비휘발성(non-volatile) 생존주기를 가진다. 반면 함수 블럭 내부(매개변수와 지역변수)에서 선언한 변수는 해당 함수에서만 접근 가능하며 함수의 실행이 종료되면 바 로 제거되는 휘발성(volatile) 생존주기를 가진다.

 

위와 같은 ANSI C 변수의 생존주기[life cycle, 라이프 사이클]와 접근영역[scope rules, 스코프룰] 규칙에 추가로 Objective C는 클래스/인스턴스 변수를 제공한다. 이중 가장 많이 사용하는 것은 인스턴스 변수로 클래스가 각 인스턴스를 만들 때마다 생성되는 인스턴스의 멤버 변수이다. 반면 클래스 변수는 클래스 내부에만 존재하며 특별한 용도 외에는 자주 사용하지는 않는다.

 

 

6. 멤버 변수로의 접근

 

인스턴스 메소드 내부에서는 자신의 멤버 변수에 직접 접근이 가능하기 때문에 굳이 getter와 setter를 호출하여 멤버 변수에 접근할 필요는 없다. 사실 우수한 객체지향 클래스 설계는 getter와 setter를 사용하지 않는 메소드를 제작하는 것이다.

 

 

alloc 메소드는 클래스 메소드로 선언과 정의에 +로 시작한다. 그렇기 때문에 이 메시지를 호출할 때는 [Fraction alloc]이라고 클래스 이름을 리시버로 쓰고 있다. 이 메시지는 인스턴스를 메모리에 할당하고 내부 멤버를 모두 0으로 초기화한 후 그 주소를 반환한다. 반면 init 메소드는 인스턴스 메소드로 선언과 정의는 -로 시작한다. 그렇기 때문에 이 메시지를 호 출할 때는 alloc 메소드의 인스턴스 반환값인 a를 리시버로 사용한다. 이렇게 하나의 메소드의 반환값을 다른 메소드의 리시버로 사용할 때는 위와 같이 구문을 하나로 합칠 수 있다.

 

7.클래스 변수

 

클래스 변수는 각각의 인스턴스에 선언되는 것이 아닌 클래스에만 선언된다. 이런 이유로 인스턴스가 파괴 되어도 프로그램 운영시간 동안 클래스 변수 기억공간은 계속 유지된다. 또한 +로 시작되는 클래스 메소드와 -로 시작하는 인스턴스 메소드 모두에서 접근 및 설정이 가능하다. 반면 +로 시작하는 클래스 메소드에서는 인스턴스 변수에는 접근하지 못한다.

 

 

Empty 예제

 

Empty.h

Empty.m

main.m

결과

더보기

2020-08-23 13:59:29.319766+0900 0202-Empty Static[4233:795178] 인스턴스 메소드로 클래스 변수 접근 : 4 4

2020-08-23 13:59:29.320218+0900 0202-Empty Static[4233:795178] 외부링크 함수로 클래스 변수 접근 : 4

2020-08-23 13:59:29.320263+0900 0202-Empty Static[4233:795178] 인스턴스 메소드로 클래스 변수 접근 : 6 6

2020-08-23 13:59:29.320315+0900 0202-Empty Static[4233:795178] 외부링크 함수로 클래스 변수 접근 : 6

Program ended with exit code: 0

 

8.클래스 객체에 대해

클래스 메소드는 인스턴스 변수에 접근할 수 없다. 그것은 클래스마다 생성되는 인스턴스는 여러개 만들어질 수 있고, 또한 접근하고자 할 때 특정 인스턴스가 생성되었는지 보장할 수 없기 때문이다. 반면 클래스 메소드와 인스턴스 메소드는 클래스 변수에 접근이 가능하다. 이것은 프로그램 내부에서 클래스 객체는 유일하며 각 클래스는 어플리케이션 운영 중에 언제나 생존해 있기 때문이다.

 

여기서 한 가지 의문을 가질 수 있다. 그것은 인스턴스 메소드가 클래스 변수에 접근하는 것이 바람직할까, 라는 점이다. 우선 간략히 논하자면 값을 참조하는 getter는 문제를 유발하지 않지만 전 페이지와 같이 값을 설정하는 setter 동작을 인스턴스 메소드에서 실행하면 문제가 발생할 수 있다. 여러 개의 인스턴스가 동시에 특정 변수에 접근하여 값을 변경하려고 하면 동시성 문제가 발생할 수 있다. 다음과 같이 클래스 변수가 4일 때 각 인스턴스는 이 값을 각각 2와 3씩 증가시켜야 한다고 가정한다.

 

위와 같이 두 개의 인스턴스 메소드가 동시에 수행될 때 결과 값은 6 또는 7을 가지게 된다. 반면 두 개의 인스턴스 메소드가 각각 독립적으로 수행된다면 4 + 2 → 6, 6 + 3 → 9를 가지게 될 것이다. 위와 같은 동시성 문제를 해결하기 위해서는 몇 가지 제약사항을 적용하여야 한다.

 

먼저 클래스 변수 값을 변경하는 동작은 클래스 메소드에서만 진행되어야 한다. 또한 이 메소드 역시 여러 요청에 의해 동시에 수행될 수 있기 때문에 후에 배울 @property에 atomic 속성을 지정하여 동시 실행을 방지[Thread safe, 스레드 세이프]하여야 한다.

 

클래스 멤버와 메소드에 관련하여 마지막으로 클래스 객체를 초기화해주는 +(void)initialize; 메소드에 대해 알아보겠다. 이 메소드는 각 클래스 객체가 첫 메시지를 실행하기 전에 시스템에의해 자동으로 수행된다. 즉, 이 메시지를 수동으로 호출하여서는 안 된다. initialize 메소드는 클래스 변수의 초기화를 위해 사용자가 재정의 할 수 있으며 시스템에 의해 각 클래스당 한 번씩 호출한다.

 

initialize 예제

결과

더보기

Class A initialized

Class A initialized

2020-08-23 14:08:06.924595+0900 0202-initialize[4304:828464] <B: 0x10060a450>

2020-08-23 14:08:06.925126+0900 0202-initialize[4304:828464] B

Program ended with exit code: 0

 

9.getter/setter의 자동생성

 

Objective C 2.0의 시대가 되면서 보다 편리하게 getter/setter를 자동으로 생성할 수 있는 도구를 지원하기 시작하였다.

 

@property는 getter/setter를 자동으로 생성해주고 @synthesize는 @property 구문에 의해 생성된 자동 변수를 인스턴스 변수와 동기화시켜 준다. @synthesize 구문을 생략하면 @property에 선언된 변수 앞에 언더바(_)를 추가하게 된다. 반면 @property 구문이 없는 @synthesize는 불가능하다. @property 구문에 사용할 수 있는 옵션(위에서는 readwrite를 사용)에 대해 알아보자. 먼저 옵션을 정리하면 다음 과 같다.

 

 

10.메소드의 상속 - init 메소드의 기본 형식

 

재정의[override, 오버라이드]

위의 예를 든 -(id)init 메소드는 NSObject에 정의되어 있다. 메소드의 오버라이드는 우리가 설계하는 새로운 클래스에 맞게 메소드를 다시 정의하는 과정을 뜻한다. 가장 먼저 [super init]를 통해 반환된 포인터 주소(id)를 self에 할당하고 각 목적에 맞는 나머지 작업은 if 블럭 안에서 수행한다. 이렇게 부모 클래스에서 정의된 메소드를 자식 클래스에서 다시 정의하는 작업을 오버라이드라고 한다. 여기서 다시 정의를 하더라도 부모가 정의한 부분을 재사용하기도 한다. 객체지향 프로그램은 클래스의 상속이고 메소드 오버라이드로 구현된다.

 

super 예제

결과

더보기

부모 클래스, 외부 메소드

부모 클래스, 내부 메소드

부모 클래스, 외부 메소드

자식 클래스, 내부 메소드

Program ended with exit code: 0

위의 예에서 id p 변수에는 Parent의 인스턴스가 할당되고 [p external]은 모두 Parent의 external > internal 메소드가 순서대로 실행된다. 반면 id c 변수에는 Child의 인스턴스가 할당된다. Child에는 external 메소드가 없으므로 슈퍼클래스인 Parent의 메소드가 실행되지만 이때 self가 가리키는 것은 Child의 인스턴스이므로 [self internal]은 Child의 메소드가 실행된다.  super와 self 예약어의 차이점만 기억하면 된다.

 

11. -(id)init의 상속

 

 

지정초기화 예제

① ChildVolume의 인스턴스 초기화를 위해 지정 초기화 호출

② 이때 부모클래스 Volume의 보조 초기화 init를 호출하는 것이 문제

③ 실제 self는 ChildVolume이므로 여기서 호출되는 것은 다시 ChildVolume의 지정초기화(①)

위의 과정을 통해 무한루프가 형성되어 실행 몇 초 후 Stack Overflow 오류 발생, 프로그램은 중단된다.

 

 

 

클래스, 인스턴스 그리고 포인터 변수

Objective C에서는 주로 클래스에서 만들어낸 인스턴스 객체를 사용한다. 우리가 설계한 클래스 객체는 시스템이 자동으로 생성해 주며 특별한 경우(새 인스턴스를 만드는 alloc 등)에만 사용한다. 

 

분수를 관리하는 예제에서 다음과 같이 포인터 변수를 선언한다는 것은 단순히 주소값 8바이트를 저장하기 위한 변수 두 개를 정의한다는 의미이다. 포인터 변수를 정의한다고 그 객체가 실제 생성되는 것은 아니다. 또한 포인터 변수는 nil 값으로 초기화 된다.

 

 

•메모리 누수(memory leak) : 인스턴스 객체가 메모리 존재하지만 이를 가리키는 포인터 변수가 없어진 경우, 해당 객체는 해제가 불가능하여 메모리의 낭비 및 시스템 오류가 발생된다.

 

•무효 포인터(dangling pointer) : 포인터 변수가 지시하는 주소에 인스턴스 객체가 사라졌거나 다른 자료가 있는 경우, 메모리 접근 오류로 시스템이 중단된다.

 

 

인스턴스 객체의 동적 할당이 이러한 문제를 유발한다면, 다음과 같은 정적할당으로 객체를 생성하면 간편하게 문제를 해결할 수 있지 않을까 라는 의문이 생긴다.

 

NSObject a;

//Interface type cannot be statically allocated.

 

 

위의 오류 메시지는 클래스의 인터페이스 형식으로 선언된 자료구조는 정적으로 할당할 수 없다는 뜻으로, 인스턴스 객체는 오로지 포인터 방식의 동적 할당만 가능하다는 의미이다. 그러나 Foundation 프레임워크에서 정의하고 있는 일부 struct 자료형인 NSPoint, NSSize, NSRect, NSRange 등의 자료형은 정적 할당이 가능하다. 이들은 @interface로 선언된 클래스 형식이 아닌 typedef 문으로 선언된 struct 구조체이기 때문이다.

 

 

22.pdf
1.61MB

 

 

 

 

출처 : objective C&Swift 프로그래밍 (글:조영석, 그림:조선근 출판사 예문사)

'아이폰 개발 > Objective C' 카테고리의 다른 글

objective c 동적결합  (0) 2020.12.26
objective c 상속과 클래스  (0) 2020.12.25
objective c 프로그램  (0) 2020.12.25
objective c 객체 개념  (0) 2020.12.25
1장 객체지향으로의 항해  (0) 2020.08.23