본문 바로가기
Book

[독서 기록] 클린 코드 4장, 5장, 6장 / 주석, 형식 맞추기, 객체와 자료 구조

by Renechoi 2022. 11. 9.

클린 코드, 로버트 C. 마틴, 박재호 이해영 옮김, 인사이트

 
Clean Code(클린 코드)
『Clean Code(클린 코드)』은 오브젝트 멘토(Object Mentor)의 동료들과 힘을 모아 ‘개발하며’ 클린 코드를 만드는 최상의 애자일 기법을 소개하고 있다. 소프트웨어 장인 정신의 가치를 심어 주며 프로그래밍 실력을 높여줄 것이다. 여러분이 노력만 한다면. 어떤 노력이 필요하냐고? 코드를 읽어야 한다. 아주 많은 코드를. 그리고 코드를 읽으면서 그 코드의 무엇이 옳은지, 그른지 생각도 해야 한다. 좀 더 중요하게는 전문가로서 자신이 지니는 가치와 장인으로서 자기 작품에 대한 헌신을 돌아보게 된다.
저자
로버트 C 마틴
출판
인사이트
출판일
2013.12.24

 

 

 

주석이 필요한 상황에 처하면 곰곰이 생각하기 바란다. 상황을 역전해 코드로 의도를 표현할 방법은 없을까? 

- 68p 

 

 

주석은 나쁜 코드를 보완하지 못한다

- 69p

 

 

코드로 의도를 표현하라

- 70p

 

// 직원에게 복지 혜택을 받을 자격이 있는지 검사한다.
if ((employee.flags & HOURLY_FLAG) &&
    (employee.age > 65)

다음 코드는 어떤가? 

if (employee.isEligibleForFullBenefits())

- 70p 

 

 

좋은 주석 

법적인 주석 

정보를 제공하는 주석 (abstract method, 정규식)

의도를 설명하는 주석

의미를 명료하게 밝히는 주석

결과를 경고하는 주석

TODO 주석 

중요성을 강조하는 주석

- 72 ~ 75p 

 

 

 

나쁜 주석

주절거리는 주석

같은 이야기를 중복하는 주석

오해할 여지가 있는 주석

의무적으로 다는 주석

있으나 마나 한 주석

무서운 잡음 

함수나 변수로 표현할 수 있다면 주석을 달지 마라

닫는 괄호에 다는 주석

공로를 돌리거나 저자를 표시하는 주석

주석으로 처리한 코드

html 주석

전역 정보
너무 많은 정보

모호한 관계 

- 76 ~ 94p

 


 

형식 맞추기 

 

 

개념은 빈 행으로 분리하라

 

package SingleTone;

public class Settings {

    private static class SettingsHolder {
        public static final Settings settings = new Settings();
    }

    private Settings() { }
    
    public static Settings getInstance() { return SettingsHolder.settings; }

    private boolean darkMode = false; // default false
    private int fontSize = 13; // default 13

    public  boolean getDarkMode(){return darkMode;}
    public int getFontSize(){return fontSize;}
    public void setDarkMode(boolean _darkMode){darkMode = _darkMode;}
    public void setFontSize(int _fontSize){fontSize = _fontSize;}

}

- 98p

 

 

 

세로 밀집도

줄바꿈이 개념을 분리한다면 세로 밀집도는 연관성을 의미한다. 즉, 서로 밀집한 코드 행은 세로로 가까이 놓여야 한다는 뜻이다. 목록 5-3을 살펴보자. 의미 없는 주석으로 두 인스턴스 변수를 떨어뜨려 놓았다. 

 

public class ReporterConfig {
    /**
     * 리포터 리스너의 클래스 이름
     */
    private String m_className;
    
    /**
     * 리포터 리스너의 속성
     */
    private List<Property> m_properties = new ArrayList<Property>();
    public void addProperty(Property property) {
        m_properties.add(property);
    }
    
}
public class ReporterConfig {
    private String m_className;
    private List<Property> m_properties = new ArrayList<Property>();
    
    public void addProperty(Property property) {
        m_properties.add(property);
    }
}

- 100 ~ 101p 

 

 

 

변수 선언. 변수는 사용하는 위치에 최대한 가까이 선언한다.

- 101p 

 

다음과 같이 들여쓰기로 범위를 제대로 표현한 코드를 선호한다. 

 

public class CommentWidget extnends TextWidget {
	public static final String REGEXP = "^#[^\r\n]*(?:(?:\t\n)|\n\r)?";
    
    public CommentWidget(ParentWidget parent, String text){
    	super(parent,text);
    }
    
    public String Render() throws Exception {
    	return "";
    }
 }

- 113p

 

 

public class CodeAnalyzer implements JavaFileAnalysis { 
	private int lineCount;
	private int maxLineWidth;
	private int widestLineNumber;
	private LineWidthHistogram lineWidthHistogram; 
	private int totalChars;
	
	public CodeAnalyzer() {
		lineWidthHistogram = new LineWidthHistogram();
	}
	
	public static List<File> findJavaFiles(File parentDirectory) { 
		List<File> files = new ArrayList<File>(); 
		findJavaFiles(parentDirectory, files);
		return files;
	}
	
	private static void findJavaFiles(File parentDirectory, List<File> files) {
		for (File file : parentDirectory.listFiles()) {
			if (file.getName().endsWith(".java")) 
				files.add(file);
			else if (file.isDirectory()) 
				findJavaFiles(file, files);
		} 
	}
	
	public void analyzeFile(File javaFile) throws Exception { 
		BufferedReader br = new BufferedReader(new FileReader(javaFile)); 
		String line;
		while ((line = br.readLine()) != null)
			measureLine(line); 
	}
	
	private void measureLine(String line) { 
		lineCount++;
		int lineSize = line.length();
		totalChars += lineSize; 
		lineWidthHistogram.addLine(lineSize, lineCount);
		recordWidestLine(lineSize);
	}
	
	private void recordWidestLine(int lineSize) { 
		if (lineSize > maxLineWidth) {
			maxLineWidth = lineSize;
			widestLineNumber = lineCount; 
		}
	}

	public int getLineCount() { 
		return lineCount;
	}

	public int getMaxLineWidth() { 
		return maxLineWidth;
	}

	public int getWidestLineNumber() { 
		return widestLineNumber;
	}

	public LineWidthHistogram getLineWidthHistogram() {
		return lineWidthHistogram;
	}
	
	public double getMeanLineWidth() { 
		return (double)totalChars/lineCount;
	}

	public int getMedianLineWidth() {
		Integer[] sortedWidths = getSortedWidths(); 
		int cumulativeLineCount = 0;
		for (int width : sortedWidths) {
			cumulativeLineCount += lineCountForWidth(width); 
			if (cumulativeLineCount > lineCount/2)
				return width;
		}
		throw new Error("Cannot get here"); 
	}
	
	private int lineCountForWidth(int width) {
		return lineWidthHistogram.getLinesforWidth(width).size();
	}
	
	private Integer[] getSortedWidths() {
		Set<Integer> widths = lineWidthHistogram.getWidths(); 
		Integer[] sortedWidths = (widths.toArray(new Integer[0])); 
		Arrays.sort(sortedWidths);
		return sortedWidths;
	} 
}

 

- 114 ~ 116p 

 


 

객체와 자료 구조

 

 

변수를 비공개private로 정의하는 이유가 있다. 남들이 변수에 의존하지 않게 만들고 싶어서다. ... 그렇다면 어째서 수많은 프로그래머가 조회get 함수와 설정set 함수를 당연하게 공개public해 비공개 변수를 외부에 노출할까? 

- 118p 

 

 

자료 추상화

목록 6-1과 목록 6-2에서 차이를 살펴보자. 두 클래스 모두 2차원 점을 표현한다. 그런데 한 클래스는 구현을 외부로 노출하고 다른 클래스는 구현을 완전히 숨긴다. 

 

6-1 구체적인 point 클래스

public class Point {
	public double x;
	public double y;
}

6-2 추상적인 point 클래스

public interface Point {
	double getX();
	double getY(); // 조회는 각각 가능하지만
	void setCartesian(double x, double y); // 2개의 값을 동시에 설정하도록 강제한다.
	double getR();
	double getTheta();
	void setPolar(double r, double theta);

정말 멋지게도, 목록 6-2는 점이 직교좌표계를 사용하는지 극좌표계를 사용하는지 알 길이 없다. 둘 다 아닐지도 모른다! 그럼에도 불구하고 인터페이스는 자료 구조를 명백하게 표현한다. 

 

- 118p 

 

 

변수 사이에 함수라는 계층을 넣는다고 구현이 저절로 감춰지지는 않는다. 구현을 감추려면 추상화가 필요하다! 그저 (형식 논리에 치우쳐) 조회 함수와 설정 함수로 변수를 다룬다고 클래스가 되지는 않는다. 그보다는 추상 인터페이스를 제공해 사용자가 구현을 모른 채 자료의 핵심을 조작할 수 있어야 진정한 의미의 클래스다.

- 119p

 

 

목록 6-3과 6-4를 살펴보자. 목록 6-3은 자동차 연료 상태를 구체적인 숫자 값으로 알려준다. 목록 6-4는 자동차 연료 상태를 백분율이라는 추상적인 개념으로 알려준다. 목록 -63은 두 함수가 변수값을 읽어 반환할 뿐이라는 사실이 거의 확실하다. 목록 6-4는 정보가 어디서 오는지 전혀 드러나지 않는다. 

 

6-3) 

public interface Vehicle {
	public getFuelThankCapacityInGallons();
	public getGallonsOfGasoline();
}

 

 

6-4) 

public interface Vehicle {
	double getPercentFuelRemaining();
}

 

 

자료를 세세하게 공개하기보다는 추상적인 개념으로 표현하는 편이 좋다. 인터페이스나 조회/설정 함수만으로는 추상화가 이뤄지지 않는다. 개발자는 객체가 포함하는 자료를 표현할 가장 좋은 방법을 심각하게 고민해야 한다. 아무 생각 없이 조회/설정 함수를 추가하는 방법이 가장 나쁘다.

- 119p

 

 

객체는 추상화 뒤로 자료를 숨긴 채 자료를 다루는 함수만 공개한다. 자료 구조는 자료를 그대로 공개하며 별다른 함수는 제공하지 않는다.

- 119p

 

 

 

목록 6-5는 절차적인 도형 클래스다. Geometry 클래스는 세 가지 도형 클래스를 다룬다. 각 도형 클래스는 간단한 자료 구조다. 즉, 아무 메서드도 제공하지 않는다. 도형이 동작하는 방식은 Geometry 클래스에서 구현한다.

 

6-5 절차적인 도형 

public class Square { 
	public Point topLeft; 
	public double side;
}

public class Rectangle { 
	public Point topLeft; 
	public double height; 
	public double width;
}

public class Circle { 
	public Point center; 
	public double radius;
}

public class Geometry {
	public final double PI = 3.141592653589793;

	public double area(Object shape) throws NoSuchShapeException {
		if (shape instanceof Square) { 
			Square s = (Square)shape; 
			return s.side * s.side;
		} else if (shape instanceof Rectangle) { 
			Rectangle r = (Rectangle)shape; 
			return r.height * r.width;
		} else if (shape instanceof Circle) {
			Circle c = (Circle)shape;
			return PI * c.radius * c.radius; 
		}
		throw new NoSuchShapeException(); 
	}
}

...

 

이번에는 목록 6-6을 살펴보자. 객체 지향적인 도형 클래스다. 여기서 area()는 다형 메서드다. Geometry 클래스는 필요 없다. 그러므로 새 도형을 추가해도 기존 함수에 아무런 영향을 미치지 않는다. 반면 새 함수를 추가하고 싶다면 도형 클래스를 전부 고쳐야 한다. 

 

6-6 

public class Square implements Shape { 
	private Point topLeft;
	private double side;

	public double area() { 
		return side * side;
	} 
}

public class Rectangle implements Shape { 
	private Point topLeft;
	private double height;
	private double width;

	public double area() { 
		return height * width;
	} 
}

public class Circle implements Shape { 
	private Point center;
	private double radius;
	public final double PI = 3.141592653589793;

	public double area() {
		return PI * radius * radius;
	} 
}

 

목록 6-5와 목록 6-6은 상호 보완적인 특질이 있다 사실상 반대다! 그래서 객체와 자료 구조는 근본적으로 양분된다.

 

(자료 구조를 사용하는) 절차적인 코드는 기존 자료 구조를 변경하지 않으면서 새 함수를 추가하기 쉽다. 반면, 객체 지향 코드는 기존 함수를 변경하지 않으면서 새 클래스를 추가하기 쉽다.

 

반대쪽도 참이다.

 

절차적인 코드는 새로운 자료 구조를 추가하기 어렵다. 그러려면 모든 함수를 고쳐야 한다. 객체 지향 코드는 새로운 함수를 추가하기 얼벼다. 그러려면 모든 클래스를 고쳐야 한다. 

 

다시 말해, 객체 지향 코드에서 어려운 변경은 절차적인 코드에서 쉬우며, 절차적인 코드에서 어려운 변경은 객체 지향 코드에서 쉽다. 

 

복잡한 시스템을 짜다 보면 새로운 함수가 아니라 새로운 자료 타입이 필요한 경우가 생긴다. 이때는 클래스와 객체 지향 기법이 가장 적합하다. 반면 새로운 자료 타입이 아니라 새로운 함수가 필요한 경우도 생긴다. 이때는 절차적인 코드와 자료 구조가 좀 더 적합하다.

 

분별 있는 프로그래머는 모든 것이 객체라는 생각이 미신임을 잘 안다. 때로는 단순한 자료 구조와 절찾거인 코드가 가장 적합한 상황도 있다. 

 

- 120 ~ 122p

 

 

 

디미터 법칙

 

모듈은 자신이 조작하는 객체의 속사정으 몰라야 한다는 법칙이다. 

- 123p 

 

 

기차 충돌 

final String ouputDir = ctxt.getOptions().getScratchDir().getAbsolutePath();

위 코드는 다음과 같이 나누는 편이 좋다.

Options options = ctxt.getOptions(); 
File scratchDir = options.getScratchDir();
final String ouputDir = scratchDir.getAbsolutePath();

- 123 ~ 124p

 

 

 

 

결론

객체는 동작을 공개하고 자료를 숨긴다. 그래서 기존 동작을 변경하지 않으면서 새 객체 타입을 추가하기는 쉬운 반면, 기존 객체에 새 동작을 추가하기는 어렵다. 자료 구조는 별다른 동작 없이 자료를 노출한다. 그래서 기존 자료 구조에 새 동작을 추가하기는 쉬우나, 기존 함수에 새 자료 구조를 추가하기는 어렵다.

 

시스템을 구현할 때, 새로운 자료 타입을 추가하는 유연성이 필요하면 객체가 더 적합하다. 다른 경우로 새로운 동작을 추가하는 유연성이 필요하면 자료 구조와 절차적인 코드가 적합하다. 우수한 소프트웨어 개발자는 편견 없이 이 사실을 이해해 직면한 문제에 최적인 해결책을 선택한다. 즉, 객체 지향 언어를 사용하다 해서 객체 지향적인 프로그그래밍을 하는 것이 아니라 상황에 따라서 절차 지향적인 구조를 취할 것인지 객체 지향적인 구조를 취할 것인지 판단해야한다.

 

-127 ~ 128p 

 

 

반응형