PHP 면접 준비

PHP 면접 질문

시니어리티 수준별로 분류한 PHP 백엔드 면접 질문 15개입니다. 기본기, 실무 트레이드오프, 시니어 수준의 프로덕션 판단을 복습하는 데 활용하세요.

PHP AI 면접 시작하기신용카드가 필요하지 않습니다. 무료 세션 1회 제공.
영어 기술 면접 연습비원어민이 기술 면접 통과를 연습할 수 있는 모드입니다.

주니어 질문

1PHP의 약한 타입 검사와 엄격한 타입 검사는 어떻게 다르며 declare(strict_types=1)은 무엇을 바꾸나요?

PHP는 보통 선언된 매개변수와 반환 타입에 호환되는 스칼라 변환을 허용합니다. 예를 들어 int 매개변수에 숫자 문자열을 전달할 수 있습니다. declare(strict_types=1)은 엄격한 스칼라 검사를 켜며, 호환되지 않는 인수나 반환 값은 TypeError를 발생시킵니다. 다만 float에는 여전히 int가 허용됩니다. 사용자 정의 함수의 인수 검사는 호출 파일이, 반환 값 검사는 정의 파일이 엄격성을 결정합니다. PHP 내부 함수에서 발생하는 호출에는 별도 주의 사항이 있으며 단순히 바깥 호출자의 strict_types를 따르는 것은 아닙니다. 지시문은 PHP 파일의 시작 부분에 있어야 합니다. PHP를 정적 타입 언어로 만들거나 언어 전체의 연산자 변환을 끄지는 않습니다.

// THREE SEPARATE FILES: save each PHP block under its indicated filename.
// File: lib.php
<?php
function takesInt(int $x): int { return $x; }
?>

// File: weak.php
<?php
require __DIR__ . '/lib.php';
echo takesInt('123'); // 123: coercive caller
?>

// File: strict.php
<?php
declare(strict_types=1);
require __DIR__ . '/lib.php';
try {
    echo takesInt('123');
} catch (TypeError $e) {
    echo 'TypeError';
}
?>
AI 코치와 함께 이 질문에 답해 보세요

2숫자 문자열, 배열, 객체, in_array()의 함정을 포함해 PHP의 ==와 === 차이를 설명하세요.

==는 필요한 경우 타입을 변환하는 느슨한 비교이고, ===는 스칼라 강제 변환 없이 타입과 값의 일치를 요구합니다. 숫자 문자열은 숫자로 비교될 수 있으므로 타입까지 정확히 일치해야 하는 값에는 느슨한 비교를 사용하지 않습니다. 배열의 ==는 비교 규칙상 같은 키/값 쌍을 포함하면 순서와 무관하게 같습니다. ===는 순서와 키 및 값의 타입까지 같아야 합니다. 객체의 ==는 같은 클래스 객체들의 속성을 비교하고, ===는 동일한 인스턴스를 요구합니다. in_array는 기본적으로 느슨하게 비교하므로 엄격한 검사에는 세 번째 인수로 true를 전달합니다. 숫자와 문자열의 느슨한 비교는 PHP 8에서 바뀌었으므로 버전 없이 과거 변환 예제에 의존하면 안 됩니다.

<?php
var_dump(123 == '123');      // loose: numeric string is converted
var_dump(123 === '123');     // strict: int !== string

$values = [0, 1, 2];
var_dump(in_array('0', $values));       // loose
var_dump(in_array('0', $values, true)); // strict
AI 코치와 함께 이 질문에 답해 보세요

3PHP 반환 타입 mixed, void, never는 어떻게 다르며 무엇을 보장하나요?

mixed는 null을 포함한 모든 반환 값을 허용합니다. void는 의미 있는 값을 반환하지 않는다는 선언입니다. 함수 끝에 도달하거나 return;을 사용할 수 있지만 표현식을 반환할 수는 없습니다. void 함수의 정상 호출 결과는 null입니다. PHP 8.1부터 제공되는 never는 함수가 호출자에게 정상적으로 돌아오지 않음을 뜻합니다. 예외를 던지거나 exit로 종료하거나 무한 루프에 머물 수 있으며, 결국 프로세스를 종료해야 하는 것은 아닙니다. never 함수에는 return 문을 넣을 수 없습니다. 핵심 차이는 void 함수는 정상 종료하여 호출자가 계속할 수 있지만 never에는 정상적인 후속 실행이 없다는 점입니다.

<?php
function parse(string $raw): mixed {
    return json_decode($raw, true);
}

function logMessage(string $message): void {
    error_log($message);
    return; // allowed
}

function fail(string $message): never {
    throw new RuntimeException($message);
}

logMessage('starting');
fail('stop');
echo 'unreachable';
AI 코치와 함께 이 질문에 답해 보세요

4PHP의 iterable은 무엇이며 array, Traversable, Iterator, Generator와 어떤 관계인가요?

iterable은 array|Traversable을 뜻합니다. Generator는 Iterator를 구현하고 Iterator는 Traversable을 확장하므로 iterable을 만족합니다. 사용자 정의 클래스는 구체 클래스에서 Traversable을 직접 구현하는 대신 Iterator나 IteratorAggregate를 통해 그 동작을 제공합니다. Iterator는 반복을 제어하는 메서드를 노출하고 IteratorAggregate는 이터레이터를 제공합니다. yield를 포함한 생성기 함수는 Generator를 반환합니다. 중요한 차이가 있습니다. foreach는 Traversable을 구현하지 않은 일반 객체의 보이는 속성도 순회할 수 있지만, 그렇다고 그 객체가 iterable 매개변수 선언을 만족하는 것은 아닙니다.

function printItems(iterable $items): void {
    foreach ($items as $item) {
        echo $item, PHP_EOL;
    }
}

printItems(['a', 'b']);

function letters(): Generator {
    yield 'x';
    yield 'y';
}

printItems(letters());
AI 코치와 함께 이 질문에 답해 보세요

5PHP의 public, protected, private 접근 규칙은 무엇이며 상속 및 트레이트와 어떻게 상호작용하나요?

public 멤버는 객체나 클래스에 접근할 수 있는 곳이면 사용할 수 있습니다. protected 멤버는 선언 클래스와 관련 부모·자식 클래스 계층에서 접근할 수 있지만 임의의 외부 코드에서는 안 됩니다. private 멤버는 선언 클래스 안에서 접근할 수 있으며, 자식 클래스는 부모의 private 멤버를 상속받은 인터페이스로 사용할 수 없습니다. 일반적인 재정의 메서드는 상속된 접근 범위를 좁힐 수 없습니다. 다만 생성자에는 특별한 상속 규칙이 있으므로 이 규칙의 무조건적 예로 취급하면 안 됩니다. 트레이트 멤버는 사용하는 클래스에 합성되어 그 접근 규칙을 따릅니다. as는 별칭을 만들거나 메서드 접근 범위를 바꾸거나 둘 다 할 수 있으며, 별칭을 추가해도 원래 이름은 사라지지 않습니다.

<?php
class Base {
    public function a() {}
    protected function b() {}
    private function c() {}
}

trait Logs {
    public function log(string $message): void {}
}

class Child extends Base {
    use Logs { log as protected writeLog; }

    public function test(): void {
        $this->a();              // OK
        $this->b();              // OK
        // $this->c();           // Not accessible: private to Base
        $this->writeLog('ok');   // OK inside class, protected alias
    }
}

$child = new Child();
$child->a();
// $child->b();           // Not accessible from outside
// $child->writeLog('x'); // Not accessible from outside
AI 코치와 함께 이 질문에 답해 보세요

6PHP 트레이트는 어떻게 동작하며 여러 트레이트가 같은 메서드를 정의하면 충돌을 어떻게 해결하나요?

트레이트는 수평적 코드 재사용을 제공합니다. 클래스는 use로 트레이트 멤버를 가져오며 트레이트 자체는 인스턴스화할 수 없습니다. 클래스에 선언한 메서드는 트레이트 메서드보다 우선하고, 트레이트 메서드는 상속된 부모 메서드보다 우선합니다. 여러 트레이트가 같은 메서드를 제공하고 클래스 메서드로 이미 해결되지 않았다면 insteadof로 구현을 선택합니다. as는 트레이트 메서드에 추가 이름을 주고 선택적으로 접근 범위도 바꿉니다. 별칭은 원래 메서드의 이름을 바꾸거나 제거하지 않습니다. 충돌 해결과 최종 공개 인터페이스의 명확성 유지는 별개의 책임입니다.

<?php
trait JsonLogger {
    public function log(string $message): void {
        echo "json:$message\n";
    }
}

trait FileLogger {
    public function log(string $message): void {
        echo "file:$message\n";
    }
}

class Service {
    use JsonLogger, FileLogger {
        JsonLogger::log insteadof FileLogger;
        FileLogger::log as logToFile;
        JsonLogger::log as protected logJson;
    }
}

$service = new Service();
$service->log('hello');
$service->logToFile('hello');
// $service->logJson('hello'); // not accessible from outside; protected alias
AI 코치와 함께 이 질문에 답해 보세요

7순수 열거형(Pure Enum)과 백트 열거형(Backed Enum)을 포함한 PHP의 열거형(enum)이란 무엇이며, 도메인 모델링을 어떻게 개선합니까?

PHP 열거형(enum)은 주문 상태나 사용자 역할처럼 특정 타입에 대해 닫힌 명명된 케이스 집합을 정의합니다. 순수 열거형(Pure Enum)은 이름이 지정된 케이스만 가지며, 백트 열거형(Backed Enum)은 각 케이스마다 고유한 문자열(string) 또는 정수(int) 값을 부여하여 데이터베이스 저장, API 연동 또는 상호 운용성에 유용합니다. 열거형은 깨지기 쉬운 원시 문자열, 정수 또는 상수를 타입 안전하고 자체 문서화되는 값으로 대체하여 도메인 모델링을 개선합니다. 또한 PHP 열거형은 메서드를 가질 수 있고 인터페이스를 구현할 수 있으며, 모든 열거형은 케이스 목록 조회를 지원하고 백트 열거형은 스칼라 값과의 상호 변환을 지원합니다.

enum OrderState
{
    case Draft;
    case Paid;
    case Shipped;
}

enum PaymentStatus: string
{
    case Pending = 'pending';
    case Complete = 'complete';
    case Failed = 'failed';
}

$status = PaymentStatus::tryFrom('complete');
var_dump($status === PaymentStatus::Complete);
AI 코치와 함께 이 질문에 답해 보세요

미들 질문

8스칼라, nullable, 유니온, 교차, DNF 타입은 PHP 인터페이스의 계약을 어떻게 표현하며 언제 사용하나요?

PHP 선언은 허용하는 값과 반환하는 값을 설명합니다. int, float, string, bool 같은 스칼라 선언은 값의 범주를 선택하며, 호환되는 스칼라 변환 허용 여부는 strict_types와 문맥에도 달려 있습니다. ?T 또는 T|null은 null을 허용합니다. PHP 8.0부터의 유니온은 int|string 같은 대안을 허용합니다. PHP 8.1부터의 순수 교차 타입은 Cacheable&JsonSerializable처럼 열거된 클래스·인터페이스 타입을 모두 만족하는 객체를 요구하며 int&string은 유효하지 않습니다. PHP 8.2부터의 DNF 타입(논리합 정규형)은 (Cacheable&JsonSerializable)|string처럼 대안과 괄호로 묶인 교차를 결합합니다. 불필요한 대안을 추가하지 말고 유효한 입력을 나타내는 가장 좁고 이해하기 쉬운 계약을 선택합니다.

<?php
interface Cacheable {}

class Report implements Cacheable, JsonSerializable {
    public function jsonSerialize(): mixed { return []; }
}

function findName(int $id): ?string {
    return $id > 0 ? 'Alice' : null;
}

function normalize(int|string $id): string {
    return (string) $id;
}

function store(Cacheable&JsonSerializable $item): void {
    // must satisfy both interfaces
}

function handle((Cacheable&JsonSerializable)|string $input): void {
    // either an object satisfying both contracts, or a string
}
AI 코치와 함께 이 질문에 답해 보세요

9PHP의 callable, Closure, 일급 callable, 호출 가능한 객체, callable 배열은 어떻게 다른가요?

callable은 함수 이름 문자열, Closure 객체, 접근 가능한 메서드 참조, __invoke가 있는 객체 등 호출 가능한 값을 설명합니다. Closure는 구체적인 호출 가능 객체로, 익명 함수나 Closure로 변환한 이름 있는 함수·메서드에서 생성될 수 있습니다. strlen(...)이나 $object->method(...) 같은 일급 callable 문법은 대상을 호출하지 않고 Closure를 만듭니다. [$object, 'method']는 접근 가능한 인스턴스 메서드를 참조합니다. 현재 PHP의 [ClassName::class, 'method']에는 적합하고 접근 가능한 정적 메서드가 필요합니다. is_callable은 해당 범위에서 호출 가능한지 검사합니다. callable은 넓은 계약이고 Closure는 구체적인 표현 방식입니다.

class Greeter {
    public function hello(string $name): string { return "Hello $name"; }
    public function __invoke(string $name): string { return "Hi $name"; }
}

$g = new Greeter();

$closure = fn(string $name) => "Hey $name";
$arrayCallable = [$g, 'hello'];
$firstClass = $g->hello(...);
$invokable = $g;

foreach ([$closure, $arrayCallable, $firstClass, $invokable] as $cb) {
    echo $cb('Sam'), PHP_EOL;
}
AI 코치와 함께 이 질문에 답해 보세요

10PHP는 정수 오버플로, 부동소수점 정밀도 및 정확한 십진수 계산을 어떻게 처리하나요?

PHP의 정수 크기는 플랫폼에 따라 결정되며, `PHP_INT_MAX`와 같은 상수로 한계값이 제공됩니다. PHP는 정수 오버플로 발생 시 원시 정수 연산 결과를 임의 정밀도 정수로 자동 변환하지 않습니다. 오버플로된 정수 연산은 `float` 타입으로 바뀌거나 고유한 정수 의미를 잃게 됩니다. PHP의 부동소수점(`float`)은 이진 부동소수점 값이므로 `0.1`과 같은 많은 십진 소수가 근사치로 표현됩니다. 따라서 정확한 십진수나 금액 계산을 수행할 때는 부동소수점을 피하고, 상황에 맞게 정수 최소 통화 단위(예: 센트, 원)를 사용하거나 BCMath와 같은 고정/임의 정밀도 십진 연산 도구를 사용해야 합니다. 임의 정밀도 정수 연산에는 GMP가 유용합니다.

var_dump(0.1 + 0.2 === 0.3);
printf('%.17f\n', 0.1 + 0.2);
AI 코치와 함께 이 질문에 답해 보세요

11PHP의 참조(&)와 쓰기 시점 복사(copy-on-write), 그리고 참조가 예상치 못한 부작용을 일으키는 방식에 대해 설명해 주세요.

PHP 변수는 내부적으로 zval 구조체로 표현됩니다. 배열이나 문자열 같은 값에 대해 PHP는 쓰기 시점 복사(copy-on-write) 방식을 사용합니다. 즉, `$b = $a`와 같이 할당하면 한 변수가 수정되기 전까지는 내부적으로 동일한 값을 공유하다가, 수정이 발생하는 시점에 PHP가 값을 분리하여 복사합니다. `&`로 생성된 참조는 변수들이 동일한 컨테이너를 가리키는 별칭이 되도록 만듭니다. 따라서 한 변수를 변경하면 다른 변수도 변경되며, 일반적인 할당에서 기대하는 분리 동작을 우회할 수 있습니다. 참조에 의한 전달(pass by reference)을 사용하면 함수가 호출자의 변수를 직접 수정할 수도 있습니다. 참조는 특히 `foreach ($array as &$value)`와 함께 사용할 때 의도치 않은 부작용을 유발할 수 있습니다. 루프가 종료된 후에도 루프 변수가 마지막 요소를 가리키는 참조 상태로 남아있기 때문에, `unset($value)`를 호출하지 않고 나중에 해당 변수를 재사용하면 의도치 않게 배열의 마지막 요소를 덮어쓸 위험이 있습니다.

$a = [1, 2];
$b = $a;      // shared until modification
$b[0] = 99;   // separates; $a is unchanged
var_dump($a, $b);

$c =& $a;     // alias/reference
$c[0] = 42;   // modifies $a too
var_dump($a, $c);
AI 코치와 함께 이 질문에 답해 보세요

12PHP에서 객체 핸들, 대입, clone, __clone, 얕은 복사, 깊은 복사는 각각 어떻게 동작하나요?

PHP에서 객체 변수는 전체 객체 복사본이 아닌 객체에 대한 핸들을 저장합니다. `$b = $a`와 같이 대입하면 핸들이 복사되므로 두 변수가 동일한 객체를 가리키게 되며, `&` 참조 연산자를 사용하지 않더라도 어느 한쪽 변수를 통해 객체를 변경하면 다른 쪽에도 즉시 반영됩니다. `clone $a`는 원본 객체의 프로퍼티를 얕은 복사(shallow copy)하여 새로운 객체 인스턴스를 생성합니다. 만약 프로퍼티 자체가 객체라면, 명시적으로 복제하지 않는 한 복제된 객체의 프로퍼티도 동일한 중첩 객체를 계속 가리킵니다. `__clone()` 메서드는 얕은 복사가 완료된 후 새 객체에서 호출되며, ID 재설정, 리소스 분리, 중첩 객체의 깊은 복사(deep copy) 등을 수행하는 위치로 사용됩니다. 깊은 복사는 공유되어서는 안 되는 중첩된 가변 객체들을 재귀적으로 복사하는 것을 의미합니다.

class Address {
    public function __construct(public string $city) {}
}

class User {
    public function __construct(public string $name, public Address $address) {}

    public function __clone() {
        $this->address = clone $this->address; // make nested object independent
    }
}

$u1 = new User('Ann', new Address('Paris'));
$u2 = $u1;          // same object handle
$u2->name = 'Beth';
echo $u1->name, PHP_EOL;

$u3 = clone $u1;    // new User object; __clone deep-clones address
$u3->address->city = 'Rome';
echo $u1->address->city, PHP_EOL;
echo $u3->address->city, PHP_EOL;
AI 코치와 함께 이 질문에 답해 보세요

시니어 질문

13데이터베이스 연결 고갈 문제를 어떻게 트러블슈팅하고, PHP-FPM(FastCGI Process Manager) 및 장시간 실행되는 워커를 위한 연결 관리 방식을 어떻게 설계하시겠습니까?

연결 고갈을 해결하기 위해 먼저 데이터베이스의 최대 연결 수 및 현재 활성 세션 수를 전체 PHP 동시성 수준과 비교합니다. 활성화된 각 PHP-FPM 자식 프로세스는 자체 DB 연결을 점유할 수 있으며, 애플리케이션 풀, 호스트 수, 크론 작업, 큐 워커, 마이그레이션 및 배포가 늘어날수록 연결 요구량이 곱절로 증가합니다. 이를 점검하기 위해 DB 세션 뷰, 연결 상태, 트랜잭션 유휴(idle-in-transaction) 세션, 슬로우 쿼리, 락 대기, 에러 로그, 연결 풀 지표, 최근 트래픽 및 워커 수의 변화를 조사합니다. PHP-FPM의 경우, max_children과 호스트별 동시성을 데이터베이스 허용 한도 내로 유지하고, 요청 범위 내에서만 짧게 트랜잭션과 연결을 유지하며, 즉시 커밋 또는 롤백하고, 응답이 느린 외부 호출 중에는 연결을 점유하지 않도록 해야 합니다. PDO 영속 연결(persistent connection)은 연결 오버헤드를 줄일 수 있지만, FPM 프로세스마다 많은 유휴 연결을 고정시키고 세션 상태를 유지할 수 있으므로 주의해서 사용해야 합니다. 애플리케이션 호환성이 확보된다면 PgBouncer와 같은 풀러나 프록시를 도입해 다수의 PHP 클라이언트를 적은 수의 DB 서버 연결로 다중화할 수 있습니다. 장시간 실행되는 워커의 경우 연결 수명주기를 명시적으로 관리해야 합니다. 즉, 헬스 체크를 수행하고 유휴 타임아웃, 프로세스 재시작, 장애 조치(failover), fork 이후 재연결을 처리하며, 세션 상태를 초기화하고 안전하거나 멱등성이 보장된 작업만 재시도하도록 구성해야 합니다.

Connection-budget example:
- 6 application servers
- 2 PHP-FPM pools on EACH server
- pm.max_children = 40 for EACH pool
- Assume at most 1 database connection per child

Potential request connections = 6 * 2 * 40 = 480.
If 40 were the TOTAL children per server across both pools, it would be 240.
Budget separately for queue workers, cron, deployments and administrators.
A limit of 300 database connections cannot accommodate 480 simultaneous connections.
AI 코치와 함께 이 질문에 답해 보세요

14PHP에서 읽기 복제본, 읽기·쓰기 분리, 복제 지연, 기본 데이터베이스 장애 조치를 어떻게 처리하나요?

SQL이 SELECT로 시작하는지만 보지 말고 작업의 의미에 따라 기본 DB와 복제본 연결을 나눕니다. 잠금을 거는 읽기, 쓰기, 기본 DB의 일관성이 필요한 트랜잭션은 기본 DB를 사용해야 합니다. 읽기 전용 트랜잭션은 일관성 요구가 허용하면 적합한 복제본을 사용할 수 있습니다. 비동기 복제에는 지연이 있을 수 있습니다. Laravel의 sticky는 현재 요청에서 쓰기 이후의 읽기를 쓰기 연결로 보냅니다. 요청 간에 일정 시간 기본 DB에 고정하는 방식은 완화책일 뿐, 자신의 쓰기를 읽는다는 무조건적 보장은 아닙니다. 보장하려면 기본 DB를 읽거나 복제본이 필요한 쓰기 위치를 반영할 때까지 기다리고, 시간 초과 시 기본 DB로 전환합니다. 프록시 동작은 다릅니다. RDS Proxy는 읽기·쓰기별 엔드포인트를 쓰며 각 SQL 문을 일반적으로 자동 분리하는 도구가 아닙니다. 장애 조치 시 오래된 연결을 버리고 상한이 있는 백오프로 재연결합니다. commit 응답을 잃으면 결과가 불분명하므로 비멱등 쓰기를 무작정 반복하지 말고 멱등성 키를 사용하거나 먼저 결과를 확인합니다.

<?php
// config/database.php
return [
    'mysql' => [
        'read' => [
            'host' => [env('DB_REPLICA_HOST_1'), env('DB_REPLICA_HOST_2')],
        ],
        'write' => [
            'host' => [env('DB_PRIMARY_HOST')],
        ],
        'sticky'    => true,
        'driver'    => 'mysql',
        'database'  => env('DB_DATABASE'),
        'username'  => env('DB_USERNAME'),
        'password'  => env('DB_PASSWORD'),
        'charset'   => 'utf8mb4',
        'collation' => 'utf8mb4_unicode_ci',
    ],
];
AI 코치와 함께 이 질문에 답해 보세요

15PHP 애플리케이션에서 DB 마이그레이션과 스키마 변경을 무중단 또는 최소 중단으로 안전하게 배포하려면 어떻게 하나요?

점진적 배포 중 구버전과 신버전이 함께 동작하도록 확장 후 축소하는 변경을 사용합니다. 먼저 호환되는 구조를 추가하고 두 표현을 모두 처리하는 코드를 배포합니다. 이중 쓰기가 필요하면 일관성을 유지하고, 더 최신 쓰기를 덮어쓰지 않는 재개 가능하고 속도 제한된 기존 데이터 보충을 설계합니다. 복사된 데이터를 검증하고 읽기를 전환한 뒤 이전 코드 경로를 제거합니다. 온라인 DDL(데이터 정의 언어)은 일부 블로킹 위험을 줄이지만 항상 잠금이 없는 것은 아닙니다. DB와 버전별 잠금, 트랜잭션 제한, 자원 사용량, 복제 영향을 확인합니다. 예를 들어 PostgreSQL의 CREATE INDEX CONCURRENTLY에는 제한이 있으며 실패하면 유효하지 않은 인덱스를 남길 수 있습니다. 구버전 인스턴스가 사라지고 필요한 롤백 기간이 지난 뒤에만 이전 열을 제거합니다. 파괴적인 축소 후에는 삭제된 데이터를 요구하는 코드로 돌아가는 것이 자동으로 안전하지 않습니다.

<?php
// Step 1: Migration (Expand) -> Add new column as nullable
Schema::table('users', function (Blueprint $table) {
    $table->string('full_name')->nullable()->after('name');
});

// Step 2: Code deployment -> Write to both, read with fallback
class User extends Model {
    public function setNameAttribute($value) {
        $this->attributes['name'] = $value;
        $this->attributes['full_name'] = $value; // Dual write
    }
}

// Step 3: Background worker backfills existing rows in small batches
// Step 4: Code deployment -> Read/write exclusively from 'full_name'
// Step 5: Migration (Contract) -> Drop legacy 'name' column
AI 코치와 함께 이 질문에 답해 보세요