클래스 체계

가장 먼저 변수 목록

static public 상수가 맨 처음

이어서 비공개 인스턴스 변수

공개 변수가 필요한 경우는 거의 없다

 

변수 목록 다음에는 공개 함수

비공개 함수는 자신을 호출하는 공개함수 직후에 넣는다 >> 추상화 단계 순차적으로

 

 

캡슐화

변수와 유틸리티 함수는 공개하지 않는 편이 좋지만, 그렇다고 반드시 숨겨야 하는 것은 아니다

때로는 protected 선언해 테스트 코드에 접근 허용하기도 한다

같은 패키지 안에서 테스트 코드가 함수 호출, 변수 사용해야 한다면 그 함수/변수를 protected 로 선언하거나 패키지 전체로 공개한다

 

 

클래스는 작아야 한다

클래스를 만들 때 첫 번째 규칙은 크기

클래스는 작아야 한다

 

함수의 경우, 물리적인 행 수로 크기를 측정했다 

클래스의 경우는 좀 다르게, 클래스가 맡은 책임을 측정한다

 

 

클래스 이름은 해당 클래스의 책임을 기술해야 한다

Proccess, Manager, Super 등과 같이 모호한 단어가 있다면, 클래스에 여러 책임을 떠안겼다는 증거다

 

 

단일 책임 원칙

Single Responsibility Principal

클래스나 모듈을 변경할 이유가 단 하나 뿐이어야 한다는 원칙

SRP 는 책임 이라는 개념을 정의하며 적절한 클래스 크기를 제시한다

 

SRP 는 객체지향 설계에서 더욱 중요한 개념

 

>> 큰 클래스가 아닌, 작은 클래스 여러개가 더 바람직하다

작은 클래스는 각자 맡은 책임이 하나이고, 다른 작은 클래스와 협력해 시스템에 필요한 동작을 수행한다

 

 

응집도

Cohesion

클래스는 인스턴스 변수가 작아야 한다

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

일반적으로 메서드가 변수를 더 많이 사용할수록 메서드, 클래스 사이의 응집도가 더 높다

(모든 메서드가 모든 인스턴스 변수를 사용하는 클래스는 응집도 최상)

 

 

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

Stack 클래스는 응집도가 높다

size() 함수를 제외한 다른 두 함수는 두 변수를 모두 사용한다

 

 

 

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

큰 함수를 작은 함수 여럿으로 나누기만 해도 클래스 수가 많아진다

 

 

변경하기 쉬운 클래스

대다수 시스템은 지속적인 변경이 가해진다

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

 

새로운 SQL 문을 지원하려면 반드시 Sql 클래스를 변경해야 한다

또한 기존 SQL 문 하나를 수정할 때도 마찬가지

ex. select 문에 내장된 select 문을 지원하려면 Sql 클래스를 고쳐야 한다

이렇듯 변경할 이유가 두 가지 이므로 Sql 클래스는 SRP 를 위반한다

 

selectWIthCriteria() 메서드는 select() 에서만 사용한다

 

 

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

 

 

 

공개 인터페이스를 각각 Sql 클래스에서 파생하는 클래스로 만들었다

valueList 같은 비공개 메서드는 해당하는 파생 클래스로 옮겼다

모든 파생 클래스가 공통으로 사용하는 비공개 메서드는 Where 과 ColumnList 라는 두 유틸리티 클래스에 넣었다

 

함수 하나를 수정한다고 다른 함수가 망가질 위험 X

테스트 관점에서 모든 논리를 구석구석 증명하기도 쉬워졌다 >> 클래스가 서로 분리되었기 때문

 

update 문을 추가할 때 기존 클래스를 변경할 필요가 전혀 없다 >> update 를 만드는 논리는 Sql 클래스에서 새 클래스 UpdateSql을 상속받아 거기에 넣으면 그만이기 때문

 

 

 

변경으로부터 격리

상세한 구현에 의존하는 클라이언트 클래스는 구현이 바뀌면 위험에 빠진다

>> 인터페이스와 추상 클래스를 사용해 구현이 미치는 영향을 격리

 

 

Portfolio 클래스에서 TokyoStockExchange API 를 직접 호출하는 대신 StockExchange 라는 인터페이스를 생성한 후 메서드 하나를 선언한다

 

public interface StockExchange{
    Money currentPrice(String symbol);
}

다음으로 StockExchange 인터페이스를 구현하는 TokyoStockExchange 클래스를 구현

또한 Portfolio 생성자를 수정해 StockExchange 참조자를 인수로 받는다

 

public Portfolio{
    private StockExchange exchange;
    public Portfolio(StockExchange exchange){
        this.exchange = exchange;
    }
    
    ...
}

TokyoStockExchange 클래스를 흉내내는 테스트용 클래스 생성 가능

 

테스트용 클래스는 StockExchange 인터페이스를 구현하며 고정된 주가를 반환

테스트용 클래스는 단순히 미리 정해놓은 표 값만 참조

>> 전체 포트폴리오 총계가 500 불인지 확인하는 테스트 코드 작성 가능

 

 

public class PortfolioTest{
    private FixedStockExchangeStub exchange;
    private Portfolio portfolio;
    
    @Before
    protected void setUp() throws Exception{
        exchange = new FixedStockExchangeStub();
        exchange.fix("MSFT", 100);
        portfolio = new Portfolio(exchange);
    }
    
    @Test
    public void GivenFiveMSFTTotalShouldBe500() throws Exception{
        portfolio.add(5, "MSFT");
        Assert.assertEquals(500, portfolio.value());
    }
}

이처럼 테스트가 가능할 정도로 시스템 결합도를 낮추면 유연성, 재사용성 증가

결합도가 낮다 => 각 시스템 요소가 다른 요소와 변경으로부터 잘 격리되어 있다는 의미

 

결합도 줄이면 DIP (Dependency Inversion Principal) 클래스 원칙을 따르는 클래스가 자연스럽게 구현된다

** DIP 는 본질적으로 클래스가 상세한 구현이 아니라 추상화에 의존해야 한다는 원칙

 

 

 

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

Clean Code -7 단위테스트  (0) 2023.08.14
Clean Code -6 오류 처리  (0) 2023.08.10
Clean Code -5 객체와 자료구조  (0) 2023.08.10
Clean Code -4 형식 맞추기  (0) 2023.08.10
Clean Code -3 주석  (0) 2023.08.10

TDD 의 법칙 세 가지

 

1. 실패하는 단위 테스트를 작성할 때 까지 실제 코드를 작성하지 않는다

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

3. 현재 실패하는 테스트를 통과할 정도로만 실제 코드를 작성한다

 

 

위 규칙을 따르면 개발, 테스트가 대략 30초 주기로 묶인다

테스트 코드와 실제 코드가 함께 도출되고

테스트 코드가 실제 코드보다 불과 몇 초 전에 나온다

 

But 방대한 테스트 코드는 심각한 관리 문제를 유발하기도 한다

 

 

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

실제 코드가 진화하면, 테스트 코드 역시 변해야 한다

테스트 코드가 복잡할수록 실제 코드를 짜는 시간보다 테스트 케이스를 추가하는 시간이 더 걸리게 된다

 

테스트코드는 실제 코드만큼 중요하다

 

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

테스트가 있으면 변경이 두렵지 않다

>> 코드에 유연성, 유지보수성, 재사용성을 제공한다

 

 

 

깨끗한 테스트 코드

깨끗한 테스트 코드를 만들기 위해서는 가독성이 중요하다

테스트 코드에서 가독성을 높이려면 명료성, 단순성, 풍부한 표현력이 필요하다

 

 

 

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

BUILD_OPERATE_CHECK 패턴

테스트는 정확하게 세 부분으로 나눠진다

 

첫 부분은 테스트 자료를 만든다

두 번째 부분은 테스트 자료를 조작한다

세 번째 부분은 조작한 결과가 올바른지 확인한다

 

잡다한 코드 없이 진짜 필요한 자료 유형과 함수만 사용하여 읽는 사람이 빠르게 이해할 수 있도록 구성한다

 

 

 

테스트 당 assert 하나

JUnit 으로 테스트 코드를 짤 때는 함수마다 assert 문을 단 하나만 사용한다

 

 

@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 turnOnCoolerAndBlowerIfTooHot() throws Exception {
  tooHot();
  assertEquals("hBChl", hw.getState()); 
}
  
@Test
public void turnOnHeaterAndBlowerIfTooCold() throws Exception {
  tooCold();
  assertEquals("HBchl", hw.getState()); 
}

@Test
public void turnOnHiTempAlarmAtThreshold() throws Exception {
  wayTooHot();
  assertEquals("hBCHl", hw.getState()); 
}

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

이 코드이다

하나의 테스트 당 하나의 assert 를 사용하여 한 눈에 알아보고 이해하기 쉽게 구현했다

But 리턴 값을 전부 비교해야 하는 테스트는 어쩔 수 없이 여러개의 assert 문을 사용해야된다

 

 

테스트 당 개념 하나

테스트 함수 마다 한 개념만 테스트 하라

 

 

F.I.R.S.T

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

 

1. 빠르게 (Fast)

테스트는 빨라야 한다

테스트가 느리면 자주 돌릴 엄두를 못낸다

 

2. 독립적으로 (Independent)

각 테스트는 서로 의존하면 안된다

한 테스트가 다음 테스트가 실행 될 환경을 준비해서는 안된다

각 테스트는 독립적으로, 어떤 순서로 실행해도 괜찮아야 한다

 

3. 반복 가능하게 (Repeatable)

테스트는 어떤 환경에서도 반복 가능해야 한다

실제 환경, QA환경, 클라우드 환경 등 테스트가 돌아가지 않는 환경이 하나라도 있다면 테스트가 실패한 이유를 둘러댈 변명이 생긴다

 

4. 자가 검증하는 (Self_validating)

테스트는 bool 값으로 결과를 내야 한다

성공 아니면 실패

통과 여부를 알기 위해 로그를 읽어서는 안된다

 

 

5. 적시에 (Timely)

테스트는 적시에 작성해야 된다

단위 테스트는 테스트하려는 실제 코드를 구현하기 직전에 구현한다

실제 코드를 구현한 다음에 테스트 코드를 만들면 실제 코드가 테스트하기 어렵다는 사싱르 발견할지도 모른다

 

 

 

 

마지막으로

테스트 코드는 실제 코드의 유연성, 유지보수성, 재사용성을 보존하고 강화하기 때문에 지속적으로 깨끗하게 관리해야 한다

표현력을 높이고 간결하게 정리하자

테스트 API를 구현해 도메인 특화 언어를 만들자 (DSL) >> 그러면 그만큼 테스트 코드 짜기 쉬워진다

 

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

Clean Code -8 클래스  (0) 2023.08.14
Clean Code -6 오류 처리  (0) 2023.08.10
Clean Code -5 객체와 자료구조  (0) 2023.08.10
Clean Code -4 형식 맞추기  (0) 2023.08.10
Clean Code -3 주석  (0) 2023.08.10

 

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

public class DeviceController {
    
    public void sendShutDown(){
        DeviceHandle = getHandle(DEV1);
        if(handle != DeviceHandle.INVALID){
            ...
        }
        else if
        ...
    }
}

이같은 방법으로 오류를 검출하고 그에 대한 동작을 부여하면 코드가 복잡해진다

차라리 오류 발생 시 예외를 던지는 것이 더 좋다 >> 호출자 코드가 더 깔끔해지기 때문

 

 

public class DeviceController{
    ...
    public void sendShutDown(){
        try{
            tryToShutDown();
        }
        catch(DeviceShutDownForError e){
            logger.log(e);
        }
    }
    
    private void tryToShutDown() throws DevieSHutDownError{
        ...
    }
}

개선한 코드

디바이스를 종료하는 알고리즘과 오류를 처리하는 알고리즘을 분리했기 때문에 가독성 증가

 

 

Try-Catch-Finally 문 부터 작성하라

try catch 문에서 catch 문은 try 블록에서 무슨 일이 생겨도 프로그램 상태를 일관성 있게 유지해야 한다

 

    public List<Object> errorCheck(String name){
        return new ArrayList<Object>();
    }

    public List<Object> errorCheck(String name){

        try{
            ...
        }
        catch(Exception e){
            throw new Exception(e);
        }
        
        return new ArrayList<Object>();
    }

위의 errorCheck 메서드에서는 ArrayList 를 바로 리턴해준다

만약 에러가 발생하면 예외를 던지지 못하므로 단위 테스트는 실패하게 된다

 

아래에서는 단위테스트를 실패할 때 에러를 던지도록 catch 문을 구현했다

코드가 예외를 던지므로 테스트 자체는 성공한다

 

try catch 문으로 범위를 정의했으므로 TDD를 사용해 필요한 논리를 추가가 가능

범위 내에서 트랜잭션의 본질을 유지하기 좋아진다

 

 

미확인 (Unchecked) 예외를 사용하라

메서드가 반환할 예외를 하나하나 열거하며 예외 처리를 하는 방법은 좋지 않다

 

 

예외에 의미를 제공하라

예외를 던지는 경우에는 전후 상황을 충분히 덧붙여라

오류 발생 원인, 위치를 찾기 쉬워지기 때문

 

오류 메세지에 정보를 담아 예외와 함께 throw

실패한 연산 이름과 실패 유형도 언급

logging 기능 사용한다면 catch 블록에서 오류 기록

 

 

호출자를 고려해 예외 클래스를 정의하라

Exception 클래스를 만드는데 가장 중요한 것은 '오류를 잡아내는 방식' 이다

라이브러리 API 디자인에 종속적이지 않게 설계할 수 있다

 

    ACMEPort port = new ACMEPort(12);
    
    try{
        port.open();
    }
    catch(DeviceResponseException e){
        reportPortError(e);
        logger.log("unlock Exception", e);
    }
    catch(GMXError e){
        reportPortError(e);
        logger.log("Device response exception");
    }
    finally{
        ...
    }

오류 검출에 대한 중복이 심하지만, 예외에 대응하는 방식이 예외 유형과 무관하게 동일하다

 

    LocalPort port = new LocalPort(12);
    try{
        port.open();
    }
    catch(PortDeviceFailure e){
        reportError(e);
        logger.log(e.getMessage(), e);
    }
    finally{
        ...
    }

따라서 이와 같이

호출하는 라이브러리 API 를 감싸면서 예외 유형 하나를 반환하도록 고칠 수 있다

 

여기서 LocalPort 클래스는 단순히 ACMEPort 클래스가 던지는 예외를 잡아 변환하는 wrapper 클래스일 뿐이다

 

실제 외부 API를 사용할 때에는 감싸기 기법이 좋다

외부 API를 감싸면 외부 라이브러리와 프로그램 사이에서 의존성이 크게 줄어든다

 

 

 

정상 흐름을 정의하라

try{
        MealExpense expenses = expenseReportDAO.getMeals(employee.getID());
        m_total += expenses.getTotal();
        
    }
    catch(MealExpensesNotFound e){
        m_total += getmealPerDiem();
    }

위에서 식비를 비용으로 청구했다면, 직원이 청구한 식비를 총계에 더한다

식비를 비용으로 청구하지 않았다면, 일일 기본 식비를 총계에 더한다

 

>> 예외가 논리를 따라가기 어렵게 만든다

특수한 상황 처리할 필요가 없다

 

    MealExpenses expenses = expenseReportDAO.getMeals(employee.getID());
    m_total += expenses.getTotal();

예외 처리를 하지 않고 위 코드로만 동작하도록 ExpenseReportDAO 를 수정

 

public class PerDiemMealExpenses implements MealExpenses{
    public int getTotal(){
        // 기본값으로 일일 식비를 반환한다
    }
}

getTotal() 메서드는 언제나 MealExpense 객체를 반환한다

청구한 식비가 없다면 일일 기본 식비를 반환한다

>> 청구된 식비 여부에 따른 예외 처리가 필요 없다

 

이를 특수사례 패턴이라고 한다

클래스를 만들거나 객체를 조작해 특수 사례를 처리하는 방식

 

 

null 을 반환하지 마라

 

public void registerItem(Item item){
    if(item != null){
        ItemRegistry registry = peristentStore.getItemRegistry();
        if(registry != null){
            Item existing = registry.getItem(item.getID());
            if(existing.getBillingPeriod().hasRetailOwner()){
                existing.register(item);
            }
        }
    }
}

 

 

한줄 건너 하나씩 null 을 체크하는 좋지 않은 코드의 예

 

null 을 반환하는 코드는 호출자에게 문제를 떠넘기게 된다

하나라도 null 확인을 빼먹는다면 애플리케이션이 통제를 벗어날 수 있다

 

위 코드에선 두 번째 행에 null 확인이 빠졌다

if(persistentStore == null) 조건이 true 라면 실행 시 NullPointException 이 발생

>> 발생 된 Exception 을 처리할 코드가 없다

 

메서드에서 null 을 반환하고싶은 유혹이 든다면, 대신 예외를 던지거나 특수 사례 객체를 반환하라

 

 

List<Employee> employees = getEmployees();
if(employee != null){
    for(Exployee e : employee){
        totalPay += e.getPay();
    }
}

 

위에서 getEmployees 는 null 도 반환한다

하지만 반드시 null 을 반환할 필요가 없으니까

 

    List<Employee> employees = getEmployees();
    for(Exployee e : employee){
        totalPay += e.getPay();
    }
    
    
    public List<Employee> getEmployees(){
        if( noEmployee ){
            return Collection.emptyList();
        }
    }

이렇게 고칠 수 있겠다

 

getEmployees() 는 언제나 리스트를 반환한다

Employee 가 없을 경우에는 미리 정의된 읽기 전용 리스트를 반환한다

 

이렇게 하면 예외 처리 필요 없이 객체를 무조건 반환 받는 방식으로 코드 작성이 가능하다

 

 

 

null 을 전달하지 마라

public class MetricsCalculator {
    public double xProjection(Point p1, Point p2){
        return (p2.x - p1.x) * 1.5;
    }

    ...
}

이같은 메소드에

 

calculator.xProjection(null, new Point(12, 13));

이렇게 null 을 던지면 NullPointException 발생

 

if(p1 == null || p2 == null) throw InvalidArgumentException("Exception message");

이렇게 if 문을 사용할 수 있지만, if 문은 InvalidArgumentException 을 잡아내는 처리기가 별도로 필요하다

 

public class MetricsCalculator {
    public double xProjection(Point p1, Point p2){
        assert p1 != null : "p1 should not be null";
        assert p2 != null : "p2 should not be null";
        return (p2.x - p1.x) * 1.5;
    }

    ...
}

assert 문을 사용하는 방법 역시 존재하나, 누군가가 null 을 전달하면 여전히 실행 오류가 발생

 

>> 애초에 null 이 안넘어오도록 코드 짜라

 

 

 

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

Clean Code -8 클래스  (0) 2023.08.14
Clean Code -7 단위테스트  (0) 2023.08.14
Clean Code -5 객체와 자료구조  (0) 2023.08.10
Clean Code -4 형식 맞추기  (0) 2023.08.10
Clean Code -3 주석  (0) 2023.08.10

변수를 private 로 정의하는 이유?

남들이 변수에 의존하지 않게 만들고 싶어서

그러면 왜 get, set 함수를 public 으로 공개해 비공개 변수를 외부에 노출할까?

 

 

자료 추상화

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

public interface Point{
    double getX();
    double getY();
    void setCartesian(double x, double y);
    double getR();
    double getTheta();
    void setPolar(double r, double theta);
}

 

위의 Point 클래스에서는 점이 직교좌표계를 사용하는지 극좌표계를 사용하는지 알 수 없다

아래의 interface 에서는 자료구조를 명백하게 표현한다

또한, 아래의 interface 에서는 클래스 메서드가 접근 정책을 강제한다

좌표를 읽을 때에는 각 값을 개별적으로 읽어야 한다 (getX, getY)

하지만 좌표를 설정할 때는 두 값을 한꺼번에 설정해야 한다 (setCartesian(double x, double y))

 

위의 클래는 직교좌표계를 사용한다

개별적으로 좌표값을 설정하게 강제한다

구현을 노출한다

변수를 private 로 선언하더라도 각 값마다 get,set 함수를 제공한다면 구현을 외부로 노출하는 셈이다

 

 

public interface Vehicle{
    double getFuelTankCapacityInGallons();
    double getGallonOfGasoline();
}

public interface Vehicle{
    double getPercentFuelRemaining();
}

아래의 인터페이스가 더 좋다

자료의 상세한 공개보다는 추상적인 개념으로 표현하는 편이 좋다

 

인터페이스나 조회/설정 함수만으로는 추상화가 이루어지지 않는다

아무 생각없이 get, set 메서드를 추가하는 것은 바람직하지 않다

 

 

자료/객체 비대칭

앞의 예제는 객체와 자료구조 사이에 벌어진 차이를 보여준다

객체는 추상화 뒤로 자료를 숨기고 자료를 다루는 함수만 공개한다

자료구조는 자료를 그대로 공개하며 별다른 함수는 제공하지 않는다

 

 

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

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

public class Geometry{
    
    public double area(Object shape){
        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;
        }
        
        ..
    }
}

 

 

위 코드에서 Geometry 클래스에 둘레 길이를 구하는 perimeter() 함수를 추가하고 싶다면?

Square, Rectangle 등의 도형 클래스는 영향을 받지 않는다

반대로 새 도형을 추가하고 싶다면 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;
    }
}

...

객체지향의 개념에 부합하는 도형 클래스를 구현했다고 치자

여기서 area 는 다형메서드이다 (Geometry 클래스는 필요없다)

그러므로 새 도형을 추가해도 기존 함수에 아무런 영향을 미치지 못한다

그러나 Shape interface 에 새 함수를 추가하고 싶다면 도형 클래스를 전부 고쳐야 한다

 

 

이 두 예시가 객체와 자료구조의 반대 개념을 가장 잘 표현해준다

객체지향 코드에서 어려운 변경은 절차지향 코드에서 쉽고, 절차에서 어려우면 객체에서 쉽다

 

 

디미터 법칙

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

객체는 자료를 숨기고 함수를 공개 >> 객체는 조회 함수로 내부 구조를 공개하면 안된다는 의미

 

클래스 C

f가 생성한 객체

f 인수로 넘어온 객체

C 인스턴스 변수에 저장된 객체

 

이 4가지 객체의 메서드만 호출해야 한다는 뜻

 

 

구조체 구조 + 객체 구조 

형태의 구조는 피하자

 

 

 

 

자료 전달 객체

자료 구조체의 전형적인 형태는 공개 변수만 있고 함수가 없는 클래스

DTO (Data Transfer Object) 라고 하기도 한다

 

DB와 통신하거나 소켓에서 받은 메세지 구문 분석 등 유용

 

좀 더 일반적인 형태는 Bean 구조

 

public class Address{
    private String street;
    private String streetExtra;
    private String city;
    private String state;
    private String zip;
    
    public Address(String street, String streetExtra, String city, String state, String zip){
        this.street = street;
        this.streetExtra = streetExtra;
        this.city = city;
        this.state = state;
        this.zip = zip;
    }
    
    public String getSreet(){
        return street;
    }
    ...
    
}

Bean 은 private 변수를 get, set 함수로 조작

일종의 사이비 캡슐화

 

 

 

활성 레코드

 

DTO의 특수한 형태

보통 save, find 같은 탐색 함수도 제공

데이터베이스 테이블이나 다른 소스에서 자료를 직접 변환한 결과

 

활성레코드는 비즈니스 로직 메서드를 추가해 자료구조 + 객체 구조 형태를 띈다

자료구조로 취급하고 비즈니스 로직을 담으면서 내부 자료를 숨기는 객체를 따로 생성하도록 한다

 

 

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

Clean Code -7 단위테스트  (0) 2023.08.14
Clean Code -6 오류 처리  (0) 2023.08.10
Clean Code -4 형식 맞추기  (0) 2023.08.10
Clean Code -3 주석  (0) 2023.08.10
Clean Code -2 함수  (0) 2023.08.09

 

형식을 맞추는 목적

코드 형식은 매우 중요

의사소통의 일환이기 때문

맨 처음 잡아놓은 구현 스타일과 가독성 수준은 유지/보수의 용이성과 확장성에 지속적인 영향을 미친다

코드는 수정될지언정 개발자의 스타일과 규율은 계속해서 남아있기 때문에

 

 

적절한 행 길이를 유지하라

 

500줄을 넘지 않고 대부분 200줄 정도인 파일로도 커다란 시스템을 구축할 수 있다

반드시 지킬 엄격한 규칙은 아니지만, 일반적으로 큰 파일보다 작은 파일이 이해하기 쉽다

 

신문기사처럼 작성하라

 

소스파일도 신문기사처럼 작성하라

이름은 간단하면서 설명이 가능하게

소스 파일 첫 부분은 고차원 개념, 알고리즘을 설명

아래로 내려갈수록 세세한 의도를 묘사

마지막에는 가장 저차원 함수와 세부 내역

 

 

개념은 빈 행으로 분리하라

 

대부분의 코드는 왼쪽에서 오른쪽으로

위에서 아래로 읽힌다

각자의 개념 사이에 빈 행을 넣어 완결됨을 표현하자

행 분리가 안되있다면 코드 가독성이 현저하게 떨어지고 피곤함을 유발한다

 

 

세로 밀집도

 

줄바꿈이 개념을 분리한다면 세로 밀집도는 연관성을 의미한다

서로 밀접한 코드 행은 세로로 가까이 놓여야 한다는 뜻이다

 

 

 

수직거리

 

서로 밀접한 개념은 세로로 가까이 두고 가능한 한 파일에 속해야 마땅하다

>> protected 변수를 피해야 하는 이유 중 하나

같은 파일에 속할 정도로 밀접한 두 개념은 세로 거리로 연관성을 표현

 

 

- 변수선언

변수는 사용하는 위치에 최대한 가깝게 선언한다

우리가 만들 함수는 짧으니까, 지역변수는 각 함수의 맨 위에 선언한다

    private static void readPreferences(){
        InputStream is = null;
        
        try{
            is = new FileInputStream(getPreferencesFile());
            setPreferences(new Properties(getPreferences()));
            
            ....
        }
        
    }

InputStream is 변수는 함수의 맨 위에 선언하고 바로 아래의 try 문에서 바로 사용된다

 

 

루프를 제어하는 변수는 루프문 안에 선언한다

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

 

 

- 인스턴스 변수

인스턴스 변수는 클래스 맨 처음에 선언한다

변수 사이에 세로 거리를 두지 않는다

잘 설계한 클래스는 클래스의 많은 메서드가 인스턴스 변수를 사용하기 때문이다

 

 

- 종속함수

한 함수가 다른 함수를 호출한다면, 두 함수는 세로로 가까이 배치한다

또한 가능하다면 호출하는 함수를 호출되는 함수보다 먼저 배치한다

 

    public void firstFunction(){
        int firstValue = secondFunction();
        ...
    }
    
    public int secondFunction(){
        ...
    }

호출되는 함수를 찾기가 쉬워지고, 그 만큼 모듈 전체의 가독성도 향상된다

 

 

- 개념적 유사성

개념적인 친화도가 높을수록 가까이 배치한다

한 함수가 다른 함수를 호출하며 생기는 종속성

변수와 그 변수를 사용하는 함수

비슷한 동작을 수행하는 함수들

 

 

public class Assert{
    static public void assertTrue(String message, boolean condition){
        ...
    }
    
    static public void assertTrue(boolean condition){
        ...
    }
    
    static public void assertFalse(String message, boolean condition){
        ...
    }
    
    static public void assertFalse(boolean condition){
        ...
    }
}

개념적인 친화도가 매우 높은 예시

기본 기능이 유사하고 서로가 서로를 호출하는 관계는 부차적인 요인

종속 관계가 없더라도 가까이 배치할 함수들

 

 

 

세로 순서

 

일반적으로 함수 호출 종속성은 아래방향으로 유지

신문기사처럼 가장 중요한 개념을 가장 먼저 표현 (세세한 사항 최대한 배제)

세세한 사항은 마지막에 표현

>> 독자가 소스를 읽을 때 첫 함수 몇 개만 읽어도 개념 파악 쉬워진다

 

 

가로 형식 맞추기

짧은 행이 바람직하다

100자 ~ 120자 정도가 적당

 

 

가로 공백과 밀집도

 

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

 

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

할당 연산자를 강조하려고 앞 뒤에 공백 부여

할당문은 왼쪽 요소와 오른쪽 요소가 분명하게 나뉜다

 

반면 함수 이름과 이어지는 괄호 사이에는 공백 X

함수와 인수는 서로 밀접한 관계를 가지기 때문

 

 

연산자 우선순위 강조를 위한 공백을 사용하기도 한다

 

public double root(double a, double b, double c){
	return (-b + Math.sqrt(determinant(a, b, c)) / (2*a));
}

곱셈은 우선순위가 가장 높기 때문에 공백을 주지 않는다

덧셈, 뺄셈은 우선순위가 곱셈보다 낮기 때문에 공백으로 표현한다

 

 

가로 정렬

 

어셈블리처럼 가로정렬 할 필요 없다

 

 

들여쓰기

 

소스파일은 윤곽도 (outline) 과 계층이 비슷

파일 전체에 적용되는 정보

파일 내 개별 클래스에 적용되는 정보

클래스 내 각 메서드에 적용되는 정보

블록 내 블록에 재귀적으로 적용되는 정보

등이 있다

 

계층에서 각 수준은 이름을 선언하는 범위이자 선언문, 실행문을 해석하는 범위이다

범위(scope) 로 이루어진 계층을 표현할 때 들여쓰기를 사용

 

 

가짜 범위

 

빈 while 문이나 for 문의 세미콜론은 행을 분리하라

 

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

 

 

 

팀 규칙

프로그래머가 팀에 속한다면 개인 규칙보다 우선 팀 규칙을 따라야 한다

그래야 코드가 일관적인 스타일을 보인다

 

 

 

 

 

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

Clean Code -6 오류 처리  (0) 2023.08.10
Clean Code -5 객체와 자료구조  (0) 2023.08.10
Clean Code -3 주석  (0) 2023.08.10
Clean Code -2 함수  (0) 2023.08.09
Clean Code -1 의미있는 이름  (0) 2023.08.08

잘 달린 주석은 좋다

그러나 주석이 필요 없는 코드를 짜는 것이 훨씬 좋다

 

주석이 불편한 이유?

코드를 유지, 보수하는 과정에서 주석까지 관리하고 다듬을 여유가 없다

>> 주석은 낡기 마련

 

코드의 수정이 많을수록 주석은 부정확할 확률이 높다

부정확한 주석을 사용할 바엔 아예 주석을 사용하지 않는 것이 좋다

 

 

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

주석이 필요한 상황? 코드의 품질이 나쁠 경우!

어수선한 코드를 주석으로 깔끔하게 정리할 시간에 그냥 코드 자체를 정리해라

 

 

코드로 의도를 표현하라

 

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

 

아래의 코드는 함수 이름만으로 의도를 표현하고 있다

많은 경우 주석으로 달려는 설명을 함수로 만들어 표현해도 충분하다

 

 

좋은 주석

어떤 주석은 필요하거나 유익하다

 

 

법적인 주석

 

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

각 소스 파일 첫머리의 저작권 정보, 소유권 정보 등

 

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

 

정보를 제공하는 주석

 

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

 

// 테스트 중인 Respnder 인스턴스 반환
protected abstract Responder responderInstance();

위 주석은 추상 메서드가 반환할 값을 설명하고 있다

 

>> 함수 이름을 responderBeingTested 로 바꾼다면 주석이 필요 없긴 하다

 

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

위의 주석은 코드에서 사용한 정규식이 시간과 날짜를 뜻한다고 설명한다

 

 

의도를 설명하는 주석

 

    public int compareTo(Ojbect o){
        if(o instanceof WikiPagePath){
            WikiPagePath p = (WikiPagePath) o;
            String compressedName = StringUtil.join(names, "");
            String compressedArgumentName = StringUtil.join(p.name, "");
            return compressedName.compareTo(compressedArgumentName);
        }
        return 1;   // 옳은 유형이므로 순위가 더 높다
    }

두 객체를 비교할 때 다른 어떤 객체보다 자기 객체에 높은 순위를 주는 코드

 

 

 

 

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

 

모호한 인수나 반환값의 의미를 명료하게 밝히는 주석이 유용

 

    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 = ouput.toString();
    assertSubString("Content-Length: 10000000", reponseString);
    assertTrue(byteSent > 10000000);
}

 

요즘에는 @Ingnore 속성을 이용해 테스트 케이스를 끄기도 한다

구체적인 설명은 @Ignore 속성에 문자열로 넣어준다

@Ignore("실행이 오래걸린다.")

 

publi static SimpleDataFormat makeStandardHttpDateFormat(){
	// SimpleDateFormat 은 스레드에 안전하지 못하다
    // 따라서 각 인스턴스를 독립적으로 생성해야 한다
    
    SimpleDateFormat df = new SimpleDateFormat("EEE, dd mmm yyyy HH:mm:ss z);
    df.setTimeZone(TimeZone.getTimeZone("GMT"));
    return df;
}

주석이 아주 적절한 예시

프로그램 효율을 높이기 위해 정적 초기화 함수를 사용하려던 프로그래머가 주석때문에 실수를 면할 수 있다

 

 

 

TODO 주석

 

앞으로 할 일을 // TODO 주석으로 남기면 편리하다

 

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

 

최신 IDE에서는 TODO 찾아 보여주는 기능이 있어서 주석을 잊어버릴 걱정도 없다

>> 주기적으로 TODO 점검하며 주석 삭제 가능

 

 

중요성을 강조하는 주석

 

대수롭지 않다고 여겨질 뭔가의 중요성을 강조하기 위해 주석 사용 가능

 

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

주석이 코드보다 더 많은 정보를 전달하지 못한다

읽기 쉽지도 않다

 

 

public abstract class ContainerBase
    implements Container, Lifecycle, Pipeline, MBeanRegistration, Serializable {

    /**
     * 이 컴포넌트의 프로세서 지연값
     */
    protected int backgroundProcessorDelay = -1;

    /**
     * 이 컴포넌트를 지원하기 위한 생명주기 이벤트
     */
    protected LifecycleSupport lifecycle = new LifecycleSupport(this);

    /**
     * 이 컴포넌트를 위한 컨테이너 이벤트 Listener
     */
    protected ArrayList listeners = new ArrayList();

    /**
     * 컨테이너와 관련된 Loader rngus
     */
    protected int backgroundProcessorDelay = -1;

    ...

Tomcat 에서 가져온 위 코드를 보면

 

1. 쓸모없고 중복된 Javadocs가 매우 많다

2. 코드만 지저분하고 정신없게 만든다

3. 기록이라는 목적에 전여 기여하지 못한다

 

 

 

오해할 여지가 있는 주석

 

위의 waitForClose() 함수를 다시 보면

this.closed 가 true 여야 메서드는 반환된다

아니면 타임아웃을 기다렸다가 this.closed 가 그래도 true 가 아니면 예외를 던진다

>> 주석에 담긴 살짝 잘못된 정보로 인해 this.closed 가 true 로 변하는 순간에 함수가 반환될거라고 생각할 수도 있다

 

 

 

의무적으로 다는 주석

 

모든 함수, 변수에 주석을 달아야 한다는 것은 어리석다

이런 주석은 코드를 복잡하게 만들고 거짓을 퍼뜨리며 혼돈과 무질서를 초래한다

 

 

/*

@param title CD제목
@param author CD저자
@param tracks CD숫자
@param durationInMinutes CD길이 (단위 분)

 */

public void addCD(String title, String author, int tracks, int durationInMunutes){
    CD cd = new CD();
    cd.title = title;
    cd.author = author;
    cd.durartion = durationInMinutes;
    cdList.add(cd);
}

 

 

이런 주석은 정말 아무런 가치가 없다

 

 

 

이력을 기록하는 주석

 

// * 12-nov-2001 : 버그수정
// * 05-dec-2001 : xx 클래스에 존재하는 오류 수정
...

소스 관리 시스템이 없을 때에는 이렇게 주석을 활용했지만, 지금은 그냥 쓰지말자

 

 

있으나 마나 한 주석

 

// 기본생성자
protected AnnulDateRule(){

}

// 월 중 일자
private int dayOfMonth;

전형적인 중복 정보를 전달하는 주석

 

 

 

닫는 괄호에 다는 주석

 

    try{
        while(true){
            ...
        }   // while
        
    }   // try
    catch(Exception e){
        ..
    }   // catch

중첩이 심하고 장황한 함수라면 의미가 있을 지 모르지만 작고 캡슐화 된 함수에는 잡음일 뿐이다

주석을 달 생각 하지 말고 함수를 줄일 생각을 하자

 

 

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

 

/* xx가 수정함 */

이런 주석은 쓸 필요가 없다

소스 코드 관리 시스템에 기록되어야 하는 정보

 

 

주석으로 처리한 코드

 

주석 처리된 코드는 이유가 있어 주석 처리 해놓았을 것이라고 짐작하게 만든다

이런 코드는 점점 쌓이고 질 나쁜 코드가 된다

 

 

 

HTML 주석

 

소스 코드에서 HTML 주석은 혐오스럽다

주석에 HTML 은 절대 넣지 말자

 

 

전역 정보

 

주석을 꼭 달아야 한다면 근처에 있는 코드만 기술하라

시스템의 전반적인 정보를 기술하지 마라

 

 

너무 많은 정보

 

주석에 흥미로운 역사나 관련없는 정보를 늘어놓지 마라

 

 

 

모호한 관계

 

주석과 주석이 설명하는 코드 사이의 관계가 명확해야 한다

 

// 모든 픽셀을 담을 만큼 충분한 배열로 시작(여기에 필터 바이트 더함)
// 그리고 헤더 정보를 위해 200바이트 더함

this.pngBytes = new byte[((this.width + 1) * this.height * 3)+ 200];

여기서 필터 바이트란 뭘까? +1과 관련이 있을까? *3 과 관련이 있을까? 아니면 둘다?

주석의 목적은 정보를 전달하는 것인데, 정보가 전달되지 않는다

코드를 설명하기 위한 주석을 설명하기 위한 무언가가 필요하다는 말 >> 안좋은 주석

 

 

함수 헤더

 

짧은 함수는 긴 설명이 필요 없다

 

 

 

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

Clean Code -6 오류 처리  (0) 2023.08.10
Clean Code -5 객체와 자료구조  (0) 2023.08.10
Clean Code -4 형식 맞추기  (0) 2023.08.10
Clean Code -2 함수  (0) 2023.08.09
Clean Code -1 의미있는 이름  (0) 2023.08.08

 

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

위 코드의 문제점은 다음과 같다

- 추상화 수준이 다양

- 하나의 함수 안에 너무 긴 코드 삽입

- 두 겹 이상 중첩 된 if 문

 

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

 

위와 같이 바꾼다면 적어도 함수가 setup 페이지와 teardown 페이지를 테스트 페이지에 넣은 후 해당 테스트 페이지를 HTML로 렌더링 한다는 사실을 짐작할 수는 있다

 

 

 

작게 만들어라

함수를 만드는 첫번 째 규칙은 '작게' 만드는 것이고

두 번째 규칙은 '더 작게' 만드는 것이다

 

!! 함수는 20줄도 길다 !!

일반적인 함수는 바로 위의 코드보다도 짧아야 한다 !!

 

 public static String renderPageWithSetupsAndTearDowns(PageData pageData, boolean isSuite) throws Exception {
        if (isTest(pageData)) {
            includeSetupAndTeardownPages(pageData, isSuite);
        }
        return pageData.getHtml();
    }

 

이정도로 !!

 

블록과 들여쓰기

다시말해 if 문, else 문, while 문등에 들어가는 블록은 한 줄이어야 한다는 의미이다

바깥을 감싸는 함수 (enclosing function)이 작아질 뿐 만 아니라 블록 안에서 호출하는 함수 이름을 적절히 짓는다면 가독성 역시 좋아진다

 

 

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

 

 

위 코드는 

1. 페이지가 테스트 페이지인지 판단

2. 맞다면 설정 페이지와 해제 페이즈를 삽입

3. 페이지를 HTML 렌더링

 

세 단계를 가진다

이 세 가지 단계는 지정된 함수 이름 아래 추상화 수준이 하나이다

우리가 함수를 만드는 이유는 큰 개념(함수 이름)을 다음 추상화 수준에서 여러 단계로 나눠 수행하기 위해서다

 

 

 

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

- getHtml() 은 추상화 수준이 매우 높다

- String page pagePathName = PathParser.render(pagepath); 는 추상화 수준이 중간이다

- .append('\n) 은 추상화 수준이 아주 낮다

 

한 함수 내에 추상화 수준을 섞으면 코드를 읽는 사람이 헷갈린다

특정 표현이 근본 개념인지, 세부사항인지 구분하기 어렵기 때문

 

 

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

코드는 위에서 아래로 이야기처럼 읽혀야 좋다

한 함수 다음에는 추상화 수준이 한 단계 낮은 함수가 온다

 

즉, 위에서 아래로 프로그램을 읽으면 함수 추상화 수준이 한 번에 한 단계씩 낮아진다 >> 내려가기 규칙

 

TO 설정 페이지와 해제 페이지를 포함하려면, 설정 페이지를 포함하고, 테스트 페이지 내용을 포함하고, 해제 페이지 내용을 포함한다.
  
TO 설정 페이지를 포함하려면, 슈트이면 슈트 설정 페이지를 포함한 후 일반 설정 페이지를   포함한다.
  
TO 슈트 설정 페이지를 포함하려면, 부모 계층에서 "SuiteSetUp" 페이지를 찾아 
include 문과 페이지 경로를 추가한다.
  
TO 부모 계층을 검색하려면....

여기서 각 TO 문단은 현재 추상화 수준을 설명하며 이어지는 아래 단계 TO 문단을 참고한다

 

핵심은 짧으면서도 한 가지만 하는 함수

TO 문단을 읽어내려 가듯 코드를 구현하면 추상화 수준을 일관되게 유지하기 쉬워진다

 

 

 

Switch 문

switch 문은 작게 만들기 어렵다

본질적으로 switch 문은 N가지를 처리한다

>> 각 switch 문을 저차원 클래스에 숨기고 절대 반복하지 않는 방법 (다형성 이용)

 

  public Money calculatePay(Employee e) throws InvalidEmployeeType {
        switch (e.type) {
            case COMMISSIONED:
                return calculateCommissionedPay(e);     
            case HOURLY:
                return calculateHourlyPay(e);
            case SALARIED:
                return cacualteSalariedPay(e);
            default:
                throw new InvalidEmployeeType(e.type);
        }
    }

직원 유형에 따라 다른 값을 계산해 반환하는 함수

 

위 함수의 문제점

1. 함수가 길다 : 새 직원 유형을 추가하면 더 길어진다

2. 한 가지 작업만 수행하지 않는다

3. SRP(Single Responsibility Principal) 를 위반한다 : 코드를 변경해야 되는 이유가 다양하다

4. OCP(Open Closed Principal) 을 위반한다 : 새 직원 유형을 추가할 때 마다 코드를 변경해야 하기 때문이다

5. 위 함수와 구조가 동일한 함수가 무한정 존재한다 : isPayday(Employee e, Date date) 등등..

 

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{
    @Override
    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)
        }
    }
}

위 함수는 switch 문을 추상 팩토리에 숨기고 보여주지 않는다

팩토리는 switch 문을 사용해 적절한 Employee 파생 클래스의 인스턴스를 생성한다

calculatePay, isPayday, deliverPay 등의 함수는 Employee 인터페이스를 거쳐 호출된다

그러면 다형성으로 인해 실제 파생 클래스의 함수가 실행된다

 

 

서술적인 이름을 사용하라

길고 서술적인 이름이 짧고 어려운 이름보다 좋다

길고 서술적인 이름이 길고 서술적인 주석보다 좋다

함수 이름을 정할 때는 여러 단어가 쉽게 읽히는 명명법을 사용한다

그 다음, 여러 단어를 사용해 그 함수의 기능을 가장 잘 표현하는 이름을 선택한다

 

includeSetupAndTeardownPages

includeSetupPages

includeSuiteSetupPage

includeSetupPage

등이 좋은 예

 

문체가 비슷하면 이야기를 순차적으로 풀어가기도 쉬워진다

 

 

 

함수 인수

함수에서 가장 이상적인 인수 개수는 0개

다음은 1개, 다음은 2개

3항 이상은 피하는 편이 좋다

4개 이상은 특별한 이유가 필요

 

 

단항

 

함수에 인수 1개를 넘기는 가장 흔한 경우

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

boolean fileExists("fileName")

파일이 존재하는지 여부를 반환

 

 

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

InputStream fileOpen("fileName")

String 형의 파일 이름을 InputStream 으로 변환

 

 

드물게 사용하지만 아주 유용한 단항 형식이 바로 이벤트

이벤트 함수는 입력 인수만 있고 출력 인수는 없다

 

passwordAttemptFailedNtimes(int attempts)

이벤트 함수의 좋은 예

 

 

위의 경우가 아니라면 단항 함수는 가급적 피하는게 좋다

 

예를 들어

StringBuffer transform(StringBuffer in)

이 코드가

 

void transform(StringBuffer out)

보다 좋다

 

 

StringBuffer transform(StringBuffer in)

이 함수가 입력 인수를 그대로 돌려주는 함수라 할지라도 변환함수 형식을 따르는 편이 좋다

적어도 변환 형태는 유지되기 때문

 

 

플래그 인수

 

플래그 인수는 사용하지 않는 것이 좋다

함수는 한 가지 동작을 해야하지만, 플래그 인수를 사용하게 되면 참일 땐 이것, 거짓일 땐 저것 등 여러 동작을 포함하기 때문이다

 

render(true) // render(boolean isSuite)

라는 코드 보다는 

 

renderForSuite() 와 renderForSingleTest() 

두 개의 함수로 나누는 것이 적절하다

 

 

이항

 

인수가 2개인 함수는 1개인 함수보다 이해하기 어렵다

writeField(name) 은 writeField(outputStream, name) 보다 이해하기 쉽다

 

writeField 메서드를 ouputstream 클래스의 구성원으로 만들어 

outputStream.writeField(name) 형태로 호출하는 것이 적절

 

 

 

 

인수가 3개인 함수는 2개인 함수보다 더 이해하기 어렵다

 

assertEquals(message, expected, actual)

이 함수는 한 번에 이해하기 어렵다

 

반면 

assertEquals(1.0, amount, .001)

이 함수는 삼항이 충분히 납득 가는 함수

 

 

** assertEquals 함수는 Junit4 에서 사용되는, 두 객체의 값이 같은지 여부를 판단하는 함수

assertEquals(message, expected, actual) 

actual 의 값이 expected 의 값과 동일한지 비교하고

테스트가 실패할 시, message 를 출력한다

 

 

 

 

인수 객체

인수가 2개, 3개가 필요하다면 일부를 독자적인 클래스 변수로 선언할 가능성을 고려해보자

 

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

 

아래줄처럼 x, y 좌표를 center 하나의 객체로 묶어 넘기려면 객체의 이름 (center) 이 필요하므로 결국은 개념을 표현하게 되므로 보다 직관적

 

 

 

인수 목록

 

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

 

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

 

위 예제처럼 가변 인수 전부를 동등하게 취급하면 List 형 인수 하나로 취급할 수 있다

>> String.format 은 사실상 이항함수

public String format(String format, Ojbect... args)

선언부 살펴보면 이항함수인 것을 확인 가능

 

하지만 3개를 넘어가는 인수를 사용하는 경우는 자제하자

 

 

 

동시와 키워드

 

함수의 의도나 인수의 순서, 의도를 제대로 표현하려면 좋은 함수 이름이 필수다

단항함수는 함수와 인수가 동사/명사 쌍을 이뤄야 한다

 

write(name)

같은 경우 '이름' 을 '쓰다' 라는 의미가 명확하다

 

더 정확하게는

writeField(name)

이렇게 표현한다면 

name 이 filed  라는 사실이 분명히 드러난다

 

함수 이름에 인수 이름을 넣는 방법을 활용할 수도 있다

assertEquals 보다

assertExpectedEqualsActual(expected, actual);

이렇게 표현하는 것이 좋다

인수의 순서를 기억할 필요가 없기 때문

 

 

 

부수 효과를 일으키지 마라

부수효과 >> 거짓말

함수에서 한 가지를 하겠다고 약속해놓고선 몰래 다른짓도 하니까!!

 

많은 경우 시간적인 결함 (temporal coupling) 이나 순서 중복성 (order dependency) 를 초래한다

 

 

public class UserValidator {
	private Cryptographer cryptographer;
	public boolean checkPassword(String userName, String password) { 
		User user = UserGateway.findByName(userName);
		if (user != User.NULL) {
			String codedPhrase = user.getPhraseEncodedByPassword(); 
			String phrase = cryptographer.decrypt(codedPhrase, password); 
			if ("Valid Password".equals(phrase)) {
				Session.initialize();
				return true; 
			}
		}
		return false; 
	}
}

 

표준 알고리즘을 사용해 userName 과 password 를 확인한다

두 인수가 올바르면 true 를 반환하고 아니면 false 반환

 

But 함수는 부수 효과를 일으킨다 >> Session.initialize() 호출 !!

 

checkPassword 함수는 이름 그대로 암호를 확인하는 함수

이름만 봐서는 세션을 초기화 하는 사실을 알 수 없다

 

함수 이름만 보고 함수를 호출하는 사용자는 사용자를 인증하면서 기존 세션 정보를 지워버릴 위험에 처할 수 있다

이런 부수 효과가 시간적인 결합을 초래하게 된다

 

즉, checkPassword 함수는 특정 상황, 세션을 초기화 해도 괜찮은 경우에만 호출이 가능

 

시간적인 결합이 필요하다면 함수 이름에 이를 분명하게 명시해야 한다

따라서 checkPasswordAndInitializeSession 이라는 이름이 위의 함수에 더 적합하다

 

 

출력인수

 

일반적으로 우리는 인수를 함수 입력으로 해석한다

 

appendFooter(s);

위 같은 함수를 보았을 때 s를 footer로 첨부한다는 뜻일까? 아니면 footer를 s에 첨부한다는 뜻일까?

의미가 모호하다

 

public void appendFooter(StringBuffer report);

 

함수 선언부를 찾아봐야지만 인수 s가 출력 인수라고 알 수 있다

 

 

** 입력 인수 : 일반적인 의미의 인수, 함수에게 전달되는 인수

** 출력 인수 : 함수에서 결과를 돌려받는 인수

void sum(int *a){
	a = a + 1;
}

포인터 형태로 매개변수를 전달받고 값을 변경

a 가 참조하는 값이 sum 함수의 동작 결과로 반영된다

 

void sum(VO vo){
	vo.setValue = vo.getValue + 1;
}

Java 에서는 위와 같이 객체도 출력 인수로 활용 가능

 

 

객체지향 개념에서 출력 인수로 사용하라고 설계한 변수 : this

 

report.appendFooter()

이렇게 호출하는 방식이 적합하다

 

일반적으로 출력 인수는 피하고, 함수에서 굳이 상태 변경을 해야한다면 함수가 속한 객체 상태를 변경하는 방식을 택하는 것이 맞다

 

 

명령과 조회를 분리하라

함수는 뭔가를 수행하거나 뭔가에 답하거나 둘 중 하나만 해야 한다

객체 상태를 변경하거나, 객체 정보를 반환하거나 둘 중 하나만, 둘 다 하면 혼란을 초래하게 된다

 

pulbic boolean set(String attribute, String value);

이 함수는 이름이 attribute 인 속성값을 찾아 값을 value 로 설정한 후 성공하면 true, 실패하면 false 를 반환한ㄴ 함수

 

if (set("username", "unclebob"))

따라서 위와 같은 괴상한 코드가 생긴다

 

함수를 모르는 사람은 위 코드를 보고 username 이 unclebob 으로 설정되있는지 확인하는 코드인가?

username 을 unclebob 으로 설정하는 코드인가?

의미가 모호할 수 있다

 

set이라는 단어가 동사인지 형용사인지 구분하기 어렵기 때문이다

 

위 함수를 구현한 개발자의 의도는 동사 set이다

하지만 우리들은 if 문을 읽을 때 

"username 이 unclebob 으로 설정되어 있다면..."

이라고 해석하기 쉽다

코드 작성자의 의도와 다르게 파악하는 것이다

 

 

set 함수를 setAndCheckIfExists 라고 바꿀 수도 있지만

 

if(attributeExists("username")){
	setAttribute("username", "unclebob");
    ...
}

위와 같이 check 와 set 을 분리해 혼란을 방지하는 방법이 가장 좋다

 

 

 

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

명령 함수에서 오류 코드를 반환하는 방식은 명령/조회 분리 규칙을 미묘하게 위반한다

 

자칫 if 문에서 명령을 표현식으로 사용하기 쉽기 때문이다

동사/형용사 혼란을 일으키지 않는 대신 여러 단계로 중첩되는 코드를 야기한다

if (deletePage(page) == E_OK)

오류 코드를 반환하면 호출자는 오류 코드를 곧바로 처리해야 한다는 문제를 마주한다

 

"오류 코드가 E_OK 일때 ...."

의 동작을 직접 구현해야 하기 때문이다

 

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

if 문의 중첩을 통해 E_OK 인지, 아닌지의 경우의 동작을 하나하나 구현해야 한다

 

반면에

 

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

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 함수는 모든 오류를 처리한다

그래서 코드를 이해하기 쉽다

 

실제로 페이지를 제거하는 함수는 deletePageAndAllReferences 이다

이 함수는 예외를 처리하지 않고 오로지 페이지를 제거하는 동작 하나만 수행한다

 

이렇게 정상 동작과 오류 처리 동작을 따로 함수로 분리하여 구현하면 코드를 이해하고 수정하기 쉬워진다

 

 

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

 

함수는 한 가지 작업만 해야 한다

오류 처리도 마찬가지

 

함수에 try 키워드가 있다면 함수는 try 문으로 시작해 catch / finally 문으로 끝나야 한다

 

 

의존성 자석

 

public enum Error{
    OK,  
    INVALID,
    NO_SUCH,
    LOCKED,
    OUT_OF_REFERENCE,
    WATING_FOR_EVENT;
}

위와 같은 코드는 의존성 자석이다

다른 클래스에서 import 해서 사용해야 한다

즉, Error enum 이 변하면 Error enum 을 사용하는 클래스 전부 다시 컴파일하고 배치해야 한다

>> Error enum 클래스는 수정이 어려워진다

이 과정이 복잡하므로 새 오류 코드를 추가하는 대신 기존 오류 코드를 재사용하게 된다

 

이런 오류코드 대신 예외를 사용하면 새 예외는 Exception 클래스에서 파생되고, 재컴파일 / 재배치 없이도 새ㅐ 예외 클래스를 추가할 수 있다

 

 

반복하지 마라

중복을 제거하라

중복이 일어나면 코드 길이, 변경사항, 오류에 대한 대처 등 신경 써야 할 부분이 기하급수적으로 늘어난다

 

AOP (Aspect Oriented Programming), COP (Component Oriented Programming) 등 중복을 위한 전략 존재

 

>> Spring 프레임워크에서는 AOP로 Exception Handling 을 global하게 관리할 수 있다

 

 

구조적 프로그래밍

다익스트라 

" 모든 함수와 함수 내 모든 블록에서 입구와 출구는 하나만 존재해야 한다 "

즉, 함수는 return 문이 하나여야 한다

 

루프 안에서 break 나 continue 를 사용해서는 안되며 goto 는 절대로 사용하면 안된다

 

 

>> 함수를 작게 만든다면 간혹 return, break, continue 를 여러차례 사용해도 된다

때로는 단일 입/출구 규칙보다 의도를 표현하기 쉬워지기도 한다

 

But goto문은 큰 함수에만 의미가 있으므로 그냥 쓰지 말자

 

 

 

함수를 어떻게 짜죠?

함수를 짜는 행위는 글짓기와 비슷하다

 

서투르고 어수선한 초안을 끊임없이 다듬고 정리해야 한다

 

우선 함수를 만들고 그 함수를 빠짐없이 테스트하는 단위 테스트케이스를 만든다

코드를 다듬고, 함수를 만들고, 이름을 변경하고, 중복을 제거하고 메서드를 줄이고 순서를 변경한다

이 와중에도 코드는 단위테스트를 항상 통과해야 한다

 

처음부터 모든것이 완벽한 함수를 짤 수는 없다

점진적으로 개선해 나아가자

 

 

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

Clean Code -6 오류 처리  (0) 2023.08.10
Clean Code -5 객체와 자료구조  (0) 2023.08.10
Clean Code -4 형식 맞추기  (0) 2023.08.10
Clean Code -3 주석  (0) 2023.08.10
Clean Code -1 의미있는 이름  (0) 2023.08.08

 

의도를 밝혀라

변수, 함수, 클래스의 이름은 

존재 이유? 수행 기능? 사용 방법?

이 질문에 대해 답할 수 있어야 한다

 

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

 

간단하지만 무슨 동작을 하는지 짐작하기 어려운 코드의 예시

 

theList 에 무엇이 들어있는지

theList 의 0 번째 값이 왜 중요한지

값 4는 무슨 의미인지

함수가 반환하는 list1 을 어떻게 사용하는지

 

위에 대한 정보가 없다

 

public List<int[]> getFlaggedCells(){
	List<int[]> flaggedCells = new ArrayList<int[]>;
    for(int[] cell : gameBoard)
    	if(cell[STATUS_VALUE]==FLAGGED)
        	flaggedCells.add(cell);
     return flaggedCells;
}

 

지뢰찾기 게임

gameBoard : 전체 게임 판

cell : 각 칸

STATUS_VALUE : 상태

FLAGGED : 깃발이 꽂힌 상태

 

이렇게 변경하면, 각각의 칸을 탐색하며 그 칸의 status 가 flagged 일 경우 flaggedCells 리스트에 넣는다, 즉, 깃발이 꽂힌 칸을 리턴하는 함수다 라는 것을 쉽게 파악할 수 있다

 

public List<Cell> getFlaggedCells(){
	List<Cell> flaggedCells = new ArrayList<Cell>;
    for(Cell cell : gameBoard)
    	if(cell[STATUS_VALUE]==FLAGGED)
        	flaggedCells.add(cell);
     return flaggedCells;
}

 

int[] 을 Cell 자료형으로 새롭게 표혀한 코드

매우 쉽게 코드를 이해할 수 있다

 

 

 

그릇된 정보를 피하라

프로그래머는 코드에 그릇된 정보를 남겨서는 안된다

예를 들어, 실제 리스트가 아닌 정보를 accountList (계정 모음) 등으로 표현하면 안된다

List 가 아님에도 변수명만 보고 List 라고 오해할 수 있기 때문

 

accountList >> accountGroup / bunchOfAccounts 등으로 표현하는 것이 적합

 

서로 흡사한 이름을 사용하지 않도록 주의하라

하나의 모듈에서 XYZControllerForEfficientHandlingOfStrings 라는 이름을 사용하고

다른 모듈에서 XYZControllerForEfficientHandlingOfStorageOfStrings 라는 이름을 사용한다면 

두 개가 헷갈릴 수 있다

 

유사한 개념은 유사한 표기법을 사용하라

왜? 일관성 유지를 위해서

 

 

int a = l;
if (O == l)
	a = O1;
else
	l = 01;

딱 봐도 O 와 0, 1과 l 가 헷갈리는 코드

매우 끔찍

 

 

의미있게 구분하라

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

위처럼 a1, a2, ... aN 등의 이름은 이름 그대로가 가지고 있는 의미가 없다

코드를 작성한 개발자의 의도가 전혀 드러나지 않는 코드 중 하나

 

 

함수 이름으로 source 와 destination 을 사용한다면 코드를 읽기 훨씬 편해진다

 

stopword 를 피하라

stopword 는 정보를 제공하지 않는 단어, 혹은 중복되는 단어

 

Info, Data 등의 접미사는 의미가 불분명한 용어

Product 라는 클래스가 있을 때, ProductInfo / ProductData 라는 클래스가 존재한다면 두 클래스의 개념이 모호해진다

 

MemberTable, 혹은 NameString 등의 변수명은 Member, Name 과 다른 의미를 가지지 못한다.

Cutomer 클래스와 CustomerObject 라는 클래스의 차이점 역시 찾기 힘들다

 

코드를 읽는 사람이 이름만 보고 차이를 알 수 있도록 이름을 지어야 한다

 

 

 

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

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

두번째 코드는 원활한 대화가 가능하다

 

 

 

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

MAX_CLASSED_PER_STUDENT 는 grep 으로 찾기 쉽지만, 숫자 '7'은 찾기 까다롭다

 

긴 이름이 짧은 이름보다 좋고, 검색하기 쉬운 이름이 상수보다 좋다

이름 길이는 범위 크기에 비례해야 한다

 

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

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

 

위 코드에서 sum 이 유용하진 않으나 검색이 가능하다

이름을 의미있게 지으면 함수가 길어진다

하지만, WORK_DAYS_PER_WEEK 를 찾기 쉽다

위처럼 상수 5를 사용한다면 검색하기 굉장히 어려워진다

 

 

인코딩을 피하라

유형, 범위 정보까지 인코딩에 넣으면 그만큼 이름을 해독하기 어려워진다

 

헝가리안 표기법은 기존 C언어 개발자들이 많이 사용

But 자바 프로그래머는 변수 이름에 타입을 인코딩할 필요가 없다

객체는 Strongly type 이며 IDE는 코드를 컴파일하지 않고도 타입 오류를 감지할 수 있다

 

 

그러나 때로는 인코딩이 필요한 경우도 있다

도형을 생성하는 Abstract Factory 의 Interface 가 존재하고 구체적인 구현은 Concrete class 에서 한다고 할 때, 

인터페이스는 ShapFactory, 구현 클래스는 ShapFactoryImpl  정도의 이름으로 정하는 것이 좋다

 

 

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

독자가 코드를 읽으면서 변수 이름을 자신이 아는 이름으로 변환해야 한다면 그 변수 이름은 바람직하지 못하다

반복문에서의 변수 i, j, k 등을 제외하면 한 글자 이름은 대부분의 경우에 적절하지 못하다

 

 

 

클래스 이름

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

 

Customer, WikiPage, Account, AddressParser 등이 좋은 예

Manager, Processor, Data, Info 등과 같은 단어는 좋지 않다

동사는 사용하지 않는 것이 좋다

 

 

메서드 이름

메서드 이름은 동사, 동사구가 적합하다

PostPayment, deletePage, save 등이 적절하다

Accessor, Mutator, Predicate 는 javabean표준에 따라 값 앞에 get, set, is 를 붙인다

String name = employee.getName();
customer.setName("mike");
if (paycheck.isPosted())...

 

생성자를 중복정의 할 때는 정적 팩토리 메서드를 사용

메서드는 인수를 설명하는 이름을 사용

Complex fulcrumPoint = Complex.FromRealNumber(23.0);

이 코드가

Complex furcrumPoint = new Complex(23.0);

이 코드보다 좋다

 

생성자 사용을 제한하려면 해당 생성자를 private 로 선언한다

 

 

기발한 이름은 피해라

재미난 이름보다 명료한 이름을 선택해라

구어체, 속어를 이름으로 사용하는 사례가 있는데, 이는 좋지 못한 표현이다

 

Kill() 대신 whack()

Abort() 대신 eatMyShort() 등

특정 문화에서만 사용하는 농담이나 속어 등은 피해라

 

 

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

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

IDE 에서 객체를 사용하면 객체가 제공하는 메서드의 목록을 볼 수 있다

그러나 메서드 명, 매개변수 이름 정도를 보여주기 때문에 메서드 이름은 독자적이고 일관적이어야 한다

 

동일 코드 기반에 controller, manager, driver 등을 섞어 쓰면 혼란스럽다

 

 

 

말장난을 하지 마라

한 단어를 두 가지 목적으로 사용하지 마라

다른 개념에 같은 단어를 사용한다면 그건 말장난에 불과하다

 

 

예를 들어, 지금까지 구현한 add 메서드는 기존 값 두 개를 더하거나 이어서 새로운 값을 만드는 메서드였는데, 

집합 전체에 값 하나를 추가하는 메서드를 역시 add 라고 부르는 것은 맥락이 다르기 때문에 좋지 못하다

add 대신 insert, append 등이 적합하다

 

 

 

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

코드를 읽는 사람도 프로그래머라는 사실을 명심하라

전산용어, 알고리즘 이름, 패턴 이름, 수학적 용어 등을 사용해도 괜찮다

 

VISITOR 패턴에 익숙한 프로그래머는 AccountVisitor 라는 이름을 금방 이해한다

기술 개념에는 기술 이름이 가장 적합한 선택이다

 

 

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

적절한 프로그래머 용어가 없다면 문제 영역에서 이름을 가져온다

코드를 보수하는 프로그래머가 분야 전문가에게 의미를 물어 파악할 수 있다

 

 

 

의미 있는 맥락을 추가하라

firstName, lastName, street, houseNumber, city, state, zipcode 라는 변수가 있을 때, 주소라는 사실을 알아차리기 쉽다

하지만 어느 메서드가 state 변수 하나만 사용한다면, 그 때는 변수 state가 주소의 일부라는 사실을 알아차리기 어렵다

 

이 때, addr 이라는 접두어를 추가한다면 이름의 맥락이 분명해진다

addrFirstName, addrLastName, addrState 라고 쓴다면 주소의 일부를 나타낸다는 것을 쉽게 알아차릴 수 있다

(당연하게도 Address 라는 클래스를 생성하는 것이 더 좋다)

 

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

위 코드에서 함수 이름은 부분적인 맥락 정보만 제공한다

따라서 알고리즘까지 이해해야 number, verb, pluralModifier 라는 변수들이 통계 추측 (guess statistics) 메세지에 사용된다는 것을 파악할 수 있다

 

public class GuessStatisticsMessage{
    
    private String number;
    private String verb;
    private String pluralModifier;
    
    public String make(char candidate, int count){
        createPluralDependentMessageParts(count);
        return String.format("There %s %s %s%s", verb, number, candidate, pluralModifier);
    }
    
    private void createPluralDependentMessageParts(int count){
        
        if(count==0){
            thereAreNoLetters();
        }
        else if(count == 1){
            thereIsOneLetter();
        }
        else{
            thereAreManyLetters(count);
        }
    }
    
    private void thereAreManyLetters(int count){
        number = Integer.toString(count);
        verb = "are";
        pluralModifier = "";
    }
    private void thereIsOneLetter(){
        number = "1";
        verb = "is";
        pluralModifier = "";
    }
    private void thereAreNoLetters(){
        number = "no";
        verb = "are";
        pluralModifier = "s";
    }
}

긴 함수

세 개의 변수를 함수 전반에 걸쳐 사용

두 가지를 해결하기 위해

GuessStatisticsMessage 라는 클래스를 만든 후 세 변수를 클래스의 필드로 선언하게 되면

number, verb, pluralModifier 세 변수는 확실하게 GuessStatisticsMessage 클래스에 속한다

즉, 함수 쪼개기가 쉽고 알고리즘도 명확해진다

 

 

 

불필요한 맥락을 없애라

예를 들어 Gas Station Deluxe 라는 애플리케이션을 만들 때, 모든 클래스 이름을 GSD 로 시작하는 것은 바람직하지 않다

 

accountAddress, customerAddress 는 Address 클래스 인스턴스로는 좋으나, 클래스 이름으로는 X

Address 는 클래스 이름으로 적합

Port, Mac, Web address 를 구분해야 한다면 PostalAddress, MAC, URI 라는 이름은 클래스 이름으로 괜찮다

 

 

 

 

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

Clean Code -6 오류 처리  (0) 2023.08.10
Clean Code -5 객체와 자료구조  (0) 2023.08.10
Clean Code -4 형식 맞추기  (0) 2023.08.10
Clean Code -3 주석  (0) 2023.08.10
Clean Code -2 함수  (0) 2023.08.09

+ Recent posts