2014. 4. 20. 22:02

마틴 파울러가 언급한 22가지 코드의 악취(Bad Smells in Code)


 중복된 코드 (Duplicated Code)

 코드가 여기저기 겹쳐 있다.

 너무 긴 메소드  (Long Method) 메소드가 너무 길다.
 거대한 클래스 (Large Class) 클래스의 파일이나 메소드가 너무 많다. 
 너무 많은 인수 (Long Parameter List) 메소드에 전달하는 인수의 수가 너무 많다.

 변경의 발산 (Divergent Change)

 사양변경이 발생한 경우 수정할 곳이 여기저기 흩어져 있다.
 변경의 분산 (Shotgun Surgery)

 어떤 클래스를 수정하면 다른 클래스도 수정하지 않으면 안 된다. 

 속성, 조작의 부적절한 관계 (Feature Envy)

 언제나 다른 클래스의 속성을 건드리고 있다.

 데이터 덩어리 (Data Clump)

 정리해서 다룰 수밖에 없는 여러 개의 데이터가 하나의 클래스에 정리되어 있지 않다. 

 기본 데이터형의 집착 (Primitive Obsession)

 클래스를 만들지 않고 int같은 기본 데이터형만을 사용한다.

 switch문 (Switch Statements)

 switch문이나 if문을 사용하여 동작을 분할하고 있다.

 평행 상속 구조 (Parallel Inheritance Hierarchies)

 서브클래스를 만들면 클래스 계층에 따로 서브클래스를 만들어야 한다. 

 게으름뱅이 클래스 (Lazy Class)

 클래스가 별로 하는 일이 없다. 

 추측성 일반화 (Speculative Generality)

 언젠가 이렇게 확장하겠지 하고 기대하는 지나친 일반화 

 일시적 속성 (Temporary Field)

 일시적으로 사용할 필드가 있다.

 메시지의 연쇄 (Message Chains)

 메소드가 호출하는 연쇄가 너무 많다.

 중개자 (Middle Man)

 위양(권리를 위임)하고 자신이 하는 일은 없는 클래스가 있다. 
 부적절한 관계 (Inappropriate Intimacy)

 필요 없는 쌍방향 링크가 걸려 있거나 IS-A 관계가 아니면서 상속을 사용한다

 클래스의 인터페이스 불일치
 (Alternative Classes with Different Interface)
 API가 부적절하다.
 미숙한 클래스 라이브러리
 (Incomplete Library Class)
 기존의 클래스라이브러리가 사용하기 힘들다.

 데이터 클래스 (Data Class)

 필드와 getter 메소드와 setter 메소드만 가지고 있는 클래스가 있다.
 상속거부 (Refused Bequest) 상속하고 있는 메소드면서 그것을 호출하면 문제가 발생한다.

 코멘트 (Comment)

 코드의 부족을 보충하기 위해 상세한 코멘트가 있다 


Posted by 곽중선
2014. 4. 20. 18:09

리팩토링(refactoring)은 다양한 코드 개선 활동 중 하나이다.

그렇다면 코드 개선 활동이란 무엇인가? 더 나은 코드를 만드는 모든 노력을 의미한다.


[코드 개선 활동의 종류]


- 코드 이해를 돕기 위한 주석(comment) 작성

- 보다 높은 성능을 보장하고, 적은 자원(resource : memory, file 등) 위한 튜닝(tunning)

- 더 나은 혹은 다양한 알고리즘 적용

- 코드 가독성(readability)를 높이고, 일관된 표준 적용을 위한 코드 포맷팅(formatting)

- 불필요하거나 오동작할 수 있는 코드 제거 (예를 들어, 입력 값 범위 체크 혹은 null 값 검사)

- 그리고, 리팩토링(refactoring)


이중에서 refactoring 행위에 대해 마틴 파울러는 코드의 

나쁜 냄새(Bad smells in codes)를 제거하는 것이라고 정의한다.


코드의 나쁜 냄새는 '이해하기 어렵거나, 수정하기 어렵고, 확장하기 어려운 상태'라고 표현할 수 있다.

그런데, 이런 것들을 제거하는 목적은 무엇일까? 그리고, 리팩토링이 다른 코드 개선 노력과 다른 점은 무엇인가?


[리팩토링의 목적]


- 다수에 의한 '공동 개발 (협력 개발)'을 이롭게 하는 것이다.

- 또다른 목적은 '코드의 품질'을 높여 장기적으로 이미 작성된 코드 덩어리가

 '쓰레기(junk)' 혹은 '스파게티' 코드로 전락하는 것을 막는 것이다. 

  ('나쁜 냄새의 의미를 더 잘 이해할 수 있다. 나쁜 냄새는 부패하기 시작하는 모든 것들을 알아 차릴 수 있는 신호다.)


[리팩토링이 다른 코드 개선 노력과 다른 점]


- 프로그램의 기능을 변경하지 않는다.

- 수행 속도, 자원 사용량 등 성능 및 효율 개선을 목표로 하지 않는다.

- 코드 자체 보다 코드를 다루는 인간(프로그래머 자신과 동료)을 위한 배려가 담겨 있다.



Posted by 곽중선
2012. 2. 7. 17:35
본 포스트는 이터너티님의 DDD(Domain-Driven Design) 해설을 학습한 과정을 정리한 것입니다.

PART 1. VALUE OBJECT와 REFERENCE OBJECT

어플리케이션을 구성하는 객체는 Value Object와 Reference Object로 나뉜다. VO와 RO를 구분하는 기준에 대해서 이터니티 님은 아래와 같은 가이드를 제시한다.

시스템 내에서 해당 객체를 계속 추적해야 하는가객체가 표현하는 개념이 유일하게 하나만 존재해야 하는가? 그렇다면 REFERENCE OBJECT로 만든다단지 객체가 추적할 필요가 없는 단순한 값인가속성값이 동일하면 동일한 객체로 간주해도 무방한가고민할 필요 없다그냥 VALUE OBJECT로 만든다.

Reference Object는 유일하게 존재해야 하는 것이라고 말한다. 어떤 것들이 해당할까?

- 유일하게 존재한다는 것은 객체들이 같거다 혹은 다르다는 점을 명확히 구분할 수 있어야 한다. 달리 말해, ID를 가지는 객체들을 말한다.
- 물리적인 장치(DB, file system)에 저장되거나, 네트워크를 통해 전송되는 객체들이다. 잠시 만들었다가 사라지는 데이터에 추적할 필요가 없기 때문이다.

이러한 두가지 특징을 가지는 객체들은 무엇이 있을까? 가장 좋은 사례는 DB에 저장되며, 기본 키(primary key)를 가지는데이터(혹은 엔티티)일 것이다. Reference Object로 정의해야 것들은 최종적으로 DB에 저장되는 객체들이라고 간주하는 것이 무방하다.
 
동일함의 의미 

앞서, Reference Object들은 유일하게 존재해야 한다고 정의했다. 그렇다면, Reference Object를 만들어 쓴다는 것은  유일한 객체임을 보장할 수 있는 수단이 제공되어야 한다는 말이다. 이너니티님의 글을 다시 인용한다.

모든 객체 지향 시스템은 생성된 객체에게 고유한 식별자(identity)를 부여한다. 대부분의 객체 지향 언어는 객체가 위치하고 있는 메모리 상의 주소를 객체의 식별자로 할당하고 이 주소 값을 사용하여 객체를 구별한다.
 
그렇다면 자바에서 객체의 고유 식별자를 알아내는 방법이 있을까? 그리고, 고유 식별자를 이용해 유일성(uniqueness)을 보장받을 수 있을까?

In java there is no any specific method that provides us the object's ID. But each object has its own unique hash value which can be treated as unique Id for that object.
- Rose India 인용

자바에서는 해시 코드(hash code)를 객체의 고유 식별자로 사용할 수 있다고 한다. 그런데, 유일성을 확실히 보장해주지 않는다고 말한다.이게 대체 무슨 말인가? 아래와 같이 설명할 수 있다.

- 동일한 객체는 항상 동일한 해시 코드를 반환한다.
- 하지만, 서로 다른 객체가 동일한 해시 코드를 반환 할 수도 있다.

달리 말해서, 동명이인(同名異人)이 존재할 수도 있다는 말이다. 즉, 해시 코드는 우리가 특정 사람을 지칭할 때 쓰는 이름과 같은 것이다. 그래서, 이너니티님은 아래와 같이 가이드하고 있다.

각 언어는 객체의 식별자를 비교할 수 있는 연산자를 제공하는데 Java의 경우 “==”와 “!=” 연산자를 사용한다. 두 참조가 가리키는 객체가 동일한 식별자를 가지는 경우, 즉 동일한 주소에 위치하는 경우 “==” 연산자는 true를 반환한다.

“==” 연산자를 사용하여 동일성을 판단하기 보다는 equals() 메소드를 오버라이딩하여 금액의 동등성을 테스트해야 한다.

한 발 더 나아가, 동일성(identity)와 동등성(equality)의 차이에 대해서 얘기해 보자.

- 동일성(identity)은 비교하려는 두 개의 객체가 동일한 식별자를 반환할 경우 같다고 판단하는 것이다.
- 동등성(equality)는 두 객체의 내용 (혹은 데이터)이 일치할 경우, 같다고 판단하는 것이다.

대부분의 상황에서는 '동일성 비교'를 기본으로 적용한다. 그러나, DDD(Domain Driven Design)에서는 동등 비교를 원칙으로 한다. (DDD에서는 데이터를 안전하고, 정확하게 사용하는 것이 주된 관심사이다.) 그러니 개발자들은 Reference Object 및 Value Object를 정의함에 있어서 어떻게 동등 비교를 실현할 수 있는지 그 방법론을 정확히 이해해야 한다.

동일 비교 시에 발생하는 문제

일반적인 동일 비교 방식을 적용했을 때, 발생하는 문제에 대해서 간단한 코드를 작성해서 테스트해 보도록 하자. 먼저, 금액을 나타내는 클래스를 작성해 보았다.

 
package org.eternity.customer;

public class Money {
	private long amount;
	
	public Money(long amount) {
		this.amount = amount;
	}
	
	public Money(int amount) {
		this.amount = amount;
	}

	public String toString() {
		return String.valueOf(amount);
	}
}
같은 데이터를 가지고 있지만, 다른 객체로 인식되는 경우를 확인해 보자. 단위 테스트 코드를 아래와 같이 작성했다. 서로 다른 객체이지만, 분명 동일한 금액을 가지고 있다.

package test.eternity.customer;

import static org.junit.Assert.*;

import org.eternity.customer.Money;
import org.junit.Test;

public class MoneyTest {

	@Test
	public void testAmount() {
		Money money1 = new Money(1000);
		Money money2 = new Money(1000);
	
		System.out.println( "money1's hash code = " + money1.hashCode() );
		System.out.println( "money2's hash code = " + money2.hashCode() );

		// money1 == money2 (?)
		assertEquals(money1, money2);
	}

}
테스트는 실패하고 만다. 서로 다른 객체로 판정하는 것이다. 왜 이런 결과가 나오는지 확인하기 위해, 해시 코드의 출력 값을 확인해 보면 아래와 같다. (해시 코드 값은 실행할 때마다 달라질 수 있다.)

money1's hash code = 4565111
money2's hash code = 20392474
이제 동등 비교를 하기 위해 Money 클래스를 아래와 같이 수정했다.

package org.eternity.customer;

public class Money {
	private long amount;
	
	public Money(long amount) {
		this.amount = amount;
	}
	
	public Money(int amount) {
		this.amount = amount;
	}

	public String toString() {
		return String.valueOf(amount);
	}
	
	public boolean equals(Object other) {
		if(this == other) 
			return true;
		
		if(!(other instanceof Money))
			return false;
		
		if( this.amount == ((Money)other).amount )
			return true;
		
		return false;
		
	}
}
이제 단위 테스트를 통과했다. 그런데, 뭔가 문제가 남아 있다. 테스트를 통과했는데 뭐가 문제냐고? 두 개의 객체가 동일하다는 결과가 나오는데 해시 코드는 서로 다르게 출력되는 것이다. 자바의 기본 원칙을 위배한 것이다.

Equal objects must produce the same hash code as long as they are equal, however unequal objects need not produce distinct hash codes. - Equals and Hash Code 참조 

자바의 원칙을 준수하면서, 동등 비교를 구현하기 위해서는 equals() 메소드와 hashCode() 메소드를 함께 오버라이드(override) 해야만 한다.


package org.eternity.customer;

public class Money {
	private long amount;
	
	public Money(long amount) {
		this.amount = amount;
	}
	
	public Money(int amount) {
		this.amount = amount;
	}

	public String toString() {
		return String.valueOf(amount);
	}
	
	public boolean equals(Object other) {
		if(this == other) 
			return true;
		
		if(!(other instanceof Money))
			return false;
		
		if( this.amount == ((Money)other).amount )
			return true;
		
		return false;
		
	}
	
	public int hashCode()
	{
		return (int)(amount ^ (amount >>> 32));
	}
}
이제 테스트를 수행하면 두 객체가 동일하다고 판정되고, 해시 코드 역시 동일한 값을 출력한다.
money1's hash code = 1000
money2's hash code = 1000
hashCode() 메소드는 자바의 컬렉션 클래스들(List, Map 등) 에서 보이지 않게(?) 호출되며, 다양한 API, 라이브러리에서 객체를 관리하기 위한 목적으로 해시 코드 반환 값을 사용하기 때문에 equals() 함수를 오버라이드(override)할 경우에는 잊지 말고, 함께 구현해야 한다. 혹여 잊어 버린다고 해서 컴파일 오류는 나지 않는다. 하지만, 예기치 않은 문제가 발생할 경우에는 원인을 추적하기 매우 어렵게 된다. 어떤 문제가 발생할 수 있는지, hashCode() 함수를 오버라이드 하지 않고 아래와 같은 테스트 코드를 작성했더니, 역시나 문제가 발생했다. 길지 않은 코드이니 직접 컴파일 한 후 확인해 보시는 것을 권장한다.
package test.eternity.customer;

import static org.junit.Assert.*;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import org.eternity.customer.Money;
import org.junit.Test;

public class MoneyTest {

	@Test
	public void testAmount() {
		Money money1 = new Money(1000);
		Money money2 = new Money(1000);
		
		System.out.println( "money1's hash code = " + money1.hashCode() );
		System.out.println( "money2's hash code = " + money2.hashCode() );
	
		// money1 == money2
		assertEquals(money1, money2);
	}
	
	@Test
	public void setSet() {
		Money money1 = new Money(1000);
		Money money2 = new Money(1000);
		
		Set moneyPocket = new HashSet();
		moneyPocket.add(money1);
		assertEquals(1, moneyPocket.size());
		moneyPocket.add(money2);
		assertEquals(2, moneyPocket.size());
		
		Map moneyMap = new HashMap();
		moneyMap.put(money1, 1);
		moneyMap.put(money2, 2);
		
		assertEquals(1, moneyMap.size());
	}
}
Posted by 곽중선