코드의 표현력과 그 코드로 이루어진 함수에 아무리 신경 쓸지라도 좀 더 차원 높은 단계까지 신경 쓰지 않으면 깨끗한 코드를 얻기는 어렵다.

 

 

 

 

 

 

 

 


클래스 체계

클래스를 정의 하는 표준 자바 관례에 따르면 변수가 나오고 그다음은 함수 이렇게 추상화 단계가 순차적으로 내려단다. 그래서 프로그램은 신문 기사처럼 읽힌다.

 

캡슐화

변수와 유틸리티 함수는 가능한 공개하지 않는 편이 낫지만 반드시 숨겨야 한다는 법칙도 없다. 항상 비공개 상태를 유지할 온갖 방법을 강구한다. 캡슐화를 풀어주는 결정은 언제나 최후의 수단이다.


클래스는 작아야 한다!

클래스를 만들 때 첫 번째 규칙은 크기다. 클래스는 작아야 한다.두 번째 규칙도 크기다. 더 작아야 한다.

그 척도는 클래스가 맡은 책임을 센다.

public class SuperDashboard extends JFrame implements MetaDataUser
 public String getCustomizerLanguagePath()
 public void setSystemConfigPath(String systemConfigPath)
 public String getSystemConfigDocument()
 public void setSystemConfigDocument(String systemConfigDocument)
 public boolean getGuruState()
 public boolean getNoviceState()
 public boolean getOpenSourceState()
 public void showObject(MetaObject object)
 public void showProgress(String s)
 public boolean isMetadataDirty()
 public void setIsMetadataDirty(boolean isMetadataDirty)
 public Component getLastFocusedComponent()
 public void setLastFocused(Component lastFocused)
 public void setMouseSelectState(boolean isMouseSelected)
 public boolean isMouseSelected()
 public LanguageManager getLanguageManager()
 public Project getProject()
 public Project getFirstProject()
 public Project getLastProject()
 public String getNewProjectName()
 public void setComponentSizes(Dimension dim)
 public String getCurrentDir()
 public void setCurrentDir(String newDir)
 public void updateStatus(int dotPos, int markPos)
 public Class[] getDataBaseClasses()
 public MetadataFeeder getMetadataFeeder()
 public void addProject(Project project)
 public boolean setCurrentProject(Project project)
 public boolean removeProject(Project project)
 public MetaProjectHeader getProgramMetadata()
 public void resetDashboard()
 public Project loadProject(String fileName, String projectName)
 public void setCanSaveMetadata(boolean canSave)
 public MetaObject getSelectedObject()
 public void deselectObjects()
 public void setProject(Project project)
 public void editorAction(String actionName, ActionEvent event)
 public void setMode(int mode)
 public FileManager getFileManager()
 public void setFileManager(FileManager fileManager)
 public ConfigManager getConfigManager()
 public void setConfigManager(ConfigManager configManager)
 public ClassLoader getClassLoader()
 public void setClassLoader(ClassLoader classLoader)
 public Properties getProps()
 public String getUserHome()
 public String getBaseDir()
 public int getMajorVersionNumber()
 public int getMinorVersionNumber()
 public int getBuildNumber()
 public MetaObject pasting(
 MetaObject target, MetaObject pasted, MetaProject project)
 public void processMenuItems(MetaObject metaObject)
 public void processMenuSeparators(MetaObject metaObject)
 public void processTabPages(MetaObject metaObject)
 public void processPlacement(MetaObject object)
 public void processCreateLayout(MetaObject object)
 public void updateDisplayLayer(MetaObject object, int layerIndex)
 public void propertyEditedRepaint(MetaObject object)
 public void processDeleteObject(MetaObject object)
 public boolean getAttachedToDesigner()
 public void processProjectChangedState(boolean hasProjectChanged)
 public void processObjectNameChanged(MetaObject object)
 public void runProject()
 public void setAllowDragging(boolean allowDragging)
 public boolean allowDragging()
 public boolean isCustomizing()
 public void setTitle(String title)
 public IdeMenuBar getIdeMenuBar()
 public void showHelper(MetaObject metaObject, String propertyName)
 // ... 많은 비공개 메서드가 이어진다 ...
}

위의 클래스는 누군가는 만능이라고 할수 있지만 그러기에는 너무 함수의 양이 많다.

public class SuperDashboard extends JFrame implements MetaDataUser
 public Component getLastFocusedComponent()
 public void setLastFocused(Component lastFocused)
 public int getMajorVersionNumber()
 public int getMinorVersionNumber()
 public int getBuildNumber()
}

그렇다면 위의 클래스는? 함수는 5개 이지만 책임이 너무 많다.


단일 책임 원칙

단일 책임 원칙(Single Responsibility Principle)은 클래스나 모듈을 변경할 이유가 하나, 단 하나뿐이어야 한다는 원칙이다. SRP는 책임이라는 개념을 정의하며 적절한 클래스 크기를 제시한다.

public class Version {
 public int getMajorVersionNumber()
 public int getMinorVersionNumber()
 public int getBuildNumber()
}

SRP는 객체 지향 설계에서 더욱 중요한 개념이다. 또한 이해하고 지키기 수월한 개념이기도 하다. 하지만 설계자가 가장 무시한다.

규모가 어느 수준에 이르는 시스템은 논리가 많고도 복잡하다. 이런 복잡성을 다루려면 체계적인 정리가 필수다.

큰 클래스 몇 개가 아니라 작은 클래스 여럿으로 이뤄진 시스템이 더 바람직하다. 작은 클래스는 각자 맡은 책임이 하나며, 변경할 이유가 하나며, 다른 작은 클래스와 협력해 시스템에 필요한 동작을 수행한다.


응집도(Cohesion)

클래스는 인스턴스 변수 수가 작아야 한다. 각 클래스 메서드는 클래스 인스턴스 변수를 하나 이상 사용해야 한다.

클래스에 속한 메서드와 변수가 서로 의존하며 논리적인 단위로 묶여야한다.

public class Stack {
 private int topOfStack = 0;
 List<Integer> elements = new LinkedList<Integer>();
 public int size() {
 return topOfStack;
 }
 public void push(int element) {
 topOfStack++;
 elements.add(element);
 }
 public int pop() throws PoppedWhenEmpty {
 if (topOfStack == 0)
 throw new PoppedWhenEmpty();
 int element = elements.get(--topOfStack);
 elements.remove(topOfStack);
 return element;
 }
}

응집도를 유지하면 작은 클래스 여럿이 나온다

큰 함수를 작은 함수 여럿으로 쪼개다 보면 종종 작은 클래스 여럿으로 쪼갤 기회가 생긴다. 그러면서 프로그램에 점점 더 체계가 잡히고 구조가 투명해진다.

재구현이 아니다! 프로그램을 처음부터 다시 짜지 않는다.


변경하기 쉬운 클래스

깨끗한 시스템은 클래스를 체계적으로 정리해 변경에 수반하는 위험을 낮춘다. 체계적으로 정리해 변경에 수반하는 위험을 낮춘다.

public class Sql {
 public Sql(String table, Column[] columns)
 public String create()
 public String insert(Object[] fields)
 public String selectAll()
 public String findByKey(String keyColumn, String keyValue)
 public String select(Column column, String pattern)
 public String select(Criteria criteria)
 public String preparedInsert()
 private String columnList(Column[] columns)
 private String valuesList(Object[] fields, final Column[] columns)
 private String selectWithCriteria(String criteria)
 private String placeholderList(Column[] columns)
}

위는 변경이 필요하면 코드에 손을 대야 하고 그러면 잠정적인 위험이 존재한다.

abstract public class Sql {
 public Sql(String table, Column[] columns)
 abstract public String generate();
}
public class CreateSql extends Sql {
 public CreateSql(String table, Column[] columns)
 @Override public String generate()
}
public class SelectSql extends Sql {
 public SelectSql(String table, Column[] columns)
 @Override public String generate()
}
public class InsertSql extends Sql {
 public InsertSql(String table, Column[] columns, Object[] fields)
 @Override public String generate()
 private String valuesList(Object[] fields, final Column[] columns)
}
public class SelectWithCriteriaSql extends Sql {
 public SelectWithCriteriaSql(
 String table, Column[] columns, Criteria criteria)
 @Override public String generate()
}
public class SelectWithMatchSql extends Sql {
 public SelectWithMatchSql(
 String table, Column[] columns, Column column, String pattern)
 @Override public String generate()
}
public class FindByKeySql extends Sql
 public FindByKeySql(
 String table, Column[] columns, String keyColumn, String keyValue)
  @Override public String generate()
}
public class PreparedInsertSql extends Sql {
 public PreparedInsertSql(String table, Column[] columns)
 @Override public String generate() {
 private String placeholderList(Column[] columns)
}
public class Where {
 public Where(String criteria)
 public String generate()
}
public class ColumnList {
 public ColumnList(Column[] columns)
 public String generate()
}

위의 각 클래스는 극도로 단순하다. 코드는 순식간에 이해되고 함수 하나를 수정했다고 다른 함수가 망가질 위험도 없다.

클래스가 서로 분리되었기 때문이다.

우선 SRP를 지원한다. 여기다 객체 지향 설계에서 또 다른 핵심 원칙인 OCP도 지원한다. OCP란 클래스는 확장에 개방적이고 수정에 폐쇄적이어야 한다는 원칙이다.


변경으로부터 격리

요구사항은 변하기 마련이다. 따라서 코드도 변하기 마련이다. 우리는 인터페이스와 추상클래스를 사용해 구현이 미치는 영향을 격리한다.

상세한 구현에 의존하는 코드는 테스트가 어렵다. 테스트가 가능할 정도로 시스템의 결합도를 낮추면 유연성과 재사용성도 더욱 높아진다. 결합도가 낮다는 소리는 각 시스템 요소가 다른 요소로부터 그리고 변경으로부터 잘 격리되어 있다는 의미다. 시스템 요소가 서로 잘 격리되어 있으면 각 요소를 이해하기도 더 쉬워진다.

 이렇게 결합도를 줄이면 DIP를 따르는 클래스가 나온다. DIP는 클래스가 상세한 구현이 아니라 추상화에 의존해야 한다는 원칙이다.

' > Clean Code' 카테고리의 다른 글

[Clean Code] 9장 단위테스트  (2) 2022.09.23
[Clean Code] 6장 객체와 자료구조  (0) 2022.09.22
[Clean Code] 5장 형식맞추기  (1) 2022.09.21
[Clean Code] 4장 주석  (0) 2022.09.20
[Clean Code] 3장 함수  (0) 2022.09.19

애자일과 TDD덕택에 단위 테스트를 자동화하는 프로그래머들이 이미 많아졌으며 점점 더 늘어나는 추세다. 하지만 우리 분야에 테스트를 추가하려고 급하게 서두르는 와중에 많은 프로그래머들이 제대로 된 테스트 케이스를 작성해야 한다는 좀 더 미묘한 사실을 놓쳐버렸다.

 

 

 

 

 

 

 


TDD 법칙 세가지

-첫째 법칙 : 실패하는 단위 테스트를 작성할 때까지 실제 코드를 작성하지 않는다.

-둘째 법칙 : 컴파일은 실패하지 않으면서 실행이 실패하는 정도로만 단위 테스트를 작성한다.

-셋째 법칙 : 현재 실패하는 테스트를 통과할 정도로만 실제 코드를 작성한다.


깨끗한 테스트 코드 유지하기

테스트를 안 하느니 지저분한 테스트 코드라도 있는 편이 좋다고 판단하면 안된다.

나중에 실제 코드가 진화하거나 변화가 생기면 그에 따른 테스트 코드도 변해야 한다. 테스트 코드가 복잡할수록 실제 코드를 짜는 시간보다 테스트 케이스를 추가하는 시간이 더 걸리기 십상이다.

이는 악순환의 반복이다. 결국 테스트를 포기하게 되고 개발자는 자신의 코드가 잘 작동하는지 알 길이 없어진다.

결국 테스트 코드는 실제 코드 못지 않게 중요하다. 

테스트 코드는 이류 시민이 아니다. 테스트 코드는 사고와 설계와 주의가 필요하다. 실제 코드 못지 않게 깨끗하게 짜야 한다.

테스트는 유연성, 유지보수성, 재사용성을 제공한다.

테스트 케이스가 있으면 버그에 대한 공포가 사라지고 변경이 쉬워진다. 따라서 테스트 코드가 지저분하면 코드를 변경하는 능력이 떨어지며 코드 구조를 개선하는 능력도 떨어진다. 테스트 코드가 지저분할수록 실제 코드도 지저분해진다. 결국 테스트 코드를 잃어버리고 실제 코드도 망가진다.


깨끗한 테스트 코드

깨끗한 테스트 코드는 3가지가 필요하다. 가독성, 가독성, 가독성!

명료성, 단순성, 풍부한 표현력이 필요하다.

public void testGetPageHieratchyAsXml() throws Exception
{
 crawler.addPage(root, PathParser.parse("PageOne"));
 crawler.addPage(root, PathParser.parse("PageOne.ChildOne"));
 crawler.addPage(root, PathParser.parse("PageTwo"));
 request.setResource("root");
 request.addInput("type", "pages");
 Responder responder = new SerializedPageResponder();
 SimpleResponse response =
 (SimpleResponse) responder.makeResponse(
 new FitNesseContext(root), request);
 String xml = response.getContent();
 assertEquals("text/xml", response.getContentType());
 assertSubString("<name>PageOne</name>", xml);
 assertSubString("<name>PageTwo</name>", xml);
 assertSubString("<name>ChildOne</name>", xml);
}
public void testGetPageHieratchyAsXmlDoesntContainSymbolicLinks()
throws Exception
{
 WikiPage pageOne = crawler.addPage(root, PathParser.parse("PageOne"));
 crawler.addPage(root, PathParser.parse("PageOne.ChildOne"));
 crawler.addPage(root, PathParser.parse("PageTwo"));
  PageData data = pageOne.getData();
 WikiPageProperties properties = data.getProperties();
 WikiPageProperty symLinks = properties.set(SymbolicPage.PROPERTY_NAME);
 symLinks.set("SymPage", "PageTwo");
 pageOne.commit(data);
 request.setResource("root");
 request.addInput("type", "pages");
 Responder responder = new SerializedPageResponder();
 SimpleResponse response =
 (SimpleResponse) responder.makeResponse(
 new FitNesseContext(root), request);
 String xml = response.getContent();
 assertEquals("text/xml", response.getContentType());
 assertSubString("<name>PageOne</name>", xml);
 assertSubString("<name>PageTwo</name>", xml);
 assertSubString("<name>ChildOne</name>", xml);
 assertNotSubString("SymPage", xml);
}
public void testGetDataAsHtml() throws Exception
{
 crawler.addPage(root, PathParser.parse("TestPageOne"), "test page");
 request.setResource("TestPageOne");
 request.addInput("type", "data");
 Responder responder = new SerializedPageResponder();
 SimpleResponse response =
 (SimpleResponse) responder.makeResponse(
 new FitNesseContext(root), request);
 String xml = response.getContent();
 assertEquals("text/xml", response.getContentType());
 assertSubString("test page", xml);
 assertSubString("<Test", xml);
}

위 코드는 너무 복잡하며 테스트 코드의 의도만 흐린다. 또 잡다하고 무관한 코드까지 이해해야 테스트 케이스를 이해한다.

public void testGetPageHierarchyAsXml() throws Exception {
 makePages("PageOne", "PageOne.ChildOne", "PageTwo");
 submitRequest("root", "type:pages");
 assertResponseIsXML();
 assertResponseContains(
 "<name>PageOne</name>", "<name>PageTwo</name>", "<name>ChildOne</name>"
 );
}
public void testSymbolicLinksAreNotInXmlPageHierarchy() throws Exception {
 WikiPage page = makePage("PageOne");
 makePages("PageOne.ChildOne", "PageTwo");
 addLinkTo(page, "PageTwo", "SymPage");
 submitRequest("root", "type:pages");
 assertResponseIsXML();
 assertResponseContains(
 "<name>PageOne</name>", "<name>PageTwo</name>", "<name>ChildOne</name>"
 );
 assertResponseDoesNotContain("SymPage");
}
public void testGetDataAsXml() throws Exception {
 makePageWithContent("TestPageOne", "test page");
 submitRequest("TestPageOne", "type:data");
 assertResponseIsXML();
 assertResponseContains("test page", "<Test");
}

위의 코드가 깔끔하고 명료하며 더 이해하기 쉽다.

 

- 도메인에 특화된 테스트 언어

이는 처음부터 설계된 API가 아니고 잡다하고 세세한 사항으로 범벅된 코드를 계속 리팩터링하다가 진화된 API다.

숙련된 개발자라면 자기 코드를 좀 더 간결하고 표현력이 풍부한 코드로 리팩터링해야 마땅하다.

 

-이중 표준

실제 환경과 테스트 환경은 요구사항이 다른만큼 판이하게 개발하는 방법이 다르다.

@Test
 public void turnOnLoTempAlarmAtThreashold() throws Exception {
 hw.setTemp(WAY_TOO_COLD);
 controller.tic();
 assertTrue(hw.heaterState());
 assertTrue(hw.blowerState());
 assertFalse(hw.coolerState());
 assertFalse(hw.hiTempAlarm());
 assertTrue(hw.loTempAlarm());
 }

위 코드는 이것 저것 너무 많이 체크 한다.

@Test
 public void turnOnLoTempAlarmAtThreshold() throws Exception {
 wayTooCold();
 assertEquals("HBchL", hw.getState());
 }

테스트 당 assert 하나

assert 문이 단 하나인 함수는 결론이 하나라서 코드를 이하기 쉽고 빠르다.

public void testGetPageHierarchyAsXml() throws Exception {
 givenPages("PageOne", "PageOne.ChildOne", "PageTwo");
 whenRequestIsIssued("root", "type:pages");
 thenResponseShouldBeXML();
}
public void testGetPageHierarchyHasRightTags() throws Exception {
 givenPages("PageOne", "PageOne.ChildOne", "PageTwo");
 whenRequestIsIssued("root", "type:pages");
 thenResponseShouldContain(
 "<name>PageOne</name>", "<name>PageTwo</name>", "<name>ChildOne</name>"
 );
}

하지만 이는 중복 코드를 야기하고 중복 코드를 우회하려면 다른 복잡성을 유발한다.

 

-결국 테스트 당 개념 하나

/**
 * addMonths() 메서드를 테스트하는 장황한 코드
 */
public void testAddMonths() {
 SerialDate d1 = SerialDate.createInstance(31, 5, 2004);
 SerialDate d2 = SerialDate.addMonths(1, d1);
 assertEquals(30, d2.getDayOfMonth());
 assertEquals(6, d2.getMonth());
 assertEquals(2004, d2.getYYYY());
 SerialDate d3 = SerialDate.addMonths(2, d1);
 assertEquals(31, d3.getDayOfMonth());
 assertEquals(7, d3.getMonth());
 assertEquals(2004, d3.getYYYY());
 SerialDate d4 = SerialDate.addMonths(1, SerialDate.addMonths(1, d1));
 assertEquals(30, d4.getDayOfMonth());
 assertEquals(7, d4.getMonth());
 assertEquals(2004, d4.getYYYY());
}

결국은 각 절에 assert 문이 여럿이라는 사실이 문제가 아니다. 한 테스트 함수에서 여러 개념을 테스트한다는 사실이 문제다. 그러므로 가장 좋은 규칙은 assert문 수를 최소로 줄여라 와 테스트 함수 하나는 개념 하나만 테스트하라이다.


F.R.I.S.T

깨끗한 테스트는 다음 다섯 가지 규칙을 따른다.

 

Fast(빠르게) : 테스트는 빨리 돌아야 한다.

 

Independent(독립적으로) : 각 테스트는 서로 의존하면 안 된다.

 

Repeatable(반복가능하게) : 테스트는 어떠 환경에서도 반복 가능해야 한다.

 

Self-Validating(자가 검증하는) : 성공 아니면 실패다.

 

Timely(적시에) : 테스트는 적시에 작성해야 한다.


결론

테스트 코드는 지속적으로 깨끗하게 관리하자. 표현력을 높이고 간결하게 정리하자. 테스트 API를 구현해 도메인 특화 언어를 만들자.

' > Clean Code' 카테고리의 다른 글

[Clean Code] 10장 클래스 - Jeff Langr  (0) 2022.09.24
[Clean Code] 6장 객체와 자료구조  (0) 2022.09.22
[Clean Code] 5장 형식맞추기  (1) 2022.09.21
[Clean Code] 4장 주석  (0) 2022.09.20
[Clean Code] 3장 함수  (0) 2022.09.19

변수를 비공개로 정의하는 이유가 있다. 남들이 변수에 의존하지 않게 만들고 싶어서다. 충동이든 변덕이든, 변수 타입이나 구현을 맘대로 바꾸고 싶어서다. 그렇다면 어째서 수많은 프로그래머가 조회함수와 설정함수를 당연하게 공개해 비공개 변수를 외부에 노출할까?


자료 추상화

두 클래스는 모두 2차원 점을 표현한다. 그런데 한 클래스는 구현을 외부로 노출하고 다른 클래스는 구현을 완전히 숨긴다.

구체적인 Point 클래스
public class Point {
 public double x;
 public double y;
}
추상적인 Point 클래스
public interface Point {
 double getX();
 double getY();
 void setCartesian(double x, double y);
 double getR();
 double getTheta();
 void setPolar(double r, double theta);
}

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

개발자는 객체가 포함하는 자료를 표현할 가장 좋은 방법을 심각하게 고민해야 한다. 아무 생각 없이 조회/설정 함수를 추가하는 방법이 가장 나쁘다.


자료/객체 비대칭

객체는 추상화 뒤로 자료를 숨긴 채 자료를 다루는 함수만 공개한다. 자료 구조는 자료를 그대로 공개하며 별다른 함수는 제공하지 않는다. 아래의 각 도형 클래스는 간단한 자료 구조다. 즉 , 아무 메서드도 제공하지 않는다. 도형이 동작하는 방식은 Geometry 클래스에서 구현한다.

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

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

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

자료구조형과 객체지향형은 상호 보완적인 특질이 있다. 사실상 반대다 그래서 객체와 자료 구조는 근본적으로 양분된다.

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

반대도 참이다.

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

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


디미터 법칙

디미터 법칙은 잘 알려진 휴리스틱으로, 모듈은 자신이 조작하는 객체의 속사정을 몰라야 한다는 법칙이다.객체는 자료를 숨기고 함수를 공개한다. 즉, 객체는 조회 함수로 내부 구조를 공개하면 안 안되다는 의미다.

좀더 명확히 표현하면 디미터 법칙은 "클래스 C의 메서드 f는 다음과 같은 객체의 메서드만 호출해야 한다"고 주장한다.

·클래스 C
·f가 생성한 객체
·f 인수로 넘어온 객체
·C 인스턴스 변수에 저장된 객체

 

위 객체에서 허용된 메서드가 반환하는 객체의 메서드는 호출하면 안된다.

다음 코드는 디미터의 법칙을 어긴다

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

기차 충돌

위와 같이 이어진 코드는 피하는게 좋다. 위 코드는 다음과 같이 나누는 편이 좋다.

Options opts = ctxt.getOptions();
File scratchDir = opts.getScratchDir();
final String outputDir = scratchDir.getAbsolutePath();

위 예제가 디미터 법칙을 위반하는지 여부는 ctxt, Options,ScratchDir이 객체인지 아니면 자료 구조인지에 달렸다. 객체라면 내부 구조를 숨겨야 하므로 확실히 디미터 법칙을 위반하지만 자료 구조라면 내부 구조를 노출하므로 디미터 법칙이 적용되지 않는다.

final String outputDir = ctxt.options.scratchDir.absolutePath;

잡종구조

이런 혼란으로 말미암아 때때로 절반은 객체, 절반은 자료 구조인 잡종 구조가 나온다. 잡종 구조는 중요한 기능을 수행하는 기능을 수행하는 함수도 있고, 공개 변수나 공개 조회/설정 함수도 있다. 공개 조회/설정 함수는 비공개 변수를 그대로 노출한다. 덕택에 다른 함수가 절차적인 프로그래밍의 자료 구조 접근 방식처럼 비공개 변수를 사용하고픈 유혹에 빠지기 십상이다.

구조체 감추기

만양 ctxt, options, scratchDir이 진짜 객체라면? 그렇다면 줄줄이 사탕으로 엮어서는 안 된다. 객체라면 내부 구조를 감춰야 한다.


자료 전달 객체

자료 구조체의 전형적인 형태는 공개 변수만 있고 함수가 없는 클래스다. 이런 자료 구조체를 때로는 자료 전달 객체라 한다. DTO는 굉장히 유용한 구조체다.

좀 더 일반적인 형태는 빈(bean) 구조다. 빈은 비공개 변수를 조회/설정 함수로 조작한다. 일종의 사이비 캡슐화다.


결론

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

어련 시스템을 구현할 때, 새로운 자료 타입을 추가하는 유연성이 필요하면 객체가 더 적합하다. 다른 경우로 새로운 동작을 추가하는 유연셩이 필요하면 자료 구조와 절차적인 코드가 더 적합하다. 우수한 소프트웨어 개발자는 편견없이 이 사실을 이해해 직면한 문제에 최적인 해결책을 선택한다.

' > Clean Code' 카테고리의 다른 글

[Clean Code] 10장 클래스 - Jeff Langr  (0) 2022.09.24
[Clean Code] 9장 단위테스트  (2) 2022.09.23
[Clean Code] 5장 형식맞추기  (1) 2022.09.21
[Clean Code] 4장 주석  (0) 2022.09.20
[Clean Code] 3장 함수  (0) 2022.09.19

뚜껑을 열었을 때 독자들이 코드가 깔끔하고, 일관적이며, 꼼꼼하다고 감탄하면 좋겠다. 질서 정연하다고 탄복하면 좋겠다. 모듈을 읽으며 두 눈이 휘둥그래 놀라면 좋겠다. 전문가가 짰다는 인상을 심어주면 좋겠다. 그 대신에 술 취한 뱃 사람 한 무리가 짜놓은 듯 어수선해 보인다면 독자들은 프로젝ㅌ의 다른 측면도 똑같이 무성의한 태도로 처리했으리라 생각할 것이다.

프로그래머라면 형식을 깔끔하게 맞춰 코드를 짜야한다. 코드 형식을 맞추기 위한 간단한 규칙을 정하고 그 규칙을 착실히 따라야 한다. 팀으로 일한다면 팀이 합의해 규칙을 정하고 모두가 그 규칙을 따라야 한다. 필요하다면 규칙을 자동으로 적용하는 도구를 활용한다.


형식을 맞추는 목적

코드 형식은 중요하다. 코드 형식은 의사소통의 일환이다. 의사소통은 전문 개발자의 일차적인 의무다.

오늘 구현한 기능이 다음 버전에서 바뀔 확률은 아주 높다. 그런데 오늘 구현한 코드의 가독성은 앞으로 바뀔 코드의 품질에 지대한 영향을 미친다. 오랜시간이 지나 원래 코드의 흔적을 더 이상 찾아보기 어려울 정도로 코드가 바뀌어도 맨 처음 잡아놓은 구현 스타일과 가독성 수준은 유지보수 용이성과 확장성에 계속 영향을 미친다.


적절한 행 길이를 유지하라

반드시 지킬 엄격한 규칙은 아니지만 바람직한 규칙으로 삼으면 좋겠다. 일반적으로 큰 파일보다 작은 파일이 이해하기 쉽다.

신문 기사처럼 작성하라

소스 파일도 신문 기사와 비슷하게 작성한다. 이름은 간단하면서도 설명이 가능하게 짓는다. 이름만 보고도 올바른 모듈을 살펴보고 있는지 아닌지를 판단할 정도로 신경 써서 짓는다. 소스 파일 첫 부분은 고차원 개념과 알고리즘을 설명한다. 아래로 내려갈수록 의도를 세세하게 묘사한다. 마지막에는 가장 저차원 함수와 세부 내역이 나온다.

개념은 빈 행으로 분리하라

각 행은 수식이나 절을 나타내고, 일련의 행 묶음은 완결된 생각 하나를 표현한다. 생각 사이는 빈 행을 넣어 분리해야 마땅하다.

package fitnesse.wikitext.widgets;

import java.util.regex.*;

public class BoldWidget extends ParentWidget {
 public static final String REGEXP = "'''.+?'''";
 private static final Pattern pattern = Pattern.compile("'''(.+?)'''",
 Pattern.MULTILINE + Pattern.DOTALL
 );

 public BoldWidget(ParentWidget parent, String text) throws Exception {
 super(parent);
 Matcher match = pattern.matcher(text);
 match.find();
 addChildWidgets(match.group(1));
 }

 public String render() throws Exception {
 StringBuffer html = new StringBuffer("<b>");
 html.append(childHtml()).append("</b>");
 return html.toString();
 }
}

세로 밀집도

줄바꿈이 개념을 분리한다면 세로 밀집도는 연관성을 의미한다. 즉, 서로 밀접한 코드 행은 세로로 가까이 놓여야 한다는 뜻이다.

public class ReporterConfig {
 /**
 * 리포터 리스너의 클래스 이름
 */
 private String m_className;

 /**
 * 리포터 리스너의 속성
 */
 private List<Property> m_properties = new ArrayList<Property>();
 public void addProperty(Property property) {
 m_properties.add(property);
}

수직 거리

함수 연관 관계와 동작 방식을 이해하려고 이 함수에서 저 함수로 오가며 소스 파일을 위아래로 뒤지는 등 뺑뺑이를 돌았으나 결국은 미로 같은 코드 때문에 혼란만 생기는 경우가 있다.

서로 밀접한 개념은 세로로 가까이 둬야한다. 두 개념이 서로 다른 파일에 속한가면 규칙이 통하지 않지만 타당한 근거가 없다면 서로 밀접한 개념은 한 파일에 속해야 한다.

변수선언

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

private static void readPreferences() {
 InputStream is= null;
 try {
 is= new FileInputStream(getPreferencesFile());
 setPreferences(new Properties(getPreferences()));
 getPreferences().load(is);
 } catch (IOException e) {
 try {
if (is != null)
 is.close();
 } catch (IOException e1) {
 }
 }
}

루프를 제어하는 변수는 흔히 루프 문 내부에 선언한다.

public int countTestCases() {
 int count= 0;
 for (Test each : tests)
 count += each.countTestCases();
 return count;
}

아주 드물지만 다소 긴 함수에서 블록 상단이나 루프 직전에 변수를 선언하는 사례도 있다.

...
for (XmlTest test : m_suite.getTests()) {
 TestRunner tr = m_runnerFactory.newTestRunner(this, test);
 tr.addListener(m_textReporter);
 m_testRunners.add(tr);
 invoker = tr.getInvoker();
 for (ITestNGMethod m : tr.getBeforeSuiteMethods()) {
 beforeSuiteMethods.put(m.getMethod(), m);
 }
 for (ITestNGMethod m : tr.getAfterSuiteMethods()) {
 afterSuiteMethods.put(m.getMethod(), m);
 }
}
...

인스턴스 변수

인스턴스 변수는 클래스 맨 처음에 선언한다. 변수 간에 세로로 거리를 두지 않는다. 잘 설계한 클래스는 많은 클래스 메서드가 인스턴스 변수를 사용하기 때문이다.

인스턴스 변수는 맨 처음 또는 맨 아래 둘중 하나 이겠지만 잘 모은다는게 중요하다.

public class TestSuite implements Test {
 static public Test createTest(Class<? extends TestCase> theClass,
 String name) {
 ...
 }
 public static Constructor<? extends TestCase>
 getTestConstructor(Class<? extends TestCase> theClass)
 throws NoSuchMethodException {
 ...
 }
 public static Test warning(final String message) {
 ...
 }
 private static String exceptionToString(Throwable t) {
 ...
 }
 private String fName;
 private Vector<Test> fTests= new Vector<Test>(10);
 public TestSuite() {
 }
 public TestSuite(final Class<? extends TestCase> theClass) {
 ...
 }
 public TestSuite(Class<? extends TestCase> theClass, String name) {
 ...
 }
 ... ... ... ... ...
}

종속 함수

한 함수가 다른 함수를 호출한다면 두 함수는 세로로 가까이 배치한다. 또한 가능하다면 호출하는 함수를 호출되는 함수보다 먼저 배치한다. 그러면 프로그램이 자연스럽게 읽힌다.

public class WikiPageResponder implements SecureResponder {
 protected WikiPage page;
 protected PageData pageData;
 protected String pageTitle;
 protected Request request;
 protected PageCrawler crawler;
 public Response makeResponse(FitNesseContext context, Request request)
 throws Exception {
 String pageName = getPageNameOrDefault(request, "FrontPage");
 loadPage(pageName, context);
 if (page == null)
 return notFoundResponse(context, request);
 else
 return makePageResponse(context);
 }
 private String getPageNameOrDefault(Request request, String defaultPageName) {
 String pageName = request.getResource();
 if (StringUtil.isBlank(pageName))
 pageName = defaultPageName;
 return pageName;
 }

 protected void loadPage(String resource, FitNesseContext context) throws Exception {
 WikiPagePath path = PathParser.parse(resource);
 crawler = context.root.getPageCrawler();
 crawler.setDeadEndStrategy(new VirtualEnabledPageCrawler());
 page = crawler.getPage(context.root, path);
 if (page != null)
 pageData = page.getData();
 }

 private Response notFoundResponse(FitNesseContext context, Request request) throws Exception {
 return new NotFoundResponder().makeResponse(context, request);
 }

 private SimpleResponse makePageResponse(FitNesseContext context)
 throws Exception {
 pageTitle = PathParser.render(crawler.getFullPath(page));
 String html = makeHtml(context);
 SimpleResponse response = new SimpleResponse();
 response.setMaxAge(0);
 response.setContent(html);
 return response;
 }
...

개념적 유사성

어떤 코드는 서로 끌어당긴다. 개념적인 친화도가 높기 때문이다. 친화도가 높을수록 코도를 가까이 배치한다. 한 함수가 다른 함수를 호출하거나 변수와 그 변수를 사용하는 함수도 한 예이다. 비슷한 동작을 수행하는 일군의 함수가 좋은 예이다.

public class Assert {
 static public void assertTrue(String message, boolean condition) {
 if (!condition)
 fail(message);
 }
 static public void assertTrue(boolean condition) {
 assertTrue(null, condition);
 }
 static public void assertFalse(String message, boolean condition) {
 assertTrue(message, !condition);
 }
 static public void assertFalse(boolean condition) {
 assertFalse(null, condition);
 }
...

세로 순서

일반적으로 함수 종속성은 아래 방향으로 유지한다. 호출되는 함수를 호출하는 함수보다 나중에 배치한다. 그러면 소스 코드 모듈이 고차원에서 저차원르오 자연스럽게 내려간다.

가로 형식 맞추기

짧은 행이 바람직하다. 옛날 홀러리스가 내놓은 80자 제한은 다소 인위적이다.요즘은 모니터의 길이도 크고 하니 크게크게 만들어도 무방하다.

가로 공백과 밀집도

가로로는 공백을 사용해 밀접한 개념과 느슨한 개념을 표현한다.

private void measureLine(String line) {
 lineCount++;
 int lineSize = line.length();
 totalChars += lineSize;
 lineWidthHistogram.addLine(lineSize, lineCount);
 recordWidestLine(lineSize);
}

공백을 넣으면 두 가지 주요 요소가 확실히 나뉜다는 사실이 더욱 분명해진다.

가로정렬

public class FitNesseExpediter implements ResponseSender
{
 private Socket socket;
 private InputStream input;
 private OutputStream output;
 private Request request;
 private Response response;
 private FitNesseContext context;
 protected long requestParsingTimeLimit;
 private long requestProgress;
 private long requestParsingDeadline;
private boolean hasError;
 public FitNesseExpediter(Socket s,
 FitNesseContext context) throws Exception
 {
 this.context = context;
 socket = s;
 input = s.getInputStream();
 output = s.getOutputStream();
 requestParsingTimeLimit = 10000;
 }

가로 정렬은 매우 유용하지 못하다. 코드가 엉뚱한 부분을 강조해 진짜 의도가 가려지기 때문이다.

들여쓰기

소스 파일은 윤과도와 계층이 비슷하다.

범위로 이뤄진 계층을 표현하기 위해 우리는 코드를 들여쓴다. 들여쓰는 정도는 계층에서 코드가 자리잡은 수준에 비례한다.

public class FitNesseServer implements SocketServer { private FitNesseContext
context; public FitNesseServer(FitNesseContext context) { this.context =
context; } public void serve(Socket s) { serve(s, 10000); } public void
serve(Socket s, long requestTimeout) { try { FitNesseExpediter sender = new
FitNesseExpediter(s, context);
sender.setRequestParsingTimeLimit(requestTimeout); sender.start(); }
catch(Exception e) { e.printStackTrace(); } } }
public class FitNesseServer implements SocketServer {
 private FitNesseContext context;
 public FitNesseServer(FitNesseContext context) {
 this.context = context;
 }
 public void serve(Socket s) {
 serve(s, 10000);
 }
 public void serve(Socket s, long requestTimeout) {
 try {
 FitNesseExpediter sender = new FitNesseExpediter(s, context);
 sender.setRequestParsingTimeLimit(requestTimeout);
 sender.start();
 }
 catch (Exception e) {
 e.printStackTrace();
 }
 }
}

들여쓰기한 파일은 구조가 한눈에 들어온다.

가짜 범위

때로는 빈 while 문이나 for문을 접한다.이런 구조는 좋지 못한다. 또는 새미콜론은 새 행에다 제대로 들여써서 넣어준다.

while (dis.read(buf, 0, readBufferSize) != -1)
;

팀 규칙

프로그래머 마다 선호하는 규칙이 있지만 팀에 속한다면 자신이 선호해야 할 규칙은 바로 팀 규칙이다.

팀은 한 가지 규칙에 합의해야 한다.그리고 모든 팀원은 그 규칙을 따라야 한다.

좋은 소프트웨어 시스템은 읽기 쉬운 문서로 이뤄진다는 사실을 기억하기 바란다. 스타일은 일관적이고 매끄러워야 한다. 한 소스 파일에서 봤던 형식이 다른 소스 파일에도 쓰이리라는 신회감을 독자에게 줘야 한다. 온갖 스타일을 뒤겄어 소스 코드를 필요 이상으로 복잡하게 만드는 실수는 반드시 피한다.

' > Clean Code' 카테고리의 다른 글

[Clean Code] 9장 단위테스트  (2) 2022.09.23
[Clean Code] 6장 객체와 자료구조  (0) 2022.09.22
[Clean Code] 4장 주석  (0) 2022.09.20
[Clean Code] 3장 함수  (0) 2022.09.19
[Clean Code] 2장 의미 있는 이름  (2) 2022.09.17

나쁜 코드에 주석을 달지 마라 새로 짜라 -브라이언 W.커니핸, P.J.플라우거

잘 달린 주석은 그 어떤 정보보다 유용하다. 경솔하고 근거 없는 주석은 코드를 이해하기 어렵게 만든다. 오래되고 조잡한 주석은 거짓과 잘못된 정보를 퍼뜨려 해악을 미친다.

주석은 쉰들러 리스트가 아니다. 주석은 '순수하게 선하지' 못하다. 사실상 주석은 기껏해야 필요악이다. 프로그래밍 언어 자체가 표현력이 풍부하다면, 아니 우리에게 프로그래밍 언어를 치밀하게 사용해 의도를 표현할 능력이 있다면 주석은 필요하지 않다.

우리는 코드로 의도를 표현하지 못한다. 그래서 실패를 만회하기 위해 주석을 사용한다.

그러므로 주석이 필요한 상황에 처하면 곰곰이 생각하기 바란다. 주석은 오래될수록 코드에서 멀어진다. 오래될수록 완전히 그릇될 가능성도 커진다.

부정확한 주석은 아예 없는 주석보다 훨씬 더 나쁘다.


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

코드에 주석을 추가하는 일반적인 이유는 코드 품질이 나쁘기 때문이다. 표현력이 풍부하고 깔끔하며 주석이 거의 없는 코드가, 복잡하고 어수선하며 주석이 많이 달린 코드보다 훨씬 좋다.


코드로 의도를 표현하라!

확실히 코드만으로 의도를 설명하기 어려운 경우가 존재한다.

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

몇 초만 더 생각하면 대다수 의도를 표현할 수 있다. 많은 경우 주석으로 달려는 설명을 함수로 만들어 표현해도 충분하다.


좋은 주석

어떤 주석은 그럼에도 필요하거나 유익하다. 하지만 그럼에도 명심하자 정말로 좋은 주석은, 주석을 달지 않을 방법을 찾아낸 주석이라는 사실!

법적인 주석

때로는 회사가 정립한 구현 표준에 맞춰 법적인 이유로 특정 주석을 넣으라고 명시한다.

// Copyright (C) 2003,2004,2005 by Object Mentor, Inc. All rights reserved.
// GNU General Public License 버전 2 이상을 따르는 조건으로 배포한다.

정보를 제공하는 주석

때로는 기본적인 정보를 주석으로 제공하면 편리하다.

// 테스트 중인 Responder 인스턴스를 반환한다.
protected abstract Responder responderInstance();

가능하다면 함수 이름에 정보를 담는 편이 더 좋다.

// kk:mm:ss EEE, MMM dd, yyyy 형식이다.
Pattern timeMatcher = Pattern.compile(
 "\\d*:\\d*:\\d* \\w*, \\w* \\d*, \\d*");

위제 제시한 주석은 코드에서 사용한 정규표현식이 시각과 날짜를 뜻한다고 설명한다. 이왕이면 시각과 날짜를 변환하는 클래스를 만들어 코드를 옮겨주면 더 좋고 더 깔끔하겠다.

의도를 설명하는 주석

때때로 주석은 구현을 이해하게 도와주는 선을 넘어 결정에 깔린 의도까지 설명한다.

public int compareTo(Object o)
{
 if(o instanceof WikiPagePath)
 {
 WikiPagePath p = (WikiPagePath) o;
String compressedName = StringUtil.join(names, "");
 String compressedArgumentName = StringUtil.join(p.names, "");
 return compressedName.compareTo(compressedArgumentName);
 }
 return 1; // 오른쪽 유형이므로 정렬 순위가 더 높다.
}
public void testConcurrentAddWidgets() throws Exception {
 WidgetBuilder widgetBuilder =
 new WidgetBuilder(new Class[]{BoldWidget.class});
 String text = "'''bold text'''";
 ParentWidget parent =
 new BoldWidget(new MockWidgetRoot(), "'''bold text'''");
 AtomicBoolean failFlag = new AtomicBoolean();
 failFlag.set(false);
 // 스레드를 대량 생성하는 방법으로 어떻게든 경쟁 조건을 만들려 시도한다.
 for (int i = 0; i < 25000; i++) {
 WidgetBuilderThread widgetBuilderThread =
 new WidgetBuilderThread(widgetBuilder, text, parent, failFlag);
 Thread thread = new Thread(widgetBuilderThread);
 thread.start();
 }
 assertEquals(false, failFlag.get());
}

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

때때로 모호한 인수나 반환값은 그 의미를 읽기 좋게 표현하면 이해하기 쉬워진다. 일반적으로는 인수나 반환값 자체를 명확하게 만들면 더 좋겠지만, 인수나 반환값이 표준 라이브러리나 변경하지 못하는 코드에 속한다면 의미를 명료하게 밝히는 주석이 유용하다.

public void testCompareTo() throws Exception
{
 WikiPagePath a = PathParser.parse("PageA");
 WikiPagePath ab = PathParser.parse("PageA.PageB");
 WikiPagePath b = PathParser.parse("PageB");
 WikiPagePath aa = PathParser.parse("PageA.PageA");
 WikiPagePath bb = PathParser.parse("PageB.PageB");
 WikiPagePath ba = PathParser.parse("PageB.PageA");
 assertTrue(a.compareTo(a) == 0); // a == a
 assertTrue(a.compareTo(b) != 0); // a != b
 assertTrue(ab.compareTo(ab) == 0); // ab == ab
 assertTrue(a.compareTo(b) == -1); // a < b
 assertTrue(aa.compareTo(ab) == -1); // aa < ab
 assertTrue(ba.compareTo(bb) == -1); // ba < bb
 assertTrue(b.compareTo(a) == 1); // b > a
 assertTrue(ab.compareTo(aa) == 1); // ab > aa
 assertTrue(bb.compareTo(ba) == 1); // bb > ba
}

그릇된 주석을 달아놓을 위험은 상당히 높다!!!

결과를 경고하는 주석

때로 다른 프로그래머에게 결과를 경고할 목적으로 주석을 사용한다.

// 여유 시간이 충분하지 않다면 실행하지 마십시오.
public void _testWithReallyBigFile()
{
 writeLinesToFile(10000000);
 response.setBody(testFile);
 response.readyToSend(this);
 String responseString = output.toString();
 assertSubString("Content-Length: 1000000000", responseString);
 assertTrue(bytesSent > 1000000000);
}

TODO 주석

때로는 '앞으로 할 일'을 주석으로 남겨두면 편하다.

// TODO-MdM 현재 필요하지 않다.
// 체크아웃 모델을 도입하면 함수가 필요 없다.
protected VersionInfo makeVersion() throws Exception
{
 return null;
}

중요성을 강조하는 주석

자칫 대수롭지 않다고 여겨질 뭔가의 중요성을 강조하기 위해서도 주석을 사용한다.

String listItemContent = match.group(3).trim();
// 여기서 trim은 정말 중요하다. trim 함수는 문자열에서 시작 공백을 제거한다.
// 문자열에 시작 공백이 있으면 다른 문자열로 인식되기 때문이다.
new ListItemWidget(this, listItemContent, this.level + 1);
return buildList(text.substring(match.end()));

나쁜 주석

대다수 주석이 이 범주에 속한다. 일반적으로 대다수 주석은 허술한 코드를 지탱하거나, 엉성한 코드를 변명하거나, 미숙한 결정을 합리화하는 등 프로그래머가 주절거리는 독백에서 크게 벗어나지 못한다.

주절거리는 주석

주석을 달기로 결정했으면 충분한 시간을 들여 최고의 주석을 단다.

public void loadProperties()
{
 try
 {
 String propertiesPath = propertiesLocation + "/" + PROPERTIES_FILE;
 FileInputStream propertiesStream = new FileInputStream(propertiesPath);
 loadedProperties.load(propertiesStream);
 }
 catch(IOException e)
 {
 // 속성 파일이 없다면 기본값을 모두 메모리로 읽어 들였다는 의미다.
 }
}

catch 블록에 있는 주석은 무슨 뜻인지 저자에게만 의미가 있고 다른 사람들에게까지는 전달되지 않는다.

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

헤더에 달린 주석이 같은 코드 내용을 그래도 중복한다. 자칫하면 코드보다 주석을 읽는 시간이 더 오래 걸린다.

// this.closed가 true일 때 반환되는 유틸리티 메서드다.
// 타임아웃에 도달하면 예외를 던진다.
public synchronized void waitForClose(final long timeoutMillis)
throws Exception
{
 if(!closed)
 {
 wait(timeoutMillis);
 if(!closed)
 throw new Exception("MockResponseSender could not be closed");
 }
}

오해할 여지가 있는 주석

때때로 의도는 좋았으나 프로그래머가 딱 맞을 정도로 엄밀하게는 주석을 달지 못하기도 한다. 살짝 잘못된 정보로 인해 잘못된 함수를 사용해서 잘못된 값이 반환되기도 한다.

의무적으로 다는 주석

모든 함수에 javaDocs를 달거나 모든 변수에 주석을 달아야 한다는 규칙은 어리석기 그지없다. 이런 주석은 코드를 복잡하게 만들며, 거짓말을 퍼뜨리고, 혼동과 무질서를 초래한다.

/**
 *
 * @param title CD 제목
 * @param author CD 저자
 * @param tracks CD 트랙 숫자
 * @param durationInMinutes CD 길이(단위: 분)
 */
public void addCD(String title, String author,
 int tracks, int durationInMinutes) {
 CD cd = new CD();
 cd.title = title;
 cd.author = author;
 cd.tracks = tracks;
 cd.duration = durationInMinutes;
 cdList.add(cd);
}

이력을 기록하는 주석

때때로 사람들은 모듈을 편집할 때마다 모듈 첫머리에 주석을 추가한다. 지금까지 모듈에 가한 변경을 기록하는 일지 또는 로그용이다.


* 변경 이력 (11-Oct-2001부터)
 * --------------------------
 * 11-Oct-2001 : 클래스를 다시 정리하고 새로운 패키지인
 * com.jrefinery.date로 옮겼다 (DG);
 * 05-Nov-2001 : getDescription() 메서드를 추가했으며
 * NotableDate class를 제거했다 (DG);
 * 12-Nov-2001 : IBD가 setDescription() 메서드를 요구한다. NotableDate
 * 클래스를 없앴다 (DG); getPreviousDayOfWeek(),
 * getFollowingDayOfWeek(), getNearestDayOfWeek()를 변경해
 * 버그를 수정했다 (DG);
 * 05-Dec-2001 : SpreadsheetDate 클래스에 존재하는 버그를 수정했다 (DG);
 * 29-May-2002 : month 상수를 독자적인 인터페이스로 옮겼다
 * (MonthConstants) (DG);
 * 27-Aug-2002 : addMonths() 메서드에 있는 버그를 수정했다. N???levka Petr 덕분이다 (DG);
 * 03-Oct-2002 : Checkstyle이 보고한 오류를 수정했다 (DG);
 * 13-Mar-2003 : Serializable을 구현했다 (DG);
 * 29-May-2003 : addMonths 메서드에 있는 버그를 수정했다 (DG);
 * 04-Sep-2003 : Comparable을 구현했다. isInRange Javadocs를 갱신했다 (DG);
 * 05-Jan-2005 : addYears() 메서드에 있는 버그를 수정했다 (1096282) (DG);

혼란만 가중된다.

있으나 마나 한 주석

너무 당연한 사실을 언급하며 새로운 정보를 제공하지 못하는 주석이다.

/**
 * 기본 생성자
 */
protected AnnualDateRule() {
}

/** 월 중 일자 */
 private int dayOfMonth;

무서운 잡음

public class wc {
public static void main(String[] args) {
 BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
 String line;
 int lineCount = 0;
 int charCount = 0;
 int wordCount = 0;
 try {
 while ((line = in.readLine()) != null) {
 lineCount++;
 charCount += line.length();
 String words[] = line.split("\\W");
 wordCount += words.length;
 } //while
 System.out.println("wordCount = " + wordCount);
 System.out.println("lineCount = " + lineCount);
 System.out.println("charCount = " + charCount);
 } //try
catch (IOException e) {
 System.err.println("Error:" + e.getMessage());
 } //catch
} //main
}

때로는 javadocs도 잡음이다.

/** The name. */
private String name;
/** The version. */
private String version;
/** The licenceName. */
private String licenceName;
/** The version. */
private String info;

단지 문서를 작성하기 위한 주석이다. 함수나 변수로 표현할 수 있다면 주석을 달지 마라.

위치를 표시하는 주석

소스 파일에서 특정 위치를 표시하려 주석을 사용한다.

닫는 괄호에 다는 주석

닫는 괄호는 작고 캡슐화된 함수에는 잡음일 뿐이다. 그러므로 닫는 괄호에 주석을 달아야겠다는 생각이 든다면 대신에 함수를 줄이려 시도하자.

public class wc {
public static void main(String[] args) {
 BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
 String line;
 int lineCount = 0;
 int charCount = 0;
 int wordCount = 0;
 try {
 while ((line = in.readLine()) != null) {
 lineCount++;
 charCount += line.length();
 String words[] = line.split("\\W");
 wordCount += words.length;
 } //while
 System.out.println("wordCount = " + wordCount);
 System.out.println("lineCount = " + lineCount);
 System.out.println("charCount = " + charCount);
 } //try
catch (IOException e) {
 System.err.println("Error:" + e.getMessage());
 } //catch
} //main
}

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

소스 코드 관리 시스템은 누가 언제 무엇을 추가했는지 귀신처럼 기억한다. 저자 이름으로 코드를 오염시킬 필요가 없다.

주석으로 처리한 코드

주석으로 처리한 코드만큼 이상한 코드도 없다.

InputStreamResponse response = new InputStreamResponse();
response.setBody(formatter.getResultStream(), formatter.getByteCount());
// InputStream resultsStream = formatter.getResultStream();
// StreamReader reader = new StreamReader(resultsStream);
// response.setContent(reader.read(formatter.getByteCount()));

꼭 이유가 있어서 남겨놓은것 같지만 아니다.!

HTML 주석

소스 코드에서 HTML 주석은 혐오 그 자체다. 다음 코드를 읽어보면 무슨 말인지 알리라.

/**
 * 적합성 테스트를 수행하기 위한 과업
 * 이 과업은 적합성 테스트를 수행해 결과를 출력한다.
 * <p/>
 * <pre>
 * 용법:
 * &lt;taskdef name=&quot;execute-fitnesse-tests&quot;
 * classname=&quot;fitnesse.ant.ExecuteFitnesseTestsTask&quot;
 * classpathref=&quot;classpath&quot; /&gt;
 * 또는
 * &lt;taskdef classpathref=&quot;classpath&quot;
 * resource=&quot;tasks.properties&quot; /&gt;
 * <p/>
 * &lt;execute-fitnesse-tests
 * suitepage=&quot;FitNesse.SuiteAcceptanceTests&quot;
 * fitnesseport=&quot;8082&quot;
 * resultsdir=&quot;${results.dir}&quot;
 * resultshtmlpage=&quot;fit-results.html&quot;
 * classpathref=&quot;classpath&quot; /&gt;
 * </pre>
 */

전역 정보

주석을 달아야 한다면 근처에 있는 코드만 기술하다. 코드 일부에 주석을 달면서 시스템의 전반적인 정보를 기술하지 마라.

/**
 * 적합성 테스트가 동작하는 포트: 기본값은 <b>8082</b>.
 *
 * @param fitnessePort
 */
public void setFitnessePort(int fitnessePort)
{
 this.fitnessePort = fitnessePort;
}

너무 많은 정보

주석에다 흥미로운 역사나 관련 없는 정보를 장황하게 늘어놓지 마라.

/*
 RFC 2045 - Multipurpose Internet Mail Extensions (MIME)
 1부: 인터넷 메시지 본체 형식
 6.8절. Base64 내용 전송 인코딩(Content-Transfer-Encoding)
인코딩 과정은 입력 비트 중 24비트 그룹을 인코딩된 4글자로 구성된
출력 문자열로 표현한다. 왼쪽에서 오른쪽으로 진행해가며, 3개를 묶어 8비트 입력
그룹을 형성한다. 이렇게 만들어진 24비트는 4개를 묶어 6비트 그룹으로 취급하며,
각각은 base64 알파벳에서 단일 자릿수로 해석된다.
 base64 인코딩으로 비트 스트림을 인코딩할 때, 비트 스트림은
 MSB(Most Significant Bit) 우선으로 정렬되어 있다고 가정한다. 따라서, 스트림에서
첫 번째 비트는 첫 8비트 바이트에서 최상위 비트가 되며, 여덟번째 비트는 첫 8비트
바이트에서 최하위 비트가 된다.
*/

모호한 관계

주석과 주석이 설명하는 코드는 둘 사이 관계가 명백해야 한다. 이왕 공들여 주석을 달았다면 적어도 독자가 주석과 코드를 읽어보고 무슨 소린지 알아야한다.

/*
 * 모든 픽셀을 담을 만큼 충분한 배열로 시작한다(여기에 필터 바이트를 더한다).
 * 그리고 헤더 정보를 위해 200바이트를 더한다.
 */
this.pngBytes = new byte[((this.width + 1) * this.height * 3) + 200];

함수 헤더

짧은 함수는 긴 설명이 필요 없다.짧고 한 가지만 수행하며 이름을 잘 붙인 함수가 주석으로 헤더를 추가한 함수보다 훨씬 좋다.

' > Clean Code' 카테고리의 다른 글

[Clean Code] 6장 객체와 자료구조  (0) 2022.09.22
[Clean Code] 5장 형식맞추기  (1) 2022.09.21
[Clean Code] 3장 함수  (0) 2022.09.19
[Clean Code] 2장 의미 있는 이름  (2) 2022.09.17
[Clean Code] 1장 깨끗한 코드  (1) 2022.09.16

들어가기

3분의 시간동안 아래의 코드를 한번 이해해본다!

public static String testableHtml(
 PageData pageData,
 boolean includeSuiteSetup
) throws Exception {
 WikiPage wikiPage = pageData.getWikiPage();
 StringBuffer buffer = new StringBuffer();
 if (pageData.hasAttribute("Test")) {
 if (includeSuiteSetup) {
 WikiPage suiteSetup =
 PageCrawlerImpl.getInheritedPage(
 SuiteResponder.SUITE_SETUP_NAME, wikiPage
 );
 if (suiteSetup != null) {
 WikiPagePath pagePath =
 suiteSetup.getPageCrawler().getFullPath(suiteSetup);
 String pagePathName = PathParser.render(pagePath);
 buffer.append("!include -setup .")
 .append(pagePathName)
 .append("\n");
 }
 }
 WikiPage setup =
 PageCrawlerImpl.getInheritedPage("SetUp", wikiPage);
 if (setup != null) {
 WikiPagePath setupPath =
 wikiPage.getPageCrawler().getFullPath(setup);
 String setupPathName = PathParser.render(setupPath);
 buffer.append("!include -setup .")
.append(setupPathName)
 .append("\n");
 }
 }
 buffer.append(pageData.getContent());
 if (pageData.hasAttribute("Test")) {
 WikiPage teardown =
 PageCrawlerImpl.getInheritedPage("TearDown", wikiPage);
 if (teardown != null) {
 WikiPagePath tearDownPath =
 wikiPage.getPageCrawler().getFullPath(teardown);
 String tearDownPathName = PathParser.render(tearDownPath);
 buffer.append("\n")
 .append("!include -teardown .")
 .append(tearDownPathName)
 .append("\n");
 }
 if (includeSuiteSetup) {
 WikiPage suiteTeardown =
 PageCrawlerImpl.getInheritedPage(
 SuiteResponder.SUITE_TEARDOWN_NAME,
 wikiPage
 );
 if (suiteTeardown != null) {
 WikiPagePath pagePath =
 suiteTeardown.getPageCrawler().getFullPath (suiteTeardown);
 String pagePathName = PathParser.render(pagePath);
 buffer.append("!include -teardown .")
 .append(pagePathName)
 .append("\n");
 }
 }
 }
 pageData.setContent(buffer.toString());
 return pageData.getHtml();
}

위의 코드는 추상화도 너무 다양하고 코드의 길이도 길뿐더러 정리도 되어 있지 않다.

public static String renderPageWithSetupsAndTeardowns(
 PageData pageData, boolean isSuite
) throws Exception {
 boolean isTestPage = pageData.hasAttribute("Test");
 if (isTestPage) {
 WikiPage testPage = pageData.getWikiPage();
 StringBuffer newPageContent = new StringBuffer();
 includeSetupPages(testPage, newPageContent, isSuite);
 newPageContent.append(pageData.getContent());
 includeTeardownPages(testPage, newPageContent, isSuite);
 pageData.setContent(newPageContent.toString());
 }
 return pageData.getHtml();
}

하지만 다음 아래의 코드는 비교적 위의 코드보다 이해하기 쉬울것이다.


작게 만들어라!

함수를 만드는 첫째 규칙은 "작게!'다. 함수를 만드는 둘째 규칙은 "더 작게!"다.

각 함수가 너무도 명백하고 각 함수가 이야기 하나를 표현해라. 이것이 답이다.


한 가지만 해라!

함수는 한 가지를 해야 한다. 그 한 가지를 잘 해야 한다. 그 한 가지만을 해야 한다.

함수가 '한 가지'만 하는지 판단하는 방법이 하나 있다. 단순히 다른 표현이 아니라 의미 있는 이름으로 다른 함수를 추출할 수 있다면 그 함수는 여러 작업을 하는 셈이다.


함수 당 추상화 수준은 하나로!

함수가 확실히 '한 가지' 작업만 하려면 함수 내 모든 문장의 추상화 수준이 동일해야 한다.

한 함수 내에 추상화 수준을 섞으면 코드를 읽는 사람이 헷갈린다. 특정 표현이 근본 개념인지 아니면 세부사항인지 구분하기 어려운 탓이다. 하지만 문제는 이 정도로 그치지 않는다. 근본 개념과 세부사항을 뒤섞기 시작하면, 깨어진 창문처럼 사람들이 함수에 세부사항을 점점 더 추가한다.

위에서 아래로 코드 읽기: 내려가기 규칙

코드는 위에서 아래로 이야기 처럼 읽혀야 좋다. 한 함수 다음에는 추상화 수준이 한 단계 낮은 함수가 온다. 즉, 위에서 아래로 프로그램을 읽으면 함수 추상화 수준이 한 번에 한 단계씩 낮아진다. 이것이 내려기가 규칙이다.


Switch 문

switch 문은 작게 만들기 어렵다. case 분기가 단 두개여도 코드가 길어지기 때문이다. 또한 '한 가지' 작업만 하는 switch문도 만들기 어렵다. 이유는 본질적으로 switch 문은 N가지를 처리하기 깨문이다.

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(single responsibility principle)를 위반한다. 넷째, OCP(open closed principle)을 위반한다.

위를 해결하기 위해서는 switch문을 추상 팩토리에 숨긴다. 팩토리는 switch 문을 사용해 적절한 Employee 파생 클래스의 인스턴스를 생성한다.그러면 다형성으로 인해 실제 파생 클래스의 함수가 실행된다.

public abstract class Employee {
 public abstract boolean isPayday();
 public abstract Money calculatePay();
 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);
 }
 }
}

서술적인 이름을 사용하라!

함수가 하는 일을 좀 더 잘 표현하는 이름이 훨씬 좋은 이름이다. 좋은 이름이 주는 가치는 아무리 강조해도 지나치지 않는다.

"코드를 읽으면서 짐작했던 기능을 각 루틴이 그대로 수행한다면 깨끗한 코드라 불러도 되겠다." -워드

함수가 작고 단순할수록 서술적인 이름을 고르기도 쉬워진다.

이름이 길더라도 서술적인 이름이 짧고 어려운 이름보다 좋다. 함수 이름을 정할 때는 여러 단어가 쉽게 읽히는 명명법을 사용한다.

이름을 붙일 때는 일관성이 있어야 한다. 모듈 내에서 함수 이름은 같은 문구, 명사, 동사를 사용한다.


함수 인수

함수에서 이상적인 인수 개수는 0개(무항)이다. 인수는 개념을 이해하기 어렵게 만든다.

갖가지 인수 조합으로 함수를 검증해야 한다면 복잡해 질것이다.

많이 쓰는 단항 형식

함수에 인수 1개를 넘기는 이유로 가장 흔한 경우는 두 가지 이다.

1. 하나는 인수에 질문을 던지는 경우

2. 인수를 뭔가로 변환해 결과를 반환하는 경우

위의 두가지는 납득이 된다.

위의 두 경우가 아니라면 단항 함수는 가급적 피한다.

플래그 인수

플래그 인수는 추하다. 플래그가 참이면 이걸하고 플래그가 불이면 저걸 한다는건 이미 함수가 2가지 일을 한다는 의미이다.

이항 함수

인수가 2개인 함수는 1개인 함수보다 어렵다. 이항 함수가 적절한 경우도 있지만 그만큼 위험이 따른다.

즉 단항 함수로 바꾸도록 애써야 한다.

삼항 함수

인수가 3개인 함수는 인수가 2개인 함수보다 훨씬 이해하기 힘들다.

인수 객체

인수가 2~3개이면 일부를 클래스 변수로 선언할 가능성이 있다.

Circle makeCircle(double x, double y, double radius);

Circle makeCircle(Point center, double radius);

객체를 생성해 인수를 줄이는 방법이 눈속임이라 여겨질지 모르지만 그렇지 않다. x 와 y를 묶었듯이 변수를 묶어 넘기려면 이름을 붙여야 하므로 결국은 개념을 표현하게 된다.

인수 목록

때로는 인수 개수가 가변적인 함수도 필요하다.

String.format("%s worked %.2f" hours.", name, hours);

public String format(String format, Object... args);

void monad(Integer... args);

void dyad(String name, Integer... args);

void triad(String name, int count, Integer... args);

동사와 키워드

함수의 의도나 인수의 순서와 의도를 제대로 표현하려면 좋은 함수 이름이 필수다. 단항 함수는 함수와 인수가 동사/명사 쌍을 이뤄야한다.

write(name)


부수 효과를 일으키지 마라!

부수 효과는 거짓말이다. 함수에서 한 가지를 하겠다고 약속하고선 남몰래 다른 짓을 하는것이다. 클래스 변수를 수정하거나 함수로 넘어온 인수, 시스템 전역 변수를 수정한다. 이는 시간적인 결합이나 순서 종속성을 초래한다.

출력 인수

일반적으로 우리는 인수를 함수 입력으로 해석한다. 하지만 인수를 출력으로 쓰이는 경우다 있다.


명령과 조회를 분리하라!

함수는 뭔가를 수행하거나 뭔가에 답하거나 둘 중 하나만 해야 한다. 둘 다 하면 안된다. 객체 상태를 변경하거나 아니면 객체 정보를 반환하거나 둘 중 하나다. 둘다 하면 혼란을 초래한다.


오류 코드보다 예외를 사용하라!

명령 함수에서 오류 코드를 반환하는 방식은 명령/조회 분리 규칙을 미묘하게 위반한다. 자칫하면 if 문에서 명령을 표현식으로 사용하기 쉬운 탓이다.

if (deletePage(page) == E_OK) {
 if (registry.deleteReference(page.name) == E_OK) {
 if (configKeys.deleteKey(page.name.makeKey()) == E_OK){
 logger.log("page deleted");
 } else {
 logger.log("configKey not deleted");
 }
 } else {
 logger.log("deleteReference from registry failed");
 }
} else {
 logger.log("delete failed");
 return E_ERROR;
}

위의 코드보다는

try {
 deletePage(page);
 registry.deleteReference(page.name);
 configKeys.deleteKey(page.name.makeKey());
}
catch (Exception e) {
 logger.log(e.getMessage());
}

로 뽑아내는게 더 깔끔하다.

try/catch 블록 뽑아내기

원래의 try/catch 블록은 추잡하지만 이를 별도 함수로 뽑아내면 깔끔해진다.

오류 처리도 한 가지 작업이다

함수는 '한 가지' 작업만 해야 한다. 오류 처리도 작업에 속한다. 그러므로 오류를 처리하는 함수는 오류만 처리해야 마땅하다.


반복하지 마라

중복은 문제다. 코드 길이가 늘어날 뿐 아니라 알고리즘이 변하면 여러곳을 손봐야 한다. 게다가 어느 한곳이라도 빠뜨리게 되면 오류가 발생할 확률도 높아진다.

중복을 없애면 모듈 가독성이 크게 높아진다.


함수를 짜는 법!

소프트웨어를 짜는 행위는 여느 글짓기와 비슷하다. 논문이나 기사를 작성할때는 먼저 생각을 기록한 후 읽기 좋게 다듬는다. 초안은 대개 서투르고 어수선하므로 원하는 대로 읽힐 때까지 말을 다듬고 문장을 고치고 문단을 정리한다.

내가 함수를 짤 때도 마찬가지다. 처음에는 길고 복잡하다. 들여쓰기 단계도 많고 중복된 루프도 많다. 인수 목록도 아주 길다. 이름은 즉흥적이고 코드는 중복된다. 하지만 나는 그 서투른 코드를 빠짐없이 테스트하는 단위 테스트 케이스도 만든다.

그런 다음 나는 코드를 다듬고, 함수를 만들고, 이름을 바꾸고, 중복을 제거한다. 메서드를 줄이고 순서를 바꾼다. 때로는 전체 클래스를 쪼개기도 한다. 이 와중에도 단위 테스트는 통과한다.

최종적으로는 이 장에서 설명한 규칙을 따르는 함수가 얻어진다. 처음부터 탁짜내지 않는다. 그게 가능한 사람은 없으니까!


결론

모든 시스템은 특정 응용 분야 시스템을 기술할 목적으로 프로그래머가 설계한 도메인 특화 언어로 만들어진다. 함수는 그 언어에서 동사며, 클래스는 명사다. 요구사항 문서에 나오는 명사와 동사를 클래스 함수 후보로 고려한다는 끔찍한 옛 규칙으로 역행하자는 이야기가 아니다. 이것은 오히려 훨씬 더 오래된 진실이다. 프로그래밍의 기술은 언제나 언어 설계의 기술이다.

시스템을 프로그램이 아니라 이야기로 여긴다. 프로그래밍 언어라는 수단을 사용해 좀 더 풀부하고 좀 더 표현력이 강한 언어를 만들어 이야기를 풀어간다. 시스템에서 발생하는 모든 동작을 설명하는 함수 계층이 바로 그 언어에 속한다. 재귀라는 기교로 각 동작은 바로 그 도메인에 특화된 언어를 사용해 자신만의 이야기를 풀어간다.

' > Clean Code' 카테고리의 다른 글

[Clean Code] 5장 형식맞추기  (1) 2022.09.21
[Clean Code] 4장 주석  (0) 2022.09.20
[Clean Code] 2장 의미 있는 이름  (2) 2022.09.17
[Clean Code] 1장 깨끗한 코드  (1) 2022.09.16
[Clean Code] 8장 경계  (2) 2022.09.15

들어가면서

소프트웨어에서 이름은 어디나 쓰인다. 여기저기 도처에서 이름을 사용한다. 이름을 잘 지으면 여러모로 편하다.


의도를 분명히 밝혀라

좋은 이름을 지으려면 시간이 걸리지만 좋은 이름으로 절약하는 시간이 훨씬 더 많다. 그러므로 이름을 주의깊게 살펴 더 나은 이름이 떠오르면 개선해야 한다.

굵직한 질문에 모두 답할수 있어야 한다. ( 변수의 존재 이유는?, 수행 기능은?, 사용 방법은? 따로 주석이 필요한지?)

int d; // 경과 시간(단위: 날짜)

위의 이름 d는 아무 의미가 없다.

int elapsedTimeInDays;
int daysSinceCreation;
int daysSinceModification;
int fileAgeInDays;

위와 같이 의도가 드러나는 이름을 사용하면 코드 이해와 변경이 쉽다.

다음 코드는 무엇을 할까?

public List<int[]> getThem() {
 List<int[]> list1 = new ArrayList<int[]>();
for (int[] x : theList)
 if (x[0] == 4)
 list1.add(x);
 return list1;
}

코드가 하는 일을 짐작하기 어렵다. 문제는 코드의 단순성이 아니라 코드의 함축성이다. 코드 맥락이 코드 자체에 명시적으로 드러나지 않는다. 위 코드는 암암리에 독자가 다음의 정보를 안다고 가정한다.

  1. theList에 무엇이 들었는가?
  2. theList에서 0번째 값이 어째서 중요한가?
  3. 값 4는 무슨 의미인가?
  4. 함수가 반환하는 리스트 list1을 어떻게 사용하는가?

단순 이름을 고치는 것만으로 함수가 하는 일을 이해하기 쉬워진다.


그릇된 정보를 피하라

프로그래머는 코드에 그릇된 단서를 남겨서는 안 된다. 이는 코드의 의미를 흐린다.

예를 들어 hp라는 변수는 유닉스 변종을 가리키는 이름이기 때문에 직각삼각형의 빗변(hypotenuse)의 약어로 좋아 보일지라도 독자에게 혼란을 줄 수 있다.

서로 흡사한 이름을 사용하지 않도록 주의한다. 한 모듈에서 XYZController orEfficientHandlingOfStrings라는 이름을 사용하고, 조금 떨어진 모듈에서 XYZControllerForEfficientStorageOfStrings 을 사용하면 ..... 겁나 헷갈린다.

유사한 개념은 유사한 표기법을 사용한다. 일관성이 떨어지는 표기법은 그릇된 정보다.


의미 있게 구분하라

이름이 달라야 한다면 의미도 달라져야 한다. 컴파일러를 통과할지라도 연속된 숫자를 덧붙이거나 불용어를 추가하는 방식은 적절하지 못하다.

public static void copyChars(char a1[], char a2[]) {
 for (int i = 0; i < a1.length; i++) {
 a2[i] = a1[i];
 }
}

함수 이름으로 source와 destination을 사용한다면 코드 읽기가 훨씬 쉬워진다.

불용어는 중복이다. variable이나 table같은 단어는 이름으로 쓰면 안된다.

getActiveAccount();
getActiveAccounts();
getActiveAccountInfo();

위와 같이 애매한 변수로 이름을 지으면 혼란만 야기한다.


발음하기 쉬운 이름을 사용하라

사람들은 단어에 능숙하고 단어는 개념의 전달이 용이하다.

class DtaRcrd102 {
 private Date genymdhms;
 private Date modymdhms;
 private final String pszqint = "102";
 /* ... */
};

class Customer {
 private Date generationTimestamp;
 private Date modificationTimestamp;
 private final String recordId = "102";
 /* ... */
}

위의 코드를 보면 명확해진다.


검색하기 쉬운 이름을 사용하라

문자 하나를 사용하는 이름과 상수는 텍스트 코드에서 눈에 띄지 않는다.

또한 나중에 grep을 이용한 검색에서 모든 단어가 검색되어 제대로 된 검색이 불가능해진다.

for (int j=0; j<34; j++) {
 s += (t[j]*4)/5;
}

int realDaysPerIdealDay = 4;
const int WORK_DAYS_PER_WEEK = 5;
int sum = 0;
for (int j=0; j < NUMBER_OF_TASKS; j++) {
 int realTaskDays = taskEstimate[j] * realDaysPerIdealDay;
 int realTaskWeeks = (realTaskDays / WORK_DAYS_PER_WEEK);
 sum += realTaskWeeks;
}

인코딩을 피하라

유형이나 범위 정보 까지 인코딩에 넣으면 해독도 어려워지고 또 인코딩을 익히느라 시간또한 쓰게 된다. 개발자는 할게 많다......불필요한 부담은 없애자!

  • 헝가리식 표기법

요즘 시대는 변수이름에 타입을 인코딩할 필요가 없다(실제로 나는 사용해본적 없음). 그러므로 이름에 변수이름에 헝가리안 표기법으로 변수 타입을 넣지 않아도 된다.

  • 멤버 변수 접두어

이전에는 멤버 변수 접두어에 m_를 붙였었나 보다. 그러지 말자.

public class Part {
 private String m_dsc; // 설명 문자열
 void setName(String name) {
 m_dsc = name;
 }
}
_________________________________________________
public class Part {
 String description;
 void setDescription(String description) {
 this.description = description;
 }
}
  • 인터페이스 클래스와 구현 클래스

인터페이스와 구현클래스의 명명을 어떻게 할 것인가. 이건 사람마다 다르겠지만 쉽게 글쓴이는 인터페이스 뒤에 Impl이라고 붙이는 것을 선호한다고 한다.

ShapeFactoryImp나 심지어 CShapeFactory가 IShape Factory보다 좋다고 한다.


자신의 기억력을 자랑하지 마라

보통 프로그래머는 머리가 똑똑하다고 한다. 그렇다고 그들이 만능은 아니다.

클래스 이름

클래스 이름과 객체 이름은 명사나 명사구가 적합하다.

메서드 이름

메서드 이름은 동사나 동사구가 적합하다. 접근자, 변경자, 조건자는 javabean 표준에 따라 값 앞에 get,set,is를 붙인다.

기발한 이름은 피하라

이름이 너무 기발하면 공감대가 비슷한 사람만 이름을 기억해 낸다. 재미난 이름보다 명료한 이름을 선택하라

한 개념에 한 단어를 사용하라

추상적인 개념 하나에 단어 하나를 선택해 이를 고수한다.

말장난을 하지 마라

한 단어를 두 가지 목적으로 사용하지 마라. 다른 개념에 같은 단어를 사용한다면 이는 말장난에 불과하다.

해법 영역에서 가져온 이름을 사용하라

코드를 읽을 사람도 프로그래머이다. 전산 용어, 알고리즘 이름, 패턴 이름, 수학 용어를 사용하면 서로에게 친숙하다.

문제 영역에서 가져온 이름을 사용하라

적절한 프로그래밍 언어가 없다면 문제 영역에서 이름을 가져온다. 그러면 그 배경지식을 습득하면서 배울수 있다.


의미 있는 맥락을 추가하라

스스로 의미가 분명한 이름이 없지 않다. 하지만 대다수 이름은 그렇지 못하다. 그래서 클래스, 함수, 이름 공간에 넣어 맥락을 부여한다. 모든 방법이 실패하면 마지막 수단으로 접두어를 붙인다.

private void printGuessStatistics(char candidate, int count) {
 String number;
 String verb;
 String pluralModifier;
 if (count == 0) {
 number = "no";
 verb = "are";
 pluralModifier = "s";
 } else if (count == 1) {
 number = "1";
 verb = "is";
 pluralModifier = "";
 } else {
 number = Integer.toString(count);
 verb = "are";
 pluralModifier = "s";
 }
 String guessMessage = String.format(
 "There %s %s %s%s", verb, number, candidate, pluralModifier
 );
 print(guessMessage);
}

함수 이름은 맥락 일부만 제공하며, 알고리즘이 나머지 맥락을 제공한다. 위의 함수를 끝까지 읽어보고 나서야 변수 세 개가 통계 추측 메시지에 사용된다는 사실이 드러난다.

불필요한 맥락을 없애라

일반적으로는 짧은 이름이 긴 이름보다 좋다. 단, 의미가 분명한 경우에 한해서다. 이름에 불필요한 맥락을 추가하지 않도록 주의한다.


마치면서

좋은 이름을 선택하려면 설명 능력이 뛰어나야 하고 문화적인 배경이 같아야 한다. 이것이 제일 어렵다. 좋은 이름을 선택하는 능력은 기술, 비즈니스, 관리 문제가 아니라 교육 문제다.

여느 코드 개선 노력과 마찬가지로 이름 역시 나름대로 바꿨다가는 누군가 질책할지도 모른다. 그렇다고 코드를 개선하려는 노력을 중단해서는 안 된다.

' > Clean Code' 카테고리의 다른 글

[Clean Code] 4장 주석  (0) 2022.09.20
[Clean Code] 3장 함수  (0) 2022.09.19
[Clean Code] 1장 깨끗한 코드  (1) 2022.09.16
[Clean Code] 8장 경계  (2) 2022.09.15
[Clean Code] 7장 오류 처리  (0) 2022.09.14

코드가 존재하리라

코드가 사라질 가망은 없다. 왜? 코드는 요구사항을 상세히 표현하는 수단이니까!

프로그래밍 언어에서 추상화의 수준은 점차 높아지겠지만 요구사항을 모호하게 줘도 우리 의도를

꿰뚫어 프로그램을 완벽하게 만드는건 불가능하다.

그러므로 코드는 항상 존재하리라


나쁜 코드

좋은 코드는 중요하다. 이전 버전에 있던 버그가 다음 버전에도 계속 남을 것이고 이것으로 인해

프로그램이 죽는 일도 발생할 것이며 다음 버전에끼치는 영향은 배가 아닌 제곱이 되므로

나중에 돌아와 정리하겠다고 생각하지 말고 깨끗한 코드를 짜려고 현재에 노력하자

*르블랑의 법칙! 나중은 결코 오지 않는다.


나쁜 코드로 치르는 대가

나쁜 코드는 개발 속도를 크게 떨어뜨린다.

프로젝트 초반에 번개처럼 잘 나아가다가도 나쁜 코드가 있다면 고칠 때마다 계속 시간이 늘어난다.

사람이 더 투여되더라도 생산성만 안 좋아질뿐 코드 자체가 좋아지진 않는다.


원대한 재설계의 꿈

나쁜 코드의 중첩으로 더 이상 좋아질 기미가 보이지 않아 재설계를 진행한다.

결국 타이거 팀이 만들어 진다.

타이거 팀의 진행 방향

1. 새로운 타이거 팀의 구성

2. 모두가 타이거 팀에 합류하고 싶어 하지만 유능한 사람이 차출 나머지는 현 시스템 유지보수

3. 유지보수팀과 타이거 팀의 경주 시작

4. 기존 시스템을 100% 기능 재현 할 때까지 개발 진행

하지만 기존 코드가 레거시 코드라면 결국 위의 진행 방향도 얼마나 걸릴지 알 수 없다는게 함정


태도

어째서 좋은 코드가 순식간에 나쁜 코드로 전락했는가?

1. 원래 설계를 뒤집는 방향으로 요구사항이 변해서?

2. 일정이 촉박해서?

3. 멍청한 관리자와 조급한 고객과 쓸모없는 마케팅 부서와 전화기 살균제 탓에?

아니다. 전적으로 우리(프로그래머)가 전문가 답지 못해서이다.

우리는 요구사항을 우리에 맞게 자문해줘야한다.

우리는 프로젝트 계획에 관여하여 일정을 잡아야한다.

우리는 관리자와, 고객과, 마케팅 부서에게 만든 프로그램의 정보를 제공해야 한다.

즉, 프로젝트의 모든 부분에 우리는 책임이 있다.

*나쁜 코드의 위험을 이해하지 못하는 관리자 말을 그대로 따르는 행동은 전문가답지 못하다.


원초적 난제

우리는 빨리 가려고 좋은 코드를 만드는데 시간을 들이지 않는다.

흔히 하는 착각은 프로그래머가 기한을 맞추려면 나쁜 코드를 양산할 수밖에 없다고 생각한다.

하지만 나쁜 코드가 시간을 늦춘다.


깨끗한 코드라는 예술?

그림을 그리는 행위와 비슷하다. 잘 그린 그림을 구분하는 능력이 그림을 잘 그리는 능력이 아닌것처럼

깨끗한 코드와 나쁜 코드를 구분할 줄 안다고 해서 깨끗한 코드를 작성할 줄 아는 뜻은 아니다.

절제와 규율을 적용해 나쁜 코드를 좋은 코드로 바꾸는 '코드 감각'이 있어야한다.


나는 우아하고 효율적인 코드를 좋아한다. 논리가 간당해야 버그가 숨어들지 못한다. 논리가 간단해야 버그가 숨어들지 못한다. 의존성을 최대한 줄여야 유지보수가 쉬워진다. 오류는 명백한 전략에 의거해 철저히 처리한다. 성능을 최적으로 유지해야 사람들이 원칙 없는 최적화로 코드를 망치려는 유혹에 빠지지 않는다. 깨끗한 코도는 한 가지를 제대로 한다.

-비야네(c++창시자이자 The c++ Programming Language 저자)

 

 

 

 

비야네는 효율을 중요하게 여긴다 하지만 글을 보면 우아한! 보기에 즐거운 이라는 표현을 쓴다. 즉 효율이라는 단어가 단순히 속도만을 뜻하는건 아니라는 뜻이다.

유혹이라는 단어도 잘 주의해서 봐야 한다. 나쁜 코드는 나쁜 코드를 유혹한다!

실용주의 프로그래머 데이브 토마스와 앤디 헌트는 이를 깨진 창문에 비유했다.

비야네는 철저한 오류 처리도 언급한다. 세세한 사항까지 꼼꼼하게 신경 쓰라는 말이다.

메모리 누수, 경쟁 상태, 일관성 없는 명명법 같이 사소하지만 세세한 사항도 신경써야 한다.

(지옥의 명명법.....;;;;;;)

마지막으로 비야네는 깨끗한 코드란 한 가지를 잘 한다고 단언한다. 수많은 저술가들이 나름의 깨끗한 코드를 가지고 있지만 깨끗한 코드는 한 가지에 '집중'한다는 것이다.


 

깨끗한 코드는 단순하고 직접적이다. 깨끗한 코드는 잘 쓴 문장처럼 읽힌다. 깨끗한 코드는 결코 설계자의 의도를 숨기지 않는다. 오히려 명쾌한 추상화와 단순한 제어문으로 가득하다.

-그래디 부치(Object Oriented Analysis and Design with Application 저자)

 

 

 

 

그래디는 비야네와 흡사한 의견을 표명하지만 가독성을 강조한다. 좋은 소설과 마찬가지로 깨끗한 코드는 해결할 문제의 긴장을 명확히 드러내야 한다.코드는 추측이 아니라 사실에 기반해야 한다. 반드시 필요한 내용만 담아야 한다. 코드를 읽는 사람에게 프로그래머가 단호하다는 인상을 줘야 한다.


깨긋한 코드는 작성자가 아닌 사람도 읽기 쉽고 고치기 쉽다. 단위 테스트 케이스와 인수 테스트 케이스가 존재한다. 깨끗한 코드에는 의미 있는 이름이 붙는다. 특정 목적을 달성하는 방법은 하나만 제공한다. 의존성은 최소이며 각 의존성을 명확히 정의한다. API는 명확하며 최소로 줄였다. 언어에 따라 필요한 모든 정보를 코드만으로 명확히 표현할 수 없기에 코드는 문학적으로 표현해야 마땅하다.

-Big 데이브 토마스(OTI 창립자이자 이클립스 전략의 대부)

 

 

빅 데이브는 가독성을 강조하지만 한 가지 중요한 반전을 더한다. 다른 사람이 고치기 쉽다는 것이다. 데이브는 테스트 케이스와 깨끗한 코드를 연관짓는다. TDD(테스트 주도 개발)가 중요해진 만큼 테스트 케이스가 없으면 깨끗한 코드는 나올수 없다. 데이브는 최소 즉 작은 코드와 문학적 즉 읽기 좋은 코드를 작성하나는 것을 강조한다.


깨긋한 코드의 특징은 많지만 그 중에서도 모두를 아우르는 특징이 하나 있다. 깨끗한 코드는 언제나 누군가 주의 깊게 짰다는 느낌을 준다. 고치려고 살펴봐도 딱히 손 댈 곳이 없다. 작성자가 이미 모든 사항을 고려했으므로, 고칠 궁리를 하다보면 언제나 제자리로 돌아온다. 그리고는 누군가 남겨준 코드, 누군가 주의 깊게 짜놓은 작품에 감사를 느낀다.

-마이클 페더스(Working Effectively with Legacy Code의 저자)

 

 

 

 

한 마디로 요약하면 '주의'다. 깨끗한 코드는 주의 깊게 작성한 코드다.누군가 시간을 들여 깔끔하고 단정하게 정리한 코드다. 세세한 사항까지 꼼꼼하게 신경쓴 코드다. 주의를 기울인 코드다.


최근 들어 나는 켄트 벡이 제안한 단순한 코드 규칙으로 구현을 시작한다. (그 리고 같은 규칙으로 구현을 거의 끝낸다.) 중요한 순으로 나열하자면 간단한 코 드는

· 모든 테스트를 통과한다.

· 중복이 없다.

· 시스템 내 모든 설계 아이디어를 표현한다. · 클래스, 메서드, 함수 등을 최대한 줄인다.

물론 나는 주로 중복에 집중한다. 같은 작업을 여러 차례 반복한다면 코드가 아 이디어를 제대로 표현하지 못한다는 증거다. 나는 문제의 아이디어를 찾아내 좀 더 명확하게 표현하려 애쓴다.

내게 있어 표현력은 의미 있는 이름을 포함한다. 보통 나는 확정하기 전에 이 름을 여러 차례 바꾼다. 이클립스와 같은 최신 개발 도구는 이름을 바꾸기가 상 당히 쉽다. 그래서 별 고충 없이 이름을 바꾼다. 하지만 표현력은 이름에만 국한되지 않는다. 나는 여러 기능을 수행하는 객체나 메서드도 찾는다. 객체가 여러 기능을 수행한다면 여러 객체로 나눈다. 메서드가 여러 기능을 수행한다면 메서 드 추출Extract Method 리팩터링 기법을 적용해 기능을 명확히 기술하는 메서드 하 나와 기능을 실제로 수행하는 메서드 여러 개로 나눈다.

중복과 표현력만 신경 써도 (내가 생각하는) 깨끗한 코드라는 목표에 성큼 다 가선다. 지저분한 코드를 손볼 때 이 두 가지만 고려해도 코드가 크게 나아진다. 하지만 나는 한 가지를 더 고려한다. 이는 설명하기 조금 까다롭다.

오랜 경험 끝에 나는 모든 프로그램이 아주 유사한 요소로 이뤄진다는 사실을 깨달았다. 한 가지 예가 ‘집합에서 항목 찾기’다. 직원 정보가 저장된 데이터베이 스든, 키/값 쌍이 저장된 해시 맵이든, 여러 값을 모아놓은 배열이든, 프로그램을 짜다 보면 어떤 집합에서 특정 항목을 찾아낼 필요가 자주 생긴다. 이런 상황이 발생하면 나는 추상 메서드나 추상 클래스를 만들어 실제 구현을 감싼다. 그러면 여러 가지 장점이 생긴다. 이제 실제 기능은 아주 간단한 방식으로, 예를 들어 해시 맵으로, 구현해도 괜 찮다. 다른 코드는 추상 클래스나 추상 메서드가 제공하는 기능을 사용하므로 실 제 구현은 언제든지 바꿔도 괜찮다. 지금은 간단하게 재빨리 구현했다가 나중에 필요할 때 바꾸면 된다. 게다가 집합을 추상화하면 ‘진짜’ 문제에 신경 쓸 여유가 생긴다. 간단한 찾기 기능이 필요한데 온갖 집합 기능을 구현하느라 시간과 노력을 낭비할 필요가 없 어진다. 중복 줄이기, 표현력 높이기, 초반부터 간단한 추상화 고려하기. 내게는 이 세 가지가 깨끗한 코드를 만드는 비결이다.

-론 제프리스(Extreme Progamming Installed와 Extreme Programming Adventure in c#의 저자)

 

결론은 중복을 피하라. 한기능만 수행해라. 제대로 표현하라. 작게 추상화해라.


코드를 읽으면서 짐작했던 기능을 각 루틴이 그 대로 수행한다면 깨끗한 코드라 불러도 되겠다. 코드가 그 문제를 풀기 위한 언어처럼 보인다면 아름다운 코드라 불러도 되겠다.

-워드 커닝햄Ward Cunningham (위키Wiki 창시자, 피트Fit 창시자, 익스트림 프로그래밍eXtreme Programming 공동 창시자, 디자인 패턴을 뒤에서 움직이는 전문가, 스몰토크Smalltalk와 객체 지향OO의 정신적 지도자, 코드를 사랑하는 프로그래머들의 대부 )

 

명백하고 단순해 마음이 끌리는 코드가 깨끗한 코드다. 너무도 잘 짜놓은 코드라 읽는 이가 그 사실을 모르고 넘어간다.


우리들 생각

절대적으로 옳은 것은 없다. 이 책은 오브젝트 멘토 진영이 생각하는 깨끗한 코드를 설명한다.

*보이스카웃 규칙

잘 짠 코드가 전부는 아니다. 시간이 지나도 언제나 깨끗하게 유지해야 한다.

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

' > Clean Code' 카테고리의 다른 글

[Clean Code] 4장 주석  (0) 2022.09.20
[Clean Code] 3장 함수  (0) 2022.09.19
[Clean Code] 2장 의미 있는 이름  (2) 2022.09.17
[Clean Code] 8장 경계  (2) 2022.09.15
[Clean Code] 7장 오류 처리  (0) 2022.09.14

+ Recent posts