1장. 잘못된 구조의 문제 깨닫기
1.1. 의미를 알 수 없는 이름
기술 중심 명명
class MemoryStateManager {
void changeIntValue0(int changeValue) {
...
}
}
일련번호 명명
class Class001 {
void method001();
void method002();
}
- 기술 중심 명명, 일련 번호 명명 은 어떠한 의도도 읽어낼 수 없음
1.2. 이해하기 어렵게 만드는 조건 분기 중첩
조건 분기 처리를 많이 하지 말아라
1.3. 수많은 악마를 만들어 내는 데이터 클래스
public class ContractAmount {
public int amountIncludingTax;
public BigDecimal salesTaxRate;
}
- 데이터를 갖고 있기만 하는 클래스
- 세금이 포함된 금액을 계산하는 로직도 필요
- 이러한 계산 로직을 데이터 클래스가 아닌 다른 클래스에 구현하게 됨
- 설계를 따로 고려하지 않아 생기는 일
1.3.1. 사양을 변경할 때 송곳니를 드러내는 악마
- 로직이 여러 군데 번져 있으면, 응집도가 낮아져 여러 문제가 발생할 수 있음
1.3.2. 코드 중복
1.3.3. 수정 누락
1.3.4. 가독성 저하
1.3.5. 초기화되지 않은 상태 (쓰레기 객체)
- 초기화되지 않으면 잘못된 데이터가 들어가는 클래스
- 추가로 초기화해야 하는 클래스는 버그가 발생하기 쉬운 불완전한 클래스
즉
- 초기화하기 않으면 쓸모 없는 클래스
- 초기화하지 않은 상태가 발생할 수 있는 클래스
1.3.6. 잘못된 값 할당
- 잘못된 값이 들어가지 않게, 데이터 클래스를 사용하는 쪽의 로직을 살짝 변경해서 유효성을 검사하게 만들 수 있음
1.4. 악마 퇴치의 기본
- 나쁜 구조의 폐해를 인지해야 함
- 나쁜 폐해를 인지하면 어떻게든 대처해야겠다고 생각
- 이 생각이 바로 좋은 설계를 위한 첫걸음
2장. 설계 첫걸음
2.1. 의도를 분명히 전달할 수 있는 이름 설계하기
int d = 0;
d = p1 + p2;
- 무언가를 계산하고 있지만 무엇을 하고 있는지 전혀 이해할 수 없음
int damageAmount = 0;
damageAmount = playerArmPower + playerWaponPower;
- 자주 바뀔 가능서이 있는 코드를 구현할 때는 변수 이름을 쉽게 붙이는 것도 아주 훌륭한 기본 설계가 될 수 있음
2.2. 목적별로 변수를 따로 만들어 사용하기
- 계산의 중간 결과를 동일한 변수에 계속해서 대입하는 코드가 많이 사용
- 변수의 값을 다시 할당하는 것을
재할당
이라고 함
- 재할당은 변수의 용도가 바뀌는 문제를 일으키기 쉬움
재할당으로 기존 변수를 다시 사용하지 말고, 목적별로 변수를 만들어서 사용
int totalPlayerAttackPower = playerArmPower + playerWeaponPower;
int totalEnemyDefence = enemyBodyDefence + enemyArmorDefence;
- 어떤 값을 계산하는 데 어떤 값을 사용하는 지 관계를 파악하기 훨씬 쉬움
2.3. 단순 나열이 아니라, 의미 있는 것을 모아 메서드로 만들기
- 일련의 흐름이 그냥 작성되어 있으면, 계산 로직들이 단순하게 나열되어 있으면, 로직이 어디에서 시작해서 어디에서 끝나는지, 무슨 일을 하는 지 알기 어려움
- 이러한 상황을 막으려면, 의미 있는 로직을 모아서 메서드로 구현하는 것이 좋음
int sumUpPlayerAttackPower(int playerArmPower, int playerWeaponPower) {
return playerArmPower + playerWeaponPower;
}
totalPlayerAttackPower = sumUpPlayerAttackPower(playerArmPower, playerWeaponPower)
- 세부 계산 로직을 메서드로 감쌌으므로, 일련의 흐름이 훨씬 쉽게 읽힘
- 또한 서로 다른 계산 작업을 각각의 메서드로 분리했으므로 쉽게 구분 가능
설계
= 유지보수와 변경이 쉽도록 변수의 이름과 로직을 신경써서 개발하는 것
2.4. 관련된 데이터와 로직을 클래스로 모으기
int hitPoint;
hitPoint = hitPoint - damageAmount;
if (hitPoint < 0) {
hitPoint = 0;
}
- 이런 로직들이 계속해서 이곳저곳 만들어짐 > 버그가 발생할 확률이 높음
- 이러한 문제를 해결해 주는 것이
클래스
class HitPoint {
private static final MIN = 0;
private static final MAX = 9999;
final int value;
HitPoint(final int value) {
if (value < MIN) throw new IllegalArgumentException();
if (value > MAX) throw new IllegalArgumentException();
this.value = value;
}
...
}
- 서로 밀접한 데이터와 로직을 한 곳에 모아 두면, 이곳저곳 찾아 다니지 않아도 괜찮음
- 잘못된 값이 유입되지 않게 만들면, 조금이나마 버그로부터 안전한 클래스 구조가 됨
3장. 클래스 설계: 모든 것과 연결되는 설계 기반
3.1. 클래스 단위로 잘 동작하도록 설계하기
3.1.1. 클래스의 구성 요소
- 인스턴스 변수
- 메서드
- 이러한 구성에서 버그를 일으키는 악마를 적게 불러오려면, 메서드의 역할을 명확하게 해야함
- 잘 만들어진 클래스는 다음 두가지로 구성
- 인스턴스 변수
- 인스턴스 변수에 잘못된 값이 할당되지 않게 막고, 정상적으로 조작하는 메서드
- 데이터 클래스는 일반적으로 인스턴스 변수를 조작하는 로직이 다른 클래스에 구현
- 연관성을 어려워서 부차적인 문제가 생길 수 있음
문제 1. 코드가 중복될 수 있고 중복 코드 중 일부를 그대로 둠
-> 가독성 저하
문제 2. 인스턴스를 생성하더라도 인스턴스 변수들은 아직 유효하지 않아 따로 초기화
-> 데이터 클래스는 초기화 작업을 하는 코드조차 다른 클래스에 구현
-> 데이터 클래스가 자기 자신을 보호할 수 있는 로직이 없음
3.1.2. 모든 클래스가 갖추어야 하는 자기 방어 임무
- 따로 초기화하지 않거나 사전 준비를 하지 않으면 사용할 수 없는 클래스와 메서드는 아무도 사용하고 싶지 않아함
- 기본적인 단위 (메서드, 클래스, 모듈 등)은 그 자체로 버그 없이, 언제나 안전하게 사용할 수 있는 품질을 갖춰야 함
- 클래스는 스스로
자기 방어 임무
를 수행할 수 있어야 품질을 높일 수 있음
3.2. 성숙한 클래스로 성장시키는 설계 기법
class Mony {
int amount;
Currency currency;
}
3.2.1. 생성자로 확실하게 정상적인 값 설정하기
raw data object
: default 생성자 (매개변수 없는 생성자) 를 사용해서 인스턴스를 생성한 후, 인스턴스 변수에 따로 값을 할당해서 초기화 = 초기화 하지 않은 상태
클래스 인스턴스를 생성하는 시점에 확실하게 인스턴스 변수가 정상적인 값을 갖게 만들어야 함
class Mony {
int amount;
Currency currency;
Money(int amount, Currency currency) {
if (amount < 0) {
throw new IllegalArgumentException();
}
if (currency == null) {
throw new IllegalArgumentException();
}
this.amount = amount;
this.currency = currency;
}
}
3.2.2. 계산 로직도 데이터를 가진 쪽에 구현하기
- 응집도가 낮은 구조 : 데이터와 데이터를 조작하는 로직이 분리되어 있는 구조
3.2.3. 불변 변수로 만들어서 예상하지 못한 동작 막기
- 변수의 값이 계속해서 바뀌면, 값이 언제 변경되었는지, 지금 값은 무엇인지 계속 신경써야함
- 따라서 예상치 못한 부수 효과가 발생할 수 있음
class Mony {
final int amount;
final Currency currency;
Money(int amount, Currency currency) {
this.amount = amount;
this.currency = currency;
}
}
final
수식자를 붙이면 한 번만 할당 할 수 있음
- 인스턴스 변수에 잘못된 값을 직접 할당할 수 없음
3.2.4. 변경하고 싶다면 새로운 인스턴스 만들기
class Mony {
...
Money add(int other) {
int added = amount + other;
return new Money(added, currency);
}
}
3.2.5. 메서드 매개변수와 지역 변수도 불변으로 만들기
void doSomething(int value) {
value = 100;
- 값이 중간에 바뀌면, 값의 변화를 추적하기 힘듬
void doSomething(final int value) {
value = 100;
3.2.6. 엉뚱한 값을 전달하지 않도록 하기
- 엉뚱한 값이 전달되지 않도록 하려면, Money 자료형만 매개변수로 받을 수 있게 메서드를 변경
class Mony {
...
Money add(final Money other) {
final int added = amount + other.amount;
return new Money(added, currency);
}
}
- int 자료형이면 다른 값을 잘 못 전달해도 컴파일 오류가 발생하지 않음
- 프로그래밍 언어가 표준적으로 제공하는 자료형 :
primitive type
- 따라서 실수로 의미가 다른 값을 전달하기 쉬움
3.2.7. 의미 없는 메서드 추가하지 않기
class Money {
...
Money multiple(Money other) {
if (!currency.equals(other.currency)) {
throw new IllegalArgumentException();
}
final int multiplied = amount * other.amount;
return new Money(multiplied, currency);
}
}
- mutiple 을 하는 값은 필요 없음
- 따라서 필요 없는 method 는 추가하지 않는 것이 좋음
3.3. 악마 퇴치 효과 검토하기
class Money {
final int amount;
final Currency currency;
Money(final int amount, final Currency currency) {
if (amount < 0) {
throw new IllegalArgumentException();
}
if (currency == null) {
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 = amount + other.amount;
return new Money(added, currency);
}
}
클래스 설계
= 인스턴스 변수가 잘못된 상태에 빠지지 않게 하기 위한 구조를 만드는 것
응집도가 놓은 구조
: 로직이 한 곳에 모여있는 구조
캡슐화
: 데이터를 조작하는 로직은 묶고, 필요한 절차만 외부에 공개하는 것
3.4. 프로그램 구조의 문제 해결에 도움을 주는 디자인 패턴
앞의 Money class 는 완전 생성자와 값 객체라는 두자기 디자인 패턴이 적용됨
3.4.1. 완전 생성자 (complete constructor)
- 잘못된 상태로부터 클래스를 보호하기 위한 디자인 패턴
- 인스턴스 변수를 초기화하지 않은 객체들이 생성 > 쓰레기 객체가 만들어지는 것을 방지
- final 수식자를 붙여서 불변으로 만들면, 생성 후에도 잘못된 상태로부터 방어 가능
3.4.2. 값 객체 (Value Object)
- 값을 class 로 나타내는 디자인 패턴
값 객체 + 완전 생성자
객체 지향 설계에서 폭넓게 사용되는 기법
4장. 불변 활용하기: 안정적으로 동작하게 만들기
4.1. 재할당
- 변수에 값을 다시 할당하는 것 =
재할당
, 파괴적 할당
4.1.1. 불변 변수로 만들어서 재할당 막기
- 재할당을 기계적으로 막을 수 있는 방법 = 변수에 final 수식자 붙이기
void doSomething() {
final int vlaue = 100;
value = 200;
4.1.2. 매개변수도 불변으로 만들기
- 재할당을 막으려면 매개변수에도 final 을 붙이면 됨
void addPrice(final int productPrice) {
final int increasedTotalPrice = totalPrice + productPrice;
}
4.2. 가변으로 인해 발생하는 의도하지 않은 영향
- 인스턴스가 가변이면 다른 부분에 의도하지 않은 영향을 주기 쉬움
- 코드를 변경했을 때 생각하지도 못했던 위치에서 상태가 변화하여 예측하지 못한 동작을 하는 경우가 있음
4.2.1. 사례 1: 가변 인스턴스 재사용하기
AttackPower attackPower = new AttackPower(20);
Weapon weaponA = new Weapon(attackPower);
Weapon weaponB = new Weapon(attackPower);
attackPower
를 변경하면 weaponA
, weaponB
에서 동시에 변경
- 이러한 상황을 방지하려면 인스턴스를 재사용하지 못하게 만들면 됨
AttackPower attackPowerA = new AttackPower(20);
Weapon weaponA = new Weapon(attackPowerA);
AttackPower attackPowerB = new AttackPower(20);
Weapon weaponB = new Weapon(attackPowerB);
4.2.2. 사례 2: 함수로 가변 인스턴스 조작하기
class AttackPower {
...
void reinforce(int increment) {
value += increment;
}
void disable() {
value = MIN;
}
}
AttackPower attackPower = new AttackPower(20);
attackPower.reinforce(15);
- 이렇게 코드를 변경했는데, 갑자기 값이 변경되는 일이 발생
- 다른 쓰레드에서 공격력을 변경시킴
- 이렇게 동작하면 안됨 > 구조적인 문제를 가지고 있음 > 부수 효과
4.2.3. 부수 효과의 단점
주요 작용
- 함수 (method) 가 매개변수를 전달 받고, 값을 리턴하는 것
부수 효과
- 주요 작용 이외의 상태 변경을 일으키는 것
- 함수가 매개변수를 전달 받고 값을 리턴하는 것 이외에 외부 상태를 변경하는 것
attackPower.value
- instance 변수를 변경하기 때문에, 작업 실행 순서에 의존해야 함
- 따라서 항상 같은 결과가 나온다고 보장할 수 없음
4.2.4. 함수의 영향 범위 한정하기
- 부수 효과가 있는 함수는 영향 범위를 예측하기 힘듬
- 예상치 못한 동작을 막으려면, 함수가 영향을 주거나 받을 수 있는 범위를 한정하는 것
- 데이터 (상태) 는 매개변수로 받음
- 상태를 변경하지 않음
- 값은 함수의 리턴 값으로 돌려줌
- 따라서 매개변수로 상태를 받고, 상태를 변경하지 않고, 값을 리턴하기만 하는 함수가 이상적
4.2.5. 불변으로 만들어서 예기치 못한 동작 막기
- 인스턴스 변수 value 가 가변적이므로, 부수 효과가 발생할 여지를 남김
- 기능 변경 때에도 의도하지 않게 부수효과가 있는 함수가 만들어져서, 예상하지 못한 동작을 일으킬 가능성은 항상 존재
class AttackPower {
static final int MIN = 0;
final int value;
...
AttackPower reinforce(final AttackPower increment) {
return new AttackPower(this.value + increment.value);
}
AttackPower disable() {
return new AttackPower(MIN);
}
}
final AttackPower attackPower = new AttackPower(20);
...
final AttackPower reinforced = attackPower.reinforce(new AttackPower(15));
final AttackPower disabled = attackPower.disable();
- 새로운 인스턴스를 만들었으므로 변경 전과 변경 후의 공격력은 서로 영향을 주지 않음
4.3. 불변과 가변은 어떻게 다루어야 할까
4.3.1. 기본적으로 불변으로
- 변수의 의미가 변하지 않으므로 혼란을 줄일 수 있음
- 동작이 안정적이게 되므로 결과를 예측하기 쉬움
- 코드의 영향 범위가 한정적으로 유지 보수가 편리해짐
4.3.2. 가변으로 설계해야 하는 경우
- 기본적으로 불변으로 설계하는 것이 좋지만, 가변이 필요한 경우가 있음
Performance 가 중요한 경우
ex) 대량의 데이터를 빠르게 처리해야 하는 경우 등
- 불변이라면 값을 변경할 때 인스턴스를 새로 생성해야 함
- 만약 크기가 큰 인스턴스를 새로 생성하면서 시간이 오려 걸려 문제가 된다면 불변 보다는 가변을 사용하는 것이 더 좋음
4.3.3. 상태를 변경하는 메서드 설계하기
mutater
: 상태를 변화시키는 메서드
- 조건에 맞는 올바른 상태로 변경하는 mutater 로 바꿔보자
class HitPoint {
private static final int MIN = 0;
int amount;
void damage(final int damageAmount) {
final int nextAmount = amount - damageAmount;
}
boolean isZero() {
return amount == MIN;
}
}
class Member {
final HitPoint hitPoint;
final States states;
void damage(final int damageAMount) {
hitPoint.damage(damageAmount);
if (hitPoint.isZero()) {
states.add(StateType.dead);
}
}
}
4.3.4. 코드 외부와 데이터 교환은 국소화하기
- 불변을 활용해서 아무리 신중하게 설계하더라도, 코드 외부와의 데이터 교환은 주의해야 함
- 파일을 읽고 쓰는 I/O 조작은 코드 외부의 상태에 의존
- 특별한 이유 없이 외부 상태에 의존하는 코드를 작성하면, 동작 예측이 힘들어지므로 문제가 발생할 가능성이 높음
Repository Patter
: Database 의 영속화를 캡슐화하는 디자인 패턴
5장. 응집도: 흩어져 있는 것들
5.1. static method 오용
- static method 오용으로 응집도가 낮아지는 경우가 있음
class OrderManager {
static int add (int moneyAmount1, int moneyAmount2) {
return moneyAmount1 + moneyAmount2;
}
}
- static method 로 정의하면 클래스의 인스턴스를 생성하지 않고, add method 호출 가능
moneyData1.amount = OrderManager.add(moneyData1.amount, moneyData2.amount);
- moneyData 와 moneyData 를 조작하는 로직이 따로 있는 것이 문제
- 악마를 불러들이는 코드 (김태리가 따로 없음)
5.1.1. static method 는 인스턴스 변수를 사용할 수 없음
- static method 는 인스턴스 변수를 사용할 수 없음
- 따라서 응집도가 낮아질 수 밖에 없음
5.1.2. 인스턴스 변수를 사용하는 구조로 변경하기
응집도를 높이는 것
: 인스턴스 변수와 인스턴스 변수를 사용하는 로직을 같은 클래스에 만드는 것
5.1.3. 인스턴스 메서드인 척하는 static method 주의하기
- static keyword 만 없을 뿐 static method 와 같은 문제를 갖고 있는 인스턴스 메서드도 자주 볼 수 있음
class PaymentManager {
private int discountRate;
...
int add(int moneyAmount1, int moneyAmount2) {
return moneyAmount1 + moneyAmount2;
}
}
add
는 인스턴스 변수를 사용하지 않으므로 static 과 다름이 없음
- 인스턴스 메서드인 척 하는 static method 도 응집도를 낮춤
5.1.4. 왜 static method 를 사용할까?
- 객체 지향 언어를 사용할 때 절차 지향 언어의 접근 방법을 사용하려 하기 때문
- 절차 지향 언어에서는 데이터와 로직이 따로 존재하도록 설계
- 이러한 접근 방법을 객체 지향 언어에 적용하여 설계하면, 데이터와 로직을 별도의 클래스에 배치하게 됨
- static method 를 남용하지 않는 것이 중요
5.1.5. 어떠한 상황에서 static method 를 사용해야 좋을까?
- 응집도의 영향을 받지 않는 경우, static method 를 사용해도 괜찮음
ex) log 출력 전용 메서드, format 변환 메서드 등
5.2. 초기화 로직 분산
- 이 GiftPoint 클래스에 로직이 데이터와 응집되어 있는 것처럼 보임
GiftPoint standardMembershipPoint = new GiftPoint(3000);
- 생성자를 public 으로 만들면 의도하지 않은 용도로 사용될 수 있음
- 관련 로직이 분산되기 때문에 유지 보수하기 힘들어짐
5.2.1. private 생성자 + 팩토리 메서드를 사용해 목적에 따라 초기화하기
class GiftPoint {
private static final int STANDARD_MEMBERSHIP_POINT = 3000;
private static final int PREMIUM_MEMBERSHIP_POINT = 10000;
...
static GiftPoint forStandardMembership() {
return new new GiftPoint(STANDARD_MEMBERSHIP_POINT);
}
static GiftPoint forPremiumMembership() {
return new new GiftPoint(PREMIUM_MEMBERSHIP_POINT);
}
...
}
- Membership 이란 factory method 를 생성
5.2.2. 생성 로직이 너무 많아지면 팩토리 클래스를 고려해보자
- 생성 로직이 너무 많아지는 것 같다면, 생성 전용 팩토리 클래스를 분리하는 방법을 고려하는 것이 좋음
5.3. 범용 처리 클래스 (Common/Util)
- 범용 처리를 위한 클래스에는 Common, Util 이라는 이름이 붙음
- static method 와 마찬가지로 응집도가 낮은 구조가 만들어 질 수 있음
- 똑같은 일을 수행해야 하는 코드가 많아지면 코드를 재사용하기 위해 범용 클래스를 만듬
- 이때 static method 로 구현되는 경우가 많음
class Common {
static BigDecimal calcAmountIncludingTax(BigDecimal amountExcludingTax, BigDecimal taxRate) {
return amountExcludingTax.multiple(taxRate);
}
}
5.3.1. 너무 많은 로직이 한 클래스에 모이는 문제
- Common, Util 이라는 이름 자체가 범용이라는 뜻이기 때문에
- 사람들이 범용적으로 사용하고 싶은 로직은 Common class 에 만들면 되겠다고 생각
- 근본적인 원인은 범용의 의미와 재사용성을 잘못 이해하고 있기 때문
- 재사용성은 설계의 응집도를 높이면 저절로 높아짐
5.3.2. 객체 지향 설계의 기본으로 돌아가기
- 꼭 필요한 경우가 아니라면 범용 처리 클래스를 만들지 않는 것이 좋음
class AmountIncludingTax {
final BigDecimal value;
AmountIncludingTax(final AmountExcludingTax amountExcludingTax, final TaxRate taxRate) {
value = amountExcludingTax.value.mutiply(taxRate.value);
}
}
5.3.3. 횡단 관심사
- 로그 출력과 오류 확인은 애플리케이션의 모든 동작에 필요한 기능
cross-cutting concern
: 다양한 상황에서 넓게 활용되는 기능
try {
shoppingCart.add(product);
} catch (IllegalAmountException e) {
Logger.repot('');
}
5.4. 결과를 리턴하는 데 매개변수 사용하지 않기
- 범용 처리 클래스 예에서 살펴보았던 것처럼 매개변수를 잘못 다루면, 응집도가 낮아지는 문제가 발생
- 출력 배개변수도 같음 문제를 일으킴
class ActorManager {
void shift(Location location, int shiftX, int shiftY) {
location.x += shiftX;
location.y += shiftY;
}
}
- 이동 대상 인스턴스를 매개변수 location 으로 전달받고 이를 변경
- 이렇게 출력으로 사용되는 매개변수를
출력 매개변수
라고 부름
- 데이터 조작 대상 = Location / 조작 로직 = ActorManager
- 데이터와 로직이 다른 클래스에 존재
class SpecialAttackManager {
void shift(Location location, int shiftX, int shiftY) {
- 출력 매개변수는 응집도 문제 이외에도 여러 문제를 발생시킴
class DiscountManager {
void set(MoneyData money) {
money.amount -= 2000;
...
}
}
- 전달한 매개변수 money 의 값을 변경
- 매개변수는 입력으로 전달하는 것이 일반적
- 이처럼 출력으로 사용해버리면, 매개변수가 입력인지 출력인지 메서드 내부의 로직을 확인해야함
- 출력 매개변수로 설계하지 말고
데이터
, 데이터를 조작하는 논리
를 같은 클래스에 배치
class Location {
final int x;
final int y;
Location(final int x, final int y) {
this.x = x;
this.y = y;
}
Location shift(int shiftX, int shiftY) {
final int nextX = x + shiftX;
final int nextY = y + shiftY;
return new Location(nextX, nextY);
}
5.5. 매개변수가 너무 많은 경우
- 매개변수가 너무 많은 메서드는 응집도가 낮아지기 쉬움
int recoverMagicPoint(int currentMagicPoint, it originalMaxMagicPoit, List<Integer> maxMagicPointIncrements, int recoveryAmount) {...}
- 이 메서드는 정상적으로 기능하지만, 구조가 좋지 않음
- 너무 많은 매개변수를 받으면 실수로 잘못된 값을 대입할 가능성이 높음
- 매개변수가 많다는 것은 많은 기능을 처리하고 싶다는 의미
- 처리할 게 많아지면, 로직이 복잡해지거나, 중복 코드가 생길 가능성이 높아짐
5.5.1. 기본 자료형에 대한 집착
primitive type
: 프로그래밍 언어가 표준적으로 제공하는 자료형
- 메서드에 매개변수를 전달한다는 것은 해당 매개변수를 사용해서 어떤 기능을 수행하고 싶다는 의미
primitive obsession
: 기본 자료형을 남용하는 현상
- 줄곧 기본 자료형만을 써 온 개발자는 클래스 설계를 고려하지 않는 경우가 많음
class Util {
boolean isFairPrice(int regularPrice) {
if (regularPrice < 0) {
throw new IllegalArgumentException();
}
}
}
- 이렇게 기본 자료형으로만 구현하면, 중복 코드가 많이 생김
- 계산 로직이 이곳 저곳 분산되기 쉬움
- 기본 자료형만으로 동작하는 코드를 구현하기는 쉬우나, 관련 있는 데이터와 로직을 집약하기는 힘듬
- 버그가 생기기 쉽고, 가독성이 떨어짐
- 데이터는 단순히 존재하기만 할 수는 없음
- 데이터를 사용해 계산하거나 데이터를 판단해서 제어 프름을 전환할 때 사용
- 기본 자료형으로만 구현하려고 하면 데이터를 사용한 계산과 제어 로직이 모두 분산됨
- 즉 응집도가 낮은 구조가 됨
class RegularPrice {
RegularPrice(final int amount) {
if (amount < 0) {
throw new IllegalArgumentException();
}
this.amount = amount;
}
}
5.5.2. 의미 있는 단위는 모든 클래스로 만들기
- 매개변수가 너무 많아지는 문제를 피하려면, 개념적으로 의미 있는 클래스를 만들어야 함
- 매개변수가 많으면 데이터 하나하나를 매개변수로 다루지 말고, 그 데이터를 인스턴스 변수로 갖는 클래스를 만들면 됨
5.6. 메서드 체인
void equiparmor(int memberId, Armor newArmor) {
if(party.members[memberId].equipments.canChange) {
party.members[memberId].equipments.armor = newArmor;
}
}
메서드 체인
: .
으로 여러 메서드를 연결해서 리턴 값으 요소에 차례차례 접근하는 방법
- 이 방법은 응집도를 낮출 수 있는 좋지 않은 작성 방법
데메테르의 법칙
: 사용하는 객체 내부를 알아서는 안된다
- 메서드 체인으로 내부 구조를 돌아다닐 수 있는 설계는 데메테르의 법칙을 위반
5.6.1. Tell, Don't ask
- 다른 객체의 내부 상태를 기반으로 판단하거나 제어하려고 하지 말고
- 메서드로 명령해서 객체가 알아서 판단하고 제어하도록 설계하라는 의미
- 인스턴스 변수는 private 으로 변경해서 외부에서 접근할 수 없게 함
- 그리고 인스턴스 변수에 대한 제어는 외부에서 메서드로 명령하는 형태로 만듬
class Equipments {
private Equipment head;
void deactivateAll() {
this.head = Equipment.EMPTY;
}
}
- 이렇게 하면 Equipment 에 방어구 탈착과 관련된 로직이 응집
6장. 조건 분기: 미궁처럼 복잡한 분기 처리를 무너뜨리는 방법
6.1. 조건 분기가 중첩되어 낮아지는 가독성
if (0 < member.hitPoint) {
if (member.canAct()) {
if (magic.costMagicPoint <= member.magicPoint) {
...
}
}
}
- 위 조건을 모두 만족했을 때만 마법을 발동할 수 있음
6.1.1. 조기 리턴으로 중첩 제거하기
if (0 > member.hitPoint) return;
if (!member.canAct()) return;
if (magic.costMagicPoint > member.magicPoint) return;
...
6.1.2. 가독성을 낮추는 else 구문도 조기 리턴으로 해결하기
- else 구문도 가독성을 나쁘게 만드는 원인 중 하나
6.2. switch 조건문 중복
- 값의 종류에 따라 다르게 처리하고 싶을 때는 switch 조건문을 사용
- switch 조건문은 악마의 저주에 걸릴 수 있음
6.2.1. switch 조건문을 사용해서 코드 작성하기
enum MagicType {
fire,
lighting
}
switch (magicType) {
case fire:
pass
case ligtening:
pass
}
6.2.2. 같은 형태의 switch 조건문이 여러 개 사용되기 시작
switch (magicType) {
case fire:
magicPoint = 10;
break;
case ligtening:
magicPoint = 20;
break;
}
switch (magicType) {
case fire:
attackPower = 10;
break;
case ligtening:
attackPower = 20;
break;
}
6.2.3. 요구 사항 변경 시 수정 누락 (case 구문 추가 누락)
6.2.4. 폭발적으로 늘어나는 switch 조건문 중복
- switch 조건문이 중복된 코드가 생김
- 주의 깊게 대응해도 실수가 발생할 수 밖에 없음
6.2.5. 조건 분기 모으기
- switch 조건문 중복을 해소하려면,
단일 책임 선택의 원칙
을 생각해 봐야함
소프트웨어 시스템이 선택지를 제공해야 한다면, 그 시스템 내부의 어떠한 모듈만으로 모든 선택지를 파악할 수 있어야 함
switch (magicType) {
case fire:
magicPoint = 10;
attackPower = 10;
break;
case ligtening:
magicPoint = 20;
attackPower = 20;
break;
}
- 이렇게 switch 문 한 곳에서 모든 것을 변경하도록 하면 실수를 줄일 수 있음
6.2.6. 인터페이스로 switch 조건문 중복 해소하기
- 단일 책임 원칙으로 switch 조건문을 하나만 사용
- 그런데 변경하고 싶은 부분이 많아지면, 로직이 점점 많아짐
- 이러한 문제를 해결할 때는 Interface 를 사용
interface Shape {
double area();
}
class Retangular implements Shape {
private final double width;
private final double height;
public double area() {
return width * height;
}
}
Shape shape = new Retangular(20, 25);
- 각각의 코드를 간단하게 실행할 수 있게 하는 것 = 인터페이스의 큰 장점
6.2.7. 인터페이스를 switch 조건문 중복에 응용하기 (전략 패턴)
종류별로 다르게 처리해야 하는 기능을 인터페이스의 메서드로 정의하기
- 인터페이스의 가장 큰 장점 = 다른 로직을 같은 방식으로 처리할 수 있음
인터페이스의 이름을 결정하는 방법 : 어떤 부류에 속하는가?
- 가장 기본적인 방법은 인터페이스를 구현하는 클래스들이 어떤 부류인가를 생각해 보는 것
switch 조건문이 아니라, Map 으로 변경하기
- key 를 enum 으로 지정하고 값을 interface 구현 클래스의 인스턴스로 지정
final Map<MagicType, Magic> magics = new HashMap<>();
void magicAttack(final MagicType magicType) {
final Magic usingMagic = magics.get(magicType);
showMagicName(usingMagic);
}
void showMagicName(final Magic magic) {
final String name = magic.name();
}
- switch 조건문을 전혀 사용하기 않고 HashMap 을 사용
메서드를 구현하지 않으면 오류로 인식하게 만들기
값 객체화하기
class Magic {
String name();
}
class Fire implements Magic {
String name() {
return 'fire';
}
}
6.3. 조건 분기 중복과 중첩
- 인터페이스는 switch 조건문의 중복을 제거할 수 있을 뿐만 아니라, 다중 중첩된 복잡한 분기를 제거하는 데 활용 가능
6.3.1. 정책 패턴으로 조건 집약하기
policy pattern
: 조건을 부품처럼 만들고, 부품으로 만든 조건을 조합해서 사용하는 패턴
interface ExcellentCustomerRule {
boolean ok(finalPurchaseHistory history);
}
class GoldCustomerPurchaseAmountRule implements ExcellentCustomerRule {
public boolean ok (final PurchaseHistory history) {
return 1000000 <= history.totalAmount;
}
}
class ExcellentCustomerPolicy {
...
boolean complyWithAll(final PurchaseHistory history) {
for (ExcellentCustomerRule each : rules) {
...
}
}
}
ExcellentCustomerPolicy goldCustomerPolicy = new ExcellentCustomerPolicy();
goldCustomerPolicy.add(new GoldCustomerPurchaseAmountRule());
goldCustomerPolicy.add(new PurchaseFrequencyRule());
goldCustomerPolicy.add(new ReturnRateRule());
- 로직은 간단해졌으나, 이렇게 작성하면 골드 회원과 무관한 로직을 삽입할 수 있음
class GoldCustomerPolicy {
private final ExcellentCustomerPolicy policy;
GoldCustomerPolicy() {
policy = new ExcellentCustomerPolicy();
policy.add(new GoldCustomerPurchaseAmountRule());
policy.add(new PurchaseFrequencyRule());
policy.add(new ReturnRateRule());
}
boolean complyWithAll(final PurchaseHistory history) {
return policy.complyWithAll(history);
}
}
6.4. 자료형 확인에 조건 분기 사용하지 않기
- interface 는 조건 분기를 제거할 때 활용할 수 있음
- interface 를 사용해도 조건 분기가 줄어들지 않는 경우가 있음
interface HotelRates {
Money fee();
}
class RegularRates implements HotelRates {
public Money fee() {...}
}
if (hotelRates instanceof RegularRates) {
busySeasonFee = hotelRates.fee().add(new Money(5000))l
}
else if (hotelRates instanceof PremiumRates) {...}
- 조건 분기 코드가 계속 중복됨
- 이와 같은 로직은
리스코프 치환 원칙
- 클래스의 기반 자료형과 하위 자료형 사이에 성립하는 규칙
- 기반 자료형을 하위 자료형으로 변경해도 코드는 문제없이 동작해야 함
- 인터페이스의 의미를 충분히 이해하지 못하고 사용하면 이와 같은 로직이 자주 만들어짐
interface HotelRates {
Money fee();
Money busySeasonFee();
}
class RegularRates implements HotelRates {
public Money fee() {
return new Money(3000);
}
public Money busySeasonFee() {
return fee().add(new Money(5000));
}
}
Money busySeasonFee = hotelRates.busySeasonFee();
6.5. 인터페이스 사용 능력이 중급으로 올라가는 첫걸음
- 인터페이스를 잘 사용하는 지가 곧 설계 능력의 전환점
- 조건 분기를 써야하는 상황에서는 일단 인터페이스 설계를 떠올리자!
6.6. flag 매개변수
damage(true, damageAmount);
flag 매개변수
: method 의 기능을 전환하는 boolean 자료형의 매개변수
- flag 매개변수를 받는 메서드는 어떤 일을 하는 지 예측이 굉장히 힘듬
- 예측을 하기 위해서는 반드시 메서드 내부 로직을 확인해야 하므로 가독성이 낮아짐
6.6.1. method 분리하기
- flag 매개변수를 받는 method는 내부적으로 여러 기능을 수행하고 있으며, flag 를 사용해서 이를 전환하는 구조
- method 는 하나의 기능만 하도록 설계하는 것이 좋음
- 따라서 flag 매개변수를 받는 method 는 기능별로 분리하는 것이 좋음
6.6.2. 전환은 전략 패턴으로 구현하기
- flag 매개변수가 아니라 전략 패턴으로 구현해야 함
interface Damage {
void execute(final int damageAmount);
}
class HitPointDamage implements Damage {
...
}
class MagicPointDamage implements Damage {
...
}
enum DamageType {
hitPoint,
magicPoint,
}
void applyDamage(final DamageType damageType, final int damageAmount) {
final Damage damage = damages.get(damageType);
damage.execute(damageAmount);
}
7장. 컬렉션: 중첩을 제거하는 구조화 테크닉
7.1. 이미 존재하는 기능을 다시 구현하지 말기
boolean hasPrisonKey = false;
for (item each : items) {
if (each.name.equals('감옥 열쇠')) {
hasPrisonKey = true;
break;
}
}
- if 조건문이 중첩되어 있어서 가독성이 좋지 않음
boolean hasPrisonKey = items.stream().anyMatch(
item -> item.name.equals('감옥 열쇠')
);
anyMatch
method 를 알고 있으면, 복잡한 로직을 직접 구현하지 않아도 됨
- for 반복문을 사용해 collection 을 직접 조작하고 있다면, 잠시 멈추고 라이브러리에 같은 기능을 하는 method 가 있는지 확인
바퀴의 재발명 : 이미 널리 사용되고 있는 기술과 해결법이 존재하는데도, 이를 전혀 모르거나 의도적으로 무시하고 비슷한 것을 새로 만들어 내는 것
7.2. 반복 처리 내부의 조건 분기 중첩
for (Member member : members) {
if (0 < member.hitPoint) {
if (member.containsState(stateType.poison)) {
member.hitPoint -= 10;
if (member.hitPoint <= 0) {
member.hitPoint = 0;
}
}
}
}
7.2.1. 조기 Continue 로 조건 분기 중첩 제거하기
- continue 로 실행하고 있는 처리를 건너 뛰고, 다음 반복으로 넘어가는 제어 구조
for (Member member : members) {
if (member.hitPoint == 0) continue;
if (!member.containsState(StateType.poison)) continue;
member.hitPoint -= 10;
if (0 < member.hitPoint) continue;
member.hitPoint = 0;
}
7.2.2. 조기 break 로 중첩 제거하기
- 로직을 중단하고 반복문 전체를 벗어나는 제어 구문
int totalDamage = 0;
for (Member member : members) {
if (!member.hasTeamAttackSucceeded()) break;
...
}
7.3. 응집도가 낮은 collection 처리
- collection 처리도 응집도가 낮아지기 쉬움
class FieldManager {
void addMember(List<Member> members, Member newMember) {
if (members.size() == MAX_MEMBER_COUNT) {...}
members.add(newMember);
}
}
class SpecialEventManager {
void addMember(List<Member> members, Member newMember) {
members.add(newMember);
}
}
- collection 과 관련된 작업을 처리하는 코드가 여기 저기 구현될 가능성이 높음
- 응집도가 낮아짐
7.3.1. collection 처리를 캡슐화하기
- collection 과 관련된 응집도가 낮아지는 문제는
First Class Collection
pattern 을 사용해 해결할 수 있음
[First Class Collection]
- collection 과 관련된 로직을 캡슐화하는 디자인 패턴
- collection 자료형의 인스턴스 변수
- collection 자료형의 인스턴스 변수에 잘못된 값이 할당되지 않게 막고, 정상적으로 조작하는 메서드
class Party {
private final List<Member> members;
Party() {
members = new ArrayList<Member>();
}
Party add(final Member newMember) {
List<Member> adding = new ArrayList<>(members);
adding.add(newMember);
return new Party(adding);
}
}
- 원래의 members 를 변화시키지 않아 부수 효과를 막을 수 있음
7.3.2. 외부로 전달할 때 컬렉션의 변경 막기
- First class collection 으로 설계한 Party class 에서 멤버 전원을 참조하려 할 때
class Party {
List<Mbmer> members() {
return members;
}
}
- 인스턴스 변수를 그대로 외부에 전달하면 Party class 외부에서 마음대로 member 를 추가하고 제거할 수 있음
members = party.members();
members.add(newMember);
- 외부로 전달할 때는 collection 의 요소를 변경하지 못하게 막아두는 것이 좋음
class Party {
List<Member> members() {
return members.unmodifiableList();
}
}
- 따라서 Party class 외부에서 마음대로 collection 을 조작하는 상황 자체를 방지할 수 있음
8장. 강한 결합: 복잡하게 얽혀서 풀 수 없는 구조
coupling
: 모듈 사이의 의존도를 나타내는 지표
tightly coupling
: 이해하고 변경하기 힘든 코드
loose coupling
: 코드 변경이 쉬워짐
8.1. 결합도와 책무
class DiscountManager {
boolean add(Product product, ProductDiscount productDiscount) {
int discountPrice = getDiscountPrice(product.price);
...
}
static int getDiscountPrice(int price) {
int discountPrice = price - 3000;
...
}
}
class SummerDiscountManager {
...
boolean add(Product product) {
if (product.canDiscount) {
tmp = discountManager.totalPrice + discountManager.getDiscountPrice(product.price);
...
}
}
...
}
8.1.1. 다양한 버그
- 일반 할인 가격을 3000원에서 4000원으로
class DiscountManager {
...
static int getDiscountPrice(int price) {
int discountPrice = price - 4000;
...
}
}
- 그럼 해당 함수를 사용하는
SummerDiscountManager
에서도 로직이 변경됨
8.1.2. 로직의 위치에 일관성이 없음
- DiscountManager 가 상품 정보 말고도 너무 많은 일을 함
- Product 가 직접 해야 하는 유효성 검사 로직이 DiscountManager, SummerDiscountManager 에 구현되어 있음
- ProductDiscount.canDiscount, Product.canDiscount 의 이름이 유사해서 어떤 것이 어떤 상품인지 구분이 어려움
- SummerDicsountManager 에서 DiscountManager 의 로직을 활용함
8.1.3. Single Responsibility Principle
- 책임 : 자신이 해야 하는 일로서, 하지 않으면 안되는 임무
- 소프트웨어에서의 책임 : 자신의 관심사와 관련해서, 정상적으로 동작하도록 제어하는 것
8.1.4. SRP 로 발생하는 악마
- 할인되는 가격이 같다는 이유만으로 메서드를 하나만 만들어서 사용하면
- 한쪽 사양이 변경될 대 다른쪽 사양도 함께 변경되어 버그가 발생
- 상품명과 가격이 타당한지 판단하는 책임은 이 데이터를 갖고 있는 Product Class 가 원래 가지고 있어야 함
- 그런데 아무 일도 하지 않음 =
미성숙한 클래스
8.1.5. 책임이 하나가 되게 클래스 설계하기
- 상품의 가격을 나타내는 RegularPrice class 를 생성
- Money class 와 같은
Value Object
class RegularPrice {
final int amount;
...
}
class RegularDiscountPrice {
final int amount;
...
}
class SummerDiscountPrice {
final int amount;
...
}
- 가격이 분리되어 있어 서로 영향을 주지 않음
느슨한 결합
: 관심사에 따라 분리해서 독립되어 있는 구조
8.1.6. DRY 원칙의 잘못된 적용
RegularPrice
, RegularDiscountPrice
, SummerDiscountPrice
의 로직은 대부분 같음
중복 코드가 작성된 것은 아닐까?
라는 생각이 들지만
- 책무를 생각하지 않고 로직의 중복을 제거하면 안됨
- 그렇게 되면 하나로 모인 로직이 여러 책무를 담당해야 함
모든 지식은 시스템 내에서 단 한 번만, 애매하지 않고, 권위 있게 표현되어야 한다 - <<실용주의 프로그래머 20주년 기념판>>
지식
: 세분화된 정도, 기술 레이어 등 다양한 관점으로 생각 가능, 소프트웨어가 대상으로 하는 비즈니스 지식
비즈니스 지식
: 소프트웨어에서 다루는 비즈니스 개념
- 같은 로직 비슷한 로직이라도 개념이 다르면 중복을 허용해야 함
- 개념적으로 다른 것까지도 무리하게 중복을 제거하려면 강한 결합 상태가 됨
8.2. 다양한 강한 결합 사례와 대처 방법
8.2.1. 상속과 관련된 강한 결합
- 상속은 주의해서 다루지 않으면 곧바로 강한 결합 구조를 유발
- 이 책에서는 상속 자체를 권장하지 않음
Super class 의존
class PhysicalAttack {...}
class FighterPhysicalAttack extends PhysicalAttack {...}
- 처음에는 이 로직이 문제 없이 동작
- 하지만 super class 에 변화가 있을 때, override 한 class 에도 변화가 생기면서 예측하지 못하는 버그가 발생
- Sub class 는 Super class 에 굉장히 크게 의존
- 따라서 sub class 는 super class의 구조를 하나하나 신경써야함
- 일반적으로는 super class 는 sub class 를 생각하지 않고 개발
상속보다 composition
- Super class 의존으로 인한 강한 결합을 피하려면 상속보다 composition 을 사용
class FighterPhysicalAttack {
private final PhysicalAttachk physicalAttack;
...
int singleAttackDamage() {
return physicalAttack.singleAttackDamage() + 20;
}
}
상속을 사용하는 나쁜 일반화
- Sub class 가 Super class 의 로직을 그대로 사용
- Super class 가 공통 로직을 두는 장소로 사용
abstract class DiscountBase {
int getDiscountPrice() {
...
}
}
class RegularDiscount extends DiscountBase {...}
class SummerDiscount extends DiscountBase {...}
getDiscountPrice
는 두 가지 책임을 지게 됨
- SRP 를 구현하는 좋은 구현이라고 말할 수 업음
- 더 좋지 않은 코드는 Super class 에서 Sub class 를 아는 것
abstract class DiscountBase {
int getDiscountPrice() {
if (this instanceof RegularDiscount) {...}
else if (this instanceof SummerDiscount) {...}
}
}
- 상속 받는 쪽에서 차이가 있는 로직만 구현하는 Template method 라는 디자인 패턴이 존재
- 하지만 상속은 강한 결합과 로직 분산 등 여러 악마들을 불러들임
- 신중하게 사용해야 함
8.2.2. 인스턴스 변수별로 클래스 분할이 가능한 로직
class Util {
void cancelReservation() {...}
void darkMode() {...}
void beginSendMail() {...}
}
- method 모두 책임이 완전히 다름 하지만 모두 Util 이라는 클래스 안에 존재
- 따라서 Util class 를 각 3개의 class 로 분리해야함
class Reservation {...}
class ViewCustomizing {...}
class MailMagazineService {...}
8.2.3. 특별한 이유 없이 public 사용하지 않기
- public 을 사용하면 강한 결합 구조가 됨
- 관곌ㄹ 맺지 않았으면 하는 클래스끼리도 결합되어, 영향 범위가 확대
- 결과적으로 유지 보수가 어려운 강한 결합 구조가 되고 맘
8.2.4. private 메서드가 너무 많다는 것은 책임이 너무 많다는 것
class OrderService {
private int calcDiscountPrice(int price) {...}
private List<Product> getProductBrowsingHistory(int userId) {...}
}
- 최근 본 상품 리스트 확인은 주문과는 다름
- 따라서 책임이 다른 메서드는 다른 클래스로 분리하는 것이 좋음
8.2.5. 높은 응집도를 오해해서 생기는 강한 결합
- 기능이 늘면서 클래스의 규모가 커짐에 따라 발생하는 강한 결합
class SellingPrice {
int calcSellingCommission() {...}
int calcDeliveryCharge() {...}
int calcShoppingPoint() {...}
}
- SellingPrice 에 모아서 놓음 > 응집도는 높으나, 필요 없는 method 가 포함 > 강한 결합
- 응집도가 높다는 개념을 염두해 두고, 관련이 깊다고 생각되는 로직을 한 곳에 모으려고 했지만,
- 결과적으로 강한 결합 구조를 만드는 상황은 매우 자주 일어남
결합이 느슨하고 응집도가 높은 설계
class SellingCommission {...}
class DeliveryCharge {...}
class ShoppingPoint {...}
8.2.6. Smart UI
- 화면 표시와 직접적인 관련이 없는 책무가 구현되어 있는 클래스
- Smart UI 는 화면 표시에 관한 책무와 그렇지 않은 책무가 강하게 결합되어 있기 때문에, 변경하기가 아주 힘듬
8.2.7. 거대 데이터 클래스
- 수많은 인스턴스 변수가 존재
- 작은 데이터 클래스보다 훨씬 더 많은 악마를 불러들이므로 주의
- 다양한 데이터를 가지므로, 수많은 UseCase 에서 사용
- 결국 전역 변수와 같은 성질을 띄게 됨 (ex) 동기화하느라 성능이 저하되는 등)
8.2.8. Transaction Script Pattern
- 메서드 내부에 일련의 처리가 하나하나 길게 작성되어 있는 구조
데이터를 보유하고 있는 class
와 데이터를 처리하는 class
를 나누어 구현할 때 자주 보임
- 이를 남용하면 메서드 하나가 길게는 수백 줄의 거대한 로직을 갖게 됨
- 응집도는 낮아지고 결합은 강해짐
8.2.9. God Class
- 하나의 클래스 내부에 수천에서 수만 줄의 로직을 담고 잇음
- 수많은 책임을 담당하는 로직이 난잡하게 섞여 있는 클래스
- 어떤 로직과 관련 있는지, 책무를 파악하기가 굉장히 힘듬
- 기능을 수정하려면, 영향 범위를 파악하기 위해 수천 수만 줄의 로직을 읽어야 함
- 영향 범위 확인 시 놓치는 부분이 생기기 쉽고, 버그 발생이 쉬움
- 최악의 상황에는 아예 작성되어 있지 않은 경우도 있음
8.2.10. 강한 결합 클래스 대처 방법
- 객체 지향 설계와 단일 책임 원칙에 따라 제대로 설계하는 것
- 거대한 강한 결합 클래스는 책임별로 클래스를 분할해야 함
9장. 설계의 건전성을 해치는 여러 악마
9.1. Dead Code
Dead Code
, Unreachable code
: 절대로 실행되지 않는 조건 내부에 있는 코드
- 코드의 가독성을 떨어뜨림
- 언젠가 버그가 될 가능성이 높음
9.2. YAGNI 원칙
You Aren't Gonna Need It
- 미래를 예측하고 미리 만들어두면 안됨
- 지금 필요 없는 기능은 만들지 말자
- 소프트웨어 대한 요구는 매일 매일 변함
- 사양으로 확정되지 않고 명확하게 언어화되지 않은 요구를 미리 에측하고 구현해도, 이러한 예측은 대부분 맞지 않음
- 예측에 들어맞지 않는 로직은 Dead Code 가 됨
9.3. Magic Number
Magic Number
: 로직 내부에 직접 작성되어 있어서, 의미를 알기 힘든 숫자
- 설명이 없는 숫자는 개발자를 혼란스럽게 만듬
- 따라서 상수로 변경하고 커밋
9.4. 문자열 자료형에 대한 집착
- 하나의 String 변수에 여러 값을 쉼표로 구분해서 저장
- 기본 자료형에 대한 집착처럼 클래스 뿐만 아니라 변수마저 추가하지 않으려는 경향
- 의미가 다른 값은 각각 다른 변수에 저장하는 것이 좋음
9.5. 전역 변수
- 모든 곳에서 접근할 수 있는 변수
- java 언어 사양에는 전역 변수가 없음
public static
으로 선언하면 모든 곳에서 접근 가능
- 여러 로직에서 전역 변수를 참조하고 값을 변경하면, 어디에서 어떤 시점에서 값을 변경했는지 파악하기 힘듬
- 전역 변수를 참조하고 있는 로직을 변경해야 할 때, 해당 변수를 참조하는 다른 로직에서 버그가 발생하는지 검토 필요
- 동기화가 필요한 경우 DeadLock 에 빠질 수 있음
- 동기화를 하고 싶은 인스턴스 변수가 하나뿐이라고 해도, 해당 인스턴스의 다른 인스턴스 변수까지 모두 잠그므로, 성능상 문제가 큼
전역 변수를 직접적으로 사용하지 않더라도, 전역 변수와 같은 개념을 알게 모르게 사용
9.5.1. 영향 범위가 최소화되도록 설계하기
- 전역 변수는 영향 범위가 너무 넓음
- 영향 범위가 가능한 한 되도록 좁게 설계 하는 것이 필요
- 관계없는 로직에서는 접근할 수 없게 설계
- 그래도 전역변수를 사용하고 싶다면, 정말로 필요한지 검토 필요
9.6. null 문제
- null 이 들어갈 수 있다고 전제하고 로직을 만들면, 모든 곳에서 null 체크 필요
- 결국 null 체크 코드가 너무 많아져서, 가독성이 떨어짐
- 실수로 null 체크를 안하면 곧바로 버그가 됨
null 이 뭘까?
- 초기화하지 않은 메모리 영역에서 값을 읽으면 문제가 생김
- 이를 피하기 위해 null이 발명
- 메모리 접근과 관련된 문제를 방지하기 위한 최소한의 구조
- 즉 잘못된 처리 를 의미
9.6.1. null을 리턴/전달하지 말기
- null 을 리턴하지 않는 설계
- null 을 전달하지 않는 설계
9.6.2. null 안전
- null 에 의한 오류가 아예 발생하지 않게 만드는 구조
null 안전 자료형
: null 을 아예 저장할 수 없게 만드는 자료형
val name: String = null
9.7. 예외를 catch 하고서 무시하는 코드
try {
reservations.add(product);
} catch (Exception e) {
}
- catch 해놓고 별다른 처리를 하고 있지 않음
9.7.1. 원인 분석을 어렵게 만듦
- 이러한 코드의 문제는 오류가 나도, 오류를 탐지할 방법이 없어진다는 것
- 예외를 catch 하고서도 무시하고 있으므로, 어느 시점에 어떤 코드에서 문제가 발생했는지 찾기 힘듬
9.7.2. 문제가 발생했다면 소리치기
- 잘못된 상태에서 어떠한 관용도 베풀어서는 안됨
- 잘못된 상태에서 계속해서 처리를 진행하는 것은 도화선에 불이 붙는 폭탄을 들고 돌아다니는 것과 같음
- 예외를 확인했다면 곧바로 통지, 기록하는 것이 좋음
Meta Programming
: 프로그램 실행 중에 해당 프로그램 구조 자체를 제어하는 프로그래밍
Reflaction Programming
: 프로그램에서 임의의 클래스에 접근할 수 있는 기능
9.8.1. Reflaction 으로 인한 Class 구조와 값 변경 문제
class Level {
private static final int MIN = 1;
private static final int MAX = 99;
final int value;
private Level(final int value) {...}
static level initialize() {...}
Level increase() {...}
...
}
Level level = Level.initialize();
Field field = Level.class.getDeclaredField('value');
field.setInt(999);
- 잘못된 값이 들어갈 수 있음
- Reflaction 을 남용하면, 뒷문을 열어 놓은 것과 같음
9.8.2. 자료형의 장점을 살리지 못하는 Hard Coding
- statis type 언어는 정적 분석으로 정확한 코드 분석이 가능하다는 장점이 있음
- meta programming 은 이러한 장점 조차 무너뜨림
- class instance 를 만들 때 일반적으로 new keyword 를 사용
- reflaction 을 사용하면, Meta 정보를 기반으로 instance 생성 가능
static Object generateInstance(String packageName, String className) throw Exception {
Class klass = Class.forName(packageName + '.' + className);
Constructor constructor = klass.getDeclaredConstructor();
}
- IDE 의 정적 분석을 사용하면, 어떤 클래스가 어디에서 참조되고 있는지 정확하게 분석할 수 있음
- 그런데 단순히 하드코딩 되어 있다면 알 수 없음
Employee user = (Employee)generateInstance("customer", "User");
- IDE 정적 분석 기능은 이름 변경 이외에도 정의한 위치로 점프, 참조하고 있는 위치 전체 검색 등 개발의 효율성과 정확성 향상에 도움을 줌
9.8.3. 단점을 이해하고 용도를 한정해서 사용하기
- Meta Programming 을 사용하면, 뭔가 특별한 능력을 배운 것만 같지만, 단점을 무조건 잘 이해해야 함
9.9 기술 중심 Packaging
- Package 를 구분할 때도 폴더를 적절하게 나누지 않으면 악마를 부를 수 있음
기술 중심 패키징
- 구조에 따라 폴더와 패키지를 나누는 것
- 비즈니스 클래스를 기술 중심 패키징에 따라 폴더를 구분하면 관련성을 알기 매우 힘듬
ex) MVC 에 따라서 폴더 구조를 변경
- 비즈니스 개념을 중심으로 폴더를 구분하는 것이 좋음
- 관련된 개념끼리 모여 있음
9.10. Sample Code 복사해서 붙여넣기
- framework 마다 공식 사이트가 있음
- 각 사이트의 샘플 코드를 그대로 복사하고 붙여 넣으면, 좋지 않은 구조가 되기 쉬움
- 따라서 샘플 코드는 참고만!
9.11. Silver Bullet
- 새로운 기술과 방법을 익히면, 곧바로 써보고 싶어짐
- 새로운 기술은 개발 현장의 모든 문제를 해결해 줄 것처럼 보임
- 하지만 현실에서 발생하는 문제는 특정 기술 하나로 해결할 수 있을 정도로 단순하지 않음
- 어떤 문제가 있을 때, 어떤 방법이 해당 문제에 효과적인지, 비용이 더 들지는 않는지 평가하고 판단하는 자세
- 문제와 목적을 머릿속에 새겨두고 적절한 기술을 선택할 수 있도록 노력해야 함
Best 가 아니라 Better 를 목표로 해야함
10장. 이름 설계: 구조를 파악할 수 있는 이름
10.1. 악마를 불러들이는 이름
- 쇼핑몰에서 상품이라는 클래스를 만들면, 영향 범위가 너무 넓어 생산성이 저하될 수 있음
10.1.1. Seperation of concerns
- 강한 결합을 해소하고, 결합이 느슨하고, 응집도가 높은 구조
- 관심사에 따라서 각각 클래스로 분할해야 함
10.1.2. 관심사에 맞는 이름 붙이기
- 모든 상품을 그냥 상품이라고 하지 않고, 각각 종류에 따라 따로 클래스를 만듬
ex) 예약 상품, 주문 상품, 재고 상품, 발송 상품
10.1.3. 포괄적이고 의미가 불분명한 이름
목적 불명 객체
: 이름이 너무 포괄적이라서 목적이 불분명한 클래스
10.2. 이름 설계하기 - 목적 중심 이름 설계
이름 설계
: 클래스와 메서드에 이름을 붙이는 것
설계
: 어떤 문제를 해결하기 위한 구조를 생각하거나 만들어 내는 것
- 이름이란? 관심사 분리를 생각하고, 비즈니스 목적에 맞게 이름을 붙이는 것
- 결합이 느슨하고 응집도가 높은 구조를 만드는 데 굉장히 중요
[목적 중심 이름 설계 방법]
- 최대한 구체적이고, 의미 범위가 좁고, 특화된 이름 선택하기
- 존재가 아니라 목적을 기반으로 하는 이름 생각하기
- 어떤 관심사가 있는지 분석하기
- 소리 내어 이야기해보기
- 이용 약관 읽어 보기
- 다른 이름으로 대체할 수 없는지 검토하기
- 결합이 느슨하고 응집도가 높은 구조인지 검토하기
10.2.1. 최대한 구체적이고, 의미 범위가 좁고, 특화된 이름 선택하기
- 이름과 관계 없는 로직을 배제하기 쉬움
- 클래스가 작아짐
- 관계된 클래스 개수가 적으므로, 결합도가 낮아짐
- 관계된 클래스 개수가 적으므로, 사양 변경 시 생각해야 하는 영향 범위가 좁음
- 목적에 특화된 이름을 갖고 있으므로, 어떤 부분을 변경해야 할 때 쉽게 찾을 수 있음
- 개발 생산성이 향상됨
10.2.2. 존재가 아니라 목적을 기반으로 하는 이름 생각하기
- 주소이라고 클래스를 지으면 존재 기반 이름이 됨
- 그런데 발송지 인지 배송지 인지 법인 주소 인지 등을 구분하면 목적 기반 이름이 됨
10.2.3. 어떤 비즈니스 목적이 있는지 분석하기
- 비즈니스 목적에 특화된 이름을 만들려면, 어떤 비즈니스를 하는지 모두 파악해야 함
- 소프트웨어가 추구하는 목적과 내용을 분석해야 함
10.2.4. 소리 내어 이야기해 보기
- 어떤 목적을 달성하고 싶은지, 어떤 형태로 사용하고 싶은지, 서로 어떤 관련이 있는지 등
- 배경과 의도를 함께 정리하고 이를 팀과 소통해서 일치시키는 것이 중요
- 비즈니스 측면을 잘 이해하고 있는 사람과 이야기를 하면 바로 피드백을 받을 수 있음
고무 오리 디버깅
: 프로그래밍에서 어떤 문제를 발견햇을 때, 문제를 누군가에게 설명하다보면 스스로 원인을 깨닫고 해결할 수 있음
- 적극적으로 이야기하면서, 대화 속에서 사용되는 이름에 주의를 기울임
ubiquitous language
: 팀 전체에서 의도를 공유하기 위한 언어
- 같은 의도를 갖고 이름을 대화, 문서, 클래스 이름, 메서드 이름 등에 활용하면, 설계에서 발생하는 여러 문제들을 해결할 수 있음
10.2.5. 이용 약관 읽어보기
- 명확하고 구체적인 단어들이 나열되어 있어 판단하는 데 도움이 됨
10.2.6. 다른 이름으로 대체할 수 없는지 검토하기
- 사용자 > 관리인, 고객 > 투숙객, 결제자
- 사전에 유의어를 확인해 보는 것도 좋음
10.2.7. 결합이 느슨하고 응집도가 높은 구조인지 검토하기
- 목적에 특화된 이름을 선택하면, 목적 이외의 로직을 배제하기 쉬워짐
- 목적과 관련된 로직이 모여 있으므로, 응집도가 높아짐
- 다른 클래스 몇 개와 관련이 있는지 개수를 확인
- 너무 많은 클래스에 관련되어 있다면 좋지 않은 징조
강한 결합 상태
일 수 있음
10.3. 이름 설계 시 주의사항
10.3.1. 이름에 관심 갖기
- 팀 개발에서는 이름이 중요
- 이름과 로직이 대응된다는 전제, 이름이 프로그램 구조를 크게 좌우 한다는 중요성을 공유해야 함
10.3.2. 사양 변경 시 '의미 범위 변경' 경계하기
- 여러 의미가 섞이면 이름이 의미하는 바를 다시 검토해야 함
- 이름을 변경하거나 클래스를 나누어야 함
10.3.3. 대화에는 등장하지만 코드에 등장하지 않는 이름 주의하기
- 대화에는 자주 등장하는 개념이 소스 코드에는 이름조차 붙어있지 않고, 잡다한 로직에 묻혀 있는 경우가 많음
- 만약 이를 코드에 반영하지 않는다면, 로직을 찾는 일이 굉장히 힘들어짐
- 대화에서 등장하는 이름을 신경써서 개발하는 것이 필요
10.3.4. 수식어를 붙여서 구별해야 하는 경우 클래스로 만들어보기
- 차이를 구분하기 어려운 코드를 단순하게 수식어를 붙여서 동료에게 설명하는 상황은 시스템 개발에서 매우 흔하게 볼 수 있음
int maxHitPoint = member.maxHitPoint + accessory;
- 이 코드를 보면
maxHitPoint
가 있다는 것은 알 수 있지만, 어떠한 최대 hit point 인지는 알 수 없음
- 따라서 장비를 착용했을 때의 최대 hit point 라는 것을 명확하게 명시해야 함
int corredtedMaxHitPoint = originalMaxHitPoint + accessory;
- 하지만 int 로 계속 구현하면 의미 차이를 이름으로만 확인해야 함
- 또한 여러 관련 구현이 이곳 저곳에 퍼져 응집도가 낮은 구조가 됨
- 수식어를 붙이면서 차이를 나타내고 싶을 때 > 클래스로 설계 하는 것이 좋음
class OriginalMaxHitPoint {...}
- 의미가 다른 개념들을 서로 다른 클래스로 설계해서 구조화하면, 개념 사이의 관계를 이해하기 쉬움
10.4. 의미를 알 수 없는 이름
int tmp3 = tmp1 - tmp2;
if (tmp3 < tmp4) {
tmp3 = tmp4;
}
- 이 로직을 보고 목적인 무엇인지 파악하기 굉장히 어려움
- 목적 중심 이름 설계의 관점에서 보았을 때, 관심사 분리에 아무런 도움이 되지 않음
- 책무를 알 수 없으므로, 강한 결합 구조가 되기 쉬움
10.4.1. 기술 중심 명명
MemoryStateManager
, changeIntValue01
와 같은 이름을 사용하면, 이름의 의도를 알기 어려움
- Embedded 처럼 HW 와 가까운 layer 의 middleware 에서는 memory 와 processor 에 직접 접근하는 로직이 많아, 어쩔 수 없이 기술 중심 명명을 해야 함
10.4.2. 로직 구조를 나타내는 이름
class Magic {
boolean isMemberHpMoreThanZeroAndIsMemberCanActAndIsMemberMpMoreThanMagicCostMp(Member member) {...}
}
- method name 이 로직 구조를 그대로 드러내고 있음
- 무엇을 의도하는지 메서드 이름만 보고 알기 힘듬
- 의도와 목적을 이해하기 쉽게 이름을 붙이기
class Magic {
boolean canEnchant(final Member member) {...}
}
10.4.3. Princople of least astonishment, Rule of least suprise
int count = order.itemCount();
- 그냥 item count 만 조회한다고 생각했는데
class Order {
...
int itemCount() {
int count = items.count();
if (10 <= 10) {
giftpoint = giftPoint.add(new GiftPoint(100));
}
return count;
}
}
- gift point 까지 추가하는 로직이 담겨 있었음
- 만약 실제로 이렇게 동작하고 있는 것을 알면 놀랄 수 있음
- 로직을 변경할 때는 항상 이 원칙을 신경써야 함
- 로직과 이름 사이에 괴리가 있다면 이름을 수정하거나, 메서드와 클래스를 의도에 맞게 따로 만들기
10.5. 구조에 악영향을 미치는 이름
10.5.1. 데이터 클래스처럼 보이는 이름
class ProductInfo {
...
}
Info
, Data
같은 이름의 클래스는 읽는 사람에게 데이터만 갖는 클래스니까 로직을 구현하면 안되는 구나 라는 이미지를 심을 수 있음
- 이렇게 되면 응집도가 낮은 구조가 되기 쉬움
ProductInfo
> Product
로 개선하는 것이 좋음
DTO (Data Transfer Object)
- 예외적으로 데이터 클래스를 사용하는 경우
- CQRS (Command and Query Repsonsibility Segregation) Architecture Pattern
- 참조 책무 : Database 에서 값을 추출하는 처리, 단순히 값을 추출
- 데이터 전송 용도로 사용되는 디자인 패턴
- 값을 변경하는 용도로 사용하면 안됨
- Data Class 를 절대 사용해서는 안된다는 것이 아니라, 의도를 이해하고 상황에 맞게 사용해야 한다는 것
10.5.2. 클래스를 거대하게 만드는 이름
- 대표적인 이름으로는
Manager
(ex) MemberManager)
- Manager 라는 이름이 붙은 클래스는 엄청나게 많은 책무를 가질 수 있음
- 즉 SRP (Single Reponsibility Principal) 을 위반
- 이러한 문제가 생겨난 원인은 Manager 라는 단어가 가진 의미가 너무 넓고 애매
- 따라서 좁은 개념을 찾는 것이 좋음
- 마찬가지로 Processor 와 Controller 같은 이름도 주의해야 함
- MVC 에서 Controller 는 전달 받는 요청 매개변수를 다른 클래스에 전달하는 책무만 가져야 함
- 금액을 계산하거나, 예약 여부를 판단하는 등의 분기 로직 구현 시 SRP 위반
- 책무가 다른 로직은 다른 클래스로 정의가 필요
10.5.3. 상황에 따라 의미가 달라질 수 있는 이름
ex)
- 배송 context : 자동차가 화물로 배송되는 context
- 판매 context : 딜러에 의해 고객에게 판매되는 context
- Context 의 차이를 생각하지 않고 Class 를 설계하면 두 개의 개념이 뒤섞임
- Context 가 다르다면 서로 다른 패키지로 선언
- 느슨하게 결합하면, 한 쪽을 변경해도 다른 쪽에는 영향을 주지 않음
10.5.4. 일련 번호 명명
- 클래스와 메서드의 이름에 번호를 붙여서 만드는 것
- 일련 번호라는 질서를 유지하기 위해 기능을 추가할 때 기존의 메서드에 로직을 추가하기 쉬움
10.6. 이름을 봤을 때, 위치가 부자연스러운 클래스
10.6.1. 동사 + 목적어 형태의 메서드 이름 주의하기
class Enemy {
...
void escape() {...}
void consmeMagicPoint(int costMagicPoint) {...}
boolean addItemToParty(list<Item> items) {...}
}
- Enemy class 의 관심사는 적인데,
addItemToParty
은 주인공의 데이터를 다룸
- 관심사가 다른 메서드는
addItemParty
처럼 동사 + 목적어
형태가 되는 경향이 있음
동사 + 목적어
로 이루어진 이름은 관계없는 책무를 가진 메서드일 가능성이 있음
10.6.2. 가능하면 메서드의 이름은 동사 하나로 구성되게 하기
- 관심사가 다른 메서드가 섞이지 못하게 막으려면 되도록 메서드 이름이 동사 하나로 구성되도록 설계하는 것이 좋음
class PartyItems {
...
PartyItems add(final Item newItem) {...}
}
10.6.3. 부적절한 위치에 있는 boolena method
- boolean 을 리턴하는 method 도 적절하지 않은 class 에 정의되어 있는 경우가 많음
class Common {
static boolean isMemberInConfusion(Member member) {...}
}
- method 가 정의된 위치가 적절한지 관심사 단위로 생각해보면 좋음
isMemberInConfusion
method 가 Common 에 있는 것이 아니라 Member class 에 정의되는 것이 자연스러움
- method 가 정의되어 있는 class 위치가 적절한지 쉽게 확인할 수 있는 방법
- boolean 자료형의 method 는
is
, has
, can
형태의 이름이 붙는 경우가 ㅏㅁㄴㅎ음
- 다음 형태로 바꾸었을 때 위화감이 없으면 좋은 것
클래스 is 상태
예를 들어
- Common is member in confusion
- Member is in confusion (자연스러움)
class Member {
...
boolean isInConfusion() {...}
}
10.7. name abbreviation
10.7.1. 의도를 알수 없는 축약
- 긴 이름이 싫어서 이름을 축약하는 경우가 있음
- 이름이 축약되면 의도를 이해하기 힘듬
int trFee = brFree + LRF * dod;
- Rental Total Fee 를 계산하는 계산식
- 하지만 주석과 문서가 없다면 주변의 로직을 읽어서 무엇을 계산하는지 유추 해야 함
10.7.2. 기본적으로 이름은 축약하지 말기
- 조금 귀찮더라도 이름은 축약하지 말고 모두 쓰기를 바람
- 하지만 관습적으로 축약하는 경우에는 사용해도 괜찮음
10.7.3. 이름을 축약할 수 있는 경우
- 축약을 어느 정도 허용할 것인가 에 관해서는 굉장히 다양한 관점이 있음
- 최대한 축약하지 말고 의도를 명확하게 전달하는 것이 중요
11장. 주석: 유지 보수와 변경의 정확성을 높이는 주석 작성 방법
11.1. 내용이 낡은 주석
class Memter {
...
boolean isPainful() {
if (states.contains(StateType.poison) ||
states.contains(StateType.paralyzed) ||
states.contains(StateType.fear)) {
return true;
}
return false;
}
}
- 주석과 로직이 다름
- 코드에 비해 주석을 유지 보수 하는 것이 어렵기 때문
- 주석이 낡아버리지 않게 구현을 변경할 때 주석도 함께 변경하는 것이 좋음
11.1.1. 주석은 실제 코드가 아님을 이해하기
- 주석의 설명도 실제 코드가 아니므로, 실제 내용을 100% 전달 할 수 없음
- 최대한 의도가 제대로 전달될 수 있게 class 와 method 의 이름을 짓고 주석을 달아야 함
11.1.2. 로직의 동작을 설명하는 주석은 낡기가 쉬움
- 코드의 동작을 그대로 설명하는 주석은 코드를 변경할 때마다 주석도 변경해야 함
- 로직을 그대로 설명하는 주석은 코드를 이해하는 데 별다른 도움이 되지 않음
11.2. 주석 때문에 이름을 대충 짓는 예
class Member {
...
boolean isNotSleepingAndIsNotParalyzedAndIsNotConfusedANdIsNotStoneAndIsNotDead() {...}
}
- 메서드의 이름만으로는 이러한 의도를 전달하기 힘듬
- 의도를 전달하기 힘든 메서드에는 의미를 다시 설명하는 주석을 달기 쉬움
- 따라서 메서드 자체의 이름을 수정하면 좋음
class Member {
...
boolean canAct() {...}
}
- method 의 가독성을 높이면 주석으로 설명을 추가하지 않아도 됨
11.3. 의도와 사양 변경 시 주의 사항을 읽는 이에게 전달하기
- 코드는 언제 읽힐까? 기본적으로 유지보수 할 때와 사양을 변겨알 때 읽힘
- 코드를 유지 보수 시 읽는 사람이 주의를 기울여야 하는 부분은
이 로직은 어떤 의도를 갖고 움직이는가
- 사양을 변경할 때 읽는 사람이 주의를 기울여야 하는 부분은
안전하게 변경하려면 무엇을 주의해야 하는가
- 따라서 주석이 이러한 내용이 담기면 좋음
11.4. 주석 규칙 정리
11.5. 문서 주석
문서 주석
: 특정 형식에 맞춰 주석을 작성하면, API 문서를 생성해주거나 코드 에디터에서 주석의 내용을 팝업으로 표시해주는 기능
12장. method(함수): 좋은 class 에는 좋은 method 가 있다
12.1. 반드시 현재 class 의 instance 변수 사용하기
- 인스턴스 변수를 안전하게 조작하도록 메서드를 설계하면, 클래스 내부가 정상적인 상태인지 보장 가능
- 메서드는 반드시 현재 클래스의 인스턴스 변수를 사용하도록 설계
- 인스턴스는 생성해야지 변경하려고 하면 좋지 않음
12.2. 불변을 활용해서 예상할 수 있는 method 만들기
- 가변 인스턴스 변수 등을 변경하는 메서드는 의도하지 않게 다른 부분에 영향을 줄 수 있음
- 이렇게 되면 예상하지 못한 동작이 발생할 수 있으며, 유지보수가 어려워짐
- 불변을 활용해서 예상치 못한 동작 자체를 막을 수 있게 설계하면 좋음
12.3. 묻지 말고 명령하라
- 어떤 class 가 다른 class 의 상태를 판단하거나, 상태에 따라 값을 변경하는 등
- 다른 class 를 확인하고 조작하는 method 구조는 응집도가 낮은 구조
ex) instance 변수 값을 추출하는 메서드 getter
, 값을 설정하는 메서드 setter
- getter, setter 는 다른 클래스를 확인하고 조작하는 메서드 구조가 되지 쉬움
- 개발 생산성이 좋지 않은 소프트웨어의 소스 코드에서 자주 볼 수 있음
12.4. CQS (Command/Query Seperation)
int gainAndGetPoint() {
point += 10;
return point;
}
- 상태와 변경과 추출을 동시에 하는 method 는 여러 문제의 원인이 됨
- 사용자도 쓰기 힘든 method 가 됨
void gainPoint() {
point += 10;
}
int getPoint() {
return point;
}
12.5. parameter
12.5.1. immutable parameter 로 만들기
- 매개 변수를 변경하면 값의 의미가 바뀌어서 어떤 의미를 나타내는지 유추하기 어려움
- 또한 어디서 변경되었는지 찾기도 힘듬
- 매개변수에
final
수식자를 붙여서 불변으로 만드는 것이 좋음
12.5.2. Flag parameter 사용하지 않기
- 코드를 읽는 사람이 method가 무슨 일을 하는지 알기 어렵게 만듬
- 무슨 일을 하는지 이해하려면, method 의 내부 로직을 확인해야 하므로 가독성이 낮아짐
12.5.3. null 전달하지 않기
- null 을 활용하는 로직은 NullPointException 이 발생할 수 있으므로, 매개변수로 null 을 전달하지 않게 설계하는 것이 필요
- null 을 전달하지 않게 설계하려면, null 에 의미를 부여해서는 안됨
12.5.4. 출력 parameter 사용하지 않기
- 출력 매개변수를 사용하면 응집도가 낮은 구조가 만들어짐
- 매개변수는 입력 값으로 사용하는 것이 기분
- 매개변수를 출력값으로 사용하면, 코드를 읽는 사람에게 혼란을 줄 수 있음
12.5.5. Parameter 는 최대한 적게 사용하기
- method 에 parameter 가 많다는 것은 method 가 여러 가지 기능을 처리한다는 의미
- method 가 처리할 게 많아지면, 그만큼 로직이 복잡해져 이는 다양한 악마를 불러들임
12.6. Return Value
12.6.1. 자료형을 사용해서 Return value 의 의도 나타내기
class Price {
...
int add(final Price other) {
return amount + other.amount;
}
}
- int 형 처럼 단순한 기본 자료형으로는 Return Value 의 의미를 호출하는 쪽에 전달할 수 없음
- 따라서 기본 자료형을 사용하지 말고, 독자적인 자료형을 사용해서 의도를 명확하게 나타내는 것이 좋음
class Price {
Price add(final Price other) {
final int added = amount + other.amount;
return new Price(added);
}
}
12.6.2. null Return 하지 않기
12.6.3. 오류는 Return Value 로 Return 하지말고 Exception 을 일으키기
class Location {
...
Location shift(final int shiftX, final int shiftY) {
...
return new Location(-1, -1);
}
}
- 오류 값으로 Location(-1, -1) 을 리턴한다는 사실을 호출자가 알고 있어야 함
- 아니라면 후속 로직에서 정상 값처럼 사용되어 버그가 생길 수 있음
- double meaning : 어떤 값으로 여러 의미를 나타내는 것
- Location(-1, -1) 은 오류로 다루는 중의적인 의미
- Return Value 로 오류 값을 리턴하지 말고 곧바로 예외를 발생시켜야 함
class Location {
...
Locaion(final int x, final int y) {
if (!valid(x, y)) {
throw new IllegalArgumentException();
}
}
}
13장. 모델링: 클래스 설계의 토대
13.1. 악마를 불러들이기 쉬운 User Class
- User Class 는 사양 변경이 굉장히 잦아서 여러 가지 문제를 일으키기 쉬움
- User 는 하나인데 여러 목적을 가지기 때문에, 많은 오류가 날 수 잇음
13.2. Modeling 으로 접근해야 하는 구조
13.2.1. System 이란?
- 수많은 구성 요소로 이루어진 집합체로서 각가의 부분이 유기적으로 연결되어, 전체적으로 하나의 목적을 갖고 움직이는 것
ex) 사람은 이족 보행 시스템, 음파를 통한 회화 시스템을 따름
정보 시스템
: 시스템 중에서 컴퓨터를 활용하는 시스템
13.2.2. System structure & Modeling
Model
- 이러한 시스템의 구조를 설명하기 위해 사용
- 시스템 구조를 설명하기 위해 단순한 상자로 도식화한 것
Modeling
- Model 의 의도를 정의하고 구조를 설계하는 것
- System 은 목적을 달성하기 위한 수단
- Model 은 System 의 구성요소 / 목적을 달성하기 위한 수단의 일부를 개념화 한 것
Model? 특정 목적을 달성하기 위해 최소한으로 필요한 요소를 갖춘 것
13.2.3. Software 설계와 Modeling
ex) Online Shopping mall : 상품 매매를 시스템화 한 것
- 온라인 쇼핑몰은 매매가 효율적으로 이루어지게 도와줌
- 온라인 쇼핑몰 덕분에 집에서도 상품을 구매할 수 있음
- 그럼 상품을 모델로 나타내면 어떻게 될까?
- 정보를 모두 포함하면, 모델의 목적을 알 수 없음
Model
: 특정 목적 달성을 위해서 최소한으로 필요한 요소를 갖춘 것
- 주문과 배송은 달성해야 하는 목적이 다름
- 즉 목적에 따라 상품의 모델이 달라짐
13.3. 안 좋은 모델의 문제점과 해결 방법
- User 의 목적은 무엇일까요? 개인 프로필과 관련된 요소
- 법인 등록 번호는 개인 프로필과 관련되지 않음
일관성이 없는 모델
: User Class 는 여러 목적에 무리하게 사용되고 있으며, Modeling 된 것처럼 보이지만 Modeling 되어 있지 않음
- 설계 품질 문제가 될 때 이유를 확인해 보면 Modeling 이 제대로 되어 있지 않아 그저 작동만 하는 코드로 작성되어 있는 경우가 많음
13.3.1. User 와 System 의 관계
- User 가 무엇인가? = 사용자는 무엇인가? = 사용자는 무엇을 사용하는가?
- 사용자는 System 을 사용 =
System User
[UML > System Usecase Diagram]
actor
: System 사용자, System 의 바깥에 있음
사각형
: System
- 시스템 내부에 User 가 포함되는 것은 조금 부자연스러움
- 정보가 안과 밖에 얽혀 있는 관계를 해소하려면, 정보 시스템만이 갖는 특징을 활용해야 함
13.3.2. 가상 세계를 표현하는 정보 시스템
- 정보 시스템의 기반 = 컴퓨터 = 0과 1 bit 로 구성되는 세계
- 물리적인 것이 아니라 개념적인 사항
정보 시스템이란? 현실 세계에 있는 개념만을 컴퓨터 세계에 투영하는 가상 현실
13.3.3. 목적별로 모델링하기
- 각각의 매체는 목적에 따라 표현 방법과 이름이 다름
- 따라서 User 또한 목적에 따라 표현이 다를 수 밖에 없음
Remind! Model? 특정 목적 달성을 위해, 최소한으로 필요한 요소를 갖춘 것
- Github 사용자의 목록을 보면 목적 별로 나누어져 있음
- 정보 시스템에서는 현실 세계에서 있는 물리적인 존재와 정보 시스템에 있는 모델이 무조건 일대일 대응이 되지 않음
- 설계 품질을 생각할 때 이 부분을 특히 주의해야 함
13.3.4. Model 은 대상이 아니라 목적 달성의 수단
Model? System 전체가 아니라 특정한 목적 달성과 관련된 부분만을 추려 표현한 시스템 일부
- 목적 달성 수단 으로 해석해야 제대로 모델링 가능
- 목적 중심으로 이름을 잘 설계하면, 목적을 달성하기에 적절한 모델을 설계 가능
13.3.5. 단일 책임이란 단일 목적
- User Class 를 너무 많은 목적으로 사용해서 문제가 발생
Class 가 이루어야 하는 목적은 반드시 하나여야 한다
- 특정 목적에 특화되게 설계해야, 변경하기 쉬운 고품질 구조를 갖게 됨
"책무를 잘 생각해서 설계하자"
13.3.6. Model 을 다시 확인하는 방법
- 해당 모델이 달성하려는 목적을 모두 찾아냄
- 목적별로 모델링을 다시 수정
- 목적 중심 이름 설계를 기반으로 모델에 이름을 붙임
- 모델에 목적 이외의 요소가 들어가 있다면 다시 수정
13.3.7. Model과 구현은 반드시 서로 피드백하기
- Model 은 구조를 단순화한 것에 불과하므로 세부적인 내용은 따로 묘하사지 않음
- Model 을 기반으로 클래스를 설계하고 코드를 구현하면서 세부적인 내용을 수정해야 함
- Model != Class
- Class 설계와 구현에서 무언가를 깨닫는다면, 이를 Model에 피드백해야 함
- Feedback 을 하면 Model 이 더 정확해짐
- 그러면 Model structure를 개선할 수 있고, 이는 코드 품질 향상에 기여
- Feedback 을 하지 않으면,
Model Structure
와 Source Code
사이에 괴리가 생김
- Feedback Cycle 을 계속 돌리는 것이 설계 품질을 높이는 것이 비결
13.4. 기능을 좌우하는 Modeling
기능성
: 소프트웨어 품질 특성 중 하나, 고객의 니즈를 만족하는 정도
13.4.1. 숨어 있는 목적 파악하기
- 온라인 쇼핑몰에서 상품 구매 모델링을 생각해보면 아래와 같음
- 이러한 모델은 기능을 발휘하기 힘듬
- 상품 구매 뒤에 숨어 있는 진짜 모습 때문
상품 등의 매매 계약은 구매 회원의 구매 신청에 대하여 회사 또는 판매 회원이
승낙의 의사를 표시함으로써 체결됨
- 매매 계약 = 지불 시기와 결ㄹ제 방법 등 지불 조건을 지정해야 함
- 위의 모델에는 지불 조건에 해당하는 요소가 없음
- 법적인 내용이 시스템에 반영되어 있지 않다면, 문제가 일어남
개념의 정체
와 뒤에 숨어 있는 중요한 목적
을 잘 파악하는 것이 중요
13.4.2. 기능성을 혁신하는 깊은 모델
- 이와 같은 추상화에는 다소 문제가 있음
- 각 모델이 어떤 역할을 하고 있는지 전혀 알 수 없음
- 모델 = 목적 달성 수단
- 위의 모델이 어떤 목적을 달성하기 위한 수단인지 확인이 필요
- 컴퓨터의 본질은 0과 1이라는 신호를 변환하고, 신호 변환을 응용해서 연산하는 것
- 뛰어난 변환 능력을 갖춘 모델을 설계하는 것이 곧 기능성의 혁신으로 이어짐
deep model? 본질적인 과제를 해결하고 기능성 현식에 공현하는 모델
- deep model 은 수많은 시행착오를 거듭하고 모델을 계속해서 개량하는 과정에서 발상이 전환됨
- 설계는 매일 매일 반복해서 개선하는 것이 중요
14장. Refactoring: 기존의 코드를 성장시키는 기술
14.1. Refactoring 흐름
Refactoring
: 실질적인 동작은 유지하면서, 구조만 정리하는 작업
class PurchasePointPayment {
...
PurchasePointPayment(final Customer customer, final Comic comic) {
if (customer.isEnable()) {
if(comic.isEnable()) {
if(comic.currentPurchasePoint.amount < customer.possessionPoint.amount) {...}
}
}
}
}
14.1.1. 중첩을 제거하여 보기 좋게 만들기
class PurchasePointPayment {
...
PurchasePointPayment(final Customer customer, final Comic comic) {
if (!customer.isEnable()) {...}
if(!comic.isEnable()) {...}
if(comic.currentPurchasePoint.amount < customer.possessionPoint.amount) {...}
}
}
14.1.2. 의미 단위로 로직 정리하기
- 조건 확인과 값 대입 로직을 각각 분리해서 정리
class PurchasePointPayment {
...
PurchasePointPayment(final Customer customer, final Comic comic) {
if (!customer.isEnable()) {...}
customerId = customer.id;
if(!comic.isEnable()) {...}
comidIc = comic.id;
if(comic.currentPurchasePoint.amount < customer.possessionPoint.amount) {...}
comsumptionPoint = comic.currentPurchasePoint;
}
}
class PurchasePointPayment {
...
PurchasePointPayment(final Customer customer, final Comic comic) {
if (!customer.isEnable()) {...}
if(!comic.isEnable()) {...}
if(comic.currentPurchasePoint.amount < customer.possessionPoint.amount) {...}
customerId = customer.id;
comidIc = comic.id;
comsumptionPoint = comic.currentPurchasePoint;
}
}
14.1.3. 조건을 읽기 쉽게 하기
customer.isEnable()
이라고 하면 보기 편하지만 !customer.isEnable()
이라고 하면 보기가 어려움
- 코드를 읽을 때 한 번 더 생각해서 유효하지 않다라고 변경해서 읽어야 함
class PurchasePointPayment {
...
PurchasePointPayment(final Customer customer, final Comic comic) {
if (customer.isDisable()) {...}
if(comic.isDisable()) {...}
...
}
}
14.1.4. 무턱대고 작성한 로직을 목적을 나타내는 method 로 바꾸기
if(comic.currentPurchasePoint.amount < customer.possessionPoint.amount) {...}
- 이렇게 작성한 로직은 목적을 알기 힘듬
- 무턱대고 로직을 작성하지 말고 목적을 나타내는 메서드로 만들어서 사용하는 것이 좋음
class Customer {
...
boolean isShortOfPoint(Comic comic) {
return comic.currentPurchasePoint.amount < customer.possessionPoint.amount
}
}
class PurchasePointPayment {
...
PurchasePointPayment(final Customer customer, final Comic comic) {
if (customer.isDisable()) {...}
if(comic.isDisable()) {...}
if(customer.isShortOfPoint(comic)) {...}
...
}
}
14.2. Unit Test로 Refactoring 중 실수 방지하기
- Unit Test : 작은 기능 단위로 동작을 검증하는 테스트
- Refactoring 을 할 때는 Unit Test 는 필수
14.2.1. 코드 과제 정리하기
14.2.2. TestCode 를 사용한 Refactoring 흐름
- 이상적인 구조의 클래스 기본 형태를 어느 정도 잡음
- 이 기본 형태를 기반으로 테스트 코드 작성
- 테스트를 실패 시킴
- 테스트를 성공시키기 위한 최소한의 코드 작성
- 기본 형태의 클래스 내부에서 Refactoring 대상 코드 호출
- 테스트가 성공할 수 있도록, 조금씩 로직을 이상적인 구조로 Refactoring
14.3. 불확실한 사양을 이해하기 위한 분석 방법
- Unit Test 를 사용한 Refactoring 은 처음부터 사양을 알고 있다는 전제가 있기에 테스트를 작성할 수 있었음
- 실제 개발을 하다보면 사양을 모르는 경우도 많음
- 사양을 제대로 모른다면 Refactoring 을 위한 Unit Test 를 작성할 수 없음
14.3.1. 사양 분석 방법 1: 문서화 테스트
public class MoneyManager {
public static int calc(int v, boolean flag) {...}
}
- calc method 가 무슨 기능을 하는지 알 수 없음
- 따라서 안전하게 Refactoring 할 수 없음
- 이때 활용하는 기법 =
문서화 테스트
- 문서화 테스트 : 메서드의 사양을 분석하는 방법
- 테스트를 변경해가면서 메서드가 어떻게 동작하는지 확인
@Test
void characterizationTest() {
int actual = MoneyManager.calc(1000, false);
assertEqual(1000, actual);
}
- 매개변수를 변경해가면서 method 가 어떻게 동작하는지 파악
문서화 테스트 : 분석하고 싶은 메서드의 테스트를 작성해서, 해당 method 가 어던 동작을 하는지 확인하는 방법
14.3.2. 사양 분석 방법2: Scratch Refactoring
- 정식 Refactoring 이 아니라 로직의 의미와 구조를 분석하기 위해 시험 삼아 Refactoring
- TestCode 를 따로 작성하지 않고 코드를 Refactoring
[장점]
- 코드의 가독성이 좋아져 로직의 사양을 이해할 수 있음
- 이상적인 구조가 보여 어느 범위를 메서드 또는 클래스로 끊어야 좋을지 보임
- Dead code 가 보임
- TestCode 를 어떻게 작성해야 할지 보임
14.4. IDE의 Refactoring 기능
14.4.1. Rename
- 한 번에 class, method, 변수 이름을 전부 변경하는 Refactoring
14.4.2. method 추출
14.5. Refactoring 주의 사항
14.5.1. 기능 추가와 Refactoring 동시에 하지 않기
- 기능 추가와 Refactoring 을 따로 구분하지 않으면 기능을 구분하기 힘듬
14.5.2. Small Step 으로 실시하기
- commit 은 어떻게 Refactoring 했는지 차이를 알 수 있는 단위로 구분
- 여러 번 commit 했다면 PR 을 작성하는 것이 좋음
14.5.3. 불필요한 사양은 제거 고려하기
- 불필요한 사양이 있으면 Refactoring 이 힘들어짐
- 이처럼 이익에 거의 기여하지 않는 사양에 해당하는 코드는 Refactoring 해도 이익이 없음
- 따라서 사양을 다시 활용해 불필요하다면 없애는 것이 좋음
15장. 설계의 의의와 설계를 대하는 방법
15.1. 이 책은 어떤 설계를 주제로 집필한 것인가?
- 설계 : 어떠한 문제를 효율적으로 해결하는 구조를 만드는 것
- 소프트웨어 설계 : 어떤 소프트웨어의 품질 특성을 향상시키기 위한 구조를 만드는 것
- 악마의 성질과 가장 관련 있는 품질 특성은
유지 보수성
- 유지 보수성 : 시스템이 정상 운용되도록 유지 보수하기가 얼마나 쉬운가를 나타내는 정도
- 수정성 : 변경 용이성이라고도 부르며, 얼마나 쉽고 정확하게 코드를 변경할 수 있는지 나타내는 지표
15.2. 설계하지 않으면 개발 생산성이 저하된다
- Legacy Code : 변경하기 어렵고, 버그가 생기기 쉬운 코드
- technical debt : Legacy code 가 축적되어 있는 상태
15.2.1. 요인 1: 버그가 발생하기 쉬운 구조
- 응집도가 낮은 구조로 인해 사양 변경 시 수정 누락이 발생하기 쉬워지고 결국 버그가 발생
- 코드를 이해하기 어려우므로 구현할 때 실수를 저지르기 쉬워지고, 결국 버그가 발생
- 잘못된 값이 들어오기 쉬워지고, 결국 버그가 발생
15.2.2. 요인 2: 가독성이 낮은 구조
- 로직의 가독성이 낮아, 읽고 이해하는 데 시간이 오래 걸림
- 관련된 로직이 이곳저곳에 흩어져 있어, 사양을 변경할 때 관련된 로직을 찾아서 돌아다니는 데 시간이 오래 걸림
- 잘못된 값이 들어와서 버그가 발생했을 때, 잘못된 값의 출처를 추적하기 어려워 짐
15.2.3. 나무꾼의 딜레마
나무꾼의 도끼의 날이 너무 무딘 것 같아요
도끼를 갈고 나무를 베는 것이 좋지 않을까요
알고 있지만, 나무를 베는 것이 바빠서 도끼를 갈 시간이 없었어요
- 제대로 설계하지 않으면, 로직 변경과 디버그에 많은 시간을 소비하게 됨
- 결국 설계할 시간 여유조차 없어지는 딜레마에 빠짐
15.2.4. 열심히 일했지만 생산성이 나쁨
- 어쨋거나 열심히 일했다! 가 아니라 성과를 내기 위해 쉬운 구조를 설계하는 데 노력을 쏟지 않았다면, 열심히 했다라고 이야기하기 어려움
15.2.5. 국가 규모의 경제 손실
- Legacy Code 의 양에 단순 비례하지 않음
- 복잡하고 이해하기 힘든 로직이 있으면, 이로 인해 더 복잡하고 이해하기 힘든 로직이 만들어짐
- 소스 코드가 점점 거대해지면, 이러한 문제가 점점 가속화됨
- 결국 기능 추가가 너무 힘들어져, 릴리스가 어려워질 정도로 악화됨
15.3. Software 와 Engineer 의 성장 가능성
- 소프트웨어의 가치와 매력을 높이기 위해 사양을 추가하고 변경하면서, 코드를 변경
- 코드의 변경 용이성이 높을수록 소프트웨어의 가치를 빠르게 높일 수 있음
- 소프트웨어가 빠르게 성장하는 것
변경 용이성이 높다
= 소프트웨어의 성장 가능성이 높다
15.3.1. Engineer 에게 Asset 이란?
- 기술력!
- 기술력은 엔지니어가 부를 창출하는 원천
- Legacy Code 는 이러한 자산의 축적, 기술력의 성장을 방해하는 무서운 존재
15.3.2. Legacy Code 는 발전을 막음
- Legacy Code 는 다음 사람으로 하여금 Legacy Code 를 작성하게 함
- 즉 낮은 수준의 기술만 사용하게 만듬
15.3.3. Legacy Code 는 고품질 설계 경험을 막음
- Legacy code 는 균형이 이미 깨져 있어, 설계를 개선하기가 매우 힘듬
15.3.4. Legacy Code 는 시간을 낭비하게 만듬
- 이해하는 데 오랜 시간이 걸림 하지만 시간은 유한함
- 따라서 원래 더 가치있는 일에 사용되었어야 하는 시간이 줄어듬
- 기술 향상을 막고, 엔지니어에게 정말 중요한 자신이라 할 수 있는 기술력의 축적을 막음
15.4. 문제 해결하기
15.4.1. 문제를 인식하지 못하면 설계에 대한 생각 자체가 떠오르지 않음
15.4.2. 인지하기 쉬운 문제와 인지하기 어려운 문제가 존재
- Software System 에 대해 정의한 metric
- 악마가 어떤 문제를 일으키는지 모른다면, 기술 부채의 존재를 인식하기 힘듬
- 소스 코드를 독해하는 스킬과 기술 부채를 인식하는 스킬은 전혀 다름
15.4.3. 이상적인 형태를 알아야 문제를 인식할 수 있음
- 이상적인 형태가 어떤 것인지 스스로 자세하게 정의할 수 있어야 함
- 이상이 무엇인지 알고 있다면, 현실과 비교하며 차근 차근 문제 해결 가능
15.4.4. 변경 용이성을 비교 수 없는 딜레마
- 기술 부채를 줄이는 변경 용이성 설계의 효과는 어떻게 측정하면 될까?
- 변경 용이성은 곧바로 비교할 수 없음
- 미래의 변경 비용이 얼마나 낮은지 나타내는 것으로 시간이 경과해야 알 수 있음
- 따라서 거의 불가능 함
15.5. 코드의 좋고 나쁨을 판단하는 지표
Code metric
, Software metric
: 코드 복잡성과 가독성 등의 품질 지표
15.5.1. 실행되는 코드의 줄 수
- 주석을 제외하고, 실행되는 로직을 포함하는 코드의 줄 수
- 많으면 많을수록 너무 많은 일을 하고 있을 가능성이 높음
- 줄 수가 너무 많으면 method 와 class 분할을 검토
15.5.2. Cyclomatic complexity
15.5.3. 응집도
- 모듈 내부에서 데이터와 로직이 관련되어 있는 정도
- 응집도가 높을수록 변경 용이성이 높고 좋은 구조
- 응집도 메트릭 LCOM (Lack of Conhension in Methods) 가 있음
15.5.4. 결합도
- 모듈간의 의존도
- 어떤 클래스가 호출하는 다른 클래스의 수
- 의존하고 있는 클래스가 많으면 많을수록, 결합도가 높을 수록 더 넓은 범위를 고려해야 함
- Class Diagram 으로 확인할 수 있음
15.5.5. Chunk
Magical Number 4
: 인간의 단기 기억은 한번에 4 ± 1 의 개념 정도만 파악할 수 있음
Chunk
: 기억할 수 있는 정보 덩어리의 단위
- 프로그래밍은 굉장히 많은 종류의 데이터/로직을 보는 직업
- 데이터와 사양 변경이 어디에 영향을 주는지, 버그가 발생하지 않는지 등 다양한 요소를 보고 검증해야 함
- 하지만 클래스가 너무 거대하면 제대로 파악할 수 없음
- class 를 설계할 때도 Magical Number 4 를 염두해두고 뇌가 쉽게 받아들일 수 있는 구조인지 생각
- 이보다 큰 Class 는 작은 Class 로 분할하는 것이 좋음
15.6. 코드 분석을 지원하는 다양한 도구
15.6.1. Code Climate Quality
- Code Climate 사에서 만든 코드 품질 분석 도구
- Github 와 연동하면 저장된 코드의 품질 점수를 자동으로 계산
15.6.2. Understand
- 코드 줄 수, 복잡도, 응집도 (LCOM), 결합도 이외의 다양한 관점의 메트릭 계측 가능
15.6.3. Visual Studio
- Community Version 을 통해 모든 license 형태에서 Code metric 계산 가능
15.7. 설계 대상과 비용 대비 효과
- 회사의 예산은 유한하고, 유한한 예산 안에서 개발 비용이 산축
- 투자한 비용과 제한된 기간 내에 어떻게든 이익을 내야 함
- 설계와 리팩터링은 무한히 할 수 없음
- 따라서 비용 대비 효과가 높은 부분을 노려야 함
15.7.1. 파레토의 법칙 (80:20의 법칙)
- 전체 결과가 80% 가 전체 원인의 20% 에서 일어남
ex) 매출의 80% 는 전체 상품 중 20% 의 상품이 만들어냄
- 소프트웨어의 처리 시간 중 80% 는 소스 코드 전체의 20%가 차지
- 소프트웨어의 기능 전체 중에서 중점적으로 사용되는 기능은 1/3 정도 밖에 안됨
- 사양이 자주 바뀌는 곳도 일부에 한정되어 있음
- 중요한 기능에는 고객들이 주목하므로 개선 요구도 당연히 많음 따라서 사양도 자주 변경됨
- 중요하고 사양 변경이 빈번한 곳의 설계를 개선하면 비용 대비 효과가 높음
15.7.2. Core Domain: Service 중심 영역
- 모든 상품과 서비스 = 우리가 판매하는 것 = 중심 가치
- 시스템에서 가장 큰 가치를 창출하는 곳
- 가치 있고 중요하고 비용 대비 효과가 가장 큰 곳
- 경쟁 우위에 있고 차별점을 만들며, 비즈니스 우위를 만들 수 있는 곳
15.7.3. 중점 설계 대상 선정에는 비즈니스 지식이 필요함
- Service 가 거대해지면 무엇이 서비스의 중심 가치인지 알기 힘들어짐
- Domain 주도 설계 = Core Domain 의 가치를 지속적으로 높이고, 서비스를 장기적으로 성장시키는 설계 방법
- 설계 비용 대비 효과를 높이려면 중점적인 설계 대상을 선정할 수 있어야 함
- 대상을 잘 선정하려면, 서비스가 해결하고 싶은 고객 과제가 무엇인지, 서비스의 본질이 무엇인지 볼 수 있는 능력이 필요
- 서비스와 관련된 비즈니스 지식이 필요
15.8. 시간을 다스리는 능력자 되기
- 변경 용이성 설계 = 개발 생산성 향상 = 미래의 시간을 다룰 수 있음
- Legacy Code 를 안고 계속해서 기술 부채를 쌓아갈지 소프트웨어를 빠르게 성장시킬지는 모두 설계자의 능력에 달림
- 엔지니어가 아니면 표면적인 기능만 봄
- 엔지니어는 내부 구조를 머릿속에 그릴 수 있음
- 머릿속에 그리는 능력과 설계 능력을 활용하면 미래의 시간을 조종할 수 있음
16장. 설계를 방해하는 개발 프로세스와의 싸움
16.1. Communication
16.1.1. Communication 이 부족하면 설계 품질에 문제가 발생
- Team Communication 이 부족하면 서로가 무엇을 하고 있는지 잘 모름
- 팀원간 의사소통에 문제가 있으면 버그가 많아짐
16.1.2. Conway's law
- 시스템의 구조는 그것을 설계하는 조직의 구조를 닮아간다
- 개발 부문이 3개의 팀으로 구분되어 있다면, 모듈의 수도 3개로 구성되는 시스템
- 왜냐하면 여러 개의 팀을 편성하면, 커뮤니케이션은 각각의 팀 내에서만 활발하게 이루어짐
- 따라서 시스템의 구조가 Release 단위, 즉 팀 단위의 구조처럼 구성
- Conway's law = Communication 비용 구조의 법칙
- 팀 내부에서 이뤄지는 커뮤니케이션 비용은 낮고
- 외부와 이뤄지는 커뮤니케이션 비용은 높음
Inverse conway's law
: 소프트웨어 구조를 먼저 설계하고 소프트웨어 구조에 맞게 조직을 편성
- 하지만 표면적으로 내세우는 것만으로 효과가 크지 않음
- 팀간의 관계, 팀 내부와의 관계를 잘 파악해야 함
16.1.3. 심리적 안정성
- 어떤 발언을 했을 때 부끄럽거나, 거절 당하지 않을 것이라는 확신
- 안심하고 자유롭게 발언 또는 행동할 수 있는 상태
- 성공적인 팀을 구축할 때 매우 중요한 개념
- 커뮤니케이션에 문제가 있을 때는 일단 심리적 안정성 향상에 힘쓰는 것이 좋음
16.2. 설계
16.2.1. 빨리 끝내고 싶다는 심리가 품질 저하의 함정
- 품질이 나쁜 시스템을 만드는 팀은 클래스 설계와 관련된 습관이 애초에 없음
- 빨리 끝내고 싶은 마음이 앞서고 그냥 동작하면 구현
- 품질을 무시하고 구현하는 과정이 반복되면 조악한 코드는 점점 조악해짐
16.2.2. 나쁜 코드를 작성하는 것이 좋은 코드를 작성하는 것보다 오래 걸린다
- TDD 를 사용하는 편이 전체적으로 보았을 때 더 빠르다는 결론
16.2.3. Class 설계와 구현 Feedback Cycle 돌리기
- 사양을 변경할 때는 최소한 메모로라도 클래스 다이어그램을 그림
- 이를 기반으로 책무와 응집도 등의 관점에서 문제가 없는지 간단하게 리뷰
16.2.4. 한 번에 완벽하게 설계하려고 들지 말고, 사이클을 돌리며 완성하기
- 대규모로 변경할 때는 그만큼 확실한 클래스 설계가 필요
- 완벽하게 설계하려는 욕심을 버리고 피드백 사이클을 돌리면서 조금씩 향상 시킴
16.2.5. 성능이 떨어질 수 있으니 클래스를 작게 나누지 말자는 맞는 말일까?
- 클래스가 많아지면 비용이 발생하는 것은 맞음 하지만 대부분은 무시할 수 있는 정도
- 실제로 성능과 관련 없는 부분인데, 이렇게 하면 빠를 것이다 라고 생각하고 작성하는 코드는 대부분 변경 용이성이 낮음
16.2.6. 설계 규칙을 다수결로 결정하면 코드 품질은 떨어진다
- 아무래도 수준이 낮은 쪽에 맞춰서 하향평준화되기 쉬움
- 조악한 규칙이 채택될 수 있고, 규칙 자체가 만들어지지 않을 수 있음
16.2.7. 설계 규칙을 정할 때 중요한 점
- 시니어 엔지니어처럼 설계 역량이 뛰어난 팀원이 중심이 되어 규칙을 만드는 것이 좋음
- 각각의 설계 규칙에는 이유와 의도를 함께 적는 것이 좋음
- 설계 규칙은 성능이나 프레임워크의 제약 등 다양한 요건과 트레이드 오프 될 가능성이 있음
- 규칙을 무조건 지켜야 하는 것은 아니며, 타협점을 찾아야 하는 상황도 분명 존재
- 팀의 설계 역량이 성숙하지 않으면, 개인에게만 맡기지 말고, 설계를 어느 정도 아는 팀원이 설계 리뷰와 코드 리뷰를 하도록 해서 설계 품질을 관리
- 팀원들과 스터디를 진행해보면서, 팀 전반의 설계 역량을 조금씩 높이는 것도 중요
16.3. 구현
16.3.1. 깨진 유리창 이론과 보이스카우트 규칙
16.3.2. 기존 코드를 믿지 말고, 냉정하게 판단하기
- 의심 없이 코드를 받아들이지 말고, 계속해서 확인해라
- 정체를 파악하는 행위
- 구조적으로 이상한 것
- 클래스 이름과 메서드 이름이 이성한 것
- 사양과 다른 이름을 갖고 있거나
- 사양이 비슷하면서 다른 의미의 이름이 붙어 있는지 않은지 확인
[장애물]
- Anchoring Effect
- 처음 제시한 수치와 정보가 기준이 되어, 이후의 판단을 왜곡하는 인지 편향
- 기존의 클래스 이름과 메서드 이름이 기준이 되어, 개발자의 판단을 왜곡시키는 경우가 많음
- 이름이 없거나 이름을 모르는 것은 인지하기 어려움
- 해결하고 싶은 내용과 달성하고 싶은 목적을 배우는 것이 중요
- 관계자를 찾아 대화하거나 관련된 글을 읽고 그곳에서 쓰이는 용어를 파악하려고 이해해야 함
16.3.3. Code Convention 사용하기
16.3.4. Naming Rule
16.4. Review
16.4.1. Code Review 구조화하기
- PR code 는 code 의 history 와 경위를 알고 있는 사람 또는 설계를 자세하게 알고 있는 사람이 리뷰하는 것이 좋음
16.4.2. Code 를 설계 시점에 리뷰하기
16.4.3. 존중과 예의
- 기술적 올바름을 두고 공격적인 코멘트를 다는 사람들을 방지해야 함
- 기술적 올바름과 유용성보다도 함께 일하는 동료를 존중하는 것이 먼저
- 존중과 예의를 갖추고 지적하는 것이 코드 품질을 높이는 가장 빠른 길
16.4.4. 정기적으로 개선 작업 진행하기
- 좋은 않은 코드가 스케쥴 문제로 반영되기도 함
- 따라서 최대한 빠른 시일 내에 수정하는 것이 중요
- Github 의 Issue 로 관리하면 좋음
16.5. 팀의 설계 능력 높이기
16.5.1. 영향력을 갖는 규모까지 동료 모으기
- 혼자 힘으로는 품질을 높이려고 해도 효과가 거의 나타나지 않음
- 설계 뿐만 아니라 일의 방식을 상향식으로 개선하려면, 주위의 협력이 반드시 필요
- 협력은 일하는 방식을 바꿀 만큼 큰 영향력이 생김
- Lanchester's laws : 전투력에 따라 적에게 미치는 피해 규모를 계산하는 이론
- Koopman Goal : 시장 점유율의 목표를 정의
- Lanchester's laws 를 시장 점유율에 적용한 경쟁 이론
- 시장 인지권 : 시장 내에서 영향력을 무시할 수 없고, 점유율 경쟁에 본격적으로 참가를 시작하는 위치
- 보통 목표치는 10.9%
- 방향성이 어느 정도 같은 동료에게 말을 걸고 고민을 나누고 협력을 해 줄 동료를 만듬
16.5.2. 천리길도 한 걸음 부터
- 조급하게 굴지 말고 매일 조금씩 설계 지식을 공유
16.5.3. 백문이 불여일견
- 어느 정도 공유했다면, 함께 클래스를 설계하고 구현한 뒤 리뷰
16.5.4. Follow up Study
- 동료를 더 모으면 설계 스터디를 진행해 보는 것도 좋음
- 책에 적혀 있는 노하우 1-2개 정도 읽음
- Production code 에서 노하우를 적용해 볼 수 있는 부분을 찾음
- 노하우를 사용해 코드 개선
- 어떻게 개선했는지 발표
- 발표 내용에 대한 질의 응답
16.5.5. Study Group 에서 발생할 수 있는 문제 해결 노하우
- 단순하게 책을 읽는 것이 아니라 코드를 개선햇을 때 실제로 좋아졌음을 느끼는 것이 중요
- 올바른 설계 방법을 강요하는 것도 좋지 않음
- 다른 사람의 관점에 맞춰 생각하는 일은 힘들지만, 공감대 없이 무리하게 이야기한다면 모두가 불행해짐
- 설계 이야기를 빨리 꺼내고 싶어서 불안하겠지만, 인내가 필요
16.5.6. Leader 와 Manager 에게 설계의 중요성과 비용 대비 효과 설명하기
16.5.7. 설계 책임자 세우기
- 설계 품질과 관련된 규칙이나 개발 프로세스 수립
- 규칙을 반복적으로 알리고 교육
- 리더와 매니저에게 효과 공유
- 품질 시각화
- 설계 품질 유지
17장. 설계 기술을 계속해서 공부하려면
17.1. 책 추천
- 현장에서 유용한 시스템 설계 원칙
- 읽기 좋은 코드가 좋은 코드다
- Refactoring 2판
- Clean Code
- Legacy Code 활용 전략
- Re-Engineering Legacy Software
- Beyond Legacy Code
- Engineering 조직으로의 초대
- Principal of Programming
- Clean Architecture
- Domain Drive Design
- Secure by Design
- 도메인 주도 설계 철저 입문
- 도메인 주도 설계 모델링/구현 가이드
- 도메인 주도 설계 샘플 코드와 FAQ
- Test Driven Development
17.2. 설계 스킬을 높이는 학습 방법
- input : output = 2:8
- 설계 효과를 반드시 머릿속에 새기기
- 악마의 구조를 파악하는 연습
- Refactoring 으로 설계 기수력 높이기
- 동작하는 코드를 작성했다면, 다시 설계하고 커밋하기
- 설계 기술서를 읽으며 더 높은 목표 찾기