Devin.KR

함수 - 타입 선언과 명명 인수

개발자KR 조회 0

이 장에서 배우는 것

앞 장에서는 메뉴와 주문 목록을 배열에 담았다. 데이터를 모았으니 이제 그 데이터로 계산하고 출력하는 일을 이름 붙은 덩어리로 묶을 차례다. 그 덩어리가 함수(function)다. 이 장에서는 빵집 주문의 금액 계산을 함수로 나누면서, 함수가 받는 값과 돌려주는 값의 자료형을 적어 두는 방법, 호출할 때 이름으로 값을 넘기는 방법, 그리고 변수가 어디까지 보이는지를 정하는 규칙을 익힌다.

  • 매개변수 타입과 반환 타입을 선언하고, declare(strict_types=1) 가 호출에 미치는 영향을 설명할 수 있다.
  • 기본값과 명명 인수(named argument)로 호출을 읽기 쉽게 만든다.
  • 가변 인자(...)로 개수가 정해지지 않은 값을 받고, 배열을 펼쳐서 넘긴다.
  • 익명 함수와 화살표 함수를 만들고, 바깥 변수를 가져오는 두 방식의 차이를 구분한다.
  • 전역 범위와 함수 범위(scope)를 구분하고, static 변수의 동작을 안다.

문제 상황

빵집 사이트의 주문 목록을 출력하는 스크립트가 있다고 하자. 처음에는 반복문 안에 계산을 그대로 적는다. 단가에 수량을 곱하고, 할인율이 있으면 곱하고, 반올림하고, 천 단위 쉼표를 붙인다. 그런데 같은 계산이 주문 목록, 영수증, 관리자 화면에 각각 필요해지면 세 곳에 같은 식을 복사하게 된다. 한 곳의 반올림 규칙만 고치고 나머지를 놓치면 화면마다 금액이 달라진다.

또 다른 어려움은 호출하는 쪽에서 생긴다. calc(3800, 2, 0.1) 이라고 적혀 있으면 2와 0.1이 무엇인지 함수 정의를 열어 봐야 안다. 수량 자리에 "2" 같은 문자열이 들어가도 조용히 넘어가다가 엉뚱한 결과가 나오기도 한다. 이 장의 목표는 계산을 한 곳에 모으고, 받는 값의 형태를 코드에 적어 두고, 호출 문장만 읽어도 뜻이 드러나게 만드는 것이다.

함수의 기본 모양과 타입 선언

함수는 function 키워드, 이름, 괄호 안의 매개변수(parameter) 목록, 중괄호 안의 몸체로 이루어진다. 함수를 호출할 때 괄호에 넣는 실제 값을 인수(argument)라 부른다. 매개변수는 함수가 값을 받는 빈칸이고, 인수는 그 빈칸에 채우는 값이다. 함수가 return 으로 돌려주는 값은 반환값이다.

function formatWon(int $amount): string
{
    return number_format($amount) . '원';
}

매개변수 앞의 int 는 매개변수 타입, 괄호 뒤 : string 은 반환 타입이다. 이 둘을 적어 두면 PHP 가 호출 시점에 값의 형태를 검사한다. 함수의 사용법이 코드에 그대로 문서로 남는 효과도 있다.

자주 쓰는 타입 선언

타입 선언의 종류와 뜻
선언뜻예
int, float, string, bool해당 타입만 받는다int $quantity
array배열을 받는다array $menu
?intint 또는 null?int 반환
int|float나열한 타입 중 하나(유니온 타입)int|float $rate
callable호출할 수 있는 값(함수, 익명 함수)callable $rule
void돌려줄 값이 없다(반환 타입에만 씀): void

?int 는 값이 없을 수 있다는 뜻이다. 메뉴에서 이름으로 가격을 찾는데 없는 메뉴일 수 있다면, 반환 타입을 ?int 로 적고 없을 때 null 을 돌려주는 식으로 쓴다. 호출하는 쪽은 결과가 null 인지 먼저 확인해야 한다는 사실을 타입만 보고 알 수 있다.

strict_types 가 바꾸는 것

파일 맨 위에 declare(strict_types=1); 를 적으면 그 파일 안의 함수 호출과 반환에서 타입을 엄격하게 검사한다. 적지 않으면 PHP 는 가능한 만큼 값을 자동으로 바꿔서 받아들인다. 예를 들어 int 매개변수에 문자열 "3" 이 오면 3으로 바꿔 준다. 이 편리함은 나중에 "3개" 같은 값이 섞였을 때 원인을 찾기 어렵게 만든다. 엄격 모드에서는 int 자리에 문자열을 넘기면 곧바로 TypeError 라는 오류가 나서 실수한 위치가 드러난다.

예외가 하나 있다. 정수는 float 매개변수에 넘길 수 있다. 정수를 소수로 넓히는 것은 값이 손상되지 않기 때문이다. 반대로 float 를 int 자리에 넘기는 일은 허용되지 않는다. 이 책의 모든 예제는 strict_types 를 켠 상태를 기준으로 한다.

기본값·명명 인수·가변 인자

기본값

매개변수에 = 값 을 붙이면 호출 때 생략할 수 있다.

function linePrice(int $unitPrice, int $quantity = 1, float $discountRate = 0.0): int
{
    return (int) round($unitPrice * $quantity * (1 - $discountRate));
}

이제 linePrice(3800) 은 수량 1, 할인 없음으로 계산한다. 기본값이 있는 매개변수는 관례상 기본값이 없는 매개변수 뒤에 둔다. 그래야 필수 값부터 차례로 넘길 수 있다.

명명 인수

인수를 이름과 함께 이름: 값 형태로 넘기면 순서에 상관없이 원하는 매개변수에 넣을 수 있다. 이름에는 $ 를 붙이지 않는다.

linePrice(unitPrice: 3800, discountRate: 0.1, quantity: 2);
linePrice(3800, discountRate: 0.1);

두 번째 호출처럼 앞쪽은 위치로, 뒤쪽은 이름으로 섞어 쓸 수 있다. 다만 이름 붙인 인수 뒤에 위치 인수를 둘 수는 없다. 이름으로 넘기면 가운데에 있는 기본값 매개변수를 건너뛰고 그 뒤 매개변수만 지정할 수 있다는 점이 특히 유용하다. 위 두 번째 호출에서 quantity 는 기본값 1을 쓴다.

이름으로 넘기면 가운데 매개변수를 건너뛰고 뒤의 매개변수만 지정할 수 있다.

명명 인수는 호출하는 쪽 코드를 읽기 좋게 하지만, 매개변수 이름이 함수의 공개된 약속이 된다는 뜻이기도 하다. 함수 이름을 바꾸는 것만큼이나 매개변수 이름을 바꾸는 것도 그 함수를 부르는 모든 코드에 영향을 준다.

가변 인자와 펼치기

매개변수 앞에 ... 를 붙이면 남은 인수를 모두 받아 배열로 만든다.

function total(int ...$prices): int
{
    return array_sum($prices);
}

total(4500, 8400);       // 12900
total();                 // 0
$list = [11400, 4500];
total(...$list);         // 15900

가변 인자 매개변수는 목록의 맨 마지막에 하나만 둘 수 있다. int ...$prices 라고 적으면 받은 값이 모두 int 인지 검사한다. 호출할 때 배열 앞에 ... 를 붙이면 반대로 배열을 낱개의 인수로 펼쳐 준다. 같은 기호가 정의에서는 모으는 역할, 호출에서는 펼치는 역할을 한다.

익명 함수·화살표 함수·스코프

익명 함수와 화살표 함수

이름 없이 만든 함수를 변수에 담을 수 있다. 이를 익명 함수(closure)라 한다. 함수를 값으로 다루면 다른 함수에 규칙을 넘길 수 있다.

$couponWon = 1000;
$applyCoupon = function (int $amount) use ($couponWon): int {
    return max(0, $amount - $couponWon);
};
echo $applyCoupon(5000);   // 4000

익명 함수는 바깥 변수를 자동으로 보지 못한다. 필요한 변수를 use (...) 로 직접 적어 가져와야 한다. 이때 가져오는 것은 변수가 아니라 만든 시점의 값의 복사본이다. 만든 뒤에 바깥의 $couponWon 을 바꿔도 함수 안의 값은 그대로다.

식 하나만 계산해서 돌려주는 짧은 함수라면 화살표 함수가 간결하다.

$feeRate = 0.1;
$rule = fn(int $p): int => (int) round($p * (1 + $feeRate));

화살표 함수는 use 없이 바깥 변수를 자동으로 가져온다. 이 역시 만든 시점의 값의 복사본이다. 몸체는 return 없이 식 하나만 쓸 수 있다. 여러 줄이 필요하면 일반 익명 함수를 쓴다.

세 가지 함수 형태 비교
형태바깥 변수몸체주로 쓰는 곳
이름 있는 함수보지 못한다여러 문장여러 곳에서 재사용
익명 함수use 로 명시해 복사여러 문장한 곳에서 쓰는 규칙
화살표 함수자동으로 복사식 하나짧은 변환과 조건

함수를 넘겨받는 쪽은 callable 타입으로 받는다. array_map 이 그런 함수다. 이미 이름이 있는 함수를 값으로 넘기고 싶으면 formatWon(...) 처럼 괄호 안에 ... 만 적는다.

스코프

변수가 보이는 범위를 스코프라 한다. 파일 최상위에서 만든 변수는 전역 범위에 있고, 함수는 호출될 때마다 자기만의 새 범위를 가진다. 함수 안에서는 바깥의 전역 변수가 보이지 않는다. 안에서 만든 변수도 함수가 끝나면 사라진다.

함수 안에서는 전역 변수가 보이지 않고, 익명 함수는 use 로 값을 복사해 온 것만 볼 수 있다.

바깥 값이 필요하면 매개변수로 넘기는 것이 가장 분명하다. global 키워드로 전역 변수를 끌어올 수도 있지만, 함수가 어떤 값에 의존하는지 호출문에서 보이지 않게 되므로 권하지 않는다. 함수의 입력은 매개변수로, 출력은 반환값으로 주고받는다고 기억해 두면 된다.

호출 사이에 값을 남겨야 할 때는 static 변수를 쓴다. 함수 안에서 static $last = 0; 이라고 적으면 초기화는 처음 한 번만 이루어지고, 이후 호출에서는 앞 호출이 남긴 값을 이어서 쓴다. 주문 번호를 하나씩 올리는 데 어울린다. 다만 이 값은 스크립트가 끝나면 사라진다. 다음 실행에도 이어야 하는 값은 파일이나 데이터베이스에 저장해야 하며, 이는 뒤에서 다룬다.

매개변수로 받은 배열이나 숫자를 함수 안에서 바꿔도 바깥 변수는 바뀌지 않는다. 인수는 기본적으로 값이 복사되어 넘어가기 때문이다. 바뀐 결과가 필요하면 새 값을 return 으로 돌려주고 호출하는 쪽에서 받는다.

완성 코드

아래 파일 하나를 main.php 로 저장한다. 주문 목록의 금액을 계산해 출력하는 프로그램이다.

main.php

<?php
declare(strict_types=1);

function formatWon(int $amount): string
{
    return number_format($amount) . '원';
}

function linePrice(int $unitPrice, int $quantity = 1, float $discountRate = 0.0): int
{
    return (int) round($unitPrice * $quantity * (1 - $discountRate));
}

function findPrice(array $menu, string $name): ?int
{
    return $menu[$name] ?? null;
}

function total(int ...$prices): int
{
    return array_sum($prices);
}

function mapPrices(array $prices, callable $rule): array
{
    return array_map($rule, $prices);
}

function nextOrderNo(): int
{
    static $last = 0;
    $last++;
    return $last;
}

function showStore(): string
{
    return isset($storeName) ? $storeName : '(이름 없음)';
}

function printLine(string $text): void
{
    echo $text, "\n";
}

$storeName = '골목 빵집';
$menu = ['소금빵' => 3800, '바게트' => 4500, '크루아상' => 4200];
$orders = [
    ['name' => '소금빵', 'qty' => 3],
    ['name' => '바게트', 'qty' => 1],
    ['name' => '크루아상', 'qty' => 2],
    ['name' => '식빵', 'qty' => 1],
];

printLine('== ' . $storeName . ' ==');
printLine('함수 안에서 본 가게 이름: ' . showStore());

$subtotals = [];
foreach ($orders as $order) {
    $price = findPrice($menu, $order['name']);
    if ($price === null) {
        printLine($order['name'] . ': 메뉴에 없음');
        continue;
    }
    $sub = linePrice($price, $order['qty']);
    $subtotals[] = $sub;
    printLine(sprintf('%s x%d = %s', $order['name'], $order['qty'], formatWon($sub)));
}

printLine('합계: ' . formatWon(total(...$subtotals)));
printLine('빈 주문 합계: ' . formatWon(total()));
printLine('소금빵 2개 10% 할인: ' . formatWon(linePrice(unitPrice: 3800, discountRate: 0.1, quantity: 2)));

$couponWon = 1000;
$applyCoupon = function (int $amount) use ($couponWon): int {
    return max(0, $amount - $couponWon);
};
printLine('쿠폰 적용 후: ' . formatWon($applyCoupon(total(...$subtotals))));

$feeRate = 0.1;
$withFee = mapPrices($subtotals, fn(int $p): int => (int) round($p * (1 + $feeRate)));
printLine('수수료 포함: ' . implode(', ', array_map(formatWon(...), $withFee)));

printLine('주문 번호: ' . nextOrderNo() . ', ' . nextOrderNo() . ', ' . nextOrderNo());

줄별 해설

formatWon: 정수 금액을 받아 number_format 으로 천 단위 쉼표를 붙이고 원 을 이어 붙인 문자열을 돌려준다. 금액 표시 규칙이 이 함수 한 곳에만 있으므로 형식을 바꿀 때 여기만 고치면 된다.

linePrice: 단가는 필수이고 수량과 할인율에는 기본값이 있다. 1 - $discountRate 는 남는 비율이다. 곱한 결과는 소수가 될 수 있어서 round 로 반올림하고 (int) 로 정수로 바꿔 : int 반환 타입을 맞춘다. round 는 float 를 돌려주므로 이 변환이 없으면 엄격 모드에서 반환 타입 오류가 난다.

findPrice: 앞 장에서 배운 ?? 로 메뉴에 키가 없으면 null 을 돌려준다. 반환 타입 ?int 가 이를 허용한다.

total: int ...$prices 는 받은 인수를 배열로 모은다. 인수가 없으면 빈 배열이고, array_sum 은 빈 배열에서 0을 돌려준다.

mapPrices: 가격 배열과 규칙 함수를 받아 모든 가격에 규칙을 적용한다. 규칙 자리에 익명 함수든 이름 있는 함수든 넘길 수 있도록 callable 로 받는다.

nextOrderNo: static $last = 0; 은 처음 호출될 때만 0으로 초기화된다. 이후 호출은 앞에서 남긴 값에서 이어서 1씩 늘린다.

showStore: 함수 안에서 전역의 $storeName 을 찾지만 보이지 않는다. isset 은 없는 변수에 경고 없이 false 를 돌려주므로 대체 문구가 나온다. 스코프가 분리되어 있음을 확인하려는 코드이며, 실제 프로그램에서는 이름을 매개변수로 받아야 한다.

printLine: 문자열 한 줄을 출력하고 줄바꿈을 붙인다. 돌려줄 값이 없으므로 void 다.

주문 목록 반복: 각 주문의 가격을 찾고, null 이면 안내를 출력한 뒤 continue 로 다음 주문으로 넘어간다. 가격이 있으면 위치 인수로 linePrice 를 호출해 소계를 구하고 $subtotals 에 쌓는다. sprintf 의 %s 는 문자열, %d 는 정수 자리다.

total(...$subtotals): 배열을 낱개의 인수로 펼쳐서 넘긴다. total() 처럼 인수 없이 부르는 것도 가능하다.

명명 인수 호출: 순서를 바꿔 이름으로 넘겼다. 이름 덕분에 0.1이 할인율이라는 사실이 호출문에서 바로 읽힌다.

$applyCoupon: use ($couponWon) 으로 쿠폰 금액의 복사본을 가져온다. max(0, ...) 는 금액이 음수가 되는 것을 막는다.

$withFee: 화살표 함수는 $feeRate 를 use 없이 가져온다. array_map(formatWon(...), $withFee) 는 이름 있는 함수를 값으로 넘겨 각 금액을 문자열로 바꾸고, implode 가 쉼표로 이어 붙인다.

마지막 줄: 식 안의 nextOrderNo() 세 번은 왼쪽부터 차례로 실행되어 1, 2, 3이 된다.

실행 결과

$ php main.php
== 골목 빵집 ==
함수 안에서 본 가게 이름: (이름 없음)
소금빵 x3 = 11,400원
바게트 x1 = 4,500원
크루아상 x2 = 8,400원
식빵: 메뉴에 없음
합계: 24,300원
빈 주문 합계: 0원
소금빵 2개 10% 할인: 6,840원
쿠폰 적용 후: 23,300원
수수료 포함: 12,540원, 4,950원, 9,240원
주문 번호: 1, 2, 3

실무에서 자주 틀리는 것

함수 안에서 바깥 변수를 그냥 쓴다

틀린 코드는 전역 변수가 함수 안에서도 보일 것이라고 기대한다.

$couponWon = 1000;

function applyCoupon(int $amount): int
{
    return $amount - $couponWon;   // $couponWon 은 이 범위에 없다
}

함수 안의 $couponWon 은 정의되지 않은 변수라 경고가 나오고 null 로 계산된다. 고친 코드는 필요한 값을 매개변수로 받는다.

function applyCoupon(int $amount, int $couponWon): int
{
    return $amount - $couponWon;
}

폼에서 온 문자열을 그대로 int 자리에 넘긴다

$qty = '3';                     // 폼 값은 문자열로 온다
echo linePrice(3800, $qty);     // TypeError

엄격 모드에서 int 매개변수에 문자열을 넘겼으므로 TypeError 가 난다. 오류를 없애려고 타입 선언을 지우지 말고, 경계에서 값을 검증하고 바꾼 뒤 넘긴다.

$qty = filter_var('3', FILTER_VALIDATE_INT);
if ($qty === false) {
    echo '수량은 정수여야 한다';
} else {
    echo linePrice(3800, $qty);
}

화살표 함수 안에서 바깥 변수를 바꾸려 한다

$count = 0;
array_map(fn(int $p): int => $count++, [100, 200]);
echo $count;   // 0

화살표 함수는 값을 복사해 가져오므로 안에서 늘린 것은 복사본이다. 바깥의 $count 는 0으로 남는다. 결과를 반환값으로 받아 바깥에서 세는 것이 낫다.

$prices = [100, 200];
$count = count($prices);
echo $count;   // 2

이름 붙인 인수 뒤에 위치 인수를 둔다

linePrice(quantity: 2, 3800);   // 컴파일 오류

이름 붙인 인수 뒤에는 위치 인수를 둘 수 없다. 위치 인수를 앞에 두거나 전부 이름으로 적는다.

linePrice(3800, quantity: 2);
linePrice(quantity: 2, unitPrice: 3800);

한눈에 보기

이 장에서 다룬 문법 요약
주제문법핵심
타입 선언function f(int $a): string호출·반환 값을 검사한다
null 허용?int값이 없을 수 있다는 약속
엄격 모드declare(strict_types=1);자동 변환 없이 TypeError
기본값int $q = 1필수 매개변수 뒤에 둔다
명명 인수f(quantity: 2)순서 무관, 위치 인수는 앞에만
가변 인자int ...$xs, f(...$list)모으기와 펼치기
익명 함수function () use ($x) {}값을 복사해 가져온다
화살표 함수fn($p) => 식자동 복사, 식 하나
static 변수static $n = 0;호출 사이에 값을 유지한다

연습 문제

  1. 정가 $price 와 할인 퍼센트 $percent (둘 다 int)를 받아 할인된 가격을 int 로 돌려주는 함수 discountedPrice 를 작성하라. 5000원에 20% 할인이면 4000이 나와야 한다. 퍼센트의 기본값은 0으로 한다.
  2. 가변 인자로 가격들을 받아 가장 비싼 가격을 돌려주는 maxPrice 를 작성하라. 인수가 하나도 없으면 null 을 돌려주어야 한다. 반환 타입은 무엇이어야 하는가.
  3. 배달비 3000원을 $delivery 변수에 두고, 화살표 함수와 array_map 으로 [4000, 5500] 의 각 금액에 배달비를 더한 배열을 만들어라.
  4. 본문의 linePrice 를 명명 인수만 써서 호출하라. 크루아상(4200원) 5개에 할인율 0.05이며, 인수는 discountRate, unitPrice, quantity 순서로 적는다. 결과 금액은 얼마인가.

정답과 해설

1번

function discountedPrice(int $price, int $percent = 0): int
{
    return intdiv($price * (100 - $percent), 100);
}

echo discountedPrice(5000, 20);   // 4000

intdiv 는 정수 나눗셈이라 int 반환 타입과 바로 맞는다. 5000 × 80 = 400000을 100으로 나눈 값이 4000이다. 소수 계산을 쓰려면 round 와 (int) 를 함께 써야 한다. 나누어떨어지지 않으면 intdiv 는 소수점 아래를 버린다는 점에 유의한다.

2번

function maxPrice(int ...$prices): ?int
{
    return $prices === [] ? null : max($prices);
}

빈 배열에 max 를 쓰면 오류가 나므로 먼저 비었는지 확인한다. 값이 없을 수 있으니 반환 타입은 ?int 다.

3번

$delivery = 3000;
$result = array_map(fn(int $p): int => $p + $delivery, [4000, 5500]);
// [7000, 8500]

화살표 함수가 $delivery 를 자동으로 복사해 가져오므로 use 가 필요 없다.

4번

echo linePrice(discountRate: 0.05, unitPrice: 4200, quantity: 5);   // 19950

4200 × 5 = 21000에 0.95를 곱하면 19950이다. 이름으로 넘기면 순서가 달라도 각 값이 알맞은 매개변수에 들어간다.

댓글 0

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

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