본문 바로가기
공부/C#

이것이 C#이다 Chapter 06 메소드로 코드 간추리기

by bokob 2024. 2. 26.

6.1 메소드란?

메소드는 객체지향 프로그래밍 언어에서 사용하는 용어.

C/C++ 에서는 함수(Function)라 부르기도 하고, 파스칼에서는 프로시저(Procedure)라 불렀다.

또는 서브루틴(Subroutine)이나 서브 프로그램(Subprogram)이라 부르는 언어도 있다.

엄밀히 말하면 의미 차이가 존재하지만, 큰 맥락에서는 같은 것을 지칭한다고 볼 수 있다.

 

메소드는 일련의 콛르르 하나의 이름 아래 묶은 것이다.

이렇게 묶은 코드는 메소드의 이름을 불러주는 것으로 실행할 수 있다. ('메소드를 호출(call)한다'고 한다) 

 

class 클래스_이름
{
    한정자 반환_타입 메소드_이름(매개변수_목록)
    {
        // 실행할 코드
        
        return 메소드_결과 // 메소드 결과의 데이터 타입은 메소드의 반환 타입과 일치해야 한다.
    }
}

메소드는 클래스 안에서 선언된다. 메소드가 함수, 프로시저, 서브루틴 등과 다른 점이라면 클래스 안에 존재한다는 것이다.

클래스 안에 선언되는 메소드는 매개변수(Parameter)와 반환 타입(Return Type)을 가진다. 매개변수는 메소드에 넣는 재료라고 볼 수 있다. 반환 타입은 재료로 만들어진 제품의 규격으로 볼 수 있다.

결국에는 메소드가 만들어진 결과를 반환(Return)한다.

호출자가 메소드를 호출하면서 인수(Argument)를 넘기면, 이 인수는 메소드의 매개변수에 입력된다. 호출 받은 메소드는 매개변수를 이용하여 계산 후 결과를 호출자에게 반환한다.

class Calculator
{
    public static int Plus(int a, int b)
    {
        Console.WriteLine("Input : {0}, {1}", a, b);
        
        int result = a + b;
        return result;
    }
}

// static으로 인해 정적 메소드로 선언되었기 때문에 바로 호출 가능
int x = Calculator.Plus(3, 4); // x는 7

 

매개변수와 인수

매개변수는 메소드가 호출자에게서 전달받은 값을 받는 변수를 의미, 인수는 호출자가 매개변수에 넘기는 값을 의미

 

void 반환 타입

모든 메소드들이 결과를 반환하는 것이 아니다. 자기 할 일만 하고 종료한다.

이런 메소드를 선언할 때는 반환 타입에 void를 넣어주면 된다.

 

static 한정자

static은 '정적'이라는 뜻을 가지고 있다. 움직이지 않는다는 뜻이다. C#에서 static은 메소드나 필드가 클래스의 인스턴스가 아닌 클래스 자체에 소속되도록 지정하는 한정자다. 즉, 인스턴스를 만들지 않고도 해당 메소드를 호출할 수 있다.

 

6.2 return에 대하여

return 문은 점프문의 한 종류이다. 프로글매의 흐름을 갑자기 호출자에게로 돌려놓는다.

return 문은 언제든지 메소드 중간에 호출되어 메소드를 종결시키고 프로그램의 흐름을 호출자에게 도려줄 수 있다.

int Fibonacci(int n)
{
    if (n < 2)
        return n;
    else
        return Fibonacci(n-1) + Fibonacci(n-2);
}

 

재귀 호출

메소드가 자기 자신을 호출하는 것을 재귀 호출(Recursive Call) 이라고 한다.

코드를 단순하게 구성할 수 있다는 장점이 있지만, 성능에는 나쁜 영향을 주기 때문에 주의해서 사용해야 한다.

 

메소드가 반환할 것이 아무것도 없는 경우인 void 타입인 경우에도 return 문 사용이 가능하다.

void PrintProfile(string name, string phone)
{
    if(name == "")
    {
        Console.WriteLine("이름을 입력하라.");
        return;
    }
    
    Console.WriteLine("Name:{0}, Phone:{1}", name, phone);
}

 

6.3 매개변수에 대하여

메소드에 인수로 넘겨진 값들은 실제로 메소드 안으로 들어가는 것이 아니다.

매개변수도 근본적으로는 '변수'이다. 인수에 있는 값을 매개변수에 할당하는 것이다.

즉, 데이터가 값이든 참조든 복사되는 것이다.

값은 같지만 별개의 메모리 공간을 갖게 된다. 따라서 매개변수를 수정해도 원래 인수는 영향을 받지 않는다.

이처럼 메소드를 호출할 때 데이터를 복사해서 매개변수에 넘기는 것을 '값에 의한 전달(pass by value)' 라고 부른다.

 

6.4 참조에 의한 매개변수 전달

'참조에 의한 전달(pass by value)로 매개변수를 넘기면 값에 의한 전달과 달리 매개변수가 넘겨진 원본 변수를 직접 참조하게 된다. 따라서 메소드 안에서 매개변수를 수정하면 이 매개변수가 참조하고 있는 원본 변수가 수정된다.

C#에서 참조에 의한 매개변수 전달은 ref 키워드를 매개변수 앞에 붙이면 된다.

static void Swap(ref int a, ref int b)
{
    int temp = b;
    int b = a;
    a = temp;
}

 

메소드를 호출할 때 다시 ref 키워드를 매개변수 앞에 붙여주면 된다.

int x = 3;
int y = 4;

Swap(ref x, ref y);

 

 

6.5 메소드의 결과를 참조로 반환하기

참조 반환값(ref return)을 이용하면 메소드의 호출자로 하여금 반환받은 결과를 참조로 다룰 수 있도록 한다.

ref 한정자를 이용해서 메소드를 선언하고, return 문이 반환하는 변수 앞에도 ref 키워드를 명시한다.

class SomeClass
{
    int SomeValue = 10;
    
    public ref int SomeMethod() // ref 키워드로 메소드를 한정한다.
    {
        // 코드
        return ref SomeValue; // return 문 사용 시 ref 키워드를 반환할 필드나 객체 앞에 붙인다.
    }
}

 

참조로 반환하는 메소드는 호출자가 특별한 키워드를 사용하지 않는 한 값으로 반환하는 평범한 메소드처럼 동작한다.

SomeClass Obj = new SomeClass();
int reulst = obj.SomeMethod(); // 값으로 반환받고 싶을 때는 평범하게 메소드를 호출하면 된다.

 

반환하는 결과를 호출자가 참조로 넘겨받고 싶다면 결과를 담는 지역 변수와 호출할 메소드의 이름 앞에 ref 키워드를 위치시켜야 한다. 이렇게 참조로 반환받은 결과를 담는 지역 변수를 참조 지역 변수(ref local)라고 부른다.

SomeClass obj = new SomeClass();

// result는 참조 지역 변수이다. result를 바꾸면 obj.SomeValue의 값도 변한다.
ref int result = ref obj.SomeMethod();

 

6.6 출력 전용 매개변수

메소드의 결과는 대부분 하나다. 하지만 두 개 이상의 결과를 얻고 싶을 때는 어떻게 할까?

바로 ref 키워드를 이용하는 것이다.

void Divide(int a, int b, ref int quotient, ref int remainder)
{
    quotient = a / b;
    remainder = a % b;
}
int a = 20;
int b = 3;
int c = 0;
int d = 0;

Divide(a, b, ref c, ref d);

Console.WriteLine("Quotient : {0}, Remainder {1}", c, d);

이처럼 ref만으로도 여러 개의 결과를 메소드에서 얻어올 수 있지만, C#은 조금 더 안전한 방법으로 똑같은 일을 할 수 있는 방법을 지원한다. 바로  out 키워드를 이용한 '출력 전용 매개변수'이다. 메소드의 선언부와 호출부에 ref 키워드 대신 out 키워드를 사용하는 것이 전부다.

void Divide(int a, int b, out int quotient, out int remainder)
{
    quotient = a / b;
    remainder = a % b;
}
int a = 20;
int b = 3;
int c;
int d;

Divide(a, b, out c, out d);

Console.WriteLine("Quotient : {0}, Remainder {1}", c, d);

별 차이가 없어 보일 수 있지만, out에는 ref에 없는 안전장치가 있다.

ref 키워드를 이용해서 매개변수를 넘기는 경우 메소드가 해당 매개변수에 결과를 저장하지 않아도 컴파일러는 아무런 경고를 하지 않는다. 반면에, out 키워드를 이용해서 매개변수를 넘길 때는 메소드가 해당 매개변수에 결과를 저장하지 않으면, 컴파일러가 에러 메시지를 출력한다.

한편, 메소드를 호출하는 쪽에서는 초기화하지 않은 지역 변수를 메소드의 out 매개변수로 넘길 수 있다. 컴파일러가 호출당하는 메소드에서 그 지역 변수를 할당할 것을 보장하기 때문이다.

출력 전용 매개변수는 메소드를 호출하기 전에 미리 선언할 필요가 없다. 호출할 때 매개변수 목록 안에서 즉석으로 선언하면 된다.

int a = 20;
int b = 3;

Divide(a, b, out int c, out int d);

Console.WriteLine("Quotient : {0}, Remainder {1}", c, d);

 

6.7 메소드 오버로딩

오버로딩(Overloading)이란 '과적하다'라는 뜻이다.

메소드 오버로딩은 하나의 메소드 이름에 여러 개의 구현을 올리는 것을 뜻한다. 즉, 같은 이름의 메소드가 여러개라는 것이다.

int Plus(int a, int b)
{
    return a + b;
}

double Plus(double a, double b)
{
    return a + b;
}

컴파일러가 메소드 호출 코드에 사용되는 매개변수의 수와 타입을 분석해서(오로지 매개변수만 분석한다. 반환 타입은 묻지도 따지지도 않는다.) 어떤 버전이 호출될지를 찾아준다. 실행할 메소드를 찾는 작업은 컴파일 시간에 이루어진다.

메소드 오버로딩은 이름에 대한 고민을 줄여주는 동시에 코드를 일관성 있게 유지해준다. 일관성 있는 코드는 메소드 작성자에게도 도움을 주지만, 메소드 사용자에게도 높은 생산성을 제공한다.

 

6.8 가변 개수의 인수

인수의 개수가 다르다는 이유만으로 똑같은 메소드를 여러 버전으로 오버로딩하고 싶을 때가 있다.

이를 위해 C# 에서는 '가변 개수의 인수' 기능을 제공한다.

가변 개수 인수란 그 개수가 유연하게 변할 수 있는 인수를 말한다.

가변 개수의 인수는 params 키워드와 배열을 이용해서 선언한다.

int Sum(params int[] args) // Sum() 메소드에 입력한 모든 매개변수는 args에 담긴다.
{
    int sum = 0;
    
    for(int i=0; i<args.Length; i++)
    {
        sum += args[i];
    }
    
    return sum;
}

매개변수의 개수가 유한하게 정해져 있다면 가변 개수의 인수보다는 메소드 오버로딩을 사용하는 것이 적절하다.

가변 개수의 인수는 타입은 같으나 인수의 개수만 유연하게 달라질 수 있는 경우에 적합하다.

 

6.9 명명된 인수

메소드를 호출할 때 매개변수 목록 중 어느 매개변수에 데이터를 할당할지 지정하는 것은 '순서'이다.

대부분 순서에 근거해서 매개변수에 인수를 할당하는 스타일을 사용하지만, C#에서는 명명된 인수(Named Argument)를 지원한다. 이는 메소드를 호출할 때 인수의 이름에 근거해서 데이터를 할당할 수 있는 기능이다.

메소드를 호출할 때만 인수의 이름 뒤에 콜론(:)을 붙인 뒤 그 뒤에 할당할 데이터를 넣어주면 된다.

static void PrintProfile(string name, string phone)
{
    Console.WriteLine("Name:{0}, Phone:{1}", name, phone);
}

static void Main(stringp[] args)
{
    PrintProfile(name : "박찬호", phone : "010-1234-1234");
}

인수가 너무 많아 어느 매개변수에 어느 인수를 할당하고 있는지 분간이 어려운 경우에는 도움이 된다.

 

6.10 선택적 인수

메소드의 매개변수는 기본값을 가질 수 있다. 즉, 매개변수를 특정 값으로 초기화하듯 메소드를 선언할 수 있다.

void MyMethod(int a, int b = 0)
{
    Console.WriteLine("{0}, {1}", a, b);
}

기본 값을 가진 매개변수는 메소드를 호출할 때 해당 인수를 생략할 수 있다.

MyMethod(3);

필요한 경우에는 인수를 입력할 수도 있다. 기본값을 가진 매개변수는 필요에 따라 인수를 할당하거나 할당하지 않을 수 있기 때문에 이를 '선택적 인수(Optional Argument)'라고 부른다.

MyMethod(3, 4);

 

선택적 인수는 항상 필수 인수 뒤에 와야 한다. 필수 인수가 하나도 없는 경우에는 상관없다.

void MyMethod_0(int a = 0)
{
    Console.WriteLine("{0}", a);
}

void MyMethod_1(int a, int b = 0)
{
    Console.WriteLine("{0}. {1}", a, b);
}

void MyMethod_2(int a, int b, int c = 10, int d = 20)
{
    Console.WriteLine("{0}. {1}, {2}, {3}", a, b, c, d);
}

 

선택적 인수는 메소드의 사용자에게 신경 쓰고 싶지 않은 인수를 염두에 두지 않도록 편의를 제공하지만, 모호함이라는 스트레스도 같이 준다. 코드를 작성할 때는 인텔리센스의 도움을 받아 어느 매개변수에 인수를 할당하는지 파악할 수 있지만, 작성한 지 꽤 된 코드를 다시 보면 어느 매개변수에 어떤 인수를 할당했는지 모를 수 있다. 매개변수의 수가 많고 여기에 선택적 인수도 있으면 문제는 심각해진다. 이런 경우 명명된 인수를 사용하면 쉽게 문제를 해결할 수 있다.

 

메소드 오버로딩 vs 선택적 매개변수

선택적 인수는 상당히 유용한 기능이지만, 메소드 오버로딩과 함께 사용할 때 혼란을 야기할 수 있다.

void MyMethod(string arg0 = "", string arg1 = "")
{
    Console.WriteLine("A");
}

void MyMethod()
{
    Console.WriteLine("B");
}

MyMethod(); // "B" 출력

 

논리는 동일하되 매개변수가 다른 경우에는 선택적 인수를 사용하고, 매개변수에 따라 논리도 함께 달라지는 경우에는 오버로딩을 사용하는 식으로 정하는게 좋다.

 

6.11 로컬 함수

로컬 함수(Local Function)는 메소드 안에서 선언되고, 선언된 메소드 안에서만 사용되는 특별한 함수다.

클래스의 멤버가 아니기 때문에 메소드가 아니라 함수라고 부른다.

선언 방법은 메소드와 다르지 않지만, 로컬 함수는 자신이 존재하는 지역에 선언되어 있는 변수를 사용할 수 있다.

class SomeClass
{
    public void SomeMethod()
    {
        int count = 0;
        SomeLocalFunction(1, 2);
        SomeLocalFunction(3, 4);
        
        void SomeLocalFunction(int a, int b)
        {
            Console.WriteLine($"count : {++count}");
        }
    }
}

 

로컬 함수는 메소드 밖에서는 다시 쓸 일 없는 반복적인 작업을 하나의 이름 아래 묶어놓는 데 제격이다.