Software Tech/Spring (feat.JAVA)

[클린코드] 1. 깨끗한 코드와 함수

SuperDev 2025. 5. 15. 18:28

사이드 프로젝트를 진행하면서, 내가 짠 코드를 보며 "왜 이렇게 지저분할까?"라는 생각이 들었습니다.

시간이 지난 후, 다시 코드를 수정하는 과정에서 지저분한 코드로 인해 수정에 들어가는 시간이 아깝게 느껴졌습니다.

 

이대로는 안되겠다는 생각이 들어, 로버트 C. 마틴이 쓴 "Clean Code" 라는 책을 읽었고, 코드 품질 개선에 많은 도움을 받았습니다. 좋은 내용을 정리하면서 두고 두고 되새김질을 해놓기 위해 이 블로그를 작성합니다.

 

1. 나쁜 코드, 깨끗한 코드

  • 코드를 급하게 짜게되면 나쁜 코드가 늘어날 수 있습니다. 나쁜 코드가 늘어나면 결국 생산성의 하락으로 이어집니다.
  • 깨끗한 코드는 한가지에 집중합니다.
  • 깨끗한 코드는 가독성이 좋습니다. 명쾌하게 반드시 필요한 내용만 담도록 노력 해야합니다.
  • 깨끗한 코드는 주의깊게 작성한 코드 입니다.
  • 하나의 코드 짜려면 열 개의 코드를 읽어야 합니다. 그래서 코드는 읽기 쉬어야 합니다.

 

2. 보이스카우트 규칙

 "캠프장은 처음 왔을 때보다 더 깨끗하게 해놓고 떠나라"

 

체크아웃할 때보다 좀 더 깨끗한 코드를 체크인한다면 코드는 절대 나빠지지 않을 것 입니다.

변수명을 개선하고, 조금 긴 함수를 분할하고, 약간의 중복을 제거하고, 복잡한 if문 하나를 정리하면 충분합니다.

 

 

3. 의미 있는 이름

소프트웨어에서 이름은 어디나 쓰입니다. 변수, 함수, 인수, 클래스, 패키지에도 이름이 붙습니다. 그렇다면 이름을 어떻게 하면 잘 짓을 수 있을까요?

  • 의도를 분명히 밝혀라. 변수의 존재 이유, 수행 기능, 사용 방법 등에 답해야 합니다. 주석이 필요하다면 의도를 분명히 들어내지 못했다는 말 입니다.
  • 그릇된 정보를 피하라. 코드의 의미를 흐리거나, 서로 흡사한 이름을 사용하지 않도록 주의해야 합니다.
  • 의미있게 구분하라. 의미가 불분명한 info, Data 등은 가급적 사용하지 않는 것이 좋습니다.
  • 발음하기 쉬운 이름을 사용하라. 팀 내 의사소통이 중요하기 때문이다.
  • 검색하기 쉬운 이름을 사용하라. 변수나 상수를 코드 여러 곳에서 사용한다면 검색하기 쉬운 이름을 사용해야 합니다.
  • 인코딩을 피하라. 접두어를 왠만하면 사용하지 말자.
클래스 이름은 명사나 명사구가 적합하다. Customer, WikiPage, Account 등
메서드 이름은 동사나 동사구가 적합니다. postPayment, deletePage, save 등과 get, set, is 등

 

 

4. 오버로딩 & 정적 팩토리 메서드 

생성자를 오버로딩 할 때는 정적 팩토리 메서드를 사용합니다. 이렇게 하면 더 명확하게 객체 생성 의도를 알 수 있습니다.
오버로딩(Overloading)이란, 한 클래스 내에서 같은 이름의 메서드나 생성자를 매개변수나 자료형을 다르게 정의하는 것

// 오버로딩 방식
public class Product {
    private String name;
    private int price;

    // 생성자 오버로딩
    public Product(String name) {
        this.name = name;
        this.price = 0;
    }

    public Product(String name, int price) {
        this.name = name;
        this.price = price;
    }
}

Product p1 = new Product("Book");
Product p2 = new Product("Pen", 500);

 

// 정적 팩토리 방식
public class Product {
    private String name;
    private int price;

    // private 생성자
    private Product(String name, int price) {
        this.name = name;
        this.price = price;
    }

    // 정적 팩토리 메서드
    public static Product ofName(String name) {
        return new Product(name, 0);
    }

    public static Product of(String name, int price) {
        return new Product(name, price);
    }
}

Product p1 = Product.ofName("Book");
Product p2 = Product.of("Pen", 500);

 

 

5. 함수를 잘 만드는 방법

  • 함수를 만드는 첫번째 규칙은 "작게" 입니다. 두번째 규칙은 "더 작게" 입니다. 함수가 길어질수록 역할과 의미가 흐려지고, 코드 유지보수와 가독성의 악화로 이르기 때문입니다.
  • if/else문 while문 등에 들어가는 블록은 한 줄이어야 한다는 의미 입니다.
  • 중첩구조가 생길만큼 함수가 커져서는 안됩니다. 그래야 함수는 읽고 이해하기 쉬워집니다.
  • 함수는 한 가지를 해야 한다. 그 한가지를 잘 해야 한다. 그 한 가지만을 해야 한다.

 

6. 함수당 추상화 수준은 하나로

  • 함수가 확실히 한 가지 작업만 하려면 함수 내 모든 문장의 추상화 수준이 동일해야 합니다.
  • 추상화 수준이 동일하면 코드를 위에서 아래로 쉽게 읽을 수 있다.

 

7. Switch문

  • switch문은 N가지를 처리할 수 있지만, 자칫 코드가 길어질 수 있습니다.
  • switch문을 완전히 피할 방법은 없으므로, switch문을 저차원 클래스에 숨기고 절대로 반복하지 않는 방법은 있습니다.
public Money calculatePay(Employee e) throws InvalidEmployeeType {
    switch (e.type) {
    	case COMMISSIONED:
        	return calculateCommissionedPay(e);
        case HOURLY:
        	return calculateHourlyPay(e);
        case SALARIED:
        	return calculateSalariedPay(e);
        default:
        	throw new InvalidEmployeeType(e.type);
    }
}

 

  • 위 함수에는 몇가지 문제가 있습니다. 첫째 함수가 길다. 둘째 '한 가지' 작업만 수행하지 않는다. 셋째 'SRP'를 위한한다. 넷째 새 직원 유형을 추가할 때마다 코드를 변경하기 때문 입니다.
  • 이를 다형성과 추상 팩토리를 활용하여 개선해봅시다.
public abstract class Employee {
	public abstract boolean isPayday();
    	public abstract Money calulatePay();
    	public abstract void deliverPay(Money pay);
}

------------------------------------------------

public interface EmployeeFactory {
	public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType;
}

------------------------------------------------

public class EmployeeFactoryImpl implements EmployeeFactory {
    public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType {
        switch (r.type) {
            case COMMISSIONED:
                return new CommissionedEmployee(r);
            case HOURLY:
                return new HourlyEmployee(r);
            case SALARIED:
                return new SalariedEmployee(r);
            default:
                throw new InvalidEmployeeType(r.type);
        }
    }
}

 

 

8. 함수 인수

  • 함수에서 이상적인 인수는 0개 입니다. 다음은 1개(단항), 그 다음은 2개(이항) 입니다.
  • 함수에 인수 1개를 넘기는 이유로 가장 흔한 경우는 2가지 입니다.
    • 하나는 인수에 질문을 던지는 경우 입니다.
    • 다른 하나는 인수를 뭔가로 변환해 결과를 반환하는 경우 입니다.
  • 플래그 인수는 추하다. 함수로 bool 값을 넘기는 관례는 끔찍합니다. 왜냐하면 함수가 여러가지를 처리한다고 대놓고 공표하는 셈이기 때문 입니다.
  • 인수가 2개인 함수는 1개인 함수보다 이해하기 어렵습니다. 이항 함수가 무조건 나쁜건 아니지만, 그만큼 위험이 따른다는 사실을 이해하고 가능하면 단항 함수로 바꾸도록 애써야 합니다.
  • 함수의 의도나 인수의 순서와 의도를 제대로 표현하려면 좋은 함수 이름이 필수 입니다. 단항 함수는 함수와 인수가 동사/명사 쌍을 이뤄야 합니다. 누구나 곧바로 이해할 수 있어야 합니다.
  • 함수 안에서 오류코드보다는 예외를 사용하라. (try ~ catch 문 사용)

 

9. Try / Catch 블록 뽑아내기

  • try/catch 블록은 원래 추하다. 코드 구조에 혼란을 일으키며, 정상 동작과 오류 처리 동작을 뒤섞는다. 그러므로 try/catch 블록을 별도 함수로 뽑아내는 편이 좋다.
public void delete(Page page) {
    try {
    	deletePageAndAllReferences(page);
    } catch (Exception e) {
    	logError(e);
    }
}

private void deletePageAndAllReferences(Page page) throws Exception {
    deletePage(page);
    registry.deleteReference(page.name);
    configKeys.deleteKey(page.name.makeKey());
}

private void logError(Exception e) {
    logger.log(e.getMessage());
}
  • 위 코드에서 delete 함수는 모든 오류를 처리합니다. 그래서 코드를 이해하기 쉽습니다. 이렇게 정상 동작과 오류 처리 동작을 분류하면 코드를 이해하고 수정하기 쉬워집니다.
  • 오류 처리도 한가지 작업이므로, 하나의 함수로 정의하는 것이 좋습니다.
  • 오류 코드를 반환한다는 이야기는, 클래스든 열거형 변수든, 어디선가 오류 코드를 정의한다는 뜻 입니다.
  • 오류 코드가 있는 Error enum을 사용한다는 것은 enum이 변하면 클래스 전부를 다시 컴파일 해야 합니다.
  • 오류 코드 대신 예외를 사용하면 새 예외는 Exception 클래스에서 파생됩니다.

 

10. 반복하지 마라!

  • 중복은 모든 소프트웨어에서 모든 악의 근원 입니다. 많은 원칙과 기법이 중복을 없애거나 제어할 목적으로 나왔는데, 중복을 제거할 목적으로 관계형 데이터베이스에 정규 형식을 만들었습니다.
  • 객체 지향 프로그래밍, AOP, COP 등은 코드를 부모 클래스로 몰아 중복을 줄입니다.
  • 구조적 프로그래밍 원칙 : 함수는 return문이 하나 입니다. 즉, 루프 안에서 break나 continue를 사용해선 안된다는 것 입니다.
  • 다만, 함수를 작게 만들기 위함이라면, break이나 continue를 사용해도 괜찮습니다.

 

함수를 어떻게 짜죠?

  • 소프트웨어를 짜는 행위는 글짓기와 비슷합니다. 논문이나, 기사를 작성할 때는 먼저 생각을 기록한 후 읽기 좋게 다듬는다. 초안은 대개 서투르고 어수선하므로 원하는대로 읽힐 때까지 말을 다듬고 문장을 고치고 문단을 정리합니다.
  • 함수도 마찬가지 입니다. 코드를 다듬고, 단위 테스트 케이스도 만들고, 중복을 제거하거나 클래스를 쪼개기도 합니다.
  • 함수는 길이가 짧고, 이름이 좋고, 체계가 잡혀있어야 합니다. 의도가 명확하고, 깔끔한 것이 좋습니다.
728x90