Skip to content

Latest commit

 

History

History
232 lines (192 loc) · 12.1 KB

ch10_클래스.md

File metadata and controls

232 lines (192 loc) · 12.1 KB

표현에 아무리 집중해도, 시스템이 잘못되면 말짱꽝이다. 여기서 시스템은 클래스 구조라고 생각한다.

클래스를 잘 구성하도록 하자.

클래스 내부의 구성 순서

  • private 함수는, 해당 함수를 사용하는 public 함수 직후에(가까운 곳에) 위치
    • 이게 앞에서 말했던, ‘위에서 아래로 읽어 내려가면서 추상화 단계가 순차적으로 내려감’ 의 의미
  • 캡슐화를 잘 해야 한다.
    • 외부에서 내부를 상세하게 들여다 보지 못하도록 해야 한다.
    • 최대한 비공개 상태를 유지
    • 테스트를 위해 public 함수를 만드는 것은 당연히 절대 절대 안되고 ( 생성자라면 고려해 볼 만 함 - 물론 도메인 관점에서도 필요한 경우에만 ) ( 테스트 시나리오를 잘 작성하면 private 함수도 테스트 됨 )
      • protected 까지는 고려해 볼 만하다고 하는데 나는 잘 모르겠음 (protected 는 같은 패키지내 접근가능 - 다른 패키지의 하위클래스도 접근가능)

클래스는 작아야 한다 (SRP)

  • 책임 관점에서 , 클래스는 작아야 한다 → 맡은 책임이 하나 여야 한다.
    • 즉, “책임” 은 “크기” 와 관련되어 있다.
  • 클래스 이름 → “책임” 을 나타낸다.
    • 클래스 크기를 줄이기 위한 첫 관문 → 클래스 이름 작명
      • 애매한 이름만 떠오름, 간결한 이름이 떠오르지 않음 == 책임이 너무 많아서 결정 못하는 것.
      • ex) SuperDashBoard → Super요? ㅎ…. 굉장히 많은 책임을 갖고 있겠군요….ㅎ
  • SRP : 단일 책임 원칙 (객체지향 원칙 SOLID 중 S)
    • 하나의 책임 == 클래스나 모듈을 “변경할 이유가 단 하나” 여야 한다.
      • 개인적으로 “단 하나의 책임” 으로 생각하는 것은 상당히 어려운데, “변경할 이유” 들을 파악 하는 것이 추상화를 더 잘 할 수 있는 것 같다
  • “돌아가게 만드는 것” 과 “깨끗하게 만드는 활동” 은 별개!🥳
    • 이러한 관심사 분리작업 역시 매우 중요하다 (보통 돌아가게만 만들고 나면 더는 다음으로 넘어가지 않져)
    • 또 어떤 사람들은 이런식으로 단일 책임을 가진 클래스들을 만들다 보면 “클래스가 너무 많아져, 큰 그림 이해가어렵다” 라고들 말한다.
      • 그런데 , 이렇게 쪼개지 않아도 양이 많은 건 많은거다. 쪼개지 않으면 오히려 “문맥” 을 살펴보면서 “코드를 이해하는 시간이 길어” 질 뿐인 것 같다 ㅎㅎ;
    • 정리 해 두어야, 어디에 위치 하는지 찾기 쉬움.
      • 변경을 가할 때, 직접 영향이 미치는 컴포넌트들 만 이해해도 됨.
  • 여러 책임을 맡게 된다면
    • (외부와의 관계에서 보면 )변경을 가할 때, 영향을 미치는 컴포넌트도 많아지고
    • (내부적으로도) 당장 알 필요 없는 사실들까지 확인해 봐야 한다.
      • 하도 많은 메서드들, 속성들이 존재하니, 이 메서드를 변경했을 때 다른 기능에도 영향을 미치지는 않는지 확인해야함.
  • 작은 클래스 여러개로 이뤄진 시스템이 더 바람직!
  • 하나의 책임 만을 맡게 된다면
    • 그래야 공개 메서드 도 적고
      • 클라이언트는 내부 구현에 의존하지 않게 되고
    • 응집도도 높아지고
      • 클래스 내부 속성들이, 대부분의 인스턴스 메서드 들에서 사용된다
      • private 메서드가 여러 public 메서드에서 사용된다
      • 하나의 메서드 를 변경하는데 있어, 다른 메서드 에 영향을 끼칠 확률이 낮아진다.
    • 결합도는 낮아진다
      • 요소가 다른 요소로부터, 그리고 변경으로부터 잘 격리되어있다.
      • 각 요소를 이해하기도 더 쉬워진다.
    • 결과적으로 해당 클래스에 대한 재 사용성 도 높아진다

응집도, 변경하기 쉬울 것

  • 응집도 를 “클래스의 인스턴스 변수 의 개수”로 나타내고 있다.🥳
    • 클래스의 인스턴스 변수 개수작을 것.
    • 클래스의 인스턴스 메서드는, 인스턴스 변수를 하나 이상 사용할 것.
      • 많은 인스턴스 변수가, 해당 인스턴스 변수를 사용할 수록 메서드와 클래스의 응집도가 높다고 볼 수 있다.
      • 클래스에 속한 메서드와 변수가 서로 의존하며 논리적인 단위로 묶이는 것.
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;
	}
}
  • 앞에서 메서드의 인자 개수가 늘어날 때 이를 객체로 리팩토링 했음 → 결과적으로 인자들을 인스턴스 변수로 갖고 있었음
    • 큰 함수 → 작은 함수로 쪼개려 보니, 큰 함수에 존재하는 지역 변수들이 사용됨. 작은 함수가 인자를 여러개 받아야함 → 그냥 인스턴스 변수로 갖도록 하자 → 그랬더니….
    • —> 이런식으로 “함수를 작게, 매개변수 목록을 짧게” 규칙을 따르다보면, “몇몇 메서드만이 사용하는 인스턴스 변수” 의 수가 매우 많아진다
      • 이는 “새로운 클래스를 쪼개야 하는 신호”
  • private 메서드가 여러 public 메서드에서 사용된다
    • 클래스 일부에서만 사용되는 private 메서드 → 이 역시 후에 책임 분리에서 쪼개질 확률이 높다.
  • 변경하기 쉬울 것 - 요구사항은 변경되기 마련이며, 코드 변경도 어쩔수 없다. 하지만 여기서 코드를 변경할 때의 위험정도는 우리가 줄일 수 있을 것이다.
    • 클래스를 분리함으로서, 함수 하나 or 인스턴스 변수 하나를 수정 했을 때 이와 관련 없던 함수가 변경될 위험은 사라졌다.
      • (참고)응집도가 높으니 얘랑 관련 있는 애들에는 영향이 가는게 당연하다.
    • 새 기능을 추가하거나, 기존의 기능을 변경할 때, 건드릴 코드가 최소인 시스템 구조가 바람직하다.
      • OCP : 확장에는 열려있고, 변경에는 닫겨 있어야 한다.
    • 상세한 구현에 의존해선 안된다.
      • 변경에 취약하다.
      • 테스트 코드 작성도 어렵다.
        • ex) 외부 API 에 의존하는 경우는 상세한 구현에 의존하는 경우로 볼 수 있다
      • 따라서 추상체를 만들어 사용하자 (DIP)
        • ex) 외부 API 호출을 감싸서 인터페이스로 만들어버리자 → 테스트에서는 인터페이스를 사용하도록 하자

예를들어 다음과 같이 클래스를 분리할 수 있다.

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

위의 코드에 대해 Update 기능 추가, 기존 select 기능 변경 이라는 두 가지 관점에서 변경 이유가 2개이니 SRP 위반한다고 말 하고 있음 ( 이 부분은 조금 많이 이상적 이어 보이기도 하고, Update, select 라는 기능 자체가 서로 다른 기능이니 그럴듯해 보이기도 하고 )

// 추상클래스 
abstract public class Sql {
		public Sql(String table, Column[] columns) 
		abstract public String generate();
	}

// 각 기능(select, insert) 은 파생클래스로 만들고
	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)
	}

// criteria 를 사용하는 클래스를 따로 분리해, 이를 인스턴스 변수로 갖고 있도록 하였다
	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()
	}이 

이 경우 Update 기능이 추가적으로 필요해지면, 추상 클래스를 추가로 선언하면 될 뿐이다.

select 기능을 수정하기 위해 Select 클래스만을 수정하면 된다. 이로 인해 prepared, find 기능이 망가질 위험이 사라졌다. 즉 함수 하나를 수정한다고 다른 함수가 망가질 위험도 사라졌다.

변경에 닫혀있고 확장에 열려있는 구조다. (OCP)

또한 이 예시 처럼 외부 API 에 의존하는 부분을 인터페이스로 만들고, 테스트 스텁을 만들어 다음과 같이 테스트를 용이하게 할 수 있다.

public interface StockExchange { 
	Money currentPrice(String symbol);
}
public Portfolio {
	private StockExchange exchange;
	public Portfolio(StockExchange exchange) {
		this.exchange = exchange; 
	}
	// ... 
}
public class PortfolioTest {
	private FixedStockExchangeStub exchange; // 테스트 스텁 구현체의 네이밍도 매우 Good 이다
	private Portfolio portfolio;
	
	@Before
	protected void setUp() throws Exception {
		exchange = new FixedStockExchangeStub(); 
		exchange.fix("MSFT", 100); // 테스트 자체에서, 이 stub 이 고정된 100 달러 값을 줄 것임을 알고 있는 것도 가능하다
		portfolio = new Portfolio(exchange);
	}

	@Test
	public void GivenFiveMSFTTotalShouldBe500() throws Exception {
		portfolio.add(5, "MSFT");
		Assert.assertEquals(500, portfolio.value()); 
	}
}

추상체 에 의존하도록 하였으니 , 깨알 DIP 다. DIP 를 통해 우리는 추상체에 의존하도록 코드를 작성해 결합도를 낮출 수 있음이 특징이다.

program 자체는 길어질 것이다

클린하게 리팩토링하다보면

  • 좀 더 길고 서술적인 변수 이름, 클래스 이름을 사용하며
  • 주석 대신, 함수 선언과 클래스 선언을 사용한다고 볼 수 있다.
  • 가독성을 위해 코드 스타일, 공백 등을 추가하며 형식을 맞춘다

이러다 보면 코드가 길어질 수 밖에 없다.

리팩토링을 위해서는 테스트를 작성해 야 한다

  • 기존과 동일한 동작 원리를 가지는지 확인할 테스트 코드를 미리 작성해 두자.
  • 그 다음에 코드를 조금씩 변경하며, 테스트를 돌리도록 하자
    • 지금 TDD 강의에서는 최대한 이런 방식으로 코드 리팩토링을 하려고 노력하고 있다.