대범하게

[클린코드] 20일차 - 변경하기 쉬운 클래스 == SRP, OCP 준수 본문

알아두면쓸데있는신기한잡학사전/독서일지

[클린코드] 20일차 - 변경하기 쉬운 클래스 == SRP, OCP 준수

대범하게 2023. 11. 16. 00:58
반응형

[클린코드] 20일차 - 변경하기 쉬운 클래스 == SRP, OCP 준수

클린코드 20일차 (p. 179~188(10장) )

클린코드 21일차 (p. 189~191(10장) / 192~197(11장) )

10장 클래스

(복습)

1. 클래스 체계

2. 클래스는 작아야 한다!


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문을 수정해야 하는 경우

 

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

*다시 언급하자면 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 클래스에서 파생하는 클래스로 수정되었다.

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

 

 

수정 후의 코드 효과

 

함수 하나를 수정했다고 다른 함수가 망가질 위험도 사라졌다.

클래스가 서로 분리되어 논리를 증명하기 쉬워졌다.

새로운 SQL문이 추가되는 경우에도 기존 클래스를 변경할 필요가 없었졌다. → SRP, OCP를 준수한다.

*OCP(Open-Closed Principle)는 클래스는 확장에 개방적이고 수정에 폐쇄적이야 한다는 원칙이다.

 

4. 클래스를 변경으로부터 격리하자.

"구체적인 클래스는 상세한 구현(코드)를 포함 / 추상 클래스는 개념만 포함"

 

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에 주입시키는 식으로 코드를 작성하면된다.

개선한 Porfolio 클래스는 TokyoStockExchange라는 상세한 구현 클래스가 아니라 StockExchange 인터페이스에 의존한다.

 

테스트 코드가 TokyoStockExchange 값에 따른 시세 영향을 받을 필요가 없어졌다. 

테스트용으로 고정된 값을 리턴해주는 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());
    }
}

 

시스템의 결합도를 낮추면 유연성과 재사용성도 더욱 높아진다. 

 

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

Comments