> This page is for 플랫폼, version V4 (default).
> For other versions, use one of these documentation indexes:
> - V4 (default): https://next.developer.frame.io/platform/v4/llms.txt
> - V4 실험적: https://next.developer.frame.io/platform/v4-experimental/llms.txt
> - 레거시: https://next.developer.frame.io/platform/v2/llms.txt

> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://next.developer.frame.io/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://next.developer.frame.io/_mcp/server.

# Frame.io Python SDK — 인증 가이드

이 가이드는 **Frame.io Python SDK**(`frameio`)를 사용하여 Frame.io API로 인증하는 방법을 설명합니다. Frame.io V4 API는 Adobe의 OAuth 2.0 ID 플랫폼인 [Adobe IMS(Identity Management Service)](https://developer.adobe.com/developer-console/docs/guides/authentication/)를 사용합니다. 이 문서는 Python 개발자를 위한 독립형 참조 문서입니다. 아래의 모든 코드 예제와 흐름은 `frameio` 패키지에만 해당됩니다.

---

## Python SDK의 인증 유형

Python SDK는 4가지 인증 옵션을 지원합니다.

| 방법                                                                                                                                                                                                                                                                                                                                                                                                                                 | 사용 사례                          | 사용자 상호 작용? | 클라이언트 시크릿 필요? |
| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------ | ---------- | ------------- |
| **정적 토큰**                                                                                                                                                                                                                                                                                                                                                                                                                          | 빠른 스크립트, 테스트 또는 이미 토큰이 있는 경우   | 아니요        | 아니요           |
| **서버 간 인증**                                                                                                                                                                                                                                                                                                                                                                                                                        | 백엔드 서비스, cron 작업, 자동화          | 아니요        | 예             |
| **웹 앱**                                                                                                                                                                                                                                                                                                                                                                                                                            | 서버측 앱(Flask, Django, FastAPI)  | 예          | 예             |
| **SPA(PKCE)**                                                                                                                                                                                                                                                                                                                                                                                                                      | 브라우저 앱, CLI 또는 시크릿을 저장할 수 없는 앱 | 예          | 아니요           |
| **서버 간 인증**을 사용하면 앱이 사용자 상호 작용 없이 서비스 계정으로 작동할 수 있습니다. [Adobe Admin Console](https://adminconsole.adobe.com/)을 통해 관리되는 Frame.io V4 계정에서만 사용할 수 있습니다. **웹 앱** 및 **SPA**를 사용하면 앱이 특정 사용자 역할을 수행할 수 있습니다. 둘 다 내부적으로 Adobe IMS를 사용합니다. 사용자가 앱을 인증하면 SDK가 결과 코드를 토큰으로 교환합니다. Python SDK는 IMS `/authorize/v2` 및 `/token/v3` 흐름을 처리합니다. 웹 앱의 경우 클라이언트 시크릿이 필요하고, SPA의 경우 [PKCE](https://datatracker.ietf.org/doc/html/rfc7636)를 대신 사용합니다. |                                |            |               |

> **Note**
>
> Adobe의 [네이티브 앱 자격 증명](https://developer.adobe.com/developer-console/docs/guides/authentication/UserAuthentication/implementation/#oauth-native-app-credential)은 사용자 지정 URI 스키마 핸들러(예: `adobe+<hash>://…</hash>`)가 OS 레벨에서 리디렉션을 가로막도록 요구합니다. Python은 이러한 핸들러를 등록하는 표준 방법이 없으므로, Python SDK는 `NativeAppAuth` 클래스를 제공하지 않습니다. 사용자 대화형 Python 앱의 경우 로컬 콜백 서버(예: Flask 또는 FastAPI)와 함께 `WebAppAuth`를 사용하세요. 대화형이 아닌 워크로드의 경우 `ServerToServerAuth`를 사용하세요.

---

## 서비스 계정 사용자

서버 간 인증을 사용할 때 애플리케이션은 **서비스 계정 사용자** 역할을 합니다. 이는 서비스를 대신하여 작업을 수행할 수 있는 고유한 계정 유형입니다. 이 계정은 Frame.io의 다른 사용자에게 표시됩니다. 서비스 계정이 작업을 수행하면 UI에 해당 이름이 표시됩니다. [Adobe Admin Console](https://adminconsole.adobe.com/) 및 [Developer Console](https://developer.adobe.com/console)을 통해 서비스 계정 액세스 권한을 부여하고 취소할 수 있습니다. 서비스 계정 이름은 Frame.io UI에서 관리됩니다. 기본적으로 첫 번째 S2S 연결의 이름은 **Service Account User**, 두 번째는 **Service Account User 2** 등으로 지정됩니다.

> **Info**
>
> 자세한 내용은 [Frame.io 서버 간 지원을 사용하여 설정 자동화하기](https://helpx.adobe.com/enterprise/using/automate-using-frame-io.html)를 참조하세요.

---

## 빠른 시작

### 사전 요구 사항

1. [Adobe Developer Console](https://developer.adobe.com/console)의 **자격 증명**:

* **클라이언트 ID** — 모든 OAuth 흐름에 필요 - **클라이언트 시크릿** — 서버 간 및 웹 앱 흐름에 필요 - **리디렉션 URI** — 웹 앱 및 SPA 흐름에 필요하며, Adobe 프로젝트에 등록되어야 함

2. **SDK 설치:**

```bash
pip install frameio
```

### 방법 선택

* **사용자가 개입하지 않습니까?** **서버 간**(`ServerToServerAuth`) 방식을 사용하세요.
* **사용자가 개입하며 시크릿을 저장할 수 있습니까?** **웹 앱**(`WebAppAuth`) 방식을 사용하세요.
* **사용자가 개입하지만 시크릿을 저장할 수 없습니까?** **SPA**(`SPAAuth`) 방식을 사용하세요.

---

## 액세스 토큰

(다른 OAuth 시스템 또는 이전 교환(예: [API 탐색기](/platform/api-reference/accounts/index?explorer=true))에서 발급받은) 액세스 토큰이 이미 있는 경우 이를 직접 전달할 수 있습니다.

```python
from frameio import Frameio

client = Frameio(token="YOUR_ACCESS_TOKEN")
```

이것이 가장 간단한 방식이지만, 토큰은 결국 만료되며 SDK가 이를 대신 새로 고쳐주지 않습니다.

### 이전 개발자 토큰

아직 [Adobe Admin Console](https://adminconsole.adobe.com/)을 통해 관리되지 않는 V4 마이그레이션 계정의 경우, [Frame.io 개발자 사이트](https://developer.frame.io/app/tokens)에서 발급받은 이전 개발자 토큰을 계속 사용할 수 있습니다. 반드시 `x-frameio-legacy-token-auth` 헤더를 포함하고 이를 `true`로 설정해야 합니다.

```python
from frameio import Frameio

client = Frameio(
    token="YOUR_LEGACY_DEVELOPER_TOKEN",
    headers={"x-frameio-legacy-token-auth": "true"},
)
```

이전 개발자 토큰은 만료되지 않지만 이는 임시적인 전환 메커니즘입니다. 새로운 통합 및 프로덕션 워크로드의 경우 아래의 OAuth 2.0 흐름 중 하나를 사용하는 것이 좋습니다. 자세한 내용은 [마이그레이션 가이드](/platform/docs/resources/migration#authentication)를 참조하세요.

---

## 서버 간 방식(클라이언트 자격 증명)

사용자 상호 작용 없이 Frame.io 액세스가 필요한 백엔드 서비스 및 스크립트에 이 방식을 사용하세요. 이 흐름은 [Adobe Admin Console](https://adminconsole.adobe.com/)을 통해 관리되는 Frame.io V4 계정에서만 사용할 수 있습니다. 애플리케이션은 사용자 개입 없이 [서비스 계정 사용자](#service-account-users)로 인증됩니다.

#### Sync

```python
    from frameio import Frameio
    from frameio.auth import ServerToServerAuth

    auth = ServerToServerAuth(
        client_id="YOUR_CLIENT_ID",
        client_secret="YOUR_CLIENT_SECRET",
    )

    client = Frameio(token=auth.get_token)
```

#### Async

```python
    from frameio import AsyncFrameio
    from frameio.auth import AsyncServerToServerAuth

    auth = AsyncServerToServerAuth(
        client_id="YOUR_CLIENT_ID",
        client_secret="YOUR_CLIENT_SECRET",
    )

    client = AsyncFrameio(token=auth.get_token)
```

이것이 전부입니다. `auth.get_token`은 모든 요청에서 SDK가 호출하는 콜러블 오브젝트입니다. 현재 토큰이 여전히 유효하면 즉시 반환됩니다. 토큰이 만료될 예정이면 먼저 완벽하게 투명한 방식으로 새 토큰을 가져옵니다.

### 작동 방식

클라이언트 자격 증명(클라이언트 ID + 시크릿)은 **절대 만료되지 않습니다**. 보안 관리를 위해 수동으로만 순환시킵니다. S2S는 수동 개입 없이 사실상 영구적이고 중단 없는 API 액세스를 제공합니다.

내부 동작 방식:

1. 첫 번째 API 호출 시, `get_token`은 `client_credentials` 권한 부여를 사용하여 Adobe IMS에 새 액세스 토큰을 요청합니다.
2. 토큰은 메모리에 캐시됩니다. 개별 액세스 토큰은 만료(일반적으로 24시간)되지만 이는 자동으로 처리됩니다.
3. 캐시된 토큰이 새로 고침 버퍼(기본값: 만료 60초 전) 내에 있으면, SDK는 동일한 클라이언트 자격 증명을 사용하여 새 토큰을 자동으로 가져옵니다.
4. 새로 고침 토큰은 관여하지 않습니다. 클라이언트 자격 증명 자체가 수명이 긴 시크릿이며, 이를 통해 언제든 새 액세스 토큰을 발행할 수 있습니다.

### 명시적 인증

(예를 들어 시작 시 잘못된 자격 증명에 대해 빠르게 실패 처리를 하기 위해) 즉시 토큰을 가져오려는 경우는 다음과 같습니다.

```python
auth = ServerToServerAuth(client_id="...", client_secret="...")
auth.authenticate()  # raises AuthenticationError if credentials are invalid
client = Frameio(token=auth.get_token)
```

---

## 웹 앱(권한 부여 코드)

사용자가 자신의 Adobe ID로 로그인하는 서버 측 애플리케이션에 이 방식을 사용하세요. 이 흐름에는 클라이언트 시크릿이 필요하며, 이는 서버에 안전하게 저장되어야 합니다.

#### 사용자를 Adobe IMS로 리디렉션

#### Sync

```python
    auth.refresh()  # fetches a new access token using the refresh token
```

#### Async

```python
    await auth.refresh()
```

#### 콜백 처리

Adobe IMS가 사용자를 `redirect_uri`로 리디렉션하면 `code` 및 `state` 매개변수를 추출합니다. 저장한 내용과 state가 일치하는지 확인한 다음, 코드를 토큰으로 교환합니다.

#### Sync

```python
    auth.refresh()  # fetches a new access token using the refresh token
```

#### Async

```python
    await auth.refresh()
```

이는 권한 부여 코드를 액세스 토큰 및 새로 고침 토큰으로 교환하고 두 가지를 내부적으로 저장합니다.

#### 클라이언트 사용

#### Sync

```python
        from frameio import Frameio

        client = Frameio(token=auth.get_token)
```

#### Async

```python
        from frameio import AsyncFrameio

        client = AsyncFrameio(token=auth.get_token)
```

이것이 전부입니다. 이 시점부터 `get_token`이 토큰 수명 주기를 자동으로 관리합니다. 액세스 토큰의 만료가 임박하면 SDK는 새로 고침 토큰을 사용하여 새 토큰을 가져옵니다. 사용자 상호 작용이 필요하지 않습니다.

### 전체 Flask 예제

```python
import secrets
from flask import Flask, redirect, request, session
from frameio import Frameio
from frameio.auth import WebAppAuth

app = Flask(__name__)
app.secret_key = secrets.token_bytes(32)

auth = WebAppAuth(
    client_id="YOUR_CLIENT_ID",
    client_secret="YOUR_CLIENT_SECRET",
    redirect_uri="http://localhost:5000/callback",
)

@app.route("/login")
def login():
    state = secrets.token_urlsafe(32)
    session["oauth_state"] = state
    return redirect(auth.get_authorization_url(state=state))

@app.route("/callback")
def callback():
    if request.args.get("state") != session.pop("oauth_state", None):
        return "Invalid state parameter", 403

    auth.exchange_code(code=request.args["code"])

    client = Frameio(token=auth.get_token)
    accounts = client.accounts.index()
    return f"Authenticated — {len(accounts.data)} account(s) accessible."
```

---

## 단일 페이지 앱 / PKCE(권한 부여 코드 + PKCE)

클라이언트 시크릿을 안전하게 저장할 수 없는 브라우저 기반 애플리케이션, 데스크톱 앱, 또는 CLI 도구에 이 방식을 사용하세요. 이 흐름은 [PKCE(RFC 7636)](https://datatracker.ietf.org/doc/html/rfc7636)를 사용하여 권한 부여 코드 교환을 보호합니다.

#### 권한 부여 URL 생성

#### Sync

```python
        from frameio.auth import SPAAuth

        auth = SPAAuth(
            client_id="YOUR_CLIENT_ID",
            redirect_uri="https://yourapp.com/callback",
        )

        import secrets
        state = secrets.token_urlsafe(32)

        result = auth.get_authorization_url(state=state)
        # result.url            -> redirect the user here
        # result.code_verifier  -> store this securely until the callback
```

#### Async

```python
        from frameio.auth import AsyncSPAAuth

        auth = AsyncSPAAuth(
            client_id="YOUR_CLIENT_ID",
            redirect_uri="https://yourapp.com/callback",
        )

        # get_authorization_url is synchronous (no I/O needed)
        import secrets
        state = secrets.token_urlsafe(32)

        result = auth.get_authorization_url(state=state)
```

`get_authorization_url`은 (PKCE `code_challenge`가 임베드된) 전체 URL 및 다음 단계에서 필요한 `code_verifier`가 포함된 `AuthorizationUrlResult`를 반환합니다.

#### 검증자를 사용하여 코드 교환

사용자가 다시 리디렉션된 경우:

#### Sync

```python
        auth.exchange_code(
            code="CODE_FROM_CALLBACK",
            code_verifier=result.code_verifier,
        )
```

#### Async

```python
        await auth.exchange_code(
            code="CODE_FROM_CALLBACK",
            code_verifier=result.code_verifier,
        )
```

#### 클라이언트 사용

#### Sync

```python
        from frameio import Frameio

        client = Frameio(token=auth.get_token)
```

#### Async

```python
        from frameio import AsyncFrameio

        client = AsyncFrameio(token=auth.get_token)
```

이것이 전부입니다. 새로 고침은 웹 앱 방식과 동일하게 작동하며, SDK가 새로 고침 토큰을 자동으로 사용합니다. 차이점은 SPA 흐름이 공개 클라이언트용으로 설계되었기 때문에 새로 고침 중에 클라이언트 시크릿이 전송되지 않는다는 점입니다.

---

## Async 사용

모든 인증 클래스에는 `Async` 접두사가 붙은 비동기 대응 클래스가 있습니다. 위의 코드 예시에는 해당하는 경우 **Sync** 및 **Async** 탭이 포함되어 있습니다.

| Sync                                                                                                                                                              | Async                     |
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- |
| `ServerToServerAuth`                                                                                                                                              | `AsyncServerToServerAuth` |
| `WebAppAuth`                                                                                                                                                      | `AsyncWebAppAuth`         |
| `SPAAuth`                                                                                                                                                         | `AsyncSPAAuth`            |
| API는 동일합니다. `get_authorization_url`은 동기식(I/O 불필요)으로 유지되는 반면, `exchange_code`, `refresh`, `revoke`, `get_token`은 모두 `async`입니다. 비동기 클래스는 `AsyncFrameio`와 함께 사용하세요. |                           |

### 수동 토큰 새로 고침

웹 앱 및 SPA 흐름의 경우, SDK는 `get_token`을 통해 토큰을 자동으로 새로 고칩니다. 명시적인 제어가 필요한 경우 `refresh()`를 직접 호출할 수 있습니다.

#### Sync

```python
    auth.refresh()  # fetches a new access token using the refresh token
```

#### Async

```python
    await auth.refresh()
```

이 기능은 자동 새로 고침 버퍼에 의존하는 대신 중요한 작업을 앞두고 강제로 새로 고치고자 할 때 유용합니다.

---

## 토큰 지속성

모든 인증 클래스는 재시작 시에도 토큰 상태를 유지할 수 있도록 `export_tokens()` 및 `import_tokens()`를 지원합니다. 웹 앱 및 SPA 흐름의 경우 액세스 토큰과 새로 고침 토큰이 기본적으로 메모리에 상주하므로 이는 특히 중요합니다. 이를 유지하지 않으면 애플리케이션이 다시 시작될 때 사용자가 재인증을 해야 합니다. 서버 간(S2S) 방식의 경우 지속성은 선택 사항이지만(클라이언트 자격 증명으로 항상 새 토큰을 발행할 수 있음), 캐시된 토큰을 가져오면 시작 시 한 번의 추가 왕복을 피할 수 있습니다.

### 내보내기 및 가져오기

```python
# After exchange_code(), save the token state
token_data = auth.export_tokens()
# token_data is a dict: {"access_token": "...", "refresh_token": "...", "expires_at": 1234567890.0}
# Save it to your database, file, or secret store

# On next startup, restore it
auth.import_tokens(token_data)
client = Frameio(token=auth.get_token)
# The SDK will automatically refresh if the token is near expiry
```

### `on_token_refreshed`를 활용한 자동 지속성

토큰이 새로 고쳐질 때마다 자동으로 유지하려면 `on_token_refreshed` 콜백을 사용합니다.

```python
import json
from pathlib import Path

TOKEN_FILE = Path("tokens.json")

def save_tokens(tokens: dict):
    TOKEN_FILE.write_text(json.dumps(tokens))

auth = WebAppAuth(
    client_id="...",
    client_secret="...",
    redirect_uri="...",
    on_token_refreshed=save_tokens,
)

# On startup, restore if available
if TOKEN_FILE.exists():
    auth.import_tokens(json.loads(TOKEN_FILE.read_text()))
```

콜백은 `export_tokens()`와 동일한 사전 형태를 수신하며, 토큰 새로 고침이 성공할 때마다 실행됩니다. 비동기 클래스의 경우 `on_token_refreshed`는 일반 함수이거나 `비동기` 함수일 수 있습니다. 두 가지 모두 지원됩니다.

---

## 토큰 해지

사용자를 로그아웃하고 Adobe IMS에서 해당 토큰을 무효화하려면 다음을 실행합니다.

```python
auth.revoke()
```

이는 액세스 토큰 및 새로 고침 토큰 모두에 대해 최선의 노력으로 Adobe IMS에 해지 요청을 보낸 다음 모든 로컬 토큰 상태를 지웁니다. 해지 후에는 사용자가 다시 인증을 거쳐야 합니다.

> **Tip**
>
> 비동기 클래스의 경우 await `await auth.revoke()`를 사용합니다.

---

## 오류 처리

모든 인증 오류는 `FrameioAuthError`에서 상속되므로, 이를 포괄적으로 잡거나 특정 케이스를 분리하여 처리할 수 있습니다.

```python
from frameio.auth import (
    FrameioAuthError,
    AuthenticationError,
    TokenExpiredError,
    ConfigurationError,
    NetworkError,
    RateLimitError,
)

try:
    auth.exchange_code(code="...")
except TokenExpiredError:
    # The refresh token has expired; redirect the user to sign in again
    pass
except AuthenticationError as e:
    # Token exchange failed
    print(f"Error: {e.error_code} - {e.error_description}")
except NetworkError:
    # Timeout or connection failure (after retries)
    pass
except RateLimitError as e:
    # 429 from Adobe IMS; retry after e.retry_after seconds
    pass
except FrameioAuthError:
    # Catch-all for any other auth error
    pass
```

### 오류 참조

| 예외                    | 발생 조건                                                                     |
| --------------------- | ------------------------------------------------------------------------- |
| `ConfigurationError`  | 구성이 누락되었거나 유효하지 않음(예: 비어 있는 `client_id`, HTTPS가 아닌 리디렉션 URI)              |
| `AuthenticationError` | Adobe IMS에서 토큰 교환 또는 새로 고침 거부(`.error_code` 및 `.error_description` 속성 포함) |
| `TokenExpiredError`   | 새로 고침 토큰 자체가 만료됨. 사용자가 다시 인증해야 함                                          |
| `NetworkError`        | 모든 재시도 후에도 HTTP 시간 초과 또는 연결 실패 발생                                         |
| `RateLimitError`      | Adobe IMS에서 429를 반환함. 백오프 지침을 위해 `.retry_after` 확인 필요                     |
| `PKCEError`           | PKCE 확인 실패(SPA 흐름)                                                        |

### 프로덕션 환경에서 만료된 새로 고침 토큰 처리하기

> **Warning**
>
> 웹 앱 및 SPA 흐름의 경우 새로 고침 토큰은 결국 만료됩니다. 이 경우 `get_token`은 `TokenExpiredError`를 발생시킵니다. 이를 잡아내어 사용자를 권한 부여 흐름을 통해 다시 리디렉션해야 합니다.

```python
from frameio.auth import TokenExpiredError

try:
    client = Frameio(token=auth.get_token)
    assets = client.files.list(project_id="...")
except TokenExpiredError:
    # Clear persisted tokens and redirect user to login
    auth.revoke()
    return redirect("/login")
```

---

## 구성 참조

모든 인증 클래스는 다음 선택적 매개변수를 허용합니다.

#### 매개변수 참조

| 매개 변수                | Default                          | 설명                                                                                                                                                |
| -------------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `범위`                 | 흐름별 기본값                          | 공백으로 구분된 OAuth 권한입니다. S2S의 기본값은 `openid AdobeID frame.s2s.all`이며, 사용자 대면 흐름의 기본값은 `openid email profile offline_access additional_info.roles`입니다. |
| `ims_base_url`       | `https://ims-na1.adobelogin.com` | Adobe IMS 기본 URL입니다. 스테이징 또는 비프로덕션 환경을 위해 이를 재정의할 수 있습니다.                                                                                         |
| `http_client`        | `None`                           | 사용자 지정 `httpx.프록시, mTLS, 또는 연결 풀링을 위한 Client`(또는 `httpx.AsyncClient`)입니다.                                                                         |
| `timeout`            | `30`                             | 토큰 엔드포인트 호출에 대한 HTTP 요청 시간 제한(초 단위)입니다.                                                                                                           |
| `max_retries`        | `2`                              | 일시적인 오류(5xx, 시간 초과) 발생 시 최대 재시도 횟수입니다. 속도 제한 재시도(429)는 별도로 추적됩니다.                                                                                 |
| `refresh_buffer`     | `60`                             | 사전 새로 고침을 트리거하기 위해 토큰이 만료되기 전까지 남은 시간(초 단위)입니다.                                                                                                   |
| `on_token_refreshed` | `None`                           | 토큰 새로 고침이 성공할 때마다 실행되는 콜백입니다. `access_token`, `refresh_token`, `expires_at`이 포함된 사전을 수신합니다.                                                       |

### 스테이징 환경

`ims_base_url`을 재정의하여 스테이징 Adobe IMS 인스턴스를 지정합니다. 또한 프로그래밍 방식으로 프로덕션 값을 참조해야 하는 경우 SDK는 `DEFAULT_IMS_BASE_URL`(`https://ims-na1.adobelogin.com`)을 내보냅니다.

```python
auth = ServerToServerAuth(
    client_id="...",
    client_secret="...",
    ims_base_url="https://ims-na1-stg1.adobelogin.com",
)
```

### 사용자 지정 HTTP 클라이언트

프록시 지원 또는 사용자 지정 TLS 구성에 사용됩니다.

```python
import httpx

http_client = httpx.Client(
    proxy="http://corporate-proxy:8080",
    verify="/path/to/custom-ca-bundle.pem",
)

auth = ServerToServerAuth(
    client_id="...",
    client_secret="...",
    http_client=http_client,
)
```

---

## 스레드 안전성

동기 인증 클래스는 스레드로부터 완전히 안전합니다. 여러 스레드가 동시에 `get_token`을 호출하여 새로 고침이 필요한 경우 단 하나의 스레드만 새로 고침을 수행합니다. 나머지 스레드들은 대기하다가 동일한 결과를 받게 됩니다. 외부 잠금이 필요하지 않습니다. 비동기 클래스는 `asyncio.Lock`을 사용하여 동일하게 이를 보장하므로, 단일 이벤트 루프 내의 동시 코루틴에서 안전하게 사용할 수 있습니다.