Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

초보개발자의 TDD 체험기

2,311 views

Published on

[20190418 튜터링 세미나]

코드의 품질이 무엇인지 몰랐던 초보개발자가
TDD와 의식적인 리팩토링을 통해 설계를 개선한 경험

Published in: Software
  • Be the first to comment

초보개발자의 TDD 체험기

  1. 1. 초보 개발자의 TDD 체험기
  2. 2. 안녕하세요.
  3. 3. 비전공자(경영학전공) 현재 취준생(백수) 김세훈입니다..!
  4. 4. 저는 튜터링에서 QA인턴을 하다 개발이 하고싶어 도망쳤스ㄴ다..(농담😋)
  5. 5. 개발을 잘 하고 싶어 방황하던 저는 개발을
  6. 6. 스승님을 만나.
  7. 7. 객체 지향적 설계, TDD, Refactoring을 통한 클린코드 작성법 좋은 프로그래머가 되기 위한 기본 소양을 배울 수 있었습니다.
  8. 8. 스승님께서 강조하신 말씀이 있습니다. TDD와 의식적인 리팩토링을 반복하면, 설계를 개선하고 '품질 높은 코드'를 만들 수 있다.
  9. 9. 저는 두 가지 이유로 저 말씀을 완전히 이해하지 못했습니다.
  10. 10. 품질 좋은 코드란? TDD를 사용하면 무엇이 개선되는지?
  11. 11. 품질 좋은 코드란 무엇인가?
  12. 12. 우선 저는 코드의 품질에 대해 고민해 본적이 없었습니다. (애초에 코드에 품질 같은게 있다고 생각도 안 했었습니다..)
  13. 13. “코드는 구현하고 싶은 기능만 작동되게 하면 되는 거 아닌가?”
  14. 14. 무지함을 해소하고자 정리해본 좋은 코드의 조건 (클린코드는 아직 다 읽지 못했습니다..ㅜ) 1. 잘 동작한다. 2. 읽기(이해하기) 쉽다. 3. 중복이 없다. 3. 확장이 용이하다.
  15. 15. 잘 동작한다. 개발의 목적은 잘 작동하는 소프트웨어를 만드는 것 (기본적으로 작동하지 않으면 무슨 소용인가?)
  16. 16. 이해하기 쉽다. 가독성 나쁜 코드는 다른 사람(혹은 본인)이 이해하기 힘들고 파악하는데 시간을 쏟게 한다. 이로 인해 추가적인 커뮤니케이션 비용이 발생한다.
  17. 17. 중복이 없다. 코드에 중복되는 내용이 많으면 수정이 힘들다. 때문에 중복된 코드는 분리하여 재사용 가능하게 해야 한다. ‘실용주의 프로그래머’ 저자 데이브 토마스 “중복은 해악이다”
  18. 18. 확장이 용이하다. 외부의 요구사항은 변하고 기능은 추가되기 마련. 쉽게 기능을 추가,확장 하기 위해서 코드간 의존성을 낮추어야 한다. 객체지향 5원칙 중 OCP
  19. 19. 그렇다면 TDD를 통해 어떻게 코드의 품질을 개선할 수 있을까?
  20. 20. TDD(Test Driven Development)는 1. 실제 동작할 프로덕션 코드를 만들기 전에 테스트 코드를 작성하고, 2. 테스트 케이스를 통과할 만큼의 프로덕션 코드를 만들어 3. 리팩토링하는 과정을 반복하는 개발 방식이다. TDD Cycle
  21. 21. 그래 테스트는 좋은거지.. 근데 이거 하면 뭐가 달라지나..? 단위테스트랑 뭐가 다른데.. 테스트 만드는거 귀찮..
  22. 22. 역시 원리만 알아서는 알 수 없죠. ㅋ 저는 레거시 코드를 TDD를 활용해 리팩토링해보기로 결정했습니다.
  23. 23. 프로젝트 주제 두 플레이어가 서로 자신의 카드를 모르고 상대방의 카드만 아는 상태에서, 지난 카드들을 통해 유추해 베팅하는 포커게임
  24. 24. 리팩토링 전 Before
  25. 25. 자바를 공부하며 만들었던 토이 프로젝트 Legacy Code AutoGamer Gamer 클래스를 상속받아, 사용자의 카드와 본인의 카드를 기억하여 자동으로 베팅하는 역할 Dealer 턴을 제어하는 로직을 가진 클래스. 턴의 승패를 판단하고, 게이머에게 칩을 분배한다. 매 턴 마다 Dealer의 상태는 초기화된다. Gamer 칩과 카드라는 상태를 가지며, 베팅이라는 행위를 할 수 있다. 사용자의 입력을 받아 베팅한다. IndianPoker 게임을 실행하고 종료조건에 따라 게임의 승패를 판단하는 역할. Dealer와 Gamer 객체에 의존성을 가짐 클래스 구성
  26. 26. 실제로 코드를 보기 전까진 “나름 객체지향적으로 설계한 거 같은데?” 라고 생각했습니다.
  27. 27. Gamer 클래스의 betting() 메소드
  28. 28. ......?
  29. 29. 문제가 많으니 하나씩 살펴봅시다.
  30. 30. 한 메소드가 너무 많은 일을 하고 있다. 일단 Gamer라는 도메인의 핵심 로직인 betting()이 직접 입출력, 유효성 검사를 하고 있다. (기능간 의존성 발생) 게다가 배팅의 유효성 검사 부분에는 동일한 기능을 하는 코드가 반복되어 중복이 발생하고 있다.
  31. 31. 많은 분기처리로 코드의 복잡성이 높아짐 모든 경우를 한곳에서 판단 하려해 분기처리가 많아졌고, 이로 인해 코드의 흐름이 복잡해졌다. 본인조차 이해하기 힘든 가독성 낮은 코드가 된 것이다..
  32. 32. 가장 큰 문제는 모든 클래스의 모든 메소드가 이런 식이라는 것
  33. 33. 일단 &%$같은 코드를 개선하기 전 두 가지 목표를 세웠다.
  34. 34. TDD, Pair Programming 방식으로 개발할 것 콘솔 기반으로 개발하되, 웹 프로젝트로 확장할 것을 고려하여 개발 할 것
  35. 35. TDD, Pair Programming 방식으로 개발할 것 개발 방식에 대한 목표
  36. 36. 페어 프로그래밍 한 컴퓨터로 파트너와 함께 번갈아 가며 프로그래밍하는 방식
  37. 37. 일정, 의견 조율 같은 커뮤니케이션 비용이 많이 들고 둘이서 한 작업을 같이하기 때문에 효율성은 떨어지지만
  38. 38. 파트너의 실시간 리뷰를 통해 익숙하지 않은 TDD 방식을 의식적으로 유지하고, 의견 조율 과정을 통해 좀 더 좋은 코드를 만드는데 도움을 준다.
  39. 39. 콘솔 기반으로 개발하되, 웹 프로젝트로 확장할 것을 고려하여 개발 할 것 설계에 대한 목표
  40. 40. 사실 TDD 방식 자체는 매우 Simple
  41. 41. 가장 중요한 부분은 어떤 설계목표를 가지고 “리팩토링“ 하냐는 것
  42. 42. Wine을 숙성시키는 ripening()이라는 로직을 만들어보자
  43. 43. 일단 기본 테스트 케이스
  44. 44. 절차적 행위(숙성)와 상태(기간)는 분리되어 있어야 한다.
  45. 45. 절차적 Wine 클래스는 age라는 상태를 가진다. (일종의 구조체 역할)
  46. 46. 절차적 ripening() 메소드는 실제 숙성시키는 행위를 담당하고 있다. 메소드는 wine 객체의 현재 age를 가져와 숙성 시킬 기간을 더 해 다시 wine 객체에 값을 저장하는 처리를 순차적으로 실행한다.
  47. 47. 함수형 함수를 실행하기 전과 후 매개변수로 쓰이는 값은 변하면 안된다. (Immutable Object로 만들어야 한다)
  48. 48. 함수형 Wine 클래스의 age 필드를 외부에서 직접 접근하지 못하도록 private 접근제어자를 주었고, 외부에서는 getAge() 메소드를 통해서 조회만 할 수 있게 하였다.
  49. 49. 함수형 함수형 인터페이스 Ripening을 구현하여 apply()를 실행할 때 인자로 넣은 wine의 age와 숙성기간을 더해 새로운 와인을 생성한다. (매개변수로 들어간 와인과 숙성된 와인은 전혀 다른 객체)
  50. 50. 객체지향적 객체는 상태(기간)와 행위(숙성)를 모두 갖는다. (객체 자신의 상태는 자신이 직접 변화시켜야 한다.)
  51. 51. 객체지향적 Wine 클래스의 age는 외부에서 접근하지 못하고, ripening() 행위를 통해 변화 시킬 수 있다.
  52. 52. 객체지향적
  53. 53. 이처럼 어떤 설계 목표를 가지고 리팩토링 하는지에 따라 TDD를 사용하더라도 결과물이 달라집니다.
  54. 54. CONSOLE WEB Our Goal To
  55. 55. SpringMVC는 MVC 패턴을 기반으로 설계된 프레임워크 가능하다면 핵심 로직을 그대로 사용하고 싶었기에, 객체지향적으로 설계하고 MVC 패턴을 적용하여 개발하기로 결정
  56. 56. 페어프로그래밍 1주일에 4시간씩 2회 콘솔 프로젝트 개발기간 약 2개월 + 웹 프로젝트는 혼자 개발할 때 1개월 정도 걸림
  57. 57. TDD를 하며 느낀 것
  58. 58. 처음부터 큰 문제를 한번에 해결하려 하면 어렵게 다가오고 중간에 포기하고 싶어진다. 큰 문제 1. 큰 문제를 작은 단위로 쪼갤 수 있다.
  59. 59. 테스트 케이스를 만들다 보면 큰 문제를 해결하기 위한 작은 문제, 그리고 그 작은 문제를 해결하기 위한 더 작은 문제를 찾게 된다. 이렇게 문제를 쪼개다 보면 자신이 감당할 수 있는 범위의 문제를 알게 된다. 큰 문제 중간 중간 작은 1. 큰 문제를 작은 단위로 쪼갤 수 있다. 작은 작은 작은
  60. 60. 1. 큰 문제를 작은 단위로 쪼갤 수 있다. 일단 생각나는 문제들을 적고 해결하기 위한 테스트 케이스를 만들었다. 테스트 케이스는 개발을 시작할 포인트가 되어주었고, 시작점에서 점진적으로 필요한 기능에 대한 테스트 케이스를 만들며 문제의 단위를 쪼갤 수 있었다.
  61. 61. 2. 개발의 흐름을 쉽게 파악할 수 있다. 아무리 개발을 좋아해도 365일 24시간 같은 것만 개발할 수 없는 법 개발의 흐름
  62. 62. 2. 개발의 흐름을 쉽게 파악할 수 있다. 자릴 비울땐 실패하는 테스트 케이스를 만들어 놓아 어디까지 개발했는지 파악할 수 있었다. Dealer가 Turn의 승패를 판단하는 테스트
  63. 63. 2. 개발의 흐름을 쉽게 파악할 수 있다. 테스트 케이스의 이름은 어떤 것이든 상관없다. 때문에 자신이 파악하기 쉽게 만드는게 좋다. 또 테스트 케이스에 작성된 요소(매개변수, 리턴값) 들이 이 메소드가 어떤 일을 하는 지 알려주는 설명서가 되어준다. BettingState Test
  64. 64. 3. 변경에 자신 있게 대처할 수 있다. 좋은 품질의 코드에는 확장이 용의하다는 조건이 있다. 이 조건은 각 기능간 의존성을 낮추어 해결할 수 있다. A는 B가 없으면 아무것도 못한다..
  65. 65. 3. 변경에 자신 있게 대처할 수 있다. 테스트 케이스를 만들면서 자연스럽게 의존관계에 대한 고민을 하고 테스트를 쉽게할 수 있도록 의존성을 낮추는 설계를 하게 된다. Turn 클래스와 의존관계가 있는 클래스들
  66. 66. 3. 변경에 자신 있게 대처할 수 있다. 고민은 결국 변화하는 부분(Player)와 변화하지 않는 부분(Turn의 행위)를 분리 함으로서 해결할 수 있다. TurnTest의 일부
  67. 67. 3. 변경에 자신 있게 대처할 수 있다.
  68. 68. 3. 변경에 자신 있게 대처할 수 있다. 테스트 케이스를 만들어 통과시켜 놓으면, 그 테스트 케이스 자체가 입력타입과 출력타입의 표준이 된다. Input - output
  69. 69. 4. 품질이 보장된 코드를 갖게 된다. 테스트를 통해 이미 검증된 도메인 로직을 갖게 된다. CONSOLE WEB
  70. 70. 5. 테스트를 통과할 때 마다 느끼는 성취감 테스트 케이스를 모두 통과시키면 나름 짜릿
  71. 71. TDD를 활용해 리팩토링한 코드 After
  72. 72. TDD를 활용해 리팩토링한 코드 Refactored 패키지 구성 controller 적절한 행위를 할 수 있도록 입력과 출력을 분기 처리하는 컨트롤러들이 있는 패키지 domain 게임의 핵심로직을 담당하는 클래스들이 모여있는 패키지 dto 도메인 로직의 처리결과를 뷰에서 보여질 수 있도록 가공한 클래스들이 모여있는 패키지 exception 직접 정의한 예외 클래스들이 모여있는 패키지 view 입력과 출력을 담당하는 클래스들이 모여있는 패키지 vo 직접 지정한 상수형 클래스들이 모여있는 패키지 support 프로그램의 실행에 직접적인 연관은 없지만 도움을 줄 수 있는 클래스들이 모여있는 패키지
  73. 73. 리팩토링한 Player 클래스의 betting() 메소드 (정확히는 Player 인터페이스를 구현한 추상클래스 AbstractPlayer의 메소드)
  74. 74. ......?
  75. 75. 일단 리팩토링 전과 달리 메소드 자체의 길이가 매우 짧아졌다.
  76. 76. 또한 입출력 로직을 분리하여 입출력 로직에서 직접 입력 값에 대한 유효성을 검사할 수 있게 되었다. (중복제거)
  77. 77. 여기서 실제 베팅이라는 행위는 도메인 객체 중 하나인 BettingState라는 클래스가 담당 BettingState로 처리를 분담시켜 분기문이 없어져 가독성이 좋아졌다. State Pattern 적용
  78. 78. TDD만을 이용해 얻은 성과일까?
  79. 79. “단위테스트”와 “TDD”는 테스트 케이스를 만드는 순서만 다르지, 프로덕션 코드를 검증하기 위한 테스트 케이스가 있다는 것은 같다. TDD의 다른 점은 리팩토링이라는 설계 개선의 과정이 있다는 것
  80. 80. 결국 중요한 것은 의식적인 리팩토링으로 더 나은 설계가 무엇인지 고민하는 과정
  81. 81. TDD란 프로그래밍 의사결정과 피드백 사이의 간극을 의식하고 이를 제어하는 기술이다. “Test Driven Development by Example” Kent Beck
  82. 82. 감사합니다 Github : https://github.com/Sehun-Kim Blog : https://sehun-kim.github.io/sehun/blog/

×