위임 (Delegation, 공식문서를 바탕으로)

EenSung Kim·2023년 12월 21일
1

스위프트 공부

목록 보기
4/6

Delegation is a design pattern that enables a class or structure to hand off (or delegate) some of its responsibilities to an instance of another type.

Protocol 문서의 Delegation 파트


Intro, 위임이란?

공식문서에 따르면 위임(Delegation)이란 클래스 또는 구조체가 다른 유형의 인스턴스에 일부 책임을 넘기거나 위임할 수 있는 디자인 패턴입니다. (참고로 GoF 가 소개한 "디자인 패턴"과는 조금 결이 다르다고 하네요.) 쭈욱 읽다보면 이 문장을 더 잘 이해할 수 있을거라 믿고 진행해 보겠습니다.


주사위 게임 예시 코드 이해

공식문서에서는 주사위(Dice) 게임에 관한 코드를 예시로 들어 위임을 설명해주고 있습니다. 이 글에서도 예시를 그대로 가져와 함께 읽어갈 예정입니다. 단, Dice 는 구현되어 있지 않아서 이 부분만 제가 임시로 따로 구현했습니다.

// 임의로 구현한 Dice 구조체
struct Dice {
    var sides: Int
    
    func roll() -> Int {
        return Int.random(in: 1...sides)
    }
}

protocol DiceGame {
    var dice: Dice { get }
    func play()
}
protocol DiceGameDelegate: AnyObject {
    func gameDidStart(_ game: DiceGame)
    func game(_ game: DiceGame, didStartNewTurnWithDiceRoll diceRoll: Int)
    func gameDidEnd(_ game: DiceGame)
}

DiceGame 프로토콜과 DiceGameDelegate 라는 프로토콜의 예시가 있습니다. DiceGame 프로토콜부터 살펴보면, Dice 타입을 갖는 dice 라는 변수, 그리고 play() 라는 메서드를 요구하는 것을 알 수 있습니다.

다음은 DiceGameDelegate 입니다. Delegate 라는 표현이 있는 것으로 보아 이 부분이 위임 패턴과 연관성이 있다고 추측해볼 수 있겠죠. 크게 어려운 내용이 아니라서 요구사항은 쭈욱 읽어보시면 될 것 같은데요. 특히 Delegate 프로토콜 내부를 들여다보면, 모든 메서드가 DiceGame 타입의 매개변수를 갖는 것을 알 수 있습니다. (이 점은 전체 예제를 보고 다시 짚어보도록 하겠습니다.)

프로토콜은 청사진이라고 앞선 글에서 말씀드렸는데요. 청사진은 실체로 구현되어야 의미가 있겠죠. 공식문서에서는 다음 예시로 DiceGame 프로토콜을 준수하는 SnakesAndLadders 클래스를 아래와 같이 보여줍니다.

class SnakesAndLadders: DiceGame {
    let finalSquare = 25
    let dice = Dice(sides: 6)
    var square = 0
    var board: [Int]
    init() {
        board = Array(repeating: 0, count: finalSquare + 1)
        board[03] = +08; board[06] = +11; board[09] = +09; board[10] = +02
        board[14] = -10; board[19] = -11; board[22] = -02; board[24] = -08
    }
    weak var delegate: DiceGameDelegate?
    func play() {
        square = 0
        delegate?.gameDidStart(self)
        gameLoop: while square != finalSquare {
            let diceRoll = dice.roll()
            delegate?.game(self, didStartNewTurnWithDiceRoll: diceRoll)
            switch square + diceRoll {
            case finalSquare:
                break gameLoop
            case let newSquare where newSquare > finalSquare:
                continue gameLoop
            default:
                square += diceRoll
                square += board[square]
            }
        }
        delegate?.gameDidEnd(self)
    }
}

SnakesAndLadders 클래스는 프로퍼티로 dice 를, 메서드로 play() 를 갖기 때문에 DiceGame 프로토콜을 준수하고 있습니다. 그리고 delegate 라는 프로퍼티가 DiceGameDelegate 프로토콜을 타입으로 가지고 있습니다. 지난 글에서 다루지 않았지만 프로토콜은 Type 으로 활용이 가능합니다.

delegate 프로퍼티는 play() 메서드 안에서만 활용되고 있습니다. 옵셔널로 선언된 이유는 예시 코드에서 delegate 가 필수적인 요소가 아니기 때문입니다. 만약 delegate 가 nil 이라면, 옵셔널 체이닝으로 인해 에러 없이 다음 코드로 넘어가게 됩니다.

play() 메서드의 기타 내용은 우리가 잘 아는 일반적인 주사위 게임의 모습과 같습니다. 25라고 하는 마지막 지점(finalSquare)에 도달할 때까지 주사위를 굴려가며 게임을 반복합니다.

class DiceGameTracker: DiceGameDelegate {
    var numberOfTurns = 0
    func gameDidStart(_ game: DiceGame) {
        numberOfTurns = 0
        if game is SnakesAndLadders {
            print("Started a new game of Snakes and Ladders")
        }
        print("The game is using a \(game.dice.sides)-sided dice")
    }
    func game(_ game: DiceGame, didStartNewTurnWithDiceRoll diceRoll: Int) {
        numberOfTurns += 1
        print("Rolled a \(diceRoll)")
    }
    func gameDidEnd(_ game: DiceGame) {
        print("The game lasted for \(numberOfTurns) turns")
    }
}

DiceGameTracker 클래스는 DiceGameDelegate 프로토콜을 준수합니다. numberOfTurns 라는 프로퍼티와, 프로토콜 준수사항인 나머지 3개의 메서드의 실제 함수 부분이 구현되어 있습니다. 사실상 위임 패턴이 어떤 형태로 동작하는지를 알아볼 수 있는 최종 보스입니다. 아래는 메서드에 대한 간략한 요약입니다.

gameDidStart()

  • numberOfTurns 프로퍼티 0으로 초기화
  • game 이 SnakesAndLadders 라면 추가 메시지 print
  • 게임에서 쓰는 주사위가 몇개의 면을 가진 주사위인지 print

game()

  • numberOfTurns 프로퍼티 1씩 증가
  • 주사위의 결과 print

gameDidEnd()

  • 게임이 몇 번의 턴만에 끝났는지 print

위임(Delegation)

앞서서 "모든 메서드가 DiceGame 타입의 매개변수를 갖는 것을 알 수 있습니다. 이 부분을 다시 한번 짚어보겠다"고 했었는데요. 위임이 왜 필요한지를 이해할 수 있는 아주 중요한 과정이기 때문입니다.

// SnakesAndLadders 의 play() 메서드
...
delegate?.gameDidStart(self)
...
delegate?.game(self, didStartNewTurnWithDiceRoll: diceRoll)
...
delegate?.gameDidEnd(self)

SnakesAndLadders 게임은 play() 의 각 과정에서 delegate 의 메서드를 호출하면서 전달인자로 self 를 전달하고 있습니다. 여기에서 self 란 SnakesAndLadders 자신을 말하죠. 분명 위임은 "일부 책임을 넘기거나 위임"하는 것이라고 했는데 자기자신을 전달한다는 건 무슨 의미가 있을까요?

DiceGameTracker 에서 전달받은 game 을 이리저리 만져보면서 그 진가를 확인할 수 있었습니다. gameDidEnd 메서드를 아래와 같이 변경해 보겠습니다.

func gameDidEnd(_ game: DiceGame) {
        print("The game lasted for \(numberOfTurns) turns")
        print(game.finalSquare)  // 에러 메시지
    }

분명 SnakesAndLadders 쪽에서 전달할 때 자기 자신을 전달했기 때문에 finalSquare 에 접근할 수 있지 않을까 했지만 xcode는 아래와 같이 에러를 띄워줍니다. 'any DiceGame' 이라는 타입은 finalSquare 를 갖고 있지 않다는 것이죠.

"SnakesAndLadders 가 전부 전달되는 것이 아니다?"

만약 프로토콜 타입으로 전달한 것이 아니라면 DiceGameTracker 는 SnakesAndLadders 전체에 접근이 가능했을 겁니다. 특별히 접근제어자도 설정되어 있지 않으니까요. 하지만 그렇게 해서는 DiceGameTracker 에게 너무 많은 권한을 위임하는 것이 됩니다. 사실상 DiceGameTracker 가 모든 것을 할 수 있게 되니까요.

그대신 위임 패턴을 활용한 위 예시에서는 DiceGame 이라는 별도의 프로토콜 타입으로 서로를 전달하고 전달받습니다. 그래서 self 를 전달하고 전달받은 game 에 접근하더라도 DiceGame 프로토콜의 내용에만 접근이 가능한 겁니다. SnakesAndLadders 는 자신의 책임 중 일부만을 위임할 수 있게 되는 것이죠.

"game 에 접근할 때 xcode 가 보여주는 자동완성 예시"

Outro

공식문서의 첫 문장을 다시 한번 보겠습니다.

위임(Delegation)이란 클래스 또는 구조체가 다른 유형의 인스턴스에 일부 책임을 넘기거나 위임할 수 있는 디자인 패턴입니다.

네, 맞습니다. 이것이 가능한 이유는 위임 패턴에 프로토콜을 활용하기 때문입니다. 프로토콜 타입으로 객체를 주고받게 되면 self 를 전달하게 되더라도 프로토콜에서 준수사항으로 요구하는 내용(속성, 메서드)에만 접근할 수 있습니다. 그럼으로 자신의 책임 중 일부를 다른 객체에게 온전히 넘겨줄 수 있게 되는 것이죠.

extension 과 활용하면 위임하려는 부분만 떼어놓을 수 있어서 더욱 가독성이 올라갈 수 있겠다는 생각도 듭니다. delegation 과 직접적으로 연결된 것은 아닌 것 같지만, protocol 을 extension 과 함께 활용하는 방법들에 대해서 궁금하다면 공식문서 이후 내용들을 참고해 보시면 좋을 것 같습니다.

저는 뭔가 개념이 딱 와닿는 순간이야말로 뇌의 장기기억에 무언가를 저장하는 순간이라고 생각하는데요. 예시를 천천히 훑어보고 직접 이리저리 만져보면서 프로토콜을 활용한 위임 패턴의 내용을 장기기억에 저장할 수 있었습니다. 이 글이 여러분에게도 도움이 되면 좋겠습니다. 감사합니다!


아래는 전체복붙이 가능한 예제코드입니다.

struct Dice {
    var sides: Int
    
    func roll() -> Int {
        return Int.random(in: 1...sides)
    }
}

protocol DiceGame {
    var dice: Dice { get }
    func play()
}
protocol DiceGameDelegate: AnyObject {
    func gameDidStart(_ game: DiceGame)
    func game(_ game: DiceGame, didStartNewTurnWithDiceRoll diceRoll: Int)
    func gameDidEnd(_ game: DiceGame)
}

class SnakesAndLadders: DiceGame {
    let finalSquare = 25
    let dice = Dice(sides: 6)
    var square = 0
    var board: [Int]
    init() {
        board = Array(repeating: 0, count: finalSquare + 1)
        board[03] = +08; board[06] = +11; board[09] = +09; board[10] = +02
        board[14] = -10; board[19] = -11; board[22] = -02; board[24] = -08
    }
    weak var delegate: DiceGameDelegate?
    func play() {
        square = 0
        delegate?.gameDidStart(self)
        gameLoop: while square != finalSquare {
            let diceRoll = dice.roll()
            delegate?.game(self, didStartNewTurnWithDiceRoll: diceRoll)
            switch square + diceRoll {
            case finalSquare:
                break gameLoop
            case let newSquare where newSquare > finalSquare:
                continue gameLoop
            default:
                square += diceRoll
                square += board[square]
            }
        }
        delegate?.gameDidEnd(self)
    }
}

class DiceGameTracker: DiceGameDelegate {
    var numberOfTurns = 0
    func gameDidStart(_ game: DiceGame) {
        numberOfTurns = 0
        if game is SnakesAndLadders {
            print("Started a new game of Snakes and Ladders")
        }
        print("The game is using a \(game.dice.sides)-sided dice")
    }
    func game(_ game: DiceGame, didStartNewTurnWithDiceRoll diceRoll: Int) {
        numberOfTurns += 1
        print("Rolled a \(diceRoll)")
    }
    func gameDidEnd(_ game: DiceGame) {
        print("The game lasted for \(numberOfTurns) turns")
        print(game.finalSquare)  // 에러 메시지
    }
}

let tracker = DiceGameTracker()
let game = SnakesAndLadders()
game.delegate = tracker
game.play()
profile
iOS 개발자로 전직하기 위해 공부 중입니다.

0개의 댓글

관련 채용 정보