객체지향 프로그래밍 기초

jonghwan·2022년 9월 27일
0

멋쟁이사자처럼

목록 보기
14/28
post-thumbnail

Swift의 모든 타입은 셋 중에 하나

원래 있거나 아니면 내가 만들었거나

구조체(struct), 열거형(enum), 클래스(class)

객체란 무엇인가 ?

객체(또는 인스턴스)는 소프트웨어 앱을 구축하는 블록으로 쉽게 사용하고 재사용할 수 있는 독립적인 기능 모듈이다.

작업을 수행하기 위한 객체나 인스턴스에서 접근되고 호출되는 속성(property)과 함께 함수(method)로 객체가 구성된다.

객체를 구성하는 데이터 변수와 함수를 포괄적으로 클래스 멤버(class member)라고한다.

클래스란 무엇인가 ?

  • 세상의 모든 것은 객체라고 생각

  • 객체들 사이의 상호작용으로 프로그램 동작

  • 객체는 하나의 역할을 수행하는 메서드와 데이터의 묶음

  • 캡슐화(encapsulation)

  • 상속(inheritance)

  • 다형성(polymorphism)

빌딩이 건축되면 어떤 모양일지 정의하는 청사진이나 건축가의 도면처럼 클래스는 객체가 생성될 때의 모습을 정의한다.

예를 들어, 메서드들이 하게 될 일이 무엇이며 어떤 프로퍼티들이 존재할지 등을 정의한다.

클래스 선언하기

객체를 인스턴스화하기 전에 먼저 객체에 대한 '청사진'인 클래스를 정의해야 한다.

여기서는 은행 계좌에 대한 클래스를 생성해본다.

새로운 Swift 클래스를 선언할 때 새롭게 만드는 클래스가 어떤 부모 클래스(parent class)에서 파생되었는지를 지정하고 클래스에 포함할 프로퍼티와 메서드를 정의한다.

class 새로운 클래스 이름: 부모클래스 {
  // 프로퍼티
  // 인스턴스 메서드
  // 타입 메서드
}

선언부의 프로퍼티(property) 부분은 이 클래스 내에 포함될 변수와 상수를 정의하며, Swift에서 변수나 상수를 선언할 때와 동일한 방법을 선언한다.

인스턴스 메서드(instance method)타입 메서드(type method) 부분은 이 클래스에서 호출되는 메서드들과 클래스 인스턴스들을 정의한다.

본질적으로 이것들은 특정 작업을 수행하는 클래스 고유의 함수다.

class BankAccount{
  
}

BankAccount 클래스를 만들기 위해 다음과 같이 시작한다.

클래스에 인스턴스 프로퍼티 추가하기

클래스의 정의에 인스턴스가 만들어지면 갖게 될 프로퍼티를 추가하기

class BankAccount{
  var acoountBalance: Float = 0
  var accountNumber: Int = 0
}

메서드 정의하기

class BankAccount{
  var acoountBalance: Float = 0
  var accountNumber: Int = 0

  func displayBalance() {
    print("Number \(accountBalance)")
    print("Current balance is \(accountNumber)")
  }
}

객체지향 프로그래밍의 핵심 목적은 데이터 캡슐화(data encapsulation)라는 개념이다.

데이터 캡슐화의 기본 개념은 클래스에 저장되고 접근될 수 있는 데이터는 오직 해당 클래스 내에 정의된 메서드만을 통해서 된다는 것이다.

클래스 내의 캡슐화된 데이터를 프로퍼티(property) 또는 인스턴스 변수(instance variable)라고한다.

BankAccount 클래스의 인스턴스는 은행 계좌와 계좌에 남은 잔고 데이터를 저장하게 될 것이다.

프로퍼티는 Swift에서 변수나 상수를 선언할 때와 동일한 방법으로 선언된다.

displayBalance 메서드는 인스턴스 메서드이므로 class 키워드가 앞에 붙지 않았다.

class BankAccount{
  var acoountBalance: Float = 0
  var accountNumber: Int = 0

  func displayBalance() {
    print("Number \(accountBalance)")
    print("Current balance is \(accountNumber)")
  }

  class func getMaxBalance() -> Float {
    return 100000.00
  }
}

BankAccount 클래스를 설계할 때 이 클래스에 저장할 수 있는 최대 금액을 알기 위하여 클래스 자신의 타입 메서드를 호출할 수 있다면 유용할 것이다.

이를 통해 앱이 클래스 인스턴스를 처음 생성하는 과정을 거치지 않아도 BankAccount 클래스가 새로운 고객 정보를 저장할 수 있는지를 식별할 수 있게 해준다.

이 메서드의 이름을 getMaxBalance라고 할 것이다.

클래스 인스턴스 선언하기와 초기화하기

우리가 구조를 정의한 BankAccount 클래스를 가지고 어떤 작업을 하려면 클래스의 인스턴스를 생성해야 한다.

이 과정에서 첫번째 단계는 인스턴스에 대한 참조체를 저장할 변수를 선언하는 것이다.

다음 코드를 사용하면 우리의 BankAccount 클래스의 인스턴스가 생성될 것이며, account1이라는 변수를 통해 접근할 수 있게 된다.

class BankAccount{
  var acoountBalance: Float = 0
  var accountNumber: Int = 0

  func displayBalance() {
    print("Number \(accountBalance)")
    print("Current balance is \(accountNumber)")
  }

  class func getMaxBalance() -> Float {
    return 100000.00
  }
}
var account1: BankAccount = BankAccount()

클래스 인스턴스 초기화하기와 소멸하기

클래스는 생성하는 시점에 해야 할 초기화 작업이 있을 수 있다.

이 작업은 클래스의 init 메서드 안에 구현된다.

BankAccount 클래스에서는 새로운 클래스 인스턴스가 생성될 때 계좌번호와 잔액을 초기화할 것이다.

이를 위해 init 메서드를 작성한다.

class BankAccount{
  var accountBalance: Float = 0
  var accountNumber: Int = 0

  init(number: Int, balance: Float) {
    accountNumber = number
    accountBalance = balance
  }

  func displayBalance() {
    print("Number \(accountBalance)")
    print("Current balance is \(accountNumber)")
  }
}
var account1: BankAccount = BankAccount(
  number: 12312312,
  balance: 400.54
)

반대로, Swift 런타임 시스템에 의해 클래스 인스턴스가 없어지기 전에 해야 할 정리 작업은 클래스 안에 소멸자(deinitializer)를 구현하면 할 수 있다.

class BankAccount{
  var accountBalance: Float = 0
  var accountNumber: Int = 0

  init(number: Int, balance: Float) {
    accountNumber = number
    accountBalance = balance
  }
  
  deinit {
  	// 필요한 정리 작업을 여기서 수행한다.
  }

  func displayBalance() {
    print("Number \(accountBalance)")
    print("Current balance is \(accountNumber)")
  }
}

메서드 호출하기와 접근하기

우리는 지금까지

우리는 BankAccount라는 이름의 새로운 클래스를 만들었다.

이 클래스에 은행 계좌 번호와 잔액을 담기 위한 프로퍼티들을 선언하였고, 현재의 잔액을 표시하기 위한 메서드도 선언하였다.

새로운 클래스의 인스턴스를 생성하고 초기화하는 데 필요한 작업도 확인했다.

이제 해야할 것은

우리가 만든 클래스에 있는 인스턴스 메서드를 어떻게 호출하며

프로퍼티에는 어떻게 접근하는가

점 표기법

점 표기법은 클래스 인스턴스 다음에 점을 찍고 그 뒤에 프로퍼티나 메서드 이름을 써서 인스턴스 변수에 접근하거나 인스턴스 메서드를 호출하게 된다.

클래스인스턴스.프로퍼티명
클래스인스턴스.인스턴스메서드()

예를 들어, accountBalance 인스턴스 변수의 현재 값을 얻으려면 다음과 같이 한다.

var balance1 = account1.accountBalance

점 표기법을 사용하면 인스턴스 프로퍼티에 값을 설정할 수도 있다.

account1.accountBalance = 6789.98

클래스 인스턴스의 메서들 호출할 때도 같은 방법을 사용한다.

예를 들어, BankAccount 클래스의 인스턴스에서 displayBalance 메서드를 호출하려면 다음과 같이 한다.

account.displayBalance()

타입 메서드 역시 점 표기법을 이용하여 출력된다.

다만, 주의할 점은 클래스 인스턴스가 아니라 클래스에서 호출되어야 한다.

클래스이름.타입메서드()

예를 들어, 앞에서 선언한 타입 메서드 getMaxBalance를 호출한다면 BankAccount 클래스가 참조된다.

var maxAllowed = BankAccount.getMaxBlance()

저장 프로퍼티와 연산 프로퍼티

Swift의 클래스 프로퍼티는 저장 프로퍼티(stored property)연산 프로퍼티(computed property)로 나뉜다.

저장 프로퍼티는 상수나 변수에 담기는 값이다.

BankAccount 예제에서 계좌 이름과 번호 프로퍼티 모두는 저장 프로퍼티다.

반면, 연산 프로퍼티는 프로퍼티에 값을 설정하거나 가져오는 시점에서 어떤 계산이나 로직에 따라 처리된 값이다.

연산 프로퍼티 게터(getter)를 생성하고 선택적으로 세터(setter) 메서드를 생성하며, 연산을 수행할 코드가 포함된다.

예를 들어, BankAccount 클래스에 은행 수수료를 뺀 현재 잔액을 담는 프로퍼티가 추가로 필요하다고 해보자.

저장 프로퍼티를 이용하는 대신에 값에 대한 요청이 있을 때마다 계산되는 연산 프로퍼티를 이용하는 것이 더 좋겠다.

class BankAccount{
  var accountBalance: Float = 0
  var accountNumber: Int = 0
  let fees: Float = 25.00

  var balanceLessFees: Float {
    get {
      return accountBalance - fees
    }
  }

  init(number: Int, balance: Float) {
    accountNumber = number
    accountBalance = balance
  }
.
.
.
}

이제 BankAccount 클래스는 이렇게 수정된다.

현재의 잔액에서 수수료를 빼는 연산 프로퍼티를 반환하는 게터를 추가했다.

class BankAccount{
  var accountBalance: Float = 0
  var accountNumber: Int = 0
  let fees: Float = 25.00

  var balanceLessFees: Float {
    get {
      return accountBalance - fees
    }

    set {
      accountBalance = newValue - fees
    }
  }

  init(number: Int, balance: Float) {
    accountNumber = number
    accountBalance = balance
  }
.
.
.
}

선택 사항인 세터역시 거의 같은 방법으로 선언할 수 있다.

새롭게 선언한 세터는 부동 소수점 값을 매개변수로 받아서 수수료를 뺀 결과를 프로퍼티에 할당한다.

점 표시법을 이용하여 접근하는 저장 프로퍼티처럼 연산 프로퍼티도 같은 방법으로 접근할 수 있다.

현재의 잔액에서 수수료를 뺀 값을 얻는 코드와 새로운 값을 설정하는 코드는 다음과 같다.

var account1 = account1.balanceLessFees
account1.balanceLessFees = 12123.12

지연 저장 프로퍼티

프로퍼티를 초기화하는 여러 방법이 있는데, 가장 기본적인 방법은 다음과 같이 직접 할당하는 것이다.

var myProperty = 10

다른 방법으로는 초기화 작업에서 프로퍼티에 값을 할당하는 것이다.

class MyClass {
  let title: String
  
  init(title: String) {
    self.title = title
  }
}

좀 더 복잡한 방법으로는 클로저를 이용하여 프로퍼티를 초기화할 수도 있다.

class MyClass {

  var myProperty: String = {
    var result = resourceIntensiveTask()
    result = processData(data: result)
    return result
  }
}

복잡한 클로저의 경우는 초기화 작업이 리소스와 시간을 많이 사용하게 될 수 있다.

클로저를 이용하여 선언하면 해당 프로퍼티가 코드 내에서 실제로 사용되는지와 상관없이 클래스의 인스턴스가 생성될 때마다 초기화 작업이 수행될 것이다.

예를 들어, 데이터베이스로부터 데이터를 가져오거나 사용자로부터 사용자 입력을 얻게 될 때, 실행 프로세스의 후반부 단계까지 프로퍼티에 값이 할당되었는지 모르게 되는 상황이 생길 수 있다.

이러한 상황에서의 더 효율적인 방법은 프로퍼티를 최초로 접근할 때만 초기화 작업을 하는 것이다.

다행이도 이 작업은 lazy로 프로퍼티를 선언하면 된다.

class MyClass {

  lazy var myProperty: String = {
    var result = resourceIntensiveTask()
    result = processData(data: result)
    return result
  }()
}

self 사용하기

객체지향 프로그래밍 언어에 익숙한 프로그래머라면 현재의 클래스 인스턴스에 속한 메서드나 프로퍼티를 가리킬 때 프로퍼티와 메서드 앞에 self를 붙이는 습관이 있을 것이다.

Swift 프로그래밍 언어 역시 그렇게 사용하기 위한 self 프로퍼티 타입을 제공한다.

class MyClass {
  var myNumber = 1

  func addTen() {
    self.myNumber += 10
  }
}

예제의 코드에서 self는 MyClass 클래스 인스턴스에 속한 myNumber라는 이름의 프로퍼티를 참조한다는 것을 컴파일러에게 알려준다.

하지만 대부분의 경우 Swift로 프로그래밍할 때는 self를 사용할 필요가 없다.

왜냐하면 self는 프로퍼티와 메서드에 대한 참조를 디폴트로 간주하기 때문이다.

애플의 The Swift Programming Language 공식문서에는 self를 자주 쓸 필요가 없다고 적혀있다.

class MyClass {
  var myNumber = 1

  func addTen() {
    self.myNumber += 10
  }
}

        |
        V
       
class MyClass {
  var myNumber = 1

  func addTen() {
    myNumber += 10
  }
}

대부분의 경우는 Swift에서 self는 선택적으로 사용된다.

self를 사용해야 하는 상황은 프로퍼티나 메서드를 클로저 표현식 내에서 참조할 경우다.

예를 들어, 다음의 클로저 표현식에서는 반드시 self를 사용해야 한다.

document?.openWithCompletionHandler

또한, 함수의 매개변수가 클래스 프로퍼티와 동일한 이름을 가질 경우와 같이 코드의 모호성을 해결하기 위하여 self를 사용해야 한다.

예를 들어, 다음 코드에서 첫 번째 print 구문은 myNumber 매개변수를 통해 함수에 전달된 값을 출력하겠지만,

두번째 print 구문은 myNumber라는 클래스 프로퍼티에 할당된 값(여기서는 10)을 출력한다.

class MyClass {
var myNumber = 10 // 클래스 프로퍼티

func addTen(myNumber: Int) {
print(myNumber)       // 함수의 매개변수 값을 출력
print(self.myNumber)  // 클래스 프로퍼티 값을 출력

self 사용 여부는 프로그래머의 취향에 달렸다.

프로퍼티나 메서드를 참조할 때 self를 사용하기 좋아하는 프로그래머들도 자신이 원하는 방식으로 스위프트 코딩을 계속해갈 수 있다.

하지만 Swift 프로그래밍을 할 때 self를 반드시 사용해야 하는 것은 아니다.

프로토콜 이해하기

클래스가 구조적으로 올바르게 되기 위하여 Swift 클래스가 반드시 따라야 할 특징 규칙은 기본적으로 없다.

하지만, 다른 클래스와 함께 작업을 해야 할 때는 특정 조건에 맞춰야 한다.

iOS SDK의 다양한 프레임워크와 함께 동작하는 클래스를 만들 때는 더욱 그러하다.

클래스가 충족해야 하는 최소한의 요구사항을 정의하는 규칙들의 집합을 프로토콜(protocol)이라고 한다.

프로토콜은 protocol 키워드를 이용하여 선언되며, 클래스가 반드시 포함해야 하는 메서드와 프로퍼티를 정의한다.

어떤 클래스가 프로토콜을 채택했으나 모든 프로토콜의 요구사항을 충족하지 않는다면, 그 클래스가 해당 프로토콜을 따르지 않는다는 에러가 발생하게 된다.

다음의 프로토콜 선언부를 보면, 이 프로토콜을 채택하는 클래스는 이름의 읽을 수 있는 문자열 값에 대한 name이라는 프로퍼티와 매개변수를 받지 않고 문자열 값을 반환하는 buildMessage() 메서드를 반드시 포함해야 한다.

protocol MessageBuilder {
  var name: String {get}
  func buildMessage() -> String
}

// MessageBuilder 프로토콜을 채용하는 클래스 선언
class MyClass: MessageBuilder {

}

안타깝게도, 현재 구현된 상태의 MyClass는 컴파일 에러가 날 것이다.
왜냐하면 프로토콜이 요구하는 name 변수와 buildMessage() 메서드가 없기 때문이다.

프로토콜을 준수하기 위해서 클래스는 다음과 같이 모든 조건을 충족해야 한다.

protocol MessageBuilder {
  var name: String {get}
  func buildMessage() -> String
}

class MyClass: MessageBuilder {

  var name: String
  
  init(name: String) {
    self.name = name
  }

  func buildMessage() -> String {
    "Hello" + name
  }
}

0개의 댓글