CH03: 클래스 설계
CHAPTER 3 : 클래스 설계
단계 : 실전
제목 : 클래스 설계, 모든 것과 연결되는 설계 기반
내용 : 이 책 전체의 기반이 되는 클래스와 객체 지향 설계의 기초를 다룹니다.
P.22
- 클래스 기반이란 “데이터”와 “그 데이터를 조작하는 논리”를 클래스라는 기본 단위로 묶어서 정의해가며, 프로그램을 작성하는 방법입니다.
- 이 장에서는 객체 지향 설계의 기본이라고 할 수 있는 클래스 설계의 기본을 설명합니다. 객체 지향 프로그래밍에서 클래스보다 작은 단위로는 조건 분기와 메서드 등이 있습니다.
- 클래스 설계가 잘 잡혀 있어야, 유지보수와 변경이 쉬운 코드를 만들 수 있습니다.
- 이 장에서는 데이터 클래스를 예로 악마 퇴치의 기본이 되는 클래스 설계 방법을 설명합니다. 데이터 클래스 속에 숨어있는 악마들을 하나하나 퇴치해서 우아하고 성숙한 클래스를 만드는 방법을 살펴봅시다.
3.1 클래스 단위로 잘 동작하도록 설계하기
- 클래스는 단독으로도 안전하게 작동할 수 있도록 설계해야 한다.
- 외부에서 복잡한 초기 설정 없이 사용할 수 있어야 하며, 내부 상태가 쉽게 망가지지 않도록 제한된 조작만 허용해야 한다.
3.1.1 클래스 구성 요소
좋은 클래스의 구성
- 인스턴스 변수
- 해당 변수를 안전하게 조작하는 메서드
데이터 클래스의 문제점
- 인스턴스 변수만 존재하고, 로직은 다른 클래스에 분산됨
→ 관련성 파악 어려움, 코드 중복, 초기화 누락 등 발생 - 초기화와 유효성 검사까지 외부 클래스에 의존
→ 잘못된 값 유입 가능, 자기 방어 능력이 없음
핵심 원칙
- 클래스는 자신의 상태를 스스로 보호할 수 있어야 하며, 유효하지 않은 상태를 방지하는 책임을 가져야 한다.
3.1.2 모든 클래스가 갖추어야 하는 자기 방어 임무
- 클래스, 메서드, 모듈 등은 각각이 독립적으로 신뢰할 수 있는 품질을 갖춰야 한다.
- 외부 도움 없이는 사용할 수 없는 클래스는 미성숙한 설계다.
- 모든 클래스는 초기화, 유효성 검사, 상태 보호 기능을 스스로 갖춰야 한다.
3.2 성숙한 클래스로 성장시키는 설계 기법
이 장에서는 금액을 나타내는 클래스를 예시로 설계 방법을 설명한다.
import java.util.Currency
class Money {
int amount;
Currency currency;
}
3.2.1 생성자로 확실하게 정상적인 값 설정하기
데이터 클래스와 초기화 문제
- 데이터 클래스는 디폴트 생성자 이후 인스턴스 변수에 값을 따로 할당해 초기화한다.
- 이 방식은 객체가 ‘초기화되지 않 상태’로 존재할 수 있게 하며, 이를 로우 데이터 객체(row data object) 라고 한다
- 로우 데이터 객체를 방지하려면 생성 시점에 인스턴스 변수가 모두 유효한 값으로 초기화되도록 해야 한다.
생성자에서 초기화 구현
- 인스턴스 변수를 모두 초기화하는 생성자를 구현하면, 객체 생성과 동시에 필드가 초기화된다.
- 예시:
Money
클래스는amount
와currency
를 매개변수로 받아 초기화하도록 만든다.
생성자에서 유효성 검사 추가
class Money {
int amount;
Currency currency;
Money(int amount, Currency currency) {
this.amount = amount;
this.currency = currency;
}
}
- 단순 초기화만으로는 부족하며, 매개변수의 유효성도 함께 검사해야 한다.
- 생성자 내부에서 조건에 따라 예외를 발생시켜 잘못된 값의 유입을 차단한다.
가드(guard)의 개념과 장점
class Money {
int amount;
Currency currency;
Money(int amount, Currency currency) {
if (amount < 0) {
throw new IllegalArgumentException("금액은 0 이상의 값을 지정해주세요.");
}
if (currency == null) {
throw new NullPointerException("통화 단위를 지정해주세요");
}
this.amount = amount;
this.currency = currency;
}
}
- 메서드 시작 부분에서 유효하지 않은 조건을 먼저 검사해 즉시 반환하거나 예외를 던지는 방식을 가드(guard) 라고 한다.
- 가드를 사용하면 불필요한 처리를 줄이고, 이후 로직을 단순하고 명확하게 유지할 수 있다.
- 생성자에서 가드를 활용하면 잘못된 인스턴스가 생성되지 않도록 보장할 수 있으며, 프로그램 전체의 안정성이 높아진다.
3.2.2 계산 로직도 데이터를 가진 쪽에 구현하기
- 데이터를 다루는 로직이 다른 클래스에 있다면 응집도가 낮은 구조가 된다.
- 응집도가 낮으면 코드 중복, 가독성 저하, 유지보수 어려움 등의 문제가 발생한다.
- 데이터를 가진 클래스에 로직도 함께 넣어야 클래스가 자립적이고 성숙한 구조가 된다
class Money{ ... void add(int other){ amount += other; } }
3.2.3 불변 변수로 만들어서 예상하지 못한 동작 막기
- 값이 계속 변하면 상태를 추적하기 어렵고, 부수 효과가 발생하기 쉽다.
- 인스턴스 변수에
final
을 붙여 한 번만 값을 할당하게 만들면 불변이 보장된다. - 생성자에 유효성 검사(가드)를 함께 적용하면 안정성이 더욱 높아진다
class Money{ ... Money (int amount, Currency currency) { this.amount = amount; this.currency = currency; } }
3.2.4 변경하고 싶다면 새로운 인스턴스 만들기
- 불변 객체로 만들면 값을 수정하지 않고 새로운 인스턴스를 생성해 변경을 반영할 수 있다.
- 기존 인스턴스는 그대로 유지되므로 상태 추적이 쉽고 안정적이다.
class Money { ... Money add(int other) { int added = amount + other; return new Money(added, currency); } }
이렇게 하면 불변을 유지하면서도 값을 변경할 수 있습니다
3.2.5 메서드 매개변수와 지역 변수도 불변으로 만들기
- 매개변수나 지역 변수의 값이 변경되면 로직 추적이 어려워지고 버그가 발생하기 쉽다.
final
을 붙이면 해당 값이 변경될 수 없게 되어 의도치 않은 수정을 방지할 수 있다.void doSomething(final int value) { value = 100; // compile error }
- 메서드 구조가 안전해지도록 매개변수에 Final을 붙입시다.
class Money { ... Money add(final int other) { int added = amount + other; return new Money(added, currency); } }
3.2.6 엉뚱한 값을 전달하지 않도록 하기
“잘못된 값의 전달” 이라는 부수 효과가 아직 남아있다.
// 3.14 금액을 의미하지 않는 값을 전달하는 경우
final int ticketCount = 3;
money.add(ticketCount);
가격이 아니라, 티켓의 수를 나타내는 숫자를 더해 버렸다. 버그이다. 이처럼 엉뚱한 값이 전달되지 않도록 하려면, Money 자료형만 매개변수로 받을 수 있게 메서드를 변경하면된다
//3.15 Money 자료형만 받도록 메서드 수정하기
class Money{
// 생략
Money add(final Money other) {
final int added = amount + other.amount;
return new Money(added, currency);
}
}
기본 자료형 위주로 사용하면, 의미가 다른 값이 여러 개 있어도 모두 int 자료형이나 String 자료형으로 정의하기 쉽다. 따라서 실수로 의미가 다른 값을 전달하기 쉽다.
반면 Money 처럼 독자적인 자료형을 사용하면, 의미가 다른 값을 전달할 경우 컴파일 오류가 발생할 수 있다.
3.2.7 의미 없는 메서드 추가하지 않기
다음 코드는 금액을 곱하는 메서드이다. 이와 같은 메서드가 의미가 있을까 ?
// 코드 3.17 금액을 곱하는 메서드가 의미가 있을까?
class Money {
// ...
Money multiply(Money other) {
if (!currency.equals(other.currency)) {
throw new IllegalArgumentException("통화 단위가 다릅니다.");
}
final int multiplied = amount * other.amount;
return new Money(multiplied, currency);
}
}
“int 자료형” 비슷한 것이니까 덧셈, 뺼셈, 곱셈, 나눗셈을 구현해 두자 라고 생각하고, 시스템 사양에 필요하지 않은 메서드를 ‘선의’로 추가했다면, 이후에 누군가 이를 무심코 사용했을 때 버그가 될 수 있습니다.
시스템 사양에 필요한 메서드만 정의합시다.
3.3 악마 퇴치 효과 검토하기
인스턴스 변수를 중심으로, 인스턴스 변수가 잘못된 상태에 빠지지 않게 설계하면 악마를 퇴치할 수 있다. 클래스 설계
란 인스턴스 변수가 잘못된 상태에 빠지지 않게 하기 위한 구조를 만드는 것이라고 해도 과언이 아니다.
같은 데이터라고 해도 메서드 매개변수, 지역 변수, static 변수로 설계했다면 악마의 공격을 방어할 수 없다. 인스턴스 변수이기 때문에 방어할 수 있는 것이다.
관련된 로직이 흩어져 있는 구조를 응집도가 낮은 구조라고 한다. 반면 방금 소개한 Money 클래스 처럼 로직이 한 곳에 모여 있는 구조는 응집도가 높은 구조라고 한다. 또한 ‘데이터’와 ‘그 데이터를 조작하는 로직’을 하나의 클래스로 묶고, 필요한 절차만 외부에 공개하는 것을 ‘캡슐화’라고 한다.
3.4 프로그램 구조의 문제 해결에 도움을 주는 디자인 패턴
응집도가 낮은 구조로 만들거나, 잘못된 상태로부터 프로그램을 방어하는 등 프로그램 구조를 개선하는 설계 방법을 디자인 패턴
이라고 부른다.
디자인 패턴은 노하우처럼 정리되어있다.
- 완전 생성자
- 잘못된 상태로부터 보호함
- 값 객체
- 특정한 값과 관련된 로직의 응집도를 높임
- 전략
- 조건 분기를 줄이고, 로직을 단순화함
- 정책 (policy)
- 조건 분기를 단순화하고, 더 자유롭게 만듦
- 일급 컬렉션 (First Class Collection)
- 값 객체의 일종으로 컬렉션과 관련된 로직의 응집도를 노임
- 스프라우트 클래스 (Sprout Class)
- 기존 로직을 변경하지 않고, 안전하게 새로운 기능을 추가함
3.4.1 완전 생성자
완전 생성자(complete constructor)는 잘못된 상태로부터 클래스를 보호하기 위한 디자인 패턴이다.
매개변수 없는 디폴트 생성자로 객체를 생성하고, 이후에 인스턴스 변수에 값을 설정하는 방법은 인스턴스 변수를 초기화하지 않을 가능성이 존재한다. 그럼 곧바로 쓰레기 객체가 만들어진다.
쓰레기 객체를 방지하려면 인스턴스 변수를 모두 초기화해야만 객체를 생성할 수 있게 매개변수를 가진 생성자를 만들면 된다. 그리고 생성자 내부에서는 가드를 사용해서 잘못된 값이 들어오지 않게 만든다. 이렇게 설계하면, 값이 모두 정상인 완전한 객체만 만들어질 것이다.
참고로 인스턴스 변수에 final 수식자를 붙여서 불변으로 만들면, 생성 후에도 잘못된 상태로부터 방어할 수 있다.
3.4.2 값 객체
값 객체란 값을 클래스로 나타내는 디자인 패턴이다.
- 애플리케이션에서 사용하는 금액, 날짜, 주문 수, 전화번호 등 다양한 값을 객체로 만들 수 있다. 이러한 값을 값 객체로 만들어서 사용하면, 각각의 값과 로직을 응집도가 높은 구조로 만들 수 있다.
값 객체와 완전 생성자는 얻을 수 있는 효과가 거의 비슷하므로, 일반적으로 함께 사용합니다.
- ‘값 객체 + 완전 생성자’는 객체 지향 설계에서 폭 넓게 사용되는 기법이라고 할 수 있습니다.
Leave a comment