실전 클린 코드: 원칙부터 리팩토링까지
반응형
목차
클린 코드의 정의
클린 코드란 단순히 "작동하는 코드"가 아닌, 다음과 같은 특성을 가진 코드를 의미합니다:
- 가독성이 높은
- 명확한 의도를 가진
- 유지보수가 용이한
- 테스트가 가능한
- 중복이 없는
클린 코드의 중요성
// Bad Code
public List<int[]> getThem() {
List<int[]> list1 = new ArrayList<int[]>();
for (int[] x : theList) {
if (x[0] == 4) list1.add(x);
}
return list1;
}
// Clean Code
public List<Cell> getFlaggedCells() {
List<Cell> flaggedCells = new ArrayList<>();
for (Cell cell : gameBoard) {
if (cell.isFlagged()) {
flaggedCells.add(cell);
}
}
return flaggedCells;
}
네이밍 컨벤션
1. 의도를 분명히 하는 이름
// Bad
int d; // elapsed time in days
// Good
int elapsedTimeInDays;
int daysSinceCreation;
int daysSinceModification;
2. 검색하기 쉬운 이름
// Bad
for (int i = 0; i < 34; i++) {
s += (t[i] * 4) / 5;
}
// Good
int realDaysPerIdealDay = 4;
int WORK_DAYS_PER_WEEK = 5;
int sum = 0;
for (int i = 0; i < NUMBER_OF_TASKS; i++) {
int realTaskDays = taskEstimate[i] * realDaysPerIdealDay;
int realTaskWeeks = realTaskDays / WORK_DAYS_PER_WEEK;
sum += realTaskWeeks;
}
3. 클래스 이름
// Bad
class Data
class Info
class Manager
// Good
class Customer
class WikiPage
class Account
함수 설계
1. 작게 만들기
// Bad
public void payEmployee() {
// 100줄 이상의 복잡한 로직
}
// Good
public void payEmployee() {
validateEmployee();
calculatePay();
adjustForTaxes();
sendPayment();
logTransaction();
}
2. 단일 책임 원칙 (SRP)
// Bad
class Employee {
public void calculatePay() { /* ... */ }
public void saveToDatabase() { /* ... */ }
public void generateReport() { /* ... */ }
}
// Good
class Employee {
private Payroll payroll;
private EmployeeRepository repository;
private ReportGenerator reportGenerator;
public Money calculatePay() {
return payroll.calculate(this);
}
}
3. 인수 개수 최소화
// Bad
public void createMenu(String title, String body, String buttonText, boolean showImage) { }
// Good
public class MenuConfig {
private String title;
private String body;
private String buttonText;
private boolean showImage;
}
public void createMenu(MenuConfig config) { }
주석 작성
1. 필요한 주석과 불필요한 주석
// Bad - 불필요한 주석
// 사용자의 나이를 반환
public int getAge() {
return age;
}
// Good - 필요한 주석
/**
* RFC 2045에 따라 Base64로 인코딩된 문자열을 반환
* 특수문자는 UTF-8로 처리됨
*/
public String encodeBase64(String input) {
// 구현...
}
2. 주석 대신 코드로 표현
// Bad
// 직원이 퇴사자인지 확인
if (employee.flag == 4) { }
// Good
if (employee.isRetired()) { }
코드 구조화
1. 수직 거리
// Bad - 관련 있는 개념들이 멀리 떨어져 있음
class Report {
private String content;
public void generate() { }
private String header;
public void print() { }
private String footer;
}
// Good - 관련 있는 개념들을 모음
class Report {
// 변수 선언을 모음
private String header;
private String content;
private String footer;
// 공개 메서드를 모음
public void generate() { }
public void print() { }
}
2. 개념의 분리
// Bad - 한 클래스에 모든 책임이 있음
class UserService {
public void createUser() { }
public void sendEmail() { }
public void validateAddress() { }
public void calculateDiscount() { }
}
// Good - 책임에 따라 분리
class UserService {
private EmailService emailService;
private ValidationService validationService;
private DiscountCalculator discountCalculator;
public void createUser() {
validateUser();
saveUser();
notifyUser();
}
}
객체와 자료구조
1. 객체 노출 최소화
// Bad
class Vehicle {
public double fuelTankCapacityInLiters;
public double litersOfGasoline;
}
// Good
class Vehicle {
private double fuelTankCapacityInLiters;
private double litersOfGasoline;
public double getPercentageFuelRemaining() {
return (litersOfGasoline / fuelTankCapacityInLiters) * 100;
}
}
2. 디미터 법칙
// Bad
final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath();
// Good
class Context {
public Options getOptions() { return options; }
public String getScratchDirectoryPath() {
return options.getScratchDirectoryPath();
}
}
오류 처리
1. 예외 처리
// Bad
try {
// 매우 긴 try 블록
} catch (Exception e) {
log.error("Error", e);
}
// Good
public void processFile(String path) {
try {
openFile(path);
processContent();
closeFile();
} catch (FileNotFoundException e) {
handleMissingFile(path);
} catch (IOException e) {
handleIOError(path);
} finally {
cleanup();
}
}
2. Null 처리
// Bad
public void processUser(User user) {
if (user != null) {
if (user.getAddress() != null) {
if (user.getAddress().getCountry() != null) {
// 처리
}
}
}
}
// Good
public void processUser(User user) {
Optional<User> optionalUser = Optional.ofNullable(user);
optionalUser
.map(User::getAddress)
.map(Address::getCountry)
.ifPresent(this::processCountry);
}
리팩토링 기법
1. 메서드 추출
// Before
public void printOwing() {
printBanner();
// 미지급금 계산
double outstanding = 0.0;
for (Order order : orders) {
outstanding += order.getAmount();
}
// 상세 내역 출력
System.out.println("name: " + name);
System.out.println("amount: " + outstanding);
}
// After
public void printOwing() {
printBanner();
double outstanding = calculateOutstanding();
printDetails(outstanding);
}
private double calculateOutstanding() {
return orders.stream()
.mapToDouble(Order::getAmount)
.sum();
}
private void printDetails(double outstanding) {
System.out.println("name: " + name);
System.out.println("amount: " + outstanding);
}
2. 조건문 간소화
// Before
double disabilityAmount() {
if (seniority < 2) return 0;
if (monthsDisabled > 12) return 0;
if (isPartTime) return 0;
// 장애 수당 계산
return base * 0.1;
}
// After
double disabilityAmount() {
if (isNotEligibleForDisability()) return 0;
return base * 0.1;
}
private boolean isNotEligibleForDisability() {
return seniority < 2
|| monthsDisabled > 12
|| isPartTime;
}
테스트 코드
1. 깨끗한 테스트 코드
// Bad
@Test
public void testSomething() {
// 테스트 설정
Invoice invoice = new Invoice();
invoice.setAmount(50);
invoice.setCustomer("John");
// 실행
invoice.process();
// 검증
assertTrue(invoice.isPaid());
assertEquals(50, invoice.getAmount());
}
// Good
@Test
public void shouldMarkInvoiceAsPaidWhenProcessed() {
// Given
Invoice invoice = createInvoiceWithAmount(50);
// When
invoice.process();
// Then
assertThat(invoice.isPaid()).isTrue();
assertThat(invoice.getAmount()).isEqualTo(50);
}
2. F.I.R.S.T 원칙
// Fast
@Test
public void shouldQuicklyValidateUserInput() {
UserValidator validator = new UserValidator();
assertThat(validator.isValid("username")).isTrue();
}
// Isolated
@Test
public void shouldCreateUserIndependentOfDatabase() {
UserRepository mockRepository = mock(UserRepository.class);
UserService service = new UserService(mockRepository);
service.createUser("username");
verify(mockRepository).save(any(User.class));
}
// Repeatable
@Test
public void shouldAlwaysReturnSameResultForSameInput() {
Calculator calc = new Calculator();
assertThat(calc.add(2, 2)).isEqualTo(4);
}
실전 리팩토링 예제
복잡한 조건문 개선
// Before
public double calculatePrice(Order order) {
double price = 0;
if (order.getCustomer().getType() == CustomerType.PREMIUM) {
if (order.getAmount() > 1000) {
price = order.getAmount() * 0.9;
if (order.getDeliveryDate().isBefore(LocalDate.now().plusDays(5))) {
price = price * 0.95;
}
} else {
price = order.getAmount() * 0.95;
}
} else {
if (order.getAmount() > 1000) {
price = order.getAmount() * 0.95;
} else {
price = order.getAmount();
}
}
return price;
}
// After
public double calculatePrice(Order order) {
return new PriceCalculator(order)
.applyCustomerDiscount()
.applyQuantityDiscount()
.applyUrgentDeliveryDiscount()
.getPrice();
}
class PriceCalculator {
private final Order order;
private double price;
public PriceCalculator(Order order) {
this.order = order;
this.price = order.getAmount();
}
public PriceCalculator applyCustomerDiscount() {
if (isPremiumCustomer()) {
price *= 0.9;
}
return this;
}
public PriceCalculator applyQuantityDiscount() {
if (isLargeOrder()) {
price *= 0.95;
}
return this;
}
public PriceCalculator applyUrgentDeliveryDiscount() {
if (isUrgentDelivery()) {
price *= 0.95;
}
return this;
}
public double getPrice() {
return price;
}
private boolean isPremiumCustomer() {
return order.getCustomer().getType() == CustomerType.PREMIUM;
}
private boolean isLargeOrder() {
return order.getAmount() > 1000;
}
private boolean isUrgentDelivery() {
return order.getDeliveryDate().isBefore(LocalDate.now().plusDays(5));
}
}
마치며
클린 코드는 단순한 규칙의 집합이 아닌, 소프트웨어 개발의 철학입니다. 지속적인 연습과 개선을 통해 더 나은 코드를 작성할 수 있습니다.
핵심 원칙
- 코드는 간단명료해야 함
- 중복을 제거할 것
- 의도를 명확히 표현할 것
- 추상화 수준을 일관되게 유지할 것
- 단위 테스트를 작성할 것
추천 도서
- Clean Code (로버트 마틴)
- Refactoring (마틴 파울러)
- The Pragmatic Programmer
반응형
댓글