클린 코드 1장-8장

sanghee·2022년 5월 2일
0

🍀인턴 스터디

목록 보기
1/12
post-thumbnail

1. 깨끗한 코드

나쁜 코드

  • 나쁜 코드가 쌓일수록 팀 생산성은 떨어진다.
  • 개발자는 좋은 코드를 사수해야 하는 책임이 있다.
  • 나중은 오지 않는다. 언제나 깨끗하게 유지하자.

코드 감각

  • 나쁜 코드와 좋은 코드를 구별할 수 있어야 한다.
  • 그리고 그것을 개선할 수 있어야 한다!

📌비야네 스트롭스트룹(C++ 창시자)

논리가 간단해야 버그가 숨어들지 못한다. 
의존성을 최대한 줄여야 유지보수가 쉬워진다. 
깨끗한 코드는 한 가지를 제대로 한다.
  1. 논리 간단
    • 간단하고 단순한 코드가 좋다.
  2. 의존성 줄이기
    • 추상화를 시킨다.
    • 의존하더라도 단방향으로 의존하도록 설계한다.
  3. 하나의 기능 수행
    • 한 메서드는 하나의 기능을 수행한다.
    • 10-200룰: 함수는 10줄 이내, 클래스는 200줄 이내

기존 코드

  • 새 코드를 짜면서 우리는 끊임없이 기존 코드를 읽는다.
  • 기존 코드가 읽기 쉬우면 생산성이 높아진다.
  • 급하다고 대충 짜면, 이후에 본인이 힘들어진다.

📌개선

  • 한꺼번에 코드를 정리할 필요가 없다.
  • 변수 이름 하나를 개선하고, 조금 긴 함수 하나를 분할하고, 약간의 중복을 제거하는 등
  • 지속적으로 개선하는 것이 중요하다!



2. 의미 있는 이름

좋은 이름

  • 좋은 이름을 지으려면 시간이 걸리지만 이후 절약하는 시간이 더 많다.

의미 없는 이름

  • a, an, the
  • Info, Data: 불분명하다.
  • NameString: Name이 Int일 리 없기에 Name으로 쓰자.
  • i, j, k: 루프에서 반복하는 경우에 통상적으로 쓰기에 괜찮지만 이왕이면 직관적인 이름을 붙이자.

클래스 이름

  • 명사, 명사구
  • 예시: Customer, Account,

메서드 이름

  • 동사, 동사구
  • deletePage, save

📌코드

  • 코드를 최대한 이해하기 쉽게 짜야 한다.
  • 논문이 아니라 잡지처럼 대충 봐도 잘 읽혀야 한다.
  • 코드를 읽을 사람도 프로그래머이다.

예시) 주소 관련

  • 변수들을 훑어보면 주소를 나타내는 사실을 알 수 있다.
  • 하지만 state만 본다면? 어떤 것인지 알기가 힘들다.
firstName, lastName, street, city, state, zipcode
  • addr를 붙여 주소라는 사실을 명확히 하였지만 좋은 선택은 아니다.
addrFirstName, addrLastName, addrStreet
  • Address라는 클래스나 구조체를 생성해서 관리하자.
struct Address {
    let firstname: String
    let lastName: String
    ...
}



3. 함수

작게!

  • 함수는 한가지 역할만 해야 한다. 그 한가지를 잘 해야 한다.
  • 2줄, 3줄, 4줄정도로 작게 만들어라.

내려가기

  • 코드는 위에서 아래로 이야기처럼 읽혀야 좋다.

Switch 문

  • Switch문은 작게 만들기 어렵다.
  • case가 너무 많으면, 그 자체로 길어지기 쉽다.
  • OCP(Open Closed Principle)를 위반한다. case가 변경될 때마다 코드를 수정해야 하기 때문이다.

서술적인 이름

  • 길어도 좋고, 네이밍이 오래 걸려도 좋으니 서술적인 이름을 지어야 한다.
  • 함수의 네이밍은 길어도, 함수 블록 내 변수들은 간결하면 좋다고 생각한다.

단항

  • 함수의 매개변수가 작을 수록 좋다.
  • 3개 이상은 무조건 피하자.

📌함수를 어떻게 짜죠?

  • 글을 쓰는 것과 같다.
  • 초안은 대개 서투르고 어수선하다. 작성한 후에 문단을 다듬고 고치는 것이 중요하다!
  • 처음에는 길고 복잡하다.
  • 그 후에 코드를 다듬고, 함수를 만들고, 이름을 바꾸고, 중복을 제거한다.
  • 메서드를 줄이고, 순서를 바꾼다. 때로는 전체 클래스를 쪼갠다.



4. 주석

주석

  • 좋은 주석은 유용하다.
  • 코드를 오히려 이해하기 어렵게 만든다.
  • 잘못된 정보를 줄 수 있다.

📌꼭 필요할까?

  • 주석을 유지하고 보수하기란 현실적으로 불가능하다.
  • 꼭 필요한 경우가 아니라면 지양하자.
  • 애초에 주석이 필요 없이 코드로만 이야기하는 게 중요하다.

좋은 주석

  • 법적인 내용을 담은 주석
  • 의도를 설명하는 주석
    • 코드가 이동하더라도 같이 이동되니까

    • 주석을 쓸 꺼라면 코드의 옆에 한 줄로 쓰는 게 좋다고 생각한다.

      return 1 // 오른쪽 유형이므로 정렬 순위가 더 높다.
  • 결과 경고
    • 만약 thread-safe하지 않는 함수라면, 미리 경고하는 것도 좋다.
  • TODO 주석
    • 앞으로 할 일을 적는다.

    • 그치만 master에 TODO가 있으면 복잡하므로,

    • 개인 브랜치에 작성하고, 머지할 때 지우는 것이 좋겠다.

      // TODO: - 할 일

나쁜 주석

  • 이해가 안 되어 다른 코드까지 찾아봐야 하는 주석은 별로다.
  • 주석으로 처리한 코드
    • 이걸 지워도 되는지, 남겨둬야 하는지 헷갈린다.



5. 형식 맞추기

독자

  • 개발자들을 독자라고 칭함

SwiftLint, SwiftFormat

  • SwiftLint
    • 정해진 룰에 맞지 않는 코드가 있으면 warning 또는 error를 일으킨다.
  • SwiftFormat
    • 정해진 룰에 맞지 않은 코드가 있다면 대신 코드를 수정한다.

코드 컨벤션

  • 오늘 구현한 기능은 다음에 수정될 확률이 높다.
  • 오늘 구현한 코드는 다음 코드의 품질에 많은 영향을 미친다.

신문 기사처럼

  • 위에서 아래로 코드를 작성한다.

개행

  • 개념은 빈 행으로 분리한다.
  • import, 각 함수 사이에 빈 행을 넣는다.

세로 밀집도

  • 연관성에 따라 세로로 밀집해 놓는다.
  • 서로 밀접한 개념은 세로로 가까이 둔다.

인스턴스 변수

  • 반면, 인스턴스 변수는 클래스 맨 처음에 선언한다.
  • 변수 간에 세로로 거리를 두지 않는다.

종속 함수

  • 한 함수가 다른 함수를 호출한다면 두 함수를 세로로 가까이 배치한다.
  • 호출하는 함수를 먼저 배치한다.

짧은 행

  • 행이 짧을수록 가독성이 높아진다.

📌가로 공백

  • 할당문은 할당 연산자를 강조하기 위해 앞뒤에 공백을 준다.
  • 함수 이름과 이어지는 괄호 사이에는 공백을 주지 않는다.
  • 곱셈은 우선순위가 가장 높기에 공백을 주지 않는다.
  • 항 사이에는 공백이 들어간다.
let marker = Marker()
func divide(safe divider: Int)
let ratio = a*b + c*d

들여쓰기 무시하기

  • 간단한 if문에서 짧은 함수에서 들여쓰기를 한다.
  • 그치만 return밖에 없는 경우는 한 줄로 쓰는게 깔끔한 것 같다.
  • 그런데 길면 들여쓰고 짧으면 한 줄로 쓰는 것보다, 둘 다 동일하게 들여쓰기를 하는게 맞는 것 같다.
guard let number = number else { return }
guard let number = number else {
    return
}



6. 객체와 자료 구조

private

  • 남들이 변수에 의존하지 않게 비공개한다.
  • 그렇다면 조회 함수와 설정 함수를 public하게 선언해 외부로 노출하는 이유는?
  • 아무 생각 없이 조회/설정 함수를 추가하는 방법이 가장 나쁘다.

자료구조 vs 인터페이스

  • 자료구조는 구현을 노출한다.
  • 인터페이스는 정보가 어디서 오는지 드러나지 않는다.

protocol

  • 정보를 세세하게 표현하지 말고 추상적인 개념으로 표출해야 한다.

디미터 법칙

  • 휴리스틱: 불충분한 순간에 경험에 의존해 결정하는 방법
  • 휴리스틱으로 모듈은 자신이 조작하는 객체의 속사정을 몰라야 한다는 법칙이다.

기차 충돌

  • 여러 객차가 한 줄로 이어진 기차처럼 보이는 코드를 말한다.
  • 일반적으로 조잡하다 여겨지는 방식이므로 피하는 편이 좋다.
let distance = location.position().distance(to: nextStep)
  • 코드를 다음과 같이 구현했따면 디미터 법칙을 거론할 필요가 없어진다.
  • 현재 코드를 position() → position으로 사용할 수 있도록 수정해야 겠다.
let position = location.position
let distance = position.distance(to: nextStep)

잡종 구조

  • 이런 혼란으로 때때로 절반은 객체, 절반은 자료구조인 잡종 구조가 탄생한다.

private 선언시 주의

  • 비공개 변수를 조회, 설정 함수로 조작한다.
  • 일종의 사이비 캡슐화로 별다른 이익이 없다면 사용하지 말자.
class Calculator {
    private let number = 1
    static func getNumber() {
        return number
    }
}

📌절차지향 프로그래밍

  • 내부 구조를 노출한다.
  • 새로운 자료 구조를 추가하려면 Geometry 클래스의 함수를 수정해야 한다.

예시

  • Circle을 추가하려면 Geometry의 area 함수도 수정해야 한다.
class Square {
    let topLeft: CGPoint
    let side: CGFloat
}

class Rectangle {
    let topLeft: CGPoint
    let height: CGFloat
    let width: CGFloat
}

// Circle을 추가하려면

class Geometry {
    let pi = 3.14
    
    func area(shape: AnyObject) -> Double {
        if let shape = shape as? Square {
            return shape.side * shape.side
        } else if let shape = shape as? Reactangle {
            return shape.height * shape.width
        // Circle 관련 코드를 추가해야 한다!
        } else {
            return -1
        }
    }   
}

📌객체지향 프로그래밍

  • 객체는 동작을 공개하고 자료를 숨긴다.
  • 새로운 자료 구조를 추가하기 쉽다.
  • 새로운 함수를 추가하기 어렵다.

예시

  • area()는 다형성을 띄고 있으므로, Geometry 클래스가 별도로 필요하지 않다.
    • 다형성: 하나의 객체에 여러 가지 타입을 대입할 수 있다. generic?
    • 단형성: 다형성의 반대. 하나의 객체에 하나의 타입만 대응할 수 있다.
  • Shape의 area 함수를 수정하면, 해당 클래스를 상속받은 Square, Rectangle가 override한 함수도 수정해야 한다.
class Shape {
    func area() -> Double {
    }
}

class Square: Shape {
    private let topLeft: CGPoint
    private let side: CGFloat
    
    override func area() -> CGFloat {
        return side * side
    }
}

class Rectangle: Shape {
    private let topLeft: CGPoint
    private let width: CGFloat
    private let height: CGFloat
    
    override func area() -> CGFloat {
        return width * height
    }
}
  • 위의 코드보다, Shape라는 프로토콜을 만들고, Square, Rectangle을 구조체로 생성해도 좋을 것 같다.
protocol Shape {
    var area: CGFloat { get }
}

struct Square: Shape {
    let topLeft: CGPoint
    let side: CGFloat

    var area: CGFloat {
        return side*side
    }
}

let square = Square(topLeft: .zero, side: 10)
let area = square.area
print(area) // 100.0



7. 오류 처리

오류

  • 뭔가 잘못된 가능성은 늘 존재한다.
  • 거기에 대한 책임은 프로그래머에게 있다.

예외

  • 오류보다는 예외를 던저라.
  • 오류를 던지면 함수를 호출한 즉시 오류를 확인하고 처리해야 하기 때문이다.

흐름 정의

  • 비즈니스 논리와 오류 처리가 잘 분리되어야 한다.

null

  • null을 반환하는 코드는 호출자에게 책임을 떠넘기는 것이다.
  • 매번 함수를 호출할 때마다 null을 처리해야 한다.
    • 차라리 객체를 만들어서 반환하자.



8. 경계

외부 소프트웨어

  • 모든 소프트웨어를 직접 개발하는 경우는 드물다.
  • 어떤 식으로든 외부 코드를 우리 코드에 맞게 깔끔하게 통합해야 한다.

제공자와 사용자의 입장 차이

  • 패키지나 프레임워크 제공자는 적용성을 최대한 넓힌다.
  • 반면 사용자는 자신의 요구에 집중하길 원한다.

외부 코드 관리

  • 해당 코드를 관리하는 클래스를 생성한다.
  • 클래스 안에서 우리 프로그램에 필요한 기능들을 제공한다.
  • 코드를 이해하기 쉽고 오용하기는 어렵게 되었다!

학습 테스트

  • 프로그램에서 사용하려는 방식대로 외부 API를 호출하는 방법을 말한다.
  • 간단하게 테스트 케이스를 작성해 학습한다.
  • 예상대로 작동하는지 확인한다.

경계

  • 구현되지 않은 API와 최대한 먼 작업부터 진행했다.
  • 우리 코드와 저쪽 코드가 만나는 경계가 서서히 좁혀진다.
profile
👩‍💻

0개의 댓글