1. Pengenalan DynamoDB
Amazon DynamoDB adalah layanan database NoSQL fully-managed dari AWS. DynamoDB dirancang untuk performa tinggi, skalabilitas, dan ketersediaan β tanpa perlu mengelola server, patching, atau konfigurasi infrastruktur.
DynamoDB digunakan oleh perusahaan besar seperti Amazon.com itu sendiri, Netflix, Lyft, Airbnb, dan Samsung untuk menangani traffic dalam skala miliaran request per hari.
Fitur Utama DynamoDB
| Fitur | Deskripsi |
|---|---|
| Fully Managed | Tidak perlu server, patching, atau maintenance |
| Serverless | Otomatis scale up/down sesuai demand |
| Single-digit ms latency | Konsisten <10ms untuk read/write |
| Auto Scaling | Otomatis menyesuaikan kapasitas |
| Global Tables | Multi-region replication untuk latency rendah global |
| ACID Transaction | Transaksi lintas tabel (sejak 2018) |
| DynamoDB Streams | Capture perubahan data real-time |
| Backup & Restore | Point-in-time recovery |
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β AWS CLOUD β β β β ββββββββββββ ββββββββββββββββββββββββββββββββββββ β β β Client ββββββΆβ DynamoDB Service β β β β (App/SDK) β β β β β ββββββββββββ β βββββββββββ βββββββββββ β β β β β Partitionβ β Partitionβ β β β β β 1 β β 2 β β β β β β(3 copies)β β(3 copies)β β β β β βββββββββββ βββββββββββ β β β β β β β β βββββββββββ βββββββββββ β β β β β Partitionβ β Partitionβ β β β β β 3 β β 4 β β β β β βββββββββββ βββββββββββ β β β β β β β β Storage: SSD-backed, replicated β β β β 3x across 3 AZ for durability β β β ββββββββββββββββββββββββββββββββββββ β β β β Features: Streams β Lambda β SNS/SQS β β DAX (in-memory cache) β β Global Tables (multi-region) β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Setup AWS CLI & SDK
# ============================================= # INSTALASI AWS CLI # ============================================= # macOS brew install awscli # Linux curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" unzip awscliv2.zip sudo ./aws/install # Konfigurasi credentials aws configure # AWS Access Key ID: AKIAIOSFODNN7EXAMPLE # AWS Secret Access Key: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY # Default region: ap-southeast-1 (Singapore) # Default output format: json # Cek konfigurasi aws sts get-caller-identity # ============================================= # PYTHON SDK (boto3) # ============================================= pip install boto3 # ============================================= # LOCAL DynamoDB (untuk development) # ============================================= # Download JAR mkdir -p ~/dynamodb-local cd ~/dynamodb-local curl -O https://d1ni2b6xgvw0s0.cloudfront.net/v2.x/dynamodb_local_latest.tar.gz tar xzf dynamodb_local_latest.tar.gz # Jalankan java -Djava.library.path=./DynamoDBLocal_lib -jar DynamoDBLocal.jar -sharedDb -port 8000 # Test dengan AWS CLI ke local aws dynamodb list-tables --endpoint-url http://localhost:8000
2. Membuat & Mengelola Tabel
# =============================================
# MEMBUAT TABEL
# =============================================
# Tabel sederhana dengan partition key saja
aws dynamodb create-table \
--table-name Products \
--attribute-definitions \
AttributeName=product_id,AttributeType=S \
--key-schema \
AttributeName=product_id,KeyType=HASH \
--billing-mode PAY_PER_REQUEST \
--table-class STANDARD
# Tabel dengan partition key + sort key
aws dynamodb create-table \
--table-name Orders \
--attribute-definitions \
AttributeName=customer_id,AttributeType=S \
AttributeName=order_date,AttributeType=S \
--key-schema \
AttributeName=customer_id,KeyType=HASH \
AttributeName=order_date,KeyType=RANGE \
--billing-mode PAY_PER_REQUEST
# Tabel dengan provisioned capacity
aws dynamodb create-table \
--table-name SensorData \
--attribute-definitions \
AttributeName=sensor_id,AttributeType=S \
AttributeName=timestamp,AttributeType=N \
--key-schema \
AttributeName=sensor_id,KeyType=HASH \
AttributeName=timestamp,KeyType=RANGE \
--provisioned-throughput \
ReadCapacityUnits=10,WriteCapacityUnits=10
# =============================================
# KELOLA TABEL
# =============================================
# Cek semua tabel
aws dynamodb list-tables
# Describe tabel (detail)
aws dynamodb describe-table --table-name Products
# Update capacity
aws dynamodb update-table \
--table-name SensorData \
--provisioned-throughput \
ReadCapacityUnits=20,WriteCapacityUnits=20
# Hapus tabel
aws dynamodb delete-table --table-name Products
# Backup
aws dynamodb create-backup \
--table-name Orders \
--backup-name orders-backup-2026-06
Konsep Kunci: Primary Key
| Tipe Key | Komponen | Cocok Untuk |
|---|---|---|
| Partition Key (HASH) | Satu atribut | Data dengan key unik (user_id, product_id) |
| Composite Key (HASH + RANGE) | Dua atribut | Data dengan pola many-to-many (customer + date) |
3. CRUD Operations
# =============================================
# PUT ITEM (Insert/Update)
# =============================================
aws dynamodb put-item \
--table-name Products \
--item '{
"product_id": {"S": "PROD-001"},
"name": {"S": "Laptop ASUS ROG"},
"price": {"N": "15000000"},
"category": {"S": "Elektronik"},
"stock": {"N": "50"},
"tags": {"SS": ["gaming", "laptop", "asus"]},
"in_stock": {"BOOL": true},
"specs": {"M": {
"cpu": {"S": "Intel i9"},
"ram": {"S": "32GB"},
"storage": {"S": "1TB SSD"}
}}
}'
# Tipe data DynamoDB:
# S = String
# N = Number (always string format!)
# B = Binary
# BOOL = Boolean
# NULL = Null
# M = Map (object/dict)
# L = List (array)
# SS = String Set
# NS = Number Set
# BS = Binary Set
# =============================================
# GET ITEM (Read by key)
# =============================================
aws dynamodb get-item \
--table-name Products \
--key '{"product_id": {"S": "PROD-001"}}' \
--projection-expression "product_id, #n, price" \
--expression-attribute-names '{"#n": "name"}'
# =============================================
# UPDATE ITEM
# =============================================
aws dynamodb update-item \
--table-name Products \
--key '{"product_id": {"S": "PROD-001"}}' \
--update-expression "SET price = :p, stock = stock - :s" \
--expression-attribute-values '{
":p": {"N": "14500000"},
":s": {"N": "1"}
}' \
--return-values UPDATED_NEW
# Conditional update (hanya jika stok > 0)
aws dynamodb update-item \
--table-name Products \
--key '{"product_id": {"S": "PROD-001"}}' \
--update-expression "SET stock = stock - :s" \
--expression-attribute-values '{
":s": {"N": "1"},
":min_stock": {"N": "0"}
}' \
--condition-expression "stock > :min_stock" \
--return-values UPDATED_NEW
# =============================================
# DELETE ITEM
# =============================================
aws dynamodb delete-item \
--table-name Products \
--key '{"product_id": {"S": "PROD-001"}}'
# Conditional delete
aws dynamodb delete-item \
--table-name Products \
--key '{"product_id": {"S": "PROD-001"}}' \
--condition-expression "attribute_exists(product_id)"
# =============================================
# BATCH WRITE (multiple items)
# =============================================
aws dynamodb batch-write-item \
--request-items '{
"Products": [
{
"PutRequest": {
"Item": {
"product_id": {"S": "PROD-002"},
"name": {"S": "Keyboard Mechanical"},
"price": {"N": "850000"},
"category": {"S": "Aksesoris"}
}
}
},
{
"PutRequest": {
"Item": {
"product_id": {"S": "PROD-003"},
"name": {"S": "Mouse Logitech"},
"price": {"N": "350000"},
"category": {"S": "Aksesoris"}
}
}
}
]
}'
4. Query vs Scan
Ini adalah konsep terpenting di DynamoDB. Query menggunakan index untuk pencarian efisien. Scan membaca seluruh tabel β sangat tidak efisien untuk tabel besar.
# =============================================
# QUERY (cepat β gunakan partition key!)
# =============================================
# Query orders berdasarkan customer_id (partition key)
aws dynamodb query \
--table-name Orders \
--key-condition-expression "customer_id = :cid" \
--expression-attribute-values '{
":cid": {"S": "CUST-001"}
}'
# Query dengan sort key range
aws dynamodb query \
--table-name Orders \
--key-condition-expression \
"customer_id = :cid AND order_date BETWEEN :d1 AND :d2" \
--expression-attribute-values '{
":cid": {"S": "CUST-001"},
":d1": {"S": "2026-01-01"},
":d2": {"S": "2026-06-30"}
}'
# Query dengan filter expression (filter SETELAH query)
aws dynamodb query \
--table-name Orders \
--key-condition-expression "customer_id = :cid" \
--filter-expression "total_amount > :min_amount AND #s = :status" \
--expression-attribute-names '{"#s": "status"}' \
--expression-attribute-values '{
":cid": {"S": "CUST-001"},
":min_amount": {"N": "100000"},
":status": {"S": "completed"}
}'
# Query dengan limit dan pagination
aws dynamodb query \
--table-name Orders \
--key-condition-expression "customer_id = :cid" \
--expression-attribute-values '{":cid": {"S": "CUST-001"}}' \
--limit 10 \
--scan-index-forward false # descending
# =============================================
# SCAN (lambat β baca SELURUH tabel!)
# =============================================
# Scan semua data (HINDARI di tabel besar!)
aws dynamodb scan --table-name Products
# Scan dengan filter
aws dynamodb scan \
--table-name Products \
--filter-expression "category = :cat AND price > :min_price" \
--expression-attribute-values '{
":cat": {"S": "Elektronik"},
":min_price": {"N": "1000000"}
}'
# Scan dengan projection (ambil kolom tertentu)
aws dynamodb scan \
--table-name Products \
--projection-expression "product_id, #n, price" \
--expression-attribute-names '{"#n": "name"}'
# =============================================
# PERBANDINGAN QUERY vs SCAN
# =============================================
# ββββββββββββββββ¬βββββββββββββββββββ¬βββββββββββββββββββ
# β Aspek β QUERY β SCAN β
# ββββββββββββββββΌβββββββββββββββββββΌβββββββββββββββββββ€
# β Kecepatan β β
Sangat cepat β β Lambat β
# β Cost β β
Murah (sedikitβ β Mahal (banyak β
# β β RCU) β RCU) β
# β Filter β Hanya key kolom β Semua kolom β
# β Skalabilitas β β
Konsisten β β Makin lambat β
# β β β seiring data β
# β Kapan pakai β Selalu! β Terakhir kali β
# ββββββββββββββββ΄βββββββββββββββββββ΄βββββββββββββββββββ
Scan di DynamoDB sama buruknya dengan SELECT * FROM tabel tanpa WHERE di SQL. Untuk tabel dengan jutaan item, scan bisa memakan waktu menit dan biaya RCU yang sangat besar. Selalu usahakan menggunakan Query dengan partition key.
5. GSI & LSI β Secondary Indexes
DynamoDB memiliki dua jenis secondary index: GSI (Global Secondary Index) dan LSI (Local Secondary Index). GSI adalah yang paling sering digunakan.
# =============================================
# GSI (Global Secondary Index)
# =============================================
# GSI = "virtual table" dengan partition key BERBEDA dari tabel utama
# Berguna untuk query berdasarkan kolom selain primary key
# Tambah GSI ke tabel yang sudah ada
aws dynamodb update-table \
--table-name Products \
--attribute-definitions \
AttributeName=category,AttributeType=S \
AttributeName=price,AttributeType=N \
--global-secondary-index-updates '[
{
"Create": {
"IndexName": "GSI-Category-Price",
"KeySchema": [
{"AttributeName": "category", "KeyType": "HASH"},
{"AttributeName": "price", "KeyType": "RANGE"}
],
"Projection": {
"ProjectionType": "INCLUDE",
"NonKeyAttributes": ["name", "stock"]
}
}
}
]' --billing-mode PAY_PER_REQUEST
# Query GSI (seperti query tabel biasa)
aws dynamodb query \
--table-name Products \
--index-name "GSI-Category-Price" \
--key-condition-expression "category = :cat AND price BETWEEN :p1 AND :p2" \
--expression-attribute-values '{
":cat": {"S": "Elektronik"},
":p1": {"N": "500000"},
":p2": {"N": "5000000"}
}'
# =============================================
# Membuat tabel BARU dengan GSI
# =============================================
aws dynamodb create-table \
--table-name Orders \
--attribute-definitions \
AttributeName=customer_id,AttributeType=S \
AttributeName=order_id,AttributeType=S \
AttributeName=status,AttributeType=S \
AttributeName=order_date,AttributeType=S \
--key-schema \
AttributeName=customer_id,KeyType=HASH \
AttributeName=order_id,KeyType=RANGE \
--global-secondary-index '[
{
"IndexName": "GSI-Status-Date",
"KeySchema": [
{"AttributeName": "status", "KeyType": "HASH"},
{"AttributeName": "order_date", "KeyType": "RANGE"}
],
"Projection": {"ProjectionType": "ALL"}
}
]' \
--billing-mode PAY_PER_REQUEST
# =============================================
# LSI (Local Secondary Index)
# =============================================
# LSI = partition key SAMA, sort key BERBEDA
# Harus dibuat saat tabel dibuat (tidak bisa ditambah nanti)
# Batasan: max 5 LSI per tabel, 10GB per partition
aws dynamodb create-table \
--table-name Orders \
--attribute-definitions \
AttributeName=customer_id,AttributeType=S \
AttributeName=order_date,AttributeType=S \
AttributeName=total_amount,AttributeType=N \
--key-schema \
AttributeName=customer_id,KeyType=HASH \
AttributeName=order_date,KeyType=RANGE \
--local-secondary-indexes '[
{
"IndexName": "LSI-Customer-Amount",
"KeySchema": [
{"AttributeName": "customer_id", "KeyType": "HASH"},
{"AttributeName": "total_amount", "KeyType": "RANGE"}
],
"Projection": {"ProjectionType": "ALL"}
}
]' \
--billing-mode PAY_PER_REQUEST
GSI vs LSI
| Aspek | GSI | LSI |
|---|---|---|
| Partition Key | Bisa berbeda dari tabel | Harus sama dengan tabel |
| Sort Key | Bisa berbeda | Harus berbeda dari tabel |
| Kapan dibuat | Kapan saja | Hanya saat buat tabel |
| Capacity | Capacity terpisah | Berbagi capacity tabel |
| Consistency | Eventually consistent | Strongly consistent |
| Ukuran | Tidak terbatas | Max 10GB per partition |
| Rekomendasi | β Gunakan ini (default) | Hanya jika butuh strong consistency |
ALL: Semua atribut tabel di-copy ke GSI (besar tapi lengkap). KEYS_ONLY: Hanya key attributes (kecil, hemat cost). INCLUDE: Key + atribut tertentu (balance). Pilih yang paling sesuai dengan query Anda.
6. Capacity Modes β On-Demand vs Provisioned
# =============================================
# ON-DEMAND MODE
# =============================================
# Bayar per request (pay-per-request)
# Cocok untuk: workload unpredictable, baru mulai, development
aws dynamodb update-table \
--table-name Products \
--billing-mode PAY_PER_REQUEST
# =============================================
# PROVISIONED MODE
# =============================================
# Bayar per kapasitas yang dialokasikan (RCU/WCU)
# Cocok untuk: workload predictable, cost optimization
aws dynamodb update-table \
--table-name Products \
--billing-mode PROVISIONED \
--provisioned-throughput \
ReadCapacityUnits=25,WriteCapacityUnits=10
# =============================================
# AUTO SCALING (Provisioned mode)
# =============================================
# Otomatis sesuaikan RCU/WCU berdasarkan utilisasi
# Register scalable target
aws application-autoscaling register-scalable-target \
--service-namespace dynamodb \
--resource-id table/Products \
--scalable-dynamodb/table:ReadCapacityUnits \
--min-capacity 5 \
--max-capacity 100
# Create scaling policy
aws application-autoscaling put-scaling-policy \
--service-namespace dynamodb \
--scalable-dynamodb/table:ReadCapacityUnits \
--resource-id table/Products \
--policy-name ReadAutoScaling \
--policy-type TargetTrackingScaling \
--target-tracking-scaling-policy-configuration '{
"TargetValue": 70.0,
"PredefinedMetricSpecification": {
"PredefinedMetricType": "DynamoDBReadCapacityUtilization"
},
"ScaleInCooldown": 60,
"ScaleOutCooldown": 60
}'
Understanding RCU & WCU
| Unit | Apa | Detail |
|---|---|---|
| 1 RCU | 1 strongly consistent read/detik | Item hingga 4KB |
| 1 RCU | 2 eventually consistent reads/detik | Item hingga 4KB |
| 1 WCU | 1 write/detik | Item hingga 1KB |
| 1 WCU | 1 read/detik (transaction) | 2x WCU |
7. DynamoDB Streams & Triggers
# =============================================
# AKTIFKAN STREAMS
# =============================================
aws dynamodb update-table \
--table-name Orders \
--stream-specification \
StreamEnabled=true,StreamViewType=NEW_AND_OLD_IMAGES
# StreamViewType options:
# KEYS_ONLY β hanya key attributes
# NEW_IMAGE β data baru saja
# OLD_IMAGE β data lama saja
# NEW_AND_OLD_IMAGES β data baru + lama (paling lengkap)
# Cek stream
aws dynamodb describe-table --table-name Orders \
--query 'Table.LatestStreamArn'
# Read stream records
aws dynamodb get-records \
--shard-iterator "ITERATOR_STRING"
# =============================================
# TRIGGER: Lambda function saat data berubah
# =============================================
# Skenario: Kirim notifikasi saat order dibuat
# Lambda function (Python):
"""
import json
def lambda_handler(event, context):
for record in event['Records']:
if record['eventName'] == 'INSERT':
new_image = record['dynamodb']['NewImage']
customer_id = new_image['customer_id']['S']
total = new_image['total']['N']
print(f"New order from {customer_id}: Rp{total}")
# Kirim notifikasi via SNS, email, dll.
# send_notification(customer_id, total)
return {'statusCode': 200}
"""
# Buat event source mapping di AWS Console atau CLI:
# aws lambda create-event-source-mapping \
# --event-source-arn arn:aws:dynamodb:region:account:table/Orders/stream/xxx \
# --function-name order-processor \
# --starting-position LATEST
8. TTL & DAX Cache
# =============================================
# TTL (Time To Live)
# =============================================
# Aktifkan TTL pada kolom "expires_at" (Unix timestamp)
aws dynamodb update-table \
--table-name SessionData \
--attribute-definitions AttributeName=expires_at,AttributeType=N \
--time-to-live-specification \
Enabled=true,AttributeName=expires_at
# Set item dengan TTL (30 hari dari sekarang)
# expires_at = current Unix timestamp + 2592000 detik
import time
# expires_at: int(time.time()) + 2592000
# =============================================
# DAX (DynamoDB Accelerator) β In-Memory Cache
# =============================================
# DAX = cache in-memory yang kompatibel dengan DynamoDB API
# Latency: DynamoDB ~5ms β DAX ~microseconds!
# Buat DAX cluster via Console atau CLI
# Cocok untuk workload read-heavy yang butuh latency sangat rendah
# Python dengan DAX:
# pip install amazon-dax-client
"""
import amazondax
import boto3
# Tanpa DAX:
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('Products')
response = table.get_item(Key={'product_id': 'PROD-001'})
# Dengan DAX:
dax_client = amazondax.AmazonDaxClient('dax://my-cluster.xxxx.dax-clusters.REGION.amazonaws.com')
table = dax_client.Table('Products')
response = table.get_item(Key={'product_id': 'PROD-001'}) # ~ΞΌs!
"""
9. Python SDK (boto3)
import boto3
from boto3.dynamodb.conditions import Key, Attr
from decimal import Decimal
import uuid
from datetime import datetime
# =============================================
# KONEKSI
# =============================================
# Production:
dynamodb = boto3.resource('dynamodb', region_name='ap-southeast-1')
# Local development:
# dynamodb = boto3.resource('dynamodb', endpoint_url='http://localhost:8000')
table = dynamodb.Table('Products')
# =============================================
# CREATE TABLE
# =============================================
def create_products_table():
dynamodb.create_table(
TableName='Products',
KeySchema=[
{'AttributeName': 'product_id', 'KeyType': 'HASH'},
],
AttributeDefinitions=[
{'AttributeName': 'product_id', 'AttributeType': 'S'},
],
BillingMode='PAY_PER_REQUEST'
)
# =============================================
# PUT ITEM
# =============================================
def add_product(name, price, category, stock):
item = {
'product_id': str(uuid.uuid4()),
'name': name,
'price': Decimal(str(price)), # Harus Decimal, bukan float!
'category': category,
'stock': stock,
'created_at': datetime.now().isoformat(),
}
response = table.put_item(Item=item)
return response
add_product('Laptop ASUS', 15000000, 'Elektronik', 50)
add_product('Keyboard Mech', 850000, 'Aksesoris', 200)
# =============================================
# GET ITEM
# =============================================
def get_product(product_id):
response = table.get_item(
Key={'product_id': product_id},
ProjectionExpression='#n, price, category',
ExpressionAttributeNames={'#n': 'name'}
)
return response.get('Item')
# =============================================
# QUERY
# =============================================
# Query GSI
def get_products_by_category(category, min_price=None):
params = {
'IndexName': 'GSI-Category-Price',
'KeyConditionExpression': Key('category').eq(category),
}
if min_price:
params['KeyConditionExpression'] = (
Key('category').eq(category) & Key('price').gte(min_price)
)
response = table.query(**params)
return response['Items']
# =============================================
# SCAN dengan filter
# =============================================
def search_products(keyword):
response = table.scan(
FilterExpression=Attr('name').contains(keyword)
)
return response['Items']
# =============================================
# UPDATE
# =============================================
def update_stock(product_id, quantity):
response = table.update_item(
Key={'product_id': product_id},
UpdateExpression='SET stock = stock - :qty',
ExpressionAttributeValues={':qty': quantity},
ConditionExpression='stock >= :qty',
ReturnValues='UPDATED_NEW'
)
return response['Attributes']
# =============================================
# DELETE
# =============================================
def delete_product(product_id):
table.delete_item(Key={'product_id': product_id})
# =============================================
# BATCH WRITE
# =============================================
def batch_write_products(items):
with table.batch_writer() as batch:
for item in items:
item['product_id'] = str(uuid.uuid4())
item['price'] = Decimal(str(item['price']))
batch.put_item(Item=item)
# batch_write_products([
# {'name': 'Mouse', 'price': 350000, 'category': 'Aksesoris', 'stock': 100},
# {'name': 'Monitor', 'price': 3500000, 'category': 'Elektronik', 'stock': 30},
# ])
# =============================================
# PAGINATION
# =============================================
def get_all_products():
items = []
params = {}
while True:
response = table.scan(**params)
items.extend(response['Items'])
if 'LastEvaluatedKey' not in response:
break
params['ExclusiveStartKey'] = response['LastEvaluatedKey']
return items
10. Best Practices & Anti-Patterns
Best Practices
| Praktik | Detail |
|---|---|
| Gunakan Query, bukan Scan | Selalu filter dengan partition key |
| Design for access patterns | Rancang tabel berdasarkan query, bukan entitas |
| Distribusi partition key | Hindari hot partition β pilih key yang merata |
| Gunakan GSI untuk query alternatif | Satu GSI = satu access pattern tambahan |
| Batch reads/writes | BatchWriteItem max 25 item, BatchGetItem max 100 item |
| Projection efficiency | Ambil hanya kolom yang dibutuhkan |
| TTL untuk data sementara | Session, cache, temporary data β otomatis dihapus |
Anti-Patterns
| Anti-Pattern | Kenapa Buruk | Solusi |
|---|---|---|
| Scan tanpa filter | Baca semua data = mahal & lambat | Gunakan Query + partition key |
| Hot partition | Satu partition kebanjiran request | Randomize/shard partition key |
| Too many GSI | Setiap GSI = biaya write tambahan | Maksimal 5 GSI, design dengan hati-hati |
| Large items (>400KB) | Batas per item 400KB | Simpan di S3, simpan pointer di DynamoDB |
| Float untuk uang | Precision error | Selalu gunakan Decimal |
11. Quiz: Uji Pemahamanmu!
Setelah membaca tutorial di atas, jawablah 5 pertanyaan berikut: