Starbucks Caramel Frappuccino
본문 바로가기
  • 그래 그렇게 조금씩
iOS Developer/Objective-C

17. Objective-C 클래스와 객체 (메모리), 접근제어

by Toughie 2024. 5. 20.

KAKAO-Choonsik

[@property, 스레드 안전, 접근제어]

 

Objective-C는 이름만 봐도 알 수 있듯이 C언어에서 객체지향의 개념을 더한 슈퍼셋 언어이다.

객체지향 프로그래밍에서 클래스는 핵심 기능인데 클래스는 객체를 찍어내기 위한 틀 혹은,

데이터와 데이터를 조작하는 메서드를 하나로 묶은 것으로 설명할 수 있다.

 

클래스와 객체

클래스는 객체를 찍어내기 위한 템플릿, 설계도이다.

객체는 일반적으로 클래스의 인스턴스를 말한다.(클래스를 바탕으로 실제로 메모리에 만들어진 것)

인스턴스는 특정 클래스에서 생성된 객체를 의미한다.(어떤 클래스에서 만들어진 것인지가 중요할 때 사용)

 

객체와 인스턴스를 엄밀히 구분하는 것은 어렵고, 객체는 클래스에서 생성된 모든 것,

인스턴스는 특정 클래스에서 생성된 것을 강조하기 위해 사용하는 용어로 객체가 인스턴스보다 좀 더 넓은 범위라고 볼 수 있다.

 

클래스 안의 데이터와 메서드는 클래스의 '멤버'라고 부르기도 한다.


NSObject

 

대부분 클래스의 상위클래스로, 메모리 할당이나 초기화 같은 기본 기능을 제공한다.

 

#import <Foundation/Foundation.h>

@interface MyKeyBoard : NSObject {
    //인스턴스 변수
    int width;
    int length;
    int height;
}
@property(nonatomic,readwrite) int height;
@end

@implementation MyKeyBoard
@synthesize height;
@end

 

다른 언어와 다르게 Objective C에서 변수 부분이 좀 달라서 정리를 해보자.


인스턴스 변수( Instance Variable)

클래스 내에서 선언되는 변수로, 클래스의 인스턴스마다 독립적으로 존재한다.

인스턴스 변수는 직접 접근이 제한된다.(encapsulation)

객체가 생성될 때(인스턴스화 될 때) 메모리에 할당되며 객체가 소멸될 때 (dealloc) 메모리에서 해제된다.


@property

인스턴스 변수에 접근하고 수정할 수 있는 접근자(accessor) 메서드를 자동으로 생성해 주는 키워드.

@property를 사용하면 컴파일러가 자동으로 getter와 setter 메서드를 생성해 준다.

(readwrite는 getter, setter / readonly는 getter만 생성)


Thread safety

@property 키워드를 통해 컴파일러가 자동으로 생성하는 getter와 setter 메서드가

멀티 스레드 환경에서 어떻게 동작할지 결정하는 속성이 있다.

 

atomic

기본 설정으로 속성에 대한 접근이 스레드 안전을 보장한다.

컴파일러는 생성된 getter와 setter 메서드가 원자성을 유지하도록 한다. 

(여러 스레드가 동시에 접근할 때 한 번에 하나의 스레드만이 접근하도록 해서 데이터의 일관성을 유지)

멀티 스레드 환경에서 데이터가 반드시 변경 전, 변경 후의 상황에서만 접근되도록 하는 것을 보장한다.

특정 스레드가 atomic 변수를 변경하는 동안 다른 스레드는 해당 변수를 읽거나 쓸 수 없다. 
즉 다른 스레드의 접근을 지연시키는 것이다. ( 다 끝날 때까지 기다려 !)

 

다만 원자성을 보장하기 위한 추가적인 비용이 발생한다.(변경이 다 될 때까지 기다려야 되니까)

또한 완전히 경쟁상태를 방지하지는 않기 때문에 복잡한 연산이나 조합의 접근 상황에서는

@synchronized나 NSLock 같은 동기화 처리가 필요할 수 있다.

nonatomic

속성 접근이 스레드 안정성을 보장하지 않는다.

멀티스레드 환경에서 동시 접근 시 데이터 일관성이 깨질 수 있지만, 성능이 더 좋다.

 

Objective-C에서 프로퍼티는 기본적으로 atomic이지만 

멀티스레드에서 접근될 이유가 없는 프로퍼티(메인 스레드에서 업데이트해야 하는 뷰 프로퍼티 등)

에는 nonatomic을 설정하는 것이 성능적으로 유리하며, Swift의 경우에는 non atomic이 기본이다.


메모리 관리 정책

@property를 정의할 때 메모리 관리 정책을 지정해서 해당 속성의 메모리 관리 방식과 객체의 소유권을 결정할 수 있다.

 

strong(ARC)

객체 소유권을 유지해 속성이 참조하는 객체가 해제되지 않도록 한다.

(객체의 참조카운트를 증가시켜 속성이 존재하는 한 객체가 메모리에서 해제되지 않는다.)

ARC 환경에서 기본 속성이다.

 

weak

객체 소유권을 유지하지 않으며, 참조하는 객체가 해제될 때 자동으로 nil로 설정된다.

참조하는 객체의 참조카운트를 증가시키지 않아 강한 순환 참조(strong retain cyle)를 방지할 수 있다. 

 

assign

객체가 아닌 기본 데이터 타입(int 등) 또는 객체의 메모리 관리를 직접 할 때 사용한다.

객체의 경우 참조카운트를 증가시키지 않지만 객체가 해제되었을 때도 참조를 유지한다.

(잘못된 메모리 접근을 일으킬 수 있다.)

 

copy

객체를 참조할 때 원본 객체의 복사본을 생성해서 속성에 할당한다. 

NSString과 같은 불변 객체나 객체의 변경을 방지할 필요가 있을 때 사용한다.

 

retain(Non-ARC)

객체 소유권을 유지하며 참조 카운트를 증가시킨다.
(strongr과 같은데 Non-ARC 환경에서 사용하는 것이다)


@synthesize

프로퍼티는 외부에서 인스턴스 변수에 접근하는 인터페이스를 제공한다.

컴파일러에게 getter와 setter 메서드를 생성하라는 명령으로, 이름이 동일한 경우

인스턴스 변수와 프로퍼티를 연결하게 된다.(synthesize) 

(최신 Xcode에서는 생략해도 알아서 해줌)

 

만약 명시적으로 인스턴스 변수를 선언하지 않고 프로퍼티만 작성할 경우

컴파일러가 자동으로 *프로퍼티를 위한 인스턴스 변수를 생성하고 이것을 사용할 수 있게 된다.

(*자동 생성 인스턴스 변수: _언더스코어로 시작하고 프로퍼티 이름을 따른다.)


객체의 메모리 할당 및 초기화

MyKeyboard *key1 = [[MyKeyboard alloc] init];

 

대괄호를 두 번이나 쓰는 이 요상한 문법의 동작 방식에 대해 알아보자.

우선 Objective-C에서 대괄호는 메서드 호출을 나타낸다. 

 

alloc

NSObject에서 상속받은 메서드로, 객체의 메모리를 할당하고 해당 포인터를 반환한다.

메모리 할당만 하고 초기화는 진행하지 않는다. 

 

init

NSObject에서 상속받은 메서드로, 객체의 초기 상태를 설정한다.

객체 초기화에 성공하면 초기화된 객체의 포인터를 반환하고 실패하면 nil을 반환한다.

(명시적으로 생성자를 구현하지 않은 경우 기본 생성자를 사용한다.)

 

[동작 순서]

먼저 alloc 메서드를 호출해서 MyKeyboard 클래스의 새로운 인스턴스를 메모리에 할당한다.

메서드 호출이 성공적으로 끝나면 새로 생성된 객체에 대한 포인터를 반환한다. (초기화는 되어 있지 않음)

 

해당 객체에서 init 메서드를 호출해서 객체를 초기화하고 초기화된 객체의 포인터를 반환한다.

 

최종적으로 반환된 포인터가 *key1에 할당되는 것이다.

이로써 key1 변수가 MyKeyboard 객체를 가리키게 된다.


데이터 멤버 접근

다행히 데이터 멤버 접근은 똑같이 . 접근자를 사용한다.

아래 예시를 통해 클래스 정의부터 생성 및 멤버 접근까지 살펴보자.

추가로 정적 변수 및 메서드 예시도 포함시켰다.

(컴파일 타임에 한 번만 초기화되어 데이터 영역에 저장되는 static)

 

#import <Foundation/Foundation.h>

@interface MyKeyboard : NSObject {
    //인스턴스 변수
    int switches;
    int keyCaps;
    
}
//getter, setter 자동 생성 및 nonatomic 설정
@property(nonatomic,readwrite) int artisans;

//인스턴스 메서드
-(int) totalParts;
//클래스(static) 메서드
+(void) checkVer;
@end

//클래스 구현
@implementation MyKeyboard

//클래스 변수
static double KeyVersion = 2.0;

//xcode 자동 제공으로 생략 가능
//@synthesize artisans;

//id는 인스턴스의 포인터이다.
-(id)init {
    //생성자를 직접 구현해- 부모 클래스의 생성자를 꼭 호출해줘야 한다.
    self = [super init];
    switches = 75;
    keyCaps = 75;
    //인스턴스 포인터를 반환
    return self;
}

-(int) totalParts {
    return switches + keyCaps + _artisans;
}

+(void) checkVer {
    NSLog(@"Keyboard Version: %f",KeyVersion);
}
@end

int main(void) {
    @autoreleasepool {
        MyKeyboard *key1 = [[MyKeyboard alloc] init];
        
        int specialArtisans = 75;
        //프로퍼티는 외부에서 접근 가능
        key1.artisans = specialArtisans;
        
        //Instance variable 'switches' is protected // Private 접근 제어가 된다.
//        NSLog(@"%d",key1->switches);
        
        NSLog(@"%d",key1.artisans);
        NSLog(@"%d",[key1 totalParts]);
        
        //static 요소 접근
        NSLog(@"%f",KeyVersion);
        [MyKeyboard checkVer];
    }
    
    return 0;
}

*접근제어 정리(Access Control)

llm,블로그,스택 등 인스턴스 변수와 프로퍼티에 대한 말이 다 너무 달라서 큰 혼란을 겪었다.

진짜 너무 애매모호한 설명들이 많아서 그냥 Xcode 컴파일 에러로그를 믿고 여러가지 테스트를 해보았다.

 

#import <Foundation/Foundation.h>

@interface AccessClass : NSObject {
    int protectedVar;
}

@property int propertyVar;

@end

@implementation AccessClass {
    int privateVar;
}

- (id)initWithNumber:(int)A andB:(int)B andC:(int)C {
    protectedVar = A;
    _propertyVar = B;
    privateVar = C;
    return self;
}
@end

@interface SubClass : AccessClass
-(id)initWithNumber:(int)A andB:(int)B andC:(int)C;

-(void) print;
@end

@implementation SubClass

-(id) initWithNumber:(int)A andB:(int)B andC:(int)C {
    [super initWithNumber:A andB:B andC:C];
    return self;
}
-(void)print {
    NSLog(@"%d",protectedVar); //직접 접근 가능
//    NSLog(@"%d",_propertyVar); // 인스턴스 변수에 직접 접근하려 해서 에러 _propertyVar is private
    NSLog(@"%d",self.propertyVar); // self를 통해 속성(Property)를 통한 접근
//    NSLog(@"%d",privateVar); //private
}
@end

int main(void) {
    AccessClass *a = [[AccessClass alloc] initWithNumber:1 andB:2 andC:3];
//    NSLog(@"%d",a.protectedVar); //protected
//    NSLog(@"%d",a.privateVar); //private
    NSLog(@"%d",a.propertyVar);
    
    SubClass *b = [[SubClass alloc] initWithNumber:7 andB:8 andC:9];
    [b print];
}

 

혼란의 가장 큰 부분은 @property를 단순히 public 접근제어 수준으로 생각했던 것이었다.

왜냐면 인스턴스 외부에서 자유롭게 접근 할 수 있기 때문이다. 하지만 이는 변수에 직접 접근하는 것이 아니라

getter와 setter를 이용해서 접근을 하는 것이다.

 

1. @interface의 중괄호에서 인스턴스 변수를 선언하는 경우

-> 접근 제어 수준은 protected로 자식 클래스에서 직접 접근 가능, 인스턴스에서 접근 불가능

 

2. @interface에서 @property를 통해 변수를 선언하는 경우

->외부에서 get,set 가능 (점 문법을 통해서) 하지만 자식 클래스에서 직접 접근 불가능(private 접근수준)

사용하려면 self.변수를 통해 프로퍼티가 제공하는 getter setter 사용해야함.

인스턴스 변수를 선언하고 프로퍼티를 선언해서 @synthesize 하지 않은 경우 자동생성 인스턴스 변수의

접근 수준은 private이라는 뜻. private이기 때문에 직접 접근은 자식이든 외부에서든 불가능하고 

반드시 getter와 setter를 사용해야 한다는 의미. 

 

3. @implementation의 중괄호에서 인스턴스 변수를 선언하는 경우

-> private 접근 제어 수준으로 외부 및 자식 클래스에서도 접근 불가능


이런거 보면 그냥 명시적으로 무조건 접근제어자 적어주는게 차라리 나을지도..라는 생각이 든다.

정리하자면 클래스에서 변수를 선언할 때 외부의 접근 가능성 및 상속관계를 전부 고려해야 한다.

 

1. 외부에서 인스턴스의 변수에 직접 접근하듯이 사용하려면? @property를 사용하자 (public이란 의미는 아니다.)

2. 정말 해당 클래스 내부에서만 사용할 변수라면? 클래스 구현부(implementation)에서 중괄호 안에 선언해주자.

3. 자식 클래스까지는 접근해야 하는 경우라면? @interface의 중괄호 안에 선언해서 protected 수준을 유지하거나, 

@property를 통해 자식 클래스 또한 getter setter로 접근하도록 하자 (변수 자체는 private 수준이다.)

_변수 이렇게 직접 접근하지 말고 self.을 통해 접근하도록 하자.


 

이렇게까지 여기저기서 말이 다른 개념은 오랜만이었다. 변수하나 만드는게 이렇게 복잡해서야.. 

(Swift에서는 또 클래스 안에 변수 만들면 internal 수준이잖아!!) 

 

 

Objective C에서 클래스의 멤버 중 인스턴스 변수와 프로퍼티에 대해 자세히 알아보았다.

encapsulation, 스레드 안전 및 메모리 정책 등 상황에 따라 커스텀 가능한(신경 써야하는) 요소가 굉장히 많다.

하지만 이는 개발자가 메모리 문제를 일으킬 수 있다는 반증이 되기도 한다.

C계열 언어는 '자유에는 책임이 따른다'는 말이 정말 잘 어울리는 것 같다..🙄