관리 메뉴

김종권의 iOS 앱 개발 알아가기

[iOS - swift] Clean Code(클린 코드) - 9. 클래스 (SRP, Cohesion) 본문

Clean Code (클린 코드)

[iOS - swift] Clean Code(클린 코드) - 9. 클래스 (SRP, Cohesion)

jake-kim 2021. 11. 23. 00:01

클린한 클래스 작성하는 방법

* 아래에서 해당 개념 설명 예정

  • SRP 준수: 클래스의 변경 이유는 한가지가 되도록 설계
  • Cohesion 준수: 인스턴스 변수를 최소화

클래스는 작게 만들 것

  • 함수에서의 클린 코드 내용과 같이 클래스 역시도 작아야 가독성, 유지보수에 이점이 있는 코드
  • 함수에서는 내용의 길이를 행의 수 20줄도 긴 수치라고 했었지만, 클래스는 맡은 책임의 개수를 보고 판단
  • 클래스의 책임의 개수 판단
    • 메소드의 개수는 5개 이하가 적정
    • 클래스 이름은 해당 클래스 책임을 기술하는 최소의 범위로 작성
      (Manaer, Processor가 붙으면 해당 클래스에서 여러 책임을 떠안겼다는 증거)

ex) 책임이 많은 클래스

WRONG

- 메소드의 개수는 5개 이지만, SuperDashboard이름에서 Super라는 작명에 의하여 책임이 너무 많은 점 존재

class SuperDashboard: SomeModule {
    func getLastFocusedComponent() -> Component
    func setLastFocused()
    func getMajorVersionNumber() -> Int
    func getMinorVersionNumber() -> Int
    func getBuildNumber() -> Int
}
  • 단일 책임 원칙 (SRP)
    • 변경할 이유가 단 한나뿐이어야 한다는 원칙
    • SRP는 `책임`이라는 개념을 정의하며 적절한 클래스 크기를 제시
    • 위 SuperDashboard 클래스의 변경할 이유는 2가지 이므로 SRP를 위반 (소프트웨어 버전 추적, 포커스된 컴포넌트 추적)

RIGHT

- `소프트웨어 버전 추적`관련 코드를 제외시켜서, SRP를 준수하도록 설계 (동시에 클래스의 크기도 작아지는 장점이 존재)

- Version 클래스를 별도로 생성

- SRP를 준수하고 있으므로 재사용하기 쉬운 구조로 탄생

class Dashboard: SomeModule {
    func getLastFocusedComponent() -> Component
    func setLastFocused()
}

class Version {
    func getMajorVersionNumber() -> Int
    func getMinorVersionNumber() -> Int
    func getBuildNumber() -> Int
}

> SRP만 준수해도 클래스를 관리하기 용이하고 추상화 시키는 작업에도 도움

클래스의 성질

  • 응집도(Cohesion)
    • 응집도의 개념: 클래스에 속한 메소드와 변수가 서로 의존하며 논리적인 단위로 묶인다는 의미
    • 응집도의 기준: 인스턴스 변수가 메소드에서 많이 사용
    • 클래스는 인스턴스 변수 수가 작아야함
      (메소드는 인스턴스 변수를 하나 이상 사용해야 하므로 메소드가 변수를 더 많이 사용할수록 "메소드+클래스" 응집도가 높음)

RIGHT

ex) elements란 인스턴스가 3개의 메소드에서 모두 사용되고 있으므로 응집도가 가장 높은 코드

class Stack {
    var elements = [Int]()
    
    func size() -> Int {
        return elements.count
    }
    
    func push(_ element: Int) {
        elements.append(element)
    }
    
    func pop() -> Int? {
        guard !elements.isEmpty else { return nil }
        let element = elements.removeLast()!
        return element
    }
}
  • 응집도를 유지하면 작은 클래스가 여러개가 탄생

ex) 응집도를 유지하여 새로운 클래스로 분리되는 과정

변수 4가지를 사용하는 큰 함수가 존재

> 큰 함수 일부를 함수 하나로 빼내고 싶은 상황

> 빼내려는 코드가 큰 함수에 정의된 변수 4가지를 사용

> 변수 4개를 새 함수의 인수로 넘기지 말고, 클래스의 인스턴스로 지정

> 클래스의 인스턴스가 여러개 생겨났으므로 독자적인 클래스로 분리

SRP를 이용하여 변경하기 쉬운 클래스 만들기

핵심: SRP를 준수하며, 함수의 인수들을 인스턴스로 변경하여 클래스들로 최대한 작게 쪼갤 것

WRONG

ex) SRP 위반 클래스

- SQL문 하나를 수정할 때도 Sql 클래스에 손대야하는 상황

- select문에 내장된 select문을 지원하려면 Sql클래스를 고쳐하는 상황

- selectWithCriteria(criteria:) 이 메소드는 특정 select문을 처리할때만 사용되므로 class이름 Sql에 어울리지 않은 메소드

class Sql {
    var table: String
    var columns: [Column]
    
    init(table: String, columns: [Column]) {
        self.table = table
        self.columns = columns
    }
    
    func create() -> String { ... }
    func insert(table: [Any]) { ... }
    func selectAll() { ... }
    func findByKey(keyColumn: String, keyValue: String) { ... }
    func select(column: Column, pattern: String) { ... }
    func select(criteria: Criteria) { ... }
    func preparedInsert() { ... }
    private func columnList(columns: [Column]) { ... }
    private func valuesList(fields: [Any], columns: [Column]) { ... }
    private func selectWithCriteria(criteria: Criteria) { ... }
    private func placeholderList(columns: [Column]) { ... }
}

RIGHT

- 공개 인터페이스를 각각 Sql클래스에서 파생하는 클래스로 생성

- valueList(fields:columns:)와 같은 비공개 메소드는 해다하는 파생 클로스로 옮긴 형태

- 모든 파생 클래스가 공통적으로 사용하는 비공개 메소드는 Where과 ColumnList라는 두 유틸리티 클래스로 생성

class Sql {
    var table: String
    var columns: [Column]
    
    init(table: String, columns: [Column]) {
        self.table = table
        self.columns = columns
    }
    
    func create() -> String { ... }
    func insert(table: [Any]) { ... }
    func selectAll() { ... }
    func findByKey(keyColumn: String, keyValue: String) { ... }
    func select(column: Column, pattern: String) { ... }
    func select(criteria: Criteria) { ... }
    func preparedInsert() { ... }
    private func columnList(columns: [Column]) { ... }
    private func valuesList(fields: [Any], columns: [Column]) { ... }
    private func selectWithCriteria(criteria: Criteria) { ... }
    private func placeholderList(columns: [Column]) { ... }
}

protocol Sql {
    func generate() -> String
}

extension Sql {
    func generate() -> String { return ... }
}

// 편의를 위해서 생성자 생략
class CresteSql: Sql {
    let table: String
    let columns: [Column]
}

class SelectSql: Sql {
    let table: String
    let columns: [Column]
}

class InsertSql: Sql {
    let table: String
    let columns: [Column]
    let fields: [Any]
}

class SelectWithCriteriaSql: Sql {
    let table: String
    let columns: [Column]
    let criteria: Criteria
}

class SelectWithMatchSql: Sql {
    let columns: [Column]
    let column: Column
    let pattern: String
}

class FindByKeySql: Sql {
    let table: String
    let columns: [Column]
    let keyValue: String
    let keyColumn: String
}

class PreparedInsertSql: Sql {
    let table: String
    let columns: [Column]
}

class Where {
}

class ColumnList {
}

> 이점

- 각 클래스는 극도로 단순하므로 보자마자 순식간에 이해되는 형태

- 테스트 관점에서 모든 논리를 구석구석 증명하기에 용이하기 쉬운 형태

- 확장에 개방적이고 수정에 폐쇄적이어야 한다는 원칙 OCP(Open Close Principle) 준수 (새 기능 추가 시 기존 코드 변경 x)

DIP를 준수하여 결합도 낮추기

  • DIP(Dependencies Interface Principle): 사용하는 쪽에서 구현체가 아닌 추상화(protocol)에 의존해야하는 원칙이므로, 클래스를 만들땐 protocol을 만든 후 구현을 지향
  • 구현체에 의존하게된다면, 테스트가 어렵지만 인터페이스에 의존하면, 구현체를 언제든 변경하여 테스트하기 용이한 형태
  • 구현내용이 바뀌어도 사용하는 쪽에서는 인터페이스인 protocol에만 의존하고 있으므로 구현체의 내용이 언제든 바뀌어도 영향받지 않은 유연한 구조
  • 구현체의 내용이 숨겨짐으로서 사용하는쪽에서 사용하는데에 필요한 정보에만 의존이 가능

* 참고: Clean Code (로버트 C. 마틴)

Comments