변수에 다시 값을 할당하는 것을 흔히 재할당, 하지만 파괴적 할당이라고도 함
재할당은 피하는 것이 좋고, 계속해서 새로운 변수를 만들어 사용하면 피할 수 있음
AttackPower attackPower = new AttackPower(20);
Weapon weaponA = new Weapon(attackPower);
Weapon weaponB = new Weapon(attackPower);
weaponA.attackPower.value = 25;
이 이후 출력을 해보면 20으로 설정했던 weaponB도 25로 변경되어 있는 것을 볼 수 있다.
이렇게 때문에 재사용을 할 때 예상치 못한 동작이 될 수 있으므로 인스턴스를 개별적으로 생성하고 재사용하지 않는 로직으로 변경하면 됨
AttackPower attackPowerA = new AttackPower(20);
AttackPower attackPowerB = new AttackPower(20);
// 이렇게 해야 A를 변경하더라도 B가 변경되지 않음
/**
* 공격력 강화 함수
* @param increment 공격력 증가량
*/
void reinforce(int increment) {
value += increment;
}
/**
* 무력화 하는 함수
*/
void disable() {
value = MIN;
}
이 이후 강화 함수를 통해, 공격력을 증가했을 땐 예상대로 공격력이 증가함
attackPower.reinforce(15)
하지만 언젠가부터 갑자기 공격력이 0 (MIN)이 되는 상황이 발생함
원인으로는 AttackPower 인스턴스가 다른 스레드에서 사용 되었음을 확인함
// 다른 스레드의 처리
attackPower.disable();
여기에서 AttackPower의 disable 메서드와 reinforce 메서드는 구조적인 문제를 갖고 있음 바로 부수 효과
부수 효과란? '함수가 매개변수를 전달 받고, 값을 리턴하는 것' 이외에 외부 상태(인스턴스 변수 등)를 변경하는 것을 가리킴
(함수나 표현식이 외부의 상태에 영향을 미치는 것)
조금 더 구체적으로, 함수(메서드)에는 주요 작용과 부수 효과가 있음
상태 변경이란 함수 밖에 있는 상태를 변경하는 것
인스턴스 변수 변경, 전역변수 변경, 매개변수 변경, I/O 조작
위의 코드 예시에서 disable과 reinforce를 실행할 때마다 AttackPower.disable 값이 계속 바뀌므로 동일한 결과를 위해서는 동일한 순서로 실행해야함
즉, 작업 실행 순서에 의존하게 됨
이런 코드들은 결과를 예측하기 힘들고 유지보수가 힘듦
인스턴스 변수뿐만 아니라, 전역 변수와 매개변수 변경에서도 동일
함수 내부에 선언한 지역 변수의 변경은 부수 효과라고 말할 수 없음 함수 외부에 영향을 끼치지 않기 때문
모듈 내부에 있는 데이터와 로직 사이의 관계가 얼마나 강한지 나타내는 지표
(모듈은 클래스, 패키지, 레이어 등을 모두 포함할 수 있는 용어)
즉, 응집도란 클래스 내부에 있는 데이터와 로직 사이의 관계가 얼마나 강한지를 나타낸다고 보면 됨
static 메서드는 알다시피, 클래스의 인스턴스를 생성하지 않고도 다른 곳에서 add 메서드를 호출할 수 있는 것
class Order {
static int add (int 머니1, int 머니2) {
return 할머니
}
}
이렇게 되어 있으면 다른 곳에서 Order.add를 사용할 때
서로 다른 클래스에 있기 때문에 응집도가 낮아짐
static은 인스턴스 변수를 활용할 수 없으므로 인스턴스 변수
와 인스턴스 변수를 사용하는 로직
을 같은 클래스에 만드는 것이 응집도를 높이는 방법
객체 지향 언어를 사용할 때 C언어 같은 절차 지향 언어의 접근 방법을 사용하려 하기 때문
절차지향에서는 데이터와 로직이 따로 존재하도록 설계하므로 객체 지향에서도 이런 방법을 적용하도록 한다면, 데이터와 로직을 별도의 클래스에 배치하게 됨
그러므로 클래스의 인스턴스를 생성하지 않고 사용할 수 있는 static 메서드를 활용하는 것
그 중 여기에서도, 응집도의 영향을 받지 않는 경우에는 static 메서드를 사용해도 좋음
로그 출력 전용 메서드, 포맷 변환 전용 메서드처럼 응집도가 관계 없는 기능은 static 메서드로 설계하는 것이 좋음
다음과 같은 쇼핑몰 기프트 포인트 클래스가 있다고 예시를 들자
class GiftPoint {
private static final int MIN_POINT = 0;
final int value;
GiftPoint(final int point) {
if (point < MIN_POINT) {
throw new IllegalArgumentException("0이상 입력해야함")
}
}
}
이런 코드에서 다음처럼 표준/프리미엄 회원 가입 포인트를 부여할 땐 다음과 같이 사용을 해야함
// 표준
GiftPoint StandardMemberPoint = new GiftPoint(3000);
// 프리미엄
GiftPoint PremiumMemberPoint = new GiftPoint(10000);
하지만 만약 이럴 때 회원 포인트를 변경하고 싶다면 소스 코드 전체를 확인해서 바꿔줘야 하기 때문에
private 생성자 + 팩토리 메서드를 사용하여 목적에 따라 초기화 하는 것이 좋음
class GiftPoint {
private static final int MIN_POINT = 0;
private static final int STANDARD_MEMBER_POINT = 3000;
private static final int PREMIUM_MEMBER_POINT = 10000;
final int value;
GiftPoint(final int point) {
if (point < MIN_POINT) {
throw new IllegalArgumentException("0이상 입력해야함")
}
}
static GiftPoint forStandardMembership() {
return new GiftPoint(STANDARD_MEMBER_POINT);
}
static GiftPoint forPremiumMembership() {
return new GiftPoint(PREMIUM_MEMBER_POINT);
}
}
당연하게도 이렇게 변경시엔 GiftPoint 클래스만을 변경하게 될 수 있는 것
// 팩토리 형태 메서드
GiftPoint standardMembershipPoint = GiftPoint.forStandardMembership();
GiftPoint premiumMembershipPoint = GiftPoint.forPremiumMembership();
자주 사용되는 메서드라고 범용 처리 클래스에 구현해두면 코드가 중복되는 것을 줄일 수 있는 함 하지만, 결국엔 응집도가 낮은 구조라는 문제를 담고 있음
게다가, static 메서드가 응집도를 낮추는 문제만을 가져오는 것이 아닌, 전역변수가 나타나게 되는 등 여러가지 악 영향이 있을 수 있음
세금을 계산가에 포함하는 로직은 자주 쓰이므로 둔다고 해도, 탈퇴 확인, 상품 주문 등 관련 없는 로직들이 Common 이라는 클래스 안에 모여있고 모두 static 이라면 이는 응집도가 낮은 구조라고 함
아쉽게도 이러한 코드들은 실제 프로덕션 코드에서도 굉장히 많이 볼 수 있음
Common, Util 등 이름 자체가 범용
이라는 뜻이기 때문에 '아, 범용적으로 사용하면 여기에 두면 되겠구나' 라고 생각하기 때문
여기에 따른 근본적인 원인은 범용의 의미와 재사용성을 잘못 이해하고 있기 때문
재사용성은 설계의 응집도를 높이면, 저절로 높아짐
꼭 필요한 경우가 아니라면, 범용 처리 클래스를 만들지 않는 것이 좋음
---> 차라리, 불변 변수를 한 개 만들어놓고 그 변수의 값을 바꾸는 메서드를 활용하면 됨
다양한 상황에서 넓게 활용되는 기능을 '횡단관심사'라고 하며
대표적으로 아래와 같은 기능등을 의미
이러한 횡단 관심사라면 범용 코드로 만들어도 괜찮음
매개 변수를 잘못 다루면 응집도가 낮아지는 문제가 발생
class 캐릭터이동 {
void 이동 (Location location, int shiftX, int shiftY) {
location.x += shiftX;
location.y += shiftY;
}
}
이동 대상 인스턴스를 매개변수 location으로 전달받고 이를 변경하고 있음
이렇게 출력으로 사용되는 매개변수를 출력 매개변수라고 하는데
각자 다른 클래스에 있으므로 응집도가 낮은 구조임을 알 수 있고
응집도가 낮은 구조는 중복을 만듦
심지어 매개변수를 전달하고 다른 값이 나와버리니까 매개 변수가 변경되었다는 것을 외부에서 알 수 없음
그러므로 출력 매개변수로 설계하지 말고
객체 지향 설계의 기본으로 돌아가서 '데이터'와 '데이터를 조작하는 논리'를 같은 클래스에 배치함
따라서, 위의 코드는 다음처럼 변경될 수 있음
class Location {
final int x;
final int y;
Location (final int x, final int y) {
this.x = x;
this.y = y;
}
Location shift(final int shiftX, final int shiftY) {
final int nextX = x + shiftX;
final int nextY = y + shiftY;
return new Location(nextX, nextY);
}
}
// 내 생각.. 매개변수를 전달한다해도 최대한 다른 변우셍 삽입하여 그 변수를 return하고 코드가 길어지더라도 더욱 좋은 가독성을 가지도록 하는 게 옳구나...
다음과 같은 메서드 체인 코드가 있다고 해보자
if (party.members[momberId].equipments.canChange) {
party.members[memberId].equipments.armor = newArmor;
}
메서드 체인을 열차사고 (train wreck)라고도 함
이 방법도 응집도를 낮추는 방법
왜냐하면 모든 곳에서 요소에 접근할 수 있기 때문에
members, equipments, canChange, armor의 사양이 조금이라도 변경되면 해당 요소에 접근하던 모든 코드를 확인하고 수정해야하는 일이 발생함
이렇게 영향이 미치는 범위가 커지므로 전역 변수와 같은 성질을 가지고
사실 전역변수 보다 훨씬 악질적인 코드
테메테르 법칙
사용하는 객체 내부를 알아서는 안 됨
(단순하게 모르는 사람에게 말 걸지 않기라고 설명한다고도 함)
메서드 체인으로 내부 구조를 돌아다닐 수 있는 설계는 테메테르의 법칙을 위반한다고도 할 수 있음
소프트웨어 설계에는 묻지 말고, 명령하기 (Tell, Don`t ask)라는 유명한 격언이 있다고 함
다른 객체의 내부 상태(변수)를 기반으로 판단하거나 제어하려고 하지 말고, 메서드로 명령해서 객체가 알아서 판단하고 제어하도록 하라는 것
인스턴스 변수를 private으로 변경해서 외부에서 접근할 수 없도록 하고 인스턴스 변수에 대한 제어는 외부에서 메서드로 명령하는 형태로 만들어서 상세한 판단과 제어는 명령을 받는 쪽에서 담당하도록!
즉, 상세한 로직은 호출하는 쪽이 아니라, 호출되는 쪽에 구현하라
class 장비다루기 {
private boolean canChange;
private Equipment head;
private Equipment armor;
private Equiment arm;
void equipArmor(final Equipment newArmor) {
if (canChange) {
armor = newArmor;
}
}
void deactivateAll() {
head = Equipment.EMPTY;
armor = Equipment.EMPTY;
arm = Equipment.EMPTY;
}
}
이렇게 되면 방어구 관련 로직이 장비 다루기 클래스에만 응축되어 이곳저곳을 찾지 않아도 됨