5장 형식 맞추기
형식을 맞추는 목적
형식은 의사소통이다
- 코드는 문서이며, 개발자 간의 의사소통 수단이다.
- 오늘 구현한 코드는 내일 수정 될 수 있다.
- → 가독성은 유지보수성과 직결된다.
적절한 행 길이를 유지하라
행 길이는 반드시 지킬 엄격한 규칙은 아니지만 일반적으로 큰 파일보다 작은 파일이 이해하기 쉽다.
- 짧고 명확한 단위로 나눈다.
- 일반적으로 한 파일은 200줄 이내가 바람직하다.
신문 기사처럼 작성하라
신문의 표제는 최상단에서 기사 내용을 몇 마디로 요약한다. 소스 파일도 신문 기사와 비슷하게 작성하는 것이 좋다.
- 가장 중요한 내용 → 세부사항 순서.
- 상단에는 고차원 개념, 아래로 갈수록 세부 구현.
- 파일명과 클래스명만 보고 역할을 파악할 수 있어야 한다.
개념은 빈 행으로 분리하라
각 행은 수식이나 절을 나타내고, 일련의 행 묶음은 완결된 생각 하나를 표현한다. 관련된 코드끼리는 묶고, 서로 다른 개념은 빈 줄로 구분한다.
👍👍👍
package fitnesse.wikitext.widgets;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
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));
}
}
👎👎👎
package fitnesse.wikitext.widgets;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
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 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);
}
수직 거리
관련 있는 코드끼리는 물리적으로 가까워야 이해하기 쉽다.
- 무엇을 하는지 이해하기 위해 탐색할 때 조각 조각들이 어디에 있는지 찾기 쉽다.
- protected, 상속 관계..👎
같은 파일에 속할 정도로 밀접한 두 개념은 세로 거리로 연관성을 표현한다.
- 변수 선언
- 변수는 사용하는 위치에 최대한 가까이 선언한다.
-
지역 변수는 각 함수 맨 처음에 선언한다.
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) { } } } -
loop문 제어 변수는 loop문 내부에 선언
public int countTestCases() { int count = 0; for (Test each : tests) count += each.countTestCases(); return count; }
- 인스턴스 변수
- 인스턴스 변수를 선언하는 위치는 잘 알려진 위치여야 한다.
- 클래스 맨 처음에 선언한다.
- 메서드 중간에 숨겨두지 않는다.
- 인스턴스 변수를 선언하는 위치는 잘 알려진 위치여야 한다.
- 종속 함수
- 한 함수가 다른 함수를 호출한다면 두 함수는 세로로 가까이 배치한다.
- 호출하는 함수를 호출되는 함수보다 먼저 배치한다.
- 이 규칙이 일관적으로 적용되면 독자는 방금 호출된 함수가 곧 정의될 거라 예측할 수 있고, 자연스럽게 코드를 읽을 수 있다.
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); } 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(); } ...
-
개념적 유사성
비슷한 역할/이름/구조를 가진 함수는 근접 배치한다.
- 친화도가 높은 요인
- 다른 함수를 호출해 생기는 직접적인 종속성
- 변수와 그 변수를 사용하는 함수
-
비슷한 동작을 수행하는 함수
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~120자 이내로 제한한다.
→ 긴 줄은 읽기 어렵고, 리뷰/디버깅 시 불편함.
가로 공백과 밀집도
밀집도는 가로 공백을 사용해서 느슨함을 표현한다.
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 Reques request;
private Response response;
private FitNesseContex 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 FitNesseExpediter implements ResponseSender {
private Socket socket;
private InputStream input;
private OutputStream output;
private Request request;
private Response response;
private FitNesseContex 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;
}
들여쓰기
소스파일은 윤곽도와 계층이 비슷하다.
- 파일 전체에 적용되는 정보
- 파일 내 개별 클래스에 적용되는 정보
- 클래스 내 각 메서드에 적용되는 정보
- 블록 내 블록에 재귀적으로 적용되는 정보
계층에서 각 수준은 이름을 선언하는 범위이자 선언문과 실행문을 해석하는 범위이다. 이 범위(scope)를 표현하기 위해 들여쓰기를 사용한다. 들여쓰기를 통해 왼쪽으로 코드를 맞춰 코드가 속하는 범위를 시각적으로 표현한다.
👎👎👎
public class CommentWidget extends TextWidget {
public static final String REGEXP = "^#[^\r\n]*(?:(?:\r\n)|\n|\r)?";
public CommentWidget(ParentWidget parent, String text){super(parent, text);}
public String render() throws Exception {return ""; }
}
👍👍👍
public class CommentWidget extends TextWidget {
public static final String REGEXP = "^#[^\r\n]*(?:(?:\r\n)|\n|\r)?";
public CommentWidget(ParentWidget parent, String text){
super(parent, text);
}
public String render() throws Exception {
return "";
}
}
가짜 범위
if,while,for뒤에 오는 아무 동작도 하지 않는 빈 문장(세미콜론;)이 오는 경우
👎👎👎
// while은 세미콜론 하나가 루프 본문처럼 동작
// → 루프는 실행되지만 아무 일도 안 함
while (inputStream.read() != -1);
// for 뒤에 ;가 있어 루프 내부가 없음
// → 실제 블록은 루프 외부이며 단 한 번만 실행
for (int i = 0; i < 10; i++);
{
System.out.println("This only runs once!");
}
;하나가 본문으로 처리됨 → 읽기 어렵고, 실수로 작성된 것처럼 보임
👍👍👍
while (inputStream.read() != -1)
; // intentionally empty
// 예: 딜레이 (busy wait) CPU를 소모하면서 일정 시간 대기
for (long wait = 0; wait < 1_000_000L; wait++)
; // intentional delay
- 주석으로 의도 명시
- 들여쓰기로 가독성 개선
| ❌ 삭제해야 할 경우 | ✅ 유지할 수 있는 경우 |
|---|---|
| 실수로 세미콜론이 들어간 경우 | 스트림을 소비하거나 busy-wait처럼 명확한 목적이 있는 경우 |
| 루프 내부에 실행 코드가 빠진 경우 | 동작이 없어야만 하는 루프일 때 (단, 주석 필수) |
팀 규칙
각자 선호하는 규칙이 있겠지만 팀에 속해있다면 팀 규칙을 따라야 한다. 그래야 소프트웨어가 일관적인 스타일을 가질 수 있다. 스타일이 일관적이고 매끄러워야 독자에게 신뢰감을 줄 수 있다.