Microsoft Graph 실무 입문 #3 Python SDK 첫 호출: 사용자와 조직 데이터 읽기
2편에서 앱 등록과 권한을 준비했으니, 이제 코드입니다. 이 시리즈는 Python을 기준 언어로 씁니다. 사내 자동화의 현실적인 표준이고, 파이썬 트랙의 독자라면 바로 이어집니다(SDK는 JavaScript/TypeScript, C#, Java, Go 등도 같은 구조라서, 이 편의 골격은 다른 언어에도 그대로 옮겨집니다). 이번 편의 목표는 이후 4〜7편에서 계속 재사용할 호출 골격을 만드는 것입니다.
설치: 패키지는 두 개입니다 #
$ pip install azure-identity msgraph-sdk역할 분담이 명확합니다. azure-identity가 인증(토큰 획득)을, msgraph-sdk가 Graph 호출을 맡습니다. 2편에서 정리한 인증 흐름들이 azure-identity에서는 각각 하나의 credential 클래스입니다. device code flow는 DeviceCodeCredential, client credentials flow는 ClientSecretCredential이라는 식이라, 개념과 코드가 1:1로 대응합니다.
application 클라이언트: 무인 자동화의 골격 #
먼저 서버 자동화의 기본형인 application(client credentials) 쪽입니다. 2편에서 만든 테넌트 ID, 클라이언트 ID, 시크릿 세 가지가 재료이고, 시크릿은 환경 변수로 읽습니다.
import os
from azure.identity.aio import ClientSecretCredential
from msgraph import GraphServiceClient
def build_app_client() -> GraphServiceClient:
credential = ClientSecretCredential(
tenant_id=os.environ["AZURE_TENANT_ID"],
client_id=os.environ["AZURE_CLIENT_ID"],
client_secret=os.environ["AZURE_CLIENT_SECRET"],
)
# application 권한은 개별 스코프 대신 .default를 씁니다.
# "앱 등록에 부여(동의)된 권한 전부"라는 뜻입니다.
scopes = ["https://graph.microsoft.com/.default"]
return GraphServiceClient(credential, scopes)두 가지가 낯설 수 있습니다. 첫째, azure.identity.aio라는 경로입니다. Python용 Graph SDK는 async 기반이라 비동기 버전의 credential을 씁니다(뒤에서 다시 봅니다). 둘째, 스코프가 .default 하나라는 점입니다. delegated처럼 스코프를 개별 나열하는 것이 아니라, “관리자가 이 앱에 동의해 준 application 권한 전부"를 의미하는 고정 문자열입니다. application 권한의 실제 범위는 코드가 아니라 앱 등록 화면에서 정해진다는 2편의 이야기가 코드에 이렇게 반영됩니다.
delegated 클라이언트: CLI 도구의 골격 #
사람이 실행하는 도구라면 delegated입니다. 터미널 도구에서 가장 실용적인 로그인 방식이 device code flow입니다. 코드가 URL과 일회용 코드를 보여 주고, 사용자가 브라우저에서 로그인하면 토큰이 발급됩니다.
import os
from azure.identity.aio import DeviceCodeCredential
from msgraph import GraphServiceClient
def build_user_client() -> GraphServiceClient:
credential = DeviceCodeCredential(
tenant_id=os.environ["AZURE_TENANT_ID"],
client_id=os.environ["AZURE_CLIENT_ID"],
# 시크릿이 없다는 점에 주목: 사용자가 로그인으로 증명합니다
)
# delegated는 필요한 스코프를 명시적으로 나열합니다
scopes = ["User.Read", "Mail.Read"]
return GraphServiceClient(credential, scopes)실행하면 터미널에 “https://microsoft.com/devicelogin을 열고 코드 XXXXXXX를 입력하세요"가 출력되고, 로그인을 마치면 호출이 진행됩니다. delegated에서는 스코프를 명시적으로 나열한다는 것, 그리고 그 권한이 “로그인한 사용자가 가진 것과의 교집합"이라는 2편의 규칙이 여기서도 그대로입니다.
첫 호출: async 패턴에 익숙해지기 #
골격이 생겼으니 호출합니다. Python SDK의 모든 호출은 await입니다.
import asyncio
from graph_app import build_app_client
async def main():
client = build_app_client()
# 조직의 사용자 목록 (application: User.Read.All 필요)
result = await client.users.get()
for user in result.value:
print(user.display_name, "|", user.mail, "|", user.id)
asyncio.run(main())SDK 경로와 REST URL의 대응 규칙을 잡아 두면 문서 없이도 코드를 쓸 수 있습니다. URL의 각 조각이 속성·메서드가 되고, {id} 자리는 by_xxx_id()가 됩니다.
# GET /users → client.users.get()
# GET /users/{id} → client.users.by_user_id(uid).get()
# GET /users/{id}/messages → client.users.by_user_id(uid).messages.get()
# GET /me → client.me.get() (delegated 전용)
# POST /users/{id}/sendMail → client.users.by_user_id(uid).send_mail.post(...)me는 delegated에서만 의미가 있다는 점에 주의합니다. application 토큰에는 “나"가 없으므로, application 코드에서는 항상 users.by_user_id(...)로 대상을 명시합니다. 이 차이로 403이나 400을 만나는 것이 초심자가 흔히 겪는 실수입니다.
OData를 SDK로: 쿼리 파라미터 객체 #
1편의 OData 문법은 SDK에서 쿼리 파라미터 객체로 표현됩니다. 코드가 다소 장황하지만 패턴은 하나뿐이라, 한 번 템플릿을 만들어 두면 계속 복사해 씁니다.
from msgraph.generated.users.users_request_builder import UsersRequestBuilder
async def engineering_users(client):
params = UsersRequestBuilder.UsersRequestBuilderGetQueryParameters(
select=["displayName", "mail", "department"],
filter="department eq 'Engineering'",
top=25,
orderby=["displayName"],
)
config = UsersRequestBuilder.UsersRequestBuilderGetRequestConfiguration(
query_parameters=params,
)
return await client.users.get(request_configuration=config)$select를 습관으로 만들라는 1편의 조언은 SDK에서도 같습니다. 그리고 filter나 정렬을 조합하다 400 에러(쿼리 조합이 지원되지 않음)를 만나면, 코드에서 씨름하기 전에 같은 쿼리를 Graph Explorer에서 먼저 재현해 보는 것이 훨씬 빠릅니다. Explorer에서 완성한 뒤 코드로 옮기는 작업 흐름을 다시 강조해 둡니다.
async가 처음이라면: 최소한의 규칙 세 가지 #
Graph SDK 때문에 async를 처음 만나는 독자를 위해 필요한 최소한만 짚습니다(자세한 내용은 모던 파이썬 중급 #7에서 다뤘습니다).
- Graph를 호출하는 함수는
async def로 선언하고, 호출에는await를 붙입니다. - 프로그램의 진입점에서
asyncio.run(main())으로 한 번 감쌉니다. await를 빠뜨리면 “coroutine was never awaited” 경고와 함께 아무 일도 일어나지 않습니다. 이 경고를 보면 await 누락부터 찾습니다.
배치 스크립트 수준에서는 이 세 가지로 충분합니다. async의 이점(여러 호출의 동시 실행)은 7편의 운영 편에서 다시 활용합니다.
막혔을 때: 토큰을 열어 보는 디버깅 #
2편의 에러 패턴(401은 자격 증명, 403은 권한)에 하나를 더합니다. 403이 나는데 권한 설정이 맞아 보인다면, 실제 토큰에 권한이 들어 있는지 열어 봅니다. credential의 get_token()으로 토큰 문자열을 얻어 jwt.ms(Microsoft의 토큰 디코더)에 붙여 넣으면, application 토큰이라면 roles 클레임에, delegated 토큰이라면 scp 클레임에 권한 목록이 보입니다. 여기에 기대한 권한이 없다면 코드가 아니라 앱 등록(동의 누락)이나 스코프 지정이 문제라는 뜻이고, 있다면 대상 리소스 쪽 문제로 방향을 좁힐 수 있습니다. 토큰은 디버깅 목적 외에 다른 곳으로 복사하지 않도록 주의합니다.
정리 #
- 패키지는 azure-identity(인증)와 msgraph-sdk(호출) 두 개이고, 2편의 인증 흐름이 credential 클래스와 1:1로 대응합니다(무인 자동화 = ClientSecretCredential, CLI 도구 = DeviceCodeCredential).
- application은 스코프가
.default하나(실제 범위는 앱 등록의 동의가 결정), delegated는 스코프를 명시적으로 나열합니다.me는 delegated 전용입니다. - SDK 경로는 REST URL과 1:1 대응입니다(
{id}→by_xxx_id()). OData는 쿼리 파라미터 객체로 표현하고, 복잡한 쿼리는 Explorer에서 먼저 재현합니다. - 모든 호출은 await입니다. async def, await, asyncio.run 세 가지 규칙이면 배치 스크립트에는 충분합니다.
- 403 디버깅의 마지막 수단은 토큰 열어 보기입니다. roles/scp 클레임과 기대 권한을 대조합니다.
골격이 완성됐습니다. 다음 편부터 이 골격 위에 실무 시나리오를 얹습니다. 첫 번째는 가장 수요가 많은 메일과 캘린더 자동화입니다.