Jelajahi pembangunan sistem andal & mudah dipelihara. Panduan ini mencakup keamanan tipe arsitektur, dari API REST, gRPC, hingga sistem berbasis peristiwa.
Memperkuat Fondasi Anda: Panduan Keamanan Tipe Desain Sistem dalam Arsitektur Perangkat Lunak Generik
Dalam dunia sistem terdistribusi, seorang pembunuh senyap mengintai di balik bayangan antar layanan. Ia tidak menyebabkan kesalahan kompilasi yang bising atau crash yang jelas selama pengembangan. Sebaliknya, ia menunggu dengan sabar saat yang tepat di produksi untuk menyerang, menjatuhkan alur kerja kritis dan menyebabkan kegagalan beruntun. Pembunuh ini adalah ketidakcocokan tipe data yang halus antara komponen yang berkomunikasi.
Bayangkan sebuah platform e-commerce di mana layanan `Orders` yang baru diterapkan mulai mengirim ID pengguna sebagai nilai numerik, `{"userId": 12345}`, sementara layanan `Payments` hilir, yang diterapkan beberapa bulan lalu, secara ketat mengharapkannya sebagai string, `{"userId": "u-12345"}`. Parser JSON layanan pembayaran mungkin gagal, atau lebih buruk lagi, mungkin salah menafsirkan data, menyebabkan pembayaran gagal, catatan rusak, dan sesi debugging larut malam yang panik. Ini bukan kegagalan sistem tipe satu bahasa pemrograman; ini adalah kegagalan integritas arsitektur.
Di sinilah Keamanan Tipe Desain Sistem berperan. Ini adalah disiplin ilmu yang krusial, namun sering diabaikan, yang berfokus pada memastikan bahwa kontrak antara bagian-bagian independen dari sistem perangkat lunak yang lebih besar didefinisikan dengan baik, divalidasi, dan dihormati. Ini mengangkat konsep keamanan tipe dari batasan satu codebase ke lanskap arsitektur perangkat lunak generik modern yang luas dan saling terhubung, termasuk mikroservis, arsitektur berorientasi layanan (SOA), dan sistem berbasis peristiwa.
Panduan komprehensif ini akan membahas prinsip, strategi, dan alat yang diperlukan untuk memperkuat fondasi sistem Anda dengan keamanan tipe arsitektur. Kami akan beralih dari teori ke praktik, mencakup cara membangun sistem yang tangguh, mudah dipelihara, dan dapat diprediksi yang dapat berevolusi tanpa rusak.
Mendekonstruksi Keamanan Tipe Desain Sistem
Ketika pengembang mendengar "keamanan tipe," mereka biasanya memikirkan pemeriksaan waktu kompilasi dalam bahasa dengan tipe statis seperti Java, C#, Go, atau TypeScript. Sebuah kompiler yang mencegah Anda menetapkan string ke variabel integer adalah jaring pengaman yang familiar. Meskipun tak ternilai harganya, ini hanyalah salah satu bagian dari teka-teki.
Di Luar Kompiler: Keamanan Tipe dalam Skala Arsitektur
Keamanan Tipe Desain Sistem beroperasi pada tingkat abstraksi yang lebih tinggi. Ini berkaitan dengan struktur data yang melintasi batas proses dan jaringan. Sementara kompiler Java dapat menjamin konsistensi tipe dalam satu mikroservis, ia tidak memiliki visibilitas ke layanan Python yang mengonsumsi API-nya, atau frontend JavaScript yang merender datanya.
Pertimbangkan perbedaan mendasar:
- Keamanan Tipe Tingkat Bahasa: Memverifikasi bahwa operasi dalam ruang memori satu program valid untuk tipe data yang terlibat. Ini diberlakukan oleh kompiler atau mesin runtime. Contoh: `int x = "hello";` // Gagal dikompilasi.
- Keamanan Tipe Tingkat Sistem: Memverifikasi bahwa data yang dipertukarkan antara dua atau lebih sistem independen (misalnya, melalui API REST, antrean pesan, atau panggilan RPC) mematuhi struktur dan serangkaian tipe yang disepakati bersama. Ini diberlakukan oleh skema, lapisan validasi, dan peralatan otomatis. Contoh: Layanan A mengirim `{"timestamp": "2023-10-27T10:00:00Z"}` sementara Layanan B mengharapkan `{"timestamp": 1698397200}`.
Keamanan tipe arsitektur ini adalah sistem kekebalan untuk arsitektur terdistribusi Anda, melindunginya dari muatan data yang tidak valid atau tidak terduga yang dapat menyebabkan banyak masalah.
Biaya Tinggi dari Ambiguitas Tipe
Kegagalan dalam membangun kontrak tipe yang kuat antar sistem bukanlah ketidaknyamanan kecil; ini adalah risiko bisnis dan teknis yang signifikan. Konsekuensinya sangat luas:
- Sistem Rapuh dan Kesalahan Runtime: Ini adalah hasil yang paling umum. Sebuah layanan menerima data dalam format yang tidak terduga, menyebabkannya crash. Dalam rantai panggilan yang kompleks, satu kegagalan semacam itu dapat memicu kaskade, menyebabkan pemadaman besar.
- Kerusakan Data Senyap: Mungkin lebih berbahaya daripada crash yang bising adalah kegagalan senyap. Jika sebuah layanan menerima nilai null di mana ia mengharapkan angka dan mengubahnya menjadi `0`, ia mungkin melanjutkan dengan perhitungan yang salah. Ini dapat merusak catatan database, menyebabkan laporan keuangan yang salah, atau memengaruhi data pengguna tanpa ada yang menyadarinya selama berminggu-minggu atau berbulan-bulan.
- Gesekan Pengembangan yang Meningkat: Ketika kontrak tidak eksplisit, tim terpaksa terlibat dalam pemrograman defensif. Mereka menambahkan logika validasi yang berlebihan, pemeriksaan null, dan penanganan kesalahan untuk setiap malformasi data yang mungkin terjadi. Ini membengkakkan codebase dan memperlambat pengembangan fitur.
- Debugging yang Menyakitkan: Melacak bug yang disebabkan oleh ketidakcocokan data antar layanan adalah mimpi buruk. Ini membutuhkan koordinasi log dari berbagai sistem, analisis lalu lintas jaringan, dan seringkali melibatkan saling tuding antar tim ("Layanan Anda mengirim data yang buruk!" "Tidak, layanan Anda tidak dapat menguraikannya dengan benar!").
- Erosi Kepercayaan dan Kecepatan: Dalam lingkungan mikroservis, tim harus dapat mempercayai API yang disediakan oleh tim lain. Tanpa kontrak yang terjamin, kepercayaan ini runtuh. Integrasi menjadi proses coba-coba yang lambat dan menyakitkan, menghancurkan kelincahan yang dijanjikan oleh mikroservis.
Pilar-Pilar Keamanan Tipe Arsitektur
Mencapai keamanan tipe di seluruh sistem bukan tentang menemukan satu alat ajaib. Ini tentang mengadopsi serangkaian prinsip inti dan menegakkannya dengan proses dan teknologi yang tepat. Empat pilar ini adalah fondasi arsitektur yang kuat dan aman tipe.
Prinsip 1: Kontrak Data yang Eksplisit dan Diberlakukan
Batu penjuru keamanan tipe arsitektur adalah kontrak data. Kontrak data adalah perjanjian formal yang dapat dibaca mesin yang menjelaskan struktur, tipe data, dan batasan data yang dipertukarkan antar sistem. Ini adalah satu-satunya sumber kebenaran yang harus dipatuhi oleh semua pihak yang berkomunikasi.
Alih-alih mengandalkan dokumentasi informal atau dari mulut ke mulut, tim menggunakan teknologi khusus untuk mendefinisikan kontrak ini:
- OpenAPI (sebelumnya Swagger): Standar industri untuk mendefinisikan API RESTful. Ini menjelaskan endpoint, badan permintaan/respons, parameter, dan metode otentikasi dalam format YAML atau JSON.
- Protocol Buffers (Protobuf): Mekanisme agnostik bahasa, netral platform untuk menserialisasi data terstruktur, dikembangkan oleh Google. Digunakan dengan gRPC, ini menyediakan komunikasi RPC yang sangat efisien dan bertipe kuat.
- GraphQL Schema Definition Language (SDL): Cara yang ampuh untuk mendefinisikan tipe dan kemampuan grafik data. Ini memungkinkan klien untuk meminta data yang mereka butuhkan, dengan semua interaksi divalidasi terhadap skema.
- Apache Avro: Sistem serialisasi data yang populer, terutama dalam ekosistem big data dan berbasis peristiwa (misalnya, dengan Apache Kafka). Ini unggul dalam evolusi skema.
- JSON Schema: Kosakata yang memungkinkan Anda menganotasi dan memvalidasi dokumen JSON, memastikan dokumen tersebut sesuai dengan aturan tertentu.
Prinsip 2: Desain Berbasis Skema (Schema-First)
Setelah Anda berkomitmen untuk menggunakan kontrak data, keputusan penting berikutnya adalah kapan untuk membuatnya. Pendekatan berbasis skema (schema-first) mendikte bahwa Anda merancang dan menyepakati kontrak data sebelum menulis satu baris kode implementasi pun.
Ini kontras dengan pendekatan berbasis kode (code-first), di mana pengembang menulis kode mereka (misalnya, kelas Java) dan kemudian menghasilkan skema darinya. Meskipun berbasis kode bisa lebih cepat untuk prototipe awal, berbasis skema menawarkan keuntungan signifikan dalam lingkungan multi-tim, multi-bahasa:
- Memaksa Penyelarasan Antar Tim: Skema menjadi artefak utama untuk diskusi dan tinjauan. Tim frontend, backend, mobile, dan QA semuanya dapat menganalisis kontrak yang diusulkan dan memberikan umpan balik sebelum upaya pengembangan apa pun terbuang.
- Memungkinkan Pengembangan Paralel: Setelah kontrak diselesaikan, tim dapat bekerja secara paralel. Tim frontend dapat membangun komponen UI terhadap server tiruan yang dihasilkan dari skema, sementara tim backend mengimplementasikan logika bisnis. Ini secara drastis mengurangi waktu integrasi.
- Kolaborasi Agnostik Bahasa: Skema adalah bahasa universal. Tim Python dan tim Go dapat berkolaborasi secara efektif dengan berfokus pada definisi Protobuf atau OpenAPI, tanpa perlu memahami seluk-beluk codebase masing-masing.
- Desain API yang Ditingkatkan: Merancang kontrak secara terpisah dari implementasi seringkali mengarah pada API yang lebih bersih dan lebih berpusat pada pengguna. Ini mendorong arsitek untuk memikirkan pengalaman konsumen daripada hanya mengekspos model database internal.
Prinsip 3: Validasi Otomatis dan Pembuatan Kode
Sebuah skema bukan hanya dokumentasi; ini adalah aset yang dapat dieksekusi. Kekuatan sejati dari pendekatan berbasis skema (schema-first) diwujudkan melalui otomatisasi.
Pembuatan Kode: Alat dapat mengurai definisi skema Anda dan secara otomatis menghasilkan sejumlah besar kode boilerplate:
- Server Stubs: Hasilkan antarmuka dan kelas model untuk server Anda, sehingga pengembang hanya perlu mengisi logika bisnis.
- Client SDKs: Hasilkan pustaka klien bertipe lengkap dalam berbagai bahasa (TypeScript, Java, Python, Go, dll.). Ini berarti konsumen dapat memanggil API Anda dengan pelengkapan otomatis dan pemeriksaan waktu kompilasi, menghilangkan seluruh kelas bug integrasi.
- Data Transfer Objects (DTOs): Buat objek data yang tidak dapat diubah yang sangat cocok dengan skema, memastikan konsistensi dalam aplikasi Anda.
Validasi Runtime: Anda dapat menggunakan skema yang sama untuk menegakkan kontrak pada waktu runtime. Gateway API atau middleware dapat secara otomatis mencegat permintaan masuk dan respons keluar, memvalidasinya terhadap skema OpenAPI. Jika permintaan tidak sesuai, permintaan tersebut segera ditolak dengan kesalahan yang jelas, mencegah data yang tidak valid mencapai logika bisnis Anda.
Prinsip 4: Registri Skema Terpusat
Dalam sistem kecil dengan beberapa layanan, pengelolaan skema dapat dilakukan dengan menyimpannya di repositori bersama. Namun, seiring organisasi berkembang menjadi puluhan atau ratusan layanan, ini menjadi tidak dapat dipertahankan. Registri Skema adalah layanan terpusat dan berdedikasi untuk menyimpan, membuat versi, dan mendistribusikan kontrak data Anda.
Fungsi utama dari registri skema meliputi:
- Sumber Kebenaran Tunggal: Ini adalah lokasi definitif untuk semua skema. Tidak perlu lagi bertanya-tanya versi skema mana yang benar.
- Versi dan Evolusi: Ini mengelola berbagai versi skema dan dapat menegakkan aturan kompatibilitas. Misalnya, Anda dapat mengkonfigurasinya untuk menolak versi skema baru apa pun yang tidak kompatibel ke belakang (backward-compatible), mencegah pengembang secara tidak sengaja menerapkan perubahan yang merusak.
- Kemudahan Penemuan: Ini menyediakan katalog yang dapat dijelajahi dan dicari dari semua kontrak data dalam organisasi, memudahkan tim untuk menemukan dan menggunakan kembali model data yang ada.
Confluent Schema Registry adalah contoh yang terkenal dalam ekosistem Kafka, tetapi pola serupa dapat diimplementasikan untuk jenis skema apa pun.
Dari Teori ke Praktik: Mengimplementasikan Arsitektur yang Aman Tipe
Mari kita jelajahi cara menerapkan prinsip-prinsip ini menggunakan pola arsitektur dan teknologi umum.
Keamanan Tipe dalam API RESTful dengan OpenAPI
API REST dengan muatan JSON adalah tulang punggung web, tetapi fleksibilitas bawaannya dapat menjadi sumber utama masalah terkait tipe. OpenAPI membawa disiplin ke dunia ini.
Skenario Contoh: Sebuah `UserService` perlu mengekspos endpoint untuk mengambil pengguna berdasarkan ID mereka.
Langkah 1: Definisikan Kontrak OpenAPI (misalnya, `user-api.v1.yaml`)
openapi: 3.0.0
info:
title: User Service API
version: 1.0.0
paths:
/users/{userId}:
get:
summary: Get user by ID
parameters:
- name: userId
in: path
required: true
schema:
type: string
format: uuid
responses:
'200':
description: A single user
content:
application/json:
schema:
$ref: '#/components/schemas/User'
'404':
description: User not found
components:
schemas:
User:
type: object
required:
- id
- email
- createdAt
properties:
id:
type: string
format: uuid
email:
type: string
format: email
firstName:
type: string
lastName:
type: string
createdAt:
type: string
format: date-time
Langkah 2: Otomatiskan dan Terapkan
- Pembuatan Klien: Tim frontend dapat menggunakan alat seperti `openapi-typescript-codegen` untuk menghasilkan klien TypeScript. Panggilan akan terlihat seperti `const user: User = await apiClient.getUserById('...')`. Tipe `User` dihasilkan secara otomatis, jadi jika mereka mencoba mengakses `user.userName` (yang tidak ada), kompiler TypeScript akan memunculkan kesalahan.
- Validasi Sisi Server: Backend Java yang menggunakan framework seperti Spring Boot dapat menggunakan library untuk secara otomatis memvalidasi permintaan masuk terhadap skema ini. Jika permintaan datang dengan `userId` non-UUID, framework menolaknya dengan `400 Bad Request` sebelum kode controller Anda berjalan.
Mencapai Kontrak yang Kokoh dengan gRPC dan Protocol Buffers
Untuk komunikasi layanan-ke-layanan internal berkinerja tinggi, gRPC dengan Protobuf adalah pilihan superior untuk keamanan tipe.
Langkah 1: Definisikan Kontrak Protobuf (misalnya, `user_service.proto`)
syntax = "proto3";
package user.v1;
import "google/protobuf/timestamp.proto";
service UserService {
rpc GetUser(GetUserRequest) returns (User);
}
message GetUserRequest {
string user_id = 1; // Field numbers are crucial for evolution
}
message User {
string id = 1;
string email = 2;
string first_name = 3;
string last_name = 4;
google.protobuf.Timestamp created_at = 5;
}
Langkah 2: Hasilkan Kode
Menggunakan kompiler `protoc`, Anda dapat menghasilkan kode untuk klien dan server dalam puluhan bahasa. Server Go akan mendapatkan struct bertipe kuat dan antarmuka layanan untuk diimplementasikan. Klien Python akan mendapatkan kelas yang melakukan panggilan RPC dan mengembalikan objek `User` yang bertipe lengkap.
Manfaat utama di sini adalah bahwa format serialisasi adalah biner dan terkait erat dengan skema. Hampir tidak mungkin untuk mengirim permintaan yang salah format yang bahkan akan coba diurai oleh server. Keamanan tipe diberlakukan pada banyak lapisan: kode yang dihasilkan, framework gRPC, dan format kawat biner.
Fleksibel Namun Aman: Sistem Tipe dalam GraphQL
Kekuatan GraphQL terletak pada skema bertipe kuatnya. Seluruh API dijelaskan dalam GraphQL SDL, yang bertindak sebagai kontrak antara klien dan server.
Langkah 1: Definisikan Skema GraphQL
type Query {
user(id: ID!): User
}
type User {
id: ID!
email: String!
firstName: String
lastName: String
createdAt: String! # Typically an ISO 8601 string
}
Langkah 2: Manfaatkan Perkakas
Klien GraphQL modern (seperti Apollo Client atau Relay) menggunakan proses yang disebut "introspeksi" untuk mengambil skema server. Mereka kemudian menggunakan skema ini selama pengembangan untuk:
- Memvalidasi Query: Jika seorang pengembang menulis query yang meminta field yang tidak ada pada tipe `User`, IDE mereka atau alat build-step akan segera menandainya sebagai kesalahan.
- Menghasilkan Tipe: Alat dapat menghasilkan tipe TypeScript atau Swift untuk setiap query, memastikan bahwa data yang diterima dari API sepenuhnya bertipe dalam aplikasi klien.
Keamanan Tipe dalam Arsitektur Asinkron & Berbasis Peristiwa (EDA)
Keamanan tipe bisa dibilang paling kritis, dan paling menantang, dalam sistem berbasis peristiwa. Produsen dan konsumen sepenuhnya terpisah; mereka dapat dikembangkan oleh tim yang berbeda dan diterapkan pada waktu yang berbeda. Muatan peristiwa yang tidak valid dapat meracuni topik dan menyebabkan semua konsumen gagal.
Di sinilah registri skema yang dikombinasikan dengan format seperti Apache Avro bersinar.
Skenario: Sebuah `UserService` menghasilkan peristiwa `UserSignedUp` ke topik Kafka ketika pengguna baru mendaftar. Sebuah `EmailService` mengonsumsi peristiwa ini untuk mengirim email sambutan.
Langkah 1: Definisikan Skema Avro (`UserSignedUp.avsc`)
{
"type": "record",
"namespace": "com.example.events",
"name": "UserSignedUp",
"fields": [
{ "name": "userId", "type": "string" },
{ "name": "email", "type": "string" },
{ "name": "timestamp", "type": "long", "logicalType": "timestamp-millis" }
]
}
Langkah 2: Gunakan Registri Skema
- `UserService` (produsen) mendaftarkan skema ini ke Registri Skema pusat, yang menetapkan ID unik kepadanya.
- Ketika menghasilkan pesan, `UserService` menserialisasi data peristiwa menggunakan skema Avro dan menambahkan ID skema ke muatan pesan sebelum mengirimkannya ke Kafka.
- `EmailService` (konsumen) menerima pesan. Ini membaca ID skema dari muatan, mengambil skema yang sesuai dari Registri Skema (jika belum di-cache), dan kemudian menggunakan skema yang tepat itu untuk mendeserialisasi pesan dengan aman.
Proses ini menjamin bahwa konsumen selalu menggunakan skema yang benar untuk menafsirkan data, bahkan jika produsen telah diperbarui dengan versi skema baru yang kompatibel ke belakang.
Menguasai Keamanan Tipe: Konsep Tingkat Lanjut dan Praktik Terbaik
Mengelola Evolusi dan Versi Skema
Sistem tidak statis. Kontrak harus berevolusi. Kuncinya adalah mengelola evolusi ini tanpa merusak klien yang ada. Ini membutuhkan pemahaman aturan kompatibilitas:
- Kompatibilitas Mundur (Backward Compatibility): Kode yang ditulis terhadap versi skema yang lebih lama masih dapat memproses data yang ditulis dengan versi yang lebih baru dengan benar. Contoh: Menambahkan field baru yang opsional. Konsumen lama hanya akan mengabaikan field baru tersebut.
- Kompatibilitas Maju (Forward Compatibility): Kode yang ditulis terhadap versi skema yang lebih baru masih dapat memproses data yang ditulis dengan versi yang lebih lama dengan benar. Contoh: Menghapus field opsional. Konsumen baru ditulis untuk menangani ketidakberadaannya.
- Kompatibilitas Penuh: Perubahan ini kompatibel mundur dan maju.
- Perubahan yang Merusak (Breaking Change): Perubahan yang tidak kompatibel mundur maupun maju. Contoh: Mengubah nama field yang wajib atau mengubah tipe datanya.
Peran Analisis Statis dan Linting
Sama seperti kita melakukan linting pada kode sumber kita, kita juga harus melakukan linting pada skema kita. Alat seperti Spectral untuk OpenAPI atau Buf untuk Protobuf dapat menegakkan panduan gaya dan praktik terbaik pada kontrak data Anda. Ini dapat meliputi:
- Menerapkan konvensi penamaan (misalnya, `camelCase` untuk field JSON).
- Memastikan semua operasi memiliki deskripsi dan tag.
- Menandai perubahan yang berpotensi merusak.
- Mewajibkan contoh untuk semua skema.
Linting menangkap kekurangan desain dan inkonsistensi sejak dini dalam proses, jauh sebelum mereka tertanam dalam sistem.
Mengintegrasikan Keamanan Tipe ke dalam Pipeline CI/CD
Untuk membuat keamanan tipe benar-benar efektif, harus otomatis dan tertanam dalam alur kerja pengembangan Anda. Pipeline CI/CD Anda adalah tempat yang tepat untuk menegakkan kontrak Anda:
- Langkah Linting: Pada setiap pull request, jalankan linter skema. Gagal build jika kontrak tidak memenuhi standar kualitas.
- Pemeriksaan Kompatibilitas: Ketika skema diubah, gunakan alat untuk memeriksanya terhadap kompatibilitas dengan versi yang saat ini ada di produksi. Secara otomatis blokir setiap pull request yang memperkenalkan perubahan yang merusak ke API `v1`.
- Langkah Pembuatan Kode: Sebagai bagian dari proses build, secara otomatis jalankan alat pembuatan kode untuk memperbarui server stub dan client SDK. Ini memastikan bahwa kode dan kontrak selalu sinkron.
Mendorong Budaya Pengembangan Berbasis Kontrak (Contract-First)
Pada akhirnya, teknologi hanyalah separuh solusi. Mencapai keamanan tipe arsitektur memerlukan perubahan budaya. Ini berarti memperlakukan kontrak data Anda sebagai warga negara kelas satu dari arsitektur Anda, sama pentingnya dengan kode itu sendiri.
- Jadikan tinjauan API sebagai praktik standar, sama seperti tinjauan kode.
- Berdayakan tim untuk menolak kontrak yang dirancang buruk atau tidak lengkap.
- Berinvestasi dalam dokumentasi dan perkakas yang memudahkan pengembang untuk menemukan, memahami, dan menggunakan kontrak data sistem.
Kesimpulan: Membangun Sistem yang Tangguh dan Mudah Dipelihara
Keamanan Tipe Desain Sistem bukan tentang menambahkan birokrasi yang membatasi. Ini tentang secara proaktif menghilangkan kategori besar bug yang kompleks, mahal, dan sulit didiagnosis. Dengan menggeser deteksi kesalahan dari waktu runtime di produksi ke waktu desain dan pembangunan dalam pengembangan, Anda menciptakan loop umpan balik yang kuat yang menghasilkan sistem yang lebih tangguh, andal, dan mudah dipelihara.
Dengan merangkul kontrak data yang eksplisit, mengadopsi pola pikir berbasis skema (schema-first), dan mengotomatiskan validasi melalui pipeline CI/CD Anda, Anda tidak hanya menghubungkan layanan; Anda sedang membangun sistem yang kohesif, dapat diprediksi, dan skalabel di mana komponen dapat berkolaborasi dan berevolusi dengan percaya diri. Mulailah dengan memilih satu API kritis dalam ekosistem Anda. Definisikan kontraknya, hasilkan klien bertipe untuk konsumen utamanya, dan bangun pemeriksaan otomatis. Stabilitas dan kecepatan pengembang yang Anda peroleh akan menjadi katalisator untuk memperluas praktik ini di seluruh arsitektur Anda.