Skip to content

Latest commit

 

History

History
359 lines (255 loc) · 13.8 KB

ch03 - 클래스 설계: 모든 것과 연결되는 설계 기반.md

File metadata and controls

359 lines (255 loc) · 13.8 KB

3.1 클래스 단위로 잘 동작하도록 설계하기

  • 클래스 단위로도 잘 동작하게 설계해야 한다. 가 핵심.
  • 클래스 그 자체로도 잘 동작할 수 있게 설계해야한다.

3.1.1 클래스의 구성 요소

  • 클래스의 구성 요소
    • 인스턴스 변수
    • 메서드

잘 만들어진 클래스

  • 잘 만들어진 클래스의 구성 요소
    • 인스턴스 변수
    • 인스턴스 변수에 잘못된 값이 할당됮 않게 막고, 정상적으로 조작하는 메서드

왜 변수랑 메서드를 같이 묶어야 하나?

  • 데이터와 로직이 따로 있으면 연관성을 알아채기 어렵다.
  • 코드가 중복될 수 있다.
    • 중복 코드는 수정하다가 다른 코드의 수정을 놓칠 수도 있다.
  • 한 곳에 있지 않으므로, 가독성을 낮춘다.

3.1.2 모든 클래스가 갖추어야 하는 자기 방어 임무

  • 클래스 스스로 자기 방어 임무를 수행해야 소프트웨어 품질을 높일 수 있다.

3.2 성숙한 클래스로 성장시키는 설계 기법

3.2.1 생성자로 확실하게 정상적인 값 설정하기

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)을 생성자 내부에 정의한다.

가드

  • 위 코드의 생성자처럼 처리 범위를 벗어나는 조건을 메서드 가장 앞부분에서 확인하는 코드를 '가드'라고 한다.
  • 생성자에 가드를 배치 시, 항상 안전하고 정상적인 인스턴스만 존재하게 된다.

3.2.2 계산 로직도 데이터를 가진 쪽에 구현하기

  • 데이터와 데이터를 조작하는 로직이 분리되어 있는 구조는 '응집도가 낮은 구조'이다.
  • 데이터와 데이터 조작 로직을 모아서, 클래스를 성숙하게 만들자.
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;
    }

}

3.2.3 불변 변수로 만들어서 예상하지 못한 동작 막기

  • 변수의 값이 계속 바뀌면, 값이 언제 변경되었는지, 지금 값은 무엇인지 계속 신경 써야 한다.
    • 비즈니스 요구 사항이 바뀌어 코드 수정하다가 의도치 않은 값을 할당하는 예상치 못한 사이드 이펙트(4.2.3에서 설명) 발생 가능.

final 을 붙여 불면 변수로 만들기

public class Money {

    final int amount;
    final Currency currency;

    public Money(final int amount, final Currency currency) {
        // 생략
        this.amount = amount;
        this.currency = currency;
    }

}
  • final을 이용하여 인스턴스 변수를 불변으로 만들자.
  • 이렇게 하면 변수 선언 시점 또는 생성자 안에서만 값을 할당할 수 있게되고 더 이상의 재할당은 불가능.

3.2.4 변경하고 싶다면 새로운 인스턴스 만들기

  • 불변이어도 변경 가능한 방법이 있다. 👉 새로운 인스턴스 만들기
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);
    }

}
  • 이렇게하면 값을 유지하면서도 불변으로 만들 수 있다!

3.2.5 메서드 메서드 매개변수와 지역 변수도 불변으로 만들기

public class Money {

    // ... 

    Money add(final int other) {
        final int added = this.amount + other;
        return new Money(added, this.currency);
    }

}
  • 값이 중간에 바뀌면 값의 변화를 추적하기 힘들기 때문에 버그 발생 가능성이 높다.
  • 기본적으로 지역변수, 매개변수는 불변으로 만드는 것이 좋음.
    • 내부에서 값 변경 시 컴파일러가 에러를 뱉어준다.

3.2.6 엉뚱한 값을 전달하지 않도록 하기 (값 포장하기)

// 문제의 코드
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);
    }

}

3.2.7 의미 없는 메서드 추가하지 않기

// 금액을 곱하는 메서드가 의미 있을까?
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
  • 시스템 사양에 불필요한 메서드를 '선의'로 추가해두면 다른 개발자가 무심코 사용했을 때 버그 발생 가능.
    • 꼭 필요한 메서드만 정의하자!

3.3 악마 퇴치 효과 검토하기

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 클래스)

퇴치된 악마들

  1. 중복 코드 - 필요한 로직이 Money 내부에 모여 있음. 👉 다른 클래스에 중복 코드가 작성될 일이 줄어든다.
  2. 수정 누락 - 응집도를 높임으로써 중복 코드가 발생하지 않게되므로 코드 수정 시 누락할 일이 줄어듦.
  3. 가독성 저하 - 필요한 로직이 Money 내부 안에 모여 있으므로, 디버깅 또는 기능 변경 시 관련된 로직을 찾으로 돌아다닐 필요가 없음. 즉 가독성이 증가함.
  4. 쓰레기 객체 - 생성자에서 인스턴스 변수의 값을 확정하므로 👉 초기화 되지 않은 상태가 있을 수 있음.
  5. 잘못된 값 - 가드 설치, 인스턴스 변수를 final로 선언 👉 잘못된 값이 들어오지 않음
  6. 생각하지 못한 부수 효과 👉 final을 붙여 불변으로 만들었으므로 부수 효과로부터 안전함.
  7. 값 전달 실수 👉 매개변수를 래핑하여 Money 타입으로 만들었으므로, 다른 타입을 실수로 넣을 때 컴파일 에러를 발생.

인스턴스 변수가 잘못된 상태에 빠지지 않게하는 것이 핵심

  • 클래스 설계란 인스턴스 변수가 잘못된 상태에 빠지지 않게 하기 위한 구조를 만드는 것. 이라 해도 과언은 아님.

응집도와 캡슐화

  • 앞의 Money 클래스처럼 로직이 한 곳에 모여있는 구조를 응집도가 높다고 함.
  • 데이터와 데이터를 조작하는 로직을 하나의 클래스로 묶고 필요한 절차(=메서드)만 외부에 공개하는 것을 캡슐화라고함.

3.4 프로그램 구조의 문제 해결에 도움을 주는 디자인 패턴

  • 디자인 패턴: 응집도가 높은 구조로 만들거나, 잘못된 상태로부터 프로그램을 방어하는 등의 프로그램의 구조를 개선하는 설계 방법
디자인 패턴 효과
완전 생성자 잘못된 상태로부터 보호함.
값 객체 특정한 값과 관련된 로직의 응집도를 높힘.
전략(strategy) 조건 분기를 줄이고, 로직을 단순화함
정책(policy) 조건 분기를 단순화하고, 더 자유롭게 만듦.
일급 컬렉션(First Class Collection) 값 객체의 일종으로 컬렉션과 관련된 로직의 응집도를 높임.
스프라우트 클래스 기존 로직을 변경하지 않고, 안전하게 새로운 기능을 추가함

3.4.1 완전 생성자

Setter가 만드는 쓰레기 객체

  • 매개변수 없는 디폴트 생성자로 객체를 생성하고, 이후에 인스턴스 변수를 설정하는 방법
    • 👉 인스턴스 변수를 초기화하지 않을 가능성이 존재.
      • 👉 인스턴스가 존재하는 쓰레기 객체가 만들어진다.

완전 생성자를 이용한 쓰레기 객체 방지

  • 인스턴스 변수를 모두 초기화해야지만 객체를 생성할 수 있는 매개변수를 가진 생성자를 만들자.
  • 생성자 내부에서는 가드를 이용하여 잘못된 값이 들어오지 않게 막는다.
  • 추가적으로 인스턴스 변수에 final 키워드를 붙여 불변으로 만들면 객체 생성 후에도 잘못된 상태로부터 방어 가능.

3.4.2 값 객체