내 코드가 그렇게 이상한가요?

10장 이름 설계: 구조를 파악할 수 있는 이름

lleesla 2024. 4. 2. 20:54

10.1 악마를 불러들이는 이름

  • 온라인 쇼핑몰을 예로 들자면 흔히 볼 수 있는 좋지 않은 이름은 상품을 그대로 ‘상품 클래스’라고 이름을 붙이는 것이다.
  • 온라인 쇼핑몰은 상품을 중심으로 이루어지기 때문에 출고, 예약, 주문, 발송 등 상품을 다루는 유스케이스가 많다.
  • 그러면 상품 클래스가 여러 클래스와 관련있는 로직을 갖게 되고, 점점 거대하고 복잡해진다.
  • 거대해진 상품 클래스에 사양 변경이 발생하면 관련된 부분에 버그가 생기지는 않았는지 관련된 클래스를 모두 확인 해야한다.

10.1.1 관심사 분리

  • 상품이 예약,주문,발송 등 다양한 관심사에 관한 로직을 가지고 있다. 즉 강한 결합 로직
  • 강한 결합을 해소하고 결합이 느슨하고 응집도가 높은 구조로 만들려면 관심사 분리를 할 수 있어야한다.
  • 관심사 분리는 ‘관심사(유스케이스, 목적, 역할)에 따라서 분리한다’ 는 것이다.
  • 따라서 상품 클래스는 관심사에 따라서 각각 클래스로 분할해야 한다.

10.1.2 관심사에 맞는 이름 붙이기

  • 분할 후 모든 클래스에 같은 ‘상품’이라는 이름을 중복해서 사용할 수 없다.
  • 각각 관심사에 맞는 이름을 붙인다. ( ‘주문 상품’ , ‘예약 상품’, ‘발송 상품’)
  • 분할 후에는 분할한 클래스 각각에 관심사에 맞는 로직을 캡슐화하면 된다.
  • 이렇게 관심사에 따라 분리하면 결합도를 낮추고 응집도를 높일 수 있다.
  • 예를 들어 주문 관련 사양 변경이 발생했을 때 주문과 관련된 클래스만 확인하면 될 것이다.

10.1.3 포괄적이고 의미가 불분명한 이름

  • 포괄적인 이름을 사용할 경우

<aside> 💡 직원 A : “이번 사양 변경으로 개발하고 있는 온라인 쇼핑몰에 예약 기능을 추가해야 합니다. 상품 예약 로직을 어디에 구현해야 할까요?

직원 B : “ 상품 클래스가 이미 있잖아요? 상품 클래스에 구현하세요”

</aside>

  • ‘상품’이라는 이름이 포괄적이라 상품과 관련된 모든 로직을 구현하면 될 것처럼 보인다.
  • 이처럼 의미가 너무 포괄적인 이름은 내부에 온갖 로직을 구현하게 만든다.
  • 이름이 포괄적이라 목적이 불분명한 클래스를 목적 불명 객체라고 부른다.
  • 이러한 상황에 빠지지 않으려면 관심사 분리 즉 비즈니스 목적에 따라 이름을 붙이면 된다.

10.2 이름 설계하기 - 목적 중심 이름 설계

  • 관심사 분리를 생각하고, 비즈니스 목적에 맞게 ‘이름을 붙이는 것’은 결합이 느슨하고 응집도가 높은 구조를 만드는 데 중요한 역할을 한다.
  • 목적 중심 이름 설계는 목적에 맞게 이름을 설계하는 것이다.
  • 소프트웨어로 달성하고 싶은 목적과 의도를 이름만으로도 알 수 있게 하는 것

10.2.1 최대한 구체적이고, 의미 범위가 좁고, 특화된 이름 선택하기

  • 목적을 달성하는 데 특화된 의미 범위가 좁은 이름을 클래스에 붙인다.
  • 그리고 고객 대상 제품 개발의 목적은 ‘회사가 사업적으로 어떤 목적을 달성하고 싶은가’ 하는 비즈니스 목적이어야한다.
  • 비즈니스 목적에 특화할 시 효과
    • 이름과 관계없는 로직을 배제하기 쉬워짐
    • 클래스가 작아짐
    • 관계된 클래스 개수가 적으므로 결합도가 낮아짐
    • 관계된 클래스 개수가 적으므로 사양 변경시 생각해야 하는 영향 범위가 좁음
    • 목적에 특화된 이름을 갖고 있으므로, 어떤 부분을 변경해야 할 때 쉽게 찾을 수 있음
    • 개발 생산성이 향상됨

10.2.2 존재가 아니라 목적을 기반으로 하는 이름 생각하기

  • 단순하게 존재를 나타내는 이름은 의미가 여러 곳에서 사용되기 쉬우며, 목적이 불분명해지기 쉽다.
  • 따라서 구체적인 목적을 알 수 있게, 목적을 기반으로 이름을 짓는 것이 좋다.
  • 비즈니스 목적에 특화된 이름이란
    • 주소 → ‘발송지’ ‘배송지’
    • 금액 → ‘청구 금액’ ‘소비세액’ ‘연체 보증료’ ‘캠페인 할인 금액’
    • 사용자 이름’ → ‘계정 이름’ ‘닉네임’ ‘본명’

10.2.3 어떤 비즈니스 목적이 있는지 분석하기

  • 비즈니스 목적에 특화된 이름을 만들려면, 어떤 비즈니스를 하는지 모두 파악해야한다.
  • 소프트웨어가 추구하는 목적과 내용을 분석해야한다.
    • 온라인 쇼핑몰: 판매 제품, 주문, 발송, 캠페인
    • 게임: 무기, 몬스터, 아이템, 기간 이벤트
  • 등장 인물과 관련 내용을 나열해 보고 관계를 정리하고 분석한다
  • 관련된 것끼리 묶어본다.

10.2.4 소리 내어 이야기해 보기

  • 어떤 목적을 달성하고 싶은지, 어떤 형태로 사용하고 싶은지, 서로 어떤 관련이 있는지 이를 팀과 소통해서 일치시키는 것이 중요하다.
  • 목적과 의도를 다르게 인식하면 이름이 제대로 된 방향성을 갖지 못할 수도 있다.
  • 비즈니스 측면을 잘 이해하고 있는 사람과 이야기해보기
  • 이야기를 통해 더 정확하고 구체적인 비즈니스 목적에 맞는 이름들을 이끌 수 있다.
  • 고무 오리 디버깅 방법
    • 프로그래밍에서 어떤 문제가 발생했을 때 문제를 누군가에게 설명하다보면 스스로 원인을 깨닫고 해결할 수 있다는 방법
  • 유비쿼터스 언어
    • 팀 전체에서 의도를 공유하기 위한 언어
  • 같은 의도를 갖고 이름을 대화, 문서, 클래스 이름, 메서드 이름에서 활용하면 설계에서 발생하는 문제를 해결하는데 도움이 된다.

10.2.5 이용 약관 읽어 보기

  • 이용 약관에는 서비스와 관련된 규칙이 엄격한 표현으로 작성되어있어 이를 활용하면 비즈니스와 관련된 이름을 알 수 있다.

<aside> 💡 구매자가 구매를 완료한 시점에 매매 계약이 체결된 것으로 한다. 매매 계약이 체결되면, 판매자는 당사에 서비스 사용료를 지불해야 한다. 서비스 사용료는 매매 계약이 체결된 시점의 상품 판매 가격에 판매 수수료율을 곱한 금액이다.

</aside>

  • ‘구매자’ ‘판매자’’ ‘매매 계약’등 비즈니스 측면에서 명확한 이름들이 여러가지 나온다.
  • 이를 참고하면 사용자를 나타내던 단순한 ‘사용자’ 클래스를 ‘구매자’클래스와 ‘판매자’ 클래스 등으로 구분할 수 있다.
  • 상품 구매는 ‘매매 계약’ 클래스 또는 ‘체결’ 메서드로 나타낼 수 있다.
  • 금액을 다루는 경우는 ‘서비스 사용료’ ‘판매 수수료율’
/** 서비스 사용료 */
class ServiceUsageFee {
	final int amount;
	
	/**
	* @param amount 요금 금액
	*/
	private ServiceUsageFee(final int amount) {
		if (amount < 0) {
			throw new IllegalArgumentException("금액은 0 이상의 값을 지정하세요.");
			}
			this.amount = amount;
		}
		
/**
* 서비스 사용료 확정하기
*
* @param salesPrice           판매 가격
* @param salesCommissionRate  판매 수수료율
* @return                     서비스 사용료
*/
static ServiceUsageFee determine (final SalePrice salesPrice,
																  final SalesCommissionRate
																  salesCommissionRate) {
		int amount = (int)(salesPrice.amount * salesCommissionRate.value);
		return new ServiceUsageFee(amount);
	}
}
  • determine 메서드는 이용 약관에 있던 서비스 사용료 정의와 일치한다.
  • 비즈니스 규칙과 클래스를 일치하게 만들면 정확하고 빠르게 변경할 수 있다.

10.2.6 다른 이름으로 대체할 수 없는지 검토하기

  • 열심히 이름을 정했어도 이름의 의미 범위가 생각보다 넓을 수 있고 여러 의미가 내포된 이름일 수 있다.
  • 그러니 다른 이름으로 바꿔보고 의미를 더 좁게 만들 수는 없는지 검토하는 것이 좋다
  • 예를 들어 호텔 숙박 예약 시스템
    • 단순하게 ‘사용자’라고 부르면 의미가 너무 넓다
    • 따라서 ‘고객’이라고 이름을 붙여본다
    • 하지만 숙박하는 사람과 숙박 요금을 결제하는 사람이 다를 수 있다.
      • ‘투숙객’과 ‘결제자’로 이름을 변경하는 것이 좋다

10.2.7 결합이 느슨하고 응집도가 높은 구조인지 검토하기

  • 목적에 특화된 이름을 선택하면, 목적 이외의 로직을 배제하기 쉬워진다.
  • 목적과 관련된 로직이 모이므로, 응집도가 높아진다. 목적이 이외의 로직이 섞인다면 이름을 잘못 붙였는지 검토하기
  • 또한 다른 클래스 몇 개와 관련 되어있는지 개수를 확인해 여러 개와 관련 되어있다면 의미가 더 좁은 특화된 이름을 찾는다.

10.3 이름 설계 시 주의 사항

10.3.1 이름에 관심 갖기

  • 팀 개발에서는 이름이 중요하다.
  • 이름과 로직이 대응된다는 전제, 이름이 프로그램 구조를 크게 좌우한다는 사실을 팀원들과 이야기해야한다.

10.3.2 사양 변경 시 ‘의미 범위 변경’ 경계하기

  • 개발 중 계속해서 반복되는 사양 변경에 의해 말이 의미하는 바가 점점 변화하는 경우가 있기 때문에 이름 설계는 중간중간 다시 검토해봐야 한다.
    • 개발 초기 고객 클래스가 있고 이 클래스는 ‘개인 고객’을 나타내는 것이었다.
    • 이후 사양이 변경되어 ‘법인 고객’도 포함 하게 되었고 법인의 등기 번호와 조직 이름이 고객 클래스에 추가되었다.
    • 고객 클래스 내부에 개인 고객 관련 로직과 법인 고객 관련 로직이 섞이기 때문에 이름을 변경하거나 클래스를 나눠야한다.

10.3.3 대화에는 등장하지만 코드에 등장하지 않는 이름 주의하기

  • 대화에는 등장하지만 코드에 등장하지 않는 이름이 있다면 주의해야한다.
  • 도서관 책 대여 서비스

<aside> 💡 A: ‘문제가 있는 회원’은 구현 했나요?

B: 네, 이미 구현 했습니다.

A: 어떤 클래스 인가요?

B: User 클래스 안에 구현했습니다.

A: User 클래스가 ‘문제가 있는 회원’을 나타내는 건가요?

B: 아니요, 인스턴스 변수 ‘대여 연체 횟수’ 와 ‘도서 파손 횟수’가 일정 횟수 이상이면 User 클래스를 ‘문제가 있는 회원’으로 구분하도록 만들었습니다.

A: 하지만 소스 코드 어디에도 ‘문제가 있는 회원’과 관련된 이름이 등장하지 않는걸요?

</aside>

  • 위의 대화처럼 이를 구현한 사람에게 직접 묻지 않으면 조재조차 알 수 없고 로직을 이해하기 힘들다.
  • 이름이 없다는 것은 메서드 또는 클래스로 설계되어 있지 않고 소스코드 내부에 사양대로 동작하게만 만든 것
  • 이럴 경우 ‘문제 있는 회원’ 관련된 사양이 변경 되었을 때 로직을 찾는 일이 힘들어진다.
  • 이를 피하기 위해 이름을 신경써서 설정하고 그 이름을 기반으로 메서드와 클래스를 설계해야한다.

10.3.4 수식어를 붙여서 구별해야 하는 경우는 클래스로 만들어보기

  • 차이를 구분하기 어려운 코드를 ‘단순하게 수식어를 붙여서 동료에게 설명하는 상황’은 시스템 개발에서 매우 흔하게 볼 수 있다.
  • 예시
    • RPG 캐릭터 ‘최대 히트 포인트’ 라는 속성을 가지고 있다.
    • 최대 히트 포인트를 높여주는 장비가 있다.
    • 액세서리에 최대 히트 포인트를 증가시켜주는 효과가 있다고 가정하여 코드를 구현
    int maxHitPoint = member.maxHitPoint + accessory.maxHitPointIncrements();
    
    • 이후 사양이 변경되어 방어구에도 같은 효과가 추가되었다.
    • 신입 사원은 이전의 구현을 모르고 방어구 효과 코드를 구현
    maxHitPoint = member.maxHitPoint + armor.maxHitPointIncrements();
    
  • 그러나 코드가 의도대로 작동하지 않았다.
  • 위 코드는 캐릭터의 원래 최대 히트포인트에 방어구의 효과를 적용하다 보니 문제가 생긴 것이다.
int maxHitPoint = member.maxHitPoint + accessory.maxHitPointIncrements() 
+ armor.maxHitPointIncrements();
  • maxHitPoint가 최대 히트포인트를 나타낸다는 것은 이름을 보고 알 수 있지만 ‘원래 최대 히트포인트’인지 ‘장비 착용으로 높아진 최대 히트포인트’인지 이름만 보고 알 수 없다.
  • 이처럼 의미가 다르거나 조건에 따라 달라지는 대상을 같은 이름으로 표현하면 차이를 구별하기 어렵다
  • 이 상황을 막기 위해서 의미 차이를 확실하게 알 수 있는 이름으로 수정해야한다.
    • 캐릭터의 원래 최대 히트포인트 : originalMaxHitPoint
      • 장비 착용으로 높아진 최대 히트 포인트 : correctedMaxHitPoint
  • 하지만 최대 히트 포인트를 단순하게 int 자료형으로 구현하면 의미 차이를 이름으로만 확인해야한다. 그러면 여러 관련 구현이 이곳저곳에 퍼져 응집도가 낮은 구조가 된다.
  • 따라서 수식어를 붙이면서 차이를 나타내고 싶은 대상은 각각 클래스로 설계하는 것이 좋다
class OriginalMaxHitPoint {
	private static final int MIN = 10;
	private static final int MAX = 999;
	final int value;
	
	OriginalMaxHitPoint(final int value) {
		if (value < MIN || MAX < value) {
			throw new IllegalArgumentException();
		}
		this.value = value;
	}
}
  • 이어서 ‘장비 착용으로 높아진 최대 히트포인트’도 설계한다.
class CorrectedMaxHitPoint {
	final int value;
	
	CorrectedMaxHitPoint(final OriginalMaxHitPoint originalMaxHitPoint,
											 final Accessory accessory,
											 final Armor) {
		value = originalMaxHitPoint.value +
						accessory.maxHitPointIncrements() +
						armor.maxHitPointIncrements();
	}
}
  • 이렇게 의미가 다른 개념들을 서로 다른 클래스로 설계해서 구조화하면, 개념 사이의 관계를 이해하기가 쉽다.
  • 수식어를 붙여서 차이를 나타낸다면, 각각을 클래스로 설계할 수 없는지 검토해봐야한다.

10.4 의미를 알 수 없는 이름

의미가 분명하지 않은 이름이 되기 쉬운 경우들을 살펴본다.

10.4.1. 기술 중심 명명

  • 프로그래밍, 컴퓨터 등 기술을 기반으로 이름 짓는 방법을 기술 중심 명명이라고 부른다.
  • 예를 들면, fucntion, method, int, str 등이 있다.

10.4.2. 로직 구조를 나타내는 이름

class Magic {
	boolean isMemberHPMoreThanZeroAndIsMemberCanActAndIsMEmberMpMoreThanMagicCostMp(Member member){
		...
	}
}
  • 세 가지 조건을 만족하는지 확인하는 메서드라고 가정하자.
  • 메서드 이름에 로직 구조를 그대로 드러내고 있다.
  • 무엇을 의도하는지 메서드 이름만 보고는 알기 힘들다.

10.4.3. 놀람 최소화 원칙

int count = order.item.itemCount();
  • 주문 상품 수를 리턴하는 것처럼 보인다.
int itemCount() {
	int count = items.count();
	
	// 주문 상품 수가 10 이상일 때, 기프트 포인트를 100만큼 추가
	if(10 <= count){
		giftPoint = giftPoint.add(new GiftPoint(100));
	}
	
	return count;
}
  • 그런데 메서드 내용을 살며보면 주문 상품 수를 리턴할 뿐만 아니라, 기프트 포인트까지 추가하고 있다.
  • itemCount 메서드를 사용했던 사람은 이런 동작까지 예측하지 못할 것이다.
  • 놀람 최소화 원칙이란 사용자가 예상하지 못한 놀라움을 최소화하도록 설계해야 한다는 원칙이다.
  • 처음에는 로직과 의도가 일치하게끔 구현했다고 해도, 사양을 변경하면서 별 생각 없이 기존 메서드에 로직을 추가하는 경우가 있다.
  • 이렇게 되면 이름과 로직 사이에 점차 괴리가 생기면서 놀람 최소화 원칙을 위반하게 된다.
  • 로직을 변경할 때는 항상 놀람 최소화 원칙을 신경써야 한다. 로직과 이름 사이에 괴리가 있다면 이름을 수정하거나, 메서드와 클래스를 의도에 맞게 따로 만들어라.

10.5 구조에 악영향을 미치는 이름

이름이 클래스 구조에 큰 악영향을 미칠 수도 있다.

10.5.1. 데이터 클래스처럼 보이는 이름

  • ~Info와 ~Data 같은 이름은 데이터 클래스라는 이미지를 심어준다.
  • ProductInfo는 Product로 개선하는 것이 좋다.

DTO(Data Transfer Object)

  • 예외적으로 데이터 클래스를 사용하는 경우가 있다.
  • 데이터 전송 용도로 사용되는 디자인 패턴인 DTO이다.
  • 데이터 클래스를 절대 사용해서는 안된다는 것이 아니다.

10.5.2. 클래스를 거대하게 만드는 이름

  • 예를 들어 Manager라는 이름의 클래스는 여러 책무를 떠안기 쉽다.
  • ‘관리’라는 단어가 가진 의미가 너무 넓고 애매하기 때문이다.
  • 너무 많은 책임을 떠안아서 단일 책임 원칙을 위반하면 안된다. 책무가 다른 로직은 다른 클래스로 정의하라.

10.5.3. 상황에 따라 의미가 달라질 수 있는 이름

  • 예를 들어 Account는 금융 업계에서는 계좌를 의미하지만 컴퓨터 보안에서는 로그인 권한을 의미한다.
  • 즉, 상황에 따라 의미가 달라지는 것이다.
  • 컨텍스트가 다르다면 각 컨텍스트를 서로 다른 패키지로 선언한다.
  • 그리고 각 패키지에 같은 클래스를 만들고 느슨하게 결합하도록 한다.

10.5.4. 일련번호 명명

  • A1, A2, A3 … 처럼 메서드 이름에 번호를 붙여 만드는 것을 일련번호 명명이라고 한다.
  • 목적과 의도를 알기 어려울뿐만 아니라 구조를 수정하기가 굉장히 어렵다.

10.6 이름을 봤을 때, 위치가 부자연스러운 클래스

  • 다른 클래스로 이동시켜야 자연스러운 메서드가 있다.
  • 이처럼 있어야할 곳에 있지 않은 부자연스러움은 이름을 통해 판단할 수 있다.

10.6.1. 동사+목적어 형태의 메서드 이름 주의하기

  • 관심사와 전혀 관계없는 메서드가 클래스에 추가되는 경우가 많다.
  • 서둘러서 구현하려고 할 때, 기존 클래스만 가지고 어떻게든 끝내고자 무리하게 구현했을 때 이렇게 되기 쉽다.
  • 동사+목적어로 이루어진 이름은 관계없는 책무를 가진 메서드일 가능성이 있다.

10.6.2. 가능하면 메서드의 이름은 동사 하나로 구성되게 하기

  • 관심사가 다른 메서드가 섞이지 못하게 막으려면 되도록 메서드의 이름이 동사 하나로 구성되도록 설계하는 것이 좋다.

10.6.3. 부적절한 위치에 있는 boolean 메서드

  • boolean 자료형을 리턴하는 메서드도 적절하지 않은 클래스에 정의되어 있는 경우가 많다.
  • boolean 자료형의 메서드를 추가할 때는 ‘클래스 is 상태’ 형태로 읽어봤을 때 자연스러운 영어 문장이 되는지 확인해보자.
class Member {
	boolean isInConfusion() {
		return states.contains(StateType.confused);
	}
}

10.7 이름 축약

  • 이름을 생략할 때는 주의해야 한다.

10.7.1. 의도를 알 수 없는 축약

  • 긴 이름이 싫어서 이름을 축약하는 경우가 있다.
  • 축약을 하게 되면서 의도를 알 수 없게 되는 경우가 발생하므로 주의해야 한다.

10.7.2. 기본적으로 이름은 축약하지 말기

  • 변수명뿐만아니라 모든 이름에서 축약하지 않고 작성하는 것이 좋다.
  • 물론 이름 생략을 완전히 금지해야한다는 말은 아니다.
  • 일반적으로도 축약한 이름이 통용된다면 축약해도 괜찮다. 예를 들어 SNS, VIP 등이다.

10.7.3. 이름을 축약할 수 있는 경우

  • for문의 카운터 변수는 관습적으로 i와 j처럼 짧은 한 글자의 문자로 표현하는 경우가 많다.
  • 또한 Go 언어 등의 일부 프로그래밍 언어에서는 짧은 변수 이름을 선호하기도 한다.