본문 바로가기
Clean Code

[클린 코드] 10장 - 클래스 (Classes)

by 노력남자 2022. 3. 11.
반응형

이번 포스팅에선 클린 코드 10장 - 클래스에 대해 알아보겠습니다.

 

클래스 체계

 

클래스 내에 아래와 같은 순서로 코드를 작성하자.

 

1) static 변수 : public -> protected -> package -> private

2) instance 변수 : public -> protected -> package -> private

3) 생성자

4) 메서드 : (public -> private -> private) -> (public -> private -> private)...

 

public 메서드에서 호출되는 private 메서드는 바로 아래에 둔다.

 

public List<Cell> getFlaggedCells() {
    List<Cell> flaggedCells = new ArrayList();

    addFlaggedCells(flaggedCells);

    return flaggedCells;
}

private void addFlaggedCells(List<Cell> flaggedCells) {
    for (Cell cell : gameBoard)
        if (cell.isFlagged())
            flaggedCells.add(cell);
}

 

클래스는 작아야 한다!

 

함수 장에서 말했던 거와 동일하게 클래스도 작아야 한다.

 

작아야 읽기 쉽고 변경하기 쉬운 코드가 된다.

 

그럼 얼마나 작아야 하나? 함수는 행 수로 크기를 쟀는데 클래스는 얼마나 작아야 하나?

 

클래스는 행 수로 크기를 재지 않고 클래스가 맡은 책임을 센다.

 

아래처럼 메소드가 많으면 큰 클래스일까요?

 

public class SuperDashboard extends JFrame implements MetaDataUser {
    public String getCustomizerLanguagePath()
    public void setSystemConfigPath(String systemConfigPath)
    public String getSystemConfigDocument()
    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 setAçowDragging(boolean allowDragging)
    public boolean allowDragging()
    public boolean isCustomizing()
    public void setTitle(String title)
    public IdeMenuBar getIdeMenuBar()
    public void showHelper(MetaObject metaObject, String propertyName)
	// ...
}

 

아니면 이렇게 5개로 줄이면 작은 클래스일까요?

 

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

 

 

아무리 메소드가 많아도 책임이 1개면 큰 클래스라고 볼 수 없다.

 

아무리 작아도 책임이 여러개면 작은 클래스라고 볼 수 없다.

 

책임을 줄이자.

 

클래스를 어떻게 책임이 많은지 알 수 있을까?

 

1. 클래스 이름이 떠오르지 않는다면? 책임이 너무 많아서다.

 

Processor, Manager, Super 등과 같은 모호한 단어가 있다면 클래스가 여러 책임을 가지고 있다는 증거다.

 

 

2. 클래스 설명을 if, and, or, but을 사용하지 않고 25단어 내외로 할 수 없다면? 바로 그거다.

 

클래스를 어떻게 작게 만들 수 있을까?

 

1. 단일 책임 원칙(SRP)을 따르자

 

단일 책임 원칙(SRP: Single Responsibility Principle)은 클래스나 모듈을 변경할 이유가 하나 뿐이어야 한다는 원칙이다.

 

클래스는 변경할 이유가 하나여야 한다.

 

이 클래스를 다시보자.

 

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

 

위 클래스를 변경할 이유는 2개다. 책임이 2개다.

 

책임1. 버전 정보를 추적한다. 버전 정보는 소프트웨어를 출시할 때마다 달라진다.

 

책임2. 자바 스윙 컴포넌트를 관리한다. 스윙 코드를 변경할 때마다 버전 번호가 달라진다.

 

SuperDashboard에 책임1번을 걷어 내서 다른 클래스로 만들면 Version 클래스로 아주 깔끔하게 나온다.

 

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

 

 

 

2. 응집도를 낮게 유지하자.

 

응집도는 모듈 내 요소들이 서로 관련되어 있는 정도를 말한다.

 

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

 

메서드가 인스턴스 변수를 사용할수록 메서드와 클래스는 응집도가 높다.

 

모든 인스턴스 변수를 메서드마다 사용하는 클래스는 응집도가 가장 높다.

 

아래 stack 코드를 봐보자.

 

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

 

topOfStack, elemetns를 메소드 전부에서 사용하고 있다. 응집도가 아주 높은 클래스이다.

 

위와 같은 코드가 아니라

 

몇몇 메서드만이 사용하는 인스턴스 변수가 많은 경우? 그게 바로 새로운 클래스로 쪼개야 하는 신호인다.

 

 

3. 변경하기 쉬운 클래스를 만들려고 하자!

 

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 클래스 변경 시점이 어떻게 될까?

 

1. 새로운 SQL문이 추가되는 경우

2. 기존 SQL문을 수정해야 하는 경우

 

추가로 selectWithCriteria()는 select()에서만 사용한다.

 

SRP를 위반하고 있다.

 

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 추가, 수정, 삭제에 자유로워 졌다. SRP, OCP를 준수한다.

 

새 기능을 수정하거나 기존 기능을 변경할 때 건드릴 코드가 최소인 시스템 구조가 바람직하다.

 

클래스를 변경으로부터 격리시키자!

 

Portfolio 클래스는 외부 TokyoStockExchange API를 사용해 포트폴리오 값을 계산한다.

 

TokyoStockExchange에서 가지고 오는 값은 5분마다 달라진다. 그럼 어떻게 테스트를 짤 수 있을까?

 

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

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

 


위처럼 stockExchange를 interface로 만들어 Portfolio에 주입시키는 식으로 코드를 작성하면된다.

 

테스트용으로 고정된 값을 리턴해주는 FixedStockExchangeStub을 작성해서 테스트에 이용해주면 된다.

 

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

 

음 근데 저렇게 까지 stub을 따로 만들어서 테스트를 해본 적이 없는 거 같다.

 

나같으면 TokyoStockExchange를 mock으로 만들어서 테스트를 했을 거 같다.

 

 

결론 : 하나의 책임을 갖는 작은 클래스를 만들자.

반응형

댓글