클린 코드 (Clean Code)#
클린 코드 (Clean Code) 는 소프트웨어 개발에서 코드의 품질을 높이기 위한 핵심 원칙과 실천 방법을 의미한다. 단순히 동작하는 코드가 아니라, 읽기 쉽고, 명확하며, 유지보수와 확장이 쉬운 코드를 목표로 한다. 이를 위해 명확한 네이밍, 함수 단순화, 중복 제거, 일관성 유지, 테스트 용이성 등 다양한 원칙과 기법을 적용한다. 클린 코드는 소프트웨어 아키텍처와 밀접하게 연관되며, 팀 협업과 프로젝트의 성공에 필수적인 요소로 자리 잡고 있다.
핵심 개념#
클린 코드 (Clean Code): 가독성과 유지보수성이 높은 코드를 의미하며, 명확한 구조와 일관된 스타일을 갖춘 코드를 지향한다.
가독성 (Readability): 코드를 읽고 이해하기 쉬운 정도를 나타내며, 명확한 네이밍과 일관된 코드 스타일이 중요하다.
유지보수성 (Maintainability): 코드를 수정하거나 확장하기 쉬운 정도를 의미하며, 모듈화와 낮은 결합도가 핵심 요소이다.
리팩토링 (Refactoring): 기능 변경 없이 코드의 구조를 개선하여 가독성과 유지보수성을 높이는 작업이다.
제품 출시에만 집중해 코드 품질을 챙기지 못하는 상황이 지속되어, 새로운 코드를 짜는 속도가 급격히 하락하는 사태가 발생하면서 클린 코드의 필요성이 대두되었다. 기존 코드를 변경할 때 해석하는 시간과 수정하는 비율이 10:1 이라는 문제점이 클린 코드 개념 발전의 배경이 되었다.
목적 및 필요성#
- 유지보수성 향상: 코드를 더 이해하기 쉽고 수정하기 쉽게 만드는 것
- 개발 생산성 증대: 개발 속도가 증가하고 코드 품질이 향상
- 버그 감소: 논리가 간단해야 버그가 숨어들지 못한다
주요 기능 및 역할#
- 코드 가독성 향상: 원하는 로직을 빠르게 찾을 수 있는 코드
- 의도 명확화: 코드 자체가 스스로를 설명할 수 있게끔 작성
- 유지보수 지원: 변경과 확장에 유연하게 대응할 수 있으며, 유지보수 비용을 절감
- 자기 설명적 (Self-Descriptive): 코드 자체가 스스로를 설명할 수 있게 작성
- 단일 목적: 깨끗한 코드는 한 가지를 제대로 한다
- 우아함과 효율성: 우아하고 효율적인 코드, 논리가 간단해야 버그가 숨어들지 못한다
기본 원리 및 원칙#
기본 원리#
- 가독성 (Readability): 모든 팀원이 이해하기 쉽도록 작성된 코드
- 단순성 (Simplicity): 간결하고 직접적인 코드 구조
- 명확성 (Clarity): 설계자의 의도를 숨기지 않는 코드
SOLID 원칙#
- SRP (Single Responsibility Principle): 하나의 클래스는 하나의 책임만 가져야 한다
- OCP (Open/Closed Principle): 확장에는 열려있고 변경에는 닫혀있어야 한다
- LSP (Liskov Substitution Principle): 서브 타입은 언제나 기반 타입으로 교체할 수 있어야 한다
- ISP (Interface Segregation Principle): 클라이언트는 자신이 사용하는 메소드에만 의존해야 한다
- DIP (Dependency Inversion Principle): 추상화된 것은 구체적인 것에 의존하면 안된다
클린 코드의 핵심 원칙#
의미 있는 이름 사용 (Meaningful Names)#
좋은 이름은 코드의 의도를 분명히 드러내고 문서화의 필요성을 줄인다.
- 의도를 분명히 밝히는 이름: 변수, 함수, 클래스의 목적이 명확해야 함
- 발음 가능한 이름: 팀원들과 의사소통이 쉬워야 함
- 검색 가능한 이름: 코드베이스에서 쉽게 찾을 수 있어야 함
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| # 잘못된 예시 ❌
d = [] # 무엇을 담는 리스트인지 알 수 없음
u = get_user() # 사용자 정보의 어떤 부분인지 불명확
def get_them(list1):
result = []
for x in list1:
if x[0] == 4:
result.append(x)
return result
# 올바른 예시 ✅
flagged_cells = [] # 플래그된 셀들을 담는 리스트
current_user = get_user() # 현재 사용자 정보
def get_flagged_cells(game_board):
"""게임 보드에서 플래그된 셀들을 반환"""
flagged_cells = []
for cell in game_board:
if cell[STATUS_VALUE] == FLAGGED:
flagged_cells.append(cell)
return flagged_cells
|
추가 가이드라인:
함수 작성 원칙 (Function Principles)#
함수는 가능한 한 작고 단일 책임을 가져야 한다.
- 한 가지 일만 수행: 함수는 하나의 책임만 가져야 함
- 작은 크기 유지: 3-5 줄 정도의 작은 함수가 이상적
- 매개변수 최소화: 3 개 이하의 매개변수 권장
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
| # 잘못된 예시 ❌
def process_user_data(user_id, name, email, age, send_email=True):
"""여러 가지 일을 동시에 수행하는 함수"""
# 사용자 데이터 검증
if not email or '@' not in email:
raise ValueError("Invalid email")
# 데이터베이스에 저장
user = User(user_id, name, email, age)
database.save(user)
# 이메일 발송
if send_email:
email_service.send_welcome_email(email)
# 로그 기록
logger.info(f"User {name} created")
return user
# 올바른 예시 ✅
def validate_email(email):
"""이메일 유효성 검증"""
if not email or '@' not in email:
raise ValueError("Invalid email")
def create_user(user_data):
"""사용자 생성"""
validate_email(user_data.email)
user = User(user_data.id, user_data.name, user_data.email, user_data.age)
return database.save(user)
def send_welcome_notification(user):
"""환영 알림 발송"""
email_service.send_welcome_email(user.email)
logger.info(f"User {user.name} created")
# 사용법
user_data = UserData(id=1, name="John", email="john@example.com", age=30)
new_user = create_user(user_data)
send_welcome_notification(new_user)
|
추가 가이드라인:
좋은 코드는 최소한의 주석만으로도 자체적으로 설명이 된다.
- 코드로 설명할 수 없는 것만 주석으로: 코드 자체가 설명이 되도록 작성
- 나쁜 코드를 주석으로 설명하지 말고 코드를 개선: 주석보다 코드 개선 우선
- 의도와 결정의 이유를 설명: 어떻게가 아닌 왜를 설명
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
| # 잘못된 주석 사용 ❌
def calculate_price(items):
# 총합을 0으로 초기화
total = 0
# 각 아이템에 대해 반복
for item in items:
# 가격에 수량을 곱함
total += item.price * item.quantity
# 세금 계산 (10%)
tax = total * 0.1
# 총합에 세금 추가
return total + tax
# 올바른 주석 사용 ✅
def calculate_price_with_tax(items):
"""
아이템 목록의 총 가격을 세금 포함하여 계산
Args:
items: 가격과 수량 정보를 가진 아이템 목록
Returns:
세금이 포함된 총 가격
"""
subtotal = sum(item.price * item.quantity for item in items)
# 비즈니스 규칙: 현재 세율은 10%이며, 정부 정책에 따라 변경될 수 있음
TAX_RATE = 0.1
tax = subtotal * TAX_RATE
return subtotal + tax
def get_user_discount(user):
"""사용자 할인율 계산"""
# TODO: 추후 VIP 등급 시스템 도입 시 할인 로직 확장 예정
if user.is_premium:
return 0.15 # 프리미엄 사용자 15% 할인
return 0.0
|
추가 가이드라인:
좋은 주석: 법적 정보, 의도 설명, 중요성 강조, API 문서화 등 정말 필요한 경우에만 주석을 사용한다.
1
2
3
4
5
6
7
8
9
10
| /**
* 사용자 계정의 구독 상태를 업데이트합니다.
* @param {string} userId - 업데이트할 사용자의 ID
* @param {Object} subscriptionDetails - 구독 세부 정보
* @returns {Promise<User>} - 업데이트된 사용자 객체
* @throws {NotFoundError} - 사용자를 찾을 수 없는 경우
*/
async function updateSubscription(userId, subscriptionDetails) {
// 구현…
}
|
나쁜 주석 피하기: 중복 정보, 의무적 주석, 코드 블록 표시, 주석 처리된 코드, 위치 표시 등 불필요한 주석은 피한다.
1
2
3
4
5
6
7
8
9
| // 나쁜 예: 중복 정보
// 사용자 이름을 설정
setUsername(name);
// 나쁜 예: 주석 처리된 코드
// calculateTotal() 메서드는 더 이상 사용되지 않아 주석 처리함
// function calculateTotal() {
// // 계산 로직…
// }
|
중복 제거 원칙 (DRY - Don’t Repeat Yourself)#
반복되는 코드를 함수, 모듈 등으로 추출하여 재사용한다.
- 동일한 로직을 여러 곳에 작성하지 않기
- 공통 기능을 함수나 모듈로 추출
- 설정값들은 상수로 관리
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
| # 잘못된 예시 ❌
class UserService:
def create_admin_user(self, name, email):
if not email or '@' not in email:
raise ValueError("Invalid email format")
if len(name) < 2:
raise ValueError("Name too short")
user = {
'id': self.generate_id(),
'name': name,
'email': email,
'role': 'admin',
'created_at': datetime.now(),
'is_active': True
}
return self.database.save(user)
def create_regular_user(self, name, email):
if not email or '@' not in email:
raise ValueError("Invalid email format")
if len(name) < 2:
raise ValueError("Name too short")
user = {
'id': self.generate_id(),
'name': name,
'email': email,
'role': 'user',
'created_at': datetime.now(),
'is_active': True
}
return self.database.save(user)
# 올바른 예시 ✅
class UserService:
MIN_NAME_LENGTH = 2
def _validate_user_data(self, name, email):
"""사용자 데이터 유효성 검증"""
if not email or '@' not in email:
raise ValueError("Invalid email format")
if len(name) < self.MIN_NAME_LENGTH:
raise ValueError(f"Name must be at least {self.MIN_NAME_LENGTH} characters")
def _create_user_dict(self, name, email, role):
"""사용자 딕셔너리 생성"""
return {
'id': self.generate_id(),
'name': name,
'email': email,
'role': role,
'created_at': datetime.now(),
'is_active': True
}
def create_user(self, name, email, role='user'):
"""사용자 생성 (역할 지정 가능)"""
self._validate_user_data(name, email)
user = self._create_user_dict(name, email, role)
return self.database.save(user)
def create_admin_user(self, name, email):
"""관리자 사용자 생성"""
return self.create_user(name, email, 'admin')
|
오류 처리 원칙 (Error Handling)#
오류 처리는 코드의 논리와 분리하여 깔끔하게 관리해야 한다.
- 예외를 사용하여 오류 상황 명확히 표현
- 의미 있는 오류 메시지 제공
- 오류 처리 로직을 비즈니스 로직과 분리
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
| # 잘못된 예시 ❌
def withdraw_money(account_id, amount):
account = get_account(account_id)
if account is None:
return None # 오류 상황이 명확하지 않음
if account.balance < amount:
return -1 # 매직 넘버 사용
account.balance -= amount
return account.balance
# 올바른 예시 ✅
class InsufficientFundsError(Exception):
"""잔액 부족 예외"""
def __init__(self, requested_amount, available_balance):
self.requested_amount = requested_amount
self.available_balance = available_balance
super().__init__(
f"Insufficient funds: requested {requested_amount}, "
f"but only {available_balance} available"
)
class AccountNotFoundError(Exception):
"""계좌를 찾을 수 없음 예외"""
def __init__(self, account_id):
self.account_id = account_id
super().__init__(f"Account not found: {account_id}")
def withdraw_money(account_id, amount):
"""
계좌에서 돈을 인출
Args:
account_id: 계좌 ID
amount: 인출할 금액
Returns:
인출 후 잔액
Raises:
AccountNotFoundError: 계좌를 찾을 수 없는 경우
InsufficientFundsError: 잔액이 부족한 경우
ValueError: 인출 금액이 0 이하인 경우
"""
if amount <= 0:
raise ValueError("Withdrawal amount must be positive")
account = get_account(account_id)
if account is None:
raise AccountNotFoundError(account_id)
if account.balance < amount:
raise InsufficientFundsError(amount, account.balance)
account.balance -= amount
save_account(account)
return account.balance
# 사용 예시
try:
new_balance = withdraw_money("ACC123", 1000)
print(f"Withdrawal successful. New balance: {new_balance}")
except AccountNotFoundError as e:
print(f"Error: {e}")
# 계좌 생성 안내 등…
except InsufficientFundsError as e:
print(f"Error: {e}")
# 충전 안내 등…
except ValueError as e:
print(f"Invalid input: {e}")
|
추가 가이드라인:
언체크 예외 사용: 체크 예외 (checked exceptions) 는 의존성 문제를 일으킬 수 있으므로 언체크 예외 (unchecked exceptions) 를 선호하는 것이 낫다.
예외에 컨텍스트 제공: 예외 메시지는 오류의 원인과 위치를 명확히 알려줘야 한다.
1
2
3
4
5
| // 나쁜 예
throw new Error("유효하지 않음");
// 좋은 예
throw new Error("사용자 ID가 없어 인증에 실패했습니다");
|
호출자에게 예외를 정의: API 는 호출자에게 어떤 예외가 발생할 수 있는지 명확히 알려줘야 한다.
1
2
3
4
5
6
7
8
9
| /**
* 주어진 ID로 사용자를 조회합니다.
* @param userId 조회할 사용자 ID
* @return 찾은 사용자 객체
* @throws UserNotFoundException 사용자를 찾을 수 없는 경우
*/
public User findUserById(String userId) throws UserNotFoundException {
// 구현…
}
|
클래스와 객체 설계 원칙#
클래스는 응집도가 높고 다른 클래스와의 결합도는 낮아야 하며, 객체와 데이터 구조의 적절한 사용은 코드의 유연성을 높인다.
- 단일 책임 원칙 (SRP): 클래스는 하나의 이유로만 변경되어야 함
- 작은 크기 유지: 클래스도 함수처럼 작게 유지
- 응집도는 높게, 결합도는 낮게
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
| # 잘못된 예시 ❌
class User:
def __init__(self, name, email):
self.name = name
self.email = email
self.created_at = datetime.now()
def save_to_database(self):
# 데이터베이스 저장 로직
pass
def send_welcome_email(self):
# 이메일 발송 로직
pass
def generate_report(self):
# 리포트 생성 로직
pass
def validate_email(self):
# 이메일 검증 로직
pass
# 올바른 예시 ✅
class User:
"""사용자 도메인 객체"""
def __init__(self, name, email):
self.name = name
self.email = email
self.created_at = datetime.now()
def get_display_name(self):
return self.name.title()
def is_recently_created(self, days=7):
return (datetime.now() - self.created_at).days <= days
class EmailValidator:
"""이메일 검증 전담 클래스"""
@staticmethod
def is_valid(email):
return email and '@' in email and '.' in email.split('@')[1]
@staticmethod
def validate(email):
if not EmailValidator.is_valid(email):
raise ValueError(f"Invalid email format: {email}")
class UserRepository:
"""사용자 데이터 저장/조회 전담 클래스"""
def __init__(self, database):
self.database = database
def save(self, user):
return self.database.insert('users', {
'name': user.name,
'email': user.email,
'created_at': user.created_at
})
def find_by_email(self, email):
data = self.database.query('users', {'email': email})
return User(data['name'], data['email']) if data else None
class NotificationService:
"""알림 발송 전담 클래스"""
def __init__(self, email_client):
self.email_client = email_client
def send_welcome_email(self, user):
subject = f"Welcome, {user.get_display_name()}!"
body = f"Thank you for joining us, {user.name}!"
self.email_client.send(user.email, subject, body)
class UserService:
"""사용자 관련 비즈니스 로직 조정"""
def __init__(self, user_repository, notification_service):
self.user_repository = user_repository
self.notification_service = notification_service
def create_user(self, name, email):
EmailValidator.validate(email)
user = User(name, email)
saved_user = self.user_repository.save(user)
self.notification_service.send_welcome_email(saved_user)
return saved_user
|
추가 가이드라인:
작은 클래스: 클래스도 함수처럼 작아야 한다. 한 클래스는 한 가지 책임만 가져야 한다 (단일 책임 원칙).
클래스 조직: 변수는 상수, 인스턴스 변수, 정적 변수 순으로 정렬하고, 메서드는 공개 메서드부터 시작하는 게 좋다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| public class User {
// 상수
public static final String DEFAULT_USERNAME = "guest";
// 인스턴스 변수
private String username;
private String email;
// 정적 변수
private static int userCount = 0;
// 생성자
public User(String username, String email) {
this.username = username;
this.email = email;
userCount++;
}
// 공개 메서드
public String getUsername() {
return username;
}
public String getEmail() {
return email;
}
// 비공개 메서드
private void validateEmail() {
// 이메일 검증 로직
}
}
|
캡슐화: 객체는 내부 데이터를 숨기고 추상화된 메서드를 통해 접근을 제공해야 한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| // 나쁜 예: 데이터 구조 노출
class Point {
public double x;
public double y;
}
// 좋은 예: 캡슐화
class Point {
private double x;
private double y;
public Point(double x, double y) {
this.x = x;
this.y = y;
}
public double distanceTo(Point other) {
return Math.sqrt(Math.pow(this.x - other.x, 2) + Math.pow(this.y - other.y, 2));
}
// 필요한 경우 게터와 세터 제공
public double getX() { return x; }
public double getY() { return y; }
}
|
객체 vs 자료 구조: 객체는 동작을 숨기고 데이터를 노출하지 않는 반면, 자료 구조는 데이터를 노출하고 의미 있는 동작은 없다. 상황에 맞게 적절히 선택한다.
디미터 법칙 (Law of Demeter): 객체는 자신이 직접 알고 있는 객체와만 대화해야 한다. " 한 번의 점만 사용하라 " 는 원칙이다.
1
2
3
4
5
| // 나쁜 예: 디미터 법칙 위반
const street = user.getAddress().getStreet();
// 좋은 예
const street = user.getStreetAddress();
|
테스트 가능한 코드 작성#
깨끗한 테스트는 가독성이 높고, 빠르게 실행되며, 독립적이어야 한다.
- 의존성 주입 활용: 외부 의존성을 주입받아 테스트 시 모킹 가능하게 함
- 순수 함수 선호: 입력이 같으면 항상 같은 출력을 보장
- 부작용 최소화: 외부 상태 변경을 최소화
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
| # 잘못된 예시 ❌ - 테스트하기 어려운 코드
import requests
from datetime import datetime
class WeatherService:
def get_current_temperature(self, city):
# 외부 API 직접 호출 - 테스트 시 실제 API 요청 발생
response = requests.get(f"http://api.weather.com/current/{city}")
data = response.json()
# 현재 시간 직접 사용 - 테스트 시 시간에 따라 결과 달라짐
current_hour = datetime.now().hour
# 파일에 직접 로그 기록 - 테스트 시 실제 파일 생성
with open('weather.log', 'a') as f:
f.write(f"{datetime.now()}: Temperature for {city}: {data['temp']}\n")
return data['temp']
# 올바른 예시 ✅ - 테스트 가능한 코드
from abc import ABC, abstractmethod
from datetime import datetime
class WeatherAPIClient(ABC):
@abstractmethod
def get_weather_data(self, city):
pass
class HttpWeatherAPIClient(WeatherAPIClient):
def __init__(self, http_client):
self.http_client = http_client
def get_weather_data(self, city):
response = self.http_client.get(f"http://api.weather.com/current/{city}")
return response.json()
class Logger(ABC):
@abstractmethod
def log(self, message):
pass
class FileLogger(Logger):
def __init__(self, filename):
self.filename = filename
def log(self, message):
with open(self.filename, 'a') as f:
f.write(f"{datetime.now()}: {message}\n")
class TimeProvider(ABC):
@abstractmethod
def now(self):
pass
class SystemTimeProvider(TimeProvider):
def now(self):
return datetime.now()
class WeatherService:
def __init__(self, weather_client, logger, time_provider):
self.weather_client = weather_client
self.logger = logger
self.time_provider = time_provider
def get_current_temperature(self, city):
weather_data = self.weather_client.get_weather_data(city)
temperature = weather_data['temp']
current_time = self.time_provider.now()
self.logger.log(f"Temperature for {city}: {temperature}")
return temperature
def is_good_time_for_outdoor_activity(self, city):
"""야외 활동하기 좋은 시간인지 판단"""
temperature = self.get_current_temperature(city)
current_hour = self.time_provider.now().hour
# 비즈니스 로직: 온도 15-25도, 시간 9-18시가 적합
return 15 <= temperature <= 25 and 9 <= current_hour <= 18
# 테스트 코드 예시
import unittest
from unittest.mock import Mock
class TestWeatherService(unittest.TestCase):
def setUp(self):
self.mock_weather_client = Mock(spec=WeatherAPIClient)
self.mock_logger = Mock(spec=Logger)
self.mock_time_provider = Mock(spec=TimeProvider)
self.weather_service = WeatherService(
self.mock_weather_client,
self.mock_logger,
self.mock_time_provider
)
def test_get_current_temperature(self):
# Given
self.mock_weather_client.get_weather_data.return_value = {'temp': 20}
# When
temperature = self.weather_service.get_current_temperature('Seoul')
# Then
self.assertEqual(temperature, 20)
self.mock_weather_client.get_weather_data.assert_called_once_with('Seoul')
self.mock_logger.log.assert_called_once()
def test_is_good_time_for_outdoor_activity_when_conditions_perfect(self):
# Given
self.mock_weather_client.get_weather_data.return_value = {'temp': 20}
mock_time = Mock()
mock_time.hour = 14 # 오후 2시
self.mock_time_provider.now.return_value = mock_time
# When
result = self.weather_service.is_good_time_for_outdoor_activity('Seoul')
# Then
self.assertTrue(result)
|
추가 가이드라인:
TDD 규칙: 테스트를 실패하게 만드는 코드를 작성한 후, 테스트를 통과하는 코드를 작성하고, 마지막으로 코드를 리팩토링한다.
가독성 있는 테스트: 테스트는 이해하기 쉽게 작성해야 한다.
1
2
3
4
5
6
7
8
9
10
11
12
| // 좋은 예: 가독성 있는 테스트
@Test
public void shouldWithdrawMoneyWhenEnoughBalance() {
// 준비(Arrange)
Account account = new Account(100);
// 실행(Act)
account.withdraw(50);
// 확인(Assert)
assertEquals(50, account.getBalance());
}
|
개념 당 하나의 assert: 각 테스트는 하나의 개념만 테스트해야 한다.
F.I.R.S.T. 원칙: 테스트는 빠르고 (Fast), 독립적이며 (Independent), 반복 가능하고 (Repeatable), 자체 검증이 가능하며 (Self-validating), 적시에 작성되어야 (Timely) 한다.
성능 고려사항#
- 가독성을 해치지 않는 선에서 최적화
- 측정 가능한 병목점만 최적화
- 알고리즘 복잡도 고려
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
| # 성능과 가독성의 균형 ✅
class UserService:
def __init__(self):
self._user_cache = {} # 간단한 캐싱
def get_user_by_email(self, email):
"""이메일로 사용자 조회 (캐시 활용)"""
if email in self._user_cache:
return self._user_cache[email]
user = self._fetch_user_from_database(email)
if user:
self._user_cache[email] = user
return user
def get_active_users_by_department(self, department_ids):
"""부서별 활성 사용자 조회 (배치 처리)"""
# 여러 번의 개별 쿼리 대신 한 번의 배치 쿼리 사용
query = """
SELECT * FROM users
WHERE department_id IN %s
AND is_active = true
ORDER BY department_id, name
"""
users = self.database.fetch_all(query, (tuple(department_ids),))
# 부서별로 그룹화하여 반환
from collections import defaultdict
users_by_dept = defaultdict(list)
for user in users:
users_by_dept[user['department_id']].append(user)
return dict(users_by_dept)
def process_large_user_list(self, user_data_list):
"""대용량 사용자 데이터 처리 (제너레이터 활용)"""
def validate_and_transform_users():
for user_data in user_data_list:
if self._is_valid_user_data(user_data):
yield self._transform_user_data(user_data)
# 메모리 효율적인 배치 처리
batch_size = 1000
batch = []
for user in validate_and_transform_users():
batch.append(user)
if len(batch) >= batch_size:
self._save_user_batch(batch)
batch = []
# 마지막 배치 처리
if batch:
self._save_user_batch(batch)
|
일관된 코드 형식은 가독성을 높이고 팀 협업을 원활하게 한다.
추가 가이드라인:
- 세로 형식: 파일 크기를 적절하게 유지하고 (500 줄 이하 권장), 관련 개념은 가까이 배치한다.
- 가로 형식: 한 줄의 길이를 제한하고 (80-120 자 권장), 들여쓰기를 일관되게 사용한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| // 좋은 예: 적절한 줄 바꿈과 들여쓰기
public class UserService {
private final UserRepository userRepository;
private final EmailService emailService;
public UserService(UserRepository userRepository, EmailService emailService) {
this.userRepository = userRepository;
this.emailService = emailService;
}
public User registerUser(String username, String email, String password) {
validateUserInput(username, email, password);
User user = new User(username, email, password);
userRepository.save(user);
emailService.sendWelcomeEmail(user);
return user;
}
private void validateUserInput(String username, String email, String password) {
// 검증 로직
}
}
|
- 팀 규칙: 팀은 하나의 형식 규칙을 정하고 모든 구성원이 이를 따라야 한다. 많은 팀이 자동 형식 지정 도구 (Prettier, ESLint 등) 를 사용한다.
경계 (Boundaries)#
외부 코드와 우리 코드 사이의 경계를 깔끔하게 관리해야 한다.
추가 가이드라인:
- 서드파티 코드 캡슐화: 외부 API 를 직접 노출하지 말고 우리 코드에 맞게 래핑한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| // 나쁜 예: 서드파티 API 직접 사용
Map<Integer, User> users = new HashMap<>();
users.put(1, new User("John"));
// 코드 전체에서 직접 Map 메서드 사용
// 좋은 예: 래퍼 클래스 사용
public class UserRepository {
private Map<Integer, User> users = new HashMap<>();
public void addUser(int id, User user) {
users.put(id, user);
}
public User findUser(int id) {
return users.get(id);
}
// 기타 필요한 메서드들
}
|
- 학습 테스트: 외부 API 사용법을 익히기 위해 테스트 케이스를 작성한다.
- 깔끔한 경계: 외부 코드와의 상호작용을 최소한의 장소로 제한하고, 나머지 코드는 이러한 상호작용을 알 필요가 없게 만든다.
구현 기법#
기법 | 정의 | 구성 | 목적 | 실제 예시 |
---|
메서드 추출 (Extract Method) | 덩치가 큰 메서드를 작은 단위로 분리 | 독립적인 로직을 별도 메서드로 분리 | 코드 재사용성, 가독성 향상 | 계산, 검증 등 하위 작업을 calculateTotal() , validateUser() 등으로 분리 |
변수 추출 (Extract Variable) | 복잡한 표현식을 의미 있는 변수로 추출 | 임시 변수로 표현식 단순화 | 표현의 의미 부여, 코드 이해 용이 | if (user.age >= 18 && user.hasLicense) → canDrive = … |
조건문 단순화 (Simplify Conditional) | 조건 로직을 명확하게 표현 | 조건을 함수나 변수로 추출, Guard Clause 도입 | 조건의 의도 명확화 | if (!user.isActive) return; 처럼 조기 반환 |
인터페이스 추출 (Extract Interface) | 공통 기능을 추상화된 인터페이스로 정의 | 클라이언트 특화 인터페이스 설계 | 유연한 설계, 의존성 분리 | UserRepository , EmailService 등 인터페이스 정의 후 구현 분리 |
분류에 따른 종류 및 유형#
분류 기준 | 유형 | 설명 |
---|
적용 범위 | 메서드 레벨 | 함수 단위의 클린 코드 적용 |
| 클래스 레벨 | SOLID 원칙 기반 클래스 설계 |
| 시스템 레벨 | 전체 아키텍처의 클린 설계 |
개선 방식 | 예방적 | 개발 초기부터 클린 코드 적용 |
| 개선적 | 기존 코드에 대한 리팩토링 |
도구 사용 | 수동 | 개발자의 수동적 코드 개선 |
| 자동화 | 정적 분석 도구, 린터 활용 |
도전 과제#
도전 과제 | 설명 | 해결책 |
---|
기술 부채 (Technical Debt) | 과거에 작성된 비효율적이고 품질이 낮은 코드가 누적되어 유지보수 어려움 유발 | 점진적 리팩토링, Boy Scout Rule(캠프장보다 깨끗하게) 적용 |
프로젝트 일정 압박 | 리팩토링이나 테스트 작성 시간을 확보하지 못하는 일정 중심 개발 환경 | TDD(Test-Driven Development) 도입, 개발 중 품질 관리 병행 |
팀 문화 부재 | 코드 품질이나 일관성을 위한 리뷰나 컨벤션 문화가 자리 잡지 않음 | 코딩 컨벤션 수립, 정기적인 코드 리뷰 및 기술 세미나 운영 |
실무에서 효과적으로 적용하기 위한 고려사항 및 주의할 점#
고려사항 | 주의할 점 | 권장사항 |
---|
팀 컨벤션 수립 | 일관성 없는 코딩 스타일 | 자동화된 코드 포매터 도입 |
점진적 적용 | 과도한 리팩토링으로 인한 일정 지연 | 작은 단위부터 시작하여 점진적 개선 |
코드 리뷰 문화 | 형식적인 리뷰 | 구체적인 피드백과 학습 중심 리뷰 |
테스트 코드 작성 | 테스트 커버리지에만 집중 | 의미 있는 테스트 케이스 작성 |
문서화 | 과도한 주석 사용 | 코드 자체로 설명되도록 작성 |
최적화하기 위한 고려사항 및 주의할 점#
고려사항 | 주의할 점 | 권장사항 |
---|
메서드 분할 정도 | 과도한 메서드 분할로 인한 호출 오버헤드 | 의미 있는 단위로 적절한 분할 |
추상화 수준 | 불필요한 추상화로 인한 성능 저하 | 비즈니스 가치가 있는 추상화만 적용 |
객체 생성 | 과도한 객체 생성 | 객체 풀링, 캐싱 전략 활용 |
코드 복잡도 | 성능을 위한 복잡한 코드 작성 | 프로파일링 기반 최적화 |
알고리즘 선택 | 가독성을 위해 비효율적 알고리즘 선택 | 성능과 가독성의 균형점 찾기 |
주제와 관련하여 주목할 내용#
주제 | 항목 | 설명 |
---|
개발 방법론 | TDD (Test-Driven Development) | 테스트 우선 개발로 클린 코드 품질 보장 |
| BDD (Behavior-Driven Development) | 비즈니스 요구사항 중심의 개발 방식 |
도구 및 기술 | 정적 분석 도구 | SonarQube, ESLint 등 코드 품질 측정 |
| 자동화 도구 | Prettier, Black 등 코드 포매팅 자동화 |
설계 패턴 | 디자인 패턴 | 반복되는 설계 문제의 해결책 |
| 아키텍처 패턴 | MVC, MVP, MVVM 등 구조적 설계 |
성능 최적화 | 프로파일링 | 성능 병목점 식별 및 개선 |
| 캐싱 전략 | 메모리, 디스크 캐시 활용 |
하위 주제로 분류해서 추가적으로 학습해야할 내용#
카테고리 | 주제 | 설명 |
---|
설계 원칙 | GRASP 원칙 | 객체지향 설계의 9 가지 기본 원칙 |
| DRY 원칙 | Don’t Repeat Yourself - 중복 제거 원칙 |
| KISS 원칙 | Keep It Simple, Stupid - 단순성 추구 |
| YAGNI 원칙 | You Aren’t Gonna Need It - 필요한 것만 구현 |
리팩토링 기법 | 코드 스멜 식별 | 리팩토링이 필요한 코드 패턴 인식 |
| 리팩토링 카탈로그 | 마틴 파울러의 리팩토링 기법 목록 |
| 레거시 코드 개선 | 기존 코드의 점진적 개선 전략 |
테스트 전략 | 단위 테스트 작성법 | 효과적인 테스트 케이스 설계 |
| 통합 테스트 | 모듈 간 상호작용 테스트 |
| 테스트 더블 | Mock, Stub, Fake 객체 활용 |
코드 품질 측정 | 메트릭스 | 순환 복잡도, 결합도, 응집도 측정 |
| 정적 분석 | 코드 품질 자동 검사 도구 |
| 코드 리뷰 | 동료 검토를 통한 품질 향상 |
추가로 알아야 하거나 학습해야할 내용#
카테고리 | 주제 | 설명 |
---|
함수형 프로그래밍 | 순수 함수 | 부작용이 없는 함수 설계 |
| 불변성 | 데이터 불변성을 통한 안정성 확보 |
| 고차 함수 | 함수를 인자로 받거나 반환하는 함수 |
동시성 프로그래밍 | 스레드 안전성 | 멀티스레드 환경에서의 코드 안전성 |
| 비동기 프로그래밍 | 논블로킹 코드 작성 기법 |
| 동시성 패턴 | Producer-Consumer, Actor 모델 등 |
성능 최적화 | 알고리즘 최적화 | 시간/공간 복잡도 개선 |
| 메모리 관리 | 가비지 컬렉션, 메모리 누수 방지 |
| 캐싱 전략 | 다양한 레벨의 캐시 활용 |
아키텍처 설계 | 마이크로서비스 | 서비스 분할 및 독립 배포 |
| 이벤트 기반 아키텍처 | 느슨한 결합을 위한 이벤트 활용 |
| 도메인 주도 설계 | 비즈니스 도메인 중심 설계 |
개발 도구 | 버전 관리 | Git 브랜치 전략 및 협업 |
| CI/CD | 지속적 통합 및 배포 자동화 |
| 모니터링 | 애플리케이션 성능 모니터링 |
용어 정리#
용어 | 설명 |
---|
클린 코드 (Clean Code) | 가독성, 유지보수성, 일관성이 높은 코드로, 읽는 사람이 쉽게 이해할 수 있고 변경이 용이한 코드 |
리팩토링 (Refactoring) | 코드의 외부 동작은 그대로 유지하면서 내부 구조를 개선하는 작업 |
코드 스멜 (Code Smell) | 구조적으로 문제를 내포하고 있으며 리팩토링의 대상이 되는 코드의 징후 |
기술 부채 (Technical Debt) | 빠른 개발을 위해 품질을 희생한 코드로 인해 미래 유지보수 비용이 증가하는 현상 |
테스트 주도 개발 (TDD) | 테스트를 먼저 작성하고 해당 테스트를 통과하는 코드를 구현하는 개발 방식 |
정적 분석 도구 | 코드의 품질이나 보안 문제를 사전에 자동으로 분석해주는 도구 (예: SonarQube) |
단일 책임 원칙 (SRP) | 하나의 클래스, 함수, 모듈은 하나의 책임만을 가져야 한다는 객체지향 원칙 (SOLID 중 하나) |
중복 제거 (DRY: Don’t Repeat Yourself) | 중복된 코드를 최소화하여 재사용 가능한 형태로 유지하는 원칙 |
매직 넘버 (Magic Number) | 코드에 하드코딩된 숫자 또는 문자열로, 의미 있는 상수로 대체해야 함 |
리터럴 (Literal) | 변수에 직접 할당되는 구체적이고 고정된 값 (예: 3.14 , "hello" ) |
의존성 (Dependency) | 한 모듈이 다른 모듈에 영향을 받거나 직접 사용하는 관계 |
의존성 주입 (Dependency Injection) | 객체 간 의존성을 외부에서 주입하여 결합도를 낮추고 테스트 가능성을 높이는 설계 패턴 |
결합도 (Coupling) | 모듈 간 의존성의 정도. 낮을수록 유지보수가 용이함 |
응집도 (Cohesion) | 모듈 내부 구성 요소들의 연관성과 집중도. 높을수록 좋은 설계 |
관심사의 분리 (Separation of Concerns, SoC) | 각 모듈 또는 클래스는 하나의 관심사에만 집중해야 한다는 원칙 |
클린 아키텍처 (Clean Architecture) | 비즈니스 로직과 외부 요소를 분리하여 유지보수성과 확장성을 높이는 계층 기반 소프트웨어 아키텍처 |
코드 스타일 가이드 | 코딩 스타일, 네이밍, 들여쓰기, 주석 방식 등에 대한 팀 또는 조직의 규칙 집합 |
코드 리뷰 (Code Review) | 개발자가 작성한 코드를 동료 개발자가 검토하여 품질을 높이는 과정 |
테스트 코드 (Test Code) | 코드의 기능이나 동작을 검증하기 위한 자동화된 테스트 스크립트 |
순환 복잡도 (Cyclomatic Complexity) | 코드 내 조건 분기 수를 기반으로 한 정량적 복잡도 측정 지표 |
추상화 (Abstraction) | 복잡한 구현을 감추고 필수적인 개념만 드러내는 설계 기법 |
Domain Specific Language (DSL) | 특정 도메인에 특화된 간결하고 표현력 높은 언어 |
Boy Scout Rule | " 캠프장을 떠날 때보다 깨끗하게 " → 기존 코드를 수정할 때 항상 더 나은 방향으로 개선하라는 원칙 |
Guard Clause | 불필요한 중첩을 줄이기 위해 조건을 먼저 검사하여 조기에 반환하는 코드 패턴 |
참고 및 출처#
국내 자료#
해외 및 공식 문서#