Devin.KR

메서드 - 이름 붙은 코드 조각

개발자KR 조회 8

이 장에서 배우는 것

앞 장에서 조건과 반복으로 주문 수량을 검사하고 같은 작업을 여러 번 실행했다. 이제 주문 처리 과정에 이름을 붙인다. 수량을 읽는 코드, 금액을 계산하는 코드, 재고를 줄이는 코드를 각각 나누면 주문이 어떤 순서로 처리되는지 드러난다. 메서드(method)는 이처럼 이름을 붙여 호출하는 코드 조각이다. 이름만 정하는 것으로는 부족하다. 무엇을 받아 무엇을 돌려주는지, 호출한 쪽의 데이터를 바꾸는지도 함께 정해야 한다.

이 장에서는 동네 카페의 라테 주문을 처리한다. 화면 입력 대신 정해진 문자열을 사용하므로 실행할 때마다 같은 결과를 확인할 수 있다. 계산은 반환값으로 전달하고, 재고 변경은 ref로 표현하며, 수량 해석의 성공 여부와 결과는 out을 이용해 나눈다.

  • 매개변수와 인수를 구분하고 반환값을 다음 작업에 연결한다.
  • 값을 전달하는 기본 방식과 ref, out의 차이를 설명한다.
  • 선택적 매개변수와 명명 인수로 호출 의도를 드러낸다.
  • 로컬 함수로 특정 작업 안에서만 쓰는 계산을 묶는다.
  • 검사, 계산, 상태 변경, 출력을 나누어 주문 처리 흐름을 읽기 쉽게 만든다.

문제 상황

카페에서 라테 한 잔을 4,500원에 판매한다. 준비된 재고는 8잔이고, 손님은 3잔을 포장 주문한다. 이번 주문에는 주문 전체에서 500원을 빼는 할인권을 적용한다. 결제 금액은 13,000원이고 남은 재고는 5잔이다. 숫자만 보면 간단하지만 프로그램은 수량이 올바른지, 재고가 충분한지, 할인액이 주문 금액보다 크지는 않은지 순서대로 판단해야 한다.

이 코드를 한곳에 길게 쓰면 포장 주문과 매장 주문에서 수량 검사가 반복되기 쉽다. 할인 계산을 수정할 때도 같은 계산이 등장하는 위치를 모두 찾아야 한다. 더 곤란한 문제는 재고 변경이다. 영수증을 미리 보여 주려던 코드에서 재고까지 줄어들면 계산과 실제 처리의 경계가 흐려진다.

작업을 나누기 전에 각 작업의 약속부터 적는다. 수량 읽기는 성공 여부와 읽은 수량을 제공한다. 금액 계산은 숫자를 받아 계산 결과를 돌려준다. 재고 차감은 성공한 경우에만 원래 재고를 바꾼다. 영수증 출력은 받은 정보를 화면에 표시한다. 이렇게 입력, 결과, 변경 대상을 정하면 함수 이름만으로도 호출 순서를 이해할 수 있다.

실제 주문에서는 결제 실패에 따른 재고 복구 같은 정책도 필요하다. 여기서는 유효한 수량과 충분한 재고가 확인되면 주문이 확정된다고 가정한다. 이 범위 안에서 각 코드 조각의 책임과 데이터 전달 방식을 살펴본다.

매개변수와 반환값으로 작업의 경계를 정한다

선언의 빈칸과 호출의 실제 값

매개변수(parameter)는 호출에서 받을 값을 선언한 이름이다. 인수(argument)는 호출할 때 실제로 넘기는 값이나 식이다. 아래 코드에서 unitPrice와 quantity는 매개변수이고, 호출에 적은 4500과 3은 인수다. 다음 조각은 문법을 설명하기 위한 예시이며, 실행할 전체 프로그램은 뒤의 완성 코드에 있다.

int total = CalculateTotal(4500, 3);

static int CalculateTotal(int unitPrice, int quantity)
{
    return unitPrice * quantity;
}

이름 앞의 int는 반환 형식이다. 계산이 끝나면 정수 하나를 호출한 곳에 제공한다는 뜻이다. 괄호 안에는 매개변수의 형식과 이름을 적는다. 본문의 return은 값을 돌려주면서 현재 호출을 끝낸다. 호출한 곳에서는 CalculateTotal(4500, 3)이라는 식이 반환된 정수로 계산되므로 그 값을 total에 저장할 수 있다.

인수에는 변수나 계산식도 쓸 수 있다. 예를 들어 CalculateTotal(price, quantity + 1)은 수량에 1을 더한 값을 전달한다. 이 호출만으로 호출자 쪽 quantity가 증가하지는 않는다. 식을 평가해 얻은 값과 원래 변수를 구분해야 한다.

반환값은 화면 출력과 다르다. 금액 계산 안에서 숫자를 출력하기만 하면 호출한 쪽이 그 숫자를 다른 계산에 쓰기 어렵다. 계산은 값을 반환하고 출력 담당 코드는 그 값을 받아 표시하도록 나눈다. 반대로 영수증 출력처럼 호출자에게 결과값을 제공할 필요가 없으면 반환 형식을 void로 쓴다. void인 함수에서도 return;으로 일찍 끝낼 수 있지만 뒤에 값을 붙이지 않는다.

금액 계산은 가격과 수량과 할인액을 받아 정수 결과를 반환한다

값을 반환하는 함수는 정상적으로 끝나는 모든 실행 경로에서 값을 반환해야 한다. 어떤 조건에서만 return하고 나머지 경로에서 본문 끝에 도달하도록 작성하면 컴파일 오류가 발생한다. 반환할 값이 없다는 상황도 결과의 일부라면 성공 여부를 반환하거나 실패를 나타내는 별도 약속이 필요하다.

최상위 문에서 사용하는 함수의 정확한 위치

이 장은 별도의 진입점 선언 없이 실행문을 적는 최상위 문을 사용한다. 최상위 문에 직접 선언한 함수는 문법상 로컬 함수(local function)다. 메서드의 매개변수와 반환을 익히기 위해 이 형태를 사용한다. 클래스에 속한 메서드를 직접 선언하는 구조는 다음 장에서 다룬다.

예시의 static은 이 로컬 함수가 바깥 지역 변수를 직접 가져다 쓰지 못하게 한다. 따라서 필요한 가격과 수량을 매개변수로 받아야 한다. 함수의 입력을 괄호 안에서 확인할 수 있다는 장점이 있다. 이것이 함수가 외부에 아무 영향도 주지 않는다는 뜻은 아니다. static인 로컬 함수도 콘솔에 출력하거나 ref로 받은 변수를 바꿀 수 있다.

완성 코드에서는 주문을 처리하는 실행문을 위에, 함수 선언을 아래에 둔다. 로컬 함수는 선언보다 앞에서 호출할 수 있으므로 먼저 주문 흐름을 읽고 필요한 세부 구현을 찾아 내려갈 수 있다. 선언한 위치까지 실행이 내려왔다고 해서 함수 본문이 저절로 실행되는 것은 아니다. 본문은 호출할 때 실행된다.

ref와 out으로 호출자의 변수에 접근한다

기본 전달과 ref의 차이

정수 매개변수는 기본적으로 인수의 값을 복사해서 받는다. 함수 안에서 그 매개변수를 다시 대입해도 호출자의 정수 변수는 바뀌지 않는다. 재고를 8에서 5로 줄여야 하는데 함수 내부의 복사본만 줄이면 화면에는 여전히 8이 남는다. 여기서는 원래 재고 변수를 변경한다는 의도를 ref로 표시한다.

int stock = 8;
bool accepted = TryTakeStock(ref stock, 3);

static bool TryTakeStock(ref int stock, int quantity)
{
    if (quantity <= 0 || stock < quantity)
    {
        return false;
    }

    stock -= quantity;
    return true;
}

ref 매개변수는 호출자가 제공한 변수의 저장 위치를 참조한다. 이 예시에서 함수 안의 stock을 바꾸면 호출자 쪽 stock도 바뀐다. 선언과 호출 양쪽에 ref를 적어 변경 가능성을 드러낸다. 호출 전에 변수에 값을 넣어 두어야 하며, ref 8이나 ref (stock + 1)처럼 단순한 값이나 계산 결과를 전달할 수는 없다.

검사를 먼저 하고 변경을 나중에 한다는 순서도 중요하다. 수량이 0 이하이거나 재고보다 많으면 false를 반환한다. 재고를 건드리는 문장은 그 아래에 있으므로 실패한 주문은 재고를 바꾸지 않는다. 이런 약속이 있으면 호출자는 실패 뒤에 원래 재고를 복구하는 코드를 쓸 필요가 없다.

정수의 기본 전달은 호출자 값을 유지하고 ref 전달은 호출자 변수의 값을 바꿀 수 있다

out은 결과를 담을 변수를 받는다

out도 호출자 쪽 변수에 접근하지만 약속이 다르다. 호출자는 미리 값을 넣지 않아도 된다. 대신 호출된 함수가 정상적으로 반환하기 전에 해당 매개변수에 값을 대입해야 한다. ref가 이미 있는 값을 읽고 바꾸는 데 어울린다면, out은 함수가 새로 제공하는 결과를 담는 데 어울린다.

수량 읽기는 결과가 두 가지다. 하나는 입력이 유효한지를 나타내는 논리값이고, 다른 하나는 읽어 낸 정수다. 이를 각각 반환값과 out으로 나눌 수 있다. 완성 코드의 TryReadQuantity는 해석에 성공하고 수량이 양수일 때만 true를 반환한다. 실패하면 false를 반환하고 수량은 0으로 정한다.

if (TryReadQuantity("3", out int quantity))
{
    Console.WriteLine(quantity);
}

out int quantity는 호출 위치에서 결과 변수를 선언하는 문법이다. 미리 int quantity;를 선언했다면 호출에서는 out quantity라고 쓴다. 두 경우 모두 정상적으로 호출이 끝나면 변수에는 값이 있다. 다만 값이 대입되었다는 사실과 업무상 유효하다는 사실은 다르다. 성공 여부가 false라면 그 수량으로 주문을 진행하지 않아야 한다.

정수 매개변수의 전달 방식에 따른 약속
방식호출 전 값함수 안의 대입호출자 변수 변경
기본 전달필요하다매개변수의 복사본에 한다대입만으로는 바뀌지 않는다
ref필요하다반드시 할 필요는 없다가능하다
out필요하지 않다정상 반환 전에 필요하다결과가 대입된다

이 표는 정수 변수로 차이를 설명한다. 앞서 배운 참조 형식도 기본 인수 전달에서는 참조값이 복사된다. 객체 내부를 바꾸는 것과 호출자의 변수 자체를 다른 값으로 대입하는 것은 구분해야 한다. 여기서 ref가 제공하는 기능은 호출자의 변수 자체에 접근하는 것이다.

변경이 필요하다고 해서 모두 ref로 만들지는 않는다. 새 금액을 계산하는 작업은 반환값 하나로 충분하다. ref를 쓰면 호출 전후의 상태를 함께 추적해야 하므로, 기존 변수를 직접 바꾸는 것이 작업의 의미일 때 선택한다.

호출을 읽기 쉽게 만들고 내부 작업을 감춘다

선택적 매개변수와 명명 인수

대부분의 주문에 할인이 없다면 매번 0을 적는 대신 선택적 매개변수(optional parameter)를 선언할 수 있다. int discount = 0처럼 기본값을 붙이면 호출자가 해당 인수를 생략했을 때 0을 사용한다. 일반적인 선언에서는 필수 매개변수를 먼저, 선택적 매개변수를 뒤에 배치한다.

int regularTotal = CalculateTotal(4500, 2);
int discountedTotal = CalculateTotal(4500, 2, discount: 500);

두 번째 호출의 discount: 500은 명명 인수(named argument)다. 값이 어느 매개변수에 대응하는지 이름으로 표시한다. CalculateTotal(4500, 2, 500)도 가능하지만 숫자 500의 역할은 선언을 찾아봐야 알 수 있다. 명명 인수는 할인처럼 같은 형식의 숫자가 여럿 등장할 때 특히 도움이 된다.

매개변수와 인수의 구분은 여기서도 이어진다. 생략할 수 있도록 만드는 것은 선언 쪽의 기본값이며, 이름을 붙여 전달하는 것은 호출 쪽의 문법이다. 명명 인수를 쓴다고 해당 매개변수가 선택적이 되지는 않는다. 이름을 잘못 쓰면 컴파일 오류가 나고, 공개된 매개변수 이름을 바꾸면 그 이름을 사용하는 호출도 수정해야 한다.

이름을 붙인 인수는 순서를 바꾸어 적을 수 있다. 다만 위치 인수와 섞을 때는 배치 규칙을 고려해야 한다. 이 장에서는 위치 인수를 먼저 쓰고 필요한 뒤쪽 인수에 이름을 붙이거나, 모든 인수에 이름을 붙이는 형태를 사용한다. 기본값을 바꿀 때는 생략된 호출의 의미도 함께 달라질 수 있다는 점을 확인해야 한다.

로컬 함수로 계산의 세부 단계를 묶는다

할인액은 음수이면 0으로, 주문 소계보다 크면 소계로 제한한다. 이 규칙은 금액 계산 안에서만 필요하므로 CalculateTotal 안에 ClampDiscount라는 로컬 함수를 둔다. 다른 작업에서 직접 호출할 수 없게 하면서 계산의 세부 단계에 이름을 붙이는 방식이다.

static int CalculateTotal(int unitPrice, int quantity, int discount = 0)
{
    int subtotal = unitPrice * quantity;
    return subtotal - ClampDiscount(discount);

    int ClampDiscount(int requested)
    {
        if (requested < 0)
        {
            return 0;
        }

        if (requested > subtotal)
        {
            return subtotal;
        }

        return requested;
    }
}

안쪽 함수는 바깥 함수의 지역 변수인 subtotal에 접근한다. 이처럼 자신이 선언된 바깥 범위의 변수를 사용하는 것을 캡처(capture)라고 한다. 이 코드에서는 소계를 읽기만 한다. 일반적으로 캡처한 지역 변수는 로컬 함수에서 변경할 수도 있으므로, 로컬 함수라고 해서 상태 변경이 없는 것은 아니다.

바깥의 CalculateTotal이 static이어도 안쪽 함수가 그 안에서 선언된 subtotal을 사용하는 것은 가능하다. 바깥 함수의 static은 그보다 바깥의 지역 변수를 가져오지 못하게 한다. 안쪽 ClampDiscount에도 static을 붙이려면 소계까지 매개변수로 전달해야 한다.

로컬 함수 선언이 return 아래에 있어도 이 구조는 유효하다. 선언은 그 자리에 도달해 실행해야 하는 작업이 아니다. 반환할 식을 계산하면서 ClampDiscount를 호출하고, 그 결과를 빼서 반환한다. 안쪽 함수의 return은 안쪽 호출만 끝내며 바깥 함수 전체를 즉시 끝내지 않는다.

완성 코드

다음은 한 파일로 실행하는 Program.cs 전체다. .NET 10 콘솔 프로젝트에서 기존 내용을 모두 교체한다. 별도 패키지는 필요하지 않다. 가격과 수량은 이 카페 예시의 작은 정수 범위로 한정하고, 원 단위 정수를 사용한다. 출력에는 지역 설정에 따라 달라지는 통화 기호나 자릿수 구분 형식을 사용하지 않는다.

using System;

int stock = 8;
int unitPrice = 4500;

Console.WriteLine($"주문 전 재고: {stock}잔");

if (TryReadQuantity("3", out int quantity))
{
    if (TryTakeStock(ref stock, quantity))
    {
        int total = CalculateTotal(unitPrice, quantity, discount: 500);
        PrintReceipt(menu: "라테", quantity: quantity, total: total, takeout: true);
    }
    else
    {
        Console.WriteLine("주문 거절: 재고가 부족하다.");
    }
}
else
{
    Console.WriteLine("주문 거절: 수량이 올바르지 않다.");
}

Console.WriteLine($"주문 후 재고: {stock}잔");

if (!TryReadQuantity("0", out int rejectedQuantity))
{
    Console.WriteLine($"수량 거절: {rejectedQuantity}");
}

int previewTotal = CalculateTotal(unitPrice, 2);
Console.WriteLine($"할인 없는 2잔: {previewTotal}원");

static bool TryReadQuantity(string text, out int quantity)
{
    if (int.TryParse(text, out quantity) && quantity > 0)
    {
        return true;
    }

    quantity = 0;
    return false;
}

static bool TryTakeStock(ref int stock, int quantity)
{
    if (quantity <= 0 || stock < quantity)
    {
        return false;
    }

    stock -= quantity;
    return true;
}

static int CalculateTotal(int unitPrice, int quantity, int discount = 0)
{
    int subtotal = unitPrice * quantity;
    return subtotal - ClampDiscount(discount);

    int ClampDiscount(int requested)
    {
        if (requested < 0)
        {
            return 0;
        }

        if (requested > subtotal)
        {
            return subtotal;
        }

        return requested;
    }
}

static void PrintReceipt(
    string menu,
    int quantity,
    int total,
    bool takeout = false)
{
    string takeoutText = takeout ? "예" : "아니요";
    Console.WriteLine($"메뉴: {menu} / 수량: {quantity}잔");
    Console.WriteLine($"결제 금액: {total}원 / 포장: {takeoutText}");
}

줄별 해설

빈 줄도 한 줄로 세면 완성 코드는 87줄이다. 아래 설명은 실행문과 함수 선언을 같은 흐름으로 연결한다. 코드의 줄 번호를 파일 안에 추가할 필요는 없다.

  1. 1줄: using System;으로 Console을 짧은 이름으로 사용한다.
  2. 3~6줄: 재고와 단가를 준비하고 주문 전 상태를 출력한다. 이후 재고가 실제로 바뀌었는지 비교할 기준이 된다.
  3. 8줄: 문자열 "3"을 수량으로 읽는다. 반환값은 조건에 사용하고, 읽은 정수는 out으로 선언한 quantity에 받는다.
  4. 10줄: 수량 검사가 성공한 경우에만 재고 차감을 시도한다. ref stock은 이 호출이 원래 재고를 변경할 수 있음을 표시한다.
  5. 12~13줄: 500원 할인을 적용한 금액을 받아 영수증에 전달한다. 할인은 잔마다 500원이 아니라 주문 전체에서 500원이다. 영수증 호출의 네 인수에는 모두 이름을 붙였다.
  6. 15~23줄: 재고 부족과 잘못된 수량을 각각 처리한다. 재고 차감 함수에는 양수가 확인된 수량만 전달하므로 여기서 차감 실패는 재고 부족을 뜻한다.
  7. 25줄: 변경된 호출자 쪽 재고를 출력한다. 재고 차감이 성공했으므로 값은 5다.
  8. 27~30줄: 정수로는 읽을 수 있지만 주문 수량으로 허용하지 않는 "0"을 시험한다. !로 실패 여부를 검사하고 실패 결과인 0을 출력한다.
  9. 32~33줄: 할인 인수를 생략해 2잔의 금액을 계산한다. 선택적 매개변수의 기본값 0을 사용하며, 이 계산은 재고를 변경하지 않는다.
  10. 35~44줄: int.TryParse가 문자열의 정수 해석을 시도한다. && 오른쪽의 양수 검사는 해석에 성공했을 때만 수행된다. 두 조건을 만족하면 성공을 반환하고, 나머지는 수량을 0으로 맞춘 뒤 실패를 반환한다.
  11. 46~55줄: 잘못된 수량과 재고 부족을 먼저 거절한다. 검사를 통과했을 때만 stock -= quantity;를 실행하므로 실패한 호출은 재고를 유지한다.
  12. 57~60줄: 단가와 수량으로 소계를 만든다. 로컬 함수가 제한한 할인액을 소계에서 빼서 반환한다. 계산 함수에는 양수 수량과 음수가 아닌 단가를 제공한다는 전제가 있다.
  13. 62~76줄: 할인액이 음수이면 0, 소계를 초과하면 소계, 그 사이이면 요청한 할인액을 반환한다. 따라서 이 예시의 입력 범위에서 결제 금액은 0보다 작아지지 않는다.
  14. 78~87줄: 영수증 출력은 결과값을 반환하지 않으므로 void다. 포장 인수를 생략하면 false를 사용한다. 표시할 문자열을 정한 뒤 두 줄을 출력한다.

특히 35줄의 함수는 실패할 때도 out에 값을 넣는다. int.TryParse의 성공만으로 주문을 허용하지 않고 양수라는 업무 조건을 추가했다. 이름에 Try를 붙였다는 이유로 언어가 이 규칙을 만들어 주지는 않는다. 성공 여부, 실패 결과, 상태 변경 여부는 작성자가 코드와 설명으로 정하는 약속이다.

실행 결과

.NET 10 SDK가 설치된 macOS 또는 Linux 터미널에서 프로젝트를 만든다. 다른 SDK도 설치되어 있다면 현재 경로의 SDK 선택 설정이 .NET 10을 사용하도록 되어 있는지 확인한다.

dotnet new console -n CafeMethods --framework net10.0
cd CafeMethods

생성된 Program.cs를 완성 코드로 교체한 뒤 다음 명령을 실행한다.

dotnet run

프로그램의 예상 표준 출력은 다음과 같다. 사용자 입력을 기다리는 부분이 없어 매번 같은 주문을 처리한다.

주문 전 재고: 8잔
메뉴: 라테 / 수량: 3잔
결제 금액: 13000원 / 포장: 예
주문 후 재고: 5잔
수량 거절: 0
할인 없는 2잔: 9000원

첫 주문은 4,500원에 3잔을 곱한 13,500원에서 500원을 뺀다. 재고는 8에서 3만큼 줄어든다. 마지막 2잔 계산은 금액만 미리 계산한 것이므로 재고를 추가로 차감하지 않는다. 실행할 때마다 시작 재고를 다시 8로 초기화하므로 이전 실행의 재고가 이어지지는 않는다.

실무에서 자주 틀리는 것

다음 코드는 실수를 비교하기 위한 조각이다. 틀린 코드에는 컴파일되지 않는 예시도 포함되어 있다. 완성 코드 전체에 이어 붙이지 말고 관련 선언과 호출을 비교한다.

반환값을 받지 않고 원래 변수가 바뀌기를 기대한다

다음 호출은 계산을 수행하지만 결과를 버린다. total은 계산 함수에 전달하지도 않았으므로 계속 0이다. 반환값을 무시하는 호출 자체는 컴파일 오류가 아니어서 결과를 확인해야 발견할 수 있다.

int total = 0;
CalculateTotal(4500, 3, discount: 500);
Console.WriteLine(total);

고친 코드는 반환값을 변수에 저장한다. 계산 함수와 호출자 사이에서 결과가 이동하는 지점이 대입문에 나타난다.

int total = CalculateTotal(4500, 3, discount: 500);
Console.WriteLine(total);

ref를 선언에만 쓰고 호출에서는 빠뜨린다

완성 코드의 재고 차감 함수는 ref int stock을 요구한다. 다음 호출은 값 전달을 요청하므로 선언과 맞지 않아 컴파일되지 않는다.

int stock = 8;
bool accepted = TryTakeStock(stock, 3);

호출에도 ref를 적어 수정한다. 재고가 바뀔 수 있다는 사실을 호출 위치에서 읽을 수 있다. 컴파일 오류를 없애려고 선언의 ref를 제거하면 호출자 재고가 줄어들지 않는 다른 문제가 생긴다.

int stock = 8;
bool accepted = TryTakeStock(ref stock, 3);

out에 성공할 때만 값을 넣는다

다음 함수는 문자열이 "3"이 아닌 경우 quantity에 대입하지 않고 반환한다. out의 대입 의무는 반환값이 true인 경로에만 적용되는 것이 아니므로 컴파일 오류가 난다.

static bool TryReadQuantity(string text, out int quantity)
{
    if (text == "3")
    {
        quantity = 3;
        return true;
    }

    return false;
}

이 비교 예시는 오직 "3"만 허용한다고 가정한다. 실패 경로에도 결과를 정하면 out의 약속을 지킬 수 있다. 여러 양수를 읽는 실제 구현은 완성 코드처럼 int.TryParse를 사용한다.

static bool TryReadQuantity(string text, out int quantity)
{
    if (text == "3")
    {
        quantity = 3;
        return true;
    }

    quantity = 0;
    return false;
}

선택적 매개변수 뒤에 필수 매개변수를 둔다

다음은 할인액만 기본값이 있는데 그 뒤에 필수 수량이 등장한다. 이런 매개변수 순서는 허용되지 않는다. 호출에서 명명 인수를 쓰겠다는 의도만으로 선언 규칙이 달라지지는 않는다.

static int CalculateTotal(int unitPrice, int discount = 0, int quantity)
{
    return unitPrice * quantity - discount;
}

필수 매개변수를 앞에 두고 선택적 매개변수를 뒤로 옮긴다. 아래는 순서만 설명하는 축약 계산이며, 완성 코드에 있는 할인 범위 제한은 생략했다.

static int CalculateTotal(int unitPrice, int quantity, int discount = 0)
{
    return unitPrice * quantity - discount;
}

이 선언에서는 CalculateTotal(4500, 3)처럼 할인액을 생략할 수 있다. 값을 지정하려면 CalculateTotal(4500, 3, discount: 500)이라고 쓴다. 이름은 호출자의 변수명이 아니라 선언에 적힌 매개변수명과 일치해야 한다.

한눈에 보기

주문 처리에서 각 문법을 선택하는 기준
문법표현하는 약속카페 예시확인할 점
일반 매개변수작업에 필요한 값을 받는다단가와 수량정수 대입은 호출자 변수를 바꾸지 않는다
return호출 결과를 제공한다결제 금액반환값을 저장하거나 사용한다
void결과값을 제공하지 않는다영수증 출력출력도 외부에 영향을 주는 동작이다
ref기존 변수에 접근한다재고 차감호출 전 초기화와 양쪽 표기가 필요하다
out결과를 변수에 대입한다해석한 주문 수량실패 경로에도 대입한다
선택적 매개변수생략 시 기본값을 쓴다할인액 0기본값 변경의 영향을 살핀다
명명 인수값의 역할을 이름으로 밝힌다takeout: true선언의 매개변수명을 쓴다
로컬 함수안쪽 작업에 이름을 붙인다할인 범위 제한바깥 변수 접근 여부를 살핀다

함수를 나누는 기준은 줄 수보다 작업의 약속이다. 계산 결과가 필요한지, 기존 상태를 바꿔야 하는지, 성공 여부와 별도의 결과가 함께 필요한지를 먼저 정한다. 그 뒤 반환값, ref, out을 고르면 선언과 호출이 같은 의도를 표현하게 된다.

연습 문제

  1. 완성 코드의 금액 계산을 사용해 단가 4,500원, 수량 2잔일 때 할인액을 생략한 결과와 discount: 10000으로 지정한 결과를 각각 구한다. 두 결과가 나오는 이유를 로컬 함수의 반환 경로로 설명한다.
  2. 재고가 2잔일 때 3잔을 차감하도록 TryTakeStock을 호출한다. 반환값과 호출 뒤 재고를 예상하고, 확인하는 코드 조각을 작성한다.
  3. TryReadQuantity에 "4", "-2", "라테"를 각각 전달했을 때 반환값과 out 결과를 적는다. 문자열을 정수로 읽는 데 성공해도 주문을 거절하는 경우를 설명한다.
  4. ClampDiscount를 static 로컬 함수로 바꾼다. 바깥 지역 변수를 직접 읽지 않도록 소계를 매개변수로 전달하고, 수정한 CalculateTotal 전체를 작성한다. 완성 코드의 실행 결과는 유지되어야 한다.

정답과 해설

1. 할인액의 기본값과 상한

할인액을 생략하면 결과는 9,000원이다. 기본값 0이 전달되고, ClampDiscount는 0을 그대로 반환한다. 10,000원 할인을 지정하면 결과는 0원이다. 소계가 9,000원이므로 로컬 함수가 할인액을 9,000원으로 제한한다. 할인액을 제한하는 단계가 없다면 결제 금액이 음수가 된다.

int regular = CalculateTotal(4500, 2);
int discounted = CalculateTotal(4500, 2, discount: 10000);

2. 실패한 차감은 재고를 유지한다

반환값은 false이고 재고는 2다. 다음 조각은 완성 코드의 실행문 영역에서 별도로 시험할 수 있다. 새 재고 변수에 대해 호출하므로 기존 stock과 구분된다.

int testStock = 2;
bool accepted = TryTakeStock(ref testStock, 3);
Console.WriteLine($"차감 성공: {accepted} / 재고: {testStock}잔");

출력은 차감 성공: False / 재고: 2잔이다. 재고가 수량보다 작다는 조건에서 먼저 반환하므로 대입문까지 진행하지 않는다. ref는 변경할 수 있게 하는 문법이며, 호출할 때마다 반드시 변경해야 한다는 뜻은 아니다.

3. 해석 성공과 업무 조건의 성공

입력별 수량 읽기 결과
입력반환값out 결과판단 이유
"4"true4정수이며 양수다
"-2"false0정수지만 양수가 아니다
"라테"false0정수로 해석할 수 없다

"-2"는 int.TryParse에는 성공하지만 양수 검사에서 거절된다. 이후 quantity = 0;이 실행되어 실패 결과를 통일한다. 호출자는 정수 해석의 세부 과정을 다시 검사하지 않고 TryReadQuantity의 반환값으로 주문 진행 여부를 결정할 수 있다.

4. 소계를 명시적으로 전달한다

바깥 변수인 subtotal을 읽던 부분을 매개변수 maximum으로 바꾼다. 호출에서는 요청 할인액과 소계를 함께 전달한다. 다음 선언으로 완성 코드의 같은 함수를 교체하면 된다.

static int CalculateTotal(int unitPrice, int quantity, int discount = 0)
{
    int subtotal = unitPrice * quantity;
    return subtotal - ClampDiscount(discount, subtotal);

    static int ClampDiscount(int requested, int maximum)
    {
        if (requested < 0)
        {
            return 0;
        }

        if (requested > maximum)
        {
            return maximum;
        }

        return requested;
    }
}

이제 로컬 함수가 필요로 하는 값이 매개변수 목록에 모두 나타난다. 캡처를 사용하는 원래 코드와 계산 결과는 같고, 함수의 위치도 금액 계산 내부로 유지된다. 짧고 가까운 계산에서는 캡처가 읽기 편할 수 있고, 의존하는 값을 분명히 드러내려면 이렇게 매개변수로 전달할 수 있다.

댓글 0

아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.

댓글을 남기려면 로그인이 필요합니다.