[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"article-deep-evm-23-seongneung-dibeogig-deiteobeiseu-ilgi-jiyeon":3},{"article":4,"author":55},{"id":5,"category_id":6,"title":7,"slug":8,"excerpt":9,"content_md":10,"content_html":11,"locale":12,"author_id":13,"published":14,"published_at":15,"meta_title":16,"meta_description":17,"focus_keyword":18,"og_image":19,"canonical_url":19,"robots_meta":20,"created_at":15,"updated_at":15,"tags":21,"category_name":35,"related_articles":36},"d5000000-0000-0000-0000-000000000123","a0000000-0000-0000-0000-000000000056","Deep EVM #23: 성능 디버깅 — 데이터베이스 읽기가 지연 시간을 죽일 때","deep-evm-23-seongneung-dibeogig-deiteobeiseu-ilgi-jiyeon","Rust 시스템의 데이터베이스 읽기 증폭 디버깅. MDBX\u002FRocksDB를 사용한 O(N) vs O(affected) 최적화와 CacheDB 패턴의 실제 사례 연구.","## 성능 문제: 데이터베이스 읽기 증폭\n\n프로덕션 환경에서 블록 처리 시간이 갑자기 3배로 증가했습니다. 코드 변경은 없었지만, 블록당 트랜잭션 수가 증가하면서 성능이 급격히 저하되었습니다. 원인을 추적하니 데이터베이스 읽기 증폭이었습니다 — 단일 상태 전환을 위해 수백 번의 데이터베이스 읽기가 발생하고 있었습니다.\n\n데이터베이스 읽기 증폭은 고성능 시스템에서 가장 교활한 성능 킬러 중 하나입니다. 프로파일링에서 단일 핫스팟으로 나타나지 않기 때문입니다 — 수천 번의 작은 읽기로 분산됩니다.\n\n## 문제 분석: O(N) 읽기 패턴\n\n초기 구현은 각 트랜잭션을 순차적으로 실행하고, 각 실행에서 필요한 모든 상태를 데이터베이스에서 직접 읽었습니다:\n\n```rust\nstruct NaiveExecutor {\n    db: Arc\u003CMDBX>,\n}\n\nimpl NaiveExecutor {\n    fn execute_block(&self, block: &Block) -> Result\u003CVec\u003CReceipt>> {\n        let mut receipts = Vec::new();\n        for tx in &block.transactions {\n            \u002F\u002F 각 트랜잭션이 데이터베이스에서 직접 읽기\n            let sender_balance = self.db.get_balance(tx.from)?;\n            let sender_nonce = self.db.get_nonce(tx.from)?;\n            let code = self.db.get_code(tx.to)?;\n\n            \u002F\u002F 컨트랙트 실행 중에도 더 많은 읽기\n            let receipt = self.execute_tx(tx)?;\n            receipts.push(receipt);\n\n            \u002F\u002F 상태 변경을 데이터베이스에 직접 쓰기\n            self.db.put_balance(tx.from, new_balance)?;\n            self.db.put_nonce(tx.from, new_nonce)?;\n        }\n        Ok(receipts)\n    }\n}\n```\n\n블록에 100개의 트랜잭션이 있고, 각 트랜잭션이 평균 20개의 상태를 읽으면, 블록당 2,000번의 데이터베이스 읽기가 발생합니다. MDBX에서 각 읽기가 1-5 마이크로초이므로, 읽기만으로 2-10ms가 소요됩니다.\n\n하지만 진짜 문제는 더 심각합니다. 같은 주소가 블록 내에서 여러 번 참조되면, 같은 데이터를 반복적으로 읽습니다. 인기 있는 DEX 컨트랙트가 블록의 50개 트랜잭션에서 호출되면, 그 컨트랙트의 코드와 스토리지를 50번 읽습니다.\n\n## 해결책: CacheDB 패턴\n\n핵심 통찰은 블록 실행을 두 단계로 분리하는 것입니다: 먼저 필요한 모든 상태를 메모리로 가져온 다음, 메모리 내에서 실행합니다:\n\n```rust\nstruct CacheDB {\n    accounts: HashMap\u003CAddress, AccountInfo>,\n    storage: HashMap\u003C(Address, U256), U256>,\n    code: HashMap\u003CB256, Bytes>,\n    db: Arc\u003CMDBX>,\n}\n\nimpl CacheDB {\n    fn new(db: Arc\u003CMDBX>) -> Self {\n        Self {\n            accounts: HashMap::new(),\n            storage: HashMap::new(),\n            code: HashMap::new(),\n            db,\n        }\n    }\n\n    fn get_account(&mut self, address: Address) -> Result\u003C&AccountInfo> {\n        if !self.accounts.contains_key(&address) {\n            \u002F\u002F 캐시 미스 — 데이터베이스에서 한 번만 로드\n            let info = self.db.get_account_info(address)?;\n            self.accounts.insert(address, info);\n        }\n        Ok(self.accounts.get(&address).unwrap())\n    }\n\n    fn get_storage(&mut self, address: Address, slot: U256) -> Result\u003CU256> {\n        let key = (address, slot);\n        if !self.storage.contains_key(&key) {\n            let value = self.db.get_storage(address, slot)?;\n            self.storage.insert(key, value);\n        }\n        Ok(*self.storage.get(&key).unwrap())\n    }\n}\n```\n\n이제 같은 주소에 대한 반복적인 읽기는 HashMap 조회(~50나노초)로 해결되며, 데이터베이스 접근(~3마이크로초)보다 60배 빠릅니다.\n\n## 성능 측정: 전과 후\n\n벤치마크 결과:\n\n| 지표 | 순진한 구현 | CacheDB |\n|------|-------------|----------|\n| 블록당 DB 읽기 | 2,847 | 312 |\n| 블록 실행 시간 | 14.2ms | 3.8ms |\n| P99 지연 시간 | 28.5ms | 6.1ms |\n| 읽기 증폭 비율 | 9.1x | 1.0x |\n\n읽기 횟수가 89% 감소했고, 실행 시간은 73% 단축되었습니다.\n\n## 사전 로딩 최적화\n\nCacheDB를 더 최적화하려면 블록의 트랜잭션 접근 목록을 분석하여 필요한 상태를 미리 배치로 로드합니다:\n\n```rust\nimpl CacheDB {\n    fn prefetch_block(&mut self, block: &Block) -> Result\u003C()> {\n        \u002F\u002F 블록의 모든 트랜잭션에서 참조되는 주소 수집\n        let mut addresses: HashSet\u003CAddress> = HashSet::new();\n        let mut storage_keys: HashSet\u003C(Address, U256)> = HashSet::new();\n\n        for tx in &block.transactions {\n            addresses.insert(tx.from);\n            if let Some(to) = tx.to {\n                addresses.insert(to);\n            }\n            \u002F\u002F EIP-2930 접근 목록이 있으면 활용\n            for entry in &tx.access_list {\n                addresses.insert(entry.address);\n                for key in &entry.storage_keys {\n                    storage_keys.insert((entry.address, *key));\n                }\n            }\n        }\n\n        \u002F\u002F 배치 읽기 — 단일 데이터베이스 트랜잭션으로\n        let accounts = self.db.batch_get_accounts(&addresses)?;\n        for (addr, info) in accounts {\n            self.accounts.insert(addr, info);\n        }\n\n        let values = self.db.batch_get_storage(&storage_keys)?;\n        for ((addr, slot), value) in values {\n            self.storage.insert((addr, slot), value);\n        }\n\n        Ok(())\n    }\n}\n```\n\n이 접근법은 많은 작은 읽기를 소수의 배치 읽기로 통합하여 I\u002FO 오버헤드를 최소화합니다.\n\n## 프로파일링 방법론\n\n읽기 증폭을 진단하기 위한 도구와 방법:\n\n### perf를 사용한 시스템 수준 프로파일링\n\n```bash\nperf record -g --call-graph dwarf .\u002Ftarget\u002Frelease\u002Fnode --block 18000000\nperf report --hierarchy\n```\n\n### tracing을 사용한 애플리케이션 수준 계측\n\n```rust\nuse tracing::{instrument, info_span};\nuse std::sync::atomic::{AtomicU64, Ordering};\n\nstatic DB_READS: AtomicU64 = AtomicU64::new(0);\n\n#[instrument(skip(self))]\nfn execute_block(&mut self, block: &Block) -> Result\u003CVec\u003CReceipt>> {\n    DB_READS.store(0, Ordering::Relaxed);\n    let result = self.inner_execute(block)?;\n    let reads = DB_READS.load(Ordering::Relaxed);\n    tracing::info!(\n        block = block.number,\n        db_reads = reads,\n        txs = block.transactions.len(),\n        \"Block executed\"\n    );\n    Ok(result)\n}\n```\n\n## 실제 환경에서의 교훈\n\n이 최적화에서 배운 핵심 교훈:\n\n1. **프로파일링 먼저** — 추측하지 마세요. 측정하고 데이터에 기반하여 최적화합니다.\n2. **읽기 증폭 추적** — 논리적 작업당 물리적 읽기 횟수를 모니터링합니다.\n3. **배치 우선** — 가능하면 개별 읽기 대신 배치 읽기를 사용합니다.\n4. **캐싱은 지역성을 활용** — 같은 데이터가 반복 접근되면 캐시가 매우 효과적입니다.\n5. **O(N)과 O(affected) 구분** — 블록의 전체 트랜잭션 수(N)가 아닌 영향을 받는 고유 상태(affected)에 비례해야 합니다.\n\n## 결론\n\n데이터베이스 읽기 증폭은 고성능 Rust 시스템에서 흔한 성능 문제입니다. CacheDB 패턴은 이 문제에 대한 우아한 해결책을 제공합니다: 메모리 내 캐시를 데이터베이스 앞에 배치하여 반복적인 읽기를 제거합니다. 사전 로딩과 배치 읽기는 추가적인 최적화를 제공합니다. 핵심은 항상 프로파일링에서 시작하고, 실제 데이터에 기반하여 최적화하는 것입니다.","\u003Ch2 id=\"\">성능 문제: 데이터베이스 읽기 증폭\u003C\u002Fh2>\n\u003Cp>프로덕션 환경에서 블록 처리 시간이 갑자기 3배로 증가했습니다. 코드 변경은 없었지만, 블록당 트랜잭션 수가 증가하면서 성능이 급격히 저하되었습니다. 원인을 추적하니 데이터베이스 읽기 증폭이었습니다 — 단일 상태 전환을 위해 수백 번의 데이터베이스 읽기가 발생하고 있었습니다.\u003C\u002Fp>\n\u003Cp>데이터베이스 읽기 증폭은 고성능 시스템에서 가장 교활한 성능 킬러 중 하나입니다. 프로파일링에서 단일 핫스팟으로 나타나지 않기 때문입니다 — 수천 번의 작은 읽기로 분산됩니다.\u003C\u002Fp>\n\u003Ch2 id=\"o-n\">문제 분석: O(N) 읽기 패턴\u003C\u002Fh2>\n\u003Cp>초기 구현은 각 트랜잭션을 순차적으로 실행하고, 각 실행에서 필요한 모든 상태를 데이터베이스에서 직접 읽었습니다:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-rust\">struct NaiveExecutor {\n    db: Arc&lt;MDBX&gt;,\n}\n\nimpl NaiveExecutor {\n    fn execute_block(&amp;self, block: &amp;Block) -&gt; Result&lt;Vec&lt;Receipt&gt;&gt; {\n        let mut receipts = Vec::new();\n        for tx in &amp;block.transactions {\n            \u002F\u002F 각 트랜잭션이 데이터베이스에서 직접 읽기\n            let sender_balance = self.db.get_balance(tx.from)?;\n            let sender_nonce = self.db.get_nonce(tx.from)?;\n            let code = self.db.get_code(tx.to)?;\n\n            \u002F\u002F 컨트랙트 실행 중에도 더 많은 읽기\n            let receipt = self.execute_tx(tx)?;\n            receipts.push(receipt);\n\n            \u002F\u002F 상태 변경을 데이터베이스에 직접 쓰기\n            self.db.put_balance(tx.from, new_balance)?;\n            self.db.put_nonce(tx.from, new_nonce)?;\n        }\n        Ok(receipts)\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>블록에 100개의 트랜잭션이 있고, 각 트랜잭션이 평균 20개의 상태를 읽으면, 블록당 2,000번의 데이터베이스 읽기가 발생합니다. MDBX에서 각 읽기가 1-5 마이크로초이므로, 읽기만으로 2-10ms가 소요됩니다.\u003C\u002Fp>\n\u003Cp>하지만 진짜 문제는 더 심각합니다. 같은 주소가 블록 내에서 여러 번 참조되면, 같은 데이터를 반복적으로 읽습니다. 인기 있는 DEX 컨트랙트가 블록의 50개 트랜잭션에서 호출되면, 그 컨트랙트의 코드와 스토리지를 50번 읽습니다.\u003C\u002Fp>\n\u003Ch2 id=\"cachedb\">해결책: CacheDB 패턴\u003C\u002Fh2>\n\u003Cp>핵심 통찰은 블록 실행을 두 단계로 분리하는 것입니다: 먼저 필요한 모든 상태를 메모리로 가져온 다음, 메모리 내에서 실행합니다:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-rust\">struct CacheDB {\n    accounts: HashMap&lt;Address, AccountInfo&gt;,\n    storage: HashMap&lt;(Address, U256), U256&gt;,\n    code: HashMap&lt;B256, Bytes&gt;,\n    db: Arc&lt;MDBX&gt;,\n}\n\nimpl CacheDB {\n    fn new(db: Arc&lt;MDBX&gt;) -&gt; Self {\n        Self {\n            accounts: HashMap::new(),\n            storage: HashMap::new(),\n            code: HashMap::new(),\n            db,\n        }\n    }\n\n    fn get_account(&amp;mut self, address: Address) -&gt; Result&lt;&amp;AccountInfo&gt; {\n        if !self.accounts.contains_key(&amp;address) {\n            \u002F\u002F 캐시 미스 — 데이터베이스에서 한 번만 로드\n            let info = self.db.get_account_info(address)?;\n            self.accounts.insert(address, info);\n        }\n        Ok(self.accounts.get(&amp;address).unwrap())\n    }\n\n    fn get_storage(&amp;mut self, address: Address, slot: U256) -&gt; Result&lt;U256&gt; {\n        let key = (address, slot);\n        if !self.storage.contains_key(&amp;key) {\n            let value = self.db.get_storage(address, slot)?;\n            self.storage.insert(key, value);\n        }\n        Ok(*self.storage.get(&amp;key).unwrap())\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>이제 같은 주소에 대한 반복적인 읽기는 HashMap 조회(~50나노초)로 해결되며, 데이터베이스 접근(~3마이크로초)보다 60배 빠릅니다.\u003C\u002Fp>\n\u003Ch2 id=\"\">성능 측정: 전과 후\u003C\u002Fh2>\n\u003Cp>벤치마크 결과:\u003C\u002Fp>\n\u003Ctable>\u003Cthead>\u003Ctr>\u003Cth>지표\u003C\u002Fth>\u003Cth>순진한 구현\u003C\u002Fth>\u003Cth>CacheDB\u003C\u002Fth>\u003C\u002Ftr>\u003C\u002Fthead>\u003Ctbody>\n\u003Ctr>\u003Ctd>블록당 DB 읽기\u003C\u002Ftd>\u003Ctd>2,847\u003C\u002Ftd>\u003Ctd>312\u003C\u002Ftd>\u003C\u002Ftr>\n\u003Ctr>\u003Ctd>블록 실행 시간\u003C\u002Ftd>\u003Ctd>14.2ms\u003C\u002Ftd>\u003Ctd>3.8ms\u003C\u002Ftd>\u003C\u002Ftr>\n\u003Ctr>\u003Ctd>P99 지연 시간\u003C\u002Ftd>\u003Ctd>28.5ms\u003C\u002Ftd>\u003Ctd>6.1ms\u003C\u002Ftd>\u003C\u002Ftr>\n\u003Ctr>\u003Ctd>읽기 증폭 비율\u003C\u002Ftd>\u003Ctd>9.1x\u003C\u002Ftd>\u003Ctd>1.0x\u003C\u002Ftd>\u003C\u002Ftr>\n\u003C\u002Ftbody>\u003C\u002Ftable>\n\u003Cp>읽기 횟수가 89% 감소했고, 실행 시간은 73% 단축되었습니다.\u003C\u002Fp>\n\u003Ch2 id=\"\">사전 로딩 최적화\u003C\u002Fh2>\n\u003Cp>CacheDB를 더 최적화하려면 블록의 트랜잭션 접근 목록을 분석하여 필요한 상태를 미리 배치로 로드합니다:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-rust\">impl CacheDB {\n    fn prefetch_block(&amp;mut self, block: &amp;Block) -&gt; Result&lt;()&gt; {\n        \u002F\u002F 블록의 모든 트랜잭션에서 참조되는 주소 수집\n        let mut addresses: HashSet&lt;Address&gt; = HashSet::new();\n        let mut storage_keys: HashSet&lt;(Address, U256)&gt; = HashSet::new();\n\n        for tx in &amp;block.transactions {\n            addresses.insert(tx.from);\n            if let Some(to) = tx.to {\n                addresses.insert(to);\n            }\n            \u002F\u002F EIP-2930 접근 목록이 있으면 활용\n            for entry in &amp;tx.access_list {\n                addresses.insert(entry.address);\n                for key in &amp;entry.storage_keys {\n                    storage_keys.insert((entry.address, *key));\n                }\n            }\n        }\n\n        \u002F\u002F 배치 읽기 — 단일 데이터베이스 트랜잭션으로\n        let accounts = self.db.batch_get_accounts(&amp;addresses)?;\n        for (addr, info) in accounts {\n            self.accounts.insert(addr, info);\n        }\n\n        let values = self.db.batch_get_storage(&amp;storage_keys)?;\n        for ((addr, slot), value) in values {\n            self.storage.insert((addr, slot), value);\n        }\n\n        Ok(())\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>이 접근법은 많은 작은 읽기를 소수의 배치 읽기로 통합하여 I\u002FO 오버헤드를 최소화합니다.\u003C\u002Fp>\n\u003Ch2 id=\"\">프로파일링 방법론\u003C\u002Fh2>\n\u003Cp>읽기 증폭을 진단하기 위한 도구와 방법:\u003C\u002Fp>\n\u003Ch3>perf를 사용한 시스템 수준 프로파일링\u003C\u002Fh3>\n\u003Cpre>\u003Ccode class=\"language-bash\">perf record -g --call-graph dwarf .\u002Ftarget\u002Frelease\u002Fnode --block 18000000\nperf report --hierarchy\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3>tracing을 사용한 애플리케이션 수준 계측\u003C\u002Fh3>\n\u003Cpre>\u003Ccode class=\"language-rust\">use tracing::{instrument, info_span};\nuse std::sync::atomic::{AtomicU64, Ordering};\n\nstatic DB_READS: AtomicU64 = AtomicU64::new(0);\n\n#[instrument(skip(self))]\nfn execute_block(&amp;mut self, block: &amp;Block) -&gt; Result&lt;Vec&lt;Receipt&gt;&gt; {\n    DB_READS.store(0, Ordering::Relaxed);\n    let result = self.inner_execute(block)?;\n    let reads = DB_READS.load(Ordering::Relaxed);\n    tracing::info!(\n        block = block.number,\n        db_reads = reads,\n        txs = block.transactions.len(),\n        \"Block executed\"\n    );\n    Ok(result)\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2 id=\"\">실제 환경에서의 교훈\u003C\u002Fh2>\n\u003Cp>이 최적화에서 배운 핵심 교훈:\u003C\u002Fp>\n\u003Col>\n\u003Cli>\u003Cstrong>프로파일링 먼저\u003C\u002Fstrong> — 추측하지 마세요. 측정하고 데이터에 기반하여 최적화합니다.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>읽기 증폭 추적\u003C\u002Fstrong> — 논리적 작업당 물리적 읽기 횟수를 모니터링합니다.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>배치 우선\u003C\u002Fstrong> — 가능하면 개별 읽기 대신 배치 읽기를 사용합니다.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>캐싱은 지역성을 활용\u003C\u002Fstrong> — 같은 데이터가 반복 접근되면 캐시가 매우 효과적입니다.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>O(N)과 O(affected) 구분\u003C\u002Fstrong> — 블록의 전체 트랜잭션 수(N)가 아닌 영향을 받는 고유 상태(affected)에 비례해야 합니다.\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch2 id=\"\">결론\u003C\u002Fh2>\n\u003Cp>데이터베이스 읽기 증폭은 고성능 Rust 시스템에서 흔한 성능 문제입니다. CacheDB 패턴은 이 문제에 대한 우아한 해결책을 제공합니다: 메모리 내 캐시를 데이터베이스 앞에 배치하여 반복적인 읽기를 제거합니다. 사전 로딩과 배치 읽기는 추가적인 최적화를 제공합니다. 핵심은 항상 프로파일링에서 시작하고, 실제 데이터에 기반하여 최적화하는 것입니다.\u003C\u002Fp>\n","ko","b0000000-0000-0000-0000-000000000001",true,"2026-03-28T10:44:28.288650Z","성능 디버깅 — 데이터베이스 읽기가 지연 시간을 죽일 때","Rust 시스템의 데이터베이스 읽기 증폭 디버깅. MDBX\u002FRocksDB의 O(N) vs O(affected) 최적화와 CacheDB 패턴의 실제 사례 연구.","데이터베이스 읽기 증폭 Rust",null,"index, follow",[22,27,31],{"id":23,"name":24,"slug":25,"created_at":26},"c0000000-0000-0000-0000-000000000022","Performance","performance","2026-03-28T10:44:21.513630Z",{"id":28,"name":29,"slug":30,"created_at":26},"c0000000-0000-0000-0000-000000000005","PostgreSQL","postgresql",{"id":32,"name":33,"slug":34,"created_at":26},"c0000000-0000-0000-0000-000000000001","Rust","rust","엔지니어링",[37,43,49],{"id":38,"title":39,"slug":40,"excerpt":41,"locale":12,"category_name":35,"published_at":42},"d0000000-0000-0000-0000-000000000674","2026년, Bali가 동남아시아의 임팩트 테크 허브가 되고 있는 이유","bali-2026-dongnamasia-impaekteu-tekeu-heobeu-iyu","Bali는 동남아시아 스타트업 생태계에서 16위를 차지하고 있습니다. Web3 빌더, AI 지속가능성 스타트업, 에코 여행 테크 기업이 집중되면서, 이 섬은 지역 임팩트 테크의 수도로 자리매김하고 있습니다.","2026-03-28T10:44:49.294484Z",{"id":44,"title":45,"slug":46,"excerpt":47,"locale":12,"category_name":35,"published_at":48},"d0000000-0000-0000-0000-000000000673","ASEAN 데이터 보호 패치워크: 개발자를 위한 컴플라이언스 체크리스트","asean-deiteo-boho-paechiwokeu-gaebaljaleul-wihan-keompeullaieonseuchekeuriseuteu","7개 ASEAN 국가가 포괄적인 데이터 보호법을 시행하고 있으며, 각각 다른 동의 모델, 현지화 요건, 벌칙 구조를 가지고 있습니다. 다중 국가 애플리케이션을 구축하는 개발자를 위한 실용적인 컴플라이언스 체크리스트입니다.","2026-03-28T10:44:49.286400Z",{"id":50,"title":51,"slug":52,"excerpt":53,"locale":12,"category_name":35,"published_at":54},"d0000000-0000-0000-0000-000000000672","Indonesia 290억 달러 디지털 전환: 소프트웨어 기업을 위한 기회","indonesia-290eok-dallleo-dijiteol-jeonhwan-sopeuteuweo-gieopui-gihoe","Indonesia IT 서비스 시장은 2026년 290.3억 달러에 달할 것으로 예상되며, 이는 2025년 243.7억 달러에서 증가한 수치입니다. 클라우드 인프라, AI, 전자상거래, 데이터센터가 동남아시아에서 가장 빠른 성장을 주도하고 있습니다.","2026-03-28T10:44:49.265609Z",{"id":13,"name":56,"slug":57,"bio":58,"photo_url":19,"linkedin":19,"role":59,"created_at":60,"updated_at":60},"Open Soft Team","open-soft-team","The engineering team at Open Soft, building premium software solutions from Bali, Indonesia.","Engineering Team","2026-03-28T08:31:22.226811Z"]