•
이 장에서는 좋은 메소드와 나쁜 메소드의 차이를 만드는 특성을 중심으로 메소드에 대해 자세히 살펴본다.
◦
메소드 설계에 영향을 미치는 요인들은 앞서 5장에서 살펴보았다.
◦
좋은 메소드의 몇 가지 중요한 특성은 8장에도 소개되어 있다.
◦
메소드와 클래스를 작성하는 단계가 궁금하다면 9장을 참고하자.
•
메소드란 무엇인가? 메소드란 한 가지 목적을 위해서 호출할 수 있는 개별 함수나 프로시저를 의미한다.
•
좋은 메소드란 무엇인가? 이는 정의내리기 어렵다. 다음 코드를 참고하면서 좋지 않은 메소드들이 가지는 특징을 알아보자.
void HandleStuff( CORP_DATA & inputRec, int crntQtr, EMP_DATA empRec,
double & estimRevenue, double ytdRevenue, int screenX, int screenY,
COLOR_TYPE & newColor, COLOR_TYPE & prevColor, StatusType & status,
int expenseType )
{
int i;
for ( i = 0; i < 100; i++ ) {
inputRec.revenue[i] = 0;
inputRec.expense[i] = corpExpense[ crntQtr ][ i ];
}
UpdateCorpDatabase( empRec );
estimRevenue = ytdRevenue * 4.0 / (double) crntQtr;
newColor = prevColor;
status = SUCCESS;
if ( expenseType == 1 ) {
for ( i = 0; i < 12; i++ )
profit[i] = revenue[i] - expense.type1[i];
}
else if ( expenseType == 2 ) {
profit[i] = revenue[i] - expense.type2[i];
}
else if ( expenseType == 3 )
profit[i] = revenue[i] - expense.type3[i];
}
C++
복사
◦
메소드 이름이 좋지 않다.
▪
HandleStuff()는 메소드가 무엇을 하는지 말해주지 않는다.
◦
메소드에 대한 설명이 없다.
◦
메소드의 레이아웃이 엉망이다. 이 코드를 보고 논리적인 구조를 이해하기가 쉽지 않다.
▪
코드 레이아웃이 원칙없이 서로 다른 부분에서 서로 다른 방식으로 사용되었다. expenseType == 2와 expenseType == 3이 그렇다.
◦
메소드의 입력 변수인 inputRect가 변경된다. 입력 변수의 값은 변경되지 않아야 한다.
▪
변수의 값을 변경할 생각이라면 이름이 inputRect여도 안된다.
◦
메소드가 전역 변수를 읽고 쓴다.
▪
이 메소드는 corpExpense 값을 읽고 profit에 쓴다. 메소드가 직접 전역 변수를 읽고 쓰는 대신 해당 기능을 제공하는 다른 메소드를 호출하는 것이 좋다.
◦
메소드의 목적이 하나가 아니다.
▪
여기서 사용된 메소드는 변수를 초기화하고 데이터베이스에 접근하고 몇 가지 계산을 수행하는데 이들은 아무런 연관성이 없는 것처럼 보인다.
▪
메소드는 분명하게 정의된 하나의 목적만을 가져야 한다.
◦
메소드가 잘못된 데이터로부터 자신을 방어하지 않는다.
▪
crntQtr이 0이면 ytdRevenue * 4.0 / (double) crntQtr 표현식은 0으로 나누기 오류를 발생시킨다.
◦
메소드가 여러 가지 매직 넘버를 사용하고 있다.
◦
메소드의 매개변수 중 사용하지 않는 것들이 존재한다.
▪
screenX와 screenY는 참조하지 않는다.
◦
메소드의 매개변수가 너무 많다.
▪
사람들이 이해할 수 있는 매개변수의 최대 갯수는 7개다. 하지만 이 메소드는 11개의 매개변수를 가지고 있다.
▪
매개변수의 배치도 대부분의 사람들이 자세히 살펴보기 어렵게 배치되어있다.
◦
메소드의 매개변수가 잘못 정렬되어 있으며 문서화도 되어있지 않다.
•
메소드는 프로그래밍의 다른 어떤 기능보다도 프로그램을 읽고 이해하기 쉽게 만든다. 따라서 앞의 코드와 같이 메소드를 사용하는 것은 범죄와 마찬가지다.
1. 메소드를 작성하는 이유
•
복잡성을 줄인다.
◦
프로그램의 복잡성을 줄이는 것이 메소드를 작성하는 가장 중요한 이유다.
◦
데이터를 처리하는 메소드를 작성하면 더는 해당 데이터 처리에 대한 구현을 더이상 고민하지 않아도 된다.
◦
물론 메소드를 작성할 때는 메소드에 관해 생각해야하지만 메소드를 구현하고 나면 메소드의 내부 동작 방식을 몰라도 해당 메소드의 구현 사항에 신경 쓰지 않고 메소드를 사용할 수 있다.
◦
내부 반복문이나 조건문이 깊게 중첩되어 있다면 메소드를 서브 메소드로 나눠야 한다. 메소드를 더 이해하기 쉽게 중첩된 부분을 별도의 메소드로 작성해야 한다.
•
이해하기 쉬운 중간 단계의 추상화를 도입한다.
◦
코드의 일부를 이해하기 쉬운 이름의 다른 메소드로 작성하는 것이 좋다.
◦
다음은 명령문을 펼쳐놓은 읽기 어려운 코드다.
if ( node <> NULL ) then
while ( node.next <> NULL ) do
node = node.next
leafName = node.name
end while
else
leafName = ""
end if
C++
복사
◦
위 코드는 다음 메소드로 대체될 수 있다.
leafName = GetLeafName( node )
C++
복사
◦
새로 작성한 메소드는 간략해서 이름만으로도 별도의 문서가 필요없을 정도다. 좋은 이름 덕분에 가독성이 좋아졌으며 메소드의 복잡도도 줄어들었다.
•
코드의 중복을 피한다.
◦
코드가 반복된다면 메소드로 추출해내야 한다. 실제로 두 메소드가 비슷한 코드를 가지고 있다면 오류가 발생하기 마련이다.
◦
중복되는 코드를 하나의 공통된 코드로 작성하면 공간을 절약할 수 있다.
◦
변경이 필요하면 공통 코드만 변경하면 되므로 변경도 쉽다.
◦
코드가 옳은지 한 곳에서만 검사하면 되므로 코드를 더 신뢰할 수 있다.
•
서브클래싱을 지원한다.
◦
길고 구조화가 덜 된 메소드보다 길이도 짧고 구조적으로 완성도 높은 메소드를 오버라이드하면 변경할 내용이 많지 않다.
◦
오버라이드가 가능한 메소드를 간단하게 유지하면 서브클래스 구현에서 오류가 발생할 확률도 줄어들 것이다.
•
코드의 실행 순서를 감춘다.
•
이식성을 높인다.
◦
메소드를 사용하면 미래에 이식이 가능한 기능과 그렇지 않은 기능을 구분할 수 있다.
◦
이식이 불가능한 기능에는 표준이 아닌 언어의 기능, 하드웨어 의존성, 운영체제 의존성 등이 존재한다.
•
복잡한 불린 테스트를 단순화한다.
◦
테스트를 함수로 작성하면 코드를 조금 더 쉽게 이해할 수 있다.
•
성능을 개선한다.
◦
코드를 한 곳에 작성해놓으면 비효율적인 부분을 찾기 위해 더 쉽게 분석할 수 있을 것이다.
•
복잡성을 고립시킨다.
•
구현 세부 사항을 숨긴다.
•
변경의 효과를 제한한다.
•
전역 데이터를 숨긴다.
•
중앙 집중 관리한다.
•
코드의 재사용을 돕는다.
•
특정한 리팩토링을 수행한다.
메소드로 작성하기에는 너무 단순해보이는 연산
•
작업이 너무 간단하면 메소드로 추출해내기 꺼려지는 경향이 있다. 하지만 길이가 짧은 메소드는 몇 가지 장점이 있다.
◦
가독성을 향상시킨다, 다음 코드는 매우 이해하기 쉽다.
points = deviceUnits * ( POINTS_PER_INCH / DeviceUnitsPerInch() )
C++
복사
▪
하지만 이름이 명확한 메소드를 생성한다면 코드를 조금 더 이해하기 쉽게 개선할 수 있다.
Function DeviceUnitsToPoints ( deviceUnits Integer ): Integer
DeviceUnitsToPoints = deviceUnits *
( POINTS_PER_INCH / DeviceUnitsPerInch() )
End Function
C++
복사
2. 루틴 수준의 설계
•
메소드에서의 응집성은 메소드에 있는 연산들이 얼마나 밀접하게 연관되어 있는지를 나타낸다. 어떤 개발자들은 강한 연결이라는 말을 선호한다.
◦
즉 메소드 내의 연산들이 얼마나 강하게 연결되어 있는지를 나타낸다.
▪
Cosine()과 같은 함수는 전체 메소드가 하나의 기능만 수행하므로 응집성이 강하다.
▪
CosineAndTan()과 같은 함수는 한 가지 이상의 작업을 수행하므로 응집성이 약하다.
◦
메소드를 작성하는 이유는 한 가지 일을 잘하도록 하는 것이지 여러 가지 일을 처리하는 데 있지 않다.
•
응집성이 높을수록 코드의 오류가 적다. 응집성은 일반적으로 몇 가지 레벨로 나누어 설명할 수 있는데, 용어 자체를 기억하기보다는 그 개념을 이해하는 것이 중요하다.
◦
기능적 응집성은 메소드가 오직 하나의 연산만 처리하는 경우 가장 강하고 바람직한 응집성이다.
◦
다음은 완벽하지는 않지만 알아둘만한 가치가 있는 응집성이다.
▪
순차적 응집성은 메소드가 특정한 순서대로 수행되어야 하고 단계마다 정보를 공유하며 동시에 수행될 때 완전한 기능을 제공하지 못하는 연산을 포함할 때 존재한다.
•
생이로부터 직원의 나이와 퇴직일을 계산하는 메소드다. 메소드가 우선 나이를 계산하고 나이로부터 퇴직일을 계산하면 이 메소드는 순차적 응집성을 갖는다.
▪
통신적 응집성은 메소드에 있는 연산들이 같은 데이터를 사용하지만 서로 아무런 연관성이 없을 때 발생한다.
•
어떤 메소드가 요약 보고서를 출력하는 기능과 메소드에 전달된 데이터를 초기화하는 기능이 있다면 이 둘은 통신적 응집성이다.
▪
시간적 응집성은 여러 연산이 동시에 수행되어야해서 하나의 메소드로 결합할 때 발생한다.
•
전형적인 예로 StartUp()과 CompleteNewEmployee()가 있다.
◦
나머지 응집성들은 주의해야하는 응집성들이다.
▪
절차적 응집성은 메소드에 있는 연산들이 정해진 순서대로 처리될 때 발생한다.
•
직원의 이름을 입력으로 받고 주소와 전화번호 순으로 입력받는다고 하자 이와 같은 순서는 사용자가 입력 화면에서 데이터를 입력하는 순서와 일치할 때만 중요하다.
•
추가적인 정보는 다른 메소드를 통해서 입력받는다. 이 메소드는 기능을 순차적으로 처리하는 것 외에는 불필요하게 결합하였기 때문에 절차적 응집성을 갖는다.
▪
논리적 응집성은 여러 가지 기능을 한 메소드에서 수행할 때 메소드에 전달되는 조건에 따라 수행되는 기능이 다른 경우에 발생한다.
•
메소드의 논리적 흐름에 의해서 각 기능을 처리한다고 해서 논리적 응집성이라고 한다.
▪
우연적 응집성은 메소드에 있는 연산이 특별한 연관 관계를 맺지 않을 때 발생한다. 다른 이름으로는 응집성 없음이나 혼란스러운 응집성이라고 할 수 있다.
3. 좋은 메소드 이름
•
메소드가 하는 모든 것을 표현하라.
◦
메소드 이름에 모든 출력과 부수적인 효과를 설명하라.
•
의미가 없거나 모호하거나 뚜렷한 특징이 없는 동사를 사용하지 마라.
◦
어떤 동사는 포괄적이고 유연해서 많은 뜻을 가지고 있다. 모든 메소드가 분명한 목적을 갖고 메소드의 기능을 정확하게 설명하는 이름을 갖도록 재구성하는 것이 가장 좋은 해결책이다.
•
메소드 이름의 길이에 신경 쓰지 마라.
◦
연구에 의하면 메소드 이름의 적절한 길이는 문자로 9자에서 15자 사이다.
◦
전반적으로 메소드 이름은 명료함에 초점을 맞춰야 하고 이름의 길이에 제약을 받지 않고 지어도 괜찮다.
•
메소드의 이름을 지을 때는 반환 값에 관해서 설명하라.
◦
값을 리턴하므로 리턴 값에 대한 내용이 이름에 포함되어야 한다.
•
메소드의 이름을 지을 때 확실한 의미가 있는 동사를 객체 이름과 함께 사용하라.
◦
기능적 응집성을 갖는 프로시저는 일반적으로 하나의 객체에 대해서 한 가지 연산만 수행한다.
◦
메소드의 이름은 메소드가 무슨 일을 하는지 반영해야하기 때문에 동사에 객체 이름을 붙여 쓴 형태를 가지는 것이 좋다.
◦
객체지향 언어에서는 객체 자체가 호출에 포함되어 있기 때문에 프로시저에 객체의 이름을 포함시킬 필요가 없다.
•
반의어를 정확하게 사용하라.
◦
반의어에 대한 이름 규약을 사용하면 일관성을 유지하는데 도움을 주고 이해하기가 쉽다.
•
공통적인 연산을 위한 규약을 만들어라.
◦
시스템 환경에 따라 각 연산을 명확하게 구분하는 일은 매우 중요하다. 이름 규약은 그러한 차이를 표현하기 가장 쉽고 신뢰할만한 방법이다.
4. 메소드의 길이에 대한 문제
•
이론적으로 코드의 가장 적합한 최대 길이는 한 화면에 꽉 찰 정도다. 요즘은 길이가 매우 짧은 메소드 여럿을 긴 메소드 몇 개와 혼합하는 경향이 있다. 그렇다고 긴 메소드가 사라지진 않는다.
•
가급적 길어도 상관없으나 200줄 이상의 큰 메소드는 장점이 여럿이지 않으며 이 한계치를 넘기지 않는 것이 좋다.
5. 메소드 매개변수 처리
•
메소드 간의 인터페이스는 프로그램에서 가장 오류가 발생하기 쉬운 영역 중 하나다. 다음은 그러한 문제를 최소화하는 몇 지침이다.
◦
매개변수를 입력 - 수정 - 출력 순서로 입력한다.
▪
매개변수를 무작위로나 알파벳 순서로 정렬하는 대신 입력만 가능한 것을 첫 번째로 입출력이 가능한 것을 두 번째로, 출력만 가능한 것을 세 번째로 나열한다.
▪
이유는 메소드가 연산을 처리할 때 데이터를 입력받고 변경한 다음 결과를 반환하는 순서로 진행되기 때문이다.
◦
모든 매개변수를 사용한다.
▪
메소드에 매개변수를 전달하면 반드시 모든 매개변수를 사용하는 것이 좋다.
◦
상태 변수나 오류 변수를 마지막에 입력한다.
▪
규약상 상태 변수와 오류를 가리키는 변수는 매개변수 목록의 마지막에 위치한다.
▪
이들은 메소드의 주요 책임에 크게 영향을 끼치지 않고 주로 값을 반환하기 위한 매개변수기 때문이다.
◦
메소드의 매개변수를 연산을 위한 변수로 사용하지 않는다.
▪
메소드에 전달된 매개변수를 연산을 위한 지역 변수처럼 사용하지 않아야 한다.
▪
매개변수는 변경되지 않아야한다는 의미랑 비슷하게 받아들였다.
◦
매개변수에 대한 제약사항을 주석으로 작성한다.
▪
메소드에서 입력받는 데이터가 특정한 조건을 만족해야 한다면 그러한 제약사항에 관해서 설명해야 한다.
•
메소드 내부는 물론 해당 메소드를 호출하는 코드에 상세하게 설명한다면 도움이 된다.
▪
그렇다면 매개변수에 관해 어떤 조건들을 주석으로 작성해야 할까?
•
매개변수가 입력을 위한 것인지, 변경되는지, 값을 반환하기 위한것인지에 대한 내용
•
숫자 매개변수의 단위
•
열거형이 아닌 경우 상태 코드와 오류 값의 의미
•
값의 범위
•
절대로 가질 수 없는 값
◦
메소드 매개변수의 수를 7개 정도로 제한한다.
▪
매개변수 여럿을 함수에 계속 전달하고 있다면 메소드들이 서로 지나치게 묶여있다고 말할 수 있다. 결합을 줄일 수 있도록 메소드를 설계하라.
▪
동일한 데이터를 여러 메소드에 전달하고 있다면 그 메소드들을 클래스로 분류하고 자주 사용되는 데이터는 클래스 데이터로 취급한다.
◦
매개변수에 사용할 입력, 수정, 출력 이름 규약을 고려한다.
◦
메소드가 인터페이스 추상화를 유지할 수 있도록 변수나 객체를 전달한다.
▪
메소드에 전달하는 매개변수의 목록을 자주 변경하고 있고 그 매개변수가 동일한 객체로부터 온 것이라면 구체적인 요소대신 전체 객체를 전달하라는 의미다.
6. 함수를 사용할 때 특별히 고려해야할 사항
•
의미론적으로 함수는 값을 반환하고 프로시저는 아무것도 반환하지 않는다.
•
함수와 프로시저의 구분은 문법적인 구분과 의미론적인 구분이 있는데, 의미론적인 구분을 따라야 한다.
함수를 사용할 때와 프로시저를 사용할 때
•
논리적으로는 프로시저처럼 동작하지만 값을 리턴하기 대문에 공식적으로 함수인 메소드를 작성하고는 한다.
•
메소드의 일차적인 목적이 함수의 이름에서 가리키고 있는 값을 반환하는 것이라면 함수를 사용하라. 그렇지 않다면 프로시저를 사용하라.
함수 리턴 값 설정
•
함수를 사용하면 함수가 틀린 값을 리턴할 것이라는 위험요소가 생긴다.
◦
일반적으로 함수는 여러 경로를 가지고 있고 그 경로 중 하나가 리턴 값을 설정하지 않는 경우에 틀린 값을 반환하는 일이 발생한다.
•
이러한 위험 요소를 줄이려면 다음 지침을 따른다.
◦
가능한 모든 반환 경로를 검사하라.
▪
메소드를 작성할 때, 모든 가능한 환경에서 함수가 값을 반환하는지 확인하기 위하여 각각의 경로를 머릿속으로 실행해 본다.
▪
함수를 시작할 때, 리턴 값을 기본값으로 초기화하는 것은 좋은 습관이다. 이러한 습관은 정확한 반환값이 설정되지 않은 경우에 대한 안전장치를 제공한다.
◦
지역 데이터에 대한 참조나 포인터를 반환하지 말라.
▪
메소드가 끝나자마자 지역 변수는 범위를 벗어나고 지역 변수에 대한 참조나 포인터는 무효한 상태가 되고 가비지 컬렉터한테 처리되길 기다리는 신세가 될 것이다.
▪
객체가 내부 데이터에 대한 정보를 리턴해야 한다면 그러한 정보를 클래스의 멤버 데이터로 저장해야 한다.
▪
그리고 지역 변수에 대한 참조나 포인터 대신 멤버 데이터 항목의 값을 반환하는 접근자 함수를 제공해야 한다.
메소드의 품질에 관한 고려사항
근본적인 문제
•
메소드를 반드시 작성해야 하는가?
•
별도의 메소드로 작성되어야 할 부분이 모두 메소드로 작성되었는가?
•
메소드 이름이 프로시저나 함수의 반환 값을 설명하는 동사 + 객체 형태인가?
•
메소드 이름이 메소드가 하는 모든 것을 설명하는가?
•
일반적인 연산에 대한 명명규칙을 결정했는가?
•
메소드가 강하고 기능적 응집성을 가지고 있는가? 즉 메소드가 오직 한 가지 일만 수행하고 잘 수행하고 있는가?
•
메소드가 느슨하게 연결되어 있는가? 즉 다른 메소드에 대한 연결이 적고 친밀하고 가시적이고 유연한가?
•
메소드의 길이가 코드 작성 표준보다는 기능과 논리에 의해서 자연스럽게 결정되었는가?
매개변수 전달 관련 문제
•
메소드의 매개변수 목록이 일관된 인터페이스 추상화를 제공하는가?
•
메소드의 매개변수가 유사한 메소드에 있는 매개변수의 순서와 일치하고 합리적인 순서로 되어 있는가?
•
메소드의 매개변수가 7개 이하 인가?
•
모든 입력 매개변수가 사용되었는가?
•
모든 출력 매개변수가 사용되었는가?
•
메소드가 입력 매개변수를 작업 변수로 사용하지 않는가?
•
메소드가 함수라면 모든 가능한 조건에서 타당한 값을 반환하는가?
요점정리
•
메소드를 작성하는 가장 중요한 이유는 사람이 관리하고 이해하기게 더 쉬운 코드를 작성하기 위한 것이다.
◦
단순히 공간을 줄이겠다는 것은 고려할 사항이 아니다. 이해하기 쉽고 믿을 수 있고 변경하기 편한 코드를 작성하는 데 그 목표가 있다.
•
때로는 별도의 메소드를 작성함으로써 얻는 이득이 크지 않을 수 있다.
◦
그래도 하는 것이 좋다.
•
메소드를 다양한 형태의 응집성에 따라 분류할 수 있지만 가장 좋은 기능적 응집성을 갖도록 메소드를 분류하는 것이 가장 좋다.
•
메소드의 품질을 이야기할 때는 이름도 고려한다.
◦
이름이 별로면서 정확하다면 메소드는 잘못 설계된 것이다.
◦
이름도 별로고 정확하지 않으면 프로그램이 무엇을 하는 지 알 수 없다. 어느 경우든지 이름이 좋지 않으면 변경해야 한다.
•
함수는 그것의 주된 목적이 함수의 이름에 묘사된 값을 반환할 때만 사용되어야 한다.