어느 개발자의 한적한 공간 살아나가면서 저지른 이야기

Effective Objective-C

오브젝티브-C의 기원과 친숙해져라

  • 함수 호출이 아닌 메세징 구조를 사용한다
  • 메세징 구조가 함수 호출과 다른 가장 큰 부분은 런타임이 실행할 코드를 정한다는 것이다.
    • 함수 호출은 그에 반해 컴파일러가 어떤 코드를 실행할지 정한다.
  • 타입 또한 동적 바인딩이라는 프로세스를 이용해 실행 시간에 찾는다.
  • 근본적으로 런타임은 로드의 집합인데, 여러분의 코드 및 그 코드와 링크된 동적 라이브러리를 함께 잘 붙이는 일을 한다.
    • 런타임이 업데이트 되면 애플리케이션도 성능 향상의 이점을 동시에 누리게 된다.
  • objc는 c언어의 확장이므로 c의 모든 기능을 쓸 수 있다.
  • 객체의 메모리는 항상 스택이 아닌 힙 공간에 할당되기 때문이다. objc 객체는 스택에 할당하는 것이 허용되지 않는다.
    • NSString *someString = @“The String”;
    • NSString *anotherString = someString;
      • someString은 NSString 객체를 포함하고 있는 특정 힙 메모리 영역을 가리킨다.
      • NSString 인스턴스는 오직 하나만 존재한다.
  • CGRect는 objc 객체를 참조하지 않는다.
    • 오브젝티브c 객체를 사용하는 부하가 성능에 영향을 주기 때문

헤더에 헤더를 포함하는 것을 최소화하라

  • 항상 헤더를 포함하는 것을 최대한 미루라. 헤더에 클래스를 포워드 선언하고 구현 파일에 포함하는 것을 의미한다.
  • 프로토콜을 따르는 것을 선언할 때와 같이 포워드 선언이 불가능할 때는 클래스 확장 카테고리로 바꿔보라.

메서드 보다는 같은 일을 하는 리터럴 문법을 사용하라

  • NSNumber *someNumber = [NSNumber numberWithInt:1]; // 메서드 대신
  • NSNumber *someNumber = @1; // 리터럴 문법 사용
  • 숫자 리터럴을 사용하면 숫자 객체 선언을 거추장스러운 문법이 아니라 값만 적을 수 있어 NSNumber 객체 사용이 훨씬 깔끔해진다.
  • 리터럴 문법으로 배열을 생성할 때 주의해야할 점 : 생성하려는 객체 중 단 하나라도 nil이면 예외를 던진다.
    • 만약 obj1,3은 정상이고 obj2가 nil이면
    • NSArray *arrayA = [NSArray arrayWithObjects:obj1, obj2, obj3, nil]; // obj1만 포함하는 배열 생성 ; nil 이전값들까지만 받아들이므로
    • NSArray *arrayB = @[obj1, obj2, obj3]; // 예외 발생.
    • 따라서 문법적으로 리터럴이 좀더 안전하다.
  • 딕셔너리인 경우 참조 방법
  NSString *lastName = [personData objectForKey:@"lastName"]; // 메서드
  NSString *lastName = personData[@"lastName"]; // 리터럴

전처리기 #define보다 타입이 있는 상수를 사용하라

  • #define ANIMATION_DURATION 0.3
  • static const NSTimeInterval kAnimationDuration = 0.3;
  • 타입 정보를 알 수 있으므로 여러모로 유용하다.
  • 내부적으로 변환 단위를 표현하는 상수의 일반적인 표기법은 k소문자를 상수 맨 앞에 붙이는 것.
  • 외부로 노출 되는 상수는 클래스 이름을 상수 앞에 붙여준다.
  • 외부로 노출할 필요가 없는 상수는 사용되는 구현 파일에 정의해야 한다.
  • 외부로 노출해야할 상수는 전역 심벌 테이블에 넣을 필요가 있다.
    • extern NSString *const EOCStringConstant; // 헤더 파일 내
    • NSString *const EOCStringConstant = @“Value”; // 구현 파일 내
    • 이 정의는 리드 백워드(read backward)이다. 뒤에서부터 읽으면 된다.
    • 이 상수는 다른 NSString 객체를 가리키도록 변경이 허용되어선 안된다.

열거형을 사용해 상태, 옵션, 상태 코드를 정의하라

  • 열거형 타입을 사용하는 이유 중 또 하나는 옵션을 조합할 수 있을 때 옵션을 정의하기 위해서다. 비트 연산인 OR 연산자를 이용해 조합될 수 있다.
  enum UIViewAutoresizing {
    UIViewAutoresizingNone = 0,
    UIViewAutoresizingFlexibleLeftMargin = 1 << 0,
    UIViewAutoresizingFlexibleWidth = 1 << 1,
  }
  // UIViewAutoresizingNone | UIViewAutoresizingFlexibleLeftMargin
  // 이런식으로 조합하여 사용 가능.
  • 상태 머신(state machine)을 정의하는 열거형을 switch 문에 사용할 때 default 문은 사용하지 않는게 최선이다.
    • 나중에 열거형에 새로운 상태를 추가했을 때 컴파일러는 새롭게 추가된 상태가 switch문에서 다루어지지 않는다는 도움이 되는 경고를 보내주기 때문이다.

프로퍼티를 이해하라

  • 프로퍼티의 속성
    • 원자성
      • 자동 생성된 접근자 메서드는 기본적으로 메서드가 원자적으로 동작하게 만드는데 필요한 락을 포함한다.
      • 속성을 nonatomic으로 하면 락이 사용되지 않는다.
      • atomic 속성이 없더라도 (nonatomic 명시가 안되어있으면 atomic) 명시적으로 원했을 경우를 대비해 컴파일 에러 없이 원자성이 적용 된다.
    • 읽기/쓰기
      • readwrite : 게터, 세터 모두 사용한다.
      • readonly : 게터만 사용된다. 프로퍼티를 외부에 읽기 전용으로 공개하고 싶을 때 사용할 수 있으나 클래스 확장 카테고리에서는 내부적으로 읽기/쓰기로 재 선언된다.
    • 메모리 관리 시맨틱
      • 프로퍼티는 데이터를 캡슐화한다. 그리고 그 데이터는 구체적인 소유권 시맨틱이 있어야 한다. 이는 오직 세터에만 해당한다.
      • assign : 세터는 CGFloat, NSInteger 같은 스칼라 타입에 사용하는 간단한 대입 연산이다.
      • strong : 프로퍼티가 값을 소유한다는 것을 의미한다. 새로운 값이 설정되면 먼저 그 값을 리테인하고 원래 값은 릴리즈한다. 그런 다음 리테인한 새 값을 설정.
      • weak : 프로퍼티가 값을 소유하지 않는 것을 나타낸다. 새로운 값이 설정되면 이 값은 리테인 하지 않을 뿐더러, 이전 값을 릴리즈 하지도 않는다.
        • assign과 비슷하지만 프로퍼티가 가리키던 객체는 언제든지 파괴되어 nil로 설정될 수 있다.
      • unsafe_unretained : assign과 같지만 타입이 소유하지 않는 관계인 객체 타입일 때 사용.
        • weak과 다르게 nil로 설정되지 않는다.
      • copy : strong과 비슷하게 값을 소유한다고 나타낸다. 그러나 값을 리테인하는 대신 복사한다.
        • 캡슐화가 잘되어있는 NSString*일 때 가끔 사용된다.
        • 세터로 전달된 값이 NSMutableString의 하위 클래스 인스턴스일 수 있는데, 이와 같이 가변 값이 가변 객체이면 프로퍼티에 설정된 후에 객체의 인지 없이 내용이 변경될 수 있기 때문에
        • 불변 복사본을 인자로 전달하면 문자열이 내부에서 변경되지 않음을 보장할 수 있다.
        • 가변일 수 있는 객체는 반드시 copy 속성을 가져야 한다.

인스턴스 변수에 내부에서 접근할 때는 직접 접근하라

  • 외부에서 객체의 인스턴스 변수에 접근할 때는 항상 프로퍼티를 사용해야 하지만,
  • 내부에서 인스턴스 변수에 접근하는 방법에 대해서는 항상 프로퍼티를 이용하라는 의견과, 인스턴스 변수에 직접 접근하라고 제안하는 두가지 의견이 분분.
  • 인스턴스 변수를 읽을 때는 직접 접근하고, 쓸때는 프로퍼티를 사용하는 방법을 추천

    • 인스턴스 변수에 직접 접근(_firstName)하는 방법이 확실히 빠르다.
    • 직접 접근 방식은 세터에 정의된 메모리 관리 시맨틱을 무시한다 - copy 방식이어도 직접 설정하면 복사본을 만들지 않을 것이다.
    • 인스턴스 변수에 직접 접근하면 키값 관찰(KVO) 알림이 발생하지 않을 것이다. - 될 수도 안될 수도 있다.
  • 주의할 점
    • 초기화 메서드 내에서 값을 설정할 때는 항상 인스턴스 변수에 직접 접근해야 한다. - 하위 클래스가 세터를 재정의할 수 있기 때문이다.
    • 프로퍼티가 지연 초기화(lazy initialization)를 사용할 때는 접근자 메서드(게터, 프로퍼티)를 이용해야 한다. - 게터를 한번도 호출하지 않고 인스턴스 변수에 직접 접근하면 초기화될 기회가 없기 때문

객체의 동등 비교를 이해하라

  • == 연산자는 포인터가 가리키는 객체를 비교하는 것이 아니라 포인터 자체를 비교하기 때문에 원하는 결과가 나오지 않는다.
  • objc는 컴파일 시간에 타입을 엄격하게 검사하지 않는다. - 그러므로 객체가 정확한 타입인지 꼭 확인해야 한다.
  • 전용 메서드를 만드는 것
    • isEqual과 hash 메서드를 구현하라. - 같은 객체는 항상 해시값이 같아야한다. 그러나 같은 해시 값을 가진 객체가 꼭 동일할 필요는 없다.
    • 깊은 동등성 vs 얕은 동등성 - 객체 전체를 비교할지 몇몇 필드만 비교할지 정해야 한다. 기본키만 비교해도 구분이 되는 경우 얕은 동등성을 갖는다 ; 꼭 필요한 프로퍼티만 비교해라.

클래스 클러스터 패턴을 사용해 구현의 상세 내용을 숨겨라

  • 클래스 클러스터 패턴은 간단한 퍼블릭 퍼사드 뒤편에 상세 구현을 숨길 때 사용할 수 있다.

연관 객체를 사용해 기존 클래스에 사용자 정의 데이터를 연관 지으라

objc_msgSend의 역할을 이해하라

  • 정적 바인딩은 호출되는 함수가 컴파일 시간에 정해지는 것을 의미한다.
  • 객체에 메세지가 전달되어 오브젝티브c의 메서드가 호출될 때는 동적 바인딩 방식을 사용한다.
    • 메세지에 호출될 함수를 정하는 것은 모두 실행 시간에 이루어지고 앱이 실행되는 도중 변경 될 수 있다.
  • 메세지는 리시버, 선택자, 파라미터들로 구성된다.
  • 호출할때 모든 메세지는 동적 메세지 디스패치 시스템을 통해 실행된다.

메세지 포워딩을 이해하라

  • 컴파일러가 메서드 구현이 존재하는지 여부를 알 수 있는 방법이 없다.
  • 동적 메서드 해결
    • 해석하지 못하는 메세지를 객체에 전달했을 때 가장 먼저 호출되는 메서드는 객체의 클래스에 있는 클래스 메서드이다.
    • 런타임에 메서드를 클래스에 추가하고 바로 사용할 때 이용된다.
    • 객체는 해석할 수 없는 선택자를 다루기 위해 다른 객체를 선언할 수 있다.
  • 완전 포워딩은 이전 두 방법으로 선택자를 처리할 수 없을때 호출.

불투명 메서드(소스를 볼 수 없는 메서드)를 디버깅할 때 메서드 스위즐링을 사용하라

  • 클래스의 선택자에 대한 메서드 구현은 실행 시간에 추가되거나 바뀔 수 있다.
  • 런타임이 메서드에 관여하는 것(즉 실행할 메서드 구현을 결정하는 행위)은 오직 디버깅할 때만 유용하다. 꼭 쓸 필요 없음.

클래스 객체가 무엇인지 이해하라

  • 제네릭 객체 타입인 id는 이미 그 자체가 포인터다.
  • 모든 객체의 첫번째 멤버 변수로 Class 타입의 변수를 포함한다.
  • 이 변수는 객체의 클래스를 정의하고 가끔 is a 포인터로 참조된다. 예를 들어 그 객체는 NSString’이다’와 같이 사용 된다(the object ‘is a’ NSString)
  • 클래스 계층도 살펴보기
    • 내성 메서드는 클래스 계층도를 알아내는데 사용할 수 있다. isMemberOfClass: 를 이용해 객체가 어떤 클래스의 인스턴스인지 알아내거나
    • isKindOfClass: 를 이용해 객체가 특정 클래스 또는 그 클래스의 상속 계층 클래스의 인스턴스인지 알아 낼 수 있다.
  • 클래스 객체와 == 연산자를 사용할 수 있다.
    • 클래스 객체는 싱글턴이기 때문. 클래스 인스턴스는 애플리케이션에서 오직 한개만 있다.

인터페이스와 API 설계

접두어를 사용해 네임스페이스 충돌을 피하라

  • 애플이 모든 두 글자 접두어 사용을 예약했다는 사실을 알아야 한다.
  • 반드시 세글자 접두어를 사용해야 한다.

지정 초기화 메서드를 만들라

  • 객체가 제대로 동작하기 위해 필요한 정보를 객체에 주는 초기화 메서드를 ‘지정 초기화 메서드(designated initializer)라고 한다.
  • 여러분 클래스의 지정 초기화 메서드를 구현하고 문서화하라. 다른 초기화 메서드들은 꼭 이 지정 메서드를 호출해야 한다.
  • 지정 초기화 메서드가 상위 클래스의 지정 초기화 메서드와 다르다면 상위 클래스의 지정 초기화 메서드를 꼭 재정의 하라.
  • 하위 클래스에서 사용하지 않을 상위 클래스의 초기화 메서드는 예외를 발생시키도록 재정의하라
    • objc에서 예뢰를 던지는 것은 심각한 에러를 의미한다.

description 메서드를 구현하라

  • description 메서드를 구현할 때 기본 구현처럼 클래스 이름과 포인터 주소도 같이 표시하는 것을 추천한다.
  • 사용자가 만든 description 메서드에서 사전을 활용하여 사전의 description 메서드가 포함하는 문자열을 반환하도록 하면 훨씬 간략한 description 메서드 구현이 가능하다.

가변 객체보다는 불변 객체를 사용하라

  • read-write 보다는 read-only
  • 친구 집합을 외부로 노출하는 일반적인 방법은 내부 가변 집합의 불변 집합 복사본을 만들고 그 복사본을 반환하는 readonly 속성의 프로퍼티를 만드는 것.

명확하고 일관된 작명법을 사용하라

  • 메서드와 변수 이름에 첫 문자를 소문자로 쓰는 카멜 표기법을 사용
  • 클래스는 첫문자 대문자, 2~3글자의 prefix
  • 긴 메서드 이름을 사용하는 것을 두려워하지 말라. 메서드 이름이 메서드가 하는 일을 정확히 표현할 만큼만 길게 만들라.
  • 단일 단어는 보통 프로퍼티로 사용 된다. - intValue
  • c 스타일 배열을 채우는 것 같이 외부 파라미터를 통해 값을 반환하는 메서드를 위해 get 접두어를 아껴두라

프라이빗 메서드 이름에 접두어를 사용하라

  • objc에서는 메서드를 프라이빗으로 선언할 수 있는 방법이 없다. 모든 객체는 모든 메세지에 응답할 수 있다(12). 그리고 실행 시간에 객체가 어떤 메세지에 응답하는지 살펴볼 수도 있다(14). 주어진 메세지에 대한 메서드 검색(lookup)은 실행 시간에 이루어진다(item 11) 그리고 누가 어떻게 언제 특정 메서드를 호출할 수 있는지에 대한 범위(scope)를 한정(limit) 짓는 방법이 없다.
  • 애플은 파라이빗 메서드를 _ 언더바로 쓴다. 따라서 애플은 접두어로 피하라고 명시해두었다.

objc 에러 모델을 이해하라

  • ARC가 예외에 안전하지 않다. 범위 끝에서 릴리스 되어야하는 객체가 예외가 발생하면 릴리스 되지 않는다는 것을 의미한다.
  • objc가 일반적인 에러를 처리할 때 쓰는 방법은, 메서드에서 에러가 발생하면 nil 또는 0을 반환하게 하거나 NSError를 사용하는 것.

iOS 개발 #iOS #Objective-C #coding #tip