GO SIWOO!

[클린 코드] 10장 - 클래스 본문

Develop/클린코드

[클린 코드] 10장 - 클래스

gosiwoo 2023. 9. 19. 23:40
 

[클린 코드] 9장 - 단위 테스트

1. TDD의 법칙 세 가지 1. 첫째 : 실패하는 단위 테스트를 작성할 때까지 실제 코드를 작성하지 않는다. 2. 둘째 : 컴파일은 실패하지 않으면서 실행이 실패하는 정도로만 단위 테스트를 작성한다. 3

gosiwoo.tistory.com

1. 클래스 체계

클래스를 정의하는 표준 자바 관례에 따르면 다음과 같다.

  1. public static 상수 (클래스 상수)
  2. private static 변수 (클래스 변수)
  3. private instance 변수
  4. public instance 변수
  5. public 함수
  6. private 함수
public class Person {
  public static final String DEFAULT_NAME = "Unknown"; // 1. 클래스 상수
  private static int instanceCount = 0; // 2. 클래스 변수
  private int age; // 3. private 인스턴스 변수
  public String name; // 4. public 인스턴스 변수
  
  // 5. public 함수
  public void introduce() {
        System.out.println("My name is " + name + " and I am " + age + " years old.");
  }
  
  // 6. private 함수
  private void doSomethingPrivate() {
        // 비공개 동작
    }
}

이 순서대로 추상화 단계가 순차적으로 내려간다.

 

캡슐화

  • 변수와 유틸리티 함수는 가능한 공개하지 않는 게 낫지만 반드시 숨겨야 하지는 않는다.
  • 캡슐화를 푸는 건 최후의 수단이며 비공개 상태를 유지할 방법을 고민하자.

2. 클래스는 작아야 한다

  • 클래스를 만들 때 첫 번째 규칙은 크기이다. 클래스는 작야야 하며 작아야 한다.
  • 클래스는 함수와 같이 작아야 한다. 물리적인 척도로의 작음이 아니라 클래스의 책임이 작아야 한다.
  • 클래스의 이름은 클래스의 책임을 기술해야 한다.
  • 클래스의 이름은 25단어 내로 만들어야 하며 if, and, or, but 등을 사용하지 않아야 한다.
  • 메서드의 수가 적어도 책임이 많으면 안된다.

단일 책임 원칙

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

위의 클래스는 버전 정보를 반환하는 책임과 JFrame인 스윙 코드를 관리한다.

아래와 같이 버전 정보를 반환하는 클래스를 분리해 Version 클래스로 만들자.

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

개발자들은 소프트웨어를 돌아가는데 집중해 깨끗하고 체계적인 소프트웨어를 만드는 건 다음 사정이다. 하지만 소프트웨어가 돌아가고 나면 이는 관심밖이 된다. 하나의 클래스에서 모든 책임을 관리하는 것이 클래스를 넘나드는 수고를 덜 수 있다고 생각하지만 이는 잘못된 이야기다. 복잡성을 다루려면 체계적인 정리가 필수다.

 

응집도

클래스는 인스턴스 변수 수가 적어야 하며 각 클래스 메서드는 인스턴스 변수를 하나 이상 사용해야 한다. 메서드가 인스턴스 변수를 많이 사용할수록 메서드와 클래스는 응집도가 더 높다.

응집도가 높은 클래스는 바람직하지 않다. 즉, 클래스 속의 메서드와 변수가 서로 의존하며 논리적인 단위로 묶이는 것은 지양하자.

 

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

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

  1. 더욱 길고 서술적인 변수 이름을 사용해 클래스를 분리
  2. 함수 선언과 클래스 선언으로 코드를 설명
  3. 가독성을 위해 공백을 추가하고 형식을 맞춤

이와 같은 방법으로 클래스를 나누자

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 쿼리문을 제공하는 Sql 클래스에 추가적인 SQL 문을 지원하려면 Sql 클래스에 손을 대야 하며 기존 SQL문 하나를 수정해도 Sql 클래스에 손을 대야 한다. 변경할 이유가 2가지나 되므로 SRP를 위반한다.

 

private 메서드인 selectWithCriteria는 select 문을 처리할 때만 사용한다.

이와 같은 비공개 메서드(private)가 있다는 것은 코드를 개선할 잠재적인 여지를 시사한다.

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 SelectWithCriteriaSql extends Sql {
  public SelectWithCriteriaSql(String table, Column[] columns, Criteria criteria)
  @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 클래스를 추상 클래스로 만들고 파생하는 클래스들을 만들어주었다. private 메서드도 파생 클래스 또한 파생 클래스로 옮겨주었다. InsertSql.valuesList 메서드가 그러하다. 또한 모든 파생 클래스가 사용하는 비공개 메서드는 Where와 ClumnList 유틸 클래스로 넣어주었다.

이는 SRP를 지원하며 객체 지향 설계의 핵심 원칙인 OCP도 지원한다.

 

변경으로부터 격리

구체적인 클래스와 추상 클래스가 있다. 구현에 의존하는 클라이언트 클래스는 구현이 바뀌면 위험이 빠지므로 인터페이스와 abstract 클래스를 사용해 구현이 미치는 영향을 격리시켜야 한다.

추상화를 통해 테스트가 가능할 정도로 시스템의 결합도를 낮춰 유연성과 재사용성도 더욱 높아지게 한다.

public interface StockExchange { 
  Money currentPrice(String symbol);
}
public class TokyoStockExchange implements StockExchange {
  @Override
  punlic Money currentPrice(String symbol) {
  ...
  }
{

자주 변경되는 TokyoStockExchange API를 사용해 개발을 한다면 StockExchange라는 인터페이스를 우선 만들어 준다.

그다음 TokyoStockExchgange 클래스를 구현한다.

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

Portfolio 생성자를 수정해 StockExchange 인터페이스를 참조자를 인수로 받아 만들어주어 TokyoStockExchange 클래스를 흉내 내는 테스트용 클래스를 만들 수 있다.

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

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

FixedStockExchangeStub 클래스가 고정된 주가를 반환하는 테스트용 클래스이다.

 

위와 같은 테스트가 가능할 정도로 시스템의 결합도를 낮추면 유연성재사용성더욱 높아진다.

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

 

이렇게 결합도를 최소로 줄이면 객체지향 설계 원칙인 DIP(Dependency Inversion Principle)을 따르는 클래스가 나온다. 구현이 아니라 추상화에 의존해야 한다는 원칙이다.

 

 

'Develop > 클린코드' 카테고리의 다른 글

[클린 코드] 12장 - 창발성  (0) 2023.09.27
[클린 코드] 11장 - 시스템  (0) 2023.09.27
[클린 코드] 9장 - 단위 테스트  (0) 2023.09.19
[클린 코드] 8장 - 경계  (0) 2023.09.19
[클린 코드] 4장 - 주석  (0) 2023.08.29