지금까지는 코드 라인을 어떻게 잘 쓸 수 있는지에 포커스를 두었다. 이번 챕터에서는 더 상위 레벨인 클래스를 잘 작성하는 방법에 대해 살펴보자!

Java에서의 클래스 표준 관례를 살펴보자
앞서 클래스 혹은 메서드의 내부는 숨겨야한다고 했다. 하지만, public variable을 가지면 좋은 이유가 드물게 있다.
이것은 step-down rule을 따르고, 뉴스 기사처럼 프로그램을 잘 읽을 수 있게 도와주기도 한다
아래 코드를 보면 알 수 있듯이 바로 static을 통해 public variable으로서 사용할 때이다!
public class Car {
//pubic static constants
final public static int MAX_SPEED = 200;
//private static variables
private static int numberOfCars = 0;
//priavte instance variables
private int year;
private String owner;
//public functions
public Car(int year, String owner) {...}
public static int getNumberOfCars() { return numberOfCars;}
public int getYear() { return year; }
public String getOwner() { return owner; }
public void drive() { ...}
...
//private functions
private void changeGears(int gear) {...}
private int checkGasCondition() {...}
자바에서 static 키워드는 클래스 레벨에서 동작하는 요소를 정의할 때 사용되며, 인스턴스에 귀속되지 않고 클래스 자체에 속하는 것을 의미한다
이를 통해 메모리 효율성을 높이고, 특정 상황에서 코드의 간결성을 유지할 수 있다.
static의 주요 역할과 특징은 다음과 같ㅋ다

우리는 변수와 활용할 기능을 private하게 하고 싶다
위에서 언급했듯이, 테스트를 하기 위해서 protected or package로 선언하기도 한다
하지만, 우리는 우선 privacy를 유지할 수 있는 방법을 살펴볼 것이다. 캡슐화를 느슨하게 하는 것은 언제나 마지막의 일!
항상 얘기해왔지만! 클래스는 작아야 한다. 생각한 것보다 더 작아야 한다
얼만큼?
이 역할은 무엇을 의미할까?

이러한 경우.. 책임이 굉장히 많은 것이다
누가봐도 그렇지만..^^
70개가 넘는 public method로 클래스가 구성되어있는 이 코드의 문제점은 무엇일까?
그렇다면 클래스가 비교적 작을 때를 살펴보자

이 클래스는 GUR 얘기와 버전과 관련된 얘기로, 두 가지의 역할을 하고 있다!
이런 경우, 하나를 다른 클래스로 뽑아낸 다음에 필요시 new로 객체를 생성해서 사용하면 된다.
클래스의 이름은 반드시 그것이 어떤 역할을 하고 있는지가 드러나도록 작명되어야 한다
만약 클래스가 하고 있는 것에 대하여 클래스명을 간결하게 작성할 수 없다면, 클래스가 너무 크다는 것을 반증한다
클래스명이 모호해질수록 클래스가 너무 많은 역할을 가진다는 것을 말해준다. 예를 들어, recoveryManager가 아니라 Processor or Manager이라면 너무 큰 범위를 다루고 있는 클래스인 셈이다
또한, 클래스에 대해서 설명을 할 때 'if', 'and', 'or', 'but'이라는 단어들 없이 25자 안으로 설명이 가능해야 한다
클래스나 모듈은 하나만 바꿀 의도를 가지고 있어야 한다
public class SuperDashBoard extends JFrame implements MetaDataUser {
public Component getLastFocusedComponent()
public void setLastFocused(Component lastFocused)
public int getMajorVersionNumber()
public int getMinorVersionNumber()
public int getBuildNumber()
}
SuperDashBoard 클래스도 두 가지 역할을 하고 있기에 바뀌어야 하는 것!
바꾸면 아래와 같다
public class SuperDashboard extends JFrame implements MetaDataUser {
public Component getLastFocusedComponent()
public void setLastFocused(Component lastFocused)
}
public class Version {
public int getMajorVersionNumber()
public int getMinorVersionNumber()
public int getBuildNumber()
}
이제 하나의 역할만 가진 클래스가 된것!
SRP는 OO design에서 가장 중요한 개념중 하나이다
비교를 해보자면..

클래스가 커지면 내부고주도 복잡해질 수밖에 없다
그러면 전체를 이해하기 어려워지고,
유지보수가 어려워지고,
테스트 측면에서도 어려워지는 것!
클래스는 응집도를 가지고 있어야 한다
응집도 큰 것이 좋은 것이고, 응집도가 크냐, 작냐에 따라 클래스가 크다, 작다라고 말할 수 있다
그렇다면 언제 응집도가 크다고 말할 수 있는 것일까?
메소드가 클래스의 변수를 다 접근하고 있다면 응집도가 높은 것이다! 각각의 메서드를 보고 메서드가 클래스의 변수를 접근하고 있는가를 살펴보고, 접근하는 개수가 적다면 응집다고 낮다고 이야기한다
예시 코드를 살펴보자

Stack을 구현하고 있는 클래스이기에, 어떤 스택이든 공통적으로 제공해야 하는 기능 세가지를 메서드로 구현한 상태이다
size() 메서드만이 선언된 두 변수를 모두 사용하는 것을 실패했다. 하지만 떼어내기 어려운 메서드이기 때문에 응집도가 높다고 이야기 할 수 있다

자신이 접근하는 변수들로 구성된 클래스로 분리를 해서 응집도를 높일 수 있다
정리를 하자면, 클래스가 작은지 큰지는 역할 관점과, 응집도 관점에서 나누어 볼 수 있는 것!
"우리는 큰 함수를 작은 함수들로 나누어야 한다"는 점을 상기시키고 있다.작은 함수로 나누는 것은 코드의 가독성과 유지보수성을 높이는 데 도움을 준다.
큰 함수를 작은 함수들로 나누면, 클래스의 응집도가 낮아질 수 있다고 경고한다. 이는 작은 함수들이 클래스의 모든 인스턴스 변수(instance variables)를 사용하지 않을 경우 발생한다.
클래스가 응집도를 잃게 되면, 관련된 메서드들을 기준으로 클래스를 나누는 것이 필요하다.이렇게 하면 각 클래스의 응집도가 높아지고, 설계가 더 명확하고 깔끔해질 수 있다.
클래스를 나누면 결과적으로 더 많은 작은 클래스들이 생성된다. 하지만 이는 프로그램을 더 잘 조직화하게 해준다!
또 다른 예시를 살펴보면...

Editor에서 너무 많은 일들이 수행되고 있다. Editor라는 클래스명에 맞는 메서드로만 클래스를 구성하는 것이 중요하다
우리는 클래스가 변화에 잘 적응할 수 있도록 구성을 해야 한다
구조를 잘 설계한다면 어떤 새로운 기능을 넣을 때 기존의 코드를 건드리지 않고 새로운 클래스를 추가하는 것이 가능해진다!
how? -> 상속을 이용하면 된다!
OCP는 확장에는 open하고 수정에는 close한 태도를 가져야 한다는 원리다
즉, 기능을 추가할 때 기존의 코드를 건드리지 않은채 추가하여 확장하자는 것!
class C {
void f();
}
class C' extends C {
void g() ..
}
이렇게 하면 C를 수정하지 않으면서 g()를 추가할 수 있다!

또 따른 예시를 살펴보자
public class Sql {
public Sql(String table, Column[] columns) // select ~~ from ~~ where ~~
public String create() // create table ___ ~~
public String insert(Object[] fields)
public String selectAll() // select * from ~
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, delete와 같은 새로운 기능을 수정하려면 기존의 class를 들어갈 수밖에 없고, public method를 추가할 수밖에 없다. 이것은 위 그림에서의 usual way에 해당하는 것!
이 구조의 문제는, 이렇게 메서드를 클래스에 추가를 하면 전체를 다 테스트 돌려야 한다.
이처럼 새로운 기능을 추가할 때 OCP의 기존 코드는 수정하지 않고 확장해야한다는 원리에 어긋나고, 또 하나의 역할만 가지고 있어야 한다는 SRP의 원리도 어긋나게 되는 형태.
▶︎ Refactored Code
abstract public class Sql { // 앞으로 나를 상속받는 애들은 이 틀을 제거해야~ 라고 하는 템플릿과 같은 개념 (abstract)
public Sql(String table, Column[] columns)
abstract public String generate(); // 하나라도 abstract 메서드가 있으면 그 클래스는 abstract 클래스
// 추상메서드: interface만 정의되어 있고 body(본문)는 비어있는 메서드
}
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 { // 필요할 때만 가져와서 new로 만들어 쓸 때
public Where(String criteria)
public String generate()
}
public class UpdateSql extends Sql { // 이 경우 OCP를 만족한다고 봄!
@Override
public String generate() {
...
}
}
public class ColumnList {
public ColumnList(Column[] columns)
public String generate()
}
이렇게 상속을 받아서 작성을 하면, 필요한 애만 가져와서 new로 만들면 된다!
s = new CreateSql();
s = new SelectSql();
s.generate();
또 하나 봐야할 것이, Where과 ColumnList 클래스가 분리되었는데, 이는 여러군데서 많이 쓰이기 때문에 중복을 피하기 위해서 떼어놓은 것이다!
public abstract class Car {
private string type;
public abstract void drive();
}
public class Mini extends Car {
public void drive() {
//mini-specific drive code
}
}
만약 추상 클래스를 객체로 만들면 컴파일 안됨 - 본체가 없기 때문!
ex. Sql s = new Sql();
클래스는 concrete 클래스가 아닌, abstract 클래스에 의존해야 한다
만약 concrete class이면 구현 디테일이 바뀌면 다 바뀌어야 하는데, 추상 클래스는 필요할 때마다 갈아낄 수 있다

이 경우 myCar는 Car 타입으로 선언되었기 때문에, Car 클래스 또는 Car 클래스에 선언된 메서드만 호출할 수 있다
다른 예시와 함께 살펴보자
public class Portfolio {
private TokyoStockExchange exchange = new TokyoStockExchange();
public Money value() {
Money total = new Money(0);
for (String item : itemList) { // 주식 리스트
total.add(exchange.currentPrice(item));
}
return total;
}
}
지금 코드의 문제는 Portfolio 클래스 내부에서 지역마다 객체를 생성해주고 있어, 코드를 수정하려면 매우 번거로워진다
이를 해결하기 위해서 StockExchange라는 abstract class를 만들고, 틀을 만들어주기 위해 abstract currentPrice()를 만들어준다
지역마다 이 StockExchange을 상속받아서 사용하면 되기 때문에 매우 편리해짐!
public interface StockExchange {
Money currentPrice(String symbol);
}
public class TokyoStockExchange implements StockExchange {
public Money currentPrice(String symbol) { //Tokyo-specific code
}
public class NewYorkStockExchange implements StockExchange {
public Money currentPrice(String symbol) { //Tokyo-specific code
}
public class HongKongStockExchange implements StockExchange {
public Money currentPrice(String symbol) { //Tokyo-specific code
}
public class Portfolio {
private StockExchange exchange; // 추상화에 의존
public Portfolio(StockExchange exchange) {
this.exchange = exchange;
}
public Money value() {
Money total = new Money(0);
for (String item : itemList) { // 주식 리스트
total.add(exchange.currentPrice(item));
}
return total;
}
}
이처럼 매개변수로 처리하면 된다!!!

그럼 이런 식으로 객체 생성 후 사용이 가능해진다!
프로그램의 정확성을 변경하지 않고, 부모 클래스의 객체를 자식 클래스의 객체로 대체할 수 있어야 한다는 원칙
또 다른 이름으로 behavioral subtyping이라 불린다

square은 모든 변의 길이가 같아야 하는데, 만약 직사각형이 부모 클래스로 되어있으면 충돌 발생
클라이언트는 사용하지 않는 메서드에 의존하면 안 된다는 원칙
하나의 일반적인 큰 인터페이스보다, 작고 구체적인 인터페이스를 여러 개 만드는 것이 더 낫다고 주장
EconomicPrinter는 Printer 인터페이스를 구현하지만, fax()와 scan() 기능을 지원하지 않으므로, 해당 메서드에서 예외를 던져야 한다
이는 클라이언트가 불필요한 메서드에 의존하도록 만들어 ISP를 위반