본문 바로가기
Five Lines Of Code

[iOS] 파이브 라인즈 오브 코드: 4장 요약 및 정리 (효율적인 코드 작성을 위한 리팩터링 기법

by Jimmy_iOS 2024. 7. 24.

효율적인 코드 작성을 위한 리팩터링 기법

들어가기 전에

여러분, 코드 작성하면서 “이렇게 하면 유지보수가 쉬울까?” 고민해본 적 있으시죠?

"파이브 라인즈 오브 코드"의 4장은 이런 고민을 해결할 수 있는 팁들을 가득 담고 있어요.

제가 경험했던 코드 문제들과 함께 책에서 제시한 해결책을 나눠볼게요.


4.1 간단한 if 문 리팩터링

if 문에서 else를 사용하지 말 것

여러분도 if-else 문을 작성하다가 코드가 너무 중첩되어서 복잡해진 경험 있으신가요?

저도 그런 경험이 많아요. 예를 들어 나이 검사를 할 때:

 

if age >= 18 {
    print("Welcome!")
} else {
    print("You are not allowed.")
}

 

이런 코드가 많아지면 가독성이 떨어지고 버그가 발생할 가능성이 높아집니다.

이때, 책에서는 guard 문을 사용하는 방법을 추천합니다.

 

func checkAge(_ age: Int) {
    guard age >= 18 else {
        print("You are not allowed.")
        return
    }
    print("Welcome!")
}

 

이렇게 하면, 코드가 훨씬 깔끔해지고, 실패 조건을 먼저 처리하므로 성공 케이스에 더 집중할 수 있습니다.

 

규칙 적용

 

사용자 입력을 검증할 때, 유효하지 않은 경우를 먼저 처리하면 코드가 더 명확해집니다.

 

func validateUserInput(input: String?) {
    guard let input = input, !input.isEmpty else {
        print("Invalid input.")
        return
    }
    print("Valid input: \(input)")
}

 

이렇게 하면 실패 조건을 먼저 처리하여, 주 로직의 가독성을 높일 수 있습니다.

 

리팩터링 패턴: 클래스로 타입 코드 대체

 

enum을 사용해서 타입을 정의하다 보면 확장성이 떨어져서 불편했던 적이 있나요?

예를 들어, AnimalTypeenum으로 정의하고 각 타입별로 다른 동작을 구현할 때 말이죠.

 

enum AnimalType {
    case dog
    case cat
    case bird
}

class Animal {
    var type: AnimalType

    init(type: AnimalType) {
        self.type = type
    }

    func makeSound() {
        switch type {
        case .dog:
            print("Bark")
        case .cat:
            print("Meow")
        case .bird:
            print("Chirp")
        }
    }
}

let myDog = Animal(type: .dog)
myDog.makeSound() // 출력: Bark

 

이 코드를 클래스로 바꿔보면 훨씬 유연해집니다.

 

protocol SoundBehavior {
    func makeSound()
}

class BarkSound: SoundBehavior {
    func makeSound() {
        print("Bark")
    }
}

class MeowSound: SoundBehavior {
    func makeSound() {
        print("Meow")
    }
}

class ChirpSound: SoundBehavior {
    func makeSound() {
        print("Chirp")
    }
}

class Animal {
    private var soundBehavior: SoundBehavior

    init(soundBehavior: SoundBehavior) {
        self.soundBehavior = soundBehavior
    }

    func performSound() {
        soundBehavior.makeSound()
    }
}

let dog = Animal(soundBehavior: BarkSound())
dog.performSound() // 출력: Bark

 

이제 Animal 클래스는 더 유연해졌고, 새로운 동물을 추가하기도 쉬워졌죠.

새로운 동물 소리를 추가하려면 SoundBehavior를 구현하는 새로운 클래스를 만들기만 하면 됩니다.

 


4.2 긴 if 문의 리팩터링

일반성 제거

 

긴 if 문을 작성하다 보면 코드가 너무 복잡해져서 가독성이 떨어지는 경우가 많죠?

예를 들어, 주문 타입에 따라 다른 처리를 해야 할 때:

 

func processOrder(type: String) {
    if type == "Online" {
        print("Processing online order")
    } else if type == "InStore" {
        print("Processing in-store order")
    } else {
        print("Unknown order type")
    }
}

 

이런 코드를 리팩터링하는 방법을 소개합니다.

 

메서드 전문화

func processOnlineOrder() {
    print("Processing online order")
}

func processInStoreOrder() {
    print("Processing in-store order")
}

func processOrder(type: String) {
    switch type {
    case "Online":
        processOnlineOrder()
    case "InStore":
        processInStoreOrder()
    default:
        print("Unknown order type")
    }
}

 

이렇게 하면 각 메서드가 독립적으로 동작하여 가독성과 유지보수성이 높아집니다.

switch를 사용하지 말 것

switch 문 대신 다형성을 사용하면 조건문을 더 깔끔하게 대체할 수 있습니다.

 

protocol Order {
    func process()
}

class OnlineOrder: Order {
    func process() {
        print("Processing online order")
    }
}

class InStoreOrder: Order {
    func process() {
        print("Processing in-store order")
    }
}

let orders: [Order] = [OnlineOrder(), InStoreOrder()]
orders.forEach { $0.process() }

 

이렇게 하면 새로운 주문 타입을 추가할 때

기존 코드를 수정할 필요 없이 간단하게 클래스를 추가할 수 있습니다.

 


4.3 코드 중복 처리

인터페이스와 추상 클래스: 그 차이점과 사용법

 

여러분, 코드 작성하면서 인터페이스와 추상 클래스 중 어떤 것을 사용해야 할지 고민해본 적 있으신가요?

 

4.3.1 인터페이스 대신 추상 클래스를 사용할 수는 없을까?

인터페이스와 추상 클래스의 차이점

 

인터페이스와 추상 클래스는 객체 지향 프로그래밍에서 중요한 개념이죠.

두 개념 모두 객체들이 공통으로 따라야 하는 규약을 정의하는 데 사용되지만,

각기 다른 특징과 장단점을 가지고 있습니다.

 

인터페이스

장점:

 

다중 상속:

Swift에서는 한 클래스가 여러 인터페이스(프로토콜)를 구현할 수 있습니다.

이는 클래스가 다양한 동작을 혼합하여 사용할 수 있게 해줍니다.

 

구현 강제:

인터페이스는 메서드의 구현을 강제합니다.

이는 모든 구현 클래스가 동일한 메서드를 가지게 하여 일관성을 유지하게 합니다.

 

유연성:

인터페이스는 특정한 구현을 강제하지 않기 때문에 더 유연한 설계를 가능하게 합니다.

 

단점:

코드 중복:

인터페이스는 구현을 포함하지 않기 때문에,

공통 동작을 여러 클래스에서 반복해서 구현해야 할 수 있습니다.

 

추상 클래스

 

장점:

 

공통 구현 제공:

추상 클래스는 공통 동작을 미리 구현할 수 있기 때문에,

이를 상속받는 클래스는 공통 동작을 재사용할 수 있습니다.

 

코드 중복 최소화:

공통 동작을 추상 클래스에 구현해 두면,

이를 상속받는 클래스는 중복 코드를 작성하지 않아도 됩니다.

 

단점:

단일 상속 제한:

Swift에서는 클래스가 하나의 부모 클래스만을 상속받을 수 있기 때문에,

추상 클래스를 사용하면 다중 상속의 유연성을 잃게 됩니다.

 

유연성 부족:

특정 구현이 포함되어 있어, 인터페이스만큼 유연하지 않을 수 있습니다.

 

코드 스멜:

추상 클래스의 메서드가 오버라이드되지 않아도 컴파일러가 에러를 발생시키지 않기 때문에,

의도하지 않은 동작을 초래할 수 있는 코드 스멜을 유발할 수 있습니다.

 

추상 클래스 예제:

 

// Swift는 추상 클래스를 명시적으로 지원하지 않지만, 이를 시뮬레이션 할 수 있습니다.
class Vehicle {
    func start() {
        // 공통 동작
        print("Vehicle started")
    }

    // 추상 메서드처럼 동작하도록 구현을 강제할 수 있습니다.
    func drive() {
        fatalError("This method must be overridden")
    }
}

class Car: Vehicle {
    override func drive() {
        print("Driving a car")
    }
}

class Truck: Vehicle {
    override func drive() {
        print("Driving a truck")
    }
}

let vehicles: [Vehicle] = [Car(), Truck()]
vehicles.forEach { 
    $0.start()
    $0.drive() 
}

 

위 예제에서는 Vehicle 클래스를 추상 클래스로 사용하여 공통 동작을 정의하고,

CarTruck 클래스에서 이를 상속받아 구현했습니다.


4.3.2 규칙: 인터페이스에서만 상속받을 것

인터페이스를 사용하면 코드의 유연성과 확장성을 극대화할 수 있습니다.

이를 통해 다양한 구현 클래스가 같은 인터페이스를 공유하면서도 각자의 독립적인 동작을 유지할 수 있습니다.

 

인터페이스(프로토콜) 예제:

 

protocol Drivable {
    func drive()
}

class Car: Drivable {
    func drive() {
        print("Driving a car")
    }
}

class Truck: Drivable {
    func drive() {
        print("Driving a truck")
    }
}

let vehicles: [Drivable] = [Car(), Truck()]
vehicles.forEach { $0.drive() }

 

위 예제에서는 Drivable 프로토콜을 사용하여 CarTruck 클래스에서 각각의 동작을 정의했습니다.

이를 통해 코드의 유연성과 확장성을 높였습니다.

 


4.4 복잡한 if 체인 구문 리팩터링

여러분, 코드 작성하면서 “if-else가 점점 많아지면 나중에 어떻게 유지보수하지?”라는 고민 해보신 적 있나요?

저도 그랬어요. 기본적인 if-else 체인을 사용한 결제 처리 코드를 한 번 볼까요?

 

기본 코드

 

class ShoppingCart {
    var paymentType: String?

    func checkout(amount: Double) {
        if paymentType == "CreditCard" {
            print("Paid \(amount) using Credit Card")
        } else if paymentType == "PayPal" {
            print("Paid \(amount) using PayPal")
        } else {
            print("Unknown payment type")
        }
    }
}

let cart = ShoppingCart()
cart.paymentType = "CreditCard"
cart.checkout(amount: 100.0)

cart.paymentType = "PayPal"
cart.checkout(amount: 200.0)

 

이런 식으로 코드를 작성하다 보면,

나중에 결제 방식이 추가되거나 변경될 때마다 if-else 문을 수정해야 하죠.

그럼 지금부터 이 복잡한 if 체인을 전략 패턴으로 리팩터링해볼까요?

 

전략 패턴 사용

protocol PaymentStrategy {
    func pay(amount: Double)
}

class CreditCardPayment: PaymentStrategy {
    func pay(amount: Double) {
        print("Paid \(amount) using Credit Card")
    }
}

class PayPalPayment: PaymentStrategy {
    func pay(amount: Double) {
        print("Paid \(amount) using PayPal")
    }
}

class ShoppingCart {
    var paymentStrategy: PaymentStrategy?

    func checkout(amount: Double) {
        paymentStrategy?.pay(amount: amount)
    }
}

let cart = ShoppingCart()
cart.paymentStrategy = CreditCardPayment()
cart.checkout(amount: 100.0)

cart.paymentStrategy = PayPalPayment()
cart.checkout(amount: 200.0)

 

이제 새로운 결제 방식을 추가할 때는 기존 코드를 수정할 필요가 없어요.

 

전략 패턴 덕분에 결제 방식이 추가되거나 변경되더라도 간단하게 새로운 클래스를 추가하고

 

PaymentStrategy를 구현하기만 하면 됩니다.

이렇게 하면 코드 유지보수도 훨씬 수월해지고, 코드의 유연성도 극대화됩니다.

 


 

마무리

 

"파이브 라인즈 오브 코드" 4장을 통해 여러분도 타입 코드를 어떻게 더 깔끔하게 만들 수 있는지 배웠을 거예요.

코드의 가독성과 유지보수성을 높이는 다양한 리팩터링 기법을 실제 프로젝트에 적용해보세요.