////
Search
Duplicate
📼

7. 좋은 메소드

이 장에서는 좋은 메소드와 나쁜 메소드의 차이를 만드는 특성을 중심으로 메소드에 대해 자세히 살펴본다.
메소드 설계에 영향을 미치는 요인들은 앞서 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 == 2expenseType == 3이 그렇다.
메소드의 입력 변수인 inputRect가 변경된다. 입력 변수의 값은 변경되지 않아야 한다.
변수의 값을 변경할 생각이라면 이름이 inputRect여도 안된다.
메소드가 전역 변수를 읽고 쓴다.
이 메소드는 corpExpense 값을 읽고 profit에 쓴다. 메소드가 직접 전역 변수를 읽고 쓰는 대신 해당 기능을 제공하는 다른 메소드를 호출하는 것이 좋다.
메소드의 목적이 하나가 아니다.
여기서 사용된 메소드는 변수를 초기화하고 데이터베이스에 접근하고 몇 가지 계산을 수행하는데 이들은 아무런 연관성이 없는 것처럼 보인다.
메소드는 분명하게 정의된 하나의 목적만을 가져야 한다.
메소드가 잘못된 데이터로부터 자신을 방어하지 않는다.
crntQtr이 0이면 ytdRevenue * 4.0 / (double) crntQtr 표현식은 0으로 나누기 오류를 발생시킨다.
메소드가 여러 가지 매직 넘버를 사용하고 있다.
메소드의 매개변수 중 사용하지 않는 것들이 존재한다.
screenXscreenY는 참조하지 않는다.
메소드의 매개변수가 너무 많다.
사람들이 이해할 수 있는 매개변수의 최대 갯수는 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개 이하 인가?
모든 입력 매개변수가 사용되었는가?
모든 출력 매개변수가 사용되었는가?
메소드가 입력 매개변수를 작업 변수로 사용하지 않는가?
메소드가 함수라면 모든 가능한 조건에서 타당한 값을 반환하는가?

요점정리

메소드를 작성하는 가장 중요한 이유는 사람이 관리하고 이해하기게 더 쉬운 코드를 작성하기 위한 것이다.
단순히 공간을 줄이겠다는 것은 고려할 사항이 아니다. 이해하기 쉽고 믿을 수 있고 변경하기 편한 코드를 작성하는 데 그 목표가 있다.
때로는 별도의 메소드를 작성함으로써 얻는 이득이 크지 않을 수 있다.
그래도 하는 것이 좋다.
메소드를 다양한 형태의 응집성에 따라 분류할 수 있지만 가장 좋은 기능적 응집성을 갖도록 메소드를 분류하는 것이 가장 좋다.
메소드의 품질을 이야기할 때는 이름도 고려한다.
이름이 별로면서 정확하다면 메소드는 잘못 설계된 것이다.
이름도 별로고 정확하지 않으면 프로그램이 무엇을 하는 지 알 수 없다. 어느 경우든지 이름이 좋지 않으면 변경해야 한다.
함수는 그것의 주된 목적이 함수의 이름에 묘사된 값을 반환할 때만 사용되어야 한다.