본문 바로가기
도서/파이브 라인스 오브 코드

1부- 4장. 타입 코드 처리하기(단계 2/4)

by 패쓰킴 2023. 9. 22.
728x90
< 코드개선 4단계 >

1. 긴 함수 조각내기
2. 타입 코드 처리
3. 유사 코드 통합
4. 데이터 보호

 

4.1. 간단한 if문 리팩터링

if-else문의 사용을 지양하고 다형성 있는 코드를 작성 해야한다.

(if-else를 사용하면 코드에서 결정이 내려지는 지점을 고정하게 된다. 그럴 경우 if-else가 있는 위치 이후에서는 다른 변형을 도입할 수 없기 때문에 코드의 유연성이 떨어진다.)

 

신호등의 동작을 코드로 만든다고 생각해보자.

func run(input: String) -> Bool {
  if input == Light.Red {
    return false
  } else if input == Light.Yello {
    return false
  } else in input == Light.Green {
    return true
  }
}

이렇게 if-else로 다섯 줄 제한에 대한 원칙을 위반하게 된다.

또한 이 코드를 다른 여러 곳에서도 사용한다고 한다면 수정이 필요할 때 사용구 마다 수정을 해주어야 하는 불편함과 오류 발생 가능성이 증가한다.

그래서 여기에서는 인터페이스를 이용하여 코드를 수정할 수 있음을 제시한다.

 

protocol TrafficLight {
  func changeStatus(car: Car)
}

인터페이스 를 만들고

 

class Red: TrafficLight {
  func changeStatus(car: Car) {
    car.stop()
  }
}

class Yello: TrafficLight {
  func changeStatus(car: Car) {
    car.stop()
  }
}

class Green: TrafficLight {
  func changeStatus(car: Car) {
    car.move()
  }
}

각 구현체를 만들어 각자 해야하는 동작을 정의 해준다.

 

func run(trafficLight: TrafficLight, car: Car) {
  trafficLight.changeStatus(car: car)
}

이렇게 만들어 놓은 인터페이스와 구현체를 사용해주면 유지보수가 간편하며 가독성 있는 코드를 만들 수 있다.

이러한 진행 방식을 메서드 인라인화 라고 한다

 

메서드 인라인화란 메서드 추출과 반대의 의미라고 생각하면 되는데

메서드 추출은 하나의 책임을 갖고 다섯 줄을 제한 하기 위해 메서드로 분리하여 진행한다면,

메서드 인라인화는 이미 가독성이 좋고 짧은 코드 라인으로 이루어져 있는 메서드가 있다면 굳이 하나의 책임과 다섯 줄 제한을 위해 메서드를 분리하지 않는 것을 말한다.

메서드 인라인화가 불필요한 경우도 있다.

func handleInput() {
  while input.count > 0 {
    let current = input.pop()
    handleInput(current)
  }
}

func handleInput(input: Input) {
  input.handle()
}

위와 같은 코드는

func handleInput() {
  while input.count > 0 {
    let current = input.pop()
    current.handle()
  }
}

이렇게 굳이 메소드를 분리하지 않고 메서드 인라인화를 통해 가독성을 높일 수 있다.

또 다른 경우는, 메서드가 한 줄만 있는 경우이다.

let numberBits = 32

func absolute(x: Int) -> Int {
  return (x * x * numberBits) - (x * numberBits)
}

위와 같은 경우 이미 리턴 한줄로 속해 있는 메서드명이 무슨일을 하는지 이해하기 쉽게 정의 되어 있어 메소드를 분리하는 것은 오히려 가독성을 떨어뜨릴 수 있다.

 

4.2. 긴 if 문의 리팩터링

// 책에 나와 있는 코드를 일부 swift로 바꾼 예제입니다.
func drawMap(g: CanvasRenderingContext2D) {
  for y in 0 ..< map.count {
    for x in 0 ..< map[y].count {
      colorOfTile(g, x, y)
    }
  }
}

func colorOfTile(g: CanvasRenderingContext2D, x: Int, y: Int) {
  if map[y][x] == Tile.Flux {
    g.fillStyle = .red
  } else if map[y][x] == Tile.Unbreakable {
    g.fillStyle = .yello
  } else if map[y][x] == Tile.Stone {
    g.fillStyle = .black
  } else if map[y][x] == Tile.Box {
    g.fillStyle = .gray
  }
  ...
}

func remove(tile: Tile) {
  for y in 0 ..< map.count {
    for x in 0 ..< map[y].count {
      if map[y][x] == tile {
        map[y][x] = Tile.Stone
      }
    }
  }
}

위의 colorOfTile와 같은 긴 if-else 문이 있다고 해보자.

먼저 4.1에서의 리팩터링을 진행해본다.

// Tile2라는 임시 인터페이스 도입
protocol Tile2 {
  func isFlux() -> Bool
  func isUnbreakable() -> Bool
  func isStone() -> Bool
  ...
}

// 인터페이스 구체화
class Flux: Tile2 {
  func isFlux() -> Bool {
    return true
  }
  
  func isUnbreakable() -> Bool {
    return false
  }
  
  func isStone() -> Bool {
    return false
  }
  ...
}

class Unbreakable: Tile2 {
  ...
}

class Stone: Tile2 {
  ...
}
// Tile enum의 이름을 변경하여 오류가 발생하는 모든 위치 파악
enum Tile {
...
}
->
enum RawTile {
...
}
func drawMap(g: CanvasRenderingContext2D) {
  for y in 0 ..< map.count {
    for x in 0 ..< map[y].count {
      colorOfTile(g, x, y)
    }
  }
}

// 위의 colorOfTile에 반영
func colorOfTile(g: CanvasRenderingContext2D, x: Int, y: Int) {
  if map[y][x].isFlux() {
    g.fillStyle = .red
  } else if map[y][x].isUnbreakable() {
    g.fillStyle = .yello
  } else if map[y][x].isStone() {
    g.fillStyle = .black
  } 
  ...
}

func remove(tile: Tile) {
  for y in 0 ..< map.count {
    for x in 0 ..< map[y].count {
      if map[y][x] == tile {
        map[y][x] = Tile.Stone
      }
    }
  }
}

여전히 colorOfTile에 긴 if-else 문이 사용 되고 있다. 그리고 또 살펴봐야 할 부분은 remove 메소드 이다.

colorOfTile을 해결하기 전에 remove메소드를 살펴보면,

map의 특정 위치가 tile일 경우 값을 바꿔주는 작업을 하는데 이것은 Tile의 특정 인스턴스인지 확인하지 않고 유사한지만 확인한다. 즉, 파라미터를 봤을 때 모든 Tile이 들어올 수 있는 것으로 착각 할 수 있고 Tile의 인스턴스들은 배제 한 상태로 단순히 타입이 Tile인지만 비교하여 오류를 발생 시킬 수 있다. 이러한 문제를 제거하는 것을 일반성 제거라 한다.

 

일반성 제거

위의 remove의 경우 잘못하면 모든 타입의 타일을 제거할 수도 있다. 이러한 일반성은 유연성이 떨어지고 변경하기 어렵게 만들기 때문에 타입을 더 특정해야 한다. 

만약 remove를 사용했지만 Tile.Flux만 제거하는 의도를 가졌다 한다면 removeFlux라는 이름으로 변경하여 아래와 같이 remove 함수의 조건을 수정해 볼 수 있다.

// map[y][x] == tile를
// map[y][x] == tile.Flux로 변경
func remove(tile: Tile) {
  for y in 0 ..< map.count {
    for x in 0 ..< map[y].count {
      if map[y][x] == tile.Flux {
        map[y][x] = Tile.Stone
      }
    }
  }
}

// 그리고 이전과 같이 메서드 호출로 변경한다
// map[y][x] == tile.Flux를
// if map[y][x].isFlux로 변경
func removeFlux(tile: Tile) {
  for y in 0 ..< map.count {
    for x in 0 ..< map[y].count {
      if map[y][x].isFlux {
        map[y][x] = Tile.Stone
      }
    }
  }
}

이렇게 일반화를 줄이고 특정화한 함수를 도입하는 과정을 메서드 전문화 라고 한다

프로그래밍을 할 때엔 일반화 하고 재사용하려는 본능 때문에 책임이 흐려지고 다양한 위치에서 코드를 호출하여 문제가 발생할 수 있다. 메서드 전문화를 통해 더 적은 위치에서 호출되어 필요성이 없어지면 더 빨리 제거할 수 있도록 한다.

 

다시 돌아와서.

colorOfTile을 살펴보자

func colorOfTile(g: CanvasRenderingContext2D, x: Int, y: Int) {
  if map[y][x].isFlux() {
    g.fillStyle = .red
  } else if map[y][x].isUnbreakable() {
    g.fillStyle = .yello
  } else if map[y][x].isStone() {
    g.fillStyle = .black
  } 
  ...
}

이 if-else 문을 없애기 위해 switch문으로 수정할 수 있다고 생각할 수 있다.

그러나 switch는 버그를 발생시킬 수 있기 때문에 사용하지 않는 것이 좋다.

1. switch로 case를 분석할 때 모든 값에 대한 처리를 실행할 필요가 없다.

  : switch의 default를 이용하여 case에 해당 하지않아도 값을 처리 할 수 있다.

2. break 키워드를 만나기 전까지 케이스를 연속해서 실행하는 폴스루(fall-through) 로직이다.

 

switch를 사용하려면 기능을 default에 두지 않고 모든 케이스에 return을 지정하여 폴스루 문제를 해결해야 한다. 모든 값을 매핑했는지 확인할 수 있게 한다면 문제 발생 여지가 없어진다.

 

colorOfTile을 수정하려면 클래스로의 코드 이관을 통해 해결 할 수 있다.

func colorOfTile(g: CanvasRenderingContext2D, x: Int, y: Int) {
  map[y][x].color(g)
}

// 클래스로의 코드 이관 진행 ->
protocol Tile {
  func color(g: CanvasRenderingContext2D)
}

class Stone: Tile {
  func color(g: CanvasRenderingContext2D) { g.fillStyle = .red }
}
class Flux: Tile {
  func color(g: CanvasRenderingContext2D) { g.fillStyle = .blue }
}
...

 

4.3. 코드 중복 처리

코드 중복은 좋지 않다. 코드 복제는 변경이 필요할 때 수정 내용을 프로그램 전체에 반영하는 방식으로 변경해야 하기 때문이다.

중복을 처리할 때에는 인터페이스를 사용하자. 인터페이스를 사용하면 인터페이스를 상속한 각 클래스에 인터페이스를 구현해 주어야 하므로 개발자의 실수를 방지 할 수 있다. 

추상 클래스를 이용하여 중복을 처리 할 수도 있지만 단점이 너무 많다. 추상클래스는 일부 메서드에는 기본 구현을 제공하고 다른 메서드는 추상화 하기 위한 것이다. 중복을 줄이고 코드의 줄을 줄이고자 할 경우엔 편리하지만 기본 구현이 있기 때문에 컴파일러를 통해 재정의가 필요한 메서드인지 잡아낼 수 없다. 이 것을 커플링(결합)을 유발한다고 한다. 이러한 문제를 방지하려면 완전히 추상화된 메서드만 구현 해두는 것이 좋다.

728x90

댓글