Get started with Azure Table Storage and the Azure Cosmos DB Table api using F#
Get started with Azure Table Storage and the Azure Cosmos DB Table api using F#
F#로 Azure Table Storage(와 Azure Cosmos DB의 Table api)를 처음 사용해 보려는 분들을 위한 가이드예요. Table Storage가 무엇인지, 어떻게 테이블을 만들고, 데이터를 넣고, 조회하고, 지우는지 실제 F# 코드를 통해 하나씩 살펴볼게요.
출처: Get started with Azure Table Storage using F# — Microsoft Learn (F# 공식 문서)
본문
Table Storage가 뭘까요?
Azure Table Storage는 클라우드에서 구조화된 NoSQL 데이터를 저장하는 서비스예요. 핵심은 키-속성(key/attribute) 기반의 스토어인데, 스키마(schemaless)가 없어서 데이터 구조가 자유롭다는 특징이 있어요. 스키마가 없으니 애플리케이션의 요구사항이 변해도 데이터를 쉽게 바꿔 끼울 수 있죠.
데이터 접근 속도도 빠르고 비용도 효율적이어서, 비슷한 데이터 양이라면 전통적인 SQL보다 훨씬 저렴한 경우가 많아요. 웹 애플리케이션의 사용자 데이터, 주소록, 기기 정보처럼 유연한 형태의 데이터셋을 저장하기에 잘 맞아요. 테이블에는 엔티티(entity)를 얼마든지 저장할 수 있고, 스토리지 계정도 용량 한도 안에서 테이블을 몇 개든 담을 수 있어요.
한편 Azure Cosmos DB는 Table API라는 걸 제공하는데요. 이건 Azure Table Storage용으로 짠 애플리케이션이 좀 더 프리미엄한 기능이 필요할 때 쓰는 끝점이에요. 예를 들면 이런 것들이죠.
- 턴키 방식의 글로벌 배포
- 전 세계 대상의 전용 처리량
- 99th 백분위에서도 한 자릿수 밀리초의 지연 시간
- 보장되는 높은 가용성
- 자동 보조 인덱스
Table Storage용으로 작성한 애플리케이션은 코드를 바꾸지 않고 Table API로 마이그레이션해 이 프리미엄 기능을 누릴 수 있어요. Table API는 .NET, Java, Python, Node.js용 클라이언트 SDK를 제공해요.
더 자세한 내용은 Introduction to Azure Cosmos DB Table API를 참고하세요. 이 튜토리얼들은 편의상 연결 문자열(connection strings)로 Azure에 인증해요. 다만 보안을 최적으로 하려면 관리 ID(managed identities)와 함께 Microsoft Entra ID를 쓰는 걸 권장해요.
이 튜토리얼의 범위
이 튜토리얼에서는 Azure Table Storage나 Azure Cosmos DB Table API를 써서 F# 코드로 자주 수행하는 작업들을 다뤄요. 테이블 만들고 지우기, 그리고 테이블 데이터의 삽입·수정·삭제·조회까지요.
사전 준비
가이드를 따라가려면 먼저 Azure 스토리지 계정을 만들거나 Azure Cosmos DB 계정을 만들어야 해요.
F# 스크립트 만들고 F# Interactive 실행하기
이 글의 샘플 코드는 F# 애플리케이션에서도, F# 스크립트에서도 쓸 수 있어요. 스크립트로 사용하려면 F# 개발 환경에 .fsx 확장자를 가진 파일(예: tables.fsx)을 만들면 돼요.
스크립트 실행하는 법
F# Interactive, 즉 dotnet fsi는 대화형으로 실행할 수도 있고, 커맨드라인에서 스크립트를 실행하는 용도로도 쓸 수 있어요. 커맨드라인 문법은 이렇게 생겼어요.
> dotnet fsi [options] [ script-file [arguments] ]
스크립트에 패키지 추가하기
이제 #r "nuget:package name" 디렉티브로 Azure.Data.Tables 패키지를 설치하고 네임스페이스를 open할게요.
> #r "nuget: Azure.Data.Tables"
open Azure.Data.Tables
네임스페이스 선언 추가하기
tables.fsx 파일 맨 위에 아래 open 문장들을 추가해요.
open System
open Azure
open Azure.Data.Tables // Namespace for Table storage types
Azure Storage 연결 문자열 가져오기
Azure Storage Table 서비스에 접속한다면 이 튜토리얼에 쓸 연결 문자열이 필요해요. Azure 포털에서 연결 문자열을 복사할 수 있어요. 연결 문자열에 대한 더 자세한 내용은 Configure Storage Connection Strings를 참고하세요.
Azure Cosmos DB 연결 문자열 가져오기
Azure Cosmos DB에 접속한다면 마찬가지로 연결 문자열이 필요해요. Azure 포털에서 복사하면 되는데, Cosmos DB 계정에서 Settings > Connection String으로 가서 Copy 버튼을 눌러 Primary Connection String을 복사하면 돼요.
튜토리얼에서는 스크립트에 이렇게 연결 문자열을 넣어요.
let storageConnString = "..." // fill this in from your storage account
Table 서비스 클라이언트 만들기
TableServiceClient 클래스로 Table storage의 테이블과 엔티티를 조회할 수 있어요. 서비스 클라이언트를 만드는 방법 중 하나는 이렇습니다.
let tableClient = TableServiceClient storageConnString
이제 Table storage에 데이터를 읽고 쓰는 코드를 짤 준비가 됐어요.
테이블 만들기
다음 예시는 테이블이 아직 없으면 만드는 방법이에요.
// Retrieve a reference to the table.
let table = tableClient.GetTableClient "people"
// Create the table if it doesn't exist.
table.CreateIfNotExists () |> ignore
테이블에 엔티티 추가하기
엔티티의 타입은 ITableEntity를 구현해야 해요. ITableEntity를 어떻게 확장하든 자유지만, 매개변수 없는 생성자는 반드시 있어야 해요. 그리고 get과 set이 둘 다 있는 속성만 Azure Table에 저장돼요.
엔티티의 파티션 키와 행 키(partition key, row key)는 테이블 안에서 그 엔티티를 유일하게 식별해요. 파티션 키가 같은 엔티티들은 다른 파티션 키를 가진 것보다 조회가 더 빠르지만, 파티션 키를 다양하게 쓰면 병렬 작업의 확장성은 더 좋아져요.
아래는 lastName을 파티션 키로, firstName을 행 키로 쓰는 Customer 예시예요.
type Customer (firstName, lastName, email: string, phone: string) =
interface ITableEntity with
member val ETag = ETag "" with get, set
member val PartitionKey = "" with get, set
member val RowKey = "" with get, set
member val Timestamp = Nullable() with get, set
new() = Customer(null, null, null, null)
member val Email = email with get, set
member val PhoneNumber = phone with get, set
member val PartitionKey = lastName with get, set
member val RowKey = firstName with get, set
이제 Customer를 테이블에 넣어볼게요. AddEntity() 메서드를 쓰면 돼요.
let customer = Customer ("Walter", "Harp", "[email protected]", "425-555-0101")
table.AddEntity customer
엔티티 배치(batch)로 넣기
테이블에 여러 엔티티를 단 한 번의 쓰기 작업으로 배치 삽입할 수 있어요. 배치 연산은 여러 작업을 한 번에 묶어 실행할 수 있게 해주지만, 몇 가지 제약이 있어요.
- 같은 배치에서 update, delete, insert를 함께 수행할 수 있어요.
- 배치 연산에는 최대 100개 엔티티를 포함할 수 있어요.
- 배치 안의 엔티티는 모두 같은 파티션 키를 가져야 해요.
- 배치에서 쿼리를 수행하는 것도 가능하지만, 그 쿼리가 배치의 유일한 연산이어야 해요.
두 개의 insert를 하나의 배치로 묶는 코드를 볼게요.
let customers =
[
Customer("Jeff", "Smith", "[email protected]", "425-555-0102")
Customer("Ben", "Smith", "[email protected]", "425-555-0103")
]
// Add the entities to be added to the batch and submit it in a transaction.
customers
|> List.map (fun customer -> TableTransactionAction (TableTransactionActionType.Add, customer))
|> table.SubmitTransaction
파티션 안의 모든 엔티티 조회하기
파티션 안의 모든 엔티티를 테이블에서 조회하려면 Query<T> 객체를 써요. 여기서는 파티션 키가 "Smith"인 엔티티로 필터링하고 있어요.
table.Query<Customer> "PartitionKey eq 'Smith'"
파티션 안의 특정 범위 엔티티 조회하기
파티션 안의 모든 엔티티를 조회하고 싶지 않다면, 파티션 키 필터에 행 키 필터를 조합해 범위를 지정할 수 있어요. 아래는 "Smith" 파티션 중에서 행 키(이름)가 알파벳에서 "M"보다 앞선 글자로 시작하는 엔티티를 두 필터로 가져오는 코드예요.
table.Query<Customer> "PartitionKey eq 'Smith' and RowKey lt 'J'"
단일 엔티티 조회하기
특정 엔티티 하나를 조회할 때는 GetEntityAsync로 고객 "Ben Smith"를 지정해요. 컬렉션 대신 Customer 하나를 돌려받는데요, 쿼리에서 파티션 키와 행 키를 둘 다 지정하는 게 Table 서비스에서 단일 엔티티를 조회하는 가장 빠른 방법이에요.
let singleResult = table.GetEntity<Customer>("Smith", "Ben").Value
이제 결과를 출력해 볼게요.
// Evaluate this value to print it out into the F# Interactive console
singleResult
엔티티 수정하기
엔티티를 수정하려면 Table 서비스에서 엔티티를 가져온 뒤 객체를 바꾸고, TableUpdateMode.Replace 연산으로 변경분을 다시 Table 서비스에 저장해요. 이러면 서버에서 엔티티가 완전히 교체되는데, 만약 조회한 이후에 서버 쪽 엔티티가 바뀌었다면 연산은 실패해요. 이 실패는 다른 소스에서 온 변경을 애플리케이션이 실수로 덮어쓰지 않도록 막아주는 장치예요.
singleResult.PhoneNumber <- "425-555-0103"
try
table.UpdateEntity (singleResult, ETag "", TableUpdateMode.Replace) |> ignore
printfn "Update succeeded"
with
| :? RequestFailedException as e ->
printfn $"Update failed: {e.Status} - {e.ErrorCode}"
엔티티 upsert 하기
때로는 엔티티가 테이블에 이미 있는지, 그리고 있다면 거기에 저장된 현재 값이 더 이상 필요 없는지 알 수 없을 때가 있어요. 그럴 때 UpsertEntity 메서드를 쓰면 엔티티가 없으면 새로 만들고, 있으면 그 상태에 관계없이 교체해 줘요.
singleResult.PhoneNumber <- "425-555-0104"
table.UpsertEntity (singleResult, TableUpdateMode.Replace)
엔티티 속성의 일부만 조회하기
테이블 쿼리는 엔티티의 모든 속성이 아니라 몇 개만 가져올 수도 있어요. 프로젝션(projection) 이라고 부르는 이 기법은 특히 큰 엔티티에서 쿼리 성능을 높여줘요. 아래는 Query<T>와 Select로 이메일 주소만 돌려받는 코드예요. 다만 로컬 스토리지 에뮬레이터에서는 프로젝션이 지원되지 않으니, 이 코드는 Table 서비스의 계정을 쓸 때만 동작해요.
query {
for customer in table.Query<Customer> () do
select customer.Email
}
엔티티를 페이지 단위로 비동기 조회하기
엔티티를 아주 많이 읽어야 하는데, 전부 받아질 때까지 기다리는 대신 받아지는 대로 처리하고 싶을 때가 있어요. 그럴 땐 분할 쿼리(segmented query) 를 쓸 수 있어요. 아래는 비동기 워크플로로 결과를 페이지 단위로 받아오는 코드인데, 큰 결과 집합을 기다리는 동안 실행이 막히지 않아요.
let pagesResults = table.Query<Customer> ()
for page in pagesResults.AsPages () do
printfn "This is a new page!"
for customer in page.Values do
printfn $"customer: {customer.RowKey} {customer.PartitionKey}"
엔티티 삭제하기
조회한 엔티티는 삭제할 수 있어요. 엔티티 수정 때와 마찬가지로, 조회 이후에 엔티티가 바뀌었다면 삭제도 실패해요.
table.DeleteEntity ("Smith", "Ben")
테이블 삭제하기
스토리지 계정에서 테이블을 삭제할 수도 있어요. 삭제된 테이블은 삭제 후 일정 시간 동안 다시 만들 수 없게 되니 유의하세요.
table.Delete ()