- 3.1 클래스 단위로 잘 동작하도록 설계하기
- 3.2 성숙한 클래스로 성장시키는 설계 기법
- 3.3 악마 퇴치 효과 검토하기
- 3.4 프로그램 구조의 문제 해결에 도움을 주는 디자인 패턴
- 클래스 단위로도 잘 동작하게 설계해야 한다. 가 핵심.
- 클래스 그 자체로도 잘 동작할 수 있게 설계해야한다.
- 클래스의 구성 요소
- 인스턴스 변수
- 메서드
- 잘 만들어진 클래스의 구성 요소
- 인스턴스 변수
- 인스턴스 변수에 잘못된 값이 할당됮 않게 막고, 정상적으로 조작하는 메서드
- 데이터와 로직이 따로 있으면 연관성을 알아채기 어렵다.
- 코드가 중복될 수 있다.
- 중복 코드는 수정하다가 다른 코드의 수정을 놓칠 수도 있다.
- 한 곳에 있지 않으므로, 가독성을 낮춘다.
- 클래스 스스로 자기 방어 임무를 수행해야 소프트웨어 품질을 높일 수 있다.
public class Money {
int amount;
Currency currency;
public Money(final int amount, final Currency currency) {
if (amount < 0) {
throw new IllegalArgumentException("금액은 0 이상의 값을 지정해주세요.");
}
if (Objects.isNull(currency)) {
throw new IllegalArgumentException("통화 단위를 지정해주세요.");
}
this.amount = amount;
this.currency = currency;
}
}
- 잘못된 값이 유입되지 못하게 유효성 검사(
validation
)을 생성자 내부에 정의한다.
- 위 코드의 생성자처럼 처리 범위를 벗어나는 조건을 메서드 가장 앞부분에서 확인하는 코드를 '가드'라고 한다.
- 생성자에 가드를 배치 시, 항상 안전하고 정상적인 인스턴스만 존재하게 된다.
- 데이터와 데이터를 조작하는 로직이 분리되어 있는 구조는 '응집도가 낮은 구조'이다.
- 데이터와 데이터 조작 로직을 모아서, 클래스를 성숙하게 만들자.
public class Money {
int amount;
Currency currency;
public Money(final int amount, final Currency currency) {
if (amount < 0) {
throw new IllegalArgumentException("금액은 0 이상의 값을 지정해주세요.");
}
if (Objects.isNull(currency)) {
throw new IllegalArgumentException("통화 단위를 지정해주세요.");
}
this.amount = amount;
this.currency = currency;
}
// 추가
public void addAmount(final int other) {
this.amount += other;
}
public int getAmount() {
return this.amount;
}
}
- 변수의 값이 계속 바뀌면, 값이 언제 변경되었는지, 지금 값은 무엇인지 계속 신경 써야 한다.
- 비즈니스 요구 사항이 바뀌어 코드 수정하다가 의도치 않은 값을 할당하는 예상치 못한 사이드 이펙트(4.2.3에서 설명) 발생 가능.
public class Money {
final int amount;
final Currency currency;
public Money(final int amount, final Currency currency) {
// 생략
this.amount = amount;
this.currency = currency;
}
}
- final을 이용하여 인스턴스 변수를 불변으로 만들자.
- 이렇게 하면 변수 선언 시점 또는 생성자 안에서만 값을 할당할 수 있게되고 더 이상의 재할당은 불가능.
- 불변이어도 변경 가능한 방법이 있다. 👉 새로운 인스턴스 만들기
public class Money {
public Money(final int amount, final Currency currency) {
// 가드 생략
this.amount = amount;
this.currency = currency;
}
// 생략
Money add(int other) {
int added = this.amount + other;
return new Money(this.amount, this.currency);
}
}
- 이렇게하면 값을 유지하면서도 불변으로 만들 수 있다!
public class Money {
// ...
Money add(final int other) {
final int added = this.amount + other;
return new Money(added, this.currency);
}
}
- 값이 중간에 바뀌면 값의 변화를 추적하기 힘들기 때문에 버그 발생 가능성이 높다.
- 기본적으로 지역변수, 매개변수는 불변으로 만드는 것이 좋음.
- 내부에서 값 변경 시 컴파일러가 에러를 뱉어준다.
// 문제의 코드
final int ticketCount = 3;
money.
add(ticketCount); // 돈을 넣어야 하는데, 티켓 수를 넣어버렸다!!
- 설마 이러한 일이 일어나겠어? 라고 하지만, 애플리케이션 규모가 커지면서 조금만 부주의해도 발생할 수 있다!
- 더 큰 문제는 값을 감싸지 않으면 위 같은 오류가 발생해도 알아차기가 어렵다는 것!
// Money 자료형만 받도록 메서드 수정하기
public class Money {
// ...
Money add(final Money other) {
final int added = this.amount + other.amount;
return new Money(added, this.currency);
}
}
- 독자적인 자료형을 사용하여 실수를 방지하도록 하자.
// add 메서드 추가로 개선하기 - 통화 단위가 다른 Money가 주어지면 예외 발생.
public class Money {
// ...
Money add(final Money other) {
if (!this.currency.equals(other.currency)) {
throw new IllegalArgumentException("통화 단위가 다릅니다.")
}
final int added = this.amount + other.amount;
return new Money(added, this.currency);
}
}
// 금액을 곱하는 메서드가 의미 있을까?
public class Money {
// ...
Money multiply(final Money other) {
if (!this.currency.equals(other.currency)) {
throw new IllegalArgumentException("통화 단위가 다릅니다.")
}
final int multiplied = this.amnount * other.amount;
return new Money(multiplied, this.currency);
}
}
- 합계 금액 계산 시에는 가산, 할인 시에는 감산, 비율을 구할 시에는 나눗셈을 사용한다.
- 금액을 곱하는 일은 일반적이 회계에서 사용 X
- 시스템 사양에 불필요한 메서드를 '선의'로 추가해두면 다른 개발자가 무심코 사용했을 때 버그 발생 가능.
- 꼭 필요한 메서드만 정의하자!
public class Money {
final int amount;
final Currency currency;
Money(final int amount, final Currency currency) {
if (amount < 0) {
throw new IllegalArgumentException("금액은 0 이상의 값을 지정해주세요.");
}
if (Objects.isNull(currency)) {
throw new IllegalArgumentException("통화 단위를 지정해주세요.");
}
this.amount = amount;
this.currency = currency;
}
Money add(final Money other) {
if (!currency.equals(other.currency)) {
throw new IllegalArgumentException("통화 단위가 다릅니다.");
}
final int added = this.amount + other.amount;
return new Money(added, this.currency);
}
}
(관련 로직을 응집해서 코드 수정 시 버그 발생이 어려워진 Money 클래스)
- 중복 코드 - 필요한 로직이 Money 내부에 모여 있음. 👉 다른 클래스에 중복 코드가 작성될 일이 줄어든다.
- 수정 누락 - 응집도를 높임으로써 중복 코드가 발생하지 않게되므로 코드 수정 시 누락할 일이 줄어듦.
- 가독성 저하 - 필요한 로직이 Money 내부 안에 모여 있으므로, 디버깅 또는 기능 변경 시 관련된 로직을 찾으로 돌아다닐 필요가 없음. 즉 가독성이 증가함.
- 쓰레기 객체 - 생성자에서 인스턴스 변수의 값을 확정하므로 👉 초기화 되지 않은 상태가 있을 수 있음.
- 잘못된 값 - 가드 설치, 인스턴스 변수를
final
로 선언 👉 잘못된 값이 들어오지 않음 - 생각하지 못한 부수 효과 👉 final을 붙여 불변으로 만들었으므로 부수 효과로부터 안전함.
- 값 전달 실수 👉 매개변수를 래핑하여 Money 타입으로 만들었으므로, 다른 타입을 실수로 넣을 때 컴파일 에러를 발생.
- 클래스 설계란 인스턴스 변수가 잘못된 상태에 빠지지 않게 하기 위한 구조를 만드는 것. 이라 해도 과언은 아님.
- 앞의 Money 클래스처럼 로직이 한 곳에 모여있는 구조를 응집도가 높다고 함.
- 데이터와 데이터를 조작하는 로직을 하나의 클래스로 묶고 필요한 절차(=메서드)만 외부에 공개하는 것을 캡슐화라고함.
- 디자인 패턴: 응집도가 높은 구조로 만들거나, 잘못된 상태로부터 프로그램을 방어하는 등의 프로그램의 구조를 개선하는 설계 방법
디자인 패턴 | 효과 |
---|---|
완전 생성자 | 잘못된 상태로부터 보호함. |
값 객체 | 특정한 값과 관련된 로직의 응집도를 높힘. |
전략(strategy) | 조건 분기를 줄이고, 로직을 단순화함 |
정책(policy) | 조건 분기를 단순화하고, 더 자유롭게 만듦. |
일급 컬렉션(First Class Collection) | 값 객체의 일종으로 컬렉션과 관련된 로직의 응집도를 높임. |
스프라우트 클래스 | 기존 로직을 변경하지 않고, 안전하게 새로운 기능을 추가함 |
- 매개변수 없는 디폴트 생성자로 객체를 생성하고, 이후에 인스턴스 변수를 설정하는 방법
- 👉 인스턴스 변수를 초기화하지 않을 가능성이 존재.
- 👉 인스턴스가 존재하는 쓰레기 객체가 만들어진다.
- 👉 인스턴스 변수를 초기화하지 않을 가능성이 존재.
- 인스턴스 변수를 모두 초기화해야지만 객체를 생성할 수 있는 매개변수를 가진 생성자를 만들자.
- 생성자 내부에서는 가드를 이용하여 잘못된 값이 들어오지 않게 막는다.
- 추가적으로 인스턴스 변수에
final
키워드를 붙여 불변으로 만들면 객체 생성 후에도 잘못된 상태로부터 방어 가능.