들어가며
"여러분, 누군가에게 좋은 코드가 뭔지 물어본다면 어떤 대답이 나올까요?
프로그래밍을 잘 모르는 사람이라면 이렇게 말할 겁니다.
'그냥 문제없이 잘 돌아가는 코드'
하지만 조금이라도 개발을 해본 사람은 다르게 이야기합니다.
'깔끔하고, 다른 사람도 쉽게 이해할 수 있고, 처음 보는 사람도 로직을 바로 파악할 수 있는 코드'
사실 우리가 코드를 작성할 때, 모든 걸 머릿속에 넣고 100% 기억할 수 있다면
굳이 클린 코드, 리팩터링 이런 걸 신경 쓸 필요 없습니다.
하지만 현실은 다르죠.
사람은 누구나 잊어버립니다.
내가 한 달 전에 짠 코드도 가물가물하고, 6개월, 1년 지나면 거의 모르는 사람이 쓴 느낌이 듭니다.
그런 상태에서 갑자기 버그가 터지거나 기능 추가가 필요하다면?
이해 안 되면 다시 처음부터 짜야 합니다.
결국 시간 낭비, 비용 낭비, 스트레스만 쌓이죠.
실제로, 기존 코드를 변경하는 데
해석 시간과 수정 시간 비율이 10:1이라는 통계가 있습니다.
다시 말해,
코드를 변경하는 데 10시간이 걸린다면
9시간 이상을 기존 코드 '분석'에 소비하고,
실제 수정 작업은 1시간도 안 걸립니다.
그리고 대부분의 오류는
기존 코드 수정 과정에서 발생합니다.
그렇다면?
- 코드가 쉽고 명확하면 → 분석 시간 단축, 오류 감소
- 코드가 어렵고 복잡하면 → 시간 낭비, 실수 급증
결국,
클린 코드는 시간을 절약하고, 품질을 지키는 가장 현실적인 방법입니다.
여기에 더해 꼭 알아야 할 개념이 바로 Technical Debt, 기술 부채입니다.
Technical Debt(기술 부채)란?
처음에는 빠른 결과를 위해 임시방편, 지저분한 코드, 복사·붙여넣기 같은 방법을 선택합니다.
당장은 시간 절약처럼 느껴지지만, 결국 그 '빚'은
유지보수 비용 증가, 개발 속도 저하, 버그 폭증으로 돌아옵니다.

변경 비용 (좌측 그래프)
- 이상적인 상황: 시간이 지나도 변경 비용은 꾸준히 낮음
- 실제 현실: 시간이 지날수록 코드가 복잡해지며 변경 비용 급증
고객 요구사항 대응 속도 (우측 그래프)
- 이상적인 상황: 지속적으로 빠른 대응 가능
- 실제 현실: 시간이 지날수록 대응 속도 둔화, 개발 지연
이 차이의 원인은 뚜렷합니다.
초기 개발 단계에서 클린 코드를 지키지 않거나,
급하게 '돌아만 가는' 코드를 양산했기 때문입니다.
이런 코드가 쌓이면
기술 부채가 늘어나고, 결국 유지보수 비용, 수정 시간, 버그 발생률이 폭발적으로 증가합니다.
클린코드 작성법
의미 있는 이름 짓기 — "이름은 의도를 드러내야 한다”
1. 발음하기 쉬운 이름
- 협업을 생각해야 한다.
- 구두로 코드를 설명하는 순간이 온다!
- → 누구나 쉽게 읽고, 말할 수 있어야.
✔️ 예시:
`genymdhms` ❌ → 의미 불명
`generateTimestamp` ✅ → '타임스탬프 생성' 바로 이해
2. 검색 쉬운 이름
- 코드 전체에서 빠르게 찾을 수 있어야.
- 모호한 한 글자 변수, 숫자만 쓰면 검색 힘들다.
✔️ 예시:
`int d;` ❌ → d가 뭘 의미?
`int elapsedTimeInDays;` ✅ → '경과 일수' 바로 확인 가능
3. 인코딩 지양
- 타입·범위 같은 정보, 이름에 억지로 집어넣지 말 것.
- 코드 읽기 어려워지고, 현대 IDE가 다 알려준다.
✔️ 예시:
`int iCount;` ❌ — `i` 붙여봤자 헷갈림
`int userCount;` ✅ — 무슨 카운트인지 명확
4. 변수·상수 이름 규칙 (명사·명사구)
- 객체, 변수 = '명사'
- 기능·동작 = '동사'로 구분
- 추상적 이름 금지
✔️ 예시:
`Customer`, `OrderList` ✅
`Data`, `Info`, `Manager` ❌ — 추상적, 용도 불분명
5. 기발한 이름 금지
- 문화 코드·농담은 개발자 바뀌면 재앙
- 직관이 최우선
✔️ 예시:
`DeleteItems()` ✅
`HolyHandGrenade()`(=수류탄) ❌ — 농담은 코멘트로! 이름은 진지하게
6. 한 개념 = 한 단어 고정
- 비슷한 의미 단어 섞으면 혼란
- 팀 내 용어 통일
✔️ 예시:
`getUser()`, `getAccount()`
`fetchData()`, `retrieveData()` 혼용 ❌
`getUser()`, `getAccount()` 통일 ✅
7. 기술 용어 적극 사용
- 알고리즘·패턴 용어는 명확하고 오해 없다.
✔️ 예시:
`AccountVisitor`, `JobQueue`, `Factory` ✅
8. 맥락 추가
- 짧은 이름으로 부족하면 접두어·전체 맥락 덧붙이기
✔️ 예시:
1firstName` → 전체에서 이름 중복 가능
`addrFirstName`, `billingFirstName` → 더 명확
함수 설계 원칙 — "작고, 명확하고, 부작용 없이"
1. 작게 설계
- 함수는 '짧을수록' 좋다
- 한눈에 읽히는 분량 목표
✔️ 예시:
10~20줄 이내 유지 중첩 블록·들여쓰기 1~2단계 이내
2. 한 가지 일만
- 함수 이름과 실제 역할 1:1 매핑
- 기능 섞이지 않게 관리
✔️ 안 좋은 예:
`saveUserAndSendEmail()` → 2가지 일 수행 ❌
✔️ 좋은 예:
`saveUser()`
`sendWelcomeEmail()` → 명확 분리 ✅
3. 서술형 이름
- 함수 이름에 의미·의도 드러내기
- 파라미터 순서 신경 안 쓰이게 → 객체 활용
✔️ 예시:
`calculateTotalPrice(cart)`
`sendEmail(toAddress, subject, body)`
4. 파라미터 최소화
- 4개 넘어가면 읽기·유지보수 난이도 급상승
- 관련 값은 객체로 묶어서 전달
✔️ 안 좋은 예:
`createUser(String name, String email, String phone, String address, String birthDate)` ❌
✔️ 좋은 예:
`createUser(UserInfo userInfo)` ✅
5. 부수 효과 금지
- 함수는 오직 '한 가지 의도된 결과'만
- 숨겨진 상태 변경 ❌
✔️ 예시:
`updateUserProfile()` 안에서 몰래 로그인 상태 변경 ❌
명확히 분리 필요
6. 오류 코드 대신 예외 사용
- `if (result == -1)` 식의 오류 코드 ❌
- 예외로 흐름 제어 → 코드 깔끔·명확
✔️ 예시:
try {
userService.register(user);
} catch (DuplicateUserException e) {
// 예외 처리 별도
}
7. 반복 금지 (DRY 원칙)
- 중복 = 잠재적 버그·유지보수 지옥
- 코드 중복 발견 즉시 함수·모듈화
8. 구조적 프로그래밍 허용
- 짧은 함수 내 `return`, `break`, `continue` 자유롭게
- 긴 함수라면 리팩토링 먼저 고려
✔️ 예시:
jif (input == null) return;
processInput(input);
복잡한 플래그 변수보다 조기 종료 활용
주석 활용 원칙 — "주석은 최후의 수단"
핵심 원칙
- 주석은 코드로 의도 표현이 실패했을 때만 사용
- 진실은 코드에 있다, 주석이 아니라!
- 읽는 사람이 코드를 보면 이해되게 만들 것
1. 나쁜 코드, 주석으로 감추지 마라
- "코드 더럽지만 주석으로 때운다" → ❌
- 근본 해결은 명확한 코드
✔️ 안 좋은 예:
// 나이 65세 이상, 시간제 근로자에게 혜택 제공
if ((employee.flags & HOURLY_FLAG) && (employee.age > 65)
✔️ 좋은 예:
if (employee.isEligibleForFullBenefits())
주석으로 설명할 필요 없이, 함수 이름 자체가 설명서
2. 의도는 코드로 표현하라
- 코드만 보고도 '왜', '무엇'을 알 수 있게
✔️ 불필요한 주석 예시:
// 급여가 5000 이상인지 확인
if (salary > 5000)
✔️ 의도가 드러나는 코드:
if (isHighIncome(salary))
3. 필요한 주석 종류 (정말 필요한 경우만!)
✅ 법적·표준 관련
✅ 정보 제공 주석 (반환값·제약 설명)
✅ 의도 설명 주석 (특수한 설계 선택 이유)
✅ 결과 주의 주석 (주의·부작용 경고)
✅ `TODO` 주석 (개선·추가 작업 예정)
✔️ 예시:
// TODO: 다음 버전에 캐싱 로직 추가 예정
4. 혐오 주석 피하기
- HTML 스타일 장황한 주석 ❌
- 쓸데없이 시끄러운 주석 ❌
✔️ 지양:
<!-- *************** 여기서부터 유저 리스트 시작 ***************** -->
코드 형식 통일 — "가독성이 곧 품질"
1. 세로 정렬 (줄 간격·블록 구분)
- 짧고 간결
- 관련 코드 가까이 배치
- 신문 기사처럼 시각적 블록화
✔️ 좋은 예:
if (isValid) {
process();
}
cleanup();
2. 가로 정렬 (공백으로 강조)
- 의미 단위 분리
- 밀접한 요소는 붙이고, 독립 개념은 띄우기
✔️ 예시:
const maxScore = 100; // 독립된 선언
add(score, bonus); // 밀접 관계는
3. 들여쓰기
- 스코프(범위) 시각화
- `if`, `for`, `while`, 함수 내부 들여쓰기 필수
✔️ 예시:
for i in range(5):
print(i)
객체 vs 자료구조 — "상황에 맞는 선택"
1. 추상화 차이
- 객체: 데이터 숨김, 동작(메서드)만 노출
- 자료구조: 데이터 직접 노출, 동작은 최소
2. 절차지향 vs 객체지향
특징 절차지향 객체지향
| 동작 추가 | 쉬움 (함수 확장) | 어려움 (새 메서드 구현) |
| 타입 추가 | 어려움 (조건 분기 증가) | 쉬움 (클래스 확장) |
✔️ 예시:
절차지향 → `calculateTax(orderType)`
객체지향 → `order.calculateTax()`
3. 디미터 법칙 (Law of Demeter)
- 객체 내부 구조 최대한 숨기기
- 체이닝 남발 ❌
✔️ 나쁜 예:
text.toLowerCase().trim().split()[0] // 객체 내부 다 드러남
✔️ 좋은 예:
text.normalizeForDisplay() // 필요한 결과만 반
4. DTO (Data Transfer Object) 활용
- 계층 간 데이터 전달 시
- 순수 데이터 구조체, 로직 최소화
✔️ 예시:
public class UserDTO {
String name;
String email;
}
5. 선택 기준
- 새로운 타입 추가 → 객체 사용
- 새로운 동작 추가 → 절차지향 활용
✔️ 정리:
단순 데이터 전달 → 자료구조
행위 중심, 캡슐화 필요 → 객체
경계 관리 — "모르는 코드와 아는 코드 구분"
1. 외부 라이브러리 주의
- 외부 라이브러리는 편리하지만, 내부 동작 완벽히 신뢰하긴 어려움
- 버전 업그레이드, 내부 변경 → 우리 코드 영향 가능
- 경계를 명확히, 직접 사용하는 부분 최소화
✔️ 예시:
Map<String, Sensor> sensorMap = new HashMap<>();
- 제네릭을 활용해 타입 안정성 확보
- 외부 라이브러리 그대로 노출 ❌ → 감싸는 구조 추천
2. 학습 테스트 필수
- 낯선 라이브러리, 프레임워크 → 작은 실험 코드로 동작 확인
- 학습 테스트는 실제 로직 오염 없이 안전하게 학습 가능
✔️ 예시:
@Test
void mapBehaviorTest() {
Map<String, String> map = new HashMap<>();
map.put("key", "value");
assertEquals("value", map.get("key"));
}
- 경계 감싸기:
public class SensorRepository {
private final Map<String, Sensor> sensors = new HashMap<>();
}
→ 외부 코드 변경 시 영향 최소화
단위 테스트 — "코드 품질의 핵심"
1. 왜 필요한가?
- 테스트는 품질의 안전망
- 유지보수, 재사용성 향상
- 테스트 코드도 '읽기 쉽고, 명확하게' 작성
2. TDD 3법칙
✅ 실패하는 테스트 먼저 → 요구사항 확인
✅ 최소한의 코드로 통과 → 오버엔지니어링 방지
✅ 리팩터링 → 깔끔한 코드로 개선
✔️ TDD 흐름 예시:
// 1단계: 실패하는 테스트
@Test
void sumShouldReturnCorrectResult() {
assertEquals(5, Calculator.sum(2, 3));
}
// 2단계: 최소 코드로 통과
public static int sum(int a, int b) {
return a + b;
}
// 3단계: 리팩터링 (필요시)
3. 테스트 코드 작성 원칙
- 성능보다 가독성 우선
- 테스트는 독립적, 빠르고 반복 가능해야 (FIRST 원칙)
FIRST 원칙:
- Fast (빠르게)
- Independent (독립적으로)
- Repeatable (반복 가능)
- Self-validating (자동 검증)
- Timely (타이밍 맞게 작성)
클래스 설계 — "책임 중심으로 깔끔하게"
1. 작고 명확하게
- 클래스는 한 가지 '책임'을 중심으로
- 이름만 들어도 역할 짐작 가능
✔️ 좋은 예:
`UserService` → 사용자 관련 로직 집중
`EmailSender` → 이메일 전송 담당
2. 단일 책임 원칙 (SRP)
- 클래스는 '변경 이유'가 하나만 있어야
- 책임 섞이면 유지보수 어려움
✔️ 안 좋은 예:
`UserManager` → 회원 관리 + 이메일 전송 모두 처리 ❌
✔️ 좋은 예:
`UserRepository`, EmailService로 분리 ✅
3. 응집도 높이기
- 인스턴스 변수 여러 메서드가 함께 사용 → 응집도 높음
- 분산되어 서로 다른 로직 많아짐 → 클래스가 과도하게 비대해짐
창발적 설계 — "테스트·리팩터링으로 자연스럽게 품질 확보"
1. 4대 단순 설계 원칙
✅ 모든 테스트 통과 → 기능 정상 동작 확인
✅ 중복 제거 → 코드 간결, 유지보수 쉬움
✅ 의도 표현 → 읽는 사람이 이해하기 쉽게
✅ 클래스·메서드 최소화 → 복잡도 낮춤
2. 테스트가 설계 개선을 이끈다
- 테스트 작성 어려움 → 설계가 안 좋은 신호
- 테스트 많을수록 결합도 낮추고 응집도 높아짐
3. 리팩터링으로 안전하게 품질 개선
- 테스트 덕분에 구조 변경해도 안정성 확보
- 반복적 리팩터링 → 중복 제거, 의도 표현, 규모 축소 가능
✔️ 리팩터링 전후 예:
// 개선 전
if (user.age > 65 && user.flags & HOURLY_FLAG) {
applyBenefits();
}
// 개선 후
if (user.isEligibleForFullBenefits()) {
applyBenefits();
}
4. 표현력 높이는 실용 팁
- 좋은 이름
- 작은 함수·클래스
- 표준 용어 일관성 있게 사용
- 단위 테스트 강화
- 읽는 사람(미래의 동료·자기 자신)을 고려
정리하면
✔️ 좋은 이름 → 코드가 스스로 말하게
✔️ 작은 함수, 단일 책임 → 읽기 쉽고 유지보수 용이
✔️ 불필요한 주석 최소화 → 코드로 의도 표현
✔️ 객체·자료구조 구분 → 상황에 맞는 설계
✔️ 테스트·리팩터링 → 품질을 지키는 안전망
✔️ 기술 부채 예방 → 미래 비용 최소화
리팩토링이 무엇일까?
지금까지 클린 코드가 왜 중요한지,
좋은 코드를 어떻게 작성해야 하는지 살펴봤습니다.
그런데 현실에서는 아무리 신경 써도
- 급하게 만든 코드
- 기능 추가하다 꼬인 로직
- 과거의 나쁜 설계
이런 것들이 남아 있기 마련입니다.
이때 필요한 게 바로 리팩토링입니다.
리팩토링의 정의
리팩토링(Refactoring)이란?
외부 동작은 그대로 유지하면서, 내부 구조를 개선하는 작업입니다.
간단히 말해,
겉보기 결과는 동일하지만, 코드 내부는 더 깔끔하고 이해하기 쉬운 형태로 바꾸는 것입니다.
왜 리팩토링이 필요한가?
✔️ 코드가 복잡해질수록 유지보수 어려움
✔️ 기능 추가·변경 시 실수 가능성 급증
✔️ 기술 부채가 누적되어 성능 저하, 개발 속도 감소
따라서 리팩토링은
✅ 기존 코드를 점진적으로 개선하고
✅ 품질을 유지하면서
✅ 새로운 요구사항에도 유연하게 대응할 수 있도록 만들어 줍니다.
리팩토링의 핵심 원칙
- 외부 동작 유지 → 기존 기능 깨지지 않아야
- 작고 점진적으로 진행 → 한번에 대규모 변경 ❌
- 테스트 기반으로 안전하게 → 실패 없는 개선
리팩토링 예시
✔️ 개선 전 — 복잡하고 의도 불명확
if ((employee.flags & HOURLY_FLAG) && (employee.age > 65)) {
applyBenefits();
}
✔️ 리팩토링 후 — 의도 명확, 가독성 개선
if (employee.isEligibleForFullBenefits()) {
applyBenefits();
}
→ 외부 동작(혜택 적용)은 동일, 내부는 더 직관적
클린코드 VS 리팩토링
클린 코드(Clean Code)란?
- 처음부터 읽기 쉽고, 이해하기 쉬운, 유지보수가 쉬운 코드 작성
- 즉, 잘 설계된 코드, 의도를 잘 표현하는 코드, 오류를 예방하는 구조
✔️ 예시:
`generateTimestamp()` → 이름만 봐도 역할을 알 수 있음
`isEligibleForFullBenefits()` → 로직을 몰라도 의미 파악 가능
클린 코드 = 처음부터 좋은 코드로 작성하는 습관
리팩토링(Refactoring)이란?
- 이미 존재하는 코드에서
- 외부 기능을 바꾸지 않고, 내부 구조를 더 나은 상태로 개선하는 작업
- 기존 코드가 복잡해졌거나, 기술 부채가 쌓였을 때 필요한 정리 과정
✔️ 예시:
복잡한 조건문을 의미 있는 함수로 분리
중복된 코드를 하나로 모으기
리팩토링 = 기존 코드를 더 좋은 코드로 개선하는 과정