본문 바로가기

기술 서적

[좋은 코드, 나쁜 코드] ch1-ch2

ch1 - 코드 품질

 

코드 작성의 4가지 상위 목표

1. 작동해야 한다.
2. 작동이 멈추면 안된다
3. 변화하는 요구사항에 적응해야 한다
4. 이미 존재하는 기능을 또다시 구현해서는 안된다

 

 

코드 품질의 6가지 핵심요소

1. 코드는 읽기 쉬워야 한다.
2. 코드는 예측가능해야 한다.
3. 코드를 오용하기 어렵게 만들라.
4. 코드를 모듈화하라.
5. 코드를 재사용 가능하고 일반화할 수 있게 작성하라
6. 테스트가 용이한 코드를 작성하고 제대로 테스트하라'

 

 

서두르지 않으면 더 빠르다

- 고품질 코드는 단기적으로 일정을 지연시키나 중장기적으로는 개발 시간이 단축된다


ch2 - 추상화 계층

- 문제가 매우 복잡해도 하위 문제를 식별하고 올바른 추상화 계층을 만든다면 그 복잡한 문제를 쉽게 다룰 수 있다

 

- 가독성

: 높은 계층의 추상화를 이해하기만 하면 된다

 

-모듈화

: 하위 문제 해결책을 나누고 구현이 노출되지 않으면 다른 계층이나 코드에 영향을 미치지 않고 계층 내에서만 구현을 변경하기 쉬워진다

 

- 재사용성 및 일반화성

: 하위문제 해결책이 간결한 추상화 계층으로 제시되면 해당 하위 문제에 대한 해결책을 재사용하기 쉬워진다. 또한 해결책은 여러가지 다른 사황에서 유용하게 일반화 될 가능성이 크다

 

- 테스트 용이성

: 코드가 추상화 계층으로 깨끗하게 분할되면 각 하위 문제에 대한 해결책을 완벽히 테스트하는 것이 쉬워진다

 


코드의 계층

1) 함수

2) 클래스

3) 인터페이스

4) 패키지, 네임스페이스, 모듈

 

API: 서비스를 사용할 때 알아야 할 것들에 대한 개념을 형식화 하고 구현 세부사항은 뒤에 감춘다

 

함수

- 단일업무 수행

- 잘 명명된 다른 함수를 호출해서 더 복잡한 동작 구성

 

함수를 작게 만들고 수행하는 작업을 명확하게 하면 코드 가독성과 재사용성이 높아진다


클래스

1)  줄수

: 되도록 300줄 이하로 한다 300줄 이상일 수 있으나, 무언가 잘못되고 있다는 신호일 수 있다

 

2)  응집력 : 클래스내 요소들이 얼마나 잘 속해있는지 보여주는 척도

- 순차적 응집력 : 한 요소의 출력이 다른 요소에 대한 입력으로 필요할 때

- 기능적 응집력 : 요소들이 모여 하나의 일을 성취하는데 기여할 때

 

3) 관심사의 분리

- 시스템이 각각 별개의 문제를 다루는 개별 요소로 분리되어야 한다

- 한 클래스는 오직  한가지 일에만 관심을 가져야 한다

 

4가지 핵심원칙
코드 가독성  : 클래스에 담긴 개념이 많을 수록 가독성이 저하된다
코드 모듈화 하위 문제 해결책이 하나의 클래스로 구현되어 있고 최소한의 부분만 공개되어 결합되어 있다면, 그 하위 문제에 대한 해결책 구현을 다른 클래스로 교체할 필요가 있을 때 이를 쉽게 할 수 있다
코드 재사용성 / 일반화 어떤 문제의 하위문제를 누군가 다른 상황에서 해결해야할 수 있다.
테스트 용이성 모든 부분의 건전성을 확인해야 한다

 

- 개발자들은 하위 문제를 해결하는 것이

-> 원래의 문제와는 다른 관심사인지 아니면

-> 본질적으로 원래 문제의 일부분으로 간주하여야 하는지에 대해 의견이 다를 수 있다

 

예를 들어 다음 코드를 보자

class TextSummarizer{

	String summarizeText(String text){
    	return splitIntoParagraphs(text)
        .filter(paragraph -> calculateImportance(paragraph >= IMPORTANCE_THRESHOLD)
        .join("\n\n");
    }
    
    private Double calculateImportance(String paragraph){
    	// 단락의 중요성을 파악한다
    }
    
    private List<String> splitIntoParagraph(String text){
    	//text를 단락으로 나눈다
    }
    
}

 

이 클래스는 다음과 같은 하위문제를 해결하는 코드를 지닌다

1) 텍스트를 단락으로 분할

2) 텍스트 문자열의 중요도 점수를 계산

---> 이 하위 문제는 중요한 명사, 동사, 형용사를 찾는 하위문제로 나뉨

 

이 코드는 클래스 분할을 통해 의존성 주입의 방식으로 나뉠 수 있다

 

class TextSummarizer{
	private final ParagraphFinder paragraphFinder
    privte final TextImprtanceScorer importanceScorer

	String summarizeText(String text){
    	return	paragraphFinder.find(text)
        .filter(paragraph -> 
        importanceScorer.isImportant(paragraph))
        .join("\n\n");
    }
}

class ParagraphFinder paragraphFinder{
    	//text를 단락으로 나눈다
    }

class TextImprtanceScorer {
    	// 단락의 중요성을 파악한다
}

 

그렇다면 높은 층위의 알고리즘을 구성하는 각 클래스의 모든 개념과 단계를 알 수 있다

- 단락을 찾는다

- 중요하지 않은 것을 걸러낸다

- 남아있는 단락을 연결한다

 

이렇게 리팩터링한 클래스는 이런 효과를 지닌다

- 코드가 더 모듈화되고 재구성할 수 있게 되었다
: 단락의 중요도를 계산하는 다른 방법을 시도하려면 textImportanceScorer를 인터페이스화하고 구현체를 만들면 된다

- 코드의 재사용성이 높아졌다
: 다른 문제에서 단락 finder를 사용가능하다

- 코드의 테스트 용이성이 더 높아졌다
: 각 하위 클래스에 대해 집중적 테스트 작성이 쉽다

 


인터페이스

 계층 사이를 뚜렷이 구분하고 구현 세부사항이 유출되지 않도록 하는 한가지 방법은 외부 노출 함수를 인터페이스를 통해 결정하는 것이다. 

 

- 한가지 구현만이 예상되고 다른 구현을 추가할 계획이 없더라도 인터페이스를 통한 추상화는 좋은가?

=> 수많은 소프트웨어 공학 철학은 인터페이스화를 추천한다

 

근거1. 퍼블릭 API를 매우 명확하게 보여준다

- 상위계층 인터페이스에만 의존하고 구현체에는 의존하지 않는다

 

근거2.  한 가지 구현만 필요하다고 잘못 추측한 것일 수 있다

 

근거3. 테스트를 쉽게 할 수 있다

 

근거4. 같은 클래스로 두가지 하위 문제를 해결할 수 있다

: 한 클래스가 두 개이상의 서로 다른 추상화 계층에 구현을 제공할 수 있다

: ex) LinkedList의 경우 Queue와 List를 둘다 구현 가능하다


> 층이 너무 얇아질때

 

- 계층 분리는 다음 비용이 발생한다

-- 코드 양이 많아진다 : 클래스 정의나 임포트와 같은 반복코드로 인해

-- 파일/클래스를 따라갈 때 더 많은 노력이 필요하다

-- 인터페이스 뒤에 계층을 숨기면 어떤 구현이 사용되는지 파악하는데 더 많은 노력이 필요하다

 

분리를 위한 분리는 의미가 없다. 불필요한 계층 분리는 관리포인트의 상승으로 이어진다