- Published on
10장. 클래스
10장. 클래스
클래스 체계
클래스를 작성할 때 우선 순위는 아래와 같습니다.
- 정적 공개 상수
- 정적 비공개 상수
- 비공개 인스턴스 변수
- 공개 인스턴스 변수: 필요한 경우는 거의 없음
- 공개 함수
- 비공개 함수는 자신을 호출하는 공개 함수 직후에 위치
즉, 추상화 단계가 순차적으로 내려가며 프로그램은 신문 기사처럼 읽힙니다.
캡슐화
변수와 유틸리티 함수는 가능한 공개하지 않는 편이 낫지만 반드시 숨겨야 한다는 법칙도 없습니다.
테스트를 작성하기 위해 protected
나 default
정도로 풀어주는 것도 괜찮습니다.
하지만 가능한 비공개 상태를 유지하려고 노력해야 하며 캡슐화를 풀어주는 결정은 언제나 최후의 수단입니다.
클래스는 작아야 한다!
클래스를 설계할 때도 함수와 마찬가지로 '작게'가 기본 규칙입니다.
단일 책임 원칙 (SRP)
SRP는 클래스나 모듈을 변경할 이유가 단 하나뿐이어야 한다는 원칙입니다.
public class SuperDashboard extends JFrame Implements MetaDataUser {
public Component getLastFousedComponent()
public void setLastFoused(Component lastFocused)
public int getMajorVersionNumber()
public int getMinorVersionNumber()
public int getBuildNumber()
}
5개 밖에 안되는 메소드를 가진 클래스지만 위 클래스는 아래 2가지 포인트가 있습니다.
- 소프트웨어 버전 정보를 추적
- 스윙 컴포넌트 관리
즉, 버전 정보와 스윙 코드를 변경하는 두 가지 경우가 있기 때문에 SRP를 위반하는 클래스 입니다.
이런 경우 버전 관리 클래스와 스윙 코드 관리 클래스로 분리하는 것이 좋습니다.
응집도
클래스는 인스턴스 변수 수가 작아야합니다.
각 클래스 메소드는 클래스 인스턴스 변수를 하나 이상 사용해야 하며, 메소드가 인스턴스 변수를 많이 사용할 수록 응집도가 높습니다.
모든 메소드가 모든 인스턴스 변수를 사용하면 응집도가 가장 높지만 이런 경우는 바람직하지 않습니다. 하지만 대체로 응집도가 높은 클래스가 좋습니다.
응집도가 높다는 말은 클래스에 속한 메소드와 변수가 서로 의존하며 논리적인 단위로 묶인다는 것을 의미합니다.
응집도를 유지하면 작은 클래스 여럿이 나온다
큰 함수를 작은 함수 여럿으로 나누기만 해도 클래스 수가 많아집니다.
큰 함수에서 작은 함수로 추출하는 중, 큰 함수에서 선언된 변수가 필요하다면 파라미터로 넘기기 보다 인스턴스 변수로 승격시키는 것이 좋습니다.
메소드의 인자 개수는 작을 수록 좋으니까요.
이렇게 하다보면 인스턴스 변수와 메소드가 늘어날 것이고, 특정 메소드에서만 사용하는 인스턴스 변수들이 존재합니다.
그럼 해당 인스턴스 변수와 메소드를 별도의 클래스로 추출해야 한다는 신호입니다.
변경하기 쉬운 클래스
대부분의 시스템은 지속적인 변경이 가해지고 뭔가 변경할 떄마다 시스템이 의도대로 동작하지 않을 위험이 따릅니다.
SQL 문자열을 만들어 주는 기능을 개발한다고 생각하면 아래처럼 클래스를 만들 것 입니다.
public class Sql {
public Sql(String table, column[] columns)
public String create()
public String insert(Object[] fields)
public String selectAll()
...
}
여기에 이제 update 문을 추가하려면 Sql 클래스에 손을 대야합니다.
또한 기존 로직을 수정할 떄도 반드시 Sql 클래스를 손대야합니다.
이렇게 Sql 클래스는 변경할 이유가 2가지이므로 SRP를 위반한다고 볼수 있습니다.
이런 경우 Sql을 추상클래스로 만들고 각 문자열 생성기 마다 상속 받아 구현 하도록 구조를 바꿀수 있습니다.
public abstract 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()
}
이 처럼 구현하면 각 클래스는 극도록 단순하기 때문에 코드를 이해하기 쉽습니다.
또한, 함수 하나를 수정한다고 다른 함수가 망가질 위험도 사라졌습니다.
클래스 자체를 분리 시켰기 때문에 각 기능을 테스트 하기도 더 쉬워졌습니다.
SRP와 OCP 원칙도 지원하게 됩니다.
OCP: 개방 폐쇄 원칙으로 변경은 불가능 하면서 확장에는 열려있도록 구현하는 원칙
위 로직 같은 경우 또 다른 SQL 문자열 생성기가 필요하면 Sql 클래스를 상속받아 구현하면 되므로 변경은 닫혀있고 확장에는 열려있다고 볼수 있습니다.
위 로직은 모든 장점만을 취한 형식이라고 볼 수 있습니다.
변경으로부터 격리
외부의 영향을 받는 코드는 테스트 코드를 짜기가 어렵습니다.
가령 5분 마다 값이 달라진다면 테스트 케이스는 실패하게 될테니까요.
이런 경우 인터페이스를 선언하고 테스트용 구현체를 따로 만들어 테스트 할 수 있습니다.
그럼 실제 코드와 로직이 다르니까 의미가 없지 않을까? 라고 생각 할 수 있지만
우리가 의존하는건 구현체가 아닌 인터페이스이기 때문에 다른 구현체로 변경이 되어도 기존 로직에 문제가 없어야 하는 것이 오히려 정상입니다.