2014.04.22 23:32

코멘트 (Comment) 리팩토링에 대해 알아보자.

마틴 파울러는 '코드의 부족을 보충하기 위해 상세한 주석이 있는 것' 또한 나쁜 냄새라고 지적했다.


그렇다면 '코드의 부족(lack of code)'이라는 무슨 의미일까? 

작성된 코드가 '대체로 잘 동작하지만, 완벽하다고 말하기는 어렵다는 의미이다.

프로그래머가 일정에 밀려서 미치 완성하지 못한 것일 수도 있고, 정말 귀찮아서 보완하지 못한 것일 수도 있다.

아니면, 실력이 없거나... 누구나 완벽한 것이 가장 좋다고 말하지만, 어디 현실이 그러한가? 

어쩔 수 없다는 핑계와 양해를 주거니 받거니 하면서 살아가는 것이다.


하지만, 백번 양보를 해도 코드가 완벽하지 못하니 알아서 잘 쓰라고 (조심하라는 의미에서...)

주석(comment)에 설명을 붙이는 것은 당신이 가는 길에 지뢰가 있으니 알아서 피하라는 표지판과 다름이 없다.


  • 표지판을 못 볼 수도 있다. (모든 개발자가 주석을 보고 나서 함수를 가져다 쓴다는 보장은 없고,
    현실적으로 많은 이들이 주석을 읽으려 하지 않는다.
  • 표지판을 보고도 그냥 지나칠 수 있다.
    설마하는 사람도 있고, 주석을 보고도 함수(혹은 API)를 가져다 쓰는 사람은 자신에게는 아무런 책임이 없다고 생각하기도 한다.
  • 주석은 실행에 아무런 영향을 주기 때문에, 경고성 주석이 유지보수 혹은 코드 정리 과정에서 사라질 수도 있다.


결론은 프로그램에 잠재적인 오류가 있다거나, 오용(잘못된 사용) 가능성이 있다면
주석으로 문제를 영원히 덮을 수 없다는 것이다.


코딩 사례를 보고 이해해보도록 하자. 많은 실무 프로젝트에서 공통 API (common API)를 만들어 사용하게 되는데 회원 정보를 대다수의 사이트에서 '나이를 계산하는 공통 함수'를 만들어 사용한다. 나이를 계산하기 위해서는 '생년월일'을 입력 받아야 하고, 개발 편의성을 높이기 위해, 굳이 Date 타입으로 입력받지 않고 문자열로 태어난 날짜를 입력 받도록 한다. 여기서 '코드의 부족(lack of code' 문제가 자주 발생하게 된다. 공통 함수이다 보니 가져다 쓰는 곳이 많고, 오류가 발생하더라도 원인 파악이 손쉬워야 함에도 불구하고 간단한 함수라 여겨 대충 만드는 일이 많다.


아래 코드는 주석에 잘못된 입력 값이 들어왔을 경우, 적절한 예외가 발생한다는 것을 명시하였다.

만일, 공통 API를 사용하는 개발자가 주석을 읽지 않고 API를 호출하더라도 잘못된 입력 값(인자 혹은 parameter) 오류의 원인을 파악할 수 있는 '메시지'가 예외 객체에 포함되기 때문에 문제를 빨리 해결할 수 있다. 


만일, 예외처리하는 로직 없이 메소드 주석에 날짜 형식은 'yyyyMMdd'이라는 안내문구를 적어둔 경우는 '코드의 부족을 위해 상세한 주석이 있는 나쁜 냄새가 나는 코드'가 되는 것이다.


package common.util;

import java.sql.Date;

import org.joda.time.DateTime;
import org.joda.time.Years;

/**
 * 업무구분 : 공통 (cn)
 * 업 무 명 : 공통 API
 * 파 일 명 : AgeUtils.java
 * 작 성 일 : 2014-04-22
 * 설    명 : 현재 혹은 기준 일자를 기준으로 한국 나이, 만 나이를 계산하는 유틸리티 메소드들을 제공한다.
 */
public final class AgeUtils {

	private AgeUtils() {
		// This class does not provider public constructor
	}

	/**
	 * 현재 날짜를 기준으로 생년월일에서 한국식 나이를 구한다.
	 * 
	 * AgeUtils.getAge("19731201"); = 41 (현재 년도가 2013년인 경우)
	 * 
	 * @param birthDate 생년월일 (yyyyMMdd 형식)
	 * @return 현재 일자를 기준으로 계산된 한국식 나이
	 * @throws IllegalArgumentException null 혹은 잘못된 형식의 날짜를 입력한 경우 예외 발생
	 */
	public static int getAge(String birthDate) {
		return getAge(birthDate, DateUtils.getTodayString());
	}

	/**
	 * 기준일자를 기준으로 생년월일에서 한국식 나이를 구한다.
	 * 
	 * AgeUtils.getAge("19731201", "20121225"); = 40
	 * 
	 * @param birthDate 생년월일 (yyyyMMdd 형식 혹은 yyyy 형식)
	 * @param refDate 기준일자 (yyyyMMdd 형식 혹은 yyyy 형식)
	 * @return 한국식 나이
	 * @throws IllegalArgumentException null 혹은 잘못된 형식의 날짜를 입력한 경우 예외 발생
	 */
	public static int getAge(String birthDate, String refDate) {
		if (StringUtils.isEmpty(birthDate) || StringUtils.isEmpty(refDate)) {
			throw new IllegalArgumentException("birthDate or refDate parameter is empty.");
		} else if (birthDate.length() < 4 || refDate.length() < 4) {
			throw new IllegalArgumentException("birthDate or refDate parameter length is too short (must longer than 3).");
		} else if (!StringUtils.isNumeric(birthDate) || !StringUtils.isNumeric(refDate)) {
			throw new IllegalArgumentException("birthDate or refDate parameter is not numeric data.");
		}
		if (birthDate.length() == 4) {
			return Integer.parseInt(refDate) - Integer.parseInt(birthDate) + 1;
		} else {
			return getAge(DateUtils.toDate(birthDate), DateUtils.toDate(refDate));
		}
	}

	/**
	 * 생년월일과 기준 일자를 비교하여 한국식 나이를 반환한다.
	 * 
	 * Date birthDate = DateUtils.toDate("20120101");
	 * AgeUtils.getAge(birthDate, DateUtils.toDate("20120201")); = 1
	 * AgeUtils.getAge(birthDate, DateUtils.toDate("20130101")); = 2
	 * 
	 * @param birthDate 생년월일 (yyyyMMdd 형식)
	 * @param refDate 기준일자 (yyyyMMdd 형식)
	 * @return 한국식 나이
	 * @throws IllegalArgumentException null 혹은 잘못된 형식의 날짜를 입력한 경우 예외 발생
	 */
	public static int getAge(Date birthDate, Date refDate) {
		if (birthDate == null || refDate == null) {
			throw new IllegalArgumentException("Invalid argument value. birthDate = '" + birthDate + "', refDate = '" + refDate + "'");
		} else {
			return new DateTime(refDate).getYear() - new DateTime(birthDate).getYear() + 1;
		}
	}

	/**
	 * 현재 일자를 기준으로 생년월일에서 만 나이를 구한다.
	 * 
	 * AgeUtils.getRealAge("19731201"); = 39 (현재 년도가 2013년인 경우)
	 * 
	 * @param birthDate 생년월일 (yyyyMMdd 형식)
	 * @return 만 나이
	 * @throws IllegalArgumentException null 혹은 잘못된 형식의 날짜를 입력한 경우 예외 발생
	 */
	public static int getRealAge(String birthDate) {
		return getRealAge(birthDate, DateUtils.getTodayString());

	}

	/**
	 * 생년월일과 기준 일자를 비교하여 만 나이를 계산한다.
	 * 
	 * AgeUtils.getRealAge("19731201", "20121125"); = 38
	 * AgeUtils.getRealAge("19731201", "20121225"); = 39
	 * 
	 * @param birthDate 생년월일 (yyyyMMdd 형식)
	 * @param refDate 기준일자 (yyyMMdd 형식)
	 * @return 만 나이
	 * @throws IllegalArgumentException null 혹은 잘못된 형식의 날짜를 입력한 경우 예외 발생
	 */
	public static int getRealAge(String birthDate, String refDate) {
		return getRealAge(DateUtils.toDate(birthDate), DateUtils.toDate(refDate));
	}

	/**
	 * 생년월일과 기준 일자를 비교하여 만 나이를 계산한다.
	 * 
	 * Date birthDate = DateUtils.toDate("20120101");
	 * AgeUtils.getRealAge(birthDate, DateUtils.toDate("20120201")); = 0
	 * AgeUtils.getRealAge(birthDate, DateUtils.toDate("20130101")); = 1
	 * 
	 * @param birthDate 생년월일 (Date 타입)
	 * @param refDate 기준일자 (Date 타입)
	 * @return 만 나이
	 * @throws IllegalArgumentException null 혹은 잘못된 형식의 날짜를 입력한 경우 예외 발생
	 */
	public static int getRealAge(Date birthDate, Date refDate) {
		if (birthDate == null || refDate == null) {
			throw new IllegalArgumentException("Invalid argument value. birthDate = '" + birthDate + "', refDate = '" + refDate + "'");
		} else {
			return Years.yearsBetween(new DateTime(birthDate), new DateTime(refDate)).getYears();
		}
	}
}



AgeUtils.java


Posted by 善 곽중선
2014.04.20 23:32
중복된 코드를 리팩토링(refactoring)하는 방법을 알아보자. 
아래 예제 소스는 자바에서 '정수형 연산'과 '실수형 연산' 처리의 수행 시간 차이를 대략적으로 알아보기 위해 작성되었다.
잘 살펴보면 중복된 코드가 눈에 띈다. 수행 시간(elapsed time)을 계산하는 로직이 2번에 걸쳐 반복되어 코딩되어 있다.
   
import java.util.Calendar;

public class DuplicatedCodes {

	private static long timeInMillis;

	/**
	 * 정수형 및 실수형 연산 처리 속도를 비교하는 테스트 프로그램
	 * 
	 * @param args
	 */
	public static void main(String[] args) {

		long elapsedTime;

		timeInMillis = Calendar.getInstance().getTimeInMillis();
		int intSum = 0;
		// 정수형 변수를 100만번 더한다.
		for (int i = 0; i < 1000000; i++) {
			intSum += 100;
		}
		elapsedTime = Calendar.getInstance().getTimeInMillis() - timeInMillis;
		System.out.println("정수형 : " + elapsedTime + "밀리초");

		timeInMillis = Calendar.getInstance().getTimeInMillis();
		float floatSum = 0;
		// 실수형 변수를 100만번 더한다.
		for (int i = 0; i < 1000000; i++) {
			floatSum += 100.0;
		}
		elapsedTime = Calendar.getInstance().getTimeInMillis() - timeInMillis;
		System.out.println("실수형 : " + elapsedTime + "밀리초");
	}
}


눈에 잘 띄는 중복 코드는 아래와 같다.

elapsedTime = Calendar.getInstance().getTimeInMillis() - timeInMillis;


리팩토링을 실시하기 전에 '중복된 코드를 리팩토링 해야 하는 이유는 무엇인가?' 라는 질문에 답해보자.


  • 재사용성(reuseablity)가 떨어지기 때문이며, 동일한 코드를 반복해서 사용하는 것은 노동력이 많이 소모된다.
  • 중복된 코드는 버그가 발견되거나 수정 사항이 발생할 때마다 고쳐야 하는 노력이 많이 든다.
    (중복된 횟수 만큼의 수정 작업을 반복해야 한다.)
  • 여러 위치에 흩어져 있기 때문에 로직(logic)에 대한 문서화 혹은 설명을 작성하기 이렵다.
  • 중복된 상태로 그대로 두었다가 누군가 흩어진 중복 코드 중에서 한 곳만 고쳐야 할 경우,
    원래의 의도와 달리 처음에는 같은 기능을 하던 코드들이 다른 방식으로 동작하게 되는 문제가 발생할 수 있다.


중복된 코드를 리팩토링하는 것은 다음과 같은 장점을 가지고 있다.


  • 다양한 리팩토링 기법 중에서 가장 우선순위를 높여서 해야 한다. (아무리 바쁘고  시간이 없더라도...)
  • 리팩토링 기법 중에서 가장 효과가 좋다. (중복 코드 제거를 안한 코드의 유지보수, 장애 처리 비용은 상상을 초월한다.)
  • 설계를 개선하고, 코드의 품질을 높이는 가장 빠르고, 확실한 방법이다. (개발자의 레벨이 상승한다.)
  • 중복된 코드가 제거된 코드는 문서화 하기 용이하고, 코드 가독성(readability)가 향상될 가능성이 높다.


반면에 다음과 같은 단점도 있다. (과유불급, 지나치면 모자르니만 못하다.)


  • 자칫하면 코드의 복잡도가 높아져 코드를 이해 하기 어렵게 된다.
  • 중복 코드를 방지하는 과정에서 미약하나마 성능 저하가 발생할 수 있다.
  • 지나치게 '중복 방지'에 집착하다보면, 제한된 시간과 노력을 낭비할 가능성이 있다.
    (중복 코드가 많지 않거나, 아주 단순한 코드임에도 리팩토링을 하려고 애쓸 필요는 없다.)


중복된 코드를 제거하는 방법은 반복적인 코드 블럭을 함수(function or method)로 변경하는 것이다.

앞서 제시한 예제에서 중복된 코드를 리팩토링한 결과는 다음과 같다.   

import java.util.Calendar;

public class DuplicatedCodes {

	private static long timeInMillis;

	/**
	 * 정수형 및 실수형 연산 처리 속도를 비교하는 테스트 프로그램
	 * 
	 * @param args
	 */
	public static void main(String[] args) {

		timeInMillis = Calendar.getInstance().getTimeInMillis();
		int intSum = 0;
		// 정수형 변수를 100만번 더한다.
		for (int i = 0; i < 1000000; i++) {
			intSum += 100;
		}
		System.out.println("정수형 : " + calcElapseTime() + "밀리초");

		float floatSum = 0;
		// 실수형 변수를 100만번 더한다.
		for (int i = 0; i < 1000000; i++) {
			floatSum += 100.0;
		}
		System.out.println("실수형 : " + calcElapseTime() + "밀리초");
	}

	/**
	 * 경과 시간을 계산한다.
	 * 
	 * @return 경과된 시간을 밀리초(millseconds) 단위로 반환한다.
	 */
	private static long calcElapseTime() {
		long currentTimeMillis = Calendar.getInstance().getTimeInMillis();
		long elapsedTime = currentTimeMillis - timeInMillis;
		timeInMillis = currentTimeMillis;

		return elapsedTime;
	}

}


위와 같이 코드를 변경했을 때, 어떤 효과를 기대할 수 있을까? 혹은 무엇이 좋아진 것일까?


  • 요건 혹은 기능이 변경되었을 때, 손쉽게 고칠 수 있다. (예를 들어, 수행 시간을 초 단위로 출력하게끔 바꾸어야 하는 경우)
  • 중복된 코드 블록을 함수로 모듈화하면서, 코드 길이가 짧아진다.
  • 함수 명칭을 통해 기능을 파악할 수 있으므로, 로직을 파악하기 쉬워지고,
    함수에 주석을 추가하는 방식으로 문서화를 할 수 있다.

DuplicatedCodes.java



Posted by 善 곽중선
2014.04.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.04.20 18:09

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

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


[코드 개선 활동의 종류]


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

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

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

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

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

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


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

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


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

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


[리팩토링의 목적]


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

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

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

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


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


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

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

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



Posted by 善 곽중선