본문 바로가기
TIL (Today I Learned)

[CS] 클린코드

by Bokoo14 2024. 2. 18.

[참고]

더보기

https://www.samsungsds.com/kr/insights/cleancode-0823.html

 

클린코드란 무엇인가? | 인사이트리포트 | 삼성SDS

개발자라면 누구나 한 번쯤은 고민해 봤을 만한 클린코드! 클린코드란 무엇이고, 클린코드를 작성하기 위한 원칙들은 어떤 것들이 있는지 알아보겠습니다.

www.samsungsds.com

https://dev-coco.tistory.com/182

 

클린 코드(Clean Code) 요약 및 정리

개발 하며 내가 작성하고 있는 코드가 과연 좋은 코드인가 문득 고민하게 되었고, 좋은 코드란 무엇인가에 대한 궁금증으로 '클린 코드' 서적을 접하게 되어 읽으면서 중요한 내용들을 정리하고

dev-coco.tistory.com

https://brocess.tistory.com/212

 

[ 클린코드 ] 깨끗한 테스트코드 5가지 규칙(FIRST), 테스트코드 잘짜기!

클린코드 책에서 읽은 '깨끗한 테스트코드 5가지 규칙(FIRST)'에 대해 포스팅 남겨보겠습니다. 프로그래밍을 할 때 어떻게 보면 테스트 코드의 작성은 가장 기본이면서 중요하다고 할 수 있는데요

brocess.tistory.com

https://yozm.wishket.com/magazine/detail/2415/

 

클린 코드는 왜 중요하고 어떻게 실천해야 할까? | 요즘IT

클린 코드(Clean Code)는 소프트웨어 개발에서 사용되는 개념으로, 읽기 쉽고 이해하기 쉬운 코드를 작성하는 것을 강조합니다. 클린 코드는 프로그램의 동작을 보장하는 것뿐만 아니라, 코드 자체

yozm.wishket.com

 

 

 

 

 

클린코드의 가장 중요한 요소 중 하나는 가독성이다.

즉, 모든 팀원이 이해(understandability)하기 쉽도록 작성된 코드이다.

 

클린 코드는 읽기 쉽고 이해하기 쉬운 코드를 작성하는 것을 강조

클린 코드는 프로그램 동작을 보장하는 것뿐만 아니라, 코드 자체가 가독성이 뛰어나고 유지보수가 쉽도록 작성되어야 한다는 원칙에 기반함.

 

💡 가독성이 왜 중요할까?

해석이 어려운 코드는 그만큼 분석하는 데에 시간이 더 많이 걸린다.

대부분의 결함은 기존 코드를 수정하는 동안 발생한다. 따라서 이해하기 쉬운 코드는 오류의 위험성을 최소화하는 셈이다.

 

클린 코드는 버그를 찾기 쉽게 만들어준다 

 

출처: https://yozm.wishket.com/magazine/detail/2415/


📝 클린코드의 요소

💡 의도를 분명히 밝혀라

변수, 함수, 클래스 등의 이름은 코드의 의도를 잘 표현할 수 있도록 지어야 하며, 이름만으로도 코드가 하는 일을 예측할 수 있도록 해야 한다.
의미 없는 약어나 애매한 이름을 피하고, 코드의 기능을 정확하게 반영하는 이름을 선택하는 것이 중요하다.

// 나쁜 예
var flag = true

// 좋은 예
var isUserLoggedIn = true

 

💡 조건을 캡슐화하라

조건의 의도를 분명히 밝히는 함수로 표현하자

// 나쁜 예//직원에게 복지 혜택을 받을 자격이 있는지 검사한다.
if ((employee. flags & HOURLY_FLAG) && (employee.age > 65)) 

// 좋은 예
if (employee.isEligibleForFullBenefits())

 

💡 객체 생성에 유의미한 이름을 사용하라

객체의 생성자를 오버로딩 하는 경우 어떤 값으로 어떻게 생성되는지 정보가 부족할 수 있다.
이런 경우, 정적 팩토리 메소드를 사용하여 인수를 설명하는 이름으로 작성하는 것이 명확하다.

class Car {
    let brand: String
    let model: String
    let color: String
    
    private init(brand: String, model: String, color: String) {
        self.brand = brand
        self.model = model
        self.color = color
    }
    
    // 정적 팩토리 메소드를 사용하여 Car 객체 생성
    static func createCar(brand: String, model: String, color: String) -> Car {
        return Car(brand: brand, model: model, color: color)
    }
    
    // getter 메소드들 생략
}

// 생성자를 사용하여 Car 객체 생성
let car1 = Car(brand: "Audi", model: "A4", color: "White")

// 정적 팩토리 메소드를 사용하여 Car 객체 생성
let car2 = Car.createCar(brand: "BMW", model: "X5", color: "Black")

 

 

💡 서술적인 이름을 사용하라

서술적인 이름을 사용하면 개발자 머릿속에서도 설계가 뚜렷해지므로 코드를 개선하기 쉬워진다.

  • 함수가 작고 단순할수록 서술적인 이름을 작성하기 쉬워진다.
  • 이름이 길어도 괜찮다. 길고 서술적인 이름이 짧고 어려운 이름보다 좋다.
  • 이름을 붙일 때는 일관성이 있어야 한다. 모듈 내에서 함수 이름은 같은 문구, 명사, 동사를 사용하자.
// 나쁜 예: 함수 이름이 너무 간단하고 서술적이지 않음
func processData(data: [Int]) -> Int {
    // data 처리
    return processedData
}

// 좋은 예: 함수 이름이 서술적이고 작업을 명확히 설명함
func calculateAverage(from numbers: [Int]) -> Double {
    // numbers 배열의 평균값을 계산
    let sum = numbers.reduce(0, +)
    return Double(sum) / Double(numbers.count)
}

 

💡 명령과 조회를 분리하라 (CQS 원칙)

함수는 뭔가를 수행하거나 뭔가를 조회하거나 하나의 역할만을 해야 한다.
두 개의 역할을 동시에 하면 혼란을 초래한다.

 

나쁜 예시

updateUser 함수는 사용자를 업데이트하고 업데이트 성공 여부를 반환한다. 하지만 함수는 번에 가지 역할을 수행하므로 명령과 조회가 혼재되어 있다.  

// 나쁜 예시
func updateUser(name: String, age: Int) -> Bool {
    // 사용자 업데이트 로직
    // ...
    // 업데이트가 성공했으면 true 반환, 실패했으면 false 반환
    return true
}

// 사용 예시
if updateUser(name: "John", age: 30) {
    // 업데이트 성공
} else {
    // 업데이트 실패
}

 

좋은 예시

updateUser 함수는 사용자를 업데이트하는 역할만 수행하고, 업데이트 성공 여부를 반환하지 않는다.

대신, 업데이트를 수행하기 전에 userExists 함수를 사용하여 사용자가 존재하는지 여부를 먼저 확인

-> 함수가 하나의 역할에만 집중할 있고, 코드의 가독성과 유지보수성이 향상된다.

// 좋은 예시
func updateUser(name: String, age: Int) {
    // 사용자 업데이트 로직
    // ...
}

func userExists(name: String) -> Bool {
    // 사용자가 존재하는지 여부를 조회하는 로직
    // 존재하면 true, 존재하지 않으면 false 반환
    return true
}

// 사용 예시
if userExists(name: "John") {
    updateUser(name: "John", age: 30)
} else {
    // 사용자가 존재하지 않음
}

 

 

💡 오류 코드보단 예외를 사용하라

오류 코드를 반환하면 그에 따른 분기가 생기고, 또 분기가 필요한 경우엔 중첩된다.

// 나쁜 예시
enum Status {
    case ok
    case error
}

func deletePage(_ page: Page) -> Status {
    if deletePage(page) == .ok {
        if registry.deleteReference(page.name) == .ok {
            if configKeys.deleteKey(page.name.makeKey()) == .ok {
                log.info("page deleted")
                return .ok
            } else {
                log.error("config key not deleted")
                return .error
            }
        } else {
            log.error("reference not deleted")
            return .error
        }
    } else {
        log.error("page not deleted")
        return .error
    }
}

// 좋은 예시
func deletePage(_ page: Page) {
    do {
        try deletePageAndAllReferences(page)
    } catch {
        log.error(error.localizedDescription)
    }
}

func deletePageAndAllReferences(_ page: Page) throws {
    try deletePage(page)
    try registry.deleteReference(page.name)
    try configKeys.deleteKey(page.name.makeKey())
}

 

 

💡 설계의 품질을 높여주는 4가지 규칙

1. 모든 테스트를 실행하라: 테스트가 쉬운 코드를 만들다 보면 SRP를 준수하고, 낮은 결합도를 갖는 설계를 얻을 수 있다.
2. 중복을 제거하라: 깔끔한 시스템을 만들기 위해 단 몇 줄이라도 중복을 제거해야 한다.
3. 개발자의 의도를 표현하라: 좋은 이름, 클래스와 함수의 작은 크기, 표준 명칭, 단위 테스트 작성을 통해 이를 달성할 수 있다.
4. 클래스와 메소드의 수를 최소로 줄여라: 클래스와 메소드를 작게 유지함으로써 시스템의 크기 역시 작게 유지할 수 있다.
 

💡 디미터 법칙

디미터 법칙은 객체 지향 프로그래밍에서 객체 간의 결합도를 낮추기 위한 원칙

원칙에 따르면 객체는 자신이 호출하는 객체의 내부 구조를 몰라야 하며, 객체 간의 상호 작용은 최소화되어야 한다. 이를 통해 객체 간의 결합도를 낮추고 유연하고 재사용 가능한 코드를 작성할 수 있다.

 

// 디미터 법칙 위반 예시
let outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath()

 

각 객체의 내부 구조에 대해 너무 많은 정보를 알아야 하며, 결합도가 높아지게 된다.

// 디미터 법칙 적용 예시
let opts = ctxt.getOptions()
let scratchDir = opts.getScratchDir()
let outputDir = scratchDir.getAbsolutePath()

 

객체는 자신의 내부 구조를 노출하지 않고, 최종적인 결과만을 반환하므로 객체 간의 결합도가 낮아지게 된다.

 

💡 테스트 코드

TDD(Test-Driven Development): 테스트가 개발의 주도 역할을 수행하는 방식

TDD의 장점: 유연성, 유지보수성, 재사용성

 

TDD의 핵심 규칙

  1. 실패하는 테스트 작성 (Red): 개발자는 먼저 구현하고자 하는 기능에 대한 테스트를 작성합니다. 테스트는 현재 구현되지 않은 상태이기 때문에 실패할 것입니다.
  2. 최소한의 코드 작성 (Green): 테스트를 통과할 있는 최소한의 코드를 작성합니다. 코드는 테스트를 통과하기 위한 가장 간단하고 단순한 형태일 있습니다.
  3. 코드 리팩토링 (Refactor): 작성한 코드를 리팩토링하여 중복을 제거하고 가독성을 높이는 등의 개선 작업을 수행합니다. 리팩토링은 기능에 영향을 주지 않으면서 코드의 품질을 향상시키는 작업입니다.

위의 세 단계를 반복적으로 수행하면서 코드를 점진적으로 개발해나간다.

테스트를 통과하는 코드를 작성하고, 이를 리팩토링하여 코드의 품질을 높이는 것이 TDD의 핵심 규칙이다.

 

깨끗한 테스트코드 5가지 규칙(FIRST)

1. 빠르게(Fast) : 테스트는 빠르게 동작해야 한다. 테스트가 느리면 자주 돌릴 엄두를 못 낸다. 
2. 독립적으로(Independent) : 각 테스트는 서로 의존해선 안되며, 독립적으로 그리고 아무 순서로 실행해도 괜찮아야 한다.
3. 반복가능하게(Repeatable): 테스트는 어떤 환경에서도 반복 가능해야 한다. 
4. 자가검증하는(Self-Validating): 테스트는 성공 또는 실패로 bool 값으로 결과를 내어 검증해야 한다.  성공 아니면 실패다. 통과 여부를 알리고 로그 파일을 읽게 만들어서는 안 된다

5. 적시에(Timely): 테스트는 적시에 즉, 테스트하려는 실제 코드를 구현하기 직전에 구현해야 한다. 실제 코드를 구현한 다음에 테스트 코드를 만들면 실제 코드가 테스트하기 어렵다는 사실을 발견할지도 모른다. 

 

💡 변경하기 쉬운 클래스

요구사항은 수시로 변할 수 있다. 변경하기 쉬운 클래스를 만드는 것은 중요하다.
변경하기 쉬운 클래스는 기본적으로 단일 책임 원칙을 지키며, 구현체보다는 추상체에 의존한다.

클래스의 역할을 더 명확하게 분리하여 유연성과 확장성이 높아짐

 

💡 적절한 추상화 수준에서 이름을 선택하라

구현  세부사항을 드러내는 이름보다는 작업 대상 클래스나 함수가 위치하는 추상화 수준을 반영하는 이름을 선택하는 것이 중요하다.

코드를 작성할 때 어떤 작업을 수행하는지에 초점을 맞추어 이름을 지어야 한다. 이는 코드를 읽는 사람이 해당 코드의 의도를 더 쉽게 이해할 수 있도록 도와준다.

 

// 잘못된 함수 이름
protocol Modem {
    func dial(phoneNumber: String) -> Bool
    func disconnect() -> Bool
    func send(_ c: Character) -> Bool
    func recv() -> Character
    func getConnectedPhoneNumber() -> String
}

// 더 나은 함수 이름
protocol Modem {
    func connect(_ connectionLocator: String) -> Bool
    func disconnect() -> Bool
    func send(_ c: Character) -> Bool
    func recv() -> Character
    func getConnectedLocator() -> String
}

주어진 인터페이스에서 "phoneNumber"이라는 이름은 구체적인 구현을 드러내고 있다. 이는 추상화 수준을 반영하는 좋은 예가 아니다.

대신에, 추상화된 개념을 반영하는 이름을 선택하는 것이 바람직하다.

"connectionLocater"로 변경하여 전화번호라는 구체적인 개념에 국한되지 않도록 하여 다양한 연결 방식에도 유연하게 사용될 수 있다.

 

'TIL (Today I Learned)' 카테고리의 다른 글

[iOS] viewDidLoad vs loadView  (0) 2024.05.20
[TIL] Ping  (0) 2024.05.10
[CS] 리팩토링이란  (0) 2024.02.18
[Swift] MVVM 패턴  (0) 2024.02.17
[CS] 동기(Sync), 비동기(Async)  (1) 2024.02.16