Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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

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

Sa-ryong Kang

November 19, 2020
Tweet

More Decks by Sa-ryong Kang

Other Decks in Programming

Transcript

  1. Saryong Kang
    Senior Developer Relations Engineer @ Google Japan
    Google의 개발문화와 프로세스(1)

    View Slide

  2. Disclaimer
    ● 기본적으로 공개된 내용에 기초하고 있지만, 해석은 어디까지나 개인 의견입니다.
    ● Don't try this at home: 아무리 성공 사례라고 해도 도입의 이유는,
    ○ "구글이 한다니까" ‍♂
    ○ "나 or 우리 조직에 필요하고 AND 적합하니까" ‍♂

    View Slide

  3. I. 서론

    View Slide

  4. 왜 문화를 알아야 하는가?
    ● 사내 문화?
    ○ 참고: Netflix Culture: Freedom & Responsibility
    ○ “회사가 실제로 어디에 가치를 두고 있는가를 보려면, 누가 보상을 받고, 승진하고,
    해고 당하는가 를 보면 된다”
    ● 큰 그림에서 회사를 볼 수 있게 됨
    ○ 회사의 건강함을 판단할 수 있는 중요한 잣대 중 하나
    ○ 문화, 비즈니스 모델, 실적

    View Slide

  5. 개발 프로세스의 세부 사항: 내가 개선 할 수 있는 일
    ● Testing
    ● Code review
    ● Documentation
    ● Version control
    ● Estimate & analysis
    ● CI / CD
    ● Dependency management
    ● Etc.

    View Slide

  6. II. Google의 문화와
    Scale & Efficiency

    View Slide

  7. 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

    View Slide

  8. Scalability
    ● Scalable: sublinear scaling with regard to human interaction
    ● 개발에서뿐만 아니라 모든 정책 결정에 영향을 미치는 요소
    ● 만약 팀이 10배가 된다면, 조직의 성과물도 10배로 증가할 수 있도록, 작업을 최대한
    최적화 / 자동화 할 수 있는가?
    ○ No 라면 뭔가 잘못된 것

    View Slide

  9. Eg 1. Code Interview
    ● Time & space complexity - O(1), O(n), O(n log n)

    View Slide

  10. Eg 2. Deprecation
    ● 전통적인 방법: "x월 y일에 기존 컴포넌트를 지울 예정이니 그전에 새로운 컴포넌트로
    변경하시오"
    ○ Mandating doesn't scale.
    ○ Heroic don't scale!
    ● Churn Rule(Since 2012): Expertise scale!
    ○ 변경을 만들어 낸 조직이 변경을 위한 best practice 등을 만들어서 전파.
    변경사항이 매우 복잡한 경우, DevOps 조직에서 요청해서 내부 컨설팅 진행

    View Slide

  11. 성공적인 조직이라면,
    반복되는 모든 것은 sub-linear한 자원
    (특히 인적 자원)만을 소비해야 한다

    View Slide

  12. Eg 3. Configuration management: weekly merge
    meeting
    ● Branch 기반 코드 관리(ie. git)의 장점과 문제점

    View Slide

  13. Solution: no merge meeting
    ● Piper: branch를 (거의) 사용하지 않는 single repository(monorepo) system
    ○ 개발용 브랜치가 오래 open되어 있을 수 없음
    ○ 커밋할 브랜치를 선택할 수 없음
    ○ 어떤 버전에 의존하는가를 선택할 수 없음
    ● 참고 문헌: Advantages of trunk based development (한글 | 영문)

    View Slide

  14. 그 외에도..
    ● 비용 지불
    ● 효율적으로 일하는 방식
    ○ 아무도 가르쳐주지 않았으나 몸에 베어있는 agility
    ○ 영웅주의 / 무리해서 일하기 배격

    View Slide

  15. III. Google의 Testing

    View Slide

  16. Test-centric Culture
    ● Google은 극도로 테스트 중심의 개발 원칙을 갖고 있음
    ○ 여기서는 테스트는 engineer-driven automated testing을 의미함
    ○ “QA doesn't scale”

    View Slide

  17. 그 전에, Google의 개발 프로세스는..
    1. Issue Tracker에 작업 등록
    2. 코드 작성 (test 코드 포함)
    3. Pull Request 신청
    4. 자동 검사 (test coverage 미달, 코드 스타일, 잠재적 취약성 등), 테스트 실행
    5. Code Review
    6. Merge!

    View Slide

  18. 이런 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]++;
    }
    }

    View Slide

  19. 버그 발생시 프로세스는..
    1. Issue Tracker에 버그 보고
    2. 해당 Pulll Request를 통째로 roll back
    3. 보고된 버그를 재현할 수 있는 테스트 코드 작성 (물론 테스트 케이스는 fail)
    4. 테스트 코드를 pass 할 수 있는 코드 작성
    5. Pull Request 신청 (이하 동일)

    View Slide

  20. 왜 테스트 코드를 짜는가?
    ● 효율적으로 버그를 잡아내기 위해
    ● 코드를 쉽게 변경할 수 있게 하기 위해

    View Slide

  21. 테스트로 얻을 수 있는 이점
    ● 코딩 생산성 증대: 확신을 갖고 시스템을 수정할 수 있음
    ● 문서로서의 테스트 코드. 테스트 케이스만 보면 특정 시스템의 기능과 의도, 올바른
    사용법을 단번에 파악 가능
    ● 협업을 촉진: code owner가 아니더라도 코드를 수정할 수 있음
    ● Simpler reviews
    ● modular한 설계에 도움: single responsibility

    View Slide

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

    View Slide

  23. Google의 테스트 정책
    ● 높은 Test Coverage*: 여기서 테스트 커버리지는 오직 small-sized test로만 측정함
    ● Beyoncé Rule: “If you liked it, then you shoulda put a test on it.”
    * 프로덕트 별로 차이가 있음

    View Slide

  24. 만약 작은 팀 혹은 혼자 코딩하고 있다면? *
    ● PR을 날리기 전에 이슈(aka 태스크, 스토리) 하나 당 적어도 두 개의 E2E 테스트
    (정상/실패), 그리고 그것이 2x 정도의 소규모 테스트 작성
    ● 이슈를 중심으로 high level -> low level로 내려가면서 테스트 작성
    ● 버그 발생 시, 재현을 위한 테스트부터 작성 후 처리하기
    * 개인적인 의견입니다

    View Slide

  25. IV. Google의 테스트 코드 작성 원칙

    View Slide

  26. Unchanging Test
    ● Brittle test: 테스트 유지 보수 과정에서 만나는 가장 큰 문제
    ● 테스트 불변의 원칙: 한 번 짠 코드는 테스트는 아래의 이유로 변경되지 않는다
    ○ Pure refactorings
    ○ New features
    ○ Bug fixes
    ● 단, behavior changes의 경우에만 테스트를 수정

    View Slide

  27. 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);
    }

    View Slide

  28. 잘못된 예
    @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");
    }

    View Slide

  29. 올바른 예
    @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);
    }

    View Slide

  30. 올바른 예
    @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);
    }

    View Slide

  31. Idiom 2. Test State, Not Interactions
    잘못된 예
    @Test
    public void shouldWriteToDatabase() {
    accounts.createUser("foobar");
    verify(database).put("foobar");
    }

    View Slide

  32. 좋은 예
    @Test
    public void shouldCreateUsers() {
    accounts.createUser("foobar");
    assertThat(accounts.getUser("foobar")).isNotNull();
    }

    View Slide

  33. 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?
    }

    View Slide

  34. 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);
    }

    View Slide

  35. 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!");
    }
    }

    View Slide

  36. 잘못된 예: 메소드 중심 테스트
    @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");
    }

    View Slide

  37. 좋은 예: 행위 중심 테스트
    @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");
    }

    View Slide

  38. 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));
    }

    View Slide

  39. 또 다른 좋은 예
    @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();

    View Slide

  40. 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"}

    View Slide

  41. 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");
    }

    View Slide

  42. 좋은 예
    @Test
    public void shouldNavigateToPhotosPage() {
    Navigator nav = new Navigator("http://photos.google.com/");
    nav.goToPhotosPage();
    assertThat(nav.getCurrentUrl()))
    .isEqualTo("http://photos.google.com//albums"); // Oops!
    }

    View Slide

  43. Idiom 8. DAMP, Not DRY
    ● DAMP: Descriptive And Meaningful Phrases
    ● DRY: Don't Repeat Yourself

    View Slide

  44. 나쁜 예
    @Test
    public void shouldAllowMultipleUsers() {
    List users = createUsers(false, false);
    Forum forum = createForumAndRegisterUsers(users);
    validateForumAndUsers(forum, users);
    }
    @Test
    public void shouldNotAllowBannedUsers() {
    List users = createUsers(true);
    Forum forum = createForumAndRegisterUsers(users);
    validateForumAndUsers(forum, users);
    }
    // Lots more tests...
    private static List createUsers(boolean... banned) {
    // ...
    }
    // ...

    View Slide

  45. 좋은 예
    @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();
    }

    View Slide

  46. 좋은 예 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();
    }

    View Slide

  47. 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();
    }
    // ...

    View Slide

  48. 좋은 예
    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");
    }

    View Slide

  49. 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");
    }

    View Slide

  50. 좋은 예
    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");
    }

    View Slide

  51. 좋은 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());
    }

    View Slide

  52. 외부 의존은 어떻게 처리할 것인가?
    ● 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();
    }

    View Slide

  53. 여기까지 모든 테스팅 관련 내용은 여기에서..
    ● Google Testing Blog
    ○ https://testing.googleblog.com/
    ● TotT(Testing on the Toilet)
    ○ https://testing.googleblog.com/search/label/TotT

    View Slide

  54. Thank You
    https://speakerdeck.com/saryong

    View Slide