🔥 Delegation 패턴

654자
7분

Delegation 은 클래스나 구조체가 자신의 책임 중 일부를 다른 타입의 인스턴스에 넘기는(delegate) 디자인 패턴입니다. 이 패턴은 위임된 책임을 캡슐화하는 프로토콜을 정의함으로써 구현되는데, 이를 통해 프로토콜을 준수하는 타입(delegate라고 함)이 위임받은 기능을 제공할 것임을 보장합니다. Delegation은 특정 작업에 응답하거나 기본 타입을 알 필요 없이 외부 소스에서 데이터를 검색하는 데 사용될 수 있습니다.

아래 예제는 주사위 게임과 게임 진행 상황을 추적하는 delegate를 위한 중첩 프로토콜을 정의합니다:

class DiceGame {
    let sides: Int
    let generator = LinearCongruentialGenerator()
    weak var delegate: Delegate?
 
    init(sides: Int) {
        self.sides = sides
    }
 
    func roll() -> Int {
        return Int(generator.random() * Double(sides)) + 1
    }
 
    func play(rounds: Int) {
        delegate?.gameDidStart(self)
        for round in 1...rounds {
            let player1 = roll()
            let player2 = roll()
            if player1 == player2 {
                delegate?.game(self, didEndRound: round, winner: nil)
            } else if player1 > player2 {
                delegate?.game(self, didEndRound: round, winner: 1)
            } else {
                delegate?.game(self, didEndRound: round, winner: 2)
            }
        }
        delegate?.gameDidEnd(self)
    }
 
    protocol Delegate: AnyObject {
        func gameDidStart(_ game: DiceGame)
        func game(_ game: DiceGame, didEndRound round: Int, winner: Int?)
        func gameDidEnd(_ game: DiceGame)
    }
}
swift

DiceGame 클래스는 각 플레이어가 주사위를 굴리고 가장 높은 숫자를 굴린 플레이어가 라운드에서 이기는 게임을 구현합니다. 이 게임에서는 이전 예제에서 사용된 선형 합동 생성기를 이용해 무작위 숫자를 생성합니다.

DiceGame.Delegate 프로토콜은 주사위 게임의 진행 상황을 추적하기 위해 채택될 수 있습니다. DiceGame.Delegate 프로토콜은 항상 주사위 게임의 맥락에서 사용되므로 DiceGame 클래스 내부에 중첩되어 있습니다. 프로토콜은 외부 선언이 제네릭이 아닌 한 구조체나 클래스와 같은 타입 선언 내부에 중첩될 수 있습니다. 타입 중첩에 대한 자세한 내용은 중첩 타입을 참조하세요.

강한 참조 순환을 방지하기 위해 delegate는 약한 참조로 선언됩니다. 약한 참조에 대한 자세한 내용은 클래스 인스턴스 간의 강한 참조 순환을 참조하세요. 프로토콜을 클래스 전용으로 표시하면 DiceGame 클래스가 delegate에 약한 참조를 사용해야 함을 선언할 수 있습니다. 클래스 전용 프로토콜에서 설명한 대로 클래스 전용 프로토콜은 AnyObject에서 상속함으로써 표시됩니다.

DiceGame.Delegate는 게임 진행 상황을 추적하기 위한 세 가지 메서드를 제공합니다. 이 세 가지 메서드는 위의 play(rounds:) 메서드에서 게임 로직에 통합됩니다. DiceGame 클래스는 새 게임이 시작될 때, 새 턴이 시작될 때, 또는 게임이 끝날 때 delegate 메서드를 호출합니다.

delegate 속성이 optional DiceGame.Delegate이기 때문에, play(rounds:) 메서드는 옵셔널 체이닝에서 설명한 대로 delegate에서 메서드를 호출할 때마다 옵셔널 체이닝을 사용합니다. delegate 속성이 nil이면 이러한 delegate 호출은 무시됩니다. delegate 속성이 nil이 아니면 delegate 메서드가 호출되고 DiceGame 인스턴스가 매개변수로 전달됩니다.

다음 예제는 DiceGame.Delegate 프로토콜을 채택하는 DiceGameTracker라는 클래스를 보여줍니다:

class DiceGameTracker: DiceGame.Delegate {
    var playerScore1 = 0
    var playerScore2 = 0
 
    func gameDidStart(_ game: DiceGame) {
        print("Started a new game")
        playerScore1 = 0
        playerScore2 = 0
    }
 
    func game(_ game: DiceGame, didEndRound round: Int, winner: Int?) {
        switch winner {
            case 1:
                playerScore1 += 1
                print("Player 1 won round \(round)")
            case 2:
                playerScore2 += 1
                print("Player 2 won round \(round)")
            default:
                print("The round was a draw")
        }
    }
 
    func gameDidEnd(_ game: DiceGame) {
        if playerScore1 == playerScore2 {
            print("The game ended in a draw.")
        } else if playerScore1 > playerScore2 {
            print("Player 1 won!")
        } else {
            print("Player 2 won!")
        }
    }
}
swift

DiceGameTracker 클래스는 DiceGame.Delegate 프로토콜에서 요구하는 세 가지 메서드를 모두 구현합니다. 새 게임이 시작될 때 두 플레이어의 점수를 0으로 만들고, 각 라운드가 끝날 때 점수를 업데이트하며, 게임이 끝날 때 승자를 발표하기 위해 이러한 메서드를 사용합니다.

아래는 DiceGameDiceGameTracker가 실제로 어떻게 동작하는지 보여줍니다:

let tracker = DiceGameTracker()
let game = DiceGame(sides: 6)
game.delegate = tracker
game.play(rounds: 3)
// Started a new game
// Player 2 won round 1
// Player 2 won round 2
// Player 1 won round 3
// Player 2 won!
swift

위 코드를 실행하면 아래와 같은 결과를 볼 수 있습니다:

Started a new game
Player 2 won round 1
Player 2 won round 2
Player 1 won round 3
Player 2 won!
text

Delegation 패턴은 코드를 모듈화하고 재사용성을 높이는 데 매우 유용합니다. 이 패턴을 사용하면 클래스나 구조체가 자신의 책임 중 일부를 다른 객체에 위임할 수 있으므로, 코드의 응집도를 높이고 결합도를 낮출 수 있죠. 또한 delegate 객체의 동작을 쉽게 사용자 정의할 수 있어 유연성과 확장성도 향상시킵니다.

앞으로도 Swift에서 Delegation 패턴을 활용하여 더욱 우아하고 효율적인 코드를 작성할 수 있기를 바랍니다! 😊