1. 깨끗한 코드
나쁜 코드
- 나쁜 코드가 쌓일수록 팀 생산성은 떨어진다.
- 개발자는 좋은 코드를 사수해야 하는 책임이 있다.
- 나중은 오지 않는다. 언제나 깨끗하게 유지하자.
코드 감각
- 나쁜 코드와 좋은 코드를 구별할 수 있어야 한다.
- 그리고 그것을 개선할 수 있어야 한다!
📌비야네 스트롭스트룹(C++ 창시자)
논리가 간단해야 버그가 숨어들지 못한다.
의존성을 최대한 줄여야 유지보수가 쉬워진다.
깨끗한 코드는 한 가지를 제대로 한다.
- 논리 간단
- 의존성 줄이기
- 추상화를 시킨다.
- 의존하더라도 단방향으로 의존하도록 설계한다.
- 하나의 기능 수행
- 한 메서드는 하나의 기능을 수행한다.
- 10-200룰: 함수는 10줄 이내, 클래스는 200줄 이내
기존 코드
- 새 코드를 짜면서 우리는 끊임없이 기존 코드를 읽는다.
- 기존 코드가 읽기 쉬우면 생산성이 높아진다.
- 급하다고 대충 짜면, 이후에 본인이 힘들어진다.
📌개선
- 한꺼번에 코드를 정리할 필요가 없다.
- 변수 이름 하나를 개선하고, 조금 긴 함수 하나를 분할하고, 약간의 중복을 제거하는 등
- 지속적으로 개선하는 것이 중요하다!
2. 의미 있는 이름
좋은 이름
- 좋은 이름을 지으려면 시간이 걸리지만 이후 절약하는 시간이 더 많다.
의미 없는 이름
- a, an, the
- Info, Data: 불분명하다.
- NameString: Name이 Int일 리 없기에 Name으로 쓰자.
- i, j, k: 루프에서 반복하는 경우에 통상적으로 쓰기에 괜찮지만 이왕이면 직관적인 이름을 붙이자.
클래스 이름
- 명사, 명사구
- 예시: Customer, Account,
메서드 이름
📌코드
- 코드를 최대한 이해하기 쉽게 짜야 한다.
- 논문이 아니라 잡지처럼 대충 봐도 잘 읽혀야 한다.
- 코드를 읽을 사람도 프로그래머이다.
예시) 주소 관련
- 변수들을 훑어보면 주소를 나타내는 사실을 알 수 있다.
- 하지만 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. 주석
주석
- 좋은 주석은 유용하다.
- 코드를 오히려 이해하기 어렵게 만든다.
- 잘못된 정보를 줄 수 있다.
📌꼭 필요할까?
- 주석을 유지하고 보수하기란 현실적으로 불가능하다.
- 꼭 필요한 경우가 아니라면 지양하자.
- 애초에 주석이 필요 없이 코드로만 이야기하는 게 중요하다.
좋은 주석
- 법적인 내용을 담은 주석
- 의도를 설명하는 주석
- 결과 경고
- 만약 thread-safe하지 않는 함수라면, 미리 경고하는 것도 좋다.
- TODO 주석
-
앞으로 할 일을 적는다.
-
그치만 master에 TODO가 있으면 복잡하므로,
-
개인 브랜치에 작성하고, 머지할 때 지우는 것이 좋겠다.
나쁜 주석
- 이해가 안 되어 다른 코드까지 찾아봐야 하는 주석은 별로다.
- 주석으로 처리한 코드
- 이걸 지워도 되는지, 남겨둬야 하는지 헷갈린다.
5. 형식 맞추기
독자
- 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
}
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
} 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)
7. 오류 처리
오류
- 뭔가 잘못된 가능성은 늘 존재한다.
- 거기에 대한 책임은 프로그래머에게 있다.
예외
- 오류보다는 예외를 던저라.
- 오류를 던지면 함수를 호출한 즉시 오류를 확인하고 처리해야 하기 때문이다.
흐름 정의
- 비즈니스 논리와 오류 처리가 잘 분리되어야 한다.
null
- null을 반환하는 코드는 호출자에게 책임을 떠넘기는 것이다.
- 매번 함수를 호출할 때마다 null을 처리해야 한다.
8. 경계
외부 소프트웨어
- 모든 소프트웨어를 직접 개발하는 경우는 드물다.
- 어떤 식으로든 외부 코드를 우리 코드에 맞게 깔끔하게 통합해야 한다.
제공자와 사용자의 입장 차이
- 패키지나 프레임워크 제공자는 적용성을 최대한 넓힌다.
- 반면 사용자는 자신의 요구에 집중하길 원한다.
외부 코드 관리
- 해당 코드를 관리하는 클래스를 생성한다.
- 클래스 안에서 우리 프로그램에 필요한 기능들을 제공한다.
- 코드를 이해하기 쉽고 오용하기는 어렵게 되었다!
학습 테스트
- 프로그램에서 사용하려는 방식대로 외부 API를 호출하는 방법을 말한다.
- 간단하게 테스트 케이스를 작성해 학습한다.
- 예상대로 작동하는지 확인한다.
경계
- 구현되지 않은 API와 최대한 먼 작업부터 진행했다.
- 우리 코드와 저쪽 코드가 만나는 경계가 서서히 좁혀진다.