$30 off During Our Annual Pro Sale. View Details »

Google의 개발문화와 프로세스(1): Scale & Efficiency / Tes...

Google의 개발문화와 프로세스(1): Scale & Efficiency / Testing

Sa-ryong Kang

November 19, 2020
Tweet

More Decks by Sa-ryong Kang

Other Decks in Programming

Transcript

  1. Disclaimer • 기본적으로 공개된 내용에 기초하고 있지만, 해석은 어디까지나 개인

    의견입니다. • Don't try this at home: 아무리 성공 사례라고 해도 도입의 이유는, ◦ "구글이 한다니까" ‍♂ ◦ "나 or 우리 조직에 필요하고 AND 적합하니까" ‍♂
  2. 왜 문화를 알아야 하는가? • 사내 문화? ◦ 참고: Netflix

    Culture: Freedom & Responsibility ◦ “회사가 실제로 어디에 가치를 두고 있는가를 보려면, 누가 보상을 받고, 승진하고, 해고 당하는가 를 보면 된다” • 큰 그림에서 회사를 볼 수 있게 됨 ◦ 회사의 건강함을 판단할 수 있는 중요한 잣대 중 하나 ◦ 문화, 비즈니스 모델, 실적
  3. 개발 프로세스의 세부 사항: 내가 개선 할 수 있는 일

    • Testing • Code review • Documentation • Version control • Estimate & analysis • CI / CD • Dependency management • Etc.
  4. 60,000+ engineers 75,000 changes per day 2 billion lines of

    code 9 million source files 500 million test cases per day Google Scale
  5. Scalability • Scalable: sublinear scaling with regard to human interaction

    • 개발에서뿐만 아니라 모든 정책 결정에 영향을 미치는 요소 • 만약 팀이 10배가 된다면, 조직의 성과물도 10배로 증가할 수 있도록, 작업을 최대한 최적화 / 자동화 할 수 있는가? ◦ No 라면 뭔가 잘못된 것
  6. Eg 2. Deprecation • 전통적인 방법: "x월 y일에 기존 컴포넌트를

    지울 예정이니 그전에 새로운 컴포넌트로 변경하시오" ◦ Mandating doesn't scale. ◦ Heroic don't scale! • Churn Rule(Since 2012): Expertise scale! ◦ 변경을 만들어 낸 조직이 변경을 위한 best practice 등을 만들어서 전파. 변경사항이 매우 복잡한 경우, DevOps 조직에서 요청해서 내부 컨설팅 진행
  7. Eg 3. Configuration management: weekly merge meeting • Branch 기반

    코드 관리(ie. git)의 장점과 문제점
  8. Solution: no merge meeting • Piper: branch를 (거의) 사용하지 않는

    single repository(monorepo) system ◦ 개발용 브랜치가 오래 open되어 있을 수 없음 ◦ 커밋할 브랜치를 선택할 수 없음 ◦ 어떤 버전에 의존하는가를 선택할 수 없음 • 참고 문헌: Advantages of trunk based development (한글 | 영문)
  9. 그 외에도.. • 비용 지불 • 효율적으로 일하는 방식 ◦

    아무도 가르쳐주지 않았으나 몸에 베어있는 agility ◦ 영웅주의 / 무리해서 일하기 배격
  10. Test-centric Culture • Google은 극도로 테스트 중심의 개발 원칙을 갖고

    있음 ◦ 여기서는 테스트는 engineer-driven automated testing을 의미함 ◦ “QA doesn't scale”
  11. 그 전에, Google의 개발 프로세스는.. 1. Issue Tracker에 작업 등록

    2. 코드 작성 (test 코드 포함) 3. Pull Request 신청 4. 자동 검사 (test coverage 미달, 코드 스타일, 잠재적 취약성 등), 테스트 실행 5. Code Review 6. Merge!
  12. 이런 embrassing한 상황에 처하기도 하지만.. • Setting this image as

    wallpaper could soft-brick your phone // TODO: Fine tune the performance here, tracking on [b/123615079] int[] histogram = new int[256]; for (int row = 0; row < height; row++) { for (int col = 0; col < width; col++) { int pixel = grayscale.getPixel(col, row); int y = Color.red(pixel) + Color.green(pixel) + Color.blue(pixel); histogram[y]++; } }
  13. 버그 발생시 프로세스는.. 1. Issue Tracker에 버그 보고 2. 해당

    Pulll Request를 통째로 roll back 3. 보고된 버그를 재현할 수 있는 테스트 코드 작성 (물론 테스트 케이스는 fail) 4. 테스트 코드를 pass 할 수 있는 코드 작성 5. Pull Request 신청 (이하 동일)
  14. 테스트로 얻을 수 있는 이점 • 코딩 생산성 증대: 확신을

    갖고 시스템을 수정할 수 있음 • 문서로서의 테스트 코드. 테스트 케이스만 보면 특정 시스템의 기능과 의도, 올바른 사용법을 단번에 파악 가능 • 협업을 촉진: code owner가 아니더라도 코드를 수정할 수 있음 • Simpler reviews • modular한 설계에 도움: single responsibility
  15. 실패 사례: Google Web Server in 2005 • 거의 테스트

    코드가 없었음 ◦ 특정 시점에서는 무려 80% 이상의 PR이 버그로 인해 롤백되었음 -_-; ◦ 개발자들이 기능을 수정할 때 잘 동작하리라는 확신이 없어짐 • 이 문제를 분석한 결과, 자동화 테스트만이 해결책이라는 결론 ◦ 코드 변경시에는 반드시 테스트를 함께 구현하도록 정책 변경 ◦ 1년 뒤 → 긴급 패치 숫자가 절반으로 감소 ◦ 현재 → GWS 단일 시스템에서만 수만 개의 테스트 케이스
  16. Google의 테스트 정책 • 높은 Test Coverage*: 여기서 테스트 커버리지는

    오직 small-sized test로만 측정함 • Beyoncé Rule: “If you liked it, then you shoulda put a test on it.” * 프로덕트 별로 차이가 있음
  17. 만약 작은 팀 혹은 혼자 코딩하고 있다면? * • PR을

    날리기 전에 이슈(aka 태스크, 스토리) 하나 당 적어도 두 개의 E2E 테스트 (정상/실패), 그리고 그것이 2x 정도의 소규모 테스트 작성 • 이슈를 중심으로 high level -> low level로 내려가면서 테스트 작성 • 버그 발생 시, 재현을 위한 테스트부터 작성 후 처리하기 * 개인적인 의견입니다
  18. Unchanging Test • Brittle test: 테스트 유지 보수 과정에서 만나는

    가장 큰 문제 • 테스트 불변의 원칙: 한 번 짠 코드는 테스트는 아래의 이유로 변경되지 않는다 ◦ Pure refactorings ◦ New features ◦ Bug fixes • 단, behavior changes의 경우에만 테스트를 수정
  19. Idiom 1. Test via Public APIs public void processTransaction(Transaction transaction)

    { if (isValid(transaction)) { saveToDatabase(transaction); } } private boolean isValid(Transaction t) { return t.getAmount() < t.getSender().getBalance(); } private void saveToDatabase(Transaction t) { String s = t.getSender() + "," + t.getRecipient() + "," + t.getAmount(); database.put(t.getId(), s); }
  20. 잘못된 예 @Test public void emptyAccountShouldNotBeValid() { assertThat(processor.isValid(newTransaction().setSender(EMPTY_ACCOUNT))) .isFalse(); }

    @Test public void shouldSaveSerializedData() { processor.saveToDatabase(newTransaction() .setId(123) .setSender("me") .setRecipient("you") .setAmount(100)); assertThat(database.get(123)).isEqualTo("me,you,100"); }
  21. 올바른 예 @Test public void shouldTransferFunds() { processor.setAccountBalance("me", 150); processor.setAccountBalance("you",

    20); processor.processTransaction(newTransaction() .setSender("me") .setRecipient("you") .setAmount(100)); assertThat(processor.getAccountBalance("me")).isEqualTo(50); assertThat(processor.getAccountBalance("you")).isEqualTo(120); }
  22. 올바른 예 @Test public void shouldNotPerformInvalidTransactions() { processor.setAccountBalance("me", 50); processor.setAccountBalance("you",

    20); processor.processTransaction(newTransaction() .setSender("me") .setRecipient("you") .setAmount(100)); assertThat(processor.getAccountBalance("me")).isEqualTo(50); assertThat(processor.getAccountBalance("you")).isEqualTo(20); }
  23. Idiom 2. Test State, Not Interactions 잘못된 예 @Test public

    void shouldWriteToDatabase() { accounts.createUser("foobar"); verify(database).put("foobar"); }
  24. Idiom 3. Make Your Tests Complete and Concise 잘못된 예

    @Test public void shouldPerformAddition() { Calculator calculator = new Calculator(new RoundingStrategy(), "unused", ENABLE_COSINE_FEATURE, 0.01, calculusEngine, false); int result = calculator.calculate(newTestCalculation()); assertThat(result).isEqualTo(5); // Where did this number come from? }
  25. Idiom 3. Make Your Tests Complete and Concise • 원칙:

    테스트 케이스의 본문은, ◦ 테스트를 이해하기 위한 모든 정보를 담고 있어야 함 ◦ 테스트와 상관없거나 이해를 방해하는 정보는 최대한 감춰야 함 @Test public void shouldPerformAddition() { Calculator calculator = newCalculator(); int result = calculator.calculate(newCalculation(2, Operation.PLUS, 3)); assertThat(result).isEqualTo(5); }
  26. Idiom 4. Test Behaviors, Not Methods 테스트 대상 코드 public

    void displayTransactionResults(User user, Transaction transaction) { ui.showMessage("You bought a " + transaction.getItemName()); if (user.getBalance() < LOW_BALANCE_THRESHOLD) { ui.showMessage("Warning: your balance is low!"); } }
  27. 잘못된 예: 메소드 중심 테스트 @Test public void testDisplayTransactionResults() {

    transactionProcessor.displayTransactionResults( newUserWithBalance( LOW_BALANCE_THRESHOLD.plus(dollars(2))), new Transaction("Some Item", dollars(3))); assertThat(ui.getText()).contains("You bought a Some Item"); assertThat(ui.getText()).contains("your balance is low"); }
  28. 좋은 예: 행위 중심 테스트 @Test public void displayTransactionResults_showsItemName() {

    transactionProcessor.displayTransactionResults( new User(), new Transaction("Some Item")); assertThat(ui.getText()).contains("You bought a Some Item"); } @Test public void displayTransactionResults_showsLowBalanceWarning() { transactionProcessor.displayTransactionResults( newUserWithBalance( LOW_BALANCE_THRESHOLD.plus(dollars(2))), new Transaction("Some Item", dollars(3))); assertThat(ui.getText()).contains("your balance is low"); }
  29. Idiom 5. Structure tests to emphasize behaviors (좋은 예) @Test

    public void transferFundsShouldMoveMoneyBetweenAccounts() { // Given two accounts with initial balances of $150 and $20 Account account1 = newAccountWithBalance(usd(150)); Account account2 = newAccountWithBalance(usd(20)); // When transferring $100 from the first to the second account bank.transferFunds(account1, account2, usd(100)); // Then the new account balances should reflect the transfer assertThat(account1.getBalance()).isEqualTo(usd(50)); assertThat(account2.getBalance()).isEqualTo(usd(120)); }
  30. 또 다른 좋은 예 @Test public void shouldTimeOutConnections() { //

    Given two users User user1 = newUser(); User user2 = newUser(); // And an empty connection pool with a 10-minute timeout Pool pool = newPool(Duration.minutes(10)); // When connecting both users to the pool pool.connect(user1); pool.connect(user2); // Then the pool should have two connections assertThat(pool.getConnections()).hasSize(2); // When waiting for 20 minutes clock.advance(Duration.minutes(20)); // Then the pool should have no connections assertThat(pool.getConnections()).isEmpty(); // And each user should be disconnected assertThat(user1.isConnected()).isFalse(); assertThat(user2.isConnected()).isFalse();
  31. Idiom 6. Write Clear Failure Messages • 나쁜 예 ◦

    Test failed: account is closed • 좋은 예 ◦ Expected an account in state CLOSED, but got account: {name: "my-account", state: "OPEN"}
  32. Idiom 7. Don’t Put Logic in Tests 나쁜 예 @Test

    public void shouldNavigateToAlbumsPage() { String baseUrl = "http://photos.google.com/"; Navigator nav = new Navigator(baseUrl); nav.goToAlbumPage(); assertThat(nav.getCurrentUrl()).isEqualTo(baseUrl + "/albums"); }
  33. 좋은 예 @Test public void shouldNavigateToPhotosPage() { Navigator nav =

    new Navigator("http://photos.google.com/"); nav.goToPhotosPage(); assertThat(nav.getCurrentUrl())) .isEqualTo("http://photos.google.com//albums"); // Oops! }
  34. Idiom 8. DAMP, Not DRY • DAMP: Descriptive And Meaningful

    Phrases • DRY: Don't Repeat Yourself
  35. 나쁜 예 @Test public void shouldAllowMultipleUsers() { List<User> users =

    createUsers(false, false); Forum forum = createForumAndRegisterUsers(users); validateForumAndUsers(forum, users); } @Test public void shouldNotAllowBannedUsers() { List<User> users = createUsers(true); Forum forum = createForumAndRegisterUsers(users); validateForumAndUsers(forum, users); } // Lots more tests... private static List<User> createUsers(boolean... banned) { // ... } // ...
  36. 좋은 예 @Test public void shouldAllowMultipleUsers() { User user1 =

    newUser().setState(State.NORMAL).build(); User user2 = newUser().setState(State.NORMAL).build(); Forum forum = new Forum(); forum.register(user1); forum.register(user2); assertThat(forum.hasRegisteredUser(user1)).isTrue(); assertThat(forum.hasRegisteredUser(user2)).isTrue(); }
  37. 좋은 예 2 @Test public void shouldNotRegisterBannedUsers() { User user

    = newUser().setState(State.BANNED).build(); Forum forum = new Forum(); try { forum.register(user); } catch(BannedUserException ignored) {} assertThat(forum.hasRegisteredUser(user)).isFalse(); }
  38. Idiom 9. No Shared Value 나쁜 예 private static final

    Account ACCOUNT_1 = Account.newBuilder() .setState(AccountState.OPEN).setBalance(50).build(); private static final Account ACCOUNT_2 = Account.newBuilder() .setState(AccountState.CLOSED).setBalance(0).build(); private static final Item ITEM = Item.newBuilder() .setName("Cheeseburger").setPrice(100).build(); // Hundreds of lines of other tests... @Test public void canBuyItem_returnsFalseForClosedAccounts() { assertThat(store.canBuyItem(ITEM, ACCOUNT_1)).isFalse(); } // ...
  39. 좋은 예 private static Contact.Builder newContact() { return Contact.newBuilder() .setFirstName("Grace")

    .setLastName("Hopper") .setPhoneNumber("555-123-4567"); } @Test public void fullNameShouldCombineFirstAndLastNames() { Contact contact = newContact() .setFirstName("Ada") .setLastName("Lovelace") .build(); assertThat(contact.getFullName()).isEqualTo("Ada Lovelace"); }
  40. Idiom 10. Shared Setup 나쁜 예 private NameService nameService; private

    UserStore userStore; @Before public void setUp() { nameService = new NameService(); nameService.set("user1", "Donald Knuth"); userStore = new UserStore(nameService); } // [... hundreds of lines of tests ...] @Test public void shouldReturnNameFromService() { UserDetails user = userStore.get("user1"); assertThat(user.getName()).isEqualTo("Donald Knuth"); }
  41. 좋은 예 private NameService nameService; private UserStore userStore; @Before public

    void setUp() { nameService = new NameService(); nameService.set("user1", "Donald Knuth"); userStore = new UserStore(nameService); } @Test public void shouldReturnNameFromService() { nameService.set("user1", "Margaret Hamilton"); UserDetails user = userStore.get("user1"); assertThat(user.getName()).isEqualTo("Margaret Hamilton"); }
  42. 좋은 helper의 예 private void assertUserHasAccessToAccount(User user, Account account) {

    for (long userId : account.getUsersWithAccess()) { if (user.getId() == userId) { return; } } fail(user.getName() + " cannot access " + account.getName()); }
  43. 외부 의존은 어떻게 처리할 것인가? • TDD by Example 입문

    이후 최대 난관 • 원칙은 helpfulness! - 테스트가 실패했다면 실제로 시스템에 문제가 있는 것임 and vice versa • Real code > Fake > Mock/Spy/Stub ◦ Real implementation - prefer realism over isolation ◦ Fake - fakes should be tested ◦ Mock - avoid overspecification @DoNotMock("Use SimpleQuery.create() instead of mocking.") public abstract class Query { public abstract String getQueryValue(); }
  44. 여기까지 모든 테스팅 관련 내용은 여기에서.. • Google Testing Blog

    ◦ https://testing.googleblog.com/ • TotT(Testing on the Toilet) ◦ https://testing.googleblog.com/search/label/TotT