Search
Duplicate

(Swift) 확장성 있는 코드 만들기 연습 + 각 코드의 역할에 대해 생각하기

간단소개
팔만코딩경 컨트리뷰터
ContributorNotionAccount
주제 / 분류
Swift
Scrap
태그
9 more properties
코드리뷰 멘토링을 받으면서, 내 코드에 아직 하드코딩으로 처리되어 있는 부분이 많다는 것을 느꼈다. 이번 기회에 오늘 피드백 받은 내용을 리팩토링 하면서 확장성있는 코드를 만드는 연습을 해보기로 했다.
기록으로 안남기면 까먹을 것 같아서..! go go!

확장성 높은 코드 만들기

주사위의 결과값을 sum 이라는 프로퍼티에 계속 더하는 간단한 반복문이다. 하지만 만약 주사위를 던져서 나올 수 있는 범위가 1에서 8까지라면? 20까지라면?? 다시 1...6 으로 표현한 곳을 찾아서 일일이 변경을 해주어야 한다.
// 기존 코드 for _ in 1...diceCount { sum += Int.random(in: 1...6) }
Swift
복사
이렇게 직접적으로 숫자를 사용하기보단, 프로퍼티를 활용해서 조금 더 재사용 가능한 코드를 만들 수 있다.
// 수정한 코드 private var rangeOfDiceValue: ClosedRange<Int> = 1...6 for _ in 1...diceCount { sum += Int.random(in: rangeOfDiceValue) }
Swift
복사

의존성 주입!

하지만 아직도 뭔가 부족한 느낌이 든다! 만약 player마다 나올수 있는 주사위의 범위가 다르다면?? 이러한 경우는 의존성 주입을 활용해서 해결할 수 있다.
// 수정한 코드 private var rangeOfDiceValue: ClosedRange<Int> for _ in 1...diceCount { sum += Int.random(in: rangeOfDiceValue) } init( ... , rangeOfDiceValue: ClosedRange<Int> = 1...6) { ... self.rangeOfDiceValue = rangeOfDiceValue }
Swift
복사
이렇게 의존성 주입을 활용하면, 조금 더 유연한 코드를 만들 수 있다. 자세한 내용이 궁금하다면 (Swift) Dependency Injection, 의존성 주입이란? (feat. DIP)

Player 모델의 역할과 접근 권한

처음에 코드를 구현할 때에는, 모델이면 딱 데이터(프로퍼티)만 들고 있어야 하는게 아닐까? 라는 생각을 가지고 있었다. 그래서 기존의 코드를 보면 모든 모델들이 정말 데이터만 가지고 있는 방식으로 구현되어있다.
// 기존 코드 struct Player { var id: String var name: String var diceCount: Int }
Swift
복사
하지만, 멘토링을 진행하고 나서 해당 모델이 해야되는 동작이 있다면, 모델 안에서 구현해야 한다고 깨달았다! 사실 java에서 class를 활용한 코드를 작성할 때에는 아무 생각없이 잘 작성했었는데, swift에서 struct로 모델을 구현하는 연습을 하다보니 struct라는 단어때문에 조금 고정관념에 사로잡혀 있었던 것 같다!!
얼른 고정관념에서 벗어나기 위해서 바로 리펙토링을 하기로 하였다! gettersetter를 구현해 주었고, takeDice(), sumOfRollDice()와 같이 외부에서 구현했던 로직들을 모델 내부에서 구현해서 player모델 코드의 재사용성을 높여 주었다! 또, 외부의 불필요한 로직이 줄어들어 ViewController에서의 코드가 더 간결해지고 가독성이 높아졌다.
// 수정한 코드 struct Player { private var id: String private var name: String private var diceCount: Int private var defaultDiceCount: Int private var rangeOfDiceValue: ClosedRange<Int> mutating func resetDiceCount() { self.diceCount = defaultDiceCount } func getDiceCount() -> Int { diceCount } mutating func takeDice(from player: Player) { self.diceCount += player.diceCount } mutating func setName(_ name: String) { self.name = name } func getName() -> String { name } func getId() -> String { id } func sumOfRollDice() -> Int { var sum: Int = 0 for _ in 1...diceCount { sum += Int.random(in: rangeOfDiceValue) } return sum } init(id: String, name: String, diceCount: Int, result: Int? = nil, defaultDiceCount: Int = 1, rangeOfDiceValue: ClosedRange<Int> = 1...6) { self.id = id self.name = name self.diceCount = diceCount self.defaultDiceCount = defaultDiceCount self.rangeOfDiceValue = rangeOfDiceValue } }
Swift
복사
그리고 접근제한자를 추가해 외부에서 내부 프로퍼티에 직접 접근할 수 없도록 설계하였다. 내부 프로퍼티의 값을 가져오거나 할당할 때에는 getter, setter를 사용한다.
// 외부에서 직접 접근해서 사용핟던 기존의 코드 userCountLabel.text = user.diceCount.description opponentCountLabel.text = opponent.diceCount.description // getter를 활용한 수정한 코드 userCountLabel.text = user.getDiceCount().description opponentCountLabel.text = opponent.getDiceCount().description
Swift
복사

추가!

단순히 값을 가져오기만 하는 메서드의 경우 읽기전용 프로퍼티를 활용할 수 있다. 예를들어 아래와 같이 id는 getId() 메서드를 통해서 값을 가져오는 것이 목적이라면 연산 프로퍼티를 활용해보자!
func getId() -> String { id }
Swift
복사
아래와 같이 간단하게 읽기전용 프로퍼티를 활용하면 더 직관적으로 구현할 수 있다.
var getId: String { id }
Swift
복사

암시적 추출 옵션의 활용

가장 고민했던 부분 중 하나인 옵셔널에 대한 부분도 수정하였다. 아래 뷰컨트롤러에서는 이전 뷰컨트롤러의 prepare()에서 useropponent를 받아오기 때문에, useropponent를 옵셔널로 지정해 주었다. 하지만 멘토링 과정에서 옵셔널은 정말 없어도 되는 프로퍼티에 대해서 사용하는 것이 좋다는 피드백을 들었다. 그리고 아래 코드처럼 무조건 있어야 하는 경우에는 암시적 추출 옵션을 사용한다는 사실을 배웠다!
마치 UILabel, UIButton처럼 말이다!
그래서 기존에 옵셔널로 user을 선언하고 user에 접근하는 모든 곳에서 forced unwrapping을 사용하던 코드를 완전 싹 뜯어 고쳐서 조금더 디버깅에 용이한 코드를 작성해 보았다!
// 기존 코드 ... private var user: Player? private var opponent: Player? ... override func viewDidLoad() { super.viewDidLoad() guard opponent != nil else { fatalError("opponent is nil") } guard user != nil else { fatalError("user is nil") } } ... private var currentGameState: State { get { gameState } set { gameState = newValue switch gameState { case .rolled: gamePlayButton.setTitle("Again!", for: .normal) case .wait: currentUserResult = 0 currentOpponentResult = 0 userCountLabel.text = user!.diceCount.description opponentCountLabel.text = opponent!.diceCount.description gamePlayButton.setTitle("Roll the Dice!", for: .normal) } } }
Swift
복사
// 수정한 코드 ... private var user: Player! private var opponent: Player! ... override func viewDidLoad() { super.viewDidLoad() guard let _ = opponent else { fatalError("opponent is nil") } guard let _ = user else { fatalError("user is nil") } } ... private var currentGameState: State { get { gameState } set { gameState = newValue switch gameState { case .rolled: gamePlayButton.setTitle("Again!", for: .normal) case .wait: currentUserResult = 0 currentOpponentResult = 0 userCountLabel.text = user.getDiceCount().description opponentCountLabel.text = opponent.getDiceCount().description gamePlayButton.setTitle("Roll the Dice!", for: .normal) } } }
Swift
복사