개발·프레임워크

BUILD / 2번째 글

Claude Code: 터미널에서 만나는 AI 소프트웨어 엔지니어

에이전트가 도는 한 사이클부터 CLAUDE.md에 무엇이 듣는가, 권한 설계와 서브 에이전트를 나누는 기준, MCP·Hooks·CI 실행까지 Claude Code를 실무 기준으로 정리합니다.

PALDYN Team30 MIN READ

지난 글에서는 IDE 안에 들어온 AI 둘, GitHub Copilot과 Cursor를 완성·채팅·멀티파일 편집·컨텍스트·규칙 파일의 자리마다 나란히 놓고 봤다. 이번엔 그 편집기 밖으로 걸어 나온 도구, Anthropic이 만든 CLI 에이전트 Claude Code를 다룬다.

가장 큰 차이는 인터페이스가 아니라 누가 확인하는가다. 자동완성은 제안을 내놓고 사람이 받아들일지 정하지만, 에이전트는 스스로 파일을 고치고 테스트를 돌려 결과를 읽고 다시 고친다. 사람은 그 되풀이가 끝난 뒤에 결과를 본다. 그래서 잘 쓰는 법의 대부분은 프롬프트를 잘 쓰는 것이 아니라 되풀이가 어디까지 가게 두고 어디서 멈추게 할 것인가를 정하는 일이다. 아래 순서는 그 기준을 하나씩 세우는 흐름이다.

에이전트 한 사이클

Claude Code 기능 개요

도구를 부르는 되풀이

Claude Code가 하는 일은 한 문장으로 적으면 「도구를 부르고 결과를 읽는 것을 끝날 때까지 되풀이하는 것」이다. 도구는 네 갈래로 나뉜다. 파일을 읽고 쓰는 것, 셸 명령을 실행하는 것, 웹에서 문서를 가져오는 것, 그리고 MCP로 붙인 외부 서비스다.

에이전트가 도는 한 사이클

한 사이클은 이렇게 돈다. 요청을 읽고 무엇을 해야 할지 세우고, 필요한 파일을 찾아 읽고, 고치고, 테스트나 린터를 돌리고, 그 출력을 읽고, 실패했으면 원인을 짚어 다시 고친다. 중요한 것은 모델이 자기 변경의 결과를 직접 본다는 점이다. 테스트가 실패했다는 사실이 다음 판단의 입력으로 들어가므로, 한 번에 맞히지 못해도 몇 바퀴 안에 수렴하는 경우가 많다.

npm install -g @anthropic-ai/claude-code

claude                       # 대화형 세션
claude -p "main.py의 실패하는 테스트를 찾아 고쳐줘"   # 한 번 실행하고 끝

git diff HEAD~1 | claude -p "이 변경으로 커밋 메시지를 써줘"

검증 수단이 결과를 정한다

이 구조에서 곧바로 따라 나오는 결론이 있다. 에이전트가 스스로 확인할 수 있는 작업일수록 잘 된다. 테스트가 있는 코드의 버그 수정, 타입 오류 제거, 린터 경고 정리는 성공 여부를 모델이 직접 볼 수 있어 되풀이가 제대로 돈다. 반대로 「코드를 읽기 좋게 리팩터링해 줘」처럼 성공을 잴 방법이 없는 요청은 되풀이할 근거가 없어서 한 번에 내놓은 것이 곧 결과다.

그래서 요청을 적을 때 가장 값을 하는 한 문장은 검증 방법을 함께 주는 것이다. 「고친 뒤 pytest tests/ -q를 돌려 통과하는지 확인해 줘」 한 줄이 결과 품질을 크게 바꾼다. 재현 절차가 있는 버그라면 그 절차를 그대로 적어 주는 것이 상세한 원인 설명보다 낫다.

멈추는 자리를 정하기

되풀이는 무한하지 않다. 테스트가 통과했거나, 더 할 일이 없다고 판단했거나, 사람의 승인이 필요한 도구를 만나면 멈춘다. 마지막 것이 설계 대상이다 — 무엇을 승인 대상으로 둘지가 곧 에이전트를 어디까지 풀어 둘지를 정한다.

한 가지 더 정해 두면 좋은 것이 작업 크기다. 한 번에 요청하는 일이 클수록 중간 판단이 쌓여 방향이 어긋날 여지가 커지고, 어긋난 것을 알아채는 시점도 늦어진다. 실패하면 되돌리기 쉬운 크기로 끊어 요청하고, 커밋을 자주 남겨 되돌릴 지점을 만들어 두는 편이 결국 빠르다.

되풀이가 제자리를 맴도는 경우도 있다. 같은 오류를 두고 고쳤다가 되돌리는 것을 반복하면, 대개 문제가 보이는 자리에 있지 않다는 뜻이다. 이때 더 설명을 덧붙이는 것보다 범위를 좁혀 다시 시키는 편이 낫다 — 실패하는 테스트 하나만 남기고 그것만 통과시키라고 하거나, 원인을 찾는 것과 고치는 것을 두 번으로 나눠 요청한다.

컨텍스트

세션이 길어질 때

대화가 길어지면 앞쪽 내용이 요약되거나 밀려난다. 겉으로는 그대로 이어지는 것처럼 보이지만, 세션 초반에 합의한 설계 방침이나 「이 파일은 건드리지 말라」 같은 단서가 흐려질 수 있다. 한참 잘 되다가 갑자기 앞서 정한 것과 어긋난 방향으로 가면 대개 이 자리다.

대응은 둘이다. 오래 유지해야 하는 것은 대화가 아니라 파일에 적어 둔다 — CLAUDE.md든 작업용 메모 파일이든 다시 읽힐 수 있는 곳이면 된다. 그리고 주제가 바뀔 때는 세션을 새로 시작한다. 앞의 맥락이 필요 없어진 시점부터는 남아 있는 것이 도움이 아니라 방해다.

무엇이 자리를 차지하는가

컨텍스트를 가장 빨리 채우는 것은 대화가 아니라 도구가 돌려주는 출력이다. 큰 파일을 통째로 읽거나, 테스트 로그 수천 줄을 그대로 받거나, 빌드 출력을 전부 읽으면 그 한 번으로 상당 부분이 찬다.

그래서 시키는 방식이 효율을 바꾼다. 로그 전체를 읽히는 대신 실패한 부분만 뽑아 보게 하고, 큰 파일은 해당 부분만 찾아 읽게 하는 편이 낫다. 앞에서 말한 서브 에이전트가 값을 하는 이유도 같다 — 긴 탐색 과정을 밖에서 돌리고 결론만 받아 온다.

작업을 끊는 단위

이 둘을 합치면 실무 규칙이 하나 나온다. 한 세션은 한 가지 일이다. 기능 하나를 구현하고 테스트를 통과시켰으면 거기서 커밋하고 끊는다. 이어서 다른 기능을 같은 세션에서 하면 앞 작업의 파일과 로그가 계속 남아 자리를 차지하면서도 새 작업에는 쓸모가 없다.

반대로 끊지 말아야 할 자리도 있다. 원인을 찾는 중이라면 그 과정에서 확인한 것들이 전부 단서이므로, 중간에 새로 시작하면 같은 탐색을 처음부터 다시 한다.

CLAUDE.md

Claude Code 명령 패턴

무엇을 적는가

CLAUDE.md는 세션이 시작될 때 자동으로 읽히는 파일이다. 매번 설명하지 않아도 되는 자리이므로, 여기에 적을 것과 적지 말아야 할 것을 가르는 기준이 필요하다.

기준은 하나다. 코드를 읽어서 알 수 없는 것만 적는다. 디렉터리 구조나 어떤 프레임워크를 쓰는지는 파일 몇 개만 열어 보면 알 수 있어 적어 둘 값이 작다. 값을 하는 것은 그 반대쪽이다.

  • 빌드·테스트·린트를 돌리는 정확한 명령 — 프로젝트마다 다르고 추측하면 틀린다
  • 하면 안 되는 일 — 마이그레이션 파일 직접 수정, 특정 디렉터리 건드리기
  • 과거에 밟은 함정 — 왜 그렇게 되어 있는지, 되돌리면 무엇이 깨지는지
  • 관례가 아닌 규칙 — 이 프로젝트만의 계층 분리, 예외 처리 방식
# PayService API

## 명령
- 테스트: `uv run pytest tests/ -v`
- 린트: `uv run ruff check . && uv run mypy app/`

## 규칙
- migrations/ 직접 수정 금지 — alembic으로만 만든다
- DB 접근은 repositories/ 계층을 통해서만
- 환경변수는 app/config.py의 Settings로만 읽는다

길이와 구체성

이 파일은 매 세션 컨텍스트에 들어가므로 길이가 곧 비용이다. 그런데 더 큰 문제는 비용이 아니라 길어질수록 개별 규칙이 덜 지켜진다는 점이다. 수십 줄짜리 목록 한가운데 있는 항목은 다섯 줄짜리 목록의 항목보다 묻히기 쉽다.

그래서 규칙을 추가할 때마다 기존 항목을 한 번 훑고 죽은 것을 지우는 편이 낫다. 그리고 구체성이 길이보다 중요하다. 「좋은 코드를 작성하세요」는 아무것도 바꾸지 않지만 「모든 함수에 타입 힌트를 단다」는 확인 가능한 지시다. 확인할 수 있게 적힌 규칙만 실제로 듣는다고 보면 대체로 맞다.

안 지켜질 때 볼 것

규칙이 무시되는 것 같으면 순서대로 확인한다. 첫째, 파일이 실제로 읽히는 위치에 있는지다. 저장소 루트에 두는 것이 기본이고, 하위 디렉터리에 둔 파일은 그쪽 파일을 다룰 때 함께 읽힌다.

둘째, 규칙이 확인 가능한 형태인지다. 앞에서 말한 그 기준이다. 셋째, 규칙끼리 부딪히지 않는지다. 「주석은 최소로」와 「모든 함수에 설명을 단다」가 같은 파일에 있으면 어느 쪽이 이겨도 어긴 것이 된다. 넷째, 대화에서 준 지시가 파일의 규칙과 다른지다 — 이때는 대화 쪽이 이기는 것이 정상 동작이므로 파일을 고칠 것이 아니라 요청을 고쳐야 한다.

지켜지길 바라는 것이 아니라 반드시 지켜져야 하는 것이라면 문서가 아니라 뒤에서 말할 Hooks나 권한 설정으로 내려야 한다. 문서는 유도하는 장치이지 강제하는 장치가 아니다.

이 파일을 키우는 가장 좋은 방법은 한 번에 잘 쓰려는 것이 아니라 어긋날 때마다 한 줄씩 보태는 것이다. 같은 지적을 두 번 하게 됐다면 그것이 적어 둘 항목이다. 반대로 한 번밖에 안 나온 일은 그냥 그때 말하면 되고, 미리 적어 두면 목록만 길어진다.

권한

허용과 거부

Claude Code는 파일을 고치거나 셸 명령을 돌리기 전에 승인을 요청한다. 매번 묻는 것은 번거롭고 매번 허용하는 것은 위험하므로, 자주 쓰는 안전한 명령을 미리 허용해 두는 것이 실제 설정이다.

{
  "permissions": {
    "allow": [
      "Bash(npm run test:*)",
      "Bash(pytest:*)",
      "Bash(ruff:*)",
      "Edit(src/**)"
    ],
    "deny": [
      "Bash(rm -rf:*)",
      "Bash(git push --force:*)",
      "Read(.env)"
    ]
  }
}

가르는 기준은 되돌릴 수 있는가다. 테스트 실행, 린터, 타입 검사, 빌드는 실패해도 파일 시스템이 원래대로이므로 허용해도 잃을 것이 없다. 반대쪽은 되돌릴 수 없는 것들이다 — 원격 저장소에 강제로 밀어 넣는 것, 파일을 지우는 것, 배포 명령, 운영 데이터베이스에 쓰는 것. 이 목록은 거부에 명시해 두는 편이 안전하다.

읽기 쪽도 살펴야 한다. 비밀값이 든 파일이나 자격 증명 디렉터리를 읽기 금지에 넣어 두면, 그 내용이 실수로 컨텍스트에 들어가는 일을 막을 수 있다.

자동 승인을 켜도 되는 조건

승인을 통째로 건너뛰는 모드가 있지만, 켜도 되는 조건이 분명하다. 잘못돼도 잃을 것이 없는 환경이어야 한다. 격리된 컨테이너나 일회용 작업 공간, 커밋되지 않은 변경이 없는 상태, 원격에 밀어 넣는 권한이 없는 환경이 그 조건이다.

반대로 켜면 안 되는 자리도 분명하다. 운영 자격 증명이 환경변수에 들어 있는 셸, 커밋 안 한 작업이 남아 있는 저장소, 여러 프로젝트가 한 디렉터리 아래 있는 구조다. 파일 편집만 자동 승인하고 셸 명령은 계속 묻는 중간 단계도 있으니, 전부 아니면 전무로 생각할 필요는 없다.

위험은 삭제가 아니라 유출

권한을 설계할 때 대개 파괴적인 명령을 먼저 떠올리지만, 실제로 더 자주 문제가 되는 것은 밖으로 나가는 것이다. 에이전트는 웹에서 문서를 가져오고 외부 서비스에 요청을 보낼 수 있으므로, 읽은 내용이 나가는 경로가 존재한다.

그래서 저장소에 비밀값을 두지 않는 기본 원칙이 여기서 두 배로 중요해진다. 환경변수로 넘기는 값도 안전하지 않다 — 셸 명령을 실행할 수 있으면 환경변수도 읽을 수 있다. 운영 자격 증명이 든 셸에서는 애초에 세션을 열지 않는 것이 유일하게 확실한 방법이다.

그리고 외부에서 읽어 온 내용 — 이슈 본문, 웹 문서, 로그 — 에 지시처럼 보이는 문장이 섞여 있을 수 있다는 점도 염두에 둔다. 사람이 쓴 요청과 도구가 가져온 데이터는 다른 무게로 다뤄야 하고, 가져온 내용이 작업 방향을 바꾸려 들면 그때는 사람이 한 번 봐야 한다.

서브 에이전트

나누면 빨라지는 작업

서브 에이전트는 별도의 컨텍스트를 가진 작업 단위다. 본 세션이 작업을 넘기면 그쪽이 혼자 수행하고 결과 요약만 돌려준다. 이 「요약만 돌려준다」가 핵심이다.

그래서 값을 하는 자리는 둘이다. 하나는 서로 독립적이라 병렬로 돌려도 되는 작업이고, 다른 하나는 중간 과정이 길지만 결론이 짧은 작업이다. 저장소 전체를 뒤져 특정 패턴을 찾는 일이 대표적이다. 파일 수십 개를 열어 보는 과정이 전부 본 세션 컨텍스트에 쌓이면 정작 중요한 코드가 밀려나는데, 서브 에이전트에 맡기면 「이 세 곳에 있다」는 결론만 돌아온다.

작업 유형별로 정의를 파일에 남겨 둘 수도 있다. 저장소의 .claude/agents/ 아래에 두면 그 작업이 필요할 때마다 같은 설정으로 불린다.

---
name: test-runner
description: 테스트를 돌리고 실패 원인만 요약해 돌려준다
tools: Read, Grep, Bash
---

테스트를 실행하고, 실패한 케이스마다 파일·줄 번호와 원인을
한 줄로 요약한다. 코드를 고치지는 않는다.

컨텍스트를 다시 전달하는 비용

나누면 언제나 빨라질 것 같지만 그렇지 않다. 서브 에이전트는 본 세션이 지금까지 알아낸 것을 모른 채 시작하므로, 필요한 배경을 전부 다시 받아야 한다. 이 전달 비용이 작업 자체보다 크면 나누는 것이 손해다.

그리고 결과가 요약으로 돌아온다는 성질이 반대로 작용하는 자리가 있다. 세부가 중요한 작업, 이를테면 미묘한 버그의 원인을 찾는 일은 요약되는 과정에서 정작 필요한 단서가 빠질 수 있다. 이런 일은 본 세션에서 직접 하는 편이 낫다.

판단 기준은 셋이다 — 작업이 서로 독립적인가, 배경 설명이 짧게 끝나는가, 결론만 받아도 되는가. 셋 다 그렇다면 나누고, 하나라도 아니면 그냥 이어서 한다.

나누면 안 되는 자리

같은 파일을 여러 갈래가 동시에 고치게 하는 것은 피해야 한다. 서로의 변경을 못 보고 각자 고치므로 나중에 덮어쓰기가 난다. 순서가 있는 작업도 마찬가지다 — 스키마를 바꾸고 그에 맞춰 코드를 고치는 일은 병렬로 나눌 수 없다.

판단이 애매하면 읽기만 하는 작업인지를 보면 대개 갈린다. 탐색·조사·검토처럼 파일을 읽고 결론만 내는 일은 여럿으로 나눠도 충돌할 것이 없다. 반대로 고치는 작업을 나눌 때는 각자 다른 파일만 만지도록 범위를 명시해 주어야 하고, 그렇게 나눌 수 없으면 애초에 나눌 작업이 아니다.

MCP

붙이는 절차

MCP는 외부 서비스를 에이전트의 도구로 노출하는 규약이다. 서버를 하나 붙이면 그 서비스의 기능이 도구 목록에 추가되어, 저장소의 이슈를 읽거나 데이터베이스를 조회하는 일을 코드를 짜지 않고 시킬 수 있다.

붙이는 방법은 둘이다. CLI로 추가하면 개인 설정에 들어가고, 저장소 루트의 .mcp.json에 적으면 그 저장소를 쓰는 사람 모두가 공유한다. 팀이 함께 쓰는 서버는 후자가 맞다.

{
  "mcpServers": {
    "github": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-github"],
      "env": { "GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_TOKEN}" }
    }
  }
}

토큰을 이 파일에 그대로 적지 않는 것이 중요하다. 공유되는 파일이므로 커밋에 그대로 실린다. 환경변수를 참조하게 두고 값은 각자의 셸에 둔다.

도구가 늘어나는 값과 대가

서버를 붙일수록 할 수 있는 일이 늘지만 대가가 둘 있다. 하나는 컨텍스트다. 도구 정의가 매 요청에 실리므로 서버를 여럿 붙이면 그만큼 자리를 차지한다. 다른 하나는 선택 정확도다 — 비슷한 이름의 도구가 여럿이면 엉뚱한 것이 불릴 여지가 생긴다. 저장소 조회 도구와 이슈 검색 도구를 각각 제공하는 서버 둘을 함께 붙이면 실제로 그런 일이 난다.

그래서 지금 하는 작업에 쓰는 서버만 켜 두는 것이 실제 운영 방식이다. 전부 붙여 두고 쓰는 것보다 필요할 때 붙이는 쪽이 낫다.

붙일지 말지를 가르는 질문도 하나 있다. 그 일을 셸 명령 한 줄로 할 수 있다면 서버는 필요 없다. 이미 CLI가 있는 서비스는 그 CLI를 허용 목록에 넣는 편이 가볍고, 도구 정의가 컨텍스트를 차지하지도 않는다. MCP가 값을 하는 자리는 CLI로 풀기 번거로운 것 — 여러 번 호출해 결과를 이어 붙여야 하거나, 인증이 복잡하거나, 응답을 구조화된 형태로 받아야 하는 경우다.

연결이 안 될 때

MCP 서버가 안 붙는 원인은 대체로 셋 안에 있다. 첫째, 실행 명령 자체가 그 환경에서 안 도는 경우다. 설정에 적힌 명령을 터미널에서 직접 실행해 보면 바로 드러난다. 둘째, 인증 정보가 비어 있는 경우다 — 환경변수 이름을 틀리면 빈 값이 넘어가고 서버는 기동했다가 첫 요청에서 실패한다. 셋째, 서버 프로세스가 표준 출력에 로그를 섞어 쓰는 경우다. 그 통로로 프로토콜 메시지가 오가므로 로그가 섞이면 통신이 깨진다. 직접 만든 서버라면 로그는 표준 오류로 보내야 한다.

Hooks와 CI

Hooks가 붙는 자리

Hooks는 정해진 시점에 셸 명령을 실행하는 설정이다. 도구를 쓰기 전과 쓴 뒤, 세션이 끝날 때 같은 지점에 붙일 수 있다. 문서로 적은 규칙과 달리 반드시 실행된다는 것이 차이다.

{
  "hooks": {
    "PostToolUse": [{
      "matcher": "Edit|Write",
      "hooks": [{
        "type": "command",
        "command": "jq -r '.tool_input.file_path' | xargs -r ruff check --fix"
      }]
    }]
  }
}

훅 명령은 이벤트 정보를 표준 입력으로 JSON 형태로 받는다. 편집된 파일 경로처럼 필요한 값은 거기서 꺼내 쓴다. 문자열 치환으로 경로가 끼워질 것이라고 가정하고 적으면 빈 값으로 돌아 아무 일도 안 하거나 엉뚱한 대상에 적용된다.

얻는 것과 느려지는 지점

값을 하는 자리는 분명하다. 편집 뒤 포매터를 돌려 두면 스타일 지적이 되풀이되지 않고, 세션 끝에 테스트를 걸어 두면 깨진 채로 끝나는 일이 없다. 효과가 겹쳐 나는 것도 있다 — 편집 직후 린터가 돌면 그 출력이 에이전트에게 바로 돌아가므로, 사람이 지적하기 전에 스스로 고친다. 규칙을 문서에 적는 것보다 훅으로 거는 쪽이 확실한 이유가 여기 있다.

대가는 시간이다. 훅은 매번 돌고 그동안 세션이 기다린다. 편집마다 전체 테스트를 거는 설정은 몇 초짜리 작업을 몇 분으로 만든다. 그래서 편집 단위에는 빠른 것만, 느린 것은 세션 끝에 두는 배치가 맞다. 포매터와 린터는 앞쪽, 전체 테스트와 빌드는 뒤쪽이다.

CI에서 돌릴 때

CI에서는 사람이 승인할 수 없으므로 권한 설계가 미리 끝나 있어야 한다. 그리고 그 환경은 자동 승인의 조건에 대체로 들어맞는다 — 일회용이고, 격리돼 있고, 원래 상태가 저장소 클론뿐이다.

name: AI Code Review
on:
  pull_request:
    types: [opened, synchronize]

jobs:
  review:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      pull-requests: write
    steps:
      - uses: actions/checkout@v4
      - uses: anthropics/claude-code-action@v1
        with:
          anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
          prompt: |
            이 PR의 변경사항만 검토해 버그와 보안 문제를 지적하고,
            확실한 것만 PR 코멘트로 남겨줘.

액션의 버전 태그는 지금 배포 중인 것으로 맞춘다. 그리고 워크플로 쪽 권한은 필요한 만큼만 준다 — 위 예시는 코드를 읽고 PR에 댓글을 다는 것까지만 할 수 있다.

한 가지 더 다른 점은 요청을 사람이 다듬어 줄 수 없다는 것이다. 대화형 세션에서는 방향이 어긋나면 그 자리에서 고쳐 말하면 되지만, CI에서는 처음 준 프롬프트가 전부다. 그래서 범위를 좁게 못 박는 것이 대화형보다 훨씬 중요하다 — 「이 PR에서 바뀐 파일만」, 「확실한 것만」처럼 판단의 경계를 문장에 넣어 두지 않으면, 저장소 전체를 훑으며 온갖 것을 지적하는 결과가 돌아온다.

CI에서 특히 정해 두어야 할 것이 실패 처리다. 에이전트가 결론을 못 내거나 도중에 멈췄을 때 그 잡을 실패로 볼지 넘길지를 미리 정해야 한다. 검토처럼 참고용인 작업은 실패해도 병합을 막지 않는 편이 낫고, 반대로 생성한 코드를 커밋하는 작업이라면 실패를 막아야 한다. 어느 쪽이든 출력을 로그에 남겨 두어야 나중에 왜 그렇게 판단했는지 볼 수 있다.


읽어주셔서 감사합니다. 😊

LATEST

개발·프레임워크의 최신 글

개발·프레임워크2026.05.28

Google Gemini SDK 활용 가이드

google-genai 패키지의 Client 하나로 Gemini API를 부르는 법 — 응답 객체와 finish_reason, 생성 설정과 구조화 출력, 인라인 데이터와 Files API, 도구 호출 왕복, 안전 설정과 재시도, 대화 비용과 컨텍스트 캐싱까지 정리한다.

26 MIN
개발·프레임워크2026.05.28

OpenAI SDK 완전 정복

Python openai 패키지로 OpenAI API를 다루는 법 — 클라이언트 설정과 재시도, 메시지와 응답 구조, Responses API 대응, 모델 고르는 축, 도구 호출 루프, 구조화 출력, 임베딩·이미지 입력, 토큰 비용과 한도까지 정리

30 MIN