본문 바로가기

실전 클린 코드: 원칙부터 리팩토링까지

민이(MInE) 2024. 11. 25.
반응형

목차

  1. 클린 코드의 정의
  2. 네이밍 컨벤션
  3. 함수 설계
  4. 주석 작성
  5. 코드 구조화
  6. 객체와 자료구조
  7. 오류 처리
  8. 리팩토링 기법
  9. 테스트 코드

클린 코드의 정의

클린 코드란 단순히 "작동하는 코드"가 아닌, 다음과 같은 특성을 가진 코드를 의미합니다:

  • 가독성이 높은
  • 명확한 의도를 가진
  • 유지보수가 용이한
  • 테스트가 가능한
  • 중복이 없는

클린 코드의 중요성

// 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));
    }
}

마치며

클린 코드는 단순한 규칙의 집합이 아닌, 소프트웨어 개발의 철학입니다. 지속적인 연습과 개선을 통해 더 나은 코드를 작성할 수 있습니다.

핵심 원칙

  1. 코드는 간단명료해야 함
  2. 중복을 제거할 것
  3. 의도를 명확히 표현할 것
  4. 추상화 수준을 일관되게 유지할 것
  5. 단위 테스트를 작성할 것

추천 도서

  • Clean Code (로버트 마틴)
  • Refactoring (마틴 파울러)
  • The Pragmatic Programmer
반응형

댓글