What is scalability for a mobile developer

Copied from my Facebook post.

Iโ€™ve had thoughts about what scalability means to a mobile developer for a long time. I started my career at Line because I wanted to experience dealing with thousands of global users. But actually, the traffic of thousands of people was something that a server developer had to deal with. I was a little disappointed.

A server developerโ€™s skill is measured by how much traffic one has handled, but for a mobile developer, I felt like there was not much difference in skills or code in whether one develops an app used by 100 people or 1 million people. So I wondered what it means to create a scalable code or system in a mobile app.

A developerโ€™s skill is concentrated in the section where bottlenecks occur. Because users get crowded in one place at the same time, the server gets loaded, and the skill that can solve this load is valuable. But an app does not get loaded because there are a lot of users. This is because the app gets delivered to the userโ€™s doorstep, not the users getting crowded in an app.๐Ÿ‘‡

Then where is the bottleneck that needs to be handled by mobile developers? Ironically, I thought the bottlenecks in a mobile app occur not because of the users, but the developers. I thought a point where a lot of developers push their code into an app that gets deployed as a single program could be seen as a bottleneck.๐Ÿ‘‡

Just as the serverโ€™s structure and codes should be different when there are 1,000 users and millions of users, an appโ€™s development environment and code structure should be completely different when there are only 3 developers and 100 developers. If not, the consequences of the bottleneck pass on to the developer and the users as well. The first app startup time increases and the app size keeps increasing. Not only that, more crashes happen, decreasing user satisfaction, or you may have to go through hotfixes the whole time due to the side effects that keep showing up. Also, build time increases, decreasing the developerโ€™s productivity and quality of life due to stress. On top of that, when 100 people are pushing on the master branch, even QA, dealing with bugs, and deploying apps arenโ€™t simple tasks. This is because someone is pushing in codes with developed features while someoneโ€™s cherry-picking while fixing bugs. It doesnโ€™t have to go up to 100 people. Even if there are more than just 10 developers, problems like these start to show up.

So I think I recently found a clue to my curiosity. I thought what scalability to a mobile developer means is that as a company grows and the mobile team grows, itโ€™s keeping both the user experience and the developer experience to not deteriorate, and keep deploying the app quickly with confidence.

Tags: scalability, mobile dev  

uber/RIBs ์œ ๋‹› ํ…Œ์ŠคํŠธ ์งœ๊ธฐ

RIBs๋Š” ์šฐ๋ฒ„์—์„œ ๊ฐœ๋ฐœํ•˜๊ณ  ์˜คํ”ˆ์†Œ์Šค๋กœ ๊ณต๊ฐœํ•œ ๋ชจ๋ฐ”์ผ ์•„ํ‚คํ…์ฒ˜ ํ”„๋ ˆ์ž„์›Œํฌ๋‹ค. RIBs ํ”„๋ ˆ์ž„์›Œํฌ๋Š” ์•ฑ์˜ ๋ณต์žกํ•œ ์ƒํƒœ ๊ด€๋ฆฌ์™€ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ RIBs ๋ฉ์–ด๋ฆฌ(์ดํ•˜ Riblet)๋กœ ๋ถ„๋ฆฌํ•œ ๋’ค ํŠธ๋ฆฌ ๊ตฌ์กฐ๋กœ ์—ฐ๊ฒฐ์‹œํ‚จ๋‹ค. ํ•˜๋‚˜์˜ Riblet ๋‹จ์œ„๋ฅผ ๊ตฌ์„ฑํ•˜๋Š” ๊ฐ์ฒด์™€ ๊ฐ๊ฐ์˜ ์—ญํ• ์€ ์•„๋ž˜์™€ ๊ฐ™๋‹ค.

RIBs์™€ ๊ฐ์ฒด์ง€ํ–ฅ ํ”„๋กœ๊ทธ๋ž˜๋ฐ

์œ„ ๋„ํ‘œ์ฒ˜๋Ÿผ ๊ฐ RIBs ๊ฐ์ฒด๋Š” ์—ญํ• ์ด ๋šœ๋ ทํ•˜๊ฒŒ ๋‚˜๋ˆ ์ ธ ์žˆ๋‹ค(Single Responsibility). ๊ทธ๋ฆฌ๊ณ  ํ™”์‚ดํ‘œ๋กœ ํ‘œ์‹œ๋ผ ์žˆ๋Š” ๊ฐ ๊ฐ์ฒด์˜ input์™€ output์€ ํ”„๋กœํ† ์ฝœ๋กœ ์ถ”์ƒํ™” ๋ผ์žˆ์–ด์„œ(Dependency Inversion) ๋”ฐ๋กœ ๋–ผ์–ด๋‚ธ ํ›„ mocking ๊ธฐ๋ฒ•์„ ํ†ตํ•ด ๋…๋ฆฝ์ ์œผ๋กœ ํ…Œ์ŠคํŠธํ•˜๊ธฐ ์ข‹๋‹ค. ๋˜ํ•œ ๋ถ€๋ชจ์™€ ์ž์‹ Riblet์€ ํŠธ๋ฆฌ ๊ตฌ์กฐ๋กœ ๋ถ„๋ฆฌ(decoupling) ๋ผ์žˆ์–ด์„œ ๋ถ€๋ชจ Riblet์˜ ์ฝ”๋“œ ์ˆ˜์ •์„ ์ตœ์†Œํ™”ํ•˜๋ฉด์„œ๋„ ๋ณต์žกํ•œ ์ž์‹ Riblet์„ ์ƒˆ๋กœ ๋งŒ๋“ค๊ฑฐ๋‚˜ ์ˆ˜์ •ํ•  ์ˆ˜ ์žˆ๋‹ค(Open-Closed Principle). ๋‘ ๋‹ฌ ์ •๋„ ํ”„๋กœ์ ํŠธ๋ฅผ ํ•ด๋ณธ ๋ฐ” ๋Š๋‚€์ ์€ RIBs ์•„ํ‚คํ…์ฒ˜๋ฅผ ์“ฐ๋ฉด SOLID์—์„œ ๋น„์ค‘์ด ํฌ๊ณ  ๊พธ์ค€ํžˆ ์ง€ํ‚ค๊ธฐ ์–ด๋ ค์šด SRP, OCP, DIP ์„ธ ๊ฐ€์ง€ ์›์น™์„ ๋ฐ˜๊ฐ•์ œ์ ์œผ๋กœ ์ง€ํ‚ค๊ฒŒ ๋œ๋‹ค. ๋ฌผ๋ก  ์–ด๊ธฐ๊ธฐ๋„ ์‰ฝ๋‹ค. ์–ด๋–ค ์•„ํ‚คํ…์ฒ˜๋ฅผ ์“ฐ๋“  ๊ฐœ๋ฐœ์ž๊ฐ€ ์ฝ”๋“œ๋ฅผ ์งœ๊ธฐ ๋‚˜๋ฆ„์ด๊ณ  ๋‹จ์ง€ ์•„ํ‚คํ…์ฒ˜๋ฅผ ๋„์ž…ํ•˜๋Š” ๊ฒƒ๋งŒ์œผ๋กœ ๋‚ด ์ฝ”๋“œ๊ฐ€ ์ข‹์•„์ง€์ง„ ์•Š๋Š”๋‹ค.

๋ฌด์—‡์„ ํ…Œ์ŠคํŠธ ํ•ด์•ผํ• ๊นŒ

RIBs ์•„ํ‚คํ…์ฒ˜๋ฅผ ๊ตฌ์„ฑํ•˜๋Š” ๊ฐ์ฒด๋“ค์€ ๋‹จ์œ„ ํ…Œ์ŠคํŠธํ•˜๊ธฐ ์ •๋ง ์šฉ์ดํ•˜๋‹ค. ์—ญํ• ์ด ๋ช…ํ™•ํ•˜๊ณ  ์ž˜๊ฒŒ ๋‚˜๋‰˜์–ด์žˆ๊ณ  ๋ถ€๋ชจ์™€ ์ž์‹ Riblet์ด ์•ฝํ•˜๊ฒŒ ์ปคํ”Œ๋ง๋œ ๋งŒํผ, ๋†’์€ ์ปค๋ฒ„๋ฆฌ์ง€๋ฅผ ๋‹ฌ์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค. Riblet๋‹น ์ตœ์†Œ 4+ ๊ฐœ์˜ ํ…Œ์ŠคํŠธ ํด๋ž˜์Šค๊ฐ€ ํ•„์š”ํ•œ๋ฐ Xcode์˜ ์ฝ”๋“œ์ƒ์„ฑ ํ…œํ”Œ๋ฆฟ์„ ์“ฐ๋ฉด ๋งค๋ฒˆ ๋งŒ๋“ค๊ธฐ ๋ฒˆ๊ฑฐ๋กœ์šด ์œ ๋‹› ํ…Œ์ŠคํŠธ ํด๋ž˜์Šค์™€ ๋ณด์ผ๋Ÿฌํ”Œ๋ ˆ์ดํŠธ ์ฝ”๋“œ๊นŒ์ง€ ์ž๋™์œผ๋กœ ์ƒ์„ฑํ•ด์ค€๋‹ค. ๊ทธ๋Ÿฐ๋ฐ ์ฒ˜์Œ ํ”„๋กœ์ ํŠธ๋ฅผ ํ• ๋•Œ๋Š” ์ด ๋งŽ์€ ํ…Œ์ŠคํŠธ ํด๋ž˜์Šค์— ๋ญ˜ ์ฑ„์›Œ ๋„ฃ์–ด์•ผํ•˜๋Š”์ง€ ๋ชฐ๋ผ ๋ง‰๋ง‰ํ–ˆ๋‹ค. ํ•˜์ง€๋งŒ ์ฝ”๋“œ ๋ฆฌ๋ทฐ๋„ ๋ฐ›๊ณ  ๊ธฐ์กด ์ฝ”๋“œ๋„ ์‚ดํŽด๋ณด๋ฉด์„œ ๊ทœ์น™์„ ๋ฐœ๊ฒฌํ–ˆ๊ณ  ์ด๋ฅผ ํ†ตํ•ด ๊ฐ ํด๋ž˜์Šค์˜ ์—ญํ• ๊ณผ ๋ชฉ์ ์— ๋”ฐ๋ผ ์–ด๋–ค ๋ฐฉ์‹์œผ๋กœ ํ…Œ์ŠคํŠธ ํ•ด์•ผํ•˜๋Š”์ง€ ํ„ฐ๋“ํ–ˆ๋‹ค.

Router Test: ์ž์‹ Riblet ๋ผ์šฐํŒ…

Router์˜ ํ…Œ์ŠคํŠธ๋Š” ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์— ๋งž์ถฐ ์ž์‹ Riblet์„ ๋—๋‹ค(attachChild) ๋ถ™์˜€๋‹ค(detachChild) ํ•˜๋Š” ๋™์ž‘์„ ๊ฒ€์‚ฌํ•ด์•ผ ํ•œ๋‹ค. Router ๊ฐ์ฒด ์„ค๋ช…์— ๋ณด๋ฉด โ€˜๋ผ์šฐํ„ฐ๊ฐ€ ์ž์‹ ๋ผ์šฐํ„ฐ๋ฅผ ๋งŒ๋“ค๋•Œ๋Š” ๊ผญ helper builder๋ฅผ ์จ์•ผ ํ•œ๋‹ค.โ€™(๋งํฌ)๋Š” ์ฃผ์„์ด ์žˆ๋‹ค. ์ž์‹ Riblet์„ ์ƒ์„ฑํ•  ๋•Œ xxBuilder ํด๋ž˜์Šค๋ฅผ ์ง์ ‘ ์“ฐ์ง€๋ง๊ณ  xxBuildable๋กœ ์ถ”์ƒํ™”ํ•˜์—ฌ ์ฃผ์ž… ๋ฐ›์•„์•ผ ํ•œ๋‹ค๋Š” ๋ง์ด๋‹ค. ์ด๋ ‡๊ฒŒ ํ•ด์•ผ ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์—์„œ xxBuildableMock์„ ์ฃผ์ž…ํ•ด์„œ ๋ผ์šฐํ„ฐ๊ฐ€ ์ž์‹ Riblet์„ ์ œ๋Œ€๋กœ ์ƒ์„ฑํ–ˆ๋Š”์ง€ ๊ฒ€์‚ฌํ•  ์ˆ˜ ์žˆ๋‹ค. Router๋Š” interactor์˜ ์š”์ฒญ์— ๋”ฐ๋ผ ๋ผ์šฐํŒ…์„ ๋Œ€์‹ ํ•ด์ฃผ๋Š” ์—ญํ• ์ด๊ธฐ ๋•Œ๋ฌธ์— ์ด๊ฒƒ ์™ธ์— ๋‹ค๋ฅธ ๋กœ์ง์€ ์—†๋Š”๊ฒŒ ๋ฐ”๋žŒ์งํ•˜๋‹ค.

Interactor Test: ๊ฐ์ข… ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง

Interactor๋Š” ์•ฑ์˜ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ๋‹ด๋‹นํ•˜๋Š” ๋ถ€๋ถ„์ด๋ผ ๋‹ค๋ฅธ ํด๋ž˜์Šค๋ณด๋‹ค ๋ณต์žกํ•˜๊ณ  ๊ฐœ๋ฐœ์ž์˜ ์ž์œ ๋„๊ฐ€ ๋†’๋‹ค. ๊ทธ๋ž˜์„œ ๋‹ค๋ฅธ ์ปดํฌ๋„ŒํŠธ์ฒ˜๋Ÿผ ์œ ํ˜•ํ™”ํ•˜๊ธฐ๊ฐ€ ์‰ฝ์ง„ ์•Š์ง€๋งŒ ์ž์ฃผ ์‚ฌ์šฉ๋˜๋Š” ๋ช‡ ๊ฐ€์ง€๊ฐ€ ์žˆ๋‹ค.

  • Interactor๋Š” ์ž์‹ interactor์— ์ •๋ณด๋ฅผ ์ „๋‹ฌํ•˜๊ธฐ ์œ„ํ•ด ๋ฆฌ์•กํ‹ฐ๋ธŒ ํ”„๋กœ๊ทธ๋ž˜๋ฐ ๋ฐฉ์‹์„ ์‚ฌ์šฉํ•œ๋‹ค(์ฐธ๊ณ : ๊ณต์‹ ๋ฌธ์„œ). ๋ฆฌ์•กํ‹ฐ๋ธŒ ์ŠคํŠธ๋ฆผ์„ ํ™œ์šฉํ•˜๋ฉด ๋ถ€๋ชจ์™€ ์ž์‹ interactor๊ฐ€ ์ง์ ‘์ ์ธ ์˜์กด ๊ด€๊ณ„(direct coupling)๋ฅผ ๋งบ์ง€ ์•Š์„ ์ˆ˜ ์žˆ๋‹ค. ์ŠคํŠธ๋ฆผ์€ ์–ด๋–ป๊ฒŒ ๊ตฌํ˜„ํ•˜๋“  ๊ฐœ๋ฐœ์ž์˜ ์ž์œ ์ง€๋งŒ ๋ณดํ†ต์€ RxSwift๋ฅผ ์“ฐ๋ฉด ์ข‹๋‹ค. RIBs ์•„ํ‚คํ…์ฒ˜๋„ Rx๋ฅผ ์“ฐ๊ณ  ์žˆ์–ด์„œ ์ด๊ฑธ ์“ฐ๋ฉด ๊ตณ์ด ๋ฆฌ์•กํ‹ฐ๋ธŒ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์—ฌ๋Ÿฌ๊ฐœ ์ถ”๊ฐ€ํ•  ํ•„์š”๊ฐ€ ์—†๋‹ค. ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์—์„œ๋Š” ์ŠคํŠธ๋ฆผ์„ mockingํ•ด์„œ interactor๊ฐ€ ์ œ๋Œ€๋กœ ๊ฐ’์„ ๋‚ด๋ณด๋‚ด๋Š”์ง€ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค. ๋ฐ˜๋Œ€๋กœ ๋ถ€๋ชจ interactorํ•œํ…Œ์„œ ์ฃผ์ž… ๋ฐ›์€ ์ŠคํŠธ๋ฆผ์— subscribeํ•ด์„œ ์ž‘์—…์„ ์ฒ˜๋ฆฌํ•ด์•ผํ•˜๋Š” ๊ฒฝ์šฐ๋ผ๋ฉด ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ mock ์ŠคํŠธ๋ฆผ์„ ์ฃผ์ž…ํ•œ ๋’ค ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค์—์„œ ๊ฐ’์„ ๋ฐ”๊ฟ”๊ฐ€๋ฉด์„œ ๋ฐ์ดํ„ฐ์— ๋งž๊ฒŒ ์ž˜ ์ฒ˜๋ฆฌํ•˜๊ณ  ์žˆ๋Š”์ง€ ๊ฒ€์‚ฌํ•œ๋‹ค.

  • Interactor๋Š” view๋กœ๋ถ€ํ„ฐ ์‚ฌ์šฉ์ž ์ž…๋ ฅ์„ ์ „๋‹ฌ๋ฐ›๋Š”๋‹ค. ์‚ฌ์šฉ์ž ์ž…๋ ฅ์— ๋”ฐ๋ผ ์ ์ ˆํ•œ ์ž‘์—…์ด ์‹คํ–‰๋๋Š”์ง€ ํ™•์ธํ•˜๊ธฐ ์œ„ํ•ด์„œ interactor์˜ view listener ๊ด€๋ จ ๋ฉ”์„œ๋“œ๋ฅผ ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค์—์„œ ์ง์ ‘ ํ˜ธ์ถœํ•ด์ค€ ๋’ค ํ•„์š”ํ•œ ์ž‘์—…์ด ์‹คํ–‰๋๋Š”์ง€ mocking๋œ ์˜์กด์„ฑ ๊ฐ์ฒด๋ฅผ ํ™•์ธํ•œ๋‹ค.

  • Interactor๋Š” router์™€ presenter(ํ˜น์€ view)๋ฅผ ์ž์ฃผ ํ˜ธ์ถœํ•œ๋‹ค. ๋ทฐ๋ฅผ ๋—๋‹ค ๋ถ™์˜€๋‹ค ํ•˜๊ธฐ๋„ ํ•˜๊ณ  UI๋ฅผ ์—…๋ฐ์ดํŠธํ•˜๊ธฐ๋„ ํ•œ๋‹ค. ๊ทธ๋ž˜์„œ router์™€ presenter ๋ฉ”์„œ๋“œ๊ฐ€ ์˜๋„์— ๋งž๊ฒŒ ๋ถˆ๋ฆฌ๋Š”์ง€ ํ™•์ธํ•œ๋‹ค. ์ด ๊ฒฝ์šฐ์—๋Š” ๋‹จ์ˆœํžˆ ๋ถˆ๋ ธ๋Š”์ง€ ์•ˆ๋ถˆ๋ ธ๋Š”์ง€ ํ™•์ธํ•˜๋Š” ๊ฒƒ๋ณด๋‹ค๋Š” ๋ถˆ๋ฆฐ ํšŸ์ˆ˜๋ฅผ ์ •ํ™•ํžˆ ๊ฒ€์‚ฌํ•˜๋Š”๊ฒŒ ์ข‹๋‹ค. UI ์—…๋ฐ์ดํŠธ๋ฅผ ๋ถˆํ•„์š”ํ•˜๊ฒŒ ์—ฌ๋Ÿฌ๋ฒˆ ํ•˜๋Š”์ง€ ํ™•์ธํ•  ์ˆ˜๋„ ์žˆ๊ณ  ์ค‘๋ณต์œผ๋กœ ๋ทฐ ๋ผ์šฐํŒ…์„ ํ•˜์ง€ ์•Š๋Š”์ง€๋„ ํ™•์ธํ•œ๋‹ค. (์ฐธ๊ณ : ๊ณต์‹ ํŠœํ† ๋ฆฌ์–ผ Mock ๊ฐ์ฒด)

Builder Test: Concrete ํด๋ž˜์Šค ์ƒ์„ฑ๊ณผ ์ฃผ์ž…

Builder๋Š” RIBs ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•˜์—ฌ ์ฐธ์กฐ์ ์„ ์—ฐ๊ฒฐํ•ด์ฃผ๊ณ , ์˜์กด์„ฑ ์ฃผ์ž…์— ํ•„์š”ํ•œ concrete ํด๋ž˜์Šค๋ฅผ ์ƒ์„ฑํ•˜๋Š” ์—ญํ• ์ด๋‹ค. ๋”ฐ๋ผ์„œ is ๋‚˜ as? ๋ฅผ ์จ์„œ ์˜ฌ๋ฐ”๋ฅธ ํด๋ž˜์Šค ํƒ€์ž…์ด ์ƒ์„ฑ๋๋Š”์ง€ ํ™•์ธํ•ด์ฃผ๋Š” ๋ฐฉ์‹์œผ๋กœ ๋™์ž‘์„ ๊ฒ€์‚ฌํ•œ๋‹ค.

Presenter Test: ๋ทฐ ๋ชจ๋ธ ์ƒ์„ฑ ๋กœ์ง

๋ฐ์ดํ„ฐ ๋ชจ๋ธ์ด ๋ทฐ ๋ชจ๋ธ๋กœ ์ž˜ ๋ณ€ํ™˜๋๋Š”์ง€ ํ™•์ธํ•œ๋‹ค. ๋ฐ์ดํ„ฐ ๋ชจ๋ธ์€ UIKit ํด๋ž˜์Šค๋ฅผ ์“ฐ์ง€ ์•Š๋„๋ก ๋งŒ๋“ค๊ณ , ๋ทฐ๋ฅผ ๊ตฌ์„ฑํ• ๋•Œ ํ•„์š”ํ•œ UIKit, Core Graphics ํƒ€์ž… ๋“ฑ์€ ๋ทฐ ๋ชจ๋ธ์—์„œ ๊ฐ€์ง€๊ณ  ์žˆ๋Š”๊ฒŒ ์ข‹๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด ๋ฐ์ดํ„ฐ ๋ชจ๋ธ์€ hex ๋ฌธ์ž์—ด๋กœ ์ƒ‰์ƒ ๊ฐ’์„ ๊ฐ€์ง€๊ณ  ์žˆ๊ณ  presenter์—์„œ ์ด๋ฅผ UIColor๋กœ ๋ณ€ํ™˜์„ ํ•œ๋‹ค. ๋˜๋Š” ๋ฐ์ดํ„ฐ ๋ชจ๋ธ์ด Date ํƒ€์ž…์œผ๋กœ ๋‚ ์งœ๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ๊ณ  presenter์—์„œ DateFormatter๋ฅผ ํ†ตํ•ด ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ํ•ด์„œ ๋ทฐ ๋ชจ๋ธ์— ์ €์žฅํ•œ๋‹ค. ์ด๊ฐ™์€ ๋ณ€ํ™˜ ๋กœ์ง์„ ๊ฒ€์‚ฌํ•ด์•ผ ํ•œ๋‹ค.

View Test: ๋ทฐ์˜ ๋ Œ๋”๋ง

View๋Š” ์ฝ”๋“œ๋งŒ์œผ๋กœ๋Š” ์œ ์˜๋ฏธํ•œ ํ…Œ์ŠคํŠธ๋ฅผ ํ•˜๊ธฐ ์–ด๋ ต๋‹ค. ์Šค๋ƒ…์ƒท ํ…Œ์ŠคํŠธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์“ฐ๋ฉด ๋ทฐ๋ฅผ ๋ Œ๋”๋งํ•ด์„œ ์ด๋ฏธ์ง€ ํŒŒ์ผ๋กœ ์ €์žฅํ•ด๋†“๊ณ , ํ…Œ์ŠคํŠธ๋ฅผ ๋Œ๋ฆด๋•Œ ๋ทฐ๋ฅผ ์ƒˆ๋กœ ๋ Œ๋”๋งํ•ด์„œ ๊ธฐ์กด์˜ ์ด๋ฏธ์ง€ ํŒŒ์ผ๊ณผ ๋น„๊ตํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ๋ทฐ ํด๋ž˜์Šค(UIView, UIViewController)๋ฅผ ๊ฒ€์‚ฌํ• ์ˆ˜ ์žˆ๋‹ค. ๋งŒ์•ฝ์— ์ฝ”๋“œ ์ˆ˜์ •์œผ๋กœ ์ธํ•ด ๋ทฐ๊ฐ€ ๋ฐ”๋€๋‹ค๋ฉด ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค๊ฐ€ ์‹คํŒจํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๋ทฐ๊ฐ€ ์ž˜๋ชป๋œ ๊ฑธ ๋ฏธ๋ฆฌ ์•Œ์•„๋‚ผ ์ˆ˜ ์žˆ๋‹ค. ์Šค๋ƒ…์ƒท ํ…Œ์ŠคํŠธ๋„ ์œ ๋‹› ํ…Œ์ŠคํŠธ๊ธฐ ๋•Œ๋ฌธ์— ์ปค๋ฒ„๋ฆฌ์ง€์— ํฌํ•จ๋˜๋Š”๊ฒŒ ์žฅ์ ์ด๋‹ค. ์Šค๋ƒ…์ƒท ํ…Œ์ŠคํŠธ๋ฅผ ์ถ”๊ฐ€ํ•ด์„œ Riblet์˜ ์ปค๋ฒ„๋ฆฌ์ง€๋ฅผ ๋Œ€๋žต 12~15% ์ •๋„ ์ฆ๊ฐ€์‹œํ‚ฌ ์ˆ˜ ์žˆ์—ˆ๋‹ค.

Tags: uber/ribs, unit tests  

Making a uber/RIBs Unit Test

RIBs is a mobile architecture framework released as an open source developed by Uber. RIBs framework separates the complex app status management and business logic into RIBs chunks(referred to as Riblets) and links them in a tree structure. The object which composes one unit riblet, and the role of each are as follows.

RIBs and Object Oriented Programming

Just like the above diagram each RIBs object has a distinctly separate role. (single responsibility) And the input and output of each object as indicated by an arrow are abstracted with a protocol(dependency inversion), so they can be separated and independently tested using the mocking method. Also, the parent and child riblets are decoupled into a tree structure, so they can be a child riblet that can be newly created or modified as it minimizes code modification of the parent riblets. (Open-closed Principle) What I felt after working on the project for about 2 months is that if you use the RIBs structure, the three principles, SRP, OCP, and DIP, which take up a large part in SOLID are semi-forced to keep. Of course, theyโ€™re easy to break as well. No matter what architecture you use, it depends on how the developer writes the codes, and just introducing an architecture doesnโ€™t improve my codes.

What should be tested

The objects that make up the RIBS architecture are very easy to unit test. The roles are clear and finely divided, and since the parent and child riblets are loosely coupled, high coverage can be achieved. At least 4+ test classes are needed for each riblet, and if you use Xcodeโ€™s code generation template, it automatically generates unit test classes as well as boilerplate codes that are cumbersome to make every time. When I was first working on the project, I was lost because I did not know what to fill in for these many test classes. But as I received code reviews and looked through the previous codes, I found a rule and learned how to test according to the roles and purpose of each class.

Router Test: Child Riblet Routing

A Routerโ€™s test needs to check the action where the child riblet is attached(attachChild) and detached(detachChild) according to business logic. If you look at the Router object description, thereโ€™s an annotation that says โ€˜when a router creates a child router, a helper builder must be used.โ€™(link). This means when generating a child Riblet, you shouldnโ€™t directly use the xxBuilder class, but abstract it using xxBuildable and get it injected. This way we can check if the router generated the child riblet properly by injecting xxBuildableMock in a test environment. The Routerโ€™s role is to do routing instead according to the interactorโ€™s request, itโ€™s desirable that thereโ€™s no other logic than this.

Interactor Test: Various Business Logics

The Interactor is in charge of an appโ€™s business logic, so itโ€™s more complicated compared to other classes and gives more freedom to developers. Although itโ€™s not easy to categorize as other components, there are a few that are being used often.

An Interactor uses the reactive programming method in order to deliver information to the child interactor(Reference: official document). If you use the reactive stream, the parent and child interactor wonโ€™t have to engage in direct coupling. It depends on the developer how to implement the stream, but itโ€™s usually good to use RxSwift. RIBs architecture also uses Rx, so if you use this, you donโ€™t necessarily have to add several reactive libraries. You can check if the interactor is exporting a value properly by mocking the stream in a testing environment. On the other hand, if you have to subscribe to the stream injected by the parent interactor to process tasks, inject a mock stream and check whether it processes well according to the data as you change the values in the test case.

  • The interactor receives a user input from view. To check if an appropriate task has been executed according to the user input, directly call the method in the interactor related to the view listener from the test case and check the mocked dependency object to see if the necessary task has been executed.

  • The Interactor calls the router and the presenter(or view) frequently. The view can be attached and detached, and the UI may get updated. So check whether the router and the presenter method are called according to the intent. In this case, rather than simply checking if it got called or not, itโ€™s better to check exactly how many times it was called. You can check how many UI updates are being done unnecessarily, and also check if the view routing is not done in duplication. (Reference: Official tutorial Mock Object)

Builder Test: Generating and Injecting a Concrete Class

The Builder generates a RIBs object to connect the reference points, and creates the concrete classes necessary for dependency injection. Therefore, check the action by using is or as? to see if a correct class type has been generated.

Presenter Test: View Model Generation Logic

Check if the data model has been converted into a view model. Itโ€™s recommended that the data model does not use the UIKit class, and keep the UIKit, Core Graphics type, etc. necessary for configuring a view within the view model.
For example, a data model has a color value as a hex string and the presenter converts it to UIColor. Also, the data model has the date in Date type, and the presenter converts the value into a string using the DateFormatter and saves it into the view model. The conversion logic like this need to be checked.

View Test: Rendering Views

For views, itโ€™s difficult to do meaningful testing only with codes. If you use a snapshot test library, the view is rendered and saved as an image file. You can check the view class(UIView, UIViewController) by rendering the view again and comparing it with the existing image file during testing. If the view changes due to modification in codes, the test case fails, and you can find out that the view is wrong in advance. Since the snapshot test is also a unit test, it gets included in the coverage. By adding snapshot tests, the coverage of the riblet could be increased by approximately 12-15%.

Tags: uber/ribs, unit tests